dsh-waifu-agents 0.2.2 → 0.2.4
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 +43 -25
- package/client.js +158 -11
- package/index.js +214 -25
- package/package.json +2 -1
- package/team-memory.js +298 -0
- package/team-runtime.js +223 -1
- package/team-simulate.js +13 -1
- package/team-snapshot.js +10 -0
- package/team.js +55 -2
- package/template.json +8 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| **顺序永远对** | 自定义人设一定排在四个内置模式(standard / ptc / minimal / cordis)**下面**,团队预设再排在**所有人设**后面;各自内部顺序可拖拽 |
|
|
16
16
|
| **面板内自升级** | 面板里检查版本 → 确认 → **进度窗**告诉你 pnpm 在干什么 → 装完明确说「重启 DSH 生效」 |
|
|
17
17
|
| **改完当场生效** | 换默认立刻反映到新任务页,连屏幕上那个还没开口的空会话也一起换 |
|
|
18
|
-
| **多人同场(团队模式)** | `/waifu-team <队名>`
|
|
18
|
+
| **多人同场(团队模式)** | `/waifu-team <队名>` 把这场会话交给一组人设,**一次生成、全员轮流开口**——她们接话、反驳、赞同,你就在场。**不接工具**;带**剧情记忆**——记得这个会话之前几轮聊了什么(默认开,可关,见第六节),定位是同场演出而非执行任务 |
|
|
19
19
|
|
|
20
20
|
## 四条常用命令
|
|
21
21
|
|
|
@@ -65,7 +65,7 @@ dsh-waifu-agents
|
|
|
65
65
|
想锁定版本就写精确版本号(升级时**必须**这么写,见第八节):
|
|
66
66
|
|
|
67
67
|
```
|
|
68
|
-
dsh-waifu-agents@0.2.
|
|
68
|
+
dsh-waifu-agents@0.2.4
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
装完**要在卡片上点「启用」**,否则一个人设都不会出现——**安装和启用是两件事**。
|
|
@@ -119,7 +119,7 @@ template: _common.md
|
|
|
119
119
|
- 换地方:路径填进「人设目录」→ 保存,插件会自己重启一次。
|
|
120
120
|
- **目录在插件包内时是只读的**:插件升级会整包替换它,写进去的人设会跟着没。看到这个提示就把目录换到包外。
|
|
121
121
|
- 目录在**你自己的用户目录**里,所以卸载插件**不会删掉任何人设**;重装后原样回来。
|
|
122
|
-
- **插件写在磁盘上的东西都在这个目录里**:你的 `.md`、一份 `overrides.json`(会话覆盖:哪个会话指定了谁、换人前是谁)、一份 `teams.json`(团队定义)、一份 `runs.jsonl
|
|
122
|
+
- **插件写在磁盘上的东西都在这个目录里**:你的 `.md`、一份 `overrides.json`(会话覆盖:哪个会话指定了谁、换人前是谁)、一份 `teams.json`(团队定义)、一份 `runs.jsonl`(团队运行账本,一行一次运行),以及一个 `team-memory/` 子目录(团队剧情记忆,按会话一份文件,超过 7 天没续上的自动清掉)。唯一的例外是目录只读时——`overrides.json` 会退到 `<DSH 主目录>/dsh-waifu-agents.overrides.json`。
|
|
123
123
|
|
|
124
124
|
## 三、面板:人设、团队与设置
|
|
125
125
|
|
|
@@ -165,7 +165,7 @@ template: _common.md
|
|
|
165
165
|
| 已隐藏 | 展开后逐条「取消隐藏」(**单个也会先弹确认窗**);「全部取消隐藏」同样先弹确认窗 |
|
|
166
166
|
| 新会话默认用谁 | 当前默认是谁 + 「关掉(谁都不指定)」;**也可能是一支队**,那时这一行显示「团队:名字」(团队页的开关写的是同一格);默认指不到人时在这一行下面直接报错 |
|
|
167
167
|
| 分页 | 每页 N 条,默认关闭、打开时默认 **10**。**同一个分组不会被拆到两页**;**打开分页会直接落到默认人设所在的那一页**(不是第 1 页);关掉再打开会记住你上次填的数。存在浏览器本地 |
|
|
168
|
-
| 彻底清理 | 清掉会话覆盖记录和全局默认指向;打开右边「连人设目录一起删」,才会动目录里的东西(`.md`、`teams.json`、`runs.jsonl
|
|
168
|
+
| 彻底清理 | 清掉会话覆盖记录和全局默认指向;打开右边「连人设目录一起删」,才会动目录里的东西(`.md`、`teams.json`、`runs.jsonl`、`team-memory/` 一起没)。弹窗勾选后才执行 |
|
|
169
169
|
| 插件版本 | 当前版本 + 更新按钮。确认后原地换成**进度窗**,装完要你自己点「关闭」(见第八节) |
|
|
170
170
|
|
|
171
171
|
## 四、日常操作
|
|
@@ -212,18 +212,29 @@ template: _common.md
|
|
|
212
212
|
| 你看到的 | 她一个人说 | 她们接龙式地聊,你在场 |
|
|
213
213
|
| 退出方式 | `/waifu-change` 再切一位,或 `/waifu-reset` 回到换人之前 | `/waifu-change` 切回单人,或 `/waifu-reset` 回到换队之前 |
|
|
214
214
|
|
|
215
|
-
###
|
|
215
|
+
### 它不做什么、又记得什么(先看这条,能省一次失望)
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
团队模式是**剧场**,不是**工位**。「不接工具」是硬边界,不是暂时没做;「记忆」则相反——它是有的,而且默认开着:
|
|
218
218
|
|
|
219
219
|
| 不做什么 | 为什么 | 你该怎么做 |
|
|
220
220
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
221
|
-
| **不调用任何工具** | 团队请求是「队伍背景 + 成员人设 + 这一句」,**不带工具清单**(`tools` 和 `toolHistory` 都没有)。她们读不到你的文件、跑不了命令、搜不了网页 | 要动手就切回单人:`/waifu-change 名字` |
|
|
222
|
-
| **不记得上一轮** | 每轮只带「系统提示 + 本次输入」,**不带历史**。第二幕的她们不知道第一幕发生了什么 | 连续剧情自己补一句前情,或者改用单人 |
|
|
221
|
+
| **不调用任何工具** | 团队请求是「队伍背景 + 成员人设 + 剧情记忆 + 这一句」,**不带工具清单**(`tools` 和 `toolHistory` 都没有)。她们读不到你的文件、跑不了命令、搜不了网页 | 要动手就切回单人:`/waifu-change 名字` |
|
|
223
222
|
|
|
224
|
-
|
|
223
|
+
这条为什么硬:**整场讨论由一次模型调用生成**,输出必须是 `〔名字〕台词` 这样的分段文本。工具调用返回的是结构化的调用请求、不是文本,而且一次调用只能返回一组、没法归给某一个角色——硬塞进来会把分段协议打散。所以不是「忘了接」,是接了就不能这样演。
|
|
225
224
|
|
|
226
|
-
|
|
225
|
+
### 剧情记忆(这一轮记得之前几轮)
|
|
226
|
+
|
|
227
|
+
每一轮结束后,插件会把这一场的剧情存进一份按会话隔离的记忆文件;下一轮请求把它拼在最前面带给模型。所以**连续聊的剧情是接得上的**——第二幕的她们记得第一幕发生了什么。
|
|
228
|
+
|
|
229
|
+
| 项 | 说明 |
|
|
230
|
+
| -------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
231
|
+
| 记忆归属 | 按「**会话 × 队伍**」存:这一场同这支队越聊越记得;新会话从零开始,不共享。**中途换队不串剧情**——B 队看不到 A 队的场次;给某支队伍**改名**会从新记忆开始,旧文件按保留期自然过期 |
|
|
232
|
+
| 装多少 | 最近的场次**逐字带**(约 4,800 字符封顶),更早的压成一段**梗概**(≤800 字符)——不会无限膨胀 |
|
|
233
|
+
| 保留期 | 超过这么多天没续上的记忆,下次读到时自动清掉、当新会话处理。天数在团队编辑器里**自己填**(像分页的数字框),默认 **7**;填 **0 = 永久保留**,一直留到手动清空为止 |
|
|
234
|
+
| 开关 | 团队编辑器里「**剧情记忆**」开关,**默认开**;关掉这一支队就回到每轮无记忆的旧形态 |
|
|
235
|
+
| 全清 | 团队编辑器里「**强制清空记忆**」:不分保留期、立刻删掉**所有**会话的记忆文件(全量清除、删了找不回来;人设、团队配置、账本不受影响) |
|
|
236
|
+
|
|
237
|
+
代价与好处一起说清:正因为不吃那套工具清单(在单人模式里它占首轮输入的 **82%**),团队依然是三种形态里最便宜的。开记忆后每轮输入不再严格恒定——记忆块有上限(约 3,900 token 封顶),且旧场次转写是稳定前缀、会被供应商的前缀缓存接住,账单上表现为「**准恒定**」:比直线略涨、涨到封顶就走平,绝不会像单人那样按轮次平方增长。
|
|
227
238
|
|
|
228
239
|
一句话:**想让她们聊,用团队;想让 AI 干活,用单人。**
|
|
229
240
|
|
|
@@ -241,7 +252,7 @@ template: _common.md
|
|
|
241
252
|
|
|
242
253
|
> **改名是改名**:点既有队伍的「**编辑**」,把名字改掉再保存,改的是**这一支队**(成员、背景都留着),不会多出一支同名的队。已经绑在这支队上的会话会跟着新名字走,不用重新 `/waifu-team`。
|
|
243
254
|
|
|
244
|
-
>
|
|
255
|
+
> **人数上限**有两层,小的说了算:**全局**在「团队」分段底部的两行——**人数上限**(留空 = 不设限,设了必须是不小于 2 的整数)和**超限怎么办**(「超出就不跑」= 这一轮整队不跑;「超出取前几个」= 只留名单里靠前的几个,其余跳过并点名)——对所有队伍生效;**每一支队伍**还可以在自己的编辑器里填「**本队人数上限**」压过全局(留空 = 跟随全局)。比如日常闲聊队填 2、剧情队填 5,超限时走的都是全局那套「超限怎么办」。
|
|
245
256
|
|
|
246
257
|
> 队伍数据背后存成 `teams.json`(和人设目录里的 `overrides.json` 同目录)、所有会话共用;但你日常只跟面板打交道,**不用手改 JSON**。每个团队同时也是一个预设,会出现在新建会话时的模式列表里,排在**所有人设后面**——选了它,这个会话一出生就在团队模式。
|
|
247
258
|
|
|
@@ -296,7 +307,7 @@ template: _common.md
|
|
|
296
307
|
|
|
297
308
|
**所以要按「刷新」的是这几件事**:手动放进目录的新 `.md`、以及在面板外改过的名字 / 简介 / 分组 / 顺序。面板里保存的不算——它当场就重新注册了;正文和骨架也不算——它们每轮现拼。
|
|
298
309
|
|
|
299
|
-
**用外部编辑器改过同一个文件?**
|
|
310
|
+
**用外部编辑器改过同一个文件?** 面板保存时会带上「我读到的那一版」的指纹;对不上就**拒绝写入**,而不是覆盖对方。重开这一行载入最新内容再合并。写入走「临时文件 + rename」,不会有半截文件——所以不会留下 `.bak` 之类的中间副本。
|
|
300
311
|
|
|
301
312
|
## 八、更新插件
|
|
302
313
|
|
|
@@ -336,10 +347,16 @@ template: _common.md
|
|
|
336
347
|
先按「刷新」;再确认文件确实在「设置 → 目录来源」显示的那个目录里、后缀是 `.md`、结尾不是 `.off`。
|
|
337
348
|
|
|
338
349
|
**某一行带「未注册」或「激活失败」。**
|
|
339
|
-
|
|
350
|
+
三种提示三种含义:
|
|
351
|
+
|
|
352
|
+
- 「未注册 · 没注册上,点刷新重试」——注册这一步没落地(改了正文、刚保存完都在这一档),按一次「刷新」就会重新注册;
|
|
353
|
+
- 「未注册 · **本插件旧实例的残留注册**」——插件文件在运行中被替换(热重载)后,旧实例的注册还占着 id;平台的注册表没有删除接口,刷新清不掉,**重启 DSH 即清除**;
|
|
354
|
+
- 「未注册 · 这个 id 被别的所有者占用了」——真的有别的插件注册了同名 id(很少见),改个名字再刷新。
|
|
355
|
+
|
|
356
|
+
「激活失败:…」冒号后面就是原因。
|
|
340
357
|
|
|
341
358
|
**保存被拒,提示「磁盘上变过」。**
|
|
342
|
-
|
|
359
|
+
文件在你打开这一行之后被别的程序改过。重开这一行载入最新内容,合并后再保存(被拒时什么都不会写进磁盘,原文件原封不动)。
|
|
343
360
|
|
|
344
361
|
**正文里写 `{{char}}` 为什么不让保存?**
|
|
345
362
|
平台的提示词渲染只认 `provider`、`model`、`cwd`,写别的会让整份系统提示词渲染失败——**会话根本起不来**那个级别。面板会拦,Host 还会再拦一次。占位符请用 `[[name]]` / `[[body]]`。
|
|
@@ -351,13 +368,13 @@ template: _common.md
|
|
|
351
368
|
「拖拽排序」**每次打开面板都是关的**(防误操作,而且这个开关不记忆),在「**人设**」工具条打开。开着的时候整行可拖,行内的展开 / 开关 / 导出 / 隐藏会一起锁住——就是为了不让拖动半路变成误点。关掉立刻恢复;重开面板也会回到关闭。
|
|
352
369
|
|
|
353
370
|
**装新版本装不上、或装完还是旧版本。**
|
|
354
|
-
用**精确版本号**装(如 `dsh-waifu-agents@0.2.
|
|
371
|
+
用**精确版本号**装(如 `dsh-waifu-agents@0.2.4`)。装到磁盘 ≠ 正在运行,重启 DSH 才会换。
|
|
355
372
|
|
|
356
373
|
**卸载插件会删掉我的人设吗?**
|
|
357
374
|
不会。人设是普通文件,在「设置 → 目录来源」显示的目录里。
|
|
358
375
|
|
|
359
376
|
**卸载之后,这个插件还会留下什么?**
|
|
360
|
-
插件自己的东西只有**人设目录**一个:你的 `.md`、`overrides.json`(会话覆盖)、`teams.json`(团队定义)、`runs.jsonl
|
|
377
|
+
插件自己的东西只有**人设目录**一个:你的 `.md`、`overrides.json`(会话覆盖)、`teams.json`(团队定义)、`runs.jsonl`(团队运行账本)、`team-memory/`(团队剧情记忆,超过 7 天没续上的本来就会自动清掉)。卸载前到「设置」点「**彻底清理**」,会清掉覆盖记录和全局默认指向;想连目录里的东西一起清,还要打开右边的「**连人设目录一起删**」。
|
|
361
378
|
另外几样是**平台的**,清完会列给你(第一项「彻底清理」会顺手处理,剩下三项插件碰不到):
|
|
362
379
|
|
|
363
380
|
- `selectedDefault`——安装/改默认时写进 profile 的一行,指向本插件的人设(「彻底清理」会顺手清掉它,所以它不算残留);
|
|
@@ -382,28 +399,29 @@ template: _common.md
|
|
|
382
399
|
不会。这些命令只是改会话的人设指向和会话标签,是本地状态操作,**不调用模型**。只有下一句正常的对话才会带新人设——而那句本来就要消耗 token。
|
|
383
400
|
|
|
384
401
|
**三种模式到底谁更省 token?**
|
|
385
|
-
|
|
402
|
+
短答:**第一轮团队最省;聊得越久,单人越贵。团队开记忆后会缓涨,但涨到封顶就走平。**
|
|
386
403
|
|
|
387
404
|

|
|
388
405
|
|
|
389
406
|
| 轮数 | 单人 | 裸平台 | 团队 3 人 | 团队 2 人 |
|
|
390
407
|
| ---: | ---------: | ---------: | --------: | --------: |
|
|
391
408
|
| 1 | 10,040 | 8,081 | 4,328 | 2,484 |
|
|
392
|
-
| 10 | 150,620 | 131,030 |
|
|
393
|
-
| 50 | 1,869,100 | 1,771,150 |
|
|
409
|
+
| 10 | 150,620 | 131,030 | 43,280 | 24,840 |
|
|
410
|
+
| 50 | 1,869,100 | 1,771,150 | 216,400 | 124,200 |
|
|
394
411
|
|
|
395
412
|
差别不在斜率,在**形状**:
|
|
396
413
|
|
|
397
414
|
- **单人 / 裸平台**:每轮把全部历史重发一遍,累计是**二次增长**。第 50 轮时单人 187 万。
|
|
398
|
-
-
|
|
399
|
-
-
|
|
415
|
+
- **团队(无记忆)**:每轮只发「队伍背景 + 成员人设 + 这一句」,累计是**一条直线**。第 50 轮 21.6 万(3 人)/12.4 万(2 人)。
|
|
416
|
+
- **团队(开记忆,默认)**:每轮多带一块记忆——最近场次逐字 + 更早的梗概,**封顶约 3,900 token**。累计线先缓涨、到封顶后走平,**绝不是单人的平方曲线**。上表两组团队数字是记忆功能加入前的实测,现在应视为**下限**:之后每轮最多再加约 3,900 token(实际远小于此——旧场次转写是稳定前缀,供应商的前缀缓存会接住大半)。
|
|
417
|
+
- 所以 50 轮时单人仍约为团队 3 人的 **8 倍以上**;轮次越多,倍数越大。
|
|
400
418
|
|
|
401
|
-
口径说明:只统计**输入**(`inputTokens + cacheReadTokens`),不含回答;单人只有第 1 轮是实测,第 2 轮起按每轮 +1,116
|
|
419
|
+
口径说明:只统计**输入**(`inputTokens + cacheReadTokens`),不含回答;单人只有第 1 轮是实测,第 2 轮起按每轮 +1,116 外推(图上用虚线);缓存命中会压低**账单**,但不改变 token 量,也不改变三条线的形状。另外,一轮若写到技术上限被自动续写,续写请求会把已说内容一起带上,那一轮的输入会高于上表——表里按**单次请求**计。团队两组数字是加入「收束规则」之前实测的,那条规则让系统提示长了约 90 字符(约 +57 token),量级和结论都不变;记忆块的实际占用写在团队运行账本每行的 `memChars` 里,可以自己对着账。
|
|
402
420
|
|
|
403
421
|
反过来记住一句:**想省 token,最大的杠杆是「这一轮要不要工具」,不是删人设、也不是剪骨架**——单人首轮 82% 花在工具清单上。
|
|
404
422
|
|
|
405
423
|
**`teams.json` 写在哪?**
|
|
406
|
-
和人设目录里的 `overrides.json`
|
|
424
|
+
和人设目录里的 `overrides.json` 同一个目录(见第二节「人设放在哪」)。文件开头那两项全局设置,就是面板「团队」分段底部那两行——**人数上限**和**超限怎么办**,对所有队伍生效;每支队伍自己的 `maxMembers`(编辑器里那行「本队人数上限」)填了就压过全局。
|
|
407
425
|
|
|
408
426
|
**团队能帮我写代码 / 改文件吗?**
|
|
409
427
|
分两半:**写代码文字可以,动你的仓库不行。**
|
|
@@ -418,8 +436,8 @@ template: _common.md
|
|
|
418
436
|
| | 真·多 Agent(每个角色一个) | 现在的团队模式 |
|
|
419
437
|
| ---------- | -------------------------------------------------- | --------------------------------------------- |
|
|
420
438
|
| 请求次数 | 每人一次,还得再多一次调度汇总——2 个人一轮至少 3 次 | **通常 1 次**;写长了自动续写,一轮最多 3 次 |
|
|
421
|
-
| 每次带什么 | 每个角色各自的系统提示 + 工具清单 + 全部历史 | 队伍背景 + 成员人设 + 这一句
|
|
422
|
-
| 每轮输入 | 人数 × 轮次,越聊越贵 | 约 2,484(2 人)/4,328(3
|
|
439
|
+
| 每次带什么 | 每个角色各自的系统提示 + 工具清单 + 全部历史 | 队伍背景 + 成员人设 + 剧情记忆(封顶约 3,900 token)+ 这一句 |
|
|
440
|
+
| 每轮输入 | 人数 × 轮次,越聊越贵 | 约 2,484(2 人)/4,328(3 人)起,记忆块封顶后走平,不按轮次平方增长 |
|
|
423
441
|
| 场景连贯 | 靠角色互相转述,第三个人开口时已经转了两手 | 同一次生成,谁接谁是一次安排的 |
|
|
424
442
|
|
|
425
443
|
真正拉开差距的是输入量:单人模式首轮 10,040 token 里,**82% 是工具清单**。真多 agent 版本等于每个成员各背一份工具清单、各背一份历史,再为「谁在调度」多付一次——而她们说的还是同一场闲聊。
|
package/client.js
CHANGED
|
@@ -204,6 +204,7 @@ window.__ModuleLoader__.load({
|
|
|
204
204
|
problems: '这一篇现在没法注册',
|
|
205
205
|
notRegistered: '未注册',
|
|
206
206
|
takenByOther: '这个 id 被别的所有者占用了',
|
|
207
|
+
takenByGhost: '本插件旧实例的残留注册(热重载留下),刷新清不掉——重启 DSH 即清除',
|
|
207
208
|
notRegisteredRetry: '没注册上,点「刷新」重试',
|
|
208
209
|
activationFailed: '激活失败',
|
|
209
210
|
loading: '读取中…',
|
|
@@ -259,12 +260,14 @@ window.__ModuleLoader__.load({
|
|
|
259
260
|
purgeHint: '把本插件留在文件系统里的东西一次清掉:会话覆盖记录、全局默认人设指向。打开右边开关会连人设目录一起删。',
|
|
260
261
|
purgeWithDir: '连人设目录一起删',
|
|
261
262
|
purgeAskTitle: '彻底清理本插件的数据',
|
|
262
|
-
purgeAskBody: '
|
|
263
|
+
purgeAskBody: '会删除:会话覆盖记录、全局默认人设指向,以及 pnpm-workspace.yaml 里属于本包的放行记录。安装日志、pnpm 索引属于平台,这里不动它们,清理完会列给你。',
|
|
263
264
|
purgeAskAck: '我知道这会清掉本插件的数据',
|
|
264
265
|
updateBanner: '有新版本 {latest}(当前 {installed})',
|
|
265
266
|
updateGo: '查看',
|
|
266
267
|
checkUpdate: '检查更新',
|
|
267
268
|
recheck: '重新检查更新',
|
|
269
|
+
updateUpToDate: '已是最新版本 {installed}',
|
|
270
|
+
updateCheckFailed: '检查更新失败',
|
|
268
271
|
upToDate: '已是最新版本',
|
|
269
272
|
canUpdateTo: '可更新到',
|
|
270
273
|
nowVersion: '当前',
|
|
@@ -324,7 +327,6 @@ window.__ModuleLoader__.load({
|
|
|
324
327
|
teamCapTruncate: '超出取前几个',
|
|
325
328
|
teamReload: '重新读文件',
|
|
326
329
|
teamReloaded: '已重新读文件并重新注册',
|
|
327
|
-
teamBindNote: '上线/下线是会话级的,面板改不了它——用 /waifu-team。',
|
|
328
330
|
teamPresetNote: '每个团队也是一个预设,会出现在新建会话时的模式列表里;选了它,这个会话一出生就在团队模式。',
|
|
329
331
|
/* The team row's default switch. Same field as the persona rows
|
|
330
332
|
(`selectedDefault`), so the tooltip has to say 「二选一」 out loud —
|
|
@@ -339,6 +341,18 @@ window.__ModuleLoader__.load({
|
|
|
339
341
|
teamStaleHint: '这些已经选不上,但还占着队伍名额。删掉或者改回目录里有的名字。',
|
|
340
342
|
teamUnitGone: '已失效',
|
|
341
343
|
teamCapPolicyHint: '上限 3 却点到 5 个人时:「超出就不跑」= 这一轮整队不跑;「超出取前几个」= 只留名单里靠前的 3 个,其余跳过并点名。',
|
|
344
|
+
teamMemory: '剧情记忆',
|
|
345
|
+
teamMaxMembers: '本队人数上限',
|
|
346
|
+
teamMaxMembersHint: '只管这一队,压过全局的「人数上限」设置。留空 = 跟随全局;填 ≥ 2 的整数,超限按全局的「超限怎么办」处理。',
|
|
347
|
+
teamMaxMembersGlobal: '跟随全局',
|
|
348
|
+
teamMemoryHint: '这一轮记得之前几轮:最近的场次逐字带、更早的压成梗概。记忆按「会话 × 队伍」存:只在这一场里积累,新会话从零开始,中途换队也不串剧情。保留多久在下面那行自己填(填 0 = 永久保留)。关掉则回到每轮无记忆、成本恒定的旧形态。',
|
|
349
|
+
teamMemoryKeep: '记忆保留(天)',
|
|
350
|
+
teamMemoryKeepHint: '超过这么多天没续上的记忆,下次读到时自动清除、当新会话处理。填 0 = 永久保留,直到用下面的「强制清空」手动删。默认 7。',
|
|
351
|
+
teamMemoryClear: '强制清空记忆',
|
|
352
|
+
teamMemoryClearHint: '不分保留期、立刻删掉所有会话的剧情记忆文件(梗概+场次),删了找不回来。人设、团队配置、账本不受影响。',
|
|
353
|
+
teamMemoryClearBtn: '清空',
|
|
354
|
+
teamMemoryClearAsk: '删除所有会话的团队剧情记忆?这是全量清除,删了找不回来。人设和团队配置不受影响。',
|
|
355
|
+
teamMemoryCleared: '已清空团队记忆({n} 个文件)。',
|
|
342
356
|
teamEditingNew: '新建团队',
|
|
343
357
|
teamEditingOne: '编辑团队',
|
|
344
358
|
},
|
|
@@ -453,6 +467,7 @@ window.__ModuleLoader__.load({
|
|
|
453
467
|
problems: 'She cannot be registered right now',
|
|
454
468
|
notRegistered: 'not registered',
|
|
455
469
|
takenByOther: 'another owner holds this id',
|
|
470
|
+
takenByGhost: "a leftover registration from this plugin's previous instance (hot reload) — refresh cannot clear it; restart DSH",
|
|
456
471
|
notRegisteredRetry: 'registration did not land — press Refresh',
|
|
457
472
|
activationFailed: 'activation failed',
|
|
458
473
|
loading: 'Loading…',
|
|
@@ -505,12 +520,14 @@ window.__ModuleLoader__.load({
|
|
|
505
520
|
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
521
|
purgeWithDir: 'Also delete the persona directory',
|
|
507
522
|
purgeAskTitle: 'Purge this plugin\'s data',
|
|
508
|
-
purgeAskBody: 'Deletes the session override store
|
|
523
|
+
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
524
|
purgeAskAck: 'I know this removes the plugin\'s data',
|
|
510
525
|
updateBanner: 'Version {latest} is out (running {installed})',
|
|
511
526
|
updateGo: 'Details',
|
|
512
527
|
checkUpdate: 'Check for updates',
|
|
513
528
|
recheck: 'Check again',
|
|
529
|
+
updateUpToDate: 'Already on the newest version {installed}',
|
|
530
|
+
updateCheckFailed: 'The update check failed',
|
|
514
531
|
upToDate: 'Up to date',
|
|
515
532
|
canUpdateTo: 'Update to',
|
|
516
533
|
nowVersion: 'running',
|
|
@@ -567,7 +584,6 @@ window.__ModuleLoader__.load({
|
|
|
567
584
|
teamCapTruncate: 'keep the first few',
|
|
568
585
|
teamReload: 're-read the file',
|
|
569
586
|
teamReloaded: 're-read and re-registered',
|
|
570
|
-
teamBindNote: 'going on/off duty is per-session; this panel cannot set it — use /waifu-team.',
|
|
571
587
|
teamPresetNote: 'every team is also a preset, so it shows up in the mode list when a session is created — pick it there and the session is born in team mode.',
|
|
572
588
|
teamDefaultTitle: 'make this team the default for new sessions (this clears the persona default — the platform has one slot)',
|
|
573
589
|
teamDefaultSet: 'Default team is now "{name}"',
|
|
@@ -579,6 +595,18 @@ window.__ModuleLoader__.load({
|
|
|
579
595
|
teamStaleHint: 'these can no longer be picked but still hold a slot. Drop them, or rename them back to something the directory has.',
|
|
580
596
|
teamUnitGone: 'unresolvable',
|
|
581
597
|
teamCapPolicyHint: 'cap 3 but 5 were named: "skip the run" drops the whole team this turn; "keep the first few" takes the first 3 in listed order, skips the rest and names them.',
|
|
598
|
+
teamMemory: 'plot memory',
|
|
599
|
+
teamMaxMembers: 'this team\'s size cap',
|
|
600
|
+
teamMaxMembersHint: 'caps THIS team only, overriding the global "size cap". Blank = follow the global setting; enter an integer ≥ 2. When over the cap, the global "when over the cap" policy applies.',
|
|
601
|
+
teamMaxMembersGlobal: 'follow global',
|
|
602
|
+
teamMemoryHint: 'this round remembers the previous ones: recent rounds verbatim, older ones folded into a summary. Memory is keyed to a session AND a team: it builds up within one session, a new session starts blank, and switching teams mid-session never bleeds plot across. Set how long it is kept in the row below (0 = keep forever). Off returns to the old stateless-per-round mode at constant cost.',
|
|
603
|
+
teamMemoryKeep: 'memory retention (days)',
|
|
604
|
+
teamMemoryKeepHint: 'memory idle for more than this many days is cleared on sight and the session starts fresh. 0 = keep forever, until the force-clear below removes it by hand. Default 7.',
|
|
605
|
+
teamMemoryClear: 'force-clear memory',
|
|
606
|
+
teamMemoryClearHint: 'deletes every session\'s plot-memory files (summary + rounds) right now, regardless of retention — gone for good. Personas, team config and the ledger are untouched.',
|
|
607
|
+
teamMemoryClearBtn: 'clear',
|
|
608
|
+
teamMemoryClearAsk: 'Delete the plot memory of every session? This is a full wipe and cannot be undone. Personas and team config are unaffected.',
|
|
609
|
+
teamMemoryCleared: 'Team memory cleared ({n} files).',
|
|
582
610
|
teamEditingNew: 'new team',
|
|
583
611
|
teamEditingOne: 'editing team',
|
|
584
612
|
},
|
|
@@ -1947,7 +1975,7 @@ window.__ModuleLoader__.load({
|
|
|
1947
1975
|
? h(
|
|
1948
1976
|
'span',
|
|
1949
1977
|
{ style: L.error },
|
|
1950
|
-
`${t('notRegistered')} · ${item.foreign === true ? t('takenByOther') : t('notRegisteredRetry')}`,
|
|
1978
|
+
`${t('notRegistered')} · ${item.ghost === true ? t('takenByGhost') : item.foreign === true ? t('takenByOther') : t('notRegisteredRetry')}`,
|
|
1951
1979
|
)
|
|
1952
1980
|
: null,
|
|
1953
1981
|
broken !== null && broken !== undefined ? h('span', { style: L.error }, `${t('activationFailed')}: ${broken}`) : null,
|
|
@@ -2217,6 +2245,52 @@ window.__ModuleLoader__.load({
|
|
|
2217
2245
|
onChange: (event) => onPatch({ background: event.target.value }),
|
|
2218
2246
|
}),
|
|
2219
2247
|
),
|
|
2248
|
+
/* Per-team size cap. Empty = follow the store-wide setting; a number
|
|
2249
|
+
≥ 2 caps this team only. The parser reads the same field, so the
|
|
2250
|
+
editor and the resolve path cannot disagree about it. */
|
|
2251
|
+
h(
|
|
2252
|
+
SettingRow,
|
|
2253
|
+
{ title: t('teamMaxMembers'), hint: t('teamMaxMembersHint') },
|
|
2254
|
+
h('input', {
|
|
2255
|
+
type: 'number',
|
|
2256
|
+
min: 2,
|
|
2257
|
+
placeholder: t('teamMaxMembersGlobal'),
|
|
2258
|
+
style: { ...L.input, width: '72px' },
|
|
2259
|
+
disabled: busy,
|
|
2260
|
+
value: draft.maxMembers ?? '',
|
|
2261
|
+
onChange: (event) => onPatch({ maxMembers: event.target.value === '' ? null : Number(event.target.value) }),
|
|
2262
|
+
}),
|
|
2263
|
+
),
|
|
2264
|
+
/* Per-team memory switch. Absent means on, and the switch mirrors
|
|
2265
|
+
that: `draft.memory !== false`. Turning it off puts THIS team back
|
|
2266
|
+
on the stateless per-round shape (each round constant-cost); the
|
|
2267
|
+
memory itself is per session and cleared from the toolbar. */
|
|
2268
|
+
h(
|
|
2269
|
+
SettingRow,
|
|
2270
|
+
{ title: t('teamMemory'), hint: t('teamMemoryHint') },
|
|
2271
|
+
h(SwitchBox, {
|
|
2272
|
+
checked: draft.memory !== false,
|
|
2273
|
+
disabled: busy,
|
|
2274
|
+
onChange: (next) => onPatch({ memory: next === true }),
|
|
2275
|
+
}),
|
|
2276
|
+
),
|
|
2277
|
+
/* Retention days, editable like the paging number. Shown only while
|
|
2278
|
+
memory is on — with memory off the number has nothing to govern.
|
|
2279
|
+
`?? 7` covers a brand-new draft that has never been patched. */
|
|
2280
|
+
draft.memory !== false
|
|
2281
|
+
? h(
|
|
2282
|
+
SettingRow,
|
|
2283
|
+
{ title: t('teamMemoryKeep'), hint: t('teamMemoryKeepHint') },
|
|
2284
|
+
h('input', {
|
|
2285
|
+
type: 'number',
|
|
2286
|
+
min: 0,
|
|
2287
|
+
style: { ...L.input, width: '72px' },
|
|
2288
|
+
disabled: busy,
|
|
2289
|
+
value: draft.memoryTtlDays ?? 7,
|
|
2290
|
+
onChange: (event) => onPatch({ memoryTtlDays: Number(event.target.value) || 0 }),
|
|
2291
|
+
}),
|
|
2292
|
+
)
|
|
2293
|
+
: null,
|
|
2220
2294
|
h(
|
|
2221
2295
|
'div',
|
|
2222
2296
|
{ style: L.sectionRow },
|
|
@@ -2391,8 +2465,10 @@ window.__ModuleLoader__.load({
|
|
|
2391
2465
|
),
|
|
2392
2466
|
// No preset, no switch: a default has to name something the
|
|
2393
2467
|
// registry holds, and this team is not registered yet (the sync
|
|
2394
|
-
// is in flight, or it failed).
|
|
2395
|
-
|
|
2468
|
+
// is in flight, or it failed). The disabled switch says it on
|
|
2469
|
+
// its own — an inline hint used to sit here and squeezed the
|
|
2470
|
+
// row out of shape on long copy, so it is gone; the refusal
|
|
2471
|
+
// stays said, in the editor, if someone forces the write.
|
|
2396
2472
|
h('span', { style: L.push }),
|
|
2397
2473
|
known
|
|
2398
2474
|
? h(SwitchBox, {
|
|
@@ -2430,7 +2506,7 @@ window.__ModuleLoader__.load({
|
|
|
2430
2506
|
* what happens past the cap — are drawn as ordinary `SettingRow`s, and the
|
|
2431
2507
|
* directory itself sits between them as content.
|
|
2432
2508
|
*/
|
|
2433
|
-
function TeamTab({ team, t, busy, draft, onOpen, onPatch, onSaveDraft, onCancelDraft, onRemove, onReload, onBatch, onDefault, defaultId, fieldProblem }) {
|
|
2509
|
+
function TeamTab({ team, t, busy, draft, onOpen, onPatch, onSaveDraft, onCancelDraft, onRemove, onReload, onBatch, onClearMemory, onDefault, defaultId, fieldProblem }) {
|
|
2434
2510
|
const [batch, setBatch] = React.useState(null);
|
|
2435
2511
|
const loaded = team !== null && team.ok === true;
|
|
2436
2512
|
const cap = batch === null ? String((loaded ? team.batch.maxTeamSize : null) ?? '') : batch.cap;
|
|
@@ -2488,7 +2564,11 @@ window.__ModuleLoader__.load({
|
|
|
2488
2564
|
onChange: (value) => setBatch({ cap, policy: value }),
|
|
2489
2565
|
}),
|
|
2490
2566
|
),
|
|
2491
|
-
h(
|
|
2567
|
+
h(
|
|
2568
|
+
SettingRow,
|
|
2569
|
+
{ title: t('teamMemoryClear'), hint: t('teamMemoryClearHint') },
|
|
2570
|
+
h(Btn, { variant: 'toolbar', disabled: busy, onClick: onClearMemory }, t('teamMemoryClearBtn')),
|
|
2571
|
+
),
|
|
2492
2572
|
h('span', { style: L.hint }, t('teamPresetNote')),
|
|
2493
2573
|
);
|
|
2494
2574
|
}
|
|
@@ -3276,6 +3356,17 @@ window.__ModuleLoader__.load({
|
|
|
3276
3356
|
/* Carried through the editor untouched unless the user edits it: an
|
|
3277
3357
|
entry opened for a rename must not silently lose its background. */
|
|
3278
3358
|
background: entry.background ?? '',
|
|
3359
|
+
memory: entry.memory !== false,
|
|
3360
|
+
memoryTtlDays:
|
|
3361
|
+
Number.isFinite(Number(entry.memoryTtlDays)) && Number(entry.memoryTtlDays) >= 0
|
|
3362
|
+
? Number(entry.memoryTtlDays)
|
|
3363
|
+
: 7,
|
|
3364
|
+
/* Per-team size cap; null = follow the global setting (the input
|
|
3365
|
+
shows empty). A malformed stored value normalizes away here. */
|
|
3366
|
+
maxMembers:
|
|
3367
|
+
Number.isSafeInteger(Number(entry.maxMembers)) && Number(entry.maxMembers) >= 2
|
|
3368
|
+
? Number(entry.maxMembers)
|
|
3369
|
+
: null,
|
|
3279
3370
|
selection: entry.selection.map((one) => ({ kind: one.kind, ref: one.ref })),
|
|
3280
3371
|
});
|
|
3281
3372
|
}, []);
|
|
@@ -3313,7 +3404,16 @@ window.__ModuleLoader__.load({
|
|
|
3313
3404
|
action: 'save',
|
|
3314
3405
|
name,
|
|
3315
3406
|
original: typeof draft.original === 'string' ? draft.original : '',
|
|
3316
|
-
team: {
|
|
3407
|
+
team: {
|
|
3408
|
+
selection: draft.selection,
|
|
3409
|
+
background: draft.background ?? '',
|
|
3410
|
+
memory: draft.memory !== false,
|
|
3411
|
+
memoryTtlDays: Number(draft.memoryTtlDays) >= 0 && Number.isFinite(Number(draft.memoryTtlDays)) ? Math.floor(Number(draft.memoryTtlDays)) : 7,
|
|
3412
|
+
maxMembers:
|
|
3413
|
+
Number.isSafeInteger(Number(draft.maxMembers)) && Number(draft.maxMembers) >= 2
|
|
3414
|
+
? Math.floor(Number(draft.maxMembers))
|
|
3415
|
+
: null,
|
|
3416
|
+
},
|
|
3317
3417
|
});
|
|
3318
3418
|
if (ok) setTeamDraft(null);
|
|
3319
3419
|
}, [teamDraft, mutateTeam, setNote, t]);
|
|
@@ -3347,6 +3447,29 @@ window.__ModuleLoader__.load({
|
|
|
3347
3447
|
await mutateTeam({ action: 'reload' }, () => t('teamReloaded'));
|
|
3348
3448
|
}, [mutateTeam, t]);
|
|
3349
3449
|
|
|
3450
|
+
/**
|
|
3451
|
+
* The team tab's 「清空团队记忆」.
|
|
3452
|
+
*
|
|
3453
|
+
* It is a whole-directory wipe, not per team and not per session — the
|
|
3454
|
+
* panel does not know session ids, and memory without its session is
|
|
3455
|
+
* unreadable clutter. The confirm window exists for the same reason the
|
|
3456
|
+
* unhide one does: "forget the plot" is one click away from "lose the
|
|
3457
|
+
* plot", and the click must be deliberate.
|
|
3458
|
+
*/
|
|
3459
|
+
const clearTeamMemory = React.useCallback(async () => {
|
|
3460
|
+
const proceed = window.confirm(t('teamMemoryClearAsk'));
|
|
3461
|
+
if (proceed !== true) return;
|
|
3462
|
+
setTeamBusy(true);
|
|
3463
|
+
try {
|
|
3464
|
+
const payload = await post({ op: 'teamMemoryClear' });
|
|
3465
|
+
setNote({ kind: 'ok', text: t('teamMemoryCleared').replace('{n}', String(payload?.removed ?? 0)) });
|
|
3466
|
+
} catch (failure) {
|
|
3467
|
+
setNote({ kind: 'error', text: String(failure?.message ?? failure) });
|
|
3468
|
+
} finally {
|
|
3469
|
+
setTeamBusy(false);
|
|
3470
|
+
}
|
|
3471
|
+
}, [t, setNote]);
|
|
3472
|
+
|
|
3350
3473
|
React.useEffect(() => {
|
|
3351
3474
|
/*
|
|
3352
3475
|
* Retire a stored reorder flag left by an older build. Without this, an
|
|
@@ -4159,9 +4282,32 @@ window.__ModuleLoader__.load({
|
|
|
4159
4282
|
async function checkUpdate() {
|
|
4160
4283
|
setBusy(true);
|
|
4161
4284
|
try {
|
|
4162
|
-
|
|
4285
|
+
const payload = await post({ op: 'updateCheck' });
|
|
4286
|
+
setUpdate(payload);
|
|
4287
|
+
/*
|
|
4288
|
+
* The banner re-renders from `update`, but a silent re-render is not
|
|
4289
|
+
* an answer: the person pressed a button and gets told, out loud,
|
|
4290
|
+
* which of the three outcomes happened — newer version, already
|
|
4291
|
+
* current, or the registry could not be reached.
|
|
4292
|
+
*/
|
|
4293
|
+
if (payload?.updateAvailable === true) {
|
|
4294
|
+
setNote({
|
|
4295
|
+
kind: 'ok',
|
|
4296
|
+
text: t('updateBanner')
|
|
4297
|
+
.replace('{latest}', payload.latest ?? '')
|
|
4298
|
+
.replace('{installed}', payload.installed ?? payload.running ?? ''),
|
|
4299
|
+
});
|
|
4300
|
+
} else if (typeof payload?.error === 'string' && payload.error !== '') {
|
|
4301
|
+
setNote({ kind: 'error', text: `${t('updateCheckFailed')}:${payload.error}` });
|
|
4302
|
+
} else {
|
|
4303
|
+
setNote({
|
|
4304
|
+
kind: 'ok',
|
|
4305
|
+
text: t('updateUpToDate').replace('{installed}', payload?.installed ?? payload?.running ?? ''),
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4163
4308
|
} catch (failure) {
|
|
4164
4309
|
setUpdate({ error: String(failure?.message ?? failure) });
|
|
4310
|
+
setNote({ kind: 'error', text: `${t('updateCheckFailed')}:${failure?.message ?? failure}` });
|
|
4165
4311
|
} finally {
|
|
4166
4312
|
setBusy(false);
|
|
4167
4313
|
}
|
|
@@ -4427,6 +4573,7 @@ window.__ModuleLoader__.load({
|
|
|
4427
4573
|
onRemove: (name) => setDialog({ kind: 'teamRemove', name }),
|
|
4428
4574
|
onReload: reloadTeams,
|
|
4429
4575
|
onBatch: batchTeam,
|
|
4576
|
+
onClearMemory: clearTeamMemory,
|
|
4430
4577
|
/*
|
|
4431
4578
|
* The default switch, and the value it reads.
|
|
4432
4579
|
*
|