@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,712 @@
1
+ /**
2
+ * pi-harness 插件管理器 —— 可选界面组件的加载与桥接。
3
+ *
4
+ * 一个插件 = <dataDir>/plugins/<id>/ 目录:
5
+ * manifest.json 元数据 { id?, name, version?, description? }(id 缺省取目录名)
6
+ * index.mjs 服务端入口(可选):export default { activate(host) → deactivate? }
7
+ * client/ 前端资源(可选),经 /plugins/<id>/client/* 以静态文件暴露;
8
+ * entry.mjs 视图入口:export default { mount(el, ctx) → cleanup? }
9
+ *
10
+ * 设计要点:
11
+ * - 不装即不存在:目录不在就没有任何协议/UI 痕迹;每次客户端 attach 时重扫目录,
12
+ * 新丢进来的插件无需重启服务即可出现在顶栏(import 只做一次并缓存)。
13
+ * - id 必须匹配 ID_RE,防路径穿越;client 静态服务同样逐段校验。
14
+ * - host 窄接口:broadcast(pluginId, payload) 广播 plugin_data、onMessage 注册
15
+ * 客户端上行处理、dataDir/cwd/log 环境。发送通道由 index.ts 注入(每个 socket
16
+ * 的 send 函数),插件本身不接触 ws。
17
+ * - activate 抛错只标记 error 字段并记日志,绝不影响主进程。
18
+ */
19
+ import { readdir, readFile, stat } from "node:fs/promises";
20
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
21
+ import { join, resolve, sep } from "node:path";
22
+ import { pathToFileURL } from "node:url";
23
+ import { PluginStorage, PluginSecrets, ensurePluginDeps, WorkspaceFS } from "./plugin-facilities.js";
24
+ import { createHash } from "node:crypto";
25
+ /** 合法插件 id:字母/数字/下划线/连字符,防路径穿越。 */
26
+ const ID_RE = /^[A-Za-z0-9_-]+$/;
27
+ /** 宿主提供的插件设施版本——manifest 声明的 apiVersion 高于此值则拒绝激活,
28
+ * 插件能拿到明确的「请升级 pi-harness」而不是在新接口上莫名 undefined。 */
29
+ export const PLUGIN_API_VERSION = 1;
30
+ /** 消息处理器超时:仅作为不再等待的日志阈值(响应由 handler 自己发出)。 */
31
+ const MESSAGE_TIMEOUT_MS = 30_000;
32
+ /** host.fs 被能力门控拒绝时的共享 rejected promise(类型对齐用)。 */
33
+ const NO_FS_PROMISE = Promise.reject(new Error('插件未声明能力 "fs"(manifest.permissions)——请求被拒'));
34
+ NO_FS_PROMISE.catch(() => { }); // 避免未处理 rejection 噪音;调用方 await 时拿到错误
35
+ // ---------------------------------------------------------------------------
36
+ // 声明式设置 schema(manifest "settings")
37
+ // ---------------------------------------------------------------------------
38
+ const SETTING_TYPES = new Set(["text", "password", "number", "boolean", "select"]);
39
+ /** 解析 manifest.settings → 合法 schema(坏字段跳过,最多 32 个)。 */
40
+ function parseSettingsSchema(raw) {
41
+ if (!Array.isArray(raw))
42
+ return [];
43
+ const out = [];
44
+ for (const f of raw) {
45
+ if (!f || typeof f !== "object")
46
+ continue;
47
+ const o = f;
48
+ const key = typeof o.key === "string" ? o.key.trim() : "";
49
+ const type = typeof o.type === "string" ? o.type : "";
50
+ if (!key || !SETTING_TYPES.has(type) || out.some((x) => x.key === key))
51
+ continue;
52
+ const field = {
53
+ key,
54
+ type: type,
55
+ label: typeof o.label === "string" && o.label ? o.label : key,
56
+ ...(o.default !== undefined ? { default: o.default } : {}),
57
+ ...(typeof o.min === "number" ? { min: o.min } : {}),
58
+ ...(typeof o.max === "number" ? { max: o.max } : {}),
59
+ ...(Array.isArray(o.options)
60
+ ? { options: o.options.filter((x) => typeof x === "string") }
61
+ : {}),
62
+ ...(typeof o.hint === "string" ? { hint: o.hint } : {}),
63
+ };
64
+ out.push(field);
65
+ if (out.length >= 32)
66
+ break;
67
+ }
68
+ return out;
69
+ }
70
+ /** 从 <pluginDir>/storage.json 读 settings 存值,按 schema 并默认值。 */
71
+ function storedSettingsValues(dir, schema) {
72
+ const out = {};
73
+ let stored = {};
74
+ try {
75
+ const parsed = JSON.parse(readFileSync(join(dir, "storage.json"), "utf8"));
76
+ if (parsed && typeof parsed === "object" && parsed.settings && typeof parsed.settings === "object") {
77
+ stored = parsed.settings;
78
+ }
79
+ }
80
+ catch {
81
+ /* 无存储文件 = 全默认 */
82
+ }
83
+ for (const f of schema)
84
+ out[f.key] = stored[f.key] ?? f.default;
85
+ return out;
86
+ }
87
+ /** 校验并写回 settings(storage.json 的 settings 键,原子写);返回错误信息或 null。 */
88
+ function saveSettingsValues(dir, schema, values) {
89
+ const clean = {};
90
+ for (const f of schema) {
91
+ const v = values?.[f.key];
92
+ if (f.type === "number") {
93
+ const n = v === undefined ? Number(f.default ?? 0) : Number(v);
94
+ if (!Number.isFinite(n) || (f.min !== undefined && n < f.min) || (f.max !== undefined && n > f.max)) {
95
+ return { error: `${f.label} 超出范围`, clean };
96
+ }
97
+ clean[f.key] = n;
98
+ }
99
+ else if (f.type === "boolean") {
100
+ clean[f.key] = v === undefined ? Boolean(f.default) : Boolean(v);
101
+ }
102
+ else if (f.type === "select") {
103
+ if (v !== undefined && !f.options?.includes(String(v)))
104
+ return { error: `${f.label} 值非法`, clean };
105
+ clean[f.key] = v === undefined ? f.default : String(v);
106
+ }
107
+ else {
108
+ clean[f.key] = v === undefined ? (f.default ?? "") : String(v);
109
+ }
110
+ }
111
+ try {
112
+ // 保留 storage.json 里其它键(插件自己的数据),只动 settings。
113
+ const file = join(dir, "storage.json");
114
+ let existing = {};
115
+ try {
116
+ existing = JSON.parse(readFileSync(file, "utf8"));
117
+ }
118
+ catch {
119
+ /* 首次 */
120
+ }
121
+ const tmp = `${file}.tmp-${process.pid}`;
122
+ writeFileSync(tmp, JSON.stringify({ ...existing, settings: clean }));
123
+ renameSync(tmp, file);
124
+ }
125
+ catch (err) {
126
+ console.error(`[plugins] settings persist failed (${dir}):`, err);
127
+ }
128
+ return { clean };
129
+ }
130
+ export class PluginManager {
131
+ dataDir;
132
+ loaded = new Map();
133
+ /** 已 import 过但无入口/失败的目录——避免重复 import 与重复报错。 */
134
+ attempted = new Set();
135
+ senders = new Set();
136
+ messageHandlers = new Map();
137
+ /** 插件常驻任务:pluginId → Map<taskId, PluginBgTask>。宿主经 bgTasks() 读取。 */
138
+ pluginBgTasks = new Map();
139
+ /** 任务集合变化回调(index.ts 接到 AgentService,重推 bg_servers)。 */
140
+ onBgTasksChanged = undefined;
141
+ /** 服务端重载纪元:每次 reload() +1,前端用作 import 缓存击穿参数。 */
142
+ epochCounter = 0;
143
+ /** 当前全局工作区(host.cwd 的背后存储)——随 notifyCwd 更新。 */
144
+ cwdValue;
145
+ constructor(dataDir, cwd) {
146
+ this.dataDir = dataDir;
147
+ this.cwdValue = resolve(cwd);
148
+ }
149
+ /** index.ts 在客户端 set_cwd 成功后调用:更新全局工作区并扇出给
150
+ * 所有已激活插件的 onCwdChange 钩子(异常隔离,不炸主进程)。 */
151
+ notifyCwd(next) {
152
+ const abs = resolve(next);
153
+ if (abs === this.cwdValue)
154
+ return; // 幂等:重复通知/同路径 no-op
155
+ this.cwdValue = abs;
156
+ for (const [id, p] of this.loaded) {
157
+ for (const h of p.cwdHandlers) {
158
+ try {
159
+ h(abs);
160
+ }
161
+ catch (err) {
162
+ console.error(`[plugin:${id}] cwd-change handler failed:`, err);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ get pluginsDir() {
168
+ return join(this.dataDir, "plugins");
169
+ }
170
+ /** 全部插件注册的斜杠命令(按插件 id 稳定排序)。 */
171
+ /** 按名查找命令(供 prompt() 拦截执行;找不到返回 null)。 */
172
+ /** 全部插件注册的常驻后台任务(扁平化为 BgServer 形状)。 */
173
+ bgTasks() {
174
+ const out = [];
175
+ for (const [pluginId, table] of this.pluginBgTasks) {
176
+ for (const t of table.values()) {
177
+ out.push({
178
+ taskId: t.id,
179
+ plugin: pluginId,
180
+ since: t.since,
181
+ name: t.label,
182
+ ...(t.status ? { status: t.status } : {}),
183
+ });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ /** 停止一个插件任务(kill_background_server with taskId);返回是否命中。 */
189
+ stopPluginBgTask(taskId) {
190
+ for (const [pluginId, table] of this.pluginBgTasks) {
191
+ const t = table.get(taskId);
192
+ if (!t)
193
+ continue;
194
+ try {
195
+ t.stop?.();
196
+ }
197
+ catch (err) {
198
+ console.error(`[plugin:${pluginId}] background task ${taskId} stop failed:`, err);
199
+ }
200
+ table.delete(taskId);
201
+ if (table.size === 0)
202
+ this.pluginBgTasks.delete(pluginId);
203
+ try {
204
+ this.onBgTasksChanged?.();
205
+ }
206
+ catch { }
207
+ return true;
208
+ }
209
+ return false;
210
+ }
211
+ /** 保存某插件的声明式设置(⚙ 面板 → plugin_settings 消息):按 schema 校验、
212
+ * 原子写 storage.json 的 settings 键、通知插件 onSettingsChanged、重推清单
213
+ * 让前端回显。返回错误信息或 null(成功)。 */
214
+ savePluginSettings(pluginId, values) {
215
+ if (!ID_RE.test(pluginId))
216
+ return { error: "非法的插件 id" };
217
+ const dir = join(this.pluginsDir, pluginId);
218
+ const info = this.loaded.get(pluginId)?.info;
219
+ const schema = info?.settingsSchema ?? [];
220
+ if (!schema.length)
221
+ return { error: "该插件没有声明式设置(manifest 未声明 settings)" };
222
+ const { error, clean } = saveSettingsValues(dir, schema, values);
223
+ if (error)
224
+ return { error };
225
+ // 通知插件(异常隔离)
226
+ for (const h of this.loaded.get(pluginId)?.settingsHandlers ?? []) {
227
+ try {
228
+ h(clean);
229
+ }
230
+ catch (err) {
231
+ console.error(`[plugin:${pluginId}] onSettingsChanged handler failed:`, err);
232
+ }
233
+ }
234
+ // 重推 plugins 清单(含新 settingsValues),前端回显。
235
+ void this.pushToAll().catch(() => { });
236
+ return {};
237
+ }
238
+ /** 当前重载纪元(随 plugins 消息下发)。 */
239
+ get epoch() {
240
+ return this.epochCounter;
241
+ }
242
+ addSender(send, cid) {
243
+ const s = { cid, send };
244
+ this.senders.add(s);
245
+ return () => this.senders.delete(s);
246
+ }
247
+ /** 客户端上行:路由给对应插件的处理器;未知/未激活的插件静默丢弃。
248
+ * 插件代码不可信——同步抛错与返回的 Promise rejection 都必须隔离在
249
+ * 这里,绝不能炸主进程。 */
250
+ handleMessage(pluginId, payload, from) {
251
+ if (!ID_RE.test(pluginId))
252
+ return;
253
+ const handlers = this.messageHandlers.get(pluginId);
254
+ if (!handlers)
255
+ return;
256
+ for (const h of handlers) {
257
+ try {
258
+ const ret = h(payload, from);
259
+ if (ret instanceof Promise) {
260
+ ret.catch((err) => {
261
+ console.error(`[plugin:${pluginId}] async message handler failed:`, err);
262
+ });
263
+ // 超时护栏:响应由 handler 自己 sendTo/broadcast 发出,超时只是记
264
+ // 日志不再等待——绝不能让单条消息把客户端 pending 管线无限拖死。
265
+ const timer = setTimeout(() => {
266
+ console.error(`[plugin:${pluginId}] message handler 超时(>${MESSAGE_TIMEOUT_MS}ms),已不再等待`);
267
+ }, MESSAGE_TIMEOUT_MS);
268
+ void ret.finally(() => clearTimeout(timer));
269
+ }
270
+ }
271
+ catch (err) {
272
+ console.error(`[plugin:${pluginId}] message handler failed:`, err);
273
+ }
274
+ }
275
+ }
276
+ /** 首次安装/能力变更时提醒在线用户(marker 文件记录上次激活时的声明)。 */
277
+ async maybeConsentNotice(info, dir, perms) {
278
+ try {
279
+ const markerFile = join(dir, ".pi-approved");
280
+ const key = createHash("sha256").update(JSON.stringify(perms)).digest("hex").slice(0, 32);
281
+ let prev = "";
282
+ try {
283
+ prev = JSON.parse(readFileSync(markerFile, "utf8"))?.key ?? "";
284
+ }
285
+ catch {
286
+ /* 无 marker = 首次安装 */
287
+ }
288
+ if (prev === key)
289
+ return; // 同版本能力清单,不再打扰
290
+ const list = perms.length ? perms.join(", ") : "无";
291
+ this.notifyAll(perms.length ? "warning" : "info", `插件「${info.name}」已激活(${prev ? "能力清单变更" : "首次安装"};声明能力:${list})——请确认来源可信`);
292
+ writeFileSync(markerFile, JSON.stringify({ v: 1, key, perms }), "utf8");
293
+ }
294
+ catch (err) {
295
+ console.error(`[plugin:${info.id}] consent notice failed:`, err);
296
+ }
297
+ }
298
+ /** index.ts 的 /plugins-api/:id/* 挂载点转发到这里:找到对应插件的已注册
299
+ * 路由并执行;未知插件/路径 → 404,handler 抛错 → 500(不炸进程)。 */
300
+ handleHttp(pluginId, method, pathIn, req, res) {
301
+ if (!ID_RE.test(pluginId)) {
302
+ res.status(404).end("plugin not found");
303
+ return;
304
+ }
305
+ const table = this.loaded.get(pluginId)?.httpRoutes;
306
+ const path = "/" + pathIn.replace(/^\/+/, "");
307
+ const handler = table?.get(`${method.toUpperCase()} ${path}`);
308
+ if (!handler) {
309
+ res.status(404).end("not found");
310
+ return;
311
+ }
312
+ try {
313
+ handler(req, res);
314
+ }
315
+ catch (err) {
316
+ console.error(`[plugin:${pluginId}] http ${method} ${path} failed:`, err);
317
+ if (!res.headersSent)
318
+ res.status(500).end("internal error");
319
+ else
320
+ res.end();
321
+ }
322
+ }
323
+ broadcast(pluginId, payload) {
324
+ this.deliverAll({ type: "plugin_data", pluginId, payload });
325
+ }
326
+ /** 系统通知:发给所有 socket(复用 notice 消息,前端 toast 展示)。 */
327
+ notifyAll(level, text) {
328
+ this.deliverAll({ type: "notice", level, text });
329
+ }
330
+ /** 给指定客户端定向发一条插件消息;找不到该 socket 时静默忽略。 */
331
+ sendTo(clientId, pluginId, payload) {
332
+ for (const s of this.senders) {
333
+ if (s.cid() !== clientId)
334
+ continue;
335
+ try {
336
+ s.send({ type: "plugin_data", pluginId, payload });
337
+ }
338
+ catch {
339
+ /* dead socket */
340
+ }
341
+ }
342
+ }
343
+ /** 目录清单 + 当前 epoch 推给所有 socket。 */
344
+ async pushToAll() {
345
+ const list = await this.scan();
346
+ this.deliverAll({ type: "plugins", plugins: list, epoch: this.epochCounter });
347
+ }
348
+ /** 服务端热重载:反激活全部 → 清缓存 → 重扫重激活 → epoch+1。
349
+ * 返回新目录清单(含激活结果)。重激活后的插件实例是新模块,
350
+ * 内存状态为初始值——逐个客户端触发 onAttach 让它们重推自身状态。 */
351
+ async reload() {
352
+ this.dispose();
353
+ this.attempted.clear();
354
+ this.epochCounter += 1;
355
+ const list = await this.ensureLoaded();
356
+ for (const s of this.senders) {
357
+ const cid = s.cid();
358
+ if (cid)
359
+ this.notifyAttach(cid);
360
+ }
361
+ return list;
362
+ }
363
+ /** 每个客户端 attach 后调用:让各插件向该客户端推送自身完整状态。
364
+ * 异常隔离——单个插件钩子报错不影响其他插件与其他钩子。 */
365
+ notifyAttach(clientId) {
366
+ for (const [id, p] of this.loaded) {
367
+ for (const h of p.attachHandlers) {
368
+ try {
369
+ h(clientId);
370
+ }
371
+ catch (err) {
372
+ console.error(`[plugin:${id}] onAttach handler failed:`, err);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ /** agent-service 调:把 SDK 工具执行事件扇出给所有插件(异常隔离)。 */
378
+ emitToolEvent(ev) {
379
+ for (const p of this.loaded.values()) {
380
+ for (const h of p.toolHandlers) {
381
+ try {
382
+ h(ev);
383
+ }
384
+ catch (err) {
385
+ console.error(`[plugin:${p.info.id}] tool-event handler failed:`, err);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ /** 注册一个供 AI 调用的工具;重名拒绝并返回空操作注销函数。 */
391
+ deliverAll(msg) {
392
+ for (const s of this.senders) {
393
+ try {
394
+ s.send(msg);
395
+ }
396
+ catch {
397
+ /* dead socket — index.ts cleans it up */
398
+ }
399
+ }
400
+ }
401
+ /** 当前目录清单(重扫 manifest,不重新 import)。 */
402
+ async list() {
403
+ return this.scan();
404
+ }
405
+ /**
406
+ * attach 时调用:重扫目录 + 激活尚未加载的新插件。
407
+ * 返回给浏览器的目录(含激活失败的条目,前端显示为不可用)。
408
+ */
409
+ async ensureLoaded() {
410
+ const found = await this.scan();
411
+ for (const info of found) {
412
+ if (this.loaded.has(info.id) || this.attempted.has(info.id))
413
+ continue;
414
+ if (!existsSync(join(this.pluginsDir, info.id, "index.mjs")))
415
+ continue; // 纯前端插件
416
+ await this.activate(info);
417
+ }
418
+ // 已被删除的插件:调用 deactivate 并移出缓存
419
+ for (const [id, p] of [...this.loaded]) {
420
+ if (!found.some((f) => f.id === id)) {
421
+ this.deactivateEntry(id, p);
422
+ }
423
+ }
424
+ return found.map((f) => this.loaded.get(f.id)?.info ?? f);
425
+ }
426
+ /** 反激活单个插件:deactivate + 注销 AI 工具 + 清缓存。 */
427
+ deactivateEntry(id, p) {
428
+ try {
429
+ p.deactivate?.();
430
+ }
431
+ catch (err) {
432
+ console.error(`[plugin:${id}] deactivate failed:`, err);
433
+ }
434
+ this.loaded.delete(id);
435
+ this.messageHandlers.delete(id);
436
+ console.log(`[plugin:${id}] removed`);
437
+ }
438
+ /** 关机时反激活全部插件。 */
439
+ dispose() {
440
+ for (const [id, p] of this.loaded) {
441
+ try {
442
+ p.deactivate?.();
443
+ }
444
+ catch (err) {
445
+ console.error(`[plugin:${id}] deactivate failed:`, err);
446
+ }
447
+ // 反激活时停掉它注册的常驻后台任务(轮询器等),不留孤儿计时器。
448
+ for (const t of this.pluginBgTasks.get(id)?.values() ?? []) {
449
+ try {
450
+ t.stop?.();
451
+ }
452
+ catch { }
453
+ }
454
+ }
455
+ this.pluginBgTasks.clear();
456
+ this.loaded.clear();
457
+ this.messageHandlers.clear();
458
+ }
459
+ /** 读 manifest 清单;坏目录(无 manifest/id 非法)直接跳过。 */
460
+ async scan() {
461
+ let names;
462
+ try {
463
+ names = await readdir(this.pluginsDir);
464
+ }
465
+ catch {
466
+ return []; // 目录不存在 = 没装任何插件
467
+ }
468
+ const out = [];
469
+ for (const name of names.sort()) {
470
+ if (!ID_RE.test(name))
471
+ continue;
472
+ const dir = join(this.pluginsDir, name);
473
+ try {
474
+ if (!(await stat(dir)).isDirectory())
475
+ continue;
476
+ const raw = await readFile(join(dir, "manifest.json"), "utf8");
477
+ const m = JSON.parse(raw);
478
+ out.push({
479
+ id: name,
480
+ name: typeof m.name === "string" && m.name ? m.name : name,
481
+ version: typeof m.version === "string" ? m.version : undefined,
482
+ description: typeof m.description === "string" ? m.description : undefined,
483
+ icon: typeof m.icon === "string" && m.icon.trim() ? m.icon.trim() : undefined,
484
+ hasClient: existsSync(join(dir, "client", "entry.mjs")),
485
+ error: this.loaded.get(name)?.info.error,
486
+ // manifest 声明的能力清单(fs/net/tools…)——设置面板展示用
487
+ permissions: Array.isArray(m.permissions)
488
+ ? m.permissions.filter((p) => typeof p === "string" && p.length > 0).slice(0, 16)
489
+ : undefined,
490
+ // 声明式设置 schema + 当前存值(⚙ 面板自动渲染表单用)
491
+ settingsSchema: parseSettingsSchema(m.settings),
492
+ settingsValues: storedSettingsValues(dir, parseSettingsSchema(m.settings)),
493
+ // 安装来源(pi-harness install 写入的 .pi-source.json)——
494
+ // 设置面板据此显示「更新」按钮;手工拷入的插件没有此文件。
495
+ source: await readFile(join(dir, ".pi-source.json"), "utf8")
496
+ .then((raw) => {
497
+ try {
498
+ const s = JSON.parse(raw);
499
+ return typeof s.source === "string" && s.source ? s.source : undefined;
500
+ }
501
+ catch {
502
+ return undefined;
503
+ }
504
+ })
505
+ .catch(() => undefined),
506
+ });
507
+ }
508
+ catch {
509
+ continue; // 无 manifest / JSON 坏 —— 不是插件
510
+ }
511
+ }
512
+ return out;
513
+ }
514
+ async activate(info) {
515
+ this.attempted.add(info.id);
516
+ const dir = join(this.pluginsDir, info.id);
517
+ const handlers = new Set();
518
+ this.messageHandlers.set(info.id, handlers);
519
+ const toolHandlers = new Set();
520
+ const attachHandlers = new Set();
521
+ const cwdHandlers = new Set();
522
+ const httpRoutes = new Map();
523
+ const bgTaskTable = new Map();
524
+ const settingsHandlers = new Set();
525
+ // 宿主 API 版本协商:插件要的比宿主新 → 明确拒绝(而不是让它在运行期
526
+ // 撞 undefined 接口莫名其妙地坏)。与激活失败同一处理:error 字段 + 置灰。
527
+ let apiVersion = 1;
528
+ try {
529
+ apiVersion = Number(JSON.parse(readFileSync(join(dir, "manifest.json"), "utf8")).apiVersion ?? 1) || 1;
530
+ }
531
+ catch { }
532
+ if (apiVersion > PLUGIN_API_VERSION) {
533
+ const msg = `插件要求宿主 API v${apiVersion},当前宿主 v${PLUGIN_API_VERSION} —— 请升级 pi-harness`;
534
+ console.error(`[plugin:${info.id}] ${msg}`);
535
+ this.loaded.set(info.id, { info: { ...info, error: msg }, toolHandlers, attachHandlers, cwdHandlers, httpRoutes, settingsHandlers: new Set() });
536
+ return;
537
+ }
538
+ // 能力声明:写了 permissions → 严格模式(受控宿主 API 按声明族强制执行);
539
+ // 未写且 apiVersion < 2 → 旧全权模式(首次使用受控 API 时警告一次,v2 起默认拒绝)。
540
+ const permsDeclared = (info.permissions ?? []).slice();
541
+ const strict = permsDeclared.length > 0 || apiVersion >= 2;
542
+ const permFamilies = new Set(permsDeclared.map((x) => x.split(":")[0]));
543
+ const p = { info, toolHandlers, attachHandlers, cwdHandlers, httpRoutes, settingsHandlers };
544
+ p.permsDeclared = permsDeclared;
545
+ p.permFamilies = permFamilies;
546
+ p.legacyWarned = false;
547
+ // 每插件的私有设施:KV 存储 + 加密 secrets + 依赖自动补装(单飞)。
548
+ const storage = new PluginStorage(join(dir, "storage.json"));
549
+ const secrets = new PluginSecrets(this.dataDir, dir);
550
+ // 受限工作区文件访问(能力 "fs" 门控;根随 set_cwd 活值移动)。
551
+ const workspaceFs = new WorkspaceFS(() => self.cwdValue);
552
+ /** 能力门控:严格模式下查声明族;旧模式放行但每个激活期只警告一次。
553
+ * 返回 false = 已记日志,调用方应拒绝。 */
554
+ const can = (family) => {
555
+ if (permFamilies.has(family))
556
+ return true;
557
+ if (!strict) {
558
+ if (!p.legacyWarned) {
559
+ p.legacyWarned = true;
560
+ console.warn(`[plugin:${info.id}] manifest 未声明 permissions(旧格式全权模式)——已放行 "${family}";apiVersion 2 起将默认拒绝,请尽快声明`);
561
+ }
562
+ return true;
563
+ }
564
+ console.error(`[plugin:${info.id}] 缺少能力声明 "${family}"(manifest.permissions)——请求被拒`);
565
+ return false;
566
+ };
567
+ const self = this; // 对象字面量 getter 里不能用插件宿主的 this
568
+ const host = {
569
+ broadcast: (payload) => this.broadcast(info.id, payload),
570
+ notify: (level, text) => this.notifyAll(level, text),
571
+ sendTo: (clientId, payload) => this.sendTo(clientId, info.id, payload),
572
+ onMessage: (h) => {
573
+ handlers.add(h);
574
+ return () => handlers.delete(h);
575
+ },
576
+ onToolEvent: (h) => {
577
+ toolHandlers.add(h);
578
+ return () => toolHandlers.delete(h);
579
+ },
580
+ onAttach: (h) => {
581
+ attachHandlers.add(h);
582
+ return () => attachHandlers.delete(h);
583
+ },
584
+ onCwdChange: (h) => {
585
+ cwdHandlers.add(h);
586
+ return () => cwdHandlers.delete(h);
587
+ },
588
+ storage,
589
+ secrets,
590
+ ensureDeps: (specs, opts) => ensurePluginDeps(dir, specs ?? [], opts?.onProgress),
591
+ route: (method, path, handler) => {
592
+ if (!can("http"))
593
+ return () => { };
594
+ const m = String(method ?? "GET").toUpperCase();
595
+ if (!["GET", "POST", "PUT", "DELETE"].includes(m) || typeof path !== "string" || !path.startsWith("/") || typeof handler !== "function") {
596
+ console.error(`[plugin:${info.id}] route: 非法参数(method=${method} path=${path}),忽略`);
597
+ return () => { };
598
+ }
599
+ httpRoutes.set(`${m} ${path}`, handler);
600
+ return () => httpRoutes.delete(`${m} ${path}`);
601
+ },
602
+ // 包一层:插件反激活时自动注销它注册的全部 AI 工具,不留悬挂项。
603
+ dir,
604
+ dataDir: this.dataDir,
605
+ get cwd() {
606
+ return self.cwdValue;
607
+ },
608
+ fs: {
609
+ list: (relDir) => (can("fs") ? workspaceFs.list(relDir) : NO_FS_PROMISE),
610
+ read: (p) => (can("fs") ? workspaceFs.read(p) : NO_FS_PROMISE),
611
+ readText: (p, max) => (can("fs") ? workspaceFs.readText(p, max) : NO_FS_PROMISE),
612
+ write: (p, data) => (can("fs") ? workspaceFs.write(p, data) : NO_FS_PROMISE),
613
+ remove: (p) => (can("fs") ? workspaceFs.remove(p) : NO_FS_PROMISE),
614
+ },
615
+ registerBackgroundTask: (task) => {
616
+ const id = String(task?.id ?? "").trim();
617
+ if (!id || bgTaskTable.has(id)) {
618
+ console.error(`[plugin:${info.id}] registerBackgroundTask: 非法/重复 id「${task?.id}」,忽略`);
619
+ return { update: () => { }, unregister: () => { } };
620
+ }
621
+ const entry = {
622
+ id,
623
+ label: String(task?.label ?? id),
624
+ since: Date.now(),
625
+ ...(typeof task?.stop === "function" ? { stop: task.stop } : {}),
626
+ ...(typeof task?.status === "string" ? { status: task.status } : {}),
627
+ };
628
+ bgTaskTable.set(id, entry);
629
+ this.pluginBgTasks.set(info.id, bgTaskTable);
630
+ const fire = () => {
631
+ try {
632
+ this.onBgTasksChanged?.();
633
+ }
634
+ catch { }
635
+ };
636
+ fire();
637
+ return {
638
+ update: (next) => {
639
+ if (!bgTaskTable.has(id))
640
+ return;
641
+ if (next.label !== undefined)
642
+ entry.label = String(next.label);
643
+ if (next.status !== undefined)
644
+ entry.status = next.status;
645
+ if (typeof next.stop === "function")
646
+ entry.stop = next.stop;
647
+ fire();
648
+ },
649
+ unregister: () => {
650
+ if (bgTaskTable.delete(id)) {
651
+ if (bgTaskTable.size === 0)
652
+ this.pluginBgTasks.delete(info.id);
653
+ fire();
654
+ }
655
+ },
656
+ };
657
+ },
658
+ getSettings: () => storedSettingsValues(dir, info.settingsSchema ?? []),
659
+ onSettingsChanged: (h) => {
660
+ settingsHandlers.add(h);
661
+ return () => settingsHandlers.delete(h);
662
+ },
663
+ log: (...args) => console.log(`[plugin:${info.id}]`, ...args),
664
+ };
665
+ try {
666
+ // Node 对同一 URL 的 import() 永远返回缓存模块——追加 epoch 作查询串
667
+ // 击穿缓存,让 plugins_reload 后的重新激活能拿到磁盘上的新代码。
668
+ const mod = (await import(pathToFileURL(join(dir, "index.mjs")).href + `?e=${this.epochCounter}`));
669
+ const ret = await mod.default?.activate?.(host);
670
+ this.loaded.set(info.id, {
671
+ info: { ...info },
672
+ deactivate: typeof ret === "function" ? ret : undefined,
673
+ toolHandlers,
674
+ attachHandlers,
675
+ cwdHandlers,
676
+ httpRoutes,
677
+ settingsHandlers,
678
+ });
679
+ console.log(`[plugin:${info.id}] activated (v${info.version ?? "?"})`);
680
+ // 首次安装/能力变更提醒(尽力而为):<dir>/.pi-approved 记录上次激活时
681
+ // 的能力清单——新装或 permissions 变更后向在线客户端推一条警告通知,
682
+ // 用户装前可见、日常启动不打扰。
683
+ void this.maybeConsentNotice(info, dir, permsDeclared);
684
+ }
685
+ catch (err) {
686
+ httpRoutes.clear();
687
+ this.loaded.set(info.id, {
688
+ info: { ...info, error: err.message },
689
+ toolHandlers,
690
+ attachHandlers,
691
+ cwdHandlers,
692
+ httpRoutes,
693
+ settingsHandlers: new Set(),
694
+ });
695
+ console.error(`[plugin:${info.id}] activate failed:`, err);
696
+ }
697
+ }
698
+ }
699
+ /**
700
+ * 把 /plugins/:id/client/<rest> 安全映射到 <pluginsDir>/<id>/client/<rest>。
701
+ * 返回绝对路径;任何越界/非法 id 返回 null(调用方回 404)。
702
+ */
703
+ export function resolvePluginClientFile(pluginsDir, id, rest) {
704
+ if (!ID_RE.test(id))
705
+ return null;
706
+ const root = resolve(join(pluginsDir, id, "client"));
707
+ // rest 由 express 路由保证不带 "..",但双保险:resolve 后必须仍在 root 内
708
+ const abs = resolve(root, rest);
709
+ if (abs !== root && !abs.startsWith(root + sep))
710
+ return null;
711
+ return abs;
712
+ }