@max-null/dsh-plugin-center 0.1.7 → 0.2.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/dist/engine.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { Service, type Context } from '@deepseek-ai/cordis';
9
9
  import { type InstalledPlugin } from './meta.ts';
10
10
  import { type MarketPlugin } from './market.ts';
11
- import { type UpdateDigest } from './update.ts';
11
+ import { type PnpmResult, type UpdateDigest } from './update.ts';
12
12
  declare module '@deepseek-ai/cordis' {
13
13
  interface Context {
14
14
  /** The plugin-center engine (provided by this package's host half). */
@@ -16,12 +16,26 @@ declare module '@deepseek-ai/cordis' {
16
16
  }
17
17
  }
18
18
  /** Which market directory the client wants to browse. */
19
- export type MarketSource = 'all' | 'awesome' | 'oh-my-dsh';
19
+ export type MarketSource = 'all' | 'awesome' | 'oh-my-dsh' | 'dsh-market';
20
20
  /** What's New read-mark result, returned by listMarket so the client waterfalls. */
21
21
  export interface MarketSnapshot {
22
22
  plugins: MarketPlugin[];
23
23
  done: boolean;
24
24
  }
25
+ /** One AI recommendation (suggest). */
26
+ export interface Suggestion {
27
+ name: string;
28
+ reason: string;
29
+ }
30
+ /** One-shot diagnostics report (diagnostics). */
31
+ export interface DiagnosticsReport {
32
+ dshVersion: string;
33
+ baseUrl: string;
34
+ node: string;
35
+ installed: InstalledPlugin[];
36
+ disabled: Record<string, boolean>;
37
+ pnpmLogTail: string;
38
+ }
25
39
  export declare class PluginCenterEngine extends Service {
26
40
  static inject: string[];
27
41
  private awesomeCache;
@@ -30,6 +44,11 @@ export declare class PluginCenterEngine extends Service {
30
44
  private ohMyDshCache;
31
45
  private ohMyDshDone;
32
46
  private ohMyDshFetching;
47
+ private dshMarketCache;
48
+ private dshMarketDone;
49
+ private dshMarketFetching;
50
+ /** README-extracted screenshot URL per plugin name (lazy, P2). */
51
+ private readonly screenshotCache;
33
52
  private installedNamesCache;
34
53
  private updatesCache;
35
54
  private readonly updatesTtlMs;
@@ -59,21 +78,17 @@ export declare class PluginCenterEngine extends Service {
59
78
  private fastNpmVersion;
60
79
  /** Start the Oh-My-DSH fetch once (single PLUGINS.md parse). */
61
80
  private prefetchOhMyDsh;
81
+ /** Start the dsh-market fetch once (2BingLing/dsh-market, ~3900 plugins, trimmed). */
82
+ private prefetchDshMarket;
62
83
  /** Installed plugin names (no file IO) — cached so market polling stays cheap. */
63
84
  private installedNames;
64
85
  /** Market snapshot for one source: what is cached so far, plus whether done. */
65
86
  listMarket(source?: MarketSource): Promise<MarketSnapshot>;
66
87
  /** Detect updates for every installed third-party/local plugin, TTL-cached. */
67
88
  checkUpdates(sinceIso: string): Promise<UpdateDigest[]>;
68
- install(spec: string): Promise<{
69
- ok: boolean;
70
- detail: string;
71
- }>;
89
+ install(spec: string): Promise<PnpmResult>;
72
90
  /** Update one installed plugin to the detected target version (exact — see update.ts). */
73
- update(name: string, version: string): Promise<{
74
- ok: boolean;
75
- detail: string;
76
- }>;
91
+ update(name: string, version: string): Promise<PnpmResult>;
77
92
  /** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
78
93
  private enqueuePnpm;
79
94
  /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
@@ -85,4 +100,18 @@ export declare class PluginCenterEngine extends Service {
85
100
  source: string;
86
101
  }[];
87
102
  }>;
103
+ /** Disable/enable one loader entry through the profile patch layer. */
104
+ toggle(id: string, disabled: boolean): Promise<{
105
+ ok: boolean;
106
+ detail: string;
107
+ nowDisabled: boolean | null;
108
+ }>;
109
+ /** One-shot diagnostics: environment, installed surface, patch stance, pnpm log tail. */
110
+ diagnostics(): Promise<DiagnosticsReport>;
111
+ /** Screenshot URL for one dsh-market plugin, lazily extracted from its README. */
112
+ screenshot(name: string): Promise<string | null>;
113
+ /** AI recommendation: keyword-filtered candidates ranked by the model. */
114
+ suggest(query: string): Promise<Suggestion[]>;
115
+ /** Wait for the dsh-market catalog (fetch or failure), with a hard deadline. */
116
+ private waitDshMarket;
88
117
  }
package/dist/engine.js CHANGED
@@ -9,9 +9,11 @@ import { Service } from '@deepseek-ai/cordis';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
12
+ import { existsSync, readFileSync } from 'node:fs';
12
13
  import { buildInstalledPlugin, clearPackageCache, resolvePackage } from "./meta.js";
13
- import { fetchAwesomePluginsJson, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins } from "./market.js";
14
+ import { fetchAwesomePluginsJson, fetchDshMarketPlugins, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins, } from "./market.js";
14
15
  import { detectUpdate, installPlugin, updatePlugin } from "./update.js";
16
+ import { readDisabledState, setDisabled } from "./toggle.js";
15
17
  /** Runtime mirror of cordis FiberState (a cross-package const enum). */
16
18
  const FIBER_PHASE = {
17
19
  0: 'pending',
@@ -23,6 +25,38 @@ const FIBER_PHASE = {
23
25
  };
24
26
  /** Sources that participate in update detection (official/builtin follow DSH itself). */
25
27
  const UPDATABLE = new Set(['installed', 'local']);
28
+ /**
29
+ * Extract the first image URL from a plugin's README (P2 screenshots):
30
+ * markdown or HTML image syntax, relative paths resolved against the raw
31
+ * branch root; only GitHub-hosted images are accepted.
32
+ */
33
+ async function extractFirstImage(repo) {
34
+ for (const branch of ['HEAD', 'master', 'main']) {
35
+ try {
36
+ const res = await fetch(`https://raw.githubusercontent.com/${repo}/${branch}/README.md`, {
37
+ signal: AbortSignal.timeout(12000),
38
+ });
39
+ if (!res.ok)
40
+ continue;
41
+ const text = await res.text();
42
+ const markdown = /!\[[^\]]*\]\(([^)]+)\)/u.exec(text);
43
+ const html = /<img[^>]+src=["']([^"']+)["']/iu.exec(text);
44
+ const raw = markdown?.[1] ?? html?.[1];
45
+ if (raw === undefined || raw === '')
46
+ continue;
47
+ const url = /^https?:\/\//u.test(raw)
48
+ ? raw
49
+ : `https://raw.githubusercontent.com/${repo}/${branch}/${raw.replace(/^\.?\//u, '')}`;
50
+ if (/^https:\/\/(raw\.)?githubusercontent\.com\//u.test(url))
51
+ return url;
52
+ if (/^https:\/\/github\.com\//u.test(url)) {
53
+ return url.replace(/^https:\/\/github\.com\/(.+?)\/blob\//u, 'https://raw.githubusercontent.com/$1/');
54
+ }
55
+ }
56
+ catch { /* try next branch */ }
57
+ }
58
+ return null;
59
+ }
26
60
  export class PluginCenterEngine extends Service {
27
61
  static inject = ['loader'];
28
62
  awesomeCache = [];
@@ -31,6 +65,11 @@ export class PluginCenterEngine extends Service {
31
65
  ohMyDshCache = [];
32
66
  ohMyDshDone = false;
33
67
  ohMyDshFetching = false;
68
+ dshMarketCache = [];
69
+ dshMarketDone = false;
70
+ dshMarketFetching = false;
71
+ /** README-extracted screenshot URL per plugin name (lazy, P2). */
72
+ screenshotCache = new Map();
34
73
  installedNamesCache = null;
35
74
  updatesCache = null;
36
75
  updatesTtlMs = 5 * 60_000;
@@ -51,6 +90,7 @@ export class PluginCenterEngine extends Service {
51
90
  catch { /* listInstalled is re-run on demand */ }
52
91
  this.prefetchAwesome();
53
92
  this.prefetchOhMyDsh();
93
+ this.prefetchDshMarket();
54
94
  }
55
95
  /** The profile directory (cordis.yml anchor) — the resolution and install cwd. */
56
96
  get baseUrl() {
@@ -179,6 +219,20 @@ export class PluginCenterEngine extends Service {
179
219
  this.ohMyDshFetching = false;
180
220
  })();
181
221
  }
222
+ /** Start the dsh-market fetch once (2BingLing/dsh-market, ~3900 plugins, trimmed). */
223
+ prefetchDshMarket() {
224
+ if (this.dshMarketFetching || this.dshMarketDone)
225
+ return;
226
+ this.dshMarketFetching = true;
227
+ void (async () => {
228
+ try {
229
+ this.dshMarketCache = mergePlugins([await fetchDshMarketPlugins()]);
230
+ }
231
+ catch { /* empty on failure */ }
232
+ this.dshMarketDone = true;
233
+ this.dshMarketFetching = false;
234
+ })();
235
+ }
182
236
  /** Installed plugin names (no file IO) — cached so market polling stays cheap. */
183
237
  async installedNames() {
184
238
  if (this.installedNamesCache !== null)
@@ -194,7 +248,7 @@ export class PluginCenterEngine extends Service {
194
248
  /** Market snapshot for one source: what is cached so far, plus whether done. */
195
249
  async listMarket(source = 'all') {
196
250
  const installedNames = await this.installedNames();
197
- const decorate = (plugins) => plugins.map(p => ({ ...p, installed: installedNames.has(p.name) }));
251
+ const decorate = (plugins) => plugins.map(p => ({ ...p, installed: installedNames.has(p.name) || installedNames.has(p.spec) }));
198
252
  if (source === 'awesome') {
199
253
  this.prefetchAwesome();
200
254
  return { plugins: decorate(this.awesomeCache), done: this.awesomeDone };
@@ -203,11 +257,20 @@ export class PluginCenterEngine extends Service {
203
257
  this.prefetchOhMyDsh();
204
258
  return { plugins: decorate(this.ohMyDshCache), done: this.ohMyDshDone };
205
259
  }
260
+ if (source === 'dsh-market') {
261
+ this.prefetchDshMarket();
262
+ // Score-first order (best first) so the big catalog reads usefully.
263
+ return {
264
+ plugins: decorate(this.dshMarketCache).sort((a, b) => (b.score?.total ?? 0) - (a.score?.total ?? 0)),
265
+ done: this.dshMarketDone,
266
+ };
267
+ }
206
268
  this.prefetchAwesome();
207
269
  this.prefetchOhMyDsh();
270
+ this.prefetchDshMarket();
208
271
  return {
209
- plugins: decorate(mergePlugins([this.awesomeCache, this.ohMyDshCache])),
210
- done: this.awesomeDone && this.ohMyDshDone,
272
+ plugins: decorate(mergePlugins([this.awesomeCache, this.ohMyDshCache, this.dshMarketCache])),
273
+ done: this.awesomeDone && this.ohMyDshDone && this.dshMarketDone,
211
274
  };
212
275
  }
213
276
  /** Detect updates for every installed third-party/local plugin, TTL-cached. */
@@ -250,4 +313,111 @@ export class PluginCenterEngine extends Service {
250
313
  installed: (await this.listInstalled()).map(p => ({ name: p.name, version: p.version, source: p.source })),
251
314
  };
252
315
  }
316
+ /** Disable/enable one loader entry through the profile patch layer. */
317
+ async toggle(id, disabled) {
318
+ const result = await setDisabled(this.baseUrl, id, disabled);
319
+ this.installedNamesCache = null;
320
+ return result;
321
+ }
322
+ /** One-shot diagnostics: environment, installed surface, patch stance, pnpm log tail. */
323
+ async diagnostics() {
324
+ const [installed, dshVersion] = await Promise.all([this.listInstalled(), this.dshVersion()]);
325
+ let pnpmLogTail = '';
326
+ try {
327
+ const logPath = join(this.baseUrl, 'plugin-center-pnpm.log');
328
+ if (existsSync(logPath)) {
329
+ const text = readFileSync(logPath, 'utf8');
330
+ pnpmLogTail = text.slice(-4000);
331
+ }
332
+ }
333
+ catch { /* best-effort */ }
334
+ return {
335
+ dshVersion,
336
+ baseUrl: this.baseUrl,
337
+ node: process.version,
338
+ installed,
339
+ disabled: Object.fromEntries(readDisabledState(join(this.baseUrl, 'cordis.patch.yml'))),
340
+ pnpmLogTail,
341
+ };
342
+ }
343
+ /** Screenshot URL for one dsh-market plugin, lazily extracted from its README. */
344
+ async screenshot(name) {
345
+ const cached = this.screenshotCache.get(name);
346
+ if (cached !== undefined)
347
+ return cached;
348
+ this.screenshotCache.set(name, null); // placeholder against concurrent dup fetches
349
+ const repo = this.dshMarketCache.find(p => p.name === name || p.spec === name)?.name ?? name;
350
+ const url = await extractFirstImage(repo);
351
+ this.screenshotCache.set(name, url);
352
+ return url;
353
+ }
354
+ /** AI recommendation: keyword-filtered candidates ranked by the model. */
355
+ async suggest(query) {
356
+ const q = query.trim();
357
+ if (q === '')
358
+ return [];
359
+ await this.waitDshMarket();
360
+ const llm = this.ctx.get('llm');
361
+ if (llm?.stream === undefined) {
362
+ throw new Error('llm 服务不可用(当前 profile 未提供 dsh-llm)');
363
+ }
364
+ const tokens = q.toLowerCase().split(/[\s,,、;;]+/u).filter(Boolean);
365
+ const scored = this.dshMarketCache
366
+ .map(p => ({
367
+ p,
368
+ hits: tokens.reduce((n, tok) => n + (p.name.toLowerCase().includes(tok)
369
+ || p.description.zh.toLowerCase().includes(tok)
370
+ || p.description.en.toLowerCase().includes(tok)
371
+ || p.categories.some(c => c.includes(tok)) ? 1 : 0), 0),
372
+ }))
373
+ .sort((a, b) => (b.hits - a.hits) || (b.p.stars ?? 0) - (a.p.stars ?? 0))
374
+ .slice(0, 25);
375
+ if (scored.length === 0)
376
+ return [];
377
+ const list = scored.map(({ p }) => `- ${p.name} | ${p.stars ?? 0}★ | ${p.description.zh.slice(0, 60)}`).join('\n');
378
+ const system = '你是 DeepSeek Harness 插件市场的推荐助手。根据用户需求从候选插件中选择 3-5 个最合适的,只输出一个 JSON 数组(不要 markdown 代码块):[{"name":"插件名","reason":"一句话中文推荐理由"}]';
379
+ const chunks = llm.stream({
380
+ provider: 'deepseek-official',
381
+ model: 'deepseek-v4-flash',
382
+ messages: [{ role: 'user', content: `用户需求:${q}\n\n候选插件列表(名称 | 星标 | 简介):\n${list}` }],
383
+ system,
384
+ maxTokens: 800,
385
+ signal: AbortSignal.timeout(40000),
386
+ });
387
+ let text = '';
388
+ let failed = false;
389
+ for await (const chunk of chunks) {
390
+ if (chunk.type === 'text-delta')
391
+ text += chunk.text ?? '';
392
+ else if (chunk.type === 'finish' && (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted'))
393
+ failed = true;
394
+ }
395
+ if (failed || text.trim() === '')
396
+ throw new Error('模型推荐失败,请重试');
397
+ const start = text.indexOf('[');
398
+ const end = text.lastIndexOf(']');
399
+ if (start < 0 || end <= start)
400
+ throw new Error(`模型输出无法解析:${text.slice(0, 200)}`);
401
+ try {
402
+ const parsed = JSON.parse(text.slice(start, end + 1));
403
+ if (!Array.isArray(parsed))
404
+ throw new Error('bad shape');
405
+ return parsed
406
+ .filter((item) => item !== null && typeof item === 'object'
407
+ && typeof item.name === 'string'
408
+ && typeof item.reason === 'string')
409
+ .slice(0, 5);
410
+ }
411
+ catch {
412
+ throw new Error(`模型输出无法解析:${text.slice(0, 200)}`);
413
+ }
414
+ }
415
+ /** Wait for the dsh-market catalog (fetch or failure), with a hard deadline. */
416
+ async waitDshMarket() {
417
+ this.prefetchDshMarket();
418
+ const deadline = Date.now() + 65_000;
419
+ while (!this.dshMarketDone && Date.now() < deadline) {
420
+ await new Promise(resolve => setTimeout(resolve, 200));
421
+ }
422
+ }
253
423
  }
package/dist/market.d.ts CHANGED
@@ -21,6 +21,12 @@ export interface MarketPlugin {
21
21
  /** Latest published version (npm), null until fetched / for non-npm plugins. */
22
22
  version: string | null;
23
23
  installed: boolean;
24
+ /** dsh-market five-dimension score (dsh-market source only), else null. */
25
+ score: {
26
+ total: number;
27
+ breakdown: Record<string, number>;
28
+ explanation: string;
29
+ } | null;
24
30
  }
25
31
  /** A per-source plugin record before merging. */
26
32
  interface RawPlugin {
@@ -34,6 +40,7 @@ interface RawPlugin {
34
40
  };
35
41
  stars: number | null;
36
42
  npm: string | null;
43
+ score: MarketPlugin['score'];
37
44
  }
38
45
  /** Map a bounded set of fetches concurrently, keeping per-fetch failures as null. */
39
46
  export declare function mapConcurrent<T>(items: readonly string[], limit: number, fn: (item: string) => Promise<T>): Promise<(T | null)[]>;
@@ -47,6 +54,13 @@ export declare function fetchOhMyDshOverrides(): Promise<Record<string, {
47
54
  category?: string;
48
55
  note?: string;
49
56
  }>>;
57
+ /**
58
+ * Fetch dsh-market's aggregated catalog (2BingLing/dsh-market plugins.json,
59
+ * ~3900 plugins with five-dimension scores and bilingual descriptions) in
60
+ * one request, then trim each entry to the market surface shape. The raw
61
+ * file is ~10 MB, so the trimmed result (~1.5 MB) is what the engine caches.
62
+ */
63
+ export declare function fetchDshMarketPlugins(): Promise<RawPlugin[]>;
50
64
  /** Parse Oh-My-DSH's PLUGINS.md (markdown table, sectioned by category). */
51
65
  export declare function fetchOhMyDshPlugins(): Promise<RawPlugin[]>;
52
66
  /** Merge raw per-source records by repo name: union categories, keep non-empty desc/stars. */
package/dist/market.js CHANGED
@@ -41,6 +41,7 @@ export async function fetchAwesomePluginsJson() {
41
41
  description: { en: p.description?.en ?? '', zh: p.description?.zh ?? '' },
42
42
  stars: typeof p.stars === 'number' ? p.stars : null,
43
43
  npm: typeof p.npm === 'string' && p.npm !== '' ? p.npm : null,
44
+ score: null,
44
45
  }));
45
46
  }
46
47
  /** Fetch Oh-My-DSH's curated overrides (min_stars filter + category/note overrides). */
@@ -58,6 +59,74 @@ export async function fetchOhMyDshOverrides() {
58
59
  return {};
59
60
  }
60
61
  }
62
+ /** Category keywords for dsh-market tag → CATEGORIES mapping (prefix match). */
63
+ const CATEGORY_KEYWORDS = [
64
+ ['ui', ['ui', 'interface', 'sidebar', 'panel', 'widget', '界面', '面板', '侧栏', '导航']],
65
+ ['theme', ['theme', 'skin', '主题', '皮肤', '壁纸']],
66
+ ['tools', ['tool', 'terminal', 'bash', '工具', '终端', '命令']],
67
+ ['model', ['model', 'llm', 'api', '模型', 'provider']],
68
+ ['session', ['session', '会话', 'history', '记忆回']],
69
+ ['memory', ['memory', '记忆']],
70
+ ['vision', ['vision', 'image', '图片', '视觉', 'screenshot']],
71
+ ['skill', ['skill', '技能', 'agent']],
72
+ ['workflow', ['workflow', 'workflow', '流程', 'automation', '自动化']],
73
+ ['notify', ['notify', '通知', 'toast', 'push']],
74
+ ['dev', ['dev', 'git', 'code', '开发', '代码', 'debug', '测试']],
75
+ ['fun', ['fun', '趣味', 'pet', '宠物', '游戏']],
76
+ ];
77
+ function categorizeTags(tags) {
78
+ const out = new Set();
79
+ for (const tag of tags ?? []) {
80
+ const low = tag.toLowerCase();
81
+ for (const [category, keywords] of CATEGORY_KEYWORDS) {
82
+ if (keywords.some(keyword => low.includes(keyword)))
83
+ out.add(category);
84
+ }
85
+ }
86
+ return [...out];
87
+ }
88
+ /**
89
+ * Fetch dsh-market's aggregated catalog (2BingLing/dsh-market plugins.json,
90
+ * ~3900 plugins with five-dimension scores and bilingual descriptions) in
91
+ * one request, then trim each entry to the market surface shape. The raw
92
+ * file is ~10 MB, so the trimmed result (~1.5 MB) is what the engine caches.
93
+ */
94
+ export async function fetchDshMarketPlugins() {
95
+ const res = await fetch('https://raw.githubusercontent.com/2BingLing/dsh-market/master/data/plugins.json', {
96
+ headers: UA,
97
+ signal: AbortSignal.timeout(60000),
98
+ });
99
+ if (!res.ok)
100
+ throw new Error(`dsh-market plugins.json: HTTP ${res.status}`);
101
+ const json = await res.json();
102
+ const out = [];
103
+ for (const p of json.plugins ?? []) {
104
+ const name = typeof p.name === 'string' && p.name !== '' ? p.name : '';
105
+ const full = typeof p.fullName === 'string' && p.fullName !== '' ? p.fullName : name;
106
+ if (full === '')
107
+ continue;
108
+ const descEn = typeof p.description === 'string' ? p.description : '';
109
+ const descZh = typeof p.descriptionZh === 'string' && p.descriptionZh !== '' ? p.descriptionZh : descEn;
110
+ out.push({
111
+ // Merge key / display name: owner/repo (matches the awesome source).
112
+ name: full,
113
+ url: `https://github.com/${full}`,
114
+ spec: name, // npm package name is the install spec
115
+ categories: categorizeTags(p.tags),
116
+ description: { en: descEn, zh: descZh },
117
+ stars: typeof p.stars === 'number' ? p.stars : null,
118
+ npm: name === '' ? null : name,
119
+ score: p.score !== null && typeof p.score === 'object'
120
+ ? {
121
+ total: typeof p.score.total === 'number' ? p.score.total : 0,
122
+ breakdown: p.score.breakdown ?? {},
123
+ explanation: typeof p.score.explanation === 'string' ? p.score.explanation : '',
124
+ }
125
+ : null,
126
+ });
127
+ }
128
+ return out;
129
+ }
61
130
  /** Parse Oh-My-DSH's PLUGINS.md (markdown table, sectioned by category). */
62
131
  export async function fetchOhMyDshPlugins() {
63
132
  try {
@@ -85,6 +154,7 @@ export async function fetchOhMyDshPlugins() {
85
154
  description: { en: '', zh: cells[6] },
86
155
  stars: Number.isFinite(stars) ? stars : null,
87
156
  npm: null,
157
+ score: null,
88
158
  });
89
159
  }
90
160
  return plugins;
@@ -108,6 +178,7 @@ export function mergePlugins(sources) {
108
178
  npm: null,
109
179
  version: null,
110
180
  installed: false,
181
+ score: null,
111
182
  };
112
183
  cur.categories = [...new Set([...cur.categories, ...item.categories])];
113
184
  if (item.description.en !== '')
@@ -118,6 +189,8 @@ export function mergePlugins(sources) {
118
189
  cur.stars = item.stars;
119
190
  if (item.npm !== null)
120
191
  cur.npm = item.npm;
192
+ if (item.score !== null)
193
+ cur.score = item.score;
121
194
  map.set(item.name, cur);
122
195
  }
123
196
  }
package/dist/rpc.js CHANGED
@@ -31,7 +31,7 @@ export class PluginCenterRpc extends Service {
31
31
  const result = await ctx.pluginCenter.install(spec);
32
32
  if (!result.ok)
33
33
  return internal(`install ${spec} 失败:${result.detail}`);
34
- return { ok: true, value: true };
34
+ return { ok: true, value: { durationMs: result.durationMs } };
35
35
  }
36
36
  case 'update': {
37
37
  const name = payload?.name;
@@ -43,7 +43,31 @@ export class PluginCenterRpc extends Service {
43
43
  const result = await ctx.pluginCenter.update(name, version);
44
44
  if (!result.ok)
45
45
  return internal(`update ${name} 失败:${result.detail}`);
46
- return { ok: true, value: true };
46
+ return { ok: true, value: { durationMs: result.durationMs } };
47
+ }
48
+ case 'toggle': {
49
+ const id = payload?.id;
50
+ const disabled = payload?.disabled;
51
+ if (typeof id !== 'string' || id === '')
52
+ return internal('toggle: id is required');
53
+ const result = await ctx.pluginCenter.toggle(id, disabled === true);
54
+ if (!result.ok)
55
+ return internal(`toggle ${id} 失败:${result.detail}`);
56
+ return { ok: true, value: { nowDisabled: result.nowDisabled } };
57
+ }
58
+ case 'diagnostics':
59
+ return { ok: true, value: await ctx.pluginCenter.diagnostics() };
60
+ case 'screenshot': {
61
+ const name = payload?.name;
62
+ if (typeof name !== 'string' || name === '')
63
+ return internal('screenshot: name is required');
64
+ return { ok: true, value: await ctx.pluginCenter.screenshot(name) };
65
+ }
66
+ case 'suggest': {
67
+ const query = payload?.query;
68
+ if (typeof query !== 'string')
69
+ return internal('suggest: query is required');
70
+ return { ok: true, value: await ctx.pluginCenter.suggest(query) };
47
71
  }
48
72
  case 'debug':
49
73
  return { ok: true, value: await ctx.pluginCenter.debug() };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hot disable/enable through the profile's user patch layer
3
+ * (`<profileDir>/cordis.patch.yml`) — the mechanism dsh-market ports from
4
+ * dsh-plugin-hub: a patch row `- id: X` + `disabled: true` stops that loader
5
+ * entry, `disabled: false` force-enables one a lower layer disabled. The
6
+ * official web profile re-composes via HMR (~1s, no restart); SSiD applies
7
+ * the same file on every boot, so the choice survives restarts there.
8
+ *
9
+ * Writes are line-level (the patch dialect is simple for toggles: a row id
10
+ * followed by an optional `disabled:` line), serialized so concurrent
11
+ * toggles cannot interleave a read-modify-write, refused when the file is
12
+ * not a plain entry list, and protected for host-infrastructure rows.
13
+ */
14
+ export interface ToggleResult {
15
+ ok: boolean;
16
+ detail: string;
17
+ /** The patch layer's stance after the write, or null when refused. */
18
+ nowDisabled: boolean | null;
19
+ }
20
+ /** What the user patch layer currently says about every row id. */
21
+ export declare function readDisabledState(patchPath: string): Map<string, boolean>;
22
+ /**
23
+ * Set one entry's disabled stance in the profile patch layer. The file is
24
+ * only touched when the stance changes; a malformed file (not a plain
25
+ * entry list) is reported instead of being made worse.
26
+ * @param profileDir - the profile directory holding cordis.patch.yml.
27
+ * @param id - the loader entry id to toggle.
28
+ * @param disabled - the target stance.
29
+ * @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
30
+ */
31
+ export declare function setDisabled(profileDir: string, id: string, disabled: boolean): Promise<ToggleResult>;