dsh-plugin-ops-bundle 0.1.2 → 0.2.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 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.1.2",
3
+ "version": "0.2.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.1.2"
60
+ "dsh-plugin-ops-core": "0.2.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@deepseek-ai/cordis": "^4.0.0"