oh-my-agent-web 0.2.0 → 0.4.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.
- package/README.md +86 -447
- package/dist/server/approval/read-only.js +107 -0
- package/dist/server/approval/rules.js +85 -0
- package/dist/server/approval/scope.js +150 -0
- package/dist/server/brand.js +2 -0
- package/dist/server/cli-config.js +52 -0
- package/dist/server/cli-info.js +101 -0
- package/dist/server/cli.js +304 -0
- package/dist/server/engine/builtin.js +61 -0
- package/dist/server/engine/catalog.js +8 -0
- package/dist/server/engine/codex-approvals.js +45 -0
- package/dist/server/engine/codex-map.js +298 -0
- package/dist/server/engine/codex-sessions.js +159 -0
- package/dist/server/engine/codex-transport.js +49 -0
- package/dist/server/engine/codex.js +314 -0
- package/dist/server/engine/dsh-history.js +195 -0
- package/dist/server/engine/dsh-log.js +245 -0
- package/dist/server/engine/dsh-map.js +221 -0
- package/dist/server/engine/dsh-sessions.js +207 -0
- package/dist/server/engine/dsh-transport.js +37 -0
- package/dist/server/engine/dsh.js +284 -0
- package/dist/server/engine/fake.js +231 -0
- package/dist/server/engine/jsonrpc-stdio.js +241 -0
- package/dist/server/engine/model-registry-reload.js +115 -0
- package/dist/server/engine/pi-events.js +148 -0
- package/dist/server/engine/pi-rename.js +14 -0
- package/dist/server/engine/pi-sdk.js +343 -0
- package/dist/server/engine/pi-session-model.js +23 -0
- package/dist/server/engine/pi-thinking.js +34 -0
- package/dist/server/engine/pi-transcript.js +225 -0
- package/dist/server/engine/pi-version.js +11 -0
- package/dist/server/engine/port.js +1 -0
- package/dist/server/engine/registry.js +47 -0
- package/dist/server/engine/scratch.js +13 -0
- package/dist/server/engine/suggestion-guidance.js +9 -0
- package/dist/server/engine/switchable.js +22 -0
- package/dist/server/engine/wiring.js +107 -0
- package/dist/server/files/dir-routes.js +40 -0
- package/dist/server/files/file-search.js +63 -0
- package/dist/server/files/file-service.js +232 -0
- package/dist/server/files/files-routes.js +182 -0
- package/dist/server/files/git-info.js +186 -0
- package/dist/server/http/auth-routes.js +99 -0
- package/dist/server/http/auth.js +191 -0
- package/dist/server/http/aux-routes.js +24 -0
- package/dist/server/http/compress.js +75 -0
- package/dist/server/http/health-and-static.js +33 -0
- package/dist/server/http/live-routes.js +113 -0
- package/dist/server/http/login-page.js +200 -0
- package/dist/server/index.js +342 -520
- package/dist/server/markers/actions.js +262 -0
- package/dist/server/protocol-parse.js +201 -0
- package/dist/server/protocol-settings.js +1 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/providers/catalog-store.js +85 -0
- package/dist/server/providers/catalog-writes.js +197 -0
- package/dist/server/providers/catalog.js +77 -0
- package/dist/server/providers/enabled-models.js +118 -0
- package/dist/server/providers/import-models.js +69 -0
- package/dist/server/providers/import-routes.js +136 -0
- package/dist/server/providers/model-test.js +52 -0
- package/dist/server/providers/probe.js +64 -0
- package/dist/server/providers/provider-handlers.js +203 -0
- package/dist/server/providers/provider-store.js +327 -0
- package/dist/server/providers/request-headers.js +7 -0
- package/dist/server/providers/validate.js +241 -0
- package/dist/server/push/push-routes.js +114 -0
- package/dist/server/push/push.js +120 -0
- package/dist/server/session/approval-bridge.js +103 -0
- package/dist/server/session/attachment-resolve.js +21 -0
- package/dist/server/session/client-view.js +82 -0
- package/dist/server/session/conversation-create.js +65 -0
- package/dist/server/session/delete-session.js +84 -0
- package/dist/server/session/engine-switch.js +108 -0
- package/dist/server/session/lock.js +82 -0
- package/dist/server/session/model-controller.js +111 -0
- package/dist/server/session/notices.js +31 -0
- package/dist/server/session/pi-opener.js +123 -0
- package/dist/server/session/pi-refresh.js +51 -0
- package/dist/server/session/pi-sessions-provider.js +46 -0
- package/dist/server/session/pi-sessions.js +272 -0
- package/dist/server/session/refresh-actions.js +42 -0
- package/dist/server/session/rename-session.js +23 -0
- package/dist/server/session/runtime-services.js +28 -0
- package/dist/server/session/runtime-types.js +1 -0
- package/dist/server/session/runtime.js +358 -0
- package/dist/server/session/session-stats.js +88 -0
- package/dist/server/session/settings.js +367 -0
- package/dist/server/session/snapshot-for.js +7 -0
- package/dist/server/session/stats-refresher.js +50 -0
- package/dist/server/session/switch-conversation.js +86 -0
- package/dist/server/session/throttle.js +14 -0
- package/dist/server/session/turn-events.js +126 -0
- package/dist/server/session/turn-finish.js +79 -0
- package/dist/server/session/turn-markers.js +26 -0
- package/dist/server/session/turn-queue.js +111 -0
- package/dist/server/session/turn-send.js +88 -0
- package/dist/server/session/turn-watchdog.js +77 -0
- package/dist/server/sessions/session-routes.js +17 -0
- package/dist/server/skills/skill-routes.js +28 -0
- package/dist/server/skills/skill-store.js +134 -0
- package/dist/server/snapshot/fan-out.js +10 -0
- package/dist/server/snapshot/messages.js +75 -0
- package/dist/server/snapshot/sender.js +62 -0
- package/dist/server/snapshot/state.js +193 -0
- package/dist/server/terminal/terminal-routes.js +176 -0
- package/dist/server/terminal/terminal-session.js +140 -0
- package/dist/server/uploads/upload-routes.js +64 -0
- package/dist/server/uploads/upload-store.js +115 -0
- package/dist/server/util/data-dir.js +36 -0
- package/dist/server/util/env.js +140 -0
- package/dist/server/util/ids.js +28 -0
- package/dist/server/util/log-file.js +67 -0
- package/dist/server/util/log.js +52 -0
- package/dist/server/util/origin.js +75 -0
- package/dist/server/util/paths.js +60 -0
- package/dist/server/util/pricing-index.js +121 -0
- package/dist/server/util/pricing.js +214 -0
- package/dist/server/util/startup.js +34 -0
- package/dist/server/util/upstream-version.js +140 -0
- package/dist/server/util/usage.js +19 -0
- package/dist/server/util/version.js +50 -0
- package/dist/server/ws/handle-message.js +95 -0
- package/package.json +49 -63
- package/web/dist/apple-touch-icon-precomposed.png +0 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
- package/web/dist/assets/arc-DviL9o3c.js +1 -0
- package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
- package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
- package/web/dist/assets/array-BifhSqXX.js +1 -0
- package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
- package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
- package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
- package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
- package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
- package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
- package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
- package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
- package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
- package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
- package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
- package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
- package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
- package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
- package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
- package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
- package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
- package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
- package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
- package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
- package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
- package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
- package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
- package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
- package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
- package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
- package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
- package/web/dist/assets/dist-C86cKTuE.js +1 -0
- package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
- package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
- package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
- package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
- package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
- package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
- package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
- package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
- package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/web/dist/assets/index-Bjn56a2i.js +76 -0
- package/web/dist/assets/index-Drp6e5iU.css +1 -0
- package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
- package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
- package/web/dist/assets/init-C-OQMol4.js +1 -0
- package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
- package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
- package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
- package/web/dist/assets/katex-B_JcIwqj.js +257 -0
- package/web/dist/assets/line-CHV9_4lL.js +1 -0
- package/web/dist/assets/linear-C-eZsBTv.js +1 -0
- package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
- package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
- package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
- package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
- package/web/dist/assets/path-fybaL0A-.js +1 -0
- package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
- package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
- package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
- package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
- package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
- package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
- package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
- package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
- package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
- package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
- package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
- package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
- package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
- package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/dist/assets/src-Da91fG3g.js +1 -0
- package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
- package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
- package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
- package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
- package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
- package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
- package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
- package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
- package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
- package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicon.svg +20 -8
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-maskable-512.png +0 -0
- package/web/dist/index.html +21 -27
- package/web/dist/manifest.webmanifest +17 -0
- package/web/dist/maskable.svg +25 -0
- package/web/dist/sw.js +40 -0
- package/bin/oh-my-agent-web-ps.mjs +0 -203
- package/bin/oh-my-agent-web-ws.mjs +0 -593
- package/bin/oh-my-agent-web.mjs +0 -321
- package/bin/update-check.mjs +0 -142
- package/dist/server/codex-client.js +0 -310
- package/dist/server/codex-supervisor.js +0 -446
- package/dist/server/fs-service.js +0 -144
- package/dist/server/request-inspector.js +0 -458
- package/dist/shared/protocol.js +0 -112
- package/scripts/manage-server.mjs +0 -155
- package/scripts/omaw.mjs +0 -74
- package/shared/protocol.ts +0 -228
- package/web/dist/assets/index-B8-VzDf2.css +0 -1
- package/web/dist/assets/index-DeC2eiiq.js +0 -42
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每个客户端「自己正在看哪个会话」(ADR-0033)。
|
|
3
|
+
*
|
|
4
|
+
* 纪律:**每个浏览器只认自己那一份**。客户端有记录就看它自己的;
|
|
5
|
+
* 没记录就是「空」(界面回到欢迎页),**绝不回落到 `state.activeId`** ——
|
|
6
|
+
* 那份全局值只是服务端给 stats / models / 文件树用的「最近操作过的会话」,
|
|
7
|
+
* 一旦拿它当别人的默认视图,多标签 / 多设备就会互相拽着跳(真实症状:
|
|
8
|
+
* 电脑上切会话,手机上也跟着换)。
|
|
9
|
+
*
|
|
10
|
+
* 这里只存「谁在看哪」和「谁收过哪一版会话清单」,**不复制任何会话内容**:内容的事实源仍是
|
|
11
|
+
* `state.messages`,快照在发送时按客户端的这个值覆盖 `active` 字段即可
|
|
12
|
+
* (`buildSnapshot(state, limit, activeId)`)。
|
|
13
|
+
*/
|
|
14
|
+
/** 会话清单最多隔这么久重发一次(防丢帧:中间被合并 / 丢掉的帧自己长回来) */
|
|
15
|
+
export const PROJECTS_RESEND_MS = 30_000;
|
|
16
|
+
/** 视图表上限:断线不再删视图,就得防止无限增长(正常人也就几个标签页 / 设备) */
|
|
17
|
+
export const MAX_CLIENT_VIEWS = 200;
|
|
18
|
+
export function createClientView() {
|
|
19
|
+
/** clientId → 它自己在看的会话(null = 明确「什么都不看」,比如刚打开的新标签页) */
|
|
20
|
+
const active = new Map();
|
|
21
|
+
/** 能接受「省略会话清单」的客户端 */
|
|
22
|
+
const slim = new Set();
|
|
23
|
+
/** 这个客户端上次收到会话清单是哪个版本、什么时候 */
|
|
24
|
+
const projectsSent = new Map();
|
|
25
|
+
/** 记一条视图:重插到末尾当「最近使用」,超上限就把最久没动的丢掉 */
|
|
26
|
+
const remember = (clientId, conversationId) => {
|
|
27
|
+
active.delete(clientId);
|
|
28
|
+
active.set(clientId, conversationId);
|
|
29
|
+
if (active.size > MAX_CLIENT_VIEWS) {
|
|
30
|
+
const oldest = active.keys().next().value;
|
|
31
|
+
if (oldest !== undefined)
|
|
32
|
+
active.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
activeFor: (clientId, fallback) => {
|
|
37
|
+
// 服务端内部(没有 clientId):用全局那份
|
|
38
|
+
if (!clientId)
|
|
39
|
+
return fallback;
|
|
40
|
+
// 认识这个客户端:**只认它自己的**(null 也照原样返回 = 空视图)
|
|
41
|
+
if (active.has(clientId))
|
|
42
|
+
return active.get(clientId) ?? null;
|
|
43
|
+
// 第一次见:记成「空」。之后它自己点会话 / 发消息会把记录填上
|
|
44
|
+
remember(clientId, null);
|
|
45
|
+
return null;
|
|
46
|
+
},
|
|
47
|
+
setActive: (clientId, conversationId) => {
|
|
48
|
+
remember(clientId, conversationId);
|
|
49
|
+
},
|
|
50
|
+
forget: (conversationId) => {
|
|
51
|
+
for (const [clientId, id] of active)
|
|
52
|
+
if (id === conversationId)
|
|
53
|
+
active.delete(clientId);
|
|
54
|
+
},
|
|
55
|
+
drop: (clientId) => {
|
|
56
|
+
// 视图(active)**故意保留**:重连后还要认自己看过哪个会话,见类型注释
|
|
57
|
+
slim.delete(clientId);
|
|
58
|
+
projectsSent.delete(clientId);
|
|
59
|
+
},
|
|
60
|
+
reset: () => {
|
|
61
|
+
active.clear();
|
|
62
|
+
projectsSent.clear();
|
|
63
|
+
},
|
|
64
|
+
size: () => [...active.values()].filter((id) => id !== "").length,
|
|
65
|
+
setSlim: (clientId, next) => {
|
|
66
|
+
if (next)
|
|
67
|
+
slim.add(clientId);
|
|
68
|
+
else
|
|
69
|
+
slim.delete(clientId);
|
|
70
|
+
},
|
|
71
|
+
needsProjects: (clientId, version, now) => {
|
|
72
|
+
// 不认识的客户端(全局快照 / WS)一律带:宁可多发,不能让人家列表变空
|
|
73
|
+
if (!clientId || !slim.has(clientId))
|
|
74
|
+
return true;
|
|
75
|
+
const previous = projectsSent.get(clientId);
|
|
76
|
+
const shouldSend = !previous || previous.version !== version || now - previous.at >= PROJECTS_RESEND_MS;
|
|
77
|
+
if (shouldSend)
|
|
78
|
+
projectsSent.set(clientId, { version, at: now });
|
|
79
|
+
return shouldSend;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function createConversationFactory(options) {
|
|
2
|
+
return {
|
|
3
|
+
async create(seedText, workspaceOverride) {
|
|
4
|
+
const { state } = options;
|
|
5
|
+
const id = options.newConversationId();
|
|
6
|
+
// 新会话沿用「当前会话/当前工作区」的目录(多项目:不把新会话丢回默认根)
|
|
7
|
+
const previousWorkspace = state.activeId
|
|
8
|
+
? state.conversations.find((item) => item.id === state.activeId)?.workspace
|
|
9
|
+
: undefined;
|
|
10
|
+
// 优先级:显式指定(项目行上的「+」)> 当前会话的目录 > 当前工作区
|
|
11
|
+
const workspace = workspaceOverride ?? previousWorkspace ?? state.files?.root ?? state.workspace;
|
|
12
|
+
const title = seedText.replace(/\s+/g, " ").trim().slice(0, options.titleChars) || "新对话";
|
|
13
|
+
const { sessionPath, engineSessionId } = await options.engine.createSession({ sessionId: id, workspace });
|
|
14
|
+
// SDK 建会话时用的是**引擎默认模型**,必须显式切一下,否则就是「选的 A、跑的 B」。
|
|
15
|
+
// 优先级:本项目的默认模型(用户设过)> 本项目最近一个会话用的 > 界面上选着的
|
|
16
|
+
const wanted = options.defaultModelFor?.(workspace) ?? state.model;
|
|
17
|
+
if (wanted) {
|
|
18
|
+
const switched = (await options.engine.switchModel?.(id, wanted)) ?? false;
|
|
19
|
+
if (switched)
|
|
20
|
+
state.model = wanted; // 选择器跟着真值走
|
|
21
|
+
else {
|
|
22
|
+
// 切不过去(模型被删/没鉴权):让选择器回到真实值,别继续骗人
|
|
23
|
+
const current = (await options.engine.currentModel?.(id)) ?? null;
|
|
24
|
+
state.model = current ? `${current.provider}/${current.model}` : null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
options.opener.register({ id, sessionPath, engineSessionId, title, workspace });
|
|
28
|
+
state.messages[id] = [];
|
|
29
|
+
state.conversations = [
|
|
30
|
+
{ id, title, createdAt: options.now(), updatedAt: options.now(), workspace, messageCount: 0 },
|
|
31
|
+
...state.conversations,
|
|
32
|
+
];
|
|
33
|
+
state.activeId = id;
|
|
34
|
+
// 新会话从 0 开始:不先把统计清掉,第一帧快照还会带着上一个会话的上下文用量
|
|
35
|
+
state.sessionStats = null;
|
|
36
|
+
state.actions = []; // 建议不跟着新会话走
|
|
37
|
+
state.files = { root: workspace, treeVersion: (state.files?.treeVersion ?? 0) + 1 };
|
|
38
|
+
await options.onReady?.(id);
|
|
39
|
+
return id;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 「新建会话」这个请求的处理(含错误归一化)。
|
|
45
|
+
*
|
|
46
|
+
* 从 runtime 抽出来:新建会话要顺带把**发起者自己的视图**指过去(ADR-0033),
|
|
47
|
+
* 这几句放回 runtime 会破 400 行护栏。
|
|
48
|
+
*/
|
|
49
|
+
export function newConversationWith(deps) {
|
|
50
|
+
return async (workspace, clientId) => {
|
|
51
|
+
try {
|
|
52
|
+
const id = await deps.create("新对话", workspace);
|
|
53
|
+
// 每个客户端各记各的视图:不把发起者的指过来,它收到的快照仍是旧会话
|
|
54
|
+
if (clientId)
|
|
55
|
+
deps.clientView.setActive(clientId, id);
|
|
56
|
+
deps.pushSnapshot(); // ← 少了这一句,侧栏不会更新(冒烟抓到过)
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
deps.notice(`新建会话失败:${message}`, "error");
|
|
62
|
+
return { ok: false, error: { code: "session.create_failed", message } };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 删除会话(侧栏 ⋯ → 删除 / 归档视图的批量删除)。
|
|
3
|
+
*
|
|
4
|
+
* 两条路,按引擎能力选:
|
|
5
|
+
* 1. 引擎有原生删除(codex 的 `thread/delete`)→ 交给引擎,Codex Desktop 里也会消失;
|
|
6
|
+
* 2. 否则(pi)→ 沿用原来的做法:把会话文件改名成 `<名字>.deleted-<时间戳>`,
|
|
7
|
+
* 扫描不到(列表里消失),但文件还在磁盘上,误删可以手动改回来。
|
|
8
|
+
*
|
|
9
|
+
* 这里刻意**不真删文件**:删除是不可逆操作,工作台还没有废纸篓。
|
|
10
|
+
*/
|
|
11
|
+
import { rename } from "node:fs/promises";
|
|
12
|
+
/** 引擎侧「本来就没有」的错误文案(codex:no rollout / thread not found)→ 统一当作未找到 */
|
|
13
|
+
function isMissingError(message) {
|
|
14
|
+
return /no rollout|not found|不存在|已删除/i.test(message);
|
|
15
|
+
}
|
|
16
|
+
export function createSessionDeleter(deps) {
|
|
17
|
+
return async (conversationId) => {
|
|
18
|
+
const session = deps
|
|
19
|
+
.projects()
|
|
20
|
+
.flatMap((project) => project.sessions)
|
|
21
|
+
.find((item) => item.id === conversationId);
|
|
22
|
+
if (!session) {
|
|
23
|
+
// 侧栏没列出来(比如引擎自己隐藏的会话:codex 临时目录里的随手跑):
|
|
24
|
+
// 只要宿主还知道它的句柄,仍然允许删 —— 否则界面上「删不掉也找不到」
|
|
25
|
+
const handle = deps.handleOf(conversationId);
|
|
26
|
+
if (!handle || !deps.engine.deleteSession) {
|
|
27
|
+
return { ok: false, error: { code: "session.not_found", message: "找不到这条会话(可能已被清理)" } };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await deps.engine.deleteSession(handle);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: isMissingError(message)
|
|
37
|
+
? { code: "session.not_found", message: "这条会话已经不在了" }
|
|
38
|
+
: { code: "session.delete_failed", message },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
deps.discard(conversationId);
|
|
42
|
+
await deps.refresh().catch(() => undefined);
|
|
43
|
+
deps.pushSnapshot();
|
|
44
|
+
return { ok: true, mode: "engine" };
|
|
45
|
+
}
|
|
46
|
+
const engineHandle = session.engineSessionId ?? null;
|
|
47
|
+
if (engineHandle && deps.engine.deleteSession) {
|
|
48
|
+
try {
|
|
49
|
+
await deps.engine.deleteSession(engineHandle);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: isMissingError(message)
|
|
56
|
+
? { code: "session.not_found", message: "这条会话已经不在了" }
|
|
57
|
+
: { code: "session.delete_failed", message },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
deps.discard(conversationId);
|
|
61
|
+
await deps.refresh().catch(() => undefined);
|
|
62
|
+
deps.pushSnapshot();
|
|
63
|
+
return { ok: true, mode: "engine" };
|
|
64
|
+
}
|
|
65
|
+
const sessionPath = session.sessionPath ?? deps.handleOf(conversationId);
|
|
66
|
+
if (!sessionPath) {
|
|
67
|
+
return { ok: false, error: { code: "session.not_found", message: "这条会话没有可删除的落盘文件" } };
|
|
68
|
+
}
|
|
69
|
+
const target = `${sessionPath}.deleted-${Date.now()}`;
|
|
70
|
+
try {
|
|
71
|
+
await rename(sessionPath, target);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: { code: "session.delete_failed", message: error instanceof Error ? error.message : String(error) },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
deps.discard(conversationId);
|
|
80
|
+
await deps.refresh().catch(() => undefined);
|
|
81
|
+
deps.pushSnapshot();
|
|
82
|
+
return { ok: true, mode: "file", movedTo: target };
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { emptyRunning } from "../snapshot/state.js";
|
|
2
|
+
/**
|
|
3
|
+
* 注册项 → 快照里的可选引擎(badge 缺省用大写名字)。
|
|
4
|
+
* `active` 用来兜底:当前引擎即使标了 hidden(比如测试用的 fake)也要出现在列表里。
|
|
5
|
+
*/
|
|
6
|
+
export function engineOptions(descriptors, active) {
|
|
7
|
+
return descriptors
|
|
8
|
+
.filter((descriptor) => descriptor.hidden !== true || descriptor.name === active)
|
|
9
|
+
.map((descriptor) => ({
|
|
10
|
+
name: descriptor.name,
|
|
11
|
+
label: descriptor.label,
|
|
12
|
+
badge: descriptor.badge ?? descriptor.name.toUpperCase(),
|
|
13
|
+
description: descriptor.description ?? "",
|
|
14
|
+
capabilities: { ...descriptor.capabilities },
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 快照里的引擎清单:有目录就用目录的全部注册项;没有目录(单测 / 最小启动)
|
|
19
|
+
* 就退化成「只有当前这一个」,切换请求会被拒 —— 界面不会出现一个点不动的列表。
|
|
20
|
+
*/
|
|
21
|
+
export function engineOptionsFor(name, capabilities, descriptors) {
|
|
22
|
+
if (descriptors)
|
|
23
|
+
return engineOptions(descriptors, name);
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
name,
|
|
27
|
+
label: name,
|
|
28
|
+
badge: name.toUpperCase(),
|
|
29
|
+
description: "",
|
|
30
|
+
capabilities: { ...capabilities },
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export function createEngineSwitcher(deps) {
|
|
35
|
+
const { state } = deps;
|
|
36
|
+
/**
|
|
37
|
+
* 清掉「属于某个引擎」的状态。
|
|
38
|
+
* 刻意**保留**的东西:approvalMode(宿主策略,不是引擎的)、settings(客户端偏好)、
|
|
39
|
+
* workspace(目录)、terminal/files(那是磁盘与 PTY,跟引擎无关)。
|
|
40
|
+
*/
|
|
41
|
+
const clearEngineScopedState = () => {
|
|
42
|
+
state.conversations = [];
|
|
43
|
+
state.messages = {};
|
|
44
|
+
state.activeId = null;
|
|
45
|
+
state.queued = [];
|
|
46
|
+
state.running = emptyRunning();
|
|
47
|
+
state.runningByConversation = {};
|
|
48
|
+
state.statsByConversation = {};
|
|
49
|
+
state.sessionStats = null;
|
|
50
|
+
state.models = [];
|
|
51
|
+
state.model = null;
|
|
52
|
+
state.thinkingLevel = null;
|
|
53
|
+
state.thinkingLevels = [];
|
|
54
|
+
state.pendingApproval = null;
|
|
55
|
+
state.actions = [];
|
|
56
|
+
state.notices = [];
|
|
57
|
+
state.piProjects = [];
|
|
58
|
+
state.piProjectsVersion += 1; // 清单变了 → 下一帧必须带上(不能沿用旧引擎的那份)
|
|
59
|
+
state.providers = [];
|
|
60
|
+
state.enabledModels = [];
|
|
61
|
+
};
|
|
62
|
+
return async (name) => {
|
|
63
|
+
if (deps.isBusy()) {
|
|
64
|
+
return { ok: false, error: { code: "engine.busy", message: "还有回合在跑,先等它结束或中断再切引擎" } };
|
|
65
|
+
}
|
|
66
|
+
const entry = deps.engines?.get(name) ?? null;
|
|
67
|
+
if (!entry) {
|
|
68
|
+
return { ok: false, error: { code: "engine.unknown", message: `没有这个引擎:${name}` } };
|
|
69
|
+
}
|
|
70
|
+
if (entry.descriptor.name === state.engine)
|
|
71
|
+
return { ok: true };
|
|
72
|
+
// 先收掉旧引擎的外部资源(codex:关 app-server → 释放它持有的全部写锁),
|
|
73
|
+
// 再换指针。收不掉也不影响切换本身。
|
|
74
|
+
const previousEngine = deps.slot.current();
|
|
75
|
+
if (previousEngine !== entry.engine) {
|
|
76
|
+
await Promise.resolve(previousEngine.dispose?.()).catch(() => undefined);
|
|
77
|
+
}
|
|
78
|
+
deps.slot.use(entry.engine);
|
|
79
|
+
state.engine = entry.descriptor.name;
|
|
80
|
+
state.engineCapabilities = { ...entry.descriptor.capabilities };
|
|
81
|
+
state.engines = engineOptionsFor(entry.descriptor.name, entry.descriptor.capabilities, deps.engines?.descriptors());
|
|
82
|
+
state.versions = { ...state.versions, engine: entry.engine.version?.() ?? null };
|
|
83
|
+
clearEngineScopedState();
|
|
84
|
+
deps.clientView.reset();
|
|
85
|
+
deps.clearSessions();
|
|
86
|
+
deps.sessionsSlot.current = deps.engines?.sessionsFor(entry.descriptor.name) ?? null;
|
|
87
|
+
entry.engine.setApprovalHandler(deps.approvalHandler);
|
|
88
|
+
// 先推一帧「已经切过去、还是空的」:前端立刻把侧栏与模型换掉,不用等扫描
|
|
89
|
+
deps.pushSnapshot();
|
|
90
|
+
try {
|
|
91
|
+
await deps.refreshModels();
|
|
92
|
+
// 懒启动的引擎(codex/dsh)在 refreshModels 里才真正连上 → 版本号这时才有
|
|
93
|
+
state.versions = { ...state.versions, engine: entry.engine.version?.() ?? null };
|
|
94
|
+
if (entry.descriptor.providerConfig !== false && deps.loadProviders) {
|
|
95
|
+
state.providers = await deps.loadProviders();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
deps.notice(`切换后加载模型失败:${error instanceof Error ? error.message : String(error)}`, "warn");
|
|
100
|
+
}
|
|
101
|
+
deps.pushSnapshot();
|
|
102
|
+
await deps.refreshSessions(true).catch((error) => {
|
|
103
|
+
deps.notice(`读取历史会话失败:${error instanceof Error ? error.message : String(error)}`, "warn");
|
|
104
|
+
});
|
|
105
|
+
deps.notice(`已切换到 ${entry.descriptor.label}`, "info");
|
|
106
|
+
return { ok: true };
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单实例锁:`<dataDir>/server.lock`(ADR-0007)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是数据完整性的一部分:ADR-0005 的「只有一个写入者」是整套持久化推理的前提。
|
|
5
|
+
* 两个进程同时追加同一批 jsonl = 转录损坏。所以「终端 A 忘关、又开终端 B」这件事
|
|
6
|
+
* 必须在启动时被拦住,而不是等数据坏了才发现。
|
|
7
|
+
*
|
|
8
|
+
* 三态(ADR-0007 守卫 1):
|
|
9
|
+
* - 无锁 → 建锁,启动;
|
|
10
|
+
* - 有锁且 pid 存活 → 拒绝启动,并把对方是谁告诉用户;
|
|
11
|
+
* - 有锁但 pid 已死 → **陈旧锁,自动接管**(否则崩一次之后服务就永远起不来了)。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
export const LOCK_FILE = "server.lock";
|
|
16
|
+
export function lockPath(dataDir) {
|
|
17
|
+
return join(dataDir, LOCK_FILE);
|
|
18
|
+
}
|
|
19
|
+
export function parseLock(text) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(text);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
28
|
+
return null;
|
|
29
|
+
const candidate = parsed;
|
|
30
|
+
if (typeof candidate.pid !== "number" || typeof candidate.port !== "number")
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
pid: candidate.pid,
|
|
34
|
+
port: candidate.port,
|
|
35
|
+
startedAt: typeof candidate.startedAt === "number" ? candidate.startedAt : 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** 默认的存活判断:`process.kill(pid, 0)` 不抛就是活着(EPERM 也算活着,只是不归我们管) */
|
|
39
|
+
export function defaultIsAlive(pid) {
|
|
40
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code === "EPERM";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function readLock(dataDir) {
|
|
51
|
+
try {
|
|
52
|
+
return parseLock(await readFile(lockPath(dataDir), "utf8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function writeLock(dataDir, info) {
|
|
59
|
+
const target = lockPath(dataDir);
|
|
60
|
+
await mkdir(dirname(target), { recursive: true });
|
|
61
|
+
await writeFile(target, `${JSON.stringify(info)}\n`, "utf8");
|
|
62
|
+
}
|
|
63
|
+
export async function acquireLock(dataDir, info, deps = { isAlive: defaultIsAlive }) {
|
|
64
|
+
const existing = await readLock(dataDir);
|
|
65
|
+
if (existing && existing.pid !== info.pid && deps.isAlive(existing.pid)) {
|
|
66
|
+
return { ok: false, holder: existing };
|
|
67
|
+
}
|
|
68
|
+
const stale = existing !== null;
|
|
69
|
+
await writeLock(dataDir, info);
|
|
70
|
+
return { ok: true, stale, previous: existing };
|
|
71
|
+
}
|
|
72
|
+
/** 释放锁:只有锁还属于自己时才删(避免把接管者的锁删掉) */
|
|
73
|
+
export async function releaseLock(dataDir, pid) {
|
|
74
|
+
const existing = await readLock(dataDir);
|
|
75
|
+
if (!existing || existing.pid !== pid)
|
|
76
|
+
return;
|
|
77
|
+
await rm(lockPath(dataDir), { force: true });
|
|
78
|
+
}
|
|
79
|
+
export function describeHolder(holder) {
|
|
80
|
+
const started = holder.startedAt > 0 ? new Date(holder.startedAt).toLocaleString() : "未知时间";
|
|
81
|
+
return `pid ${holder.pid} · 端口 ${holder.port} · 启动于 ${started}`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function createModelController(options) {
|
|
2
|
+
return {
|
|
3
|
+
async refresh(conversationId) {
|
|
4
|
+
// 取不到就**保留旧列表**,绝不清空。
|
|
5
|
+
// 原因:切会话 / 引擎刚启动的那一瞬间 listModels 会返回空或抛错,
|
|
6
|
+
// 一旦清空,输入区的模型选择器与侧栏「模型」面板里的内置 provider 会一起消失
|
|
7
|
+
// (用户看到的就是「刚才还有,怎么又不见了」)。
|
|
8
|
+
try {
|
|
9
|
+
const list = (await options.engine.listModels?.(conversationId ?? undefined)) ?? [];
|
|
10
|
+
if (list.length > 0 || options.state.models.length === 0) {
|
|
11
|
+
options.state.models = list.map((model) => ({ id: model.id, label: model.label }));
|
|
12
|
+
}
|
|
13
|
+
// 还没有当前模型(刚建会话 / 刚切引擎):用引擎标的默认项,别让选择器空着
|
|
14
|
+
if (!options.state.model && list.length > 0) {
|
|
15
|
+
const preferred = list.find((model) => model.current) ?? list[0];
|
|
16
|
+
if (preferred)
|
|
17
|
+
options.state.model = preferred.id;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// 保留旧值:下次刷新成功自然会更新
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
async reload(conversationId) {
|
|
25
|
+
await options.engine.reloadModels?.().catch(() => undefined);
|
|
26
|
+
await this.refresh(conversationId);
|
|
27
|
+
await this.refreshThinking(conversationId ?? null);
|
|
28
|
+
options.onChanged();
|
|
29
|
+
},
|
|
30
|
+
async refreshThinking(conversationId) {
|
|
31
|
+
// 与模型列表同一套纪律:取不到就保留旧值,别让控件闪没
|
|
32
|
+
try {
|
|
33
|
+
const levels = (await options.engine.listThinkingLevels?.(conversationId ?? undefined)) ?? [];
|
|
34
|
+
const current = (await options.engine.getThinkingLevel?.(conversationId ?? undefined)) ?? null;
|
|
35
|
+
if (levels.length === 0 && options.state.thinkingLevels.length > 0)
|
|
36
|
+
return;
|
|
37
|
+
options.state.thinkingLevels = levels;
|
|
38
|
+
options.state.thinkingLevel = current;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 保留旧值
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async setThinking(conversationId, level) {
|
|
45
|
+
if (!conversationId)
|
|
46
|
+
return { ok: false, error: { code: "session.none", message: "先打开一个会话" } };
|
|
47
|
+
const applied = (await options.engine.setThinkingLevel?.(conversationId, level)) ?? false;
|
|
48
|
+
if (!applied) {
|
|
49
|
+
const reason = options.engine.lastError?.();
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: {
|
|
53
|
+
code: "thinking.unsupported",
|
|
54
|
+
message: reason ? `切换思考档位失败:${reason}` : `这个模型不支持档位:${level}`,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
options.state.thinkingLevel = level;
|
|
59
|
+
options.onChanged();
|
|
60
|
+
return { ok: true };
|
|
61
|
+
},
|
|
62
|
+
async switchTo(conversationId, modelId) {
|
|
63
|
+
if (!conversationId)
|
|
64
|
+
return { ok: false, error: { code: "session.none", message: "先打开一个会话" } };
|
|
65
|
+
const switched = (await options.engine.switchModel?.(conversationId, modelId)) ?? false;
|
|
66
|
+
if (!switched) {
|
|
67
|
+
const reason = options.engine.lastError?.();
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
error: { code: "model.switch_failed", message: reason ? `切换模型失败:${reason}` : `切换模型失败:${modelId}` },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
options.state.model = modelId;
|
|
74
|
+
// 不再推提示:输入区的模型选择器已经在显示当前模型(提示条太占地方)
|
|
75
|
+
options.onChanged();
|
|
76
|
+
return { ok: true };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 「重读模型表」的独立入口(切引擎与 provider 热重载共用):
|
|
82
|
+
* prewarm(有的引擎要先把模型表挂到会话上)→ reloadModels → listModels。
|
|
83
|
+
* 取不到就**保留旧表**:模型下拉宁可短暂显示旧的,也不能整块消失。
|
|
84
|
+
*/
|
|
85
|
+
export function createModelRefresher(options) {
|
|
86
|
+
return async () => {
|
|
87
|
+
try {
|
|
88
|
+
await options.engine.prewarm?.();
|
|
89
|
+
await options.engine.reloadModels?.();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return; // 引擎没起起来(比如 codex 没装):保留旧表,别把切换流程打断
|
|
93
|
+
}
|
|
94
|
+
let list = [];
|
|
95
|
+
try {
|
|
96
|
+
list = (await options.engine.listModels?.()) ?? [];
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (list.length > 0) {
|
|
102
|
+
options.state.models = list.map((model) => ({ id: model.id, label: model.label }));
|
|
103
|
+
// 还没有当前模型(刚切引擎 / 还没开会话):用引擎给的默认项,别让选择器空着
|
|
104
|
+
if (!options.state.model) {
|
|
105
|
+
const preferred = list.find((model) => model.current) ?? list[0];
|
|
106
|
+
if (preferred)
|
|
107
|
+
options.state.model = preferred.id;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createNoticeSink(options) {
|
|
2
|
+
const dedupeMs = options.dedupeMs ?? 5000;
|
|
3
|
+
const max = options.max ?? 20;
|
|
4
|
+
const recent = new Map();
|
|
5
|
+
let notices = [];
|
|
6
|
+
return {
|
|
7
|
+
push(text, level, noticeKey) {
|
|
8
|
+
const key = `${level}:${text}`;
|
|
9
|
+
const ts = options.now();
|
|
10
|
+
const last = recent.get(key);
|
|
11
|
+
if (last !== undefined && ts - last < dedupeMs)
|
|
12
|
+
return null;
|
|
13
|
+
recent.set(key, ts);
|
|
14
|
+
// 同 key 原地替换:保留 id 与位置,只换文案/级别/时间戳
|
|
15
|
+
const existing = noticeKey ? notices.find((notice) => notice.key === noticeKey) : undefined;
|
|
16
|
+
const notice = {
|
|
17
|
+
id: existing?.id ?? options.nextId("notice"),
|
|
18
|
+
level,
|
|
19
|
+
text,
|
|
20
|
+
ts,
|
|
21
|
+
...(noticeKey ? { key: noticeKey } : {}),
|
|
22
|
+
};
|
|
23
|
+
notices = existing
|
|
24
|
+
? notices.map((item) => (item.id === existing.id ? notice : item))
|
|
25
|
+
: [...notices, notice].slice(-max);
|
|
26
|
+
options.onNotice(notice);
|
|
27
|
+
return notice;
|
|
28
|
+
},
|
|
29
|
+
all: () => notices,
|
|
30
|
+
};
|
|
31
|
+
}
|