dsh-lighthouse 0.0.0-stage → 0.3.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 +167 -2
- package/bin/dsh-lighthouse.mjs +133 -0
- package/client.js +388 -0
- package/cordis.patch.yml +4 -0
- package/lib/collect.js +484 -0
- package/lib/fleet.js +139 -0
- package/lib/index.js +385 -0
- package/lib/render.js +415 -0
- package/lib/status-route.js +126 -0
- package/package.json +93 -6
package/lib/index.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-lighthouse —— 宿主入口。
|
|
3
|
+
*
|
|
4
|
+
* 一句话:**一屏看清你发出去的每个作品现在怎么样了** —— 本地拿什么版本、GitHub 有没有动静、
|
|
5
|
+
* 市场收没收录、被下载多少次、还给收录 PR 等了多久(对比上游中位数)。
|
|
6
|
+
*
|
|
7
|
+
* 它只读:不发任何写请求,不动你的仓库,也不生成任何"看起来像"的数字 —— 要么来自 API,要么标"读不到"。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-lighthouse
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { collectAll } from './collect.js';
|
|
16
|
+
import { STATUS_ROUTE, createStatusHandler } from './status-route.js';
|
|
17
|
+
import { defaultFleetPath, parseLoose, readFleet, validateFleet, writeFleet } from './fleet.js';
|
|
18
|
+
import { renderText, writeReport } from './render.js';
|
|
19
|
+
|
|
20
|
+
/** 插件身份。 */
|
|
21
|
+
export const name = 'dsh-lighthouse';
|
|
22
|
+
|
|
23
|
+
/** 需要宿主提供的服务(拿不到就退化,不报错)。 */
|
|
24
|
+
export const inject = ['tools', 'systemPrompt'];
|
|
25
|
+
|
|
26
|
+
const SECTION_ORDER = 207;
|
|
27
|
+
const TOOL_TIMEOUT_MS = 180000;
|
|
28
|
+
|
|
29
|
+
/** 面向模型的公告(常驻那一段)。 */
|
|
30
|
+
export const LIGHTHOUSE_GUIDANCE = [
|
|
31
|
+
'本机已安装 **dsh-lighthouse(灯塔)**:`lighthouse_report` 一屏汇总每个作品的现状(本地版本/文件数、GitHub star 与最后推送、市场**是否已收录/下载量/同类排名**、收录 PR 等了多久对比上游中位数),并落一张**可直接截图**的 HTML 卡片。',
|
|
32
|
+
'它**只读**:所有请求都是 GET;读不到就标"读不到",绝不编数字。',
|
|
33
|
+
'名册(照谁看)是一份**显式文件**,默认 `~/.dsh/lighthouse-fleet.json`;`lighthouse_fleet` 可以 show/init 它 —— **名册不自动发现**(不许从目录名猜哪些是你的作品)。',
|
|
34
|
+
'典型用法:想知道"我们的插件上市场了吗 / 有人装吗"→ 调 `lighthouse_report`;卡片路径会随结果返回,可直接给用户看。',
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
/** 动态取 defineTool:宿主里有就用真的,单测/CLI 场景退化成恒等。 */
|
|
38
|
+
let defineTool = (def) => def;
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import('@deepseek-ai/dsh-tools');
|
|
41
|
+
if (mod && typeof mod.defineTool === 'function') defineTool = mod.defineTool;
|
|
42
|
+
} catch {
|
|
43
|
+
/* 宿主外运行 */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 找 PAT(跟滑道同一套顺序)。
|
|
48
|
+
* @param {string} [explicit] 显式路径
|
|
49
|
+
* @returns {Promise<string|null>} token
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveToken(explicit) {
|
|
52
|
+
const candidates = [];
|
|
53
|
+
if (typeof explicit === 'string' && explicit !== '') candidates.push(explicit);
|
|
54
|
+
if (typeof process.env.DSH_SLIPWAY_TOKEN_FILE === 'string' && process.env.DSH_SLIPWAY_TOKEN_FILE !== '') {
|
|
55
|
+
candidates.push(process.env.DSH_SLIPWAY_TOKEN_FILE);
|
|
56
|
+
}
|
|
57
|
+
candidates.push(join(homedir(), '.dsh', '.gh-token'));
|
|
58
|
+
candidates.push('D:\\dsh\\.gh-token');
|
|
59
|
+
for (const file of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
const text = await readFile(file, 'utf8');
|
|
62
|
+
const token = text.trim();
|
|
63
|
+
if (token !== '') return token;
|
|
64
|
+
} catch {
|
|
65
|
+
/* 换下一个 */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 默认市场地址。
|
|
73
|
+
*
|
|
74
|
+
* 市场登记表的路由(`/dsh-market/registry`)挂在**宿主自己的 web 服务**上,
|
|
75
|
+
* 所以默认地址必须问 webServer 要 origin —— 写死端口(比如 19387)迟早会失联,
|
|
76
|
+
* 那时报告只会显示"读不到",而人容易把它误读成"没人用"。
|
|
77
|
+
*
|
|
78
|
+
* @param {object} [ctx] 宿主上下文(CLI 场景没有)
|
|
79
|
+
* @returns {string} 市场接口根地址
|
|
80
|
+
*/
|
|
81
|
+
export function defaultMarketUrl(ctx) {
|
|
82
|
+
try {
|
|
83
|
+
const web = typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined;
|
|
84
|
+
const origin = typeof web?.getOrigin === 'function' ? web.getOrigin() : '';
|
|
85
|
+
if (typeof origin === 'string' && origin.trim() !== '') return origin.trim();
|
|
86
|
+
} catch {
|
|
87
|
+
/* 没有 webServer:退回环境变量/旧默认 */
|
|
88
|
+
}
|
|
89
|
+
return process.env.DSH_WEB_URL ?? 'http://127.0.0.1:19387';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 默认卡片目录。 */
|
|
93
|
+
export function defaultReportDir() {
|
|
94
|
+
return join(homedir(), '.dsh', 'lighthouse-reports');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 跑一次报告(工具与 CLI 共用)。
|
|
99
|
+
* @param {object} input 参数
|
|
100
|
+
* @returns {Promise<object>} `{ ok, report, htmlPath, jsonPath, errors }`
|
|
101
|
+
*/
|
|
102
|
+
export async function runReport(input = {}, ctx = undefined) {
|
|
103
|
+
const fleetPath = input.fleetFile ?? defaultFleetPath();
|
|
104
|
+
const read = await readFleet(fleetPath);
|
|
105
|
+
if (read.ok !== true) return { ok: false, errors: read.errors, fleetPath };
|
|
106
|
+
|
|
107
|
+
const token = await resolveToken(input.tokenFile);
|
|
108
|
+
const report = await collectAll(read.fleet, {
|
|
109
|
+
token,
|
|
110
|
+
marketUrl: input.marketUrl ?? defaultMarketUrl(ctx),
|
|
111
|
+
includeNpm: input.includeNpm !== false,
|
|
112
|
+
});
|
|
113
|
+
report.tokenFound = token !== null;
|
|
114
|
+
|
|
115
|
+
let paths = { htmlPath: null, jsonPath: null };
|
|
116
|
+
if (input.writeFiles !== false) {
|
|
117
|
+
paths = await writeReport(report, {
|
|
118
|
+
dir: input.reportDir ?? defaultReportDir(),
|
|
119
|
+
html: input.html !== false,
|
|
120
|
+
json: input.json === true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, report, ...paths, fleetPath };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 挂载插件。
|
|
128
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx 宿主上下文
|
|
129
|
+
* @param {object} [config] 插件配置
|
|
130
|
+
*/
|
|
131
|
+
export function apply(ctx, config) {
|
|
132
|
+
const cfg = config ?? {};
|
|
133
|
+
if (cfg.enabled === false) return;
|
|
134
|
+
const disposers = [];
|
|
135
|
+
|
|
136
|
+
if (ctx.tools !== undefined && typeof ctx.tools.register === 'function') {
|
|
137
|
+
disposers.push(ctx.tools.register(defineTool({
|
|
138
|
+
name: 'lighthouse_report',
|
|
139
|
+
description: [
|
|
140
|
+
'一屏看清每个作品的现状:本地(版本/文件数/有无自测)、GitHub(star/fork/最后推送)、市场(**是否已收录、下载量、同类排名**)、收录 PR(等了多少 vs 上游中位数),并落一张可截图的 HTML 卡片。',
|
|
141
|
+
'**只读**:全部是 GET 请求;某个数据源读不到就标"读不到",不会编数字。',
|
|
142
|
+
'名册(要看守哪些作品)来自 `~/.dsh/lighthouse-fleet.json`,可用 fleetFile 指到别处。',
|
|
143
|
+
].join('\n'),
|
|
144
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
145
|
+
parameters: {
|
|
146
|
+
fleetFile: { type: 'string', description: '名册文件路径(默认 ~/.dsh/lighthouse-fleet.json)' },
|
|
147
|
+
reportDir: { type: 'string', description: '卡片落盘目录(默认 ~/.dsh/lighthouse-reports)' },
|
|
148
|
+
html: { type: 'boolean', description: '是否落 HTML 卡片(默认 true)' },
|
|
149
|
+
includeNpm: { type: 'boolean', description: '是否顺手查 npm(默认 true)' },
|
|
150
|
+
marketUrl: { type: 'string', description: `市场接口根地址(默认取宿主 web 服务的 origin,即 ${defaultMarketUrl()})` },
|
|
151
|
+
tokenFile: { type: 'string', description: 'PAT 文件路径(不填则按 env / ~/.dsh / D:\\dsh 依次找)' },
|
|
152
|
+
},
|
|
153
|
+
output: {
|
|
154
|
+
schema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
properties: {
|
|
158
|
+
ok: { type: 'boolean', required: true },
|
|
159
|
+
data: { type: 'json' },
|
|
160
|
+
error: { type: 'string' },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
render: (_args, value) => {
|
|
164
|
+
if (value.ok === false && value.data === undefined) {
|
|
165
|
+
return [{ type: 'text', text: `lighthouse_report 失败:${String(value.error ?? '未知错误')}` }];
|
|
166
|
+
}
|
|
167
|
+
const d = value.data ?? {};
|
|
168
|
+
if (d.ok === false) {
|
|
169
|
+
return [{ type: 'text', text: `lighthouse_report 失败:\n ${(d.errors ?? []).join('\n ')}\n (名册:${d.fleetPath ?? '?'})` }];
|
|
170
|
+
}
|
|
171
|
+
const lines = [renderText(d.report)];
|
|
172
|
+
if (d.htmlPath) lines.push(` 卡片已落盘:${d.htmlPath}`);
|
|
173
|
+
if (d.report?.tokenFound === false) lines.push(' ⚠️ 没找到 PAT:GitHub 部分可能只有公开信息或读不到');
|
|
174
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
async execute(args) {
|
|
178
|
+
try {
|
|
179
|
+
const data = await runReport({
|
|
180
|
+
fleetFile: args?.fleetFile ?? cfg.fleetFile,
|
|
181
|
+
reportDir: args?.reportDir ?? cfg.reportDir,
|
|
182
|
+
html: args?.html,
|
|
183
|
+
includeNpm: args?.includeNpm,
|
|
184
|
+
marketUrl: args?.marketUrl ?? cfg.marketUrl,
|
|
185
|
+
tokenFile: args?.tokenFile ?? cfg.tokenFile,
|
|
186
|
+
}, ctx);
|
|
187
|
+
// **不能**写 `error: undefined`:宿主用 lossless-JSON 校验返回对象,
|
|
188
|
+
// JSON.stringify 会悄悄丢掉 undefined,校验器不会 —— 会被判 "invalid output"。
|
|
189
|
+
const out = { ok: data.ok === true, data };
|
|
190
|
+
if (out.ok !== true) out.error = String((data.errors ?? [])[0] ?? '未完成');
|
|
191
|
+
return out;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
})));
|
|
197
|
+
|
|
198
|
+
disposers.push(ctx.tools.register(defineTool({
|
|
199
|
+
name: 'lighthouse_fleet',
|
|
200
|
+
description: [
|
|
201
|
+
'看/写灯塔的**名册**(它照着名册看,不自动发现作品)。',
|
|
202
|
+
"action='show':显示名册路径与它看守的作品。",
|
|
203
|
+
"action='sync':拿每个作品本地 package.json 的 `dsh.displayName` / `repository` 回填名册里**空着**的中文名与直达链接,并报告「名册包名与仓库对不上」的情况(只报告,不擅自改名)。滑道发布过(尤其改名)之后跑一次,灯塔就跟上了。",
|
|
204
|
+
"action='init':用显式给的 owner 与 works 建名册 —— works 是 JSON 数组字符串,每条形如 {\\\"name\\\":\\\"dsh-slipway\\\",\\\"repo\\\":\\\"dsh-slipway\\\",\\\"category\\\":\\\"tools\\\",\\\"localDir\\\":\\\"D:\\\\dsh\\\\projects\\\\dsh-slipway\\\"};**必须显式给,不猜**。",
|
|
205
|
+
].join('\n'),
|
|
206
|
+
timeoutMs: 30000,
|
|
207
|
+
parameters: {
|
|
208
|
+
action: { type: 'string', description: 'show | sync(回填中文名/链接并报告对不上的) | init' },
|
|
209
|
+
fleetFile: { type: 'string', description: '名册文件路径(默认 ~/.dsh/lighthouse-fleet.json)' },
|
|
210
|
+
owner: { type: 'string', description: 'init 用:GitHub 账号' },
|
|
211
|
+
works: { type: 'string', description: 'init 用:works 的 JSON 数组字符串' },
|
|
212
|
+
upstreamOwner: { type: 'string', description: 'init 用:上游收录仓库 owner(默认 awesome-dsh-plugin)' },
|
|
213
|
+
upstreamRepo: { type: 'string', description: 'init 用:上游收录仓库名(默认 awesome-dsh-plugin)' },
|
|
214
|
+
},
|
|
215
|
+
output: {
|
|
216
|
+
schema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
properties: {
|
|
220
|
+
ok: { type: 'boolean', required: true },
|
|
221
|
+
data: { type: 'json' },
|
|
222
|
+
error: { type: 'string' },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
render: (_args, value) => {
|
|
226
|
+
if (value.ok !== true) return [{ type: 'text', text: `lighthouse_fleet 失败:${String(value.error ?? '未知错误')}` }];
|
|
227
|
+
const d = value.data ?? {};
|
|
228
|
+
if (d.action === 'init') {
|
|
229
|
+
return [{ type: 'text', text: `名册已写入:${d.fleetPath}\n · owner:${d.owner}\n · 看守 ${d.count} 个作品:${(d.names ?? []).join(', ')}` }];
|
|
230
|
+
}
|
|
231
|
+
if (d.action === 'sync') {
|
|
232
|
+
const fixed = Array.isArray(d.updated) ? d.updated : [];
|
|
233
|
+
const bad = Array.isArray(d.mismatched) ? d.mismatched : [];
|
|
234
|
+
const lines = [`名册已同步:${d.fleetPath}(${d.count} 个作品)`];
|
|
235
|
+
if (fixed.length > 0) lines.push(` · 回填:${fixed.join(';')}`);
|
|
236
|
+
else lines.push(' · 没有需要回填的空字段');
|
|
237
|
+
if (bad.length > 0) lines.push(` · ⚠ 对不上:${bad.map((x) => `${x.name} —— ${x.problem}`).join(';')}`);
|
|
238
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
239
|
+
}
|
|
240
|
+
return [{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: `名册:${d.fleetPath}\n · owner:${d.owner ?? '(缺)'}\n · ${d.count} 个作品:${(d.names ?? []).join(', ')}\n · 上游:${d.upstream ?? '?'}${d.error ? `\n · 读名册出错:${d.error}` : ''}`,
|
|
243
|
+
}];
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
async execute(args) {
|
|
247
|
+
const a = args ?? {};
|
|
248
|
+
const action = String(a.action ?? 'show').toLowerCase();
|
|
249
|
+
const fleetPath = a.fleetFile ?? cfg.fleetFile ?? defaultFleetPath();
|
|
250
|
+
try {
|
|
251
|
+
if (action === 'init') {
|
|
252
|
+
let works;
|
|
253
|
+
try {
|
|
254
|
+
works = JSON.parse(String(a.works ?? ''));
|
|
255
|
+
} catch {
|
|
256
|
+
return { ok: false, error: 'works 不是合法 JSON 数组字符串' };
|
|
257
|
+
}
|
|
258
|
+
const checked = validateFleet({
|
|
259
|
+
owner: a.owner,
|
|
260
|
+
upstream: { owner: a.upstreamOwner, repo: a.upstreamRepo },
|
|
261
|
+
works,
|
|
262
|
+
});
|
|
263
|
+
if (checked.ok !== true) return { ok: false, error: checked.errors.join(';') };
|
|
264
|
+
await writeFleet(fleetPath, checked.fleet);
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
data: { action: 'init', fleetPath, owner: checked.fleet.owner, count: checked.fleet.works.length, names: checked.fleet.works.map((w) => w.name) },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const read = await readFleet(fleetPath);
|
|
271
|
+
const fleet = read.fleet;
|
|
272
|
+
if (action === 'sync') {
|
|
273
|
+
// 拿本地 package.json 回填 nameZh / url,并报告"名册与仓库对不上"的情况。
|
|
274
|
+
// 只补**空**字段(不覆盖你手写的),对不上的只报告、不擅自改 name/repo。
|
|
275
|
+
if (read.ok !== true) return { ok: false, error: read.errors.join(';') };
|
|
276
|
+
const fixed = [];
|
|
277
|
+
const mismatched = [];
|
|
278
|
+
const next = JSON.parse(JSON.stringify(fleet));
|
|
279
|
+
for (const work of next.works) {
|
|
280
|
+
if (work.localDir === null) continue;
|
|
281
|
+
let manifest = null;
|
|
282
|
+
try {
|
|
283
|
+
const text = await readFile(join(work.localDir, 'package.json'), 'utf8');
|
|
284
|
+
manifest = parseLoose(text);
|
|
285
|
+
} catch {
|
|
286
|
+
mismatched.push({ name: work.name, problem: `读不到 ${work.localDir}\\package.json` });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (manifest.name && manifest.name !== work.name) {
|
|
290
|
+
mismatched.push({ name: work.name, problem: `包名是 ${manifest.name}(名册写的 ${work.name})` });
|
|
291
|
+
}
|
|
292
|
+
if (!work.nameZh && typeof manifest?.dsh?.displayName === 'string' && manifest.dsh.displayName !== '') {
|
|
293
|
+
work.nameZh = manifest.dsh.displayName;
|
|
294
|
+
fixed.push(`${work.name} 补中文名「${work.nameZh}」`);
|
|
295
|
+
}
|
|
296
|
+
if (!work.url) {
|
|
297
|
+
const repo = manifest.repository?.url ?? manifest.homepage ?? null;
|
|
298
|
+
if (typeof repo === 'string' && repo !== '') {
|
|
299
|
+
work.url = repo.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
300
|
+
fixed.push(`${work.name} 补链接 ${work.url}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (fixed.length > 0) await writeFleet(fleetPath, next);
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
data: {
|
|
308
|
+
action: 'sync',
|
|
309
|
+
fleetPath,
|
|
310
|
+
updated: fixed,
|
|
311
|
+
mismatched,
|
|
312
|
+
count: next.works.length,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
data: {
|
|
319
|
+
action: 'show',
|
|
320
|
+
fleetPath,
|
|
321
|
+
owner: fleet?.owner ?? null,
|
|
322
|
+
count: fleet?.works?.length ?? 0,
|
|
323
|
+
names: (fleet?.works ?? []).map((w) => w.name),
|
|
324
|
+
upstream: fleet ? `${fleet.upstream.owner}/${fleet.upstream.repo}@${fleet.upstream.branch}` : null,
|
|
325
|
+
error: read.ok === true ? null : read.errors.join(';'),
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
})));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// —— 界面「作品观察区」的数据口:GET /dsh-lighthouse/status(只读,与工具同一条采集路径)
|
|
336
|
+
//
|
|
337
|
+
// 必须用 `ctx.inject([...], host => ...)` 等 webServer 就位再注册(官方插件也是这么写的)。
|
|
338
|
+
// 实测教训:写成 `ctx.get('webServer')` 时,apply 那一刻服务还没挂上 → 拿到 undefined →
|
|
339
|
+
// 整段被跳过 → 界面读数据 404,而插件其它能力一切正常 —— "只少一块"最难查。
|
|
340
|
+
try {
|
|
341
|
+
if (typeof ctx.inject === 'function') {
|
|
342
|
+
ctx.inject(['webServer'], (host) => {
|
|
343
|
+
host.effect(() => host.webServer.register({
|
|
344
|
+
kind: 'exact',
|
|
345
|
+
path: STATUS_ROUTE,
|
|
346
|
+
handler: createStatusHandler({
|
|
347
|
+
fleetFile: cfg.fleetFile ?? defaultFleetPath(),
|
|
348
|
+
marketUrl: cfg.marketUrl ?? defaultMarketUrl(host),
|
|
349
|
+
// 延迟解析:注册路由时还没有 token,第一次请求时再拿。
|
|
350
|
+
// 注意:`resolveToken` 返回的**就是字符串**(不是 { token, from } 那种对象)——
|
|
351
|
+
// 写成 `.then((found) => found.token)` 会拿到 undefined,于是整条口子**静默退化成匿名请求**;
|
|
352
|
+
// 实测后果:匿名额度 60/小时一用完,7 个仓库全 403,面板上一片"读不到仓库"。
|
|
353
|
+
token: () => resolveToken(cfg.tokenFile),
|
|
354
|
+
}),
|
|
355
|
+
}, 'dsh-lighthouse: status route'));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
/* 没有 web server 的表面(例如纯 CLI):观察区缺席,其它能力不受影响 */
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (cfg.announceToAgent === true && ctx.systemPrompt !== undefined
|
|
363
|
+
&& typeof ctx.systemPrompt.section === 'function') {
|
|
364
|
+
disposers.push(ctx.systemPrompt.section({
|
|
365
|
+
name: 'plugin:lighthouse',
|
|
366
|
+
order: SECTION_ORDER,
|
|
367
|
+
text: LIGHTHOUSE_GUIDANCE,
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof ctx.effect === 'function' && disposers.length > 0) {
|
|
372
|
+
ctx.effect(() => () => {
|
|
373
|
+
for (const dispose of disposers) {
|
|
374
|
+
try {
|
|
375
|
+
if (typeof dispose === 'function') dispose();
|
|
376
|
+
} catch {
|
|
377
|
+
/* 卸载失败不阻断其它清理 */
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}, 'dsh-lighthouse: teardown');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 供 CLI 复用的写文件工具(保证 CLI 与工具用同一套名册格式)。 */
|
|
385
|
+
export { writeFile };
|