@co0ontty/wand 4.77.0 → 4.78.0-beta.ga1e4f59

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 (54) hide show
  1. package/README.md +21 -46
  2. package/dist/agent-dispatch.js +3 -2
  3. package/dist/ai-team-live.d.ts +25 -0
  4. package/dist/ai-team-live.js +135 -0
  5. package/dist/ai-team-prompts.d.ts +10 -6
  6. package/dist/ai-team-prompts.js +74 -41
  7. package/dist/ai-team-runner.d.ts +85 -3
  8. package/dist/ai-team-runner.js +336 -33
  9. package/dist/ai-team-types.d.ts +57 -2
  10. package/dist/ai-team-types.js +4 -0
  11. package/dist/build-info.json +4 -4
  12. package/dist/commit-task-archive.js +14 -18
  13. package/dist/config.js +14 -6
  14. package/dist/git-quick-commit.js +1 -1
  15. package/dist/middleware/rate-limit.d.ts +18 -4
  16. package/dist/middleware/rate-limit.js +55 -23
  17. package/dist/missions.js +0 -3
  18. package/dist/models.d.ts +6 -1
  19. package/dist/models.js +35 -0
  20. package/dist/path-repair.d.ts +6 -5
  21. package/dist/path-repair.js +3 -3
  22. package/dist/render-protocol.d.ts +1 -1
  23. package/dist/render-protocol.js +1 -1
  24. package/dist/server-ai-team-routes.d.ts +1 -0
  25. package/dist/server-ai-team-routes.js +28 -14
  26. package/dist/server-local-preview-routes.js +1 -1
  27. package/dist/server-session-routes.js +0 -3
  28. package/dist/server-settings-routes.d.ts +1 -1
  29. package/dist/server-settings-routes.js +3 -2
  30. package/dist/server-task-routes.js +16 -32
  31. package/dist/server-workspace-routes.js +31 -24
  32. package/dist/server.js +60 -21
  33. package/dist/storage.d.ts +54 -21
  34. package/dist/storage.js +485 -122
  35. package/dist/types.d.ts +7 -0
  36. package/dist/upload-routes.d.ts +2 -2
  37. package/dist/upload-routes.js +10 -3
  38. package/dist/wand-task-sync.d.ts +2 -34
  39. package/dist/wand-task-sync.js +0 -206
  40. package/dist/web-ui/asset-version.d.ts +5 -0
  41. package/dist/web-ui/asset-version.js +5 -0
  42. package/dist/web-ui/content/ai-teams.js +17 -8
  43. package/dist/web-ui/content/scripts.js +67 -66
  44. package/dist/web-ui/content/styles.css +1 -1
  45. package/dist/web-ui/content/tailwind.css +0 -21
  46. package/dist/web-ui/embedded-assets.js +8 -7
  47. package/dist/web-ui/index.js +9 -17
  48. package/dist/web-ui/scripts.d.ts +3 -1
  49. package/dist/web-ui/scripts.js +52 -25
  50. package/dist/web-ui/styles.d.ts +2 -0
  51. package/dist/web-ui/styles.js +12 -0
  52. package/package.json +13 -12
  53. package/scripts/install-render-binary.js +2 -2
  54. package/scripts/stage-render-binaries.js +1 -1
@@ -8,7 +8,7 @@ import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_
8
8
  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
