minecodex 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -14
  4. package/features/images/src/http-server.mjs +15 -5
  5. package/features/images/src/image-library.mjs +37 -9
  6. package/features/images/src/library-prompt-page.mjs +239 -0
  7. package/features/images/src/prompt-index.mjs +27 -24
  8. package/features/images/src/prompt-source.mjs +100 -0
  9. package/features/images/web/app.js +180 -67
  10. package/features/images/web/i18n.mjs +5 -0
  11. package/features/images/web/index.html +29 -24
  12. package/features/images/web/styles.css +169 -139
  13. package/features/model-slider/README.md +13 -1
  14. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  15. package/features/notes/DESIGN.md +66 -0
  16. package/features/notes/README.md +18 -99
  17. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  18. package/features/notes/assets/icons/book-open.svg +16 -0
  19. package/features/notes/assets/native-emojis.json +1 -0
  20. package/features/notes/assets/native-prompt-icons.json +28 -0
  21. package/features/notes/codex-feature.json +12 -70
  22. package/features/notes/src/main.mjs +2 -2
  23. package/features/notes/src/product.mjs +287 -613
  24. package/features/notes/src/prompt-schema.mjs +117 -0
  25. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  26. package/features/notes/src/quick-prompts-server.mjs +167 -0
  27. package/features/session-status/DESIGN.md +10 -2
  28. package/features/session-status/README.md +13 -3
  29. package/features/session-status/src/session-status.mjs +329 -24
  30. package/features/sidebar-focus/DESIGN.md +16 -0
  31. package/features/sidebar-focus/codex-feature.json +20 -0
  32. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  33. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  34. package/features/unread-inbox/DESIGN.md +28 -0
  35. package/features/unread-inbox/codex-feature.json +20 -0
  36. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  37. package/features/usage-meter/DESIGN.md +49 -0
  38. package/features/usage-meter/README.md +18 -0
  39. package/features/usage-meter/codex-feature.json +13 -0
  40. package/features/usage-meter/src/usage-meter.mjs +200 -0
  41. package/package.json +21 -6
  42. package/packages/cli/src/config.mjs +4 -1
  43. package/packages/cli/src/control-server.mjs +2 -2
  44. package/packages/runtime-host/src/cloud-images.mjs +123 -0
  45. package/packages/runtime-host/src/codex-cdp.mjs +42 -8
  46. package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
  47. package/packages/runtime-host/src/codex-injection.mjs +364 -116
  48. package/packages/runtime-host/src/codex-runtime.mjs +105 -28
  49. package/packages/runtime-host/src/feature-registry.mjs +3 -0
  50. package/packages/runtime-host/src/session-status-reader.mjs +65 -24
  51. package/features/notes/assets/icons/circle.svg +0 -15
  52. package/features/notes/assets/icons/ellipsis.svg +0 -17
  53. package/features/notes/assets/icons/external-link.svg +0 -17
  54. package/features/notes/assets/icons/list-todo.svg +0 -19
  55. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  56. package/features/notes/assets/icons/paperclip.svg +0 -15
  57. package/features/notes/assets/icons/star-off.svg +0 -17
  58. package/features/notes/assets/icons/star.svg +0 -15
  59. package/features/notes/assets/icons/sticky-note.svg +0 -16
  60. package/features/notes/assets/icons/trash-2.svg +0 -19
  61. package/features/notes/src/http-server.mjs +0 -580
  62. package/features/notes/web/app.js +0 -1202
  63. package/features/notes/web/codex-artifact-document.svg +0 -1
  64. package/features/notes/web/codex-attachment.svg +0 -1
  65. package/features/notes/web/codex-build.svg +0 -1
  66. package/features/notes/web/codex-code.svg +0 -1
  67. package/features/notes/web/codex-cplusplus.svg +0 -1
  68. package/features/notes/web/codex-css.svg +0 -1
  69. package/features/notes/web/codex-document.svg +0 -1
  70. package/features/notes/web/codex-file.svg +0 -1
  71. package/features/notes/web/codex-folder.svg +0 -1
  72. package/features/notes/web/codex-hashes.svg +0 -1
  73. package/features/notes/web/codex-html.svg +0 -1
  74. package/features/notes/web/codex-image.svg +0 -1
  75. package/features/notes/web/codex-java.svg +0 -1
  76. package/features/notes/web/codex-javascript.svg +0 -1
  77. package/features/notes/web/codex-json.svg +0 -1
  78. package/features/notes/web/codex-notebook.svg +0 -1
  79. package/features/notes/web/codex-pdf.svg +0 -1
  80. package/features/notes/web/codex-pencil.svg +0 -3
  81. package/features/notes/web/codex-php.svg +0 -1
  82. package/features/notes/web/codex-plus.svg +0 -3
  83. package/features/notes/web/codex-presentation.svg +0 -1
  84. package/features/notes/web/codex-python.svg +0 -1
  85. package/features/notes/web/codex-react.svg +0 -1
  86. package/features/notes/web/codex-rust.svg +0 -1
  87. package/features/notes/web/codex-shell.svg +0 -1
  88. package/features/notes/web/codex-skill.svg +0 -1
  89. package/features/notes/web/codex-spreadsheet.svg +0 -1
  90. package/features/notes/web/codex-task.svg +0 -3
  91. package/features/notes/web/codex-terminal.svg +0 -1
  92. package/features/notes/web/codex-toml.svg +0 -1
  93. package/features/notes/web/codex-typescript.svg +0 -1
  94. package/features/notes/web/codex-view-all.svg +0 -3
  95. package/features/notes/web/file-icons.mjs +0 -68
  96. package/features/notes/web/i18n.mjs +0 -237
  97. package/features/notes/web/index.html +0 -15
  98. package/features/notes/web/styles.css +0 -1155
  99. package/features/notes/web/todo-drag.mjs +0 -18
