dsh-agent-persona 0.1.7 → 0.1.8
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/CHANGELOG.md +55 -0
- package/README.md +15 -11
- package/README_EN.md +17 -12
- package/lib/client.js +50 -17
- package/lib/index.d.ts +15 -9
- package/lib/index.js +401 -103
- package/package.json +11 -7
- package/scripts/generate-remote.mjs +179 -0
- package/src/client.ts +3312 -0
- package/src/endpoints.ts +33 -0
- package/src/index.ts +2229 -0
- package/src/remote.ts +58 -0
- package/src/typert.ts +276 -0
- package/tsconfig.client.json +13 -0
- package/tsconfig.json +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,61 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.1.8] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
这一版几乎全部来自一次逐行读代码的审查:不改功能,修的是「它看起来在做 A,实际在做 B」的地方。
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **人设文件的守卫改成按能力判定,不再按工具名。** 原来维护两张工具名白名单,名单外的工具(`str_replace_editor`、
|
|
14
|
+
`terminal_send`,以及任何后来装上的插件工具)无论参数指向哪里都放行;`apply_patch` 的补丁正文更是完全看不见。
|
|
15
|
+
现在问的是「这次调用会不会写到受保护的文件」:参数里的路径(解析 `~` 与相对路径)、shell 行的重定向目标与写入型
|
|
16
|
+
命令、补丁正文里的 `*** Update File:` 目标,命中主文件、历史文件及其 `.bak-*`/`.tmp-*` 副本就拒。
|
|
17
|
+
- **不再误杀只读的重定向。** `jq . <人设文件> > /tmp/c.json` 以前因为同一行里既有路径又有 `>` 而被拒;现在看的是
|
|
18
|
+
`>` 后面那个目标,写到别处就放行,写到人设文件才拒(`2>/dev/null` 也不再算写入)。
|
|
19
|
+
- **`prefix` 规则不再把隔壁项目算进来。** `raw.startsWith(value)` 让 `/tmp/ws` 匹配到 `/tmp/ws-legacy`;现在两侧
|
|
20
|
+
`resolve` 后按分隔符比较,Windows 上按大小写不敏感比,`exact` 也开始认 `~/`。
|
|
21
|
+
- **正则规则不再能卡死事件循环。** 长度上限 500 字符,编译缓存上限 64 条(原来既无上限也无淘汰)。
|
|
22
|
+
- **局部更新不再毁字段。** `savePersona({ id, name })` 以前会把正文清空、把停用的人设重新启用(缺字段一律取默认值);
|
|
23
|
+
现在每个字段都是「传了才覆盖」。
|
|
24
|
+
- **保存失败不再留下脏内存。** 所有写操作先克隆当前 store 再改,持久化成功后才替换内存与时间戳;此前 `commit`
|
|
25
|
+
不刷新 `storeStamp`,写进去的内容下一次 reload 就被自己丢掉。
|
|
26
|
+
- **版本历史坏掉不再让保存永久失败。** 记录前先校验链能否重放到当前正文,不能就重新以当前正文为基准(自愈),
|
|
27
|
+
而不是抛「版本历史损坏」;历史文件损坏时先复制成 `.bak-<时间戳>` 再重建,不再直接覆盖。
|
|
28
|
+
- **`{"personas": []}` 不再静默吞掉全部版本。** 数组被当成对象接受,版本写到数组属性上,`JSON.stringify` 一序列化就没了。
|
|
29
|
+
- **删除人设会清掉它的历史。** 之前 `personas.history.json` 只增不减。
|
|
30
|
+
- **`duplicatePersona` 的副本保留注入方式。** 复制 `replace` 人设会退化成 `append`。
|
|
31
|
+
- **备份文件名不再同秒覆盖。** 时间戳加到毫秒。
|
|
32
|
+
- **`clipMarkdown` 不再劈开代理对。** 按 UTF-16 长度切片时可能停在半个 emoji 上,现在退一位。
|
|
33
|
+
- **RPC 失败只回 `error.message`。** 以前把 `error.stack` 整段回给浏览器。
|
|
34
|
+
- **配置里的 `tuneProvider` / `tuneModel` 进了 schema。** 代码一直在读它们,README 一直在写它们,只有校验层不认。
|
|
35
|
+
- **设置页保存不再吞掉在途输入。** 保存返回时用服务端数据覆盖草稿,保存期间继续敲的字会消失;现在只有草稿没被改过
|
|
36
|
+
才覆盖,并用 `saveSeq` 丢弃过期响应。
|
|
37
|
+
- **恢复版本、切换开关不再与在途保存打架。** 恢复前取消自动保存定时器并推进 `saveSeq`;版本 diff 请求带令牌,
|
|
38
|
+
过期响应不会覆盖新选择。
|
|
39
|
+
- **关掉设置页不会再丢最后一次编辑。** 卸载时若草稿是脏的且自动保存开着,补一次保存。
|
|
40
|
+
- **`Cmd/Ctrl+S` 不再能重入。** 保存中按快捷键会并发发两个请求。
|
|
41
|
+
- **toast 不再常驻。** `onDone` 每次渲染都是新函数,宿主 `Toast` 的 `useEffect` 依赖它 → 定时器被不断续命。
|
|
42
|
+
- **自动保存不再每次都弹两句话。** 去掉了先报的「正在自动保存…」,只留结果。
|
|
43
|
+
- **偏好同步只发改动的那一项。** 以前发整份 prefs,两个标签页会互相覆盖。
|
|
44
|
+
- **设置页刷新后偏好与存储一致。** `view.prefs` 现在也回填到界面状态,不只是写 localStorage。
|
|
45
|
+
- **导入失败后重新拉取。** replace 模式中途失败会留下删了一半的列表。
|
|
46
|
+
- **开关的无障碍语义。** `role="switch"` + `aria-checked` + `aria-describedby`(说明不再塞进 `aria-label` 变成一句话)。
|
|
47
|
+
- **`injectStyles` 缓存不再指向已移除的节点。**
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `files` 补上 `src`、`scripts/generate-remote.mjs` 与两份 tsconfig:以前 `npm ci` 之后 `prepare` 会用构建产物
|
|
52
|
+
覆盖源码,仓库里「生成产物漂移」的校验因此永远是绿的,而消费者从一个 tarball 里 rebuild 会直接失败。
|
|
53
|
+
- CI 增加矩阵(Node 22 / 24)、`git diff --exit-code` 漂移门,以及一个把 tarball 装进空目录再 import 的冒烟 job。
|
|
54
|
+
- `release.yml` 的手动触发默认 `dry_run: true`(以前默认直接发布)。
|
|
55
|
+
- peer 依赖加上上界 `<0.3.0`:`>=` 表示「任何未来版本都能用」,这不成立。
|
|
56
|
+
|
|
57
|
+
### Known
|
|
58
|
+
|
|
59
|
+
- `exports["./client"]` 仍然没有 `types`:`lib/client.js` 是 esbuild 打成的一整个 IIFE,没有对应的 `.d.ts` 产物。
|
|
60
|
+
补它需要单独的类型入口,留给下一版。
|
|
61
|
+
|
|
7
62
|
## [0.1.7] - 2026-10-23
|
|
8
63
|
|
|
9
64
|
### Fixed
|
package/README.md
CHANGED
|
@@ -145,7 +145,8 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
145
145
|
| 编辑后自动保存 | 关 | 停手约 1 秒自动保存编辑区内容 | 立即;切换到人设规则行(工作区/会话)时**不**自动保存,避免存下半截选择 |
|
|
146
146
|
| 在对话页显示「人设」标签 | 开 | 是否注册对话页那个视图 | 立即(运行时注册/注销) |
|
|
147
147
|
| 注册到 dsh-better-sidebar | 开 | 是否把「人设」页面注册进 dsh-better-sidebar 的右侧栏 | 立即;**没装该插件时这个开关不显示** |
|
|
148
|
-
| 禁止模型改写人设文件 | 开 |
|
|
148
|
+
| 禁止模型改写人设文件 | 开 | 按**能力**判定:任何会写入/删除人设存储(含它的历史文件与备份副本)的工具调用都拒绝,读取、`grep`、把路径写进笔记都不受影响;关掉后不拦 | 立即;对**下一次工具调用**生效 |
|
|
149
|
+
| 保存人设版本历史 | 关 | 每次保存记一条可重放的 diff(最多 200 条,可查看/对比/恢复) | 立即;关掉后不再记新版本,已有历史仍可查看 |
|
|
149
150
|
|
|
150
151
|
偏好存在人设文件的 `prefs` 字段里(不是浏览器),所以它和数据的可见范围一致;浏览器里只留一份镜像,
|
|
151
152
|
供"加载时决定要不要注册视图"用。
|
|
@@ -179,8 +180,9 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
179
180
|
|
|
180
181
|
```
|
|
181
182
|
~/.dsh/dsh-agent-persona/
|
|
182
|
-
├── personas.json
|
|
183
|
-
|
|
183
|
+
├── personas.json # 人设数据 + 偏好,权限 600,原子写入
|
|
184
|
+
├── personas.history.json # 版本历史(开关打开后才出现)
|
|
185
|
+
└── state.json # 加载心跳,每次加载时重写
|
|
184
186
|
```
|
|
185
187
|
|
|
186
188
|
正文里可以写 `{{model}}` 和 `{{cwd}}`,渲染时替换成当前的值;其他 `{{...}}` 会被去掉(未注册的变量会让
|
|
@@ -218,16 +220,18 @@ AI 只产出建议稿,点「采用」或「追加」才会进编辑框,再
|
|
|
218
220
|
## 几句实话
|
|
219
221
|
|
|
220
222
|
- **人设是行为约束,不是安全边界**。它约束模型怎么说,不阻止模型用工具。
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
- 人设文件的防篡改是**能力层面**的判定:插件注册了全局 `tools.guard`,它不看工具叫什么名字,而是看这次调用会不会写到
|
|
224
|
+
受保护的文件 —— 参数里的路径(解析 `~` 与相对路径)、shell 命令里的重定向目标、写入型命令(删、移、原地改、解释器
|
|
225
|
+
`-c`),以及补丁正文里的 `*** Update File:` 目标。受保护的是主文件 `personas.json`、它的历史文件和它们的备份/临时副本。
|
|
226
|
+
- 它仍然只是行为约束,绕过面写在明处:本机 `danger-full-access` 下改插件代码、改 profile 补丁文件都能绕过;脚本里
|
|
227
|
+
运行时拼出来的路径静态看不出;`$VAR` 展开看不出;别名/函数包装也看不出。要更结实得在本机层面做:
|
|
224
228
|
`chflags uchg <人设文件>`(自己改之前先 `chflags nouchg`),或者把 DSH 跑在受限沙箱里、让人设文件属于别的用户。
|
|
225
|
-
-
|
|
229
|
+
- 判定**按单次工具调用**生效:同一条命令里既读又写,写的那部分照样被拒,只读的部分不会被连坐。
|
|
226
230
|
- 多处同时匹配时**只取第一条**,不拼接;规则是线性扫的,几十条没问题,几百条就该重新设计了。
|
|
227
231
|
- 一个 profile 里只装一次。同一个进程挂两个同 id 的行会让组合起不来。
|
|
228
|
-
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(236 条宿主断言 + 187 条客户端断言 + 生成产物漂移校验;
|
|
233
|
+
这个数字由测试自己统计,不会随文件改动而失真),它们保证逻辑与代码形状,**不驱动浏览器** —— 界面行为
|
|
234
|
+
(视图渲染、开关、侧边栏卡片)目前靠人工核对。
|
|
231
235
|
|
|
232
236
|
## 开发
|
|
233
237
|
|
|
@@ -246,7 +250,7 @@ docs/ 架构、开发笔记、设计决策
|
|
|
246
250
|
npm install
|
|
247
251
|
npm run build # generate:remote → tsc → esbuild(产出 lib/)
|
|
248
252
|
npm run typecheck # 两个 tsconfig 都不报错
|
|
249
|
-
npm test #
|
|
253
|
+
npm test # 236 + 187 条断言 + 生成产物漂移校验
|
|
250
254
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # 组合自检,应输出 1
|
|
251
255
|
```
|
|
252
256
|
|
package/README_EN.md
CHANGED
|
@@ -194,8 +194,9 @@ error.
|
|
|
194
194
|
|
|
195
195
|
```
|
|
196
196
|
~/.dsh/dsh-agent-persona/
|
|
197
|
-
├── personas.json
|
|
198
|
-
|
|
197
|
+
├── personas.json # personas + preferences, mode 600, written atomically
|
|
198
|
+
├── personas.history.json # version history (only while the switch is on)
|
|
199
|
+
└── state.json # load heartbeat, rewritten on every load
|
|
199
200
|
```
|
|
200
201
|
|
|
201
202
|
The body may use `{{model}}` and `{{cwd}}`; other `{{...}}` are de-braced (an unregistered variable would make the
|
|
@@ -229,18 +230,22 @@ Agent 人设 page (`保存在 …`), so it is never a mystery which file you are
|
|
|
229
230
|
|
|
230
231
|
- **A persona is a behavioural constraint, not a security boundary.** It shapes what the model says; it does not
|
|
231
232
|
stop tools.
|
|
232
|
-
- Tamper protection is
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
- Tamper protection is decided by **capability, not by tool name**: a global `tools.guard` asks whether this call
|
|
234
|
+
would write a protected file — the paths in its arguments (resolved through `~` and relative paths), redirect
|
|
235
|
+
targets and writing commands in a shell line (delete, move, in-place edit, interpreter `-c`), and the targets
|
|
236
|
+
named by `*** Update File:` markers in a patch body. Protected: `personas.json`, its history file, and their
|
|
237
|
+
backup and temp copies.
|
|
238
|
+
- It is still a behavioural measure, and the bypass surface is stated plainly: on `danger-full-access` you can
|
|
239
|
+
edit the plugin or the profile patch; a path assembled at runtime inside a script, `$VAR` expansion, or an
|
|
240
|
+
alias/function wrapper is invisible to a static read. Harder options live at the machine level: `chflags uchg
|
|
241
|
+
<store>` (run `chflags nouchg` before editing it yourself), or a restricted sandbox with the store owned by
|
|
242
|
+
another user.
|
|
243
|
+
- The decision is per **tool call**: a line that both reads and writes has its writing half refused; the reading
|
|
244
|
+
half is not implicated.
|
|
240
245
|
- Only the **first** match is used — no concatenation. Matching is a linear scan; dozens of rules are fine,
|
|
241
246
|
hundreds want a redesign.
|
|
242
247
|
- Install once per profile: two rows with the same id in one process make the composition fail.
|
|
243
|
-
- **Test boundary**: the suite is unit tests plus source audits (
|
|
248
|
+
- **Test boundary**: the suite is unit tests plus source audits (236 host assertions, 187 client assertions, and a
|
|
244
249
|
generated-artifact drift check; the numbers are counted by the suites themselves, so they cannot fall behind
|
|
245
250
|
the files). It does not drive a browser — UI behaviour is checked by hand today.
|
|
246
251
|
|
|
@@ -261,7 +266,7 @@ docs/ architecture, development notes, design decisions
|
|
|
261
266
|
npm install
|
|
262
267
|
npm run build # generate:remote → tsc → esbuild (writes lib/)
|
|
263
268
|
npm run typecheck # both tsconfigs, no errors
|
|
264
|
-
npm test #
|
|
269
|
+
npm test # 236 + 187 assertions plus the generated-artifact drift check
|
|
265
270
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # composition self-check, expects 1
|
|
266
271
|
```
|
|
267
272
|
|
package/lib/client.js
CHANGED
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
report({ prefs: next, status: prefStatus(patch) });
|
|
247
247
|
syncSidebarModule();
|
|
248
248
|
if (api === void 0 || typeof api.savePrefs !== "function") return next;
|
|
249
|
-
void api.savePrefs(
|
|
249
|
+
void api.savePrefs(patch).then((result) => {
|
|
250
250
|
const view = unwrap === void 0 ? result : unwrap(result);
|
|
251
251
|
const stored = view !== void 0 && view !== null && view.prefs !== void 0 && view.prefs !== null ? writePrefs(view.prefs) : void 0;
|
|
252
252
|
report(stored === void 0 ? { view } : { prefs: { ...next, ...stored }, view });
|
|
@@ -496,6 +496,10 @@
|
|
|
496
496
|
.wsp-switch input:checked + .wsp-switch-track { background: var(--wsp-success); }
|
|
497
497
|
.wsp-switch input:checked + .wsp-switch-track::after { transform: translateX(12px); }
|
|
498
498
|
.wsp-switch input:focus-visible + .wsp-switch-track { outline: 2px solid var(--wsp-accent); outline-offset: 2px; }
|
|
499
|
+
.wsp-sr-only {
|
|
500
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
501
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
502
|
+
}
|
|
499
503
|
.wsp-switch-text { font-size: 11.5px; color: var(--wsp-muted); }
|
|
500
504
|
|
|
501
505
|
/* \u2500\u2500 editor / preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -756,7 +760,8 @@
|
|
|
756
760
|
`;
|
|
757
761
|
let styleElement;
|
|
758
762
|
const injectStyles = () => {
|
|
759
|
-
if (styleElement !== void 0) return styleElement;
|
|
763
|
+
if (styleElement !== void 0 && styleElement.isConnected) return styleElement;
|
|
764
|
+
if (styleElement !== void 0) styleElement.remove();
|
|
760
765
|
styleElement = document.createElement("style");
|
|
761
766
|
styleElement.setAttribute("data-dsh-agent-persona", "");
|
|
762
767
|
styleElement.textContent = CSS;
|
|
@@ -1151,18 +1156,27 @@
|
|
|
1151
1156
|
const Badge = ({ children, tone }) => h("span", {
|
|
1152
1157
|
className: tone === void 0 ? "wsp-badge" : `wsp-badge wsp-badge-${tone}`
|
|
1153
1158
|
}, children);
|
|
1159
|
+
let switchId = 0;
|
|
1154
1160
|
const Switch = ({ checked, disabled, label, hint, onChange }) => {
|
|
1161
|
+
const descriptionId = React.useMemo(() => `wsp-switch-hint-${switchId += 1}`, []);
|
|
1155
1162
|
const control = h("label", { className: "wsp-switch", title: hint === void 0 ? label : void 0 }, [
|
|
1156
1163
|
h("input", {
|
|
1157
1164
|
key: "i",
|
|
1158
1165
|
type: "checkbox",
|
|
1166
|
+
// Role and state, not just appearance: a checkbox announced as "checkbox" next to a track that
|
|
1167
|
+
// says "switch" is two different controls to a screen reader. The hint belongs in the
|
|
1168
|
+
// description, not in the name — a name that reads a whole sentence is unusable in a list.
|
|
1169
|
+
role: "switch",
|
|
1170
|
+
"aria-checked": checked === true,
|
|
1159
1171
|
checked,
|
|
1160
1172
|
disabled,
|
|
1161
|
-
"aria-label":
|
|
1173
|
+
"aria-label": label,
|
|
1174
|
+
"aria-describedby": hint === void 0 ? void 0 : descriptionId,
|
|
1162
1175
|
onChange: (event) => onChange(event.target.checked)
|
|
1163
1176
|
}),
|
|
1164
1177
|
h("span", { key: "t", className: "wsp-switch-track" }),
|
|
1165
|
-
h("span", { key: "l", className: "wsp-switch-text" }, label)
|
|
1178
|
+
h("span", { key: "l", className: "wsp-switch-text" }, label),
|
|
1179
|
+
hint === void 0 ? null : h("span", { key: "d", id: descriptionId, className: "wsp-sr-only" }, hint)
|
|
1166
1180
|
]);
|
|
1167
1181
|
return hint === void 0 ? control : h(Tooltip, { label: hint, side: "top", delayMs: 260, maxWidth: 320 }, control);
|
|
1168
1182
|
};
|
|
@@ -1211,6 +1225,9 @@
|
|
|
1211
1225
|
this.importHighlightRef = React.createRef();
|
|
1212
1226
|
this.importTimer = void 0;
|
|
1213
1227
|
this.statusSeq = 0;
|
|
1228
|
+
this.saveSeq = 0;
|
|
1229
|
+
this.versionSeq = 0;
|
|
1230
|
+
this.clearStatus = () => this.onChange({ status: "" });
|
|
1214
1231
|
this.onChange = (patch) => {
|
|
1215
1232
|
if (!this.mounted) return;
|
|
1216
1233
|
if (patch !== null && typeof patch === "object" && "status" in patch) {
|
|
@@ -1225,6 +1242,7 @@
|
|
|
1225
1242
|
const root = this.rootRef.current;
|
|
1226
1243
|
if (root === null || !root.contains(event.target)) return;
|
|
1227
1244
|
event.preventDefault();
|
|
1245
|
+
if (this.state.busy === "save") return;
|
|
1228
1246
|
void this.save();
|
|
1229
1247
|
};
|
|
1230
1248
|
}
|
|
@@ -1236,6 +1254,11 @@
|
|
|
1236
1254
|
void this.load();
|
|
1237
1255
|
}
|
|
1238
1256
|
componentWillUnmount() {
|
|
1257
|
+
if (this.autosaveTimer !== void 0 && this.isDirty()) {
|
|
1258
|
+
clearTimeout(this.autosaveTimer);
|
|
1259
|
+
this.autosaveTimer = void 0;
|
|
1260
|
+
void this.save();
|
|
1261
|
+
}
|
|
1239
1262
|
clearTimeout(this.autosaveTimer);
|
|
1240
1263
|
clearTimeout(this.importTimer);
|
|
1241
1264
|
this.mounted = false;
|
|
@@ -1285,12 +1308,13 @@
|
|
|
1285
1308
|
this.onChange({ busy: "load", error: "" });
|
|
1286
1309
|
try {
|
|
1287
1310
|
const view = this.unwrap(await api.listPersonas());
|
|
1288
|
-
|
|
1311
|
+
const stored = view?.prefs !== void 0 && view?.prefs !== null ? writePrefs(view.prefs) : void 0;
|
|
1289
1312
|
const wanted = keepOpen === true ? this.state.openId : void 0;
|
|
1290
1313
|
const persona = wanted === void 0 ? void 0 : (view.personas || []).find((item) => item.id === wanted);
|
|
1291
1314
|
this.onChange({
|
|
1292
1315
|
view,
|
|
1293
1316
|
busy: "",
|
|
1317
|
+
...stored === void 0 ? {} : { prefs: { ...this.state.prefs, ...stored } },
|
|
1294
1318
|
openId: persona === void 0 ? void 0 : wanted,
|
|
1295
1319
|
draft: persona === void 0 ? void 0 : this.draftFrom(persona)
|
|
1296
1320
|
});
|
|
@@ -1390,11 +1414,14 @@
|
|
|
1390
1414
|
const api = this.api();
|
|
1391
1415
|
const versions = this.state.versions;
|
|
1392
1416
|
if (api === void 0 || versions === void 0 || index < 0) return;
|
|
1417
|
+
const seq = this.versionSeq += 1;
|
|
1393
1418
|
this.onChange({ versionsBusy: "diff", versionIndex: index, versionsError: "" });
|
|
1394
1419
|
try {
|
|
1395
1420
|
const diff = this.unwrap(await api.versionDiff({ id: versions.id, index }));
|
|
1421
|
+
if (seq !== this.versionSeq) return;
|
|
1396
1422
|
this.onChange({ versionsBusy: "", versionDiff: diff });
|
|
1397
1423
|
} catch (error) {
|
|
1424
|
+
if (seq !== this.versionSeq) return;
|
|
1398
1425
|
this.onChange({ versionsBusy: "", versionDiff: void 0, versionsError: String(error && error.message || error) });
|
|
1399
1426
|
}
|
|
1400
1427
|
}
|
|
@@ -1403,6 +1430,9 @@
|
|
|
1403
1430
|
const api = this.api();
|
|
1404
1431
|
const versions = this.state.versions;
|
|
1405
1432
|
if (api === void 0 || versions === void 0) return;
|
|
1433
|
+
clearTimeout(this.autosaveTimer);
|
|
1434
|
+
this.autosaveTimer = void 0;
|
|
1435
|
+
this.saveSeq += 1;
|
|
1406
1436
|
this.onChange({ versionsBusy: "restore", versionsError: "" });
|
|
1407
1437
|
try {
|
|
1408
1438
|
const view = this.unwrap(await api.restoreVersion({ id: versions.id, index }));
|
|
@@ -1606,7 +1636,6 @@
|
|
|
1606
1636
|
clearTimeout(this.autosaveTimer);
|
|
1607
1637
|
this.autosaveTimer = setTimeout(() => {
|
|
1608
1638
|
this.autosaveTimer = void 0;
|
|
1609
|
-
this.onChange({ status: "\u6B63\u5728\u81EA\u52A8\u4FDD\u5B58\u2026" });
|
|
1610
1639
|
void this.save();
|
|
1611
1640
|
}, 1200);
|
|
1612
1641
|
}
|
|
@@ -1789,6 +1818,7 @@
|
|
|
1789
1818
|
});
|
|
1790
1819
|
} catch (error) {
|
|
1791
1820
|
this.onChange({ transfer: { ...transfer, busy: false, error: `\u5BFC\u5165\u5931\u8D25\uFF1A${String(error && error.message || error)}` } });
|
|
1821
|
+
await this.load(true);
|
|
1792
1822
|
}
|
|
1793
1823
|
}
|
|
1794
1824
|
async pickImportFile(event) {
|
|
@@ -1808,6 +1838,8 @@
|
|
|
1808
1838
|
const api = this.api();
|
|
1809
1839
|
const draft = this.state.draft;
|
|
1810
1840
|
if (api === void 0 || draft === void 0) return;
|
|
1841
|
+
if (this.state.busy === "save") return;
|
|
1842
|
+
const seq = this.saveSeq += 1;
|
|
1811
1843
|
this.onChange({ busy: "save", error: "", status: "" });
|
|
1812
1844
|
try {
|
|
1813
1845
|
const view = this.unwrap(await api.savePersona({
|
|
@@ -1819,14 +1851,13 @@
|
|
|
1819
1851
|
text: draft.text,
|
|
1820
1852
|
targets: draft.targets
|
|
1821
1853
|
}));
|
|
1854
|
+
if (seq !== this.saveSeq) return;
|
|
1822
1855
|
const persona = (view.personas || []).find((item) => item.id === draft.id);
|
|
1823
|
-
this.
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
draft: persona === void 0 ? void 0 : this.draftFrom(persona),
|
|
1827
|
-
status: view.notice ?? "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6761\u6D88\u606F\u5C31\u4F1A\u7528\u4E0A"
|
|
1828
|
-
});
|
|
1856
|
+
const latest = this.state.draft;
|
|
1857
|
+
const draftAfter = latest === draft && persona !== void 0 ? this.draftFrom(persona) : latest;
|
|
1858
|
+
this.onChange({ view, busy: "", draft: draftAfter, status: view.notice ?? "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6761\u6D88\u606F\u5C31\u4F1A\u7528\u4E0A" });
|
|
1829
1859
|
} catch (error) {
|
|
1860
|
+
if (seq !== this.saveSeq) return;
|
|
1830
1861
|
this.onChange({ busy: "", error: String(error && error.message || error) });
|
|
1831
1862
|
}
|
|
1832
1863
|
}
|
|
@@ -2447,7 +2478,6 @@ ${state.proposal.trim()}
|
|
|
2447
2478
|
onClick: () => this.onChange({ filter: "" })
|
|
2448
2479
|
}, "\u6E05\u7A7A")
|
|
2449
2480
|
]);
|
|
2450
|
-
const hiddenNote = null;
|
|
2451
2481
|
const conflictNotice = conflicts.size === 0 ? null : h("div", { className: "wsp-conflict-notice", key: "conflicts" }, [
|
|
2452
2482
|
h(IconWarningOutline16, { key: "i" }),
|
|
2453
2483
|
`${conflicts.size} \u6761\u4EBA\u8BBE\u7684\u8303\u56F4\u53EF\u80FD\u88AB\u4E0A\u9762\u7684\u4EBA\u8BBE\u5148\u547D\u4E2D\uFF08\u9F20\u6807\u60AC\u505C\u5BF9\u5E94\u5361\u7247\u770B\u539F\u56E0\uFF09`
|
|
@@ -2500,7 +2530,7 @@ ${state.proposal.trim()}
|
|
|
2500
2530
|
key: `st-${String(state.statusSeq)}`,
|
|
2501
2531
|
text: state.status,
|
|
2502
2532
|
icon: h(IconCheckOutline14, {}),
|
|
2503
|
-
onDone:
|
|
2533
|
+
onDone: this.clearStatus
|
|
2504
2534
|
});
|
|
2505
2535
|
const history = state.history;
|
|
2506
2536
|
const historyModal = h(Modal, {
|
|
@@ -2605,7 +2635,7 @@ ${state.proposal.trim()}
|
|
|
2605
2635
|
key: "guard",
|
|
2606
2636
|
checked: prefs.guard !== false,
|
|
2607
2637
|
label: "\u7981\u6B62\u6A21\u578B\u6539\u5199\u4EBA\u8BBE\u6587\u4EF6",
|
|
2608
|
-
hint: "\
|
|
2638
|
+
hint: "\u6309\u80FD\u529B\u5224\u5B9A\uFF1A\u4EFB\u4F55\u4F1A\u5199\u5165\u6216\u5220\u9664\u4EBA\u8BBE\u5B58\u50A8\uFF08\u542B\u5B83\u7684\u5386\u53F2\u6587\u4EF6\u4E0E\u5907\u4EFD\u526F\u672C\uFF09\u7684\u5DE5\u5177\u8C03\u7528\u90FD\u4F1A\u88AB\u62D2\uFF0C\u4E0D\u770B\u5DE5\u5177\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1B\u8BFB\u53D6\u3001\u641C\u7D22\u3001\u628A\u8DEF\u5F84\u5199\u8FDB\u7B14\u8BB0\u90FD\u4E0D\u53D7\u5F71\u54CD\u3002\u8FD9\u662F\u884C\u4E3A\u5C42\u9762\u7684\u9650\u5236\uFF0C\u4E0D\u662F\u5B89\u5168\u8FB9\u754C\uFF1A\u672C\u673A\u4E0A\u7684\u5176\u5B83\u7A0B\u5E8F\u4ECD\u53EF\u4FEE\u6539\u3002",
|
|
2609
2639
|
onChange: (next) => this.setPref({ guard: next })
|
|
2610
2640
|
}),
|
|
2611
2641
|
h(Switch, {
|
|
@@ -2797,13 +2827,16 @@ ${state.proposal.trim()}
|
|
|
2797
2827
|
])
|
|
2798
2828
|
])
|
|
2799
2829
|
]));
|
|
2800
|
-
return h("div", rootProps, [header, healthNote, prefsRow, transferRow, searchRow,
|
|
2830
|
+
return h("div", rootProps, [header, healthNote, prefsRow, transferRow, searchRow, conflictNotice, h("div", { className: "wsp-cards", key: "cards" }, cards), status, historyModal, confirmModal, versionsModal, importModal]);
|
|
2801
2831
|
}
|
|
2802
2832
|
}
|
|
2803
2833
|
const inject = ["slots", "remote", "connection"];
|
|
2804
2834
|
function apply(ctx) {
|
|
2805
2835
|
const style = injectStyles();
|
|
2806
|
-
ctx.effect(() => () =>
|
|
2836
|
+
ctx.effect(() => () => {
|
|
2837
|
+
style.remove();
|
|
2838
|
+
if (styleElement === style) styleElement = void 0;
|
|
2839
|
+
}, `${NS}: styles`);
|
|
2807
2840
|
const prefs = readPrefs();
|
|
2808
2841
|
if (prefs.showTab) ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
2809
2842
|
name: "conversation.view",
|
package/lib/index.d.ts
CHANGED
|
@@ -52,11 +52,17 @@ export declare const name = "agent-persona";
|
|
|
52
52
|
* when the row loads, and a wrong value fails loudly instead of being ignored.
|
|
53
53
|
*/
|
|
54
54
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
55
|
-
/** Where the personas live. Empty
|
|
55
|
+
/** Where the personas live. Empty selects the default store under the DSH state directory. */
|
|
56
56
|
storePath: Schema<string, string>;
|
|
57
|
+
/** Provider and model the AI-tuning button calls. Empty means whatever DSH is set to. */
|
|
58
|
+
tuneProvider: Schema<string, string>;
|
|
59
|
+
tuneModel: Schema<string, string>;
|
|
57
60
|
}>, Schemastery.ObjectT<{
|
|
58
|
-
/** Where the personas live. Empty
|
|
61
|
+
/** Where the personas live. Empty selects the default store under the DSH state directory. */
|
|
59
62
|
storePath: Schema<string, string>;
|
|
63
|
+
/** Provider and model the AI-tuning button calls. Empty means whatever DSH is set to. */
|
|
64
|
+
tuneProvider: Schema<string, string>;
|
|
65
|
+
tuneModel: Schema<string, string>;
|
|
60
66
|
}>>;
|
|
61
67
|
export declare const inject: string[];
|
|
62
68
|
/** Persona injection modes: append after the preset persona, or replace it. */
|
|
@@ -92,7 +98,7 @@ export declare const normalizePrefs: (raw: any) => {
|
|
|
92
98
|
/** Opt-in version history: off by default, because it writes a second file on every save. */
|
|
93
99
|
history: boolean;
|
|
94
100
|
};
|
|
95
|
-
/** Filename-safe local timestamp for a store backup. */
|
|
101
|
+
/** Filename-safe local timestamp for a store backup (milliseconds keep same-second backups apart). */
|
|
96
102
|
export declare const backupStamp: (now?: Date) => string;
|
|
97
103
|
/**
|
|
98
104
|
* Copy the store aside before it is overwritten for the first time after a failed load. An exception here
|
|
@@ -101,14 +107,14 @@ export declare const backupStamp: (now?: Date) => string;
|
|
|
101
107
|
export declare const backupStoreFile: (storePath: any, stamp: any) => string;
|
|
102
108
|
/**
|
|
103
109
|
* The store is this plugin's own instruction source, so a session running under a persona must not be able
|
|
104
|
-
* to rewrite or delete that persona. This
|
|
105
|
-
*
|
|
110
|
+
* to rewrite or delete that persona. This guard refuses any call that would write the store (or the history
|
|
111
|
+
* file beside it), decided by what the arguments resolve to rather than by which tools happen to exist.
|
|
106
112
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
113
|
+
* Honest limits, all deliberate: it constrains behaviour rather than enforcing security (a plugin edit, a
|
|
114
|
+
* manual edit, or a path assembled at runtime inside a script still get through), it reads only the strings
|
|
115
|
+
* a tool call carries, and a command line is analysed statically — no shell is run to expand variables.
|
|
110
116
|
*/
|
|
111
|
-
export declare const personaStoreGuardReason: (execution: any, storePath: any) => string;
|
|
117
|
+
export declare const personaStoreGuardReason: (execution: any, storePath: any, cwd: any) => string;
|
|
112
118
|
export declare const sanitizePersona: (text: any, warn?: () => void) => string;
|
|
113
119
|
/**
|
|
114
120
|
* Opt-in per-persona version history.
|