@xiaohhhh1/canvas-agent 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/agent-instructions.md +21 -0
- package/dist/agent/claude.d.ts +3 -0
- package/dist/agent/claude.js +46 -0
- package/dist/agent/codex-client.d.ts +73 -0
- package/dist/agent/codex-client.js +438 -0
- package/dist/agent/codex-history.d.ts +25 -0
- package/dist/agent/codex-history.js +405 -0
- package/dist/agent/codex-protocol.d.ts +208 -0
- package/dist/{agents.d.ts → agent/codex.d.ts} +34 -31
- package/dist/agent/codex.js +210 -0
- package/dist/agent/types.d.ts +14 -0
- package/dist/agent/types.js +1 -0
- package/dist/canvas/operations.d.ts +13 -0
- package/dist/canvas/operations.js +161 -0
- package/dist/{schemas.d.ts → canvas/schemas.d.ts} +21 -20
- package/dist/{schemas.js → canvas/schemas.js} +1 -0
- package/dist/{canvas-session.d.ts → canvas/session.d.ts} +21 -1
- package/dist/canvas/session.js +256 -0
- package/dist/{tools.d.ts → canvas/tools.d.ts} +13 -8
- package/dist/{tools.js → canvas/tools.js} +5 -0
- package/dist/{types.d.ts → canvas/types.d.ts} +1 -10
- package/dist/canvas/types.js +1 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +22 -4
- package/dist/index.js +2 -2
- package/dist/server/http.d.ts +2 -0
- package/dist/{http-server.js → server/http.js} +100 -16
- package/dist/server/mcp.d.ts +2 -0
- package/dist/{mcp-server.js → server/mcp.js} +5 -2
- package/dist/utils/date.d.ts +2 -0
- package/dist/utils/date.js +7 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +83 -0
- package/dist/utils/value.d.ts +5 -0
- package/dist/utils/value.js +8 -0
- package/package.json +7 -4
- package/dist/agents.js +0 -557
- package/dist/canvas-session.js +0 -391
- package/dist/http-server.d.ts +0 -1
- package/dist/mcp-server.d.ts +0 -1
- /package/dist/{types.js → agent/codex-protocol.js} +0 -0
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
import type { AgentAttachment, AgentEmit } from "./types.js";
|
|
2
|
-
type Json = Record<string, unknown>;
|
|
1
|
+
import type { AgentAttachment, AgentEmit, AgentPermissionMode } from "./types.js";
|
|
3
2
|
type CodexRunOptions = {
|
|
4
3
|
threadId?: string;
|
|
5
4
|
cwd?: string;
|
|
5
|
+
permissionMode?: AgentPermissionMode;
|
|
6
6
|
appEmit?: AgentEmit;
|
|
7
7
|
onStart?: () => void;
|
|
8
8
|
onThread?: (threadId: string) => void;
|
|
9
9
|
onTurn?: (turnId: string) => void;
|
|
10
10
|
onFinish?: () => void;
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
role: "user" | "assistant" | "tool" | "error";
|
|
15
|
-
title?: string;
|
|
16
|
-
text: string;
|
|
17
|
-
detail?: unknown;
|
|
18
|
-
streamId?: string;
|
|
19
|
-
};
|
|
20
|
-
export declare function withAgentPrompt(prompt: string): string;
|
|
12
|
+
export { summarizeCodexThread } from "./codex-history.js";
|
|
13
|
+
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
21
14
|
export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<void>;
|
|
22
|
-
|
|
23
|
-
export declare function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
/** 中断当前线程正在执行的 Codex turn。 */
|
|
16
|
+
export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
|
|
17
|
+
/** 回复当前 app-server 的待处理权限请求。 */
|
|
18
|
+
export declare function resolveCodexApproval(requestId: string, decision: string): Promise<boolean>;
|
|
19
|
+
/** 创建新的 Codex 线程并记录当前线程 ID。 */
|
|
20
|
+
export declare function startCodexThread(emit: AgentEmit, cwd?: string, permissionMode?: AgentPermissionMode): Promise<import("./codex-protocol.js").CodexThread>;
|
|
21
|
+
/** 恢复指定 Codex 线程并返回聊天历史。 */
|
|
22
|
+
export declare function resumeCodexThread(emit: AgentEmit, threadId: string, cwd?: string, permissionMode?: AgentPermissionMode): Promise<{
|
|
23
|
+
thread: import("./codex-protocol.js").CodexThread;
|
|
24
|
+
messages: {
|
|
25
|
+
id: string;
|
|
26
|
+
role: "user" | "assistant" | "tool" | "error";
|
|
27
|
+
title?: string;
|
|
28
|
+
text: string;
|
|
29
|
+
detail?: unknown;
|
|
30
|
+
streamId?: string;
|
|
31
|
+
}[];
|
|
27
32
|
}>;
|
|
33
|
+
/** 查询当前工作空间中的 Codex 线程。 */
|
|
28
34
|
export declare function listCodexThreads(emit: AgentEmit, options: {
|
|
29
35
|
cwd: string;
|
|
30
36
|
searchTerm?: string;
|
|
@@ -45,6 +51,7 @@ export declare function listCodexThreads(emit: AgentEmit, options: {
|
|
|
45
51
|
nextCursor: {} | null;
|
|
46
52
|
backwardsCursor: {} | null;
|
|
47
53
|
}>;
|
|
54
|
+
/** 读取指定 Codex 线程及其聊天历史。 */
|
|
48
55
|
export declare function readCodexThread(emit: AgentEmit, threadId: string, cwd?: string): Promise<{
|
|
49
56
|
thread: {
|
|
50
57
|
id: string;
|
|
@@ -58,22 +65,18 @@ export declare function readCodexThread(emit: AgentEmit, threadId: string, cwd?:
|
|
|
58
65
|
createdAt: number;
|
|
59
66
|
updatedAt: number;
|
|
60
67
|
};
|
|
61
|
-
messages:
|
|
68
|
+
messages: {
|
|
69
|
+
id: string;
|
|
70
|
+
role: "user" | "assistant" | "tool" | "error";
|
|
71
|
+
title?: string;
|
|
72
|
+
text: string;
|
|
73
|
+
detail?: unknown;
|
|
74
|
+
streamId?: string;
|
|
75
|
+
}[];
|
|
62
76
|
}>;
|
|
77
|
+
/** 确认指定 Codex 线程属于当前工作空间。 */
|
|
63
78
|
export declare function verifyCodexThreadWorkspace(emit: AgentEmit, threadId: string, cwd: string): Promise<void>;
|
|
79
|
+
/** 归档指定 Codex 线程。 */
|
|
64
80
|
export declare function archiveCodexThread(emit: AgentEmit, threadId: string, cwd?: string): Promise<void>;
|
|
65
|
-
|
|
81
|
+
/** 判断线程异常是否允许自动新建线程后重试。 */
|
|
66
82
|
export declare function isRecoverableThreadError(error: unknown): boolean;
|
|
67
|
-
export declare function summarizeCodexThread(thread: unknown): {
|
|
68
|
-
id: string;
|
|
69
|
-
sessionId: string;
|
|
70
|
-
preview: string;
|
|
71
|
-
name: string | null;
|
|
72
|
-
cwd: string;
|
|
73
|
-
status: string;
|
|
74
|
-
source: unknown;
|
|
75
|
-
threadSource: unknown;
|
|
76
|
-
createdAt: number;
|
|
77
|
-
updatedAt: number;
|
|
78
|
-
};
|
|
79
|
-
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { errorMessage, field } from "../utils/value.js";
|
|
6
|
+
import { CodexAppClient } from "./codex-client.js";
|
|
7
|
+
import { summarizeCodexThread, threadMessages } from "./codex-history.js";
|
|
8
|
+
let codexQueue = Promise.resolve();
|
|
9
|
+
let codexApp = null;
|
|
10
|
+
let codexAppStart = null;
|
|
11
|
+
let codexThreadId = "";
|
|
12
|
+
const unmaterializedThreadIds = new Set();
|
|
13
|
+
export { summarizeCodexThread } from "./codex-history.js";
|
|
14
|
+
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
15
|
+
export async function runCodexTurn(prompt, emit, attachments = [], options = {}) {
|
|
16
|
+
if (!prompt.trim())
|
|
17
|
+
return;
|
|
18
|
+
codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments, options));
|
|
19
|
+
await codexQueue;
|
|
20
|
+
}
|
|
21
|
+
/** 中断当前线程正在执行的 Codex turn。 */
|
|
22
|
+
export async function interruptCodexTurn(threadId) {
|
|
23
|
+
if (!codexApp || (threadId && threadId !== codexThreadId))
|
|
24
|
+
return false;
|
|
25
|
+
return await codexApp.interruptCurrentTurn();
|
|
26
|
+
}
|
|
27
|
+
/** 回复当前 app-server 的待处理权限请求。 */
|
|
28
|
+
export async function resolveCodexApproval(requestId, decision) {
|
|
29
|
+
return Boolean(codexApp?.resolveApproval(requestId, decision));
|
|
30
|
+
}
|
|
31
|
+
/** 创建新的 Codex 线程并记录当前线程 ID。 */
|
|
32
|
+
export async function startCodexThread(emit, cwd, permissionMode = "request") {
|
|
33
|
+
const app = await getCodexApp(emit);
|
|
34
|
+
const thread = await app.startThread(cwd, permissionMode);
|
|
35
|
+
codexThreadId = String(field(thread, "id") || "");
|
|
36
|
+
if (codexThreadId)
|
|
37
|
+
unmaterializedThreadIds.add(codexThreadId);
|
|
38
|
+
return thread;
|
|
39
|
+
}
|
|
40
|
+
/** 恢复指定 Codex 线程并返回聊天历史。 */
|
|
41
|
+
export async function resumeCodexThread(emit, threadId, cwd, permissionMode = "request") {
|
|
42
|
+
const app = await getCodexApp(emit);
|
|
43
|
+
await loadCodexThread(emit, threadId, cwd, false);
|
|
44
|
+
const thread = await app.resumeThread(threadId, cwd, permissionMode);
|
|
45
|
+
assertThreadWorkspace(thread, cwd);
|
|
46
|
+
codexThreadId = String(field(thread, "id") || threadId);
|
|
47
|
+
const historyThread = await loadCodexThread(emit, codexThreadId, cwd, true);
|
|
48
|
+
return { thread, messages: threadMessages(historyThread, app.planUpdates(threadId)) };
|
|
49
|
+
}
|
|
50
|
+
/** 查询当前工作空间中的 Codex 线程。 */
|
|
51
|
+
export async function listCodexThreads(emit, options) {
|
|
52
|
+
const app = await getCodexApp(emit);
|
|
53
|
+
const result = await app.listThreads({
|
|
54
|
+
limit: options.limit || 40,
|
|
55
|
+
sortKey: "updated_at",
|
|
56
|
+
sortDirection: "desc",
|
|
57
|
+
sourceKinds: ["cli", "vscode", "appServer", "exec"],
|
|
58
|
+
cwd: options.cwd,
|
|
59
|
+
...(options.searchTerm ? { searchTerm: options.searchTerm } : {}),
|
|
60
|
+
});
|
|
61
|
+
const data = Array.isArray(field(result, "data")) ? field(result, "data").map(summarizeCodexThread).filter((thread) => threadInWorkspace(thread, options.cwd)) : [];
|
|
62
|
+
return { data, nextCursor: field(result, "nextCursor") || null, backwardsCursor: field(result, "backwardsCursor") || null };
|
|
63
|
+
}
|
|
64
|
+
/** 读取指定 Codex 线程及其聊天历史。 */
|
|
65
|
+
export async function readCodexThread(emit, threadId, cwd) {
|
|
66
|
+
const app = await getCodexApp(emit);
|
|
67
|
+
let thread;
|
|
68
|
+
try {
|
|
69
|
+
thread = await loadCodexThread(emit, threadId, cwd, !unmaterializedThreadIds.has(threadId));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!/not materialized yet.*includeTurns/i.test(errorMessage(error)))
|
|
73
|
+
throw error;
|
|
74
|
+
unmaterializedThreadIds.add(threadId);
|
|
75
|
+
thread = await loadCodexThread(emit, threadId, cwd, false);
|
|
76
|
+
}
|
|
77
|
+
return { thread: summarizeCodexThread(thread), messages: threadMessages(thread, app.planUpdates(threadId)) };
|
|
78
|
+
}
|
|
79
|
+
/** 确认指定 Codex 线程属于当前工作空间。 */
|
|
80
|
+
export async function verifyCodexThreadWorkspace(emit, threadId, cwd) {
|
|
81
|
+
await loadCodexThread(emit, threadId, cwd, false);
|
|
82
|
+
}
|
|
83
|
+
/** 归档指定 Codex 线程。 */
|
|
84
|
+
export async function archiveCodexThread(emit, threadId, cwd) {
|
|
85
|
+
const app = await getCodexApp(emit);
|
|
86
|
+
await loadCodexThread(emit, threadId, cwd, false);
|
|
87
|
+
await app.archiveThread(threadId);
|
|
88
|
+
app.clearPlanUpdates(threadId);
|
|
89
|
+
unmaterializedThreadIds.delete(threadId);
|
|
90
|
+
}
|
|
91
|
+
/** 判断线程异常是否允许自动新建线程后重试。 */
|
|
92
|
+
export function isRecoverableThreadError(error) {
|
|
93
|
+
return /thread not loaded|no rollout found/i.test(errorMessage(error));
|
|
94
|
+
}
|
|
95
|
+
/** 执行一次 Codex turn,并负责附件临时文件和线程恢复。 */
|
|
96
|
+
async function runCodexTurnNow(prompt, emit, attachments, options) {
|
|
97
|
+
let files = [];
|
|
98
|
+
try {
|
|
99
|
+
options.onStart?.();
|
|
100
|
+
files = await writeAttachmentFiles(attachments);
|
|
101
|
+
const app = await getCodexApp(options.appEmit || emit);
|
|
102
|
+
let threadId = await ensureCodexThread(app, options, emit);
|
|
103
|
+
options.onThread?.(threadId);
|
|
104
|
+
unmaterializedThreadIds.delete(threadId);
|
|
105
|
+
try {
|
|
106
|
+
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!isRecoverableThreadError(error))
|
|
110
|
+
throw error;
|
|
111
|
+
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
|
|
112
|
+
codexThreadId = "";
|
|
113
|
+
threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit);
|
|
114
|
+
options.onThread?.(threadId);
|
|
115
|
+
unmaterializedThreadIds.delete(threadId);
|
|
116
|
+
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger.error("Codex turn failed", error);
|
|
121
|
+
emit("agent_error", { message: errorMessage(error) });
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
options.onFinish?.();
|
|
125
|
+
await Promise.all(files.map((file) => fs.unlink(file).catch(() => undefined)));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** 恢复请求线程或创建新的 Codex 线程。 */
|
|
129
|
+
async function ensureCodexThread(app, options, emit) {
|
|
130
|
+
if (options.threadId) {
|
|
131
|
+
if (options.threadId === codexThreadId)
|
|
132
|
+
return codexThreadId;
|
|
133
|
+
try {
|
|
134
|
+
const result = await app.readThread(options.threadId, false);
|
|
135
|
+
assertThreadWorkspace(field(result, "thread") || {}, options.cwd);
|
|
136
|
+
const thread = await app.resumeThread(options.threadId, options.cwd, options.permissionMode || "request");
|
|
137
|
+
assertThreadWorkspace(thread, options.cwd);
|
|
138
|
+
codexThreadId = String(field(thread, "id") || options.threadId);
|
|
139
|
+
return codexThreadId;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (!isRecoverableThreadError(error))
|
|
143
|
+
throw error;
|
|
144
|
+
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!codexThreadId) {
|
|
148
|
+
const thread = await app.startThread(options.cwd, options.permissionMode || "request");
|
|
149
|
+
codexThreadId = String(field(thread, "id") || "");
|
|
150
|
+
if (codexThreadId)
|
|
151
|
+
unmaterializedThreadIds.add(codexThreadId);
|
|
152
|
+
}
|
|
153
|
+
return codexThreadId;
|
|
154
|
+
}
|
|
155
|
+
/** 从 app-server 读取线程并校验工作空间。 */
|
|
156
|
+
async function loadCodexThread(emit, threadId, cwd, includeTurns) {
|
|
157
|
+
const app = await getCodexApp(emit);
|
|
158
|
+
const result = await app.readThread(threadId, includeTurns);
|
|
159
|
+
const thread = field(result, "thread") || {};
|
|
160
|
+
assertThreadWorkspace(thread, cwd);
|
|
161
|
+
return thread;
|
|
162
|
+
}
|
|
163
|
+
/** 获取已启动的 Codex app-server 客户端。 */
|
|
164
|
+
async function getCodexApp(emit) {
|
|
165
|
+
if (codexApp)
|
|
166
|
+
return codexApp;
|
|
167
|
+
codexAppStart ||= CodexAppClient.start(emit, () => {
|
|
168
|
+
codexApp = null;
|
|
169
|
+
codexThreadId = "";
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
codexApp = await codexAppStart;
|
|
173
|
+
return codexApp;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
codexAppStart = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** 校验线程是否属于指定工作空间。 */
|
|
180
|
+
function assertThreadWorkspace(thread, cwd) {
|
|
181
|
+
if (!cwd || threadInWorkspace(thread, cwd))
|
|
182
|
+
return;
|
|
183
|
+
throw new Error("该 Codex 会话不属于当前画布工作空间");
|
|
184
|
+
}
|
|
185
|
+
/** 判断线程工作目录是否与当前工作空间一致。 */
|
|
186
|
+
function threadInWorkspace(thread, cwd) {
|
|
187
|
+
const threadCwd = String(field(thread, "cwd") || "");
|
|
188
|
+
return Boolean(threadCwd && path.resolve(threadCwd) === path.resolve(cwd));
|
|
189
|
+
}
|
|
190
|
+
/** 将图片附件写入临时文件供 Codex 读取。 */
|
|
191
|
+
async function writeAttachmentFiles(attachments) {
|
|
192
|
+
return await Promise.all(attachments.filter((item) => item.dataUrl?.startsWith("data:image/")).map(writeAttachmentFile));
|
|
193
|
+
}
|
|
194
|
+
/** 将单个 Data URL 图片附件写入临时文件。 */
|
|
195
|
+
async function writeAttachmentFile(item) {
|
|
196
|
+
const [, meta = "", data = ""] = item.dataUrl?.match(/^data:([^;]+);base64,(.+)$/) || [];
|
|
197
|
+
if (!data)
|
|
198
|
+
throw new Error(`图片附件无效:${item.name || "未命名图片"}`);
|
|
199
|
+
const file = path.join(os.tmpdir(), `infinite-canvas-${Date.now()}-${Math.random().toString(16).slice(2)}.${imageExt(meta || item.type)}`);
|
|
200
|
+
await fs.writeFile(file, Buffer.from(data, "base64"));
|
|
201
|
+
return file;
|
|
202
|
+
}
|
|
203
|
+
/** 根据图片 MIME 类型返回临时文件扩展名。 */
|
|
204
|
+
function imageExt(type = "") {
|
|
205
|
+
if (type.includes("png"))
|
|
206
|
+
return "png";
|
|
207
|
+
if (type.includes("webp"))
|
|
208
|
+
return "webp";
|
|
209
|
+
return "jpg";
|
|
210
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Agent 向网页广播事件的函数类型。 */
|
|
2
|
+
export type AgentEmit = (type: string, payload: unknown) => void;
|
|
3
|
+
/** 用户随当前 Agent 消息上传的附件。 */
|
|
4
|
+
export type AgentAttachment = {
|
|
5
|
+
id?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
dataUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Codex 文件、命令和网络权限模式。 */
|
|
14
|
+
export type AgentPermissionMode = "request" | "automatic" | "full";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolName } from "./schemas.js";
|
|
2
|
+
import type { CanvasSnapshot } from "./types.js";
|
|
3
|
+
export type CanvasToolRequest = {
|
|
4
|
+
name: "canvas_apply_ops";
|
|
5
|
+
input: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
/** 将上层画布工具调用转换为前端可执行的批量操作。 */
|
|
8
|
+
export declare function buildCanvasToolRequest(name: ToolName, input: Record<string, unknown>, state: CanvasSnapshot | null): CanvasToolRequest;
|
|
9
|
+
/** 按最大边限制计算附件图片节点尺寸,并保持原始比例。 */
|
|
10
|
+
export declare function fitAttachmentNodeSize(width: number, height: number): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { nextCanvasX } from "./tools.js";
|
|
3
|
+
/** 将上层画布工具调用转换为前端可执行的批量操作。 */
|
|
4
|
+
export function buildCanvasToolRequest(name, input, state) {
|
|
5
|
+
if (name === "canvas_apply_ops")
|
|
6
|
+
return { name, input };
|
|
7
|
+
if (name === "canvas_create_node") {
|
|
8
|
+
const data = input;
|
|
9
|
+
return applyOps([{ type: "add_node", nodeType: data.nodeType, title: data.title, position: { x: data.x ?? nextCanvasX(state), y: data.y ?? 0 }, width: data.width, height: data.height, metadata: data.metadata }]);
|
|
10
|
+
}
|
|
11
|
+
if (name === "canvas_create_text_node") {
|
|
12
|
+
const data = input;
|
|
13
|
+
return applyOps([textNodeOp(data, data.x ?? nextCanvasX(state), data.y ?? 0)]);
|
|
14
|
+
}
|
|
15
|
+
if (name === "canvas_create_text_nodes") {
|
|
16
|
+
const data = input;
|
|
17
|
+
const x = Number(data.x ?? nextCanvasX(state));
|
|
18
|
+
const y = Number(data.y ?? 0);
|
|
19
|
+
const gap = Number(data.gap ?? 40);
|
|
20
|
+
return applyOps(data.items.map((item, index) => textNodeOp(item, item.x ?? (data.direction === "row" ? x + index * (340 + gap) : x), item.y ?? (data.direction === "row" ? y : y + index * (240 + gap)))));
|
|
21
|
+
}
|
|
22
|
+
if (name === "canvas_create_image_prompt_flow")
|
|
23
|
+
return applyOps(generationFlowOps({ ...input, mode: "image" }, state));
|
|
24
|
+
if (name === "canvas_create_config_node") {
|
|
25
|
+
const x = Number(input.x ?? nextCanvasX(state));
|
|
26
|
+
const y = Number(input.y ?? 0);
|
|
27
|
+
const configId = `config-${crypto.randomUUID()}`;
|
|
28
|
+
const mode = generationMode(input.mode);
|
|
29
|
+
const prompt = String(input.prompt || "");
|
|
30
|
+
return applyOps([configNodeOp(configId, input, x, y), ...(input.autoRun ? [runGenerationOp(configId, mode, prompt)] : [])]);
|
|
31
|
+
}
|
|
32
|
+
if (name === "canvas_create_generation_flow")
|
|
33
|
+
return applyOps(generationFlowOps(input, state));
|
|
34
|
+
if (name === "canvas_generate_text" || name === "canvas_generate_image" || name === "canvas_generate_video" || name === "canvas_generate_audio") {
|
|
35
|
+
return applyOps(generationFlowOps({ ...input, mode: name.replace("canvas_generate_", ""), autoRun: true }, state));
|
|
36
|
+
}
|
|
37
|
+
if (name === "canvas_update_node") {
|
|
38
|
+
const data = input;
|
|
39
|
+
return applyOps([{ type: "update_node", id: data.id, patch: data.patch, metadata: data.metadata }]);
|
|
40
|
+
}
|
|
41
|
+
if (name === "canvas_update_node_text") {
|
|
42
|
+
const data = input;
|
|
43
|
+
return applyOps([{ type: "update_node", id: data.id, patch: { ...(data.title ? { title: data.title } : {}) }, metadata: { content: data.text, status: "success" } }]);
|
|
44
|
+
}
|
|
45
|
+
if (name === "canvas_move_nodes") {
|
|
46
|
+
const data = input;
|
|
47
|
+
return applyOps(data.items.map((item) => {
|
|
48
|
+
const current = findNode(state, item.id);
|
|
49
|
+
return { type: "update_node", id: item.id, patch: { position: { x: item.x ?? ((current?.position.x || 0) + (item.dx || 0)), y: item.y ?? ((current?.position.y || 0) + (item.dy || 0)) } } };
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
if (name === "canvas_resize_node") {
|
|
53
|
+
const data = input;
|
|
54
|
+
return applyOps([{ type: "update_node", id: data.id, patch: { width: data.width, height: data.height }, metadata: data.freeResize === undefined ? undefined : { freeResize: data.freeResize } }]);
|
|
55
|
+
}
|
|
56
|
+
if (name === "canvas_delete_nodes")
|
|
57
|
+
return applyOps([{ type: "delete_node", ids: input.ids }]);
|
|
58
|
+
if (name === "canvas_connect_nodes") {
|
|
59
|
+
const data = input;
|
|
60
|
+
return applyOps(data.connections.map((connection) => ({ type: "connect_nodes", ...connection })));
|
|
61
|
+
}
|
|
62
|
+
if (name === "canvas_select_nodes")
|
|
63
|
+
return applyOps([{ type: "select_nodes", ids: input.ids }]);
|
|
64
|
+
if (name === "canvas_set_viewport")
|
|
65
|
+
return applyOps([{ type: "set_viewport", viewport: input.viewport }]);
|
|
66
|
+
if (name === "canvas_run_generation") {
|
|
67
|
+
const data = input;
|
|
68
|
+
return applyOps([runGenerationOp(data.nodeId, generationMode(data.mode), data.prompt)]);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`未知工具:${name}`);
|
|
71
|
+
}
|
|
72
|
+
/** 按最大边限制计算附件图片节点尺寸,并保持原始比例。 */
|
|
73
|
+
export function fitAttachmentNodeSize(width, height) {
|
|
74
|
+
const scale = Math.min(1, 640 / width, 640 / height);
|
|
75
|
+
return { width: width * scale, height: height * scale };
|
|
76
|
+
}
|
|
77
|
+
/** 创建统一的批量画布操作请求。 */
|
|
78
|
+
function applyOps(ops) {
|
|
79
|
+
return { name: "canvas_apply_ops", input: { ops } };
|
|
80
|
+
}
|
|
81
|
+
/** 创建文本节点操作。 */
|
|
82
|
+
function textNodeOp(input, x, y) {
|
|
83
|
+
return { type: "add_node", id: input.id, nodeType: "text", title: input.title, position: { x, y }, width: input.width, height: input.height, metadata: { content: input.text || "", status: "success", fontSize: 14 } };
|
|
84
|
+
}
|
|
85
|
+
/** 创建生成配置节点操作。 */
|
|
86
|
+
function configNodeOp(id, input, x, y) {
|
|
87
|
+
const mode = generationMode(input.mode);
|
|
88
|
+
const prompt = String(input.prompt || "");
|
|
89
|
+
return {
|
|
90
|
+
type: "add_node",
|
|
91
|
+
id,
|
|
92
|
+
nodeType: "config",
|
|
93
|
+
title: String(input.title || generationTitle(mode)),
|
|
94
|
+
position: { x, y },
|
|
95
|
+
width: typeof input.width === "number" ? input.width : undefined,
|
|
96
|
+
height: typeof input.height === "number" ? input.height : undefined,
|
|
97
|
+
metadata: cleanRecord({
|
|
98
|
+
generationMode: mode,
|
|
99
|
+
composerContent: prompt,
|
|
100
|
+
prompt,
|
|
101
|
+
status: "idle",
|
|
102
|
+
model: input.model,
|
|
103
|
+
size: input.size,
|
|
104
|
+
quality: input.quality,
|
|
105
|
+
count: input.count,
|
|
106
|
+
seconds: input.seconds,
|
|
107
|
+
vquality: input.vquality,
|
|
108
|
+
generateAudio: input.generateAudio,
|
|
109
|
+
watermark: input.watermark,
|
|
110
|
+
audioVoice: input.audioVoice,
|
|
111
|
+
audioFormat: input.audioFormat,
|
|
112
|
+
audioSpeed: input.audioSpeed,
|
|
113
|
+
audioInstructions: input.audioInstructions,
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** 创建包含提示词、配置节点和引用连线的生成流程。 */
|
|
118
|
+
function generationFlowOps(input, state) {
|
|
119
|
+
const mode = generationMode(input.mode);
|
|
120
|
+
const prompt = String(input.prompt || "");
|
|
121
|
+
const x = Number(input.x ?? nextCanvasX(state));
|
|
122
|
+
const y = Number(input.y ?? 0);
|
|
123
|
+
const textId = `text-${crypto.randomUUID()}`;
|
|
124
|
+
const configId = `config-${crypto.randomUUID()}`;
|
|
125
|
+
const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id) => typeof id === "string") : [];
|
|
126
|
+
const tokens = [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)];
|
|
127
|
+
return [
|
|
128
|
+
textNodeOp({ id: textId, text: prompt, title: String(input.title || "提示词") }, x, y),
|
|
129
|
+
configNodeOp(configId, { ...input, prompt: tokens.join("\n") }, x + 420, y),
|
|
130
|
+
{ type: "connect_nodes", fromNodeId: textId, toNodeId: configId },
|
|
131
|
+
...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: configId })),
|
|
132
|
+
{ type: "select_nodes", ids: [configId] },
|
|
133
|
+
...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/** 创建触发节点生成的画布操作。 */
|
|
137
|
+
function runGenerationOp(nodeId, mode, prompt) {
|
|
138
|
+
return { type: "run_generation", nodeId, mode, prompt };
|
|
139
|
+
}
|
|
140
|
+
/** 将未知生成模式归一为画布支持的模式。 */
|
|
141
|
+
function generationMode(value) {
|
|
142
|
+
return value === "text" || value === "video" || value === "audio" ? value : "image";
|
|
143
|
+
}
|
|
144
|
+
/** 获取生成模式对应的默认节点标题。 */
|
|
145
|
+
function generationTitle(mode) {
|
|
146
|
+
if (mode === "text")
|
|
147
|
+
return "文本生成";
|
|
148
|
+
if (mode === "video")
|
|
149
|
+
return "视频生成";
|
|
150
|
+
if (mode === "audio")
|
|
151
|
+
return "音频生成";
|
|
152
|
+
return "图片生成";
|
|
153
|
+
}
|
|
154
|
+
/** 按节点 ID 查找当前画布节点。 */
|
|
155
|
+
function findNode(state, id) {
|
|
156
|
+
return (state?.nodes || []).find((node) => node.id === id);
|
|
157
|
+
}
|
|
158
|
+
/** 移除对象中未设置的生成参数。 */
|
|
159
|
+
function cleanRecord(value) {
|
|
160
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== ""));
|
|
161
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Canvas Agent 对外提供的工具名称。 */
|
|
2
3
|
export declare const toolNames: readonly ["site_navigate", "canvas_list_projects", "canvas_get_state", "canvas_get_selection", "canvas_export_snapshot", "canvas_export_media", "canvas_apply_ops", "canvas_create_node", "canvas_create_attachment_nodes", "canvas_create_text_node", "canvas_create_text_nodes", "canvas_create_config_node", "canvas_create_image_prompt_flow", "canvas_create_generation_flow", "canvas_generate_text", "canvas_generate_image", "canvas_generate_video", "canvas_generate_audio", "canvas_update_node", "canvas_update_node_text", "canvas_move_nodes", "canvas_resize_node", "canvas_delete_nodes", "canvas_connect_nodes", "canvas_select_nodes", "canvas_set_viewport", "canvas_run_generation", "generation_get_status", "workbench_image_get_config", "workbench_image_generate", "workbench_video_get_config", "workbench_video_generate", "prompts_search", "assets_list", "assets_add"];
|
|
3
4
|
export type ToolName = (typeof toolNames)[number];
|
|
4
5
|
export declare const canvasOpSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
@@ -545,18 +546,18 @@ export declare const toolInputSchemas: {
|
|
|
545
546
|
height: z.ZodOptional<z.ZodNumber>;
|
|
546
547
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
547
548
|
}, "strip", z.ZodTypeAny, {
|
|
548
|
-
nodeType: "
|
|
549
|
+
nodeType: "text" | "image" | "config" | "video" | "audio";
|
|
550
|
+
title?: string | undefined;
|
|
549
551
|
x?: number | undefined;
|
|
550
552
|
y?: number | undefined;
|
|
551
|
-
title?: string | undefined;
|
|
552
553
|
width?: number | undefined;
|
|
553
554
|
height?: number | undefined;
|
|
554
555
|
metadata?: Record<string, unknown> | undefined;
|
|
555
556
|
}, {
|
|
556
|
-
nodeType: "
|
|
557
|
+
nodeType: "text" | "image" | "config" | "video" | "audio";
|
|
558
|
+
title?: string | undefined;
|
|
557
559
|
x?: number | undefined;
|
|
558
560
|
y?: number | undefined;
|
|
559
|
-
title?: string | undefined;
|
|
560
561
|
width?: number | undefined;
|
|
561
562
|
height?: number | undefined;
|
|
562
563
|
metadata?: Record<string, unknown> | undefined;
|
|
@@ -589,16 +590,16 @@ export declare const toolInputSchemas: {
|
|
|
589
590
|
height: z.ZodOptional<z.ZodNumber>;
|
|
590
591
|
}, "strip", z.ZodTypeAny, {
|
|
591
592
|
text?: string | undefined;
|
|
593
|
+
title?: string | undefined;
|
|
592
594
|
x?: number | undefined;
|
|
593
595
|
y?: number | undefined;
|
|
594
|
-
title?: string | undefined;
|
|
595
596
|
width?: number | undefined;
|
|
596
597
|
height?: number | undefined;
|
|
597
598
|
}, {
|
|
598
599
|
text?: string | undefined;
|
|
600
|
+
title?: string | undefined;
|
|
599
601
|
x?: number | undefined;
|
|
600
602
|
y?: number | undefined;
|
|
601
|
-
title?: string | undefined;
|
|
602
603
|
width?: number | undefined;
|
|
603
604
|
height?: number | undefined;
|
|
604
605
|
}>;
|
|
@@ -612,16 +613,16 @@ export declare const toolInputSchemas: {
|
|
|
612
613
|
height: z.ZodOptional<z.ZodNumber>;
|
|
613
614
|
}, "strip", z.ZodTypeAny, {
|
|
614
615
|
text: string;
|
|
616
|
+
title?: string | undefined;
|
|
615
617
|
x?: number | undefined;
|
|
616
618
|
y?: number | undefined;
|
|
617
|
-
title?: string | undefined;
|
|
618
619
|
width?: number | undefined;
|
|
619
620
|
height?: number | undefined;
|
|
620
621
|
}, {
|
|
621
622
|
text: string;
|
|
623
|
+
title?: string | undefined;
|
|
622
624
|
x?: number | undefined;
|
|
623
625
|
y?: number | undefined;
|
|
624
|
-
title?: string | undefined;
|
|
625
626
|
width?: number | undefined;
|
|
626
627
|
height?: number | undefined;
|
|
627
628
|
}>, "many">;
|
|
@@ -632,9 +633,9 @@ export declare const toolInputSchemas: {
|
|
|
632
633
|
}, "strip", z.ZodTypeAny, {
|
|
633
634
|
items: {
|
|
634
635
|
text: string;
|
|
636
|
+
title?: string | undefined;
|
|
635
637
|
x?: number | undefined;
|
|
636
638
|
y?: number | undefined;
|
|
637
|
-
title?: string | undefined;
|
|
638
639
|
width?: number | undefined;
|
|
639
640
|
height?: number | undefined;
|
|
640
641
|
}[];
|
|
@@ -645,9 +646,9 @@ export declare const toolInputSchemas: {
|
|
|
645
646
|
}, {
|
|
646
647
|
items: {
|
|
647
648
|
text: string;
|
|
649
|
+
title?: string | undefined;
|
|
648
650
|
x?: number | undefined;
|
|
649
651
|
y?: number | undefined;
|
|
650
|
-
title?: string | undefined;
|
|
651
652
|
width?: number | undefined;
|
|
652
653
|
height?: number | undefined;
|
|
653
654
|
}[];
|
|
@@ -853,11 +854,11 @@ export declare const toolInputSchemas: {
|
|
|
853
854
|
prompt: z.ZodOptional<z.ZodString>;
|
|
854
855
|
}, "strip", z.ZodTypeAny, {
|
|
855
856
|
nodeId: string;
|
|
856
|
-
mode?: "
|
|
857
|
+
mode?: "text" | "image" | "video" | "audio" | undefined;
|
|
857
858
|
prompt?: string | undefined;
|
|
858
859
|
}, {
|
|
859
860
|
nodeId: string;
|
|
860
|
-
mode?: "
|
|
861
|
+
mode?: "text" | "image" | "video" | "audio" | undefined;
|
|
861
862
|
prompt?: string | undefined;
|
|
862
863
|
}>;
|
|
863
864
|
generation_get_status: z.ZodObject<{
|
|
@@ -866,15 +867,15 @@ export declare const toolInputSchemas: {
|
|
|
866
867
|
nodeIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
867
868
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
868
869
|
}, "strip", z.ZodTypeAny, {
|
|
869
|
-
limit?: number | undefined;
|
|
870
870
|
nodeIds?: string[] | undefined;
|
|
871
871
|
scope?: "image" | "video" | "all" | "canvas" | undefined;
|
|
872
872
|
taskId?: string | undefined;
|
|
873
|
-
}, {
|
|
874
873
|
limit?: number | undefined;
|
|
874
|
+
}, {
|
|
875
875
|
nodeIds?: string[] | undefined;
|
|
876
876
|
scope?: "image" | "video" | "all" | "canvas" | undefined;
|
|
877
877
|
taskId?: string | undefined;
|
|
878
|
+
limit?: number | undefined;
|
|
878
879
|
}>;
|
|
879
880
|
workbench_image_get_config: z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>;
|
|
880
881
|
workbench_image_generate: z.ZodObject<{
|
|
@@ -886,17 +887,17 @@ export declare const toolInputSchemas: {
|
|
|
886
887
|
run: z.ZodOptional<z.ZodBoolean>;
|
|
887
888
|
}, "strip", z.ZodTypeAny, {
|
|
888
889
|
prompt: string;
|
|
890
|
+
count?: number | undefined;
|
|
889
891
|
model?: string | undefined;
|
|
890
892
|
size?: string | undefined;
|
|
891
893
|
quality?: string | undefined;
|
|
892
|
-
count?: number | undefined;
|
|
893
894
|
run?: boolean | undefined;
|
|
894
895
|
}, {
|
|
895
896
|
prompt: string;
|
|
897
|
+
count?: number | undefined;
|
|
896
898
|
model?: string | undefined;
|
|
897
899
|
size?: string | undefined;
|
|
898
900
|
quality?: string | undefined;
|
|
899
|
-
count?: number | undefined;
|
|
900
901
|
run?: boolean | undefined;
|
|
901
902
|
}>;
|
|
902
903
|
workbench_video_get_config: z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>;
|
|
@@ -953,15 +954,15 @@ export declare const toolInputSchemas: {
|
|
|
953
954
|
page: z.ZodOptional<z.ZodNumber>;
|
|
954
955
|
pageSize: z.ZodOptional<z.ZodNumber>;
|
|
955
956
|
}, "strip", z.ZodTypeAny, {
|
|
957
|
+
kind?: "text" | "image" | "video" | "all" | undefined;
|
|
956
958
|
keyword?: string | undefined;
|
|
957
959
|
page?: number | undefined;
|
|
958
960
|
pageSize?: number | undefined;
|
|
959
|
-
kind?: "image" | "text" | "video" | "all" | undefined;
|
|
960
961
|
}, {
|
|
962
|
+
kind?: "text" | "image" | "video" | "all" | undefined;
|
|
961
963
|
keyword?: string | undefined;
|
|
962
964
|
page?: number | undefined;
|
|
963
965
|
pageSize?: number | undefined;
|
|
964
|
-
kind?: "image" | "text" | "video" | "all" | undefined;
|
|
965
966
|
}>;
|
|
966
967
|
assets_add: z.ZodObject<{
|
|
967
968
|
kind: z.ZodEnum<["text", "image"]>;
|
|
@@ -973,7 +974,7 @@ export declare const toolInputSchemas: {
|
|
|
973
974
|
note: z.ZodOptional<z.ZodString>;
|
|
974
975
|
}, "strip", z.ZodTypeAny, {
|
|
975
976
|
title: string;
|
|
976
|
-
kind: "
|
|
977
|
+
kind: "text" | "image";
|
|
977
978
|
source?: string | undefined;
|
|
978
979
|
content?: string | undefined;
|
|
979
980
|
tags?: string[] | undefined;
|
|
@@ -981,7 +982,7 @@ export declare const toolInputSchemas: {
|
|
|
981
982
|
note?: string | undefined;
|
|
982
983
|
}, {
|
|
983
984
|
title: string;
|
|
984
|
-
kind: "
|
|
985
|
+
kind: "text" | "image";
|
|
985
986
|
source?: string | undefined;
|
|
986
987
|
content?: string | undefined;
|
|
987
988
|
tags?: string[] | undefined;
|