dsh-m 0.8.4 → 0.9.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/README.en.md +28 -5
- package/README.md +28 -5
- package/docs/DESIGN.md +1 -1
- package/lib/cli.js +12 -0
- package/lib/client.js +18 -6
- package/lib/core/active-profile.js +73 -0
- package/lib/core/community.js +6 -5
- package/lib/core/env.js +14 -2
- package/lib/core/host-api.js +66 -10
- package/lib/core/market.js +21 -13
- package/lib/core/patch-yaml.js +1 -1
- package/lib/core/profile-ops.js +158 -0
- package/lib/core/registry-controller.js +15 -13
- package/lib/core/registry.js +34 -31
- package/lib/core/restart.js +3 -30
- package/lib/host.js +41 -3
- package/lib/tools.js +38 -3
- package/package.json +1 -1
package/lib/host.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createApiDispatcher } from './core/host-api.js';
|
|
|
4
4
|
import { bindLoaderHost } from './core/live-plugin.js';
|
|
5
5
|
import { createRegistryController } from './core/registry-controller.js';
|
|
6
6
|
import { wireRegistrySettings, unwrapConfig } from './core/settings-compat.js';
|
|
7
|
+
import { resolveActiveProfile } from './core/active-profile.js';
|
|
7
8
|
import { registerTools } from './tools.js';
|
|
8
9
|
import { appExitFromContext, scheduleRestart } from './core/restart.js';
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
@@ -30,17 +31,21 @@ export const Config = Schema.object({
|
|
|
30
31
|
export function apply(ctx, config) {
|
|
31
32
|
// 卸载前的 live-disable 依赖 loader(skillhub 同款)
|
|
32
33
|
bindLoaderHost(ctx);
|
|
34
|
+
// 0.9.0 双 profile(ADR-0005):宿主当前 profile 单一事实源——apply 期解析一次,
|
|
35
|
+
// host 生命周期内不可变;GUI/工具/HTTP/缓存分段全部消费同一对象。
|
|
36
|
+
const profile = resolveActiveProfile(ctx);
|
|
37
|
+
ctx.logger?.info?.('dsh-m: active profile %s (dir=%s, source=%s)', profile.name, profile.dir, profile.source);
|
|
33
38
|
// registry controller:active config / configured / pending / rejected 分离 + generation fence;
|
|
34
39
|
// tools 与 Host API 共用同一 active config object(apply 原地更新字段,live 生效)。
|
|
35
40
|
// unwrapConfig:0.1.7 上 volatile 字段是 cosmokit Volatile 引用,先解包成 plain 值再进 controller。
|
|
36
|
-
const controller = createRegistryController(unwrapConfig(config));
|
|
41
|
+
const controller = createRegistryController(unwrapConfig(config), { profile: profile.name });
|
|
37
42
|
// DSH 0.1.2-rc.1 / 0.1.5-rc.1 / 0.1.7-rc.1 / 0.1.7-rc.2 都经 dsh-cmdline 暴露 appExit;
|
|
38
43
|
// using it avoids guessing the service unit from release-specific cgroups.
|
|
39
44
|
const appExit = appExitFromContext(ctx);
|
|
40
45
|
const restart = (port = null) => scheduleRestart(port, { appExit });
|
|
41
46
|
// 开关委派的官方服务探测(ADR-0001):运行时按服务存在性探测,不判 DSH 版本号
|
|
42
47
|
const getService = () => ctx.get?.('pluginManager');
|
|
43
|
-
registerTools(ctx, controller.config, { restart });
|
|
48
|
+
registerTools(ctx, controller.config, { restart }, profile);
|
|
44
49
|
// 设置页接线(双代兼容,详见 core/settings-compat.ts):
|
|
45
50
|
// - ≤0.1.5:settings.register('dshm', …) scope(get/update/watch);
|
|
46
51
|
// - 0.1.7-rc.1 / rc.2:Config `.volatile()` 字段自动生成设置页(autoGenerate 默认开,无需 configure),
|
|
@@ -67,15 +72,21 @@ export function apply(ctx, config) {
|
|
|
67
72
|
ctx.logger?.warn?.(err);
|
|
68
73
|
}
|
|
69
74
|
});
|
|
70
|
-
// 本地 API:单路由 + method
|
|
75
|
+
// 本地 API:单路由 + method 分发(信任检查与状态映射在 core/host-api.ts)。
|
|
76
|
+
// 0.9.0(ADR-0005):入口信任检查全量委派官方 connection.requestRejection——
|
|
77
|
+
// Desktop 桥剥 Origin 的请求按官方语义(trustedHosts/loopback/cross-site/Origin:null)判定,
|
|
78
|
+
// 服务缺席或抛错一律 fail-closed 403(warn 一次)。
|
|
71
79
|
ctx.inject(['webServer'], (c) => {
|
|
72
80
|
const server = c.webServer;
|
|
81
|
+
const rejectRequest = createRequestRejection(ctx);
|
|
73
82
|
const handleApi = createApiDispatcher({
|
|
74
83
|
controller,
|
|
75
84
|
pkg,
|
|
85
|
+
profile,
|
|
76
86
|
deps: {
|
|
77
87
|
scheduleRestart: restart,
|
|
78
88
|
getService,
|
|
89
|
+
rejectRequest,
|
|
79
90
|
},
|
|
80
91
|
});
|
|
81
92
|
server.register({
|
|
@@ -87,3 +98,30 @@ export function apply(ctx, config) {
|
|
|
87
98
|
});
|
|
88
99
|
});
|
|
89
100
|
}
|
|
101
|
+
/** 守卫式官方 requestRejection 委派:缺席/抛错 fail-closed 403(warn 一次,不刷日志)。 */
|
|
102
|
+
function createRequestRejection(ctx) {
|
|
103
|
+
let warned = false;
|
|
104
|
+
const warnOnce = (err) => {
|
|
105
|
+
if (warned)
|
|
106
|
+
return;
|
|
107
|
+
warned = true;
|
|
108
|
+
ctx.logger?.warn?.('dsh-m: 官方 connection.requestRejection 不可用,/dshm 全入口 fail-closed 403(ADR-0005)');
|
|
109
|
+
if (err)
|
|
110
|
+
ctx.logger?.warn?.(err);
|
|
111
|
+
};
|
|
112
|
+
return (req) => {
|
|
113
|
+
try {
|
|
114
|
+
const connection = ctx.get?.('connection');
|
|
115
|
+
const reject = connection?.requestRejection;
|
|
116
|
+
if (typeof reject !== 'function') {
|
|
117
|
+
warnOnce(undefined);
|
|
118
|
+
return 403;
|
|
119
|
+
}
|
|
120
|
+
return reject.call(connection, { headers: req.headers }) ?? undefined;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
warnOnce(err);
|
|
124
|
+
return 403;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -4,6 +4,8 @@ import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin
|
|
|
4
4
|
import { COMMUNITY_CATEGORY_LABELS } from './core/community.js';
|
|
5
5
|
import { appExitFromContext, scheduleRestart } from './core/restart.js';
|
|
6
6
|
import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
|
|
7
|
+
import { resolveActiveProfile, assertWriteAllowed } from './core/active-profile.js';
|
|
8
|
+
import { desktopInstallFromRegistry, desktopToggle } from './core/profile-ops.js';
|
|
7
9
|
export const CATEGORY_LABELS = {
|
|
8
10
|
market: '市场',
|
|
9
11
|
tools: '工具',
|
|
@@ -55,7 +57,7 @@ function cloneJson(value) {
|
|
|
55
57
|
function summaryOf(state) {
|
|
56
58
|
return { isDefault: state.isDefault, status: state.status, stale: state.stale };
|
|
57
59
|
}
|
|
58
|
-
export function registerTools(ctx, cfg, deps = {}) {
|
|
60
|
+
export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfile(ctx)) {
|
|
59
61
|
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
60
62
|
const m = {
|
|
61
63
|
listMarket: deps.listMarket ?? listMarket,
|
|
@@ -63,6 +65,8 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
63
65
|
installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
|
|
64
66
|
uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
|
|
65
67
|
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
68
|
+
desktopInstall: deps.desktopInstall ?? desktopInstallFromRegistry,
|
|
69
|
+
desktopToggle: deps.desktopToggle ?? desktopToggle,
|
|
66
70
|
};
|
|
67
71
|
const restart = deps.restart ?? ((port = null) => scheduleRestart(port, { appExit: appExitFromContext(ctx) }));
|
|
68
72
|
const toggle = deps.togglePlugin ?? coreTogglePlugin;
|
|
@@ -132,6 +136,8 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
132
136
|
namespace: 'host',
|
|
133
137
|
deadlineMs: SEARCH_CORE_DEADLINE_MS,
|
|
134
138
|
signal: exec?.signal,
|
|
139
|
+
profileDir: profile.dir,
|
|
140
|
+
profile: profile.name,
|
|
135
141
|
});
|
|
136
142
|
// 安装标注唯一来源:listMarket 的单次 profile 快照。
|
|
137
143
|
// 状态不完整且存在可被误标的条目时 fail-closed——不把未知安装状态呈现成未安装;
|
|
@@ -173,7 +179,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
173
179
|
}));
|
|
174
180
|
ctx.tools.register(defineTool({
|
|
175
181
|
name: 'dshm_list',
|
|
176
|
-
description: 'List plugins installed in the DSH web
|
|
182
|
+
description: 'List plugins installed in the current DSH profile (web or desktop), annotated with 市场安装/非市场安装, sources, and outdated flags. Use when the user asks what plugins are installed or wants to manage local plugins.',
|
|
177
183
|
parameters: {},
|
|
178
184
|
output: {
|
|
179
185
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -192,6 +198,8 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
192
198
|
namespace: 'host',
|
|
193
199
|
deadlineMs: INSTALLED_CORE_DEADLINE_MS,
|
|
194
200
|
signal: exec?.signal,
|
|
201
|
+
profileDir: profile.dir,
|
|
202
|
+
profile: profile.name,
|
|
195
203
|
});
|
|
196
204
|
return cloneJson({
|
|
197
205
|
registry: summaryOf(result.registryState),
|
|
@@ -239,6 +247,11 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
239
247
|
throw new Error('缺少收录 id');
|
|
240
248
|
const version = typeof args.version === 'string' && args.version.trim() ? args.version.trim() : undefined;
|
|
241
249
|
const force = args.force === true;
|
|
250
|
+
// 0.9.0:desktop → 官方 pluginManager 委派(新包);未知 profile → 结构化拒绝
|
|
251
|
+
if (profile.kind !== 'web') {
|
|
252
|
+
assertWriteAllowed(profile, 'install');
|
|
253
|
+
return cloneJson(await m.desktopInstall(id, cfg, { version, forceIncompatible: force, namespace: 'host', profile: profile.name }));
|
|
254
|
+
}
|
|
242
255
|
try {
|
|
243
256
|
return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host' }));
|
|
244
257
|
}
|
|
@@ -280,12 +293,17 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
280
293
|
throw new Error('缺少 pkg');
|
|
281
294
|
if (typeof args.enabled !== 'boolean')
|
|
282
295
|
throw new Error('缺少 enabled(boolean)');
|
|
296
|
+
// 0.9.0:desktop → 官方管理器委派(服务缺席结构化拒绝);web → 既有委派/降级
|
|
297
|
+
if (profile.kind !== 'web') {
|
|
298
|
+
assertWriteAllowed(profile, 'set-enabled');
|
|
299
|
+
return cloneJson(await m.desktopToggle(target, args.enabled, { getService: getService, profileDir: profile.dir }));
|
|
300
|
+
}
|
|
283
301
|
return cloneJson(await toggle(target, args.enabled, { getService: getService }));
|
|
284
302
|
},
|
|
285
303
|
}));
|
|
286
304
|
ctx.tools.register(defineTool({
|
|
287
305
|
name: 'dshm_uninstall',
|
|
288
|
-
description: 'Uninstall a DSH plugin from the
|
|
306
|
+
description: 'Uninstall a DSH plugin from the current DSH profile by package name (pkg from dshm_list). Confirm with the user BEFORE calling. Does not delete plugin data; reports leftover paths instead.',
|
|
289
307
|
parameters: {
|
|
290
308
|
pkg: { type: 'string', required: true, description: '包名 from dshm_list, e.g. dsh-web-search' },
|
|
291
309
|
},
|
|
@@ -305,6 +323,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
305
323
|
const target = String(args.pkg || '').trim();
|
|
306
324
|
if (!target)
|
|
307
325
|
throw new Error('缺少 pkg');
|
|
326
|
+
assertWriteAllowed(profile, 'uninstall');
|
|
308
327
|
return cloneJson(await m.uninstallPlugin(target, cfg, { namespace: 'host' }));
|
|
309
328
|
},
|
|
310
329
|
}));
|
|
@@ -338,6 +357,8 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
338
357
|
namespace: 'host',
|
|
339
358
|
deadlineMs: INSTALLED_CORE_DEADLINE_MS,
|
|
340
359
|
signal: exec?.signal,
|
|
360
|
+
profileDir: profile.dir,
|
|
361
|
+
profile: profile.name,
|
|
341
362
|
});
|
|
342
363
|
const items = result.items.map((it) => ({
|
|
343
364
|
pkg: it.pkg,
|
|
@@ -385,6 +406,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
385
406
|
if (!target)
|
|
386
407
|
throw new Error('缺少 pkg');
|
|
387
408
|
const force = args.force === true;
|
|
409
|
+
assertWriteAllowed(profile, 'upgrade');
|
|
388
410
|
try {
|
|
389
411
|
return cloneJson(await m.upgradePlugin(target, cfg, { forceIncompatible: force, namespace: 'host' }));
|
|
390
412
|
}
|
|
@@ -416,6 +438,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
416
438
|
}),
|
|
417
439
|
timeoutMs: 15_000,
|
|
418
440
|
async execute() {
|
|
441
|
+
assertWriteAllowed(profile, 'restart');
|
|
419
442
|
return cloneJson(restart(null));
|
|
420
443
|
},
|
|
421
444
|
}));
|
|
@@ -431,6 +454,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
431
454
|
'For installed plugins, call dshm_list / dshm_outdated. Upgrade only after the user confirms which one: dshm_upgrade. Uninstall only after confirmation: dshm_uninstall.',
|
|
432
455
|
'dshm_toggle switches a plugin on/off: confirm first unless the user already said it in the same message (把 skins 关掉 → execute directly); report live vs restart-required accordingly. ',
|
|
433
456
|
'dshm_restart only after the user agrees to restart; afterwards tell them to refresh once the page recovers.',
|
|
457
|
+
'dsh-m operates on the current host profile (web or desktop, per ping.profile): on desktop only read-only market + install-new-package + toggle work; upgrade/uninstall/self-upgrade/restart return structured refusals with guidance — relay them instead of retrying.',
|
|
434
458
|
].join(' '),
|
|
435
459
|
});
|
|
436
460
|
});
|
|
@@ -541,6 +565,17 @@ function renderOutdated(out) {
|
|
|
541
565
|
return head.join('\n');
|
|
542
566
|
}
|
|
543
567
|
function renderUpgrade(out) {
|
|
568
|
+
// 0.8.5 修复:升级工具会命中装后守卫拦截(复用 installEntry,link/file prior 不可回退等),
|
|
569
|
+
// execute 返回 guard 形态结果——旧 render 没有该分支,把拦截渲染成「✅ undefined 已升级」假成功。
|
|
570
|
+
if (out.guard === true) {
|
|
571
|
+
return [
|
|
572
|
+
`⛔ 升级被装后守卫拦截:${out.message ?? ''}`,
|
|
573
|
+
`终态:${out.compensation?.status ?? '—'}(${out.compensation?.note ?? ''})`,
|
|
574
|
+
out.repairBasis ? `修复依据:${out.repairBasis}` : null,
|
|
575
|
+
out.restartSafe === true ? '可以重启 DSH Web。' : '修复后再重启(不要现在一键重启)。',
|
|
576
|
+
'对用户如实说明守卫拦截与终态,不得说「已升级成功」或「已恢复原版本」。',
|
|
577
|
+
].filter(Boolean).join('\n');
|
|
578
|
+
}
|
|
544
579
|
const from = out.fromVersion ? `v${out.fromVersion} → ` : '';
|
|
545
580
|
const to = out.version ? `v${out.version}` : out.sha ? out.sha.slice(0, 7) : '最新';
|
|
546
581
|
const extra = buildsNote(out);
|