dsh-homepage 0.1.2 → 0.1.3
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.md +1 -0
- package/README.zh.md +1 -0
- package/lib/audit-instructions.js +21 -0
- package/lib/cards/plugin-audit.js +182 -27
- package/lib/client.js +209 -28
- package/lib/framework.part.js +27 -1
- package/lib/host/audit-runner.js +90 -17
- package/lib/host/audit.js +28 -25
- package/lib/index.js +8 -1
- package/package.json +2 -1
- package/screenshots.json +4 -0
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
[](package.json)
|
|
7
|
+
[](https://awesome-dsh-plugin.com)
|
|
7
8
|
|
|
8
9
|
[English](README.md) | [简体中文](README.zh.md)
|
|
9
10
|
|
package/README.zh.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
[](package.json)
|
|
7
|
+
[](https://awesome-dsh-plugin.com)
|
|
7
8
|
|
|
8
9
|
[English](README.md) | [简体中文](README.zh.md)
|
|
9
10
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Editable audit guidance shared by the host and the client build. */
|
|
2
|
+
export const DEFAULT_AUDIT_INSTRUCTIONS = [
|
|
3
|
+
'1. 取源码:`web_fetch` 读 `https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path>`,或利用其他本地工具获取源码;必要时读取 npm 包 tarball 的内容。先读 `package.json`,再读入口文件,再顺着依赖与被动态加载的代码走下去。',
|
|
4
|
+
'2. 读代码,不读宣言:README 说什么是它想让你以为的,代码做什么才是要报的。',
|
|
5
|
+
'3. 按下面的顺序找问题:',
|
|
6
|
+
' - 恶意行为:读取或外传凭据/令牌(`~/.dsh`、环境变量、ssh、云厂商配置)、上传本机文件、反弹 shell、挖矿、删除或加密用户文件、篡改 DSH 本身或其他插件;',
|
|
7
|
+
' - 执行链:安装期脚本(preinstall/install/postinstall)执行命令;运行期 `child_process`;`eval`/`new Function`;从网络下载再执行;写入 `~/.dsh`、profile 或系统目录;',
|
|
8
|
+
' - 隐蔽性:混淆或加密的字符串、base64 之后 eval、与功能无关的外联域名、把内容塞进资源文件里;',
|
|
9
|
+
' - 越权:绕过 DSH 的沙箱与审批,或去调用别的插件的私有服务;',
|
|
10
|
+
' - 版本:peer 范围与当前 DSH 版本是否相容(DSH 在安装时会拒绝不相容的包)。',
|
|
11
|
+
'4. 结论只有三档:',
|
|
12
|
+
' - `safe`:没有发现上述任何问题,且代码与它声称的功能一致;',
|
|
13
|
+
' - `warn`:可疑、需要用户知情(安装脚本、外部网络、权限偏大、版本不相容、源码读不到),但不像恶意;',
|
|
14
|
+
' - `unsafe`:发现明确的恶意行为或高风险执行链。',
|
|
15
|
+
'5. 每一条 `warn` 都要带 `risk`,替用户回答四个问题——这是他唯一能据以决定装不装的东西:',
|
|
16
|
+
' - `role`:这段代码在插件里做什么(功能角色);',
|
|
17
|
+
' - `why`:插件为什么需要它(合理的功能需要,还是抄来的惯例、与功能无关);',
|
|
18
|
+
' - `impact`:一旦被滥用会造成什么后果(拿到了什么、改了什么、传出了什么);',
|
|
19
|
+
' - `scenario` 取 `supply-chain`(供应链攻击:安装期/更新链上执行代码)、`privilege-escalation`(Agent 越权:绕过沙箱或审批、调用别的插件的私有服务)、`sandbox-escape`(沙箱逃逸)、`data-leak`(数据泄露:凭据/文件/环境变量被读取或外传)、`unsafe-publish`(不安全的网络发布)、`other`。',
|
|
20
|
+
' 只知道一部分就写那一部分,并在 `detail` 里说清你没看到什么;不要凭空补。`info`/`error` 不需要 `risk`。',
|
|
21
|
+
].join('\n')
|
|
@@ -331,7 +331,19 @@ function useStarter(api, view) {
|
|
|
331
331
|
var decision = chosen === MODE_REVIEW ? MODE_REVIEW : MODE_INSTALL
|
|
332
332
|
setError('')
|
|
333
333
|
setBusy('start')
|
|
334
|
-
|
|
334
|
+
var options = { target: wanted, mode: decision }
|
|
335
|
+
var prompt = String(api.control('auditPrompt') || '')
|
|
336
|
+
if (prompt) options.prompt = prompt
|
|
337
|
+
var model = String(api.control('auditModel') || '')
|
|
338
|
+
if (model) {
|
|
339
|
+
try {
|
|
340
|
+
var pair = JSON.parse(model)
|
|
341
|
+
options.selection = { provider: pair[0], model: pair[1] }
|
|
342
|
+
} catch (reason) { setBusy(''); setError(String(reason)); return }
|
|
343
|
+
}
|
|
344
|
+
var effort = String(api.control('auditEffort') || '')
|
|
345
|
+
if (effort) options.selection = Object.assign({}, options.selection || {}, { reasoningEffort: effort })
|
|
346
|
+
api.remote(START, { options: options })
|
|
335
347
|
.then(function (next) {
|
|
336
348
|
setBusy('')
|
|
337
349
|
if (next && next.auditId) api.setControl('auditId', String(next.auditId))
|
|
@@ -380,7 +392,7 @@ function TargetRow(props) {
|
|
|
380
392
|
onStart(wanted, mode)
|
|
381
393
|
setDraft('')
|
|
382
394
|
}
|
|
383
|
-
return h('div', { className: 'ph-loose' },
|
|
395
|
+
return h('div', { className: chooseMode ? 'ph-loose ph-audit-target-form' : 'ph-loose' },
|
|
384
396
|
chooseMode
|
|
385
397
|
? h('div', { className: 'ph-loose-row' },
|
|
386
398
|
h(UI.Segmented, {
|
|
@@ -414,6 +426,7 @@ function TargetRow(props) {
|
|
|
414
426
|
onKeyDown: function (event) { if (event.key === 'Enter') submit() },
|
|
415
427
|
}),
|
|
416
428
|
h(UI.Button, {
|
|
429
|
+
tone: chooseMode ? 'primary' : undefined,
|
|
417
430
|
disabled: busy || String(draft || '').trim() === '',
|
|
418
431
|
onClick: submit,
|
|
419
432
|
}, t('pluginAudit.action.start'))))
|
|
@@ -580,8 +593,9 @@ function FindingRow(props) {
|
|
|
580
593
|
h('span', { className: 'ph-audit-finding-title' }, title),
|
|
581
594
|
scenario ? h('span', { className: 'ph-audit-finding-tag' }, scenario) : null,
|
|
582
595
|
h('span', { className: 'ph-audit-finding-more' }, t(open ? 'pluginAudit.finding.collapse' : 'pluginAudit.finding.expand'))),
|
|
583
|
-
open
|
|
584
|
-
|
|
596
|
+
h('div', { className: 'ph-audit-reveal', hidden: !open },
|
|
597
|
+
h('div', { className: 'ph-audit-reveal-inner' },
|
|
598
|
+
h('div', { className: 'ph-audit-finding-body' },
|
|
585
599
|
detail ? h('p', { className: 'ph-audit-finding-detail' }, detail) : null,
|
|
586
600
|
risk
|
|
587
601
|
? h('div', { className: 'ph-audit-risk' },
|
|
@@ -592,8 +606,7 @@ function FindingRow(props) {
|
|
|
592
606
|
: (level === 'warn' ? h('p', { className: 'ph-audit-risk-missing' }, t('pluginAudit.risk.missing')) : null),
|
|
593
607
|
finding.ref
|
|
594
608
|
? h('p', { className: 'ph-audit-finding-ref' }, t('pluginAudit.finding.ref', { ref: String(finding.ref) }))
|
|
595
|
-
: null)
|
|
596
|
-
: null)
|
|
609
|
+
: null))))
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
/** The wall reading: where the review is, or an honest empty state. */
|
|
@@ -675,12 +688,101 @@ function HistoryList(props) {
|
|
|
675
688
|
* installs from its own row — which is how somebody asks for several reviews at once
|
|
676
689
|
* and decides afterwards.
|
|
677
690
|
*/
|
|
691
|
+
var defaultInstructions = /* @audit-instructions */ ''
|
|
692
|
+
|
|
693
|
+
function AuditModelPicker(props) {
|
|
694
|
+
var api = props.api
|
|
695
|
+
var t = props.t
|
|
696
|
+
var catalog = props.catalog
|
|
697
|
+
var data = catalog.data || {}
|
|
698
|
+
var models = Array.isArray(data.models) ? data.models : []
|
|
699
|
+
var chosen = String(api.control('auditModel') || '')
|
|
700
|
+
var effort = String(api.control('auditEffort') || '')
|
|
701
|
+
var defaults = data.defaultSelection || {}
|
|
702
|
+
var defaultKey = defaults.provider && defaults.model ? JSON.stringify([defaults.provider, defaults.model]) : ''
|
|
703
|
+
var selected = models.filter(function (item) { return JSON.stringify([item.provider, item.model]) === (chosen || defaultKey) })[0]
|
|
704
|
+
var efforts = selected && Array.isArray(selected.efforts) ? selected.efforts : []
|
|
705
|
+
function catalogLabel(value, fallback) {
|
|
706
|
+
if (typeof value === 'string') return value
|
|
707
|
+
return value && typeof value === 'object' ? (value[t('pluginAudit.catalogLanguage')] || value.en || value.zh || fallback) : fallback
|
|
708
|
+
}
|
|
709
|
+
return h('div', { className: 'ph-audit-models' },
|
|
710
|
+
h('label', { className: 'ph-audit-model-field' }, h('span', null, t('pluginAudit.model')),
|
|
711
|
+
h('select', { className: 'ph-input ph-audit-model-select', value: chosen, disabled: props.busy || !models.length,
|
|
712
|
+
onChange: function (event) { api.setControl('auditModel', event.target.value); api.setControl('auditEffort', '') },
|
|
713
|
+
}, h('option', { value: '' }, t('pluginAudit.model.default')),
|
|
714
|
+
chosen && !selected ? h('option', { value: chosen }, t('pluginAudit.model.unavailable')) : null,
|
|
715
|
+
models.map(function (item) {
|
|
716
|
+
var key = JSON.stringify([item.provider, item.model])
|
|
717
|
+
return h('option', { key: key, value: key }, catalogLabel(item.name, item.model) + ' · ' + item.provider)
|
|
718
|
+
}))),
|
|
719
|
+
h('label', { className: 'ph-audit-model-field' }, h('span', null, t('pluginAudit.effort')),
|
|
720
|
+
h('select', { className: 'ph-input ph-audit-effort-select', value: effort, disabled: props.busy || !efforts.length,
|
|
721
|
+
onChange: function (event) {
|
|
722
|
+
if (!chosen && event.target.value && defaultKey) api.setControl('auditModel', defaultKey)
|
|
723
|
+
api.setControl('auditEffort', event.target.value)
|
|
724
|
+
},
|
|
725
|
+
}, h('option', { value: '' }, t(efforts.length ? 'pluginAudit.effort.default' : 'pluginAudit.effort.none')),
|
|
726
|
+
efforts.map(function (item) { return h('option', { key: item.id, value: item.id }, catalogLabel(item.name, item.id)) }))),
|
|
727
|
+
catalog.error ? h('span', { className: 'ph-groups-error' }, t('pluginAudit.model.error')) : null)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function PromptEditor(props) {
|
|
731
|
+
var api = props.api
|
|
732
|
+
var t = props.t
|
|
733
|
+
var saved = String(api.control('auditPrompt') || '')
|
|
734
|
+
var state = React.useState(saved || defaultInstructions)
|
|
735
|
+
var draft = state[0]
|
|
736
|
+
var setDraft = state[1]
|
|
737
|
+
var dirty = draft !== (saved || defaultInstructions)
|
|
738
|
+
var expanded = React.useState(false)
|
|
739
|
+
var open = expanded[0]
|
|
740
|
+
return h('section', { className: 'ph-audit-launch' },
|
|
741
|
+
h('div', { className: 'ph-audit-launch-head' },
|
|
742
|
+
h('div', { className: 'ph-audit-launch-heading' },
|
|
743
|
+
h('span', { className: 'ph-audit-launch-icon', 'aria-hidden': true }, '↗'),
|
|
744
|
+
h('div', null,
|
|
745
|
+
h('h3', { className: 'ph-audit-launch-title' }, t('pluginAudit.section.target')))),
|
|
746
|
+
h('button', {
|
|
747
|
+
type: 'button', className: 'ph-audit-prompt-trigger', 'aria-expanded': open,
|
|
748
|
+
title: t('pluginAudit.prompt.hint'), onClick: function () { expanded[1](!open) },
|
|
749
|
+
}, h('span', { 'aria-hidden': true }, '✎'), t('pluginAudit.prompt.edit'),
|
|
750
|
+
h('span', { className: 'ph-audit-prompt-badge' }, t(saved ? 'pluginAudit.prompt.custom' : 'pluginAudit.prompt.default')))),
|
|
751
|
+
props.children,
|
|
752
|
+
h(AuditModelPicker, { api: api, t: t, busy: props.busy, catalog: props.catalog }),
|
|
753
|
+
h('div', { className: 'ph-audit-settings ph-audit-reveal', hidden: !open },
|
|
754
|
+
h('div', { className: 'ph-audit-reveal-inner ph-loose' },
|
|
755
|
+
h('div', { className: 'ph-audit-editor-head' },
|
|
756
|
+
h('strong', null, t('pluginAudit.prompt.title')),
|
|
757
|
+
h('span', { className: 'ph-note' }, String(draft.length) + ' / 20000')),
|
|
758
|
+
h('p', { className: 'ph-note' }, t('pluginAudit.prompt.hint')),
|
|
759
|
+
h('textarea', {
|
|
760
|
+
className: 'ph-input ph-audit-prompt', value: draft, maxLength: 20000,
|
|
761
|
+
'aria-label': t('pluginAudit.prompt.title'),
|
|
762
|
+
onChange: function (event) { setDraft(event.target.value) },
|
|
763
|
+
}),
|
|
764
|
+
h('div', { className: 'ph-audit-editor-footer' },
|
|
765
|
+
h('span', { className: 'ph-note', role: 'status' }, t(dirty ? 'pluginAudit.prompt.unsaved' : 'pluginAudit.prompt.ready')),
|
|
766
|
+
h(UI.Button, { tone: 'primary', disabled: !dirty, onClick: function () {
|
|
767
|
+
var value = draft.trim() ? draft : defaultInstructions
|
|
768
|
+
api.setControl('auditPrompt', value === defaultInstructions ? '' : value)
|
|
769
|
+
setDraft(value)
|
|
770
|
+
} }, t('pluginAudit.prompt.save')),
|
|
771
|
+
h(UI.Button, { disabled: !saved && draft === defaultInstructions, onClick: function () {
|
|
772
|
+
api.setControl('auditPrompt', '')
|
|
773
|
+
setDraft(defaultInstructions)
|
|
774
|
+
} }, t('pluginAudit.prompt.reset'))))))
|
|
775
|
+
}
|
|
776
|
+
|
|
678
777
|
function AuditDetail(props) {
|
|
679
778
|
var api = props.api
|
|
680
779
|
var t = props.t
|
|
681
780
|
var view = useAudit(api)
|
|
682
781
|
useAnnounce(api, 'pluginAudit', auditNotice(view.audit, t))
|
|
683
782
|
var starter = useStarter(api, view)
|
|
783
|
+
// Keep the catalog above the tab panes: returning to Current must not
|
|
784
|
+
// rediscover every provider or briefly disable the model selectors.
|
|
785
|
+
var modelCatalog = useRemote('homepage/pluginAuditModels', { options: {} }, { refreshMs: 0 })
|
|
684
786
|
var tab = React.useState('current')
|
|
685
787
|
var pane = tab[0]
|
|
686
788
|
var setPane = tab[1]
|
|
@@ -703,9 +805,15 @@ function AuditDetail(props) {
|
|
|
703
805
|
var history = listed.map(function (entry) {
|
|
704
806
|
return audit && entry && entry.auditId === audit.auditId ? audit : entry
|
|
705
807
|
})
|
|
808
|
+
function changePane(next) {
|
|
809
|
+
if (next === pane) return
|
|
810
|
+
// Update immediately; the pane's local CSS animation needs no page snapshot
|
|
811
|
+
// or animation-frame handshake before React can commit the selected tab.
|
|
812
|
+
setPane(next)
|
|
813
|
+
}
|
|
706
814
|
function openRecord(id) {
|
|
707
815
|
api.setControl('auditId', id)
|
|
708
|
-
|
|
816
|
+
changePane('current')
|
|
709
817
|
}
|
|
710
818
|
function decideFromRow(id, decision) {
|
|
711
819
|
if (pendingRow) return
|
|
@@ -723,7 +831,7 @@ function AuditDetail(props) {
|
|
|
723
831
|
setRowError(String((reason && reason.message) || reason))
|
|
724
832
|
})
|
|
725
833
|
}
|
|
726
|
-
return h('div', { className: 'ph-detail-block' },
|
|
834
|
+
return h('div', { className: 'ph-detail-block ph-audit-detail' },
|
|
727
835
|
h(UI.Segmented, {
|
|
728
836
|
options: [
|
|
729
837
|
{ value: 'current', label: t('pluginAudit.tab.current') },
|
|
@@ -735,11 +843,11 @@ function AuditDetail(props) {
|
|
|
735
843
|
// Opening the history asks the host for it: the records are the host's, and
|
|
736
844
|
// a review started while this card was closed has to be in the list.
|
|
737
845
|
if (wanted === 'history' && typeof view.reloadList === 'function') view.reloadList(true)
|
|
738
|
-
|
|
846
|
+
changePane(wanted)
|
|
739
847
|
},
|
|
740
848
|
}),
|
|
741
849
|
pane === 'history'
|
|
742
|
-
? h('div', { className: 'ph-loose' },
|
|
850
|
+
? h('div', { className: 'ph-loose ph-audit-pane', key: 'history' },
|
|
743
851
|
h('div', { className: 'ph-section-head' },
|
|
744
852
|
h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.history'))),
|
|
745
853
|
h(HistoryList, {
|
|
@@ -751,16 +859,26 @@ function AuditDetail(props) {
|
|
|
751
859
|
onDecide: decideFromRow,
|
|
752
860
|
}),
|
|
753
861
|
rowError ? h('span', { className: 'ph-groups-error' }, rowError) : null)
|
|
754
|
-
: h('div', { className: 'ph-loose' },
|
|
755
|
-
h(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
862
|
+
: h('div', { className: 'ph-loose ph-audit-pane', key: 'current' },
|
|
863
|
+
h(PromptEditor, { api: api, t: t, busy: starter.busy, catalog: modelCatalog },
|
|
864
|
+
h(TargetRow, {
|
|
865
|
+
t: t, busy: starter.busy, mode: starter.mode, setMode: starter.setMode,
|
|
866
|
+
onStart: starter.start, draft: draft, setDraft: setDraft,
|
|
867
|
+
})),
|
|
760
868
|
starter.error ? h('span', { className: 'ph-groups-error' }, starter.error) : null,
|
|
761
869
|
view.error ? h('span', { className: 'ph-groups-error' }, String(view.error)) : null,
|
|
762
870
|
audit
|
|
763
|
-
? h('div', { className: 'ph-
|
|
871
|
+
? h('div', { className: 'ph-audit-result' },
|
|
872
|
+
h('div', { className: 'ph-audit-result-head' },
|
|
873
|
+
h(UI.Chip, { tone: STATE_TONES[audit.state] || null }, stateText(t, audit.state)),
|
|
874
|
+
h('strong', { className: 'ph-audit-name' }, auditName(audit, t)),
|
|
875
|
+
plugin.version ? h('span', { className: 'ph-note' }, String(plugin.version)) : null),
|
|
876
|
+
h('p', { className: 'ph-audit-summary' }, audit.summary || progressText(t, audit)),
|
|
877
|
+
(canInstall(audit) || canDecline(audit)) ? h('div', { className: 'ph-audit-result-actions' },
|
|
878
|
+
h(VerdictActions, { api: api, t: t, audit: audit, view: view })) : null,
|
|
879
|
+
h('details', { className: 'ph-audit-metadata' },
|
|
880
|
+
h('summary', null, t('pluginAudit.metadata')),
|
|
881
|
+
h('div', { className: 'ph-kv' },
|
|
764
882
|
h('div', { className: 'ph-kv-row' },
|
|
765
883
|
h('span', { className: 'ph-kv-key' }, t('pluginAudit.kv.plugin')),
|
|
766
884
|
h('span', { className: 'ph-kv-value' }, auditName(audit, t))),
|
|
@@ -785,20 +903,18 @@ function AuditDetail(props) {
|
|
|
785
903
|
h('span', { className: 'ph-kv-value' }, stateText(t, audit.state) + (audit.stage ? ' · ' + progressText(t, audit) : ''))),
|
|
786
904
|
h('div', { className: 'ph-kv-row' },
|
|
787
905
|
h('span', { className: 'ph-kv-key' }, t('pluginAudit.kv.installed')),
|
|
788
|
-
h('span', { className: 'ph-kv-value' }, t(audit.installed ? 'pluginAudit.value.yes' : 'pluginAudit.value.no'))))
|
|
906
|
+
h('span', { className: 'ph-kv-value' }, t(audit.installed ? 'pluginAudit.value.yes' : 'pluginAudit.value.no'))))))
|
|
789
907
|
: h('p', { className: 'ph-note' }, view.error ? t('pluginAudit.error.status') : (view.busy ? t('pluginAudit.loading') : t('pluginAudit.idle'))),
|
|
790
908
|
audit && audit.detail ? h('p', { className: 'ph-note' }, String(audit.detail)) : null,
|
|
791
|
-
audit
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
h('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.findings'))),
|
|
909
|
+
audit ? h('div', { className: 'ph-section-head' },
|
|
910
|
+
h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.findings')),
|
|
911
|
+
h('span', { className: 'ph-note' }, String(findings.length))) : null,
|
|
795
912
|
findings.length === 0
|
|
796
|
-
? h('p', { className: 'ph-note' }, t('pluginAudit.findings.none'))
|
|
913
|
+
? (audit ? h('p', { className: 'ph-note' }, t('pluginAudit.findings.none')) : null)
|
|
797
914
|
: h('div', { className: 'ph-audit-findings' }, sortFindings(findings).map(function (finding, index) {
|
|
798
915
|
return h(FindingRow, { t: t, finding: finding, key: 'f' + index })
|
|
799
916
|
})),
|
|
800
|
-
h(
|
|
801
|
-
audit ? h(SessionRow, { t: t, audit: audit }) : h('p', { className: 'ph-note' }, t('pluginAudit.session.none'))))
|
|
917
|
+
audit ? h(SessionRow, { t: t, audit: audit }) : null))
|
|
802
918
|
}
|
|
803
919
|
|
|
804
920
|
defineCard({
|
|
@@ -824,6 +940,9 @@ defineCard({
|
|
|
824
940
|
actions: [],
|
|
825
941
|
detailActions: [
|
|
826
942
|
{ id: 'auditId', type: 'text', hidden: true, value: '' },
|
|
943
|
+
{ id: 'auditPrompt', type: 'text', hidden: true, value: '' },
|
|
944
|
+
{ id: 'auditModel', type: 'text', hidden: true, value: '' },
|
|
945
|
+
{ id: 'auditEffort', type: 'text', hidden: true, value: '' },
|
|
827
946
|
// The changed-audit signal (see SIGNAL): a control rather than component state
|
|
828
947
|
// because the preview and the expanded body are separate instances that must
|
|
829
948
|
// agree, and a control is the one value the framework shares between them.
|
|
@@ -834,6 +953,24 @@ defineCard({
|
|
|
834
953
|
dict({
|
|
835
954
|
zh: {
|
|
836
955
|
'card.pluginAudit.title': '插件审核',
|
|
956
|
+
'pluginAudit.metadata': '插件详情',
|
|
957
|
+
'pluginAudit.catalogLanguage': 'zh',
|
|
958
|
+
'pluginAudit.model': '审核模型',
|
|
959
|
+
'pluginAudit.model.default': '跟随全局默认模型',
|
|
960
|
+
'pluginAudit.model.unavailable': '已选模型不可用,请重新选择',
|
|
961
|
+
'pluginAudit.model.error': '模型列表读取失败,仍可使用全局默认模型',
|
|
962
|
+
'pluginAudit.effort': '思考强度',
|
|
963
|
+
'pluginAudit.effort.default': '模型默认',
|
|
964
|
+
'pluginAudit.effort.none': '不支持或未提供思考强度',
|
|
965
|
+
'pluginAudit.prompt.edit': '编辑提示词',
|
|
966
|
+
'pluginAudit.prompt.title': '审核提示词',
|
|
967
|
+
'pluginAudit.prompt.custom': '自定义',
|
|
968
|
+
'pluginAudit.prompt.default': '默认',
|
|
969
|
+
'pluginAudit.prompt.hint': '保存后用于后续审核。审核对象、结果回传格式与安全约束由系统自动附加。',
|
|
970
|
+
'pluginAudit.prompt.save': '保存提示词',
|
|
971
|
+
'pluginAudit.prompt.reset': '恢复默认',
|
|
972
|
+
'pluginAudit.prompt.unsaved': '有未保存的修改',
|
|
973
|
+
'pluginAudit.prompt.ready': '已保存 · 下次审核生效',
|
|
837
974
|
|
|
838
975
|
'pluginAudit.state.none': '无审核',
|
|
839
976
|
'pluginAudit.state.running': '审核中',
|
|
@@ -912,7 +1049,7 @@ dict({
|
|
|
912
1049
|
'pluginAudit.tab.current': '当前审核',
|
|
913
1050
|
'pluginAudit.tab.history': '审核记录',
|
|
914
1051
|
'pluginAudit.history.empty': '还没有审核记录',
|
|
915
|
-
'pluginAudit.history.view': '
|
|
1052
|
+
'pluginAudit.history.view': '加载条目',
|
|
916
1053
|
|
|
917
1054
|
'pluginAudit.kv.plugin': '插件',
|
|
918
1055
|
'pluginAudit.kv.version': '版本',
|
|
@@ -930,6 +1067,24 @@ dict({
|
|
|
930
1067
|
},
|
|
931
1068
|
en: {
|
|
932
1069
|
'card.pluginAudit.title': 'Plugin audit',
|
|
1070
|
+
'pluginAudit.metadata': 'Plugin details',
|
|
1071
|
+
'pluginAudit.catalogLanguage': 'en',
|
|
1072
|
+
'pluginAudit.model': 'Review model',
|
|
1073
|
+
'pluginAudit.model.default': 'Use global default model',
|
|
1074
|
+
'pluginAudit.model.unavailable': 'Selected model unavailable — choose again',
|
|
1075
|
+
'pluginAudit.model.error': 'Could not load models. The global default is still available.',
|
|
1076
|
+
'pluginAudit.effort': 'Reasoning effort',
|
|
1077
|
+
'pluginAudit.effort.default': 'Model default',
|
|
1078
|
+
'pluginAudit.effort.none': 'Reasoning effort unavailable',
|
|
1079
|
+
'pluginAudit.prompt.edit': 'Edit prompt',
|
|
1080
|
+
'pluginAudit.prompt.title': 'Review prompt',
|
|
1081
|
+
'pluginAudit.prompt.custom': 'Custom',
|
|
1082
|
+
'pluginAudit.prompt.default': 'Default',
|
|
1083
|
+
'pluginAudit.prompt.hint': 'Applies to future reviews once saved. The target, report format and safety constraints are appended automatically.',
|
|
1084
|
+
'pluginAudit.prompt.save': 'Save prompt',
|
|
1085
|
+
'pluginAudit.prompt.reset': 'Restore default',
|
|
1086
|
+
'pluginAudit.prompt.unsaved': 'Unsaved changes',
|
|
1087
|
+
'pluginAudit.prompt.ready': 'Saved · applies to the next review',
|
|
933
1088
|
|
|
934
1089
|
'pluginAudit.state.none': 'No audit',
|
|
935
1090
|
'pluginAudit.state.running': 'Reviewing',
|
|
@@ -1008,7 +1163,7 @@ dict({
|
|
|
1008
1163
|
'pluginAudit.tab.current': 'Current review',
|
|
1009
1164
|
'pluginAudit.tab.history': 'Review history',
|
|
1010
1165
|
'pluginAudit.history.empty': 'No reviews yet',
|
|
1011
|
-
'pluginAudit.history.view': '
|
|
1166
|
+
'pluginAudit.history.view': 'Load entry',
|
|
1012
1167
|
|
|
1013
1168
|
'pluginAudit.kv.plugin': 'Plugin',
|
|
1014
1169
|
'pluginAudit.kv.version': 'Version',
|
package/lib/client.js
CHANGED
|
@@ -4166,7 +4166,33 @@ var STYLES = [
|
|
|
4166
4166
|
// A finding is what the user came to read, so it does not use the note size,
|
|
4167
4167
|
// and its head is a real button (the whole row folds it) so the list can be
|
|
4168
4168
|
// read and opened from the keyboard.
|
|
4169
|
-
'.ph-audit-
|
|
4169
|
+
'.ph-audit-detail{gap:18px}.ph-audit-detail>.ph-loose{gap:14px}',
|
|
4170
|
+
'.ph-audit-launch{position:relative;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-primary,#e6e8ea) 14%,transparent);background:linear-gradient(125deg,rgba(99,102,241,.09),transparent 65%),var(--dsw-alias-bg-layer-2,rgba(255,255,255,.035));box-shadow:0 8px 28px rgba(0,0,0,.035)}',
|
|
4171
|
+
'.ph-audit-launch-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:24px;flex-wrap:wrap}',
|
|
4172
|
+
'.ph-audit-launch-heading{display:flex;align-items:center;gap:12px}.ph-audit-launch-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:12px;background:rgba(99,102,241,.12);color:var(--dsw-alias-label-primary,#e6e8ea);font-size:23px}',
|
|
4173
|
+
'.ph-audit-launch-title{margin:0;font-size:18px;font-weight:650;letter-spacing:-.025em}',
|
|
4174
|
+
'.ph-audit-prompt-trigger{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:6px 10px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.04));color:inherit;font:inherit;font-size:12px;cursor:pointer;transition:background .16s,border-color .16s}',
|
|
4175
|
+
'.ph-audit-prompt-trigger:hover,.ph-audit-prompt-trigger[aria-expanded=true]{background:rgba(99,102,241,.12);border-color:rgba(129,140,248,.5)}.ph-audit-prompt-badge{font-size:10px;padding:2px 6px;border-radius:5px;background:rgba(127,127,127,.1);color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
|
4176
|
+
'.ph-audit-target-form{gap:14px}.ph-audit-target-form>.ph-loose-row:last-child{order:-1;display:flex;gap:10px;flex-wrap:nowrap}.ph-audit-target-form .ph-input{min-width:0;flex:1;height:48px;padding:0 15px;border-radius:12px;font-size:14px;box-sizing:border-box;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.12))}',
|
|
4177
|
+
'.ph-audit-target-form>.ph-loose-row:last-child button{min-height:48px;flex:none;padding:0 24px;border-radius:12px;font-size:13px;font-weight:650}.ph-audit-target-form .ph-segment{min-height:30px;font-size:11px;padding:5px 12px}.ph-audit-target-form .ph-segmented{padding:3px}',
|
|
4178
|
+
'.ph-audit-detail>.ph-segmented{display:flex;width:100%;box-sizing:border-box;padding:5px;gap:5px;border-radius:13px}.ph-audit-detail>.ph-segmented>.ph-segment{flex:1;min-height:42px;font-size:14px;font-weight:650;border-radius:9px;transition:background .2s,color .2s,box-shadow .2s}',
|
|
4179
|
+
'.ph-audit-models{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);gap:12px;margin-top:18px;padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.09))}.ph-audit-model-field{display:flex;flex-direction:column;gap:7px;min-width:0;font-size:11px;color:var(--dsw-alias-label-secondary,#9aa4b2)}.ph-audit-model-field select{width:100%;min-width:0;height:38px;border-radius:9px;padding:0 10px;color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
4180
|
+
'.ph-audit-settings.ph-audit-reveal{margin-top:20px;padding-top:20px;border-top:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.1));transition:grid-template-rows .25s ease,opacity .2s ease,margin .25s,padding .25s,border-color .25s}.ph-audit-settings[hidden]{margin-top:0;padding-top:0;border-color:transparent}',
|
|
4181
|
+
'.ph-audit-reveal,.ph-audit-reveal[hidden]{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s ease,opacity .2s ease}.ph-audit-reveal[hidden]{grid-template-rows:0fr;opacity:0;pointer-events:none}.ph-audit-reveal-inner{min-height:0;overflow:hidden;visibility:visible;transition:visibility .25s}.ph-audit-reveal[hidden]>.ph-audit-reveal-inner{visibility:hidden}.ph-audit-settings>.ph-loose{gap:12px}',
|
|
4182
|
+
'@keyframes ph-audit-enter{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.ph-audit-pane{animation:ph-audit-enter .16s ease both}.ph-audit-result{animation:ph-audit-enter .22s ease both}.ph-audit-detail button{transition:background .18s,color .18s,border-color .18s,box-shadow .18s}',
|
|
4183
|
+
'@supports(interpolate-size:allow-keywords){.ph-audit-metadata{interpolate-size:allow-keywords}.ph-audit-metadata::details-content{overflow:hidden;block-size:0;opacity:0;transition:block-size .25s,opacity .2s,content-visibility .25s allow-discrete}.ph-audit-metadata[open]::details-content{block-size:auto;opacity:1}}',
|
|
4184
|
+
'@media(max-width:600px){.ph-audit-models{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.ph-audit-detail *,.ph-audit-detail *::details-content{animation:none!important;transition:none!important}}',
|
|
4185
|
+
'.ph-audit-editor-head{display:flex;align-items:center;justify-content:space-between;font-size:13px}.ph-audit-editor-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}.ph-audit-editor-footer>[role=status]{margin-right:auto}.ph-audit-editor-footer button{min-height:34px;padding:6px 12px;border-radius:8px}',
|
|
4186
|
+
'.ph-audit-metadata>summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa4b2);padding:6px 0}',
|
|
4187
|
+
'.ph-audit-prompt{width:100%;min-height:260px;resize:vertical;box-sizing:border-box;font-family:inherit;font-size:12px;line-height:1.8;padding:14px;border-radius:10px}',
|
|
4188
|
+
'.ph-audit-prompt-trigger:focus-visible{outline:2px solid #818cf8;outline-offset:3px}@media(max-width:600px){.ph-audit-launch{padding:16px}.ph-audit-target-form>.ph-loose-row:last-child{flex-wrap:wrap}.ph-audit-target-form>.ph-loose-row:last-child button{width:100%}.ph-audit-target-form .ph-input{flex-basis:100%}}',
|
|
4189
|
+
'.ph-audit-result-actions{margin:16px 0}.ph-audit-result-actions button{min-height:36px;padding:7px 16px;border-radius:9px}',
|
|
4190
|
+
'.ph-audit-result{padding:20px;border-radius:16px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.04));border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.09))}',
|
|
4191
|
+
'.ph-audit-result-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ph-audit-name{font-size:15px;overflow-wrap:anywhere}',
|
|
4192
|
+
'.ph-audit-detail .ph-audit-finding-head{padding:11px 12px}.ph-audit-detail .ph-audit-finding-title{white-space:normal;overflow-wrap:anywhere;line-height:1.5}',
|
|
4193
|
+
'.ph-audit-summary{font-size:14px;line-height:1.75;margin:12px 0;color:var(--dsw-alias-label-primary,#e6e8ea);overflow-wrap:anywhere}',
|
|
4194
|
+
'.ph-audit-metadata .ph-kv{margin-top:10px}.ph-audit-detail summary:focus-visible{outline:2px solid currentColor;outline-offset:3px;border-radius:4px}',
|
|
4195
|
+
'.ph-audit-findings{display:flex;flex-direction:column;gap:8px}',
|
|
4170
4196
|
'.ph-audit-finding{border-radius:9px;overflow:hidden;' +
|
|
4171
4197
|
'background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.04));' +
|
|
4172
4198
|
'border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08))}',
|
|
@@ -8425,7 +8451,19 @@ var STYLES = [
|
|
|
8425
8451
|
var decision = chosen === MODE_REVIEW ? MODE_REVIEW : MODE_INSTALL
|
|
8426
8452
|
setError('')
|
|
8427
8453
|
setBusy('start')
|
|
8428
|
-
|
|
8454
|
+
var options = { target: wanted, mode: decision }
|
|
8455
|
+
var prompt = String(api.control('auditPrompt') || '')
|
|
8456
|
+
if (prompt) options.prompt = prompt
|
|
8457
|
+
var model = String(api.control('auditModel') || '')
|
|
8458
|
+
if (model) {
|
|
8459
|
+
try {
|
|
8460
|
+
var pair = JSON.parse(model)
|
|
8461
|
+
options.selection = { provider: pair[0], model: pair[1] }
|
|
8462
|
+
} catch (reason) { setBusy(''); setError(String(reason)); return }
|
|
8463
|
+
}
|
|
8464
|
+
var effort = String(api.control('auditEffort') || '')
|
|
8465
|
+
if (effort) options.selection = Object.assign({}, options.selection || {}, { reasoningEffort: effort })
|
|
8466
|
+
api.remote(START, { options: options })
|
|
8429
8467
|
.then(function (next) {
|
|
8430
8468
|
setBusy('')
|
|
8431
8469
|
if (next && next.auditId) api.setControl('auditId', String(next.auditId))
|
|
@@ -8474,7 +8512,7 @@ var STYLES = [
|
|
|
8474
8512
|
onStart(wanted, mode)
|
|
8475
8513
|
setDraft('')
|
|
8476
8514
|
}
|
|
8477
|
-
return h('div', { className: 'ph-loose' },
|
|
8515
|
+
return h('div', { className: chooseMode ? 'ph-loose ph-audit-target-form' : 'ph-loose' },
|
|
8478
8516
|
chooseMode
|
|
8479
8517
|
? h('div', { className: 'ph-loose-row' },
|
|
8480
8518
|
h(UI.Segmented, {
|
|
@@ -8508,6 +8546,7 @@ var STYLES = [
|
|
|
8508
8546
|
onKeyDown: function (event) { if (event.key === 'Enter') submit() },
|
|
8509
8547
|
}),
|
|
8510
8548
|
h(UI.Button, {
|
|
8549
|
+
tone: chooseMode ? 'primary' : undefined,
|
|
8511
8550
|
disabled: busy || String(draft || '').trim() === '',
|
|
8512
8551
|
onClick: submit,
|
|
8513
8552
|
}, t('pluginAudit.action.start'))))
|
|
@@ -8674,8 +8713,9 @@ var STYLES = [
|
|
|
8674
8713
|
h('span', { className: 'ph-audit-finding-title' }, title),
|
|
8675
8714
|
scenario ? h('span', { className: 'ph-audit-finding-tag' }, scenario) : null,
|
|
8676
8715
|
h('span', { className: 'ph-audit-finding-more' }, t(open ? 'pluginAudit.finding.collapse' : 'pluginAudit.finding.expand'))),
|
|
8677
|
-
open
|
|
8678
|
-
|
|
8716
|
+
h('div', { className: 'ph-audit-reveal', hidden: !open },
|
|
8717
|
+
h('div', { className: 'ph-audit-reveal-inner' },
|
|
8718
|
+
h('div', { className: 'ph-audit-finding-body' },
|
|
8679
8719
|
detail ? h('p', { className: 'ph-audit-finding-detail' }, detail) : null,
|
|
8680
8720
|
risk
|
|
8681
8721
|
? h('div', { className: 'ph-audit-risk' },
|
|
@@ -8686,8 +8726,7 @@ var STYLES = [
|
|
|
8686
8726
|
: (level === 'warn' ? h('p', { className: 'ph-audit-risk-missing' }, t('pluginAudit.risk.missing')) : null),
|
|
8687
8727
|
finding.ref
|
|
8688
8728
|
? h('p', { className: 'ph-audit-finding-ref' }, t('pluginAudit.finding.ref', { ref: String(finding.ref) }))
|
|
8689
|
-
: null)
|
|
8690
|
-
: null)
|
|
8729
|
+
: null))))
|
|
8691
8730
|
}
|
|
8692
8731
|
|
|
8693
8732
|
/** The wall reading: where the review is, or an honest empty state. */
|
|
@@ -8769,12 +8808,101 @@ var STYLES = [
|
|
|
8769
8808
|
* installs from its own row — which is how somebody asks for several reviews at once
|
|
8770
8809
|
* and decides afterwards.
|
|
8771
8810
|
*/
|
|
8811
|
+
var defaultInstructions = "1. 取源码:`web_fetch` 读 `https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path>`,或用 `github_file`/`github_api` 列目录、读文件;必要时读取 npm 包 tarball 的内容。先读 `package.json`,再读入口文件,再顺着依赖与被动态加载的代码走下去。\n2. 读代码,不读宣言:README 说什么是它想让你以为的,代码做什么才是要报的。\n3. 按下面的顺序找问题:\n - 恶意行为:读取或外传凭据/令牌(`~/.dsh`、环境变量、ssh、云厂商配置)、上传本机文件、反弹 shell、挖矿、删除或加密用户文件、篡改 DSH 本身或其他插件;\n - 执行链:安装期脚本(preinstall/install/postinstall)执行命令;运行期 `child_process`;`eval`/`new Function`;从网络下载再执行;写入 `~/.dsh`、profile 或系统目录;\n - 隐蔽性:混淆或加密的字符串、base64 之后 eval、与功能无关的外联域名、把内容塞进资源文件里;\n - 越权:绕过 DSH 的沙箱与审批,或去调用别的插件的私有服务;\n - 版本:peer 范围与当前 DSH 版本是否相容(DSH 在安装时会拒绝不相容的包)。\n4. 结论只有三档:\n - `safe`:没有发现上述任何问题,且代码与它声称的功能一致;\n - `warn`:可疑、需要用户知情(安装脚本、外部网络、权限偏大、版本不相容、源码读不到),但不像恶意;\n - `unsafe`:发现明确的恶意行为或高风险执行链。\n5. 每一条 `warn` 都要带 `risk`,替用户回答四个问题——这是他唯一能据以决定装不装的东西:\n - `role`:这段代码在插件里做什么(功能角色);\n - `why`:插件为什么需要它(合理的功能需要,还是抄来的惯例、与功能无关);\n - `impact`:一旦被滥用会造成什么后果(拿到了什么、改了什么、传出了什么);\n - `scenario` 取 `supply-chain`(供应链攻击:安装期/更新链上执行代码)、`privilege-escalation`(Agent 越权:绕过沙箱或审批、调用别的插件的私有服务)、`sandbox-escape`(沙箱逃逸)、`data-leak`(数据泄露:凭据/文件/环境变量被读取或外传)、`unsafe-publish`(不安全的网络发布)、`other`。\n 只知道一部分就写那一部分,并在 `detail` 里说清你没看到什么;不要凭空补。`info`/`error` 不需要 `risk`。"
|
|
8812
|
+
|
|
8813
|
+
function AuditModelPicker(props) {
|
|
8814
|
+
var api = props.api
|
|
8815
|
+
var t = props.t
|
|
8816
|
+
var catalog = props.catalog
|
|
8817
|
+
var data = catalog.data || {}
|
|
8818
|
+
var models = Array.isArray(data.models) ? data.models : []
|
|
8819
|
+
var chosen = String(api.control('auditModel') || '')
|
|
8820
|
+
var effort = String(api.control('auditEffort') || '')
|
|
8821
|
+
var defaults = data.defaultSelection || {}
|
|
8822
|
+
var defaultKey = defaults.provider && defaults.model ? JSON.stringify([defaults.provider, defaults.model]) : ''
|
|
8823
|
+
var selected = models.filter(function (item) { return JSON.stringify([item.provider, item.model]) === (chosen || defaultKey) })[0]
|
|
8824
|
+
var efforts = selected && Array.isArray(selected.efforts) ? selected.efforts : []
|
|
8825
|
+
function catalogLabel(value, fallback) {
|
|
8826
|
+
if (typeof value === 'string') return value
|
|
8827
|
+
return value && typeof value === 'object' ? (value[t('pluginAudit.catalogLanguage')] || value.en || value.zh || fallback) : fallback
|
|
8828
|
+
}
|
|
8829
|
+
return h('div', { className: 'ph-audit-models' },
|
|
8830
|
+
h('label', { className: 'ph-audit-model-field' }, h('span', null, t('pluginAudit.model')),
|
|
8831
|
+
h('select', { className: 'ph-input ph-audit-model-select', value: chosen, disabled: props.busy || !models.length,
|
|
8832
|
+
onChange: function (event) { api.setControl('auditModel', event.target.value); api.setControl('auditEffort', '') },
|
|
8833
|
+
}, h('option', { value: '' }, t('pluginAudit.model.default')),
|
|
8834
|
+
chosen && !selected ? h('option', { value: chosen }, t('pluginAudit.model.unavailable')) : null,
|
|
8835
|
+
models.map(function (item) {
|
|
8836
|
+
var key = JSON.stringify([item.provider, item.model])
|
|
8837
|
+
return h('option', { key: key, value: key }, catalogLabel(item.name, item.model) + ' · ' + item.provider)
|
|
8838
|
+
}))),
|
|
8839
|
+
h('label', { className: 'ph-audit-model-field' }, h('span', null, t('pluginAudit.effort')),
|
|
8840
|
+
h('select', { className: 'ph-input ph-audit-effort-select', value: effort, disabled: props.busy || !efforts.length,
|
|
8841
|
+
onChange: function (event) {
|
|
8842
|
+
if (!chosen && event.target.value && defaultKey) api.setControl('auditModel', defaultKey)
|
|
8843
|
+
api.setControl('auditEffort', event.target.value)
|
|
8844
|
+
},
|
|
8845
|
+
}, h('option', { value: '' }, t(efforts.length ? 'pluginAudit.effort.default' : 'pluginAudit.effort.none')),
|
|
8846
|
+
efforts.map(function (item) { return h('option', { key: item.id, value: item.id }, catalogLabel(item.name, item.id)) }))),
|
|
8847
|
+
catalog.error ? h('span', { className: 'ph-groups-error' }, t('pluginAudit.model.error')) : null)
|
|
8848
|
+
}
|
|
8849
|
+
|
|
8850
|
+
function PromptEditor(props) {
|
|
8851
|
+
var api = props.api
|
|
8852
|
+
var t = props.t
|
|
8853
|
+
var saved = String(api.control('auditPrompt') || '')
|
|
8854
|
+
var state = React.useState(saved || defaultInstructions)
|
|
8855
|
+
var draft = state[0]
|
|
8856
|
+
var setDraft = state[1]
|
|
8857
|
+
var dirty = draft !== (saved || defaultInstructions)
|
|
8858
|
+
var expanded = React.useState(false)
|
|
8859
|
+
var open = expanded[0]
|
|
8860
|
+
return h('section', { className: 'ph-audit-launch' },
|
|
8861
|
+
h('div', { className: 'ph-audit-launch-head' },
|
|
8862
|
+
h('div', { className: 'ph-audit-launch-heading' },
|
|
8863
|
+
h('span', { className: 'ph-audit-launch-icon', 'aria-hidden': true }, '↗'),
|
|
8864
|
+
h('div', null,
|
|
8865
|
+
h('h3', { className: 'ph-audit-launch-title' }, t('pluginAudit.section.target')))),
|
|
8866
|
+
h('button', {
|
|
8867
|
+
type: 'button', className: 'ph-audit-prompt-trigger', 'aria-expanded': open,
|
|
8868
|
+
title: t('pluginAudit.prompt.hint'), onClick: function () { expanded[1](!open) },
|
|
8869
|
+
}, h('span', { 'aria-hidden': true }, '✎'), t('pluginAudit.prompt.edit'),
|
|
8870
|
+
h('span', { className: 'ph-audit-prompt-badge' }, t(saved ? 'pluginAudit.prompt.custom' : 'pluginAudit.prompt.default')))),
|
|
8871
|
+
props.children,
|
|
8872
|
+
h(AuditModelPicker, { api: api, t: t, busy: props.busy, catalog: props.catalog }),
|
|
8873
|
+
h('div', { className: 'ph-audit-settings ph-audit-reveal', hidden: !open },
|
|
8874
|
+
h('div', { className: 'ph-audit-reveal-inner ph-loose' },
|
|
8875
|
+
h('div', { className: 'ph-audit-editor-head' },
|
|
8876
|
+
h('strong', null, t('pluginAudit.prompt.title')),
|
|
8877
|
+
h('span', { className: 'ph-note' }, String(draft.length) + ' / 20000')),
|
|
8878
|
+
h('p', { className: 'ph-note' }, t('pluginAudit.prompt.hint')),
|
|
8879
|
+
h('textarea', {
|
|
8880
|
+
className: 'ph-input ph-audit-prompt', value: draft, maxLength: 20000,
|
|
8881
|
+
'aria-label': t('pluginAudit.prompt.title'),
|
|
8882
|
+
onChange: function (event) { setDraft(event.target.value) },
|
|
8883
|
+
}),
|
|
8884
|
+
h('div', { className: 'ph-audit-editor-footer' },
|
|
8885
|
+
h('span', { className: 'ph-note', role: 'status' }, t(dirty ? 'pluginAudit.prompt.unsaved' : 'pluginAudit.prompt.ready')),
|
|
8886
|
+
h(UI.Button, { tone: 'primary', disabled: !dirty, onClick: function () {
|
|
8887
|
+
var value = draft.trim() ? draft : defaultInstructions
|
|
8888
|
+
api.setControl('auditPrompt', value === defaultInstructions ? '' : value)
|
|
8889
|
+
setDraft(value)
|
|
8890
|
+
} }, t('pluginAudit.prompt.save')),
|
|
8891
|
+
h(UI.Button, { disabled: !saved && draft === defaultInstructions, onClick: function () {
|
|
8892
|
+
api.setControl('auditPrompt', '')
|
|
8893
|
+
setDraft(defaultInstructions)
|
|
8894
|
+
} }, t('pluginAudit.prompt.reset'))))))
|
|
8895
|
+
}
|
|
8896
|
+
|
|
8772
8897
|
function AuditDetail(props) {
|
|
8773
8898
|
var api = props.api
|
|
8774
8899
|
var t = props.t
|
|
8775
8900
|
var view = useAudit(api)
|
|
8776
8901
|
useAnnounce(api, 'pluginAudit', auditNotice(view.audit, t))
|
|
8777
8902
|
var starter = useStarter(api, view)
|
|
8903
|
+
// Keep the catalog above the tab panes: returning to Current must not
|
|
8904
|
+
// rediscover every provider or briefly disable the model selectors.
|
|
8905
|
+
var modelCatalog = useRemote('homepage/pluginAuditModels', { options: {} }, { refreshMs: 0 })
|
|
8778
8906
|
var tab = React.useState('current')
|
|
8779
8907
|
var pane = tab[0]
|
|
8780
8908
|
var setPane = tab[1]
|
|
@@ -8797,9 +8925,15 @@ var STYLES = [
|
|
|
8797
8925
|
var history = listed.map(function (entry) {
|
|
8798
8926
|
return audit && entry && entry.auditId === audit.auditId ? audit : entry
|
|
8799
8927
|
})
|
|
8928
|
+
function changePane(next) {
|
|
8929
|
+
if (next === pane) return
|
|
8930
|
+
// Update immediately; the pane's local CSS animation needs no page snapshot
|
|
8931
|
+
// or animation-frame handshake before React can commit the selected tab.
|
|
8932
|
+
setPane(next)
|
|
8933
|
+
}
|
|
8800
8934
|
function openRecord(id) {
|
|
8801
8935
|
api.setControl('auditId', id)
|
|
8802
|
-
|
|
8936
|
+
changePane('current')
|
|
8803
8937
|
}
|
|
8804
8938
|
function decideFromRow(id, decision) {
|
|
8805
8939
|
if (pendingRow) return
|
|
@@ -8817,7 +8951,7 @@ var STYLES = [
|
|
|
8817
8951
|
setRowError(String((reason && reason.message) || reason))
|
|
8818
8952
|
})
|
|
8819
8953
|
}
|
|
8820
|
-
return h('div', { className: 'ph-detail-block' },
|
|
8954
|
+
return h('div', { className: 'ph-detail-block ph-audit-detail' },
|
|
8821
8955
|
h(UI.Segmented, {
|
|
8822
8956
|
options: [
|
|
8823
8957
|
{ value: 'current', label: t('pluginAudit.tab.current') },
|
|
@@ -8829,11 +8963,11 @@ var STYLES = [
|
|
|
8829
8963
|
// Opening the history asks the host for it: the records are the host's, and
|
|
8830
8964
|
// a review started while this card was closed has to be in the list.
|
|
8831
8965
|
if (wanted === 'history' && typeof view.reloadList === 'function') view.reloadList(true)
|
|
8832
|
-
|
|
8966
|
+
changePane(wanted)
|
|
8833
8967
|
},
|
|
8834
8968
|
}),
|
|
8835
8969
|
pane === 'history'
|
|
8836
|
-
? h('div', { className: 'ph-loose' },
|
|
8970
|
+
? h('div', { className: 'ph-loose ph-audit-pane', key: 'history' },
|
|
8837
8971
|
h('div', { className: 'ph-section-head' },
|
|
8838
8972
|
h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.history'))),
|
|
8839
8973
|
h(HistoryList, {
|
|
@@ -8845,16 +8979,26 @@ var STYLES = [
|
|
|
8845
8979
|
onDecide: decideFromRow,
|
|
8846
8980
|
}),
|
|
8847
8981
|
rowError ? h('span', { className: 'ph-groups-error' }, rowError) : null)
|
|
8848
|
-
: h('div', { className: 'ph-loose' },
|
|
8849
|
-
h(
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8982
|
+
: h('div', { className: 'ph-loose ph-audit-pane', key: 'current' },
|
|
8983
|
+
h(PromptEditor, { api: api, t: t, busy: starter.busy, catalog: modelCatalog },
|
|
8984
|
+
h(TargetRow, {
|
|
8985
|
+
t: t, busy: starter.busy, mode: starter.mode, setMode: starter.setMode,
|
|
8986
|
+
onStart: starter.start, draft: draft, setDraft: setDraft,
|
|
8987
|
+
})),
|
|
8854
8988
|
starter.error ? h('span', { className: 'ph-groups-error' }, starter.error) : null,
|
|
8855
8989
|
view.error ? h('span', { className: 'ph-groups-error' }, String(view.error)) : null,
|
|
8856
8990
|
audit
|
|
8857
|
-
? h('div', { className: 'ph-
|
|
8991
|
+
? h('div', { className: 'ph-audit-result' },
|
|
8992
|
+
h('div', { className: 'ph-audit-result-head' },
|
|
8993
|
+
h(UI.Chip, { tone: STATE_TONES[audit.state] || null }, stateText(t, audit.state)),
|
|
8994
|
+
h('strong', { className: 'ph-audit-name' }, auditName(audit, t)),
|
|
8995
|
+
plugin.version ? h('span', { className: 'ph-note' }, String(plugin.version)) : null),
|
|
8996
|
+
h('p', { className: 'ph-audit-summary' }, audit.summary || progressText(t, audit)),
|
|
8997
|
+
(canInstall(audit) || canDecline(audit)) ? h('div', { className: 'ph-audit-result-actions' },
|
|
8998
|
+
h(VerdictActions, { api: api, t: t, audit: audit, view: view })) : null,
|
|
8999
|
+
h('details', { className: 'ph-audit-metadata' },
|
|
9000
|
+
h('summary', null, t('pluginAudit.metadata')),
|
|
9001
|
+
h('div', { className: 'ph-kv' },
|
|
8858
9002
|
h('div', { className: 'ph-kv-row' },
|
|
8859
9003
|
h('span', { className: 'ph-kv-key' }, t('pluginAudit.kv.plugin')),
|
|
8860
9004
|
h('span', { className: 'ph-kv-value' }, auditName(audit, t))),
|
|
@@ -8879,20 +9023,18 @@ var STYLES = [
|
|
|
8879
9023
|
h('span', { className: 'ph-kv-value' }, stateText(t, audit.state) + (audit.stage ? ' · ' + progressText(t, audit) : ''))),
|
|
8880
9024
|
h('div', { className: 'ph-kv-row' },
|
|
8881
9025
|
h('span', { className: 'ph-kv-key' }, t('pluginAudit.kv.installed')),
|
|
8882
|
-
h('span', { className: 'ph-kv-value' }, t(audit.installed ? 'pluginAudit.value.yes' : 'pluginAudit.value.no'))))
|
|
9026
|
+
h('span', { className: 'ph-kv-value' }, t(audit.installed ? 'pluginAudit.value.yes' : 'pluginAudit.value.no'))))))
|
|
8883
9027
|
: h('p', { className: 'ph-note' }, view.error ? t('pluginAudit.error.status') : (view.busy ? t('pluginAudit.loading') : t('pluginAudit.idle'))),
|
|
8884
9028
|
audit && audit.detail ? h('p', { className: 'ph-note' }, String(audit.detail)) : null,
|
|
8885
|
-
audit
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
h('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.findings'))),
|
|
9029
|
+
audit ? h('div', { className: 'ph-section-head' },
|
|
9030
|
+
h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.findings')),
|
|
9031
|
+
h('span', { className: 'ph-note' }, String(findings.length))) : null,
|
|
8889
9032
|
findings.length === 0
|
|
8890
|
-
? h('p', { className: 'ph-note' }, t('pluginAudit.findings.none'))
|
|
9033
|
+
? (audit ? h('p', { className: 'ph-note' }, t('pluginAudit.findings.none')) : null)
|
|
8891
9034
|
: h('div', { className: 'ph-audit-findings' }, sortFindings(findings).map(function (finding, index) {
|
|
8892
9035
|
return h(FindingRow, { t: t, finding: finding, key: 'f' + index })
|
|
8893
9036
|
})),
|
|
8894
|
-
h(
|
|
8895
|
-
audit ? h(SessionRow, { t: t, audit: audit }) : h('p', { className: 'ph-note' }, t('pluginAudit.session.none'))))
|
|
9037
|
+
audit ? h(SessionRow, { t: t, audit: audit }) : null))
|
|
8896
9038
|
}
|
|
8897
9039
|
|
|
8898
9040
|
defineCard({
|
|
@@ -8918,6 +9060,9 @@ var STYLES = [
|
|
|
8918
9060
|
actions: [],
|
|
8919
9061
|
detailActions: [
|
|
8920
9062
|
{ id: 'auditId', type: 'text', hidden: true, value: '' },
|
|
9063
|
+
{ id: 'auditPrompt', type: 'text', hidden: true, value: '' },
|
|
9064
|
+
{ id: 'auditModel', type: 'text', hidden: true, value: '' },
|
|
9065
|
+
{ id: 'auditEffort', type: 'text', hidden: true, value: '' },
|
|
8921
9066
|
// The changed-audit signal (see SIGNAL): a control rather than component state
|
|
8922
9067
|
// because the preview and the expanded body are separate instances that must
|
|
8923
9068
|
// agree, and a control is the one value the framework shares between them.
|
|
@@ -8928,6 +9073,24 @@ var STYLES = [
|
|
|
8928
9073
|
dict({
|
|
8929
9074
|
zh: {
|
|
8930
9075
|
'card.pluginAudit.title': '插件审核',
|
|
9076
|
+
'pluginAudit.metadata': '插件详情',
|
|
9077
|
+
'pluginAudit.catalogLanguage': 'zh',
|
|
9078
|
+
'pluginAudit.model': '审核模型',
|
|
9079
|
+
'pluginAudit.model.default': '跟随全局默认模型',
|
|
9080
|
+
'pluginAudit.model.unavailable': '已选模型不可用,请重新选择',
|
|
9081
|
+
'pluginAudit.model.error': '模型列表读取失败,仍可使用全局默认模型',
|
|
9082
|
+
'pluginAudit.effort': '思考强度',
|
|
9083
|
+
'pluginAudit.effort.default': '模型默认',
|
|
9084
|
+
'pluginAudit.effort.none': '不支持或未提供思考强度',
|
|
9085
|
+
'pluginAudit.prompt.edit': '编辑提示词',
|
|
9086
|
+
'pluginAudit.prompt.title': '审核提示词',
|
|
9087
|
+
'pluginAudit.prompt.custom': '自定义',
|
|
9088
|
+
'pluginAudit.prompt.default': '默认',
|
|
9089
|
+
'pluginAudit.prompt.hint': '保存后用于后续审核。审核对象、结果回传格式与安全约束由系统自动附加。',
|
|
9090
|
+
'pluginAudit.prompt.save': '保存提示词',
|
|
9091
|
+
'pluginAudit.prompt.reset': '恢复默认',
|
|
9092
|
+
'pluginAudit.prompt.unsaved': '有未保存的修改',
|
|
9093
|
+
'pluginAudit.prompt.ready': '已保存 · 下次审核生效',
|
|
8931
9094
|
|
|
8932
9095
|
'pluginAudit.state.none': '无审核',
|
|
8933
9096
|
'pluginAudit.state.running': '审核中',
|
|
@@ -9006,7 +9169,7 @@ var STYLES = [
|
|
|
9006
9169
|
'pluginAudit.tab.current': '当前审核',
|
|
9007
9170
|
'pluginAudit.tab.history': '审核记录',
|
|
9008
9171
|
'pluginAudit.history.empty': '还没有审核记录',
|
|
9009
|
-
'pluginAudit.history.view': '
|
|
9172
|
+
'pluginAudit.history.view': '加载条目',
|
|
9010
9173
|
|
|
9011
9174
|
'pluginAudit.kv.plugin': '插件',
|
|
9012
9175
|
'pluginAudit.kv.version': '版本',
|
|
@@ -9024,6 +9187,24 @@ var STYLES = [
|
|
|
9024
9187
|
},
|
|
9025
9188
|
en: {
|
|
9026
9189
|
'card.pluginAudit.title': 'Plugin audit',
|
|
9190
|
+
'pluginAudit.metadata': 'Plugin details',
|
|
9191
|
+
'pluginAudit.catalogLanguage': 'en',
|
|
9192
|
+
'pluginAudit.model': 'Review model',
|
|
9193
|
+
'pluginAudit.model.default': 'Use global default model',
|
|
9194
|
+
'pluginAudit.model.unavailable': 'Selected model unavailable — choose again',
|
|
9195
|
+
'pluginAudit.model.error': 'Could not load models. The global default is still available.',
|
|
9196
|
+
'pluginAudit.effort': 'Reasoning effort',
|
|
9197
|
+
'pluginAudit.effort.default': 'Model default',
|
|
9198
|
+
'pluginAudit.effort.none': 'Reasoning effort unavailable',
|
|
9199
|
+
'pluginAudit.prompt.edit': 'Edit prompt',
|
|
9200
|
+
'pluginAudit.prompt.title': 'Review prompt',
|
|
9201
|
+
'pluginAudit.prompt.custom': 'Custom',
|
|
9202
|
+
'pluginAudit.prompt.default': 'Default',
|
|
9203
|
+
'pluginAudit.prompt.hint': 'Applies to future reviews once saved. The target, report format and safety constraints are appended automatically.',
|
|
9204
|
+
'pluginAudit.prompt.save': 'Save prompt',
|
|
9205
|
+
'pluginAudit.prompt.reset': 'Restore default',
|
|
9206
|
+
'pluginAudit.prompt.unsaved': 'Unsaved changes',
|
|
9207
|
+
'pluginAudit.prompt.ready': 'Saved · applies to the next review',
|
|
9027
9208
|
|
|
9028
9209
|
'pluginAudit.state.none': 'No audit',
|
|
9029
9210
|
'pluginAudit.state.running': 'Reviewing',
|
|
@@ -9102,7 +9283,7 @@ var STYLES = [
|
|
|
9102
9283
|
'pluginAudit.tab.current': 'Current review',
|
|
9103
9284
|
'pluginAudit.tab.history': 'Review history',
|
|
9104
9285
|
'pluginAudit.history.empty': 'No reviews yet',
|
|
9105
|
-
'pluginAudit.history.view': '
|
|
9286
|
+
'pluginAudit.history.view': 'Load entry',
|
|
9106
9287
|
|
|
9107
9288
|
'pluginAudit.kv.plugin': 'Plugin',
|
|
9108
9289
|
'pluginAudit.kv.version': 'Version',
|
package/lib/framework.part.js
CHANGED
|
@@ -4154,7 +4154,33 @@ var STYLES = [
|
|
|
4154
4154
|
// A finding is what the user came to read, so it does not use the note size,
|
|
4155
4155
|
// and its head is a real button (the whole row folds it) so the list can be
|
|
4156
4156
|
// read and opened from the keyboard.
|
|
4157
|
-
'.ph-audit-
|
|
4157
|
+
'.ph-audit-detail{gap:18px}.ph-audit-detail>.ph-loose{gap:14px}',
|
|
4158
|
+
'.ph-audit-launch{position:relative;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-primary,#e6e8ea) 14%,transparent);background:linear-gradient(125deg,rgba(99,102,241,.09),transparent 65%),var(--dsw-alias-bg-layer-2,rgba(255,255,255,.035));box-shadow:0 8px 28px rgba(0,0,0,.035)}',
|
|
4159
|
+
'.ph-audit-launch-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:24px;flex-wrap:wrap}',
|
|
4160
|
+
'.ph-audit-launch-heading{display:flex;align-items:center;gap:12px}.ph-audit-launch-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:12px;background:rgba(99,102,241,.12);color:var(--dsw-alias-label-primary,#e6e8ea);font-size:23px}',
|
|
4161
|
+
'.ph-audit-launch-title{margin:0;font-size:18px;font-weight:650;letter-spacing:-.025em}',
|
|
4162
|
+
'.ph-audit-prompt-trigger{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:6px 10px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.04));color:inherit;font:inherit;font-size:12px;cursor:pointer;transition:background .16s,border-color .16s}',
|
|
4163
|
+
'.ph-audit-prompt-trigger:hover,.ph-audit-prompt-trigger[aria-expanded=true]{background:rgba(99,102,241,.12);border-color:rgba(129,140,248,.5)}.ph-audit-prompt-badge{font-size:10px;padding:2px 6px;border-radius:5px;background:rgba(127,127,127,.1);color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
|
4164
|
+
'.ph-audit-target-form{gap:14px}.ph-audit-target-form>.ph-loose-row:last-child{order:-1;display:flex;gap:10px;flex-wrap:nowrap}.ph-audit-target-form .ph-input{min-width:0;flex:1;height:48px;padding:0 15px;border-radius:12px;font-size:14px;box-sizing:border-box;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.12))}',
|
|
4165
|
+
'.ph-audit-target-form>.ph-loose-row:last-child button{min-height:48px;flex:none;padding:0 24px;border-radius:12px;font-size:13px;font-weight:650}.ph-audit-target-form .ph-segment{min-height:30px;font-size:11px;padding:5px 12px}.ph-audit-target-form .ph-segmented{padding:3px}',
|
|
4166
|
+
'.ph-audit-detail>.ph-segmented{display:flex;width:100%;box-sizing:border-box;padding:5px;gap:5px;border-radius:13px}.ph-audit-detail>.ph-segmented>.ph-segment{flex:1;min-height:42px;font-size:14px;font-weight:650;border-radius:9px;transition:background .2s,color .2s,box-shadow .2s}',
|
|
4167
|
+
'.ph-audit-models{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);gap:12px;margin-top:18px;padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.09))}.ph-audit-model-field{display:flex;flex-direction:column;gap:7px;min-width:0;font-size:11px;color:var(--dsw-alias-label-secondary,#9aa4b2)}.ph-audit-model-field select{width:100%;min-width:0;height:38px;border-radius:9px;padding:0 10px;color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
4168
|
+
'.ph-audit-settings.ph-audit-reveal{margin-top:20px;padding-top:20px;border-top:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.1));transition:grid-template-rows .25s ease,opacity .2s ease,margin .25s,padding .25s,border-color .25s}.ph-audit-settings[hidden]{margin-top:0;padding-top:0;border-color:transparent}',
|
|
4169
|
+
'.ph-audit-reveal,.ph-audit-reveal[hidden]{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s ease,opacity .2s ease}.ph-audit-reveal[hidden]{grid-template-rows:0fr;opacity:0;pointer-events:none}.ph-audit-reveal-inner{min-height:0;overflow:hidden;visibility:visible;transition:visibility .25s}.ph-audit-reveal[hidden]>.ph-audit-reveal-inner{visibility:hidden}.ph-audit-settings>.ph-loose{gap:12px}',
|
|
4170
|
+
'@keyframes ph-audit-enter{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.ph-audit-pane{animation:ph-audit-enter .16s ease both}.ph-audit-result{animation:ph-audit-enter .22s ease both}.ph-audit-detail button{transition:background .18s,color .18s,border-color .18s,box-shadow .18s}',
|
|
4171
|
+
'@supports(interpolate-size:allow-keywords){.ph-audit-metadata{interpolate-size:allow-keywords}.ph-audit-metadata::details-content{overflow:hidden;block-size:0;opacity:0;transition:block-size .25s,opacity .2s,content-visibility .25s allow-discrete}.ph-audit-metadata[open]::details-content{block-size:auto;opacity:1}}',
|
|
4172
|
+
'@media(max-width:600px){.ph-audit-models{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.ph-audit-detail *,.ph-audit-detail *::details-content{animation:none!important;transition:none!important}}',
|
|
4173
|
+
'.ph-audit-editor-head{display:flex;align-items:center;justify-content:space-between;font-size:13px}.ph-audit-editor-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}.ph-audit-editor-footer>[role=status]{margin-right:auto}.ph-audit-editor-footer button{min-height:34px;padding:6px 12px;border-radius:8px}',
|
|
4174
|
+
'.ph-audit-metadata>summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa4b2);padding:6px 0}',
|
|
4175
|
+
'.ph-audit-prompt{width:100%;min-height:260px;resize:vertical;box-sizing:border-box;font-family:inherit;font-size:12px;line-height:1.8;padding:14px;border-radius:10px}',
|
|
4176
|
+
'.ph-audit-prompt-trigger:focus-visible{outline:2px solid #818cf8;outline-offset:3px}@media(max-width:600px){.ph-audit-launch{padding:16px}.ph-audit-target-form>.ph-loose-row:last-child{flex-wrap:wrap}.ph-audit-target-form>.ph-loose-row:last-child button{width:100%}.ph-audit-target-form .ph-input{flex-basis:100%}}',
|
|
4177
|
+
'.ph-audit-result-actions{margin:16px 0}.ph-audit-result-actions button{min-height:36px;padding:7px 16px;border-radius:9px}',
|
|
4178
|
+
'.ph-audit-result{padding:20px;border-radius:16px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.04));border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.09))}',
|
|
4179
|
+
'.ph-audit-result-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ph-audit-name{font-size:15px;overflow-wrap:anywhere}',
|
|
4180
|
+
'.ph-audit-detail .ph-audit-finding-head{padding:11px 12px}.ph-audit-detail .ph-audit-finding-title{white-space:normal;overflow-wrap:anywhere;line-height:1.5}',
|
|
4181
|
+
'.ph-audit-summary{font-size:14px;line-height:1.75;margin:12px 0;color:var(--dsw-alias-label-primary,#e6e8ea);overflow-wrap:anywhere}',
|
|
4182
|
+
'.ph-audit-metadata .ph-kv{margin-top:10px}.ph-audit-detail summary:focus-visible{outline:2px solid currentColor;outline-offset:3px;border-radius:4px}',
|
|
4183
|
+
'.ph-audit-findings{display:flex;flex-direction:column;gap:8px}',
|
|
4158
4184
|
'.ph-audit-finding{border-radius:9px;overflow:hidden;' +
|
|
4159
4185
|
'background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.04));' +
|
|
4160
4186
|
'border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08))}',
|
package/lib/host/audit-runner.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* its session, and the agent id *is* the session id
|
|
11
11
|
* - `sessions()` the host `sessions` service, to flush the log to disk
|
|
12
12
|
* - `presets()` the `agentPresets` service, when a preset is asked for
|
|
13
|
+
* - `llm()` the live provider/model registry, including reasoning efforts
|
|
14
|
+
* - `defaultModel()` the profile's default selection (read only)
|
|
13
15
|
* - `title()` the `sessionTitle` service, for the session's name
|
|
14
16
|
* - `on()` `ctx.on`, for the session's progress events
|
|
15
17
|
* - `install()` `pluginManager.installBundle(spec, options)`
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
compatibilityCheck,
|
|
32
34
|
createAuditRegistry,
|
|
33
35
|
needsConfirmation,
|
|
36
|
+
normalizeAuditInstructions,
|
|
34
37
|
resolveTarget,
|
|
35
38
|
verdictAcknowledgement,
|
|
36
39
|
} from './audit.js'
|
|
@@ -124,7 +127,7 @@ export function describeInstall(result) {
|
|
|
124
127
|
/**
|
|
125
128
|
* Build the runner.
|
|
126
129
|
* @param options - the injected engine surface (see the file comment).
|
|
127
|
-
* @returns `{ start, status, list, decide, report, tool }`.
|
|
130
|
+
* @returns `{ models, start, status, list, decide, report, tool }`.
|
|
128
131
|
*/
|
|
129
132
|
export function createAuditRunner(options = {}) {
|
|
130
133
|
const now = typeof options.now === 'function' ? options.now : () => Date.now()
|
|
@@ -431,8 +434,71 @@ export function createAuditRunner(options = {}) {
|
|
|
431
434
|
})
|
|
432
435
|
}
|
|
433
436
|
|
|
434
|
-
/**
|
|
435
|
-
|
|
437
|
+
/** Copy only the default selection fields exposed by the audit API. */
|
|
438
|
+
function defaultSelection() {
|
|
439
|
+
const defaults = serviceOf('defaultModel')
|
|
440
|
+
const selected = defaults && typeof defaults.currentSelection === 'function'
|
|
441
|
+
? defaults.currentSelection()
|
|
442
|
+
: null
|
|
443
|
+
if (!selected || !selected.provider || !selected.model) return null
|
|
444
|
+
return {
|
|
445
|
+
provider: selected.provider,
|
|
446
|
+
model: selected.model,
|
|
447
|
+
...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }),
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Project the live catalog just as DSH's session-controller/catalog.ts does. */
|
|
452
|
+
async function providerModels(llm, provider) {
|
|
453
|
+
const models = await llm.listModels(provider)
|
|
454
|
+
return Promise.all(models.map(async (model) => {
|
|
455
|
+
const resolved = typeof llm.resolveModelInfo === 'function'
|
|
456
|
+
? await llm.resolveModelInfo(provider, model.id)
|
|
457
|
+
: model
|
|
458
|
+
return {
|
|
459
|
+
provider,
|
|
460
|
+
model: model.id,
|
|
461
|
+
name: model.name,
|
|
462
|
+
efforts: (resolved.reasoning?.efforts || []).map(({ id, name }) => ({ id, name })),
|
|
463
|
+
}
|
|
464
|
+
}))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Validate before fetching source or creating history; never change defaults. */
|
|
468
|
+
async function selectModel(input) {
|
|
469
|
+
const selected = input === undefined ? defaultSelection() : input
|
|
470
|
+
if (input === undefined && selected === null) throw new Error('读不到默认模型,审核会话无法启动')
|
|
471
|
+
if (!selected || typeof selected !== 'object' || Array.isArray(selected)
|
|
472
|
+
|| typeof selected.provider !== 'string' || selected.provider.trim() === ''
|
|
473
|
+
|| typeof selected.model !== 'string' || selected.model.trim() === '') {
|
|
474
|
+
throw new Error('selection 必须包含 provider 和 model 字符串')
|
|
475
|
+
}
|
|
476
|
+
// An explicit model with no effort uses that model's own default, even when
|
|
477
|
+
// it happens to be the profile's current model. Do not merge the selections.
|
|
478
|
+
const selection = {
|
|
479
|
+
provider: selected.provider,
|
|
480
|
+
model: selected.model,
|
|
481
|
+
...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }),
|
|
482
|
+
}
|
|
483
|
+
const llm = serviceOf('llm')
|
|
484
|
+
if (!llm || typeof llm.listProviders !== 'function' || typeof llm.listModels !== 'function') {
|
|
485
|
+
throw new Error('llm 模型目录不可用,审核会话无法启动')
|
|
486
|
+
}
|
|
487
|
+
if (!(await llm.listProviders()).some((provider) => provider.id === selection.provider)) {
|
|
488
|
+
throw new Error('审核模型 provider 不可用:' + selection.provider)
|
|
489
|
+
}
|
|
490
|
+
const models = await providerModels(llm, selection.provider)
|
|
491
|
+
const model = models.find((entry) => entry.model === selection.model)
|
|
492
|
+
if (!model) throw new Error('审核 model 不可用:' + selection.model)
|
|
493
|
+
if (selection.reasoningEffort !== undefined
|
|
494
|
+
&& !model.efforts.some((effort) => effort.id === selection.reasoningEffort)) {
|
|
495
|
+
throw new Error('审核模型不支持 reasoningEffort:' + String(selection.reasoningEffort))
|
|
496
|
+
}
|
|
497
|
+
return selection
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Create the review session with its validated selection and hand it the prompt. */
|
|
501
|
+
async function openSession(id, instructions, selection) {
|
|
436
502
|
const audit = registry.get(id)
|
|
437
503
|
if (!audit) return
|
|
438
504
|
let handle = null
|
|
@@ -445,17 +511,6 @@ export function createAuditRunner(options = {}) {
|
|
|
445
511
|
const preset = presets && typeof presets.resolve === 'function' && options.preset
|
|
446
512
|
? await presets.resolve(options.preset)
|
|
447
513
|
: null
|
|
448
|
-
// Which model the review runs on. Not decoration: the persona needs it.
|
|
449
|
-
const defaultModel = serviceOf('defaultModel')
|
|
450
|
-
let selection = null
|
|
451
|
-
if (defaultModel && typeof defaultModel.currentSelection === 'function') {
|
|
452
|
-
const selected = defaultModel.currentSelection()
|
|
453
|
-
if (selected && selected.provider && selected.model) {
|
|
454
|
-
selection = { provider: selected.provider, model: selected.model }
|
|
455
|
-
if (selected.reasoningEffort !== undefined) selection.reasoningEffort = selected.reasoningEffort
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (!selection) throw new Error('读不到默认模型,审核会话无法启动')
|
|
459
514
|
const dir = typeof options.cwd === 'function' ? await options.cwd() : undefined
|
|
460
515
|
handle = await agents.create({
|
|
461
516
|
sessionId: 'plugin-audit-' + randomUUID(),
|
|
@@ -491,7 +546,7 @@ export function createAuditRunner(options = {}) {
|
|
|
491
546
|
}
|
|
492
547
|
registry.attachSession(id, { id: String(agent.id), title }, '审核会话正在读源码…')
|
|
493
548
|
watch(id, agent)
|
|
494
|
-
const prompt = registry.prompt(id)
|
|
549
|
+
const prompt = registry.prompt(id, instructions)
|
|
495
550
|
if (typeof agent.followup === 'function') await agent.followup(await userMessage(prompt))
|
|
496
551
|
else if (typeof agent.send === 'function') await agent.send(await userMessage(prompt))
|
|
497
552
|
else throw new Error('审核会话不接受消息')
|
|
@@ -578,15 +633,33 @@ export function createAuditRunner(options = {}) {
|
|
|
578
633
|
}
|
|
579
634
|
|
|
580
635
|
return {
|
|
636
|
+
/** @returns live models and the profile default; unavailable providers are omitted. */
|
|
637
|
+
async models() {
|
|
638
|
+
const selection = defaultSelection()
|
|
639
|
+
const llm = serviceOf('llm')
|
|
640
|
+
if (!llm || typeof llm.listProviders !== 'function' || typeof llm.listModels !== 'function') {
|
|
641
|
+
return { models: [], defaultSelection: selection }
|
|
642
|
+
}
|
|
643
|
+
const providers = await llm.listProviders()
|
|
644
|
+
const groups = await Promise.all(providers.map(async (provider) => {
|
|
645
|
+
try { return await providerModels(llm, provider.id) }
|
|
646
|
+
catch { return [] }
|
|
647
|
+
}))
|
|
648
|
+
return { models: groups.flat(), defaultSelection: selection }
|
|
649
|
+
},
|
|
581
650
|
/**
|
|
582
651
|
* The card's one entry point: give it what the user typed, get back a running
|
|
583
652
|
* audit. The session is open before this resolves, the verdict is not.
|
|
584
|
-
* @param input - `{ target, mode }`, where mode is `install` (default) or `review
|
|
653
|
+
* @param input - `{ target, mode, prompt, selection }`, where mode is `install` (default) or `review`,
|
|
654
|
+
* prompt is optional guidance (at most 20000 characters), and selection is
|
|
655
|
+
* `{ provider, model, reasoningEffort? }` (omitted means the profile default).
|
|
585
656
|
* @returns the audit in the endpoint shape.
|
|
586
657
|
*/
|
|
587
658
|
async start(input = {}) {
|
|
659
|
+
const instructions = normalizeAuditInstructions(input.prompt)
|
|
588
660
|
const raw = typeof input.target === 'string' ? input.target.trim() : ''
|
|
589
661
|
if (raw === '') throw new Error('请填写要审核的插件:npm 包名、owner/repo 或仓库地址')
|
|
662
|
+
const selection = await selectModel(input.selection)
|
|
590
663
|
await ensureLoaded()
|
|
591
664
|
const resolved = await resolveTarget(raw, { fetchJson })
|
|
592
665
|
// Before a session is spent on it: does this runtime accept the package at
|
|
@@ -599,7 +672,7 @@ export function createAuditRunner(options = {}) {
|
|
|
599
672
|
findings: (Array.isArray(resolved.findings) ? resolved.findings : []).concat(compatibility.findings),
|
|
600
673
|
compatibility,
|
|
601
674
|
})
|
|
602
|
-
await openSession(audit.id)
|
|
675
|
+
await openSession(audit.id, instructions, selection)
|
|
603
676
|
return present(registry.get(audit.id))
|
|
604
677
|
},
|
|
605
678
|
/**
|
package/lib/host/audit.js
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { dirname, join } from 'node:path'
|
|
32
|
+
import { DEFAULT_AUDIT_INSTRUCTIONS } from '../audit-instructions.js'
|
|
33
|
+
|
|
34
|
+
export { DEFAULT_AUDIT_INSTRUCTIONS }
|
|
32
35
|
|
|
33
36
|
/** The three answers an audit may produce, in ascending severity. */
|
|
34
37
|
export const AUDIT_VERDICTS = Object.freeze(['safe', 'warn', 'unsafe'])
|
|
@@ -632,18 +635,36 @@ export function normalizeVerdict(payload) {
|
|
|
632
635
|
return { verdict, summary, findings }
|
|
633
636
|
}
|
|
634
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Validate editable guidance before starting any work. Only omitted or blank
|
|
640
|
+
* strings use the default; a nonblank string keeps its original formatting.
|
|
641
|
+
* @param prompt - optional guidance supplied by the card.
|
|
642
|
+
* @returns the guidance text, without host-owned context or constraints.
|
|
643
|
+
*/
|
|
644
|
+
export function normalizeAuditInstructions(prompt) {
|
|
645
|
+
if (prompt === undefined) return DEFAULT_AUDIT_INSTRUCTIONS
|
|
646
|
+
if (typeof prompt !== 'string') throw new TypeError('prompt 必须是字符串')
|
|
647
|
+
if (prompt.length > 20000) throw new RangeError('prompt 最多 20000 字符')
|
|
648
|
+
return prompt.trim() === '' ? DEFAULT_AUDIT_INSTRUCTIONS : prompt
|
|
649
|
+
}
|
|
650
|
+
|
|
635
651
|
/**
|
|
636
652
|
* The instruction the audit session receives. It is written for a reader that
|
|
637
653
|
* has tools and no context: where the source is, what to look for, what the
|
|
638
654
|
* three verdicts mean, and the exact shape the answer has to take.
|
|
639
655
|
* @param audit - a record from the registry.
|
|
656
|
+
* @param prompt - optional editable guidance; host context and constraints always follow.
|
|
640
657
|
* @returns the prompt text.
|
|
641
658
|
*/
|
|
642
|
-
export function buildAuditPrompt(audit) {
|
|
659
|
+
export function buildAuditPrompt(audit, prompt) {
|
|
660
|
+
const instructions = normalizeAuditInstructions(prompt)
|
|
643
661
|
const target = audit && audit.target ? audit.target : {}
|
|
644
662
|
const lines = []
|
|
645
663
|
lines.push('这台机器上有一张「插件审核」卡片,你负责这一次审核。请读完待审插件的源码,判断它是否安全,最后调用 `' + AUDIT_TOOL_NAME + '` 工具汇报结论。')
|
|
646
664
|
lines.push('')
|
|
665
|
+
lines.push('## 怎么做')
|
|
666
|
+
lines.push(instructions)
|
|
667
|
+
lines.push('')
|
|
647
668
|
lines.push('## 待审对象')
|
|
648
669
|
lines.push('- 用户输入:' + String(target.input || ''))
|
|
649
670
|
lines.push('- 名称:' + String(target.name || '(未知)') + (target.version ? ' @ ' + target.version : ''))
|
|
@@ -664,27 +685,9 @@ export function buildAuditPrompt(audit) {
|
|
|
664
685
|
}
|
|
665
686
|
}
|
|
666
687
|
lines.push('')
|
|
667
|
-
lines.push('##
|
|
668
|
-
lines.push('
|
|
669
|
-
lines.push('
|
|
670
|
-
lines.push('3. 按下面的顺序找问题:')
|
|
671
|
-
lines.push(' - 恶意行为:读取或外传凭据/令牌(`~/.dsh`、环境变量、ssh、云厂商配置)、上传本机文件、反弹 shell、挖矿、删除或加密用户文件、篡改 DSH 本身或其他插件;')
|
|
672
|
-
lines.push(' - 执行链:安装期脚本(preinstall/install/postinstall)执行命令;运行期 `child_process`;`eval`/`new Function`;从网络下载再执行;写入 `~/.dsh`、profile 或系统目录;')
|
|
673
|
-
lines.push(' - 隐蔽性:混淆或加密的字符串、base64 之后 eval、与功能无关的外联域名、把内容塞进资源文件里;')
|
|
674
|
-
lines.push(' - 越权:绕过 DSH 的沙箱与审批,或去调用别的插件的私有服务;')
|
|
675
|
-
lines.push(' - 版本:peer 范围与当前 DSH 版本是否相容(DSH 在安装时会拒绝不相容的包)。')
|
|
676
|
-
lines.push('4. 结论只有三档:')
|
|
677
|
-
lines.push(' - `safe`:没有发现上述任何问题,且代码与它声称的功能一致;')
|
|
678
|
-
lines.push(' - `warn`:可疑、需要用户知情(安装脚本、外部网络、权限偏大、版本不相容、源码读不到),但不像恶意;')
|
|
679
|
-
lines.push(' - `unsafe`:发现明确的恶意行为或高风险执行链。')
|
|
680
|
-
lines.push('5. 每一条 `warn` 都要带 `risk`,替用户回答四个问题——这是他唯一能据以决定装不装的东西:')
|
|
681
|
-
lines.push(' - `role`:这段代码在插件里做什么(功能角色);')
|
|
682
|
-
lines.push(' - `why`:插件为什么需要它(合理的功能需要,还是抄来的惯例、与功能无关);')
|
|
683
|
-
lines.push(' - `impact`:一旦被滥用会造成什么后果(拿到了什么、改了什么、传出了什么);')
|
|
684
|
-
lines.push(' - `scenario` 取 `supply-chain`(供应链攻击:安装期/更新链上执行代码)、`privilege-escalation`(Agent 越权:绕过沙箱或审批、调用别的插件的私有服务)、`sandbox-escape`(沙箱逃逸)、`data-leak`(数据泄露:凭据/文件/环境变量被读取或外传)、`unsafe-publish`(不安全的网络发布)、`other`。')
|
|
685
|
-
lines.push(' 只知道一部分就写那一部分,并在 `detail` 里说清你没看到什么;不要凭空补。`info`/`error` 不需要 `risk`。')
|
|
686
|
-
lines.push('6. 调用 `' + AUDIT_TOOL_NAME + '`,`auditId` 必须是 `' + String((audit && audit.id) || '') + '`;`summary` 用中文一到两句说清结论与最重要的理由;`findings` 每一项 `{ level, title, detail, ref }`(`warn` 再加 `risk: { role, why, impact, scenario }`),`level` 取 `info`/`warn`/`error`,`ref` 写具体位置(例如 `lib/index.js:120`)。')
|
|
687
|
-
lines.push('7. 只汇报一次。不确定就给 `warn`,并在 findings 里写清你没能看到什么。不要修改任何文件,不要自己安装插件,也不要执行仓库里的代码。')
|
|
688
|
+
lines.push('## 汇报要求与安全约束(始终适用)')
|
|
689
|
+
lines.push('调用 `' + AUDIT_TOOL_NAME + '`,`auditId` 必须是 `' + String((audit && audit.id) || '') + '`;`summary` 用中文一到两句说清结论与最重要的理由;`findings` 每一项 `{ level, title, detail, ref }`(`warn` 再加 `risk: { role, why, impact, scenario }`),`level` 取 `info`/`warn`/`error`,`ref` 写具体位置(例如 `lib/index.js:120`)。')
|
|
690
|
+
lines.push('只汇报一次。不确定就给 `warn`,并在 findings 里写清你没能看到什么。不要修改任何文件,不要自己安装插件,也不要执行仓库里的代码。')
|
|
688
691
|
lines.push('')
|
|
689
692
|
const mode = audit && audit.mode === 'review' ? 'review' : 'install'
|
|
690
693
|
lines.push(mode === 'review'
|
|
@@ -881,9 +884,9 @@ export function createAuditRegistry(options = {}) {
|
|
|
881
884
|
audit.updatedAt = now()
|
|
882
885
|
return changed(audit)
|
|
883
886
|
},
|
|
884
|
-
/** @returns the prompt for one audit. */
|
|
885
|
-
prompt(id) {
|
|
886
|
-
return buildAuditPrompt(need(id))
|
|
887
|
+
/** @returns the prompt for one audit, with optional editable guidance. */
|
|
888
|
+
prompt(id, instructions) {
|
|
889
|
+
return buildAuditPrompt(need(id), instructions)
|
|
887
890
|
},
|
|
888
891
|
/**
|
|
889
892
|
* Record what the session reported. One report per audit: a second call is
|
package/lib/index.js
CHANGED
|
@@ -1395,6 +1395,7 @@ export function apply(ctx, config) {
|
|
|
1395
1395
|
// from `agent.options.model`, so a session created without a selection cannot
|
|
1396
1396
|
// assemble its prompt (that was the first live failure of this card).
|
|
1397
1397
|
defaultModel: () => service(ctx, 'agentDefaultModel'),
|
|
1398
|
+
llm: () => service(ctx, 'llm'),
|
|
1398
1399
|
permissions: () => service(ctx, 'permissionPresets'),
|
|
1399
1400
|
title: () => service(ctx, 'sessionTitle'),
|
|
1400
1401
|
on: (event, handler) => ctx.on(event, handler),
|
|
@@ -1642,11 +1643,16 @@ export function apply(ctx, config) {
|
|
|
1642
1643
|
try { return await sidebarStore().write(layout) }
|
|
1643
1644
|
catch (error) { throw new Error(remoteError(error)) }
|
|
1644
1645
|
},
|
|
1646
|
+
/** @returns available audit models, reasoning efforts and the profile default selection. */
|
|
1647
|
+
async pluginAuditModels() {
|
|
1648
|
+
try { return await audits.models() }
|
|
1649
|
+
catch (error) { throw new Error(remoteError(error)) }
|
|
1650
|
+
},
|
|
1645
1651
|
/**
|
|
1646
1652
|
* Open a security review of one plugin: resolve the target, create the
|
|
1647
1653
|
* review session, hand it the prompt. The verdict arrives later, through the
|
|
1648
1654
|
* `report_plugin_audit` tool, so the caller polls `pluginAuditStatus`.
|
|
1649
|
-
* @param options - `{ target, mode }
|
|
1655
|
+
* @param options - `{ target, mode, prompt, selection }`; selection is `{ provider, model, reasoningEffort? }`.
|
|
1650
1656
|
* @returns the audit in the endpoint shape.
|
|
1651
1657
|
*/
|
|
1652
1658
|
async pluginAuditStart(options) {
|
|
@@ -1896,6 +1902,7 @@ export function apply(ctx, config) {
|
|
|
1896
1902
|
remoteInvocation('sidebarLayout', []),
|
|
1897
1903
|
remoteInvocation('saveSidebarLayout', ['layout']),
|
|
1898
1904
|
remoteInvocation('pluginAuditStart', ['options']),
|
|
1905
|
+
remoteInvocation('pluginAuditModels', ['options']),
|
|
1899
1906
|
remoteInvocation('pluginAuditStatus', ['options']),
|
|
1900
1907
|
remoteInvocation('pluginAuditList', []),
|
|
1901
1908
|
remoteInvocation('pluginAuditDecide', ['options']),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-homepage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a personal home page in the sidebar for a DSH instance. Review a plugin before installing it, and read sessions, projects, tool usage, device, storage, DSH cores, Skills and cost from one composable, registrable card wall.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"locale/*.json",
|
|
51
51
|
"icon.svg",
|
|
52
52
|
"docs",
|
|
53
|
+
"screenshots.json",
|
|
53
54
|
"README.md",
|
|
54
55
|
"README.zh.md",
|
|
55
56
|
"LICENSE"
|
package/screenshots.json
ADDED