- import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, syncWorkspaceTaskToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
11
+ import { isAutoNameableBoardTask, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
12
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"]);
@@ -268,12 +268,6 @@ export function registerTaskRoutes(app, deps) {
268
268
  const { storage, sessions, structured, processes, config } = deps;
269
269
  const dto = (task) => taskDto(deps, task);
270
270
  app.get("/api/wand-tasks", (req, res) => {
271
- try {
272
- syncUngroupedSessionsToBoard(storage);
273
- }
274
- catch (error) {
275
- console.error("[WandTask] Failed to sync ungrouped sessions onto the board:", getErrorMessage(error));
276
- }
277
271
  try {
278
272
  // 侧栏建的任务 / 后续新增的会话都会在这里补上自动标题。
279
273
  refreshAutoBoardTaskTitles(storage, { config, generateTitle: deps.generateTitle });
@@ -406,22 +400,18 @@ export function registerTaskRoutes(app, deps) {
406
400
  const requestedMilestoneId = milestoneIdFrom(storage, body.milestoneId) ?? defaultMilestoneIdForWrite(storage);
407
401
  const milestoneId = scopedMilestoneId(storage, requestedMilestoneId, workspaceId)
408
402
  ?? defaultMilestoneIdForWrite(storage);
409
- const task = storage.transaction(() => {
410
- const card = storage.createWandTask({
411
- workspaceId,
412
- parentTaskId,
413
- title,
414
- titleSource,
415
- description,
416
- status,
417
- priority,
418
- labels,
419
- dueDate,
420
- milestoneId,
421
- agent,
422
- });
423
- ensureWorkspaceTaskForBoardTask(storage, card);
424
- return storage.getWandTask(card.id);
403
+ const task = storage.createWandTask({
404
+ workspaceId,
405
+ parentTaskId,
406
+ title,
407
+ titleSource,
408
+ description,
409
+ status,
410
+ priority,
411
+ labels,
412
+ dueDate,
413
+ milestoneId,
414
+ agent,
425
415
  });
426
416
  if (titleSource === "auto") {
427
417
  refreshAutoBoardTaskTitles(storage, {
@@ -505,8 +495,6 @@ export function registerTaskRoutes(app, deps) {
505
495
  res.status(404).json({ error: "未找到该任务。" });
506
496
  return;
507
497
  }
508
- if (task.status === "done" || task.status === "archived")
509
- syncClosedBoardTask(storage, task);
510
498
  res.json(dto(storage.getWandTask(task.id) ?? task));
511
499
  }
512
500
  catch (error) {
@@ -514,7 +502,7 @@ export function registerTaskRoutes(app, deps) {
514
502
  }
515
503
  });
516
504
  app.delete("/api/wand-tasks/:id", (req, res) => {
517
- const archived = archiveBoardTask(storage, req.params.id);
505
+ const archived = storage.updateWandTask(req.params.id, { status: "archived" });
518
506
  if (!archived) {
519
507
  res.status(404).json({ error: "未找到该任务。" });
520
508
  return;
@@ -533,8 +521,7 @@ export function registerTaskRoutes(app, deps) {
533
521
  res.status(404).json({ error: "未找到该任务。" });
534
522
  return;
535
523
  }
536
- const task = ensureWorkspaceTaskForBoardTask(storage, card);
537
- moveSessionToWorkspaceTask(storage, sessionId, task.id);
524
+ storage.bindWandTaskSession(card.id, sessionId);
538
525
  sessions?.refreshSessionWorkspace(sessionId);
539
526
  res.status(201).json(dto(storage.getWandTask(req.params.id)));
540
527
  }
@@ -546,7 +533,7 @@ export function registerTaskRoutes(app, deps) {
546
533
  const card = storage.getWandTask(req.params.id);
547
534
  const session = storage.getSession(req.params.sessionId);
548
535
  if (card?.workspaceTaskId && session?.workspaceTaskId === card.workspaceTaskId) {
549
- moveSessionToWorkspaceTask(storage, session.id, null);
536
+ storage.moveSessionToWorkspaceTask(session.id, null);
550
537
  sessions?.refreshSessionWorkspace(session.id);
551
538
  }
552
539
  else {
@@ -626,9 +613,6 @@ export function registerTaskRoutes(app, deps) {
626
613
  }
627
614
  }));
628
615
  app.get("/api/wand-tasks/:id", asyncRoute(async (req, res) => {
629
- // 单卡读取不跑全量同步,但至少把这张卡片自己的任务归属对上:
630
- // 原生客户端 / 详情面板只拉单卡时,也不会看到一张没有会话的卡片。
631
- syncWorkspaceTaskToBoard(storage, storage.getWandTask(req.params.id)?.workspaceTaskId);
632
616
  const task = dto(storage.getWandTask(req.params.id));
633
617
  if (!task) {
634
618
  res.status(404).json({ error: "未找到该任务。" });
@@ -10,7 +10,7 @@ import { collectSessionTopicBlocklist } from "./session-topic.js";
10
10
  import { resolveSessionDisplayTitle } from "./session-transport.js";
11
11
  import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
12
12
  import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
13
- import { archiveBoardTaskForWorkspaceTask, archiveWorkspaceTask, ensureBoardTaskForWorkspaceTask, isUnnamedWorkspaceTaskName, moveSessionToWorkspaceTask, syncSidebarTasksFromBoard, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
13
+ import { isUnnamedWorkspaceTaskName, 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
16
  import { parentTaskIdFrom, refreshAutoBoardTaskTitles } from "./server-task-routes.js";
@@ -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, teamChat) {
28
+ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
29
29
  const live = liveSession(session, registry);
30
30
  return {
31
31
  id: live.id,
@@ -40,12 +40,30 @@ function workspaceSessionSummary(session, names = {}, registry, teamChat) {
40
40
  inFlight: live.structuredState?.inFlight === true,
41
41
  cwd: live.cwd,
42
42
  startedAt: live.startedAt,
43
- ...(teamChat ? { teamChat } : {}),
43
+ ...(team.chat ? { teamChat: team.chat } : {}),
44
+ ...(team.step ? { teamStep: team.step } : {}),
44
45
  };
45
46
  }
47
+ /**
48
+ * 会话列表用的团队标记索引:群聊入口 + 成员派发步骤,各一次查询建整张表,
49
+ * 列表里每条会话只做 Map 命中,不逐个查库。
50
+ */
51
+ function teamSessionMarkers(storage) {
52
+ return { chat: storage.listAiTeamRunChatMarkers(), step: storage.listAiTeamStepSessionMarkers() };
53
+ }
54
+ /** 单条会话的团队标记;群聊条目只按群聊算,别再当成派发步骤折进任务下面的团队分组。 */
55
+ function teamMarkersFor(markers, sessionId) {
56
+ const chat = markers.chat.get(sessionId);
57
+ if (chat)
58
+ return { chat };
59
+ const step = markers.step.get(sessionId);
60
+ return step ? { step } : {};
61
+ }
46
62
  function cheapTasksRevision(storage, registry) {
47
63
  const fingerprint = [
48
64
  storage.tasksAggregateFingerprint(),
65
+ // 团队标记(群聊入口、成员步骤与运行状态)也在列表里。
66
+ storage.teamSessionFingerprint(),
49
67
  // 排序只更新偏好,不改变任务和会话;轮询 revision 也必须跟着变化。
50
68
  storage.getWorkspaceGroupOrder(),
51
69
  ...(registry?.listSlim() ?? []).map((session) => [
@@ -192,11 +210,7 @@ function createTaskForWorkspace(storage, workspace, body) {
192
210
  cwd: storedCwd,
193
211
  worktree,
194
212
  milestoneId,
195
- });
196
- ensureBoardTaskForWorkspaceTask(storage, task, workspace, {
197
- titleSource: named ? "user" : "auto",
198
- description,
199
- parentTaskId,
213
+ board: { titleSource: named ? "user" : "auto", description, parentTaskId },
200
214
  });
201
215
  return {
202
216
  task,
@@ -357,11 +371,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
357
371
  return;
358
372
  }
359
373
  storage.touchWorkspace(workspace.id);
360
- const teamChatMarkers = storage.listAiTeamRunChatMarkers();
374
+ const teamMarkers = teamSessionMarkers(storage);
361
375
  res.json({
362
376
  ...workspaceWithCounts(storage, workspace),
363
377
  sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
364
- ...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamChatMarkers.get(session.id)),
378
+ ...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamMarkersFor(teamMarkers, session.id)),
365
379
  workspaceTaskId: session.workspaceTaskId,
366
380
  })),
367
381
  });
@@ -480,7 +494,6 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
480
494
  }));
481
495
  // 目录组为一级容器的任务聚合列表,供侧栏「任务」视图一次拉全。
482
496
  app.get("/api/tasks", (req, res) => {
483
- syncSidebarTasksFromBoard(storage);
484
497
  // 侧栏轮询也在补自动标题:不打开看板的任务(以及新增会话后的改名)也要吃到。
485
498
  try {
486
499
  refreshAutoBoardTaskTitles(storage, titleOptions);
@@ -511,9 +524,9 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
511
524
  // messages 大字段 JSON 解析一遍(单会话可达十几 MB,整表 ~30MB)。
512
525
  const allSessions = storage.loadSessionsSlim();
513
526
  const workspaces = storage.listWorkspaces();
514
- // 群聊标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
515
- const teamChatMarkers = storage.listAiTeamRunChatMarkers();
516
- const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions, teamChatMarkers.get(session.id));
527
+ // 群聊与成员派发标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
528
+ const teamMarkers = teamSessionMarkers(storage);
529
+ const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions, teamMarkersFor(teamMarkers, session.id));
517
530
  // 目录自定义名(工作区名称):没有项目实体的合成目录也按这个名字显示。
518
531
  const directoryNames = storage.listSessionDirectoryNames();
519
532
  const customNameForCwd = (cwd) => {
@@ -811,7 +824,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
811
824
  }
812
825
  storage.touchWorkspaceTask(task.id);
813
826
  const workspace = storage.getWorkspace(task.workspaceId);
814
- const teamChatMarkers = storage.listAiTeamRunChatMarkers();
827
+ const teamMarkers = teamSessionMarkers(storage);
815
828
  res.json({
816
829
  ...task,
817
830
  ...resolvedMilestoneFields(storage, task.milestoneId),
@@ -820,7 +833,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
820
833
  ...workspaceSessionSummary(session, {
821
834
  taskName: task.name,
822
835
  workspaceName: workspace?.name,
823
- }, sessions, teamChatMarkers.get(session.id)),
836
+ }, sessions, teamMarkersFor(teamMarkers, session.id)),
824
837
  workspaceTaskId: task.id,
825
838
  })),
826
839
  });
@@ -833,7 +846,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
833
846
  return;
834
847
  }
835
848
  try {
836
- moveSessionToWorkspaceTask(storage, sessionId, req.params.taskId);
849
+ storage.moveSessionToWorkspaceTask(sessionId, req.params.taskId);
837
850
  sessions?.refreshSessionWorkspace(sessionId);
838
851
  res.json({ ok: true });
839
852
  }
@@ -864,8 +877,6 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
864
877
  patch.milestoneId = milestoneId || defaultMilestoneIdForWrite(storage);
865
878
  }
866
879
  storage.updateWorkspaceTask(existing.id, patch);
867
- if (patch.status === "done")
868
- archiveBoardTaskForWorkspaceTask(storage, existing.id);
869
880
  const updated = storage.getWorkspaceTask(existing.id);
870
881
  res.json({
871
882
  ...updated,
@@ -879,7 +890,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
879
890
  res.status(404).json({ error: "未找到该任务。" });
880
891
  return;
881
892
  }
882
- archiveWorkspaceTask(storage, existing);
893
+ storage.archiveWorkspaceTask(existing.id);
883
894
  const archived = storage.getWorkspaceTask(existing.id);
884
895
  res.json({
885
896
  ...archived,
@@ -908,10 +919,6 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
908
919
  // 尽力清理 worktree 与分支;失败不阻塞删除任务行。
909
920
  if (cascade)
910
921
  cleanupWorktreeSync(existing.worktree);
911
- archiveBoardTaskForWorkspaceTask(storage, existing.id);
912
- const board = storage.getWandTaskByWorkspaceTaskId(existing.id);
913
- if (board)
914
- storage.updateWandTask(board.id, { workspaceTaskId: null, status: "archived" });
915
922
  storage.deleteWorkspaceTask(existing.id, { cascade });
916
923
  res.json({ ok: true });
917
924
  });
package/dist/server.js CHANGED
@@ -48,7 +48,6 @@ import { RuntimeConfigState } from "./runtime-config.js";
48
48
  import { safeServiceInstalled } from "./tui/runtime-utils.js";
49
49
  import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
50
50
  import { toSessionDetailDTO } from "./session-transport.js";
51
- import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
52
51
  import { registerUploadRoutes } from "./upload-routes.js";
53
52
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
54
53
  import { resolveDatabasePath, WandStorage } from "./storage.js";
@@ -57,12 +56,13 @@ import { DistributionManager } from "./distribution-manager.js";
57
56
  import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
58
57
  import { EMBEDDED_WEB_ASSETS } from "./web-ui/embedded-assets.js";
59
58
  import { renderApp } from "./web-ui/index.js";
60
- import { getAiTeamsChunk } from "./web-ui/scripts.js";
59
+ import { getAiTeamsChunk, getScriptAsset } from "./web-ui/scripts.js";
60
+ import { getStylesAsset } from "./web-ui/styles.js";
61
61
  import { WsBroadcastManager } from "./ws-broadcast.js";
62
62
  import { TerminalDaemonClient } from "./terminal-daemon-client.js";
63
63
  import { createUpgradeAwareTerminalHost } from "./render-host.js";
64
64
  import { createUpgradeAwareStructuredHost } from "./render-structured-host.js";
65
- import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
65
+ import { checkPasswordRateLimit, recordFailedPassword, resetPasswordRateLimit } from "./middleware/rate-limit.js";
66
66
  import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
67
67
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
68
68
  const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
@@ -70,7 +70,7 @@ const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
70
70
  const PKG_JSON = JSON.parse(readFileSync(path.join(RUNTIME_ROOT_DIR, "package.json"), "utf8"));
71
71
  const PKG_NAME = PKG_JSON.name;
72
72
  const PKG_VERSION = PKG_JSON.version;
73
- const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=22.5.0";
73
+ const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=26.10.0";
74
74
  const PKG_REPO_URL = "https://github.com/co0ontty/wand";
75
75
  /** 结构化聊天头像允许的图片类型;未知扩展名回 415。 */
76
76
  const AVATAR_CONTENT_TYPES = {
@@ -349,6 +349,8 @@ export async function startServer(config, configPath, options = {}) {
349
349
  const missions = new Missions(storage, structuredSessions, sessionRegistry);
350
350
  // wsManager 在后面才建:团队运行的变更通知经由这个转发口,接好之前静默丢弃。
351
351
  let notifyAiTeamRun = (_data) => { };
352
+ // 运行中步骤的 live 文本推送(§4.9),同样走系统通知,接好之前静默丢弃。
353
+ let notifyAiTeamRunLive = (_update) => { };
352
354
  // 团队降级的 model-unknown 事前比对只看这份已发现的清单;没刷新过就是空,判定方向是放行。
353
355
  const aiTeamModelIds = (provider) => {
354
356
  const cache = modelCatalog.snapshot();
@@ -365,7 +367,10 @@ export async function startServer(config, configPath, options = {}) {
365
367
  const aiTeams = createAiTeamRunner({
366
368
  storage, config, structured: structuredSessions, processes, sessions: sessionRegistry,
367
369
  notify: (run) => notifyAiTeamRun({ kind: "ai-team-run", runId: run.id, taskId: run.taskId }),
370
+ notifyLive: (update) => notifyAiTeamRunLive(update),
368
371
  models: aiTeamModelIds,
372
+ // 成员名单里的模型名:`default` 哨兵换成服务端为该 CLI 配置的默认模型。
373
+ defaultModelOf: (provider) => getDefaultModelForProvider(config, provider),
369
374
  });
370
375
  const updateState = new ServerUpdateState();
371
376
  const getUpdateChannel = () => normalizeUpdateChannel(storage.getConfigValue("updateChannel"));
@@ -412,20 +417,36 @@ export async function startServer(config, configPath, options = {}) {
412
417
  }
413
418
  next();
414
419
  });
415
- const sendEmbeddedVendorAsset = (assetPath, _req, res) => {
420
+ const requestedHash = (req) => typeof req.query.v === "string" ? req.query.v : undefined;
421
+ const setAssetCache = (req, res, hash, visibility) => {
422
+ // Never cache new bytes under an old URL after a package replacement. An old
423
+ // process can still serve its embedded version; a new process serves current
424
+ // bytes without caching the mismatch so an in-flight page remains usable.
425
+ res.setHeader("Cache-Control", requestedHash(req) === hash
426
+ ? `${visibility}, max-age=31536000, immutable`
427
+ : "no-store");
428
+ };
429
+ const sendEmbeddedVendorAsset = (assetPath, req, res) => {
416
430
  const asset = EMBEDDED_WEB_ASSETS.vendor[assetPath];
417
- res.setHeader("Cache-Control", "public, max-age=604800, immutable");
431
+ setAssetCache(req, res, asset.hash, "public");
418
432
  res.type(asset.contentType).send(asset.content);
419
433
  };
420
434
  app.get("/vendor/xterm/xterm.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/xterm/xterm.bundle.js", req, res));
421
435
  app.get("/vendor/xterm/xterm.css", (req, res) => sendEmbeddedVendorAsset("/vendor/xterm/xterm.css", req, res));
422
436
  app.get("/vendor/qrcode/qrcode.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/qrcode/qrcode.bundle.js", req, res));
423
- // AI 团队页按需脚本:页面 meta 里的地址带内容 hash,hash 对得上才长缓存。
437
+ app.get("/assets/app.css", (req, res) => {
438
+ const asset = getStylesAsset(requestedHash(req));
439
+ setAssetCache(req, res, asset.hash, "public");
440
+ res.type("text/css").send(asset.content);
441
+ });
442
+ app.get("/assets/app.js", (req, res) => {
443
+ const asset = getScriptAsset(configPath, requestedHash(req));
444
+ setAssetCache(req, res, asset.hash, "private");
445
+ res.type("application/javascript").send(asset.content);
446
+ });
424
447
  app.get("/assets/ai-teams.js", (req, res) => {
425
- const chunk = getAiTeamsChunk();
426
- res.setHeader("Cache-Control", req.query.v === chunk.hash
427
- ? "public, max-age=604800, immutable"
428
- : "no-cache");
448
+ const chunk = getAiTeamsChunk(requestedHash(req));
449
+ setAssetCache(req, res, chunk.hash, "public");
429
450
  res.type("application/javascript").send(chunk.content);
430
451
  });
431
452
  // ── Web UI endpoints ──
@@ -467,14 +488,13 @@ export async function startServer(config, configPath, options = {}) {
467
488
  // ── Auth routes ──
468
489
  app.post("/api/login", (req, res) => {
469
490
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
470
- if (!checkRateLimit(clientIp)) {
471
- res.status(429).json({ error: "登录尝试次数过多,请在 15 分钟后再试。" });
472
- return;
473
- }
474
491
  const { password, appToken, client } = req.body;
475
492
  const effectivePassword = getEffectivePassword(storage, config);
476
493
  // App token login is intentionally restricted even though the token remains
477
494
  // password-derived for compatibility with existing connect codes.
495
+ // A stale token gets 401 without feeding the counter: the client cannot fix it by
496
+ // retrying, and one device holding one used to lock out every user behind the same
497
+ // reverse proxy IP. An active lock still gates unverified attempts.
478
498
  let principal = null;
479
499
  if (appToken) {
480
500
  try {
@@ -486,15 +506,27 @@ export async function startServer(config, configPath, options = {}) {
486
506
  principal = null;
487
507
  }
488
508
  }
509
+ if (!principal) {
510
+ const lock = checkPasswordRateLimit(clientIp);
511
+ if (lock) {
512
+ const minutes = Math.max(1, Math.ceil(lock.retryAfter / 60));
513
+ res.setHeader("Retry-After", String(lock.retryAfter));
514
+ res
515
+ .status(429)
516
+ .json({ error: `登录尝试次数过多,请在约 ${minutes} 分钟后再试。`, retryAfter: lock.retryAfter });
517
+ return;
518
+ }
519
+ }
489
520
  if (!principal) {
490
521
  if (password !== effectivePassword) {
491
- recordFailedLogin(clientIp);
522
+ if (!appToken)
523
+ recordFailedPassword(clientIp);
492
524
  res.status(401).json({ error: "密码错误,请重试。" });
493
525
  return;
494
526
  }
495
527
  principal = { ...BROWSER_ADMIN_PRINCIPAL, scopes: [...BROWSER_ADMIN_PRINCIPAL.scopes] };
496
528
  }
497
- resetRateLimit(clientIp);
529
+ resetPasswordRateLimit(clientIp);
498
530
  const token = authService.createSession(principal);
499
531
  const cookieOpts = {
500
532
  httpOnly: true,
@@ -676,7 +708,11 @@ export async function startServer(config, configPath, options = {}) {
676
708
  registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
677
709
  // 任务管理与 Missions 一样只需登录:原生 connected-app 也要能列/建/派发。
678
710
  registerTaskRoutes(app, { storage, sessions: sessionRegistry, structured: structuredSessions, processes, config });
679
- registerAiTeamRoutes(app, { storage, runner: aiTeams });
711
+ registerAiTeamRoutes(app, {
712
+ storage,
713
+ runner: aiTeams,
714
+ notifyTeamChanged: (teamId) => notifyAiTeamRun({ kind: "ai-team-definition", teamId }),
715
+ });
680
716
  registerAttentionRoutes(app, { sessions: sessionRegistry, runner: aiTeams });
681
717
  registerAdminUpdateRoutes(app, {
682
718
  storage,
@@ -708,7 +744,7 @@ export async function startServer(config, configPath, options = {}) {
708
744
  registerClaudeHistoryRoutes(app, processes, storage);
709
745
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
710
746
  registerMissionRoutes(app, missions);
711
- registerUploadRoutes(app, processes);
747
+ registerUploadRoutes(app, sessionRegistry);
712
748
  app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
713
749
  const body = (req.body ?? {});
714
750
  const text = typeof body.text === "string" ? body.text : "";
@@ -800,8 +836,6 @@ export async function startServer(config, configPath, options = {}) {
800
836
  ...origin,
801
837
  }));
802
838
  recordRecentPath(storage, snapshot.cwd);
803
- // 会话一落地就挂到任务的看板卡片上,不等下一次看板列表的兜底同步。
804
- syncWorkspaceTaskToBoard(storage, snapshot.workspaceTaskId);
805
839
  res.status(201).json(toSessionDetailDTO(snapshot));
806
840
  }
807
841
  catch (error) {
@@ -885,6 +919,11 @@ export async function startServer(config, configPath, options = {}) {
885
919
  notifyAiTeamRun = (data) => {
886
920
  wsManager.emitEvent({ type: "notification", sessionId: "__system__", data });
887
921
  };
922
+ notifyAiTeamRunLive = (update) => {
923
+ wsManager.emitEvent({
924
+ type: "notification", sessionId: "__system__", data: { kind: "ai-team-step-live", ...update },
925
+ });
926
+ };
888
927
  // Re-attach structured CLI runs that kept going inside terminald while the
889
928
  // previous web process was down; fire-and-forget, failures are logged inside.
890
929
  void structuredSessions.recoverDetachedRuns();
package/dist/storage.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SessionSnapshot, ConversationTurn, SessionProvider, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceKind, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
2
- import type { AiTeam, AiTeamRun, AiTeamRunChatMarker, AiTeamRunStatus, AiTeamStep, CandidateFailureKind, StepDispatchInfo } from "./ai-team-types.js";
3
- import type { WandTaskAgentKind } from "./task-types.js";
2
+ import type { AiTeam, AiTeamRun, AiTeamRunChatMarker, AiTeamRunStatus, AiTeamStep, AiTeamStepSessionMarker, CandidateFailureKind, StepDispatchInfo } from "./ai-team-types.js";
3
+ import type { WandTask, WandTaskAgent, WandTaskAgentKind, WandTaskTitleSource } from "./task-types.js";
4
4
  import type { AgentActivityItem, Mission, MissionAttempt, MissionReviewComment, MissionReviewStatus, MissionStatus } from "./mission-types.js";
5
5
  import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
6
6
  /** `ai_team_runs.run_state_json`:本次运行的候选黑名单(§3.4 修正 B12 两层键值 + host-disabled)。 */
@@ -42,6 +42,21 @@ export interface PersistedAuthSession {
42
42
  }
43
43
  export declare function resolveDatabasePath(configPath: string): string;
44
44
  export declare function ensureDatabaseFile(dbPath: string): boolean;
45
+ interface CreateWandTaskInput {
46
+ workspaceId?: string | null;
47
+ workspaceTaskId?: string | null;
48
+ parentTaskId?: string | null;
49
+ title: string;
50
+ titleSource?: WandTaskTitleSource;
51
+ description?: string;
52
+ status?: WandTask["status"];
53
+ priority?: WandTask["priority"];
54
+ labels?: string[];
55
+ dueDate?: string | null;
56
+ milestoneId?: string | null;
57
+ agent?: WandTaskAgent | null;
58
+ }
59
+ type WandTaskPatch = Partial<Pick<WandTask, "workspaceId" | "workspaceTaskId" | "parentTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>;
45
60
  export declare class WandStorage {
46
61
  private readonly db;
47
62
  private readonly dbPath;
@@ -112,9 +127,11 @@ export declare class WandStorage {
112
127
  cwd: string;
113
128
  worktree: SessionSnapshot["worktree"];
114
129
  }>;
115
- /** 侧栏任务顺序:新创建在前。GET /api/tasks 按此顺序填充目录组。 */
130
+ /** Both task DTOs read one set of task metadata; containers own only local layout/worktree. */
116
131
  listWorkspaceTasks(workspaceId: string): WorkspaceTask[];
117
132
  getWorkspaceTask(id: string): WorkspaceTask | null;
133
+ private insertWorkspaceTask;
134
+ private taskMilestoneForWrite;
118
135
  createWorkspaceTask(input: {
119
136
  workspaceId: string;
120
137
  name: string;
@@ -122,6 +139,11 @@ export declare class WandStorage {
122
139
  cwd?: string | null;
123
140
  status?: WorkspaceTaskStatus;
124
141
  milestoneId?: string | null;
142
+ board?: {
143
+ titleSource?: WandTaskTitleSource;
144
+ description?: string;
145
+ parentTaskId?: string | null;
146
+ };
125
147
  }): WorkspaceTask;
126
148
  updateWorkspaceTask(id: string, patch: {
127
149
  name?: string;
@@ -130,6 +152,7 @@ export declare class WandStorage {
130
152
  milestoneId?: string | null;
131
153
  workspaceId?: string;
132
154
  }): void;
155
+ archiveWorkspaceTask(id: string): WandTask | null;
133
156
  saveWorkspaceTaskLayout(id: string, layout: TaskWindowLayout | null, expectedRevision?: number): {
134
157
  ok: true;
135
158
  layoutRevision: number;
@@ -144,27 +167,26 @@ export declare class WandStorage {
144
167
  cascade?: boolean;
145
168
  }): void;
146
169
  tasksAggregateFingerprint(): string;
170
+ /**
171
+ * 会话列表团队标记的廉价指纹:运行/步骤状态一变,`/api/tasks` 里的 `teamChat`/`teamStep`
172
+ * 就跟着变,客户端必须肯重拉。只看聚合值和最近若干条状态,不逐行拼整张表。
173
+ */
174
+ teamSessionFingerprint(): string;
147
175
  /** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
148
176
  listWandTasks(workspaceId?: string | null): import("./task-types.js").WandTask[];
149
177
  private mapWandTaskRow;
150
- getWandTask(id: string): import("./task-types.js").WandTask | null;
151
- getWandTaskByWorkspaceTaskId(workspaceTaskId: string): import("./task-types.js").WandTask | null;
152
- createWandTask(input: {
153
- workspaceId?: string | null;
154
- workspaceTaskId?: string | null;
155
- parentTaskId?: string | null;
156
- title: string;
157
- titleSource?: import("./task-types.js").WandTaskTitleSource;
158
- description?: string;
159
- status?: import("./task-types.js").WandTaskStatus;
160
- priority?: import("./task-types.js").WandTaskPriority;
161
- labels?: string[];
162
- dueDate?: string | null;
163
- milestoneId?: string | null;
164
- agent?: import("./task-types.js").WandTaskAgent | null;
165
- }): import("./task-types.js").WandTask;
166
- updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "parentTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
178
+ getWandTask(id: string): WandTask | null;
179
+ getWandTaskByWorkspaceTaskId(workspaceTaskId: string): WandTask | null;
180
+ private insertWandTask;
181
+ createWandTask(input: CreateWandTaskInput): WandTask;
182
+ /** Private canonical writer; callers own the transaction, never a reverse writer. */
183
+ private writeWandTask;
184
+ updateWandTask(id: string, patch: WandTaskPatch): WandTask | null;
185
+ /** Commit completion archives only the selected completed cards, atomically. */
186
+ archiveCompletedWandTasks(ids: readonly string[]): string[];
167
187
  deleteWandTask(id: string): void;
188
+ /** One-time id-based reconciliation; normal reads never create tasks or repair membership. */
189
+ private migrateTaskRecords;
168
190
  /**
169
191
  * 里程碑列表。传工作区 id 时返回「该工作区 + 全局(workspace_id IS NULL)」;
170
192
  * 未指定工作区(null / undefined / 空串)时返回全部。
@@ -224,12 +246,17 @@ export declare class WandStorage {
224
246
  listIterationPromptsByIds(ids: readonly string[]): import("./task-types.js").WandIterationPrompt[];
225
247
  /** 标记这些记录已被某次提交用掉,下一轮「上次提交以来」从这里开始。 */
226
248
  markIterationPromptsConsumed(ids: readonly string[], commit: string): number;
227
- /** 会话绑定的看板任务 id;没绑或已删除时返回 null。 */
249
+ /** Live membership has a single owner: command_sessions.workspace_task_id. */
228
250
  getWandTaskIdForSession(sessionId: string): string | null;
251
+ private legacyWandTaskSessionIds;
229
252
  listWandTaskSessionIds(taskId: string): string[];
230
253
  listBoundWandTaskSessionIds(): string[];
231
254
  bindWandTaskSession(taskId: string, sessionId: string): void;
232
255
  unbindWandTaskSession(taskId: string, sessionId: string): void;
256
+ /** Atomic exclusive ownership change; cwd, output and execution are untouched. */
257
+ moveSessionToWorkspaceTask(sessionId: string, taskId: string | null): void;
258
+ /** Metadata promotion happens on session creation/move, never on reads or checkpoints. */
259
+ private recordTaskSession;
233
260
  listGithubIssueBindings(owner: string, repo: string, issueNumber: number): Array<{
234
261
  sessionId: string;
235
262
  boundAt: string;
@@ -296,6 +323,12 @@ export declare class WandStorage {
296
323
  * 一次查询建整张表,供 /api/tasks 每请求一次构建;不逐会话查询。
297
324
  */
298
325
  listAiTeamRunChatMarkers(): Map<string, AiTeamRunChatMarker>;
326
+ /**
327
+ * 会话列表用的成员步骤标记:session_id → 这一步的身份。
328
+ * 和 `listAiTeamRunChatMarkers()` 一样一次查询建整张表,供 /api/tasks 每请求一次构建。
329
+ * 同一个会话被多个步骤复用时取 seq 最大的那一步(最近一次派发)。
330
+ */
331
+ listAiTeamStepSessionMarkers(): Map<string, AiTeamStepSessionMarker>;
299
332
  getAiTeamRun(id: string): AiTeamRun | null;
300
333
  listAiTeamRuns(filter?: {
301
334
  taskId?: string;