dsh-custom-mode 0.1.6-alpha.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/client.js +652 -0
- package/composition.mjs +558 -0
- package/cordis.patch.yml +17 -0
- package/index.mjs +377 -0
- package/locales.mjs +219 -0
- package/meta.mjs +94 -0
- package/package.json +56 -0
- package/paths.mjs +38 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half: 「自定义模式」settings page.
|
|
3
|
+
*
|
|
4
|
+
* Serves one private HTTP route; the browser half calls it with `fetch`:
|
|
5
|
+
*
|
|
6
|
+
* GET — the current base mode, the row tree with switch states, prompt text.
|
|
7
|
+
* POST — { mode, overrides, prompt }: validate, render a fresh
|
|
8
|
+
* `agent.cordis.yml`, write both files.
|
|
9
|
+
*
|
|
10
|
+
* Why a private route instead of a Remote namespace or `dsh-settings`: this
|
|
11
|
+
* plugin then owns no Cordis service name and cannot collide with anything, and
|
|
12
|
+
* it stays independent of the settings API whose helper names differ between dsh
|
|
13
|
+
* releases (see docs/ARCHITECTURE.md).
|
|
14
|
+
*
|
|
15
|
+
* A route registered on the raw `webServer` table is however OUTSIDE the
|
|
16
|
+
* platform's browser-trust fence, which only guards the channels the Connection
|
|
17
|
+
* service mounts (`/`, `/api`, …). Measured on 0.1.6-alpha.1: an unauthenticated
|
|
18
|
+
* POST with `content-type: text/plain` rewrote `prompt.md`, while every official
|
|
19
|
+
* route answered 401 — and a cross-site form post needs no preflight, so any page
|
|
20
|
+
* the user visited could have rewritten their agent's system prompt. The route
|
|
21
|
+
* therefore runs the platform's own check first, via
|
|
22
|
+
* `ctx.connection.requestRejection(req)` (Host/Origin fence + browser auth),
|
|
23
|
+
* which is the same verdict `/api` gets.
|
|
24
|
+
*
|
|
25
|
+
* Storage is deliberately file-only and stateless: the composition file IS the
|
|
26
|
+
* saved state, so no second document can drift from it. The page derives which
|
|
27
|
+
* rows the user changed by diffing against the same shipped base mode.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
31
|
+
import { dirname } from 'node:path'
|
|
32
|
+
import { PROMPT_PATH, COMPOSITION_PATH, ROUTE_PATH, PRESET_DIR } from './paths.mjs'
|
|
33
|
+
import {
|
|
34
|
+
BASE_MODES,
|
|
35
|
+
collectRows,
|
|
36
|
+
renderComposition,
|
|
37
|
+
modeOf,
|
|
38
|
+
overridesOf,
|
|
39
|
+
readBaseComposition,
|
|
40
|
+
setShippedPresetsDir,
|
|
41
|
+
} from './composition.mjs'
|
|
42
|
+
import { readPresetMeta, writePresetMeta, PRESET_META_PATH } from './meta.mjs'
|
|
43
|
+
|
|
44
|
+
export { PROMPT_PATH, COMPOSITION_PATH, ROUTE_PATH, PRESET_DIR, PRESET_META_PATH }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Variable names the prompt renderer accepts, mirroring
|
|
48
|
+
* `VARIABLE_NAME = /^[a-z][a-z0-9_]*$/` in `@deepseek-ai/dsh-system-prompt`.
|
|
49
|
+
*/
|
|
50
|
+
const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Variables this deployment registers for every agent (`dsh-agent-loop`).
|
|
54
|
+
*
|
|
55
|
+
* The persona section renders with strict interpolation: an unknown `{{name}}`
|
|
56
|
+
* or a malformed group makes the renderer THROW, failing every model request in
|
|
57
|
+
* this mode. Rejecting it at the write is what keeps a typo from bricking it.
|
|
58
|
+
*
|
|
59
|
+
* Mirrors `checkPromptText` in the preset's `prompt-tool.mjs`; the duplication is
|
|
60
|
+
* deliberate so neither side depends on the other's install location.
|
|
61
|
+
*/
|
|
62
|
+
const KNOWN_VARIABLES = ['model', 'cwd', 'provider']
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check prompt text against the renderer's interpolation rules.
|
|
66
|
+
*
|
|
67
|
+
* A complete `{{...}}` group must hold a valid, registered name; a lone `{{`
|
|
68
|
+
* with no later `}}` is literal prose and passes.
|
|
69
|
+
*/
|
|
70
|
+
export function checkPromptText(text) {
|
|
71
|
+
let index = 0
|
|
72
|
+
for (;;) {
|
|
73
|
+
const open = text.indexOf('{{', index)
|
|
74
|
+
if (open === -1) return { ok: true }
|
|
75
|
+
const close = text.indexOf('}}', open + 2)
|
|
76
|
+
if (close === -1) return { ok: true }
|
|
77
|
+
const variable = text.slice(open + 2, close)
|
|
78
|
+
if (!VARIABLE_NAME.test(variable)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error:
|
|
82
|
+
'保存被拒绝:{{' +
|
|
83
|
+
variable +
|
|
84
|
+
'}} 不是合法的变量引用(合法名只能用小写字母、数字、下划线且以字母开头)。' +
|
|
85
|
+
'若只想要字面量花括号,请用单个 { 或不闭合的 {{。',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!KNOWN_VARIABLES.includes(variable)) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error:
|
|
92
|
+
'保存被拒绝:{{' +
|
|
93
|
+
variable +
|
|
94
|
+
'}} 不是已注册的变量,渲染时会报错并让本模式每个请求都失败。可用:' +
|
|
95
|
+
KNOWN_VARIABLES.map((item) => '{{' + item + '}}').join('、') +
|
|
96
|
+
'。',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
index = close + 2
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read the prompt file, or report a typed failure the page can show. */
|
|
104
|
+
export function readPrompt() {
|
|
105
|
+
try {
|
|
106
|
+
return { ok: true, path: PROMPT_PATH, text: readFileSync(PROMPT_PATH, 'utf8') }
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { ok: false, error: '读取提示词失败:' + String((error && error.message) || error) }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read the installed composition. */
|
|
113
|
+
function readComposition() {
|
|
114
|
+
return existsSync(COMPOSITION_PATH) ? readFileSync(COMPOSITION_PATH, 'utf8') : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Everything the settings page renders from.
|
|
119
|
+
*
|
|
120
|
+
* `mode` and `overrides` are derived from the composition file rather than
|
|
121
|
+
* stored separately, so the file stays the single source of truth.
|
|
122
|
+
*/
|
|
123
|
+
export function readState() {
|
|
124
|
+
const text = readComposition()
|
|
125
|
+
if (text === null) {
|
|
126
|
+
return { ok: false, error: '找不到组成文件:' + COMPOSITION_PATH }
|
|
127
|
+
}
|
|
128
|
+
const mode = modeOf(text)
|
|
129
|
+
const prompt = readPrompt()
|
|
130
|
+
const meta = readPresetMeta()
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
mode,
|
|
134
|
+
modes: BASE_MODES,
|
|
135
|
+
rows: collectRows(text),
|
|
136
|
+
overrides: overridesOf(text, mode),
|
|
137
|
+
prompt: prompt.ok === true ? prompt.text : '',
|
|
138
|
+
name: meta.name,
|
|
139
|
+
description: meta.description,
|
|
140
|
+
presetMetaPath: PRESET_META_PATH,
|
|
141
|
+
promptPath: PROMPT_PATH,
|
|
142
|
+
compositionPath: COMPOSITION_PATH,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply one save: validate the prompt, render the composition, write both files.
|
|
148
|
+
*
|
|
149
|
+
* The render always carries a fresh timestamp, and `agent-presets` re-mounts a
|
|
150
|
+
* preset when the composition file's `mtimeMs`/`size` differ — so the new
|
|
151
|
+
* configuration reaches the next session without a process restart.
|
|
152
|
+
*/
|
|
153
|
+
export function saveState(input) {
|
|
154
|
+
const mode = input !== null && typeof input === 'object' && typeof input.mode === 'string' ? input.mode : ''
|
|
155
|
+
if (!BASE_MODES.some((entry) => entry.id === mode)) {
|
|
156
|
+
return { ok: false, error: '未知的基础模式:' + mode }
|
|
157
|
+
}
|
|
158
|
+
const prompt = input !== null && typeof input === 'object' && typeof input.prompt === 'string' ? input.prompt : ''
|
|
159
|
+
if (prompt.trim() === '') {
|
|
160
|
+
return { ok: false, error: '保存被拒绝:系统提示词为空。留空不会清空身份,读取器会沿用上一版。' }
|
|
161
|
+
}
|
|
162
|
+
const verdict = checkPromptText(prompt)
|
|
163
|
+
if (verdict.ok !== true) return { ok: false, error: verdict.error }
|
|
164
|
+
|
|
165
|
+
const overrides = new Map()
|
|
166
|
+
const raw = input !== null && typeof input === 'object' ? input.overrides : undefined
|
|
167
|
+
if (raw !== null && typeof raw === 'object') {
|
|
168
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
169
|
+
if (typeof value === 'boolean') overrides.set(id, value)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let composition
|
|
174
|
+
try {
|
|
175
|
+
composition = renderComposition(mode, overrides)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return { ok: false, error: '生成组成文件失败:' + String((error && error.message) || error) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Self-check our own output before publishing it: a composition that lost its
|
|
181
|
+
// rows would break the mode on its next mount, and the cause would be opaque.
|
|
182
|
+
try {
|
|
183
|
+
if (collectRows(composition).length === 0) throw new Error('生成的组成文件没有任何行')
|
|
184
|
+
readBaseComposition(mode)
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return { ok: false, error: '生成结果自检失败,已放弃写入:' + String((error && error.message) || error) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
mkdirSync(dirname(COMPOSITION_PATH), { recursive: true })
|
|
191
|
+
writeFileSync(COMPOSITION_PATH, composition, 'utf8')
|
|
192
|
+
writeFileSync(PROMPT_PATH, prompt, 'utf8')
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { ok: false, error: '写入失败:' + String((error && error.message) || error) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (typeof input.name === 'string' && input.name.trim() !== '') {
|
|
198
|
+
const metaResult = writePresetMeta(input.name, input.description)
|
|
199
|
+
if (metaResult.ok !== true) return metaResult
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
mode,
|
|
205
|
+
note: '已保存(基础模式:' + mode + ')。新建会话即生效,当前会话保持原配置。',
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Send JSON with no-store caching, so a save is never read back stale. */
|
|
210
|
+
function sendJson(res, status, value) {
|
|
211
|
+
const body = JSON.stringify(value)
|
|
212
|
+
res.writeHead(status, {
|
|
213
|
+
'content-type': 'application/json; charset=utf-8',
|
|
214
|
+
'cache-control': 'no-store',
|
|
215
|
+
'content-length': String(Buffer.byteLength(body, 'utf8')),
|
|
216
|
+
})
|
|
217
|
+
res.end(body)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Read a request body with a hard cap. */
|
|
221
|
+
async function readBody(req) {
|
|
222
|
+
const chunks = []
|
|
223
|
+
let size = 0
|
|
224
|
+
for await (const chunk of req) {
|
|
225
|
+
size += chunk.length
|
|
226
|
+
if (size > 4_000_000) throw new Error('请求体过大(上限 4MB)')
|
|
227
|
+
chunks.push(chunk)
|
|
228
|
+
}
|
|
229
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Rejection status for one request, or undefined when it may proceed.
|
|
234
|
+
*
|
|
235
|
+
* The platform's fence is verified through the Connection service, which is where
|
|
236
|
+
* the Host/Origin check and the browser-session check live:
|
|
237
|
+
*
|
|
238
|
+
* - Host must be loopback (or a declared trusted authority) → defeats DNS
|
|
239
|
+
* rebinding, where the socket reaches this server but the Host names the
|
|
240
|
+
* attacker's domain;
|
|
241
|
+
* - `Sec-Fetch-Site: cross-site` and a mismatching `Origin` are refused → defeats
|
|
242
|
+
* a malicious page posting to this local port (a simple form post needs no
|
|
243
|
+
* preflight, so CORS alone would not have stopped it);
|
|
244
|
+
* - the signed `dsh-auth-*` cookie must be present → without the browser session
|
|
245
|
+
* that the launch URL establishes, the route is closed.
|
|
246
|
+
*
|
|
247
|
+
* The service is resolved lazily, per request, and NOT through `inject`: measured
|
|
248
|
+
* on 0.1.6-alpha.1, `connection` is provided after this bundle row's `apply` runs,
|
|
249
|
+
* so an `inject` here would park the plugin in `pending` for no reason — while by
|
|
250
|
+
* request time the service is always there.
|
|
251
|
+
*
|
|
252
|
+
* When the service is absent the route FAILS CLOSED. A dead settings page is a
|
|
253
|
+
* visible, honest failure; an unauthenticated write path that rewrites the agent's
|
|
254
|
+
* system prompt is a silent one.
|
|
255
|
+
*/
|
|
256
|
+
let warnedMissingConnection = false
|
|
257
|
+
function connectionRejection(ctx, req) {
|
|
258
|
+
const connection = ctx.get('connection')
|
|
259
|
+
if (connection !== undefined && typeof connection.requestRejection === 'function') {
|
|
260
|
+
return connection.requestRejection(req)
|
|
261
|
+
}
|
|
262
|
+
if (!warnedMissingConnection) {
|
|
263
|
+
warnedMissingConnection = true
|
|
264
|
+
console.error(
|
|
265
|
+
'custom-mode: connection 服务不可用(DSH 版本不匹配?),已拒绝该设置页的所有请求以保守处理。' +
|
|
266
|
+
'prompt.md 与 custom_prompt 工具不受影响。',
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
return 503
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Where the settings page can exist at all.
|
|
274
|
+
*
|
|
275
|
+
* The page is a WEB page: without `webServer` there is nothing to serve the route on,
|
|
276
|
+
* and without `agentPresets` the base-mode list cannot be built. The tui profile has
|
|
277
|
+
* neither.
|
|
278
|
+
*
|
|
279
|
+
* These must NOT go into the row's own `inject`. Measured on 0.1.6-alpha.1:
|
|
280
|
+
* `./install.sh --profile tui` — a usage both `install.sh --help` and the READMEs
|
|
281
|
+
* advertise — installs this web-only bundle into a profile with no web server, and a
|
|
282
|
+
* row-level `inject` then parks the whole entry forever:
|
|
283
|
+
*
|
|
284
|
+
* dsh: warning: 1 entry did not activate
|
|
285
|
+
* custom-mode (dsh-custom-mode): pending (waiting for services: webServer, agentPresets)
|
|
286
|
+
*
|
|
287
|
+
* That is the SAME line a broken installation prints, so it teaches users to ignore the
|
|
288
|
+
* one warning that matters. Instead the row always activates, and the route is
|
|
289
|
+
* registered from a scoped fiber that waits for those two services (`ctx.inject`),
|
|
290
|
+
* which is the dynamic form of the same declaration.
|
|
291
|
+
*/
|
|
292
|
+
const WEB_SERVICES = ['webServer', 'agentPresets']
|
|
293
|
+
|
|
294
|
+
export function apply(ctx) {
|
|
295
|
+
ctx.inject(WEB_SERVICES, (scope) => {
|
|
296
|
+
// Compatibility guard: this plugin reads host APIs that a future DSH release could
|
|
297
|
+
// reshape. Check them once and say so plainly, instead of letting every request
|
|
298
|
+
// fail with an opaque 500.
|
|
299
|
+
const missing = []
|
|
300
|
+
if (typeof scope.agentPresets?.list !== 'function') missing.push('agentPresets.list()')
|
|
301
|
+
if (typeof scope.webServer?.register !== 'function') missing.push('webServer.register()')
|
|
302
|
+
if (missing.length > 0) {
|
|
303
|
+
console.error(
|
|
304
|
+
'custom-mode: 当前 DSH 版本缺少所需 API:' +
|
|
305
|
+
missing.join('、') +
|
|
306
|
+
'。设置页将不可用,请核对 DSH 版本或提 issue。',
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolve the shipped-preset directory through the roster, which reports each
|
|
313
|
+
* preset's absolute path and is therefore independent of install layout.
|
|
314
|
+
*/
|
|
315
|
+
let shippedReady = null
|
|
316
|
+
const ensureShipped = () => {
|
|
317
|
+
if (shippedReady === null) {
|
|
318
|
+
shippedReady = (async () => {
|
|
319
|
+
try {
|
|
320
|
+
const rows = await scope.agentPresets.list()
|
|
321
|
+
const system = rows.find((row) => row.trust === 'system' && typeof row.path === 'string')
|
|
322
|
+
// <presets>/<id>/agent.cordis.yml -> <presets>
|
|
323
|
+
if (system !== undefined) setShippedPresetsDir(dirname(dirname(system.path)))
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error(
|
|
326
|
+
'custom-mode: 无法从 roster 解析出厂预设目录:' + String((error && error.message) || error),
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
})()
|
|
330
|
+
}
|
|
331
|
+
return shippedReady
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const handler = async (req, res) => {
|
|
335
|
+
try {
|
|
336
|
+
// The fence comes first, before any method dispatch: the GET leaks the whole
|
|
337
|
+
// system prompt and the POST rewrites it, so neither may run unauthenticated.
|
|
338
|
+
const rejection = connectionRejection(scope, req)
|
|
339
|
+
if (rejection !== undefined) {
|
|
340
|
+
// 401/403 与平台对 /api 的措辞一致;503 是"我们自己保守关闭"(connection 服务
|
|
341
|
+
// 取不到),它既不是未授权也不是被禁止,别把响应体写成 forbidden 误导排查的人。
|
|
342
|
+
const reason = rejection === 401 ? 'unauthorized' : rejection === 403 ? 'forbidden' : 'unavailable'
|
|
343
|
+
res.writeHead(rejection, { 'content-type': 'text/plain; charset=utf-8' })
|
|
344
|
+
res.end(reason)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
if (req.method === 'GET') {
|
|
348
|
+
await ensureShipped()
|
|
349
|
+
sendJson(res, 200, readState())
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
if (req.method === 'POST') {
|
|
353
|
+
await ensureShipped()
|
|
354
|
+
const raw = await readBody(req)
|
|
355
|
+
let parsed
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(raw)
|
|
358
|
+
} catch {
|
|
359
|
+
sendJson(res, 400, { ok: false, error: '请求体不是合法 JSON' })
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
const result = saveState(parsed)
|
|
363
|
+
sendJson(res, result.ok === true ? 200 : 400, result)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
sendJson(res, 405, { ok: false, error: '只支持 GET 与 POST' })
|
|
367
|
+
} catch (error) {
|
|
368
|
+
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
scope.effect(
|
|
373
|
+
() => scope.webServer.register({ kind: 'exact', path: ROUTE_PATH, handler }),
|
|
374
|
+
'custom-mode.route',
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
}
|
package/locales.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bilingual copy for the 「自定义模式」settings page.
|
|
3
|
+
*
|
|
4
|
+
* Registered with the official `locale` service (`ctx.locale.register(ns, {zh, en})`),
|
|
5
|
+
* and the page is registered with `locale: NS` so the shell hands the component a
|
|
6
|
+
* bound `t` — the same contract the shipped settings plugins use.
|
|
7
|
+
*
|
|
8
|
+
* Row labels are keyed by row id (`row.<id>.label`), derived rather than stored:
|
|
9
|
+
* `composition.mjs` therefore keeps working unchanged for any row it knows, and a
|
|
10
|
+
* row this dictionary does not cover falls back to that source (or to its bare id).
|
|
11
|
+
* Adding a new row to a base mode never requires touching this file.
|
|
12
|
+
*
|
|
13
|
+
* Key-set parity is asserted by `test/locales.test.mjs`: a key present in one
|
|
14
|
+
* language and missing in the other would render as a raw key in that locale.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Simplified Chinese dictionary — the key source of truth. */
|
|
18
|
+
export const zh = {
|
|
19
|
+
nav: '自定义模式',
|
|
20
|
+
|
|
21
|
+
'name.heading': '模式名称',
|
|
22
|
+
'name.hint': '改名只影响显示(模式选择器和这里的导航项),内部标识保持不变,已有会话不受影响。新建会话即可看到新名称。',
|
|
23
|
+
'name.placeholder': '自定义模式',
|
|
24
|
+
'name.descriptionPlaceholder': '模式描述(显示在模式选择器里,可留空)',
|
|
25
|
+
|
|
26
|
+
'mode.heading': '基础模式',
|
|
27
|
+
'mode.hint': '选一个官方模式作为底子,下面再按行微调。改完保存后,新建会话即生效,不需要重启。',
|
|
28
|
+
|
|
29
|
+
'rows.heading': '插件开关',
|
|
30
|
+
'rows.hint': '逐行控制这个模式挂载哪些插件,和官方插件列表一样按行铺开。没拨过的行保持官方默认(含平台判断);你手动拨了就以你的为准。',
|
|
31
|
+
|
|
32
|
+
'prompt.heading': '系统提示词',
|
|
33
|
+
'prompt.hint': '这一步编辑的文本会在每次模型调用前重新读取,保存后下一步即生效。仅影响使用本模式的会话。',
|
|
34
|
+
|
|
35
|
+
'status.enabled': '已启用',
|
|
36
|
+
'status.disabled': '已停用',
|
|
37
|
+
'status.changed': '已改',
|
|
38
|
+
'tag.essential': '基础能力',
|
|
39
|
+
'tag.followPlatform': '跟随平台',
|
|
40
|
+
|
|
41
|
+
'btn.save': '保存',
|
|
42
|
+
'btn.saving': '处理中…',
|
|
43
|
+
'btn.reload': '重新读取',
|
|
44
|
+
|
|
45
|
+
'msg.unsaved': '有未保存的修改',
|
|
46
|
+
'msg.loading': '正在读取…',
|
|
47
|
+
'msg.notLoaded': '(尚未读取)',
|
|
48
|
+
'msg.reread': '已重新读取',
|
|
49
|
+
'msg.readFailed': '读取失败',
|
|
50
|
+
'msg.saveFailed': '保存失败',
|
|
51
|
+
'msg.saved': '已保存。新建会话即生效,当前会话保持原配置。',
|
|
52
|
+
|
|
53
|
+
// ── row labels ────────────────────────────────────────────────────────────
|
|
54
|
+
'row.persona.label': '身份(系统提示词)',
|
|
55
|
+
'row.persona.note': '提示词注入点;关掉后本模式用回部署默认身份',
|
|
56
|
+
'row.custom-prompt-tool.label': 'custom_prompt 工具',
|
|
57
|
+
'row.custom-prompt-tool.note': '关掉后无法用对话改提示词(设置页仍可用)',
|
|
58
|
+
'row.agent-instructions.label': '项目指令 AGENTS.md',
|
|
59
|
+
'row.agent-instructions.note': '读取 AGENTS.md / CLAUDE.md',
|
|
60
|
+
'row.tool-bash.label': 'Shell(bash)',
|
|
61
|
+
'row.tool-pwsh.label': 'Shell(pwsh)',
|
|
62
|
+
'row.tool-fs.label': '文件读写',
|
|
63
|
+
'row.tool-fs.note': '关掉后 agent 无法读写文件',
|
|
64
|
+
'row.tool-fs-search.label': '文件搜索(glob/grep)',
|
|
65
|
+
'row.tool-jobs.label': '后台任务',
|
|
66
|
+
'row.planning.label': '计划模式(分组)',
|
|
67
|
+
'row.planning.note': '含 isolate realm,关掉等于移除整个计划能力',
|
|
68
|
+
'row.plan-mode.label': '计划模式实现',
|
|
69
|
+
'row.compaction.label': '上下文压缩(分组)',
|
|
70
|
+
'row.compaction.note': '含 isolate realm',
|
|
71
|
+
'row.compaction-basic.label': '基础压缩',
|
|
72
|
+
'row.command-compact.label': '/compact 命令',
|
|
73
|
+
'row.tool-result-pruner.label': '工具结果裁剪',
|
|
74
|
+
'row.delegation.label': '委派与工作流(分组)',
|
|
75
|
+
'row.delegation.note': '含 isolate realm;关掉等于移除子代理与工作流',
|
|
76
|
+
'row.tool-subagent.label': '子代理(spawn)',
|
|
77
|
+
'row.tool-subagent-fork.label': '子代理(fork)',
|
|
78
|
+
'row.tool-subagent-control.label': '子代理控制',
|
|
79
|
+
'row.tool-subagent-list-agents.label': '列出子代理',
|
|
80
|
+
'row.tool-subagent-codex.label': 'Codex 子代理',
|
|
81
|
+
'row.tool-subagent-codex.note': '默认关闭:需要先安装对应 Bundle',
|
|
82
|
+
'row.tool-subagent-claude-code.label': 'Claude Code 子代理',
|
|
83
|
+
'row.tool-subagent-claude-code.note': '默认关闭:需要先安装对应 Bundle',
|
|
84
|
+
'row.workflow-ptc.label': '工作流引擎',
|
|
85
|
+
'row.tool-workflow.label': '工作流工具',
|
|
86
|
+
'row.tool-ralph.label': 'Ralph 工作流',
|
|
87
|
+
'row.tool-ralph.note': '默认关闭',
|
|
88
|
+
'row.tool-web.label': '网页检索与抓取',
|
|
89
|
+
'row.tool-skill.label': '技能工具',
|
|
90
|
+
'row.skill-filesystem.label': '技能发现',
|
|
91
|
+
'row.tool-cordis.label': 'Cordis 运行时工具',
|
|
92
|
+
'row.tool-cordis.note': '可读写 harness 运行时',
|
|
93
|
+
'row.tool-presentation.label': 'PTC 工具呈现',
|
|
94
|
+
'row.present.label': '交付文件(present)',
|
|
95
|
+
'row.command-goal.label': '目标命令',
|
|
96
|
+
'row.tool-goal.label': '目标',
|
|
97
|
+
'row.tool-todo.label': '待办清单',
|
|
98
|
+
'row.tool-ask-user.label': '向用户提问',
|
|
99
|
+
'row.persistent-shell.label': '持久 Shell',
|
|
100
|
+
'row.pty.label': 'PTY 终端',
|
|
101
|
+
'row.terminal-bash.label': '终端(bash)',
|
|
102
|
+
'row.persistent-bash.label': '持久 bash',
|
|
103
|
+
'row.terminal-pwsh.label': '终端(pwsh)',
|
|
104
|
+
'row.persistent-pwsh.label': '持久 pwsh',
|
|
105
|
+
|
|
106
|
+
// ── base mode labels ──────────────────────────────────────────────────────
|
|
107
|
+
'base.standard.label': '标准模式',
|
|
108
|
+
'base.standard.note': '完整编码能力:Shell、文件、检索、技能、计划、目标、子代理、工作流',
|
|
109
|
+
'base.ptc.label': 'PTC 模式',
|
|
110
|
+
'base.ptc.note': '在标准模式基础上启用 PTC 工具呈现(tool-presentation)',
|
|
111
|
+
'base.minimal.label': '极简模式',
|
|
112
|
+
'base.minimal.note': '只有 Shell 与终端,共 7 行;没有文件、检索、技能、子代理',
|
|
113
|
+
'base.cordis.label': 'Cordis 模式',
|
|
114
|
+
'base.cordis.note': '标准模式 + 读写运行时的 Cordis 工具集,可让 agent 自己改 harness',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** English dictionary; the parity test keeps its key set equal to {@link zh}. */
|
|
118
|
+
export const en = {
|
|
119
|
+
nav: 'Custom mode',
|
|
120
|
+
|
|
121
|
+
'name.heading': 'Mode name',
|
|
122
|
+
'name.hint':
|
|
123
|
+
'Renaming changes only what is displayed (the mode picker and this nav entry); the internal id stays the same and existing sessions are unaffected. Start a new session to see it.',
|
|
124
|
+
'name.placeholder': 'Custom mode',
|
|
125
|
+
'name.descriptionPlaceholder': 'Mode description (shown in the mode picker, optional)',
|
|
126
|
+
|
|
127
|
+
'mode.heading': 'Base mode',
|
|
128
|
+
'mode.hint':
|
|
129
|
+
'Choose an official mode as the base, then fine-tune individual rows below. After saving, a new session picks it up — no restart needed.',
|
|
130
|
+
|
|
131
|
+
'rows.heading': 'Plugin switches',
|
|
132
|
+
'rows.hint':
|
|
133
|
+
'Control row by row which plugins this mode mounts, laid out like the official plugin list. Untouched rows keep the official default (platform conditions included); your manual choice wins.',
|
|
134
|
+
|
|
135
|
+
'prompt.heading': 'System prompt',
|
|
136
|
+
'prompt.hint':
|
|
137
|
+
'This text is re-read before every model call, so a save applies on the next step. It affects only sessions using this mode.',
|
|
138
|
+
|
|
139
|
+
'status.enabled': 'Enabled',
|
|
140
|
+
'status.disabled': 'Disabled',
|
|
141
|
+
'status.changed': 'changed',
|
|
142
|
+
'tag.essential': 'core',
|
|
143
|
+
'tag.followPlatform': 'follows platform',
|
|
144
|
+
|
|
145
|
+
'btn.save': 'Save',
|
|
146
|
+
'btn.saving': 'Working…',
|
|
147
|
+
'btn.reload': 'Reload',
|
|
148
|
+
|
|
149
|
+
'msg.unsaved': 'Unsaved changes',
|
|
150
|
+
'msg.loading': 'Loading…',
|
|
151
|
+
'msg.notLoaded': '(not loaded)',
|
|
152
|
+
'msg.reread': 'Reloaded',
|
|
153
|
+
'msg.readFailed': 'Load failed',
|
|
154
|
+
'msg.saveFailed': 'Save failed',
|
|
155
|
+
'msg.saved': 'Saved. A new session picks it up; the current one keeps its configuration.',
|
|
156
|
+
|
|
157
|
+
// ── row labels ────────────────────────────────────────────────────────────
|
|
158
|
+
'row.persona.label': 'Identity (system prompt)',
|
|
159
|
+
'row.persona.note': 'Where the prompt is injected; turning it off falls back to the deployment identity',
|
|
160
|
+
'row.custom-prompt-tool.label': 'custom_prompt tool',
|
|
161
|
+
'row.custom-prompt-tool.note': 'Without it the prompt can still be edited here, but not by asking the agent',
|
|
162
|
+
'row.agent-instructions.label': 'Project instructions (AGENTS.md)',
|
|
163
|
+
'row.agent-instructions.note': 'Reads AGENTS.md / CLAUDE.md',
|
|
164
|
+
'row.tool-bash.label': 'Shell (bash)',
|
|
165
|
+
'row.tool-pwsh.label': 'Shell (pwsh)',
|
|
166
|
+
'row.tool-fs.label': 'File access',
|
|
167
|
+
'row.tool-fs.note': 'Without it the agent cannot read or write files',
|
|
168
|
+
'row.tool-fs-search.label': 'File search (glob/grep)',
|
|
169
|
+
'row.tool-jobs.label': 'Background jobs',
|
|
170
|
+
'row.planning.label': 'Plan mode (group)',
|
|
171
|
+
'row.planning.note': 'Carries an isolate realm; turning it off removes planning entirely',
|
|
172
|
+
'row.plan-mode.label': 'Plan mode implementation',
|
|
173
|
+
'row.compaction.label': 'Context compaction (group)',
|
|
174
|
+
'row.compaction.note': 'Carries an isolate realm',
|
|
175
|
+
'row.compaction-basic.label': 'Basic compaction',
|
|
176
|
+
'row.command-compact.label': '/compact command',
|
|
177
|
+
'row.tool-result-pruner.label': 'Tool-result pruning',
|
|
178
|
+
'row.delegation.label': 'Delegation and workflows (group)',
|
|
179
|
+
'row.delegation.note': 'Carries an isolate realm; turning it off removes subagents and workflows',
|
|
180
|
+
'row.tool-subagent.label': 'Subagent (spawn)',
|
|
181
|
+
'row.tool-subagent-fork.label': 'Subagent (fork)',
|
|
182
|
+
'row.tool-subagent-control.label': 'Subagent control',
|
|
183
|
+
'row.tool-subagent-list-agents.label': 'List subagents',
|
|
184
|
+
'row.tool-subagent-codex.label': 'Codex subagent',
|
|
185
|
+
'row.tool-subagent-codex.note': 'Off by default: install the matching Bundle first',
|
|
186
|
+
'row.tool-subagent-claude-code.label': 'Claude Code subagent',
|
|
187
|
+
'row.tool-subagent-claude-code.note': 'Off by default: install the matching Bundle first',
|
|
188
|
+
'row.workflow-ptc.label': 'Workflow engine',
|
|
189
|
+
'row.tool-workflow.label': 'Workflow tool',
|
|
190
|
+
'row.tool-ralph.label': 'Ralph workflow',
|
|
191
|
+
'row.tool-ralph.note': 'Off by default',
|
|
192
|
+
'row.tool-web.label': 'Web search and fetch',
|
|
193
|
+
'row.tool-skill.label': 'Skill tool',
|
|
194
|
+
'row.skill-filesystem.label': 'Skill discovery',
|
|
195
|
+
'row.tool-cordis.label': 'Cordis runtime tools',
|
|
196
|
+
'row.tool-cordis.note': 'Can read and modify the running harness',
|
|
197
|
+
'row.tool-presentation.label': 'PTC tool presentation',
|
|
198
|
+
'row.present.label': 'Deliverables (present)',
|
|
199
|
+
'row.command-goal.label': 'Goal command',
|
|
200
|
+
'row.tool-goal.label': 'Goals',
|
|
201
|
+
'row.tool-todo.label': 'Todo list',
|
|
202
|
+
'row.tool-ask-user.label': 'Ask the user',
|
|
203
|
+
'row.persistent-shell.label': 'Persistent shell',
|
|
204
|
+
'row.pty.label': 'PTY terminal',
|
|
205
|
+
'row.terminal-bash.label': 'Terminal (bash)',
|
|
206
|
+
'row.persistent-bash.label': 'Persistent bash',
|
|
207
|
+
'row.terminal-pwsh.label': 'Terminal (pwsh)',
|
|
208
|
+
'row.persistent-pwsh.label': 'Persistent pwsh',
|
|
209
|
+
|
|
210
|
+
// ── base mode labels ──────────────────────────────────────────────────────
|
|
211
|
+
'base.standard.label': 'Standard',
|
|
212
|
+
'base.standard.note': 'Full coding agent: shell, files, search, skills, planning, goals, subagents, workflows',
|
|
213
|
+
'base.ptc.label': 'PTC',
|
|
214
|
+
'base.ptc.note': 'Standard plus PTC tool presentation (tool-presentation)',
|
|
215
|
+
'base.minimal.label': 'Minimal',
|
|
216
|
+
'base.minimal.note': 'Shell and terminal only, 7 rows; no files, search, skills or subagents',
|
|
217
|
+
'base.cordis.label': 'Cordis',
|
|
218
|
+
'base.cordis.note': 'Standard plus the Cordis toolset, letting the agent modify its own harness',
|
|
219
|
+
}
|