@co0ontty/wand 4.57.0 → 4.58.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 (177) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/ensure-node-pty-helper.d.ts +0 -1
  3. package/dist/ensure-node-pty-helper.js +0 -5
  4. package/dist/git-quick-commit.js +18 -19
  5. package/dist/github-connector.d.ts +11 -5
  6. package/dist/github-connector.js +105 -33
  7. package/dist/missions.d.ts +1 -1
  8. package/dist/missions.js +17 -4
  9. package/dist/server-file-routes.js +43 -2
  10. package/dist/server-github-routes.js +50 -53
  11. package/dist/server-mission-routes.js +11 -10
  12. package/dist/server-task-routes.d.ts +14 -2
  13. package/dist/server-task-routes.js +248 -84
  14. package/dist/server-workspace-routes.js +90 -16
  15. package/dist/server.js +2 -20
  16. package/dist/storage.d.ts +23 -6
  17. package/dist/storage.js +130 -53
  18. package/dist/structured-exec-host.d.ts +13 -2
  19. package/dist/structured-exec-host.js +71 -36
  20. package/dist/structured-exec-pump.d.ts +1 -1
  21. package/dist/structured-exec-pump.js +14 -8
  22. package/dist/task-types.d.ts +15 -0
  23. package/dist/terminal-daemon-client.d.ts +2 -0
  24. package/dist/terminal-daemon-client.js +7 -1
  25. package/dist/terminal-daemon-server.js +17 -18
  26. package/dist/tui/ipc-server.js +3 -1
  27. package/dist/types.d.ts +1 -0
  28. package/dist/wand-task-sync.d.ts +9 -0
  29. package/dist/wand-task-sync.js +50 -0
  30. package/dist/web-ui/content/scripts.js +85 -85
  31. package/dist/web-ui/content/styles.css +1 -1
  32. package/dist/web-ui/embedded-assets.js +2 -2
  33. package/dist/ws-broadcast.js +5 -1
  34. package/package.json +2 -3
  35. package/dist/taskboard-bridge.d.ts +0 -11
  36. package/dist/taskboard-bridge.js +0 -371
  37. package/vendor/codex-taskboard/LICENSE +0 -201
  38. package/vendor/codex-taskboard/dist/web/assets/AiChat-BNn4VJ5a.js +0 -12
  39. package/vendor/codex-taskboard/dist/web/assets/GanttView-D8wnfGrG.css +0 -1
  40. package/vendor/codex-taskboard/dist/web/assets/GanttView-rGWhuEX9.js +0 -39
  41. package/vendor/codex-taskboard/dist/web/assets/abnfDiagram-N423BO3Z-DbFG6lbA.js +0 -1
  42. package/vendor/codex-taskboard/dist/web/assets/arc-BjSQqbzd.js +0 -1
  43. package/vendor/codex-taskboard/dist/web/assets/architecture-TIHT7OUA-C9-t8p4o.js +0 -1
  44. package/vendor/codex-taskboard/dist/web/assets/architectureDiagram-T3A2C74G-Db4kJZwO.js +0 -36
  45. package/vendor/codex-taskboard/dist/web/assets/array-BifhSqXX.js +0 -1
  46. package/vendor/codex-taskboard/dist/web/assets/blockDiagram-VBNYF7ZC-C-BywuWV.js +0 -132
  47. package/vendor/codex-taskboard/dist/web/assets/c4Diagram-5PPSVZJV-Dq-80rjP.js +0 -10
  48. package/vendor/codex-taskboard/dist/web/assets/channel-C7mnz8Xc.js +0 -1
  49. package/vendor/codex-taskboard/dist/web/assets/chunk-2GRJ4B5K-B2G57us9.js +0 -1
  50. package/vendor/codex-taskboard/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +0 -1
  51. package/vendor/codex-taskboard/dist/web/assets/chunk-3NCLNEKW-DVGIRib_.js +0 -1
  52. package/vendor/codex-taskboard/dist/web/assets/chunk-4I5QYGJK-kNoGFqr0.js +0 -1
  53. package/vendor/codex-taskboard/dist/web/assets/chunk-5RXB4S5H-4iv6-xM6.js +0 -231
  54. package/vendor/codex-taskboard/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +0 -15
  55. package/vendor/codex-taskboard/dist/web/assets/chunk-6Q2QTUOP-1J34vxHr.js +0 -88
  56. package/vendor/codex-taskboard/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
  57. package/vendor/codex-taskboard/dist/web/assets/chunk-7Z6QIM7H-CDQpQlxX.js +0 -10
  58. package/vendor/codex-taskboard/dist/web/assets/chunk-GF5L2VYU-Dg92oHHl.js +0 -206
  59. package/vendor/codex-taskboard/dist/web/assets/chunk-I66GZJ75-Bk8_gFs2.js +0 -125
  60. package/vendor/codex-taskboard/dist/web/assets/chunk-J7OUQ5F2-CJRcenlr.js +0 -2
  61. package/vendor/codex-taskboard/dist/web/assets/chunk-JQJVKLGR-BLBV_Sus.js +0 -156
  62. package/vendor/codex-taskboard/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +0 -1
  63. package/vendor/codex-taskboard/dist/web/assets/chunk-KBJHAD2P-Da9Yjz0p.js +0 -1
  64. package/vendor/codex-taskboard/dist/web/assets/chunk-KEIR6QF5-Dj-OpFgW.js +0 -161
  65. package/vendor/codex-taskboard/dist/web/assets/chunk-NSK5VX7P-Cb56tnGK.js +0 -2
  66. package/vendor/codex-taskboard/dist/web/assets/chunk-QR6OTTB3-BwoszTxn.js +0 -62
  67. package/vendor/codex-taskboard/dist/web/assets/chunk-RYQCIY6F-D_L2RdcQ.js +0 -1
  68. package/vendor/codex-taskboard/dist/web/assets/chunk-UBXNYLIW-YX5mN8cl.js +0 -1
  69. package/vendor/codex-taskboard/dist/web/assets/chunk-W5SLKNZC-DlOLhhDO.js +0 -1
  70. package/vendor/codex-taskboard/dist/web/assets/chunk-WRU74C26-B7SBmP0K.js +0 -70
  71. package/vendor/codex-taskboard/dist/web/assets/chunk-XXDRQBXY-BuE3VzE_.js +0 -1
  72. package/vendor/codex-taskboard/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +0 -1
  73. package/vendor/codex-taskboard/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +0 -32
  74. package/vendor/codex-taskboard/dist/web/assets/classDiagram-JCYQIIEL-KIdCVvbA.js +0 -1
  75. package/vendor/codex-taskboard/dist/web/assets/classDiagram-v2-OCEON4UE-KIdCVvbA.js +0 -1
  76. package/vendor/codex-taskboard/dist/web/assets/cose-bilkent-JH36ORCC-BzQvy5TC.js +0 -1
  77. package/vendor/codex-taskboard/dist/web/assets/cynefin-VYW2F7L2-D1_5O1XR.js +0 -1
  78. package/vendor/codex-taskboard/dist/web/assets/cynefinDiagram-MW4NZA55-DOW_U5QN.js +0 -62
  79. package/vendor/codex-taskboard/dist/web/assets/cytoscape.esm-DnGd5Fuo.js +0 -321
  80. package/vendor/codex-taskboard/dist/web/assets/dagre-VZM6K2ZE-0Dntc3NT.js +0 -4
  81. package/vendor/codex-taskboard/dist/web/assets/dagre-dpRSp0QF.js +0 -1
  82. package/vendor/codex-taskboard/dist/web/assets/defaultLocale-C8Fc0cco.js +0 -1
  83. package/vendor/codex-taskboard/dist/web/assets/diagram-7IWD3JNH-D7GleYAM.js +0 -30
  84. package/vendor/codex-taskboard/dist/web/assets/diagram-B4RE2ZJO-DCXTe6q5.js +0 -3
  85. package/vendor/codex-taskboard/dist/web/assets/diagram-LBJQPF4R-ZFJyXEXl.js +0 -24
  86. package/vendor/codex-taskboard/dist/web/assets/diagram-Q27KOJAE-DUGCuGd9.js +0 -24
  87. package/vendor/codex-taskboard/dist/web/assets/diagram-UB23O5K3-DRmp4Nng.js +0 -41
  88. package/vendor/codex-taskboard/dist/web/assets/dist-D9sYb5Oa.js +0 -1
  89. package/vendor/codex-taskboard/dist/web/assets/ebnfDiagram-BXEA7PRR-CkKoaf7T.js +0 -1
  90. package/vendor/codex-taskboard/dist/web/assets/erDiagram-JOGREHBK-nBX5z4R4.js +0 -85
  91. package/vendor/codex-taskboard/dist/web/assets/eventmodeling-45OFAUF4-pexmK65N.js +0 -1
  92. package/vendor/codex-taskboard/dist/web/assets/flowDiagram-UKHOOZJN-aroohRbL.js +0 -1
  93. package/vendor/codex-taskboard/dist/web/assets/ganttDiagram-PKOTCBZU-BOJWUTXa.js +0 -292
  94. package/vendor/codex-taskboard/dist/web/assets/gitGraph-TEB2WS4Q-DxnuhSZU.js +0 -1
  95. package/vendor/codex-taskboard/dist/web/assets/gitGraphDiagram-DS77QQ5N-Cte975TC.js +0 -106
  96. package/vendor/codex-taskboard/dist/web/assets/graphlib-DS17s2tU.js +0 -1
  97. package/vendor/codex-taskboard/dist/web/assets/index-Docmx4uQ.css +0 -1
  98. package/vendor/codex-taskboard/dist/web/assets/index-Du07lEGz.js +0 -121
  99. package/vendor/codex-taskboard/dist/web/assets/info-DKCQHKI2-DOTjgYxk.js +0 -1
  100. package/vendor/codex-taskboard/dist/web/assets/infoDiagram-6WML65LV-SpyV9UJd.js +0 -2
  101. package/vendor/codex-taskboard/dist/web/assets/init-D6jRqBbL.js +0 -1
  102. package/vendor/codex-taskboard/dist/web/assets/ishikawaDiagram-WSZJBQD7-T0PZwoUZ.js +0 -70
  103. package/vendor/codex-taskboard/dist/web/assets/journeyDiagram-NVQOT4AX-B88b8UgX.js +0 -139
  104. package/vendor/codex-taskboard/dist/web/assets/kanban-definition-27J2QSJJ-CIlfNnl_.js +0 -89
  105. package/vendor/codex-taskboard/dist/web/assets/katex-B7rAX3Vi.js +0 -257
  106. package/vendor/codex-taskboard/dist/web/assets/line-fWrwPZhB.js +0 -1
  107. package/vendor/codex-taskboard/dist/web/assets/linear-B7l8qgEw.js +0 -1
  108. package/vendor/codex-taskboard/dist/web/assets/map-BaFkSB1l.js +0 -1
  109. package/vendor/codex-taskboard/dist/web/assets/mermaid-parser.core-C5gls2nC.js +0 -7
  110. package/vendor/codex-taskboard/dist/web/assets/mermaid.core-POz2Xcvr.js +0 -11
  111. package/vendor/codex-taskboard/dist/web/assets/mindmap-definition-FAOFIHXS-BM0-egZ5.js +0 -96
  112. package/vendor/codex-taskboard/dist/web/assets/ordinal-hYBb2elL.js +0 -1
  113. package/vendor/codex-taskboard/dist/web/assets/packet-7NZHBO7P-DQZzyVos.js +0 -1
  114. package/vendor/codex-taskboard/dist/web/assets/path-BWPyau1x.js +0 -1
  115. package/vendor/codex-taskboard/dist/web/assets/pegDiagram-VL7TDLO6-B8aEwl-7.js +0 -1
  116. package/vendor/codex-taskboard/dist/web/assets/pie-RZYD4A2V-DRUflilR.js +0 -1
  117. package/vendor/codex-taskboard/dist/web/assets/pieDiagram-7S7Q4E2Y-0cDDxlua.js +0 -39
  118. package/vendor/codex-taskboard/dist/web/assets/purify.es-Bwt4ulIK.js +0 -3
  119. package/vendor/codex-taskboard/dist/web/assets/quadrantDiagram-CIZ2JOQS-CBgb0YZ0.js +0 -7
  120. package/vendor/codex-taskboard/dist/web/assets/radar-I7S5WNFK-tRocoKxq.js +0 -1
  121. package/vendor/codex-taskboard/dist/web/assets/railroad-3IZDKUUU-DaS4ZlX_.js +0 -1
  122. package/vendor/codex-taskboard/dist/web/assets/railroad-abnf-AHOZXSZD-BN1T5dBT.js +0 -1
  123. package/vendor/codex-taskboard/dist/web/assets/railroad-ebnf-EBAXGLYW-Cuf8qyKs.js +0 -1
  124. package/vendor/codex-taskboard/dist/web/assets/railroad-peg-LSFZ7HO6-B511_EYq.js +0 -1
  125. package/vendor/codex-taskboard/dist/web/assets/railroadDiagram-AXF67PYL-QtPo-JZb.js +0 -1
  126. package/vendor/codex-taskboard/dist/web/assets/requirementDiagram-LRYGKXZP-CuBIF85r.js +0 -84
  127. package/vendor/codex-taskboard/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
  128. package/vendor/codex-taskboard/dist/web/assets/rough.esm-CSKSodPl.js +0 -1
  129. package/vendor/codex-taskboard/dist/web/assets/sankeyDiagram-W5VNT64P-CRA-b_za.js +0 -40
  130. package/vendor/codex-taskboard/dist/web/assets/sequenceDiagram-SI44F4Z6-CoddVWwY.js +0 -162
  131. package/vendor/codex-taskboard/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
  132. package/vendor/codex-taskboard/dist/web/assets/src-_wZywoZs.js +0 -1
  133. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-OKZ733FA-D8DMM2ln.js +0 -1
  134. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-v2-UEYNNEHI-CVp0eYAS.js +0 -1
  135. package/vendor/codex-taskboard/dist/web/assets/swimlanes-SLNWSIFB-C1fC7EFu.js +0 -2
  136. package/vendor/codex-taskboard/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CPd58Xso.js +0 -8
  137. package/vendor/codex-taskboard/dist/web/assets/timeline-definition-Z64GVDOM-BUBv8aKP.js +0 -120
  138. package/vendor/codex-taskboard/dist/web/assets/treeView-QDETBFTQ-C-Bve2kY.js +0 -1
  139. package/vendor/codex-taskboard/dist/web/assets/treemap-6X3UGDF4-BcphHDdV.js +0 -1
  140. package/vendor/codex-taskboard/dist/web/assets/vennDiagram-T6HMQDX7-0wQnwAyA.js +0 -34
  141. package/vendor/codex-taskboard/dist/web/assets/wardley-OPB4EBWU-CJZ4BevB.js +0 -1
  142. package/vendor/codex-taskboard/dist/web/assets/wardleyDiagram-T6FBY63Y-U9nwPrkN.js +0 -78
  143. package/vendor/codex-taskboard/dist/web/assets/xychartDiagram-ELKLHX3M-CyCqnzgE.js +0 -7
  144. package/vendor/codex-taskboard/dist/web/codex-agent-logo.png +0 -0
  145. package/vendor/codex-taskboard/dist/web/codex-app-icon.png +0 -0
  146. package/vendor/codex-taskboard/dist/web/favicon.svg +0 -7
  147. package/vendor/codex-taskboard/dist/web/index.html +0 -20
  148. package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +0 -1031
  149. package/vendor/codex-taskboard/server/ai-chat-process.mjs +0 -462
  150. package/vendor/codex-taskboard/server/ai-chat.mjs +0 -1237
  151. package/vendor/codex-taskboard/server/ai-turn-owner.mjs +0 -39
  152. package/vendor/codex-taskboard/server/app.mjs +0 -3211
  153. package/vendor/codex-taskboard/server/cloud-config.mjs +0 -188
  154. package/vendor/codex-taskboard/server/cloud-proxy.mjs +0 -310
  155. package/vendor/codex-taskboard/server/codex-app-server.mjs +0 -376
  156. package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +0 -62
  157. package/vendor/codex-taskboard/server/database.mjs +0 -2845
  158. package/vendor/codex-taskboard/server/index.mjs +0 -41
  159. package/vendor/codex-taskboard/server/jira-config.mjs +0 -181
  160. package/vendor/codex-taskboard/server/jira-integration.mjs +0 -487
  161. package/vendor/codex-taskboard/server/project-summary.mjs +0 -159
  162. package/vendor/codex-taskboard/shared/api-fields.mjs +0 -163
  163. package/vendor/codex-taskboard/shared/codex-environment.mjs +0 -5
  164. package/vendor/codex-taskboard/shared/codex-executable.mjs +0 -77
  165. package/vendor/codex-taskboard/shared/codex-permissions.mjs +0 -9
  166. package/vendor/codex-taskboard/shared/composer-reference.d.mts +0 -19
  167. package/vendor/codex-taskboard/shared/composer-reference.mjs +0 -90
  168. package/vendor/codex-taskboard/shared/domain.mjs +0 -34
  169. package/vendor/codex-taskboard/shared/executable-command.mjs +0 -13
  170. package/vendor/codex-taskboard/shared/process-tree.mjs +0 -27
  171. package/vendor/codex-taskboard/shared/task-input.mjs +0 -308
  172. package/vendor/codex-taskboard/shared/task-records.mjs +0 -199
  173. package/vendor/codex-taskboard/shared/task-relations.mjs +0 -55
  174. package/vendor/codex-taskboard/shared/taskboard-automation.mjs +0 -277
  175. package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +0 -45
  176. package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +0 -4
  177. package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +0 -211
