@sidady/dsh-steward 0.0.0-stage → 0.9.12

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/lib/brain.js ADDED
@@ -0,0 +1,167 @@
1
+ // L2+ 模型理解层:一句话 → 模型 → 结构化结论(仍然只产出意图,绝不执行)。
2
+ //
3
+ // 与 intent.js 的分工:
4
+ // intent.js 规则匹配,零 token、毫秒级、离线可用,覆盖常见说法。
5
+ // brain.js 模型理解,覆盖规则覆盖不到的自然语言;每一句都花一次 token,
6
+ // 所以是「用户显式选了模型理解」时才走这条路。
7
+ //
8
+ // 三条硬约束与 intent.js 完全一致,任何改动都不许放宽:
9
+ // 1. 只产出意图,绝不执行。返回值最终仍落到 plan → confirm → audit。
10
+ // 2. 模型输出必须是一个 JSON 对象,且 kind 只能取既有七类之一;解析不出来
11
+ // 就返回 clarify,绝不把模型的原话当指令。
12
+ // 3. 不 import 任何其它插件的内部模块(跨包版本漂移会重造问题①)。
13
+
14
+ export const KINDS = ['clean', 'backup', 'disable', 'enable', 'knowledge', 'ack', 'unack', 'report', 'goto'];
15
+
16
+ const SYSTEM = [
17
+ '你是 DSH 智能管家的语言层。你的唯一职责:把用户的一句话翻译成一个 JSON 对象。',
18
+ '可选 kind:clean(清理纯缓存)、backup(易失数据加密备份)、disable(停用插件)、',
19
+ 'enable(启用插件)、knowledge(沉淀本次结论)、ack(静音某条提醒)、',
20
+ 'unack(恢复某条提醒)、report(只想看看状况)、goto(只是想去某一页:总览/动作中心/审计趋势)。',
21
+ '输出格式(只输出 JSON,不要解释、不要 markdown 代码块):',
22
+ '{"kind":"clean|backup|disable|enable|knowledge|ack|unack|report|goto","plugin":"包名(仅 disable/enable)","findingId":"报告条目 id(仅 ack/unack)","tab":"overview|actions|audit(仅 goto)","reply":"一句中文回话,告诉用户你理解成了什么"}',
23
+ '规则:听不懂、或同时说了两件事、或拿不准参数时,kind 输出 "none",并在 reply 里问一句最关键的澄清。',
24
+ '你只做翻译,不做决定:永远不要说"我已经帮你清理/备份/停用了"。',
25
+ ].join('');
26
+
27
+ function textOf(chunk) {
28
+ if (!chunk || typeof chunk !== 'object') return '';
29
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') return chunk.text;
30
+ // 少数适配器把增量放在 content 上;只在明确是文本增量时采用。
31
+ if (chunk.type === 'text' && typeof chunk.text === 'string') return chunk.text;
32
+ return '';
33
+ }
34
+
35
+ /**
36
+ * 跑一次模型调用,把增量拼成纯文本。
37
+ * deps.llm 由调用方经 ctx.get('llm') 容错取得——本模块不持有 ctx。
38
+ */
39
+ export async function runModel(deps, options) {
40
+ const llm = deps && deps.llm;
41
+ if (!llm || typeof llm.stream !== 'function') {
42
+ const error = new Error('宿主没有可用的模型服务(llm 不可用)');
43
+ error.code = 'no-llm';
44
+ throw error;
45
+ }
46
+ const provider = String((options && options.provider) || '');
47
+ const model = String((options && options.model) || '');
48
+ if (!provider || !model) {
49
+ const error = new Error('没有选模型');
50
+ error.code = 'no-model';
51
+ throw error;
52
+ }
53
+ const signal = options && options.signal;
54
+ const messages = [{
55
+ role: 'user',
56
+ content: [{ type: 'text', text: String((options && options.prompt) || '') }],
57
+ }];
58
+ let text = '';
59
+ let finish = null;
60
+ for await (const chunk of llm.stream({
61
+ provider,
62
+ model,
63
+ messages,
64
+ system: (options && options.system) || SYSTEM,
65
+ maxTokens: (options && options.maxTokens) || 400,
66
+ signal,
67
+ })) {
68
+ if (!chunk || typeof chunk !== 'object') continue;
69
+ if (chunk.type === 'text-delta' || chunk.type === 'text') { text += textOf(chunk); continue; }
70
+ // 结束/错误块只记录,不抛:让调用方拿到已经收到的部分再判断。
71
+ if (chunk.type === 'finish' || chunk.type === 'message-stop' || chunk.type === 'error') finish = chunk;
72
+ }
73
+ return { text: text.trim(), finish };
74
+ }
75
+
76
+ /** 从模型输出里取出第一个 JSON 对象;容忍 ```json 围栏与前后废话。 */
77
+ export function extractJson(raw) {
78
+ const text = String(raw == null ? '' : raw).trim();
79
+ if (!text) return null;
80
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
81
+ const body = fenced ? fenced[1] : text;
82
+ const start = body.indexOf('{');
83
+ const end = body.lastIndexOf('}');
84
+ if (start < 0 || end <= start) return null;
85
+ try {
86
+ const parsed = JSON.parse(body.slice(start, end + 1));
87
+ return parsed && typeof parsed === 'object' ? parsed : null;
88
+ } catch { return null; }
89
+ }
90
+
91
+ /**
92
+ * 把模型输出规范成与 intent.js 相同形状的结论。
93
+ * 任何不合规的字段一律丢弃:宁可少一层理解,也不要多一条写入口。
94
+ */
95
+ export function normalizeBrainOutput(raw, ctx) {
96
+ const parsed = extractJson(raw);
97
+ const options = ctx || {};
98
+ if (!parsed) {
99
+ return {
100
+ ok: true, intent: null, via: 'model',
101
+ clarify: '模型这句我没解析成明确动作——不猜。可以换个说法,或者直接用规则式理解。',
102
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
103
+ };
104
+ }
105
+ const kind = String(parsed.kind || '').trim().toLowerCase();
106
+ const reply = typeof parsed.reply === 'string' ? parsed.reply.slice(0, 300) : '';
107
+ if (kind === 'none' || !KINDS.includes(kind)) {
108
+ return {
109
+ ok: true, intent: null, via: 'model',
110
+ clarify: reply || '这句我听不出要做什么——不猜。',
111
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
112
+ };
113
+ }
114
+ const params = {};
115
+ if (kind === 'disable' || kind === 'enable') {
116
+ const plugin = String(parsed.plugin || '').trim();
117
+ const known = Array.isArray(options.bundles) ? options.bundles : [];
118
+ const hit = known.find((b) => String(b && b.name || '').toLowerCase() === plugin.toLowerCase());
119
+ if (!plugin || !hit) {
120
+ return {
121
+ ok: true, intent: null, via: 'model',
122
+ clarify: plugin
123
+ ? `模型给的包名「${plugin}」不在当前已装列表里,不敢照着停/启。`
124
+ : '模型没给出要停用/启用哪个插件。',
125
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
126
+ };
127
+ }
128
+ params.plugin = hit.name;
129
+ }
130
+ if (kind === 'goto') {
131
+ const tab = String(parsed.tab || '').trim().toLowerCase();
132
+ const ok = ['overview', 'actions', 'audit'];
133
+ if (!ok.includes(tab)) {
134
+ return {
135
+ ok: true, intent: null, via: 'model',
136
+ clarify: '指南一页是哪里?面板只有「体检总览」「动作中心」「审计趋势」三页。',
137
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
138
+ };
139
+ }
140
+ params.tab = tab;
141
+ }
142
+ if (kind === 'ack' || kind === 'unack') {
143
+ const findings = options.report && Array.isArray(options.report.findings) ? options.report.findings : [];
144
+ const wanted = String(parsed.findingId || '').trim();
145
+ const hit = findings.find((f) => String(f && f.id || '') === wanted);
146
+ if (!hit) {
147
+ return {
148
+ ok: true, intent: null, via: 'model',
149
+ clarify: wanted
150
+ ? `模型指的条目「${wanted}」不在当前报告里。`
151
+ : '模型没指清要静音/恢复哪一条。',
152
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
153
+ };
154
+ }
155
+ params.id = hit.id;
156
+ params.title = hit.title;
157
+ }
158
+ return {
159
+ ok: true,
160
+ via: 'model',
161
+ intent: { kind, params, matched: ['model'], score: 0 },
162
+ reply: reply ? [reply] : [`我理解成「${kind}」,先给你出零写入预览。`],
163
+ suggestions: Array.isArray(options.suggestions) ? options.suggestions : [],
164
+ };
165
+ }
166
+
167
+ export default runModel;