dsh-m 0.8.5 → 0.9.1
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 +30 -5
- package/README.md +30 -5
- package/docs/DESIGN.md +1 -1
- package/lib/cli.js +12 -0
- package/lib/client.js +159 -9
- 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 +27 -3
- package/package.json +1 -1
package/lib/core/registry.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
import { isAbsolute, join, normalize } from 'node:path';
|
|
9
9
|
import { constants as fsConstants, mkdirSync } from 'node:fs';
|
|
10
10
|
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
11
|
-
import {
|
|
11
|
+
import { WEB_PROFILE, cacheRoot } from './env.js';
|
|
12
12
|
import { decodeUtf8Fatal, fetchJsonLimitedMeta, describeFetchFailure } from './httpx.js';
|
|
13
13
|
export const CATEGORIES = ['market', 'tools', 'ui', 'search', 'other'];
|
|
14
14
|
/** 容量上限:2 MiB 是原始 UTF-8 bytes(不是字符数);条目超限拒绝整份清单。 */
|
|
@@ -367,14 +367,14 @@ export async function readRegistryFile(path, hooks = {}) {
|
|
|
367
367
|
const CACHE_FILE_VERSION = 2;
|
|
368
368
|
const DEFAULT_CACHE_KEY = 'default';
|
|
369
369
|
const ACTIVE_SOURCE_FILE = 'active-source.json';
|
|
370
|
-
function nsDir(namespace) {
|
|
371
|
-
return join(
|
|
370
|
+
function nsDir(namespace, profile = WEB_PROFILE) {
|
|
371
|
+
return join(cacheRoot(profile), namespace);
|
|
372
372
|
}
|
|
373
|
-
function cacheFilePath(namespace, cacheKey) {
|
|
374
|
-
return join(nsDir(namespace), `${cacheKey}.json`);
|
|
373
|
+
function cacheFilePath(namespace, cacheKey, profile = WEB_PROFILE) {
|
|
374
|
+
return join(nsDir(namespace, profile), `${cacheKey}.json`);
|
|
375
375
|
}
|
|
376
|
-
function cacheDirSyncMode() {
|
|
377
|
-
mkdirSync(
|
|
376
|
+
function cacheDirSyncMode(profile = WEB_PROFILE) {
|
|
377
|
+
mkdirSync(cacheRoot(profile), { recursive: true, mode: 0o700 });
|
|
378
378
|
}
|
|
379
379
|
/** 同 key 写锁:进程内串行化同一 cache 文件的写入。 */
|
|
380
380
|
const writeLocks = new Map();
|
|
@@ -390,11 +390,11 @@ function withKeyLock(key, task) {
|
|
|
390
390
|
return next;
|
|
391
391
|
}
|
|
392
392
|
/** 原子写:0600 临时文件(O_CREAT|O_EXCL)→ fsync → rename。目标为 symlink 时拒绝。 */
|
|
393
|
-
async function atomicWriteJson(target, dir, value) {
|
|
393
|
+
async function atomicWriteJson(target, dir, value, profile = WEB_PROFILE) {
|
|
394
394
|
return withKeyLock(target, async () => {
|
|
395
395
|
try {
|
|
396
396
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
397
|
-
cacheDirSyncMode();
|
|
397
|
+
cacheDirSyncMode(profile);
|
|
398
398
|
let existing = null;
|
|
399
399
|
try {
|
|
400
400
|
existing = await lstat(target);
|
|
@@ -421,12 +421,12 @@ async function atomicWriteJson(target, dir, value) {
|
|
|
421
421
|
}
|
|
422
422
|
});
|
|
423
423
|
}
|
|
424
|
-
async function writeCacheFile(file) {
|
|
425
|
-
return atomicWriteJson(cacheFilePath(file.namespace, file.cacheKey), nsDir(file.namespace), file);
|
|
424
|
+
async function writeCacheFile(file, profile = WEB_PROFILE) {
|
|
425
|
+
return atomicWriteJson(cacheFilePath(file.namespace, file.cacheKey, profile), nsDir(file.namespace, profile), file, profile);
|
|
426
426
|
}
|
|
427
|
-
async function readCacheFile(namespace, cacheKey) {
|
|
427
|
+
async function readCacheFile(namespace, cacheKey, profile = WEB_PROFILE) {
|
|
428
428
|
try {
|
|
429
|
-
const raw = JSON.parse(await readFile(cacheFilePath(namespace, cacheKey), 'utf8'));
|
|
429
|
+
const raw = JSON.parse(await readFile(cacheFilePath(namespace, cacheKey, profile), 'utf8'));
|
|
430
430
|
if (!raw || typeof raw !== 'object')
|
|
431
431
|
return null;
|
|
432
432
|
if (raw.version !== CACHE_FILE_VERSION || raw.namespace !== namespace || raw.cacheKey !== cacheKey)
|
|
@@ -449,9 +449,9 @@ function cacheFresh(file, ttlMin) {
|
|
|
449
449
|
return Date.now() - t < ttlMin * 60_000;
|
|
450
450
|
}
|
|
451
451
|
/** 清理 namespace 内非 default、非当前 custom 的 cache 文件(不动 metadata)。 */
|
|
452
|
-
async function pruneCaches(namespace, keepCustomKey) {
|
|
452
|
+
async function pruneCaches(namespace, keepCustomKey, profile = WEB_PROFILE) {
|
|
453
453
|
try {
|
|
454
|
-
const dir = nsDir(namespace);
|
|
454
|
+
const dir = nsDir(namespace, profile);
|
|
455
455
|
const entries = await readdir(dir).catch(() => []);
|
|
456
456
|
const keep = new Set([`${DEFAULT_CACHE_KEY}.json`, ACTIVE_SOURCE_FILE]);
|
|
457
457
|
if (keepCustomKey)
|
|
@@ -474,7 +474,7 @@ async function pruneCaches(namespace, keepCustomKey) {
|
|
|
474
474
|
* 再 prune 非当前 custom cache。metadata 失败 → 不 prune;prune 失败 → 保留旧 cache;
|
|
475
475
|
* 都只返回 warning,不反向撤销已成功的 settings update。
|
|
476
476
|
*/
|
|
477
|
-
export async function commitActiveSource(address, namespace) {
|
|
477
|
+
export async function commitActiveSource(address, namespace, profile = WEB_PROFILE) {
|
|
478
478
|
let metadataCommitted = true;
|
|
479
479
|
if (namespace === 'host') {
|
|
480
480
|
const meta = {
|
|
@@ -484,12 +484,12 @@ export async function commitActiveSource(address, namespace) {
|
|
|
484
484
|
cacheKey: address.cacheKey,
|
|
485
485
|
savedAt: new Date().toISOString(),
|
|
486
486
|
};
|
|
487
|
-
metadataCommitted = await atomicWriteJson(join(nsDir('host'), ACTIVE_SOURCE_FILE), nsDir('host'), meta);
|
|
487
|
+
metadataCommitted = await atomicWriteJson(join(nsDir('host', profile), ACTIVE_SOURCE_FILE), nsDir('host', profile), meta, profile);
|
|
488
488
|
}
|
|
489
489
|
if (!metadataCommitted) {
|
|
490
490
|
return { metadataCommitted: false, pruned: false, warning: 'accepted-source 元数据写入失败,已跳过旧 cache 清理(新配置仍生效)' };
|
|
491
491
|
}
|
|
492
|
-
const pruned = await pruneCaches(namespace, address.kind === 'default' ? null : address.cacheKey);
|
|
492
|
+
const pruned = await pruneCaches(namespace, address.kind === 'default' ? null : address.cacheKey, profile);
|
|
493
493
|
if (!pruned) {
|
|
494
494
|
return { metadataCommitted: true, pruned: false, warning: '旧来源 cache 清理失败(不影响新配置生效)' };
|
|
495
495
|
}
|
|
@@ -588,7 +588,7 @@ async function loadDefaultChain(cfg, opts, chain) {
|
|
|
588
588
|
source: candidate.source,
|
|
589
589
|
fetchedAt,
|
|
590
590
|
registry,
|
|
591
|
-
});
|
|
591
|
+
}, chain.profile);
|
|
592
592
|
return {
|
|
593
593
|
...buildState({
|
|
594
594
|
configuredAddress: '',
|
|
@@ -606,7 +606,7 @@ async function loadDefaultChain(cfg, opts, chain) {
|
|
|
606
606
|
errors.push(describeFetchFailure({ label: candidate.source, url: candidate.url, err, elapsedMs: Date.now() - startedAt }));
|
|
607
607
|
}
|
|
608
608
|
}
|
|
609
|
-
const cached = await readCacheFile(chain.namespace, DEFAULT_CACHE_KEY);
|
|
609
|
+
const cached = await readCacheFile(chain.namespace, DEFAULT_CACHE_KEY, chain.profile);
|
|
610
610
|
if (cached)
|
|
611
611
|
return loadedFromCacheFile(cached, '', errors);
|
|
612
612
|
if (chain.includeBundled) {
|
|
@@ -656,7 +656,7 @@ async function loadCustomChain(address, cfg, opts, chain) {
|
|
|
656
656
|
source,
|
|
657
657
|
fetchedAt,
|
|
658
658
|
registry: result.registry,
|
|
659
|
-
});
|
|
659
|
+
}, chain.profile);
|
|
660
660
|
return {
|
|
661
661
|
...buildState({
|
|
662
662
|
configuredAddress: address.normalized,
|
|
@@ -674,7 +674,7 @@ async function loadCustomChain(address, cfg, opts, chain) {
|
|
|
674
674
|
errors.push(describeFetchFailure({ label: source, url: address.normalized, err, elapsedMs: Date.now() - startedAt }));
|
|
675
675
|
}
|
|
676
676
|
if (chain.allowCacheFallback) {
|
|
677
|
-
const cached = await readCacheFile(chain.namespace, address.cacheKey);
|
|
677
|
+
const cached = await readCacheFile(chain.namespace, address.cacheKey, chain.profile);
|
|
678
678
|
if (cached)
|
|
679
679
|
return loadedFromCacheFile(cached, address.normalized, errors);
|
|
680
680
|
}
|
|
@@ -694,42 +694,45 @@ async function loadCustomChain(address, cfg, opts, chain) {
|
|
|
694
694
|
/** active 读取:default/custom 各自 fallback;网络成功后可在本 namespace 内 prune。 */
|
|
695
695
|
export async function loadRegistry(cfg = {}, opts = {}) {
|
|
696
696
|
const namespace = opts.namespace ?? 'host';
|
|
697
|
+
const profile = opts.profile ?? WEB_PROFILE;
|
|
697
698
|
const address = parseRegistryAddress(cfg.registryUrl);
|
|
698
699
|
const ttlMin = Math.max(0, cfg.cacheTtlMin ?? 60);
|
|
699
700
|
if (!opts.force) {
|
|
700
701
|
const cacheKey = address.kind === 'default' ? DEFAULT_CACHE_KEY : address.cacheKey;
|
|
701
|
-
const cached = await readCacheFile(namespace, cacheKey);
|
|
702
|
+
const cached = await readCacheFile(namespace, cacheKey, profile);
|
|
702
703
|
if (cached && cacheFresh(cached, ttlMin))
|
|
703
704
|
return loadedFromCacheFile(cached, address.normalized, []);
|
|
704
705
|
}
|
|
705
706
|
if (address.kind === 'default') {
|
|
706
|
-
const loaded = await loadDefaultChain(cfg, opts, { namespace, includeBundled: true });
|
|
707
|
+
const loaded = await loadDefaultChain(cfg, opts, { namespace, profile, includeBundled: true });
|
|
707
708
|
if (opts.prune !== false && loaded.status === 'ready')
|
|
708
|
-
await pruneCaches(namespace, null);
|
|
709
|
+
await pruneCaches(namespace, null, profile);
|
|
709
710
|
return loaded;
|
|
710
711
|
}
|
|
711
|
-
const loaded = await loadCustomChain(address, cfg, opts, { namespace, allowCacheFallback: true });
|
|
712
|
+
const loaded = await loadCustomChain(address, cfg, opts, { namespace, allowCacheFallback: true, profile });
|
|
712
713
|
if (opts.prune !== false && loaded.status === 'ready')
|
|
713
|
-
await pruneCaches(namespace, address.cacheKey);
|
|
714
|
+
await pruneCaches(namespace, address.cacheKey, profile);
|
|
714
715
|
return loaded;
|
|
715
716
|
}
|
|
716
717
|
/** candidate 读取:apply 前校验专用。只写候选 cache、绝不 prune;default 不落 bundled。 */
|
|
717
718
|
export async function loadRegistryCandidate(cfg, opts = {}) {
|
|
718
719
|
const namespace = opts.namespace ?? 'host';
|
|
720
|
+
const profile = opts.profile ?? WEB_PROFILE;
|
|
719
721
|
const address = parseRegistryAddress(cfg.registryUrl);
|
|
720
722
|
if (address.kind === 'default') {
|
|
721
|
-
return loadDefaultChain(cfg, opts, { namespace, includeBundled: false });
|
|
723
|
+
return loadDefaultChain(cfg, opts, { namespace, profile, includeBundled: false });
|
|
722
724
|
}
|
|
723
|
-
return loadCustomChain(address, cfg, opts, { namespace, allowCacheFallback: true });
|
|
725
|
+
return loadCustomChain(address, cfg, opts, { namespace, allowCacheFallback: true, profile });
|
|
724
726
|
}
|
|
725
727
|
/** default 显式加载(下载/主动刷新)。写 default cache,从不 prune。 */
|
|
726
728
|
export async function loadDefaultRegistry(cfg = {}, opts = {}) {
|
|
727
729
|
const namespace = opts.namespace ?? 'host';
|
|
730
|
+
const profile = opts.profile ?? WEB_PROFILE;
|
|
728
731
|
const ttlMin = Math.max(0, cfg.cacheTtlMin ?? 60);
|
|
729
732
|
if (!opts.force) {
|
|
730
|
-
const cached = await readCacheFile(namespace, DEFAULT_CACHE_KEY);
|
|
733
|
+
const cached = await readCacheFile(namespace, DEFAULT_CACHE_KEY, profile);
|
|
731
734
|
if (cached && cacheFresh(cached, ttlMin))
|
|
732
735
|
return loadedFromCacheFile(cached, '', []);
|
|
733
736
|
}
|
|
734
|
-
return loadDefaultChain(cfg, opts, { namespace, includeBundled: true });
|
|
737
|
+
return loadDefaultChain(cfg, opts, { namespace, profile, includeBundled: true });
|
|
735
738
|
}
|
package/lib/core/restart.js
CHANGED
|
@@ -32,17 +32,6 @@ function isLoopbackHost(host) {
|
|
|
32
32
|
return false;
|
|
33
33
|
return parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost' || parsed.hostname === '::1';
|
|
34
34
|
}
|
|
35
|
-
function hostsMatch(originHost, candidate) {
|
|
36
|
-
const a = parseHost(originHost);
|
|
37
|
-
const b = parseHost(candidate);
|
|
38
|
-
if (a === null || b === null)
|
|
39
|
-
return false;
|
|
40
|
-
if (a.hostname !== b.hostname)
|
|
41
|
-
return false;
|
|
42
|
-
if (a.port !== '' && b.port !== '' && a.port !== b.port)
|
|
43
|
-
return false;
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
35
|
export function servingPort(request) {
|
|
47
36
|
const host = headerString(request.headers.host);
|
|
48
37
|
if (host === undefined || !isLoopbackHost(host))
|
|
@@ -53,25 +42,9 @@ export function servingPort(request) {
|
|
|
53
42
|
const port = Number(match[1]);
|
|
54
43
|
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return false;
|
|
60
|
-
let from;
|
|
61
|
-
try {
|
|
62
|
-
const parsed = new URL(origin);
|
|
63
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
64
|
-
return false;
|
|
65
|
-
from = parsed.host;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
const host = headerString(request.headers.host);
|
|
71
|
-
const forwardedHost = headerString(request.headers['x-forwarded-host']);
|
|
72
|
-
const candidates = [host, forwardedHost].filter((value) => value !== undefined);
|
|
73
|
-
return candidates.some((candidate) => hostsMatch(from, candidate));
|
|
74
|
-
}
|
|
45
|
+
// 0.9.0 起trustedRestartRequest 已删除:/dshm 入口信任检查改委派官方
|
|
46
|
+
// `connection.requestRejection()`(ADR-0005,语义含 trustedHosts/cross-site/Origin:null),
|
|
47
|
+
// 本模块只剩 servingPort / 重启实现。
|
|
75
48
|
export function readProcCgroup(readFile = (path, encoding) => readFileSync(path, encoding)) {
|
|
76
49
|
try {
|
|
77
50
|
return readFile('/proc/self/cgroup', 'utf8');
|
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
|
});
|