dsh-redteam-report 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/WORKSPACE-REPORTS.md +48 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +747 -0
- package/lib/host.js +119 -0
- package/lib/parts/client.head.js +15 -0
- package/lib/parts/client.shim.js +45 -0
- package/lib/parts/client.tail.js +5 -0
- package/lib/parts/host.head.js +80 -0
- package/lib/parts/host.tail.js +37 -0
- package/package.json +68 -0
- package/src/client.js +602 -0
- package/src/docx.js +776 -0
- package/src/host.js +1176 -0
- package/src/workspace-client.js +108 -0
- package/src/workspace-evidence.js +310 -0
- package/src/workspace-install.js +60 -0
- package/src/workspace-runtime.js +409 -0
- package/tools/build-lib.mjs +120 -0
- package/tools/prepare-gh-packages.mjs +66 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// 红队报告 · Client 半边
|
|
2
|
+
//
|
|
3
|
+
// lib/client.js 由 `npm run build:lib` 从本文件生成,不要手改 lib/。
|
|
4
|
+
// 生成器以函数名 `applyClient` 为入口锚点,并在其中注入 host.call / styles.insert 垫片
|
|
5
|
+
// ——静态 bundle 里没有这两个闭包符号,原因见 ../../docs/DEVELOPMENT.md。
|
|
6
|
+
//
|
|
7
|
+
// 注意:本文件必须以 `return { name, inject, apply }` 块【结尾】,生成器据此剥离动态包装。
|
|
8
|
+
//
|
|
9
|
+
// ── 界面结构 ──────────────────────────────────────────────────────────────────
|
|
10
|
+
// ┌ 顶栏:报告份数 · 当前字数(未保存)· 证据规模 · 生成进度 + 刷新
|
|
11
|
+
// ├ 标签页:报告 / 证据 / 设置 / 日志
|
|
12
|
+
// ├ 报告:报告列表(切换)· 标题 · 操作行(生成 / 保存 | 导出 MD·HTML·Word | 导入记忆 / 删除)
|
|
13
|
+
// │ 左编辑(Markdown,带字数)右预览(宿主渲染的完整 HTML,所见即导出)
|
|
14
|
+
// ├ 证据:试算证据 —— 会话采集表 + **真正喂给模型的那份 digest**
|
|
15
|
+
// ├ 设置:撰写模型 · 证据预算 · 路径 · 额外要求
|
|
16
|
+
// └ 日志:最近操作与错误
|
|
17
|
+
//
|
|
18
|
+
// ── 两条设计纪律 ─────────────────────────────────────────────────────────────
|
|
19
|
+
// 1. **一屏只留一个主操作**。原来九个按钮挤在一行里(生成 / 新建×2 / 保存 / 导出×3 /
|
|
20
|
+
// 导入 / 删除),主次不分;现在生成与保存是主,导出归成一组,删除这类破坏性操作
|
|
21
|
+
// 弱化到右侧并带二次语义(红色文字而不是红色实心块)。
|
|
22
|
+
// 2. **颜色只用真实存在的主题 token**。之前写的 --dsw-alias-bg-l1/l2/l3 并不存在,
|
|
23
|
+
// 于是背景一直掉回硬编码 rgba —— 深浅色主题下观感不一致,看着「脏」。
|
|
24
|
+
|
|
25
|
+
function applyClient(ctx) {
|
|
26
|
+
const slots = ctx.slots
|
|
27
|
+
|
|
28
|
+
const PANEL_KEY = 'redteam-report'
|
|
29
|
+
const TABS = [['doc', '报告'], ['evi', '证据'], ['set', '设置'], ['log', '日志']]
|
|
30
|
+
const PREVIEW_DEBOUNCE_MS = 400
|
|
31
|
+
|
|
32
|
+
// ── 通用小工具 ──────────────────────────────────────────────────────────────
|
|
33
|
+
function el(tag, props) {
|
|
34
|
+
const children = Array.prototype.slice.call(arguments, 2)
|
|
35
|
+
return React.createElement.apply(React, [tag, props || {}].concat(children))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 过桥的参数必须是 JSON:丢掉 undefined / null,否则 bridge 会直接拒绝
|
|
39
|
+
function jsonArgs(o) {
|
|
40
|
+
const out = {}
|
|
41
|
+
for (const k of Object.keys(o || {})) {
|
|
42
|
+
const v = o[k]
|
|
43
|
+
if (v === undefined || v === null) continue
|
|
44
|
+
out[k] = v
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fmtTime(ms) {
|
|
50
|
+
if (!ms) return '—'
|
|
51
|
+
const n = Number(ms)
|
|
52
|
+
if (!isFinite(n) || n <= 0) return String(ms)
|
|
53
|
+
const d = new Date(n)
|
|
54
|
+
if (isNaN(d.getTime())) return String(ms)
|
|
55
|
+
const p = function (x) { return x < 10 ? '0' + x : String(x) }
|
|
56
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function reportError(e) { return String((e && e.message) || e) }
|
|
60
|
+
function fmtNum(n) { const v = Number(n) || 0; return v >= 10000 ? (v / 1000).toFixed(1) + 'k' : String(v) }
|
|
61
|
+
|
|
62
|
+
// UI 基元:按钮 / 卡片 / 徽标 / 按钮组。两个层级(主/次)就够,
|
|
63
|
+
// 全部平铺才是「丑」的根源 —— 人看不出该点哪个。
|
|
64
|
+
function btn(label, opts, onClick) {
|
|
65
|
+
const o = opts || {}
|
|
66
|
+
const cls = 'rtr-btn' + (o.primary ? ' rtr-btn-primary' : '') + (o.danger ? ' rtr-btn-danger' : '') + (o.mini ? ' rtr-btn-mini' : '')
|
|
67
|
+
return el('button', { key: o.key || label, className: cls, disabled: o.disabled === true, title: o.title || '', onClick: onClick }, label)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function card(title, sub, children, extra) {
|
|
71
|
+
return el('section', { className: 'rtr-card' },
|
|
72
|
+
el('div', { className: 'rtr-card-h' },
|
|
73
|
+
el('span', { className: 'rtr-card-t' }, title),
|
|
74
|
+
sub ? el('span', { className: 'rtr-card-s' }, sub) : null,
|
|
75
|
+
el('span', { className: 'rtr-sp' }),
|
|
76
|
+
extra || null),
|
|
77
|
+
el('div', { className: 'rtr-card-b' }, children))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function badge(text, kind) {
|
|
81
|
+
return el('span', { className: 'rtr-badge' + (kind ? ' rtr-badge-' + kind : '') }, text)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function group(label, children) {
|
|
85
|
+
return el('div', { className: 'rtr-group' }, el('span', { className: 'rtr-group-l' }, label), children)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hint(text) { return el('span', { className: 'rtr-hint' }, text) }
|
|
89
|
+
|
|
90
|
+
function Panel() {
|
|
91
|
+
const [snap, setSnap] = React.useState(null)
|
|
92
|
+
const [tab, setTab] = React.useState('doc')
|
|
93
|
+
const [busy, setBusy] = React.useState('')
|
|
94
|
+
const [error, setError] = React.useState(null)
|
|
95
|
+
const [toast, setToast] = React.useState('')
|
|
96
|
+
const [draft, setDraft] = React.useState(null)
|
|
97
|
+
const [title, setTitle] = React.useState('')
|
|
98
|
+
const [markdown, setMarkdown] = React.useState('')
|
|
99
|
+
const [dirty, setDirty] = React.useState(false)
|
|
100
|
+
const [html, setHtml] = React.useState('')
|
|
101
|
+
const [instruction, setInstruction] = React.useState('')
|
|
102
|
+
const [evidence, setEvidence] = React.useState(null)
|
|
103
|
+
const [exportInfo, setExportInfo] = React.useState(null)
|
|
104
|
+
|
|
105
|
+
function applySnap(r) {
|
|
106
|
+
if (!r || r.ok !== true) { setError((r && r.error) || '宿主没有返回数据'); return }
|
|
107
|
+
setError(null)
|
|
108
|
+
setSnap(r.snapshot)
|
|
109
|
+
setDraft(function (d) { return d || JSON.parse(JSON.stringify(r.snapshot.settings)) })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function refresh() {
|
|
113
|
+
setBusy('load')
|
|
114
|
+
return host.call('snapshot', {}).then(applySnap)
|
|
115
|
+
.catch(function (e) { setError(reportError(e)) })
|
|
116
|
+
.then(function () { setBusy('') })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function call(method, args, label) {
|
|
120
|
+
setBusy(method)
|
|
121
|
+
setToast('')
|
|
122
|
+
return host.call(method, jsonArgs(args || {}))
|
|
123
|
+
.then(function (r) {
|
|
124
|
+
if (r && r.snapshot) applySnap({ ok: true, snapshot: r.snapshot })
|
|
125
|
+
if (r && r.ok === false) { setError(r.error || (label + '失败')); return r }
|
|
126
|
+
setError(null)
|
|
127
|
+
if (label) setToast(label + '完成')
|
|
128
|
+
return r
|
|
129
|
+
})
|
|
130
|
+
.catch(function (e) { setError(reportError(e)); return null })
|
|
131
|
+
.then(function (r) { setBusy(''); return r })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
React.useEffect(function () { refresh() }, [])
|
|
135
|
+
|
|
136
|
+
// 生成是后台任务:轮询 snapshot 才能看到进度与正在长出来的正文。
|
|
137
|
+
const liveRef = React.useRef({ generating: false, dirty: false })
|
|
138
|
+
liveRef.current = { generating: !!(snap && snap.status && snap.status.generating), dirty: dirty }
|
|
139
|
+
React.useEffect(function () {
|
|
140
|
+
if (typeof ctx.interval !== 'function') return undefined
|
|
141
|
+
return ctx.interval(function () {
|
|
142
|
+
if (!liveRef.current.generating) return
|
|
143
|
+
host.call('snapshot', {}).then(function (r) {
|
|
144
|
+
if (!r || r.ok !== true) return
|
|
145
|
+
setSnap(r.snapshot)
|
|
146
|
+
// 用户改过就不覆盖他的字;没改过就把流出来的正文同步进来。
|
|
147
|
+
if (!liveRef.current.dirty && r.snapshot.current) {
|
|
148
|
+
setMarkdown(r.snapshot.current.markdown || '')
|
|
149
|
+
setTitle(r.snapshot.current.title || '')
|
|
150
|
+
}
|
|
151
|
+
}).catch(function () {})
|
|
152
|
+
}, 2500)
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
// 切换当前报告时同步编辑框(用户有未保存的改动就不动)
|
|
156
|
+
const curId = snap && snap.currentId ? snap.currentId : ''
|
|
157
|
+
React.useEffect(function () {
|
|
158
|
+
if (!snap || !snap.current || dirty) return
|
|
159
|
+
setTitle(snap.current.title || '')
|
|
160
|
+
setMarkdown(snap.current.markdown || '')
|
|
161
|
+
}, [curId])
|
|
162
|
+
|
|
163
|
+
// 预览:防抖后让宿主渲染,拿到完整 HTML 文档塞进 iframe。
|
|
164
|
+
// 定时器走 ctx.get('timer'):动态客户端沙箱里没有 setTimeout。
|
|
165
|
+
React.useEffect(function () {
|
|
166
|
+
const timer = ctx.get('timer')
|
|
167
|
+
const run = function () {
|
|
168
|
+
host.call('preview', jsonArgs({ id: curId, title: title, markdown: markdown }))
|
|
169
|
+
.then(function (r) { if (r && r.ok === true) setHtml(r.html) })
|
|
170
|
+
.catch(function () {})
|
|
171
|
+
}
|
|
172
|
+
if (timer && typeof timer.timeout === 'function') {
|
|
173
|
+
const dispose = timer.timeout(run, PREVIEW_DEBOUNCE_MS)
|
|
174
|
+
return function () { try { dispose() } catch (e) {} }
|
|
175
|
+
}
|
|
176
|
+
run()
|
|
177
|
+
return undefined
|
|
178
|
+
}, [markdown, title, curId])
|
|
179
|
+
|
|
180
|
+
function doGenerate(createNew) {
|
|
181
|
+
setToast('')
|
|
182
|
+
setExportInfo(null)
|
|
183
|
+
call('generate', { reportId: createNew ? undefined : curId, createNew: createNew === true, instruction: instruction.trim() || undefined }, '已开始生成')
|
|
184
|
+
.then(function (r) {
|
|
185
|
+
if (r && r.ok === true) { setDirty(false); setMarkdown(''); setTitle(''); setTab('doc') }
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function doSave() {
|
|
190
|
+
call('saveDraft', { id: curId, title: title, markdown: markdown }, '保存').then(function (r) {
|
|
191
|
+
if (r && r.ok === true) setDirty(false)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function doExport(format) {
|
|
196
|
+
setExportInfo(null)
|
|
197
|
+
call('export', { id: curId, format: format }).then(function (r) {
|
|
198
|
+
if (r && r.ok === true) setExportInfo(r)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function doImport() {
|
|
203
|
+
call('importToMemory', { id: curId }, '导入记忆').then(function (r) {
|
|
204
|
+
if (r && r.ok === true) setToast('已导入记忆:新增 ' + r.added + ' 条 / 覆盖 ' + r.updated + ' 条' + (r.indexError ? '(索引未同步,本地已存)' : ''))
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function doCollect() {
|
|
209
|
+
setEvidence(null)
|
|
210
|
+
call('collect', { preview: 12000 }, '试算证据').then(function (r) {
|
|
211
|
+
if (r && r.ok === true) setEvidence(r.evidence)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function doNew() {
|
|
216
|
+
call('create', { title: '' }, '新建').then(function (r) {
|
|
217
|
+
if (r && r.ok === true) { setDirty(false); setMarkdown(''); setTitle(''); setTab('doc') }
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function doRemove() {
|
|
222
|
+
if (!curId) { setError('先选中一份报告'); return }
|
|
223
|
+
call('remove', { ids: [curId] }, '删除报告').then(function (r) {
|
|
224
|
+
if (r && r.ok === true) { setDirty(false); setMarkdown(''); setTitle(''); setEvidence(null) }
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function setField(k, v) {
|
|
229
|
+
setDraft(function (d) {
|
|
230
|
+
const n = JSON.parse(JSON.stringify(d || {}))
|
|
231
|
+
n[k] = v
|
|
232
|
+
return n
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function setModelField(k, v) {
|
|
237
|
+
setDraft(function (d) {
|
|
238
|
+
const n = JSON.parse(JSON.stringify(d || {}))
|
|
239
|
+
if (!n.model) n.model = { provider: '', model: '' }
|
|
240
|
+
n.model[k] = v
|
|
241
|
+
return n
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const st = snap ? snap.status : null
|
|
246
|
+
const reports = (snap && snap.reports) || []
|
|
247
|
+
const cur = snap && snap.current ? snap.current : null
|
|
248
|
+
const generating = !!(st && st.generating)
|
|
249
|
+
const evMeta = (cur && cur.meta && cur.meta.evidence) || (st && st.evidence) || null
|
|
250
|
+
const hasBody = !!String(markdown || '').trim()
|
|
251
|
+
|
|
252
|
+
function head() {
|
|
253
|
+
const bits = ['报告 ' + reports.length + ' 份']
|
|
254
|
+
if (cur) bits.push(fmtNum(markdown.length) + ' 字' + (dirty ? '(未保存)' : ''))
|
|
255
|
+
if (evMeta) bits.push('证据:会话 ' + (evMeta.sessions || 0) + ' · 矩阵 ' + (evMeta.matrixConfirmed || 0) + '/' + (evMeta.matrixSuspected || 0) + ' · 记忆 ' + (evMeta.memoryHits || 0))
|
|
256
|
+
return el('div', { className: 'rtr-head' },
|
|
257
|
+
el('span', { className: 'rtr-brand' }, '红队报告'),
|
|
258
|
+
el('span', { className: 'rtr-stat' }, bits.join(' · ')),
|
|
259
|
+
st && st.progress ? el('span', { className: 'rtr-stat rtr-brand-c' }, st.progress.text) : null,
|
|
260
|
+
el('span', { className: 'rtr-sp' }),
|
|
261
|
+
btn('刷新', { disabled: !!busy }, refresh))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── 报告 tab ──────────────────────────────────────────────────────────────
|
|
265
|
+
function docTab() {
|
|
266
|
+
if (!reports.length) {
|
|
267
|
+
return el('div', { className: 'rtr-body' },
|
|
268
|
+
card('还没有报告', '一句话说清它读什么', [
|
|
269
|
+
el('div', { className: 'rtr-hint' },
|
|
270
|
+
'点下面的按钮,AI 会自动收集三样东西再动笔:**当前工作区的对话**(用户要求 / 关键操作 / 结果与结论)、'
|
|
271
|
+
+ '**攻击矩阵的命中**(已确认与疑似分开,带技术点名字与判据)、**记忆库里相关的知识条目**。'
|
|
272
|
+
+ '写完之后可以在「证据」页看到它到底读到了什么。'),
|
|
273
|
+
el('div', { className: 'rtr-row' },
|
|
274
|
+
btn('生成报告', { primary: true, disabled: !!busy || generating }, function () { doGenerate(false) }),
|
|
275
|
+
btn('新建空白报告', { disabled: !!busy }, doNew)),
|
|
276
|
+
]))
|
|
277
|
+
}
|
|
278
|
+
return el('div', { className: 'rtr-body' },
|
|
279
|
+
// 报告列表:切换用,带字数与时间,比一行纯标题好认
|
|
280
|
+
card('报告', reports.length + ' 份', [
|
|
281
|
+
el('div', { className: 'rtr-chips' }, reports.map(function (r) {
|
|
282
|
+
return el('button', {
|
|
283
|
+
key: r.id, className: 'rtr-chip' + (r.id === curId ? ' rtr-chip-on' : ''), disabled: !!busy,
|
|
284
|
+
title: (r.provider ? r.provider + '/' + r.model + ' · ' : '') + '更新于 ' + fmtTime(r.updatedAt),
|
|
285
|
+
onClick: function () { if (r.id === curId) return; setDirty(false); call('select', { id: r.id }, '') },
|
|
286
|
+
},
|
|
287
|
+
el('span', { className: 'rtr-chip-t' }, r.title || '未命名'),
|
|
288
|
+
el('span', { className: 'rtr-chip-m' }, fmtNum(r.chars) + ' 字 · ' + fmtTime(r.updatedAt)))
|
|
289
|
+
})),
|
|
290
|
+
], btn('新建空白', { mini: true, disabled: !!busy }, doNew)),
|
|
291
|
+
|
|
292
|
+
curId ? el('div', { className: 'rtr-edit' },
|
|
293
|
+
el('input', {
|
|
294
|
+
className: 'rtr-in rtr-title', value: title, placeholder: '报告标题',
|
|
295
|
+
onChange: function (e) { setTitle(e.target.value); setDirty(true) },
|
|
296
|
+
}),
|
|
297
|
+
|
|
298
|
+
// 一条操作行:主操作在左,导出成组在中间,破坏性操作弱化在右
|
|
299
|
+
el('div', { className: 'rtr-tools' },
|
|
300
|
+
btn(generating ? '生成中…' : (hasBody ? '重新生成' : '生成报告'), { primary: true, disabled: !!busy || generating }, function () { doGenerate(false) }),
|
|
301
|
+
btn(dirty ? '保存 *' : '保存', { disabled: !!busy || generating || !dirty }, doSave),
|
|
302
|
+
el('span', { className: 'rtr-sep' }),
|
|
303
|
+
group('导出', el('span', { className: 'rtr-seg' },
|
|
304
|
+
btn('Markdown', { mini: true, disabled: !!busy || !hasBody }, function () { doExport('md') }),
|
|
305
|
+
btn('HTML', { mini: true, disabled: !!busy || !hasBody }, function () { doExport('html') }),
|
|
306
|
+
btn('Word', { mini: true, primary: true, disabled: !!busy || !hasBody, title: '导出 .docx(落盘路径显示在下面)' }, function () { doExport('docx') }))),
|
|
307
|
+
btn('导入记忆', { disabled: !!busy || !hasBody, title: '把报告按章节切段写进红队记忆库' }, doImport),
|
|
308
|
+
el('span', { className: 'rtr-sp' }),
|
|
309
|
+
btn('删除', { danger: true, mini: true, disabled: !!busy, title: '删除这份报告(不可恢复)' }, doRemove)),
|
|
310
|
+
|
|
311
|
+
el('div', { className: 'rtr-panes' },
|
|
312
|
+
el('div', { className: 'rtr-pane' },
|
|
313
|
+
el('div', { className: 'rtr-pane-h' },
|
|
314
|
+
el('span', null, 'Markdown'),
|
|
315
|
+
el('span', { className: 'rtr-sp' }),
|
|
316
|
+
hint(fmtNum(markdown.length) + ' 字') + ''),
|
|
317
|
+
el('textarea', {
|
|
318
|
+
className: 'rtr-ta', value: markdown, placeholder: '点「生成报告」让 AI 写,或者直接在这里手写…',
|
|
319
|
+
onChange: function (e) { setMarkdown(e.target.value); setDirty(true) },
|
|
320
|
+
})),
|
|
321
|
+
el('div', { className: 'rtr-pane' },
|
|
322
|
+
el('div', { className: 'rtr-pane-h' }, el('span', null, '预览'), el('span', { className: 'rtr-sp' }), hint('与导出的 HTML / Word 同一套渲染')),
|
|
323
|
+
html
|
|
324
|
+
? el('iframe', { className: 'rtr-frame', srcDoc: html, sandbox: 'allow-same-origin' })
|
|
325
|
+
: el('div', { className: 'rtr-frame rtr-frame-empty' }, '预览生成中…'))),
|
|
326
|
+
|
|
327
|
+
exportInfo ? el('div', { className: exportInfo.writeError ? 'rtr-warn' : 'rtr-ok' },
|
|
328
|
+
'已导出 ' + exportInfo.name + '(' + exportInfo.bytes + ' 字节)'
|
|
329
|
+
+ (exportInfo.path ? ':' + exportInfo.path : '(未落盘)')
|
|
330
|
+
+ (exportInfo.writeError ? '|落盘失败:' + exportInfo.writeError : '')) : null,
|
|
331
|
+
|
|
332
|
+
cur && cur.meta && cur.meta.generatedAt
|
|
333
|
+
? el('div', { className: 'rtr-hint' }, '生成于 ' + fmtTime(cur.meta.generatedAt) + ' · 模型 ' + (cur.meta.provider || '?') + '/' + (cur.meta.model || '?')
|
|
334
|
+
+ (cur.meta.evidence ? ' · 证据 digest ' + fmtNum(cur.meta.evidence.digestChars) + ' 字' : ''))
|
|
335
|
+
: null) : null
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── 证据 tab ──────────────────────────────────────────────────────────────
|
|
340
|
+
function eviTab() {
|
|
341
|
+
return el('div', { className: 'rtr-body' },
|
|
342
|
+
el('div', { className: 'rtr-tools' },
|
|
343
|
+
btn(busy === 'collect' ? '试算中…' : '试算证据', { primary: true, disabled: !!busy }, doCollect),
|
|
344
|
+
hint('把真正喂给模型的那份材料打出来 —— 报告写得不对,九成是证据没采到或提示词没说清')),
|
|
345
|
+
|
|
346
|
+
!evidence ? card('还没试算', null, [
|
|
347
|
+
el('div', { className: 'rtr-hint' }, '点「试算证据」:它会真的跑一遍采集(读工作区会话、攻击矩阵、记忆库),并把裁剪后的 digest 原样显示在这里。不写任何文件、不调模型。'),
|
|
348
|
+
]) : el('div', { className: 'rtr-body' },
|
|
349
|
+
el('div', { className: 'rtr-row' },
|
|
350
|
+
badge('会话 ' + evidence.sessions.length, evidence.sessions.length ? 'brand' : 'warn'),
|
|
351
|
+
badge('已确认 ' + evidence.matrix.confirmed, evidence.matrix.confirmed ? 'ok' : 'warn'),
|
|
352
|
+
badge('疑似 ' + evidence.matrix.suspected, evidence.matrix.suspected ? 'warn' : ''),
|
|
353
|
+
badge('记忆 ' + evidence.memory.count, evidence.memory.count ? 'brand' : 'warn'),
|
|
354
|
+
badge('digest ' + fmtNum(evidence.digestChars) + ' 字', 'brand')),
|
|
355
|
+
hint('工作区 ' + ((evidence.workspace && evidence.workspace.path) || '(无)')
|
|
356
|
+
+ ' · 矩阵来源 ' + evidence.matrix.from
|
|
357
|
+
+ (evidence.memory.available ? '' : ' · 记忆插件未在运行')),
|
|
358
|
+
evidence.matrix.error ? el('div', { className: 'rtr-warn' }, '矩阵读取问题:' + evidence.matrix.error) : null,
|
|
359
|
+
evidence.queries && evidence.queries.length ? hint('记忆检索词:' + evidence.queries.join(' / ')) : null,
|
|
360
|
+
|
|
361
|
+
card('会话采集', evidence.sessions.length + ' 个',
|
|
362
|
+
evidence.sessions.length
|
|
363
|
+
? el('table', { className: 'rtr-tbl' },
|
|
364
|
+
el('thead', null, el('tr', null,
|
|
365
|
+
el('th', null, '会话'), el('th', null, '用户要求'), el('th', null, '操作'), el('th', null, '结果'), el('th', null, '时间'))),
|
|
366
|
+
el('tbody', null, evidence.sessions.map(function (s) {
|
|
367
|
+
return el('tr', { key: s.id },
|
|
368
|
+
el('td', null, s.title || String(s.id).slice(0, 14)),
|
|
369
|
+
el('td', null, String(s.users)),
|
|
370
|
+
el('td', null, String(s.ops)),
|
|
371
|
+
el('td', null, String(s.results)),
|
|
372
|
+
el('td', { className: 'rtr-hint' }, fmtTime(s.firstAt) + ' → ' + fmtTime(s.lastAt)))
|
|
373
|
+
})))
|
|
374
|
+
: el('div', { className: 'rtr-dim' }, '没有采到会话(工作区里还没有对话,或者 sessions 服务不可用)')),
|
|
375
|
+
|
|
376
|
+
card('digest', '前 ' + fmtNum(evidence.digest.length) + ' 字' + (evidence.truncated ? ',实际 ' + fmtNum(evidence.digestChars) + ' 字' : ''),
|
|
377
|
+
el('pre', { className: 'rtr-pre' }, evidence.digest)))
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── 设置 tab ──────────────────────────────────────────────────────────────
|
|
382
|
+
function setTab_() {
|
|
383
|
+
if (!draft) return el('div', { className: 'rtr-body rtr-dim' }, '加载中…')
|
|
384
|
+
return el('div', { className: 'rtr-body' },
|
|
385
|
+
el('div', { className: 'rtr-tools' },
|
|
386
|
+
btn('保存设置', { primary: true, disabled: !!busy }, function () { call('saveSettings', draft, '保存设置') }),
|
|
387
|
+
btn('用当前会话默认模型', { disabled: !!busy }, function () { setModelField('provider', ''); setModelField('model', '') }),
|
|
388
|
+
hint('留空 provider / model = 用你正在对话的那个模型(' + ((snap.model && snap.model.provider) || '?') + '/' + ((snap.model && snap.model.model) || '?') + ',来源 ' + ((snap.model && snap.model.from) || '?') + ')')),
|
|
389
|
+
|
|
390
|
+
el('div', { className: 'rtr-grid' },
|
|
391
|
+
card('撰写模型', '报告由它写', [
|
|
392
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, 'provider'),
|
|
393
|
+
el('input', { className: 'rtr-in', value: (draft.model && draft.model.provider) || '', placeholder: '留空 = 当前会话默认(例如 deepseek-official)', onChange: function (e) { setModelField('provider', e.target.value) } })),
|
|
394
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, 'model'),
|
|
395
|
+
el('input', { className: 'rtr-in', value: (draft.model && draft.model.model) || '', placeholder: '留空 = 当前会话默认(例如 deepseek-flash)', onChange: function (e) { setModelField('model', e.target.value) } })),
|
|
396
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, 'maxTokens'),
|
|
397
|
+
el('input', { className: 'rtr-in rtr-in-xs', type: 'number', value: draft.maxTokens, onChange: function (e) { setField('maxTokens', Number(e.target.value)) } })),
|
|
398
|
+
]),
|
|
399
|
+
|
|
400
|
+
card('证据预算', '喂给模型多少材料', [
|
|
401
|
+
el('div', { className: 'rtr-grid2' },
|
|
402
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '最多读几个会话'),
|
|
403
|
+
el('input', { className: 'rtr-in', type: 'number', value: draft.sessionLimit, onChange: function (e) { setField('sessionLimit', Number(e.target.value)) } })),
|
|
404
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '每个会话最多取多少字'),
|
|
405
|
+
el('input', { className: 'rtr-in', type: 'number', value: draft.sessionChars, onChange: function (e) { setField('sessionChars', Number(e.target.value)) } })),
|
|
406
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '已确认命中上限'),
|
|
407
|
+
el('input', { className: 'rtr-in', type: 'number', value: draft.maxConfirmed, onChange: function (e) { setField('maxConfirmed', Number(e.target.value)) } })),
|
|
408
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '疑似命中上限'),
|
|
409
|
+
el('input', { className: 'rtr-in', type: 'number', value: draft.maxSuspected, onChange: function (e) { setField('maxSuspected', Number(e.target.value)) } })),
|
|
410
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, 'digest 总上限(字)'),
|
|
411
|
+
el('input', { className: 'rtr-in', type: 'number', value: draft.digestMax, onChange: function (e) { setField('digestMax', Number(e.target.value)) } })),
|
|
412
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '记忆:条数 / 查询数'),
|
|
413
|
+
el('div', { className: 'rtr-row' },
|
|
414
|
+
el('input', { className: 'rtr-in rtr-in-xs', type: 'number', value: draft.memoryTopK, onChange: function (e) { setField('memoryTopK', Number(e.target.value)) } }),
|
|
415
|
+
el('input', { className: 'rtr-in rtr-in-xs', type: 'number', value: draft.memoryQueries, onChange: function (e) { setField('memoryQueries', Number(e.target.value)) } })))),
|
|
416
|
+
]),
|
|
417
|
+
|
|
418
|
+
card('路径', '留空就用默认', [
|
|
419
|
+
hint('相对名由宿主按插件自己的工作区解析(本机实测落在 /home/kali/桌面);写不进去时改成能写的绝对路径。'),
|
|
420
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '攻击矩阵存储'),
|
|
421
|
+
el('input', { className: 'rtr-in', value: draft.matrixStore || '', placeholder: (st && st.matrixPath) || '', onChange: function (e) { setField('matrixStore', e.target.value) } })),
|
|
422
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '报告库文件'),
|
|
423
|
+
el('input', { className: 'rtr-in', value: draft.storePath || '', onChange: function (e) { setField('storePath', e.target.value) } })),
|
|
424
|
+
el('label', { className: 'rtr-f' }, el('span', { className: 'rtr-f-l' }, '导出目录'),
|
|
425
|
+
el('input', { className: 'rtr-in', value: draft.exportDir || '', placeholder: '留空 = 与报告库同目录', onChange: function (e) { setField('exportDir', e.target.value) } })),
|
|
426
|
+
hint('当前落盘:' + ((st && st.storePath) || '(未解析)')),
|
|
427
|
+
]),
|
|
428
|
+
|
|
429
|
+
card('额外要求', '每次生成都会带上', [
|
|
430
|
+
el('textarea', {
|
|
431
|
+
className: 'rtr-ta rtr-ta-sm', value: draft.instruction || '',
|
|
432
|
+
placeholder: '例如:重点写未授权访问链路,每条发现给出修复优先级;不要写攻击载荷细节。',
|
|
433
|
+
onChange: function (e) { setField('instruction', e.target.value) },
|
|
434
|
+
}),
|
|
435
|
+
]))
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function logTab() {
|
|
440
|
+
return el('div', { className: 'rtr-body' },
|
|
441
|
+
el('div', { className: 'rtr-tools' },
|
|
442
|
+
btn('刷新', { disabled: !!busy }, refresh),
|
|
443
|
+
btn('清空日志', { disabled: !!busy }, function () { call('logClear', {}, '清空日志') })),
|
|
444
|
+
el('div', { className: 'rtr-logs' }, (snap.log || []).slice().reverse().map(function (l) {
|
|
445
|
+
return el('div', { key: l.seq, className: 'rtr-ln rtr-ln-' + (l.level || 'info') },
|
|
446
|
+
el('span', { className: 'rtr-lt' }, fmtTime(l.at)),
|
|
447
|
+
el('span', { className: 'rtr-ll' }, l.level || ''),
|
|
448
|
+
el('span', { className: 'rtr-lx' }, l.text))
|
|
449
|
+
})))
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return el('div', { className: 'rtr-root' },
|
|
453
|
+
head(),
|
|
454
|
+
error ? el('div', { className: 'rtr-errbar' }, el('span', null, error), el('button', { className: 'rtr-x', onClick: function () { setError(null) } }, '×')) : null,
|
|
455
|
+
toast ? el('div', { className: 'rtr-ok' }, toast) : null,
|
|
456
|
+
st && st.persistence === 'memory' ? el('div', { className: 'rtr-warn' }, 'fs 服务不可用:报告只存在内存里,重启会丢') : null,
|
|
457
|
+
st && st.lastError && st.persistence === 'error' ? el('div', { className: 'rtr-warn' }, st.lastError) : null,
|
|
458
|
+
|
|
459
|
+
el('div', { className: 'rtr-tabs' }, TABS.map(function (t) {
|
|
460
|
+
return el('button', { key: t[0], className: 'rtr-tab' + (tab === t[0] ? ' rtr-tab-on' : ''), onClick: function () { setTab(t[0]) } }, t[1])
|
|
461
|
+
})),
|
|
462
|
+
|
|
463
|
+
!snap ? el('div', { className: 'rtr-body rtr-dim' }, busy === 'load' ? '加载中…' : '正在读取报告库…') : null,
|
|
464
|
+
snap && tab === 'doc' ? docTab() : null,
|
|
465
|
+
snap && tab === 'evi' ? eviTab() : null,
|
|
466
|
+
snap && tab === 'set' ? setTab_() : null,
|
|
467
|
+
snap && tab === 'log' ? logTab() : null
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 侧边栏图标:一份带批注点的文档
|
|
472
|
+
function Glyph(props) {
|
|
473
|
+
const size = props && props.size ? props.size : 16
|
|
474
|
+
const active = props && props.active
|
|
475
|
+
const c = active ? 'var(--dsw-alias-brand-primary)' : 'currentColor'
|
|
476
|
+
return React.createElement('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none' },
|
|
477
|
+
React.createElement('path', { d: 'M6 3.5h8.5L19 8v12.5H6z', stroke: c, strokeWidth: 1.6, strokeLinejoin: 'round' }),
|
|
478
|
+
React.createElement('path', { d: 'M14.2 3.6V8H19', stroke: c, strokeWidth: 1.4, strokeLinejoin: 'round' }),
|
|
479
|
+
React.createElement('path', { d: 'M9 12h6M9 15h6M9 18h3.5', stroke: c, strokeWidth: 1.4, strokeLinecap: 'round' }),
|
|
480
|
+
React.createElement('circle', { cx: 5.2, cy: 12, r: 1.5, fill: c }),
|
|
481
|
+
React.createElement('circle', { cx: 5.2, cy: 16.6, r: 1.5, fill: c })
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── 样式:全部走主题 token ──────────────────────────────────────────────────
|
|
486
|
+
ctx.effect(function () {
|
|
487
|
+
return styles.insert([
|
|
488
|
+
'.rtr-root{padding:14px 16px;display:flex;flex-direction:column;gap:12px;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
489
|
+
'.rtr-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
|
|
490
|
+
'.rtr-brand{font-size:14px;font-weight:600;letter-spacing:.2px}',
|
|
491
|
+
'.rtr-brand-c{color:var(--dsw-alias-brand-primary)}',
|
|
492
|
+
'.rtr-stat{font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
|
|
493
|
+
'.rtr-sp{flex:1 1 auto}',
|
|
494
|
+
'.rtr-dim{color:var(--dsw-alias-label-secondary)}',
|
|
495
|
+
'.rtr-hint{font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
|
|
496
|
+
'.rtr-err{color:var(--dsw-alias-state-error-primary);font-size:12px}',
|
|
497
|
+
'.rtr-ok{color:var(--dsw-alias-state-success-primary);font-size:12px;word-break:break-all}',
|
|
498
|
+
'.rtr-warn{color:var(--dsw-alias-state-warn-primary);font-size:12px}',
|
|
499
|
+
'.rtr-errbar{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;color:var(--dsw-alias-state-error-primary);font-size:12px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px}',
|
|
500
|
+
'.rtr-x{background:transparent;border:none;color:inherit;font-size:16px;line-height:1;cursor:pointer;padding:0 4px}',
|
|
501
|
+
|
|
502
|
+
'.rtr-tabs{display:flex;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
503
|
+
'.rtr-tab{background:transparent;border:none;border-bottom:2px solid transparent;color:var(--dsw-alias-label-secondary);padding:6px 12px;font-size:12.5px;cursor:pointer;font-family:inherit}',
|
|
504
|
+
'.rtr-tab:hover{color:var(--dsw-alias-label-primary)}',
|
|
505
|
+
'.rtr-tab-on{color:var(--dsw-alias-brand-primary);border-bottom-color:var(--dsw-alias-brand-primary);font-weight:600}',
|
|
506
|
+
'.rtr-body{display:flex;flex-direction:column;gap:12px}',
|
|
507
|
+
|
|
508
|
+
'.rtr-tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
|
|
509
|
+
'.rtr-sep{width:1px;height:18px;background:var(--dsw-alias-border-l1)}',
|
|
510
|
+
'.rtr-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
|
|
511
|
+
'.rtr-group{display:flex;align-items:center;gap:6px}',
|
|
512
|
+
'.rtr-group-l{font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
|
|
513
|
+
'.rtr-seg{display:inline-flex}',
|
|
514
|
+
|
|
515
|
+
'.rtr-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}',
|
|
516
|
+
'.rtr-card-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
517
|
+
'.rtr-card-t{font-size:12.5px;font-weight:600}',
|
|
518
|
+
'.rtr-card-s{font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
|
|
519
|
+
'.rtr-card-b{display:flex;flex-direction:column;gap:8px}',
|
|
520
|
+
|
|
521
|
+
'.rtr-btn{height:26px;padding:0 10px;border-radius:7px;font-size:12px;font-family:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
|
|
522
|
+
'.rtr-btn:hover:not(:disabled){border-color:var(--dsw-alias-border-l2)}',
|
|
523
|
+
'.rtr-btn:disabled{opacity:.45;cursor:not-allowed}',
|
|
524
|
+
'.rtr-btn-primary{background:var(--dsw-alias-brand-primary);border-color:transparent;color:#fff}',
|
|
525
|
+
'.rtr-btn-primary:hover:not(:disabled){filter:brightness(1.06)}',
|
|
526
|
+
'.rtr-btn-danger{color:var(--dsw-alias-state-error-primary)}',
|
|
527
|
+
'.rtr-btn-mini{height:22px;padding:0 8px;font-size:11.5px}',
|
|
528
|
+
'.rtr-seg .rtr-btn{border-radius:0;margin-left:-1px}',
|
|
529
|
+
'.rtr-seg .rtr-btn:first-child{border-radius:7px 0 0 7px;margin-left:0}',
|
|
530
|
+
'.rtr-seg .rtr-btn:last-child{border-radius:0 7px 7px 0}',
|
|
531
|
+
|
|
532
|
+
'.rtr-in{height:26px;box-sizing:border-box;width:100%;padding:0 8px;border-radius:7px;font-size:12px;font-family:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1)}',
|
|
533
|
+
'.rtr-in:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}',
|
|
534
|
+
'.rtr-in-xs{width:64px}',
|
|
535
|
+
'.rtr-title{height:32px;font-size:14px;font-weight:600}',
|
|
536
|
+
'.rtr-ta{min-height:120px;box-sizing:border-box;width:100%;padding:8px;border-radius:8px;font-size:12px;font-family:ui-monospace,monospace;line-height:1.6;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);resize:vertical;flex:1 1 auto}',
|
|
537
|
+
'.rtr-ta:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}',
|
|
538
|
+
'.rtr-ta-sm{min-height:74px}',
|
|
539
|
+
|
|
540
|
+
'.rtr-badge{display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:5px;font-size:11.5px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);white-space:nowrap}',
|
|
541
|
+
'.rtr-badge-ok{color:var(--dsw-alias-state-success-primary)}',
|
|
542
|
+
'.rtr-badge-warn{color:var(--dsw-alias-state-warn-primary)}',
|
|
543
|
+
'.rtr-badge-brand{color:var(--dsw-alias-brand-primary)}',
|
|
544
|
+
|
|
545
|
+
'.rtr-chips{display:flex;gap:6px;flex-wrap:wrap}',
|
|
546
|
+
'.rtr-chip{display:flex;flex-direction:column;align-items:flex-start;gap:1px;text-align:left;max-width:280px;padding:5px 10px;border-radius:8px;cursor:pointer;font-family:inherit;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary)}',
|
|
547
|
+
'.rtr-chip:hover:not(:disabled){border-color:var(--dsw-alias-border-l2)}',
|
|
548
|
+
'.rtr-chip-on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}',
|
|
549
|
+
'.rtr-chip-t{font-size:12px;font-weight:600;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
550
|
+
'.rtr-chip-m{font-size:11px;color:var(--dsw-alias-label-secondary)}',
|
|
551
|
+
|
|
552
|
+
'.rtr-edit{display:flex;flex-direction:column;gap:8px}',
|
|
553
|
+
'.rtr-panes{display:grid;grid-template-columns:1fr 1fr;gap:10px;min-height:52vh}',
|
|
554
|
+
'.rtr-pane{display:flex;flex-direction:column;gap:5px;min-width:0}',
|
|
555
|
+
'.rtr-pane-h{display:flex;align-items:baseline;gap:8px;font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
556
|
+
'.rtr-frame{flex:1 1 auto;min-height:46vh;width:100%;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:#fff}',
|
|
557
|
+
'.rtr-frame-empty{padding:10px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
558
|
+
|
|
559
|
+
'.rtr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px;align-items:start}',
|
|
560
|
+
'.rtr-grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px}',
|
|
561
|
+
'.rtr-f{display:flex;flex-direction:column;gap:4px}',
|
|
562
|
+
'.rtr-f-l{font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
|
|
563
|
+
|
|
564
|
+
'.rtr-tbl{width:100%;border-collapse:collapse;font-size:12px}',
|
|
565
|
+
'.rtr-tbl th{text-align:left;font-weight:500;font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
566
|
+
'.rtr-tbl td{padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l1);vertical-align:top}',
|
|
567
|
+
'.rtr-tbl tbody tr:hover td{background:var(--dsw-alias-bg-layer-2)}',
|
|
568
|
+
'.rtr-tbl tbody tr:last-child td{border-bottom:none}',
|
|
569
|
+
|
|
570
|
+
'.rtr-pre{white-space:pre-wrap;word-break:break-word;font-size:11.5px;line-height:1.6;max-height:52vh;overflow:auto;margin:0;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);font-family:ui-monospace,monospace}',
|
|
571
|
+
|
|
572
|
+
'.rtr-logs{display:flex;flex-direction:column;gap:2px;font-size:11.5px;max-height:60vh;overflow:auto}',
|
|
573
|
+
'.rtr-ln{display:flex;gap:8px;align-items:baseline}',
|
|
574
|
+
'.rtr-lt{flex:0 0 auto;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}',
|
|
575
|
+
'.rtr-ll{flex:0 0 auto;min-width:34px;color:var(--dsw-alias-label-secondary)}',
|
|
576
|
+
'.rtr-lx{white-space:pre-wrap;word-break:break-word}',
|
|
577
|
+
'.rtr-ln-err .rtr-lx{color:var(--dsw-alias-state-error-primary)}',
|
|
578
|
+
'.rtr-ln-warn .rtr-lx{color:var(--dsw-alias-state-warn-primary)}',
|
|
579
|
+
'.rtr-ln-ok .rtr-lx{color:var(--dsw-alias-state-success-primary)}',
|
|
580
|
+
].join('\n'))
|
|
581
|
+
}, 'redteam-report: styles')
|
|
582
|
+
|
|
583
|
+
ctx.effect(function () {
|
|
584
|
+
return slots.inject('sidebar.panellist', function () {
|
|
585
|
+
return slots.register({ name: 'sidebar.panellist', id: PANEL_KEY, order: 60, label: '红队报告' }, Glyph)
|
|
586
|
+
})
|
|
587
|
+
}, 'redteam-report: panel button')
|
|
588
|
+
|
|
589
|
+
ctx.effect(function () {
|
|
590
|
+
return slots.inject('main', function () {
|
|
591
|
+
return slots.register({ name: 'main', key: PANEL_KEY }, Panel)
|
|
592
|
+
})
|
|
593
|
+
}, 'redteam-report: main panel')
|
|
594
|
+
|
|
595
|
+
console.log('[rtreport] redteam-report client half ready; panel =', PANEL_KEY)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
name: 'redteam-report',
|
|
600
|
+
inject: ['slots', 'timer'],
|
|
601
|
+
apply: applyClient
|
|
602
|
+
}
|