dsh-plugin-pack 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +13 -1
- package/lib/import-plugin.js +3 -1
- package/lib/index.js +19 -1
- package/lib/scan.js +123 -27
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -103,6 +103,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
103
103
|
'import.restoreData': '还原数据目录',
|
|
104
104
|
'import.link': '建立 node_modules 链接',
|
|
105
105
|
'import.done': '导入完成',
|
|
106
|
+
'import.written': '已写入磁盘',
|
|
107
|
+
'import.whyNotListed':
|
|
108
|
+
'文件、依赖和配置都已经写到 profile 里了。「已安装」列表是 dsh 启动时读取的,重启后新插件才会出现在那里 —— 现在没显示不代表没装上。',
|
|
106
109
|
'import.restart': '重启 dsh 后生效',
|
|
107
110
|
'import.empty': '先选一个 zip 文件',
|
|
108
111
|
'import.source.bundle': '整合包',
|
|
@@ -286,6 +289,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
286
289
|
'import.restoreData': 'Restore data dirs',
|
|
287
290
|
'import.link': 'Link node_modules',
|
|
288
291
|
'import.done': 'Import finished',
|
|
292
|
+
'import.written': 'Written to disk',
|
|
293
|
+
'import.whyNotListed':
|
|
294
|
+
'Files, dependencies and config are already written into the profile. The "Installed" list is read by dsh at startup, so new plugins appear there only after a restart — not showing now does not mean it failed.',
|
|
289
295
|
'import.restart': 'Restart dsh to apply',
|
|
290
296
|
'import.empty': 'Choose a zip first',
|
|
291
297
|
'import.source.bundle': 'Bundle',
|
|
@@ -2626,8 +2632,14 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2626
2632
|
{ kind: 'ok' },
|
|
2627
2633
|
importResult.dryRun
|
|
2628
2634
|
? `${t('import.dry')} · ${importResult.plugins?.length ?? 0} ${t('plugins')}`
|
|
2629
|
-
: `${t('import.
|
|
2635
|
+
: `${t('import.written')} · ${importResult.plugins?.length ?? 0} ${t('plugins')} · ${t('import.restart')}`,
|
|
2630
2636
|
),
|
|
2637
|
+
/* 说清楚为什么"已安装"列表里还看不到:
|
|
2638
|
+
* 那个列表是 dsh 启动时读 profile 生成的,运行中不会自己刷新。
|
|
2639
|
+
* 文件现在确实已经在磁盘上了 —— 不说明这点用户会以为装失败。 */
|
|
2640
|
+
!importResult.dryRun
|
|
2641
|
+
? h('div', { className: 'pp-hint', style: { margin: '6px 0 0' } }, t('import.whyNotListed'))
|
|
2642
|
+
: null,
|
|
2631
2643
|
importResult.plugins?.length
|
|
2632
2644
|
? h(
|
|
2633
2645
|
'table',
|
package/lib/import-plugin.js
CHANGED
|
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|
|
16
16
|
import { readJson, writeJson, readText, writeText, ensureDir, isDir, isFile, copyTree, removeTree } from './util.js';
|
|
17
17
|
import { packSegment, stamp, humanBytes } from './home.js';
|
|
18
18
|
import { existingIds, rowId, splitTopLevel, splitInsertChildren, entryMatchesPlugin, appendInsertBlock } from './patch-text.js';
|
|
19
|
-
import { resolveEntry, isAbsoluteSpec } from './scan.js';
|
|
19
|
+
import { resolveEntry, isAbsoluteSpec, clearScanCache } from './scan.js';
|
|
20
20
|
|
|
21
21
|
/** 从一个目录读出插件身份,判断它能不能装。 */
|
|
22
22
|
export function describeLocalPlugin(dir) {
|
|
@@ -238,6 +238,8 @@ export function importLocalPlugin(options = {}) {
|
|
|
238
238
|
|
|
239
239
|
record.links = link ? [link] : [];
|
|
240
240
|
writeJson(path.join(backupRoot, 'record.json'), record);
|
|
241
|
+
// 装完必须让扫描缓存失效,否则界面还按旧结果报"未安装"
|
|
242
|
+
clearScanCache();
|
|
241
243
|
|
|
242
244
|
return {
|
|
243
245
|
ok: true,
|
package/lib/index.js
CHANGED
|
@@ -462,7 +462,25 @@ export function apply(ctx, config = {}) {
|
|
|
462
462
|
try {
|
|
463
463
|
mountRoutes(ctx, cfg);
|
|
464
464
|
} catch (error) {
|
|
465
|
-
console.warn('[dsh-plugin-pack] 路由挂载失败:', String(error?.message
|
|
465
|
+
console.warn('[dsh-plugin-pack] 路由挂载失败:', String(error?.message || error));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* 启动后台预热扫描缓存。
|
|
469
|
+
*
|
|
470
|
+
* 一次完整扫描要遍历上万文件(每个 stat 一次),冷启动要好几秒。
|
|
471
|
+
* 用户点开插件页时如果正好没缓存,就得干等 —— 这就是"加载插件时间很长"。
|
|
472
|
+
* 启动后延迟几秒(避开 dsh 自己的启动高峰)在后台扫一次,
|
|
473
|
+
* 之后界面打开直接命中缓存,毫秒级返回。
|
|
474
|
+
*
|
|
475
|
+
* 失败不阻断启动:最坏情况就是第一次打开慢一点。 */
|
|
476
|
+
try {
|
|
477
|
+
const warm = setTimeout(() => {
|
|
478
|
+
scanProfile({}).catch(() => {});
|
|
479
|
+
}, 5000);
|
|
480
|
+
warm.unref?.();
|
|
481
|
+
ctx.effect?.(() => () => clearTimeout(warm), 'dsh-plugin-pack: warm scan cache');
|
|
482
|
+
} catch {
|
|
483
|
+
/* 没有 effect 上下文时跳过 */
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
const commands = ctx.get?.('commands');
|
package/lib/scan.js
CHANGED
|
@@ -27,13 +27,73 @@ const OFFICIAL_PREFIX = '@deepseek-ai/';
|
|
|
27
27
|
* @type {Map<string, { at: number, value: object, pkgMtime: number, patchMtime: number }>}
|
|
28
28
|
*/
|
|
29
29
|
const scanCache = new Map();
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* 缓存有效期。
|
|
32
|
+
*
|
|
33
|
+
* 5 分钟:失效主要靠 mtime 判断(配置或插件目录一变就重扫),
|
|
34
|
+
* TTL 只是兜底防止极端情况下永远不刷新。这样界面打开是毫秒级,
|
|
35
|
+
* 装完插件下一轮也会立刻看到新状态。
|
|
36
|
+
*/
|
|
37
|
+
const SCAN_TTL_MS = 5 * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 单个插件目录的体积缓存(key = 目录真实路径)。
|
|
41
|
+
*
|
|
42
|
+
* 整个扫描里最贵的是 measureTree:dsh-ops 那种上万文件的插件,
|
|
43
|
+
* 光 stat 就要一两秒。但插件没装没删的时候体积不会变,
|
|
44
|
+
* 按目录 mtime 缓存一下,重复扫描就从"秒级"掉到"毫秒级"。
|
|
45
|
+
*
|
|
46
|
+
* 注意:目录 mtime 只在增删直接子项时变化,深处文件被改写不会变。
|
|
47
|
+
* 所以导出(要精确体积)走 fresh=true 全量测量,只有界面展示用缓存。
|
|
48
|
+
*
|
|
49
|
+
* @type {Map<string, { mtimeMs: number, bytes: number, files: number }>}
|
|
50
|
+
*/
|
|
51
|
+
const sizeCache = new Map();
|
|
31
52
|
|
|
32
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* 让扫描结果缓存失效(导入/导出/回滚后调用)。
|
|
55
|
+
*
|
|
56
|
+
* 只清「扫描结果」,不清「体积缓存」—— 体积缓存是按目录 mtime 自校验的:
|
|
57
|
+
* 刚装的那个插件目录 mtime 变了,下次自动重算;没动过的插件继续命中,
|
|
58
|
+
* 这样装完插件后的刷新仍然是快的(不用把上万个文件重新 stat 一遍)。
|
|
59
|
+
*/
|
|
33
60
|
export function clearScanCache() {
|
|
34
61
|
scanCache.clear();
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
/** 彻底清掉体积缓存(测试用;正式流程不必调,它自己会按 mtime 失效)。 */
|
|
65
|
+
export function clearSizeCache() {
|
|
66
|
+
sizeCache.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 带缓存版本的 measureTree。
|
|
71
|
+
*
|
|
72
|
+
* 目录 mtime 没变就直接用上次的结果,避免重复遍历上万个文件。
|
|
73
|
+
* 缓存的是"某个目录的总字节/文件数",与调用方传的 skipDirs 无关 ——
|
|
74
|
+
* 所以只缓存参数固定的那几处调用(主目录、依赖、数据目录各自独立 key 前缀)。
|
|
75
|
+
*
|
|
76
|
+
* @param {string} root
|
|
77
|
+
* @param {object} options
|
|
78
|
+
* @param {string} variant 区分不同过滤参数('main' / 'dep' / 'data')
|
|
79
|
+
*/
|
|
80
|
+
async function measureCached(root, options, variant) {
|
|
81
|
+
const key = `${variant}\0${root}`;
|
|
82
|
+
const mtimeMs = (() => {
|
|
83
|
+
try {
|
|
84
|
+
return fs.statSync(root).mtimeMs;
|
|
85
|
+
} catch {
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
const hit = sizeCache.get(key);
|
|
90
|
+
if (hit && hit.mtimeMs === mtimeMs && mtimeMs !== -1) return { bytes: hit.bytes, files: hit.files };
|
|
91
|
+
|
|
92
|
+
const measured = await measureTreeAsync(root, options);
|
|
93
|
+
if (mtimeMs !== -1) sizeCache.set(key, { mtimeMs, bytes: measured.bytes, files: measured.files });
|
|
94
|
+
return measured;
|
|
95
|
+
}
|
|
96
|
+
|
|
37
97
|
/** 这些包永远由宿主提供,即便出现在 dependencies 里也不打包。 */
|
|
38
98
|
const HOST_PROVIDED = new Set([
|
|
39
99
|
'react',
|
|
@@ -78,20 +138,38 @@ export async function scanProfile(options = {}) {
|
|
|
78
138
|
|
|
79
139
|
const pkgFile = path.join(dir, 'package.json');
|
|
80
140
|
const patchFile = path.join(dir, 'cordis.patch.yml');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
const localPluginsDir = path.join(dir, 'local-plugins');
|
|
142
|
+
const nodeModulesDir = path.join(dir, 'node_modules');
|
|
143
|
+
|
|
144
|
+
/* 缓存:一次完整扫描要好几秒(dsh-ops 上万文件,每个都要 stat)。
|
|
145
|
+
* 界面一打开就调 status,等于每次干等。
|
|
146
|
+
*
|
|
147
|
+
* 失效条件(任一):
|
|
148
|
+
* - 超过 TTL
|
|
149
|
+
* - package.json / cordis.patch.yml 变了
|
|
150
|
+
* - local-plugins 或 node_modules 目录 mtime 变了
|
|
151
|
+
* ← 这一条很关键:装完插件如果只往 local-plugins 放了目录、
|
|
152
|
+
* 而 package.json 因为某些原因没变,旧缓存会让界面显示"未安装"。
|
|
153
|
+
*/
|
|
85
154
|
const cacheKey = [dir, includeSecrets ? 's' : '', includeData ? 'd' : '', (options.include ?? []).join('|'), (options.exclude ?? []).join('|')].join('\0');
|
|
155
|
+
|
|
156
|
+
/** 读一个路径的 mtimeMs(不存在返回 -1),用于判断缓存是否还新鲜。 */
|
|
157
|
+
const mtimeOf = (target) => {
|
|
158
|
+
try {
|
|
159
|
+
return fs.statSync(target).mtimeMs;
|
|
160
|
+
} catch {
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
86
165
|
if (!options.fresh) {
|
|
87
166
|
const cached = scanCache.get(cacheKey);
|
|
88
167
|
if (cached && Date.now() - cached.at < SCAN_TTL_MS) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
168
|
+
const stampOk =
|
|
169
|
+
mtimeOf(pkgFile) === cached.pkgMtime &&
|
|
170
|
+
mtimeOf(patchFile) === cached.patchMtime &&
|
|
171
|
+
mtimeOf(localPluginsDir) === cached.localMtime &&
|
|
172
|
+
mtimeOf(nodeModulesDir) === cached.modulesMtime;
|
|
95
173
|
if (stampOk) return cached.value;
|
|
96
174
|
}
|
|
97
175
|
}
|
|
@@ -99,6 +177,14 @@ export async function scanProfile(options = {}) {
|
|
|
99
177
|
const pkg = readJson(pkgFile, null);
|
|
100
178
|
if (!pkg) warnings.push(`没有找到 profile 的 package.json: ${pkgFile}`);
|
|
101
179
|
|
|
180
|
+
/**
|
|
181
|
+
* 正式路径用的体积测量。
|
|
182
|
+
* fresh=true → 全量重算(导出要精确数字)
|
|
183
|
+
* 否则 → 命中目录 mtime 缓存(界面展示要快)
|
|
184
|
+
*/
|
|
185
|
+
const effectiveMeasure = async (root, opts, variant = 'x') =>
|
|
186
|
+
options.fresh ? measureTreeAsync(root, opts) : measureCached(root, opts, variant);
|
|
187
|
+
|
|
102
188
|
const bundles = Array.isArray(pkg?.dsh?.profile?.bundles) ? [...pkg.dsh.profile.bundles] : [];
|
|
103
189
|
const dependencies = pkg?.dependencies && typeof pkg.dependencies === 'object' ? { ...pkg.dependencies } : {};
|
|
104
190
|
|
|
@@ -154,8 +240,7 @@ export async function scanProfile(options = {}) {
|
|
|
154
240
|
addCandidate({ dir: resolved, declaredName: depName, link: spec, inBundles: false, source: 'dependencies' });
|
|
155
241
|
}
|
|
156
242
|
|
|
157
|
-
// 3) local-plugins
|
|
158
|
-
const localPluginsDir = path.join(dir, 'local-plugins');
|
|
243
|
+
// 3) local-plugins 下手工放的(目录变量在上面缓存校验时已声明)
|
|
159
244
|
if (isDir(localPluginsDir)) {
|
|
160
245
|
for (const entry of fs.readdirSync(localPluginsDir, { withFileTypes: true })) {
|
|
161
246
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
@@ -182,7 +267,8 @@ export async function scanProfile(options = {}) {
|
|
|
182
267
|
includeSecrets,
|
|
183
268
|
includeData,
|
|
184
269
|
warnings,
|
|
185
|
-
|
|
270
|
+
// 测试可注入自定义 measure;正式路径用带缓存的版本
|
|
271
|
+
measure: options.measure ?? effectiveMeasure,
|
|
186
272
|
});
|
|
187
273
|
if (!plugin) continue;
|
|
188
274
|
// dependencies 来源可能是纯工具库:既不是 dsh-* 命名,也没有 dsh 字段,
|
|
@@ -245,13 +331,15 @@ export async function scanProfile(options = {}) {
|
|
|
245
331
|
warnings,
|
|
246
332
|
};
|
|
247
333
|
|
|
248
|
-
//
|
|
334
|
+
// 写入缓存(记下配置与插件目录的 mtime,之后据此判断失效)
|
|
249
335
|
try {
|
|
250
336
|
scanCache.set(cacheKey, {
|
|
251
337
|
at: Date.now(),
|
|
252
338
|
value: result,
|
|
253
|
-
pkgMtime:
|
|
254
|
-
patchMtime:
|
|
339
|
+
pkgMtime: mtimeOf(pkgFile),
|
|
340
|
+
patchMtime: mtimeOf(patchFile),
|
|
341
|
+
localMtime: mtimeOf(localPluginsDir),
|
|
342
|
+
modulesMtime: mtimeOf(nodeModulesDir),
|
|
255
343
|
});
|
|
256
344
|
} catch {
|
|
257
345
|
/* 缓存可有可无 */
|
|
@@ -355,10 +443,14 @@ async function collectDepsAsync(rootDir, meta, { profileDir, includeSecrets, war
|
|
|
355
443
|
if (seen.has(depDir)) continue;
|
|
356
444
|
seen.add(depDir);
|
|
357
445
|
const depMeta = readJson(path.join(depDir, 'package.json'), {});
|
|
358
|
-
const size = await measure(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
446
|
+
const size = await measure(
|
|
447
|
+
depDir,
|
|
448
|
+
{
|
|
449
|
+
skipDirs: new Set([...SKIP_DIRS, 'node_modules']),
|
|
450
|
+
keepFile,
|
|
451
|
+
},
|
|
452
|
+
'dep',
|
|
453
|
+
);
|
|
362
454
|
const record = {
|
|
363
455
|
name: depName,
|
|
364
456
|
version: depMeta.version ?? '0.0.0',
|
|
@@ -438,10 +530,14 @@ async function buildPlugin(item, { profileDir, dshHome, topEntries, includeSecre
|
|
|
438
530
|
}
|
|
439
531
|
|
|
440
532
|
const skipFileKeep = fileFilter({ includeSecrets });
|
|
441
|
-
const mainSize = await measure(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
533
|
+
const mainSize = await measure(
|
|
534
|
+
item.dir,
|
|
535
|
+
{
|
|
536
|
+
skipDirs: new Set([...SKIP_DIRS, 'node_modules']),
|
|
537
|
+
keepFile: skipFileKeep,
|
|
538
|
+
},
|
|
539
|
+
'main',
|
|
540
|
+
);
|
|
445
541
|
const deps = await collectDepsAsync(item.dir, meta, { profileDir, includeSecrets, warnings: pluginWarnings, measure });
|
|
446
542
|
|
|
447
543
|
const patchInfo = matchPatchRows(topEntries, { name, dir: item.dir });
|
|
@@ -450,7 +546,7 @@ async function buildPlugin(item, { profileDir, dshHome, topEntries, includeSecre
|
|
|
450
546
|
const dataDirs = [];
|
|
451
547
|
if (includeData) {
|
|
452
548
|
for (const candidate of dataCandidates) {
|
|
453
|
-
const size = await measure(candidate.path, { skipDirs: SKIP_DIRS, keepFile: skipFileKeep });
|
|
549
|
+
const size = await measure(candidate.path, { skipDirs: SKIP_DIRS, keepFile: skipFileKeep }, 'data');
|
|
454
550
|
dataDirs.push({ ...candidate, bytes: size.bytes, files: size.files });
|
|
455
551
|
}
|
|
456
552
|
}
|
package/package.json
CHANGED