@kira905/dsh-ecosystem-panel 0.0.0-stage → 0.4.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/lib/config.js ADDED
@@ -0,0 +1,362 @@
1
+ // dsh-ecosystem-panel / lib/config.js
2
+ // 配置层:把原本写死在源码里的路径 / 白名单 / 规则表 / 文案全部外置。
3
+ //
4
+ // 解析优先级(**每个键独立**):
5
+ // ① 显式传入(插件 config、本模块 cli 入参;夹具与 CLI 用这一层)
6
+ // ② 环境变量(前缀 DSH_ECOSYSTEM_PANEL_;见下表)
7
+ // ③ 配置文件(DSH_ECOSYSTEM_PANEL_CONFIG 指向的 JSON,键名与 ① 同名)
8
+ // ④ 自动探测(只探测形态确定的东西:用户目录 / 全局 npm 布局 / 项目相对路径)
9
+ // ⑤ 内置默认
10
+ //
11
+ // ⚠️ 设计红线:**探测不到就返回 null + 原因,绝不猜、绝不用 0 或空表冒充"全绿"**。
12
+ // 下游(state.js / upgrade.js)对每个 null 都必须有显式降级分支(见各自的 collect* 函数)。
13
+ //
14
+ // 环境变量一览:
15
+ // DSH_ECOSYSTEM_PANEL_HOME DSH 数据根(默认 <os.homedir()>/.dsh)
16
+ // DSH_HOME 同上(DSH 官方口径,作为兜底)
17
+ // DSH_ECOSYSTEM_PANEL_PROFILE profile 名(默认 web)
18
+ // DSH_ECOSYSTEM_PANEL_WEB_URL 本实例的 Web 基址(默认 http://127.0.0.1:3080)
19
+ // DSH_WEB_URL 同上(DSH 宿主会注入,作为兜底)
20
+ // DSH_ECOSYSTEM_PANEL_TOOLS_DIR 外部工具目录(**不自动探测**,不设则该类目降级)
21
+ // DSH_ECOSYSTEM_PANEL_MAIN_NM 主包 node_modules(默认按全局 npm 布局探测)
22
+ // DSH_ECOSYSTEM_PANEL_CONFIG 配置文件 JSON 路径
23
+ // DSH_ECOSYSTEM_PANEL_CACHE_FILE 升级体检缓存文件(默认 <HOME>/ecosystem-panel-upgrade-cache.json)
24
+ // DSH_ECOSYSTEM_PANEL_DECOUPLING_SCRIPT 解耦检查脚本路径
25
+ // DSH_ECOSYSTEM_PANEL_TRIAGE_CLI 升级档位只读 CLI 路径
26
+ // DSH_ECOSYSTEM_PANEL_REGISTRY_AUDIT_LIB 跨表准入校验器路径
27
+ // DSH_ECOSYSTEM_PANEL_HOST_ADAPTER_SYNC 适配层一致性校验器路径(可选)
28
+ // DSH_ECOSYSTEM_PANEL_PATCH_MANIFEST_FILE / _UPGRADE_RULES_FILE / _BREAKING_SYMBOLS_FILE / _LABELS_FILE
29
+ // 四个数据清单的 JSON 文件路径
30
+ // DSH_ECOSYSTEM_PANEL_WEBSERVER_SERVICE 宿主 web server 服务名(默认 webServer,见 host-protocol.js)
31
+ // DSH_ECOSYSTEM_PANEL_DISABLED_GROUPS 逗号分隔:把某几类体检整组标为"未启用"
32
+
33
+ import fs from 'node:fs';
34
+ import os from 'node:os';
35
+ import path from 'node:path';
36
+
37
+ export const PLUGIN_ID = 'dsh-ecosystem-panel';
38
+ export const ENV_PREFIX = 'DSH_ECOSYSTEM_PANEL_';
39
+
40
+ /** 内置默认值:**全部与具体机器无关**(这里出现任何绝对路径或个人环境清单都是缺陷) */
41
+ export const DEFAULTS = {
42
+ profile: 'web',
43
+ webUrl: 'http://127.0.0.1:3080',
44
+ /** 随主包发行、profile 里没有实体目录的官方 core 包(client 由子包提供,boot 有子包条目即证在跑) */
45
+ coreBundles: [
46
+ '@deepseek-ai/dsh-base',
47
+ '@deepseek-ai/dsh-web-app',
48
+ '@deepseek-ai/dsh-client-ui-jobs'
49
+ ],
50
+ /** 定制补丁清单:默认**空**(发布集不内嵌任何个人环境的补丁表) */
51
+ patchManifest: [],
52
+ /** 升级决策规则表:默认**空**(不内嵌任何个人环境的禁升/冻结结论) */
53
+ upgradeRules: {},
54
+ /** 已知断裂符号表:默认**空** */
55
+ breakingSymbols: {},
56
+ /** 包名 → 一句话简介:默认**空** */
57
+ labels: {},
58
+ /** 界面文案覆盖:默认**空**(前端有内置中性文案,这里只做覆盖) */
59
+ text: {},
60
+ /** 整组停用的类目(例如不想要补丁体检就写 ["patches"]) */
61
+ disabledGroups: []
62
+ };
63
+
64
+ const SCRIPT_KEYS = ['decoupling', 'triageCli', 'registryAudit', 'hostAdapterSync'];
65
+ /** 各外部脚本在 toolsDir 下的默认相对位置(只作候选;文件不存在即降级,不影响其它类目) */
66
+ const SCRIPT_DEFAULT_REL = {
67
+ decoupling: 'check-decoupling.mjs',
68
+ triageCli: 'upgrade-triage-cli.mjs',
69
+ registryAudit: 'registry-audit.mjs',
70
+ hostAdapterSync: 'sync-host-adapter.mjs'
71
+ };
72
+ const MANIFEST_KEYS = {
73
+ patchManifest: 'PATCH_MANIFEST_FILE',
74
+ upgradeRules: 'UPGRADE_RULES_FILE',
75
+ breakingSymbols: 'BREAKING_SYMBOLS_FILE',
76
+ labels: 'LABELS_FILE'
77
+ };
78
+
79
+ function str(v) {
80
+ return typeof v === 'string' && v.trim() !== '' ? v.trim() : null;
81
+ }
82
+ function firstOf(...vals) {
83
+ for (const v of vals) {
84
+ const x = str(v);
85
+ if (x !== null) return x;
86
+ }
87
+ return null;
88
+ }
89
+ function toAbs(v, base) {
90
+ const x = str(v);
91
+ if (x === null) return null;
92
+ return path.isAbsolute(x) ? path.normalize(x) : path.resolve(base ?? process.cwd(), x);
93
+ }
94
+ function readJsonFile(p) {
95
+ try {
96
+ return { ok: true, value: JSON.parse(fs.readFileSync(p, 'utf8')), error: null };
97
+ } catch (e) {
98
+ return { ok: false, value: null, error: `${p} 读取/解析失败:${String(e?.message ?? e)}` };
99
+ }
100
+ }
101
+ function dirExists(p) {
102
+ try {
103
+ return fs.statSync(p).isDirectory();
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /** 把配置里的 ${HOME} / ${PROFILE_DIR} / ${PROFILE_NM} / ${SKILLS_DIR} / ${MAIN_NM} 占位符展开成实际路径 */
110
+ export function expandVars(value, cfg) {
111
+ if (typeof value !== 'string' || !value.includes('${')) return value;
112
+ const map = {
113
+ HOME: cfg.home ?? '',
114
+ PROFILE_DIR: cfg.profileDir ?? '',
115
+ PROFILE_NM: cfg.profileNm ?? '',
116
+ SKILLS_DIR: cfg.skillsDir ?? '',
117
+ MAIN_NM: cfg.mainNm ?? '',
118
+ MAIN_PKG_DIR: cfg.mainPkgDir ?? ''
119
+ };
120
+ return value.replace(/\$\{([A-Z_]+)\}/g, (m, k) => (k in map ? map[k] : m));
121
+ }
122
+ function expandDeep(value, cfg) {
123
+ if (typeof value === 'string') return expandVars(value, cfg);
124
+ if (Array.isArray(value)) return value.map((v) => expandDeep(v, cfg));
125
+ if (value && typeof value === 'object') {
126
+ const out = {};
127
+ for (const [k, v] of Object.entries(value)) out[k] = expandDeep(v, cfg);
128
+ return out;
129
+ }
130
+ return value;
131
+ }
132
+
133
+ /**
134
+ * 解析出一份完整配置(纯函数:不改全局状态,可被夹具直接断言)。
135
+ * @param {object} [cli] 显式覆盖(优先级最高)
136
+ * @param {object} [env] 环境变量表(默认 process.env)
137
+ */
138
+ export function resolveConfig(cli = {}, env = process.env) {
139
+ const errors = [];
140
+ const envVal = (key) => str(env[ENV_PREFIX + key]);
141
+ const cliVal = (key) => (cli && cli[key] !== undefined ? cli[key] : undefined);
142
+
143
+ // ---- 配置文件(③ 层)------------------------------------------------------
144
+ const configFile = toAbs(firstOf(cliVal('configFile'), envVal('CONFIG')));
145
+ let fileCfg = {};
146
+ if (configFile) {
147
+ const r = readJsonFile(configFile);
148
+ if (r.ok && r.value && typeof r.value === 'object' && !Array.isArray(r.value)) fileCfg = r.value;
149
+ else errors.push(r.error ?? `${configFile} 内容不是对象`);
150
+ }
151
+ const fileVal = (key) => (fileCfg && fileCfg[key] !== undefined ? fileCfg[key] : undefined);
152
+
153
+ const sources = {};
154
+ /** 三层取值(cli > env > 配置文件),返回 { value, from } */
155
+ const pick = (key, envKey) => {
156
+ const c = cliVal(key);
157
+ if (str(c) !== null) return (sources[key] = { value: c, from: 'cli' }), sources[key];
158
+ const e = envKey ? envVal(envKey) : null;
159
+ if (e !== null) return (sources[key] = { value: e, from: `env:${ENV_PREFIX}${envKey}` }), sources[key];
160
+ const f = fileVal(key);
161
+ if (str(f) !== null) return (sources[key] = { value: f, from: `config-file(${path.basename(configFile)})` }), sources[key];
162
+ return (sources[key] = { value: null, from: null }), sources[key];
163
+ };
164
+
165
+ // ---- 数据根 / profile ------------------------------------------------------
166
+ const home = toAbs(firstOf(cliVal('home'), envVal('HOME'), env.DSH_HOME, fileVal('home')))
167
+ ?? path.join(os.homedir(), '.dsh');
168
+ sources.home = {
169
+ value: home,
170
+ from: str(cliVal('home')) !== null ? 'cli'
171
+ : str(envVal('HOME')) !== null ? `env:${ENV_PREFIX}HOME`
172
+ : str(env.DSH_HOME) !== null ? 'env:DSH_HOME'
173
+ : str(fileVal('home')) !== null ? `config-file(${path.basename(configFile ?? '-')})`
174
+ : 'default(<os.homedir()>/.dsh)'
175
+ };
176
+ const profile = firstOf(cliVal('profile'), envVal('PROFILE'), fileVal('profile')) ?? DEFAULTS.profile;
177
+ sources.profile = {
178
+ value: profile,
179
+ from: str(cliVal('profile')) !== null ? 'cli'
180
+ : str(envVal('PROFILE')) !== null ? `env:${ENV_PREFIX}PROFILE`
181
+ : str(fileVal('profile')) !== null ? `config-file(${path.basename(configFile ?? '-')})`
182
+ : 'default(web)'
183
+ };
184
+ const profileDir = path.join(home, 'profiles', profile);
185
+ const profileNm = path.join(profileDir, 'node_modules');
186
+ const sharedNm = path.join(home, 'profiles', 'node_modules');
187
+ const skillsDir = path.join(home, 'skills');
188
+
189
+ const webUrl = firstOf(cliVal('webUrl'), envVal('WEB_URL'), env.DSH_WEB_URL, fileVal('webUrl')) ?? DEFAULTS.webUrl;
190
+ sources.webUrl = {
191
+ value: webUrl,
192
+ from: str(cliVal('webUrl')) !== null ? 'cli'
193
+ : str(envVal('WEB_URL')) !== null ? `env:${ENV_PREFIX}WEB_URL`
194
+ : str(env.DSH_WEB_URL) !== null ? 'env:DSH_WEB_URL'
195
+ : str(fileVal('webUrl')) !== null ? 'config-file'
196
+ : 'default(127.0.0.1:3080)'
197
+ };
198
+
199
+ // ---- 主包 node_modules(自动探测:全局 npm 布局)----------------------------
200
+ const mainNmExplicit = toAbs(firstOf(cliVal('mainNm'), envVal('MAIN_NM'), fileVal('mainNm')));
201
+ const mainNmDefault = env.APPDATA
202
+ ? path.join(env.APPDATA, 'npm', 'node_modules', '@deepseek-ai', 'dsh', 'node_modules')
203
+ : null;
204
+ const mainNm = mainNmExplicit ?? (mainNmDefault && dirExists(mainNmDefault) ? mainNmDefault : null);
205
+ sources.mainNm = {
206
+ value: mainNm,
207
+ from: mainNmExplicit !== null ? (str(cliVal('mainNm')) !== null ? 'cli' : (str(envVal('MAIN_NM')) !== null ? `env:${ENV_PREFIX}MAIN_NM` : 'config-file'))
208
+ : mainNm !== null ? 'probe(%APPDATA%/npm/node_modules/@deepseek-ai/dsh/node_modules)'
209
+ : 'unavailable(未探测到全局 npm 主包布局;用 ' + ENV_PREFIX + 'MAIN_NM 显式指定)'
210
+ };
211
+ // mainNm = .../node_modules/@deepseek-ai/dsh/node_modules ⇒ 主包目录 = 它向上一级
212
+ const mainPkgDir = mainNm ? path.dirname(mainNm) : null;
213
+
214
+ // ---- 外部工具目录(**不自动探测**)----------------------------------------
215
+ const toolsDir = toAbs(firstOf(cliVal('toolsDir'), envVal('TOOLS_DIR'), fileVal('toolsDir')));
216
+ sources.toolsDir = {
217
+ value: toolsDir,
218
+ from: str(cliVal('toolsDir')) !== null ? 'cli'
219
+ : str(envVal('TOOLS_DIR')) !== null ? `env:${ENV_PREFIX}TOOLS_DIR`
220
+ : str(fileVal('toolsDir')) !== null ? 'config-file'
221
+ : 'unavailable(未配置:依赖外部工具的三类体检会显式降级,不会假绿)'
222
+ };
223
+
224
+ const cfg = {
225
+ home,
226
+ profile,
227
+ profileDir,
228
+ profileNm,
229
+ sharedNm,
230
+ skillsDir,
231
+ mainNm,
232
+ mainPkgDir,
233
+ webUrl,
234
+ toolsDir,
235
+ configFile,
236
+ cacheFile: toAbs(firstOf(cliVal('cacheFile'), envVal('CACHE_FILE'), fileVal('cacheFile')))
237
+ ?? path.join(home, 'ecosystem-panel-upgrade-cache.json'),
238
+ scripts: {},
239
+ errors,
240
+ sources
241
+ };
242
+
243
+ // ---- 四个外部脚本(cli > env > 配置文件 > toolsDir 派生 > null)------------
244
+ for (const key of SCRIPT_KEYS) {
245
+ const envKey = { decoupling: 'DECOUPLING_SCRIPT', triageCli: 'TRIAGE_CLI', registryAudit: 'REGISTRY_AUDIT_LIB', hostAdapterSync: 'HOST_ADAPTER_SYNC' }[key];
246
+ const cliScripts = (cli && typeof cli.scripts === 'object' && cli.scripts) || {};
247
+ const fileScripts = (fileCfg && typeof fileCfg.scripts === 'object' && fileCfg.scripts) || {};
248
+ let value = toAbs(firstOf(cliScripts[key], envVal(envKey), fileScripts[key]));
249
+ let from = str(cliScripts[key]) !== null ? 'cli'
250
+ : str(envVal(envKey)) !== null ? `env:${ENV_PREFIX}${envKey}`
251
+ : str(fileScripts[key]) !== null ? 'config-file'
252
+ : null;
253
+ if (value === null && toolsDir) {
254
+ value = path.join(toolsDir, SCRIPT_DEFAULT_REL[key]);
255
+ from = `tools-dir(${SCRIPT_DEFAULT_REL[key]})`;
256
+ }
257
+ cfg.scripts[key] = { path: value, from: from ?? 'unavailable(未配置)' };
258
+ }
259
+
260
+ // ---- 四份数据清单(内联 > 文件 > 默认空)-----------------------------------
261
+ const readManifest = (key) => {
262
+ const inline = cliVal(key) ?? fileVal(key);
263
+ if (inline && typeof inline === 'object') return { value: inline, from: str(cliVal(key)) !== null ? 'cli' : 'config-file' };
264
+ const p = toAbs(firstOf(cliVal(key + 'File'), envVal(MANIFEST_KEYS[key]), fileVal(key + 'File')));
265
+ if (p === null) return { value: null, from: 'default(空)' };
266
+ const r = readJsonFile(p);
267
+ if (!r.ok) {
268
+ errors.push(r.error);
269
+ return { value: null, from: `failed(${p})` };
270
+ }
271
+ return { value: r.value, from: p };
272
+ };
273
+ for (const key of Object.keys(MANIFEST_KEYS)) {
274
+ const r = readManifest(key);
275
+ const fallback = DEFAULTS[key];
276
+ const value = r.value === null ? fallback : r.value;
277
+ cfg[key] = Array.isArray(value) ? value.slice() : { ...value };
278
+ cfg.sources[key] = { value: Array.isArray(cfg[key]) ? `[${cfg[key].length}]` : `{${Object.keys(cfg[key]).length}}`, from: r.from };
279
+ }
280
+ // 清单里的 ${HOME} 之类占位符在此展开(使用者不必写死绝对路径)
281
+ cfg.patchManifest = expandDeep(cfg.patchManifest, cfg);
282
+
283
+ // ---- core 包白名单 / 停用类目 / 文案 ---------------------------------------
284
+ const coreExtra = cliVal('coreBundles') ?? fileVal('coreBundles');
285
+ cfg.coreBundles = [...new Set([...DEFAULTS.coreBundles, ...(Array.isArray(coreExtra) ? coreExtra.filter((x) => typeof x === 'string') : [])])];
286
+ cfg.sources.coreBundles = { value: `[${cfg.coreBundles.length}]`, from: Array.isArray(coreExtra) ? 'default+config-file' : 'default(官方 core 3 包)' };
287
+
288
+ // 三种形态都认:显式数组(cli / 配置文件)、逗号分隔字符串(环境变量)、不设(默认全开)
289
+ const disabledCli = cliVal('disabledGroups');
290
+ const disabledFile = fileVal('disabledGroups');
291
+ let disabledArr;
292
+ let disabledFrom;
293
+ if (Array.isArray(disabledCli)) {
294
+ disabledArr = disabledCli.filter((x) => typeof x === 'string');
295
+ disabledFrom = 'cli';
296
+ } else {
297
+ const disabledRaw = firstOf(disabledCli, envVal('DISABLED_GROUPS'));
298
+ if (disabledRaw !== null) {
299
+ disabledArr = String(disabledRaw).split(/[,;]/).map((s) => s.trim()).filter(Boolean);
300
+ disabledFrom = str(disabledCli) !== null ? 'cli' : `env:${ENV_PREFIX}DISABLED_GROUPS`;
301
+ } else if (Array.isArray(disabledFile)) {
302
+ disabledArr = disabledFile.filter((x) => typeof x === 'string');
303
+ disabledFrom = 'config-file';
304
+ } else {
305
+ disabledArr = DEFAULTS.disabledGroups;
306
+ disabledFrom = 'default(全开)';
307
+ }
308
+ }
309
+ cfg.disabledGroups = [...new Set(disabledArr)];
310
+ cfg.sources.disabledGroups = { value: cfg.disabledGroups.join(',') || '(none)', from: disabledFrom };
311
+
312
+ const textOverride = cliVal('text') ?? fileVal('text');
313
+ cfg.text = textOverride && typeof textOverride === 'object' && !Array.isArray(textOverride) ? { ...textOverride } : {};
314
+ cfg.sources.text = { value: `{${Object.keys(cfg.text).length}}`, from: Object.keys(cfg.text).length ? 'config-file/cli' : 'default(前端内置中性文案)' };
315
+
316
+ cfg.disabledGroupSet = new Set(cfg.disabledGroups);
317
+ cfg.isGroupDisabled = (g) => cfg.disabledGroupSet.has(g);
318
+ return cfg;
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // 进程内单例(同一份 cli 入参只解析一次;夹具/校验器可用 resetConfig 清空)
323
+ // ---------------------------------------------------------------------------
324
+ let _override = null;
325
+ let _cache = null; // { key, cfg }
326
+
327
+ /** 供插件入口注入宿主给的 plugin config(等价于 cli 层) */
328
+ export function setConfigOverride(obj) {
329
+ if (obj && typeof obj === 'object') {
330
+ _override = { ...(_override ?? {}), ...obj };
331
+ _cache = null;
332
+ }
333
+ }
334
+ export function resetConfig() {
335
+ _override = null;
336
+ _cache = null;
337
+ }
338
+ /** @param {object} [cli] 显式覆盖(会与已 setConfigOverride 的合并) */
339
+ export function getConfig(cli = null) {
340
+ const merged = { ...(_override ?? {}), ...(cli ?? {}) };
341
+ let key;
342
+ try {
343
+ key = JSON.stringify(merged);
344
+ } catch {
345
+ key = null; // 含循环引用等不可序列化入参 ⇒ 不缓存
346
+ }
347
+ if (key !== null && _cache && _cache.key === key) return _cache.cfg;
348
+ const cfg = resolveConfig(merged);
349
+ if (key !== null) _cache = { key, cfg };
350
+ return cfg;
351
+ }
352
+
353
+ /** 来源自证:每个键"从哪读到的"(写进面板 meta,便于陌生环境排障) */
354
+ export function describeResolution(cfg = getConfig()) {
355
+ const out = {};
356
+ for (const [k, v] of Object.entries(cfg.sources ?? {})) out[k] = { value: v.value, from: v.from };
357
+ for (const key of SCRIPT_KEYS) {
358
+ out['scripts.' + key] = { value: cfg.scripts?.[key]?.path ?? null, from: cfg.scripts?.[key]?.from ?? null };
359
+ }
360
+ if (cfg.errors?.length) out.errors = { value: cfg.errors.length, from: 'config 解析期收集的失败项' };
361
+ return out;
362
+ }
@@ -0,0 +1,33 @@
1
+ // dsh-ecosystem-panel / lib/host-protocol.js
2
+ // 宿主协议常量(本插件只用到「web server 服务名」这一个符号)。
3
+ //
4
+ // 为什么单独一个文件、而不是引用宿主适配层:
5
+ // 本插件服务端只需要把 route 注册到宿主的 web server 服务上,因此 `inject` 里只能出现
6
+ // 服务名字面量。发布集**自带这一份常量**,使用者不需要自备任何适配层(开箱即用);
7
+ // 同时它是**唯一**与本插件耦合的宿主符号,升级宿主时只需核对这一行。
8
+ //
9
+ // 核对方法(升级主包后建议做一次):
10
+ // ① 在你自己的实例上确认 web server 服务名仍是 `webServer`
11
+ // (例如在主包里 grep 服务注册点,或在插件里打印 `ctx.get('webServer')` 是否非空);
12
+ // ② 若宿主把它改名,**不必改代码**:设环境变量
13
+ // DSH_ECOSYSTEM_PANEL_WEBSERVER_SERVICE=<新服务名>
14
+ // 即可(本文件在模块加载期读取,插件启动前设好即可生效)。
15
+ //
16
+ // 注:主包曾把若干服务名在 0.1.5 线收敛/改名(如 apiProxy / conversationEvents 等三方服务)。
17
+ // 本插件只依赖 webServer 这一个,未受影响;若日后 `webServer` 也被移除,症状是
18
+ // `inject` 找不到服务 ⇒ 插件不激活(面板按钮不出现),而不是静默出错。
19
+
20
+ /** 环境变量覆盖键(见文件头说明) */
21
+ const OVERRIDE = typeof process !== 'undefined' && process.env
22
+ ? String(process.env.DSH_ECOSYSTEM_PANEL_WEBSERVER_SERVICE ?? '').trim()
23
+ : '';
24
+
25
+ /** 宿主服务名常量表(本插件只用 webServer,其余按需增补) */
26
+ export const SVC = Object.freeze({
27
+ webServer: OVERRIDE || 'webServer'
28
+ });
29
+
30
+ /** 本插件用到的外部 API 前缀(与服务端 route 同源,客户端产物里不要另写一份) */
31
+ export const API_PREFIX = '/api/dsh-ecosystem-panel';
32
+
33
+ export const HOST_PROTOCOL_VERSION = '1.0.0';
package/lib/index.js ADDED
@@ -0,0 +1,59 @@
1
+ // dsh-ecosystem-panel / lib/index.js(服务端)
2
+ // 注册 GET /api/dsh-ecosystem-panel/state —— 只读聚合,无副作用,不加请求栅栏。
3
+ // 本 API 只暴露一个只读 GET(没有任何写操作入口),因此不需要控制面栅栏。
4
+ import { API_PREFIX, buildState } from './state.js';
5
+ // 宿主服务名常量(自带,不依赖任何外部适配层;服务名可经环境变量覆盖,见 host-protocol.js)
6
+ import { SVC } from './host-protocol.js';
7
+ import { setConfigOverride } from './config.js';
8
+
9
+ export const inject = [SVC.webServer];
10
+ export const name = 'dsh-ecosystem-panel';
11
+
12
+ const JSON_HEADERS = {
13
+ 'content-type': 'application/json; charset=utf-8',
14
+ 'referrer-policy': 'no-referrer'
15
+ };
16
+ function writeJson(res, status, body, headers = {}) {
17
+ const payload = JSON.stringify(body);
18
+ res.writeHead(status, { ...JSON_HEADERS, ...headers });
19
+ res.end(payload);
20
+ }
21
+
22
+ /**
23
+ * @param {object} ctx 宿主上下文(cordis)
24
+ * @param {object} [pluginConfig] 宿主传给插件的 config(可含 home / profile / toolsDir / patchManifest …
25
+ * 等任意配置层键;优先级高于配置文件与环境变量,见 lib/config.js)
26
+ */
27
+ function apply(ctx, pluginConfig) {
28
+ if (pluginConfig && typeof pluginConfig === 'object') setConfigOverride(pluginConfig);
29
+ ctx.effect(() => {
30
+ const disposers = [];
31
+ try {
32
+ disposers.push(ctx.webServer.register({
33
+ kind: 'exact',
34
+ path: `${API_PREFIX}/state`,
35
+ handler: async (req, res) => {
36
+ if (req.method !== 'GET') {
37
+ return writeJson(res, 405, { ok: false, error: 'method-not-allowed' }, { 'cache-control': 'no-store' });
38
+ }
39
+ try {
40
+ // ?refresh=1 强制重查 npm latest(默认走 24h 文件缓存,面板打开不卡)
41
+ const refresh = /(?:\?|&)refresh=1(?:&|$)/.test(req.url ?? '');
42
+ const state = await buildState({ refreshUpgrades: refresh });
43
+ writeJson(res, 200, state, { 'cache-control': 'no-store' });
44
+ } catch (error) {
45
+ writeJson(res, 500, { ok: false, error: String(error?.message ?? error) }, { 'cache-control': 'no-store' });
46
+ }
47
+ }
48
+ }));
49
+ } catch (error) {
50
+ for (const dispose of disposers) dispose();
51
+ throw error;
52
+ }
53
+ return () => {
54
+ for (const dispose of disposers) dispose();
55
+ };
56
+ }, 'ecosystem-panel: state route');
57
+ }
58
+
59
+ export { apply };