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 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 - the validated save.
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
- params: { name: name === '' ? id : name, mode },
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: name, assistantId: id })
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: name === '' ? id : name, mode },
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.13",
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",