@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
@@ -1,391 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js";
3
- const SITE_TOOLS = new Set([
4
- "site_navigate",
5
- "canvas_list_projects",
6
- "workbench_image_get_config",
7
- "workbench_image_generate",
8
- "workbench_video_get_config",
9
- "workbench_video_generate",
10
- "prompts_search",
11
- "assets_list",
12
- "assets_add",
13
- "generation_get_status",
14
- ]);
15
- export class CanvasSession {
16
- clients = new Map();
17
- clientFocusOrder = new Map();
18
- pending = new Map();
19
- canvasStates = new Map();
20
- turnAttachments = new Map();
21
- activeClientId = "";
22
- boundClientId = "";
23
- focusSequence = 0;
24
- codexState = { busy: false, threadId: "", turnId: "" };
25
- get canvasState() {
26
- return this.canvasStates.get(this.targetClientId) || null;
27
- }
28
- get targetClientId() {
29
- return this.boundClientId || this.activeClientId;
30
- }
31
- health() {
32
- return { ok: true, hasCanvas: Boolean(this.canvasState), clients: this.clients.size, codexBusy: this.codexState.busy };
33
- }
34
- get codexBusy() {
35
- return this.codexState.busy;
36
- }
37
- setCodexState(patch) {
38
- this.codexState = { ...this.codexState, ...patch };
39
- this.emitAll("codex_state", this.codexState);
40
- }
41
- openEvents(url, res) {
42
- const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
43
- const statusOnly = url.searchParams.get("role") === "status";
44
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
45
- if (!statusOnly) {
46
- this.clients.set(clientId, res);
47
- if (!this.clientFocusOrder.has(clientId))
48
- this.clientFocusOrder.set(clientId, 0);
49
- if (!this.activeClientId) {
50
- this.activeClientId = clientId;
51
- this.clientFocusOrder.set(clientId, ++this.focusSequence);
52
- }
53
- }
54
- sendEvent(res, "hello", { ok: true, clientId, codex: this.codexState });
55
- const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
56
- res.on("close", () => {
57
- clearInterval(timer);
58
- if (statusOnly || this.clients.get(clientId) !== res)
59
- return;
60
- this.clients.delete(clientId);
61
- this.clientFocusOrder.delete(clientId);
62
- this.canvasStates.delete(clientId);
63
- if (this.boundClientId === clientId)
64
- this.boundClientId = "";
65
- this.pending.forEach((item, requestId) => {
66
- if (item.clientId !== clientId)
67
- return;
68
- this.pending.delete(requestId);
69
- item.reject(new Error("请求页面已断开"));
70
- });
71
- if (this.activeClientId === clientId)
72
- this.activeClientId = [...this.clients.keys()].sort((a, b) => (this.clientFocusOrder.get(b) || 0) - (this.clientFocusOrder.get(a) || 0))[0] || "";
73
- });
74
- }
75
- updateState(body, clientId) {
76
- const targetClientId = clientId || this.activeClientId;
77
- if (!targetClientId)
78
- return;
79
- this.canvasStates.set(targetClientId, { ...(body && typeof body === "object" && !Array.isArray(body) ? body : {}), clientId: targetClientId });
80
- }
81
- activateClient(clientId) {
82
- if (!this.clients.has(clientId))
83
- throw new Error("当前网页未连接");
84
- this.activeClientId = clientId;
85
- this.clientFocusOrder.set(clientId, ++this.focusSequence);
86
- }
87
- bindClient(clientId) {
88
- if (!this.clients.has(clientId))
89
- throw new Error("当前网页未连接");
90
- this.boundClientId = clientId;
91
- }
92
- releaseClient(clientId) {
93
- if (this.boundClientId === clientId)
94
- this.boundClientId = "";
95
- }
96
- setTurnAttachments(clientId, attachments) {
97
- this.turnAttachments.clear();
98
- return attachments.flatMap((item, index) => {
99
- if (!item.dataUrl?.startsWith("data:image/"))
100
- return [];
101
- const id = item.id?.trim() || `attachment-${crypto.randomUUID()}`;
102
- const attachment = {
103
- clientId,
104
- id,
105
- name: item.name?.trim() || `图片 ${index + 1}`,
106
- type: item.type?.startsWith("image/") ? item.type : item.dataUrl.match(/^data:([^;]+)/)?.[1] || "image/png",
107
- size: positiveNumber(item.size, 0),
108
- width: positiveNumber(item.width, 1024),
109
- height: positiveNumber(item.height, 1024),
110
- dataUrl: item.dataUrl,
111
- };
112
- this.turnAttachments.set(id, attachment);
113
- return [{ id, name: attachment.name, type: attachment.type, size: attachment.size, width: attachment.width, height: attachment.height }];
114
- });
115
- }
116
- clearTurnAttachments(clientId) {
117
- this.turnAttachments.forEach((item, id) => {
118
- if (!clientId || item.clientId === clientId)
119
- this.turnAttachments.delete(id);
120
- });
121
- }
122
- getTurnAttachment(clientId, attachmentId) {
123
- const attachment = this.turnAttachments.get(attachmentId);
124
- if (!attachment)
125
- throw new Error(`找不到本轮图片附件:${attachmentId}`);
126
- if (attachment.clientId !== clientId)
127
- throw new Error("图片附件不属于当前 turn 的发起标签页");
128
- return attachment;
129
- }
130
- resolveResult(clientId, body) {
131
- const item = body.requestId ? this.pending.get(body.requestId) : null;
132
- if (!item || !body.requestId || item.clientId !== clientId)
133
- return false;
134
- this.pending.delete(body.requestId);
135
- body.error ? item.reject(new Error(body.error)) : item.resolve(body.result);
136
- return true;
137
- }
138
- emitAll(type, payload) {
139
- this.clients.forEach((client) => sendEvent(client, type, payload));
140
- }
141
- emitThread(type, threadId, payload = {}) {
142
- this.emitAll(type, { ...payload, threadId });
143
- }
144
- async callTool(name, rawInput) {
145
- if (!isToolName(name))
146
- throw new Error(`未知工具:${String(name)}`);
147
- let tool = name;
148
- let input = parseToolInput(tool, rawInput);
149
- if (SITE_TOOLS.has(tool)) {
150
- if (!this.clients.size)
151
- throw new Error("当前没有已连接网页");
152
- return await this.requestCanvasTool(tool, input);
153
- }
154
- if (tool === "canvas_export_media") {
155
- if (!this.clients.size || !this.canvasState)
156
- throw new Error("当前没有已连接画布");
157
- return await this.requestCanvasTool(tool, input);
158
- }
159
- const readTool = ["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot"].includes(tool);
160
- if (readTool && (!this.clients.size || !this.canvasState))
161
- throw new Error("当前没有已连接画布");
162
- if (tool === "canvas_get_state" || tool === "canvas_export_snapshot")
163
- return compactCanvasState(this.canvasState);
164
- if (tool === "canvas_get_selection") {
165
- const ids = new Set(this.canvasState?.selectedNodeIds || []);
166
- return { nodes: (this.canvasState?.nodes || []).filter((node) => ids.has(node.id)).map(compactNode) };
167
- }
168
- if (tool === "canvas_create_attachment_nodes")
169
- return await this.createAttachmentNodes(input);
170
- if (tool === "canvas_create_node") {
171
- const data = input;
172
- input = { ops: [{ type: "add_node", nodeType: data.nodeType, title: data.title, position: { x: data.x ?? nextCanvasX(this.canvasState), y: data.y ?? 0 }, width: data.width, height: data.height, metadata: data.metadata }] };
173
- tool = "canvas_apply_ops";
174
- }
175
- if (tool === "canvas_create_text_node") {
176
- const text = input;
177
- input = { ops: [textNodeOp(text, text.x ?? nextCanvasX(this.canvasState), text.y ?? 0)] };
178
- tool = "canvas_apply_ops";
179
- }
180
- if (tool === "canvas_create_text_nodes") {
181
- const data = input;
182
- const x = Number(data.x ?? nextCanvasX(this.canvasState));
183
- const y = Number(data.y ?? 0);
184
- const gap = Number(data.gap ?? 40);
185
- input = {
186
- ops: 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)))),
187
- };
188
- tool = "canvas_apply_ops";
189
- }
190
- if (tool === "canvas_create_image_prompt_flow") {
191
- input = { ops: generationFlowOps({ ...input, mode: "image" }, this.canvasState) };
192
- tool = "canvas_apply_ops";
193
- }
194
- if (tool === "canvas_create_config_node") {
195
- const data = input;
196
- const x = Number(data.x ?? nextCanvasX(this.canvasState));
197
- const y = Number(data.y ?? 0);
198
- const configId = `config-${crypto.randomUUID()}`;
199
- const mode = generationMode(data.mode);
200
- const prompt = String(data.prompt || "");
201
- input = { ops: [configNodeOp(configId, data, x, y), ...(data.autoRun ? [runGenerationOp(configId, mode, prompt)] : [])] };
202
- tool = "canvas_apply_ops";
203
- }
204
- if (tool === "canvas_create_generation_flow") {
205
- input = { ops: generationFlowOps(input, this.canvasState) };
206
- tool = "canvas_apply_ops";
207
- }
208
- if (tool === "canvas_generate_text" || tool === "canvas_generate_image" || tool === "canvas_generate_video" || tool === "canvas_generate_audio") {
209
- input = { ops: generationFlowOps({ ...input, mode: tool.replace("canvas_generate_", ""), autoRun: true }, this.canvasState) };
210
- tool = "canvas_apply_ops";
211
- }
212
- if (tool === "canvas_update_node") {
213
- const data = input;
214
- input = { ops: [{ type: "update_node", id: data.id, patch: data.patch, metadata: data.metadata }] };
215
- tool = "canvas_apply_ops";
216
- }
217
- if (tool === "canvas_update_node_text") {
218
- const data = input;
219
- input = { ops: [{ type: "update_node", id: data.id, patch: { ...(data.title ? { title: data.title } : {}) }, metadata: { content: data.text, status: "success" } }] };
220
- tool = "canvas_apply_ops";
221
- }
222
- if (tool === "canvas_move_nodes") {
223
- const data = input;
224
- input = {
225
- ops: data.items.map((item) => {
226
- const current = findNode(this.canvasState, item.id);
227
- 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)) } } };
228
- }),
229
- };
230
- tool = "canvas_apply_ops";
231
- }
232
- if (tool === "canvas_resize_node") {
233
- const data = input;
234
- input = { ops: [{ type: "update_node", id: data.id, patch: { width: data.width, height: data.height }, metadata: data.freeResize === undefined ? undefined : { freeResize: data.freeResize } }] };
235
- tool = "canvas_apply_ops";
236
- }
237
- if (tool === "canvas_delete_nodes") {
238
- input = { ops: [{ type: "delete_node", ids: input.ids }] };
239
- tool = "canvas_apply_ops";
240
- }
241
- if (tool === "canvas_connect_nodes") {
242
- const data = input;
243
- input = { ops: data.connections.map((connection) => ({ type: "connect_nodes", ...connection })) };
244
- tool = "canvas_apply_ops";
245
- }
246
- if (tool === "canvas_select_nodes") {
247
- input = { ops: [{ type: "select_nodes", ids: input.ids }] };
248
- tool = "canvas_apply_ops";
249
- }
250
- if (tool === "canvas_set_viewport") {
251
- input = { ops: [{ type: "set_viewport", viewport: input.viewport }] };
252
- tool = "canvas_apply_ops";
253
- }
254
- if (tool === "canvas_run_generation") {
255
- const data = input;
256
- input = { ops: [runGenerationOp(data.nodeId, generationMode(data.mode), data.prompt)] };
257
- tool = "canvas_apply_ops";
258
- }
259
- if (tool !== "canvas_apply_ops")
260
- throw new Error(`未知工具:${tool}`);
261
- if (!this.clients.size)
262
- throw new Error("当前没有已连接画布");
263
- return await this.requestCanvasTool(tool, input);
264
- }
265
- async createAttachmentNodes(input) {
266
- const clientId = this.targetClientId;
267
- if (!this.clients.has(clientId))
268
- throw new Error("当前没有已连接画布");
269
- const attachments = input.attachmentIds.map((id) => this.getTurnAttachment(clientId, id));
270
- const x = Number(input.x ?? nextCanvasX(this.canvasState));
271
- const y = Number(input.y ?? 0);
272
- const gap = Number(input.gap ?? 40);
273
- const direction = input.direction || "row";
274
- let offset = 0;
275
- const nodes = attachments.map((attachment) => {
276
- const size = fitAttachmentNodeSize(attachment.width, attachment.height);
277
- const node = {
278
- id: `image-${crypto.randomUUID()}`,
279
- attachmentId: attachment.id,
280
- title: attachment.name,
281
- position: { x: direction === "row" ? x + offset : x, y: direction === "column" ? y + offset : y },
282
- width: size.width,
283
- height: size.height,
284
- };
285
- offset += (direction === "row" ? size.width : size.height) + gap;
286
- return node;
287
- });
288
- await this.requestCanvasTool("canvas_create_attachment_nodes", { nodes });
289
- return { nodes: nodes.map(({ id, attachmentId, title }) => ({ id, attachmentId, title })) };
290
- }
291
- async requestCanvasTool(name, input) {
292
- const requestId = crypto.randomUUID();
293
- const clientId = this.targetClientId;
294
- const client = this.clients.get(clientId);
295
- if (!client)
296
- throw new Error("当前没有已连接画布");
297
- sendEvent(client, "tool_call", { requestId, name, input });
298
- return await new Promise((resolve, reject) => {
299
- const timer = setTimeout(() => {
300
- this.pending.delete(requestId);
301
- reject(new Error("画布操作超时"));
302
- }, 30000);
303
- this.pending.set(requestId, { clientId, resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) });
304
- });
305
- }
306
- }
307
- function sendEvent(res, type, payload) {
308
- res.write(`event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`);
309
- }
310
- function textNodeOp(input, x, y) {
311
- 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 } };
312
- }
313
- function configNodeOp(id, input, x, y) {
314
- const mode = generationMode(input.mode);
315
- const prompt = String(input.prompt || "");
316
- return {
317
- type: "add_node",
318
- id,
319
- nodeType: "config",
320
- title: String(input.title || generationTitle(mode)),
321
- position: { x, y },
322
- width: typeof input.width === "number" ? input.width : undefined,
323
- height: typeof input.height === "number" ? input.height : undefined,
324
- metadata: cleanRecord({
325
- generationMode: mode,
326
- composerContent: prompt,
327
- prompt,
328
- status: "idle",
329
- model: input.model,
330
- size: input.size,
331
- quality: input.quality,
332
- count: input.count,
333
- seconds: input.seconds,
334
- vquality: input.vquality,
335
- generateAudio: input.generateAudio,
336
- watermark: input.watermark,
337
- audioVoice: input.audioVoice,
338
- audioFormat: input.audioFormat,
339
- audioSpeed: input.audioSpeed,
340
- audioInstructions: input.audioInstructions,
341
- }),
342
- };
343
- }
344
- function generationFlowOps(input, state) {
345
- const mode = generationMode(input.mode);
346
- const prompt = String(input.prompt || "");
347
- const x = Number(input.x ?? nextCanvasX(state));
348
- const y = Number(input.y ?? 0);
349
- const textId = `text-${crypto.randomUUID()}`;
350
- const configId = `config-${crypto.randomUUID()}`;
351
- const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id) => typeof id === "string") : [];
352
- const tokens = [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)];
353
- const configInput = { ...input, prompt: tokens.join("\n") };
354
- return [
355
- textNodeOp({ id: textId, text: prompt, title: String(input.title || "提示词") }, x, y),
356
- configNodeOp(configId, configInput, x + 420, y),
357
- { type: "connect_nodes", fromNodeId: textId, toNodeId: configId },
358
- ...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: configId })),
359
- { type: "select_nodes", ids: [configId] },
360
- ...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []),
361
- ];
362
- }
363
- function runGenerationOp(nodeId, mode, prompt) {
364
- return { type: "run_generation", nodeId, mode, prompt };
365
- }
366
- function generationMode(value) {
367
- return value === "text" || value === "video" || value === "audio" ? value : "image";
368
- }
369
- function generationTitle(mode) {
370
- if (mode === "text")
371
- return "文本生成";
372
- if (mode === "video")
373
- return "视频生成";
374
- if (mode === "audio")
375
- return "音频生成";
376
- return "图片生成";
377
- }
378
- function findNode(state, id) {
379
- return (state?.nodes || []).find((node) => node.id === id);
380
- }
381
- function cleanRecord(value) {
382
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== ""));
383
- }
384
- function positiveNumber(value, fallback) {
385
- const number = Number(value);
386
- return Number.isFinite(number) && number > 0 ? number : fallback;
387
- }
388
- function fitAttachmentNodeSize(width, height) {
389
- const scale = Math.min(1, 640 / width, 640 / height);
390
- return { width: width * scale, height: height * scale };
391
- }
@@ -1 +0,0 @@
1
- export declare function startHttpServer(): void;
@@ -1 +0,0 @@
1
- export declare function startMcpServer(): Promise<void>;
File without changes