@@ -0,0 +1,200 @@
1
+ // 订阅原生额度缓存;不另行轮询,也不复制账号信息到插件服务。
2
+ const MARKER = "data-minecodex-usage-meter";
3
+ const TIP_ID = "minecodex-usage-tooltip";
4
+ const STYLE_ID = "minecodex-usage-style";
5
+ let record = null;
6
+ let client = null;
7
+ let unsubscribe = null;
8
+ let queued = false;
9
+ let hoverTimer = null;
10
+
11
+ function remainingWindows(data) {
12
+ const windows = [data?.rate_limit?.primary_window, data?.rate_limit?.secondary_window];
13
+ const remaining = seconds => {
14
+ const window = windows.find(window => window?.limit_window_seconds === seconds);
15
+ return typeof window?.used_percent === "number" && Number.isFinite(window.used_percent)
16
+ ? Math.round(Math.max(0, Math.min(100, 100 - window.used_percent))) : null;
17
+ };
18
+ const weekly = remaining(604800), fiveHour = remaining(18000);
19
+ return weekly == null ? null : { weekly, fiveHour, primary: fiveHour ?? weekly };
20
+ }
21
+
22
+ function nativeContext(rail) {
23
+ const key = Object.keys(rail).find(key => key.startsWith("__reactFiber$"));
24
+ let fiber = key ? rail[key] : null;
25
+ const result = { auth: null, client: null, router: null };
26
+ for (let depth = 0; fiber && depth < 200; depth++, fiber = fiber.return) {
27
+ const props = fiber.memoizedProps;
28
+ if (!props) continue;
29
+ for (const value of [props.auth, props.value]) {
30
+ if (!result.auth && value && typeof value === "object" && "authMethod" in value && "userId" in value && "accountId" in value) result.auth = value;
31
+ }
32
+ for (const value of [props.queryClient, props.client, props.value]) {
33
+ if (!result.client && typeof value?.getQueryCache === "function" && typeof value?.getQueryData === "function") result.client = value;
34
+ }
35
+ const router = props.router ?? props.value?.router;
36
+ if (!result.router && typeof router?.navigate === "function") result.router = router;
37
+ if (result.auth && result.client && result.router) break;
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function currentWindows(context) {
43
+ const { auth, client } = context;
44
+ if (auth?.isLoading || !auth?.userId || !auth?.accountId || !["chatgpt", "chatgptAuthTokens"].includes(auth.authMethod)) return null;
45
+ const data = client?.getQueryData(["rate-limit-status", auth.userId, auth.accountId]);
46
+ // 账号与缓存必须同时一致;旧账号的最后一次响应不能留在导航栏。
47
+ if (!data || data.user_id !== auth.userId || data.account_id !== auth.accountId) return null;
48
+ return remainingWindows(data);
49
+ }
50
+
51
+ const style = document.createElement("style");
52
+ style.id = STYLE_ID;
53
+ style.textContent = `
54
+ [${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; -webkit-app-region:no-drag; }
55
+ [${MARKER}] .mcx-usage-ring { display:block; width:24px; height:24px; overflow:visible; }
56
+ [${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:inherit; font-family:var(--font-sans-default); font-size:calc(var(--text-xs) * 2 / 3); line-height:1; font-weight:500; letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
57
+ [${MARKER}] [data-window="weekly"] { color:inherit; }
58
+ [${MARKER}] [data-window="fiveHour"] { color:var(--color-text-secondary); }
59
+ #${TIP_ID} { position:fixed; z-index:2147483000; width:max-content; max-width:calc(100vw - 16px); pointer-events:none; user-select:none; padding:5px 12px; border:1px solid var(--color-border-tooltip); border-radius:var(--radius-2xl); background:var(--color-background-tooltip); color:var(--color-text-tooltip); box-shadow:var(--shadow-tooltip); font-family:var(--font-sans-default); font-size:var(--text-sm); line-height:18px; font-weight:var(--tooltip-compact-font-weight); letter-spacing:var(--tracking-tooltip); }
60
+ #${TIP_ID}[hidden] { display:none; }
61
+ #${TIP_ID} dl { display:grid; grid-template-columns:auto auto; gap:2px 16px; margin:0; }
62
+ #${TIP_ID} dt { text-align:left; }
63
+ #${TIP_ID} dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; }
64
+ `;
65
+ document.head.append(style);
66
+
67
+ function svgNode(name, attributes) {
68
+ const node = document.createElementNS("http://www.w3.org/2000/svg", name);
69
+ for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));
70
+ return node;
71
+ }
72
+
73
+ function draw(windows) {
74
+ const svg = svgNode("svg", { viewBox: "0 0 32 32", class: "mcx-usage-ring", "aria-hidden": "true" });
75
+ for (const [key, radius] of [["weekly", 43 / 3], ["fiveHour", 10.5]]) {
76
+ if (windows[key] == null) continue;
77
+ const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3 });
78
+ group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, opacity: 0.18 }));
79
+ if (windows[key] > 0) group.append(svgNode("circle", {
80
+ cx: 16, cy: 16, r: radius, pathLength: 100,
81
+ "stroke-dasharray": `${windows[key]} 100`, "stroke-linecap": "round", transform: "rotate(-90 16 16)",
82
+ }));
83
+ svg.append(group);
84
+ }
85
+ const number = document.createElement("span");
86
+ number.className = "mcx-usage-number";
87
+ number.textContent = String(windows.primary);
88
+ number.setAttribute("data-dual", String(windows.fiveHour != null));
89
+ number.setAttribute("data-digits", String(number.textContent.length));
90
+ record.button.replaceChildren(svg, number);
91
+ const list = document.createElement("dl");
92
+ const rows = windows.fiveHour == null ? [] : [["5h Limit", windows.fiveHour]];
93
+ rows.push(["Weekly Limit", windows.weekly]);
94
+ for (const [label, percent] of rows) {
95
+ const term = document.createElement("dt"), value = document.createElement("dd");
96
+ term.textContent = label;
97
+ value.textContent = `${percent}%`;
98
+ list.append(term, value);
99
+ }
100
+ record.tooltip.replaceChildren(list);
101
+ record.button.setAttribute("aria-label", rows.map(([label, percent]) => `${label} ${percent}%`).join(", "));
102
+ record.button.setAttribute("aria-description", "Open Usage settings");
103
+ if (!record.tooltip.hidden) positionTooltip();
104
+ }
105
+
106
+ function positionTooltip() {
107
+ if (!record) return;
108
+ const rect = record.button.getBoundingClientRect();
109
+ const tooltip = record.tooltip;
110
+ // 按物理视口定位,兼容原生 app-shell 的 CSS zoom。
111
+ tooltip.style.left = `${Math.max(8, Math.min(rect.right + 2, window.innerWidth - tooltip.offsetWidth - 8))}px`;
112
+ tooltip.style.top = `${Math.max(8, Math.min(rect.top + (rect.height - tooltip.offsetHeight) / 2, window.innerHeight - tooltip.offsetHeight - 8))}px`;
113
+ }
114
+
115
+ function showTooltip() {
116
+ clearTimeout(hoverTimer);
117
+ if (!record) return;
118
+ record.tooltip.hidden = false;
119
+ record.button.setAttribute("aria-describedby", TIP_ID);
120
+ positionTooltip();
121
+ }
122
+ function hideTooltip() {
123
+ clearTimeout(hoverTimer);
124
+ if (!record) return;
125
+ record.tooltip.hidden = true;
126
+ record.button.removeAttribute("aria-describedby");
127
+ }
128
+ function removeControl() {
129
+ hideTooltip();
130
+ record?.button.remove();
131
+ record?.tooltip.remove();
132
+ record = null;
133
+ }
134
+
135
+ function mount(help, rail) {
136
+ // 只克隆按钮的外观属性,不携带原生 Help 的菜单行为或 id。
137
+ const button = help.cloneNode(false);
138
+ for (const attribute of [...button.attributes]) {
139
+ if (attribute.name.startsWith("aria-") || attribute.name === "id" || attribute.name === "data-state") button.removeAttribute(attribute.name);
140
+ }
141
+ button.setAttribute(MARKER, "");
142
+ const tooltip = document.createElement("div");
143
+ tooltip.id = TIP_ID;
144
+ tooltip.setAttribute("role", "tooltip");
145
+ tooltip.hidden = true;
146
+ const anchor = help.parentElement.classList.contains("contents") ? help.parentElement : help;
147
+ anchor.after(button);
148
+ document.body.append(tooltip);
149
+ record = { button, tooltip, rail, help, signature: null };
150
+ button.addEventListener("pointerenter", () => { hoverTimer = setTimeout(showTooltip, 350); });
151
+ button.addEventListener("pointerleave", hideTooltip);
152
+ button.addEventListener("focus", showTooltip);
153
+ button.addEventListener("blur", hideTooltip);
154
+ button.addEventListener("keydown", event => { if (event.key === "Escape") { event.stopPropagation(); hideTooltip(); } });
155
+ button.addEventListener("click", () => {
156
+ hideTooltip();
157
+ const { router } = nativeContext(rail);
158
+ router?.navigate("/settings/usage?tab=overview", { state: router.state?.location?.state });
159
+ });
160
+ }
161
+
162
+ function sync() {
163
+ queued = false;
164
+ if (signal.aborted) return;
165
+ const rail = [...document.querySelectorAll('nav[data-app-navigation-rail="true"]')].find(rail => rail.getBoundingClientRect().width > 0);
166
+ const help = rail?.querySelector('button[aria-label="Help menu"],button[aria-label="帮助菜单"]');
167
+ const context = rail ? nativeContext(rail) : {};
168
+ if (client !== context.client) {
169
+ unsubscribe?.();
170
+ client = context.client;
171
+ unsubscribe = client?.getQueryCache().subscribe(event => {
172
+ if (event.query?.queryKey?.[0] === "rate-limit-status") schedule();
173
+ });
174
+ }
175
+ const windows = currentWindows(context);
176
+ if (!help || !context.router || !windows) { removeControl(); return; }
177
+ if (record?.help !== help || !record?.button.isConnected) { removeControl(); mount(help, rail); }
178
+ const signature = JSON.stringify(windows);
179
+ if (record.signature !== signature) { record.signature = signature; draw(windows); }
180
+ }
181
+ function schedule() {
182
+ if (queued || signal.aborted) return;
183
+ queued = true;
184
+ queueMicrotask(sync);
185
+ }
186
+ const observer = new MutationObserver(schedule);
187
+ observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-label"] });
188
+ window.addEventListener("resize", positionTooltip);
189
+ window.addEventListener("focus", schedule);
190
+ const cleanup = () => {
191
+ observer.disconnect();
192
+ unsubscribe?.();
193
+ window.removeEventListener("resize", positionTooltip);
194
+ window.removeEventListener("focus", schedule);
195
+ removeControl();
196
+ style.remove();
197
+ };
198
+ signal.addEventListener("abort", cleanup, { once: true });
199
+ sync();
200
+ return cleanup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "1.2.1",
3
+ "version": "2.0.0",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,9 +23,10 @@
23
23
  "features/model-slider/THIRD_PARTY_NOTICES.md",
