release: v0.91.0

This commit is contained in:
coso
2026-03-19 19:05:53 +08:00
parent ad8427be93
commit a414f44872
175 changed files with 10467 additions and 11013 deletions
+7
View File
@@ -94,6 +94,12 @@ export interface AsterSessionInfo {
working_dir?: string;
}
export interface AsterTodoItem {
content: string;
status: "pending" | "in_progress" | "completed";
active_form?: string;
}
/**
* TauriMessageContent(匹配后端 TauriMessageContent 枚举)
*/
@@ -135,6 +141,7 @@ export interface AsterSessionDetail {
turns?: AgentThreadTurn[];
items?: AgentThreadItem[];
queued_turns?: QueuedTurnSnapshot[];
todo_items?: AsterTodoItem[];
}
export interface AgentTurnConfigSnapshot {
-105
View File
@@ -1,105 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import { ContextMemoryAPI } from "./contextMemory";
vi.mock("@/lib/dev-bridge", () => ({
safeInvoke: vi.fn(),
}));
describe("contextMemory API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("应代理基础记忆命令", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce([{ id: "m1", title: "任务" }])
.mockResolvedValueOnce("上下文")
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce({ session_id: "session-1", active_memories: 2 })
.mockResolvedValueOnce(undefined);
await expect(
ContextMemoryAPI.saveMemoryEntry({
session_id: "session-1",
file_type: "task_plan",
title: "计划",
content: "内容",
tags: ["任务"],
priority: 3,
}),
).resolves.toBeUndefined();
await expect(
ContextMemoryAPI.getSessionMemories("session-1"),
).resolves.toEqual([expect.objectContaining({ id: "m1" })]);
await expect(ContextMemoryAPI.getMemoryContext("session-1")).resolves.toBe(
"上下文",
);
await expect(
ContextMemoryAPI.recordError({
session_id: "session-1",
error_description: "错误",
attempted_solution: "方案",
}),
).resolves.toBeUndefined();
await expect(
ContextMemoryAPI.shouldAvoidOperation("session-1", "重复操作"),
).resolves.toBe(true);
await expect(
ContextMemoryAPI.markErrorResolved({
session_id: "session-1",
error_description: "错误",
resolution: "已修复",
}),
).resolves.toBeUndefined();
await expect(ContextMemoryAPI.getMemoryStats("session-1")).resolves.toEqual(
expect.objectContaining({ active_memories: 2 }),
);
await expect(
ContextMemoryAPI.cleanupExpiredMemories(),
).resolves.toBeUndefined();
});
it("应通过辅助方法复用基础命令", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(false);
await ContextMemoryAPI.saveTaskPlan("session-2", "计划", "内容");
await ContextMemoryAPI.saveFinding("session-2", "发现", "内容", ["关键"]);
await ContextMemoryAPI.logProgress("session-2", "进度", "完成一半");
await ContextMemoryAPI.apply2ActionRule("session-2", "发现了线索");
await expect(
ContextMemoryAPI.recordErrorWithCheck(
"session-2",
"失败",
"重试",
"再次点击",
),
).resolves.toEqual({ shouldAvoid: false });
expect(safeInvoke).toHaveBeenCalledWith("save_memory_entry", {
request: expect.objectContaining({ file_type: "task_plan" }),
});
expect(safeInvoke).toHaveBeenCalledWith("save_memory_entry", {
request: expect.objectContaining({ file_type: "findings" }),
});
expect(safeInvoke).toHaveBeenCalledWith("save_memory_entry", {
request: expect.objectContaining({ file_type: "progress" }),
});
expect(safeInvoke).toHaveBeenCalledWith("record_error", {
request: expect.objectContaining({ session_id: "session-2" }),
});
expect(safeInvoke).toHaveBeenCalledWith("should_avoid_operation", {
sessionId: "session-2",
operationDescription: "再次点击",
});
});
});
-230
View File
@@ -1,230 +0,0 @@
/**
* 上下文记忆管理 API
*
* 基于文件系统的持久化记忆系统,解决 AI Agent 的上下文丢失、目标漂移、错误重复问题
*/
import { safeInvoke } from "@/lib/dev-bridge";
export interface MemoryEntry {
id: string;
session_id: string;
file_type: MemoryFileType;
title: string;
content: string;
tags: string[];
priority: number;
created_at: number;
updated_at: number;
archived: boolean;
}
export type MemoryFileType =
| "task_plan"
| "findings"
| "progress"
| "error_log";
export interface MemoryStats {
session_id: string;
active_memories: number;
archived_memories: number;
unresolved_errors: number;
resolved_errors: number;
memory_by_type: Record<MemoryFileType, number>;
last_updated: number;
}
export interface SaveMemoryRequest {
session_id: string;
file_type: MemoryFileType;
title: string;
content: string;
tags: string[];
priority: number;
}
export interface RecordErrorRequest {
session_id: string;
error_description: string;
attempted_solution: string;
}
export interface ResolveErrorRequest {
session_id: string;
error_description: string;
resolution: string;
}
/**
* 上下文记忆管理 API 类
*/
export class ContextMemoryAPI {
/**
* 保存记忆条目
*/
static async saveMemoryEntry(request: SaveMemoryRequest): Promise<void> {
return safeInvoke<void>("save_memory_entry", { request });
}
/**
* 获取会话记忆
*/
static async getSessionMemories(
sessionId: string,
fileType?: MemoryFileType,
): Promise<MemoryEntry[]> {
return safeInvoke<MemoryEntry[]>("get_session_memories", {
sessionId,
fileType: fileType || null,
});
}
/**
* 获取记忆上下文(用于 AI 上下文)
*/
static async getMemoryContext(sessionId: string): Promise<string> {
return safeInvoke<string>("get_memory_context", { sessionId });
}
/**
* 记录错误
*/
static async recordError(request: RecordErrorRequest): Promise<void> {
return safeInvoke<void>("record_error", { request });
}
/**
* 检查是否应该避免某个操作(3次错误协议)
*/
static async shouldAvoidOperation(
sessionId: string,
operationDescription: string,
): Promise<boolean> {
return safeInvoke<boolean>("should_avoid_operation", {
sessionId,
operationDescription,
});
}
/**
* 标记错误已解决
*/
static async markErrorResolved(request: ResolveErrorRequest): Promise<void> {
return safeInvoke<void>("mark_error_resolved", { request });
}
/**
* 获取记忆统计信息
*/
static async getMemoryStats(sessionId: string): Promise<MemoryStats> {
return safeInvoke<MemoryStats>("get_memory_stats", { sessionId });
}
/**
* 清理过期记忆
*/
static async cleanupExpiredMemories(): Promise<void> {
return safeInvoke<void>("cleanup_expired_memories");
}
/**
* 保存任务计划记忆
*/
static async saveTaskPlan(
sessionId: string,
title: string,
content: string,
priority: number = 3,
): Promise<void> {
return this.saveMemoryEntry({
session_id: sessionId,
file_type: "task_plan",
title,
content,
tags: ["任务计划"],
priority,
});
}
/**
* 保存研究发现
*/
static async saveFinding(
sessionId: string,
title: string,
content: string,
tags: string[] = [],
priority: number = 4,
): Promise<void> {
return this.saveMemoryEntry({
session_id: sessionId,
file_type: "findings",
title,
content,
tags: ["发现", ...tags],
priority,
});
}
/**
* 记录进度
*/
static async logProgress(
sessionId: string,
title: string,
content: string,
): Promise<void> {
return this.saveMemoryEntry({
session_id: sessionId,
file_type: "progress",
title,
content,
tags: ["进度"],
priority: 2,
});
}
/**
* 应用 2-Action 规则:每2次视觉操作后保存发现
*/
static async apply2ActionRule(
sessionId: string,
finding: string,
): Promise<void> {
const timestamp = new Date().toLocaleTimeString();
return this.saveFinding(
sessionId,
`2-Action 规则发现 (${timestamp})`,
finding,
["2-Action规则", "自动保存"],
4,
);
}
/**
* 记录错误并检查是否需要避免重复操作
*/
static async recordErrorWithCheck(
sessionId: string,
errorDescription: string,
attemptedSolution: string,
operationDescription?: string,
): Promise<{ shouldAvoid: boolean }> {
// 记录错误
await this.recordError({
session_id: sessionId,
error_description: errorDescription,
attempted_solution: attemptedSolution,
});
// 检查是否应该避免该操作
const shouldAvoid = operationDescription
? await this.shouldAvoidOperation(sessionId, operationDescription)
: false;
return { shouldAvoid };
}
}
export default ContextMemoryAPI;
+68 -18
View File
@@ -1,14 +1,14 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import {
cleanupMemory,
getMemoryAutoIndex,
getMemoryEffectiveSources,
getMemoryOverview,
getMemoryStats,
requestMemoryAnalysis,
toggleMemoryAuto,
updateMemoryAutoNote,
analyzeContextMemory,
cleanupContextMemory,
getContextMemoryAutoIndex,
getContextMemoryEffectiveSources,
getContextMemoryOverview,
getContextMemoryStats,
toggleContextMemoryAuto,
updateContextMemoryAutoNote,
} from "./memoryRuntime";
vi.mock("@/lib/dev-bridge", () => ({
@@ -20,7 +20,7 @@ describe("memoryRuntime API", () => {
vi.clearAllMocks();
});
it("应代理记忆查询命令", async () => {
it("应通过 context memory 命名代理记忆查询命令", async () => {
vi.mocked(safeInvoke).mockImplementation(async (command) => {
switch (command) {
case "memory_runtime_get_stats":
@@ -44,22 +44,22 @@ describe("memoryRuntime API", () => {
}
});
await expect(getMemoryStats()).resolves.toEqual(
await expect(getContextMemoryStats()).resolves.toEqual(
expect.objectContaining({ total_entries: 1 }),
);
await expect(requestMemoryAnalysis()).resolves.toEqual(
await expect(analyzeContextMemory()).resolves.toEqual(
expect.objectContaining({ analyzed_sessions: 1 }),
);
await expect(cleanupMemory()).resolves.toEqual(
await expect(cleanupContextMemory()).resolves.toEqual(
expect.objectContaining({ cleaned_entries: 1 }),
);
await expect(getMemoryOverview(200)).resolves.toEqual(
await expect(getContextMemoryOverview(200)).resolves.toEqual(
expect.objectContaining({ entries: [] }),
);
await expect(getMemoryEffectiveSources()).resolves.toEqual(
await expect(getContextMemoryEffectiveSources()).resolves.toEqual(
expect.objectContaining({ sources: [] }),
);
await expect(getMemoryAutoIndex()).resolves.toEqual(
await expect(getContextMemoryAutoIndex()).resolves.toEqual(
expect.objectContaining({ items: [] }),
);
@@ -81,15 +81,65 @@ describe("memoryRuntime API", () => {
});
});
it("应代理自动记忆开关与写入命令", async () => {
it("应暴露清晰的 context memory 命名", async () => {
vi.mocked(safeInvoke).mockImplementation(async (command) => {
switch (command) {
case "memory_runtime_get_stats":
return { total_entries: 9 };
case "memory_runtime_request_analysis":
return { analyzed_sessions: 3 };
case "memory_runtime_cleanup":
return { cleaned_entries: 4 };
case "memory_runtime_get_overview":
return { stats: {}, categories: [], entries: [] };
case "memory_get_effective_sources":
return { sources: [] };
case "memory_get_auto_index":
return { items: [] };
case "memory_toggle_auto":
return { enabled: true };
case "memory_update_auto_note":
return { items: [] };
default:
return null;
}
});
await expect(getContextMemoryStats()).resolves.toEqual(
expect.objectContaining({ total_entries: 9 }),
);
await expect(analyzeContextMemory()).resolves.toEqual(
expect.objectContaining({ analyzed_sessions: 3 }),
);
await expect(cleanupContextMemory()).resolves.toEqual(
expect.objectContaining({ cleaned_entries: 4 }),
);
await expect(getContextMemoryOverview()).resolves.toEqual(
expect.objectContaining({ entries: [] }),
);
await expect(getContextMemoryEffectiveSources()).resolves.toEqual(
expect.objectContaining({ sources: [] }),
);
await expect(getContextMemoryAutoIndex()).resolves.toEqual(
expect.objectContaining({ items: [] }),
);
await expect(toggleContextMemoryAuto(true)).resolves.toEqual(
expect.objectContaining({ enabled: true }),
);
await expect(updateContextMemoryAutoNote("note")).resolves.toEqual(
expect.objectContaining({ items: [] }),
);
});
it("应代理 context memory 自动记忆开关与写入命令", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce({ enabled: true })
.mockResolvedValueOnce({ items: [] });
await expect(toggleMemoryAuto(true)).resolves.toEqual(
await expect(toggleContextMemoryAuto(true)).resolves.toEqual(
expect.objectContaining({ enabled: true }),
);
await expect(updateMemoryAutoNote("note", "topic")).resolves.toEqual(
await expect(updateContextMemoryAutoNote("note", "topic")).resolves.toEqual(
expect.objectContaining({ items: [] }),
);
});
+8 -8
View File
@@ -28,17 +28,17 @@ export type {
MemoryStatsResponse,
} from "./memoryRuntimeTypes";
export async function getMemoryOverview(
export async function getContextMemoryOverview(
limit?: number,
): Promise<MemoryOverviewResponse> {
return safeInvoke("memory_runtime_get_overview", { limit });
}
export async function getMemoryStats(): Promise<MemoryStatsResponse> {
export async function getContextMemoryStats(): Promise<MemoryStatsResponse> {
return safeInvoke("memory_runtime_get_stats");
}
export async function requestMemoryAnalysis(
export async function analyzeContextMemory(
fromTimestamp?: number,
toTimestamp?: number,
): Promise<MemoryAnalysisResult> {
@@ -48,11 +48,11 @@ export async function requestMemoryAnalysis(
});
}
export async function cleanupMemory(): Promise<CleanupMemoryResult> {
export async function cleanupContextMemory(): Promise<CleanupMemoryResult> {
return safeInvoke("memory_runtime_cleanup");
}
export async function getMemoryEffectiveSources(
export async function getContextMemoryEffectiveSources(
workingDir?: string,
activeRelativePath?: string,
): Promise<EffectiveMemorySourcesResponse> {
@@ -62,19 +62,19 @@ export async function getMemoryEffectiveSources(
});
}
export async function getMemoryAutoIndex(
export async function getContextMemoryAutoIndex(
workingDir?: string,
): Promise<AutoMemoryIndexResponse> {
return safeInvoke("memory_get_auto_index", { workingDir });
}
export async function toggleMemoryAuto(
export async function toggleContextMemoryAuto(
enabled: boolean,
): Promise<MemoryAutoToggleResponse> {
return safeInvoke("memory_toggle_auto", { enabled });
}
export async function updateMemoryAutoNote(
export async function updateContextMemoryAutoNote(
note: string,
topic?: string,
workingDir?: string,
+3
View File
@@ -47,6 +47,9 @@ export interface OpenClawEnvironmentDiagnostics {
whereCandidates?: string[];
supplementalSearchDirs?: string[];
supplementalCommandCandidates?: string[];
gitWhereCandidates?: string[];
gitSupplementalSearchDirs?: string[];
gitSupplementalCommandCandidates?: string[];
}
export interface OpenClawActionResult {
-100
View File
@@ -1,100 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import { ToolHooksAPI } from "./toolHooks";
vi.mock("@/lib/dev-bridge", () => ({
safeInvoke: vi.fn(),
}));
describe("toolHooks API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("应代理钩子规则管理命令", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce([{ id: "rule-1", name: "规则1" }])
.mockResolvedValueOnce({ "rule-1": { execution_count: 3 } })
.mockResolvedValueOnce(undefined);
const rule = ToolHooksAPI.createCustomRule(
"rule-1",
"规则1",
"说明",
"session_start",
[],
[],
);
await expect(
ToolHooksAPI.executeHooks({
trigger: "session_start",
context: { session_id: "session-1", message_count: 0, metadata: {} },
}),
).resolves.toBeUndefined();
await expect(ToolHooksAPI.addHookRule(rule)).resolves.toBeUndefined();
await expect(
ToolHooksAPI.removeHookRule("rule-1"),
).resolves.toBeUndefined();
await expect(
ToolHooksAPI.toggleHookRule("rule-1", true),
).resolves.toBeUndefined();
await expect(ToolHooksAPI.getHookRules()).resolves.toEqual([
expect.objectContaining({ id: "rule-1" }),
]);
await expect(ToolHooksAPI.getHookExecutionStats()).resolves.toEqual(
expect.objectContaining({ "rule-1": expect.any(Object) }),
);
await expect(
ToolHooksAPI.clearHookExecutionStats(),
).resolves.toBeUndefined();
});
it("应基于上下文辅助方法生成 executeHooks 请求", async () => {
vi.mocked(safeInvoke)
.mockResolvedValue(undefined)
.mockResolvedValue(undefined)
.mockResolvedValue(undefined);
await ToolHooksAPI.triggerSessionStart("session-1", { source: "test" });
await ToolHooksAPI.triggerPreToolUse(
"session-1",
"read_file",
{ path: "/tmp/a.ts" },
"读取文件",
2,
);
await ToolHooksAPI.triggerStop("session-1", 5, { source: "test" });
expect(safeInvoke).toHaveBeenNthCalledWith(1, "execute_hooks", {
request: expect.objectContaining({
trigger: "session_start",
context: expect.objectContaining({
session_id: "session-1",
metadata: expect.objectContaining({ source: "test" }),
}),
}),
});
expect(safeInvoke).toHaveBeenNthCalledWith(2, "execute_hooks", {
request: expect.objectContaining({
trigger: "pre_tool_use",
context: expect.objectContaining({
tool_name: "read_file",
message_count: 2,
}),
}),
});
expect(safeInvoke).toHaveBeenNthCalledWith(3, "execute_hooks", {
request: expect.objectContaining({
trigger: "stop",
context: expect.objectContaining({
message_count: 5,
}),
}),
});
});
});
-344
View File
@@ -1,344 +0,0 @@
/**
* 工具钩子管理 API
*
* 提供工具执行前后的钩子机制,用于自动化上下文记忆管理
*/
import { safeInvoke } from "@/lib/dev-bridge";
export type HookTrigger =
| "session_start"
| "pre_tool_use"
| "post_tool_use"
| "stop";
export interface HookRule {
id: string;
name: string;
description: string;
trigger: HookTrigger;
conditions: HookCondition[];
actions: HookAction[];
enabled: boolean;
priority: number;
created_at: number;
}
export type HookCondition =
| { tool_name_equals: string }
| { tool_name_contains: string }
| { message_contains: string }
| { message_count_greater_than: number }
| { error_count_greater_than: number }
| { custom: { condition_type: string; parameters: Record<string, string> } };
export type HookAction =
| {
save_finding: {
title: string;
content: string;
tags: string[];
priority: number;
};
}
| { update_task_plan: { title: string; content: string; priority: number } }
| { log_progress: { title: string; content: string } }
| { record_error: { error_description: string; attempted_solution: string } }
| { custom: { action_type: string; parameters: Record<string, string> } };
export interface HookExecutionStats {
execution_count: number;
success_count: number;
failure_count: number;
last_execution_at: number;
average_execution_time_ms: number;
}
export interface HookContextData {
session_id: string;
tool_name?: string;
tool_parameters?: Record<string, string>;
tool_result?: string;
message_content?: string;
message_count: number;
error_info?: string;
metadata: Record<string, string>;
}
export interface ExecuteHooksRequest {
trigger: HookTrigger;
context: HookContextData;
}
/**
* 工具钩子管理 API 类
*/
export class ToolHooksAPI {
/**
* 执行钩子
*/
static async executeHooks(request: ExecuteHooksRequest): Promise<void> {
return safeInvoke<void>("execute_hooks", { request });
}
/**
* 添加钩子规则
*/
static async addHookRule(rule: HookRule): Promise<void> {
return safeInvoke<void>("add_hook_rule", { rule });
}
/**
* 移除钩子规则
*/
static async removeHookRule(ruleId: string): Promise<void> {
return safeInvoke<void>("remove_hook_rule", { ruleId });
}
/**
* 启用/禁用钩子规则
*/
static async toggleHookRule(ruleId: string, enabled: boolean): Promise<void> {
return safeInvoke<void>("toggle_hook_rule", { ruleId, enabled });
}
/**
* 获取所有钩子规则
*/
static async getHookRules(): Promise<HookRule[]> {
return safeInvoke<HookRule[]>("get_hook_rules");
}
/**
* 获取钩子执行统计
*/
static async getHookExecutionStats(): Promise<
Record<string, HookExecutionStats>
> {
return safeInvoke<Record<string, HookExecutionStats>>(
"get_hook_execution_stats",
);
}
/**
* 清理钩子执行统计
*/
static async clearHookExecutionStats(): Promise<void> {
return safeInvoke<void>("clear_hook_execution_stats");
}
/**
* 触发会话开始钩子
*/
static async triggerSessionStart(
sessionId: string,
metadata: Record<string, string> = {},
): Promise<void> {
return this.executeHooks({
trigger: "session_start",
context: {
session_id: sessionId,
message_count: 0,
metadata: {
timestamp: new Date().toISOString(),
...metadata,
},
},
});
}
/**
* 触发工具使用前钩子
*/
static async triggerPreToolUse(
sessionId: string,
toolName: string,
toolParameters: Record<string, string> = {},
messageContent?: string,
messageCount: number = 0,
): Promise<void> {
return this.executeHooks({
trigger: "pre_tool_use",
context: {
session_id: sessionId,
tool_name: toolName,
tool_parameters: toolParameters,
message_content: messageContent,
message_count: messageCount,
metadata: {
timestamp: new Date().toISOString(),
tool_name: toolName,
},
},
});
}
/**
* 触发工具使用后钩子
*/
static async triggerPostToolUse(
sessionId: string,
toolName: string,
toolResult: string,
toolParameters: Record<string, string> = {},
messageContent?: string,
messageCount: number = 0,
errorInfo?: string,
): Promise<void> {
return this.executeHooks({
trigger: "post_tool_use",
context: {
session_id: sessionId,
tool_name: toolName,
tool_parameters: toolParameters,
tool_result: toolResult,
message_content: messageContent,
message_count: messageCount,
error_info: errorInfo,
metadata: {
timestamp: new Date().toISOString(),
tool_name: toolName,
has_error: errorInfo ? "true" : "false",
},
},
});
}
/**
* 触发会话停止钩子
*/
static async triggerStop(
sessionId: string,
messageCount: number,
metadata: Record<string, string> = {},
): Promise<void> {
return this.executeHooks({
trigger: "stop",
context: {
session_id: sessionId,
message_count: messageCount,
metadata: {
timestamp: new Date().toISOString(),
session_end: "true",
...metadata,
},
},
});
}
/**
* 创建自定义钩子规则
*/
static createCustomRule(
id: string,
name: string,
description: string,
trigger: HookTrigger,
conditions: HookCondition[],
actions: HookAction[],
priority: number = 100,
): HookRule {
return {
id,
name,
description,
trigger,
conditions,
actions,
enabled: true,
priority,
created_at: Date.now(),
};
}
/**
* 创建重要发现自动保存规则
*/
static createImportantFindingRule(): HookRule {
return this.createCustomRule(
"important-finding-auto-save",
"重要发现自动保存",
"检测到重要信息时自动保存到 findings.md",
"post_tool_use",
[{ message_contains: "重要" }, { message_contains: "发现" }],
[
{
save_finding: {
title: "重要发现 (自动检测)",
content: "检测到重要信息,已自动保存",
tags: ["重要", "自动保存"],
priority: 4,
},
},
],
1,
);
}
/**
* 创建错误自动记录规则
*/
static createErrorAutoRecordRule(): HookRule {
return this.createCustomRule(
"error-auto-record",
"错误自动记录",
"检测到错误时自动记录到错误日志",
"post_tool_use",
[{ message_contains: "错误" }],
[
{
record_error: {
error_description: "检测到错误",
attempted_solution: "正在尝试解决",
},
},
],
1,
);
}
/**
* 创建 2-Action 规则
*/
static create2ActionRule(): HookRule {
return this.createCustomRule(
"2-action-rule",
"2-Action 规则",
"每2次视觉操作后自动保存发现",
"post_tool_use",
[{ tool_name_contains: "view" }],
[
{
save_finding: {
title: "2-Action 规则触发",
content: "视觉操作完成,自动保存发现",
tags: ["2-Action规则", "视觉操作"],
priority: 3,
},
},
],
2,
);
}
/**
* 批量添加默认钩子规则
*/
static async addDefaultRules(): Promise<void> {
const rules = [
this.createImportantFindingRule(),
this.createErrorAutoRecordRule(),
this.create2ActionRule(),
];
for (const rule of rules) {
try {
await this.addHookRule(rule);
} catch (error) {
console.warn(`添加钩子规则失败: ${rule.name}`, error);
}
}
}
}
export default ToolHooksAPI;
-145
View File
@@ -1,145 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import {
configureProvider,
createSession,
deleteSession,
generateEventName,
getMessages,
getSession,
listSessions,
parseStreamEvent,
renameSession,
sendMessage,
stopGeneration,
} from "./unified-chat";
vi.mock("@/lib/dev-bridge", () => ({
safeInvoke: vi.fn(),
}));
describe("unified-chat API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("应代理会话与消息命令并转换消息结构", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce({ id: "session-1", title: "测试会话" })
.mockResolvedValueOnce([{ id: "session-1", title: "测试会话" }])
.mockResolvedValueOnce({ id: "session-1", title: "测试会话" })
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce([
{
id: 1,
session_id: "session-1",
role: "assistant",
content: [{ type: "text", text: "你好" }],
created_at: "2024-01-01T00:00:00Z",
},
])
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(undefined);
await expect(createSession({ mode: "general" } as never)).resolves.toEqual(
expect.objectContaining({ id: "session-1" }),
);
await expect(listSessions()).resolves.toEqual([
expect.objectContaining({ id: "session-1" }),
]);
await expect(getSession("session-1")).resolves.toEqual(
expect.objectContaining({ id: "session-1" }),
);
await expect(deleteSession("session-1")).resolves.toBe(true);
await expect(renameSession("session-1", "新标题")).resolves.toBeUndefined();
await expect(getMessages("session-1")).resolves.toEqual([
expect.objectContaining({
sessionId: "session-1",
content: "你好",
}),
]);
await expect(
sendMessage({ sessionId: "session-1", content: "hello" } as never),
).resolves.toBeUndefined();
await expect(stopGeneration("session-1")).resolves.toBe(true);
await expect(
configureProvider("session-1", "openai", "gpt-4o"),
).resolves.toBeUndefined();
});
it("应解析流事件并生成事件名", () => {
expect(parseStreamEvent({ type: "text_delta", text: "hi" })).toEqual({
type: "text_delta",
text: "hi",
});
expect(
parseStreamEvent({ type: "tool_end", tool_id: "tool-1", result: "done" }),
).toEqual({
type: "tool_end",
tool_id: "tool-1",
result: "done",
});
expect(
parseStreamEvent({
type: "harness_event",
kind: "artifact_created",
session_id: "session-1",
stage: "drafting",
}),
).toEqual({
type: "harness_event",
event: {
kind: "artifact_created",
sessionId: "session-1",
runId: undefined,
correlationId: undefined,
theme: undefined,
stage: "drafting",
summary: undefined,
artifact: undefined,
metadata: undefined,
},
});
expect(
parseStreamEvent({
type: "artifact_snapshot",
artifact_id: "artifact-1",
file_path: "draft.md",
content: "# 标题",
}),
).toEqual({
type: "artifact_snapshot",
artifact: {
artifactId: "artifact-1",
filePath: "draft.md",
content: "# 标题",
metadata: undefined,
},
});
expect(
parseStreamEvent({
type: "artifact_snapshot",
artifact: {
artifactId: "artifact-2",
filePath: "nested.md",
content: "nested content",
metadata: { complete: false },
},
}),
).toEqual({
type: "artifact_snapshot",
artifact: {
artifactId: "artifact-2",
filePath: "nested.md",
content: "nested content",
metadata: { complete: false },
},
});
expect(parseStreamEvent({ type: "unknown" })).toBeNull();
expect(generateEventName("session-1")).toMatch(
/^unified-chat-stream-session-1-/,
);
});
});
-372
View File
@@ -1,372 +0,0 @@
/**
* @file unified-chat.ts
* @description 统一对话 API 封装
* @module lib/api/unified-chat
*
* 封装所有统一对话相关的 Tauri 命令调用
*/
import { safeInvoke } from "@/lib/dev-bridge";
import type {
ChatMode,
ChatMessage,
SessionResponse,
CreateSessionRequest,
SendMessageRequest,
StreamEvent,
ToolCall,
ToolEndEvent,
FinalDoneEvent,
HarnessArtifactSnapshot,
HarnessEventPayload,
} from "@/types/chat";
// ============================================================================
// 会话管理 API
// ============================================================================
/**
* 创建新会话
*/
export async function createSession(
request: CreateSessionRequest,
): Promise<SessionResponse> {
return safeInvoke<SessionResponse>("chat_create_session", { request });
}
/**
* 获取会话列表
*/
export async function listSessions(
mode?: ChatMode,
): Promise<SessionResponse[]> {
return safeInvoke<SessionResponse[]>("chat_list_sessions", { mode });
}
/**
* 获取会话详情
*/
export async function getSession(sessionId: string): Promise<SessionResponse> {
return safeInvoke<SessionResponse>("chat_get_session", { sessionId });
}
/**
* 删除会话
*/
export async function deleteSession(sessionId: string): Promise<boolean> {
return safeInvoke<boolean>("chat_delete_session", { sessionId });
}
/**
* 重命名会话
*/
export async function renameSession(
sessionId: string,
title: string,
): Promise<void> {
return safeInvoke<void>("chat_rename_session", { sessionId, title });
}
// ============================================================================
// 消息管理 API
// ============================================================================
/**
* 获取会话消息列表
*/
export async function getMessages(
sessionId: string,
limit?: number,
): Promise<ChatMessage[]> {
const messages = await safeInvoke<
Array<{
id: number;
session_id: string;
role: string;
content: unknown;
tool_calls?: unknown;
tool_call_id?: string;
metadata?: unknown;
created_at: string;
}>
>("chat_get_messages", { sessionId, limit });
// 转换后端格式为前端格式
return messages.map(convertBackendMessage);
}
/**
* 发送消息(流式)
*/
export async function sendMessage(request: SendMessageRequest): Promise<void> {
return safeInvoke<void>("chat_send_message", { request });
}
/**
* 停止生成
*/
export async function stopGeneration(sessionId: string): Promise<boolean> {
return safeInvoke<boolean>("chat_stop_generation", { sessionId });
}
/**
* 配置会话的 Provider
*/
export async function configureProvider(
sessionId: string,
providerType: string,
model: string,
): Promise<void> {
return safeInvoke<void>("chat_configure_provider", {
sessionId,
providerType,
model,
});
}
// ============================================================================
// 辅助函数
// ============================================================================
/**
* 转换后端消息格式为前端格式
*/
function convertBackendMessage(msg: {
id: number;
session_id: string;
role: string;
content: unknown;
tool_calls?: unknown;
tool_call_id?: string;
metadata?: unknown;
created_at: string;
}): ChatMessage {
// 提取文本内容
let textContent = "";
if (typeof msg.content === "string") {
textContent = msg.content;
} else if (Array.isArray(msg.content)) {
textContent = msg.content
.filter(
(part): part is { type: "text"; text: string } =>
typeof part === "object" && part !== null && part.type === "text",
)
.map((part) => part.text)
.join("\n");
} else if (typeof msg.content === "object" && msg.content !== null) {
// 尝试从对象中提取文本
const contentObj = msg.content as Record<string, unknown>;
if (typeof contentObj.text === "string") {
textContent = contentObj.text;
}
}
return {
id: msg.id,
sessionId: msg.session_id,
role: msg.role as ChatMessage["role"],
content: textContent,
contentBlocks: Array.isArray(msg.content)
? msg.content.map(convertContentBlock)
: [{ type: "text" as const, text: textContent }],
toolCalls: msg.tool_calls ? convertToolCalls(msg.tool_calls) : undefined,
toolCallId: msg.tool_call_id,
status: "complete",
metadata: msg.metadata as ChatMessage["metadata"],
createdAt: msg.created_at,
};
}
/**
* 转换内容块
*/
function convertContentBlock(
block: unknown,
): NonNullable<ChatMessage["contentBlocks"]>[number] {
if (typeof block !== "object" || block === null) {
return { type: "text", text: String(block) };
}
const b = block as Record<string, unknown>;
switch (b.type) {
case "text":
return { type: "text", text: String(b.text || "") };
case "image":
return { type: "image", url: String(b.url || ""), alt: b.alt as string };
case "file":
return {
type: "file",
path: String(b.path || ""),
name: String(b.name || ""),
};
case "canvas":
return {
type: "canvas",
canvasType: String(b.canvasType || ""),
content: String(b.content || ""),
};
default:
return { type: "text", text: JSON.stringify(block) };
}
}
/**
* 转换工具调用
*/
function convertToolCalls(toolCalls: unknown): ChatMessage["toolCalls"] {
if (!Array.isArray(toolCalls)) return undefined;
return toolCalls.map((tc) => {
const call = tc as Record<string, unknown>;
return {
id: String(call.id || ""),
name: String(call.name || ""),
arguments: call.arguments as string,
status: (call.status as ToolCall["status"]) || "completed",
result: call.result as ToolCall["result"],
};
});
}
/**
* 解析流式事件
*/
export function parseStreamEvent(payload: unknown): StreamEvent | null {
if (typeof payload !== "object" || payload === null) {
return null;
}
const event = payload as Record<string, unknown>;
const type = event.type as string;
switch (type) {
case "TextDelta":
case "text_delta":
return {
type: "text_delta",
text: String(event.text || event.content || ""),
};
case "ThinkingDelta":
case "thinking_delta":
return {
type: "thinking_delta",
text: String(event.text || event.content || ""),
};
case "ToolStart":
case "tool_start":
return {
type: "tool_start",
tool_id: String(event.tool_id || event.id || ""),
tool_name: String(event.tool_name || event.name || ""),
arguments: event.arguments as string,
};
case "ToolEnd":
case "tool_end":
return {
type: "tool_end",
tool_id: String(event.tool_id || event.id || ""),
result: event.result as ToolEndEvent["result"],
};
case "HarnessEvent":
case "harness_event":
return {
type: "harness_event",
event: {
kind: String(event.kind || event.event_kind || "unknown"),
sessionId:
(event.session_id as string) || (event.sessionId as string),
runId: (event.run_id as string) || (event.runId as string),
correlationId:
(event.correlation_id as string) ||
(event.correlationId as string),
theme: event.theme as string,
stage: event.stage as string,
summary: event.summary as string,
artifact: (event.artifact ||
(event.snapshot as HarnessArtifactSnapshot) ||
undefined) as HarnessArtifactSnapshot | undefined,
metadata: event.metadata as HarnessEventPayload["metadata"],
},
};
case "ArtifactSnapshot":
case "artifact_snapshot":
{
const nestedArtifact =
event.artifact && typeof event.artifact === "object"
? (event.artifact as Record<string, unknown>)
: undefined;
return {
type: "artifact_snapshot",
artifact: {
artifactId: String(
nestedArtifact?.artifactId ||
nestedArtifact?.artifact_id ||
event.artifact_id ||
event.artifactId ||
event.id ||
"artifact-unknown",
),
filePath:
(nestedArtifact?.filePath as string | undefined) ||
(nestedArtifact?.file_path as string | undefined) ||
(event.file_path as string | undefined) ||
(event.filePath as string | undefined),
content:
(nestedArtifact?.content as string | undefined) ||
(event.content as string | undefined),
metadata:
(nestedArtifact?.metadata as Record<string, unknown> | undefined) ||
(event.metadata as Record<string, unknown> | undefined),
},
};
}
case "ActionRequired":
case "action_required":
return {
type: "action_required",
request_id: String(event.request_id || ""),
action_type: String(event.action_type || ""),
tool_name: event.tool_name as string,
arguments: event.arguments as string,
prompt: event.prompt as string,
questions: event.questions as unknown[],
requested_schema: event.requested_schema,
};
case "Done":
case "done":
return { type: "done" };
case "FinalDone":
case "final_done":
return {
type: "final_done",
usage: event.usage as FinalDoneEvent["usage"],
};
case "Error":
case "error":
return {
type: "error",
message: String(event.message || "Unknown error"),
};
default:
console.warn("[parseStreamEvent] 未知事件类型:", type);
return null;
}
}
/**
* 生成唯一事件名称
*/
export function generateEventName(sessionId: string): string {
return `unified-chat-stream-${sessionId}-${Date.now()}`;
}
+17 -17
View File
@@ -63,22 +63,22 @@
"legacyCommandSurfaceMonitors": [
{
"id": "agent-create-session-compat-command",
"classification": "compat",
"description": "agent_create_session compat 命令前端边界",
"classification": "dead-candidate",
"description": "已零引用的旧 agent_create_session 命令边界",
"commands": ["agent_create_session"],
"allowedPaths": ["src/lib/api/agentRuntime.ts"]
"allowedPaths": []
},
{
"id": "agent-session-message-legacy-command",
"classification": "deprecated",
"description": "旧 agent session message 命令前端边界",
"classification": "dead-candidate",
"description": "已零引用的旧 agent session message 命令边界",
"commands": ["agent_get_session_messages"],
"allowedPaths": ["src/lib/api/agentRuntime.ts"]
"allowedPaths": []
},
{
"id": "agent-session-compat-commands",
"classification": "deprecated",
"description": "旧 agent session compat 命令前端边界",
"classification": "dead-candidate",
"description": "已零引用的旧 agent session compat 命令边界",
"commands": [
"agent_list_sessions",
"agent_get_session",
@@ -91,8 +91,8 @@
"legacyHelperSurfaceMonitors": [
{
"id": "agent-legacy-session-api-helpers",
"classification": "deprecated",
"description": "旧 Agent session compat helper 直连回流",
"classification": "dead-candidate",
"description": "已零引用的旧 Agent session compat helper 回流",
"helpers": [
"createAgentSession",
"listAgentSessions",
@@ -102,24 +102,24 @@
"deleteAgentSession",
"generateAgentTitle"
],
"allowedPaths": ["src/lib/api/agentRuntime.ts"]
"allowedPaths": []
},
{
"id": "agent-legacy-stream-action-helpers",
"classification": "deprecated",
"description": "旧 Aster stream/action helper 直连回流",
"classification": "dead-candidate",
"description": "已零引用的旧 Aster stream/action helper 回流",
"helpers": [
"sendAsterMessageStream",
"confirmAsterAction",
"submitAsterElicitationResponse",
"stopAsterSession"
],
"allowedPaths": ["src/lib/api/agentRuntime.ts"]
"allowedPaths": []
},
{
"id": "aster-session-helper-direct-usage",
"classification": "deprecated",
"description": "前端 direct Aster session helper 回流",
"classification": "dead-candidate",
"description": "已零引用的前端 direct Aster session helper 回流",
"helpers": [
"createAsterSession",
"listAsterSessions",
@@ -127,7 +127,7 @@
"deleteAsterSession",
"renameAsterSession"
],
"allowedPaths": ["src/lib/api/agentRuntime.ts"]
"allowedPaths": []
}
]
}
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import {
inferModelCapabilities,
inferVisionCapability,
} from "./inferModelCapabilities";
describe("inferModelCapabilities", () => {
it("应将 gpt-5.4 识别为支持视觉的模型", () => {
expect(
inferVisionCapability({
modelId: "gpt-5.4",
providerId: "codex",
}),
).toBe(true);
});
it("应避免将生图模型误判为视觉聊天模型", () => {
expect(
inferVisionCapability({
modelId: "gemini-3-pro-image-preview",
providerId: "gemini",
}),
).toBe(false);
});
it("应保留 thinking 模型的推理能力推断", () => {
expect(
inferModelCapabilities({
modelId: "gpt-5.4-thinking",
providerId: "openai",
}),
).toMatchObject({
vision: true,
reasoning: true,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
});
});
});
+105
View File
@@ -0,0 +1,105 @@
import type { ModelCapabilities } from "@/lib/types/modelRegistry";
const REASONING_TOKEN_PATTERN = /(^|[._/-])(thinking|reasoning)(?=$|[._/-])/i;
const VISION_HINT_PATTERN =
/\b(vision|multimodal|multi-modal|omni|image-input|image understanding)\b/i;
const NON_VISION_PATTERN =
/\b(embedding|embed|rerank|tts|stt|transcribe|transcription|speech|audio|moderation)\b/i;
const IMAGE_GENERATION_PATTERN =
/\b(imagen|dall-e|dalle|stable[ -]?diffusion|sdxl|sd3|midjourney|mj|flux|image[ -]?generation|image-gen|image-preview)\b/i;
const OPENAI_VISION_PATTERN =
/\b(gpt-5(?:[._/-]|\b)|gpt-4o(?:[._/-]|\b)|gpt-4\.1(?:[._/-]|\b)|gpt-4\.5(?:[._/-]|\b)|gpt-5.*codex)\b/i;
const GEMINI_VISION_PATTERN = /\bgemini(?:[._/-]|\b)/i;
const CLAUDE_VISION_PATTERN = /\bclaude(?:[._/-]|\b)/i;
const QWEN_VISION_PATTERN = /\bqwen(?:[._/-]|\b).*(vl|vision)|\bqvq\b/i;
const GLM_VISION_PATTERN = /\bglm-[\w.-]*v[\w.-]*\b/i;
const normalize = (value?: string | null): string =>
(value || "").trim().toLowerCase();
function buildSearchText(parts: Array<string | null | undefined>): string {
return parts
.map((part) => normalize(part))
.filter(Boolean)
.join(" ");
}
export function inferReasoningCapability(modelId: string): boolean {
return REASONING_TOKEN_PATTERN.test(modelId.trim().toLowerCase());
}
export function inferVisionCapability(params: {
modelId: string;
providerId?: string | null;
family?: string | null;
description?: string | null;
}): boolean {
const { modelId, providerId, family, description } = params;
const text = buildSearchText([modelId, family, description]);
const provider = normalize(providerId);
if (!text) {
return false;
}
if (NON_VISION_PATTERN.test(text) || IMAGE_GENERATION_PATTERN.test(text)) {
return false;
}
if (VISION_HINT_PATTERN.test(text)) {
return true;
}
if (OPENAI_VISION_PATTERN.test(text)) {
return true;
}
if (provider === "codex" || provider === "openai") {
return OPENAI_VISION_PATTERN.test(text);
}
if (provider === "gemini") {
return GEMINI_VISION_PATTERN.test(text);
}
if (provider === "anthropic" || provider === "claude") {
return CLAUDE_VISION_PATTERN.test(text);
}
if (provider === "qwen" || provider === "alibaba") {
return QWEN_VISION_PATTERN.test(text);
}
if (provider === "zhipuai") {
return GLM_VISION_PATTERN.test(text);
}
return (
GEMINI_VISION_PATTERN.test(text) ||
CLAUDE_VISION_PATTERN.test(text) ||
QWEN_VISION_PATTERN.test(text) ||
GLM_VISION_PATTERN.test(text)
);
}
export function inferModelCapabilities(params: {
modelId: string;
providerId?: string | null;
family?: string | null;
description?: string | null;
}): ModelCapabilities {
const { modelId, providerId, family, description } = params;
return {
vision: inferVisionCapability({
modelId,
providerId,
family,
description,
}),
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: inferReasoningCapability(modelId),
};
}
+176
View File
@@ -0,0 +1,176 @@
import { describe, expect, it } from "vitest";
import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry";
import { resolveVisionModel } from "./visionModelResolver";
function createModel(
id: string,
overrides: Partial<EnhancedModelMetadata> = {},
): EnhancedModelMetadata {
return {
id,
display_name: id,
provider_id: "zhipuai",
provider_name: "Zhipu AI",
family: id,
tier: "pro",
capabilities: {
vision: false,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: false,
},
pricing: null,
limits: {
context_length: null,
max_output_tokens: null,
requests_per_minute: null,
tokens_per_minute: null,
},
status: "active",
release_date: "2026-01-01",
is_latest: false,
description: null,
source: "local",
created_at: 0,
updated_at: 0,
...overrides,
};
}
describe("resolveVisionModel", () => {
it("当前模型已支持视觉时应保持不变", () => {
const models = [
createModel("glm-4.6v-flash", {
capabilities: {
vision: true,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: false,
},
}),
];
const result = resolveVisionModel({
currentModelId: "glm-4.6v-flash",
models,
});
expect(result).toEqual({
targetModelId: "glm-4.6v-flash",
switched: false,
reason: "already_vision",
});
});
it("当前模型未收录但模型名可推断支持视觉时应保持不变", () => {
const models = [
createModel("gpt-5.3-codex", {
provider_id: "codex",
provider_name: "Codex",
capabilities: {
vision: true,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: true,
},
}),
];
const result = resolveVisionModel({
currentModelId: "gpt-5.4",
models,
});
expect(result).toEqual({
targetModelId: "gpt-5.4",
switched: false,
reason: "already_vision",
});
});
it("应优先选择支持视觉的聊天模型,而不是纯生图模型", () => {
const models = [
createModel("gemini-3-pro-image-preview", {
family: "gemini-3-pro-image",
capabilities: {
vision: true,
tools: false,
streaming: true,
json_mode: false,
function_calling: false,
reasoning: false,
},
description: "image generation model",
is_latest: true,
}),
createModel("glm-4.6v-flash", {
family: "glm-4.6v",
tier: "mini",
capabilities: {
vision: true,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: false,
},
release_date: "2026-02-01",
is_latest: true,
}),
createModel("glm-4.7", {
family: "glm-4.7",
capabilities: {
vision: false,
tools: true,
streaming: true,
json_mode: true,
function_calling: true,
reasoning: true,
},
}),
];
const result = resolveVisionModel({
currentModelId: "glm-4.7",
models,
});
expect(result.targetModelId).toBe("glm-4.6v-flash");
expect(result.switched).toBe(true);
expect(result.reason).toBe("fallback_latest");
});
it("没有可用视觉聊天模型时应返回 no_vision_model", () => {
const models = [
createModel("glm-4.7"),
createModel("gemini-3-pro-image-preview", {
capabilities: {
vision: true,
tools: false,
streaming: true,
json_mode: false,
function_calling: false,
reasoning: false,
},
description: "image generation model",
}),
];
const result = resolveVisionModel({
currentModelId: "glm-4.7",
models,
});
expect(result).toEqual({
targetModelId: "glm-4.7",
switched: false,
reason: "no_vision_model",
});
});
});
+187
View File
@@ -0,0 +1,187 @@
import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry";
import { inferVisionCapability } from "./inferModelCapabilities";
export type VisionResolveReason =
| "already_vision"
| "matched"
| "fallback_latest"
| "no_vision_model";
export interface VisionResolveResult {
targetModelId: string;
switched: boolean;
reason: VisionResolveReason;
}
interface ResolveVisionModelParams {
currentModelId: string;
models: EnhancedModelMetadata[];
}
const IMAGE_GENERATION_KEYWORDS = [
"imagen",
"dall-e",
"stable-diffusion",
"stable diffusion",
"sdxl",
"sd3",
"midjourney",
"mj",
"flux",
"image generation",
"image-gen",
];
const TIER_WEIGHT: Record<EnhancedModelMetadata["tier"], number> = {
mini: 1,
pro: 2,
max: 3,
};
const normalize = (value?: string | null): string =>
(value || "").trim().toLowerCase();
const findModelMeta = (
modelId: string,
models: EnhancedModelMetadata[],
): EnhancedModelMetadata | undefined => {
const normalizedId = normalize(modelId);
return models.find((model) => normalize(model.id) === normalizedId);
};
const buildSearchText = (model: EnhancedModelMetadata): string =>
[
model.id,
model.display_name,
model.family || "",
model.description || "",
]
.join(" ")
.toLowerCase();
const isLikelyImageGenerationModel = (
model: EnhancedModelMetadata,
): boolean => {
const text = buildSearchText(model);
if (!IMAGE_GENERATION_KEYWORDS.some((keyword) => text.includes(keyword))) {
return false;
}
return (
!model.capabilities.tools &&
!model.capabilities.function_calling &&
!model.capabilities.json_mode
);
};
const supportsVision = (
model: EnhancedModelMetadata | undefined,
fallbackModelId?: string,
): boolean => {
if (model?.capabilities.vision) {
return true;
}
if (!fallbackModelId) {
return false;
}
return inferVisionCapability({
modelId: fallbackModelId,
providerId: model?.provider_id,
family: model?.family,
description: model?.description,
});
};
const capabilityScore = (model: EnhancedModelMetadata): number => {
let score = 0;
if (model.capabilities.tools) score += 5;
if (model.capabilities.function_calling) score += 4;
if (model.capabilities.json_mode) score += 3;
if (model.capabilities.reasoning) score += 2;
if (model.capabilities.streaming) score += 1;
return score;
};
function compareReleaseDateDesc(
left: EnhancedModelMetadata,
right: EnhancedModelMetadata,
): number {
if (left.release_date && right.release_date) {
return right.release_date.localeCompare(left.release_date);
}
if (left.release_date && !right.release_date) return -1;
if (!left.release_date && right.release_date) return 1;
return 0;
}
export function resolveVisionModel(
params: ResolveVisionModelParams,
): VisionResolveResult {
const { currentModelId, models } = params;
const currentModel = findModelMeta(currentModelId, models);
if (supportsVision(currentModel, currentModelId)) {
return {
targetModelId: currentModel?.id || currentModelId,
switched: false,
reason: "already_vision",
};
}
const currentFamily = normalize(currentModel?.family);
const candidates = models.filter(
(model) => model.capabilities.vision && !isLikelyImageGenerationModel(model),
);
if (candidates.length === 0) {
return {
targetModelId: currentModelId,
switched: false,
reason: "no_vision_model",
};
}
const sortedCandidates = [...candidates].sort((left, right) => {
const leftSameFamily = currentFamily.length > 0 && normalize(left.family) === currentFamily;
const rightSameFamily =
currentFamily.length > 0 && normalize(right.family) === currentFamily;
if (leftSameFamily !== rightSameFamily) {
return leftSameFamily ? -1 : 1;
}
const capabilityDelta = capabilityScore(right) - capabilityScore(left);
if (capabilityDelta !== 0) {
return capabilityDelta;
}
if (left.is_latest !== right.is_latest) {
return left.is_latest ? -1 : 1;
}
const tierDelta = TIER_WEIGHT[right.tier] - TIER_WEIGHT[left.tier];
if (tierDelta !== 0) {
return tierDelta;
}
const releaseDelta = compareReleaseDateDesc(left, right);
if (releaseDelta !== 0) {
return releaseDelta;
}
return left.id.localeCompare(right.id);
});
const target = sortedCandidates[0];
const reason =
currentFamily.length > 0 && normalize(target.family) === currentFamily
? "matched"
: "fallback_latest";
return {
targetModelId: target.id,
switched: normalize(target.id) !== normalize(currentModelId),
reason,
};
}
+3
View File
@@ -724,6 +724,9 @@ const defaultMocks: Record<string, any> = {
whereCandidates: [],
supplementalSearchDirs: ["/opt/homebrew/bin", "/usr/local/bin"],
supplementalCommandCandidates: [],
gitWhereCandidates: [],
gitSupplementalSearchDirs: [],
gitSupplementalCommandCandidates: [],
},
tempArtifacts: [],
}),
-402
View File
@@ -1,402 +0,0 @@
/**
* 三阶段工作流管理器
*
* 基于 planning-with-files 的核心机制,实现:
* - Pre-Action → Action → Post-Action 三阶段工作流
* - 自动化上下文工程和错误学习
* - 2-Action 规则和 3次错误协议
*/
import { ContextMemoryAPI } from "../api/contextMemory";
import { ToolHooksAPI } from "../api/toolHooks";
export interface WorkflowPhase {
number: number;
name: string;
status: "pending" | "in_progress" | "complete";
tasks: string[];
notes?: string;
}
export interface WorkflowConfig {
sessionId: string;
projectName: string;
goal: string;
phases: WorkflowPhase[];
}
export interface ActionContext {
sessionId: string;
actionType: string;
actionDescription: string;
toolName?: string;
toolParameters?: Record<string, string>;
messageCount: number;
}
/**
* 三阶段工作流管理器
*/
export class ThreeStageWorkflowManager {
private sessionId: string;
private visualOperationCount: number = 0;
private errorAttempts: Map<string, number> = new Map();
constructor(sessionId: string) {
this.sessionId = sessionId;
}
/**
* 初始化工作流
*/
async initializeWorkflow(config: WorkflowConfig): Promise<void> {
// 触发会话开始钩子
await ToolHooksAPI.triggerSessionStart(this.sessionId, {
project_name: config.projectName,
goal: config.goal,
});
// 创建任务计划
const taskPlanContent = this.generateTaskPlanContent(config);
await ContextMemoryAPI.saveTaskPlan(
this.sessionId,
`任务计划: ${config.projectName}`,
taskPlanContent,
5,
);
// 创建初始发现记录
await ContextMemoryAPI.saveFinding(
this.sessionId,
"工作流初始化",
`三阶段工作流已初始化\n项目: ${config.projectName}\n目标: ${config.goal}`,
["初始化", "工作流"],
3,
);
// 记录初始进度
await ContextMemoryAPI.logProgress(
this.sessionId,
"工作流启动",
`三阶段工作流已启动,共 ${config.phases.length} 个阶段`,
);
}
/**
* Pre-Action 阶段:执行操作前的上下文刷新
*/
async preAction(context: ActionContext): Promise<string> {
// 触发 Pre-Tool-Use 钩子
await ToolHooksAPI.triggerPreToolUse(
context.sessionId,
context.toolName || context.actionType,
context.toolParameters || {},
context.actionDescription,
context.messageCount,
);
// 获取当前记忆上下文
const memoryContext = await ContextMemoryAPI.getMemoryContext(
context.sessionId,
);
// 检查是否应该避免该操作(3次错误协议)
const shouldAvoid = await ContextMemoryAPI.shouldAvoidOperation(
context.sessionId,
context.actionDescription,
);
if (shouldAvoid) {
const warning = `⚠️ 3次错误协议警告: 该操作已失败3次,建议更换方法\n操作: ${context.actionDescription}`;
await ContextMemoryAPI.recordError({
session_id: context.sessionId,
error_description: `重复失败操作: ${context.actionDescription}`,
attempted_solution: "触发3次错误协议,建议更换方法",
});
return `${warning}\n\n当前上下文:\n${memoryContext}`;
}
// 记录上下文刷新
await ContextMemoryAPI.logProgress(
context.sessionId,
"Pre-Action 上下文刷新",
`准备执行: ${context.actionDescription}`,
);
return `🔄 Pre-Action 上下文刷新完成\n\n准备执行: ${context.actionDescription}\n\n当前记忆上下文:\n${memoryContext}`;
}
/**
* Action 阶段:执行实际操作
*/
async executeAction(
context: ActionContext,
actionResult: string,
): Promise<void> {
// 记录操作执行
await ContextMemoryAPI.logProgress(
context.sessionId,
`执行操作: ${context.actionType}`,
`操作描述: ${context.actionDescription}\n结果: ${actionResult.substring(0, 200)}${actionResult.length > 200 ? "..." : ""}`,
);
// 如果是视觉操作,增加计数
if (this.isVisualOperation(context.actionType)) {
this.visualOperationCount++;
}
}
/**
* Post-Action 阶段:操作后的状态更新
*/
async postAction(
context: ActionContext,
actionResult: string,
error?: string,
): Promise<string> {
let message = "📝 Post-Action 状态更新:\n\n";
// 处理错误情况
if (error) {
const errorKey = context.actionDescription;
const attemptCount = (this.errorAttempts.get(errorKey) || 0) + 1;
this.errorAttempts.set(errorKey, attemptCount);
const { shouldAvoid } = await ContextMemoryAPI.recordErrorWithCheck(
context.sessionId,
error,
`尝试次数: ${attemptCount}`,
context.actionDescription,
);
message += `🚨 错误记录 (第${attemptCount}次尝试): ${error}\n`;
if (shouldAvoid) {
message += `⚠️ 已达到3次错误限制,建议更换方法\n`;
}
}
// 触发 Post-Tool-Use 钩子
await ToolHooksAPI.triggerPostToolUse(
context.sessionId,
context.toolName || context.actionType,
actionResult,
context.toolParameters || {},
context.actionDescription,
context.messageCount,
error,
);
// 应用 2-Action 规则
if (this.visualOperationCount >= 2) {
await this.apply2ActionRule(actionResult);
message += `🎯 2-Action 规则已应用 (视觉操作计数: ${this.visualOperationCount})\n`;
this.visualOperationCount = 0; // 重置计数
}
// 提醒更新状态
message += `\n💡 提醒:\n`;
message += `- 如果完成了某个阶段,请更新任务计划状态\n`;
message += `- 有新发现请记录到 findings.md\n`;
message += `- 重要进展请更新 progress.md\n`;
return message;
}
/**
* 应用 2-Action 规则
*/
private async apply2ActionRule(actionResult: string): Promise<void> {
const timestamp = new Date().toLocaleTimeString();
const finding = `2-Action 规则触发 (${timestamp})\n\n最近操作结果:\n${actionResult.substring(0, 500)}${actionResult.length > 500 ? "..." : ""}`;
await ContextMemoryAPI.apply2ActionRule(this.sessionId, finding);
}
/**
* 更新阶段状态
*/
async updatePhaseStatus(
phaseNumber: number,
status: "pending" | "in_progress" | "complete",
notes?: string,
): Promise<void> {
const statusText = {
pending: "待开始",
in_progress: "进行中",
complete: "已完成",
}[status];
await ContextMemoryAPI.saveTaskPlan(
this.sessionId,
`阶段 ${phaseNumber} 状态更新`,
`阶段 ${phaseNumber} 状态已更新为: ${statusText}${notes ? `\n备注: ${notes}` : ""}`,
4,
);
await ContextMemoryAPI.logProgress(
this.sessionId,
`阶段 ${phaseNumber} 状态更新`,
`状态: ${statusText}${notes ? `\n备注: ${notes}` : ""}`,
);
}
/**
* 记录重要发现
*/
async recordFinding(
title: string,
content: string,
tags: string[] = [],
): Promise<void> {
await ContextMemoryAPI.saveFinding(
this.sessionId,
title,
content,
["发现", ...tags],
4,
);
}
/**
* 记录决策
*/
async recordDecision(decision: string, rationale: string): Promise<void> {
await ContextMemoryAPI.saveFinding(
this.sessionId,
`决策: ${decision}`,
`决策内容: ${decision}\n\n决策理由:\n${rationale}`,
["决策", "重要"],
5,
);
}
/**
* 检查任务完成状态
*/
async checkCompletion(): Promise<{ isComplete: boolean; summary: string }> {
const stats = await ContextMemoryAPI.getMemoryStats(this.sessionId);
const memories = await ContextMemoryAPI.getSessionMemories(this.sessionId);
// 简单的完成度检查逻辑
const taskPlanMemories = memories.filter(
(m) => m.file_type === "task_plan",
);
const hasCompletedPhases = taskPlanMemories.some(
(m) => m.content.includes("已完成") || m.content.includes("complete"),
);
const summary =
`📊 任务完成状态检查:\n\n` +
`- 活跃记忆: ${stats.active_memories} 个\n` +
`- 未解决错误: ${stats.unresolved_errors} 个\n` +
`- 已解决错误: ${stats.resolved_errors} 个\n` +
`- 是否有已完成阶段: ${hasCompletedPhases ? "是" : "否"}\n\n` +
`${stats.unresolved_errors > 0 ? "⚠️ 仍有未解决的错误需要处理" : "✅ 无未解决错误"}`;
return {
isComplete: hasCompletedPhases && stats.unresolved_errors === 0,
summary,
};
}
/**
* 结束工作流
*/
async finalizeWorkflow(): Promise<string> {
const { isComplete, summary } = await this.checkCompletion();
// 触发停止钩子
await ToolHooksAPI.triggerStop(this.sessionId, 0, {
workflow_complete: isComplete.toString(),
});
// 保存会话摘要
await ContextMemoryAPI.saveFinding(
this.sessionId,
"工作流会话摘要",
`三阶段工作流已结束\n\n${summary}`,
["摘要", "会话结束"],
5,
);
return `🎉 三阶段工作流已结束\n\n${summary}`;
}
/**
* 生成任务计划内容
*/
private generateTaskPlanContent(config: WorkflowConfig): string {
let content = `# 任务计划: ${config.projectName}\n\n`;
content += `## 目标\n${config.goal}\n\n`;
content += `## 当前阶段\n阶段 1\n\n`;
content += `## 阶段列表\n\n`;
config.phases.forEach((phase) => {
content += `### 阶段 ${phase.number}: ${phase.name}\n`;
phase.tasks.forEach((task) => {
content += `- [ ] ${task}\n`;
});
content += `- **状态**: ${phase.status}\n\n`;
});
content += `## 关键问题\n`;
content += `1. [需要回答的重要问题]\n`;
content += `2. [另一个关键问题]\n\n`;
content += `## 已做决策\n`;
content += `| 决策 | 理由 |\n`;
content += `|------|------|\n`;
content += `| | |\n\n`;
content += `## 遇到的错误\n`;
content += `| 错误 | 尝试次数 | 解决方案 |\n`;
content += `|------|----------|----------|\n`;
content += `| | 1 | |\n\n`;
content += `## 注意事项\n`;
content += `- **2-Action 规则**: 每2次视觉操作后立即保存发现\n`;
content += `- **3次错误协议**: 永不重复相同的失败操作\n`;
content += `- **上下文刷新**: 重要决策前重新阅读计划文件\n`;
return content;
}
/**
* 判断是否为视觉操作
*/
private isVisualOperation(actionType: string): boolean {
const visualActions = [
"view",
"read",
"browse",
"search",
"screenshot",
"image",
];
return visualActions.some((action) =>
actionType.toLowerCase().includes(action),
);
}
/**
* 获取会话统计
*/
async getSessionStats(): Promise<{
memoryStats: any;
visualOperationCount: number;
errorAttempts: Record<string, number>;
}> {
const memoryStats = await ContextMemoryAPI.getMemoryStats(this.sessionId);
return {
memoryStats,
visualOperationCount: this.visualOperationCount,
errorAttempts: Object.fromEntries(this.errorAttempts),
};
}
}
export default ThreeStageWorkflowManager;