@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.
Files changed (291) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +7 -2
  3. package/dist/distribution-manager.d.ts +2 -0
  4. package/dist/distribution-manager.js +23 -0
  5. package/dist/employee-input-scheduling.d.ts +2 -1
  6. package/dist/employee-knowledge-cli.d.ts +1 -1
  7. package/dist/employee-knowledge-cli.js +91 -20
  8. package/dist/employee-knowledge-content.js +1 -1
  9. package/dist/employee-knowledge-types.d.ts +1 -1
  10. package/dist/employee-knowledge-types.js +1 -1
  11. package/dist/employee-knowledge.d.ts +6 -3
  12. package/dist/employee-knowledge.js +54 -58
  13. package/dist/env-utils.js +1 -1
  14. package/dist/html-preview-policy.d.ts +4 -0
  15. package/dist/html-preview-policy.js +4 -0
  16. package/dist/ios-ota.d.ts +5 -0
  17. package/dist/ios-ota.js +11 -1
  18. package/dist/knowledge-skill.d.ts +2 -0
  19. package/dist/knowledge-skill.js +17 -0
  20. package/dist/knowledge-types.d.ts +36 -0
  21. package/dist/knowledge-types.js +8 -0
  22. package/dist/managed-skills-ensure.js +2 -0
  23. package/dist/middleware/api-scope.js +2 -0
  24. package/dist/path-repair.js +28 -6
  25. package/dist/pi-resource-catalog.d.ts +3 -1
  26. package/dist/pi-resource-catalog.js +25 -0
  27. package/dist/pi-session-settings.d.ts +2 -0
  28. package/dist/pi-session-settings.js +25 -0
  29. package/dist/process-manager.d.ts +2 -0
  30. package/dist/process-manager.js +35 -12
  31. package/dist/quick-terminal-session.d.ts +3 -0
  32. package/dist/quick-terminal-session.js +5 -0
  33. package/dist/retention-protection.d.ts +15 -0
  34. package/dist/retention-protection.js +134 -0
  35. package/dist/retention.d.ts +3 -2
  36. package/dist/retention.js +58 -76
  37. package/dist/server-employee-routes.js +1 -1
  38. package/dist/server-file-routes.js +84 -16
  39. package/dist/server-knowledge-routes.d.ts +4 -0
  40. package/dist/server-knowledge-routes.js +61 -0
  41. package/dist/server-local-preview-routes.js +28 -6
  42. package/dist/server-pi-defaults-routes.d.ts +1 -1
  43. package/dist/server-pi-defaults-routes.js +13 -20
  44. package/dist/server-session-lifecycle-routes.js +17 -17
  45. package/dist/server-session-list-routes.js +1 -1
  46. package/dist/server-task-routes.js +6 -0
  47. package/dist/server-update-routes.d.ts +1 -0
  48. package/dist/server-update-routes.js +62 -17
  49. package/dist/server-workspace-routes.js +5 -5
  50. package/dist/server.js +4 -2
  51. package/dist/session-questions.d.ts +7 -0
  52. package/dist/session-questions.js +25 -0
  53. package/dist/session-registry.d.ts +3 -0
  54. package/dist/session-registry.js +17 -0
  55. package/dist/session-request.d.ts +1 -1
  56. package/dist/session-request.js +6 -2
  57. package/dist/session-service.js +1 -1
  58. package/dist/storage/employees-repository.js +5 -2
  59. package/dist/storage/knowledge-repository.d.ts +35 -0
  60. package/dist/storage/knowledge-repository.js +208 -0
  61. package/dist/storage/mappers.js +1 -1
  62. package/dist/storage/migrations.js +51 -0
  63. package/dist/storage/sessions-repository.d.ts +2 -0
  64. package/dist/storage/sessions-repository.js +16 -0
  65. package/dist/storage/tasks-repository.d.ts +7 -0
  66. package/dist/storage/tasks-repository.js +21 -0
  67. package/dist/storage/workspaces-repository.d.ts +1 -0
  68. package/dist/storage/workspaces-repository.js +2 -1
  69. package/dist/storage.d.ts +3 -1
  70. package/dist/storage.js +4 -0
  71. package/dist/structured-session-manager.d.ts +3 -0
  72. package/dist/structured-session-manager.js +13 -4
  73. package/dist/task-action-types.d.ts +22 -0
  74. package/dist/task-action-types.js +1 -0
  75. package/dist/task-actions.d.ts +5 -0
  76. package/dist/task-actions.js +86 -0
  77. package/dist/task-execution-queue.d.ts +1 -1
  78. package/dist/task-execution-queue.js +4 -1
  79. package/dist/task-retention.d.ts +4 -2
  80. package/dist/task-retention.js +2 -0
  81. package/dist/types.d.ts +2 -1
  82. package/dist/web-ui/content/ai-teams.js +12 -12
  83. package/dist/web-ui/content/module-manifest.json +1 -1
  84. package/dist/web-ui/content/modules/00b168a19664439e/dropdown-surface.js +1 -0
  85. package/dist/web-ui/content/modules/059c5fb7f128d340/task-board-host.js +27 -0
  86. package/dist/web-ui/content/modules/07494f5a623e4471/chat-render.js +76 -0
  87. package/dist/web-ui/content/modules/{bc28252270bcc51a → 0a743ad3b4db6874}/chunk.js +1 -1
  88. package/dist/web-ui/content/modules/0d76e582a7a71b2f/chunk.js +1 -0
  89. package/dist/web-ui/content/modules/11af5d14b30e57b6/chunk.js +1 -0
  90. package/dist/web-ui/content/modules/16dab1297efd71a7/chunk.js +10 -0
  91. package/dist/web-ui/content/modules/182929e188c1ab3a/host.js +5 -0
  92. package/dist/web-ui/content/modules/1858670c0f84fec5/employee-profile.js +1 -0
  93. package/dist/web-ui/content/modules/{1cf252d2f64a6279 → 1eab7410d5586038}/chunk.js +1 -1
  94. package/dist/web-ui/content/modules/20c47dc0442f5a04/host.js +1 -0
  95. package/dist/web-ui/content/modules/226241b999d12008/host.js +1 -0
  96. package/dist/web-ui/content/modules/23c4b8d223ec64c5/host.js +61 -0
  97. package/dist/web-ui/content/modules/2882c97388ff4f7b/employee-card.js +1 -0
  98. package/dist/web-ui/content/modules/2a791cd2e14aa1e0/notice-view.js +1 -0
  99. package/dist/web-ui/content/modules/2c90c3be260e6cce/progress.js +1 -0
  100. package/dist/web-ui/content/modules/2d7be9935394f397/workspace-window.js +1 -0
  101. package/dist/web-ui/content/modules/2f801e53dda62770/chunk.js +1 -0
  102. package/dist/web-ui/content/modules/2f97ddba3842adaa/chunk.js +1 -0
  103. package/dist/web-ui/content/modules/31157ceb6d23b809/chunk.js +15 -0
  104. package/dist/web-ui/content/modules/389a5112332ec23a/host.js +1 -0
  105. package/dist/web-ui/content/modules/3ea13e8b3c2deaa4/panel-host.js +1 -0
  106. package/dist/web-ui/content/modules/3f8830f8ad28decc/chunk.js +3 -0
  107. package/dist/web-ui/content/modules/{70715360138f4d99 → 3fe11a75284ad8ae}/host.js +1 -1
  108. package/dist/web-ui/content/modules/{e0a1ae9163e77556 → 41445c6589dba325}/host.js +2 -2
  109. package/dist/web-ui/content/modules/{8b2f9a027ae1d7eb → 42e5c34002fcffb7}/chunk.js +1 -1
  110. package/dist/web-ui/content/modules/438807b8bf112cd0/chunk.js +1 -0
  111. package/dist/web-ui/content/modules/4861eca9a8a87ad0/chunk.js +1 -0
  112. package/dist/web-ui/content/modules/{78d06dfdc4b13d78 → 4970319a83432ec7}/chunk.js +1 -1
  113. package/dist/web-ui/content/modules/4a5d669c90f545cb/host.js +1 -0
  114. package/dist/web-ui/content/modules/4ba1e8c74bed7ef2/chunk.js +1 -0
  115. package/dist/web-ui/content/modules/5083a5dbb5b155b7/chunk.js +1 -0
  116. package/dist/web-ui/content/modules/50da446c1377c220/chunk.js +20 -0
  117. package/dist/web-ui/content/modules/52574891ef08e29b/host.js +30 -0
  118. package/dist/web-ui/content/modules/5705d25468274534/host.js +1 -0
  119. package/dist/web-ui/content/modules/579d486763d47c0d/chunk.js +1 -0
  120. package/dist/web-ui/content/modules/57c357d321db5df9/chunk.js +1 -0
  121. package/dist/web-ui/content/modules/5933ce8965aa4004/chunk.js +1 -0
  122. package/dist/web-ui/content/modules/5961ab2947c86ca1/shell-file-panel-drawer.js +1 -0
  123. package/dist/web-ui/content/modules/5b6e1369df7dea69/chunk.js +1 -0
  124. package/dist/web-ui/content/modules/5f298a8f43dd0589/chunk.js +1 -0
  125. package/dist/web-ui/content/modules/62e0023451f428cf/chunk.js +1 -0
  126. package/dist/web-ui/content/modules/6787bdcf56f743a5/host.js +1 -0
  127. package/dist/web-ui/content/modules/67c39b0398186a9f/host.js +1 -0
  128. package/dist/web-ui/content/modules/6b16f35c0627bafb/chunk.js +1 -0
  129. package/dist/web-ui/content/modules/6dd4f3e2f619f7a0/chunk.js +1 -0
  130. package/dist/web-ui/content/modules/70a2e13e0ccc8549/panel.js +1 -0
  131. package/dist/web-ui/content/modules/710db590e9707f73/runtime.js +338 -0
  132. package/dist/web-ui/content/modules/7249950f8771dbeb/workspaces-panel.js +4 -0
  133. package/dist/web-ui/content/modules/771439a41e737ee2/chunk.js +1 -0
  134. package/dist/web-ui/content/modules/{2061d4a88875b9f9 → 816eb9b3a000fb2c}/chunk.js +1 -1
  135. package/dist/web-ui/content/modules/{c0384ffb4ef740a0 → 831b6c7b43e800be}/host.js +1 -1
  136. package/dist/web-ui/content/modules/87c7f50d9455d55e/chunk.js +1 -0
  137. package/dist/web-ui/content/modules/87dcd516ffd9f688/composer-surface-adapter.js +1 -0
  138. package/dist/web-ui/content/modules/8891ca8165435ca1/chunk.js +1 -0
  139. package/dist/web-ui/content/modules/8b7cd7bffdfd69e7/brand-identity.js +1 -0
  140. package/dist/web-ui/content/modules/8bf9917f6c3429f6/host.js +1 -0
  141. package/dist/web-ui/content/modules/8de8368570ab089d/chunk.js +57 -0
  142. package/dist/web-ui/content/modules/8e612d4881fa4118/home.js +6 -0
  143. package/dist/web-ui/content/modules/8e6178262ea63dec/markdown.js +1 -0
  144. package/dist/web-ui/content/modules/913187c92a98a1ac/chunk.js +1 -0
  145. package/dist/web-ui/content/modules/96f2ae970f8c576b/host.js +1 -0
  146. package/dist/web-ui/content/modules/97d7f267959fd385/chunk.js +1 -0
  147. package/dist/web-ui/content/modules/{22497085d7cdd83b → 9b12c2301f3a18f3}/chunk.js +1 -1
  148. package/dist/web-ui/content/modules/{9f4e26a1c1867d65 → 9d941d6ff39861b8}/chunk.js +2 -2
  149. package/dist/web-ui/content/modules/9e163d54ff8fd1aa/chunk.js +1 -0
  150. package/dist/web-ui/content/modules/{0567e161e2cca41e → a2caed0139df4b21}/chunk.js +1 -1
  151. package/dist/web-ui/content/modules/a59734be368daa7e/chunk.js +1 -0
  152. package/dist/web-ui/content/modules/{e4503f8e6379ccdc → a61393bfa976d8d5}/chunk.js +1 -1
  153. package/dist/web-ui/content/modules/{df5d48b4ff04b443 → b63e075f222a1266}/library-bridge.js +2 -2
  154. package/dist/web-ui/content/modules/b713cad51ca4c512/chunk.js +1 -0
  155. package/dist/web-ui/content/modules/ba89cd97627e6682/queue-view-adapter.js +1 -0
  156. package/dist/web-ui/content/modules/bd3daf04a8bf4422/chunk.js +1 -0
  157. package/dist/web-ui/content/modules/{aa7d409d04bbd736 → bd7dc4bb46731ee7}/chunk.js +1 -1
  158. package/dist/web-ui/content/modules/bf3ee58e0da44822/chunk.js +1 -0
  159. package/dist/web-ui/content/modules/c0a446100d969f3e/chunk.js +8 -0
  160. package/dist/web-ui/content/modules/c2f09c8a472f5d83/dialog-modal.js +4 -0
  161. package/dist/web-ui/content/modules/c33935155289c22d/chunk.js +3 -0
  162. package/dist/web-ui/content/modules/c351eb11e0bdec2c/sidebar-peek.js +1 -0
  163. package/dist/web-ui/content/modules/ca7ca5902a35d26e/chunk.js +1 -0
  164. package/dist/web-ui/content/modules/cba6f13c0b2aec1c/chunk.js +1 -0
  165. package/dist/web-ui/content/modules/d434edbb2f4672e5/chunk.js +1 -0
  166. package/dist/web-ui/content/modules/{d569c96d4acfdec3 → d47d75a78ae598e3}/chunk.js +25 -25
  167. package/dist/web-ui/content/modules/d49f106c4d106057/chunk.js +1 -0
  168. package/dist/web-ui/content/modules/d5d28ec375826ef2/chunk.js +3 -0
  169. package/dist/web-ui/content/modules/{7edec8f23f8e0834 → d6b16dedb9011c33}/chunk.js +1 -1
  170. package/dist/web-ui/content/modules/d6fffe2c2a3a738d/toast-region.js +1 -0
  171. package/dist/web-ui/content/modules/d8a7011760f61719/group-editor.js +1 -0
  172. package/dist/web-ui/content/modules/{bf0b693a654e2655 → d9e2b65e9679ad39}/host.js +3 -3
  173. package/dist/web-ui/content/modules/db521a246e14e5e4/chunk.js +4 -0
  174. package/dist/web-ui/content/modules/{7994dec16e8b1039 → dedd4a19bb70e327}/page.js +2 -2
  175. package/dist/web-ui/content/modules/e060508ecd50fa7e/chunk.js +1 -0
  176. package/dist/web-ui/content/modules/e5174201f9d918fb/chunk.js +1 -0
  177. package/dist/web-ui/content/modules/e63582a3bce48f7d/chunk.js +1 -0
  178. package/dist/web-ui/content/modules/ea8fe4b9999fd2aa/chunk.js +1 -0
  179. package/dist/web-ui/content/modules/{d7cdd7dca9a0c9f8 → eac72f69716dba05}/chunk.js +1 -1
  180. package/dist/web-ui/content/modules/ebc53dedc3945514/host.js +1 -0
  181. package/dist/web-ui/content/modules/{0d7f7d954382dc42 → ec33e17c00c1eb24}/chunk.js +1 -1
  182. package/dist/web-ui/content/modules/{99e5cdf0c3bd0e3d → ed7405f064944215}/sha256.js +1 -1
  183. package/dist/web-ui/content/modules/f27a5d3c6c02a71c/chunk.js +1 -0
  184. package/dist/web-ui/content/modules/f383d892fef73483/chunk.js +1 -0
  185. package/dist/web-ui/content/modules/{084dca1ea52bf38e → f3cff7792ba39df8}/login-visual.js +1 -1
  186. package/dist/web-ui/content/modules/f43da7050badf54c/host.js +1 -0
  187. package/dist/web-ui/content/modules/face3dbf117d63e3/chunk.js +1 -0
  188. package/dist/web-ui/content/modules/fb2356eced24c4cc/host-bridge.js +1 -0
  189. package/dist/web-ui/content/styles.css +1 -1
  190. package/dist/web-ui/embedded-assets.d.ts +413 -420
  191. package/dist/web-ui/embedded-assets.js +5 -5
  192. package/dist/web-ui/group-chat-visibility.d.ts +1 -0
  193. package/dist/web-ui/group-chat-visibility.js +1 -1
  194. package/dist/web-ui/markdown.js +10 -36
  195. package/dist/web-ui/message-file-target.d.ts +8 -0
  196. package/dist/web-ui/message-file-target.js +105 -15
  197. package/dist/web-ui/page-route.js +2 -0
  198. package/dist/web-ui/react/local-preview/controller.d.ts +2 -2
  199. package/dist/web-ui/react/local-preview/controller.js +53 -10
  200. package/dist/web-ui/react/workspaces/types.d.ts +1 -0
  201. package/dist/web-ui/server-link-url.d.ts +3 -0
  202. package/dist/web-ui/server-link-url.js +18 -0
  203. package/package.json +2 -1
  204. package/skills/wand-knowledge/SKILL.md +47 -0
  205. package/skills/wand-knowledge/scripts/knowledge.mjs +18 -0
  206. package/dist/web-ui/content/modules/0341f0d00f88a16f/chunk.js +0 -1
  207. package/dist/web-ui/content/modules/069f5a4267b0a9e3/runtime.js +0 -279
  208. package/dist/web-ui/content/modules/0d62c22b7aed5f1f/host-bridge.js +0 -1
  209. package/dist/web-ui/content/modules/0da28408ec089cf7/chunk.js +0 -1
  210. package/dist/web-ui/content/modules/130e8905cc7f639c/toast-region.js +0 -1
  211. package/dist/web-ui/content/modules/186d6e3b44a3c539/host.js +0 -1
  212. package/dist/web-ui/content/modules/1870280f0e783c62/chunk.js +0 -4
  213. package/dist/web-ui/content/modules/19d3bc36fa1e9160/chunk.js +0 -1
  214. package/dist/web-ui/content/modules/1a0b5db535599f72/chunk.js +0 -1
  215. package/dist/web-ui/content/modules/1d1dc0fd8a0210aa/composer-surface-adapter.js +0 -1
  216. package/dist/web-ui/content/modules/23f15023aa2279a3/shell-file-panel-drawer.js +0 -1
  217. package/dist/web-ui/content/modules/2cab58e5d148844c/host.js +0 -1
  218. package/dist/web-ui/content/modules/32edeb64dbf339f6/chunk.js +0 -1
  219. package/dist/web-ui/content/modules/33daaeb4bf68c7ba/chunk.js +0 -9
  220. package/dist/web-ui/content/modules/37da177222b6308d/workspaces-panel.js +0 -4
  221. package/dist/web-ui/content/modules/3af7facf8a7588c7/chunk.js +0 -1
  222. package/dist/web-ui/content/modules/3b95dc1376e932ea/host.js +0 -1
  223. package/dist/web-ui/content/modules/3cbbad48df7998f8/chunk.js +0 -1
  224. package/dist/web-ui/content/modules/41cf79c899bc83c8/host.js +0 -1
  225. package/dist/web-ui/content/modules/444af9de1698eea0/host.js +0 -1
  226. package/dist/web-ui/content/modules/452327275cc5d8f8/host.js +0 -1
  227. package/dist/web-ui/content/modules/466310a17dac465f/chunk.js +0 -3
  228. package/dist/web-ui/content/modules/46f1b6c3274d399b/host.js +0 -1
  229. package/dist/web-ui/content/modules/47998d82dc65bfeb/sidebar-peek.js +0 -1
  230. package/dist/web-ui/content/modules/486a828390c5d44d/host.js +0 -30
  231. package/dist/web-ui/content/modules/4cc15b385bc6cc32/chunk.js +0 -1
  232. package/dist/web-ui/content/modules/4d5ab67f6c0ce19f/chunk.js +0 -1
  233. package/dist/web-ui/content/modules/54aaf60ac85dcb2b/chunk.js +0 -1
  234. package/dist/web-ui/content/modules/54c22eddaf009d00/chunk.js +0 -1
  235. package/dist/web-ui/content/modules/55b931232c672f4f/chunk.js +0 -1
  236. package/dist/web-ui/content/modules/583049de28ef2b02/panel.js +0 -1
  237. package/dist/web-ui/content/modules/5a2a58334d63e127/chunk.js +0 -20
  238. package/dist/web-ui/content/modules/5ba0668f5a22133b/home.js +0 -6
  239. package/dist/web-ui/content/modules/5cd5d3d65d966c5f/chunk.js +0 -1
  240. package/dist/web-ui/content/modules/6b41e59ab342482f/notice-view.js +0 -1
  241. package/dist/web-ui/content/modules/6b5445f41bef0a66/chunk.js +0 -1
  242. package/dist/web-ui/content/modules/6e8395f61c89b5aa/chunk.js +0 -1
  243. package/dist/web-ui/content/modules/6f74cede70164e19/chunk.js +0 -1
  244. package/dist/web-ui/content/modules/704690f5d6ab9157/chunk.js +0 -1
  245. package/dist/web-ui/content/modules/7dce56c1987e8d43/chunk.js +0 -29
  246. package/dist/web-ui/content/modules/80e92c14a6789896/progress.js +0 -1
  247. package/dist/web-ui/content/modules/8a828b4d087667dd/chunk.js +0 -1
  248. package/dist/web-ui/content/modules/8d6f223764846e18/brand-identity.js +0 -1
  249. package/dist/web-ui/content/modules/9136bae16758fd4c/chunk.js +0 -1
  250. package/dist/web-ui/content/modules/93fa5c35e4bcab96/chunk.js +0 -1
  251. package/dist/web-ui/content/modules/96fe39f39930d9e4/chunk.js +0 -8
  252. package/dist/web-ui/content/modules/9db41ad5ac96ffa2/chunk.js +0 -1
  253. package/dist/web-ui/content/modules/a073cb6d182ccd32/chunk.js +0 -1
  254. package/dist/web-ui/content/modules/a8cba9f1d6b49ef1/chunk.js +0 -1
  255. package/dist/web-ui/content/modules/a97b3b9b68f9ca38/chunk.js +0 -57
  256. package/dist/web-ui/content/modules/ac34ed6692b43ede/dropdown-surface.js +0 -1
  257. package/dist/web-ui/content/modules/aff19b31898aa157/chunk.js +0 -1
  258. package/dist/web-ui/content/modules/b2f6e088e6e45878/chunk.js +0 -1
  259. package/dist/web-ui/content/modules/b4446bf92e182bfb/employee-card.js +0 -1
  260. package/dist/web-ui/content/modules/b79c15aace456028/chunk.js +0 -6
  261. package/dist/web-ui/content/modules/b8c18baf7cfb61b1/chat-render.js +0 -76
  262. package/dist/web-ui/content/modules/b8c447171e3d60a0/chunk.js +0 -1
  263. package/dist/web-ui/content/modules/b945370f1fa784f9/chunk.js +0 -13
  264. package/dist/web-ui/content/modules/ba5fa76884e1f806/chunk.js +0 -1
  265. package/dist/web-ui/content/modules/bb1bb9460cc10fbe/host.js +0 -1
  266. package/dist/web-ui/content/modules/be7f1f9183a1247f/chunk.js +0 -3
  267. package/dist/web-ui/content/modules/bf7449bcf95e1454/chunk.js +0 -1
  268. package/dist/web-ui/content/modules/c091efd46e221c29/chunk.js +0 -1
  269. package/dist/web-ui/content/modules/c1ec2ebce8ea5dd8/dialog-modal.js +0 -4
  270. package/dist/web-ui/content/modules/c2f7bb50118f1603/chunk.js +0 -1
  271. package/dist/web-ui/content/modules/c369ce4eb71277e6/host.js +0 -1
  272. package/dist/web-ui/content/modules/c46b3501573953e9/chunk.js +0 -1
  273. package/dist/web-ui/content/modules/c577ad641502d509/panel-host.js +0 -1
  274. package/dist/web-ui/content/modules/c75976857c18f251/host.js +0 -1
  275. package/dist/web-ui/content/modules/c8eaffbedda0d3d0/queue-view-adapter.js +0 -3
  276. package/dist/web-ui/content/modules/cadd26ae50359434/chunk.js +0 -1
  277. package/dist/web-ui/content/modules/d37ff6684c9aeeec/chunk.js +0 -3
  278. package/dist/web-ui/content/modules/d48c1f89342bc4bc/workspace-window.js +0 -1
  279. package/dist/web-ui/content/modules/d859a83f486aab23/chunk.js +0 -15
  280. package/dist/web-ui/content/modules/d8b258a6455d77da/host.js +0 -5
  281. package/dist/web-ui/content/modules/d937e939122180b9/host.js +0 -1
  282. package/dist/web-ui/content/modules/e020beea3e7da7db/chunk.js +0 -1
  283. package/dist/web-ui/content/modules/e2feda63a603c64e/group-editor.js +0 -1
  284. package/dist/web-ui/content/modules/e70296674a841d60/chunk.js +0 -1
  285. package/dist/web-ui/content/modules/ec2f6b23d12d3a15/chunk.js +0 -1
  286. package/dist/web-ui/content/modules/ede751e1f2500d03/employee-profile.js +0 -1
  287. package/dist/web-ui/content/modules/ee4301b734e2fd43/chunk.js +0 -1
  288. package/dist/web-ui/content/modules/f4e6095edc32562c/markdown.js +0 -1
  289. package/dist/web-ui/content/modules/f7f0710962cab0e0/chunk.js +0 -10
  290. package/dist/web-ui/content/modules/fe30627fc1e59d4f/chunk.js +0 -1
  291. 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
