dsh-skill-browser 1.5.4
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 +63 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +941 -0
- package/lib/index.js +436 -0
- package/lib/ledger-writer.js +416 -0
- package/lib/pick-dir.ps1 +14 -0
- package/lib/scan.js +220 -0
- package/lib/zh.js +84 -0
- package/package.json +53 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// dsh-skill-browser - host half.
|
|
2
|
+
// Serves a JSON API for the client panel: skill library scanning (root is
|
|
3
|
+
// user-configurable per machine - see detectRoot), category tree, Chinese
|
|
4
|
+
// one-liners, full SKILL.md bodies, and the skill-failure ledger engine.
|
|
5
|
+
// Pure host service; visual work lives in lib/client.js.
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { scanSkills, categoriesMeta, readSkillBody, parseLedgerTable, parseLedgerDetail, findLedgerFile, readLedgerAtPath, ledgerDefaultPath } from "./scan.js";
|
|
11
|
+
import {
|
|
12
|
+
createLedgerState, setLedgerRoot, setLedgerPath, recordSkillResult, reportFailure,
|
|
13
|
+
flushNow, initializeLedger, ledgerFilePath, autoLogFilePath, readAutoLog
|
|
14
|
+
} from "./ledger-writer.js";
|
|
15
|
+
import { ZH } from "./zh.js";
|
|
16
|
+
|
|
17
|
+
var PREFIX = "/dsh-skill-browser";
|
|
18
|
+
var VERSION = "1.5.1";
|
|
19
|
+
var CACHE_TTL_MS = 30 * 1000;
|
|
20
|
+
var DIAG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "diag.log");
|
|
21
|
+
var CONFIG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "config.json");
|
|
22
|
+
|
|
23
|
+
export var name = "dsh-skill-browser";
|
|
24
|
+
export var inject = ["webServer"];
|
|
25
|
+
|
|
26
|
+
function isLoopback(req) {
|
|
27
|
+
var a = (req.socket && req.socket.remoteAddress) || "";
|
|
28
|
+
var n = a.toLowerCase();
|
|
29
|
+
if (n === "::1") return true;
|
|
30
|
+
if (n.startsWith("::ffff:")) return n.slice(7).startsWith("127.");
|
|
31
|
+
return n.startsWith("127.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 运行时配置(设置页保存的覆盖项,存 ~/.dsh/data 下,无 BOM) ──
|
|
35
|
+
function loadRuntimeConfig() {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
38
|
+
var parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
39
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
40
|
+
} catch (e) { return {}; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveRuntimeConfig(cfg) {
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(dirname(CONFIG_FILE), { recursive: true });
|
|
46
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
|
|
47
|
+
return true;
|
|
48
|
+
} catch (e) { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Skill root resolution (shared-plugin safe: ZERO machine-specific paths):
|
|
52
|
+
// 1. runtime config override (settings page "技能库位置" / folder picker)
|
|
53
|
+
// 2. env DSH_SKILLS_DIR
|
|
54
|
+
// 3. cordis config.skillsDir
|
|
55
|
+
// 4. official default user root ~/.dsh/skills
|
|
56
|
+
// No hit at all → root stays empty and the panel asks the user to pick a
|
|
57
|
+
// folder in settings. Every machine teaches the plugin its own location.
|
|
58
|
+
function countSkills(dir) {
|
|
59
|
+
try {
|
|
60
|
+
var entries = readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
var n = 0;
|
|
62
|
+
for (var i = 0; i < entries.length; i++) {
|
|
63
|
+
var e = entries[i];
|
|
64
|
+
if (!e.isDirectory()) continue;
|
|
65
|
+
if (e.name.charAt(0) === "_" || e.name.charAt(0) === ".") continue;
|
|
66
|
+
if (existsSync(join(dir, e.name, "SKILL.md"))) n++;
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
} catch (e) { return 0; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectRoot(config, runtime) {
|
|
73
|
+
var candidates = [];
|
|
74
|
+
function push(p, source) {
|
|
75
|
+
if (!p || typeof p !== "string") return;
|
|
76
|
+
candidates.push({ path: p, source: source, missing: !existsSync(p), count: 0 });
|
|
77
|
+
}
|
|
78
|
+
var rc = runtime || {};
|
|
79
|
+
push(rc.skillsDir, "settings-override");
|
|
80
|
+
push(process.env.DSH_SKILLS_DIR, "env");
|
|
81
|
+
push(config && typeof config.skillsDir === "string" ? config.skillsDir : "", "config");
|
|
82
|
+
push(join(homedir(), ".dsh", "skills"), "auto-detect");
|
|
83
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
84
|
+
if (!candidates[i].missing) candidates[i].count = countSkills(candidates[i].path);
|
|
85
|
+
}
|
|
86
|
+
if (!candidates.length) return { root: "", source: "none", candidates: [] };
|
|
87
|
+
// explicit sources: user intent wins as long as the path exists
|
|
88
|
+
for (var j = 0; j < candidates.length; j++) {
|
|
89
|
+
var c = candidates[j];
|
|
90
|
+
if (c.source !== "auto-detect" && !c.missing) return { root: c.path, source: c.source, candidates: candidates };
|
|
91
|
+
}
|
|
92
|
+
// auto-detect: only the official default root; prefer non-empty over empty
|
|
93
|
+
var best = null;
|
|
94
|
+
for (var k = 0; k < candidates.length; k++) {
|
|
95
|
+
var a = candidates[k];
|
|
96
|
+
if (a.missing) continue;
|
|
97
|
+
if (!best || a.count > best.count) best = a;
|
|
98
|
+
}
|
|
99
|
+
if (!best) return { root: "", source: "none", candidates: candidates };
|
|
100
|
+
return { root: best.path, source: best.count > 0 ? "auto-detect" : "auto-detect(empty)", candidates: candidates };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendJson(res, code, obj) {
|
|
104
|
+
res.writeHead(code, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
105
|
+
res.end(JSON.stringify(obj));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 系统目录选择框(Windows FolderBrowserDialog,宿主端直弹) ──
|
|
109
|
+
// Electron renderer 的 File API 拿不到可靠真实路径(Electron 32+ 移除
|
|
110
|
+
// File.path,webUtils 依赖 preload 暴露,webkitdirectory 实测失败),改由
|
|
111
|
+
// 宿主端 spawn powershell -STA -File pick-dir.ps1 弹 WinForms 对话框。
|
|
112
|
+
// ⚠ ps1 必须落盘执行:-Command 内联会在嵌套 shell 层被吞 $ 变量。
|
|
113
|
+
// 返回 Promise<string|null>;用户取消/超时为 null。
|
|
114
|
+
import { fileURLToPath } from "node:url";
|
|
115
|
+
|
|
116
|
+
function pickDirViaPowerShell() {
|
|
117
|
+
return new Promise(function (resolve) {
|
|
118
|
+
var script = join(dirname(fileURLToPath(import.meta.url)), "pick-dir.ps1");
|
|
119
|
+
var child;
|
|
120
|
+
try {
|
|
121
|
+
// ⚠ 不设 windowsHide:true(SW_HIDE 链);控制台窗口用 -WindowStyle Hidden
|
|
122
|
+
// 隐藏(只藏控制台,不影响 WinForms 对话框)。弹框期间控制台不闪现。
|
|
123
|
+
child = spawn("powershell.exe", ["-NoProfile", "-STA", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass", "-File", script], {
|
|
124
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
resolve(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
var out = "";
|
|
131
|
+
var settled = false;
|
|
132
|
+
function finish(path) {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
resolve(path);
|
|
136
|
+
}
|
|
137
|
+
child.stdout.on("data", function (d) { out += d; });
|
|
138
|
+
child.on("close", function () {
|
|
139
|
+
var p = out.trim();
|
|
140
|
+
finish(p === "" ? null : p);
|
|
141
|
+
});
|
|
142
|
+
child.on("error", function () { finish(null); });
|
|
143
|
+
// 保险丝:10 分钟无操作视为放弃(用户可能一直不关对话框)
|
|
144
|
+
setTimeout(function () {
|
|
145
|
+
if (!settled) { try { child.kill(); } catch (e) {} finish(null); }
|
|
146
|
+
}, 10 * 60 * 1000);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function apply(ctx, config) {
|
|
151
|
+
var runtime = loadRuntimeConfig();
|
|
152
|
+
var detected = detectRoot(config, runtime);
|
|
153
|
+
var root = detected.root;
|
|
154
|
+
var cache = { ts: 0, payload: null };
|
|
155
|
+
var cacheLedger = { ts: 0, payload: null };
|
|
156
|
+
ctx.logger.info("dsh-skill-browser: host mounted, skill root = " + (root || "(not found)") + " (source: " + detected.source + ")");
|
|
157
|
+
|
|
158
|
+
// ── 失效台账:搜索已有 + 初始化 + 自动登记 ──────────────────
|
|
159
|
+
// 台账发现:先全库搜索(失效台账/失败台账等关键词文件,约定路径优先);
|
|
160
|
+
// 搜到 → 直接用它(人工内容不动,首接管备份一次);没搜到 → 在约定路径
|
|
161
|
+
// 创建默认台账(对齐现行格式、零隐私、内置登记口令)+ skill-master 骨架。
|
|
162
|
+
var ledger = createLedgerState({ root: root, logger: ctx.logger });
|
|
163
|
+
var ledgerFile = null; // 解析到的台账实际路径(缓存)
|
|
164
|
+
function resolveLedger(force) {
|
|
165
|
+
if (!root) return null;
|
|
166
|
+
if (!force && ledgerFile) return ledgerFile;
|
|
167
|
+
var found = findLedgerFile(root);
|
|
168
|
+
if (found) {
|
|
169
|
+
setLedgerPath(ledger, found);
|
|
170
|
+
ledgerFile = found;
|
|
171
|
+
ctx.logger.info("dsh-skill-browser: ledger found at " + found);
|
|
172
|
+
return ledgerFile;
|
|
173
|
+
}
|
|
174
|
+
// 未搜索到:初始化(创建默认台账 + skill-master 骨架),台账落在约定路径
|
|
175
|
+
initializeLedger(ledger);
|
|
176
|
+
ledgerFile = ledgerDefaultPath(root);
|
|
177
|
+
setLedgerPath(ledger, ledgerFile);
|
|
178
|
+
ctx.logger.info("dsh-skill-browser: no ledger found, default created at " + ledgerFile);
|
|
179
|
+
return ledgerFile;
|
|
180
|
+
}
|
|
181
|
+
if (root) resolveLedger(false);
|
|
182
|
+
try {
|
|
183
|
+
ctx.on("tools/result", function (exec, result) {
|
|
184
|
+
try {
|
|
185
|
+
if (runtime.ledgerEnabled === false) return;
|
|
186
|
+
recordSkillResult(ledger, exec, result);
|
|
187
|
+
} catch (e) { /* 监听器绝不影响工具调用本身 */ }
|
|
188
|
+
});
|
|
189
|
+
ctx.logger.info("dsh-skill-browser: ledger auto-recorder armed (tools/result)");
|
|
190
|
+
} catch (e) {
|
|
191
|
+
ctx.logger.warn("dsh-skill-browser: tools/result listener failed: " + (e && e.message));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildPayload() {
|
|
195
|
+
var skills = scanSkills(root, ZH);
|
|
196
|
+
// Per-category counts, emitted in CATS order so the client renders a
|
|
197
|
+
// stable chip sequence; categories with zero skills are dropped.
|
|
198
|
+
var meta = categoriesMeta();
|
|
199
|
+
var counts = {};
|
|
200
|
+
for (var i = 0; i < skills.length; i++) {
|
|
201
|
+
counts[skills[i].cat] = (counts[skills[i].cat] || 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
var categories = [];
|
|
204
|
+
for (var j = 0; j < meta.length; j++) {
|
|
205
|
+
var c = meta[j];
|
|
206
|
+
if (!counts[c.id]) continue;
|
|
207
|
+
categories.push({ id: c.id, label: c.label, icon: c.icon, subs: c.subs || [] });
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
root: root,
|
|
212
|
+
total: skills.length,
|
|
213
|
+
generatedAt: new Date().toISOString(),
|
|
214
|
+
categories: categories,
|
|
215
|
+
skills: skills
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getPayload(force) {
|
|
220
|
+
var now = Date.now();
|
|
221
|
+
if (!force && cache.payload && now - cache.ts < CACHE_TTL_MS) return cache.payload;
|
|
222
|
+
if (!root) {
|
|
223
|
+
var re = detectRoot(config, runtime);
|
|
224
|
+
root = re.root;
|
|
225
|
+
setLedgerRoot(ledger, root);
|
|
226
|
+
ledgerFile = null; // 换根重搜台账
|
|
227
|
+
}
|
|
228
|
+
if (root) resolveLedger(force); // 首扫/强刷时:搜索台账,无则创建(幂等)
|
|
229
|
+
cache.payload = buildPayload();
|
|
230
|
+
cache.ts = now;
|
|
231
|
+
return cache.payload;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getLedgerPayload(force) {
|
|
235
|
+
var now = Date.now();
|
|
236
|
+
if (!force && cacheLedger.payload && now - cacheLedger.ts < CACHE_TTL_MS) return cacheLedger.payload;
|
|
237
|
+
if (!ledgerFile && root) resolveLedger(force);
|
|
238
|
+
var text = ledgerFile ? readLedgerAtPath(ledgerFile) : null;
|
|
239
|
+
var entries = text == null ? [] : parseLedgerTable(text);
|
|
240
|
+
var totalFail = 0, failCount = 0;
|
|
241
|
+
for (var z = 0; z < entries.length; z++) {
|
|
242
|
+
if (entries[z].fails > 0) { failCount++; totalFail += entries[z].fails; }
|
|
243
|
+
}
|
|
244
|
+
var allAuto = readAutoLog(root);
|
|
245
|
+
var autoTotal = allAuto.length, autoFailTotal = 0;
|
|
246
|
+
for (var a = 0; a < allAuto.length; a++) { if (!allAuto[a].ok) autoFailTotal++; }
|
|
247
|
+
cacheLedger.payload = {
|
|
248
|
+
ok: true,
|
|
249
|
+
ledgerPath: ledgerFile || (root ? ledgerDefaultPath(root) : null),
|
|
250
|
+
ledgerFound: !!ledgerFile,
|
|
251
|
+
autoLogPath: root ? autoLogFilePath(root) : null,
|
|
252
|
+
totalFailSkills: failCount,
|
|
253
|
+
totalFails: totalFail,
|
|
254
|
+
entries: entries,
|
|
255
|
+
autoTotal: autoTotal,
|
|
256
|
+
autoFailTotal: autoFailTotal,
|
|
257
|
+
autoRecent: allAuto.slice(-30).reverse()
|
|
258
|
+
};
|
|
259
|
+
cacheLedger.ts = now;
|
|
260
|
+
return cacheLedger.payload;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
ctx.effect(function () {
|
|
264
|
+
var handler = function (req, res) {
|
|
265
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
|
|
266
|
+
var url = req.url || "/";
|
|
267
|
+
var path = url.slice(0, url.indexOf("?") >= 0 ? url.indexOf("?") : url.length);
|
|
268
|
+
var query = url.indexOf("?") >= 0 ? url.slice(url.indexOf("?") + 1) : "";
|
|
269
|
+
|
|
270
|
+
if (path === PREFIX + "/health") {
|
|
271
|
+
sendJson(res, 200, { ok: true, plugin: "dsh-skill-browser", version: VERSION, root: root, source: detected.source });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── 运行时配置(设置页:技能库位置 / 自动台账开关) ──
|
|
276
|
+
if (path === PREFIX + "/config" && req.method === "POST") {
|
|
277
|
+
var cfgChunks = [];
|
|
278
|
+
req.on("data", function (c) { cfgChunks.push(c); });
|
|
279
|
+
req.on("end", function () {
|
|
280
|
+
try {
|
|
281
|
+
var body = JSON.parse(Buffer.concat(cfgChunks).toString("utf8")) || {};
|
|
282
|
+
var next = loadRuntimeConfig();
|
|
283
|
+
if (typeof body.skillsDir === "string") {
|
|
284
|
+
var trimmed = body.skillsDir.trim();
|
|
285
|
+
if (trimmed) next.skillsDir = trimmed; else delete next.skillsDir;
|
|
286
|
+
}
|
|
287
|
+
if (typeof body.ledgerEnabled === "boolean") next.ledgerEnabled = body.ledgerEnabled;
|
|
288
|
+
var saved = saveRuntimeConfig(next);
|
|
289
|
+
var re2 = detectRoot(config, next);
|
|
290
|
+
root = re2.root;
|
|
291
|
+
detected = re2;
|
|
292
|
+
runtime = next;
|
|
293
|
+
setLedgerRoot(ledger, root);
|
|
294
|
+
cache.payload = null; cacheLedger.payload = null;
|
|
295
|
+
ctx.logger.info("dsh-skill-browser: config updated -> root=" + (root || "(not found)") + " (source: " + re2.source + ")");
|
|
296
|
+
sendJson(res, 200, { ok: saved, root: root, source: re2.source, runtime: next });
|
|
297
|
+
} catch (e) {
|
|
298
|
+
sendJson(res, 200, { ok: false, error: String(e && e.message || e) });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (path === PREFIX + "/config") {
|
|
305
|
+
if (!ledgerFile && root) resolveLedger(false);
|
|
306
|
+
sendJson(res, 200, {
|
|
307
|
+
ok: true,
|
|
308
|
+
root: root,
|
|
309
|
+
source: detected.source,
|
|
310
|
+
runtime: runtime,
|
|
311
|
+
ledgerEnabled: runtime.ledgerEnabled !== false,
|
|
312
|
+
ledgerPath: ledgerFile || (root ? ledgerDefaultPath(root) : null),
|
|
313
|
+
ledgerExists: ledgerFile ? existsSync(ledgerFile) : false,
|
|
314
|
+
autoLogPath: root ? autoLogFilePath(root) : null
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (path === PREFIX + "/skills") {
|
|
320
|
+
var force = /(^|&)refresh=1(&|$)/.test(query);
|
|
321
|
+
try {
|
|
322
|
+
sendJson(res, 200, getPayload(force));
|
|
323
|
+
} catch (e) {
|
|
324
|
+
ctx.logger.warn("dsh-skill-browser: scan failed " + (e && e.message));
|
|
325
|
+
sendJson(res, 500, { ok: false, error: String(e && e.message || e) });
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (path === PREFIX + "/skill") {
|
|
331
|
+
var m = /(^|&)dir=([^&]*)/.exec(query);
|
|
332
|
+
var dir = m ? decodeURIComponent(m[2]) : "";
|
|
333
|
+
var body = dir ? readSkillBody(root, dir) : null;
|
|
334
|
+
if (body == null) {
|
|
335
|
+
sendJson(res, 404, { ok: false, error: "skill not found: " + dir });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
sendJson(res, 200, { ok: true, dir: dir, content: body });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (path === PREFIX + "/ledger") {
|
|
343
|
+
try {
|
|
344
|
+
sendJson(res, 200, getLedgerPayload(/(^|&)refresh=1(&|$)/.test(query)));
|
|
345
|
+
} catch (e) {
|
|
346
|
+
ctx.logger.warn("dsh-skill-browser: ledger read failed " + (e && e.message));
|
|
347
|
+
sendJson(res, 500, { ok: false, error: String(e && e.message || e) });
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 语义层手动上报:{ skill, note, kind }
|
|
353
|
+
if (path === PREFIX + "/ledger/report" && req.method === "POST") {
|
|
354
|
+
var rpChunks = [];
|
|
355
|
+
req.on("data", function (c) { rpChunks.push(c); });
|
|
356
|
+
req.on("end", function () {
|
|
357
|
+
try {
|
|
358
|
+
var body = JSON.parse(Buffer.concat(rpChunks).toString("utf8")) || {};
|
|
359
|
+
var out = reportFailure(ledger, body);
|
|
360
|
+
sendJson(res, 200, out);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
sendJson(res, 200, { ok: false, error: String(e && e.message || e) });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (path === PREFIX + "/ledger/flush" && req.method === "POST") {
|
|
369
|
+
flushNow(ledger);
|
|
370
|
+
sendJson(res, 200, { ok: true, pending: ledger.queue.length });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (path === PREFIX + "/ledger-detail") {
|
|
375
|
+
var m2 = /(^|&)skill=([^&]*)/.exec(query);
|
|
376
|
+
var skill = m2 ? decodeURIComponent(m2[2]) : "";
|
|
377
|
+
if (!skill) { sendJson(res, 400, { ok: false, error: "missing skill" }); return; }
|
|
378
|
+
if (!ledgerFile && root) resolveLedger(false);
|
|
379
|
+
var text2 = ledgerFile ? readLedgerAtPath(ledgerFile) : null;
|
|
380
|
+
var detail = text2 == null ? "" : (parseLedgerDetail(text2, skill) || "");
|
|
381
|
+
var autoEntries = readAutoLog(root, skill);
|
|
382
|
+
sendJson(res, 200, { ok: true, skill: skill, detail: detail, autoEntries: autoEntries });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 系统目录选择框:POST /pick-dir → 宿主弹 Windows FolderBrowserDialog
|
|
387
|
+
if (path === PREFIX + "/pick-dir" && req.method === "POST") {
|
|
388
|
+
pickDirViaPowerShell().then(function (p) {
|
|
389
|
+
if (p) {
|
|
390
|
+
ctx.logger.info("dsh-skill-browser: pick-dir -> " + p);
|
|
391
|
+
sendJson(res, 200, { ok: true, path: p });
|
|
392
|
+
} else {
|
|
393
|
+
sendJson(res, 200, { ok: true, path: null }); // 用户取消或失败
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (path === PREFIX + "/diag") {
|
|
400
|
+
// 客户端诊断日志:POST 诊断信息,写入文件供后续排查
|
|
401
|
+
if (req.method === "POST") {
|
|
402
|
+
var chunks = [];
|
|
403
|
+
req.on("data", function (c) { chunks.push(c); });
|
|
404
|
+
req.on("end", function () {
|
|
405
|
+
try {
|
|
406
|
+
var body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
407
|
+
mkdirSync(dirname(DIAG_FILE), { recursive: true });
|
|
408
|
+
appendFileSync(DIAG_FILE, JSON.stringify({ ts: new Date().toISOString(), diag: body }) + "\n", "utf8");
|
|
409
|
+
sendJson(res, 200, { ok: true });
|
|
410
|
+
} catch (e) {
|
|
411
|
+
sendJson(res, 200, { ok: true, error: String(e) });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
// GET 返回所有诊断记录
|
|
416
|
+
try {
|
|
417
|
+
if (existsSync(DIAG_FILE)) {
|
|
418
|
+
var text = readFileSync(DIAG_FILE, "utf8");
|
|
419
|
+
sendJson(res, 200, { ok: true, lines: text.split("\n").filter(Boolean).slice(-20).map(function(l){ try { return JSON.parse(l); } catch(e) { return l; } }) });
|
|
420
|
+
} else {
|
|
421
|
+
sendJson(res, 200, { ok: true, lines: [] });
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {
|
|
424
|
+
sendJson(res, 200, { ok: true, lines: [], error: String(e) });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
sendJson(res, 404, { ok: false, error: "unknown path" });
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
var dispose = ctx.webServer.register({ kind: "prefix", path: PREFIX, handler: handler });
|
|
434
|
+
return function () { try { dispose(); } catch (e) {} };
|
|
435
|
+
}, "dsh-skill-browser: api routes");
|
|
436
|
+
}
|