@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readdirSync, lstatSync, unlinkSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* 迁移前 SQLite 快照(`wand.db.pre-v<N>-<时间戳>-<随机>.bak`)的有界保留。
|
|
5
|
+
*
|
|
6
|
+
* 每次 schema 升级都会 `VACUUM INTO` 一份完整副本,而快照体积等于整库大小,
|
|
7
|
+
* 没有上限时旧备份会无限累积。这里只做「有界保留」,不做数据清理:
|
|
8
|
+
*
|
|
9
|
+
* - 迁移**失败**时绝不调用(调用方保证只在迁移成功且健康检查通过后调用);
|
|
10
|
+
* - 默认保留最近 `keep` 份,外加调用方明确要求保留的那一份(刚成功这次迁移的回滚点);
|
|
11
|
+
* - 只认迁移备份的命名,不碰 `wand.db`、`backups/` 里的人工恢复点或配置快照;
|
|
12
|
+
* - 删除失败不影响迁移结果。
|
|
13
|
+
*/
|
|
14
|
+
/** 迁移前快照的命名:`<db>.pre-v12-2026-10-18T09-58-41-937Z-ca8636.bak`。 */
|
|
15
|
+
const MIGRATION_BACKUP_PATTERN = /\.pre-v\d+-\d{4}-\d{2}-\d{2}T[\d-]+Z-[0-9a-f]{6}\.bak$/;
|
|
16
|
+
/** 人工标记的迁移前快照,例如 `pre-organization-baseline-repair-<时间戳>.bak`。 */
|
|
17
|
+
const MANUAL_BACKUP_PATTERN = /\.pre-(?!v\d+-)[A-Za-z0-9-]+-\d{4}-\d{2}-\d{2}T[\d-]+Z\.bak$/;
|
|
18
|
+
export const DEFAULT_MIGRATION_BACKUP_KEEP = 2;
|
|
19
|
+
/**
|
|
20
|
+
* 保留最近若干份迁移快照。`keepPath` 是本次刚生成、必须保留的回滚点。
|
|
21
|
+
*
|
|
22
|
+
* `keep` 统计的是「自动迁移快照」,人工命名的快照不计入额度,单独保留最近一份,
|
|
23
|
+
* 避免把开发者手工留的基线修复点一起清掉。
|
|
24
|
+
*/
|
|
25
|
+
export function pruneMigrationBackups(dbPath, options = {}) {
|
|
26
|
+
const result = { deleted: [], freedBytes: 0, kept: [] };
|
|
27
|
+
const keepCount = Math.max(1, options.keep ?? DEFAULT_MIGRATION_BACKUP_KEEP);
|
|
28
|
+
const directory = path.dirname(dbPath);
|
|
29
|
+
const base = path.basename(dbPath);
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
const automatic = [];
|
|
38
|
+
const manual = [];
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isFile() || !entry.name.startsWith(`${base}.pre-`))
|
|
41
|
+
continue;
|
|
42
|
+
const filePath = path.join(directory, entry.name);
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
stat = lstatSync(filePath);
|
|
46
|
+
// 符号链接一律不碰:备份文件必须是普通文件。
|
|
47
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const record = { name: entry.name, filePath, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
54
|
+
if (MIGRATION_BACKUP_PATTERN.test(entry.name))
|
|
55
|
+
automatic.push(record);
|
|
56
|
+
else if (MANUAL_BACKUP_PATTERN.test(entry.name))
|
|
57
|
+
manual.push(record);
|
|
58
|
+
}
|
|
59
|
+
const keepName = options.keepPath ? path.basename(options.keepPath) : null;
|
|
60
|
+
const newestFirst = (a, b) => b.mtimeMs - a.mtimeMs;
|
|
61
|
+
automatic.sort(newestFirst);
|
|
62
|
+
manual.sort(newestFirst);
|
|
63
|
+
const keep = new Set();
|
|
64
|
+
if (keepName)
|
|
65
|
+
keep.add(keepName);
|
|
66
|
+
for (const record of automatic.slice(0, keepCount))
|
|
67
|
+
keep.add(record.name);
|
|
68
|
+
// 人工快照只留最近一份,其余按同一规则回收。
|
|
69
|
+
if (manual.length > 0)
|
|
70
|
+
keep.add(manual[0].name);
|
|
71
|
+
for (const record of [...automatic, ...manual]) {
|
|
72
|
+
if (keep.has(record.name)) {
|
|
73
|
+
result.kept.push(record.name);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (options.dryRun) {
|
|
77
|
+
result.deleted.push(record.name);
|
|
78
|
+
result.freedBytes += record.size;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
unlinkSync(record.filePath);
|
|
83
|
+
result.deleted.push(record.name);
|
|
84
|
+
result.freedBytes += record.size;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 删不掉(权限/占用)就留着,下次扫描再试。
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
package/dist/pi-tmux-host.d.ts
CHANGED
|
@@ -40,5 +40,33 @@ export declare class PiTmuxHost implements StructuredExecHost {
|
|
|
40
40
|
listRuns(): Promise<StructuredRunState[]>;
|
|
41
41
|
kill(job: NativeJob): Promise<void>;
|
|
42
42
|
forgetRun(runId: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* 回收 pi-native 下已经没用的执行产物。每次 Pi 原生回合都会在 slot 里
|
|
45
|
+
* `mkdtemp` 一个新 turn 目录(events.jsonl / stderr.log / state.json 等),
|
|
46
|
+
* 而 forgetRun 只写 ack 标记、从不删目录,所以会按回合数无限堆积。
|
|
47
|
+
*
|
|
48
|
+
* 只回收三类确定无主的东西:
|
|
49
|
+
* - slot 已 ack 且 runId 不再属于任何存活会话:整个 slot 无主;
|
|
50
|
+
* - slot 无主的,整槽删除;
|
|
51
|
+
* - slot 仍有主的,只删非 current.json 引用、已 ack 且进程已退出、超保留期的 turn。
|
|
52
|
+
*
|
|
53
|
+
* 保留 current.json 指向的 turn:`listRuns` / `adoptRun` 仍靠它的 events.jsonl
|
|
54
|
+
* 重放恢复;未 ack 的 slot 可能是尚未收尾的回合,一律不动。
|
|
55
|
+
*/
|
|
56
|
+
collectGarbage(options: {
|
|
57
|
+
/** 仍在数据库里的会话 id;不在其中的 slot 视为无主。 */
|
|
58
|
+
liveSessionIds: ReadonlySet<string>;
|
|
59
|
+
retentionMs: number;
|
|
60
|
+
now?: number;
|
|
61
|
+
}): PiNativeGcReport;
|
|
62
|
+
private readJob;
|
|
63
|
+
/** 无主 slot 是否已整体静默:所有 turn 都不再是运行中,且最新写入超过保留期。 */
|
|
64
|
+
private slotIsIdle;
|
|
65
|
+
}
|
|
66
|
+
/** pi-native 回收结果:删了几个 slot / 几个旧 turn / 释放多少字节。 */
|
|
67
|
+
export interface PiNativeGcReport {
|
|
68
|
+
removedSlots: number;
|
|
69
|
+
removedTurns: number;
|
|
70
|
+
freedBytes: number;
|
|
43
71
|
}
|
|
44
72
|
export {};
|
package/dist/pi-tmux-host.js
CHANGED
|
@@ -382,4 +382,193 @@ export class PiTmuxHost {
|
|
|
382
382
|
writeFileSync(job.ack, "consumed\n", { mode: 0o600 });
|
|
383
383
|
this.observed.delete(runId);
|
|
384
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* 回收 pi-native 下已经没用的执行产物。每次 Pi 原生回合都会在 slot 里
|
|
387
|
+
* `mkdtemp` 一个新 turn 目录(events.jsonl / stderr.log / state.json 等),
|
|
388
|
+
* 而 forgetRun 只写 ack 标记、从不删目录,所以会按回合数无限堆积。
|
|
389
|
+
*
|
|
390
|
+
* 只回收三类确定无主的东西:
|
|
391
|
+
* - slot 已 ack 且 runId 不再属于任何存活会话:整个 slot 无主;
|
|
392
|
+
* - slot 无主的,整槽删除;
|
|
393
|
+
* - slot 仍有主的,只删非 current.json 引用、已 ack 且进程已退出、超保留期的 turn。
|
|
394
|
+
*
|
|
395
|
+
* 保留 current.json 指向的 turn:`listRuns` / `adoptRun` 仍靠它的 events.jsonl
|
|
396
|
+
* 重放恢复;未 ack 的 slot 可能是尚未收尾的回合,一律不动。
|
|
397
|
+
*/
|
|
398
|
+
collectGarbage(options) {
|
|
399
|
+
const report = { removedSlots: 0, removedTurns: 0, freedBytes: 0 };
|
|
400
|
+
const now = options.now ?? Date.now();
|
|
401
|
+
let entries;
|
|
402
|
+
try {
|
|
403
|
+
entries = readdirSync(this.root, { withFileTypes: true });
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return report;
|
|
407
|
+
}
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
if (!entry.isDirectory())
|
|
410
|
+
continue;
|
|
411
|
+
const slot = path.join(this.root, entry.name);
|
|
412
|
+
const job = this.readJob(slot);
|
|
413
|
+
if (!job)
|
|
414
|
+
continue;
|
|
415
|
+
const acked = existsSync(job.ack);
|
|
416
|
+
const sessionId = job.runId.replace(/^structured:/, "");
|
|
417
|
+
const orphan = !options.liveSessionIds.has(sessionId);
|
|
418
|
+
if (orphan) {
|
|
419
|
+
// 无主 slot 也得等 ack 完成:未 ack 说明回合可能还在收尾。
|
|
420
|
+
if (!acked)
|
|
421
|
+
continue;
|
|
422
|
+
if (!this.slotIsIdle(slot, job, now, options.retentionMs))
|
|
423
|
+
continue;
|
|
424
|
+
report.freedBytes += directoryBytes(slot);
|
|
425
|
+
if (removeTree(slot))
|
|
426
|
+
report.removedSlots += 1;
|
|
427
|
+
else
|
|
428
|
+
report.freedBytes -= directoryBytes(slot);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// 有主 slot:只删不被 current.json 引用的旧 turn。
|
|
432
|
+
const current = path.dirname(job.events);
|
|
433
|
+
let turns;
|
|
434
|
+
try {
|
|
435
|
+
turns = readdirSync(slot, { withFileTypes: true });
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
for (const turn of turns) {
|
|
441
|
+
if (!turn.isDirectory() || !turn.name.startsWith("turn-"))
|
|
442
|
+
continue;
|
|
443
|
+
const directory = path.join(slot, turn.name);
|
|
444
|
+
if (directory === current)
|
|
445
|
+
continue;
|
|
446
|
+
if (!existsSync(path.join(directory, "ack")))
|
|
447
|
+
continue;
|
|
448
|
+
const state = readJson(path.join(directory, "state.json"));
|
|
449
|
+
// 进程还活着的回合不能被当成垃圾。state 写 running 但 PID 已消失的
|
|
450
|
+
// (异常退出/被强杀)按已退出处理,否则这种回合会永久堆积。
|
|
451
|
+
if (!state || stateAlive(state))
|
|
452
|
+
continue;
|
|
453
|
+
let latest;
|
|
454
|
+
try {
|
|
455
|
+
latest = directoryLatestMtime(directory);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (now - latest < options.retentionMs)
|
|
461
|
+
continue;
|
|
462
|
+
const bytes = directoryBytes(directory);
|
|
463
|
+
if (!removeTree(directory))
|
|
464
|
+
continue;
|
|
465
|
+
report.removedTurns += 1;
|
|
466
|
+
report.freedBytes += bytes;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return report;
|
|
470
|
+
}
|
|
471
|
+
readJob(slot) {
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(readFileSync(path.join(slot, "current.json"), "utf8"));
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** 无主 slot 是否已整体静默:所有 turn 都不再是运行中,且最新写入超过保留期。 */
|
|
480
|
+
slotIsIdle(slot, job, now, retentionMs) {
|
|
481
|
+
const current = path.dirname(job.events);
|
|
482
|
+
const state = readJson(job.state);
|
|
483
|
+
if (state && stateAlive(state))
|
|
484
|
+
return false;
|
|
485
|
+
let latest;
|
|
486
|
+
try {
|
|
487
|
+
latest = directoryLatestMtime(slot);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (now - latest < retentionMs)
|
|
493
|
+
return false;
|
|
494
|
+
// 非 current 的 turn 里如果还有活进程,整槽不能删。
|
|
495
|
+
let turns;
|
|
496
|
+
try {
|
|
497
|
+
turns = readdirSync(slot, { withFileTypes: true });
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
for (const turn of turns) {
|
|
503
|
+
if (!turn.isDirectory() || !turn.name.startsWith("turn-"))
|
|
504
|
+
continue;
|
|
505
|
+
const directory = path.join(slot, turn.name);
|
|
506
|
+
if (directory === current)
|
|
507
|
+
continue;
|
|
508
|
+
const other = readJson(path.join(directory, "state.json"));
|
|
509
|
+
if (other && stateAlive(other))
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function readJson(file) {
|
|
516
|
+
try {
|
|
517
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/** 回合是否仍活着:state 声称未退出且 PID 真实存在。 */
|
|
524
|
+
function stateAlive(state) {
|
|
525
|
+
return state.status !== "exited" && pidAlive(state.pid);
|
|
526
|
+
}
|
|
527
|
+
function pidAlive(pid) {
|
|
528
|
+
if (!pid || pid <= 0)
|
|
529
|
+
return false;
|
|
530
|
+
try {
|
|
531
|
+
process.kill(pid, 0);
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
// EPERM means the PID exists but belongs to another user; treat as alive.
|
|
536
|
+
return error.code === "EPERM";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function directoryLatestMtime(directory) {
|
|
540
|
+
let latest = lstatSync(directory).mtimeMs;
|
|
541
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
542
|
+
const child = path.join(directory, entry.name);
|
|
543
|
+
const stat = lstatSync(child);
|
|
544
|
+
if (stat.isDirectory())
|
|
545
|
+
latest = Math.max(latest, directoryLatestMtime(child));
|
|
546
|
+
// 只看 mtime:ctime 无法回拨,且文件被任何元数据操作碰一下就会刷新,
|
|
547
|
+
// 用它做保留期会把「早已结束的回合」永远当成刚活动过。
|
|
548
|
+
else
|
|
549
|
+
latest = Math.max(latest, stat.mtimeMs);
|
|
550
|
+
}
|
|
551
|
+
return latest;
|
|
552
|
+
}
|
|
553
|
+
function directoryBytes(directory) {
|
|
554
|
+
let total = 0;
|
|
555
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
556
|
+
const child = path.join(directory, entry.name);
|
|
557
|
+
const stat = lstatSync(child);
|
|
558
|
+
if (stat.isDirectory())
|
|
559
|
+
total += directoryBytes(child);
|
|
560
|
+
else
|
|
561
|
+
total += stat.size;
|
|
562
|
+
}
|
|
563
|
+
return total;
|
|
564
|
+
}
|
|
565
|
+
/** 只删目录,不跟随符号链接。 */
|
|
566
|
+
function removeTree(directory) {
|
|
567
|
+
try {
|
|
568
|
+
rmSync(directory, { recursive: true, force: false });
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
385
574
|
}
|
|
@@ -5,6 +5,7 @@ import type { QuickCommitAiOptions } from "./ai-text-runner.js";
|
|
|
5
5
|
import type { WandStorage } from "./storage.js";
|
|
6
6
|
import type { WandTaskAgent } from "./task-types.js";
|
|
7
7
|
import type { WandConfig } from "./types.js";
|
|
8
|
+
export declare function parseSystemSessionLimit(value: unknown, existing?: SiliconEmployee | null): number | null;
|
|
8
9
|
/** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
|
|
9
10
|
export declare function employeeDraftAiOptions(storage: WandStorage, config?: WandConfig, free?: Pick<OpenRouterFreeModelsService, "resolveForCall">): QuickCommitAiOptions;
|
|
10
11
|
export interface EmployeeDraftRequest {
|
|
@@ -15,6 +15,17 @@ import { SESSION_PROVIDERS } from "./session-provider.js";
|
|
|
15
15
|
import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
|
|
16
16
|
import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
|
|
17
17
|
import { badRequest } from "./http-error.js";
|
|
18
|
+
import { systemAiSessions } from "./system-ai-session-recorder.js";
|
|
19
|
+
export function parseSystemSessionLimit(value, existing) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return existing?.systemSessionLimit ?? null;
|
|
22
|
+
if (value === null)
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 10_000) {
|
|
25
|
+
throw badRequest("系统调用历史保留上限必须是 0 到 10000 的整数,或 null(不限)。");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
18
29
|
/** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
|
|
19
30
|
export function employeeDraftAiOptions(storage, config, free) {
|
|
20
31
|
if (!config)
|
|
@@ -90,7 +101,7 @@ export function parseSystemEmployeeAgents(value, existing) {
|
|
|
90
101
|
if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
|
|
91
102
|
throw badRequest("该员工是内置的,身份不可修改。");
|
|
92
103
|
}
|
|
93
|
-
return
|
|
104
|
+
return body.agents === undefined ? existing.agents : parseEmployeeAgents(body.agents);
|
|
94
105
|
}
|
|
95
106
|
/** Partial edits preserve omitted fields and the raw saved prompt, not a generated read projection. */
|
|
96
107
|
export function parseFixedAvatarEmployeeInput(value, existing, now) {
|
|
@@ -103,7 +114,7 @@ export function parseFixedAvatarEmployeeInput(value, existing, now) {
|
|
|
103
114
|
throw badRequest("员工职责不能超过 2000 个字符。");
|
|
104
115
|
if (prompt.length > 20_000)
|
|
105
116
|
throw badRequest("员工设定 Prompt 不能超过 20000 个字符。");
|
|
106
|
-
return { ...existing, name, duty, prompt, agents, updatedAt: now };
|
|
117
|
+
return { ...existing, name, duty, prompt, agents, systemSessionLimit: parseSystemSessionLimit(body.systemSessionLimit, existing), updatedAt: now };
|
|
107
118
|
}
|
|
108
119
|
/** One validation owner for every employee candidate list, including legacy single agents. */
|
|
109
120
|
function parseEmployeeAgents(value) {
|
|
@@ -148,6 +159,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
148
159
|
avatar,
|
|
149
160
|
agents,
|
|
150
161
|
tags,
|
|
162
|
+
systemSessionLimit: parseSystemSessionLimit(body.systemSessionLimit, existing),
|
|
151
163
|
archivedAt: existing?.archivedAt,
|
|
152
164
|
createdAt: existing ? existing.createdAt : now,
|
|
153
165
|
updatedAt: now,
|
|
@@ -337,9 +349,10 @@ export function registerSiliconEmployeeRoutes(app, deps) {
|
|
|
337
349
|
const employee = isFixedAvatarEmployee(definition)
|
|
338
350
|
? parseFixedAvatarEmployeeInput(input, definition, now)
|
|
339
351
|
: isBuiltinSiliconEmployee(existing)
|
|
340
|
-
? { ...existing, agents: parseSystemEmployeeAgents(input, existing), updatedAt: now }
|
|
341
|
-
: parseSiliconEmployeeInput(input,
|
|
352
|
+
? { ...existing, agents: parseSystemEmployeeAgents(input, existing), systemSessionLimit: parseSystemSessionLimit(input.systemSessionLimit, existing), updatedAt: now }
|
|
353
|
+
: parseSiliconEmployeeInput({ ...definition, ...input }, definition, now);
|
|
342
354
|
storage.saveSiliconEmployee(employee);
|
|
355
|
+
systemAiSessions(storage).enforceRetention(employee.id);
|
|
343
356
|
notifyEmployee(employee.id);
|
|
344
357
|
res.json(storage.getSiliconEmployee(employee.id));
|
|
345
358
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { DecisionError } from "./decision-types.js";
|
|
2
|
-
import { SpeechError } from "./speech-service.js";
|
|
3
2
|
/** Behind generic /api authentication; inference-only capabilities cannot install dependencies. */
|
|
4
3
|
export function registerLocalModelRoutes(app, deps) {
|
|
5
4
|
app.use("/api/local-models", deps.requireSessions, (_req, res, next) => { res.set("Cache-Control", "no-store"); next(); });
|
|
6
5
|
const kind = (req, res, next) => {
|
|
7
|
-
if (req.params.kind !== "laya"
|
|
6
|
+
if (req.params.kind !== "laya") {
|
|
8
7
|
res.status(404).json({ error: "未知本地模型。" });
|
|
9
8
|
return;
|
|
10
9
|
}
|
|
@@ -51,20 +50,8 @@ export function registerLocalModelRoutes(app, deps) {
|
|
|
51
50
|
next(error);
|
|
52
51
|
}
|
|
53
52
|
});
|
|
54
|
-
app.patch("/api/local-models/speech/settings", deps.requireAdmin, async (req, res, next) => {
|
|
55
|
-
try {
|
|
56
|
-
if (!req.body || Array.isArray(req.body) || Object.keys(req.body).length !== 1 || typeof req.body.enabled !== "boolean") {
|
|
57
|
-
throw new DecisionError("INVALID_REQUEST", "仅允许修改语音启用状态。");
|
|
58
|
-
}
|
|
59
|
-
await deps.models.setSpeechEnabled(req.body.enabled);
|
|
60
|
-
res.status(202).json(await deps.models.status());
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
next(error);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
53
|
const errors = (error, _req, res, _next) => {
|
|
67
|
-
if (error instanceof DecisionError
|
|
54
|
+
if (error instanceof DecisionError) {
|
|
68
55
|
res.status(error.status).json({ error: error.message, code: error.code });
|
|
69
56
|
return;
|
|
70
57
|
}
|
|
@@ -68,11 +68,11 @@ export function registerSessionInputRoutes(app, ctx) {
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
try {
|
|
71
|
-
const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
|
|
71
|
+
const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)), req.body?.expectedQueue);
|
|
72
72
|
res.json(sessionResponseDTO(snapshot, req));
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
75
|
-
sendRouteError(res, surfaceDomainError(error, /^(
|
|
75
|
+
sendRouteError(res, surfaceDomainError(error, /^(排序长度与当前队列不一致,请刷新后重试。|排序参数无效。|排队消息已变化,请刷新后重试。)$/) ?? error, "无法调整排队顺序。");
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
app.patch("/api/structured-sessions/:id/queued/:index", (req, res) => {
|
|
@@ -126,7 +126,7 @@ export function registerSessionInputRoutes(app, ctx) {
|
|
|
126
126
|
}));
|
|
127
127
|
app.delete("/api/structured-sessions/:id/queued", (req, res) => {
|
|
128
128
|
try {
|
|
129
|
-
const snapshot = structured.clearQueuedMessages(req.params.id);
|
|
129
|
+
const snapshot = structured.clearQueuedMessages(req.params.id, req.body?.expectedQueue);
|
|
130
130
|
res.json(sessionResponseDTO(snapshot, req));
|
|
131
131
|
}
|
|
132
132
|
catch (error) {
|
|
@@ -4,6 +4,12 @@ import { defaultRoleForCli } from "./default-employee.js";
|
|
|
4
4
|
import { fallbackProviderModel, resolveExplicitOrFallbackModel, sessionEmployeeFields } from "./session-model.js";
|
|
5
5
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
6
6
|
import { asyncRoute } from "./express-async.js";
|
|
7
|
+
import { principalHasScope } from "./auth.js";
|
|
8
|
+
import { inferProviderFromCommand, providerCliCommand } from "./session-provider.js";
|
|
9
|
+
import { recordRecentPath } from "./recent-paths.js";
|
|
10
|
+
import { toSessionDetailDTO } from "./session-transport.js";
|
|
11
|
+
import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
12
|
+
import { enrichStructuredMessages } from "./structured-client-protocol.js";
|
|
7
13
|
import { installedPiExtensions } from "./pi-extension-resources.js";
|
|
8
14
|
import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
|
|
9
15
|
import { effectivePiSessionSettings, patchPiSessionSettings } from "./pi-session-settings.js";
|
|
@@ -18,9 +24,86 @@ import { getInputDebugMeta, startResumedPtySession, restoreArchivedSession } fro
|
|
|
18
24
|
/** Session creation, metadata/provider/model settings, resume, archive and deletion routes. */
|
|
19
25
|
export function registerSessionLifecycleRoutes(app, ctx) {
|
|
20
26
|
const { processes, structured, storage, defaultMode, config, sessions, onSessionCreated, decisions, wantsCompactTools, sessionResponseDTO, piSettingsResponse, requirePiSession, } = ctx;
|
|
21
|
-
app.post("/api/
|
|
27
|
+
app.post("/api/sessions", asyncRoute(async (req, res) => {
|
|
22
28
|
const body = req.body;
|
|
23
29
|
try {
|
|
30
|
+
if (body.kind !== "structured" && body.kind !== "pty")
|
|
31
|
+
throw badRequest("kind 必须是 structured 或 pty。");
|
|
32
|
+
if (body.prompt !== undefined && typeof body.prompt !== "string")
|
|
33
|
+
throw badRequest("prompt 必须是字符串。");
|
|
34
|
+
if (body.initialInput !== undefined && typeof body.initialInput !== "string")
|
|
35
|
+
throw badRequest("initialInput 必须是字符串。");
|
|
36
|
+
const prompt = body.prompt?.trim();
|
|
37
|
+
if (body.kind === "pty") {
|
|
38
|
+
if (prompt !== undefined)
|
|
39
|
+
throw badRequest("PTY 会话不接受结构化 prompt,请使用 initialInput。");
|
|
40
|
+
if ((body.engine !== undefined && body.engine !== "cli") || body.runner !== undefined
|
|
41
|
+
|| body.employeeId !== undefined || body.teamId !== undefined
|
|
42
|
+
|| (body.subject !== undefined && body.subject?.type !== "cli")) {
|
|
43
|
+
throw badRequest("PTY 不支持 SDK engine、runner 或员工/团队身份。");
|
|
44
|
+
}
|
|
45
|
+
if (body.provider !== undefined && !isSessionProvider(body.provider))
|
|
46
|
+
throw badRequest("请选择有效的执行工具。");
|
|
47
|
+
const interactiveShell = body.shell === true;
|
|
48
|
+
if (interactiveShell && (body.command !== undefined || body.provider !== undefined)) {
|
|
49
|
+
throw badRequest("shell:true 不能同时指定 command 或 provider。");
|
|
50
|
+
}
|
|
51
|
+
if (!req.principal || !principalHasScope(req.principal, "admin")) {
|
|
52
|
+
if (!interactiveShell) {
|
|
53
|
+
const rawCommand = typeof body.command === "string" ? body.command.trim() : "";
|
|
54
|
+
const provider = isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand);
|
|
55
|
+
const isProviderCli = !!provider && (!rawCommand || rawCommand === provider || rawCommand === providerCliCommand(provider));
|
|
56
|
+
if (!isProviderCli) {
|
|
57
|
+
res.status(403).json({ error: "当前连接只能启动已配置的 CLI 工具,不能执行自定义命令。", code: "forbidden" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!interactiveShell && !body.command?.trim())
|
|
63
|
+
throw badRequest("请输入要执行的命令。");
|
|
64
|
+
if (body.mode !== undefined && !isExecutionMode(body.mode))
|
|
65
|
+
throw badRequest(`无效执行模式: ${String(body.mode)}`);
|
|
66
|
+
const initialInput = body.initialInput?.trim();
|
|
67
|
+
const origin = parseSessionCreationOrigin(body);
|
|
68
|
+
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
69
|
+
const rawCommand = body.command?.trim() ?? "";
|
|
70
|
+
const provider = interactiveShell
|
|
71
|
+
? undefined
|
|
72
|
+
: (isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand) ?? "claude");
|
|
73
|
+
const command = provider === "qoder" && rawCommand === "qoder" ? "qodercli" : rawCommand;
|
|
74
|
+
const effectiveModel = provider ? rawModel || fallbackProviderModel(config, provider) : undefined;
|
|
75
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
76
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
77
|
+
const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
78
|
+
const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
|
|
79
|
+
const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
|
|
80
|
+
&& (isSessionProvider(body.provider) || inferProviderFromCommand(command))
|
|
81
|
+
? defaultRoleForCli(storage, provider) : null;
|
|
82
|
+
const snapshot = await (interactiveShell
|
|
83
|
+
? processes.startShell(sessionCwd, body.mode ?? "default", {
|
|
84
|
+
worktreeEnabled: body.worktreeEnabled === true, cols: reqCols, rows: reqRows, workspaceId,
|
|
85
|
+
workspaceTaskId: body.workspaceTaskId, ...origin,
|
|
86
|
+
})
|
|
87
|
+
: processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
|
|
88
|
+
worktreeEnabled: body.worktreeEnabled === true, provider, model: effectiveModel, cols: reqCols, rows: reqRows,
|
|
89
|
+
thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
|
|
90
|
+
systemPrompt: body.systemPrompt?.trim() || role?.prompt, ...sessionEmployeeFields(role),
|
|
91
|
+
workspaceId, workspaceTaskId: body.workspaceTaskId, ...origin,
|
|
92
|
+
}));
|
|
93
|
+
recordRecentPath(storage, snapshot.cwd);
|
|
94
|
+
const messages = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
95
|
+
const windowed = windowMessagesForTransport(wantsCompactTools(req) ? compactToolMessagesForTransport(messages) : messages, config.cardDefaults ?? {});
|
|
96
|
+
res.status(201).json(toSessionDetailDTO(snapshot, {
|
|
97
|
+
messages: windowed.messages, messageOffset: windowed.messageOffset, messageTotal: windowed.messageTotal,
|
|
98
|
+
}));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (body.shell !== undefined || body.command !== undefined || body.initialInput !== undefined
|
|
102
|
+
|| body.kind !== "structured") {
|
|
103
|
+
throw badRequest("结构化会话不接受 PTY 参数。");
|
|
104
|
+
}
|
|
105
|
+
if (body.engine !== undefined && body.engine !== "cli" && body.engine !== "sdk")
|
|
106
|
+
throw badRequest("请选择有效的执行引擎。");
|
|
24
107
|
if (body.teamId !== undefined || body.subject?.type === "team") {
|
|
25
108
|
throw badRequest("AI 团队请通过团队开工入口创建群聊。");
|
|
26
109
|
}
|
|
@@ -30,7 +113,7 @@ export function registerSessionLifecycleRoutes(app, ctx) {
|
|
|
30
113
|
if (employeeId === null || (body.employeeId !== undefined && !employeeId)) {
|
|
31
114
|
throw badRequest("employeeId 必须是员工 ID。");
|
|
32
115
|
}
|
|
33
|
-
if (employeeId &&
|
|
116
|
+
if (employeeId && body.runner === "pty") {
|
|
34
117
|
throw badRequest("硅基员工只支持结构化会话。");
|
|
35
118
|
}
|
|
36
119
|
const employee = employeeId ? storage.getSiliconEmployee(employeeId) : null;
|
|
@@ -103,30 +186,23 @@ export function registerSessionLifecycleRoutes(app, ctx) {
|
|
|
103
186
|
...origin,
|
|
104
187
|
});
|
|
105
188
|
onSessionCreated?.(snapshot.cwd);
|
|
106
|
-
const prompt = body.prompt?.trim();
|
|
107
189
|
if (prompt) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// 等在 HTTP 上会把用户锁在「正在创建…」且关闭按钮被禁用。与
|
|
111
|
-
// /api/sessions/:id/input 的 respondImmediately、/api/wand-tasks/:id/dispatch
|
|
112
|
-
// 一致:先回会话快照,首轮进度继续走结构化事件。
|
|
113
|
-
structured.sendMessage(snapshot.id, prompt).catch((error) => {
|
|
190
|
+
try {
|
|
191
|
+
Promise.resolve(structured.sendMessage(snapshot.id, prompt)).catch((error) => {
|
|
114
192
|
console.error("[wand] Accepted structured prompt later failed", {
|
|
115
193
|
sessionId: snapshot.id,
|
|
116
194
|
error: getInputDebugMeta(error),
|
|
117
195
|
});
|
|
118
196
|
});
|
|
119
|
-
const accepted = structured.get(snapshot.id);
|
|
120
|
-
if (!accepted)
|
|
121
|
-
throw badRequest("未找到该结构化会话。");
|
|
122
|
-
res.status(201).json(sessionResponseDTO(accepted, req));
|
|
123
|
-
return;
|
|
124
197
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error("[wand] Accepted structured prompt later failed", {
|
|
200
|
+
sessionId: snapshot.id,
|
|
201
|
+
error: getInputDebugMeta(error),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
128
204
|
}
|
|
129
|
-
res.status(201).json(sessionResponseDTO(snapshot, req));
|
|
205
|
+
res.status(201).json(sessionResponseDTO(structured.get(snapshot.id) ?? snapshot, req));
|
|
130
206
|
}
|
|
131
207
|
catch (error) {
|
|
132
208
|
sendRouteError(res, surfaceDomainError(error, /Wand Agent|harness|core/i) ?? error, "无法启动结构化会话。");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { taskDispatchReceipt } from "./task-dispatch-receipt.js";
|
|
2
|
-
import {
|
|
2
|
+
import { registerTaskCommentRoutes, submitTaskComment } from "./task-comment-dispatch.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { AgentDispatchPreflightError, dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
|
|
3
5
|
import { memberAgents } from "./ai-team-types.js";
|
|
4
6
|
import { defaultRoleForCli } from "./default-employee.js";
|
|
5
7
|
import { asyncRoute } from "./express-async.js";
|
|
@@ -200,13 +202,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
200
202
|
res.json(model.tasks.filter(task => workspaceId === undefined || task.workspaceId === workspaceId)
|
|
201
203
|
.map(task => taskDto(deps, task, model)));
|
|
202
204
|
});
|
|
203
|
-
app
|
|
204
|
-
if (!storage.getWandTask(req.params.id)) {
|
|
205
|
-
res.status(404).json({ error: "未找到该任务。" });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
res.set("Cache-Control", "no-store").json({ comments: storage.listWandTaskComments(req.params.id) });
|
|
209
|
-
});
|
|
205
|
+
registerTaskCommentRoutes(app, deps);
|
|
210
206
|
app.get("/api/wand-task-agent-defaults", (_req, res) => {
|
|
211
207
|
res.json(readTaskBoardLastAgent(storage));
|
|
212
208
|
});
|
|
@@ -644,7 +640,6 @@ export function registerTaskRoutes(app, deps) {
|
|
|
644
640
|
throw badRequest("所选工具不支持该运行模式,请重新选择。");
|
|
645
641
|
if (employee && body.agent !== undefined && !override)
|
|
646
642
|
throw badRequest("请选择有效的本次工具配置。");
|
|
647
|
-
const executionEmployee = employee && override ? { ...employee, agents: [override] } : employee;
|
|
648
643
|
const agent = override ?? selected?.agent ?? (body.agent === undefined
|
|
649
644
|
? cliDefault ?? task.agent ?? fallbackAgent ?? parseWandTaskAgent(JSON.stringify(body))
|
|
650
645
|
: parseTaskAgent(body.agent, task.agent?.mode, task.agent?.kind));
|
|
@@ -666,9 +661,19 @@ export function registerTaskRoutes(app, deps) {
|
|
|
666
661
|
assertTaskWorkspaceMove(storage, task.id, workspaceId);
|
|
667
662
|
task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
|
|
668
663
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
664
|
+
// Compatibility creation/drag clients also enter the same durable comment path for employees.
|
|
665
|
+
// No implicit description fallback: a first dispatch needs an explicit round instruction too.
|
|
666
|
+
if (employee) {
|
|
667
|
+
const result = await submitTaskComment(deps, task.id, { requestId: text(body.requestId) || randomUUID(), body: requestedPrompt, employeeId: employee.id }, override ?? undefined);
|
|
668
|
+
const session = result.comment.dispatch?.sessionId ? sessions?.getSlim(result.comment.dispatch.sessionId)
|
|
669
|
+
?? storage.getSessionSlim(result.comment.dispatch.sessionId) : null;
|
|
670
|
+
if (session)
|
|
671
|
+
storage.updateWandTask(task.id, { agent, executionSubject: { type: "employee", id: employee.id, ...(override ? { agentOverride: override } : {}) }, status: task.status === "todo" ? "doing" : task.status });
|
|
672
|
+
res.status(202).json({ ok: true, taskId: task.id, subject: { type: "employee", id: employee.id }, comment: result.comment,
|
|
673
|
+
session: session ? { ...taskSessionSummary(session), employeeCandidateIndex: session.employeeCandidateIndex } : null });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}` });
|
|
672
677
|
task = storage.getWandTask(task.id);
|
|
673
678
|
const usedSubject = subject?.type === "employee" ? { type: "employee", id: subject.id, ...(override ? { agentOverride: override } : {}) } : subject ?? { type: "cli", id: agent.provider };
|
|
674
679
|
storage.updateWandTask(task.id, {
|
|
@@ -709,7 +714,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
709
714
|
});
|
|
710
715
|
}
|
|
711
716
|
catch (error) {
|
|
712
|
-
sendRouteError(res, surfaceDomainError(error, /^(任务所属项目已被删除,请重新指定。|任务容器不存在,请恢复任务后再派发。|绑定员工只支持结构化会话。|员工执行候选与启动快照不一致。|当前服务未启用结构化会话,无法派发 Agent。|当前服务未启用终端会话,无法派发 Agent。|LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。)$/) ?? error, "无法派发 Agent。");
|
|
717
|
+
sendRouteError(res, error instanceof AgentDispatchPreflightError ? badRequest(error.message) : surfaceDomainError(error, /^(任务所属项目已被删除,请重新指定。|任务容器不存在,请恢复任务后再派发。|绑定员工只支持结构化会话。|员工执行候选与启动快照不一致。|当前服务未启用结构化会话,无法派发 Agent。|当前服务未启用终端会话,无法派发 Agent。|LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。)$/) ?? error, "无法派发 Agent。");
|
|
713
718
|
}
|
|
714
719
|
}));
|
|
715
720
|
app.get("/api/wand-tasks/:id", asyncRoute(async (req, res) => {
|