@baique/pi-web-sky 0.1.39 → 0.1.41
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 +7 -12
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +51 -51
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/routes-manifest.json +3 -45
- 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]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/route.js +2 -2
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/all-providers/route.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/providers/route.js +1 -1
- package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -0
- package/.next/server/app/api/boards/[id]/reconcile/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/boards/[id]/route.js +1 -1
- package/.next/server/app/api/boards/purge-orphans/route.js +1 -1
- package/.next/server/app/api/boards/reorder/route.js +1 -1
- package/.next/server/app/api/boards/route.js +1 -1
- package/.next/server/app/api/cwd/browse/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +2 -2
- package/.next/server/app/api/default-cwd/route.js +2 -2
- package/.next/server/app/api/file-index/route.js +2 -2
- package/.next/server/app/api/files/[...path]/route.js +3 -3
- package/.next/server/app/api/files/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/git/diff/route.js +1 -1
- package/.next/server/app/api/git/status/route.js +1 -1
- package/.next/server/app/api/home/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +2 -2
- package/.next/server/app/api/models-config/catalog/route.js +1 -1
- package/.next/server/app/api/models-config/discover/route.js +1 -1
- package/.next/server/app/api/models-config/route.js +1 -1
- package/.next/server/app/api/models-config/test/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +2 -2
- package/.next/server/app/api/project-trust/route.js +2 -2
- package/.next/server/app/api/quota/route.js +1 -1
- package/.next/server/app/api/search/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +3 -3
- package/.next/server/app/api/sessions/[id]/context/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/todos/route.js +2 -2
- package/.next/server/app/api/sessions/route.js +2 -2
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/install/route.js +2 -2
- package/.next/server/app/api/skills/route.js +1 -1
- package/.next/server/app/api/skills/search/route.js +1 -1
- package/.next/server/app/api/skills/update/route.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/route.js +1 -1
- package/.next/server/app/api/task-cards/[id]/route.js +1 -1
- package/.next/server/app/api/task-cards/route.js +1 -1
- package/.next/server/app/api/task-scheduler/status/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/route.js +1 -1
- package/.next/server/app/api/tasks/reorder/route.js +1 -1
- package/.next/server/app/api/tasks/route.js +1 -1
- package/.next/server/app/api/terminal/[id]/events/route.js +2 -2
- package/.next/server/app/api/terminal/[id]/route.js +2 -2
- package/.next/server/app/api/terminal/route.js +2 -2
- package/.next/server/app/api/worktrees/route.js +2 -2
- 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/page.js +7 -7
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -12
- package/.next/server/chunks/1094.js +2 -2
- package/.next/server/chunks/1601.js +1 -1
- package/.next/server/chunks/1860.js +1 -1
- package/.next/server/chunks/2075.js +1 -1
- package/.next/server/chunks/2169.js +1 -1
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/2524.js +1 -0
- package/.next/server/chunks/3012.js +1 -1
- package/.next/server/chunks/{5760.js → 3379.js} +1 -1
- package/.next/server/chunks/3500.js +1 -1
- package/.next/server/chunks/4226.js +5 -2
- package/.next/server/chunks/4267.js +1 -1
- package/.next/server/chunks/4563.js +1 -1
- package/.next/server/chunks/5318.js +1 -1
- package/.next/server/chunks/542.js +3 -44
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6052.js +1 -1
- package/.next/server/chunks/6253.js +1 -1
- package/.next/server/chunks/6288.js +1 -1
- package/.next/server/chunks/6366.js +1 -1
- package/.next/server/chunks/673.js +1 -1
- package/.next/server/chunks/6916.js +2 -2
- package/.next/server/chunks/7128.js +43 -0
- package/.next/server/chunks/7265.js +2 -2
- package/.next/server/chunks/7799.js +1 -1
- package/.next/server/chunks/8106.js +1 -1
- package/.next/server/chunks/8333.js +1 -1
- package/.next/server/chunks/8604.js +1 -1
- package/.next/server/chunks/861.js +1 -0
- package/.next/server/chunks/8713.js +1 -1
- package/.next/server/chunks/8782.js +2 -2
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/chunks/922.js +2 -2
- package/.next/server/chunks/9280.js +6 -6
- package/.next/server/chunks/9510.js +1 -1
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +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/{103.23142a67b02246a8.js → 103.6c91a059aa01d92c.js} +2 -2
- package/.next/static/chunks/{1032.12b382a9096ab2a4.js → 1032.9e8f793d46f463f8.js} +1 -1
- package/.next/static/chunks/{1063.775c442bdbeef08b.js → 1063.470df8a037dc8409.js} +1 -1
- package/.next/static/chunks/1a258343.f4fc2c133917ded8.js +2 -0
- package/.next/static/chunks/{1ba63609.b2ae90f43319a29e.js → 1ba63609.569928c7234f832c.js} +1 -1
- package/.next/static/chunks/{2181.ba21239d3ac39f81.js → 2181.b8dda21843ef5e74.js} +1 -1
- package/.next/static/chunks/{2325.23caf22075e51aa5.js → 2325.23e538886331cf64.js} +1 -1
- package/.next/static/chunks/{2356.523277bb8c6bf72a.js → 2356.b7312589c345edec.js} +1 -1
- package/.next/static/chunks/{268.72794ee2773c2060.js → 268.a1d90aca268be18a.js} +1 -1
- package/.next/static/chunks/{2695.6f49435bae1e6f33.js → 2695.5624aa3aae540e87.js} +1 -1
- package/.next/static/chunks/{2703.495a65ed14b2f094.js → 2703.097bc88286733740.js} +1 -1
- package/.next/static/chunks/{2821.934bcf60fbdc28c6.js → 2821.d2392a93d192fe92.js} +1 -1
- package/.next/static/chunks/3308.2cd6f20e6c0f4e92.js +73 -0
- package/.next/static/chunks/{3336.a76f9485fe7cb762.js → 3336.9b1dc9ec3507d64f.js} +1 -1
- package/.next/static/chunks/3546.779d2c7646aa77cc.js +1 -0
- package/.next/static/chunks/{370.8e8f3cf8eb0e29dd.js → 370.cde2a84caa84054f.js} +1 -1
- package/.next/static/chunks/{3716.72bf4c9d7325debd.js → 3716.5bac496cea32ca8f.js} +1 -1
- package/.next/static/chunks/{3995.eca0f5bef1fc3ae5.js → 3995.cd112debcd64c610.js} +1 -1
- package/.next/static/chunks/{4004.74b9bcf5478b1663.js → 4004.a8f79ef62f32aa30.js} +2 -2
- package/.next/static/chunks/{4202.8440832386f023c7.js → 4202.96f5b1beb8b3e3f5.js} +1 -1
- package/.next/static/chunks/{4238.6f102dcbcf2ae339.js → 4238.28efbf9aa0c82d90.js} +1 -1
- package/.next/static/chunks/{4405.7a19458960a1058e.js → 4405.cda46a596a02aef7.js} +1 -1
- package/.next/static/chunks/54a60aa6.5e4421e7afde4de8.js +78 -0
- package/.next/static/chunks/{6121.eb9023b1cbc78ee2.js → 6121.5e650e714b402b6f.js} +1 -1
- package/.next/static/chunks/{6195.1153369f5f8b13a2.js → 6195.1b7f9dc127381bbc.js} +1 -1
- package/.next/static/chunks/6380.d148a8e2c701ad78.js +68 -0
- package/.next/static/chunks/6582.fb2c709abc2c5981.js +1 -0
- package/.next/static/chunks/6be7e44c.00d0163f409986f0.js +1 -0
- package/.next/static/chunks/{7331.2add3c4e125a2dba.js → 7331.4afa66d6353c8e07.js} +1 -1
- package/.next/static/chunks/752.a05a2904b96af4d0.js +1 -0
- package/.next/static/chunks/{8200.a36b9f8c983c6b9e.js → 8200.350d650649b977eb.js} +1 -1
- package/.next/static/chunks/{8885.cf60cac98bf8b70c.js → 8885.2c060cb45c85547b.js} +1 -1
- package/.next/static/chunks/{918.6a9bbe564e4a2d0f.js → 918.593eb466d35cb8ad.js} +1 -1
- package/.next/static/chunks/{9698.4d09a99b9d99f3e5.js → 9698.758755d0387e0e22.js} +2 -2
- package/.next/static/chunks/97.fb73c551e14b3612.js +2 -0
- package/.next/static/chunks/{993e5495.8797001fcfcf2889.js → 993e5495.12cdfbc3af5c79e9.js} +1 -1
- package/.next/static/chunks/{9974.9a0f79beca65e8c4.js → 9974.5ee982e074ad047e.js} +1 -1
- package/.next/static/chunks/app/_global-error/page-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/bash-output/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/agent/running/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/app-update/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/auth/all-providers/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/auth/api-key/[provider]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/auth/login/[provider]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/auth/logout/[provider]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/auth/providers/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/boards/[id]/reconcile/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/boards/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/boards/purge-orphans/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/boards/reorder/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/boards/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/cwd/browse/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/cwd/validate/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/default-cwd/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/file-index/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/files/[...path]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/git/diff/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/git/status/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/home/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/mcp/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/models/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/models-config/catalog/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/models-config/discover/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/models-config/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/models-config/test/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/project-trust/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/quota/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/search/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/context/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/state/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/todos/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/skills/check/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/skills/install/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/skills/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/skills/search/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/skills/update/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/task-card-questions/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/task-cards/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/task-cards/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/task-scheduler/status/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/tasks/[id]/board/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/tasks/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/tasks/reorder/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/tasks/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/terminal/[id]/events/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/terminal/[id]/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/terminal/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/api/worktrees/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/{layout-7761e430ed68b347.js → layout-2bec2662143a3d1b.js} +1 -1
- package/.next/static/chunks/app/manifest.webmanifest/route-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/app/page-e38c6cf3174fa143.js +89 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-bb8f79f321e26750.js +1 -0
- package/.next/static/chunks/webpack-b4924d2a22ad3650.js +1 -0
- package/.next/static/css/0c7244f7214af81f.css +1 -0
- package/.next/static/css/12456255969ac8de.css +31 -0
- package/.next/trace +8 -49
- package/.next/trace-build +1 -1
- package/.next/types/app/api/boards/[id]/{reconcile-task-edges → reconcile}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -7
- package/.next/types/validator.ts +3 -48
- package/README.md +6 -9
- package/THIRD_PARTY_NOTICES.md +10 -82
- package/bin/pi-web-sky.js +3 -3
- package/lib/board-reconcile-scheduler.ts +56 -0
- package/lib/board-reconcile.test.mjs +252 -0
- package/lib/board-reconcile.ts +421 -0
- package/lib/board-store.test.mjs +26 -4
- package/lib/board-store.ts +44 -6
- package/lib/board-types.ts +7 -2
- package/lib/i18n/messages/en.ts +1 -1
- package/lib/i18n/messages/zh-CN.ts +1 -1
- package/lib/session-delete.ts +5 -0
- package/lib/sqlite-db.ts +13 -1
- package/lib/task-card-store.test.mjs +16 -1
- package/lib/task-card-store.ts +37 -1
- package/lib/task-scheduler.test.mjs +79 -0
- package/lib/task-scheduler.ts +195 -21
- package/lib/yjs-room-server.mjs +139 -0
- package/package.json +12 -9
- package/server.mjs +7 -7
- package/.next/server/app/api/boards/[id]/canvas/route.js +0 -1
- package/.next/server/app/api/boards/[id]/canvas/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/boards/[id]/edges/[eid]/route.js +0 -1
- package/.next/server/app/api/boards/[id]/edges/[eid]/route.js.nft.json +0 -1
- package/.next/server/app/api/boards/[id]/edges/[eid]/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/boards/[id]/edges/route.js +0 -1
- package/.next/server/app/api/boards/[id]/edges/route.js.nft.json +0 -1
- package/.next/server/app/api/boards/[id]/edges/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js +0 -1
- package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js.nft.json +0 -1
- package/.next/server/app/api/boards/[id]/nodes/[nid]/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/boards/[id]/nodes/route.js +0 -1
- package/.next/server/app/api/boards/[id]/nodes/route.js.nft.json +0 -1
- package/.next/server/app/api/boards/[id]/nodes/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js +0 -1
- package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js.nft.json +0 -1
- package/.next/server/app/api/boards/[id]/reconcile-task-edges/route_client-reference-manifest.js +0 -1
- package/.next/static/chunks/3308.857c554428b3294e.js +0 -73
- package/.next/static/chunks/333.bf0651c789b2eb33.js +0 -1
- package/.next/static/chunks/5315.dba7515ae6e139ae.js +0 -4
- package/.next/static/chunks/54a60aa6.0e43cde7298ebcb7.js +0 -78
- package/.next/static/chunks/7999.879d5a20e8641752.js +0 -259
- package/.next/static/chunks/app/_global-error/page-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/bash-output/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/agent/running/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/app-update/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/auth/all-providers/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/auth/api-key/[provider]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/auth/login/[provider]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/auth/logout/[provider]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/auth/providers/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/canvas/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/edges/[eid]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/edges/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/nodes/[nid]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/nodes/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/reconcile-task-edges/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/purge-orphans/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/reorder/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/boards/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/cwd/browse/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/cwd/validate/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/default-cwd/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/file-index/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/files/[...path]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/git/diff/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/git/status/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/home/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/mcp/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/models/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/models-config/catalog/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/models-config/discover/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/models-config/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/models-config/test/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/project-trust/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/quota/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/search/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/context/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/state/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/todos/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/skills/check/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/skills/install/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/skills/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/skills/search/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/skills/update/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/task-card-questions/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/task-cards/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/task-cards/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/task-scheduler/status/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/tasks/[id]/board/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/tasks/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/tasks/reorder/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/tasks/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/terminal/[id]/events/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/terminal/[id]/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/terminal/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/api/worktrees/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/manifest.webmanifest/route-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/app/page-2c87c41e7f848f45.js +0 -89
- package/.next/static/chunks/e3e7ddb2.a35db430b3564999.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-d6f52627eddf9119.js +0 -1
- package/.next/static/chunks/webpack-4bbf04891e910713.js +0 -1
- package/.next/static/css/80c818c5781fbadc.css +0 -4
- package/.next/static/css/d3a2b5077b52c727.css +0 -1
- package/.next/types/app/api/boards/[id]/canvas/route.ts +0 -353
- package/.next/types/app/api/boards/[id]/edges/[eid]/route.ts +0 -353
- package/.next/types/app/api/boards/[id]/edges/route.ts +0 -353
- package/.next/types/app/api/boards/[id]/nodes/[nid]/route.ts +0 -353
- package/.next/types/app/api/boards/[id]/nodes/route.ts +0 -353
- package/lib/board-search.test.mjs +0 -110
- package/lib/board-search.ts +0 -63
- package/lib/file-explorer-state.test.mjs +0 -63
- package/lib/file-explorer-state.ts +0 -36
- package/lib/sync-room-server.mjs +0 -137
- /package/.next/server/app/api/boards/[id]/{canvas → reconcile}/route.js.nft.json +0 -0
- /package/.next/static/{IzmOG04jJst1VpUvr_fr5 → 8V884w_cp5HEp_rRpjnCR}/_buildManifest.js +0 -0
- /package/.next/static/{IzmOG04jJst1VpUvr_fr5 → 8V884w_cp5HEp_rRpjnCR}/_ssgManifest.js +0 -0
package/lib/task-card-store.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface TaskCard {
|
|
|
43
43
|
sessionId: string | null;
|
|
44
44
|
/** 跨进程派发互斥 token:谁抢到谁执行(多实例共库防双会话) */
|
|
45
45
|
dispatchToken: string | null;
|
|
46
|
+
/** 状态判定实例归属:派发/续会话时写入的实例 id;null=旧数据(任何实例可接管) */
|
|
47
|
+
owner: string | null;
|
|
48
|
+
/** 归属心跳(ms epoch):owner 每 ~10s 刷新一次;过期视 owner 已死可接管 */
|
|
49
|
+
heartbeat: number;
|
|
46
50
|
created: number;
|
|
47
51
|
updated: number;
|
|
48
52
|
}
|
|
@@ -73,6 +77,8 @@ interface TaskCardRow {
|
|
|
73
77
|
retryCount: number;
|
|
74
78
|
sessionId: string | null;
|
|
75
79
|
dispatchToken: string | null;
|
|
80
|
+
owner: string | null;
|
|
81
|
+
heartbeat: number;
|
|
76
82
|
created: number;
|
|
77
83
|
updated: number;
|
|
78
84
|
}
|
|
@@ -106,7 +112,7 @@ function rowToCard(row: TaskCardRow): TaskCard {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
const CARD_COLUMNS =
|
|
109
|
-
"id, board_id AS boardId, project_key AS projectKey, number, name, description, ready_status AS readyStatus, exec_status AS execStatus, priority, due, attachments, cwd, use_worktree AS useWorktree, max_retries AS maxRetries, retry_count AS retryCount, session_id AS sessionId, dispatch_token AS dispatchToken, created, updated";
|
|
115
|
+
"id, board_id AS boardId, project_key AS projectKey, number, name, description, ready_status AS readyStatus, exec_status AS execStatus, priority, due, attachments, cwd, use_worktree AS useWorktree, max_retries AS maxRetries, retry_count AS retryCount, session_id AS sessionId, dispatch_token AS dispatchToken, owner, heartbeat, created, updated";
|
|
110
116
|
|
|
111
117
|
function getCardRow(id: string): TaskCardRow | undefined {
|
|
112
118
|
return getDb()
|
|
@@ -120,6 +126,17 @@ export function getCard(id: string): TaskCard | undefined {
|
|
|
120
126
|
return row ? rowToCard(row) : undefined;
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
/** 按 id 列表批量取卡(保持传入顺序;不存在 id 跳过)。可见卡状态查询用,单次 IN 查询。 */
|
|
130
|
+
export function listCardsByIds(ids: string[]): TaskCard[] {
|
|
131
|
+
if (ids.length === 0) return [];
|
|
132
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
133
|
+
const rows = getDb()
|
|
134
|
+
.prepare(`SELECT ${CARD_COLUMNS} FROM task_cards WHERE id IN (${placeholders})`)
|
|
135
|
+
.all(...ids) as unknown as TaskCardRow[];
|
|
136
|
+
const byId = new Map(rows.map((r) => [r.id, rowToCard(r)]));
|
|
137
|
+
return ids.map((id) => byId.get(id)).filter((c): c is TaskCard => Boolean(c));
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
/** 某看板的全部任务卡,按编号升序。 */
|
|
124
141
|
export function listCards(boardId: string): TaskCard[] {
|
|
125
142
|
const rows = getDb()
|
|
@@ -202,6 +219,8 @@ export function updateCard(
|
|
|
202
219
|
maxRetries?: number;
|
|
203
220
|
sessionId?: string | null;
|
|
204
221
|
retryCount?: number;
|
|
222
|
+
owner?: string | null;
|
|
223
|
+
heartbeat?: number;
|
|
205
224
|
},
|
|
206
225
|
): TaskCard | null {
|
|
207
226
|
const db = getDb();
|
|
@@ -231,6 +250,8 @@ export function updateCard(
|
|
|
231
250
|
if (patch.maxRetries !== undefined) bump("max_retries", patch.maxRetries);
|
|
232
251
|
if (patch.sessionId !== undefined) bump("session_id", patch.sessionId);
|
|
233
252
|
if (patch.retryCount !== undefined) bump("retry_count", patch.retryCount);
|
|
253
|
+
if (patch.owner !== undefined) bump("owner", patch.owner);
|
|
254
|
+
if (patch.heartbeat !== undefined) bump("heartbeat", patch.heartbeat);
|
|
234
255
|
if (sets.length === 0) return rowToCard(existing);
|
|
235
256
|
|
|
236
257
|
bump("updated", now());
|
|
@@ -428,6 +449,21 @@ export function clearDispatchToken(cardId: string, token: string): void {
|
|
|
428
449
|
.run(cardId, now(), token);
|
|
429
450
|
}
|
|
430
451
|
|
|
452
|
+
/**
|
|
453
|
+
* 归属心跳刷新:把本实例拥有且仍在活跃生命周期(running/review/waiting_reply)
|
|
454
|
+
* 的卡的 heartbeat 推到 now。多实例共库时,非 owner 靠它判断 owner 是否活着
|
|
455
|
+
* (心跳过期 = owner 已死 → 可接管判定),避免误翻他人正在跑的卡。
|
|
456
|
+
*/
|
|
457
|
+
export function heartbeatOwnedCards(owner: string): number {
|
|
458
|
+
const res = getDb()
|
|
459
|
+
.prepare(
|
|
460
|
+
`UPDATE task_cards SET heartbeat = ?1
|
|
461
|
+
WHERE owner = ?2 AND exec_status IN ('running', 'review', 'waiting_reply')`,
|
|
462
|
+
)
|
|
463
|
+
.run(now(), owner);
|
|
464
|
+
return Number(res.changes);
|
|
465
|
+
}
|
|
466
|
+
|
|
431
467
|
/** 调度器已派发且正在运行的任务卡数(全局并发闸门用)。 */
|
|
432
468
|
export function countRunningDispatched(): number {
|
|
433
469
|
const row = getDb()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
|
|
6
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
7
|
+
const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
8
|
+
const { createCard, updateCard, getCard, heartbeatOwnedCards } = await jiti.import("./task-card-store.ts");
|
|
9
|
+
const { createBoard } = await jiti.import("./board-store.ts");
|
|
10
|
+
// 只 import 纯函数/归属守卫,不触发 rpc-manager(避免拉起整个 coding-agent SDK)
|
|
11
|
+
const { isCardHandledByInstance, OWNER_STALE_MS } = await jiti.import("./task-scheduler.ts");
|
|
12
|
+
|
|
13
|
+
function freshDb() {
|
|
14
|
+
setDbForTesting(new DatabaseSync(":memory:"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("迁移后 task_cards 含 owner/heartbeat 列", () => {
|
|
18
|
+
freshDb();
|
|
19
|
+
const b = createBoard("proj-a", "看板A");
|
|
20
|
+
const c = createCard({ boardId: b.id, projectKey: "proj-a", name: "卡" });
|
|
21
|
+
assert.equal(c.owner, null);
|
|
22
|
+
assert.equal(c.heartbeat, 0);
|
|
23
|
+
const cols = getDb().prepare("PRAGMA table_info(task_cards)").all().map((r) => r.name);
|
|
24
|
+
assert.ok(cols.includes("owner"));
|
|
25
|
+
assert.ok(cols.includes("heartbeat"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("updateCard 可写 owner/heartbeat", () => {
|
|
29
|
+
freshDb();
|
|
30
|
+
const b = createBoard("proj-a", "看板A");
|
|
31
|
+
const c = createCard({ boardId: b.id, projectKey: "proj-a", name: "卡" });
|
|
32
|
+
const ts = 12345;
|
|
33
|
+
const updated = updateCard(c.id, { execStatus: "running", owner: "inst-a", heartbeat: ts });
|
|
34
|
+
assert.equal(updated?.owner, "inst-a");
|
|
35
|
+
assert.equal(updated?.heartbeat, ts);
|
|
36
|
+
assert.equal(getCard(c.id)?.owner, "inst-a");
|
|
37
|
+
assert.equal(getCard(c.id)?.heartbeat, ts);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("heartbeatOwnedCards 只刷本实例活跃生命周期(running/review/waiting_reply)卡", () => {
|
|
41
|
+
freshDb();
|
|
42
|
+
const b = createBoard("proj-a", "看板A");
|
|
43
|
+
const running = createCard({ boardId: b.id, projectKey: "proj-a", name: "跑" });
|
|
44
|
+
const review = createCard({ boardId: b.id, projectKey: "proj-a", name: "审" });
|
|
45
|
+
const waiting = createCard({ boardId: b.id, projectKey: "proj-a", name: "等" });
|
|
46
|
+
const done = createCard({ boardId: b.id, projectKey: "proj-a", name: "完" });
|
|
47
|
+
const other = createCard({ boardId: b.id, projectKey: "proj-a", name: "别人" });
|
|
48
|
+
updateCard(running.id, { execStatus: "running", owner: "me" });
|
|
49
|
+
updateCard(review.id, { execStatus: "review", owner: "me" });
|
|
50
|
+
updateCard(waiting.id, { execStatus: "waiting_reply", owner: "me" });
|
|
51
|
+
updateCard(done.id, { execStatus: "done", owner: "me" });
|
|
52
|
+
updateCard(other.id, { execStatus: "running", owner: "them" });
|
|
53
|
+
|
|
54
|
+
heartbeatOwnedCards("me");
|
|
55
|
+
assert.ok(getCard(running.id)?.heartbeat > 0);
|
|
56
|
+
assert.ok(getCard(review.id)?.heartbeat > 0);
|
|
57
|
+
assert.ok(getCard(waiting.id)?.heartbeat > 0);
|
|
58
|
+
assert.equal(getCard(done.id)?.heartbeat, 0); // 结束态不刷新
|
|
59
|
+
assert.equal(getCard(other.id)?.heartbeat, 0); // 别人家不刷新
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("isCardHandledByInstance 归属守卫矩阵(#27 核心)", () => {
|
|
63
|
+
const me = "inst-me";
|
|
64
|
+
const them = "inst-them";
|
|
65
|
+
const now = 1_000_000;
|
|
66
|
+
const fresh = now - 5_000; // 5s 前心跳(OWNER_STALE_MS=120s 内 → 活着)
|
|
67
|
+
const stale = now - (OWNER_STALE_MS + 1_000); // 超期 → 已死
|
|
68
|
+
|
|
69
|
+
// owner 为空(旧数据)→ 任何实例可处理
|
|
70
|
+
assert.equal(isCardHandledByInstance({ owner: null, heartbeat: 0 }, me, now), true);
|
|
71
|
+
// owner == 我 → 我负责
|
|
72
|
+
assert.equal(isCardHandledByInstance({ owner: me, heartbeat: 0 }, me, now), true);
|
|
73
|
+
// owner == 他人 且 心跳新鲜 → 跳过(← 防误翻他人正在跑的会话)
|
|
74
|
+
assert.equal(isCardHandledByInstance({ owner: them, heartbeat: fresh }, me, now), false);
|
|
75
|
+
// owner == 他人 但 心跳过期 → 接管
|
|
76
|
+
assert.equal(isCardHandledByInstance({ owner: them, heartbeat: stale }, me, now), true);
|
|
77
|
+
// 边界:正好 = OWNER_STALE_MS 视为过期(>=)
|
|
78
|
+
assert.equal(isCardHandledByInstance({ owner: them, heartbeat: now - OWNER_STALE_MS }, me, now), true);
|
|
79
|
+
});
|
package/lib/task-scheduler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { getBoard } from "./board-store";
|
|
4
|
+
import { reconcileBoard } from "./board-reconcile";
|
|
4
5
|
import { startRpcSession, getRunningRpcSessionIds } from "./rpc-manager";
|
|
5
6
|
import { resolveSessionPath } from "./session-reader";
|
|
6
7
|
import { assignSessionToTask } from "./task-store";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
updateCard,
|
|
17
18
|
acquireDispatchToken,
|
|
18
19
|
clearDispatchToken,
|
|
20
|
+
heartbeatOwnedCards,
|
|
19
21
|
type TaskCard,
|
|
20
22
|
type TaskCardQuestion,
|
|
21
23
|
} from "./task-card-store";
|
|
@@ -36,6 +38,11 @@ import {
|
|
|
36
38
|
export const TASK_SCHEDULER_INTERVAL_MS = 10_000;
|
|
37
39
|
export const TASK_SCHEDULER_MAX_CONCURRENCY = 1;
|
|
38
40
|
|
|
41
|
+
/** 归属心跳刷新周期(独立于长 tick,防审核阻塞期间心跳断更) */
|
|
42
|
+
export const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
43
|
+
/** owner 心跳过期阈值:超时视为 owner 已死,其他实例可接管判定 */
|
|
44
|
+
export const OWNER_STALE_MS = 120_000;
|
|
45
|
+
|
|
39
46
|
// ============================================================================
|
|
40
47
|
// S3 巡检/审核参数
|
|
41
48
|
// ============================================================================
|
|
@@ -58,10 +65,15 @@ export const SESSION_SETTLE_MS = 5_000;
|
|
|
58
65
|
declare global {
|
|
59
66
|
var __piTaskScheduler: {
|
|
60
67
|
timer: ReturnType<typeof setInterval>;
|
|
68
|
+
heartbeatTimer: ReturnType<typeof setInterval>;
|
|
61
69
|
startedAt: number;
|
|
62
70
|
/** 最近一次调度动作(挂 globalThis 防 instrumentation 与 API 路由加载两份模块实例不互通) */
|
|
63
71
|
lastAction: TaskSchedulerLastAction;
|
|
72
|
+
/** 调度器此刻正在做的动作(看板状态展示:工作中/休眠两态);无=休眠 */
|
|
73
|
+
activity: SchedulerActivity | null;
|
|
64
74
|
} | undefined;
|
|
75
|
+
/** 本进程实例 id(防热重载换 id;多实例各持不同 id,作为任务卡 owner) */
|
|
76
|
+
var __piWebInstanceId: string | undefined;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/** 派发 cwd:card.cwd 优先;否则看板所属项目根(project_key 若是存在的目录)。 */
|
|
@@ -111,6 +123,47 @@ async function ensureExecutionSession(
|
|
|
111
123
|
return { ...fresh, created: true };
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
/** 本进程实例 id:随机 UUID,globalThis 缓存(热重载不换),多实例各不同。 */
|
|
127
|
+
export function getInstanceId(): string {
|
|
128
|
+
if (!globalThis.__piWebInstanceId) globalThis.__piWebInstanceId = randomUUID();
|
|
129
|
+
return globalThis.__piWebInstanceId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 归属守卫:这张卡的「状态判定」该不该由本实例做(#27 根因修复)。
|
|
134
|
+
* - owner 为空(旧数据/未升级实例派发)→ 任何实例可处理(保持旧语义)
|
|
135
|
+
* - owner == 我 → 我负责
|
|
136
|
+
* - owner == 他人且心跳新鲜 → 他人活着,跳过(防误翻他人正在跑的会话)
|
|
137
|
+
* - owner == 他人但心跳过期 → 他人已死,接管
|
|
138
|
+
*/
|
|
139
|
+
export function isCardHandledByInstance(
|
|
140
|
+
card: Pick<TaskCard, "owner" | "heartbeat">,
|
|
141
|
+
myId: string,
|
|
142
|
+
now: number,
|
|
143
|
+
): boolean {
|
|
144
|
+
return !card.owner || card.owner === myId || now - card.heartbeat >= OWNER_STALE_MS;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 自愈:本地 registry 里真实在跑、但卡状态却是 review 的 → 拉回 running 并接管归属。
|
|
149
|
+
* 兜底任何来源(未升级旧实例 / 竞态 / 手动)把还在跑的卡误标成 review 的情况——
|
|
150
|
+
* 只有会话确实在跑(isRunning 真)才回拉,真结束待审核的卡不会被动。
|
|
151
|
+
*/
|
|
152
|
+
export async function restoreRunningReviewCards(): Promise<number> {
|
|
153
|
+
const myId = getInstanceId();
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const runningIds = new Set(getRunningRpcSessionIds());
|
|
156
|
+
let restored = 0;
|
|
157
|
+
for (const card of listCardsByExecStatus(["review"])) {
|
|
158
|
+
if (!card.sessionId) continue;
|
|
159
|
+
if (!runningIds.has(card.sessionId)) continue;
|
|
160
|
+
updateCard(card.id, { execStatus: "running", owner: myId, heartbeat: now });
|
|
161
|
+
console.log(`[task-scheduler] #${card.number} ${card.name} 自愈:会话仍在跑 → 拉回 running`);
|
|
162
|
+
restored += 1;
|
|
163
|
+
}
|
|
164
|
+
return restored;
|
|
165
|
+
}
|
|
166
|
+
|
|
114
167
|
/** 派发单卡:建/复执行会话 → 命名 #N 标题 → 任务看板归属 → node 绑 session → 发 prompt → running。
|
|
115
168
|
* 跨进程互斥:派发前先原子抢 dispatch_token(谁抢到谁执行),防多实例共库双会话。 */
|
|
116
169
|
export async function dispatchCard(card: TaskCard): Promise<boolean> {
|
|
@@ -135,11 +188,16 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
|
|
|
135
188
|
const board = getBoard(card.boardId);
|
|
136
189
|
if (board?.taskId) {
|
|
137
190
|
assignSessionToTask(session.realSessionId, board.taskId);
|
|
191
|
+
// 注意:不在 assignSessionToTask 后 reconcile——此刻卡 sessionId 未落表,
|
|
192
|
+
// reconcile 只能补会话卡、建不了 exec 线 → 造成「先出卡后出线」。
|
|
193
|
+
// 统一在 updateCard(sessionId) 之后触发(补卡+建线同一次事务、一次广播)。
|
|
138
194
|
}
|
|
139
195
|
|
|
140
196
|
// 会话正忙(已在流式/处理中):说明上一轮已发过 prompt,直接标 running,不重复发
|
|
141
197
|
if (session.session.isRunning()) {
|
|
142
|
-
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
|
|
198
|
+
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
|
|
199
|
+
// sessionId 落表后立即 reconcile:补执行会话卡 + exec 线(卡线同现,不依赖定时兜底)
|
|
200
|
+
if (board?.taskId) reconcileTaskBoard(card.boardId);
|
|
143
201
|
watchForAgentEnd(card, session.session);
|
|
144
202
|
clearDispatchToken(card.id, token);
|
|
145
203
|
console.log(`[task-scheduler] #${card.number} ${card.name} 会话忙(复用中),标 running 不重发`);
|
|
@@ -148,7 +206,9 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
|
|
|
148
206
|
|
|
149
207
|
await session.session.send({ type: "prompt", message: buildTaskPrompt(card) });
|
|
150
208
|
|
|
151
|
-
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
|
|
209
|
+
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
|
|
210
|
+
// sessionId 落表后立即 reconcile:补执行会话卡 + exec 线(卡线同现,不依赖定时兜底)
|
|
211
|
+
if (board?.taskId) reconcileTaskBoard(card.boardId);
|
|
152
212
|
watchForAgentEnd(card, session.session);
|
|
153
213
|
clearDispatchToken(card.id, token);
|
|
154
214
|
console.log(
|
|
@@ -173,6 +233,13 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
|
|
|
173
233
|
}
|
|
174
234
|
}
|
|
175
235
|
|
|
236
|
+
/** 任务看板派生 reconcile(后端权威):补执行会话卡 + exec 线,广播到所有客户端 */
|
|
237
|
+
function reconcileTaskBoard(boardId: string): void {
|
|
238
|
+
void reconcileBoard(boardId).catch((e) =>
|
|
239
|
+
console.warn(`[task-scheduler] reconcile ${boardId} 异常:`, e?.message ?? e),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
176
243
|
/**
|
|
177
244
|
* 订阅执行会话 agent_end:会话跑完立即把卡转 review(待审核),不等下一轮 reconcile。
|
|
178
245
|
* 事件驱动实现「会话执行完立即进入待审核」。回调只处理卡仍为 running 的情况,避免覆盖后续流转。
|
|
@@ -198,9 +265,12 @@ function watchForAgentEnd(card: TaskCard, session: Awaited<ReturnType<typeof sta
|
|
|
198
265
|
export async function reconcileEndedRunningCards(): Promise<number> {
|
|
199
266
|
const runningIds = new Set(getRunningRpcSessionIds());
|
|
200
267
|
const now = Date.now();
|
|
268
|
+
const myId = getInstanceId();
|
|
201
269
|
let flipped = 0;
|
|
202
270
|
for (const card of listRunningDispatched()) {
|
|
203
271
|
if (!card.sessionId) continue;
|
|
272
|
+
// 归属守卫:他人活着拥有的卡不归本实例判定(防误翻别人正在跑的会话,#27 根因)
|
|
273
|
+
if (!isCardHandledByInstance(card, myId, now)) continue;
|
|
204
274
|
// 会话真实在跑 → 保持 running
|
|
205
275
|
if (runningIds.has(card.sessionId)) continue;
|
|
206
276
|
// 会话不在跑:确认已静默一段时间(防回合间隙误判),才转 review
|
|
@@ -216,6 +286,8 @@ export async function reconcileEndedRunningCards(): Promise<number> {
|
|
|
216
286
|
|
|
217
287
|
/** 单轮调度:并发闸门未满才派发;最多补满到全局上限。返回本轮派发数。 */
|
|
218
288
|
export async function runSchedulerTick(): Promise<number> {
|
|
289
|
+
// 自愈:本地在跑但被误标 review 的卡先拉回 running(先于审核,避免对运行中卡审核烧 token)
|
|
290
|
+
await restoreRunningReviewCards();
|
|
219
291
|
// 先巡检:会话已结束的 running 卡流转走(review),释放闸门(以真实会话状态为准)
|
|
220
292
|
await reconcileEndedRunningCards();
|
|
221
293
|
// S3:回复队列优先 → 审核 review 卡 → 巡检阻塞
|
|
@@ -235,7 +307,10 @@ export async function runSchedulerTick(): Promise<number> {
|
|
|
235
307
|
for (const card of cards) {
|
|
236
308
|
// 并发闸门按 running 数实时判断(上一张派发后 running 可能已满)
|
|
237
309
|
if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
|
|
238
|
-
const ok = await
|
|
310
|
+
const ok = await withSchedulerActivity(
|
|
311
|
+
{ kind: "dispatch", cardNumber: card.number, cardName: card.name, at: Date.now() },
|
|
312
|
+
() => dispatchCard(card),
|
|
313
|
+
);
|
|
239
314
|
if (ok) {
|
|
240
315
|
dispatched += 1;
|
|
241
316
|
setLastAction({ type: "dispatch", cardNumber: card.number, cardName: card.name, at: Date.now() });
|
|
@@ -250,12 +325,19 @@ export async function runSchedulerTick(): Promise<number> {
|
|
|
250
325
|
|
|
251
326
|
/** 回复队列:answered 且卡仍在 waiting_reply → 发回复续会话 → running。 */
|
|
252
327
|
export async function processReplyQueue(): Promise<number> {
|
|
328
|
+
const myId = getInstanceId();
|
|
329
|
+
const now = Date.now();
|
|
253
330
|
let resumed = 0;
|
|
254
331
|
for (const q of listAnswerableQuestions()) {
|
|
255
332
|
const card = getCard(q.cardId);
|
|
256
333
|
if (!card || card.execStatus !== "waiting_reply" || !card.sessionId) continue;
|
|
334
|
+
// 归属守卫:他人活着拥有的卡不归本实例续会话(防双实例重复发回复)
|
|
335
|
+
if (!isCardHandledByInstance(card, myId, now)) continue;
|
|
257
336
|
if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
|
|
258
|
-
const ok = await
|
|
337
|
+
const ok = await withSchedulerActivity(
|
|
338
|
+
{ kind: "resume", cardNumber: card.number, cardName: card.name, at: Date.now() },
|
|
339
|
+
() => resumeWithAnswer(card, q),
|
|
340
|
+
);
|
|
259
341
|
if (ok) {
|
|
260
342
|
resumed += 1;
|
|
261
343
|
setLastAction({ type: "resume", cardNumber: card.number, cardName: card.name, at: Date.now() });
|
|
@@ -273,7 +355,7 @@ async function resumeWithAnswer(card: TaskCard, q: TaskCardQuestion): Promise<bo
|
|
|
273
355
|
type: "prompt",
|
|
274
356
|
message: `【用户回答了你的提问】\n${q.answer}\n\n请据此继续完成任务。`,
|
|
275
357
|
});
|
|
276
|
-
updateCard(card.id, { execStatus: "running" });
|
|
358
|
+
updateCard(card.id, { execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
|
|
277
359
|
console.log(`[task-scheduler] 回复续会话 #${card.number} ${card.name}`);
|
|
278
360
|
return true;
|
|
279
361
|
} catch (error) {
|
|
@@ -309,8 +391,11 @@ function enterWaitingReply(card: TaskCard, questionHint: string): void {
|
|
|
309
391
|
/** 审核 review 卡:程序检测失败→failed;否则 AI 审核 → done/failed/waiting_reply/other。 */
|
|
310
392
|
export async function processReviewCards(): Promise<number> {
|
|
311
393
|
const now = Date.now();
|
|
394
|
+
const myId = getInstanceId();
|
|
312
395
|
let processed = 0;
|
|
313
396
|
for (const card of listCardsByExecStatus(["review"])) {
|
|
397
|
+
// 归属守卫:他人活着拥有的卡不归本实例审核(防双实例重复审核/判定互覆盖)
|
|
398
|
+
if (!isCardHandledByInstance(card, myId, now)) continue;
|
|
314
399
|
const snapshot = card.sessionId ? await readSessionAuditSnapshot(card.sessionId) : null;
|
|
315
400
|
|
|
316
401
|
// 1. 程序检测:最后消息有失败迹象 → 直接 failed(计次重试)
|
|
@@ -332,13 +417,16 @@ export async function processReviewCards(): Promise<number> {
|
|
|
332
417
|
reviewAuditAt.set(card.id, now);
|
|
333
418
|
const cwd = resolveDispatchCwd(card);
|
|
334
419
|
if (!cwd) continue;
|
|
335
|
-
const result = await
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
420
|
+
const result = await withSchedulerActivity(
|
|
421
|
+
{ kind: "review", cardNumber: card.number, cardName: card.name, at: Date.now() },
|
|
422
|
+
() => runAuditVerdict({
|
|
423
|
+
cwd,
|
|
424
|
+
cardNumber: card.number,
|
|
425
|
+
cardName: card.name,
|
|
426
|
+
taskDescription: card.description,
|
|
427
|
+
recentMessages: snapshot.recentText,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
342
430
|
if (result?.verdict === "done") {
|
|
343
431
|
updateCard(card.id, { execStatus: "done" });
|
|
344
432
|
console.log(`[task-scheduler] #${card.number} 审核通过 → done(${result.reason})`);
|
|
@@ -361,9 +449,12 @@ export async function processReviewCards(): Promise<number> {
|
|
|
361
449
|
/** running 卡阻塞巡检:最后消息 > BLOCK_IDLE_MS 无进展 → AI 判定阻塞类型 → 处置。 */
|
|
362
450
|
export async function checkRunningCardsBlocked(): Promise<number> {
|
|
363
451
|
const now = Date.now();
|
|
452
|
+
const myId = getInstanceId();
|
|
364
453
|
let handled = 0;
|
|
365
454
|
for (const card of listCardsByExecStatus(["running"])) {
|
|
366
455
|
if (!card.sessionId) continue;
|
|
456
|
+
// 归属守卫:他人活着拥有的卡不归本实例巡检
|
|
457
|
+
if (!isCardHandledByInstance(card, myId, now)) continue;
|
|
367
458
|
// 冷却:同一卡冷却期内不重复判定
|
|
368
459
|
const last = blockCheckAt.get(card.id) ?? 0;
|
|
369
460
|
if (now - last < BLOCK_COOLDOWN_MS) continue;
|
|
@@ -374,13 +465,16 @@ export async function checkRunningCardsBlocked(): Promise<number> {
|
|
|
374
465
|
blockCheckAt.set(card.id, now);
|
|
375
466
|
const cwd = resolveDispatchCwd(card);
|
|
376
467
|
if (!cwd) continue;
|
|
377
|
-
const result = await
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
468
|
+
const result = await withSchedulerActivity(
|
|
469
|
+
{ kind: "blockcheck", cardNumber: card.number, cardName: card.name, at: Date.now() },
|
|
470
|
+
() => runBlockCheck({
|
|
471
|
+
cwd,
|
|
472
|
+
cardNumber: card.number,
|
|
473
|
+
cardName: card.name,
|
|
474
|
+
taskDescription: card.description,
|
|
475
|
+
recentMessages: snapshot.recentText,
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
384
478
|
if (!result) continue;
|
|
385
479
|
switch (result.kind) {
|
|
386
480
|
case "sync_server":
|
|
@@ -453,7 +547,23 @@ export function startTaskScheduler(): void {
|
|
|
453
547
|
});
|
|
454
548
|
}, TASK_SCHEDULER_INTERVAL_MS);
|
|
455
549
|
timer.unref?.();
|
|
456
|
-
|
|
550
|
+
// 归属心跳:独立于长 tick(审核最长 90s),owner 存活期间心跳不断更,
|
|
551
|
+
// 其他实例据此知道该卡归谁、不误翻。
|
|
552
|
+
const heartbeatTimer = setInterval(() => {
|
|
553
|
+
try {
|
|
554
|
+
heartbeatOwnedCards(getInstanceId());
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error("[task-scheduler] heartbeat error:", error instanceof Error ? error.message : error);
|
|
557
|
+
}
|
|
558
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
559
|
+
heartbeatTimer.unref?.();
|
|
560
|
+
const sched = {
|
|
561
|
+
timer,
|
|
562
|
+
heartbeatTimer,
|
|
563
|
+
startedAt: Date.now(),
|
|
564
|
+
lastAction: { type: "none" as const, at: 0 },
|
|
565
|
+
activity: null,
|
|
566
|
+
};
|
|
457
567
|
globalThis.__piTaskScheduler = sched;
|
|
458
568
|
console.log(`[pi-web] task scheduler started (interval ${TASK_SCHEDULER_INTERVAL_MS}ms, max ${TASK_SCHEDULER_MAX_CONCURRENCY})`);
|
|
459
569
|
}
|
|
@@ -477,6 +587,41 @@ export interface TaskSchedulerStatus {
|
|
|
477
587
|
running: TaskCard[];
|
|
478
588
|
/** 最近一次调度动作 */
|
|
479
589
|
lastAction: TaskSchedulerLastAction;
|
|
590
|
+
/** 调度器此刻正在做的动作(工作中);null=休眠 */
|
|
591
|
+
activity: SchedulerActivity | null;
|
|
592
|
+
/** 任务卡队列统计(按执行状态实时算,展示用) */
|
|
593
|
+
queue: TaskSchedulerQueueStats;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** 调度器当前动作(看板状态展示)。kind:派发/回复续会话/审核/阻塞巡检。 */
|
|
597
|
+
export interface SchedulerActivity {
|
|
598
|
+
kind: "dispatch" | "resume" | "review" | "blockcheck";
|
|
599
|
+
cardNumber?: number;
|
|
600
|
+
cardName?: string;
|
|
601
|
+
at: number;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface TaskSchedulerQueueStats {
|
|
605
|
+
/** 就绪可派发(todo & 未开始/可重试 & 前置均完成) */
|
|
606
|
+
dispatchable: TaskCardBrief[];
|
|
607
|
+
/** 待审核(review 态) */
|
|
608
|
+
review: TaskCardBrief[];
|
|
609
|
+
/** 等回复(waiting_reply 态) */
|
|
610
|
+
waitingReply: TaskCardBrief[];
|
|
611
|
+
/** 失败(含重试超限) */
|
|
612
|
+
failed: TaskCardBrief[];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** 队列卡片轻量字段(展示/定位用,不携带 description 等大字段) */
|
|
616
|
+
export interface TaskCardBrief {
|
|
617
|
+
id: string;
|
|
618
|
+
boardId: string;
|
|
619
|
+
number: number;
|
|
620
|
+
name: string;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function toBrief(card: TaskCard): TaskCardBrief {
|
|
624
|
+
return { id: card.id, boardId: card.boardId, number: card.number, name: card.name };
|
|
480
625
|
}
|
|
481
626
|
|
|
482
627
|
function getLastAction(): TaskSchedulerLastAction {
|
|
@@ -486,13 +631,42 @@ function setLastAction(action: TaskSchedulerLastAction): void {
|
|
|
486
631
|
if (globalThis.__piTaskScheduler) globalThis.__piTaskScheduler.lastAction = action;
|
|
487
632
|
}
|
|
488
633
|
|
|
634
|
+
/** 读取当前调度器动作(工作中状态),无则 null */
|
|
635
|
+
function getActivity(): SchedulerActivity | null {
|
|
636
|
+
return globalThis.__piTaskScheduler?.activity ?? null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** 置/清当前调度器动作。UI 轮询读到非空 = 该动作工作中。 */
|
|
640
|
+
function setActivity(activity: SchedulerActivity | null): void {
|
|
641
|
+
if (globalThis.__piTaskScheduler) globalThis.__piTaskScheduler.activity = activity;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** 包一段调度动作:执行期间置 activity(工作中),结束清空(休眠)。
|
|
645
|
+
* 串行 tick 内各阶段不嵌套(同 tick 无并发),末尾 clear 不影响其他阶段。 */
|
|
646
|
+
async function withSchedulerActivity<T>(activity: SchedulerActivity, fn: () => Promise<T>): Promise<T> {
|
|
647
|
+
setActivity(activity);
|
|
648
|
+
try {
|
|
649
|
+
return await fn();
|
|
650
|
+
} finally {
|
|
651
|
+
setActivity(null);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
489
655
|
/** 当前调度器状态:真实运行中的卡 + 最近动作。UI 轮询展示「正在运行 xxx任务」。
|
|
490
656
|
* running 只列执行会话真实在跑的卡(以 getRunningRpcSessionIds 为准,不读过期卡状态)。 */
|
|
491
657
|
export function getSchedulerStatus(): TaskSchedulerStatus {
|
|
492
658
|
const runningIds = new Set(getRunningRpcSessionIds());
|
|
659
|
+
const running = listRunningDispatched().filter((c) => c.sessionId && runningIds.has(c.sessionId));
|
|
493
660
|
return {
|
|
494
661
|
started: Boolean(globalThis.__piTaskScheduler),
|
|
495
|
-
running
|
|
662
|
+
running,
|
|
496
663
|
lastAction: getLastAction(),
|
|
664
|
+
activity: getActivity(),
|
|
665
|
+
queue: {
|
|
666
|
+
dispatchable: listDispatchableCards().map(toBrief),
|
|
667
|
+
review: listCardsByExecStatus(["review"]).map(toBrief),
|
|
668
|
+
waitingReply: listCardsByExecStatus(["waiting_reply"]).map(toBrief),
|
|
669
|
+
failed: listCardsByExecStatus(["failed"]).map(toBrief),
|
|
670
|
+
},
|
|
497
671
|
};
|
|
498
672
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// yjs 协同房间服务器(Hocuspocus 内嵌版)—— 替代 tldraw sync(TLSocketRoom)
|
|
3
|
+
// - 每看板一个 Y.Doc(documentName = boardId),结构:nodes/edges/view 三个 Y.Map
|
|
4
|
+
// - 持久化:SQLite(sync.db 独立表 yjs_documents),onStoreDocument 存全量 update
|
|
5
|
+
// - 内嵌:server.mjs 的 upgrade 事件分流 —— 仅 /connect 走 crossws,其余交 Next
|
|
6
|
+
// - 服务端权威写:openDirectConnection(boardId).transact() —— 派生 reconcile 用
|
|
7
|
+
//
|
|
8
|
+
// 用法(内嵌):server.on("upgrade", handleYjsUpgrade) —— 返回 false 表示未处理
|
|
9
|
+
// ============================================================================
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join, normalize } from "node:path";
|
|
14
|
+
|
|
15
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
16
|
+
const { Server } = require("@hocuspocus/server");
|
|
17
|
+
const Y = require("yjs");
|
|
18
|
+
|
|
19
|
+
// ---- agent dir(与 lib/sqlite-db.ts 同语义)----
|
|
20
|
+
export function getAgentDir() {
|
|
21
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
22
|
+
if (envDir) {
|
|
23
|
+
const expanded = envDir.startsWith("~") ? join(homedir(), envDir.slice(1)) : envDir;
|
|
24
|
+
return normalize(expanded);
|
|
25
|
+
}
|
|
26
|
+
return join(homedir(), ".pi", "agent");
|
|
27
|
+
}
|
|
28
|
+
export const YJS_DB_FILE = join(getAgentDir(), "sync.db");
|
|
29
|
+
|
|
30
|
+
// 单一 DatabaseSync 连接(node:sqlite 同步,单连接线程安全)
|
|
31
|
+
const db = new DatabaseSync(YJS_DB_FILE);
|
|
32
|
+
db.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS yjs_documents (
|
|
34
|
+
board_id TEXT PRIMARY KEY,
|
|
35
|
+
data BLOB NOT NULL,
|
|
36
|
+
updated INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
/** Hocuspocus server 实例(惰性创建) */
|
|
41
|
+
let hocuspocus = null;
|
|
42
|
+
|
|
43
|
+
/** 建(或复用)Hocuspocus server */
|
|
44
|
+
function getServer() {
|
|
45
|
+
if (hocuspocus) return hocuspocus;
|
|
46
|
+
hocuspocus = new Server({
|
|
47
|
+
// 端口留给外部 httpServer,这里不 listen
|
|
48
|
+
port: 0,
|
|
49
|
+
// 客户端断开后延迟持久化再卸载
|
|
50
|
+
unloadImmediately: false,
|
|
51
|
+
// 文档变更持久化 debounce(默认 2s,配小一点让写库及时)
|
|
52
|
+
debounce: 500,
|
|
53
|
+
async onLoadDocument({ documentName, document }) {
|
|
54
|
+
const row = db.prepare("SELECT data FROM yjs_documents WHERE board_id = ?").get(documentName);
|
|
55
|
+
if (row) {
|
|
56
|
+
try {
|
|
57
|
+
Y.applyUpdate(document, new Uint8Array(row.data));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn(`[yjs] ${documentName} onLoad 解码失败:`, e?.message ?? e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async onStoreDocument({ documentName, document }) {
|
|
64
|
+
const update = Y.encodeStateAsUpdate(document);
|
|
65
|
+
db.prepare(
|
|
66
|
+
"INSERT INTO yjs_documents (board_id, data, updated) VALUES (?, ?, ?) " +
|
|
67
|
+
"ON CONFLICT(board_id) DO UPDATE SET data = excluded.data, updated = excluded.updated",
|
|
68
|
+
).run(documentName, Buffer.from(update), Date.now());
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return hocuspocus;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 处理一条 WebSocket upgrade 请求(挂在 Node http server 的 "upgrade" 事件)。
|
|
76
|
+
* 仅 /connect 路径交 Hocuspocus(provider 的 name 参数 = documentName);
|
|
77
|
+
* 其余(Next HMR 等)返回 false,调用方转交 Next。
|
|
78
|
+
* @returns {boolean} 是否已处理
|
|
79
|
+
*/
|
|
80
|
+
export function handleYjsUpgrade(req, socket, head) {
|
|
81
|
+
let url;
|
|
82
|
+
try {
|
|
83
|
+
url = new URL(req.url, "http://localhost");
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (url.pathname !== "/connect") return false;
|
|
88
|
+
const server = getServer();
|
|
89
|
+
server.crossws.handleUpgrade(req, socket, head).catch((e) => {
|
|
90
|
+
console.error(`[yjs] /connect handleUpgrade 异常:`, e?.message ?? e);
|
|
91
|
+
try { socket.destroy(); } catch { /* ignore */ }
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 服务端权威写:对指定看板执行一个事务(直接改 Y.Doc,不经 WebSocket)。
|
|
98
|
+
* transaction(maps, doc) 内用 maps.nodes / maps.edges / maps.view 操作。
|
|
99
|
+
* 变更自动经 Hocuspocus 广播到已连接客户端并持久化(onStoreDocument)。
|
|
100
|
+
*/
|
|
101
|
+
export async function mutateBoard(boardId, transaction) {
|
|
102
|
+
const server = getServer();
|
|
103
|
+
const dc = await server.hocuspocus.openDirectConnection(boardId);
|
|
104
|
+
try {
|
|
105
|
+
await dc.transact((document) => {
|
|
106
|
+
const maps = {
|
|
107
|
+
nodes: document.getMap("nodes"),
|
|
108
|
+
edges: document.getMap("edges"),
|
|
109
|
+
view: document.getMap("view"),
|
|
110
|
+
ydoc: document,
|
|
111
|
+
};
|
|
112
|
+
transaction(maps, document);
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
await dc.disconnect().catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 删除看板文档(删除看板时调用) */
|
|
120
|
+
export async function destroyBoardDocument(boardId) {
|
|
121
|
+
const server = getServer();
|
|
122
|
+
try {
|
|
123
|
+
await server.hocuspocus.closeConnections(boardId);
|
|
124
|
+
db.prepare("DELETE FROM yjs_documents WHERE board_id = ?").run(boardId);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.warn(`[yjs] destroy ${boardId} 异常:`, e?.message ?? e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 当前加载中的文档数(诊断用) */
|
|
131
|
+
export function getYjsRoomCount() {
|
|
132
|
+
return hocuspocus?.hocuspocus?.documents?.size ?? 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- 注入 globalThis(供 lib/board-reconcile.ts 调用,避免 Next 打包 node:sqlite)----
|
|
136
|
+
globalThis.__yjsBoard = {
|
|
137
|
+
mutateBoard,
|
|
138
|
+
destroyBoardDocument,
|
|
139
|
+
};
|