dsh-custom-mode 1.8.0 → 1.9.2
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/client.js +218 -15
- package/composition.mjs +76 -1
- package/index.mjs +73 -3
- package/journal.mjs +4 -31
- package/locales.mjs +40 -6
- package/package.json +1 -1
- package/preset/preset.yml +1 -1
package/client.js
CHANGED
|
@@ -98,15 +98,30 @@ try {
|
|
|
98
98
|
"api.alreadyLast": "「{name}」已经在最后面。",
|
|
99
99
|
"api.badVariableName": "变量引用的写法不合法:{variable} 里的名字只能用小写字母、数字、下划线,且以字母开头。要写字面量花括号,请用单个左花括号,或不闭合的双左花括号。",
|
|
100
100
|
"api.unknownVariable": "{variable} 不是已注册的变量,渲染会报错并让本模式每个请求都失败。可用:{known}。",
|
|
101
|
+
"assistant.short": "每个助手是一个独立模式:自己的系统提示词、基础模式与插件开关。",
|
|
102
|
+
"name.short": "改名只影响显示,内部标识与已有会话不受影响。",
|
|
103
|
+
"mode.short": "底子决定「行集合」与工具能力;persona 行始终由本模式替换。",
|
|
104
|
+
"mode.pendingRows": "底子已改为「{mode}」:保存后,下面的行列表会按新底子重算。",
|
|
105
|
+
"rows.short": "逐行控制挂载哪些插件;没拨过的行保持官方默认。",
|
|
106
|
+
"prompt.short": "这段文本就是本模式的系统提示词,保存后下一步生效。",
|
|
107
|
+
"aria.expandHint": "展开完整说明",
|
|
108
|
+
"aria.collapseHint": "收起完整说明",
|
|
101
109
|
"aria.expand": "展开详情",
|
|
102
110
|
"aria.collapse": "收起详情",
|
|
103
111
|
"detail.id": "行 id",
|
|
112
|
+
"detail.shipped": "出厂状态",
|
|
113
|
+
"detail.shippedOn": "启用",
|
|
114
|
+
"detail.shippedOff": "关闭",
|
|
104
115
|
"detail.note": "说明",
|
|
105
116
|
"detail.state": "开关状态",
|
|
106
117
|
"detail.explicitOn": "已手动启用",
|
|
107
118
|
"detail.explicitOff": "已手动停用",
|
|
108
119
|
"detail.untouched": "未改动(跟随官方默认)",
|
|
109
120
|
"detail.platform": "平台条件",
|
|
121
|
+
"btn.repair": "按本线修复",
|
|
122
|
+
"msg.repaired": "已修复",
|
|
123
|
+
"meta.version": "插件版本",
|
|
124
|
+
"meta.versionHint": "安装时不钉版本号会受 pnpm 发布冷却期影响(默认 24 小时),可能装到较旧的版本。要换版本请按 README 的钉版本命令重装,然后重启 DSH。",
|
|
110
125
|
"api.saved": "已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。",
|
|
111
126
|
"api.created": "已创建「{name}」。现在可以为它写系统提示词。",
|
|
112
127
|
"api.duplicated": "已复制自「{from}」。两份从此各改各的。",
|
|
@@ -128,6 +143,8 @@ try {
|
|
|
128
143
|
"api.deleteFailed": "删除失败:{detail}",
|
|
129
144
|
"api.versionMissing": "找不到这个版本(历史可能已被上限裁剪)。",
|
|
130
145
|
"api.badJson": "请求体不是合法 JSON",
|
|
146
|
+
"warn.approvalGateMissing": "审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。",
|
|
147
|
+
"warn.unresolvableRows": "有行在本机这条 DSH 线上无法解析:平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。点右侧的「按本线修复」即可(只关掉那几行,其它选择不动)。",
|
|
131
148
|
"warn.approvalGateMissing.label": "审批闸门未启用",
|
|
132
149
|
"warn.approvalGateMissing.hint": "这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词**不会**弹审批。设置页不受影响;要恢复保护请升级 DSH,或把「custom_prompt 工具」那一行关掉。",
|
|
133
150
|
"warn.personaOffWithPrompt": "「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 你写的提示词现在不起作用。要么打开这一行,要么清空提示词。",
|
|
@@ -203,15 +220,15 @@ try {
|
|
|
203
220
|
"row.tool-subagent-control.label": "子代理控制",
|
|
204
221
|
"row.tool-subagent-list-agents.label": "列出子代理",
|
|
205
222
|
"row.tool-subagent-codex.label": "Codex 子代理",
|
|
206
|
-
"row.tool-subagent-codex.note": "
|
|
223
|
+
"row.tool-subagent-codex.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
|
|
207
224
|
"row.tool-subagent-claude-code.label": "Claude Code 子代理",
|
|
208
|
-
"row.tool-subagent-claude-code.note": "
|
|
225
|
+
"row.tool-subagent-claude-code.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
|
|
209
226
|
"row.workflow-ptc.label": "工作流引擎",
|
|
210
227
|
"row.workflow-worker-thread.label": "工作流 Worker 线程",
|
|
211
228
|
"row.workflow-worker-thread.note": "把工作流跑在独立的 worker 线程里",
|
|
212
229
|
"row.tool-workflow.label": "工作流工具",
|
|
213
230
|
"row.tool-ralph.label": "Ralph 工作流",
|
|
214
|
-
"row.tool-ralph.note": "
|
|
231
|
+
"row.tool-ralph.note": "Ralph 工作流工具;出厂状态见「详情」",
|
|
215
232
|
"row.tool-web.label": "网页检索与抓取",
|
|
216
233
|
"row.tool-skill.label": "技能工具",
|
|
217
234
|
"row.skill-filesystem.label": "技能发现",
|
|
@@ -281,15 +298,30 @@ try {
|
|
|
281
298
|
"api.alreadyLast": "「{name}」 is already last.",
|
|
282
299
|
"api.badVariableName": "{variable} is not a valid variable reference: names may use lower-case letters, digits and underscores, and must start with a letter. For a literal brace, use a single opening brace or an unclosed double brace.",
|
|
283
300
|
"api.unknownVariable": "{variable} is not a registered variable — rendering would fail every request in this mode. Available: {known}.",
|
|
301
|
+
"assistant.short": "Each assistant is its own mode: its own system prompt, base mode and plugin switches.",
|
|
302
|
+
"name.short": "Renaming only changes what is displayed — not the internal id or existing sessions.",
|
|
303
|
+
"mode.short": "The base decides the row set and tool abilities; the persona row is always replaced by this mode.",
|
|
304
|
+
"mode.pendingRows": "Base changed to 「{mode}」: the row list below is recomputed from the new base when you save.",
|
|
305
|
+
"rows.short": "Control which plugins this mode mounts, row by row; untouched rows keep the shipped default.",
|
|
306
|
+
"prompt.short": "Saving this text makes it the system prompt of this mode, and it takes effect on the next step.",
|
|
307
|
+
"aria.expandHint": "Show the full explanation",
|
|
308
|
+
"aria.collapseHint": "Hide the full explanation",
|
|
284
309
|
"aria.expand": "Show details",
|
|
285
310
|
"aria.collapse": "Hide details",
|
|
286
311
|
"detail.id": "Row id",
|
|
312
|
+
"detail.shipped": "Shipped",
|
|
313
|
+
"detail.shippedOn": "enabled",
|
|
314
|
+
"detail.shippedOff": "disabled",
|
|
287
315
|
"detail.note": "Note",
|
|
288
316
|
"detail.state": "Switch",
|
|
289
317
|
"detail.explicitOn": "Set to on by you",
|
|
290
318
|
"detail.explicitOff": "Set to off by you",
|
|
291
319
|
"detail.untouched": "Untouched (follows the shipped default)",
|
|
292
320
|
"detail.platform": "Platform condition",
|
|
321
|
+
"btn.repair": "Fix for this line",
|
|
322
|
+
"msg.repaired": "Repaired",
|
|
323
|
+
"meta.version": "Plugin version",
|
|
324
|
+
"meta.versionHint": "Installing without a pinned version is subject to pnpm’s release cooldown (24 h by default) and can land on an older release. To change version, reinstall with the pinned command from the README and restart DSH.",
|
|
293
325
|
"api.saved": "Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.",
|
|
294
326
|
"api.created": "Created 「{name}」. You can write its system prompt now.",
|
|
295
327
|
"api.duplicated": "Copied from 「{from}」. The two are independent from now on.",
|
|
@@ -314,6 +346,8 @@ try {
|
|
|
314
346
|
"warn.personaOffWithPrompt": "The \"Identity (system prompt)\" row is off, so prompt.md is never injected — the prompt you wrote has no effect. Turn the row on, or clear the prompt.",
|
|
315
347
|
"warn.toolOff": "The \"custom_prompt tool\" row is off: the agent cannot change the prompt from inside a session, only this page can.",
|
|
316
348
|
"warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
|
|
349
|
+
"warn.approvalGateMissing": "The approval gate is off: this DSH build has no tools/pre-execute event, so in-session prompt rewrites do not ask for approval. See the details.",
|
|
350
|
+
"warn.unresolvableRows": "Some rows cannot be resolved on this DSH line: the platform marks the whole mode broken and **silently drops it** from the new-session picker. Click 「Fix for this line」 — it only turns those rows off and leaves your other choices alone.",
|
|
317
351
|
"warn.approvalGateMissing.label": "Approval gate is off",
|
|
318
352
|
"warn.approvalGateMissing.hint": "This DSH build has no tools/pre-execute event, so in-session prompt rewrites do NOT ask for approval. The settings page is unaffected; upgrade DSH or turn the 「custom_prompt tool」 row off to restore the gate.",
|
|
319
353
|
"warn.noName": "No name: the mode picker will show the directory id (e.g. custom).",
|
|
@@ -386,15 +420,15 @@ try {
|
|
|
386
420
|
"row.tool-subagent-control.label": "Subagent control",
|
|
387
421
|
"row.tool-subagent-list-agents.label": "List subagents",
|
|
388
422
|
"row.tool-subagent-codex.label": "Codex subagent",
|
|
389
|
-
"row.tool-subagent-codex.note": "
|
|
423
|
+
"row.tool-subagent-codex.note": "Needs its Bundle installed first; the shipped state is in the details",
|
|
390
424
|
"row.tool-subagent-claude-code.label": "Claude Code subagent",
|
|
391
|
-
"row.tool-subagent-claude-code.note": "
|
|
425
|
+
"row.tool-subagent-claude-code.note": "Needs its Bundle installed first; the shipped state is in the details",
|
|
392
426
|
"row.workflow-ptc.label": "Workflow engine",
|
|
393
427
|
"row.workflow-worker-thread.label": "Workflow worker thread",
|
|
394
428
|
"row.workflow-worker-thread.note": "Runs workflows on a separate worker thread",
|
|
395
429
|
"row.tool-workflow.label": "Workflow tool",
|
|
396
430
|
"row.tool-ralph.label": "Ralph workflow",
|
|
397
|
-
"row.tool-ralph.note": "
|
|
431
|
+
"row.tool-ralph.note": "The Ralph workflow tool; the shipped state is in the details",
|
|
398
432
|
"row.tool-web.label": "Web search and fetch",
|
|
399
433
|
"row.tool-skill.label": "Skill tool",
|
|
400
434
|
"row.skill-filesystem.label": "Skill discovery",
|
|
@@ -578,6 +612,15 @@ try {
|
|
|
578
612
|
".cpfe{--g:8px;display:flex;flex-direction:column;gap:24px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px}",
|
|
579
613
|
".cpfe-h{margin:0 0 4px;font-size:15px;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
580
614
|
".cpfe-sub{margin:0 0 12px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
|
|
615
|
+
// 一行提示 + 详情下拉(与插件行同一套语言)
|
|
616
|
+
".cpfe-hint{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px}",
|
|
617
|
+
".cpfe-hint-line{flex:1;min-width:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
618
|
+
".cpfe-hint-toggle{flex:0 0 auto;width:20px;height:20px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;line-height:1}",
|
|
619
|
+
".cpfe-hint-toggle:hover{background:var(--dsw-alias-bg-layer-2)}",
|
|
620
|
+
".cpfe-hint-detail{flex:1 0 100%;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}",
|
|
621
|
+
// 描述:多行、自适应高度(没有多行输入组件,所以用 textarea + 同一批语义变量)
|
|
622
|
+
".cpfe-desc{box-sizing:border-box;min-height:56px;max-height:200px;resize:vertical;padding:8px 12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;margin-bottom:8px}",
|
|
623
|
+
".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
|
|
581
624
|
".cpfe-note{display:block;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
|
|
582
625
|
".cpfe-mono{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
583
626
|
".cpfe-pills{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
|
|
@@ -613,6 +656,8 @@ try {
|
|
|
613
656
|
".cpfe-err{color:var(--dsw-alias-state-error-primary)}",
|
|
614
657
|
".cpfe-dirty{color:var(--dsw-alias-state-warn-primary)}",
|
|
615
658
|
".cpfe-danger{color:var(--dsw-alias-state-error-primary)}",
|
|
659
|
+
".cpfe-meta-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0}",
|
|
660
|
+
".cpfe-version{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
616
661
|
".cpfe-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
617
662
|
// ── fallback-path controls (unused when the shell provides the atoms) ──
|
|
618
663
|
".cpfe-btn{appearance:none;cursor:pointer;padding:0 14px;height:32px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
|
|
@@ -652,6 +697,7 @@ try {
|
|
|
652
697
|
create: ROUTE + "/create",
|
|
653
698
|
delete: ROUTE + "/delete",
|
|
654
699
|
reorder: ROUTE + "/reorder",
|
|
700
|
+
repair: ROUTE + "/repair",
|
|
655
701
|
}
|
|
656
702
|
|
|
657
703
|
/** Every assistant this feature manages. */
|
|
@@ -733,6 +779,41 @@ try {
|
|
|
733
779
|
return String(template).replace("{name}", name)
|
|
734
780
|
}
|
|
735
781
|
|
|
782
|
+
/**
|
|
783
|
+
* A section's one-line hint, with the full explanation behind a disclosure.
|
|
784
|
+
*
|
|
785
|
+
* The sections used to open with a paragraph each. On a real screen that is a wall of text above the
|
|
786
|
+
* controls the user came for, and the same "compact line + open on demand" language as the row list keeps
|
|
787
|
+
* the page scannable without dropping a word of the explanation.
|
|
788
|
+
*/
|
|
789
|
+
function SectionHint(props) {
|
|
790
|
+
const { id, hint, detail, expanded, onToggleExpand, t } = props
|
|
791
|
+
const open = expanded[id] === true
|
|
792
|
+
const collapsible = typeof detail === "string" && detail !== "" && detail !== hint
|
|
793
|
+
return react.createElement(
|
|
794
|
+
"div",
|
|
795
|
+
{ className: "cpfe-hint" },
|
|
796
|
+
react.createElement("span", { className: "cpfe-hint-line", title: hint }, hint),
|
|
797
|
+
collapsible === false
|
|
798
|
+
? null
|
|
799
|
+
: react.createElement(
|
|
800
|
+
"button",
|
|
801
|
+
{
|
|
802
|
+
type: "button",
|
|
803
|
+
className: "cpfe-hint-toggle",
|
|
804
|
+
"aria-expanded": open,
|
|
805
|
+
"aria-label": t(open ? "aria.collapse" : "aria.expandHint"),
|
|
806
|
+
title: t(open ? "aria.collapse" : "aria.expandHint"),
|
|
807
|
+
onClick: () => onToggleExpand(id),
|
|
808
|
+
},
|
|
809
|
+
open ? "▾" : "▸",
|
|
810
|
+
),
|
|
811
|
+
open === true && collapsible === true
|
|
812
|
+
? react.createElement("p", { className: "cpfe-hint-detail" }, detail)
|
|
813
|
+
: null,
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
736
817
|
/**
|
|
737
818
|
* One row of the plugin switch list.
|
|
738
819
|
*
|
|
@@ -825,6 +906,18 @@ try {
|
|
|
825
906
|
react.createElement("span", { className: "cpfe-detail-key" }, t("detail.note")),
|
|
826
907
|
react.createElement("span", { className: "cpfe-detail-value" }, note),
|
|
827
908
|
),
|
|
909
|
+
react.createElement(
|
|
910
|
+
"div",
|
|
911
|
+
{ className: "cpfe-detail-line" },
|
|
912
|
+
react.createElement("span", { className: "cpfe-detail-key" }, t("detail.shipped")),
|
|
913
|
+
// 出厂状态来自**本机实际文件**(row.disabled),不是写死的文案:同一个行在两条 dsh 线上
|
|
914
|
+
// 的出厂状态可能不同(实测:Ralph 在稳定线出厂是启用的,在预览线是关闭的)。
|
|
915
|
+
react.createElement(
|
|
916
|
+
"span",
|
|
917
|
+
{ className: "cpfe-detail-value" },
|
|
918
|
+
row.disabled ? t("detail.shippedOff") : t("detail.shippedOn"),
|
|
919
|
+
),
|
|
920
|
+
),
|
|
828
921
|
react.createElement(
|
|
829
922
|
"div",
|
|
830
923
|
{ className: "cpfe-detail-line" },
|
|
@@ -922,6 +1015,28 @@ try {
|
|
|
922
1015
|
* differs per user, so nothing is expanded by default and nothing is persisted.
|
|
923
1016
|
*/
|
|
924
1017
|
const [expandedRows, setExpandedRows] = react.useState({})
|
|
1018
|
+
/** 本机这条线上有无法解析的行时,一键把它们关掉(服务端复用保存同一条排版手术)。 */
|
|
1019
|
+
const repairRowsNow = async () => {
|
|
1020
|
+
if (draft === null) return
|
|
1021
|
+
setBusy(true)
|
|
1022
|
+
setFailed(false)
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await postJson(ROUTES.repair, { id: selected })
|
|
1025
|
+
if (result !== null && result.ok === true) {
|
|
1026
|
+
setStatus(apiText(result, "msg.repaired"))
|
|
1027
|
+
await reload()
|
|
1028
|
+
} else {
|
|
1029
|
+
setFailed(true)
|
|
1030
|
+
setStatus(apiText(result, "msg.saveFailed"))
|
|
1031
|
+
}
|
|
1032
|
+
} catch {
|
|
1033
|
+
setFailed(true)
|
|
1034
|
+
setStatus(t("msg.saveFailed"))
|
|
1035
|
+
} finally {
|
|
1036
|
+
setBusy(false)
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
925
1040
|
const toggleRowExpanded = (id) =>
|
|
926
1041
|
setExpandedRows((previous) => {
|
|
927
1042
|
const next = { ...previous }
|
|
@@ -934,6 +1049,22 @@ try {
|
|
|
934
1049
|
|
|
935
1050
|
const entry = selected === "" ? undefined : entries[selected]
|
|
936
1051
|
const draft = entry === undefined ? null : entry.value
|
|
1052
|
+
|
|
1053
|
+
/** 描述框:随内容长高,避免双语描述被单行截断。draft 在未选中助手时是 null,依赖项要先取出来。 */
|
|
1054
|
+
const descriptionRef = react.useRef(null)
|
|
1055
|
+
const draftDescription = draft === null ? "" : draft.description
|
|
1056
|
+
// 已保存的底子:用来提示"改了但还没保存"—— 行列表是按它渲染的。
|
|
1057
|
+
const savedMode =
|
|
1058
|
+
entry === undefined || entry === null || entry.saved === null || entry.saved === undefined
|
|
1059
|
+
? null
|
|
1060
|
+
: entry.saved.mode
|
|
1061
|
+
react.useEffect(() => {
|
|
1062
|
+
const el = descriptionRef.current
|
|
1063
|
+
if (el === null || el === undefined) return
|
|
1064
|
+
el.style.height = "auto"
|
|
1065
|
+
el.style.height = String(Math.min(el.scrollHeight, 200)) + "px"
|
|
1066
|
+
}, [draftDescription, selected])
|
|
1067
|
+
|
|
937
1068
|
const payload = entry === undefined ? null : entry.payload
|
|
938
1069
|
const editorReady = draft !== null && payload !== null
|
|
939
1070
|
const dirty = editorReady && !sameDraft(draft, entry.saved)
|
|
@@ -1327,7 +1458,14 @@ try {
|
|
|
1327
1458
|
"section",
|
|
1328
1459
|
null,
|
|
1329
1460
|
react.createElement("h2", { className: "cpfe-h" }, t("assistant.heading")),
|
|
1330
|
-
react.createElement(
|
|
1461
|
+
react.createElement(SectionHint, {
|
|
1462
|
+
id: "hint:assistant",
|
|
1463
|
+
hint: t("assistant.short", t("assistant.hint")),
|
|
1464
|
+
detail: t("assistant.hint"),
|
|
1465
|
+
expanded: expandedRows,
|
|
1466
|
+
onToggleExpand: toggleRowExpanded,
|
|
1467
|
+
t: t,
|
|
1468
|
+
}),
|
|
1331
1469
|
list === null
|
|
1332
1470
|
? react.createElement("p", { className: "cpfe-sub" }, t("assistant.loadingList"))
|
|
1333
1471
|
: assistants.length === 0
|
|
@@ -1395,7 +1533,14 @@ try {
|
|
|
1395
1533
|
"section",
|
|
1396
1534
|
{ key: "name" },
|
|
1397
1535
|
react.createElement("h2", { className: "cpfe-h" }, t("name.heading")),
|
|
1398
|
-
react.createElement(
|
|
1536
|
+
react.createElement(SectionHint, {
|
|
1537
|
+
id: "hint:name",
|
|
1538
|
+
hint: t("name.short", t("name.hint")),
|
|
1539
|
+
detail: t("name.hint"),
|
|
1540
|
+
expanded: expandedRows,
|
|
1541
|
+
onToggleExpand: toggleRowExpanded,
|
|
1542
|
+
t: t,
|
|
1543
|
+
}),
|
|
1399
1544
|
react.createElement(A.Input, {
|
|
1400
1545
|
className: "cpfe-field",
|
|
1401
1546
|
value: draft.name,
|
|
@@ -1403,9 +1548,13 @@ try {
|
|
|
1403
1548
|
"aria-label": t("name.heading"),
|
|
1404
1549
|
onChange: (event) => update({ name: event.target.value }),
|
|
1405
1550
|
}),
|
|
1406
|
-
|
|
1407
|
-
|
|
1551
|
+
// 描述用 textarea 而不是单行 Input:shell 的组件里没有多行输入,而双语描述在单行框里
|
|
1552
|
+
// 会被截断(实测界面上只看到 "… / Ful")。高度随内容自适应,样式沿用同一批语义变量。
|
|
1553
|
+
react.createElement("textarea", {
|
|
1554
|
+
ref: descriptionRef,
|
|
1555
|
+
className: "cpfe-field cpfe-desc",
|
|
1408
1556
|
value: draft.description,
|
|
1557
|
+
rows: 2,
|
|
1409
1558
|
placeholder: t("name.descriptionPlaceholder"),
|
|
1410
1559
|
"aria-label": t("name.descriptionPlaceholder"),
|
|
1411
1560
|
onChange: (event) => update({ description: event.target.value }),
|
|
@@ -1464,7 +1613,14 @@ try {
|
|
|
1464
1613
|
"section",
|
|
1465
1614
|
{ key: "mode" },
|
|
1466
1615
|
react.createElement("h2", { className: "cpfe-h" }, t("mode.heading")),
|
|
1467
|
-
react.createElement(
|
|
1616
|
+
react.createElement(SectionHint, {
|
|
1617
|
+
id: "hint:mode",
|
|
1618
|
+
hint: t("mode.short", t("mode.hint")),
|
|
1619
|
+
detail: t("mode.hint"),
|
|
1620
|
+
expanded: expandedRows,
|
|
1621
|
+
onToggleExpand: toggleRowExpanded,
|
|
1622
|
+
t: t,
|
|
1623
|
+
}),
|
|
1468
1624
|
react.createElement(
|
|
1469
1625
|
"div",
|
|
1470
1626
|
{ className: "cpfe-pills" },
|
|
@@ -1481,12 +1637,27 @@ try {
|
|
|
1481
1637
|
{ className: "cpfe-note" },
|
|
1482
1638
|
payload.modes.reduce((note, mode) => (mode.id === draft.mode ? t("base." + mode.id + ".note", mode.note) : note), ""),
|
|
1483
1639
|
),
|
|
1640
|
+
// 底子改过但还没保存时明说一句:行列表是按**已保存**的组成渲染的,审阅把它记成了"点了没反应"。
|
|
1641
|
+
draft.mode !== savedMode
|
|
1642
|
+
? react.createElement(
|
|
1643
|
+
"p",
|
|
1644
|
+
{ className: "cpfe-note cpfe-base-pending" },
|
|
1645
|
+
fillPlaceholders(t("mode.pendingRows"), { mode: t("base." + draft.mode + ".label", draft.mode) }),
|
|
1646
|
+
)
|
|
1647
|
+
: null,
|
|
1484
1648
|
),
|
|
1485
1649
|
react.createElement(
|
|
1486
1650
|
"section",
|
|
1487
1651
|
{ key: "rows" },
|
|
1488
1652
|
react.createElement("h2", { className: "cpfe-h" }, t("rows.heading")),
|
|
1489
|
-
react.createElement(
|
|
1653
|
+
react.createElement(SectionHint, {
|
|
1654
|
+
id: "hint:rows",
|
|
1655
|
+
hint: t("rows.short", t("rows.hint")),
|
|
1656
|
+
detail: t("rows.hint"),
|
|
1657
|
+
expanded: expandedRows,
|
|
1658
|
+
onToggleExpand: toggleRowExpanded,
|
|
1659
|
+
t: t,
|
|
1660
|
+
}),
|
|
1490
1661
|
react.createElement(RowList, {
|
|
1491
1662
|
expanded: expandedRows,
|
|
1492
1663
|
onToggleExpand: toggleRowExpanded,
|
|
@@ -1560,7 +1731,14 @@ try {
|
|
|
1560
1731
|
}),
|
|
1561
1732
|
),
|
|
1562
1733
|
),
|
|
1563
|
-
react.createElement(
|
|
1734
|
+
react.createElement(SectionHint, {
|
|
1735
|
+
id: "hint:prompt",
|
|
1736
|
+
hint: t("prompt.short", t("prompt.hint")),
|
|
1737
|
+
detail: t("prompt.hint"),
|
|
1738
|
+
expanded: expandedRows,
|
|
1739
|
+
onToggleExpand: toggleRowExpanded,
|
|
1740
|
+
t: t,
|
|
1741
|
+
}),
|
|
1564
1742
|
react.createElement("textarea", {
|
|
1565
1743
|
className: "cpfe-editor",
|
|
1566
1744
|
value: draft.prompt,
|
|
@@ -1622,7 +1800,19 @@ try {
|
|
|
1622
1800
|
"div",
|
|
1623
1801
|
{ className: "cpfe-warns" },
|
|
1624
1802
|
...draft.warnings.map((code) =>
|
|
1625
|
-
react.createElement(
|
|
1803
|
+
react.createElement(
|
|
1804
|
+
"div",
|
|
1805
|
+
{ key: code, className: "cpfe-warn-row" },
|
|
1806
|
+
react.createElement("p", { className: "cpfe-warn" }, "⚠ " + t("warn." + code)),
|
|
1807
|
+
// 本线无法解析的行可以一键修(只关掉那几行;用户创建它的那个版本可能早于播种改为派生的版本)。
|
|
1808
|
+
code === "unresolvableRows"
|
|
1809
|
+
? react.createElement(
|
|
1810
|
+
A.Button,
|
|
1811
|
+
{ disabled: busy, onClick: repairRowsNow },
|
|
1812
|
+
t("btn.repair"),
|
|
1813
|
+
)
|
|
1814
|
+
: null,
|
|
1815
|
+
),
|
|
1626
1816
|
),
|
|
1627
1817
|
),
|
|
1628
1818
|
...editorSections,
|
|
@@ -1653,7 +1843,20 @@ try {
|
|
|
1653
1843
|
dirty ? t("btn.reloadDiscard") : t("btn.reload"),
|
|
1654
1844
|
),
|
|
1655
1845
|
react.createElement("span", { className: statusClass }, shown),
|
|
1656
|
-
react.createElement(
|
|
1846
|
+
react.createElement(
|
|
1847
|
+
"span",
|
|
1848
|
+
{ className: "cpfe-meta-line" },
|
|
1849
|
+
// 让用户能自己判断装到的是哪一版:pnpm 的发布冷却期会让"不钉版本"的安装落到旧版
|
|
1850
|
+
// (实测:干净机器上按名安装装到 1.0.1,而 latest 是 1.9.x)。
|
|
1851
|
+
react.createElement(
|
|
1852
|
+
"span",
|
|
1853
|
+
{ className: "cpfe-version", title: t("meta.versionHint") },
|
|
1854
|
+
// payload 在"还没读到任何助手"时是 null —— 页脚仍然会渲染,所以必须判空
|
|
1855
|
+
// (同一条错误这一轮被浏览器验收抓到过三次,单元测试一次都看不到)。
|
|
1856
|
+
payload === null ? "" : t("meta.version") + " v" + String(payload.version ?? "?"),
|
|
1857
|
+
),
|
|
1858
|
+
react.createElement("span", { className: "cpfe-path" }, editorReady ? payload.compositionPath : ""),
|
|
1859
|
+
),
|
|
1657
1860
|
),
|
|
1658
1861
|
// The confirmation is a portal: rendering it here keeps every piece of this page's
|
|
1659
1862
|
// state in one component.
|
package/composition.mjs
CHANGED
|
@@ -387,7 +387,7 @@ export function collectRows(text) {
|
|
|
387
387
|
/**
|
|
388
388
|
* Rewrite one level of rows, applying disabled overrides by id.
|
|
389
389
|
*
|
|
390
|
-
* `overrides` maps a row id to
|
|
390
|
+
* `overrides` maps a row id to its explicit **enabled** state (`true` = on, `false` = off). A row absent is
|
|
391
391
|
* left byte-for-byte as shipped — that is how an untouched `!!js` platform
|
|
392
392
|
* condition and the rows that ship disabled survive a regeneration.
|
|
393
393
|
*
|
|
@@ -559,6 +559,81 @@ function yamlScalar(value) {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
/** Read the base mode recorded in a generated composition, defaulting to standard. */
|
|
562
|
+
/**
|
|
563
|
+
* Enabled rows whose plugin package cannot be resolved in **this** installation.
|
|
564
|
+
*
|
|
565
|
+
* Why this exists (measured): a preset with an enabled row whose package this dsh line does not ship is marked
|
|
566
|
+
* broken by the platform and **silently dropped from every picker**, while the settings page keeps working — the
|
|
567
|
+
* P0 an external review found. Fresh installs are safe because seeding derives the composition from the installed
|
|
568
|
+
* line, but an assistant created by an **older** version keeps its old file forever (seeding never overwrites user
|
|
569
|
+
* data), so this is how the page can tell the user instead of leaving them with an invisible mode.
|
|
570
|
+
*
|
|
571
|
+
* Relative modules (`./prompt-reader.mjs`, ours) and `cordis:` pseudo-packages (the runtime's) are skipped.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} text - a composition.
|
|
574
|
+
* @returns {Array<{id: string, name: string}>} enabled rows that cannot resolve.
|
|
575
|
+
*/
|
|
576
|
+
/**
|
|
577
|
+
* Turn the named rows **off in place**, leaving every other byte of the file alone.
|
|
578
|
+
*
|
|
579
|
+
* Used by the "fix for this line" action: the composition a user has may contain rows this dsh line cannot
|
|
580
|
+
* resolve (typically written by an older version of this plugin), and the platform then drops the whole preset
|
|
581
|
+
* from every picker. Re-rendering the file from the base would also fix it — but it would silently discard any
|
|
582
|
+
* row the user (or a future version) added outside the base. This edits only the offending rows.
|
|
583
|
+
*
|
|
584
|
+
* @param {string} text - the composition.
|
|
585
|
+
* @param {Iterable<string>} ids - row ids to disable.
|
|
586
|
+
* @returns {string} the rewritten composition.
|
|
587
|
+
*/
|
|
588
|
+
export function disableRowsInPlace(text, ids) {
|
|
589
|
+
const wanted = [...new Set(ids)]
|
|
590
|
+
if (wanted.length === 0) return text
|
|
591
|
+
// `applyLevel` 把 Map 的值直接交给 `setDisabled(...)`,所以值是**关闭**布尔(true = 关闭)。
|
|
592
|
+
const off = new Map(wanted.map((id) => [id, true]))
|
|
593
|
+
const top = applyLevel(text, true, off, false)
|
|
594
|
+
return applyLevel(top, false, off, false)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function unresolvableRows(text) {
|
|
598
|
+
let root
|
|
599
|
+
try {
|
|
600
|
+
root = join(shippedPresetsDir(), '..', '..', '..')
|
|
601
|
+
} catch {
|
|
602
|
+
return []
|
|
603
|
+
}
|
|
604
|
+
// **判断不了就不要报警**:如果这个根下根本没有 node_modules(例如测试用的是一个临时出厂目录),
|
|
605
|
+
// 那么"查不到某个包"只说明我们不知道,不说明那行坏了。误报的代价是用户被引导去关掉本来正常的行。
|
|
606
|
+
if (existsSync(join(root, '@deepseek-ai')) === false) return []
|
|
607
|
+
const out = []
|
|
608
|
+
const lines = text.split('\n')
|
|
609
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
610
|
+
const row = /^( {0,4})- id: (.+?)\s*$/.exec(lines[index])
|
|
611
|
+
if (row === null) continue
|
|
612
|
+
const indent = row[1].length
|
|
613
|
+
let name
|
|
614
|
+
let disabled = false
|
|
615
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
616
|
+
const line = lines[next]
|
|
617
|
+
if (line.trim() !== '' && line.search(/\S/) <= indent) break
|
|
618
|
+
if (/^\s+name: ['"]?(.+?)['"]?\s*$/.test(line)) name = /^\s+name: ['"]?(.+?)['"]?\s*$/.exec(line)[1]
|
|
619
|
+
// 平台条件行(`disabled: !!js …`)必须**求值**判定:只看有没有字面 `true` 会把"本平台已启用"的行
|
|
620
|
+
// 误判成关闭,也会把"本平台本来就关闭"的行(如 Windows 专用的 pwsh)误报成"无法解析"。
|
|
621
|
+
const flag = /^\s+disabled:\s*(.+?)\s*$/.exec(line)
|
|
622
|
+
if (flag !== null) {
|
|
623
|
+
// 字面量直接读,`!!js` 一类交给求值器(它对字面量不做布尔化)。
|
|
624
|
+
const raw = flag[1].replace(/^['"]|['"]$/g, '')
|
|
625
|
+
disabled = raw === 'true' ? true : raw === 'false' ? false : evalDisabledExpression(flag[1]) === true
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (name === undefined || disabled) continue
|
|
629
|
+
if (name.startsWith('.') || name.startsWith('cordis:')) continue
|
|
630
|
+
const parts = name.split('/')
|
|
631
|
+
const pkg = name.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
|
|
632
|
+
if (existsSync(join(root, pkg)) === false) out.push({ id: row[2], name })
|
|
633
|
+
}
|
|
634
|
+
return out
|
|
635
|
+
}
|
|
636
|
+
|
|
562
637
|
export function modeOf(text) {
|
|
563
638
|
const match = /^# 基础模式: (\S+)\s*$/m.exec(text)
|
|
564
639
|
const id = match === null ? undefined : match[1]
|
package/index.mjs
CHANGED
|
@@ -44,11 +44,13 @@ import { PROMPT_PATH, COMPOSITION_PATH, ROUTE_PATH, PRESET_DIR } from './paths.m
|
|
|
44
44
|
import {
|
|
45
45
|
BASE_MODES,
|
|
46
46
|
collectRows,
|
|
47
|
-
|
|
47
|
+
disableRowsInPlace,
|
|
48
48
|
modeOf,
|
|
49
49
|
overridesOf,
|
|
50
50
|
readBaseComposition,
|
|
51
|
+
renderComposition,
|
|
51
52
|
setShippedPresetsDir,
|
|
53
|
+
unresolvableRows,
|
|
52
54
|
} from './composition.mjs'
|
|
53
55
|
import { readPresetMeta, writePresetMeta, presetMetaPath, PRESET_META_PATH } from './meta.mjs'
|
|
54
56
|
import { listHistory, readVersion, recordExternalChange, recordPrompt, HISTORY_SOURCE } from './journal.mjs'
|
|
@@ -72,6 +74,7 @@ const STATE_PATH = ROUTE_PATH + '/state'
|
|
|
72
74
|
const CREATE_PATH = ROUTE_PATH + '/create'
|
|
73
75
|
const DELETE_PATH = ROUTE_PATH + '/delete'
|
|
74
76
|
const REORDER_PATH = ROUTE_PATH + '/reorder'
|
|
77
|
+
const REPAIR_PATH = ROUTE_PATH + '/repair'
|
|
75
78
|
const HISTORY_PATH = ROUTE_PATH + '/history'
|
|
76
79
|
|
|
77
80
|
/** Which verbs each endpoint answers. `undefined` for a path means 404. */
|
|
@@ -82,6 +85,7 @@ const METHODS = {
|
|
|
82
85
|
[CREATE_PATH]: ['POST'],
|
|
83
86
|
[DELETE_PATH]: ['POST'],
|
|
84
87
|
[REORDER_PATH]: ['POST'],
|
|
88
|
+
[REPAIR_PATH]: ['POST'],
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/** 只告警一次:避免每个请求都刷同一行日志。 */
|
|
@@ -90,6 +94,21 @@ let warnedRosterShape = false
|
|
|
90
94
|
/** 应用层请求体上限;平台的 buffered cap 是第一道,这个是我们自己的兜底(见 handler 里的注释)。 */
|
|
91
95
|
const MAX_BODY_BYTES = 4 * 1024 * 1024
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 本插件的版本。
|
|
99
|
+
*
|
|
100
|
+
* 页面把它显示出来,是因为**用户很难自己判断装到的是哪一版**:pnpm 的发布冷却期(默认 24 小时)会让
|
|
101
|
+
* 不钉版本的安装落到"超过 24 小时的最新版",实测在一台干净机器上 `dsh plugin add dsh-custom-mode`
|
|
102
|
+
* 装到的是 1.0.1 而不是最新的 1.9.x。看见版本号,用户才知道要不要按 README 的钉版本命令重装。
|
|
103
|
+
*/
|
|
104
|
+
const PLUGIN_VERSION = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
|
|
107
|
+
} catch {
|
|
108
|
+
return 'unknown'
|
|
109
|
+
}
|
|
110
|
+
})()
|
|
111
|
+
|
|
93
112
|
/** Longest display name / description the page accepts, so one paste cannot bloat every picker. */
|
|
94
113
|
const MAX_NAME = 80
|
|
95
114
|
const MAX_DESCRIPTION = 400
|
|
@@ -278,12 +297,17 @@ export function readState(rows, id, options = {}) {
|
|
|
278
297
|
factoryPrompt: typeof options.factoryPrompt === 'string' ? options.factoryPrompt : null,
|
|
279
298
|
// 改动历史(只有元数据,正文按需取:见 GET /custom-mode/history)。
|
|
280
299
|
history: listHistory(directory),
|
|
300
|
+
// 本插件版本 + 本机装的那条 dsh 线上无法解析的行(页面据此显示版本与"按本线修复")。
|
|
301
|
+
version: PLUGIN_VERSION,
|
|
302
|
+
unresolvable: unresolvableRows(text),
|
|
281
303
|
// 「配置了却不生效」的告警码(文案在页面侧按语言渲染)。
|
|
282
304
|
warnings: [
|
|
283
305
|
...configWarnings(text, prompt.ok === true ? prompt.text : '', meta),
|
|
284
306
|
// 审批闸门缺失:由预置侧在注册失败时留下标记文件(宿主半看不到那个事件是否真的有人监听)。
|
|
285
307
|
// 诚实地把它变成页面上的告警,而不是只留在 console.error 里。
|
|
286
308
|
...(existsSync(join(directory, 'approval-gate-missing')) ? ['approvalGateMissing'] : []),
|
|
309
|
+
// 本线无法解析的启用行 = 平台会把整个预设判为 broken 并从选择器里丢掉(曾经的 P0)。
|
|
310
|
+
...(unresolvableRows(text).length > 0 ? ['unresolvableRows'] : []),
|
|
287
311
|
],
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -525,6 +549,42 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
|
|
|
525
549
|
* @param {object} input - `{ id }`.
|
|
526
550
|
* @param {{remove: (id: string) => Promise<void>}} agentPresets - the roster service.
|
|
527
551
|
*/
|
|
552
|
+
/**
|
|
553
|
+
* 把"本机这条 dsh 线上无法解析的启用行"关掉,让预设重新健康。
|
|
554
|
+
*
|
|
555
|
+
* 为什么需要它:老版本创建(或老版本播种)的组成文件会一直留着 —— 播种只补缺失文件、从不覆盖用户数据。
|
|
556
|
+
* 如果那份文件里有一行启用了本线不提供的插件,平台会把整个预设判为 broken 并从所有选择器里**静默丢弃**
|
|
557
|
+
* (设置页照常能开,所以用户完全不知道)。这里复用与保存同一条排版手术:读出现有 overrides,把那几行显式
|
|
558
|
+
* 关闭后重新渲染。用户的其它选择一字不动。
|
|
559
|
+
*/
|
|
560
|
+
export function repairComposition(rows, input) {
|
|
561
|
+
const id = input !== null && typeof input === 'object' && typeof input.id === 'string' ? input.id : ''
|
|
562
|
+
const directory = assistantDir(rows, id)
|
|
563
|
+
if (directory === undefined) return unknownAssistant(id)
|
|
564
|
+
const file = compositionFile(directory)
|
|
565
|
+
if (!existsSync(file)) return { ok: false, code: 'compositionMissing', params: { path: file }, error: '找不到组成文件:' + file }
|
|
566
|
+
const text = readFileSync(file, 'utf8')
|
|
567
|
+
const bad = unresolvableRows(text)
|
|
568
|
+
if (bad.length === 0) {
|
|
569
|
+
return { ok: true, id, code: 'repairNotNeeded', note: '这个助手在本机没有无法解析的行,无需修复。' }
|
|
570
|
+
}
|
|
571
|
+
// **就地**关闭那几行,不重渲染:重渲染会顺手丢掉 base 之外的自有行,而用户的数据不该被这样动。
|
|
572
|
+
const rendered = disableRowsInPlace(text, bad.map((row) => row.id))
|
|
573
|
+
try {
|
|
574
|
+
writeAtomic(file, rendered)
|
|
575
|
+
} catch (error) {
|
|
576
|
+
return { ok: false, code: 'writeFailed', params: { detail: describe(error) }, error: '写入失败:' + describe(error) }
|
|
577
|
+
}
|
|
578
|
+
const ids = bad.map((row) => row.id).join('、')
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
id,
|
|
582
|
+
code: 'repaired',
|
|
583
|
+
params: { count: bad.length, ids },
|
|
584
|
+
note: '已按本机这条 dsh 线关闭 ' + String(bad.length) + ' 个无法解析的行(' + ids + ')。现在这个模式能重新出现在选择器里。',
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
528
588
|
export async function deleteAssistant(rows, input, agentPresets) {
|
|
529
589
|
const id = input !== null && typeof input === 'object' && typeof input.id === 'string' ? input.id : ''
|
|
530
590
|
if (assistantDir(rows, id) === undefined) return unknownAssistant(id)
|
|
@@ -717,13 +777,19 @@ export function apply(ctx) {
|
|
|
717
777
|
// an unbounded amount; the platform's cap remains the primary guard.
|
|
718
778
|
const declared = Number(request.headers.get('content-length') ?? '')
|
|
719
779
|
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
720
|
-
return json({ ok: false, error: `请求体过大(上限 ${String(MAX_BODY_BYTES)} 字节)` }, 413)
|
|
780
|
+
return json({ ok: false, code: 'bodyTooLarge', params: { max: MAX_BODY_BYTES }, error: `请求体过大(上限 ${String(MAX_BODY_BYTES)} 字节)` }, 413)
|
|
721
781
|
}
|
|
722
782
|
|
|
723
783
|
// Everything below writes, so the body is read and parsed exactly once.
|
|
724
784
|
let parsed
|
|
725
785
|
try {
|
|
726
|
-
|
|
786
|
+
// 只信 `content-length` 会被 chunked(或不带该头)的请求绕过 —— 所以读完再按**实际字节**判一次。
|
|
787
|
+
// 平台自己的 buffered cap 仍是第一道;这一道保证"我们绝不 buffer 一个无上限的请求体"。
|
|
788
|
+
const raw = await request.text()
|
|
789
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_BODY_BYTES) {
|
|
790
|
+
return json({ ok: false, code: 'bodyTooLarge', params: { max: MAX_BODY_BYTES }, error: `请求体过大(上限 ${String(MAX_BODY_BYTES)} 字节)` }, 413)
|
|
791
|
+
}
|
|
792
|
+
parsed = JSON.parse(raw)
|
|
727
793
|
} catch {
|
|
728
794
|
return json({ ok: false, code: 'badJson', error: '请求体不是合法 JSON' }, 400)
|
|
729
795
|
}
|
|
@@ -738,6 +804,10 @@ export function apply(ctx) {
|
|
|
738
804
|
const result = await serializedWrite('tree', async () => createAssistant(await roster(), parsed))
|
|
739
805
|
return json(result, result.ok === true ? 200 : 400)
|
|
740
806
|
}
|
|
807
|
+
if (pathname === REPAIR_PATH) {
|
|
808
|
+
const result = await serializedWrite('repair:' + targetId, async () => repairComposition(await roster(), parsed))
|
|
809
|
+
return json(result, result.ok === true ? 200 : 400)
|
|
810
|
+
}
|
|
741
811
|
if (pathname === REORDER_PATH) {
|
|
742
812
|
const result = await serializedWrite('tree', async () => reorderAssistant(await roster(), parsed))
|
|
743
813
|
return json(result, result.ok === true ? 200 : 400)
|
package/journal.mjs
CHANGED
|
@@ -25,27 +25,9 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { randomBytes } from 'node:crypto'
|
|
27
27
|
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
28
|
+
import { writeAtomic } from './atomic.mjs'
|
|
28
29
|
import { join } from 'node:path'
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* `rename` 带重试(与宿主半同一实现,见 editor/index.mjs 的说明)。
|
|
32
|
-
*
|
|
33
|
-
* 这两个文件刻意各留一份:preset 侧的模块是独立分发的,不能 import 宿主半 —— 与 `checkPromptText`
|
|
34
|
-
* 同样的取舍。journal 只在宿主半写,所以这里只需与宿主半保持一致的重试策略。
|
|
35
|
-
*/
|
|
36
|
-
function renameWithRetry(from, to, attempts = 5) {
|
|
37
|
-
for (let attempt = 1; ; attempt += 1) {
|
|
38
|
-
try {
|
|
39
|
-
renameSync(from, to)
|
|
40
|
-
return
|
|
41
|
-
} catch (error) {
|
|
42
|
-
const code = error !== null && typeof error === 'object' ? error.code : undefined
|
|
43
|
-
const retryable = code === 'EPERM' || code === 'EBUSY' || code === 'EACCES'
|
|
44
|
-
if (retryable !== true || attempt >= attempts) throw error
|
|
45
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20 * attempt)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
31
|
|
|
50
32
|
/** 每个助手目录下的日志文件名。 */
|
|
51
33
|
export const HISTORY_NAME = 'prompt-history.jsonl'
|
|
@@ -121,18 +103,9 @@ export function recordPrompt(directory, text, by = HISTORY_SOURCE.settings) {
|
|
|
121
103
|
const file = historyFile(directory)
|
|
122
104
|
// 与宿主半同一条纪律:随机临时名(消除 tmp-vs-tmp 碰撞)+ 重试(Windows 上目标被并发 rename
|
|
123
105
|
// 持有时会短暂 EPERM)+ 失败清理。
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
renameWithRetry(temporary, file)
|
|
128
|
-
} catch (error) {
|
|
129
|
-
try {
|
|
130
|
-
rmSync(temporary, { force: true })
|
|
131
|
-
} catch {
|
|
132
|
-
/* 不掩盖原始错误 */
|
|
133
|
-
}
|
|
134
|
-
throw error
|
|
135
|
-
}
|
|
106
|
+
// 与宿主半共用同一份实现(editor/atomic.mjs):这里原先自己留了一份重试与清理,两份会各自漂移
|
|
107
|
+
// —— 外部审阅点名了这一点。预设侧(prompt-tool.mjs)仍保留自己的副本,因为那个文件独立分发、不能 import 宿主半。
|
|
108
|
+
writeAtomic(file, lines.join('\n') + '\n')
|
|
136
109
|
return { recorded: true, at, n }
|
|
137
110
|
}
|
|
138
111
|
|
package/locales.mjs
CHANGED
|
@@ -57,15 +57,30 @@ export const zh = {
|
|
|
57
57
|
'api.alreadyLast': '「{name}」已经在最后面。',
|
|
58
58
|
'api.badVariableName': '变量引用的写法不合法:{variable} 里的名字只能用小写字母、数字、下划线,且以字母开头。要写字面量花括号,请用单个左花括号,或不闭合的双左花括号。',
|
|
59
59
|
'api.unknownVariable': '{variable} 不是已注册的变量,渲染会报错并让本模式每个请求都失败。可用:{known}。',
|
|
60
|
+
'assistant.short': '每个助手是一个独立模式:自己的系统提示词、基础模式与插件开关。',
|
|
61
|
+
'name.short': '改名只影响显示,内部标识与已有会话不受影响。',
|
|
62
|
+
'mode.short': '底子决定「行集合」与工具能力;persona 行始终由本模式替换。',
|
|
63
|
+
'mode.pendingRows': '底子已改为「{mode}」:保存后,下面的行列表会按新底子重算。',
|
|
64
|
+
'rows.short': '逐行控制挂载哪些插件;没拨过的行保持官方默认。',
|
|
65
|
+
'prompt.short': '这段文本就是本模式的系统提示词,保存后下一步生效。',
|
|
66
|
+
'aria.expandHint': '展开完整说明',
|
|
67
|
+
'aria.collapseHint': '收起完整说明',
|
|
60
68
|
'aria.expand': '展开详情',
|
|
61
69
|
'aria.collapse': '收起详情',
|
|
62
70
|
'detail.id': '行 id',
|
|
71
|
+
'detail.shipped': '出厂状态',
|
|
72
|
+
'detail.shippedOn': '启用',
|
|
73
|
+
'detail.shippedOff': '关闭',
|
|
63
74
|
'detail.note': '说明',
|
|
64
75
|
'detail.state': '开关状态',
|
|
65
76
|
'detail.explicitOn': '已手动启用',
|
|
66
77
|
'detail.explicitOff': '已手动停用',
|
|
67
78
|
'detail.untouched': '未改动(跟随官方默认)',
|
|
68
79
|
'detail.platform': '平台条件',
|
|
80
|
+
'btn.repair': '按本线修复',
|
|
81
|
+
'msg.repaired': '已修复',
|
|
82
|
+
'meta.version': '插件版本',
|
|
83
|
+
'meta.versionHint': '安装时不钉版本号会受 pnpm 发布冷却期影响(默认 24 小时),可能装到较旧的版本。要换版本请按 README 的钉版本命令重装,然后重启 DSH。',
|
|
69
84
|
'api.saved': '已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。',
|
|
70
85
|
'api.created': '已创建「{name}」。现在可以为它写系统提示词。',
|
|
71
86
|
'api.duplicated': '已复制自「{from}」。两份从此各改各的。',
|
|
@@ -87,6 +102,8 @@ export const zh = {
|
|
|
87
102
|
'api.deleteFailed': '删除失败:{detail}',
|
|
88
103
|
'api.versionMissing': '找不到这个版本(历史可能已被上限裁剪)。',
|
|
89
104
|
'api.badJson': '请求体不是合法 JSON',
|
|
105
|
+
'warn.approvalGateMissing': '审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。',
|
|
106
|
+
'warn.unresolvableRows': '有行在本机这条 DSH 线上无法解析:平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。点右侧的「按本线修复」即可(只关掉那几行,其它选择不动)。',
|
|
90
107
|
'warn.approvalGateMissing.label': '审批闸门未启用',
|
|
91
108
|
'warn.approvalGateMissing.hint': '这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词**不会**弹审批。设置页不受影响;要恢复保护请升级 DSH,或把「custom_prompt 工具」那一行关掉。',
|
|
92
109
|
'warn.personaOffWithPrompt': '「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 你写的提示词现在不起作用。要么打开这一行,要么清空提示词。',
|
|
@@ -173,15 +190,15 @@ export const zh = {
|
|
|
173
190
|
'row.tool-subagent-control.label': '子代理控制',
|
|
174
191
|
'row.tool-subagent-list-agents.label': '列出子代理',
|
|
175
192
|
'row.tool-subagent-codex.label': 'Codex 子代理',
|
|
176
|
-
'row.tool-subagent-codex.note': '
|
|
193
|
+
'row.tool-subagent-codex.note': '需要先安装对应 Bundle 才能用;出厂状态见「详情」',
|
|
177
194
|
'row.tool-subagent-claude-code.label': 'Claude Code 子代理',
|
|
178
|
-
'row.tool-subagent-claude-code.note': '
|
|
195
|
+
'row.tool-subagent-claude-code.note': '需要先安装对应 Bundle 才能用;出厂状态见「详情」',
|
|
179
196
|
'row.workflow-ptc.label': '工作流引擎',
|
|
180
197
|
'row.workflow-worker-thread.label': '工作流 Worker 线程',
|
|
181
198
|
'row.workflow-worker-thread.note': '把工作流跑在独立的 worker 线程里',
|
|
182
199
|
'row.tool-workflow.label': '工作流工具',
|
|
183
200
|
'row.tool-ralph.label': 'Ralph 工作流',
|
|
184
|
-
'row.tool-ralph.note': '
|
|
201
|
+
'row.tool-ralph.note': 'Ralph 工作流工具;出厂状态见「详情」',
|
|
185
202
|
'row.tool-web.label': '网页检索与抓取',
|
|
186
203
|
'row.tool-skill.label': '技能工具',
|
|
187
204
|
'row.skill-filesystem.label': '技能发现',
|
|
@@ -256,15 +273,30 @@ export const en = {
|
|
|
256
273
|
'api.alreadyLast': '「{name}」 is already last.',
|
|
257
274
|
'api.badVariableName': '{variable} is not a valid variable reference: names may use lower-case letters, digits and underscores, and must start with a letter. For a literal brace, use a single opening brace or an unclosed double brace.',
|
|
258
275
|
'api.unknownVariable': '{variable} is not a registered variable — rendering would fail every request in this mode. Available: {known}.',
|
|
276
|
+
'assistant.short': 'Each assistant is its own mode: its own system prompt, base mode and plugin switches.',
|
|
277
|
+
'name.short': 'Renaming only changes what is displayed — not the internal id or existing sessions.',
|
|
278
|
+
'mode.short': 'The base decides the row set and tool abilities; the persona row is always replaced by this mode.',
|
|
279
|
+
'mode.pendingRows': 'Base changed to 「{mode}」: the row list below is recomputed from the new base when you save.',
|
|
280
|
+
'rows.short': 'Control which plugins this mode mounts, row by row; untouched rows keep the shipped default.',
|
|
281
|
+
'prompt.short': 'Saving this text makes it the system prompt of this mode, and it takes effect on the next step.',
|
|
282
|
+
'aria.expandHint': 'Show the full explanation',
|
|
283
|
+
'aria.collapseHint': 'Hide the full explanation',
|
|
259
284
|
'aria.expand': 'Show details',
|
|
260
285
|
'aria.collapse': 'Hide details',
|
|
261
286
|
'detail.id': 'Row id',
|
|
287
|
+
'detail.shipped': 'Shipped',
|
|
288
|
+
'detail.shippedOn': 'enabled',
|
|
289
|
+
'detail.shippedOff': 'disabled',
|
|
262
290
|
'detail.note': 'Note',
|
|
263
291
|
'detail.state': 'Switch',
|
|
264
292
|
'detail.explicitOn': 'Set to on by you',
|
|
265
293
|
'detail.explicitOff': 'Set to off by you',
|
|
266
294
|
'detail.untouched': 'Untouched (follows the shipped default)',
|
|
267
295
|
'detail.platform': 'Platform condition',
|
|
296
|
+
'btn.repair': 'Fix for this line',
|
|
297
|
+
'msg.repaired': 'Repaired',
|
|
298
|
+
'meta.version': 'Plugin version',
|
|
299
|
+
'meta.versionHint': 'Installing without a pinned version is subject to pnpm’s release cooldown (24 h by default) and can land on an older release. To change version, reinstall with the pinned command from the README and restart DSH.',
|
|
268
300
|
'api.saved': 'Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.',
|
|
269
301
|
'api.created': 'Created 「{name}」. You can write its system prompt now.',
|
|
270
302
|
'api.duplicated': 'Copied from 「{from}」. The two are independent from now on.',
|
|
@@ -289,6 +321,8 @@ export const en = {
|
|
|
289
321
|
'warn.personaOffWithPrompt': 'The "Identity (system prompt)" row is off, so prompt.md is never injected — the prompt you wrote has no effect. Turn the row on, or clear the prompt.',
|
|
290
322
|
'warn.toolOff': 'The "custom_prompt tool" row is off: the agent cannot change the prompt from inside a session, only this page can.',
|
|
291
323
|
'warn.noDescription': 'No description: the new-session mode picker will show it as "no description yet".',
|
|
324
|
+
'warn.approvalGateMissing': 'The approval gate is off: this DSH build has no tools/pre-execute event, so in-session prompt rewrites do not ask for approval. See the details.',
|
|
325
|
+
'warn.unresolvableRows': 'Some rows cannot be resolved on this DSH line: the platform marks the whole mode broken and **silently drops it** from the new-session picker. Click 「Fix for this line」 — it only turns those rows off and leaves your other choices alone.',
|
|
292
326
|
'warn.approvalGateMissing.label': 'Approval gate is off',
|
|
293
327
|
'warn.approvalGateMissing.hint': 'This DSH build has no tools/pre-execute event, so in-session prompt rewrites do NOT ask for approval. The settings page is unaffected; upgrade DSH or turn the 「custom_prompt tool」 row off to restore the gate.',
|
|
294
328
|
'warn.noName': 'No name: the mode picker will show the directory id (e.g. custom).',
|
|
@@ -375,15 +409,15 @@ export const en = {
|
|
|
375
409
|
'row.tool-subagent-control.label': 'Subagent control',
|
|
376
410
|
'row.tool-subagent-list-agents.label': 'List subagents',
|
|
377
411
|
'row.tool-subagent-codex.label': 'Codex subagent',
|
|
378
|
-
'row.tool-subagent-codex.note': '
|
|
412
|
+
'row.tool-subagent-codex.note': 'Needs its Bundle installed first; the shipped state is in the details',
|
|
379
413
|
'row.tool-subagent-claude-code.label': 'Claude Code subagent',
|
|
380
|
-
'row.tool-subagent-claude-code.note': '
|
|
414
|
+
'row.tool-subagent-claude-code.note': 'Needs its Bundle installed first; the shipped state is in the details',
|
|
381
415
|
'row.workflow-ptc.label': 'Workflow engine',
|
|
382
416
|
'row.workflow-worker-thread.label': 'Workflow worker thread',
|
|
383
417
|
'row.workflow-worker-thread.note': 'Runs workflows on a separate worker thread',
|
|
384
418
|
'row.tool-workflow.label': 'Workflow tool',
|
|
385
419
|
'row.tool-ralph.label': 'Ralph workflow',
|
|
386
|
-
'row.tool-ralph.note': '
|
|
420
|
+
'row.tool-ralph.note': 'The Ralph workflow tool; the shipped state is in the details',
|
|
387
421
|
'row.tool-web.label': 'Web search and fetch',
|
|
388
422
|
'row.tool-skill.label': 'Skill tool',
|
|
389
423
|
'row.skill-filesystem.label': 'Skill discovery',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.2",
|
|
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), choose its base mode, switch plugins row by row, and keep several assistants side by side.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/preset/preset.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
name: 自定义模式
|
|
2
|
-
description:
|
|
2
|
+
description: 在设置页编辑本模式的系统提示词 / Edit this mode's system prompt in Settings.
|