dsh-plugin-ops-bundle 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +469 -0
- package/lib/host.d.ts +24 -0
- package/lib/host.js +127 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +54 -0
- package/lib/llm-channel.d.ts +36 -0
- package/lib/llm-channel.js +45 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-plugin-ops contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# dsh-plugin-ops-bundle
|
|
2
|
+
|
|
3
|
+
Embedded [dsh-plugin-ops](https://github.com/f-infinite-z/dsh-plugin-ops)
|
|
4
|
+
bundle for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):
|
|
5
|
+
installs a **dsh-ops** health-check section into the harness Web settings page
|
|
6
|
+
(scan, findings, plugin-row enable/disable, fault timeline, diagnosis chat).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
dsh plugin --profile web add dsh-plugin-ops-bundle
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then restart the dsh web process. The section appears in Settings.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
- **Host half** (`lib/index.js`): a cordis plugin that waits for the optional
|
|
19
|
+
`webServer` service and serves the panel API under the `/dsh-ops` prefix,
|
|
20
|
+
reusing the shared engine (`dsh-plugin-ops-core`). The diagnosis chat prefers
|
|
21
|
+
the harness `ctx.llm` seam with the configured default model and falls back
|
|
22
|
+
to a direct OpenAI-compatible channel when the tree exposes no llm service.
|
|
23
|
+
- **Browser half** (`lib/client.js`): registers a `settings.section` through
|
|
24
|
+
`dsh.client` and fetches the same-origin `/dsh-ops/api/*` routes.
|
|
25
|
+
- Profiles without a Web server are unaffected: the host half activates but
|
|
26
|
+
registers nothing.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-plugin-ops bundle patch: mounts the dsh-ops host half as a Loader row.
|
|
2
|
+
# The browser half ships through package.json `dsh.client` and registers a
|
|
3
|
+
# settings-page section that fetches the same `/dsh-ops` API.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: dsh-ops-bundle
|
|
6
|
+
name: 'dsh-plugin-ops-bundle'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-plugin-ops-bundle", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
"use strict";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client/index.ts
|
|
23
|
+
var client_exports = {};
|
|
24
|
+
__export(client_exports, {
|
|
25
|
+
apply: () => apply,
|
|
26
|
+
inject: () => inject
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(client_exports);
|
|
29
|
+
|
|
30
|
+
// src/client/HealthSection.tsx
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/client/i18n.ts
|
|
34
|
+
var DICTS = {
|
|
35
|
+
zh: {
|
|
36
|
+
title: "健康检查",
|
|
37
|
+
subtitle: "dsh-ops 内嵌面板:扫描当前 profile 的插件健康状态",
|
|
38
|
+
refresh: "刷新",
|
|
39
|
+
scan: "扫描",
|
|
40
|
+
scanning: "扫描中…",
|
|
41
|
+
fix: "修复",
|
|
42
|
+
fixing: "修复中…",
|
|
43
|
+
clean: "未发现问题",
|
|
44
|
+
fatal: "致命",
|
|
45
|
+
warn: "警告",
|
|
46
|
+
info: "信息",
|
|
47
|
+
findings: "扫描结果",
|
|
48
|
+
rows: "插件行管理",
|
|
49
|
+
filterAll: "全部",
|
|
50
|
+
filterFatal: "致命",
|
|
51
|
+
filterWarn: "警告",
|
|
52
|
+
filterOk: "正常",
|
|
53
|
+
disable: "禁用",
|
|
54
|
+
enable: "启用",
|
|
55
|
+
protectedTag: "官方保护",
|
|
56
|
+
disabledTag: "已禁用",
|
|
57
|
+
prev: "上一页",
|
|
58
|
+
next: "下一页",
|
|
59
|
+
pageOf: "第 {cur} / {total} 页",
|
|
60
|
+
timeline: "故障时间线",
|
|
61
|
+
emptyTimeline: "暂无记录",
|
|
62
|
+
chat: "诊断对话",
|
|
63
|
+
chatHint: "基于当前 profile 的实时扫描结果;建议的修复请用上方白名单按钮执行",
|
|
64
|
+
chatWelcome: "我是诊断助手。已载入当前 profile 的扫描结果,想问什么?",
|
|
65
|
+
chatPlaceholder: "问:这些警告是什么意思?该怎么处理?",
|
|
66
|
+
chatSend: "发送",
|
|
67
|
+
chatThinking: "思考中…",
|
|
68
|
+
chatNoChannel: "未找到可用模型通道(dsh 未提供 llm 服务且未配置 API key)",
|
|
69
|
+
loadFail: "加载失败:",
|
|
70
|
+
profile: "Profile",
|
|
71
|
+
home: "DSH_HOME"
|
|
72
|
+
},
|
|
73
|
+
en: {
|
|
74
|
+
title: "Health Check",
|
|
75
|
+
subtitle: "Embedded dsh-ops panel: plugin health of the current profile",
|
|
76
|
+
refresh: "Refresh",
|
|
77
|
+
scan: "Scan",
|
|
78
|
+
scanning: "Scanning…",
|
|
79
|
+
fix: "Fix",
|
|
80
|
+
fixing: "Fixing…",
|
|
81
|
+
clean: "No findings",
|
|
82
|
+
fatal: "Fatal",
|
|
83
|
+
warn: "Warn",
|
|
84
|
+
info: "Info",
|
|
85
|
+
findings: "Scan results",
|
|
86
|
+
rows: "Plugin rows",
|
|
87
|
+
filterAll: "All",
|
|
88
|
+
filterFatal: "Fatal",
|
|
89
|
+
filterWarn: "Warn",
|
|
90
|
+
filterOk: "OK",
|
|
91
|
+
disable: "Disable",
|
|
92
|
+
enable: "Enable",
|
|
93
|
+
protectedTag: "official",
|
|
94
|
+
disabledTag: "disabled",
|
|
95
|
+
prev: "Prev",
|
|
96
|
+
next: "Next",
|
|
97
|
+
pageOf: "Page {cur} / {total}",
|
|
98
|
+
timeline: "Fault timeline",
|
|
99
|
+
emptyTimeline: "No records",
|
|
100
|
+
chat: "Diagnosis chat",
|
|
101
|
+
chatHint: "Based on a live scan of the current profile; use the whitelisted buttons above to apply suggested fixes",
|
|
102
|
+
chatWelcome: "Diagnosis assistant here. The current profile scan is loaded — what would you like to know?",
|
|
103
|
+
chatPlaceholder: "Ask: what do these warnings mean? How do I fix them?",
|
|
104
|
+
chatSend: "Send",
|
|
105
|
+
chatThinking: "Thinking…",
|
|
106
|
+
chatNoChannel: "No model channel available (dsh exposes no llm service and no API key is configured)",
|
|
107
|
+
loadFail: "Load failed: ",
|
|
108
|
+
profile: "Profile",
|
|
109
|
+
home: "DSH_HOME"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function detectLang() {
|
|
113
|
+
const nav = typeof navigator === "undefined" ? "" : navigator.language;
|
|
114
|
+
return nav.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
115
|
+
}
|
|
116
|
+
function format(template, values) {
|
|
117
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? `{${key}}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/client/HealthSection.tsx
|
|
121
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
122
|
+
var API = "/dsh-ops";
|
|
123
|
+
var PAGE_SIZE = 10;
|
|
124
|
+
async function api(path, init) {
|
|
125
|
+
const res = await fetch(`${API}${path}`, {
|
|
126
|
+
method: init?.method ?? "GET",
|
|
127
|
+
...init?.body === void 0 ? {} : { body: init.body },
|
|
128
|
+
headers: { "Content-Type": "application/json" }
|
|
129
|
+
});
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
var STYLE_ID = "dsh-ops-health-style";
|
|
135
|
+
var CSS = `
|
|
136
|
+
.dshops-root{display:flex;flex-direction:column;gap:14px;font-size:13px;line-height:1.5}
|
|
137
|
+
.dshops-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
138
|
+
.dshops-title{font-size:15px;font-weight:600;margin-right:auto}
|
|
139
|
+
.dshops-sub{opacity:.65;font-size:12px}
|
|
140
|
+
.dshops-btn{border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;border-radius:6px;padding:3px 10px;cursor:pointer;font-size:12px}
|
|
141
|
+
.dshops-btn:disabled{opacity:.5;cursor:default}
|
|
142
|
+
.dshops-btn-primary{background:#4d6bfe;border-color:#4d6bfe;color:#fff}
|
|
143
|
+
.dshops-select{border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;border-radius:6px;padding:3px 6px;font-size:12px}
|
|
144
|
+
.dshops-card{border:1px solid rgba(128,128,128,.25);border-radius:8px;padding:10px 12px}
|
|
145
|
+
.dshops-card h3{margin:0 0 8px;font-size:13px;font-weight:600}
|
|
146
|
+
.dshops-counts{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
147
|
+
.dshops-pill{border-radius:10px;padding:1px 8px;font-size:11px}
|
|
148
|
+
.dshops-pill-fatal{background:rgba(229,57,53,.15);color:#e53935}
|
|
149
|
+
.dshops-pill-warn{background:rgba(251,140,0,.18);color:#fb8c00}
|
|
150
|
+
.dshops-pill-info{background:rgba(30,136,229,.15);color:#1e88e5}
|
|
151
|
+
.dshops-pill-ok{background:rgba(67,160,71,.15);color:#43a047}
|
|
152
|
+
.dshops-list{display:flex;flex-direction:column;gap:6px}
|
|
153
|
+
.dshops-item{border-left:3px solid rgba(128,128,128,.35);padding:4px 8px;background:rgba(128,128,128,.06);border-radius:4px}
|
|
154
|
+
.dshops-item-fatal{border-left-color:#e53935;background:rgba(229,57,53,.07)}
|
|
155
|
+
.dshops-item-warn{border-left-color:#fb8c00;background:rgba(251,140,0,.07)}
|
|
156
|
+
.dshops-row{display:flex;align-items:center;gap:8px;padding:4px 6px;border-bottom:1px solid rgba(128,128,128,.12)}
|
|
157
|
+
.dshops-row:last-child{border-bottom:none}
|
|
158
|
+
.dshops-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
|
|
159
|
+
.dshops-dim{opacity:.6}
|
|
160
|
+
.dshops-chat{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow:auto}
|
|
161
|
+
.dshops-msg{max-width:85%;padding:6px 10px;border-radius:8px;white-space:pre-wrap}
|
|
162
|
+
.dshops-msg-user{align-self:flex-end;background:rgba(77,107,254,.18)}
|
|
163
|
+
.dshops-msg-assistant{align-self:flex-start;background:rgba(128,128,128,.12)}
|
|
164
|
+
.dshops-chatbar{display:flex;gap:6px;margin-top:8px}
|
|
165
|
+
.dshops-input{flex:1;border:1px solid rgba(128,128,128,.35);border-radius:6px;padding:5px 8px;background:transparent;color:inherit;font-size:12px}
|
|
166
|
+
.dshops-err{color:#e53935;font-size:12px}
|
|
167
|
+
.dshops-footer{display:flex;align-items:center;gap:8px;margin-top:8px}
|
|
168
|
+
`;
|
|
169
|
+
function useStyles() {
|
|
170
|
+
(0, import_react.useEffect)(() => {
|
|
171
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
172
|
+
const el = document.createElement("style");
|
|
173
|
+
el.id = STYLE_ID;
|
|
174
|
+
el.textContent = CSS;
|
|
175
|
+
document.head.appendChild(el);
|
|
176
|
+
}, []);
|
|
177
|
+
}
|
|
178
|
+
function severityOf(row, scan) {
|
|
179
|
+
if (scan === null) return "ok";
|
|
180
|
+
let worst = "ok";
|
|
181
|
+
for (const finding of scan.findings) {
|
|
182
|
+
if (finding.packageName === void 0) continue;
|
|
183
|
+
if (finding.packageName !== row.packageName && finding.packageName !== row.rowId) continue;
|
|
184
|
+
if (finding.severity === "fatal") return "fatal";
|
|
185
|
+
if (finding.severity === "warn") worst = "warn";
|
|
186
|
+
}
|
|
187
|
+
return worst;
|
|
188
|
+
}
|
|
189
|
+
function HealthSection() {
|
|
190
|
+
useStyles();
|
|
191
|
+
const [lang, setLang] = (0, import_react.useState)(detectLang);
|
|
192
|
+
const t = DICTS[lang];
|
|
193
|
+
const [info, setInfo] = (0, import_react.useState)(null);
|
|
194
|
+
const [profile, setProfile] = (0, import_react.useState)("");
|
|
195
|
+
const [scan, setScan] = (0, import_react.useState)(null);
|
|
196
|
+
const [rows, setRows] = (0, import_react.useState)([]);
|
|
197
|
+
const [events, setEvents] = (0, import_react.useState)([]);
|
|
198
|
+
const [filter, setFilter] = (0, import_react.useState)("all");
|
|
199
|
+
const [page, setPage] = (0, import_react.useState)(0);
|
|
200
|
+
const [chat, setChat] = (0, import_react.useState)([]);
|
|
201
|
+
const [chatInput, setChatInput] = (0, import_react.useState)("");
|
|
202
|
+
const [chatState, setChatState] = (0, import_react.useState)("");
|
|
203
|
+
const [busy, setBusy] = (0, import_react.useState)("");
|
|
204
|
+
const [error, setError] = (0, import_react.useState)("");
|
|
205
|
+
const load = (0, import_react.useCallback)(async (name, scanOnly = false) => {
|
|
206
|
+
setError("");
|
|
207
|
+
setBusy(scanOnly ? "scan" : "load");
|
|
208
|
+
try {
|
|
209
|
+
const encoded = encodeURIComponent(name);
|
|
210
|
+
const scanRes = await api(`/api/scan?profile=${encoded}`);
|
|
211
|
+
setScan(scanRes);
|
|
212
|
+
if (!scanOnly) {
|
|
213
|
+
const [rowsRes, memRes] = await Promise.all([
|
|
214
|
+
api(`/api/plugins?profile=${encoded}`),
|
|
215
|
+
api(`/api/memory?profile=${encoded}`)
|
|
216
|
+
]);
|
|
217
|
+
setRows(rowsRes.rows);
|
|
218
|
+
setEvents(memRes.events);
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
222
|
+
} finally {
|
|
223
|
+
setBusy("");
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
226
|
+
(0, import_react.useEffect)(() => {
|
|
227
|
+
let cancelled = false;
|
|
228
|
+
void (async () => {
|
|
229
|
+
try {
|
|
230
|
+
const infoRes = await api("/api/info");
|
|
231
|
+
if (cancelled) return;
|
|
232
|
+
setInfo(infoRes);
|
|
233
|
+
const initial = infoRes.defaultProfile ?? infoRes.profiles[0]?.name ?? "web";
|
|
234
|
+
setProfile(initial);
|
|
235
|
+
await load(initial);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled = true;
|
|
242
|
+
};
|
|
243
|
+
}, [load]);
|
|
244
|
+
const changeProfile = (name) => {
|
|
245
|
+
setProfile(name);
|
|
246
|
+
setPage(0);
|
|
247
|
+
setScan(null);
|
|
248
|
+
setRows([]);
|
|
249
|
+
setEvents([]);
|
|
250
|
+
setChat([]);
|
|
251
|
+
void load(name);
|
|
252
|
+
};
|
|
253
|
+
const alignFindings = scan?.findings.filter((f) => f.severity === "fatal" && f.fix.kind === "align-lockfile") ?? [];
|
|
254
|
+
const runFix = async () => {
|
|
255
|
+
if (profile === "") return;
|
|
256
|
+
setBusy("fix");
|
|
257
|
+
setError("");
|
|
258
|
+
try {
|
|
259
|
+
await api(`/api/fix/execute?profile=${encodeURIComponent(profile)}`, { method: "POST", body: "{}" });
|
|
260
|
+
await load(profile);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
263
|
+
} finally {
|
|
264
|
+
setBusy("");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const toggleRow = async (row) => {
|
|
268
|
+
setError("");
|
|
269
|
+
try {
|
|
270
|
+
const action = row.disabled ? "enable" : "disable";
|
|
271
|
+
await api(`/api/plugins/${action}?profile=${encodeURIComponent(profile)}`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: JSON.stringify({ rowId: row.rowId })
|
|
274
|
+
});
|
|
275
|
+
await load(profile, true);
|
|
276
|
+
const rowsRes = await api(`/api/plugins?profile=${encodeURIComponent(profile)}`);
|
|
277
|
+
setRows(rowsRes.rows);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const sendChat = async () => {
|
|
283
|
+
const text = chatInput.trim();
|
|
284
|
+
if (text === "" || busy === "chat") return;
|
|
285
|
+
const next = [...chat, { role: "user", content: text }];
|
|
286
|
+
setChat(next);
|
|
287
|
+
setChatInput("");
|
|
288
|
+
setBusy("chat");
|
|
289
|
+
setChatState(t.chatThinking);
|
|
290
|
+
try {
|
|
291
|
+
const res = await api("/api/chat", {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: JSON.stringify({ profile, lang, messages: next })
|
|
294
|
+
});
|
|
295
|
+
if (res.ok) {
|
|
296
|
+
setChat([...next, { role: "assistant", content: res.reply }]);
|
|
297
|
+
} else {
|
|
298
|
+
setChatState(res.error ?? t.chatNoChannel);
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
setChatState(`${t.loadFail}${e instanceof Error ? e.message : String(e)}`);
|
|
302
|
+
} finally {
|
|
303
|
+
setBusy("");
|
|
304
|
+
setChatState((state) => state === t.chatThinking ? "" : state);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const visibleRows = rows.filter((row) => filter === "all" || severityOf(row, scan) === filter);
|
|
308
|
+
const totalPages = Math.max(1, Math.ceil(visibleRows.length / PAGE_SIZE));
|
|
309
|
+
const pageRows = visibleRows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
310
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-root", children: [
|
|
311
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-head", children: [
|
|
312
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-title", children: t.title }),
|
|
313
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-sub", children: t.subtitle }),
|
|
314
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", onClick: () => setLang(lang === "zh" ? "en" : "zh"), children: lang === "zh" ? "EN" : "中文" }),
|
|
315
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: profile === "" || busy !== "", onClick: () => void load(profile), children: busy === "scan" ? t.scanning : t.refresh }),
|
|
316
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn dshops-btn-primary", disabled: profile === "" || busy !== "", onClick: () => void load(profile, true), children: t.scan })
|
|
317
|
+
] }),
|
|
318
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-head", children: [
|
|
319
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-dim", children: [
|
|
320
|
+
t.profile,
|
|
321
|
+
":"
|
|
322
|
+
] }),
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { className: "dshops-select", value: profile, onChange: (e) => changeProfile(e.target.value), children: (info?.profiles ?? []).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", { value: p.name, children: [
|
|
324
|
+
p.name,
|
|
325
|
+
" (",
|
|
326
|
+
p.bundles,
|
|
327
|
+
")"
|
|
328
|
+
] }, p.name)) }),
|
|
329
|
+
info !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim dshops-mono", children: info.home })
|
|
330
|
+
] }),
|
|
331
|
+
error !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-err", children: error }),
|
|
332
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
333
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t.findings }),
|
|
334
|
+
scan === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", children: busy === "load" ? t.scanning : "" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
335
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-counts", children: [
|
|
336
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-pill dshops-pill-fatal", children: [
|
|
337
|
+
t.fatal,
|
|
338
|
+
" ",
|
|
339
|
+
scan.counts.fatal
|
|
340
|
+
] }),
|
|
341
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-pill dshops-pill-warn", children: [
|
|
342
|
+
t.warn,
|
|
343
|
+
" ",
|
|
344
|
+
scan.counts.warn
|
|
345
|
+
] }),
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-pill dshops-pill-info", children: [
|
|
347
|
+
t.info,
|
|
348
|
+
" ",
|
|
349
|
+
scan.counts.info
|
|
350
|
+
] }),
|
|
351
|
+
scan.ok && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-pill dshops-pill-ok", children: t.clean }),
|
|
352
|
+
alignFindings.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: busy !== "", onClick: () => void runFix(), children: busy === "fix" ? t.fixing : t.fix })
|
|
353
|
+
] }),
|
|
354
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-list", style: { marginTop: 8 }, children: scan.findings.map((finding, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `dshops-item dshops-item-${finding.severity}`, children: [
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dshops-pill dshops-pill-${finding.severity}`, children: finding.severity }),
|
|
356
|
+
" ",
|
|
357
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-mono", children: [
|
|
358
|
+
"[",
|
|
359
|
+
finding.ruleId,
|
|
360
|
+
"]"
|
|
361
|
+
] }),
|
|
362
|
+
" ",
|
|
363
|
+
finding.packageName !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-mono", children: finding.packageName }),
|
|
364
|
+
" ",
|
|
365
|
+
finding.message,
|
|
366
|
+
finding.detail !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-dim", children: finding.detail })
|
|
367
|
+
] }, `${finding.ruleId}-${index}`)) })
|
|
368
|
+
] })
|
|
369
|
+
] }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
371
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
|
|
372
|
+
t.rows,
|
|
373
|
+
" ",
|
|
374
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { className: "dshops-select", value: filter, onChange: (e) => {
|
|
375
|
+
setFilter(e.target.value);
|
|
376
|
+
setPage(0);
|
|
377
|
+
}, children: [
|
|
378
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t.filterAll }),
|
|
379
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "fatal", children: t.filterFatal }),
|
|
380
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "warn", children: t.filterWarn }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "ok", children: t.filterOk })
|
|
382
|
+
] })
|
|
383
|
+
] }),
|
|
384
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
385
|
+
pageRows.map((row) => {
|
|
386
|
+
const severity = severityOf(row, scan);
|
|
387
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-row", children: [
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dshops-pill dshops-pill-${severity === "ok" ? "ok" : severity}`, children: severity }),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-mono", children: row.rowId }),
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim dshops-mono", children: row.packageName ?? "" }),
|
|
391
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: "auto" }, className: "dshops-dim", children: [
|
|
392
|
+
row.protected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-pill", children: t.protectedTag }),
|
|
393
|
+
row.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-pill dshops-pill-warn", children: t.disabledTag })
|
|
394
|
+
] }),
|
|
395
|
+
!row.protected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: busy !== "", onClick: () => void toggleRow(row), children: row.disabled ? t.enable : t.disable })
|
|
396
|
+
] }, `${row.rowId}-${row.source}`);
|
|
397
|
+
}),
|
|
398
|
+
visibleRows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-dim", children: t.clean })
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-footer", children: [
|
|
401
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: page <= 0, onClick: () => setPage((p) => Math.max(0, p - 1)), children: t.prev }),
|
|
402
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", children: format(t.pageOf, { cur: page + 1, total: totalPages }) }),
|
|
403
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn", disabled: page + 1 >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
|
|
404
|
+
] })
|
|
405
|
+
] }),
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
407
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t.timeline }),
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-list", children: [
|
|
409
|
+
events.map((event, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-item", children: [
|
|
410
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-mono", children: event.ts }),
|
|
411
|
+
" ",
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshops-mono", children: [
|
|
413
|
+
"[",
|
|
414
|
+
event.type,
|
|
415
|
+
"]"
|
|
416
|
+
] }),
|
|
417
|
+
" ",
|
|
418
|
+
event.detail
|
|
419
|
+
] }, `${event.ts}-${index}`)),
|
|
420
|
+
events.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-dim", children: t.emptyTimeline })
|
|
421
|
+
] })
|
|
422
|
+
] }),
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-card", children: [
|
|
424
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
|
|
425
|
+
t.chat,
|
|
426
|
+
" ",
|
|
427
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshops-dim", children: t.chatHint })
|
|
428
|
+
] }),
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-chat", children: [
|
|
430
|
+
chat.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-msg dshops-msg-assistant dshops-dim", children: t.chatWelcome }),
|
|
431
|
+
chat.map((message, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshops-msg dshops-msg-${message.role}`, children: message.content }, index))
|
|
432
|
+
] }),
|
|
433
|
+
chatState !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshops-err", style: { marginTop: 6 }, children: chatState }),
|
|
434
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshops-chatbar", children: [
|
|
435
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
436
|
+
"input",
|
|
437
|
+
{
|
|
438
|
+
className: "dshops-input",
|
|
439
|
+
value: chatInput,
|
|
440
|
+
placeholder: t.chatPlaceholder,
|
|
441
|
+
onChange: (e) => setChatInput(e.target.value),
|
|
442
|
+
onKeyDown: (e) => {
|
|
443
|
+
if (e.key === "Enter") void sendChat();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
),
|
|
447
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dshops-btn dshops-btn-primary", disabled: busy === "chat", onClick: () => void sendChat(), children: busy === "chat" ? t.chatThinking : t.chatSend })
|
|
448
|
+
] })
|
|
449
|
+
] })
|
|
450
|
+
] });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/client/index.ts
|
|
454
|
+
var inject = ["slots"];
|
|
455
|
+
function apply(ctx) {
|
|
456
|
+
ctx.slots.inject(
|
|
457
|
+
"settings.section",
|
|
458
|
+
() => ctx.slots.register(
|
|
459
|
+
{
|
|
460
|
+
name: "settings.section",
|
|
461
|
+
id: "dsh-ops",
|
|
462
|
+
order: 50,
|
|
463
|
+
label: () => "dsh-ops"
|
|
464
|
+
},
|
|
465
|
+
HealthSection
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return module.exports; } });
|
package/lib/host.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { type ModelChannel, type OpsConfig } from 'dsh-plugin-ops-core';
|
|
3
|
+
/** HTTP prefix the bundle owns on the harness Web server. */
|
|
4
|
+
export declare const ROUTE_PREFIX = "/dsh-ops";
|
|
5
|
+
export interface HostHandlerOptions {
|
|
6
|
+
home: string;
|
|
7
|
+
profile: string;
|
|
8
|
+
config: OpsConfig;
|
|
9
|
+
/** Chat channel for this host; null when no provider is available. */
|
|
10
|
+
channel: () => ModelChannel | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Derive the profile name from the Loader's base URL, which is the profile
|
|
14
|
+
* directory (`file:///.../profiles/<name>/`). Returns null when the URL does
|
|
15
|
+
* not sit under a `profiles` directory (nested include or unexpected boot).
|
|
16
|
+
*/
|
|
17
|
+
export declare function profileFromBaseUrl(baseUrl: string | undefined): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Build the HTTP handler served under {@link ROUTE_PREFIX}. Panel API routes
|
|
20
|
+
* are delegated to the shared engine; the chat route is answered here because
|
|
21
|
+
* it needs the host's model channel. Errors are contained and answered as
|
|
22
|
+
* JSON — a failing panel must never reach the plugin tree.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createHostHandler(options: HostHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
package/lib/host.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { PanelApiError, ScanError, buildChatContext, buildSystemPrompt, handlePanelApi, resolveDshPaths, } from 'dsh-plugin-ops-core';
|
|
4
|
+
/** HTTP prefix the bundle owns on the harness Web server. */
|
|
5
|
+
export const ROUTE_PREFIX = '/dsh-ops';
|
|
6
|
+
/**
|
|
7
|
+
* Derive the profile name from the Loader's base URL, which is the profile
|
|
8
|
+
* directory (`file:///.../profiles/<name>/`). Returns null when the URL does
|
|
9
|
+
* not sit under a `profiles` directory (nested include or unexpected boot).
|
|
10
|
+
*/
|
|
11
|
+
export function profileFromBaseUrl(baseUrl) {
|
|
12
|
+
if (baseUrl === undefined || baseUrl === '')
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const dir = fileURLToPath(baseUrl);
|
|
16
|
+
if (basename(dirname(dir)) !== 'profiles')
|
|
17
|
+
return null;
|
|
18
|
+
const name = basename(dir);
|
|
19
|
+
return /^[\w.-]+$/.test(name) ? name : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function json(res, status, body) {
|
|
26
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
27
|
+
res.end(JSON.stringify(body));
|
|
28
|
+
}
|
|
29
|
+
async function collectBody(req) {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
for await (const chunk of req) {
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
if (chunks.reduce((n, c) => n + c.length, 0) > 64 * 1024) {
|
|
34
|
+
throw new PanelApiError('request body too large', 413);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
38
|
+
}
|
|
39
|
+
/** Browsers on the same machine must not drive writes cross-site; bare CLI/curl requests are allowed. */
|
|
40
|
+
function sameOrigin(req) {
|
|
41
|
+
const origin = req.headers.origin;
|
|
42
|
+
if (origin === undefined)
|
|
43
|
+
return true;
|
|
44
|
+
try {
|
|
45
|
+
return new URL(origin).host === req.headers.host;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the HTTP handler served under {@link ROUTE_PREFIX}. Panel API routes
|
|
53
|
+
* are delegated to the shared engine; the chat route is answered here because
|
|
54
|
+
* it needs the host's model channel. Errors are contained and answered as
|
|
55
|
+
* JSON — a failing panel must never reach the plugin tree.
|
|
56
|
+
*/
|
|
57
|
+
export function createHostHandler(options) {
|
|
58
|
+
const paths = resolveDshPaths(options.profile, options.home);
|
|
59
|
+
const apiOptions = { paths, config: options.config, defaultProfile: options.profile };
|
|
60
|
+
return async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
if (!sameOrigin(req)) {
|
|
63
|
+
json(res, 403, { error: 'cross-origin request rejected' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
67
|
+
if (url.pathname !== ROUTE_PREFIX && !url.pathname.startsWith(`${ROUTE_PREFIX}/`)) {
|
|
68
|
+
json(res, 404, { error: `no route for ${req.method} ${url.pathname}` });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const apiPath = url.pathname.slice(ROUTE_PREFIX.length);
|
|
72
|
+
const body = req.method === 'POST' ? await collectBody(req) : undefined;
|
|
73
|
+
if (req.method === 'POST' && apiPath === '/api/chat') {
|
|
74
|
+
await handleChat(res, options, paths, url, body);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const apiUrl = new URL(url);
|
|
78
|
+
apiUrl.pathname = apiPath;
|
|
79
|
+
const result = await handlePanelApi(req.method ?? 'GET', apiUrl, apiOptions, body);
|
|
80
|
+
json(res, result.status, result.body);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof PanelApiError)
|
|
84
|
+
json(res, error.status, { error: error.message });
|
|
85
|
+
else if (error instanceof ScanError)
|
|
86
|
+
json(res, 400, { error: error.message });
|
|
87
|
+
else
|
|
88
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function handleChat(res, options, paths, url, body) {
|
|
93
|
+
const parsed = JSON.parse(body ?? '{}');
|
|
94
|
+
const profile = typeof parsed.profile === 'string' && parsed.profile !== '' ? parsed.profile : options.profile;
|
|
95
|
+
const lang = typeof parsed.lang === 'string' ? parsed.lang : 'zh';
|
|
96
|
+
const messages = Array.isArray(parsed.messages)
|
|
97
|
+
? parsed.messages
|
|
98
|
+
.filter((m) => typeof m === 'object' && m !== null
|
|
99
|
+
&& (m.role === 'user' || m.role === 'assistant')
|
|
100
|
+
&& typeof m.content === 'string')
|
|
101
|
+
.slice(-10)
|
|
102
|
+
: [];
|
|
103
|
+
if (messages.length === 0) {
|
|
104
|
+
json(res, 400, { error: 'no user messages' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const channel = options.channel();
|
|
108
|
+
if (channel === null) {
|
|
109
|
+
json(res, 200, {
|
|
110
|
+
ok: false,
|
|
111
|
+
reply: '',
|
|
112
|
+
error: 'no model channel available (no harness llm service and no provider key found); set DSH_OPS_LLM_API_KEY to override',
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const context = await buildChatContext(paths, profile, options.config);
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), 60000);
|
|
119
|
+
try {
|
|
120
|
+
const result = await channel.complete(buildSystemPrompt(context, lang), messages, controller.signal);
|
|
121
|
+
json(res, result.ok ? 200 : 502, { ok: result.ok, reply: result.reply, error: result.error });
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=host.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-ops host half: mounts the panel API under the `/dsh-ops` HTTP
|
|
3
|
+
* prefix of the running harness Web server. The browser half ships through
|
|
4
|
+
* `exports["./client"]` (a settings-page section). This half is deliberately
|
|
5
|
+
* thin and defensive: it imports no official runtime code, waits for the
|
|
6
|
+
* optional `webServer` service through `ctx.inject` (so profiles without a
|
|
7
|
+
* Web server still boot), and contains every error so a broken panel can
|
|
8
|
+
* never fail the plugin tree (the harness aborts the whole tree when one
|
|
9
|
+
* plugin fails).
|
|
10
|
+
*/
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
12
|
+
export declare const name = "dsh-ops-bundle";
|
|
13
|
+
/** Minimal view of the cordis context the host half relies on. */
|
|
14
|
+
export interface HostContext {
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
webServer: {
|
|
17
|
+
register(route: {
|
|
18
|
+
kind: 'prefix';
|
|
19
|
+
path: string;
|
|
20
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
21
|
+
}): () => void;
|
|
22
|
+
};
|
|
23
|
+
get(name: string): unknown;
|
|
24
|
+
inject(deps: string[], callback: (ctx: HostContext) => void): unknown;
|
|
25
|
+
effect(callback: () => (() => void) | void, label?: string): unknown;
|
|
26
|
+
}
|
|
27
|
+
export declare function apply(ctx: HostContext): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { OpenAiCompatibleChannel, readOpsConfig, resolveDshPaths, resolveModelConfig, } from 'dsh-plugin-ops-core';
|
|
2
|
+
import { ROUTE_PREFIX, createHostHandler, profileFromBaseUrl } from './host.js';
|
|
3
|
+
import { LlmChannel } from './llm-channel.js';
|
|
4
|
+
export const name = 'dsh-ops-bundle';
|
|
5
|
+
export function apply(ctx) {
|
|
6
|
+
try {
|
|
7
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
8
|
+
try {
|
|
9
|
+
registerPanel(webCtx);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error('[dsh-ops] host half disabled:', error);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
console.error('[dsh-ops] host half disabled:', error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function registerPanel(ctx) {
|
|
21
|
+
const profile = profileFromBaseUrl(ctx.baseUrl) ?? 'web';
|
|
22
|
+
const paths = resolveDshPaths(profile);
|
|
23
|
+
const read = readOpsConfig(paths.configFile);
|
|
24
|
+
if (!read.ok) {
|
|
25
|
+
console.error(`[dsh-ops] ignoring unreadable config: ${read.problem ?? 'unknown problem'}`);
|
|
26
|
+
}
|
|
27
|
+
const handler = createHostHandler({
|
|
28
|
+
home: paths.home,
|
|
29
|
+
profile,
|
|
30
|
+
config: read.ok ? read.config : {},
|
|
31
|
+
channel: () => resolveChannel(ctx, paths.home),
|
|
32
|
+
});
|
|
33
|
+
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }), 'dsh-ops-bundle: /dsh-ops prefix');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Prefer the harness llm seam with the configured default model; fall back to
|
|
37
|
+
* the direct OpenAI-compatible channel when the tree exposes no llm service or
|
|
38
|
+
* no default selection.
|
|
39
|
+
*/
|
|
40
|
+
function resolveChannel(ctx, home) {
|
|
41
|
+
const llm = ctx.get('llm');
|
|
42
|
+
const defaultModel = ctx.get('agentDefaultModel');
|
|
43
|
+
if (llm !== undefined && defaultModel !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
return new LlmChannel(llm, defaultModel.currentSelection());
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('[dsh-ops] llm channel unavailable, falling back to direct:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const config = resolveModelConfig(home);
|
|
52
|
+
return config === null ? null : new OpenAiCompatibleChannel(config);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ChatMessage, ChatReply, ModelChannel } from 'dsh-plugin-ops-core';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal duck-typed view of the harness llm service. The bundle never
|
|
4
|
+
* imports official runtime code (self-reliance): only the stable public
|
|
5
|
+
* method surface the panel uses.
|
|
6
|
+
*/
|
|
7
|
+
export interface LlmRuntimeLike {
|
|
8
|
+
stream(options: {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
messages: unknown[];
|
|
12
|
+
system?: string;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): AsyncIterable<{
|
|
16
|
+
type: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export interface ModelSelectionLike {
|
|
21
|
+
provider: string;
|
|
22
|
+
model: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Model channel over the official `ctx.llm` seam. Messages are plain objects
|
|
26
|
+
* matching the harness Message contract (`id`/`role`/`content`/`source`); the
|
|
27
|
+
* adapter reads `role` and `content`. Text deltas are concatenated into one
|
|
28
|
+
* reply; any failure is reported through {@link ChatReply} instead of throwing
|
|
29
|
+
* into the plugin tree.
|
|
30
|
+
*/
|
|
31
|
+
export declare class LlmChannel implements ModelChannel {
|
|
32
|
+
private readonly llm;
|
|
33
|
+
private readonly selection;
|
|
34
|
+
constructor(llm: LlmRuntimeLike, selection: ModelSelectionLike);
|
|
35
|
+
complete(system: string, messages: ChatMessage[], signal?: AbortSignal): Promise<ChatReply>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Model channel over the official `ctx.llm` seam. Messages are plain objects
|
|
4
|
+
* matching the harness Message contract (`id`/`role`/`content`/`source`); the
|
|
5
|
+
* adapter reads `role` and `content`. Text deltas are concatenated into one
|
|
6
|
+
* reply; any failure is reported through {@link ChatReply} instead of throwing
|
|
7
|
+
* into the plugin tree.
|
|
8
|
+
*/
|
|
9
|
+
export class LlmChannel {
|
|
10
|
+
llm;
|
|
11
|
+
selection;
|
|
12
|
+
constructor(llm, selection) {
|
|
13
|
+
this.llm = llm;
|
|
14
|
+
this.selection = selection;
|
|
15
|
+
}
|
|
16
|
+
async complete(system, messages, signal) {
|
|
17
|
+
try {
|
|
18
|
+
const options = {
|
|
19
|
+
provider: this.selection.provider,
|
|
20
|
+
model: this.selection.model,
|
|
21
|
+
system,
|
|
22
|
+
maxTokens: 800,
|
|
23
|
+
messages: messages.map((message) => ({
|
|
24
|
+
id: randomUUID(),
|
|
25
|
+
role: message.role,
|
|
26
|
+
content: [{ type: 'text', text: message.content }],
|
|
27
|
+
source: { kind: 'plugin', plugin: 'dsh-plugin-ops-bundle' },
|
|
28
|
+
})),
|
|
29
|
+
...(signal === undefined ? {} : { signal }),
|
|
30
|
+
};
|
|
31
|
+
let reply = '';
|
|
32
|
+
for await (const chunk of this.llm.stream(options)) {
|
|
33
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string')
|
|
34
|
+
reply += chunk.text;
|
|
35
|
+
}
|
|
36
|
+
return reply === ''
|
|
37
|
+
? { reply: '', ok: false, error: 'model returned no text' }
|
|
38
|
+
: { reply, ok: true };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return { reply: '', ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=llm-channel.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-ops-bundle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Embedded dsh bundle for dsh-plugin-ops: a settings-page health panel (scan, plugin rows, diagnosis chat) over the shared engine",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/f-infinite-z/dsh-plugin-ops.git",
|
|
13
|
+
"directory": "packages/bundle"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/f-infinite-z/dsh-plugin-ops#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"deepseek",
|
|
18
|
+
"harness",
|
|
19
|
+
"dsh",
|
|
20
|
+
"dsh-plugin",
|
|
21
|
+
"cordis",
|
|
22
|
+
"plugin",
|
|
23
|
+
"health-check",
|
|
24
|
+
"diagnostics"
|
|
25
|
+
],
|
|
26
|
+
"main": "lib/index.js",
|
|
27
|
+
"types": "lib/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./lib/index.d.ts",
|
|
31
|
+
"default": "./lib/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./client": {
|
|
34
|
+
"default": "./lib/client.js"
|
|
35
|
+
},
|
|
36
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"lib/index.js",
|
|
41
|
+
"lib/host.js",
|
|
42
|
+
"lib/llm-channel.js",
|
|
43
|
+
"lib/*.d.ts",
|
|
44
|
+
"lib/client.js",
|
|
45
|
+
"cordis.patch.yml"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
},
|
|
54
|
+
"client": {
|
|
55
|
+
"platform": "web",
|
|
56
|
+
"inject": []
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"dsh-plugin-ops-core": "0.1.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/react": "~18.3.1",
|
|
67
|
+
"react": "^18.2.0",
|
|
68
|
+
"tsup": "^8.5.1",
|
|
69
|
+
"typescript": "^7.0.2"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "tsc -p tsconfig.json && tsup --config tsup.client.config.ts",
|
|
73
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"clean": "node -e \"require('fs').rmSync('lib',{recursive:true,force:true})\""
|
|
76
|
+
}
|
|
77
|
+
}
|