dsh-custom-mode 2.0.0 → 2.1.0
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.i18n.yaml +2 -2
- package/README.md +13 -8
- package/README.zh.md +8 -7
- package/client.js +23 -20
- package/index.mjs +52 -5
- package/locales.mjs +5 -3
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 双语配对一致性记录:两侧在「上次确认一致」时的 git blob 哈希。
|
|
2
2
|
# 两份文档权威相同——改完任意一侧,请把另一侧也改掉,然后重新记录:
|
|
3
3
|
# node tools/verify-translation-pairing.mjs --write
|
|
4
|
-
README.md:
|
|
5
|
-
README.zh.md:
|
|
4
|
+
README.md: d70fd341a9be167188f2e4d6117f93517d6221de
|
|
5
|
+
README.zh.md: aa63c0c8050a3832e9eab670fe77e0ba4dcabbb7
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ One command installs everything — the settings-page plugin, and the preset it
|
|
|
56
56
|
activation:
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
|
-
dsh plugin --profile web add dsh-custom-mode@2.
|
|
59
|
+
dsh plugin --profile web add dsh-custom-mode@2.1.0 # pin the version to get this one for sure
|
|
60
60
|
# A bare `add dsh-custom-mode` is subject to pnpm's release cooldown (`minimumReleaseAge`, 1 day by
|
|
61
61
|
# default): for hours after a release it can silently install an OLDER version — measured: a bare
|
|
62
62
|
# install 38 minutes after 1.3.0 shipped landed on 1.0.3. Check what you got with `npm ls
|
|
@@ -396,17 +396,22 @@ installing again — your data is not touched:
|
|
|
396
396
|
|
|
397
397
|
```sh
|
|
398
398
|
# the pinned form: what you ask for is what you get
|
|
399
|
-
dsh plugin --profile web add dsh-custom-mode@2.
|
|
399
|
+
dsh plugin --profile web add dsh-custom-mode@2.1.0
|
|
400
400
|
# then restart the DSH process that serves the web profile
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
-
**Do not install by bare name
|
|
404
|
-
24 hours by default)
|
|
405
|
-
hours*. Measured
|
|
406
|
-
|
|
403
|
+
**Do not install by bare name, and do not use `@latest`.** pnpm applies a release cooldown
|
|
404
|
+
(`minimumReleaseAge`, 24 hours by default). A bare `dsh plugin add dsh-custom-mode` resolves to *the newest
|
|
405
|
+
version older than 24 hours*. Measured: while npm `latest` was 1.12.2 and six minutes old, `@latest` installed
|
|
406
|
+
1.11.6. Pin the exact version.
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
Each exact pin appends a `minimumReleaseAgeExclude` line in the profile's `pnpm-workspace.yaml`. pnpm 11.7.0
|
|
409
|
+
honours only the first line that matches the package name ([pnpm#12463](https://github.com/pnpm/pnpm/issues/12463)).
|
|
410
|
+
Keep one line, `dsh-custom-mode`, not a list of versions. This plugin does not edit that file. If install fails
|
|
411
|
+
with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`, merge the list to that one line and run the install again:
|
|
412
|
+
`node_modules` may already show the new version while `package.json` still names the old one.
|
|
413
|
+
|
|
414
|
+
**How to know the upgrade worked**: restart dsh, then read the version at the bottom of this settings page.
|
|
410
415
|
|
|
411
416
|
**What an update does not touch**: `$DSH_HOME/.agent-presets/<your assistants>/` — `prompt.md`, `preset.yml` and
|
|
412
417
|
your row switches are yours. Seeding only fills in *missing* files, so a prompt you wrote is never overwritten.
|
package/README.zh.md
CHANGED
|
@@ -49,7 +49,7 @@ base mode / plugin switches / multi-assistant / multi-persona。
|
|
|
49
49
|
一条命令装完——设置页插件,以及它在首次激活时自动播种的 preset:
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
dsh plugin --profile web add dsh-custom-mode@2.
|
|
52
|
+
dsh plugin --profile web add dsh-custom-mode@2.1.0 # 钉版本才能确定拿到这一版
|
|
53
53
|
# 不带版本号会受 pnpm 的发布冷却期影响(`minimumReleaseAge`,默认一天):发布后数小时内按名安装
|
|
54
54
|
# 可能**静默装到旧版** —— 实测 1.3.0 发布 38 分钟后按名安装装到了 1.0.3。用 profile 里的
|
|
55
55
|
# `npm ls dsh-custom-mode` 核对实际装到的版本,或像上面那样钉版本。
|
|
@@ -68,7 +68,7 @@ dsh `0.1.6-alpha.2` 起有插件管理页:**侧边栏 → 插件 → 添加插
|
|
|
68
68
|
|
|
69
69
|
| 输入 | 填什么 | 说明 |
|
|
70
70
|
| --- | --- | --- |
|
|
71
|
-
| **包名** | `dsh-custom-mode` | 受 pnpm 发布冷却期影响,可能装到旧版。要这一版请钉 `@2.
|
|
71
|
+
| **包名** | `dsh-custom-mode` | 受 pnpm 发布冷却期影响,可能装到旧版。要这一版请钉 `@2.1.0` |
|
|
72
72
|
| **GitHub 仓库地址** | `https://github.com/BOWLUNA/dsh-custom-mode` | 指向**仓库根**即可 |
|
|
73
73
|
| **本地插件目录** | `<你 clone 的路径>` | 就是仓库根 —— 仓库根**本身**就是发布包 |
|
|
74
74
|
|
|
@@ -300,15 +300,16 @@ MIT
|
|
|
300
300
|
|
|
301
301
|
```sh
|
|
302
302
|
# 钉版本的写法:要哪版就是哪版
|
|
303
|
-
dsh plugin --profile web add dsh-custom-mode@2.
|
|
303
|
+
dsh plugin --profile web add dsh-custom-mode@2.1.0
|
|
304
304
|
# 然后重启为该 profile 提供服务的 DSH 进程
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
`dsh plugin add dsh-custom-mode`
|
|
309
|
-
不带版本号的命令装到的是 **1.0.1**。钉版本可以绕过冷却期,所以上面的命令带 `@`。
|
|
307
|
+
**不要按裸名安装,也不要用 `@latest`。** pnpm 有发布冷却期(`minimumReleaseAge`,默认 24 小时)。
|
|
308
|
+
`dsh plugin add dsh-custom-mode` 会解析成**超过 24 小时的最新版**。实测:npm 上 `latest` 已是发布 6 分钟的 1.12.2 时,`@latest` 装成了 1.11.6。请钉精确版本。
|
|
310
309
|
|
|
311
|
-
|
|
310
|
+
每次钉精确版本,pnpm 都会在 profile 的 `pnpm-workspace.yaml` 里追加一条 `minimumReleaseAgeExclude`。pnpm 11.7.0 只认第一条命中包名的记录([pnpm#12463](https://github.com/pnpm/pnpm/issues/12463))。这个包只留一条 `dsh-custom-mode`,不要留一串版本号。本插件不改这个文件。若安装报 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`,把列表收成那一条再装一次:`node_modules` 里可能已经是新版本,而 `package.json` 仍写着旧版本。
|
|
311
|
+
|
|
312
|
+
**怎样算升级成功**:重启 dsh,再看本设置页底部的版本号。
|
|
312
313
|
|
|
313
314
|
**更新不会碰的东西**:`$DSH_HOME/.agent-presets/<你的助手>/` —— `prompt.md`、`preset.yml` 与你逐行拨过的开关都属于你。
|
|
314
315
|
播种只补**缺失**的文件,你写过的提示词永远不会被覆盖。
|
package/client.js
CHANGED
|
@@ -127,6 +127,7 @@ try {
|
|
|
127
127
|
"meta.version": "插件版本",
|
|
128
128
|
"meta.versionHint": "未钉版本时,可能装到较旧的发布。要换版本,按 README 钉版本重装,然后重启。",
|
|
129
129
|
"api.saved": "已保存({name},{mode})。新会话使用新配置,当前会话不变。",
|
|
130
|
+
"api.conflict": "磁盘上的提示词已经变了,这次没有写入。先重新读取,再决定要不要覆盖。",
|
|
130
131
|
"api.savedWithExclusiveRows": "已保存({name},{mode})。已关闭不能同时启用的行:{rows}。",
|
|
131
132
|
"api.created": "已创建「{name}」。刷新后,新会话的模式菜单里会出现它。",
|
|
132
133
|
"api.savedPrompt": "已保存系统提示词({name})。下一步模型调用即生效。",
|
|
@@ -208,8 +209,8 @@ try {
|
|
|
208
209
|
"status.enabled": "已启用",
|
|
209
210
|
"status.disabled": "已停用",
|
|
210
211
|
"status.changed": "已改",
|
|
211
|
-
"tag.essential": "
|
|
212
|
-
"tag.followPlatform": "
|
|
212
|
+
"tag.essential": "核心",
|
|
213
|
+
"tag.followPlatform": "随平台",
|
|
213
214
|
"btn.save": "保存",
|
|
214
215
|
"btn.saving": "处理中…",
|
|
215
216
|
"btn.reload": "重新读取",
|
|
@@ -360,6 +361,7 @@ try {
|
|
|
360
361
|
"meta.version": "Plugin version",
|
|
361
362
|
"meta.versionHint": "Without a pinned version, an older release may be installed. To change it, reinstall with the version pin from the README, then restart.",
|
|
362
363
|
"api.saved": "Saved ({name}, {mode}). New sessions use it. The current session does not change.",
|
|
364
|
+
"api.conflict": "The prompt on disk has changed, so this save was not written. Reload, then decide whether to overwrite.",
|
|
363
365
|
"api.savedWithExclusiveRows": "Saved ({name}, {mode}). Turned off rows that cannot be on together: {rows}.",
|
|
364
366
|
"api.created": "Created {name}. Refresh once, then it appears in the mode menu for a new session.",
|
|
365
367
|
"api.savedPrompt": "System prompt saved ({name}). It applies on the next model step.",
|
|
@@ -442,7 +444,7 @@ try {
|
|
|
442
444
|
"status.disabled": "Disabled",
|
|
443
445
|
"status.changed": "changed",
|
|
444
446
|
"tag.essential": "Core",
|
|
445
|
-
"tag.followPlatform": "
|
|
447
|
+
"tag.followPlatform": "Platform",
|
|
446
448
|
"btn.save": "Save",
|
|
447
449
|
"btn.saving": "Working…",
|
|
448
450
|
"btn.reload": "Reload",
|
|
@@ -731,7 +733,9 @@ try {
|
|
|
731
733
|
* grids, the status line).
|
|
732
734
|
*/
|
|
733
735
|
const CSS = [
|
|
734
|
-
".cpfe{--g:8px;display:flex;flex-direction:column;gap:
|
|
736
|
+
".cpfe{--g:8px;display:flex;flex-direction:column;gap:16px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px;font:inherit;color:inherit}",
|
|
737
|
+
".cpfe > section{padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
738
|
+
".cpfe > section:first-child{padding-top:0;border-top:0}",
|
|
735
739
|
// 分节标题:度量抄官方设置面板里的同类文本。
|
|
736
740
|
// 实测(**0.1.7-rc.2 与 0.2.0-rc.2 各量一次,结论相同**):官方 `_title`(设置行标题)
|
|
737
741
|
// 与 `groupTitle`(插件页分组标题)都是 14px/22px **w400**;官方唯一的 14px w500 是插件
|
|
@@ -745,11 +749,13 @@ try {
|
|
|
745
749
|
".cpfe-hint-line{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
746
750
|
".cpfe-hint-detail{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
747
751
|
// 描述:多行、自适应高度(没有多行输入组件,所以用 textarea + 同一批语义变量)
|
|
748
|
-
".cpfe-desc{box-sizing:border-box;min-height:
|
|
752
|
+
".cpfe-desc{box-sizing:border-box;min-height:40px;max-height:120px;resize:vertical;padding:8px 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;margin-bottom:0}",
|
|
749
753
|
".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
|
|
750
754
|
".cpfe-note{display:block;margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
751
755
|
".cpfe-mono{display:block;margin:0;font-family:var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
752
756
|
".cpfe-pills{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
|
|
757
|
+
".cpfe-pills > *{flex:0 0 auto;white-space:nowrap}",
|
|
758
|
+
".cpfe-pills button{white-space:nowrap}",
|
|
753
759
|
".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
|
|
754
760
|
// 壳的菜单浮层靠一个独立的 backing 元素上色(实测在 0.1.7-rc.2 + headless 下那块是透明的,
|
|
755
761
|
// 文字会"压"在下面的输入框上)。用官方 token 显式补一层底色,真实浏览器与 headless 一致。
|
|
@@ -766,7 +772,7 @@ try {
|
|
|
766
772
|
".cpfe-row-head{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
767
773
|
".cpfe-row-switch{flex:0 0 auto}",
|
|
768
774
|
".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
|
|
769
|
-
".cpfe-row-note{
|
|
775
|
+
".cpfe-row-note{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
770
776
|
".cpfe-row-toggle{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}",
|
|
771
777
|
".cpfe-row-toggle:hover{background:var(--dsw-alias-bg-layer-2)}",
|
|
772
778
|
".cpfe-row-open{border-color:var(--dsw-alias-border-l2)}",
|
|
@@ -785,10 +791,10 @@ try {
|
|
|
785
791
|
".cpfe-line:last-child{border-bottom:0}",
|
|
786
792
|
".cpfe-group{display:flex;flex-direction:column;min-width:0}",
|
|
787
793
|
".cpfe-kids{display:flex;flex-direction:column;margin-left:0;padding-left:16px}",
|
|
788
|
-
".cpfe-row{display:flex;
|
|
789
|
-
".cpfe-row-
|
|
790
|
-
".cpfe-row
|
|
791
|
-
".cpfe-editor{box-sizing:border-box;width:100%;min-height:
|
|
794
|
+
".cpfe-row-meta{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}",
|
|
795
|
+
".cpfe-row-badges{display:flex;align-items:center;gap:4px;flex:0 1 auto;min-width:0}",
|
|
796
|
+
".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:74px;padding:16px 0;border:0;border-radius:0;background:none}",
|
|
797
|
+
".cpfe-editor{box-sizing:border-box;width:100%;min-height:120px;resize:vertical;padding:12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:13px;line-height:20px}",
|
|
792
798
|
".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap;margin-top:8px}",
|
|
793
799
|
".cpfe-disclosure{appearance:none;display:inline-flex;align-items:center;gap:6px;margin:8px 0 0;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;font-weight:400;cursor:pointer}",
|
|
794
800
|
".cpfe-filter{box-sizing:border-box;width:100%;height:36px;margin:0 0 8px;padding:0 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
|
|
@@ -1023,11 +1029,6 @@ try {
|
|
|
1023
1029
|
react.createElement(
|
|
1024
1030
|
"div",
|
|
1025
1031
|
{ className: "cpfe-row-badges" },
|
|
1026
|
-
react.createElement(
|
|
1027
|
-
A.Tag,
|
|
1028
|
-
{ tone: effective ? "success" : "neutral" },
|
|
1029
|
-
effective ? t("status.enabled") : t("status.disabled"),
|
|
1030
|
-
),
|
|
1031
1032
|
row.essential ? react.createElement(A.Tag, { tone: "warning" }, t("tag.essential")) : null,
|
|
1032
1033
|
row.disabledExpression !== null && row.disabledExpression !== undefined
|
|
1033
1034
|
? react.createElement(A.Tag, { tone: "outline" }, t("tag.followPlatform"))
|
|
@@ -1035,9 +1036,9 @@ try {
|
|
|
1035
1036
|
changed ? react.createElement(A.Tag, { tone: "info" }, t("status.changed")) : null,
|
|
1036
1037
|
),
|
|
1037
1038
|
),
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1039
|
+
note
|
|
1040
|
+
? react.createElement("span", { className: "cpfe-row-note", title: note }, note)
|
|
1041
|
+
: null,
|
|
1041
1042
|
),
|
|
1042
1043
|
react.createElement(
|
|
1043
1044
|
"button",
|
|
@@ -1662,6 +1663,7 @@ try {
|
|
|
1662
1663
|
const save = async () => {
|
|
1663
1664
|
setBusy(true)
|
|
1664
1665
|
try {
|
|
1666
|
+
const newest = Array.isArray(draft.history) && draft.history.length > 0 ? draft.history[0].n : undefined
|
|
1665
1667
|
const result = await postJson(ROUTES.state, {
|
|
1666
1668
|
id: draft.id,
|
|
1667
1669
|
mode: draft.mode,
|
|
@@ -1669,6 +1671,7 @@ try {
|
|
|
1669
1671
|
prompt: draft.prompt,
|
|
1670
1672
|
name: draft.name,
|
|
1671
1673
|
description: draft.description,
|
|
1674
|
+
...(typeof newest === "number" ? { baseN: newest } : {}),
|
|
1672
1675
|
})
|
|
1673
1676
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1674
1677
|
setFailed(false)
|
|
@@ -1924,6 +1927,7 @@ try {
|
|
|
1924
1927
|
),
|
|
1925
1928
|
)
|
|
1926
1929
|
: null,
|
|
1930
|
+
createGroup,
|
|
1927
1931
|
typeof (assistants.find((item) => item.id === selected) || {}).broken === "string"
|
|
1928
1932
|
? react.createElement("p", { className: "cpfe-note" }, t("assistant.brokenHint"))
|
|
1929
1933
|
: null,
|
|
@@ -2178,11 +2182,11 @@ try {
|
|
|
2178
2182
|
variant: "outline",
|
|
2179
2183
|
size: "sm",
|
|
2180
2184
|
disabled: busy || draft.historyPick === "",
|
|
2185
|
+
title: t("history.hint"),
|
|
2181
2186
|
onClick: loadVersion,
|
|
2182
2187
|
},
|
|
2183
2188
|
t("history.load"),
|
|
2184
2189
|
),
|
|
2185
|
-
react.createElement("span", { className: "cpfe-history-hint" }, t("history.hint")),
|
|
2186
2190
|
),
|
|
2187
2191
|
),
|
|
2188
2192
|
]
|
|
@@ -2322,7 +2326,6 @@ try {
|
|
|
2322
2326
|
: null,
|
|
2323
2327
|
)
|
|
2324
2328
|
: null,
|
|
2325
|
-
createGroup,
|
|
2326
2329
|
// The confirmation is a portal: rendering it here keeps every piece of this page's
|
|
2327
2330
|
// state in one component.
|
|
2328
2331
|
renderable(A.RiskConfirmation) && editorReady
|
package/index.mjs
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* GET /api/custom-mode — every assistant this feature manages.
|
|
8
8
|
* GET /api/custom-mode/state?id=… — one assistant: base mode, rows, switches, prompt.
|
|
9
9
|
* GET /api/custom-mode/history?id=…&n=… — one prompt revision (metadata comes with the state).
|
|
10
|
-
* POST /api/custom-mode/state — { id, mode, overrides, prompt, name, description }:
|
|
10
|
+
* POST /api/custom-mode/state — { id, mode, overrides, prompt, name, description, baseN? }:
|
|
11
11
|
* validate, render a fresh `agent.cordis.yml`, write both files.
|
|
12
|
+
* `baseN` is the newest history number the page loaded. A JSON
|
|
13
|
+
* number that is not the prompt now on disk refuses the write.
|
|
12
14
|
* POST /api/custom-mode/create — { name, description, posture?, locale? }: seed a new
|
|
13
15
|
* assistant. Omitting `posture` (or sending `develop`) is the
|
|
14
16
|
* historical path: the packaged prompt and the standard base,
|
|
@@ -492,6 +494,38 @@ export function readList(rows) {
|
|
|
492
494
|
*/
|
|
493
495
|
const writeChains = new Map()
|
|
494
496
|
|
|
497
|
+
/**
|
|
498
|
+
* 打开页面之后、按下保存之前,`prompt.md` 可能已被会话工具或手工改过。
|
|
499
|
+
*
|
|
500
|
+
* 先把磁盘上的那一版记进历史。页面带了 JSON 数字 `baseN`,且那个版本不是磁盘上这一版时,
|
|
501
|
+
* 拒绝写入:磁盘保持外部文本。不带 `baseN` 的旧客户端仍覆盖,但原文已经在历史里。
|
|
502
|
+
* 数字字符串不算 `baseN`。留痕自己失败时不挡住这次保存。
|
|
503
|
+
*
|
|
504
|
+
* @param {string} directory
|
|
505
|
+
* @param {string} prompt - the text this save wants to write.
|
|
506
|
+
* @param {unknown} baseN
|
|
507
|
+
* @returns {object|null} a `conflict` result, or null when the write may proceed.
|
|
508
|
+
*/
|
|
509
|
+
function refuseStalePrompt(directory, prompt, baseN) {
|
|
510
|
+
try {
|
|
511
|
+
const diskPrompt = readPrompt(directory)
|
|
512
|
+
if (diskPrompt.ok !== true || diskPrompt.text === prompt) return null
|
|
513
|
+
recordExternalChange(directory, diskPrompt.text)
|
|
514
|
+
if (typeof baseN === 'number' && readVersion(directory, baseN) !== diskPrompt.text) {
|
|
515
|
+
return {
|
|
516
|
+
ok: false,
|
|
517
|
+
code: 'conflict',
|
|
518
|
+
params: { baseN },
|
|
519
|
+
error: '磁盘上的提示词已经变了,这次保存没有写入。',
|
|
520
|
+
note: '磁盘上的提示词已经变了,这次保存没有写入。',
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
/* 审计留痕失败不挡住这次保存 */
|
|
525
|
+
}
|
|
526
|
+
return null
|
|
527
|
+
}
|
|
528
|
+
|
|
495
529
|
function serializedWrite(key, work) {
|
|
496
530
|
const previous = writeChains.get(key) ?? Promise.resolve()
|
|
497
531
|
const next = previous.then(work, work)
|
|
@@ -509,12 +543,13 @@ function serializedWrite(key, work) {
|
|
|
509
543
|
* 的行集合也就不变,这是所有选项里破坏性最小的一个。
|
|
510
544
|
*
|
|
511
545
|
* @param {string} directory - the assistant's preset directory.
|
|
512
|
-
* @param {{id: string, mode: string, prompt: string, name: string, description: string, displayName?: string}} input
|
|
546
|
+
* @param {{id: string, mode: string, prompt: string, name: string, description: string, displayName?: string, baseN?: number}} input
|
|
513
547
|
* - the validated save; `name` decides whether `preset.yml` is written, `displayName` is what the response
|
|
514
|
-
* reports (the disk name when the request omitted one — issue #9).
|
|
548
|
+
* reports (the disk name when the request omitted one — issue #9). `baseN` is the same stale-save
|
|
549
|
+
* token as {@link saveState}: a JSON number only.
|
|
515
550
|
* @returns {object} the shape `saveState` returns, with `code: 'savedPromptOnly'`.
|
|
516
551
|
*/
|
|
517
|
-
export function savePromptOnly(directory, { id, mode, prompt, name, description, displayName }) {
|
|
552
|
+
export function savePromptOnly(directory, { id, mode, prompt, name, description, displayName, baseN }) {
|
|
518
553
|
// 与正常保存同一条纪律:提示词与 preset.yml 一起换名,失败则两个都不动。
|
|
519
554
|
let metaText = null
|
|
520
555
|
if (name !== '') {
|
|
@@ -522,6 +557,8 @@ export function savePromptOnly(directory, { id, mode, prompt, name, description,
|
|
|
522
557
|
if (rendered.ok !== true) return rendered
|
|
523
558
|
metaText = rendered.text
|
|
524
559
|
}
|
|
560
|
+
const conflict = refuseStalePrompt(directory, prompt, baseN)
|
|
561
|
+
if (conflict !== null) return conflict
|
|
525
562
|
try {
|
|
526
563
|
writeAtomicPair([
|
|
527
564
|
[promptFile(directory), prompt],
|
|
@@ -618,7 +655,15 @@ export function saveState(rows, input) {
|
|
|
618
655
|
// 还想改开关时明确拒绝,而不是悄悄丢掉用户的意图。
|
|
619
656
|
if (isBaseCompositionUnavailable(error)) {
|
|
620
657
|
if (overrides.size === 0 && existsSync(compositionFile(directory))) {
|
|
621
|
-
return savePromptOnly(directory, {
|
|
658
|
+
return savePromptOnly(directory, {
|
|
659
|
+
id,
|
|
660
|
+
mode,
|
|
661
|
+
prompt,
|
|
662
|
+
name,
|
|
663
|
+
description,
|
|
664
|
+
displayName: effectiveName,
|
|
665
|
+
baseN: input !== null && typeof input === 'object' ? input.baseN : undefined,
|
|
666
|
+
})
|
|
622
667
|
}
|
|
623
668
|
return {
|
|
624
669
|
ok: false,
|
|
@@ -650,6 +695,8 @@ export function saveState(rows, input) {
|
|
|
650
695
|
if (rendered.ok !== true) return rendered
|
|
651
696
|
metaText = rendered.text
|
|
652
697
|
}
|
|
698
|
+
const conflict = refuseStalePrompt(directory, prompt, input !== null && typeof input === 'object' ? input.baseN : undefined)
|
|
699
|
+
if (conflict !== null) return conflict
|
|
653
700
|
try {
|
|
654
701
|
writeAtomicPair([
|
|
655
702
|
[compositionFile(directory), composition],
|
package/locales.mjs
CHANGED
|
@@ -88,6 +88,7 @@ export const zh = {
|
|
|
88
88
|
'meta.version': '插件版本',
|
|
89
89
|
'meta.versionHint': '未钉版本时,可能装到较旧的发布。要换版本,按 README 钉版本重装,然后重启。',
|
|
90
90
|
'api.saved': '已保存({name},{mode})。新会话使用新配置,当前会话不变。',
|
|
91
|
+
'api.conflict': '磁盘上的提示词已经变了,这次没有写入。先重新读取,再决定要不要覆盖。',
|
|
91
92
|
'api.savedWithExclusiveRows': '已保存({name},{mode})。已关闭不能同时启用的行:{rows}。',
|
|
92
93
|
'api.created': '已创建「{name}」。刷新后,新会话的模式菜单里会出现它。',
|
|
93
94
|
'api.savedPrompt': '已保存系统提示词({name})。下一步模型调用即生效。',
|
|
@@ -174,8 +175,8 @@ export const zh = {
|
|
|
174
175
|
'status.enabled': '已启用',
|
|
175
176
|
'status.disabled': '已停用',
|
|
176
177
|
'status.changed': '已改',
|
|
177
|
-
'tag.essential': '
|
|
178
|
-
'tag.followPlatform': '
|
|
178
|
+
'tag.essential': '核心',
|
|
179
|
+
'tag.followPlatform': '随平台',
|
|
179
180
|
|
|
180
181
|
'btn.save': '保存',
|
|
181
182
|
'btn.saving': '处理中…',
|
|
@@ -343,6 +344,7 @@ export const en = {
|
|
|
343
344
|
'meta.version': 'Plugin version',
|
|
344
345
|
'meta.versionHint': 'Without a pinned version, an older release may be installed. To change it, reinstall with the version pin from the README, then restart.',
|
|
345
346
|
'api.saved': 'Saved ({name}, {mode}). New sessions use it. The current session does not change.',
|
|
347
|
+
'api.conflict': 'The prompt on disk has changed, so this save was not written. Reload, then decide whether to overwrite.',
|
|
346
348
|
'api.savedWithExclusiveRows': 'Saved ({name}, {mode}). Turned off rows that cannot be on together: {rows}.',
|
|
347
349
|
'api.created': 'Created {name}. Refresh once, then it appears in the mode menu for a new session.',
|
|
348
350
|
'api.savedPrompt': 'System prompt saved ({name}). It applies on the next model step.',
|
|
@@ -430,7 +432,7 @@ export const en = {
|
|
|
430
432
|
'status.disabled': 'Disabled',
|
|
431
433
|
'status.changed': 'changed',
|
|
432
434
|
'tag.essential': 'Core',
|
|
433
|
-
'tag.followPlatform': '
|
|
435
|
+
'tag.followPlatform': 'Platform',
|
|
434
436
|
|
|
435
437
|
'btn.save': 'Save',
|
|
436
438
|
'btn.saving': 'Working…',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Custom modes and custom prompts for DeepSeek Harness (dsh): edit a mode's system prompt on the settings page (it takes effect on the next model step), pick its base mode, switch plugin rows one by one, and keep several assistants side by side — coding, chat or roleplay (RP) personas (multi-mode / multi-agent).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|