@dsh-workbench/shared 1.1.15 → 1.1.17
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/api/dsh.d.ts +31 -1
- package/dist/contract/protocol/bundle-install.d.ts +50 -0
- package/dist/host/adapter-types.d.ts +4 -1
- package/dist/host/bundle-manager.js +153 -0
- package/dist/host/diy-preset.d.ts +26 -0
- package/dist/host/diy-preset.js +115 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/package.json +2 -2
- package/src/api/dsh.ts +35 -1
- package/src/contract/protocol/bundle-install.ts +60 -0
- package/src/host/__tests__/diy-preset.test.ts +152 -0
- package/src/host/adapter-types.ts +6 -0
- package/src/host/bundle-manager.ts +155 -0
- package/src/host/diy-preset.ts +129 -0
- package/src/host.ts +1 -0
package/dist/api/dsh.d.ts
CHANGED
|
@@ -199,10 +199,40 @@ export interface DshAgentPresetRow {
|
|
|
199
199
|
readonly broken?: string;
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
|
-
*
|
|
202
|
+
* 一条 agent preset 的定义:`@deepseek-ai/dsh-agent-preset` 收的那份配置(`plugins` 是整表插件行)。
|
|
203
|
+
* DIY 的那一份在启动时从这一轮 dsh 的 `standard` 现场派生,不再有生成物。
|
|
204
|
+
*/
|
|
205
|
+
export interface DshAgentPresetDefinition {
|
|
206
|
+
readonly id: string;
|
|
207
|
+
readonly name?: string;
|
|
208
|
+
readonly description?: string;
|
|
209
|
+
readonly order?: number;
|
|
210
|
+
readonly plugins: readonly unknown[];
|
|
211
|
+
}
|
|
212
|
+
/** 注册成功后拿到的注销函数;插件卸载时调它,别把 preset 留在名册里。 */
|
|
213
|
+
export type DshAgentPresetUnregister = () => Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* 宿主侧的 agent preset 名册(服务名 `agentPresets`):`list()` 给启动体检读,`register()` 给 DIY 注册现场派生出来的那份。
|
|
203
216
|
*/
|
|
204
217
|
export interface DshAgentPresetRegistry {
|
|
205
218
|
list(): Promise<readonly DshAgentPresetRow[]>;
|
|
219
|
+
register?(definition: DshAgentPresetDefinition): Promise<DshAgentPresetUnregister>;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* dsh loader 里的一行:只读插件名与它收到的原始 config。
|
|
223
|
+
* DIY 从 `standard` 那一行的 config 现场派生;loader 的 `entries()` 递归给出这一轮组合里的所有行。
|
|
224
|
+
*/
|
|
225
|
+
export interface DshLoaderEntry {
|
|
226
|
+
readonly id?: string;
|
|
227
|
+
readonly disabled?: boolean;
|
|
228
|
+
readonly options?: {
|
|
229
|
+
readonly name?: string;
|
|
230
|
+
readonly config?: unknown;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** loader 服务(服务名 `loader`):插件行都在它的 `entries()` 里。 */
|
|
234
|
+
export interface DshLoader {
|
|
235
|
+
entries(): Iterable<DshLoaderEntry>;
|
|
206
236
|
}
|
|
207
237
|
/**
|
|
208
238
|
* 把相对路径拼成 dsh home 下的绝对路径(服务名 dshHomePath);外壳存自己的状态用它,不自己拼 home。
|
|
@@ -66,6 +66,12 @@ export interface BundlePackageResult {
|
|
|
66
66
|
readonly logPath: string;
|
|
67
67
|
readonly kind?: BundleFailureKind;
|
|
68
68
|
}
|
|
69
|
+
export interface BundleIncompatible {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly version: string;
|
|
72
|
+
readonly runtimeVersion: string;
|
|
73
|
+
readonly requirements: Readonly<Record<string, string>>;
|
|
74
|
+
}
|
|
69
75
|
export interface BundleChange {
|
|
70
76
|
readonly application: BundleApplication;
|
|
71
77
|
readonly stage: BundleChangeStage;
|
|
@@ -75,6 +81,7 @@ export interface BundleChange {
|
|
|
75
81
|
readonly version?: string;
|
|
76
82
|
readonly errorCode?: string;
|
|
77
83
|
readonly errorDetail?: string;
|
|
84
|
+
readonly incompatible?: readonly BundleIncompatible[];
|
|
78
85
|
readonly packageResult?: BundlePackageResult;
|
|
79
86
|
readonly pendingBuilds?: readonly string[];
|
|
80
87
|
readonly restartReason?: RestartReason;
|
|
@@ -87,6 +94,48 @@ export interface BundleInstallRequest {
|
|
|
87
94
|
readonly approvedBuilds?: readonly string[];
|
|
88
95
|
}
|
|
89
96
|
export type BundleCancellation = 'cancelled' | 'too-late' | 'not-running';
|
|
97
|
+
export type BundleVersionProblem = 'not-updatable' | 'not-found' | 'network' | 'unknown';
|
|
98
|
+
export interface BundleRegistryAttempt {
|
|
99
|
+
readonly registry: BundleRegistry;
|
|
100
|
+
readonly problem?: BundleVersionProblem;
|
|
101
|
+
readonly reason?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface BundleVersionEntry {
|
|
104
|
+
readonly version: string;
|
|
105
|
+
readonly publishedAt?: string;
|
|
106
|
+
readonly tags: readonly string[];
|
|
107
|
+
readonly prerelease: boolean;
|
|
108
|
+
readonly bundle: boolean;
|
|
109
|
+
readonly deprecated?: string;
|
|
110
|
+
readonly incompatible?: BundleIncompatible;
|
|
111
|
+
}
|
|
112
|
+
export interface BundleVersionListListed {
|
|
113
|
+
readonly status: 'listed';
|
|
114
|
+
readonly installed: string;
|
|
115
|
+
readonly registry: BundleRegistry;
|
|
116
|
+
readonly attempts: readonly BundleRegistryAttempt[];
|
|
117
|
+
readonly versions: readonly BundleVersionEntry[];
|
|
118
|
+
readonly omitted: number;
|
|
119
|
+
}
|
|
120
|
+
export interface BundleVersionListRefused {
|
|
121
|
+
readonly status: 'refused';
|
|
122
|
+
readonly problem: BundleVersionProblem;
|
|
123
|
+
readonly reason: string;
|
|
124
|
+
readonly attempts: readonly BundleRegistryAttempt[];
|
|
125
|
+
}
|
|
126
|
+
export type BundleVersionList = BundleVersionListListed | BundleVersionListRefused;
|
|
127
|
+
export type BundleUpdateStep = 'check' | 'remove' | 'install';
|
|
128
|
+
export interface BundleUpdateOutcome {
|
|
129
|
+
readonly from: string;
|
|
130
|
+
readonly to: string;
|
|
131
|
+
readonly step: BundleUpdateStep;
|
|
132
|
+
readonly change: BundleChange;
|
|
133
|
+
readonly restore?: BundleChange;
|
|
134
|
+
}
|
|
135
|
+
export interface BundleUpdateRequest {
|
|
136
|
+
readonly registry?: BundleRegistry;
|
|
137
|
+
readonly requestId?: string;
|
|
138
|
+
}
|
|
90
139
|
export type BundleSupplyState = 'not-a-module' | 'no-needs' | 'installed' | 'failed' | 'pending-restart';
|
|
91
140
|
export interface BundleModulePayload {
|
|
92
141
|
readonly kind: 'module' | 'not-module' | 'unknown';
|
|
@@ -131,6 +180,7 @@ export interface ModuleVersion {
|
|
|
131
180
|
readonly local?: boolean;
|
|
132
181
|
readonly refused?: readonly string[];
|
|
133
182
|
readonly market?: MarketInstalledModule;
|
|
183
|
+
readonly versions?: BundleVersionList;
|
|
134
184
|
readonly spec?: string;
|
|
135
185
|
readonly registry?: string;
|
|
136
186
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BundleCancellation, BundleChange, BundleInspection, BundleInstallRequest, BundleRegistries, BundleRegistry } from '../contract/protocol/bundle-install.js';
|
|
1
|
+
import type { BundleCancellation, BundleChange, BundleInspection, BundleInstallRequest, BundleRegistries, BundleRegistry, BundleUpdateOutcome, BundleUpdateRequest, BundleVersionList } from '../contract/protocol/bundle-install.js';
|
|
2
2
|
export type InstalledModule = {
|
|
3
3
|
readonly kind: 'module';
|
|
4
4
|
readonly pkg: string;
|
|
@@ -34,6 +34,9 @@ export interface BundleManager {
|
|
|
34
34
|
remove(name: string): Promise<BundleChange | null>;
|
|
35
35
|
setEnabled?(name: string, enabled: boolean): Promise<BundleChange | null>;
|
|
36
36
|
cancel(requestId: string): Promise<BundleCancellation | null>;
|
|
37
|
+
listVersions?(name: string, registry?: BundleRegistry): Promise<BundleVersionList | null>;
|
|
38
|
+
updateBundle?(name: string, version: string, request?: BundleUpdateRequest): Promise<BundleUpdateOutcome | null>;
|
|
39
|
+
waitForInstall?(requestId: string): Promise<BundleChange | null>;
|
|
37
40
|
}
|
|
38
41
|
export interface RegistryEntry {
|
|
39
42
|
readonly scope: string;
|
|
@@ -86,6 +86,7 @@ function asChange(raw) {
|
|
|
86
86
|
const version = asText(record.version);
|
|
87
87
|
const errorCode = error === undefined ? '' : asText(error.code);
|
|
88
88
|
const errorDetail = error === undefined ? '' : asText(error.diagnostic);
|
|
89
|
+
const incompatible = asIncompatibleList(error?.incompatible);
|
|
89
90
|
return {
|
|
90
91
|
application,
|
|
91
92
|
stage,
|
|
@@ -95,6 +96,7 @@ function asChange(raw) {
|
|
|
95
96
|
...(version === '' ? {} : { version }),
|
|
96
97
|
...(errorCode === '' ? {} : { errorCode }),
|
|
97
98
|
...(errorDetail === '' ? {} : { errorDetail }),
|
|
99
|
+
...(incompatible.length === 0 ? {} : { incompatible }),
|
|
98
100
|
...(packageResult === undefined ? {} : { packageResult }),
|
|
99
101
|
...(pendingBuilds.length === 0 ? {} : { pendingBuilds }),
|
|
100
102
|
...(restartReason === undefined ? {} : { restartReason }),
|
|
@@ -139,6 +141,138 @@ function asInspection(raw) {
|
|
|
139
141
|
...(host === '' ? {} : { host }),
|
|
140
142
|
};
|
|
141
143
|
}
|
|
144
|
+
const VERSION_PROBLEMS = ['not-updatable', 'not-found', 'network', 'unknown'];
|
|
145
|
+
const UPDATE_STEPS = ['check', 'remove', 'install'];
|
|
146
|
+
function asRequirementMap(value) {
|
|
147
|
+
const record = asRecord(value);
|
|
148
|
+
if (record === undefined)
|
|
149
|
+
return {};
|
|
150
|
+
const entries = [];
|
|
151
|
+
for (const [key, raw] of Object.entries(record)) {
|
|
152
|
+
if (typeof raw === 'string')
|
|
153
|
+
entries.push([key, raw]);
|
|
154
|
+
}
|
|
155
|
+
return Object.fromEntries(entries);
|
|
156
|
+
}
|
|
157
|
+
function asIncompatible(raw) {
|
|
158
|
+
const record = asRecord(raw);
|
|
159
|
+
if (record === undefined)
|
|
160
|
+
return undefined;
|
|
161
|
+
const name = asText(record.name);
|
|
162
|
+
const version = asText(record.version);
|
|
163
|
+
const runtimeVersion = asText(record.runtimeVersion);
|
|
164
|
+
if (name === '' || version === '' || runtimeVersion === '')
|
|
165
|
+
return undefined;
|
|
166
|
+
return { name, version, runtimeVersion, requirements: asRequirementMap(record.requirements) };
|
|
167
|
+
}
|
|
168
|
+
function asIncompatibleList(raw) {
|
|
169
|
+
if (!Array.isArray(raw))
|
|
170
|
+
return [];
|
|
171
|
+
const rows = [];
|
|
172
|
+
for (const item of raw) {
|
|
173
|
+
const entry = asIncompatible(item);
|
|
174
|
+
if (entry !== undefined)
|
|
175
|
+
rows.push(entry);
|
|
176
|
+
}
|
|
177
|
+
return rows;
|
|
178
|
+
}
|
|
179
|
+
function asRegistryAttempt(raw) {
|
|
180
|
+
const record = asRecord(raw);
|
|
181
|
+
if (record === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
const registry = asRegistry(record.registry);
|
|
184
|
+
if (registry === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const problem = pick(VERSION_PROBLEMS, record.problem);
|
|
187
|
+
const reason = asText(record.reason);
|
|
188
|
+
return {
|
|
189
|
+
registry,
|
|
190
|
+
...(problem === undefined ? {} : { problem }),
|
|
191
|
+
...(reason === '' ? {} : { reason }),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function asRegistryAttempts(raw) {
|
|
195
|
+
if (!Array.isArray(raw))
|
|
196
|
+
return [];
|
|
197
|
+
const rows = [];
|
|
198
|
+
for (const item of raw) {
|
|
199
|
+
const attempt = asRegistryAttempt(item);
|
|
200
|
+
if (attempt !== undefined)
|
|
201
|
+
rows.push(attempt);
|
|
202
|
+
}
|
|
203
|
+
return rows;
|
|
204
|
+
}
|
|
205
|
+
function asVersionEntry(raw) {
|
|
206
|
+
const record = asRecord(raw);
|
|
207
|
+
if (record === undefined)
|
|
208
|
+
return null;
|
|
209
|
+
const version = asText(record.version);
|
|
210
|
+
if (version === '')
|
|
211
|
+
return null;
|
|
212
|
+
const publishedAt = asText(record.publishedAt);
|
|
213
|
+
const deprecated = asText(record.deprecated);
|
|
214
|
+
const incompatible = asIncompatible(record.incompatible);
|
|
215
|
+
return {
|
|
216
|
+
version,
|
|
217
|
+
tags: asTextArray(record.tags),
|
|
218
|
+
prerelease: record.prerelease === true,
|
|
219
|
+
bundle: record.bundle === true,
|
|
220
|
+
...(publishedAt === '' ? {} : { publishedAt }),
|
|
221
|
+
...(deprecated === '' ? {} : { deprecated }),
|
|
222
|
+
...(incompatible === undefined ? {} : { incompatible }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function asVersionList(raw) {
|
|
226
|
+
const record = asRecord(raw);
|
|
227
|
+
if (record === undefined)
|
|
228
|
+
return null;
|
|
229
|
+
const status = asText(record.status);
|
|
230
|
+
const attempts = asRegistryAttempts(record.attempts);
|
|
231
|
+
if (status === 'refused') {
|
|
232
|
+
return {
|
|
233
|
+
status: 'refused',
|
|
234
|
+
problem: pick(VERSION_PROBLEMS, record.problem) ?? 'unknown',
|
|
235
|
+
reason: asText(record.reason),
|
|
236
|
+
attempts,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (status !== 'listed')
|
|
240
|
+
return null;
|
|
241
|
+
const registry = asRegistry(record.registry);
|
|
242
|
+
const versions = [];
|
|
243
|
+
if (Array.isArray(record.versions)) {
|
|
244
|
+
for (const item of record.versions) {
|
|
245
|
+
const entry = asVersionEntry(item);
|
|
246
|
+
if (entry !== null)
|
|
247
|
+
versions.push(entry);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
status: 'listed',
|
|
252
|
+
installed: asText(record.installed),
|
|
253
|
+
registry: registry ?? null,
|
|
254
|
+
attempts,
|
|
255
|
+
versions,
|
|
256
|
+
omitted: typeof record.omitted === 'number' ? record.omitted : 0,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function asUpdateOutcome(raw) {
|
|
260
|
+
const record = asRecord(raw);
|
|
261
|
+
if (record === undefined)
|
|
262
|
+
return null;
|
|
263
|
+
const step = pick(UPDATE_STEPS, record.step);
|
|
264
|
+
const change = asChange(record.result);
|
|
265
|
+
if (step === undefined || change === null)
|
|
266
|
+
return null;
|
|
267
|
+
const restore = record.restore === undefined ? null : asChange(record.restore);
|
|
268
|
+
return {
|
|
269
|
+
from: asText(record.from),
|
|
270
|
+
to: asText(record.to),
|
|
271
|
+
step,
|
|
272
|
+
change,
|
|
273
|
+
...(restore === null ? {} : { restore }),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
142
276
|
export function readBundleManager(ctx) {
|
|
143
277
|
const face = ctx.get(MANAGER_SERVICE);
|
|
144
278
|
if (face === undefined)
|
|
@@ -177,5 +311,24 @@ export function readBundleManager(ctx) {
|
|
|
177
311
|
return null;
|
|
178
312
|
return pick(CANCELLATIONS, (asRecord(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
|
|
179
313
|
},
|
|
314
|
+
async listVersions(name, registry) {
|
|
315
|
+
if (typeof face.listVersions !== 'function')
|
|
316
|
+
return null;
|
|
317
|
+
return asVersionList(await face.listVersions(name, registry === undefined ? undefined : { registry }));
|
|
318
|
+
},
|
|
319
|
+
async updateBundle(name, version, request = {}) {
|
|
320
|
+
if (typeof face.updateBundle !== 'function')
|
|
321
|
+
return null;
|
|
322
|
+
const options = {
|
|
323
|
+
...(request.registry === undefined ? {} : { registry: request.registry }),
|
|
324
|
+
...(request.requestId === undefined ? {} : { requestId: request.requestId }),
|
|
325
|
+
};
|
|
326
|
+
return asUpdateOutcome(await face.updateBundle(name, version, options));
|
|
327
|
+
},
|
|
328
|
+
async waitForInstall(requestId) {
|
|
329
|
+
if (typeof face.waitForInstall !== 'function')
|
|
330
|
+
return null;
|
|
331
|
+
return asChange(await face.waitForInstall(requestId));
|
|
332
|
+
},
|
|
180
333
|
};
|
|
181
334
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DshAgentPresetDefinition, DshLoaderEntry } from '../api/dsh.js';
|
|
2
|
+
/** DIY preset 在模式菜单里的名字。 */
|
|
3
|
+
export declare const DIY_PRESET_NAME = "\u5DE5\u4F5C\u53F0 DIY \u6A21\u5F0F";
|
|
4
|
+
/** DIY preset 在模式菜单里的说明。 */
|
|
5
|
+
export declare const DIY_PRESET_DESCRIPTION = "\u770B / \u8C03\u8FD9\u4E2A\u5DE5\u4F5C\u533A\u7684\u9762\u677F\uFF0C\u6216\u8005\u7ED9\u5B83\u9020\u4E00\u5757\u65B0\u5185\u5BB9 \u2014\u2014 \u7528\u5927\u767D\u8BDD\u8BF4\u5C31\u884C\u3002\u4EFB\u4F55\u5DE5\u4F5C\u533A\u90FD\u80FD\u7528\uFF08\u7A7A\u9879\u76EE\u4E5F\u7B97\uFF09\uFF1Bagent \u4F1A\u770B\u9762\u677F\u3001\u6293\u8BF7\u6C42\uFF0C\u5FC5\u8981\u65F6\u628A\u6A21\u5757\u9020\u51FA\u6765\u88C5\u4E0A\u3002\u4E0D\u7528\u5199\u4EE3\u7801\u3002";
|
|
6
|
+
/** DIY preset 在模式菜单里的排序(排在 dsh 出厂的几个模式之后)。 */
|
|
7
|
+
export declare const DIY_PRESET_ORDER = 9;
|
|
8
|
+
/** 这一轮 dsh 的 standard preset 没法派生时抛的错:带上哪一段锚点变了。 */
|
|
9
|
+
export declare class DiyPresetSourceError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 从这一轮 dsh 的 `standard` preset 配置派生 `workbench-diy`。
|
|
14
|
+
*
|
|
15
|
+
* 差分只有三处:`skill-filesystem` 与 `tool-skill` 一起搬到末尾(前者加 `customSkillDirs` 指向工作台的
|
|
16
|
+
* `skills/`)、末尾补 `tool-cordis`、`tool-plugin-manager` 打开(工作台只活在 profile 里)。
|
|
17
|
+
* 其余行原样跟着 dsh 走,所以 dsh 加工具 / 改 config 时不需要重新生成任何文件。
|
|
18
|
+
* @param standard dsh loader 里 `preset-standard` 那一行收到的 config。
|
|
19
|
+
* @param skillsDir 工作台的 `skills/` 绝对路径。
|
|
20
|
+
*/
|
|
21
|
+
export declare function diyPresetFrom(standard: unknown, skillsDir: string): DshAgentPresetDefinition;
|
|
22
|
+
/**
|
|
23
|
+
* 在 loader 的行里找这一轮 dsh 的 `standard` preset 配置;没有(或那一行被关掉)就算不知道。
|
|
24
|
+
* @param entries loader 的 `entries()`(递归给全部行)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function standardPresetConfigOf(entries: Iterable<DshLoaderEntry>): unknown;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { DIY_PRESET_ID } from '../contract/protocol/diy.js';
|
|
2
|
+
const AGENT_PRESET_PLUGIN = '@deepseek-ai/dsh-agent-preset';
|
|
3
|
+
const STANDARD_PRESET_ID = 'standard';
|
|
4
|
+
/** DIY preset 在模式菜单里的名字。 */
|
|
5
|
+
export const DIY_PRESET_NAME = '工作台 DIY 模式';
|
|
6
|
+
/** DIY preset 在模式菜单里的说明。 */
|
|
7
|
+
export const DIY_PRESET_DESCRIPTION = '看 / 调这个工作区的面板,或者给它造一块新内容 —— 用大白话说就行。任何工作区都能用(空项目也算);agent 会看面板、抓请求,必要时把模块造出来装上。不用写代码。';
|
|
8
|
+
/** DIY preset 在模式菜单里的排序(排在 dsh 出厂的几个模式之后)。 */
|
|
9
|
+
export const DIY_PRESET_ORDER = 9;
|
|
10
|
+
/** 这一轮 dsh 的 standard preset 没法派生时抛的错:带上哪一段锚点变了。 */
|
|
11
|
+
export class DiyPresetSourceError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'DiyPresetSourceError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isRow(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function rowAt(value, at) {
|
|
21
|
+
if (!isRow(value))
|
|
22
|
+
throw new DiyPresetSourceError(`${at} 不是一行插件配置`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function standardConfigOf(value) {
|
|
26
|
+
const standard = rowAt(value, 'dsh 的 standard preset');
|
|
27
|
+
if (!Array.isArray(standard.plugins)) {
|
|
28
|
+
throw new DiyPresetSourceError('dsh 的 standard preset 没有 plugins 表 —— 派生不出 DIY 那一份');
|
|
29
|
+
}
|
|
30
|
+
return standard;
|
|
31
|
+
}
|
|
32
|
+
function clonedPluginsOf(standard) {
|
|
33
|
+
let cloned;
|
|
34
|
+
try {
|
|
35
|
+
cloned = structuredClone(standard);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 配置复制不了:${String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
const rows = rowAt(cloned, 'dsh 的 standard preset').plugins;
|
|
41
|
+
if (!Array.isArray(rows)) {
|
|
42
|
+
throw new DiyPresetSourceError('dsh 的 standard preset 没有 plugins 表 —— 派生不出 DIY 那一份');
|
|
43
|
+
}
|
|
44
|
+
return rows.map((row, index) => {
|
|
45
|
+
const parsed = rowAt(row, `dsh 的 standard preset 第 ${index + 1} 行`);
|
|
46
|
+
if (typeof parsed.name !== 'string' || parsed.name === '') {
|
|
47
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 第 ${index + 1} 行没有插件名`);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function takeRow(plugins, id) {
|
|
53
|
+
const index = plugins.findIndex((row) => row.id === id);
|
|
54
|
+
if (index < 0) {
|
|
55
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 里找不到 \`${id}\` 那一行 —— DIY 的差分锚点变了`);
|
|
56
|
+
}
|
|
57
|
+
return plugins.splice(index, 1)[0];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 从这一轮 dsh 的 `standard` preset 配置派生 `workbench-diy`。
|
|
61
|
+
*
|
|
62
|
+
* 差分只有三处:`skill-filesystem` 与 `tool-skill` 一起搬到末尾(前者加 `customSkillDirs` 指向工作台的
|
|
63
|
+
* `skills/`)、末尾补 `tool-cordis`、`tool-plugin-manager` 打开(工作台只活在 profile 里)。
|
|
64
|
+
* 其余行原样跟着 dsh 走,所以 dsh 加工具 / 改 config 时不需要重新生成任何文件。
|
|
65
|
+
* @param standard dsh loader 里 `preset-standard` 那一行收到的 config。
|
|
66
|
+
* @param skillsDir 工作台的 `skills/` 绝对路径。
|
|
67
|
+
*/
|
|
68
|
+
export function diyPresetFrom(standard, skillsDir) {
|
|
69
|
+
const plugins = clonedPluginsOf(standardConfigOf(standard));
|
|
70
|
+
const skillFilesystem = takeRow(plugins, 'skill-filesystem');
|
|
71
|
+
const toolSkill = takeRow(plugins, 'tool-skill');
|
|
72
|
+
skillFilesystem.config = {
|
|
73
|
+
...(isRow(skillFilesystem.config) ? skillFilesystem.config : {}),
|
|
74
|
+
customSkillDirs: [skillsDir],
|
|
75
|
+
};
|
|
76
|
+
const manager = plugins.find((row) => row.id === 'tool-plugin-manager');
|
|
77
|
+
if (manager !== undefined)
|
|
78
|
+
manager.disabled = false;
|
|
79
|
+
if (!plugins.some((row) => row.id === 'tool-cordis')) {
|
|
80
|
+
plugins.push({ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' });
|
|
81
|
+
}
|
|
82
|
+
plugins.push(skillFilesystem, toolSkill);
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const row of plugins) {
|
|
85
|
+
if (typeof row.id !== 'string' || row.id === '')
|
|
86
|
+
continue;
|
|
87
|
+
if (seen.has(row.id)) {
|
|
88
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 里出现重复的 id:${row.id}`);
|
|
89
|
+
}
|
|
90
|
+
seen.add(row.id);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
id: DIY_PRESET_ID,
|
|
94
|
+
name: DIY_PRESET_NAME,
|
|
95
|
+
description: DIY_PRESET_DESCRIPTION,
|
|
96
|
+
order: DIY_PRESET_ORDER,
|
|
97
|
+
plugins,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 在 loader 的行里找这一轮 dsh 的 `standard` preset 配置;没有(或那一行被关掉)就算不知道。
|
|
102
|
+
* @param entries loader 的 `entries()`(递归给全部行)。
|
|
103
|
+
*/
|
|
104
|
+
export function standardPresetConfigOf(entries) {
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (entry.disabled === true)
|
|
107
|
+
continue;
|
|
108
|
+
if (entry.options?.name !== AGENT_PRESET_PLUGIN)
|
|
109
|
+
continue;
|
|
110
|
+
const config = entry.options.config;
|
|
111
|
+
if (isRow(config) && config.id === STANDARD_PRESET_ID)
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { configSyncAction, configSyncApply, configSyncHandler, configSyncSnapsho
|
|
|
36
36
|
export * from './host/profile-create.js';
|
|
37
37
|
export * from './host/context-ref.js';
|
|
38
38
|
export * from './host/diy-debug.js';
|
|
39
|
+
export * from './host/diy-preset.js';
|
|
39
40
|
export * from './host/diy-record.js';
|
|
40
41
|
export * from './host/env-policy.js';
|
|
41
42
|
export * from './host/facts.js';
|
package/dist/host.js
CHANGED
|
@@ -36,6 +36,7 @@ export { configSyncAction, configSyncApply, configSyncHandler, configSyncSnapsho
|
|
|
36
36
|
export * from './host/profile-create.js';
|
|
37
37
|
export * from './host/context-ref.js';
|
|
38
38
|
export * from './host/diy-debug.js';
|
|
39
|
+
export * from './host/diy-preset.js';
|
|
39
40
|
export * from './host/diy-record.js';
|
|
40
41
|
export * from './host/env-policy.js';
|
|
41
42
|
export * from './host/facts.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/shared",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@arch-guard/core": "^0.11.3",
|
|
37
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
|
|
38
38
|
"@types/node": "^24.19.0",
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
40
40
|
"@types/semver": "^7.8.0",
|
package/src/api/dsh.ts
CHANGED
|
@@ -195,10 +195,44 @@ export interface DshAgentPresetRow {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
*
|
|
198
|
+
* 一条 agent preset 的定义:`@deepseek-ai/dsh-agent-preset` 收的那份配置(`plugins` 是整表插件行)。
|
|
199
|
+
* DIY 的那一份在启动时从这一轮 dsh 的 `standard` 现场派生,不再有生成物。
|
|
200
|
+
*/
|
|
201
|
+
export interface DshAgentPresetDefinition {
|
|
202
|
+
readonly id: string;
|
|
203
|
+
readonly name?: string;
|
|
204
|
+
readonly description?: string;
|
|
205
|
+
readonly order?: number;
|
|
206
|
+
readonly plugins: readonly unknown[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 注册成功后拿到的注销函数;插件卸载时调它,别把 preset 留在名册里。 */
|
|
210
|
+
export type DshAgentPresetUnregister = () => Promise<void>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 宿主侧的 agent preset 名册(服务名 `agentPresets`):`list()` 给启动体检读,`register()` 给 DIY 注册现场派生出来的那份。
|
|
199
214
|
*/
|
|
200
215
|
export interface DshAgentPresetRegistry {
|
|
201
216
|
list(): Promise<readonly DshAgentPresetRow[]>;
|
|
217
|
+
register?(definition: DshAgentPresetDefinition): Promise<DshAgentPresetUnregister>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* dsh loader 里的一行:只读插件名与它收到的原始 config。
|
|
222
|
+
* DIY 从 `standard` 那一行的 config 现场派生;loader 的 `entries()` 递归给出这一轮组合里的所有行。
|
|
223
|
+
*/
|
|
224
|
+
export interface DshLoaderEntry {
|
|
225
|
+
readonly id?: string;
|
|
226
|
+
readonly disabled?: boolean;
|
|
227
|
+
readonly options?: {
|
|
228
|
+
readonly name?: string;
|
|
229
|
+
readonly config?: unknown;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** loader 服务(服务名 `loader`):插件行都在它的 `entries()` 里。 */
|
|
234
|
+
export interface DshLoader {
|
|
235
|
+
entries(): Iterable<DshLoaderEntry>;
|
|
202
236
|
}
|
|
203
237
|
|
|
204
238
|
/**
|
|
@@ -117,6 +117,13 @@ export interface BundlePackageResult {
|
|
|
117
117
|
readonly kind?: BundleFailureKind;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export interface BundleIncompatible {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
readonly version: string;
|
|
123
|
+
readonly runtimeVersion: string;
|
|
124
|
+
readonly requirements: Readonly<Record<string, string>>;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
export interface BundleChange {
|
|
121
128
|
readonly application: BundleApplication;
|
|
122
129
|
readonly stage: BundleChangeStage;
|
|
@@ -126,6 +133,7 @@ export interface BundleChange {
|
|
|
126
133
|
readonly version?: string;
|
|
127
134
|
readonly errorCode?: string;
|
|
128
135
|
readonly errorDetail?: string;
|
|
136
|
+
readonly incompatible?: readonly BundleIncompatible[];
|
|
129
137
|
readonly packageResult?: BundlePackageResult;
|
|
130
138
|
readonly pendingBuilds?: readonly string[];
|
|
131
139
|
readonly restartReason?: RestartReason;
|
|
@@ -141,6 +149,57 @@ export interface BundleInstallRequest {
|
|
|
141
149
|
|
|
142
150
|
export type BundleCancellation = 'cancelled' | 'too-late' | 'not-running';
|
|
143
151
|
|
|
152
|
+
export type BundleVersionProblem = 'not-updatable' | 'not-found' | 'network' | 'unknown';
|
|
153
|
+
|
|
154
|
+
export interface BundleRegistryAttempt {
|
|
155
|
+
readonly registry: BundleRegistry;
|
|
156
|
+
readonly problem?: BundleVersionProblem;
|
|
157
|
+
readonly reason?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface BundleVersionEntry {
|
|
161
|
+
readonly version: string;
|
|
162
|
+
readonly publishedAt?: string;
|
|
163
|
+
readonly tags: readonly string[];
|
|
164
|
+
readonly prerelease: boolean;
|
|
165
|
+
readonly bundle: boolean;
|
|
166
|
+
readonly deprecated?: string;
|
|
167
|
+
readonly incompatible?: BundleIncompatible;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface BundleVersionListListed {
|
|
171
|
+
readonly status: 'listed';
|
|
172
|
+
readonly installed: string;
|
|
173
|
+
readonly registry: BundleRegistry;
|
|
174
|
+
readonly attempts: readonly BundleRegistryAttempt[];
|
|
175
|
+
readonly versions: readonly BundleVersionEntry[];
|
|
176
|
+
readonly omitted: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface BundleVersionListRefused {
|
|
180
|
+
readonly status: 'refused';
|
|
181
|
+
readonly problem: BundleVersionProblem;
|
|
182
|
+
readonly reason: string;
|
|
183
|
+
readonly attempts: readonly BundleRegistryAttempt[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type BundleVersionList = BundleVersionListListed | BundleVersionListRefused;
|
|
187
|
+
|
|
188
|
+
export type BundleUpdateStep = 'check' | 'remove' | 'install';
|
|
189
|
+
|
|
190
|
+
export interface BundleUpdateOutcome {
|
|
191
|
+
readonly from: string;
|
|
192
|
+
readonly to: string;
|
|
193
|
+
readonly step: BundleUpdateStep;
|
|
194
|
+
readonly change: BundleChange;
|
|
195
|
+
readonly restore?: BundleChange;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface BundleUpdateRequest {
|
|
199
|
+
readonly registry?: BundleRegistry;
|
|
200
|
+
readonly requestId?: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
144
203
|
export type BundleSupplyState = 'not-a-module' | 'no-needs' | 'installed' | 'failed' | 'pending-restart';
|
|
145
204
|
|
|
146
205
|
export interface BundleModulePayload {
|
|
@@ -198,6 +257,7 @@ export interface ModuleVersion {
|
|
|
198
257
|
readonly local?: boolean;
|
|
199
258
|
readonly refused?: readonly string[];
|
|
200
259
|
readonly market?: MarketInstalledModule;
|
|
260
|
+
readonly versions?: BundleVersionList;
|
|
201
261
|
readonly spec?: string;
|
|
202
262
|
readonly registry?: string;
|
|
203
263
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DIY_PRESET_ID } from '../../contract/protocol/diy.js';
|
|
3
|
+
import { DiyPresetSourceError, diyPresetFrom, standardPresetConfigOf } from '../diy-preset.js';
|
|
4
|
+
|
|
5
|
+
const SKILLS = '/opt/workbench/skills';
|
|
6
|
+
|
|
7
|
+
function standard(overrides: { plugins?: unknown[] } = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'standard',
|
|
10
|
+
order: 1,
|
|
11
|
+
plugins: overrides.plugins ?? [
|
|
12
|
+
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: 'x' } },
|
|
13
|
+
{ id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs', config: { applyPatch: false } },
|
|
14
|
+
{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' },
|
|
15
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
16
|
+
{
|
|
17
|
+
id: 'tool-plugin-manager',
|
|
18
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools',
|
|
19
|
+
disabled: { __jsExpr: "!ctx.get('profileContext')" },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const idsOf = (value: { plugins: readonly unknown[] }) =>
|
|
26
|
+
value.plugins.map((row) => (row as { id?: string }).id);
|
|
27
|
+
|
|
28
|
+
describe('diyPresetFrom(从 dsh 的 standard 现场派生)', () => {
|
|
29
|
+
it('★ 三处差分都在:skill 两行搬到末尾、tool-cordis 补上、plugin-manager 打开', () => {
|
|
30
|
+
const derived = diyPresetFrom(standard(), SKILLS);
|
|
31
|
+
expect(idsOf(derived)).toEqual([
|
|
32
|
+
'persona',
|
|
33
|
+
'tool-fs',
|
|
34
|
+
'tool-plugin-manager',
|
|
35
|
+
'tool-cordis',
|
|
36
|
+
'skill-filesystem',
|
|
37
|
+
'tool-skill',
|
|
38
|
+
]);
|
|
39
|
+
const manager = derived.plugins.find((row) => (row as { id?: string }).id === 'tool-plugin-manager') as {
|
|
40
|
+
disabled?: unknown;
|
|
41
|
+
};
|
|
42
|
+
expect(manager.disabled).toBe(false);
|
|
43
|
+
const skills = derived.plugins.find((row) => (row as { id?: string }).id === 'skill-filesystem') as {
|
|
44
|
+
config?: { customSkillDirs?: string[] };
|
|
45
|
+
};
|
|
46
|
+
expect(skills.config?.customSkillDirs).toEqual([SKILLS]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('★ 兄弟行原样跟着走:dsh 加工具 / 改 config 不需要重新生成任何文件', () => {
|
|
50
|
+
const source = standard();
|
|
51
|
+
const derived = diyPresetFrom(source, SKILLS);
|
|
52
|
+
const fs = derived.plugins.find((row) => (row as { id?: string }).id === 'tool-fs') as {
|
|
53
|
+
config?: { applyPatch?: boolean };
|
|
54
|
+
};
|
|
55
|
+
expect(fs.config?.applyPatch).toBe(false);
|
|
56
|
+
expect(derived.id).toBe(DIY_PRESET_ID);
|
|
57
|
+
expect(derived.name).toBe('工作台 DIY 模式');
|
|
58
|
+
expect(derived.order).toBe(9);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('★ 输入的一份都不改(克隆后再动;否则会把 dsh 自己的 standard 也搬走两行)', () => {
|
|
62
|
+
const source = standard();
|
|
63
|
+
const manager = source.plugins.find((row) => (row as { id?: string }).id === 'tool-plugin-manager') as {
|
|
64
|
+
disabled?: unknown;
|
|
65
|
+
};
|
|
66
|
+
const skills = source.plugins.find((row) => (row as { id?: string }).id === 'skill-filesystem') as {
|
|
67
|
+
config?: unknown;
|
|
68
|
+
};
|
|
69
|
+
diyPresetFrom(source, SKILLS);
|
|
70
|
+
expect(idsOf(source)).toEqual([
|
|
71
|
+
'persona',
|
|
72
|
+
'tool-fs',
|
|
73
|
+
'skill-filesystem',
|
|
74
|
+
'tool-skill',
|
|
75
|
+
'tool-plugin-manager',
|
|
76
|
+
]);
|
|
77
|
+
expect(manager.disabled).toEqual({ __jsExpr: "!ctx.get('profileContext')" });
|
|
78
|
+
expect(skills.config).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('dsh 已经把 tool-cordis 列进 standard:不再补第二行', () => {
|
|
82
|
+
const derived = diyPresetFrom(
|
|
83
|
+
standard({
|
|
84
|
+
plugins: [
|
|
85
|
+
{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' },
|
|
86
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
87
|
+
{ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' },
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
SKILLS,
|
|
91
|
+
);
|
|
92
|
+
expect(idsOf(derived).filter((id) => id === 'tool-cordis')).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('★ 锚点变了就当场报错并点名(静默少一行 = DIY 会话少一把工具,且不报错)', () => {
|
|
96
|
+
expect(() =>
|
|
97
|
+
diyPresetFrom(
|
|
98
|
+
standard({ plugins: [{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' }] }),
|
|
99
|
+
SKILLS,
|
|
100
|
+
),
|
|
101
|
+
).toThrow(/skill-filesystem/);
|
|
102
|
+
expect(() =>
|
|
103
|
+
diyPresetFrom(
|
|
104
|
+
standard({ plugins: [{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' }] }),
|
|
105
|
+
SKILLS,
|
|
106
|
+
),
|
|
107
|
+
).toThrow(/tool-skill/);
|
|
108
|
+
expect(() => diyPresetFrom({ id: 'standard' }, SKILLS)).toThrow(DiyPresetSourceError);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('★ standard 里出现重复 id 就不派生(别把一份带重复行的 preset 交给 dsh)', () => {
|
|
112
|
+
expect(() =>
|
|
113
|
+
diyPresetFrom(
|
|
114
|
+
standard({
|
|
115
|
+
plugins: [
|
|
116
|
+
{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' },
|
|
117
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
118
|
+
{ id: 'persona', name: '@deepseek-ai/dsh-persona' },
|
|
119
|
+
{ id: 'persona', name: '@deepseek-ai/dsh-persona' },
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
SKILLS,
|
|
123
|
+
),
|
|
124
|
+
).toThrow(/persona/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('standardPresetConfigOf(从 loader 的行里认 standard)', () => {
|
|
129
|
+
it('★ 认 agent-preset 插件 + config.id=standard,不是靠行 id 的 include 前缀', () => {
|
|
130
|
+
const found = standardPresetConfigOf([
|
|
131
|
+
{ id: 'include:persona', options: { name: '@deepseek-ai/dsh-persona' } },
|
|
132
|
+
{
|
|
133
|
+
id: 'include:preset-standard',
|
|
134
|
+
options: { name: '@deepseek-ai/dsh-agent-preset', config: standard() },
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
expect(found).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('被关掉的那一行不算 / 没有 standard 就不猜', () => {
|
|
141
|
+
expect(
|
|
142
|
+
standardPresetConfigOf([
|
|
143
|
+
{
|
|
144
|
+
id: 'include:preset-standard',
|
|
145
|
+
disabled: true,
|
|
146
|
+
options: { name: '@deepseek-ai/dsh-agent-preset', config: standard() },
|
|
147
|
+
},
|
|
148
|
+
]),
|
|
149
|
+
).toBeUndefined();
|
|
150
|
+
expect(standardPresetConfigOf([])).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -5,6 +5,9 @@ import type {
|
|
|
5
5
|
BundleInstallRequest,
|
|
6
6
|
BundleRegistries,
|
|
7
7
|
BundleRegistry,
|
|
8
|
+
BundleUpdateOutcome,
|
|
9
|
+
BundleUpdateRequest,
|
|
10
|
+
BundleVersionList,
|
|
8
11
|
} from '../contract/protocol/bundle-install.js';
|
|
9
12
|
|
|
10
13
|
export type InstalledModule =
|
|
@@ -42,6 +45,9 @@ export interface BundleManager {
|
|
|
42
45
|
remove(name: string): Promise<BundleChange | null>;
|
|
43
46
|
setEnabled?(name: string, enabled: boolean): Promise<BundleChange | null>;
|
|
44
47
|
cancel(requestId: string): Promise<BundleCancellation | null>;
|
|
48
|
+
listVersions?(name: string, registry?: BundleRegistry): Promise<BundleVersionList | null>;
|
|
49
|
+
updateBundle?(name: string, version: string, request?: BundleUpdateRequest): Promise<BundleUpdateOutcome | null>;
|
|
50
|
+
waitForInstall?(requestId: string): Promise<BundleChange | null>;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export interface RegistryEntry {
|
|
@@ -3,9 +3,15 @@ import { RESTART_REASONS } from '../contract/protocol/bundle-install.js';
|
|
|
3
3
|
import type { BundleManager } from './adapter-types.js';
|
|
4
4
|
import type {
|
|
5
5
|
BundleChange,
|
|
6
|
+
BundleIncompatible,
|
|
6
7
|
BundleInspection,
|
|
7
8
|
BundleRegistries,
|
|
8
9
|
BundleRegistry,
|
|
10
|
+
BundleRegistryAttempt,
|
|
11
|
+
BundleUpdateOutcome,
|
|
12
|
+
BundleUpdateRequest,
|
|
13
|
+
BundleVersionEntry,
|
|
14
|
+
BundleVersionList,
|
|
9
15
|
} from '../contract/protocol/bundle-install.js';
|
|
10
16
|
|
|
11
17
|
const MANAGER_SERVICE = 'pluginManager';
|
|
@@ -17,6 +23,13 @@ interface PluginManagerFace {
|
|
|
17
23
|
removeBundle?(name: string): Promise<unknown>;
|
|
18
24
|
setBundleEnabled?(name: string, enabled: boolean): Promise<unknown>;
|
|
19
25
|
cancelInstall?(requestId: string): Promise<unknown>;
|
|
26
|
+
listVersions?(name: string, options?: { readonly registry?: BundleRegistry }): Promise<unknown>;
|
|
27
|
+
updateBundle?(
|
|
28
|
+
name: string,
|
|
29
|
+
version: string,
|
|
30
|
+
options?: { readonly registry?: BundleRegistry; readonly requestId?: string },
|
|
31
|
+
): Promise<unknown>;
|
|
32
|
+
waitForInstall?(requestId: string): Promise<unknown>;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
const APPLICATIONS = ['applied', 'restart-required', 'overridden', 'failed', 'cancelled'] as const;
|
|
@@ -107,6 +120,7 @@ function asChange(raw: unknown): BundleChange | null {
|
|
|
107
120
|
const version = asText(record.version);
|
|
108
121
|
const errorCode = error === undefined ? '' : asText(error.code);
|
|
109
122
|
const errorDetail = error === undefined ? '' : asText(error.diagnostic);
|
|
123
|
+
const incompatible = asIncompatibleList(error?.incompatible);
|
|
110
124
|
return {
|
|
111
125
|
application,
|
|
112
126
|
stage,
|
|
@@ -116,6 +130,7 @@ function asChange(raw: unknown): BundleChange | null {
|
|
|
116
130
|
...(version === '' ? {} : { version }),
|
|
117
131
|
...(errorCode === '' ? {} : { errorCode }),
|
|
118
132
|
...(errorDetail === '' ? {} : { errorDetail }),
|
|
133
|
+
...(incompatible.length === 0 ? {} : { incompatible }),
|
|
119
134
|
...(packageResult === undefined ? {} : { packageResult }),
|
|
120
135
|
...(pendingBuilds.length === 0 ? {} : { pendingBuilds }),
|
|
121
136
|
...(restartReason === undefined ? {} : { restartReason }),
|
|
@@ -159,6 +174,130 @@ function asInspection(raw: unknown): BundleInspection | null {
|
|
|
159
174
|
};
|
|
160
175
|
}
|
|
161
176
|
|
|
177
|
+
const VERSION_PROBLEMS = ['not-updatable', 'not-found', 'network', 'unknown'] as const;
|
|
178
|
+
const UPDATE_STEPS = ['check', 'remove', 'install'] as const;
|
|
179
|
+
|
|
180
|
+
function asRequirementMap(value: unknown): Readonly<Record<string, string>> {
|
|
181
|
+
const record = asRecord(value);
|
|
182
|
+
if (record === undefined) return {};
|
|
183
|
+
const entries: [string, string][] = [];
|
|
184
|
+
for (const [key, raw] of Object.entries(record)) {
|
|
185
|
+
if (typeof raw === 'string') entries.push([key, raw]);
|
|
186
|
+
}
|
|
187
|
+
return Object.fromEntries(entries);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function asIncompatible(raw: unknown): BundleIncompatible | undefined {
|
|
191
|
+
const record = asRecord(raw);
|
|
192
|
+
if (record === undefined) return undefined;
|
|
193
|
+
const name = asText(record.name);
|
|
194
|
+
const version = asText(record.version);
|
|
195
|
+
const runtimeVersion = asText(record.runtimeVersion);
|
|
196
|
+
if (name === '' || version === '' || runtimeVersion === '') return undefined;
|
|
197
|
+
return { name, version, runtimeVersion, requirements: asRequirementMap(record.requirements) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asIncompatibleList(raw: unknown): BundleIncompatible[] {
|
|
201
|
+
if (!Array.isArray(raw)) return [];
|
|
202
|
+
const rows: BundleIncompatible[] = [];
|
|
203
|
+
for (const item of raw) {
|
|
204
|
+
const entry = asIncompatible(item);
|
|
205
|
+
if (entry !== undefined) rows.push(entry);
|
|
206
|
+
}
|
|
207
|
+
return rows;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function asRegistryAttempt(raw: unknown): BundleRegistryAttempt | undefined {
|
|
211
|
+
const record = asRecord(raw);
|
|
212
|
+
if (record === undefined) return undefined;
|
|
213
|
+
const registry = asRegistry(record.registry);
|
|
214
|
+
if (registry === undefined) return undefined;
|
|
215
|
+
const problem = pick(VERSION_PROBLEMS, record.problem);
|
|
216
|
+
const reason = asText(record.reason);
|
|
217
|
+
return {
|
|
218
|
+
registry,
|
|
219
|
+
...(problem === undefined ? {} : { problem }),
|
|
220
|
+
...(reason === '' ? {} : { reason }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function asRegistryAttempts(raw: unknown): BundleRegistryAttempt[] {
|
|
225
|
+
if (!Array.isArray(raw)) return [];
|
|
226
|
+
const rows: BundleRegistryAttempt[] = [];
|
|
227
|
+
for (const item of raw) {
|
|
228
|
+
const attempt = asRegistryAttempt(item);
|
|
229
|
+
if (attempt !== undefined) rows.push(attempt);
|
|
230
|
+
}
|
|
231
|
+
return rows;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function asVersionEntry(raw: unknown): BundleVersionEntry | null {
|
|
235
|
+
const record = asRecord(raw);
|
|
236
|
+
if (record === undefined) return null;
|
|
237
|
+
const version = asText(record.version);
|
|
238
|
+
if (version === '') return null;
|
|
239
|
+
const publishedAt = asText(record.publishedAt);
|
|
240
|
+
const deprecated = asText(record.deprecated);
|
|
241
|
+
const incompatible = asIncompatible(record.incompatible);
|
|
242
|
+
return {
|
|
243
|
+
version,
|
|
244
|
+
tags: asTextArray(record.tags),
|
|
245
|
+
prerelease: record.prerelease === true,
|
|
246
|
+
bundle: record.bundle === true,
|
|
247
|
+
...(publishedAt === '' ? {} : { publishedAt }),
|
|
248
|
+
...(deprecated === '' ? {} : { deprecated }),
|
|
249
|
+
...(incompatible === undefined ? {} : { incompatible }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function asVersionList(raw: unknown): BundleVersionList | null {
|
|
254
|
+
const record = asRecord(raw);
|
|
255
|
+
if (record === undefined) return null;
|
|
256
|
+
const status = asText(record.status);
|
|
257
|
+
const attempts = asRegistryAttempts(record.attempts);
|
|
258
|
+
if (status === 'refused') {
|
|
259
|
+
return {
|
|
260
|
+
status: 'refused',
|
|
261
|
+
problem: pick(VERSION_PROBLEMS, record.problem) ?? 'unknown',
|
|
262
|
+
reason: asText(record.reason),
|
|
263
|
+
attempts,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (status !== 'listed') return null;
|
|
267
|
+
const registry = asRegistry(record.registry);
|
|
268
|
+
const versions: BundleVersionEntry[] = [];
|
|
269
|
+
if (Array.isArray(record.versions)) {
|
|
270
|
+
for (const item of record.versions) {
|
|
271
|
+
const entry = asVersionEntry(item);
|
|
272
|
+
if (entry !== null) versions.push(entry);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
status: 'listed',
|
|
277
|
+
installed: asText(record.installed),
|
|
278
|
+
registry: registry ?? null,
|
|
279
|
+
attempts,
|
|
280
|
+
versions,
|
|
281
|
+
omitted: typeof record.omitted === 'number' ? record.omitted : 0,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function asUpdateOutcome(raw: unknown): BundleUpdateOutcome | null {
|
|
286
|
+
const record = asRecord(raw);
|
|
287
|
+
if (record === undefined) return null;
|
|
288
|
+
const step = pick(UPDATE_STEPS, record.step);
|
|
289
|
+
const change = asChange(record.result);
|
|
290
|
+
if (step === undefined || change === null) return null;
|
|
291
|
+
const restore = record.restore === undefined ? null : asChange(record.restore);
|
|
292
|
+
return {
|
|
293
|
+
from: asText(record.from),
|
|
294
|
+
to: asText(record.to),
|
|
295
|
+
step,
|
|
296
|
+
change,
|
|
297
|
+
...(restore === null ? {} : { restore }),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
162
301
|
export function readBundleManager(ctx: DshHostContext): BundleManager | undefined {
|
|
163
302
|
const face = ctx.get<PluginManagerFace>(MANAGER_SERVICE);
|
|
164
303
|
if (face === undefined) return undefined;
|
|
@@ -191,5 +330,21 @@ export function readBundleManager(ctx: DshHostContext): BundleManager | undefine
|
|
|
191
330
|
if (typeof face.cancelInstall !== 'function') return null;
|
|
192
331
|
return pick(CANCELLATIONS, (asRecord(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
|
|
193
332
|
},
|
|
333
|
+
async listVersions(name, registry) {
|
|
334
|
+
if (typeof face.listVersions !== 'function') return null;
|
|
335
|
+
return asVersionList(await face.listVersions(name, registry === undefined ? undefined : { registry }));
|
|
336
|
+
},
|
|
337
|
+
async updateBundle(name, version, request: BundleUpdateRequest = {}) {
|
|
338
|
+
if (typeof face.updateBundle !== 'function') return null;
|
|
339
|
+
const options = {
|
|
340
|
+
...(request.registry === undefined ? {} : { registry: request.registry }),
|
|
341
|
+
...(request.requestId === undefined ? {} : { requestId: request.requestId }),
|
|
342
|
+
};
|
|
343
|
+
return asUpdateOutcome(await face.updateBundle(name, version, options));
|
|
344
|
+
},
|
|
345
|
+
async waitForInstall(requestId) {
|
|
346
|
+
if (typeof face.waitForInstall !== 'function') return null;
|
|
347
|
+
return asChange(await face.waitForInstall(requestId));
|
|
348
|
+
},
|
|
194
349
|
};
|
|
195
350
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { DshAgentPresetDefinition, DshLoaderEntry } from '../api/dsh.js';
|
|
2
|
+
import { DIY_PRESET_ID } from '../contract/protocol/diy.js';
|
|
3
|
+
|
|
4
|
+
const AGENT_PRESET_PLUGIN = '@deepseek-ai/dsh-agent-preset';
|
|
5
|
+
const STANDARD_PRESET_ID = 'standard';
|
|
6
|
+
|
|
7
|
+
/** DIY preset 在模式菜单里的名字。 */
|
|
8
|
+
export const DIY_PRESET_NAME = '工作台 DIY 模式';
|
|
9
|
+
|
|
10
|
+
/** DIY preset 在模式菜单里的说明。 */
|
|
11
|
+
export const DIY_PRESET_DESCRIPTION =
|
|
12
|
+
'看 / 调这个工作区的面板,或者给它造一块新内容 —— 用大白话说就行。任何工作区都能用(空项目也算);agent 会看面板、抓请求,必要时把模块造出来装上。不用写代码。';
|
|
13
|
+
|
|
14
|
+
/** DIY preset 在模式菜单里的排序(排在 dsh 出厂的几个模式之后)。 */
|
|
15
|
+
export const DIY_PRESET_ORDER = 9;
|
|
16
|
+
|
|
17
|
+
/** 这一轮 dsh 的 standard preset 没法派生时抛的错:带上哪一段锚点变了。 */
|
|
18
|
+
export class DiyPresetSourceError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'DiyPresetSourceError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Row = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
function isRow(value: unknown): value is Row {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rowAt(value: unknown, at: string): Row {
|
|
32
|
+
if (!isRow(value)) throw new DiyPresetSourceError(`${at} 不是一行插件配置`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function standardConfigOf(value: unknown): Row {
|
|
37
|
+
const standard = rowAt(value, 'dsh 的 standard preset');
|
|
38
|
+
if (!Array.isArray(standard.plugins)) {
|
|
39
|
+
throw new DiyPresetSourceError('dsh 的 standard preset 没有 plugins 表 —— 派生不出 DIY 那一份');
|
|
40
|
+
}
|
|
41
|
+
return standard;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clonedPluginsOf(standard: Row): Row[] {
|
|
45
|
+
let cloned: unknown;
|
|
46
|
+
try {
|
|
47
|
+
cloned = structuredClone(standard);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 配置复制不了:${String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
const rows = rowAt(cloned, 'dsh 的 standard preset').plugins;
|
|
52
|
+
if (!Array.isArray(rows)) {
|
|
53
|
+
throw new DiyPresetSourceError('dsh 的 standard preset 没有 plugins 表 —— 派生不出 DIY 那一份');
|
|
54
|
+
}
|
|
55
|
+
return rows.map((row, index) => {
|
|
56
|
+
const parsed = rowAt(row, `dsh 的 standard preset 第 ${index + 1} 行`);
|
|
57
|
+
if (typeof parsed.name !== 'string' || parsed.name === '') {
|
|
58
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 第 ${index + 1} 行没有插件名`);
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function takeRow(plugins: Row[], id: string): Row {
|
|
65
|
+
const index = plugins.findIndex((row) => row.id === id);
|
|
66
|
+
if (index < 0) {
|
|
67
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 里找不到 \`${id}\` 那一行 —— DIY 的差分锚点变了`);
|
|
68
|
+
}
|
|
69
|
+
return plugins.splice(index, 1)[0] as Row;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 从这一轮 dsh 的 `standard` preset 配置派生 `workbench-diy`。
|
|
74
|
+
*
|
|
75
|
+
* 差分只有三处:`skill-filesystem` 与 `tool-skill` 一起搬到末尾(前者加 `customSkillDirs` 指向工作台的
|
|
76
|
+
* `skills/`)、末尾补 `tool-cordis`、`tool-plugin-manager` 打开(工作台只活在 profile 里)。
|
|
77
|
+
* 其余行原样跟着 dsh 走,所以 dsh 加工具 / 改 config 时不需要重新生成任何文件。
|
|
78
|
+
* @param standard dsh loader 里 `preset-standard` 那一行收到的 config。
|
|
79
|
+
* @param skillsDir 工作台的 `skills/` 绝对路径。
|
|
80
|
+
*/
|
|
81
|
+
export function diyPresetFrom(standard: unknown, skillsDir: string): DshAgentPresetDefinition {
|
|
82
|
+
const plugins = clonedPluginsOf(standardConfigOf(standard));
|
|
83
|
+
|
|
84
|
+
const skillFilesystem = takeRow(plugins, 'skill-filesystem');
|
|
85
|
+
const toolSkill = takeRow(plugins, 'tool-skill');
|
|
86
|
+
skillFilesystem.config = {
|
|
87
|
+
...(isRow(skillFilesystem.config) ? skillFilesystem.config : {}),
|
|
88
|
+
customSkillDirs: [skillsDir],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const manager = plugins.find((row) => row.id === 'tool-plugin-manager');
|
|
92
|
+
if (manager !== undefined) manager.disabled = false;
|
|
93
|
+
|
|
94
|
+
if (!plugins.some((row) => row.id === 'tool-cordis')) {
|
|
95
|
+
plugins.push({ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' });
|
|
96
|
+
}
|
|
97
|
+
plugins.push(skillFilesystem, toolSkill);
|
|
98
|
+
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
for (const row of plugins) {
|
|
101
|
+
if (typeof row.id !== 'string' || row.id === '') continue;
|
|
102
|
+
if (seen.has(row.id)) {
|
|
103
|
+
throw new DiyPresetSourceError(`dsh 的 standard preset 里出现重复的 id:${row.id}`);
|
|
104
|
+
}
|
|
105
|
+
seen.add(row.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: DIY_PRESET_ID,
|
|
110
|
+
name: DIY_PRESET_NAME,
|
|
111
|
+
description: DIY_PRESET_DESCRIPTION,
|
|
112
|
+
order: DIY_PRESET_ORDER,
|
|
113
|
+
plugins,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 在 loader 的行里找这一轮 dsh 的 `standard` preset 配置;没有(或那一行被关掉)就算不知道。
|
|
119
|
+
* @param entries loader 的 `entries()`(递归给全部行)。
|
|
120
|
+
*/
|
|
121
|
+
export function standardPresetConfigOf(entries: Iterable<DshLoaderEntry>): unknown {
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (entry.disabled === true) continue;
|
|
124
|
+
if (entry.options?.name !== AGENT_PRESET_PLUGIN) continue;
|
|
125
|
+
const config = entry.options.config;
|
|
126
|
+
if (isRow(config) && config.id === STANDARD_PRESET_ID) return config;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
package/src/host.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
export * from './host/profile-create.js';
|
|
47
47
|
export * from './host/context-ref.js';
|
|
48
48
|
export * from './host/diy-debug.js';
|
|
49
|
+
export * from './host/diy-preset.js';
|
|
49
50
|
export * from './host/diy-record.js';
|
|
50
51
|
export * from './host/env-policy.js';
|
|
51
52
|
export * from './host/facts.js';
|