@kobinflow/canvas-agent 0.6.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/LICENSE +22 -0
- package/README.md +159 -0
- package/agent-instructions.md +13 -0
- package/dist/agent/claude.d.ts +3 -0
- package/dist/agent/claude.js +54 -0
- package/dist/agent/codex-client.d.ts +139 -0
- package/dist/agent/codex-client.js +937 -0
- package/dist/agent/codex-event-history.d.ts +21 -0
- package/dist/agent/codex-event-history.js +163 -0
- package/dist/agent/codex-history.d.ts +46 -0
- package/dist/agent/codex-history.js +822 -0
- package/dist/agent/codex-protocol.d.ts +335 -0
- package/dist/agent/codex-protocol.js +1 -0
- package/dist/agent/codex.d.ts +178 -0
- package/dist/agent/codex.js +509 -0
- package/dist/agent/message-metadata.d.ts +51 -0
- package/dist/agent/message-metadata.js +378 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/canvas/operations.d.ts +13 -0
- package/dist/canvas/operations.js +167 -0
- package/dist/canvas/schemas.d.ts +989 -0
- package/dist/canvas/schemas.js +155 -0
- package/dist/canvas/session.d.ts +163 -0
- package/dist/canvas/session.js +523 -0
- package/dist/canvas/tools.d.ts +214 -0
- package/dist/canvas/tools.js +27 -0
- package/dist/canvas/types.d.ts +34 -0
- package/dist/canvas/types.js +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +576 -0
- package/dist/server/mcp.d.ts +2 -0
- package/dist/server/mcp.js +27 -0
- package/dist/skills/store.d.ts +54 -0
- package/dist/skills/store.js +426 -0
- package/dist/utils/agent-runtime.d.ts +8 -0
- package/dist/utils/agent-runtime.js +33 -0
- 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 +81 -0
- package/dist/utils/value.d.ts +5 -0
- package/dist/utils/value.js +8 -0
- package/dist/version-check.d.ts +2 -0
- package/dist/version-check.js +65 -0
- package/package.json +55 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CONFIG_DIR } from "../config.js";
|
|
5
|
+
const STORAGE_VERSION = 1;
|
|
6
|
+
const MAX_PREVIEW_LENGTH = 500_000;
|
|
7
|
+
const MAX_TEXT_LENGTH = 20_000;
|
|
8
|
+
const MANIFEST_FILE = "manifest.json";
|
|
9
|
+
const MESSAGE_ASSET_PREFIX = "agent-asset:";
|
|
10
|
+
const MESSAGE_METADATA_DIRECTORY = path.join(CONFIG_DIR, "message-metadata");
|
|
11
|
+
export class MessageMetadataStore {
|
|
12
|
+
directory;
|
|
13
|
+
queue = Promise.resolve();
|
|
14
|
+
ready;
|
|
15
|
+
constructor(directory = MESSAGE_METADATA_DIRECTORY) {
|
|
16
|
+
this.directory = directory;
|
|
17
|
+
}
|
|
18
|
+
recordPending(clientMessageId, value) {
|
|
19
|
+
return this.run(async () => {
|
|
20
|
+
const metadata = normalizeMetadata(value, false);
|
|
21
|
+
if (!clientMessageId || !metadata)
|
|
22
|
+
return undefined;
|
|
23
|
+
await this.ensureReady();
|
|
24
|
+
const storedMetadata = await persistMetadataPreviews(this.directory, clientMessageId, metadata);
|
|
25
|
+
const record = { version: STORAGE_VERSION, clientMessageId, createdAt: Date.now(), metadata: storedMetadata };
|
|
26
|
+
await writeJson(this.pendingFile(clientMessageId), record);
|
|
27
|
+
return structuredClone(storedMetadata);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
bindThread(clientMessageId, threadId) {
|
|
31
|
+
return this.run(async () => {
|
|
32
|
+
if (!clientMessageId || !threadId)
|
|
33
|
+
return;
|
|
34
|
+
await this.ensureReady();
|
|
35
|
+
const pendingFile = this.pendingFile(clientMessageId);
|
|
36
|
+
const targetFile = this.threadFile(threadId, clientMessageId);
|
|
37
|
+
const record = await readRecord(targetFile) || await readRecord(pendingFile);
|
|
38
|
+
if (!record)
|
|
39
|
+
return;
|
|
40
|
+
await writeJson(targetFile, { ...record, threadId, turnId: record.threadId === threadId ? record.turnId : undefined });
|
|
41
|
+
await removeFile(pendingFile);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
bindTurn(clientMessageId, threadId, turnId) {
|
|
45
|
+
return this.run(async () => {
|
|
46
|
+
if (!clientMessageId || !threadId || !turnId)
|
|
47
|
+
return;
|
|
48
|
+
await this.ensureReady();
|
|
49
|
+
const pendingFile = this.pendingFile(clientMessageId);
|
|
50
|
+
const targetFile = this.threadFile(threadId, clientMessageId);
|
|
51
|
+
const record = await readRecord(targetFile) || await readRecord(pendingFile);
|
|
52
|
+
if (!record)
|
|
53
|
+
return;
|
|
54
|
+
await writeJson(targetFile, { ...record, threadId, turnId });
|
|
55
|
+
await removeFile(pendingFile);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
mergeThread(threadId, messages) {
|
|
59
|
+
return this.run(async () => {
|
|
60
|
+
await this.ensureReady();
|
|
61
|
+
const records = (await readRecords(this.threadDirectory(threadId))).filter((item) => item.threadId === threadId && item.turnId);
|
|
62
|
+
const byTurnId = new Map(records.map((item) => [item.turnId, item]));
|
|
63
|
+
return messages.map((message) => {
|
|
64
|
+
if (message.role !== "user" || message.threadId !== threadId)
|
|
65
|
+
return message;
|
|
66
|
+
const record = byTurnId.get(message.turnId);
|
|
67
|
+
return record ? { ...message, clientMessageId: record.clientMessageId, ...structuredClone(record.metadata) } : message;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
remove(clientMessageId, threadId = "") {
|
|
72
|
+
return this.run(async () => {
|
|
73
|
+
if (!clientMessageId)
|
|
74
|
+
return;
|
|
75
|
+
await this.ensureReady();
|
|
76
|
+
await Promise.all([
|
|
77
|
+
removeFile(this.pendingFile(clientMessageId)),
|
|
78
|
+
...(threadId ? [removeFile(this.threadFile(threadId, clientMessageId))] : []),
|
|
79
|
+
fs.rm(this.assetDirectory(clientMessageId), { recursive: true, force: true }),
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
removeThread(threadId) {
|
|
84
|
+
return this.run(async () => {
|
|
85
|
+
if (!threadId)
|
|
86
|
+
return;
|
|
87
|
+
await this.ensureReady();
|
|
88
|
+
const records = await readRecords(this.threadDirectory(threadId));
|
|
89
|
+
await Promise.all([
|
|
90
|
+
fs.rm(this.threadDirectory(threadId), { recursive: true, force: true }),
|
|
91
|
+
...records.map((record) => fs.rm(this.assetDirectory(record.clientMessageId), { recursive: true, force: true })),
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
readAsset(messageKey, assetFile) {
|
|
96
|
+
return this.run(async () => {
|
|
97
|
+
await this.ensureReady();
|
|
98
|
+
if (!/^[a-f0-9]{64}$/.test(messageKey) || !/^[a-f0-9]{64}\.(?:gif|jpe?g|png|webp)$/.test(assetFile))
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
return { data: await fs.readFile(path.join(this.directory, "assets", messageKey, assetFile)), contentType: assetContentType(assetFile) };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (isMissing(error))
|
|
105
|
+
return undefined;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
run(task) {
|
|
111
|
+
const result = this.queue.then(task, task);
|
|
112
|
+
this.queue = result.then(() => undefined, () => undefined);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
ensureReady() {
|
|
116
|
+
this.ready ||= this.initialize();
|
|
117
|
+
return this.ready;
|
|
118
|
+
}
|
|
119
|
+
async initialize() {
|
|
120
|
+
const manifestFile = path.join(this.directory, MANIFEST_FILE);
|
|
121
|
+
try {
|
|
122
|
+
const manifest = await readJson(manifestFile);
|
|
123
|
+
if (manifest.version !== STORAGE_VERSION)
|
|
124
|
+
throw unsupportedVersion(manifest.version);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!isMissing(error))
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (await exists(this.directory))
|
|
132
|
+
throw new Error(`Message metadata store is missing ${MANIFEST_FILE}; refusing to overwrite it.`);
|
|
133
|
+
await createStorage(this.directory);
|
|
134
|
+
}
|
|
135
|
+
pendingFile(clientMessageId) {
|
|
136
|
+
return path.join(this.directory, "pending", `${storageKey(clientMessageId)}.json`);
|
|
137
|
+
}
|
|
138
|
+
threadDirectory(threadId) {
|
|
139
|
+
return path.join(this.directory, "threads", storageKey(threadId));
|
|
140
|
+
}
|
|
141
|
+
threadFile(threadId, clientMessageId) {
|
|
142
|
+
return path.join(this.threadDirectory(threadId), `${storageKey(clientMessageId)}.json`);
|
|
143
|
+
}
|
|
144
|
+
assetDirectory(clientMessageId) {
|
|
145
|
+
return path.join(this.directory, "assets", storageKey(clientMessageId));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export const messageMetadataStore = new MessageMetadataStore();
|
|
149
|
+
async function createStorage(directory) {
|
|
150
|
+
const temporaryDirectory = `${directory}.${process.pid}.${Date.now()}.tmp`;
|
|
151
|
+
try {
|
|
152
|
+
await fs.mkdir(path.join(temporaryDirectory, "pending"), { recursive: true });
|
|
153
|
+
await fs.mkdir(path.join(temporaryDirectory, "threads"), { recursive: true });
|
|
154
|
+
await fs.mkdir(path.join(temporaryDirectory, "assets"), { recursive: true });
|
|
155
|
+
await writeJson(path.join(temporaryDirectory, MANIFEST_FILE), { version: STORAGE_VERSION });
|
|
156
|
+
await fs.rename(temporaryDirectory, directory);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await fs.rm(temporaryDirectory, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function readRecords(directory) {
|
|
163
|
+
let files;
|
|
164
|
+
try {
|
|
165
|
+
files = (await fs.readdir(directory, { withFileTypes: true })).filter((item) => item.isFile() && item.name.endsWith(".json")).map((item) => path.join(directory, item.name));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (isMissing(error))
|
|
169
|
+
return [];
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
return (await Promise.all(files.map(readRecord))).filter((item) => Boolean(item));
|
|
173
|
+
}
|
|
174
|
+
async function readRecord(file) {
|
|
175
|
+
try {
|
|
176
|
+
const value = await readJson(file);
|
|
177
|
+
if (value.version !== STORAGE_VERSION)
|
|
178
|
+
throw unsupportedVersion(value.version);
|
|
179
|
+
const clientMessageId = text(value.clientMessageId, 200);
|
|
180
|
+
const metadata = normalizeMetadata(value.metadata, true);
|
|
181
|
+
if (!clientMessageId || !metadata)
|
|
182
|
+
throw new Error(`Message metadata record is invalid: ${file}`);
|
|
183
|
+
const threadId = text(value.threadId, 200);
|
|
184
|
+
const turnId = text(value.turnId, 200);
|
|
185
|
+
return { version: STORAGE_VERSION, clientMessageId, createdAt: Number(value.createdAt) || 0, metadata, ...(threadId ? { threadId } : {}), ...(turnId ? { turnId } : {}) };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (isMissing(error))
|
|
189
|
+
return undefined;
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function normalizeMetadata(value, allowAsset) {
|
|
194
|
+
if (value === undefined || value === null)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
197
|
+
throw new Error("Message metadata must be an object.");
|
|
198
|
+
const source = value;
|
|
199
|
+
const attachments = normalizeList(source.attachments, 6, (item) => normalizeAttachment(item, allowAsset), "attachments");
|
|
200
|
+
const canvasReferences = normalizeList(source.canvasReferences, 50, (item) => normalizeCanvasReference(item, allowAsset), "canvas references");
|
|
201
|
+
const skill = normalizeSkill(source.skill);
|
|
202
|
+
if (source.skill !== undefined && !skill)
|
|
203
|
+
throw new Error("Message skill metadata is invalid.");
|
|
204
|
+
if (!attachments.length && !canvasReferences.length && !skill)
|
|
205
|
+
return undefined;
|
|
206
|
+
return { ...(attachments.length ? { attachments } : {}), ...(canvasReferences.length ? { canvasReferences } : {}), ...(skill ? { skill } : {}) };
|
|
207
|
+
}
|
|
208
|
+
function normalizeList(value, limit, normalize, label) {
|
|
209
|
+
if (value === undefined)
|
|
210
|
+
return [];
|
|
211
|
+
if (!Array.isArray(value))
|
|
212
|
+
throw new Error(`Message ${label} metadata must be an array.`);
|
|
213
|
+
if (value.length > limit)
|
|
214
|
+
throw new Error(`Message ${label} metadata exceeds the limit of ${limit}.`);
|
|
215
|
+
const normalized = value.map(normalize).filter((item) => Boolean(item));
|
|
216
|
+
if (normalized.length !== value.length)
|
|
217
|
+
throw new Error(`Message ${label} metadata is invalid.`);
|
|
218
|
+
return normalized;
|
|
219
|
+
}
|
|
220
|
+
function normalizeAttachment(value, allowAsset) {
|
|
221
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
222
|
+
return undefined;
|
|
223
|
+
const item = value;
|
|
224
|
+
const id = text(item.id, 200);
|
|
225
|
+
const name = text(item.name, 500);
|
|
226
|
+
const url = imagePreview(item.url, allowAsset);
|
|
227
|
+
if (!id || !name || !url)
|
|
228
|
+
return undefined;
|
|
229
|
+
return { id, name, url, ...numberFields(item) };
|
|
230
|
+
}
|
|
231
|
+
function normalizeCanvasReference(value, allowAsset) {
|
|
232
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
233
|
+
return undefined;
|
|
234
|
+
const item = value;
|
|
235
|
+
const nodeId = text(item.nodeId, 200);
|
|
236
|
+
const label = text(item.label, 200);
|
|
237
|
+
const title = text(item.title, 500);
|
|
238
|
+
const kind = String(item.kind || "");
|
|
239
|
+
if (!nodeId || !label || !title || !["image", "video", "audio", "text"].includes(kind))
|
|
240
|
+
return undefined;
|
|
241
|
+
const previewUrl = kind === "image" ? imagePreview(item.previewUrl, allowAsset) : remotePreview(item.previewUrl);
|
|
242
|
+
const referenceText = text(item.text, MAX_TEXT_LENGTH);
|
|
243
|
+
return { nodeId, label, title, kind: kind, ...(previewUrl ? { previewUrl } : {}), ...(referenceText ? { text: referenceText } : {}) };
|
|
244
|
+
}
|
|
245
|
+
function normalizeSkill(value) {
|
|
246
|
+
if (value === undefined)
|
|
247
|
+
return undefined;
|
|
248
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
249
|
+
return undefined;
|
|
250
|
+
const item = value;
|
|
251
|
+
const name = text(item.name, 200);
|
|
252
|
+
const skillPath = text(item.path, 2000);
|
|
253
|
+
const displayName = text(item.displayName, 200);
|
|
254
|
+
return name && skillPath ? { name, path: skillPath, ...(displayName ? { displayName } : {}) } : undefined;
|
|
255
|
+
}
|
|
256
|
+
function numberFields(item) {
|
|
257
|
+
const fields = {};
|
|
258
|
+
const type = text(item.type, 200);
|
|
259
|
+
if (type)
|
|
260
|
+
fields.type = type;
|
|
261
|
+
for (const key of ["size", "width", "height"]) {
|
|
262
|
+
const value = Number(item[key]);
|
|
263
|
+
if (Number.isFinite(value) && value >= 0)
|
|
264
|
+
fields[key] = value;
|
|
265
|
+
}
|
|
266
|
+
return fields;
|
|
267
|
+
}
|
|
268
|
+
function imagePreview(value, allowAsset = false) {
|
|
269
|
+
if (typeof value !== "string")
|
|
270
|
+
return "";
|
|
271
|
+
const url = value.trim();
|
|
272
|
+
if (allowAsset && new RegExp(`^${MESSAGE_ASSET_PREFIX}[a-f0-9]{64}/[a-f0-9]{64}\\.(?:gif|jpe?g|png|webp)$`).test(url))
|
|
273
|
+
return url;
|
|
274
|
+
if (/^https?:\/\//i.test(url))
|
|
275
|
+
return url.length <= 5000 ? url : "";
|
|
276
|
+
return url.length <= MAX_PREVIEW_LENGTH && /^data:image\/[a-z0-9.+-]+;base64,/i.test(url) ? url : "";
|
|
277
|
+
}
|
|
278
|
+
function remotePreview(value) {
|
|
279
|
+
if (typeof value !== "string")
|
|
280
|
+
return "";
|
|
281
|
+
const url = value.trim();
|
|
282
|
+
return url.length <= 5000 && /^https?:\/\//i.test(url) ? url : "";
|
|
283
|
+
}
|
|
284
|
+
function text(value, limit) {
|
|
285
|
+
return typeof value === "string" ? value.trim().slice(0, limit) : "";
|
|
286
|
+
}
|
|
287
|
+
function storageKey(value) {
|
|
288
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
289
|
+
}
|
|
290
|
+
async function persistMetadataPreviews(directory, clientMessageId, metadata) {
|
|
291
|
+
const attachments = await Promise.all((metadata.attachments || []).map(async (item) => ({ ...item, url: await persistImagePreview(directory, clientMessageId, `attachment:${item.id}`, item.url) })));
|
|
292
|
+
const canvasReferences = await Promise.all((metadata.canvasReferences || []).map(async (item) => ({
|
|
293
|
+
...item,
|
|
294
|
+
...(item.kind === "image" && item.previewUrl ? { previewUrl: await persistImagePreview(directory, clientMessageId, `reference:${item.nodeId}`, item.previewUrl) } : {}),
|
|
295
|
+
})));
|
|
296
|
+
return {
|
|
297
|
+
...(attachments.length ? { attachments } : {}),
|
|
298
|
+
...(canvasReferences.length ? { canvasReferences } : {}),
|
|
299
|
+
...(metadata.skill ? { skill: metadata.skill } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function persistImagePreview(directory, clientMessageId, key, value) {
|
|
303
|
+
const match = value.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i);
|
|
304
|
+
if (!match)
|
|
305
|
+
return value;
|
|
306
|
+
const extension = imageExtension(match[1]);
|
|
307
|
+
if (!extension)
|
|
308
|
+
throw new Error(`Unsupported message preview type: ${match[1]}`);
|
|
309
|
+
const messageKey = storageKey(clientMessageId);
|
|
310
|
+
const assetFile = `${storageKey(key)}.${extension}`;
|
|
311
|
+
await writeFile(path.join(directory, "assets", messageKey, assetFile), Buffer.from(match[2], "base64"));
|
|
312
|
+
return `${MESSAGE_ASSET_PREFIX}${messageKey}/${assetFile}`;
|
|
313
|
+
}
|
|
314
|
+
function imageExtension(contentType) {
|
|
315
|
+
const type = contentType.toLowerCase();
|
|
316
|
+
if (type === "image/jpeg" || type === "image/jpg")
|
|
317
|
+
return "jpg";
|
|
318
|
+
if (type === "image/png")
|
|
319
|
+
return "png";
|
|
320
|
+
if (type === "image/webp")
|
|
321
|
+
return "webp";
|
|
322
|
+
if (type === "image/gif")
|
|
323
|
+
return "gif";
|
|
324
|
+
return "";
|
|
325
|
+
}
|
|
326
|
+
function assetContentType(file) {
|
|
327
|
+
const extension = path.extname(file).slice(1).toLowerCase();
|
|
328
|
+
if (extension === "jpg" || extension === "jpeg")
|
|
329
|
+
return "image/jpeg";
|
|
330
|
+
return `image/${extension}`;
|
|
331
|
+
}
|
|
332
|
+
async function readJson(file) {
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(await fs.readFile(file, "utf8"));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error instanceof SyntaxError)
|
|
338
|
+
throw new Error(`Message metadata JSON is invalid: ${file}`);
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function writeJson(file, value) {
|
|
343
|
+
await writeFile(file, JSON.stringify(value));
|
|
344
|
+
}
|
|
345
|
+
async function writeFile(file, value) {
|
|
346
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
347
|
+
const temporaryFile = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
348
|
+
try {
|
|
349
|
+
await fs.writeFile(temporaryFile, value);
|
|
350
|
+
await fs.rename(temporaryFile, file);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await removeFile(temporaryFile);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function removeFile(file) {
|
|
357
|
+
await fs.unlink(file).catch((error) => {
|
|
358
|
+
if (error.code !== "ENOENT")
|
|
359
|
+
throw error;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function exists(file) {
|
|
363
|
+
try {
|
|
364
|
+
await fs.access(file);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (isMissing(error))
|
|
369
|
+
return false;
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function unsupportedVersion(version) {
|
|
374
|
+
return new Error(`Unsupported message metadata version: ${String(version ?? "missing")}. Refusing to overwrite existing data.`);
|
|
375
|
+
}
|
|
376
|
+
function isMissing(error) {
|
|
377
|
+
return error?.code === "ENOENT";
|
|
378
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Agent 向网页广播事件的函数类型。 */
|
|
2
|
+
export type AgentEmit = (type: string, payload: unknown) => void;
|
|
3
|
+
/** 用户随当前 Agent 消息上传的附件。 */
|
|
4
|
+
export type AgentAttachment = {
|
|
5
|
+
id?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
dataUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
/** 用户消息在历史记录中展示所需的附件信息。 */
|
|
14
|
+
export type AgentAttachmentDisplay = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
url: string;
|
|
22
|
+
};
|
|
23
|
+
/** 用户消息引用的画布素材。 */
|
|
24
|
+
export type AgentCanvasReference = {
|
|
25
|
+
nodeId: string;
|
|
26
|
+
label: string;
|
|
27
|
+
title: string;
|
|
28
|
+
kind: "image" | "video" | "audio" | "text";
|
|
29
|
+
previewUrl?: string;
|
|
30
|
+
text?: string;
|
|
31
|
+
};
|
|
32
|
+
/** 用户消息调用的 Codex Skill。 */
|
|
33
|
+
export type AgentSkillReference = {
|
|
34
|
+
name: string;
|
|
35
|
+
path: string;
|
|
36
|
+
displayName?: string;
|
|
37
|
+
};
|
|
38
|
+
/** 独立于 Codex 原生线程历史保存的用户消息展示元数据。 */
|
|
39
|
+
export type AgentMessageMetadata = {
|
|
40
|
+
attachments?: AgentAttachmentDisplay[];
|
|
41
|
+
canvasReferences?: AgentCanvasReference[];
|
|
42
|
+
skill?: AgentSkillReference;
|
|
43
|
+
};
|
|
44
|
+
/** Codex 文件、命令和网络权限模式。 */
|
|
45
|
+
export type AgentPermissionMode = "request" | "automatic" | "full";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolName } from "./schemas.js";
|
|
2
|
+
import type { CanvasSnapshot } from "./types.js";
|
|
3
|
+
export type CanvasToolRequest = {
|
|
4
|
+
name: "canvas_apply_ops";
|
|
5
|
+
input: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
/** 将上层画布工具调用转换为前端可执行的批量操作。 */
|
|
8
|
+
export declare function buildCanvasToolRequest(name: ToolName, input: Record<string, unknown>, state: CanvasSnapshot | null): CanvasToolRequest;
|
|
9
|
+
/** 按最大边限制计算附件图片节点尺寸,并保持原始比例。 */
|
|
10
|
+
export declare function fitAttachmentNodeSize(width: number, height: number): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { nextCanvasX } from "./tools.js";
|
|
3
|
+
/** 将上层画布工具调用转换为前端可执行的批量操作。 */
|
|
4
|
+
export function buildCanvasToolRequest(name, input, state) {
|
|
5
|
+
if (name === "canvas_apply_ops")
|
|
6
|
+
return { name, input };
|
|
7
|
+
if (name === "canvas_create_node") {
|
|
8
|
+
const data = input;
|
|
9
|
+
return applyOps([{ type: "add_node", nodeType: data.nodeType, title: data.title, position: { x: data.x ?? nextCanvasX(state), y: data.y ?? 0 }, width: data.width, height: data.height, metadata: data.metadata }]);
|
|
10
|
+
}
|
|
11
|
+
if (name === "canvas_create_text_node") {
|
|
12
|
+
const data = input;
|
|
13
|
+
return applyOps([textNodeOp(data, data.x ?? nextCanvasX(state), data.y ?? 0)]);
|
|
14
|
+
}
|
|
15
|
+
if (name === "canvas_create_text_nodes") {
|
|
16
|
+
const data = input;
|
|
17
|
+
const x = Number(data.x ?? nextCanvasX(state));
|
|
18
|
+
const y = Number(data.y ?? 0);
|
|
19
|
+
const gap = Number(data.gap ?? 40);
|
|
20
|
+
return applyOps(data.items.map((item, index) => textNodeOp(item, item.x ?? (data.direction === "row" ? x + index * (340 + gap) : x), item.y ?? (data.direction === "row" ? y : y + index * (240 + gap)))));
|
|
21
|
+
}
|
|
22
|
+
if (name === "canvas_create_image_prompt_flow")
|
|
23
|
+
return applyOps(generationFlowOps({ ...input, mode: "image" }, state));
|
|
24
|
+
if (name === "canvas_create_config_node") {
|
|
25
|
+
const x = Number(input.x ?? nextCanvasX(state));
|
|
26
|
+
const y = Number(input.y ?? 0);
|
|
27
|
+
const configId = `config-${crypto.randomUUID()}`;
|
|
28
|
+
const mode = generationMode(input.mode);
|
|
29
|
+
const prompt = String(input.prompt || "");
|
|
30
|
+
return applyOps([configNodeOp(configId, input, x, y), ...(input.autoRun ? [runGenerationOp(configId, mode, prompt)] : [])]);
|
|
31
|
+
}
|
|
32
|
+
if (name === "canvas_create_generation_flow")
|
|
33
|
+
return applyOps(generationFlowOps(input, state));
|
|
34
|
+
if (name === "canvas_generate_text" || name === "canvas_generate_image" || name === "canvas_generate_video" || name === "canvas_generate_audio") {
|
|
35
|
+
return applyOps(generationFlowOps({ ...input, mode: name.replace("canvas_generate_", ""), autoRun: true }, state));
|
|
36
|
+
}
|
|
37
|
+
if (name === "canvas_update_node") {
|
|
38
|
+
const data = input;
|
|
39
|
+
return applyOps([{ type: "update_node", id: data.id, patch: data.patch, metadata: data.metadata }]);
|
|
40
|
+
}
|
|
41
|
+
if (name === "canvas_update_node_text") {
|
|
42
|
+
const data = input;
|
|
43
|
+
return applyOps([{ type: "update_node", id: data.id, patch: { ...(data.title ? { title: data.title } : {}) }, metadata: { content: data.text, status: "success" } }]);
|
|
44
|
+
}
|
|
45
|
+
if (name === "canvas_move_nodes") {
|
|
46
|
+
const data = input;
|
|
47
|
+
return applyOps(data.items.map((item) => {
|
|
48
|
+
const current = findNode(state, item.id);
|
|
49
|
+
return { type: "update_node", id: item.id, patch: { position: { x: item.x ?? ((current?.position.x || 0) + (item.dx || 0)), y: item.y ?? ((current?.position.y || 0) + (item.dy || 0)) } } };
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
if (name === "canvas_resize_node") {
|
|
53
|
+
const data = input;
|
|
54
|
+
return applyOps([{ type: "update_node", id: data.id, patch: { width: data.width, height: data.height }, metadata: data.freeResize === undefined ? undefined : { freeResize: data.freeResize } }]);
|
|
55
|
+
}
|
|
56
|
+
if (name === "canvas_delete_nodes")
|
|
57
|
+
return applyOps([{ type: "delete_node", ids: input.ids }]);
|
|
58
|
+
if (name === "canvas_connect_nodes") {
|
|
59
|
+
const data = input;
|
|
60
|
+
return applyOps(data.connections.map((connection) => ({ type: "connect_nodes", ...connection })));
|
|
61
|
+
}
|
|
62
|
+
if (name === "canvas_select_nodes")
|
|
63
|
+
return applyOps([{ type: "select_nodes", ids: input.ids }]);
|
|
64
|
+
if (name === "canvas_set_viewport")
|
|
65
|
+
return applyOps([{ type: "set_viewport", viewport: input.viewport }]);
|
|
66
|
+
if (name === "canvas_run_generation") {
|
|
67
|
+
const data = input;
|
|
68
|
+
return applyOps([runGenerationOp(data.nodeId, generationMode(data.mode), data.prompt)]);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`未知工具:${name}`);
|
|
71
|
+
}
|
|
72
|
+
/** 按最大边限制计算附件图片节点尺寸,并保持原始比例。 */
|
|
73
|
+
export function fitAttachmentNodeSize(width, height) {
|
|
74
|
+
const scale = Math.min(1, 640 / width, 640 / height);
|
|
75
|
+
return { width: width * scale, height: height * scale };
|
|
76
|
+
}
|
|
77
|
+
/** 创建统一的批量画布操作请求。 */
|
|
78
|
+
function applyOps(ops) {
|
|
79
|
+
return { name: "canvas_apply_ops", input: { ops } };
|
|
80
|
+
}
|
|
81
|
+
/** 创建文本节点操作。 */
|
|
82
|
+
function textNodeOp(input, x, y) {
|
|
83
|
+
return { type: "add_node", id: input.id, nodeType: "text", title: input.title, position: { x, y }, width: input.width, height: input.height, metadata: { content: input.text || "", status: "success", fontSize: 14 } };
|
|
84
|
+
}
|
|
85
|
+
/** 创建生成配置节点操作。 */
|
|
86
|
+
function configNodeOp(id, input, x, y) {
|
|
87
|
+
const mode = generationMode(input.mode);
|
|
88
|
+
const prompt = String(input.prompt || "");
|
|
89
|
+
return {
|
|
90
|
+
type: "add_node",
|
|
91
|
+
id,
|
|
92
|
+
nodeType: "config",
|
|
93
|
+
title: String(input.title || generationTitle(mode)),
|
|
94
|
+
position: { x, y },
|
|
95
|
+
width: typeof input.width === "number" ? input.width : undefined,
|
|
96
|
+
height: typeof input.height === "number" ? input.height : undefined,
|
|
97
|
+
metadata: cleanRecord({
|
|
98
|
+
generationMode: mode,
|
|
99
|
+
composerContent: prompt,
|
|
100
|
+
prompt,
|
|
101
|
+
status: "idle",
|
|
102
|
+
model: input.model,
|
|
103
|
+
size: input.size,
|
|
104
|
+
quality: input.quality,
|
|
105
|
+
count: input.count,
|
|
106
|
+
seconds: input.seconds,
|
|
107
|
+
vquality: input.vquality,
|
|
108
|
+
generateAudio: input.generateAudio,
|
|
109
|
+
watermark: input.watermark,
|
|
110
|
+
videoMode: input.videoMode,
|
|
111
|
+
audioVoice: input.audioVoice,
|
|
112
|
+
audioFormat: input.audioFormat,
|
|
113
|
+
audioSpeed: input.audioSpeed,
|
|
114
|
+
audioInstructions: input.audioInstructions,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** 创建包含提示词、配置节点和引用连线的生成流程。 */
|
|
119
|
+
function generationFlowOps(input, state) {
|
|
120
|
+
const mode = generationMode(input.mode);
|
|
121
|
+
const prompt = String(input.prompt || "");
|
|
122
|
+
const x = Number(input.x ?? nextCanvasX(state));
|
|
123
|
+
const y = Number(input.y ?? 0);
|
|
124
|
+
const textId = `text-${crypto.randomUUID()}`;
|
|
125
|
+
const configId = `config-${crypto.randomUUID()}`;
|
|
126
|
+
const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id) => typeof id === "string") : [];
|
|
127
|
+
// When the prompt only @-mentions nodes already passed as references, reuse them instead of minting a duplicate text node.
|
|
128
|
+
const mentionedIds = [...prompt.matchAll(/@\[node:([\w-]+)\]/g)].map((match) => match[1]);
|
|
129
|
+
const reuseReferences = referenceNodeIds.length > 0 && mentionedIds.length > 0
|
|
130
|
+
&& mentionedIds.every((id) => referenceNodeIds.includes(id))
|
|
131
|
+
&& prompt.replace(/@\[node:[\w-]+\]/g, "").trim() === "";
|
|
132
|
+
const tokens = reuseReferences ? referenceNodeIds.map((id) => `@[node:${id}]`) : [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)];
|
|
133
|
+
return [
|
|
134
|
+
...(reuseReferences ? [] : [textNodeOp({ id: textId, text: prompt, title: String(input.title || "提示词") }, x, y)]),
|
|
135
|
+
configNodeOp(configId, { ...input, prompt: tokens.join("\n") }, x + 420, y),
|
|
136
|
+
...(reuseReferences ? [] : [{ type: "connect_nodes", fromNodeId: textId, toNodeId: configId }]),
|
|
137
|
+
...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: configId })),
|
|
138
|
+
{ type: "select_nodes", ids: [configId] },
|
|
139
|
+
...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
/** 创建触发节点生成的画布操作。 */
|
|
143
|
+
function runGenerationOp(nodeId, mode, prompt) {
|
|
144
|
+
return { type: "run_generation", nodeId, mode, prompt };
|
|
145
|
+
}
|
|
146
|
+
/** 将未知生成模式归一为画布支持的模式。 */
|
|
147
|
+
function generationMode(value) {
|
|
148
|
+
return value === "text" || value === "video" || value === "audio" ? value : "image";
|
|
149
|
+
}
|
|
150
|
+
/** 获取生成模式对应的默认节点标题。 */
|
|
151
|
+
function generationTitle(mode) {
|
|
152
|
+
if (mode === "text")
|
|
153
|
+
return "文本生成";
|
|
154
|
+
if (mode === "video")
|
|
155
|
+
return "视频生成";
|
|
156
|
+
if (mode === "audio")
|
|
157
|
+
return "音频生成";
|
|
158
|
+
return "图片生成";
|
|
159
|
+
}
|
|
160
|
+
/** 按节点 ID 查找当前画布节点。 */
|
|
161
|
+
function findNode(state, id) {
|
|
162
|
+
return (state?.nodes || []).find((node) => node.id === id);
|
|
163
|
+
}
|
|
164
|
+
/** 移除对象中未设置的生成参数。 */
|
|
165
|
+
function cleanRecord(value) {
|
|
166
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== ""));
|
|
167
|
+
}
|