@youweichen/pi-harness 0.0.0-stage → 1.0.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 (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/pi-harness.mjs +1819 -0
  4. package/deploy/com.youweichen.pi-harness.plist +48 -0
  5. package/deploy/nginx-subpath.conf +88 -0
  6. package/deploy/pi-harness-task.xml +71 -0
  7. package/deploy/pi-harness.service +31 -0
  8. package/dist/server/agent-service.js +3051 -0
  9. package/dist/server/app-version.js +19 -0
  10. package/dist/server/attachments.js +526 -0
  11. package/dist/server/auth-file.js +61 -0
  12. package/dist/server/bg-servers.js +154 -0
  13. package/dist/server/client-eviction.js +19 -0
  14. package/dist/server/client-id.js +9 -0
  15. package/dist/server/client-state.js +202 -0
  16. package/dist/server/codemode-image-routes.js +36 -0
  17. package/dist/server/codemode-presentation.js +19 -0
  18. package/dist/server/component-updates.js +135 -0
  19. package/dist/server/context-breakdown.js +56 -0
  20. package/dist/server/control-socket.js +177 -0
  21. package/dist/server/conversation.js +65 -0
  22. package/dist/server/dir-browser.js +77 -0
  23. package/dist/server/editor-snapshot.js +38 -0
  24. package/dist/server/ensure-bash.js +80 -0
  25. package/dist/server/extension-display.js +23 -0
  26. package/dist/server/extensions-catalog.js +64 -0
  27. package/dist/server/extensions-model.js +46 -0
  28. package/dist/server/extensions-routes.js +255 -0
  29. package/dist/server/extensions-worker.js +187 -0
  30. package/dist/server/files-service.js +795 -0
  31. package/dist/server/index.js +1029 -0
  32. package/dist/server/markdown-images.js +42 -0
  33. package/dist/server/model-admin.js +801 -0
  34. package/dist/server/model-config-merge.js +32 -0
  35. package/dist/server/native-mcp-config.js +103 -0
  36. package/dist/server/native-mcp-presentation.js +12 -0
  37. package/dist/server/native-prompt-template.js +12 -0
  38. package/dist/server/native-tools.js +32 -0
  39. package/dist/server/node-sources.js +142 -0
  40. package/dist/server/node-workbench.js +668 -0
  41. package/dist/server/origin-policy.js +50 -0
  42. package/dist/server/patch-node-pty.js +96 -0
  43. package/dist/server/pi-environment.js +203 -0
  44. package/dist/server/plan/extension.js +74 -0
  45. package/dist/server/plan/progress.js +90 -0
  46. package/dist/server/plan/settings.js +41 -0
  47. package/dist/server/plan/state.js +113 -0
  48. package/dist/server/plugin-facilities.js +304 -0
  49. package/dist/server/plugin-updater.js +245 -0
  50. package/dist/server/plugins.js +712 -0
  51. package/dist/server/private-file.js +28 -0
  52. package/dist/server/process-utils.js +112 -0
  53. package/dist/server/prompt-delivery.js +80 -0
  54. package/dist/server/protocol-version.js +11 -0
  55. package/dist/server/protocol.js +6 -0
  56. package/dist/server/provider-auth.js +99 -0
  57. package/dist/server/query-cache.js +45 -0
  58. package/dist/server/recovery-state.js +37 -0
  59. package/dist/server/scm.js +459 -0
  60. package/dist/server/security-headers.js +24 -0
  61. package/dist/server/serialize.js +254 -0
  62. package/dist/server/session-file-read.js +225 -0
  63. package/dist/server/session-record-worker.js +21 -0
  64. package/dist/server/session-reload.js +13 -0
  65. package/dist/server/session-tree-controller.js +250 -0
  66. package/dist/server/session-tree.js +191 -0
  67. package/dist/server/settings-service.js +53 -0
  68. package/dist/server/skills-service.js +87 -0
  69. package/dist/server/slash-commands.js +280 -0
  70. package/dist/server/sqlite-preview.js +58 -0
  71. package/dist/server/sqlite-query.js +67 -0
  72. package/dist/server/sqlite-worker.js +12 -0
  73. package/dist/server/system-prompt-files.js +158 -0
  74. package/dist/server/system-prompt-routes.js +40 -0
  75. package/dist/server/system-prompt-view.js +49 -0
  76. package/dist/server/task-progress.js +233 -0
  77. package/dist/server/terminals.js +833 -0
  78. package/dist/server/text-sniff.js +269 -0
  79. package/dist/server/thinking-timing.js +91 -0
  80. package/dist/server/token-auth.js +37 -0
  81. package/dist/server/tool-output.js +97 -0
  82. package/dist/server/tool-text-incidents.js +82 -0
  83. package/dist/server/tool-text.js +67 -0
  84. package/dist/server/uploads.js +112 -0
  85. package/dist/server/user-attachments.js +96 -0
  86. package/dist/server/webui-context.js +257 -0
  87. package/dist/server/wiki-links.js +136 -0
  88. package/dist/server/wiki-pdf-worker.js +23 -0
  89. package/dist/server/wiki-pdf.js +36 -0
  90. package/dist/server/wiki-routes.js +177 -0
  91. package/dist/server/wiki-service.js +511 -0
  92. package/electron/main.mjs +607 -0
  93. package/extensions/webui.ts +192 -0
  94. package/package.json +122 -4
  95. package/web/dist/apple-touch-icon.png +0 -0
  96. package/web/dist/assets/NodeWorkbench-C4U1PldE.js +3 -0
  97. package/web/dist/assets/TerminalPanel-CWh6vw_N.js +2 -0
  98. package/web/dist/assets/abnfDiagram-VCTEODGH-D5Ti0Kpa.js +1 -0
  99. package/web/dist/assets/arc-DKJilLd6.js +1 -0
  100. package/web/dist/assets/architectureDiagram-5GKGNRK7-Cf0w38g-.js +36 -0
  101. package/web/dist/assets/blockDiagram-I7D4REHJ-BOTLWLIL.js +129 -0
  102. package/web/dist/assets/c4Diagram-7LVT6UL2-CLCCoJs3.js +38 -0
  103. package/web/dist/assets/channel-D8Hd7T4P.js +1 -0
  104. package/web/dist/assets/chunk-2Q5K7J3B-BHjga2Ge.js +1 -0
  105. package/web/dist/assets/chunk-5VM5RSS4-CQJ5VfDZ.js +15 -0
  106. package/web/dist/assets/chunk-F27PBJKO-C67qoPJH.js +1 -0
  107. package/web/dist/assets/chunk-IMKFNOWR-CgXXGwI8.js +231 -0
  108. package/web/dist/assets/chunk-JWPE2WC7-D0RPyByP.js +1 -0
  109. package/web/dist/assets/chunk-POPQ4Y6H-BeYC9s9x.js +1 -0
  110. package/web/dist/assets/chunk-SVP7TREG-eQPl4UBQ.js +88 -0
  111. package/web/dist/assets/chunk-TICWLB2K-DT73arPU.js +206 -0
  112. package/web/dist/assets/chunk-XXDRQBXY-B4FwQcP-.js +1 -0
  113. package/web/dist/assets/classDiagram-ZZMXUADV-B_btgYRB.js +1 -0
  114. package/web/dist/assets/classDiagram-v2-VYDZK3BY-B_btgYRB.js +1 -0
  115. package/web/dist/assets/cose-bilkent-JH36ORCC-Bnn5nlQN.js +1 -0
  116. package/web/dist/assets/cynefin-OW5HDTMX-CStWX9kE.js +178 -0
  117. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-BMVHC03f.js +62 -0
  118. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
  119. package/web/dist/assets/dagre-GXQ25YYZ-ClSaO4ml.js +4 -0
  120. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  121. package/web/dist/assets/diagram-S7CK7UJ4-C43LL08C.js +30 -0
  122. package/web/dist/assets/diagram-UQ7AKVKN-BPVn_ySW.js +41 -0
  123. package/web/dist/assets/diagram-VSXAHHWV-C34FFhRi.js +3 -0
  124. package/web/dist/assets/diagram-VX7I27RA-BRk4bncf.js +24 -0
  125. package/web/dist/assets/diagram-Z3DM3KII-O5hYPLEd.js +24 -0
  126. package/web/dist/assets/ebnfDiagram-PWID7BFC-DVGEBm99.js +1 -0
  127. package/web/dist/assets/erDiagram-RLTQ6QDP-M-BLpTEV.js +99 -0
  128. package/web/dist/assets/flowDiagram-HODETNUW-BwdLopEx.js +168 -0
  129. package/web/dist/assets/ganttDiagram-EL5Y4UJY-DYxx2ppA.js +292 -0
  130. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-DUU0wMYB.js +106 -0
  131. package/web/dist/assets/index-CI2KfVSr.css +10 -0
  132. package/web/dist/assets/index-DQcpggFU.js +275 -0
  133. package/web/dist/assets/infoDiagram-27XIBGKW-CFaW5C0C.js +2 -0
  134. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  135. package/web/dist/assets/ishikawaDiagram-5VMMS53U--ENP1MTB.js +70 -0
  136. package/web/dist/assets/journeyDiagram-3NMN7TZE-DgnAzSTj.js +139 -0
  137. package/web/dist/assets/kanban-definition-UXKFOSKX-_STZN1vU.js +89 -0
  138. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  139. package/web/dist/assets/layout-CkUClkXt.js +1 -0
  140. package/web/dist/assets/linear-sYHiUOxa.js +1 -0
  141. package/web/dist/assets/markdown-vNTCZJ6d.js +51 -0
  142. package/web/dist/assets/mermaid.core-B8fKGWsc.js +312 -0
  143. package/web/dist/assets/mindmap-definition-YA3MSWOX-DP0BYOEx.js +96 -0
  144. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  145. package/web/dist/assets/pegDiagram-XKGWAZYB-DkWTmZre.js +1 -0
  146. package/web/dist/assets/pieDiagram-E7YTZNPT-DfZgMV4u.js +39 -0
  147. package/web/dist/assets/quadrantDiagram-AXDQQJYC-Djl4TT7q.js +7 -0
  148. package/web/dist/assets/railroadDiagram-O6MQD6OU-DbKF6zXn.js +1 -0
  149. package/web/dist/assets/react-BNj3n7jX.js +24 -0
  150. package/web/dist/assets/requirementDiagram-BXWQKSXE-idola5qb.js +84 -0
  151. package/web/dist/assets/sankeyDiagram-P5KCCOFB-KA0hQOle.js +40 -0
  152. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BXSKvxzP.js +162 -0
  153. package/web/dist/assets/sizeCapture-INFHLROL-C_Ewv3S5.js +1 -0
  154. package/web/dist/assets/stateDiagram-D77RDMKH-DIf5X8oo.js +1 -0
  155. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DYwe_2mB.js +1 -0
  156. package/web/dist/assets/swimlanes-42K2YHIH-Cx_Rc8M2.js +1 -0
  157. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-52Y8tgRV.js +8 -0
  158. package/web/dist/assets/timeline-definition-24CTP7MA-BRrDhWVz.js +120 -0
  159. package/web/dist/assets/vennDiagram-4TSXK5OY-Ad6G0YwD.js +34 -0
  160. package/web/dist/assets/wardleyDiagram-VM6X3IG4-X6-ENpSi.js +78 -0
  161. package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
  162. package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
  163. package/web/dist/assets/xychartDiagram-S5SC5T6Z-DhegNS6d.js +7 -0
  164. package/web/dist/brand-mark.svg +5 -0
  165. package/web/dist/favicon-32.png +0 -0
  166. package/web/dist/favicon.svg +5 -0
  167. package/web/dist/fonts/WIKI-FONTS.md +5 -0
  168. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  169. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  170. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  171. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  172. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  173. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  174. package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
  175. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  176. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  177. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  178. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  179. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  180. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  181. package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  182. package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  183. package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  184. package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  185. package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  186. package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  187. package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  188. package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  189. package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  190. package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  191. package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
  192. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  193. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  194. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  195. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  196. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  197. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  198. package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
  199. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  200. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  201. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  202. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  203. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  204. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  205. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  206. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  207. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  208. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  209. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  210. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  211. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  212. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  213. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  214. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  215. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  216. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  217. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  218. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  219. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  220. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  221. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  222. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  223. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  224. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  225. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  226. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  227. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  228. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  229. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  230. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  231. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  232. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  233. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  234. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  235. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  236. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  237. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  238. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  239. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  240. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  241. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  242. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  243. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  244. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  245. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  246. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  247. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  248. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  249. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  250. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  251. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  252. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  253. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  254. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  255. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  256. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  257. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  258. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  259. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  260. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  261. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  262. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  263. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  264. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  265. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  266. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  267. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  268. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  269. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  270. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  271. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  272. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  273. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  274. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  275. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  276. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  277. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  278. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  279. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  280. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  281. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  282. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  283. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  284. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  285. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  286. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  287. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  288. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  289. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  290. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  291. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  292. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  293. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  294. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  295. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  296. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  297. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  298. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  299. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  300. package/web/dist/fonts/source-serif-4/LICENSE +93 -0
  301. package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  302. package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  303. package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  304. package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  305. package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  306. package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  307. package/web/dist/icon/1a-mark.svg +1 -0
  308. package/web/dist/icon.ico +0 -0
  309. package/web/dist/index.html +18 -0
  310. package/web/public/apple-touch-icon.png +0 -0
  311. package/web/public/brand-mark.svg +5 -0
  312. package/web/public/favicon-32.png +0 -0
  313. package/web/public/favicon.svg +5 -0
  314. package/web/public/fonts/WIKI-FONTS.md +5 -0
  315. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  316. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  317. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  318. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  319. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  320. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  321. package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
  322. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  323. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  324. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  325. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  326. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  327. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  328. package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  329. package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  330. package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  331. package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  332. package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  333. package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  334. package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  335. package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  336. package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  337. package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  338. package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
  339. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  340. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  341. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  342. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  343. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  344. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  345. package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
  346. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  347. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  348. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  349. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  350. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  351. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  352. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  353. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  354. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  355. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  356. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  357. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  358. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  359. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  360. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  361. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  362. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  363. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  364. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  365. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  366. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  367. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  368. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  369. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  370. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  371. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  372. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  373. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  374. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  375. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  376. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  377. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  378. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  379. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  380. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  381. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  382. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  383. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  384. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  385. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  386. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  387. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  388. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  389. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  390. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  391. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  392. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  393. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  394. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  395. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  396. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  397. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  398. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  399. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  400. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  401. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  402. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  403. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  404. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  405. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  406. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  407. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  408. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  409. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  410. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  411. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  412. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  413. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  414. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  415. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  416. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  417. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  418. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  419. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  420. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  421. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  422. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  423. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  424. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  425. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  426. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  427. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  428. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  429. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  430. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  431. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  432. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  433. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  434. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  435. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  436. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  437. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  438. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  439. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  440. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  441. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  442. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  443. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  444. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  445. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  446. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  447. package/web/public/fonts/source-serif-4/LICENSE +93 -0
  448. package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  449. package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  450. package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  451. package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  452. package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  453. package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  454. package/web/public/icon/1a-mark.svg +1 -0
  455. package/web/public/icon.ico +0 -0
@@ -0,0 +1,1029 @@
1
+ import { installSystemPromptRoutes } from "./system-prompt-routes.js";
2
+ import { installExtensionsRoutes } from "./extensions-routes.js";
3
+ import { installCodemodeImageRoutes } from "./codemode-image-routes.js";
4
+ import { installWikiRoutes } from "./wiki-routes.js";
5
+ /**
6
+ * pi-harness server entry.
7
+ *
8
+ * - Serves the built frontend (web/dist) in production; in dev, Vite serves it
9
+ * on :5173 and proxies /ws to this server.
10
+ * - Exposes /api/health and a WebSocket endpoint at /ws carrying the chat
11
+ * protocol defined in protocol.ts.
12
+ *
13
+ * Env:
14
+ * PORT HTTP port (default 8787)
15
+ * PI_WEB_CWD workspace the agent operates in (default: process.cwd())
16
+ * PI_WEB_DATA_DIR where per-client UI state is stored (client-state.json,
17
+ * default: <home>/.pi-web). Chat sessions are NOT stored here — they live
18
+ * in the pi agent's global TUI session dir (~/.pi/agent/sessions/--<cwd>--/)
19
+ * via the SDK default, so this web UI, the dev instance, and the pi CLI/TUI
20
+ * all share one conversation list per project.
21
+ * PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
22
+ */
23
+ import { existsSync } from "node:fs";
24
+ import { stat } from "node:fs/promises";
25
+ import { createServer } from "node:http";
26
+ import { createConnection } from "node:net";
27
+ import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
28
+ import { homedir } from "node:os";
29
+ import { fileURLToPath } from "node:url";
30
+ import { randomUUID } from "node:crypto";
31
+ import express from "express";
32
+ import compression from "compression";
33
+ import { WebSocket, WebSocketServer } from "ws";
34
+ import { VERSION, getAgentDir } from "@earendil-works/pi-coding-agent";
35
+ import { PROTOCOL_VERSION } from "./protocol-version.js";
36
+ import { appVersion } from "./app-version.js";
37
+ import { AgentService, workspacePath, QuiesceRejectedError, } from "./agent-service.js";
38
+ import { isRealPathInWorkspace } from "./files-service.js";
39
+ import { previewKind } from "./text-sniff.js";
40
+ import { startControlServer } from "./control-socket.js";
41
+ import { readSqlitePreview } from "./sqlite-preview.js";
42
+ import { saveMarkdownImage } from "./markdown-images.js";
43
+ import { scheduleUploadCleanup } from "./uploads.js";
44
+ import { ensureWindowsBash, windowsBashDir } from "./ensure-bash.js";
45
+ import { isValidClientId } from "./client-id.js";
46
+ import { securityHeaders } from "./security-headers.js";
47
+ import { needsTokenCookie, tokenCookie, tokenMatches } from "./token-auth.js";
48
+ import { createOriginPolicy } from "./origin-policy.js";
49
+ import { PluginManager, resolvePluginClientFile } from "./plugins.js";
50
+ import { NodeWorkbench } from "./node-workbench.js";
51
+ const PORT = Number(process.env.PORT ?? 8787);
52
+ const CWD = resolve(process.env.PI_WEB_CWD ?? process.cwd());
53
+ const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
54
+ /** Bind address. Default is loopback ONLY — the service is a local personal
55
+ * tool and should not be reachable from the network unless explicitly asked
56
+ * (e.g. PI_WEB_HOST=0.0.0.0 for LAN access / Docker port mapping). */
57
+ const HOST = process.env.PI_WEB_HOST ?? "127.0.0.1";
58
+ /** Optional strict hostname allowlist (comma-separated) — only used when set.
59
+ * Origin / Host same-authority matching happens regardless. */
60
+ const ALLOW_HOSTS = (process.env.PI_WEB_ALLOW_HOSTS ?? "")
61
+ .split(",")
62
+ .map((s) => s.trim().toLowerCase())
63
+ .filter(Boolean);
64
+ /** Optional extra Origins allowed through the same-authority check (comma-
65
+ * separated, e.g. reverse-proxy setups where the browser origin differs
66
+ * from the Host the backend sees). */
67
+ const ALLOW_ORIGINS = (process.env.PI_WEB_ALLOW_ORIGINS ?? "")
68
+ .split(",")
69
+ .map((s) => s.trim().toLowerCase())
70
+ .filter(Boolean);
71
+ /** 可选共享口令(PI_WEB_TOKEN):设置后所有 HTTP/WS 请求必须携带——
72
+ * Authorization: Bearer / X-PI-Token 头、?token= 查询参数或 pi_web_token cookie
73
+ * 任一匹配即可;供 0.0.0.0 / 反代等暴露场景兜底,未设置则行为不变。 */
74
+ const AUTH_TOKEN = process.env.PI_WEB_TOKEN?.trim() ?? "";
75
+ // The token authenticates browsers to this server. Terminals, the agent's bash tool and
76
+ // worker processes inherit process.env, so leaving it there would hand the secret to
77
+ // anything the agent runs (and into model context via `env`). Nothing re-reads it.
78
+ delete process.env.PI_WEB_TOKEN;
79
+ // Root of the SDK default per-project session dirs — chat transcripts live in
80
+ // <SESSION_DIR_ROOT>/--<cwd>--/, shared with the pi CLI/TUI (getAgentDir
81
+ // honors PI_CODING_AGENT_DIR).
82
+ const SESSION_DIR_ROOT = join(getAgentDir(), "sessions");
83
+ // Windows 轻量 bash 兜底:把 <home>/.pi-web/bin 前置到 PATH(SDK 的 bash 工具经
84
+ // findBashOnPath 会找到其中的 bash.exe),并在无 Git Bash 时后台下载 busybox-w32。
85
+ // 终端面板的 shell 探测链也已包含该目录(见 terminals.ts resolveShell)。
86
+ if (process.platform === "win32") {
87
+ process.env.PATH = `${windowsBashDir()}${delimiter}${process.env.PATH ?? ""}`;
88
+ void ensureWindowsBash();
89
+ }
90
+ const app = express();
91
+ app.use(express.json({ limit: "10mb" }));
92
+ app.use(securityHeaders);
93
+ /** 从请求中提取候选 token:头 / 查询参数 / cookie(浏览器导航场景靠 cookie 续命)。 */
94
+ function requestTokens(req) {
95
+ const out = [];
96
+ const auth = req.headers.authorization;
97
+ if (typeof auth === "string" && auth.startsWith("Bearer "))
98
+ out.push(auth.slice(7).trim());
99
+ const header = req.headers["x-pi-token"];
100
+ if (typeof header === "string")
101
+ out.push(header.trim());
102
+ try {
103
+ const q = new URL(req.url ?? "/", "http://localhost").searchParams.get("token");
104
+ if (q)
105
+ out.push(q.trim());
106
+ }
107
+ catch {
108
+ /* ignore malformed url */
109
+ }
110
+ const cookie = req.headers.cookie;
111
+ if (typeof cookie === "string") {
112
+ for (const part of cookie.split(";")) {
113
+ const [k, ...rest] = part.trim().split("=");
114
+ if (k === "pi_web_token")
115
+ out.push(rest.join("=").trim());
116
+ }
117
+ }
118
+ return out.filter(Boolean);
119
+ }
120
+ function tokenOk(req) {
121
+ return tokenMatches(requestTokens(req), AUTH_TOKEN);
122
+ }
123
+ if (AUTH_TOKEN) {
124
+ // /api/health 保持开放:无敏感信息,容器/监控探针需要它
125
+ app.use((req, res, next) => {
126
+ const authenticated = tokenOk(req);
127
+ if (authenticated || req.path === "/api/health") {
128
+ // 浏览器经 ?token= 首次进入后下发 HttpOnly cookie,后续导航/资源请求免带参数。
129
+ // 只发给已认证的请求:匿名健康检查拿到 cookie 就等于拿到口令。
130
+ if (authenticated && needsTokenCookie(req.headers.cookie, AUTH_TOKEN)) {
131
+ const https = req.socket.encrypted === true || req.headers["x-forwarded-proto"] === "https";
132
+ res.setHeader("Set-Cookie", tokenCookie(AUTH_TOKEN, https));
133
+ }
134
+ next();
135
+ return;
136
+ }
137
+ res.status(401).send("unauthorized: PI_WEB_TOKEN required (?token=…)");
138
+ });
139
+ }
140
+ installCodemodeImageRoutes(app, () => service, originAllowed);
141
+ installExtensionsRoutes(app, () => service, originAllowed);
142
+ installSystemPromptRoutes(app, () => service, originAllowed);
143
+ installWikiRoutes(app, () => service, join(DATA_DIR, "wiki"), originAllowed);
144
+ app.get("/api/health", (_req, res) => {
145
+ res.json({ ok: true, piVersion: VERSION, pid: process.pid });
146
+ });
147
+ app.get("/api/sqlite", async (req, res) => {
148
+ res.setHeader("Cache-Control", "no-store");
149
+ if (!originAllowed(req)) {
150
+ res.status(403).json({ error: "Forbidden" });
151
+ return;
152
+ }
153
+ const { clientId, cwd, path, requestId, table, offset = "0" } = req.query;
154
+ if (![clientId, cwd, path, requestId, offset].every((value) => typeof value === "string") || (table !== undefined && typeof table !== "string")) {
155
+ res.status(400).json({ error: "Invalid request" });
156
+ return;
157
+ }
158
+ const cs = service.get(clientId);
159
+ const identity = { requestId, cwd, path };
160
+ if (!cs || cs.switchingWorkspace || cs.cwd !== cwd) {
161
+ res.status(409).json({ ...identity, error: "Workspace unavailable" });
162
+ return;
163
+ }
164
+ const controller = new AbortController();
165
+ res.once("close", () => controller.abort());
166
+ try {
167
+ const data = await readSqlitePreview({ cwd: cwd, path: path, table: table, offset: Number(offset) }, controller.signal);
168
+ if (controller.signal.aborted)
169
+ return;
170
+ if (cs.switchingWorkspace || cs.cwd !== cwd) {
171
+ res.status(409).json({ ...identity, error: "Workspace changed" });
172
+ return;
173
+ }
174
+ res.json({ ...identity, data });
175
+ }
176
+ catch (error) {
177
+ if (!controller.signal.aborted)
178
+ res.status(400).json({ ...identity, error: error.message });
179
+ }
180
+ });
181
+ app.post("/api/markdown-image", (req, res) => {
182
+ if (!originAllowed(req) || !req.is("application/json")) {
183
+ res.status(403).json({ error: "Forbidden" });
184
+ return;
185
+ }
186
+ const { clientId, cwd, path, data } = req.body ?? {};
187
+ if (![clientId, cwd, path, data].every((value) => typeof value === "string")) {
188
+ res.status(400).json({ error: "Invalid request" });
189
+ return;
190
+ }
191
+ const cs = service.get(clientId);
192
+ if (!cs || cs.switchingWorkspace || cs.cwd !== cwd || service.quiesceInfo().quiesced) {
193
+ res.status(409).json({ error: "Workspace unavailable" });
194
+ return;
195
+ }
196
+ try {
197
+ res.json({ path: saveMarkdownImage(cwd, path, data) });
198
+ }
199
+ catch (error) {
200
+ res.status(400).json({ error: error.message });
201
+ }
202
+ });
203
+ /**
204
+ * Stream a workspace file over HTTP.
205
+ *
206
+ * Media preview (no download param): only image/video kinds are served —
207
+ * text goes over the WebSocket, and exe/jar/etc. are never exposed here.
208
+ * express's sendFile handles Range requests, so video seeking works.
209
+ *
210
+ * Download (?download=1): any file kind is served with
211
+ * Content-Disposition: attachment so the browser saves it instead of
212
+ * rendering. Path is validated against the workspace root either way.
213
+ */
214
+ app.get(["/api/tool-output", "/api/tool-output-list"], async (req, res) => {
215
+ if (!originAllowed(req)) {
216
+ res.status(403).end("Forbidden");
217
+ return;
218
+ }
219
+ const { clientId, conversationId, toolCallId } = req.query;
220
+ if (typeof clientId !== "string" || typeof conversationId !== "string" || typeof toolCallId !== "string") {
221
+ res.status(400).end("Invalid request");
222
+ return;
223
+ }
224
+ try {
225
+ const cs = service.get(clientId);
226
+ if (!cs) {
227
+ res.status(403).end("Output access denied");
228
+ return;
229
+ }
230
+ if (req.path === "/api/tool-output-list") {
231
+ res.json((await cs.listToolOutputs(conversationId, toolCallId)).map(({ id, name, default: isDefault }) => ({ id, name, default: isDefault })));
232
+ return;
233
+ }
234
+ const { handle, name } = await cs.downloadToolOutput(conversationId, toolCallId, typeof req.query.outputId === "string" ? req.query.outputId : undefined);
235
+ res.attachment(name);
236
+ res.setHeader("X-Content-Type-Options", "nosniff");
237
+ const stream = handle.createReadStream();
238
+ res.on("close", () => stream.destroy());
239
+ stream.on("error", () => res.destroy());
240
+ stream.pipe(res);
241
+ }
242
+ catch (error) {
243
+ const code = error.code;
244
+ res.status(code === "ENOENT" ? 404 : code === "EACCES" || code === "EPERM" ? 403 : 500).end(code === "ENOENT" ? "Output file may have been cleaned up" : code === "EACCES" || code === "EPERM" ? "Output access denied" : "Output server error");
245
+ }
246
+ });
247
+ app.get("/api/file", async (req, res) => {
248
+ if (!originAllowed(req)) {
249
+ res.status(403).end("Forbidden");
250
+ return;
251
+ }
252
+ try {
253
+ const raw = typeof req.query.path === "string" ? req.query.path : "";
254
+ // Resolve against the requesting client's workspace (the opened
255
+ // project), not the server's startup cwd — they can differ when the
256
+ // client switched projects or restored a previous workspace. Fall
257
+ // back to the server cwd for requests without a known client.
258
+ const cid = typeof req.query.clientId === "string" ? req.query.clientId : "";
259
+ const cs = cid ? service.get(cid) : undefined;
260
+ // No anonymous fallback to the server cwd: a request must belong to a live client.
261
+ if (!cs) {
262
+ res.status(403).end("unknown client");
263
+ return;
264
+ }
265
+ if (req.query.cwd && (cs.switchingWorkspace || req.query.cwd !== cs.cwd)) {
266
+ res.status(409).end("workspace changed");
267
+ return;
268
+ }
269
+ const wp = workspacePath(cs.cwd, raw);
270
+ if (!wp) {
271
+ res.status(400).end("path outside workspace");
272
+ return;
273
+ }
274
+ const abs = wp.abs;
275
+ if (!isRealPathInWorkspace(cs.cwd, abs)) {
276
+ res.status(400).end("path outside workspace");
277
+ return;
278
+ }
279
+ const name = basename(abs);
280
+ const kind = previewKind(name);
281
+ const isDownload = req.query.download === "1";
282
+ if (!isDownload && kind !== "image" && kind !== "video") {
283
+ res.status(400).end("not a previewable media file");
284
+ return;
285
+ }
286
+ const st = await stat(abs);
287
+ if (!st.isFile()) {
288
+ res.status(400).end("not a file");
289
+ return;
290
+ }
291
+ if (isDownload) {
292
+ // res.download sets Content-Disposition: attachment and RFC 5987
293
+ // filename* encoding for non-ASCII names.
294
+ res.download(abs, name);
295
+ }
296
+ else {
297
+ res.sendFile(abs);
298
+ }
299
+ }
300
+ catch {
301
+ res.status(404).end("not found");
302
+ }
303
+ });
304
+ // Production: serve the built frontend from web/dist. Resolve relative to this
305
+ // module so it works when installed as a package (global/npx/Docker), not just
306
+ // from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
307
+ const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <pkg>/server
308
+ // Resolve the package root robustly: dev runs from <repo>/server (tsx), prod
309
+ // from <pkg>/dist/server — the ancestor that actually has package.json wins.
310
+ function resolvePkgRoot() {
311
+ // PI_WEB_PKG_ROOT: Electron 桌面版打包后,server 子进程从 extraResources 目录
312
+ //(process.resourcesPath)加载 web/dist 和 themes。通过这个 env var 告诉
313
+ // server 去哪里找 pkgRoot,避免 resolvePkgRoot 的候选路径找不到 package.json。
314
+ if (process.env.PI_WEB_PKG_ROOT)
315
+ return process.env.PI_WEB_PKG_ROOT;
316
+ const candidates = [
317
+ resolve(here, ".."),
318
+ resolve(here, "..", ".."),
319
+ resolve(here, "..", "..", ".."),
320
+ ];
321
+ for (const c of candidates) {
322
+ if (existsSync(join(c, "package.json")))
323
+ return c;
324
+ }
325
+ return candidates[0];
326
+ }
327
+ const pkgRoot = resolvePkgRoot();
328
+ const APP_VERSION = appVersion(pkgRoot);
329
+ // Plugin client bundles: <dataDir>/plugins/<id>/client/* served at
330
+ // /plugins/<id>/client/* so the frontend can import() plugin views. Only the
331
+ // client/ subtree is exposed — manifest.json and the server-side index.mjs
332
+ // (which may hold credentials) never leave the machine. Registered BEFORE the
333
+ // SPA catch-all below.
334
+ const PLUGINS_DIR = join(DATA_DIR, "plugins");
335
+ // 插件 HTTP 路由挂载点:host.route("GET", "/inbox") 实际暴露为
336
+ // /plugins-api/<id>/inbox。PI_WEB_TOKEN 鉴权(上方 app.use)自动覆盖;
337
+ // 响应已在前面过了 express.json。注意不要在此 catch-all 里消费 body。
338
+ app.all(["/plugins-api/:id/*", "/plugins-api/:id"], (req, res) => {
339
+ const rest = String(req.params[0] ?? "");
340
+ pluginMgr.handleHttp(String(req.params.id ?? ""), req.method, rest, req, res);
341
+ });
342
+ app.get("/plugins/:id/client/*", (req, res) => {
343
+ // express 4 的通配参数在运行时落在 params[0],但类型声明里没有 —— 显式取
344
+ const rest = String(req.params[0] ?? "");
345
+ const abs = resolvePluginClientFile(PLUGINS_DIR, req.params.id, rest);
346
+ if (!abs) {
347
+ res.status(404).end("plugin not found");
348
+ return;
349
+ }
350
+ // .mjs 常不在老 mime 表里,手动定 Content-Type 保证 import() 可用
351
+ if (/\.(mjs|js)$/.test(abs)) {
352
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
353
+ }
354
+ res.setHeader("Cache-Control", "no-cache"); // 开发期改文件即生效
355
+ res.sendFile(abs, (err) => {
356
+ if (err && !res.headersSent)
357
+ res.status(err.statusCode === 404 ? 404 : 500).end("not found");
358
+ });
359
+ });
360
+ /** Set in the env of the replacement child spawned by a self-update restart. */
361
+ const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
362
+ const webDist = join(pkgRoot, "web", "dist");
363
+ if (existsSync(webDist)) {
364
+ // gzip/deflate 响应压缩:前端 bundle ~1MB,局域网/反代场景传输量降到 ~1/4;
365
+ // 对 API JSON 同样生效,WS 升级不受影响
366
+ app.use(compression());
367
+ app.use(express.static(webDist, {
368
+ // Vite 产物文件名带内容 hash,可永久强缓存——业务发版后 hash 变化自然失效,
369
+ // index.html 由下方 catch-all 处理(sendFile 不走这里)
370
+ setHeaders(res, filePath) {
371
+ if (filePath.includes(`${sep}assets${sep}`)) {
372
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
373
+ }
374
+ },
375
+ }));
376
+ app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
377
+ // Callback form: a failed stat here (npm i -g is mid-replacement of the
378
+ // package dir) responds 503 instead of crashing the request pipeline
379
+ // with an unhandled ENOENT stack trace.
380
+ res.sendFile(join(webDist, "index.html"), (err) => {
381
+ if (err && !res.headersSent) {
382
+ res.status(503).send("正在更新 pi-harness,请稍后刷新…");
383
+ }
384
+ });
385
+ });
386
+ }
387
+ else if (process.env[RESTART_CHILD_ENV]) {
388
+ // Auto-restart replacement of a self-update whose npm install did not
389
+ // complete (Windows: locked files / rollback can leave the global package
390
+ // without web/dist). Fail loudly with a repair hint instead of serving a
391
+ // UI-less 404 with no explanation.
392
+ console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" +
393
+ " 请手动执行 npm i -g @youweichen/pi-harness@latest 修复后重新启动。");
394
+ process.exit(1);
395
+ }
396
+ const httpServer = createServer(app);
397
+ const wss = new WebSocketServer({
398
+ noServer: true,
399
+ // Per-message deflate: big-session snapshots serialize to multi-MB JSON
400
+ // strings; wire-level compression cuts that several-fold. threshold keeps
401
+ // tiny messages (notices/heartbeats) uncompressed to save CPU.
402
+ perMessageDeflate: { threshold: 16 * 1024 },
403
+ });
404
+ // ---------------------------------------------------------------------------
405
+ // Origin / Host admission for WebSocket upgrades.
406
+ //
407
+ // Browsers attach an Origin header; non-browser clients (curl, ws scripts)
408
+ // usually don't — they're admitted by the network layer / reverse proxy.
409
+ // Policy lives in origin-policy.ts: Host allowlist (loopback by default when
410
+ // bound to loopback, against DNS rebinding), then same-authority Origin match.
411
+ //
412
+ // Dev-mode note: the Vite dev server (:5173) proxies /ws to the backend on
413
+ // :8788, so their authorities differ — the dev:server script sets
414
+ // PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 for that.
415
+ // LAN / reverse-proxy setups add their own origin the same way.
416
+ // ---------------------------------------------------------------------------
417
+ const originPolicy = createOriginPolicy({ bindHost: HOST, allowHosts: ALLOW_HOSTS, allowOrigins: ALLOW_ORIGINS });
418
+ function originAllowed(req) {
419
+ return originPolicy(req.headers);
420
+ }
421
+ httpServer.on("upgrade", (req, socket, head) => {
422
+ let pathname = "/";
423
+ try {
424
+ pathname = new URL(req.url ?? "/", "http://localhost").pathname;
425
+ }
426
+ catch {
427
+ /* fall through to the path check below */
428
+ }
429
+ if (pathname !== "/ws") {
430
+ socket.destroy();
431
+ return;
432
+ }
433
+ if (!originAllowed(req)) {
434
+ // Reject cross-origin browser pages outright. The browser sees a failed
435
+ // WS connect; the page's own reconnect loop then backs off and retries.
436
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
437
+ socket.destroy();
438
+ return;
439
+ }
440
+ if (AUTH_TOKEN && !tokenOk(req)) {
441
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
442
+ socket.destroy();
443
+ return;
444
+ }
445
+ wss.handleUpgrade(req, socket, head, (ws) => {
446
+ wss.emit("connection", ws, req);
447
+ });
448
+ });
449
+ // Heartbeat: lets clients detect half-open connections (server killed without
450
+ // closing sockets, sleep/wake, network partitions). Idle connections otherwise
451
+ // carry no traffic and TCP keepalive defaults are far too slow (~2h).
452
+ const heartbeatTimer = setInterval(() => {
453
+ for (const ws of wss.clients) {
454
+ if (ws.readyState === WebSocket.OPEN) {
455
+ ws.send(JSON.stringify({ type: "heartbeat" }));
456
+ }
457
+ }
458
+ }, 10_000);
459
+ const service = new AgentService(CWD,
460
+ // Per-client persisted UI state: last-used workspace + recent projects.
461
+ join(DATA_DIR, "client-state.json"));
462
+ const nodeWorkbench = new NodeWorkbench(DATA_DIR);
463
+ // Optional UI plugins (<dataDir>/plugins/<id>/): scanned on every client
464
+ // attach so freshly dropped plugins appear without a server restart.
465
+ const pluginMgr = new PluginManager(DATA_DIR, CWD);
466
+ // 插件扩展点:SDK 工具执行事件(bash/读文件等 start+end)转发给已注册的插件。
467
+ service.onToolEvent = (ev) => pluginMgr.emitToolEvent(ev);
468
+ // 插件扩展点:插件常驻后台任务(registerBackgroundTask)→ 并入「后台任务」面板。
469
+ pluginMgr.onBgTasksChanged = () => service.refreshBackgroundServers();
470
+ service.pluginBgTasksProvider = () => pluginMgr.bgTasks();
471
+ service.pluginStopBgTask = (taskId) => pluginMgr.stopPluginBgTask(taskId);
472
+ // 插件宿主工作区实时跟随当前项目:任意客户端 set_cwd 成功后同步给
473
+ // PluginManager,编辑器等工作区跟随型插件随即切根(详见 plugins.ts notifyCwd)。
474
+ service.onClientCwdChanged = (cwd) => pluginMgr.notifyCwd(cwd);
475
+ // ---------------------------------------------------------------------------
476
+ // Self-update
477
+ // ---------------------------------------------------------------------------
478
+ // In-app updates now run `npm i -g @youweichen/pi-harness@latest` in a visible terminal
479
+ // tab (frontend-initiated); after it finishes the user restarts via
480
+ // `pi-harness server restart`. The PI_WEB_RESTART_CHILD port-wait handshake
481
+ // below stays: an externally orchestrated replacement child still needs it.
482
+ function scheduleQuit() {
483
+ const isLaunchd = process.platform === "darwin" && process.ppid === 1;
484
+ const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
485
+ const inDocker = existsSync("/.dockerenv");
486
+ if (isLaunchd || isSystemd || inDocker) {
487
+ setTimeout(() => {
488
+ console.log("pi-harness:quit — shutting down (supervisor will restart)…");
489
+ if (isSystemd)
490
+ process.exit(3);
491
+ void shutdown();
492
+ }, 300);
493
+ return true;
494
+ }
495
+ setTimeout(() => {
496
+ console.log("pi-harness:quit — shutting down (restart to reload)…");
497
+ void shutdown();
498
+ }, 300);
499
+ return true;
500
+ }
501
+ service.onQuit = scheduleQuit;
502
+ /** 背压相对倍数:socket 未发送积压超过「最近一份 snapshot 大小 × 此倍数」时丢弃
503
+ * (issue #11 及其评论区的自适应建议)。固定 1MB 阈值在长会话下单份 snapshot 可达
504
+ * ~10MB——连半份都没发完就丢,前端频繁跳帧;短会话又太迟钝。相对阈值语义稳定在
505
+ * 「缓冲堆了约 N 份快照」,不随会话长短漂移。 */
506
+ const SNAPSHOT_BACKPRESSURE_FACTOR = 3;
507
+ /** 背压绝对下限:低于此积压永不丢快照(小会话的相对阈值只有几 KB,会被
508
+ * 正常的消息突发误伤,见 send() 内注释)。 */
509
+ const SNAPSHOT_BACKPRESSURE_MIN_BYTES = 262_144;
510
+ /** 背压丢弃后的延迟重发间隔。 */
511
+ const SNAPSHOT_RETRY_MS = 250;
512
+ /**
513
+ * Multi-tab serialization sharing: emit() hands the SAME message object to
514
+ * every socket of a client, but each send() used to JSON.stringify it
515
+ * separately — N open tabs serialized the same multi-MB snapshot N times per
516
+ * push. Keyed by object identity (WeakMap): a new snapshot is a new object,
517
+ * so the cache self-invalidates and never grows.
518
+ */
519
+ const serializedCache = new WeakMap();
520
+ function serializeShared(msg) {
521
+ let s = serializedCache.get(msg);
522
+ if (s === undefined) {
523
+ s = JSON.stringify(msg);
524
+ serializedCache.set(msg, s);
525
+ }
526
+ return s;
527
+ }
528
+ /** Async handlers are fired with `void`; a rejection there is a bug in one request,
529
+ * not a reason to exit. Log with the stack so it can still be found. */
530
+ process.on("unhandledRejection", (reason) => {
531
+ console.error("[server] unhandled rejection:", reason instanceof Error ? reason.stack ?? reason.message : reason);
532
+ });
533
+ function reportBadMessage(type, err) {
534
+ console.error(`[ws] handler for "${String(type)}" threw:`, err instanceof Error ? err.stack ?? err.message : err);
535
+ }
536
+ wss.on("connection", (ws) => {
537
+ // Count attached sockets (the control socket reports REAL sockets, not
538
+ // cached client-session objects).
539
+ service.noteSocketOpen();
540
+ let clientId = null;
541
+ let closed = false;
542
+ /** 最近一份全量 snapshot 的估算字节数(UTF-16 ×2),供背压相对阈值用(issue #11)。 */
543
+ let lastSnapshotBytes = 0;
544
+ /** Commands received while the session is still being created — replayed after attach. */
545
+ let pending = [];
546
+ /** 背压丢快照后的延迟重发定时器(去重:一次只排一个)。 */
547
+ let snapshotRetryTimer = null;
548
+ // 协议层错误(非法帧/未 masked 帧等):不注册 handler 会作为 uncaught
549
+ // exception 打崩整个进程(issue #11 附带发现)。记日志并按坏连接关闭。
550
+ ws.on("error", (err) => {
551
+ console.error(`[ws] socket error${clientId ? ` (${clientId})` : ""}:`, err.message);
552
+ try {
553
+ ws.close();
554
+ }
555
+ catch {
556
+ /* already closing */
557
+ }
558
+ });
559
+ const send = (msg) => {
560
+ if (closed || ws.readyState !== WebSocket.OPEN)
561
+ return;
562
+ // 发送背压(issue #11):socket 消费不过来时(前端慢/网络差),堆里会堆积
563
+ // 每份可达 ~10MB 的全量 snapshot 字符串,低内存主机直接 OOM。snapshot 是全量
564
+ // 幂等的且稍后必有更新的一份,可以安全丢弃——在序列化之前丢,连
565
+ // stringify 的分配都省掉。ready/notice/error/tool_delta 等消息必须送达。
566
+ // 阈值相对化(评论区建议):用「最近一份 snapshot 的字节数 × 倍数」做基准,
567
+ // 首份无基准不丢(首次必达)。wire.length 是 UTF-16 字符数,×2 估算字节。
568
+ // 下限保护(小会话误伤修复):小会话一份 snapshot 才 ~1KB,相对阈值只有几
569
+ // KB——前面一批 settings_state/slash_commands 的正常突发就能把 bufferedAmount
570
+ // 抬过阈值,把紧随其后的 snapshot_delta 静默丢掉;而丢弃后若无后续事件就
571
+ // 再也没有快照,客户端永远停在旧状态(前端靠 rev 缺口 get_state 自愈,
572
+ // 协议测试则直接卡死)。绝对下限保证小会话永不触发背压。
573
+ if ((msg.type === "snapshot" || msg.type === "snapshot_delta") &&
574
+ lastSnapshotBytes > 0 &&
575
+ ws.bufferedAmount > Math.max(SNAPSHOT_BACKPRESSURE_MIN_BYTES, SNAPSHOT_BACKPRESSURE_FACTOR * lastSnapshotBytes)) {
576
+ // 真正的慢客户端:丢弃是安全的,但不能「丢完就没了」——安排一次延迟
577
+ // 重发,等缓冲排空后快照最终必达(否则若此后再无事件,客户端将永久
578
+ // 停留在旧快照)。重发仍走 flushSnapshot:缓冲未排空则再次顺延。
579
+ if (!snapshotRetryTimer) {
580
+ snapshotRetryTimer = setTimeout(() => {
581
+ snapshotRetryTimer = null;
582
+ service.get(clientId ?? "")?.flushSnapshot();
583
+ }, SNAPSHOT_RETRY_MS);
584
+ }
585
+ return;
586
+ }
587
+ const wire = serializeShared(msg);
588
+ if (msg.type === "snapshot")
589
+ lastSnapshotBytes = wire.length * 2;
590
+ ws.send(wire);
591
+ };
592
+ // Plugins broadcast to every open socket; unregister on close below. The
593
+ // cid getter lets plugins target THIS socket via host.sendTo(clientId).
594
+ const removePluginSender = pluginMgr.addSender(send, () => clientId);
595
+ let detachNodes;
596
+ /** One malformed or unexpected message must never take the whole server (and every
597
+ * client's running agent) down: report it to this client and keep going. */
598
+ const dispatch = (msg) => {
599
+ try {
600
+ dispatchUnsafe(msg);
601
+ }
602
+ catch (err) {
603
+ reportBadMessage(msg?.type, err);
604
+ send({ type: "notice", level: "error", text: `请求处理失败(${String(msg?.type)}):${err.message}` });
605
+ }
606
+ };
607
+ const dispatchUnsafe = (msg) => {
608
+ if (!clientId) {
609
+ pending.push(msg);
610
+ return;
611
+ }
612
+ const cs = service.get(clientId);
613
+ if (!cs) {
614
+ // Session not ready yet (hello processing) — hold the command.
615
+ pending.push(msg);
616
+ return;
617
+ }
618
+ if (cs.switchingWorkspace && msg.type !== "set_cwd" && msg.type !== "get_state") {
619
+ if (msg.type === "prompt" && msg.requestId)
620
+ send({ type: "prompt_result", requestId: msg.requestId, ok: false });
621
+ if (msg.type === "read_file" || msg.type === "write_file")
622
+ send({
623
+ type: "file_result", operation: msg.type === "read_file" ? "read" : "write",
624
+ requestId: msg.requestId, cwd: msg.cwd ?? cs.cwd, path: msg.path,
625
+ ok: false, error: "工作区切换中,请稍后重试",
626
+ });
627
+ return;
628
+ }
629
+ switch (msg.type) {
630
+ case "node_request":
631
+ void nodeWorkbench.handle(clientId, msg);
632
+ break;
633
+ case "prompt":
634
+ void cs.prompt(msg.text, msg.attachments, msg.queue, msg.requestId);
635
+ break;
636
+ case "abort":
637
+ void cs.abort();
638
+ break;
639
+ case "retry_silent_prompt":
640
+ void cs.retrySilentPrompt(msg.conversationId, msg.text);
641
+ break;
642
+ case "kill_background_server":
643
+ void cs.killBackgroundServer(msg.port, msg.taskId);
644
+ break;
645
+ case "kill_background_servers":
646
+ void cs.killAllBackgroundServers();
647
+ break;
648
+ case "list_bg_servers":
649
+ void cs.listBgServers();
650
+ break;
651
+ case "new_chat":
652
+ void cs.newChat();
653
+ break;
654
+ case "tree_get":
655
+ case "tree_content":
656
+ case "tree_preview":
657
+ case "tree_navigate":
658
+ case "tree_label":
659
+ case "session_clone":
660
+ case "session_fork":
661
+ case "session_reopen":
662
+ void cs.treeRequest(msg);
663
+ break;
664
+ case "edit_message":
665
+ void cs.editMessage(msg.messageId, msg.text, msg.attachments, msg);
666
+ break;
667
+ case "cycle_model":
668
+ void cs.cycleModel();
669
+ break;
670
+ case "cycle_thinking":
671
+ cs.cycleThinking();
672
+ break;
673
+ case "get_state":
674
+ // Always a FULL snapshot: the client is (re)connecting or detected
675
+ // a rev/seq gap — it needs an authoritative state to rebuild from.
676
+ cs.flushSnapshot(true);
677
+ break;
678
+ case "get_commands":
679
+ void cs.pushSlashCommands();
680
+ break;
681
+ case "list_sessions":
682
+ void cs.refreshSessions();
683
+ break;
684
+ case "list_projects":
685
+ void cs.pushProjects();
686
+ break;
687
+ case "remove_project":
688
+ void cs.removeProject(msg.path);
689
+ break;
690
+ case "delete_session":
691
+ void cs.deleteSession(msg.path);
692
+ break;
693
+ case "rename_session":
694
+ void cs.renameSession(msg.path, msg.name);
695
+ break;
696
+ case "browse_dirs":
697
+ void cs.browseDirs(msg.path);
698
+ break;
699
+ case "switch_session":
700
+ void cs.switchSession(msg.path);
701
+ break;
702
+ case "switch_conversation":
703
+ void cs.switchConversation(msg.id);
704
+ break;
705
+ case "list_files":
706
+ void cs.listFiles(msg.path);
707
+ break;
708
+ case "check_conversation_files":
709
+ cs.checkConversationFiles(msg.cwd, msg.reqId, msg.paths);
710
+ break;
711
+ case "search_files":
712
+ void cs.searchFiles(msg.query, msg.reqId);
713
+ break;
714
+ case "get_git_branch":
715
+ void cs.gitBranch();
716
+ break;
717
+ case "scm_diff":
718
+ void cs.scmQuery("diff", msg.reqId, { scope: msg.scope, base: msg.base });
719
+ break;
720
+ case "scm_status":
721
+ void cs.scmQuery("status", msg.reqId);
722
+ break;
723
+ case "scm_history":
724
+ void cs.scmQuery("history", msg.reqId);
725
+ break;
726
+ case "scm_filediff":
727
+ void cs.scmQuery("filediff", msg.reqId, { path: msg.path });
728
+ break;
729
+ case "scm_commit":
730
+ void cs.scmQuery("commit", msg.reqId, { hash: msg.hash });
731
+ break;
732
+ case "read_file":
733
+ void cs.readFile(msg.path, msg);
734
+ break;
735
+ case "write_file":
736
+ void cs.writeFile(msg.path, msg.text, msg);
737
+ break;
738
+ case "list_models":
739
+ void cs.listModels();
740
+ break;
741
+ case "set_model":
742
+ void cs.setModel(msg.modelId);
743
+ break;
744
+ case "set_thinking":
745
+ cs.setThinking(msg.level);
746
+ break;
747
+ case "set_cwd":
748
+ void cs.setCwd(msg.path, msg.requestId, msg.source);
749
+ break;
750
+ case "complete_path":
751
+ void cs.completePath(msg.path);
752
+ break;
753
+ case "check_component_updates":
754
+ void cs.checkComponentUpdates(msg.requestId);
755
+ break;
756
+ case "update_component":
757
+ if (service.quiesceInfo().quiesced || service.activeConversations() || service.pendingMessages()) {
758
+ send({ type: "component_updates", requestId: msg.requestId, cwd: cs.cwd, phase: "error", items: [], error: "请等待所有任务结束后再更新扩展" });
759
+ }
760
+ else {
761
+ service.quiesce();
762
+ void cs.updateComponent(msg.requestId, msg.id).finally(() => service.unquiesce());
763
+ }
764
+ break;
765
+ case "check_update":
766
+ void cs.checkUpdate();
767
+ break;
768
+ case "queue_recall_ack":
769
+ cs.acknowledgeQueueRecall(msg.conversationId, msg.requestId);
770
+ break;
771
+ case "recall_queue":
772
+ cs.recallQueue(msg.conversationId, msg.requestId);
773
+ break;
774
+ case "cancel_recovery":
775
+ cs.cancelRecovery(msg.conversationId, msg.operationId);
776
+ break;
777
+ case "set_run_settings":
778
+ cs.setRunSettings(msg);
779
+ break;
780
+ case "dialog_response":
781
+ cs.resolveDialog(msg.conversationId, msg.id, msg.value);
782
+ break;
783
+ case "install_pi_agent":
784
+ void cs.installPiAgent();
785
+ break;
786
+ case "logout_provider":
787
+ void cs.providerAuth.logout(msg.provider);
788
+ break;
789
+ case "native_mcp_request":
790
+ void cs.nativeMcpRequest(msg);
791
+ break;
792
+ case "login_provider":
793
+ void cs.providerAuth.login(msg.provider);
794
+ break;
795
+ case "cancel_provider_login":
796
+ cs.providerAuth.cancel(msg.requestId);
797
+ break;
798
+ case "provider_auth_response":
799
+ cs.providerAuth.respond(msg.requestId, msg.promptId, msg.value);
800
+ break;
801
+ case "set_provider_api_key":
802
+ void cs.setProviderApiKey(msg.provider, msg.apiKey);
803
+ break;
804
+ case "clear_provider_api_key":
805
+ void cs.clearProviderApiKey(msg.provider);
806
+ break;
807
+ case "list_models_config":
808
+ void cs.listModelsConfig();
809
+ break;
810
+ case "save_model_config":
811
+ void cs.saveModelConfig(msg.providerId, msg.config);
812
+ break;
813
+ case "delete_model_config":
814
+ void cs.deleteModelConfig(msg.providerId);
815
+ break;
816
+ case "list_providers":
817
+ void cs.listProviders();
818
+ break;
819
+ case "fetch_models":
820
+ void cs.fetchModelsList(msg.reqId, msg.baseUrl, msg.apiKey, msg.authHeader, msg.api);
821
+ break;
822
+ case "refresh_provider_models":
823
+ void cs.refreshProviderModels(msg.providerId, msg.reqId);
824
+ break;
825
+ case "clone_provider":
826
+ void cs.cloneProvider(msg.provider, msg.reqId);
827
+ break;
828
+ case "terminal_create": {
829
+ const tm = cs.getTerminalManager(msg.conversationId);
830
+ if (tm)
831
+ tm.create(msg.terminalId, msg.cwd, msg.cols, msg.rows, cs.getTerminalCwd(msg.conversationId));
832
+ break;
833
+ }
834
+ case "terminal_input":
835
+ cs.getTerminalManager(msg.conversationId)?.input(msg.terminalId, msg.data);
836
+ break;
837
+ case "terminal_resize":
838
+ cs.getTerminalManager(msg.conversationId)?.resize(msg.terminalId, msg.cols, msg.rows);
839
+ break;
840
+ case "terminal_kill":
841
+ cs.getTerminalManager(msg.conversationId)?.kill(msg.terminalId);
842
+ break;
843
+ case "run_command":
844
+ cs.getTerminalManager(msg.conversationId)?.runCommand(msg.terminalId, msg.command, msg.cols, msg.rows, cs.getTerminalCwd(msg.conversationId));
845
+ break;
846
+ case "list_commands":
847
+ void cs.listCommands();
848
+ break;
849
+ case "save_commands":
850
+ void cs.saveCommands(msg.commands);
851
+ break;
852
+ case "set_plan_enabled":
853
+ void cs.setPlanEnabled(msg.enabled, msg.conversationId).catch(error => send({ type: "notice", level: "error", text: String(error.message ?? error) }));
854
+ break;
855
+ case "get_settings":
856
+ cs.pushSettings();
857
+ break;
858
+ case "set_settings":
859
+ void cs.setSettings({
860
+ editResendNewSession: msg.editResendNewSession,
861
+ disabledPlugins: msg.disabledPlugins,
862
+ thinkingWrap: msg.thinkingWrap,
863
+ toolsWrap: msg.toolsWrap,
864
+ });
865
+ break;
866
+ case "extensions_reload":
867
+ void cs.reloadExtensions();
868
+ break;
869
+ case "plugin_message":
870
+ pluginMgr.handleMessage(msg.pluginId, msg.payload, clientId ?? undefined);
871
+ break;
872
+ case "plugin_settings": {
873
+ const r = pluginMgr.savePluginSettings(msg.pluginId, msg.values ?? {});
874
+ if (r.error) {
875
+ cs?.emitNotice("error", `插件设置保存失败:${r.error}`);
876
+ }
877
+ else {
878
+ cs?.emitNotice("info", "插件设置已保存");
879
+ }
880
+ break;
881
+ }
882
+ case "plugins_reload":
883
+ void pluginMgr.reload().then(() => pluginMgr.pushToAll());
884
+ break;
885
+ default:
886
+ break;
887
+ }
888
+ };
889
+ ws.on("message", (data) => {
890
+ let msg;
891
+ try {
892
+ msg = JSON.parse(data.toString());
893
+ }
894
+ catch {
895
+ return;
896
+ }
897
+ if (msg.type === "hello") {
898
+ if (msg.clientId !== undefined && msg.clientId !== "" && !isValidClientId(msg.clientId)) {
899
+ ws.close(4400, "invalid clientId");
900
+ return;
901
+ }
902
+ const cid = msg.clientId || randomUUID();
903
+ clientId = cid;
904
+ service
905
+ .attach(cid, send)
906
+ .then((cs) => {
907
+ if (closed)
908
+ return;
909
+ detachNodes?.();
910
+ detachNodes = nodeWorkbench.attach(cid, send);
911
+ send({
912
+ type: "ready",
913
+ clientId: cid,
914
+ serverVersion: APP_VERSION,
915
+ protocolVersion: PROTOCOL_VERSION,
916
+ });
917
+ cs.flushSnapshot();
918
+ // Plugin catalog: re-scan + activate new dirs on every attach so
919
+ // freshly dropped plugins show up without a server restart.
920
+ pluginMgr
921
+ .ensureLoaded()
922
+ .then((plugins) => {
923
+ send({ type: "plugins", plugins, epoch: pluginMgr.epoch });
924
+ // 让各插件向新接入的客户端推送自身初始状态(onAttach 钩子)——
925
+ // 插件不要依赖客户端挂载后自己拉(见 plugins.ts onAttach 注释)。
926
+ pluginMgr.notifyAttach(cid);
927
+ // 插件命令可能在本客户端 attach 过程中才注册(首载竞态)——
928
+ // 重推一次目录,保证选择器完整。
929
+ })
930
+ .catch(() => { });
931
+ // Replay anything that arrived while the session was starting.
932
+ const queued = pending;
933
+ pending = [];
934
+ for (const m of queued)
935
+ dispatch(m);
936
+ })
937
+ .catch((err) => {
938
+ // Admission refused (quiesce): close the socket so the browser
939
+ // reconnect loop keeps retrying until admission reopens. Do NOT
940
+ // leave a half-alive connection that can only show an error.
941
+ if (err instanceof QuiesceRejectedError) {
942
+ closed = true;
943
+ if (ws.readyState === WebSocket.OPEN) {
944
+ ws.close(4403, "quiesced");
945
+ }
946
+ ws.terminate?.();
947
+ return;
948
+ }
949
+ // Real init failure (bad agent dir etc.) — keep the connection
950
+ // open so the user can see the error and fix it.
951
+ send({
952
+ type: "notice",
953
+ level: "error",
954
+ text: `会话初始化失败:${err.message}`,
955
+ });
956
+ });
957
+ return;
958
+ }
959
+ dispatch(msg);
960
+ });
961
+ ws.on("close", () => {
962
+ service.noteSocketClose();
963
+ closed = true;
964
+ pending = [];
965
+ removePluginSender();
966
+ detachNodes?.();
967
+ if (snapshotRetryTimer) {
968
+ clearTimeout(snapshotRetryTimer);
969
+ snapshotRetryTimer = null;
970
+ }
971
+ if (clientId)
972
+ service.detach(clientId, send);
973
+ });
974
+ });
975
+ // When spawned by the old process as an auto-restart replacement, wait for
976
+ // the old instance to release the port before binding (it exits right after
977
+ // spawning us). Probe by attempting a connection: refused = free.
978
+ if (process.env[RESTART_CHILD_ENV] === "1") {
979
+ const deadline = Date.now() + 20_000;
980
+ const portFree = () => new Promise((resolve) => {
981
+ const sock = createConnection({ port: PORT, host: "127.0.0.1" });
982
+ sock.once("connect", () => {
983
+ sock.destroy();
984
+ resolve(false); // busy — old instance still up
985
+ });
986
+ sock.once("error", () => resolve(true)); // refused → free
987
+ sock.setTimeout(500, () => {
988
+ sock.destroy();
989
+ resolve(false);
990
+ });
991
+ });
992
+ while (Date.now() < deadline) {
993
+ if (await portFree())
994
+ break;
995
+ await new Promise((r) => setTimeout(r, 300));
996
+ }
997
+ }
998
+ httpServer.listen(PORT, HOST, () => {
999
+ console.log("");
1000
+ console.log(" ⚡ pi-harness — web chat for the pi coding agent");
1001
+ console.log(` http://localhost:${PORT}`);
1002
+ console.log(` workspace : ${CWD}`);
1003
+ console.log(` session dir : ${SESSION_DIR_ROOT}`);
1004
+ console.log(` pi SDK : v${VERSION}`);
1005
+ console.log(` bind : ${HOST}:${PORT}`);
1006
+ console.log("");
1007
+ });
1008
+ // 上传文件保留期清理:启动扫一次 + 每 6 小时一次(best-effort,见 uploads.ts)
1009
+ scheduleUploadCleanup();
1010
+ // Local control socket (status / quiesce / unquiesce) — same data dir the
1011
+ // CLI uses, so `pi-harness server status|quiesce|unquiesce` just works.
1012
+ const stopControl = startControlServer({ service, dataDir: DATA_DIR, port: PORT });
1013
+ let shuttingDown = false;
1014
+ async function shutdown() {
1015
+ if (shuttingDown)
1016
+ return;
1017
+ shuttingDown = true;
1018
+ console.log("\nshutting down…");
1019
+ clearInterval(heartbeatTimer);
1020
+ stopControl();
1021
+ nodeWorkbench.dispose();
1022
+ pluginMgr.dispose();
1023
+ await service.disposeAll();
1024
+ wss.close();
1025
+ httpServer.close();
1026
+ process.exit(0);
1027
+ }
1028
+ process.on("SIGINT", () => void shutdown());
1029
+ process.on("SIGTERM", () => void shutdown());