-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpanels-ui.js
More file actions
716 lines (697 loc) · 33.5 KB
/
Copy pathpanels-ui.js
File metadata and controls
716 lines (697 loc) · 33.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
(function (root, factory) {
const api = factory(root || {});
if (typeof module === "object" && module.exports) module.exports = api;
if (root) root.CodexPanelsUi = Object.freeze(api);
})(typeof window !== "undefined" ? window : globalThis, function (root) {
const fallbackEscapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[char]);
function createPanelsUi(deps = {}) {
const formatCore = deps.formatCore || root.CodexFormatCore || {};
const auditCore = deps.auditCore || root.CodexAuditCore || {};
const escapeHtml = deps.escapeHtml || formatCore.escapeHtml || fallbackEscapeHtml;
const formatTime = deps.formatTime || formatCore.formatTime || ((value) => value || "无记录");
const formatBytes = deps.formatBytes || formatCore.formatBytes || ((value) => `${Number(value) || 0} B`);
const auditTitle = deps.auditTitle || auditCore.auditTitle || ((item) => item?.action || "操作记录");
const auditDescription = deps.auditDescription || auditCore.auditDescription || ((item) => item?.result || "已完成");
function codexStatusSourceLabel(status = {}) {
if (status.source === "logs_2.sqlite") return "任务日志";
if (status.source === "process") return "进程检测";
if (!status.protocol_connected) return "任务日志";
return "任务日志";
}
function compareVersion(left, right) {
const a = String(left || "").split(".").map((part) => Number(part) || 0);
const b = String(right || "").split(".").map((part) => Number(part) || 0);
for (let index = 0; index < Math.max(a.length, b.length); index++) {
if ((a[index] || 0) !== (b[index] || 0)) return (a[index] || 0) - (b[index] || 0);
}
return 0;
}
function helperAutoSwitch(helper = {}) {
return helper.auto_switch || helper.autoSwitch || {};
}
function meaningfulText(value) {
const text = String(value || "").trim();
if (!text) return "";
if (/^(无|暂无|none|null|undefined|n\/a)$/i.test(text)) return "";
return text;
}
function helperDownloadUrl(release = {}) {
const file = release.downloadUrl || release.file || "downloads/CodexDockHelper.exe";
if (/^https?:\/\//i.test(file)) return file;
return file.startsWith("/") ? file : `/${file}`;
}
function helperPackageUrl(release = {}) {
const helperPackage = release.package || {};
const file = helperPackage.downloadUrl || helperPackage.file || "";
if (!file) return "";
if (/^https?:\/\//i.test(file)) return file;
return file.startsWith("/") ? file : `/${file}`;
}
function shortSha(value) {
const text = String(value || "").trim();
if (text.length <= 18) return text;
return `${text.slice(0, 12)}...${text.slice(-6)}`;
}
function hasAnyText(text, patterns = []) {
return patterns.some((pattern) => pattern.test(text));
}
function autoSwitchStage({
helperReady = false,
helper = {},
codex = {},
helperAuthorized = false,
userPresent = false,
minimumHelperVersion = "0.4.2",
} = {}) {
const autoSwitch = helperAutoSwitch(helper);
const version = helper.version || "";
const outdated = helperReady && (!version || compareVersion(version, minimumHelperVersion) < 0);
const livePendingReason = meaningfulText(codex.pending_switch_reason);
const restoredPendingReason = meaningfulText(autoSwitch.pending_reason || autoSwitch.pendingReason);
const pendingReason = livePendingReason || restoredPendingReason;
const pendingRevalidation = !livePendingReason && Boolean(restoredPendingReason)
&& (autoSwitch.pending_revalidation ?? autoSwitch.pendingRevalidation ?? true) !== false;
const stageKey = meaningfulText(autoSwitch.last_stage || autoSwitch.lastStage);
const stageLabel = meaningfulText(autoSwitch.last_stage_label || autoSwitch.lastStageLabel);
const failureStage = meaningfulText(autoSwitch.last_failure_stage || autoSwitch.lastFailureStage);
const failureDetail = meaningfulText(autoSwitch.last_failure_detail || autoSwitch.lastFailureDetail);
const failureCount = Number(autoSwitch.failure_count || autoSwitch.failureCount || 0);
const backoffUntil = autoSwitch.failure_backoff_until || autoSwitch.failureBackoffUntil || "";
const pauseUntil = autoSwitch.failure_pause_until || autoSwitch.failurePauseUntil || "";
const pauseReason = meaningfulText(autoSwitch.failure_pause_reason || autoSwitch.failurePauseReason);
const lastResult = meaningfulText(autoSwitch.last_result || autoSwitch.lastResult);
const lastReason = meaningfulText(autoSwitch.last_reason || autoSwitch.lastReason);
const normalizedStageKey = stageKey.toLowerCase().replace(/_/g, "-");
const backoffText = backoffUntil ? `退避至 ${formatTime(backoffUntil)}` : "";
const pauseText = pauseUntil ? `自动暂停至 ${formatTime(pauseUntil)}` : "";
const resultText = meaningfulText([lastReason, lastResult, failureDetail ? `失败详情:${failureDetail}` : "", pauseReason ? `暂停原因:${pauseReason}` : "", backoffText, pauseText].filter(Boolean).join(";"));
const resultProbe = resultText.toLowerCase();
const sourceLabel = helperReady ? codexStatusSourceLabel(codex) : "未连接";
const taskEvent = meaningfulText(codex.last_task_event) || meaningfulText(codex.detail) || meaningfulText(codex.label);
const evidence = helperReady
? `${codex.safe_to_switch ? "安全门已打开" : codex.safe_to_switch === false ? "安全门关闭" : "安全门确认中"} · ${sourceLabel}${taskEvent ? ` · ${taskEvent}` : ""}`
: "Agent 未连接,暂无本机边界证据";
const trigger = pendingReason || lastReason || "暂无触发";
const lastSeen = autoSwitch.last_check || autoSwitch.cloud_last_sync || autoSwitch.last_switch || "";
const base = {
className: "warn",
key: "monitoring",
eyebrow: "自动切换阶段",
title: "持续监控",
summary: "Agent 按阈值观察额度和任务状态,只在安全边界换号。",
trigger,
evidence,
result: resultText || "暂无执行结果",
stage: stageLabel || stageKey || "监控中",
next: "保持 Agent 在线;触发后会先保护当前任务。",
lastSeen,
};
if (!helperReady) {
return {
...base,
className: "bad",
key: "offline",
title: "等待 Agent 在线",
summary: "本机 Agent 未连接,无法读取任务边界或执行自动切换。",
trigger: "未连接",
result: "本机状态不可用",
next: "启动或安装最新版 Agent,再刷新状态。",
};
}
if (outdated) {
return {
...base,
className: "warn",
key: "upgrade_required",
title: "等待 Agent 升级",
summary: `当前 Agent 版本 ${version || "未上报"} 低于最低支持版本 ${minimumHelperVersion}。`,
result: resultText || "版本不满足自动切换要求",
next: "下载最新版 Agent 并重启。",
};
}
if (!helperAuthorized) {
return {
...base,
className: "warn",
key: "unauthorized",
title: "等待设备授权",
summary: userPresent ? "Agent 在线,但还不能接收当前云控制台下发的切换任务。" : "登录后才能把这台 Agent 授权给云控制台。",
trigger: "未授权",
result: resultText || "自动切换未绑定当前控制台",
next: userPresent ? "点击“授权 Agent”,绑定当前设备。" : "先登录云账号,再授权 Agent。",
};
}
if (autoSwitch.enabled === false) {
return {
...base,
className: "warn",
key: "disabled",
title: "自动切换未开启",
summary: "设备已授权,但后台自动切换守护处于关闭状态。",
result: resultText || "等待启用",
next: "在智能切换设置中开启后台自动切换。",
};
}
if (normalizedStageKey === "failure-paused" || pauseUntil) {
return {
...base,
className: "bad",
key: "failure_paused",
title: "自动切换已暂停",
summary: `连续失败${failureCount ? ` ${failureCount} 次` : ""}后,Agent 暂停本机自动切换,避免重复消耗账号和刷屏。`,
result: resultText || "等待处理失败原因",
stage: stageLabel || "自动暂停",
next: "处理候选账号、RT、设备授权或本机写入问题后,点击“恢复自动切换”。",
};
}
if (normalizedStageKey === "failure-backoff" || backoffUntil) {
return {
...base,
className: "warn",
key: "failure_backoff",
title: "失败退避中",
summary: "上一轮自动切换未完成,Agent 已暂停重复触发。",
result: resultText || "等待退避结束",
stage: stageLabel || "失败退避",
next: "等待退避结束;同时检查候选账号、RT 状态和额度刷新来源。",
};
}
if (pendingRevalidation) {
return {
...base,
className: "warn",
key: "pending_revalidation",
title: "恢复待切计划",
summary: "Agent 重启后保留了尚未处理的触发原因,正在重新核验。",
result: resultText || "等待重新核验",
stage: stageLabel || "恢复待切计划",
next: "核验完成前不会写入 auth 或重启 Codex;保持 Agent 在线即可。",
};
}
if ((pendingReason && codex.safe_to_switch === false) || ["draining-active-turn", "waiting-boundary"].includes(normalizedStageKey)) {
return {
...base,
className: "warn",
key: "draining_active_turn",
title: "保护当前任务",
summary: "额度已耗尽或账号状态已触发切换,但当前 Codex 轮次仍可能继续执行,暂不抢切。",
result: resultText || "等待安全边界",
next: "等待当前轮完成;安全门打开后再请求候选账号。",
};
}
if ((pendingReason && codex.safe_to_switch === true) || ["boundary-confirming", "boundary-confirmed"].includes(normalizedStageKey)) {
return {
...base,
className: "ok",
key: "boundary_confirming",
title: "安全边界已确认",
summary: "触发条件仍存在,当前任务边界已经安全,正在确认安全切换时机。",
result: resultText || "准备执行切换",
next: "Agent 将请求云端候选账号,随后写入 auth 并重启 Codex。",
};
}
if (normalizedStageKey === "candidate-selecting") {
return {
...base,
className: "ok",
key: "candidate_selecting",
title: "正在请求候选账号",
summary: "安全边界已确认,Agent 正在请求可切换账号。",
result: resultText || "等待云端候选选择",
next: "云端只下发候选载荷;真正切换由本机 Agent 执行。",
};
}
if (normalizedStageKey === "payload-issued") {
return {
...base,
className: "ok",
key: "payload_issued",
title: "已取得切换载荷",
summary: "云端已下发候选账号,Agent 准备写入新的 auth。",
result: resultText || "准备写入 auth",
next: "进入 auth 写入阶段;写入完成后再重启并恢复 Codex。",
};
}
if (normalizedStageKey === "writing-auth") {
return {
...base,
className: "warn",
key: "writing_auth",
title: "正在写入 auth",
summary: "Agent 正在备份并写入新的 auth.json。",
result: resultText || "正在写入 auth.json",
next: "写入成功后会刷新本地授权状态,并按配置重启 Codex。",
};
}
if (normalizedStageKey === "restarting-codex") {
return {
...base,
className: "warn",
key: "restarting_codex",
title: "正在重启 Codex",
summary: "auth 已写入,Agent 正在重启 Codex。",
result: resultText || "等待 Codex 重启",
next: "等待 Codex 启动完成;如果有原窗口目标,下一步会尝试恢复。",
};
}
if (normalizedStageKey === "restoring-window") {
return {
...base,
className: "warn",
key: "restoring_window",
title: "正在恢复窗口",
summary: "Codex 已重新启动,Agent 正在恢复目标窗口。",
result: resultText || "等待窗口恢复",
next: "恢复完成后会记录已切换;若窗口无法识别,Codex 仍会保持新授权状态。",
};
}
if (normalizedStageKey === "switching" || hasAnyText(resultProbe, [/正在安全切换|安全切换|boundary-confirmed|正在准备切换/i])) {
return {
...base,
className: "warn",
key: "switching",
title: "正在执行切换",
summary: "任务边界已确认,Agent 正在请求候选、写入 auth 并恢复 Codex。",
next: "等待切换任务完成;如果长时间停留,导出诊断查看写入或启动阶段。",
};
}
if (hasAnyText(resultProbe, [/失败|failed|error|异常|401|403|500|missing/i])) {
return {
...base,
className: "bad",
key: "failed",
title: "自动切换失败",
summary: "最近一次自动切换没有完成,需要查看候选 payload、auth 写入或 Codex 重启阶段。",
next: "导出诊断并检查账号 RT、设备授权、auth 写入权限和本机 Codex 启动状态。",
};
}
if (failureStage === "no-candidate" || hasAnyText(resultProbe, [/无可用候选|no-candidate|候选账号/i])) {
return {
...base,
className: "bad",
key: "no_candidate",
title: "没有可用候选",
summary: "云端已收到触发,但候选账号被冷却、不可用或不满足策略。",
next: "导入可用 RT 账号,或调整付费优先、避开当前账号和冷却策略。",
};
}
if (hasAnyText(resultProbe, [/冷却|cooldown/i])) {
return {
...base,
className: "warn",
key: "cooldown",
title: "切换冷却中",
summary: "已命中触发条件,但全局冷却正在保护账号池,避免连续抖动切换。",
next: "等待冷却结束;必要时用手动切换处理紧急任务。",
};
}
if (hasAnyText(resultProbe, [/未确认切换条件|未切换|not-triggered|not-switched/i])) {
return {
...base,
className: "warn",
key: "held_by_cloud",
title: "云端暂未放行",
summary: "Agent 已上报触发信息,但云端策略暂未放行。",
next: "继续观察额度和失败信号;如策略过严,可在设置中调整阈值。",
};
}
if (hasAnyText(resultProbe, [/已自动切换|switched/i]) || autoSwitch.last_switch) {
return {
...base,
className: "ok",
key: "switched",
title: "最近已切换",
summary: "最近一次自动切换已完成,当前账号池进入正常监控。",
next: "继续观察用量;冷却期内不会重复选择刚切过的账号。",
};
}
if (codex.safe_to_switch === false) {
return {
...base,
className: "warn",
key: "tail_observing",
title: "观察任务边界",
summary: "当前没有明确切换触发,但 Codex 仍未稳定空闲,自动切换会保持观察。",
next: "无需操作;触发出现后仍会先等待当前轮结束。",
};
}
return {
...base,
className: "ok",
key: "healthy",
title: "持续监控",
result: resultText || "检查正常",
next: "无需操作;当额度或授权信号触发时再进入保护流程。",
};
}
function renderAutoSwitchStage(stage = {}) {
const rows = [
["当前阶段", stage.stage || stage.title || "状态确认中"],
["触发", stage.trigger || "暂无触发"],
["边界证据", stage.evidence || "暂无证据"],
["最近结果", stage.result || "暂无执行结果"],
["最近检查", stage.lastSeen ? formatTime(stage.lastSeen) : "暂无记录"],
["下一步", stage.next || "继续观察"],
];
return `
<div class="auto-switch-stage-card ${escapeHtml(stage.className || "warn")}" data-auto-switch-stage="${escapeHtml(stage.key || "unknown")}">
<div class="auto-switch-stage-head">
<span>${escapeHtml(stage.eyebrow || "自动切换阶段")}</span>
<strong>${escapeHtml(stage.title || "状态确认中")}</strong>
<p>${escapeHtml(stage.summary || "正在根据 Agent 上报状态确认自动切换阶段。")}</p>
</div>
<div class="auto-switch-stage-grid">
${rows.map(([label, value]) => `
<div class="auto-switch-stage-item">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
</div>
`).join("")}
</div>
${stage.lastSeen ? `<small class="auto-switch-stage-time">最近检查 ${escapeHtml(formatTime(stage.lastSeen))}</small>` : ""}
</div>
`;
}
function renderHelperRelease({ helperReady = false, helper = {}, helperRelease = {}, minimumHelperVersion = "0.4.2" } = {}) {
const latestVersion = helperRelease.version || minimumHelperVersion;
const latestBuild = helperRelease.build_date || helperRelease.buildDate || "";
const currentVersion = helperReady ? (helper.version || "旧版未上报") : "未连接";
const currentUnsupported = helperReady && (!helper.version || compareVersion(helper.version, minimumHelperVersion) < 0);
const updateAvailable = helperReady && helper.version && latestVersion && compareVersion(helper.version, latestVersion) < 0;
const releaseKnown = Boolean(helperRelease.file || helperRelease.sha256 || helperRelease.version);
const packageInfo = helperRelease.package || {};
const packageUrl = helperPackageUrl(helperRelease);
const packageSummary = packageInfo.sha256
? ` · portable ${escapeHtml(formatBytes(packageInfo.bytes || 0))} · ZIP ${escapeHtml(shortSha(packageInfo.sha256))}`
: "";
const cardClass = helperReady && !currentUnsupported && !updateAvailable ? "ok" : "warn";
const statusText = !helperReady
? "未检测到本机 Agent,可先下载最新版。"
: currentUnsupported
? `当前 ${currentVersion} 低于最低支持版本 v${minimumHelperVersion},建议升级后重启 Agent。`
: updateAvailable
? `当前 ${currentVersion} 可用,但已有 v${latestVersion} 发布,建议在空闲时升级。`
: `当前 ${currentVersion} 可用;如需重新安装,可下载同版本发布包。`;
return `
<div class="helper-release-card ${escapeHtml(cardClass)}">
<div class="helper-release-main">
<span>Agent 版本</span>
<strong>最新版 v${escapeHtml(latestVersion)}${latestBuild ? ` · ${escapeHtml(latestBuild)}` : ""}</strong>
<small>${releaseKnown ? `EXE ${escapeHtml(formatBytes(helperRelease.bytes || 0))} · SHA-256 ${escapeHtml(shortSha(helperRelease.sha256))}${packageSummary}` : "发布包信息加载中。"}</small>
</div>
<div class="helper-release-current">
<span>当前设备</span>
<strong>${escapeHtml(statusText)}</strong>
</div>
<div class="helper-release-actions">
<a class="button-link primary-link" href="${escapeHtml(helperDownloadUrl(helperRelease))}" download>下载 Agent</a>
${packageUrl ? `<a class="button-link" href="${escapeHtml(packageUrl)}" download>下载 portable 包</a>` : ""}
<button type="button" data-helper-action="check-update" ${helperReady ? "" : "disabled"}>检查更新</button>
<button type="button" data-helper-action="copy-helper-sha" ${helperRelease.sha256 ? "" : "disabled"}>复制校验</button>
</div>
</div>
`;
}
function helperDiagnostic({
helperReady = false,
helper = {},
codex = {},
helperAuthorized = false,
userPresent = false,
minimumHelperVersion = "0.4.2",
} = {}) {
const autoSwitch = helperAutoSwitch(helper);
const version = helper.version || "";
const outdated = helperReady && (!version || compareVersion(version, minimumHelperVersion) < 0);
const tray = helper.tray || {};
const failurePauseUntil = autoSwitch.failure_pause_until || autoSwitch.failurePauseUntil || "";
const failurePauseReason = meaningfulText(autoSwitch.failure_pause_reason || autoSwitch.failurePauseReason);
const livePendingReason = meaningfulText(codex.pending_switch_reason);
const restoredPendingReason = meaningfulText(autoSwitch.pending_reason || autoSwitch.pendingReason);
const pendingRevalidation = !livePendingReason && Boolean(restoredPendingReason)
&& (autoSwitch.pending_revalidation ?? autoSwitch.pendingRevalidation ?? true) !== false;
if (!helperReady) {
return {
className: "bad",
title: "Agent 未连接",
reason: "未探测到本机代理,无法写入 auth、监测状态或自动切换。",
action: "启动或下载最新版 Agent,然后刷新状态。",
};
}
if (outdated) {
return {
className: "warn",
title: "Agent 需要升级",
reason: `当前 Agent 版本 ${version || "未上报"} 低于最低支持版本 ${minimumHelperVersion}。`,
action: "下载最新版 Agent 并重启。",
};
}
if (tray.last_error || tray.visible === false) {
return {
className: "warn",
title: "托盘需要修复",
reason: tray.last_error || "Agent 在线,但托盘状态未确认可见。",
action: "点击“修复托盘图标”,无需重启账号池。",
};
}
if (!helperAuthorized) {
return {
className: "warn",
title: "需要授权 Agent",
reason: userPresent
? "Agent 在线,但还没有绑定当前云控制台的设备令牌。"
: "Agent 在线;登录云账号后才能授权自动切换。",
action: userPresent ? "点击“授权 Agent”,让云端只向这台设备下发切换任务。" : "先登录云账号,再授权本机 Agent。",
};
}
if (failurePauseUntil) {
return {
className: "bad",
title: "自动切换已暂停",
reason: failurePauseReason || `连续失败后暂停至 ${formatTime(failurePauseUntil)}。`,
action: "处理失败原因后点击“恢复自动切换”,或等待暂停到期自动重试。",
};
}
if (pendingRevalidation) {
return {
className: "warn",
title: "恢复待切计划",
reason: restoredPendingReason,
action: "Agent 正在重新核验额度和任务边界;核验前不会写入 auth。",
};
}
const pendingSwitchReason = livePendingReason;
if (pendingSwitchReason) {
return {
className: codex.safe_to_switch ? "ok" : "warn",
title: codex.safe_to_switch ? "安全边界已确认" : "保护当前任务",
reason: pendingSwitchReason,
action: codex.safe_to_switch ? "已到安全边界,可以继续执行切换。" : "等待当前 Codex 轮次完成,不会抢切账号。",
};
}
if (codex.safe_to_switch === false) {
return {
className: "warn",
title: "等待安全边界",
reason: codex.detail || "Codex 当前仍在执行或尚未稳定空闲。",
action: "自动切换会继续观察任务日志,确认安全后再换号。",
};
}
if (autoSwitch.enabled === false) {
return {
className: "warn",
title: "自动切换未开启",
reason: "Agent 已授权,但本机自动切换守护当前处于关闭状态。",
action: "在智能切换设置中开启后台自动切换。",
};
}
return {
className: "ok",
title: "Agent 可用",
reason: "Agent 在线、已授权,Codex 当前处于可解释状态。",
action: "可以手动切换、刷新额度或交给智能切换。",
};
}
function renderAudit(audit = []) {
const list = audit.slice(0, 8);
if (!list.length) return '<div class="empty small">还没有云端运行记录。本地离线切换不会强制写云审计。</div>';
return list.map((item) => `
<div class="audit-item">
<span>${escapeHtml(formatTime(item.at || item.createdAt))}</span>
<strong>${escapeHtml(auditTitle(item))}</strong>
<span>${escapeHtml(auditDescription(item))}</span>
</div>
`).join("");
}
function renderDevice({
helperReady = false,
helper = {},
codex = {},
helperBase = "",
helperAuthorized = false,
userPresent = false,
minimumHelperVersion = "0.4.2",
helperRelease = {},
currentAuthChecking = false,
currentAuthMatched = false,
} = {}) {
const autoSwitch = helperAutoSwitch(helper);
const tray = helper.tray || {};
const diagnostic = helperDiagnostic({
helperReady,
helper,
codex,
helperAuthorized,
userPresent,
minimumHelperVersion,
});
const stage = autoSwitchStage({
helperReady,
helper,
codex,
helperAuthorized,
userPresent,
minimumHelperVersion,
});
const idleSeconds = Number(codex.idle_seconds);
const stableSeconds = Number(codex.stable_seconds);
const idleText = Number.isFinite(idleSeconds) && idleSeconds >= 0
? `${Math.floor(idleSeconds)} 秒`
: Number.isFinite(stableSeconds) && stableSeconds >= 0 ? `${Math.floor(stableSeconds)} 秒` : "未确认";
const lastEventTime = codex.last_task_event_at ? ` · ${formatTime(codex.last_task_event_at)}` : "";
const lastEvent = codex.last_task_event ? `${codex.last_task_event}${lastEventTime}` : "暂无近期任务事件";
const pendingReason = meaningfulText(codex.pending_switch_reason)
|| meaningfulText(autoSwitch.pending_reason || autoSwitch.pendingReason)
|| "无";
const switchSafety = codex.safe_to_switch ? "可安全切换" : "暂不切换";
const failurePauseUntil = autoSwitch.failure_pause_until || autoSwitch.failurePauseUntil || "";
const lastSwitch = autoSwitch.last_switch
? `${autoSwitch.last_switch_label || "已切换"} · ${formatTime(autoSwitch.last_switch)}`
: "无记录";
const lastResult = autoSwitch.last_reason || autoSwitch.last_result || "无";
const heartbeat = autoSwitch.cloud_last_sync || autoSwitch.last_check || "";
const trayStatus = helperReady
? `${tray.visible === false ? "未确认" : "已注册"}${tray.last_reason ? ` · ${tray.last_reason}` : ""}${tray.last_error ? ` · ${tray.last_error}` : ""}`
: "未连接";
const rows = [
["连接", helperReady ? "在线" : "未连接"],
["Agent 版本", helperReady ? (helper.version ? `v${helper.version}${helper.build_date ? ` · ${helper.build_date}` : ""}` : "旧版未上报") : "未连接"],
["地址", helperReady ? helperBase || "本机" : "未探测到"],
["端口", helper.port || "未识别"],
["设备授权", helperReady ? (helperAuthorized ? "已授权当前控制台" : "未授权或授权到其它控制台") : "未连接"],
["最近心跳", helperReady ? (heartbeat ? formatTime(heartbeat) : "无记录") : "未连接"],
["最近切换", helperReady ? lastSwitch : "未连接"],
["最近结果", helperReady ? lastResult : "未连接"],
["令牌到期", helperReady ? (autoSwitch.token_expires_at ? formatTime(autoSwitch.token_expires_at) : "未授权") : "未连接"],
["托盘", trayStatus],
["Codex 状态", helperReady ? (codex.label || "确认中") : "未探测"],
["状态来源", helperReady ? codexStatusSourceLabel(codex) : "未连接"],
["空闲时长", helperReady ? idleText : "未确认"],
["最近任务", helperReady ? lastEvent : "未连接"],
["待切换原因", helperReady ? pendingReason : "未连接"],
["安全门", helperReady ? switchSafety : "未连接"],
["当前 auth", currentAuthChecking ? "正在确认" : (currentAuthMatched ? "已识别" : "未匹配账号池")],
["执行", "写入 auth 并重启 Codex"],
];
const runtimeSignals = [
["连接", helperReady ? "在线" : "未连接", helperReady ? "ok" : "warn"],
["授权", helperReady ? (helperAuthorized ? "已授权" : "待授权") : "未连接", helperReady && helperAuthorized ? "ok" : "warn"],
["Codex", helperReady ? (codex.label || "确认中") : "未探测", helperReady && codex.safe_to_switch ? "ok" : "warn"],
["安全门", helperReady ? switchSafety : "未连接", helperReady && codex.safe_to_switch ? "ok" : "warn"],
];
return `
<div class="helper-diagnostic ${escapeHtml(diagnostic.className)}">
<div>
<span>结论</span>
<strong>${escapeHtml(diagnostic.title)}</strong>
</div>
<div>
<span>原因</span>
<strong>${escapeHtml(diagnostic.reason)}</strong>
</div>
<div>
<span>下一步</span>
<strong>${escapeHtml(diagnostic.action)}</strong>
</div>
</div>
<div class="helper-runtime-strip" aria-label="Agent 运行摘要">
${runtimeSignals.map(([label, value, className]) => `
<div class="helper-runtime-signal ${escapeHtml(className)}">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
</div>
`).join("")}
</div>
<div class="helper-action-row">
<button type="button" data-helper-action="refresh">刷新</button>
<button type="button" data-helper-action="authorize" ${helperReady && userPresent ? "" : "disabled"}>${helperAuthorized ? "重新授权" : "授权 Agent"}</button>
<button type="button" data-helper-action="repair-tray" ${helperReady ? "" : "disabled"}>修复托盘</button>
${failurePauseUntil ? `<button type="button" data-helper-action="resume-auto-switch" ${helperReady ? "" : "disabled"}>恢复自动切换</button>` : ""}
<button type="button" data-helper-action="open-status" ${helperReady ? "" : "disabled"}>状态页</button>
<button type="button" data-helper-action="export-diagnostics" ${helperReady ? "" : "disabled"}>导出诊断</button>
</div>
${renderAutoSwitchStage(stage)}
${renderHelperRelease({ helperReady, helper, helperRelease, minimumHelperVersion })}
<details class="helper-technical-details">
<summary>技术明细</summary>
<div class="device-grid">
${rows.map(([label, value]) => `
<div class="device-row">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
</div>
`).join("")}
</div>
</details>
<p class="muted-line">Agent 只读取本机任务事件,不展示或上传对话内容。</p>
`;
}
function securitySummary(account, helpers = {}) {
if (!account) {
return {
preview: "选择账号后显示摘要。",
warningHidden: true,
warningText: "",
};
}
const accountPlan = helpers.accountPlan || ((item) => item?.planType || "");
const hasUsableRefreshToken = helpers.hasUsableRefreshToken || ((item) => Boolean(item?.session?.tokens?.refresh_token || item?.hasRefreshToken));
let warningText = "";
if (!account.hasLocalSecret && account.cloudId && !helpers.userPresent) {
warningText = "这个账号只有云端元数据,需要登录云账号后才能获取切换 payload。";
} else if (!hasUsableRefreshToken(account)) {
warningText = "这个账号的 refresh_token 缺失或是占位值,长期可用性取决于 Codex 是否还能刷新。";
}
return {
preview: JSON.stringify({
account_id: account.accountId || "",
email: account.email || "",
plan_type: accountPlan(account),
expires_at: account.expiresAt || "",
has_refresh_token: hasUsableRefreshToken(account),
}, null, 2),
warningHidden: !warningText,
warningText,
};
}
return Object.freeze({
codexStatusSourceLabel,
autoSwitchStage,
renderAutoSwitchStage,
helperDiagnostic,
renderHelperRelease,
renderAudit,
renderDevice,
securitySummary,
});
}
return Object.freeze({
createPanelsUi,
});
});