dsh-advisor-group 0.0.1 → 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 -98
- package/README.zh.md +131 -103
- package/lib/client.js +307 -23
- package/lib/index.d.mts +21 -1
- package/lib/index.mjs +565 -87
- package/package.json +43 -30
package/lib/index.mjs
CHANGED
|
@@ -83,7 +83,23 @@ const DiscussionConfig = Schema.object({
|
|
|
83
83
|
const TriggerConfig = Schema.object({
|
|
84
84
|
requireClassifier: Schema.boolean().default(true).description("Run the pre-classifier before starting a consultation."),
|
|
85
85
|
allowWebFallback: Schema.boolean().default(true).description("When classifier says web search is better, return that hint to the main model."),
|
|
86
|
-
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({})
|
|
87
103
|
});
|
|
88
104
|
const UiConfig = Schema.object({
|
|
89
105
|
theme: Schema.union([
|
|
@@ -99,13 +115,19 @@ const QuotaConfig = Schema.object({
|
|
|
99
115
|
maxPerDay: Schema.natural().min(1).max(1e5).default(50).description("Max new consultations per UTC day (1–100000); ignored when enabled is false.")
|
|
100
116
|
});
|
|
101
117
|
const Config = Schema.object({
|
|
102
|
-
enabled: Schema.boolean().default(true),
|
|
103
|
-
discussion: DiscussionConfig,
|
|
104
|
-
trigger: TriggerConfig,
|
|
105
|
-
ui: UiConfig,
|
|
106
|
-
quota: QuotaConfig,
|
|
107
|
-
advisors: Schema.array(AdvisorConfig).default([])
|
|
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()
|
|
108
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
|
+
}
|
|
109
131
|
//#endregion
|
|
110
132
|
//#region src/providers/advisor-prompt.ts
|
|
111
133
|
/**
|
|
@@ -206,10 +228,7 @@ async function callViaCtxLlm(ctx, advisor, transcript, signal, onDelta, timeoutM
|
|
|
206
228
|
type: "text",
|
|
207
229
|
text: userText
|
|
208
230
|
}],
|
|
209
|
-
source: {
|
|
210
|
-
kind: "plugin",
|
|
211
|
-
plugin: "dsh-advisor-group"
|
|
212
|
-
}
|
|
231
|
+
source: { kind: "user" }
|
|
213
232
|
})];
|
|
214
233
|
const pair = timeoutSignalPair(timeoutMs ?? 12e4, signal);
|
|
215
234
|
const reasoningEffort = advisor.thinking === "disabled" ? "off" : advisor.reasoningEffort;
|
|
@@ -982,7 +1001,7 @@ async function streamOpenAICompatible(baseURL, advisor, apiKey, transcriptText,
|
|
|
982
1001
|
} catch (error) {
|
|
983
1002
|
const errName = error?.name ?? "UnknownError";
|
|
984
1003
|
const errMessage = error instanceof Error ? error.message : String(error);
|
|
985
|
-
console.warn(`[dsh-advisor-group] direct-http stream read error: name=${errName} msg=${errMessage} isTimeout=${pair.isTimeout()} signalAborted=${signal?.aborted ?? false} collectedChars=${content.length + thinking.length}`);
|
|
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}`);
|
|
986
1005
|
if (pair.isTimeout()) return {
|
|
987
1006
|
content,
|
|
988
1007
|
thinking,
|
|
@@ -1112,7 +1131,7 @@ async function streamAnthropic(baseURL, advisor, apiKey, transcriptText, authMod
|
|
|
1112
1131
|
} catch (error) {
|
|
1113
1132
|
const errName = error?.name ?? "UnknownError";
|
|
1114
1133
|
const errMessage = error instanceof Error ? error.message : String(error);
|
|
1115
|
-
console.warn(`[dsh-advisor-group] anthropic stream read error: name=${errName} msg=${errMessage} isTimeout=${pair.isTimeout()} signalAborted=${signal?.aborted ?? false} collectedChars=${content.length + thinking.length}`);
|
|
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}`);
|
|
1116
1135
|
if (pair.isTimeout()) return {
|
|
1117
1136
|
content,
|
|
1118
1137
|
thinking,
|
|
@@ -1249,7 +1268,7 @@ function transcriptOf(session) {
|
|
|
1249
1268
|
return `[${message.role === "main" ? "主模型" : message.advisorName ?? message.advisorId ?? "顾问"}]\n${message.content}`;
|
|
1250
1269
|
}).join("\n\n").slice(-24e3);
|
|
1251
1270
|
}
|
|
1252
|
-
async function generateWith(ctx, source, system, prompt, signal, timeoutMs = 6e5) {
|
|
1271
|
+
async function generateWith(ctx, source, sessionId, system, prompt, signal, timeoutMs = 6e5) {
|
|
1253
1272
|
try {
|
|
1254
1273
|
const options = {
|
|
1255
1274
|
provider: source.provider,
|
|
@@ -1260,10 +1279,7 @@ async function generateWith(ctx, source, system, prompt, signal, timeoutMs = 6e5
|
|
|
1260
1279
|
type: "text",
|
|
1261
1280
|
text: prompt
|
|
1262
1281
|
}],
|
|
1263
|
-
source: {
|
|
1264
|
-
kind: "plugin",
|
|
1265
|
-
plugin: "dsh-advisor-group"
|
|
1266
|
-
}
|
|
1282
|
+
source: { kind: "user" }
|
|
1267
1283
|
})],
|
|
1268
1284
|
temperature: .7,
|
|
1269
1285
|
maxTokens: 2048,
|
|
@@ -1281,23 +1297,38 @@ async function generateWith(ctx, source, system, prompt, signal, timeoutMs = 6e5
|
|
|
1281
1297
|
} else reasoningDeltas += 1;
|
|
1282
1298
|
}
|
|
1283
1299
|
const trimmed = text.trim();
|
|
1284
|
-
if (!trimmed) console.warn(`[dsh-advisor-group] 驱动模型空响应(${source.provider}/${source.model}
|
|
1300
|
+
if (!trimmed) console.warn(`[dsh-advisor-group] 驱动模型空响应(${source.provider}/${source.model}):`, sessionId, `chunks=${chunks} textDeltas=${textDeltas} reasoningDeltas=${reasoningDeltas};建议配置 discussion.driverModel 或提高 maxTokens`);
|
|
1285
1301
|
return trimmed || void 0;
|
|
1286
1302
|
} catch (error) {
|
|
1287
1303
|
if (signal?.aborted) throw error;
|
|
1288
|
-
console.warn(`[dsh-advisor-group] 驱动模型生成失败(${source.provider}/${source.model}):`, error instanceof Error ? error.message : String(error));
|
|
1304
|
+
console.warn(`[dsh-advisor-group] 驱动模型生成失败(${source.provider}/${source.model}):`, sessionId, error instanceof Error ? error.message : String(error));
|
|
1289
1305
|
return;
|
|
1290
1306
|
}
|
|
1291
1307
|
}
|
|
1292
1308
|
/** One deepening question for the next round (never throws; returns fallback text). */
|
|
1293
1309
|
async function generateDeepenQuestion(ctx, session, source, signal, timeoutMs) {
|
|
1294
1310
|
if (!source) return FALLBACK_DEEPEN_QUESTION;
|
|
1295
|
-
return await generateWith(ctx, source, DRIVER_SYSTEM_PROMPT, `以下是一轮顾问群的讨论记录:\n\n${transcriptOf(session)}\n\n请给出下一步的深入追问。`, signal, timeoutMs) || "请基于以上全部讨论,针对仍然存在的分歧、未验证的假设与主要风险,提出一个有深度的后续问题。";
|
|
1311
|
+
return await generateWith(ctx, source, session.id, DRIVER_SYSTEM_PROMPT, `以下是一轮顾问群的讨论记录:\n\n${transcriptOf(session)}\n\n请给出下一步的深入追问。`, signal, timeoutMs) || "请基于以上全部讨论,针对仍然存在的分歧、未验证的假设与主要风险,提出一个有深度的后续问题。";
|
|
1296
1312
|
}
|
|
1297
1313
|
/** Final synthesis after all rounds (never throws; returns fallback text). */
|
|
1298
1314
|
async function generateConclusion(ctx, session, source, signal, timeoutMs) {
|
|
1299
1315
|
if (!source) return FALLBACK_CONCLUSION;
|
|
1300
|
-
return await generateWith(ctx, source, CONCLUSION_SYSTEM_PROMPT, `以下是顾问群全部 ${session.messages.filter((m) => m.role === "advisor").length} 条顾问回答的讨论记录:\n\n${transcriptOf(session)}\n\n请给出综合结论。`, signal, timeoutMs) || "(⚠ 驱动模型未能生成综合结论:请主模型以上述顾问讨论为基础,自行总结共识、分歧与最可靠的行动建议。)";
|
|
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;
|
|
1301
1332
|
}
|
|
1302
1333
|
//#endregion
|
|
1303
1334
|
//#region src/session-log.ts
|
|
@@ -1625,7 +1656,8 @@ async function runAdvisorToolLoop(tools, streamOnce, executeTool, onToolStep) {
|
|
|
1625
1656
|
if (!READONLY_TOOL_NAMES.has(call.name)) console.warn(`[dsh-advisor-group] 顾问调用非只读工具(${call.name}):`, argsPreview);
|
|
1626
1657
|
const { outcome, reused } = await executeDeduped(call);
|
|
1627
1658
|
if (outcome.trim()) {
|
|
1628
|
-
const
|
|
1659
|
+
const flat = outcome.replace(/\n+/g, " ").trim();
|
|
1660
|
+
const preview = flat.length > 240 ? `${flat.slice(0, 240)}…` : flat;
|
|
1629
1661
|
onToolStep({
|
|
1630
1662
|
kind: "result",
|
|
1631
1663
|
name: call.name,
|
|
@@ -1649,7 +1681,8 @@ async function runAdvisorToolLoop(tools, streamOnce, executeTool, onToolStep) {
|
|
|
1649
1681
|
if (!READONLY_TOOL_NAMES.has(call.name)) console.warn(`[dsh-advisor-group] 顾问调用非只读工具(${call.name}):`, argsPreview);
|
|
1650
1682
|
const { outcome, reused } = await executeDeduped(call);
|
|
1651
1683
|
if (outcome.trim()) {
|
|
1652
|
-
const
|
|
1684
|
+
const flat = outcome.replace(/\n+/g, " ").trim();
|
|
1685
|
+
const preview = flat.length > 240 ? `${flat.slice(0, 240)}…` : flat;
|
|
1653
1686
|
onToolStep({
|
|
1654
1687
|
kind: "result",
|
|
1655
1688
|
name: call.name,
|
|
@@ -1798,12 +1831,14 @@ var AdvisorGroupService = class {
|
|
|
1798
1831
|
cwd: raw.cwd,
|
|
1799
1832
|
dshSessionId: raw.dshSessionId,
|
|
1800
1833
|
driverSource: raw.driverSource,
|
|
1801
|
-
stopReason: raw.stopReason,
|
|
1834
|
+
stopReason: raw.stopReason ?? "abort-error",
|
|
1802
1835
|
createdAt: raw.createdAt,
|
|
1803
1836
|
updatedAt: raw.updatedAt
|
|
1804
1837
|
};
|
|
1805
1838
|
this.sessions.set(session.id, session);
|
|
1806
|
-
} catch {
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
console.warn("[dsh-advisor-group] 会话快照读取失败,已跳过:", file, error instanceof Error ? error.message : String(error));
|
|
1841
|
+
}
|
|
1807
1842
|
}
|
|
1808
1843
|
/** Serialization-safe snapshot: advisor identity only, NEVER credentials. */
|
|
1809
1844
|
snapshotPayload(session) {
|
|
@@ -1850,9 +1885,18 @@ var AdvisorGroupService = class {
|
|
|
1850
1885
|
await promises.writeFile(tmp, payload, "utf8");
|
|
1851
1886
|
await promises.rename(tmp, target);
|
|
1852
1887
|
}).catch((error) => {
|
|
1853
|
-
console.warn("[dsh-advisor-group] 咨询快照持久化失败:", error instanceof Error ? error.message : String(error));
|
|
1888
|
+
console.warn("[dsh-advisor-group] 咨询快照持久化失败:", session.id, error instanceof Error ? error.message : String(error));
|
|
1854
1889
|
});
|
|
1855
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
|
+
}
|
|
1856
1900
|
/** Wait for queued durable writes before a host or test tears down storage. */
|
|
1857
1901
|
async flushPersistence() {
|
|
1858
1902
|
await Promise.all([this.dailyWriteChain, this.sessionWriteChain]);
|
|
@@ -1950,7 +1994,7 @@ var AdvisorGroupService = class {
|
|
|
1950
1994
|
store.enter(prepared);
|
|
1951
1995
|
return prepared;
|
|
1952
1996
|
} catch (error) {
|
|
1953
|
-
console.warn("[dsh-advisor-group] 重建会话句柄失败(降级为 detached):", error instanceof Error ? error.message : String(error));
|
|
1997
|
+
console.warn("[dsh-advisor-group] 重建会话句柄失败(降级为 detached):", session.id, error instanceof Error ? error.message : String(error));
|
|
1954
1998
|
const again = store.get(resumeId);
|
|
1955
1999
|
if (again) return again;
|
|
1956
2000
|
return this.buildDetachedSession(session, resumeId);
|
|
@@ -1996,7 +2040,9 @@ var AdvisorGroupService = class {
|
|
|
1996
2040
|
this.setEnabled(enabled);
|
|
1997
2041
|
if (this.persistEnabledCallback) try {
|
|
1998
2042
|
await this.persistEnabledCallback(enabled);
|
|
1999
|
-
} catch {
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
console.warn("[dsh-advisor-group] 启用开关保存失败(内存态未回滚):", error instanceof Error ? error.message : String(error));
|
|
2045
|
+
}
|
|
2000
2046
|
return this.enabled;
|
|
2001
2047
|
}
|
|
2002
2048
|
/**
|
|
@@ -2214,6 +2260,10 @@ var AdvisorGroupService = class {
|
|
|
2214
2260
|
stop.abort();
|
|
2215
2261
|
return true;
|
|
2216
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
|
+
}
|
|
2217
2267
|
/**
|
|
2218
2268
|
* Resume a stopped/interrupted consultation from its interruption point.
|
|
2219
2269
|
* Works for a user stop (live session) and after a dsh restart (snapshot
|
|
@@ -2252,7 +2302,9 @@ var AdvisorGroupService = class {
|
|
|
2252
2302
|
this.persistSession(session);
|
|
2253
2303
|
if (dshSession) appendAdvisorResume(dshSession, session.id);
|
|
2254
2304
|
this.ctx.emit("advisor-group/resume", { sessionId: session.id });
|
|
2255
|
-
this.runAutoPipeline(session, void 0, dshSession, resumeAgent).catch(() => {
|
|
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));
|
|
2256
2308
|
return { ok: true };
|
|
2257
2309
|
}
|
|
2258
2310
|
/** Atomic cross-instance lock (mkdir-based, with stale takeover). */
|
|
@@ -2327,18 +2379,37 @@ var AdvisorGroupService = class {
|
|
|
2327
2379
|
* advisor models talking to themselves back-to-back.
|
|
2328
2380
|
*/
|
|
2329
2381
|
async runOneRoundAndSummarize(session, signal, sessionLog, agent) {
|
|
2330
|
-
|
|
2331
|
-
session.
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
+
}
|
|
2342
2413
|
}
|
|
2343
2414
|
getRoundCount(session) {
|
|
2344
2415
|
const advisorCount = Math.max(1, session.advisors.length);
|
|
@@ -2512,7 +2583,9 @@ var AdvisorGroupService = class {
|
|
|
2512
2583
|
if (actionDescriptions.length > 0) message.actionDescriptions = actionDescriptions;
|
|
2513
2584
|
} catch (error) {
|
|
2514
2585
|
if (isAbortError(error, signal)) throw error;
|
|
2515
|
-
|
|
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)})`;
|
|
2516
2589
|
}
|
|
2517
2590
|
publish(session.id, {
|
|
2518
2591
|
advisorId: advisor.id,
|
|
@@ -2547,9 +2620,7 @@ var AdvisorGroupService = class {
|
|
|
2547
2620
|
corePoints: last ? this.extractCorePoints(last.content) : []
|
|
2548
2621
|
};
|
|
2549
2622
|
});
|
|
2550
|
-
const riskNotes = advisorMessages
|
|
2551
|
-
if (advisorMessages.some((message) => message.truncated !== void 0)) riskNotes.push("有顾问输出在流式过程中被截断(超时或网络中断),其正文可能不完整,且该顾问本轮可能只提供了部分意见。");
|
|
2552
|
-
if (cancelled) riskNotes.push("本次顾问群讨论已被用户或主模型取消,结论可能不完整。");
|
|
2623
|
+
const riskNotes = buildRiskNotes(advisorMessages, cancelled);
|
|
2553
2624
|
return {
|
|
2554
2625
|
sessionId: session.id,
|
|
2555
2626
|
question: session.question,
|
|
@@ -2904,6 +2975,167 @@ function matchAdvisors(text, advisors) {
|
|
|
2904
2975
|
return [...matches];
|
|
2905
2976
|
}
|
|
2906
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
|
|
2907
3139
|
//#region src/tools.ts
|
|
2908
3140
|
function registerAdvisorTools(service) {
|
|
2909
3141
|
return [defineTool({
|
|
@@ -2934,6 +3166,10 @@ function registerAdvisorTools(service) {
|
|
|
2934
3166
|
confidence: {
|
|
2935
3167
|
type: "number",
|
|
2936
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."
|
|
2937
3173
|
}
|
|
2938
3174
|
},
|
|
2939
3175
|
output: {
|
|
@@ -2989,7 +3225,18 @@ function registerAdvisorTools(service) {
|
|
|
2989
3225
|
const questionText = typeof args.question === "string" ? args.question : "";
|
|
2990
3226
|
const contextText = typeof args.context === "string" ? args.context : "";
|
|
2991
3227
|
const mentionedAdvisorGroup = /@顾问群|@顧問群/.test(`${questionText}\n${contextText}`);
|
|
2992
|
-
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
|
+
};
|
|
2993
3240
|
if (args.sessionId) {
|
|
2994
3241
|
if (!args.followUp) return {
|
|
2995
3242
|
sessionId: args.sessionId,
|
|
@@ -3004,6 +3251,12 @@ function registerAdvisorTools(service) {
|
|
|
3004
3251
|
skipped: true,
|
|
3005
3252
|
reason: "session-not-found"
|
|
3006
3253
|
};
|
|
3254
|
+
if (service.isSessionRunning(session.id)) return {
|
|
3255
|
+
sessionId: session.id,
|
|
3256
|
+
advice: "该顾问群会话正在运行中,请等本轮结束后再追问。",
|
|
3257
|
+
skipped: true,
|
|
3258
|
+
reason: "session-running"
|
|
3259
|
+
};
|
|
3007
3260
|
if (service.hasReachedMaxRounds(session)) return {
|
|
3008
3261
|
sessionId: session.id,
|
|
3009
3262
|
advice: "本轮已是顾问群讨论的最后一轮,请综合现有顾问意见给出最终答复,不要再追问。",
|
|
@@ -3011,11 +3264,11 @@ function registerAdvisorTools(service) {
|
|
|
3011
3264
|
reason: "max-rounds-reached"
|
|
3012
3265
|
};
|
|
3013
3266
|
service.appendFollowUp(session.id, args.followUp, sessionLog);
|
|
3014
|
-
await service.runOneRoundAndSummarize(session, exec.signal, sessionLog, exec.agent);
|
|
3267
|
+
const summary = await service.runOneRoundAndSummarize(session, exec.signal, sessionLog, exec.agent);
|
|
3015
3268
|
const canContinue = !service.hasReachedMaxRounds(session);
|
|
3016
3269
|
return {
|
|
3017
3270
|
sessionId: session.id,
|
|
3018
|
-
advice: formatConversation(session) + (canContinue ? "\n\n(如需继续,可再次传入 sessionId 和 followUp 追问。)" : "\n\n(讨论轮数已达上限,请综合顾问意见给出最终答复。)"),
|
|
3271
|
+
advice: formatConversation(session) + formatRiskNotes(summary) + (canContinue ? "\n\n(如需继续,可再次传入 sessionId 和 followUp 追问。)" : "\n\n(讨论轮数已达上限,请综合顾问意见给出最终答复。)"),
|
|
3019
3272
|
skipped: false
|
|
3020
3273
|
};
|
|
3021
3274
|
}
|
|
@@ -3026,17 +3279,41 @@ function registerAdvisorTools(service) {
|
|
|
3026
3279
|
reason: "missing-question"
|
|
3027
3280
|
};
|
|
3028
3281
|
if (cfg.trigger.requireClassifier && !mentionedAdvisorGroup && !forcedByRepeat) {
|
|
3029
|
-
|
|
3030
|
-
|
|
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 = {
|
|
3031
3302
|
ts: Date.now(),
|
|
3032
3303
|
question: args.question,
|
|
3033
3304
|
...typeof args.confidence === "number" ? { confidence: args.confidence } : {},
|
|
3034
3305
|
shouldEscalate: classification.shouldEscalate,
|
|
3035
3306
|
reason: classification.reason,
|
|
3036
3307
|
suggestWebSearch: classification.suggestWebSearch,
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
+
};
|
|
3039
3315
|
if (!classification.shouldEscalate) {
|
|
3316
|
+
recordShadow(false);
|
|
3040
3317
|
if (classification.suggestWebSearch && cfg.trigger.allowWebFallback) return {
|
|
3041
3318
|
sessionId: "",
|
|
3042
3319
|
advice: `前置分类器建议不启动顾问群:${classification.reason}`,
|
|
@@ -3050,29 +3327,45 @@ function registerAdvisorTools(service) {
|
|
|
3050
3327
|
reason: "classifier-rejected"
|
|
3051
3328
|
};
|
|
3052
3329
|
}
|
|
3053
|
-
}
|
|
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
|
+
};
|
|
3054
3340
|
if (args.advisorIds && args.advisorIds.length > 0) {
|
|
3055
3341
|
const configuredIds = new Set(cfg.advisors.map((advisor) => advisor.id));
|
|
3056
|
-
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 {
|
|
3057
3356
|
sessionId: "",
|
|
3058
|
-
advice:
|
|
3357
|
+
advice: costGuard.reason,
|
|
3059
3358
|
skipped: true,
|
|
3060
|
-
reason: "
|
|
3359
|
+
reason: "daily-limit-reached"
|
|
3061
3360
|
};
|
|
3062
3361
|
}
|
|
3063
|
-
const costGuard = service.tryStartConsultation();
|
|
3064
|
-
if (!costGuard.ok) return {
|
|
3065
|
-
sessionId: "",
|
|
3066
|
-
advice: costGuard.reason,
|
|
3067
|
-
skipped: true,
|
|
3068
|
-
reason: "daily-limit-reached"
|
|
3069
|
-
};
|
|
3070
3362
|
const session = service.createSession(args.question, args.context, args.advisorIds, sessionLog ? sessionLog.header.cwd : void 0, sessionLog ? sessionLog.id : void 0);
|
|
3071
3363
|
if (sessionLog) appendAdvisorStart(sessionLog, session);
|
|
3364
|
+
recordShadow(true);
|
|
3072
3365
|
const summary = await service.runAutoPipeline(session, exec.signal, sessionLog, exec.agent);
|
|
3073
3366
|
return {
|
|
3074
3367
|
sessionId: session.id,
|
|
3075
|
-
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(已达设置的最大讨论轮数,请参考上述顾问意见与综合结论给出最终答复。)" : ""),
|
|
3076
3369
|
skipped: false
|
|
3077
3370
|
};
|
|
3078
3371
|
}
|
|
@@ -3103,6 +3396,9 @@ function registerAdvisorTools(service) {
|
|
|
3103
3396
|
}
|
|
3104
3397
|
})];
|
|
3105
3398
|
}
|
|
3399
|
+
function formatRiskNotes(summary) {
|
|
3400
|
+
return summary.riskNotes.length > 0 ? `\n\n【风险提示】\n${summary.riskNotes.join("\n")}` : "";
|
|
3401
|
+
}
|
|
3106
3402
|
function formatConversation(session) {
|
|
3107
3403
|
const lines = [`顾问群对话(session: ${session.id})`, ""];
|
|
3108
3404
|
for (const message of session.messages) {
|
|
@@ -3129,11 +3425,135 @@ const STOP_REASON_TEXT = {
|
|
|
3129
3425
|
network: "网络中断"
|
|
3130
3426
|
};
|
|
3131
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
|
+
}
|
|
3536
|
+
//#endregion
|
|
3132
3537
|
//#region src/settings-api.ts
|
|
3133
3538
|
function sendJson(res, status, body) {
|
|
3134
3539
|
res.writeHead(status, { "content-type": "application/json" });
|
|
3135
3540
|
res.end(JSON.stringify(body));
|
|
3136
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
|
+
}
|
|
3137
3557
|
/** Mask a stored API key so it never leaves the host in plain text. */
|
|
3138
3558
|
function maskApiKey(apiKey) {
|
|
3139
3559
|
if (!apiKey) return apiKey;
|
|
@@ -3156,15 +3576,27 @@ function isMaskedApiKey(incoming, previous) {
|
|
|
3156
3576
|
/**
|
|
3157
3577
|
* Return a copy of the config safe to send to the browser.
|
|
3158
3578
|
*
|
|
3159
|
-
*
|
|
3579
|
+
* SecretField convergence: no key material (not even a mask) leaves
|
|
3160
3580
|
* the host. `apiKey` is returned empty and per-provider presence facts
|
|
3161
3581
|
* (`configured` + `last4`) ride in `apiKeyMetaByProvider`, so the card can
|
|
3162
3582
|
* render the official "已配置/未配置" badge and publish new/cleared keys
|
|
3163
3583
|
* without ever round-tripping a mask.
|
|
3164
3584
|
*/
|
|
3165
3585
|
function sanitizeConfig(config) {
|
|
3586
|
+
const jev = normalizeJevConfig(config.trigger.jev);
|
|
3166
3587
|
return {
|
|
3167
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
|
+
},
|
|
3168
3600
|
advisors: config.advisors.map((advisor) => {
|
|
3169
3601
|
const { apiKeysByProvider: _apiKeysByProvider, clearApiKey: _clearApiKey, apiKeyMetaByProvider: _meta, ...rest } = advisor;
|
|
3170
3602
|
const meta = {};
|
|
@@ -3204,8 +3636,19 @@ function hasSameCredentialScope(a, b) {
|
|
|
3204
3636
|
*/
|
|
3205
3637
|
function reconcileApiKeys(incoming, previous) {
|
|
3206
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;
|
|
3207
3642
|
return {
|
|
3208
3643
|
...incoming,
|
|
3644
|
+
trigger: {
|
|
3645
|
+
...incoming.trigger,
|
|
3646
|
+
jev: {
|
|
3647
|
+
...incomingJev,
|
|
3648
|
+
...jevApiKey ? { apiKey: jevApiKey } : {},
|
|
3649
|
+
apiKeyMeta: void 0
|
|
3650
|
+
}
|
|
3651
|
+
},
|
|
3209
3652
|
advisors: incoming.advisors.map((advisor) => {
|
|
3210
3653
|
const { clearApiKey, ...advisorRest } = advisor;
|
|
3211
3654
|
const prev = previousById.get(advisor.id);
|
|
@@ -3273,7 +3716,14 @@ async function parseLookupParams(req) {
|
|
|
3273
3716
|
return params;
|
|
3274
3717
|
}
|
|
3275
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");
|
|
3276
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}`;
|
|
3277
3727
|
const ids = /* @__PURE__ */ new Set();
|
|
3278
3728
|
for (const advisor of input.advisors) {
|
|
3279
3729
|
if (ids.has(advisor.id)) return `顾问 id 重复:${advisor.id}`;
|
|
@@ -3465,10 +3915,21 @@ async function handleModelsRequest(ctx, service, req, res) {
|
|
|
3465
3915
|
} catch (error) {
|
|
3466
3916
|
sendJson(res, 400, {
|
|
3467
3917
|
ok: false,
|
|
3468
|
-
error:
|
|
3918
|
+
error: describeConnectionError(error)
|
|
3469
3919
|
});
|
|
3470
3920
|
}
|
|
3471
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
|
+
}
|
|
3472
3933
|
async function handleTestConnectionRequest(ctx, service, req, res) {
|
|
3473
3934
|
const params = await parseLookupParams(req);
|
|
3474
3935
|
const provider = params.provider ?? "";
|
|
@@ -3545,7 +4006,7 @@ async function handleTestConnectionRequest(ctx, service, req, res) {
|
|
|
3545
4006
|
} catch (error) {
|
|
3546
4007
|
sendJson(res, 400, {
|
|
3547
4008
|
ok: false,
|
|
3548
|
-
error:
|
|
4009
|
+
error: describeConnectionError(error)
|
|
3549
4010
|
});
|
|
3550
4011
|
}
|
|
3551
4012
|
}
|
|
@@ -3580,7 +4041,7 @@ function handleStreamRequest(req, res) {
|
|
|
3580
4041
|
});
|
|
3581
4042
|
}
|
|
3582
4043
|
function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
3583
|
-
const namespace = "advisor-group";
|
|
4044
|
+
const namespace = "dsh-advisor-group";
|
|
3584
4045
|
return async (req, res) => {
|
|
3585
4046
|
const rawUrl = req.url ?? "";
|
|
3586
4047
|
const url = new URL(rawUrl, "http://localhost");
|
|
@@ -3625,6 +4086,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3625
4086
|
stopped: service.stopConsultation(parsed.sessionId)
|
|
3626
4087
|
});
|
|
3627
4088
|
} catch (error) {
|
|
4089
|
+
console.warn("[dsh-advisor-group] stop 请求处理失败:", error instanceof Error ? error.message : String(error));
|
|
3628
4090
|
sendJson(res, 400, {
|
|
3629
4091
|
ok: false,
|
|
3630
4092
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -3649,6 +4111,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3649
4111
|
...result.reason ? { error: result.reason } : {}
|
|
3650
4112
|
});
|
|
3651
4113
|
} catch (error) {
|
|
4114
|
+
console.warn("[dsh-advisor-group] resume 请求处理失败:", error instanceof Error ? error.message : String(error));
|
|
3652
4115
|
sendJson(res, 400, {
|
|
3653
4116
|
ok: false,
|
|
3654
4117
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -3664,12 +4127,20 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3664
4127
|
return;
|
|
3665
4128
|
}
|
|
3666
4129
|
if (req.method === "GET" && (pathname === "/advisor-group/config" || pathname === "/advisor-group/config/")) {
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
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
|
+
}
|
|
3673
4144
|
return;
|
|
3674
4145
|
}
|
|
3675
4146
|
if (req.method === "POST" && (pathname === "/advisor-group/config" || pathname === "/advisor-group/config/")) {
|
|
@@ -3678,7 +4149,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3678
4149
|
const parsed = JSON.parse(raw);
|
|
3679
4150
|
const configInput = parsed.config ?? parsed;
|
|
3680
4151
|
const expectedRevision = typeof parsed.expectedRevision === "number" ? parsed.expectedRevision : currentRevision(scopedCtx, namespace);
|
|
3681
|
-
const reconciled = reconcileApiKeys(Config(configInput), service.getConfig());
|
|
4152
|
+
const reconciled = reconcileApiKeys(unwrapVolatileConfig(Config(configInput)), service.getConfig());
|
|
3682
4153
|
const securityError = validateSecurity(reconciled);
|
|
3683
4154
|
if (securityError) {
|
|
3684
4155
|
sendJson(res, 400, {
|
|
@@ -3687,8 +4158,10 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3687
4158
|
});
|
|
3688
4159
|
return;
|
|
3689
4160
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
4161
|
+
const guarded = guardAdvisorPrompts(reconciled);
|
|
4162
|
+
reportPromptGuardFindings(guarded.findings);
|
|
4163
|
+
await scopedCtx.settings.replace(namespace, guarded.config, expectedRevision);
|
|
4164
|
+
service.setConfig(guarded.config);
|
|
3692
4165
|
sendJson(res, 200, {
|
|
3693
4166
|
ok: true,
|
|
3694
4167
|
config: sanitizeConfig(service.getConfig()),
|
|
@@ -3704,7 +4177,7 @@ function handleConfigRequest(ctx, scopedCtx, service, authToken) {
|
|
|
3704
4177
|
}
|
|
3705
4178
|
sendJson(res, 400, {
|
|
3706
4179
|
ok: false,
|
|
3707
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4180
|
+
error: `保存失败:请检查刚才修改项的格式(原始信息:${error instanceof Error ? error.message : String(error)})`
|
|
3708
4181
|
});
|
|
3709
4182
|
}
|
|
3710
4183
|
return;
|
|
@@ -3752,14 +4225,16 @@ function registerAdvisorSettingsAndRoutes(ctx, config, service) {
|
|
|
3752
4225
|
if (!inject) return;
|
|
3753
4226
|
inject(["settings", "webServer"], (scoped) => {
|
|
3754
4227
|
const scopedCtx = scoped;
|
|
3755
|
-
const namespace = "advisor-group";
|
|
3756
|
-
|
|
3757
|
-
|
|
4228
|
+
const namespace = "dsh-advisor-group";
|
|
4229
|
+
service.setConfig(config);
|
|
4230
|
+
const presentationDisposer = scopedCtx.settings.configure({ auto: false }, ctx.fiber);
|
|
3758
4231
|
service.setPersistEnabled(async (enabled) => {
|
|
3759
4232
|
await scopedCtx.settings.update(namespace, { enabled });
|
|
3760
4233
|
});
|
|
3761
|
-
const watchDisposer =
|
|
3762
|
-
|
|
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);
|
|
3763
4238
|
});
|
|
3764
4239
|
const authToken = randomUUID();
|
|
3765
4240
|
const indexTapDisposer = scopedCtx.webServer.tapIndex ? scopedCtx.webServer.tapIndex((html) => html.replace("<head>", `<head><script>globalThis.__ADVISOR_GROUP_TOKEN__ = ${JSON.stringify(authToken)}<\/script>`)) : null;
|
|
@@ -3772,6 +4247,7 @@ function registerAdvisorSettingsAndRoutes(ctx, config, service) {
|
|
|
3772
4247
|
watchDisposer();
|
|
3773
4248
|
routeDisposer();
|
|
3774
4249
|
indexTapDisposer?.();
|
|
4250
|
+
presentationDisposer();
|
|
3775
4251
|
}, "dsh-advisor-group: settings watcher + config routes");
|
|
3776
4252
|
});
|
|
3777
4253
|
}
|
|
@@ -3817,7 +4293,7 @@ function buildAdvisorPrompt(config, repeatPressure) {
|
|
|
3817
4293
|
/**
|
|
3818
4294
|
* Host-only registration for the advisor-group session event vocabulary.
|
|
3819
4295
|
*
|
|
3820
|
-
* The official
|
|
4296
|
+
* The official compatibility mechanism is the per-event
|
|
3821
4297
|
* `SessionEvent.ignorable: true` envelope marker: `dsh-session-persistence`
|
|
3822
4298
|
* refuses unknown event types unless `KNOWN_SESSION_EVENT_TYPES.has(type)`
|
|
3823
4299
|
* or the stored event is marked ignorable. However `Session.append()` exposes
|
|
@@ -3854,8 +4330,10 @@ const inject = [
|
|
|
3854
4330
|
"sessions"
|
|
3855
4331
|
];
|
|
3856
4332
|
function apply(ctx, config) {
|
|
3857
|
-
const
|
|
3858
|
-
|
|
4333
|
+
const guarded = guardAdvisorPrompts(unwrapVolatileConfig(config));
|
|
4334
|
+
reportPromptGuardFindings(guarded.findings);
|
|
4335
|
+
const service = new AdvisorGroupService(ctx, guarded.config);
|
|
4336
|
+
registerAdvisorSettingsAndRoutes(ctx, guarded.config, service);
|
|
3859
4337
|
for (const tool of registerAdvisorTools(service)) ctx.tools.register(tool);
|
|
3860
4338
|
const systemPrompt = ctx.systemPrompt;
|
|
3861
4339
|
if (systemPrompt) ctx.effect(() => systemPrompt.section({
|