@co0ontty/wand 4.76.0 → 4.77.0-beta.g6c4df97
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/agent-dispatch.d.ts +46 -0
- package/dist/agent-dispatch.js +84 -0
- package/dist/ai-team-availability.d.ts +69 -0
- package/dist/ai-team-availability.js +100 -0
- package/dist/ai-team-prompts.d.ts +113 -0
- package/dist/ai-team-prompts.js +435 -0
- package/dist/ai-team-runner.d.ts +241 -0
- package/dist/ai-team-runner.js +1370 -0
- package/dist/ai-team-types.d.ts +147 -0
- package/dist/ai-team-types.js +32 -0
- package/dist/attention.d.ts +40 -0
- package/dist/attention.js +86 -0
- package/dist/build-info.json +4 -4
- package/dist/claude-sdk-runner.d.ts +5 -0
- package/dist/claude-sdk-runner.js +2 -1
- package/dist/commit-task-archive.d.ts +3 -0
- package/dist/commit-task-archive.js +43 -0
- package/dist/config.js +0 -2
- package/dist/git-quick-commit.d.ts +2 -1
- package/dist/git-quick-commit.js +72 -60
- package/dist/message-truncator.d.ts +41 -5
- package/dist/message-truncator.js +255 -38
- package/dist/missions.d.ts +4 -2
- package/dist/missions.js +2 -2
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +17 -8
- package/dist/prompt-optimizer.js +13 -13
- package/dist/server-ai-team-routes.d.ts +11 -0
- package/dist/server-ai-team-routes.js +283 -0
- package/dist/server-attention-routes.d.ts +9 -0
- package/dist/server-attention-routes.js +26 -0
- package/dist/server-file-routes.js +1 -1
- package/dist/server-session-routes.js +41 -5
- package/dist/server-settings-routes.js +1 -1
- package/dist/server-task-routes.d.ts +2 -0
- package/dist/server-task-routes.js +6 -40
- package/dist/server-workspace-routes.js +13 -5
- package/dist/server.js +43 -0
- package/dist/session-topic.js +13 -13
- package/dist/session-transport.d.ts +3 -0
- package/dist/session-transport.js +1 -0
- package/dist/storage.d.ts +54 -1
- package/dist/storage.js +413 -2
- package/dist/structured-claude-adapter.d.ts +5 -0
- package/dist/structured-claude-adapter.js +13 -2
- package/dist/structured-claude-protocol.d.ts +2 -0
- package/dist/structured-claude-protocol.js +4 -0
- package/dist/structured-codex-adapter.js +2 -2
- package/dist/structured-grok-adapter.js +2 -1
- package/dist/structured-opencode-adapter.js +2 -2
- package/dist/structured-pi-adapter.js +2 -1
- package/dist/structured-provider-common.d.ts +17 -0
- package/dist/structured-provider-common.js +52 -0
- package/dist/structured-qoder-adapter.js +2 -1
- package/dist/structured-session-manager.d.ts +18 -1
- package/dist/structured-session-manager.js +67 -10
- package/dist/system-ai.d.ts +13 -2
- package/dist/system-ai.js +9 -5
- package/dist/task-title.js +10 -10
- package/dist/types.d.ts +25 -2
- package/dist/wand-task-sync.d.ts +1 -0
- package/dist/wand-task-sync.js +1 -0
- package/dist/web-ui/content/ai-teams.js +12 -0
- package/dist/web-ui/content/scripts.js +86 -5301
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -0
- package/dist/web-ui/embedded-assets.js +3 -2
- package/dist/web-ui/scripts.d.ts +8 -0
- package/dist/web-ui/scripts.js +28 -2
- package/dist/ws-broadcast.js +52 -19
- package/package.json +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AiTeamConflictError } from "./ai-team-runner.js";
|
|
3
|
+
import { AI_TEAM_AVATAR_MAX_CHARS, AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_MAX_CANDIDATES, AI_TEAM_MAX_MEMBERS, AI_TEAM_MAX_STEPS, AI_TEAM_MIN_MEMBERS, AI_TEAM_MIN_STEPS, agentKey, isTeamMemberRole, memberAgents, } from "./ai-team-types.js";
|
|
4
|
+
import { asyncRoute } from "./express-async.js";
|
|
5
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
6
|
+
import { defaultMilestoneIdForWrite, scopedMilestoneId } from "./milestone-scope.js";
|
|
7
|
+
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
8
|
+
import { parseTaskAgent } from "./server-task-routes.js";
|
|
9
|
+
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
10
|
+
import { ensureWorkspaceTaskForBoardTask } from "./wand-task-sync.js";
|
|
11
|
+
const NOT_FOUND = /不存在/;
|
|
12
|
+
/** 团队页「直接开工」自动建卡的标记(§4.2 / Q3):看板靠它区分团队卡与手写卡。 */
|
|
13
|
+
export const TEAM_DIRECT_LABEL = "team_direct";
|
|
14
|
+
/** note 上限,与团队协作指令同口径(§4.2)。 */
|
|
15
|
+
const TEAM_DIRECT_NOTE_MAX = 4000;
|
|
16
|
+
/** `GET /api/ai-teams/:id` 附带多少个运行摘要(§4.1 B14):面板只要最近这一段。 */
|
|
17
|
+
const TEAM_DIRECT_RUNS_LIMIT = 50;
|
|
18
|
+
function sendTeamError(res, error) {
|
|
19
|
+
const message = getErrorMessage(error, "团队操作失败。");
|
|
20
|
+
const status = error instanceof AiTeamConflictError ? 409 : NOT_FOUND.test(message) ? 404 : 400;
|
|
21
|
+
sendRouteError(res, error, "团队操作失败。", status);
|
|
22
|
+
}
|
|
23
|
+
function boundedText(value, label, min, max) {
|
|
24
|
+
const result = text(value);
|
|
25
|
+
if (result.length < min)
|
|
26
|
+
throw new Error(`${label}不能为空。`);
|
|
27
|
+
if (result.length > max)
|
|
28
|
+
throw new Error(`${label}不能超过 ${max} 个字符。`);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function parseAvatar(value, name) {
|
|
32
|
+
const avatar = text(value);
|
|
33
|
+
if (!avatar || /^cat:\d{1,2}$/.test(avatar))
|
|
34
|
+
return avatar;
|
|
35
|
+
if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar))
|
|
36
|
+
throw new Error(`成员「${name}」的头像格式无效。`);
|
|
37
|
+
if (avatar.length > AI_TEAM_AVATAR_MAX_CHARS)
|
|
38
|
+
throw new Error(`成员「${name}」的头像太大。`);
|
|
39
|
+
return avatar;
|
|
40
|
+
}
|
|
41
|
+
function parseMembers(value) {
|
|
42
|
+
if (!Array.isArray(value))
|
|
43
|
+
throw new Error("成员列表必须是数组。");
|
|
44
|
+
if (value.length < AI_TEAM_MIN_MEMBERS || value.length > AI_TEAM_MAX_MEMBERS) {
|
|
45
|
+
throw new Error(`团队需要 ${AI_TEAM_MIN_MEMBERS}–${AI_TEAM_MAX_MEMBERS} 名成员。`);
|
|
46
|
+
}
|
|
47
|
+
const names = new Set();
|
|
48
|
+
const ids = new Set();
|
|
49
|
+
const members = value.map((raw, index) => {
|
|
50
|
+
const body = bodyObject(raw);
|
|
51
|
+
const name = boundedText(body.name, `第 ${index + 1} 位成员的名字`, 1, 40);
|
|
52
|
+
const key = name.toLowerCase();
|
|
53
|
+
if (names.has(key))
|
|
54
|
+
throw new Error(`成员名字「${name}」重复。`);
|
|
55
|
+
names.add(key);
|
|
56
|
+
const rawId = text(body.id);
|
|
57
|
+
const id = /^m_[A-Za-z0-9]{1,32}$/.test(rawId) && !ids.has(rawId)
|
|
58
|
+
? rawId
|
|
59
|
+
: `m_${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
60
|
+
ids.add(id);
|
|
61
|
+
const duty = text(body.duty);
|
|
62
|
+
if (duty.length > 2000)
|
|
63
|
+
throw new Error(`成员「${name}」的职责不能超过 2000 个字符。`);
|
|
64
|
+
let agents;
|
|
65
|
+
try {
|
|
66
|
+
if (Array.isArray(body.agents)) {
|
|
67
|
+
if (body.agents.length === 0)
|
|
68
|
+
throw new Error("至少需要一个执行候选。");
|
|
69
|
+
if (body.agents.length > AI_TEAM_MAX_CANDIDATES)
|
|
70
|
+
throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
|
|
71
|
+
agents = body.agents.map((rawAgent) => {
|
|
72
|
+
const parsed = parseTaskAgent(rawAgent ?? {});
|
|
73
|
+
if (!parsed)
|
|
74
|
+
throw new Error("请选择有效的 CLI 工具。");
|
|
75
|
+
return parsed;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (body.agents !== undefined && body.agents !== null) {
|
|
79
|
+
throw new Error("候选执行配置必须是数组。");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const agent = parseTaskAgent(body.agent ?? {});
|
|
83
|
+
if (!agent)
|
|
84
|
+
throw new Error("没有选择 CLI 工具。");
|
|
85
|
+
agents = [agent];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new Error(`成员「${name}」:${getErrorMessage(error)}`);
|
|
90
|
+
}
|
|
91
|
+
const seenKeys = new Set(agents.map(agentKey));
|
|
92
|
+
if (seenKeys.size !== agents.length)
|
|
93
|
+
throw new Error(`成员「${name}」的执行候选重复。`);
|
|
94
|
+
const member = {
|
|
95
|
+
id, name, duty, agents,
|
|
96
|
+
// 服务端强制兼容字段 = 首选候选,不信任客户端传来的 agent。
|
|
97
|
+
agent: agents[0],
|
|
98
|
+
isLeader: body.isLeader === true,
|
|
99
|
+
avatar: parseAvatar(body.avatar, name),
|
|
100
|
+
};
|
|
101
|
+
if (isTeamMemberRole(body.role))
|
|
102
|
+
member.role = body.role;
|
|
103
|
+
return member;
|
|
104
|
+
});
|
|
105
|
+
if (members.filter((member) => member.isLeader).length !== 1)
|
|
106
|
+
throw new Error("团队需要恰好一位负责人。");
|
|
107
|
+
return members;
|
|
108
|
+
}
|
|
109
|
+
export function parseAiTeamInput(value, existing, now) {
|
|
110
|
+
const body = bodyObject(value);
|
|
111
|
+
const maxSteps = body.maxSteps === undefined ? existing?.maxSteps ?? AI_TEAM_DEFAULT_MAX_STEPS : Number(body.maxSteps);
|
|
112
|
+
if (!Number.isInteger(maxSteps) || maxSteps < AI_TEAM_MIN_STEPS || maxSteps > AI_TEAM_MAX_STEPS) {
|
|
113
|
+
throw new Error(`步数上限需要是 ${AI_TEAM_MIN_STEPS}–${AI_TEAM_MAX_STEPS} 之间的整数。`);
|
|
114
|
+
}
|
|
115
|
+
const description = text(body.description);
|
|
116
|
+
if (description.length > 500)
|
|
117
|
+
throw new Error("说明不能超过 500 个字符。");
|
|
118
|
+
const instructions = body.instructions === undefined ? existing?.instructions ?? "" : text(body.instructions);
|
|
119
|
+
if (instructions.length > 4000)
|
|
120
|
+
throw new Error("协作指令不能超过 4000 个字符。");
|
|
121
|
+
return {
|
|
122
|
+
id: existing?.id ?? `team_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
|
|
123
|
+
name: boundedText(body.name, "团队名", 1, 60),
|
|
124
|
+
description,
|
|
125
|
+
instructions,
|
|
126
|
+
members: parseMembers(body.members),
|
|
127
|
+
requirePlanApproval: typeof body.requirePlanApproval === "boolean"
|
|
128
|
+
? body.requirePlanApproval
|
|
129
|
+
: existing?.requirePlanApproval ?? true,
|
|
130
|
+
maxSteps,
|
|
131
|
+
createdAt: existing?.createdAt ?? now,
|
|
132
|
+
updatedAt: now,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function registerAiTeamRoutes(app, deps) {
|
|
136
|
+
const { storage, runner } = deps;
|
|
137
|
+
app.get("/api/ai-teams", (_req, res) => {
|
|
138
|
+
res.json(storage.listAiTeams());
|
|
139
|
+
});
|
|
140
|
+
app.post("/api/ai-teams", (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const team = parseAiTeamInput(req.body, null, new Date().toISOString());
|
|
143
|
+
storage.saveAiTeam(team);
|
|
144
|
+
res.status(201).json(team);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
sendTeamError(res, error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
app.put("/api/ai-teams/:id", (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const existing = storage.getAiTeam(req.params.id);
|
|
153
|
+
if (!existing)
|
|
154
|
+
throw new Error("团队不存在。");
|
|
155
|
+
const team = parseAiTeamInput(req.body, existing, new Date().toISOString());
|
|
156
|
+
storage.saveAiTeam(team);
|
|
157
|
+
res.json(team);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
sendTeamError(res, error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
app.delete("/api/ai-teams/:id", (req, res) => {
|
|
164
|
+
storage.deleteAiTeam(req.params.id);
|
|
165
|
+
res.json({ ok: true });
|
|
166
|
+
});
|
|
167
|
+
/** 团队详情:团队对象 + 该团队的运行摘要(修正 B14,Android AiTeamDetailScreen 与 Web 面板共用)。 */
|
|
168
|
+
app.get("/api/ai-teams/:id", (req, res) => {
|
|
169
|
+
try {
|
|
170
|
+
const team = storage.getAiTeam(req.params.id);
|
|
171
|
+
if (!team)
|
|
172
|
+
throw new Error("团队不存在。");
|
|
173
|
+
res.json({ team, runs: runner.listRuns({ teamId: team.id, limit: TEAM_DIRECT_RUNS_LIMIT }) });
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
sendTeamError(res, error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* 直接开工(§4.2,需求 D):服务端自己建一张 `team_direct` 卡再起 run,**没有 cwd 入参**。
|
|
181
|
+
* 工作目录只能来自已校验的非 global workspace —— 缺省 workspaceId 的卡不会在建卡时报错,
|
|
182
|
+
* 而是在派发时静默落到 `<configDir>/scratch`(N2),所以整条校验链必须卡在入口。
|
|
183
|
+
*/
|
|
184
|
+
app.post("/api/ai-teams/:id/runs", asyncRoute(async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const body = bodyObject(req.body ?? {});
|
|
187
|
+
const workspaceId = text(body.workspaceId);
|
|
188
|
+
if (!workspaceId)
|
|
189
|
+
throw new Error("请选择工作项目。");
|
|
190
|
+
const workspace = storage.getWorkspace(workspaceId);
|
|
191
|
+
if (!workspace)
|
|
192
|
+
throw new Error("请选择工作项目。");
|
|
193
|
+
if (workspace.kind === "global") {
|
|
194
|
+
throw new Error("AI 团队不能在全局暂存工作区运行,请先选择或创建一个项目。");
|
|
195
|
+
}
|
|
196
|
+
if (!text(workspace.cwd))
|
|
197
|
+
throw new Error("这个项目还没有工作目录,请先选择一个项目。");
|
|
198
|
+
const note = boundedText(body.note, "开工说明", 1, TEAM_DIRECT_NOTE_MAX);
|
|
199
|
+
const team = storage.getAiTeam(req.params.id);
|
|
200
|
+
if (!team)
|
|
201
|
+
throw new Error("团队不存在。");
|
|
202
|
+
// 卡片上的执行配置取团队首选(负责人的第一个候选),看板与快捷提交都按它显示。
|
|
203
|
+
const preferredMember = team.members.find((member) => member.isLeader) ?? team.members[0];
|
|
204
|
+
const agent = preferredMember ? memberAgents(preferredMember)[0] ?? null : null;
|
|
205
|
+
const milestoneId = defaultMilestoneIdForWrite(storage);
|
|
206
|
+
const task = storage.transaction(() => {
|
|
207
|
+
const card = storage.createWandTask({
|
|
208
|
+
workspaceId: workspace.id,
|
|
209
|
+
title: provisionalTaskTitleFromDescription(note) || team.name,
|
|
210
|
+
description: note,
|
|
211
|
+
// 团队开工即进行中(§4.2 的 "processing" 对应看板列 doing;runner.start 不再回退它)。
|
|
212
|
+
status: "doing",
|
|
213
|
+
labels: [TEAM_DIRECT_LABEL],
|
|
214
|
+
milestoneId: scopedMilestoneId(storage, milestoneId, workspace.id) ?? milestoneId,
|
|
215
|
+
agent,
|
|
216
|
+
});
|
|
217
|
+
ensureWorkspaceTaskForBoardTask(storage, card);
|
|
218
|
+
return storage.getWandTask(card.id);
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
const detail = await runner.start({ teamId: team.id, taskId: task.id, note });
|
|
222
|
+
res.status(202).json({ ...detail, taskId: task.id });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// 不留空卡:起 run 失败就删掉刚建的卡;回滚本身失败只记日志,错误原样映射出去。
|
|
226
|
+
try {
|
|
227
|
+
storage.deleteWandTask(task.id);
|
|
228
|
+
}
|
|
229
|
+
catch (rollbackError) {
|
|
230
|
+
console.error(`[AiTeam] rollback direct task ${task.id} failed:`, getErrorMessage(rollbackError));
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
sendTeamError(res, error);
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
app.get("/api/ai-team-runs", (req, res) => {
|
|
240
|
+
const teamId = typeof req.query.teamId === "string" ? req.query.teamId : undefined;
|
|
241
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
|
242
|
+
res.json(runner.listRuns({ teamId, activeOnly: req.query.active === "1", limit }));
|
|
243
|
+
});
|
|
244
|
+
app.get("/api/wand-tasks/:id/team-runs", (req, res) => {
|
|
245
|
+
res.json(runner.listForTask(req.params.id));
|
|
246
|
+
});
|
|
247
|
+
app.post("/api/wand-tasks/:id/team-runs", asyncRoute(async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const body = bodyObject(req.body);
|
|
250
|
+
const teamId = text(body.teamId);
|
|
251
|
+
if (!teamId)
|
|
252
|
+
throw new Error("请选择团队。");
|
|
253
|
+
res.status(202).json(await runner.start({ teamId, taskId: req.params.id, note: text(body.note) }));
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
sendTeamError(res, error);
|
|
257
|
+
}
|
|
258
|
+
}));
|
|
259
|
+
app.get("/api/ai-team-runs/:id", (req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
res.json(runner.detail(req.params.id));
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
sendTeamError(res, error);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const action = (path, handler) => {
|
|
268
|
+
app.post(path, asyncRoute(async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
res.json(await handler(req.params.id, bodyObject(req.body ?? {}), req.params));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
sendTeamError(res, error);
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
};
|
|
277
|
+
action("/api/ai-team-runs/:id/approve", (runId) => runner.approve(runId));
|
|
278
|
+
action("/api/ai-team-runs/:id/reject", (runId, body) => runner.reject(runId, text(body.feedback)));
|
|
279
|
+
action("/api/ai-team-runs/:id/reply", (runId, body) => runner.reply(runId, text(body.text)));
|
|
280
|
+
action("/api/ai-team-runs/:id/continue", (runId, body) => runner.continueRun(runId, Number(body.extraSteps) || 10));
|
|
281
|
+
action("/api/ai-team-runs/:id/steps/:stepId/complete", (runId, body, params) => (runner.completeStep(runId, params.stepId, text(body.report))));
|
|
282
|
+
action("/api/ai-team-runs/:id/stop", (runId) => runner.stop(runId));
|
|
283
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { AiTeamRunner } from "./ai-team-runner.js";
|
|
3
|
+
import type { SessionSnapshot } from "./types.js";
|
|
4
|
+
export declare function registerAttentionRoutes(app: Express, deps: {
|
|
5
|
+
sessions: {
|
|
6
|
+
listSlim(): SessionSnapshot[];
|
|
7
|
+
};
|
|
8
|
+
runner: AiTeamRunner;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { collectAttentionItems } from "./attention.js";
|
|
2
|
+
export function registerAttentionRoutes(app, deps) {
|
|
3
|
+
app.get("/api/attention", (_req, res) => {
|
|
4
|
+
const runs = deps.runner.listRuns({ limit: 80 });
|
|
5
|
+
const teamRuns = runs.map((run) => ({
|
|
6
|
+
id: run.id,
|
|
7
|
+
status: run.status,
|
|
8
|
+
statusDetail: run.statusDetail,
|
|
9
|
+
taskTitle: run.taskTitle,
|
|
10
|
+
teamName: run.team.name,
|
|
11
|
+
chatSessionId: run.chatSessionId,
|
|
12
|
+
updatedAt: run.updatedAt,
|
|
13
|
+
}));
|
|
14
|
+
const sessions = deps.sessions.listSlim().map((session) => ({
|
|
15
|
+
id: session.id,
|
|
16
|
+
title: session.title?.trim() || session.description?.trim() || "",
|
|
17
|
+
archived: session.archived,
|
|
18
|
+
status: session.status,
|
|
19
|
+
lastError: session.structuredState?.lastError ?? null,
|
|
20
|
+
worktreeMergeStatus: session.worktreeMergeStatus ?? null,
|
|
21
|
+
automationId: session.automationId ?? null,
|
|
22
|
+
updatedAt: session.endedAt || session.startedAt,
|
|
23
|
+
}));
|
|
24
|
+
res.json({ items: collectAttentionItems({ runs: teamRuns, sessions }) });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -593,7 +593,7 @@ export function registerFileRoutes(app, deps) {
|
|
|
593
593
|
const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
|
|
594
594
|
const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
|
|
595
595
|
const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
|
|
596
|
-
const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads"]);
|
|
596
|
+
const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads", ".wand-team"]);
|
|
597
597
|
const maxVisitedEntries = 20_000;
|
|
598
598
|
let resolvedCwd;
|
|
599
599
|
try {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { PtyInputDeliveryError, SessionInputError } from "./process-manager.js";
|
|
3
3
|
import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
|
|
4
|
-
import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
4
|
+
import { alignedBlockStart, blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
|
|
5
5
|
import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
|
|
6
6
|
import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
|
|
7
7
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
8
8
|
import { resolveCommitAiContext } from "./session-ai-context.js";
|
|
9
9
|
import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
10
10
|
import { getErrorMessage } from "./error-utils.js";
|
|
11
|
+
import { archiveCommitTasks } from "./commit-task-archive.js";
|
|
11
12
|
import { buildIterationCommitContext, COMMIT_CONTEXT_MODE_PREF_KEY, isCommitContextMode, readCommitContextMode, resolveCommitContextInput, } from "./iteration-log.js";
|
|
12
13
|
import { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
|
|
13
14
|
import { buildProviderResumeCommand, isProviderSessionId } from "./resume-policy.js";
|
|
@@ -327,6 +328,8 @@ async function startResumedPtySession(processes, storage, existingSession, sessi
|
|
|
327
328
|
provider,
|
|
328
329
|
model: existingSession.selectedModel ?? undefined,
|
|
329
330
|
thinkingEffort: existingSession.thinkingEffort ?? undefined,
|
|
331
|
+
// 恢复会话也要把角色与规则带回来(PTY 的指令是拼在启动命令上的)。
|
|
332
|
+
systemPrompt: existingSession.systemPrompt ?? undefined,
|
|
330
333
|
workspaceId: existingSession.workspaceId
|
|
331
334
|
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
332
335
|
workspaceTaskId: existingSession.workspaceTaskId,
|
|
@@ -449,6 +452,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
449
452
|
app.get("/api/sessions", (_req, res) => {
|
|
450
453
|
res.json(sessions.listSlim().map(toSessionListItemDTO));
|
|
451
454
|
});
|
|
455
|
+
app.get("/api/sessions/provider-usage", (_req, res) => {
|
|
456
|
+
res.json(storage.countInteractiveSessionsByProvider());
|
|
457
|
+
});
|
|
452
458
|
app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
|
|
453
459
|
const body = req.body;
|
|
454
460
|
try {
|
|
@@ -474,6 +480,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
474
480
|
: config.defaultThinkingEffort,
|
|
475
481
|
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
|
|
476
482
|
workspaceTaskId: body.workspaceTaskId,
|
|
483
|
+
// 角色与规则走系统提示通道,不拼进 prompt(见 AGENTS.md「Session 输入契约」)。
|
|
484
|
+
systemPrompt: body.systemPrompt,
|
|
477
485
|
...origin,
|
|
478
486
|
});
|
|
479
487
|
onSessionCreated?.(snapshot.cwd);
|
|
@@ -876,8 +884,23 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
876
884
|
const consumed = result.ok && result.commit?.hash && context.entryIds.length > 0
|
|
877
885
|
? storage.markIterationPromptsConsumed(context.entryIds, result.commit.hash)
|
|
878
886
|
: 0;
|
|
887
|
+
let archivedTaskIds = [];
|
|
888
|
+
let archiveError;
|
|
889
|
+
if (body.archiveRelatedTasks === true && result.ok && result.commit?.hash) {
|
|
890
|
+
try {
|
|
891
|
+
// A commit is already durable, even when push failed. Archiving must not turn that
|
|
892
|
+
// successful commit into a retryable HTTP error (which would duplicate the commit).
|
|
893
|
+
archivedTaskIds = archiveCommitTasks(storage, snapshot, context.entryIds);
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
archiveError = getErrorMessage(error, "归档关联任务失败。");
|
|
897
|
+
console.error("[QuickCommit] Failed to archive related tasks:", archiveError);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
879
900
|
res.json({
|
|
880
901
|
...result,
|
|
902
|
+
archivedTaskIds,
|
|
903
|
+
...(archiveError ? { archiveError } : {}),
|
|
881
904
|
commitContext: {
|
|
882
905
|
source: commitMessageFromIteration(body) && context.digest ? "iteration" : "diff",
|
|
883
906
|
entryIds: context.entryIds,
|
|
@@ -1111,6 +1134,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1111
1134
|
messageTotal: windowed.messageTotal,
|
|
1112
1135
|
leadingBlockOffset: windowed.leadingBlockOffset,
|
|
1113
1136
|
leadingBlockTotal: windowed.leadingBlockTotal,
|
|
1137
|
+
leadingVisibleCount: windowed.leadingVisibleCount,
|
|
1114
1138
|
}));
|
|
1115
1139
|
return;
|
|
1116
1140
|
}
|
|
@@ -1149,14 +1173,16 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1149
1173
|
offset: windowed.messageOffset, total });
|
|
1150
1174
|
return;
|
|
1151
1175
|
}
|
|
1152
|
-
// 块级翻页(iOS):?turn=<i>&blockOffset=<当前 leading 偏移>&blockLimit=<N>
|
|
1176
|
+
// 块级翻页(iOS / Android):?turn=<i>&blockOffset=<当前 leading 偏移>&blockLimit=<N>
|
|
1153
1177
|
// 取该 turn 的 [start, blockOffset) 段(start = max(0, blockOffset - blockLimit))。
|
|
1178
|
+
// 起点同样吸附到干净边界(不切开折叠的工具段、不留下无头 tool_result),并回一份
|
|
1179
|
+
// blockVisible:被切掉的头部里还剩多少条用户可感知的块(默认收起的工具块不计入)。
|
|
1154
1180
|
const rawTurn = parseInt(String(req.query.turn ?? ""), 10);
|
|
1155
1181
|
if (Number.isFinite(rawTurn)) {
|
|
1156
1182
|
const turnIndex = Math.min(Math.max(rawTurn, 0), Math.max(total - 1, 0));
|
|
1157
1183
|
const turn = all[turnIndex];
|
|
1158
1184
|
if (!turn) {
|
|
1159
|
-
res.json({ turnIndex, blocks: [], blockOffset: 0, blockTotal: 0 });
|
|
1185
|
+
res.json({ turnIndex, blocks: [], blockOffset: 0, blockTotal: 0, blockVisible: 0 });
|
|
1160
1186
|
return;
|
|
1161
1187
|
}
|
|
1162
1188
|
const blockTotal = turn.content.length;
|
|
@@ -1165,11 +1191,20 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1165
1191
|
const blockLimit = Math.min(Math.max(Number.isFinite(rawBlockLimit) ? rawBlockLimit : 40, 1), 200);
|
|
1166
1192
|
const blockEnd = Math.min(Math.max(Number.isFinite(rawBlockOffset) ? rawBlockOffset : blockTotal, 0), blockTotal);
|
|
1167
1193
|
const blockStart = Math.max(0, blockEnd - blockLimit);
|
|
1194
|
+
const cardDefaults = config.cardDefaults ?? {};
|
|
1195
|
+
const startOffset = alignedBlockStart(turn.content, cardDefaults, blockStart, blockEnd);
|
|
1168
1196
|
const blocks = enrichStructuredMessages([{
|
|
1169
1197
|
...turn,
|
|
1170
|
-
content: sliceTurnBlocksForTransport(turn,
|
|
1198
|
+
content: sliceTurnBlocksForTransport(turn, startOffset, blockEnd, cardDefaults),
|
|
1171
1199
|
}], snapshot.id)[0].content;
|
|
1172
|
-
res.json({
|
|
1200
|
+
res.json({
|
|
1201
|
+
wandProtocolVersion: WAND_PROTOCOL_VERSION,
|
|
1202
|
+
turnIndex,
|
|
1203
|
+
blocks,
|
|
1204
|
+
blockOffset: startOffset,
|
|
1205
|
+
blockTotal,
|
|
1206
|
+
blockVisible: visibleBlockCount(turn.content, cardDefaults, startOffset),
|
|
1207
|
+
});
|
|
1173
1208
|
return;
|
|
1174
1209
|
}
|
|
1175
1210
|
const rawLimit = parseInt(String(req.query.limit ?? ""), 10);
|
|
@@ -1240,6 +1275,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1240
1275
|
reuseId: existingSession.id,
|
|
1241
1276
|
cols: reqCols,
|
|
1242
1277
|
rows: reqRows,
|
|
1278
|
+
systemPrompt: existingSession.systemPrompt ?? undefined,
|
|
1243
1279
|
workspaceId: existingSession.workspaceId
|
|
1244
1280
|
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
1245
1281
|
workspaceTaskId: existingSession.workspaceTaskId,
|
|
@@ -210,7 +210,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
210
210
|
}
|
|
211
211
|
const startedAt = Date.now();
|
|
212
212
|
try {
|
|
213
|
-
const text = await callSystemAiText("这是 Wand 系统 API 线路验收。请只回复 WAND_API_OK。", route, 30_000);
|
|
213
|
+
const text = await callSystemAiText({ system: "你是 Wand 的系统 API 线路探针。", prompt: "这是 Wand 系统 API 线路验收。请只回复 WAND_API_OK。" }, route, 30_000);
|
|
214
214
|
if (!text.trim())
|
|
215
215
|
throw new Error("系统 AI API 返回了空结果。");
|
|
216
216
|
res.json({
|
|
@@ -19,6 +19,8 @@ export interface TaskRouteDependencies {
|
|
|
19
19
|
/** 可注入的任务标题生成器;测试里换成同步桩,避免真的起 CLI。 */
|
|
20
20
|
generateTitle?: typeof generateWandTaskTitle;
|
|
21
21
|
}
|
|
22
|
+
/** 新认领只能挂到同项目且正在处理的任务;历史归属在父任务完成后仍保留。 */
|
|
23
|
+
export declare function parentTaskIdFrom(storage: WandStorage, value: unknown, workspaceId: string | null, childId?: string | null, existingParentId?: string | null): string | null;
|
|
22
24
|
/**
|
|
23
25
|
* 任务派发配置:provider / model / thinkingEffort 必须合法;mode 缺省时用 `fallbackMode`。
|
|
24
26
|
* 老客户端(Android / iOS 尚未发 mode)不传 mode,PATCH / dispatch 时传任务当前值,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dispatchAgentForTask } from "./agent-dispatch.js";
|
|
2
2
|
import { asyncRoute } from "./express-async.js";
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
4
|
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
@@ -9,7 +9,7 @@ import { recordIterationPromptForTask } from "./iteration-log.js";
|
|
|
9
9
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
10
10
|
import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
|
|
11
11
|
import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, syncWorkspaceTaskToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
|
|
12
|
-
import { isSessionProvider
|
|
12
|
+
import { isSessionProvider } from "./session-provider.js";
|
|
13
13
|
import { isThinkingEffort } from "./structured-provider-common.js";
|
|
14
14
|
const STATUSES = new Set(["todo", "doing", "done", "archived"]);
|
|
15
15
|
const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
|
|
@@ -54,7 +54,7 @@ function labelsFrom(value) {
|
|
|
54
54
|
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20);
|
|
55
55
|
}
|
|
56
56
|
/** 新认领只能挂到同项目且正在处理的任务;历史归属在父任务完成后仍保留。 */
|
|
57
|
-
function parentTaskIdFrom(storage, value, workspaceId, childId = null, existingParentId = null) {
|
|
57
|
+
export function parentTaskIdFrom(storage, value, workspaceId, childId = null, existingParentId = null) {
|
|
58
58
|
if (value === null || value === "")
|
|
59
59
|
return null;
|
|
60
60
|
if (typeof value !== "string" || !value.trim())
|
|
@@ -588,59 +588,25 @@ export function registerTaskRoutes(app, deps) {
|
|
|
588
588
|
assertTaskWorkspaceMove(storage, task.id, workspaceId);
|
|
589
589
|
task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
|
|
590
590
|
}
|
|
591
|
-
const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
|
|
592
|
-
if (task.workspaceId && !workspace)
|
|
593
|
-
throw new Error("任务所属项目已被删除,请重新指定。");
|
|
594
|
-
const group = ensureWorkspaceTaskForBoardTask(storage, task);
|
|
595
|
-
task = storage.getWandTask(task.id);
|
|
596
|
-
const cwd = group.worktree?.path || group.cwd || workspace?.cwd || config.defaultCwd;
|
|
597
591
|
const requestedPrompt = text(body.prompt);
|
|
598
592
|
const existingSessions = storage.listWandTaskSessionIds(task.id).length;
|
|
599
593
|
if (existingSessions > 0 && !requestedPrompt)
|
|
600
594
|
throw new Error("请输入提示词。");
|
|
601
595
|
const prompt = requestedPrompt || task.description.trim() || task.title || "执行此任务";
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
const resolvedModel = model || getDefaultModelForProvider(config, provider) || undefined;
|
|
605
|
-
const session = agent.kind === "pty"
|
|
606
|
-
? await processes.start(providerCliCommand(provider), cwd, agent.mode, prompt, {
|
|
607
|
-
provider,
|
|
608
|
-
model: resolvedModel,
|
|
609
|
-
thinkingEffort: agent.thinkingEffort,
|
|
610
|
-
sessionSource: "automation",
|
|
611
|
-
automationId: `wand-task:${task.id}`,
|
|
612
|
-
workspaceId: group.workspaceId,
|
|
613
|
-
workspaceTaskId: group.id,
|
|
614
|
-
})
|
|
615
|
-
: structured.createSession({
|
|
616
|
-
cwd,
|
|
617
|
-
mode: agent.mode,
|
|
618
|
-
provider,
|
|
619
|
-
model: resolvedModel,
|
|
620
|
-
thinkingEffort: agent.thinkingEffort,
|
|
621
|
-
worktreeEnabled: false,
|
|
622
|
-
sessionSource: "automation",
|
|
623
|
-
automationId: `wand-task:${task.id}`,
|
|
624
|
-
workspaceId: group.workspaceId,
|
|
625
|
-
workspaceTaskId: group.id,
|
|
626
|
-
});
|
|
596
|
+
const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}` });
|
|
597
|
+
task = storage.getWandTask(task.id);
|
|
627
598
|
storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
|
|
628
|
-
storage.bindWandTaskSession(task.id, session.id);
|
|
629
599
|
// 派发也算一轮迭代里的改动意图:直接把任务的标题 / 描述记进迭代记录。
|
|
630
600
|
recordIterationPromptForTask(storage, {
|
|
631
601
|
sessionId: session.id,
|
|
632
602
|
cwd,
|
|
633
|
-
workspaceId
|
|
603
|
+
workspaceId,
|
|
634
604
|
milestoneId: task.milestoneId,
|
|
635
605
|
taskId: task.id,
|
|
636
606
|
title: task.title,
|
|
637
607
|
detail: task.description || prompt,
|
|
638
608
|
source: "dispatch",
|
|
639
609
|
});
|
|
640
|
-
if (agent.kind !== "pty") {
|
|
641
|
-
const completion = structured.sendMessage(session.id, prompt);
|
|
642
|
-
completion.catch((error) => console.error(`[WandTask] Agent dispatch failed for ${task.id}:`, error));
|
|
643
|
-
}
|
|
644
610
|
res.status(202).json({
|
|
645
611
|
ok: true,
|
|
646
612
|
taskId: task.id,
|
|
@@ -13,7 +13,7 @@ import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId
|
|
|
13
13
|
import { archiveBoardTaskForWorkspaceTask, archiveWorkspaceTask, ensureBoardTaskForWorkspaceTask, isUnnamedWorkspaceTaskName, moveSessionToWorkspaceTask, syncSidebarTasksFromBoard, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
|
|
14
14
|
import { isSessionProvider } from "./session-provider.js";
|
|
15
15
|
import { firstLayoutTabId } from "./layout-tree.js";
|
|
16
|
-
import { refreshAutoBoardTaskTitles } from "./server-task-routes.js";
|
|
16
|
+
import { parentTaskIdFrom, refreshAutoBoardTaskTitles } from "./server-task-routes.js";
|
|
17
17
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
18
18
|
function workspaceSessionTitle(session, names = {}) {
|
|
19
19
|
return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
|
|
@@ -25,7 +25,7 @@ function workspaceSessionTitle(session, names = {}) {
|
|
|
25
25
|
function liveSession(session, registry) {
|
|
26
26
|
return registry?.getLatest(session.id) ?? session;
|
|
27
27
|
}
|
|
28
|
-
function workspaceSessionSummary(session, names = {}, registry) {
|
|
28
|
+
function workspaceSessionSummary(session, names = {}, registry, teamChat) {
|
|
29
29
|
const live = liveSession(session, registry);
|
|
30
30
|
return {
|
|
31
31
|
id: live.id,
|
|
@@ -40,6 +40,7 @@ function workspaceSessionSummary(session, names = {}, registry) {
|
|
|
40
40
|
inFlight: live.structuredState?.inFlight === true,
|
|
41
41
|
cwd: live.cwd,
|
|
42
42
|
startedAt: live.startedAt,
|
|
43
|
+
...(teamChat ? { teamChat } : {}),
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
function cheapTasksRevision(storage, registry) {
|
|
@@ -150,6 +151,8 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
150
151
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
151
152
|
// 首个会话的提示词:任务没起名时用它先总结一个标题,不再留「未命名任务」占位。
|
|
152
153
|
const description = typeof body.description === "string" ? body.description.trim() : "";
|
|
154
|
+
// 在建 workspace task / worktree 前校验,避免父任务失效时留下孤立任务。
|
|
155
|
+
const parentTaskId = parentTaskIdFrom(storage, body.parentTaskId ?? null, isGlobalWorkspace(workspace) ? null : workspace.id);
|
|
153
156
|
// 留空(或老客户端回传的占位名)走自动命名:先用提示词总结,再交给模型改写。
|
|
154
157
|
const named = name.length > 0 && !isUnnamedWorkspaceTaskName(name);
|
|
155
158
|
const provisional = named ? "" : provisionalTaskTitleFromDescription(description);
|
|
@@ -193,6 +196,7 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
193
196
|
ensureBoardTaskForWorkspaceTask(storage, task, workspace, {
|
|
194
197
|
titleSource: named ? "user" : "auto",
|
|
195
198
|
description,
|
|
199
|
+
parentTaskId,
|
|
196
200
|
});
|
|
197
201
|
return {
|
|
198
202
|
task,
|
|
@@ -353,10 +357,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
353
357
|
return;
|
|
354
358
|
}
|
|
355
359
|
storage.touchWorkspace(workspace.id);
|
|
360
|
+
const teamChatMarkers = storage.listAiTeamRunChatMarkers();
|
|
356
361
|
res.json({
|
|
357
362
|
...workspaceWithCounts(storage, workspace),
|
|
358
363
|
sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
|
|
359
|
-
...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions),
|
|
364
|
+
...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamChatMarkers.get(session.id)),
|
|
360
365
|
workspaceTaskId: session.workspaceTaskId,
|
|
361
366
|
})),
|
|
362
367
|
});
|
|
@@ -506,7 +511,9 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
506
511
|
// messages 大字段 JSON 解析一遍(单会话可达十几 MB,整表 ~30MB)。
|
|
507
512
|
const allSessions = storage.loadSessionsSlim();
|
|
508
513
|
const workspaces = storage.listWorkspaces();
|
|
509
|
-
|
|
514
|
+
// 群聊标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
|
|
515
|
+
const teamChatMarkers = storage.listAiTeamRunChatMarkers();
|
|
516
|
+
const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions, teamChatMarkers.get(session.id));
|
|
510
517
|
// 目录自定义名(工作区名称):没有项目实体的合成目录也按这个名字显示。
|
|
511
518
|
const directoryNames = storage.listSessionDirectoryNames();
|
|
512
519
|
const customNameForCwd = (cwd) => {
|
|
@@ -804,6 +811,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
804
811
|
}
|
|
805
812
|
storage.touchWorkspaceTask(task.id);
|
|
806
813
|
const workspace = storage.getWorkspace(task.workspaceId);
|
|
814
|
+
const teamChatMarkers = storage.listAiTeamRunChatMarkers();
|
|
807
815
|
res.json({
|
|
808
816
|
...task,
|
|
809
817
|
...resolvedMilestoneFields(storage, task.milestoneId),
|
|
@@ -812,7 +820,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
812
820
|
...workspaceSessionSummary(session, {
|
|
813
821
|
taskName: task.name,
|
|
814
822
|
workspaceName: workspace?.name,
|
|
815
|
-
}, sessions),
|
|
823
|
+
}, sessions, teamChatMarkers.get(session.id)),
|
|
816
824
|
workspaceTaskId: task.id,
|
|
817
825
|
})),
|
|
818
826
|
});
|