minecodex 1.2.0 → 1.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/features/images/codex-feature.json +11 -2
- package/features/images/src/http-server.mjs +15 -5
- package/features/images/src/image-library.mjs +37 -9
- package/features/images/src/prompt-index.mjs +27 -24
- package/features/images/src/prompt-source.mjs +100 -0
- package/features/images/web/app.js +180 -67
- package/features/images/web/i18n.mjs +5 -0
- package/features/images/web/index.html +29 -24
- package/features/images/web/styles.css +169 -139
- package/features/model-slider/README.md +13 -1
- package/features/session-status/DESIGN.md +2 -0
- package/features/session-status/README.md +7 -1
- package/features/session-status/src/session-status.mjs +77 -5
- package/package.json +5 -2
- package/packages/runtime-host/src/cloud-images.mjs +123 -0
- package/packages/runtime-host/src/codex-cdp.mjs +42 -8
- package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
- package/packages/runtime-host/src/codex-injection.mjs +326 -113
- package/packages/runtime-host/src/codex-runtime.mjs +88 -23
- package/packages/runtime-host/src/feature-registry.mjs +2 -0
- package/packages/runtime-host/src/session-status-reader.mjs +25 -8
|
@@ -16,12 +16,21 @@
|
|
|
16
16
|
"pageScript": {
|
|
17
17
|
"resource": "src/chatgpt-image-source-page.mjs"
|
|
18
18
|
},
|
|
19
|
-
"hostActions": [
|
|
19
|
+
"hostActions": [
|
|
20
|
+
"import-generated-image",
|
|
21
|
+
"create-image-edit-thread",
|
|
22
|
+
"open-sidebar",
|
|
23
|
+
"list-cloud-images",
|
|
24
|
+
"import-cloud-image"
|
|
25
|
+
],
|
|
20
26
|
"placement": {
|
|
21
27
|
"after": "sites",
|
|
22
28
|
"order": 10
|
|
23
29
|
},
|
|
24
|
-
"command": [
|
|
30
|
+
"command": [
|
|
31
|
+
"node",
|
|
32
|
+
"src/main.mjs"
|
|
33
|
+
],
|
|
25
34
|
"icon": {
|
|
26
35
|
"viewBox": "0 0 24 24",
|
|
27
36
|
"markup": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"></rect><circle cx=\"9\" cy=\"9\" r=\"2\"></circle><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"></path>"
|
|
@@ -48,7 +48,7 @@ function publicImage(image, { includePrompt = false } = {}) {
|
|
|
48
48
|
downloadUrl: `/api/images/${image.id}/download`,
|
|
49
49
|
sourceUrl: image.threadId.startsWith("chatgpt:") ? null : `codex://threads/${image.threadId}`,
|
|
50
50
|
};
|
|
51
|
-
if (includePrompt) result.prompt = image.prompt;
|
|
51
|
+
if (includePrompt) { result.prompt = image.prompt; result.promptKind = image.promptKind; }
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -295,11 +295,13 @@ export async function createHttpServer({
|
|
|
295
295
|
|
|
296
296
|
if (request.method === "GET" && url.pathname === "/api/images") {
|
|
297
297
|
const { limit, offset } = paginationFor(url);
|
|
298
|
+
const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
298
299
|
const source = url.searchParams.get("source") ?? "all";
|
|
299
300
|
const threadId = url.searchParams.get("threadId") || null;
|
|
300
301
|
const sort = url.searchParams.get("sort") === "asc" ? "asc" : "desc";
|
|
301
302
|
const query = url.searchParams.get("q") ?? "";
|
|
302
303
|
const images = library.list({
|
|
304
|
+
snapshot,
|
|
303
305
|
query,
|
|
304
306
|
source,
|
|
305
307
|
threadId,
|
|
@@ -309,7 +311,8 @@ export async function createHttpServer({
|
|
|
309
311
|
}).map(publicImage);
|
|
310
312
|
json(response, 200, {
|
|
311
313
|
images,
|
|
312
|
-
count: library.count({ query, source, threadId }),
|
|
314
|
+
count: library.count({ query, source, threadId, snapshot }),
|
|
315
|
+
snapshot,
|
|
313
316
|
limit,
|
|
314
317
|
offset,
|
|
315
318
|
});
|
|
@@ -323,18 +326,25 @@ export async function createHttpServer({
|
|
|
323
326
|
|
|
324
327
|
if (request.method === "GET" && url.pathname === "/api/image-groups") {
|
|
325
328
|
const { limit, offset } = paginationFor(url);
|
|
329
|
+
const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
326
330
|
const source = url.searchParams.get("source") ?? "all";
|
|
327
|
-
const groups = library.groups({ source, limit, offset }).map(publicGroup);
|
|
331
|
+
const groups = library.groups({ source, limit, offset, snapshot }).map(publicGroup);
|
|
328
332
|
json(response, 200, {
|
|
329
333
|
groups,
|
|
330
|
-
count: library.countGroups({ source }),
|
|
331
|
-
imageCount: library.count({ source }),
|
|
334
|
+
count: library.countGroups({ source, snapshot }),
|
|
335
|
+
imageCount: library.count({ source, snapshot }),
|
|
336
|
+
snapshot,
|
|
332
337
|
limit,
|
|
333
338
|
offset,
|
|
334
339
|
});
|
|
335
340
|
return;
|
|
336
341
|
}
|
|
337
342
|
|
|
343
|
+
if (request.method === "GET" && url.pathname === "/api/cloud-image-cache") {
|
|
344
|
+
json(response, 200, { items: library.cloudCache() });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
338
348
|
if (request.method === "POST" && url.pathname === "/api/rescan") {
|
|
339
349
|
if (library.rescanStatus().inProgress) {
|
|
340
350
|
json(response, 202, { imported: 0, total: library.count(), inProgress: true });
|
|
@@ -28,10 +28,15 @@ function imageIdFor(sourcePath) {
|
|
|
28
28
|
return createHash("sha256").update(sourcePath).digest("hex").slice(0, 24);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function buildWhere({ query = "", source = "all", threadId = null } = {}) {
|
|
31
|
+
function buildWhere({ query = "", source = "all", threadId = null, snapshot = null } = {}) {
|
|
32
32
|
const clauses = [];
|
|
33
33
|
const parameters = [];
|
|
34
34
|
|
|
35
|
+
if (snapshot !== null) {
|
|
36
|
+
clauses.push("images.rowid <= ?");
|
|
37
|
+
parameters.push(snapshot);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
if (threadId) {
|
|
36
41
|
clauses.push("images.thread_id = ?");
|
|
37
42
|
parameters.push(threadId);
|
|
@@ -153,6 +158,7 @@ function rowToImage(row) {
|
|
|
153
158
|
imageGenerationItemId: row.image_generation_item_id,
|
|
154
159
|
sourcePath: row.source_path,
|
|
155
160
|
prompt: row.prompt,
|
|
161
|
+
promptKind: row.prompt_kind ?? "generation",
|
|
156
162
|
width: row.width,
|
|
157
163
|
height: row.height,
|
|
158
164
|
byteLength: row.byte_length,
|
|
@@ -235,6 +241,10 @@ export class ImageLibrary {
|
|
|
235
241
|
updated_at TEXT NOT NULL
|
|
236
242
|
);
|
|
237
243
|
`);
|
|
244
|
+
const imageColumns = new Set(this.database.prepare("PRAGMA table_info(images)").all().map(column => column.name));
|
|
245
|
+
for (const name of ["prompt_kind", "cloud_library_id", "cloud_version"]) {
|
|
246
|
+
if (!imageColumns.has(name)) this.database.exec(`ALTER TABLE images ADD COLUMN ${name} TEXT`);
|
|
247
|
+
}
|
|
238
248
|
const threadSourceColumns = new Set(
|
|
239
249
|
this.database.prepare("PRAGMA table_info(thread_sources)").all().map((column) => column.name),
|
|
240
250
|
);
|
|
@@ -299,6 +309,10 @@ export class ImageLibrary {
|
|
|
299
309
|
});
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
snapshot() {
|
|
313
|
+
return this.database.prepare("SELECT COALESCE(MAX(rowid), 0) AS value FROM images").get().value;
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
count(options = {}) {
|
|
303
317
|
const { sql, parameters } = buildWhere(options);
|
|
304
318
|
return this.database.prepare(`
|
|
@@ -309,8 +323,8 @@ export class ImageLibrary {
|
|
|
309
323
|
`).get(...parameters).count;
|
|
310
324
|
}
|
|
311
325
|
|
|
312
|
-
list({ query = "", source = "all", threadId = null, sort = "desc", limit = null, offset = 0 } = {}) {
|
|
313
|
-
const { sql, parameters } = buildWhere({ query, source, threadId });
|
|
326
|
+
list({ query = "", source = "all", threadId = null, sort = "desc", limit = null, offset = 0, snapshot = null } = {}) {
|
|
327
|
+
const { sql, parameters } = buildWhere({ query, source, threadId, snapshot });
|
|
314
328
|
const pagination = Number.isInteger(limit) ? "LIMIT ? OFFSET ?" : "";
|
|
315
329
|
if (pagination) parameters.push(limit, offset);
|
|
316
330
|
const direction = sort === "asc" ? "ASC" : "DESC";
|
|
@@ -358,8 +372,8 @@ export class ImageLibrary {
|
|
|
358
372
|
return { total: this.count(), chats, works, projects };
|
|
359
373
|
}
|
|
360
374
|
|
|
361
|
-
countGroups({ source = "all" } = {}) {
|
|
362
|
-
const { sql, parameters } = buildWhere({ source });
|
|
375
|
+
countGroups({ source = "all", snapshot = null } = {}) {
|
|
376
|
+
const { sql, parameters } = buildWhere({ source, snapshot });
|
|
363
377
|
return this.database.prepare(`
|
|
364
378
|
SELECT COUNT(DISTINCT images.thread_id) AS count
|
|
365
379
|
FROM images
|
|
@@ -368,8 +382,8 @@ export class ImageLibrary {
|
|
|
368
382
|
`).get(...parameters).count;
|
|
369
383
|
}
|
|
370
384
|
|
|
371
|
-
groups({ source = "all", limit = null, offset = 0, previewLimit = 4 } = {}) {
|
|
372
|
-
const { sql, parameters } = buildWhere({ source });
|
|
385
|
+
groups({ source = "all", limit = null, offset = 0, previewLimit = 4, snapshot = null } = {}) {
|
|
386
|
+
const { sql, parameters } = buildWhere({ source, snapshot });
|
|
373
387
|
const pagination = Number.isInteger(limit) ? "LIMIT ? OFFSET ?" : "";
|
|
374
388
|
if (pagination) parameters.push(limit, offset);
|
|
375
389
|
const rows = this.database.prepare(`
|
|
@@ -400,10 +414,11 @@ export class ImageLibrary {
|
|
|
400
414
|
FROM images
|
|
401
415
|
LEFT JOIN thread_sources ON thread_sources.thread_id = images.thread_id
|
|
402
416
|
WHERE images.thread_id IN (${placeholders})
|
|
417
|
+
${snapshot === null ? "" : "AND images.rowid <= ?"}
|
|
403
418
|
)
|
|
404
419
|
WHERE preview_rank <= ?
|
|
405
420
|
ORDER BY thread_id ASC, created_at ASC, id ASC
|
|
406
|
-
`).all(...threadIds, previewLimit);
|
|
421
|
+
`).all(...threadIds, ...(snapshot === null ? [] : [snapshot]), previewLimit);
|
|
407
422
|
for (const previewRow of previewRows) {
|
|
408
423
|
const previews = previewsByThread.get(previewRow.thread_id) ?? [];
|
|
409
424
|
previews.push(rowToImage(previewRow));
|
|
@@ -621,6 +636,10 @@ export class ImageLibrary {
|
|
|
621
636
|
return true;
|
|
622
637
|
}
|
|
623
638
|
|
|
639
|
+
cloudCache() {
|
|
640
|
+
return this.database.prepare("SELECT cloud_library_id AS libraryId, cloud_version AS version FROM images WHERE cloud_library_id IS NOT NULL").all();
|
|
641
|
+
}
|
|
642
|
+
|
|
624
643
|
async importExternalImage({
|
|
625
644
|
bytes,
|
|
626
645
|
mimeType,
|
|
@@ -630,6 +649,9 @@ export class ImageLibrary {
|
|
|
630
649
|
sourceKind,
|
|
631
650
|
prompt = null,
|
|
632
651
|
createdAt = null,
|
|
652
|
+
promptKind = "generation",
|
|
653
|
+
cloudLibraryId = null,
|
|
654
|
+
cloudVersion = null,
|
|
633
655
|
} = {}) {
|
|
634
656
|
if (!this.isLifecycleActive() || !Buffer.isBuffer(bytes)) return { imported: false, reason: "INVALID_BYTES" };
|
|
635
657
|
const normalizedMime = normalizedImportMime(mimeType);
|
|
@@ -660,10 +682,15 @@ export class ImageLibrary {
|
|
|
660
682
|
const assetDir = path.join(this.dataDir, "external-images");
|
|
661
683
|
const sourcePath = path.join(assetDir, `${id}.${extension}`);
|
|
662
684
|
const existing = this.database.prepare("SELECT source_path, byte_length FROM images WHERE id = ?").get(id);
|
|
685
|
+
const updateCloudMetadata = () => {
|
|
686
|
+
if (typeof cloudLibraryId !== "string" || !/^libfile_[\w-]+$/.test(cloudLibraryId)) return;
|
|
687
|
+
this.database.prepare("UPDATE images SET cloud_library_id = ?, cloud_version = ?, prompt = ?, prompt_kind = ? WHERE id = ?")
|
|
688
|
+
.run(cloudLibraryId, String(cloudVersion ?? "").slice(0, 256), safePrompt, promptKind === "request" ? "request" : "generation", id);
|
|
689
|
+
};
|
|
663
690
|
if (existing && existing.byte_length === bytes.length) {
|
|
664
691
|
try {
|
|
665
692
|
const existingBytes = await readFile(existing.source_path);
|
|
666
|
-
if (existingBytes.equals(bytes)) return { imported: false, id, reason: "DUPLICATE" };
|
|
693
|
+
if (existingBytes.equals(bytes)) { updateCloudMetadata(); return { imported: false, id, reason: "DUPLICATE" }; }
|
|
667
694
|
} catch (error) {
|
|
668
695
|
if (error.code !== "ENOENT") throw error;
|
|
669
696
|
}
|
|
@@ -712,6 +739,7 @@ export class ImageLibrary {
|
|
|
712
739
|
normalizedCreatedAt,
|
|
713
740
|
importedAt,
|
|
714
741
|
);
|
|
742
|
+
updateCloudMetadata();
|
|
715
743
|
this.database
|
|
716
744
|
.prepare(`
|
|
717
745
|
INSERT INTO thread_sources (
|
|
@@ -3,6 +3,8 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
|
|
6
|
+
import { tracePromptSource, recordedCommandOutput } from "./prompt-source.mjs";
|
|
7
|
+
|
|
6
8
|
const DEFAULT_NEGATIVE_CACHE_TTL_MS = 5_000;
|
|
7
9
|
|
|
8
10
|
async function findThreadLogs(directory, threadId, output = []) {
|
|
@@ -28,40 +30,26 @@ function legacyPromptFromPayload(payload) {
|
|
|
28
30
|
return { imageGenerationItemId: payload.call_id, prompt: payload.revised_prompt };
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
function wrappedImagegenPrompt(payload) {
|
|
32
|
-
if (payload?.type !== "custom_tool_call" || payload.name !== "exec") return null;
|
|
33
|
-
if (typeof payload.call_id !== "string" || typeof payload.input !== "string") return null;
|
|
34
|
-
const marker = "tools.image_gen__imagegen";
|
|
35
|
-
const markerIndex = payload.input.indexOf(marker);
|
|
36
|
-
if (markerIndex < 0) return null;
|
|
37
|
-
const generatedImageIndex = payload.input.indexOf("generatedImage(", markerIndex + marker.length);
|
|
38
|
-
const callSource = payload.input.slice(markerIndex, generatedImageIndex < 0 ? undefined : generatedImageIndex);
|
|
39
|
-
const match = callSource.match(/\bprompt\s*:\s*("(?:\\.|[^"\\])*")/s);
|
|
40
|
-
if (!match) return null;
|
|
41
|
-
try {
|
|
42
|
-
const prompt = JSON.parse(match[1]);
|
|
43
|
-
return typeof prompt === "string" ? { callId: payload.call_id, prompt } : null;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
33
|
function generatedItemIdsFromOutputLine(line) {
|
|
50
|
-
if (
|
|
51
|
-
const callId = line.match(/"call_id"
|
|
34
|
+
if (!/"type"\s*:\s*"(?:custom_tool_call_output|function_call_output)"/.test(line)) return null;
|
|
35
|
+
const callId = line.match(/"call_id"\s*:\s*"([a-zA-Z0-9_-]+)"/)?.[1];
|
|
52
36
|
if (!callId) return null;
|
|
53
37
|
const itemIds = new Set();
|
|
54
38
|
const pattern = /generated_images[\\/][^\\/\s]+[\\/]([a-zA-Z0-9_-]+)\.png\b/g;
|
|
55
39
|
for (const match of line.matchAll(pattern)) itemIds.add(match[1]);
|
|
56
|
-
|
|
40
|
+
const cellId = line.match(/Script running with cell ID ([a-zA-Z0-9_-]+)/)?.[1];
|
|
41
|
+
return { callId, itemIds, cellId };
|
|
57
42
|
}
|
|
58
43
|
|
|
59
44
|
async function readPrompts(logPath, prompts) {
|
|
60
45
|
const input = createReadStream(logPath, { encoding: "utf8" });
|
|
61
46
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
62
47
|
const wrappedPrompts = new Map();
|
|
48
|
+
const cellPrompts = new Map();
|
|
49
|
+
const stored = new Map();
|
|
50
|
+
const pendingSources = new Map();
|
|
63
51
|
for await (const line of lines) {
|
|
64
|
-
if (line.includes('"image_generation_end"') || line.includes(
|
|
52
|
+
if (line.includes('"image_generation_end"') || /"type"\s*:\s*"custom_tool_call"/.test(line) || /"name"\s*:\s*"wait"/.test(line) || [...pendingSources.keys()].some(id => line.includes(id))) {
|
|
65
53
|
let event;
|
|
66
54
|
try { event = JSON.parse(line); } catch { continue; }
|
|
67
55
|
const payload = event?.payload ?? event;
|
|
@@ -69,12 +57,27 @@ async function readPrompts(logPath, prompts) {
|
|
|
69
57
|
if (legacy && !prompts.has(legacy.imageGenerationItemId)) {
|
|
70
58
|
prompts.set(legacy.imageGenerationItemId, legacy.prompt);
|
|
71
59
|
}
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
if (payload?.type === "custom_tool_call" && payload.name === "exec" && typeof payload.input === "string") {
|
|
61
|
+
const prompt = tracePromptSource(payload.input, new Map(stored));
|
|
62
|
+
if (prompt) wrappedPrompts.set(payload.call_id, prompt);
|
|
63
|
+
if (/\bstore\s*\(/.test(payload.input)) pendingSources.set(payload.call_id, payload.input);
|
|
64
|
+
}
|
|
65
|
+
if (payload?.type === "custom_tool_call_output" && pendingSources.has(payload.call_id)) {
|
|
66
|
+
tracePromptSource(pendingSources.get(payload.call_id), stored, recordedCommandOutput(payload.output));
|
|
67
|
+
pendingSources.delete(payload.call_id);
|
|
68
|
+
}
|
|
69
|
+
if (payload?.type === "function_call" && payload.name === "wait") {
|
|
70
|
+
try {
|
|
71
|
+
const args = JSON.parse(payload.arguments);
|
|
72
|
+
const prompt = cellPrompts.get(String(args.cell_id));
|
|
73
|
+
if (prompt) wrappedPrompts.set(payload.call_id, prompt);
|
|
74
|
+
} catch { /* 不完整的调用记录无法关联。 */ }
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
77
|
const generated = generatedItemIdsFromOutputLine(line);
|
|
76
78
|
const prompt = generated ? wrappedPrompts.get(generated.callId) : null;
|
|
77
79
|
if (!prompt) continue;
|
|
80
|
+
if (generated.cellId) cellPrompts.set(generated.cellId, prompt);
|
|
78
81
|
for (const itemId of generated.itemIds) {
|
|
79
82
|
if (!prompts.has(itemId)) prompts.set(itemId, prompt);
|
|
80
83
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parse } from "acorn";
|
|
2
|
+
|
|
3
|
+
// 仅解释可确定的字符串表达式,不执行会话中的 JavaScript 或历史工具。
|
|
4
|
+
export function tracePromptSource(source, stored, recordedOutput) {
|
|
5
|
+
let program;
|
|
6
|
+
try { program = parse(source, { ecmaVersion: "latest", sourceType: "module" }); }
|
|
7
|
+
catch { stored.clear(); return null; }
|
|
8
|
+
const locals = new Map();
|
|
9
|
+
const isCall = (node, name) => node?.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === name;
|
|
10
|
+
const isTool = (node, name) => node?.type === "CallExpression" && node.callee.type === "MemberExpression"
|
|
11
|
+
&& !node.callee.computed && node.callee.object.name === "tools" && node.callee.property.name === name;
|
|
12
|
+
const statements = program.body;
|
|
13
|
+
const commandVariables = statements.flatMap(statement => statement.type === "VariableDeclaration"
|
|
14
|
+
? statement.declarations.filter(d => d.id.type === "Identifier" && isTool(d.init?.argument, "exec_command")).map(d => d.id.name)
|
|
15
|
+
: []);
|
|
16
|
+
const echoedOutput = commandVariables.length === 1 && statements.some(statement => {
|
|
17
|
+
const call = statement.expression;
|
|
18
|
+
const arg = call?.arguments?.[0];
|
|
19
|
+
return isCall(call, "text") && call.arguments.length === 1 && arg?.type === "MemberExpression"
|
|
20
|
+
&& !arg.computed && arg.object.name === commandVariables[0] && arg.property.name === "output";
|
|
21
|
+
});
|
|
22
|
+
let prompt = null;
|
|
23
|
+
let imageCalls = 0;
|
|
24
|
+
function value(node) {
|
|
25
|
+
if (!node) return undefined;
|
|
26
|
+
if (node.type === "Literal") return typeof node.value === "string" || typeof node.value === "number" ? node.value : undefined;
|
|
27
|
+
if (node.type === "Identifier") return locals.get(node.name);
|
|
28
|
+
if (node.type === "AssignmentExpression" && node.left.type === "Identifier") {
|
|
29
|
+
const result = node.operator === "=" ? value(node.right) : undefined;
|
|
30
|
+
locals.set(node.left.name, result);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
if (node.type === "TemplateLiteral") {
|
|
34
|
+
const parts = node.expressions.map(value);
|
|
35
|
+
if (parts.some(part => typeof part !== "string" && typeof part !== "number")) return undefined;
|
|
36
|
+
return node.quasis.map((part, i) => part.value.cooked + (parts[i] ?? "")).join("");
|
|
37
|
+
}
|
|
38
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
39
|
+
const left = value(node.left), right = value(node.right);
|
|
40
|
+
if (![left, right].every(part => typeof part === "string" || typeof part === "number")) return undefined;
|
|
41
|
+
return left + right;
|
|
42
|
+
}
|
|
43
|
+
if (node.type === "AwaitExpression") return value(node.argument);
|
|
44
|
+
if (node.type === "MemberExpression" && !node.computed && node.property.name === "output") return value(node.object)?.output;
|
|
45
|
+
if (isCall(node, "load") && node.arguments.length === 1) return stored.get(value(node.arguments[0]));
|
|
46
|
+
if (isCall(node, "generatedImage") || isCall(node, "text")) {
|
|
47
|
+
node.arguments.forEach(value);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (isCall(node, "store") && node.arguments.length === 2) {
|
|
51
|
+
const key = value(node.arguments[0]), result = value(node.arguments[1]);
|
|
52
|
+
if (typeof key === "string") {
|
|
53
|
+
if (typeof result === "string") stored.set(key, result);
|
|
54
|
+
else stored.delete(key);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (isTool(node, "exec_command")) return echoedOutput && typeof recordedOutput === "string" ? { output: recordedOutput } : undefined;
|
|
59
|
+
if (isTool(node, "image_gen__imagegen")) {
|
|
60
|
+
imageCalls += 1;
|
|
61
|
+
const options = node.arguments[0];
|
|
62
|
+
if (options?.type === "ObjectExpression") {
|
|
63
|
+
const field = options.properties.find(p => p.type === "Property" && !p.computed && (p.key.name ?? p.key.value) === "prompt");
|
|
64
|
+
const resolved = value(field?.value);
|
|
65
|
+
if (typeof resolved === "string") prompt = resolved;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && !node.callee.computed) {
|
|
70
|
+
const receiver = value(node.callee.object), args = node.arguments.map(value);
|
|
71
|
+
if (typeof receiver !== "string" || args.some(arg => arg === undefined)) return undefined;
|
|
72
|
+
const method = node.callee.property.name;
|
|
73
|
+
if (method === "indexOf" && typeof args[0] === "string" && args.length === 1) return receiver.indexOf(args[0]);
|
|
74
|
+
if (["slice", "substring"].includes(method) && args.length <= 2 && args.every(Number.isInteger)) return receiver[method](...args);
|
|
75
|
+
if (method === "trim" && args.length === 0) return receiver.trim();
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
for (const statement of statements) {
|
|
80
|
+
if (statement.type === "VariableDeclaration") {
|
|
81
|
+
for (const declaration of statement.declarations) {
|
|
82
|
+
if (declaration.id.type === "Identifier") locals.set(declaration.id.name, value(declaration.init));
|
|
83
|
+
}
|
|
84
|
+
} else if (statement.type === "ExpressionStatement") value(statement.expression);
|
|
85
|
+
else if (statement.type !== "EmptyStatement") {
|
|
86
|
+
// 分支和循环的实际执行路径不可确定,不能沿用旧存储值。
|
|
87
|
+
stored.clear();
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return imageCalls === 1 ? prompt : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function recordedCommandOutput(output) {
|
|
95
|
+
if (!Array.isArray(output)) return undefined;
|
|
96
|
+
const texts = output.filter(part => part.type === "input_text" && typeof part.text === "string")
|
|
97
|
+
.map(part => part.text).filter(text => !/^Script (?:completed|running)\b/.test(text));
|
|
98
|
+
if (texts.length !== 1 || /(?:output truncated|tokens truncated|Warning: truncated)/i.test(texts[0])) return undefined;
|
|
99
|
+
return texts[0];
|
|
100
|
+
}
|