oh-my-agent-web 0.3.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.
Files changed (250) hide show
  1. package/README.md +86 -455
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +61 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/pi-events.js +148 -0
  26. package/dist/server/engine/pi-rename.js +14 -0
  27. package/dist/server/engine/pi-sdk.js +343 -0
  28. package/dist/server/engine/pi-session-model.js +23 -0
  29. package/dist/server/engine/pi-thinking.js +34 -0
  30. package/dist/server/engine/pi-transcript.js +225 -0
  31. package/dist/server/engine/pi-version.js +11 -0
  32. package/dist/server/engine/port.js +1 -0
  33. package/dist/server/engine/registry.js +47 -0
  34. package/dist/server/engine/scratch.js +13 -0
  35. package/dist/server/engine/suggestion-guidance.js +9 -0
  36. package/dist/server/engine/switchable.js +22 -0
  37. package/dist/server/engine/wiring.js +107 -0
  38. package/dist/server/files/dir-routes.js +40 -0
  39. package/dist/server/files/file-search.js +63 -0
  40. package/dist/server/files/file-service.js +232 -0
  41. package/dist/server/files/files-routes.js +182 -0
  42. package/dist/server/files/git-info.js +186 -0
  43. package/dist/server/http/auth-routes.js +99 -0
  44. package/dist/server/http/auth.js +191 -0
  45. package/dist/server/http/aux-routes.js +24 -0
  46. package/dist/server/http/compress.js +75 -0
  47. package/dist/server/http/health-and-static.js +33 -0
  48. package/dist/server/http/live-routes.js +113 -0
  49. package/dist/server/http/login-page.js +200 -0
  50. package/dist/server/index.js +342 -520
  51. package/dist/server/markers/actions.js +262 -0
  52. package/dist/server/protocol-parse.js +201 -0
  53. package/dist/server/protocol-settings.js +1 -0
  54. package/dist/server/protocol.js +6 -0
  55. package/dist/server/providers/catalog-store.js +85 -0
  56. package/dist/server/providers/catalog-writes.js +197 -0
  57. package/dist/server/providers/catalog.js +77 -0
  58. package/dist/server/providers/enabled-models.js +118 -0
  59. package/dist/server/providers/import-models.js +69 -0
  60. package/dist/server/providers/import-routes.js +136 -0
  61. package/dist/server/providers/model-test.js +52 -0
  62. package/dist/server/providers/probe.js +64 -0
  63. package/dist/server/providers/provider-handlers.js +203 -0
  64. package/dist/server/providers/provider-store.js +327 -0
  65. package/dist/server/providers/request-headers.js +7 -0
  66. package/dist/server/providers/validate.js +241 -0
  67. package/dist/server/push/push-routes.js +114 -0
  68. package/dist/server/push/push.js +120 -0
  69. package/dist/server/session/approval-bridge.js +103 -0
  70. package/dist/server/session/attachment-resolve.js +21 -0
  71. package/dist/server/session/client-view.js +82 -0
  72. package/dist/server/session/conversation-create.js +65 -0
  73. package/dist/server/session/delete-session.js +84 -0
  74. package/dist/server/session/engine-switch.js +108 -0
  75. package/dist/server/session/lock.js +82 -0
  76. package/dist/server/session/model-controller.js +111 -0
  77. package/dist/server/session/notices.js +31 -0
  78. package/dist/server/session/pi-opener.js +123 -0
  79. package/dist/server/session/pi-refresh.js +51 -0
  80. package/dist/server/session/pi-sessions-provider.js +46 -0
  81. package/dist/server/session/pi-sessions.js +272 -0
  82. package/dist/server/session/refresh-actions.js +42 -0
  83. package/dist/server/session/rename-session.js +23 -0
  84. package/dist/server/session/runtime-services.js +28 -0
  85. package/dist/server/session/runtime-types.js +1 -0
  86. package/dist/server/session/runtime.js +358 -0
  87. package/dist/server/session/session-stats.js +88 -0
  88. package/dist/server/session/settings.js +367 -0
  89. package/dist/server/session/snapshot-for.js +7 -0
  90. package/dist/server/session/stats-refresher.js +50 -0
  91. package/dist/server/session/switch-conversation.js +86 -0
  92. package/dist/server/session/throttle.js +14 -0
  93. package/dist/server/session/turn-events.js +126 -0
  94. package/dist/server/session/turn-finish.js +79 -0
  95. package/dist/server/session/turn-markers.js +26 -0
  96. package/dist/server/session/turn-queue.js +111 -0
  97. package/dist/server/session/turn-send.js +88 -0
  98. package/dist/server/session/turn-watchdog.js +77 -0
  99. package/dist/server/sessions/session-routes.js +17 -0
  100. package/dist/server/skills/skill-routes.js +28 -0
  101. package/dist/server/skills/skill-store.js +134 -0
  102. package/dist/server/snapshot/fan-out.js +10 -0
  103. package/dist/server/snapshot/messages.js +75 -0
  104. package/dist/server/snapshot/sender.js +62 -0
  105. package/dist/server/snapshot/state.js +193 -0
  106. package/dist/server/terminal/terminal-routes.js +176 -0
  107. package/dist/server/terminal/terminal-session.js +140 -0
  108. package/dist/server/uploads/upload-routes.js +64 -0
  109. package/dist/server/uploads/upload-store.js +115 -0
  110. package/dist/server/util/data-dir.js +36 -0
  111. package/dist/server/util/env.js +140 -0
  112. package/dist/server/util/ids.js +28 -0
  113. package/dist/server/util/log-file.js +67 -0
  114. package/dist/server/util/log.js +52 -0
  115. package/dist/server/util/origin.js +75 -0
  116. package/dist/server/util/paths.js +60 -0
  117. package/dist/server/util/pricing-index.js +121 -0
  118. package/dist/server/util/pricing.js +214 -0
  119. package/dist/server/util/startup.js +34 -0
  120. package/dist/server/util/upstream-version.js +140 -0
  121. package/dist/server/util/usage.js +19 -0
  122. package/dist/server/util/version.js +50 -0
  123. package/dist/server/ws/handle-message.js +95 -0
  124. package/package.json +49 -63
  125. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  126. package/web/dist/apple-touch-icon.png +0 -0
  127. package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
  128. package/web/dist/assets/arc-DviL9o3c.js +1 -0
  129. package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
  130. package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
  131. package/web/dist/assets/array-BifhSqXX.js +1 -0
  132. package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
  133. package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
  134. package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
  135. package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
  136. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  137. package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
  138. package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
  139. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  140. package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
  141. package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
  142. package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
  143. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  144. package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
  145. package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
  146. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  147. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  148. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  149. package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
  150. package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
  151. package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
  152. package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
  153. package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
  154. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  155. package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
  156. package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
  157. package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
  158. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
  159. package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
  160. package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
  161. package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
  162. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  163. package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
  164. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  165. package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
  166. package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
  167. package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
  168. package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
  169. package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
  170. package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
  171. package/web/dist/assets/dist-C86cKTuE.js +1 -0
  172. package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
  173. package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
  174. package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
  175. package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
  176. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
  177. package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
  178. package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
  179. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
  180. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  181. package/web/dist/assets/index-Bjn56a2i.js +76 -0
  182. package/web/dist/assets/index-Drp6e5iU.css +1 -0
  183. package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
  184. package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
  185. package/web/dist/assets/init-C-OQMol4.js +1 -0
  186. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
  187. package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
  188. package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
  189. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  190. package/web/dist/assets/line-CHV9_4lL.js +1 -0
  191. package/web/dist/assets/linear-C-eZsBTv.js +1 -0
  192. package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
  193. package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
  194. package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
  195. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  196. package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
  197. package/web/dist/assets/path-fybaL0A-.js +1 -0
  198. package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
  199. package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
  200. package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
  201. package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
  202. package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
  203. package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
  204. package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
  205. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
  206. package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
  207. package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
  208. package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
  209. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  210. package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
  211. package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
  212. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  213. package/web/dist/assets/src-Da91fG3g.js +1 -0
  214. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
  215. package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
  216. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
  217. package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
  218. package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
  219. package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
  220. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
  221. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
  222. package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
  223. package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
  224. package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
  225. package/web/dist/favicon-32.png +0 -0
  226. package/web/dist/favicon.ico +0 -0
  227. package/web/dist/favicon.svg +20 -8
  228. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  229. package/web/dist/icons/icon-192.png +0 -0
  230. package/web/dist/icons/icon-512.png +0 -0
  231. package/web/dist/icons/icon-maskable-512.png +0 -0
  232. package/web/dist/index.html +21 -27
  233. package/web/dist/manifest.webmanifest +17 -0
  234. package/web/dist/maskable.svg +25 -0
  235. package/web/dist/sw.js +40 -0
  236. package/bin/oh-my-agent-web-ps.mjs +0 -203
  237. package/bin/oh-my-agent-web-ws.mjs +0 -782
  238. package/bin/oh-my-agent-web.mjs +0 -321
  239. package/bin/update-check.mjs +0 -142
  240. package/dist/server/codex-client.js +0 -310
  241. package/dist/server/codex-supervisor.js +0 -446
  242. package/dist/server/fs-service.js +0 -144
  243. package/dist/server/request-inspector.js +0 -458
  244. package/dist/shared/protocol.js +0 -112
  245. package/scripts/manage-server.mjs +0 -155
  246. package/scripts/omaw.mjs +0 -74
  247. package/shared/protocol.ts +0 -228
  248. package/web/dist/assets/index-5LJ5fRcO.js +0 -42
  249. package/web/dist/assets/index-QiDfPKFo.css +0 -1
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,314 @@
1
+ import { friendlyCodexError, itemsToTranscript, itemsToUiMessages, mapModels, thinkingLevelsOf, toolEndOf, toolStartOf, turnTs, } from "./codex-map.js";
2
+ import { createCodexClient } from "./codex-transport.js";
3
+ import { registerCodexApprovals } from "./codex-approvals.js";
4
+ import { sanitizeHistoricalActions } from "../markers/actions.js";
5
+ import { ensureCodexProject } from "./codex-sessions.js";
6
+ export function createCodexEngine(options = {}) {
7
+ const client = options.client ?? createCodexClient({ bin: options.bin, home: options.home, log: options.log ?? (() => { }) });
8
+ const bindings = new Map();
9
+ const byThread = new Map();
10
+ const active = new Map();
11
+ let models = null;
12
+ let approvalHandler = null;
13
+ /** 最近一次可预期失败的原因(给界面用,见 EnginePort.lastError) */
14
+ let lastError = null;
15
+ const threadOf = (sessionId) => {
16
+ const binding = bindings.get(sessionId);
17
+ if (binding)
18
+ return binding;
19
+ // 历史会话:侧栏给的 id 就是 threadId(provider 保证),直接认
20
+ if (/^[0-9a-f-]{36}$/i.test(sessionId)) {
21
+ const adopted = { threadId: sessionId, workspace: "", live: false };
22
+ bindings.set(sessionId, adopted);
23
+ byThread.set(sessionId, sessionId);
24
+ return adopted;
25
+ }
26
+ return null;
27
+ };
28
+ const loadModels = async () => {
29
+ if (models)
30
+ return models;
31
+ const result = await client.request("model/list", {});
32
+ models = result?.data ?? [];
33
+ return models;
34
+ };
35
+ const handleNotification = (notification) => {
36
+ const params = notification.params;
37
+ const threadId = typeof params.threadId === "string" ? params.threadId : "";
38
+ const conversationId = byThread.get(threadId);
39
+ const turn = conversationId ? active.get(conversationId) : undefined;
40
+ const emit = (event) => {
41
+ if (turn && !turn.ended)
42
+ turn.onEvent(event);
43
+ };
44
+ switch (notification.method) {
45
+ case "item/agentMessage/delta":
46
+ emit({ type: "text_delta", text: typeof params.delta === "string" ? params.delta : "" });
47
+ return;
48
+ case "item/reasoning/textDelta":
49
+ case "item/reasoning/summaryTextDelta":
50
+ emit({ type: "thinking_delta", text: typeof params.delta === "string" ? params.delta : "" });
51
+ return;
52
+ case "item/started": {
53
+ const event = toolStartOf((params.item ?? {}));
54
+ if (event)
55
+ emit(event);
56
+ return;
57
+ }
58
+ case "item/completed": {
59
+ const event = toolEndOf((params.item ?? {}));
60
+ if (event)
61
+ emit(event);
62
+ return;
63
+ }
64
+ case "thread/tokenUsage/updated": {
65
+ const usage = params.tokenUsage;
66
+ const last = usage?.last;
67
+ if (!last)
68
+ return;
69
+ emit({
70
+ type: "usage",
71
+ input: last.inputTokens ?? 0,
72
+ output: last.outputTokens ?? 0,
73
+ cacheRead: last.cachedInputTokens ?? 0,
74
+ cacheWrite: last.cacheWriteInputTokens ?? 0,
75
+ });
76
+ return;
77
+ }
78
+ case "turn/completed": {
79
+ if (!turn)
80
+ return;
81
+ const completed = (params.turn ?? {});
82
+ if (turn.turnId && completed.id && completed.id !== turn.turnId)
83
+ return;
84
+ const reason = completed.status === "interrupted" ? "interrupted" : completed.status === "failed" ? "error" : "done";
85
+ emit({ type: "turn_end", reason });
86
+ turn.ended = true;
87
+ turn.finish();
88
+ return;
89
+ }
90
+ case "error": {
91
+ // willRetry=true 是 codex 内部的瞬时重试(断流、429…):不该在对话里留一条红色错误
92
+ if (params.willRetry === true)
93
+ return;
94
+ const error = params.error;
95
+ const message = error?.message ?? "codex 报错";
96
+ emit({ type: "error", code: "engine.codex_error", message });
97
+ if (turn) {
98
+ emit({ type: "turn_end", reason: "error" });
99
+ turn.ended = true;
100
+ turn.finish();
101
+ }
102
+ return;
103
+ }
104
+ default:
105
+ return;
106
+ }
107
+ };
108
+ client.onNotification(handleNotification);
109
+ registerCodexApprovals({ client, getHandler: () => approvalHandler });
110
+ /** 当前模型(provider/model 口径):thread/read 是最准的来源 */
111
+ const currentModelOf = async (sessionId) => {
112
+ const binding = threadOf(sessionId);
113
+ if (!binding)
114
+ return null;
115
+ const result = await client.request("thread/read", { threadId: binding.threadId });
116
+ const model = result?.thread?.model ?? null;
117
+ if (!model)
118
+ return null;
119
+ // provider 留空 = 「id 本身就是全名」:codex 的模型 id 就是 slug,界面上不该显示 `codex/xxx`
120
+ return { provider: "", model };
121
+ };
122
+ /** 读最近若干轮(codex 默认倒序分页 → 翻正序;40 轮够对话区与侧栏用) */
123
+ const loadTurns = async (sessionId) => {
124
+ const binding = threadOf(sessionId);
125
+ if (!binding)
126
+ return [];
127
+ const result = await client.request("thread/turns/list", {
128
+ threadId: binding.threadId,
129
+ limit: 40,
130
+ sortDirection: "desc",
131
+ });
132
+ return [...(result?.data ?? [])].reverse();
133
+ };
134
+ /** 确保这个会话对应的 thread 已加载(历史会话要先 resume,否则无法开回合 / 改设置) */
135
+ const ensureLive = async (sessionId) => {
136
+ const binding = threadOf(sessionId);
137
+ if (!binding)
138
+ return null;
139
+ if (binding.live)
140
+ return binding;
141
+ await client.request("thread/resume", { threadId: binding.threadId, excludeTurns: true });
142
+ binding.live = true;
143
+ return binding;
144
+ };
145
+ return {
146
+ client,
147
+ version: () => client.version(),
148
+ async createSession(sessionOptions) {
149
+ // 对齐原生:新会话落在 codex 的 project 里(没有就建一个,幂等键按目录)
150
+ const projectId = await ensureCodexProject(client, sessionOptions.workspace);
151
+ const response = await client.request("thread/start", {
152
+ cwd: sessionOptions.workspace,
153
+ ...(projectId ? { projectId } : {}),
154
+ });
155
+ const threadId = response?.thread?.id;
156
+ if (!threadId)
157
+ throw new Error("codex 没有返回 thread id");
158
+ const binding = { threadId, workspace: sessionOptions.workspace, live: true };
159
+ bindings.set(sessionOptions.sessionId, binding);
160
+ byThread.set(threadId, sessionOptions.sessionId);
161
+ return { sessionPath: null, engineSessionId: threadId };
162
+ },
163
+ async openSession(openOptions) {
164
+ const threadId = openOptions.engineSessionId ?? openOptions.sessionPath ?? openOptions.sessionId;
165
+ const binding = { threadId, workspace: "", live: false };
166
+ bindings.set(openOptions.sessionId, binding);
167
+ byThread.set(threadId, openOptions.sessionId);
168
+ // 不在打开时 resume:历史会话常常只需要读;真要发消息时再 resume(少一次加载)
169
+ },
170
+ async sendMessage(sessionId, text, onEvent) {
171
+ const binding = await ensureLive(sessionId).catch((error) => {
172
+ throw new Error(friendlyCodexError(error));
173
+ });
174
+ if (!binding)
175
+ throw new Error("codex 会话还没准备好(先新建或打开一个会话)");
176
+ let finish = () => { };
177
+ const done = new Promise((resolve) => {
178
+ finish = resolve;
179
+ });
180
+ const turn = { turnId: null, onEvent, finish, ended: false };
181
+ active.set(sessionId, turn);
182
+ try {
183
+ const response = await client.request("turn/start", { threadId: binding.threadId, input: [{ type: "text", text, text_elements: [] }] }, { timeoutMs: 120_000 });
184
+ turn.turnId = response?.turn?.id ?? null;
185
+ await done;
186
+ }
187
+ finally {
188
+ turn.ended = true;
189
+ active.delete(sessionId);
190
+ }
191
+ },
192
+ async interrupt(sessionId) {
193
+ const binding = threadOf(sessionId);
194
+ const turn = active.get(sessionId);
195
+ if (!binding || !turn?.turnId)
196
+ return;
197
+ // 停止按钮是 fire-and-forget(看门狗也会直接 void 调它):RPC 失败不能让
198
+ // Promise 拒绝一路飘到进程级(Node 默认会直接把服务带走)。
199
+ try {
200
+ await client.request("turn/interrupt", { threadId: binding.threadId, turnId: turn.turnId });
201
+ }
202
+ catch (error) {
203
+ // 只记日志:停止失败最坏是「这一轮继续跑」,不该把进程一起带走
204
+ options.log?.(`停止失败:${error instanceof Error ? error.message : String(error)}`);
205
+ }
206
+ },
207
+ async closeSession(sessionId) {
208
+ const binding = bindings.get(sessionId);
209
+ bindings.delete(sessionId);
210
+ if (!binding)
211
+ return;
212
+ byThread.delete(binding.threadId);
213
+ await client.request("thread/unsubscribe", { threadId: binding.threadId }).catch(() => undefined);
214
+ },
215
+ async readUiMessages(sessionId) {
216
+ const turns = await loadTurns(sessionId);
217
+ const now = Date.now();
218
+ return sanitizeHistoricalActions(turns.flatMap((turn) => itemsToUiMessages(turn.items ?? [], turnTs(turn, now))));
219
+ },
220
+ async readTranscript(sessionId) {
221
+ const turns = await loadTurns(sessionId);
222
+ const now = Date.now();
223
+ return turns.flatMap((turn) => itemsToTranscript(turn.items ?? [], turnTs(turn, now)));
224
+ },
225
+ renameSession: (handle, name) => client.request("thread/name/set", { threadId: handle, name }).then(() => undefined),
226
+ deleteSession: (handle) => client.request("thread/delete", { threadId: handle }).then(() => undefined),
227
+ /**
228
+ * 不看了就放开写锁(`thread/unsubscribe`):Codex 桌面端随即可以打开这条会话。
229
+ * 下次再发消息会重新 resume(几毫秒),换来「两端不会互相锁死」。
230
+ */
231
+ async parkSession(sessionId) {
232
+ const binding = bindings.get(sessionId);
233
+ if (!binding || !binding.live)
234
+ return;
235
+ binding.live = false;
236
+ await client.request("thread/unsubscribe", { threadId: binding.threadId }).catch(() => undefined);
237
+ },
238
+ /**
239
+ * 换引擎 / 退出:关掉 app-server 子进程。
240
+ * 它持有我们 resume 过的**全部** thread 的写锁,不关就会一直挡着 Codex 桌面端。
241
+ */
242
+ async dispose() {
243
+ bindings.clear();
244
+ byThread.clear();
245
+ active.clear();
246
+ models = null;
247
+ client.close();
248
+ },
249
+ setApprovalHandler(handler) {
250
+ approvalHandler = handler;
251
+ },
252
+ async prewarm() {
253
+ await client.ensureStarted();
254
+ await loadModels().catch(() => []); // 模型表拿不到不影响会话,只是下拉为空
255
+ },
256
+ reloadModels: async () => {
257
+ models = null;
258
+ },
259
+ async listModels() {
260
+ return mapModels(await loadModels());
261
+ },
262
+ currentModel: currentModelOf,
263
+ sessionModel: currentModelOf,
264
+ async switchModel(sessionId, modelId) {
265
+ lastError = null;
266
+ try {
267
+ // 历史会话要先 resume:没加载的 thread 改不了设置,发回合也不行
268
+ const binding = await ensureLive(sessionId);
269
+ if (!binding) {
270
+ lastError = "先打开一个会话";
271
+ return false;
272
+ }
273
+ await client.request("thread/settings/update", { threadId: binding.threadId, model: modelId });
274
+ return true;
275
+ }
276
+ catch (error) {
277
+ lastError = friendlyCodexError(error);
278
+ return false;
279
+ }
280
+ },
281
+ lastError: () => lastError,
282
+ async listThinkingLevels(sessionId) {
283
+ const catalog = await loadModels().catch(() => []);
284
+ return thinkingLevelsOf(catalog, sessionId ? ((await currentModelOf(sessionId))?.model ?? null) : null);
285
+ },
286
+ async getThinkingLevel(sessionId) {
287
+ if (!sessionId)
288
+ return null;
289
+ const binding = threadOf(sessionId);
290
+ if (!binding)
291
+ return null;
292
+ const result = await client.request("thread/read", {
293
+ threadId: binding.threadId,
294
+ });
295
+ return result?.thread?.reasoningEffort ?? null;
296
+ },
297
+ async setThinkingLevel(sessionId, level) {
298
+ lastError = null;
299
+ try {
300
+ const binding = await ensureLive(sessionId);
301
+ if (!binding) {
302
+ lastError = "先打开一个会话";
303
+ return false;
304
+ }
305
+ await client.request("thread/settings/update", { threadId: binding.threadId, effort: level });
306
+ return true;
307
+ }
308
+ catch (error) {
309
+ lastError = friendlyCodexError(error);
310
+ return false;
311
+ }
312
+ },
313
+ };
314
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * DSH 会话日志 → 工作台的对话区消息 / 转录 / 统计(纯函数,单测直接覆盖)。
3
+ *
4
+ * 与 `dsh-log.ts` 分工:那边只管**怎么把文件读出来**(多帧 zstd、定位、标题素材),
5
+ * 这里只管**怎么映射成我们的形状**(消息块 / 工具状态 / 四桶用量)。
6
+ *
7
+ * 两条纪律:
8
+ * 1. `source.kind !== "user"` 的 user/message 是系统注入(runtime-context / skill-catalog / goal…),
9
+ * 不进对话区、不参与计数;
10
+ * 2. 工具块先立成 running,由 `tool/result` 收口;没有结果的(被中断)最后标 failed,
11
+ * 不能一直转圈骗人。
12
+ */
13
+ import { TOOL_EXCERPT_CHARS } from "../protocol.js";
14
+ import { isInternalUserMessage } from "./dsh-log.js";
15
+ /** 取 content[] 里的文本(user 消息与工具结果共用) */
16
+ function textOf(blocks) {
17
+ if (!Array.isArray(blocks))
18
+ return "";
19
+ return blocks
20
+ .map((block) => {
21
+ const item = block;
22
+ return item?.type === "text" && typeof item.text === "string" ? item.text : "";
23
+ })
24
+ .join("");
25
+ }
26
+ /** 日志 → 对话区消息(历史回放)。工具块先立成 running,由 tool/result 收口 */
27
+ export function logToUiMessages(log) {
28
+ const messages = [];
29
+ const toolBlocks = new Map();
30
+ for (const event of log.events) {
31
+ const ts = typeof event.time === "number" ? event.time : 0;
32
+ if (event.type === "user/message") {
33
+ if (isInternalUserMessage(event.data))
34
+ continue;
35
+ const text = textOf(event.data?.content);
36
+ if (text.trim() === "")
37
+ continue;
38
+ const id = typeof event.data?.id === "string" ? event.data.id : `u-${event.seq ?? messages.length}`;
39
+ messages.push({ id: `u-${id}`, role: "user", ts, blocks: [{ kind: "text", text }] });
40
+ continue;
41
+ }
42
+ if (event.type === "assistant/message") {
43
+ const message = event.data?.message;
44
+ if (!message)
45
+ continue;
46
+ const id = message.id ?? `a-${event.seq ?? messages.length}`;
47
+ const blocks = [];
48
+ for (const block of message.content ?? []) {
49
+ if (block.type === "reasoning" && typeof block.text === "string" && block.text !== "") {
50
+ blocks.push({ kind: "thinking", text: block.text });
51
+ continue;
52
+ }
53
+ if (block.type === "text" && typeof block.text === "string" && block.text !== "") {
54
+ blocks.push({ kind: "text", text: block.text });
55
+ continue;
56
+ }
57
+ if (block.type === "tool-call") {
58
+ const toolCallId = block.id ?? `tool-${event.seq ?? 0}`;
59
+ const args = block.arguments ?? "";
60
+ const toolBlock = {
61
+ kind: "tool",
62
+ toolCallId,
63
+ name: block.name ?? "tool",
64
+ argsSummary: args.length > 300 ? `${args.slice(0, 300)}…` : args,
65
+ argsFull: args,
66
+ status: "running",
67
+ outputExcerpt: "",
68
+ truncated: false,
69
+ startedAt: ts,
70
+ };
71
+ blocks.push(toolBlock);
72
+ continue;
73
+ }
74
+ }
75
+ if (blocks.length === 0)
76
+ continue;
77
+ const ui = {
78
+ id: `a-${id}`,
79
+ role: "assistant",
80
+ ts,
81
+ endedAt: ts,
82
+ blocks,
83
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
84
+ };
85
+ messages.push(ui);
86
+ for (const block of blocks)
87
+ if (block.kind === "tool")
88
+ toolBlocks.set(block.toolCallId, { message: ui, block });
89
+ continue;
90
+ }
91
+ if (event.type === "tool/result") {
92
+ const result = event.data?.message;
93
+ const toolCallId = result?.toolCallId;
94
+ if (!toolCallId)
95
+ continue;
96
+ const found = toolBlocks.get(toolCallId);
97
+ if (!found)
98
+ continue;
99
+ const raw = textOf(result?.content);
100
+ const clipped = raw.length > TOOL_EXCERPT_CHARS ? `${raw.slice(0, TOOL_EXCERPT_CHARS)}…` : raw;
101
+ found.block.status = result?.isError === true ? "failed" : "ok";
102
+ found.block.outputExcerpt = clipped;
103
+ found.block.truncated = raw.length > TOOL_EXCERPT_CHARS;
104
+ found.block.endedAt = ts;
105
+ }
106
+ }
107
+ // 没有结果的工具调用(被中断/崩溃):不能留在 running 上骗人
108
+ for (const { block } of toolBlocks.values()) {
109
+ if (block.status === "running")
110
+ block.status = "failed";
111
+ }
112
+ return messages;
113
+ }
114
+ /** 日志 → 引擎转录(只留正文,给需要纯文本的地方用) */
115
+ export function logToTranscript(log) {
116
+ const out = [];
117
+ const source = log.events;
118
+ for (const event of source) {
119
+ if (event.type === "user/message") {
120
+ if (isInternalUserMessage(event.data))
121
+ continue;
122
+ const text = textOf(event.data?.content).trim();
123
+ if (text !== "")
124
+ out.push({ role: "user", text, ts: typeof event.time === "number" ? event.time : 0 });
125
+ continue;
126
+ }
127
+ if (event.type === "assistant/message") {
128
+ const message = event.data?.message;
129
+ const text = (message?.content ?? [])
130
+ .filter((block) => block.type === "text")
131
+ .map((block) => block.text ?? "")
132
+ .join("");
133
+ if (text.trim() !== "")
134
+ out.push({ role: "assistant", text, ts: typeof event.time === "number" ? event.time : 0 });
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /** 日志 → 会话统计(用量来自 assistant/message.data.usage,四桶互斥) */
140
+ export function statsFromLog(log) {
141
+ const stats = {
142
+ userMessages: 0,
143
+ assistantMessages: 0,
144
+ toolCalls: 0,
145
+ toolResults: 0,
146
+ totalMessages: 0,
147
+ inputTokens: 0,
148
+ outputTokens: 0,
149
+ cacheRead: 0,
150
+ cacheWrite: 0,
151
+ totalTokens: 0,
152
+ contextTokens: 0,
153
+ };
154
+ let seen = false;
155
+ for (const event of log.events) {
156
+ if (event.type === "user/message") {
157
+ if (isInternalUserMessage(event.data))
158
+ continue;
159
+ stats.userMessages += 1;
160
+ seen = true;
161
+ continue;
162
+ }
163
+ if (event.type === "assistant/message") {
164
+ stats.assistantMessages += 1;
165
+ seen = true;
166
+ const content = event.data?.message?.content ?? [];
167
+ stats.toolCalls += content.filter((block) => block.type === "tool-call").length;
168
+ const usage = event.data?.usage;
169
+ if (usage) {
170
+ const input = number(usage.inputTokens);
171
+ const output = number(usage.outputTokens);
172
+ const cacheRead = number(usage.cacheReadTokens);
173
+ const cacheWrite = number(usage.cacheWriteTokens);
174
+ stats.inputTokens += input;
175
+ stats.outputTokens += output;
176
+ stats.cacheRead += cacheRead;
177
+ stats.cacheWrite += cacheWrite;
178
+ stats.contextTokens = input + cacheRead + cacheWrite;
179
+ }
180
+ continue;
181
+ }
182
+ if (event.type === "tool/result") {
183
+ stats.toolResults += 1;
184
+ seen = true;
185
+ }
186
+ }
187
+ if (!seen)
188
+ return null;
189
+ stats.totalMessages = stats.userMessages + stats.assistantMessages;
190
+ stats.totalTokens = stats.inputTokens + stats.outputTokens + stats.cacheRead + stats.cacheWrite;
191
+ return stats;
192
+ }
193
+ function number(value) {
194
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
195
+ }