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
|
@@ -32,6 +32,9 @@ style.id = STYLE_ID;
|
|
|
32
32
|
style.textContent = `
|
|
33
33
|
[${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 12px; margin:-2px auto -6px; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--font-sans-default,inherit); font-size:var(--text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--color-text-tertiary); }
|
|
34
34
|
[${MARKER}][hidden] { display:none; }
|
|
35
|
+
[${MARKER}]:not([hidden]) { animation:mcx-session-status-enter 160ms ease-out both; }
|
|
36
|
+
@keyframes mcx-session-status-enter { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
|
|
37
|
+
@media (prefers-reduced-motion: reduce) { [${MARKER}]:not([hidden]) { animation:none; } }
|
|
35
38
|
[${MARKER}] button { box-sizing:border-box; display:inline-flex; align-items:center; gap:5px; height:22px; color:inherit; font:inherit; border:0; border-radius:var(--radius-xl,12px); background:transparent; padding:3px 7px; cursor:pointer; white-space:nowrap; }
|
|
36
39
|
[${MARKER}] svg, [data-minecodex-session-status-panel] h3 svg { width:14px; height:14px; flex:none; stroke-width:1.75; }
|
|
37
40
|
[${MARKER}] [data-kind="context"] svg, [data-minecodex-session-status-panel] h3 .context-ring { width:12px; height:12px; stroke-width:2; }
|
|
@@ -105,6 +108,11 @@ let snapshot = null;
|
|
|
105
108
|
let panel = null;
|
|
106
109
|
let openKind = null;
|
|
107
110
|
let rescanQueued = false;
|
|
111
|
+
let activeThreadId = null;
|
|
112
|
+
let refreshTimer = null;
|
|
113
|
+
let refreshSequence = 0;
|
|
114
|
+
let disposed = false;
|
|
115
|
+
let lastRevision = 0;
|
|
108
116
|
|
|
109
117
|
function number(value) { return value == null ? "—" : new Intl.NumberFormat().format(value); }
|
|
110
118
|
function compact(value) {
|
|
@@ -261,8 +269,59 @@ function findComposer() {
|
|
|
261
269
|
return document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
|
|
262
270
|
?? document.querySelector('[contenteditable="true"][role="textbox"]');
|
|
263
271
|
}
|
|
272
|
+
function currentThreadId(composer) {
|
|
273
|
+
if (!composer) return null;
|
|
274
|
+
const card = composer?.closest('[data-codex-composer-root]');
|
|
275
|
+
const portal = card?.querySelector('[data-above-composer-portal]');
|
|
276
|
+
let value;
|
|
277
|
+
if (portal) value = portal.getAttribute('data-above-composer-conversation-id');
|
|
278
|
+
else if (card?.getAttribute('data-composer-placement') === 'home') return null;
|
|
279
|
+
else value = document.querySelector('[data-app-action-sidebar-thread-selected="true"]')?.getAttribute('data-app-action-sidebar-thread-id')
|
|
280
|
+
?? window.location.pathname + window.location.search + window.location.hash;
|
|
281
|
+
return value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
|
|
282
|
+
}
|
|
283
|
+
function requestRefresh() {
|
|
284
|
+
if (!activeThreadId || signal.aborted) return;
|
|
285
|
+
if (typeof window[config.bindingName] === "function") {
|
|
286
|
+
window[config.bindingName](JSON.stringify({
|
|
287
|
+
token: config.bindingToken,
|
|
288
|
+
featureId: config.featureId,
|
|
289
|
+
requestId: `session-status:${++refreshSequence}`,
|
|
290
|
+
action: "refresh-session-status",
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function scheduleRefresh() {
|
|
295
|
+
clearTimeout(refreshTimer);
|
|
296
|
+
refreshTimer = null;
|
|
297
|
+
if (!activeThreadId || signal.aborted) return;
|
|
298
|
+
const waiting = snapshot?.turns == null || (snapshot.turns === 0 && snapshot.totalTokens == null);
|
|
299
|
+
const delay = waiting ? Math.max(500, snapshot?.retryAfterMs ?? 500) : snapshot?.isRunning ? 500 : 0;
|
|
300
|
+
if (!delay) return;
|
|
301
|
+
refreshTimer = setTimeout(() => {
|
|
302
|
+
refreshTimer = null;
|
|
303
|
+
requestRefresh();
|
|
304
|
+
scheduleRefresh();
|
|
305
|
+
}, delay);
|
|
306
|
+
}
|
|
307
|
+
function syncVisibility(composer) {
|
|
308
|
+
const threadId = currentThreadId(composer);
|
|
309
|
+
root.hidden = !threadId || snapshot?.threadId !== threadId;
|
|
310
|
+
if (root.hidden) closePanel();
|
|
311
|
+
}
|
|
264
312
|
function ensurePosition() {
|
|
313
|
+
if (disposed || signal.aborted) return;
|
|
265
314
|
const composer = findComposer();
|
|
315
|
+
const threadId = currentThreadId(composer);
|
|
316
|
+
if (threadId !== activeThreadId) {
|
|
317
|
+
activeThreadId = threadId;
|
|
318
|
+
// 会话建立后先展示占位,不让日志落盘速度决定状态栏是否出现。
|
|
319
|
+
snapshot = threadId ? { threadId } : null;
|
|
320
|
+
closePanel();
|
|
321
|
+
render();
|
|
322
|
+
requestRefresh();
|
|
323
|
+
scheduleRefresh();
|
|
324
|
+
}
|
|
266
325
|
if (!composer) { root.remove(); closePanel(); return; }
|
|
267
326
|
const form = composer.closest("form");
|
|
268
327
|
const card = composer.closest("[data-codex-composer-root]") ?? form ?? composer.parentElement?.parentElement;
|
|
@@ -271,10 +330,11 @@ function ensurePosition() {
|
|
|
271
330
|
card.after(root);
|
|
272
331
|
closePanel();
|
|
273
332
|
}
|
|
333
|
+
syncVisibility(composer);
|
|
274
334
|
}
|
|
275
335
|
function render() {
|
|
276
|
-
|
|
277
|
-
if (root.hidden)
|
|
336
|
+
syncVisibility(findComposer());
|
|
337
|
+
if (root.hidden) return;
|
|
278
338
|
buttons[0].querySelector(".status-label").textContent = `${number(snapshot.turns)} ${copy.turns} ${number(snapshot.steps)} ${copy.steps} · ${snapshot.observedTps == null ? "—" : number(snapshot.observedTps)} tok/s`;
|
|
279
339
|
buttons[1].querySelector(".status-label").textContent = `${compact(snapshot.totalTokens)} · ${copy.cache} ${snapshot.cacheHitPercent == null ? "—" : `${snapshot.cacheHitPercent}%`}`;
|
|
280
340
|
buttons[2].querySelector(".status-label").textContent = snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`;
|
|
@@ -298,7 +358,10 @@ const observer = new MutationObserver(() => {
|
|
|
298
358
|
rescanQueued = true;
|
|
299
359
|
queueMicrotask(() => { rescanQueued = false; ensurePosition(); });
|
|
300
360
|
});
|
|
301
|
-
observer.observe(document.documentElement, {
|
|
361
|
+
observer.observe(document.documentElement, {
|
|
362
|
+
childList: true, subtree: true, attributes: true,
|
|
363
|
+
attributeFilter: ["data-above-composer-conversation-id", "data-composer-placement", "data-app-action-sidebar-thread-selected"],
|
|
364
|
+
});
|
|
302
365
|
const localeObserver = new MutationObserver(() => {
|
|
303
366
|
const nextCopy = selectedCopy();
|
|
304
367
|
if (nextCopy === copy) return;
|
|
@@ -309,14 +372,23 @@ localeObserver.observe(document.documentElement, { attributes: true, attributeFi
|
|
|
309
372
|
ensurePosition();
|
|
310
373
|
const previous = window.__minecodexSessionStatus;
|
|
311
374
|
const receiver = {
|
|
312
|
-
update(value) {
|
|
313
|
-
snapshot = value;
|
|
375
|
+
update(value, revision) {
|
|
314
376
|
ensurePosition();
|
|
377
|
+
// 切换会话期间的迟到快照不得覆盖新会话,也不清除其占位。
|
|
378
|
+
if (!activeThreadId || value?.threadId !== activeThreadId) return;
|
|
379
|
+
if (Number.isSafeInteger(revision)) {
|
|
380
|
+
if (revision <= lastRevision) return;
|
|
381
|
+
lastRevision = revision;
|
|
382
|
+
}
|
|
383
|
+
snapshot = value;
|
|
315
384
|
render();
|
|
385
|
+
scheduleRefresh();
|
|
316
386
|
},
|
|
317
387
|
};
|
|
318
388
|
window.__minecodexSessionStatus = receiver;
|
|
319
389
|
return () => {
|
|
390
|
+
disposed = true;
|
|
391
|
+
clearTimeout(refreshTimer);
|
|
320
392
|
observer.disconnect();
|
|
321
393
|
localeObserver.disconnect();
|
|
322
394
|
closePanel();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -75,5 +75,8 @@
|
|
|
75
75
|
"os": [
|
|
76
76
|
"darwin",
|
|
77
77
|
"win32"
|
|
78
|
-
]
|
|
78
|
+
],
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"acorn": "^8.18.0"
|
|
81
|
+
}
|
|
79
82
|
}
|
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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",
|