dsh-custom-mode 1.9.13 → 1.9.14
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 +20 -3
- package/index.mjs +15 -6
- package/package.json +1 -1
package/assistants.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* file work, so it can be tested without a running harness.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
29
29
|
import { dirname, join } from 'node:path'
|
|
30
30
|
import { dshHome, PRESET_DIR } from './paths.mjs'
|
|
31
31
|
// `presetMetaPath` 曾被漏掉:reorderAssistant 用它做"快照 → 失败回滚",于是两处调用都抛
|
|
@@ -267,14 +267,31 @@ export function createAssistantDir({ root, id, composition, templateDir }) {
|
|
|
267
267
|
const dir = join(root, id)
|
|
268
268
|
if (existsSync(dir)) return { ok: false, code: 'dirExists', params: { path: dir }, error: `目录已存在:${dir}` }
|
|
269
269
|
|
|
270
|
+
// ★ 失败必须**撤干净**(issue #4)。上面的 existsSync 已经证明这个目录是本函数刚创建的,所以
|
|
271
|
+
// 失败时把它整体删掉是安全的。不这么做的话:用户看到"没建成",磁盘上却留下一个半成品目录,
|
|
272
|
+
// 重启后它作为助手出现在选择器里,而且这个名字被**永久占用**(再建同名直接报"目录已存在")。
|
|
273
|
+
// 删除本身也做失败兜底:删不掉时如实说明"留下了什么",而不是假装干净。
|
|
274
|
+
const rollback = (result) => {
|
|
275
|
+
try {
|
|
276
|
+
rmSync(dir, { recursive: true, force: true })
|
|
277
|
+
return result
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
...result,
|
|
281
|
+
params: { ...result.params, leftover: dir, cleanupFailed: describe(error) },
|
|
282
|
+
error: result.error + ';且未能清理半成品目录 ' + dir + '(' + describe(error) + ')',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
270
287
|
const seeded = seedPreset(dir, templateDir)
|
|
271
288
|
if (seeded.errors.length > 0) {
|
|
272
|
-
return { ok: false, code: 'seedFailed', params: { detail: seeded.errors.join(';') }, error: '复制模式模板失败:' + seeded.errors.join(';') }
|
|
289
|
+
return rollback({ ok: false, code: 'seedFailed', params: { detail: seeded.errors.join(';') }, error: '复制模式模板失败:' + seeded.errors.join(';') })
|
|
273
290
|
}
|
|
274
291
|
try {
|
|
275
292
|
writeFileSync(join(dir, COMPOSITION_FILE), composition, 'utf8')
|
|
276
293
|
} catch (error) {
|
|
277
|
-
return { ok: false, code: 'writeFailed', params: { detail: describe(error) }, error: '写入组成文件失败:' + describe(error) }
|
|
294
|
+
return rollback({ ok: false, code: 'writeFailed', params: { detail: describe(error) }, error: '写入组成文件失败:' + describe(error) })
|
|
278
295
|
}
|
|
279
296
|
return { ok: true, id, dir }
|
|
280
297
|
}
|
package/index.mjs
CHANGED
|
@@ -402,10 +402,12 @@ function serializedWrite(key, work) {
|
|
|
402
402
|
* 的行集合也就不变,这是所有选项里破坏性最小的一个。
|
|
403
403
|
*
|
|
404
404
|
* @param {string} directory - the assistant's preset directory.
|
|
405
|
-
* @param {{id: string, mode: string, prompt: string, name: string, description: string}} input
|
|
405
|
+
* @param {{id: string, mode: string, prompt: string, name: string, description: string, displayName?: string}} input
|
|
406
|
+
* - the validated save; `name` decides whether `preset.yml` is written, `displayName` is what the response
|
|
407
|
+
* reports (the disk name when the request omitted one — issue #9).
|
|
406
408
|
* @returns {object} the shape `saveState` returns, with `code: 'savedPromptOnly'`.
|
|
407
409
|
*/
|
|
408
|
-
export function savePromptOnly(directory, { id, mode, prompt, name, description }) {
|
|
410
|
+
export function savePromptOnly(directory, { id, mode, prompt, name, description, displayName }) {
|
|
409
411
|
// 与正常保存同一条纪律:提示词与 preset.yml 一起换名,失败则两个都不动。
|
|
410
412
|
let metaText = null
|
|
411
413
|
if (name !== '') {
|
|
@@ -431,7 +433,8 @@ export function savePromptOnly(directory, { id, mode, prompt, name, description
|
|
|
431
433
|
id,
|
|
432
434
|
mode,
|
|
433
435
|
code: 'savedPromptOnly',
|
|
434
|
-
|
|
436
|
+
// 响应里的名字用**磁盘上的那个**(issue #9):请求省略 name 时不该把 params.name 报成裸目录 id。
|
|
437
|
+
params: { name: (displayName === undefined || displayName === '' ? name : displayName) === '' ? id : (displayName === undefined || displayName === '' ? name : displayName), mode },
|
|
435
438
|
note: '已保存系统提示词(本机取不到基础模式的出厂组成,插件开关与基础模式未改动)。新建会话即生效。',
|
|
436
439
|
}
|
|
437
440
|
}
|
|
@@ -462,6 +465,12 @@ export function saveState(rows, input) {
|
|
|
462
465
|
}
|
|
463
466
|
// 请求里**没带** description 时保留原值:API 调用方只改提示词,不该顺手把描述清空(外部评审实测)。
|
|
464
467
|
const meta = readPresetMeta(directory)
|
|
468
|
+
// ★ 请求里没带 name(或页面清空)时,渲染组成仍要用**磁盘上那个名字**(issue #9)。
|
|
469
|
+
// 组成里 custom-prompt-tool 行的 config.modeName 是"会话内工具知道自己在改哪个助手"的唯一凭据;
|
|
470
|
+
// 用空串渲染会把它整块删掉,而 preset.yml 又(有意)保留旧名字 —— 两处就此不一致,
|
|
471
|
+
// 同时成功响应还会把 params.name 报成裸目录 id。请求**没带** name 与"用户把名字清空"在这里
|
|
472
|
+
// 是同一个语义:保持现状,而不是把它抹掉。
|
|
473
|
+
const effectiveName = name !== '' ? name : typeof meta.name === 'string' ? meta.name : ''
|
|
465
474
|
const rawDescription =
|
|
466
475
|
input !== null && typeof input === 'object' && typeof input.description === 'string'
|
|
467
476
|
? input.description
|
|
@@ -481,14 +490,14 @@ export function saveState(rows, input) {
|
|
|
481
490
|
|
|
482
491
|
let composition
|
|
483
492
|
try {
|
|
484
|
-
composition = renderComposition(mode, overrides, { modeName:
|
|
493
|
+
composition = renderComposition(mode, overrides, { modeName: effectiveName, assistantId: id })
|
|
485
494
|
} catch (error) {
|
|
486
495
|
// 拿不到出厂组成:这一页**降级**但不能全废 —— 提示词仍可单独保存(组成文件原样不动)。
|
|
487
496
|
// 页面在降级态下把基础模式与开关都设为不可编辑,所以开关集合必为空;HTTP API 直接调用
|
|
488
497
|
// 还想改开关时明确拒绝,而不是悄悄丢掉用户的意图。
|
|
489
498
|
if (isBaseCompositionUnavailable(error)) {
|
|
490
499
|
if (overrides.size === 0 && existsSync(compositionFile(directory))) {
|
|
491
|
-
return savePromptOnly(directory, { id, mode, prompt, name, description })
|
|
500
|
+
return savePromptOnly(directory, { id, mode, prompt, name, description, displayName: effectiveName })
|
|
492
501
|
}
|
|
493
502
|
return {
|
|
494
503
|
ok: false,
|
|
@@ -547,7 +556,7 @@ export function saveState(rows, input) {
|
|
|
547
556
|
id,
|
|
548
557
|
mode,
|
|
549
558
|
code: 'saved',
|
|
550
|
-
params: { name:
|
|
559
|
+
params: { name: effectiveName === '' ? id : effectiveName, mode },
|
|
551
560
|
note: '已保存(' + (name === '' ? id : name) + ',基础模式 ' + mode + ')。新建会话即生效,当前会话保持原配置。',
|
|
552
561
|
}
|
|
553
562
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.14",
|
|
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",
|