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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cloudImagesAction } from "./cloud-images.mjs";
|
|
1
2
|
// CodexRuntime:Codex Renderer 生命周期与 host action 执行。
|
|
2
3
|
// 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
|
|
3
4
|
|
|
@@ -44,6 +45,19 @@ export {
|
|
|
44
45
|
responsiveSummaryDisplayMode,
|
|
45
46
|
responsiveSummaryVisibleSurface,
|
|
46
47
|
} from "./codex-injection.mjs";
|
|
48
|
+
|
|
49
|
+
export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
|
|
50
|
+
const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
|
|
51
|
+
?? document.querySelector('[contenteditable="true"][role="textbox"]');
|
|
52
|
+
const card = composer?.closest('[data-codex-composer-root]');
|
|
53
|
+
const portal = card?.querySelector('[data-above-composer-portal]');
|
|
54
|
+
if (portal) return portal.getAttribute('data-above-composer-conversation-id');
|
|
55
|
+
if (card?.getAttribute('data-composer-placement') === 'home') return null;
|
|
56
|
+
const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
|
|
57
|
+
return selected?.getAttribute('data-app-action-sidebar-thread-id')
|
|
58
|
+
?? location.pathname + location.search + location.hash;
|
|
59
|
+
})()`;
|
|
60
|
+
|
|
47
61
|
export class CodexRuntime {
|
|
48
62
|
constructor({
|
|
49
63
|
features,
|
|
@@ -90,6 +104,8 @@ export class CodexRuntime {
|
|
|
90
104
|
this.scriptIds = new Map();
|
|
91
105
|
this.bindingTokens = new Map();
|
|
92
106
|
this.hostActionChains = new Map();
|
|
107
|
+
this.sessionStatusUpdates = new Map();
|
|
108
|
+
this.sessionStatusRevision = 0;
|
|
93
109
|
this.refreshPromise = null;
|
|
94
110
|
this.stopping = false;
|
|
95
111
|
this.monitorPromise = null;
|
|
@@ -277,7 +293,9 @@ export class CodexRuntime {
|
|
|
277
293
|
}
|
|
278
294
|
|
|
279
295
|
async startTargetDiscovery() {
|
|
280
|
-
const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version
|
|
296
|
+
const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
|
|
297
|
+
signal: AbortSignal.timeout(3_000),
|
|
298
|
+
});
|
|
281
299
|
if (!response?.ok) {
|
|
282
300
|
throw new Error(`Codex CDP version returned HTTP ${response?.status ?? "error"}`);
|
|
283
301
|
}
|
|
@@ -342,7 +360,9 @@ export class CodexRuntime {
|
|
|
342
360
|
|
|
343
361
|
async isAvailable() {
|
|
344
362
|
try {
|
|
345
|
-
const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version
|
|
363
|
+
const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
|
|
364
|
+
signal: AbortSignal.timeout(3_000),
|
|
365
|
+
});
|
|
346
366
|
if (!response?.ok) return false;
|
|
347
367
|
requireCdpWebSocketUrl(await response.json(), this.cdpPort);
|
|
348
368
|
return true;
|
|
@@ -505,6 +525,12 @@ export class CodexRuntime {
|
|
|
505
525
|
}
|
|
506
526
|
if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
|
|
507
527
|
const feature = this.features.find((candidate) => candidate.id === request.featureId);
|
|
528
|
+
if (feature?.id === "session-status" && request.action === "refresh-session-status") {
|
|
529
|
+
if (this.sessionStatusReader && this.clients.get(targetId) === client && !this.stopping) {
|
|
530
|
+
await this.publishSessionStatus(targetId);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
508
534
|
if (feature && request.action === SURFACE_LOAD_ACTION) {
|
|
509
535
|
try {
|
|
510
536
|
const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
|
|
@@ -528,6 +554,18 @@ export class CodexRuntime {
|
|
|
528
554
|
await this.importGeneratedImage(feature, request.payload);
|
|
529
555
|
return;
|
|
530
556
|
}
|
|
557
|
+
if (["list-cloud-images", "import-cloud-image"].includes(request.action)) {
|
|
558
|
+
try {
|
|
559
|
+
let result = await cloudImagesAction(client, request.action === "list-cloud-images" ? "list" : "import", request.payload);
|
|
560
|
+
if (request.action === "import-cloud-image" && !result.skipped) {
|
|
561
|
+
result = await this.importGeneratedImage(feature, result);
|
|
562
|
+
}
|
|
563
|
+
await this.resolveHostAction(client, request.requestId, { ok: true, result });
|
|
564
|
+
} catch (error) {
|
|
565
|
+
await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "CLOUD_IMAGES_FAILED", message: error.message } });
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
531
569
|
try {
|
|
532
570
|
const result = request.action === "create-image-edit-thread"
|
|
533
571
|
? await this.createImageEditThread(client, feature, request.payload, request.requestId)
|
|
@@ -555,12 +593,15 @@ export class CodexRuntime {
|
|
|
555
593
|
if (!response?.ok) {
|
|
556
594
|
throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
|
|
557
595
|
}
|
|
596
|
+
return response.json();
|
|
558
597
|
}
|
|
559
598
|
|
|
560
599
|
async refreshTargets() {
|
|
561
600
|
let response;
|
|
562
601
|
try {
|
|
563
|
-
response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list
|
|
602
|
+
response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list`, {
|
|
603
|
+
signal: AbortSignal.timeout(3_000),
|
|
604
|
+
});
|
|
564
605
|
} catch (error) {
|
|
565
606
|
throw this.markCodexUnavailable(error);
|
|
566
607
|
}
|
|
@@ -591,7 +632,8 @@ export class CodexRuntime {
|
|
|
591
632
|
...this.targetFailures.keys(),
|
|
592
633
|
]);
|
|
593
634
|
for (const id of trackedTargetIds) {
|
|
594
|
-
|
|
635
|
+
const socketState = this.clients.get(id)?.socket.readyState;
|
|
636
|
+
if (liveRendererIds.has(id) && (socketState == null || socketState === 1)) continue;
|
|
595
637
|
removedTarget = true;
|
|
596
638
|
this.clients.get(id)?.socket.close();
|
|
597
639
|
this.clients.delete(id);
|
|
@@ -627,7 +669,13 @@ export class CodexRuntime {
|
|
|
627
669
|
const bindingToken = randomBytes(24).toString("hex");
|
|
628
670
|
this.bindingTokens.set(target.id, bindingToken);
|
|
629
671
|
client.on?.("Runtime.bindingCalled", (params) => {
|
|
630
|
-
|
|
672
|
+
let action;
|
|
673
|
+
try { action = JSON.parse(params.payload)?.action; } catch { return; }
|
|
674
|
+
// 统计读取独立合并,避免排在图片等用户操作后面。
|
|
675
|
+
const work = action === "refresh-session-status"
|
|
676
|
+
? this.handleBindingCalled(target.id, client, params)
|
|
677
|
+
: this.enqueueHostAction(target.id, () => this.handleBindingCalled(target.id, client, params));
|
|
678
|
+
work
|
|
631
679
|
.catch((error) => this.logger.warn?.("Codex Host action failed", error.message));
|
|
632
680
|
});
|
|
633
681
|
const source = createInjectionSource(this.features, {
|
|
@@ -680,26 +728,43 @@ export class CodexRuntime {
|
|
|
680
728
|
this.notifyStatusChange();
|
|
681
729
|
}
|
|
682
730
|
|
|
683
|
-
async publishSessionStatus() {
|
|
731
|
+
async publishSessionStatus(onlyTargetId = null) {
|
|
684
732
|
for (const [targetId, client] of this.clients) {
|
|
733
|
+
if (onlyTargetId && targetId !== onlyTargetId) continue;
|
|
685
734
|
if (!this.injectedTargets.has(targetId)) continue;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
735
|
+
const pending = this.sessionStatusUpdates.get(targetId);
|
|
736
|
+
if (pending) { await pending; continue; }
|
|
737
|
+
const revision = ++this.sessionStatusRevision;
|
|
738
|
+
const update = (async () => {
|
|
739
|
+
try {
|
|
740
|
+
const response = await client.send("Runtime.evaluate", {
|
|
741
|
+
expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
|
|
742
|
+
returnByValue: true,
|
|
743
|
+
}, { timeoutMs: 3_000 });
|
|
744
|
+
const threadId = threadIdFromLocation(response.result?.value);
|
|
745
|
+
const data = threadId ? await this.sessionStatusReader.read(threadId) : null;
|
|
746
|
+
const snapshot = data ?? (threadId ? {
|
|
747
|
+
threadId,
|
|
748
|
+
retryAfterMs: this.sessionStatusReader.retryAfterMs?.(threadId) ?? 500,
|
|
749
|
+
} : null);
|
|
750
|
+
if (this.stopping || this.clients.get(targetId) !== client) return;
|
|
751
|
+
await client.send("Runtime.evaluate", {
|
|
752
|
+
expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)}, ${revision})`,
|
|
753
|
+
returnByValue: true,
|
|
754
|
+
}, { timeoutMs: 3_000 });
|
|
755
|
+
} catch (error) {
|
|
756
|
+
this.logger.warn?.("Session Status update failed", error.message);
|
|
757
|
+
if (this.clients.get(targetId) === client) {
|
|
758
|
+
client.socket.close();
|
|
759
|
+
this.injectedTargets.delete(targetId);
|
|
760
|
+
this.notifyStatusChange();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
})();
|
|
764
|
+
this.sessionStatusUpdates.set(targetId, update);
|
|
765
|
+
try { await update; }
|
|
766
|
+
finally {
|
|
767
|
+
if (this.sessionStatusUpdates.get(targetId) === update) this.sessionStatusUpdates.delete(targetId);
|
|
703
768
|
}
|
|
704
769
|
}
|
|
705
770
|
}
|
|
@@ -62,6 +62,7 @@ function emptyState(filePath) {
|
|
|
62
62
|
offset: 0,
|
|
63
63
|
pending: "",
|
|
64
64
|
turns: new Set(),
|
|
65
|
+
runningTurns: new Set(),
|
|
65
66
|
responses: new Set(),
|
|
66
67
|
hasResponseUsage: false,
|
|
67
68
|
total: null,
|
|
@@ -134,7 +135,14 @@ export function applySessionStatusEvent(state, event) {
|
|
|
134
135
|
}
|
|
135
136
|
if (event?.type === "event_msg") {
|
|
136
137
|
if (payload.type === "context_compacted") state.compactionEvents += 1;
|
|
137
|
-
if (payload.type === "task_started" && payload.turn_id)
|
|
138
|
+
if (payload.type === "task_started" && payload.turn_id) {
|
|
139
|
+
state.turns.add(payload.turn_id);
|
|
140
|
+
state.runningTurns.add(payload.turn_id);
|
|
141
|
+
}
|
|
142
|
+
if (["task_complete", "task_interrupted", "turn_aborted"].includes(payload.type)) {
|
|
143
|
+
if (payload.turn_id) state.runningTurns.delete(payload.turn_id);
|
|
144
|
+
else state.runningTurns.clear();
|
|
145
|
+
}
|
|
138
146
|
if (payload.type === "task_complete" && Number.isFinite(payload.time_to_first_token_ms)) {
|
|
139
147
|
state.ttftTotalMs += payload.time_to_first_token_ms;
|
|
140
148
|
state.ttftSamples += 1;
|
|
@@ -200,6 +208,7 @@ export function sessionStatusSnapshot(state, threadId) {
|
|
|
200
208
|
const contextWindow = state.contextWindow > 0 ? state.contextWindow : null;
|
|
201
209
|
return {
|
|
202
210
|
threadId,
|
|
211
|
+
isRunning: state.runningTurns.size > 0,
|
|
203
212
|
turns: state.turns.size,
|
|
204
213
|
steps: state.responses.size,
|
|
205
214
|
totalTokens: total?.total_tokens ?? null,
|
|
@@ -248,24 +257,32 @@ async function findLog(directory, threadId) {
|
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
export class SessionStatusReader {
|
|
251
|
-
constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions") } = {}) {
|
|
260
|
+
constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions"), now = Date.now } = {}) {
|
|
252
261
|
this.sessionsDir = sessionsDir;
|
|
262
|
+
this.now = now;
|
|
253
263
|
this.states = new Map();
|
|
254
|
-
this.
|
|
264
|
+
this.missingChecks = new Map();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
retryAfterMs(threadId) {
|
|
268
|
+
return Math.max(0, (this.missingChecks.get(threadId)?.nextCheckAt ?? 0) - this.now());
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
async read(threadId) {
|
|
258
272
|
if (!THREAD_ID.test(threadId ?? "")) return null;
|
|
259
273
|
let state = this.states.get(threadId);
|
|
260
274
|
if (!state) {
|
|
261
|
-
|
|
262
|
-
if (Date.now() - lastCheck < 10_000) return null;
|
|
263
|
-
this.missingCheckedAt.set(threadId, Date.now());
|
|
275
|
+
if (this.retryAfterMs(threadId) > 0) return null;
|
|
264
276
|
const filePath = await findLog(this.sessionsDir, threadId);
|
|
265
|
-
if (!filePath)
|
|
277
|
+
if (!filePath) {
|
|
278
|
+
const attempts = this.missingChecks.get(threadId)?.attempts ?? 0;
|
|
279
|
+
const delay = [500, 1000, 2000][attempts] ?? 10_000;
|
|
280
|
+
this.missingChecks.set(threadId, { attempts: attempts + 1, nextCheckAt: this.now() + delay });
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
266
283
|
state = emptyState(filePath);
|
|
267
284
|
this.states.set(threadId, state);
|
|
268
|
-
this.
|
|
285
|
+
this.missingChecks.delete(threadId);
|
|
269
286
|
}
|
|
270
287
|
let info;
|
|
271
288
|
try { info = await stat(state.filePath); }
|