- try {
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
- if (isIdleLivePty(session)) {
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 activeTeamTaskIds = new Set(deps.storage.listAiTeamRuns({ statuses: AI_TEAM_ACTIVE_RUN_STATUSES }).map(run => run.taskId));
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 (now - archivedAt < taskWindows.archivedMs)
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 (running || task.status === "doing" || taskWindows.idleMs === null)
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 sweepTeamRuns(deps, windows, snapshots, now, result) {
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 deps.storage.listAiTeamRuns()) {
145
- if (run.status === "running")
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 || isRetentionBusy(session))
172
+ if (!session || !protection.canPurgeSession(id))
160
173
  continue;
161
- if (isIdleLivePty(session)) {
162
- try {
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 || session.archived || isRetentionBusy(session))
185
+ if (!session || !protection.canArchiveSession(id))
183
186
  continue;
184
- if (isIdleLivePty(session)) {
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
- if (!relative || path.isAbsolute(relative) || /[\u0000-\u001f]/.test(relative) || /^[a-z][a-z\d+.-]*:/i.test(relative)) {
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
- const resolved = path.resolve(session.cwd, relative);
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
- res.redirect(307, "/api/local-file/" + Buffer.from(resolved).toString("base64url") + "/");
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
- const resolvedPath = path.resolve(filePath);
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 = path.extname(filePath).toLowerCase();
138
- const baseName = path.basename(filePath);
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 = path.extname(resolvedPath).toLowerCase();
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
- const resolvedPath = path.resolve(filePath);
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(filePath).toLowerCase();
392
- const baseName = path.basename(filePath);
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": "sandbox allow-scripts; connect-src 'none'; form-action 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'" } : {}),
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
- const requestedRoot = normalizeFolderPath(rootPath);
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.trim() || (rootStat.isFile() ? path.basename(requestedRoot) : "index.html");
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
- const ext = path.extname(filePath).toLowerCase();
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": "sandbox allow-scripts; connect-src 'none'; form-action 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'" } : {}),
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 url = new URL(value);
59
- if (url.hostname === target.hostname && url.port === String(target.port)) {
60
- return `/api/local-preview/${target.hostname}/${target.port}${url.pathname}${url.search}`;
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;