dsh-plugin-showcase 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +286 -2
- package/README.zh.md +262 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +5 -0
- package/lib/client.js +570 -0
- package/lib/index.js +664 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/market/index.json +53 -0
- package/package.json +53 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const name = 'showcase'
|
|
5
|
+
export const inject = ['tools', 'commands']
|
|
6
|
+
|
|
7
|
+
const VERSION = '0.2.0'
|
|
8
|
+
const TOOL_NAME = 'showcase_inspect'
|
|
9
|
+
const DOCTOR_TOOL_NAME = 'showcase_doctor'
|
|
10
|
+
const COMMAND_NAME = 'showcase'
|
|
11
|
+
const DENY_REASON = 'showcase: 拒绝执行破坏性命令'
|
|
12
|
+
const USAGE = '/showcase [on|off|doctor|about]'
|
|
13
|
+
const UNKNOWN = '未知'
|
|
14
|
+
|
|
15
|
+
const LOCALE_NAMESPACE = 'locale'
|
|
16
|
+
const LOCALE_FIELD = 'preference'
|
|
17
|
+
const APP_BOOT_PACKAGE = '@deepseek-ai/dsh-app-boot'
|
|
18
|
+
const STATUS_LABELS = Object.freeze({ ok: 'OK', warn: 'WARN', fail: 'FAIL', unknown: '?' })
|
|
19
|
+
const HINT_FAULT = '这是 showcase 自身的缺陷:重跑 /showcase doctor,仍失败请到插件页反馈'
|
|
20
|
+
|
|
21
|
+
const DESTRUCTIVE_PATTERNS = Object.freeze([
|
|
22
|
+
'rm -rf /',
|
|
23
|
+
'rm -rf ~',
|
|
24
|
+
':(){:|:&;}',
|
|
25
|
+
':(){ :|:& };:',
|
|
26
|
+
'.(){ :|:& };:',
|
|
27
|
+
'bomb(){ bomb|bomb& };bomb',
|
|
28
|
+
'del /f /s /q c:\\',
|
|
29
|
+
'format c:',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
// Whitespace-removed twins: "rm -rf /", "FORMAT\tC:", and the collapsed fork-bomb spellings.
|
|
33
|
+
const COMPACT_DESTRUCTIVE_PATTERNS = Object.freeze(
|
|
34
|
+
DESTRUCTIVE_PATTERNS.map(pattern => pattern.replace(/\s+/gu, '')),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const ELEVATION_PREFIXES = Object.freeze(['sudo', 'doas', 'pkexec'])
|
|
38
|
+
const SHUTDOWN_WORD = /^shutdown(\.exe)?(\/(?:s|r|f)\w*)?$/u
|
|
39
|
+
|
|
40
|
+
const COMMAND_ARG_KEYS = Object.freeze(['command', 'cmd', 'script', 'code'])
|
|
41
|
+
|
|
42
|
+
function nullableString() {
|
|
43
|
+
return { oneOf: [{ type: 'string' }, { type: 'null' }] }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildSnapshotSchema() {
|
|
47
|
+
return {
|
|
48
|
+
type: 'object',
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
required: ['generatedAt', 'workspace', 'session', 'plugin', 'sections'],
|
|
51
|
+
properties: {
|
|
52
|
+
generatedAt: { type: 'string', description: 'ISO 8601 timestamp of this snapshot.' },
|
|
53
|
+
workspace: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
required: ['cwd', 'gitBranch'],
|
|
57
|
+
properties: {
|
|
58
|
+
cwd: { type: 'string', description: 'Effective working directory, empty when unknown.' },
|
|
59
|
+
gitBranch: { ...nullableString(), description: 'Checked-out git branch, null when not a repository.' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
session: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
required: ['id', 'model'],
|
|
66
|
+
properties: {
|
|
67
|
+
id: { ...nullableString(), description: 'Session identity of the last observed agent.' },
|
|
68
|
+
model: { ...nullableString(), description: 'Provider-qualified model id, null when unknown.' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
plugin: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ['version', 'gateEnabled', 'toolName', 'command'],
|
|
75
|
+
properties: {
|
|
76
|
+
version: { type: 'string' },
|
|
77
|
+
gateEnabled: { type: 'boolean' },
|
|
78
|
+
toolName: { type: 'string' },
|
|
79
|
+
command: { type: 'string' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
sections: {
|
|
83
|
+
type: 'array',
|
|
84
|
+
items: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
required: ['title', 'items'],
|
|
88
|
+
properties: {
|
|
89
|
+
title: { type: 'string' },
|
|
90
|
+
items: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
items: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
required: ['label', 'value'],
|
|
96
|
+
properties: {
|
|
97
|
+
label: { type: 'string' },
|
|
98
|
+
value: { type: 'string' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readService(ctx, key) {
|
|
110
|
+
try {
|
|
111
|
+
const service = ctx.get(key)
|
|
112
|
+
return service === null || typeof service !== 'object' ? null : service
|
|
113
|
+
} catch {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readAgentId(agent) {
|
|
119
|
+
try {
|
|
120
|
+
return agent !== null && typeof agent === 'object' && typeof agent.id === 'string' && agent.id !== ''
|
|
121
|
+
? agent.id
|
|
122
|
+
: null
|
|
123
|
+
} catch {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readModel(ctx) {
|
|
129
|
+
const selection = readService(ctx, 'agentDefaultModel')
|
|
130
|
+
if (selection === null) return null
|
|
131
|
+
try {
|
|
132
|
+
const current = typeof selection.currentSelection === 'function' ? selection.currentSelection() : null
|
|
133
|
+
if (current === null || typeof current !== 'object') return null
|
|
134
|
+
const model = typeof current.model === 'string' ? current.model : ''
|
|
135
|
+
if (model === '') return null
|
|
136
|
+
const provider = typeof current.provider === 'string' ? current.provider : ''
|
|
137
|
+
return provider === '' ? model : provider + '/' + model
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readCwd(ctx, agent) {
|
|
144
|
+
const directory = readService(ctx, 'workingDirectory')
|
|
145
|
+
if (directory === null) return ''
|
|
146
|
+
if (agent !== null) {
|
|
147
|
+
try {
|
|
148
|
+
const ensured = await directory.ensure(agent)
|
|
149
|
+
if (typeof ensured === 'string' && ensured !== '') return ensured
|
|
150
|
+
} catch {
|
|
151
|
+
/* fall through to the deployment fallback */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return typeof directory.defaultDirectory === 'string' ? directory.defaultDirectory : ''
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readGitBranch(cwd) {
|
|
158
|
+
if (cwd === '') return null
|
|
159
|
+
try {
|
|
160
|
+
const head = (await readFile(join(cwd, '.git', 'HEAD'), 'utf8')).trim()
|
|
161
|
+
const match = /^ref:\s*refs\/heads\/(.+)$/u.exec(head)
|
|
162
|
+
if (match !== null && typeof match[1] === 'string' && match[1] !== '') return match[1]
|
|
163
|
+
return /^[0-9a-f]{7,40}$/u.test(head) ? head : null
|
|
164
|
+
} catch {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function display(value) {
|
|
170
|
+
return value === null || value === '' ? UNKNOWN : value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderSnapshot(value) {
|
|
174
|
+
const snapshot = value !== null && typeof value === 'object' ? value : {}
|
|
175
|
+
const generatedAt = typeof snapshot.generatedAt === 'string' ? snapshot.generatedAt : ''
|
|
176
|
+
const lines = ['DSH 巡检报告 ' + generatedAt]
|
|
177
|
+
const sections = Array.isArray(snapshot.sections) ? snapshot.sections : []
|
|
178
|
+
for (const section of sections) {
|
|
179
|
+
if (section === null || typeof section !== 'object') continue
|
|
180
|
+
lines.push('## ' + (typeof section.title === 'string' ? section.title : UNKNOWN))
|
|
181
|
+
const items = Array.isArray(section.items) ? section.items : []
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
if (item === null || typeof item !== 'object') continue
|
|
184
|
+
const label = typeof item.label === 'string' ? item.label : ''
|
|
185
|
+
const itemValue = typeof item.value === 'string' ? item.value : ''
|
|
186
|
+
lines.push('- ' + label + ': ' + itemValue)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function buildSnapshot(ctx, state, agent) {
|
|
193
|
+
const id = readAgentId(agent)
|
|
194
|
+
const model = readModel(ctx)
|
|
195
|
+
if (id !== null) state.session.id = id
|
|
196
|
+
if (model !== null) state.session.model = model
|
|
197
|
+
|
|
198
|
+
const cwd = await readCwd(ctx, agent)
|
|
199
|
+
const gitBranch = await readGitBranch(cwd)
|
|
200
|
+
const sessionId = state.session.id
|
|
201
|
+
const sessionModel = state.session.model
|
|
202
|
+
const gate = state.gateEnabled
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
generatedAt: new Date().toISOString(),
|
|
206
|
+
workspace: { cwd, gitBranch },
|
|
207
|
+
session: { id: sessionId, model: sessionModel },
|
|
208
|
+
plugin: { version: (await readOwnManifest()).version, gateEnabled: gate, toolName: TOOL_NAME, command: '/' + COMMAND_NAME },
|
|
209
|
+
sections: [
|
|
210
|
+
{
|
|
211
|
+
title: '工作区',
|
|
212
|
+
items: [
|
|
213
|
+
{ label: '目录', value: display(cwd) },
|
|
214
|
+
{ label: 'Git 分支', value: display(gitBranch) },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
title: '会话',
|
|
219
|
+
items: [
|
|
220
|
+
{ label: '会话 ID', value: display(sessionId) },
|
|
221
|
+
{ label: '模型', value: display(sessionModel) },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
title: '插件',
|
|
226
|
+
items: [
|
|
227
|
+
{ label: '版本', value: (await readOwnManifest()).version },
|
|
228
|
+
{ label: '拦截门', value: gate ? '开启' : '关闭' },
|
|
229
|
+
{ label: '工具', value: TOOL_NAME },
|
|
230
|
+
{ label: '命令', value: '/' + COMMAND_NAME },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function messageOf(error) {
|
|
238
|
+
return error instanceof Error ? error.message : String(error)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let ownManifestPromise = null
|
|
242
|
+
|
|
243
|
+
async function loadOwnManifest() {
|
|
244
|
+
try {
|
|
245
|
+
const text = await readFile(new URL('../package.json', import.meta.url), 'utf8')
|
|
246
|
+
const parsed = JSON.parse(text)
|
|
247
|
+
const engines = parsed !== null && typeof parsed === 'object' && parsed.engines !== null && typeof parsed.engines === 'object'
|
|
248
|
+
? parsed.engines
|
|
249
|
+
: {}
|
|
250
|
+
return {
|
|
251
|
+
version: typeof parsed.version === 'string' && parsed.version !== '' ? parsed.version : VERSION,
|
|
252
|
+
dshRange: typeof engines.dsh === 'string' && engines.dsh !== '' ? engines.dsh : null,
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
return { version: VERSION, dshRange: null }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readOwnManifest() {
|
|
260
|
+
if (ownManifestPromise === null) ownManifestPromise = loadOwnManifest()
|
|
261
|
+
return ownManifestPromise
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u
|
|
265
|
+
const COMPARATOR = /^(>=|<=|>|<|=|\^|~)?(\S+)$/u
|
|
266
|
+
|
|
267
|
+
function parseSemver(text) {
|
|
268
|
+
const match = typeof text === 'string' ? SEMVER.exec(text.trim()) : null
|
|
269
|
+
if (match === null) return null
|
|
270
|
+
return {
|
|
271
|
+
major: Number(match[1]),
|
|
272
|
+
minor: Number(match[2]),
|
|
273
|
+
patch: Number(match[3]),
|
|
274
|
+
pre: match[4] === undefined ? [] : match[4].split('.'),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function compareIdentifier(left, right) {
|
|
279
|
+
const leftNumeric = /^\d+$/u.test(left)
|
|
280
|
+
const rightNumeric = /^\d+$/u.test(right)
|
|
281
|
+
if (leftNumeric && rightNumeric) return Number(left) - Number(right)
|
|
282
|
+
if (leftNumeric) return -1
|
|
283
|
+
if (rightNumeric) return 1
|
|
284
|
+
return left === right ? 0 : left < right ? -1 : 1
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function compareSemver(left, right) {
|
|
288
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
289
|
+
if (left[key] !== right[key]) return left[key] - right[key]
|
|
290
|
+
}
|
|
291
|
+
if (left.pre.length === 0 && right.pre.length === 0) return 0
|
|
292
|
+
if (left.pre.length === 0) return 1
|
|
293
|
+
if (right.pre.length === 0) return -1
|
|
294
|
+
const length = Math.max(left.pre.length, right.pre.length)
|
|
295
|
+
for (let index = 0; index < length; index += 1) {
|
|
296
|
+
const leftPart = left.pre[index]
|
|
297
|
+
const rightPart = right.pre[index]
|
|
298
|
+
if (leftPart === undefined) return -1
|
|
299
|
+
if (rightPart === undefined) return 1
|
|
300
|
+
const delta = compareIdentifier(leftPart, rightPart)
|
|
301
|
+
if (delta !== 0) return delta
|
|
302
|
+
}
|
|
303
|
+
return 0
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function upperBound(operator, target) {
|
|
307
|
+
if (operator === '^') {
|
|
308
|
+
if (target.major > 0) return { major: target.major + 1, minor: 0, patch: 0, pre: [] }
|
|
309
|
+
if (target.minor > 0) return { major: 0, minor: target.minor + 1, patch: 0, pre: [] }
|
|
310
|
+
return { major: 0, minor: 0, patch: target.patch + 1, pre: [] }
|
|
311
|
+
}
|
|
312
|
+
return { major: target.major, minor: target.minor + 1, patch: 0, pre: [] }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** true/false when the range is a plain comparator set; null when it is not one this host can parse offline. */
|
|
316
|
+
function satisfiesRange(version, range) {
|
|
317
|
+
const text = typeof range === 'string' ? range.trim() : ''
|
|
318
|
+
if (text === '' || text === '*' || text === 'x') return true
|
|
319
|
+
if (text.includes('||')) return null
|
|
320
|
+
for (const token of text.split(/\s+/u).filter(part => part !== '')) {
|
|
321
|
+
const match = COMPARATOR.exec(token)
|
|
322
|
+
if (match === null) return null
|
|
323
|
+
const operator = match[1] === undefined ? '=' : match[1]
|
|
324
|
+
const target = parseSemver(match[2])
|
|
325
|
+
if (target === null) return null
|
|
326
|
+
const delta = compareSemver(version, target)
|
|
327
|
+
if (operator === '>=' && delta < 0) return false
|
|
328
|
+
if (operator === '>' && delta <= 0) return false
|
|
329
|
+
if (operator === '<=' && delta > 0) return false
|
|
330
|
+
if (operator === '<' && delta >= 0) return false
|
|
331
|
+
if (operator === '=' && delta !== 0) return false
|
|
332
|
+
if (operator === '^' || operator === '~') {
|
|
333
|
+
if (delta < 0) return false
|
|
334
|
+
if (compareSemver(version, upperBound(operator, target)) >= 0) return false
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function readDshVersion(ctx) {
|
|
341
|
+
const profile = readService(ctx, 'profileContext')
|
|
342
|
+
const dir = profile !== null && typeof profile.dir === 'string' && profile.dir !== '' ? profile.dir : null
|
|
343
|
+
if (dir === null) return null
|
|
344
|
+
try {
|
|
345
|
+
const parts = APP_BOOT_PACKAGE.split('/')
|
|
346
|
+
const text = await readFile(join(dir, 'node_modules', ...parts, 'package.json'), 'utf8')
|
|
347
|
+
const parsed = JSON.parse(text)
|
|
348
|
+
const version = parsed !== null && typeof parsed === 'object' ? parsed.version : undefined
|
|
349
|
+
return typeof version === 'string' && version !== '' ? version : null
|
|
350
|
+
} catch {
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function readLocale(ctx) {
|
|
356
|
+
const settings = readService(ctx, 'settings')
|
|
357
|
+
if (settings === null || typeof settings.describe !== 'function') return null
|
|
358
|
+
const descriptors = settings.describe()
|
|
359
|
+
if (!Array.isArray(descriptors)) return null
|
|
360
|
+
for (const descriptor of descriptors) {
|
|
361
|
+
if (descriptor === null || typeof descriptor !== 'object') continue
|
|
362
|
+
if (String(descriptor.ns) !== LOCALE_NAMESPACE) continue
|
|
363
|
+
const value = descriptor.value !== null && typeof descriptor.value === 'object' ? descriptor.value : {}
|
|
364
|
+
const preference = typeof value[LOCALE_FIELD] === 'string' ? value[LOCALE_FIELD] : ''
|
|
365
|
+
return preference === '' ? null : preference
|
|
366
|
+
}
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readMarketIndexes(ctx) {
|
|
371
|
+
const market = readService(ctx, 'pluginMarket')
|
|
372
|
+
if (market === null || typeof market.listIndexes !== 'function') return { available: false, entries: [] }
|
|
373
|
+
const entries = market.listIndexes()
|
|
374
|
+
if (!Array.isArray(entries)) return { available: true, entries: [] }
|
|
375
|
+
return { available: true, entries: entries.filter(entry => entry !== null && typeof entry === 'object') }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function checkEngine(manifest, dshVersion) {
|
|
379
|
+
if (dshVersion === null) {
|
|
380
|
+
return {
|
|
381
|
+
status: 'unknown',
|
|
382
|
+
detail: '读不到 DSH 运行时版本',
|
|
383
|
+
hint: '需要 profileContext 服务,且 <profile>/node_modules/' + APP_BOOT_PACKAGE + '/package.json 可读',
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const range = manifest.dshRange
|
|
387
|
+
if (range === null) {
|
|
388
|
+
return { status: 'warn', detail: '运行 ' + dshVersion + ',package.json 未声明 engines.dsh', hint: '补上 engines.dsh 后本项可比对兼容性' }
|
|
389
|
+
}
|
|
390
|
+
const parsed = parseSemver(dshVersion)
|
|
391
|
+
const satisfied = parsed === null ? null : satisfiesRange(parsed, range)
|
|
392
|
+
if (satisfied === null) {
|
|
393
|
+
return { status: 'unknown', detail: '运行 ' + dshVersion + ',要求 ' + range + '(范围写法无法离线解析)', hint: '该范围不是简单比较式,请人工确认兼容性' }
|
|
394
|
+
}
|
|
395
|
+
if (satisfied) return { status: 'ok', detail: '运行 ' + dshVersion + ',要求 ' + range, hint: null }
|
|
396
|
+
return { status: 'fail', detail: '运行 ' + dshVersion + ',要求 ' + range, hint: '升级 DSH,或改用与本机版本兼容的插件版本' }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function checkCwd(cwd) {
|
|
400
|
+
if (cwd === '') return { status: 'warn', detail: '读不到工作目录', hint: '需要 workingDirectory 服务,或本会话尚未选定目录' }
|
|
401
|
+
return { status: 'ok', detail: cwd, hint: null }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function checkModel(model) {
|
|
405
|
+
if (model === null) return { status: 'unknown', detail: '读不到默认模型', hint: '需要 agentDefaultModel 服务,或在设置里选定一次默认模型' }
|
|
406
|
+
return { status: 'ok', detail: model, hint: null }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function checkGate(gateEnabled) {
|
|
410
|
+
if (gateEnabled) return { status: 'ok', detail: '拦截门已开启', hint: null }
|
|
411
|
+
return { status: 'warn', detail: '拦截门已关闭', hint: '用 /showcase on 重新开启' }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function checkLocale(locale) {
|
|
415
|
+
if (locale === null) return { status: 'unknown', detail: '读不到宿主语言偏好', hint: '在设置里显式选择界面语言后,本项会显示 locale 偏好' }
|
|
416
|
+
return { status: 'ok', detail: locale, hint: null }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function checkMarket(market) {
|
|
420
|
+
if (!market.available) return { status: 'unknown', detail: '读不到市场索引服务 pluginMarket', hint: '该部署未挂载插件市场时本项可以忽略' }
|
|
421
|
+
if (market.entries.length === 0) return { status: 'warn', detail: '未配置市场索引源', hint: '在设置 → 插件市场里添加索引 URL' }
|
|
422
|
+
const describe = entry => (typeof entry.url === 'string' && entry.url !== '' ? entry.url : UNKNOWN)
|
|
423
|
+
const broken = market.entries.filter(entry => typeof entry.error === 'string' && entry.error !== '')
|
|
424
|
+
if (broken.length > 0) {
|
|
425
|
+
return {
|
|
426
|
+
status: 'warn',
|
|
427
|
+
detail: market.entries.length + ' 个索引源,' + broken.length + ' 个不可用: ' + broken.map(describe).join(', '),
|
|
428
|
+
hint: '修正或删除不可用的索引 URL',
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { status: 'ok', detail: market.entries.length + ' 个索引源: ' + market.entries.map(describe).join(', '), hint: null }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function runCheck(id, title, produce) {
|
|
435
|
+
try {
|
|
436
|
+
const result = await produce()
|
|
437
|
+
if (result === null || typeof result !== 'object') {
|
|
438
|
+
return { id, title, status: 'unknown', detail: '检查未返回结果', hint: HINT_FAULT }
|
|
439
|
+
}
|
|
440
|
+
const status = STATUS_LABELS[result.status] === undefined ? 'unknown' : result.status
|
|
441
|
+
return {
|
|
442
|
+
id,
|
|
443
|
+
title,
|
|
444
|
+
status,
|
|
445
|
+
detail: typeof result.detail === 'string' ? result.detail : '',
|
|
446
|
+
hint: typeof result.hint === 'string' && result.hint !== '' ? result.hint : null,
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return { id, title, status: 'unknown', detail: '检查异常: ' + messageOf(error), hint: HINT_FAULT }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildHealthSchema() {
|
|
454
|
+
return {
|
|
455
|
+
type: 'object',
|
|
456
|
+
additionalProperties: false,
|
|
457
|
+
required: ['generatedAt', 'pluginVersion', 'dshVersion', 'summary', 'checks'],
|
|
458
|
+
properties: {
|
|
459
|
+
generatedAt: { type: 'string', description: 'ISO 8601 timestamp of this report.' },
|
|
460
|
+
pluginVersion: { type: 'string' },
|
|
461
|
+
dshVersion: { ...nullableString(), description: 'Running DSH version, null when unreadable.' },
|
|
462
|
+
summary: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
additionalProperties: false,
|
|
465
|
+
required: ['ok', 'warn', 'fail', 'unknown'],
|
|
466
|
+
properties: {
|
|
467
|
+
ok: { type: 'integer' },
|
|
468
|
+
warn: { type: 'integer' },
|
|
469
|
+
fail: { type: 'integer' },
|
|
470
|
+
unknown: { type: 'integer' },
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
checks: {
|
|
474
|
+
type: 'array',
|
|
475
|
+
items: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
additionalProperties: false,
|
|
478
|
+
required: ['id', 'title', 'status', 'detail', 'hint'],
|
|
479
|
+
properties: {
|
|
480
|
+
id: { type: 'string' },
|
|
481
|
+
title: { type: 'string' },
|
|
482
|
+
status: { type: 'string', enum: ['ok', 'warn', 'fail', 'unknown'] },
|
|
483
|
+
detail: { type: 'string' },
|
|
484
|
+
hint: { ...nullableString(), description: 'Next step, null when there is none.' },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function renderHealth(value) {
|
|
493
|
+
const report = value !== null && typeof value === 'object' ? value : {}
|
|
494
|
+
const generatedAt = typeof report.generatedAt === 'string' ? report.generatedAt : ''
|
|
495
|
+
const lines = ['DSH 体检报告 ' + generatedAt]
|
|
496
|
+
const checks = Array.isArray(report.checks) ? report.checks : []
|
|
497
|
+
for (const check of checks) {
|
|
498
|
+
if (check === null || typeof check !== 'object') continue
|
|
499
|
+
const label = STATUS_LABELS[check.status] === undefined ? STATUS_LABELS.unknown : STATUS_LABELS[check.status]
|
|
500
|
+
const title = typeof check.title === 'string' ? check.title : UNKNOWN
|
|
501
|
+
const detail = typeof check.detail === 'string' ? check.detail : ''
|
|
502
|
+
lines.push('[' + label + '] ' + title + ' — ' + detail)
|
|
503
|
+
if (typeof check.hint === 'string' && check.hint !== '') lines.push(' ↳ ' + check.hint)
|
|
504
|
+
}
|
|
505
|
+
const summary = report.summary !== null && typeof report.summary === 'object' ? report.summary : {}
|
|
506
|
+
const count = key => (typeof summary[key] === 'number' ? summary[key] : 0)
|
|
507
|
+
lines.push('合计: ok ' + count('ok') + ' / warn ' + count('warn') + ' / fail ' + count('fail') + ' / unknown ' + count('unknown'))
|
|
508
|
+
return lines.join('\n')
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function buildHealthReport(ctx, state, agent) {
|
|
512
|
+
const manifest = await readOwnManifest()
|
|
513
|
+
const [dshVersion, cwd] = await Promise.all([readDshVersion(ctx), readCwd(ctx, agent)])
|
|
514
|
+
const model = readModel(ctx)
|
|
515
|
+
const checks = await Promise.all([
|
|
516
|
+
runCheck('engine.dsh', 'DSH 版本', () => checkEngine(manifest, dshVersion)),
|
|
517
|
+
runCheck('workspace.cwd', '工作目录', () => checkCwd(cwd)),
|
|
518
|
+
runCheck('model.default', '默认模型', () => checkModel(model)),
|
|
519
|
+
runCheck('plugin.gate', '拦截门', () => checkGate(state.gateEnabled)),
|
|
520
|
+
runCheck('plugin.locale', '界面语言', () => checkLocale(readLocale(ctx))),
|
|
521
|
+
runCheck('market.index', '市场索引', () => checkMarket(readMarketIndexes(ctx))),
|
|
522
|
+
])
|
|
523
|
+
const summary = { ok: 0, warn: 0, fail: 0, unknown: 0 }
|
|
524
|
+
for (const check of checks) summary[check.status] += 1
|
|
525
|
+
return { generatedAt: new Date().toISOString(), pluginVersion: manifest.version, dshVersion, summary, checks }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function aboutText(ctx, state) {
|
|
529
|
+
const manifest = await readOwnManifest()
|
|
530
|
+
const dshVersion = await readDshVersion(ctx)
|
|
531
|
+
return 'dsh-plugin-showcase ' + manifest.version
|
|
532
|
+
+ ' · 工具 ' + TOOL_NAME + '、' + DOCTOR_TOOL_NAME
|
|
533
|
+
+ ' · 命令 /' + COMMAND_NAME + ' on|off|doctor|about'
|
|
534
|
+
+ ' · 拦截门' + (state.gateEnabled ? '已开启' : '已关闭')
|
|
535
|
+
+ ' · DSH ' + (dshVersion === null ? UNKNOWN : dshVersion)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function collectCommandText(exec) {
|
|
539
|
+
const args = exec !== null && typeof exec === 'object' ? exec.arguments : null
|
|
540
|
+
if (args === null || typeof args !== 'object') return ''
|
|
541
|
+
const parts = []
|
|
542
|
+
for (const key of COMMAND_ARG_KEYS) {
|
|
543
|
+
if (!Object.hasOwn(args, key)) continue
|
|
544
|
+
const value = args[key]
|
|
545
|
+
if (typeof value === 'string') parts.push(value)
|
|
546
|
+
else if (Array.isArray(value)) {
|
|
547
|
+
for (const entry of value) {
|
|
548
|
+
if (typeof entry === 'string') parts.push(entry)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return parts.join('\n').toLowerCase()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// A shutdown only counts in command position: "echo shutdown" and
|
|
556
|
+
// "npm run shutdown-check" are arguments, not a power action.
|
|
557
|
+
function hasShutdownCommand(haystack) {
|
|
558
|
+
for (const segment of haystack.split(/[\n;&|]+/u)) {
|
|
559
|
+
const tokens = segment.split(/\s+/u).filter(token => token !== '')
|
|
560
|
+
let index = 0
|
|
561
|
+
while (index < tokens.length && ELEVATION_PREFIXES.includes(tokens[index])) index += 1
|
|
562
|
+
const word = tokens[index]
|
|
563
|
+
if (word !== undefined && SHUTDOWN_WORD.test(word)) return true
|
|
564
|
+
}
|
|
565
|
+
return false
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function isDestructive(exec) {
|
|
569
|
+
const haystack = collectCommandText(exec)
|
|
570
|
+
if (haystack === '') return false
|
|
571
|
+
if (hasShutdownCommand(haystack)) return true
|
|
572
|
+
if (DESTRUCTIVE_PATTERNS.some(pattern => haystack.includes(pattern))) return true
|
|
573
|
+
const compact = haystack.replace(/\s+/gu, '')
|
|
574
|
+
return COMPACT_DESTRUCTIVE_PATTERNS.some(pattern => compact.includes(pattern))
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function runCommand(ctx, state, invocation) {
|
|
578
|
+
const raw = invocation !== null && typeof invocation === 'object' && typeof invocation.rawInput === 'string'
|
|
579
|
+
? invocation.rawInput.trim().toLowerCase()
|
|
580
|
+
: ''
|
|
581
|
+
if (raw === 'on') {
|
|
582
|
+
state.gateEnabled = true
|
|
583
|
+
return { kind: 'success', text: 'showcase 拦截门: 已开启' }
|
|
584
|
+
}
|
|
585
|
+
if (raw === 'off') {
|
|
586
|
+
state.gateEnabled = false
|
|
587
|
+
return { kind: 'success', text: 'showcase 拦截门: 已关闭' }
|
|
588
|
+
}
|
|
589
|
+
const agent = invocation !== null && typeof invocation === 'object' ? invocation.agent ?? null : null
|
|
590
|
+
if (raw === 'doctor') return { kind: 'success', text: renderHealth(await buildHealthReport(ctx, state, agent)) }
|
|
591
|
+
if (raw === 'about') return { kind: 'success', text: await aboutText(ctx, state) }
|
|
592
|
+
if (raw !== '') return { kind: 'success', text: USAGE }
|
|
593
|
+
return { kind: 'success', text: renderSnapshot(await buildSnapshot(ctx, state, agent)) }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @param {import('@deepseek-ai/cordis').Context} ctx */
|
|
597
|
+
export function apply(ctx) {
|
|
598
|
+
const state = { gateEnabled: true, session: { id: null, model: null } }
|
|
599
|
+
|
|
600
|
+
const inspectTool = {
|
|
601
|
+
name: TOOL_NAME,
|
|
602
|
+
description: 'Inspect this DSH host and return one snapshot: working directory, git branch, session identity, and the showcase plugin state.',
|
|
603
|
+
parameters: {
|
|
604
|
+
type: 'object',
|
|
605
|
+
additionalProperties: false,
|
|
606
|
+
properties: {},
|
|
607
|
+
required: [],
|
|
608
|
+
},
|
|
609
|
+
output: {
|
|
610
|
+
schema: buildSnapshotSchema(),
|
|
611
|
+
render: (_args, value) => [{ type: 'text', text: renderSnapshot(value) }],
|
|
612
|
+
},
|
|
613
|
+
async execute(_args, exec) {
|
|
614
|
+
const agent = exec !== null && typeof exec === 'object' ? exec.agent ?? null : null
|
|
615
|
+
return await buildSnapshot(ctx, state, agent)
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const doctorTool = {
|
|
620
|
+
name: DOCTOR_TOOL_NAME,
|
|
621
|
+
description: 'Run the DSH health check and return one report: engine version, working directory, default model, gate state, UI language, and market index sources.',
|
|
622
|
+
parameters: {
|
|
623
|
+
type: 'object',
|
|
624
|
+
additionalProperties: false,
|
|
625
|
+
properties: {},
|
|
626
|
+
required: [],
|
|
627
|
+
},
|
|
628
|
+
output: {
|
|
629
|
+
schema: buildHealthSchema(),
|
|
630
|
+
render: (_args, value) => [{ type: 'text', text: renderHealth(value) }],
|
|
631
|
+
},
|
|
632
|
+
async execute(_args, exec) {
|
|
633
|
+
const agent = exec !== null && typeof exec === 'object' ? exec.agent ?? null : null
|
|
634
|
+
return await buildHealthReport(ctx, state, agent)
|
|
635
|
+
},
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
ctx.effect(() => ctx.tools.register(inspectTool), 'showcase: inspect tool')
|
|
639
|
+
ctx.effect(() => ctx.tools.register(doctorTool), 'showcase: doctor tool')
|
|
640
|
+
|
|
641
|
+
ctx.effect(() => ctx.on('tools/pre-execute', async (exec, next) => {
|
|
642
|
+
let decision = null
|
|
643
|
+
try {
|
|
644
|
+
if (state.gateEnabled && isDestructive(exec)) decision = { kind: 'deny', reason: DENY_REASON }
|
|
645
|
+
} catch {
|
|
646
|
+
/* a plugin fault must never block tool calls */
|
|
647
|
+
decision = null
|
|
648
|
+
}
|
|
649
|
+
return decision === null ? next() : decision
|
|
650
|
+
}), 'showcase: pre-execute gate')
|
|
651
|
+
|
|
652
|
+
ctx.effect(() => ctx.commands.register({
|
|
653
|
+
name: COMMAND_NAME,
|
|
654
|
+
description: 'Inspect this DSH host. Usage: /showcase [on|off|doctor|about]',
|
|
655
|
+
input: { hint: '[on|off|doctor|about]' },
|
|
656
|
+
handler: invocation => runCommand(ctx, state, invocation),
|
|
657
|
+
}), 'showcase: /showcase command')
|
|
658
|
+
|
|
659
|
+
ctx.provide('showcase', {
|
|
660
|
+
snapshot: () => buildSnapshot(ctx, state, null),
|
|
661
|
+
gateEnabled: () => state.gateEnabled,
|
|
662
|
+
setGateEnabled: enabled => { state.gateEnabled = enabled === true },
|
|
663
|
+
})
|
|
664
|
+
}
|
package/locale/en.json
ADDED