@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.
- package/dist/build-info.json +3 -3
- package/dist/ensure-node-pty-helper.d.ts +0 -1
- package/dist/ensure-node-pty-helper.js +0 -5
- package/dist/git-quick-commit.js +18 -19
- package/dist/github-connector.d.ts +11 -5
- package/dist/github-connector.js +105 -33
- package/dist/missions.d.ts +1 -1
- package/dist/missions.js +17 -4
- package/dist/server-file-routes.js +43 -2
- package/dist/server-github-routes.js +50 -53
- package/dist/server-mission-routes.js +11 -10
- package/dist/server-task-routes.d.ts +14 -2
- package/dist/server-task-routes.js +248 -84
- package/dist/server-workspace-routes.js +90 -16
- package/dist/server.js +2 -20
- package/dist/storage.d.ts +23 -6
- package/dist/storage.js +130 -53
- package/dist/structured-exec-host.d.ts +13 -2
- package/dist/structured-exec-host.js +71 -36
- package/dist/structured-exec-pump.d.ts +1 -1
- package/dist/structured-exec-pump.js +14 -8
- package/dist/task-types.d.ts +15 -0
- package/dist/terminal-daemon-client.d.ts +2 -0
- package/dist/terminal-daemon-client.js +7 -1
- package/dist/terminal-daemon-server.js +17 -18
- package/dist/tui/ipc-server.js +3 -1
- package/dist/types.d.ts +1 -0
- package/dist/wand-task-sync.d.ts +9 -0
- package/dist/wand-task-sync.js +50 -0
- package/dist/web-ui/content/scripts.js +85 -85
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.js +5 -1
- package/package.json +2 -3
- package/dist/taskboard-bridge.d.ts +0 -11
- package/dist/taskboard-bridge.js +0 -371
- package/vendor/codex-taskboard/LICENSE +0 -201
- package/vendor/codex-taskboard/dist/web/assets/AiChat-BNn4VJ5a.js +0 -12
- package/vendor/codex-taskboard/dist/web/assets/GanttView-D8wnfGrG.css +0 -1
- package/vendor/codex-taskboard/dist/web/assets/GanttView-rGWhuEX9.js +0 -39
- package/vendor/codex-taskboard/dist/web/assets/abnfDiagram-N423BO3Z-DbFG6lbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/arc-BjSQqbzd.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/architecture-TIHT7OUA-C9-t8p4o.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/architectureDiagram-T3A2C74G-Db4kJZwO.js +0 -36
- package/vendor/codex-taskboard/dist/web/assets/array-BifhSqXX.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/blockDiagram-VBNYF7ZC-C-BywuWV.js +0 -132
- package/vendor/codex-taskboard/dist/web/assets/c4Diagram-5PPSVZJV-Dq-80rjP.js +0 -10
- package/vendor/codex-taskboard/dist/web/assets/channel-C7mnz8Xc.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-2GRJ4B5K-B2G57us9.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-3NCLNEKW-DVGIRib_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-4I5QYGJK-kNoGFqr0.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-5RXB4S5H-4iv6-xM6.js +0 -231
- package/vendor/codex-taskboard/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +0 -15
- package/vendor/codex-taskboard/dist/web/assets/chunk-6Q2QTUOP-1J34vxHr.js +0 -88
- package/vendor/codex-taskboard/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-7Z6QIM7H-CDQpQlxX.js +0 -10
- package/vendor/codex-taskboard/dist/web/assets/chunk-GF5L2VYU-Dg92oHHl.js +0 -206
- package/vendor/codex-taskboard/dist/web/assets/chunk-I66GZJ75-Bk8_gFs2.js +0 -125
- package/vendor/codex-taskboard/dist/web/assets/chunk-J7OUQ5F2-CJRcenlr.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/chunk-JQJVKLGR-BLBV_Sus.js +0 -156
- package/vendor/codex-taskboard/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-KBJHAD2P-Da9Yjz0p.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-KEIR6QF5-Dj-OpFgW.js +0 -161
- package/vendor/codex-taskboard/dist/web/assets/chunk-NSK5VX7P-Cb56tnGK.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/chunk-QR6OTTB3-BwoszTxn.js +0 -62
- package/vendor/codex-taskboard/dist/web/assets/chunk-RYQCIY6F-D_L2RdcQ.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-UBXNYLIW-YX5mN8cl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-W5SLKNZC-DlOLhhDO.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-WRU74C26-B7SBmP0K.js +0 -70
- package/vendor/codex-taskboard/dist/web/assets/chunk-XXDRQBXY-BuE3VzE_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +0 -32
- package/vendor/codex-taskboard/dist/web/assets/classDiagram-JCYQIIEL-KIdCVvbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/classDiagram-v2-OCEON4UE-KIdCVvbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cose-bilkent-JH36ORCC-BzQvy5TC.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cynefin-VYW2F7L2-D1_5O1XR.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cynefinDiagram-MW4NZA55-DOW_U5QN.js +0 -62
- package/vendor/codex-taskboard/dist/web/assets/cytoscape.esm-DnGd5Fuo.js +0 -321
- package/vendor/codex-taskboard/dist/web/assets/dagre-VZM6K2ZE-0Dntc3NT.js +0 -4
- package/vendor/codex-taskboard/dist/web/assets/dagre-dpRSp0QF.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/defaultLocale-C8Fc0cco.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/diagram-7IWD3JNH-D7GleYAM.js +0 -30
- package/vendor/codex-taskboard/dist/web/assets/diagram-B4RE2ZJO-DCXTe6q5.js +0 -3
- package/vendor/codex-taskboard/dist/web/assets/diagram-LBJQPF4R-ZFJyXEXl.js +0 -24
- package/vendor/codex-taskboard/dist/web/assets/diagram-Q27KOJAE-DUGCuGd9.js +0 -24
- package/vendor/codex-taskboard/dist/web/assets/diagram-UB23O5K3-DRmp4Nng.js +0 -41
- package/vendor/codex-taskboard/dist/web/assets/dist-D9sYb5Oa.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ebnfDiagram-BXEA7PRR-CkKoaf7T.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/erDiagram-JOGREHBK-nBX5z4R4.js +0 -85
- package/vendor/codex-taskboard/dist/web/assets/eventmodeling-45OFAUF4-pexmK65N.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/flowDiagram-UKHOOZJN-aroohRbL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ganttDiagram-PKOTCBZU-BOJWUTXa.js +0 -292
- package/vendor/codex-taskboard/dist/web/assets/gitGraph-TEB2WS4Q-DxnuhSZU.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/gitGraphDiagram-DS77QQ5N-Cte975TC.js +0 -106
- package/vendor/codex-taskboard/dist/web/assets/graphlib-DS17s2tU.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/index-Docmx4uQ.css +0 -1
- package/vendor/codex-taskboard/dist/web/assets/index-Du07lEGz.js +0 -121
- package/vendor/codex-taskboard/dist/web/assets/info-DKCQHKI2-DOTjgYxk.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/infoDiagram-6WML65LV-SpyV9UJd.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/init-D6jRqBbL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ishikawaDiagram-WSZJBQD7-T0PZwoUZ.js +0 -70
- package/vendor/codex-taskboard/dist/web/assets/journeyDiagram-NVQOT4AX-B88b8UgX.js +0 -139
- package/vendor/codex-taskboard/dist/web/assets/kanban-definition-27J2QSJJ-CIlfNnl_.js +0 -89
- package/vendor/codex-taskboard/dist/web/assets/katex-B7rAX3Vi.js +0 -257
- package/vendor/codex-taskboard/dist/web/assets/line-fWrwPZhB.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/linear-B7l8qgEw.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/map-BaFkSB1l.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/mermaid-parser.core-C5gls2nC.js +0 -7
- package/vendor/codex-taskboard/dist/web/assets/mermaid.core-POz2Xcvr.js +0 -11
- package/vendor/codex-taskboard/dist/web/assets/mindmap-definition-FAOFIHXS-BM0-egZ5.js +0 -96
- package/vendor/codex-taskboard/dist/web/assets/ordinal-hYBb2elL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/packet-7NZHBO7P-DQZzyVos.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/path-BWPyau1x.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pegDiagram-VL7TDLO6-B8aEwl-7.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pie-RZYD4A2V-DRUflilR.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pieDiagram-7S7Q4E2Y-0cDDxlua.js +0 -39
- package/vendor/codex-taskboard/dist/web/assets/purify.es-Bwt4ulIK.js +0 -3
- package/vendor/codex-taskboard/dist/web/assets/quadrantDiagram-CIZ2JOQS-CBgb0YZ0.js +0 -7
- package/vendor/codex-taskboard/dist/web/assets/radar-I7S5WNFK-tRocoKxq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-3IZDKUUU-DaS4ZlX_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-abnf-AHOZXSZD-BN1T5dBT.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-ebnf-EBAXGLYW-Cuf8qyKs.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-peg-LSFZ7HO6-B511_EYq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroadDiagram-AXF67PYL-QtPo-JZb.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/requirementDiagram-LRYGKXZP-CuBIF85r.js +0 -84
- package/vendor/codex-taskboard/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/rough.esm-CSKSodPl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/sankeyDiagram-W5VNT64P-CRA-b_za.js +0 -40
- package/vendor/codex-taskboard/dist/web/assets/sequenceDiagram-SI44F4Z6-CoddVWwY.js +0 -162
- package/vendor/codex-taskboard/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/src-_wZywoZs.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/stateDiagram-OKZ733FA-D8DMM2ln.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/stateDiagram-v2-UEYNNEHI-CVp0eYAS.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/swimlanes-SLNWSIFB-C1fC7EFu.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CPd58Xso.js +0 -8
- package/vendor/codex-taskboard/dist/web/assets/timeline-definition-Z64GVDOM-BUBv8aKP.js +0 -120
- package/vendor/codex-taskboard/dist/web/assets/treeView-QDETBFTQ-C-Bve2kY.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/treemap-6X3UGDF4-BcphHDdV.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/vennDiagram-T6HMQDX7-0wQnwAyA.js +0 -34
- package/vendor/codex-taskboard/dist/web/assets/wardley-OPB4EBWU-CJZ4BevB.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/wardleyDiagram-T6FBY63Y-U9nwPrkN.js +0 -78
- package/vendor/codex-taskboard/dist/web/assets/xychartDiagram-ELKLHX3M-CyCqnzgE.js +0 -7
- package/vendor/codex-taskboard/dist/web/codex-agent-logo.png +0 -0
- package/vendor/codex-taskboard/dist/web/codex-app-icon.png +0 -0
- package/vendor/codex-taskboard/dist/web/favicon.svg +0 -7
- package/vendor/codex-taskboard/dist/web/index.html +0 -20
- package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +0 -1031
- package/vendor/codex-taskboard/server/ai-chat-process.mjs +0 -462
- package/vendor/codex-taskboard/server/ai-chat.mjs +0 -1237
- package/vendor/codex-taskboard/server/ai-turn-owner.mjs +0 -39
- package/vendor/codex-taskboard/server/app.mjs +0 -3211
- package/vendor/codex-taskboard/server/cloud-config.mjs +0 -188
- package/vendor/codex-taskboard/server/cloud-proxy.mjs +0 -310
- package/vendor/codex-taskboard/server/codex-app-server.mjs +0 -376
- package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +0 -62
- package/vendor/codex-taskboard/server/database.mjs +0 -2845
- package/vendor/codex-taskboard/server/index.mjs +0 -41
- package/vendor/codex-taskboard/server/jira-config.mjs +0 -181
- package/vendor/codex-taskboard/server/jira-integration.mjs +0 -487
- package/vendor/codex-taskboard/server/project-summary.mjs +0 -159
- package/vendor/codex-taskboard/shared/api-fields.mjs +0 -163
- package/vendor/codex-taskboard/shared/codex-environment.mjs +0 -5
- package/vendor/codex-taskboard/shared/codex-executable.mjs +0 -77
- package/vendor/codex-taskboard/shared/codex-permissions.mjs +0 -9
- package/vendor/codex-taskboard/shared/composer-reference.d.mts +0 -19
- package/vendor/codex-taskboard/shared/composer-reference.mjs +0 -90
- package/vendor/codex-taskboard/shared/domain.mjs +0 -34
- package/vendor/codex-taskboard/shared/executable-command.mjs +0 -13
- package/vendor/codex-taskboard/shared/process-tree.mjs +0 -27
- package/vendor/codex-taskboard/shared/task-input.mjs +0 -308
- package/vendor/codex-taskboard/shared/task-records.mjs +0 -199
- package/vendor/codex-taskboard/shared/task-relations.mjs +0 -55
- package/vendor/codex-taskboard/shared/taskboard-automation.mjs +0 -277
- package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +0 -45
- package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +0 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
|
2
|
+
import { type WandStorage } from "./storage.js";
|
|
3
3
|
import type { SessionRegistry } from "./session-registry.js";
|
|
4
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
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
|
|
41
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
688
|
-
|
|
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
|
}
|