@dsh-workbench/core 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/dsh-workbench +67 -1
- package/client.js +122 -59
- package/dist/client/client.body.js +122 -59
- package/dist/client/index.js +122 -59
- package/dist/host/index.js +142 -6
- package/dist/types/app/layouts/env-gate/boot-gate.d.ts +8 -0
- package/dist/types/app/layouts/rightbar/market-rows.d.ts +1 -1
- package/dist/types/app/layouts/rightbar/market-shared-rows.d.ts +0 -1
- package/dist/types/shared/api/host/diy-preset-health.d.ts +35 -0
- package/dist/types/shared/api/host/installed-module.d.ts +9 -0
- package/dist/types/shared/api/host/preflight-route.d.ts +15 -0
- package/dist/types/shared/api/host/preflight.d.ts +12 -1
- package/docs/HANDOFF.md +23 -18
- package/docs/OPERATIONS.md +3 -0
- package/package.json +6 -2
- package/scripts/seed-profile.sh +34 -0
package/dist/host/index.js
CHANGED
|
@@ -2364,6 +2364,9 @@ function createWorkspaceStore(options) {
|
|
|
2364
2364
|
}
|
|
2365
2365
|
};
|
|
2366
2366
|
}
|
|
2367
|
+
//#endregion
|
|
2368
|
+
//#region src/shared/api/diy.ts
|
|
2369
|
+
var DIY_PRESET_ID = "workbench-diy";
|
|
2367
2370
|
var WORKSHOP_RELATIVE = "workbench/diy";
|
|
2368
2371
|
var WORKSHOP_TITLE = "工作台DIY";
|
|
2369
2372
|
//#endregion
|
|
@@ -2712,8 +2715,23 @@ function readInstalledModule(profileDir, pkg) {
|
|
|
2712
2715
|
...registry === "" ? {} : { registry }
|
|
2713
2716
|
};
|
|
2714
2717
|
}
|
|
2718
|
+
var PACKED_ARTIFACT = /\.(?:tgz|tar\.gz|tar)$/i;
|
|
2719
|
+
/**
|
|
2720
|
+
* 这份依赖是不是"从一个**打好的包**装的"(`file:/tmp/dsh-workbench-core-0.3.8.tgz`、
|
|
2721
|
+
* `./x-0.3.8.tgz`)。它跟 `link:` / 目录装的**不是**一回事:那是工作树(改了立刻生效,
|
|
2722
|
+
* 也不可能跟源上比出个新旧),而 tgz 是打包产物 —— 包名与版本就是源上那一套,能比、
|
|
2723
|
+
* 也该让人更新。混在一起会把"从文件装的发布产物"说成"本仓正在开发的那一份",
|
|
2724
|
+
* 顺带把版本比对与更新一起关掉。
|
|
2725
|
+
*/
|
|
2726
|
+
function isPackedArtifactSpec(spec) {
|
|
2727
|
+
const trimmed = spec.trim();
|
|
2728
|
+
const pkgPath = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
|
|
2729
|
+
return PACKED_ARTIFACT.test(pkgPath);
|
|
2730
|
+
}
|
|
2731
|
+
/** 这份依赖是不是"本仓那份"(`link:` / 目录 / 相对或绝对路径)—— 只有工作树才算。 */
|
|
2715
2732
|
function isLocalDependencySpec(spec) {
|
|
2716
2733
|
const trimmed = spec.trim();
|
|
2734
|
+
if (isPackedArtifactSpec(trimmed)) return false;
|
|
2717
2735
|
return trimmed.startsWith("link:") || trimmed.startsWith("file:") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/");
|
|
2718
2736
|
}
|
|
2719
2737
|
function readProfilePackage(profileDir, pkg) {
|
|
@@ -9894,7 +9912,7 @@ function stateOf(entry) {
|
|
|
9894
9912
|
}
|
|
9895
9913
|
function stateOfDependency(dep) {
|
|
9896
9914
|
switch (dep.status) {
|
|
9897
|
-
case "ok": return "
|
|
9915
|
+
case "ok": return "已就绪";
|
|
9898
9916
|
case "outdated": return "有新版";
|
|
9899
9917
|
case "unsatisfied": return "版本不对";
|
|
9900
9918
|
case "missing": return "未装";
|
|
@@ -11891,6 +11909,75 @@ function createContextInjector(options) {
|
|
|
11891
11909
|
} };
|
|
11892
11910
|
}
|
|
11893
11911
|
//#endregion
|
|
11912
|
+
//#region src/shared/api/host/diy-preset-health.ts
|
|
11913
|
+
/**
|
|
11914
|
+
* DIY 模式挂不起来时给用户的那条修法:升级 dsh,再重启一次工作台(启动器那道拒绝用的是同一条)。
|
|
11915
|
+
*/
|
|
11916
|
+
var DIY_UPGRADE_COMMAND = "npm i -g @deepseek-ai/dsh@latest";
|
|
11917
|
+
/**
|
|
11918
|
+
* 把 dsh 名册里那句 `broken` 原文拆成一项一个行名。
|
|
11919
|
+
* @param detail - registry 给的原文,一行一项,形如 `tool-schedule (@deepseek-ai/dsh-tool-schedule): never started`。
|
|
11920
|
+
* @returns 每行 `(` 之前那一段;空原文给空数组。
|
|
11921
|
+
*/
|
|
11922
|
+
function brokenRowsOf(detail) {
|
|
11923
|
+
const rows = [];
|
|
11924
|
+
for (const line of String(detail ?? "").split("\n")) {
|
|
11925
|
+
const text = line.trim();
|
|
11926
|
+
if (text === "") continue;
|
|
11927
|
+
const at = text.indexOf(" (");
|
|
11928
|
+
rows.push(at < 0 ? text : text.slice(0, at));
|
|
11929
|
+
}
|
|
11930
|
+
return rows;
|
|
11931
|
+
}
|
|
11932
|
+
/**
|
|
11933
|
+
* 从 preset 名册判 DIY 模式挂不挂得起来。
|
|
11934
|
+
* @param presets - `agentPresets.list()` 的回值。
|
|
11935
|
+
* @returns 名册还空着(preset 行还没登记完)给 `undefined` —— 那是"还不知道",不是故障;否则给结论。
|
|
11936
|
+
*/
|
|
11937
|
+
function diyPresetVerdictOf(presets) {
|
|
11938
|
+
if (!Array.isArray(presets) || presets.length === 0) return void 0;
|
|
11939
|
+
const mine = presets.find((row) => row?.id === DIY_PRESET_ID);
|
|
11940
|
+
if (mine === void 0) return {
|
|
11941
|
+
ok: false,
|
|
11942
|
+
rows: [],
|
|
11943
|
+
detail: `preset 名单里没有 ${DIY_PRESET_ID}`
|
|
11944
|
+
};
|
|
11945
|
+
const broken = typeof mine.broken === "string" ? mine.broken.trim() : "";
|
|
11946
|
+
if (broken === "") return {
|
|
11947
|
+
ok: true,
|
|
11948
|
+
rows: [],
|
|
11949
|
+
detail: ""
|
|
11950
|
+
};
|
|
11951
|
+
return {
|
|
11952
|
+
ok: false,
|
|
11953
|
+
rows: brokenRowsOf(broken),
|
|
11954
|
+
detail: broken
|
|
11955
|
+
};
|
|
11956
|
+
}
|
|
11957
|
+
/**
|
|
11958
|
+
* 造一个读 preset 名册的探针:名册还没登记完就每 `everyMs` 重读,到 `deadlineMs` 还没读到就放弃。
|
|
11959
|
+
* @param ctx - 宿主上下文(取 `agentPresets` 服务;那个服务不在时给一个恒回 `undefined` 的探针)。
|
|
11960
|
+
* @param options - 两个时间旋钮(测试用):多久放弃、隔多久重读。
|
|
11961
|
+
* @returns 探针:调一次读一轮,`undefined` 表示"这次没读到"。
|
|
11962
|
+
*/
|
|
11963
|
+
function createDiyPresetProbe(ctx, options = {}) {
|
|
11964
|
+
const deadlineMs = options.deadlineMs ?? 15e3;
|
|
11965
|
+
const everyMs = options.everyMs ?? 400;
|
|
11966
|
+
return async () => {
|
|
11967
|
+
const registry = ctx.get("agentPresets");
|
|
11968
|
+
if (registry === void 0 || typeof registry.list !== "function") return void 0;
|
|
11969
|
+
const until = Date.now() + deadlineMs;
|
|
11970
|
+
for (;;) {
|
|
11971
|
+
const rows = await registry.list().catch(() => void 0);
|
|
11972
|
+
if (rows === void 0) return void 0;
|
|
11973
|
+
const verdict = diyPresetVerdictOf(rows);
|
|
11974
|
+
if (verdict !== void 0) return verdict;
|
|
11975
|
+
if (Date.now() >= until) return void 0;
|
|
11976
|
+
await new Promise((resolve) => setTimeout(resolve, everyMs));
|
|
11977
|
+
}
|
|
11978
|
+
};
|
|
11979
|
+
}
|
|
11980
|
+
//#endregion
|
|
11894
11981
|
//#region src/shared/api/host/preflight.ts
|
|
11895
11982
|
function classifyFailure(text) {
|
|
11896
11983
|
const lower = text.toLowerCase();
|
|
@@ -11953,6 +12040,7 @@ function createPreflight(options) {
|
|
|
11953
12040
|
const steps = () => [
|
|
11954
12041
|
step("opencli"),
|
|
11955
12042
|
step("bridge"),
|
|
12043
|
+
...options.probeDiy === void 0 ? [] : [step("diy")],
|
|
11956
12044
|
...warmReads().length === 0 ? [] : [step("warm")]
|
|
11957
12045
|
];
|
|
11958
12046
|
let current = {
|
|
@@ -12067,6 +12155,31 @@ function createPreflight(options) {
|
|
|
12067
12155
|
});
|
|
12068
12156
|
return again;
|
|
12069
12157
|
}
|
|
12158
|
+
async function settleDiy(started) {
|
|
12159
|
+
if (started === void 0) return void 0;
|
|
12160
|
+
const verdict = await started.catch(() => void 0);
|
|
12161
|
+
if (verdict === void 0) {
|
|
12162
|
+
set({ steps: current.steps.filter((item) => item.id !== "diy") });
|
|
12163
|
+
return;
|
|
12164
|
+
}
|
|
12165
|
+
if (verdict.ok) {
|
|
12166
|
+
patchStep("diy", {
|
|
12167
|
+
status: "ok",
|
|
12168
|
+
detail: verdict.detail
|
|
12169
|
+
});
|
|
12170
|
+
return;
|
|
12171
|
+
}
|
|
12172
|
+
patchStep("diy", {
|
|
12173
|
+
status: "warn",
|
|
12174
|
+
detail: verdict.detail,
|
|
12175
|
+
command: DIY_UPGRADE_COMMAND
|
|
12176
|
+
});
|
|
12177
|
+
return {
|
|
12178
|
+
rows: verdict.rows,
|
|
12179
|
+
detail: verdict.detail,
|
|
12180
|
+
command: DIY_UPGRADE_COMMAND
|
|
12181
|
+
};
|
|
12182
|
+
}
|
|
12070
12183
|
async function doRun() {
|
|
12071
12184
|
set({
|
|
12072
12185
|
phase: "checking",
|
|
@@ -12078,6 +12191,8 @@ function createPreflight(options) {
|
|
|
12078
12191
|
status: "running",
|
|
12079
12192
|
action: `${command} --version`
|
|
12080
12193
|
});
|
|
12194
|
+
const diyStarted = options.probeDiy?.();
|
|
12195
|
+
if (diyStarted !== void 0) patchStep("diy", { status: "running" });
|
|
12081
12196
|
let version = await probeVersion();
|
|
12082
12197
|
if (!version.ok && !version.missing) {
|
|
12083
12198
|
patchStep("opencli", {
|
|
@@ -12149,10 +12264,12 @@ function createPreflight(options) {
|
|
|
12149
12264
|
...bridge.command === void 0 ? {} : { command: bridge.command }
|
|
12150
12265
|
});
|
|
12151
12266
|
const settled = await warmSites(bridge);
|
|
12267
|
+
const diyGap = await settleDiy(diyStarted);
|
|
12152
12268
|
logger?.warn?.(`[dsh-workbench] 环境体检:${current.steps.map((item) => `${item.id}=${item.status}`).join(" ")}`);
|
|
12153
12269
|
return set({
|
|
12154
12270
|
phase: "ready",
|
|
12155
|
-
...settled.ok ? {} : { bridgeIssues: settled.issues }
|
|
12271
|
+
...settled.ok ? {} : { bridgeIssues: settled.issues },
|
|
12272
|
+
...diyGap === void 0 ? {} : { diyGap }
|
|
12156
12273
|
});
|
|
12157
12274
|
}
|
|
12158
12275
|
function freshReady() {
|
|
@@ -12175,6 +12292,24 @@ function createPreflight(options) {
|
|
|
12175
12292
|
};
|
|
12176
12293
|
}
|
|
12177
12294
|
//#endregion
|
|
12295
|
+
//#region src/shared/api/host/preflight-route.ts
|
|
12296
|
+
/**
|
|
12297
|
+
* 把启动体检接上 `preflight` 那条路由。
|
|
12298
|
+
* @param input - 路由表、宿主上下文、执行缝,以及 opencli 命令 / 装它用的命令(可选)与各套件声明的预热读命令。
|
|
12299
|
+
* @returns 无(路由挂在 `input.routes.preflight` 上)。
|
|
12300
|
+
*/
|
|
12301
|
+
function registerPreflight(input) {
|
|
12302
|
+
const preflight = createPreflight({
|
|
12303
|
+
shell: input.shell,
|
|
12304
|
+
command: input.opencliCommand,
|
|
12305
|
+
...input.installCommand === void 0 ? {} : { installCommand: input.installCommand },
|
|
12306
|
+
logger: input.ctx.logger,
|
|
12307
|
+
warmReads: input.warmReads,
|
|
12308
|
+
probeDiy: createDiyPresetProbe(input.ctx)
|
|
12309
|
+
});
|
|
12310
|
+
input.routes.preflight = preflightHandler(preflight);
|
|
12311
|
+
}
|
|
12312
|
+
//#endregion
|
|
12178
12313
|
//#region src/shared/api/host/probe-cache.ts
|
|
12179
12314
|
function createTimedMemo(ttlMs) {
|
|
12180
12315
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -13980,13 +14115,14 @@ function apply(ctx) {
|
|
|
13980
14115
|
}
|
|
13981
14116
|
if (shell === void 0) ctx.logger?.warn?.("[dsh-workbench] 没有 shell 服务:需要执行缝的套件会降级");
|
|
13982
14117
|
else {
|
|
13983
|
-
|
|
14118
|
+
registerPreflight({
|
|
14119
|
+
routes,
|
|
14120
|
+
ctx,
|
|
13984
14121
|
shell,
|
|
13985
|
-
|
|
14122
|
+
opencliCommand: opencli.command,
|
|
13986
14123
|
...installEnv === void 0 ? {} : { installCommand: installEnv },
|
|
13987
|
-
logger: ctx.logger,
|
|
13988
14124
|
warmReads: () => [...mounted.values()].flatMap((item) => item.plan.warmReads())
|
|
13989
|
-
})
|
|
14125
|
+
});
|
|
13990
14126
|
routes.capability = capabilityHandler(async (unit, force) => {
|
|
13991
14127
|
if (unit === void 0) return { units: (await probeUnits({
|
|
13992
14128
|
shell,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Translate } from '@dsh-workbench/shared';
|
|
2
|
+
import type { PreflightSnapshot } from '../../../shared/api/host/preflight';
|
|
2
3
|
import { apiGet } from '@dsh-workbench/shared';
|
|
3
4
|
/**
|
|
4
5
|
* 每一拍轮询带什么参数:平时什么都不带(宿主那边跟着正在跑的那一轮,刚跑出 `ready` 的直接回上一次),
|
|
@@ -14,6 +15,13 @@ export declare function createForcePlan(): {
|
|
|
14
15
|
export interface BootGate {
|
|
15
16
|
close(): void;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* 拿到这一拍之后门该怎么办:`close` 撤掉放行、`hold` 停在原地等用户动手、`wait` 接着轮询。
|
|
20
|
+
* 桥接坏了(`bridgeIssues` 非空)与 DIY 模式挂不起来(`diyGap` 非空)都是**不拦路**的,
|
|
21
|
+
* 但那块「怎么解决」得先真的画出来 —— 停在原地 + 一颗「先进入工作台」才叫不拦路,
|
|
22
|
+
* 就绪即撤等于把那块提示连同修法一起丢掉,用户进去只会发现「工作台 DIY」整个不在。
|
|
23
|
+
*/
|
|
24
|
+
export declare function gateActionOf(snapshot: PreflightSnapshot): 'close' | 'hold' | 'wait';
|
|
17
25
|
export declare function createBootGate(ctx: {
|
|
18
26
|
locale: {
|
|
19
27
|
bind(namespace: string): Translate;
|
|
@@ -5,7 +5,7 @@ import type { SharedRow } from './market-shared-rows';
|
|
|
5
5
|
import type { CapabilityMap } from '../../../shared/hooks/use-capabilities';
|
|
6
6
|
export type { MarketHintKey, MarketNote, MarketRow, MarketRowAction, MarketState, ModuleUpdate, } from './market-row-types';
|
|
7
7
|
export type { SharedRow, SharedState } from './market-shared-rows';
|
|
8
|
-
export {
|
|
8
|
+
export { SHARED_STATE_KEY, sharedActions, sharedRows } from './market-shared-rows';
|
|
9
9
|
export declare const MARKET_STATE_KEY: Readonly<Record<MarketState, string>>;
|
|
10
10
|
export declare const MARKET_DOT: Readonly<Record<MarketState, 'ongoing' | 'done' | 'warning' | 'idle' | 'error'>>;
|
|
11
11
|
export type ModuleUpdateMap = Readonly<Record<string, ModuleUpdate>>;
|
|
@@ -3,7 +3,6 @@ import type { CapabilityMap } from '../../../shared/hooks/use-capabilities';
|
|
|
3
3
|
import type { MarketRowAction } from './market-row-types';
|
|
4
4
|
export type SharedState = 'checking' | 'installed' | 'outdated' | 'unsatisfied' | 'missing' | 'unchecked';
|
|
5
5
|
export declare const SHARED_STATE_KEY: Readonly<Record<SharedState, string>>;
|
|
6
|
-
export declare const SHARED_DOT: Readonly<Record<SharedState, 'ongoing' | 'done' | 'warning' | 'idle'>>;
|
|
7
6
|
export interface SharedRow {
|
|
8
7
|
readonly id: string;
|
|
9
8
|
readonly kind: 'plugin' | 'lib';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DshAgentPresetRow, DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
/**
|
|
3
|
+
* DIY 模式挂不起来时给用户的那条修法:升级 dsh,再重启一次工作台(启动器那道拒绝用的是同一条)。
|
|
4
|
+
*/
|
|
5
|
+
export declare const DIY_UPGRADE_COMMAND = "npm i -g @deepseek-ai/dsh@latest";
|
|
6
|
+
/**
|
|
7
|
+
* 一次 DIY 模式体检的结论:`ok` 为真 = 名册里有那一行且挂着;为假时 `rows` 是挂不起来的行名、`detail` 是 dsh 给的原文。
|
|
8
|
+
*/
|
|
9
|
+
export interface DiyPresetVerdict {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
readonly rows: readonly string[];
|
|
12
|
+
readonly detail: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 把 dsh 名册里那句 `broken` 原文拆成一项一个行名。
|
|
16
|
+
* @param detail - registry 给的原文,一行一项,形如 `tool-schedule (@deepseek-ai/dsh-tool-schedule): never started`。
|
|
17
|
+
* @returns 每行 `(` 之前那一段;空原文给空数组。
|
|
18
|
+
*/
|
|
19
|
+
export declare function brokenRowsOf(detail: string): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* 从 preset 名册判 DIY 模式挂不挂得起来。
|
|
22
|
+
* @param presets - `agentPresets.list()` 的回值。
|
|
23
|
+
* @returns 名册还空着(preset 行还没登记完)给 `undefined` —— 那是"还不知道",不是故障;否则给结论。
|
|
24
|
+
*/
|
|
25
|
+
export declare function diyPresetVerdictOf(presets: readonly DshAgentPresetRow[]): DiyPresetVerdict | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* 造一个读 preset 名册的探针:名册还没登记完就每 `everyMs` 重读,到 `deadlineMs` 还没读到就放弃。
|
|
28
|
+
* @param ctx - 宿主上下文(取 `agentPresets` 服务;那个服务不在时给一个恒回 `undefined` 的探针)。
|
|
29
|
+
* @param options - 两个时间旋钮(测试用):多久放弃、隔多久重读。
|
|
30
|
+
* @returns 探针:调一次读一轮,`undefined` 表示"这次没读到"。
|
|
31
|
+
*/
|
|
32
|
+
export declare function createDiyPresetProbe(ctx: DshHostContext, options?: {
|
|
33
|
+
deadlineMs?: number;
|
|
34
|
+
everyMs?: number;
|
|
35
|
+
}): () => Promise<DiyPresetVerdict | undefined>;
|
|
@@ -34,6 +34,15 @@ export interface DeclarationGap {
|
|
|
34
34
|
export declare function declarationGaps(profileDir: string | null): readonly DeclarationGap[] | undefined;
|
|
35
35
|
export declare function declaredPluginCommands(profileDir: string | null): ReadonlyMap<string, ReadonlySet<string>>;
|
|
36
36
|
export declare function readInstalledModule(profileDir: string | null, pkg: string): InstalledModule;
|
|
37
|
+
/**
|
|
38
|
+
* 这份依赖是不是"从一个**打好的包**装的"(`file:/tmp/dsh-workbench-core-0.3.8.tgz`、
|
|
39
|
+
* `./x-0.3.8.tgz`)。它跟 `link:` / 目录装的**不是**一回事:那是工作树(改了立刻生效,
|
|
40
|
+
* 也不可能跟源上比出个新旧),而 tgz 是打包产物 —— 包名与版本就是源上那一套,能比、
|
|
41
|
+
* 也该让人更新。混在一起会把"从文件装的发布产物"说成"本仓正在开发的那一份",
|
|
42
|
+
* 顺带把版本比对与更新一起关掉。
|
|
43
|
+
*/
|
|
44
|
+
export declare function isPackedArtifactSpec(spec: string): boolean;
|
|
45
|
+
/** 这份依赖是不是"本仓那份"(`link:` / 目录 / 相对或绝对路径)—— 只有工作树才算。 */
|
|
37
46
|
export declare function isLocalDependencySpec(spec: string): boolean;
|
|
38
47
|
export interface ProfilePackage {
|
|
39
48
|
readonly pkg: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DshHostContext, DshShell } from '@dsh-workbench/shared';
|
|
2
|
+
import type { RouteTable } from '@dsh-workbench/shared/host';
|
|
3
|
+
/**
|
|
4
|
+
* 把启动体检接上 `preflight` 那条路由。
|
|
5
|
+
* @param input - 路由表、宿主上下文、执行缝,以及 opencli 命令 / 装它用的命令(可选)与各套件声明的预热读命令。
|
|
6
|
+
* @returns 无(路由挂在 `input.routes.preflight` 上)。
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerPreflight(input: {
|
|
9
|
+
routes: RouteTable;
|
|
10
|
+
ctx: DshHostContext;
|
|
11
|
+
shell: DshShell;
|
|
12
|
+
opencliCommand: string;
|
|
13
|
+
installCommand?: string | undefined;
|
|
14
|
+
warmReads: () => readonly (readonly string[])[];
|
|
15
|
+
}): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DshShell } from '@dsh-workbench/shared';
|
|
2
|
-
|
|
2
|
+
import type { DiyPresetVerdict } from './diy-preset-health';
|
|
3
|
+
export type PreflightStepId = 'opencli' | 'bridge' | 'diy' | 'warm';
|
|
3
4
|
export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
|
|
4
5
|
export interface PreflightStep {
|
|
5
6
|
readonly id: PreflightStepId;
|
|
@@ -17,6 +18,14 @@ export declare function readBridgeReport(stdout: string): {
|
|
|
17
18
|
readonly issues: readonly PreflightBridgeIssue[];
|
|
18
19
|
readonly detail: string;
|
|
19
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* DIY 模式挂不起来时随快照送出去的那三件:挂不起来的行、dsh 给的原文、修法(升级 dsh)。
|
|
23
|
+
*/
|
|
24
|
+
export interface PreflightDiyGap {
|
|
25
|
+
readonly rows: readonly string[];
|
|
26
|
+
readonly detail: string;
|
|
27
|
+
readonly command: string;
|
|
28
|
+
}
|
|
20
29
|
export interface PreflightSnapshot {
|
|
21
30
|
readonly phase: 'idle' | 'checking' | 'installing' | 'ready' | 'failed';
|
|
22
31
|
readonly steps: readonly PreflightStep[];
|
|
@@ -25,6 +34,7 @@ export interface PreflightSnapshot {
|
|
|
25
34
|
readonly failure?: PreflightFailure;
|
|
26
35
|
readonly guidance?: PreflightGuidance;
|
|
27
36
|
readonly bridgeIssues?: readonly PreflightBridgeIssue[];
|
|
37
|
+
readonly diyGap?: PreflightDiyGap;
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* 环境体检:`run()` 跑一次并回快照,`snapshot()` 看眼下这一拍。`run` 的取舍是:
|
|
@@ -61,4 +71,5 @@ export declare function createPreflight(options: {
|
|
|
61
71
|
detail: string;
|
|
62
72
|
}>) | undefined;
|
|
63
73
|
warmReads?: (() => readonly (readonly string[])[]) | undefined;
|
|
74
|
+
probeDiy?: (() => Promise<DiyPresetVerdict | undefined>) | undefined;
|
|
64
75
|
}): Preflight;
|
package/docs/HANDOFF.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
| # | 东西 | 为什么 | 怎么有 |
|
|
21
21
|
| --- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
|
|
22
|
-
| 1 | `dsh` | 本体。本仓库在 `0.1
|
|
22
|
+
| 1 | `dsh` | 本体。本仓库在 `0.2.1-rc.2` 上验过,**低于它的会被拒绝** —— 见第八节 | `npm i -g @deepseek-ai/dsh@latest`(公网 npm 上有) |
|
|
23
23
|
| 2 | `node` + **`pnpm`** | dsh 的 `plugin` 子命令(以及 web 的 Plugins 页)都是**转发给 pnpm** 的;没有 pnpm 会直接报 `dsh: pnpm was not found` | 各自装 |
|
|
24
24
|
| 3 | 跑过一次 `dsh web` | 走 A 需要它给出那个 profile;走 B 时 `setup` 拿 `~/.dsh/profiles/web` 当模板。没有它会**明确报错**,不会静默建出半个 profile | `dsh web` |
|
|
25
25
|
| 4 | 一个能解析这个包的源 | 包在**公网 npm** 上,默认源即可。只有将来改发内网源时,才要 `npm config set @<scope>:registry …` + `npm login`(带 scope 的包只去 `.npmrc` 里给这个 scope 指的源找) | `npm view @dsh-workbench/core version` 出号即可 |
|
|
@@ -239,9 +239,26 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
|
|
|
239
239
|
|
|
240
240
|
## 八、版本与兼容(实话)
|
|
241
241
|
|
|
242
|
-
`package.json`
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
外壳在 `package.json` 里声明了 **`engines.dsh: ">=0.2.1-rc.2"`**(本仓验过的那一代,2026-10-18)。
|
|
243
|
+
这条声明是后补的,来历见
|
|
244
|
+
[`.scratch/dsh-version-floor/spec.md`](../../../.scratch/dsh-version-floor/spec.md):真实用户装了
|
|
245
|
+
`dsh 0.2.0-rc.2`(当时 npm `latest` 还是它)+ 工作台,工作台照常起得来,**只有 DIY 模式整个不见、
|
|
246
|
+
一句错都不报** —— `preset/diy.patch.yml` 是从 dsh 0.2.1-rc.2 那份 `standard.patch.yml` 派生的,
|
|
247
|
+
里面一行 `tool-schedule` 在 0.2.0 上根本不存在,preset 挂不起来;而 dsh 的模式选择器**只列挂得起来的**
|
|
248
|
+
(healthy presets only),于是"工作台 DIY"从用户面前消失。同一代的 `uiWorkspace.startSession()`
|
|
249
|
+
也**不收** `agentPreset`(0.2.1-alpha.3 才收),所以这不是"少一把工具"的降级档,是 DIY 模式整块不可用。
|
|
250
|
+
|
|
251
|
+
因此**声明一个下限**(而不是"谁都能装"):dsh 自 `0.2.1-alpha.4` 起预检**先读 `engines.dsh`**
|
|
252
|
+
(声明了它,peer 就被整体忽略;写 `workspace:` 一律判不兼容;`engines` 不是对象或 `engines.dsh`
|
|
253
|
+
不是字符串会让清单不可读并**直接拒绝**),不满足就拒绝安装,
|
|
254
|
+
可用 `dsh plugin --profile workbench allow-version <包@版本> --dsh-version <版本> --accept-risk` 豁免。
|
|
255
|
+
旧到不认识 `engines.dsh` 的 dsh(0.2.0 及更早只读 `peerDependencies`)由**启动器那道硬门禁**兜
|
|
256
|
+
(`bin/dsh-workbench`,与"bundle 没被选中"同一档:拒绝启动 + 打印升级命令)。
|
|
257
|
+
|
|
258
|
+
⚠️ **别把下限改写成 `peerDependencies['@deepseek-ai/dsh']`**:pnpm 的 `auto-install-peers`
|
|
259
|
+
(v11 实测:连 `peerDependenciesMeta.optional` 的 peer 也照装)会把**整套 dsh**拖进 profile。
|
|
260
|
+
下限只有一处真相(`package.json` 的 `engines.dsh`),启动器读它、不另写版本号 —— selfcheck 有一条判据
|
|
261
|
+
钉着"下限 = `@deepseek-ai/dsh-client-ui-primitives` 那个 pin"。
|
|
245
262
|
|
|
246
263
|
代价要清楚:工作台依赖 dsh 的注入面(`@deepseek-ai/dsh-client-ui-slots`、
|
|
247
264
|
`@deepseek-ai/dsh-client-locale`,见 `dsh.client.inject`)与一批**按名字取的服务**
|
|
@@ -253,20 +270,8 @@ dsh 现在发的是预发布(npm 的 `next` 频道;本仓**验过的**是 `0
|
|
|
253
270
|
**devDependency 里的 `@deepseek-ai/dsh-client-ui-primitives` 跟着"验过的 dsh 版本"走**
|
|
254
271
|
([ADR-0028 §4.1](../../../docs/adr/0028-workbench-ui-uses-dsh-primitives.md)):它只出类型,
|
|
255
272
|
但类型比运行时旧一代,编译期就替不了你把关。当前两份都钉 `0.2.1-rc.2`
|
|
256
|
-
(`packages/workbench` / `packages/shared`;改完要跑一次 `pnpm install` 更新 `pnpm-lock.yaml
|
|
257
|
-
|
|
258
|
-
想改成"装的时候就拦住",在 `package.json` 里声明:
|
|
259
|
-
|
|
260
|
-
```json
|
|
261
|
-
"peerDependencies": { "@deepseek-ai/dsh": "^0.2.1-rc.2" }
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
dsh 会用这条做**安装预检**,不满足就拒绝安装,
|
|
265
|
-
可用 `dsh plugin --profile workbench allow-version <包@版本> --dsh-version <版本> --accept-risk` 豁免。
|
|
266
|
-
**自 0.2.1-alpha.4 起预检先读包自己的 `engines.dsh`**:声明了它,上面这条 peer 就被整体忽略
|
|
267
|
-
(只有没有 `engines.dsh` 时才看 peer;`engines.dsh` 里写 `workspace:` 一律判不兼容,
|
|
268
|
-
`engines` 不是对象或 `engines.dsh` 不是字符串会让清单不可读并**直接拒绝**)。要支持一段 dsh 版本,
|
|
269
|
-
写 `engines.dsh` 比写 peer 更直接。
|
|
273
|
+
(`packages/workbench` / `packages/shared`;改完要跑一次 `pnpm install` 更新 `pnpm-lock.yaml`),
|
|
274
|
+
**升这一份时要连 `engines.dsh` 一起升**(selfcheck 那条判据把两者钉在一起)。
|
|
270
275
|
|
|
271
276
|
> `NPM_REGISTRY` 是**你们自己的**内网 npm 源地址(形如 `https://<源主机>/<路径>/`)。
|
|
272
277
|
> 本仓不写它:开源包里不该出现任何内网地址。
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
`OPENCLI_PIN` 锁),但它连的 **daemon 是整机一个** —— 见下面「隔离边界」。
|
|
27
27
|
|
|
28
28
|
前提:`dsh` 和 `pnpm` 在 PATH 上(`dsh plugin` 是转发给 pnpm 的),
|
|
29
|
+
**`dsh` 不低于验过的那一代**(`engines.dsh`;旧到不认识它的 dsh 由启动器当场拒绝并给
|
|
30
|
+
`npm i -g @deepseek-ai/dsh@latest` —— 低于它的 dsh 上 DIY 模式整个不出现且不报错,见
|
|
31
|
+
[`.scratch/dsh-version-floor/spec.md`](../../../.scratch/dsh-version-floor/spec.md)),
|
|
29
32
|
且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)—— seed 拿它当**模板**
|
|
30
33
|
(bundle 清单),之后也是「配置同步」的默认来源。
|
|
31
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"icon": "./icon.svg",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
]
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
+
"//engines": "DIY 模式(preset/diy.patch.yml)是从 **dsh 出厂那份 standard.patch.yml** 派生的,所以这份外壳只跟着它派生的那一代 dsh 走:dsh 太旧时那个 preset 挂不起来,而 dsh 的模式选择器**只列挂得起来的** ⇒ 用户面前「工作台 DIY」整个不见,一句错都不报(实测 0.2.0-rc.2:缺 dsh-tool-schedule)。这里声明下限,让 dsh 自己的兼容性预检在**装的时候**拒绝(自 0.2.1-alpha.4 起 engines.dsh 优先于 peer)。⚠️ **不写 peerDependencies['@deepseek-ai/dsh']**:pnpm 的 auto-install-peers(v11 连 optional 也装)会把整套 dsh 拖进 profile。旧到不认识 engines.dsh 的 dsh(0.2.0 及更早)由启动器那道硬门禁兜 —— 见 bin/dsh-workbench。判据:selfcheck 的「声明下限 = 验过的 dsh 版本」那条。",
|
|
66
|
+
"engines": {
|
|
67
|
+
"dsh": ">=0.2.1-rc.2"
|
|
68
|
+
},
|
|
65
69
|
"peerDependencies": {
|
|
66
70
|
"react": "*"
|
|
67
71
|
},
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
"access": "public"
|
|
88
92
|
},
|
|
89
93
|
"dependencies": {
|
|
90
|
-
"@dsh-workbench/shared": "0.3.
|
|
94
|
+
"@dsh-workbench/shared": "0.3.10"
|
|
91
95
|
},
|
|
92
96
|
"scripts": {
|
|
93
97
|
"build": "node scripts/build.mjs && node scripts/build-types.mjs",
|
package/scripts/seed-profile.sh
CHANGED
|
@@ -28,6 +28,12 @@ DST_PROFILE="$DSH_HOME/profiles/$PROFILE"
|
|
|
28
28
|
PKG='@dsh-workbench/core'
|
|
29
29
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
30
30
|
GIT_TOP="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
31
|
+
# 同 bin/dsh-workbench 那一处(那边写清了为什么):`--show-toplevel` 只说明**某个祖先**是仓库,
|
|
32
|
+
# 装出来的那份可能刚好落在某个仓库里(`$HOME` 是 dotfiles 仓库时最常见)—— 还要证明这份
|
|
33
|
+
# package.json 本身被那个仓库跟踪,才算是仓库副本。
|
|
34
|
+
if [ -n "$GIT_TOP" ] && ! git -C "$REPO_DIR" ls-files --error-unmatch package.json >/dev/null 2>&1; then
|
|
35
|
+
GIT_TOP=""
|
|
36
|
+
fi
|
|
31
37
|
ISOLATION="$REPO_DIR/profile/isolation.patch.yml"
|
|
32
38
|
|
|
33
39
|
# `verify.sh` 留在**仓库根**(它证明的是"dsh web 未被动过 / 组合树差异 / 数据分家",
|
|
@@ -134,6 +140,34 @@ fs.writeFileSync(process.env.WB_MANIFEST, JSON.stringify(wb, null, 2) + "\n");
|
|
|
134
140
|
console.log("→ 已继承 web 的 bundle 列表: " + merged.join(", "));
|
|
135
141
|
'
|
|
136
142
|
|
|
143
|
+
# ── 第 2.5 步:修掉旧版启动器留下的"装出来的那份被当成仓库副本" ──────────
|
|
144
|
+
#
|
|
145
|
+
# 旧判据只问 `git rev-parse --show-toplevel`("**某个祖先**是不是仓库"),于是包目录落在任何
|
|
146
|
+
# 仓库里的机器(最常见:`$HOME` 是 dotfiles 仓库,而 npm/nvm 的前缀就在 `$HOME` 下)在 setup
|
|
147
|
+
# 时会把**全局安装目录**以 `link:` 装进 profile —— 市场里工作台那一行因此说「这份是本仓正在
|
|
148
|
+
# 开发的那一份,不从源上取。」(不比对源、不提示更新),而 `npm i -g …@latest` 还会悄悄换掉
|
|
149
|
+
# profile 跑的东西。判据窄到只认那一个坏形状:**我自己是装出来的那份**(GIT_TOP 为空)
|
|
150
|
+
# **且 profile 里那条 `link:` 指着的正是我自己这份目录** —— 指别处的 link:(用户自己链的)不动。
|
|
151
|
+
if [ -z "$GIT_TOP" ] && [ -f "$DST_PROFILE/package.json" ]; then
|
|
152
|
+
WB_MANIFEST="$DST_PROFILE/package.json" WB_PKG="$PKG" WB_SELF="$REPO_DIR" \
|
|
153
|
+
WB_TARGET="$PKG@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)" \
|
|
154
|
+
node -e '
|
|
155
|
+
const fs = require("node:fs");
|
|
156
|
+
const path = require("node:path");
|
|
157
|
+
const file = process.env.WB_MANIFEST;
|
|
158
|
+
const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
159
|
+
const deps = manifest.dependencies ?? {};
|
|
160
|
+
const spec = String(deps[process.env.WB_PKG] ?? "");
|
|
161
|
+
if (!spec.startsWith("link:")) process.exit(0);
|
|
162
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
163
|
+
const target = path.resolve(path.dirname(file), spec.slice("link:".length));
|
|
164
|
+
if (real(target) !== real(process.env.WB_SELF)) process.exit(0);
|
|
165
|
+
manifest.dependencies = Object.assign({}, deps, { [process.env.WB_PKG]: process.env.WB_TARGET });
|
|
166
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
|
|
167
|
+
console.log("→ profile 里那条 link: 指的是我这份装出来的副本(旧启动器判错的产物)—— 已改回 " + process.env.WB_TARGET);
|
|
168
|
+
'
|
|
169
|
+
fi
|
|
170
|
+
|
|
137
171
|
# ── 第 3 步:追加会话 / 存储 / 附件隔离层 ───────────────────────────────
|
|
138
172
|
if grep -q 'id: session-persistence-jsonl' "$DST_PATCH"; then
|
|
139
173
|
echo "→ 隔离层已存在,跳过"
|