@co0ontty/wand 4.77.0 → 4.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +3 -3
- 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
package/dist/ai-team-types.d.ts
CHANGED
|
@@ -71,10 +71,12 @@ export interface AiTeam {
|
|
|
71
71
|
}
|
|
72
72
|
export type AiTeamRunStatus = "running" | "awaiting_approval" | "waiting_user" | "done" | "failed" | "stopped";
|
|
73
73
|
export declare const AI_TEAM_ACTIVE_RUN_STATUSES: readonly AiTeamRunStatus[];
|
|
74
|
+
/** 终态:不会再有 live 变化,runner 据此清掉该 run 的推送指纹,避免长跑服务按 run 数攒字符串。 */
|
|
75
|
+
export declare const AI_TEAM_TERMINAL_RUN_STATUSES: readonly AiTeamRunStatus[];
|
|
74
76
|
export interface AiTeamRun {
|
|
75
77
|
id: string;
|
|
76
78
|
teamId: string;
|
|
77
|
-
/**
|
|
79
|
+
/** 执行快照;仅 runner 在明确续跑/回复边界按规则刷新,展示层改名不写回这里。 */
|
|
78
80
|
team: AiTeam;
|
|
79
81
|
taskId: string;
|
|
80
82
|
objective: string;
|
|
@@ -103,6 +105,24 @@ export interface AiTeamRunChatMarker {
|
|
|
103
105
|
teamName: string;
|
|
104
106
|
memberCount: number;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 会话列表用的成员步骤标记:`ai_team_steps.session_id` → 这一步的身份(`AiTeamStepSessionMarker` 索引的 value)。
|
|
110
|
+
* 派发出来的会话没有人类起的标题,列表用 `title` 当短标题;`runFinished` 决定它算进行中还是历史。
|
|
111
|
+
* 成员名按当前团队定义投影,团队或成员被删时退回运行快照,和群聊署名同一套规则。
|
|
112
|
+
*/
|
|
113
|
+
export interface AiTeamStepSessionMarker {
|
|
114
|
+
runId: string;
|
|
115
|
+
stepId: string;
|
|
116
|
+
kind: AiTeamStepKind;
|
|
117
|
+
/** 这一步的任务标题,短、稳定。 */
|
|
118
|
+
title: string;
|
|
119
|
+
memberId: string;
|
|
120
|
+
memberName: string;
|
|
121
|
+
teamName: string;
|
|
122
|
+
stepStatus: AiTeamStepStatus;
|
|
123
|
+
runStatus: AiTeamRunStatus;
|
|
124
|
+
runFinished: boolean;
|
|
125
|
+
}
|
|
106
126
|
export type AiTeamStepKind = "leader" | "work";
|
|
107
127
|
export type AiTeamStepStatus = "queued" | "running" | "done" | "failed" | "skipped";
|
|
108
128
|
export interface AiTeamStep {
|
|
@@ -137,11 +157,46 @@ export interface AiTeamRunSummary extends AiTeamRun {
|
|
|
137
157
|
}
|
|
138
158
|
/** detail() 里群聊回合的截尾条数(§4.4):面板只给最近这一段,完整会话走「打开群聊」。 */
|
|
139
159
|
export declare const AI_TEAM_DETAIL_CHAT_TURNS = 200;
|
|
160
|
+
/** live 卡片文本的尾部保留上限(§4.9),超出部分回报 omittedChars 给前端显示「已省略前面 N 字」。 */
|
|
161
|
+
export declare const AI_TEAM_LIVE_TEXT_MAX_CHARS = 2000;
|
|
162
|
+
/**
|
|
163
|
+
* 一个正在干活的运行中步骤的实时快照(§4.9):Web 走 ai-team-step-live 推送,
|
|
164
|
+
* 移动端轮询 GET /api/ai-team-runs/:id/live,两边共用这个形状。
|
|
165
|
+
*/
|
|
166
|
+
export interface AiTeamLiveStep {
|
|
167
|
+
stepId: string;
|
|
168
|
+
seq: number;
|
|
169
|
+
memberId: string;
|
|
170
|
+
memberName: string;
|
|
171
|
+
/** 该步实际使用候选的 provider(署名口径同 §3.6)。 */
|
|
172
|
+
provider: WandTaskAgent["provider"];
|
|
173
|
+
/**
|
|
174
|
+
* 该步实际使用候选的模型 id(provider 的原生 model id,`"default"` = 跟随服务端默认);
|
|
175
|
+
* 取不到候选就不填。中文标签由客户端做。
|
|
176
|
+
*/
|
|
177
|
+
model?: string;
|
|
178
|
+
/** 该步实际使用候选的思考深度;同上,取不到不填。 */
|
|
179
|
+
thinkingEffort?: string;
|
|
180
|
+
sessionId: string;
|
|
181
|
+
state: AgentActivityState;
|
|
182
|
+
/** renderLiveStepText 的渲染结果,尾部保留、至多 AI_TEAM_LIVE_TEXT_MAX_CHARS 字。 */
|
|
183
|
+
text: string;
|
|
184
|
+
omittedChars: number;
|
|
185
|
+
updatedAt: string;
|
|
186
|
+
}
|
|
187
|
+
/** notifyLive 回调与 ai-team-step-live 通知的 payload(§4.9)。 */
|
|
188
|
+
export interface AiTeamLiveUpdate {
|
|
189
|
+
runId: string;
|
|
190
|
+
taskId: string;
|
|
191
|
+
steps: AiTeamLiveStep[];
|
|
192
|
+
}
|
|
140
193
|
export interface AiTeamRunDetail {
|
|
141
194
|
run: AiTeamRun;
|
|
142
195
|
steps: AiTeamStep[];
|
|
143
196
|
/** 运行中步骤所在会话的实时状态(等待授权 / 等待回答等)。 */
|
|
144
197
|
memberStates: Record<string, AgentActivityState>;
|
|
145
|
-
/**
|
|
198
|
+
/** 仅供展示:运行时成员顺序/职责保持快照,仍存在的成员按 id 取当前名字/头像。不得用于派工。 */
|
|
199
|
+
displayTeam?: AiTeam;
|
|
200
|
+
/** 群聊 relay 会话的原始消息;作者和正文是历史记录,不因改名而改写。 */
|
|
146
201
|
chatTurns: ConversationTurn[];
|
|
147
202
|
}
|
package/dist/ai-team-types.js
CHANGED
|
@@ -28,5 +28,9 @@ export function agentKey(agent) {
|
|
|
28
28
|
/** 上传头像在前端缩成小图后的上限(data URL 字符数)。 */
|
|
29
29
|
export const AI_TEAM_AVATAR_MAX_CHARS = 60_000;
|
|
30
30
|
export const AI_TEAM_ACTIVE_RUN_STATUSES = ["running", "awaiting_approval", "waiting_user"];
|
|
31
|
+
/** 终态:不会再有 live 变化,runner 据此清掉该 run 的推送指纹,避免长跑服务按 run 数攒字符串。 */
|
|
32
|
+
export const AI_TEAM_TERMINAL_RUN_STATUSES = ["done", "failed", "stopped"];
|
|
31
33
|
/** detail() 里群聊回合的截尾条数(§4.4):面板只给最近这一段,完整会话走「打开群聊」。 */
|
|
32
34
|
export const AI_TEAM_DETAIL_CHAT_TURNS = 200;
|
|
35
|
+
/** live 卡片文本的尾部保留上限(§4.9),超出部分回报 omittedChars 给前端显示「已省略前面 N 字」。 */
|
|
36
|
+
export const AI_TEAM_LIVE_TEXT_MAX_CHARS = 2000;
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "a1e4f594e3431de8d5d816fba8ed84c8c4611dbf",
|
|
3
|
+
"builtAt": "2026-09-29T11:29:24.761Z",
|
|
4
|
+
"version": "4.78.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -22,22 +22,18 @@ export function archiveCommitTasks(storage, session, entryIds) {
|
|
|
22
22
|
if (taskId)
|
|
23
23
|
related.add(taskId);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
archived.push(id);
|
|
40
|
-
}
|
|
41
|
-
return archived;
|
|
42
|
-
});
|
|
25
|
+
const completed = [];
|
|
26
|
+
for (const id of related) {
|
|
27
|
+
const task = storage.getWandTask(id);
|
|
28
|
+
if (task?.status !== "done")
|
|
29
|
+
continue;
|
|
30
|
+
// Project directory constrains selected history; standalone cards qualify via this session.
|
|
31
|
+
const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
|
|
32
|
+
const sameDirectory = !!workspace && workspace.kind !== "global"
|
|
33
|
+
&& !!directory && normalizeProjectCwd(workspace.cwd) === directory;
|
|
34
|
+
if (!sameDirectory && !(id === currentTaskId && (!workspace || workspace.kind === "global")))
|
|
35
|
+
continue;
|
|
36
|
+
completed.push(id);
|
|
37
|
+
}
|
|
38
|
+
return storage.archiveCompletedWandTasks(completed);
|
|
43
39
|
}
|
package/dist/config.js
CHANGED
|
@@ -804,14 +804,22 @@ function mergeWithDefaults(input) {
|
|
|
804
804
|
export function isExecutionMode(value) {
|
|
805
805
|
return value === "assist" || value === "agent" || value === "agent-max" || value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
|
|
806
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* 配置里的默认模型 id:`default` 是「跟随 CLI 默认」的哨兵值、不是模型 id,
|
|
809
|
+
* 历史设置里可能存过它(会一路传成 `--model default`),读到就当没配。
|
|
810
|
+
*/
|
|
811
|
+
function configuredModelId(value) {
|
|
812
|
+
const trimmed = (value ?? "").trim();
|
|
813
|
+
return trimmed === "default" ? "" : trimmed;
|
|
814
|
+
}
|
|
807
815
|
export function getProviderDefaultModels(config) {
|
|
808
816
|
return {
|
|
809
|
-
claude: (config.defaultModel
|
|
810
|
-
codex: (config.defaultCodexModel
|
|
811
|
-
opencode: (config.defaultOpenCodeModel
|
|
812
|
-
grok: (config.defaultGrokModel
|
|
813
|
-
qoder: (config.defaultQoderModel
|
|
814
|
-
pi: (config.defaultPiModel
|
|
817
|
+
claude: configuredModelId(config.defaultModel),
|
|
818
|
+
codex: configuredModelId(config.defaultCodexModel),
|
|
819
|
+
opencode: configuredModelId(config.defaultOpenCodeModel),
|
|
820
|
+
grok: configuredModelId(config.defaultGrokModel),
|
|
821
|
+
qoder: configuredModelId(config.defaultQoderModel),
|
|
822
|
+
pi: configuredModelId(config.defaultPiModel),
|
|
815
823
|
};
|
|
816
824
|
}
|
|
817
825
|
export function getDefaultModelForProvider(config, provider) {
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -768,7 +768,7 @@ function sanitizeSuggestedTag(value) {
|
|
|
768
768
|
return cleaned;
|
|
769
769
|
}
|
|
770
770
|
async function generateCommitMessageWithTag(cwd, language, ai = {}, options = {}) {
|
|
771
|
-
const { input
|
|
771
|
+
const { input } = await buildCommitPromptInput(cwd, options);
|
|
772
772
|
let latestTag;
|
|
773
773
|
try {
|
|
774
774
|
latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Login rate limiter — tracks failed attempts per IP.
|
|
2
|
+
* Login rate limiter — tracks failed admin-password attempts per IP.
|
|
3
3
|
* In-memory only; resets on process restart.
|
|
4
|
+
*
|
|
5
|
+
* Only human-typed passwords count. An app token is a machine-held credential the
|
|
6
|
+
* user cannot "type correctly", so a stale connect code must not lock out the whole
|
|
7
|
+
* shared IP bucket (that is what kept the web console answering 429).
|
|
4
8
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
export interface LoginLimitState {
|
|
10
|
+
lockedUntil: number;
|
|
11
|
+
/** Seconds to advertise via Retry-After; 0 when not locked. */
|
|
12
|
+
retryAfter: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function checkPasswordRateLimit(ip: string, now?: number): LoginLimitState | null;
|
|
15
|
+
/**
|
|
16
|
+
* Records one wrong admin password. Returns the lock that was just imposed, if any.
|
|
17
|
+
* A previous lock that has already expired starts a fresh window instead of being
|
|
18
|
+
* topped up by the first typo after it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function recordFailedPassword(ip: string, now?: number): LoginLimitState | null;
|
|
21
|
+
export declare function resetPasswordRateLimit(ip: string): void;
|
|
@@ -1,35 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Login rate limiter — tracks failed attempts per IP.
|
|
2
|
+
* Login rate limiter — tracks failed admin-password attempts per IP.
|
|
3
3
|
* In-memory only; resets on process restart.
|
|
4
|
+
*
|
|
5
|
+
* Only human-typed passwords count. An app token is a machine-held credential the
|
|
6
|
+
* user cannot "type correctly", so a stale connect code must not lock out the whole
|
|
7
|
+
* shared IP bucket (that is what kept the web console answering 429).
|
|
4
8
|
*/
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
9
|
+
const PASSWORD_WINDOW = 15 * 60 * 1000; // failures are counted over a sliding window
|
|
10
|
+
const PASSWORD_MAX = 10; // allowed failures inside the window
|
|
11
|
+
const LOCKOUT = 15 * 60 * 1000; // fixed cooldown once the window is exhausted
|
|
12
|
+
const passwordAttempts = new Map();
|
|
13
|
+
function prune(attempts, now) {
|
|
14
|
+
const cutoff = now - PASSWORD_WINDOW;
|
|
15
|
+
let start = 0;
|
|
16
|
+
while (start < attempts.length && attempts[start] <= cutoff)
|
|
17
|
+
start++;
|
|
18
|
+
return start === 0 ? attempts : attempts.slice(start);
|
|
15
19
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function lockedState(lockedUntil, now) {
|
|
21
|
+
return { lockedUntil, retryAfter: Math.ceil((lockedUntil - now) / 1000) };
|
|
22
|
+
}
|
|
23
|
+
export function checkPasswordRateLimit(ip, now = Date.now()) {
|
|
24
|
+
const record = passwordAttempts.get(ip);
|
|
25
|
+
if (!record)
|
|
26
|
+
return null;
|
|
27
|
+
if (now < record.lockedUntil)
|
|
28
|
+
return lockedState(record.lockedUntil, now);
|
|
29
|
+
record.attempts = prune(record.attempts, now);
|
|
30
|
+
if (!record.attempts.length)
|
|
31
|
+
passwordAttempts.delete(ip);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Records one wrong admin password. Returns the lock that was just imposed, if any.
|
|
36
|
+
* A previous lock that has already expired starts a fresh window instead of being
|
|
37
|
+
* topped up by the first typo after it.
|
|
38
|
+
*/
|
|
39
|
+
export function recordFailedPassword(ip, now = Date.now()) {
|
|
40
|
+
const existing = passwordAttempts.get(ip);
|
|
41
|
+
// A lock that has already expired must not leave its failures behind to tip the new
|
|
42
|
+
// window over the limit on the first typo; `lockedUntil` 0 means "never locked".
|
|
43
|
+
const expiredLock = !!existing && existing.lockedUntil > 0 && now >= existing.lockedUntil;
|
|
44
|
+
const record = !existing || expiredLock ? { attempts: [], lockedUntil: 0 } : existing;
|
|
45
|
+
record.attempts = prune(record.attempts, now);
|
|
46
|
+
record.attempts.push(now);
|
|
47
|
+
let lock = null;
|
|
48
|
+
if (record.attempts.length >= PASSWORD_MAX) {
|
|
49
|
+
record.lockedUntil = now + LOCKOUT;
|
|
50
|
+
record.attempts = [];
|
|
51
|
+
lock = lockedState(record.lockedUntil, now);
|
|
22
52
|
}
|
|
23
|
-
record
|
|
53
|
+
passwordAttempts.set(ip, record);
|
|
54
|
+
return lock;
|
|
24
55
|
}
|
|
25
|
-
export function
|
|
26
|
-
|
|
56
|
+
export function resetPasswordRateLimit(ip) {
|
|
57
|
+
passwordAttempts.delete(ip);
|
|
27
58
|
}
|
|
28
59
|
function cleanupRateLimiter() {
|
|
29
60
|
const now = Date.now();
|
|
30
|
-
for (const [ip, record] of
|
|
31
|
-
|
|
32
|
-
|
|
61
|
+
for (const [ip, record] of passwordAttempts.entries()) {
|
|
62
|
+
record.attempts = prune(record.attempts, now);
|
|
63
|
+
if (!record.attempts.length && now >= record.lockedUntil) {
|
|
64
|
+
passwordAttempts.delete(ip);
|
|
33
65
|
}
|
|
34
66
|
}
|
|
35
67
|
}
|
package/dist/missions.js
CHANGED
|
@@ -4,7 +4,6 @@ import path from "node:path";
|
|
|
4
4
|
import { buildMissionDiff } from "./mission-diff.js";
|
|
5
5
|
import { recordIterationPromptForTask } from "./iteration-log.js";
|
|
6
6
|
import { isSessionProvider } from "./session-provider.js";
|
|
7
|
-
import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
|
|
8
7
|
const MAX_ATTEMPTS = 6;
|
|
9
8
|
function nowIso() {
|
|
10
9
|
return new Date().toISOString();
|
|
@@ -334,8 +333,6 @@ export class Missions {
|
|
|
334
333
|
sessionSource: "automation",
|
|
335
334
|
automationId: mission.id,
|
|
336
335
|
});
|
|
337
|
-
// 关联任务的派发会话立刻落到任务卡片上,不等下一次看板列表的兜底同步。
|
|
338
|
-
syncWorkspaceTaskToBoard(this.storage, mission.taskId);
|
|
339
336
|
attempt = {
|
|
340
337
|
...attempt,
|
|
341
338
|
sessionId: session.id,
|
package/dist/models.d.ts
CHANGED
|
@@ -64,7 +64,12 @@ export interface ModelCache {
|
|
|
64
64
|
}
|
|
65
65
|
/** 探针失败时的已知档位。下一次 CLI 成功应答会把它换掉。 */
|
|
66
66
|
export declare const FALLBACK_PROVIDER_THINKING: ProviderThinkingEfforts;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 目录里 `default` 项(「不传 --model」那一项)的标签补上服务端配置的默认模型名:
|
|
69
|
+
* CLI 自己报的文案(「跟随 Claude Code 默认」)看不出真正会跑哪个模型,两端选择器只能显示占位。
|
|
70
|
+
* 没配置、列表里没有这一项、或标签里已经写了这个 id 时原样返回(引用不变,便于上游比对缓存)。
|
|
71
|
+
*/
|
|
72
|
+
export declare function withConfiguredDefaultModelLabels<T extends object>(catalog: T, defaults: Readonly<Record<string, string>>): T;
|
|
68
73
|
export interface ModelCatalogSnapshot extends ModelCache {
|
|
69
74
|
/** SHA-256 of the catalog excluding `refreshedAt`. Changes only with content. */
|
|
70
75
|
revision: string;
|
package/dist/models.js
CHANGED
|
@@ -79,6 +79,41 @@ function cloneThinkingEfforts(efforts) {
|
|
|
79
79
|
pi: copy(efforts.pi),
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
/** Immutable-looking snapshot returned to API clients. */
|
|
83
|
+
/** provider → 目录里的模型列表字段(与 `/api/models` 的 payload 同源)。 */
|
|
84
|
+
const PROVIDER_MODEL_LIST_FIELDS = {
|
|
85
|
+
claude: "models",
|
|
86
|
+
codex: "codexModels",
|
|
87
|
+
opencode: "opencodeModels",
|
|
88
|
+
grok: "grokModels",
|
|
89
|
+
qoder: "qoderModels",
|
|
90
|
+
pi: "piModels",
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* 目录里 `default` 项(「不传 --model」那一项)的标签补上服务端配置的默认模型名:
|
|
94
|
+
* CLI 自己报的文案(「跟随 Claude Code 默认」)看不出真正会跑哪个模型,两端选择器只能显示占位。
|
|
95
|
+
* 没配置、列表里没有这一项、或标签里已经写了这个 id 时原样返回(引用不变,便于上游比对缓存)。
|
|
96
|
+
*/
|
|
97
|
+
export function withConfiguredDefaultModelLabels(catalog, defaults) {
|
|
98
|
+
let next = null;
|
|
99
|
+
for (const [provider, field] of Object.entries(PROVIDER_MODEL_LIST_FIELDS)) {
|
|
100
|
+
const list = catalog[field];
|
|
101
|
+
const configured = (defaults[provider] ?? "").trim();
|
|
102
|
+
if (!configured || !Array.isArray(list))
|
|
103
|
+
continue;
|
|
104
|
+
const at = list.findIndex((entry) => (!!entry && typeof entry === "object" && entry.id === "default"));
|
|
105
|
+
if (at < 0)
|
|
106
|
+
continue;
|
|
107
|
+
const entry = list[at];
|
|
108
|
+
if (String(entry.label ?? "").includes(configured))
|
|
109
|
+
continue;
|
|
110
|
+
const updated = [...list];
|
|
111
|
+
updated[at] = { ...entry, label: `跟随服务端默认(${configured})` };
|
|
112
|
+
next ??= { ...catalog };
|
|
113
|
+
next[field] = updated;
|
|
114
|
+
}
|
|
115
|
+
return (next ?? catalog);
|
|
116
|
+
}
|
|
82
117
|
function cloneModels(models) {
|
|
83
118
|
return models.map((model) => ({
|
|
84
119
|
...model,
|
package/dist/path-repair.d.ts
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* 修复分两层(都在 startServer() 开头跑):
|
|
16
16
|
* 1. repairRuntimePath()(同步、cheap):扫常见工具链 bin 目录,把存在但 PATH
|
|
17
17
|
* 里缺的"追加"到 process.env.PATH 末尾。
|
|
18
|
-
* 2. deepRepairRuntimePath()
|
|
19
|
-
* PATH
|
|
20
|
-
*
|
|
18
|
+
* 2. deepRepairRuntimePath()(异步、贵一点):起一个交互式 login shell 拉用户实际的
|
|
19
|
+
* PATH(与 provider PTY 的 -lic 一致,覆盖 ~/.bashrc、~/.zshrc、
|
|
20
|
+
* nvm/fnm/volta 的 shell-init 等动态逻辑),把里面 PATH 里我们还没的目录
|
|
21
|
+
* "前插"到 process.env.PATH 前面。这
|
|
21
22
|
* 是真正能修好"unit 里 PATH 太旧、用户 shell 里却好好的"这种场景的关键。
|
|
22
23
|
*
|
|
23
24
|
* 设计决策:
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* - 用 path.delimiter:Windows 用 `;`,POSIX 用 `:`,跨平台安全。
|
|
28
29
|
* - 不重写 service unit:那需要 sudo,且语义太重;只在 install.sh 主动调
|
|
29
30
|
* `wand service:install` 时才重新烧 PATH。
|
|
30
|
-
* -
|
|
31
|
+
* - provider PTY 用 -lic,PATH 探测也用 -lic(不是 -lc);4 秒超时,失败走同步路径。
|
|
31
32
|
*
|
|
32
33
|
* WAND_PATH_REPAIR_DISABLE=1 可以彻底关掉同步那层(极端情况下用户想完全控制 PATH 时用)。
|
|
33
34
|
* WAND_PATH_REPAIR_DEEP_DISABLE=1 只关掉 login shell 探测,但保留同步追加。
|
|
@@ -57,7 +58,7 @@ export declare function repairRuntimePath(): PathRepairResult;
|
|
|
57
58
|
* 这种动态注入的 PATH。
|
|
58
59
|
*
|
|
59
60
|
* 行为:
|
|
60
|
-
* - 跑 `${shell} -
|
|
61
|
+
* - 跑 `${shell} -lic '...'` 拉 $PATH 和 command -v claude/codex/opencode(4s 超时)
|
|
61
62
|
* - 按 login shell 的 PATH 顺序重排 process.env.PATH,再把仅存在于 service 的
|
|
62
63
|
* 目录追加回去。不能只前插新增目录:同一个 CLI 同时装在 nvm 和 Homebrew 时,
|
|
63
64
|
* 两个目录本来都存在,旧实现不会重排,structured 与 PTY 会命中不同版本。
|
package/dist/path-repair.js
CHANGED
|
@@ -154,7 +154,7 @@ export function repairRuntimePath() {
|
|
|
154
154
|
* 这种动态注入的 PATH。
|
|
155
155
|
*
|
|
156
156
|
* 行为:
|
|
157
|
-
* - 跑 `${shell} -
|
|
157
|
+
* - 跑 `${shell} -lic '...'` 拉 $PATH 和 command -v claude/codex/opencode(4s 超时)
|
|
158
158
|
* - 按 login shell 的 PATH 顺序重排 process.env.PATH,再把仅存在于 service 的
|
|
159
159
|
* 目录追加回去。不能只前插新增目录:同一个 CLI 同时装在 nvm 和 Homebrew 时,
|
|
160
160
|
* 两个目录本来都存在,旧实现不会重排,structured 与 PTY 会命中不同版本。
|
|
@@ -254,7 +254,7 @@ function pickProbeShell(configured) {
|
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
|
-
*
|
|
257
|
+
* 用与 PTY provider 相同的交互式 login shell 跑最小脚本,拿到实际的 PATH 和 CLI 路径。
|
|
258
258
|
* 用 \x1f(ASCII Unit Separator)作字段分隔符避免和路径里的字符冲突。
|
|
259
259
|
*/
|
|
260
260
|
function probeLoginShell(shell, timeoutMs) {
|
|
@@ -265,7 +265,7 @@ function probeLoginShell(shell, timeoutMs) {
|
|
|
265
265
|
`printf 'GROK\\x1f%s\\n' "$(command -v grok 2>/dev/null)"; ` +
|
|
266
266
|
`printf 'QODERCLI\\x1f%s\\n' "$(command -v qodercli 2>/dev/null)"`;
|
|
267
267
|
return new Promise((resolve, reject) => {
|
|
268
|
-
const child = spawn(shell, ["-
|
|
268
|
+
const child = spawn(shell, ["-lic", script], {
|
|
269
269
|
env: {
|
|
270
270
|
...process.env,
|
|
271
271
|
// 防止 PROMPT_COMMAND / PS1 等钩子往 stdout 喷东西干扰解析
|
package/dist/render-protocol.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
/**
|
|
6
6
|
* Render 协议 v1 的 TS 镜像。Rust 单一真源在
|
|
7
7
|
* `render/crates/wand-render-protocol/src/lib.rs`,契约文档在
|
|
8
|
-
* `render/docs/render-protocol.md
|
|
8
|
+
* `render/docs/render-protocol.md`。
|
|
9
9
|
*
|
|
10
10
|
* 两侧常量与字段名必须逐一对齐;任一侧改动都要同时提升版本号并更新文档。
|
|
11
11
|
*/
|
|
@@ -7,7 +7,6 @@ import { defaultMilestoneIdForWrite, scopedMilestoneId } from "./milestone-scope
|
|
|
7
7
|
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
8
8
|
import { parseTaskAgent } from "./server-task-routes.js";
|
|
9
9
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
10
|
-
import { ensureWorkspaceTaskForBoardTask } from "./wand-task-sync.js";
|
|
11
10
|
const NOT_FOUND = /不存在/;
|
|
12
11
|
/** 团队页「直接开工」自动建卡的标记(§4.2 / Q3):看板靠它区分团队卡与手写卡。 */
|
|
13
12
|
export const TEAM_DIRECT_LABEL = "team_direct";
|
|
@@ -134,6 +133,14 @@ export function parseAiTeamInput(value, existing, now) {
|
|
|
134
133
|
}
|
|
135
134
|
export function registerAiTeamRoutes(app, deps) {
|
|
136
135
|
const { storage, runner } = deps;
|
|
136
|
+
const notifyTeam = (teamId) => {
|
|
137
|
+
try {
|
|
138
|
+
deps.notifyTeamChanged?.(teamId);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error("[AiTeam] notify definition failed:", getErrorMessage(error));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
137
144
|
app.get("/api/ai-teams", (_req, res) => {
|
|
138
145
|
res.json(storage.listAiTeams());
|
|
139
146
|
});
|
|
@@ -154,6 +161,7 @@ export function registerAiTeamRoutes(app, deps) {
|
|
|
154
161
|
throw new Error("团队不存在。");
|
|
155
162
|
const team = parseAiTeamInput(req.body, existing, new Date().toISOString());
|
|
156
163
|
storage.saveAiTeam(team);
|
|
164
|
+
notifyTeam(team.id);
|
|
157
165
|
res.json(team);
|
|
158
166
|
}
|
|
159
167
|
catch (error) {
|
|
@@ -162,6 +170,7 @@ export function registerAiTeamRoutes(app, deps) {
|
|
|
162
170
|
});
|
|
163
171
|
app.delete("/api/ai-teams/:id", (req, res) => {
|
|
164
172
|
storage.deleteAiTeam(req.params.id);
|
|
173
|
+
notifyTeam(req.params.id);
|
|
165
174
|
res.json({ ok: true });
|
|
166
175
|
});
|
|
167
176
|
/** 团队详情:团队对象 + 该团队的运行摘要(修正 B14,Android AiTeamDetailScreen 与 Web 面板共用)。 */
|
|
@@ -203,19 +212,15 @@ export function registerAiTeamRoutes(app, deps) {
|
|
|
203
212
|
const preferredMember = team.members.find((member) => member.isLeader) ?? team.members[0];
|
|
204
213
|
const agent = preferredMember ? memberAgents(preferredMember)[0] ?? null : null;
|
|
205
214
|
const milestoneId = defaultMilestoneIdForWrite(storage);
|
|
206
|
-
const task = storage.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
agent,
|
|
216
|
-
});
|
|
217
|
-
ensureWorkspaceTaskForBoardTask(storage, card);
|
|
218
|
-
return storage.getWandTask(card.id);
|
|
215
|
+
const task = storage.createWandTask({
|
|
216
|
+
workspaceId: workspace.id,
|
|
217
|
+
title: provisionalTaskTitleFromDescription(note) || team.name,
|
|
218
|
+
description: note,
|
|
219
|
+
// 团队开工即进行中(§4.2 的 "processing" 对应看板列 doing;runner.start 不再回退它)。
|
|
220
|
+
status: "doing",
|
|
221
|
+
labels: [TEAM_DIRECT_LABEL],
|
|
222
|
+
milestoneId: scopedMilestoneId(storage, milestoneId, workspace.id) ?? milestoneId,
|
|
223
|
+
agent,
|
|
219
224
|
});
|
|
220
225
|
try {
|
|
221
226
|
const detail = await runner.start({ teamId: team.id, taskId: task.id, note });
|
|
@@ -264,6 +269,15 @@ export function registerAiTeamRoutes(app, deps) {
|
|
|
264
269
|
sendTeamError(res, error);
|
|
265
270
|
}
|
|
266
271
|
});
|
|
272
|
+
/** 运行中步骤的 live 文本(§4.9):Web 走 ai-team-step-live 推送,移动端轻量轮询这个端点。 */
|
|
273
|
+
app.get("/api/ai-team-runs/:id/live", (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
res.json({ runId: req.params.id, steps: runner.live(req.params.id) });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
sendTeamError(res, error);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
267
281
|
const action = (path, handler) => {
|
|
268
282
|
app.post(path, asyncRoute(async (req, res) => {
|
|
269
283
|
try {
|
|
@@ -71,7 +71,7 @@ function proxyLocation(value, target) {
|
|
|
71
71
|
* uploads and other content types that Express does not need to inspect.
|
|
72
72
|
*/
|
|
73
73
|
export function registerLocalPreviewRoutes(app, dependencies) {
|
|
74
|
-
const handler = asyncRoute(async (req, res
|
|
74
|
+
const handler = asyncRoute(async (req, res) => {
|
|
75
75
|
const target = parseLocalPreviewTarget(String(req.params.host ?? ""), String(req.params.port ?? ""), req.url || "/");
|
|
76
76
|
if (!target) {
|
|
77
77
|
res.status(400).json({ error: "只允许预览本机(localhost)HTTP 服务。" });
|
|
@@ -19,7 +19,6 @@ import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
|
|
|
19
19
|
import { addHiddenSessionIds, removeHiddenSessionIds } from "./session-registry.js";
|
|
20
20
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
21
21
|
import { asRecord, isStructuredImagePart } from "./structured-content.js";
|
|
22
|
-
import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
|
|
23
22
|
import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
|
|
24
23
|
import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
|
|
25
24
|
export function parseExecutionMode(value, fallback) {
|
|
@@ -485,8 +484,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
485
484
|
...origin,
|
|
486
485
|
});
|
|
487
486
|
onSessionCreated?.(snapshot.cwd);
|
|
488
|
-
// 会话一落地就挂到任务的看板卡片上,不等下一次看板列表的兜底同步。
|
|
489
|
-
syncWorkspaceTaskToBoard(storage, snapshot.workspaceTaskId);
|
|
490
487
|
const prompt = body.prompt?.trim();
|
|
491
488
|
if (prompt) {
|
|
492
489
|
if (body.respondImmediately === true) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Express, Request, RequestHandler } from "express";
|
|
2
2
|
import { type BuildInfo } from "./build-info.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ModelCatalogService } from "./models.js";
|
|
4
4
|
import { type RuntimeConfigState } from "./runtime-config.js";
|
|
5
5
|
import type { WandStorage } from "./storage.js";
|
|
6
6
|
import type { WandConfig } from "./types.js";
|
|
@@ -6,6 +6,7 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
6
6
|
import { asyncRoute } from "./express-async.js";
|
|
7
7
|
import { sendRouteError } from "./server-request.js";
|
|
8
8
|
import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, validateCommitAiConfig, writePreferenceToStorage, } from "./config.js";
|
|
9
|
+
import { withConfiguredDefaultModelLabels } from "./models.js";
|
|
9
10
|
import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
|
|
10
11
|
import { callSystemAiText, discoverCliSystemAiConfigs, mergeSystemAiConfigs, normalizeSystemAiConfig, } from "./system-ai.js";
|
|
11
12
|
function systemAiRouteIdentity(profile) {
|
|
@@ -399,7 +400,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
399
400
|
const cached = deps.modelCatalog.snapshot();
|
|
400
401
|
const defaults = getProviderDefaultModels(config);
|
|
401
402
|
res.json({
|
|
402
|
-
...cached,
|
|
403
|
+
...withConfiguredDefaultModelLabels(cached, defaults),
|
|
403
404
|
defaultModel: defaults.claude,
|
|
404
405
|
defaultCodexModel: defaults.codex,
|
|
405
406
|
defaultOpenCodeModel: defaults.opencode,
|
|
@@ -414,7 +415,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
414
415
|
const refreshed = await deps.modelCatalog.refresh({ verifyClaudeCandidates: true });
|
|
415
416
|
const defaults = getProviderDefaultModels(config);
|
|
416
417
|
res.json({
|
|
417
|
-
...refreshed,
|
|
418
|
+
...withConfiguredDefaultModelLabels(refreshed, defaults),
|
|
418
419
|
defaultModel: defaults.claude,
|
|
419
420
|
defaultCodexModel: defaults.codex,
|
|
420
421
|
defaultOpenCodeModel: defaults.opencode,
|