@baique/pi-web-sky 0.1.35 → 0.1.37
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +11 -11
- package/.next/build-manifest.json +4 -4
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +2 -2
- package/.next/required-server-files.js +6 -6
- package/.next/required-server-files.json +6 -6
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +3 -3
- package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +2 -2
- package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/agent/running/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/app-update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/all-providers/route.js +1 -1
- package/.next/server/app/api/auth/all-providers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +3 -3
- package/.next/server/app/api/auth/login/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/providers/route.js +1 -1
- package/.next/server/app/api/auth/providers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/canvas/route.js +1 -1
- package/.next/server/app/api/boards/[id]/canvas/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/edges/[eid]/route.js +1 -1
- package/.next/server/app/api/boards/[id]/edges/[eid]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/edges/route.js +1 -1
- package/.next/server/app/api/boards/[id]/edges/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js +1 -1
- package/.next/server/app/api/boards/[id]/nodes/[nid]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/nodes/route.js +1 -1
- package/.next/server/app/api/boards/[id]/nodes/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js +1 -1
- package/.next/server/app/api/boards/[id]/reconcile-task-edges/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/[id]/route.js +1 -1
- package/.next/server/app/api/boards/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/purge-orphans/route.js +1 -1
- package/.next/server/app/api/boards/purge-orphans/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/reorder/route.js +1 -1
- package/.next/server/app/api/boards/reorder/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/boards/route.js +1 -1
- package/.next/server/app/api/boards/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/cwd/browse/route.js +1 -1
- package/.next/server/app/api/cwd/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +2 -2
- package/.next/server/app/api/cwd/validate/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +2 -2
- package/.next/server/app/api/default-cwd/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/file-index/route.js +2 -2
- package/.next/server/app/api/file-index/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +2 -2
- package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/git/diff/route.js +1 -1
- package/.next/server/app/api/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/git/status/route.js +1 -1
- package/.next/server/app/api/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/home/route.js +1 -1
- package/.next/server/app/api/home/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/mcp/route.js +2 -2
- package/.next/server/app/api/mcp/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models/route.js +2 -2
- package/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/catalog/route.js +1 -1
- package/.next/server/app/api/models-config/catalog/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/discover/route.js +1 -1
- package/.next/server/app/api/models-config/discover/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/route.js +1 -1
- package/.next/server/app/api/models-config/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/test/route.js +1 -1
- package/.next/server/app/api/models-config/test/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/plugins/route.js +2 -2
- package/.next/server/app/api/plugins/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/project-trust/route.js +2 -2
- package/.next/server/app/api/project-trust/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/quota/route.js +1 -1
- package/.next/server/app/api/quota/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +2 -2
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +3 -3
- package/.next/server/app/api/sessions/[id]/auto-name/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/context/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/export/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/state/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/route.js +2 -2
- package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/install/route.js +2 -2
- package/.next/server/app/api/skills/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/search/route.js +1 -1
- package/.next/server/app/api/skills/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task-card-questions/[id]/answer/route.js +1 -1
- package/.next/server/app/api/task-card-questions/[id]/answer/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task-card-questions/route.js +1 -1
- package/.next/server/app/api/task-card-questions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task-cards/[id]/route.js +1 -1
- package/.next/server/app/api/task-cards/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task-cards/route.js +1 -1
- package/.next/server/app/api/task-cards/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task-scheduler/status/route.js +1 -1
- package/.next/server/app/api/task-scheduler/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/board/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[id]/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/reorder/route.js +1 -1
- package/.next/server/app/api/tasks/reorder/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/route.js +1 -1
- package/.next/server/app/api/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/terminal/[id]/events/route.js +3 -3
- package/.next/server/app/api/terminal/[id]/events/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/terminal/[id]/route.js +2 -2
- package/.next/server/app/api/terminal/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/terminal/route.js +2 -2
- package/.next/server/app/api/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route.js +2 -2
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/manifest.webmanifest/route.js +2 -2
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +11 -11
- package/.next/server/chunks/2404.js +2 -2
- package/.next/server/chunks/2430.js +4 -4
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +11 -6
- package/.next/server/chunks/5794.js +2 -2
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +2 -2
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +3 -3
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/5315.dba7515ae6e139ae.js +4 -0
- package/.next/static/chunks/{7999.f4ab221861378788.js → 7999.879d5a20e8641752.js} +1 -1
- package/.next/static/chunks/app/_not-found/{page-af4fe23994cb3d66.js → page-8cea8d8de35f4da4.js} +1 -1
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-c4ca28ccc08bc975.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-c9ead38525a85686.js} +3 -3
- package/.next/static/chunks/{main-app-b3e8a8d15d83e1f0.js → main-app-dd2385a1a5a588df.js} +1 -1
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-be943dcb89867ab9.js +1 -0
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-4bbf04891e910713.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/.next/types/app/api/agent/[id]/bash-output/route.ts +1 -1
- package/.next/types/app/api/agent/[id]/events/route.ts +1 -1
- package/.next/types/app/api/agent/[id]/route.ts +1 -1
- package/.next/types/app/api/agent/new/route.ts +1 -1
- package/.next/types/app/api/agent/running/route.ts +1 -1
- package/.next/types/app/api/app-update/route.ts +1 -1
- package/.next/types/app/api/auth/all-providers/route.ts +1 -1
- package/.next/types/app/api/auth/api-key/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/login/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/logout/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/providers/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/canvas/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/edges/[eid]/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/edges/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/nodes/[nid]/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/nodes/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/reconcile-task-edges/route.ts +1 -1
- package/.next/types/app/api/boards/[id]/route.ts +1 -1
- package/.next/types/app/api/boards/purge-orphans/route.ts +1 -1
- package/.next/types/app/api/boards/reorder/route.ts +1 -1
- package/.next/types/app/api/boards/route.ts +1 -1
- package/.next/types/app/api/cwd/browse/route.ts +1 -1
- package/.next/types/app/api/cwd/validate/route.ts +1 -1
- package/.next/types/app/api/default-cwd/route.ts +1 -1
- package/.next/types/app/api/file-index/route.ts +1 -1
- package/.next/types/app/api/files/[...path]/route.ts +1 -1
- package/.next/types/app/api/git/diff/route.ts +1 -1
- package/.next/types/app/api/git/status/route.ts +1 -1
- package/.next/types/app/api/home/route.ts +1 -1
- package/.next/types/app/api/mcp/route.ts +1 -1
- package/.next/types/app/api/models/route.ts +1 -1
- package/.next/types/app/api/models-config/catalog/route.ts +1 -1
- package/.next/types/app/api/models-config/discover/route.ts +1 -1
- package/.next/types/app/api/models-config/route.ts +1 -1
- package/.next/types/app/api/models-config/test/route.ts +1 -1
- package/.next/types/app/api/plugins/route.ts +1 -1
- package/.next/types/app/api/project-trust/route.ts +1 -1
- package/.next/types/app/api/quota/route.ts +1 -1
- package/.next/types/app/api/search/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/auto-name/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/context/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/entries/[entryId]/thinking/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/state/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/todos/route.ts +1 -1
- package/.next/types/app/api/sessions/route.ts +1 -1
- package/.next/types/app/api/skills/check/route.ts +1 -1
- package/.next/types/app/api/skills/install/route.ts +1 -1
- package/.next/types/app/api/skills/route.ts +1 -1
- package/.next/types/app/api/skills/search/route.ts +1 -1
- package/.next/types/app/api/skills/update/route.ts +1 -1
- package/.next/types/app/api/task-card-questions/[id]/answer/route.ts +1 -1
- package/.next/types/app/api/task-card-questions/route.ts +1 -1
- package/.next/types/app/api/task-cards/[id]/route.ts +1 -1
- package/.next/types/app/api/task-cards/route.ts +1 -1
- package/.next/types/app/api/task-scheduler/status/route.ts +1 -1
- package/.next/types/app/api/tasks/[id]/board/route.ts +1 -1
- package/.next/types/app/api/tasks/[id]/route.ts +1 -1
- package/.next/types/app/api/tasks/reorder/route.ts +1 -1
- package/.next/types/app/api/tasks/route.ts +1 -1
- package/.next/types/app/api/terminal/[id]/events/route.ts +1 -1
- package/.next/types/app/api/terminal/[id]/route.ts +1 -1
- package/.next/types/app/api/terminal/route.ts +1 -1
- package/.next/types/app/api/worktrees/route.ts +1 -1
- package/.next/types/app/layout.ts +1 -1
- package/.next/types/app/page.ts +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-00a4badaccb388e6.js +0 -1
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → 7sDSz-wW-RWYnxh1l6-fD}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → 7sDSz-wW-RWYnxh1l6-fD}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { getDb } from "./sqlite-db";
|
|
3
|
+
import { getCard, listLinks, type LinkKind } from "./task-card-store";
|
|
4
|
+
import {
|
|
5
|
+
SYSTEM_RUNNING_BOARD_ID,
|
|
6
|
+
type BoardCanvas,
|
|
7
|
+
type BoardEdge,
|
|
8
|
+
type BoardInfo,
|
|
9
|
+
type BoardNode,
|
|
10
|
+
type BoardNodeKind,
|
|
11
|
+
type BoardView,
|
|
12
|
+
} from "./board-types";
|
|
13
|
+
|
|
14
|
+
const now = () => Date.now();
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// rows
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
interface BoardRow {
|
|
21
|
+
id: string;
|
|
22
|
+
projectKey: string;
|
|
23
|
+
name: string;
|
|
24
|
+
isSystem: number;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
sortOrder: number;
|
|
27
|
+
created: number;
|
|
28
|
+
updated: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface BoardNodeRow {
|
|
32
|
+
id: string;
|
|
33
|
+
boardId: string;
|
|
34
|
+
kind: BoardNodeKind;
|
|
35
|
+
refId: string | null;
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
w: number;
|
|
39
|
+
h: number;
|
|
40
|
+
expanded: number;
|
|
41
|
+
props: string;
|
|
42
|
+
created: number;
|
|
43
|
+
updated: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BoardEdgeRow {
|
|
47
|
+
id: string;
|
|
48
|
+
boardId: string;
|
|
49
|
+
fromId: string;
|
|
50
|
+
toId: string;
|
|
51
|
+
label: string | null;
|
|
52
|
+
color: string | null;
|
|
53
|
+
dashed: number;
|
|
54
|
+
created: number;
|
|
55
|
+
updated: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface BoardViewRow {
|
|
59
|
+
boardId: string;
|
|
60
|
+
cameraX: number;
|
|
61
|
+
cameraY: number;
|
|
62
|
+
cameraZ: number;
|
|
63
|
+
updated: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rowToBoard(row: BoardRow, nodeCount: number): BoardInfo {
|
|
67
|
+
return {
|
|
68
|
+
id: row.id,
|
|
69
|
+
projectKey: row.projectKey,
|
|
70
|
+
name: row.name,
|
|
71
|
+
isSystem: row.isSystem === 1,
|
|
72
|
+
taskId: row.taskId,
|
|
73
|
+
sortOrder: row.sortOrder,
|
|
74
|
+
created: row.created,
|
|
75
|
+
updated: row.updated,
|
|
76
|
+
nodeCount,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rowToNode(row: BoardNodeRow): BoardNode {
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
boardId: row.boardId,
|
|
84
|
+
kind: row.kind,
|
|
85
|
+
refId: row.refId,
|
|
86
|
+
x: row.x,
|
|
87
|
+
y: row.y,
|
|
88
|
+
w: row.w,
|
|
89
|
+
h: row.h,
|
|
90
|
+
expanded: row.expanded === 1,
|
|
91
|
+
props: parseProps(row.props),
|
|
92
|
+
created: row.created,
|
|
93
|
+
updated: row.updated,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rowToEdge(row: BoardEdgeRow): BoardEdge {
|
|
98
|
+
return {
|
|
99
|
+
id: row.id,
|
|
100
|
+
boardId: row.boardId,
|
|
101
|
+
fromId: row.fromId,
|
|
102
|
+
toId: row.toId,
|
|
103
|
+
label: row.label,
|
|
104
|
+
color: row.color,
|
|
105
|
+
dashed: row.dashed === 1,
|
|
106
|
+
created: row.created,
|
|
107
|
+
updated: row.updated,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rowToView(row: BoardViewRow): BoardView {
|
|
112
|
+
return {
|
|
113
|
+
boardId: row.boardId,
|
|
114
|
+
cameraX: row.cameraX,
|
|
115
|
+
cameraY: row.cameraY,
|
|
116
|
+
cameraZ: row.cameraZ,
|
|
117
|
+
updated: row.updated,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseProps(raw: string): Record<string, unknown> {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
124
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
125
|
+
? (parsed as Record<string, unknown>)
|
|
126
|
+
: {};
|
|
127
|
+
} catch {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function countNodes(boardId: string): number {
|
|
133
|
+
const row = getDb()
|
|
134
|
+
.prepare("SELECT COUNT(*) AS n FROM board_nodes WHERE board_id = ?")
|
|
135
|
+
.get(boardId) as { n: number };
|
|
136
|
+
return row.n;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// boards
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function getBoardRow(id: string): BoardRow | undefined {
|
|
144
|
+
return getDb()
|
|
145
|
+
.prepare("SELECT id, project_key AS projectKey, name, is_system AS isSystem, task_id AS taskId, sort_order AS sortOrder, created, updated FROM boards WHERE id = ?")
|
|
146
|
+
.get(id) as BoardRow | undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 系统「运行中」看板:不落库,恒存在。列表时以 isSystem 形态返回;
|
|
151
|
+
* projectKey 用空串(跨项目聚合,不归任何项目)。
|
|
152
|
+
*/
|
|
153
|
+
export function getSystemRunningBoard(): BoardInfo {
|
|
154
|
+
return {
|
|
155
|
+
id: SYSTEM_RUNNING_BOARD_ID,
|
|
156
|
+
projectKey: "",
|
|
157
|
+
name: "running",
|
|
158
|
+
isSystem: true,
|
|
159
|
+
taskId: null,
|
|
160
|
+
sortOrder: 0,
|
|
161
|
+
created: 0,
|
|
162
|
+
updated: 0,
|
|
163
|
+
nodeCount: 0,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 单看板(含系统看板);不存在返回 undefined。 */
|
|
168
|
+
export function getBoard(id: string): BoardInfo | undefined {
|
|
169
|
+
if (id === SYSTEM_RUNNING_BOARD_ID) return getSystemRunningBoard();
|
|
170
|
+
const row = getBoardRow(id);
|
|
171
|
+
return row ? rowToBoard(row, countNodes(row.id)) : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 某项目的全部看板(不含系统看板,系统看板由调用方前置插入)。
|
|
175
|
+
* 含任务型看板(taskId 非空);展示层自行决定是否过滤。 */
|
|
176
|
+
export function listBoards(projectKey: string): BoardInfo[] {
|
|
177
|
+
const rows = getDb()
|
|
178
|
+
.prepare(
|
|
179
|
+
"SELECT id, project_key AS projectKey, name, is_system AS isSystem, task_id AS taskId, sort_order AS sortOrder, created, updated FROM boards WHERE project_key = ? ORDER BY sort_order, created, rowid",
|
|
180
|
+
)
|
|
181
|
+
.all(projectKey) as unknown as BoardRow[];
|
|
182
|
+
return rows.map((row) => rowToBoard(row, countNodes(row.id)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 项目内看板批量排序(完整有序 id 列表)。id 必须都属于该项目,否则事务回滚。
|
|
187
|
+
*/
|
|
188
|
+
export function reorderBoards(projectKey: string, orderedIds: string[]): BoardInfo[] {
|
|
189
|
+
const db = getDb();
|
|
190
|
+
if (orderedIds.length === 0) return listBoards(projectKey);
|
|
191
|
+
db.exec("BEGIN");
|
|
192
|
+
try {
|
|
193
|
+
const stmt = db.prepare("UPDATE boards SET sort_order = ?, updated = ? WHERE id = ? AND project_key = ?");
|
|
194
|
+
orderedIds.forEach((id, index) => {
|
|
195
|
+
const res = stmt.run(index, now(), id, projectKey);
|
|
196
|
+
if (res.changes === 0) {
|
|
197
|
+
throw new Error(`board ${id} does not belong to project ${projectKey}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
db.exec("COMMIT");
|
|
201
|
+
} catch (error) {
|
|
202
|
+
db.exec("ROLLBACK");
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
return listBoards(projectKey);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 创建看板。空 projectKey / 空名抛错。taskId 可选:非空时看板为任务型(id = taskId)。 */
|
|
209
|
+
export function createBoard(projectKey: string, name: string, taskId?: string): BoardInfo {
|
|
210
|
+
const trimmed = name.trim();
|
|
211
|
+
if (!projectKey) throw new Error("projectKey is required");
|
|
212
|
+
if (!trimmed) throw new Error("name must not be empty");
|
|
213
|
+
const id = taskId ?? randomUUID();
|
|
214
|
+
const ts = now();
|
|
215
|
+
// 新看板置顶:sort_order 取当前项目最小值 - 1
|
|
216
|
+
const minRow = getDb()
|
|
217
|
+
.prepare("SELECT MIN(sort_order) AS minOrder FROM boards WHERE project_key = ?")
|
|
218
|
+
.get(projectKey) as { minOrder: number | null };
|
|
219
|
+
const sortOrder = minRow.minOrder === null ? 0 : minRow.minOrder - 1;
|
|
220
|
+
getDb()
|
|
221
|
+
.prepare("INSERT INTO boards (id, project_key, name, is_system, task_id, sort_order, created, updated) VALUES (?, ?, ?, 0, ?, ?, ?, ?)")
|
|
222
|
+
.run(id, projectKey, trimmed, taskId ?? null, sortOrder, ts, ts);
|
|
223
|
+
return { id, projectKey, name: trimmed, isSystem: false, taskId: taskId ?? null, sortOrder, created: ts, updated: ts, nodeCount: 0 };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 任务看板懒创建(原子 upsert):查 boards WHERE task_id = ?;不存在则创建
|
|
228
|
+
* (id = taskId,名 = 任务名)。返回 BoardInfo。
|
|
229
|
+
*
|
|
230
|
+
* 并发安全:boards.task_id 有 UNIQUE 索引(迁移 v6),多请求同时懒创建时
|
|
231
|
+
* 只有一个 INSERT 成功,其余 `INSERT OR IGNORE` 静默跳过;统一以最后查回
|
|
232
|
+
* 的结果返回——无论谁插入,所有并发请求都拿到同一行,不会重复创建。
|
|
233
|
+
*/
|
|
234
|
+
export function getOrCreateTaskBoard(taskId: string, projectKey: string, name: string): BoardInfo {
|
|
235
|
+
const db = getDb();
|
|
236
|
+
const query = "SELECT id, project_key AS projectKey, name, is_system AS isSystem, task_id AS taskId, sort_order AS sortOrder, created, updated FROM boards WHERE task_id = ?";
|
|
237
|
+
const existing = db.prepare(query).get(taskId) as BoardRow | undefined;
|
|
238
|
+
if (existing) return rowToBoard(existing, countNodes(existing.id));
|
|
239
|
+
|
|
240
|
+
// 原子插入:task_id UNIQUE 约束下并发只有一个成功,其余被 IGNORE
|
|
241
|
+
const trimmed = name.trim();
|
|
242
|
+
const ts = now();
|
|
243
|
+
// 新看板置顶:sort_order 取当前项目最小值 - 1(并发算同值无害,仅排序并列)
|
|
244
|
+
const minRow = db.prepare("SELECT MIN(sort_order) AS minOrder FROM boards WHERE project_key = ?").get(projectKey) as { minOrder: number | null };
|
|
245
|
+
const sortOrder = minRow.minOrder === null ? 0 : minRow.minOrder - 1;
|
|
246
|
+
db.prepare("INSERT OR IGNORE INTO boards (id, project_key, name, is_system, task_id, sort_order, created, updated) VALUES (?, ?, ?, 0, ?, ?, ?, ?)")
|
|
247
|
+
.run(taskId, projectKey, trimmed, taskId, sortOrder, ts, ts);
|
|
248
|
+
|
|
249
|
+
// 统一查回:无论刚插入还是并发者已插入,task_id 唯一 → 同一行
|
|
250
|
+
const row = db.prepare(query).get(taskId) as BoardRow | undefined;
|
|
251
|
+
if (!row) {
|
|
252
|
+
// 理论不可达(INSERT OR IGNORE 后必存在);防御性兜底
|
|
253
|
+
throw new Error(`Failed to create task board for task ${taskId}`);
|
|
254
|
+
}
|
|
255
|
+
return rowToBoard(row, countNodes(row.id));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 改名。系统看板 / 不存在返回 null。 */
|
|
259
|
+
export function renameBoard(id: string, name: string): BoardInfo | null {
|
|
260
|
+
if (id === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
261
|
+
const trimmed = name.trim();
|
|
262
|
+
if (!trimmed) throw new Error("name must not be empty");
|
|
263
|
+
const db = getDb();
|
|
264
|
+
const row = getBoardRow(id);
|
|
265
|
+
if (!row) return null;
|
|
266
|
+
const ts = now();
|
|
267
|
+
db.prepare("UPDATE boards SET name = ?, updated = ? WHERE id = ?").run(trimmed, ts, id);
|
|
268
|
+
return rowToBoard({ ...row, name: trimmed, updated: ts }, countNodes(id));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 级联删除看板数据(nodes/edges/view/boards 行)。**无事务**——由调用方
|
|
273
|
+
* (deleteBoard / task-store.deleteTask)在自身事务内调用,避免 SQLite 嵌套 BEGIN。
|
|
274
|
+
*/
|
|
275
|
+
export function deleteBoardCascade(id: string): void {
|
|
276
|
+
getDb().prepare("DELETE FROM board_nodes WHERE board_id = ?").run(id);
|
|
277
|
+
getDb().prepare("DELETE FROM board_edges WHERE board_id = ?").run(id);
|
|
278
|
+
getDb().prepare("DELETE FROM board_view WHERE board_id = ?").run(id);
|
|
279
|
+
getDb().prepare("DELETE FROM boards WHERE id = ?").run(id);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 删除看板(级联删 nodes/edges/view)。系统看板 / 不存在返回 false。 */
|
|
283
|
+
export function deleteBoard(id: string): boolean {
|
|
284
|
+
if (id === SYSTEM_RUNNING_BOARD_ID) return false;
|
|
285
|
+
const db = getDb();
|
|
286
|
+
if (!getBoardRow(id)) return false;
|
|
287
|
+
db.exec("BEGIN");
|
|
288
|
+
try {
|
|
289
|
+
deleteBoardCascade(id);
|
|
290
|
+
db.exec("COMMIT");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
db.exec("ROLLBACK");
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 任务改名时同步看板名(看板不存在则 0 行无害)。**无事务**——由调用方
|
|
300
|
+
* (task-store.updateTask)在自身事务内调用。
|
|
301
|
+
*/
|
|
302
|
+
export function renameTaskBoard(taskId: string, name: string): void {
|
|
303
|
+
getDb()
|
|
304
|
+
.prepare("UPDATE boards SET name = ?, updated = ? WHERE task_id = ?")
|
|
305
|
+
.run(name, now(), taskId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// canvas / nodes / edges / view
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
export function getBoardCanvas(boardId: string): BoardCanvas | null {
|
|
313
|
+
const board = getBoard(boardId);
|
|
314
|
+
if (!board) return null;
|
|
315
|
+
const nodes = getDb()
|
|
316
|
+
.prepare("SELECT id, board_id AS boardId, kind, ref_id AS refId, x, y, w, h, expanded, props, created, updated FROM board_nodes WHERE board_id = ? ORDER BY created, rowid")
|
|
317
|
+
.all(boardId) as unknown as BoardNodeRow[];
|
|
318
|
+
const edges = getDb()
|
|
319
|
+
.prepare("SELECT id, board_id AS boardId, from_id AS fromId, to_id AS toId, label, color, dashed, created, updated FROM board_edges WHERE board_id = ? ORDER BY created, rowid")
|
|
320
|
+
.all(boardId) as unknown as BoardEdgeRow[];
|
|
321
|
+
const view = getDb()
|
|
322
|
+
.prepare("SELECT board_id AS boardId, camera_x AS cameraX, camera_y AS cameraY, camera_z AS cameraZ, updated FROM board_view WHERE board_id = ?")
|
|
323
|
+
.get(boardId) as BoardViewRow | undefined;
|
|
324
|
+
return { board, nodes: nodes.map(rowToNode), edges: edges.map(rowToEdge), view: view ? rowToView(view) : null };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 全量保存整张画布(nodes/edges/view 一次事务替换)。
|
|
329
|
+
* 系统看板不可写;看板不存在返回 false。
|
|
330
|
+
*/
|
|
331
|
+
export function putBoardCanvas(
|
|
332
|
+
boardId: string,
|
|
333
|
+
payload: { nodes?: BoardNode[]; edges?: BoardEdge[]; view?: BoardView | null },
|
|
334
|
+
opts?: { baseUpdated?: number; allowEmpty?: boolean },
|
|
335
|
+
): boolean | "empty-overwrite" | "conflict" {
|
|
336
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return false;
|
|
337
|
+
const db = getDb();
|
|
338
|
+
const row = getBoardRow(boardId);
|
|
339
|
+
if (!row) return false;
|
|
340
|
+
// 乐观锁:客户端基于 baseUpdated 快照全量替换。若期间有其他客户端/标签页保存过
|
|
341
|
+
// (boards.updated 已变化),说明本地快照过期,直接拒绝 —— 由客户端拉取远端合并后重试。
|
|
342
|
+
if (opts?.baseUpdated !== undefined && opts.baseUpdated !== row.updated) {
|
|
343
|
+
return "conflict";
|
|
344
|
+
}
|
|
345
|
+
// 防数据丢失(多次真实发生):客户端物化失败/未加载时,会带着空/部分节点集自动保存,
|
|
346
|
+
// 全量替换会直接把看板删空。硬性兜底:看板已有节点而本次 payload 节点为空 → 拒绝写入。
|
|
347
|
+
// 用户主动「清空画布」走 allowEmpty 显式放行。
|
|
348
|
+
if (!opts?.allowEmpty && payload.nodes !== undefined && payload.nodes.length === 0) {
|
|
349
|
+
const existing = db.prepare("SELECT COUNT(*) c FROM board_nodes WHERE board_id = ?").get(boardId) as { c: number };
|
|
350
|
+
if (existing.c > 0) return "empty-overwrite";
|
|
351
|
+
}
|
|
352
|
+
const ts = now();
|
|
353
|
+
db.exec("BEGIN");
|
|
354
|
+
try {
|
|
355
|
+
if (payload.nodes) {
|
|
356
|
+
db.prepare("DELETE FROM board_nodes WHERE board_id = ?").run(boardId);
|
|
357
|
+
const insert = db.prepare(
|
|
358
|
+
"INSERT INTO board_nodes (id, board_id, kind, ref_id, x, y, w, h, expanded, props, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
359
|
+
);
|
|
360
|
+
for (const n of payload.nodes) {
|
|
361
|
+
insert.run(n.id, boardId, n.kind, n.refId, n.x, n.y, n.w, n.h, n.expanded ? 1 : 0, JSON.stringify(n.props ?? {}), n.created || ts, ts);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (payload.edges) {
|
|
365
|
+
db.prepare("DELETE FROM board_edges WHERE board_id = ?").run(boardId);
|
|
366
|
+
const insert = db.prepare(
|
|
367
|
+
"INSERT INTO board_edges (id, board_id, from_id, to_id, label, color, dashed, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
368
|
+
);
|
|
369
|
+
for (const e of payload.edges) {
|
|
370
|
+
insert.run(e.id, boardId, e.fromId, e.toId, e.label ?? null, e.color ?? null, e.dashed ? 1 : 0, e.created || ts, ts);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (payload.view !== undefined) {
|
|
374
|
+
db.prepare("DELETE FROM board_view WHERE board_id = ?").run(boardId);
|
|
375
|
+
if (payload.view) {
|
|
376
|
+
db.prepare(
|
|
377
|
+
"INSERT INTO board_view (board_id, camera_x, camera_y, camera_z, updated) VALUES (?, ?, ?, ?, ?)",
|
|
378
|
+
).run(boardId, payload.view.cameraX ?? 0, payload.view.cameraY ?? 0, payload.view.cameraZ ?? 1, ts);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
382
|
+
db.exec("COMMIT");
|
|
383
|
+
} catch (error) {
|
|
384
|
+
db.exec("ROLLBACK");
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface CreateNodeInput {
|
|
391
|
+
kind?: BoardNodeKind;
|
|
392
|
+
refId?: string | null;
|
|
393
|
+
x: number;
|
|
394
|
+
y: number;
|
|
395
|
+
w?: number;
|
|
396
|
+
h?: number;
|
|
397
|
+
expanded?: boolean;
|
|
398
|
+
props?: Record<string, unknown>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** 添加节点。系统看板不可写;看板不存在返回 null。 */
|
|
402
|
+
export function addNode(boardId: string, input: CreateNodeInput): BoardNode | null {
|
|
403
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
404
|
+
if (!getBoardRow(boardId)) return null;
|
|
405
|
+
const id = randomUUID();
|
|
406
|
+
const ts = now();
|
|
407
|
+
getDb()
|
|
408
|
+
.prepare(
|
|
409
|
+
"INSERT INTO board_nodes (id, board_id, kind, ref_id, x, y, w, h, expanded, props, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
410
|
+
)
|
|
411
|
+
.run(id, boardId, input.kind ?? "session", input.refId ?? null, input.x, input.y, input.w ?? 0, input.h ?? 0, input.expanded ? 1 : 0, JSON.stringify(input.props ?? {}), ts, ts);
|
|
412
|
+
getDb().prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
413
|
+
return getNode(boardId, id) ?? null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getNode(boardId: string, nodeId: string): BoardNode | undefined {
|
|
417
|
+
const row = getDb()
|
|
418
|
+
.prepare("SELECT id, board_id AS boardId, kind, ref_id AS refId, x, y, w, h, expanded, props, created, updated FROM board_nodes WHERE board_id = ? AND id = ?")
|
|
419
|
+
.get(boardId, nodeId) as BoardNodeRow | undefined;
|
|
420
|
+
return row ? rowToNode(row) : undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 按全局 nodeId 绑定会话(board_nodes.ref_id 写回;看板已迁 tldraw sync 后废弃保留)。
|
|
425
|
+
* nodeId 是 board_nodes.id(randomUUID,全局唯一),无需 boardId。
|
|
426
|
+
* 绑定成功 bump 所属看板 updated(乐观锁基线),返回更新后的节点;
|
|
427
|
+
* 节点不存在 / 属于系统看板返回 null。
|
|
428
|
+
*/
|
|
429
|
+
export function bindNodeToSession(nodeId: string, sessionId: string): BoardNode | null {
|
|
430
|
+
const db = getDb();
|
|
431
|
+
const row = db
|
|
432
|
+
.prepare("SELECT board_id AS boardId FROM board_nodes WHERE id = ?")
|
|
433
|
+
.get(nodeId) as { boardId: string } | undefined;
|
|
434
|
+
if (!row) return null;
|
|
435
|
+
if (row.boardId === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
436
|
+
const ts = now();
|
|
437
|
+
db.prepare("UPDATE board_nodes SET ref_id = ?, updated = ? WHERE id = ?").run(sessionId, ts, nodeId);
|
|
438
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, row.boardId);
|
|
439
|
+
return getNode(row.boardId, nodeId) ?? null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** 按全局 nodeId 读节点(未转正卡轮询用);不存在返回 undefined。 */
|
|
443
|
+
export function getNodeByGlobalId(nodeId: string): BoardNode | undefined {
|
|
444
|
+
const row = getDb()
|
|
445
|
+
.prepare("SELECT id, board_id AS boardId, kind, ref_id AS refId, x, y, w, h, expanded, props, created, updated FROM board_nodes WHERE id = ?")
|
|
446
|
+
.get(nodeId) as BoardNodeRow | undefined;
|
|
447
|
+
return row ? rowToNode(row) : undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** 按看板内 refId 查节点(任务卡 node 用:ref_id = cardId);可选按 kind 过滤。 */
|
|
451
|
+
export function getNodeByRefId(
|
|
452
|
+
boardId: string,
|
|
453
|
+
refId: string,
|
|
454
|
+
kind?: BoardNodeKind,
|
|
455
|
+
): BoardNode | undefined {
|
|
456
|
+
const sql =
|
|
457
|
+
"SELECT id, board_id AS boardId, kind, ref_id AS refId, x, y, w, h, expanded, props, created, updated FROM board_nodes " +
|
|
458
|
+
(kind
|
|
459
|
+
? "WHERE board_id = ? AND ref_id = ? AND kind = ?"
|
|
460
|
+
: "WHERE board_id = ? AND ref_id = ?");
|
|
461
|
+
const row = kind
|
|
462
|
+
? getDb().prepare(sql).get(boardId, refId, kind)
|
|
463
|
+
: getDb().prepare(sql).get(boardId, refId);
|
|
464
|
+
return row ? rowToNode(row as unknown as BoardNodeRow) : undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export interface PatchNodeInput {
|
|
468
|
+
/** 会话卡绑定的会话 id(draft 转正时写入)。null 显式解绑;缺省不改。 */
|
|
469
|
+
refId?: string | null;
|
|
470
|
+
x?: number;
|
|
471
|
+
y?: number;
|
|
472
|
+
w?: number;
|
|
473
|
+
h?: number;
|
|
474
|
+
expanded?: boolean;
|
|
475
|
+
props?: Record<string, unknown>;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** 更新节点(移动/改尺寸/展开标记/refId/属性)。系统看板或节点不存在返回 null。 */
|
|
479
|
+
export function patchNode(boardId: string, nodeId: string, patch: PatchNodeInput): BoardNode | null {
|
|
480
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
481
|
+
const db = getDb();
|
|
482
|
+
const existing = getNode(boardId, nodeId);
|
|
483
|
+
if (!existing) return null;
|
|
484
|
+
const ts = now();
|
|
485
|
+
const next = {
|
|
486
|
+
refId: patch.refId !== undefined ? patch.refId : existing.refId,
|
|
487
|
+
x: patch.x ?? existing.x,
|
|
488
|
+
y: patch.y ?? existing.y,
|
|
489
|
+
w: patch.w ?? existing.w,
|
|
490
|
+
h: patch.h ?? existing.h,
|
|
491
|
+
expanded: patch.expanded ?? existing.expanded,
|
|
492
|
+
props: patch.props !== undefined ? { ...existing.props, ...patch.props } : existing.props,
|
|
493
|
+
};
|
|
494
|
+
db.prepare(
|
|
495
|
+
"UPDATE board_nodes SET ref_id = ?, x = ?, y = ?, w = ?, h = ?, expanded = ?, props = ?, updated = ? WHERE board_id = ? AND id = ?",
|
|
496
|
+
).run(next.refId, next.x, next.y, next.w, next.h, next.expanded ? 1 : 0, JSON.stringify(next.props), ts, boardId, nodeId);
|
|
497
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
498
|
+
return getNode(boardId, nodeId)!;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 删除节点(级联删以它为端点的边)。系统看板或节点不存在返回 false。 */
|
|
502
|
+
export function deleteNode(boardId: string, nodeId: string): boolean {
|
|
503
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return false;
|
|
504
|
+
const db = getDb();
|
|
505
|
+
if (!getNode(boardId, nodeId)) return false;
|
|
506
|
+
const ts = now();
|
|
507
|
+
db.exec("BEGIN");
|
|
508
|
+
try {
|
|
509
|
+
db.prepare("DELETE FROM board_edges WHERE board_id = ? AND (from_id = ? OR to_id = ?)").run(boardId, nodeId, nodeId);
|
|
510
|
+
db.prepare("DELETE FROM board_nodes WHERE board_id = ? AND id = ?").run(boardId, nodeId);
|
|
511
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
512
|
+
db.exec("COMMIT");
|
|
513
|
+
} catch (error) {
|
|
514
|
+
db.exec("ROLLBACK");
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 批量删除节点(可跨看板)。级联删以任一被删节点为端点的边;bump 所属看板 updated。
|
|
522
|
+
* 单事务内完成(SQLite 不支持嵌套 BEGIN,调用方须在自身事务之外调用)。
|
|
523
|
+
* 返回删除的节点/边数量与受影响的看板 id 列表。系统看板节点不删。
|
|
524
|
+
*/
|
|
525
|
+
export function deleteNodesByIds(
|
|
526
|
+
nodeIds: string[],
|
|
527
|
+
): { deletedNodes: number; deletedEdges: number; boards: string[] } {
|
|
528
|
+
if (nodeIds.length === 0) return { deletedNodes: 0, deletedEdges: 0, boards: [] };
|
|
529
|
+
const db = getDb();
|
|
530
|
+
const placeholders = nodeIds.map(() => "?").join(",");
|
|
531
|
+
// 过滤系统看板(running 只读)节点
|
|
532
|
+
const rows = db
|
|
533
|
+
.prepare(`SELECT id, board_id AS boardId FROM board_nodes WHERE id IN (${placeholders}) AND board_id != ?`)
|
|
534
|
+
.all(...nodeIds, SYSTEM_RUNNING_BOARD_ID) as Array<{ id: string; boardId: string }>;
|
|
535
|
+
if (rows.length === 0) return { deletedNodes: 0, deletedEdges: 0, boards: [] };
|
|
536
|
+
const ids = rows.map((r) => r.id);
|
|
537
|
+
const idPlaceholders = ids.map(() => "?").join(",");
|
|
538
|
+
const boardIds = [...new Set(rows.map((r) => r.boardId))];
|
|
539
|
+
const ts = now();
|
|
540
|
+
db.exec("BEGIN");
|
|
541
|
+
try {
|
|
542
|
+
// 级联删边:以任一被删节点为端点(一条边两端都在删除集时只命中一次删除)
|
|
543
|
+
const edgeResult = db
|
|
544
|
+
.prepare(`DELETE FROM board_edges WHERE from_id IN (${idPlaceholders}) OR to_id IN (${idPlaceholders})`)
|
|
545
|
+
.run(...ids, ...ids);
|
|
546
|
+
const nodeResult = db
|
|
547
|
+
.prepare(`DELETE FROM board_nodes WHERE id IN (${idPlaceholders})`)
|
|
548
|
+
.run(...ids);
|
|
549
|
+
// bump 受影响看板 updated(乐观锁基线,防迟到全量保存覆盖)
|
|
550
|
+
for (const bid of boardIds) {
|
|
551
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, bid);
|
|
552
|
+
}
|
|
553
|
+
db.exec("COMMIT");
|
|
554
|
+
return { deletedNodes: Number(nodeResult.changes), deletedEdges: Number(edgeResult.changes), boards: boardIds };
|
|
555
|
+
} catch (error) {
|
|
556
|
+
db.exec("ROLLBACK");
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export interface CreateEdgeInput {
|
|
562
|
+
fromId: string;
|
|
563
|
+
toId: string;
|
|
564
|
+
label?: string | null;
|
|
565
|
+
color?: string | null;
|
|
566
|
+
dashed?: boolean;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** 加连线(两端节点必须属于该看板)。系统看板或看板不存在返回 null。 */
|
|
570
|
+
export function addEdge(boardId: string, input: CreateEdgeInput): BoardEdge | null {
|
|
571
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
572
|
+
if (!getBoardRow(boardId)) return null;
|
|
573
|
+
const db = getDb();
|
|
574
|
+
const from = getNode(boardId, input.fromId);
|
|
575
|
+
const to = getNode(boardId, input.toId);
|
|
576
|
+
if (!from || !to) return null;
|
|
577
|
+
if (input.fromId === input.toId) return null;
|
|
578
|
+
const id = randomUUID();
|
|
579
|
+
const ts = now();
|
|
580
|
+
db.prepare(
|
|
581
|
+
"INSERT INTO board_edges (id, board_id, from_id, to_id, label, color, dashed, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
582
|
+
).run(id, boardId, input.fromId, input.toId, input.label ?? null, input.color ?? null, input.dashed ? 1 : 0, ts, ts);
|
|
583
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
584
|
+
return getEdge(boardId, id) ?? null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function getEdge(boardId: string, edgeId: string): BoardEdge | undefined {
|
|
588
|
+
const row = getDb()
|
|
589
|
+
.prepare("SELECT id, board_id AS boardId, from_id AS fromId, to_id AS toId, label, color, dashed, created, updated FROM board_edges WHERE board_id = ? AND id = ?")
|
|
590
|
+
.get(boardId, edgeId) as BoardEdgeRow | undefined;
|
|
591
|
+
return row ? rowToEdge(row) : undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 删除连线。系统看板或连线不存在返回 false。 */
|
|
595
|
+
export function deleteEdge(boardId: string, edgeId: string): boolean {
|
|
596
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return false;
|
|
597
|
+
const db = getDb();
|
|
598
|
+
if (!getEdge(boardId, edgeId)) return false;
|
|
599
|
+
const ts = now();
|
|
600
|
+
db.prepare("DELETE FROM board_edges WHERE board_id = ? AND id = ?").run(boardId, edgeId);
|
|
601
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 按任务卡的依赖关系 reconcile 画布自动连线(label=kind 识别,禁删语义)。
|
|
608
|
+
* 真相源是 task_card_links:缺失的依赖边补回、多余的自动边删除。
|
|
609
|
+
* 只 reconcile 本卡的「出边」(from=本卡 node),避免双向 reconcile 打架。
|
|
610
|
+
* 本函数不包事务(内部多次 addEdge/deleteEdge 无事务),由调用方包事务。
|
|
611
|
+
*/
|
|
612
|
+
export function syncCardEdges(cardId: string): void {
|
|
613
|
+
const card = getCard(cardId);
|
|
614
|
+
if (!card) return;
|
|
615
|
+
const db = getDb();
|
|
616
|
+
const node = getNodeByRefId(card.boardId, cardId, "taskcard");
|
|
617
|
+
if (!node) return;
|
|
618
|
+
|
|
619
|
+
const existing = db
|
|
620
|
+
.prepare(
|
|
621
|
+
"SELECT id, from_id AS fromId, to_id AS toId, label FROM board_edges " +
|
|
622
|
+
"WHERE board_id = ? AND from_id = ? AND label IN ('prerequisite', 'related')",
|
|
623
|
+
)
|
|
624
|
+
.all(card.boardId, node.id) as Array<{ id: string; fromId: string; toId: string; label: string | null }>;
|
|
625
|
+
|
|
626
|
+
const wanted = listLinks(cardId)
|
|
627
|
+
.map((link) => {
|
|
628
|
+
const targetNode = getNodeByRefId(card.boardId, link.targetCardId, "taskcard");
|
|
629
|
+
return targetNode ? { toId: targetNode.id, label: link.kind } : null;
|
|
630
|
+
})
|
|
631
|
+
.filter((x): x is { toId: string; label: LinkKind } => x !== null);
|
|
632
|
+
|
|
633
|
+
const keyOf = (toId: string, label: string) => `${toId}:${label}`;
|
|
634
|
+
const wantedKeys = new Set(wanted.map((w) => keyOf(w.toId, w.label)));
|
|
635
|
+
const existingKeys = new Set(existing.map((e) => keyOf(e.toId, e.label ?? "")));
|
|
636
|
+
|
|
637
|
+
for (const e of existing) {
|
|
638
|
+
if (!wantedKeys.has(keyOf(e.toId, e.label ?? ""))) {
|
|
639
|
+
deleteEdge(card.boardId, e.id);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const w of wanted) {
|
|
643
|
+
if (!existingKeys.has(keyOf(w.toId, w.label))) {
|
|
644
|
+
addEdge(card.boardId, { fromId: node.id, toId: w.toId, label: w.label });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 任务卡执行会话线 reconcile:真相源 task_cards.session_id。
|
|
651
|
+
* 期望 = 一条 label='exec' 的边(from=本卡 taskcard 节点 → to=session 节点),
|
|
652
|
+
* 原子-链接语义:任务卡通过这条边引用画布上独立存在的执行会话卡。
|
|
653
|
+
* 任一端节点缺失 → 不建线、清掉残留 exec 边(节点后补由 reconcile 兜底)。
|
|
654
|
+
* 禁删语义:手动删 exec 边会被本函数补回;真正的删除 = 清 session_id 或删任务卡。
|
|
655
|
+
* 本函数不包事务(内部多次 addEdge/deleteEdge 无事务),由调用方包事务。
|
|
656
|
+
*/
|
|
657
|
+
export function syncExecEdge(cardId: string): void {
|
|
658
|
+
const card = getCard(cardId);
|
|
659
|
+
if (!card) return;
|
|
660
|
+
const db = getDb();
|
|
661
|
+
const node = getNodeByRefId(card.boardId, cardId, "taskcard");
|
|
662
|
+
if (!node) return;
|
|
663
|
+
|
|
664
|
+
const existing = db
|
|
665
|
+
.prepare("SELECT id, to_id AS toId FROM board_edges WHERE board_id = ? AND from_id = ? AND label = 'exec'")
|
|
666
|
+
.all(card.boardId, node.id) as Array<{ id: string; toId: string }>;
|
|
667
|
+
|
|
668
|
+
// 期望目标:有 sessionId 且会话节点存在 → 该节点
|
|
669
|
+
let wantedTo: string | null = null;
|
|
670
|
+
if (card.sessionId) {
|
|
671
|
+
const sessionNode = getNodeByRefId(card.boardId, card.sessionId, "session");
|
|
672
|
+
if (sessionNode) wantedTo = sessionNode.id;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const e of existing) {
|
|
676
|
+
if (e.toId !== wantedTo) deleteEdge(card.boardId, e.id);
|
|
677
|
+
}
|
|
678
|
+
if (wantedTo && !existing.some((e) => e.toId === wantedTo)) {
|
|
679
|
+
// exec 线虚线(区别于依赖线/用户画的实线,见 task-cards.md)
|
|
680
|
+
addEdge(card.boardId, { fromId: node.id, toId: wantedTo, label: "exec", dashed: true });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* 删会话前清理所有看板上的引用(单事务,不留孤儿):
|
|
686
|
+
* 1. 清掉引用该会话的任务卡 `session_id`(原子-链接:会话被删,任务卡回到无关联);
|
|
687
|
+
* 2. 删除指向该会话节点的 exec 线;
|
|
688
|
+
* 3. 删除画布上该会话的全部 session 节点(级联删边)并 bump 受影响看板 updated。
|
|
689
|
+
* 返回清理的节点数;无引用返回 0,幂等。
|
|
690
|
+
* 供删会话流程调用(会话文件删除在事务外,由调用方负责)。
|
|
691
|
+
*/
|
|
692
|
+
export interface RemoveSessionFromBoardsResult {
|
|
693
|
+
/** 清理的画布节点数 */
|
|
694
|
+
removedNodes: number;
|
|
695
|
+
/** 受影响看板及其最新 updated(前端刷新乐观锁基线,防删除后防抖保存 409) */
|
|
696
|
+
boards: Array<{ boardId: string; updated: number }>;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function removeSessionFromBoards(sessionId: string): RemoveSessionFromBoardsResult {
|
|
700
|
+
const db = getDb();
|
|
701
|
+
const sessionNodes = db
|
|
702
|
+
.prepare("SELECT id, board_id AS boardId FROM board_nodes WHERE kind = 'session' AND ref_id = ?")
|
|
703
|
+
.all(sessionId) as Array<{ id: string; boardId: string }>;
|
|
704
|
+
if (sessionNodes.length === 0) return { removedNodes: 0, boards: [] };
|
|
705
|
+
|
|
706
|
+
const ts = now();
|
|
707
|
+
const touched = new Map<string, number>();
|
|
708
|
+
db.exec("BEGIN");
|
|
709
|
+
try {
|
|
710
|
+
// 1. 任务卡解绑:所有 session_id 引用该会话的卡置空
|
|
711
|
+
db.prepare("UPDATE task_cards SET session_id = NULL, updated = ? WHERE session_id = ?").run(ts, sessionId);
|
|
712
|
+
for (const n of sessionNodes) {
|
|
713
|
+
// 2. 删指向该会话节点的 exec 线(from=taskcard node → to=会话节点)
|
|
714
|
+
db.prepare("DELETE FROM board_edges WHERE board_id = ? AND to_id = ? AND label = 'exec'").run(n.boardId, n.id);
|
|
715
|
+
// 3. 删会话节点 + 关联边
|
|
716
|
+
db.prepare("DELETE FROM board_edges WHERE board_id = ? AND (from_id = ? OR to_id = ?)").run(n.boardId, n.id, n.id);
|
|
717
|
+
db.prepare("DELETE FROM board_nodes WHERE board_id = ? AND id = ?").run(n.boardId, n.id);
|
|
718
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, n.boardId);
|
|
719
|
+
touched.set(n.boardId, ts);
|
|
720
|
+
}
|
|
721
|
+
db.exec("COMMIT");
|
|
722
|
+
return {
|
|
723
|
+
removedNodes: sessionNodes.length,
|
|
724
|
+
boards: [...touched.entries()].map(([boardId, updated]) => ({ boardId, updated })),
|
|
725
|
+
};
|
|
726
|
+
} catch (error) {
|
|
727
|
+
db.exec("ROLLBACK");
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** 该看板全部任务卡(供派生边兜底 reconcile) */
|
|
733
|
+
function listBoardTaskCards(boardId: string): Array<{ id: string }> {
|
|
734
|
+
return getDb()
|
|
735
|
+
.prepare("SELECT id FROM task_cards WHERE board_id = ?")
|
|
736
|
+
.all(boardId) as Array<{ id: string }>;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 画布加载后兜底:对该看板所有任务卡 reconcile 派生边(依赖线 syncCardEdges + 执行会话线 syncExecEdge)。
|
|
741
|
+
* 幂等(只补缺失边);供进入看板/重载后调一次,之后由真相源写入点(建卡/改依赖/绑 sessionId/删卡)各自触发。
|
|
742
|
+
* 不包事务,由调用方包。
|
|
743
|
+
*/
|
|
744
|
+
export function reconcileBoardTaskEdges(boardId: string): void {
|
|
745
|
+
const cards = listBoardTaskCards(boardId);
|
|
746
|
+
for (const card of cards) {
|
|
747
|
+
syncCardEdges(card.id);
|
|
748
|
+
syncExecEdge(card.id);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 任务卡画布节点 upsert:nodeId 存在 → 绑定 ref_id=cardId(转正);
|
|
754
|
+
* 不存在 → 新建 taskcard node(id=nodeId,与 tldraw shape.id 对齐,后续全量保存覆盖不重复)。
|
|
755
|
+
* 无事务(单语句 + bump),由调用方包事务。
|
|
756
|
+
*/
|
|
757
|
+
export function upsertTaskCardNode(
|
|
758
|
+
nodeId: string,
|
|
759
|
+
boardId: string,
|
|
760
|
+
card: { id: string; number: number; name: string; readyStatus: string; execStatus: string; priority: number; due: number | null },
|
|
761
|
+
x: number,
|
|
762
|
+
y: number,
|
|
763
|
+
): BoardNode | null {
|
|
764
|
+
const db = getDb();
|
|
765
|
+
if (boardId === SYSTEM_RUNNING_BOARD_ID) return null;
|
|
766
|
+
if (!getBoardRow(boardId)) return null;
|
|
767
|
+
const ts = now();
|
|
768
|
+
// 完整 shapeProps 落库:hydrate 恢复 task-card shape 依赖它(建卡时 node 可能不存在)
|
|
769
|
+
const shapeProps = {
|
|
770
|
+
cardId: card.id,
|
|
771
|
+
number: card.number,
|
|
772
|
+
name: card.name,
|
|
773
|
+
readyStatus: card.readyStatus,
|
|
774
|
+
execStatus: card.execStatus,
|
|
775
|
+
priority: card.priority,
|
|
776
|
+
due: card.due ?? undefined,
|
|
777
|
+
expanded: false,
|
|
778
|
+
w: 220,
|
|
779
|
+
h: 120,
|
|
780
|
+
};
|
|
781
|
+
const props = JSON.stringify({ parentId: null, shapeProps });
|
|
782
|
+
const existing = db.prepare("SELECT id FROM board_nodes WHERE id = ?").get(nodeId);
|
|
783
|
+
if (existing) {
|
|
784
|
+
db.prepare("UPDATE board_nodes SET ref_id = ?, props = ?, updated = ? WHERE id = ?").run(card.id, props, ts, nodeId);
|
|
785
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
786
|
+
} else {
|
|
787
|
+
db.prepare(
|
|
788
|
+
"INSERT INTO board_nodes (id, board_id, kind, ref_id, x, y, w, h, expanded, props, created, updated) " +
|
|
789
|
+
"VALUES (?, ?, 'taskcard', ?, ?, ?, 220, 120, 0, ?, ?, ?)",
|
|
790
|
+
).run(nodeId, boardId, card.id, x, y, props, ts, ts);
|
|
791
|
+
db.prepare("UPDATE boards SET updated = ? WHERE id = ?").run(ts, boardId);
|
|
792
|
+
}
|
|
793
|
+
return getNode(boardId, nodeId) ?? null;
|
|
794
|
+
}
|