@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.
Files changed (42) hide show
  1. package/README.md +9 -1
  2. package/agent-instructions.md +21 -0
  3. package/dist/agent/claude.d.ts +3 -0
  4. package/dist/agent/claude.js +46 -0
  5. package/dist/agent/codex-client.d.ts +73 -0
  6. package/dist/agent/codex-client.js +438 -0
  7. package/dist/agent/codex-history.d.ts +25 -0
  8. package/dist/agent/codex-history.js +405 -0
  9. package/dist/agent/codex-protocol.d.ts +208 -0
  10. package/dist/{agents.d.ts → agent/codex.d.ts} +34 -31
  11. package/dist/agent/codex.js +210 -0
  12. package/dist/agent/types.d.ts +14 -0
  13. package/dist/agent/types.js +1 -0
  14. package/dist/canvas/operations.d.ts +13 -0
  15. package/dist/canvas/operations.js +161 -0
  16. package/dist/{schemas.d.ts → canvas/schemas.d.ts} +21 -20
  17. package/dist/{schemas.js → canvas/schemas.js} +1 -0
  18. package/dist/{canvas-session.d.ts → canvas/session.d.ts} +21 -1
  19. package/dist/canvas/session.js +256 -0
  20. package/dist/{tools.d.ts → canvas/tools.d.ts} +13 -8
  21. package/dist/{tools.js → canvas/tools.js} +5 -0
  22. package/dist/{types.d.ts → canvas/types.d.ts} +1 -10
  23. package/dist/canvas/types.js +1 -0
  24. package/dist/config.d.ts +5 -1
  25. package/dist/config.js +22 -4
  26. package/dist/index.js +2 -2
  27. package/dist/server/http.d.ts +2 -0
  28. package/dist/{http-server.js → server/http.js} +100 -16
  29. package/dist/server/mcp.d.ts +2 -0
  30. package/dist/{mcp-server.js → server/mcp.js} +5 -2
  31. package/dist/utils/date.d.ts +2 -0
  32. package/dist/utils/date.js +7 -0
  33. package/dist/utils/logger.d.ts +17 -0
  34. package/dist/utils/logger.js +83 -0
  35. package/dist/utils/value.d.ts +5 -0
  36. package/dist/utils/value.js +8 -0
  37. package/package.json +7 -4
  38. package/dist/agents.js +0 -557
  39. package/dist/canvas-session.js +0 -391
  40. package/dist/http-server.d.ts +0 -1
  41. package/dist/mcp-server.d.ts +0 -1
  42. /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 "./types.js";
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: "image" | "text" | "config" | "video" | "audio";
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?: "image" | "text" | "video" | "audio" | undefined;
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: "image" | "text";
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 = "\u4F60\u6B63\u5728\u5E2E\u52A9\u7528\u6237\u64CD\u4F5C Infinite Canvas \u7F51\u7AD9\u3002\u5207\u6362\u7F51\u7AD9\u9875\u9762\u7528 site_navigate\uFF0C\u53EF\u8DF3 / (\u9996\u9875)\u3001/canvas (\u6211\u7684\u753B\u5E03)\u3001/canvas/:id (\u6307\u5B9A\u753B\u5E03)\u3001/image\u3001/video\u3001/prompts\u3001/assets\u3001/config\u3002\u9700\u8981\u6539\u52A8\u753B\u5E03\u65F6\u4F18\u5148\u4F7F\u7528\u5DF2\u914D\u7F6E\u7684 infinite-canvas MCP \u5DE5\u5177\uFF1A\u5148 canvas_get_state \u8BFB\u53D6\u5F53\u524D\u753B\u5E03\uFF0C\u518D\u6839\u636E\u4EFB\u52A1\u4F7F\u7528 canvas_create_text_node\u3001canvas_generate_text\u3001canvas_generate_image\u3001canvas_generate_video\u3001canvas_generate_audio\u3001canvas_create_generation_flow\u3001canvas_create_config_node\u3001canvas_run_generation\u3001canvas_update_node\u3001canvas_connect_nodes \u7B49\u901A\u7528\u5DE5\u5177\uFF1B\u590D\u6742\u6279\u91CF\u6539\u52A8\u518D\u7528 canvas_apply_ops\uFF0C\u5220\u9664\u8FDE\u7EBF\u53EF\u7528 delete_connections\u3002\u672C\u8F6E\u82E5\u6709\u7528\u6237\u4E0A\u4F20\u7684\u56FE\u7247\u9644\u4EF6\uFF0C\u4F1A\u540C\u65F6\u7ED9\u51FA attachmentId\uFF1B\u7528\u6237\u8981\u6C42\u628A\u9644\u4EF6\u653E\u5165\u753B\u5E03\u6216\u4F5C\u4E3A\u751F\u6210\u53C2\u8003\u56FE\u65F6\uFF0C\u5FC5\u987B\u5148\u7528 canvas_create_attachment_nodes \u521B\u5EFA\u771F\u5B9E\u56FE\u7247\u8282\u70B9\uFF0C\u518D\u628A\u8FD4\u56DE\u7684\u8282\u70B9 ID \u4F20\u7ED9 canvas_create_generation_flow.referenceNodeIds\uFF0C\u4E0D\u8981\u521B\u5EFA\u7A7A\u56FE\u7247\u5360\u4F4D\u8282\u70B9\u3002\u82E5\u5F53\u524D\u4E0D\u5728\u753B\u5E03\u9875\uFF0C\u753B\u5E03\u5DE5\u5177\u4F1A\u62A5\u9519\uFF0C\u9700\u5148\u7528 site_navigate \u6253\u5F00\u753B\u5E03\u3002\u60F3\u4E86\u89E3\u6216\u6253\u5F00\u7528\u6237\u5DF2\u6709\u753B\u5E03\uFF0C\u7528 canvas_list_projects \u83B7\u53D6\u753B\u5E03\u6E05\u5355\u548C id\uFF0C\u518D\u7528 site_navigate \u8DF3 /canvas/:id \u6253\u5F00\u3002\u751F\u56FE\u5DE5\u4F5C\u53F0\u53EF\u7528 workbench_image_get_config \u770B\u53EF\u9009\u9879\u3001workbench_image_generate \u586B\u63D0\u793A\u8BCD\u5E76\u751F\u6210\uFF1B\u89C6\u9891\u521B\u4F5C\u53F0\u5BF9\u5E94 workbench_video_get_config \u4E0E workbench_video_generate\uFF1B\u7528 prompts_search \u5206\u9875\u641C\u7D22\u63D0\u793A\u8BCD\u5E93\uFF1B\u7528 assets_list \u67E5\u770B\u300C\u6211\u7684\u7D20\u6750\u300D\u3001assets_add \u65B0\u589E\u6587\u672C\u6216\u56FE\u7247\u7D20\u6750\u3002\u9700\u8981\u751F\u6210\u5185\u5BB9\u65F6\u76F4\u63A5\u8C03\u7528\u5BF9\u5E94\u751F\u6210\u5DE5\u5177\uFF0C\u4E0D\u8981\u7ED1\u5B9A\u7279\u5B9A\u4E1A\u52A1\u573A\u666F\u3002\u4E0D\u8981\u6A21\u62DF\u9F20\u6807\u70B9\u51FB\uFF0C\u4E0D\u8981\u8981\u6C42\u7528\u6237\u624B\u52A8\u590D\u5236 JSON\u3002";
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 = "你正在帮助用户操作 Infinite Canvas 网站。切换网站页面用 site_navigate,可跳 / (首页)、/canvas (我的画布)、/canvas/:id (指定画布)、/image、/video、/prompts、/assets、/config。需要改动画布时优先使用已配置的 infinite-canvas MCP 工具:先 canvas_get_state 读取当前画布,再根据任务使用 canvas_create_text_node、canvas_generate_text、canvas_generate_image、canvas_generate_video、canvas_generate_audio、canvas_create_generation_flow、canvas_create_config_node、canvas_run_generation、canvas_update_node、canvas_connect_nodes 等通用工具;复杂批量改动再用 canvas_apply_ops,删除连线可用 delete_connections。本轮若有用户上传的图片附件,会同时给出 attachmentId;用户要求把附件放入画布或作为生成参考图时,必须先用 canvas_create_attachment_nodes 创建真实图片节点,再把返回的节点 ID 传给 canvas_create_generation_flow.referenceNodeIds,不要创建空图片占位节点。若当前不在画布页,画布工具会报错,需先用 site_navigate 打开画布。想了解或打开用户已有画布,用 canvas_list_projects 获取画布清单和 id,再用 site_navigate 跳 /canvas/:id 打开。生图工作台可用 workbench_image_get_config 看可选项、workbench_image_generate 填提示词并生成;视频创作台对应 workbench_video_get_config 与 workbench_video_generate;用 prompts_search 分页搜索提示词库;用 assets_list 查看「我的素材」、assets_add 新增文本或图片素材。需要生成内容时直接调用对应生成工具,不要绑定特定业务场景。不要模拟鼠标点击,不要要求用户手动复制 JSON。";
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
- fs.mkdirSync(workspacePath, { recursive: true });
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
- fs.mkdirSync(workspacePath, { recursive: true });
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
- fs.mkdirSync(workspacePath, { recursive: true });
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-server.js";
3
- import { startMcpServer } from "./mcp-server.js";
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
@@ -0,0 +1,2 @@
1
+ /** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
2
+ export declare function startHttpServer(): void;