@co0ontty/wand 4.80.0 → 4.80.1-beta.g1fbbefb
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/dist/ai-team-availability.js +0 -1
- package/dist/ai-team-live.d.ts +3 -1
- package/dist/ai-team-live.js +31 -2
- package/dist/ai-team-runner.d.ts +2 -1
- package/dist/ai-team-runner.js +17 -7
- package/dist/ai-team-types.d.ts +8 -0
- package/dist/ai-team-types.js +4 -0
- package/dist/build-info.json +4 -4
- package/dist/claude-pty-bridge.js +16 -1
- package/dist/cli.js +0 -9
- package/dist/config.d.ts +2 -10
- package/dist/config.js +1 -73
- package/dist/git-quick-commit.d.ts +3 -6
- package/dist/git-quick-commit.js +38 -75
- package/dist/language-prompt.d.ts +1 -1
- package/dist/language-prompt.js +1 -1
- package/dist/native-session-title.d.ts +17 -0
- package/dist/native-session-title.js +257 -0
- package/dist/process-manager.d.ts +3 -0
- package/dist/process-manager.js +37 -6
- package/dist/server-session-routes.js +4 -29
- package/dist/server-settings-routes.js +2 -177
- package/dist/server-task-routes.js +1 -1
- package/dist/server-workspace-routes.js +1 -1
- package/dist/server.js +1 -13
- package/dist/session-ai-context.d.ts +3 -7
- package/dist/session-ai-context.js +0 -25
- package/dist/session-topic.d.ts +19 -0
- package/dist/session-topic.js +53 -3
- package/dist/session-transport.js +0 -1
- package/dist/silicon-employee-draft.d.ts +1 -1
- package/dist/storage.js +12 -18
- package/dist/structured-claude-adapter.d.ts +0 -6
- package/dist/structured-claude-adapter.js +1 -5
- package/dist/structured-provider-common.d.ts +8 -4
- package/dist/structured-provider-common.js +14 -15
- package/dist/structured-session-manager.d.ts +4 -40
- package/dist/structured-session-manager.js +53 -773
- package/dist/types.d.ts +12 -27
- package/dist/web-ui/content/ai-teams.js +9 -9
- package/dist/web-ui/content/scripts.js +70 -70
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/ws-broadcast.js +19 -7
- package/package.json +1 -2
- package/dist/claude-sdk-runner.d.ts +0 -59
- package/dist/claude-sdk-runner.js +0 -188
- package/dist/claude-skills.d.ts +0 -6
- package/dist/claude-skills.js +0 -53
- package/dist/system-ai.d.ts +0 -27
- package/dist/system-ai.js +0 -424
package/dist/git-quick-commit.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
4
|
-
import { callSystemAiTextWithFallback } from "./system-ai.js";
|
|
5
3
|
import { buildChildEnv } from "./env-utils.js";
|
|
4
|
+
import { buildLanguageDirective } from "./language-prompt.js";
|
|
6
5
|
import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
|
|
7
6
|
import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
|
|
8
7
|
import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
|
|
@@ -17,10 +16,6 @@ const MAX_FILE_ENTRIES = 200;
|
|
|
17
16
|
// 30s 在 API 抖动时不够用,放宽到 60s。
|
|
18
17
|
const CLAUDE_MESSAGE_TIMEOUT_MS = 60_000;
|
|
19
18
|
const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
|
|
20
|
-
// API mode can contain several profiles. Keep each probe bounded so the whole
|
|
21
|
-
// chain can still reach the current-session CLI before native clients' 180s
|
|
22
|
-
// request deadline.
|
|
23
|
-
const DIRECT_API_PROFILE_TIMEOUT_MS = 20_000;
|
|
24
19
|
const QUICK_COMMIT_CLI_TIMEOUT_MS = 120_000;
|
|
25
20
|
const MAX_DIFF_FOR_AI = 100_000;
|
|
26
21
|
// 迭代提示词模式下只给文件清单做核对,不需要完整 diff:单次上限比 diff 小一个量级。
|
|
@@ -283,31 +278,37 @@ export class QuickCommitError extends Error {
|
|
|
283
278
|
}
|
|
284
279
|
}
|
|
285
280
|
// ── AI commit message generation ──
|
|
281
|
+
/**
|
|
282
|
+
* 一次性 Claude 文本生成(commit message / tag / 提示词优化…)。
|
|
283
|
+
*
|
|
284
|
+
* 直接 spawn 本机 `claude -p`,与 PTY / 结构化会话同源:认证、模型、`~/.claude`
|
|
285
|
+
* 配置完全一致。对齐原来 SDK 时代的行为:
|
|
286
|
+
* - `--tools ""` 关掉全部工具:纯文本生成不需要工具,也避免一次性短调用顺手读文件;
|
|
287
|
+
* - `--no-session-persistence`:不污染 `~/.claude/projects/` 的会话历史;
|
|
288
|
+
* - `--strict-mcp-config`(不给 --mcp-config):跳过用户配置的 MCP,省启动时间与 token。
|
|
289
|
+
* 语言指令与任务规则都走 `--append-system-prompt`,任务自己的输出格式排在后面。
|
|
290
|
+
*/
|
|
286
291
|
async function callClaudeText(request, cwd, language, opts) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
throw new QuickCommitError(error.message, error.code);
|
|
308
|
-
}
|
|
309
|
-
throw error;
|
|
310
|
-
}
|
|
292
|
+
const args = ["-p", "--output-format", "text", "--tools", "", "--no-session-persistence", "--strict-mcp-config"];
|
|
293
|
+
const languageDirective = language ? buildLanguageDirective(language) : "";
|
|
294
|
+
const systemPrompt = [languageDirective, (request.system ?? "").trim()].filter(Boolean).join("\n\n");
|
|
295
|
+
if (systemPrompt)
|
|
296
|
+
args.push("--append-system-prompt", systemPrompt);
|
|
297
|
+
const model = opts.model?.trim();
|
|
298
|
+
if (model && model !== "default")
|
|
299
|
+
args.push("--model", model);
|
|
300
|
+
const effort = thinkingEffortToClaudeCliEffort(opts.thinkingEffort ?? "off");
|
|
301
|
+
if (effort)
|
|
302
|
+
args.push("--effort", effort);
|
|
303
|
+
const stdout = await runCliText("claude", args, request.prompt, {
|
|
304
|
+
cwd,
|
|
305
|
+
timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS,
|
|
306
|
+
inheritEnv: opts.inheritEnv,
|
|
307
|
+
});
|
|
308
|
+
const text = stdout.trim();
|
|
309
|
+
if (!text)
|
|
310
|
+
throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
311
|
+
return text;
|
|
311
312
|
}
|
|
312
313
|
function stripFences(raw) {
|
|
313
314
|
return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
@@ -657,17 +658,6 @@ async function callCliAiText(request, cwd, language, opts) {
|
|
|
657
658
|
return callGeminiText(request, cwd, opts);
|
|
658
659
|
return callClaudeText(request, cwd, language, opts);
|
|
659
660
|
}
|
|
660
|
-
async function callDirectApiText(request, systemAi) {
|
|
661
|
-
const text = await callSystemAiTextWithFallback(request, systemAi, DIRECT_API_PROFILE_TIMEOUT_MS);
|
|
662
|
-
if (!text.trim()) {
|
|
663
|
-
throw new QuickCommitError("直连 API 返回了空结果。", "EMPTY_AI_MESSAGE");
|
|
664
|
-
}
|
|
665
|
-
return text;
|
|
666
|
-
}
|
|
667
|
-
function aiFallbackFailed(primary, primaryError, fallbackError) {
|
|
668
|
-
const fallback = primary === "直连 API" ? "CLI" : "直连 API";
|
|
669
|
-
return new QuickCommitError(`${primary} 与 ${fallback} 均失败:${getGitErrorMessage(primaryError)};${getGitErrorMessage(fallbackError)}`, "AI_FALLBACK_FAILED");
|
|
670
|
-
}
|
|
671
661
|
/**
|
|
672
662
|
* 多候选降级时给整条链的时间上限。原生客户端的请求宽限约 180s,
|
|
673
663
|
* 候选再多也不能把整个请求拖死;单次仍受各自 CLI 超时保护。
|
|
@@ -729,28 +719,11 @@ async function callCliCandidates(request, cwd, language, opts) {
|
|
|
729
719
|
throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
|
|
730
720
|
}
|
|
731
721
|
/**
|
|
732
|
-
* Run a lightweight AI request through the
|
|
733
|
-
*
|
|
734
|
-
* CLI chain. CLI mode walks that chain in order.
|
|
722
|
+
* Run a lightweight AI request through the system employee's CLI chain.
|
|
723
|
+
* 所有 Wand 自有文本调用都走本机已安装的 CLI,没有任何直连 API 分支。
|
|
735
724
|
*/
|
|
736
725
|
export async function callConfiguredAiText(request, cwd, language, opts) {
|
|
737
|
-
|
|
738
|
-
if (opts.systemAi?.enabled) {
|
|
739
|
-
try {
|
|
740
|
-
return await callDirectApiText(effective, opts.systemAi);
|
|
741
|
-
}
|
|
742
|
-
catch (apiError) {
|
|
743
|
-
try {
|
|
744
|
-
// The user selected the direct API first. If it is unavailable or
|
|
745
|
-
// empty, retry this exact request through their selected CLI chain.
|
|
746
|
-
return await callCliCandidates(effective, cwd, language, opts);
|
|
747
|
-
}
|
|
748
|
-
catch (cliError) {
|
|
749
|
-
throw aiFallbackFailed("直连 API", apiError, cliError);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
return callCliCandidates(effective, cwd, language, opts);
|
|
726
|
+
return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts);
|
|
754
727
|
}
|
|
755
728
|
/** Read the unstaged + staged tree without touching the index. */
|
|
756
729
|
async function collectWorkingTreeDiff(cwd) {
|
|
@@ -1037,13 +1010,7 @@ export async function runTagHead(opts) {
|
|
|
1037
1010
|
catch {
|
|
1038
1011
|
headSubject = "";
|
|
1039
1012
|
}
|
|
1040
|
-
tagName = await generateTagAfterCommit(cwd, language, headSubject || "",
|
|
1041
|
-
provider: opts.provider,
|
|
1042
|
-
model: opts.model,
|
|
1043
|
-
thinkingEffort: opts.thinkingEffort,
|
|
1044
|
-
inheritEnv: opts.inheritEnv,
|
|
1045
|
-
systemAi: opts.systemAi,
|
|
1046
|
-
});
|
|
1013
|
+
tagName = await generateTagAfterCommit(cwd, language, headSubject || "", opts);
|
|
1047
1014
|
}
|
|
1048
1015
|
if (!tagName) {
|
|
1049
1016
|
throw new QuickCommitError("请填写 tag 名称,或开启 AI 生成。", "EMPTY_TAG");
|
|
@@ -1507,13 +1474,9 @@ export async function runQuickCommitWithFallback(opts) {
|
|
|
1507
1474
|
}
|
|
1508
1475
|
export async function runQuickCommit(opts) {
|
|
1509
1476
|
const { cwd, language, autoMessage, customMessage, tag, autoTag, push, submodule } = opts;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
thinkingEffort: opts.thinkingEffort,
|
|
1514
|
-
inheritEnv: opts.inheritEnv,
|
|
1515
|
-
systemAi: opts.systemAi,
|
|
1516
|
-
};
|
|
1477
|
+
// QuickCommitOptions 本身带全套 AI 字段(模型、系统运维人设、候选链):
|
|
1478
|
+
// 逐字段抄一份会静默丢掉新增字段,直接复用同一个对象。
|
|
1479
|
+
const ai = opts;
|
|
1517
1480
|
await assertGitWorkTreeAsync(cwd);
|
|
1518
1481
|
// 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
|
|
1519
1482
|
// AI 生成 message 时也基于这个 staged diff。
|
package/dist/language-prompt.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SessionProvider } from "./types.js";
|
|
2
|
+
/** 各 provider 状态根目录,测试可整体替换。 */
|
|
3
|
+
export interface NativeTitleHomes {
|
|
4
|
+
codexHome?: string;
|
|
5
|
+
qoderProjectsDirs?: string[];
|
|
6
|
+
openCodeDatabasePath?: string;
|
|
7
|
+
grokSessionsDir?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* provider 自己在运行期写下的会话标题:
|
|
11
|
+
* codex `session_index.jsonl` 的 thread_name、qoder 转录里的 ai-title、
|
|
12
|
+
* OpenCode SQLite 的 session.title、grok `summary.json` 的 generated_title。
|
|
13
|
+
* Claude / Pi / Gemini 没有这个入口,返回空串让调用方回退系统硅基员工。
|
|
14
|
+
*/
|
|
15
|
+
export declare function readNativeSessionTitle(provider: SessionProvider, providerSessionId: string | null | undefined, cwd: string, homes?: NativeTitleHomes): Promise<string>;
|
|
16
|
+
/** 只用于测试与降级:清掉按文件指纹缓存的原生标题。 */
|
|
17
|
+
export declare function clearNativeSessionTitleCache(): void;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { existsSync, openSync, readSync, closeSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { isPlausibleTaskTitle } from "./task-title.js";
|
|
6
|
+
const SESSION_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,199}$/i;
|
|
7
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const READ_CAP_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const GROK_WALK_LIMIT = 4_000;
|
|
10
|
+
/**
|
|
11
|
+
* provider 自己在运行期写下的会话标题:
|
|
12
|
+
* codex `session_index.jsonl` 的 thread_name、qoder 转录里的 ai-title、
|
|
13
|
+
* OpenCode SQLite 的 session.title、grok `summary.json` 的 generated_title。
|
|
14
|
+
* Claude / Pi / Gemini 没有这个入口,返回空串让调用方回退系统硅基员工。
|
|
15
|
+
*/
|
|
16
|
+
export async function readNativeSessionTitle(provider, providerSessionId, cwd, homes = {}) {
|
|
17
|
+
const id = providerSessionId?.trim() ?? "";
|
|
18
|
+
if (!id || !SESSION_ID_PATTERN.test(id))
|
|
19
|
+
return "";
|
|
20
|
+
switch (provider) {
|
|
21
|
+
case "codex":
|
|
22
|
+
return readCodexNativeTitle(id, homes.codexHome);
|
|
23
|
+
case "qoder":
|
|
24
|
+
return readQoderNativeTitle(id, homes.qoderProjectsDirs);
|
|
25
|
+
case "opencode":
|
|
26
|
+
return readOpenCodeNativeTitle(id, homes.openCodeDatabasePath);
|
|
27
|
+
case "grok":
|
|
28
|
+
return readGrokNativeTitle(id, cwd, homes.grokSessionsDir);
|
|
29
|
+
default:
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function acceptNativeTitle(value) {
|
|
34
|
+
if (typeof value !== "string")
|
|
35
|
+
return "";
|
|
36
|
+
const title = value.replace(/\s+/g, " ").trim();
|
|
37
|
+
return title && isPlausibleTaskTitle(title) ? title : "";
|
|
38
|
+
}
|
|
39
|
+
function readTail(filePath, maxBytes) {
|
|
40
|
+
let fd = null;
|
|
41
|
+
try {
|
|
42
|
+
const stats = statSync(filePath);
|
|
43
|
+
const length = Math.min(maxBytes, Math.max(0, stats.size));
|
|
44
|
+
const buffer = Buffer.alloc(length);
|
|
45
|
+
fd = openSync(filePath, "r");
|
|
46
|
+
const readAt = stats.size - length;
|
|
47
|
+
const bytesRead = readSync(fd, buffer, 0, length, readAt);
|
|
48
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (fd !== null) {
|
|
55
|
+
try {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
catch { /* 只读兜底 */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readHead(filePath, maxBytes) {
|
|
63
|
+
let fd = null;
|
|
64
|
+
try {
|
|
65
|
+
const stats = statSync(filePath);
|
|
66
|
+
fd = openSync(filePath, "r");
|
|
67
|
+
const buffer = Buffer.alloc(Math.min(maxBytes, Math.max(1, stats.size)));
|
|
68
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
69
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (fd !== null) {
|
|
76
|
+
try {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
catch { /* 只读兜底 */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const nativeTitleCache = new Map();
|
|
84
|
+
function fingerprint(filePath) {
|
|
85
|
+
try {
|
|
86
|
+
const stats = statSync(filePath);
|
|
87
|
+
return `${stats.mtimeMs}:${stats.size}`;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "missing";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** 同一会话在同一次生成里会被反复问到(标题、快捷提交、resume),按文件指纹缓存。 */
|
|
94
|
+
function cachedTitle(cacheKey, sourcePath, read) {
|
|
95
|
+
const key = `${cacheKey}|${sourcePath}`;
|
|
96
|
+
const stamp = fingerprint(sourcePath);
|
|
97
|
+
const hit = nativeTitleCache.get(key);
|
|
98
|
+
if (hit && hit.key === stamp)
|
|
99
|
+
return hit.title;
|
|
100
|
+
const title = read();
|
|
101
|
+
if (nativeTitleCache.size > 512)
|
|
102
|
+
nativeTitleCache.clear();
|
|
103
|
+
nativeTitleCache.set(key, { key: stamp, title });
|
|
104
|
+
return title;
|
|
105
|
+
}
|
|
106
|
+
function readCodexNativeTitle(threadId, codexHome) {
|
|
107
|
+
if (!UUID_PATTERN.test(threadId))
|
|
108
|
+
return "";
|
|
109
|
+
const index = path.join(codexHome ?? path.join(os.homedir(), ".codex"), "session_index.jsonl");
|
|
110
|
+
if (!existsSync(index))
|
|
111
|
+
return "";
|
|
112
|
+
return cachedTitle(`codex:${threadId}`, index, () => {
|
|
113
|
+
let title = "";
|
|
114
|
+
for (const line of readTail(index, READ_CAP_BYTES).split("\n")) {
|
|
115
|
+
if (!line.includes(threadId))
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(line);
|
|
119
|
+
if (parsed.id === threadId)
|
|
120
|
+
title = acceptNativeTitle(parsed.thread_name);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return title;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function readQoderNativeTitle(sessionId, projectsDirs) {
|
|
130
|
+
const dirs = projectsDirs ?? [
|
|
131
|
+
path.join(os.homedir(), ".qoder", "projects"),
|
|
132
|
+
path.join(os.homedir(), ".qoder-cn", "projects"),
|
|
133
|
+
];
|
|
134
|
+
for (const projectsDir of dirs) {
|
|
135
|
+
let projects;
|
|
136
|
+
try {
|
|
137
|
+
projects = readdirSync(projectsDir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const project of projects) {
|
|
143
|
+
const transcript = path.join(projectsDir, project, `${sessionId}.jsonl`);
|
|
144
|
+
if (!existsSync(transcript))
|
|
145
|
+
continue;
|
|
146
|
+
const title = cachedTitle(`qoder:${sessionId}`, transcript, () => {
|
|
147
|
+
let found = "";
|
|
148
|
+
for (const line of readHead(transcript, READ_CAP_BYTES).split("\n")) {
|
|
149
|
+
if (!line.includes('"ai-title"'))
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(line);
|
|
153
|
+
if (parsed.type === "ai-title")
|
|
154
|
+
found = acceptNativeTitle(parsed.aiTitle);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
});
|
|
162
|
+
if (title)
|
|
163
|
+
return title;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
/** OpenCode 未命名会话的占位标题不能当原生标题用。 */
|
|
169
|
+
const OPENCODE_PLACEHOLDER_TITLE = /^(?:new session|untitled session)\b/i;
|
|
170
|
+
function readOpenCodeNativeTitle(sessionId, databasePath) {
|
|
171
|
+
const dbPath = databasePath ?? path.join(process.env.XDG_DATA_HOME?.trim() || path.join(os.homedir(), ".local", "share"), "opencode", "opencode.db");
|
|
172
|
+
if (!existsSync(dbPath))
|
|
173
|
+
return "";
|
|
174
|
+
return cachedTitle(`opencode:${sessionId}`, dbPath, () => {
|
|
175
|
+
let database = null;
|
|
176
|
+
try {
|
|
177
|
+
database = new DatabaseSync(dbPath, { readOnly: true });
|
|
178
|
+
const row = database.prepare("SELECT title FROM session WHERE id = ?").get(sessionId);
|
|
179
|
+
const title = acceptNativeTitle(row?.title);
|
|
180
|
+
return title && !OPENCODE_PLACEHOLDER_TITLE.test(title) ? title : "";
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
try {
|
|
187
|
+
database?.close();
|
|
188
|
+
}
|
|
189
|
+
catch { /* 只读探测 */ }
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function grokSessionDirs(grokSessionsDir, cwd) {
|
|
194
|
+
const encoded = encodeURIComponent(cwd);
|
|
195
|
+
const primary = path.join(grokSessionsDir, encoded);
|
|
196
|
+
if (existsSync(primary))
|
|
197
|
+
return [primary];
|
|
198
|
+
const visited = [];
|
|
199
|
+
let queue = [{ dir: grokSessionsDir, depth: 0 }];
|
|
200
|
+
while (queue.length > 0 && visited.length < GROK_WALK_LIMIT) {
|
|
201
|
+
const next = [];
|
|
202
|
+
for (const item of queue) {
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = readdirSync(item.dir);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
const full = path.join(item.dir, entry);
|
|
212
|
+
let stats;
|
|
213
|
+
try {
|
|
214
|
+
stats = statSync(full);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!stats.isDirectory())
|
|
220
|
+
continue;
|
|
221
|
+
visited.push(full);
|
|
222
|
+
if (item.depth < 2)
|
|
223
|
+
next.push({ dir: full, depth: item.depth + 1 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
queue = next;
|
|
227
|
+
}
|
|
228
|
+
return visited;
|
|
229
|
+
}
|
|
230
|
+
function readGrokNativeTitle(sessionId, cwd, grokSessionsDir) {
|
|
231
|
+
if (!UUID_PATTERN.test(sessionId))
|
|
232
|
+
return "";
|
|
233
|
+
const root = grokSessionsDir ?? path.join(os.homedir(), ".grok", "sessions");
|
|
234
|
+
if (!existsSync(root))
|
|
235
|
+
return "";
|
|
236
|
+
for (const dir of grokSessionDirs(root, cwd)) {
|
|
237
|
+
const summary = path.join(dir, sessionId, "summary.json");
|
|
238
|
+
if (!existsSync(summary))
|
|
239
|
+
continue;
|
|
240
|
+
const title = cachedTitle(`grok:${sessionId}`, summary, () => {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(readHead(summary, 65_536));
|
|
243
|
+
return acceptNativeTitle(parsed.generated_title);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
if (title)
|
|
250
|
+
return title;
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
/** 只用于测试与降级:清掉按文件指纹缓存的原生标题。 */
|
|
255
|
+
export function clearNativeSessionTitleCache() {
|
|
256
|
+
nativeTitleCache.clear();
|
|
257
|
+
}
|
|
@@ -37,6 +37,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
37
37
|
/** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
|
|
38
38
|
private orphanRecoveredCount;
|
|
39
39
|
private readonly topicCoordinator;
|
|
40
|
+
private readonly nativeTitles;
|
|
40
41
|
private disposed;
|
|
41
42
|
private readonly terminalHost;
|
|
42
43
|
constructor(config: WandConfig, storage: WandStorage, configDir?: string, terminalHost?: TerminalHost);
|
|
@@ -145,6 +146,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
145
146
|
*/
|
|
146
147
|
setWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
|
|
147
148
|
private maybeGenerateSessionTopic;
|
|
149
|
+
/** 原生标题优先于模型标题:只受「不能是任务名/目录名」约束,命中且变化时才落库。 */
|
|
150
|
+
private applyNativeSessionTitle;
|
|
148
151
|
private defaultAutonomyPolicy;
|
|
149
152
|
resolveEscalation(id: string, requestId: string, resolution?: PermissionResolution): SessionSnapshot;
|
|
150
153
|
approvePermission(id: string): SessionSnapshot;
|
package/dist/process-manager.js
CHANGED
|
@@ -14,11 +14,12 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
|
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
16
16
|
import { commandWithSystemPrompt, composeSystemFallback, normalizeThinkingEffort, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
|
|
17
|
-
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
17
|
+
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
18
|
+
import { readNativeSessionTitle } from "./native-session-title.js";
|
|
18
19
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
20
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
20
21
|
import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
|
|
21
|
-
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
22
|
+
import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
|
|
22
23
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
23
24
|
import { inferProviderFromCommand } from "./session-provider.js";
|
|
24
25
|
import { PtyTerminalState } from "./pty-terminal-state.js";
|
|
@@ -547,6 +548,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
547
548
|
/** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
|
|
548
549
|
orphanRecoveredCount = 0;
|
|
549
550
|
topicCoordinator = new SessionTopicCoordinator();
|
|
551
|
+
nativeTitles = new SessionNativeTitleTracker();
|
|
550
552
|
disposed = false;
|
|
551
553
|
terminalHost;
|
|
552
554
|
constructor(config, storage, configDir, terminalHost) {
|
|
@@ -930,6 +932,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
930
932
|
}
|
|
931
933
|
}
|
|
932
934
|
this.topicCoordinator.clear();
|
|
935
|
+
this.nativeTitles.clear();
|
|
933
936
|
this.removeAllListeners("process");
|
|
934
937
|
this.logger.dispose();
|
|
935
938
|
this.terminalHost.disconnect();
|
|
@@ -2015,8 +2018,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
2015
2018
|
// 迭代提示词记录:和会话标题共用同一套「有没有信息量」判断,用户不用多做一步。
|
|
2016
2019
|
recordIterationPrompt(this.storage, record, prompt, "session");
|
|
2017
2020
|
const blockedTitles = sessionTopicBlocklistForSnapshot(record, this.storage);
|
|
2021
|
+
// CLI 自己起了名字就归它,模型标题与输入首行都不再覆盖;取不到原生标题才走系统硅基员工。
|
|
2022
|
+
const hasNativeTitle = this.nativeTitles.has(id);
|
|
2018
2023
|
const provisional = provisionalSessionTopic(prompt, blockedTitles);
|
|
2019
2024
|
if (provisional
|
|
2025
|
+
&& !hasNativeTitle
|
|
2020
2026
|
&& shouldGenerateSessionTopicFromInput(prompt)
|
|
2021
2027
|
&& (record.title !== provisional.title || record.description !== provisional.description)) {
|
|
2022
2028
|
this.setSessionTopic(id, provisional.title, provisional.description);
|
|
@@ -2027,12 +2033,22 @@ export class ProcessManager extends EventEmitter {
|
|
|
2027
2033
|
cwd: record.cwd,
|
|
2028
2034
|
language: this.config.language,
|
|
2029
2035
|
ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
|
|
2036
|
+
readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
|
|
2037
|
+
hasNativeTitle,
|
|
2030
2038
|
onGenerating: (generating) => {
|
|
2031
2039
|
if (!this.disposed)
|
|
2032
2040
|
this.setSessionTopicGenerating(id, generating);
|
|
2033
2041
|
},
|
|
2034
|
-
onTopic: ({ title, description }) => {
|
|
2035
|
-
if (
|
|
2042
|
+
onTopic: ({ title, description, source }) => {
|
|
2043
|
+
if (this.disposed || !this.sessions.has(id))
|
|
2044
|
+
return;
|
|
2045
|
+
if (source === "native") {
|
|
2046
|
+
this.applyNativeSessionTitle(id, title, blockedTitles);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (this.nativeTitles.has(id))
|
|
2050
|
+
return;
|
|
2051
|
+
if (shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
|
|
2036
2052
|
this.setSessionTopic(id, title, description);
|
|
2037
2053
|
}
|
|
2038
2054
|
},
|
|
@@ -2041,6 +2057,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
2041
2057
|
},
|
|
2042
2058
|
});
|
|
2043
2059
|
}
|
|
2060
|
+
/** 原生标题优先于模型标题:只受「不能是任务名/目录名」约束,命中且变化时才落库。 */
|
|
2061
|
+
applyNativeSessionTitle(id, title, blockedTitles) {
|
|
2062
|
+
this.nativeTitles.record(id, title);
|
|
2063
|
+
const current = this.sessions.get(id);
|
|
2064
|
+
if (!current || !shouldAcceptGeneratedSessionTitle(title, blockedTitles))
|
|
2065
|
+
return;
|
|
2066
|
+
if (current.title === title && current.description === title)
|
|
2067
|
+
return;
|
|
2068
|
+
this.setSessionTopic(id, title, title);
|
|
2069
|
+
}
|
|
2044
2070
|
defaultAutonomyPolicy(mode) {
|
|
2045
2071
|
if (mode === "agent" || mode === "agent-max" || mode === "managed" || mode === "native" || mode === "full-access") {
|
|
2046
2072
|
return "agent";
|
|
@@ -2288,8 +2314,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
2288
2314
|
case "output.chat": {
|
|
2289
2315
|
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
2290
2316
|
const rawMessages = record.ptyBridge?.getMessages() ?? [];
|
|
2291
|
-
const isStreaming = record.status === "running";
|
|
2292
2317
|
const bridgeData = event.data;
|
|
2318
|
+
const sequenceChanged = record.ptyChatMessageCount !== rawMessages.length;
|
|
2319
|
+
const isStreaming = record.status === "running" && bridgeData?.isResponding === true;
|
|
2320
|
+
record.ptyChatMessageCount = rawMessages.length;
|
|
2293
2321
|
// 每轮忙碌信号:turn 开始置 true,回复结束清 false。只在翻转时广播,
|
|
2294
2322
|
// 避免流式期间每个 chunk 都发 status 事件。
|
|
2295
2323
|
const responding = bridgeData?.isResponding === true;
|
|
@@ -2310,7 +2338,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
2310
2338
|
if (bridgeData && typeof bridgeData.isResponding === "boolean") {
|
|
2311
2339
|
data.isResponding = bridgeData.isResponding;
|
|
2312
2340
|
}
|
|
2313
|
-
|
|
2341
|
+
// A new round can append user + assistant at once. Last-only payloads
|
|
2342
|
+
// cannot establish that sequence for any of the existing clients.
|
|
2343
|
+
// Idle is a turn boundary even when the shell process remains running.
|
|
2344
|
+
if (isStreaming && !sequenceChanged && rawMessages.length > 0) {
|
|
2314
2345
|
data.incremental = true;
|
|
2315
2346
|
const lastTurn = rawMessages[rawMessages.length - 1];
|
|
2316
2347
|
const truncatedLast = truncateMessagesForTransport([lastTurn], this.config.cardDefaults ?? {}, 0);
|
|
@@ -5,7 +5,7 @@ import { alignedBlockStart, blockWindowMessagesForTransport, sliceTurnBlocksForT
|
|
|
5
5
|
import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
|
|
6
6
|
import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
|
|
7
7
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
9
9
|
import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
10
10
|
import { getErrorMessage } from "./error-utils.js";
|
|
11
11
|
import { archiveCommitTasks } from "./commit-task-archive.js";
|
|
@@ -111,24 +111,6 @@ function getInputErrorResponse(error, sessionId) {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
function parseClaudeSdkSkills(value) {
|
|
115
|
-
if (!Array.isArray(value)) {
|
|
116
|
-
throw new Error("skills 必须是数组。");
|
|
117
|
-
}
|
|
118
|
-
if (value.length > 50) {
|
|
119
|
-
throw new Error("最多选择 50 个 skills。");
|
|
120
|
-
}
|
|
121
|
-
const names = new Set();
|
|
122
|
-
for (const item of value) {
|
|
123
|
-
if (typeof item !== "string")
|
|
124
|
-
throw new Error("skills 只能包含字符串。");
|
|
125
|
-
const name = item.trim();
|
|
126
|
-
if (!name || name.length > 128)
|
|
127
|
-
throw new Error("skill 名称无效。");
|
|
128
|
-
names.add(name);
|
|
129
|
-
}
|
|
130
|
-
return Array.from(names);
|
|
131
|
-
}
|
|
132
114
|
function getInputDebugMeta(error) {
|
|
133
115
|
if (error instanceof Error) {
|
|
134
116
|
return { name: error.name, message: error.message, stack: error.stack };
|
|
@@ -630,17 +612,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
630
612
|
res.status(404).json({ error: "未找到该结构化会话。" });
|
|
631
613
|
return;
|
|
632
614
|
}
|
|
633
|
-
const hasSkills = Object.prototype.hasOwnProperty.call(req.body ?? {}, "skills");
|
|
634
|
-
if (hasSkills && (session.provider !== "claude" || session.runner !== "claude-sdk")) {
|
|
635
|
-
res.status(400).json({ error: "skills 仅支持 Claude SDK 结构化会话。" });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const skills = hasSkills ? parseClaudeSdkSkills(req.body.skills) : [];
|
|
639
615
|
const snapshot = await structured.sendMessage(req.params.id, input, {
|
|
640
616
|
interrupt,
|
|
641
617
|
preserveQueue,
|
|
642
618
|
idempotencyKey,
|
|
643
|
-
skills,
|
|
644
619
|
});
|
|
645
620
|
res.json(sessionResponseDTO(snapshot));
|
|
646
621
|
}
|
|
@@ -887,7 +862,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
887
862
|
}
|
|
888
863
|
const body = (req.body ?? {});
|
|
889
864
|
try {
|
|
890
|
-
const ai =
|
|
865
|
+
const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
|
|
891
866
|
// 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
|
|
892
867
|
const context = await iterationCommitInput(storage, snapshot, body);
|
|
893
868
|
const result = await runQuickCommitWithFallback({
|
|
@@ -983,7 +958,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
983
958
|
}
|
|
984
959
|
const body = (req.body ?? {});
|
|
985
960
|
try {
|
|
986
|
-
const ai =
|
|
961
|
+
const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
|
|
987
962
|
const context = await iterationCommitInput(storage, snapshot, body);
|
|
988
963
|
const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
|
|
989
964
|
...ai,
|
|
@@ -1020,7 +995,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1020
995
|
}
|
|
1021
996
|
const body = (req.body ?? {});
|
|
1022
997
|
try {
|
|
1023
|
-
const ai =
|
|
998
|
+
const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
|
|
1024
999
|
const result = await runTagHead({
|
|
1025
1000
|
cwd: snapshot.cwd,
|
|
1026
1001
|
language: config.language ?? "",
|