@co0ontty/wand 4.97.0 → 4.98.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 +0 -4
- package/dist/agent-dispatch.d.ts +1 -0
- package/dist/agent-dispatch.js +13 -2
- package/dist/ai-team-types.d.ts +1 -1
- package/dist/ai-team-types.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/core-runner.d.ts +0 -4
- package/dist/core-runner.js +41 -3
- package/dist/decision-expert-employee.js +1 -0
- package/dist/distribution-manager.d.ts +29 -4
- package/dist/distribution-manager.js +147 -57
- package/dist/employee-types.d.ts +2 -1
- package/dist/employee-types.js +2 -4
- package/dist/feedback-feature-map.d.ts +0 -1
- package/dist/feedback-feature-map.js +0 -1
- package/dist/feedback.d.ts +0 -1
- package/dist/feedback.js +1 -1
- package/dist/git-quick-commit.js +13 -52
- package/dist/git-tag-generation.d.ts +11 -0
- package/dist/git-tag-generation.js +47 -0
- package/dist/hr-employee.js +1 -0
- package/dist/local-model-setup.d.ts +1 -9
- package/dist/local-model-setup.js +52 -166
- package/dist/local-model-types.d.ts +2 -5
- package/dist/managed-temp.d.ts +1 -1
- package/dist/managed-temp.js +1 -1
- package/dist/middleware/api-scope.js +2 -4
- package/dist/migration-backup-retention.d.ts +17 -0
- package/dist/migration-backup-retention.js +91 -0
- package/dist/pi-tmux-host.d.ts +28 -0
- package/dist/pi-tmux-host.js +189 -0
- package/dist/server-employee-routes.d.ts +1 -0
- package/dist/server-employee-routes.js +17 -4
- package/dist/server-local-model-routes.js +2 -15
- package/dist/server-session-input-routes.js +3 -3
- package/dist/server-session-lifecycle-routes.js +94 -18
- package/dist/server-task-routes.js +18 -13
- package/dist/server-update-routes.d.ts +11 -1
- package/dist/server-update-routes.js +25 -12
- package/dist/server-workspace-routes.js +11 -5
- package/dist/server.js +29 -129
- package/dist/storage/conversations-repository.d.ts +2 -0
- package/dist/storage/conversations-repository.js +4 -0
- package/dist/storage/employees-repository.d.ts +2 -2
- package/dist/storage/employees-repository.js +12 -15
- package/dist/storage/internal.js +2 -2
- package/dist/storage/migrations.js +20 -0
- package/dist/storage/readonly.js +3 -0
- package/dist/storage/sessions-repository.d.ts +2 -0
- package/dist/storage/sessions-repository.js +13 -0
- package/dist/storage/tasks-repository.d.ts +22 -0
- package/dist/storage/tasks-repository.js +52 -3
- package/dist/storage/workspaces-repository.d.ts +1 -0
- package/dist/storage/workspaces-repository.js +3 -1
- package/dist/storage-cleanup.d.ts +37 -2
- package/dist/storage-cleanup.js +62 -2
- package/dist/storage-schema.js +9 -0
- package/dist/structured-claude-adapter.js +5 -1
- package/dist/structured-claude-protocol.d.ts +1 -1
- package/dist/structured-claude-protocol.js +12 -1
- package/dist/structured-runner.d.ts +2 -0
- package/dist/structured-session-manager.d.ts +14 -2
- package/dist/structured-session-manager.js +87 -14
- package/dist/system-ai-call.d.ts +5 -1
- package/dist/system-ai-call.js +32 -1
- package/dist/system-ai-session-recorder.d.ts +2 -0
- package/dist/system-ai-session-recorder.js +35 -3
- package/dist/system-cli.js +6 -1
- package/dist/system-employee.js +1 -0
- package/dist/task-comment-dispatch.d.ts +18 -0
- package/dist/task-comment-dispatch.js +121 -0
- package/dist/task-types.d.ts +14 -0
- package/dist/version-utils.d.ts +31 -0
- package/dist/version-utils.js +35 -0
- package/dist/web-ui/content/ai-teams.js +10 -10
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/02b1ec89c69dc0dd/host.js +30 -0
- package/dist/web-ui/content/modules/{e8f0cda6b9ac8a70 → 0434a45abd2c1d15}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e9a01a25ec573294 → 0afa04591224bc23}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/{b33c567946044d83 → 0bf4a78c8be1bdc9}/host.js +1 -1
- package/dist/web-ui/content/modules/17f72e0fb014a302/chat-render.js +75 -0
- package/dist/web-ui/content/modules/{5903104a2e0e3888 → 183db4802a7d99ed}/host.js +1 -1
- package/dist/web-ui/content/modules/23d2006fcfcf3c0c/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/{0c96341a022aa360 → 32f151da1a006041}/host.js +1 -1
- package/dist/web-ui/content/modules/{3172fe0b46d51f8d → 34a7674efc329383}/chunk.js +1 -1
- package/dist/web-ui/content/modules/3541ba4cb2886e29/chunk.js +1 -0
- package/dist/web-ui/content/modules/{801e7f686ce7eb28 → 395257bd20cd0098}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3ddcbd8505bfcdf4 → 3fd75af0a6891549}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c41d8ffa27816766 → 428352e422932c40}/host.js +1 -1
- package/dist/web-ui/content/modules/485a72b2c83e7680/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/489dfe1f7441ef2c/markdown.js +1 -0
- package/dist/web-ui/content/modules/4b42b4336458b7b5/runtime.js +550 -0
- package/dist/web-ui/content/modules/{de00ab5329b5e05a → 528c5d97e8f9d582}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3086df78444998b3 → 535aad05d34079d2}/host.js +1 -1
- package/dist/web-ui/content/modules/62028b9bbf8ffc88/page.js +6 -0
- package/dist/web-ui/content/modules/6868f75a5981ad08/host.js +69 -0
- package/dist/web-ui/content/modules/6c72c8c166e34de7/host.js +1 -0
- package/dist/web-ui/content/modules/6ff6ce370ef26753/host.js +1 -0
- package/dist/web-ui/content/modules/{87379801875d1921 → 72bef6889fdafefe}/host.js +1 -1
- package/dist/web-ui/content/modules/{5e5eabd9a9366a56 → 763c79f5527d762a}/panel.js +1 -1
- package/dist/web-ui/content/modules/{d30ac9af6dc7d555 → 77ea205020273767}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e01e1e25b7fe37e9 → 7fb02c44fd6f877b}/host.js +1 -1
- package/dist/web-ui/content/modules/{349ee129eef5a182 → 818d87c1651fc6a1}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{66f36cd2c38cfb07 → 8cb5d84a2731789e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f5b3fb6fedd9b05d → 96e78c7f2cd9acd5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{64c1b0217f5377aa → a60a07ba1abcdc3f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ee12d3c4421a5c12 → b4bcb9bf5cfb0b3a}/host.js +1 -1
- package/dist/web-ui/content/modules/b71cda413318d71f/employee-card.js +1 -0
- package/dist/web-ui/content/modules/bb0c299cf006e157/task-board-host.js +17 -0
- package/dist/web-ui/content/modules/{42ae495da5903b07 → c8c6adee81d4bd98}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{854b0f6dc887923b → cd0cec0ba1887ffd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3338b55c7767bacb → d673a76becc7d1f5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2bce1137f402bd70 → d99ea7e921488348}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a9f15bd03bc590e7 → e862ccabc908eb0c}/host.js +1 -1
- package/dist/web-ui/content/modules/{9984f6bb05ac55bb → eb46bf62b1ae1bea}/host.js +1 -1
- package/dist/web-ui/content/plush-avatar.js +166 -166
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +177 -177
- package/dist/web-ui/embedded-assets.js +6 -6
- package/dist/web-ui/page-route.d.ts +24 -0
- package/dist/web-ui/page-route.js +130 -0
- package/dist/web-ui/pty-input-state.d.ts +2 -0
- package/dist/web-ui/pty-input-state.js +2 -2
- package/dist/web-ui/react/workspaces/types.d.ts +5 -0
- package/package.json +1 -3
- package/dist/server-speech-routes.d.ts +0 -10
- package/dist/server-speech-routes.js +0 -93
- package/dist/speech-models.d.ts +0 -24
- package/dist/speech-models.js +0 -16
- package/dist/speech-polisher-employee.d.ts +0 -6
- package/dist/speech-polisher-employee.js +0 -23
- package/dist/speech-polisher-identity.d.ts +0 -4
- package/dist/speech-polisher-identity.js +0 -4
- package/dist/speech-polisher-service.d.ts +0 -33
- package/dist/speech-polisher-service.js +0 -110
- package/dist/speech-service.d.ts +0 -59
- package/dist/speech-service.js +0 -420
- package/dist/speech-support.d.ts +0 -6
- package/dist/speech-support.js +0 -24
- package/dist/speech-types.d.ts +0 -52
- package/dist/speech-types.js +0 -3
- package/dist/web-ui/content/modules/15e32e85e086f6b4/page.js +0 -6
- package/dist/web-ui/content/modules/1e43259bddbc6805/markdown.js +0 -1
- package/dist/web-ui/content/modules/271d488d262909c2/host.js +0 -30
- package/dist/web-ui/content/modules/43746e7237d61da9/task-board-host.js +0 -17
- package/dist/web-ui/content/modules/4485e6c1f0459aa8/chunk.js +0 -1
- package/dist/web-ui/content/modules/5b541e0f296a3592/employee-card.js +0 -1
- package/dist/web-ui/content/modules/6ed52e6604661678/chat-render.js +0 -75
- package/dist/web-ui/content/modules/89888da0972e5033/host.js +0 -50
- package/dist/web-ui/content/modules/c11165f843d818f4/host-bridge.js +0 -1
- package/dist/web-ui/content/modules/c1829693f91fd25c/host.js +0 -1
- package/dist/web-ui/content/modules/cdb882ddab12e325/host.js +0 -1
- package/dist/web-ui/content/modules/ea67fd4e96c9f45a/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/fd767282c29173b5/runtime.js +0 -539
- package/dist/web-ui/speech-audio.d.ts +0 -2
- package/dist/web-ui/speech-audio.js +0 -41
- package/scripts/install-speech-runtime.js +0 -82
|
@@ -26,11 +26,21 @@ export interface PublicUpdateRoutesDependencies {
|
|
|
26
26
|
resolveAndroidDownload(channel: "stable" | "beta"): Promise<DownloadAsset | null>;
|
|
27
27
|
/** GitHub 来源由服务端代拉,避免手机直连 CDN。测试 stub 可不实现。 */
|
|
28
28
|
resolveGitHubApkDownload?(): Promise<GitHubApkDownload | null>;
|
|
29
|
-
/** Beta
|
|
29
|
+
/** Beta 包下发时清理分发目录,只保留最新一套产物(可选,测试 stub 可不实现)。 */
|
|
30
30
|
pruneAndroidApkDirectory?(keepFileName?: string): Promise<{
|
|
31
31
|
deleted: string[];
|
|
32
32
|
freedBytes: number;
|
|
33
33
|
}>;
|
|
34
|
+
/** macOS Beta 包下发时清理分发目录,保留最新一整套 zip/dmg/update.json。 */
|
|
35
|
+
pruneMacosBetaDirectory?(keepFileName?: string): Promise<{
|
|
36
|
+
deleted: string[];
|
|
37
|
+
freedBytes: number;
|
|
38
|
+
}>;
|
|
39
|
+
/** iOS IPA 下发时清理分发目录,保留最新一个包。 */
|
|
40
|
+
pruneIosIpaDirectory?(keepFileName?: string): Promise<{
|
|
41
|
+
deleted: string[];
|
|
42
|
+
freedBytes: number;
|
|
43
|
+
}>;
|
|
34
44
|
/** 计算本地分发文件的 SHA-256(hex);失败返回 null,下载响应不带哈希头。 */
|
|
35
45
|
computeAssetSha256(asset: DownloadAsset): Promise<string | null>;
|
|
36
46
|
resolveLatestDmg(): Promise<ResolvedUpdateAsset | null>;
|
|
@@ -13,6 +13,24 @@ import { compareApkInstallOrder, compareSemver, compareWandInstallOrder } from "
|
|
|
13
13
|
import { HttpError } from "./http-error.js";
|
|
14
14
|
import { canUseDetachedUpdateHelper, readDetachedUpdateStatus, readLatestDetachedUpdate, startDetachedUpdateHelper } from "./update-helper.js";
|
|
15
15
|
const GITHUB_PROXY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* 分发目录清理的共用执行器:清理失败绝不能阻断本次下载或更新检查。
|
|
18
|
+
* 只在真正开始下发时调用,避免定时扫描去删一个没人要的文件。
|
|
19
|
+
*/
|
|
20
|
+
async function pruneQuietly(prune, keepFileName, label) {
|
|
21
|
+
if (!prune)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
const pruned = await prune(keepFileName);
|
|
25
|
+
if (pruned.deleted.length > 0) {
|
|
26
|
+
process.stdout.write(`[wand] 已清理旧版${label}:${pruned.deleted.join(", ")}`
|
|
27
|
+
+ `(释放 ${(pruned.freedBytes / 1024 / 1024).toFixed(1)} MB)\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.stdout.write(`[wand] 清理旧版${label}失败:${getErrorMessage(error)}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
|
|
17
35
|
let remote;
|
|
18
36
|
try {
|
|
@@ -113,18 +131,8 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
|
|
|
113
131
|
// Beta 包下发时顺手清理分发目录:debug 构建每次都带独立时间戳版本号,
|
|
114
132
|
// 不清理会按构建次数无限堆积。只保留最新一个;正在下发的文件与
|
|
115
133
|
// currentApkFile 固定指向的文件不会被删。清理失败不阻断本次下载。
|
|
116
|
-
if (channel === "beta"
|
|
117
|
-
|
|
118
|
-
const pruned = await deps.pruneAndroidApkDirectory(asset.fileName);
|
|
119
|
-
if (pruned.deleted.length > 0) {
|
|
120
|
-
process.stdout.write(`[wand] 已清理旧版安装包:${pruned.deleted.join(", ")}`
|
|
121
|
-
+ `(释放 ${(pruned.freedBytes / 1024 / 1024).toFixed(1)} MB)\n`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
process.stdout.write(`[wand] 清理旧版安装包失败:${getErrorMessage(error)}\n`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
134
|
+
if (channel === "beta")
|
|
135
|
+
await pruneQuietly(deps.pruneAndroidApkDirectory, asset.fileName, "安装包");
|
|
128
136
|
// 响应头携带实际发送字节的 SHA-256:check 与 download 之间 APK 目录若被
|
|
129
137
|
// 重新构建/覆盖,客户端以响应头为准,避免用过期 check 快照误报校验失败。
|
|
130
138
|
const headers = {};
|
|
@@ -175,6 +183,9 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
|
|
|
175
183
|
res.status(503).json({ error: "无法校验 macOS Beta 更新包。" });
|
|
176
184
|
return;
|
|
177
185
|
}
|
|
186
|
+
// 开始下发后再清理:macOS 每次 debug 构建产出独立的 zip+dmg+update.json,
|
|
187
|
+
// 不清理会按构建次数堆积。清理保留最新一整套,不影响本次传输。
|
|
188
|
+
await pruneQuietly(deps.pruneMacosBetaDirectory, asset.fileName, " macOS 构建包");
|
|
178
189
|
streamFileWithRange(req, res, {
|
|
179
190
|
filePath: asset.filePath,
|
|
180
191
|
size: asset.size,
|
|
@@ -294,6 +305,8 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
|
|
|
294
305
|
res.status(404).json({ error: "当前没有可下载的 IPA 文件。" });
|
|
295
306
|
return;
|
|
296
307
|
}
|
|
308
|
+
// iOS 每次 debug 构建产出独立时间戳的 IPA,不清理会按构建次数堆积。
|
|
309
|
+
await pruneQuietly(deps.pruneIosIpaDirectory, asset.fileName, " iOS 构建包");
|
|
297
310
|
streamFileWithRange(req, res, {
|
|
298
311
|
filePath: asset.filePath,
|
|
299
312
|
size: asset.size,
|
|
@@ -21,6 +21,7 @@ import { parentTaskIdFrom } from "./task-agent-parsing.js";
|
|
|
21
21
|
import { taskTitleService, refreshAutoBoardTaskTitles } from "./task-title-service.js";
|
|
22
22
|
import { organizeSelectedByTitle, titleJudgeForStorage } from "./task-title-organize-service.js";
|
|
23
23
|
import { systemAiSessions } from "./system-ai-session-recorder.js";
|
|
24
|
+
import { isSystemAiCall } from "./system-ai-call.js";
|
|
24
25
|
import { registerConversationOrganizeRoutes } from "./server-conversation-organize-routes.js";
|
|
25
26
|
import { TITLE_ORGANIZE_MAX_SESSIONS, TITLE_ORGANIZE_MAX_TASKS } from "./task-title-organize.js";
|
|
26
27
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
@@ -223,7 +224,7 @@ function workspaceWithCounts(storage, workspace, sessionCounts, includeArchived
|
|
|
223
224
|
const worktreeCount = storage.listWorkspaceTasks(workspace.id)
|
|
224
225
|
.filter((task) => (includeArchived || !task.archived) && task.worktree !== null)
|
|
225
226
|
.length;
|
|
226
|
-
const sessionCount = (sessionCounts ?? storage.countSessionsByWorkspace({ includeArchived })).get(workspace.id) ?? 0;
|
|
227
|
+
const sessionCount = (sessionCounts ?? storage.countSessionsByWorkspace({ includeArchived, excludeSystemAiCalls: true })).get(workspace.id) ?? 0;
|
|
227
228
|
return { ...workspace, worktreeCount, sessionCount };
|
|
228
229
|
}
|
|
229
230
|
// ── Layout validation / sanitization(前端 PUT 与测试共用)──
|
|
@@ -337,7 +338,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
337
338
|
// 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
|
|
338
339
|
app.get("/api/workspaces", (req, res) => {
|
|
339
340
|
const includeArchived = req.query.includeArchived === "1";
|
|
340
|
-
const sessionCounts = storage.countSessionsByWorkspace({ includeArchived });
|
|
341
|
+
const sessionCounts = storage.countSessionsByWorkspace({ includeArchived, excludeSystemAiCalls: true });
|
|
341
342
|
res.json(storage.listWorkspaces()
|
|
342
343
|
.filter((workspace) => !isGlobalWorkspace(workspace))
|
|
343
344
|
.map((workspace) => workspaceWithCounts(storage, workspace, sessionCounts, includeArchived)));
|
|
@@ -376,7 +377,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
376
377
|
const archivedTaskIds = new Set(storage.listWorkspaceTasks(workspace.id)
|
|
377
378
|
.filter((task) => task.archived).map((task) => task.id));
|
|
378
379
|
const teamMarkers = teamSessionMarkers(storage);
|
|
379
|
-
const visibleSessions = storage.listSessionsByWorkspaceSlim(workspace.id).map((session) => ({
|
|
380
|
+
const visibleSessions = storage.listSessionsByWorkspaceSlim(workspace.id).filter((session) => !isSystemAiCall(session)).map((session) => ({
|
|
380
381
|
...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamMarkersFor(teamMarkers, session.id)),
|
|
381
382
|
workspaceTaskId: session.workspaceTaskId,
|
|
382
383
|
})).filter((session) => includeArchived || (!session.archived
|
|
@@ -590,6 +591,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
590
591
|
const snapshots = sessions?.listSlim();
|
|
591
592
|
const revision = crypto.createHash("sha256").update(JSON.stringify({
|
|
592
593
|
storage: storage.projectionRevision(),
|
|
594
|
+
projection: "workspace-work-sessions-v1",
|
|
593
595
|
query: { includeArchived, workspaceFilter, taskLimit, sessionLimit },
|
|
594
596
|
runtime: snapshots,
|
|
595
597
|
})).digest("base64url");
|
|
@@ -599,7 +601,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
599
601
|
}
|
|
600
602
|
const model = new TaskReadModel(storage, sessions, snapshots);
|
|
601
603
|
const allSessions = model.sessions;
|
|
602
|
-
|
|
604
|
+
// Server-owned one-shot feature logs are not Agent work. Do not filter by employee,
|
|
605
|
+
// sessionSource=automation or generic automation IDs: those also carry dispatch,
|
|
606
|
+
// communication and scheduled work. Keep storage and employee audit history intact.
|
|
607
|
+
const sessionVisible = (session) => !isSystemAiCall(session)
|
|
608
|
+
&& (includeArchived || !session.archived);
|
|
603
609
|
const workspaces = [...model.workspaces.values()];
|
|
604
610
|
// 群聊与成员派发标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
|
|
605
611
|
const teamMarkers = teamSessionMarkers(storage);
|
|
@@ -933,7 +939,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
|
|
|
933
939
|
...task,
|
|
934
940
|
...resolvedMilestoneFields(storage, task.milestoneId),
|
|
935
941
|
cwd: taskRuntimeCwd(task, workspace),
|
|
936
|
-
sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => ({
|
|
942
|
+
sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).filter((session) => !isSystemAiCall(session)).map((session) => ({
|
|
937
943
|
...workspaceSessionSummary(session, {
|
|
938
944
|
taskName: task.name,
|
|
939
945
|
workspaceName: workspace?.name,
|
package/dist/server.js
CHANGED
|
@@ -16,9 +16,9 @@ import { authenticateBearerAppToken, createRequireAuth, getEffectivePassword, re
|
|
|
16
16
|
import { createApiScopeMiddleware } from "./middleware/api-scope.js";
|
|
17
17
|
import { createExtensionCors } from "./middleware/cors.js";
|
|
18
18
|
import { ensureCertificates } from "./cert.js";
|
|
19
|
-
import { getDefaultModelForProvider, getProviderDefaultModels,
|
|
19
|
+
import { getDefaultModelForProvider, getProviderDefaultModels, resolveConfigDir, writePreferenceToStorage, } from "./config.js";
|
|
20
20
|
import { ModelCatalogService } from "./models.js";
|
|
21
|
-
import {
|
|
21
|
+
import { readProviderModelPreferences } from "./session-model.js";
|
|
22
22
|
import { ProcessManager } from "./process-manager.js";
|
|
23
23
|
import { SessionLogger } from "./session-logger.js";
|
|
24
24
|
import { coreHarnessAgentDir } from "./harness-engine.js";
|
|
@@ -34,21 +34,17 @@ import { registerFileRoutes } from "./server-file-routes.js";
|
|
|
34
34
|
import { recordRecentPath } from "./recent-paths.js";
|
|
35
35
|
import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
|
|
36
36
|
import { registerSettingsRoutes } from "./server-settings-routes.js";
|
|
37
|
-
import { registerSpeechRoutes } from "./server-speech-routes.js";
|
|
38
|
-
import { SpeechPolisherService } from "./speech-polisher-service.js";
|
|
39
|
-
import { SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
|
|
40
37
|
import { registerLocalModelRoutes } from "./server-local-model-routes.js";
|
|
41
|
-
import { createStorageCleanup } from "./storage-cleanup.js";
|
|
38
|
+
import { createStorageCleanup, STORAGE_CLEANUP_RETENTION_DAYS } from "./storage-cleanup.js";
|
|
39
|
+
import { pruneMigrationBackups } from "./migration-backup-retention.js";
|
|
42
40
|
import { registerStorageCleanupRoutes } from "./server-storage-cleanup-routes.js";
|
|
43
41
|
import { LocalModelSetupService } from "./local-model-setup.js";
|
|
44
42
|
import { DecisionExpertService } from "./decision-expert-service.js";
|
|
45
43
|
import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
|
|
46
|
-
import { SpeechService } from "./speech-service.js";
|
|
47
44
|
import { buildStructuredChatPersonaPayload, resolveAppConnectCode, } from "./server-app-connect.js";
|
|
48
45
|
import { resolveTrustProxy, sendRouteError } from "./server-request.js";
|
|
49
46
|
import { registerAuthRoutes } from "./server-auth-routes.js";
|
|
50
47
|
import { WebLoginTickets } from "./web-login-tickets.js";
|
|
51
|
-
import { inferProviderFromCommand, isSessionProvider, providerCliCommand } from "./session-provider.js";
|
|
52
48
|
import { registerVaultRoutes } from "./server-vault-routes.js";
|
|
53
49
|
import { registerGithubRoutes } from "./server-github-routes.js";
|
|
54
50
|
import { registerTaskRoutes } from "./server-task-routes.js";
|
|
@@ -69,16 +65,13 @@ import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
|
|
|
69
65
|
import { EmployeeImService } from "./employee-im.js";
|
|
70
66
|
import { EmployeeImLifecycle } from "./employee-im-lifecycle.js";
|
|
71
67
|
import { registerEmployeeImRoutes } from "./server-employee-im-routes.js";
|
|
72
|
-
import { defaultRoleForCli } from "./default-employee.js";
|
|
73
68
|
import { UserMemoryService } from "./user-memory.js";
|
|
74
69
|
import { registerUserMemoryRoutes, userMemoryOperationLog } from "./server-user-memory.js";
|
|
75
70
|
import { registerAttentionRoutes } from "./server-attention-routes.js";
|
|
76
71
|
import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, launchServerUpdate, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
|
|
77
72
|
import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
78
|
-
import { parseSessionCreationOrigin } from "./session-request.js";
|
|
79
73
|
import { registerWorkspaceRoutes } from "./server-workspace-routes.js";
|
|
80
|
-
import {
|
|
81
|
-
import { backfillSessionWorkspaces, resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
|
|
74
|
+
import { backfillSessionWorkspaces } from "./workspace-binding.js";
|
|
82
75
|
import { getErrorMessage } from "./error-utils.js";
|
|
83
76
|
import { asyncRoute, jsonErrorHandler } from "./express-async.js";
|
|
84
77
|
import { FeedbackReceiver, FeedbackReporter } from "./feedback.js";
|
|
@@ -89,9 +82,6 @@ import { computeRelaunch } from "./relaunch.js";
|
|
|
89
82
|
import { createRetentionSweep, startRetentionTimer } from "./retention.js";
|
|
90
83
|
import { RuntimeConfigState } from "./runtime-config.js";
|
|
91
84
|
import { safeServiceInstalled } from "./tui/runtime-utils.js";
|
|
92
|
-
import { toSessionDetailDTO } from "./session-transport.js";
|
|
93
|
-
import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
94
|
-
import { enrichStructuredMessages } from "./structured-client-protocol.js";
|
|
95
85
|
import { registerUploadRoutes, invalidateUploadUsage } from "./upload-routes.js";
|
|
96
86
|
import { uploadReferenceGuard } from "./upload-reference-guard.js";
|
|
97
87
|
import { configTemporaryRoot } from "./managed-temp.js";
|
|
@@ -162,15 +152,6 @@ function readBuildInfo() {
|
|
|
162
152
|
const BUILD_INFO = readBuildInfo();
|
|
163
153
|
const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
|
|
164
154
|
const SERVER_INSTANCE_ID = crypto.randomUUID();
|
|
165
|
-
/**
|
|
166
|
-
* 解析 PTY 会话的 provider:显式 `provider` 字段优先,其次按命令前缀识别;
|
|
167
|
-
* 都识别不出时回落到 Claude,与旧版客户端行为保持一致。
|
|
168
|
-
*/
|
|
169
|
-
function resolveProviderForCommand(provider, command) {
|
|
170
|
-
if (isSessionProvider(provider))
|
|
171
|
-
return provider;
|
|
172
|
-
return inferProviderFromCommand(command) ?? "claude";
|
|
173
|
-
}
|
|
174
155
|
// ── Startup error handling ──
|
|
175
156
|
let activeFeedback = null;
|
|
176
157
|
process.on("uncaughtException", (err) => {
|
|
@@ -263,7 +244,8 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
263
244
|
// Explicit and bounded: login/bearer limits key on req.ip, so forwarded headers are only
|
|
264
245
|
// honoured for the configured proxy hops (see resolveTrustProxy).
|
|
265
246
|
app.set("trust proxy", resolveTrustProxy(config.trustProxy));
|
|
266
|
-
const
|
|
247
|
+
const databasePath = resolveDatabasePath(configPath);
|
|
248
|
+
const storage = new WandStorage(databasePath);
|
|
267
249
|
const systemCalls = systemAiSessions(storage);
|
|
268
250
|
systemCalls.recoverInterrupted();
|
|
269
251
|
if (!storage.getConfigValue("migration:session-workspaces:v1")) {
|
|
@@ -273,9 +255,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
273
255
|
const taskTitles = taskTitleService(storage, { config });
|
|
274
256
|
const runtimeConfig = new RuntimeConfigState(config);
|
|
275
257
|
const openRouter = new OpenRouterFreeModelsService(storage, options.openRouterFetch);
|
|
276
|
-
const speechPolisher = new SpeechPolisherService({ config, free: openRouter,
|
|
277
|
-
employee: () => storage.getSystemSiliconEmployee(SPEECH_POLISHER_KEY),
|
|
278
|
-
trace: employee => systemCalls.forEmployee(employee) });
|
|
279
258
|
const authService = new AuthService(storage);
|
|
280
259
|
const webLoginTickets = new WebLoginTickets();
|
|
281
260
|
const settingsAccess = new SettingsWebAccess();
|
|
@@ -345,9 +324,27 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
345
324
|
}
|
|
346
325
|
const storageCleanup = createStorageCleanup(configDir, { uploads: storage.uploads,
|
|
347
326
|
uploadsActive: () => sessionRegistry.listSlim().some(session => (session.sessionKind === "structured" && session.status === "running") || (session.queuedMessages?.length ?? 0) > 0),
|
|
348
|
-
onUploadRemoved: invalidateUploadUsage
|
|
349
|
-
|
|
350
|
-
|
|
327
|
+
onUploadRemoved: invalidateUploadUsage,
|
|
328
|
+
// 历史构建包也会持续堆积(每次 debug 构建一个新版本号),跟着同一轮定时回收走。
|
|
329
|
+
pruneDistributionArtifacts: async () => {
|
|
330
|
+
const results = await Promise.all([
|
|
331
|
+
distributionManager.pruneAndroidApkDirectory(),
|
|
332
|
+
distributionManager.pruneMacosBetaDirectory(),
|
|
333
|
+
distributionManager.pruneIosIpaDirectory(),
|
|
334
|
+
]);
|
|
335
|
+
return {
|
|
336
|
+
deleted: results.flatMap((result) => result.deleted),
|
|
337
|
+
freedBytes: results.reduce((total, result) => total + result.freedBytes, 0),
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
// Pi 原生回合目录(pi-native/)按回合数增长,同样交给同一轮定时回收。
|
|
341
|
+
collectNativeArtifacts: () => structuredSessions.collectNativeGarbage({
|
|
342
|
+
liveSessionIds: new Set(sessionRegistry.listSlim().map((session) => session.id)),
|
|
343
|
+
retentionMs: STORAGE_CLEANUP_RETENTION_DAYS * 24 * 60 * 60 * 1000,
|
|
344
|
+
}),
|
|
345
|
+
// 旧版本遗留的迁移前快照:无上限堆积。迁移成功路径已在 storage-schema 修剪。
|
|
346
|
+
pruneMigrationSnapshots: () => pruneMigrationBackups(databasePath, { dryRun: false }) });
|
|
347
|
+
const localModels = new LocalModelSetupService({ configDir, decisions,
|
|
351
348
|
decisionConfig: () => config.localDecision ?? { enabled: false, pythonPath: "", modelPath: "" },
|
|
352
349
|
configureDecision(next) {
|
|
353
350
|
decisions.assertConfigurable();
|
|
@@ -676,7 +673,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
676
673
|
registerWorkspaceRoutes(app, storage, sessionRegistry, { config, free: openRouter });
|
|
677
674
|
registerMissionRoutes(app, missions);
|
|
678
675
|
registerUploadRoutes(app, sessionRegistry, { uploads: storage.uploads, temporaryRoot: configTemporaryRoot(configDir) });
|
|
679
|
-
registerSpeechRoutes(app, { speech, requireSessions, requireAdmin, polisher: speechPolisher });
|
|
680
676
|
registerLocalModelRoutes(app, { models: localModels, requireSessions, requireAdmin });
|
|
681
677
|
registerStorageCleanupRoutes(app, storageCleanup, requireAdmin);
|
|
682
678
|
app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
|
|
@@ -723,99 +719,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
723
719
|
}));
|
|
724
720
|
registerFileRoutes(app, { storage, defaultCwd: config.defaultCwd });
|
|
725
721
|
// ── Session control ──
|
|
726
|
-
app.post("/api/commands", asyncRoute(async (req, res) => {
|
|
727
|
-
const body = req.body;
|
|
728
|
-
if (body.employeeId !== undefined || body.teamId !== undefined
|
|
729
|
-
|| (body.subject !== undefined && body.subject?.type !== "cli")) {
|
|
730
|
-
res.status(400).json({ error: "PTY 只能选择 CLI 工具。" });
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (body.provider !== undefined && !isSessionProvider(body.provider)) {
|
|
734
|
-
res.status(400).json({ error: "请选择有效的执行工具。" });
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
const interactiveShell = body.shell === true;
|
|
738
|
-
// 连接应用只能启动 Provider CLI 会话(provider 字段 + 该 CLI 的标准可执行名)。
|
|
739
|
-
// 任意自定义命令字符串需要 admin。shell:true 是原生客户端“空白终端”(服务端配置的
|
|
740
|
-
// 登录 shell,不接受外部命令):为保持现有客户端可用而保留,见 AGENTS 与报告。
|
|
741
|
-
if (!principalHasScope(req.principal, "admin") && !interactiveShell) {
|
|
742
|
-
// Provider CLI only: the executable must be the provider's own CLI name (old clients send
|
|
743
|
-
// the provider id as the command, so an absent provider is inferred from it).
|
|
744
|
-
const rawCommand = typeof body.command === "string" ? body.command.trim() : "";
|
|
745
|
-
const provider = isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand);
|
|
746
|
-
const isProviderCli = !!provider && (!rawCommand || rawCommand === provider || rawCommand === providerCliCommand(provider));
|
|
747
|
-
if (!isProviderCli) {
|
|
748
|
-
res.status(403).json({ error: "当前连接只能启动已配置的 CLI 工具,不能执行自定义命令。", code: "forbidden" });
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (!interactiveShell && !body.command?.trim()) {
|
|
753
|
-
res.status(400).json({ error: "请输入要执行的命令。" });
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (body.mode !== undefined && !isExecutionMode(body.mode)) {
|
|
757
|
-
res.status(400).json({ error: `无效执行模式: ${String(body.mode)}` });
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
const initialInput = body.initialInput?.trim();
|
|
761
|
-
try {
|
|
762
|
-
const origin = parseSessionCreationOrigin(body);
|
|
763
|
-
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
764
|
-
const rawCommand = body.command?.trim() ?? "";
|
|
765
|
-
const provider = interactiveShell
|
|
766
|
-
? undefined
|
|
767
|
-
: resolveProviderForCommand(body.provider, rawCommand);
|
|
768
|
-
// Older clients used the provider id as the PTY command. Qoder's executable
|
|
769
|
-
// is named qodercli, so keep those clients working while preserving custom commands.
|
|
770
|
-
const command = provider === "qoder" && rawCommand === "qoder"
|
|
771
|
-
? "qodercli"
|
|
772
|
-
: rawCommand;
|
|
773
|
-
const effectiveModel = provider ? rawModel || fallbackProviderModel(config, provider) : undefined;
|
|
774
|
-
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
775
|
-
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
776
|
-
const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
777
|
-
const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
|
|
778
|
-
const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
|
|
779
|
-
&& (isSessionProvider(body.provider) || inferProviderFromCommand(command))
|
|
780
|
-
? defaultRoleForCli(storage, provider) : null;
|
|
781
|
-
const snapshot = await (interactiveShell
|
|
782
|
-
? processes.startShell(sessionCwd, body.mode ?? "default", {
|
|
783
|
-
worktreeEnabled: body.worktreeEnabled === true,
|
|
784
|
-
cols: reqCols,
|
|
785
|
-
rows: reqRows,
|
|
786
|
-
workspaceId,
|
|
787
|
-
workspaceTaskId: body.workspaceTaskId,
|
|
788
|
-
...origin,
|
|
789
|
-
})
|
|
790
|
-
: processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
|
|
791
|
-
worktreeEnabled: body.worktreeEnabled === true,
|
|
792
|
-
provider,
|
|
793
|
-
model: effectiveModel,
|
|
794
|
-
cols: reqCols,
|
|
795
|
-
rows: reqRows,
|
|
796
|
-
thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
|
|
797
|
-
systemPrompt: body.systemPrompt?.trim() || role?.prompt,
|
|
798
|
-
...sessionEmployeeFields(role),
|
|
799
|
-
workspaceId,
|
|
800
|
-
workspaceTaskId: body.workspaceTaskId,
|
|
801
|
-
...origin,
|
|
802
|
-
}));
|
|
803
|
-
recordRecentPath(storage, snapshot.cwd);
|
|
804
|
-
const compactTools = req.get("X-Wand-Tool-Projection") === "compact"
|
|
805
|
-
|| req.query.compactTools === "1";
|
|
806
|
-
if (compactTools) {
|
|
807
|
-
const messages = compactToolMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id));
|
|
808
|
-
const windowed = windowMessagesForTransport(messages, config.cardDefaults ?? {});
|
|
809
|
-
res.status(201).json(toSessionDetailDTO(snapshot, windowed));
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
res.status(201).json(toSessionDetailDTO(snapshot));
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
sendRouteError(res, error, "无法启动命令。请检查命令是否安装。");
|
|
817
|
-
}
|
|
818
|
-
}));
|
|
819
722
|
// ── WebSocket broadcast layer ──
|
|
820
723
|
let activeSslCertPath = null;
|
|
821
724
|
const server = useHttps
|
|
@@ -891,6 +794,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
891
794
|
});
|
|
892
795
|
structuredSessions.setEventEmitter((event) => {
|
|
893
796
|
taskTitles.ingest(event);
|
|
797
|
+
storage.recordTaskCommentSessionEvent(event);
|
|
894
798
|
conversationService.ingestSessionEvent(event);
|
|
895
799
|
scheduledTasks.ingestSessionEvent(event);
|
|
896
800
|
sessionCompletions.ingest(event);
|
|
@@ -990,8 +894,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
990
894
|
catch { /* noop */ }
|
|
991
895
|
conversationService.dispose();
|
|
992
896
|
localModels.dispose();
|
|
993
|
-
speech.dispose();
|
|
994
|
-
speechPolisher.dispose();
|
|
995
897
|
try {
|
|
996
898
|
structuredSessions.dispose();
|
|
997
899
|
}
|
|
@@ -1322,8 +1224,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1322
1224
|
catch { /* best-effort shutdown */ }
|
|
1323
1225
|
conversationService.dispose();
|
|
1324
1226
|
localModels.dispose();
|
|
1325
|
-
speech.dispose();
|
|
1326
|
-
speechPolisher.dispose();
|
|
1327
1227
|
try {
|
|
1328
1228
|
structuredSessions.dispose();
|
|
1329
1229
|
}
|
|
@@ -42,6 +42,8 @@ export declare class ConversationRepository {
|
|
|
42
42
|
acceptConversationSession(instance: ConversationInstance, createSession: () => string, title: string, input: string, request: ConversationRequest): void;
|
|
43
43
|
/** Attach an independent interactive session, without copying its transcript or executing it. */
|
|
44
44
|
linkEmployeeSession(instance: ConversationInstance, session: SessionSnapshot): void;
|
|
45
|
+
/** Remove only the canonical system/employee link, never a user's chat message. */
|
|
46
|
+
unlinkEmployeeSession(sessionId: string): void;
|
|
45
47
|
employeeConversationForSession(sessionId: string): string | null;
|
|
46
48
|
conversationSessionIds(id: string): string[];
|
|
47
49
|
conversationTaskStartup(taskId: string): ConversationRequest["receipt"] | null;
|
|
@@ -163,6 +163,10 @@ export class ConversationRepository {
|
|
|
163
163
|
.run(messageId, instance.id, JSON.stringify(turn), session.startedAt);
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
+
/** Remove only the canonical system/employee link, never a user's chat message. */
|
|
167
|
+
unlinkEmployeeSession(sessionId) {
|
|
168
|
+
this.db.prepare("DELETE FROM conversation_messages WHERE id = ?").run(`employee-session:${sessionId}`);
|
|
169
|
+
}
|
|
166
170
|
employeeConversationForSession(sessionId) {
|
|
167
171
|
const row = this.db.prepare("SELECT conversation_id FROM conversation_messages WHERE id = ?")
|
|
168
172
|
.get(`employee-session:${sessionId}`);
|
|
@@ -26,8 +26,8 @@ export declare class EmployeeRepository {
|
|
|
26
26
|
ensureSystemSiliconEmployee(seed?: SystemEmployeeSeed): SiliconEmployee;
|
|
27
27
|
/** Stable built-in decision employee; seed once and retain the user's ordered call chain. */
|
|
28
28
|
ensureDecisionExpertEmployee(): SiliconEmployee;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** The speech polisher built-in was removed; archive (never delete) a leftover row so history stays readable. */
|
|
30
|
+
retireLegacySpeechPolisherEmployee(): void;
|
|
31
31
|
/** HR owns recruitment independently; seed from configured system candidates once. */
|
|
32
32
|
ensureHrEmployee(seedAgents?: WandTaskAgent[]): SiliconEmployee;
|
|
33
33
|
saveSiliconEmployee(employee: SiliconEmployee): void;
|
|
@@ -8,8 +8,6 @@ import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "../employee-knowledge-types.js";
|
|
|
8
8
|
import { FIXED_EMPLOYEE_AVATARS, employeeAvatarValue, fixedEmployeeAvatar, isFixedAvatarEmployee } from "../fixed-employee-avatar.js";
|
|
9
9
|
import { HR_EMPLOYEE_KEY } from "../hr-employee-identity.js";
|
|
10
10
|
import { hrEmployeeDefinition } from "../hr-employee.js";
|
|
11
|
-
import { speechPolisherDefinition } from "../speech-polisher-employee.js";
|
|
12
|
-
import { SPEECH_POLISHER_KEY } from "../speech-polisher-identity.js";
|
|
13
11
|
import { SYSTEM_EMPLOYEE_KEY, systemEmployeeDefinition, systemEmployeeDefaultAgents, systemEmployeeSeedAgents } from "../system-employee.js";
|
|
14
12
|
import { LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS, mapSiliconEmployeeRow } from "./internal.js";
|
|
15
13
|
/** Employee storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
|
|
@@ -87,16 +85,10 @@ export class EmployeeRepository {
|
|
|
87
85
|
}
|
|
88
86
|
return existing;
|
|
89
87
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (!existing || existing.name !== definition.name || existing.duty !== definition.duty || existing.prompt !== definition.prompt
|
|
95
|
-
|| existing.avatar !== definition.avatar || existing.archivedAt || !existing.agents.length) {
|
|
96
|
-
this.saveSiliconEmployee(definition);
|
|
97
|
-
return definition;
|
|
98
|
-
}
|
|
99
|
-
return existing;
|
|
88
|
+
/** The speech polisher built-in was removed; archive (never delete) a leftover row so history stays readable. */
|
|
89
|
+
retireLegacySpeechPolisherEmployee() {
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
this.db.prepare("UPDATE silicon_employees SET archived_at = ?, updated_at = ? WHERE system_key = 'wand-speech-polisher' AND archived_at IS NULL").run(now, now);
|
|
100
92
|
}
|
|
101
93
|
/** HR owns recruitment independently; seed from configured system candidates once. */
|
|
102
94
|
ensureHrEmployee(seedAgents) {
|
|
@@ -141,9 +133,13 @@ export class EmployeeRepository {
|
|
|
141
133
|
const tags = isBuiltinSiliconEmployee(employee)
|
|
142
134
|
? siliconEmployeeTags(employee)
|
|
143
135
|
: parseSiliconEmployeeTags(employee.tags ?? []);
|
|
136
|
+
const systemSessionLimit = employee.systemSessionLimit === undefined ? existing?.systemSessionLimit ?? null : employee.systemSessionLimit;
|
|
137
|
+
if (systemSessionLimit !== null && (!Number.isInteger(systemSessionLimit) || systemSessionLimit < 0 || systemSessionLimit > 10_000)) {
|
|
138
|
+
throw new Error("系统调用历史保留上限必须是 0 到 10000 的整数,或 null(不限)。");
|
|
139
|
+
}
|
|
144
140
|
this.db.prepare(`INSERT INTO silicon_employees (
|
|
145
|
-
id, name, duty, prompt, avatar, agents_json, system_key, archived_at, created_at, updated_at, tags_json
|
|
146
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
141
|
+
id, name, duty, prompt, avatar, agents_json, system_key, archived_at, created_at, updated_at, tags_json, system_session_limit
|
|
142
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
147
143
|
ON CONFLICT(id) DO UPDATE SET
|
|
148
144
|
name = excluded.name,
|
|
149
145
|
duty = excluded.duty,
|
|
@@ -151,11 +147,12 @@ export class EmployeeRepository {
|
|
|
151
147
|
avatar = excluded.avatar,
|
|
152
148
|
agents_json = excluded.agents_json,
|
|
153
149
|
tags_json = excluded.tags_json,
|
|
150
|
+
system_session_limit = excluded.system_session_limit,
|
|
154
151
|
system_key = excluded.system_key,
|
|
155
152
|
archived_at = excluded.archived_at,
|
|
156
153
|
updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt,
|
|
157
154
|
// Accept a canonical read echo without persisting a display projection over a legacy value.
|
|
158
|
-
target && existing ? existing.avatar : employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags));
|
|
155
|
+
target && existing ? existing.avatar : employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags), systemSessionLimit);
|
|
159
156
|
}
|
|
160
157
|
/** Explicit, atomic migration; not called from GET, config load, seeds, or ordinary saves.
|
|
161
158
|
* Compare all expected avatar values first; change only avatar, including preserving timestamps.
|
package/dist/storage/internal.js
CHANGED
|
@@ -4,14 +4,13 @@ import { AI_TEAM_DEFAULT_MAX_STEPS, isTeamMemberRole, memberAgents, siliconEmplo
|
|
|
4
4
|
import { decisionExpertDefinition } from "../decision-expert-employee.js";
|
|
5
5
|
import { hrEmployeeDefinition } from "../hr-employee.js";
|
|
6
6
|
import { isSessionProvider } from "../session-provider.js";
|
|
7
|
-
import { speechPolisherDefinition } from "../speech-polisher-employee.js";
|
|
8
7
|
import { isWandTaskAgentKind } from "../task-types.js";
|
|
9
8
|
import { GLOBAL_WORKSPACE_ID } from "../types.js";
|
|
10
9
|
import { safeJsonParse } from "./mappers.js";
|
|
11
10
|
import { parseWandTaskAgent } from "./mappers.js";
|
|
12
11
|
// Shared module-level helpers for the storage repositories (moved verbatim from storage.ts).
|
|
13
12
|
// These system profiles are code-owned; candidates and timestamps are not part of the lock.
|
|
14
|
-
export const LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS = [decisionExpertDefinition(""),
|
|
13
|
+
export const LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS = [decisionExpertDefinition(""), hrEmployeeDefinition("")];
|
|
15
14
|
export const DEFAULT_DB_FILE = "wand.db";
|
|
16
15
|
export function resolveDatabasePath(configPath) {
|
|
17
16
|
return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
|
|
@@ -64,6 +63,7 @@ export function mapSiliconEmployeeRow(row) {
|
|
|
64
63
|
agents,
|
|
65
64
|
systemKey,
|
|
66
65
|
tags: siliconEmployeeTags({ systemKey, tags }),
|
|
66
|
+
systemSessionLimit: typeof row.system_session_limit === "number" ? row.system_session_limit : null,
|
|
67
67
|
archivedAt: typeof row.archived_at === "string" && row.archived_at ? row.archived_at : undefined,
|
|
68
68
|
createdAt: String(row.created_at),
|
|
69
69
|
updatedAt: String(row.updated_at),
|
|
@@ -272,4 +272,24 @@ export const STORAGE_MIGRATIONS = [
|
|
|
272
272
|
if (!columns.some(column => column.name === "acceptance_json"))
|
|
273
273
|
db.exec("ALTER TABLE wand_tasks ADD COLUMN acceptance_json TEXT");
|
|
274
274
|
} },
|
|
275
|
+
{ version: 11, name: "task-comment-dispatch", up: db => db.exec(`
|
|
276
|
+
CREATE TABLE IF NOT EXISTS wand_task_comment_dispatches (
|
|
277
|
+
comment_id TEXT PRIMARY KEY REFERENCES wand_task_comments(id) ON DELETE CASCADE,
|
|
278
|
+
task_id TEXT NOT NULL REFERENCES wand_tasks(id) ON DELETE CASCADE,
|
|
279
|
+
request_id TEXT NOT NULL, signature TEXT NOT NULL, source_comment_id TEXT,
|
|
280
|
+
employee_id TEXT, employee_name TEXT, instruction TEXT NOT NULL,
|
|
281
|
+
task_version TEXT NOT NULL, state TEXT NOT NULL, session_id TEXT, error TEXT, execution_status TEXT,
|
|
282
|
+
UNIQUE(task_id, request_id)
|
|
283
|
+
);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_comment_dispatch_session ON wand_task_comment_dispatches(session_id);
|
|
285
|
+
`) },
|
|
286
|
+
{ version: 12, name: "employee-system-session-limit", up: db => {
|
|
287
|
+
const columns = db.prepare("PRAGMA table_info(silicon_employees)").all();
|
|
288
|
+
if (!columns.some(column => column.name === "system_session_limit")) {
|
|
289
|
+
db.exec("ALTER TABLE silicon_employees ADD COLUMN system_session_limit INTEGER");
|
|
290
|
+
}
|
|
291
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_system_ai_employee_history
|
|
292
|
+
ON command_sessions(json_extract(session_options, '$.employeeId'), started_at DESC)
|
|
293
|
+
WHERE automation_id GLOB 'system-ai:*'`);
|
|
294
|
+
} },
|
|
275
295
|
];
|
package/dist/storage/readonly.js
CHANGED
|
@@ -3,6 +3,9 @@ import { decryptVaultSecretTyped } from "../password-manager.js";
|
|
|
3
3
|
import { readVaultKey } from "./vault-keys.js";
|
|
4
4
|
export function openStorageReadonly(dbPath) {
|
|
5
5
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
6
|
+
// Local CLI readers share the file with the Server writer. Configure only this connection;
|
|
7
|
+
// a transient exclusive/schema lock is not proof of a missing/revoked capability.
|
|
8
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
6
9
|
const readConfig = (key) => {
|
|
7
10
|
const value = db.prepare("SELECT value FROM app_config WHERE key = ?").get(key)?.value;
|
|
8
11
|
return typeof value === "string" ? value : null;
|
|
@@ -54,5 +54,7 @@ export declare class SessionRepository {
|
|
|
54
54
|
/** List durable metadata without reading or parsing output/messages payloads. */
|
|
55
55
|
loadSessionsSlim(): SessionSnapshot[];
|
|
56
56
|
mapSessionRow(row: SessionRow): SessionSnapshot;
|
|
57
|
+
/** Metadata-only indexed scan. Running or ambiguous records never count toward the history cap. */
|
|
58
|
+
excessSystemAiSessionIds(employeeId: string, keep: number): string[];
|
|
57
59
|
deleteSession(id: string): void;
|
|
58
60
|
}
|
|
@@ -2,6 +2,8 @@ import { sessionPersistValues } from "./mappers.js";
|
|
|
2
2
|
import { sessionRuntimeMetadataValues } from "./mappers.js";
|
|
3
3
|
import { nowIso } from "../password-manager.js";
|
|
4
4
|
import { sessionSelectFields, sessionPersistFields, sessionPersistAssignments, sessionRuntimeMetadataAssignments, mapSessionCore, sessionRowQuery } from "./mappers.js";
|
|
5
|
+
import { SYSTEM_AI_AUXILIARY_PURPOSES, SYSTEM_AI_CALL_PREFIX } from "../system-ai-call.js";
|
|
6
|
+
const AUXILIARY_HISTORY_PATTERNS = SYSTEM_AI_AUXILIARY_PURPOSES.map(purpose => `${SYSTEM_AI_CALL_PREFIX}${purpose}:?*`);
|
|
5
7
|
/** Session storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
|
|
6
8
|
export class SessionRepository {
|
|
7
9
|
db;
|
|
@@ -216,6 +218,17 @@ export class SessionRepository {
|
|
|
216
218
|
mapSessionRow(row) {
|
|
217
219
|
return mapSessionCore(row);
|
|
218
220
|
}
|
|
221
|
+
/** Metadata-only indexed scan. Running or ambiguous records never count toward the history cap. */
|
|
222
|
+
excessSystemAiSessionIds(employeeId, keep) {
|
|
223
|
+
if (!Number.isInteger(keep) || keep < 0 || keep > 10_000)
|
|
224
|
+
return [];
|
|
225
|
+
const rows = this.db.prepare(`SELECT id FROM command_sessions
|
|
226
|
+
WHERE (${AUXILIARY_HISTORY_PATTERNS.map(() => "automation_id GLOB ?").join(" OR ")})
|
|
227
|
+
AND json_extract(session_options, '$.employeeId') = ?
|
|
228
|
+
AND status IN ('exited', 'failed', 'stopped') AND ended_at IS NOT NULL
|
|
229
|
+
ORDER BY started_at DESC, rowid DESC LIMIT -1 OFFSET ?`).all(...AUXILIARY_HISTORY_PATTERNS, employeeId, keep);
|
|
230
|
+
return rows.map(row => row.id);
|
|
231
|
+
}
|
|
219
232
|
deleteSession(id) {
|
|
220
233
|
this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
|
|
221
234
|
}
|