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,245 @@
1
+ /**
2
+ * DSH 会话日志(v4)的只读解析 —— 侧栏标题、历史回放、token 统计都靠它。
3
+ *
4
+ * 为什么必须自己读文件:ACP 明确不支持 `session/load`(不回放历史),
5
+ * 所以「打开一条历史会话」只能读 DSH 自己落盘的日志。这里只**读**,从不写。
6
+ *
7
+ * 布局(dsh 0.2.0-rc.2,实测):
8
+ * <DSH_HOME>/sessions/<projectKey(cwd)>/session-<uuid>/session.v4.jsonl.zstd
9
+ * 每个写批次追加**一个 zstd 帧** → 必须多帧解压(`zstdDecompressSync` 只解首帧)。
10
+ * 帧内是 JSONL:第一行 `{type:"session",version:4,id,cwd,createdAt…}`,之后逐行事件
11
+ * `{type, seq, time, data}`。
12
+ *
13
+ * 事件面(v4 实测,只列 UI 用得到的):
14
+ * user/message data:{content:[{type:"text",text}], source:{kind}, id}
15
+ * assistant/message data:{turn,step,message:{role,content:[reasoning|text|tool-call],id},usage,stream}
16
+ * tool/result data:{turn,step,message:{toolCallId,content:[{type:"text",text}],isError}}
17
+ * session/title data:{title,source}
18
+ * turn/start|end、step/start|end、session 等结构性事件(不进 UI)
19
+ *
20
+ * ⚠️ `user/message` 里大量是**系统注入**(runtime-context / skill-catalog / goal …):
21
+ * 只有 `source.kind === "user"` 才是真的用户发言,其余一律不进对话区、也不拿来取标题。
22
+ */
23
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { zstdDecompressSync } from "node:zlib";
26
+ /** projectKey(cwd) —— 镜像 DSH 的会话目录命名:分隔符 → "-",其它非法字符 → ~XXXX(大写 hex) */
27
+ export function projectKey(cwd) {
28
+ let readable = "";
29
+ let separatorRun = false;
30
+ for (let i = 0; i < cwd.length; i += 1) {
31
+ const code = cwd.charCodeAt(i);
32
+ const ch = String.fromCharCode(code);
33
+ if (ch === "/" || ch === "\\" || ch === ":") {
34
+ if (!separatorRun)
35
+ readable += "-";
36
+ separatorRun = true;
37
+ }
38
+ else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
39
+ readable += ch;
40
+ separatorRun = false;
41
+ }
42
+ else {
43
+ readable += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
44
+ separatorRun = false;
45
+ }
46
+ }
47
+ return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
48
+ }
49
+ /** 多帧 zstd 解压:按魔数切帧;切错了就向后合并重试(数据体里可能误命中魔数) */
50
+ export function zstdDecompressAll(buf) {
51
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
52
+ const starts = [];
53
+ for (let i = 0; i + 4 <= buf.length; i += 1) {
54
+ if (buf[i] === MAGIC[0] && buf[i + 1] === MAGIC[1] && buf[i + 2] === MAGIC[2] && buf[i + 3] === MAGIC[3]) {
55
+ starts.push(i);
56
+ }
57
+ }
58
+ let out = "";
59
+ let index = 0;
60
+ while (index < starts.length) {
61
+ const from = starts[index];
62
+ let next = index + 1;
63
+ let decompressed = null;
64
+ while (next <= starts.length) {
65
+ const to = next < starts.length ? starts[next] : buf.length;
66
+ try {
67
+ decompressed = zstdDecompressSync(buf.subarray(from, to));
68
+ break;
69
+ }
70
+ catch {
71
+ next += 1;
72
+ }
73
+ }
74
+ if (decompressed) {
75
+ out += decompressed.toString("utf8");
76
+ index = next;
77
+ }
78
+ else {
79
+ index += 1;
80
+ }
81
+ }
82
+ return out || buf.toString("utf8");
83
+ }
84
+ /** 读一个会话日志文件 → { header, events }(坏行跳过,不因为一行报废整份历史) */
85
+ export function readSessionLog(file) {
86
+ const raw = readFileSync(file);
87
+ const text = file.endsWith(".zstd") ? zstdDecompressAll(raw) : raw.toString("utf8");
88
+ let header = null;
89
+ const events = [];
90
+ for (const line of text.split("\n")) {
91
+ if (line.trim() === "")
92
+ continue;
93
+ let value;
94
+ try {
95
+ value = JSON.parse(line);
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ if (value?.type === "session") {
101
+ header = value;
102
+ continue;
103
+ }
104
+ if (typeof value?.type === "string")
105
+ events.push(value);
106
+ }
107
+ return { header, events };
108
+ }
109
+ /** 只读文件头(第一帧的前几 KB)—— 扫侧栏时不必解整份日志 */
110
+ export function readSessionHeader(file) {
111
+ if (!file.endsWith(".zstd")) {
112
+ const text = readFileSync(file, "utf8");
113
+ const first = text.split("\n").find((line) => line.trim() !== "");
114
+ try {
115
+ return first ? JSON.parse(first) : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ const handle = readFileSync(file);
122
+ const chunk = handle.subarray(0, Math.min(handle.length, 16 * 1024));
123
+ const text = zstdDecompressAll(chunk);
124
+ const first = text.split("\n").find((line) => line.trim() !== "");
125
+ try {
126
+ return first ? JSON.parse(first) : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /** 系统注入的 user/message?白名单铁律:`source.kind !== "user"` 一律不算用户发言 */
133
+ export function isInternalUserMessage(data) {
134
+ if (!data)
135
+ return false;
136
+ const source = data.source;
137
+ if (source && typeof source.kind === "string")
138
+ return source.kind !== "user";
139
+ // 老版本没有 source:靠文本特征兜底
140
+ const content = data.content;
141
+ if (!Array.isArray(content))
142
+ return false;
143
+ return content.some((block) => typeof block?.text === "string" &&
144
+ (block.text.includes("<system-reminder>") ||
145
+ block.text.includes("<goal_round") ||
146
+ block.text.startsWith("Current runtime context") ||
147
+ block.text.includes("Current DSH file policy:")));
148
+ }
149
+ function textOf(blocks) {
150
+ if (!Array.isArray(blocks))
151
+ return "";
152
+ return blocks
153
+ .map((block) => {
154
+ const item = block;
155
+ return item?.type === "text" && typeof item.text === "string" ? item.text : "";
156
+ })
157
+ .join("");
158
+ }
159
+ /** 会话标题:日志里最后一条 `session/title`(DSH 自己也会写 fallback 标题) */
160
+ export function sessionTitle(log) {
161
+ let title = null;
162
+ for (const event of log.events) {
163
+ if (event.type !== "session/title")
164
+ continue;
165
+ const value = event.data?.title;
166
+ if (typeof value === "string" && value.trim() !== "")
167
+ title = value.trim();
168
+ }
169
+ return title;
170
+ }
171
+ /** 首条真实用户发言(标题兜底) */
172
+ export function firstUserText(log, maxChars = 30) {
173
+ for (const event of log.events) {
174
+ if (event.type !== "user/message")
175
+ continue;
176
+ if (isInternalUserMessage(event.data))
177
+ continue;
178
+ const text = textOf(event.data?.content).trim().replace(/\s+/g, " ");
179
+ if (text !== "")
180
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
181
+ }
182
+ return null;
183
+ }
184
+ /** 递归列出目录下的会话日志文件(按 mtime 倒序) */
185
+ export function findSessionFiles(root) {
186
+ if (!existsSync(root))
187
+ return [];
188
+ const out = [];
189
+ const walk = (dir, depth) => {
190
+ if (depth > 3)
191
+ return;
192
+ let entries;
193
+ try {
194
+ entries = readdirSync(dir, { withFileTypes: true });
195
+ }
196
+ catch {
197
+ return;
198
+ }
199
+ for (const entry of entries) {
200
+ const full = join(dir, entry.name);
201
+ if (entry.isDirectory())
202
+ walk(full, depth + 1);
203
+ else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl"))
204
+ out.push(full);
205
+ }
206
+ };
207
+ walk(root, 0);
208
+ return out.sort((left, right) => {
209
+ try {
210
+ return statSync(right).mtimeMs - statSync(left).mtimeMs;
211
+ }
212
+ catch {
213
+ return 0;
214
+ }
215
+ });
216
+ }
217
+ /** 按会话 id 找它的日志文件:优先用 cwd 直算目录,否则在 sessions 下一层找 */
218
+ export function locateSessionFile(sessionRoot, sessionId, cwd) {
219
+ const candidates = [];
220
+ const names = ["session.v4.jsonl.zstd", "session.jsonl.zstd"];
221
+ if (cwd)
222
+ for (const name of names)
223
+ candidates.push(join(sessionRoot, projectKey(cwd), sessionId, name));
224
+ for (const candidate of candidates)
225
+ if (existsSync(candidate))
226
+ return candidate;
227
+ // cwd 未知(历史会话还没打开过):扫一层
228
+ let groups;
229
+ try {
230
+ groups = readdirSync(sessionRoot, { withFileTypes: true });
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ for (const group of groups) {
236
+ if (!group.isDirectory())
237
+ continue;
238
+ for (const name of names) {
239
+ const candidate = join(sessionRoot, group.name, sessionId, name);
240
+ if (existsSync(candidate))
241
+ return candidate;
242
+ }
243
+ }
244
+ return null;
245
+ }
@@ -0,0 +1,221 @@
1
+ /** 把 configOptions 里可能分组的选项摊平(保留 group 名,便于显示) */
2
+ export function flattenChoices(option) {
3
+ if (!option?.options)
4
+ return [];
5
+ const out = [];
6
+ for (const entry of option.options) {
7
+ if ("options" in entry && Array.isArray(entry.options)) {
8
+ for (const choice of entry.options)
9
+ out.push({ ...choice, group: choice.group ?? entry.group ?? entry.name });
10
+ }
11
+ else {
12
+ out.push(entry);
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+ /** `["provider","model"]` → { provider, model };解析失败给 null(不猜) */
18
+ export function parseModelValue(value) {
19
+ if (!value)
20
+ return null;
21
+ try {
22
+ const parsed = JSON.parse(value);
23
+ if (!Array.isArray(parsed) || parsed.length < 2)
24
+ return null;
25
+ const [provider, model] = parsed;
26
+ if (typeof provider !== "string" || typeof model !== "string" || provider === "" || model === "")
27
+ return null;
28
+ return { provider, model };
29
+ }
30
+ catch {
31
+ // 老版本可能直接给 `provider/model`
32
+ const [provider, model] = value.split("/");
33
+ return provider && model ? { provider, model } : null;
34
+ }
35
+ }
36
+ /** 我们在界面上用的模型 id:`provider/model`(可读、可回填) */
37
+ export function modelIdOf(choice) {
38
+ const parsed = parseModelValue(choice.value);
39
+ return parsed ? `${parsed.provider}/${parsed.model}` : null;
40
+ }
41
+ export function modelOptions(config) {
42
+ const option = (config ?? []).find((entry) => entry.id === "model");
43
+ const current = parseModelValue(option?.currentValue);
44
+ const seen = new Set();
45
+ const models = [];
46
+ for (const choice of flattenChoices(option)) {
47
+ const id = modelIdOf(choice);
48
+ if (!id || seen.has(id))
49
+ continue;
50
+ seen.add(id);
51
+ models.push({
52
+ id,
53
+ label: choice.name ?? id,
54
+ ...((current && id === `${current.provider}/${current.model}`) ? { current: true } : {}),
55
+ });
56
+ }
57
+ return models;
58
+ }
59
+ export function currentModelId(config) {
60
+ const parsed = parseModelValue((config ?? []).find((entry) => entry.id === "model")?.currentValue);
61
+ return parsed ? `${parsed.provider}/${parsed.model}` : null;
62
+ }
63
+ /** `provider/model` → ACP 需要的原始 value;找不到返回 null */
64
+ export function modelValueFor(config, modelId) {
65
+ const option = (config ?? []).find((entry) => entry.id === "model");
66
+ for (const choice of flattenChoices(option)) {
67
+ if (modelIdOf(choice) === modelId)
68
+ return choice.value;
69
+ }
70
+ return null;
71
+ }
72
+ export function thinkingLevels(config) {
73
+ const option = (config ?? []).find((entry) => entry.id === "reasoning_effort");
74
+ return flattenChoices(option)
75
+ .map((choice) => choice.value)
76
+ .filter((value) => typeof value === "string" && value !== "");
77
+ }
78
+ export function currentThinkingLevel(config) {
79
+ return (config ?? []).find((entry) => entry.id === "reasoning_effort")?.currentValue ?? null;
80
+ }
81
+ /**
82
+ * 新会话默认用哪条模型路由。
83
+ *
84
+ * 为什么要挑:ACP 的默认路由是 `deepseek-official`,它要 `DEEPSEEK_API_KEY`;
85
+ * 只登录了 DeepSeek 账号的机器上第一次就报 "no API key"。所以优先选**账号路由**
86
+ * (`deepseek-account`),其次才回落到默认。顺序可被 `AGENT_WEB_DSH_MODEL` 覆盖。
87
+ */
88
+ export function pickDefaultModelValue(config, preferred) {
89
+ const option = (config ?? []).find((entry) => entry.id === "model");
90
+ const choices = flattenChoices(option);
91
+ if (choices.length === 0)
92
+ return null;
93
+ if (preferred) {
94
+ const hit = choices.find((choice) => modelIdOf(choice) === preferred) ?? choices.find((choice) => choice.value === preferred);
95
+ if (hit)
96
+ return hit.value;
97
+ }
98
+ const current = parseModelValue(option?.currentValue);
99
+ // DSH 自己的默认不是官方路由(说明它配了自定义 provider / 账号)→ 尊重它,不要覆盖
100
+ if (current && current.provider !== "deepseek-official")
101
+ return null;
102
+ // 官方路由要 DEEPSEEK_API_KEY,没配时第一次 prompt 就报 no API key:
103
+ // 优先用用户自定义的 provider(如 nexita-deepseek),其次账号路由。
104
+ const providerOf = (choice) => parseModelValue(choice.value)?.provider ?? "";
105
+ const custom = choices.find((choice) => !["deepseek-official", "deepseek-account"].includes(providerOf(choice)));
106
+ if (custom)
107
+ return custom.value;
108
+ const account = choices.find((choice) => providerOf(choice) === "deepseek-account");
109
+ return (account ?? choices[0]).value;
110
+ }
111
+ /** ACP stopReason → 我们的回合结束原因 */
112
+ export function stopReasonToTurnEnd(reason) {
113
+ if (reason === "cancelled")
114
+ return "interrupted";
115
+ if (reason === "refusal")
116
+ return "error";
117
+ return "done"; // end_turn / max_tokens / max_turn_requests:这轮确实结束了
118
+ }
119
+ function textOfContent(update) {
120
+ const content = update.content;
121
+ if (!content)
122
+ return "";
123
+ if (Array.isArray(content)) {
124
+ return content
125
+ .map((item) => (item?.content?.type === "text" ? (item.content.text ?? "") : ""))
126
+ .join("");
127
+ }
128
+ return content.type === "text" ? (content.text ?? "") : "";
129
+ }
130
+ /**
131
+ * `session/update` → EngineEvent。
132
+ * 注意:DSH 只给**已提交**的语义更新,所以这里每个 chunk 是一条完整 block 或一段文本,
133
+ * 不是逐 token 增量 —— 界面是按段落出现的,这是 ACP 的既定行为(不是丢帧)。
134
+ */
135
+ export function updateToEvent(update) {
136
+ switch (update.sessionUpdate) {
137
+ case "agent_message_chunk": {
138
+ const text = textOfContent(update);
139
+ return text === "" ? null : { type: "text_delta", text };
140
+ }
141
+ case "agent_thought_chunk": {
142
+ const text = textOfContent(update);
143
+ return text === "" ? null : { type: "thinking_delta", text };
144
+ }
145
+ case "tool_call": {
146
+ const raw = update.rawInput === undefined ? "" : JSON.stringify(update.rawInput);
147
+ return {
148
+ type: "tool_start",
149
+ toolCallId: update.toolCallId ?? "dsh-tool",
150
+ name: update.title ?? update.kind ?? "tool",
151
+ argsSummary: raw.length > 300 ? `${raw.slice(0, 300)}…` : raw,
152
+ // 摘要是截过的;完整那份留给「展开看全 + 复制」
153
+ argsFull: raw,
154
+ };
155
+ }
156
+ case "tool_call_update": {
157
+ const status = update.status === "failed" ? "failed" : "ok";
158
+ return {
159
+ type: "tool_end",
160
+ toolCallId: update.toolCallId ?? "dsh-tool",
161
+ status,
162
+ output: textOfContent(update),
163
+ };
164
+ }
165
+ default:
166
+ // usage_update 只有上下文占用(没有 input/output 分桶)、config_option_update 由引擎自己处理
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * 权限请求 → 我们的审批请求。
172
+ *
173
+ * DSH 的请求体只带 `toolCallId`(没有命令/参数),所以调用方应先用记住的
174
+ * `tool_call` 事实补全 `tool`(见 `rememberToolCall` 的用法)。
175
+ */
176
+ export function permissionApprovalRequest(request, tool) {
177
+ const title = tool?.title ?? request.toolCall?.title ?? "dsh 工具调用";
178
+ const args = tool?.rawInput === undefined ? request.toolCall?.rawInput : tool.rawInput;
179
+ const summary = args === undefined ? (request.toolCall?.toolCallId ?? "") : JSON.stringify(args).slice(0, 300);
180
+ return {
181
+ toolName: title,
182
+ argsSummary: summary,
183
+ scopeKey: `dsh|${title}`,
184
+ scopeHint: title,
185
+ riskNote: "DSH 请求执行这个操作;是否放行由你决定",
186
+ };
187
+ }
188
+ /** 我们的三态 → 权限选项 id(优先按 kind 匹配,其次按 optionId 猜) */
189
+ export function permissionOptionId(options, decision) {
190
+ const list = options ?? [];
191
+ const byKind = (kinds) => {
192
+ for (const kind of kinds) {
193
+ const hit = list.find((option) => option.kind === kind);
194
+ if (hit)
195
+ return hit.optionId;
196
+ }
197
+ return null;
198
+ };
199
+ const picked = decision === "deny"
200
+ ? (byKind(["reject_once", "reject_always"]) ??
201
+ list.find((option) => /reject|deny|cancel/i.test(option.optionId))?.optionId)
202
+ : (byKind(decision === "always" ? ["allow_always", "allow_once"] : ["allow_once", "allow_always"]) ??
203
+ list.find((option) => /allow|accept|approve/i.test(option.optionId))?.optionId);
204
+ return picked ?? list[0]?.optionId ?? (decision === "deny" ? "reject-once" : "allow-once");
205
+ }
206
+ /**
207
+ * DSH 的错误 → 人话。
208
+ * 最常见两条:会话被 DSH 自己的 web/tui 占着(写句柄),以及模型路由没凭据。
209
+ */
210
+ export function friendlyDshError(error) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ if (/active write handle|already owned/i.test(message)) {
213
+ return "这个会话正在 DSH 自己的界面(web / tui)里打开着:先在那里关掉它,或新建一条会话";
214
+ }
215
+ if (/no API key/i.test(message)) {
216
+ return "这条模型路由没有凭据:换一条(例如 deepseek-account)或在环境里配置 DEEPSEEK_API_KEY";
217
+ }
218
+ if (/not found|unknown session/i.test(message))
219
+ return "DSH 里找不到这个会话(可能已被删除)";
220
+ return message;
221
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * DSH 的「项目 / 会话清单」提供者(对齐 DSH 原生:项目 = workspace,会话 = session)。
3
+ *
4
+ * 数据来源(只读):
5
+ * - `<DSH_HOME>/storages/workspace.json` —— 原生 workspace:path / title / sessionIds,
6
+ * 以及全局的 pinned / archived 会话 id;
7
+ * - `<DSH_HOME>/storages/session_projcache/sessions/<id>.json` —— 会话投影缓存:
8
+ * 标题、最后提问时间、最近用的模型(**免解压日志**的快路径);
9
+ * - `<DSH_HOME>/sessions/<group>/<session-id>/session.v4.jsonl.zstd` —— 会话日志本身:
10
+ * 拿 header(cwd/createdAt)与 `session/title`,缓存里没有时兜底。
11
+ *
12
+ * 为什么要自己扫目录:ACP 的 `session/list` 只回 `{sessionId, cwd}`,**没有标题和时间**;
13
+ * 侧栏需要标题/时间/模型,所以直接读 DSH 自己的持久化(与 pi-web-ui 的做法一致,但我们
14
+ * 额外用了 workspace.json 与投影缓存,避免每个会话都解压日志)。
15
+ */
16
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { basename, join } from "node:path";
19
+ import { firstUserText, locateSessionFile, readSessionHeader, readSessionLog, sessionTitle } from "./dsh-log.js";
20
+ import { isScratchCwd } from "./scratch.js";
21
+ /** 读 workspace.json;没有/坏了都返回空表(侧栏退回按 cwd 分组) */
22
+ export function readWorkspaces(home) {
23
+ const file = join(home, "storages", "workspace.json");
24
+ if (!existsSync(file))
25
+ return {};
26
+ try {
27
+ return JSON.parse(readFileSync(file, "utf8"));
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ /** workspace.json → path → 项目名(title 优先,缺了用目录名) */
34
+ export function workspaceNames(workspaces) {
35
+ const out = new Map();
36
+ for (const row of Object.values(workspaces.tables?.workspaces ?? {})) {
37
+ const path = row.path?.trim();
38
+ if (!path)
39
+ continue;
40
+ out.set(path, row.title?.trim() || basename(path) || path);
41
+ }
42
+ return out;
43
+ }
44
+ /** projcache 一条会话的元数据(标题/时间/模型,免解压) */
45
+ function readProjCache(home, sessionId) {
46
+ const file = join(home, "storages", "session_projcache", "sessions", `${sessionId}.json`);
47
+ if (!existsSync(file))
48
+ return { title: null, updatedAt: null, cwd: null, startedAt: null, model: null };
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
51
+ const rows = parsed.record?.rows;
52
+ const title = rows?.title?.val;
53
+ const lastPromptAt = rows?.sessionListMetadata?.val?.lastPromptAt;
54
+ const lastUsed = rows?.modelSelection?.val?.lastUsed;
55
+ const model = typeof lastUsed?.provider === "string" && typeof lastUsed?.model === "string"
56
+ ? `${lastUsed.provider}/${lastUsed.model}`
57
+ : null;
58
+ return {
59
+ title: typeof title === "string" && title.trim() !== "" ? title.trim() : null,
60
+ updatedAt: typeof lastPromptAt === "number" ? lastPromptAt : null,
61
+ cwd: parsed.record?.identity?.cwd ?? null,
62
+ startedAt: typeof parsed.record?.identity?.createdAt === "number" ? parsed.record.identity.createdAt : null,
63
+ model,
64
+ };
65
+ }
66
+ catch {
67
+ return { title: null, updatedAt: null, cwd: null, startedAt: null, model: null };
68
+ }
69
+ }
70
+ /** 会话目录名就是会话 id(`session-<uuid>`) */
71
+ function sessionDirs(sessionsRoot) {
72
+ if (!existsSync(sessionsRoot))
73
+ return [];
74
+ const out = [];
75
+ let groups;
76
+ try {
77
+ groups = readdirSync(sessionsRoot, { withFileTypes: true });
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ for (const group of groups) {
83
+ if (!group.isDirectory())
84
+ continue;
85
+ const groupDir = join(sessionsRoot, group.name);
86
+ let entries;
87
+ try {
88
+ entries = readdirSync(groupDir, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory() || !entry.name.startsWith("session-"))
95
+ continue;
96
+ const file = locateSessionFile(sessionsRoot, entry.name, undefined);
97
+ if (!file)
98
+ continue;
99
+ let mtimeMs = 0;
100
+ try {
101
+ mtimeMs = statSync(file).mtimeMs;
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ out.push({ id: entry.name, file, mtimeMs });
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ /** 日志兜底读取(每个文件按 mtime 缓存,扫描侧栏时不反复解压) */
112
+ const logCache = new Map();
113
+ function logMeta(file, mtimeMs) {
114
+ const known = logCache.get(file);
115
+ if (known && known.mtimeMs === mtimeMs)
116
+ return known;
117
+ const header = readSessionHeader(file);
118
+ let title = null;
119
+ try {
120
+ const log = readSessionLog(file);
121
+ title = sessionTitle(log) ?? firstUserText(log);
122
+ }
123
+ catch {
124
+ title = null;
125
+ }
126
+ const entry = {
127
+ mtimeMs,
128
+ cwd: typeof header?.cwd === "string" ? header.cwd : "",
129
+ title,
130
+ createdAt: typeof header?.createdAt === "number" ? header.createdAt : 0,
131
+ };
132
+ logCache.set(file, entry);
133
+ return entry;
134
+ }
135
+ /** 会话 id(`session-<uuid>`)→ 统一成侧栏用的 id(等于目录名,ACP 也认这个) */
136
+ function summaryOf(home, session) {
137
+ const cache = readProjCache(home, session.id);
138
+ const fallback = cache.cwd && cache.title ? null : logMeta(session.file, session.mtimeMs);
139
+ const cwd = cache.cwd ?? fallback?.cwd ?? "";
140
+ if (cwd === "" || isScratchCwd(cwd))
141
+ return null;
142
+ return {
143
+ id: session.id,
144
+ cwd,
145
+ title: cache.title ?? fallback?.title ?? session.id,
146
+ startedAt: cache.startedAt ?? fallback?.createdAt ?? session.mtimeMs,
147
+ updatedAt: cache.updatedAt ?? session.mtimeMs,
148
+ model: cache.model,
149
+ };
150
+ }
151
+ /** 纯分组:会话 → 项目(workspace 的 path 精确匹配;否则按 cwd 单独成组) */
152
+ export function groupDshSessions(sessions, names, options = {}) {
153
+ const groups = new Map();
154
+ for (const session of sessions) {
155
+ if (options.archived?.has(session.id))
156
+ continue;
157
+ const key = session.cwd;
158
+ const group = groups.get(key) ??
159
+ {
160
+ cwd: session.cwd,
161
+ name: names.get(session.cwd) ?? basename(session.cwd) ?? session.cwd,
162
+ sessions: [],
163
+ };
164
+ group.sessions.push({
165
+ id: session.id,
166
+ engineSessionId: session.id,
167
+ title: session.title,
168
+ startedAt: session.startedAt,
169
+ updatedAt: session.updatedAt,
170
+ cwd: session.cwd,
171
+ model: session.model,
172
+ });
173
+ groups.set(key, group);
174
+ }
175
+ return [...groups.values()]
176
+ .map((group) => ({
177
+ cwd: group.cwd,
178
+ name: group.name,
179
+ sessions: [...group.sessions].sort((left, right) => right.updatedAt - left.updatedAt),
180
+ }))
181
+ .sort((left, right) => (right.sessions[0]?.updatedAt ?? 0) - (left.sessions[0]?.updatedAt ?? 0));
182
+ }
183
+ export function createDshSessionsProvider(options) {
184
+ const home = options.home ?? process.env.DSH_HOME ?? join(homedir(), ".dsh");
185
+ const cacheMs = options.cacheMs ?? 5000;
186
+ const now = options.now ?? Date.now;
187
+ let cached = null;
188
+ return async () => {
189
+ if (cached && now() - cached.at < cacheMs)
190
+ return cached.value;
191
+ try {
192
+ const workspaces = readWorkspaces(home);
193
+ const names = workspaceNames(workspaces);
194
+ const archived = new Set(workspaces.global?.archivedSessionIds ?? []);
195
+ const sessions = sessionDirs(join(home, "sessions"))
196
+ .map((session) => summaryOf(home, session))
197
+ .filter((session) => session !== null);
198
+ const value = groupDshSessions(sessions, names, { archived });
199
+ cached = { at: now(), value };
200
+ return value;
201
+ }
202
+ catch (error) {
203
+ options.log?.(`读取 dsh 会话清单失败:${error instanceof Error ? error.message : String(error)}`);
204
+ return cached?.value ?? [];
205
+ }
206
+ };
207
+ }