dsh-custom-mode 2.2.6 → 2.3.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/README.i18n.yaml +2 -2
- package/README.md +55 -22
- package/README.zh.md +55 -22
- package/client.js +508 -40
- package/composition.mjs +22 -5
- package/index.mjs +171 -36
- package/locales.mjs +88 -2
- package/package.json +2 -2
- package/preset-backend/declarative.mjs +38 -15
- package/preset-backend/parse-composition.mjs +51 -3
|
@@ -26,20 +26,17 @@
|
|
|
26
26
|
import { existsSync, readFileSync, renameSync, rmSync } from 'node:fs'
|
|
27
27
|
import { basename, dirname, join } from 'node:path'
|
|
28
28
|
import { pathToFileURL } from 'node:url'
|
|
29
|
-
import { parseComposition } from './parse-composition.mjs'
|
|
29
|
+
import { parseComposition, evaluateDisabledPredicate, profileContextPredicate } from './parse-composition.mjs'
|
|
30
30
|
|
|
31
31
|
export const BACKEND_ID = 'declarative'
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/** Preserve the host-native predicate node so the loader evaluates it against its real Entry.ctx. */
|
|
34
34
|
function resolveDisabled(value) {
|
|
35
35
|
if (value !== null && typeof value === 'object' && typeof value.__js === 'string') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// 表达式求值不了时按"关掉"处理:宁可少一行能力,也不要让整条 preset 挂载失败。
|
|
41
|
-
return true
|
|
42
|
-
}
|
|
36
|
+
// An unknown ctx service can return undefined rather than throw, which would
|
|
37
|
+
// make the native loader enable it. Keep unsupported conditions truly off.
|
|
38
|
+
if (profileContextPredicate(value.__js) === null && evaluateDisabledPredicate(value.__js) === undefined) return true
|
|
39
|
+
return { __jsExpr: value.__js }
|
|
43
40
|
}
|
|
44
41
|
return value === true
|
|
45
42
|
}
|
|
@@ -103,6 +100,8 @@ export function createDeclarativeBackend({ scope, log = console.log, warn = cons
|
|
|
103
100
|
/** presetId → { dispose, dir } —— 当前挂在注册表里的东西,注销的唯一凭据。 */
|
|
104
101
|
const mounted = new Map()
|
|
105
102
|
let mutations = Promise.resolve()
|
|
103
|
+
let stopped = false
|
|
104
|
+
const stoppedResult = () => ({ ok: false, code: 'backendStopped', error: 'backend stopped' })
|
|
106
105
|
const serialize = (work) => {
|
|
107
106
|
const next = mutations.then(work, work)
|
|
108
107
|
mutations = next.then(() => undefined, () => undefined)
|
|
@@ -135,6 +134,7 @@ export function createDeclarativeBackend({ scope, log = console.log, warn = cons
|
|
|
135
134
|
* registry refuses a duplicate id outright.
|
|
136
135
|
*/
|
|
137
136
|
async function mountOne(assistant) {
|
|
137
|
+
if (stopped) return stoppedResult()
|
|
138
138
|
let definition
|
|
139
139
|
try {
|
|
140
140
|
definition = buildDefinition(assistant)
|
|
@@ -142,26 +142,40 @@ export function createDeclarativeBackend({ scope, log = console.log, warn = cons
|
|
|
142
142
|
// 解析失败是**用户可见**的问题(手改坏了 composition),要显式说,不能静默跳过。
|
|
143
143
|
warn(`custom-mode: 预设 "${assistant.id}" 的 composition 解析失败 —— ${describe(error)}。该助手不会出现在选择器里。`)
|
|
144
144
|
await unmount(assistant.id)
|
|
145
|
-
return { ok: false, error: describe(error) }
|
|
145
|
+
return { ok: false, code: 'compositionParseFailed', error: describe(error) }
|
|
146
146
|
}
|
|
147
147
|
if (definition === null) {
|
|
148
148
|
await unmount(assistant.id)
|
|
149
|
-
return { ok: false, error: 'no composition' }
|
|
149
|
+
return { ok: false, code: 'noComposition', error: 'no composition' }
|
|
150
150
|
}
|
|
151
|
+
let registering = false
|
|
151
152
|
try {
|
|
152
153
|
await unmount(assistant.id)
|
|
154
|
+
if (stopped) return stoppedResult()
|
|
155
|
+
registering = true
|
|
153
156
|
const dispose = await scope.agentPresets.register(definition)
|
|
157
|
+
registering = false
|
|
154
158
|
mounted.set(assistant.id, { dispose, dir: assistant.dir })
|
|
159
|
+
// register() can finish after the scope starts stopping. Keep its disposer until
|
|
160
|
+
// it succeeds, and release only this generation's own registration.
|
|
161
|
+
if (stopped) {
|
|
162
|
+
await unmount(assistant.id)
|
|
163
|
+
return stoppedResult()
|
|
164
|
+
}
|
|
155
165
|
return { ok: true }
|
|
156
166
|
} catch (error) {
|
|
157
167
|
// register 会抛(Duplicate / id 空 / 已存在):说清是哪个助手、哪条错误。
|
|
158
168
|
warn(`custom-mode: 注册预设 "${assistant.id}" 失败 —— ${describe(error)}`)
|
|
159
|
-
|
|
169
|
+
const code = registering
|
|
170
|
+
? describe(error) === 'Duplicate agent preset: ' + assistant.id ? 'registrationConflict' : 'registrationFailed'
|
|
171
|
+
: 'registrationCleanupFailed'
|
|
172
|
+
return { ok: false, code, error: describe(error) }
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
/** Bring the registry in line with the given assistant list: mount all, drop the rest. */
|
|
164
177
|
async function sync(assistants) {
|
|
178
|
+
if (stopped) return assistants.map(({ id }) => ({ id, ...stoppedResult() }))
|
|
165
179
|
const results = []
|
|
166
180
|
for (const assistant of assistants) results.push({ id: assistant.id, ...(await mountOne(assistant)) })
|
|
167
181
|
const wanted = new Set(assistants.map((a) => a.id))
|
|
@@ -175,7 +189,11 @@ export function createDeclarativeBackend({ scope, log = console.log, warn = cons
|
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
async function disposeAll() {
|
|
178
|
-
|
|
192
|
+
const errors = []
|
|
193
|
+
for (const id of [...mounted.keys()]) {
|
|
194
|
+
try { await unmount(id) } catch (error) { errors.push(error) }
|
|
195
|
+
}
|
|
196
|
+
if (errors.length > 0) throw new AggregateError(errors, 'preset registration cleanup failed')
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
/**
|
|
@@ -262,8 +280,13 @@ export function createDeclarativeBackend({ scope, log = console.log, warn = cons
|
|
|
262
280
|
sync: (assistants) => serialize(() => sync(assistants)),
|
|
263
281
|
mountOne: (assistant) => serialize(() => mountOne(assistant)),
|
|
264
282
|
unmount: (id) => serialize(() => unmount(id)),
|
|
265
|
-
remove: (id, dir) => serialize(() => remove(id, dir)),
|
|
266
|
-
disposeAll: () =>
|
|
283
|
+
remove: (id, dir) => serialize(() => stopped ? stoppedResult() : remove(id, dir)),
|
|
284
|
+
disposeAll: () => {
|
|
285
|
+
// Close admission immediately, before awaiting queued/in-flight mutations. A
|
|
286
|
+
// startup sync still reading host metadata must not resurrect this backend.
|
|
287
|
+
stopped = true
|
|
288
|
+
return serialize(disposeAll)
|
|
289
|
+
},
|
|
267
290
|
fetchBaseCompositions,
|
|
268
291
|
mountedIds: () => [...mounted.keys()],
|
|
269
292
|
}
|
|
@@ -39,6 +39,56 @@ export class CompositionParseError extends Error {
|
|
|
39
39
|
const isBlank = (line) => line.trim() === '' || line.trim().startsWith('#')
|
|
40
40
|
const indentOf = (line) => line.length - line.trimStart().length
|
|
41
41
|
|
|
42
|
+
/** Decode the YAML scalar around a tagged JS source; never execute the expression. */
|
|
43
|
+
export function decodeJsExpression(raw, lineNo = 0) {
|
|
44
|
+
const source = String(raw).trim()
|
|
45
|
+
if (source === '') throw new CompositionParseError('!!js 后面没有表达式', lineNo)
|
|
46
|
+
if (source.startsWith("'")) {
|
|
47
|
+
if (!/^'(?:[^']|'')*'$/.test(source)) throw new CompositionParseError('!!js 单引号标量无效', lineNo)
|
|
48
|
+
return source.slice(1, -1).replace(/''/g, "'")
|
|
49
|
+
}
|
|
50
|
+
if (source.startsWith('"')) {
|
|
51
|
+
try {
|
|
52
|
+
const decoded = JSON.parse(source)
|
|
53
|
+
if (typeof decoded !== 'string') throw new Error('Expected a string')
|
|
54
|
+
return decoded
|
|
55
|
+
} catch {
|
|
56
|
+
throw new CompositionParseError('!!js 双引号标量无效', lineNo)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return source
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The only context-service predicate currently understood by the editor. */
|
|
63
|
+
export function profileContextPredicate(source) {
|
|
64
|
+
const match = /^(!{0,2})\s*ctx\s*\.\s*get\s*\(\s*(['"])profileContext\2\s*\)$/.exec(source)
|
|
65
|
+
return match === null ? null : match[1].length
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Preserve existing non-context evaluation semantics for installed composition
|
|
70
|
+
* code, while unknown context references stay unsupported rather than guessed.
|
|
71
|
+
*/
|
|
72
|
+
export function evaluateDisabledPredicate(source, capabilities = {}) {
|
|
73
|
+
const negations = profileContextPredicate(source)
|
|
74
|
+
if (negations !== null) {
|
|
75
|
+
if (typeof capabilities?.profileContext !== 'boolean') return undefined
|
|
76
|
+
return negations === 1 ? !capabilities.profileContext : capabilities.profileContext
|
|
77
|
+
}
|
|
78
|
+
const code = source
|
|
79
|
+
.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, '')
|
|
80
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, '')
|
|
81
|
+
if (/\bctx\b/.test(code)) return undefined
|
|
82
|
+
try {
|
|
83
|
+
// Composition files are installed executable code. No real host context is
|
|
84
|
+
// supplied, and the ctx parameter shadows any unrelated global of that name.
|
|
85
|
+
const evaluate = new Function('process', 'ctx', `return (${source});`)
|
|
86
|
+
return Boolean(evaluate(process, undefined))
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
/**
|
|
43
93
|
* Parse a composition document into plain JS.
|
|
44
94
|
*
|
|
@@ -156,9 +206,7 @@ export function parseScalar(raw, lineNo = 0) {
|
|
|
156
206
|
const value = raw.trim()
|
|
157
207
|
if (value === '') return null
|
|
158
208
|
if (value.startsWith('!!js')) {
|
|
159
|
-
|
|
160
|
-
if (source === '') throw new CompositionParseError('!!js 后面没有表达式', lineNo)
|
|
161
|
-
return { __js: source }
|
|
209
|
+
return { __js: decodeJsExpression(value.slice(4), lineNo) }
|
|
162
210
|
}
|
|
163
211
|
if (value === 'true') return true
|
|
164
212
|
if (value === 'false') return false
|