@co0ontty/wand 4.105.1 → 4.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +7 -2
- package/dist/distribution-manager.d.ts +2 -0
- package/dist/distribution-manager.js +23 -0
- package/dist/employee-input-scheduling.d.ts +2 -1
- package/dist/employee-knowledge-cli.d.ts +1 -1
- package/dist/employee-knowledge-cli.js +91 -20
- package/dist/employee-knowledge-content.js +1 -1
- package/dist/employee-knowledge-types.d.ts +1 -1
- package/dist/employee-knowledge-types.js +1 -1
- package/dist/employee-knowledge.d.ts +6 -3
- package/dist/employee-knowledge.js +54 -58
- package/dist/env-utils.js +1 -1
- package/dist/html-preview-policy.d.ts +4 -0
- package/dist/html-preview-policy.js +4 -0
- package/dist/ios-ota.d.ts +5 -0
- package/dist/ios-ota.js +11 -1
- package/dist/knowledge-skill.d.ts +2 -0
- package/dist/knowledge-skill.js +17 -0
- package/dist/knowledge-types.d.ts +36 -0
- package/dist/knowledge-types.js +8 -0
- package/dist/managed-skills-ensure.js +2 -0
- package/dist/middleware/api-scope.js +2 -0
- package/dist/path-repair.js +28 -6
- package/dist/pi-resource-catalog.d.ts +3 -1
- package/dist/pi-resource-catalog.js +25 -0
- package/dist/pi-session-settings.d.ts +2 -0
- package/dist/pi-session-settings.js +25 -0
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.js +35 -12
- package/dist/quick-terminal-session.d.ts +3 -0
- package/dist/quick-terminal-session.js +5 -0
- package/dist/retention-protection.d.ts +15 -0
- package/dist/retention-protection.js +134 -0
- package/dist/retention.d.ts +3 -2
- package/dist/retention.js +58 -76
- package/dist/server-employee-routes.js +1 -1
- package/dist/server-file-routes.js +84 -16
- package/dist/server-knowledge-routes.d.ts +4 -0
- package/dist/server-knowledge-routes.js +61 -0
- package/dist/server-local-preview-routes.js +28 -6
- package/dist/server-pi-defaults-routes.d.ts +1 -1
- package/dist/server-pi-defaults-routes.js +13 -20
- package/dist/server-session-lifecycle-routes.js +17 -17
- package/dist/server-session-list-routes.js +1 -1
- package/dist/server-task-routes.js +6 -0
- package/dist/server-update-routes.d.ts +1 -0
- package/dist/server-update-routes.js +62 -17
- package/dist/server-workspace-routes.js +5 -5
- package/dist/server.js +4 -2
- package/dist/session-questions.d.ts +7 -0
- package/dist/session-questions.js +25 -0
- package/dist/session-registry.d.ts +3 -0
- package/dist/session-registry.js +17 -0
- package/dist/session-request.d.ts +1 -1
- package/dist/session-request.js +6 -2
- package/dist/session-service.js +1 -1
- package/dist/storage/employees-repository.js +5 -2
- package/dist/storage/knowledge-repository.d.ts +35 -0
- package/dist/storage/knowledge-repository.js +208 -0
- package/dist/storage/mappers.js +1 -1
- package/dist/storage/migrations.js +51 -0
- package/dist/storage/sessions-repository.d.ts +2 -0
- package/dist/storage/sessions-repository.js +16 -0
- package/dist/storage/tasks-repository.d.ts +7 -0
- package/dist/storage/tasks-repository.js +21 -0
- package/dist/storage/workspaces-repository.d.ts +1 -0
- package/dist/storage/workspaces-repository.js +2 -1
- package/dist/storage.d.ts +3 -1
- package/dist/storage.js +4 -0
- package/dist/structured-session-manager.d.ts +3 -0
- package/dist/structured-session-manager.js +13 -4
- package/dist/task-action-types.d.ts +22 -0
- package/dist/task-action-types.js +1 -0
- package/dist/task-actions.d.ts +5 -0
- package/dist/task-actions.js +86 -0
- package/dist/task-execution-queue.d.ts +1 -1
- package/dist/task-execution-queue.js +4 -1
- package/dist/task-retention.d.ts +4 -2
- package/dist/task-retention.js +2 -0
- package/dist/types.d.ts +2 -1
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/00b168a19664439e/dropdown-surface.js +1 -0
- package/dist/web-ui/content/modules/059c5fb7f128d340/task-board-host.js +27 -0
- package/dist/web-ui/content/modules/07494f5a623e4471/chat-render.js +76 -0
- package/dist/web-ui/content/modules/{bc28252270bcc51a → 0a743ad3b4db6874}/chunk.js +1 -1
- package/dist/web-ui/content/modules/0d76e582a7a71b2f/chunk.js +1 -0
- package/dist/web-ui/content/modules/11af5d14b30e57b6/chunk.js +1 -0
- package/dist/web-ui/content/modules/16dab1297efd71a7/chunk.js +10 -0
- package/dist/web-ui/content/modules/182929e188c1ab3a/host.js +5 -0
- package/dist/web-ui/content/modules/1858670c0f84fec5/employee-profile.js +1 -0
- package/dist/web-ui/content/modules/{1cf252d2f64a6279 → 1eab7410d5586038}/chunk.js +1 -1
- package/dist/web-ui/content/modules/20c47dc0442f5a04/host.js +1 -0
- package/dist/web-ui/content/modules/226241b999d12008/host.js +1 -0
- package/dist/web-ui/content/modules/23c4b8d223ec64c5/host.js +61 -0
- package/dist/web-ui/content/modules/2882c97388ff4f7b/employee-card.js +1 -0
- package/dist/web-ui/content/modules/2a791cd2e14aa1e0/notice-view.js +1 -0
- package/dist/web-ui/content/modules/2c90c3be260e6cce/progress.js +1 -0
- package/dist/web-ui/content/modules/2d7be9935394f397/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/2f801e53dda62770/chunk.js +1 -0
- package/dist/web-ui/content/modules/2f97ddba3842adaa/chunk.js +1 -0
- package/dist/web-ui/content/modules/31157ceb6d23b809/chunk.js +15 -0
- package/dist/web-ui/content/modules/389a5112332ec23a/host.js +1 -0
- package/dist/web-ui/content/modules/3ea13e8b3c2deaa4/panel-host.js +1 -0
- package/dist/web-ui/content/modules/3f8830f8ad28decc/chunk.js +3 -0
- package/dist/web-ui/content/modules/{70715360138f4d99 → 3fe11a75284ad8ae}/host.js +1 -1
- package/dist/web-ui/content/modules/{e0a1ae9163e77556 → 41445c6589dba325}/host.js +2 -2
- package/dist/web-ui/content/modules/{8b2f9a027ae1d7eb → 42e5c34002fcffb7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/438807b8bf112cd0/chunk.js +1 -0
- package/dist/web-ui/content/modules/4861eca9a8a87ad0/chunk.js +1 -0
- package/dist/web-ui/content/modules/{78d06dfdc4b13d78 → 4970319a83432ec7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/4a5d669c90f545cb/host.js +1 -0
- package/dist/web-ui/content/modules/4ba1e8c74bed7ef2/chunk.js +1 -0
- package/dist/web-ui/content/modules/5083a5dbb5b155b7/chunk.js +1 -0
- package/dist/web-ui/content/modules/50da446c1377c220/chunk.js +20 -0
- package/dist/web-ui/content/modules/52574891ef08e29b/host.js +30 -0
- package/dist/web-ui/content/modules/5705d25468274534/host.js +1 -0
- package/dist/web-ui/content/modules/579d486763d47c0d/chunk.js +1 -0
- package/dist/web-ui/content/modules/57c357d321db5df9/chunk.js +1 -0
- package/dist/web-ui/content/modules/5933ce8965aa4004/chunk.js +1 -0
- package/dist/web-ui/content/modules/5961ab2947c86ca1/shell-file-panel-drawer.js +1 -0
- package/dist/web-ui/content/modules/5b6e1369df7dea69/chunk.js +1 -0
- package/dist/web-ui/content/modules/5f298a8f43dd0589/chunk.js +1 -0
- package/dist/web-ui/content/modules/62e0023451f428cf/chunk.js +1 -0
- package/dist/web-ui/content/modules/6787bdcf56f743a5/host.js +1 -0
- package/dist/web-ui/content/modules/67c39b0398186a9f/host.js +1 -0
- package/dist/web-ui/content/modules/6b16f35c0627bafb/chunk.js +1 -0
- package/dist/web-ui/content/modules/6dd4f3e2f619f7a0/chunk.js +1 -0
- package/dist/web-ui/content/modules/70a2e13e0ccc8549/panel.js +1 -0
- package/dist/web-ui/content/modules/710db590e9707f73/runtime.js +338 -0
- package/dist/web-ui/content/modules/7249950f8771dbeb/workspaces-panel.js +4 -0
- package/dist/web-ui/content/modules/771439a41e737ee2/chunk.js +1 -0
- package/dist/web-ui/content/modules/{2061d4a88875b9f9 → 816eb9b3a000fb2c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c0384ffb4ef740a0 → 831b6c7b43e800be}/host.js +1 -1
- package/dist/web-ui/content/modules/87c7f50d9455d55e/chunk.js +1 -0
- package/dist/web-ui/content/modules/87dcd516ffd9f688/composer-surface-adapter.js +1 -0
- package/dist/web-ui/content/modules/8891ca8165435ca1/chunk.js +1 -0
- package/dist/web-ui/content/modules/8b7cd7bffdfd69e7/brand-identity.js +1 -0
- package/dist/web-ui/content/modules/8bf9917f6c3429f6/host.js +1 -0
- package/dist/web-ui/content/modules/8de8368570ab089d/chunk.js +57 -0
- package/dist/web-ui/content/modules/8e612d4881fa4118/home.js +6 -0
- package/dist/web-ui/content/modules/8e6178262ea63dec/markdown.js +1 -0
- package/dist/web-ui/content/modules/913187c92a98a1ac/chunk.js +1 -0
- package/dist/web-ui/content/modules/96f2ae970f8c576b/host.js +1 -0
- package/dist/web-ui/content/modules/97d7f267959fd385/chunk.js +1 -0
- package/dist/web-ui/content/modules/{22497085d7cdd83b → 9b12c2301f3a18f3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9f4e26a1c1867d65 → 9d941d6ff39861b8}/chunk.js +2 -2
- package/dist/web-ui/content/modules/9e163d54ff8fd1aa/chunk.js +1 -0
- package/dist/web-ui/content/modules/{0567e161e2cca41e → a2caed0139df4b21}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a59734be368daa7e/chunk.js +1 -0
- package/dist/web-ui/content/modules/{e4503f8e6379ccdc → a61393bfa976d8d5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{df5d48b4ff04b443 → b63e075f222a1266}/library-bridge.js +2 -2
- package/dist/web-ui/content/modules/b713cad51ca4c512/chunk.js +1 -0
- package/dist/web-ui/content/modules/ba89cd97627e6682/queue-view-adapter.js +1 -0
- package/dist/web-ui/content/modules/bd3daf04a8bf4422/chunk.js +1 -0
- package/dist/web-ui/content/modules/{aa7d409d04bbd736 → bd7dc4bb46731ee7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/bf3ee58e0da44822/chunk.js +1 -0
- package/dist/web-ui/content/modules/c0a446100d969f3e/chunk.js +8 -0
- package/dist/web-ui/content/modules/c2f09c8a472f5d83/dialog-modal.js +4 -0
- package/dist/web-ui/content/modules/c33935155289c22d/chunk.js +3 -0
- package/dist/web-ui/content/modules/c351eb11e0bdec2c/sidebar-peek.js +1 -0
- package/dist/web-ui/content/modules/ca7ca5902a35d26e/chunk.js +1 -0
- package/dist/web-ui/content/modules/cba6f13c0b2aec1c/chunk.js +1 -0
- package/dist/web-ui/content/modules/d434edbb2f4672e5/chunk.js +1 -0
- package/dist/web-ui/content/modules/{d569c96d4acfdec3 → d47d75a78ae598e3}/chunk.js +25 -25
- package/dist/web-ui/content/modules/d49f106c4d106057/chunk.js +1 -0
- package/dist/web-ui/content/modules/d5d28ec375826ef2/chunk.js +3 -0
- package/dist/web-ui/content/modules/{7edec8f23f8e0834 → d6b16dedb9011c33}/chunk.js +1 -1
- package/dist/web-ui/content/modules/d6fffe2c2a3a738d/toast-region.js +1 -0
- package/dist/web-ui/content/modules/d8a7011760f61719/group-editor.js +1 -0
- package/dist/web-ui/content/modules/{bf0b693a654e2655 → d9e2b65e9679ad39}/host.js +3 -3
- package/dist/web-ui/content/modules/db521a246e14e5e4/chunk.js +4 -0
- package/dist/web-ui/content/modules/{7994dec16e8b1039 → dedd4a19bb70e327}/page.js +2 -2
- package/dist/web-ui/content/modules/e060508ecd50fa7e/chunk.js +1 -0
- package/dist/web-ui/content/modules/e5174201f9d918fb/chunk.js +1 -0
- package/dist/web-ui/content/modules/e63582a3bce48f7d/chunk.js +1 -0
- package/dist/web-ui/content/modules/ea8fe4b9999fd2aa/chunk.js +1 -0
- package/dist/web-ui/content/modules/{d7cdd7dca9a0c9f8 → eac72f69716dba05}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ebc53dedc3945514/host.js +1 -0
- package/dist/web-ui/content/modules/{0d7f7d954382dc42 → ec33e17c00c1eb24}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{99e5cdf0c3bd0e3d → ed7405f064944215}/sha256.js +1 -1
- package/dist/web-ui/content/modules/f27a5d3c6c02a71c/chunk.js +1 -0
- package/dist/web-ui/content/modules/f383d892fef73483/chunk.js +1 -0
- package/dist/web-ui/content/modules/{084dca1ea52bf38e → f3cff7792ba39df8}/login-visual.js +1 -1
- package/dist/web-ui/content/modules/f43da7050badf54c/host.js +1 -0
- package/dist/web-ui/content/modules/face3dbf117d63e3/chunk.js +1 -0
- package/dist/web-ui/content/modules/fb2356eced24c4cc/host-bridge.js +1 -0
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +413 -420
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/group-chat-visibility.d.ts +1 -0
- package/dist/web-ui/group-chat-visibility.js +1 -1
- package/dist/web-ui/markdown.js +10 -36
- package/dist/web-ui/message-file-target.d.ts +8 -0
- package/dist/web-ui/message-file-target.js +105 -15
- package/dist/web-ui/page-route.js +2 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +2 -2
- package/dist/web-ui/react/local-preview/controller.js +53 -10
- package/dist/web-ui/react/workspaces/types.d.ts +1 -0
- package/dist/web-ui/server-link-url.d.ts +3 -0
- package/dist/web-ui/server-link-url.js +18 -0
- package/package.json +2 -1
- package/skills/wand-knowledge/SKILL.md +47 -0
- package/skills/wand-knowledge/scripts/knowledge.mjs +18 -0
- package/dist/web-ui/content/modules/0341f0d00f88a16f/chunk.js +0 -1
- package/dist/web-ui/content/modules/069f5a4267b0a9e3/runtime.js +0 -279
- package/dist/web-ui/content/modules/0d62c22b7aed5f1f/host-bridge.js +0 -1
- package/dist/web-ui/content/modules/0da28408ec089cf7/chunk.js +0 -1
- package/dist/web-ui/content/modules/130e8905cc7f639c/toast-region.js +0 -1
- package/dist/web-ui/content/modules/186d6e3b44a3c539/host.js +0 -1
- package/dist/web-ui/content/modules/1870280f0e783c62/chunk.js +0 -4
- package/dist/web-ui/content/modules/19d3bc36fa1e9160/chunk.js +0 -1
- package/dist/web-ui/content/modules/1a0b5db535599f72/chunk.js +0 -1
- package/dist/web-ui/content/modules/1d1dc0fd8a0210aa/composer-surface-adapter.js +0 -1
- package/dist/web-ui/content/modules/23f15023aa2279a3/shell-file-panel-drawer.js +0 -1
- package/dist/web-ui/content/modules/2cab58e5d148844c/host.js +0 -1
- package/dist/web-ui/content/modules/32edeb64dbf339f6/chunk.js +0 -1
- package/dist/web-ui/content/modules/33daaeb4bf68c7ba/chunk.js +0 -9
- package/dist/web-ui/content/modules/37da177222b6308d/workspaces-panel.js +0 -4
- package/dist/web-ui/content/modules/3af7facf8a7588c7/chunk.js +0 -1
- package/dist/web-ui/content/modules/3b95dc1376e932ea/host.js +0 -1
- package/dist/web-ui/content/modules/3cbbad48df7998f8/chunk.js +0 -1
- package/dist/web-ui/content/modules/41cf79c899bc83c8/host.js +0 -1
- package/dist/web-ui/content/modules/444af9de1698eea0/host.js +0 -1
- package/dist/web-ui/content/modules/452327275cc5d8f8/host.js +0 -1
- package/dist/web-ui/content/modules/466310a17dac465f/chunk.js +0 -3
- package/dist/web-ui/content/modules/46f1b6c3274d399b/host.js +0 -1
- package/dist/web-ui/content/modules/47998d82dc65bfeb/sidebar-peek.js +0 -1
- package/dist/web-ui/content/modules/486a828390c5d44d/host.js +0 -30
- package/dist/web-ui/content/modules/4cc15b385bc6cc32/chunk.js +0 -1
- package/dist/web-ui/content/modules/4d5ab67f6c0ce19f/chunk.js +0 -1
- package/dist/web-ui/content/modules/54aaf60ac85dcb2b/chunk.js +0 -1
- package/dist/web-ui/content/modules/54c22eddaf009d00/chunk.js +0 -1
- package/dist/web-ui/content/modules/55b931232c672f4f/chunk.js +0 -1
- package/dist/web-ui/content/modules/583049de28ef2b02/panel.js +0 -1
- package/dist/web-ui/content/modules/5a2a58334d63e127/chunk.js +0 -20
- package/dist/web-ui/content/modules/5ba0668f5a22133b/home.js +0 -6
- package/dist/web-ui/content/modules/5cd5d3d65d966c5f/chunk.js +0 -1
- package/dist/web-ui/content/modules/6b41e59ab342482f/notice-view.js +0 -1
- package/dist/web-ui/content/modules/6b5445f41bef0a66/chunk.js +0 -1
- package/dist/web-ui/content/modules/6e8395f61c89b5aa/chunk.js +0 -1
- package/dist/web-ui/content/modules/6f74cede70164e19/chunk.js +0 -1
- package/dist/web-ui/content/modules/704690f5d6ab9157/chunk.js +0 -1
- package/dist/web-ui/content/modules/7dce56c1987e8d43/chunk.js +0 -29
- package/dist/web-ui/content/modules/80e92c14a6789896/progress.js +0 -1
- package/dist/web-ui/content/modules/8a828b4d087667dd/chunk.js +0 -1
- package/dist/web-ui/content/modules/8d6f223764846e18/brand-identity.js +0 -1
- package/dist/web-ui/content/modules/9136bae16758fd4c/chunk.js +0 -1
- package/dist/web-ui/content/modules/93fa5c35e4bcab96/chunk.js +0 -1
- package/dist/web-ui/content/modules/96fe39f39930d9e4/chunk.js +0 -8
- package/dist/web-ui/content/modules/9db41ad5ac96ffa2/chunk.js +0 -1
- package/dist/web-ui/content/modules/a073cb6d182ccd32/chunk.js +0 -1
- package/dist/web-ui/content/modules/a8cba9f1d6b49ef1/chunk.js +0 -1
- package/dist/web-ui/content/modules/a97b3b9b68f9ca38/chunk.js +0 -57
- package/dist/web-ui/content/modules/ac34ed6692b43ede/dropdown-surface.js +0 -1
- package/dist/web-ui/content/modules/aff19b31898aa157/chunk.js +0 -1
- package/dist/web-ui/content/modules/b2f6e088e6e45878/chunk.js +0 -1
- package/dist/web-ui/content/modules/b4446bf92e182bfb/employee-card.js +0 -1
- package/dist/web-ui/content/modules/b79c15aace456028/chunk.js +0 -6
- package/dist/web-ui/content/modules/b8c18baf7cfb61b1/chat-render.js +0 -76
- package/dist/web-ui/content/modules/b8c447171e3d60a0/chunk.js +0 -1
- package/dist/web-ui/content/modules/b945370f1fa784f9/chunk.js +0 -13
- package/dist/web-ui/content/modules/ba5fa76884e1f806/chunk.js +0 -1
- package/dist/web-ui/content/modules/bb1bb9460cc10fbe/host.js +0 -1
- package/dist/web-ui/content/modules/be7f1f9183a1247f/chunk.js +0 -3
- package/dist/web-ui/content/modules/bf7449bcf95e1454/chunk.js +0 -1
- package/dist/web-ui/content/modules/c091efd46e221c29/chunk.js +0 -1
- package/dist/web-ui/content/modules/c1ec2ebce8ea5dd8/dialog-modal.js +0 -4
- package/dist/web-ui/content/modules/c2f7bb50118f1603/chunk.js +0 -1
- package/dist/web-ui/content/modules/c369ce4eb71277e6/host.js +0 -1
- package/dist/web-ui/content/modules/c46b3501573953e9/chunk.js +0 -1
- package/dist/web-ui/content/modules/c577ad641502d509/panel-host.js +0 -1
- package/dist/web-ui/content/modules/c75976857c18f251/host.js +0 -1
- package/dist/web-ui/content/modules/c8eaffbedda0d3d0/queue-view-adapter.js +0 -3
- package/dist/web-ui/content/modules/cadd26ae50359434/chunk.js +0 -1
- package/dist/web-ui/content/modules/d37ff6684c9aeeec/chunk.js +0 -3
- package/dist/web-ui/content/modules/d48c1f89342bc4bc/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/d859a83f486aab23/chunk.js +0 -15
- package/dist/web-ui/content/modules/d8b258a6455d77da/host.js +0 -5
- package/dist/web-ui/content/modules/d937e939122180b9/host.js +0 -1
- package/dist/web-ui/content/modules/e020beea3e7da7db/chunk.js +0 -1
- package/dist/web-ui/content/modules/e2feda63a603c64e/group-editor.js +0 -1
- package/dist/web-ui/content/modules/e70296674a841d60/chunk.js +0 -1
- package/dist/web-ui/content/modules/ec2f6b23d12d3a15/chunk.js +0 -1
- package/dist/web-ui/content/modules/ede751e1f2500d03/employee-profile.js +0 -1
- package/dist/web-ui/content/modules/ee4301b734e2fd43/chunk.js +0 -1
- package/dist/web-ui/content/modules/f4e6095edc32562c/markdown.js +0 -1
- package/dist/web-ui/content/modules/f7f0710962cab0e0/chunk.js +0 -10
- package/dist/web-ui/content/modules/fe30627fc1e59d4f/chunk.js +0 -1
- package/dist/web-ui/content/modules/ffbd3bcaecd8e814/task-board-host.js +0 -10
package/dist/retention.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AI_TEAM_ACTIVE_RUN_STATUSES } from "./ai-team-types.js";
|
|
2
1
|
import { isArchiveBlocked } from "./archive-policy.js";
|
|
3
2
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
3
|
+
import { createRetentionProtection } from "./retention-protection.js";
|
|
4
4
|
import { defaultTaskRetention, taskRetentionWindows } from "./task-retention.js";
|
|
5
5
|
import { parseTimestampMs as timestamp } from "./time-utils.js";
|
|
6
6
|
/** Backward-compatible exports for consumers of the historical default windows. */
|
|
@@ -48,12 +48,16 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
48
48
|
};
|
|
49
49
|
const taskWindows = taskRetentionWindows(typeof deps.taskRetention === "function" ? deps.taskRetention() : deps.taskRetention);
|
|
50
50
|
const snapshots = deps.sessions.listSlim();
|
|
51
|
+
// Eligibility is fixed from the pre-mutation snapshot, so a task archived in this pass cannot be purged in it.
|
|
52
|
+
const protection = createRetentionProtection(deps.storage, snapshots, taskWindows.archivedMs, now);
|
|
53
|
+
const purgedSessionIds = new Set();
|
|
54
|
+
const archivedSessionIds = new Set();
|
|
51
55
|
for (const session of snapshots) {
|
|
52
56
|
// 真正还在处理的会话不能归档或杀掉。空闲 PTY 壳不算。
|
|
53
57
|
if (isArchiveBlocked(session))
|
|
54
58
|
continue;
|
|
55
59
|
if (session.archived) {
|
|
56
|
-
if (taskWindows.archivedMs === null)
|
|
60
|
+
if (!protection.canPurgeSession(session.id) || taskWindows.archivedMs === null)
|
|
57
61
|
continue;
|
|
58
62
|
const archivedAt = timestamp(session.archivedAt);
|
|
59
63
|
if (archivedAt === null) {
|
|
@@ -66,29 +70,15 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
66
70
|
}
|
|
67
71
|
if (now - archivedAt < taskWindows.archivedMs)
|
|
68
72
|
continue;
|
|
69
|
-
|
|
70
|
-
deps.sessions.delete(session.id);
|
|
71
|
-
result.purgedSessions += 1;
|
|
72
|
-
}
|
|
73
|
-
catch { /* best-effort: a session may disappear between the read and the purge */ }
|
|
73
|
+
purgeRetentionSession(deps, session, purgedSessionIds, result);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
|
-
if (taskWindows.idleMs === null)
|
|
76
|
+
if (taskWindows.idleMs === null || !protection.canArchiveSession(session.id))
|
|
77
77
|
continue;
|
|
78
78
|
const reference = timestamp(session.endedAt ?? session.startedAt);
|
|
79
79
|
if (reference === null || now - reference < taskWindows.idleMs)
|
|
80
80
|
continue;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
deps.sessions.stop(session.id);
|
|
84
|
-
}
|
|
85
|
-
catch { /* shell may already be gone */ }
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
deps.sessions.setArchived(session.id, true);
|
|
89
|
-
result.archivedSessions += 1;
|
|
90
|
-
}
|
|
91
|
-
catch { /* best-effort */ }
|
|
81
|
+
archiveRetentionSession(deps, session, archivedSessionIds, result);
|
|
92
82
|
}
|
|
93
83
|
const sessionsByTask = new Map();
|
|
94
84
|
for (const session of snapshots) {
|
|
@@ -101,21 +91,16 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
101
91
|
else
|
|
102
92
|
sessionsByTask.set(taskId, [session]);
|
|
103
93
|
}
|
|
104
|
-
const
|
|
105
|
-
for (const task of deps.storage.listWandTasks()) {
|
|
94
|
+
for (const task of protection.tasks) {
|
|
106
95
|
const bound = task.workspaceTaskId ? sessionsByTask.get(task.workspaceTaskId) ?? [] : [];
|
|
107
|
-
const running = bound.some((session) => isArchiveBlocked(session))
|
|
108
|
-
|| activeTeamTaskIds.has(task.id) || (!!task.workspaceTaskId && activeTeamTaskIds.has(task.workspaceTaskId));
|
|
109
96
|
if (task.status === "archived") {
|
|
110
|
-
if (running || taskWindows.archivedMs === null)
|
|
111
|
-
continue;
|
|
112
97
|
const archivedAt = timestamp(task.archivedAt);
|
|
113
98
|
if (archivedAt === null) {
|
|
114
|
-
//
|
|
99
|
+
// 只补时间戳;状态未变,不会把升级前归档标记为可自动删除。
|
|
115
100
|
deps.storage.updateWandTask(task.id, { status: "archived" });
|
|
116
101
|
continue;
|
|
117
102
|
}
|
|
118
|
-
if (
|
|
103
|
+
if (!protection.canPurgeTask(task.id))
|
|
119
104
|
continue;
|
|
120
105
|
const container = task.workspaceTaskId ? deps.storage.getWorkspaceTask(task.workspaceTaskId) : null;
|
|
121
106
|
if (container?.worktree) {
|
|
@@ -128,7 +113,7 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
128
113
|
result.purgedTasks += 1;
|
|
129
114
|
continue;
|
|
130
115
|
}
|
|
131
|
-
if (
|
|
116
|
+
if (taskWindows.idleMs === null || !protection.canArchiveTask(task))
|
|
132
117
|
continue;
|
|
133
118
|
const lastActivity = taskLastActivity(deps.storage, task, bound);
|
|
134
119
|
if (lastActivity === null || now - lastActivity < taskWindows.idleMs)
|
|
@@ -136,76 +121,73 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
136
121
|
deps.storage.updateWandTask(task.id, { status: "archived" });
|
|
137
122
|
result.archivedTasks += 1;
|
|
138
123
|
}
|
|
139
|
-
sweepTeamRuns(deps, taskWindows, snapshots, now, result);
|
|
124
|
+
sweepTeamRuns(deps, protection, taskWindows, snapshots, purgedSessionIds, archivedSessionIds, now, result);
|
|
140
125
|
return result;
|
|
141
126
|
}
|
|
142
|
-
function
|
|
127
|
+
function archiveRetentionSession(deps, session, archivedSessionIds, result) {
|
|
128
|
+
if (session.archived || archivedSessionIds.has(session.id))
|
|
129
|
+
return;
|
|
130
|
+
if (isIdleLivePty(session)) {
|
|
131
|
+
try {
|
|
132
|
+
deps.sessions.stop(session.id);
|
|
133
|
+
}
|
|
134
|
+
catch { /* shell may already be gone */ }
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
deps.sessions.setArchived(session.id, true);
|
|
138
|
+
session.archived = true;
|
|
139
|
+
archivedSessionIds.add(session.id);
|
|
140
|
+
result.archivedSessions += 1;
|
|
141
|
+
}
|
|
142
|
+
catch { /* best-effort */ }
|
|
143
|
+
}
|
|
144
|
+
function purgeRetentionSession(deps, session, purgedSessionIds, result) {
|
|
145
|
+
if (purgedSessionIds.has(session.id))
|
|
146
|
+
return;
|
|
147
|
+
if (isIdleLivePty(session)) {
|
|
148
|
+
try {
|
|
149
|
+
deps.sessions.stop(session.id);
|
|
150
|
+
}
|
|
151
|
+
catch { /* already gone */ }
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
deps.sessions.delete(session.id);
|
|
155
|
+
purgedSessionIds.add(session.id);
|
|
156
|
+
result.purgedSessions += 1;
|
|
157
|
+
}
|
|
158
|
+
catch { /* best-effort: a session may disappear between the read and the purge */ }
|
|
159
|
+
}
|
|
160
|
+
function sweepTeamRuns(deps, protection, windows, snapshots, purgedSessionIds, archivedSessionIds, now, result) {
|
|
143
161
|
const sessionById = new Map(snapshots.map((session) => [session.id, session]));
|
|
144
|
-
for (const run of
|
|
145
|
-
if (run.
|
|
146
|
-
continue;
|
|
147
|
-
const linked = teamRunSessionIds(deps.storage, run.id, run.chatSessionId);
|
|
148
|
-
if (linked.some((id) => {
|
|
149
|
-
const session = sessionById.get(id);
|
|
150
|
-
return session ? isRetentionBusy(session) : false;
|
|
151
|
-
}))
|
|
162
|
+
for (const run of protection.runs) {
|
|
163
|
+
if (!protection.canPurgeRun(run.id) && !protection.canArchiveRun(run.id))
|
|
152
164
|
continue;
|
|
165
|
+
const linked = protection.teamSessionIds.get(run.id) ?? [];
|
|
153
166
|
const activity = teamRunActivity(run.updatedAt, linked, sessionById);
|
|
154
167
|
if (activity === null)
|
|
155
168
|
continue;
|
|
156
|
-
if (windows.archivedMs !== null && now - activity >= windows.archivedMs) {
|
|
169
|
+
if (protection.canPurgeRun(run.id) && windows.archivedMs !== null && now - activity >= windows.archivedMs) {
|
|
157
170
|
for (const id of linked) {
|
|
158
171
|
const session = sessionById.get(id);
|
|
159
|
-
if (!session ||
|
|
172
|
+
if (!session || !protection.canPurgeSession(id))
|
|
160
173
|
continue;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
deps.sessions.stop(id);
|
|
164
|
-
}
|
|
165
|
-
catch { /* already gone */ }
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
deps.sessions.delete(id);
|
|
169
|
-
result.purgedSessions += 1;
|
|
170
|
-
sessionById.delete(id);
|
|
171
|
-
}
|
|
172
|
-
catch { /* already gone */ }
|
|
174
|
+
purgeRetentionSession(deps, session, purgedSessionIds, result);
|
|
175
|
+
sessionById.delete(id);
|
|
173
176
|
}
|
|
174
177
|
deps.storage.deleteAiTeamRun(run.id);
|
|
175
178
|
result.purgedTeamRuns += 1;
|
|
176
179
|
continue;
|
|
177
180
|
}
|
|
178
|
-
if (windows.idleMs === null || now - activity < windows.idleMs)
|
|
181
|
+
if (!protection.canArchiveRun(run.id) || windows.idleMs === null || now - activity < windows.idleMs)
|
|
179
182
|
continue;
|
|
180
183
|
for (const id of linked) {
|
|
181
184
|
const session = sessionById.get(id);
|
|
182
|
-
if (!session ||
|
|
185
|
+
if (!session || !protection.canArchiveSession(id))
|
|
183
186
|
continue;
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
deps.sessions.stop(id);
|
|
187
|
-
}
|
|
188
|
-
catch { /* already gone */ }
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
deps.sessions.setArchived(id, true);
|
|
192
|
-
session.archived = true;
|
|
193
|
-
result.archivedSessions += 1;
|
|
194
|
-
}
|
|
195
|
-
catch { /* best-effort */ }
|
|
187
|
+
archiveRetentionSession(deps, session, archivedSessionIds, result);
|
|
196
188
|
}
|
|
197
189
|
}
|
|
198
190
|
}
|
|
199
|
-
function teamRunSessionIds(storage, runId, chatSessionId) {
|
|
200
|
-
const ids = new Set();
|
|
201
|
-
if (chatSessionId)
|
|
202
|
-
ids.add(chatSessionId);
|
|
203
|
-
for (const step of storage.listAiTeamSteps(runId)) {
|
|
204
|
-
if (step.sessionId)
|
|
205
|
-
ids.add(step.sessionId);
|
|
206
|
-
}
|
|
207
|
-
return [...ids];
|
|
208
|
-
}
|
|
209
191
|
function teamRunActivity(updatedAt, sessionIds, sessionById) {
|
|
210
192
|
const times = [];
|
|
211
193
|
const updated = timestamp(updatedAt);
|
|
@@ -308,7 +308,7 @@ export function registerSiliconEmployeeRoutes(app, deps) {
|
|
|
308
308
|
res.setHeader("Cache-Control", "no-store");
|
|
309
309
|
const employeeId = req.params.id;
|
|
310
310
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
311
|
-
res.json({ employeeId, entries: storage.listEmployeeKnowledge(employeeId, query),
|
|
311
|
+
res.json({ employeeId, entries: storage.listEmployeeKnowledge(employeeId, query, EMPLOYEE_KNOWLEDGE_MAX_ENTRIES),
|
|
312
312
|
total: storage.countEmployeeKnowledge(employeeId), maxEntries: EMPLOYEE_KNOWLEDGE_MAX_ENTRIES });
|
|
313
313
|
}
|
|
314
314
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
3
|
import { lstat, mkdir, readdir, readFile, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { getErrorMessage } from "./error-utils.js";
|
|
@@ -12,10 +13,46 @@ import { parseBoundedInteger } from "./request-limits.js";
|
|
|
12
13
|
import { runGitRawAsync } from "./git-utils.js";
|
|
13
14
|
import { parseStoredPathList, recordRecentPath } from "./recent-paths.js";
|
|
14
15
|
import { streamFileWithRange } from "./http-file-stream.js";
|
|
16
|
+
import { HTML_PREVIEW_CSP } from "./html-preview-policy.js";
|
|
15
17
|
const execAsync = promisify(exec);
|
|
16
18
|
const DIRECTORY_MAX_ITEMS = 200;
|
|
17
19
|
const MAX_TEXT_PREVIEW_SIZE = 512 * 1024;
|
|
18
20
|
const MAX_TEXT_WRITE_SIZE = 1024 * 1024;
|
|
21
|
+
/** Query/root values are already decoded filesystem names; whitespace may be
|
|
22
|
+
* literal. Home expansion must not apply the folder input's trimming policy. */
|
|
23
|
+
function resolveReadPath(value) {
|
|
24
|
+
return path.resolve(value.startsWith("~/") ? path.join(homedir(), value.slice(2)) : value);
|
|
25
|
+
}
|
|
26
|
+
/** Editor links may end in :line:column, but those characters are legal in
|
|
27
|
+
* filenames. Read the exact file first; writes never use this fallback. */
|
|
28
|
+
async function readableFilePath(input, allowDirectory = false) {
|
|
29
|
+
const fallback = input.replace(/:\d+(?::\d+)?$/, "");
|
|
30
|
+
if (fallback === input)
|
|
31
|
+
return input;
|
|
32
|
+
try {
|
|
33
|
+
const exact = await stat(input);
|
|
34
|
+
if (exact.isFile() || (allowDirectory && exact.isDirectory()))
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code || ""))
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (isBlockedFolderPath(fallback))
|
|
42
|
+
return input;
|
|
43
|
+
try {
|
|
44
|
+
if ((await stat(fallback)).isFile())
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code || ""))
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
function readableFileExtension(filePath) {
|
|
54
|
+
return path.extname(filePath.replace(/:\d+(?::\d+)?$/, "")).toLowerCase();
|
|
55
|
+
}
|
|
19
56
|
export function registerFileRoutes(app, deps) {
|
|
20
57
|
const { storage, defaultCwd } = deps;
|
|
21
58
|
// Resolve relative artifacts against the stored source session, never a client-supplied cwd.
|
|
@@ -26,15 +63,17 @@ export function registerFileRoutes(app, deps) {
|
|
|
26
63
|
return;
|
|
27
64
|
}
|
|
28
65
|
const relative = typeof req.query.path === "string" ? req.query.path : "";
|
|
29
|
-
|
|
66
|
+
const editorLocation = /^[^/:]+:\d+(?::\d+)?$/.test(relative);
|
|
67
|
+
if (!relative || path.isAbsolute(relative) || /[\u0000-\u001f]/.test(relative) || (/^[a-z][a-z\d+.-]*:/i.test(relative) && !editorLocation)) {
|
|
30
68
|
res.status(400).json({ error: "文件相对路径无效。" });
|
|
31
69
|
return;
|
|
32
70
|
}
|
|
33
|
-
|
|
71
|
+
let resolved = path.resolve(session.cwd, relative);
|
|
34
72
|
if (!isPathWithinBase(resolved, path.resolve(session.cwd)) || isBlockedFolderPath(resolved)) {
|
|
35
73
|
res.status(403).json({ error: "文件路径超出会话目录。" });
|
|
36
74
|
return;
|
|
37
75
|
}
|
|
76
|
+
resolved = await readableFilePath(resolved, relative.endsWith("/"));
|
|
38
77
|
const canonical = await realpath(resolved).catch(() => null);
|
|
39
78
|
const base = await realpath(session.cwd).catch(() => null);
|
|
40
79
|
if (canonical && base && (!isPathWithinBase(canonical, base) || isBlockedFolderPath(canonical))) {
|
|
@@ -42,11 +81,22 @@ export function registerFileRoutes(app, deps) {
|
|
|
42
81
|
return;
|
|
43
82
|
}
|
|
44
83
|
if (req.query.mode === "preview") {
|
|
45
|
-
if (!/\.html?$/i.test(resolved)) {
|
|
84
|
+
if (!relative.endsWith("/") && !/\.html?(?::\d+(?::\d+)?)?$/i.test(resolved)) {
|
|
46
85
|
res.status(400).json({ error: "目标不是 HTML 文件。" });
|
|
47
86
|
return;
|
|
48
87
|
}
|
|
49
|
-
|
|
88
|
+
const search = typeof req.query.search === "string" ? req.query.search : "";
|
|
89
|
+
if (search && (!search.startsWith("?") || /[\u0000-\u0020#]/.test(search) || search.length > 8192)) {
|
|
90
|
+
res.status(400).json({ error: "预览参数无效。" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.redirect(307, "/api/local-file/" + Buffer.from(resolved).toString("base64url") + "/" + search);
|
|
94
|
+
}
|
|
95
|
+
else if (relative.endsWith("/") && req.query.mode === "download") {
|
|
96
|
+
res.redirect(307, "/api/local-file/" + Buffer.from(resolved).toString("base64url") + "/?__wand_file_action=download");
|
|
97
|
+
}
|
|
98
|
+
else if (req.query.mode === "metadata") {
|
|
99
|
+
res.redirect(307, "/api/file-preview?path=" + encodeURIComponent(resolved));
|
|
50
100
|
}
|
|
51
101
|
else {
|
|
52
102
|
res.redirect(307, "/api/file-raw?path=" + encodeURIComponent(resolved) + (req.query.mode === "download" ? "&download=1" : ""));
|
|
@@ -123,19 +173,20 @@ export function registerFileRoutes(app, deps) {
|
|
|
123
173
|
res.status(400).json({ error: "Missing path parameter" });
|
|
124
174
|
return;
|
|
125
175
|
}
|
|
126
|
-
|
|
176
|
+
let resolvedPath = resolveReadPath(filePath);
|
|
127
177
|
if (isBlockedFolderPath(resolvedPath)) {
|
|
128
178
|
res.status(403).json({ error: "Access denied" });
|
|
129
179
|
return;
|
|
130
180
|
}
|
|
131
181
|
try {
|
|
182
|
+
resolvedPath = await readableFilePath(resolvedPath);
|
|
132
183
|
const fileStat = await stat(resolvedPath);
|
|
133
184
|
if (fileStat.isDirectory()) {
|
|
134
185
|
res.status(400).json({ error: "Cannot preview a directory" });
|
|
135
186
|
return;
|
|
136
187
|
}
|
|
137
|
-
const ext =
|
|
138
|
-
const baseName = path.basename(
|
|
188
|
+
const ext = readableFileExtension(resolvedPath);
|
|
189
|
+
const baseName = path.basename(resolvedPath);
|
|
139
190
|
const kind = classifyFile(ext, baseName);
|
|
140
191
|
const mime = mimeForExt(ext);
|
|
141
192
|
if (kind !== "text") {
|
|
@@ -153,6 +204,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
153
204
|
if (fileStat.size > MAX_TEXT_PREVIEW_SIZE) {
|
|
154
205
|
res.status(413).json({
|
|
155
206
|
error: "文件太大,无法在线预览(限 512 KB)。",
|
|
207
|
+
path: resolvedPath,
|
|
156
208
|
truncated: true,
|
|
157
209
|
size: fileStat.size,
|
|
158
210
|
maxSize: MAX_TEXT_PREVIEW_SIZE,
|
|
@@ -212,7 +264,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
212
264
|
res.status(400).json({ error: "目标不是普通文件。" });
|
|
213
265
|
return;
|
|
214
266
|
}
|
|
215
|
-
const ext =
|
|
267
|
+
const ext = readableFileExtension(resolvedPath);
|
|
216
268
|
const baseName = path.basename(resolvedPath);
|
|
217
269
|
if (classifyFile(ext, baseName) !== "text") {
|
|
218
270
|
res.status(415).json({ error: "仅支持编辑文本类文件。" });
|
|
@@ -377,19 +429,20 @@ export function registerFileRoutes(app, deps) {
|
|
|
377
429
|
res.status(400).json({ error: "Missing path parameter" });
|
|
378
430
|
return;
|
|
379
431
|
}
|
|
380
|
-
|
|
432
|
+
let resolvedPath = resolveReadPath(filePath);
|
|
381
433
|
if (isBlockedFolderPath(resolvedPath)) {
|
|
382
434
|
res.status(403).json({ error: "Access denied" });
|
|
383
435
|
return;
|
|
384
436
|
}
|
|
385
437
|
try {
|
|
438
|
+
resolvedPath = await readableFilePath(resolvedPath);
|
|
386
439
|
const fileStat = await stat(resolvedPath);
|
|
387
440
|
if (!fileStat.isFile()) {
|
|
388
441
|
res.status(400).json({ error: "Not a regular file" });
|
|
389
442
|
return;
|
|
390
443
|
}
|
|
391
|
-
const ext = path.extname(
|
|
392
|
-
const baseName = path.basename(
|
|
444
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
445
|
+
const baseName = path.basename(resolvedPath);
|
|
393
446
|
const kind = classifyFile(ext, baseName);
|
|
394
447
|
const cap = RAW_MAX_BYTES_BY_KIND[kind] ?? RAW_MAX_BYTES_BY_KIND.binary;
|
|
395
448
|
// Inline responses are previews and stay size-bounded. Explicit downloads are
|
|
@@ -413,7 +466,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
413
466
|
headers: {
|
|
414
467
|
"Cache-Control": "private, max-age=60",
|
|
415
468
|
"X-Content-Type-Options": "nosniff",
|
|
416
|
-
...(/\.html?$/i.test(ext) ? { "Content-Security-Policy":
|
|
469
|
+
...(/\.html?$/i.test(ext) ? { "Content-Security-Policy": HTML_PREVIEW_CSP } : {}),
|
|
417
470
|
},
|
|
418
471
|
readErrorMessage: "Failed to read file",
|
|
419
472
|
});
|
|
@@ -430,6 +483,11 @@ export function registerFileRoutes(app, deps) {
|
|
|
430
483
|
res.status(405).json({ error: "本地文件预览只支持 GET。" });
|
|
431
484
|
return;
|
|
432
485
|
}
|
|
486
|
+
if (req.originalUrl.split("?")[0] === req.baseUrl) {
|
|
487
|
+
const search = req.originalUrl.includes("?") ? req.originalUrl.slice(req.originalUrl.indexOf("?")) : "";
|
|
488
|
+
res.redirect(307, req.baseUrl + "/" + search);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
433
491
|
const encodedRoot = String(req.params.encodedRoot ?? "");
|
|
434
492
|
if (!/^[A-Za-z0-9_-]*$/.test(encodedRoot)) {
|
|
435
493
|
res.status(400).json({ error: "本地预览路径格式无效。" });
|
|
@@ -447,7 +505,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
447
505
|
res.status(400).json({ error: "缺少本地预览路径。" });
|
|
448
506
|
return;
|
|
449
507
|
}
|
|
450
|
-
|
|
508
|
+
let requestedRoot = resolveReadPath(rootPath);
|
|
451
509
|
if (isBlockedFolderPath(requestedRoot)) {
|
|
452
510
|
res.status(403).json({ error: "访问被拒绝:无法访问系统敏感目录。" });
|
|
453
511
|
return;
|
|
@@ -462,9 +520,10 @@ export function registerFileRoutes(app, deps) {
|
|
|
462
520
|
}
|
|
463
521
|
relativePath = relativePath.replace(/^\/+/, "");
|
|
464
522
|
try {
|
|
523
|
+
requestedRoot = await readableFilePath(requestedRoot, true);
|
|
465
524
|
const rootStat = await stat(requestedRoot);
|
|
466
525
|
const baseRoot = rootStat.isFile() ? path.dirname(requestedRoot) : requestedRoot;
|
|
467
|
-
const requested = relativePath
|
|
526
|
+
const requested = relativePath || (rootStat.isFile() ? path.basename(requestedRoot) : "index.html");
|
|
468
527
|
const resolvedPath = path.resolve(baseRoot, requested);
|
|
469
528
|
if (!isPathWithinBase(resolvedPath, baseRoot) || isBlockedFolderPath(resolvedPath)) {
|
|
470
529
|
res.status(403).json({ error: "访问被拒绝:路径越界。" });
|
|
@@ -473,6 +532,11 @@ export function registerFileRoutes(app, deps) {
|
|
|
473
532
|
let fileStat = await stat(resolvedPath);
|
|
474
533
|
let filePath = resolvedPath;
|
|
475
534
|
if (fileStat.isDirectory()) {
|
|
535
|
+
if (!req.originalUrl.split("?")[0].endsWith("/")) {
|
|
536
|
+
const search = req.originalUrl.includes("?") ? req.originalUrl.slice(req.originalUrl.indexOf("?")) : "";
|
|
537
|
+
res.redirect(307, req.originalUrl.split("?")[0] + "/" + search);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
476
540
|
filePath = path.join(resolvedPath, "index.html");
|
|
477
541
|
fileStat = await stat(filePath);
|
|
478
542
|
}
|
|
@@ -486,7 +550,11 @@ export function registerFileRoutes(app, deps) {
|
|
|
486
550
|
res.status(403).json({ error: "访问被拒绝:资源链接目标越界。" });
|
|
487
551
|
return;
|
|
488
552
|
}
|
|
489
|
-
|
|
553
|
+
if (req.query.__wand_file_action === "metadata" || req.query.__wand_file_action === "download") {
|
|
554
|
+
res.redirect(307, (req.query.__wand_file_action === "metadata" ? "/api/file-preview?path=" : "/api/file-raw?download=1&path=") + encodeURIComponent(filePath));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const ext = readableFileExtension(filePath);
|
|
490
558
|
const baseName = path.basename(filePath);
|
|
491
559
|
const encodedName = encodeURIComponent(baseName);
|
|
492
560
|
streamFileWithRange(req, res, {
|
|
@@ -498,7 +566,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
498
566
|
"Cache-Control": "private, no-cache",
|
|
499
567
|
"X-Content-Type-Options": "nosniff",
|
|
500
568
|
// Even opening the artifact directly must not grant the Wand origin.
|
|
501
|
-
...(/\.html?$/i.test(ext) ? { "Content-Security-Policy":
|
|
569
|
+
...(/\.html?$/i.test(ext) ? { "Content-Security-Policy": HTML_PREVIEW_CSP } : {}),
|
|
502
570
|
},
|
|
503
571
|
readErrorMessage: "读取本地文件失败。",
|
|
504
572
|
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
/** Mounted after requireAuth and the sessions scope guard. No capability/credential DTOs. */
|
|
4
|
+
export declare function registerKnowledgeRoutes(app: Express, storage: WandStorage): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KnowledgeError } from "./knowledge-types.js";
|
|
2
|
+
function fields(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
|
+
throw new KnowledgeError("请求必须包含知识库归属。");
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function namespace(value) {
|
|
8
|
+
const data = fields(value);
|
|
9
|
+
if (data.scope !== "personal" && data.scope !== "project")
|
|
10
|
+
throw new KnowledgeError("请选择个人或项目知识库。");
|
|
11
|
+
if (typeof data.ownerId !== "string" || !data.ownerId.trim() || data.ownerId.length > 200)
|
|
12
|
+
throw new KnowledgeError("请选择员工或项目。");
|
|
13
|
+
return { scope: data.scope, ownerId: data.ownerId };
|
|
14
|
+
}
|
|
15
|
+
function documentInput(value) {
|
|
16
|
+
const data = fields(value);
|
|
17
|
+
if (typeof data.content !== "string" || typeof data.title !== "string" || typeof data.folder !== "string") {
|
|
18
|
+
throw new KnowledgeError("正文、标题和目录必须是文字。");
|
|
19
|
+
}
|
|
20
|
+
return { content: data.content, title: data.title, folder: data.folder };
|
|
21
|
+
}
|
|
22
|
+
function revision(value) {
|
|
23
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
24
|
+
throw new KnowledgeError("请先读取文档的最新版本。");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** Mounted after requireAuth and the sessions scope guard. No capability/credential DTOs. */
|
|
28
|
+
export function registerKnowledgeRoutes(app, storage) {
|
|
29
|
+
const handle = (fn) => (req, res) => {
|
|
30
|
+
res.setHeader("Cache-Control", "no-store");
|
|
31
|
+
try {
|
|
32
|
+
res.json(fn(req));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof KnowledgeError)
|
|
36
|
+
res.status(error.status).json({ error: error.message });
|
|
37
|
+
else
|
|
38
|
+
res.status(500).json({ error: "知识库暂时不可用,请稍后重试。" });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
app.get("/api/knowledge/owners", handle(() => storage.knowledgeOwners()));
|
|
42
|
+
app.get("/api/knowledge", handle(req => {
|
|
43
|
+
const { scope, ownerId } = namespace(req.query);
|
|
44
|
+
if (req.query.q !== undefined && typeof req.query.q !== "string")
|
|
45
|
+
throw new KnowledgeError("搜索内容必须是文字。");
|
|
46
|
+
return storage.readKnowledgeLibrary(scope, ownerId, req.query.q);
|
|
47
|
+
}));
|
|
48
|
+
app.post("/api/knowledge", handle(req => {
|
|
49
|
+
const { scope, ownerId } = namespace(req.body);
|
|
50
|
+
return storage.createKnowledgeDocument(scope, ownerId, documentInput(req.body));
|
|
51
|
+
}));
|
|
52
|
+
app.patch("/api/knowledge/:id", handle(req => {
|
|
53
|
+
const { scope, ownerId } = namespace(req.body);
|
|
54
|
+
return { entry: storage.updateKnowledgeDocument(scope, ownerId, String(req.params.id), revision(req.body.revision), documentInput(req.body)) };
|
|
55
|
+
}));
|
|
56
|
+
app.delete("/api/knowledge/:id", handle(req => {
|
|
57
|
+
const { scope, ownerId } = namespace(req.body);
|
|
58
|
+
storage.deleteKnowledgeDocument(scope, ownerId, String(req.params.id), revision(req.body.revision));
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { getErrorMessage } from "./error-utils.js";
|
|
3
3
|
import { asyncRoute } from "./express-async.js";
|
|
4
|
+
import { HTML_PREVIEW_CSP } from "./html-preview-policy.js";
|
|
4
5
|
const MAX_PORT = 65535;
|
|
5
6
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
6
7
|
/** URL 里可能写 `localhost` / `[::1]`,建立连接前规范成裸主机名。 */
|
|
@@ -51,13 +52,12 @@ export function parseLocalPreviewTarget(rawHost, rawPort, rawPath) {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
function proxyLocation(value, target) {
|
|
54
|
-
if (value.startsWith("/")) {
|
|
55
|
-
return `/api/local-preview/${target.hostname}/${target.port}${value}`;
|
|
56
|
-
}
|
|
57
55
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const host = target.hostname.includes(":") ? `[${target.hostname}]` : target.hostname;
|
|
57
|
+
const url = new URL(value, `http://${host}:${target.port}${target.path}`);
|
|
58
|
+
const hostname = NORMALIZED_LOOPBACK_HOSTNAMES[url.hostname] ?? url.hostname;
|
|
59
|
+
if (url.protocol === "http:" && !url.username && !url.password && hostname === target.hostname && (url.port || "80") === String(target.port)) {
|
|
60
|
+
return `/api/local-preview/${target.hostname}/${target.port}${url.pathname}${url.search}${url.hash}`;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
catch {
|
|
@@ -77,6 +77,11 @@ export function registerLocalPreviewRoutes(app, dependencies) {
|
|
|
77
77
|
res.status(400).json({ error: "只允许预览本机(localhost)HTTP 服务。" });
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
+
if (req.originalUrl.split("?")[0] === req.baseUrl) {
|
|
81
|
+
const search = req.originalUrl.includes("?") ? req.originalUrl.slice(req.originalUrl.indexOf("?")) : "";
|
|
82
|
+
res.redirect(307, req.baseUrl + "/" + search);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
80
85
|
const headers = {};
|
|
81
86
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
82
87
|
if (FILTERED_REQUEST_HEADERS.has(key) || value === undefined)
|
|
@@ -96,12 +101,26 @@ export function registerLocalPreviewRoutes(app, dependencies) {
|
|
|
96
101
|
method: req.method,
|
|
97
102
|
headers,
|
|
98
103
|
}, resolve);
|
|
104
|
+
const cancel = () => proxyReq.destroy();
|
|
105
|
+
req.once("aborted", cancel);
|
|
106
|
+
res.once("close", cancel);
|
|
107
|
+
proxyReq.once("close", () => {
|
|
108
|
+
req.off("aborted", cancel);
|
|
109
|
+
res.off("close", cancel);
|
|
110
|
+
});
|
|
111
|
+
proxyReq.setTimeout(15_000, () => {
|
|
112
|
+
const error = new Error("本地服务响应超时。");
|
|
113
|
+
error.code = "ETIMEDOUT";
|
|
114
|
+
proxyReq.destroy(error);
|
|
115
|
+
});
|
|
99
116
|
proxyReq.once("error", reject);
|
|
100
117
|
req.once("error", reject);
|
|
101
118
|
req.pipe(proxyReq);
|
|
102
119
|
});
|
|
103
120
|
}
|
|
104
121
|
catch (error) {
|
|
122
|
+
if (res.destroyed)
|
|
123
|
+
return;
|
|
105
124
|
if (!res.headersSent) {
|
|
106
125
|
const code = error.code;
|
|
107
126
|
const message = code === "ECONNREFUSED"
|
|
@@ -124,6 +143,9 @@ export function registerLocalPreviewRoutes(app, dependencies) {
|
|
|
124
143
|
}
|
|
125
144
|
res.setHeader(key, value);
|
|
126
145
|
}
|
|
146
|
+
res.setHeader("Content-Security-Policy", HTML_PREVIEW_CSP);
|
|
147
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
148
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
127
149
|
if (req.method === "HEAD" || !upstream.statusCode) {
|
|
128
150
|
upstream.destroy();
|
|
129
151
|
res.end();
|
|
@@ -18,6 +18,6 @@ export interface PiDefaultsRouteDeps {
|
|
|
18
18
|
* 之前只有「在某个会话里改一次,顺带成为新会话默认」这一条隐式路径,用户在设置页
|
|
19
19
|
* 找不到它;而会话里的写入又只能表达会话本身的选择。这里给出显式入口,语义与
|
|
20
20
|
* `StructuredSessionManager.setPiSettings` 写回默认的那一段保持一致(字段级合并、
|
|
21
|
-
*
|
|
21
|
+
* 新增资源必须真实存在、不可表达的值直接拒绝)。
|
|
22
22
|
*/
|
|
23
23
|
export declare function registerPiDefaultsRoutes(app: Express, deps: PiDefaultsRouteDeps): void;
|