@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.
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/bin/pi-harness.mjs +1819 -0
- package/deploy/com.youweichen.pi-harness.plist +48 -0
- package/deploy/nginx-subpath.conf +88 -0
- package/deploy/pi-harness-task.xml +71 -0
- package/deploy/pi-harness.service +31 -0
- package/dist/server/agent-service.js +3051 -0
- package/dist/server/app-version.js +19 -0
- package/dist/server/attachments.js +526 -0
- package/dist/server/auth-file.js +61 -0
- package/dist/server/bg-servers.js +154 -0
- package/dist/server/client-eviction.js +19 -0
- package/dist/server/client-id.js +9 -0
- package/dist/server/client-state.js +202 -0
- package/dist/server/codemode-image-routes.js +36 -0
- package/dist/server/codemode-presentation.js +19 -0
- package/dist/server/component-updates.js +135 -0
- package/dist/server/context-breakdown.js +56 -0
- package/dist/server/control-socket.js +177 -0
- package/dist/server/conversation.js +65 -0
- package/dist/server/dir-browser.js +77 -0
- package/dist/server/editor-snapshot.js +38 -0
- package/dist/server/ensure-bash.js +80 -0
- package/dist/server/extension-display.js +23 -0
- package/dist/server/extensions-catalog.js +64 -0
- package/dist/server/extensions-model.js +46 -0
- package/dist/server/extensions-routes.js +255 -0
- package/dist/server/extensions-worker.js +187 -0
- package/dist/server/files-service.js +795 -0
- package/dist/server/index.js +1029 -0
- package/dist/server/markdown-images.js +42 -0
- package/dist/server/model-admin.js +801 -0
- package/dist/server/model-config-merge.js +32 -0
- package/dist/server/native-mcp-config.js +103 -0
- package/dist/server/native-mcp-presentation.js +12 -0
- package/dist/server/native-prompt-template.js +12 -0
- package/dist/server/native-tools.js +32 -0
- package/dist/server/node-sources.js +142 -0
- package/dist/server/node-workbench.js +668 -0
- package/dist/server/origin-policy.js +50 -0
- package/dist/server/patch-node-pty.js +96 -0
- package/dist/server/pi-environment.js +203 -0
- package/dist/server/plan/extension.js +74 -0
- package/dist/server/plan/progress.js +90 -0
- package/dist/server/plan/settings.js +41 -0
- package/dist/server/plan/state.js +113 -0
- package/dist/server/plugin-facilities.js +304 -0
- package/dist/server/plugin-updater.js +245 -0
- package/dist/server/plugins.js +712 -0
- package/dist/server/private-file.js +28 -0
- package/dist/server/process-utils.js +112 -0
- package/dist/server/prompt-delivery.js +80 -0
- package/dist/server/protocol-version.js +11 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/provider-auth.js +99 -0
- package/dist/server/query-cache.js +45 -0
- package/dist/server/recovery-state.js +37 -0
- package/dist/server/scm.js +459 -0
- package/dist/server/security-headers.js +24 -0
- package/dist/server/serialize.js +254 -0
- package/dist/server/session-file-read.js +225 -0
- package/dist/server/session-record-worker.js +21 -0
- package/dist/server/session-reload.js +13 -0
- package/dist/server/session-tree-controller.js +250 -0
- package/dist/server/session-tree.js +191 -0
- package/dist/server/settings-service.js +53 -0
- package/dist/server/skills-service.js +87 -0
- package/dist/server/slash-commands.js +280 -0
- package/dist/server/sqlite-preview.js +58 -0
- package/dist/server/sqlite-query.js +67 -0
- package/dist/server/sqlite-worker.js +12 -0
- package/dist/server/system-prompt-files.js +158 -0
- package/dist/server/system-prompt-routes.js +40 -0
- package/dist/server/system-prompt-view.js +49 -0
- package/dist/server/task-progress.js +233 -0
- package/dist/server/terminals.js +833 -0
- package/dist/server/text-sniff.js +269 -0
- package/dist/server/thinking-timing.js +91 -0
- package/dist/server/token-auth.js +37 -0
- package/dist/server/tool-output.js +97 -0
- package/dist/server/tool-text-incidents.js +82 -0
- package/dist/server/tool-text.js +67 -0
- package/dist/server/uploads.js +112 -0
- package/dist/server/user-attachments.js +96 -0
- package/dist/server/webui-context.js +257 -0
- package/dist/server/wiki-links.js +136 -0
- package/dist/server/wiki-pdf-worker.js +23 -0
- package/dist/server/wiki-pdf.js +36 -0
- package/dist/server/wiki-routes.js +177 -0
- package/dist/server/wiki-service.js +511 -0
- package/electron/main.mjs +607 -0
- package/extensions/webui.ts +192 -0
- package/package.json +122 -4
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/NodeWorkbench-C4U1PldE.js +3 -0
- package/web/dist/assets/TerminalPanel-CWh6vw_N.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-D5Ti0Kpa.js +1 -0
- package/web/dist/assets/arc-DKJilLd6.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-Cf0w38g-.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-BOTLWLIL.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CLCCoJs3.js +38 -0
- package/web/dist/assets/channel-D8Hd7T4P.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-BHjga2Ge.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-CQJ5VfDZ.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-C67qoPJH.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-CgXXGwI8.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-D0RPyByP.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-BeYC9s9x.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-eQPl4UBQ.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-DT73arPU.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-B4FwQcP-.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-B_btgYRB.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-B_btgYRB.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Bnn5nlQN.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-CStWX9kE.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-BMVHC03f.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-ClSaO4ml.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-C43LL08C.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-BPVn_ySW.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C34FFhRi.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-BRk4bncf.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-O5hYPLEd.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-DVGEBm99.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-M-BLpTEV.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-BwdLopEx.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-DYxx2ppA.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-DUU0wMYB.js +106 -0
- package/web/dist/assets/index-CI2KfVSr.css +10 -0
- package/web/dist/assets/index-DQcpggFU.js +275 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-CFaW5C0C.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U--ENP1MTB.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-DgnAzSTj.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-_STZN1vU.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-CkUClkXt.js +1 -0
- package/web/dist/assets/linear-sYHiUOxa.js +1 -0
- package/web/dist/assets/markdown-vNTCZJ6d.js +51 -0
- package/web/dist/assets/mermaid.core-B8fKGWsc.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-DP0BYOEx.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-DkWTmZre.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-DfZgMV4u.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-Djl4TT7q.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-DbKF6zXn.js +1 -0
- package/web/dist/assets/react-BNj3n7jX.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-idola5qb.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-KA0hQOle.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BXSKvxzP.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-C_Ewv3S5.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-DIf5X8oo.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DYwe_2mB.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-Cx_Rc8M2.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-52Y8tgRV.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-BRrDhWVz.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-Ad6G0YwD.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-X6-ENpSi.js +78 -0
- package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
- package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-DhegNS6d.js +7 -0
- package/web/dist/brand-mark.svg +5 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.svg +5 -0
- package/web/dist/fonts/WIKI-FONTS.md +5 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/LICENSE +93 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/dist/icon/1a-mark.svg +1 -0
- package/web/dist/icon.ico +0 -0
- package/web/dist/index.html +18 -0
- package/web/public/apple-touch-icon.png +0 -0
- package/web/public/brand-mark.svg +5 -0
- package/web/public/favicon-32.png +0 -0
- package/web/public/favicon.svg +5 -0
- package/web/public/fonts/WIKI-FONTS.md +5 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/public/fonts/source-serif-4/LICENSE +93 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/public/icon/1a-mark.svg +1 -0
- 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
|
+
}
|