@xiaohhhh1/canvas-agent 0.2.2
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 +149 -0
- package/dist/agents.d.ts +79 -0
- package/dist/agents.js +557 -0
- package/dist/canvas-session.d.ts +64 -0
- package/dist/canvas-session.js +391 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +62 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/http-server.js +230 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +24 -0
- package/dist/relay-bridge.d.ts +7 -0
- package/dist/relay-bridge.js +126 -0
- package/dist/schemas.d.ts +992 -0
- package/dist/schemas.js +156 -0
- package/dist/tools.d.ts +210 -0
- package/dist/tools.js +22 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const DEFAULT_PORT = 17371;
|
|
2
|
+
export declare const CONFIG_DIR: string;
|
|
3
|
+
export declare const CONFIG_FILE: string;
|
|
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";
|
|
6
|
+
export type SiteWorkspaceConfig = {
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
activeThreadId?: string;
|
|
9
|
+
pinnedThreadIds?: string[];
|
|
10
|
+
};
|
|
11
|
+
export type CanvasAgentConfig = {
|
|
12
|
+
url: string;
|
|
13
|
+
token: string;
|
|
14
|
+
origins?: string[];
|
|
15
|
+
workspace?: SiteWorkspaceConfig;
|
|
16
|
+
};
|
|
17
|
+
export declare function loadConfig(create?: boolean): CanvasAgentConfig;
|
|
18
|
+
export declare function saveConfig(config: CanvasAgentConfig): void;
|
|
19
|
+
export declare function ensureSiteWorkspace(config: CanvasAgentConfig): {
|
|
20
|
+
workspacePath: string;
|
|
21
|
+
activeThreadId?: string;
|
|
22
|
+
pinnedThreadIds?: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare function updateSiteWorkspace(config: CanvasAgentConfig, patch: Partial<SiteWorkspaceConfig>): SiteWorkspaceConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const DEFAULT_PORT = 17371;
|
|
6
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas");
|
|
7
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json");
|
|
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。";
|
|
10
|
+
export function loadConfig(create = false) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
const config = { url: `http://127.0.0.1:${Number(process.env.PORT) || DEFAULT_PORT}`, token: crypto.randomBytes(18).toString("hex") };
|
|
16
|
+
if (create)
|
|
17
|
+
saveConfig(config);
|
|
18
|
+
return config;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveConfig(config) {
|
|
22
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export function ensureSiteWorkspace(config) {
|
|
26
|
+
const current = config.workspace;
|
|
27
|
+
if (current?.workspacePath) {
|
|
28
|
+
const workspacePath = resolveWorkspacePath(current.workspacePath);
|
|
29
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
30
|
+
return { ...current, workspacePath };
|
|
31
|
+
}
|
|
32
|
+
const workspacePath = path.join(CONFIG_DIR, "codex-workspaces", "site");
|
|
33
|
+
config.workspace = { workspacePath };
|
|
34
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
35
|
+
saveConfig(config);
|
|
36
|
+
return { workspacePath };
|
|
37
|
+
}
|
|
38
|
+
export function updateSiteWorkspace(config, patch) {
|
|
39
|
+
const current = ensureSiteWorkspace(config);
|
|
40
|
+
const workspacePath = patch.workspacePath ? resolveWorkspacePath(patch.workspacePath) : current.workspacePath;
|
|
41
|
+
const next = { ...current, ...patch, workspacePath };
|
|
42
|
+
config.workspace = { workspacePath: next.workspacePath, activeThreadId: next.activeThreadId, pinnedThreadIds: next.pinnedThreadIds };
|
|
43
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
44
|
+
saveConfig(config);
|
|
45
|
+
return config.workspace;
|
|
46
|
+
}
|
|
47
|
+
function resolveWorkspacePath(value) {
|
|
48
|
+
if (value === "~")
|
|
49
|
+
return os.homedir();
|
|
50
|
+
if (value.startsWith("~/"))
|
|
51
|
+
return path.join(os.homedir(), value.slice(2));
|
|
52
|
+
return path.resolve(value);
|
|
53
|
+
}
|
|
54
|
+
function readPackageVersion() {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
57
|
+
return pkg.version || "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "0.0.0";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startHttpServer(): void;
|