@steerable/agent-shell 0.6.20 → 0.6.22
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 +34 -0
- package/dist/auth/index.d.ts +38 -0
- package/dist/auth/index.js +20 -0
- package/dist/host/pack-assembly.d.ts +7 -7
- package/dist/host/runtime.d.ts +6 -2
- package/dist/host/runtime.js +54 -15
- package/dist/host/visible-terminal-exec.d.ts +0 -2
- package/dist/host/visible-terminal-exec.js +4 -8
- package/dist/insights/flush.d.ts +5 -4
- package/dist/insights/flush.js +14 -15
- package/dist/insights/record.d.ts +6 -5
- package/dist/insights/record.js +9 -10
- package/dist/llm/index.d.ts +6 -2
- package/dist/llm/index.js +19 -9
- package/dist/local-backend/ai-suggestions.d.ts +32 -0
- package/dist/local-backend/ai-suggestions.js +160 -0
- package/dist/local-backend/coreloop-stream.js +1 -1
- package/dist/local-backend/mentioned-paths.d.ts +28 -0
- package/dist/local-backend/mentioned-paths.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +10 -4
- package/dist/local-backend/prompt-builder.js +13 -12
- package/dist/local-backend/router.d.ts +26 -4
- package/dist/local-backend/router.js +243 -96
- package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
- package/dist/local-backend/task-service.d.ts +14 -13
- package/dist/local-backend/task-service.js +32 -29
- package/dist/local-backend/turn-files.d.ts +57 -0
- package/dist/local-backend/turn-files.js +285 -0
- package/dist/local-backend/worktree-service.d.ts +1 -1
- package/dist/local-backend/worktree-service.js +6 -6
- package/dist/main.js +45 -26
- package/dist/preload.cjs +30 -10
- package/dist/preload.js +33 -10
- package/dist/server/http-server.d.ts +13 -1
- package/dist/server/http-server.js +45 -8
- package/dist/server/index.js +4 -90
- package/dist/server/sse-bus.d.ts +1 -1
- package/dist/server/start.d.ts +26 -0
- package/dist/server/start.js +111 -0
- package/dist/sidecar/boot.d.ts +3 -1
- package/dist/sidecar/boot.js +6 -7
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -1
- package/dist/sidecar/reverse-ask-user.d.ts +2 -0
- package/dist/sidecar/reverse-ask-user.js +2 -1
- package/dist/sidecar/reverse-spawn.js +12 -4
- package/dist/sidecar/reverse-tools.d.ts +1 -1
- package/dist/sidecar/reverse-tools.js +1 -1
- package/dist/sidecar/storage-path.js +2 -3
- package/dist/storage/driver.d.ts +57 -0
- package/dist/storage/driver.js +104 -0
- package/dist/storage/index.d.ts +80 -67
- package/dist/storage/index.js +528 -296
- package/dist/storage/pack-migrations.d.ts +2 -4
- package/dist/storage/pack-migrations.js +2 -4
- package/dist/storage/scoped-store.d.ts +125 -0
- package/dist/storage/scoped-store.js +1 -0
- package/dist/storage/sqlite-driver.d.ts +16 -0
- package/dist/storage/sqlite-driver.js +135 -0
- package/dist/storage/write-lease-error.d.ts +11 -0
- package/dist/storage/write-lease-error.js +18 -0
- package/package.json +14 -5
- package/dist/storage/local-store-singleton.d.ts +0 -28
- package/dist/storage/local-store-singleton.js +0 -38
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回合结束后生成 3 条「下一轮用户输入」建议(WorkBuddy 式快捷追问)。
|
|
3
|
+
*
|
|
4
|
+
* 主路径走一次短 LLM 调用,根据本轮用户请求 + 助手回复写成用户口吻的短句。
|
|
5
|
+
* 失败时用启发式兜底(PPT / 计划 / 代码 / 通用),永不抛错。
|
|
6
|
+
*
|
|
7
|
+
* 调用方应该 fire-and-forget:先把兜底三条推上 UI,LLM 成功后再替换。
|
|
8
|
+
* 不要挂在 SSE 主流程里阻塞 `[DONE]`。
|
|
9
|
+
*/
|
|
10
|
+
export declare const SUGGESTED_REPLY_COUNT = 3;
|
|
11
|
+
/** 单条建议清洗:去编号/引号、压空白、截断。不合格返回空串。 */
|
|
12
|
+
export declare function cleanSuggestedReply(raw: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* 从模型原文抽出最多 3 条建议。接受 JSON 数组、markdown 代码块、或编号/项目列表。
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSuggestedReplies(raw: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* 不调模型的启发式三条。PPT / 计划 / 代码有专用句子,其它走通用。
|
|
19
|
+
* 始终返回正好 3 条(输入全空时仍给通用三条)。
|
|
20
|
+
*/
|
|
21
|
+
export declare function fallbackSuggestedReplies(userText: string, assistantText: string): string[];
|
|
22
|
+
export interface GenerateSuggestedRepliesResult {
|
|
23
|
+
suggestions: string[];
|
|
24
|
+
/** true = LLM 失败或输出不合格,suggestions 来自启发式兜底。 */
|
|
25
|
+
usedFallback: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 生成本轮 3 条追问建议。永不抛错,始终返回 3 条。
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateSuggestedReplies(userText: string, assistantText: string, opts?: {
|
|
31
|
+
perAttemptTimeoutMs?: number;
|
|
32
|
+
}): Promise<GenerateSuggestedRepliesResult>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回合结束后生成 3 条「下一轮用户输入」建议(WorkBuddy 式快捷追问)。
|
|
3
|
+
*
|
|
4
|
+
* 主路径走一次短 LLM 调用,根据本轮用户请求 + 助手回复写成用户口吻的短句。
|
|
5
|
+
* 失败时用启发式兜底(PPT / 计划 / 代码 / 通用),永不抛错。
|
|
6
|
+
*
|
|
7
|
+
* 调用方应该 fire-and-forget:先把兜底三条推上 UI,LLM 成功后再替换。
|
|
8
|
+
* 不要挂在 SSE 主流程里阻塞 `[DONE]`。
|
|
9
|
+
*/
|
|
10
|
+
import { llmService } from '../llm/index.js';
|
|
11
|
+
export const SUGGESTED_REPLY_COUNT = 3;
|
|
12
|
+
const MAX_SUGGESTION_CHARS = 36;
|
|
13
|
+
const USER_TEXT_LIMIT = 500;
|
|
14
|
+
const ASSISTANT_TEXT_LIMIT = 1600;
|
|
15
|
+
const SYSTEM_PROMPT = `你是对话追问建议助手。根据用户上一轮请求和助手刚刚完成的回复,生成 3 条用户可以点一下就发出去的下一轮输入。
|
|
16
|
+
|
|
17
|
+
要求:
|
|
18
|
+
1. 正好 3 条,彼此不重复
|
|
19
|
+
2. 每条 8-28 个字,用用户口吻,像用户会打的话
|
|
20
|
+
3. 紧扣刚刚完成的工作:改内容、改样式、继续深入、换方向;不要空泛的「再详细说说」
|
|
21
|
+
4. 如果助手已经邀请后续操作(例如「如需修改内容或调整样式」),把那些邀请写成具体可执行的短句
|
|
22
|
+
5. 不要编号、不要引号、不要解释
|
|
23
|
+
6. 只输出 JSON 字符串数组,例如 ["调整封面配色","把个人简介写得更具体","再加一页项目案例"]`;
|
|
24
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
25
|
+
if (timeoutMs <= 0)
|
|
26
|
+
return promise;
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
promise.then((value) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (err) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(err);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function clip(text, limit) {
|
|
41
|
+
const chars = Array.from((text ?? '').trim());
|
|
42
|
+
if (chars.length <= limit)
|
|
43
|
+
return chars.join('');
|
|
44
|
+
return `${chars.slice(0, limit).join('')}…`;
|
|
45
|
+
}
|
|
46
|
+
/** 单条建议清洗:去编号/引号、压空白、截断。不合格返回空串。 */
|
|
47
|
+
export function cleanSuggestedReply(raw) {
|
|
48
|
+
let text = (raw ?? '').trim();
|
|
49
|
+
text = text.replace(/^[\s"'“”‘’「」『』《》【】]+|[\s"'“”‘’「」『』《》【】。.!!??,,;;::]+$/g, '');
|
|
50
|
+
text = text.replace(/^(?:\d+[\.\)、]|[-*•])\s*/, '');
|
|
51
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
52
|
+
if (text.length < 2)
|
|
53
|
+
return '';
|
|
54
|
+
const chars = Array.from(text);
|
|
55
|
+
if (chars.length > MAX_SUGGESTION_CHARS) {
|
|
56
|
+
text = chars.slice(0, MAX_SUGGESTION_CHARS).join('').trim();
|
|
57
|
+
}
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
function uniqueThree(candidates, extras = []) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const raw of [...candidates, ...extras]) {
|
|
64
|
+
const cleaned = cleanSuggestedReply(raw);
|
|
65
|
+
if (!cleaned || seen.has(cleaned))
|
|
66
|
+
continue;
|
|
67
|
+
seen.add(cleaned);
|
|
68
|
+
out.push(cleaned);
|
|
69
|
+
if (out.length === SUGGESTED_REPLY_COUNT)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 从模型原文抽出最多 3 条建议。接受 JSON 数组、markdown 代码块、或编号/项目列表。
|
|
76
|
+
*/
|
|
77
|
+
export function parseSuggestedReplies(raw) {
|
|
78
|
+
const trimmed = (raw ?? '').trim();
|
|
79
|
+
if (!trimmed)
|
|
80
|
+
return [];
|
|
81
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
82
|
+
const candidate = (fenced?.[1] ?? trimmed).trim();
|
|
83
|
+
const start = candidate.indexOf('[');
|
|
84
|
+
const end = candidate.lastIndexOf(']');
|
|
85
|
+
if (start >= 0 && end > start) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(candidate.slice(start, end + 1));
|
|
88
|
+
if (Array.isArray(parsed)) {
|
|
89
|
+
return uniqueThree(parsed.filter((item) => typeof item === 'string'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// 落到分行解析。
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const lines = candidate
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.map((line) => line.trim())
|
|
99
|
+
.filter((line) => line.length > 0 && !/^```/.test(line));
|
|
100
|
+
const listLines = lines.filter((line) => /^(?:\d+[\.\)、]|[-*•])\s+/.test(line));
|
|
101
|
+
return uniqueThree(listLines.length > 0 ? listLines : lines);
|
|
102
|
+
}
|
|
103
|
+
const PPT_FALLBACK = ['调整封面标题和配色', '把某一页内容写得更具体', '再加一页项目案例'];
|
|
104
|
+
const PLAN_FALLBACK = ['按这个计划开始执行', '先改第 2 步再执行', '把计划写得更细一点'];
|
|
105
|
+
const CODE_FALLBACK = ['解释这段实现的思路', '帮我补上测试', '再优化一下可读性'];
|
|
106
|
+
const GENERIC_FALLBACK = ['继续完善这份结果', '换一种呈现方式', '告诉我下一步怎么做'];
|
|
107
|
+
const PPT_OFFER_FALLBACK = ['调整幻灯片的内容和文案', '调整配色和版式', '再加一页补充材料'];
|
|
108
|
+
/**
|
|
109
|
+
* 不调模型的启发式三条。PPT / 计划 / 代码有专用句子,其它走通用。
|
|
110
|
+
* 始终返回正好 3 条(输入全空时仍给通用三条)。
|
|
111
|
+
*/
|
|
112
|
+
export function fallbackSuggestedReplies(userText, assistantText) {
|
|
113
|
+
const blob = `${userText}\n${assistantText}`;
|
|
114
|
+
if (/\.pptx\b|幻灯片|演示文稿|\bppt\b/i.test(blob)) { // shell-neutral:allow — Office 扩展名 'ppt'/'.pptx',不是产品品牌
|
|
115
|
+
if (/修改内容|调整样式/.test(assistantText)) {
|
|
116
|
+
return uniqueThree(PPT_OFFER_FALLBACK, PPT_FALLBACK);
|
|
117
|
+
}
|
|
118
|
+
return uniqueThree(PPT_FALLBACK, GENERIC_FALLBACK);
|
|
119
|
+
}
|
|
120
|
+
if (/标准工作流程|待办清单|\bTODO\b|先制定计划/.test(blob) || /^\s*计划已/.test(assistantText)) {
|
|
121
|
+
return uniqueThree(PLAN_FALLBACK, GENERIC_FALLBACK);
|
|
122
|
+
}
|
|
123
|
+
if (/```|单元测试|函数实现|补测试/.test(blob)) {
|
|
124
|
+
return uniqueThree(CODE_FALLBACK, GENERIC_FALLBACK);
|
|
125
|
+
}
|
|
126
|
+
return uniqueThree(GENERIC_FALLBACK);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 生成本轮 3 条追问建议。永不抛错,始终返回 3 条。
|
|
130
|
+
*/
|
|
131
|
+
export async function generateSuggestedReplies(userText, assistantText, opts = {}) {
|
|
132
|
+
const fallback = fallbackSuggestedReplies(userText, assistantText);
|
|
133
|
+
const user = clip(userText, USER_TEXT_LIMIT);
|
|
134
|
+
const assistant = clip(assistantText, ASSISTANT_TEXT_LIMIT);
|
|
135
|
+
if (!user && !assistant) {
|
|
136
|
+
return { suggestions: fallback, usedFallback: true };
|
|
137
|
+
}
|
|
138
|
+
const perAttemptTimeoutMs = opts.perAttemptTimeoutMs ?? 12_000;
|
|
139
|
+
try {
|
|
140
|
+
const result = await withTimeout(llmService.generate({
|
|
141
|
+
messages: [
|
|
142
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
143
|
+
{
|
|
144
|
+
role: 'user',
|
|
145
|
+
content: `用户请求:\n${user || '(空)'}\n\n助手回复:\n${assistant || '(空)'}`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
temperature: 0.6,
|
|
149
|
+
}), perAttemptTimeoutMs, 'ai-suggestions');
|
|
150
|
+
const parsed = parseSuggestedReplies(result.content ?? '');
|
|
151
|
+
const suggestions = uniqueThree(parsed, fallback);
|
|
152
|
+
if (suggestions.length === SUGGESTED_REPLY_COUNT && parsed.length > 0) {
|
|
153
|
+
return { suggestions, usedFallback: false };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.warn('[ai-suggestions] LLM 追问建议失败,用启发式兜底', err);
|
|
158
|
+
}
|
|
159
|
+
return { suggestions: fallback, usedFallback: true };
|
|
160
|
+
}
|
|
@@ -99,7 +99,7 @@ export async function streamCoreLoopTurn(options) {
|
|
|
99
99
|
streamRawChunks: options.streamRawChunks,
|
|
100
100
|
// W8: advertise the sidecar-hosted ask_user tool; the desktop answers
|
|
101
101
|
// `ask_user.request` reverse calls with the renderer question card
|
|
102
|
-
// (
|
|
102
|
+
// (AskUserPromptProvider). Under toolsViaHost the sidecar intercepts
|
|
103
103
|
// ask_user locally, so it never reaches the host's tool.invoke.
|
|
104
104
|
// 4.6a: background task streams opt out (options.askUser === false).
|
|
105
105
|
askUser: options.askUser ?? true,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话正文里提到的文件路径的存在性解析(「点击打开正文里的路径」用)。
|
|
3
|
+
*
|
|
4
|
+
* 渲染层从助手回答的行内代码里挑出「看着像路径」的字面量,批量送到这里
|
|
5
|
+
* 落地成绝对路径并 stat 验证。只有真实存在的条目会回给渲染层——路径是
|
|
6
|
+
* 模型写出来的自然语言片段,猜错在所难免,靠 stat 证伪而不是靠解析可靠。
|
|
7
|
+
*
|
|
8
|
+
* 相对路径的基准目录由调用方给(绑定项目的对话用项目根,否则用 home),
|
|
9
|
+
* 与 exec 工具缺省 cwd 的回落规则一致,这样模型写 `./out.bin` 时指向的
|
|
10
|
+
* 就是它当时真正落盘的位置。
|
|
11
|
+
*/
|
|
12
|
+
export interface ResolvedMentionedPath {
|
|
13
|
+
/** 渲染层送来的原始字面量(用于回填到对应的行内代码节点)。 */
|
|
14
|
+
candidate: string;
|
|
15
|
+
/** 落地后的绝对路径(点击打开直接用)。 */
|
|
16
|
+
path: string;
|
|
17
|
+
/** 目录会用文件管理器打开,渲染层据此换图标。 */
|
|
18
|
+
isDirectory: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 批量解析候选路径,只返回真实存在的条目。`baseDir` 是相对路径的基准
|
|
22
|
+
* 目录;`homeDir` 仅测试注入用,默认 os.homedir()。
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveMentionedPaths(options: {
|
|
25
|
+
candidates: readonly string[];
|
|
26
|
+
baseDir: string;
|
|
27
|
+
homeDir?: string;
|
|
28
|
+
}): Promise<ResolvedMentionedPath[]>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话正文里提到的文件路径的存在性解析(「点击打开正文里的路径」用)。
|
|
3
|
+
*
|
|
4
|
+
* 渲染层从助手回答的行内代码里挑出「看着像路径」的字面量,批量送到这里
|
|
5
|
+
* 落地成绝对路径并 stat 验证。只有真实存在的条目会回给渲染层——路径是
|
|
6
|
+
* 模型写出来的自然语言片段,猜错在所难免,靠 stat 证伪而不是靠解析可靠。
|
|
7
|
+
*
|
|
8
|
+
* 相对路径的基准目录由调用方给(绑定项目的对话用项目根,否则用 home),
|
|
9
|
+
* 与 exec 工具缺省 cwd 的回落规则一致,这样模型写 `./out.bin` 时指向的
|
|
10
|
+
* 就是它当时真正落盘的位置。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
/** 单次请求的候选上限与单条长度上限:渲染层是不可信输入,挡住刷 stat。 */
|
|
16
|
+
const MAX_CANDIDATES = 64;
|
|
17
|
+
const MAX_CANDIDATE_LENGTH = 512;
|
|
18
|
+
/**
|
|
19
|
+
* 批量解析候选路径,只返回真实存在的条目。`baseDir` 是相对路径的基准
|
|
20
|
+
* 目录;`homeDir` 仅测试注入用,默认 os.homedir()。
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveMentionedPaths(options) {
|
|
23
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
24
|
+
const out = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const raw of options.candidates.slice(0, MAX_CANDIDATES)) {
|
|
27
|
+
if (typeof raw !== 'string')
|
|
28
|
+
continue;
|
|
29
|
+
const candidate = raw.trim();
|
|
30
|
+
if (!candidate || candidate.length > MAX_CANDIDATE_LENGTH)
|
|
31
|
+
continue;
|
|
32
|
+
// 换行/NUL 不可能是单个路径字面量,且会污染后续 stat。
|
|
33
|
+
if (/[\n\r\0]/.test(candidate))
|
|
34
|
+
continue;
|
|
35
|
+
if (seen.has(candidate))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(candidate);
|
|
38
|
+
const full = toAbsolute(candidate, options.baseDir, homeDir);
|
|
39
|
+
if (!full)
|
|
40
|
+
continue;
|
|
41
|
+
let stat;
|
|
42
|
+
try {
|
|
43
|
+
stat = await fs.stat(full);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 不存在/无权限:模型写错或文件已被删,静默丢弃。
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
50
|
+
continue;
|
|
51
|
+
out.push({ candidate, path: full, isDirectory: stat.isDirectory() });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** `~` 展开 → 绝对路径原样 → 其余按 baseDir 解析。 */
|
|
56
|
+
function toAbsolute(candidate, baseDir, homeDir) {
|
|
57
|
+
if (candidate === '~')
|
|
58
|
+
return homeDir;
|
|
59
|
+
if (candidate.startsWith('~/') || candidate.startsWith('~\\')) {
|
|
60
|
+
return path.resolve(path.join(homeDir, candidate.slice(2)));
|
|
61
|
+
}
|
|
62
|
+
if (path.isAbsolute(candidate))
|
|
63
|
+
return path.resolve(candidate);
|
|
64
|
+
if (!baseDir)
|
|
65
|
+
return null;
|
|
66
|
+
return path.resolve(baseDir, candidate);
|
|
67
|
+
}
|
|
@@ -48,6 +48,11 @@ export interface BuildPromptOptions {
|
|
|
48
48
|
* 仍然优先——模式级排除(plan 模式的执行类技能)不因勾选而失效。
|
|
49
49
|
*/
|
|
50
50
|
pinnedSkillNames?: Iterable<string>;
|
|
51
|
+
/**
|
|
52
|
+
* 本轮自称。有绑定智能体时用智能体显示名,渲染技能正文 `{agentName}`;
|
|
53
|
+
* 未传则回落产品品牌 `brand.agentName`。
|
|
54
|
+
*/
|
|
55
|
+
identityName?: string | null;
|
|
51
56
|
}
|
|
52
57
|
export interface BuiltPrompt {
|
|
53
58
|
prompt: string;
|
|
@@ -84,11 +89,12 @@ export declare function conditionsFromTools(toolNames: Iterable<string>, autoHas
|
|
|
84
89
|
*/
|
|
85
90
|
export declare function buildSystemPrompt(options?: BuildPromptOptions): Promise<BuiltPrompt>;
|
|
86
91
|
/**
|
|
87
|
-
*
|
|
88
|
-
* `{
|
|
89
|
-
* 中性默认)。eager 系统提示词路径统一注入;包技能同理可用。
|
|
92
|
+
* 品牌/身份占位符:`{brandDisplayName}` / `{brandTagline}` 始终是产品品牌;
|
|
93
|
+
* `{agentName}` 有本轮智能体时用智能体显示名,否则回落 `brand.agentName`。
|
|
90
94
|
*/
|
|
91
|
-
export declare function brandSkillVars(
|
|
95
|
+
export declare function brandSkillVars(options?: {
|
|
96
|
+
identityName?: string | null;
|
|
97
|
+
}): Record<string, string>;
|
|
92
98
|
/** 技能正文渲染:把 `{scripts}` 及额外占位符解析为绝对路径。 */
|
|
93
99
|
export declare function renderSkillContent(m: SkillModule, vars?: Record<string, string>): string;
|
|
94
100
|
/** 用户显式指定技能的优先级指令头,渲染在该技能内容之前。 */
|
|
@@ -14,16 +14,17 @@ import path from 'node:path';
|
|
|
14
14
|
import { loadSkills } from './skill-loader.js';
|
|
15
15
|
import { isSkillPinned } from './agent-capability.js';
|
|
16
16
|
import { getBrand } from '../brand.js';
|
|
17
|
-
|
|
17
|
+
function fallbackPrompt(identityName) {
|
|
18
18
|
const brand = getBrand();
|
|
19
|
+
const name = identityName?.trim() || brand.agentName;
|
|
19
20
|
return [
|
|
20
21
|
'# 角色',
|
|
21
22
|
'',
|
|
22
|
-
`你是 ${
|
|
23
|
+
`你是 ${name},${brand.tagline},运行在用户自己的 Windows / macOS / Linux 机器上。`,
|
|
23
24
|
'通过工具直接操作用户的本地环境(shell、文件、当前产品安装的场景工具等)。用中文回复。',
|
|
24
25
|
'不要凭空编造任何工具返回值。',
|
|
25
26
|
].join('\n');
|
|
26
|
-
}
|
|
27
|
+
}
|
|
27
28
|
const DEFAULT_CHAR_BUDGET = 60_000;
|
|
28
29
|
/**
|
|
29
30
|
* Derive condition strings from the set of tool names available this turn.
|
|
@@ -99,7 +100,7 @@ export async function buildSystemPrompt(options = {}) {
|
|
|
99
100
|
prompt: assembleFinal({
|
|
100
101
|
personaPreamble: options.personaPreamble ?? '',
|
|
101
102
|
chatSystemPrompt: options.chatSystemPrompt ?? '',
|
|
102
|
-
body:
|
|
103
|
+
body: fallbackPrompt(options.identityName),
|
|
103
104
|
realityCheckSuffix: options.realityCheckSuffix ?? '',
|
|
104
105
|
}),
|
|
105
106
|
modules,
|
|
@@ -116,9 +117,10 @@ export async function buildSystemPrompt(options = {}) {
|
|
|
116
117
|
if (options.forcedMcpTool) {
|
|
117
118
|
modules.push(buildForcedMcpToolModule(options.forcedMcpTool));
|
|
118
119
|
}
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// `{agentName}` 优先用本轮智能体显示名,没有绑定智能体时才用产品品牌。
|
|
121
|
+
const body = modules
|
|
122
|
+
.map((m) => renderSkillContent(m, brandSkillVars({ identityName: options.identityName })))
|
|
123
|
+
.join('\n\n');
|
|
122
124
|
return {
|
|
123
125
|
prompt: assembleFinal({
|
|
124
126
|
personaPreamble: options.personaPreamble ?? '',
|
|
@@ -169,14 +171,13 @@ function buildForcedMcpToolModule(t) {
|
|
|
169
171
|
};
|
|
170
172
|
}
|
|
171
173
|
/**
|
|
172
|
-
*
|
|
173
|
-
* `{
|
|
174
|
-
* 中性默认)。eager 系统提示词路径统一注入;包技能同理可用。
|
|
174
|
+
* 品牌/身份占位符:`{brandDisplayName}` / `{brandTagline}` 始终是产品品牌;
|
|
175
|
+
* `{agentName}` 有本轮智能体时用智能体显示名,否则回落 `brand.agentName`。
|
|
175
176
|
*/
|
|
176
|
-
export function brandSkillVars() {
|
|
177
|
+
export function brandSkillVars(options = {}) {
|
|
177
178
|
const brand = getBrand();
|
|
178
179
|
return {
|
|
179
|
-
agentName: brand.agentName,
|
|
180
|
+
agentName: options.identityName?.trim() || brand.agentName,
|
|
180
181
|
brandDisplayName: brand.displayName,
|
|
181
182
|
brandTagline: brand.tagline,
|
|
182
183
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
1
2
|
import { ToolRouter } from '../tool-router.js';
|
|
2
3
|
import type { TaskService } from './task-service.js';
|
|
4
|
+
import { type Principal } from '../auth/index.js';
|
|
3
5
|
export interface LocalBackendRequest {
|
|
4
6
|
method: string;
|
|
5
7
|
path: string;
|
|
6
8
|
body?: unknown;
|
|
9
|
+
/** Authenticated identity supplied by the BS HTTP layer. */
|
|
10
|
+
principal?: Principal;
|
|
7
11
|
}
|
|
8
12
|
export interface LocalBackendResponse<T = unknown> {
|
|
9
13
|
status: number;
|
|
@@ -39,14 +43,23 @@ export interface StreamOptions {
|
|
|
39
43
|
export type LocalBackendBroadcast = (eventName: string, payload: unknown) => void;
|
|
40
44
|
export declare class LocalBackendRouter {
|
|
41
45
|
private readonly toolRouter;
|
|
46
|
+
private readonly defaultStore;
|
|
47
|
+
private readonly storeContext;
|
|
48
|
+
private readonly resolveStore;
|
|
42
49
|
private readonly broadcast;
|
|
43
50
|
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
44
51
|
private readonly taskService;
|
|
45
|
-
constructor(toolRouter: ToolRouter, options
|
|
52
|
+
constructor(toolRouter: ToolRouter, options: {
|
|
53
|
+
store: ScopedStore;
|
|
54
|
+
resolveStore?: (principal?: Principal) => ScopedStore;
|
|
46
55
|
broadcast?: LocalBackendBroadcast;
|
|
47
56
|
taskService?: TaskService;
|
|
48
57
|
});
|
|
58
|
+
private get store();
|
|
59
|
+
/** Store selected for the active request, or the host default outside one. */
|
|
60
|
+
get activeStore(): ScopedStore;
|
|
49
61
|
handle(request: LocalBackendRequest): Promise<LocalBackendResponse>;
|
|
62
|
+
private handleScoped;
|
|
50
63
|
/**
|
|
51
64
|
* 对未显式实现的路由做"温柔降级":GET 返回空集合/空对象,不让 UI 因 404 抛错跳到
|
|
52
65
|
* not-found;其他方法返回 success:true 让前端流程继续。所有兜底都在主进程终端
|
|
@@ -62,10 +75,12 @@ export declare class LocalBackendRouter {
|
|
|
62
75
|
* `accumulatedContent += parsedData.content` 一行追加渲染。
|
|
63
76
|
* - 同步真实 messageId:`data: {"type":"message_id","messageId":"..."}`
|
|
64
77
|
* - 工具执行结果(无需用户确认):`data: {"type":"executed_actions","actions":[...]}`
|
|
78
|
+
* - 回合产物文件列表(仅非空时发,在 message_id 之前):`data: {"type":"turn_files","files":[...]}`
|
|
65
79
|
* - 错误:保留 `event:error` + `data: {"message":"..."}`(前端会忽略,但日志/未来 UI 用)
|
|
66
80
|
* - 结束:标准 SSE `[DONE]` 串。
|
|
67
81
|
*/
|
|
68
82
|
handleStream(request: LocalBackendRequest, emit: StreamEmit, options?: StreamOptions): Promise<StreamResult>;
|
|
83
|
+
private handleStreamScoped;
|
|
69
84
|
/**
|
|
70
85
|
* 后台异步生成 chat 标题。**完全 fire-and-forget**——不阻塞 `[DONE]`,
|
|
71
86
|
* 调用方只要 `void this.runTitleGenInBackground(...)` 就行。
|
|
@@ -88,10 +103,10 @@ export declare class LocalBackendRouter {
|
|
|
88
103
|
* Public: main.ts 的反向通道(reverse-tools.ts)经它给 CoreLoop 路径的
|
|
89
104
|
* 工具调用补上 projectRoot 围栏。
|
|
90
105
|
*/
|
|
91
|
-
resolveChatProject(chatId: string): {
|
|
106
|
+
resolveChatProject(chatId: string): Promise<{
|
|
92
107
|
name: string;
|
|
93
108
|
folderPath: string;
|
|
94
|
-
} | null
|
|
109
|
+
} | null>;
|
|
95
110
|
/**
|
|
96
111
|
* 解析本轮生效的智能体:人设前言、技能勾选、工具策略同源于这一次解析。
|
|
97
112
|
*
|
|
@@ -105,7 +120,7 @@ export declare class LocalBackendRouter {
|
|
|
105
120
|
*
|
|
106
121
|
* @param chatId 当前对话 id。
|
|
107
122
|
* @param payload 前端提交的流请求体。
|
|
108
|
-
* @returns
|
|
123
|
+
* @returns 本轮的智能体顺序、人设智能体、自称,以及合并后的能力面。
|
|
109
124
|
*/
|
|
110
125
|
private resolveTurnAgents;
|
|
111
126
|
private buildConversationMessages;
|
|
@@ -158,6 +173,13 @@ export declare class LocalBackendRouter {
|
|
|
158
173
|
* the reverse-channel handler registered in main.ts.
|
|
159
174
|
*/
|
|
160
175
|
private handleCoreLoopTurn;
|
|
176
|
+
/**
|
|
177
|
+
* 回合结束后生成 3 条下一轮用户输入建议。先同步推启发式兜底(芯片立刻出现),
|
|
178
|
+
* 再 fire-and-forget 跑 LLM,成功则替换。走 broadcast 而不是 SSE:`[DONE]`
|
|
179
|
+
* 之后渲染端已经不再监听这条流。
|
|
180
|
+
*/
|
|
181
|
+
private runSuggestedRepliesInBackground;
|
|
182
|
+
private publishSuggestedReplies;
|
|
161
183
|
/**
|
|
162
184
|
* 标准"匿名事件" SSE:不带 event 行,前端 SSEParser 会落到默认 onMessage 分支,
|
|
163
185
|
* 用 parseSSEData 把 data 反序列化为 JSON 后用 type/content 字段路由。
|