Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ yarn.lock
.claude

CLAUDE.md
.omc

test-results
playwright-report
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"@eslint/compat": "^1.4.1",
"@eslint/js": "9.39.2",
"@playwright/test": "^1.58.2",
"@rspack/cli": "^1.7.6",
"@rspack/core": "^1.6.8",
"@rspack/cli": "^1.7.11",
"@rspack/core": "^1.7.11",
"@swc/helpers": "^0.5.17",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
Expand Down Expand Up @@ -107,7 +107,7 @@
"unocss": "66.5.4",
"vitest": "^4.0.18"
},
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.33.0",
"sideEffects": [
"**/*.css",
"**/*.scss",
Expand Down
1,453 changes: 430 additions & 1,023 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minimumReleaseAge: 10080
19 changes: 19 additions & 0 deletions src/app/service/agent/core/optimize_prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const OPTIMIZE_PROMPT_SYSTEM = `Act as an expert prompt engineer operating inside the AI Agent feature of ScriptCat (a userscript manager). Your sole function is to transform whatever the user types into a clear, actionable LLM prompt for AI Agent. Transform the input into an actionable LLM prompt covering the relevant dimensions among: role, task, context, constraints, format, and tone. Preserve intent exactly. Inject context ONLY to resolve ambiguity. Do not embellish or over-engineer. Apply minimal edits to already precise inputs. Match input language. Output ONLY the raw prompt text wrapped with <optimized> tag with no preambles, commentary, or markdown fences.`;

export function buildOptimizeUserPrompt(userInput: string): string {
return `"""
${userInput}
"""`;
}

/** 从 LLM 响应中提取 <optimized> 标签内容,fallback 到去除常见包裹后的全文 */
export function extractOptimized(content: string): string {
const match = content.match(/<optimized>\s*([\s\S]*?)\s*<\/optimized>/i);
if (match) return match[1].trim();

// Fallback: 清理常见包裹
let text = content.trim();
text = text.replace(/^```[a-z]*\n?/i, "").replace(/\n?```$/, "");
text = text.replace(/<\/?optimized>/gi, "");
return text.trim();
}
1 change: 1 addition & 0 deletions src/app/service/agent/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export type ChatStreamEvent =
}>;
}
| { type: "compact_done"; summary: string; originalCount: number }
| { type: "optimize_done"; optimized: string }
| {
type: "sync";
streamingMessage?: { content: string; thinking?: string; toolCalls: ToolCall[] };
Expand Down
16 changes: 16 additions & 0 deletions src/app/service/agent/service_worker/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export class AgentService {
this.group.on("conversation", this.handleConversation.bind(this));
// 流式聊天(UI 和 Sandbox 共用)
this.group.on("conversationChat", this.handleConversationChat.bind(this));
// Prompt 优化(ephemeral 单次调用,复用 conversationChat 路径)
this.group.on("optimizePrompt", this.handleOptimizePrompt.bind(this));
// 附加到后台运行中的会话
this.group.on("attachToConversation", this.handleAttachToConversation.bind(this));
// 获取正在运行的会话 ID 列表
Expand Down Expand Up @@ -394,6 +396,20 @@ export class AgentService {
return this.chatService.handleConversationChat(params, sender);
}

// Prompt 优化入口:无状态 ephemeral 调用,复用 ChatService.handleConversationChat
private async handleOptimizePrompt(params: { modelId: string; input: string }, sender: IGetSender) {
return this.chatService.handleConversationChat(
{
conversationId: `__optimize_${Date.now()}`,
message: "",
modelId: params.modelId,
optimizePrompt: true,
optimizeInput: params.input,
},
sender
);
}

// 对内容做摘要/提取(供 tab 工具使用)
// 优先使用摘要模型,fallback 到默认模型
private async summarizeContent(content: string, prompt: string): Promise<string> {
Expand Down
54 changes: 54 additions & 0 deletions src/app/service/agent/service_worker/chat_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {
buildCompactUserPrompt,
extractSummary,
} from "@App/app/service/agent/core/compact_prompt";
import {
OPTIMIZE_PROMPT_SYSTEM,
buildOptimizeUserPrompt,
extractOptimized,
} from "@App/app/service/agent/core/optimize_prompt";
import { createTaskTools } from "@App/app/service/agent/core/tools/task_tools";
import { createAskUserTool } from "@App/app/service/agent/core/tools/ask_user";
import { createSubAgentTool } from "@App/app/service/agent/core/tools/sub_agent";
Expand Down Expand Up @@ -72,6 +77,9 @@ type ConversationChatParams = {
// compact 模式
compact?: boolean;
compactInstruction?: string;
// optimize prompt 模式
optimizePrompt?: boolean;
optimizeInput?: string;
// 后台运行模式
background?: boolean;
};
Expand Down Expand Up @@ -258,6 +266,12 @@ export class ChatService {
return;
}

// optimizePrompt
if (params.optimizePrompt) {
await this.handleOptimizePromptChat(params, sendEvent, abortController);
return;
}

// 获取对话和模型
const conv = await this.getConversation(params.conversationId);
if (!conv) {
Expand Down Expand Up @@ -467,6 +481,46 @@ export class ChatService {
sendEvent({ type: "done", usage: result.usage });
}

/**
* Prompt 优化:直接调用 LLM,不走 tool loop,不持久化。
* 模仿 handleCompactChat 的调用方式。
*/
private async handleOptimizePromptChat(
params: ConversationChatParams,
sendEvent: (event: ChatStreamEvent) => void,
abortController: AbortController
): Promise<void> {
if (!params.optimizeInput?.trim()) {
sendEvent({ type: "error", message: "No input to optimize" });
return;
}

const model = await this.modelService.getModel(params.modelId);

const messages: ChatRequest["messages"] = [
{ role: "system", content: OPTIMIZE_PROMPT_SYSTEM },
{ role: "user", content: buildOptimizeUserPrompt(params.optimizeInput) },
];

// 吞掉 content_delta / thinking_delta,避免把带 <optimized> 标签的原始流送给 UI
const silentSendEvent = (event: ChatStreamEvent) => {
if (event.type === "content_delta" || event.type === "thinking_delta") return;
sendEvent(event);
};

const result = await this.llmDeps.callLLM(
model,
{ messages, cache: false },
silentSendEvent,
abortController.signal
);

const optimized = extractOptimized(result.content);

sendEvent({ type: "optimize_done", optimized } as any);
sendEvent({ type: "done", usage: result.usage });
}

/**
* 构建 session 级工具注册表。
* 每个 chat 请求独立一个 SessionToolRegistry(parent = 全局 toolRegistry),
Expand Down
98 changes: 98 additions & 0 deletions src/app/service/service_worker/resource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { initTestEnv } from "@Tests/utils";
import { ResourceService } from "./resource";
import { vi, describe, it, expect, beforeEach } from "vitest";
import type { Group } from "@Packages/message/server";
import type { IMessageQueue } from "@Packages/message/message_queue";

initTestEnv();

// mock fetch
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

// 创建文本 blob 和二进制 blob 的辅助函数
function textBlob(content: string, contentType = "text/plain") {
return new Blob([content], { type: contentType });
}

function binaryBlob(bytes: number[]) {
return new Blob([new Uint8Array(bytes)], { type: "application/octet-stream" });
}

function mockResponse(blob: Blob, status = 200, contentType?: string) {
return {
status,
blob: () => Promise.resolve(blob),
headers: new Headers(contentType ? { "content-type": contentType } : {}),
} as unknown as Response;
}

describe("ResourceService - loadByUrl", () => {
let service: ResourceService;

beforeEach(() => {
vi.clearAllMocks();
const mockGroup = {} as Group;
const mockMQ = {} as IMessageQueue;
service = new ResourceService(mockGroup, mockMQ);
// calculateHash 不影响核心逻辑,直接 mock
vi.spyOn(service, "calculateHash").mockResolvedValue({
md5: "mock-md5",
sha1: "",
sha256: "",
sha384: "",
sha512: "",
});
});

it("加载文本资源(require)时应设置 content", async () => {
const jsCode = "console.log('hello');";
mockFetch.mockResolvedValue(mockResponse(textBlob(jsCode), 200, "application/javascript; charset=utf-8"));

const res = await service.loadByUrl("https://example.com/lib.js", "require");

expect(res.url).toBe("https://example.com/lib.js");
expect(res.content).toBeTruthy();
expect(res.contentType).toBe("application/javascript");
expect(res.base64).toBeTruthy();
expect(res.type).toBe("require");
});

it("加载文本资源(resource)时应通过 blob.text() 设置 content", async () => {
const text = "plain text content";
mockFetch.mockResolvedValue(mockResponse(textBlob(text), 200, "text/plain"));

const res = await service.loadByUrl("https://example.com/data.txt", "resource");

expect(res.content).toBe(text);
expect(res.type).toBe("resource");
});

it("加载二进制资源时 content 应为空", async () => {
// 包含 null 字节的二进制数据,isText 会返回 false
const bytes = [0x89, 0x50, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00];
mockFetch.mockResolvedValue(mockResponse(binaryBlob(bytes), 200, "image/png"));

const res = await service.loadByUrl("https://example.com/img.png", "resource");

expect(res.content).toBe("");
expect(res.base64).toBeTruthy();
expect(res.contentType).toBe("image/png");
});

it("响应非200时应抛出异常", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob(""), 404));

await expect(service.loadByUrl("https://example.com/404", "require")).rejects.toThrow(
"resource response status not 200: 404"
);
});

it("没有 content-type 时应默认为 application/octet-stream", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob("data"), 200));

const res = await service.loadByUrl("https://example.com/noct", "resource");

expect(res.contentType).toBe("application/octet-stream");
});
});
3 changes: 1 addition & 2 deletions src/app/service/service_worker/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export class ResourceService {
throw new Error(`resource response status not 200: ${resp.status}`);
}
const data = await resp.blob();
const [hash, arrayBuffer, base64] = await Promise.all([
const [hash, uint8Array, base64] = await Promise.all([
this.calculateHash(data),
blobToUint8Array(data),
blobToBase64(data),
Expand All @@ -280,7 +280,6 @@ export class ResourceService {
type,
createtime: Date.now(),
};
const uint8Array = new Uint8Array(arrayBuffer);
if (isText(uint8Array)) {
if (type === "require" || type === "require-css") {
resource.content = await readBlobContent(data, contentType); // @require和@require-css 是会转换成代码运行的,可以进行解码
Expand Down
4 changes: 4 additions & 0 deletions src/locales/de-DE/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@
"agent_mcp_no_prompts": "Keine Prompts verfügbar",
"agent_mcp_loading": "Laden...",
"agent_mcp_parameters": "Parameter",
"agent_optimize_prompt": "Prompt-Optimierer",
"agent_optimizing_prompt": "Optimierung...",
"agent_optimize_prompt_success": "Prompt optimiert",
"agent_optimize_prompt_failed": "Optimierung fehlgeschlagen",
"agent_settings": "Einstellungen",
"agent_settings_title": "Agent-Einstellungen",
"agent_model_settings": "Modelleinstellungen",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,10 @@
"agent_mcp_no_prompts": "No prompts available",
"agent_mcp_loading": "Loading...",
"agent_mcp_parameters": "Parameters",
"agent_optimize_prompt": "Prompt Optimizer",
"agent_optimizing_prompt": "Optimizing...",
"agent_optimize_prompt_success": "Prompt optimized",
"agent_optimize_prompt_failed": "Optimization failed",
"agent_tasks": "Tasks",
"agent_tasks_title": "Scheduled Tasks",
"agent_tasks_create": "Create Task",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ja-JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@
"agent_mcp_no_prompts": "利用可能なプロンプトはありません",
"agent_mcp_loading": "読み込み中...",
"agent_mcp_parameters": "パラメータ",
"agent_optimize_prompt": "プロンプト最適化",
"agent_optimizing_prompt": "最適化中...",
"agent_optimize_prompt_success": "最適化完了",
"agent_optimize_prompt_failed": "最適化に失敗しました",
"agent_settings": "設定",
"agent_settings_title": "Agent 設定",
"agent_model_settings": "モデル設定",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ru-RU/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@
"agent_mcp_no_prompts": "Нет доступных промптов",
"agent_mcp_loading": "Загрузка...",
"agent_mcp_parameters": "Параметры",
"agent_optimize_prompt": "Оптимизатор промптов",
"agent_optimizing_prompt": "Оптимизация...",
"agent_optimize_prompt_success": "Промпт оптимизирован",
"agent_optimize_prompt_failed": "Ошибка оптимизации",
"agent_settings": "Настройки",
"agent_settings_title": "Настройки Agent",
"agent_model_settings": "Настройки модели",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/vi-VN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@
"agent_mcp_no_prompts": "Không có lời nhắc nào",
"agent_mcp_loading": "Đang tải...",
"agent_mcp_parameters": "Tham số",
"agent_optimize_prompt": "Trình tối ưu hóa câu lệnh",
"agent_optimizing_prompt": "Đang tối ưu hóa...",
"agent_optimize_prompt_success": "Đã tối ưu hóa",
"agent_optimize_prompt_failed": "Tối ưu hóa thất bại",
"agent_settings": "Cài đặt",
"agent_settings_title": "Cài đặt Agent",
"agent_model_settings": "Cài đặt mô hình",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,10 @@
"agent_mcp_no_prompts": "暂无可用提示词",
"agent_mcp_loading": "加载中...",
"agent_mcp_parameters": "参数",
"agent_optimize_prompt": "提示词优化器",
"agent_optimizing_prompt": "优化中...",
"agent_optimize_prompt_success": "优化完成",
"agent_optimize_prompt_failed": "优化失败",
"agent_tasks": "定时任务",
"agent_tasks_title": "定时任务管理",
"agent_tasks_create": "创建任务",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@
"agent_mcp_no_prompts": "暫無可用提示詞",
"agent_mcp_loading": "載入中...",
"agent_mcp_parameters": "參數",
"agent_optimize_prompt": "提示詞優化器",
"agent_optimizing_prompt": "優化中...",
"agent_optimize_prompt_success": "優化完成",
"agent_optimize_prompt_failed": "優化失敗",
"agent_settings": "設定",
"agent_settings_title": "Agent 設定",
"agent_model_settings": "模型設定",
Expand Down
Loading
Loading