dsh-custom-mode 1.13.0 → 2.0.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/composition.mjs CHANGED
@@ -1025,6 +1025,35 @@ export function overridesOf(text, modeId) {
1025
1025
  return overrides
1026
1026
  }
1027
1027
 
1028
+ /**
1029
+ * 按允许名单生成显式开关。
1030
+ *
1031
+ * 只写下和出厂状态不同的行,这样没被点名的行仍保持 `!!js` 平台条件。名单里没有、而出厂是开的行,
1032
+ * 写成关闭。上游以后新增的行不在名单里,因此对「写作 / 聊天」默认关闭。
1033
+ *
1034
+ * @param {string} modeId - one of {@link BASE_MODES}.
1035
+ * @param {string[]} allowIds - 希望保持可用的行 id。`persona` 与 `custom-prompt-tool` 总会被加进去。
1036
+ * @returns {Map<string, boolean>}
1037
+ */
1038
+ export function allowlistOverrides(modeId, allowIds) {
1039
+ const allow = new Set(Array.isArray(allowIds) ? allowIds : [])
1040
+ allow.add('persona')
1041
+ allow.add('custom-prompt-tool')
1042
+ const rows = flattenRowObjects(collectRows(readBaseComposition(modeId)))
1043
+ for (const extra of EXTRA_ROWS) {
1044
+ if (rows.has(extra.id) === false) {
1045
+ rows.set(extra.id, { id: extra.id, disabled: false, disabledExpression: null })
1046
+ }
1047
+ }
1048
+ const overrides = new Map()
1049
+ for (const [id, row] of rows) {
1050
+ const want = allow.has(id)
1051
+ const shippedOn = row.disabled !== true
1052
+ if (want !== shippedOn) overrides.set(id, want)
1053
+ }
1054
+ return overrides
1055
+ }
1056
+
1028
1057
  /**
1029
1058
  * Rows this feature always adds, for the settings UI to show alongside the base
1030
1059
  * mode's own rows.
package/index.mjs CHANGED
@@ -9,8 +9,11 @@
9
9
  * GET /api/custom-mode/history?id=…&n=… — one prompt revision (metadata comes with the state).
10
10
  * POST /api/custom-mode/state — { id, mode, overrides, prompt, name, description }:
11
11
  * validate, render a fresh `agent.cordis.yml`, write both files.
12
- * POST /api/custom-mode/create — { name, description }: seed a new assistant from the
13
- * packaged template and give it the standard base mode.
12
+ * POST /api/custom-mode/create — { name, description, posture?, locale? }: seed a new
13
+ * assistant. Omitting `posture` (or sending `develop`) is the
14
+ * historical path: the packaged prompt and the standard base,
15
+ * with shipped row states. Other postures still use an official
16
+ * base; they only change the starter prompt and the initial switches.
14
17
  * POST /api/custom-mode/delete — { id }: remove a locally authored assistant.
15
18
  * POST /api/custom-mode/reorder — { id, direction }: move one assistant up/down in the
16
19
  * picker order by writing `order` into each `preset.yml`.
@@ -52,6 +55,7 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
52
55
  import { dirname, join } from 'node:path'
53
56
  import { PROMPT_PATH, COMPOSITION_PATH, ROUTE_PATH, PRESET_DIR } from './paths.mjs'
54
57
  import {
58
+ allowlistOverrides,
55
59
  applyRowExclusivity,
56
60
  BASE_MODE_IDS,
57
61
  BASE_MODES,
@@ -72,6 +76,7 @@ import {
72
76
  import { presetMetaText, readPresetMeta, writePresetMeta, presetMetaPath, PRESET_META_PATH } from './meta.mjs'
73
77
  import { listHistory, readVersion, recordExternalChange, recordPrompt, HISTORY_SOURCE } from './journal.mjs'
74
78
  import { packagedPresetDir, starterComposition } from './seed.mjs'
79
+ import { postureById, postureLocale, posturePrompt, publicPostures } from './postures.mjs'
75
80
  import {
76
81
  allocateId,
77
82
  assistantDir,
@@ -196,6 +201,33 @@ export function promptFile(directory) {
196
201
  return join(directory, 'prompt.md')
197
202
  }
198
203
 
204
+ /**
205
+ * The prompt this assistant was created with.
206
+ *
207
+ * Kept beside `prompt.md` and never updated by a later save, so "restore the
208
+ * starter" means that text on Windows and Linux alike. Assistants created
209
+ * before this file existed fall back to the packaged coding prompt, which is
210
+ * what they were seeded with.
211
+ */
212
+ export const STARTER_PROMPT_FILE = 'prompt.starter.md'
213
+
214
+ /** @param {string} directory */
215
+ export function starterPromptFile(directory) {
216
+ return join(directory, STARTER_PROMPT_FILE)
217
+ }
218
+
219
+ /**
220
+ * @param {string} directory
221
+ * @returns {string | null}
222
+ */
223
+ function readStarterPrompt(directory) {
224
+ try {
225
+ return readFileSync(starterPromptFile(directory), 'utf8')
226
+ } catch {
227
+ return null
228
+ }
229
+ }
230
+
199
231
  /** Absolute path of one preset directory's composition file. */
