dsh-plugin-discover 0.0.0-stage → 1.0.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/client.js ADDED
@@ -0,0 +1,477 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-plugin-discover',
3
+ factory(require) {
4
+ const React = require('react')
5
+ const h = React.createElement
6
+
7
+ const NS = 'dshPluginDiscover'
8
+ const COPY = {
9
+ zh: {
10
+ summary: '本地插件检索 · 中文口语查询 · 过滤、向量化增强与 README 抓取',
11
+ indexDir: '索引目录',
12
+ indexDirHint: '默认按用户全局固定。修改后 CLI 需用 --index-dir 指向同一目录。',
13
+ updateCycle: '索引更新周期',
14
+ day: '天(1 天)',
15
+ week: '周(7 天,默认)',
16
+ month: '月(30 天)',
17
+ quarter: '3 个月(90 天)',
18
+ updateCycleHint: '用户发起插件检索时检查;索引超过此周期会先等待刷新完成,再开始检索。',
19
+ searchSection: '检索与过滤',
20
+ filterPreset: '过滤档位',
21
+ strict: '严格',
22
+ balanced: '均衡(默认)',
23
+ loose: '宽松',
24
+ offFilter: '关闭',
25
+ strictDesc: '保留验证等级 L5、安全审计 audited low、兼容性已知通过的条目。',
26
+ balancedDesc: '要求验证等级不低于下限;排除 audited high/critical;未知验证和未知兼容保留。',
27
+ looseDesc: '只排除 audited high/critical;不设验证等级下限,兼容性全部保留。',
28
+ offDesc: '只排除 audited critical;其余全部保留。',
29
+ autoRelaxHint: '候选不足 3 条时,会自动放宽一档。',
30
+ minVerificationLevel: '最低验证等级(仅均衡档)',
31
+ minVerificationHint: '0 = 不限;1–5 要求验证等级 ≥ N。未知验证等级不会被删;严格档固定 L5,宽松/关闭不设下限。',
32
+ noFloor: '0 — 不限',
33
+ resultLimit: '结果返回数量(top N)',
34
+ resultLimitHint: '范围 1–20,默认 10;综合检索与向量化增强后交给会话 LLM 确认。',
35
+ noiseFilter: '无关结果筛选强度',
36
+ noiseNone: 'none(关闭)',
37
+ noiseLow: 'low(宽松)',
38
+ noiseMedium: 'medium(均衡,默认)',
39
+ noiseHigh: 'high(严格)',
40
+ noiseFilterHint: 'none 关闭 gate1/gate2;low/medium/high 逐级提高词面覆盖率和向量相似度阈值。只影响 noMatch 判断,不改变排序。',
41
+ vectorMode: '向量化检索增强',
42
+ vectorSection: '向量化检索增强',
43
+ vectorOff: 'off',
44
+ vectorBalance: 'balance',
45
+ vectorHigh: 'high',
46
+ vectorHint: '平衡和极强模式需要下载向量化模型包;极强模式还需要联网抓取 GitHub README 文档,速度较慢。',
47
+ vectorTopN: '向量化范围(top N)',
48
+ vectorTopNHint: '范围 2–200,默认 50;检索算法得到的 topN 结果作为向量化增强候选。向量化增强为 off 时不生效。',
49
+ readmeSection: 'README 抓取',
50
+ readmeHint: '仅在向量化检索增强为 high 时生效;保存后热生效。',
51
+ configTitle: '配置',
52
+ requestTimeout: '单请求超时(秒)',
53
+ requestTimeoutHint: '默认 30;范围 5–300。',
54
+ totalBudget: 'README抓取总超时时长(分钟)',
55
+ totalBudgetHint: '范围 1–10,默认 5;超时后剩余记录直接走 npm CDN。',
56
+ githubConcurrency: 'GitHub 请求并发',
57
+ githubConcurrencyHint: '默认 4;范围 1–8。',
58
+ buildHint: '更新索引:npx dsh-plugin-discover build,或在会话里让模型调用 plugin_discover 并传 refresh: true。',
59
+ save: '保存',
60
+ saving: '保存中…',
61
+ loading: '加载中…',
62
+ saved: '已保存,配置会热生效。',
63
+ saveFailed: '保存失败,请重试。',
64
+ noChanges: '没有改动。',
65
+ unavailable: '配置暂不可用。',
66
+ readOnly: '当前配置只读。',
67
+ },
68
+ en: {
69
+ summary: 'Local plugin search · Chinese natural language · filters, vector enhancement, and README fetching',
70
+ indexDir: 'Index directory',
71
+ indexDirHint: 'Shared per user by default. After changing it, point the CLI at the same directory with --index-dir.',
72
+ updateCycle: 'Index update cycle',
73
+ day: 'Day (1 day)',
74
+ week: 'Week (7 days, default)',
75
+ month: 'Month (30 days)',
76
+ quarter: '3 months (90 days)',
77
+ updateCycleHint: 'Checked when the user asks to search plugins; an older index is refreshed and the search waits for it.',
78
+ searchSection: 'Search and filters',
79
+ filterPreset: 'Filter preset',
80
+ strict: 'Strict',
81
+ balanced: 'Balanced (default)',
82
+ loose: 'Loose',
83
+ offFilter: 'Off',
84
+ strictDesc: 'Keeps only audited-low, L5, known-compatible entries.',
85
+ balancedDesc: 'Applies the verification floor; excludes audited high/critical; keeps unknown verification and compatibility.',
86
+ looseDesc: 'Excludes audited high/critical only; no verification floor; compatibility is unrestricted.',
87
+ offDesc: 'Excludes audited critical only; everything else is kept.',
88
+ autoRelaxHint: 'If fewer than three entries survive, the filter automatically relaxes one step.',
89
+ minVerificationLevel: 'Minimum verification level (balanced only)',
90
+ minVerificationHint: '0 = no floor; 1–5 require level ≥ N. Unknown verification is kept; strict fixes L5; loose/off have no floor.',
91
+ noFloor: '0 — no floor',
92
+ resultLimit: 'Result count (top N)',
93
+ resultLimitHint: 'Range 1–20, default 10; the combined lexical and vector ranking is handed to the session LLM.',
94
+ noiseFilter: 'Irrelevant-result filter strength',
95
+ noiseNone: 'none (off)',
96
+ noiseLow: 'low (lenient)',
97
+ noiseMedium: 'medium (balanced, default)',
98
+ noiseHigh: 'high (strict)',
99
+ noiseFilterHint: 'none disables gate1/gate2; low/medium/high raise the lexical coverage and vector similarity thresholds. It only decides noMatch and never changes ranking.',
100
+ vectorMode: 'Vector search enhancement',
101
+ vectorSection: 'Vector search enhancement',
102
+ vectorOff: 'off',
103
+ vectorBalance: 'balance',
104
+ vectorHigh: 'high',
105
+ vectorHint: 'Balance and high need the optional vector model package; high also fetches GitHub READMEs over the network, so it is slower.',
106
+ vectorTopN: 'Vectorization range (top N)',
107
+ vectorTopNHint: 'Range 2–200, default 50; the topN lexical results become vector candidates. Ignored when enhancement is off.',
108
+ readmeSection: 'README fetching',
109
+ readmeHint: 'Applies only when vector enhancement is high; changes take effect live.',
110
+ configTitle: 'Configuration',
111
+ requestTimeout: 'Single-request timeout (seconds)',
112
+ requestTimeoutHint: 'Default 30; range 5–300.',
113
+ totalBudget: 'Total README fetch timeout (minutes)',
114
+ totalBudgetHint: 'Range 1–10, default 5; after it, remaining records go straight to the npm CDN.',
115
+ githubConcurrency: 'GitHub request concurrency',
116
+ githubConcurrencyHint: 'Default 4; range 1–8.',
117
+ buildHint: 'Update the index with npx dsh-plugin-discover build, or call plugin_discover with refresh: true.',
118
+ save: 'Save',
119
+ saving: 'Saving…',
120
+ loading: 'Loading…',
121
+ saved: 'Saved. The configuration reloads live.',
122
+ saveFailed: 'Save failed. Try again.',
123
+ noChanges: 'No changes.',
124
+ unavailable: 'Configuration is unavailable.',
125
+ readOnly: 'This configuration is read-only.',
126
+ },
127
+ }
128
+
129
+ function option(value, label) {
130
+ return h('option', { key: value, value }, label)
131
+ }
132
+
133
+ function field(label, hint, control) {
134
+ return h('label', { style: { display: 'grid', gap: 6 } }, [
135
+ h('span', { key: 'label', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, label),
136
+ control,
137
+ hint ? h('span', { key: 'hint', style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.5 } }, hint) : null,
138
+ ])
139
+ }
140
+
141
+ const inputStyle = {
142
+ width: '100%',
143
+ boxSizing: 'border-box',
144
+ padding: '6px 8px',
145
+ borderRadius: 6,
146
+ border: '1px solid var(--dsw-alias-border-l1)',
147
+ background: 'var(--dsw-alias-bg-layer-1)',
148
+ color: 'var(--dsw-alias-label-primary)',
149
+ fontSize: 13,
150
+ fontFamily: 'inherit',
151
+ }
152
+
153
+ function displayVectorMode(value) {
154
+ if (value === 'fields') return 'balance'
155
+ if (value === 'readme') return 'high'
156
+ return value === 'balance' || value === 'high' ? value : 'off'
157
+ }
158
+
159
+ function ConfigPage(props) {
160
+ const { t, form } = props
161
+ const state = form?.state
162
+ const value = state?.value ?? {}
163
+ const [draft, setDraft] = React.useState(() => ({
164
+ indexDir: String(value.indexDir ?? ''),
165
+ updateCycle: String(value.updateCycle ?? 'week'),
166
+ filterPreset: String(value.filterPreset ?? 'balanced'),
167
+ minVerificationLevel: Number(value.minVerificationLevel ?? 4),
168
+ resultLimit: Number(value.resultLimit ?? 10),
169
+ noiseFilter: String(value.noiseFilter ?? 'medium'),
170
+ vectorMode: displayVectorMode(value.vector?.mode),
171
+ vectorTopN: Number(value.vector?.topN ?? 50),
172
+ requestTimeoutSeconds: Number(value.readme?.requestTimeoutSeconds ?? 30),
173
+ totalBudgetMinutes: Number(value.readme?.totalBudgetMinutes ?? 5),
174
+ githubConcurrency: Number(value.readme?.githubConcurrency ?? 4),
175
+ }))
176
+ const [status, setStatus] = React.useState('')
177
+ const [busy, setBusy] = React.useState(false)
178
+
179
+ React.useEffect(() => {
180
+ setDraft({
181
+ indexDir: String(value.indexDir ?? ''),
182
+ updateCycle: String(value.updateCycle ?? 'week'),
183
+ filterPreset: String(value.filterPreset ?? 'balanced'),
184
+ minVerificationLevel: Number(value.minVerificationLevel ?? 4),
185
+ resultLimit: Number(value.resultLimit ?? 10),
186
+ noiseFilter: String(value.noiseFilter ?? 'medium'),
187
+ vectorMode: displayVectorMode(value.vector?.mode),
188
+ vectorTopN: Number(value.vector?.topN ?? 50),
189
+ requestTimeoutSeconds: Number(value.readme?.requestTimeoutSeconds ?? 30),
190
+ totalBudgetMinutes: Number(value.readme?.totalBudgetMinutes ?? 5),
191
+ githubConcurrency: Number(value.readme?.githubConcurrency ?? 4),
192
+ })
193
+ }, [state?.revision])
194
+
195
+ if (!form || !state) return h('p', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('unavailable'))
196
+ if (state.status === 'loading') return h('p', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('loading'))
197
+ if (state.status === 'unavailable') return h('p', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('unavailable'))
198
+ const disabled = state.writable === false || busy
199
+ const presetDescriptions = [
200
+ ['strict', t('strict'), t('strictDesc')],
201
+ ['balanced', t('balanced'), t('balancedDesc')],
202
+ ['loose', t('loose'), t('looseDesc')],
203
+ ['off', t('offFilter'), t('offDesc')],
204
+ ]
205
+
206
+ const update = (key, next) => setDraft(current => ({ ...current, [key]: next }))
207
+
208
+ async function save() {
209
+ const current = state.value ?? {}
210
+ const ops = []
211
+ if (draft.indexDir !== String(current.indexDir ?? '')) ops.push({ op: 'set', path: ['indexDir'], value: draft.indexDir })
212
+ if (draft.updateCycle !== String(current.updateCycle ?? 'week')) {
213
+ ops.push({ op: 'set', path: ['updateCycle'], value: draft.updateCycle })
214
+ }
215
+ if (draft.filterPreset !== String(current.filterPreset ?? 'balanced')) ops.push({ op: 'set', path: ['filterPreset'], value: draft.filterPreset })
216
+ if (Number(draft.minVerificationLevel) !== Number(current.minVerificationLevel ?? 4)) {
217
+ ops.push({ op: 'set', path: ['minVerificationLevel'], value: Number(draft.minVerificationLevel) })
218
+ }
219
+ if (Number(draft.resultLimit) !== Number(current.resultLimit ?? 10)) {
220
+ ops.push({ op: 'set', path: ['resultLimit'], value: Number(draft.resultLimit) })
221
+ }
222
+ if (draft.noiseFilter !== String(current.noiseFilter ?? 'medium')) {
223
+ ops.push({ op: 'set', path: ['noiseFilter'], value: draft.noiseFilter })
224
+ }
225
+ if (draft.vectorMode !== displayVectorMode(current.vector?.mode)) {
226
+ ops.push({ op: 'set', path: ['vector', 'mode'], value: draft.vectorMode })
227
+ }
228
+ if (Number(draft.vectorTopN) !== Number(current.vector?.topN ?? 50)) {
229
+ ops.push({ op: 'set', path: ['vector', 'topN'], value: Number(draft.vectorTopN) })
230
+ }
231
+ if (Number(draft.requestTimeoutSeconds) !== Number(current.readme?.requestTimeoutSeconds ?? 30)) {
232
+ ops.push({ op: 'set', path: ['readme', 'requestTimeoutSeconds'], value: Number(draft.requestTimeoutSeconds) })
233
+ }
234
+ if (Number(draft.totalBudgetMinutes) !== Number(current.readme?.totalBudgetMinutes ?? 5)) {
235
+ ops.push({ op: 'set', path: ['readme', 'totalBudgetMinutes'], value: Number(draft.totalBudgetMinutes) })
236
+ }
237
+ if (Number(draft.githubConcurrency) !== Number(current.readme?.githubConcurrency ?? 4)) {
238
+ ops.push({ op: 'set', path: ['readme', 'githubConcurrency'], value: Number(draft.githubConcurrency) })
239
+ }
240
+ if (ops.length === 0) {
241
+ setStatus(t('noChanges'))
242
+ return
243
+ }
244
+ setBusy(true)
245
+ setStatus('')
246
+ try {
247
+ const ok = await form.mutate(ops, state.revision)
248
+ setStatus(ok ? t('saved') : t('saveFailed'))
249
+ } catch {
250
+ setStatus(t('saveFailed'))
251
+ } finally {
252
+ setBusy(false)
253
+ }
254
+ }
255
+
256
+ return h('div', { style: { display: 'grid', gap: 16, maxWidth: 620 } }, [
257
+ field(t('indexDir'), t('indexDirHint'), h('input', {
258
+ key: 'indexDir',
259
+ type: 'text',
260
+ value: draft.indexDir,
261
+ disabled,
262
+ spellCheck: false,
263
+ style: inputStyle,
264
+ onChange: event => update('indexDir', event.target.value),
265
+ })),
266
+ field(t('updateCycle'), t('updateCycleHint'), h('select', {
267
+ key: 'updateCycle',
268
+ value: draft.updateCycle,
269
+ disabled,
270
+ style: inputStyle,
271
+ onChange: event => update('updateCycle', event.target.value),
272
+ }, [
273
+ option('day', t('day')),
274
+ option('week', t('week')),
275
+ option('month', t('month')),
276
+ option('quarter', t('quarter')),
277
+ ])),
278
+ h('h3', { key: 'searchSection', style: { margin: '4px 0 0', fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('searchSection')),
279
+ field(t('filterPreset'), t('autoRelaxHint'), h('select', {
280
+ key: 'filterPreset',
281
+ value: draft.filterPreset,
282
+ disabled,
283
+ style: inputStyle,
284
+ onChange: event => update('filterPreset', event.target.value),
285
+ }, [
286
+ option('strict', t('strict')),
287
+ option('balanced', t('balanced')),
288
+ option('loose', t('loose')),
289
+ option('off', t('offFilter')),
290
+ ])),
291
+ h('div', {
292
+ key: 'presetDescriptions',
293
+ style: {
294
+ display: 'grid',
295
+ gap: 6,
296
+ padding: '8px 10px',
297
+ border: '1px solid var(--dsw-alias-border-l1)',
298
+ borderRadius: 6,
299
+ background: 'var(--dsw-alias-bg-layer-1)',
300
+ },
301
+ }, presetDescriptions.map(([key, label, description]) => h('div', {
302
+ key,
303
+ style: { fontSize: 11, lineHeight: 1.55, color: 'var(--dsw-alias-label-secondary)' },
304
+ }, [
305
+ h('strong', { key: 'label', style: { color: 'var(--dsw-alias-label-primary)' } }, `${label}:`),
306
+ h('span', { key: 'text' }, description),
307
+ ]))),
308
+ field(t('minVerificationLevel'), t('minVerificationHint'), h('select', {
309
+ key: 'minVerificationLevel',
310
+ value: String(draft.minVerificationLevel),
311
+ disabled,
312
+ style: inputStyle,
313
+ onChange: event => update('minVerificationLevel', Number(event.target.value)),
314
+ }, [0, 1, 2, 3, 4, 5].map(level => option(String(level), level === 0 ? t('noFloor') : `L${level}`)))),
315
+ field(t('resultLimit'), t('resultLimitHint'), h('input', {
316
+ key: 'resultLimit',
317
+ type: 'number',
318
+ min: 1,
319
+ max: 20,
320
+ step: 1,
321
+ value: draft.resultLimit,
322
+ disabled,
323
+ style: inputStyle,
324
+ onChange: event => update('resultLimit', Number(event.target.value)),
325
+ })),
326
+ field(t('noiseFilter'), t('noiseFilterHint'), h('select', {
327
+ key: 'noiseFilter',
328
+ value: draft.noiseFilter,
329
+ disabled,
330
+ style: inputStyle,
331
+ onChange: event => update('noiseFilter', event.target.value),
332
+ }, [
333
+ option('none', t('noiseNone')),
334
+ option('low', t('noiseLow')),
335
+ option('medium', t('noiseMedium')),
336
+ option('high', t('noiseHigh')),
337
+ ])),
338
+ h('h3', { key: 'vectorSection', style: { margin: '4px 0 0', fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('vectorSection')),
339
+ field(t('vectorMode'), t('vectorHint'), h('select', {
340
+ key: 'vectorMode',
341
+ value: draft.vectorMode,
342
+ disabled,
343
+ style: inputStyle,
344
+ onChange: event => update('vectorMode', event.target.value),
345
+ }, [
346
+ option('off', t('vectorOff')),
347
+ option('balance', t('vectorBalance')),
348
+ option('high', t('vectorHigh')),
349
+ ])),
350
+ field(t('vectorTopN'), t('vectorTopNHint'), h('input', {
351
+ key: 'vectorTopN',
352
+ type: 'number',
353
+ min: 2,
354
+ max: 200,
355
+ step: 1,
356
+ value: draft.vectorTopN,
357
+ disabled,
358
+ style: inputStyle,
359
+ onChange: event => update('vectorTopN', Number(event.target.value)),
360
+ })),
361
+ h('h3', { key: 'readmeSection', style: { margin: '4px 0 0', fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('readmeSection')),
362
+ field(t('requestTimeout'), t('requestTimeoutHint'), h('input', {
363
+ key: 'requestTimeoutSeconds',
364
+ type: 'number',
365
+ min: 5,
366
+ max: 300,
367
+ step: 1,
368
+ value: draft.requestTimeoutSeconds,
369
+ disabled,
370
+ style: inputStyle,
371
+ onChange: event => update('requestTimeoutSeconds', Number(event.target.value)),
372
+ })),
373
+ field(t('totalBudget'), t('totalBudgetHint'), h('input', {
374
+ key: 'totalBudgetMinutes',
375
+ type: 'number',
376
+ min: 1,
377
+ max: 10,
378
+ step: 1,
379
+ value: draft.totalBudgetMinutes,
380
+ disabled,
381
+ style: inputStyle,
382
+ onChange: event => update('totalBudgetMinutes', Number(event.target.value)),
383
+ })),
384
+ field(t('githubConcurrency'), t('githubConcurrencyHint'), h('input', {
385
+ key: 'githubConcurrency',
386
+ type: 'number',
387
+ min: 1,
388
+ max: 8,
389
+ step: 1,
390
+ value: draft.githubConcurrency,
391
+ disabled,
392
+ style: inputStyle,
393
+ onChange: event => update('githubConcurrency', Number(event.target.value)),
394
+ })),
395
+ h('p', { key: 'readmeHint', style: { margin: 0, fontSize: 11, lineHeight: 1.6, color: 'var(--dsw-alias-label-secondary)' } }, t('readmeHint')),
396
+ h('p', { key: 'buildHint', style: { margin: 0, fontSize: 11, lineHeight: 1.6, color: 'var(--dsw-alias-label-secondary)' } }, t('buildHint')),
397
+ h('div', { key: 'actions', style: { display: 'flex', alignItems: 'center', gap: 12 } }, [
398
+ h('button', {
399
+ key: 'save',
400
+ type: 'button',
401
+ disabled,
402
+ onClick: () => { void save() },
403
+ style: {
404
+ padding: '6px 16px',
405
+ borderRadius: 6,
406
+ border: 'none',
407
+ background: 'var(--dsw-alias-brand-primary)',
408
+ color: '#fff',
409
+ fontSize: 13,
410
+ cursor: disabled ? 'default' : 'pointer',
411
+ opacity: disabled ? 0.6 : 1,
412
+ },
413
+ }, busy ? t('saving') : t('save')),
414
+ status ? h('span', {
415
+ key: 'status',
416
+ role: 'status',
417
+ style: {
418
+ fontSize: 12,
419
+ color: status === t('saveFailed') ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-state-success-primary)',
420
+ },
421
+ }, status) : null,
422
+ state.writable === false ? h('span', { key: 'readonly', style: { fontSize: 12, color: 'var(--dsw-alias-state-warn-primary)' } }, t('readOnly')) : null,
423
+ ]),
424
+ ])
425
+ }
426
+
427
+ return {
428
+ inject: ['slots', 'locale', 'configForms'],
429
+ apply(ctx) {
430
+ ctx.effect(() => ctx.locale.register(NS, 'zh', COPY.zh), 'dsh-plugin-discover: zh dictionary')
431
+ ctx.effect(() => ctx.locale.register(NS, 'en', COPY.en), 'dsh-plugin-discover: en dictionary')
432
+ const configForms = ['include:discover', 'discover', 'dsh-plugin-discover', 'dsh-plugin-discover#discover']
433
+ .map(id => ctx.configForms.get(id))
434
+ const readyForm = () => configForms.find(entry => entry.getSnapshot().status === 'ready') ?? configForms[0]
435
+ const t = ctx.locale.bind(NS)
436
+
437
+ function DetailConfig(props) {
438
+ const subject = props?.subject
439
+ const [form, setForm] = React.useState(readyForm)
440
+ const [state, setState] = React.useState(() => form.getSnapshot())
441
+ React.useEffect(() => {
442
+ const offs = configForms.map(entry => entry.subscribe(() => {
443
+ const next = readyForm()
444
+ setForm(next)
445
+ setState(next.getSnapshot())
446
+ }))
447
+ return () => offs.forEach(off => off())
448
+ }, [])
449
+ if (subject?.kind !== 'bundle' || subject.pkg?.name !== 'dsh-plugin-discover') return null
450
+ return h('section', {
451
+ 'data-plugin-discover-config': true,
452
+ style: { display: 'grid', gap: 12, marginTop: 8 },
453
+ }, [
454
+ h('h4', {
455
+ key: 'title',
456
+ style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-primary)' },
457
+ }, t('configTitle')),
458
+ h(ConfigPage, {
459
+ key: 'form',
460
+ t,
461
+ form: {
462
+ state,
463
+ mutate: (ops, revision) => form.mutate(ops, revision),
464
+ },
465
+ }),
466
+ ])
467
+ }
468
+
469
+ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({
470
+ name: 'plugins.detail.section',
471
+ id: 'dsh-plugin-discover-config',
472
+ locale: NS,
473
+ }, DetailConfig))
474
+ },
475
+ }
476
+ },
477
+ })
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: discover
3
+ name: dsh-plugin-discover
package/icon.svg ADDED
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Plugin Discover">
2
+ <defs>
3
+ <linearGradient id="lens" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#4f8cff"/>
5
+ <stop offset="1" stop-color="#1f5fd0"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="4" y="4" width="56" height="56" rx="14" fill="#0f172a"/>
9
+ <rect x="9" y="9" width="46" height="46" rx="11" fill="url(#lens)" opacity="0.18"/>
10
+ <path d="M17 24h12v-4a3 3 0 0 1 6 0v4h12v12h-4a3 3 0 0 0 0 6h4v12H35v-4a3 3 0 0 0-6 0v4H17V42h4a3 3 0 0 0 0-6h-4V24z"
11
+ fill="none" stroke="#e8f0ff" stroke-width="3" stroke-linejoin="round"/>
12
+ <circle cx="41" cy="41" r="9" fill="#0f172a" stroke="#7fb2ff" stroke-width="3"/>
13
+ <path d="M47.5 47.5L53 53" stroke="#7fb2ff" stroke-width="3" stroke-linecap="round"/>
14
+ </svg>