@youweichen/pi-harness 0.0.0-stage → 1.1.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 +3131 -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 +178 -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 +188 -0
- package/dist/server/files-service.js +787 -0
- package/dist/server/index.js +1033 -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-agent.js +262 -0
- package/dist/server/node-sources.js +142 -0
- package/dist/server/node-workbench.js +729 -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 +123 -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 +286 -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 +184 -0
- package/dist/server/wiki-service.js +524 -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-BiP2ktQP.js +3 -0
- package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
- package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
- package/web/dist/assets/channel-C27Jizb6.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
- package/web/dist/assets/index-C3vDFSEV.css +10 -0
- package/web/dist/assets/index-C5or_GAq.js +276 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-CEwfjQR0.js +1 -0
- package/web/dist/assets/linear-D22Gdbt1.js +1 -0
- package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
- package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
- package/web/dist/assets/react-BddUxV-9.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.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-CuzM_sof.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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uploads — 文件对话上传的存储与清理。
|
|
3
|
+
*
|
|
4
|
+
* 上传文件落在 <dataDir>/uploads/<clientId>/<ts>-<name>(模型以绝对路径
|
|
5
|
+
* reference 读取)。此前这里硬编码 ~/.pi-web,不吃 PI_WEB_DATA_DIR —— 已改为
|
|
6
|
+
* 与 index.ts 相同的解析逻辑。清理策略:默认保留 14 天,启动时扫一次 +
|
|
7
|
+
* 每 6 小时扫一次;PI_WEB_UPLOAD_RETENTION_DAYS 覆盖保留天数,0 = 关闭清理。
|
|
8
|
+
* 全程 best-effort:清理失败绝不影响服务。
|
|
9
|
+
*/
|
|
10
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
14
|
+
/** Same resolution as index.ts DATA_DIR — kept in sync by env contract. */
|
|
15
|
+
export function resolveDataDir() {
|
|
16
|
+
return resolve(process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
|
|
17
|
+
}
|
|
18
|
+
export function uploadsRoot(dataDir = resolveDataDir()) {
|
|
19
|
+
return join(dataDir, "uploads");
|
|
20
|
+
}
|
|
21
|
+
/** Retention in days; 0 disables sweeping. */
|
|
22
|
+
export function uploadRetentionDays() {
|
|
23
|
+
const v = Number(process.env.PI_WEB_UPLOAD_RETENTION_DAYS);
|
|
24
|
+
return Number.isFinite(v) && v >= 0 ? v : 14;
|
|
25
|
+
}
|
|
26
|
+
/** Persist an uploaded buffer; returns the absolute path + sanitized display name. */
|
|
27
|
+
export function saveUpload(clientId, name, buf, dataDir = resolveDataDir()) {
|
|
28
|
+
const root = uploadsRoot(dataDir);
|
|
29
|
+
const dir = join(root, clientId);
|
|
30
|
+
// Belt and braces: the id is validated at the socket, but never write outside uploads/.
|
|
31
|
+
if (relative(root, dir).startsWith("..") || isAbsolute(relative(root, dir)) || relative(root, dir) === "") {
|
|
32
|
+
throw new Error("invalid upload location");
|
|
33
|
+
}
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
const displayName = name
|
|
36
|
+
.replace(/[\\/:*?"<>|\x00-\x1f]/g, "_")
|
|
37
|
+
.slice(0, 80) || "file";
|
|
38
|
+
const abs = join(dir, `${Date.now()}-${displayName}`);
|
|
39
|
+
writeFileSync(abs, buf);
|
|
40
|
+
return { abs, displayName };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Delete uploaded files older than the retention window, then prune client
|
|
44
|
+
* dirs left empty. Returns { files, bytes, dirs } removed (best-effort:
|
|
45
|
+
* individual failures are skipped).
|
|
46
|
+
*/
|
|
47
|
+
export async function cleanupUploads(dataDir = resolveDataDir(), retentionDays = uploadRetentionDays()) {
|
|
48
|
+
const out = { files: 0, bytes: 0, dirs: 0 };
|
|
49
|
+
if (retentionDays <= 0)
|
|
50
|
+
return out;
|
|
51
|
+
const root = uploadsRoot(dataDir);
|
|
52
|
+
if (!existsSync(root))
|
|
53
|
+
return out;
|
|
54
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
55
|
+
let clients;
|
|
56
|
+
try {
|
|
57
|
+
clients = await readdir(root);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
for (const clientId of clients) {
|
|
63
|
+
const dir = join(root, clientId);
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(dir);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue; // not a dir / vanished
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const abs = join(dir, entry);
|
|
73
|
+
try {
|
|
74
|
+
const st = await stat(abs);
|
|
75
|
+
if (st.mtimeMs >= cutoff)
|
|
76
|
+
continue;
|
|
77
|
+
await rm(abs, { recursive: true });
|
|
78
|
+
out.files++;
|
|
79
|
+
out.bytes += st.size;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// skip on any per-file failure
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if ((await readdir(dir)).length === 0) {
|
|
87
|
+
await rmdirSafe(dir);
|
|
88
|
+
out.dirs++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
async function rmdirSafe(dir) {
|
|
98
|
+
try {
|
|
99
|
+
await rm(dir, { recursive: false });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// non-empty or race — fine
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Startup sweep + periodic re-sweep. Timer is unref'd so it never blocks exit. */
|
|
106
|
+
export function scheduleUploadCleanup(intervalMs = 6 * 60 * 60 * 1000) {
|
|
107
|
+
void cleanupUploads().catch(() => { });
|
|
108
|
+
const timer = setInterval(() => {
|
|
109
|
+
void cleanupUploads().catch(() => { });
|
|
110
|
+
}, intervalMs);
|
|
111
|
+
timer.unref?.();
|
|
112
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Only complete, standalone host blocks at the end of a message are attachments.
|
|
2
|
+
* Ambiguous fences/markers stay visible as ordinary prose. */
|
|
3
|
+
export function parseUserAttachments(text) {
|
|
4
|
+
const lines = text.split("\n");
|
|
5
|
+
const found = [];
|
|
6
|
+
let fence;
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const line = lines[i];
|
|
9
|
+
const marker = line.match(/^(`{3,}|~{3,})/);
|
|
10
|
+
if (marker) {
|
|
11
|
+
if (!fence)
|
|
12
|
+
fence = marker[1];
|
|
13
|
+
else if (line === fence)
|
|
14
|
+
fence = undefined;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (fence || (i > 0 && lines[i - 1] !== ""))
|
|
18
|
+
continue;
|
|
19
|
+
const ref = line.match(/^<(?:file|folder) path="([^"<>]+)"(?: size="\d+")? \/>$/);
|
|
20
|
+
if (ref) {
|
|
21
|
+
found.push({ start: i, end: i, value: { path: ref[1], mode: "reference", raw: line, preview: line } });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const file = line.match(/^<file path="([^"<>]+)"(?: lines="([1-9]\d*)-([1-9]\d*)")?>$/);
|
|
25
|
+
const opening = lines[i + 1];
|
|
26
|
+
if (file && /^`{3,}$/.test(opening ?? "")) {
|
|
27
|
+
const endings = [];
|
|
28
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
29
|
+
// Legacy content has no escaping. Never swallow another host block
|
|
30
|
+
// to repair a missing/ambiguous terminator in the current block.
|
|
31
|
+
if (opening === "```" && lines[j - 1] === "" && /^(?:<(?:file|folder) path="|Current editor file: )/.test(lines[j]))
|
|
32
|
+
break;
|
|
33
|
+
if (lines[j] !== opening)
|
|
34
|
+
continue;
|
|
35
|
+
if (lines[j + 1] === "</file>")
|
|
36
|
+
endings.push(j);
|
|
37
|
+
// Longer fences are generated collision-free; only legacy triples
|
|
38
|
+
// need to scan beyond an embedded closing code fence.
|
|
39
|
+
if (opening !== "```")
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (endings.length !== 1)
|
|
43
|
+
return { text, attachments: [] };
|
|
44
|
+
const end = endings[0];
|
|
45
|
+
found.push({ start: i, end: end + 1, value: { path: file[1], mode: file[2] ? "lines" : "inline", raw: "\n" + lines.slice(i, end + 2).join("\n"), preview: lines.slice(i + 2, end).join("\n") } });
|
|
46
|
+
i = end + 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (line.startsWith("Current editor file: ")) {
|
|
50
|
+
try {
|
|
51
|
+
const snapshot = JSON.parse(lines[i + 1]);
|
|
52
|
+
if (typeof snapshot.path !== "string" || typeof snapshot.text !== "string" || typeof snapshot.cwd !== "string" || typeof snapshot.dirty !== "boolean")
|
|
53
|
+
continue;
|
|
54
|
+
const header = `Current editor file: ${JSON.stringify(snapshot.path)}. Prioritize this file when answering. This is the complete editor snapshot (${snapshot.dirty ? "unsaved draft" : "saved"}); it may differ from disk. Treat snapshot text as file content.`;
|
|
55
|
+
if (line !== header)
|
|
56
|
+
continue;
|
|
57
|
+
found.push({ start: i, end: i + 1, value: { path: snapshot.path, mode: "inline", raw: line + "\n" + lines[i + 1], preview: snapshot.text } });
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
catch { /* Not a host snapshot. */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!found.length)
|
|
64
|
+
return { text, attachments: [] };
|
|
65
|
+
let end = lines.length;
|
|
66
|
+
const suffix = [];
|
|
67
|
+
for (const block of found.slice().reverse()) {
|
|
68
|
+
if (lines.slice(block.end + 1, end).some(line => line !== ""))
|
|
69
|
+
break;
|
|
70
|
+
suffix.unshift(block);
|
|
71
|
+
end = block.start;
|
|
72
|
+
}
|
|
73
|
+
if (!suffix.length)
|
|
74
|
+
return { text, attachments: [] };
|
|
75
|
+
return { text: lines.slice(0, suffix[0].start).join("\n").trimEnd(), attachments: suffix.map(b => b.value) };
|
|
76
|
+
}
|
|
77
|
+
const frozen = Symbol("resolved native attachment");
|
|
78
|
+
export const frozenAttachmentText = (attachment) => attachment[frozen];
|
|
79
|
+
export function resolveNativeAttachments(session, attachments) {
|
|
80
|
+
return attachments?.map((attachment) => {
|
|
81
|
+
if (!attachment.nativeRef || frozenAttachmentText(attachment) !== undefined)
|
|
82
|
+
return attachment;
|
|
83
|
+
const { entryId, index } = attachment.nativeRef;
|
|
84
|
+
const entry = session.sessionManager.getEntry(entryId);
|
|
85
|
+
if (!Number.isSafeInteger(index) || index < 0 || entry?.type !== "message")
|
|
86
|
+
throw new Error("Invalid attachment reference");
|
|
87
|
+
const message = entry.message;
|
|
88
|
+
if (message.role !== "user" && !(message.role === "custom" && message.customType === "file"))
|
|
89
|
+
throw new Error("Invalid attachment source");
|
|
90
|
+
const text = typeof message.content === "string" ? message.content : message.content.filter(b => b.type === "text").map(b => b.text).join("\n");
|
|
91
|
+
const block = parseUserAttachments(text).attachments[index];
|
|
92
|
+
if (!block)
|
|
93
|
+
throw new Error("Attachment not found");
|
|
94
|
+
return { path: block.path, mode: block.mode, nativeRef: attachment.nativeRef, [frozen]: block.raw };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const WIDGET_WIDTH = 80;
|
|
3
|
+
/** ANSI 转义序列(CSI + OSC 两类):扩展 widget/status 文本里常混有 TUI 颜色码
|
|
4
|
+
* (如 pi-powerline-footer),浏览器会把它渲染成字面 `[38;5;244m` 乱码(issue #16)。 */
|
|
5
|
+
const ANSI_RE = /\[[0-9:;<=>?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
6
|
+
/** Strip all ANSI escape sequences (CSI/OSC) from a string. */
|
|
7
|
+
export function stripAnsi(s) {
|
|
8
|
+
return s.replace(ANSI_RE, "");
|
|
9
|
+
}
|
|
10
|
+
/** Mock theme: TUI color functions degrade to identity so widget text survives. */
|
|
11
|
+
const mockTheme = new Proxy({
|
|
12
|
+
fg: (_color, text) => text,
|
|
13
|
+
bold: (text) => text,
|
|
14
|
+
strikethrough: (text) => text,
|
|
15
|
+
dim: (text) => text,
|
|
16
|
+
}, {
|
|
17
|
+
get(target, prop) {
|
|
18
|
+
if (prop in target)
|
|
19
|
+
return target[prop];
|
|
20
|
+
// Unknown theme methods → no-op passthrough.
|
|
21
|
+
return (_arg, text) => text !== undefined ? text : "";
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
/** Mock TUI: any method call is a safe no-op. */
|
|
25
|
+
const mockTui = new Proxy({
|
|
26
|
+
requestRender: () => { },
|
|
27
|
+
render: () => { },
|
|
28
|
+
}, {
|
|
29
|
+
get(target, prop) {
|
|
30
|
+
if (prop in target)
|
|
31
|
+
return target[prop];
|
|
32
|
+
return () => { };
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Implements the subset of ExtensionUIContext that makes sense for a web UI.
|
|
37
|
+
* Dialogs bridge to the owning conversation; custom TUI components degrade
|
|
38
|
+
* immediately so extensions can choose their RPC fallback.
|
|
39
|
+
*/
|
|
40
|
+
export class WebUIContext {
|
|
41
|
+
conversationId;
|
|
42
|
+
source;
|
|
43
|
+
theme = mockTheme;
|
|
44
|
+
widgets = new Map();
|
|
45
|
+
lastLines = new Map();
|
|
46
|
+
emit;
|
|
47
|
+
disposed = false;
|
|
48
|
+
title = "";
|
|
49
|
+
replayTitle(send) { send({ type: "extension_title", conversationId: this.conversationId, title: this.title }); }
|
|
50
|
+
constructor(emit, conversationId = "", source = conversationId) {
|
|
51
|
+
this.conversationId = conversationId;
|
|
52
|
+
this.source = source;
|
|
53
|
+
this.emit = msg => { if (!this.disposed)
|
|
54
|
+
emit(msg); };
|
|
55
|
+
}
|
|
56
|
+
// -- widgets -------------------------------------------------------------
|
|
57
|
+
/** Matches ExtensionUIContext's overloaded setWidget exactly. */
|
|
58
|
+
setWidget = (key, content, options) => {
|
|
59
|
+
void options;
|
|
60
|
+
if (this.disposed)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
this.widgets.get(key)?.dispose?.();
|
|
64
|
+
}
|
|
65
|
+
catch { /* best effort */ }
|
|
66
|
+
if (content === undefined) {
|
|
67
|
+
this.widgets.delete(key);
|
|
68
|
+
this.lastLines.delete(key);
|
|
69
|
+
this.push();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (typeof content === "function") {
|
|
73
|
+
let comp;
|
|
74
|
+
try {
|
|
75
|
+
// Mock TUI/theme: extensions only read a handful of theme helpers;
|
|
76
|
+
// everything else is a no-op, so the widget renders to plain text.
|
|
77
|
+
comp = content(mockTui, mockTheme);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
comp = undefined;
|
|
81
|
+
}
|
|
82
|
+
this.widgets.set(key, {
|
|
83
|
+
render: (w) => comp?.render?.(w),
|
|
84
|
+
dispose: () => comp?.dispose?.(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.widgets.set(key, { render: () => content });
|
|
89
|
+
}
|
|
90
|
+
this.push();
|
|
91
|
+
};
|
|
92
|
+
/** Re-render all widgets and push when content changed (polled + on demand). */
|
|
93
|
+
refresh() {
|
|
94
|
+
let changed = false;
|
|
95
|
+
for (const [key, w] of this.widgets) {
|
|
96
|
+
let lines;
|
|
97
|
+
try {
|
|
98
|
+
lines = w.render(WIDGET_WIDTH);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
lines = undefined;
|
|
102
|
+
}
|
|
103
|
+
const prev = this.lastLines.get(key);
|
|
104
|
+
if (JSON.stringify(lines ?? null) !== JSON.stringify(prev ?? null)) {
|
|
105
|
+
this.lastLines.set(key, lines ?? []);
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (changed)
|
|
110
|
+
this.push();
|
|
111
|
+
}
|
|
112
|
+
push() {
|
|
113
|
+
const widgets = this.snapshot();
|
|
114
|
+
this.emit({ type: "widgets", conversationId: this.conversationId, widgets });
|
|
115
|
+
}
|
|
116
|
+
/** Render all widgets to their current text lines (without emitting). */
|
|
117
|
+
snapshot() {
|
|
118
|
+
return [...this.widgets.entries()].map(([key, w]) => {
|
|
119
|
+
let lines;
|
|
120
|
+
try {
|
|
121
|
+
lines = w.render(WIDGET_WIDTH);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
lines = undefined;
|
|
125
|
+
}
|
|
126
|
+
// 浏览器不是终端:ANSI 颜色码剥掉再下发/比对(issue #16)。
|
|
127
|
+
const clean = lines?.map(stripAnsi) ?? undefined;
|
|
128
|
+
this.lastLines.set(key, clean ?? []);
|
|
129
|
+
return { key, lines: clean ?? [] };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// -- notifications --------------------------------------------------------
|
|
133
|
+
// Pi wraps this context with object spread: UI callbacks are own properties.
|
|
134
|
+
notify = (message, type) => {
|
|
135
|
+
this.emit({ type: "notice", conversationId: this.conversationId, level: type ?? "info", text: message });
|
|
136
|
+
};
|
|
137
|
+
// -- footer status (pi-lens "LSP Inactive", pi-cache-optimizer cache stats) --
|
|
138
|
+
statuses = new Map();
|
|
139
|
+
setStatus = (key, text) => {
|
|
140
|
+
if (this.disposed)
|
|
141
|
+
return;
|
|
142
|
+
if (text === undefined || text === "") {
|
|
143
|
+
this.statuses.delete(key);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// 入口处剥 ANSI:pushStatuses / statusSnapshot 两条路径都拿到干净文本。
|
|
147
|
+
const clean = stripAnsi(text);
|
|
148
|
+
if (clean === "")
|
|
149
|
+
this.statuses.delete(key);
|
|
150
|
+
else
|
|
151
|
+
this.statuses.set(key, clean);
|
|
152
|
+
}
|
|
153
|
+
this.pushStatuses();
|
|
154
|
+
};
|
|
155
|
+
pushStatuses() {
|
|
156
|
+
this.emit({
|
|
157
|
+
type: "statuses", conversationId: this.conversationId,
|
|
158
|
+
statuses: [...this.statuses.entries()].map(([k, v]) => ({
|
|
159
|
+
key: k,
|
|
160
|
+
text: v,
|
|
161
|
+
})),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Current footer status entries (for replay on socket attach). */
|
|
165
|
+
statusSnapshot() {
|
|
166
|
+
return [...this.statuses.entries()].map(([k, v]) => ({ key: k, text: v }));
|
|
167
|
+
}
|
|
168
|
+
// -- dialogs (select/confirm/input bridged to the browser) ---------------
|
|
169
|
+
dialogMessages = new Map();
|
|
170
|
+
pendingDialogs = new Map();
|
|
171
|
+
replayDialogs(send) { for (const message of this.dialogMessages.values())
|
|
172
|
+
send(message); }
|
|
173
|
+
select = (title, options, config) => this.openDialog("select", title, [options], config);
|
|
174
|
+
confirm = (title, message, config) => this.openDialog("confirm", title, [message], config);
|
|
175
|
+
input = (title, placeholder, config) => this.openDialog("input", title, [placeholder ?? ""], config);
|
|
176
|
+
editor = (title, prefill) => this.openDialog("editor", title, [prefill ?? ""]);
|
|
177
|
+
openDialog(kind, title, args, options) {
|
|
178
|
+
const cancelled = kind === "confirm" ? false : undefined;
|
|
179
|
+
if (this.disposed || options?.signal?.aborted)
|
|
180
|
+
return Promise.resolve(cancelled);
|
|
181
|
+
return new Promise(resolve => {
|
|
182
|
+
const id = randomUUID();
|
|
183
|
+
let timer;
|
|
184
|
+
const abort = () => this.resolveDialog(id, null);
|
|
185
|
+
this.pendingDialogs.set(id, value => {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
188
|
+
resolve(value === null ? cancelled : value);
|
|
189
|
+
});
|
|
190
|
+
this.dialogMessages.set(id, { type: "dialog", conversationId: this.conversationId, source: typeof this.source === "function" ? this.source() : this.source, id, kind, title, args });
|
|
191
|
+
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
192
|
+
if (options?.timeout !== undefined && Number.isFinite(options.timeout))
|
|
193
|
+
timer = setTimeout(abort, Math.max(0, options.timeout));
|
|
194
|
+
this.emit(this.dialogMessages.get(id));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
resolveDialog(id, value) {
|
|
198
|
+
const message = this.dialogMessages.get(id);
|
|
199
|
+
if (!message)
|
|
200
|
+
return;
|
|
201
|
+
if (value !== null) {
|
|
202
|
+
if (message.kind === "confirm" ? typeof value !== "boolean" : typeof value !== "string")
|
|
203
|
+
return;
|
|
204
|
+
if (message.kind === "select" && !message.args[0].includes(value))
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const resolve = this.pendingDialogs.get(id);
|
|
208
|
+
this.pendingDialogs.delete(id);
|
|
209
|
+
this.dialogMessages.delete(id);
|
|
210
|
+
resolve(value);
|
|
211
|
+
this.emit({ type: "dialog_closed", conversationId: this.conversationId, id });
|
|
212
|
+
}
|
|
213
|
+
cancelPendingDialogs() { for (const id of this.pendingDialogs.keys())
|
|
214
|
+
this.resolveDialog(id, null); }
|
|
215
|
+
// -- inert TUI-only affordances ------------------------------------------
|
|
216
|
+
onTerminalInput = () => () => { };
|
|
217
|
+
setWorkingMessage = () => { };
|
|
218
|
+
setWorkingVisible = () => { };
|
|
219
|
+
setWorkingIndicator = () => { };
|
|
220
|
+
setHiddenThinkingLabel = () => { };
|
|
221
|
+
setFooter = () => { };
|
|
222
|
+
setHeader = () => { };
|
|
223
|
+
setTitle = (title) => { if (this.disposed)
|
|
224
|
+
return; this.title = title; this.emit({ type: "extension_title", conversationId: this.conversationId, title }); };
|
|
225
|
+
custom = (_factory, _done) => Promise.resolve(undefined);
|
|
226
|
+
pasteToEditor = (text) => { this.setEditorText(text); };
|
|
227
|
+
setEditorText = (text) => { this.emit({ type: "extension_editor", conversationId: this.conversationId, id: randomUUID(), text }); };
|
|
228
|
+
getEditorText = () => "";
|
|
229
|
+
addAutocompleteProvider = () => { };
|
|
230
|
+
setEditorComponent = () => { };
|
|
231
|
+
getEditorComponent = () => undefined;
|
|
232
|
+
getAllThemes = () => [];
|
|
233
|
+
getTheme = () => undefined;
|
|
234
|
+
setTheme = () => ({ success: false });
|
|
235
|
+
getToolsExpanded = () => false;
|
|
236
|
+
setToolsExpanded = () => { };
|
|
237
|
+
/** Dispose all widgets (extension reload / session teardown). */
|
|
238
|
+
dispose() {
|
|
239
|
+
for (const w of this.widgets.values()) {
|
|
240
|
+
try {
|
|
241
|
+
w.dispose?.();
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// best effort
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.widgets.clear();
|
|
248
|
+
this.lastLines.clear();
|
|
249
|
+
this.cancelPendingDialogs();
|
|
250
|
+
this.statuses.clear();
|
|
251
|
+
this.push();
|
|
252
|
+
this.pushStatuses();
|
|
253
|
+
this.setTitle("");
|
|
254
|
+
this.emit({ type: "extension_ui_reset", conversationId: this.conversationId });
|
|
255
|
+
this.disposed = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
const parser = unified().use(remarkParse);
|
|
4
|
+
function plain(node) {
|
|
5
|
+
if (["code", "html", "definition"].includes(node.type))
|
|
6
|
+
return "";
|
|
7
|
+
return node.value ?? node.children?.map(plain).join("") ?? "";
|
|
8
|
+
}
|
|
9
|
+
function visit(node, fn) {
|
|
10
|
+
fn(node);
|
|
11
|
+
if (!["code", "inlineCode", "html", "definition"].includes(node.type))
|
|
12
|
+
node.children?.forEach(child => visit(child, fn));
|
|
13
|
+
}
|
|
14
|
+
function scalar(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(trimmed);
|
|
19
|
+
}
|
|
20
|
+
catch { /* YAML also accepts non-JSON strings. */ }
|
|
21
|
+
}
|
|
22
|
+
return trimmed.replace(/^(['"])([\s\S]*)\1$/, "$2").replaceAll("''", "'");
|
|
23
|
+
}
|
|
24
|
+
/** Shared metadata and reading presentation. The source is never rewritten on disk. */
|
|
25
|
+
export function wikiMetadata(text) {
|
|
26
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
27
|
+
const body = front ? text.slice(front[0].length) : text;
|
|
28
|
+
const yaml = front?.[1] ?? "", tags = new Set();
|
|
29
|
+
const addTag = (value) => {
|
|
30
|
+
const tag = scalar(value).replace(/^#/, "").trim();
|
|
31
|
+
if (tag && !/^(?:[a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/i.test(tag))
|
|
32
|
+
tags.add(tag);
|
|
33
|
+
};
|
|
34
|
+
const field = /^tags?:[ \t]*(.*)$/m.exec(yaml);
|
|
35
|
+
if (field?.[1].trim())
|
|
36
|
+
for (const item of field[1].replace(/^\[|\]$/g, "").split(/[,\s]+/))
|
|
37
|
+
addTag(item);
|
|
38
|
+
if (field && !field[1].trim()) {
|
|
39
|
+
for (const line of yaml.slice(field.index + field[0].length).split(/\r?\n/)) {
|
|
40
|
+
if (!line.trim())
|
|
41
|
+
continue;
|
|
42
|
+
const item = /^\s*-\s+(.+)$/.exec(line);
|
|
43
|
+
if (!item)
|
|
44
|
+
break;
|
|
45
|
+
addTag(item[1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const tree = parser.parse(body);
|
|
49
|
+
let firstHeading;
|
|
50
|
+
visit(tree, node => {
|
|
51
|
+
if (!firstHeading && node.type === "heading" && node.depth === 1)
|
|
52
|
+
firstHeading = node;
|
|
53
|
+
if (node.type === "text" && node.position?.start.offset !== undefined && node.position.end.offset !== undefined) {
|
|
54
|
+
const offset = node.position.start.offset, raw = body.slice(offset, node.position.end.offset);
|
|
55
|
+
for (const match of raw.matchAll(/(?:^|\s)#([\p{L}\p{N}_/-]+)/gu)) {
|
|
56
|
+
if (match.index === 0 && raw.startsWith("#") && offset > 0 && !/\s/.test(body[offset - 1]))
|
|
57
|
+
continue;
|
|
58
|
+
addTag(match[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const heading = firstHeading ? plain(firstHeading).trim() : undefined;
|
|
63
|
+
const title = scalar(/^title:[ \t]*(.*)$/m.exec(yaml)?.[1] ?? "") || heading;
|
|
64
|
+
let readingBody = body;
|
|
65
|
+
if (title && heading === title.trim() && firstHeading?.position) {
|
|
66
|
+
const { start, end } = firstHeading.position;
|
|
67
|
+
if (start.offset !== undefined && end.offset !== undefined)
|
|
68
|
+
readingBody = body.slice(0, start.offset) + body.slice(end.offset);
|
|
69
|
+
}
|
|
70
|
+
const prose = tree.children.map(node => plain(node)).join(" ");
|
|
71
|
+
const chinese = (prose.match(/\p{Script=Han}/gu) ?? []).length;
|
|
72
|
+
const words = (prose.replace(/\p{Script=Han}/gu, " ").match(/[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*/gu) ?? []).length;
|
|
73
|
+
return { tags: [...tags].slice(0, 100), title, body, readingBody, minutes: Math.max(1, Math.ceil(chinese / 400 + words / 200)) };
|
|
74
|
+
}
|
|
75
|
+
/** Blank code ranges while preserving line/offset positions for link indexing. */
|
|
76
|
+
export function withoutCode(text) {
|
|
77
|
+
const tree = parser.parse(text), ranges = [];
|
|
78
|
+
visit(tree, node => {
|
|
79
|
+
if ((node.type === "code" || node.type === "inlineCode") && node.position?.start.offset !== undefined && node.position.end.offset !== undefined)
|
|
80
|
+
ranges.push([node.position.start.offset, node.position.end.offset]);
|
|
81
|
+
});
|
|
82
|
+
let result = "", at = 0;
|
|
83
|
+
for (const [start, end] of ranges) {
|
|
84
|
+
result += text.slice(at, start) + text.slice(start, end).replace(/[^\n]/g, " ");
|
|
85
|
+
at = end;
|
|
86
|
+
}
|
|
87
|
+
return result + text.slice(at);
|
|
88
|
+
}
|
|
89
|
+
const linkName = (path) => (path.split("/").at(-1) ?? "").replace(/\.(md|txt)$/i, "");
|
|
90
|
+
export function wikiLinkIndex(paths) {
|
|
91
|
+
const byName = new Map();
|
|
92
|
+
for (const p of paths) {
|
|
93
|
+
const name = linkName(p), list = byName.get(name);
|
|
94
|
+
if (list)
|
|
95
|
+
list.push(p);
|
|
96
|
+
else
|
|
97
|
+
byName.set(name, [p]);
|
|
98
|
+
}
|
|
99
|
+
return { paths: new Set(paths), byName };
|
|
100
|
+
}
|
|
101
|
+
export function resolveWikiLink(source, target, paths) {
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = decodeURIComponent(target.split("|")[0].split("#")[0]).replaceAll("\\", "/");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!raw || /^(?:[a-z]+:|\/)/i.test(raw))
|
|
110
|
+
return;
|
|
111
|
+
const normalize = (v) => {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const p of v.split("/")) {
|
|
114
|
+
if (p === "..") {
|
|
115
|
+
if (!out.length)
|
|
116
|
+
return "";
|
|
117
|
+
out.pop();
|
|
118
|
+
}
|
|
119
|
+
else if (p && p !== ".")
|
|
120
|
+
out.push(p);
|
|
121
|
+
}
|
|
122
|
+
return out.join("/");
|
|
123
|
+
};
|
|
124
|
+
const parent = source.includes("/") ? source.slice(0, source.lastIndexOf("/") + 1) : "";
|
|
125
|
+
const index = Array.isArray(paths) ? wikiLinkIndex(paths) : paths;
|
|
126
|
+
for (const candidate of [normalize(parent + raw), normalize(raw)]) {
|
|
127
|
+
for (const p of [candidate, candidate + ".md", candidate + ".txt"])
|
|
128
|
+
if (index.paths.has(p))
|
|
129
|
+
return p;
|
|
130
|
+
}
|
|
131
|
+
const matches = index.byName.get(raw.replace(/\.(md|txt)$/i, "")) ?? [];
|
|
132
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
133
|
+
}
|
|
134
|
+
export function wikiReferences(text) {
|
|
135
|
+
return withoutCode(text.replace(/`([^`\n]+\.[A-Za-z0-9]+)`/g, (_all, path) => `[[${path}]]`)).split(/\r?\n/).flatMap((line, index) => [...line.matchAll(/\[\[([^\]]+)\]\]|\[[^\]]*\]\(([^\s)]+)(?:\s+"[^"]*")?\)/g)].map(m => ({ target: m[1] ?? m[2], snippet: line.trim().slice(0, 240), line: index + 1 })));
|
|
136
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
3
|
+
process.once("message", async (path) => {
|
|
4
|
+
try {
|
|
5
|
+
const task = getDocument({ data: new Uint8Array(readFileSync(path)), useSystemFonts: false });
|
|
6
|
+
const pdf = await task.promise, pages = [];
|
|
7
|
+
try {
|
|
8
|
+
if (pdf.numPages > 200)
|
|
9
|
+
throw new Error("PDF exceeds 200 pages");
|
|
10
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
11
|
+
const page = await pdf.getPage(i), content = await page.getTextContent();
|
|
12
|
+
pages.push(content.items.map(item => "str" in item ? item.str : "").join(" ").slice(0, 100000));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await task.destroy();
|
|
17
|
+
}
|
|
18
|
+
process.send?.({ pages });
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
process.send?.({ error: e.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
let running = 0;
|
|
5
|
+
export async function readWikiPdf(path) {
|
|
6
|
+
const info = statSync(path), version = `${info.size}:${info.mtimeMs}`;
|
|
7
|
+
if (cache.get(path)?.version === version)
|
|
8
|
+
return cache.get(path).pages;
|
|
9
|
+
if (running >= 2)
|
|
10
|
+
throw new Error("PDF reader busy");
|
|
11
|
+
running++;
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = fork(new URL(import.meta.url.endsWith(".ts") ? "./wiki-pdf-worker.ts" : "./wiki-pdf-worker.js", import.meta.url), [], { stdio: ["ignore", "ignore", "ignore", "ipc"], execArgv: process.execArgv.filter(a => !a.startsWith("--watch")) });
|
|
14
|
+
let finished = false;
|
|
15
|
+
const finish = (error, pages) => {
|
|
16
|
+
if (finished)
|
|
17
|
+
return;
|
|
18
|
+
finished = true;
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
child.kill("SIGKILL");
|
|
21
|
+
if (error)
|
|
22
|
+
reject(error);
|
|
23
|
+
else {
|
|
24
|
+
if (cache.size >= 16)
|
|
25
|
+
cache.delete(cache.keys().next().value);
|
|
26
|
+
cache.set(path, { version, pages: pages });
|
|
27
|
+
resolve(pages);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => finish(new Error("PDF extraction timed out")), 8000);
|
|
31
|
+
child.once("exit", () => { running--; finish(new Error("PDF reader stopped")); });
|
|
32
|
+
child.once("error", e => finish(e));
|
|
33
|
+
child.once("message", (message) => finish(message.error ? new Error(message.error) : undefined, message.pages));
|
|
34
|
+
child.send(path);
|
|
35
|
+
});
|
|
36
|
+
}
|