@co0ontty/wand 4.89.2 → 4.89.5
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 +12 -1
- package/browser-extension/icons/wand-128.png +0 -0
- package/browser-extension/icons/wand-16.png +0 -0
- package/browser-extension/icons/wand-32.png +0 -0
- package/browser-extension/icons/wand-48.png +0 -0
- package/dist/ai-team-employee-binding.js +2 -1
- package/dist/archive-policy.d.ts +17 -0
- package/dist/archive-policy.js +8 -0
- package/dist/build-info.json +3 -3
- package/dist/fixed-employee-avatar.d.ts +6 -0
- package/dist/fixed-employee-avatar.js +4 -0
- package/dist/npm-update-utils.js +14 -12
- package/dist/plush-avatar.d.ts +2 -1
- package/dist/plush-avatar.js +4 -2
- package/dist/retention.js +8 -10
- package/dist/server-employee-routes.js +9 -3
- package/dist/server-session-routes.js +27 -23
- package/dist/server-task-routes.js +10 -0
- package/dist/server-update-routes.js +12 -3
- package/dist/server-workspace-routes.js +12 -0
- package/dist/server.js +4 -4
- package/dist/session-registry.js +7 -0
- package/dist/storage.js +12 -4
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +43 -9
- package/dist/task-archive-policy.d.ts +4 -0
- package/dist/task-archive-policy.js +16 -0
- package/dist/tui/commands.d.ts +1 -1
- package/dist/tui/commands.js +6 -3
- package/dist/web-ui/chat-message-meta.d.ts +3 -1
- package/dist/web-ui/chat-message-meta.js +21 -1
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/licenses/codex-LICENSE.txt +21 -0
- package/dist/web-ui/content/plush-avatar.js +34 -34
- package/dist/web-ui/content/scripts.js +304 -400
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.js +8 -8
- package/dist/web-ui/markdown.js +3 -1
- package/dist/web-ui/provider-identity.d.ts +1 -1
- package/dist/web-ui/provider-identity.js +4 -3
- package/package.json +1 -1
- package/dist/commit-task-archive.d.ts +0 -8
- package/dist/commit-task-archive.js +0 -65
package/README.md
CHANGED
|
@@ -233,7 +233,7 @@ npm install -g @google/gemini-cli@latest
|
|
|
233
233
|
|
|
234
234
|
#### 交互体验
|
|
235
235
|
|
|
236
|
-
- **结构化对话** —
|
|
236
|
+
- **结构化对话** — 代码块语法高亮、工具调用折叠/展开、多问题分组渲染、输入区展示最近一轮 Token 用量与费用
|
|
237
237
|
- **个性化角色** — 像素风猫咪头像(赛博虎妞 / 勤劳初二),支持自定义对话角色名称
|
|
238
238
|
- **消息排队** — 在 AI 思考时可继续输入,消息自动排队发送
|
|
239
239
|
- **文件浏览器** — 内置路径浏览和搜索功能
|
|
@@ -245,6 +245,17 @@ npm install -g @google/gemini-cli@latest
|
|
|
245
245
|
- **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
|
|
246
246
|
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
247
247
|
- **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
|
|
248
|
+
|
|
249
|
+
Web 设置中的「Beta 通道」决定服务端 npm 更新来源:关闭时仅检查
|
|
250
|
+
`@co0ontty/wand@latest`,开启时检查 `@co0ontty/wand@beta`;「自动更新 Web 服务」
|
|
251
|
+
独立决定是否自动安装。检查后安装锁定具体版本,避免 dist-tag 在安装期间移动。
|
|
252
|
+
关闭 Beta 不会自动降级;需要立即返回较旧正式版时,点击「更新或重新安装」。
|
|
253
|
+
|
|
254
|
+
正式 npm 发布由 `vX.Y.Z` Git tag 触发,只写入 `latest`,拒绝预发布版本。
|
|
255
|
+
master 的 Beta 发布只写入 `beta`,版本为下一补丁号加
|
|
256
|
+
`-beta.<构建序号>.<重试序号>.g<提交>`,例如 `4.89.5-beta.100.1.gabcdef0`。
|
|
257
|
+
版本基数同时检查正式 Git tags 和 npm latest,兼容未推送 Git 的本地正式发布。
|
|
258
|
+
Beta 尚未发布、通道版本无效或 npm 暂不可达时,不自动安装其他通道。
|
|
248
259
|
- **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI、Pi 和 Gemini CLI,也可开启自动更新
|
|
249
260
|
|
|
250
261
|
### 配置
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -31,7 +31,8 @@ export function requireTeamEmployee(source, id) {
|
|
|
31
31
|
}
|
|
32
32
|
// The default partner's read projection includes short-term habits; they are not team snapshot data.
|
|
33
33
|
return isDefaultSiliconEmployee(employee)
|
|
34
|
-
? defaultEmployeeDefinition(employee.agents, employee.updatedAt, source.getSiliconEmployeeDefinition?.(id) ?? employee, null)
|
|
34
|
+
? { ...defaultEmployeeDefinition(employee.agents, employee.updatedAt, source.getSiliconEmployeeDefinition?.(id) ?? employee, null),
|
|
35
|
+
avatar: employee.avatar }
|
|
35
36
|
: employee;
|
|
36
37
|
}
|
|
37
38
|
/** Resolve once per new run. Team duty/role/leadership remain local; no knowledge is read. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared execution facts for archive controls and server enforcement. An idle PTY shell is safe. */
|
|
2
|
+
export interface ArchiveSessionFacts {
|
|
3
|
+
sessionKind?: string;
|
|
4
|
+
runner?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
ptyBusy?: boolean;
|
|
7
|
+
ptyCommandRunning?: boolean;
|
|
8
|
+
inFlight?: boolean;
|
|
9
|
+
structuredState?: {
|
|
10
|
+
inFlight?: boolean;
|
|
11
|
+
} | null;
|
|
12
|
+
permissionBlocked?: boolean;
|
|
13
|
+
pendingEscalation?: unknown;
|
|
14
|
+
queuedMessages?: readonly unknown[];
|
|
15
|
+
queuedMessageCount?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function isArchiveBlocked(session: ArchiveSessionFacts): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function isArchiveBlocked(session) {
|
|
2
|
+
if (session.ptyBusy || session.ptyCommandRunning || session.inFlight || session.structuredState?.inFlight
|
|
3
|
+
|| session.permissionBlocked || session.pendingEscalation || session.queuedMessages?.length || session.queuedMessageCount)
|
|
4
|
+
return true;
|
|
5
|
+
if (["thinking", "initializing", "starting", "reconnecting", "permission-blocked", "waiting-input", "waiting_input"].includes(session.status ?? ""))
|
|
6
|
+
return true;
|
|
7
|
+
return session.status === "running" && (session.sessionKind === "structured" || session.runner === "claude-cli-print");
|
|
8
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.89.
|
|
2
|
+
"commit": "ec13fefa558fb2d1236d377d46e6c3152e4eb79b",
|
|
3
|
+
"builtAt": "2026-10-10T07:01:42.094Z",
|
|
4
|
+
"version": "4.89.5",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -12,6 +12,12 @@ export declare function fixedEmployeeAvatar(identity: {
|
|
|
12
12
|
id?: string | null;
|
|
13
13
|
systemKey?: string | null;
|
|
14
14
|
}): string | null;
|
|
15
|
+
/** One read-only avatar value for API projections and client drafts; never migrate saved data while displaying it. */
|
|
16
|
+
export declare function employeeAvatarValue(identity: {
|
|
17
|
+
id?: string | null;
|
|
18
|
+
systemKey?: string | null;
|
|
19
|
+
avatar?: string | null;
|
|
20
|
+
}): string;
|
|
15
21
|
/** The avatar exception does not change any other built-in employee's edit policy. */
|
|
16
22
|
export declare function isFixedAvatarEmployee(identity: {
|
|
17
23
|
id?: string | null;
|
|
@@ -7,6 +7,10 @@ export const FIXED_EMPLOYEE_AVATARS = [
|
|
|
7
7
|
export function fixedEmployeeAvatar(identity) {
|
|
8
8
|
return FIXED_EMPLOYEE_AVATARS.find((entry) => entry.id === identity.id && entry.systemKey === identity.systemKey)?.avatar ?? null;
|
|
9
9
|
}
|
|
10
|
+
/** One read-only avatar value for API projections and client drafts; never migrate saved data while displaying it. */
|
|
11
|
+
export function employeeAvatarValue(identity) {
|
|
12
|
+
return fixedEmployeeAvatar(identity) ?? identity.avatar ?? "";
|
|
13
|
+
}
|
|
10
14
|
/** The avatar exception does not change any other built-in employee's edit policy. */
|
|
11
15
|
export function isFixedAvatarEmployee(identity) {
|
|
12
16
|
return fixedEmployeeAvatar(identity) !== null;
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -45,16 +45,19 @@ export function getInstallSpecForChannel(channel) {
|
|
|
45
45
|
function cleanVersion(value) {
|
|
46
46
|
return value.trim().replace(/^v/, "");
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
function acceptedVersion(version, channel) {
|
|
49
|
+
const value = version ? cleanVersion(version) : "";
|
|
50
|
+
// Never turn a prerelease into a nonexistent stable version or install a tag
|
|
51
|
+
// that changed between checking and installing.
|
|
52
|
+
const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
53
|
+
const beta = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
54
|
+
return (channel === "stable" ? stable : beta).test(value) ? value : null;
|
|
55
|
+
}
|
|
56
|
+
function computeUpdateAvailable(currentVersion, latestVersion) {
|
|
52
57
|
if (!latestVersion)
|
|
53
58
|
return false;
|
|
54
59
|
const current = cleanVersion(currentVersion);
|
|
55
|
-
const target =
|
|
56
|
-
? getStableTagVersion(latestVersion)
|
|
57
|
-
: cleanVersion(latestVersion);
|
|
60
|
+
const target = latestVersion;
|
|
58
61
|
// Invalid/manual labels cannot be ordered safely, so offer the selected
|
|
59
62
|
// dist-tag. Valid versions only update forward. In particular, Wand's local
|
|
60
63
|
// X.Y.Z-debug.* build represents code after X.Y.Z and must not be replaced
|
|
@@ -64,15 +67,14 @@ function computeUpdateAvailable(currentVersion, latestVersion, channel) {
|
|
|
64
67
|
return compareWandInstallOrder(target, current) > 0;
|
|
65
68
|
}
|
|
66
69
|
export function buildPackageUpdateInfo(currentVersion, channel, latestVersion) {
|
|
67
|
-
const latest = latestVersion
|
|
68
|
-
const stableLatest = channel === "stable" && latest ? getStableTagVersion(latest) : latest;
|
|
70
|
+
const latest = acceptedVersion(latestVersion, channel);
|
|
69
71
|
return {
|
|
70
72
|
channel,
|
|
71
73
|
current: cleanVersion(currentVersion),
|
|
72
|
-
latest
|
|
73
|
-
updateAvailable: computeUpdateAvailable(currentVersion,
|
|
74
|
+
latest,
|
|
75
|
+
updateAvailable: computeUpdateAvailable(currentVersion, latest),
|
|
74
76
|
distTag: getUpdateDistTag(channel),
|
|
75
|
-
installSpec: getInstallSpecForChannel(channel),
|
|
77
|
+
installSpec: latest ? `${PACKAGE_NAME}@${latest}` : getInstallSpecForChannel(channel),
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
async function viewPackageVersionAsync(channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
|
package/dist/plush-avatar.d.ts
CHANGED
|
@@ -84,12 +84,13 @@ export type PlushRenderConfig = PlushAvatarConfig | PlushCatAvatarConfig;
|
|
|
84
84
|
export declare function isPlushCatAvatar(config: PlushRenderConfig): config is PlushCatAvatarConfig;
|
|
85
85
|
export declare function parsePlushCatAvatar(value: string): PlushCatAvatarConfig | null;
|
|
86
86
|
export declare function encodePlushCatAvatar(config: PlushCatAvatarConfig): string;
|
|
87
|
-
/**
|
|
87
|
+
/** All employee surfaces share the API's avatar policy, including stale fixed-identity drafts. */
|
|
88
88
|
export declare function resolveEmployeeAvatar(identity: PlushAvatarIdentity): PlushRenderConfig | null;
|
|
89
89
|
export interface PlushAvatarIdentity {
|
|
90
90
|
id?: string | null;
|
|
91
91
|
name?: string | null;
|
|
92
92
|
avatar?: string | null;
|
|
93
|
+
systemKey?: string | null;
|
|
93
94
|
}
|
|
94
95
|
/** Only the exact known v1 contract is accepted for explicit configuration; no URL or future-version guessing. */
|
|
95
96
|
export declare function parsePlushAvatar(value: string): PlushAvatarConfig | null;
|
package/dist/plush-avatar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { employeeAvatarValue } from "./fixed-employee-avatar.js";
|
|
1
2
|
/** Shared, versioned employee avatar contract. Keep v1 option IDs and seed rules stable across clients. */
|
|
2
3
|
export const PLUSH_SHAPES = [
|
|
3
4
|
{ id: "heart", label: "心形" },
|
|
@@ -41,9 +42,10 @@ export function encodePlushCatAvatar(config) {
|
|
|
41
42
|
throw new Error("毛绒猫头像配置无效。");
|
|
42
43
|
return value;
|
|
43
44
|
}
|
|
44
|
-
/**
|
|
45
|
+
/** All employee surfaces share the API's avatar policy, including stale fixed-identity drafts. */
|
|
45
46
|
export function resolveEmployeeAvatar(identity) {
|
|
46
|
-
|
|
47
|
+
const avatar = employeeAvatarValue(identity);
|
|
48
|
+
return parsePlushCatAvatar(avatar) ?? resolvePlushAvatar({ ...identity, avatar });
|
|
47
49
|
}
|
|
48
50
|
function hasId(options, id) {
|
|
49
51
|
return options.some(option => option.id === id);
|
package/dist/retention.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AI_TEAM_ACTIVE_RUN_STATUSES } from "./ai-team-types.js";
|
|
2
|
+
import { isArchiveBlocked } from "./archive-policy.js";
|
|
1
3
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
2
4
|
import { taskRetentionWindows } from "./task-retention.js";
|
|
3
5
|
/**
|
|
@@ -12,13 +14,7 @@ export const RETENTION_ARCHIVED_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
12
14
|
* 空闲 zsh 不算。真正挡住清理的是正在回复、或停在权限/确认上。
|
|
13
15
|
*/
|
|
14
16
|
export function isRetentionBusy(session) {
|
|
15
|
-
|
|
16
|
-
return true;
|
|
17
|
-
if (session.permissionBlocked || session.pendingEscalation)
|
|
18
|
-
return true;
|
|
19
|
-
if ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty")
|
|
20
|
-
return false;
|
|
21
|
-
return session.status === "running";
|
|
17
|
+
return isArchiveBlocked(session);
|
|
22
18
|
}
|
|
23
19
|
function isIdleLivePty(session) {
|
|
24
20
|
return session.status === "running" && !isRetentionBusy(session)
|
|
@@ -62,7 +58,7 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
62
58
|
const snapshots = deps.sessions.listSlim();
|
|
63
59
|
for (const session of snapshots) {
|
|
64
60
|
// 真正还在处理的会话不能归档或杀掉。空闲 PTY 壳不算。
|
|
65
|
-
if (
|
|
61
|
+
if (isArchiveBlocked(session))
|
|
66
62
|
continue;
|
|
67
63
|
if (session.archived) {
|
|
68
64
|
if (taskWindows.archivedMs === null)
|
|
@@ -113,9 +109,11 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
113
109
|
else
|
|
114
110
|
sessionsByTask.set(taskId, [session]);
|
|
115
111
|
}
|
|
112
|
+
const activeTeamTaskIds = new Set(deps.storage.listAiTeamRuns({ statuses: AI_TEAM_ACTIVE_RUN_STATUSES }).map(run => run.taskId));
|
|
116
113
|
for (const task of deps.storage.listWandTasks()) {
|
|
117
114
|
const bound = task.workspaceTaskId ? sessionsByTask.get(task.workspaceTaskId) ?? [] : [];
|
|
118
|
-
const running = bound.some((session) =>
|
|
115
|
+
const running = bound.some((session) => isArchiveBlocked(session))
|
|
116
|
+
|| activeTeamTaskIds.has(task.id) || (!!task.workspaceTaskId && activeTeamTaskIds.has(task.workspaceTaskId));
|
|
119
117
|
if (task.status === "archived") {
|
|
120
118
|
if (running || taskWindows.archivedMs === null)
|
|
121
119
|
continue;
|
|
@@ -138,7 +136,7 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
138
136
|
result.purgedTasks += 1;
|
|
139
137
|
continue;
|
|
140
138
|
}
|
|
141
|
-
if (running || taskWindows.idleMs === null)
|
|
139
|
+
if (running || task.status === "doing" || taskWindows.idleMs === null)
|
|
142
140
|
continue;
|
|
143
141
|
const lastActivity = taskLastActivity(deps.storage, task, bound);
|
|
144
142
|
if (lastActivity === null || now - lastActivity < taskWindows.idleMs)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
|
|
1
|
+
import { employeeAvatarValue, isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
|
|
4
4
|
import { getErrorMessage } from "./error-utils.js";
|
|
@@ -93,7 +93,8 @@ export function parseSystemEmployeeAgents(value, existing) {
|
|
|
93
93
|
if (incoming === undefined || (incoming === null && !isFixedAvatarEmployee(existing)))
|
|
94
94
|
continue;
|
|
95
95
|
const current = existing[field.key];
|
|
96
|
-
const isSame = typeof incoming === "string" && incoming === current
|
|
96
|
+
const isSame = typeof incoming === "string" && (incoming === current
|
|
97
|
+
|| (field.key === "avatar" && isFixedAvatarEmployee(existing) && incoming === employeeAvatarValue(existing)));
|
|
97
98
|
if (!isSame) {
|
|
98
99
|
throw new Error(`该员工是内置的,${field.label}不可修改。`);
|
|
99
100
|
}
|
|
@@ -214,6 +215,11 @@ export function registerSiliconEmployeeRoutes(app, deps) {
|
|
|
214
215
|
console.error("[SiliconEmployee] definition notification failed:", getErrorMessage(error));
|
|
215
216
|
}
|
|
216
217
|
};
|
|
218
|
+
// Identity is live data, not a versioned asset; old HTTP caches must not retain another face.
|
|
219
|
+
app.use("/api/silicon-employees", (_req, res, next) => {
|
|
220
|
+
res.setHeader("Cache-Control", "no-store");
|
|
221
|
+
next();
|
|
222
|
+
});
|
|
217
223
|
app.get("/api/silicon-employees", (req, res) => {
|
|
218
224
|
try {
|
|
219
225
|
const includeArchived = req.query.includeArchived === "true" || req.query.includeArchived === "1";
|
|
@@ -361,7 +367,7 @@ export function registerSiliconEmployeeRoutes(app, deps) {
|
|
|
361
367
|
: parseSiliconEmployeeInput(input, existing, now);
|
|
362
368
|
storage.saveSiliconEmployee(employee);
|
|
363
369
|
notifyEmployee(employee.id);
|
|
364
|
-
res.json(employee);
|
|
370
|
+
res.json(storage.getSiliconEmployee(employee.id));
|
|
365
371
|
}
|
|
366
372
|
catch (error) {
|
|
367
373
|
sendEmployeeError(res, error);
|
|
@@ -11,7 +11,6 @@ import { resolveSessionCwd } from "./session-cwd.js";
|
|
|
11
11
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
12
12
|
import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
13
13
|
import { getErrorMessage } from "./error-utils.js";
|
|
14
|
-
import { archiveCommitTasks, archiveCommitStandaloneSessions } from "./commit-task-archive.js";
|
|
15
14
|
import { buildIterationCommitContext, COMMIT_CONTEXT_MODE_PREF_KEY, isCommitContextMode, readCommitContextMode, resolveCommitContextInput, } from "./iteration-log.js";
|
|
16
15
|
import { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
|
|
17
16
|
import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
|
|
@@ -597,6 +596,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
597
596
|
sendRouteError(res, error, "切换运行目录失败。");
|
|
598
597
|
}
|
|
599
598
|
});
|
|
599
|
+
app.post("/api/sessions/:id/employee", (req, res) => {
|
|
600
|
+
const employeeId = req.body?.employeeId;
|
|
601
|
+
if (typeof employeeId !== "string" || !employeeId.trim()) {
|
|
602
|
+
res.status(400).json({ error: "请选择有效的员工身份。" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const id = req.params.id;
|
|
606
|
+
const owner = sessions.ownerOf(id);
|
|
607
|
+
if (!owner) {
|
|
608
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (owner !== "structured") {
|
|
612
|
+
res.status(400).json({ error: "只有新建空白结构化对话可以更换员工。" });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
res.json(sessionResponseDTO(structured.setSessionEmployee(id, employeeId.trim()), req));
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
sendRouteError(res, error, "切换员工失败。");
|
|
620
|
+
}
|
|
621
|
+
});
|
|
600
622
|
app.post("/api/sessions/:id/provider", (req, res) => {
|
|
601
623
|
const provider = req.body?.provider;
|
|
602
624
|
const engine = req.body?.engine;
|
|
@@ -1119,29 +1141,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1119
1141
|
const consumed = result.ok && result.commit?.hash && context.entryIds.length > 0
|
|
1120
1142
|
? storage.markIterationPromptsConsumed(context.entryIds, result.commit.hash)
|
|
1121
1143
|
: 0;
|
|
1122
|
-
let archivedTaskIds = [];
|
|
1123
|
-
const archivedSessionIds = [];
|
|
1124
|
-
let archiveError;
|
|
1125
|
-
if (body.archiveRelatedTasks === true && result.ok && result.commit?.hash) {
|
|
1126
|
-
try {
|
|
1127
|
-
// A commit is already durable, even when push failed. Archiving must not turn that
|
|
1128
|
-
// successful commit into a retryable HTTP error (which would duplicate the commit).
|
|
1129
|
-
const openedTaskId = typeof body.workspaceTaskId === "string" ? body.workspaceTaskId.trim() : "";
|
|
1130
|
-
archivedTaskIds = archiveCommitTasks(storage, snapshot, context.entryIds, {
|
|
1131
|
-
workspaceTaskId: openedTaskId || null,
|
|
1132
|
-
});
|
|
1133
|
-
archiveCommitStandaloneSessions(storage, sessions, snapshot, archivedSessionIds);
|
|
1134
|
-
}
|
|
1135
|
-
catch (error) {
|
|
1136
|
-
archiveError = getErrorMessage(error, "归档任务与会话失败。");
|
|
1137
|
-
console.error("[QuickCommit] Failed to archive related tasks:", archiveError);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
1144
|
res.json({
|
|
1141
1145
|
...result,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1146
|
+
// Compatibility for older clients: Git never changes task/session lifecycle.
|
|
1147
|
+
archivedTaskIds: [],
|
|
1148
|
+
archivedSessionIds: [],
|
|
1145
1149
|
commitContext: {
|
|
1146
1150
|
source: commitMessageFromIteration(body) && context.digest ? "iteration" : "diff",
|
|
1147
1151
|
entryIds: context.entryIds,
|
|
@@ -1747,7 +1751,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1747
1751
|
res.json(sessionResponseDTO(updated, req));
|
|
1748
1752
|
}
|
|
1749
1753
|
catch (error) {
|
|
1750
|
-
sendRouteError(res, error, "无法归档会话。");
|
|
1754
|
+
sendRouteError(res, error, "无法归档会话。", 409);
|
|
1751
1755
|
}
|
|
1752
1756
|
});
|
|
1753
1757
|
app.post("/api/sessions/:id/unarchive", asyncRoute(async (req, res) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canArchiveTask } from "./task-archive-policy.js";
|
|
1
2
|
import { dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
|
|
2
3
|
import { asyncRoute } from "./express-async.js";
|
|
3
4
|
import { getErrorMessage } from "./error-utils.js";
|
|
@@ -207,6 +208,7 @@ function taskDto({ storage, sessions }, task) {
|
|
|
207
208
|
const milestone = resolvedMilestoneFields(storage, task.milestoneId);
|
|
208
209
|
return {
|
|
209
210
|
...task,
|
|
211
|
+
canArchive: canArchiveTask(storage, sessions, task.id),
|
|
210
212
|
sessionIds,
|
|
211
213
|
sessions: sessionIds.flatMap((id) => {
|
|
212
214
|
const session = sessions?.getLatest(id) ?? storage.getSession(id);
|
|
@@ -587,6 +589,10 @@ export function registerTaskRoutes(app, deps) {
|
|
|
587
589
|
if (body.sortOrder !== undefined && Number.isFinite(Number(body.sortOrder))) {
|
|
588
590
|
patch.sortOrder = Math.floor(Number(body.sortOrder));
|
|
589
591
|
}
|
|
592
|
+
if (patch.status === "archived" && !canArchiveTask(storage, sessions, currentTask.id)) {
|
|
593
|
+
res.status(409).json({ error: "任务尚未完成或仍有执行,完成后才能归档。" });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
590
596
|
const task = storage.updateWandTask(req.params.id, patch);
|
|
591
597
|
if (!task) {
|
|
592
598
|
res.status(404).json({ error: "未找到该任务。" });
|
|
@@ -637,6 +643,10 @@ export function registerTaskRoutes(app, deps) {
|
|
|
637
643
|
}
|
|
638
644
|
});
|
|
639
645
|
app.delete("/api/wand-tasks/:id", (req, res) => {
|
|
646
|
+
if (storage.getWandTask(req.params.id) && !canArchiveTask(storage, sessions, req.params.id)) {
|
|
647
|
+
res.status(409).json({ error: "任务尚未完成或仍有执行,完成后才能归档。" });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
640
650
|
const archived = storage.updateWandTask(req.params.id, { status: "archived" });
|
|
641
651
|
if (!archived) {
|
|
642
652
|
res.status(404).json({ error: "未找到该任务。" });
|
|
@@ -449,9 +449,14 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
449
449
|
}
|
|
450
450
|
state.updateInFlight = true;
|
|
451
451
|
try {
|
|
452
|
-
const
|
|
452
|
+
const channel = deps.getUpdateChannel();
|
|
453
|
+
const info = await deps.checkLatestPackageVersion(channel, true);
|
|
454
|
+
if (channel !== deps.getUpdateChannel()) {
|
|
455
|
+
res.status(409).json({ error: "更新通道已切换,请重新检查更新。" });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
453
458
|
if (!info.latest) {
|
|
454
|
-
res.status(502).json({ error: "
|
|
459
|
+
res.status(502).json({ error: "npm 所选通道暂无有效版本,或 registry 暂时不可用。" });
|
|
455
460
|
return;
|
|
456
461
|
}
|
|
457
462
|
if (!canUseDetachedUpdateHelper()) {
|
|
@@ -529,7 +534,11 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
529
534
|
});
|
|
530
535
|
app.post("/api/update-channel", requireAdmin, (req, res) => {
|
|
531
536
|
const body = (req.body ?? {});
|
|
532
|
-
|
|
537
|
+
if (body.channel !== "beta" && body.channel !== "stable") {
|
|
538
|
+
res.status(400).json({ error: "更新通道必须为 stable 或 beta。" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const channel = body.channel;
|
|
533
542
|
storage.setConfigValue("updateChannel", channel);
|
|
534
543
|
res.json({ channel });
|
|
535
544
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { canArchiveTask } from "./task-archive-policy.js";
|
|
2
|
+
import { isArchiveBlocked } from "./archive-policy.js";
|
|
1
3
|
import path from "node:path";
|
|
2
4
|
import crypto from "node:crypto";
|
|
3
5
|
import { existsSync, statSync } from "node:fs";
|
|
@@ -44,6 +46,7 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
|
|
|
44
46
|
providerCliActive: live.providerCliActive,
|
|
45
47
|
providerCliExitCode: live.providerCliExitCode,
|
|
46
48
|
inFlight: live.structuredState?.inFlight === true,
|
|
49
|
+
canArchive: !isArchiveBlocked(live),
|
|
47
50
|
cwd: live.cwd,
|
|
48
51
|
startedAt: live.startedAt,
|
|
49
52
|
employeeId: live.employeeId,
|
|
@@ -88,6 +91,7 @@ function cheapTasksRevision(storage, registry, includeArchived = false) {
|
|
|
88
91
|
session.ptyCommandRunning === true ? 1 : 0,
|
|
89
92
|
session.providerCliActive ? 1 : 0,
|
|
90
93
|
session.structuredState?.inFlight === true ? 1 : 0,
|
|
94
|
+
isArchiveBlocked(session) ? 1 : 0,
|
|
91
95
|
session.title ?? "",
|
|
92
96
|
session.workspaceId ?? "",
|
|
93
97
|
session.workspaceTaskId ?? "",
|
|
@@ -610,6 +614,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
610
614
|
}
|
|
611
615
|
return snapshots;
|
|
612
616
|
};
|
|
617
|
+
const sessionRegistry = sessions;
|
|
613
618
|
const appendTask = (target, workspace, task) => {
|
|
614
619
|
const visibleSessions = taskSessions(task.id).filter(sessionVisible);
|
|
615
620
|
const sessions = visibleSessions
|
|
@@ -620,6 +625,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
620
625
|
}));
|
|
621
626
|
target.tasks.push({
|
|
622
627
|
...task,
|
|
628
|
+
canArchive: (() => { const card = storage.getWandTaskByWorkspaceTaskId(task.id); return card ? canArchiveTask(storage, sessionRegistry, card.id) : true; })(),
|
|
623
629
|
// 没单独指定迭代的任务读作默认迭代(id 与名字成对)。
|
|
624
630
|
...resolvedMilestoneFields(storage, task.milestoneId),
|
|
625
631
|
cwd: taskRuntimeCwd(task, workspace),
|
|
@@ -746,6 +752,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
746
752
|
.filter((task) => req.query.includeArchived === "1" || !task.archived)
|
|
747
753
|
.map((task) => ({
|
|
748
754
|
...task,
|
|
755
|
+
canArchive: (() => { const card = storage.getWandTaskByWorkspaceTaskId(task.id); return card ? canArchiveTask(storage, sessions, card.id) : true; })(),
|
|
749
756
|
...resolvedMilestoneFields(storage, task.milestoneId),
|
|
750
757
|
})));
|
|
751
758
|
});
|
|
@@ -923,6 +930,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
923
930
|
res.status(404).json({ error: "未找到该任务。" });
|
|
924
931
|
return;
|
|
925
932
|
}
|
|
933
|
+
const card = storage.getWandTaskByWorkspaceTaskId(existing.id);
|
|
934
|
+
if (card && !canArchiveTask(storage, sessions, card.id)) {
|
|
935
|
+
res.status(409).json({ error: "任务尚未完成或仍有执行,完成后才能归档。" });
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
926
938
|
storage.archiveWorkspaceTask(existing.id);
|
|
927
939
|
const archived = storage.getWorkspaceTask(existing.id);
|
|
928
940
|
res.json({
|
package/dist/server.js
CHANGED
|
@@ -821,8 +821,8 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
821
821
|
// 把语言偏好暴露给前端做 UI 文案 i18n。后端原本只用它给 Claude 拼 system prompt,
|
|
822
822
|
// 前端没收到 → "SUBAGENT" / "Read" 这些 UI label 一直是英文,跟用户设的中文不匹配。
|
|
823
823
|
language: config.language ?? "",
|
|
824
|
-
updateAvailable: cachedUpdateInfo?.updateAvailable
|
|
825
|
-
latestVersion: cachedUpdateInfo?.latest
|
|
824
|
+
updateAvailable: cachedUpdateInfo?.channel === getUpdateChannel() ? cachedUpdateInfo.updateAvailable : false,
|
|
825
|
+
latestVersion: cachedUpdateInfo?.channel === getUpdateChannel() ? cachedUpdateInfo.latest : null,
|
|
826
826
|
updateChannel: getUpdateChannel(),
|
|
827
827
|
currentVersion: DISPLAY_VERSION,
|
|
828
828
|
packageVersion: PKG_VERSION,
|
|
@@ -844,7 +844,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
844
844
|
requireAdminOrSessionPreferences,
|
|
845
845
|
packageInfo: { version: DISPLAY_VERSION, name: PKG_NAME, nodeVersion: PKG_NODE_REQ, repoUrl: PKG_REPO_URL },
|
|
846
846
|
buildInfo: BUILD_INFO,
|
|
847
|
-
getCachedUpdateInfo: () => cachedUpdateInfo,
|
|
847
|
+
getCachedUpdateInfo: () => cachedUpdateInfo?.channel === getUpdateChannel() ? cachedUpdateInfo : null,
|
|
848
848
|
getUpdateChannel,
|
|
849
849
|
getDistributionSettings,
|
|
850
850
|
getGithubConnector: () => getGithubConnectorStatus(storage),
|
|
@@ -1327,7 +1327,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1327
1327
|
return;
|
|
1328
1328
|
const channel = getUpdateChannel();
|
|
1329
1329
|
const info = await checkLatestPackageVersion(channel, true);
|
|
1330
|
-
if (shuttingDown)
|
|
1330
|
+
if (shuttingDown || channel !== getUpdateChannel())
|
|
1331
1331
|
return;
|
|
1332
1332
|
cachedUpdateInfo = info;
|
|
1333
1333
|
if (!info.latest || !info.updateAvailable)
|
package/dist/session-registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isArchiveBlocked } from "./archive-policy.js";
|
|
1
2
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
2
3
|
import { inferProviderFromCommand } from "./session-provider.js";
|
|
3
4
|
export function deleteProviderNativeHistory(deleter, provider, providerSessionId, cwd) {
|
|
@@ -180,6 +181,12 @@ export class SessionRegistry {
|
|
|
180
181
|
return this.processes.stop(id);
|
|
181
182
|
}
|
|
182
183
|
setArchived(id, archived) {
|
|
184
|
+
const current = this.getLatest(id);
|
|
185
|
+
if (current && !!current.archived === archived && (!archived || current.archivedAt))
|
|
186
|
+
return current;
|
|
187
|
+
if (archived && current && isArchiveBlocked(current)) {
|
|
188
|
+
throw Object.assign(new Error("会话仍在执行或等待处理,完成后才能归档。"), { statusCode: 409 });
|
|
189
|
+
}
|
|
183
190
|
const owner = this.ownerOf(id);
|
|
184
191
|
if (owner === "structured")
|
|
185
192
|
return this.structured.setSessionArchived(id, archived);
|
package/dist/storage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { FIXED_EMPLOYEE_AVATARS, fixedEmployeeAvatar, isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
|
|
2
|
+
import { FIXED_EMPLOYEE_AVATARS, employeeAvatarValue, fixedEmployeeAvatar, isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
|
|
3
3
|
import { CONVERSATION_OWNER } from "./conversation-types.js";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -2715,6 +2715,8 @@ export class WandStorage {
|
|
|
2715
2715
|
return rows.map((row) => this.projectDefaultEmployee(mapSiliconEmployeeRow(row)));
|
|
2716
2716
|
}
|
|
2717
2717
|
projectDefaultEmployee(employee) {
|
|
2718
|
+
// Presentation only: legacy columns stay untouched until an explicit migration.
|
|
2719
|
+
employee = { ...employee, avatar: employeeAvatarValue(employee) };
|
|
2718
2720
|
if (employee.systemKey !== DEFAULT_EMPLOYEE_KEY)
|
|
2719
2721
|
return employee;
|
|
2720
2722
|
const state = this.getUserMemoryState();
|
|
@@ -2745,7 +2747,10 @@ export class WandStorage {
|
|
|
2745
2747
|
/** 内置「系统运维」员工;未创建(或被删除过)时返回 null。 */
|
|
2746
2748
|
getSystemSiliconEmployee(key = SYSTEM_EMPLOYEE_KEY) {
|
|
2747
2749
|
const row = this.db.prepare("SELECT * FROM silicon_employees WHERE system_key = ?").get(key);
|
|
2748
|
-
|
|
2750
|
+
if (!row)
|
|
2751
|
+
return null;
|
|
2752
|
+
const employee = mapSiliconEmployeeRow(row);
|
|
2753
|
+
return { ...employee, avatar: employeeAvatarValue(employee) };
|
|
2749
2754
|
}
|
|
2750
2755
|
/** Seed only when missing; restart never overwrites user profile, candidates, or legacy avatars. */
|
|
2751
2756
|
ensureSystemSiliconEmployee(seed = {}) {
|
|
@@ -2786,8 +2791,9 @@ export class WandStorage {
|
|
|
2786
2791
|
throw new Error("内置员工身份不可修改或冒用。");
|
|
2787
2792
|
if (existing && !isFixedAvatarEmployee(existing))
|
|
2788
2793
|
throw new Error("内置员工身份冲突,不能覆盖已有记录。");
|
|
2789
|
-
if (employee.avatar !== (existing?.avatar ?? target.avatar))
|
|
2794
|
+
if (employee.avatar !== (existing?.avatar ?? target.avatar) && employee.avatar !== employeeAvatarValue(employee)) {
|
|
2790
2795
|
throw new Error("该员工是内置的,头像不可修改。");
|
|
2796
|
+
}
|
|
2791
2797
|
if (existing && JSON.stringify(employee.tags) !== JSON.stringify(existing.tags))
|
|
2792
2798
|
throw new Error("该员工是内置的,标签不可修改。");
|
|
2793
2799
|
if (employee.archivedAt !== existing?.archivedAt)
|
|
@@ -2808,7 +2814,9 @@ export class WandStorage {
|
|
|
2808
2814
|
tags_json = excluded.tags_json,
|
|
2809
2815
|
system_key = excluded.system_key,
|
|
2810
2816
|
archived_at = excluded.archived_at,
|
|
2811
|
-
updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt,
|
|
2817
|
+
updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt,
|
|
2818
|
+
// Accept a canonical read echo without persisting a display projection over a legacy value.
|
|
2819
|
+
target && existing ? existing.avatar : employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags));
|
|
2812
2820
|
}
|
|
2813
2821
|
/** Explicit, atomic migration; not called from GET, config load, seeds, or ordinary saves.
|
|
2814
2822
|
* Compare all expected avatar values first; change only avatar, including preserving timestamps.
|
|
@@ -236,6 +236,11 @@ export declare class StructuredSessionManager {
|
|
|
236
236
|
setSessionDirectory(sessionId: string, cwd: string): SessionSnapshot;
|
|
237
237
|
/** 空白对话原位换执行工具;引擎也是工具身份,不跨工具搬历史或能力设置。 */
|
|
238
238
|
setSessionProvider(sessionId: string, provider: SessionProvider, engine?: "cli" | "sdk"): SessionSnapshot;
|
|
239
|
+
/** 首次发送前显式选择真实员工;同一会话、目录和未发送输入的归属不变。 */
|
|
240
|
+
setSessionEmployee(sessionId: string, employeeId: string): SessionSnapshot;
|
|
241
|
+
private requireBlankSessionSelection;
|
|
242
|
+
private blankSessionToolSelection;
|
|
243
|
+
private saveBlankSessionSelection;
|
|
239
244
|
/**
|
|
240
245
|
* 新建 pi 结构化会话前的引擎裁决。
|
|
241
246
|
*
|