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/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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Showcase",
4
+ "description": "DeepSeek Harness reference plugin: tools, hooks, commands, a settings card and a sidebar panel."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "功能展示",
4
+ "description": "DeepSeek Harness 参考插件:工具、钩子、命令、设置卡片与侧栏面板。"
5
+ }
6
+ }