dsh-custom-mode 1.9.2 → 1.9.3
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 -0
- package/client.js +9 -9
- package/composition.mjs +7 -4
- package/index.mjs +6 -2
- package/locales.mjs +9 -9
- package/package.json +2 -2
- package/seed.mjs +33 -4
package/assistants.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { dirname, join } from 'node:path'
|
|
|
30
30
|
import { dshHome, PRESET_DIR } from './paths.mjs'
|
|
31
31
|
import { readPresetMeta, writePresetMeta } from './meta.mjs'
|
|
32
32
|
import { writeAtomic } from './atomic.mjs'
|
|
33
|
+
import { unresolvableRows } from './composition.mjs'
|
|
33
34
|
import { seedPreset, seedPresetWithLog } from './seed.mjs'
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -303,6 +304,25 @@ export function seedOnActivation({ root, templateDir, composition, log = console
|
|
|
303
304
|
for (const error of result.errors) log(`custom-mode: 补全 ${dir} 失败 —— ${error}`)
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
// 启动告警:组成文件里有本机这条线解析不到的行时,平台会把整个预设判为 broken 并从选择器里**静默丢弃**。
|
|
308
|
+
// 以前这件事只在设置页可见 —— 用户从不打开设置页就完全无感知(外部评审实测)。这里至少在宿主日志里喊一声。
|
|
309
|
+
for (const dir of [...existing, join(root, LEGACY_ID)]) {
|
|
310
|
+
try {
|
|
311
|
+
const file = join(dir, 'agent.cordis.yml')
|
|
312
|
+
if (existsSync(file) === false) continue
|
|
313
|
+
const bad = unresolvableRows(readFileSync(file, 'utf8'))
|
|
314
|
+
if (bad.length > 0) {
|
|
315
|
+
log(
|
|
316
|
+
`custom-mode: ${dir} 的组成文件里有 ${String(bad.length)} 行在这条 dsh 线上无法解析` +
|
|
317
|
+
`(${bad.map((row) => row.id).join(', ')})—— 平台会把整个模式从新建会话的选择器里丢弃。` +
|
|
318
|
+
'打开 设置 → 自定义模式 并点「按本线修复」。',
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
/* 启动告警失败不能影响激活 */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
306
326
|
if (isSeeded(root)) return { created: false, repaired, adopted: false }
|
|
307
327
|
if (existing.length > 0) {
|
|
308
328
|
markSeeded(root)
|
package/client.js
CHANGED
|
@@ -293,15 +293,15 @@ try {
|
|
|
293
293
|
"msg.imported": "Imported into the editor (not saved yet) — review it, then click Save.",
|
|
294
294
|
"msg.importFailed": "Import failed",
|
|
295
295
|
"api.badDirection": "Unknown reorder direction: {direction}",
|
|
296
|
-
"api.unknownAssistant": "No assistant
|
|
297
|
-
"api.alreadyFirst": "
|
|
298
|
-
"api.alreadyLast": "
|
|
296
|
+
"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).",
|
|
297
|
+
"api.alreadyFirst": "{name} is already first.",
|
|
298
|
+
"api.alreadyLast": "{name} is already last.",
|
|
299
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.",
|
|
300
300
|
"api.unknownVariable": "{variable} is not a registered variable — rendering would fail every request in this mode. Available: {known}.",
|
|
301
301
|
"assistant.short": "Each assistant is its own mode: its own system prompt, base mode and plugin switches.",
|
|
302
302
|
"name.short": "Renaming only changes what is displayed — not the internal id or existing sessions.",
|
|
303
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
|
|
304
|
+
"mode.pendingRows": "Base changed to {mode}: the row list below is recomputed from the new base when you save.",
|
|
305
305
|
"rows.short": "Control which plugins this mode mounts, row by row; untouched rows keep the shipped default.",
|
|
306
306
|
"prompt.short": "Saving this text makes it the system prompt of this mode, and it takes effect on the next step.",
|
|
307
307
|
"aria.expandHint": "Show the full explanation",
|
|
@@ -323,9 +323,9 @@ try {
|
|
|
323
323
|
"meta.version": "Plugin version",
|
|
324
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.",
|
|
325
325
|
"api.saved": "Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.",
|
|
326
|
-
"api.created": "Created
|
|
327
|
-
"api.duplicated": "Copied from
|
|
328
|
-
"api.deleted": "Deleted
|
|
326
|
+
"api.created": "Created {name}. You can write its system prompt now.",
|
|
327
|
+
"api.duplicated": "Copied from {from}. The two are independent from now on.",
|
|
328
|
+
"api.deleted": "Deleted {name}. Sessions already using it keep running; it no longer appears for new sessions.",
|
|
329
329
|
"api.reordered": "Order saved: the new-session mode picker follows it.",
|
|
330
330
|
"api.nameRequired": "Give the new assistant a name first.",
|
|
331
331
|
"api.nameTooLong": "That name is too long (limit {max} characters).",
|
|
@@ -347,9 +347,9 @@ try {
|
|
|
347
347
|
"warn.toolOff": "The \"custom_prompt tool\" row is off: the agent cannot change the prompt from inside a session, only this page can.",
|
|
348
348
|
"warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
|
|
349
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
|
|
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.",
|
|
351
351
|
"warn.approvalGateMissing.label": "Approval gate is off",
|
|
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
|
|
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.",
|
|
353
353
|
"warn.noName": "No name: the mode picker will show the directory id (e.g. custom).",
|
|
354
354
|
"history.label": "Change history",
|
|
355
355
|
"history.pick": "Pick a version to load…",
|
package/composition.mjs
CHANGED
|
@@ -397,14 +397,17 @@ export function collectRows(text) {
|
|
|
397
397
|
* @param {boolean} nested - whether this call rewrites a group's contents.
|
|
398
398
|
* @returns {string} the rewritten level.
|
|
399
399
|
*/
|
|
400
|
-
function applyLevel(text, topLevel, overrides, nested) {
|
|
400
|
+
function applyLevel(text, topLevel, overrides, nested, replacePersona = true) {
|
|
401
401
|
const { lead, segments } = splitSegments(text, topLevel)
|
|
402
402
|
if (segments.length === 0) return text
|
|
403
403
|
const rendered = segments.map((segment) => {
|
|
404
404
|
// The persona row is always replaced by this feature's own reader row: the
|
|
405
405
|
// shipped one is a static-string persona whose text cannot be edited, so
|
|
406
406
|
// keeping it would silently disable the editable prompt.
|
|
407
|
-
|
|
407
|
+
// `replacePersona` 只有**重新渲染**时才为真:那时整段 persona 换成我们的读取器行是对的。
|
|
408
|
+
// 但"按本线修复"是**就地**手术,它必须连 persona 段里的注释都原样保留 ——
|
|
409
|
+
// 之前这里无条件替换,导致每次修复都会丢注释 / 或多复制一行身份注释(外部评审实测)。
|
|
410
|
+
if (!nested && segment.id === 'persona' && replacePersona === true) {
|
|
408
411
|
return setDisabled(PERSONA_ROW, overrides.get('persona'))
|
|
409
412
|
}
|
|
410
413
|
let body = segment.text
|
|
@@ -590,8 +593,8 @@ export function disableRowsInPlace(text, ids) {
|
|
|
590
593
|
if (wanted.length === 0) return text
|
|
591
594
|
// `applyLevel` 把 Map 的值直接交给 `setDisabled(...)`,所以值是**关闭**布尔(true = 关闭)。
|
|
592
595
|
const off = new Map(wanted.map((id) => [id, true]))
|
|
593
|
-
const top = applyLevel(text, true, off, false)
|
|
594
|
-
return applyLevel(top, false, off, false)
|
|
596
|
+
const top = applyLevel(text, true, off, false, false)
|
|
597
|
+
return applyLevel(top, false, off, false, false)
|
|
595
598
|
}
|
|
596
599
|
|
|
597
600
|
export function unresolvableRows(text) {
|
package/index.mjs
CHANGED
|
@@ -161,7 +161,7 @@ export function checkPromptText(text) {
|
|
|
161
161
|
return {
|
|
162
162
|
ok: false,
|
|
163
163
|
code: 'unknownVariable',
|
|
164
|
-
params: { variable: '{{' + variable + '}}', known: KNOWN_VARIABLES.map((item) => '{{' + item + '}}').join('
|
|
164
|
+
params: { variable: '{{' + variable + '}}', known: KNOWN_VARIABLES.map((item) => '{{' + item + '}}').join(', ') },
|
|
165
165
|
error:
|
|
166
166
|
'保存被拒绝:{{' +
|
|
167
167
|
variable +
|
|
@@ -372,8 +372,12 @@ export function saveState(rows, input) {
|
|
|
372
372
|
if (name.length > MAX_NAME) {
|
|
373
373
|
return { ok: false, code: 'nameTooLong', params: { max: MAX_NAME }, error: '保存被拒绝:模式名称过长(上限 ' + String(MAX_NAME) + ' 个字符)。' }
|
|
374
374
|
}
|
|
375
|
+
// 请求里**没带** description 时保留原值:API 调用方只改提示词,不该顺手把描述清空(外部评审实测)。
|
|
376
|
+
const meta = readPresetMeta(directory)
|
|
375
377
|
const rawDescription =
|
|
376
|
-
input !== null && typeof input === 'object' && typeof input.description === 'string'
|
|
378
|
+
input !== null && typeof input === 'object' && typeof input.description === 'string'
|
|
379
|
+
? input.description
|
|
380
|
+
: (typeof meta.description === 'string' ? meta.description : '')
|
|
377
381
|
const description = rawDescription.replace(/\r?\n/g, ' ').trim()
|
|
378
382
|
if (description.length > MAX_DESCRIPTION) {
|
|
379
383
|
return { ok: false, code: 'descriptionTooLong', params: { max: MAX_DESCRIPTION }, error: '保存被拒绝:模式描述过长(上限 ' + String(MAX_DESCRIPTION) + ' 个字符)。' }
|
package/locales.mjs
CHANGED
|
@@ -268,15 +268,15 @@ export const en = {
|
|
|
268
268
|
'msg.imported': 'Imported into the editor (not saved yet) — review it, then click Save.',
|
|
269
269
|
'msg.importFailed': 'Import failed',
|
|
270
270
|
'api.badDirection': 'Unknown reorder direction: {direction}',
|
|
271
|
-
'api.unknownAssistant': 'No assistant
|
|
272
|
-
'api.alreadyFirst': '
|
|
273
|
-
'api.alreadyLast': '
|
|
271
|
+
'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).',
|
|
272
|
+
'api.alreadyFirst': '{name} is already first.',
|
|
273
|
+
'api.alreadyLast': '{name} is already last.',
|
|
274
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.',
|
|
275
275
|
'api.unknownVariable': '{variable} is not a registered variable — rendering would fail every request in this mode. Available: {known}.',
|
|
276
276
|
'assistant.short': 'Each assistant is its own mode: its own system prompt, base mode and plugin switches.',
|
|
277
277
|
'name.short': 'Renaming only changes what is displayed — not the internal id or existing sessions.',
|
|
278
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
|
|
279
|
+
'mode.pendingRows': 'Base changed to {mode}: the row list below is recomputed from the new base when you save.',
|
|
280
280
|
'rows.short': 'Control which plugins this mode mounts, row by row; untouched rows keep the shipped default.',
|
|
281
281
|
'prompt.short': 'Saving this text makes it the system prompt of this mode, and it takes effect on the next step.',
|
|
282
282
|
'aria.expandHint': 'Show the full explanation',
|
|
@@ -298,9 +298,9 @@ export const en = {
|
|
|
298
298
|
'meta.version': 'Plugin version',
|
|
299
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.',
|
|
300
300
|
'api.saved': 'Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.',
|
|
301
|
-
'api.created': 'Created
|
|
302
|
-
'api.duplicated': 'Copied from
|
|
303
|
-
'api.deleted': 'Deleted
|
|
301
|
+
'api.created': 'Created {name}. You can write its system prompt now.',
|
|
302
|
+
'api.duplicated': 'Copied from {from}. The two are independent from now on.',
|
|
303
|
+
'api.deleted': 'Deleted {name}. Sessions already using it keep running; it no longer appears for new sessions.',
|
|
304
304
|
'api.reordered': 'Order saved: the new-session mode picker follows it.',
|
|
305
305
|
'api.nameRequired': 'Give the new assistant a name first.',
|
|
306
306
|
'api.nameTooLong': 'That name is too long (limit {max} characters).',
|
|
@@ -322,9 +322,9 @@ export const en = {
|
|
|
322
322
|
'warn.toolOff': 'The "custom_prompt tool" row is off: the agent cannot change the prompt from inside a session, only this page can.',
|
|
323
323
|
'warn.noDescription': 'No description: the new-session mode picker will show it as "no description yet".',
|
|
324
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
|
|
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.',
|
|
326
326
|
'warn.approvalGateMissing.label': 'Approval gate is off',
|
|
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
|
|
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.',
|
|
328
328
|
'warn.noName': 'No name: the mode picker will show the directory id (e.g. custom).',
|
|
329
329
|
'history.label': 'Change history',
|
|
330
330
|
'history.pick': 'Pick a version to load…',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"publishConfig": {
|
|
67
67
|
"access": "public",
|
|
68
|
-
"tag": "
|
|
68
|
+
"tag": "latest"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@deepseek-ai/dsh": {
|
package/seed.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* 3. **Idempotent.** A second activation writes nothing and changes nothing.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
25
25
|
import { renderComposition } from './composition.mjs'
|
|
26
26
|
import { writeAtomic } from './atomic.mjs'
|
|
27
27
|
import { dirname, join } from 'node:path'
|
|
@@ -91,8 +91,19 @@ export function starterComposition(options = {}) {
|
|
|
91
91
|
* @param {{composition?: string|null}} [options] - a rendered composition to write instead of copying
|
|
92
92
|
* `agent.cordis.yml`; see {@link starterComposition}.
|
|
93
93
|
*/
|
|
94
|
+
/**
|
|
95
|
+
* 我们自己的**代码**模块(相对于"用户数据")。
|
|
96
|
+
*
|
|
97
|
+
* 这两个文件是随助手目录一起分发的**代码**,不是用户内容:老版本创建的助手会一直留着旧副本,
|
|
98
|
+
* 于是 1.0.x / 1.1.x 首装的用户即使把插件升到最新,会话内改写提示词的**审批闸门仍然是缺的** ——
|
|
99
|
+
* 外部评审实测到了这一点(P1 安全)。所以它们在升级时**刷新**。
|
|
100
|
+
* `prompt.md` / `preset.yml` / `agent.cordis.yml` 是用户数据(提示词、开关、名字),保持"只补不缺"。
|
|
101
|
+
*/
|
|
102
|
+
const REFRESHABLE_MODULES = ['prompt-reader.mjs', 'prompt-tool.mjs']
|
|
103
|
+
|
|
94
104
|
export function seedPreset(presetDir, sourceDir = packagedPresetDir(), options = {}) {
|
|
95
105
|
const created = []
|
|
106
|
+
const refreshed = []
|
|
96
107
|
const kept = []
|
|
97
108
|
const errors = []
|
|
98
109
|
|
|
@@ -111,7 +122,22 @@ export function seedPreset(presetDir, sourceDir = packagedPresetDir(), options =
|
|
|
111
122
|
|
|
112
123
|
for (const name of PRESET_FILES) {
|
|
113
124
|
const target = join(presetDir, name)
|
|
125
|
+
const source = join(sourceDir, name)
|
|
114
126
|
if (existsSync(target)) {
|
|
127
|
+
// 代码模块:内容与包内不一致就升级(用原子写,带 Windows 重试)。
|
|
128
|
+
if (REFRESHABLE_MODULES.includes(name) && existsSync(source)) {
|
|
129
|
+
try {
|
|
130
|
+
const shipped = readFileSync(source, 'utf8')
|
|
131
|
+
if (readFileSync(target, 'utf8') !== shipped) {
|
|
132
|
+
writeAtomic(target, shipped)
|
|
133
|
+
refreshed.push(name)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
errors.push(`刷新 ${name} 失败: ${describe(error)}`)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
115
141
|
kept.push(name)
|
|
116
142
|
continue
|
|
117
143
|
}
|
|
@@ -126,19 +152,19 @@ export function seedPreset(presetDir, sourceDir = packagedPresetDir(), options =
|
|
|
126
152
|
continue
|
|
127
153
|
}
|
|
128
154
|
}
|
|
129
|
-
const source = join(sourceDir, name)
|
|
130
155
|
if (!existsSync(source)) {
|
|
131
156
|
errors.push(`包内缺少 ${name}`)
|
|
132
157
|
continue
|
|
133
158
|
}
|
|
134
159
|
try {
|
|
135
|
-
copyFileSync
|
|
160
|
+
// 原子写(内部带 Windows 重试):原先裸 copyFileSync 在两个实例共抢一个 DSH_HOME 首启时会 EBUSY。
|
|
161
|
+
writeAtomic(target, readFileSync(source, 'utf8'))
|
|
136
162
|
created.push(name)
|
|
137
163
|
} catch (error) {
|
|
138
164
|
errors.push(`写入 ${name} 失败: ${describe(error)}`)
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
|
-
return { created, kept, errors }
|
|
167
|
+
return { created, refreshed, kept, errors }
|
|
142
168
|
}
|
|
143
169
|
|
|
144
170
|
/**
|
|
@@ -163,6 +189,9 @@ export function seedPresetWithLog(presetDir, log = console.error, info = console
|
|
|
163
189
|
log(`custom-mode: 播种 preset 时出现意外错误(已忽略): ${describe(error)}`)
|
|
164
190
|
return { created: [], kept: [], errors: [describe(error)] }
|
|
165
191
|
}
|
|
192
|
+
if (result.refreshed !== undefined && result.refreshed.length > 0) {
|
|
193
|
+
info(`custom-mode: 已刷新 ${presetDir} 里的代码模块(${result.refreshed.join(', ')})`)
|
|
194
|
+
}
|
|
166
195
|
if (result.created.length > 0) {
|
|
167
196
|
info(`custom-mode: 已播种 preset 到 ${presetDir}(新建 ${result.created.length} 个文件: ${result.created.join(', ')})`)
|
|
168
197
|
}
|