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.
- 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 +1 -1
- package/features/session-status/README.md +7 -1
- package/features/session-status/src/session-status.mjs +68 -7
- 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 +75 -19
- 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
|
|
|
@@ -103,6 +104,8 @@ export class CodexRuntime {
|
|
|
103
104
|
this.scriptIds = new Map();
|
|
104
105
|
this.bindingTokens = new Map();
|
|
105
106
|
this.hostActionChains = new Map();
|
|
107
|
+
this.sessionStatusUpdates = new Map();
|
|
108
|
+
this.sessionStatusRevision = 0;
|
|
106
109
|
this.refreshPromise = null;
|
|
107
110
|
this.stopping = false;
|
|
108
111
|
this.monitorPromise = null;
|
|
@@ -290,7 +293,9 @@ export class CodexRuntime {
|
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
async startTargetDiscovery() {
|
|
293
|
-
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
|
+
});
|
|
294
299
|
if (!response?.ok) {
|
|
295
300
|
throw new Error(`Codex CDP version returned HTTP ${response?.status ?? "error"}`);
|
|
296
301
|
}
|
|
@@ -355,7 +360,9 @@ export class CodexRuntime {
|
|
|
355
360
|
|
|
356
361
|
async isAvailable() {
|
|
357
362
|
try {
|
|
358
|
-
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
|
+
});
|
|
359
366
|
if (!response?.ok) return false;
|
|
360
367
|
requireCdpWebSocketUrl(await response.json(), this.cdpPort);
|
|
361
368
|
return true;
|
|
@@ -518,6 +525,12 @@ export class CodexRuntime {
|
|
|
518
525
|
}
|
|
519
526
|
if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
|
|
520
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
|
+
}
|
|
521
534
|
if (feature && request.action === SURFACE_LOAD_ACTION) {
|
|
522
535
|
try {
|
|
523
536
|
const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
|
|
@@ -541,6 +554,18 @@ export class CodexRuntime {
|
|
|
541
554
|
await this.importGeneratedImage(feature, request.payload);
|
|
542
555
|
return;
|
|
543
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
|
+
}
|
|
544
569
|
try {
|
|
545
570
|
const result = request.action === "create-image-edit-thread"
|
|
546
571
|
? await this.createImageEditThread(client, feature, request.payload, request.requestId)
|
|
@@ -568,12 +593,15 @@ export class CodexRuntime {
|
|
|
568
593
|
if (!response?.ok) {
|
|
569
594
|
throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
|
|
570
595
|
}
|
|
596
|
+
return response.json();
|
|
571
597
|
}
|
|
572
598
|
|
|
573
599
|
async refreshTargets() {
|
|
574
600
|
let response;
|
|
575
601
|
try {
|
|
576
|
-
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
|
+
});
|
|
577
605
|
} catch (error) {
|
|
578
606
|
throw this.markCodexUnavailable(error);
|
|
579
607
|
}
|
|
@@ -604,7 +632,8 @@ export class CodexRuntime {
|
|
|
604
632
|
...this.targetFailures.keys(),
|
|
605
633
|
]);
|
|
606
634
|
for (const id of trackedTargetIds) {
|
|
607
|
-
|
|
635
|
+
const socketState = this.clients.get(id)?.socket.readyState;
|
|
636
|
+
if (liveRendererIds.has(id) && (socketState == null || socketState === 1)) continue;
|
|
608
637
|
removedTarget = true;
|
|
609
638
|
this.clients.get(id)?.socket.close();
|
|
610
639
|
this.clients.delete(id);
|
|
@@ -640,7 +669,13 @@ export class CodexRuntime {
|
|
|
640
669
|
const bindingToken = randomBytes(24).toString("hex");
|
|
641
670
|
this.bindingTokens.set(target.id, bindingToken);
|
|
642
671
|
client.on?.("Runtime.bindingCalled", (params) => {
|
|
643
|
-
|
|
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
|
|
644
679
|
.catch((error) => this.logger.warn?.("Codex Host action failed", error.message));
|
|
645
680
|
});
|
|
646
681
|
const source = createInjectionSource(this.features, {
|
|
@@ -693,22 +728,43 @@ export class CodexRuntime {
|
|
|
693
728
|
this.notifyStatusChange();
|
|
694
729
|
}
|
|
695
730
|
|
|
696
|
-
async publishSessionStatus() {
|
|
731
|
+
async publishSessionStatus(onlyTargetId = null) {
|
|
697
732
|
for (const [targetId, client] of this.clients) {
|
|
733
|
+
if (onlyTargetId && targetId !== onlyTargetId) continue;
|
|
698
734
|
if (!this.injectedTargets.has(targetId)) continue;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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);
|
|
712
768
|
}
|
|
713
769
|
}
|
|
714
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); }
|