@@ -1,9 +1,15 @@
1
+ import { asyncRoute } from "./express-async.js";
1
2
  import { getErrorMessage } from "./error-utils.js";
2
3
  function sendMissionError(res, error) {
3
4
  const message = getErrorMessage(error, "任务操作失败。");
4
5
  const missing = /^(任务不存在|任务 attempt 不存在)|没有关联会话|当前不可用/.test(message);
5
6
  res.status(missing ? 404 : 400).json({ error: message });
6
7
  }
8
+ function commentIdsFrom(body) {
9
+ if (!body || typeof body !== "object" || !Array.isArray(body.commentIds))
10
+ return [];
11
+ return body.commentIds.filter((id) => typeof id === "string");
12
+ }
7
13
  export function registerMissionRoutes(app, missions) {
8
14
  app.get("/api/inbox", (_req, res) => {
9
15
  res.json({ items: missions.inbox() });
@@ -56,23 +62,18 @@ export function registerMissionRoutes(app, missions) {
56
62
  sendMissionError(res, error);
57
63
  }
58
64
  });
59
- app.post("/api/missions/:missionId/attempts/:attemptId/review/send", (req, res) => {
65
+ app.post("/api/missions/:missionId/attempts/:attemptId/review/send", asyncRoute(async (req, res) => {
60
66
  try {
61
- const commentIds = Array.isArray(req.body?.commentIds)
62
- ? req.body.commentIds.filter((id) => typeof id === "string")
63
- : undefined;
64
- res.status(202).json({ comments: missions.sendReview(req.params.missionId, req.params.attemptId, commentIds) });
67
+ const commentIds = Array.isArray(req.body?.commentIds) ? commentIdsFrom(req.body) : undefined;
68
+ res.status(202).json({ comments: await missions.sendReview(req.params.missionId, req.params.attemptId, commentIds) });
65
69
  }
66
70
  catch (error) {
67
71
  sendMissionError(res, error);
68
72
  }
69
- });
73
+ }));
70
74
  app.post("/api/missions/:missionId/attempts/:attemptId/review/resolve", (req, res) => {
71
75
  try {
72
- const commentIds = Array.isArray(req.body?.commentIds)
73
- ? req.body.commentIds.filter((id) => typeof id === "string")
74
- : [];
75
- res.json({ comments: missions.resolveReview(req.params.missionId, req.params.attemptId, commentIds) });
76
+ res.json({ comments: missions.resolveReview(req.params.missionId, req.params.attemptId, commentIdsFrom(req.body)) });
76
77
  }
77
78
  catch (error) {
78
79
  sendMissionError(res, error);
@@ -1,4 +1,16 @@
1
1
  import type { Express, RequestHandler } from "express";
2
- import type { WandStorage } from "./storage.js";
2
+ import { type WandStorage } from "./storage.js";
3
3
  import type { SessionRegistry } from "./session-registry.js";
4
- export declare function registerTaskRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, requireAdmin?: RequestHandler): void;
4
+ import type { StructuredSessionManager } from "./structured-session-manager.js";
5
+ import type { WandTaskAgent } from "./task-types.js";
6
+ import type { WandConfig } from "./types.js";
7
+ export interface TaskRouteDependencies {
8
+ storage: WandStorage;
9
+ sessions?: SessionRegistry;
10
+ structured?: StructuredSessionManager;
11
+ config?: WandConfig;
12
+ requireAdmin?: RequestHandler;
13
+ }
14
+ /** 任务派发配置:provider / model / thinkingEffort 三者必须同时给出且合法。 */
15
+ export declare function parseTaskAgent(value: unknown): WandTaskAgent | null;
16
+ export declare function registerTaskRoutes(app: Express, deps: TaskRouteDependencies | WandStorage, sessionsLegacy?: SessionRegistry, requireAdminLegacy?: RequestHandler): void;
@@ -1,99 +1,263 @@
1
+ import { getDefaultModelForProvider } from "./config.js";
1
2
  import { asyncRoute } from "./express-async.js";
2
3
  import { getErrorMessage } from "./error-utils.js";
4
+ import { parseWandTaskAgent } from "./storage.js";
5
+ import { archiveBoardTask } from "./wand-task-sync.js";
3
6
  const STATUSES = new Set(["todo", "doing", "done"]);
4
7
  const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
5
- function dateValue(value) { if (value === undefined)
6
- return undefined; if (value === null || value === "")
7
- return null; if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value.trim()))
8
- throw new Error("截止日期必须使用 YYYY-MM-DD 格式。"); return value.trim(); }
9
- function bodyObject(value) { if (!value || typeof value !== "object" || Array.isArray(value))
10
- throw new Error("请求体必须是对象。"); return value; }
11
- function text(value) { return typeof value === "string" ? value.trim() : ""; }
12
- function taskDto(storage, task) { if (!task)
13
- return null; return { ...task, sessionIds: storage.listWandTaskSessionIds(task.id) }; }
14
- export function registerTaskRoutes(app, storage, sessions, requireAdmin) {
15
- const guard = requireAdmin ?? ((_req, _res, next) => next());
16
- app.get("/api/wand-tasks", guard, (req, res) => res.json(storage.listWandTasks(typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined).map((task) => taskDto(storage, task))));
17
- app.post("/api/wand-tasks", guard, (req, res) => { try {
18
- const body = bodyObject(req.body);
19
- const title = text(body.title);
20
- if (!title)
21
- throw new Error("任务标题不能为空。");
22
- const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
23
- if (workspaceId && !storage.getWorkspace(workspaceId))
24
- throw new Error("项目不存在。");
25
- const status = STATUSES.has(body.status) ? body.status : "todo";
26
- const priority = PRIORITIES.has(body.priority) ? body.priority : "none";
27
- const labels = Array.isArray(body.labels) ? body.labels.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20) : [];
28
- const task = storage.createWandTask({ workspaceId, title, description: text(body.description), status, priority, labels, dueDate: dateValue(body.dueDate) ?? null });
29
- res.status(201).json(taskDto(storage, task));
8
+ const AGENT_PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
9
+ const AGENT_EFFORTS = new Set(["off", "standard", "deep", "max"]);
10
+ function dateValue(value) {
11
+ if (value === undefined)
12
+ return undefined;
13
+ if (value === null || value === "")
14
+ return null;
15
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
16
+ throw new Error("截止日期必须使用 YYYY-MM-DD 格式。");
30
17
  }
31
- catch (error) {
32
- res.status(400).json({ error: getErrorMessage(error, "无法创建任务。") });
33
- } });
34
- app.patch("/api/wand-tasks/:id", guard, (req, res) => { try {
35
- const body = bodyObject(req.body);
36
- const patch = {};
37
- if (body.title !== undefined) {
38
- const value = text(body.title);
39
- if (!value)
18
+ return value.trim();
19
+ }
20
+ function bodyObject(value) {
21
+ if (!value || typeof value !== "object" || Array.isArray(value))
22
+ throw new Error("请求体必须是对象。");
23
+ return value;
24
+ }
25
+ function text(value) {
26
+ return typeof value === "string" ? value.trim() : "";
27
+ }
28
+ function labelsFrom(value) {
29
+ if (!Array.isArray(value))
30
+ return [];
31
+ return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20);
32
+ }
33
+ /** 任务派发配置:provider / model / thinkingEffort 三者必须同时给出且合法。 */
34
+ export function parseTaskAgent(value) {
35
+ if (value === null)
36
+ return null;
37
+ if (!value || typeof value !== "object" || Array.isArray(value))
38
+ throw new Error("Agent 配置必须是对象。");
39
+ const body = value;
40
+ const provider = text(body.provider);
41
+ if (!AGENT_PROVIDERS.has(provider))
42
+ throw new Error("请选择有效的 CLI 工具。");
43
+ const model = text(body.model) || "default";
44
+ if (model.length > 128)
45
+ throw new Error("模型名称过长。");
46
+ const thinkingEffort = (text(body.thinkingEffort) || "off");
47
+ if (!AGENT_EFFORTS.has(thinkingEffort))
48
+ throw new Error("思考深度无效。");
49
+ return { provider, model, thinkingEffort };
50
+ }
51
+ function taskDto({ storage, sessions }, task) {
52
+ if (!task)
53
+ return null;
54
+ const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
55
+ const sessionIds = storage.listWandTaskSessionIds(task.id);
56
+ return {
57
+ ...task,
58
+ sessionIds,
59
+ sessions: sessionIds.flatMap((id) => {
60
+ const session = sessions?.getLatest(id) ?? storage.getSession(id);
61
+ return session ? [taskSessionSummary(session)] : [];
62
+ }),
63
+ workspace: workspace
64
+ ? { id: workspace.id, name: workspace.name, cwd: workspace.cwd }
65
+ : null,
66
+ };
67
+ }
68
+ function taskSessionSummary(session) {
69
+ return {
70
+ id: session.id,
71
+ provider: session.provider ?? "",
72
+ sessionKind: session.sessionKind ?? "",
73
+ title: session.title || session.description || session.command,
74
+ status: session.status,
75
+ cwd: session.cwd,
76
+ model: session.selectedModel || session.structuredState?.model || "",
77
+ thinkingEffort: session.thinkingEffort || "off",
78
+ };
79
+ }
80
+ function sendTaskError(res, error, fallback) {
81
+ res.status(400).json({ error: getErrorMessage(error, fallback) });
82
+ }
83
+ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy) {
84
+ // 兼容旧的 positional 调用;新代码统一用对象形式的依赖。
85
+ const resolved = deps && typeof deps === "object" && "storage" in deps
86
+ ? deps
87
+ : { storage: deps, sessions: sessionsLegacy, requireAdmin: requireAdminLegacy };
88
+ const { storage, sessions, structured, config } = resolved;
89
+ const guard = resolved.requireAdmin ?? ((_req, _res, next) => next());
90
+ const dto = (task) => taskDto(resolved, task);
91
+ app.get("/api/wand-tasks", guard, (req, res) => {
92
+ const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined;
93
+ res.json(storage.listWandTasks(workspaceId).map((task) => dto(task)));
94
+ });
95
+ app.post("/api/wand-tasks", guard, (req, res) => {
96
+ try {
97
+ const body = bodyObject(req.body);
98
+ const title = text(body.title);
99
+ if (!title)
40
100
  throw new Error("任务标题不能为空。");
41
- patch.title = value;
101
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
102
+ if (workspaceId && !storage.getWorkspace(workspaceId))
103
+ throw new Error("项目不存在。");
104
+ const status = STATUSES.has(body.status) ? body.status : "todo";
105
+ const priority = PRIORITIES.has(body.priority) ? body.priority : "none";
106
+ const agent = body.agent === undefined ? null : parseTaskAgent(body.agent);
107
+ const task = storage.createWandTask({
108
+ workspaceId,
109
+ title,
110
+ description: text(body.description),
111
+ status,
112
+ priority,
113
+ labels: labelsFrom(body.labels),
114
+ dueDate: dateValue(body.dueDate) ?? null,
115
+ agent,
116
+ });
117
+ res.status(201).json(dto(task));
42
118
  }
43
- if (body.description !== undefined)
44
- patch.description = text(body.description);
45
- if (body.status !== undefined) {
46
- if (!STATUSES.has(body.status))
47
- throw new Error("任务状态无效。");
48
- patch.status = body.status;
119
+ catch (error) {
120
+ sendTaskError(res, error, "无法创建任务。");
49
121
  }
50
- if (body.priority !== undefined) {
51
- if (!PRIORITIES.has(body.priority))
52
- throw new Error("任务优先级无效。");
53
- patch.priority = body.priority;
122
+ });
123
+ app.patch("/api/wand-tasks/:id", guard, (req, res) => {
124
+ try {
125
+ const body = bodyObject(req.body);
126
+ const patch = {};
127
+ if (body.title !== undefined) {
128
+ const value = text(body.title);
129
+ if (!value)
130
+ throw new Error("任务标题不能为空。");
131
+ patch.title = value;
132
+ }
133
+ if (body.description !== undefined)
134
+ patch.description = text(body.description);
135
+ if (body.status !== undefined) {
136
+ if (!STATUSES.has(body.status))
137
+ throw new Error("任务状态无效。");
138
+ patch.status = body.status;
139
+ }
140
+ if (body.priority !== undefined) {
141
+ if (!PRIORITIES.has(body.priority))
142
+ throw new Error("任务优先级无效。");
143
+ patch.priority = body.priority;
144
+ }
145
+ if (body.labels !== undefined)
146
+ patch.labels = labelsFrom(body.labels);
147
+ if (body.dueDate !== undefined)
148
+ patch.dueDate = dateValue(body.dueDate) ?? null;
149
+ if (body.workspaceId !== undefined) {
150
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId);
151
+ if (workspaceId && !storage.getWorkspace(workspaceId))
152
+ throw new Error("项目不存在。");
153
+ patch.workspaceId = workspaceId || null;
154
+ }
155
+ if (body.agent !== undefined)
156
+ patch.agent = parseTaskAgent(body.agent);
157
+ if (body.sortOrder !== undefined && Number.isFinite(Number(body.sortOrder))) {
158
+ patch.sortOrder = Math.floor(Number(body.sortOrder));
159
+ }
160
+ const task = storage.updateWandTask(req.params.id, patch);
161
+ if (!task) {
162
+ res.status(404).json({ error: "未找到该任务。" });
163
+ return;
164
+ }
165
+ if (task.status === "done")
166
+ archiveBoardTask(storage, task.id);
167
+ res.json(dto(storage.getWandTask(task.id) ?? task));
54
168
  }
55
- if (body.labels !== undefined)
56
- patch.labels = Array.isArray(body.labels) ? body.labels.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20) : [];
57
- if (body.dueDate !== undefined)
58
- patch.dueDate = dateValue(body.dueDate) ?? null;
59
- if (body.workspaceId !== undefined) {
60
- const workspaceId = body.workspaceId === null ? null : text(body.workspaceId);
61
- if (workspaceId && !storage.getWorkspace(workspaceId))
62
- throw new Error("项目不存在。");
63
- patch.workspaceId = workspaceId || null;
169
+ catch (error) {
170
+ sendTaskError(res, error, "无法更新任务。");
171
+ }
172
+ });
173
+ app.delete("/api/wand-tasks/:id", guard, (req, res) => {
174
+ const archived = archiveBoardTask(storage, req.params.id);
175
+ if (!archived) {
176
+ res.status(404).json({ error: "未找到该任务。" });
177
+ return;
178
+ }
179
+ res.json({ ok: true, ...dto(archived) });
180
+ });
181
+ app.post("/api/wand-tasks/:id/sessions", guard, (req, res) => {
182
+ try {
183
+ const sessionId = text(bodyObject(req.body).sessionId);
184
+ if (!sessionId)
185
+ throw new Error("缺少 sessionId。");
186
+ if (sessions && !sessions.get(sessionId))
187
+ throw new Error("未找到该会话。");
188
+ storage.bindWandTaskSession(req.params.id, sessionId);
189
+ res.status(201).json(dto(storage.getWandTask(req.params.id)));
64
190
  }
65
- if (body.sortOrder !== undefined && Number.isFinite(Number(body.sortOrder)))
66
- patch.sortOrder = Math.floor(Number(body.sortOrder));
67
- const task = storage.updateWandTask(req.params.id, patch);
191
+ catch (error) {
192
+ sendTaskError(res, error, "无法绑定会话。");
193
+ }
194
+ });
195
+ app.delete("/api/wand-tasks/:id/sessions/:sessionId", guard, (req, res) => {
196
+ storage.unbindWandTaskSession(req.params.id, req.params.sessionId);
197
+ res.json({ ok: true });
198
+ });
199
+ /**
200
+ * 用任务上选定的 CLI 工具开一个结构化会话,把任务标题 + 描述作为首个 prompt
201
+ * 发过去,并把会话绑定回该任务。cwd 取任务所属项目目录;未指定项目时用全局默认目录。
202
+ */
203
+ app.post("/api/wand-tasks/:id/dispatch", guard, asyncRoute(async (req, res) => {
204
+ try {
205
+ const task = storage.getWandTask(req.params.id);
206
+ if (!task) {
207
+ res.status(404).json({ error: "未找到该任务。" });
208
+ return;
209
+ }
210
+ if (!structured || !config)
211
+ throw new Error("当前服务未启用结构化会话,无法派发 Agent。");
212
+ const body = bodyObject(req.body);
213
+ const agent = body.agent === undefined
214
+ ? task.agent ?? parseWandTaskAgent(JSON.stringify(body))
215
+ : parseTaskAgent(body.agent);
216
+ if (!agent)
217
+ throw new Error("请先为该任务选择 CLI 工具。");
218
+ const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
219
+ if (task.workspaceId && !workspace)
220
+ throw new Error("任务所属项目已被删除,请重新指定。");
221
+ const cwd = workspace?.cwd || config.defaultCwd;
222
+ const model = agent.model === "default" ? "" : agent.model;
223
+ const session = structured.createSession({
224
+ cwd,
225
+ mode: "agent",
226
+ provider: agent.provider,
227
+ model: model || getDefaultModelForProvider(config, agent.provider) || undefined,
228
+ thinkingEffort: agent.thinkingEffort,
229
+ worktreeEnabled: false,
230
+ sessionSource: "automation",
231
+ automationId: `wand-task:${task.id}`,
232
+ workspaceId: task.workspaceId ?? undefined,
233
+ });
234
+ storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
235
+ storage.bindWandTaskSession(task.id, session.id);
236
+ const prompt = [task.title, task.description.trim()].filter(Boolean).join("\n\n");
237
+ const completion = structured.sendMessage(session.id, prompt || task.title || "执行此任务");
238
+ completion.catch((error) => console.error(`[WandTask] Agent dispatch failed for ${task.id}:`, error));
239
+ res.status(202).json({
240
+ ok: true,
241
+ taskId: task.id,
242
+ session: {
243
+ id: session.id,
244
+ provider: session.provider,
245
+ model: session.selectedModel,
246
+ thinkingEffort: session.thinkingEffort,
247
+ cwd: session.cwd,
248
+ },
249
+ });
250
+ }
251
+ catch (error) {
252
+ sendTaskError(res, error, "无法派发 Agent。");
253
+ }
254
+ }));
255
+ app.get("/api/wand-tasks/:id", guard, asyncRoute(async (req, res) => {
256
+ const task = dto(storage.getWandTask(req.params.id));
68
257
  if (!task) {
69
258
  res.status(404).json({ error: "未找到该任务。" });
70
259
  return;
71
260
  }
72
- res.json(taskDto(storage, task));
73
- }
74
- catch (error) {
75
- res.status(400).json({ error: getErrorMessage(error, "无法更新任务。") });
76
- } });
77
- app.delete("/api/wand-tasks/:id", guard, (req, res) => { if (!storage.getWandTask(req.params.id)) {
78
- res.status(404).json({ error: "未找到该任务。" });
79
- return;
80
- } storage.deleteWandTask(req.params.id); res.json({ ok: true }); });
81
- app.post("/api/wand-tasks/:id/sessions", guard, (req, res) => { try {
82
- const body = bodyObject(req.body);
83
- const sessionId = text(body.sessionId);
84
- if (!sessionId)
85
- throw new Error("缺少 sessionId。");
86
- if (sessions && !sessions.get(sessionId))
87
- throw new Error("未找到该会话。");
88
- storage.bindWandTaskSession(req.params.id, sessionId);
89
- res.status(201).json(taskDto(storage, storage.getWandTask(req.params.id)));
90
- }
91
- catch (error) {
92
- res.status(400).json({ error: getErrorMessage(error, "无法绑定会话。") });
93
- } });
94
- app.delete("/api/wand-tasks/:id/sessions/:sessionId", guard, (req, res) => { storage.unbindWandTaskSession(req.params.id, req.params.sessionId); res.json({ ok: true }); });
95
- app.get("/api/wand-tasks/:id", guard, asyncRoute(async (req, res) => { const task = taskDto(storage, storage.getWandTask(req.params.id)); if (!task) {
96
- res.status(404).json({ error: "未找到该任务。" });
97
- return;
98
- } res.json(task); }));
261
+ res.json(task);
262
+ }));
99
263
  }
@@ -9,6 +9,7 @@ import { collectSessionTopicBlocklist } from "./session-topic.js";
9
9
  import { resolveSessionDisplayTitle } from "./session-transport.js";
10
10
  import { GLOBAL_WORKSPACE_ID } from "./types.js";
11
11
  import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, } from "./workspace-binding.js";
