Skip to content

Commit 116c5de

Browse files
committed
🐛 修复 Copilot Review 发现的问题
- SSE 解析器空行时无条件重置 currentEvent,防止残留污染 - OPFS 写入 Uint8Array 时精确截取字节段,修复切片视图 bug - execute_script 超时消息改为动态生成,匹配实际超时时间 - cleanupIfDone 回调中重新检查会话状态,防止误删已复用会话 - 模型能力检测函数移至 core/model_capabilities.ts,消除 core→UI 反向依赖 - web_search max_results 改用 optionalNumber 防止 NaN - task_tools subject/description 使用类型安全的参数校验
1 parent 4ffbc44 commit 116c5de

11 files changed

Lines changed: 96 additions & 81 deletions

File tree

src/app/service/agent/core/attachment_resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ChatRequest, AgentModelConfig } from "./types";
22
import { isContentBlocks } from "./content_utils";
3-
import { supportsVision } from "@App/pages/options/routes/AgentChat/model_utils";
3+
import { supportsVision } from "./model_capabilities";
44

55
/**
66
* 解析消息中 image+vision 的 attachmentId → base64 data URL
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { AgentModelConfig } from "./types";
2+
3+
// 通过模型 ID 字符串检测是否支持视觉输入
4+
export function supportsVisionByModelId(modelId: string): boolean {
5+
const m = modelId.toLowerCase();
6+
7+
// OpenAI 视觉模型
8+
if (m.includes("gpt-4o") || m.includes("gpt-4-turbo") || m.includes("gpt-4-vision")) return true;
9+
if (m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4")) return true;
10+
11+
// Anthropic Claude 3+ 全部支持视觉
12+
if (
13+
m.startsWith("claude-3") ||
14+
m.startsWith("claude-sonnet") ||
15+
m.startsWith("claude-opus") ||
16+
m.startsWith("claude-haiku")
17+
)
18+
return true;
19+
20+
// Google Gemini 基本都支持视觉
21+
if (m.startsWith("gemini")) return true;
22+
23+
// Grok 视觉
24+
if (m.includes("grok") && m.includes("vision")) return true;
25+
26+
// Qwen-VL
27+
if (m.includes("qwen") && (m.includes("vl") || m.includes("vision"))) return true;
28+
29+
// GLM-4V
30+
if (m.includes("glm") && m.includes("v")) return true;
31+
32+
// Pixtral (Mistral 视觉模型)
33+
if (m.startsWith("pixtral")) return true;
34+
35+
// DeepSeek-VL
36+
if (m.includes("deepseek") && m.includes("vl")) return true;
37+
38+
// Llama 视觉
39+
if (m.includes("llama") && (m.includes("vision") || m.includes("scout"))) return true;
40+
41+
return false;
42+
}
43+
44+
// 检测模型是否支持视觉输入(用户手动设置优先于自动检测)
45+
export function supportsVision(model: AgentModelConfig): boolean {
46+
if (model.supportsVision !== undefined) return model.supportsVision;
47+
return supportsVisionByModelId(model.model);
48+
}
49+
50+
// 通过模型 ID 字符串检测是否支持图片输出
51+
export function supportsImageOutputByModelId(modelId: string): boolean {
52+
const m = modelId.toLowerCase();
53+
// GPT-4o 系列支持图片生成(不含 mini/audio)
54+
if (m.includes("gpt-4o") && !m.includes("mini") && !m.includes("audio")) return true;
55+
// Gemini 2.0 Flash 支持原生图片生成(不含 1.5 等旧版本)
56+
if (m.includes("gemini-2") && m.includes("flash") && !m.includes("lite")) return true;
57+
// Gemini 3+ 带 image 标识的模型支持图片生成
58+
if (m.includes("gemini-") && m.includes("image")) return true;
59+
// DALL-E
60+
if (m.startsWith("dall-e")) return true;
61+
return false;
62+
}
63+
64+
// 检测模型是否支持图片输出(用户手动设置优先于自动检测)
65+
export function supportsImageOutput(model: AgentModelConfig): boolean {
66+
if (model.supportsImageOutput !== undefined) return model.supportsImageOutput;
67+
return supportsImageOutputByModelId(model.model);
68+
}

src/app/service/agent/core/opfs_helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export async function writeWorkspaceFile(
9191
if (data instanceof Blob) {
9292
await writable.write(data);
9393
} else if (data instanceof Uint8Array) {
94-
await writable.write(data.buffer as ArrayBuffer);
94+
// 精确截取视图对应的字节段,避免切片视图写入整个底层 buffer
95+
await writable.write((data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength));
9596
} else {
9697
await writable.write(data);
9798
}

src/app/service/agent/core/sse_parser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ export class SSEParser {
2525
event: this.currentEvent || "message",
2626
data: this.currentData.join("\n"),
2727
});
28-
this.currentEvent = "";
29-
this.currentData = [];
3028
}
29+
// 无条件重置,防止 currentEvent 残留污染下一条事件
30+
this.currentEvent = "";
31+
this.currentData = [];
3132
continue;
3233
}
3334

src/app/service/agent/core/tools/execute_script.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe("execute_script 工具", () => {
102102
const { executor } = createExecuteScriptTool(deps);
103103

104104
await expect(executor.execute({ code: "while(true){}", target: "page" })).rejects.toThrow(
105-
"execute_script timed out after 30s"
105+
"execute_script timed out after 0.05s"
106106
);
107107
});
108108

@@ -112,7 +112,7 @@ describe("execute_script 工具", () => {
112112
const { executor } = createExecuteScriptTool(deps);
113113

114114
await expect(executor.execute({ code: "while(true){}", target: "sandbox" })).rejects.toThrow(
115-
"execute_script timed out after 30s"
115+
"execute_script timed out after 0.05s"
116116
);
117117
});
118118
});

src/app/service/agent/core/tools/execute_script.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function createExecuteScriptTool(deps: ExecuteScriptDeps): {
5656
const { result, tabId: actualTabId } = await withTimeout(
5757
deps.executeInPage(code, { tabId }),
5858
timeoutMs,
59-
() => new Error("execute_script timed out after 30s")
59+
() => new Error(`execute_script timed out after ${timeoutMs / 1000}s`)
6060
);
6161
return JSON.stringify({ result: result ?? null, target: "page", tab_id: actualTabId });
6262
}
@@ -65,7 +65,7 @@ export function createExecuteScriptTool(deps: ExecuteScriptDeps): {
6565
const result = await withTimeout(
6666
deps.executeInSandbox(code),
6767
timeoutMs,
68-
() => new Error("execute_script timed out after 30s")
68+
() => new Error(`execute_script timed out after ${timeoutMs / 1000}s`)
6969
);
7070
return JSON.stringify({ result: result ?? null, target: "sandbox" });
7171
},

src/app/service/agent/core/tools/task_tools.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ToolDefinition, ChatStreamEvent } from "@App/app/service/agent/core/types";
22
import type { ToolExecutor } from "@App/app/service/agent/core/tool_registry";
3-
import { requireString } from "./param_utils";
3+
import { requireString, optionalString } from "./param_utils";
44

55
export type Task = {
66
id: string;
@@ -106,8 +106,8 @@ export function createTaskTools(options?: TaskToolsOptions): {
106106
execute: async (args: Record<string, unknown>) => {
107107
const task: Task = {
108108
id: String(nextId++),
109-
subject: args.subject as string,
110-
description: args.description as string | undefined,
109+
subject: requireString(args, "subject"),
110+
description: optionalString(args, "description"),
111111
status: "pending",
112112
};
113113
tasks.set(task.id, task);

src/app/service/agent/core/tools/web_search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { MessageSend } from "@Packages/message/types";
44
import type { SearchConfigRepo } from "./search_config";
55
import { extractSearchResults, extractBingResults, extractBaiduResults } from "@App/app/service/offscreen/client";
66
import { withTimeout } from "@App/pkg/utils/with_timeout";
7-
import { requireString } from "./param_utils";
7+
import { requireString, optionalNumber } from "./param_utils";
88

99
// Agent User-Agent 字符串
1010
const AGENT_USER_AGENT = "Mozilla/5.0 (compatible; ScriptCat Agent)";
@@ -51,7 +51,7 @@ export class WebSearchExecutor implements ToolExecutor {
5151

5252
async execute(args: Record<string, unknown>): Promise<string> {
5353
const query = requireString(args, "query");
54-
const maxResults = Math.min((args.max_results as number) || 5, 10);
54+
const maxResults = Math.min(optionalNumber(args, "max_results") ?? 5, 10);
5555

5656
const config = await this.configRepo.getConfig();
5757

src/app/service/agent/service_worker/background_session_manager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ export class BackgroundSessionManager {
179179
const rc = this.runningConversations.get(conversationId);
180180
if (!rc) return;
181181
setTimeout(() => {
182-
this.runningConversations.delete(conversationId);
182+
// 重新检查:如果会话已被复用(新的 rc 实例)或正在运行,则不删除
183+
const current = this.runningConversations.get(conversationId);
184+
if (current === rc && current.status !== "running") {
185+
this.runningConversations.delete(conversationId);
186+
}
183187
}, 30_000);
184188
}
185189
}

src/app/service/agent/service_worker/model_service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AgentModelConfig, AgentModelSafeConfig, ModelApiRequest } from "@App/app/service/agent/core/types";
22
import { AgentModelRepo } from "@App/app/repo/agent_model";
3-
import { supportsVision, supportsImageOutput } from "@App/pages/options/routes/AgentChat/model_utils";
3+
import { supportsVision, supportsImageOutput } from "@App/app/service/agent/core/model_capabilities";
44
import type { Group } from "@Packages/message/server";
55

66
export class AgentModelService {

0 commit comments

Comments
 (0)