@dsh-workbench/core 1.0.0-beta.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -16
- package/bin/dsh-workbench +59 -26
- package/client.js +227 -328
- package/dist/client/client.body.js +227 -328
- package/dist/client/index.js +227 -328
- package/dist/host/index.js +108 -74
- package/dist/types/app/layouts/rightbar/market-shared-rows.d.ts +0 -1
- package/dist/types/app/layouts/workbench-settings/ConfigSyncRows.d.ts +1 -3
- package/dist/types/kernel/contract/config-sync.d.ts +1 -1
- package/dist/types/modules/panels/model/diy-context.d.ts +14 -4
- package/dist/types/modules/panels/model/diy-report.d.ts +2 -0
- package/dist/types/modules/panels/model/diy-tools.d.ts +2 -1
- package/package.json +5 -3
- package/profile/sandbox.patch.yml +7 -5
- package/scripts/clone-profile.mjs +11 -199
- package/scripts/create-workbench.mjs +7 -20
- package/scripts/patch-blocks.mjs +60 -0
- package/scripts/seed-profile.sh +17 -28
- package/scripts/share-data-root.mjs +419 -0
- package/skills/workbench-module-diy/FAMILY.md +4 -6
- package/skills/workbench-module-diy/SKILL.md +4 -5
- package/skills/workbench-module-diy/WIRING.md +4 -13
- package/dist/types/shared/api/host/git.test.d.ts +0 -1
- package/profile/isolation.patch.yml +0 -46
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import {
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { parseBlocks } from './patch-blocks.mjs';
|
|
6
7
|
|
|
7
8
|
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
|
|
8
9
|
const sourceName = process.env.WB_SOURCE_PROFILE?.trim() || 'web';
|
|
9
10
|
const targetName = process.env.WB_TARGET_PROFILE?.trim() || '';
|
|
10
11
|
const sourceDir = join(home, 'profiles', sourceName);
|
|
11
12
|
const targetDir = join(home, 'profiles', targetName);
|
|
12
|
-
const sourceCandidate = join(home, sourceName);
|
|
13
|
-
const sourcePatchForHome = join(sourceDir, 'cordis.patch.yml');
|
|
14
|
-
const sourcePatchText = existsSync(sourcePatchForHome) ? readText(sourcePatchForHome) : '';
|
|
15
|
-
const sourceHasIsolation = /id:\s*session-persistence-jsonl/.test(sourcePatchText);
|
|
16
|
-
const sourceHome =
|
|
17
|
-
process.env.WB_SOURCE_HOME?.trim() ||
|
|
18
|
-
(!sourceHasIsolation
|
|
19
|
-
? home
|
|
20
|
-
: sourcePatchText.includes('DSH_WORKBENCH_HOME') && sourceName !== 'workbench'
|
|
21
|
-
? sourceCandidate
|
|
22
|
-
: join(home, 'workbench'));
|
|
23
|
-
const targetHome = process.env.DSH_WORKBENCH_HOME?.trim() || join(home, targetName);
|
|
24
13
|
const copySettings = process.env.WB_COPY_SETTINGS === '1';
|
|
25
|
-
const copyPlugins = process.env.WB_COPY_PLUGINS === '1';
|
|
26
|
-
const copyProjects = process.env.WB_COPY_PROJECTS === '1';
|
|
27
|
-
const copySessions = process.env.WB_COPY_SESSIONS === '1';
|
|
28
14
|
|
|
29
15
|
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
30
|
-
const
|
|
16
|
+
const SKIPPED_IDS = new Set([
|
|
31
17
|
'session-persistence-jsonl',
|
|
32
18
|
'storage-json',
|
|
33
19
|
'attachment-local',
|
|
34
20
|
'sandbox-policy',
|
|
35
21
|
]);
|
|
36
|
-
const ASSET_MAX_BYTES = 8 * 1024 * 1024;
|
|
37
|
-
const ASSET_MAX_FILES = 500;
|
|
38
|
-
const ASSET_SKIP = new Set(['node_modules', '.git', '.DS_Store']);
|
|
39
22
|
|
|
40
23
|
function say(message) {
|
|
41
24
|
process.stdout.write(`→ ${message}\n`);
|
|
@@ -56,105 +39,6 @@ function readText(file) {
|
|
|
56
39
|
return readFileSync(file, 'utf8');
|
|
57
40
|
}
|
|
58
41
|
|
|
59
|
-
if (copySettings && !copyPlugins) {
|
|
60
|
-
const refs = localPluginRefs();
|
|
61
|
-
if (refs > 0) {
|
|
62
|
-
fail(`设置里有 ${refs} 个本地插件文件引用;不能只复制设置不复制插件(会指向不存在的文件)。要么一起复制插件,要么取消设置。`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseBlocks(text) {
|
|
67
|
-
const lines = text.split('\n');
|
|
68
|
-
const starts = [];
|
|
69
|
-
lines.forEach((line, index) => {
|
|
70
|
-
if (/^- /.test(line) || line === '-') starts.push(index);
|
|
71
|
-
});
|
|
72
|
-
const blocks = [];
|
|
73
|
-
for (let i = 0; i < starts.length; i += 1) {
|
|
74
|
-
const from = starts[i];
|
|
75
|
-
const to = i + 1 < starts.length ? starts[i + 1] : lines.length;
|
|
76
|
-
let end = to;
|
|
77
|
-
while (end > from && (lines[end - 1] ?? '').trim() === '') end -= 1;
|
|
78
|
-
const body = lines.slice(from, end).join('\n');
|
|
79
|
-
if (body.trim() === '') continue;
|
|
80
|
-
const first = body.split('\n')[0] ?? '';
|
|
81
|
-
const id = /^- id:[ \t]*"?([^"\s#]+)/.exec(first)?.[1];
|
|
82
|
-
const insert = id === undefined && /^- insert:[ \t]*$/.test(first);
|
|
83
|
-
const nested = body
|
|
84
|
-
.split('\n')
|
|
85
|
-
.map((line) => /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/.exec(line)?.[1])
|
|
86
|
-
.filter(Boolean);
|
|
87
|
-
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(i)}`;
|
|
88
|
-
blocks.push({ key, id: id ?? nested.join(', ') ?? key, insert, body });
|
|
89
|
-
}
|
|
90
|
-
return blocks;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function namesOf(body) {
|
|
94
|
-
return body
|
|
95
|
-
.split('\n')
|
|
96
|
-
.map((line) => /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/.exec(line)?.[1])
|
|
97
|
-
.filter((name) => name !== undefined && (name.startsWith('./') || name.startsWith('../') || name.startsWith('file://') || isAbsolute(name)));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function insideProfile(dir, ref) {
|
|
101
|
-
if (ref.startsWith('file://')) return null;
|
|
102
|
-
if (!ref.startsWith('./') && !ref.startsWith('../') && !isAbsolute(ref)) return null;
|
|
103
|
-
const abs = resolve(dir, ref);
|
|
104
|
-
const rel = relative(dir, abs);
|
|
105
|
-
if (rel === '' || rel === '..' || rel.startsWith(`..${requireSep()}`) || isAbsolute(rel)) return null;
|
|
106
|
-
return { rel: rel.split(requireSep()).join('/'), abs };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function requireSep() {
|
|
110
|
-
return process.platform === 'win32' ? '\\' : '/';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function copyFileAsset(source, target, budget) {
|
|
114
|
-
const stat = statSync(source);
|
|
115
|
-
if (stat.isSymbolicLink() || !stat.isFile()) return false;
|
|
116
|
-
if (stat.size > ASSET_MAX_BYTES) return false;
|
|
117
|
-
budget.bytes += stat.size;
|
|
118
|
-
if (budget.bytes > 32 * 1024 * 1024 || budget.files >= ASSET_MAX_FILES) return false;
|
|
119
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
120
|
-
cpSync(source, target);
|
|
121
|
-
budget.files += 1;
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function copyDirAsset(source, target, budget) {
|
|
126
|
-
const walk = (dir) => {
|
|
127
|
-
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
128
|
-
if (ASSET_SKIP.has(entry.name)) continue;
|
|
129
|
-
const abs = join(dir, entry.name);
|
|
130
|
-
if (entry.isSymbolicLink()) return false;
|
|
131
|
-
if (entry.isDirectory()) {
|
|
132
|
-
if (!walk(abs)) return false;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (!entry.isFile()) continue;
|
|
136
|
-
const rel = relative(source, abs);
|
|
137
|
-
if (!copyFileAsset(abs, join(target, rel), budget)) return false;
|
|
138
|
-
}
|
|
139
|
-
return true;
|
|
140
|
-
};
|
|
141
|
-
return walk(source);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function localPluginRefs() {
|
|
145
|
-
const sourcePatch = join(sourceDir, 'cordis.patch.yml');
|
|
146
|
-
if (!existsSync(sourcePatch)) return 0;
|
|
147
|
-
const seen = new Set();
|
|
148
|
-
for (const block of parseBlocks(readText(sourcePatch))) {
|
|
149
|
-
if (!block.insert) continue;
|
|
150
|
-
for (const ref of namesOf(block.body)) {
|
|
151
|
-
const located = insideProfile(sourceDir, ref);
|
|
152
|
-
if (located !== null) seen.add(located.rel);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return seen.size;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
42
|
function rewritePaths(body) {
|
|
159
43
|
return body.replace(/(dshHomePath\(\s*['"])workbench(?=[/'"])/g, `$1${targetName}`);
|
|
160
44
|
}
|
|
@@ -172,8 +56,13 @@ function copySettingRows() {
|
|
|
172
56
|
const targetKeys = new Set(parseBlocks(current).map((block) => block.key));
|
|
173
57
|
const copied = [];
|
|
174
58
|
const skipped = [];
|
|
59
|
+
let plugins = 0;
|
|
175
60
|
for (const block of sourceBlocks) {
|
|
176
|
-
if (
|
|
61
|
+
if (block.insert) {
|
|
62
|
+
plugins += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (SKIPPED_IDS.has(block.id) || targetKeys.has(block.key)) {
|
|
177
66
|
skipped.push(block.id);
|
|
178
67
|
continue;
|
|
179
68
|
}
|
|
@@ -183,86 +72,9 @@ function copySettingRows() {
|
|
|
183
72
|
targetKeys.add(block.key);
|
|
184
73
|
copied.push(block.id);
|
|
185
74
|
}
|
|
186
|
-
say(`设置行:复制 ${copied.length},跳过 ${skipped.length}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function copyPluginAssets() {
|
|
190
|
-
const sourcePatch = join(sourceDir, 'cordis.patch.yml');
|
|
191
|
-
if (!existsSync(sourcePatch)) return;
|
|
192
|
-
const budget = { bytes: 0, files: 0 };
|
|
193
|
-
const seen = new Set();
|
|
194
|
-
let copied = 0;
|
|
195
|
-
for (const block of parseBlocks(readText(sourcePatch))) {
|
|
196
|
-
if (!block.insert || ISOLATION_IDS.has(block.id)) continue;
|
|
197
|
-
for (const ref of namesOf(block.body)) {
|
|
198
|
-
const located = insideProfile(sourceDir, ref);
|
|
199
|
-
if (located === null || seen.has(located.rel)) continue;
|
|
200
|
-
seen.add(located.rel);
|
|
201
|
-
const source = join(sourceDir, located.rel);
|
|
202
|
-
const target = join(targetDir, located.rel);
|
|
203
|
-
if (!existsSync(source)) {
|
|
204
|
-
say(`缺文件,跳过:${located.rel}`);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const stat = statSync(source);
|
|
208
|
-
const ok = stat.isDirectory()
|
|
209
|
-
? copyDirAsset(source, target, budget)
|
|
210
|
-
: copyFileAsset(source, target, budget);
|
|
211
|
-
if (ok) copied += 1;
|
|
212
|
-
else say(`文件太大或含符号链接,跳过:${located.rel}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
say(`本地插件文件:复制 ${copied} 个`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function copyProjectList() {
|
|
219
|
-
const source = join(sourceHome, 'storages', 'workspace.json');
|
|
220
|
-
if (!existsSync(source)) {
|
|
221
|
-
say('来源没有项目列表,跳过');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const target = join(targetHome, 'storages', 'workspace.json');
|
|
225
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
226
|
-
cpSync(source, target);
|
|
227
|
-
say('项目列表:已复制');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function copySessionData() {
|
|
231
|
-
const source = join(sourceHome, 'sessions');
|
|
232
|
-
if (!existsSync(source)) {
|
|
233
|
-
say('来源没有会话记录,跳过');
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const target = join(targetHome, 'sessions');
|
|
237
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
238
|
-
cpSync(source, target, { recursive: true });
|
|
239
|
-
say('会话记录:已复制');
|
|
240
|
-
const attachments = join(sourceHome, 'attachments');
|
|
241
|
-
if (existsSync(attachments)) cpSync(attachments, join(targetHome, 'attachments'), { recursive: true });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function mergeDependencies() {
|
|
245
|
-
const sourceManifest = JSON.parse(readText(join(sourceDir, 'package.json')));
|
|
246
|
-
const targetManifest = JSON.parse(readText(join(targetDir, 'package.json')));
|
|
247
|
-
const sourceDeps = sourceManifest.dependencies ?? {};
|
|
248
|
-
const targetDeps = targetManifest.dependencies ?? {};
|
|
249
|
-
const merged = { ...targetDeps };
|
|
250
|
-
for (const [name, spec] of Object.entries(sourceDeps)) {
|
|
251
|
-
if (name === '@dsh-workbench/core') continue;
|
|
252
|
-
if (!Object.hasOwn(merged, name)) merged[name] = spec;
|
|
253
|
-
}
|
|
254
|
-
targetManifest.dependencies = merged;
|
|
255
|
-
const tmp = `${join(targetDir, 'package.json')}.wb-clone`;
|
|
256
|
-
writeFileSync(tmp, `${JSON.stringify(targetManifest, null, 2)}\n`, 'utf8');
|
|
257
|
-
renameSync(tmp, join(targetDir, 'package.json'));
|
|
258
|
-
say(`依赖:并入 ${Object.keys(merged).length - Object.keys(targetDeps).length} 项`);
|
|
75
|
+
say(`设置行:复制 ${copied.length},跳过 ${skipped.length};挂新插件的 ${plugins} 行不同步`);
|
|
259
76
|
}
|
|
260
77
|
|
|
261
78
|
mkdirSync(targetDir, { recursive: true });
|
|
262
|
-
mkdirSync(targetHome, { recursive: true });
|
|
263
79
|
if (copySettings) copySettingRows();
|
|
264
|
-
if (copyPlugins) copyPluginAssets();
|
|
265
|
-
mergeDependencies();
|
|
266
|
-
if (copyProjects) copyProjectList();
|
|
267
|
-
if (copySessions) copySessionData();
|
|
268
80
|
say('完成');
|
|
@@ -122,32 +122,22 @@ const nameAnswer = await prompts(
|
|
|
122
122
|
);
|
|
123
123
|
const name = String(nameAnswer.name).trim();
|
|
124
124
|
|
|
125
|
+
say('会话、项目、附件跟 web / Desktop 共用一份,不用复制;这里只问设置。');
|
|
125
126
|
const copyAnswer = await prompts(
|
|
126
127
|
{
|
|
127
|
-
type: '
|
|
128
|
-
name: '
|
|
129
|
-
message: `从 ${source}
|
|
130
|
-
|
|
131
|
-
choices: [
|
|
132
|
-
{ title: '设置(模型、主题等)', value: 'settings', selected: true },
|
|
133
|
-
{ title: '本地插件文件', value: 'plugins', selected: true },
|
|
134
|
-
{ title: '项目列表', value: 'projects', selected: true },
|
|
135
|
-
{ title: '会话记录(可选,可能很多)', value: 'sessions', selected: false },
|
|
136
|
-
],
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
name: 'settings',
|
|
130
|
+
message: `从 ${source} 复制已配好的设置(模型、主题等)?`,
|
|
131
|
+
initial: true,
|
|
137
132
|
},
|
|
138
133
|
{ onCancel },
|
|
139
134
|
);
|
|
140
|
-
const copy = new Set(copyAnswer.copy ?? []);
|
|
141
|
-
if (copy.has('settings') && !copy.has('plugins')) {
|
|
142
|
-
copy.add('plugins');
|
|
143
|
-
say('设置行引用的本地插件会一起复制(否则配置会指向不存在的文件)。');
|
|
144
|
-
}
|
|
145
135
|
|
|
146
136
|
const confirmAnswer = await prompts(
|
|
147
137
|
{
|
|
148
138
|
type: 'confirm',
|
|
149
139
|
name: 'ok',
|
|
150
|
-
message: `创建 ${name}(来源 ${source}
|
|
140
|
+
message: `创建 ${name}(来源 ${source},${copyAnswer.settings ? '复制设置' : '不复制设置'})?`,
|
|
151
141
|
initial: true,
|
|
152
142
|
},
|
|
153
143
|
{ onCancel },
|
|
@@ -158,9 +148,6 @@ if (!confirmAnswer.ok) {
|
|
|
158
148
|
}
|
|
159
149
|
|
|
160
150
|
const args = ['setup', '--from', source, '--profile', name];
|
|
161
|
-
if (
|
|
162
|
-
if (!copy.has('settings')) args.push('--no-settings');
|
|
163
|
-
if (!copy.has('plugins')) args.push('--no-plugins');
|
|
164
|
-
if (!copy.has('projects')) args.push('--no-projects');
|
|
151
|
+
if (!copyAnswer.settings) args.push('--no-settings');
|
|
165
152
|
|
|
166
153
|
runSetup(args);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function parseBlocks(text) {
|
|
2
|
+
const lines = text.split('\n');
|
|
3
|
+
const starts = [];
|
|
4
|
+
lines.forEach((line, index) => {
|
|
5
|
+
if (/^- /.test(line) || line === '-') starts.push(index);
|
|
6
|
+
});
|
|
7
|
+
const blocks = [];
|
|
8
|
+
for (let i = 0; i < starts.length; i += 1) {
|
|
9
|
+
const from = starts[i];
|
|
10
|
+
const to = i + 1 < starts.length ? starts[i + 1] : lines.length;
|
|
11
|
+
let end = to;
|
|
12
|
+
while (end > from && (lines[end - 1] ?? '').trim() === '') end -= 1;
|
|
13
|
+
const body = lines.slice(from, end).join('\n');
|
|
14
|
+
if (body.trim() === '') continue;
|
|
15
|
+
const first = body.split('\n')[0] ?? '';
|
|
16
|
+
const id = /^- id:[ \t]*"?([^"\s#]+)/.exec(first)?.[1];
|
|
17
|
+
const insert = id === undefined && /^- insert:[ \t]*$/.test(first);
|
|
18
|
+
const nested = body
|
|
19
|
+
.split('\n')
|
|
20
|
+
.map((line) => /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/.exec(line)?.[1])
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(i)}`;
|
|
23
|
+
blocks.push({
|
|
24
|
+
key,
|
|
25
|
+
id: id ?? nested.join(', ') ?? key,
|
|
26
|
+
insert,
|
|
27
|
+
body,
|
|
28
|
+
lineStart: from,
|
|
29
|
+
lineEnd: end,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return blocks;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function removeBlocks(text, predicate) {
|
|
36
|
+
const blocks = parseBlocks(text).filter(predicate);
|
|
37
|
+
if (blocks.length === 0) return text;
|
|
38
|
+
const lines = text.split('\n');
|
|
39
|
+
const drop = new Set();
|
|
40
|
+
for (const block of blocks) {
|
|
41
|
+
let end = block.lineEnd;
|
|
42
|
+
for (let i = block.lineEnd; i < lines.length; i += 1) {
|
|
43
|
+
if ((lines[i] ?? '').trim() !== '') break;
|
|
44
|
+
drop.add(i);
|
|
45
|
+
end = i + 1;
|
|
46
|
+
}
|
|
47
|
+
for (let i = block.lineStart; i < end; i += 1) drop.add(i);
|
|
48
|
+
}
|
|
49
|
+
const kept = lines.filter((_, index) => !drop.has(index));
|
|
50
|
+
return `${kept.join('\n').replace(/\s+$/, '')}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appendBlocks(text, body) {
|
|
54
|
+
const stripped = text
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter((line) => line.trim() !== '[]')
|
|
57
|
+
.join('\n')
|
|
58
|
+
.replace(/\s+$/, '');
|
|
59
|
+
return `${stripped}\n\n${body.replace(/\s+$/, '')}\n`;
|
|
60
|
+
}
|
package/scripts/seed-profile.sh
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
#
|
|
3
3
|
# 一次性引导:从 shipped 的 web 模板创建 workbench profile,
|
|
4
4
|
# 继承 web 的 **bundle 清单**(这台机器上装了哪些 bundle),
|
|
5
|
-
#
|
|
5
|
+
# 补上可写根那一行;新 profile 直接跟 web 共用数据根,旧 profile 保持原状,等用户跑 migrate。
|
|
6
6
|
#
|
|
7
7
|
# **设置不在这里继承**(这里曾经把 web 的 cordis.patch.yml 快照一份过来):
|
|
8
|
-
# 工作台自己的 patch
|
|
8
|
+
# 工作台自己的 patch 是唯一真相,想同步 web 的哪几行由用户走「设置 → 配置同步」。
|
|
9
9
|
# 为什么这么分(安装 vs 设置)、为什么不再是自动的:内部设计记录
|
|
10
10
|
#
|
|
11
|
-
# 幂等 / 可续跑:profile
|
|
12
|
-
# —— 重复运行不会覆盖任何东西。
|
|
11
|
+
# 幂等 / 可续跑:profile 已存在就跳过创建;数据根已经是共用的、或旧数据还没迁
|
|
12
|
+
# (只提示 migrate)就什么都不写 —— 重复运行不会覆盖任何东西。
|
|
13
13
|
# 安全:全程只**读** web 的 profile 目录,只**写** workbench 自己的目录。
|
|
14
14
|
#
|
|
15
15
|
# 用法:
|
|
@@ -37,7 +37,6 @@ GIT_TOP="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
|
37
37
|
if [ -n "$GIT_TOP" ] && ! git -C "$REPO_DIR" ls-files --error-unmatch package.json >/dev/null 2>&1; then
|
|
38
38
|
GIT_TOP=""
|
|
39
39
|
fi
|
|
40
|
-
ISOLATION="$REPO_DIR/profile/isolation.patch.yml"
|
|
41
40
|
|
|
42
41
|
# `verify.sh` 留在**仓库根**(它证明的是"dsh web 未被动过 / 组合树差异 / 数据分家",
|
|
43
42
|
# 与包边界无关),所以不能按 $REPO_DIR 拼。装成安装包(没有 git 工作树)时它不存在,
|
|
@@ -100,7 +99,6 @@ HAS_SOURCE=0
|
|
|
100
99
|
if [ -f "$SRC_PROFILE/package.json" ]; then
|
|
101
100
|
HAS_SOURCE=1
|
|
102
101
|
fi
|
|
103
|
-
[ -f "$ISOLATION" ] || die "找不到 $ISOLATION"
|
|
104
102
|
|
|
105
103
|
DST_PATCH="$DST_PROFILE/cordis.patch.yml"
|
|
106
104
|
|
|
@@ -178,24 +176,14 @@ console.log("→ profile 里那条 link: 指的是我这份装出来的副本(
|
|
|
178
176
|
'
|
|
179
177
|
fi
|
|
180
178
|
|
|
181
|
-
# ── 第 3
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# 所以追加之前先把那行单独的 [] 拿掉;本来就没有(或已经是序列)时这一步什么都不做。
|
|
190
|
-
if grep -qx '\[\]' "$DST_PATCH"; then
|
|
191
|
-
grep -vx '\[\]' "$DST_PATCH" >"$DST_PATCH.tmp" && mv "$DST_PATCH.tmp" "$DST_PATCH"
|
|
192
|
-
echo "→ 模板 patch 里的空数组 [] 已让位(否则接不上隔离层)"
|
|
193
|
-
fi
|
|
194
|
-
printf '\n' >>"$DST_PATCH"
|
|
195
|
-
cat "$ISOLATION" >>"$DST_PATCH"
|
|
196
|
-
echo "→ 已追加隔离层(sessions / storages / attachments 分家)"
|
|
197
|
-
rm -f "$DST_PATCH.tmp"
|
|
198
|
-
fi
|
|
179
|
+
# ── 第 3 步:数据根配置 + 可写根那一行 ─────────────────────────────────
|
|
180
|
+
#
|
|
181
|
+
# 新 profile 跟 `dsh web` / Desktop 一样只分 profile,数据落在 `$DSH_HOME`。
|
|
182
|
+
# 旧 profile 如果还有没迁的数据,这里只打印 migrate 提示,保持隔离、不动数据;
|
|
183
|
+
# 用户自己跑 `dsh-workbench migrate` 才切共用根并并数据。
|
|
184
|
+
# 每一步都幂等:已经是共用数据根的 profile 一个字节都不写。
|
|
185
|
+
WORKBENCH_PROFILE="$PROFILE" DSH_HOME="$DSH_HOME" DSH_WORKBENCH_HOME="$WORKBENCH_HOME" \
|
|
186
|
+
node "$REPO_DIR/scripts/share-data-root.mjs" || die "整理数据根失败(见上一行)"
|
|
199
187
|
|
|
200
188
|
# ── 第 4 步:建 DIY 工坊 ────────────────────────────────────────────────
|
|
201
189
|
#
|
|
@@ -215,11 +203,12 @@ fi
|
|
|
215
203
|
|
|
216
204
|
cat <<EOF
|
|
217
205
|
|
|
218
|
-
profile '$DST_PROFILE' 的骨架好了(bundle 清单 +
|
|
206
|
+
profile '$DST_PROFILE' 的骨架好了(bundle 清单 + 可写根)。
|
|
207
|
+
旧实例如果还有数据没迁,启动时会提示;要切共用根并并旧数据,跑 dsh-workbench migrate。
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
bundle 清单,想要 web 那边的模型供应商 / 搜索源 / MCP,起工作台后去
|
|
222
|
-
「设置 → 配置同步」按行勾(读的是那个 profile 的 cordis.patch.yml
|
|
209
|
+
设置按创建时的勾选从 $SRC_PROFILE_NAME 继承(只同步改已有插件的行;挂新插件的行不同步);
|
|
210
|
+
直接跑 setup 时只继承 bundle 清单,想要 web 那边的模型供应商 / 搜索源 / MCP,起工作台后去
|
|
211
|
+
「设置 → 配置同步」按行勾(读的是那个 profile 的 cordis.patch.yml;同步完重启生效)。
|
|
223
212
|
|
|
224
213
|
装 bundle —— 两条**等价**的路,任选一条(都走 dsh 的 runPluginCommand,装完会
|
|
225
214
|
把 bundle 登记进 profile 清单):
|