dsh-custom-mode 1.3.0 → 1.4.1
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/assistants.mjs +5 -4
- package/client.js +90 -10
- package/composition.mjs +3 -0
- package/index.mjs +74 -25
- package/journal.mjs +40 -6
- package/locales.mjs +54 -0
- package/package.json +3 -3
package/assistants.mjs
CHANGED
|
@@ -334,14 +334,14 @@ export function reorderAssistant(rows, input, write = writePresetMeta) {
|
|
|
334
334
|
const id = input !== null && typeof input === 'object' && typeof input.id === 'string' ? input.id : ''
|
|
335
335
|
const direction = input !== null && typeof input === 'object' ? input.direction : undefined
|
|
336
336
|
if (direction !== 'up' && direction !== 'down') {
|
|
337
|
-
return { ok: false, error: '未知的排序方向:' + String(direction) }
|
|
337
|
+
return { ok: false, code: 'badDirection', params: { direction: String(direction) }, error: '未知的排序方向:' + String(direction) }
|
|
338
338
|
}
|
|
339
339
|
const list = assistantsFromRoster(rows)
|
|
340
340
|
const index = list.findIndex((item) => item.id === id)
|
|
341
|
-
if (index === -1) return { ok: false, error: '找不到助手「' + id + '」。' }
|
|
341
|
+
if (index === -1) return { ok: false, code: 'unknownAssistant', params: { id }, error: '找不到助手「' + id + '」。' }
|
|
342
342
|
const target = direction === 'up' ? index - 1 : index + 1
|
|
343
|
-
if (target < 0) return { ok: false, error: '「' + (list[index].name || id) + '」已经在最前面。' }
|
|
344
|
-
if (target >= list.length) return { ok: false, error: '「' + (list[index].name || id) + '」已经在最后面。' }
|
|
343
|
+
if (target < 0) return { ok: false, code: 'alreadyFirst', params: { name: list[index].name || id }, error: '「' + (list[index].name || id) + '」已经在最前面。' }
|
|
344
|
+
if (target >= list.length) return { ok: false, code: 'alreadyLast', params: { name: list[index].name || id }, error: '「' + (list[index].name || id) + '」已经在最后面。' }
|
|
345
345
|
|
|
346
346
|
const next = [...list]
|
|
347
347
|
const [moved] = next.splice(index, 1)
|
|
@@ -357,6 +357,7 @@ export function reorderAssistant(rows, input, write = writePresetMeta) {
|
|
|
357
357
|
ok: true,
|
|
358
358
|
id,
|
|
359
359
|
order: next.map((item) => item.id),
|
|
360
|
+
code: 'reordered',
|
|
360
361
|
note: '顺序已保存:新建会话时的模式选择器按这个顺序排列。',
|
|
361
362
|
}
|
|
362
363
|
}
|
package/client.js
CHANGED
|
@@ -92,6 +92,31 @@ try {
|
|
|
92
92
|
"msg.reorderFailed": "调整顺序失败",
|
|
93
93
|
"msg.imported": "已导入到编辑器(还没有保存):检查后点「保存」。",
|
|
94
94
|
"msg.importFailed": "导入失败",
|
|
95
|
+
"api.badDirection": "未知的排序方向:{direction}",
|
|
96
|
+
"api.unknownAssistant": "找不到助手「{id}」:这一页只管理本工具创建的助手(目录里有 prompt.md,且组成文件用 prompt-reader.mjs 注入身份)。",
|
|
97
|
+
"api.alreadyFirst": "「{name}」已经在最前面。",
|
|
98
|
+
"api.alreadyLast": "「{name}」已经在最后面。",
|
|
99
|
+
"api.saved": "已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。",
|
|
100
|
+
"api.created": "已创建「{name}」。现在可以为它写系统提示词。",
|
|
101
|
+
"api.duplicated": "已复制自「{from}」。两份从此各改各的。",
|
|
102
|
+
"api.deleted": "已删除「{name}」。正在使用它的会话不受影响;新建会话时它不再出现。",
|
|
103
|
+
"api.reordered": "顺序已保存:新建会话时的模式选择器按这个顺序排列。",
|
|
104
|
+
"api.nameRequired": "请先给新助手起个名字。",
|
|
105
|
+
"api.nameTooLong": "名字太长了(上限 {max} 个字符)。",
|
|
106
|
+
"api.descriptionTooLong": "描述太长了(上限 {max} 个字符)。",
|
|
107
|
+
"api.promptEmpty": "保存被拒绝:系统提示词为空。留空不会清空身份,读取器会沿用上一版。",
|
|
108
|
+
"api.badMode": "未知的基础模式:{mode}",
|
|
109
|
+
"api.dirExists": "目录已存在,请换一个名字:{path}",
|
|
110
|
+
"api.compositionMissing": "找不到组成文件:{path}",
|
|
111
|
+
"api.writeFailed": "写入失败:{detail}",
|
|
112
|
+
"api.promptWriteFailed": "写入提示词失败:{detail}",
|
|
113
|
+
"api.renderFailed": "生成组成文件失败:{detail}",
|
|
114
|
+
"api.selfCheckFailed": "生成结果自检失败,已放弃写入:{detail}",
|
|
115
|
+
"api.promptReadFailed": "读取提示词失败:{detail}",
|
|
116
|
+
"api.noRemoveApi": "当前 DSH 版本没有 agentPresets.remove(),无法删除。",
|
|
117
|
+
"api.deleteFailed": "删除失败:{detail}",
|
|
118
|
+
"api.versionMissing": "找不到这个版本(历史可能已被上限裁剪)。",
|
|
119
|
+
"api.badJson": "请求体不是合法 JSON",
|
|
95
120
|
"warn.personaOffWithPrompt": "「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 你写的提示词现在不起作用。要么打开这一行,要么清空提示词。",
|
|
96
121
|
"warn.toolOff": "「custom_prompt 工具」这一行是关的:会话里无法让 agent 改提示词,只能在本页改。",
|
|
97
122
|
"warn.noDescription": "没有描述:新建会话的模式选择器里会显示成「暂无描述」。",
|
|
@@ -169,6 +194,8 @@ try {
|
|
|
169
194
|
"row.tool-subagent-claude-code.label": "Claude Code 子代理",
|
|
170
195
|
"row.tool-subagent-claude-code.note": "默认关闭:需要先安装对应 Bundle",
|
|
171
196
|
"row.workflow-ptc.label": "工作流引擎",
|
|
197
|
+
"row.workflow-worker-thread.label": "工作流 Worker 线程",
|
|
198
|
+
"row.workflow-worker-thread.note": "把工作流跑在独立的 worker 线程里",
|
|
172
199
|
"row.tool-workflow.label": "工作流工具",
|
|
173
200
|
"row.tool-ralph.label": "Ralph 工作流",
|
|
174
201
|
"row.tool-ralph.note": "默认关闭",
|
|
@@ -235,6 +262,31 @@ try {
|
|
|
235
262
|
"msg.reorderFailed": "Could not reorder",
|
|
236
263
|
"msg.imported": "Imported into the editor (not saved yet) — review it, then click Save.",
|
|
237
264
|
"msg.importFailed": "Import failed",
|
|
265
|
+
"api.badDirection": "Unknown reorder direction: {direction}",
|
|
266
|
+
"api.unknownAssistant": "No assistant 「{id}」: this page only manages the assistants it created (a directory with prompt.md whose composition injects the identity through prompt-reader.mjs).",
|
|
267
|
+
"api.alreadyFirst": "「{name}」 is already first.",
|
|
268
|
+
"api.alreadyLast": "「{name}」 is already last.",
|
|
269
|
+
"api.saved": "Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.",
|
|
270
|
+
"api.created": "Created 「{name}」. You can write its system prompt now.",
|
|
271
|
+
"api.duplicated": "Copied from 「{from}」. The two are independent from now on.",
|
|
272
|
+
"api.deleted": "Deleted 「{name}」. Sessions already using it keep running; it no longer appears for new sessions.",
|
|
273
|
+
"api.reordered": "Order saved: the new-session mode picker follows it.",
|
|
274
|
+
"api.nameRequired": "Give the new assistant a name first.",
|
|
275
|
+
"api.nameTooLong": "That name is too long (limit {max} characters).",
|
|
276
|
+
"api.descriptionTooLong": "That description is too long (limit {max} characters).",
|
|
277
|
+
"api.promptEmpty": "Save rejected: the system prompt is empty. Emptying it would not clear the identity — the reader keeps the last good text.",
|
|
278
|
+
"api.badMode": "Unknown base mode: {mode}",
|
|
279
|
+
"api.dirExists": "That directory already exists — pick another name: {path}",
|
|
280
|
+
"api.compositionMissing": "Composition file not found: {path}",
|
|
281
|
+
"api.writeFailed": "Write failed: {detail}",
|
|
282
|
+
"api.promptWriteFailed": "Writing the prompt failed: {detail}",
|
|
283
|
+
"api.renderFailed": "Rendering the composition failed: {detail}",
|
|
284
|
+
"api.selfCheckFailed": "The generated composition failed its self-check, so nothing was written: {detail}",
|
|
285
|
+
"api.promptReadFailed": "Reading the prompt failed: {detail}",
|
|
286
|
+
"api.noRemoveApi": "This DSH build has no agentPresets.remove(), so deleting is unavailable.",
|
|
287
|
+
"api.deleteFailed": "Delete failed: {detail}",
|
|
288
|
+
"api.versionMissing": "That version is gone (the history is capped).",
|
|
289
|
+
"api.badJson": "The request body is not valid JSON",
|
|
238
290
|
"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.",
|
|
239
291
|
"warn.toolOff": "The \"custom_prompt tool\" row is off: the agent cannot change the prompt from inside a session, only this page can.",
|
|
240
292
|
"warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
|
|
@@ -312,6 +364,8 @@ try {
|
|
|
312
364
|
"row.tool-subagent-claude-code.label": "Claude Code subagent",
|
|
313
365
|
"row.tool-subagent-claude-code.note": "Off by default: install the matching Bundle first",
|
|
314
366
|
"row.workflow-ptc.label": "Workflow engine",
|
|
367
|
+
"row.workflow-worker-thread.label": "Workflow worker thread",
|
|
368
|
+
"row.workflow-worker-thread.note": "Runs workflows on a separate worker thread",
|
|
315
369
|
"row.tool-workflow.label": "Workflow tool",
|
|
316
370
|
"row.tool-ralph.label": "Ralph workflow",
|
|
317
371
|
"row.tool-ralph.note": "Off by default",
|
|
@@ -720,6 +774,32 @@ try {
|
|
|
720
774
|
*/
|
|
721
775
|
const t = (key, fallback) => translate(key, fallback, shellT)
|
|
722
776
|
|
|
777
|
+
/** 把 `{name}` 这类占位符换成 params 里的值(缺失就原样留着,便于发现漏参)。 */
|
|
778
|
+
const fillPlaceholders = (template, params) =>
|
|
779
|
+
template.replace(/\{(\w+)\}/g, (match, key) =>
|
|
780
|
+
params !== null && typeof params === "object" && params[key] !== undefined ? String(params[key]) : match,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* 服务端结果的**本地化渲染**。
|
|
785
|
+
*
|
|
786
|
+
* 宿主仍然回中文的 `note`/`error`(那是 HTTP API 的兼容面),但页面优先用自己的词典按 `code`
|
|
787
|
+
* 渲染 —— 否则英文界面会在出错那一刻掉回中文(外部审阅点名的"硬伤")。服务端若带来了页面无从
|
|
788
|
+
* 知道的细节(路径、底层错误),放在 `params` 里填进模板。没有 `code` 时退回宿主文案,兼容旧宿主。
|
|
789
|
+
*/
|
|
790
|
+
const apiText = (result, fallbackKey) => {
|
|
791
|
+
const code = result !== null && typeof result === "object" && typeof result.code === "string" ? result.code : ""
|
|
792
|
+
if (code !== "") {
|
|
793
|
+
const template = translate("api." + code, "", shellT)
|
|
794
|
+
if (template !== "") return fillPlaceholders(template, result.params)
|
|
795
|
+
}
|
|
796
|
+
if (result !== null && typeof result === "object") {
|
|
797
|
+
if (typeof result.error === "string" && result.error !== "") return result.error
|
|
798
|
+
if (typeof result.note === "string" && result.note !== "") return result.note
|
|
799
|
+
}
|
|
800
|
+
return t(fallbackKey)
|
|
801
|
+
}
|
|
802
|
+
|
|
723
803
|
const [list, setList] = react.useState(null)
|
|
724
804
|
const [selected, setSelected] = react.useState("")
|
|
725
805
|
/** id → { payload, saved, value }: loaded state, baseline and live draft. */
|
|
@@ -764,7 +844,7 @@ try {
|
|
|
764
844
|
const result = await fetchState(id)
|
|
765
845
|
if (result === null || result === undefined || result.ok !== true) {
|
|
766
846
|
setFailed(true)
|
|
767
|
-
setStatus((result
|
|
847
|
+
setStatus(apiText(result, "msg.readFailed"))
|
|
768
848
|
return
|
|
769
849
|
}
|
|
770
850
|
const fresh = draftOf(result)
|
|
@@ -781,7 +861,7 @@ try {
|
|
|
781
861
|
const result = await fetchList()
|
|
782
862
|
if (result === null || result === undefined || result.ok !== true) {
|
|
783
863
|
setFailed(true)
|
|
784
|
-
setStatus((result
|
|
864
|
+
setStatus(apiText(result, "msg.readFailed"))
|
|
785
865
|
return
|
|
786
866
|
}
|
|
787
867
|
const assistants = Array.isArray(result.assistants) ? result.assistants : []
|
|
@@ -895,11 +975,11 @@ try {
|
|
|
895
975
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
896
976
|
setNewName("")
|
|
897
977
|
setFailed(false)
|
|
898
|
-
setStatus(result
|
|
978
|
+
setStatus(apiText(result, from === undefined ? "msg.created" : "msg.duplicated"))
|
|
899
979
|
await reload(result.id)
|
|
900
980
|
} else {
|
|
901
981
|
setFailed(true)
|
|
902
|
-
setStatus((result
|
|
982
|
+
setStatus(apiText(result, "msg.createFailed"))
|
|
903
983
|
}
|
|
904
984
|
} catch (error) {
|
|
905
985
|
setFailed(true)
|
|
@@ -923,11 +1003,11 @@ try {
|
|
|
923
1003
|
const result = await postJson(ROUTES.reorder, { id: draft.id, direction })
|
|
924
1004
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
925
1005
|
setFailed(false)
|
|
926
|
-
setStatus(result
|
|
1006
|
+
setStatus(apiText(result, "msg.reordered"))
|
|
927
1007
|
await reload(draft.id)
|
|
928
1008
|
} else {
|
|
929
1009
|
setFailed(true)
|
|
930
|
-
setStatus((result
|
|
1010
|
+
setStatus(apiText(result, "msg.reorderFailed"))
|
|
931
1011
|
}
|
|
932
1012
|
} catch (error) {
|
|
933
1013
|
setFailed(true)
|
|
@@ -1032,7 +1112,7 @@ try {
|
|
|
1032
1112
|
setDeleteOpen(false)
|
|
1033
1113
|
setAcknowledged(false)
|
|
1034
1114
|
setFailed(false)
|
|
1035
|
-
setStatus(result
|
|
1115
|
+
setStatus(apiText(result, "msg.deleted"))
|
|
1036
1116
|
setEntries((previous) => {
|
|
1037
1117
|
const next = { ...previous }
|
|
1038
1118
|
delete next[id]
|
|
@@ -1041,7 +1121,7 @@ try {
|
|
|
1041
1121
|
await reload("")
|
|
1042
1122
|
} else {
|
|
1043
1123
|
setFailed(true)
|
|
1044
|
-
setStatus((result
|
|
1124
|
+
setStatus(apiText(result, "msg.deleteFailed"))
|
|
1045
1125
|
}
|
|
1046
1126
|
} catch (error) {
|
|
1047
1127
|
setFailed(true)
|
|
@@ -1087,7 +1167,7 @@ try {
|
|
|
1087
1167
|
})
|
|
1088
1168
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1089
1169
|
setFailed(false)
|
|
1090
|
-
setStatus(result
|
|
1170
|
+
setStatus(apiText(result, "msg.saved"))
|
|
1091
1171
|
// The server normalises what it stores (trimmed name, recomputed overrides),
|
|
1092
1172
|
// so the just-saved draft is replaced by what it actually wrote. Without this
|
|
1093
1173
|
// the page would show 「已保存」 and 「未保存」 at the same time.
|
|
@@ -1104,7 +1184,7 @@ try {
|
|
|
1104
1184
|
}
|
|
1105
1185
|
} else {
|
|
1106
1186
|
setFailed(true)
|
|
1107
|
-
setStatus((result
|
|
1187
|
+
setStatus(apiText(result, "msg.saveFailed"))
|
|
1108
1188
|
}
|
|
1109
1189
|
} catch (error) {
|
|
1110
1190
|
setFailed(true)
|
package/composition.mjs
CHANGED
|
@@ -112,6 +112,9 @@ export const ROW_META = {
|
|
|
112
112
|
'tool-subagent-codex': { label: 'Codex 子代理', note: '默认关闭:需要先安装对应 Bundle' },
|
|
113
113
|
'tool-subagent-claude-code': { label: 'Claude Code 子代理', note: '默认关闭:需要先安装对应 Bundle' },
|
|
114
114
|
'workflow-ptc': { label: '工作流引擎' },
|
|
115
|
+
// 只出现在**稳定线**(0.1.5-rc.2)的委派分组里;预览线没有这一行。CI 的稳定线任务抓到了它
|
|
116
|
+
// 缺标签("每个出厂行都能查到标签"),这正是双线矩阵的价值。
|
|
117
|
+
'workflow-worker-thread': { label: '工作流 Worker 线程', note: '把工作流跑在独立的 worker 线程里' },
|
|
115
118
|
'tool-workflow': { label: '工作流工具' },
|
|
116
119
|
'tool-ralph': { label: 'Ralph 工作流', note: '默认关闭' },
|
|
117
120
|
'tool-web': { label: '网页检索与抓取' },
|
package/index.mjs
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { randomBytes } from 'node:crypto'
|
|
41
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
41
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
42
42
|
import { dirname, join } from 'node:path'
|
|
43
43
|
import { PROMPT_PATH, COMPOSITION_PATH, ROUTE_PATH, PRESET_DIR } from './paths.mjs'
|
|
44
44
|
import {
|
|
@@ -166,7 +166,7 @@ export function readPrompt(directory) {
|
|
|
166
166
|
try {
|
|
167
167
|
return { ok: true, path, text: readFileSync(path, 'utf8') }
|
|
168
168
|
} catch (error) {
|
|
169
|
-
return { ok: false, error: '读取提示词失败:' + describe(error) }
|
|
169
|
+
return { ok: false, code: 'promptReadFailed', params: { detail: describe(error) }, error: '读取提示词失败:' + describe(error) }
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -246,7 +246,7 @@ export function readState(rows, id, options = {}) {
|
|
|
246
246
|
const directory = assistantDir(rows, id)
|
|
247
247
|
if (directory === undefined) return unknownAssistant(id)
|
|
248
248
|
const composition = compositionFile(directory)
|
|
249
|
-
if (!existsSync(composition)) return { ok: false, error: '找不到组成文件:' + composition }
|
|
249
|
+
if (!existsSync(composition)) return { ok: false, code: 'compositionMissing', params: { path: composition }, error: '找不到组成文件:' + composition }
|
|
250
250
|
const text = readFileSync(composition, 'utf8')
|
|
251
251
|
const mode = modeOf(text)
|
|
252
252
|
const prompt = readPrompt(directory)
|
|
@@ -304,11 +304,11 @@ export function saveState(rows, input) {
|
|
|
304
304
|
|
|
305
305
|
const mode = input !== null && typeof input === 'object' && typeof input.mode === 'string' ? input.mode : ''
|
|
306
306
|
if (!BASE_MODES.some((entry) => entry.id === mode)) {
|
|
307
|
-
return { ok: false, error: '未知的基础模式:' + mode }
|
|
307
|
+
return { ok: false, code: 'badMode', params: { mode }, error: '未知的基础模式:' + mode }
|
|
308
308
|
}
|
|
309
309
|
const prompt = input !== null && typeof input === 'object' && typeof input.prompt === 'string' ? input.prompt : ''
|
|
310
310
|
if (prompt.trim() === '') {
|
|
311
|
-
return { ok: false, error: '保存被拒绝:系统提示词为空。留空不会清空身份,读取器会沿用上一版。' }
|
|
311
|
+
return { ok: false, code: 'promptEmpty', error: '保存被拒绝:系统提示词为空。留空不会清空身份,读取器会沿用上一版。' }
|
|
312
312
|
}
|
|
313
313
|
const verdict = checkPromptText(prompt)
|
|
314
314
|
if (verdict.ok !== true) return { ok: false, error: verdict.error }
|
|
@@ -316,13 +316,13 @@ export function saveState(rows, input) {
|
|
|
316
316
|
const rawName = input !== null && typeof input === 'object' && typeof input.name === 'string' ? input.name : ''
|
|
317
317
|
const name = rawName.replace(/\r?\n/g, ' ').trim()
|
|
318
318
|
if (name.length > MAX_NAME) {
|
|
319
|
-
return { ok: false, error: '保存被拒绝:模式名称过长(上限 ' + String(MAX_NAME) + ' 个字符)。' }
|
|
319
|
+
return { ok: false, code: 'nameTooLong', params: { max: MAX_NAME }, error: '保存被拒绝:模式名称过长(上限 ' + String(MAX_NAME) + ' 个字符)。' }
|
|
320
320
|
}
|
|
321
321
|
const rawDescription =
|
|
322
322
|
input !== null && typeof input === 'object' && typeof input.description === 'string' ? input.description : ''
|
|
323
323
|
const description = rawDescription.replace(/\r?\n/g, ' ').trim()
|
|
324
324
|
if (description.length > MAX_DESCRIPTION) {
|
|
325
|
-
return { ok: false, error: '保存被拒绝:模式描述过长(上限 ' + String(MAX_DESCRIPTION) + ' 个字符)。' }
|
|
325
|
+
return { ok: false, code: 'descriptionTooLong', params: { max: MAX_DESCRIPTION }, error: '保存被拒绝:模式描述过长(上限 ' + String(MAX_DESCRIPTION) + ' 个字符)。' }
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
const overrides = new Map()
|
|
@@ -337,7 +337,7 @@ export function saveState(rows, input) {
|
|
|
337
337
|
try {
|
|
338
338
|
composition = renderComposition(mode, overrides, { modeName: name, assistantId: id })
|
|
339
339
|
} catch (error) {
|
|
340
|
-
return { ok: false, error: '生成组成文件失败:' + describe(error) }
|
|
340
|
+
return { ok: false, code: 'renderFailed', params: { detail: describe(error) }, error: '生成组成文件失败:' + describe(error) }
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
// Self-check our own output before publishing it: a composition that lost its
|
|
@@ -346,7 +346,7 @@ export function saveState(rows, input) {
|
|
|
346
346
|
if (collectRows(composition).length === 0) throw new Error('生成的组成文件没有任何行')
|
|
347
347
|
readBaseComposition(mode)
|
|
348
348
|
} catch (error) {
|
|
349
|
-
return { ok: false, error: '生成结果自检失败,已放弃写入:' + describe(error) }
|
|
349
|
+
return { ok: false, code: 'selfCheckFailed', params: { detail: describe(error) }, error: '生成结果自检失败,已放弃写入:' + describe(error) }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
try {
|
|
@@ -356,7 +356,7 @@ export function saveState(rows, input) {
|
|
|
356
356
|
// 另外两条由 readState 对比补记(见 journal.mjs 的单写者说明)。
|
|
357
357
|
recordPrompt(directory, prompt, HISTORY_SOURCE.settings)
|
|
358
358
|
} catch (error) {
|
|
359
|
-
return { ok: false, error: '写入失败:' + describe(error) }
|
|
359
|
+
return { ok: false, code: 'writeFailed', params: { detail: describe(error) }, error: '写入失败:' + describe(error) }
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
// A name the user cleared is left alone rather than written as an empty scalar:
|
|
@@ -370,6 +370,8 @@ export function saveState(rows, input) {
|
|
|
370
370
|
ok: true,
|
|
371
371
|
id,
|
|
372
372
|
mode,
|
|
373
|
+
code: 'saved',
|
|
374
|
+
params: { name: name === '' ? id : name, mode },
|
|
373
375
|
note: '已保存(' + (name === '' ? id : name) + ',基础模式 ' + mode + ')。新建会话即生效,当前会话保持原配置。',
|
|
374
376
|
}
|
|
375
377
|
}
|
|
@@ -390,15 +392,15 @@ export function saveState(rows, input) {
|
|
|
390
392
|
export function createAssistant(rows, input, templateDir = packagedPresetDir()) {
|
|
391
393
|
const rawName = input !== null && typeof input === 'object' && typeof input.name === 'string' ? input.name : ''
|
|
392
394
|
const name = rawName.replace(/\r?\n/g, ' ').trim()
|
|
393
|
-
if (name === '') return { ok: false, error: '请先给新助手起个名字。' }
|
|
395
|
+
if (name === '') return { ok: false, code: 'nameRequired', error: '请先给新助手起个名字。' }
|
|
394
396
|
if (name.length > MAX_NAME) {
|
|
395
|
-
return { ok: false, error: '名字太长了(上限 ' + String(MAX_NAME) + ' 个字符)。' }
|
|
397
|
+
return { ok: false, code: 'nameTooLong', params: { max: MAX_NAME }, error: '名字太长了(上限 ' + String(MAX_NAME) + ' 个字符)。' }
|
|
396
398
|
}
|
|
397
399
|
const rawDescription =
|
|
398
400
|
input !== null && typeof input === 'object' && typeof input.description === 'string' ? input.description : ''
|
|
399
401
|
const description = rawDescription.replace(/\r?\n/g, ' ').trim()
|
|
400
402
|
if (description.length > MAX_DESCRIPTION) {
|
|
401
|
-
return { ok: false, error: '描述太长了(上限 ' + String(MAX_DESCRIPTION) + ' 个字符)。' }
|
|
403
|
+
return { ok: false, code: 'descriptionTooLong', params: { max: MAX_DESCRIPTION }, error: '描述太长了(上限 ' + String(MAX_DESCRIPTION) + ' 个字符)。' }
|
|
402
404
|
}
|
|
403
405
|
|
|
404
406
|
const root = userPresetRoot(rows)
|
|
@@ -410,7 +412,7 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
|
|
|
410
412
|
// The roster can lag a directory it skipped (a hand-made one with no
|
|
411
413
|
// composition). Refuse the name rather than half-own that directory.
|
|
412
414
|
if (existsSync(join(root, id))) {
|
|
413
|
-
return { ok: false, error: '目录已存在,请换一个名字:' + join(root, id) }
|
|
415
|
+
return { ok: false, code: 'dirExists', params: { path: join(root, id) }, error: '目录已存在,请换一个名字:' + join(root, id) }
|
|
414
416
|
}
|
|
415
417
|
|
|
416
418
|
// A "duplicate" carries the source's prompt, base mode and row switches into a
|
|
@@ -424,7 +426,7 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
|
|
|
424
426
|
const fromDir = assistantDir(rows, from)
|
|
425
427
|
if (fromDir === undefined) return unknownAssistant(from)
|
|
426
428
|
const fromComposition = compositionFile(fromDir)
|
|
427
|
-
if (!existsSync(fromComposition)) return { ok: false, error: '找不到组成文件:' + fromComposition }
|
|
429
|
+
if (!existsSync(fromComposition)) return { ok: false, code: 'compositionMissing', params: { path: fromComposition }, error: '找不到组成文件:' + fromComposition }
|
|
428
430
|
const text = readFileSync(fromComposition, 'utf8')
|
|
429
431
|
const sourceMode = modeOf(text)
|
|
430
432
|
const sourcePrompt = readPrompt(fromDir)
|
|
@@ -453,7 +455,7 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
|
|
|
453
455
|
try {
|
|
454
456
|
writeAtomic(promptFile(created.dir), source.prompt)
|
|
455
457
|
} catch (error) {
|
|
456
|
-
return { ok: false, error: '写入提示词失败:' + describe(error) }
|
|
458
|
+
return { ok: false, code: 'promptWriteFailed', params: { detail: describe(error) }, error: '写入提示词失败:' + describe(error) }
|
|
457
459
|
}
|
|
458
460
|
}
|
|
459
461
|
const metaResult = writePresetMeta(name, description === '' && source !== null ? source.description : description, created.dir)
|
|
@@ -463,6 +465,9 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
|
|
|
463
465
|
ok: true,
|
|
464
466
|
id,
|
|
465
467
|
name,
|
|
468
|
+
code: source === null ? 'created' : 'duplicated',
|
|
469
|
+
// D5:文案里用**显示名**而不是内部目录 id。
|
|
470
|
+
params: source === null ? { name } : { name, from: source === null ? '' : from },
|
|
466
471
|
note: source === null
|
|
467
472
|
? '已创建「' + name + '」。它的系统提示词现在是模板默认文本;写好后新建会话即可选择它。'
|
|
468
473
|
: '已复制出「' + name + '」:提示词、基础模式与插件开关都来自「' + from + '」,之后各改各的,互不影响。',
|
|
@@ -485,14 +490,14 @@ export async function deleteAssistant(rows, input, agentPresets) {
|
|
|
485
490
|
const id = input !== null && typeof input === 'object' && typeof input.id === 'string' ? input.id : ''
|
|
486
491
|
if (assistantDir(rows, id) === undefined) return unknownAssistant(id)
|
|
487
492
|
if (typeof agentPresets?.remove !== 'function') {
|
|
488
|
-
return { ok: false, error: '当前 DSH 版本没有 agentPresets.remove(),无法删除。' }
|
|
493
|
+
return { ok: false, code: 'noRemoveApi', error: '当前 DSH 版本没有 agentPresets.remove(),无法删除。' }
|
|
489
494
|
}
|
|
490
495
|
try {
|
|
491
496
|
await agentPresets.remove(id)
|
|
492
497
|
} catch (error) {
|
|
493
|
-
return { ok: false, error: '删除失败:' + describe(error) }
|
|
498
|
+
return { ok: false, code: 'deleteFailed', params: { detail: describe(error) }, error: '删除失败:' + describe(error) }
|
|
494
499
|
}
|
|
495
|
-
return { ok: true, id, note: '已删除「' + id + '」。正在使用它的会话不受影响;新建会话时不再出现。' }
|
|
500
|
+
return { ok: true, id, code: 'deleted', params: { name: id }, note: '已删除「' + id + '」。正在使用它的会话不受影响;新建会话时不再出现。' }
|
|
496
501
|
}
|
|
497
502
|
|
|
498
503
|
/**
|
|
@@ -651,7 +656,7 @@ export function apply(ctx) {
|
|
|
651
656
|
const directory = assistantDir(await roster(), id)
|
|
652
657
|
if (directory === undefined) return json(unknownAssistant(id), 404)
|
|
653
658
|
const text = readVersion(directory, url.searchParams.get('n') ?? '')
|
|
654
|
-
if (text === null) return json({ ok: false, error: '找不到这个版本(历史可能已被上限裁剪)。' }, 404)
|
|
659
|
+
if (text === null) return json({ ok: false, code: 'versionMissing', error: '找不到这个版本(历史可能已被上限裁剪)。' }, 404)
|
|
655
660
|
return json({ ok: true, id, n: url.searchParams.get('n'), text })
|
|
656
661
|
}
|
|
657
662
|
|
|
@@ -669,7 +674,7 @@ export function apply(ctx) {
|
|
|
669
674
|
try {
|
|
670
675
|
parsed = await request.json()
|
|
671
676
|
} catch {
|
|
672
|
-
return json({ ok: false, error: '请求体不是合法 JSON' }, 400)
|
|
677
|
+
return json({ ok: false, code: 'badJson', error: '请求体不是合法 JSON' }, 400)
|
|
673
678
|
}
|
|
674
679
|
await ensureShipped()
|
|
675
680
|
if (pathname === STATE_PATH) {
|
|
@@ -716,12 +721,56 @@ export function apply(ctx) {
|
|
|
716
721
|
* prompt,而这个文件正是"用户的提示词"。
|
|
717
722
|
*/
|
|
718
723
|
function writeAtomic(file, text) {
|
|
719
|
-
// 临时名必须**每个请求唯一**:只带 pid
|
|
720
|
-
//
|
|
721
|
-
//
|
|
724
|
+
// 临时名必须**每个请求唯一**:只带 pid 时,同一进程内两个并发保存会争同一个临时名。
|
|
725
|
+
// 但随机后缀只消除 tmp-vs-tmp 竞争 —— Windows 上**两个 rename 指向同一目标**仍会以
|
|
726
|
+
// EPERM/EBUSY 失败(实测:10 并发保存 2 例 400),所以还要短退避重试。
|
|
722
727
|
const temporary = `${file}.tmp-${String(process.pid)}-${randomBytes(4).toString('hex')}`
|
|
723
728
|
writeFileSync(temporary, text, 'utf8')
|
|
724
|
-
|
|
729
|
+
try {
|
|
730
|
+
renameWithRetry(temporary, file)
|
|
731
|
+
} catch (error) {
|
|
732
|
+
// 失败必须清掉临时文件:助手目录会被 agent-presets 扫描,孤儿 tmp 是脏残留(实测留下过
|
|
733
|
+
// `agent.cordis.yml.tmp-135052-36a5bf5c`)。
|
|
734
|
+
try {
|
|
735
|
+
rmSync(temporary, { force: true })
|
|
736
|
+
} catch {
|
|
737
|
+
/* 清理失败不再掩盖原始错误 */
|
|
738
|
+
}
|
|
739
|
+
throw error
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** 同步退避:这条写路径本身是同步的,等一小会儿比把整个调用链改成异步更合适。 */
|
|
744
|
+
function sleepSync(ms) {
|
|
745
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* `rename` 带重试。
|
|
750
|
+
*
|
|
751
|
+
* Windows 上并发 rename 到同一目标会短暂报 `EPERM`/`EBUSY`/`EACCES`(目标被另一个 rename 持有),
|
|
752
|
+
* 退避几毫秒就能跨过这个窗口;其它错误立刻抛出,不做无谓等待。`rename`/`sleep` 可注入,
|
|
753
|
+
* 于是这条重试逻辑在 Linux 上也能被测试钉住(见 test/journal.test.mjs 的对应条目)。
|
|
754
|
+
*
|
|
755
|
+
* @param {string} from - 临时文件名(已写好内容)。
|
|
756
|
+
* @param {string} to - 目标文件名。
|
|
757
|
+
* @returns {void}
|
|
758
|
+
*/
|
|
759
|
+
export function renameWithRetry(from, to, options = {}) {
|
|
760
|
+
const rename = typeof options.rename === 'function' ? options.rename : renameSync
|
|
761
|
+
const sleep = typeof options.sleep === 'function' ? options.sleep : sleepSync
|
|
762
|
+
const attempts = Number.isInteger(options.attempts) ? options.attempts : 5
|
|
763
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
764
|
+
try {
|
|
765
|
+
rename(from, to)
|
|
766
|
+
return
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const code = error !== null && typeof error === 'object' ? error.code : undefined
|
|
769
|
+
const retryable = code === 'EPERM' || code === 'EBUSY' || code === 'EACCES'
|
|
770
|
+
if (retryable !== true || attempt >= attempts) throw error
|
|
771
|
+
sleep(20 * attempt)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
725
774
|
}
|
|
726
775
|
|
|
727
776
|
/** `error` as a readable string, without assuming it is an Error. */
|
package/journal.mjs
CHANGED
|
@@ -23,9 +23,30 @@
|
|
|
23
23
|
* 但这里记录的是**已发生的事实**而不是待确认的提议 —— 我们的场景是"改了什么要看得见、回得去",
|
|
24
24
|
* 不需要它的确认闸门。
|
|
25
25
|
*/
|
|
26
|
-
import {
|
|
26
|
+
import { randomBytes } from 'node:crypto'
|
|
27
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
27
28
|
import { join } from 'node:path'
|
|
28
29
|
|
|
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
|
+
|
|
29
50
|
/** 每个助手目录下的日志文件名。 */
|
|
30
51
|
export const HISTORY_NAME = 'prompt-history.jsonl'
|
|
31
52
|
|
|
@@ -89,16 +110,29 @@ export function recordPrompt(directory, text, by = HISTORY_SOURCE.settings) {
|
|
|
89
110
|
|
|
90
111
|
const at = new Date().toISOString()
|
|
91
112
|
const n = entries.length === 0 ? 1 : entries[entries.length - 1].n + 1
|
|
113
|
+
// `bytes` 是给人看的体积,所以按 **UTF-8 字节**计(原先用 text.length = UTF-16 码元,
|
|
114
|
+
// CJK 内容下显示值只有真实字节的约 1/3,而界面标注是 "B")。
|
|
92
115
|
const lines = entries.slice(-(HISTORY_MAX - 1)).map((entry) =>
|
|
93
|
-
JSON.stringify({ n: entry.n, at: entry.at, by: entry.by, bytes: entry.text
|
|
116
|
+
JSON.stringify({ n: entry.n, at: entry.at, by: entry.by, bytes: Buffer.byteLength(entry.text, 'utf8'), text: entry.text }),
|
|
94
117
|
)
|
|
95
|
-
lines.push(JSON.stringify({ n, at, by, bytes: text
|
|
118
|
+
lines.push(JSON.stringify({ n, at, by, bytes: Buffer.byteLength(text, 'utf8'), text }))
|
|
96
119
|
|
|
97
120
|
// 唯一的整文件重写:顺手把上限外的旧条目丢掉,用原子写落盘(读取器不会看到写了一半的文件)。
|
|
98
121
|
const file = historyFile(directory)
|
|
99
|
-
|
|
122
|
+
// 与宿主半同一条纪律:随机临时名(消除 tmp-vs-tmp 碰撞)+ 重试(Windows 上目标被并发 rename
|
|
123
|
+
// 持有时会短暂 EPERM)+ 失败清理。
|
|
124
|
+
const temporary = `${file}.tmp-${String(process.pid)}-${randomBytes(4).toString('hex')}`
|
|
100
125
|
writeFileSync(temporary, lines.join('\n') + '\n', 'utf8')
|
|
101
|
-
|
|
126
|
+
try {
|
|
127
|
+
renameWithRetry(temporary, file)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
try {
|
|
130
|
+
rmSync(temporary, { force: true })
|
|
131
|
+
} catch {
|
|
132
|
+
/* 不掩盖原始错误 */
|
|
133
|
+
}
|
|
134
|
+
throw error
|
|
135
|
+
}
|
|
102
136
|
return { recorded: true, at, n }
|
|
103
137
|
}
|
|
104
138
|
|
|
@@ -119,7 +153,7 @@ export function listHistory(directory, limit = HISTORY_MAX) {
|
|
|
119
153
|
n: entry.n,
|
|
120
154
|
at: entry.at,
|
|
121
155
|
by: entry.by,
|
|
122
|
-
bytes: entry.text
|
|
156
|
+
bytes: Buffer.byteLength(entry.text, 'utf8'),
|
|
123
157
|
preview: firstLine.trim().slice(0, 80),
|
|
124
158
|
})
|
|
125
159
|
}
|
package/locales.mjs
CHANGED
|
@@ -52,6 +52,31 @@ export const zh = {
|
|
|
52
52
|
'msg.reorderFailed': '调整顺序失败',
|
|
53
53
|
'msg.imported': '已导入到编辑器(还没有保存):检查后点「保存」。',
|
|
54
54
|
'msg.importFailed': '导入失败',
|
|
55
|
+
'api.badDirection': '未知的排序方向:{direction}',
|
|
56
|
+
'api.unknownAssistant': '找不到助手「{id}」:这一页只管理本工具创建的助手(目录里有 prompt.md,且组成文件用 prompt-reader.mjs 注入身份)。',
|
|
57
|
+
'api.alreadyFirst': '「{name}」已经在最前面。',
|
|
58
|
+
'api.alreadyLast': '「{name}」已经在最后面。',
|
|
59
|
+
'api.saved': '已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。',
|
|
60
|
+
'api.created': '已创建「{name}」。现在可以为它写系统提示词。',
|
|
61
|
+
'api.duplicated': '已复制自「{from}」。两份从此各改各的。',
|
|
62
|
+
'api.deleted': '已删除「{name}」。正在使用它的会话不受影响;新建会话时它不再出现。',
|
|
63
|
+
'api.reordered': '顺序已保存:新建会话时的模式选择器按这个顺序排列。',
|
|
64
|
+
'api.nameRequired': '请先给新助手起个名字。',
|
|
65
|
+
'api.nameTooLong': '名字太长了(上限 {max} 个字符)。',
|
|
66
|
+
'api.descriptionTooLong': '描述太长了(上限 {max} 个字符)。',
|
|
67
|
+
'api.promptEmpty': '保存被拒绝:系统提示词为空。留空不会清空身份,读取器会沿用上一版。',
|
|
68
|
+
'api.badMode': '未知的基础模式:{mode}',
|
|
69
|
+
'api.dirExists': '目录已存在,请换一个名字:{path}',
|
|
70
|
+
'api.compositionMissing': '找不到组成文件:{path}',
|
|
71
|
+
'api.writeFailed': '写入失败:{detail}',
|
|
72
|
+
'api.promptWriteFailed': '写入提示词失败:{detail}',
|
|
73
|
+
'api.renderFailed': '生成组成文件失败:{detail}',
|
|
74
|
+
'api.selfCheckFailed': '生成结果自检失败,已放弃写入:{detail}',
|
|
75
|
+
'api.promptReadFailed': '读取提示词失败:{detail}',
|
|
76
|
+
'api.noRemoveApi': '当前 DSH 版本没有 agentPresets.remove(),无法删除。',
|
|
77
|
+
'api.deleteFailed': '删除失败:{detail}',
|
|
78
|
+
'api.versionMissing': '找不到这个版本(历史可能已被上限裁剪)。',
|
|
79
|
+
'api.badJson': '请求体不是合法 JSON',
|
|
55
80
|
'warn.personaOffWithPrompt': '「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 你写的提示词现在不起作用。要么打开这一行,要么清空提示词。',
|
|
56
81
|
'warn.toolOff': '「custom_prompt 工具」这一行是关的:会话里无法让 agent 改提示词,只能在本页改。',
|
|
57
82
|
'warn.noDescription': '没有描述:新建会话的模式选择器里会显示成「暂无描述」。',
|
|
@@ -140,6 +165,8 @@ export const zh = {
|
|
|
140
165
|
'row.tool-subagent-claude-code.label': 'Claude Code 子代理',
|
|
141
166
|
'row.tool-subagent-claude-code.note': '默认关闭:需要先安装对应 Bundle',
|
|
142
167
|
'row.workflow-ptc.label': '工作流引擎',
|
|
168
|
+
'row.workflow-worker-thread.label': '工作流 Worker 线程',
|
|
169
|
+
'row.workflow-worker-thread.note': '把工作流跑在独立的 worker 线程里',
|
|
143
170
|
'row.tool-workflow.label': '工作流工具',
|
|
144
171
|
'row.tool-ralph.label': 'Ralph 工作流',
|
|
145
172
|
'row.tool-ralph.note': '默认关闭',
|
|
@@ -212,6 +239,31 @@ export const en = {
|
|
|
212
239
|
'msg.reorderFailed': 'Could not reorder',
|
|
213
240
|
'msg.imported': 'Imported into the editor (not saved yet) — review it, then click Save.',
|
|
214
241
|
'msg.importFailed': 'Import failed',
|
|
242
|
+
'api.badDirection': 'Unknown reorder direction: {direction}',
|
|
243
|
+
'api.unknownAssistant': 'No assistant 「{id}」: this page only manages the assistants it created (a directory with prompt.md whose composition injects the identity through prompt-reader.mjs).',
|
|
244
|
+
'api.alreadyFirst': '「{name}」 is already first.',
|
|
245
|
+
'api.alreadyLast': '「{name}」 is already last.',
|
|
246
|
+
'api.saved': 'Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.',
|
|
247
|
+
'api.created': 'Created 「{name}」. You can write its system prompt now.',
|
|
248
|
+
'api.duplicated': 'Copied from 「{from}」. The two are independent from now on.',
|
|
249
|
+
'api.deleted': 'Deleted 「{name}」. Sessions already using it keep running; it no longer appears for new sessions.',
|
|
250
|
+
'api.reordered': 'Order saved: the new-session mode picker follows it.',
|
|
251
|
+
'api.nameRequired': 'Give the new assistant a name first.',
|
|
252
|
+
'api.nameTooLong': 'That name is too long (limit {max} characters).',
|
|
253
|
+
'api.descriptionTooLong': 'That description is too long (limit {max} characters).',
|
|
254
|
+
'api.promptEmpty': 'Save rejected: the system prompt is empty. Emptying it would not clear the identity — the reader keeps the last good text.',
|
|
255
|
+
'api.badMode': 'Unknown base mode: {mode}',
|
|
256
|
+
'api.dirExists': 'That directory already exists — pick another name: {path}',
|
|
257
|
+
'api.compositionMissing': 'Composition file not found: {path}',
|
|
258
|
+
'api.writeFailed': 'Write failed: {detail}',
|
|
259
|
+
'api.promptWriteFailed': 'Writing the prompt failed: {detail}',
|
|
260
|
+
'api.renderFailed': 'Rendering the composition failed: {detail}',
|
|
261
|
+
'api.selfCheckFailed': 'The generated composition failed its self-check, so nothing was written: {detail}',
|
|
262
|
+
'api.promptReadFailed': 'Reading the prompt failed: {detail}',
|
|
263
|
+
'api.noRemoveApi': 'This DSH build has no agentPresets.remove(), so deleting is unavailable.',
|
|
264
|
+
'api.deleteFailed': 'Delete failed: {detail}',
|
|
265
|
+
'api.versionMissing': 'That version is gone (the history is capped).',
|
|
266
|
+
'api.badJson': 'The request body is not valid JSON',
|
|
215
267
|
'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.',
|
|
216
268
|
'warn.toolOff': 'The "custom_prompt tool" row is off: the agent cannot change the prompt from inside a session, only this page can.',
|
|
217
269
|
'warn.noDescription': 'No description: the new-session mode picker will show it as "no description yet".',
|
|
@@ -303,6 +355,8 @@ export const en = {
|
|
|
303
355
|
'row.tool-subagent-claude-code.label': 'Claude Code subagent',
|
|
304
356
|
'row.tool-subagent-claude-code.note': 'Off by default: install the matching Bundle first',
|
|
305
357
|
'row.workflow-ptc.label': 'Workflow engine',
|
|
358
|
+
'row.workflow-worker-thread.label': 'Workflow worker thread',
|
|
359
|
+
'row.workflow-worker-thread.note': 'Runs workflows on a separate worker thread',
|
|
306
360
|
'row.tool-workflow.label': 'Workflow tool',
|
|
307
361
|
'row.tool-ralph.label': 'Ralph workflow',
|
|
308
362
|
'row.tool-ralph.note': 'Off by default',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test": "node ../test/run.mjs"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@deepseek-ai/dsh": ">=0.1.
|
|
63
|
+
"@deepseek-ai/dsh": ">=0.1.5-rc.2 <0.2.0-0"
|
|
64
64
|
},
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public",
|
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
|
-
"dsh": ">=0.1.
|
|
75
|
+
"dsh": ">=0.1.5-rc.2 <0.2.0-0"
|
|
76
76
|
}
|
|
77
77
|
}
|