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.
Files changed (250) hide show
  1. package/README.md +86 -447
  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 -593
  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-B8-VzDf2.css +0 -1
  249. package/web/dist/assets/index-DeC2eiiq.js +0 -42
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,47 @@
1
+ const ALL_ON = {
2
+ approvals: true,
3
+ tools: true,
4
+ modelSwitch: true,
5
+ thinkingLevel: true,
6
+ resume: true,
7
+ };
8
+ export function createEngineRegistry(descriptors, fallbackName = "pi") {
9
+ const byName = new Map(descriptors.map((descriptor) => [descriptor.name, descriptor]));
10
+ const fallback = byName.get(fallbackName) ?? descriptors[0];
11
+ if (!fallback)
12
+ throw new Error("引擎注册表为空(至少要注册一个引擎)");
13
+ /** name → 实例(懒建);切换引擎复用它,避免每次 new 一个带状态的引擎 */
14
+ const instances = new Map();
15
+ const instantiate = (descriptor) => {
16
+ const known = instances.get(descriptor.name);
17
+ if (known)
18
+ return known;
19
+ const created = descriptor.create();
20
+ instances.set(descriptor.name, created);
21
+ return created;
22
+ };
23
+ return {
24
+ names: () => [...byName.keys()],
25
+ descriptors: () => descriptors.map((descriptor) => ({ ...descriptor })),
26
+ get(name) {
27
+ const found = byName.get(name);
28
+ return found ? { engine: instantiate(found), descriptor: found } : null;
29
+ },
30
+ resolve(name) {
31
+ const found = byName.get(name);
32
+ if (found)
33
+ return { engine: instantiate(found), descriptor: found, fallbackFrom: null };
34
+ return { engine: instantiate(fallback), descriptor: fallback, fallbackFrom: name };
35
+ },
36
+ capabilitiesOf: (name) => (byName.get(name) ?? fallback).capabilities,
37
+ };
38
+ }
39
+ /** 假引擎的能力:**没有审批**(它只是剧本,不需要拦),其余从简 */
40
+ export const FAKE_CAPABILITIES = {
41
+ approvals: true, // 假引擎仍会走审批剧本(approve 触发词),保留 true 便于测试
42
+ tools: true,
43
+ modelSwitch: true,
44
+ thinkingLevel: false,
45
+ resume: true,
46
+ };
47
+ export const PI_CAPABILITIES = ALL_ON;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 「一次性会话」判定:临时目录里跑出来的会话不进侧栏(codex / dsh 共用)。
3
+ *
4
+ * 两个引擎都会在 `/tmp` 里留下大量 `codex exec` / 测试跑出来的会话,
5
+ * 把它们塞进侧栏只是噪音。判据放在一处,两个引擎的过滤规则保持一致。
6
+ */
7
+ import { tmpdir } from "node:os";
8
+ export function isScratchCwd(cwd) {
9
+ if (cwd === "")
10
+ return true;
11
+ const scratch = [tmpdir(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"];
12
+ return scratch.some((prefix) => cwd === prefix || cwd.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`));
13
+ }
@@ -0,0 +1,9 @@
1
+ /** 教模型「下一步建议」的写法(ADR-0022):纯文本标记,不需要调用任何工具 */
2
+ export const SUGGESTION_GUIDANCE = [
3
+ "## Suggested next actions",
4
+ "When the user must choose what to do next, end the reply with 4 to 5 lines of:",
5
+ "[[action:suggest:<button text>]]",
6
+ "Clicking a button sends that text as the next user message.",
7
+ "Use [[action:suggest:<short label>,prompt=<text to send>]] when the label should differ from the sent text.",
8
+ "Put them on their own lines at the very end, and never mention this syntax in prose.",
9
+ ].join("\n");
@@ -0,0 +1,22 @@
1
+ export function createSwitchableEngine(first) {
2
+ let current = first;
3
+ const port = new Proxy({}, {
4
+ get(_target, property) {
5
+ const value = current[property];
6
+ return typeof value === "function" ? value.bind(current) : value;
7
+ },
8
+ has: (_target, property) => property in current,
9
+ ownKeys: () => Reflect.ownKeys(current),
10
+ getOwnPropertyDescriptor: (_target, property) => {
11
+ const descriptor = Reflect.getOwnPropertyDescriptor(current, property);
12
+ return descriptor ? { ...descriptor, configurable: true } : undefined;
13
+ },
14
+ });
15
+ return {
16
+ port,
17
+ use: (engine) => {
18
+ current = engine;
19
+ },
20
+ current: () => current,
21
+ };
22
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 引擎接线(index.ts 的 400 行护栏 + 一处装配)。
3
+ *
4
+ * 把三件事绑在一起,因为它们必须指向**同一批实例**:
5
+ * 1. 注册表(有哪些引擎、怎么造);
6
+ * 2. codex 的 app-server 客户端(引擎与会话清单共用一个子进程);
7
+ * 3. 目录(每个引擎的历史会话从哪来)。
8
+ *
9
+ * codex 客户端构造时不 spawn:机器上没装 codex 也能正常启动,只有在切到它时才报错。
10
+ */
11
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { createBuiltinEngineRegistry } from "./builtin.js";
14
+ import { createEngineCatalog } from "./catalog.js";
15
+ import { createCodexSessionsProvider } from "./codex-sessions.js";
16
+ import { createCodexClient } from "./codex-transport.js";
17
+ import { createDshClient } from "./dsh-transport.js";
18
+ import { createDshSessionsProvider } from "./dsh-sessions.js";
19
+ /**
20
+ * 读 `<patchDir>/default-model.json`(`{"provider":"…","model":"…"}` 或 `{"id":"provider/model"}`)。
21
+ *
22
+ * 为什么要有它:把自定义 provider 通过 `--patch` 带给 acp 之后,ACP 的 `session/new`
23
+ * 仍然按它自己的默认路由给 `currentValue`(实测 `agent-default-model` 不生效),
24
+ * 于是「新会话默认用哪个模型」必须由我们自己指定。
25
+ */
26
+ export function readDshDefaultModel(dir) {
27
+ const file = join(dir, "default-model.json");
28
+ if (!existsSync(file))
29
+ return null;
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
32
+ if (typeof parsed.id === "string" && parsed.id.trim() !== "")
33
+ return parsed.id.trim();
34
+ if (typeof parsed.provider === "string" && typeof parsed.model === "string") {
35
+ return `${parsed.provider}/${parsed.model}`;
36
+ }
37
+ return null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export function createEngineWiring(options) {
44
+ const codexClient = createCodexClient({
45
+ bin: options.codexBin,
46
+ home: options.codexHome ?? undefined,
47
+ log: (text) => options.onLog(text, { engine: "codex" }),
48
+ });
49
+ /**
50
+ * dsh 的 patch 叠加层:把「自定义 provider / 默认模型」带给 acp profile。
51
+ * 顺序 = 环境变量里显式给的 → 用户目录里的 `*.yml`(按文件名)。
52
+ */
53
+ const patchDir = options.dshPatchDir;
54
+ const patchFiles = [
55
+ ...(options.dshPatches ?? []),
56
+ ...(patchDir && existsSync(patchDir)
57
+ ? readdirSync(patchDir)
58
+ .filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
59
+ .sort()
60
+ .map((name) => join(patchDir, name))
61
+ : []),
62
+ ].filter((file) => existsSync(file));
63
+ if (patchFiles.length > 0)
64
+ options.onLog(`dsh:加载 ${patchFiles.length} 个 patch 叠加层`, { files: patchFiles });
65
+ // 自定义 provider 的「默认模型」:patch 里的 agent-default-model 不会被 ACP 带到
66
+ // session/new 的 configOptions(实测),所以这里用一个伴随文件显式指定。
67
+ const defaultModel = options.dshModel ?? (patchDir ? readDshDefaultModel(patchDir) : null);
68
+ const dshClient = createDshClient({
69
+ bin: options.dshBin,
70
+ home: options.dshHome ?? undefined,
71
+ patches: patchFiles,
72
+ log: (text) => options.onLog(text, { engine: "dsh" }),
73
+ });
74
+ const registry = createBuiltinEngineRegistry({
75
+ onNotice: options.onNotice,
76
+ onLog: (text) => options.onLog(text),
77
+ codex: { client: codexClient },
78
+ dsh: { client: dshClient, model: defaultModel },
79
+ });
80
+ const catalog = createEngineCatalog({
81
+ registry,
82
+ sessions: {
83
+ pi: options.piSessions,
84
+ // fake 是「剧本回放」引擎:历史沿用 pi 的存储(本地演练 / 冒烟时侧栏不至于全空)
85
+ fake: options.piSessions,
86
+ codex: createCodexSessionsProvider({ client: codexClient, log: (text) => options.onLog(text) }),
87
+ // dsh 的历史会话清单:读 ~/.dsh 的 workspace.json + 会话投影缓存(只读)
88
+ dsh: createDshSessionsProvider({ home: options.dshHome ?? undefined, log: (text) => options.onLog(text) }),
89
+ },
90
+ });
91
+ const resolved = registry.resolve(options.engineName);
92
+ if (resolved.fallbackFrom) {
93
+ options.onLog(`AGENT_WEB_ENGINE 未注册,回落到 pi(可选:${registry.names().join(" / ")})`, {
94
+ requested: resolved.fallbackFrom,
95
+ });
96
+ }
97
+ return {
98
+ registry,
99
+ catalog,
100
+ resolved,
101
+ // 收掉外部引擎的子进程;pi 是进程内 SDK,不需要额外收
102
+ close: () => {
103
+ codexClient.close();
104
+ dshClient.close();
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 目录浏览(「新建项目」用):**只读列目录**,不创建、不删除任何东西。
3
+ *
4
+ * 与文件面板的区别:这里要能走出当前工作区(新建项目就是要在别处选目录),
5
+ * 所以只做「绝对路径 + 存在 + 是目录」的校验,且只返回**目录名**(不返回文件、不返回内容)。
6
+ */
7
+ import { realpathSync, statSync } from "node:fs";
8
+ import { readdir } from "node:fs/promises";
9
+ import { dirname, join } from "node:path";
10
+ export function registerDirRoutes(app, options) {
11
+ app.get("/api/fs/dirs", async (request, response) => {
12
+ const raw = typeof request.query.path === "string" && request.query.path.startsWith("/")
13
+ ? request.query.path
14
+ : (options.workspaceOf() ?? process.env.HOME ?? "/");
15
+ let real;
16
+ try {
17
+ real = realpathSync(raw);
18
+ if (!statSync(real).isDirectory())
19
+ throw new Error("not a directory");
20
+ }
21
+ catch {
22
+ response.status(400).json({ error: { code: "fs.bad_path", message: "不是可用的目录" } });
23
+ return;
24
+ }
25
+ let entries = [];
26
+ try {
27
+ const items = await readdir(real, { withFileTypes: true });
28
+ entries = items
29
+ .filter((item) => item.isDirectory() && !item.name.startsWith(".") && item.name !== "node_modules")
30
+ .map((item) => ({ name: item.name, path: join(real, item.name) }))
31
+ .sort((a, b) => a.name.localeCompare(b.name))
32
+ .slice(0, 500);
33
+ }
34
+ catch {
35
+ response.status(403).json({ error: { code: "fs.unreadable", message: "这个目录读不了(权限)" } });
36
+ return;
37
+ }
38
+ response.json({ path: real, parent: real === "/" ? null : dirname(real), entries });
39
+ });
40
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 文件名搜索(文件管理标题栏的放大镜)。
3
+ *
4
+ * 只按**文件名**匹配(不做内容搜索:那是 grep 的活,也不该把大仓库全读一遍)。
5
+ * 约束:只在工作区根内、跳过 node_modules/.git 等重目录、深度与结果数都有上限 ——
6
+ * 单用户工作台里「点了没反应」比「慢一点」更难受,所以宁可少返回也要快。
7
+ */
8
+ import { readdir } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { IGNORED_ENTRIES } from "./file-service.js";
11
+ export const SEARCH_MAX_RESULTS = 200;
12
+ export const SEARCH_MAX_DEPTH = 8;
13
+ /** 纯函数:名字是否命中(大小写不敏感的包含匹配) */
14
+ export function matchesQuery(name, query) {
15
+ return name.toLowerCase().includes(query.toLowerCase());
16
+ }
17
+ export async function searchFiles(root, query, options = {}) {
18
+ const needle = query.trim();
19
+ if (needle === "")
20
+ return { ok: true, entries: [], truncated: false };
21
+ const maxResults = options.maxResults ?? SEARCH_MAX_RESULTS;
22
+ const maxDepth = options.maxDepth ?? SEARCH_MAX_DEPTH;
23
+ const found = [];
24
+ let truncated = false;
25
+ /** 广度优先一层层扫:先命中的是浅层文件,视觉上更符合预期 */
26
+ const walk = async (dir, relPath, depth) => {
27
+ if (depth > maxDepth || found.length >= maxResults) {
28
+ truncated ||= found.length >= maxResults;
29
+ return;
30
+ }
31
+ let items;
32
+ try {
33
+ items = await readdir(dir, { withFileTypes: true });
34
+ }
35
+ catch {
36
+ return; // 没权限 / 刚被删:跳过
37
+ }
38
+ const dirs = [];
39
+ for (const item of items) {
40
+ if (IGNORED_ENTRIES.has(item.name))
41
+ continue;
42
+ if (found.length >= maxResults) {
43
+ truncated = true;
44
+ return;
45
+ }
46
+ const childPath = join(dir, item.name);
47
+ const childRel = join(relPath, item.name);
48
+ if (item.isDirectory())
49
+ dirs.push({ path: childPath, rel: childRel });
50
+ if (matchesQuery(item.name, needle))
51
+ found.push({ name: item.name, path: childRel, isDir: item.isDirectory() });
52
+ }
53
+ for (const dir2 of dirs)
54
+ await walk(dir2.path, dir2.rel, depth + 1);
55
+ };
56
+ try {
57
+ await walk(root, "", 0);
58
+ return { ok: true, entries: found, truncated };
59
+ }
60
+ catch (error) {
61
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
62
+ }
63
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * 文件浏览与只读预览(M4)——服务端。
3
+ *
4
+ * 三条硬约束:
5
+ * 1. **只能看当前项目内**(ADR-0006):所有路径 `realpath` 后必须落在工作区根内;
6
+ * 2. **懒加载**:列目录只列一层(91 个会话那种大目录也不卡);
7
+ * 3. **有上限**:预览只读前 N 字节,二进制给「不可预览」而不是塞满界面。
8
+ */
9
+ import { realpathSync } from "node:fs";
10
+ import { open, readFile, readdir, stat } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { resolveInsideRoot } from "../util/paths.js";
13
+ export const MAX_PREVIEW_BYTES = 512 * 1024;
14
+ /** 默认不列的目录/文件(与参考实现的 IGNORED_ENTRIES 同口径:依赖与构建产物不该占满文件树) */
15
+ export const IGNORED_ENTRIES = new Set(["node_modules", ".git", "dist", "web/dist", "coverage", ".next", "target"]);
16
+ /** 目录里最多列多少项(防止某个目录把界面撑爆) */
17
+ export const MAX_DIR_ENTRIES = 500;
18
+ /** 纯函数:按「目录优先、同类按名字」排序(与常见文件树一致) */
19
+ export function sortEntries(entries) {
20
+ return [...entries].sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1));
21
+ }
22
+ /** 纯函数:看起来像文本吗(NUL 字节 = 二进制;常见图片扩展名直接判二进制) */
23
+ export function looksBinary(bytes, name) {
24
+ if (/\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|tar|exe|dylib|so|woff2?|ttf|mp4|mp3|wav)$/i.test(name))
25
+ return true;
26
+ const sample = bytes.subarray(0, 4096);
27
+ for (const byte of sample)
28
+ if (byte === 0)
29
+ return true;
30
+ return false;
31
+ }
32
+ /**
33
+ * 列一层目录。
34
+ *
35
+ * `showHidden` 控制**点开头的隐藏文件**(.env / .github 之类):默认只放行 .gitignore 与 .env.example
36
+ * (它们是项目说明的一部分),需要时由界面的「眼睛」按钮显式打开。
37
+ * `node_modules` / `dist` 这类永远不列(ADR:依赖与构建产物不该占满文件树)。
38
+ */
39
+ export async function listDirectory(root, relPath, options = {}) {
40
+ const target = relPath === "" ? root : join(root, relPath);
41
+ const safe = resolveInsideRoot(root, target, realpathSync);
42
+ if (!safe.ok)
43
+ return { ok: false, error: safe.reason };
44
+ try {
45
+ const items = await readdir(safe.path, { withFileTypes: true });
46
+ const entries = items
47
+ .filter((item) => !IGNORED_ENTRIES.has(item.name))
48
+ .filter((item) => options.showHidden === true ||
49
+ !item.name.startsWith(".") ||
50
+ item.name === ".gitignore" ||
51
+ item.name === ".env.example")
52
+ .map((item) => ({ name: item.name, path: join(relPath, item.name), isDir: item.isDirectory() }));
53
+ return {
54
+ ok: true,
55
+ root,
56
+ path: relPath,
57
+ entries: sortEntries(entries).slice(0, MAX_DIR_ENTRIES),
58
+ truncated: entries.length > MAX_DIR_ENTRIES,
59
+ };
60
+ }
61
+ catch (error) {
62
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
63
+ }
64
+ }
65
+ export async function readPreview(root, relPath, limit = MAX_PREVIEW_BYTES) {
66
+ const safe = resolveInsideRoot(root, join(root, relPath), realpathSync);
67
+ if (!safe.ok)
68
+ return { ok: false, error: safe.reason };
69
+ try {
70
+ const info = await stat(safe.path);
71
+ if (info.isDirectory())
72
+ return { ok: false, error: "这是一个目录" };
73
+ // 只读前 limit 字节:以前是 readFile 全读进来再截断(工作区里一个几 GB 的日志
74
+ // 就能把进程内存打爆),512KB 的上限等于没设。
75
+ const size = info.size;
76
+ const take = Math.min(size, limit);
77
+ const handle = await open(safe.path, "r");
78
+ let bytes;
79
+ try {
80
+ bytes = Buffer.alloc(take);
81
+ if (take > 0)
82
+ await handle.read(bytes, 0, take, 0);
83
+ }
84
+ finally {
85
+ await handle.close();
86
+ }
87
+ const binary = looksBinary(bytes, safe.path);
88
+ return {
89
+ ok: true,
90
+ path: relPath,
91
+ text: binary ? "" : bytes.toString("utf8"),
92
+ bytes: size,
93
+ truncated: size > limit,
94
+ binary,
95
+ };
96
+ }
97
+ catch (error) {
98
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
99
+ }
100
+ }
101
+ /** 允许直接当图片/文档发给浏览器的扩展名(其余一律拒绝,避免变成任意文件下载器) */
102
+ const RAW_MIME = {
103
+ png: "image/png",
104
+ jpg: "image/jpeg",
105
+ jpeg: "image/jpeg",
106
+ gif: "image/gif",
107
+ webp: "image/webp",
108
+ avif: "image/avif",
109
+ svg: "image/svg+xml",
110
+ ico: "image/x-icon",
111
+ bmp: "image/bmp",
112
+ pdf: "application/pdf",
113
+ };
114
+ /** 图片原始大小上限(超过就拒绝,别让浏览器卡死) */
115
+ export const MAX_RAW_BYTES = 20 * 1024 * 1024;
116
+ /** 纯函数:按扩展名给 Content-Type(不在白名单里返回空串 = 拒绝) */
117
+ export function mimeOf(path) {
118
+ const extension = path.split(".").pop()?.toLowerCase() ?? "";
119
+ return RAW_MIME[extension] ?? "";
120
+ }
121
+ /** 读取图片原始字节(markdown 里 \`![](docs/shot.png)\` 能直接显示的关键一环) */
122
+ export async function readRaw(root, relPath) {
123
+ const mime = mimeOf(relPath);
124
+ if (mime === "")
125
+ return { ok: false, error: `不支持的文件类型:${relPath.split(".").pop() ?? "无扩展名"}` };
126
+ // 绝对路径(agent 把截图写到 /tmp/x.png 这种):只做「存在 + 是文件 + 类型白名单」,
127
+ // 不做工作区包含 —— 否则模型刚生成的图在对话里就看不见。相对路径仍限制在工作区内。
128
+ let safe;
129
+ try {
130
+ safe = relPath.startsWith("/")
131
+ ? { ok: true, path: realpathSync(relPath) }
132
+ : resolveInsideRoot(root, join(root, relPath), realpathSync);
133
+ }
134
+ catch (error) {
135
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
136
+ }
137
+ if (!safe.ok)
138
+ return { ok: false, error: safe.reason };
139
+ try {
140
+ const info = await stat(safe.path);
141
+ if (info.isDirectory())
142
+ return { ok: false, error: "这是一个目录" };
143
+ if (info.size > MAX_RAW_BYTES)
144
+ return { ok: false, error: "文件太大(上限 20MB)", tooLarge: true };
145
+ return { ok: true, bytes: await readFile(safe.path), mime };
146
+ }
147
+ catch (error) {
148
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
149
+ }
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // 视频 / 音频:走「流 + Range」,不整块读进内存(大文件也能拖动进度条)
153
+ // ---------------------------------------------------------------------------
154
+ /** 允许内联播放的媒体类型(白名单之外的扩展名一律拒绝) */
155
+ const MEDIA_MIME = {
156
+ mp4: "video/mp4",
157
+ m4v: "video/mp4",
158
+ webm: "video/webm",
159
+ ogv: "video/ogg",
160
+ mov: "video/quicktime",
161
+ mkv: "video/x-matroska",
162
+ mp3: "audio/mpeg",
163
+ m4a: "audio/mp4",
164
+ aac: "audio/aac",
165
+ wav: "audio/wav",
166
+ ogg: "audio/ogg",
167
+ opus: "audio/opus",
168
+ flac: "audio/flac",
169
+ };
170
+ /** 纯函数:媒体扩展名 → Content-Type(不是媒体返回空串) */
171
+ export function mediaMimeOf(path) {
172
+ const extension = path.split(".").pop()?.toLowerCase() ?? "";
173
+ return MEDIA_MIME[extension] ?? "";
174
+ }
175
+ /**
176
+ * 解析一个可播放的媒体文件:校验路径(与 readRaw 同规矩)、类型白名单、大小上限。
177
+ * 只 stat,不读内容 —— 真正的字节由路由用 createReadStream + Range 流出去。
178
+ */
179
+ export async function resolveMediaFile(root, relPath) {
180
+ const mime = mediaMimeOf(relPath);
181
+ if (mime === "")
182
+ return { ok: false, error: `不支持的媒体类型:${relPath.split(".").pop() ?? "无扩展名"}` };
183
+ let safePath;
184
+ try {
185
+ safePath = relPath.startsWith("/") ? realpathSync(relPath) : "";
186
+ if (safePath === "") {
187
+ const safe = resolveInsideRoot(root, join(root, relPath), realpathSync);
188
+ if (!safe.ok)
189
+ return { ok: false, error: safe.reason };
190
+ safePath = safe.path;
191
+ }
192
+ }
193
+ catch (error) {
194
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
195
+ }
196
+ try {
197
+ const info = await stat(safePath);
198
+ if (info.isDirectory())
199
+ return { ok: false, error: "这是一个目录" };
200
+ if (info.size > MAX_MEDIA_BYTES)
201
+ return { ok: false, error: "文件太大(上限 4GB)", tooLarge: true };
202
+ return { ok: true, path: safePath, size: info.size, mime };
203
+ }
204
+ catch (error) {
205
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
206
+ }
207
+ }
208
+ /** 媒体文件大小上限:4GB(流式发送,不会占内存;只是别把「伪装的巨型文件」当视频播) */
209
+ export const MAX_MEDIA_BYTES = 4 * 1024 * 1024 * 1024;
210
+ /** 纯函数:解析 Range 头(只认单段 `bytes=a-b`),返回闭区间;无效返回 null(按整文件发) */
211
+ export function parseRange(header, size) {
212
+ if (!header)
213
+ return null;
214
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
215
+ if (!match)
216
+ return null;
217
+ const [, rawStart, rawEnd] = match;
218
+ if (rawStart === "" && rawEnd === "")
219
+ return null;
220
+ if (rawStart === "") {
221
+ // 后缀写法:bytes=-N → 最后 N 字节
222
+ const length = Number(rawEnd);
223
+ if (!Number.isFinite(length) || length <= 0)
224
+ return null;
225
+ return { start: Math.max(0, size - length), end: size - 1 };
226
+ }
227
+ const start = Number(rawStart);
228
+ const end = rawEnd === "" ? size - 1 : Number(rawEnd);
229
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
230
+ return null;
231
+ return { start: Math.min(start, size - 1), end: Math.min(end, size - 1) };
232
+ }