minecodex 1.2.1 → 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.
@@ -16,12 +16,21 @@
16
16
  "pageScript": {
17
17
  "resource": "src/chatgpt-image-source-page.mjs"
18
18
  },
19
- "hostActions": ["import-generated-image", "create-image-edit-thread", "open-sidebar"],
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": ["node", "src/main.mjs"],
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 (!line.includes('"type":"custom_tool_call_output"') || !line.includes("generated_images")) return null;
51
- const callId = line.match(/"call_id":"([a-zA-Z0-9_-]+)"/)?.[1];
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
- return { callId, itemIds };
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("image_gen__imagegen")) {
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
- const wrapped = wrappedImagegenPrompt(payload);
73
- if (wrapped) wrappedPrompts.set(wrapped.callId, wrapped.prompt);
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
+ }