@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.
- package/README.md +21 -46
- package/dist/agent-dispatch.js +3 -2
- package/dist/ai-team-live.d.ts +25 -0
- package/dist/ai-team-live.js +135 -0
- package/dist/ai-team-prompts.d.ts +10 -6
- package/dist/ai-team-prompts.js +74 -41
- package/dist/ai-team-runner.d.ts +85 -3
- package/dist/ai-team-runner.js +336 -33
- package/dist/ai-team-types.d.ts +57 -2
- package/dist/ai-team-types.js +4 -0
- package/dist/build-info.json +4 -4
- package/dist/commit-task-archive.js +14 -18
- package/dist/config.js +14 -6
- package/dist/git-quick-commit.js +1 -1
- package/dist/middleware/rate-limit.d.ts +18 -4
- package/dist/middleware/rate-limit.js +55 -23
- package/dist/missions.js +0 -3
- package/dist/models.d.ts +6 -1
- package/dist/models.js +35 -0
- package/dist/path-repair.d.ts +6 -5
- package/dist/path-repair.js +3 -3
- package/dist/render-protocol.d.ts +1 -1
- package/dist/render-protocol.js +1 -1
- package/dist/server-ai-team-routes.d.ts +1 -0
- package/dist/server-ai-team-routes.js +28 -14
- package/dist/server-local-preview-routes.js +1 -1
- package/dist/server-session-routes.js +0 -3
- package/dist/server-settings-routes.d.ts +1 -1
- package/dist/server-settings-routes.js +3 -2
- package/dist/server-task-routes.js +16 -32
- package/dist/server-workspace-routes.js +31 -24
- package/dist/server.js +60 -21
- package/dist/storage.d.ts +54 -21
- package/dist/storage.js +485 -122
- package/dist/types.d.ts +7 -0
- package/dist/upload-routes.d.ts +2 -2
- package/dist/upload-routes.js +10 -3
- package/dist/wand-task-sync.d.ts +2 -34
- package/dist/wand-task-sync.js +0 -206
- package/dist/web-ui/asset-version.d.ts +5 -0
- package/dist/web-ui/asset-version.js +5 -0
- package/dist/web-ui/content/ai-teams.js +17 -8
- package/dist/web-ui/content/scripts.js +67 -66
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +0 -21
- package/dist/web-ui/embedded-assets.js +8 -7
- package/dist/web-ui/index.js +9 -17
- package/dist/web-ui/scripts.d.ts +3 -1
- package/dist/web-ui/scripts.js +52 -25
- package/dist/web-ui/styles.d.ts +2 -0
- package/dist/web-ui/styles.js +12 -0
- package/package.json +13 -12
- package/scripts/install-render-binary.js +2 -2
- 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 {
|
|
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.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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,
|
|
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
|
-
...(
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
515
|
-
const
|
|
516
|
-
const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions,
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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 {
|
|
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 ?? ">=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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,
|
|
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
|
-
/**
|
|
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):
|
|
151
|
-
getWandTaskByWorkspaceTaskId(workspaceTaskId: string):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
/**
|
|
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;
|