dsh-plugin-ops-bundle 0.1.2 → 0.3.0
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/client.js +153 -2
- package/lib/host.js +53 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -66,6 +66,20 @@ var DICTS = {
|
|
|
66
66
|
chatSend: "发送",
|
|
67
67
|
chatThinking: "思考中…",
|
|
68
68
|
chatNoChannel: "未找到可用模型通道(dsh 未提供 llm 服务且未配置 API key)",
|
|
69
|
+
ragToggle: "增强检索(知识库)",
|
|
70
|
+
btnDeposit: "沉淀为知识",
|
|
71
|
+
depositOk: "已沉淀为知识条目",
|
|
72
|
+
depositFail: "沉淀失败:",
|
|
73
|
+
depositNoChat: "先进行一轮对话再沉淀",
|
|
74
|
+
knowledge: "知识库(插件排障经验)",
|
|
75
|
+
knowledgeHint: "bug 与修复经验沉淀为 md;开关开启时诊断对话自动检索命中条目",
|
|
76
|
+
knowledgePlaceholder: "搜索:错误信息 / 包名 / 规则…",
|
|
77
|
+
search: "搜索",
|
|
78
|
+
showAll: "全部",
|
|
79
|
+
knowledgeEmpty: "暂无知识条目(修复成功或对话沉淀后自动积累)",
|
|
80
|
+
knowledgeSearchEmpty: "无命中条目",
|
|
81
|
+
delete: "删除",
|
|
82
|
+
times: "出现 {n} 次",
|
|
69
83
|
loadFail: "加载失败:",
|
|
70
84
|
profile: "Profile",
|
|
71
85
|
home: "DSH_HOME"
|
|
@@ -104,6 +118,20 @@ var DICTS = {
|
|
|
104
118
|
chatSend: "Send",
|
|
105
119
|
chatThinking: "Thinking…",
|
|
106
120
|
chatNoChannel: "No model channel available (dsh exposes no llm service and no API key is configured)",
|
|
121
|
+
ragToggle: "Enhanced retrieval (knowledge base)",
|
|
122
|
+
btnDeposit: "Deposit as knowledge",
|
|
123
|
+
depositOk: "Deposited as a knowledge entry",
|
|
124
|
+
depositFail: "Deposit failed: ",
|
|
125
|
+
depositNoChat: "Chat once before depositing",
|
|
126
|
+
knowledge: "Knowledge base (plugin troubleshooting)",
|
|
127
|
+
knowledgeHint: "Bugs and fixes deposit as Markdown; the toggle retrieves matching entries for the chat",
|
|
128
|
+
knowledgePlaceholder: "Search: error text / package / rule…",
|
|
129
|
+
search: "Search",
|
|
130
|
+
showAll: "All",
|
|
131
|
+
knowledgeEmpty: "No entries yet (fixes and deposits accumulate here)",
|
|
132
|
+
knowledgeSearchEmpty: "No matches",
|
|
133
|
+
delete: "Delete",
|
|
134
|
+
times: "seen {n}×",
|
|
107
135
|
loadFail: "Load failed: ",
|
|
108
136
|
profile: "Profile",
|
|
109
137
|
home: "DSH_HOME"
|
|
@@ -200,6 +228,9 @@ function HealthSection() {
|
|
|
200
228
|
const [chat, setChat] = (0, import_react.useState)([]);
|
|
201
229
|
const [chatInput, setChatInput] = (0, import_react.useState)("");
|
|
202
230
|
const [chatState, setChatState] = (0, import_react.useState)("");
|
|
231
|
+
const [rag, setRag] = (0, import_react.useState)(false);
|
|
232
|
+
const [knowledge, setKnowledge] = (0, import_react.useState)([]);
|
|
233
|
+
const [knowledgeQuery, setKnowledgeQuery] = (0, import_react.useState)("");
|
|
203
234
|
const [busy, setBusy] = (0, import_react.useState)("");
|
|
204
235
|
const [error, setError] = (0, import_react.useState)("");
|
|
205
236
|
const load = (0, import_react.useCallback)(async (name, scanOnly = false) => {
|
|
@@ -223,6 +254,17 @@ function HealthSection() {
|
|
|
223
254
|
setBusy("");
|
|
224
255
|
}
|
|
225
256
|
}, []);
|
|
257
|
+
const loadKnowledge = (0, import_react.useCallback)(async () => {
|
|
258
|
+
try {
|
|
259
|
+
const res = await api("/api/knowledge");
|
|
260
|
+
setKnowledge(res.entries);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
263
|
+
}
|
|
264
|
+
}, []);
|
|
265
|
+
(0, import_react.useEffect)(() => {
|
|
266
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("dshops-rag") === "1") setRag(true);
|
|
267
|
+
}, []);
|
|
226
268
|
(0, import_react.useEffect)(() => {
|
|
227
269
|
let cancelled = false;
|
|
228
270
|
void (async () => {
|
|
@@ -233,6 +275,7 @@ function HealthSection() {
|
|
|
233
275
|
const initial = infoRes.defaultProfile ?? infoRes.profiles[0]?.name ?? "web";
|
|
234
276
|
setProfile(initial);
|
|
235
277
|
await load(initial);
|
|
278
|
+
await loadKnowledge();
|
|
236
279
|
} catch (e) {
|
|
237
280
|
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
238
281
|
}
|
|
@@ -240,7 +283,7 @@ function HealthSection() {
|
|
|
240
283
|
return () => {
|
|
241
284
|
cancelled = true;
|
|
242
285
|
};
|
|
243
|
-
}, [load]);
|
|
286
|
+
}, [load, loadKnowledge]);
|
|
244
287
|
const changeProfile = (name) => {
|
|
245
288
|
setProfile(name);
|
|
246
289
|
setPage(0);
|
|
@@ -290,7 +333,7 @@ function HealthSection() {
|
|
|
290
333
|
try {
|
|
291
334
|
const res = await api("/api/chat", {
|
|
292
335
|
method: "POST",
|
|
293
|
-
body: JSON.stringify({ profile, lang, messages: next })
|
|
336
|
+
body: JSON.stringify({ profile, lang, rag, messages: next })
|
|
294
337
|
});
|
|
295
338
|
if (res.ok) {
|
|
296
339
|
setChat([...next, { role: "assistant", content: res.reply }]);
|
|
@@ -304,6 +347,56 @@ function HealthSection() {
|
|
|
304
347
|
setChatState((state) => state === t.chatThinking ? "" : state);
|
|
305
348
|
}
|
|
306
349
|
};
|
|
350
|
+
const toggleRag = (next) => {
|
|
351
|
+
setRag(next);
|
|
352
|
+
if (typeof localStorage !== "undefined") localStorage.setItem("dshops-rag", next ? "1" : "0");
|
|
353
|
+
};
|
|
354
|
+
const searchKnowledge = async () => {
|
|
355
|
+
const query = knowledgeQuery.trim();
|
|
356
|
+
if (query === "") {
|
|
357
|
+
await loadKnowledge();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
setError("");
|
|
361
|
+
try {
|
|
362
|
+
const res = await api(`/api/knowledge?q=${encodeURIComponent(query)}&limit=10`);
|
|
363
|
+
setKnowledge(res.hits);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const removeKnowledge = async (id) => {
|
|
369
|
+
try {
|
|
370
|
+
await api("/api/knowledge/delete", { method: "POST", body: JSON.stringify({ id }) });
|
|
371
|
+
await loadKnowledge();
|
|
372
|
+
} catch (e) {
|
|
373
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const deposit = async () => {
|
|
377
|
+
if (chat.length === 0) {
|
|
378
|
+
setChatState(t.depositNoChat);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
setBusy("deposit");
|
|
382
|
+
setChatState("");
|
|
383
|
+
try {
|
|
384
|
+
const res = await api("/api/knowledge/deposit", {
|
|
385
|
+
method: "POST",
|
|
386
|
+
body: JSON.stringify({ lang, messages: chat })
|
|
387
|
+
});
|
|
388
|
+
if (res.ok) {
|
|
389
|
+
setChatState(t.depositOk);
|
|
390
|
+
await loadKnowledge();
|
|
391
|
+
} else {
|
|
392
|
+
setChatState(res.error ?? t.depositFail);
|
|
393
|
+
}
|
|
394
|
+
} catch (e) {
|
|
395
|
+
setChatState(`${t.depositFail}${e instanceof Error ? e.message : String(e)}`);
|
|
396
|
+
} finally {
|
|
397
|
+
setBusy("");
|
|
398
|
+
}
|
|
399
|
+
};
|
|
307
400
|
const visibleRows = rows.filter((row) => filter === "all" || severityOf(row, scan) === filter);
|
|
308
401
|
const totalPages = Math.max(1, Math.ceil(visibleRows.length / PAGE_SIZE));
|
|
309
402
|
const pageRows = visibleRows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
@@ -403,6 +496,57 @@ function HealthSection() {
|
|
|
403
496
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: page + 1 >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
|
|
404
497
|
] })
|
|
405
498
|
] }),
|
|
499
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
500
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
|
|
501
|
+
t.knowledge,
|
|
502
|
+
" ",
|
|
503
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", children: t.knowledgeHint })
|
|
504
|
+
] }),
|
|
505
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-chatbar", style: { marginTop: 0, marginBottom: 8 }, children: [
|
|
506
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
507
|
+
"input",
|
|
508
|
+
{
|
|
509
|
+
className: "dshops-input",
|
|
510
|
+
value: knowledgeQuery,
|
|
511
|
+
placeholder: t.knowledgePlaceholder,
|
|
512
|
+
onChange: (e) => setKnowledgeQuery(e.target.value),
|
|
513
|
+
onKeyDown: (e) => {
|
|
514
|
+
if (e.key === "Enter") void searchKnowledge();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
),
|
|
518
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", onClick: () => void searchKnowledge(), children: t.search }),
|
|
519
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
520
|
+
"button",
|
|
521
|
+
{
|
|
522
|
+
className: "dshops-btn",
|
|
523
|
+
onClick: () => {
|
|
524
|
+
setKnowledgeQuery("");
|
|
525
|
+
void loadKnowledge();
|
|
526
|
+
},
|
|
527
|
+
children: t.showAll
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
] }),
|
|
531
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-list", children: [
|
|
532
|
+
knowledge.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-item", children: [
|
|
533
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
|
|
534
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12 }, children: item.title }),
|
|
535
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", style: { fontSize: 11 }, children: [
|
|
536
|
+
item.occurrences === void 0 ? "" : format(t.times, { n: item.occurrences }),
|
|
537
|
+
item.score === void 0 ? "" : `${t.search}: ${item.score}`,
|
|
538
|
+
item.source ?? "",
|
|
539
|
+
item.createdAt === void 0 ? "" : item.createdAt.slice(0, 10)
|
|
540
|
+
].filter((text) => text !== "").join(" · ") }),
|
|
541
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", style: { marginLeft: "auto" }, onClick: () => void removeKnowledge(item.id), children: t.delete })
|
|
542
|
+
] }),
|
|
543
|
+
item.tags !== void 0 && item.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-dim", style: { fontSize: 11 }, children: item.tags.join(", ") }),
|
|
544
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, whiteSpace: "pre-wrap" }, children: (item.symptom ?? "") + (item.fix !== void 0 && item.fix !== "" ? `
|
|
545
|
+
→ ${item.fix}` : "") })
|
|
546
|
+
] }, item.id)),
|
|
547
|
+
knowledge.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-dim", children: t.knowledgeEmpty })
|
|
548
|
+
] })
|
|
549
|
+
] }),
|
|
406
550
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
407
551
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t.timeline }),
|
|
408
552
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-list", children: [
|
|
@@ -426,6 +570,13 @@ function HealthSection() {
|
|
|
426
570
|
" ",
|
|
427
571
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", children: t.chatHint })
|
|
428
572
|
] }),
|
|
573
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-head", style: { marginBottom: 8 }, children: [
|
|
574
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshops-dim", style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, cursor: "pointer" }, children: [
|
|
575
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: rag, onChange: (e) => toggleRag(e.target.checked) }),
|
|
576
|
+
t.ragToggle
|
|
577
|
+
] }),
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", style: { marginLeft: "auto" }, disabled: busy !== "", onClick: () => void deposit(), children: busy === "deposit" ? t.chatThinking : t.btnDeposit })
|
|
579
|
+
] }),
|
|
429
580
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-chat", children: [
|
|
430
581
|
chat.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-msg dshops-msg-assistant dshops-dim", children: t.chatWelcome }),
|
|
431
582
|
chat.map((message, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshops-msg dshops-msg-${message.role}`, children: message.content }, index))
|
package/lib/host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, dirname } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { PanelApiError, ScanError, buildChatContext, buildSystemPrompt, handlePanelApi, resolveDshPaths, } from 'dsh-plugin-ops-core';
|
|
3
|
+
import { PanelApiError, ScanError, buildChatContext, buildDepositPrompt, buildSystemPrompt, formatKnowledgeContext, handlePanelApi, parseDepositReply, resolveDshPaths, retrieveKnowledge, upsertKnowledge, } from 'dsh-plugin-ops-core';
|
|
4
4
|
/** HTTP prefix the bundle owns on the harness Web server. */
|
|
5
5
|
export const ROUTE_PREFIX = '/dsh-ops';
|
|
6
6
|
/**
|
|
@@ -74,6 +74,10 @@ export function createHostHandler(options) {
|
|
|
74
74
|
await handleChat(res, options, paths, url, body);
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
+
if (req.method === 'POST' && apiPath === '/api/knowledge/deposit') {
|
|
78
|
+
await handleDeposit(res, options, paths, body);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
77
81
|
const apiUrl = new URL(url);
|
|
78
82
|
apiUrl.pathname = apiPath;
|
|
79
83
|
const result = await handlePanelApi(req.method ?? 'GET', apiUrl, apiOptions, body);
|
|
@@ -93,6 +97,7 @@ async function handleChat(res, options, paths, url, body) {
|
|
|
93
97
|
const parsed = JSON.parse(body ?? '{}');
|
|
94
98
|
const profile = typeof parsed.profile === 'string' && parsed.profile !== '' ? parsed.profile : options.profile;
|
|
95
99
|
const lang = typeof parsed.lang === 'string' ? parsed.lang : 'zh';
|
|
100
|
+
const rag = parsed.rag === true;
|
|
96
101
|
const messages = Array.isArray(parsed.messages)
|
|
97
102
|
? parsed.messages
|
|
98
103
|
.filter((m) => typeof m === 'object' && m !== null
|
|
@@ -114,6 +119,13 @@ async function handleChat(res, options, paths, url, body) {
|
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
const context = await buildChatContext(paths, profile, options.config);
|
|
122
|
+
if (rag) {
|
|
123
|
+
const lastUser = [...messages].reverse().find((message) => message.role === 'user');
|
|
124
|
+
const query = `${lastUser?.content ?? ''}\n${context.findingsJson}`;
|
|
125
|
+
const hits = await retrieveKnowledge(paths, query, 3);
|
|
126
|
+
if (hits.length > 0)
|
|
127
|
+
context.knowledge = formatKnowledgeContext(hits);
|
|
128
|
+
}
|
|
117
129
|
const controller = new AbortController();
|
|
118
130
|
const timer = setTimeout(() => controller.abort(), 60000);
|
|
119
131
|
try {
|
|
@@ -124,4 +136,44 @@ async function handleChat(res, options, paths, url, body) {
|
|
|
124
136
|
clearTimeout(timer);
|
|
125
137
|
}
|
|
126
138
|
}
|
|
139
|
+
/** Summarize the current troubleshooting chat into one knowledge entry. */
|
|
140
|
+
async function handleDeposit(res, options, paths, body) {
|
|
141
|
+
const parsed = JSON.parse(body ?? '{}');
|
|
142
|
+
const lang = typeof parsed.lang === 'string' ? parsed.lang : 'zh';
|
|
143
|
+
const messages = Array.isArray(parsed.messages)
|
|
144
|
+
? parsed.messages
|
|
145
|
+
.filter((m) => typeof m === 'object' && m !== null
|
|
146
|
+
&& (m.role === 'user' || m.role === 'assistant')
|
|
147
|
+
&& typeof m.content === 'string')
|
|
148
|
+
.slice(-12)
|
|
149
|
+
: [];
|
|
150
|
+
if (messages.length === 0) {
|
|
151
|
+
json(res, 400, { error: 'no messages to deposit' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const channel = options.channel();
|
|
155
|
+
if (channel === null) {
|
|
156
|
+
json(res, 200, { ok: false, error: 'no model channel available; configure a model to summarize the conversation' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const timer = setTimeout(() => controller.abort(), 60000);
|
|
161
|
+
try {
|
|
162
|
+
const result = await channel.complete(buildDepositPrompt(messages, lang), [], controller.signal);
|
|
163
|
+
if (!result.ok) {
|
|
164
|
+
json(res, 502, { ok: false, error: result.error });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const draft = parseDepositReply(result.reply);
|
|
168
|
+
if (draft === null) {
|
|
169
|
+
json(res, 502, { ok: false, error: 'model reply was not a parsable knowledge entry' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const written = upsertKnowledge(paths, { ...draft, source: 'chat' });
|
|
173
|
+
json(res, written.ok ? 200 : 500, { ok: written.ok, id: written.id, problem: written.problem });
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
127
179
|
//# sourceMappingURL=host.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-ops-bundle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Embedded dsh bundle for dsh-plugin-ops: a settings-page health panel (scan, plugin rows, diagnosis chat) over the shared engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"dsh-plugin-ops-core": "0.
|
|
60
|
+
"dsh-plugin-ops-core": "0.3.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.0"
|