dsh-waifu-agents 0.2.2 → 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.
Files changed (3) hide show
  1. package/client.js +34 -5
  2. package/index.js +170 -9
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -259,12 +259,14 @@ window.__ModuleLoader__.load({
259
259
  purgeHint: '把本插件留在文件系统里的东西一次清掉:会话覆盖记录、全局默认人设指向。打开右边开关会连人设目录一起删。',
260
260
  purgeWithDir: '连人设目录一起删',
261
261
  purgeAskTitle: '彻底清理本插件的数据',
262
- purgeAskBody: '会删除:会话覆盖记录、全局默认人设指向。安装日志、pnpm 索引、pnpm-workspace.yaml 里的放行记录属于平台,这里不动它们,清理完会列给你。',
262
+ purgeAskBody: '会删除:会话覆盖记录、全局默认人设指向,以及 pnpm-workspace.yaml 里属于本包的放行记录。安装日志、pnpm 索引属于平台,这里不动它们,清理完会列给你。',
263
263
  purgeAskAck: '我知道这会清掉本插件的数据',
264
264
  updateBanner: '有新版本 {latest}(当前 {installed})',
265
265
  updateGo: '查看',
266
266
  checkUpdate: '检查更新',
267
267
  recheck: '重新检查更新',
268
+ updateUpToDate: '已是最新版本 {installed}',
269
+ updateCheckFailed: '检查更新失败',
268
270
  upToDate: '已是最新版本',
269
271
  canUpdateTo: '可更新到',
270
272
  nowVersion: '当前',
@@ -505,12 +507,14 @@ window.__ModuleLoader__.load({
505
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.',
506
508
  purgeWithDir: 'Also delete the persona directory',
507
509
  purgeAskTitle: 'Purge this plugin\'s data',
508
- purgeAskBody: 'Deletes the session override store and the global-default pointer. The install logs, the pnpm store index and the release-age allow-list entry belong to the platform and are listed afterwards instead of touched.',
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.',
509
511
  purgeAskAck: 'I know this removes the plugin\'s data',
510
512
  updateBanner: 'Version {latest} is out (running {installed})',
511
513
  updateGo: 'Details',
512
514
  checkUpdate: 'Check for updates',
513
515
  recheck: 'Check again',
516
+ updateUpToDate: 'Already on the newest version {installed}',
517
+ updateCheckFailed: 'The update check failed',
514
518
  upToDate: 'Up to date',
515
519
  canUpdateTo: 'Update to',
516
520
  nowVersion: 'running',
@@ -2391,8 +2395,10 @@ window.__ModuleLoader__.load({
2391
2395
  ),
2392
2396
  // No preset, no switch: a default has to name something the
2393
2397
  // registry holds, and this team is not registered yet (the sync
2394
- // is in flight, or it failed). Saying which is `teamNoPreset`.
2395
- noPreset ? h('span', { style: L.hint }, t('teamNoPreset')) : null,
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.
2396
2402
  h('span', { style: L.push }),
2397
2403
  known
2398
2404
  ? h(SwitchBox, {
@@ -4159,9 +4165,32 @@ window.__ModuleLoader__.load({
4159
4165
  async function checkUpdate() {
4160
4166
  setBusy(true);
4161
4167
  try {
4162
- setUpdate(await post({ op: 'updateCheck' }));
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
+ }
4163
4191
  } catch (failure) {
4164
4192
  setUpdate({ error: String(failure?.message ?? failure) });
4193
+ setNote({ kind: 'error', text: `${t('updateCheckFailed')}:${failure?.message ?? failure}` });
4165
4194
  } finally {
4166
4195
  setBusy(false);
4167
4196
  }
package/index.js CHANGED
@@ -2522,6 +2522,32 @@ export function apply(ctx, config = {}) {
2522
2522
  owned.set(id, { dispose, rev: registrationRev(skeleton, body) });
2523
2523
  }
2524
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
+
2525
2551
  /**
2526
2552
  * Register every file that is not already taken by this or another owner, and
2527
2553
  * RE-register the ones whose text changed since they were registered.
@@ -2539,9 +2565,10 @@ export function apply(ctx, config = {}) {
2539
2565
  * entry would be a lie that survives a fix.
2540
2566
  */
2541
2567
  foreign.clear();
2542
- const taken = new Set();
2568
+ const roster = new Map();
2543
2569
  try {
2544
- for (const preset of await ctx.agentPresets.list()) taken.add(preset.id);
2570
+ for (const preset of await ctx.agentPresets.list())
2571
+ roster.set(preset.id, preset);
2545
2572
  } catch (error) {
2546
2573
  return {
2547
2574
  added: [],
@@ -2589,14 +2616,40 @@ export function apply(ctx, config = {}) {
2589
2616
  }
2590
2617
  continue;
2591
2618
  }
2592
- if (taken.has(item.id)) {
2619
+ if (roster.has(item.id)) {
2593
2620
  /*
2594
- * `taken` counts OUR rows too, and an id that is mid-registration is
2621
+ * `roster` counts OUR rows too, and an id that is mid-registration is
2595
2622
  * still ours. Calling that a foreign conflict is how the panel ended up
2596
2623
  * accusing a wife of stealing her own id; a read that lands here simply
2597
2624
  * has nothing to do.
2598
2625
  */
2599
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
+ }
2600
2653
  foreign.add(item.id);
2601
2654
  skipped.push({ id: item.id, reason: "已经有人注册了这个 id" });
2602
2655
  continue;
@@ -5225,16 +5278,117 @@ export function apply(ctx, config = {}) {
5225
5278
  return { ok: true, order: ids, changed, reloaded: reloaded.reloaded ?? [] };
5226
5279
  }
5227
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
+
5228
5381
  /**
5229
5382
  * Undo everything this plugin put outside its own package.
5230
5383
  *
5231
5384
  * Only three things are ours and outlive an uninstall: the per-session
5232
5385
  * override store, the `selectedDefault` line this panel writes into the
5233
5386
  * agent-preset registry's config, and — only when explicitly asked — the
5234
- * persona directory itself. Everything else an install leaves behind belongs to
5235
- * the platform (the plugin-manager's operation logs, pnpm's store index, the
5236
- * release-age allow-list entry it wrote at install time), so this op names them
5237
- * instead of pretending to clean them.
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.
5238
5392
  * @param {{ dir?: boolean }} request
5239
5393
  */
5240
5394
  async function purge(request) {
@@ -5251,6 +5405,14 @@ export function apply(ctx, config = {}) {
5251
5405
  }
5252
5406
  }
5253
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
+ }
5254
5416
  try {
5255
5417
  const configEditor = ctx.get("configEditor");
5256
5418
  const entry =
@@ -5335,7 +5497,6 @@ export function apply(ctx, config = {}) {
5335
5497
  platform: [
5336
5498
  ".plugin-manager\\logs\\operation-*(安装日志,平台自己累积)",
5337
5499
  "pnpm 内容库索引:需要 pnpm store prune",
5338
- "pnpm-workspace.yaml 的 minimumReleaseAgeExclude(安装时 pnpm 自己写的放行记录)",
5339
5500
  ],
5340
5501
  };
5341
5502
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-waifu-agents",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Registers every *.md in a persona directory as an agent preset, and manages them from the Plugins settings page.",
6
6
  "license": "MIT",