12
+ import { archiveBoardTaskForWorkspaceTask, ensureBoardTaskForWorkspaceTask } from "./wand-task-sync.js";
12
13
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
13
14
  function workspaceSessionTitle(session, names = {}) {
14
15
  return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
@@ -37,8 +38,39 @@ function workspaceSessionSummary(session, names = {}, registry) {
37
38
  startedAt: live.startedAt,
38
39
  };
39
40
  }
40
- function tasksRevision(groups) {
41
- return crypto.createHash("sha256").update(JSON.stringify(groups)).digest("base64url");
41
+ function cheapTasksRevision(storage, registry) {
42
+ const fingerprint = [
43
+ storage.tasksAggregateFingerprint(),
44
+ ...(registry?.listSlim() ?? []).map((session) => [
45
+ session.id,
46
+ session.status,
47
+ session.ptyBusy === true ? 1 : 0,
48
+ session.providerCliActive ? 1 : 0,
49
+ session.structuredState?.inFlight === true ? 1 : 0,
50
+ session.title ?? "",
51
+ session.workspaceId ?? "",
52
+ session.workspaceTaskId ?? "",
53
+ ]),
54
+ ];
55
+ return crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("base64url");
56
+ }
57
+ function compareCreatedDesc(left, right) {
58
+ const a = typeof left === "string" ? left.trim() : "";
59
+ const b = typeof right === "string" ? right.trim() : "";
60
+ if (a === b)
61
+ return 0;
62
+ if (!a)
63
+ return 1;
64
+ if (!b)
65
+ return -1;
66
+ return b.localeCompare(a);
67
+ }
68
+ function rememberCreatedAt(target, iso) {
69
+ const value = typeof iso === "string" ? iso.trim() : "";
70
+ if (!value)
71
+ return;
72
+ if (!target.createdAt || value < target.createdAt)
73
+ target.createdAt = value;
42
74
  }
43
75
  function parseDefaultProvider(value) {
44
76
  return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
@@ -82,7 +114,8 @@ function createTaskForWorkspace(storage, workspace, body) {
82
114
  }
83
115
  const baseRef = typeof body.baseRef === "string" && body.baseRef.trim() ? body.baseRef.trim() : undefined;
84
116
  const runCwd = mountedCwd ?? workspace.cwd;
85
- const wantWorktree = body.worktree === true || (body.worktree !== false && !isGlobalWorkspace(workspace));
117
+ const requireWorktree = body.worktree === true;
118
+ const wantWorktree = requireWorktree || (body.worktree !== false && !isGlobalWorkspace(workspace));
86
119
  let worktree = null;
87
120
  let worktreeError;
88
121
  if (wantWorktree) {
@@ -95,6 +128,9 @@ function createTaskForWorkspace(storage, workspace, body) {
95
128
  worktree = setup.worktree;
96
129
  }
97
130
  catch (error) {
131
+ if (requireWorktree) {
132
+ throw new Error(getErrorMessage(error, "无法创建隔离 worktree。"));
133
+ }
98
134
  worktreeError = getErrorMessage(error, "无法创建 worktree,将在项目目录直接运行。");
99
135
  }
100
136
  }
@@ -105,6 +141,7 @@ function createTaskForWorkspace(storage, workspace, body) {
105
141
  cwd: storedCwd,
106
142
  worktree,
107
143
  });
144
+ ensureBoardTaskForWorkspaceTask(storage, task, workspace);
108
145
  return {
109
146
  task,
110
147
  cwd: taskRuntimeCwd(task, workspace),
@@ -231,7 +268,7 @@ export function sanitizeTaskLayout(value) {
231
268
  * - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
232
269
  */
233
270
  export function registerWorkspaceRoutes(app, storage, sessions) {
234
- // 列出所有项目(按创建时间升序,保证项目层级稳定)
271
+ // 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
235
272
  app.get("/api/workspaces", (_req, res) => {
236
273
  backfillSessionWorkspaces(storage);
237
274
  const sessionCounts = storage.countSessionsByWorkspace();
@@ -374,6 +411,15 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
374
411
  app.get("/api/tasks", (req, res) => {
375
412
  // 查询参数:workspaceId 过滤单目录;limit 截断每目录任务数;
376
413
  // maxSessions 截断每任务内嵌会话数(大数据量时控制响应体积)。
414
+ const hasRevisionQuery = typeof req.query.revision === "string";
415
+ const requestedRevision = hasRevisionQuery ? req.query.revision : "";
416
+ if (hasRevisionQuery && requestedRevision) {
417
+ const cheap = cheapTasksRevision(storage, sessions);
418
+ if (requestedRevision === cheap) {
419
+ res.json({ unchanged: true, revision: cheap, groups: [] });
420
+ return;
421
+ }
422
+ }
377
423
  const workspaceFilter = typeof req.query.workspaceId === "string" ? req.query.workspaceId : "";
378
424
  const parseBoundedCount = (raw) => {
379
425
  const value = Number(raw);
@@ -394,6 +440,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
394
440
  workspaceId: workspace.id,
395
441
  workspaceName: global ? "全局" : workspace.name,
396
442
  workspaceCwd: workspace.cwd,
443
+ createdAt: workspace.createdAt,
397
444
  ...(global ? { global: true } : {}),
398
445
  tasks: [],
399
446
  standaloneSessions: [],
@@ -435,6 +482,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
435
482
  sessions,
436
483
  totalSessions: allSessions.length,
437
484
  });
485
+ if (target.synthetic)
486
+ rememberCreatedAt(target, task.createdAt);
438
487
  };
439
488
  for (const workspace of visibleWorkspaces) {
440
489
  const base = groups.get(workspace.id);
@@ -488,25 +537,33 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
488
537
  };
489
538
  groups.set(id, synthetic);
490
539
  }
540
+ rememberCreatedAt(synthetic, session.startedAt);
491
541
  group = synthetic;
492
542
  }
493
543
  // workspaceId 过滤时,不属于目标目录组的会话直接排除(含合成组)。
494
544
  if (workspaceFilter && group?.workspaceId !== workspaceFilter)
495
545
  continue;
496
- group?.standaloneSessions.push(summarize(session, {
546
+ if (!group)
547
+ continue;
548
+ group.standaloneSessions.push(summarize(session, {
497
549
  workspaceName: group.workspaceName,
498
550
  }));
551
+ if (group.synthetic)
552
+ rememberCreatedAt(group, session.startedAt);
499
553
  }
500
554
  const payload = [...groups.values()]
501
555
  .filter((group) => !group.global || group.tasks.length > 0 || group.standaloneSessions.length > 0)
502
- .sort((left, right) => Number(Boolean(right.global)) - Number(Boolean(left.global)));
503
- const revision = tasksRevision(payload);
504
- if (typeof req.query.revision === "string") {
505
- if (req.query.revision === revision) {
506
- res.json({ unchanged: true, revision, groups: [] });
507
- return;
508
- }
509
- res.json({ unchanged: false, revision, groups: payload });
556
+ .sort((left, right) => {
557
+ const global = Number(Boolean(right.global)) - Number(Boolean(left.global));
558
+ if (global)
559
+ return global;
560
+ const created = compareCreatedDesc(left.createdAt, right.createdAt);
561
+ if (created)
562
+ return created;
563
+ return String(left.workspaceId).localeCompare(String(right.workspaceId));
564
+ });
565
+ if (hasRevisionQuery) {
566
+ res.json({ unchanged: false, revision: cheapTasksRevision(storage, sessions), groups: payload });
510
567
  return;
511
568
  }
512
569
  res.json(payload);
@@ -597,7 +654,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
597
654
  });
598
655
  }));
599
656
  // 新建任务:命名 + 可选独立 worktree(默认尝试创建,非 git 仓库时退化为直接用项目目录;
600
- // 显式传 worktree:false 时跳过隔离,会话直接跑在项目目录)。
657
+ // 显式 worktree:false 跳过隔离;显式 worktree:true 失败则 400,不静默降级)。
601
658
  app.post("/api/workspaces/:id/tasks", asyncRoute(async (req, res) => {
602
659
  const workspace = storage.getWorkspace(req.params.id);
603
660
  if (!workspace) {
@@ -652,6 +709,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
652
709
  if (body.status === "active" || body.status === "done")
653
710
  patch.status = body.status;
654
711
  storage.updateWorkspaceTask(existing.id, patch);
712
+ if (patch.status === "done")
713
+ archiveBoardTaskForWorkspaceTask(storage, existing.id);
655
714
  res.json(storage.getWorkspaceTask(existing.id));
656
715
  });
657
716
  // 删除任务;cascade=true 连带删会话,否则仅解绑;尽力清理 worktree
@@ -672,6 +731,10 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
672
731
  // 尽力清理 worktree 与分支;失败不阻塞删除任务行。
673
732
  if (cascade)
674
733
  cleanupWorktreeSync(existing.worktree);
734
+ archiveBoardTaskForWorkspaceTask(storage, existing.id);
735
+ const board = storage.getWandTaskByWorkspaceTaskId(existing.id);
736
+ if (board)
737
+ storage.updateWandTask(board.id, { workspaceTaskId: null });
675
738
  storage.deleteWorkspaceTask(existing.id, { cascade });
676
739
  res.json({ ok: true });
677
740
  });
@@ -684,7 +747,18 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
684
747
  }
685
748
  const body = req.body;
686
749
  const layout = sanitizeTaskLayout(body === null || typeof body !== "object" ? undefined : body.layout);
687
- storage.saveWorkspaceTaskLayout(existing.id, layout);
688
- res.json({ ok: true, layout });
750
+ const expected = typeof body.layoutRevision === "number" && Number.isFinite(body.layoutRevision)
751
+ ? Math.floor(body.layoutRevision)
752
+ : undefined;
753
+ const saved = storage.saveWorkspaceTaskLayout(existing.id, layout, expected);
754
+ if (!saved.ok) {
755
+ res.status(409).json({
756
+ error: "布局已被其他端更新。",
757
+ layout: saved.layout,
758
+ layoutRevision: saved.layoutRevision,
759
+ });
760
+ return;
761
+ }
762
+ res.json({ ok: true, layout, layoutRevision: saved.layoutRevision });
689
763
  });
690
764
  }