dsh-agent-persona 0.1.8 → 0.1.9
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 +24 -0
- package/README.md +4 -0
- package/README_EN.md +5 -0
- package/lib/client.js +45 -4
- package/lib/endpoints.d.ts +1 -1
- package/lib/endpoints.js +1 -0
- package/lib/index.d.ts +19 -2
- package/lib/index.js +90 -13
- package/lib/remote.js +1 -0
- package/lib/typert.js +17 -0
- package/package.json +1 -1
- package/src/client.ts +44 -3
- package/src/endpoints.ts +1 -0
- package/src/index.ts +87 -11
- package/src/remote.ts +1 -0
- package/src/typert.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ 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.9] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
版本历史从「只能看和恢复」补成「能管理」,并且不再记录重复的正文。
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **可以删除单个版本。** 选中一条,点「删除此版本」(连点两次确认)。删掉中间一条后,它后面的版本会
|
|
14
|
+
按新的前一条重新算 diff,链不会断:剩下的每一条仍然能逐条打开、恢复。删掉最后一条时这条人设的历史
|
|
15
|
+
记录一起清掉。
|
|
16
|
+
- **正文哈希去重。** 每条版本原本就存了 sha256 前 16 位,现在保存时先比对:如果这次要存的内容和已有
|
|
17
|
+
某条版本完全相同(撤销、恢复之后再保存、导入同一份正文都会遇到),就不新增记录,只提示「正文与第 N
|
|
18
|
+
个版本相同,没有新增版本」。列表行和对比窗口头部都显示这个哈希。
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`savePersona({ id, name })` 仍会清空正文。** 0.1.8 声称修好了部分更新,但入口把整个 store 当成
|
|
23
|
+
「已有值」传下去,store 顶层没有 name/text 这些字段,于是没传的正文按默认值覆盖。现在按 id 取到那条
|
|
24
|
+
人设再合并:只改名,正文、范围、启用状态都不动。
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- 新增去重、删除(删中间/删首条/删最后一条/越界)与哈希格式的断言;测试总数 254。
|
|
29
|
+
- 删除后的链式重放、重算后的 `+n/−m` 统计、以及被删版本之后的 diff 都在测试里逐条验证。
|
|
30
|
+
|
|
7
31
|
## [0.1.8] - 2026-10-23
|
|
8
32
|
|
|
9
33
|
这一版几乎全部来自一次逐行读代码的审查:不改功能,修的是「它看起来在做 A,实际在做 B」的地方。
|
package/README.md
CHANGED
|
@@ -111,6 +111,10 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
111
111
|
- **只记正文**:名称、启用状态、适用范围不参与版本,改这些不会产生一条记录。
|
|
112
112
|
- **每条都是 diff**:相等行只记行数,只有真正改动的行才存原文,所以一百条版本也远小于一百份正文。
|
|
113
113
|
- **恢复**:选中任意一条点「恢复到此版本」写回正文;恢复动作本身也记一条,恢复错了还能再回滚。
|
|
114
|
+
- **删除**:选中任意一条点「删除此版本」(连点两次确认)就把这条记录删掉;它后面的版本会重新计算
|
|
115
|
+
diff,仍然可以逐条打开,链不会断。
|
|
116
|
+
- **同一份正文只存一条**:每条版本带正文的 sha256 前 16 位,保存时若和已有版本的内容哈希相同,就不
|
|
117
|
+
再新增记录(撤销、恢复后再保存、导入同样的正文都属于这种情况),列表和对比窗口里都能看到哈希。
|
|
114
118
|
- **链式重放**:第一条版本作为基准快照,之后逐条重放;超过 200 条时把最旧的几条折进基准,
|
|
115
119
|
保留下来的版本仍可逐条打开。每条记录带内容摘要,历史文件被手改坏时重放会报错,不会给出别的正文。
|
|
116
120
|
- 关掉开关只是停止记录,已有历史仍可查看与恢复。
|
package/README_EN.md
CHANGED
|
@@ -121,6 +121,11 @@ Turn on **Save persona version history** in Settings and every save records the
|
|
|
121
121
|
- **Text only**: name, enabled flag and targets are not versioned.
|
|
122
122
|
- **Diffs, not snapshots**: equal runs are stored as line counts; only changed lines keep their text.
|
|
123
123
|
- **Restore**: pick any version and restore it; the restore itself is recorded, so it can be undone too.
|
|
124
|
+
- **Delete**: pick a version and press Delete (twice: the first press arms it). The versions after it are
|
|
125
|
+
re-diffed against its predecessor, so the chain stays replayable.
|
|
126
|
+
- **One row per text**: each record carries the first 16 hex characters of its text's sha256. A save whose
|
|
127
|
+
text hashes to a version that is already stored adds nothing — undo, restore-then-save and importing the
|
|
128
|
+
same body all land on the existing row. The hash is shown in the row and in the diff header.
|
|
124
129
|
- **Chained replay**: the first text is the base snapshot, later versions replay on top of it; past 200
|
|
125
130
|
versions the oldest are folded into the base. A content hash per record makes a hand-edited history fail
|
|
126
131
|
loudly instead of restoring different text.
|
package/lib/client.js
CHANGED
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
method("savePrefs", withInput()),
|
|
48
48
|
method("listVersions", withInput()),
|
|
49
49
|
method("versionDiff", withInput()),
|
|
50
|
-
method("restoreVersion", withInput())
|
|
50
|
+
method("restoreVersion", withInput()),
|
|
51
|
+
method("deleteVersion", withInput())
|
|
51
52
|
]
|
|
52
53
|
};
|
|
53
54
|
|
|
@@ -750,6 +751,7 @@
|
|
|
750
751
|
.wsp-version-title { font-size: 13px; font-weight: 600; }
|
|
751
752
|
.wsp-version-meta { font-size: 11px; opacity: 0.72; font-variant-numeric: tabular-nums; }
|
|
752
753
|
.wsp-version-preview { font-size: 11px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
754
|
+
.wsp-version-hash { font-family: var(--wsp-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 11px; opacity: 0.55; }
|
|
753
755
|
.wsp-versions-diff { display: flex; flex-direction: column; gap: 8px; min-width: 0; overflow: hidden; }
|
|
754
756
|
.wsp-diff-head { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
|
|
755
757
|
.wsp-diff-body { overflow: auto; border: 1px solid var(--wsp-line); border-radius: 8px; padding: 8px 0; background: var(--wsp-line-soft); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.55; }
|
|
@@ -1202,6 +1204,7 @@
|
|
|
1202
1204
|
versionsError: "",
|
|
1203
1205
|
versionDiff: void 0,
|
|
1204
1206
|
versionIndex: void 0,
|
|
1207
|
+
versionConfirm: false,
|
|
1205
1208
|
models: [],
|
|
1206
1209
|
model: "",
|
|
1207
1210
|
tuneOpen: false,
|
|
@@ -1400,7 +1403,7 @@
|
|
|
1400
1403
|
this.onChange({ versionsError: "\u8FD9\u4E2A\u7248\u672C\u8FD8\u6CA1\u6709\u7248\u672C\u5386\u53F2\u63A5\u53E3\uFF1A\u8BF7\u66F4\u65B0\u63D2\u4EF6\u540E\u91CD\u542F dsh web" });
|
|
1401
1404
|
return;
|
|
1402
1405
|
}
|
|
1403
|
-
this.onChange({ versionsBusy: "list", versionsError: "", versions: { id }, versionDiff: void 0, versionIndex: void 0 });
|
|
1406
|
+
this.onChange({ versionsBusy: "list", versionsError: "", versions: { id }, versionDiff: void 0, versionIndex: void 0, versionConfirm: false });
|
|
1404
1407
|
try {
|
|
1405
1408
|
const data = this.unwrap(await api.listVersions({ id }));
|
|
1406
1409
|
this.onChange({ versionsBusy: "", versions: data });
|
|
@@ -1415,7 +1418,7 @@
|
|
|
1415
1418
|
const versions = this.state.versions;
|
|
1416
1419
|
if (api === void 0 || versions === void 0 || index < 0) return;
|
|
1417
1420
|
const seq = this.versionSeq += 1;
|
|
1418
|
-
this.onChange({ versionsBusy: "diff", versionIndex: index, versionsError: "" });
|
|
1421
|
+
this.onChange({ versionsBusy: "diff", versionIndex: index, versionsError: "", versionConfirm: false });
|
|
1419
1422
|
try {
|
|
1420
1423
|
const diff = this.unwrap(await api.versionDiff({ id: versions.id, index }));
|
|
1421
1424
|
if (seq !== this.versionSeq) return;
|
|
@@ -1448,6 +1451,36 @@
|
|
|
1448
1451
|
this.onChange({ versionsBusy: "", versionsError: String(error && error.message || error) });
|
|
1449
1452
|
}
|
|
1450
1453
|
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Delete one stored version. Two clicks: the first arms the footer button, the second deletes — a
|
|
1456
|
+
* history entry is the only copy of a text that may not exist anywhere else.
|
|
1457
|
+
*/
|
|
1458
|
+
async deleteVersion(index) {
|
|
1459
|
+
const api = this.api();
|
|
1460
|
+
const versions = this.state.versions;
|
|
1461
|
+
if (api === void 0 || versions === void 0) return;
|
|
1462
|
+
if (this.state.versionConfirm !== true) {
|
|
1463
|
+
this.onChange({ versionConfirm: true });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
clearTimeout(this.autosaveTimer);
|
|
1467
|
+
this.autosaveTimer = void 0;
|
|
1468
|
+
this.saveSeq += 1;
|
|
1469
|
+
this.onChange({ versionsBusy: "delete", versionsError: "", versionConfirm: false });
|
|
1470
|
+
try {
|
|
1471
|
+
const view = this.unwrap(await api.deleteVersion({ id: versions.id, index }));
|
|
1472
|
+
const found = (view?.personas ?? []).find((item) => item.id === versions.id);
|
|
1473
|
+
this.onChange({
|
|
1474
|
+
view,
|
|
1475
|
+
versionsBusy: "",
|
|
1476
|
+
status: view?.notice ?? "\u5DF2\u5220\u9664\u8BE5\u7248\u672C",
|
|
1477
|
+
draft: this.state.openId === versions.id && found !== void 0 ? this.draftFrom(found) : this.state.draft
|
|
1478
|
+
});
|
|
1479
|
+
await this.openVersions(versions.id);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
this.onChange({ versionsBusy: "", versionsError: String(error && error.message || error) });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1451
1484
|
/** Read a session's recent messages so the user can recognise it. */
|
|
1452
1485
|
async openHistory(sessionId) {
|
|
1453
1486
|
const api = this.api();
|
|
@@ -2689,6 +2722,13 @@ ${state.proposal.trim()}
|
|
|
2689
2722
|
size: "sm",
|
|
2690
2723
|
onClick: () => this.onChange({ versions: void 0, versionDiff: void 0, versionsError: "", versionIndex: void 0 })
|
|
2691
2724
|
}, "\u5173\u95ED"),
|
|
2725
|
+
h(Button, {
|
|
2726
|
+
key: "delete",
|
|
2727
|
+
variant: "outline",
|
|
2728
|
+
size: "sm",
|
|
2729
|
+
disabled: state.versionsBusy !== "" || state.versionIndex === void 0 || diffState === void 0,
|
|
2730
|
+
onClick: () => void this.deleteVersion(state.versionIndex)
|
|
2731
|
+
}, state.versionsBusy === "delete" ? "\u5220\u9664\u4E2D\u2026" : state.versionConfirm === true ? "\u518D\u70B9\u4E00\u6B21\uFF1A\u5220\u9664" : "\u5220\u9664\u6B64\u7248\u672C"),
|
|
2692
2732
|
h(Button, {
|
|
2693
2733
|
key: "restore",
|
|
2694
2734
|
variant: "primary",
|
|
@@ -2710,13 +2750,14 @@ ${state.proposal.trim()}
|
|
|
2710
2750
|
{ className: "wsp-version-meta", key: "m" },
|
|
2711
2751
|
`${String(version.at).replace("T", " ").slice(5, 19)}\u3000+${version.added} / \u2212${version.removed}`
|
|
2712
2752
|
),
|
|
2753
|
+
h("span", { className: "wsp-version-hash", key: "h" }, `${version.hash ?? ""}`),
|
|
2713
2754
|
h("span", { className: "wsp-version-preview", key: "p" }, version.preview || "\uFF08\u65E0\u53EF\u663E\u793A\u7684\u6539\u52A8\u884C\uFF09")
|
|
2714
2755
|
]))),
|
|
2715
2756
|
h("div", { className: "wsp-versions-diff", key: "d" }, diffState === void 0 ? h("div", { className: "wsp-card-note" }, "\u5DE6\u4FA7\u9009\u4E00\u4E2A\u7248\u672C\uFF0C\u8FD9\u91CC\u663E\u793A\u5B83\u76F8\u5BF9\u4E0A\u4E00\u7248\u7684\u6539\u52A8\u3002") : [
|
|
2716
2757
|
h(
|
|
2717
2758
|
"div",
|
|
2718
2759
|
{ className: "wsp-diff-head", key: "h" },
|
|
2719
|
-
`${String(diffState.at).replace("T", " ").slice(0, 19)}\u3000\xB7\u3000${diffState.action === "restore" ? "\u6062\u590D" : diffState.action === "create" ? "\u65B0\u5EFA" : "\u4FEE\u6539"}\u3000\xB7\u3000+${diffState.added} / \u2212${diffState.removed}`
|
|
2760
|
+
`${String(diffState.at).replace("T", " ").slice(0, 19)}\u3000\xB7\u3000${diffState.action === "restore" ? "\u6062\u590D" : diffState.action === "create" ? "\u65B0\u5EFA" : "\u4FEE\u6539"}\u3000\xB7\u3000+${diffState.added} / \u2212${diffState.removed}\u3000\xB7\u3000${diffState.hash ?? ""}`
|
|
2720
2761
|
),
|
|
2721
2762
|
h("div", { className: "wsp-diff-body", key: "b" }, diffLines)
|
|
2722
2763
|
])
|
package/lib/endpoints.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export declare const RPC_PACKAGE = "dsh-agent-persona";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const RPC_SERVICE = "agentPersona";
|
|
13
13
|
export declare const RPC_CHANNEL = "/agent-persona";
|
|
14
|
-
export declare const RPC_ENDPOINTS: readonly ['listPersonas', 'savePersona', 'deletePersona', 'movePersona', 'duplicatePersona', 'reorderPersona', 'listTargets', 'sessionHistory', 'sessionPrompt', 'tunePersona', 'listModels', 'savePrefs', 'listVersions', 'versionDiff', 'restoreVersion'];
|
|
14
|
+
export declare const RPC_ENDPOINTS: readonly ['listPersonas', 'savePersona', 'deletePersona', 'movePersona', 'duplicatePersona', 'reorderPersona', 'listTargets', 'sessionHistory', 'sessionPrompt', 'tunePersona', 'listModels', 'savePrefs', 'listVersions', 'versionDiff', 'restoreVersion', 'deleteVersion'];
|
|
15
15
|
/** The three reads that take no argument. Everything else receives one JSON object. */
|
|
16
16
|
export declare const RPC_NO_INPUT: readonly ['listPersonas', 'listTargets', 'listModels'];
|
package/lib/endpoints.js
CHANGED
|
@@ -27,6 +27,7 @@ export const RPC_ENDPOINTS = [
|
|
|
27
27
|
'listVersions',
|
|
28
28
|
'versionDiff',
|
|
29
29
|
'restoreVersion',
|
|
30
|
+
'deleteVersion',
|
|
30
31
|
];
|
|
31
32
|
/** The three reads that take no argument. Everything else receives one JSON object. */
|
|
32
33
|
export const RPC_NO_INPUT = ['listPersonas', 'listTargets', 'listModels'];
|
package/lib/index.d.ts
CHANGED
|
@@ -150,11 +150,28 @@ export declare const diffStats: (ops: any) => {
|
|
|
150
150
|
};
|
|
151
151
|
/** Current text of one stored version (index 0 is the oldest kept version). */
|
|
152
152
|
export declare const versionTextAt: (entryState: any, index: any) => any;
|
|
153
|
+
/** Content hash of a version's text. Two versions that hash the same are the same text. */
|
|
154
|
+
export declare const hashText: (text: any) => any;
|
|
153
155
|
/**
|
|
154
156
|
* Record one new version. `before` is the text the persona had, `after` the text it now has.
|
|
155
|
-
*
|
|
157
|
+
*
|
|
158
|
+
* A save whose text already exists in the chain is not a new version: edits that walk back to an earlier
|
|
159
|
+
* text (undo, restore then save, an import of the same body) would otherwise pile up entries that differ
|
|
160
|
+
* only in their timestamp. Returns `{ recorded: false, duplicateOf }` in that case.
|
|
161
|
+
*/
|
|
162
|
+
export declare const recordVersion: (history: any, personaId: any, before: any, after: any, action: any, now?: string) => {
|
|
163
|
+
recorded: boolean;
|
|
164
|
+
duplicateOf?: undefined;
|
|
165
|
+
} | {
|
|
166
|
+
recorded: boolean;
|
|
167
|
+
duplicateOf: any;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Drop one version and re-diff what is left, so every remaining version still replays from the base.
|
|
171
|
+
* The dropped entry's text stops being reachable; the others keep their timestamp, action and text.
|
|
172
|
+
* Returns how many versions remain.
|
|
156
173
|
*/
|
|
157
|
-
export declare const
|
|
174
|
+
export declare const deleteVersionAt: (state: any, index: any) => any;
|
|
158
175
|
/**
|
|
159
176
|
* Does one target claim this subject?
|
|
160
177
|
* @param target - normalized target record.
|
package/lib/index.js
CHANGED
|
@@ -723,15 +723,20 @@ const chainMatches = (state, beforeText) => {
|
|
|
723
723
|
return false;
|
|
724
724
|
}
|
|
725
725
|
};
|
|
726
|
+
/** Content hash of a version's text. Two versions that hash the same are the same text. */
|
|
727
|
+
export const hashText = (text) => createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex').slice(0, 16);
|
|
726
728
|
/**
|
|
727
729
|
* Record one new version. `before` is the text the persona had, `after` the text it now has.
|
|
728
|
-
*
|
|
730
|
+
*
|
|
731
|
+
* A save whose text already exists in the chain is not a new version: edits that walk back to an earlier
|
|
732
|
+
* text (undo, restore then save, an import of the same body) would otherwise pile up entries that differ
|
|
733
|
+
* only in their timestamp. Returns `{ recorded: false, duplicateOf }` in that case.
|
|
729
734
|
*/
|
|
730
735
|
export const recordVersion = (history, personaId, before, after, action, now = new Date().toISOString()) => {
|
|
731
736
|
const beforeText = String(before ?? '');
|
|
732
737
|
const afterText = String(after ?? '');
|
|
733
738
|
if (beforeText === afterText)
|
|
734
|
-
return false;
|
|
739
|
+
return { recorded: false };
|
|
735
740
|
const ops = diffLines(beforeText, afterText);
|
|
736
741
|
const stats = diffStats(ops);
|
|
737
742
|
let state = history.personas[personaId];
|
|
@@ -746,13 +751,18 @@ export const recordVersion = (history, personaId, before, after, action, now = n
|
|
|
746
751
|
state.baseAt = now;
|
|
747
752
|
state.versions = [];
|
|
748
753
|
}
|
|
754
|
+
const hash = hashText(afterText);
|
|
755
|
+
// Checked after the re-base above: a re-based chain has no versions left to collide with.
|
|
756
|
+
const duplicateOf = state.versions.findIndex((version) => version?.hash === hash);
|
|
757
|
+
if (duplicateOf !== -1)
|
|
758
|
+
return { recorded: false, duplicateOf };
|
|
749
759
|
state.versions.push({
|
|
750
760
|
at: now,
|
|
751
761
|
action,
|
|
752
762
|
ops,
|
|
753
763
|
added: stats.added,
|
|
754
764
|
removed: stats.removed,
|
|
755
|
-
hash
|
|
765
|
+
hash,
|
|
756
766
|
size: Buffer.byteLength(afterText, 'utf8'),
|
|
757
767
|
});
|
|
758
768
|
// Fold the oldest versions into the base once the cap is hit, so every kept version stays replayable.
|
|
@@ -761,7 +771,33 @@ export const recordVersion = (history, personaId, before, after, action, now = n
|
|
|
761
771
|
state.base = applyDiff(state.base, oldest.ops);
|
|
762
772
|
state.baseAt = oldest.at;
|
|
763
773
|
}
|
|
764
|
-
return true;
|
|
774
|
+
return { recorded: true };
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Drop one version and re-diff what is left, so every remaining version still replays from the base.
|
|
778
|
+
* The dropped entry's text stops being reachable; the others keep their timestamp, action and text.
|
|
779
|
+
* Returns how many versions remain.
|
|
780
|
+
*/
|
|
781
|
+
export const deleteVersionAt = (state, index) => {
|
|
782
|
+
const versions = Array.isArray(state?.versions) ? state.versions : [];
|
|
783
|
+
if (!Number.isInteger(index) || index < 0 || index >= versions.length)
|
|
784
|
+
throw new Error('版本不存在');
|
|
785
|
+
const texts = versions.map((_, at) => versionTextAt(state, at));
|
|
786
|
+
const kept = versions.filter((_, at) => at !== index);
|
|
787
|
+
const keptTexts = texts.filter((_, at) => at !== index);
|
|
788
|
+
let previous = state.base;
|
|
789
|
+
kept.forEach((version, at) => {
|
|
790
|
+
const ops = diffLines(previous, keptTexts[at]);
|
|
791
|
+
const stats = diffStats(ops);
|
|
792
|
+
version.ops = ops;
|
|
793
|
+
version.added = stats.added;
|
|
794
|
+
version.removed = stats.removed;
|
|
795
|
+
version.hash = hashText(keptTexts[at]);
|
|
796
|
+
version.size = Buffer.byteLength(keptTexts[at], 'utf8');
|
|
797
|
+
previous = keptTexts[at];
|
|
798
|
+
});
|
|
799
|
+
state.versions = kept;
|
|
800
|
+
return kept.length;
|
|
765
801
|
};
|
|
766
802
|
// ─────────────────────────────────────────────────────────── matching
|
|
767
803
|
const regexCache = new Map();
|
|
@@ -1593,6 +1629,15 @@ class WorkspacePersonaService extends TypertRemoteService {
|
|
|
1593
1629
|
return { error: `restoreVersion failed: ${errorMessage(error)}` };
|
|
1594
1630
|
}
|
|
1595
1631
|
}
|
|
1632
|
+
/** Drop one stored version (the chain is re-diffed afterwards). */
|
|
1633
|
+
async deleteVersion(input) {
|
|
1634
|
+
try {
|
|
1635
|
+
return this.runtime.deleteVersion(input);
|
|
1636
|
+
}
|
|
1637
|
+
catch (error) {
|
|
1638
|
+
return { error: `deleteVersion failed: ${errorMessage(error)}` };
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1596
1641
|
/** Remove one persona. */
|
|
1597
1642
|
async deletePersona(input) {
|
|
1598
1643
|
try {
|
|
@@ -1943,11 +1988,12 @@ export function apply(ctx, config = {}) {
|
|
|
1943
1988
|
const historyEnabled = () => currentStore().prefs?.history === true;
|
|
1944
1989
|
const recordIfEnabled = (personaId, before, after, action) => {
|
|
1945
1990
|
if (!historyEnabled() || personaId === undefined)
|
|
1946
|
-
return;
|
|
1991
|
+
return { recorded: false };
|
|
1947
1992
|
const history = readHistory(storePath, warn);
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1993
|
+
const result = recordVersion(history, personaId, before, after, action);
|
|
1994
|
+
if (result.recorded)
|
|
1995
|
+
writeHistory(storePath, history, warn);
|
|
1996
|
+
return result;
|
|
1951
1997
|
};
|
|
1952
1998
|
/** Forget a deleted persona's versions; the history of something that no longer exists is just growth. */
|
|
1953
1999
|
const dropHistory = (personaId) => {
|
|
@@ -1987,6 +2033,7 @@ export function apply(ctx, config = {}) {
|
|
|
1987
2033
|
added: version.added,
|
|
1988
2034
|
removed: version.removed,
|
|
1989
2035
|
size: version.size,
|
|
2036
|
+
hash: version.hash ?? '',
|
|
1990
2037
|
preview: (version.ops ?? []).find((op) => op.t === '+')?.lines?.[0] ?? '',
|
|
1991
2038
|
})),
|
|
1992
2039
|
};
|
|
@@ -2013,6 +2060,7 @@ export function apply(ctx, config = {}) {
|
|
|
2013
2060
|
added: version.added,
|
|
2014
2061
|
removed: version.removed,
|
|
2015
2062
|
ops: version.ops,
|
|
2063
|
+
hash: version.hash ?? '',
|
|
2016
2064
|
before,
|
|
2017
2065
|
after,
|
|
2018
2066
|
current: persona.text,
|
|
@@ -2030,10 +2078,33 @@ export function apply(ctx, config = {}) {
|
|
|
2030
2078
|
const before = persona.text;
|
|
2031
2079
|
persona.text = text;
|
|
2032
2080
|
commit(current);
|
|
2033
|
-
recordIfEnabled(id, before, text, 'restore');
|
|
2081
|
+
const recorded = recordIfEnabled(id, before, text, 'restore');
|
|
2034
2082
|
info(`restored persona "${persona.name}" to version ${String(input?.index)}`);
|
|
2035
2083
|
const result = view();
|
|
2036
|
-
result.notice =
|
|
2084
|
+
result.notice = recorded.duplicateOf === undefined
|
|
2085
|
+
? `已恢复到第 ${Number(input?.index) + 1} 个版本`
|
|
2086
|
+
: `已恢复到第 ${Number(input?.index) + 1} 个版本(正文与第 ${recorded.duplicateOf + 1} 个版本相同,未新增记录)`;
|
|
2087
|
+
return result;
|
|
2088
|
+
},
|
|
2089
|
+
/** Drop one stored version; the remaining versions are re-diffed so the chain stays replayable. */
|
|
2090
|
+
deleteVersion(input) {
|
|
2091
|
+
const current = editableStore();
|
|
2092
|
+
const id = typeof input?.id === 'string' ? input.id : '';
|
|
2093
|
+
const persona = current.personas.find((item) => item.id === id);
|
|
2094
|
+
if (persona === undefined)
|
|
2095
|
+
throw new Error('人设不存在(可能已被删除)');
|
|
2096
|
+
const history = readHistory(storePath, warn);
|
|
2097
|
+
const state = history.personas[id];
|
|
2098
|
+
const index = Number(input?.index);
|
|
2099
|
+
const removed = versionTextAt(state, index);
|
|
2100
|
+
const remaining = deleteVersionAt(state, index);
|
|
2101
|
+
if (remaining === 0)
|
|
2102
|
+
delete history.personas[id];
|
|
2103
|
+
writeHistory(storePath, history, warn);
|
|
2104
|
+
info(`deleted version ${String(index)} of persona "${persona.name}" (${String(remaining)} left)`);
|
|
2105
|
+
const result = view();
|
|
2106
|
+
result.notice = `已删除第 ${index + 1} 个版本`;
|
|
2107
|
+
result.deleted = { index, size: Buffer.byteLength(removed, 'utf8') };
|
|
2037
2108
|
return result;
|
|
2038
2109
|
},
|
|
2039
2110
|
/** Merge preferences into the store; every field is optional so one switch can be saved. */
|
|
@@ -2046,7 +2117,11 @@ export function apply(ctx, config = {}) {
|
|
|
2046
2117
|
},
|
|
2047
2118
|
save(input) {
|
|
2048
2119
|
const current = editableStore();
|
|
2049
|
-
|
|
2120
|
+
// Fall back to the stored persona, not to the store: a payload that names an id and one field must
|
|
2121
|
+
// keep everything it did not mention, and `{ ...store }` has none of those fields at the top level.
|
|
2122
|
+
const requestedId = input !== null && typeof input?.id === 'string' && input.id !== '' ? input.id : undefined;
|
|
2123
|
+
const existing = requestedId === undefined ? undefined : current.personas.find((item) => item.id === requestedId);
|
|
2124
|
+
const next = normalizeInput(input, existing);
|
|
2050
2125
|
const beforeText = next.id === undefined ? '' : (current.personas.find((item) => item.id === next.id)?.text ?? '');
|
|
2051
2126
|
const created = next.id === undefined;
|
|
2052
2127
|
let persona;
|
|
@@ -2081,9 +2156,11 @@ export function apply(ctx, config = {}) {
|
|
|
2081
2156
|
}
|
|
2082
2157
|
}
|
|
2083
2158
|
commit(current);
|
|
2084
|
-
|
|
2085
|
-
recordIfEnabled(persona.id, beforeText, next.text, created ? 'create' : 'update');
|
|
2159
|
+
const recorded = next.text === beforeText ? { recorded: false } : recordIfEnabled(persona.id, beforeText, next.text, created ? 'create' : 'update');
|
|
2086
2160
|
const result = view();
|
|
2161
|
+
if (recorded.recorded === false && recorded.duplicateOf !== undefined) {
|
|
2162
|
+
result.notice = `正文与第 ${recorded.duplicateOf + 1} 个版本相同,没有新增版本`;
|
|
2163
|
+
}
|
|
2087
2164
|
if (released.length > 0) {
|
|
2088
2165
|
result.notice = `「${persona.name}」现在是默认人设;「${released.join('」「')}」已取消默认`;
|
|
2089
2166
|
info(`default persona moved from: ${released.join(', ')}`);
|
package/lib/remote.js
CHANGED
package/lib/typert.js
CHANGED
|
@@ -43,6 +43,7 @@ export const TYPERT = {
|
|
|
43
43
|
{ name: "listVersions", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
44
44
|
{ name: "versionDiff", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
45
45
|
{ name: "restoreVersion", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
46
|
+
{ name: "deleteVersion", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
46
47
|
],
|
|
47
48
|
types: [],
|
|
48
49
|
},
|
|
@@ -270,5 +271,21 @@ export const TYPERT = {
|
|
|
270
271
|
],
|
|
271
272
|
result: result("Json"),
|
|
272
273
|
},
|
|
274
|
+
{
|
|
275
|
+
id: `dsh-agent-persona#agentPersona/deleteVersion`,
|
|
276
|
+
service: "agentPersona",
|
|
277
|
+
namespace: "agentPersona",
|
|
278
|
+
method: "deleteVersion",
|
|
279
|
+
invocation: direct,
|
|
280
|
+
parameters: [
|
|
281
|
+
{
|
|
282
|
+
name: "input",
|
|
283
|
+
wire: "input",
|
|
284
|
+
source: "json",
|
|
285
|
+
codec: jsonCodec("Json"),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
result: result("Json"),
|
|
289
|
+
},
|
|
273
290
|
],
|
|
274
291
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-persona",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Workspace personas for DeepSeek Harness (DSH): create many system-prompt personas and scope each one by workspace path or session id (exact / prefix / regex / contains), managed from a Web settings page with Markdown editing and AI tuning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/client.ts
CHANGED
|
@@ -833,6 +833,7 @@ const readPrefs = () => {
|
|
|
833
833
|
.wsp-version-title { font-size: 13px; font-weight: 600; }
|
|
834
834
|
.wsp-version-meta { font-size: 11px; opacity: 0.72; font-variant-numeric: tabular-nums; }
|
|
835
835
|
.wsp-version-preview { font-size: 11px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
836
|
+
.wsp-version-hash { font-family: var(--wsp-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 11px; opacity: 0.55; }
|
|
836
837
|
.wsp-versions-diff { display: flex; flex-direction: column; gap: 8px; min-width: 0; overflow: hidden; }
|
|
837
838
|
.wsp-diff-head { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
|
|
838
839
|
.wsp-diff-body { overflow: auto; border: 1px solid var(--wsp-line); border-radius: 8px; padding: 8px 0; background: var(--wsp-line-soft); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.55; }
|
|
@@ -1359,6 +1360,7 @@ const since = (timestamp) => {
|
|
|
1359
1360
|
versionsError: '',
|
|
1360
1361
|
versionDiff: undefined,
|
|
1361
1362
|
versionIndex: undefined,
|
|
1363
|
+
versionConfirm: false,
|
|
1362
1364
|
models: [],
|
|
1363
1365
|
model: '',
|
|
1364
1366
|
tuneOpen: false,
|
|
@@ -1595,7 +1597,7 @@ const since = (timestamp) => {
|
|
|
1595
1597
|
this.onChange({ versionsError: '这个版本还没有版本历史接口:请更新插件后重启 dsh web' })
|
|
1596
1598
|
return
|
|
1597
1599
|
}
|
|
1598
|
-
this.onChange({ versionsBusy: 'list', versionsError: '', versions: { id }, versionDiff: undefined, versionIndex: undefined })
|
|
1600
|
+
this.onChange({ versionsBusy: 'list', versionsError: '', versions: { id }, versionDiff: undefined, versionIndex: undefined, versionConfirm: false })
|
|
1599
1601
|
try {
|
|
1600
1602
|
const data = this.unwrap(await api.listVersions({ id }))
|
|
1601
1603
|
this.onChange({ versionsBusy: '', versions: data })
|
|
@@ -1611,7 +1613,7 @@ const since = (timestamp) => {
|
|
|
1611
1613
|
const versions = this.state.versions
|
|
1612
1614
|
if (api === undefined || versions === undefined || index < 0) return
|
|
1613
1615
|
const seq = (this.versionSeq += 1)
|
|
1614
|
-
this.onChange({ versionsBusy: 'diff', versionIndex: index, versionsError: '' })
|
|
1616
|
+
this.onChange({ versionsBusy: 'diff', versionIndex: index, versionsError: '', versionConfirm: false })
|
|
1615
1617
|
try {
|
|
1616
1618
|
const diff = this.unwrap(await api.versionDiff({ id: versions.id, index }))
|
|
1617
1619
|
// Rows are clickable while a diff is loading: without this token two clicks in a row can show the
|
|
@@ -1650,6 +1652,37 @@ const since = (timestamp) => {
|
|
|
1650
1652
|
}
|
|
1651
1653
|
}
|
|
1652
1654
|
|
|
1655
|
+
/**
|
|
1656
|
+
* Delete one stored version. Two clicks: the first arms the footer button, the second deletes — a
|
|
1657
|
+
* history entry is the only copy of a text that may not exist anywhere else.
|
|
1658
|
+
*/
|
|
1659
|
+
async deleteVersion(index) {
|
|
1660
|
+
const api = this.api()
|
|
1661
|
+
const versions = this.state.versions
|
|
1662
|
+
if (api === undefined || versions === undefined) return
|
|
1663
|
+
if (this.state.versionConfirm !== true) {
|
|
1664
|
+
this.onChange({ versionConfirm: true })
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
clearTimeout(this.autosaveTimer)
|
|
1668
|
+
this.autosaveTimer = undefined
|
|
1669
|
+
this.saveSeq += 1
|
|
1670
|
+
this.onChange({ versionsBusy: 'delete', versionsError: '', versionConfirm: false })
|
|
1671
|
+
try {
|
|
1672
|
+
const view = this.unwrap(await api.deleteVersion({ id: versions.id, index }))
|
|
1673
|
+
const found = (view?.personas ?? []).find((item) => item.id === versions.id)
|
|
1674
|
+
this.onChange({
|
|
1675
|
+
view,
|
|
1676
|
+
versionsBusy: '',
|
|
1677
|
+
status: view?.notice ?? '已删除该版本',
|
|
1678
|
+
draft: this.state.openId === versions.id && found !== undefined ? this.draftFrom(found) : this.state.draft,
|
|
1679
|
+
})
|
|
1680
|
+
await this.openVersions(versions.id)
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
this.onChange({ versionsBusy: '', versionsError: String((error && error.message) || error) })
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1653
1686
|
/** Read a session's recent messages so the user can recognise it. */
|
|
1654
1687
|
async openHistory(sessionId) {
|
|
1655
1688
|
const api = this.api()
|
|
@@ -3034,6 +3067,13 @@ const since = (timestamp) => {
|
|
|
3034
3067
|
size: 'sm',
|
|
3035
3068
|
onClick: () => this.onChange({ versions: undefined, versionDiff: undefined, versionsError: '', versionIndex: undefined }),
|
|
3036
3069
|
}, '关闭'),
|
|
3070
|
+
h(Button, {
|
|
3071
|
+
key: 'delete',
|
|
3072
|
+
variant: 'outline',
|
|
3073
|
+
size: 'sm',
|
|
3074
|
+
disabled: state.versionsBusy !== '' || state.versionIndex === undefined || diffState === undefined,
|
|
3075
|
+
onClick: () => void this.deleteVersion(state.versionIndex),
|
|
3076
|
+
}, state.versionsBusy === 'delete' ? '删除中…' : state.versionConfirm === true ? '再点一次:删除' : '删除此版本'),
|
|
3037
3077
|
h(Button, {
|
|
3038
3078
|
key: 'restore',
|
|
3039
3079
|
variant: 'primary',
|
|
@@ -3056,13 +3096,14 @@ const since = (timestamp) => {
|
|
|
3056
3096
|
h('span', { className: 'wsp-version-title', key: 't' }, `第 ${version.index + 1} 版`),
|
|
3057
3097
|
h('span', { className: 'wsp-version-meta', key: 'm' },
|
|
3058
3098
|
`${String(version.at).replace('T', ' ').slice(5, 19)} +${version.added} / −${version.removed}`),
|
|
3099
|
+
h('span', { className: 'wsp-version-hash', key: 'h' }, `${version.hash ?? ''}`),
|
|
3059
3100
|
h('span', { className: 'wsp-version-preview', key: 'p' }, version.preview || '(无可显示的改动行)'),
|
|
3060
3101
|
]))),
|
|
3061
3102
|
h('div', { className: 'wsp-versions-diff', key: 'd' }, diffState === undefined
|
|
3062
3103
|
? h('div', { className: 'wsp-card-note' }, '左侧选一个版本,这里显示它相对上一版的改动。')
|
|
3063
3104
|
: [
|
|
3064
3105
|
h('div', { className: 'wsp-diff-head', key: 'h' },
|
|
3065
|
-
`${String(diffState.at).replace('T', ' ').slice(0, 19)} · ${diffState.action === 'restore' ? '恢复' : diffState.action === 'create' ? '新建' : '修改'} · +${diffState.added} / −${diffState.removed}`),
|
|
3106
|
+
`${String(diffState.at).replace('T', ' ').slice(0, 19)} · ${diffState.action === 'restore' ? '恢复' : diffState.action === 'create' ? '新建' : '修改'} · +${diffState.added} / −${diffState.removed} · ${diffState.hash ?? ''}`),
|
|
3066
3107
|
h('div', { className: 'wsp-diff-body', key: 'b' }, diffLines),
|
|
3067
3108
|
]),
|
|
3068
3109
|
]))
|
package/src/endpoints.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -725,14 +725,20 @@ const chainMatches = (state, beforeText) => {
|
|
|
725
725
|
}
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
/** Content hash of a version's text. Two versions that hash the same are the same text. */
|
|
729
|
+
export const hashText = (text) => createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex').slice(0, 16)
|
|
730
|
+
|
|
728
731
|
/**
|
|
729
732
|
* Record one new version. `before` is the text the persona had, `after` the text it now has.
|
|
730
|
-
*
|
|
733
|
+
*
|
|
734
|
+
* A save whose text already exists in the chain is not a new version: edits that walk back to an earlier
|
|
735
|
+
* text (undo, restore then save, an import of the same body) would otherwise pile up entries that differ
|
|
736
|
+
* only in their timestamp. Returns `{ recorded: false, duplicateOf }` in that case.
|
|
731
737
|
*/
|
|
732
738
|
export const recordVersion = (history, personaId, before, after, action, now = new Date().toISOString()) => {
|
|
733
739
|
const beforeText = String(before ?? '')
|
|
734
740
|
const afterText = String(after ?? '')
|
|
735
|
-
if (beforeText === afterText) return false
|
|
741
|
+
if (beforeText === afterText) return { recorded: false }
|
|
736
742
|
const ops = diffLines(beforeText, afterText)
|
|
737
743
|
const stats = diffStats(ops)
|
|
738
744
|
let state = history.personas[personaId]
|
|
@@ -746,13 +752,17 @@ export const recordVersion = (history, personaId, before, after, action, now = n
|
|
|
746
752
|
state.baseAt = now
|
|
747
753
|
state.versions = []
|
|
748
754
|
}
|
|
755
|
+
const hash = hashText(afterText)
|
|
756
|
+
// Checked after the re-base above: a re-based chain has no versions left to collide with.
|
|
757
|
+
const duplicateOf = state.versions.findIndex((version) => version?.hash === hash)
|
|
758
|
+
if (duplicateOf !== -1) return { recorded: false, duplicateOf }
|
|
749
759
|
state.versions.push({
|
|
750
760
|
at: now,
|
|
751
761
|
action,
|
|
752
762
|
ops,
|
|
753
763
|
added: stats.added,
|
|
754
764
|
removed: stats.removed,
|
|
755
|
-
hash
|
|
765
|
+
hash,
|
|
756
766
|
size: Buffer.byteLength(afterText, 'utf8'),
|
|
757
767
|
})
|
|
758
768
|
// Fold the oldest versions into the base once the cap is hit, so every kept version stays replayable.
|
|
@@ -761,7 +771,33 @@ export const recordVersion = (history, personaId, before, after, action, now = n
|
|
|
761
771
|
state.base = applyDiff(state.base, oldest.ops)
|
|
762
772
|
state.baseAt = oldest.at
|
|
763
773
|
}
|
|
764
|
-
return true
|
|
774
|
+
return { recorded: true }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Drop one version and re-diff what is left, so every remaining version still replays from the base.
|
|
779
|
+
* The dropped entry's text stops being reachable; the others keep their timestamp, action and text.
|
|
780
|
+
* Returns how many versions remain.
|
|
781
|
+
*/
|
|
782
|
+
export const deleteVersionAt = (state, index) => {
|
|
783
|
+
const versions = Array.isArray(state?.versions) ? state.versions : []
|
|
784
|
+
if (!Number.isInteger(index) || index < 0 || index >= versions.length) throw new Error('版本不存在')
|
|
785
|
+
const texts = versions.map((_, at) => versionTextAt(state, at))
|
|
786
|
+
const kept = versions.filter((_, at) => at !== index)
|
|
787
|
+
const keptTexts = texts.filter((_, at) => at !== index)
|
|
788
|
+
let previous = state.base
|
|
789
|
+
kept.forEach((version, at) => {
|
|
790
|
+
const ops = diffLines(previous, keptTexts[at])
|
|
791
|
+
const stats = diffStats(ops)
|
|
792
|
+
version.ops = ops
|
|
793
|
+
version.added = stats.added
|
|
794
|
+
version.removed = stats.removed
|
|
795
|
+
version.hash = hashText(keptTexts[at])
|
|
796
|
+
version.size = Buffer.byteLength(keptTexts[at], 'utf8')
|
|
797
|
+
previous = keptTexts[at]
|
|
798
|
+
})
|
|
799
|
+
state.versions = kept
|
|
800
|
+
return kept.length
|
|
765
801
|
}
|
|
766
802
|
|
|
767
803
|
// ─────────────────────────────────────────────────────────── matching
|
|
@@ -1554,6 +1590,15 @@ class WorkspacePersonaService extends TypertRemoteService {
|
|
|
1554
1590
|
}
|
|
1555
1591
|
}
|
|
1556
1592
|
|
|
1593
|
+
/** Drop one stored version (the chain is re-diffed afterwards). */
|
|
1594
|
+
async deleteVersion(input) {
|
|
1595
|
+
try {
|
|
1596
|
+
return this.runtime.deleteVersion(input)
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
return { error: `deleteVersion failed: ${errorMessage(error)}` }
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1557
1602
|
/** Remove one persona. */
|
|
1558
1603
|
async deletePersona(input) {
|
|
1559
1604
|
try {
|
|
@@ -1908,10 +1953,11 @@ export function apply(ctx, config = {}) {
|
|
|
1908
1953
|
/** Read + write the history file around one mutation; a disabled switch costs nothing. */
|
|
1909
1954
|
const historyEnabled = () => currentStore().prefs?.history === true
|
|
1910
1955
|
const recordIfEnabled = (personaId, before, after, action) => {
|
|
1911
|
-
if (!historyEnabled() || personaId === undefined) return
|
|
1956
|
+
if (!historyEnabled() || personaId === undefined) return { recorded: false }
|
|
1912
1957
|
const history = readHistory(storePath, warn)
|
|
1913
|
-
|
|
1914
|
-
writeHistory(storePath, history, warn)
|
|
1958
|
+
const result = recordVersion(history, personaId, before, after, action)
|
|
1959
|
+
if (result.recorded) writeHistory(storePath, history, warn)
|
|
1960
|
+
return result
|
|
1915
1961
|
}
|
|
1916
1962
|
|
|
1917
1963
|
/** Forget a deleted persona's versions; the history of something that no longer exists is just growth. */
|
|
@@ -1950,6 +1996,7 @@ export function apply(ctx, config = {}) {
|
|
|
1950
1996
|
added: version.added,
|
|
1951
1997
|
removed: version.removed,
|
|
1952
1998
|
size: version.size,
|
|
1999
|
+
hash: version.hash ?? '',
|
|
1953
2000
|
preview: (version.ops ?? []).find((op) => op.t === '+')?.lines?.[0] ?? '',
|
|
1954
2001
|
})),
|
|
1955
2002
|
}
|
|
@@ -1974,6 +2021,7 @@ export function apply(ctx, config = {}) {
|
|
|
1974
2021
|
added: version.added,
|
|
1975
2022
|
removed: version.removed,
|
|
1976
2023
|
ops: version.ops,
|
|
2024
|
+
hash: version.hash ?? '',
|
|
1977
2025
|
before,
|
|
1978
2026
|
after,
|
|
1979
2027
|
current: persona.text,
|
|
@@ -1990,10 +2038,31 @@ export function apply(ctx, config = {}) {
|
|
|
1990
2038
|
const before = persona.text
|
|
1991
2039
|
persona.text = text
|
|
1992
2040
|
commit(current)
|
|
1993
|
-
recordIfEnabled(id, before, text, 'restore')
|
|
2041
|
+
const recorded = recordIfEnabled(id, before, text, 'restore')
|
|
1994
2042
|
info(`restored persona "${persona.name}" to version ${String(input?.index)}`)
|
|
1995
2043
|
const result = view()
|
|
1996
|
-
result.notice =
|
|
2044
|
+
result.notice = recorded.duplicateOf === undefined
|
|
2045
|
+
? `已恢复到第 ${Number(input?.index) + 1} 个版本`
|
|
2046
|
+
: `已恢复到第 ${Number(input?.index) + 1} 个版本(正文与第 ${recorded.duplicateOf + 1} 个版本相同,未新增记录)`
|
|
2047
|
+
return result
|
|
2048
|
+
},
|
|
2049
|
+
/** Drop one stored version; the remaining versions are re-diffed so the chain stays replayable. */
|
|
2050
|
+
deleteVersion(input) {
|
|
2051
|
+
const current = editableStore()
|
|
2052
|
+
const id = typeof input?.id === 'string' ? input.id : ''
|
|
2053
|
+
const persona = current.personas.find((item) => item.id === id)
|
|
2054
|
+
if (persona === undefined) throw new Error('人设不存在(可能已被删除)')
|
|
2055
|
+
const history = readHistory(storePath, warn)
|
|
2056
|
+
const state = history.personas[id]
|
|
2057
|
+
const index = Number(input?.index)
|
|
2058
|
+
const removed = versionTextAt(state, index)
|
|
2059
|
+
const remaining = deleteVersionAt(state, index)
|
|
2060
|
+
if (remaining === 0) delete history.personas[id]
|
|
2061
|
+
writeHistory(storePath, history, warn)
|
|
2062
|
+
info(`deleted version ${String(index)} of persona "${persona.name}" (${String(remaining)} left)`)
|
|
2063
|
+
const result = view()
|
|
2064
|
+
result.notice = `已删除第 ${index + 1} 个版本`
|
|
2065
|
+
result.deleted = { index, size: Buffer.byteLength(removed, 'utf8') }
|
|
1997
2066
|
return result
|
|
1998
2067
|
},
|
|
1999
2068
|
/** Merge preferences into the store; every field is optional so one switch can be saved. */
|
|
@@ -2006,7 +2075,11 @@ export function apply(ctx, config = {}) {
|
|
|
2006
2075
|
},
|
|
2007
2076
|
save(input) {
|
|
2008
2077
|
const current = editableStore()
|
|
2009
|
-
|
|
2078
|
+
// Fall back to the stored persona, not to the store: a payload that names an id and one field must
|
|
2079
|
+
// keep everything it did not mention, and `{ ...store }` has none of those fields at the top level.
|
|
2080
|
+
const requestedId = input !== null && typeof input?.id === 'string' && input.id !== '' ? input.id : undefined
|
|
2081
|
+
const existing = requestedId === undefined ? undefined : current.personas.find((item) => item.id === requestedId)
|
|
2082
|
+
const next = normalizeInput(input, existing)
|
|
2010
2083
|
const beforeText = next.id === undefined ? '' : (current.personas.find((item) => item.id === next.id)?.text ?? '')
|
|
2011
2084
|
const created = next.id === undefined
|
|
2012
2085
|
let persona
|
|
@@ -2038,8 +2111,11 @@ export function apply(ctx, config = {}) {
|
|
|
2038
2111
|
}
|
|
2039
2112
|
}
|
|
2040
2113
|
commit(current)
|
|
2041
|
-
|
|
2114
|
+
const recorded = next.text === beforeText ? { recorded: false } : recordIfEnabled(persona.id, beforeText, next.text, created ? 'create' : 'update')
|
|
2042
2115
|
const result = view()
|
|
2116
|
+
if (recorded.recorded === false && recorded.duplicateOf !== undefined) {
|
|
2117
|
+
result.notice = `正文与第 ${recorded.duplicateOf + 1} 个版本相同,没有新增版本`
|
|
2118
|
+
}
|
|
2043
2119
|
if (released.length > 0) {
|
|
2044
2120
|
result.notice = `「${persona.name}」现在是默认人设;「${released.join('」「')}」已取消默认`
|
|
2045
2121
|
info(`default persona moved from: ${released.join(', ')}`)
|
package/src/remote.ts
CHANGED
package/src/typert.ts
CHANGED
|
@@ -45,6 +45,7 @@ export const TYPERT = {
|
|
|
45
45
|
{ name: "listVersions", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
46
46
|
{ name: "versionDiff", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
47
47
|
{ name: "restoreVersion", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
48
|
+
{ name: "deleteVersion", kind: "method", signature: "(input: unknown): Promise<unknown>" },
|
|
48
49
|
],
|
|
49
50
|
types: [],
|
|
50
51
|
},
|
|
@@ -272,5 +273,21 @@ export const TYPERT = {
|
|
|
272
273
|
],
|
|
273
274
|
result: result("Json"),
|
|
274
275
|
},
|
|
276
|
+
{
|
|
277
|
+
id: `dsh-agent-persona#agentPersona/deleteVersion`,
|
|
278
|
+
service: "agentPersona",
|
|
279
|
+
namespace: "agentPersona",
|
|
280
|
+
method: "deleteVersion",
|
|
281
|
+
invocation: direct,
|
|
282
|
+
parameters: [
|
|
283
|
+
{
|
|
284
|
+
name: "input",
|
|
285
|
+
wire: "input",
|
|
286
|
+
source: "json",
|
|
287
|
+
codec: jsonCodec("Json"),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
result: result("Json"),
|
|
291
|
+
},
|
|
275
292
|
],
|
|
276
293
|
}
|