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 CHANGED
@@ -4,6 +4,7 @@
4
4
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![node: >=22.19](https://img.shields.io/badge/node-%3E%3D22.19-339933.svg)](package.json)
6
6
  [![DSH: 0.2.x](https://img.shields.io/badge/DSH-0.2.x-6f42c1.svg)](package.json)
7
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![node: >=22.19](https://img.shields.io/badge/node-%3E%3D22.19-339933.svg)](package.json)
6
6
  [![DSH: 0.2.x](https://img.shields.io/badge/DSH-0.2.x-6f42c1.svg)](package.json)
7
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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
- api.remote(START, { options: { target: wanted, mode: decision } })
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
- ? h('div', { className: 'ph-audit-finding-body' },
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
- setPane('current')
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
- setPane(wanted)
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('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.target'))),
756
- h(TargetRow, {
757
- t: t, busy: starter.busy, mode: starter.mode, setMode: starter.setMode,
758
- onStart: starter.start, draft: draft, setDraft: setDraft,
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-kv' },
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 && (canInstall(audit) || canDecline(audit)) ? h(VerdictActions, { api: api, t: t, audit: audit, view: view }) : null,
792
- h('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.verdict'))),
793
- h('p', { className: 'ph-note' }, audit && audit.summary ? String(audit.summary) : t('pluginAudit.summary.none')),
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('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.session'))),
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': '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-findings{display:flex;flex-direction:column;gap:6px}',
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
- api.remote(START, { options: { target: wanted, mode: decision } })
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
- ? h('div', { className: 'ph-audit-finding-body' },
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
- setPane('current')
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
- setPane(wanted)
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('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.target'))),
8850
- h(TargetRow, {
8851
- t: t, busy: starter.busy, mode: starter.mode, setMode: starter.setMode,
8852
- onStart: starter.start, draft: draft, setDraft: setDraft,
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-kv' },
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 && (canInstall(audit) || canDecline(audit)) ? h(VerdictActions, { api: api, t: t, audit: audit, view: view }) : null,
8886
- h('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.verdict'))),
8887
- h('p', { className: 'ph-note' }, audit && audit.summary ? String(audit.summary) : t('pluginAudit.summary.none')),
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('div', { className: 'ph-section-head' }, h('h4', { className: 'ph-section-title' }, t('pluginAudit.section.session'))),
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': 'View',
9286
+ 'pluginAudit.history.view': 'Load entry',
9106
9287
 
9107
9288
  'pluginAudit.kv.plugin': 'Plugin',
9108
9289
  'pluginAudit.kv.version': 'Version',
@@ -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-findings{display:flex;flex-direction:column;gap:6px}',
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))}',
@@ -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
- /** Create the review session and hand it the prompt. */
435
- async function openSession(id) {
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('1. 取源码:`web_fetch` 读 `https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path>`,或用 `github_file`/`github_api` 列目录、读文件;必要时读取 npm 包 tarball 的内容。先读 `package.json`,再读入口文件,再顺着依赖与被动态加载的代码走下去。')
669
- lines.push('2. 读代码,不读宣言:README 说什么是它想让你以为的,代码做什么才是要报的。')
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 }`, where mode is `install` or `review`.
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.2",
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"
@@ -0,0 +1,4 @@
1
+ [
2
+ "docs/img/en.png",
3
+ "docs/img/zh.png"
4
+ ]