24
24
  "features/model-slider/vendor",
25
25
  "features/notes/src",
26
- "features/notes/web",
27
26
  "features/notes/assets",
28
27
  "features/notes/codex-feature.json",
28
+ "features/notes/DESIGN.md",
29
+ "features/notes/README.md",
29
30
  "features/images/src",
30
31
  "features/images/web",
31
32
  "features/images/codex-feature.json",
@@ -41,17 +42,28 @@
41
42
  "apps/dashboard/favicon.svg",
42
43
  "apps/dashboard/index.html",
43
44
  "apps/dashboard/styles.css",
44
- "README.md"
45
+ "README.md",
46
+ "features/usage-meter/src",
47
+ "features/usage-meter/codex-feature.json",
48
+ "features/usage-meter/DESIGN.md",
49
+ "features/usage-meter/README.md",
50
+ "features/unread-inbox/src",
51
+ "features/unread-inbox/codex-feature.json",
52
+ "features/unread-inbox/DESIGN.md",
53
+ "features/sidebar-focus/src",
54
+ "features/sidebar-focus/codex-feature.json",
55
+ "features/sidebar-focus/DESIGN.md",
56
+ "features/sidebar-focus/vendor/lucide-LICENSE"
45
57
  ],
46
58
  "engines": {
47
59
  "node": ">=22.5.0"
48
60
  },
