@max-null/dsh-plugin-center 0.2.12 → 0.2.14
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/client.js +514 -17
- package/dist/engine.d.ts +27 -3
- package/dist/engine.js +90 -6
- package/dist/llm-log.d.ts +25 -0
- package/dist/llm-log.js +70 -0
- package/dist/rpc.js +45 -3
- package/dist/toggle.d.ts +15 -1
- package/dist/toggle.js +69 -4
- package/dist/update.d.ts +66 -0
- package/dist/update.js +232 -3
- package/package.json +7 -4
- package/skills/dsh-plugin-upgrade/SKILL.md +121 -0
package/dist/engine.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
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 PnpmResult, type UpdateDigest } from './update.ts';
|
|
11
|
+
import { type LlmUpdatePackage, type PnpmResult, type UpdateDigest } from './update.ts';
|
|
12
|
+
import { type LlmLogRecord } from './llm-log.ts';
|
|
12
13
|
declare module '@deepseek-ai/cordis' {
|
|
13
14
|
interface Context {
|
|
14
15
|
/** The plugin-center engine (provided by this package's host half). */
|
|
@@ -75,6 +76,11 @@ export declare class PluginCenterEngine extends Service {
|
|
|
75
76
|
readVersions(): Promise<Record<string, string>>;
|
|
76
77
|
/** Persist the read-mark (best-effort; a quota/IO failure just loses the mark). */
|
|
77
78
|
markRead(versions: Record<string, string>): Promise<void>;
|
|
79
|
+
/** 弹窗当天已展示标记(2026-08-27):host 文件(DSH web 端口随机,
|
|
80
|
+
* localStorage 按 origin 隔离会丢;文件侧稳定)。 */
|
|
81
|
+
private get whatsNewDailyPath();
|
|
82
|
+
whatsNewDaily(): Promise<string>;
|
|
83
|
+
markWhatsNewDaily(day: string): Promise<void>;
|
|
78
84
|
/** Current DSH version, read from the installed @deepseek-ai/dsh package. */
|
|
79
85
|
dshVersion(): Promise<string>;
|
|
80
86
|
/** Non-group Loader entries, cross-matched with market categories. */
|
|
@@ -105,6 +111,20 @@ export declare class PluginCenterEngine extends Service {
|
|
|
105
111
|
* - 官方 dsh web(无消费方)→ 仿社区市场返回可复制 CLI 指令;
|
|
106
112
|
* 3. 非锁失败(网络/版本)→ 原样报错。 */
|
|
107
113
|
update(name: string, version: string): Promise<PnpmResult>;
|
|
114
|
+
/** LLM 驱动更新准备:采集信息包供确认面板/会话 prompt 使用(2026-08-28)。
|
|
115
|
+
* 只读采集(npm/GitHub/本地 package.json),不执行任何安装——执行由 LLM
|
|
116
|
+
* Agent 在「插件更新」会话中按 skill 决策后完成。 */
|
|
117
|
+
llmUpdatePrepare(name: string): Promise<LlmUpdatePackage | null>;
|
|
118
|
+
/** 追加一条 LLM 更新动作日志(JSONL,供 client 轮询结果展示)。 */
|
|
119
|
+
appendLlmUpdateLog(entry: {
|
|
120
|
+
name: string;
|
|
121
|
+
action: string;
|
|
122
|
+
detail: string;
|
|
123
|
+
status: 'pending' | 'running' | 'success' | 'failed';
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
/** 读取某个插件最近一条 LLM 更新动作(JSONL 逆序找 name 匹配);
|
|
126
|
+
* 无记录返回 null。client 轮询据此做三态(进行中/成功/失败)。 */
|
|
127
|
+
readLlmUpdateResult(name: string): Promise<LlmLogRecord | null>;
|
|
108
128
|
/** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
|
|
109
129
|
private enqueuePnpm;
|
|
110
130
|
/** Temporary diagnostics for the empty-update bug; removed once root-caused. */
|
|
@@ -116,8 +136,12 @@ export declare class PluginCenterEngine extends Service {
|
|
|
116
136
|
source: string;
|
|
117
137
|
}[];
|
|
118
138
|
}>;
|
|
119
|
-
/** Disable/enable one loader entry through the profile patch layer.
|
|
120
|
-
|
|
139
|
+
/** Disable/enable one loader entry through the profile patch layer.
|
|
140
|
+
* 2026-08-25 禁用失效:`dsh plugin add` 清单的 insert 子条目无 id,loader
|
|
141
|
+
* 每次启动分配随机运行时 id,按它写禁用行重启后永远匹配不到。当 patch
|
|
142
|
+
* 文件中没有 `- id: <entryId>` 行时,改用该条目的包名 name 作寻址键
|
|
143
|
+
* (setDisabled 内按 name 把 insert 子条目升级为稳定 id 后再写禁用行)。 */
|
|
144
|
+
toggle(id: string, name: string, disabled: boolean): Promise<{
|
|
121
145
|
ok: boolean;
|
|
122
146
|
detail: string;
|
|
123
147
|
nowDisabled: boolean | null;
|
package/dist/engine.js
CHANGED
|
@@ -12,9 +12,10 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
12
12
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { buildInstalledPlugin, clearPackageCache, resolvePackage } from "./meta.js";
|
|
14
14
|
import { fetchAwesomePluginsJson, fetchDshMarketPlugins, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins, } from "./market.js";
|
|
15
|
-
import { detectUpdate, installPlugin, preparePluginUpdate, updatePlugin } from "./update.js";
|
|
15
|
+
import { detectUpdate, installPlugin, preparePluginUpdate, updatePlugin, buildLlmPackage, dependencySpecifierOf, isSameUpstream, sourceOf, } from "./update.js";
|
|
16
16
|
import { reconcileInstalled, readDependencyKeys } from "./reconcile.js";
|
|
17
|
-
import { readDisabledState, setDisabled } from "./toggle.js";
|
|
17
|
+
import { readDisabledState, setDisabled, escapeRegExp } from "./toggle.js";
|
|
18
|
+
import { appendLlmLog, readLlmLogLatest } from "./llm-log.js";
|
|
18
19
|
/** Runtime mirror of cordis FiberState (a cross-package const enum). */
|
|
19
20
|
const FIBER_PHASE = {
|
|
20
21
|
0: 'pending',
|
|
@@ -135,6 +136,30 @@ export class PluginCenterEngine extends Service {
|
|
|
135
136
|
}
|
|
136
137
|
catch { /* best-effort */ }
|
|
137
138
|
}
|
|
139
|
+
/** 弹窗当天已展示标记(2026-08-27):host 文件(DSH web 端口随机,
|
|
140
|
+
* localStorage 按 origin 隔离会丢;文件侧稳定)。 */
|
|
141
|
+
get whatsNewDailyPath() {
|
|
142
|
+
return join(this.dshHome, 'plugin-center-whatsnew-daily.json');
|
|
143
|
+
}
|
|
144
|
+
async whatsNewDaily() {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(await readFile(this.whatsNewDailyPath, 'utf8'));
|
|
147
|
+
return typeof parsed === 'object' && parsed !== null && typeof parsed.day === 'string'
|
|
148
|
+
? parsed.day
|
|
149
|
+
: '';
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async markWhatsNewDaily(day) {
|
|
156
|
+
try {
|
|
157
|
+
const path = this.whatsNewDailyPath;
|
|
158
|
+
await mkdir(dirname(path), { recursive: true });
|
|
159
|
+
await writeFile(path, JSON.stringify({ day, at: new Date().toISOString() }), 'utf8');
|
|
160
|
+
}
|
|
161
|
+
catch { /* best-effort */ }
|
|
162
|
+
}
|
|
138
163
|
/** Current DSH version, read from the installed @deepseek-ai/dsh package. */
|
|
139
164
|
async dshVersion() {
|
|
140
165
|
const resolved = await resolvePackage(this.baseUrl, '@deepseek-ai/dsh');
|
|
@@ -310,7 +335,22 @@ export class PluginCenterEngine extends Service {
|
|
|
310
335
|
return hit.digests;
|
|
311
336
|
const [installed, localDsh] = await Promise.all([this.listInstalled(), this.dshVersion()]);
|
|
312
337
|
const candidates = installed.filter(p => UPDATABLE.has(p.source) && p.version !== null);
|
|
313
|
-
const digests = await Promise.all(candidates.map(
|
|
338
|
+
const digests = await Promise.all(candidates.map(async (p) => {
|
|
339
|
+
// 同名异源保护(2026-08-29):本地自定义来源(依赖声明 vendor/tarball/
|
|
340
|
+
// local-file)时,校验 npm 同名包是否同一上游——不一致(如 dream12347
|
|
341
|
+
// 定制 vs hkkz9522 独立同名项目)排除出更新列表,防误报与覆盖定制。
|
|
342
|
+
// 无法判定(任一侧缺 repository,如 dsh-session-manager 0.2.2 作者未
|
|
343
|
+
// 声明)同样排除:宁少报不漏报——漏报只少一个提示,误报是 LLM 白跑 +
|
|
344
|
+
// 用户被误导;人工核对入口见 InstalledView/LLM 会话。
|
|
345
|
+
const spec = dependencySpecifierOf(this.baseUrl, p.name);
|
|
346
|
+
const src = spec === null ? 'npm' : sourceOf(spec, this.baseUrl);
|
|
347
|
+
if (src === 'vendor' || src === 'tarball' || src === 'local-file') {
|
|
348
|
+
const same = await isSameUpstream(p.repoUrl, p.name);
|
|
349
|
+
if (same !== true)
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
return detectUpdate(p.name, p.version, p.repoUrl, p.compatRange, localDsh, sinceIso);
|
|
353
|
+
}));
|
|
314
354
|
this.updatesCache = { since: sinceIso, at: now, digests: digests.filter((d) => d !== null) };
|
|
315
355
|
return this.updatesCache.digests;
|
|
316
356
|
}
|
|
@@ -357,6 +397,29 @@ export class PluginCenterEngine extends Service {
|
|
|
357
397
|
const profileName = basename(this.baseUrl) || 'web';
|
|
358
398
|
return { ok: true, detail: '', durationMs: direct.durationMs, command: `dsh plugin --profile ${profileName} add ${name}@${version}` };
|
|
359
399
|
}
|
|
400
|
+
/** LLM 驱动更新准备:采集信息包供确认面板/会话 prompt 使用(2026-08-28)。
|
|
401
|
+
* 只读采集(npm/GitHub/本地 package.json),不执行任何安装——执行由 LLM
|
|
402
|
+
* Agent 在「插件更新」会话中按 skill 决策后完成。 */
|
|
403
|
+
async llmUpdatePrepare(name) {
|
|
404
|
+
// 从已安装列表找该插件的元数据(版本/repo/兼容范围)
|
|
405
|
+
const installed = await this.listInstalled();
|
|
406
|
+
const p = installed.find(i => i.name === name);
|
|
407
|
+
if (p === undefined || p.version === null)
|
|
408
|
+
return null;
|
|
409
|
+
const localDsh = await this.dshVersion();
|
|
410
|
+
const since = new Date(Date.now() - 30 * 24 * 3600 * 1000).toISOString();
|
|
411
|
+
// detectUpdate 同款采集参数,复用 buildLlmPackage
|
|
412
|
+
return buildLlmPackage(p.name, p.version, p.repoUrl, p.compatRange, localDsh, since, this.baseUrl);
|
|
413
|
+
}
|
|
414
|
+
/** 追加一条 LLM 更新动作日志(JSONL,供 client 轮询结果展示)。 */
|
|
415
|
+
async appendLlmUpdateLog(entry) {
|
|
416
|
+
await appendLlmLog(entry);
|
|
417
|
+
}
|
|
418
|
+
/** 读取某个插件最近一条 LLM 更新动作(JSONL 逆序找 name 匹配);
|
|
419
|
+
* 无记录返回 null。client 轮询据此做三态(进行中/成功/失败)。 */
|
|
420
|
+
async readLlmUpdateResult(name) {
|
|
421
|
+
return readLlmLogLatest(name);
|
|
422
|
+
}
|
|
360
423
|
/** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
|
|
361
424
|
enqueuePnpm(op) {
|
|
362
425
|
const run = this.pnpmChain.then(async () => {
|
|
@@ -378,9 +441,30 @@ export class PluginCenterEngine extends Service {
|
|
|
378
441
|
installed: (await this.listInstalled()).map(p => ({ name: p.name, version: p.version, source: p.source })),
|
|
379
442
|
};
|
|
380
443
|
}
|
|
381
|
-
/** Disable/enable one loader entry through the profile patch layer.
|
|
382
|
-
|
|
383
|
-
|
|
444
|
+
/** Disable/enable one loader entry through the profile patch layer.
|
|
445
|
+
* 2026-08-25 禁用失效:`dsh plugin add` 清单的 insert 子条目无 id,loader
|
|
446
|
+
* 每次启动分配随机运行时 id,按它写禁用行重启后永远匹配不到。当 patch
|
|
447
|
+
* 文件中没有 `- id: <entryId>` 行时,改用该条目的包名 name 作寻址键
|
|
448
|
+
* (setDisabled 内按 name 把 insert 子条目升级为稳定 id 后再写禁用行)。 */
|
|
449
|
+
async toggle(id, name, disabled) {
|
|
450
|
+
// 老调用方(未透传 name 的 client)兜底:从 loader 实时取包名。
|
|
451
|
+
const entryName = name !== '' ? name : [...this.ctx.loader.entries()].find(e => e.id === id)?.options.name ?? '';
|
|
452
|
+
if (entryName === '') {
|
|
453
|
+
return { ok: false, detail: `entry "${id}" not found in loader`, nowDisabled: null };
|
|
454
|
+
}
|
|
455
|
+
// 寻址键:patch 文件中已有 `- id: <id>` 行 → 稳定 id 直接用;否则是该
|
|
456
|
+
// 条目的随机运行时 id → 只能用 name 找到它的 insert 子条目。
|
|
457
|
+
const patchId = id.replace(/^include:/u, '');
|
|
458
|
+
let key = entryName;
|
|
459
|
+
try {
|
|
460
|
+
const patchPath = join(this.baseUrl, 'cordis.patch.yml');
|
|
461
|
+
if (existsSync(patchPath)
|
|
462
|
+
&& new RegExp(`^- id: ${escapeRegExp(patchId)}$`, 'm').test(readFileSync(patchPath, 'utf8'))) {
|
|
463
|
+
key = patchId;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch { /* 读失败:走 name 寻址,由 setDisabled 报具体错误 */ }
|
|
467
|
+
const result = await setDisabled(this.baseUrl, key, entryName, disabled);
|
|
384
468
|
this.installedNamesCache = null;
|
|
385
469
|
return result;
|
|
386
470
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type LlmLogStatus = 'pending' | 'running' | 'success' | 'failed';
|
|
2
|
+
export interface LlmLogEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
action: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
status: LlmLogStatus;
|
|
7
|
+
}
|
|
8
|
+
/** 最近一条记录(JSONL 逆序找 name 匹配;坏行跳过;无记录/文件缺失 → null)。 */
|
|
9
|
+
export interface LlmLogRecord {
|
|
10
|
+
at: number;
|
|
11
|
+
name: string;
|
|
12
|
+
action: string;
|
|
13
|
+
detail: string;
|
|
14
|
+
status: LlmLogStatus;
|
|
15
|
+
}
|
|
16
|
+
/** 日志文件路径(DSH_HOME 未设时回退 ~/.dsh)。 */
|
|
17
|
+
export declare function llmLogPath(dshHome?: string | undefined): string;
|
|
18
|
+
/** 追加一条记录(目录自动创建;失败静默——日志绝不阻断主流程)。 */
|
|
19
|
+
export declare function appendLlmLog(entry: LlmLogEntry, dshHome?: string | undefined): Promise<void>;
|
|
20
|
+
/** 解析一行 JSON:合法且 name 匹配 → 记录;其他 → null。 */
|
|
21
|
+
export declare function parseLlmLogLine(line: string, name: string): LlmLogRecord | null;
|
|
22
|
+
/** 读取某插件最近一条记录(逆序;文件缺失/全坏行 → null)。 */
|
|
23
|
+
export declare function readLlmLogLatest(name: string, dshHome?: string | undefined): Promise<LlmLogRecord | null>;
|
|
24
|
+
/** 按文本扫最后一行含 name 的 JSON(提取解析出的 attr——测试断言用)。 */
|
|
25
|
+
export declare function extractLlmLogField(line: string): Record<string, unknown> | null;
|
package/dist/llm-log.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 更新动作日志(JSONL)——纯文件读写模块(无 engine 依赖,可单测)。
|
|
3
|
+
* 文件位置:$DSH_HOME/plugin-center/llm-update-log.jsonl
|
|
4
|
+
* 行格式:{"at":number,"name":string,"action":string,"detail":string,"status":...}
|
|
5
|
+
* client 侧轮询 llm-update.result 依赖此文件做三态(执行中/成功/失败)。
|
|
6
|
+
*/
|
|
7
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
/** 日志文件路径(DSH_HOME 未设时回退 ~/.dsh)。 */
|
|
11
|
+
export function llmLogPath(dshHome = process.env.DSH_HOME) {
|
|
12
|
+
return join(dshHome ?? join(homedir(), '.dsh'), 'plugin-center', 'llm-update-log.jsonl');
|
|
13
|
+
}
|
|
14
|
+
/** 追加一条记录(目录自动创建;失败静默——日志绝不阻断主流程)。 */
|
|
15
|
+
export async function appendLlmLog(entry, dshHome) {
|
|
16
|
+
try {
|
|
17
|
+
const file = llmLogPath(dshHome);
|
|
18
|
+
await mkdir(dirname(file), { recursive: true });
|
|
19
|
+
const line = JSON.stringify({ at: Date.now(), ...entry }) + '\n';
|
|
20
|
+
await appendFile(file, line, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch { /* best-effort */ }
|
|
23
|
+
}
|
|
24
|
+
/** 解析一行 JSON:合法且 name 匹配 → 记录;其他 → null。 */
|
|
25
|
+
export function parseLlmLogLine(line, name) {
|
|
26
|
+
try {
|
|
27
|
+
const rec = JSON.parse(line);
|
|
28
|
+
if (rec.name !== name)
|
|
29
|
+
return null;
|
|
30
|
+
const status = rec.status === 'pending' || rec.status === 'running' || rec.status === 'success' || rec.status === 'failed' ? rec.status : 'running';
|
|
31
|
+
return {
|
|
32
|
+
at: typeof rec.at === 'number' ? rec.at : 0,
|
|
33
|
+
name,
|
|
34
|
+
action: typeof rec.action === 'string' ? rec.action : '',
|
|
35
|
+
detail: typeof rec.detail === 'string' ? rec.detail : '',
|
|
36
|
+
status,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** 读取某插件最近一条记录(逆序;文件缺失/全坏行 → null)。 */
|
|
44
|
+
export async function readLlmLogLatest(name, dshHome) {
|
|
45
|
+
try {
|
|
46
|
+
const text = await readFile(llmLogPath(dshHome), 'utf8');
|
|
47
|
+
const lines = text.split('\n');
|
|
48
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
49
|
+
const line = lines[i]?.trim();
|
|
50
|
+
if (line === undefined || line === '')
|
|
51
|
+
continue;
|
|
52
|
+
const rec = parseLlmLogLine(line, name);
|
|
53
|
+
if (rec !== null)
|
|
54
|
+
return rec;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** 按文本扫最后一行含 name 的 JSON(提取解析出的 attr——测试断言用)。 */
|
|
63
|
+
export function extractLlmLogField(line) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(line);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/rpc.js
CHANGED
|
@@ -54,12 +54,46 @@ export class PluginCenterRpc extends Service {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
case 'llm-update.prepare': {
|
|
58
|
+
// LLM 驱动更新信息包(只读采集):来源/版本/兼容/变更,供确认面板与
|
|
59
|
+
// 会话 prompt。执行由 LLM Agent 在插件更新会话中按 skill 决策完成。
|
|
60
|
+
const name = payload?.name;
|
|
61
|
+
if (typeof name !== 'string' || name === '')
|
|
62
|
+
return internal('llm-update.prepare: name is required');
|
|
63
|
+
const pkg = await ctx.pluginCenter.llmUpdatePrepare(name);
|
|
64
|
+
if (pkg === null)
|
|
65
|
+
return internal('llm-update.prepare: 插件不存在或版本未知');
|
|
66
|
+
return { ok: true, value: pkg };
|
|
67
|
+
}
|
|
68
|
+
case 'llm-update.log': {
|
|
69
|
+
// 追加一条 LLM 更新动作日志(host JSONL,client 轮询结果展示)。
|
|
70
|
+
const p = payload;
|
|
71
|
+
if (typeof p?.name !== 'string' || typeof p.status !== 'string')
|
|
72
|
+
return internal('llm-update.log: bad payload');
|
|
73
|
+
await ctx.pluginCenter.appendLlmUpdateLog({
|
|
74
|
+
name: p.name,
|
|
75
|
+
action: typeof p.action === 'string' ? p.action : '',
|
|
76
|
+
detail: typeof p.detail === 'string' ? p.detail : '',
|
|
77
|
+
status: p.status,
|
|
78
|
+
});
|
|
79
|
+
return { ok: true, value: null };
|
|
80
|
+
}
|
|
81
|
+
case 'llm-update.result': {
|
|
82
|
+
// 读某插件最近一条 LLM 更新动作(轮询三态:running/success/failed)。
|
|
83
|
+
const name = payload?.name;
|
|
84
|
+
if (typeof name !== 'string' || name === '')
|
|
85
|
+
return internal('llm-update.result: name is required');
|
|
86
|
+
return { ok: true, value: await ctx.pluginCenter.readLlmUpdateResult(name) };
|
|
87
|
+
}
|
|
57
88
|
case 'toggle': {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
89
|
+
const payload2 = payload;
|
|
90
|
+
const id = payload2?.id;
|
|
91
|
+
const name = payload2?.name;
|
|
92
|
+
const disabled = payload2?.disabled;
|
|
60
93
|
if (typeof id !== 'string' || id === '')
|
|
61
94
|
return internal('toggle: id is required');
|
|
62
|
-
|
|
95
|
+
// name 用于无稳定 id 条目的 seek-by-name 寻址(2026-08-25 禁用失效修复)。
|
|
96
|
+
const result = await ctx.pluginCenter.toggle(id, typeof name === 'string' ? name : '', disabled === true);
|
|
63
97
|
if (!result.ok)
|
|
64
98
|
return internal(`toggle ${id} 失败:${result.detail}`);
|
|
65
99
|
return { ok: true, value: { nowDisabled: result.nowDisabled } };
|
|
@@ -86,6 +120,14 @@ export class PluginCenterRpc extends Service {
|
|
|
86
120
|
const versions = payload?.versions ?? {};
|
|
87
121
|
return { ok: true, value: await ctx.pluginCenter.markRead(versions) };
|
|
88
122
|
}
|
|
123
|
+
case 'whatsNewDaily':
|
|
124
|
+
return { ok: true, value: await ctx.pluginCenter.whatsNewDaily() };
|
|
125
|
+
case 'markWhatsNewDaily': {
|
|
126
|
+
const day = payload?.day;
|
|
127
|
+
if (typeof day !== 'string' || day === '')
|
|
128
|
+
return internal('markWhatsNewDaily: day is required');
|
|
129
|
+
return { ok: true, value: await ctx.pluginCenter.markWhatsNewDaily(day) };
|
|
130
|
+
}
|
|
89
131
|
default:
|
|
90
132
|
return internal(`unknown endpoint "${endpoint}"`);
|
|
91
133
|
}
|
package/dist/toggle.d.ts
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
* followed by an optional `disabled:` line), serialized so concurrent
|
|
11
11
|
* toggles cannot interleave a read-modify-write, refused when the file is
|
|
12
12
|
* not a plain entry list, and protected for host-infrastructure rows.
|
|
13
|
+
*
|
|
14
|
+
* Stable ids: `dsh plugin add` install lists mount entries as id-less
|
|
15
|
+
* `insert` children (`- name: X`), which the Loader gives a RANDOM runtime
|
|
16
|
+
* id on every boot (cordis-plugin-loader ensureId). Toggling by that
|
|
17
|
+
* runtime id writes a row no later boot matches (applyEntryPatches warns
|
|
18
|
+
* and skips) — the 2026-08-25 disable-broken bug. When no `- id:` row
|
|
19
|
+
* matches, this module addresses the entry by `name` instead: the id-less
|
|
20
|
+
* insert child is upgraded in place to a stable `- id: X` so the appended
|
|
21
|
+
* disable row actually hits. It never guesses: no match = refused write.
|
|
13
22
|
*/
|
|
14
23
|
export interface ToggleResult {
|
|
15
24
|
ok: boolean;
|
|
@@ -19,13 +28,18 @@ export interface ToggleResult {
|
|
|
19
28
|
}
|
|
20
29
|
/** What the user patch layer currently says about every row id. */
|
|
21
30
|
export declare function readDisabledState(patchPath: string): Map<string, boolean>;
|
|
31
|
+
/** Escape a literal for use inside a RegExp (plugin names may contain `.` etc.). */
|
|
32
|
+
export declare function escapeRegExp(text: string): string;
|
|
22
33
|
/**
|
|
23
34
|
* Set one entry's disabled stance in the profile patch layer. The file is
|
|
24
35
|
* only touched when the stance changes; a malformed file (not a plain
|
|
25
36
|
* entry list) is reported instead of being made worse.
|
|
26
37
|
* @param profileDir - the profile directory holding cordis.patch.yml.
|
|
27
38
|
* @param id - the loader entry id to toggle.
|
|
39
|
+
* @param name - the entry's package name; used as the addressing key when
|
|
40
|
+
* `id` is a Loader-assigned random runtime id with no `- id:` row in the
|
|
41
|
+
* patch file (id-less insert children of `dsh plugin add` lists).
|
|
28
42
|
* @param disabled - the target stance.
|
|
29
43
|
* @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
|
|
30
44
|
*/
|
|
31
|
-
export declare function setDisabled(profileDir: string, entryId: string, disabled: boolean): Promise<ToggleResult>;
|
|
45
|
+
export declare function setDisabled(profileDir: string, entryId: string, name: string, disabled: boolean): Promise<ToggleResult>;
|
package/dist/toggle.js
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
* followed by an optional `disabled:` line), serialized so concurrent
|
|
11
11
|
* toggles cannot interleave a read-modify-write, refused when the file is
|
|
12
12
|
* not a plain entry list, and protected for host-infrastructure rows.
|
|
13
|
+
*
|
|
14
|
+
* Stable ids: `dsh plugin add` install lists mount entries as id-less
|
|
15
|
+
* `insert` children (`- name: X`), which the Loader gives a RANDOM runtime
|
|
16
|
+
* id on every boot (cordis-plugin-loader ensureId). Toggling by that
|
|
17
|
+
* runtime id writes a row no later boot matches (applyEntryPatches warns
|
|
18
|
+
* and skips) — the 2026-08-25 disable-broken bug. When no `- id:` row
|
|
19
|
+
* matches, this module addresses the entry by `name` instead: the id-less
|
|
20
|
+
* insert child is upgraded in place to a stable `- id: X` so the appended
|
|
21
|
+
* disable row actually hits. It never guesses: no match = refused write.
|
|
13
22
|
*/
|
|
14
23
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
24
|
import { join } from 'node:path';
|
|
@@ -89,6 +98,46 @@ export function readDisabledState(patchPath) {
|
|
|
89
98
|
}
|
|
90
99
|
return state;
|
|
91
100
|
}
|
|
101
|
+
/** Escape a literal for use inside a RegExp (plugin names may contain `.` etc.). */
|
|
102
|
+
export function escapeRegExp(text) {
|
|
103
|
+
return text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Address a `dsh plugin add` install-list insert child by name: an id-less
|
|
107
|
+
* child (` - name: X`, 4-space indent under `- insert:`) gets upgraded in
|
|
108
|
+
* place to a stable-id form (` - id: X` + ` name: X`), then a
|
|
109
|
+
* `- id: X / disabled: <bool>` row is appended. A child that already carries
|
|
110
|
+
* the stable id (` - id: X`) just gets the row appended (the upgrade must
|
|
111
|
+
* not regress to a random runtime id). The insert block always stays before
|
|
112
|
+
* the appended row, so applyEntryPatches registers the id before the toggle
|
|
113
|
+
* row reads it. Returns false when nothing matches (or `name` is empty) —
|
|
114
|
+
* callers must refuse the write, never append blindly.
|
|
115
|
+
*/
|
|
116
|
+
function patchInsertChildByName(lines, name, disabled) {
|
|
117
|
+
if (name === '')
|
|
118
|
+
return false;
|
|
119
|
+
const idPattern = new RegExp(`^ {4}- id: ${escapeRegExp(name)}$`, 'u');
|
|
120
|
+
const namePattern = new RegExp(`^ {4}- name: ${escapeRegExp(name)}$`, 'u');
|
|
121
|
+
let found = false;
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
if (namePattern.test(lines[i])) {
|
|
124
|
+
// 无 id 子条目 → 原地升级为稳定 id(4 空格 + 6 空格 name)。
|
|
125
|
+
lines[i] = ` - id: ${name}\n name: ${name}`;
|
|
126
|
+
found = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (idPattern.test(lines[i])) {
|
|
130
|
+
// 已是稳定 id 子条目 → 无需升级,直接追加禁用行。
|
|
131
|
+
found = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!found)
|
|
136
|
+
return false;
|
|
137
|
+
const tail = lines.length > 0 && lines[lines.length - 1] !== '' ? '\n' : '';
|
|
138
|
+
lines.push(`${tail}- id: ${name}\n disabled: ${String(disabled)}`);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
92
141
|
/** Serialize toggles so concurrent writes cannot interleave. */
|
|
93
142
|
let toggleChain = Promise.resolve();
|
|
94
143
|
/**
|
|
@@ -97,10 +146,13 @@ let toggleChain = Promise.resolve();
|
|
|
97
146
|
* entry list) is reported instead of being made worse.
|
|
98
147
|
* @param profileDir - the profile directory holding cordis.patch.yml.
|
|
99
148
|
* @param id - the loader entry id to toggle.
|
|
149
|
+
* @param name - the entry's package name; used as the addressing key when
|
|
150
|
+
* `id` is a Loader-assigned random runtime id with no `- id:` row in the
|
|
151
|
+
* patch file (id-less insert children of `dsh plugin add` lists).
|
|
100
152
|
* @param disabled - the target stance.
|
|
101
153
|
* @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
|
|
102
154
|
*/
|
|
103
|
-
export function setDisabled(profileDir, entryId, disabled) {
|
|
155
|
+
export function setDisabled(profileDir, entryId, name, disabled) {
|
|
104
156
|
const run = toggleChain.then(async () => {
|
|
105
157
|
// Loader runtime ids (include:<id>) never match patch composition —
|
|
106
158
|
// always address rows by their original patch id.
|
|
@@ -157,9 +209,22 @@ export function setDisabled(profileDir, entryId, disabled) {
|
|
|
157
209
|
out.push(line);
|
|
158
210
|
}
|
|
159
211
|
if (!patched) {
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
212
|
+
// 2026-08-25 禁用失效根因:id-less insert 子条目每次启动拿随机运行时
|
|
213
|
+
// id,按它追加的禁用行重启后永远匹配不到(applyEntryPatches warn 后
|
|
214
|
+
// 静默跳过)。所以这里绝不静默追加:先按 name 寻址 insert 子条目行,
|
|
215
|
+
// 原地升级为稳定 id 再追加;都找不到 → 拒绝(不写文件)。
|
|
216
|
+
if (!patchInsertChildByName(out, name, disabled)) {
|
|
217
|
+
if (/^[0-9a-f]{8}$/u.test(id)) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
detail: `entry "${id}" has no stable patch id (random runtime id); ` +
|
|
221
|
+
'edit cordis.patch.yml to give its insert child an explicit id',
|
|
222
|
+
nowDisabled: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { ok: false, detail: `no patch row or insert child matches "${id}"`, nowDisabled: null };
|
|
226
|
+
}
|
|
227
|
+
patched = true;
|
|
163
228
|
}
|
|
164
229
|
try {
|
|
165
230
|
writeFileSync(patchPath, out.join('\n') + '\n');
|
package/dist/update.d.ts
CHANGED
|
@@ -7,8 +7,24 @@ export interface UpdateDigest {
|
|
|
7
7
|
compat: 'compatible' | 'incompatible' | 'unknown';
|
|
8
8
|
compatRange: string | null;
|
|
9
9
|
}
|
|
10
|
+
/** 服务面判定:目标客户端 bundle 是否深度依赖 Remote BFF(ctx.remote.*)。
|
|
11
|
+
* SSiD 内核(0.1.x)无 remote BFF 服务(走 /plugin-center RPC channel),
|
|
12
|
+
* 这类版本在 SSiD 上必然「pending waiting for service: remote.session」。
|
|
13
|
+
* 案例: dsh-sidebar-qa 0.4.1/0.4.2(2026-08-29 两次实崩)。 */
|
|
14
|
+
export declare function clientBundleUsesRemote(content: string): boolean;
|
|
15
|
+
/** 下载目标 tgz 并抽取 client bundle,判定 remote 服务依赖。
|
|
16
|
+
* 仅返回 boolean 不用 pnpm(直接 registry 下载 tgz + bsdtar 抽文件)。 */
|
|
17
|
+
export declare function targetClientUsesRemote(name: string, version: string): Promise<boolean>;
|
|
10
18
|
/** Latest published version on the npm registry; null when unreachable/unpublished. */
|
|
11
19
|
export declare function npmLatest(packageName: string): Promise<string | null>;
|
|
20
|
+
/** 测试用:清空 repository 缓存(生产无调用)。 */
|
|
21
|
+
export declare function clearNpmRepoCache(): void;
|
|
22
|
+
/** 读 npm 包根级 repository.url(带 24h 缓存;失败/缺失 null)。 */
|
|
23
|
+
export declare function npmRepository(packageName: string): Promise<string | null>;
|
|
24
|
+
/** 仓库 URL 归一化(去 scheme/git+ 前缀/尾 .git/尾斜杠/大小写)用于同源比较。 */
|
|
25
|
+
export declare function normalizeRepoUrl(url: string): string;
|
|
26
|
+
/** 同源判定:true=同一上游;false=同名异源;null=无法判定(任一侧缺 repo)。 */
|
|
27
|
+
export declare function isSameUpstream(localRepoUrl: string | null, packageName: string): Promise<boolean | null>;
|
|
12
28
|
/** Commit-message changelog: the reliable source for repos without release notes. */
|
|
13
29
|
export declare function fetchCommitChangelog(repoUrl: string | null, sinceIso: string): Promise<string[]>;
|
|
14
30
|
/**
|
|
@@ -64,6 +80,15 @@ export declare function logPnpm(profileDir: string, args: readonly string[], res
|
|
|
64
80
|
* CreateProcess 只找 pnpm.exe(.cmd/.ps1 必须经 shell)——2026-08-17 实测
|
|
65
81
|
* spawn('pnpm', shell:false) 直接 ENOENT,更新永远假成功。 */
|
|
66
82
|
export declare function pnpmCandidates(): string[];
|
|
83
|
+
/** Wrap a bundled pnpm CLI path into a runnable command line. SSID_PNPM
|
|
84
|
+
* (SSiD 捆绑 pnpm) points at `pnpm.cjs` — a node script. On Windows,
|
|
85
|
+
* spawning it directly through `shell: true` makes cmd hand the .cjs to
|
|
86
|
+
* its file association (ShellExecute): cmd returns exit 0 immediately and
|
|
87
|
+
* node never runs (2026-08-25 another machine: 142ms fake success, npm
|
|
88
|
+
* add 未生效; output redirection test produced a 0-byte file). Non-.cjs
|
|
89
|
+
* paths (e.g. pnpm.exe) are used as-is.
|
|
90
|
+
*/
|
|
91
|
+
export declare function pnpmExecCommand(bundled: string): string;
|
|
67
92
|
/** 归档 profile 的 node_modules 由 pnpm `<major>` 生成(SSiD 部署时把构建机
|
|
68
93
|
* store 元数据改写成本机路径且保留 major 后缀——shell/main.mjs rewire)。
|
|
69
94
|
* 若执行机全局 pnpm 是另一个 major(常见:机器装 pnpm 10,归档是 pnpm 11
|
|
@@ -113,3 +138,44 @@ export declare function hostFilesChanged(before: FileIdentity[], after: FileIden
|
|
|
113
138
|
* `minimumReleaseAgeExclude` itself (2026-08-18, reproduced in-process).
|
|
114
139
|
*/
|
|
115
140
|
export declare function updatePlugin(packageName: string, version: string, profileDir: string): Promise<PnpmResult>;
|
|
141
|
+
/** 插件来源判定:依据依赖声明形态 + profile 目录(复用前置设计 §4.2 算法)。
|
|
142
|
+
* vendor 定制是 SSiD 生态核心(open-sea-skin/genui/panels 均本地魔改),
|
|
143
|
+
* 机械更新会把 file: 覆盖回 npm —— 来源标记给 LLM 决策「保持 vendor」。 */
|
|
144
|
+
export type PluginSource = 'official' | 'npm' | 'vendor' | 'tarball' | 'local-file';
|
|
145
|
+
export declare function sourceOf(specifier: string, profileDir: string): PluginSource;
|
|
146
|
+
/** 读 profile dependencies 里该插件的声明形态(npm 纯净 / file: vendor / link: 等)。 */
|
|
147
|
+
export declare function dependencySpecifierOf(profileDir: string, name: string): string | null;
|
|
148
|
+
/** LLM 更新信息包:在 UpdateDigest 基础上补充来源/定制标记,驱动 Agent 决策。 */
|
|
149
|
+
export interface LlmUpdatePackage {
|
|
150
|
+
name: string;
|
|
151
|
+
/** 当前本地版本(实体 package.json)。 */
|
|
152
|
+
fromVersion: string;
|
|
153
|
+
/** npm latest(可 null=未发布/不可达,LLM 走 GitHub commit 路径)。 */
|
|
154
|
+
toVersion: string | null;
|
|
155
|
+
/** GitHub commit changelog(更新前后差异,截前 10 条)。 */
|
|
156
|
+
changelog: string[];
|
|
157
|
+
/** DSH 兼容性(peer 检查)。 */
|
|
158
|
+
compat: 'compatible' | 'incompatible' | 'unknown';
|
|
159
|
+
/** peer 声明的 DSH 版本范围。 */
|
|
160
|
+
compatRange: string | null;
|
|
161
|
+
/** 来源判定。 */
|
|
162
|
+
source: PluginSource;
|
|
163
|
+
/** 依赖声明形态(file:/github:等等),机械更新可能覆盖定制的线索。 */
|
|
164
|
+
specifier: string | null;
|
|
165
|
+
/** 是否本地定制(vendor/tarball/local-file)。 */
|
|
166
|
+
isVendorModified: boolean;
|
|
167
|
+
/** 插件所在 profile 目录(host 运行时锚点;LLM 只允许在此目录内操作)。
|
|
168
|
+
* 内部技术字段——不直接展示给用户,由 runtimeLabel 承担语义化表达。 */
|
|
169
|
+
profileDir: string;
|
|
170
|
+
/** 同名异源警告:npm 同名包与本地上游不是同一项目(repository 不一致)。
|
|
171
|
+
* 真机案例: dsh-session-manager(dream12347 定制)vs npm 0.4.1(独立项目)。 */
|
|
172
|
+
upstreamMismatch: boolean;
|
|
173
|
+
/** 用户可读的环境标签(小白视角):'SSID'(思灵应用内)/'DSH-WEB'。 */
|
|
174
|
+
runtimeLabel: string;
|
|
175
|
+
/** 已组装的 Agent prompt(host 单一来源,client 直接注入会话)。 */
|
|
176
|
+
prompt: string;
|
|
177
|
+
}
|
|
178
|
+
/** 采集一个插件用于 LLM 更新的完整信息包。 */
|
|
179
|
+
export declare function buildLlmPackage(name: string, localVersion: string, repoUrl: string | null, compatRange: string | null, localDshVersion: string, sinceIso: string, profileDir: string): Promise<LlmUpdatePackage>;
|
|
180
|
+
/** 组装发给 LLM 会话的 prompt(角色设定 + 信息包 + 规则引用)。 */
|
|
181
|
+
export declare function buildLlmPrompt(pkg: LlmUpdatePackage): string;
|