200
232
  export function compositionFile(directory) {
201
233
  return join(directory, COMPOSITION_FILE)
@@ -398,7 +430,9 @@ export function readState(rows, id, options = {}) {
398
430
  presetMetaPath: presetMetaPath(directory),
399
431
  // 回退用的出厂文本。它不是"当前值",页面只把它填进编辑器,保存前不落盘 —— 所以
400
432
  // 一次误点不会破坏任何东西(重新读取即可丢弃)。
401
- factoryPrompt: typeof options.factoryPrompt === 'string' ? options.factoryPrompt : null,
433
+ // 回退用的起步文本。优先是这个助手新建时写下的 prompt.starter.md;
434
+ // 没有该文件的旧助手用打包的开发提示词,那就是它们当初得到的文本。
435
+ factoryPrompt: readStarterPrompt(directory) ?? (typeof options.factoryPrompt === 'string' ? options.factoryPrompt : null),
402
436
  // 改动历史(只有元数据,正文按需取:见 GET /custom-mode/history)。
403
437
  history: listHistory(directory),
404
438
  // 本插件版本 + 本机装的那条 dsh 线上无法解析的行(页面据此显示版本与"按本线修复")。
@@ -436,7 +470,7 @@ export function readList(rows) {
436
470
  const list = assistantsFromRoster(rows).map((item) =>
437
471
  typeof item.broken === 'string' ? { ...item, brokenRows: brokenRowIds(item.broken) } : item,
438
472
  )
439
- return { ok: true, assistants: list, root: userPresetRoot(rows) }
473
+ return { ok: true, assistants: list, postures: publicPostures(), root: userPresetRoot(rows) }
440
474
  }
441
475
 
442
476
  /**
@@ -754,11 +788,41 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
754
788
  }
755
789
  }
756
790
 
791
+ // 复制沿用源助手,不套姿态。省略姿态或显式 `develop` 与旧版创建相同:标准底、出厂开关、出厂提示词。
792
+ const requestedPosture = source !== null
793
+ ? ''
794
+ : input !== null && typeof input === 'object' && typeof input.posture === 'string'
795
+ ? input.posture.trim()
796
+ : ''
797
+ const posture = requestedPosture === '' || requestedPosture === 'develop' ? null : postureById(requestedPosture)
798
+ if (requestedPosture !== '' && requestedPosture !== 'develop' && posture === null) {
799
+ return {
800
+ ok: false,
801
+ code: 'postureUnknown',
802
+ params: { posture: requestedPosture },
803
+ error: '没有这个姿态:' + requestedPosture,
804
+ }
805
+ }
806
+ if (posture !== null && posture.hands !== 'allow') {
807
+ return {
808
+ ok: false,
809
+ code: 'postureUnknown',
810
+ params: { posture: posture.id },
811
+ error: '没有这个姿态:' + posture.id,
812
+ }
813
+ }
814
+ const createMode = source !== null ? source.mode : posture === null ? 'standard' : posture.base
815
+
757
816
  let composition
758
817
  try {
759
- composition = source === null
760
- ? renderComposition('standard', new Map(), { modeName: name, assistantId: id })
761
- : renderComposition(source.mode, source.overrides, { modeName: name, assistantId: id })
818
+ if (source !== null) {
819
+ composition = renderComposition(source.mode, source.overrides, { modeName: name, assistantId: id })
820
+ } else if (posture !== null && posture.hands === 'allow') {
821
+ const exclusive = applyRowExclusivity(posture.base, allowlistOverrides(posture.base, posture.allow))
822
+ composition = renderComposition(posture.base, exclusive.overrides, { modeName: name, assistantId: id })
823
+ } else {
824
+ composition = renderComposition('standard', new Map(), { modeName: name, assistantId: id })
825
+ }
762
826
  } catch (error) {
763
827
  // 与保存路径的 `renderFailed` 同一套:**每个**用户可见的结果都要带 `code`,否则页面无从本地化
764
828
  // —— client.js 的 apiText 在没有 code 时直接退回下面这串中文,英文界面会在这一刻掉回中文
@@ -767,7 +831,7 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
767
831
  return {
768
832
  ok: false,
769
833
  code: error.code,
770
- params: { mode: 'standard', detail: describe(error) },
834
+ params: { mode: createMode, detail: describe(error) },
771
835
  error: '本机取不到出厂组成,暂时无法新建助手。',
772
836
  }
773
837
  }
@@ -789,15 +853,38 @@ export function createAssistant(rows, input, templateDir = packagedPresetDir())
789
853
  }
790
854
 
791
855
  // The template ships a starter prompt; a duplicate replaces it with the source's.
856
+ // 姿态只替换草稿,不改模板目录里的 prompt.md。`develop` 与省略姿态停在模板那一份。
792
857
  if (source !== null) {
793
858
  try {
794
859
  writeAtomic(promptFile(created.dir), source.prompt)
795
860
  } catch (error) {
796
861
  return abortCreate({ ok: false, code: 'promptWriteFailed', params: { detail: describe(error) }, error: '写入提示词失败:' + describe(error) })
797
862
  }
863
+ } else if (posture !== null) {
864
+ let starter
865
+ try {
866
+ starter = posturePrompt(posture.id, postureLocale(input.locale))
867
+ } catch (error) {
868
+ return abortCreate({
869
+ ok: false,
870
+ code: 'posturePromptMissing',
871
+ params: { posture: posture.id, detail: describe(error) },
872
+ error: '读不到这个姿态的起步提示词:' + describe(error),
873
+ })
874
+ }
875
+ try {
876
+ writeAtomic(promptFile(created.dir), starter)
877
+ } catch (error) {
878
+ return abortCreate({ ok: false, code: 'promptWriteFailed', params: { detail: describe(error) }, error: '写入提示词失败:' + describe(error) })
879
+ }
798
880
  }
799
881
  const metaResult = writePresetMeta(name, description === '' && source !== null ? source.description : description, created.dir)
800
882
  if (metaResult.ok !== true) return abortCreate(metaResult)
883
+ try {
884
+ writeAtomic(starterPromptFile(created.dir), readFileSync(promptFile(created.dir), 'utf8'))
885
+ } catch (error) {
886
+ return abortCreate({ ok: false, code: 'promptWriteFailed', params: { detail: describe(error) }, error: '写入提示词失败:' + describe(error) })
887
+ }
801
888
 
802
889
  return {
803
890
  ok: true,