dsh-advisor-group 0.1.0 → 0.1.1
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 +128 -100
- package/README.zh.md +131 -100
- package/lib/client.js +777 -90
- package/lib/index.d.mts +108 -1
- package/lib/index.mjs +1944 -245
- package/package.json +42 -28
package/lib/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import Schema from "@deepseek-ai/schemastery";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { promises, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, promises, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { KNOWN_SESSION_EVENT_TYPES, Session } from "@deepseek-ai/dsh-session";
|
|
6
7
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
9
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
9
|
-
import { KNOWN_SESSION_EVENT_TYPES } from "@deepseek-ai/dsh-session";
|
|
10
10
|
//#region src/defaults.ts
|
|
11
11
|
const DEFAULT_ADVISOR_PROMPT = `
|
|
12
12
|
你是顾问群中的资深专家。你的任务不是替用户执行,而是提供高质量的专业意见供主模型参考。
|
|
@@ -47,7 +47,19 @@ const AdvisorConfig = Schema.object({
|
|
|
47
47
|
clearApiKey: Schema.boolean().description("Transient client intent: clear the current provider direct key. Stripped before persistence."),
|
|
48
48
|
apiKeyEnv: Schema.string().description("Optional env var name for direct-http fallback."),
|
|
49
49
|
protocol: ProviderProtocol,
|
|
50
|
-
authMode: ProviderAuthMode.description("Anthropic auth header style: x-api-key or bearer.")
|
|
50
|
+
authMode: ProviderAuthMode.description("Anthropic auth header style: x-api-key or bearer."),
|
|
51
|
+
thinking: Schema.union([Schema.const("enabled"), Schema.const("disabled")]).description("Per-advisor thinking mode; unset = provider default. disabled forces no thinking block (DSH built-in channel maps it to reasoningEffort off)."),
|
|
52
|
+
reasoningEffort: Schema.union([
|
|
53
|
+
Schema.const("off"),
|
|
54
|
+
Schema.const("low"),
|
|
55
|
+
Schema.const("high"),
|
|
56
|
+
Schema.const("max")
|
|
57
|
+
]).description("Per-advisor thinking strength; unset = provider/model default. Standard DSH effort ids; off disables thinking per request."),
|
|
58
|
+
tools: Schema.union([
|
|
59
|
+
Schema.const("readonly"),
|
|
60
|
+
Schema.const("all"),
|
|
61
|
+
Schema.const("off")
|
|
62
|
+
]).description("Per-advisor tool calling scope override; unset = follow the global discussion.advisorTools default.")
|
|
51
63
|
});
|
|
52
64
|
const DriverModel = Schema.object({
|
|
53
65
|
provider: Schema.string().required().description("Provider route of the fallback driver model."),
|
|
@@ -60,12 +72,34 @@ const DiscussionConfig = Schema.object({
|
|
|
60
72
|
autoDeepen: Schema.boolean().default(true).description("Run the auto-deepen pipeline: after each round the driver model asks a deeper follow-up, and a final conclusion closes the consultation."),
|
|
61
73
|
driverModel: Schema.union([DriverModel, Schema.const(void 0)]).description("Fallback driver model (provider/model) used when the session header cannot be read."),
|
|
62
74
|
stopOnConsensus: Schema.boolean().default(false).description("Deprecated: dormant under the auto-deepen pipeline; kept for stored-config compatibility."),
|
|
63
|
-
advisorTimeoutMs: Schema.number().min(1e3).max(6e5).default(
|
|
75
|
+
advisorTimeoutMs: Schema.number().min(1e3).max(6e5).default(6e5).description("Per-advisor call timeout in milliseconds (default 600000 = 10 min). Long-reasoning models can exhaust shorter budgets on the thinking chain and lose the answer body; a timeout keeps the partial thinking chain and marks the message as truncated."),
|
|
76
|
+
driverTimeoutMs: Schema.number().min(1e3).max(12e5).default(6e5).description("Driver generation timeout in milliseconds (deep-question / conclusion; default 600000 = 10 min). Long discussions with many tool results need a real budget."),
|
|
77
|
+
advisorTools: Schema.union([
|
|
78
|
+
Schema.const("readonly"),
|
|
79
|
+
Schema.const("all"),
|
|
80
|
+
Schema.const("off")
|
|
81
|
+
]).default("readonly").description("Global default advisor tool calling scope for advisors that do not set their own `tools` field: readonly (default; read-only session tools), all (every session-visible tool incl. writable ones), off (disabled). Only the direct-http OpenAI/Anthropic channel supports tool calling.")
|
|
64
82
|
});
|
|
65
83
|
const TriggerConfig = Schema.object({
|
|
66
84
|
requireClassifier: Schema.boolean().default(true).description("Run the pre-classifier before starting a consultation."),
|
|
67
85
|
allowWebFallback: Schema.boolean().default(true).description("When classifier says web search is better, return that hint to the main model."),
|
|
68
|
-
confidenceThreshold: Schema.number().min(0).max(1).default(.6).description("Below this confidence score, ask_advisors should escalate even without domain keywords.")
|
|
86
|
+
confidenceThreshold: Schema.number().min(0).max(1).default(.6).description("Below this confidence score, ask_advisors should escalate even without domain keywords."),
|
|
87
|
+
jev: Schema.object({
|
|
88
|
+
enabled: Schema.boolean().default(false).description("Use Jev for semantic pre-classification when configured."),
|
|
89
|
+
provider: Schema.union([Schema.const("typesafe"), Schema.const("openrouter")]).default("typesafe"),
|
|
90
|
+
model: Schema.string().default("jev-latest"),
|
|
91
|
+
baseURL: Schema.string().description("Optional custom Jev API endpoint."),
|
|
92
|
+
apiKey: Schema.string().role("secret").description("Jev API key; prefer apiKeyEnv when possible."),
|
|
93
|
+
apiKeyEnv: Schema.string().description("Optional environment variable containing the Jev API key."),
|
|
94
|
+
timeoutMs: Schema.number().min(1e3).max(6e4).default(1e4),
|
|
95
|
+
confidenceThreshold: Schema.number().min(0).max(1).default(.6),
|
|
96
|
+
useEnglishState: Schema.boolean().default(false).description("Judge with the caller-provided English gist (questionEn) as the Jev state instead of the Chinese question; display and session records stay Chinese."),
|
|
97
|
+
highRiskThreshold: Schema.number().min(0).max(1).description("Threshold for the high_risk question; unset means follow confidenceThreshold."),
|
|
98
|
+
apiKeyMeta: Schema.object({
|
|
99
|
+
configured: Schema.boolean(),
|
|
100
|
+
last4: Schema.string()
|
|
101
|
+
}).description("Server-only API key presence metadata.")
|
|
102
|
+
}).default({})
|
|
69
103
|
});
|
|
70
104
|
const UiConfig = Schema.object({
|
|
71
105
|
theme: Schema.union([
|
|
@@ -76,13 +110,24 @@ const UiConfig = Schema.object({
|
|
|
76
110
|
showTimestamps: Schema.boolean().default(true),
|
|
77
111
|
autoExpand: Schema.boolean().default(true)
|
|
78
112
|
});
|
|
113
|
+
const QuotaConfig = Schema.object({
|
|
114
|
+
enabled: Schema.boolean().default(true).description("Enable the daily new-consultation cap (cost safety valve)."),
|
|
115
|
+
maxPerDay: Schema.natural().min(1).max(1e5).default(50).description("Max new consultations per UTC day (1–100000); ignored when enabled is false.")
|
|
116
|
+
});
|
|
79
117
|
const Config = Schema.object({
|
|
80
|
-
enabled: Schema.boolean().default(true),
|
|
81
|
-
discussion: DiscussionConfig,
|
|
82
|
-
trigger: TriggerConfig,
|
|
83
|
-
ui: UiConfig,
|
|
84
|
-
|
|
118
|
+
enabled: Schema.boolean().default(true).volatile(),
|
|
119
|
+
discussion: DiscussionConfig.volatile(),
|
|
120
|
+
trigger: TriggerConfig.volatile(),
|
|
121
|
+
ui: UiConfig.volatile(),
|
|
122
|
+
quota: QuotaConfig.volatile(),
|
|
123
|
+
advisors: Schema.array(AdvisorConfig).default([]).volatile()
|
|
85
124
|
});
|
|
125
|
+
function unwrapVolatileConfig(value) {
|
|
126
|
+
if (value && typeof value === "object" && "get" in value && typeof value.get === "function") return unwrapVolatileConfig(value.get());
|
|
127
|
+
if (Array.isArray(value)) return value.map((item) => unwrapVolatileConfig(item));
|
|
128
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unwrapVolatileConfig(item)]));
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
86
131
|
//#endregion
|
|
87
132
|
//#region src/providers/advisor-prompt.ts
|
|
88
133
|
/**
|
|
@@ -96,7 +141,23 @@ const ADVISOR_OUTPUT_POLICY = `
|
|
|
96
141
|
回答要求:
|
|
97
142
|
- 先给结论,再简要说明依据;
|
|
98
143
|
- 在回答末尾用一行标注:置信度(0-10)或不确定性;
|
|
99
|
-
-
|
|
144
|
+
- 如果没有把握,请明确写“不确定”,不要强行编造;
|
|
145
|
+
- **全程使用中文输出**:包括调用工具前的行动说明、正文与最终结论(代码、路径、专业术语可保留原文);不要把“我接下来要做什么”的说明写成英文;
|
|
146
|
+
- 思考(思维链)请保持精简:只列关键推理要点,切勿让思考链占满输出预算——必须保证正文完整(历史教训:长思考+长上下文会耗尽输出上限,导致正文丢失/中途截断);
|
|
147
|
+
- **最终答复必须输出一段完整的中文正文回答**:不要只输出工具调用描述/标记,不要输出空白或空消息——即使本轮没有新结论,也要用正文说明你的判断(这是硬性输出约束)。
|
|
148
|
+
`;
|
|
149
|
+
/**
|
|
150
|
+
* Tool-calling guidance appended to an advisor's system prompt ONLY when the
|
|
151
|
+
* advisor actually has tools configured and the session-visible set resolved
|
|
152
|
+
* (callAdvisor adds it on the direct-http tool loop path).
|
|
153
|
+
*/
|
|
154
|
+
const ADVISOR_TOOL_GUIDANCE = `
|
|
155
|
+
|
|
156
|
+
工具使用指引:
|
|
157
|
+
- 你可以调用当前会话提供的工具(检索/读取/搜索等)来辅助回答;
|
|
158
|
+
- 对于你不了解、不熟悉的内容,尤其是项目背景、代码细节、仓库状态等不在你知识范围内的情况,请优先使用联网工具(如 web_search、web_fetch)搜索核实,再基于实际信息给出意见;
|
|
159
|
+
- 仅在确有必要时调用工具:先了解现状,再下结论;不要编造你未核实的事实;
|
|
160
|
+
- **调用工具前必须先用一句中文说明你准备做什么**(行动说明),例如“我先读取 package.json 确认版本”;这段行动说明会显示在卡片上,不要用英文写。
|
|
100
161
|
`;
|
|
101
162
|
/**
|
|
102
163
|
* Sequential-relay role hint appended to one advisor's system prompt.
|
|
@@ -129,6 +190,18 @@ function withTimeout(ms, signal) {
|
|
|
129
190
|
if (!signal) return timeout;
|
|
130
191
|
return AbortSignal.any([signal, timeout]);
|
|
131
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Same combination, but keeps the timeout signal reachable so the caller can
|
|
195
|
+
* classify an aborted stream: timeout fired -> graceful truncation (keep the
|
|
196
|
+
* partial thinking, mark `truncated`); caller signal fired -> cancellation.
|
|
197
|
+
*/
|
|
198
|
+
function timeoutSignalPair(ms, signal) {
|
|
199
|
+
const timeout = AbortSignal.timeout(ms);
|
|
200
|
+
return {
|
|
201
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
202
|
+
isTimeout: () => timeout.aborted
|
|
203
|
+
};
|
|
204
|
+
}
|
|
132
205
|
//#endregion
|
|
133
206
|
//#region src/providers/ctx-llm.ts
|
|
134
207
|
function formatTranscript(entries) {
|
|
@@ -140,6 +213,13 @@ function formatTranscript(entries) {
|
|
|
140
213
|
* This is the primary path: it reuses whatever provider routes the user has
|
|
141
214
|
* already configured in DSH (deepseek-official, llm-pi-ai providers, etc.),
|
|
142
215
|
* including credentials resolved by the harness.
|
|
216
|
+
*
|
|
217
|
+
* Timeout semantics (root cause of "thinking cut mid-sentence, no body"): a
|
|
218
|
+
* long-reasoning model can spend the whole advisor budget on its thinking
|
|
219
|
+
* chain. When `timeoutMs` fires, the stream simply stops (it does not throw),
|
|
220
|
+
* which used to produce a silent empty body. We now detect the timeout and
|
|
221
|
+
* report `truncated` so the caller can surface it to the user instead of
|
|
222
|
+
* treating the reply as complete. A caller-initiated abort still propagates.
|
|
143
223
|
*/
|
|
144
224
|
async function callViaCtxLlm(ctx, advisor, transcript, signal, onDelta, timeoutMs) {
|
|
145
225
|
const userText = formatTranscript(transcript);
|
|
@@ -148,26 +228,54 @@ async function callViaCtxLlm(ctx, advisor, transcript, signal, onDelta, timeoutM
|
|
|
148
228
|
type: "text",
|
|
149
229
|
text: userText
|
|
150
230
|
}],
|
|
151
|
-
source: {
|
|
152
|
-
kind: "plugin",
|
|
153
|
-
plugin: "dsh-advisor-group"
|
|
154
|
-
}
|
|
231
|
+
source: { kind: "user" }
|
|
155
232
|
})];
|
|
233
|
+
const pair = timeoutSignalPair(timeoutMs ?? 12e4, signal);
|
|
234
|
+
const reasoningEffort = advisor.thinking === "disabled" ? "off" : advisor.reasoningEffort;
|
|
156
235
|
const options = {
|
|
157
236
|
provider: advisor.provider,
|
|
158
237
|
model: advisor.model,
|
|
159
238
|
system: `${advisor.systemPrompt}${ADVISOR_OUTPUT_POLICY}`,
|
|
160
239
|
messages,
|
|
240
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort },
|
|
161
241
|
temperature: advisor.temperature,
|
|
162
|
-
maxTokens: advisor.maxTokens,
|
|
163
|
-
signal:
|
|
242
|
+
maxTokens: advisor.maxTokens ?? 16384,
|
|
243
|
+
signal: pair.signal
|
|
164
244
|
};
|
|
165
245
|
let text = "";
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
246
|
+
let thinking = "";
|
|
247
|
+
try {
|
|
248
|
+
for await (const chunk of ctx.llm.stream(options)) if (chunk.type === "text-delta") {
|
|
249
|
+
text += chunk.text;
|
|
250
|
+
onDelta?.({ text: chunk.text });
|
|
251
|
+
} else if (chunk.type === "reasoning-delta") {
|
|
252
|
+
thinking += chunk.text;
|
|
253
|
+
onDelta?.({ thinking: chunk.text });
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (pair.isTimeout()) return {
|
|
257
|
+
content: text,
|
|
258
|
+
thinking,
|
|
259
|
+
truncated: {
|
|
260
|
+
reason: "timeout",
|
|
261
|
+
atMs: Date.now()
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (pair.isTimeout()) return {
|
|
267
|
+
content: text,
|
|
268
|
+
thinking,
|
|
269
|
+
truncated: {
|
|
270
|
+
reason: "timeout",
|
|
271
|
+
atMs: Date.now()
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
275
|
+
return {
|
|
276
|
+
content: text,
|
|
277
|
+
thinking
|
|
278
|
+
};
|
|
171
279
|
}
|
|
172
280
|
//#endregion
|
|
173
281
|
//#region src/providers/presets.ts
|
|
@@ -178,7 +286,11 @@ const PROVIDER_PRESETS = {
|
|
|
178
286
|
protocol: "openai",
|
|
179
287
|
baseURL: "https://api.deepseek.com",
|
|
180
288
|
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
181
|
-
defaultModels: [
|
|
289
|
+
defaultModels: [
|
|
290
|
+
"deepseek-v4-flash",
|
|
291
|
+
"deepseek-v4-pro",
|
|
292
|
+
"deepseek-v4-flash-vision-exp"
|
|
293
|
+
],
|
|
182
294
|
notes: "Also available as deepseek-official via dsh-llm-deepseek, and Anthropic-compatible at https://api.deepseek.com/anthropic."
|
|
183
295
|
},
|
|
184
296
|
"deepseek-anthropic": {
|
|
@@ -187,7 +299,11 @@ const PROVIDER_PRESETS = {
|
|
|
187
299
|
protocol: "anthropic",
|
|
188
300
|
baseURL: "https://api.deepseek.com/anthropic",
|
|
189
301
|
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
190
|
-
defaultModels: [
|
|
302
|
+
defaultModels: [
|
|
303
|
+
"deepseek-v4-flash",
|
|
304
|
+
"deepseek-v4-pro",
|
|
305
|
+
"deepseek-v4-flash-vision-exp"
|
|
306
|
+
],
|
|
191
307
|
authMode: "x-api-key",
|
|
192
308
|
modelsBaseURL: "https://api.deepseek.com",
|
|
193
309
|
modelsAuthMode: "bearer",
|
|
@@ -199,7 +315,12 @@ const PROVIDER_PRESETS = {
|
|
|
199
315
|
protocol: "openai",
|
|
200
316
|
baseURL: "https://api.moonshot.cn/v1",
|
|
201
317
|
apiKeyEnv: "MOONSHOT_API_KEY",
|
|
202
|
-
defaultModels: [
|
|
318
|
+
defaultModels: [
|
|
319
|
+
"kimi-k3",
|
|
320
|
+
"kimi-k2.7-code",
|
|
321
|
+
"kimi-k2.7-code-highspeed",
|
|
322
|
+
"kimi-k2.6"
|
|
323
|
+
],
|
|
203
324
|
notes: "OpenAI-compatible; Anthropic-compatible endpoint at https://api.moonshot.cn/anthropic"
|
|
204
325
|
},
|
|
205
326
|
"moonshot-anthropic": {
|
|
@@ -208,7 +329,12 @@ const PROVIDER_PRESETS = {
|
|
|
208
329
|
protocol: "anthropic",
|
|
209
330
|
baseURL: "https://api.moonshot.cn/anthropic",
|
|
210
331
|
apiKeyEnv: "MOONSHOT_API_KEY",
|
|
211
|
-
defaultModels: [
|
|
332
|
+
defaultModels: [
|
|
333
|
+
"kimi-k3",
|
|
334
|
+
"kimi-k2.7-code",
|
|
335
|
+
"kimi-k2.7-code-highspeed",
|
|
336
|
+
"kimi-k2.6"
|
|
337
|
+
],
|
|
212
338
|
authMode: "bearer",
|
|
213
339
|
modelsBaseURL: "https://api.moonshot.cn/v1",
|
|
214
340
|
modelsAuthMode: "bearer",
|
|
@@ -221,14 +347,14 @@ const PROVIDER_PRESETS = {
|
|
|
221
347
|
baseURL: "https://api.kimi.com/coding/v1",
|
|
222
348
|
apiKeyEnv: "KIMI_CODE_API_KEY",
|
|
223
349
|
defaultModels: [
|
|
224
|
-
"k3",
|
|
225
|
-
"
|
|
226
|
-
"kimi-
|
|
227
|
-
"kimi-for-coding
|
|
350
|
+
"kimi-k3",
|
|
351
|
+
"kimi-k2.7-code",
|
|
352
|
+
"kimi-k2.7-code-highspeed",
|
|
353
|
+
"kimi-for-coding"
|
|
228
354
|
],
|
|
229
355
|
modelsBaseURL: "https://api.kimi.com/coding/v1",
|
|
230
356
|
modelsAuthMode: "bearer",
|
|
231
|
-
notes: "Kimi Code 会员 API
|
|
357
|
+
notes: "Kimi Code 会员 API;可用模型以会员页为准,常用 kimi-k3 / kimi-k2.7-code / kimi-k2.7-code-highspeed,旧 kimi-for-coding 仍兼容。"
|
|
232
358
|
},
|
|
233
359
|
"kimi-code-anthropic": {
|
|
234
360
|
id: "kimi-code-anthropic",
|
|
@@ -237,10 +363,10 @@ const PROVIDER_PRESETS = {
|
|
|
237
363
|
baseURL: "https://api.kimi.com/coding",
|
|
238
364
|
apiKeyEnv: "KIMI_CODE_API_KEY",
|
|
239
365
|
defaultModels: [
|
|
240
|
-
"k3",
|
|
241
|
-
"
|
|
242
|
-
"kimi-
|
|
243
|
-
"kimi-for-coding
|
|
366
|
+
"kimi-k3",
|
|
367
|
+
"kimi-k2.7-code",
|
|
368
|
+
"kimi-k2.7-code-highspeed",
|
|
369
|
+
"kimi-for-coding"
|
|
244
370
|
],
|
|
245
371
|
authMode: "bearer",
|
|
246
372
|
modelsBaseURL: "https://api.kimi.com/coding/v1",
|
|
@@ -254,9 +380,9 @@ const PROVIDER_PRESETS = {
|
|
|
254
380
|
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
255
381
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
256
382
|
defaultModels: [
|
|
257
|
-
"qwen3.
|
|
258
|
-
"qwen3.
|
|
259
|
-
"
|
|
383
|
+
"qwen3.8-max",
|
|
384
|
+
"qwen3.8-flash",
|
|
385
|
+
"qwen3.7-plus"
|
|
260
386
|
],
|
|
261
387
|
notes: "DashScope OpenAI-compatible endpoint(北京公共域名)。"
|
|
262
388
|
},
|
|
@@ -267,9 +393,9 @@ const PROVIDER_PRESETS = {
|
|
|
267
393
|
baseURL: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
268
394
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
269
395
|
defaultModels: [
|
|
270
|
-
"qwen3.
|
|
271
|
-
"qwen3.
|
|
272
|
-
"
|
|
396
|
+
"qwen3.8-max",
|
|
397
|
+
"qwen3.8-flash",
|
|
398
|
+
"qwen3.7-plus"
|
|
273
399
|
],
|
|
274
400
|
notes: "DashScope 新加坡公共域名 OpenAI 兼容端点。"
|
|
275
401
|
},
|
|
@@ -280,9 +406,9 @@ const PROVIDER_PRESETS = {
|
|
|
280
406
|
baseURL: "https://dashscope-us.aliyuncs.com/compatible-mode/v1",
|
|
281
407
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
282
408
|
defaultModels: [
|
|
283
|
-
"qwen3.
|
|
284
|
-
"qwen3.
|
|
285
|
-
"
|
|
409
|
+
"qwen3.8-max",
|
|
410
|
+
"qwen3.8-flash",
|
|
411
|
+
"qwen3.7-plus"
|
|
286
412
|
],
|
|
287
413
|
notes: "DashScope 美国(弗吉尼亚)公共域名 OpenAI 兼容端点。"
|
|
288
414
|
},
|
|
@@ -293,13 +419,13 @@ const PROVIDER_PRESETS = {
|
|
|
293
419
|
baseURL: "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/apps/anthropic",
|
|
294
420
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
295
421
|
defaultModels: [
|
|
296
|
-
"qwen3.
|
|
422
|
+
"qwen3.8-max",
|
|
423
|
+
"qwen3.8-flash",
|
|
297
424
|
"qwen3.7-plus",
|
|
298
|
-
"qwen3.6-plus",
|
|
299
425
|
"deepseek-v4-pro",
|
|
300
426
|
"deepseek-v4-flash",
|
|
301
|
-
"kimi-
|
|
302
|
-
"glm-5.
|
|
427
|
+
"kimi-k3",
|
|
428
|
+
"glm-5.3"
|
|
303
429
|
],
|
|
304
430
|
authMode: "bearer",
|
|
305
431
|
modelsBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
@@ -313,13 +439,13 @@ const PROVIDER_PRESETS = {
|
|
|
313
439
|
baseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
|
|
314
440
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
315
441
|
defaultModels: [
|
|
316
|
-
"qwen3.
|
|
442
|
+
"qwen3.8-max",
|
|
443
|
+
"qwen3.8-flash",
|
|
317
444
|
"qwen3.7-plus",
|
|
318
|
-
"qwen3.6-plus",
|
|
319
445
|
"deepseek-v4-pro",
|
|
320
446
|
"deepseek-v4-flash",
|
|
321
|
-
"kimi-
|
|
322
|
-
"glm-5.
|
|
447
|
+
"kimi-k3",
|
|
448
|
+
"glm-5.3"
|
|
323
449
|
],
|
|
324
450
|
authMode: "bearer",
|
|
325
451
|
modelsBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
@@ -333,13 +459,13 @@ const PROVIDER_PRESETS = {
|
|
|
333
459
|
baseURL: "https://dashscope-intl.aliyuncs.com/apps/anthropic",
|
|
334
460
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
335
461
|
defaultModels: [
|
|
336
|
-
"qwen3.
|
|
462
|
+
"qwen3.8-max",
|
|
463
|
+
"qwen3.8-flash",
|
|
337
464
|
"qwen3.7-plus",
|
|
338
|
-
"qwen3.6-plus",
|
|
339
465
|
"deepseek-v4-pro",
|
|
340
466
|
"deepseek-v4-flash",
|
|
341
|
-
"kimi-
|
|
342
|
-
"glm-5.
|
|
467
|
+
"kimi-k3",
|
|
468
|
+
"glm-5.3"
|
|
343
469
|
],
|
|
344
470
|
authMode: "bearer",
|
|
345
471
|
modelsBaseURL: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
@@ -353,13 +479,13 @@ const PROVIDER_PRESETS = {
|
|
|
353
479
|
baseURL: "https://dashscope-us.aliyuncs.com/apps/anthropic",
|
|
354
480
|
apiKeyEnv: "DASHSCOPE_API_KEY",
|
|
355
481
|
defaultModels: [
|
|
356
|
-
"qwen3.
|
|
482
|
+
"qwen3.8-max",
|
|
483
|
+
"qwen3.8-flash",
|
|
357
484
|
"qwen3.7-plus",
|
|
358
|
-
"qwen3.6-plus",
|
|
359
485
|
"deepseek-v4-pro",
|
|
360
486
|
"deepseek-v4-flash",
|
|
361
|
-
"kimi-
|
|
362
|
-
"glm-5.
|
|
487
|
+
"kimi-k3",
|
|
488
|
+
"glm-5.3"
|
|
363
489
|
],
|
|
364
490
|
authMode: "bearer",
|
|
365
491
|
modelsBaseURL: "https://dashscope-us.aliyuncs.com/compatible-mode/v1",
|
|
@@ -373,13 +499,13 @@ const PROVIDER_PRESETS = {
|
|
|
373
499
|
baseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic",
|
|
374
500
|
apiKeyEnv: "DASHSCOPE_TOKEN_PLAN_API_KEY",
|
|
375
501
|
defaultModels: [
|
|
376
|
-
"qwen3.
|
|
502
|
+
"qwen3.8-max",
|
|
503
|
+
"qwen3.8-flash",
|
|
377
504
|
"qwen3.7-plus",
|
|
378
|
-
"qwen3.6-plus",
|
|
379
505
|
"deepseek-v4-pro",
|
|
380
506
|
"deepseek-v4-flash",
|
|
381
|
-
"kimi-
|
|
382
|
-
"glm-5.
|
|
507
|
+
"kimi-k3",
|
|
508
|
+
"glm-5.3"
|
|
383
509
|
],
|
|
384
510
|
authMode: "bearer",
|
|
385
511
|
modelsBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
@@ -393,13 +519,13 @@ const PROVIDER_PRESETS = {
|
|
|
393
519
|
baseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
|
394
520
|
apiKeyEnv: "DASHSCOPE_CODING_PLAN_API_KEY",
|
|
395
521
|
defaultModels: [
|
|
396
|
-
"qwen3.
|
|
522
|
+
"qwen3.8-max",
|
|
523
|
+
"qwen3.8-flash",
|
|
397
524
|
"qwen3.7-plus",
|
|
398
|
-
"qwen3.6-plus",
|
|
399
525
|
"deepseek-v4-pro",
|
|
400
526
|
"deepseek-v4-flash",
|
|
401
|
-
"kimi-
|
|
402
|
-
"glm-5.
|
|
527
|
+
"kimi-k3",
|
|
528
|
+
"glm-5.3"
|
|
403
529
|
],
|
|
404
530
|
authMode: "bearer",
|
|
405
531
|
modelsBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
@@ -414,12 +540,9 @@ const PROVIDER_PRESETS = {
|
|
|
414
540
|
apiKeyEnv: "ZHIPU_API_KEY",
|
|
415
541
|
defaultModels: [
|
|
416
542
|
"glm-5.3",
|
|
543
|
+
"glm-5.3-flash",
|
|
417
544
|
"glm-5.2",
|
|
418
|
-
"glm-5"
|
|
419
|
-
"glm-4.6",
|
|
420
|
-
"glm-4.6v",
|
|
421
|
-
"glm-4.6v-flashx",
|
|
422
|
-
"glm-4.5v"
|
|
545
|
+
"glm-5.1"
|
|
423
546
|
],
|
|
424
547
|
modelsBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
|
425
548
|
modelsAuthMode: "bearer",
|
|
@@ -433,12 +556,9 @@ const PROVIDER_PRESETS = {
|
|
|
433
556
|
apiKeyEnv: "ZHIPU_API_KEY",
|
|
434
557
|
defaultModels: [
|
|
435
558
|
"glm-5.3",
|
|
559
|
+
"glm-5.3-flash",
|
|
436
560
|
"glm-5.2",
|
|
437
|
-
"glm-5"
|
|
438
|
-
"glm-4.6",
|
|
439
|
-
"glm-4.6v",
|
|
440
|
-
"glm-4.6v-flashx",
|
|
441
|
-
"glm-4.5v"
|
|
561
|
+
"glm-5.1"
|
|
442
562
|
],
|
|
443
563
|
authMode: "bearer",
|
|
444
564
|
modelsBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
|
@@ -451,7 +571,14 @@ const PROVIDER_PRESETS = {
|
|
|
451
571
|
protocol: "openai",
|
|
452
572
|
baseURL: "https://api.openai.com/v1",
|
|
453
573
|
apiKeyEnv: "OPENAI_API_KEY",
|
|
454
|
-
defaultModels: [
|
|
574
|
+
defaultModels: [
|
|
575
|
+
"gpt-6-astra",
|
|
576
|
+
"gpt-5.6-sol",
|
|
577
|
+
"gpt-5.6-terra",
|
|
578
|
+
"gpt-5.6-luna",
|
|
579
|
+
"gpt-5.5",
|
|
580
|
+
"gpt-5.4"
|
|
581
|
+
],
|
|
455
582
|
notes: "Official OpenAI API."
|
|
456
583
|
},
|
|
457
584
|
anthropic: {
|
|
@@ -460,7 +587,13 @@ const PROVIDER_PRESETS = {
|
|
|
460
587
|
protocol: "anthropic",
|
|
461
588
|
baseURL: "https://api.anthropic.com",
|
|
462
589
|
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
463
|
-
defaultModels: [
|
|
590
|
+
defaultModels: [
|
|
591
|
+
"claude-opus-5",
|
|
592
|
+
"claude-sonnet-5",
|
|
593
|
+
"claude-opus-4-8",
|
|
594
|
+
"claude-opus-4-7",
|
|
595
|
+
"claude-sonnet-4-6"
|
|
596
|
+
],
|
|
464
597
|
authMode: "x-api-key",
|
|
465
598
|
notes: "Anthropic Messages API; docs in D:/X/X/14-claude."
|
|
466
599
|
},
|
|
@@ -470,7 +603,12 @@ const PROVIDER_PRESETS = {
|
|
|
470
603
|
protocol: "gemini",
|
|
471
604
|
baseURL: "https://generativelanguage.googleapis.com",
|
|
472
605
|
apiKeyEnv: "GEMINI_API_KEY",
|
|
473
|
-
defaultModels: [
|
|
606
|
+
defaultModels: [
|
|
607
|
+
"gemini-3.8-flash",
|
|
608
|
+
"gemini-3.7-flash",
|
|
609
|
+
"gemini-3.1-pro-preview",
|
|
610
|
+
"gemini-3-flash-preview"
|
|
611
|
+
],
|
|
474
612
|
notes: "Google Gemini API; docs in D:/X/X/13-gemini."
|
|
475
613
|
},
|
|
476
614
|
siliconflow: {
|
|
@@ -479,7 +617,11 @@ const PROVIDER_PRESETS = {
|
|
|
479
617
|
protocol: "openai",
|
|
480
618
|
baseURL: "https://api.siliconflow.cn/v1",
|
|
481
619
|
apiKeyEnv: "SILICONFLOW_API_KEY",
|
|
482
|
-
defaultModels: [
|
|
620
|
+
defaultModels: [
|
|
621
|
+
"deepseek-ai/DeepSeek-V3.2",
|
|
622
|
+
"Qwen/Qwen3-235B-A22B",
|
|
623
|
+
"zai-org/GLM-5.1"
|
|
624
|
+
],
|
|
483
625
|
notes: "OpenAI-compatible; also supports Anthropic-compatible at /v1/messages."
|
|
484
626
|
},
|
|
485
627
|
"siliconflow-anthropic": {
|
|
@@ -490,8 +632,8 @@ const PROVIDER_PRESETS = {
|
|
|
490
632
|
apiKeyEnv: "SILICONFLOW_API_KEY",
|
|
491
633
|
defaultModels: [
|
|
492
634
|
"deepseek-ai/DeepSeek-V3.2",
|
|
493
|
-
"
|
|
494
|
-
"
|
|
635
|
+
"Qwen/Qwen3-32B",
|
|
636
|
+
"zai-org/GLM-5.1"
|
|
495
637
|
],
|
|
496
638
|
authMode: "bearer",
|
|
497
639
|
modelsBaseURL: "https://api.siliconflow.cn/v1",
|
|
@@ -505,8 +647,9 @@ const PROVIDER_PRESETS = {
|
|
|
505
647
|
baseURL: "https://aihubmix.com/v1",
|
|
506
648
|
apiKeyEnv: "AIHUBMIX_API_KEY",
|
|
507
649
|
defaultModels: [
|
|
508
|
-
"gpt-5",
|
|
650
|
+
"gpt-5.2",
|
|
509
651
|
"claude-sonnet-4-5",
|
|
652
|
+
"claude-opus-4-5",
|
|
510
653
|
"gemini-3-flash-preview"
|
|
511
654
|
],
|
|
512
655
|
notes: "Aggregator; also supports Anthropic-compatible at https://aihubmix.com."
|
|
@@ -519,8 +662,8 @@ const PROVIDER_PRESETS = {
|
|
|
519
662
|
apiKeyEnv: "AIHUBMIX_API_KEY",
|
|
520
663
|
defaultModels: [
|
|
521
664
|
"claude-sonnet-4-5",
|
|
522
|
-
"claude-opus-4-
|
|
523
|
-
"
|
|
665
|
+
"claude-opus-4-6",
|
|
666
|
+
"claude-opus-4-5"
|
|
524
667
|
],
|
|
525
668
|
authMode: "x-api-key",
|
|
526
669
|
modelsBaseURL: "https://aihubmix.com/v1",
|
|
@@ -534,8 +677,9 @@ const PROVIDER_PRESETS = {
|
|
|
534
677
|
baseURL: "https://openrouter.ai/api/v1",
|
|
535
678
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
536
679
|
defaultModels: [
|
|
537
|
-
"openai/gpt-5",
|
|
538
|
-
"
|
|
680
|
+
"openai/gpt-5.6",
|
|
681
|
+
"openai/gpt-5.5",
|
|
682
|
+
"anthropic/claude-sonnet-4.6",
|
|
539
683
|
"deepseek/deepseek-chat"
|
|
540
684
|
],
|
|
541
685
|
notes: "OpenAI-compatible; docs in D:/X/X/06-OpenRouter."
|
|
@@ -547,9 +691,9 @@ const PROVIDER_PRESETS = {
|
|
|
547
691
|
baseURL: "https://openrouter.ai/api",
|
|
548
692
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
549
693
|
defaultModels: [
|
|
550
|
-
"anthropic/claude-sonnet-4.
|
|
551
|
-
"anthropic/claude-opus-4-
|
|
552
|
-
"openai/gpt-5",
|
|
694
|
+
"anthropic/claude-sonnet-4.6",
|
|
695
|
+
"anthropic/claude-opus-4-6",
|
|
696
|
+
"openai/gpt-5.6",
|
|
553
697
|
"deepseek/deepseek-chat"
|
|
554
698
|
],
|
|
555
699
|
authMode: "bearer",
|
|
@@ -562,52 +706,282 @@ function getProviderPreset(id) {
|
|
|
562
706
|
return PROVIDER_PRESETS[id];
|
|
563
707
|
}
|
|
564
708
|
//#endregion
|
|
709
|
+
//#region src/dsml.ts
|
|
710
|
+
const OUTER_OPEN = /<[||]{0,2}(?:DSML[||]{0,2})?(?:tool_calls|function_calls)\s*>/g;
|
|
711
|
+
const OUTER_CLOSE_RE = /<\/[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}(?:tool_calls|function_calls)\s*>/;
|
|
712
|
+
const INVOKE_RE = /<[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}\s*invoke\s+name="([^"]+)">([\s\S]*?)<\/[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}\s*invoke\s*>/g;
|
|
713
|
+
const PARAM_RE = /<[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}\s*parameter\s+name="([^"]+)"(?:\s+string="true")?>([\s\S]*?)<\/[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}\s*parameter\s*>/g;
|
|
714
|
+
const PARAM_OPEN_RE = /<[||]{0,2}(?:DSML[||]{0,2})?[||]{0,2}\s*parameter\s+name="[^"]+"/g;
|
|
715
|
+
function insideFence(content, index) {
|
|
716
|
+
const fences = content.slice(0, index).match(/```/g);
|
|
717
|
+
return Boolean(fences && fences.length % 2 === 1);
|
|
718
|
+
}
|
|
719
|
+
/** Parse every invoke inside one outer block; null when any gate fails. */
|
|
720
|
+
function parseOuterBlock(block, allowedNames) {
|
|
721
|
+
const calls = [];
|
|
722
|
+
INVOKE_RE.lastIndex = 0;
|
|
723
|
+
let invoke;
|
|
724
|
+
let invokeCount = 0;
|
|
725
|
+
while ((invoke = INVOKE_RE.exec(block)) !== null) {
|
|
726
|
+
invokeCount += 1;
|
|
727
|
+
const name = invoke[1];
|
|
728
|
+
if (!allowedNames.has(name)) return null;
|
|
729
|
+
const body = invoke[2];
|
|
730
|
+
const params = {};
|
|
731
|
+
PARAM_RE.lastIndex = 0;
|
|
732
|
+
let param;
|
|
733
|
+
let paramCount = 0;
|
|
734
|
+
while ((param = PARAM_RE.exec(body)) !== null) {
|
|
735
|
+
paramCount += 1;
|
|
736
|
+
if (PARAM_OPEN_RE.test(param[2])) return null;
|
|
737
|
+
params[param[1]] = param[2];
|
|
738
|
+
PARAM_OPEN_RE.lastIndex = 0;
|
|
739
|
+
}
|
|
740
|
+
if ((body.match(PARAM_OPEN_RE) ?? []).length !== paramCount) return null;
|
|
741
|
+
PARAM_OPEN_RE.lastIndex = 0;
|
|
742
|
+
calls.push({
|
|
743
|
+
id: `text-${calls.length}`,
|
|
744
|
+
name,
|
|
745
|
+
argumentsJson: paramCount > 0 ? JSON.stringify(params) : "{}"
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (invokeCount === 0) return null;
|
|
749
|
+
return calls;
|
|
750
|
+
}
|
|
751
|
+
/** Extract text tool calls; blocks outside code fences, whitelisted names. */
|
|
752
|
+
function parseTextToolCalls(content, allowedNames) {
|
|
753
|
+
if (!content || allowedNames.size === 0) return {
|
|
754
|
+
toolCalls: [],
|
|
755
|
+
cleaned: content
|
|
756
|
+
};
|
|
757
|
+
const ranges = [];
|
|
758
|
+
const allCalls = [];
|
|
759
|
+
OUTER_OPEN.lastIndex = 0;
|
|
760
|
+
let match;
|
|
761
|
+
while ((match = OUTER_OPEN.exec(content)) !== null) {
|
|
762
|
+
const start = match.index;
|
|
763
|
+
if (insideFence(content, start)) continue;
|
|
764
|
+
const rest = content.slice(start);
|
|
765
|
+
const close = rest.match(OUTER_CLOSE_RE);
|
|
766
|
+
const calls = parseOuterBlock(close && close.index !== void 0 ? rest.slice(0, close.index) : rest, allowedNames);
|
|
767
|
+
if (calls === null) continue;
|
|
768
|
+
const end = close && close.index !== void 0 ? start + close.index + close[0].length : content.length;
|
|
769
|
+
allCalls.push(...calls);
|
|
770
|
+
ranges.push([start, end]);
|
|
771
|
+
}
|
|
772
|
+
if (allCalls.length === 0) return {
|
|
773
|
+
toolCalls: [],
|
|
774
|
+
cleaned: content
|
|
775
|
+
};
|
|
776
|
+
let cleaned = content;
|
|
777
|
+
for (let i = ranges.length - 1; i >= 0; i -= 1) {
|
|
778
|
+
const [from, to] = ranges[i];
|
|
779
|
+
cleaned = cleaned.slice(0, from) + cleaned.slice(to);
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
toolCalls: allCalls,
|
|
783
|
+
cleaned: cleaned.trim()
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
565
787
|
//#region src/providers/direct-http.ts
|
|
788
|
+
/** True when the user asked to force thinking off, through either spelling. */
|
|
789
|
+
function thinkingOff(advisor) {
|
|
790
|
+
return advisor.thinking === "disabled" || advisor.reasoningEffort === "off";
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* OpenAI-compatible reasoning params, provider-aware (2026-09-05 知识 X 最新):
|
|
794
|
+
* - DeepSeek: `thinking {type}` switch + `reasoning_effort` low/high/max.
|
|
795
|
+
* - Kimi/Kimi Code: kimi-k3 = top-level `reasoning_effort` low/high/max (no
|
|
796
|
+
* `thinking`); kimi-k2.7-code = always thinking, no params; kimi-k2.6 =
|
|
797
|
+
* `thinking {type}` switch only.
|
|
798
|
+
* - OpenAI official: `reasoning_effort` minimal|low|medium|high (no max, no off).
|
|
799
|
+
* - 阿里云百炼: `enable_thinking` bool (Qwen chat completions).
|
|
800
|
+
* - 智谱: `thinking {type}` switch + `reasoning_effort` (GLM-5.3 force-enabled,
|
|
801
|
+
* disabled→enabled+low; GLM-5.2/5.1/5 can disable).
|
|
802
|
+
* - 硅基流动: `enable_thinking` + `thinking_budget`.
|
|
803
|
+
* - Others (AIHubMix/OpenRouter/custom): best-effort `reasoning_effort`.
|
|
804
|
+
*/
|
|
805
|
+
function openAiThinkingParams(advisor) {
|
|
806
|
+
const provider = advisor.provider;
|
|
807
|
+
const model = advisor.model;
|
|
808
|
+
const effort = advisor.reasoningEffort;
|
|
809
|
+
if (provider === "deepseek") {
|
|
810
|
+
if (thinkingOff(advisor)) return { thinking: { type: "disabled" } };
|
|
811
|
+
return {
|
|
812
|
+
...advisor.thinking === "enabled" ? { thinking: { type: "enabled" } } : {},
|
|
813
|
+
...effort && effort !== "off" ? { reasoning_effort: effort } : {}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (provider === "moonshot" || provider === "kimi-code") {
|
|
817
|
+
if (/^(k3|kimi-k3)(-|$)/i.test(model)) {
|
|
818
|
+
if (effort && effort !== "off") return { reasoning_effort: effort };
|
|
819
|
+
return {};
|
|
820
|
+
}
|
|
821
|
+
if (/kimi-k2\.7-code|kimi-for-coding/i.test(model)) return {};
|
|
822
|
+
if (advisor.thinking === "disabled") return { thinking: { type: "disabled" } };
|
|
823
|
+
if (advisor.thinking === "enabled") return { thinking: { type: "enabled" } };
|
|
824
|
+
return {};
|
|
825
|
+
}
|
|
826
|
+
if (provider === "openai") {
|
|
827
|
+
if (effort === "low") return { reasoning_effort: "low" };
|
|
828
|
+
if (effort === "high" || effort === "max") return { reasoning_effort: "high" };
|
|
829
|
+
return {};
|
|
830
|
+
}
|
|
831
|
+
if (provider === "bailian" || provider === "bailian-openai-singapore" || provider === "bailian-openai-us") {
|
|
832
|
+
if (thinkingOff(advisor)) return { enable_thinking: false };
|
|
833
|
+
if (advisor.thinking === "enabled") return { enable_thinking: true };
|
|
834
|
+
return {};
|
|
835
|
+
}
|
|
836
|
+
if (provider === "zhipu") {
|
|
837
|
+
if (/glm-5\.3/i.test(model)) return {
|
|
838
|
+
thinking: { type: "enabled" },
|
|
839
|
+
reasoning_effort: thinkingOff(advisor) ? "low" : effort && effort !== "off" ? effort : "max"
|
|
840
|
+
};
|
|
841
|
+
if (/glm-5/i.test(model)) {
|
|
842
|
+
if (thinkingOff(advisor)) return { thinking: { type: "disabled" } };
|
|
843
|
+
return {
|
|
844
|
+
...advisor.thinking === "enabled" ? { thinking: { type: "enabled" } } : {},
|
|
845
|
+
...effort && effort !== "off" ? { reasoning_effort: effort } : {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
if (advisor.thinking === "disabled") return { thinking: { type: "disabled" } };
|
|
849
|
+
if (advisor.thinking === "enabled") return { thinking: { type: "enabled" } };
|
|
850
|
+
return {};
|
|
851
|
+
}
|
|
852
|
+
if (provider === "siliconflow") {
|
|
853
|
+
if (thinkingOff(advisor)) return { enable_thinking: false };
|
|
854
|
+
const budget = effort === "low" ? 1024 : effort === "high" ? 8192 : effort === "max" ? 16384 : 0;
|
|
855
|
+
return {
|
|
856
|
+
...advisor.thinking === "enabled" || budget > 0 ? { enable_thinking: true } : {},
|
|
857
|
+
...budget > 0 ? { thinking_budget: budget } : {}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (effort && effort !== "off") return { reasoning_effort: effort };
|
|
861
|
+
return {};
|
|
862
|
+
}
|
|
863
|
+
/** Anthropic Messages reasoning params, provider-aware.
|
|
864
|
+
* Official Anthropic: adaptive (`thinking:{type:'adaptive'}` + `output_config.effort`)
|
|
865
|
+
* for Opus 4.6/4.7/Sonnet 4.6/Mythos; manual `enabled`+`budget_tokens` for older
|
|
866
|
+
* models. DeepSeek's Anthropic-compatible endpoint uses `reasoning.effort` +
|
|
867
|
+
* `output_config.effort` (its `budget_tokens` is ignored). Other Anthropic skins
|
|
868
|
+
* follow the official Anthropic wire shape. */
|
|
869
|
+
function anthropicThinkingParams(advisor) {
|
|
870
|
+
if (advisor.provider === "deepseek-anthropic") {
|
|
871
|
+
if (thinkingOff(advisor)) return { reasoning: { effort: "none" } };
|
|
872
|
+
if (advisor.reasoningEffort && advisor.reasoningEffort !== "off") return {
|
|
873
|
+
reasoning: { effort: advisor.reasoningEffort },
|
|
874
|
+
output_config: { effort: advisor.reasoningEffort }
|
|
875
|
+
};
|
|
876
|
+
return {};
|
|
877
|
+
}
|
|
878
|
+
if (thinkingOff(advisor)) return { thinking: { type: "disabled" } };
|
|
879
|
+
if (advisor.reasoningEffort && advisor.reasoningEffort !== "off") {
|
|
880
|
+
if (/(?:opus-4-[6-8]|opus-5|sonnet-4-6|sonnet-5|fable|mythos)/i.test(advisor.model)) return {
|
|
881
|
+
thinking: { type: "adaptive" },
|
|
882
|
+
output_config: { effort: advisor.reasoningEffort }
|
|
883
|
+
};
|
|
884
|
+
return { thinking: {
|
|
885
|
+
type: "enabled",
|
|
886
|
+
budget_tokens: advisor.reasoningEffort === "low" ? 2048 : advisor.reasoningEffort === "high" ? 8192 : 16384
|
|
887
|
+
} };
|
|
888
|
+
}
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
/** Gemini `generateContent` thinking config. Disabling thinking uses
|
|
892
|
+
* `thinkingBudget: 0` (NOT `includeThoughts:false`, which only hides returned
|
|
893
|
+
* thoughts). Effort uses `thinkingBudget`, the backward-compatible knob that
|
|
894
|
+
* works across Gemini 2.5 and Gemini 3 per Google's migration note. */
|
|
895
|
+
function geminiThinkingParams(advisor) {
|
|
896
|
+
if (thinkingOff(advisor)) return { thinkingConfig: { thinkingBudget: 0 } };
|
|
897
|
+
const budget = advisor.reasoningEffort === "low" ? 1024 : advisor.reasoningEffort === "high" ? 8192 : advisor.reasoningEffort === "max" ? 16384 : 0;
|
|
898
|
+
if (budget > 0) return { thinkingConfig: { thinkingBudget: budget } };
|
|
899
|
+
return {};
|
|
900
|
+
}
|
|
901
|
+
/** Validate the per-advisor thinking fields at the direct-http boundary so a
|
|
902
|
+
* malformed stored config fails loudly instead of silently ignoring. */
|
|
903
|
+
function assertThinkingParams(advisor) {
|
|
904
|
+
if (advisor.thinking !== void 0 && advisor.thinking !== "enabled" && advisor.thinking !== "disabled") throw new Error(`Unsupported thinking mode: ${String(advisor.thinking)}`);
|
|
905
|
+
if (advisor.reasoningEffort !== void 0 && ![
|
|
906
|
+
"off",
|
|
907
|
+
"low",
|
|
908
|
+
"high",
|
|
909
|
+
"max"
|
|
910
|
+
].includes(advisor.reasoningEffort)) throw new Error(`Unsupported reasoningEffort: ${String(advisor.reasoningEffort)}`);
|
|
911
|
+
}
|
|
566
912
|
/** Stream an advisor response through direct HTTP (OpenAI/Anthropic SSE). */
|
|
567
|
-
async function streamDirectHttp(advisor, transcript, onDelta, signal, timeoutMs) {
|
|
913
|
+
async function streamDirectHttp(advisor, transcript, onDelta, signal, timeoutMs, tools, parseToolNames) {
|
|
568
914
|
const preset = advisor.provider ? getProviderPreset(advisor.provider) : void 0;
|
|
569
915
|
const baseURL = (advisor.baseURL ?? preset?.baseURL ?? "").replace(/\/+$/, "");
|
|
570
916
|
const apiKey = advisor.apiKey ?? process.env[advisor.apiKeyEnv ?? preset?.apiKeyEnv ?? ""] ?? "";
|
|
571
917
|
const protocol = advisor.protocol ?? preset?.protocol ?? "openai";
|
|
572
918
|
const authMode = advisor.authMode ?? preset?.authMode ?? "x-api-key";
|
|
919
|
+
assertThinkingParams(advisor);
|
|
573
920
|
if (!baseURL || !apiKey) throw new Error(`Direct HTTP fallback is not configured for advisor "${advisor.name}" (${advisor.provider}). Add baseURL/apiKey or configure the provider in DSH.`);
|
|
574
921
|
const transcriptText = transcript.map((entry) => `[${entry.role === "main" ? "主模型" : entry.name}]\n${entry.content}`).join("\n\n");
|
|
922
|
+
let result;
|
|
575
923
|
switch (protocol) {
|
|
576
|
-
case "openai":
|
|
577
|
-
|
|
924
|
+
case "openai":
|
|
925
|
+
result = await streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText, onDelta, signal, timeoutMs, tools);
|
|
926
|
+
break;
|
|
927
|
+
case "anthropic":
|
|
928
|
+
result = await streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMode, onDelta, signal, timeoutMs, tools);
|
|
929
|
+
break;
|
|
578
930
|
case "gemini": {
|
|
579
931
|
const content = await callGemini(baseURL, advisor, apiKey, transcriptText, signal, timeoutMs);
|
|
580
932
|
onDelta({ text: content });
|
|
581
|
-
|
|
933
|
+
result = {
|
|
582
934
|
content,
|
|
583
935
|
thinking: ""
|
|
584
936
|
};
|
|
937
|
+
break;
|
|
585
938
|
}
|
|
586
939
|
default: throw new Error(`Unsupported protocol: ${String(protocol)}`);
|
|
587
940
|
}
|
|
941
|
+
const parsed = parseTextToolCalls(result.content, new Set(parseToolNames ?? (tools ?? []).map((tool) => tool.name)));
|
|
942
|
+
if (parsed.toolCalls.length > 0) {
|
|
943
|
+
console.warn(`[dsh-advisor-group] 检测到文本工具调用(DSML/非结构流),已还原并交给 runAdvisorToolLoop 执行:${parsed.toolCalls.map((tool) => tool.name).join("、")}`);
|
|
944
|
+
return {
|
|
945
|
+
...result,
|
|
946
|
+
content: parsed.cleaned,
|
|
947
|
+
toolCalls: [...result.toolCalls ?? [], ...parsed.toolCalls]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return result;
|
|
588
951
|
}
|
|
589
|
-
async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText, onDelta, signal, timeoutMs) {
|
|
952
|
+
async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText, onDelta, signal, timeoutMs, tools) {
|
|
590
953
|
const endpoint = baseURL.endsWith("/chat/completions") ? baseURL : `${baseURL.replace(/\/+$/, "")}/chat/completions`;
|
|
954
|
+
const pair = timeoutSignalPair(timeoutMs ?? 12e4, signal);
|
|
955
|
+
const body = {
|
|
956
|
+
model: advisor.model,
|
|
957
|
+
messages: [{
|
|
958
|
+
role: "system",
|
|
959
|
+
content: advisor.systemPrompt + ADVISOR_OUTPUT_POLICY
|
|
960
|
+
}, {
|
|
961
|
+
role: "user",
|
|
962
|
+
content: transcriptText
|
|
963
|
+
}],
|
|
964
|
+
temperature: advisor.temperature ?? .3,
|
|
965
|
+
max_tokens: advisor.maxTokens ?? 16384,
|
|
966
|
+
...openAiThinkingParams(advisor),
|
|
967
|
+
stream: true
|
|
968
|
+
};
|
|
969
|
+
if (tools && tools.length > 0) body.tools = tools.map((tool) => ({
|
|
970
|
+
type: "function",
|
|
971
|
+
function: {
|
|
972
|
+
name: tool.name,
|
|
973
|
+
...tool.description === void 0 ? {} : { description: tool.description },
|
|
974
|
+
parameters: tool.parameters ?? { type: "object" }
|
|
975
|
+
}
|
|
976
|
+
}));
|
|
591
977
|
const response = await fetch(endpoint, {
|
|
592
978
|
method: "POST",
|
|
593
979
|
headers: {
|
|
594
980
|
"content-type": "application/json",
|
|
595
981
|
authorization: `Bearer ${apiKey}`
|
|
596
982
|
},
|
|
597
|
-
body: JSON.stringify(
|
|
598
|
-
|
|
599
|
-
messages: [{
|
|
600
|
-
role: "system",
|
|
601
|
-
content: advisor.systemPrompt + ADVISOR_OUTPUT_POLICY
|
|
602
|
-
}, {
|
|
603
|
-
role: "user",
|
|
604
|
-
content: transcriptText
|
|
605
|
-
}],
|
|
606
|
-
temperature: advisor.temperature ?? .3,
|
|
607
|
-
max_tokens: advisor.maxTokens ?? 16384,
|
|
608
|
-
stream: true
|
|
609
|
-
}),
|
|
610
|
-
signal: withTimeout(timeoutMs ?? 12e4, signal)
|
|
983
|
+
body: JSON.stringify(body),
|
|
984
|
+
signal: pair.signal
|
|
611
985
|
});
|
|
612
986
|
if (!response.ok) throw new Error(`OpenAI-compatible request failed: ${response.status} ${await response.text()}`);
|
|
613
987
|
if (!response.body) throw new Error("OpenAI-compatible response has no body");
|
|
@@ -616,6 +990,7 @@ async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText,
|
|
|
616
990
|
let buffer = "";
|
|
617
991
|
let content = "";
|
|
618
992
|
let thinking = "";
|
|
993
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
619
994
|
while (true) {
|
|
620
995
|
let done;
|
|
621
996
|
let value;
|
|
@@ -624,8 +999,28 @@ async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText,
|
|
|
624
999
|
done = read.done;
|
|
625
1000
|
value = read.value;
|
|
626
1001
|
} catch (error) {
|
|
1002
|
+
const errName = error?.name ?? "UnknownError";
|
|
1003
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
1004
|
+
console.warn(`[dsh-advisor-group] direct-http stream read error: advisor=${advisor.name} base=${baseURL} name=${errName} msg=${errMessage} isTimeout=${pair.isTimeout()} signalAborted=${signal?.aborted ?? false} collectedChars=${content.length + thinking.length}`);
|
|
1005
|
+
if (pair.isTimeout()) return {
|
|
1006
|
+
content,
|
|
1007
|
+
thinking,
|
|
1008
|
+
toolCalls: collectToolCalls(toolCalls),
|
|
1009
|
+
truncated: {
|
|
1010
|
+
reason: "timeout",
|
|
1011
|
+
atMs: Date.now()
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
627
1014
|
if (signal?.aborted || error?.name === "AbortError") throw error;
|
|
628
|
-
|
|
1015
|
+
return {
|
|
1016
|
+
content,
|
|
1017
|
+
thinking,
|
|
1018
|
+
toolCalls: collectToolCalls(toolCalls),
|
|
1019
|
+
truncated: {
|
|
1020
|
+
reason: "network",
|
|
1021
|
+
atMs: Date.now()
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
629
1024
|
}
|
|
630
1025
|
if (done) break;
|
|
631
1026
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -641,6 +1036,18 @@ async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText,
|
|
|
641
1036
|
const delta = JSON.parse(data).choices?.[0]?.delta ?? {};
|
|
642
1037
|
const text = typeof delta.content === "string" ? delta.content : "";
|
|
643
1038
|
const think = typeof delta.reasoning_content === "string" ? delta.reasoning_content : typeof delta.reasoning === "string" ? delta.reasoning : "";
|
|
1039
|
+
if (delta.tool_calls) for (const tc of delta.tool_calls) {
|
|
1040
|
+
const idx = tc.index ?? 0;
|
|
1041
|
+
const current = toolCalls.get(idx) ?? {
|
|
1042
|
+
id: "",
|
|
1043
|
+
name: "",
|
|
1044
|
+
args: ""
|
|
1045
|
+
};
|
|
1046
|
+
if (tc.id) current.id = tc.id;
|
|
1047
|
+
if (tc.function?.name) current.name = tc.function.name;
|
|
1048
|
+
if (tc.function?.arguments) current.args += tc.function.arguments;
|
|
1049
|
+
toolCalls.set(idx, current);
|
|
1050
|
+
}
|
|
644
1051
|
if (text) {
|
|
645
1052
|
content += text;
|
|
646
1053
|
onDelta({ text });
|
|
@@ -655,10 +1062,24 @@ async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText,
|
|
|
655
1062
|
}
|
|
656
1063
|
return {
|
|
657
1064
|
content,
|
|
658
|
-
thinking
|
|
1065
|
+
thinking,
|
|
1066
|
+
toolCalls: collectToolCalls(toolCalls)
|
|
659
1067
|
};
|
|
660
1068
|
}
|
|
661
|
-
|
|
1069
|
+
/** Map<index, {id,name,args}> → model-facing tool call list. */
|
|
1070
|
+
function collectToolCalls(byIndex) {
|
|
1071
|
+
const calls = [];
|
|
1072
|
+
for (const [, current] of byIndex) {
|
|
1073
|
+
if (!current.name) continue;
|
|
1074
|
+
calls.push({
|
|
1075
|
+
id: current.id || `tool-${calls.length}`,
|
|
1076
|
+
name: current.name,
|
|
1077
|
+
argumentsJson: current.args
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
return calls.length > 0 ? calls : void 0;
|
|
1081
|
+
}
|
|
1082
|
+
async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMode, onDelta, signal, timeoutMs, tools) {
|
|
662
1083
|
const endpoint = baseURL.endsWith("/v1/messages") ? baseURL : baseURL.endsWith("/v1") ? `${baseURL}/messages` : `${baseURL.replace(/\/+$/, "")}/v1/messages`;
|
|
663
1084
|
const headers = {
|
|
664
1085
|
"content-type": "application/json",
|
|
@@ -666,21 +1087,29 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
666
1087
|
};
|
|
667
1088
|
if (authMode === "bearer") headers["authorization"] = `Bearer ${apiKey}`;
|
|
668
1089
|
else headers["x-api-key"] = apiKey;
|
|
1090
|
+
const body = {
|
|
1091
|
+
model: advisor.model,
|
|
1092
|
+
system: advisor.systemPrompt + ADVISOR_OUTPUT_POLICY,
|
|
1093
|
+
messages: [{
|
|
1094
|
+
role: "user",
|
|
1095
|
+
content: transcriptText
|
|
1096
|
+
}],
|
|
1097
|
+
temperature: advisor.temperature ?? .3,
|
|
1098
|
+
max_tokens: advisor.maxTokens ?? 16384,
|
|
1099
|
+
...anthropicThinkingParams(advisor),
|
|
1100
|
+
stream: true
|
|
1101
|
+
};
|
|
1102
|
+
if (tools && tools.length > 0) body.tools = tools.map((tool) => ({
|
|
1103
|
+
name: tool.name,
|
|
1104
|
+
...tool.description === void 0 ? {} : { description: tool.description },
|
|
1105
|
+
input_schema: tool.parameters ?? { type: "object" }
|
|
1106
|
+
}));
|
|
1107
|
+
const pair = timeoutSignalPair(timeoutMs ?? 12e4, signal);
|
|
669
1108
|
const response = await fetch(endpoint, {
|
|
670
1109
|
method: "POST",
|
|
671
1110
|
headers,
|
|
672
|
-
body: JSON.stringify(
|
|
673
|
-
|
|
674
|
-
system: advisor.systemPrompt + ADVISOR_OUTPUT_POLICY,
|
|
675
|
-
messages: [{
|
|
676
|
-
role: "user",
|
|
677
|
-
content: transcriptText
|
|
678
|
-
}],
|
|
679
|
-
temperature: advisor.temperature ?? .3,
|
|
680
|
-
max_tokens: advisor.maxTokens ?? 16384,
|
|
681
|
-
stream: true
|
|
682
|
-
}),
|
|
683
|
-
signal: withTimeout(timeoutMs ?? 12e4, signal)
|
|
1111
|
+
body: JSON.stringify(body),
|
|
1112
|
+
signal: pair.signal
|
|
684
1113
|
});
|
|
685
1114
|
if (!response.ok) throw new Error(`Anthropic request failed: ${response.status} ${await response.text()}`);
|
|
686
1115
|
if (!response.body) throw new Error("Anthropic response has no body");
|
|
@@ -689,6 +1118,9 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
689
1118
|
let buffer = "";
|
|
690
1119
|
let content = "";
|
|
691
1120
|
let thinking = "";
|
|
1121
|
+
let currentTool = null;
|
|
1122
|
+
let activeTool = false;
|
|
1123
|
+
const toolCalls = [];
|
|
692
1124
|
while (true) {
|
|
693
1125
|
let done;
|
|
694
1126
|
let value;
|
|
@@ -697,8 +1129,28 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
697
1129
|
done = read.done;
|
|
698
1130
|
value = read.value;
|
|
699
1131
|
} catch (error) {
|
|
1132
|
+
const errName = error?.name ?? "UnknownError";
|
|
1133
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
1134
|
+
console.warn(`[dsh-advisor-group] anthropic stream read error: advisor=${advisor.name} base=${baseURL} name=${errName} msg=${errMessage} isTimeout=${pair.isTimeout()} signalAborted=${signal?.aborted ?? false} collectedChars=${content.length + thinking.length}`);
|
|
1135
|
+
if (pair.isTimeout()) return {
|
|
1136
|
+
content,
|
|
1137
|
+
thinking,
|
|
1138
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1139
|
+
truncated: {
|
|
1140
|
+
reason: "timeout",
|
|
1141
|
+
atMs: Date.now()
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
700
1144
|
if (signal?.aborted || error?.name === "AbortError") throw error;
|
|
701
|
-
|
|
1145
|
+
return {
|
|
1146
|
+
content,
|
|
1147
|
+
thinking,
|
|
1148
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1149
|
+
truncated: {
|
|
1150
|
+
reason: "network",
|
|
1151
|
+
atMs: Date.now()
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
702
1154
|
}
|
|
703
1155
|
if (done) break;
|
|
704
1156
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -712,13 +1164,32 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
712
1164
|
if (!data) continue;
|
|
713
1165
|
try {
|
|
714
1166
|
const json = JSON.parse(data);
|
|
715
|
-
if (json.type === "
|
|
1167
|
+
if (json.type === "content_block_start" && json.content_block?.type === "tool_use") {
|
|
1168
|
+
currentTool = currentTool === null ? {
|
|
1169
|
+
id: json.content_block.id ?? "",
|
|
1170
|
+
name: json.content_block.name ?? "",
|
|
1171
|
+
args: ""
|
|
1172
|
+
} : currentTool;
|
|
1173
|
+
activeTool = true;
|
|
1174
|
+
} else if (json.type === "content_block_delta" && json.delta) {
|
|
716
1175
|
if (json.delta.type === "text_delta" && typeof json.delta.text === "string") {
|
|
717
1176
|
content += json.delta.text;
|
|
718
1177
|
onDelta({ text: json.delta.text });
|
|
719
1178
|
} else if (json.delta.type === "thinking_delta" && typeof json.delta.thinking === "string") {
|
|
720
1179
|
thinking += json.delta.thinking;
|
|
721
1180
|
onDelta({ thinking: json.delta.thinking });
|
|
1181
|
+
} else if (json.delta.type === "input_json_delta" && typeof json.delta.partial_json === "string") {
|
|
1182
|
+
if (currentTool) currentTool.args += json.delta.partial_json;
|
|
1183
|
+
}
|
|
1184
|
+
} else if (json.type === "content_block_stop") {
|
|
1185
|
+
if (activeTool && currentTool) {
|
|
1186
|
+
toolCalls.push({
|
|
1187
|
+
id: currentTool.id,
|
|
1188
|
+
name: currentTool.name,
|
|
1189
|
+
argumentsJson: currentTool.args
|
|
1190
|
+
});
|
|
1191
|
+
currentTool = null;
|
|
1192
|
+
activeTool = false;
|
|
722
1193
|
}
|
|
723
1194
|
}
|
|
724
1195
|
} catch {}
|
|
@@ -727,7 +1198,8 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
727
1198
|
}
|
|
728
1199
|
return {
|
|
729
1200
|
content,
|
|
730
|
-
thinking
|
|
1201
|
+
thinking,
|
|
1202
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
731
1203
|
};
|
|
732
1204
|
}
|
|
733
1205
|
async function callGemini(baseURL, advisor, apiKey, transcriptText, signal, timeoutMs) {
|
|
@@ -745,7 +1217,8 @@ async function callGemini(baseURL, advisor, apiKey, transcriptText, signal, time
|
|
|
745
1217
|
systemInstruction: { parts: [{ text: advisor.systemPrompt + ADVISOR_OUTPUT_POLICY }] },
|
|
746
1218
|
generationConfig: {
|
|
747
1219
|
temperature: advisor.temperature ?? .3,
|
|
748
|
-
maxOutputTokens: advisor.maxTokens ?? 16384
|
|
1220
|
+
maxOutputTokens: advisor.maxTokens ?? 16384,
|
|
1221
|
+
...geminiThinkingParams(advisor)
|
|
749
1222
|
}
|
|
750
1223
|
}),
|
|
751
1224
|
signal: withTimeout(timeoutMs ?? 12e4, signal)
|
|
@@ -764,7 +1237,7 @@ async function callGemini(baseURL, advisor, apiKey, transcriptText, signal, time
|
|
|
764
1237
|
* stalls. Any generation error degrades to the fallback text, never throws.
|
|
765
1238
|
*/
|
|
766
1239
|
const FALLBACK_DEEPEN_QUESTION = "请基于以上全部讨论,针对仍然存在的分歧、未验证的假设与主要风险,提出一个有深度的后续问题。";
|
|
767
|
-
const FALLBACK_CONCLUSION = "
|
|
1240
|
+
const FALLBACK_CONCLUSION = "(⚠ 驱动模型未能生成综合结论:请主模型以上述顾问讨论为基础,自行总结共识、分歧与最可靠的行动建议。)";
|
|
768
1241
|
const DRIVER_SYSTEM_PROMPT = `
|
|
769
1242
|
你是顾问群的主持人(驱动模型)。你的职责:
|
|
770
1243
|
1. 阅读项目背景、主模型问题与全部顾问回答;
|
|
@@ -795,7 +1268,7 @@ function transcriptOf(session) {
|
|
|
795
1268
|
return `[${message.role === "main" ? "主模型" : message.advisorName ?? message.advisorId ?? "顾问"}]\n${message.content}`;
|
|
796
1269
|
}).join("\n\n").slice(-24e3);
|
|
797
1270
|
}
|
|
798
|
-
async function generateWith(ctx, source, system, prompt, signal) {
|
|
1271
|
+
async function generateWith(ctx, source, sessionId, system, prompt, signal, timeoutMs = 6e5) {
|
|
799
1272
|
try {
|
|
800
1273
|
const options = {
|
|
801
1274
|
provider: source.provider,
|
|
@@ -806,31 +1279,56 @@ async function generateWith(ctx, source, system, prompt, signal) {
|
|
|
806
1279
|
type: "text",
|
|
807
1280
|
text: prompt
|
|
808
1281
|
}],
|
|
809
|
-
source: {
|
|
810
|
-
kind: "plugin",
|
|
811
|
-
plugin: "dsh-advisor-group"
|
|
812
|
-
}
|
|
1282
|
+
source: { kind: "user" }
|
|
813
1283
|
})],
|
|
814
1284
|
temperature: .7,
|
|
815
|
-
maxTokens:
|
|
816
|
-
signal: withTimeout(
|
|
1285
|
+
maxTokens: 2048,
|
|
1286
|
+
signal: withTimeout(timeoutMs, signal)
|
|
817
1287
|
};
|
|
818
1288
|
let text = "";
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1289
|
+
let chunks = 0;
|
|
1290
|
+
let textDeltas = 0;
|
|
1291
|
+
let reasoningDeltas = 0;
|
|
1292
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
1293
|
+
chunks += 1;
|
|
1294
|
+
if (chunk.type === "text-delta") {
|
|
1295
|
+
textDeltas += 1;
|
|
1296
|
+
text += chunk.text;
|
|
1297
|
+
} else reasoningDeltas += 1;
|
|
1298
|
+
}
|
|
1299
|
+
const trimmed = text.trim();
|
|
1300
|
+
if (!trimmed) console.warn(`[dsh-advisor-group] 驱动模型空响应(${source.provider}/${source.model}):`, sessionId, `chunks=${chunks} textDeltas=${textDeltas} reasoningDeltas=${reasoningDeltas};建议配置 discussion.driverModel 或提高 maxTokens`);
|
|
1301
|
+
return trimmed || void 0;
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
if (signal?.aborted) throw error;
|
|
1304
|
+
console.warn(`[dsh-advisor-group] 驱动模型生成失败(${source.provider}/${source.model}):`, sessionId, error instanceof Error ? error.message : String(error));
|
|
822
1305
|
return;
|
|
823
1306
|
}
|
|
824
1307
|
}
|
|
825
1308
|
/** One deepening question for the next round (never throws; returns fallback text). */
|
|
826
|
-
async function generateDeepenQuestion(ctx, session, source, signal) {
|
|
1309
|
+
async function generateDeepenQuestion(ctx, session, source, signal, timeoutMs) {
|
|
827
1310
|
if (!source) return FALLBACK_DEEPEN_QUESTION;
|
|
828
|
-
return await generateWith(ctx, source, DRIVER_SYSTEM_PROMPT, `以下是一轮顾问群的讨论记录:\n\n${transcriptOf(session)}\n\n请给出下一步的深入追问。`, signal) || "请基于以上全部讨论,针对仍然存在的分歧、未验证的假设与主要风险,提出一个有深度的后续问题。";
|
|
1311
|
+
return await generateWith(ctx, source, session.id, DRIVER_SYSTEM_PROMPT, `以下是一轮顾问群的讨论记录:\n\n${transcriptOf(session)}\n\n请给出下一步的深入追问。`, signal, timeoutMs) || "请基于以上全部讨论,针对仍然存在的分歧、未验证的假设与主要风险,提出一个有深度的后续问题。";
|
|
829
1312
|
}
|
|
830
1313
|
/** Final synthesis after all rounds (never throws; returns fallback text). */
|
|
831
|
-
async function generateConclusion(ctx, session, source, signal) {
|
|
1314
|
+
async function generateConclusion(ctx, session, source, signal, timeoutMs) {
|
|
832
1315
|
if (!source) return FALLBACK_CONCLUSION;
|
|
833
|
-
return await generateWith(ctx, source, CONCLUSION_SYSTEM_PROMPT, `以下是顾问群全部 ${session.messages.filter((m) => m.role === "advisor").length} 条顾问回答的讨论记录:\n\n${transcriptOf(session)}\n\n请给出综合结论。`, signal) || "
|
|
1316
|
+
return await generateWith(ctx, source, session.id, CONCLUSION_SYSTEM_PROMPT, `以下是顾问群全部 ${session.messages.filter((m) => m.role === "advisor").length} 条顾问回答的讨论记录:\n\n${transcriptOf(session)}\n\n请给出综合结论。`, signal, timeoutMs) || "(⚠ 驱动模型未能生成综合结论:请主模型以上述顾问讨论为基础,自行总结共识、分歧与最可靠的行动建议。)";
|
|
1317
|
+
}
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region src/risk-notes.ts
|
|
1320
|
+
const TRUNCATION_NOTE = "有顾问输出在流式过程中被截断(超时或网络中断),其正文可能不完整,且该顾问本轮可能只提供了部分意见。";
|
|
1321
|
+
const CANCEL_NOTE = "本次顾问群讨论已被用户或主模型取消,结论可能不完整。";
|
|
1322
|
+
/**
|
|
1323
|
+
* 汇总风险提示(两条规则:截断 / 取消)。
|
|
1324
|
+
* 判定只看消息上的截断标志与 cancelled 标志,**不读正文**——没有基于文本的猜测,
|
|
1325
|
+
* 因此不会误报。content 字段保留在入参类型里只为兼容既有调用方(本函数不再读它)。
|
|
1326
|
+
*/
|
|
1327
|
+
function buildRiskNotes(messages, cancelled) {
|
|
1328
|
+
const notes = [];
|
|
1329
|
+
if (messages.some((message) => message.truncated !== void 0)) notes.push(TRUNCATION_NOTE);
|
|
1330
|
+
if (cancelled) notes.push(CANCEL_NOTE);
|
|
1331
|
+
return notes;
|
|
834
1332
|
}
|
|
835
1333
|
//#endregion
|
|
836
1334
|
//#region src/session-log.ts
|
|
@@ -889,7 +1387,11 @@ function appendAdvisorMessage(log, sessionId, message) {
|
|
|
889
1387
|
...message.advisorId === void 0 ? {} : { advisorId: message.advisorId },
|
|
890
1388
|
...message.advisorName === void 0 ? {} : { advisorName: message.advisorName },
|
|
891
1389
|
...message.round === void 0 ? {} : { round: message.round },
|
|
892
|
-
...message.thinking === void 0 ? {} : { thinking: message.thinking }
|
|
1390
|
+
...message.thinking === void 0 ? {} : { thinking: message.thinking },
|
|
1391
|
+
...message.thinkingSegments === void 0 ? {} : { thinkingSegments: message.thinkingSegments },
|
|
1392
|
+
...message.actionDescriptions === void 0 ? {} : { actionDescriptions: message.actionDescriptions },
|
|
1393
|
+
...message.toolSteps === void 0 ? {} : { toolSteps: message.toolSteps },
|
|
1394
|
+
...message.truncated === void 0 ? {} : { truncated: message.truncated }
|
|
893
1395
|
};
|
|
894
1396
|
log.append("advisor-group/message", data);
|
|
895
1397
|
}
|
|
@@ -904,10 +1406,19 @@ function appendAdvisorDelta(log, sessionId, delta) {
|
|
|
904
1406
|
...delta.round === void 0 ? {} : { round: delta.round },
|
|
905
1407
|
...delta.contentDelta === void 0 ? {} : { contentDelta: delta.contentDelta },
|
|
906
1408
|
...delta.thinkingDelta === void 0 ? {} : { thinkingDelta: delta.thinkingDelta },
|
|
1409
|
+
...delta.toolStep === void 0 ? {} : { toolStep: delta.toolStep },
|
|
907
1410
|
...delta.done === void 0 ? {} : { done: delta.done }
|
|
908
1411
|
};
|
|
909
1412
|
log.append("advisor-group/delta", data);
|
|
910
1413
|
}
|
|
1414
|
+
function appendAdvisorResume(log, sessionId) {
|
|
1415
|
+
const { turn, step } = latestTurnStep(log);
|
|
1416
|
+
log.append("advisor-group/resume", {
|
|
1417
|
+
sessionId,
|
|
1418
|
+
turn,
|
|
1419
|
+
step
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
911
1422
|
function appendAdvisorEnd(log, session, summary) {
|
|
912
1423
|
const { turn, step } = latestTurnStep(log);
|
|
913
1424
|
const data = {
|
|
@@ -921,7 +1432,9 @@ function appendAdvisorEnd(log, session, summary) {
|
|
|
921
1432
|
advisorName,
|
|
922
1433
|
corePoints
|
|
923
1434
|
})),
|
|
924
|
-
...summary.riskNotes.length > 0 ? { riskNotes: summary.riskNotes } : {}
|
|
1435
|
+
...summary.riskNotes.length > 0 ? { riskNotes: summary.riskNotes } : {},
|
|
1436
|
+
...summary.stopped === void 0 ? {} : { stopped: summary.stopped },
|
|
1437
|
+
...summary.conclusion === void 0 ? {} : { conclusion: summary.conclusion }
|
|
925
1438
|
}
|
|
926
1439
|
};
|
|
927
1440
|
log.append("advisor-group/end", data);
|
|
@@ -998,12 +1511,212 @@ function publish(sessionId, delta) {
|
|
|
998
1511
|
for (const res of set) writeFrame(res, frame);
|
|
999
1512
|
}
|
|
1000
1513
|
//#endregion
|
|
1514
|
+
//#region src/advisor-tools.ts
|
|
1515
|
+
/** Raw tool-call / DSML markup that some providers echo as plain text instead
|
|
1516
|
+
* of a structured tool call. Observed shapes (deepseek-v4-pro, anthropic
|
|
1517
|
+
* channel):
|
|
1518
|
+
* - plain `<tool_calls>…</tool_calls>` (line-start block)
|
|
1519
|
+
* - piped `<||tool_calls>\n<||DSML|…` without a closing tag (U+FF5C)
|
|
1520
|
+
* - MID-LINE occurrence: `证据。<tool_calls><invoke name="bash">…` — the
|
|
1521
|
+
* marker is glued to prose, so line-start anchoring alone is NOT enough. */
|
|
1522
|
+
const RAW_TOOL_CALL_BLOCK = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]*?<\/(?:[||]{1,2})?(?:tool_calls|DSML)\s*>/g;
|
|
1523
|
+
/** Unclosed tail: cut from the first marker that is actually a STRUCTURED
|
|
1524
|
+
* block (followed by `<invoke`/`<parameter`/`<||DSML` within the next ~60
|
|
1525
|
+
* chars). An isolated prose/code mention like `` `<tool_calls>` `` is never
|
|
1526
|
+
* followed by those children, so it stays untouched. */
|
|
1527
|
+
const RAW_TOOL_CALL_TAIL = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]{0,60}?(?:<invoke|<parameter|<[||]{1,2}DSML)[\s\S]*$/g;
|
|
1528
|
+
/**
|
|
1529
|
+
* Strip raw `<tool_calls>` / piped `<||tool_calls>` / `<||DSML|` markup from
|
|
1530
|
+
* an advisor's final content (line-start OR mid-line, closed or unclosed).
|
|
1531
|
+
* Returns the cleaned text; when the content was only leaked tool markup, a
|
|
1532
|
+
* short notice replaces it so the bubble is never empty/raw (the thinking
|
|
1533
|
+
* panel and tool rows still carry the real substance).
|
|
1534
|
+
*/
|
|
1535
|
+
function sanitizeAdvisorContent(content) {
|
|
1536
|
+
if (!content.trim()) return content;
|
|
1537
|
+
return content.replace(RAW_TOOL_CALL_BLOCK, "").replace(RAW_TOOL_CALL_TAIL, "").trim() || "(顾问未生成正文:输出仅为工具调用描述,请参考其思维链与工具调用记录。)";
|
|
1538
|
+
}
|
|
1539
|
+
/** Conservative read-only whitelist for the default advisor tool scope. */
|
|
1540
|
+
const READONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1541
|
+
"read",
|
|
1542
|
+
"grep",
|
|
1543
|
+
"glob",
|
|
1544
|
+
"web_search",
|
|
1545
|
+
"web_fetch",
|
|
1546
|
+
"scan_discover",
|
|
1547
|
+
"list_imported_sessions"
|
|
1548
|
+
]);
|
|
1549
|
+
const MAX_TOOL_ROUNDS = 4;
|
|
1550
|
+
const TOOL_EXEC_TIMEOUT_MS = 3e4;
|
|
1551
|
+
const TOOL_RESULT_MAX_CHARS = 8e3;
|
|
1552
|
+
/**
|
|
1553
|
+
* Resolve the advisor-visible tool schemas. `agent` (when provided) makes the
|
|
1554
|
+
* registry resolve the scoped view — exactly the tools that session's Agent
|
|
1555
|
+
* loop serves, honouring presets and restrict() filters.
|
|
1556
|
+
*/
|
|
1557
|
+
function resolveAdvisorToolSchemas(ctx, agent, mode) {
|
|
1558
|
+
if (mode === "off") return [];
|
|
1559
|
+
const schemas = ctx.tools?.schemas?.(agent) ?? [];
|
|
1560
|
+
let list = schemas;
|
|
1561
|
+
if (mode === "readonly") list = schemas.filter((schema) => typeof schema === "object" && schema !== null && READONLY_TOOL_NAMES.has(schema.name));
|
|
1562
|
+
return list.map((schema) => {
|
|
1563
|
+
const { name, description, parameters } = schema;
|
|
1564
|
+
return {
|
|
1565
|
+
name,
|
|
1566
|
+
...typeof description === "string" ? { description } : {},
|
|
1567
|
+
...parameters === void 0 ? {} : { parameters }
|
|
1568
|
+
};
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
/** Human-readable one-line summary of a tool result (for the thinking panel). */
|
|
1572
|
+
function summarizeToolResult(result) {
|
|
1573
|
+
let text = "";
|
|
1574
|
+
if (result.isError) text = `(错误)${result.error?.message ?? "unknown"}`;
|
|
1575
|
+
else {
|
|
1576
|
+
text = (result.content ?? []).map((block) => typeof block === "object" && block !== null && "text" in block && typeof block.text === "string" ? block.text : "").join("").trim();
|
|
1577
|
+
if (!text && result.value !== void 0) try {
|
|
1578
|
+
text = JSON.stringify(result.value);
|
|
1579
|
+
} catch {
|
|
1580
|
+
text = String(result.value);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return text.length > TOOL_RESULT_MAX_CHARS ? `${text.slice(0, TOOL_RESULT_MAX_CHARS)}…(已截断)` : text;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Execute one tool via the official pipeline (scoped dispatch when `agent` is
|
|
1587
|
+
* provided). Returns the model-facing text; never throws.
|
|
1588
|
+
*/
|
|
1589
|
+
async function executeAdvisorTool(ctx, agent, call, signal) {
|
|
1590
|
+
const tools = ctx.tools;
|
|
1591
|
+
if (!tools?.execute) return "(顾问工具执行不可用:当前环境未提供工具执行服务)";
|
|
1592
|
+
let args = {};
|
|
1593
|
+
try {
|
|
1594
|
+
args = call.argumentsJson ? JSON.parse(call.argumentsJson) : {};
|
|
1595
|
+
} catch {
|
|
1596
|
+
return "(工具参数不是合法 JSON)";
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
return summarizeToolResult(await tools.execute({
|
|
1600
|
+
callId: call.id,
|
|
1601
|
+
name: call.name,
|
|
1602
|
+
arguments: args,
|
|
1603
|
+
...agent === void 0 ? {} : { agent },
|
|
1604
|
+
signal: withTimeout(TOOL_EXEC_TIMEOUT_MS, signal)
|
|
1605
|
+
}));
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
return `(工具执行失败:${error instanceof Error ? error.message : String(error)})`;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Drive the tool-calling loop: stream with `tools` → execute every
|
|
1612
|
+
* model-requested call (each step surfaced via `onToolStep` as its own row) →
|
|
1613
|
+
* feed a compact "already executed" block back → repeat until the model
|
|
1614
|
+
* answers (or MAX_TOOL_ROUNDS, then one final no-tools round to force a text
|
|
1615
|
+
* answer).
|
|
1616
|
+
*/
|
|
1617
|
+
async function runAdvisorToolLoop(tools, streamOnce, executeTool, onToolStep) {
|
|
1618
|
+
let extraContext = "";
|
|
1619
|
+
let truncated;
|
|
1620
|
+
const executedMemo = /* @__PURE__ */ new Map();
|
|
1621
|
+
const executeDeduped = async (call) => {
|
|
1622
|
+
const key = `${call.name}:${call.argumentsJson}`;
|
|
1623
|
+
const cached = executedMemo.get(key);
|
|
1624
|
+
if (cached !== void 0) {
|
|
1625
|
+
console.warn(`[dsh-advisor-group] 同参工具调用去重(复用已执行结果):${call.name}`);
|
|
1626
|
+
return {
|
|
1627
|
+
outcome: cached,
|
|
1628
|
+
reused: true
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const outcome = await executeTool(call);
|
|
1632
|
+
executedMemo.set(key, outcome);
|
|
1633
|
+
return {
|
|
1634
|
+
outcome,
|
|
1635
|
+
reused: false
|
|
1636
|
+
};
|
|
1637
|
+
};
|
|
1638
|
+
/** Result fed back to the model: capped so extraContext never balloons
|
|
1639
|
+
* (display rows keep their own 240-char preview). */
|
|
1640
|
+
const capOutcome = (outcome) => outcome.length > 800 ? `${outcome.slice(0, 800)}…` : outcome;
|
|
1641
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
1642
|
+
const result = await streamOnce(tools, extraContext);
|
|
1643
|
+
if (result.toolCalls.length === 0) return {
|
|
1644
|
+
content: result.content,
|
|
1645
|
+
truncated: result.truncated
|
|
1646
|
+
};
|
|
1647
|
+
truncated = result.truncated ?? truncated;
|
|
1648
|
+
const executed = [];
|
|
1649
|
+
for (const call of result.toolCalls) {
|
|
1650
|
+
const argsPreview = call.argumentsJson.length > 120 ? `${call.argumentsJson.slice(0, 120)}…` : call.argumentsJson;
|
|
1651
|
+
onToolStep({
|
|
1652
|
+
kind: "call",
|
|
1653
|
+
name: call.name,
|
|
1654
|
+
text: argsPreview || "无参数"
|
|
1655
|
+
});
|
|
1656
|
+
if (!READONLY_TOOL_NAMES.has(call.name)) console.warn(`[dsh-advisor-group] 顾问调用非只读工具(${call.name}):`, argsPreview);
|
|
1657
|
+
const { outcome, reused } = await executeDeduped(call);
|
|
1658
|
+
if (outcome.trim()) {
|
|
1659
|
+
const flat = outcome.replace(/\n+/g, " ").trim();
|
|
1660
|
+
const preview = flat.length > 240 ? `${flat.slice(0, 240)}…` : flat;
|
|
1661
|
+
onToolStep({
|
|
1662
|
+
kind: "result",
|
|
1663
|
+
name: call.name,
|
|
1664
|
+
text: `${reused ? "(复用已执行结果)" : ""}${preview}`
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
executed.push(`- ${call.name}${call.argumentsJson ? ` 参数:${call.argumentsJson.slice(0, 200)}` : ""}\n结果:${capOutcome(outcome)}`);
|
|
1668
|
+
}
|
|
1669
|
+
extraContext = `${extraContext}\n\n【顾问已执行工具】\n${executed.join("\n\n")}`;
|
|
1670
|
+
}
|
|
1671
|
+
const final = await streamOnce([], extraContext);
|
|
1672
|
+
if (final.toolCalls.length > 0) {
|
|
1673
|
+
const executed = [];
|
|
1674
|
+
for (const call of final.toolCalls) {
|
|
1675
|
+
const argsPreview = call.argumentsJson.length > 120 ? `${call.argumentsJson.slice(0, 120)}…` : call.argumentsJson;
|
|
1676
|
+
onToolStep({
|
|
1677
|
+
kind: "call",
|
|
1678
|
+
name: call.name,
|
|
1679
|
+
text: argsPreview || "无参数"
|
|
1680
|
+
});
|
|
1681
|
+
if (!READONLY_TOOL_NAMES.has(call.name)) console.warn(`[dsh-advisor-group] 顾问调用非只读工具(${call.name}):`, argsPreview);
|
|
1682
|
+
const { outcome, reused } = await executeDeduped(call);
|
|
1683
|
+
if (outcome.trim()) {
|
|
1684
|
+
const flat = outcome.replace(/\n+/g, " ").trim();
|
|
1685
|
+
const preview = flat.length > 240 ? `${flat.slice(0, 240)}…` : flat;
|
|
1686
|
+
onToolStep({
|
|
1687
|
+
kind: "result",
|
|
1688
|
+
name: call.name,
|
|
1689
|
+
text: `${reused ? "(复用已执行结果)" : ""}${preview}`
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
executed.push(`- ${call.name}${call.argumentsJson ? ` 参数:${call.argumentsJson.slice(0, 200)}` : ""}\n结果:${capOutcome(outcome)}`);
|
|
1693
|
+
}
|
|
1694
|
+
const final2 = await streamOnce([], `${extraContext}\n\n【顾问已执行工具】\n${executed.join("\n\n")}`);
|
|
1695
|
+
return {
|
|
1696
|
+
content: final2.content,
|
|
1697
|
+
truncated: final2.truncated ?? final.truncated ?? truncated
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
content: final.content,
|
|
1702
|
+
truncated: final.truncated ?? truncated
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
//#endregion
|
|
1001
1706
|
//#region src/service.ts
|
|
1002
1707
|
function isAbortError(error, signal) {
|
|
1003
1708
|
if (signal?.aborted) return true;
|
|
1004
1709
|
return error instanceof Error && error.name === "AbortError";
|
|
1005
1710
|
}
|
|
1006
|
-
const
|
|
1711
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1712
|
+
/** Real consult ids are server-minted uuids; anything else is rejected. */
|
|
1713
|
+
function isSafeConsultId(id) {
|
|
1714
|
+
return UUID_RE.test(id);
|
|
1715
|
+
}
|
|
1716
|
+
/** Agent DSH session ids look like `session-<uuid>` or a plain uuid. */
|
|
1717
|
+
function isSafeDshSessionId(id) {
|
|
1718
|
+
return UUID_RE.test(id) || UUID_RE.test(id.replace(/^session-/i, ""));
|
|
1719
|
+
}
|
|
1007
1720
|
function extractUserMessageText(event) {
|
|
1008
1721
|
if (event.type !== "user/message") return null;
|
|
1009
1722
|
const data = event.data;
|
|
@@ -1021,6 +1734,11 @@ var AdvisorGroupService = class {
|
|
|
1021
1734
|
/** Durable daily-guard counter fixture under the DSH home storage area. */
|
|
1022
1735
|
dailyGuardPath;
|
|
1023
1736
|
dailyWriteChain = Promise.resolve();
|
|
1737
|
+
/** Per-session stop controllers for the auto-deepen pipeline. */
|
|
1738
|
+
activeAborts = /* @__PURE__ */ new Map();
|
|
1739
|
+
/** Durable consultation snapshots (usable after a dsh restart). */
|
|
1740
|
+
sessionsDir;
|
|
1741
|
+
sessionWriteChain = Promise.resolve();
|
|
1024
1742
|
enabled;
|
|
1025
1743
|
config;
|
|
1026
1744
|
constructor(ctx, config) {
|
|
@@ -1029,7 +1747,9 @@ var AdvisorGroupService = class {
|
|
|
1029
1747
|
this.enabled = config.enabled;
|
|
1030
1748
|
const dshHome = process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
1031
1749
|
this.dailyGuardPath = join(dshHome, "storages", "advisor-group", "daily-guard.json");
|
|
1750
|
+
this.sessionsDir = join(dshHome, "storages", "advisor-group", "sessions");
|
|
1032
1751
|
this.loadDailyGuard();
|
|
1752
|
+
this.loadStoredSessions();
|
|
1033
1753
|
ctx.on("session/event", (session, event) => {
|
|
1034
1754
|
const text = extractUserMessageText(event);
|
|
1035
1755
|
if (!text) return;
|
|
@@ -1065,6 +1785,228 @@ var AdvisorGroupService = class {
|
|
|
1065
1785
|
console.warn("[dsh-advisor-group] 每日护栏持久化失败:", error instanceof Error ? error.message : String(error));
|
|
1066
1786
|
});
|
|
1067
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Restore interrupted consultations from durable snapshots so the card's
|
|
1790
|
+
* 「▶ 继续聊天」 button still works after a dsh restart. Completed sessions
|
|
1791
|
+
* are not restored (they cannot resume) and their snapshot is cleaned up.
|
|
1792
|
+
*/
|
|
1793
|
+
loadStoredSessions() {
|
|
1794
|
+
let files;
|
|
1795
|
+
try {
|
|
1796
|
+
files = readdirSync(this.sessionsDir).filter((name) => name.endsWith(".json"));
|
|
1797
|
+
} catch {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
for (const file of files) try {
|
|
1801
|
+
const raw = JSON.parse(readFileSync(join(this.sessionsDir, file), "utf8"));
|
|
1802
|
+
if (raw.version !== 1 || typeof raw.id !== "string" || typeof raw.question !== "string") continue;
|
|
1803
|
+
if (!isSafeConsultId(raw.id)) continue;
|
|
1804
|
+
if (raw.dshSessionId !== void 0 && !isSafeDshSessionId(raw.dshSessionId)) continue;
|
|
1805
|
+
if (raw.status === "completed") {
|
|
1806
|
+
unlinkSync(join(this.sessionsDir, file));
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
const advisors = this.resolveAdvisors(raw.advisorIds);
|
|
1810
|
+
if (advisors.length === 0) continue;
|
|
1811
|
+
const session = {
|
|
1812
|
+
id: raw.id,
|
|
1813
|
+
status: "cancelled",
|
|
1814
|
+
question: raw.question,
|
|
1815
|
+
context: raw.context,
|
|
1816
|
+
advisors,
|
|
1817
|
+
maxRounds: raw.maxRounds,
|
|
1818
|
+
messages: raw.messages.map((message) => ({
|
|
1819
|
+
role: message.role,
|
|
1820
|
+
advisorId: message.advisorId,
|
|
1821
|
+
advisorName: message.advisorName,
|
|
1822
|
+
content: message.content,
|
|
1823
|
+
thinking: message.thinking,
|
|
1824
|
+
thinkingSegments: message.thinkingSegments,
|
|
1825
|
+
actionDescriptions: message.actionDescriptions,
|
|
1826
|
+
round: message.round,
|
|
1827
|
+
truncated: message.truncated,
|
|
1828
|
+
toolSteps: message.toolSteps,
|
|
1829
|
+
ts: message.ts
|
|
1830
|
+
})),
|
|
1831
|
+
cwd: raw.cwd,
|
|
1832
|
+
dshSessionId: raw.dshSessionId,
|
|
1833
|
+
driverSource: raw.driverSource,
|
|
1834
|
+
stopReason: raw.stopReason ?? "abort-error",
|
|
1835
|
+
createdAt: raw.createdAt,
|
|
1836
|
+
updatedAt: raw.updatedAt
|
|
1837
|
+
};
|
|
1838
|
+
this.sessions.set(session.id, session);
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
console.warn("[dsh-advisor-group] 会话快照读取失败,已跳过:", file, error instanceof Error ? error.message : String(error));
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
/** Serialization-safe snapshot: advisor identity only, NEVER credentials. */
|
|
1844
|
+
snapshotPayload(session) {
|
|
1845
|
+
return {
|
|
1846
|
+
version: 1,
|
|
1847
|
+
id: session.id,
|
|
1848
|
+
status: session.status,
|
|
1849
|
+
question: session.question,
|
|
1850
|
+
...session.context === void 0 ? {} : { context: session.context },
|
|
1851
|
+
...session.cwd === void 0 ? {} : { cwd: session.cwd },
|
|
1852
|
+
...session.dshSessionId === void 0 ? {} : { dshSessionId: session.dshSessionId },
|
|
1853
|
+
...session.driverSource === void 0 ? {} : { driverSource: session.driverSource },
|
|
1854
|
+
...session.stopReason === void 0 ? {} : { stopReason: session.stopReason },
|
|
1855
|
+
advisorIds: session.advisors.map((advisor) => advisor.id),
|
|
1856
|
+
maxRounds: session.maxRounds,
|
|
1857
|
+
createdAt: session.createdAt,
|
|
1858
|
+
updatedAt: session.updatedAt,
|
|
1859
|
+
messages: session.messages.map((message) => ({
|
|
1860
|
+
role: message.role,
|
|
1861
|
+
...message.advisorId === void 0 ? {} : { advisorId: message.advisorId },
|
|
1862
|
+
...message.advisorName === void 0 ? {} : { advisorName: message.advisorName },
|
|
1863
|
+
content: message.content,
|
|
1864
|
+
...message.thinking === void 0 ? {} : { thinking: message.thinking },
|
|
1865
|
+
...message.thinkingSegments === void 0 ? {} : { thinkingSegments: message.thinkingSegments },
|
|
1866
|
+
...message.actionDescriptions === void 0 ? {} : { actionDescriptions: message.actionDescriptions },
|
|
1867
|
+
...message.round === void 0 ? {} : { round: message.round },
|
|
1868
|
+
...message.truncated === void 0 ? {} : { truncated: message.truncated },
|
|
1869
|
+
...message.toolSteps === void 0 ? {} : { toolSteps: message.toolSteps },
|
|
1870
|
+
ts: message.ts
|
|
1871
|
+
}))
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
/** Serialized, atomic (tmp + rename) consultation snapshot persistence. */
|
|
1875
|
+
persistSession(session) {
|
|
1876
|
+
if (!isSafeConsultId(session.id)) {
|
|
1877
|
+
console.warn("[dsh-advisor-group] 拒绝持久化非法会话 id:", session.id);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const payload = JSON.stringify(this.snapshotPayload(session));
|
|
1881
|
+
const target = join(this.sessionsDir, `${session.id}.json`);
|
|
1882
|
+
this.sessionWriteChain = this.sessionWriteChain.then(async () => {
|
|
1883
|
+
await promises.mkdir(this.sessionsDir, { recursive: true });
|
|
1884
|
+
const tmp = `${target}.tmp`;
|
|
1885
|
+
await promises.writeFile(tmp, payload, "utf8");
|
|
1886
|
+
await promises.rename(tmp, target);
|
|
1887
|
+
}).catch((error) => {
|
|
1888
|
+
console.warn("[dsh-advisor-group] 咨询快照持久化失败:", session.id, error instanceof Error ? error.message : String(error));
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Card-facing advisor failure text: credentials redacted, collapsed to one
|
|
1893
|
+
* line, length-capped. The full raw error is logged server-side instead.
|
|
1894
|
+
*/
|
|
1895
|
+
summarizeAdvisorError(raw) {
|
|
1896
|
+
const singleLine = raw.replace(/\b(api[_-]?key["']?\s*[:=]\s*["']?)[^\s"',}&]+/gi, "$1[已隐藏]").replace(/([?&]key=)[^\s&"']+/gi, "$1[已隐藏]").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [已隐藏]").replace(/\bsk-[A-Za-z0-9_-]{8,}/g, "[已隐藏]").replace(/\s+/g, " ").trim();
|
|
1897
|
+
if (singleLine.length <= 300) return singleLine;
|
|
1898
|
+
return `${singleLine.slice(0, 300)}…(完整错误已记录到服务端日志)`;
|
|
1899
|
+
}
|
|
1900
|
+
/** Wait for queued durable writes before a host or test tears down storage. */
|
|
1901
|
+
async flushPersistence() {
|
|
1902
|
+
await Promise.all([this.dailyWriteChain, this.sessionWriteChain]);
|
|
1903
|
+
}
|
|
1904
|
+
/** Structural view of the DSH in-memory session store (may be absent). */
|
|
1905
|
+
get sessionStore() {
|
|
1906
|
+
let store;
|
|
1907
|
+
try {
|
|
1908
|
+
store = this.ctx.sessions;
|
|
1909
|
+
} catch {
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (!store || typeof store !== "object") return void 0;
|
|
1913
|
+
const typed = store;
|
|
1914
|
+
if (typeof typed.get !== "function" || typeof typed.prepare !== "function" || typeof typed.enter !== "function" || typeof typed.flush !== "function") return;
|
|
1915
|
+
return store;
|
|
1916
|
+
}
|
|
1917
|
+
/** Best-effort durability barrier for the advisor-group log events. */
|
|
1918
|
+
async flushLog(sessionLog) {
|
|
1919
|
+
if (!sessionLog) return;
|
|
1920
|
+
const store = this.sessionStore;
|
|
1921
|
+
if (!store) return;
|
|
1922
|
+
try {
|
|
1923
|
+
await store.flush(sessionLog);
|
|
1924
|
+
} catch {}
|
|
1925
|
+
}
|
|
1926
|
+
/** Build a contiguous seed log from the snapshot (cross-restart resume). */
|
|
1927
|
+
buildSeedEvents(session) {
|
|
1928
|
+
const events = [];
|
|
1929
|
+
let seq = 0;
|
|
1930
|
+
events.push({
|
|
1931
|
+
type: "advisor-group/start",
|
|
1932
|
+
seq: seq++,
|
|
1933
|
+
time: session.createdAt,
|
|
1934
|
+
data: {
|
|
1935
|
+
sessionId: session.id,
|
|
1936
|
+
turn: 0,
|
|
1937
|
+
step: 0,
|
|
1938
|
+
question: session.question,
|
|
1939
|
+
...session.context === void 0 ? {} : { context: session.context },
|
|
1940
|
+
advisors: session.advisors.map(({ id, name, avatar }) => avatar === void 0 ? {
|
|
1941
|
+
id,
|
|
1942
|
+
name
|
|
1943
|
+
} : {
|
|
1944
|
+
id,
|
|
1945
|
+
name,
|
|
1946
|
+
avatar
|
|
1947
|
+
})
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
for (const message of session.messages) {
|
|
1951
|
+
if (message.role === "system") continue;
|
|
1952
|
+
events.push({
|
|
1953
|
+
type: "advisor-group/message",
|
|
1954
|
+
seq: seq++,
|
|
1955
|
+
time: message.ts || Date.now(),
|
|
1956
|
+
data: {
|
|
1957
|
+
sessionId: session.id,
|
|
1958
|
+
turn: 0,
|
|
1959
|
+
step: 0,
|
|
1960
|
+
role: message.role,
|
|
1961
|
+
content: message.content,
|
|
1962
|
+
...message.advisorId === void 0 ? {} : { advisorId: message.advisorId },
|
|
1963
|
+
...message.advisorName === void 0 ? {} : { advisorName: message.advisorName },
|
|
1964
|
+
...message.thinking === void 0 ? {} : { thinking: message.thinking },
|
|
1965
|
+
...message.round === void 0 ? {} : { round: message.round },
|
|
1966
|
+
...message.truncated === void 0 ? {} : { truncated: message.truncated }
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
return events;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Obtain the DSH Session handle for durable log events. Prefers the live
|
|
1974
|
+
* store entry (same process). After a restart the store is empty, so the
|
|
1975
|
+
* session is rebuilt with `prepare` + `enter` (append hooks publish events
|
|
1976
|
+
* and the persistence layer picks them up; the session is NOT announced, so
|
|
1977
|
+
* no agent lifecycle reactivation is triggered). Falls back to a detached
|
|
1978
|
+
* `Session.create` (live SSE + snapshot only, no durable log) when neither
|
|
1979
|
+
* works.
|
|
1980
|
+
*/
|
|
1981
|
+
resolveSessionHandle(session) {
|
|
1982
|
+
const store = this.sessionStore;
|
|
1983
|
+
const resumeId = session.dshSessionId !== void 0 && isSafeDshSessionId(session.dshSessionId) ? session.dshSessionId : isSafeConsultId(session.id) ? session.id : "";
|
|
1984
|
+
if (!resumeId) return void 0;
|
|
1985
|
+
const live = store?.get(resumeId);
|
|
1986
|
+
if (live) return live;
|
|
1987
|
+
if (!store) return this.buildDetachedSession(session, resumeId);
|
|
1988
|
+
try {
|
|
1989
|
+
const meta = { cwd: session.cwd !== void 0 && isAbsolute(session.cwd) ? session.cwd : process.cwd() };
|
|
1990
|
+
const prepared = store.prepare(resumeId, {
|
|
1991
|
+
seed: this.buildSeedEvents(session),
|
|
1992
|
+
meta
|
|
1993
|
+
});
|
|
1994
|
+
store.enter(prepared);
|
|
1995
|
+
return prepared;
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
console.warn("[dsh-advisor-group] 重建会话句柄失败(降级为 detached):", session.id, error instanceof Error ? error.message : String(error));
|
|
1998
|
+
const again = store.get(resumeId);
|
|
1999
|
+
if (again) return again;
|
|
2000
|
+
return this.buildDetachedSession(session, resumeId);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
buildDetachedSession(session, sessionId = session.id) {
|
|
2004
|
+
try {
|
|
2005
|
+
return Session.create(sessionId, this.buildSeedEvents(session), void 0, 0);
|
|
2006
|
+
} catch {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1068
2010
|
getRepeatPressure(sessionId) {
|
|
1069
2011
|
if (!sessionId) return void 0;
|
|
1070
2012
|
const tracked = this.repeatTracker.get(sessionId);
|
|
@@ -1098,26 +2040,35 @@ var AdvisorGroupService = class {
|
|
|
1098
2040
|
this.setEnabled(enabled);
|
|
1099
2041
|
if (this.persistEnabledCallback) try {
|
|
1100
2042
|
await this.persistEnabledCallback(enabled);
|
|
1101
|
-
} catch {
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
console.warn("[dsh-advisor-group] 启用开关保存失败(内存态未回滚):", error instanceof Error ? error.message : String(error));
|
|
2045
|
+
}
|
|
1102
2046
|
return this.enabled;
|
|
1103
2047
|
}
|
|
1104
2048
|
/**
|
|
1105
2049
|
* Current daily-guard facts for display (does NOT increment — read-only).
|
|
1106
2050
|
* Rolls the in-memory counter to a new UTC day view without persisting.
|
|
2051
|
+
* `enabled` reflects the configurable cap; `remaining` is -1 when the cap
|
|
2052
|
+
* is disabled (the counter still accumulates for visibility).
|
|
1107
2053
|
*/
|
|
1108
2054
|
getDailyGuard() {
|
|
1109
2055
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1110
2056
|
const used = this.dailyConsultationDate === today ? this.dailyConsultationCount : 0;
|
|
2057
|
+
const enabled = this.config.quota.enabled;
|
|
2058
|
+
const limit = this.config.quota.maxPerDay;
|
|
1111
2059
|
return {
|
|
1112
2060
|
used,
|
|
1113
|
-
|
|
1114
|
-
|
|
2061
|
+
enabled,
|
|
2062
|
+
limit,
|
|
2063
|
+
remaining: enabled ? Math.max(0, limit - used) : -1
|
|
1115
2064
|
};
|
|
1116
2065
|
}
|
|
1117
2066
|
/**
|
|
1118
2067
|
* Minimal cost guard: atomically check and increment the per-day new
|
|
1119
2068
|
* consultation counter. Node is single-threaded, so doing both inside one
|
|
1120
2069
|
* synchronous block closes the TOCTOU window a two-step check would leave.
|
|
2070
|
+
* The cap is configurable (`quota.enabled` / `quota.maxPerDay`); when the
|
|
2071
|
+
* cap is disabled the counter still increments (used for display only).
|
|
1121
2072
|
* The counter is persisted (UTC day key) so a harness restart does not
|
|
1122
2073
|
* reset the quota; persistence is fire-and-forget after the sync block.
|
|
1123
2074
|
*/
|
|
@@ -1127,9 +2078,9 @@ var AdvisorGroupService = class {
|
|
|
1127
2078
|
this.dailyConsultationDate = today;
|
|
1128
2079
|
this.dailyConsultationCount = 0;
|
|
1129
2080
|
}
|
|
1130
|
-
if (this.dailyConsultationCount >=
|
|
2081
|
+
if (this.config.quota.enabled && this.dailyConsultationCount >= this.config.quota.maxPerDay) return {
|
|
1131
2082
|
ok: false,
|
|
1132
|
-
reason: "
|
|
2083
|
+
reason: "今日顾问咨询次数已达上限,请明天再试,或在设置中调整每日咨询上限。"
|
|
1133
2084
|
};
|
|
1134
2085
|
this.dailyConsultationCount += 1;
|
|
1135
2086
|
this.persistDailyGuard();
|
|
@@ -1138,9 +2089,10 @@ var AdvisorGroupService = class {
|
|
|
1138
2089
|
getSession(id) {
|
|
1139
2090
|
return this.sessions.get(id);
|
|
1140
2091
|
}
|
|
1141
|
-
createSession(question, context, advisorIds = []) {
|
|
2092
|
+
createSession(question, context, advisorIds = [], cwd, dshSessionId) {
|
|
1142
2093
|
const maxAdvisors = this.config.discussion.maxAdvisorsPerCall;
|
|
1143
2094
|
const advisors = this.resolveAdvisors(advisorIds).slice(0, maxAdvisors);
|
|
2095
|
+
const now = Date.now();
|
|
1144
2096
|
const session = {
|
|
1145
2097
|
id: randomUUID(),
|
|
1146
2098
|
status: "active",
|
|
@@ -1151,20 +2103,23 @@ var AdvisorGroupService = class {
|
|
|
1151
2103
|
messages: [{
|
|
1152
2104
|
role: "system",
|
|
1153
2105
|
content: `顾问群已建立,共 ${advisors.length} 位顾问。`,
|
|
1154
|
-
ts:
|
|
2106
|
+
ts: now
|
|
1155
2107
|
}, {
|
|
1156
2108
|
role: "main",
|
|
1157
2109
|
content: context ? `${context}\n\n${question}` : question,
|
|
1158
|
-
ts:
|
|
2110
|
+
ts: now
|
|
1159
2111
|
}],
|
|
1160
|
-
|
|
1161
|
-
|
|
2112
|
+
cwd,
|
|
2113
|
+
dshSessionId,
|
|
2114
|
+
createdAt: now,
|
|
2115
|
+
updatedAt: now
|
|
1162
2116
|
};
|
|
1163
2117
|
if (this.sessions.size >= 100) {
|
|
1164
2118
|
const oldest = this.sessions.keys().next().value;
|
|
1165
2119
|
if (oldest !== void 0) this.sessions.delete(oldest);
|
|
1166
2120
|
}
|
|
1167
2121
|
this.sessions.set(session.id, session);
|
|
2122
|
+
this.persistSession(session);
|
|
1168
2123
|
this.ctx.emit("advisor-group/session-start", session);
|
|
1169
2124
|
return session;
|
|
1170
2125
|
}
|
|
@@ -1179,22 +2134,60 @@ var AdvisorGroupService = class {
|
|
|
1179
2134
|
session.messages.push(message);
|
|
1180
2135
|
session.updatedAt = Date.now();
|
|
1181
2136
|
if (sessionLog) appendAdvisorMessage(sessionLog, session.id, message);
|
|
2137
|
+
this.persistSession(session);
|
|
1182
2138
|
this.ctx.emit("advisor-group/message", {
|
|
1183
2139
|
sessionId: session.id,
|
|
1184
2140
|
message
|
|
1185
2141
|
});
|
|
1186
2142
|
return session;
|
|
1187
2143
|
}
|
|
1188
|
-
|
|
2144
|
+
/**
|
|
2145
|
+
* The next round that still needs work, derived from the durable messages:
|
|
2146
|
+
* when the last round is incomplete (an advisor answered, a later one was
|
|
2147
|
+
* cut by a stop), that round resumes with ONLY the missing advisors; when
|
|
2148
|
+
* it is complete, the next round is fresh. `undefined` means finished.
|
|
2149
|
+
*/
|
|
2150
|
+
nextPendingRound(session) {
|
|
2151
|
+
const advisorMessages = session.messages.filter((message) => message.role === "advisor");
|
|
2152
|
+
const maxRound = advisorMessages.reduce((max, message) => Math.max(max, message.round ?? 1), 0);
|
|
1189
2153
|
const advisorCount = Math.max(1, session.advisors.length);
|
|
1190
|
-
|
|
2154
|
+
if (maxRound === 0) return {
|
|
2155
|
+
round: 1,
|
|
2156
|
+
answeredIds: []
|
|
2157
|
+
};
|
|
2158
|
+
if (maxRound > session.maxRounds) return void 0;
|
|
2159
|
+
const answeredInMax = advisorMessages.filter((message) => (message.round ?? 1) === maxRound).map((message) => message.advisorId ?? "").filter((id) => id !== "");
|
|
2160
|
+
const unique = [...new Set(answeredInMax)];
|
|
2161
|
+
if (unique.length < advisorCount) return {
|
|
2162
|
+
round: maxRound,
|
|
2163
|
+
answeredIds: unique
|
|
2164
|
+
};
|
|
2165
|
+
if (maxRound + 1 > session.maxRounds) return void 0;
|
|
2166
|
+
return {
|
|
2167
|
+
round: maxRound + 1,
|
|
2168
|
+
answeredIds: []
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
/** Number of main-model messages: the opening question + all deep-questions. */
|
|
2172
|
+
mainMessageCount(session) {
|
|
2173
|
+
return session.messages.filter((message) => message.role === "main").length;
|
|
2174
|
+
}
|
|
2175
|
+
/** Run exactly one round: every advisor that has NOT answered this round. */
|
|
2176
|
+
async runRoundFrom(session, round, answeredIds, signal, sessionLog, agent) {
|
|
1191
2177
|
for (let index = 0; index < session.advisors.length; index++) {
|
|
1192
2178
|
const advisor = session.advisors[index];
|
|
2179
|
+
if (answeredIds.includes(advisor.id)) continue;
|
|
1193
2180
|
const transcript = this.buildTranscript(session);
|
|
1194
|
-
await this.callAdvisor(session, advisor, transcript, round, index + 1, signal, sessionLog);
|
|
2181
|
+
await this.callAdvisor(session, advisor, transcript, round, index + 1, signal, sessionLog, agent);
|
|
1195
2182
|
}
|
|
1196
2183
|
session.status = "active";
|
|
1197
2184
|
session.updatedAt = Date.now();
|
|
2185
|
+
this.persistSession(session);
|
|
2186
|
+
}
|
|
2187
|
+
async runOneRound(session, signal, sessionLog, agent) {
|
|
2188
|
+
const pending = this.nextPendingRound(session);
|
|
2189
|
+
if (!pending) return;
|
|
2190
|
+
await this.runRoundFrom(session, pending.round, pending.answeredIds, signal, sessionLog, agent);
|
|
1198
2191
|
}
|
|
1199
2192
|
/**
|
|
1200
2193
|
* Auto-deepen pipeline (2026-09-05): one ask_advisors call runs the whole
|
|
@@ -1202,21 +2195,159 @@ var AdvisorGroupService = class {
|
|
|
1202
2195
|
* [driver deep-question (after the first)] → advisor A → advisor B (sees A)
|
|
1203
2196
|
* → advisor C (sees A+B) → …
|
|
1204
2197
|
* and closes with a driver-generated conclusion. The pipeline is synchronous
|
|
1205
|
-
* (SSE keeps the card streaming); `exec.signal` aborts the whole run
|
|
2198
|
+
* (SSE keeps the card streaming); `exec.signal` aborts the whole run and the
|
|
2199
|
+
* user may stop it explicitly via `stopConsultation(sessionId)` (POST
|
|
2200
|
+
* /advisor-group/stop) — a stop degrades to a graceful partial summary.
|
|
2201
|
+
*
|
|
2202
|
+
* Resumption: after a stop (or a dsh restart that restored the snapshot)
|
|
2203
|
+
* `resumeConsultation()` re-enters the SAME pipeline; `nextPendingRound()`
|
|
2204
|
+
* detects the interruption point — a partially-answered round is completed
|
|
2205
|
+
* with only the missing advisors (the deep-question is NOT regenerated for
|
|
2206
|
+
* that round), then remaining rounds run as usual.
|
|
1206
2207
|
*/
|
|
1207
|
-
async runAutoPipeline(session, signal, sessionLog) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
2208
|
+
async runAutoPipeline(session, signal, sessionLog, agent) {
|
|
2209
|
+
const stop = new AbortController();
|
|
2210
|
+
this.activeAborts.set(session.id, stop);
|
|
2211
|
+
const combined = signal ? AbortSignal.any([signal, stop.signal]) : stop.signal;
|
|
2212
|
+
if (!session.driverSource) {
|
|
2213
|
+
const source = resolveDriverSource(sessionLog, this.config.discussion.driverModel);
|
|
2214
|
+
if (source) {
|
|
2215
|
+
session.driverSource = source;
|
|
2216
|
+
this.persistSession(session);
|
|
1212
2217
|
}
|
|
1213
|
-
await this.runOneRound(session, signal, sessionLog);
|
|
1214
2218
|
}
|
|
1215
|
-
|
|
2219
|
+
try {
|
|
2220
|
+
while (true) {
|
|
2221
|
+
const pending = this.nextPendingRound(session);
|
|
2222
|
+
if (!pending) break;
|
|
2223
|
+
const freshRound = pending.answeredIds.length === 0;
|
|
2224
|
+
const questionAlreadyAsked = this.mainMessageCount(session) >= pending.round;
|
|
2225
|
+
if (pending.round > 1 && freshRound && this.config.discussion.autoDeepen && !questionAlreadyAsked) {
|
|
2226
|
+
const question = await generateDeepenQuestion(this.ctx, session, session.driverSource ?? resolveDriverSource(sessionLog, this.config.discussion.driverModel), combined, this.config.discussion.driverTimeoutMs);
|
|
2227
|
+
this.appendMainMessage(session, question, sessionLog);
|
|
2228
|
+
}
|
|
2229
|
+
await this.runRoundFrom(session, pending.round, pending.answeredIds, combined, sessionLog, agent);
|
|
2230
|
+
}
|
|
2231
|
+
session.status = "completed";
|
|
2232
|
+
session.stopReason = void 0;
|
|
2233
|
+
const summary = await this.generateFinalSummary(session, combined, sessionLog, false);
|
|
2234
|
+
this.persistSession(session);
|
|
2235
|
+
await this.flushLog(sessionLog);
|
|
2236
|
+
return summary;
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
if (isAbortError(error, combined) || stop.signal.aborted) {
|
|
2239
|
+
if (stop.signal.aborted) session.stopReason = "user-stop";
|
|
2240
|
+
else if (signal?.aborted) session.stopReason = "exec-cancel";
|
|
2241
|
+
else session.stopReason = "abort-error";
|
|
2242
|
+
session.status = "cancelled";
|
|
2243
|
+
this.persistSession(session);
|
|
2244
|
+
const summary = await this.generateFinalSummary(session, void 0, sessionLog, true);
|
|
2245
|
+
await this.flushLog(sessionLog);
|
|
2246
|
+
return summary;
|
|
2247
|
+
}
|
|
2248
|
+
throw error;
|
|
2249
|
+
} finally {
|
|
2250
|
+
this.activeAborts.delete(session.id);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* User-initiated stop: aborts the in-flight consultation pipeline. Returns
|
|
2255
|
+
* whether a running consultation was found and aborted.
|
|
2256
|
+
*/
|
|
2257
|
+
stopConsultation(sessionId) {
|
|
2258
|
+
const stop = this.activeAborts.get(sessionId);
|
|
2259
|
+
if (!stop) return false;
|
|
2260
|
+
stop.abort();
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
/** True while a pipeline run (auto-deepen or a follow-up round) is in flight. */
|
|
2264
|
+
isSessionRunning(sessionId) {
|
|
2265
|
+
return this.activeAborts.has(sessionId);
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Resume a stopped/interrupted consultation from its interruption point.
|
|
2269
|
+
* Works for a user stop (live session) and after a dsh restart (snapshot
|
|
2270
|
+
* restored; the DSH session handle is rebuilt via `prepare` + `enter`).
|
|
2271
|
+
* Returns immediately; the pipeline runs in the background and streams over
|
|
2272
|
+
* the existing SSE channel, then closes with a fresh `advisor-group/end`.
|
|
2273
|
+
*/
|
|
2274
|
+
resumeConsultation(sessionId) {
|
|
2275
|
+
const session = this.sessions.get(sessionId);
|
|
2276
|
+
if (!session) return {
|
|
2277
|
+
ok: false,
|
|
2278
|
+
reason: "咨询会话不存在或已过期,请重新发起咨询。"
|
|
2279
|
+
};
|
|
2280
|
+
if (this.activeAborts.has(sessionId)) return {
|
|
2281
|
+
ok: false,
|
|
2282
|
+
reason: "该咨询正在运行中,无需继续。"
|
|
2283
|
+
};
|
|
2284
|
+
if (session.status === "completed" || this.nextPendingRound(session) === void 0) return {
|
|
2285
|
+
ok: false,
|
|
2286
|
+
reason: "该咨询已完成,无需继续。"
|
|
2287
|
+
};
|
|
2288
|
+
if (session.advisors.length === 0) return {
|
|
2289
|
+
ok: false,
|
|
2290
|
+
reason: "恢复失败:当前配置无法匹配该咨询的原顾问(顾问配置可能已被修改)。"
|
|
2291
|
+
};
|
|
2292
|
+
if (!this.acquireResumeLock(session.id)) return {
|
|
2293
|
+
ok: false,
|
|
2294
|
+
reason: "该咨询正在另一实例中恢复,请稍后再试(若确认无其他实例运行,10 分钟后会自动接管该锁)。"
|
|
2295
|
+
};
|
|
2296
|
+
const dshSession = this.resolveSessionHandle(session);
|
|
2297
|
+
const agentRegistry = this.ctx.agents;
|
|
2298
|
+
const resumeAgent = session.dshSessionId !== void 0 && agentRegistry?.get ? agentRegistry.get(session.dshSessionId) : void 0;
|
|
2299
|
+
session.status = "active";
|
|
2300
|
+
session.stopReason = void 0;
|
|
1216
2301
|
session.updatedAt = Date.now();
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
2302
|
+
this.persistSession(session);
|
|
2303
|
+
if (dshSession) appendAdvisorResume(dshSession, session.id);
|
|
2304
|
+
this.ctx.emit("advisor-group/resume", { sessionId: session.id });
|
|
2305
|
+
this.runAutoPipeline(session, void 0, dshSession, resumeAgent).catch((error) => {
|
|
2306
|
+
console.error("[dsh-advisor-group] 续跑流水线失败:", session.id, error instanceof Error ? error.message : String(error));
|
|
2307
|
+
}).finally(() => this.releaseResumeLock(session.id));
|
|
2308
|
+
return { ok: true };
|
|
2309
|
+
}
|
|
2310
|
+
/** Atomic cross-instance lock (mkdir-based, with stale takeover). */
|
|
2311
|
+
acquireResumeLock(sessionId) {
|
|
2312
|
+
const lockDir = join(this.sessionsDir, `${sessionId}.lock`);
|
|
2313
|
+
try {
|
|
2314
|
+
mkdirSync(this.sessionsDir, { recursive: true });
|
|
2315
|
+
mkdirSync(lockDir);
|
|
2316
|
+
try {
|
|
2317
|
+
writeFileSync(join(lockDir, "owner"), String(process.pid), "utf8");
|
|
2318
|
+
} catch {}
|
|
2319
|
+
return true;
|
|
2320
|
+
} catch {
|
|
2321
|
+
try {
|
|
2322
|
+
const stat = statSync(lockDir);
|
|
2323
|
+
if (Date.now() - stat.mtimeMs > 6e5) {
|
|
2324
|
+
rmSync(lockDir, {
|
|
2325
|
+
recursive: true,
|
|
2326
|
+
force: true
|
|
2327
|
+
});
|
|
2328
|
+
mkdirSync(lockDir);
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
} catch {}
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
releaseResumeLock(sessionId) {
|
|
2336
|
+
try {
|
|
2337
|
+
rmSync(join(this.sessionsDir, `${sessionId}.lock`), {
|
|
2338
|
+
recursive: true,
|
|
2339
|
+
force: true
|
|
2340
|
+
});
|
|
2341
|
+
} catch {}
|
|
2342
|
+
}
|
|
2343
|
+
async generateFinalSummary(session, signal, sessionLog, stopped) {
|
|
2344
|
+
session.updatedAt = Date.now();
|
|
2345
|
+
const summary = this.buildSummary(session, stopped);
|
|
2346
|
+
const conclusion = stopped ? "" : await generateConclusion(this.ctx, session, session.driverSource ?? resolveDriverSource(sessionLog, this.config.discussion.driverModel), signal, this.config.discussion.driverTimeoutMs);
|
|
2347
|
+
const finalSummary = stopped ? {
|
|
2348
|
+
...summary,
|
|
2349
|
+
stopped: true
|
|
2350
|
+
} : {
|
|
1220
2351
|
...summary,
|
|
1221
2352
|
conclusion
|
|
1222
2353
|
};
|
|
@@ -1235,6 +2366,7 @@ var AdvisorGroupService = class {
|
|
|
1235
2366
|
};
|
|
1236
2367
|
session.messages.push(message);
|
|
1237
2368
|
if (sessionLog) appendAdvisorMessage(sessionLog, session.id, message);
|
|
2369
|
+
this.persistSession(session);
|
|
1238
2370
|
this.ctx.emit("advisor-group/message", {
|
|
1239
2371
|
sessionId: session.id,
|
|
1240
2372
|
message
|
|
@@ -1246,17 +2378,38 @@ var AdvisorGroupService = class {
|
|
|
1246
2378
|
* calling ask_advisors again with sessionId + followUp, never by the
|
|
1247
2379
|
* advisor models talking to themselves back-to-back.
|
|
1248
2380
|
*/
|
|
1249
|
-
async runOneRoundAndSummarize(session, signal, sessionLog) {
|
|
1250
|
-
|
|
1251
|
-
session.
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
summary
|
|
1258
|
-
|
|
1259
|
-
|
|
2381
|
+
async runOneRoundAndSummarize(session, signal, sessionLog, agent) {
|
|
2382
|
+
const stop = new AbortController();
|
|
2383
|
+
this.activeAborts.set(session.id, stop);
|
|
2384
|
+
const combined = signal ? AbortSignal.any([signal, stop.signal]) : stop.signal;
|
|
2385
|
+
try {
|
|
2386
|
+
await this.runOneRound(session, combined, sessionLog, agent);
|
|
2387
|
+
session.status = "completed";
|
|
2388
|
+
session.updatedAt = Date.now();
|
|
2389
|
+
const summary = this.buildSummary(session, false);
|
|
2390
|
+
if (sessionLog) appendAdvisorEnd(sessionLog, session, summary);
|
|
2391
|
+
this.persistSession(session);
|
|
2392
|
+
await this.flushLog(sessionLog);
|
|
2393
|
+
this.ctx.emit("advisor-group/session-end", {
|
|
2394
|
+
sessionId: session.id,
|
|
2395
|
+
summary
|
|
2396
|
+
});
|
|
2397
|
+
return summary;
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
if (isAbortError(error, combined) || stop.signal.aborted) {
|
|
2400
|
+
if (stop.signal.aborted) session.stopReason = "user-stop";
|
|
2401
|
+
else if (signal?.aborted) session.stopReason = "exec-cancel";
|
|
2402
|
+
else session.stopReason = "abort-error";
|
|
2403
|
+
session.status = "cancelled";
|
|
2404
|
+
this.persistSession(session);
|
|
2405
|
+
const summary = await this.generateFinalSummary(session, void 0, sessionLog, true);
|
|
2406
|
+
await this.flushLog(sessionLog);
|
|
2407
|
+
return summary;
|
|
2408
|
+
}
|
|
2409
|
+
throw error;
|
|
2410
|
+
} finally {
|
|
2411
|
+
this.activeAborts.delete(session.id);
|
|
2412
|
+
}
|
|
1260
2413
|
}
|
|
1261
2414
|
getRoundCount(session) {
|
|
1262
2415
|
const advisorCount = Math.max(1, session.advisors.length);
|
|
@@ -1271,21 +2424,21 @@ var AdvisorGroupService = class {
|
|
|
1271
2424
|
return this.config.advisors.filter((advisor) => advisorIds.includes(advisor.id));
|
|
1272
2425
|
}
|
|
1273
2426
|
buildTranscript(session) {
|
|
1274
|
-
return session.messages.filter((message) => message.role !== "system").slice(-
|
|
2427
|
+
return session.messages.filter((message) => message.role !== "system").slice(-12).map((message) => {
|
|
1275
2428
|
if (message.role === "main") return {
|
|
1276
2429
|
role: "main",
|
|
1277
2430
|
name: "主模型",
|
|
1278
|
-
content: message.content.slice(0,
|
|
2431
|
+
content: message.content.slice(0, 3e3)
|
|
1279
2432
|
};
|
|
1280
2433
|
if (message.role === "advisor" && message.advisorName) return {
|
|
1281
2434
|
role: "advisor",
|
|
1282
2435
|
name: message.advisorName,
|
|
1283
|
-
content: message.content.slice(0,
|
|
2436
|
+
content: message.content.slice(0, 3e3)
|
|
1284
2437
|
};
|
|
1285
2438
|
return null;
|
|
1286
2439
|
}).filter((entry) => entry !== null);
|
|
1287
2440
|
}
|
|
1288
|
-
async callAdvisor(session, advisor, transcript, round, joinIndex, signal, sessionLog) {
|
|
2441
|
+
async callAdvisor(session, advisor, transcript, round, joinIndex, signal, sessionLog, agent) {
|
|
1289
2442
|
const relational = { ...advisor };
|
|
1290
2443
|
relational.systemPrompt = `${advisor.systemPrompt}\n${advisorJoinPrompt(joinIndex, session.advisors.length)}`;
|
|
1291
2444
|
const message = {
|
|
@@ -1323,18 +2476,21 @@ var AdvisorGroupService = class {
|
|
|
1323
2476
|
pendingText = "";
|
|
1324
2477
|
pendingThinking = "";
|
|
1325
2478
|
};
|
|
1326
|
-
const emitDelta = (delta) => {
|
|
2479
|
+
const emitDelta = (delta, phase = "answer") => {
|
|
1327
2480
|
if (delta.text) {
|
|
1328
2481
|
message.content += delta.text;
|
|
1329
2482
|
pendingText += delta.text;
|
|
1330
2483
|
}
|
|
1331
2484
|
if (delta.thinking) {
|
|
1332
2485
|
message.thinking = (message.thinking ?? "") + delta.thinking;
|
|
2486
|
+
thinkingBuffer += delta.thinking;
|
|
1333
2487
|
pendingThinking += delta.thinking;
|
|
1334
2488
|
}
|
|
1335
2489
|
publish(session.id, {
|
|
1336
2490
|
advisorId: advisor.id,
|
|
1337
2491
|
advisorName: advisor.name,
|
|
2492
|
+
round,
|
|
2493
|
+
phase,
|
|
1338
2494
|
contentDelta: delta.text,
|
|
1339
2495
|
thinkingDelta: delta.thinking,
|
|
1340
2496
|
done: false
|
|
@@ -1342,28 +2498,105 @@ var AdvisorGroupService = class {
|
|
|
1342
2498
|
if (!timer) timer = setTimeout(flush, 200);
|
|
1343
2499
|
};
|
|
1344
2500
|
const advisorTimeoutMs = this.config.discussion.advisorTimeoutMs;
|
|
2501
|
+
let truncated;
|
|
2502
|
+
const thinkingSegments = [];
|
|
2503
|
+
let thinkingBuffer = "";
|
|
2504
|
+
const actionDescriptions = [];
|
|
1345
2505
|
if (!isDshLlmProvider || advisor.baseURL || advisor.apiKey || advisor.apiKeyEnv) {
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
2506
|
+
const toolMode = advisor.tools ?? this.config.discussion.advisorTools ?? "readonly";
|
|
2507
|
+
const toolSchemas = resolveAdvisorToolSchemas(this.ctx, agent, toolMode);
|
|
2508
|
+
if (toolSchemas.length > 0) {
|
|
2509
|
+
relational.systemPrompt = `${relational.systemPrompt}${ADVISOR_TOOL_GUIDANCE}`;
|
|
2510
|
+
const messageToolSteps = [];
|
|
2511
|
+
let narrationBuffer = "";
|
|
2512
|
+
const emitToolStep = (step) => {
|
|
2513
|
+
const full = {
|
|
2514
|
+
...step,
|
|
2515
|
+
atMs: Date.now()
|
|
2516
|
+
};
|
|
2517
|
+
const parts = [];
|
|
2518
|
+
if (narrationBuffer.trim()) {
|
|
2519
|
+
parts.push(narrationBuffer.trim());
|
|
2520
|
+
narrationBuffer = "";
|
|
2521
|
+
}
|
|
2522
|
+
if (thinkingBuffer.trim()) parts.push(thinkingBuffer);
|
|
2523
|
+
if (parts.length > 0) {
|
|
2524
|
+
thinkingSegments.push(parts.join("\n\n"));
|
|
2525
|
+
thinkingBuffer = "";
|
|
2526
|
+
}
|
|
2527
|
+
messageToolSteps.push(full);
|
|
2528
|
+
if (sessionLog) appendAdvisorDelta(sessionLog, session.id, {
|
|
2529
|
+
sessionId: session.id,
|
|
2530
|
+
advisorId: advisor.id,
|
|
2531
|
+
advisorName: advisor.name,
|
|
2532
|
+
round,
|
|
2533
|
+
toolStep: full
|
|
2534
|
+
});
|
|
2535
|
+
publish(session.id, {
|
|
2536
|
+
advisorId: advisor.id,
|
|
2537
|
+
advisorName: advisor.name,
|
|
2538
|
+
round,
|
|
2539
|
+
toolStep: full
|
|
2540
|
+
});
|
|
2541
|
+
};
|
|
2542
|
+
const streamOnce = (tools, extra) => {
|
|
2543
|
+
const withExtra = extra.trim() ? [...transcript, {
|
|
2544
|
+
role: "main",
|
|
2545
|
+
name: "已执行工具",
|
|
2546
|
+
content: extra.trim()
|
|
2547
|
+
}] : transcript;
|
|
2548
|
+
const parseNames = toolSchemas.map((tool) => tool.name);
|
|
2549
|
+
const emitRoundDelta = (delta) => emitDelta(delta, tools.length > 0 ? "tool" : "answer");
|
|
2550
|
+
return streamDirectHttp(relational, withExtra, emitRoundDelta, signal, advisorTimeoutMs, tools, parseNames).then((result) => {
|
|
2551
|
+
narrationBuffer = result.content.trim();
|
|
2552
|
+
if (tools.length > 0 && narrationBuffer) actionDescriptions.push(narrationBuffer);
|
|
2553
|
+
return {
|
|
2554
|
+
content: result.content,
|
|
2555
|
+
thinking: result.thinking,
|
|
2556
|
+
toolCalls: result.toolCalls ?? [],
|
|
2557
|
+
truncated: result.truncated
|
|
2558
|
+
};
|
|
2559
|
+
});
|
|
2560
|
+
};
|
|
2561
|
+
const loop = await runAdvisorToolLoop(toolSchemas, streamOnce, (call) => executeAdvisorTool(this.ctx, agent, call, signal), (step) => emitToolStep(step));
|
|
2562
|
+
flush();
|
|
2563
|
+
content = loop.content;
|
|
2564
|
+
truncated = loop.truncated;
|
|
2565
|
+
message.toolSteps = messageToolSteps;
|
|
2566
|
+
} else {
|
|
2567
|
+
const streamResult = await streamDirectHttp(relational, transcript, emitDelta, signal, advisorTimeoutMs);
|
|
2568
|
+
flush();
|
|
2569
|
+
content = streamResult.content;
|
|
2570
|
+
message.thinking = streamResult.thinking;
|
|
2571
|
+
truncated = streamResult.truncated;
|
|
2572
|
+
}
|
|
1350
2573
|
} else {
|
|
1351
|
-
|
|
2574
|
+
const result = await callViaCtxLlm(this.ctx, relational, transcript, signal, emitDelta, advisorTimeoutMs);
|
|
1352
2575
|
flush();
|
|
2576
|
+
content = result.content;
|
|
2577
|
+
truncated = result.truncated;
|
|
1353
2578
|
}
|
|
1354
|
-
message.content = content;
|
|
2579
|
+
message.content = sanitizeAdvisorContent(content);
|
|
2580
|
+
if (truncated) message.truncated = truncated;
|
|
2581
|
+
if (thinkingBuffer.trim()) thinkingSegments.push(thinkingBuffer);
|
|
2582
|
+
if (thinkingSegments.length > 0) message.thinkingSegments = thinkingSegments;
|
|
2583
|
+
if (actionDescriptions.length > 0) message.actionDescriptions = actionDescriptions;
|
|
1355
2584
|
} catch (error) {
|
|
1356
2585
|
if (isAbortError(error, signal)) throw error;
|
|
1357
|
-
|
|
2586
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
2587
|
+
console.warn("[dsh-advisor-group] 顾问调用失败:", session.id, advisor.name, rawMessage);
|
|
2588
|
+
message.content = `(顾问调用失败:${this.summarizeAdvisorError(rawMessage)})`;
|
|
1358
2589
|
}
|
|
1359
2590
|
publish(session.id, {
|
|
1360
2591
|
advisorId: advisor.id,
|
|
1361
2592
|
advisorName: advisor.name,
|
|
2593
|
+
round,
|
|
1362
2594
|
done: true
|
|
1363
2595
|
});
|
|
1364
2596
|
session.messages.push(message);
|
|
1365
2597
|
if (sessionLog) appendAdvisorMessage(sessionLog, session.id, message);
|
|
1366
2598
|
session.updatedAt = Date.now();
|
|
2599
|
+
this.persistSession(session);
|
|
1367
2600
|
this.ctx.emit("advisor-group/message", {
|
|
1368
2601
|
sessionId: session.id,
|
|
1369
2602
|
message
|
|
@@ -1379,7 +2612,7 @@ var AdvisorGroupService = class {
|
|
|
1379
2612
|
}
|
|
1380
2613
|
const advisors = [...byAdvisor.entries()].map(([id, messages]) => {
|
|
1381
2614
|
const advisor = session.advisors.find((item) => item.id === id);
|
|
1382
|
-
const meaningful = messages.filter((message) => message.content && message.content.trim() && !message.content.startsWith("(顾问调用失败"));
|
|
2615
|
+
const meaningful = messages.filter((message) => message.content && message.content.trim() && !message.content.startsWith("(顾问调用失败") && !message.content.startsWith("(顾问未生成正文"));
|
|
1383
2616
|
const last = meaningful[meaningful.length - 1] ?? messages[messages.length - 1];
|
|
1384
2617
|
return {
|
|
1385
2618
|
advisorId: id,
|
|
@@ -1387,8 +2620,7 @@ var AdvisorGroupService = class {
|
|
|
1387
2620
|
corePoints: last ? this.extractCorePoints(last.content) : []
|
|
1388
2621
|
};
|
|
1389
2622
|
});
|
|
1390
|
-
const riskNotes = advisorMessages
|
|
1391
|
-
if (cancelled) riskNotes.push("本次顾问群讨论已被用户或主模型取消,结论可能不完整。");
|
|
2623
|
+
const riskNotes = buildRiskNotes(advisorMessages, cancelled);
|
|
1392
2624
|
return {
|
|
1393
2625
|
sessionId: session.id,
|
|
1394
2626
|
question: session.question,
|
|
@@ -1743,6 +2975,167 @@ function matchAdvisors(text, advisors) {
|
|
|
1743
2975
|
return [...matches];
|
|
1744
2976
|
}
|
|
1745
2977
|
//#endregion
|
|
2978
|
+
//#region src/providers/jev.ts
|
|
2979
|
+
const DEFAULT_ENDPOINTS = {
|
|
2980
|
+
typesafe: "https://api.typesafe.ai/v1/systemone",
|
|
2981
|
+
openrouter: "https://openrouter.ai/api/alpha/decisions"
|
|
2982
|
+
};
|
|
2983
|
+
function numberValue(value) {
|
|
2984
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2985
|
+
}
|
|
2986
|
+
function answerIsTrue(answer, threshold) {
|
|
2987
|
+
const noul = numberValue(answer?.noul);
|
|
2988
|
+
if (noul !== void 0) return noul >= threshold;
|
|
2989
|
+
const choice = typeof answer?.choice === "string" ? answer.choice.toLowerCase() : "";
|
|
2990
|
+
if (choice === "yes" || choice === "true") return true;
|
|
2991
|
+
if (choice === "no" || choice === "false") return false;
|
|
2992
|
+
return false;
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* An answer is usable only when it carries a finite noul or an explicit
|
|
2996
|
+
* yes/no choice. Anything else (missing key, string noul, "maybe") would be
|
|
2997
|
+
* read as `false` by answerIsTrue — which for needs_advisor/high_risk means a
|
|
2998
|
+
* silent rejection with no fallback to the local classifier.
|
|
2999
|
+
*/
|
|
3000
|
+
function answerUsable(answer) {
|
|
3001
|
+
if (numberValue(answer?.noul) !== void 0) return true;
|
|
3002
|
+
const choice = typeof answer?.choice === "string" ? answer.choice.toLowerCase() : "";
|
|
3003
|
+
return choice === "yes" || choice === "true" || choice === "no" || choice === "false";
|
|
3004
|
+
}
|
|
3005
|
+
function parseResponse(body, config, advisors) {
|
|
3006
|
+
if (!body || typeof body !== "object") throw new Error("Jev 响应不是对象");
|
|
3007
|
+
const answers = body.answers;
|
|
3008
|
+
if (!answers || typeof answers !== "object") throw new Error("Jev 响应缺少 answers");
|
|
3009
|
+
const needsAdvisor = answers.needs_advisor;
|
|
3010
|
+
const webSearch = answers.web_search;
|
|
3011
|
+
const risk = answers.high_risk;
|
|
3012
|
+
const domainAnswer = answers.domain;
|
|
3013
|
+
if (!answerUsable(needsAdvisor)) throw new Error("Jev 响应缺少 needs_advisor 的有效判定");
|
|
3014
|
+
if (!answerUsable(risk)) throw new Error("Jev 响应缺少 high_risk 的有效判定");
|
|
3015
|
+
const domain = typeof domainAnswer?.choice === "string" ? domainAnswer.choice : "general";
|
|
3016
|
+
const needsAdvisorNoul = numberValue(needsAdvisor?.noul);
|
|
3017
|
+
const confidence = numberValue(needsAdvisor?.confidence) ?? needsAdvisorNoul;
|
|
3018
|
+
const shouldEscalate = answerIsTrue(needsAdvisor, config.confidenceThreshold);
|
|
3019
|
+
const suggestWebSearch = answerIsTrue(webSearch, config.confidenceThreshold);
|
|
3020
|
+
const highRisk = answerIsTrue(risk, config.highRiskThreshold ?? config.confidenceThreshold);
|
|
3021
|
+
const reportedModel = body.model;
|
|
3022
|
+
const webSearchNoul = numberValue(webSearch?.noul);
|
|
3023
|
+
const riskNoul = numberValue(risk?.noul);
|
|
3024
|
+
const rawAnswers = {
|
|
3025
|
+
...needsAdvisorNoul === void 0 ? {} : { needsAdvisor: needsAdvisorNoul },
|
|
3026
|
+
...webSearchNoul === void 0 ? {} : { webSearch: webSearchNoul },
|
|
3027
|
+
...riskNoul === void 0 ? {} : { highRisk: riskNoul },
|
|
3028
|
+
domain
|
|
3029
|
+
};
|
|
3030
|
+
return {
|
|
3031
|
+
shouldEscalate: shouldEscalate || highRisk,
|
|
3032
|
+
suggestWebSearch: !shouldEscalate && !highRisk && suggestWebSearch,
|
|
3033
|
+
suggestedAdvisors: matchAdvisors(`${domain}\n${domainAnswer?.choice ?? ""}`, advisors),
|
|
3034
|
+
reason: highRisk ? "Jev 判断该问题包含高风险因素,建议启动顾问群。" : shouldEscalate ? `Jev 判断该问题需要顾问群(${confidence === void 0 ? "无置信度" : `置信度 ${confidence}`})。` : suggestWebSearch ? "Jev 判断该问题更适合联网搜索。" : "Jev 判断该问题不需要启动顾问群。",
|
|
3035
|
+
...confidence === void 0 ? {} : { confidence },
|
|
3036
|
+
rawAnswers,
|
|
3037
|
+
...typeof reportedModel === "string" ? { model: reportedModel } : {},
|
|
3038
|
+
provider: config.provider
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
async function classifyWithJev(question, context, advisors, config, signal, questionEn) {
|
|
3042
|
+
const endpoint = config.baseURL?.trim() || DEFAULT_ENDPOINTS[config.provider];
|
|
3043
|
+
const storedKey = typeof config.apiKey === "string" ? config.apiKey : config.apiKey && typeof config.apiKey === "object" && "value" in config.apiKey ? String(config.apiKey.value ?? "") : void 0;
|
|
3044
|
+
const apiKey = (config.apiKeyEnv ? process.env[config.apiKeyEnv] : void 0) || storedKey;
|
|
3045
|
+
if (!apiKey) throw new Error("Jev 未配置 API key");
|
|
3046
|
+
const controller = new AbortController();
|
|
3047
|
+
const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`Jev 超时(${config.timeoutMs}ms)`)), config.timeoutMs);
|
|
3048
|
+
const abort = () => controller.abort();
|
|
3049
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3050
|
+
try {
|
|
3051
|
+
const response = await fetch(endpoint, {
|
|
3052
|
+
method: "POST",
|
|
3053
|
+
headers: {
|
|
3054
|
+
authorization: `Bearer ${apiKey}`,
|
|
3055
|
+
"content-type": "application/json"
|
|
3056
|
+
},
|
|
3057
|
+
body: JSON.stringify({
|
|
3058
|
+
state: config.useEnglishState && questionEn?.trim() ? questionEn.trim() : `${question}\n\n背景:${context || "无"}`,
|
|
3059
|
+
model: config.model || "jev-latest",
|
|
3060
|
+
questions: {
|
|
3061
|
+
needs_advisor: {
|
|
3062
|
+
type: "noul",
|
|
3063
|
+
instructions: "Does answering this question require expert-level knowledge?",
|
|
3064
|
+
criteria: {
|
|
3065
|
+
true: {
|
|
3066
|
+
what: "The subject matter is professional, specialist, or long-tail knowledge a competent generalist would not reliably get right, or the field itself has no single settled answer without specialist judgment.",
|
|
3067
|
+
examples: [
|
|
3068
|
+
"professional-domain judgment such as accounting treatment, engineering trade-offs, or contract drafting",
|
|
3069
|
+
"rare or long-tail situations with little public consensus",
|
|
3070
|
+
"topics where reasonable experts disagree and picking correctly matters"
|
|
3071
|
+
]
|
|
3072
|
+
},
|
|
3073
|
+
false: {
|
|
3074
|
+
what: "A competent generalist can answer from common knowledge, widely documented facts, or ordinary reasoning without specialist judgment.",
|
|
3075
|
+
examples: ["everyday facts, definitions, or casual opinions", "well-documented mainstream topics with an agreed answer"]
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
},
|
|
3079
|
+
web_search: {
|
|
3080
|
+
type: "noul",
|
|
3081
|
+
instructions: "Does answering this question primarily require current, real-time, or externally verifiable information?",
|
|
3082
|
+
criteria: {
|
|
3083
|
+
true: {
|
|
3084
|
+
what: "The correct answer depends on facts that change over time or live outside the provided state, so it must be looked up rather than reasoned out.",
|
|
3085
|
+
examples: [
|
|
3086
|
+
"today’s weather, prices, exchange rates, or quotes",
|
|
3087
|
+
"recent releases, news, or version-specific changelogs",
|
|
3088
|
+
"local or niche facts with no authoritative source in memory"
|
|
3089
|
+
]
|
|
3090
|
+
},
|
|
3091
|
+
false: {
|
|
3092
|
+
what: "The answer follows from stable knowledge or reasoning already available in the state.",
|
|
3093
|
+
examples: ["concepts, definitions, and established best practices", "questions fully answerable from the provided background"]
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
},
|
|
3097
|
+
high_risk: {
|
|
3098
|
+
type: "noul",
|
|
3099
|
+
instructions: "Does this question involve medical, legal, financial, safety, compliance, or other high-risk advice?",
|
|
3100
|
+
criteria: {
|
|
3101
|
+
true: {
|
|
3102
|
+
what: "A wrong or careless answer could materially harm the asker or a third party in health, money, safety, or legal standing.",
|
|
3103
|
+
examples: [
|
|
3104
|
+
"medical symptoms, medication, diagnosis, or treatment decisions",
|
|
3105
|
+
"legal rights, contracts, litigation, or regulatory compliance",
|
|
3106
|
+
"investment, insurance, tax, or other money decisions with real downside",
|
|
3107
|
+
"physical safety, hazardous actions, or regulated activities"
|
|
3108
|
+
]
|
|
3109
|
+
},
|
|
3110
|
+
false: {
|
|
3111
|
+
what: "Being wrong carries no material harm to health, money, safety, or legal standing.",
|
|
3112
|
+
examples: ["general knowledge, opinions, or entertainment questions", "technical details whose worst case is wasted time"]
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
},
|
|
3116
|
+
domain: {
|
|
3117
|
+
type: "choice",
|
|
3118
|
+
instructions: "Which domain best describes this question?",
|
|
3119
|
+
criteria: {
|
|
3120
|
+
legal: "law, contracts, litigation, compliance, or taxation",
|
|
3121
|
+
medical: "medicine, symptoms, medication, diagnosis, or treatment",
|
|
3122
|
+
finance: "investment, insurance, accounting, or financial risk",
|
|
3123
|
+
code: "software, programming, architecture, debugging, or APIs",
|
|
3124
|
+
general: "none of the listed domains"
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}),
|
|
3129
|
+
signal: controller.signal
|
|
3130
|
+
});
|
|
3131
|
+
if (!response.ok) throw new Error(`Jev HTTP ${response.status}`);
|
|
3132
|
+
return parseResponse(await response.json(), config, advisors);
|
|
3133
|
+
} finally {
|
|
3134
|
+
clearTimeout(timeout);
|
|
3135
|
+
signal?.removeEventListener("abort", abort);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
//#endregion
|
|
1746
3139
|
//#region src/tools.ts
|
|
1747
3140
|
function registerAdvisorTools(service) {
|
|
1748
3141
|
return [defineTool({
|
|
@@ -1773,6 +3166,10 @@ function registerAdvisorTools(service) {
|
|
|
1773
3166
|
confidence: {
|
|
1774
3167
|
type: "number",
|
|
1775
3168
|
description: "Optional self-assessed confidence 0-1. Values below the configured threshold escalate even without domain keywords."
|
|
3169
|
+
},
|
|
3170
|
+
questionEn: {
|
|
3171
|
+
type: "string",
|
|
3172
|
+
description: "Optional self-contained English gist of the question plus key background. Used as the Jev pre-classification input only when the「Jev 英文判定」setting is enabled; the displayed question, session records, and duplicate detection always keep using question/context in Chinese. Omit it otherwise."
|
|
1776
3173
|
}
|
|
1777
3174
|
},
|
|
1778
3175
|
output: {
|
|
@@ -1828,7 +3225,18 @@ function registerAdvisorTools(service) {
|
|
|
1828
3225
|
const questionText = typeof args.question === "string" ? args.question : "";
|
|
1829
3226
|
const contextText = typeof args.context === "string" ? args.context : "";
|
|
1830
3227
|
const mentionedAdvisorGroup = /@顾问群|@顧問群/.test(`${questionText}\n${contextText}`);
|
|
1831
|
-
const
|
|
3228
|
+
const repeatCount = service.getRepeatPressure(sessionLog?.id)?.count ?? 0;
|
|
3229
|
+
const forcedByRepeat = repeatCount >= 3;
|
|
3230
|
+
let shadow;
|
|
3231
|
+
let shadowWritten = false;
|
|
3232
|
+
const recordShadow = (launched) => {
|
|
3233
|
+
if (!shadow || shadowWritten) return;
|
|
3234
|
+
shadowWritten = true;
|
|
3235
|
+
appendShadowSample({
|
|
3236
|
+
...shadow,
|
|
3237
|
+
launched
|
|
3238
|
+
});
|
|
3239
|
+
};
|
|
1832
3240
|
if (args.sessionId) {
|
|
1833
3241
|
if (!args.followUp) return {
|
|
1834
3242
|
sessionId: args.sessionId,
|
|
@@ -1843,6 +3251,12 @@ function registerAdvisorTools(service) {
|
|
|
1843
3251
|
skipped: true,
|
|
1844
3252
|
reason: "session-not-found"
|
|
1845
3253
|
};
|
|
3254
|
+
if (service.isSessionRunning(session.id)) return {
|
|
3255
|
+
sessionId: session.id,
|
|
3256
|
+
advice: "该顾问群会话正在运行中,请等本轮结束后再追问。",
|
|
3257
|
+
skipped: true,
|
|
3258
|
+
reason: "session-running"
|
|
3259
|
+
};
|
|
1846
3260
|
if (service.hasReachedMaxRounds(session)) return {
|
|
1847
3261
|
sessionId: session.id,
|
|
1848
3262
|
advice: "本轮已是顾问群讨论的最后一轮,请综合现有顾问意见给出最终答复,不要再追问。",
|
|
@@ -1850,11 +3264,11 @@ function registerAdvisorTools(service) {
|
|
|
1850
3264
|
reason: "max-rounds-reached"
|
|
1851
3265
|
};
|
|
1852
3266
|
service.appendFollowUp(session.id, args.followUp, sessionLog);
|
|
1853
|
-
await service.runOneRoundAndSummarize(session, exec.signal, sessionLog);
|
|
3267
|
+
const summary = await service.runOneRoundAndSummarize(session, exec.signal, sessionLog, exec.agent);
|
|
1854
3268
|
const canContinue = !service.hasReachedMaxRounds(session);
|
|
1855
3269
|
return {
|
|
1856
3270
|
sessionId: session.id,
|
|
1857
|
-
advice: formatConversation(session) + (canContinue ? "\n\n(如需继续,可再次传入 sessionId 和 followUp 追问。)" : "\n\n(讨论轮数已达上限,请综合顾问意见给出最终答复。)"),
|
|
3271
|
+
advice: formatConversation(session) + formatRiskNotes(summary) + (canContinue ? "\n\n(如需继续,可再次传入 sessionId 和 followUp 追问。)" : "\n\n(讨论轮数已达上限,请综合顾问意见给出最终答复。)"),
|
|
1858
3272
|
skipped: false
|
|
1859
3273
|
};
|
|
1860
3274
|
}
|
|
@@ -1865,17 +3279,41 @@ function registerAdvisorTools(service) {
|
|
|
1865
3279
|
reason: "missing-question"
|
|
1866
3280
|
};
|
|
1867
3281
|
if (cfg.trigger.requireClassifier && !mentionedAdvisorGroup && !forcedByRepeat) {
|
|
1868
|
-
|
|
1869
|
-
|
|
3282
|
+
let classification;
|
|
3283
|
+
let jevResult;
|
|
3284
|
+
let jevError;
|
|
3285
|
+
let jevLatencyMs;
|
|
3286
|
+
const jev = cfg.trigger.jev;
|
|
3287
|
+
if (jev?.enabled) {
|
|
3288
|
+
const jevStartedAt = Date.now();
|
|
3289
|
+
try {
|
|
3290
|
+
jevResult = await classifyWithJev(args.question, args.context, cfg.advisors, jev, exec.signal, typeof args.questionEn === "string" ? args.questionEn : void 0);
|
|
3291
|
+
classification = jevResult;
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
if (exec.signal.aborted) throw error;
|
|
3294
|
+
jevError = error instanceof Error ? error.message : String(error);
|
|
3295
|
+
console.warn("[dsh-advisor-group] Jev 分类失败,回退到本地分类器:", sessionLog?.id ?? "no-session", jevError);
|
|
3296
|
+
} finally {
|
|
3297
|
+
jevLatencyMs = Date.now() - jevStartedAt;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
classification ??= classifyRequest(args.question, args.context, cfg.advisors, args.confidence, cfg.trigger.confidenceThreshold);
|
|
3301
|
+
shadow = {
|
|
1870
3302
|
ts: Date.now(),
|
|
1871
3303
|
question: args.question,
|
|
1872
3304
|
...typeof args.confidence === "number" ? { confidence: args.confidence } : {},
|
|
1873
3305
|
shouldEscalate: classification.shouldEscalate,
|
|
1874
3306
|
reason: classification.reason,
|
|
1875
3307
|
suggestWebSearch: classification.suggestWebSearch,
|
|
1876
|
-
|
|
1877
|
-
|
|
3308
|
+
provider: jevResult ? "jev" : "local",
|
|
3309
|
+
...jevError === void 0 ? {} : { jevError },
|
|
3310
|
+
...jevLatencyMs === void 0 ? {} : { jevLatencyMs },
|
|
3311
|
+
...repeatCount > 0 ? { repeatCount } : {},
|
|
3312
|
+
...jevResult?.rawAnswers ? { scores: jevResult.rawAnswers } : {},
|
|
3313
|
+
...jevResult?.model ? { model: jevResult.model } : {}
|
|
3314
|
+
};
|
|
1878
3315
|
if (!classification.shouldEscalate) {
|
|
3316
|
+
recordShadow(false);
|
|
1879
3317
|
if (classification.suggestWebSearch && cfg.trigger.allowWebFallback) return {
|
|
1880
3318
|
sessionId: "",
|
|
1881
3319
|
advice: `前置分类器建议不启动顾问群:${classification.reason}`,
|
|
@@ -1889,29 +3327,45 @@ function registerAdvisorTools(service) {
|
|
|
1889
3327
|
reason: "classifier-rejected"
|
|
1890
3328
|
};
|
|
1891
3329
|
}
|
|
1892
|
-
}
|
|
3330
|
+
} else if (cfg.trigger.requireClassifier) shadow = {
|
|
3331
|
+
ts: Date.now(),
|
|
3332
|
+
question: args.question,
|
|
3333
|
+
shouldEscalate: true,
|
|
3334
|
+
reason: mentionedAdvisorGroup ? "用户点名 @顾问群,跳过前置分类器。" : `同一问题已重复 ${repeatCount} 次,强制升级跳过前置分类器。`,
|
|
3335
|
+
suggestWebSearch: false,
|
|
3336
|
+
provider: "bypass",
|
|
3337
|
+
bypass: mentionedAdvisorGroup ? "mention" : "repeat",
|
|
3338
|
+
repeatCount
|
|
3339
|
+
};
|
|
1893
3340
|
if (args.advisorIds && args.advisorIds.length > 0) {
|
|
1894
3341
|
const configuredIds = new Set(cfg.advisors.map((advisor) => advisor.id));
|
|
1895
|
-
if (args.advisorIds.filter((id) => configuredIds.has(id)).length === 0)
|
|
3342
|
+
if (args.advisorIds.filter((id) => configuredIds.has(id)).length === 0) {
|
|
3343
|
+
recordShadow(false);
|
|
3344
|
+
return {
|
|
3345
|
+
sessionId: "",
|
|
3346
|
+
advice: "未匹配到任何指定顾问,请检查 advisorIds 是否与设置中的顾问 id 一致。",
|
|
3347
|
+
skipped: true,
|
|
3348
|
+
reason: "no-matching-advisors"
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
const costGuard = service.tryStartConsultation();
|
|
3353
|
+
if (!costGuard.ok) {
|
|
3354
|
+
recordShadow(false);
|
|
3355
|
+
return {
|
|
1896
3356
|
sessionId: "",
|
|
1897
|
-
advice:
|
|
3357
|
+
advice: costGuard.reason,
|
|
1898
3358
|
skipped: true,
|
|
1899
|
-
reason: "
|
|
3359
|
+
reason: "daily-limit-reached"
|
|
1900
3360
|
};
|
|
1901
3361
|
}
|
|
1902
|
-
const
|
|
1903
|
-
if (!costGuard.ok) return {
|
|
1904
|
-
sessionId: "",
|
|
1905
|
-
advice: costGuard.reason,
|
|
1906
|
-
skipped: true,
|
|
1907
|
-
reason: "daily-limit-reached"
|
|
1908
|
-
};
|
|
1909
|
-
const session = service.createSession(args.question, args.context, args.advisorIds);
|
|
3362
|
+
const session = service.createSession(args.question, args.context, args.advisorIds, sessionLog ? sessionLog.header.cwd : void 0, sessionLog ? sessionLog.id : void 0);
|
|
1910
3363
|
if (sessionLog) appendAdvisorStart(sessionLog, session);
|
|
1911
|
-
|
|
3364
|
+
recordShadow(true);
|
|
3365
|
+
const summary = await service.runAutoPipeline(session, exec.signal, sessionLog, exec.agent);
|
|
1912
3366
|
return {
|
|
1913
3367
|
sessionId: session.id,
|
|
1914
|
-
advice: formatConversation(session) + (summary.conclusion ? `\n\n【综合结论】\n${summary.conclusion}` : "") + (service.hasReachedMaxRounds(session) ? "\n\n(已达设置的最大讨论轮数,请参考上述顾问意见与综合结论给出最终答复。)" : ""),
|
|
3368
|
+
advice: formatConversation(session) + (summary.conclusion ? `\n\n【综合结论】\n${summary.conclusion}` : "") + formatRiskNotes(summary) + (service.hasReachedMaxRounds(session) ? "\n\n(已达设置的最大讨论轮数,请参考上述顾问意见与综合结论给出最终答复。)" : ""),
|
|
1915
3369
|
skipped: false
|
|
1916
3370
|
};
|
|
1917
3371
|
}
|
|
@@ -1942,26 +3396,164 @@ function registerAdvisorTools(service) {
|
|
|
1942
3396
|
}
|
|
1943
3397
|
})];
|
|
1944
3398
|
}
|
|
3399
|
+
function formatRiskNotes(summary) {
|
|
3400
|
+
return summary.riskNotes.length > 0 ? `\n\n【风险提示】\n${summary.riskNotes.join("\n")}` : "";
|
|
3401
|
+
}
|
|
1945
3402
|
function formatConversation(session) {
|
|
1946
3403
|
const lines = [`顾问群对话(session: ${session.id})`, ""];
|
|
1947
3404
|
for (const message of session.messages) {
|
|
1948
3405
|
if (message.role === "system") continue;
|
|
1949
3406
|
if (message.role === "main") lines.push(`主模型:${message.content}`);
|
|
1950
3407
|
else if (message.role === "advisor") {
|
|
1951
|
-
const
|
|
3408
|
+
const truncatedNote = message.truncated === void 0 ? "" : `(顾问输出在流式过程中被截断:${message.truncated.reason === "timeout" ? "超时" : "网络中断"},正文可能不完整)\n`;
|
|
3409
|
+
const toolNote = message.toolSteps && message.toolSteps.length > 0 ? `${message.toolSteps.map((step) => step.kind === "call" ? ` ⛭ 调用 ${step.name} · ${step.text}` : ` ↳ ${step.text}`).join("\n")}\n` : "";
|
|
3410
|
+
const body = message.content && message.content.trim() ? truncatedNote + toolNote + message.content : truncatedNote + toolNote + (message.thinking && message.thinking.trim() ? `(思维链)\n${message.thinking}` : "(顾问未返回正文)");
|
|
1952
3411
|
lines.push(`【${message.advisorName ?? message.advisorId ?? "顾问"}】:${body}`);
|
|
1953
3412
|
}
|
|
1954
3413
|
lines.push("");
|
|
1955
3414
|
}
|
|
3415
|
+
const stopNote = session.status === "cancelled" && session.stopReason ? `(注意:本次咨询被中断(${STOP_REASON_TEXT[session.stopReason] ?? session.stopReason});可在对话卡片点击「▶ 继续聊天」从断点续跑。)` : "";
|
|
3416
|
+
lines.push(stopNote);
|
|
1956
3417
|
lines.push("(以上是顾问模型与主模型的对话记录,主模型可直接基于其中内容继续思考或追问。)");
|
|
1957
3418
|
return lines.join("\n");
|
|
1958
3419
|
}
|
|
3420
|
+
const STOP_REASON_TEXT = {
|
|
3421
|
+
"user-stop": "用户主动停止",
|
|
3422
|
+
"exec-cancel": "宿主取消",
|
|
3423
|
+
"abort-error": "流异常中断",
|
|
3424
|
+
timeout: "顾问响应超时",
|
|
3425
|
+
network: "网络中断"
|
|
3426
|
+
};
|
|
3427
|
+
//#endregion
|
|
3428
|
+
//#region src/prompt-guard.ts
|
|
3429
|
+
const REPLACEMENT = "�";
|
|
3430
|
+
const BOM = "";
|
|
3431
|
+
/**
|
|
3432
|
+
* Unambiguity check: same line count, and every character either equals the
|
|
3433
|
+
* template or is a replacement char (the byte-loss corruption signature).
|
|
3434
|
+
*/
|
|
3435
|
+
function matchesTemplateWithReplacement(storedLines, templateLines) {
|
|
3436
|
+
for (let i = 0; i < storedLines.length; i++) {
|
|
3437
|
+
const stored = storedLines[i];
|
|
3438
|
+
const template = templateLines[i];
|
|
3439
|
+
if (stored === template) continue;
|
|
3440
|
+
if (stored.length !== template.length) return false;
|
|
3441
|
+
for (let j = 0; j < stored.length; j++) if (stored[j] !== template[j] && stored[j] !== REPLACEMENT) return false;
|
|
3442
|
+
}
|
|
3443
|
+
return true;
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Advisor system-prompt corruption guard (P1, 2026-09-27).
|
|
3447
|
+
* Fast path is free: prompts without replacement chars pass through
|
|
3448
|
+
* untouched (a customized prompt is legitimate and must never be overwritten).
|
|
3449
|
+
* A leading BOM alone is stripped (action 'stripped', no template involved).
|
|
3450
|
+
* Only on a replacement char do we compare against the built-in template:
|
|
3451
|
+
* unambiguous (line count + per-character match) -> heal to the template;
|
|
3452
|
+
* anything else keeps the stored value and reports it for manual handling.
|
|
3453
|
+
*/
|
|
3454
|
+
function healAdvisorSystemPrompt(stored, template = DEFAULT_ADVISOR_PROMPT) {
|
|
3455
|
+
let value = stored;
|
|
3456
|
+
let hadBom = false;
|
|
3457
|
+
if (value.startsWith(BOM)) {
|
|
3458
|
+
value = value.slice(1);
|
|
3459
|
+
hadBom = true;
|
|
3460
|
+
}
|
|
3461
|
+
const replacements = (value.match(/\uFFFD/g) ?? []).length;
|
|
3462
|
+
if (replacements === 0) {
|
|
3463
|
+
if (hadBom) return {
|
|
3464
|
+
action: "stripped",
|
|
3465
|
+
value,
|
|
3466
|
+
reason: "开头含 BOM,已剥除"
|
|
3467
|
+
};
|
|
3468
|
+
return {
|
|
3469
|
+
action: "ok",
|
|
3470
|
+
value: stored
|
|
3471
|
+
};
|
|
3472
|
+
}
|
|
3473
|
+
const storedLines = value.split("\n");
|
|
3474
|
+
const templateLines = template.split("\n");
|
|
3475
|
+
if (storedLines.length !== templateLines.length) return {
|
|
3476
|
+
action: "needs-manual",
|
|
3477
|
+
value: stored,
|
|
3478
|
+
reason: `含 ${replacements} 个替换符,行数 ${storedLines.length} 与模板 ${templateLines.length} 不一致,无法无歧义回填`
|
|
3479
|
+
};
|
|
3480
|
+
if (!matchesTemplateWithReplacement(storedLines, templateLines)) return {
|
|
3481
|
+
action: "needs-manual",
|
|
3482
|
+
value: stored,
|
|
3483
|
+
reason: `含 ${replacements} 个替换符,但与模板逐位比对存在不一致(可能含自定义内容),未回填`
|
|
3484
|
+
};
|
|
3485
|
+
return {
|
|
3486
|
+
action: "healed",
|
|
3487
|
+
value: template,
|
|
3488
|
+
reason: `含 ${replacements} 个替换符${hadBom ? ",另开头含 BOM" : ""},行数与逐位比对均与模板一致,已回填内置模板`
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Load-time guard over every advisor prompt. Returns the original config
|
|
3493
|
+
* object untouched when nothing was healed, so callers can rely on identity
|
|
3494
|
+
* checks to detect "no change".
|
|
3495
|
+
*/
|
|
3496
|
+
function guardAdvisorPrompts(config) {
|
|
3497
|
+
const findings = [];
|
|
3498
|
+
let healedAny = false;
|
|
3499
|
+
const advisors = config.advisors.map((advisor) => {
|
|
3500
|
+
const result = healAdvisorSystemPrompt(advisor.systemPrompt);
|
|
3501
|
+
if (result.action === "ok") return advisor;
|
|
3502
|
+
findings.push({
|
|
3503
|
+
advisorId: advisor.id,
|
|
3504
|
+
action: result.action,
|
|
3505
|
+
reason: result.reason ?? ""
|
|
3506
|
+
});
|
|
3507
|
+
if (result.action === "healed" || result.action === "stripped") {
|
|
3508
|
+
healedAny = true;
|
|
3509
|
+
return {
|
|
3510
|
+
...advisor,
|
|
3511
|
+
systemPrompt: result.value
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
return advisor;
|
|
3515
|
+
});
|
|
3516
|
+
return {
|
|
3517
|
+
config: healedAny ? {
|
|
3518
|
+
...config,
|
|
3519
|
+
advisors
|
|
3520
|
+
} : config,
|
|
3521
|
+
findings
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Shared logger for both call sites: one line per finding, stating the action
|
|
3526
|
+
* actually taken (healed vs left untouched) plus the concrete reason.
|
|
3527
|
+
* Every line carries an ISO-8601 timestamp so the host console redirect
|
|
3528
|
+
* (which does not timestamp lines itself) stays forensically usable.
|
|
3529
|
+
*/
|
|
3530
|
+
function reportPromptGuardFindings(findings) {
|
|
3531
|
+
const stamp = () => `[${(/* @__PURE__ */ new Date()).toISOString()}]`;
|
|
3532
|
+
for (const finding of findings) if (finding.action === "healed") console.warn(`${stamp()} [dsh-advisor-group] 顾问提示词损坏,已按内置模板回填:${finding.advisorId}(${finding.reason})`);
|
|
3533
|
+
else if (finding.action === "stripped") console.warn(`${stamp()} [dsh-advisor-group] 顾问提示词开头含 BOM,已剥除、正文未改动:${finding.advisorId}(${finding.reason})`);
|
|
3534
|
+
else console.warn(`${stamp()} [dsh-advisor-group] 顾问提示词损坏且无法无歧义回填,未改动、需人工处理:${finding.advisorId}(${finding.reason})`);
|
|
3535
|
+
}
|
|
1959
3536
|
//#endregion
|
|
1960
3537
|
//#region src/settings-api.ts
|
|
1961
3538
|
function sendJson(res, status, body) {
|
|
1962
3539
|
res.writeHead(status, { "content-type": "application/json" });
|
|
1963
3540
|
res.end(JSON.stringify(body));
|
|
1964
3541
|
}
|
|
3542
|
+
function normalizeJevConfig(input) {
|
|
3543
|
+
const apiKey = typeof input?.apiKey === "string" ? input.apiKey : input?.apiKey && typeof input.apiKey === "object" && "value" in input.apiKey ? String(input.apiKey.value ?? "") : void 0;
|
|
3544
|
+
return {
|
|
3545
|
+
enabled: input?.enabled ?? false,
|
|
3546
|
+
provider: input?.provider ?? "typesafe",
|
|
3547
|
+
model: input?.model ?? "jev-latest",
|
|
3548
|
+
...input?.baseURL ? { baseURL: input.baseURL } : {},
|
|
3549
|
+
...apiKey ? { apiKey } : {},
|
|
3550
|
+
...input?.apiKeyEnv ? { apiKeyEnv: input.apiKeyEnv } : {},
|
|
3551
|
+
timeoutMs: input?.timeoutMs ?? 1e4,
|
|
3552
|
+
confidenceThreshold: input?.confidenceThreshold ?? .6,
|
|
3553
|
+
useEnglishState: input?.useEnglishState ?? false,
|
|
3554
|
+
...typeof input?.highRiskThreshold === "number" ? { highRiskThreshold: input.highRiskThreshold } : {}
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
1965
3557
|
/** Mask a stored API key so it never leaves the host in plain text. */
|
|
1966
3558
|
function maskApiKey(apiKey) {
|
|
1967
3559
|
if (!apiKey) return apiKey;
|
|
@@ -1984,15 +3576,27 @@ function isMaskedApiKey(incoming, previous) {
|
|
|
1984
3576
|
/**
|
|
1985
3577
|
* Return a copy of the config safe to send to the browser.
|
|
1986
3578
|
*
|
|
1987
|
-
*
|
|
3579
|
+
* SecretField convergence: no key material (not even a mask) leaves
|
|
1988
3580
|
* the host. `apiKey` is returned empty and per-provider presence facts
|
|
1989
3581
|
* (`configured` + `last4`) ride in `apiKeyMetaByProvider`, so the card can
|
|
1990
3582
|
* render the official "已配置/未配置" badge and publish new/cleared keys
|
|
1991
3583
|
* without ever round-tripping a mask.
|
|
1992
3584
|
*/
|
|
1993
3585
|
function sanitizeConfig(config) {
|
|
3586
|
+
const jev = normalizeJevConfig(config.trigger.jev);
|
|
1994
3587
|
return {
|
|
1995
3588
|
...config,
|
|
3589
|
+
trigger: {
|
|
3590
|
+
...config.trigger,
|
|
3591
|
+
jev: {
|
|
3592
|
+
...jev,
|
|
3593
|
+
apiKey: "",
|
|
3594
|
+
...jev?.apiKey ? { apiKeyMeta: {
|
|
3595
|
+
configured: true,
|
|
3596
|
+
last4: jev.apiKey.slice(-4)
|
|
3597
|
+
} } : {}
|
|
3598
|
+
}
|
|
3599
|
+
},
|
|
1996
3600
|
advisors: config.advisors.map((advisor) => {
|
|
1997
3601
|
const { apiKeysByProvider: _apiKeysByProvider, clearApiKey: _clearApiKey, apiKeyMetaByProvider: _meta, ...rest } = advisor;
|
|
1998
3602
|
const meta = {};
|
|
@@ -2032,8 +3636,19 @@ function hasSameCredentialScope(a, b) {
|
|
|
2032
3636
|
*/
|
|
2033
3637
|
function reconcileApiKeys(incoming, previous) {
|
|
2034
3638
|
const previousById = new Map(previous.advisors.map((advisor) => [advisor.id, advisor]));
|
|
3639
|
+
const incomingJev = normalizeJevConfig(incoming.trigger.jev);
|
|
3640
|
+
const previousJev = normalizeJevConfig(previous.trigger.jev);
|
|
3641
|
+
const jevApiKey = incomingJev?.apiKey || previousJev?.apiKey;
|
|
2035
3642
|
return {
|
|
2036
3643
|
...incoming,
|
|
3644
|
+
trigger: {
|
|
3645
|
+
...incoming.trigger,
|
|
3646
|
+
jev: {
|
|
3647
|
+
...incomingJev,
|
|
3648
|
+
...jevApiKey ? { apiKey: jevApiKey } : {},
|
|
3649
|
+
apiKeyMeta: void 0
|
|
3650
|
+
}
|
|
3651
|
+
},
|
|
2037
3652
|
advisors: incoming.advisors.map((advisor) => {
|
|
2038
3653
|
const { clearApiKey, ...advisorRest } = advisor;
|
|
2039
3654
|
const prev = previousById.get(advisor.id);
|
|
@@ -2101,7 +3716,14 @@ async function parseLookupParams(req) {
|
|
|
2101
3716
|
return params;
|
|
2102
3717
|
}
|
|
2103
3718
|
const ALLOWED_API_KEY_ENVS = new Set(Object.values(PROVIDER_PRESETS).map((preset) => preset.apiKeyEnv));
|
|
3719
|
+
ALLOWED_API_KEY_ENVS.add("TYPESAFE_API_KEY");
|
|
3720
|
+
ALLOWED_API_KEY_ENVS.add("OPENROUTER_API_KEY");
|
|
2104
3721
|
function validateSecurity(input) {
|
|
3722
|
+
if (input.trigger.jev?.baseURL) {
|
|
3723
|
+
const url = input.trigger.jev.baseURL.trim();
|
|
3724
|
+
if (!(url.startsWith("https://") || url.startsWith("http://127.0.0.1") || url.startsWith("http://localhost"))) return "Jev API 地址仅允许 https:// 或本机 http://127.0.0.1 / http://localhost";
|
|
3725
|
+
}
|
|
3726
|
+
if (input.trigger.jev?.apiKeyEnv && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(input.trigger.jev.apiKeyEnv.trim())) return `Jev API Key 环境变量名无效:${input.trigger.jev.apiKeyEnv}`;
|
|
2105
3727
|
const ids = /* @__PURE__ */ new Set();
|
|
2106
3728
|
for (const advisor of input.advisors) {
|
|
2107
3729
|
if (ids.has(advisor.id)) return `顾问 id 重复:${advisor.id}`;
|
|
@@ -2293,10 +3915,21 @@ async function handleModelsRequest(ctx, service, req, res) {
|
|
|
2293
3915
|
} catch (error) {
|
|
2294
3916
|
sendJson(res, 400, {
|
|
2295
3917
|
ok: false,
|
|
2296
|
-
error:
|
|
3918
|
+
error: describeConnectionError(error)
|
|
2297
3919
|
});
|
|
2298
3920
|
}
|
|
2299
3921
|
}
|
|
3922
|
+
/** 网络/HTTP 类失败的设置页可读文案;原始信息保留在括号里。 */
|
|
3923
|
+
function describeConnectionError(error) {
|
|
3924
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3925
|
+
if (/HTTP (401|403)\b/i.test(message)) return `密钥无效或没有访问权限(${message})`;
|
|
3926
|
+
if (/HTTP 404\b/i.test(message)) return `地址不存在,检查 baseURL 是否写对(${message})`;
|
|
3927
|
+
if (/ENOTFOUND|EAI_AGAIN/i.test(message)) return `找不到这个地址,检查域名拼写与网络(${message})`;
|
|
3928
|
+
if (/ECONNREFUSED/i.test(message)) return `对方拒绝了连接,检查地址与端口(${message})`;
|
|
3929
|
+
if (/ETIMEDOUT|\btimeout\b|aborted/i.test(message)) return `连接超时(${message})`;
|
|
3930
|
+
if (/fetch failed/i.test(message)) return `网络连接失败,检查网络后重试(${message})`;
|
|
3931
|
+
return `连接失败(${message})`;
|
|
3932
|
+
}
|
|
2300
3933
|
async function handleTestConnectionRequest(ctx, service, req, res) {
|
|
2301
3934
|
const params = await parseLookupParams(req);
|
|
2302
3935
|
const provider = params.provider ?? "";
|
|
@@ -2373,7 +4006,7 @@ async function handleTestConnectionRequest(ctx, service, req, res) {
|
|
|
2373
4006
|
} catch (error) {
|
|
2374
4007
|
sendJson(res, 400, {
|
|
2375
4008
|
ok: false,
|
|
2376
|
-
error:
|
|
4009
|
+
error: describeConnectionError(error)
|
|
2377
4010
|
});
|
|
2378
4011
|
}
|
|
2379
4012
|
}
|
|
@@ -2408,7 +4041,7 @@ function handleStreamRequest(req, res) {
|
|
|
2408
4041
|
});
|
|
2409
4042
|
}
|
|
2410
4043
|
function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
2411
|
-
const namespace = "advisor-group";
|
|
4044
|
+
const namespace = "dsh-advisor-group";
|
|
2412
4045
|
return async (req, res) => {
|
|
2413
4046
|
const rawUrl = req.url ?? "";
|
|
2414
4047
|
const url = new URL(rawUrl, "http://localhost");
|
|
@@ -2437,6 +4070,55 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
2437
4070
|
await sendProviders(ctx, res);
|
|
2438
4071
|
return;
|
|
2439
4072
|
}
|
|
4073
|
+
if (req.method === "POST" && (pathname === "/advisor-group/stop" || pathname === "/advisor-group/stop/")) {
|
|
4074
|
+
try {
|
|
4075
|
+
const raw = await readBody(req);
|
|
4076
|
+
const parsed = JSON.parse(raw);
|
|
4077
|
+
if (typeof parsed.sessionId !== "string" || !parsed.sessionId) {
|
|
4078
|
+
sendJson(res, 400, {
|
|
4079
|
+
ok: false,
|
|
4080
|
+
error: "缺少 sessionId"
|
|
4081
|
+
});
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
sendJson(res, 200, {
|
|
4085
|
+
ok: true,
|
|
4086
|
+
stopped: service.stopConsultation(parsed.sessionId)
|
|
4087
|
+
});
|
|
4088
|
+
} catch (error) {
|
|
4089
|
+
console.warn("[dsh-advisor-group] stop 请求处理失败:", error instanceof Error ? error.message : String(error));
|
|
4090
|
+
sendJson(res, 400, {
|
|
4091
|
+
ok: false,
|
|
4092
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
if (req.method === "POST" && (pathname === "/advisor-group/resume" || pathname === "/advisor-group/resume/")) {
|
|
4098
|
+
try {
|
|
4099
|
+
const raw = await readBody(req);
|
|
4100
|
+
const parsed = JSON.parse(raw);
|
|
4101
|
+
if (typeof parsed.sessionId !== "string" || !parsed.sessionId) {
|
|
4102
|
+
sendJson(res, 400, {
|
|
4103
|
+
ok: false,
|
|
4104
|
+
error: "缺少 sessionId"
|
|
4105
|
+
});
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
const result = service.resumeConsultation(parsed.sessionId);
|
|
4109
|
+
sendJson(res, result.ok ? 200 : 400, {
|
|
4110
|
+
ok: result.ok,
|
|
4111
|
+
...result.reason ? { error: result.reason } : {}
|
|
4112
|
+
});
|
|
4113
|
+
} catch (error) {
|
|
4114
|
+
console.warn("[dsh-advisor-group] resume 请求处理失败:", error instanceof Error ? error.message : String(error));
|
|
4115
|
+
sendJson(res, 400, {
|
|
4116
|
+
ok: false,
|
|
4117
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
2440
4122
|
if (req.method === "GET" && (pathname === "/advisor-group/shadow" || pathname === "/advisor-group/shadow/")) {
|
|
2441
4123
|
sendJson(res, 200, {
|
|
2442
4124
|
ok: true,
|
|
@@ -2445,12 +4127,20 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
2445
4127
|
return;
|
|
2446
4128
|
}
|
|
2447
4129
|
if (req.method === "GET" && (pathname === "/advisor-group/config" || pathname === "/advisor-group/config/")) {
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
4130
|
+
try {
|
|
4131
|
+
sendJson(res, 200, {
|
|
4132
|
+
ok: true,
|
|
4133
|
+
config: sanitizeConfig(service.getConfig()),
|
|
4134
|
+
revision: currentRevision(scopedCtx, namespace),
|
|
4135
|
+
dailyGuard: service.getDailyGuard()
|
|
4136
|
+
});
|
|
4137
|
+
} catch (error) {
|
|
4138
|
+
console.error("[dsh-advisor-group] config read failed:", error instanceof Error ? error.message : String(error));
|
|
4139
|
+
sendJson(res, 500, {
|
|
4140
|
+
ok: false,
|
|
4141
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
2454
4144
|
return;
|
|
2455
4145
|
}
|
|
2456
4146
|
if (req.method === "POST" && (pathname === "/advisor-group/config" || pathname === "/advisor-group/config/")) {
|
|
@@ -2459,7 +4149,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
2459
4149
|
const parsed = JSON.parse(raw);
|
|
2460
4150
|
const configInput = parsed.config ?? parsed;
|
|
2461
4151
|
const expectedRevision = typeof parsed.expectedRevision === "number" ? parsed.expectedRevision : currentRevision(scopedCtx, namespace);
|
|
2462
|
-
const reconciled = reconcileApiKeys(Config(configInput), service.getConfig());
|
|
4152
|
+
const reconciled = reconcileApiKeys(unwrapVolatileConfig(Config(configInput)), service.getConfig());
|
|
2463
4153
|
const securityError = validateSecurity(reconciled);
|
|
2464
4154
|
if (securityError) {
|
|
2465
4155
|
sendJson(res, 400, {
|
|
@@ -2468,8 +4158,10 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
2468
4158
|
});
|
|
2469
4159
|
return;
|
|
2470
4160
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
4161
|
+
const guarded = guardAdvisorPrompts(reconciled);
|
|
4162
|
+
reportPromptGuardFindings(guarded.findings);
|
|
4163
|
+
await scopedCtx.settings.replace(namespace, guarded.config, expectedRevision);
|
|
4164
|
+
service.setConfig(guarded.config);
|
|
2473
4165
|
sendJson(res, 200, {
|
|
2474
4166
|
ok: true,
|
|
2475
4167
|
config: sanitizeConfig(service.getConfig()),
|
|
@@ -2485,7 +4177,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
2485
4177
|
}
|
|
2486
4178
|
sendJson(res, 400, {
|
|
2487
4179
|
ok: false,
|
|
2488
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4180
|
+
error: `保存失败:请检查刚才修改项的格式(原始信息:${error instanceof Error ? error.message : String(error)})`
|
|
2489
4181
|
});
|
|
2490
4182
|
}
|
|
2491
4183
|
return;
|
|
@@ -2533,14 +4225,16 @@ function registerAdvisorSettingsAndRoutes(ctx, config, service) {
|
|
|
2533
4225
|
if (!inject) return;
|
|
2534
4226
|
inject(["settings", "webServer"], (scoped) => {
|
|
2535
4227
|
const scopedCtx = scoped;
|
|
2536
|
-
const namespace = "advisor-group";
|
|
2537
|
-
|
|
2538
|
-
|
|
4228
|
+
const namespace = "dsh-advisor-group";
|
|
4229
|
+
service.setConfig(config);
|
|
4230
|
+
const presentationDisposer = scopedCtx.settings.configure({ auto: false }, ctx.fiber);
|
|
2539
4231
|
service.setPersistEnabled(async (enabled) => {
|
|
2540
4232
|
await scopedCtx.settings.update(namespace, { enabled });
|
|
2541
4233
|
});
|
|
2542
|
-
const watchDisposer =
|
|
2543
|
-
|
|
4234
|
+
const watchDisposer = ctx.on("loader/volatile-update", () => {
|
|
4235
|
+
const guarded = guardAdvisorPrompts(unwrapVolatileConfig(ctx.fiber.config));
|
|
4236
|
+
reportPromptGuardFindings(guarded.findings);
|
|
4237
|
+
service.setConfig(guarded.config);
|
|
2544
4238
|
});
|
|
2545
4239
|
const authToken = randomUUID();
|
|
2546
4240
|
const indexTapDisposer = scopedCtx.webServer.tapIndex ? scopedCtx.webServer.tapIndex((html) => html.replace("<head>", `<head><script>globalThis.__ADVISOR_GROUP_TOKEN__ = ${JSON.stringify(authToken)}<\/script>`)) : null;
|
|
@@ -2553,6 +4247,7 @@ function registerAdvisorSettingsAndRoutes(ctx, config, service) {
|
|
|
2553
4247
|
watchDisposer();
|
|
2554
4248
|
routeDisposer();
|
|
2555
4249
|
indexTapDisposer?.();
|
|
4250
|
+
presentationDisposer();
|
|
2556
4251
|
}, "dsh-advisor-group: settings watcher + config routes");
|
|
2557
4252
|
});
|
|
2558
4253
|
}
|
|
@@ -2598,7 +4293,7 @@ function buildAdvisorPrompt(config, repeatPressure) {
|
|
|
2598
4293
|
/**
|
|
2599
4294
|
* Host-only registration for the advisor-group session event vocabulary.
|
|
2600
4295
|
*
|
|
2601
|
-
* The official
|
|
4296
|
+
* The official compatibility mechanism is the per-event
|
|
2602
4297
|
* `SessionEvent.ignorable: true` envelope marker: `dsh-session-persistence`
|
|
2603
4298
|
* refuses unknown event types unless `KNOWN_SESSION_EVENT_TYPES.has(type)`
|
|
2604
4299
|
* or the stored event is marked ignorable. However `Session.append()` exposes
|
|
@@ -2615,7 +4310,8 @@ const ADVISOR_GROUP_EVENT_TYPES = [
|
|
|
2615
4310
|
"advisor-group/start",
|
|
2616
4311
|
"advisor-group/message",
|
|
2617
4312
|
"advisor-group/delta",
|
|
2618
|
-
"advisor-group/end"
|
|
4313
|
+
"advisor-group/end",
|
|
4314
|
+
"advisor-group/resume"
|
|
2619
4315
|
];
|
|
2620
4316
|
function ensureAdvisorGroupSessionEventTypes() {
|
|
2621
4317
|
const known = KNOWN_SESSION_EVENT_TYPES;
|
|
@@ -2630,11 +4326,14 @@ const name = "dsh-advisor-group";
|
|
|
2630
4326
|
const inject = [
|
|
2631
4327
|
"tools",
|
|
2632
4328
|
"llm",
|
|
2633
|
-
"systemPrompt"
|
|
4329
|
+
"systemPrompt",
|
|
4330
|
+
"sessions"
|
|
2634
4331
|
];
|
|
2635
4332
|
function apply(ctx, config) {
|
|
2636
|
-
const
|
|
2637
|
-
|
|
4333
|
+
const guarded = guardAdvisorPrompts(unwrapVolatileConfig(config));
|
|
4334
|
+
reportPromptGuardFindings(guarded.findings);
|
|
4335
|
+
const service = new AdvisorGroupService(ctx, guarded.config);
|
|
4336
|
+
registerAdvisorSettingsAndRoutes(ctx, guarded.config, service);
|
|
2638
4337
|
for (const tool of registerAdvisorTools(service)) ctx.tools.register(tool);
|
|
2639
4338
|
const systemPrompt = ctx.systemPrompt;
|
|
2640
4339
|
if (systemPrompt) ctx.effect(() => systemPrompt.section({
|