@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
|
@@ -4,6 +4,7 @@ const positionSchema = z.object({ x: z.number(), y: z.number() });
|
|
|
4
4
|
const viewportSchema = z.object({ x: z.number(), y: z.number(), k: z.number() });
|
|
5
5
|
const nodeTypeSchema = z.enum(["image", "text", "config", "video", "audio"]);
|
|
6
6
|
const generationModeSchema = z.enum(["text", "image", "video", "audio"]);
|
|
7
|
+
/** Canvas Agent 对外提供的工具名称。 */
|
|
7
8
|
export const toolNames = [
|
|
8
9
|
"site_navigate",
|
|
9
10
|
"canvas_list_projects",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
|
-
import type { AgentAttachment } from "
|
|
2
|
+
import type { AgentAttachment } from "../agent/types.js";
|
|
3
3
|
type TurnAttachment = {
|
|
4
4
|
clientId: string;
|
|
5
5
|
id: string;
|
|
@@ -15,6 +15,7 @@ export type CodexState = {
|
|
|
15
15
|
threadId: string;
|
|
16
16
|
turnId: string;
|
|
17
17
|
};
|
|
18
|
+
/** 管理网页画布连接、状态、附件和工具请求。 */
|
|
18
19
|
export declare class CanvasSession {
|
|
19
20
|
private clients;
|
|
20
21
|
private clientFocusOrder;
|
|
@@ -25,21 +26,32 @@ export declare class CanvasSession {
|
|
|
25
26
|
private boundClientId;
|
|
26
27
|
private focusSequence;
|
|
27
28
|
private codexState;
|
|
29
|
+
/** 获取当前目标网页的画布状态。 */
|
|
28
30
|
private get canvasState();
|
|
31
|
+
/** 获取当前 turn 绑定或最近激活的网页客户端。 */
|
|
29
32
|
private get targetClientId();
|
|
33
|
+
/** 返回 Canvas Agent 当前连接状态。 */
|
|
30
34
|
health(): {
|
|
31
35
|
ok: boolean;
|
|
32
36
|
hasCanvas: boolean;
|
|
33
37
|
clients: number;
|
|
34
38
|
codexBusy: boolean;
|
|
35
39
|
};
|
|
40
|
+
/** 返回 Codex 是否正在执行任务。 */
|
|
36
41
|
get codexBusy(): boolean;
|
|
42
|
+
/** 更新并广播 Codex 运行状态。 */
|
|
37
43
|
setCodexState(patch: Partial<CodexState>): void;
|
|
44
|
+
/** 建立网页与 Canvas Agent 之间的 SSE 连接。 */
|
|
38
45
|
openEvents(url: URL, res: ServerResponse): void;
|
|
46
|
+
/** 保存指定网页上报的最新画布快照。 */
|
|
39
47
|
updateState(body: unknown, clientId?: string): void;
|
|
48
|
+
/** 将指定网页设为最近激活的工具目标。 */
|
|
40
49
|
activateClient(clientId: string): void;
|
|
50
|
+
/** 将当前 Agent turn 固定绑定到指定网页。 */
|
|
41
51
|
bindClient(clientId: string): void;
|
|
52
|
+
/** 解除当前 Agent turn 的网页绑定。 */
|
|
42
53
|
releaseClient(clientId: string): void;
|
|
54
|
+
/** 保存当前 turn 可用的图片附件并返回安全引用。 */
|
|
43
55
|
setTurnAttachments(clientId: string, attachments: AgentAttachment[]): {
|
|
44
56
|
id: string;
|
|
45
57
|
name: string;
|
|
@@ -48,17 +60,25 @@ export declare class CanvasSession {
|
|
|
48
60
|
width: number;
|
|
49
61
|
height: number;
|
|
50
62
|
}[];
|
|
63
|
+
/** 清理指定网页或全部 turn 附件。 */
|
|
51
64
|
clearTurnAttachments(clientId?: string): void;
|
|
65
|
+
/** 获取属于指定网页 turn 的图片附件。 */
|
|
52
66
|
getTurnAttachment(clientId: string, attachmentId: string): TurnAttachment;
|
|
67
|
+
/** 接收网页返回的工具调用结果。 */
|
|
53
68
|
resolveResult(clientId: string, body: {
|
|
54
69
|
requestId?: string;
|
|
55
70
|
error?: string;
|
|
56
71
|
result?: unknown;
|
|
57
72
|
}): boolean;
|
|
73
|
+
/** 向全部已连接网页广播事件。 */
|
|
58
74
|
emitAll(type: string, payload: unknown): void;
|
|
75
|
+
/** 向全部网页广播带线程归属的事件。 */
|
|
59
76
|
emitThread(type: string, threadId: string, payload?: Record<string, unknown>): void;
|
|
77
|
+
/** 校验工具参数并将调用分派到当前目标网页。 */
|
|
60
78
|
callTool(name: unknown, rawInput: unknown): Promise<unknown>;
|
|
79
|
+
/** 将当前 turn 的附件转换为画布图片节点。 */
|
|
61
80
|
private createAttachmentNodes;
|
|
81
|
+
/** 向目标网页发送工具请求并等待调用结果。 */
|
|
62
82
|
private requestCanvasTool;
|
|
63
83
|
}
|
|
64
84
|
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { buildCanvasToolRequest, fitAttachmentNodeSize } from "./operations.js";
|
|
4
|
+
import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js";
|
|
5
|
+
const SITE_TOOLS = new Set([
|
|
6
|
+
"site_navigate",
|
|
7
|
+
"canvas_list_projects",
|
|
8
|
+
"workbench_image_get_config",
|
|
9
|
+
"workbench_image_generate",
|
|
10
|
+
"workbench_video_get_config",
|
|
11
|
+
"workbench_video_generate",
|
|
12
|
+
"prompts_search",
|
|
13
|
+
"assets_list",
|
|
14
|
+
"assets_add",
|
|
15
|
+
"generation_get_status",
|
|
16
|
+
]);
|
|
17
|
+
/** 管理网页画布连接、状态、附件和工具请求。 */
|
|
18
|
+
export class CanvasSession {
|
|
19
|
+
clients = new Map();
|
|
20
|
+
clientFocusOrder = new Map();
|
|
21
|
+
pending = new Map();
|
|
22
|
+
canvasStates = new Map();
|
|
23
|
+
turnAttachments = new Map();
|
|
24
|
+
activeClientId = "";
|
|
25
|
+
boundClientId = "";
|
|
26
|
+
focusSequence = 0;
|
|
27
|
+
codexState = { busy: false, threadId: "", turnId: "" };
|
|
28
|
+
/** 获取当前目标网页的画布状态。 */
|
|
29
|
+
get canvasState() {
|
|
30
|
+
return this.canvasStates.get(this.targetClientId) || null;
|
|
31
|
+
}
|
|
32
|
+
/** 获取当前 turn 绑定或最近激活的网页客户端。 */
|
|
33
|
+
get targetClientId() {
|
|
34
|
+
return this.boundClientId || this.activeClientId;
|
|
35
|
+
}
|
|
36
|
+
/** 返回 Canvas Agent 当前连接状态。 */
|
|
37
|
+
health() {
|
|
38
|
+
return { ok: true, hasCanvas: Boolean(this.canvasState), clients: this.clients.size, codexBusy: this.codexState.busy };
|
|
39
|
+
}
|
|
40
|
+
/** 返回 Codex 是否正在执行任务。 */
|
|
41
|
+
get codexBusy() {
|
|
42
|
+
return this.codexState.busy;
|
|
43
|
+
}
|
|
44
|
+
/** 更新并广播 Codex 运行状态。 */
|
|
45
|
+
setCodexState(patch) {
|
|
46
|
+
const next = { ...this.codexState, ...patch };
|
|
47
|
+
if (next.busy === this.codexState.busy && next.threadId === this.codexState.threadId && next.turnId === this.codexState.turnId)
|
|
48
|
+
return;
|
|
49
|
+
this.codexState = next;
|
|
50
|
+
logger.debug("Codex state changed", this.codexState);
|
|
51
|
+
this.emitAll("codex_state", this.codexState);
|
|
52
|
+
}
|
|
53
|
+
/** 建立网页与 Canvas Agent 之间的 SSE 连接。 */
|
|
54
|
+
openEvents(url, res) {
|
|
55
|
+
const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
|
|
56
|
+
const statusOnly = url.searchParams.get("role") === "status";
|
|
57
|
+
logger.info("SSE client connected", { clientId, statusOnly });
|
|
58
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
59
|
+
if (!statusOnly) {
|
|
60
|
+
this.clients.set(clientId, res);
|
|
61
|
+
if (!this.clientFocusOrder.has(clientId))
|
|
62
|
+
this.clientFocusOrder.set(clientId, 0);
|
|
63
|
+
if (!this.activeClientId) {
|
|
64
|
+
this.activeClientId = clientId;
|
|
65
|
+
this.clientFocusOrder.set(clientId, ++this.focusSequence);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
sendEvent(res, "hello", { ok: true, clientId, codex: this.codexState });
|
|
69
|
+
const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
|
|
70
|
+
res.on("close", () => {
|
|
71
|
+
clearInterval(timer);
|
|
72
|
+
logger.info("SSE client disconnected", { clientId, statusOnly });
|
|
73
|
+
if (statusOnly || this.clients.get(clientId) !== res)
|
|
74
|
+
return;
|
|
75
|
+
this.clients.delete(clientId);
|
|
76
|
+
this.clientFocusOrder.delete(clientId);
|
|
77
|
+
this.canvasStates.delete(clientId);
|
|
78
|
+
if (this.boundClientId === clientId)
|
|
79
|
+
this.boundClientId = "";
|
|
80
|
+
this.pending.forEach((item, requestId) => {
|
|
81
|
+
if (item.clientId !== clientId)
|
|
82
|
+
return;
|
|
83
|
+
this.pending.delete(requestId);
|
|
84
|
+
item.reject(new Error("请求页面已断开"));
|
|
85
|
+
});
|
|
86
|
+
if (this.activeClientId === clientId)
|
|
87
|
+
this.activeClientId = [...this.clients.keys()].sort((a, b) => (this.clientFocusOrder.get(b) || 0) - (this.clientFocusOrder.get(a) || 0))[0] || "";
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** 保存指定网页上报的最新画布快照。 */
|
|
91
|
+
updateState(body, clientId) {
|
|
92
|
+
const targetClientId = clientId || this.activeClientId;
|
|
93
|
+
if (!targetClientId)
|
|
94
|
+
return;
|
|
95
|
+
const state = { ...(body && typeof body === "object" && !Array.isArray(body) ? body : {}), clientId: targetClientId };
|
|
96
|
+
this.canvasStates.set(targetClientId, state);
|
|
97
|
+
logger.debug("Canvas state updated", { clientId: targetClientId, nodes: state.nodes?.length || 0, connections: state.connections?.length || 0 });
|
|
98
|
+
}
|
|
99
|
+
/** 将指定网页设为最近激活的工具目标。 */
|
|
100
|
+
activateClient(clientId) {
|
|
101
|
+
if (!this.clients.has(clientId))
|
|
102
|
+
throw new Error("当前网页未连接");
|
|
103
|
+
this.activeClientId = clientId;
|
|
104
|
+
this.clientFocusOrder.set(clientId, ++this.focusSequence);
|
|
105
|
+
logger.debug("Canvas client activated", { clientId });
|
|
106
|
+
}
|
|
107
|
+
/** 将当前 Agent turn 固定绑定到指定网页。 */
|
|
108
|
+
bindClient(clientId) {
|
|
109
|
+
if (!this.clients.has(clientId))
|
|
110
|
+
throw new Error("当前网页未连接");
|
|
111
|
+
this.boundClientId = clientId;
|
|
112
|
+
logger.debug("Canvas client bound to turn", { clientId });
|
|
113
|
+
}
|
|
114
|
+
/** 解除当前 Agent turn 的网页绑定。 */
|
|
115
|
+
releaseClient(clientId) {
|
|
116
|
+
if (this.boundClientId === clientId)
|
|
117
|
+
this.boundClientId = "";
|
|
118
|
+
logger.debug("Canvas client released from turn", { clientId });
|
|
119
|
+
}
|
|
120
|
+
/** 保存当前 turn 可用的图片附件并返回安全引用。 */
|
|
121
|
+
setTurnAttachments(clientId, attachments) {
|
|
122
|
+
this.turnAttachments.clear();
|
|
123
|
+
return attachments.flatMap((item, index) => {
|
|
124
|
+
if (!item.dataUrl?.startsWith("data:image/"))
|
|
125
|
+
return [];
|
|
126
|
+
const id = item.id?.trim() || `attachment-${crypto.randomUUID()}`;
|
|
127
|
+
const attachment = {
|
|
128
|
+
clientId,
|
|
129
|
+
id,
|
|
130
|
+
name: item.name?.trim() || `图片 ${index + 1}`,
|
|
131
|
+
type: item.type?.startsWith("image/") ? item.type : item.dataUrl.match(/^data:([^;]+)/)?.[1] || "image/png",
|
|
132
|
+
size: positiveNumber(item.size, 0),
|
|
133
|
+
width: positiveNumber(item.width, 1024),
|
|
134
|
+
height: positiveNumber(item.height, 1024),
|
|
135
|
+
dataUrl: item.dataUrl,
|
|
136
|
+
};
|
|
137
|
+
this.turnAttachments.set(id, attachment);
|
|
138
|
+
return [{ id, name: attachment.name, type: attachment.type, size: attachment.size, width: attachment.width, height: attachment.height }];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** 清理指定网页或全部 turn 附件。 */
|
|
142
|
+
clearTurnAttachments(clientId) {
|
|
143
|
+
this.turnAttachments.forEach((item, id) => {
|
|
144
|
+
if (!clientId || item.clientId === clientId)
|
|
145
|
+
this.turnAttachments.delete(id);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** 获取属于指定网页 turn 的图片附件。 */
|
|
149
|
+
getTurnAttachment(clientId, attachmentId) {
|
|
150
|
+
const attachment = this.turnAttachments.get(attachmentId);
|
|
151
|
+
if (!attachment)
|
|
152
|
+
throw new Error(`找不到本轮图片附件:${attachmentId}`);
|
|
153
|
+
if (attachment.clientId !== clientId)
|
|
154
|
+
throw new Error("图片附件不属于当前 turn 的发起标签页");
|
|
155
|
+
return attachment;
|
|
156
|
+
}
|
|
157
|
+
/** 接收网页返回的工具调用结果。 */
|
|
158
|
+
resolveResult(clientId, body) {
|
|
159
|
+
const item = body.requestId ? this.pending.get(body.requestId) : null;
|
|
160
|
+
if (!item || !body.requestId || item.clientId !== clientId)
|
|
161
|
+
return false;
|
|
162
|
+
this.pending.delete(body.requestId);
|
|
163
|
+
logger.debug("Canvas tool result received", { clientId, requestId: body.requestId, error: body.error, result: body.result });
|
|
164
|
+
body.error ? item.reject(new Error(body.error)) : item.resolve(body.result);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/** 向全部已连接网页广播事件。 */
|
|
168
|
+
emitAll(type, payload) {
|
|
169
|
+
this.clients.forEach((client) => sendEvent(client, type, payload));
|
|
170
|
+
}
|
|
171
|
+
/** 向全部网页广播带线程归属的事件。 */
|
|
172
|
+
emitThread(type, threadId, payload = {}) {
|
|
173
|
+
this.emitAll(type, { ...payload, threadId });
|
|
174
|
+
}
|
|
175
|
+
/** 校验工具参数并将调用分派到当前目标网页。 */
|
|
176
|
+
async callTool(name, rawInput) {
|
|
177
|
+
if (!isToolName(name))
|
|
178
|
+
throw new Error(`未知工具:${String(name)}`);
|
|
179
|
+
logger.info("MCP tool called", { name, input: rawInput, targetClientId: this.targetClientId });
|
|
180
|
+
const input = parseToolInput(name, rawInput);
|
|
181
|
+
if (SITE_TOOLS.has(name)) {
|
|
182
|
+
if (!this.clients.size)
|
|
183
|
+
throw new Error("当前没有已连接网页");
|
|
184
|
+
return await this.requestCanvasTool(name, input);
|
|
185
|
+
}
|
|
186
|
+
const readTool = ["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot"].includes(name);
|
|
187
|
+
if (readTool && (!this.clients.size || !this.canvasState))
|
|
188
|
+
throw new Error("当前没有已连接画布");
|
|
189
|
+
if (name === "canvas_get_state" || name === "canvas_export_snapshot")
|
|
190
|
+
return compactCanvasState(this.canvasState);
|
|
191
|
+
if (name === "canvas_get_selection") {
|
|
192
|
+
const ids = new Set(this.canvasState?.selectedNodeIds || []);
|
|
193
|
+
return { nodes: (this.canvasState?.nodes || []).filter((node) => ids.has(node.id)).map(compactNode) };
|
|
194
|
+
}
|
|
195
|
+
if (name === "canvas_create_attachment_nodes")
|
|
196
|
+
return await this.createAttachmentNodes(input);
|
|
197
|
+
if (!this.clients.size)
|
|
198
|
+
throw new Error("当前没有已连接画布");
|
|
199
|
+
const request = buildCanvasToolRequest(name, input, this.canvasState);
|
|
200
|
+
return await this.requestCanvasTool(request.name, request.input);
|
|
201
|
+
}
|
|
202
|
+
/** 将当前 turn 的附件转换为画布图片节点。 */
|
|
203
|
+
async createAttachmentNodes(input) {
|
|
204
|
+
const clientId = this.targetClientId;
|
|
205
|
+
if (!this.clients.has(clientId))
|
|
206
|
+
throw new Error("当前没有已连接画布");
|
|
207
|
+
const attachments = input.attachmentIds.map((id) => this.getTurnAttachment(clientId, id));
|
|
208
|
+
const x = Number(input.x ?? nextCanvasX(this.canvasState));
|
|
209
|
+
const y = Number(input.y ?? 0);
|
|
210
|
+
const gap = Number(input.gap ?? 40);
|
|
211
|
+
const direction = input.direction || "row";
|
|
212
|
+
let offset = 0;
|
|
213
|
+
const nodes = attachments.map((attachment) => {
|
|
214
|
+
const size = fitAttachmentNodeSize(attachment.width, attachment.height);
|
|
215
|
+
const node = {
|
|
216
|
+
id: `image-${crypto.randomUUID()}`,
|
|
217
|
+
attachmentId: attachment.id,
|
|
218
|
+
title: attachment.name,
|
|
219
|
+
position: { x: direction === "row" ? x + offset : x, y: direction === "column" ? y + offset : y },
|
|
220
|
+
width: size.width,
|
|
221
|
+
height: size.height,
|
|
222
|
+
};
|
|
223
|
+
offset += (direction === "row" ? size.width : size.height) + gap;
|
|
224
|
+
return node;
|
|
225
|
+
});
|
|
226
|
+
await this.requestCanvasTool("canvas_create_attachment_nodes", { nodes });
|
|
227
|
+
return { nodes: nodes.map(({ id, attachmentId, title }) => ({ id, attachmentId, title })) };
|
|
228
|
+
}
|
|
229
|
+
/** 向目标网页发送工具请求并等待调用结果。 */
|
|
230
|
+
async requestCanvasTool(name, input) {
|
|
231
|
+
const requestId = crypto.randomUUID();
|
|
232
|
+
const clientId = this.targetClientId;
|
|
233
|
+
const client = this.clients.get(clientId);
|
|
234
|
+
if (!client)
|
|
235
|
+
throw new Error("当前没有已连接画布");
|
|
236
|
+
sendEvent(client, "tool_call", { requestId, name, input });
|
|
237
|
+
logger.debug("Canvas tool request sent", { requestId, name, input, clientId });
|
|
238
|
+
return await new Promise((resolve, reject) => {
|
|
239
|
+
const timer = setTimeout(() => {
|
|
240
|
+
this.pending.delete(requestId);
|
|
241
|
+
logger.warn("Canvas tool request timed out", { requestId, name, clientId });
|
|
242
|
+
reject(new Error("画布操作超时"));
|
|
243
|
+
}, 30000);
|
|
244
|
+
this.pending.set(requestId, { clientId, resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** 向 SSE 连接写入一个事件。 */
|
|
249
|
+
function sendEvent(res, type, payload) {
|
|
250
|
+
res.write(`event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
251
|
+
}
|
|
252
|
+
/** 将未知数值转换为正数,否则使用默认值。 */
|
|
253
|
+
function positiveNumber(value, fallback) {
|
|
254
|
+
const number = Number(value);
|
|
255
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
256
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ToolName } from "./schemas.js";
|
|
2
2
|
import type { CanvasNode, CanvasSnapshot } from "./types.js";
|
|
3
|
+
/** 判断传入名称是否为已注册的画布工具。 */
|
|
3
4
|
export declare function isToolName(name: unknown): name is ToolName;
|
|
5
|
+
/** 按工具名称校验并解析调用参数。 */
|
|
4
6
|
export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
5
7
|
path: string;
|
|
6
8
|
} | {
|
|
@@ -74,10 +76,10 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
74
76
|
prompt: import("zod").ZodOptional<import("zod").ZodString>;
|
|
75
77
|
}, import("zod").ZodTypeAny, "passthrough">)[];
|
|
76
78
|
} | {
|
|
77
|
-
nodeType: "
|
|
79
|
+
nodeType: "text" | "image" | "config" | "video" | "audio";
|
|
80
|
+
title?: string | undefined;
|
|
78
81
|
x?: number | undefined;
|
|
79
82
|
y?: number | undefined;
|
|
80
|
-
title?: string | undefined;
|
|
81
83
|
width?: number | undefined;
|
|
82
84
|
height?: number | undefined;
|
|
83
85
|
metadata?: Record<string, unknown> | undefined;
|
|
@@ -89,17 +91,17 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
89
91
|
direction?: "row" | "column" | undefined;
|
|
90
92
|
} | {
|
|
91
93
|
text?: string | undefined;
|
|
94
|
+
title?: string | undefined;
|
|
92
95
|
x?: number | undefined;
|
|
93
96
|
y?: number | undefined;
|
|
94
|
-
title?: string | undefined;
|
|
95
97
|
width?: number | undefined;
|
|
96
98
|
height?: number | undefined;
|
|
97
99
|
} | {
|
|
98
100
|
items: {
|
|
99
101
|
text: string;
|
|
102
|
+
title?: string | undefined;
|
|
100
103
|
x?: number | undefined;
|
|
101
104
|
y?: number | undefined;
|
|
102
|
-
title?: string | undefined;
|
|
103
105
|
width?: number | undefined;
|
|
104
106
|
height?: number | undefined;
|
|
105
107
|
}[];
|
|
@@ -145,19 +147,19 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
145
147
|
};
|
|
146
148
|
} | {
|
|
147
149
|
nodeId: string;
|
|
148
|
-
mode?: "
|
|
150
|
+
mode?: "text" | "image" | "video" | "audio" | undefined;
|
|
149
151
|
prompt?: string | undefined;
|
|
150
152
|
} | {
|
|
151
|
-
limit?: number | undefined;
|
|
152
153
|
nodeIds?: string[] | undefined;
|
|
153
154
|
scope?: "image" | "video" | "all" | "canvas" | undefined;
|
|
154
155
|
taskId?: string | undefined;
|
|
156
|
+
limit?: number | undefined;
|
|
155
157
|
} | {
|
|
156
158
|
prompt: string;
|
|
159
|
+
count?: number | undefined;
|
|
157
160
|
model?: string | undefined;
|
|
158
161
|
size?: string | undefined;
|
|
159
162
|
quality?: string | undefined;
|
|
160
|
-
count?: number | undefined;
|
|
161
163
|
run?: boolean | undefined;
|
|
162
164
|
} | {
|
|
163
165
|
prompt: string;
|
|
@@ -170,13 +172,14 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
170
172
|
resolution?: string | undefined;
|
|
171
173
|
} | {
|
|
172
174
|
title: string;
|
|
173
|
-
kind: "
|
|
175
|
+
kind: "text" | "image";
|
|
174
176
|
source?: string | undefined;
|
|
175
177
|
content?: string | undefined;
|
|
176
178
|
tags?: string[] | undefined;
|
|
177
179
|
imageUrl?: string | undefined;
|
|
178
180
|
note?: string | undefined;
|
|
179
181
|
};
|
|
182
|
+
/** 压缩画布快照,避免向 Agent 返回过长的节点内容。 */
|
|
180
183
|
export declare function compactCanvasState(state: CanvasSnapshot | null): {
|
|
181
184
|
nodes: {
|
|
182
185
|
id: string;
|
|
@@ -196,6 +199,7 @@ export declare function compactCanvasState(state: CanvasSnapshot | null): {
|
|
|
196
199
|
viewport?: import("./types.js").Viewport;
|
|
197
200
|
clientId?: string;
|
|
198
201
|
};
|
|
202
|
+
/** 压缩单个画布节点的元数据内容。 */
|
|
199
203
|
export declare function compactNode(node: CanvasNode): {
|
|
200
204
|
id: string;
|
|
201
205
|
type: import("./types.js").CanvasNodeType;
|
|
@@ -207,4 +211,5 @@ export declare function compactNode(node: CanvasNode): {
|
|
|
207
211
|
[x: string]: unknown;
|
|
208
212
|
};
|
|
209
213
|
};
|
|
214
|
+
/** 计算新节点在当前画布右侧的默认横坐标。 */
|
|
210
215
|
export declare function nextCanvasX(state: CanvasSnapshot | null): number;
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { toolInputSchemas, toolNames } from "./schemas.js";
|
|
2
|
+
/** 判断传入名称是否为已注册的画布工具。 */
|
|
2
3
|
export function isToolName(name) {
|
|
3
4
|
return typeof name === "string" && toolNames.includes(name);
|
|
4
5
|
}
|
|
6
|
+
/** 按工具名称校验并解析调用参数。 */
|
|
5
7
|
export function parseToolInput(name, input) {
|
|
6
8
|
return toolInputSchemas[name].parse(input ?? {});
|
|
7
9
|
}
|
|
10
|
+
/** 压缩画布快照,避免向 Agent 返回过长的节点内容。 */
|
|
8
11
|
export function compactCanvasState(state) {
|
|
9
12
|
if (!state)
|
|
10
13
|
throw new Error("当前没有已连接画布");
|
|
11
14
|
return { ...state, nodes: (state.nodes || []).map(compactNode) };
|
|
12
15
|
}
|
|
16
|
+
/** 压缩单个画布节点的元数据内容。 */
|
|
13
17
|
export function compactNode(node) {
|
|
14
18
|
const metadata = { ...(node.metadata || {}) };
|
|
15
19
|
if (typeof metadata.content === "string" && metadata.content.length > 240)
|
|
16
20
|
metadata.content = `${metadata.content.slice(0, 120)}...`;
|
|
17
21
|
return { id: node.id, type: node.type, title: node.title, position: node.position, width: node.width, height: node.height, metadata };
|
|
18
22
|
}
|
|
23
|
+
/** 计算新节点在当前画布右侧的默认横坐标。 */
|
|
19
24
|
export function nextCanvasX(state) {
|
|
20
25
|
const nodes = state?.nodes || [];
|
|
21
26
|
return nodes.length ? Math.max(...nodes.map((node) => node.position.x + node.width)) + 80 : 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** 画布坐标。 */
|
|
1
2
|
export type Position = {
|
|
2
3
|
x: number;
|
|
3
4
|
y: number;
|
|
@@ -31,13 +32,3 @@ export type CanvasSnapshot = {
|
|
|
31
32
|
viewport?: Viewport;
|
|
32
33
|
clientId?: string;
|
|
33
34
|
};
|
|
34
|
-
export type AgentEmit = (type: string, payload: unknown) => void;
|
|
35
|
-
export type AgentAttachment = {
|
|
36
|
-
id?: string;
|
|
37
|
-
name?: string;
|
|
38
|
-
type?: string;
|
|
39
|
-
size?: number;
|
|
40
|
-
width?: number;
|
|
41
|
-
height?: number;
|
|
42
|
-
dataUrl?: string;
|
|
43
|
-
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/config.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare const DEFAULT_PORT = 17371;
|
|
|
2
2
|
export declare const CONFIG_DIR: string;
|
|
3
3
|
export declare const CONFIG_FILE: string;
|
|
4
4
|
export declare const VERSION: string;
|
|
5
|
-
export declare const AGENT_PROMPT
|
|
5
|
+
export declare const AGENT_PROMPT: string;
|
|
6
6
|
export type SiteWorkspaceConfig = {
|
|
7
7
|
workspacePath: string;
|
|
8
8
|
activeThreadId?: string;
|
|
@@ -14,11 +14,15 @@ export type CanvasAgentConfig = {
|
|
|
14
14
|
origins?: string[];
|
|
15
15
|
workspace?: SiteWorkspaceConfig;
|
|
16
16
|
};
|
|
17
|
+
/** 读取本地 Canvas Agent 配置,不存在时生成默认配置。 */
|
|
17
18
|
export declare function loadConfig(create?: boolean): CanvasAgentConfig;
|
|
19
|
+
/** 将 Canvas Agent 配置写入用户配置目录。 */
|
|
18
20
|
export declare function saveConfig(config: CanvasAgentConfig): void;
|
|
21
|
+
/** 确保站点级 Codex 工作空间存在并已初始化。 */
|
|
19
22
|
export declare function ensureSiteWorkspace(config: CanvasAgentConfig): {
|
|
20
23
|
workspacePath: string;
|
|
21
24
|
activeThreadId?: string;
|
|
22
25
|
pinnedThreadIds?: string[];
|
|
23
26
|
};
|
|
27
|
+
/** 更新站点级 Codex 工作空间配置。 */
|
|
24
28
|
export declare function updateSiteWorkspace(config: CanvasAgentConfig, patch: Partial<SiteWorkspaceConfig>): SiteWorkspaceConfig;
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,9 @@ export const DEFAULT_PORT = 17371;
|
|
|
6
6
|
export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas");
|
|
7
7
|
export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json");
|
|
8
8
|
export const VERSION = readPackageVersion();
|
|
9
|
-
export const AGENT_PROMPT =
|
|
9
|
+
export const AGENT_PROMPT = fs.readFileSync(new URL("../agent-instructions.md", import.meta.url), "utf8");
|
|
10
|
+
const initializedWorkspaces = new Set();
|
|
11
|
+
/** 读取本地 Canvas Agent 配置,不存在时生成默认配置。 */
|
|
10
12
|
export function loadConfig(create = false) {
|
|
11
13
|
try {
|
|
12
14
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
@@ -18,32 +20,47 @@ export function loadConfig(create = false) {
|
|
|
18
20
|
return config;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
23
|
+
/** 将 Canvas Agent 配置写入用户配置目录。 */
|
|
21
24
|
export function saveConfig(config) {
|
|
22
25
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
26
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
27
|
}
|
|
28
|
+
/** 确保站点级 Codex 工作空间存在并已初始化。 */
|
|
25
29
|
export function ensureSiteWorkspace(config) {
|
|
26
30
|
const current = config.workspace;
|
|
27
31
|
if (current?.workspacePath) {
|
|
28
32
|
const workspacePath = resolveWorkspacePath(current.workspacePath);
|
|
29
|
-
|
|
33
|
+
initializeWorkspace(workspacePath);
|
|
30
34
|
return { ...current, workspacePath };
|
|
31
35
|
}
|
|
32
36
|
const workspacePath = path.join(CONFIG_DIR, "codex-workspaces", "site");
|
|
33
37
|
config.workspace = { workspacePath };
|
|
34
|
-
|
|
38
|
+
initializeWorkspace(workspacePath);
|
|
35
39
|
saveConfig(config);
|
|
36
40
|
return { workspacePath };
|
|
37
41
|
}
|
|
42
|
+
/** 更新站点级 Codex 工作空间配置。 */
|
|
38
43
|
export function updateSiteWorkspace(config, patch) {
|
|
39
44
|
const current = ensureSiteWorkspace(config);
|
|
40
45
|
const workspacePath = patch.workspacePath ? resolveWorkspacePath(patch.workspacePath) : current.workspacePath;
|
|
41
46
|
const next = { ...current, ...patch, workspacePath };
|
|
42
47
|
config.workspace = { workspacePath: next.workspacePath, activeThreadId: next.activeThreadId, pinnedThreadIds: next.pinnedThreadIds };
|
|
43
|
-
|
|
48
|
+
initializeWorkspace(workspacePath);
|
|
44
49
|
saveConfig(config);
|
|
45
50
|
return config.workspace;
|
|
46
51
|
}
|
|
52
|
+
/** 创建工作空间目录并写入默认 AGENTS.md。 */
|
|
53
|
+
function initializeWorkspace(workspacePath) {
|
|
54
|
+
if (initializedWorkspaces.has(workspacePath))
|
|
55
|
+
return;
|
|
56
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
57
|
+
const instructionsFile = path.join(workspacePath, "AGENTS.md");
|
|
58
|
+
const current = fs.existsSync(instructionsFile) ? fs.readFileSync(instructionsFile, "utf8") : "";
|
|
59
|
+
if (!current || current.startsWith("# Infinite Canvas Agent"))
|
|
60
|
+
fs.writeFileSync(instructionsFile, AGENT_PROMPT);
|
|
61
|
+
initializedWorkspaces.add(workspacePath);
|
|
62
|
+
}
|
|
63
|
+
/** 将用户输入的工作空间路径解析为绝对路径。 */
|
|
47
64
|
function resolveWorkspacePath(value) {
|
|
48
65
|
if (value === "~")
|
|
49
66
|
return os.homedir();
|
|
@@ -51,6 +68,7 @@ function resolveWorkspacePath(value) {
|
|
|
51
68
|
return path.join(os.homedir(), value.slice(2));
|
|
52
69
|
return path.resolve(value);
|
|
53
70
|
}
|
|
71
|
+
/** 从当前包信息中读取 Canvas Agent 版本号。 */
|
|
54
72
|
function readPackageVersion() {
|
|
55
73
|
try {
|
|
56
74
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startHttpServer } from "./http
|
|
3
|
-
import { startMcpServer } from "./mcp
|
|
2
|
+
import { startHttpServer } from "./server/http.js";
|
|
3
|
+
import { startMcpServer } from "./server/mcp.js";
|
|
4
4
|
if (process.argv[2] === "mcp")
|
|
5
5
|
await startMcpServer();
|
|
6
6
|
else
|