dsh-waifu-agents 0.2.1 → 0.2.3
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 +82 -8
- package/index.js +347 -37
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -149,6 +149,9 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
updateInstallingFrom: '正在从 {registry} 安装(第 {index}/{total} 个源)…',
|
|
150
150
|
updateApplying: '正在应用配置,请稍候…',
|
|
151
151
|
updateCancelling: '正在停止安装…',
|
|
152
|
+
updateRemoving: '正在卸载旧版本…',
|
|
153
|
+
updateReinstalling: '正在安装新版本…',
|
|
154
|
+
updateHandedOff: '插件已随更新重启;到「插件」页确认版本即可。',
|
|
152
155
|
updateWaiting: '安装进行中,别关掉这个窗口',
|
|
153
156
|
updateElapsed: '已用 {s} 秒',
|
|
154
157
|
updateQuiet: '已经 {s} 秒没等到平台的进度回报,但 pnpm 还在跑',
|
|
@@ -256,12 +259,14 @@ window.__ModuleLoader__.load({
|
|
|
256
259
|
purgeHint: '把本插件留在文件系统里的东西一次清掉:会话覆盖记录、全局默认人设指向。打开右边开关会连人设目录一起删。',
|
|
257
260
|
purgeWithDir: '连人设目录一起删',
|
|
258
261
|
purgeAskTitle: '彻底清理本插件的数据',
|
|
259
|
-
purgeAskBody: '
|
|
262
|
+
purgeAskBody: '会删除:会话覆盖记录、全局默认人设指向,以及 pnpm-workspace.yaml 里属于本包的放行记录。安装日志、pnpm 索引属于平台,这里不动它们,清理完会列给你。',
|
|
260
263
|
purgeAskAck: '我知道这会清掉本插件的数据',
|
|
261
264
|
updateBanner: '有新版本 {latest}(当前 {installed})',
|
|
262
265
|
updateGo: '查看',
|
|
263
266
|
checkUpdate: '检查更新',
|
|
264
267
|
recheck: '重新检查更新',
|
|
268
|
+
updateUpToDate: '已是最新版本 {installed}',
|
|
269
|
+
updateCheckFailed: '检查更新失败',
|
|
265
270
|
upToDate: '已是最新版本',
|
|
266
271
|
canUpdateTo: '可更新到',
|
|
267
272
|
nowVersion: '当前',
|
|
@@ -395,6 +400,9 @@ window.__ModuleLoader__.load({
|
|
|
395
400
|
updateInstallingFrom: 'Installing from {registry} (source {index} of {total})…',
|
|
396
401
|
updateApplying: 'Applying the configuration, please wait…',
|
|
397
402
|
updateCancelling: 'Stopping the installation…',
|
|
403
|
+
updateRemoving: 'Removing the old version…',
|
|
404
|
+
updateReinstalling: 'Installing the new version…',
|
|
405
|
+
updateHandedOff: 'The plugin restarted with the update; confirm the version on the Plugins page.',
|
|
398
406
|
updateWaiting: 'Installing — leave this window open',
|
|
399
407
|
updateElapsed: '{s}s elapsed',
|
|
400
408
|
updateQuiet: 'no progress report from the platform for {s}s, but pnpm is still running',
|
|
@@ -499,12 +507,14 @@ window.__ModuleLoader__.load({
|
|
|
499
507
|
purgeHint: 'Remove everything this plugin left on disk in one go: the session override store and the global-default pointer. The switch on the right also deletes the persona directory.',
|
|
500
508
|
purgeWithDir: 'Also delete the persona directory',
|
|
501
509
|
purgeAskTitle: 'Purge this plugin\'s data',
|
|
502
|
-
purgeAskBody: 'Deletes the session override store
|
|
510
|
+
purgeAskBody: 'Deletes the session override store, the global-default pointer, and this package\'s own entries in pnpm-workspace.yaml\'s release-age allow-list. The install logs and the pnpm store index belong to the platform and are listed afterwards instead of touched.',
|
|
503
511
|
purgeAskAck: 'I know this removes the plugin\'s data',
|
|
504
512
|
updateBanner: 'Version {latest} is out (running {installed})',
|
|
505
513
|
updateGo: 'Details',
|
|
506
514
|
checkUpdate: 'Check for updates',
|
|
507
515
|
recheck: 'Check again',
|
|
516
|
+
updateUpToDate: 'Already on the newest version {installed}',
|
|
517
|
+
updateCheckFailed: 'The update check failed',
|
|
508
518
|
upToDate: 'Up to date',
|
|
509
519
|
canUpdateTo: 'Update to',
|
|
510
520
|
nowVersion: 'running',
|
|
@@ -1401,7 +1411,16 @@ window.__ModuleLoader__.load({
|
|
|
1401
1411
|
const phase = String(job.phase ?? 'starting');
|
|
1402
1412
|
let floor = 3;
|
|
1403
1413
|
let ceiling = 10;
|
|
1404
|
-
if (phase === '
|
|
1414
|
+
if (phase === 'removing') {
|
|
1415
|
+
// The self-removal before the reinstall: brief, but not instant.
|
|
1416
|
+
floor = 6;
|
|
1417
|
+
ceiling = 16;
|
|
1418
|
+
} else if (phase === 'reinstalling') {
|
|
1419
|
+
// The fresh install after the self-removal, before the platform's own
|
|
1420
|
+
// per-registry phases take over and redraw the band.
|
|
1421
|
+
floor = 14;
|
|
1422
|
+
ceiling = 24;
|
|
1423
|
+
} else if (phase === 'installing') {
|
|
1405
1424
|
// One band per registry tried, so a fallback to a second source is
|
|
1406
1425
|
// visible as progress instead of as a bar going backwards.
|
|
1407
1426
|
const index = Math.max(1, Number(job.attempt?.index ?? 1) || 1);
|
|
@@ -1431,10 +1450,13 @@ window.__ModuleLoader__.load({
|
|
|
1431
1450
|
*/
|
|
1432
1451
|
function progressHeadline(job, t) {
|
|
1433
1452
|
if (job === null || job === undefined) return '';
|
|
1453
|
+
if (job.handedOff === true) return t('updateHandedOff');
|
|
1434
1454
|
if (job.done === true) {
|
|
1435
1455
|
return typeof job.error === 'string' && job.error !== '' ? t('updateFailedTitle') : t('updateDoneTitle');
|
|
1436
1456
|
}
|
|
1437
1457
|
const phase = String(job.phase ?? 'starting');
|
|
1458
|
+
if (phase === 'removing') return t('updateRemoving');
|
|
1459
|
+
if (phase === 'reinstalling') return t('updateReinstalling');
|
|
1438
1460
|
if (phase === 'installing') {
|
|
1439
1461
|
const attempt = job.attempt;
|
|
1440
1462
|
const total = Number(attempt?.total ?? 1) || 1;
|
|
@@ -1495,6 +1517,7 @@ window.__ModuleLoader__.load({
|
|
|
1495
1517
|
.replace('{s}', String(seconds));
|
|
1496
1518
|
}
|
|
1497
1519
|
if (typeof job?.error === 'string' && job.error !== '') return t('updateStripFailed');
|
|
1520
|
+
if (job?.handedOff === true) return t('updateHandedOff');
|
|
1498
1521
|
return t('updateStripDone').replace('{v}', String(job?.result?.version ?? job?.version ?? ''));
|
|
1499
1522
|
}
|
|
1500
1523
|
|
|
@@ -1547,7 +1570,17 @@ window.__ModuleLoader__.load({
|
|
|
1547
1570
|
{ style: { ...L.dialog, maxWidth: '480px', width: '100%' } },
|
|
1548
1571
|
h('span', { style: L.rowTitle }, t('updateProgressTitle')),
|
|
1549
1572
|
h('div', { style: L.progressTrack }, h('div', { style: fill })),
|
|
1550
|
-
h(
|
|
1573
|
+
h(
|
|
1574
|
+
'span',
|
|
1575
|
+
{ style: failed ? L.error : L.name },
|
|
1576
|
+
failed
|
|
1577
|
+
? String(job.error)
|
|
1578
|
+
: job.handedOff === true
|
|
1579
|
+
? t('updateHandedOff')
|
|
1580
|
+
: done
|
|
1581
|
+
? progressOutcome(job, t)
|
|
1582
|
+
: progressHeadline(job, t),
|
|
1583
|
+
),
|
|
1551
1584
|
note === '' ? null : h('span', { style: L.meta }, note),
|
|
1552
1585
|
exit === '' ? null : h('span', { style: L.meta }, exit),
|
|
1553
1586
|
log === ''
|
|
@@ -2362,8 +2395,10 @@ window.__ModuleLoader__.load({
|
|
|
2362
2395
|
),
|
|
2363
2396
|
// No preset, no switch: a default has to name something the
|
|
2364
2397
|
// registry holds, and this team is not registered yet (the sync
|
|
2365
|
-
// is in flight, or it failed).
|
|
2366
|
-
|
|
2398
|
+
// is in flight, or it failed). The disabled switch says it on
|
|
2399
|
+
// its own — an inline hint used to sit here and squeezed the
|
|
2400
|
+
// row out of shape on long copy, so it is gone; the refusal
|
|
2401
|
+
// stays said, in the editor, if someone forces the write.
|
|
2367
2402
|
h('span', { style: L.push }),
|
|
2368
2403
|
known
|
|
2369
2404
|
? h(SwitchBox, {
|
|
@@ -3356,7 +3391,23 @@ window.__ModuleLoader__.load({
|
|
|
3356
3391
|
const poll = () => {
|
|
3357
3392
|
post({ op: 'updateProgress', requestId })
|
|
3358
3393
|
.then((payload) => {
|
|
3359
|
-
if (!alive
|
|
3394
|
+
if (!alive) return;
|
|
3395
|
+
if (payload.idle === true) {
|
|
3396
|
+
/*
|
|
3397
|
+
* The host no longer knows this job. That is the two-step
|
|
3398
|
+
* update succeeding the loud way: the removal plus reinstall
|
|
3399
|
+
* ended in an HMR reload, the module that owned the job is
|
|
3400
|
+
* gone, and the fresh module answering polls has never heard
|
|
3401
|
+
* of this requestId. Report it as a hand-off to the restart
|
|
3402
|
+
* instead of letting the bar sit on its last phase forever.
|
|
3403
|
+
*/
|
|
3404
|
+
setJob((prev) =>
|
|
3405
|
+
prev !== null && prev !== undefined && prev.done !== true
|
|
3406
|
+
? { ...prev, done: true, handedOff: true, error: null }
|
|
3407
|
+
: prev,
|
|
3408
|
+
);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3360
3411
|
setJob((prev) => (prev === null || prev === undefined ? prev : { ...prev, ...payload }));
|
|
3361
3412
|
})
|
|
3362
3413
|
.catch(() => {
|
|
@@ -4114,9 +4165,32 @@ window.__ModuleLoader__.load({
|
|
|
4114
4165
|
async function checkUpdate() {
|
|
4115
4166
|
setBusy(true);
|
|
4116
4167
|
try {
|
|
4117
|
-
|
|
4168
|
+
const payload = await post({ op: 'updateCheck' });
|
|
4169
|
+
setUpdate(payload);
|
|
4170
|
+
/*
|
|
4171
|
+
* The banner re-renders from `update`, but a silent re-render is not
|
|
4172
|
+
* an answer: the person pressed a button and gets told, out loud,
|
|
4173
|
+
* which of the three outcomes happened — newer version, already
|
|
4174
|
+
* current, or the registry could not be reached.
|
|
4175
|
+
*/
|
|
4176
|
+
if (payload?.updateAvailable === true) {
|
|
4177
|
+
setNote({
|
|
4178
|
+
kind: 'ok',
|
|
4179
|
+
text: t('updateBanner')
|
|
4180
|
+
.replace('{latest}', payload.latest ?? '')
|
|
4181
|
+
.replace('{installed}', payload.installed ?? payload.running ?? ''),
|
|
4182
|
+
});
|
|
4183
|
+
} else if (typeof payload?.error === 'string' && payload.error !== '') {
|
|
4184
|
+
setNote({ kind: 'error', text: `${t('updateCheckFailed')}:${payload.error}` });
|
|
4185
|
+
} else {
|
|
4186
|
+
setNote({
|
|
4187
|
+
kind: 'ok',
|
|
4188
|
+
text: t('updateUpToDate').replace('{installed}', payload?.installed ?? payload?.running ?? ''),
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4118
4191
|
} catch (failure) {
|
|
4119
4192
|
setUpdate({ error: String(failure?.message ?? failure) });
|
|
4193
|
+
setNote({ kind: 'error', text: `${t('updateCheckFailed')}:${failure?.message ?? failure}` });
|
|
4120
4194
|
} finally {
|
|
4121
4195
|
setBusy(false);
|
|
4122
4196
|
}
|
package/index.js
CHANGED
|
@@ -40,12 +40,31 @@
|
|
|
40
40
|
*
|
|
41
41
|
* `updateApply` is what makes a published fix reachable without a shell: the
|
|
42
42
|
* panel posts it, `latestVersion` asks the registry, and the install goes
|
|
43
|
-
* through the very service the Plugins page uses
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
43
|
+
* through the very service the Plugins page uses. Three layers, tried in
|
|
44
|
+
* order:
|
|
45
|
+
*
|
|
46
|
+
* 1. TARBALL (updates of an installed package): the new version's tarball URL
|
|
47
|
+
* is the one spec shape the manager's HMR precheck cannot match by name —
|
|
48
|
+
* its package name is only known after pnpm runs — so the install reaches
|
|
49
|
+
* pnpm over the installed copy, and the HMR re-composition replaces the
|
|
50
|
+
* running plugin in place. No removal, no restart in the success path.
|
|
51
|
+
* This also rewrites the profile's saved dependency from a version range
|
|
52
|
+
* to the exact URL; the next update's URL differs again, so nothing is
|
|
53
|
+
* ever refused as already-installed on this road twice.
|
|
54
|
+
* 2. NAME (fresh installs, and the two-step's second half): the plain
|
|
55
|
+
* `name@version` spec keeps a fresh install's saved dependency a clean
|
|
56
|
+
* range.
|
|
57
|
+
* 3. TWO-STEP (fallback if the tarball road is somehow refused with
|
|
58
|
+
* `already-installed`): remove the running bundle, then install the new
|
|
59
|
+
* version from the same fiber — a resolved promise never interrupts the
|
|
60
|
+
* fiber that awaited it. Whether the fiber survives its own removal is
|
|
61
|
+
* what real-machine use finds out; every failure is caught and reported
|
|
62
|
+
* with the manual remove-then-install on the Plugins page as the last
|
|
63
|
+
* resort.
|
|
64
|
+
*
|
|
65
|
+
* The personas directory and `teams.json` live outside the package, so no
|
|
66
|
+
* path through any layer — success, half-done, or failure — touches user
|
|
67
|
+
* data.
|
|
49
68
|
*
|
|
50
69
|
* No configuration is required: `resolveDir` picks the directory from the row
|
|
51
70
|
* config, then from `<home>/personas`, then — on a machine that does not have it
|
|
@@ -228,6 +247,50 @@ async function latestVersion(registry) {
|
|
|
228
247
|
return { version: latest, registry: base };
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The tarball URL of a published version.
|
|
252
|
+
*
|
|
253
|
+
* This is the one spec shape the manager's HMR precheck CANNOT refuse with
|
|
254
|
+
* `already-installed`: the precheck matches specs it can name before pnpm
|
|
255
|
+
* runs — a registry name, a local directory, or the exact git/tarball spec the
|
|
256
|
+
* dependency was saved with — and a fresh tarball URL names a package only
|
|
257
|
+
* after pnpm has fetched it. Installing version N+1 from its own tarball
|
|
258
|
+
* therefore reaches pnpm over an installed version N, and the HMR
|
|
259
|
+
* re-composition replaces the running plugin in place (the documented
|
|
260
|
+
* behavior). The abbreviated packument names the URL authoritatively
|
|
261
|
+
* (`dist.tarball`), so one small GET asks the registry; a registry that will
|
|
262
|
+
* not answer falls back to the npm tarball path convention, which every
|
|
263
|
+
* registry this plugin can be pointed at follows
|
|
264
|
+
* (`<base>/<name>/-/<name>-<version>.tgz`). Constructing never throws, so this
|
|
265
|
+
* helper always returns a usable URL.
|
|
266
|
+
* @param {string} [registry] registry base URL; the row config may override it
|
|
267
|
+
* @param {string} version exact published version
|
|
268
|
+
* @returns {Promise<string>}
|
|
269
|
+
*/
|
|
270
|
+
async function tarballUrlOf(registry, version) {
|
|
271
|
+
const base = String(registry ?? "").trim() || DEFAULT_REGISTRY;
|
|
272
|
+
const construct = () =>
|
|
273
|
+
new URL(
|
|
274
|
+
`${PACKAGE_NAME}/-/${PACKAGE_NAME}-${version}.tgz`,
|
|
275
|
+
base.endsWith("/") ? base : `${base}/`,
|
|
276
|
+
).href;
|
|
277
|
+
if (typeof fetch !== "function") return construct();
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(PACKAGE_NAME, base.endsWith("/") ? base : `${base}/`)
|
|
280
|
+
.href;
|
|
281
|
+
const response = await fetch(url, {
|
|
282
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
283
|
+
signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) return construct();
|
|
286
|
+
const meta = await response.json();
|
|
287
|
+
const tarball = meta?.versions?.[version]?.dist?.tarball;
|
|
288
|
+
return typeof tarball === "string" && tarball !== "" ? tarball : construct();
|
|
289
|
+
} catch {
|
|
290
|
+
return construct();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
/**
|
|
232
295
|
* The shared skeleton: a reserved name in the persona directory that is never a
|
|
233
296
|
* wife. It is the frame a `template:`-using persona is poured into, with
|
|
@@ -2459,6 +2522,32 @@ export function apply(ctx, config = {}) {
|
|
|
2459
2522
|
owned.set(id, { dispose, rev: registrationRev(skeleton, body) });
|
|
2460
2523
|
}
|
|
2461
2524
|
|
|
2525
|
+
/**
|
|
2526
|
+
* Does this roster row carry OUR fingerprint?
|
|
2527
|
+
*
|
|
2528
|
+
* Both persona presets and team presets are registered through
|
|
2529
|
+
* `buildDefinition`, whose `plugins` rows include `@deepseek-ai/dsh-persona`
|
|
2530
|
+
* rewritten with `PERSONA_PLACEHOLDER` as the prefix — a combination nothing
|
|
2531
|
+
* else registers. A roster row matching it is this plugin's own previous
|
|
2532
|
+
* instance, not a foreign owner.
|
|
2533
|
+
*
|
|
2534
|
+
* The shape of `agentPresets.list()` rows is NOT promised to include the
|
|
2535
|
+
* registered `plugins` array; if it does not, this answers `false` and the
|
|
2536
|
+
* scan reports the conflict exactly as it did before — the fingerprint is
|
|
2537
|
+
* only ever evidence FOR a reclaim, never against a row.
|
|
2538
|
+
* @param {unknown} row - one entry of `ctx.agentPresets.list()`
|
|
2539
|
+
* @returns {boolean}
|
|
2540
|
+
*/
|
|
2541
|
+
function isOwnPresetShape(row) {
|
|
2542
|
+
const plugins = row?.plugins;
|
|
2543
|
+
if (!Array.isArray(plugins)) return false;
|
|
2544
|
+
return plugins.some(
|
|
2545
|
+
(plugin) =>
|
|
2546
|
+
plugin?.name === "@deepseek-ai/dsh-persona" &&
|
|
2547
|
+
plugin?.config?.prefix === PERSONA_PLACEHOLDER,
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2462
2551
|
/**
|
|
2463
2552
|
* Register every file that is not already taken by this or another owner, and
|
|
2464
2553
|
* RE-register the ones whose text changed since they were registered.
|
|
@@ -2476,9 +2565,10 @@ export function apply(ctx, config = {}) {
|
|
|
2476
2565
|
* entry would be a lie that survives a fix.
|
|
2477
2566
|
*/
|
|
2478
2567
|
foreign.clear();
|
|
2479
|
-
const
|
|
2568
|
+
const roster = new Map();
|
|
2480
2569
|
try {
|
|
2481
|
-
for (const preset of await ctx.agentPresets.list())
|
|
2570
|
+
for (const preset of await ctx.agentPresets.list())
|
|
2571
|
+
roster.set(preset.id, preset);
|
|
2482
2572
|
} catch (error) {
|
|
2483
2573
|
return {
|
|
2484
2574
|
added: [],
|
|
@@ -2526,14 +2616,40 @@ export function apply(ctx, config = {}) {
|
|
|
2526
2616
|
}
|
|
2527
2617
|
continue;
|
|
2528
2618
|
}
|
|
2529
|
-
if (
|
|
2619
|
+
if (roster.has(item.id)) {
|
|
2530
2620
|
/*
|
|
2531
|
-
* `
|
|
2621
|
+
* `roster` counts OUR rows too, and an id that is mid-registration is
|
|
2532
2622
|
* still ours. Calling that a foreign conflict is how the panel ended up
|
|
2533
2623
|
* accusing a wife of stealing her own id; a read that lands here simply
|
|
2534
2624
|
* has nothing to do.
|
|
2535
2625
|
*/
|
|
2536
2626
|
if (registering.has(item.id)) continue;
|
|
2627
|
+
/*
|
|
2628
|
+
* Ghost reclaim. Within one DSH process, an uninstall leaves the
|
|
2629
|
+
* preset table holding this plugin's old entries (observed on a
|
|
2630
|
+
* remove-then-reinstall with HMR: every row read 「被别的所有者占用」
|
|
2631
|
+
* until a restart cleared the table). Such a ghost is not another
|
|
2632
|
+
* owner — it is OUR previous instance, and it carries our fingerprint:
|
|
2633
|
+
* only this plugin registers presets whose plugins rows contain
|
|
2634
|
+
* `@deepseek-ai/dsh-persona` with our placeholder as the prefix.
|
|
2635
|
+
* Re-registering over it is therefore reclaiming, not stealing — and
|
|
2636
|
+
* if the registry refuses the duplicate, the catch falls through to
|
|
2637
|
+
* the same foreign report as before, so the attempt cannot make
|
|
2638
|
+
* anything worse.
|
|
2639
|
+
*/
|
|
2640
|
+
if (isOwnPresetShape(roster.get(item.id))) {
|
|
2641
|
+
holdRegistration(item.id);
|
|
2642
|
+
try {
|
|
2643
|
+
await registerOne(item.id, parsed.meta, parsed.body);
|
|
2644
|
+
added.push(item.id);
|
|
2645
|
+
continue;
|
|
2646
|
+
} catch {
|
|
2647
|
+
// The registry refused the overwrite: report the conflict below,
|
|
2648
|
+
// exactly as if no reclaim had been attempted.
|
|
2649
|
+
} finally {
|
|
2650
|
+
releaseRegistration(item.id);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2537
2653
|
foreign.add(item.id);
|
|
2538
2654
|
skipped.push({ id: item.id, reason: "已经有人注册了这个 id" });
|
|
2539
2655
|
continue;
|
|
@@ -4295,20 +4411,20 @@ export function apply(ctx, config = {}) {
|
|
|
4295
4411
|
* Install the newest published version, through the same service the Plugins
|
|
4296
4412
|
* page uses.
|
|
4297
4413
|
*
|
|
4298
|
-
* The exact version is passed, never a range.
|
|
4299
|
-
*
|
|
4300
|
-
*
|
|
4301
|
-
*
|
|
4302
|
-
*
|
|
4303
|
-
*
|
|
4304
|
-
*
|
|
4305
|
-
*
|
|
4306
|
-
* first
|
|
4307
|
-
* `
|
|
4308
|
-
*
|
|
4309
|
-
*
|
|
4310
|
-
*
|
|
4311
|
-
*
|
|
4414
|
+
* The exact version is passed, never a range. The spec it installs FROM
|
|
4415
|
+
* depends on what is already on disk (see the comment at the choice below):
|
|
4416
|
+
* an update goes through the new version's tarball URL — the spec shape the
|
|
4417
|
+
* HMR precheck cannot refuse — and a fresh install keeps the plain
|
|
4418
|
+
* `name@version`, whose install rewrites the profile's declared range.
|
|
4419
|
+
*
|
|
4420
|
+
* `minimumReleaseAge`: a registry-name install is subject to the 24h
|
|
4421
|
+
* freshness policy and must be recorded in the workspace's
|
|
4422
|
+
* `minimumReleaseAgeExclude` first, or the lockfile check refuses outright
|
|
4423
|
+
* with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`. A tarball URL install
|
|
4424
|
+
* fetches the package file directly and plausibly never consults publish
|
|
4425
|
+
* times — expected but NOT verified against the manager's pnpm invocation,
|
|
4426
|
+
* so the failure, should it come, is surfaced verbatim rather than promised
|
|
4427
|
+
* away.
|
|
4312
4428
|
*
|
|
4313
4429
|
* Returns as soon as the install is STARTED, not when it finishes, and hands
|
|
4314
4430
|
* back the `requestId` that makes the platform publish progress for it. The
|
|
@@ -4375,6 +4491,25 @@ export function apply(ctx, config = {}) {
|
|
|
4375
4491
|
},
|
|
4376
4492
|
};
|
|
4377
4493
|
}
|
|
4494
|
+
/*
|
|
4495
|
+
* Which spec to install from, decided by what is already on disk.
|
|
4496
|
+
*
|
|
4497
|
+
* An UPDATE of an installed package installs from the new version's
|
|
4498
|
+
* tarball URL: that is the one spec shape the HMR precheck cannot match by
|
|
4499
|
+
* name, so it reaches pnpm over the installed copy and the re-composition
|
|
4500
|
+
* replaces the running plugin in place — no removal, no fiber gamble. It
|
|
4501
|
+
* also rewrites the profile's saved dependency from a version range to the
|
|
4502
|
+
* exact URL, which is deliberate: the next update's URL differs again, so
|
|
4503
|
+
* no future install is ever refused as already-installed.
|
|
4504
|
+
*
|
|
4505
|
+
* A FRESH install (nothing readable on disk) keeps the registry name:
|
|
4506
|
+
* there is nothing installed for the precheck to refuse, and the saved
|
|
4507
|
+
* spec stays a plain dependency. If that judgement is wrong — the copy on
|
|
4508
|
+
* disk was unreadable, not absent — the name spec is refused with
|
|
4509
|
+
* `already-installed` and the two-step below takes over.
|
|
4510
|
+
*/
|
|
4511
|
+
let spec = `${PACKAGE_NAME}@${version}`;
|
|
4512
|
+
if (before !== null) spec = await tarballUrlOf(config.registry, version);
|
|
4378
4513
|
/*
|
|
4379
4514
|
* The id is minted here, or accepted from the panel when it sent one. It
|
|
4380
4515
|
* exists because the platform publishes progress ONLY for an installation
|
|
@@ -4411,25 +4546,92 @@ export function apply(ctx, config = {}) {
|
|
|
4411
4546
|
*/
|
|
4412
4547
|
void (async () => {
|
|
4413
4548
|
try {
|
|
4414
|
-
const outcome = await manager.installBundle(
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
);
|
|
4418
|
-
job.done = true;
|
|
4549
|
+
const outcome = await manager.installBundle(spec, {
|
|
4550
|
+
enabled: true,
|
|
4551
|
+
requestId,
|
|
4552
|
+
});
|
|
4419
4553
|
if (outcome?.application === "failed") {
|
|
4554
|
+
/*
|
|
4555
|
+
* The tarball spec should never be refused with `already-installed`
|
|
4556
|
+
* (its package name is only known after pnpm runs), and a fresh
|
|
4557
|
+
* install has nothing installed to refuse. Landing here anyway
|
|
4558
|
+
* means the manager named the installed package from the spec
|
|
4559
|
+
* after all, or the disk judgement above was wrong — either way the
|
|
4560
|
+
* documented cross-version path is remove-then-install, and the
|
|
4561
|
+
* fiber that awaited the removal is never interrupted by the
|
|
4562
|
+
* resolution itself, so this fallback performs the two-step right
|
|
4563
|
+
* here: remove, then install the new version from the same fiber
|
|
4564
|
+
* under its plain registry name. Whether the host lets the fiber
|
|
4565
|
+
* survive its own removal is what real-machine use finds out; every
|
|
4566
|
+
* way it can die is caught below and reported with the manual path
|
|
4567
|
+
* as the last resort. Nothing here can touch user data — personas
|
|
4568
|
+
* and teams.json are outside the package.
|
|
4569
|
+
*/
|
|
4570
|
+
const code = String(outcome.error?.code ?? outcome.error?.message ?? "");
|
|
4571
|
+
if (code.includes("already-installed")) {
|
|
4572
|
+
ctx.logger?.warn?.(
|
|
4573
|
+
`dsh-waifu-agents: installBundle refused ${version} (already-installed); performing remove-then-install.`,
|
|
4574
|
+
);
|
|
4575
|
+
job.phase = "removing";
|
|
4576
|
+
try {
|
|
4577
|
+
await manager.removeBundle(PACKAGE_NAME);
|
|
4578
|
+
} catch (removeFailure) {
|
|
4579
|
+
job.done = true;
|
|
4580
|
+
job.error = `卸载旧版本失败(${removeFailure?.message ?? removeFailure}),已安装的版本没有变动。请到「插件」页手动卸载后重新安装 ${version}。`;
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4583
|
+
/*
|
|
4584
|
+
* Still running — the fiber survived its own removal. The spec no
|
|
4585
|
+
* longer names an installed package, so the precheck lets the new
|
|
4586
|
+
* version through to pnpm.
|
|
4587
|
+
*/
|
|
4588
|
+
job.phase = "reinstalling";
|
|
4589
|
+
let retry = null;
|
|
4590
|
+
try {
|
|
4591
|
+
retry = await manager.installBundle(`${PACKAGE_NAME}@${version}`, {
|
|
4592
|
+
enabled: true,
|
|
4593
|
+
requestId,
|
|
4594
|
+
});
|
|
4595
|
+
} catch (installFailure) {
|
|
4596
|
+
job.done = true;
|
|
4597
|
+
job.error = `旧版本已卸载,但安装 ${version} 失败:${installFailure?.message ?? installFailure}。请到「插件」页重新安装——人设目录(personas)和团队配置(teams.json)不在包里,不受影响。`;
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
if (retry?.application === "failed") {
|
|
4601
|
+
job.done = true;
|
|
4602
|
+
job.error = `旧版本已卸载,但安装 ${version} 失败:${retry.error?.message ?? retry.error?.code ?? JSON.stringify(retry.error)}。请到「插件」页重新安装——人设目录(personas)和团队配置(teams.json)不在包里,不受影响。`;
|
|
4603
|
+
if (job.log === "") job.log = String(retry?.packageResult?.output ?? "");
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
job.done = true;
|
|
4607
|
+
job.result = {
|
|
4608
|
+
ok: true,
|
|
4609
|
+
version,
|
|
4610
|
+
previous: before,
|
|
4611
|
+
application: retry?.application ?? null,
|
|
4612
|
+
changed: retry?.changed === true,
|
|
4613
|
+
installed: readOwnVersion(),
|
|
4614
|
+
log: String(retry?.packageResult?.output ?? ""),
|
|
4615
|
+
twoStep: true,
|
|
4616
|
+
};
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
job.done = true;
|
|
4420
4620
|
job.error = `安装失败:${outcome.error?.message ?? outcome.error?.code ?? JSON.stringify(outcome.error)}`;
|
|
4421
4621
|
// The manager's own transcript is the useful half of this failure,
|
|
4422
4622
|
// and it is only here if the streaming feed never spoke.
|
|
4423
4623
|
if (job.log === "") job.log = String(outcome?.packageResult?.output ?? "");
|
|
4424
4624
|
return;
|
|
4425
4625
|
}
|
|
4626
|
+
job.done = true;
|
|
4426
4627
|
job.result = {
|
|
4427
4628
|
ok: true,
|
|
4428
4629
|
version,
|
|
4429
4630
|
previous: before,
|
|
4430
|
-
//
|
|
4431
|
-
//
|
|
4432
|
-
// rather than a promise this code cannot
|
|
4631
|
+
// Without HMR this is `restart-required`; with HMR a fresh install
|
|
4632
|
+
// (the two-step above included) may genuinely apply. The manager's
|
|
4633
|
+
// own word is passed through rather than a promise this code cannot
|
|
4634
|
+
// keep.
|
|
4433
4635
|
application: outcome?.application ?? null,
|
|
4434
4636
|
changed: outcome?.changed === true,
|
|
4435
4637
|
installed: readOwnVersion(),
|
|
@@ -5076,16 +5278,117 @@ export function apply(ctx, config = {}) {
|
|
|
5076
5278
|
return { ok: true, order: ids, changed, reloaded: reloaded.reloaded ?? [] };
|
|
5077
5279
|
}
|
|
5078
5280
|
|
|
5281
|
+
/**
|
|
5282
|
+
* Remove THIS package's entries from `minimumReleaseAgeExclude`.
|
|
5283
|
+
*
|
|
5284
|
+
* The list lives in a profile's `pnpm-workspace.yaml` — a file pnpm owns —
|
|
5285
|
+
* but every entry naming `dsh-waifu-agents` exists BECAUSE of this plugin:
|
|
5286
|
+
* pnpm wrote it to let our install past the 24h freshness policy. Deleting
|
|
5287
|
+
* our own lines is not reaching into another owner's file any more than
|
|
5288
|
+
* unregistering our presets is; the entries that name other packages, and
|
|
5289
|
+
* every other byte of the file, pass through untouched.
|
|
5290
|
+
*
|
|
5291
|
+
* The profile name is only visible when `DSH_PROFILE_DIR` reaches this
|
|
5292
|
+
* process; without it every profile is scanned, which is safe precisely
|
|
5293
|
+
* because the filter matches our name and nothing else. A profile that
|
|
5294
|
+
* never installed this plugin has no matching line and is left identical —
|
|
5295
|
+
* the write happens only when a line was actually removed.
|
|
5296
|
+
* @returns {{ cleaned: string[], failed: string[] }}
|
|
5297
|
+
*/
|
|
5298
|
+
function cleanReleaseAgeExcludes() {
|
|
5299
|
+
const cleaned = [];
|
|
5300
|
+
const failed = [];
|
|
5301
|
+
const profileDir = process.env.DSH_PROFILE_DIR
|
|
5302
|
+
? path.resolve(process.env.DSH_PROFILE_DIR)
|
|
5303
|
+
: null;
|
|
5304
|
+
let candidates;
|
|
5305
|
+
if (profileDir !== null) {
|
|
5306
|
+
candidates = [path.join(profileDir, "pnpm-workspace.yaml")];
|
|
5307
|
+
} else {
|
|
5308
|
+
const profiles = path.join(homeFromEnv(process.env).home, "profiles");
|
|
5309
|
+
try {
|
|
5310
|
+
candidates = fs
|
|
5311
|
+
.readdirSync(profiles, { withFileTypes: true })
|
|
5312
|
+
.filter((row) => row.isDirectory())
|
|
5313
|
+
.map((row) => path.join(profiles, row.name, "pnpm-workspace.yaml"));
|
|
5314
|
+
} catch {
|
|
5315
|
+
candidates = [];
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
for (const full of candidates) {
|
|
5319
|
+
let raw;
|
|
5320
|
+
try {
|
|
5321
|
+
raw = fs.readFileSync(full, "utf8");
|
|
5322
|
+
} catch {
|
|
5323
|
+
continue; // no such file — a profile without pnpm state, nothing to do
|
|
5324
|
+
}
|
|
5325
|
+
const lines = raw.split("\n");
|
|
5326
|
+
const keep = [];
|
|
5327
|
+
let changed = false;
|
|
5328
|
+
for (let at = 0; at < lines.length; ) {
|
|
5329
|
+
const line = lines[at];
|
|
5330
|
+
if (!/^minimumReleaseAgeExclude\s*:/.test(line)) {
|
|
5331
|
+
keep.push(line);
|
|
5332
|
+
at += 1;
|
|
5333
|
+
continue;
|
|
5334
|
+
}
|
|
5335
|
+
// Flow form `key: [a, b]`: filter inside the brackets, drop the key
|
|
5336
|
+
// when nothing of ours was in it (then the line is reproduced as-is).
|
|
5337
|
+
const flow = /^minimumReleaseAgeExclude\s*:\s*\[(.*)\]\s*\r?$/.exec(line);
|
|
5338
|
+
if (flow !== null) {
|
|
5339
|
+
const items = flow[1]
|
|
5340
|
+
.split(",")
|
|
5341
|
+
.filter((item) => item.trim() !== "");
|
|
5342
|
+
const rest = items.filter((item) => !item.includes(PACKAGE_NAME));
|
|
5343
|
+
if (rest.length === items.length) {
|
|
5344
|
+
keep.push(line);
|
|
5345
|
+
} else {
|
|
5346
|
+
changed = true;
|
|
5347
|
+
if (rest.length > 0)
|
|
5348
|
+
keep.push(`minimumReleaseAgeExclude: [${rest.join(", ")}]`);
|
|
5349
|
+
}
|
|
5350
|
+
at += 1;
|
|
5351
|
+
continue;
|
|
5352
|
+
}
|
|
5353
|
+
// Block form: gather the list items that follow.
|
|
5354
|
+
const items = [];
|
|
5355
|
+
let cursor = at + 1;
|
|
5356
|
+
while (cursor < lines.length && /^\s+-\s/.test(lines[cursor])) {
|
|
5357
|
+
items.push(lines[cursor]);
|
|
5358
|
+
cursor += 1;
|
|
5359
|
+
}
|
|
5360
|
+
const rest = items.filter((item) => !item.includes(PACKAGE_NAME));
|
|
5361
|
+
if (rest.length === items.length) {
|
|
5362
|
+
keep.push(line, ...items);
|
|
5363
|
+
} else {
|
|
5364
|
+
changed = true;
|
|
5365
|
+
if (rest.length > 0) keep.push(line, ...rest);
|
|
5366
|
+
// zero left: the key goes too — an empty list is noise
|
|
5367
|
+
}
|
|
5368
|
+
at = cursor;
|
|
5369
|
+
}
|
|
5370
|
+
if (!changed) continue;
|
|
5371
|
+
try {
|
|
5372
|
+
writeAtomic(full, keep.join("\n"));
|
|
5373
|
+
cleaned.push(full);
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
failed.push(`${full}:${error?.message ?? error}`);
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
return { cleaned, failed };
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5079
5381
|
/**
|
|
5080
5382
|
* Undo everything this plugin put outside its own package.
|
|
5081
5383
|
*
|
|
5082
5384
|
* Only three things are ours and outlive an uninstall: the per-session
|
|
5083
5385
|
* override store, the `selectedDefault` line this panel writes into the
|
|
5084
5386
|
* agent-preset registry's config, and — only when explicitly asked — the
|
|
5085
|
-
* persona directory itself.
|
|
5086
|
-
*
|
|
5087
|
-
*
|
|
5088
|
-
*
|
|
5387
|
+
* persona directory itself. The freshness allow-list lines naming this
|
|
5388
|
+
* package are ours too (pnpm wrote them for OUR install), and are stripped.
|
|
5389
|
+
* Everything else an install leaves behind belongs to
|
|
5390
|
+
* the platform (the plugin-manager's operation logs, pnpm's store index),
|
|
5391
|
+
* so this op names them instead of pretending to clean them.
|
|
5089
5392
|
* @param {{ dir?: boolean }} request
|
|
5090
5393
|
*/
|
|
5091
5394
|
async function purge(request) {
|
|
@@ -5102,6 +5405,14 @@ export function apply(ctx, config = {}) {
|
|
|
5102
5405
|
}
|
|
5103
5406
|
}
|
|
5104
5407
|
overrides = {};
|
|
5408
|
+
try {
|
|
5409
|
+
const excludes = cleanReleaseAgeExcludes();
|
|
5410
|
+
for (const full of excludes.cleaned)
|
|
5411
|
+
done.push(`清掉放行记录 ${full} 里属于本包的条目`);
|
|
5412
|
+
for (const message of excludes.failed) failed.push(`放行记录:${message}`);
|
|
5413
|
+
} catch (error) {
|
|
5414
|
+
failed.push(`放行记录:${error?.message ?? error}`);
|
|
5415
|
+
}
|
|
5105
5416
|
try {
|
|
5106
5417
|
const configEditor = ctx.get("configEditor");
|
|
5107
5418
|
const entry =
|
|
@@ -5186,7 +5497,6 @@ export function apply(ctx, config = {}) {
|
|
|
5186
5497
|
platform: [
|
|
5187
5498
|
".plugin-manager\\logs\\operation-*(安装日志,平台自己累积)",
|
|
5188
5499
|
"pnpm 内容库索引:需要 pnpm store prune",
|
|
5189
|
-
"pnpm-workspace.yaml 的 minimumReleaseAgeExclude(安装时 pnpm 自己写的放行记录)",
|
|
5190
5500
|
],
|
|
5191
5501
|
};
|
|
5192
5502
|
}
|
package/package.json
CHANGED