49
61
  "scripts": {
50
62
  "start": "node packages/cli/bin/mcx.mjs serve",
51
- "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs apps/dashboard/test/*.test.mjs",
63
+ "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/sidebar-focus/test/*.test.mjs",
52
64
  "test:cli": "node --test packages/cli/test/*.test.mjs",
53
65
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
54
- "check": "node --check packages/cli/bin/mcx.mjs && node --check packages/cli/src/*.mjs && node --check packages/runtime-host/src/*.mjs",
66
+ "check": "node scripts/check-syntax.mjs",
55
67
  "build:service": "scripts/build-service-app.zsh",
56
68
  "pack:check": "npm pack --dry-run",
57
69
  "pack:audit": "node scripts/audit-package.mjs",
@@ -75,5 +87,8 @@
75
87
  "os": [
76
88
  "darwin",
77
89
  "win32"
78
- ]
90
+ ],
91
+ "dependencies": {
92
+ "acorn": "^8.18.0"
93
+ }
79
94
  }
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status"]);
5
+ export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "unread-inbox", "sidebar-focus"]);
6
6
  export const DEFAULT_CONFIG = Object.freeze({
7
7
  schemaVersion: 1,
8
8
  features: Object.freeze({
@@ -11,6 +11,9 @@ export const DEFAULT_CONFIG = Object.freeze({
11
11
  images: true,
12
12
  "hide-upsell-banner": true,
13
13
  "session-status": true,
14
+ "usage-meter": true,
15
+ "unread-inbox": true,
16
+ "sidebar-focus": true,
14
17
  }),
15
18
  });
16
19
 
@@ -196,8 +196,8 @@ async function iconResponse(id, paths, response) {
196
196
  if (id === "model-slider") {
197
197
  iconPath = path.join(paths.featuresRoot, id, "assets", "ui", "sparkles.svg");
198
198
  } else if (id === "notes") {
199
- iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "sticky-note.svg");
200
- } else if (["images", "hide-upsell-banner", "session-status"].includes(id)) {
199
+ iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "book-open.svg");
200
+ } else if (["images", "hide-upsell-banner", "session-status", "usage-meter"].includes(id)) {
201
201
  const manifest = JSON.parse(await readFile(path.join(paths.featuresRoot, id, "codex-feature.json"), "utf8"));
202
202
  const viewBox = manifest.icon?.viewBox ?? "0 0 24 24";
203
203
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
@@ -0,0 +1,123 @@
1
+ import { evaluationValue } from "./codex-cdp.mjs";
2
+
3
+ // 在原生 Renderer 内使用现有登录态;凭据和临时下载地址不跨出 Renderer。
4
+ export async function cloudImagesInRenderer(operation, payload) {
5
+ const key = Symbol.for("minecodex.cloud-images.v1");
6
+ const state = window[key] ??= { files: new Map(), conversations: new Map() };
7
+ if (!state.capability) {
8
+ state.capability = (async () => {
9
+ const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
10
+ .map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
11
+ if (!initialUrl) throw new Error("Cloud Library is unavailable in this Codex version");
12
+ const initialSource = await (await fetch(initialUrl)).text();
13
+ const asset = name => {
14
+ const file = initialSource.match(new RegExp(`${name}-[\\w-]+\\.js`))?.[0];
15
+ if (!file) throw new Error(`Cloud Library capability ${name} is unavailable`);
16
+ return new URL(file, initialUrl).href;
17
+ };
18
+ const metadataUrl = asset("library-file-metadata");
19
+ const metadataSource = await (await fetch(metadataUrl)).text();
20
+ const apiLocal = metadataSource.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
21
+ const importBlock = [...metadataSource.matchAll(/import\{([^}]+)\}from["']([^"']*app-initial[^"']+)["']/g)][0];
22
+ const apiExport = importBlock?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)).find(parts => parts.at(-1) === apiLocal)?.[0];
23
+ if (!apiExport) throw new Error("Cloud Library API contract changed");
24
+ const metadataInit = metadataSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
25
+ const metadataExports = metadataSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
26
+ const initExport = metadataExports.find(parts => parts[0] === metadataInit)?.at(-1);
27
+ const metadata = await import(metadataUrl);
28
+ if (typeof metadata[initExport] !== "function") throw new Error("Cloud Library initialization contract changed");
29
+ metadata[initExport]();
30
+ const module = await import(initialUrl);
31
+ const api = module[apiExport];
32
+ if (!api?.safeGet || !api?.safePost) throw new Error("Cloud Library API is not ready");
33
+ const downloadUrl = asset("library-file-download");
34
+ const downloadSource = await (await fetch(downloadUrl)).text();
35
+ const exports = downloadSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
36
+ const responseLocal = downloadSource.match(/async function ([\w$]+)\([^)]*\)\{return [\w$]+\(await [\w$]+\([^;]+showInline:!0/)?.[1];
37
+ const initLocal = downloadSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
38
+ const exported = local => exports.find(parts => parts[0] === local)?.at(-1);
39
+ const download = await import(downloadUrl);
40
+ const initialize = download[exported(initLocal)], response = download[exported(responseLocal)];
41
+ if (typeof initialize !== "function" || typeof response !== "function") throw new Error("Cloud image download contract changed");
42
+ initialize();
43
+ return { api, response };
44
+ })().catch(error => { state.capability = null; throw error; });
45
+ }
46
+ const { api, response } = await state.capability;
47
+ if (operation === "list") {
48
+ if (!payload.cursor) { state.files.clear(); state.conversations.clear(); }
49
+ const result = await api.safePost("/files/library", {
50
+ requestBody: { limit: 50, cursor: payload.cursor ?? null, include_hidden_files: false, include_saved_entities: true },
51
+ signal: AbortSignal.timeout(25_000),
52
+ });
53
+ const files = result.items.filter(file => ["image/png", "image/jpeg", "image/webp"].includes(file.mime_type)
54
+ && file.origination_thread_id && file.origination_message_id && !file.trashed_at);
55
+ for (const file of files) state.files.set(file.id, file);
56
+ return { items: files.map(file => ({ libraryId: file.id, version: `${file.current_version_number}:${file.updated_at}`, name: file.file_name })), cursor: result.cursor ?? null };
57
+ }
58
+ const file = state.files.get(payload.libraryId);
59
+ if (!file) throw new Error("Cloud image must come from the current Library listing");
60
+ const threadId = file.origination_thread_id;
61
+ if (!state.conversations.has(threadId)) {
62
+ const request = api.safeGet("/conversation/{conversation_id}", {
63
+ parameters: { path: { conversation_id: threadId } }, signal: AbortSignal.timeout(25_000),
64
+ }).catch(error => { state.conversations.delete(threadId); throw error; });
65
+ state.conversations.set(threadId, request);
66
+ }
67
+ const conversation = await state.conversations.get(threadId);
68
+ const node = conversation.mapping?.[file.origination_message_id];
69
+ const parts = node?.message?.content?.parts ?? [];
70
+ const part = parts.find(part => typeof part === "object" && part.content_type === "image_asset_pointer"
71
+ && (part.asset_pointer === `sediment://${file.file_id}` || part.asset_pointer === file.file_id));
72
+ // 明确验证生成结果,避免把用户上传的参考图混入生成图库。
73
+ if (!part || !["assistant", "tool"].includes(node.message.author?.role)
74
+ || !(part.metadata?.generation?.gen_id || part.metadata?.dalle?.gen_id)) return { skipped: true };
75
+ let prompt = part.metadata?.dalle?.prompt || null;
76
+ let promptKind = prompt ? "generation" : null;
77
+ if (!prompt) {
78
+ let ancestor = conversation.mapping[node.parent];
79
+ const visited = new Set();
80
+ while (ancestor && !visited.has(ancestor.id)) {
81
+ visited.add(ancestor.id);
82
+ if (ancestor.message?.author?.role === "user") {
83
+ prompt = ancestor.message.content?.parts?.filter(p => typeof p === "string").join("\n") || null;
84
+ promptKind = prompt ? "request" : null;
85
+ break;
86
+ }
87
+ ancestor = conversation.mapping[ancestor.parent];
88
+ }
89
+ }
90
+ const max = 32 * 1024 * 1024;
91
+ if (file.file_size_bytes > max) throw new Error("Cloud image exceeds 32 MB");
92
+ const imageBlob = await response(file, AbortSignal.timeout(60_000));
93
+ if (!(imageBlob instanceof Blob) || imageBlob.size > max) throw new Error("Cloud image download returned an invalid image");
94
+ const reader = imageBlob.stream().getReader(), chunks = [];
95
+ let length = 0;
96
+ try {
97
+ while (true) {
98
+ const {done, value} = await reader.read(); if (done) break;
99
+ length += value.length;
100
+ if (length > max) throw new Error("Cloud image exceeds 32 MB");
101
+ chunks.push(value);
102
+ }
103
+ } finally { await reader.cancel().catch(() => {}); }
104
+ let binary = "";
105
+ for (const chunk of chunks) for (let i = 0; i < chunk.length; i += 0x8000) binary += String.fromCharCode(...chunk.subarray(i, i + 0x8000));
106
+ return {
107
+ bytesBase64: btoa(binary), mimeType: imageBlob.type.split(";")[0] || file.mime_type,
108
+ itemId: file.file_id, conversationId: threadId, conversationTitle: conversation.title,
109
+ sourceKind: "chat", prompt, promptKind,
110
+ cloudLibraryId: file.id, cloudVersion: `${file.current_version_number}:${file.updated_at}`,
111
+ createdAt: new Date((node.message.create_time || conversation.create_time) * 1000).toISOString(),
112
+ };
113
+ }
114
+
115
+ export async function cloudImagesAction(client, operation, payload = {}) {
116
+ if (!["list", "import"].includes(operation)) throw new Error("Unknown cloud image operation");
117
+ if (payload.cursor != null && (typeof payload.cursor !== "string" || payload.cursor.length > 4096)) throw new Error("Invalid Library cursor");
118
+ if (operation === "import" && (typeof payload.libraryId !== "string" || !/^libfile_[\w-]+$/.test(payload.libraryId))) throw new Error("Invalid Library file ID");
119
+ return evaluationValue(await client.send("Runtime.evaluate", {
120
+ expression: `(${cloudImagesInRenderer.toString()})(${JSON.stringify(operation)},${JSON.stringify(payload)})`,
121
+ awaitPromise: true, returnByValue: true,
122
+ }, { timeoutMs: 110_000 }));
123
+ }
@@ -1,6 +1,6 @@
1
1
  // CDP (Chrome DevTools Protocol) 客户端:WebSocket 连接、表达式求值与进程等待。
2
2
  // 与注入内容无关;CodexRuntime 通过这里的 connectClient 与 Codex Renderer 通信。
3
- export async function connect(url) {
3
+ export async function connect(url, { timeoutMs = 10_000 } = {}) {
4
4
  const socket = new WebSocket(url);
5
5
  const pending = new Map();
6
6
  const eventWaiters = new Map();
@@ -8,8 +8,24 @@ export async function connect(url) {
8
8
  let nextId = 1;
9
9
 
10
10
  await new Promise((resolve, reject) => {
11
- socket.addEventListener("open", resolve, { once: true });
12
- socket.addEventListener("error", reject, { once: true });
11
+ const finish = (error) => {
12
+ clearTimeout(timer);
13
+ socket.removeEventListener("open", onOpen);
14
+ socket.removeEventListener("error", onError);
15
+ socket.removeEventListener("close", onClose);
16
+ if (error) reject(error);
17
+ else resolve();
18
+ };
19
+ const onOpen = () => finish();
20
+ const onError = () => finish(new Error("CDP WebSocket connection failed"));
21
+ const onClose = () => finish(new Error("CDP WebSocket closed before opening"));
22
+ const timer = setTimeout(() => {
23
+ finish(new Error("CDP WebSocket connection timed out"));
24
+ socket.close();
25
+ }, timeoutMs);
26
+ socket.addEventListener("open", onOpen, { once: true });
27
+ socket.addEventListener("error", onError, { once: true });
28
+ socket.addEventListener("close", onClose, { once: true });
13
29
  });
14
30
  socket.addEventListener("message", (event) => {
15
31
  const message = JSON.parse(String(event.data));
@@ -39,11 +55,27 @@ export async function connect(url) {
39
55
 
40
56
  return {
41
57
  socket,
42
- send(method, params = {}) {
58
+ send(method, params = {}, { timeoutMs = 30_000 } = {}) {
59
+ // 关闭后的 send 不会再收到 close 事件,必须立即结束请求。
60
+ if (socket.readyState !== WebSocket.OPEN) {
61
+ return Promise.reject(new Error("CDP WebSocket is not open"));
62
+ }
43
63
  const id = nextId++;
44
64
  return new Promise((resolve, reject) => {
45
- pending.set(id, { resolve, reject });
46
- socket.send(JSON.stringify({ id, method, params }));
65
+ const finish = (callback, value) => {
66
+ clearTimeout(timer);
67
+ pending.delete(id);
68
+ callback(value);
69
+ };
70
+ const timer = setTimeout(() => {
71
+ finish(reject, new Error(`CDP request timed out: ${method}`));
72
+ }, timeoutMs);
73
+ pending.set(id, {
74
+ resolve: (value) => finish(resolve, value),
75
+ reject: (error) => finish(reject, error),
76
+ });
77
+ try { socket.send(JSON.stringify({ id, method, params })); }
78
+ catch (error) { finish(reject, error); }
47
79
  });
48
80
  },
49
81
  on(method, listener) {
@@ -60,7 +92,10 @@ export async function connect(url) {
60
92
  clearTimeout(timeout);
61
93
  resolve(value);
62
94
  },
63
- reject,
95
+ reject(error) {
96
+ clearTimeout(timeout);
97
+ reject(error);
98
+ },
64
99
  };
65
100
  const timeout = setTimeout(() => {
66
101
  const remaining = (eventWaiters.get(method) ?? []).filter((item) => item !== waiter);
@@ -150,4 +185,3 @@ export function waitForProcessChildExit(child, timeoutMs) {
150
185
  timer = setTimeout(() => finish(false), timeoutMs);
151
186
  });
152
187
  }
153
-
@@ -3,6 +3,16 @@
3
3
  // 后经 CDP 注入),因此 install 内通过 config.themeTokens 拿到本模块的名单;
4
4
  // 名单只在这里维护,install 不再持有自己的字面量副本。
5
5
 
6
+ // 同一语义的历史名称;app-color 前缀迁移由适配器统一处理。
7
+ export const CODEX_THEME_TOKEN_ALIASES = Object.freeze({
8
+ "--color-text-foreground": ["--color-token-foreground"],
9
+ "--color-text-foreground-secondary": ["--color-token-text-secondary"],
10
+ "--color-text-foreground-tertiary": ["--color-token-text-tertiary"],
11
+ "--color-token-input-foreground": ["--color-text-foreground", "--color-token-foreground"],
12
+ "--color-text-accent": ["--color-token-text-link-foreground"],
13
+ "--color-border-focus": ["--color-token-focus-border"],
14
+ });
15
+
6
16
  export const CODEX_THEME_TOKENS = Object.freeze([
7
17
  "--codex-base-accent",
8
18
  "--codex-base-ink",