@zmainer/dsh-wx-bridge 1.0.8

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/client.js +440 -0
  5. package/lib/index.js +650 -0
  6. package/lib/kernel/acp-overlay-chat.yml +26 -0
  7. package/lib/kernel/acp-overlay.yml +22 -0
  8. package/lib/kernel/acp-preset-shim.mjs +162 -0
  9. package/lib/kernel/acp.mjs +232 -0
  10. package/lib/kernel/bridge.mjs +1341 -0
  11. package/lib/kernel/keeper.mjs +302 -0
  12. package/lib/kernel/second-brain-contract.txt +7 -0
  13. package/lib/pairing.js +225 -0
  14. package/lib/vendor/qr-svg.cjs +58 -0
  15. package/lib/vendor/qrcode-core/LICENSE +10 -0
  16. package/lib/vendor/qrcode-core/NOTICE.md +21 -0
  17. package/lib/vendor/qrcode-core/alignment-pattern.js +83 -0
  18. package/lib/vendor/qrcode-core/alphanumeric-data.js +59 -0
  19. package/lib/vendor/qrcode-core/bit-buffer.js +37 -0
  20. package/lib/vendor/qrcode-core/bit-matrix.js +65 -0
  21. package/lib/vendor/qrcode-core/byte-data.js +30 -0
  22. package/lib/vendor/qrcode-core/dijkstrajs.LICENSE +19 -0
  23. package/lib/vendor/qrcode-core/dijkstrajs.js +165 -0
  24. package/lib/vendor/qrcode-core/error-correction-code.js +135 -0
  25. package/lib/vendor/qrcode-core/error-correction-level.js +50 -0
  26. package/lib/vendor/qrcode-core/finder-pattern.js +22 -0
  27. package/lib/vendor/qrcode-core/format-info.js +29 -0
  28. package/lib/vendor/qrcode-core/galois-field.js +69 -0
  29. package/lib/vendor/qrcode-core/kanji-data.js +54 -0
  30. package/lib/vendor/qrcode-core/mask-pattern.js +234 -0
  31. package/lib/vendor/qrcode-core/mode.js +167 -0
  32. package/lib/vendor/qrcode-core/numeric-data.js +43 -0
  33. package/lib/vendor/qrcode-core/package.json +8 -0
  34. package/lib/vendor/qrcode-core/polynomial.js +62 -0
  35. package/lib/vendor/qrcode-core/qrcode.js +495 -0
  36. package/lib/vendor/qrcode-core/reed-solomon-encoder.js +56 -0
  37. package/lib/vendor/qrcode-core/regex.js +31 -0
  38. package/lib/vendor/qrcode-core/segments.js +330 -0
  39. package/lib/vendor/qrcode-core/utils.js +63 -0
  40. package/lib/vendor/qrcode-core/version-check.js +9 -0
  41. package/lib/vendor/qrcode-core/version.js +163 -0
  42. package/package.json +40 -0
  43. package/scripts/build-client.mjs +35 -0
  44. package/src/client.js +427 -0
package/lib/client.js ADDED
@@ -0,0 +1,440 @@
1
+ /* @zmainer/dsh-wx-bridge client bundle — generated by scripts/build-client.mjs */
2
+ window.__ModuleLoader__.load({
3
+ id: "@zmainer/dsh-wx-bridge",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ // DSH 客户端半:设置 → 微信连接(React 组件;factory 内可用 require)
8
+ const name = 'wxbridge'
9
+ const inject = ['slots', 'connection']
10
+
11
+ function apply(ctx) {
12
+ const slots = ctx && ctx.slots
13
+ if (!slots || typeof slots.inject !== 'function') return
14
+ slots.inject('settings.section', () => slots.register(
15
+ { name: 'settings.section', id: 'wxbridge', order: 31, label: '微信连接' },
16
+ Panel
17
+ ))
18
+ }
19
+
20
+ const STATUS_TEXT = {
21
+ wait: '等待手机扫码…',
22
+ scaned: '已扫码,正在确认…',
23
+ need_verifycode: '需要手机微信上显示的数字配对码',
24
+ scaned_but_redirect: '已扫码,正在切换网关…',
25
+ binded_redirect: '该 ClawBot 已绑定(如本机没有凭据,请先在原绑定端解除)',
26
+ expired: '二维码已过期,正在刷新…',
27
+ confirmed: '✅ 配对成功',
28
+ }
29
+
30
+ /** 微信品牌绿(固定色,深浅皮肤下都成立)。 */
31
+ const WX = '#07C160'
32
+ const WX_DARK = '#06AD56'
33
+
34
+ /**
35
+ * 原生控件的皮肤适配。
36
+ * 坑(2026-09-21 用户实测):暗色皮肤下原生 <select>/<input> 的**弹出层**会退回系统白底,
37
+ * 而文字用 `color: inherit` 继承了皮肤的浅色 → 白底白字、看不到内容。
38
+ * 对策:① 控件给**不透明**底色(弹出层无法合成半透明);② 同步给 `color-scheme`;
39
+ * ③ `option` 也逐项上色兜底。底色按当前皮肤推导(body 背景亮度 + 提亮)。
40
+ */
41
+ function controlSkin() {
42
+ let dark = true
43
+ let base = null
44
+ try {
45
+ const root = document.documentElement
46
+ const cs = String(getComputedStyle(root).colorScheme || '') + ' ' + String(getComputedStyle(document.body).colorScheme || '')
47
+ const m = String(getComputedStyle(document.body).backgroundColor || '').match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/)
48
+ if (m) {
49
+ const r = Number(m[1]), g = Number(m[2]), b = Number(m[3])
50
+ dark = (r * 0.299 + g * 0.587 + b * 0.114) < 128
51
+ base = [r, g, b]
52
+ }
53
+ if (/dark/i.test(cs) && !/light/i.test(cs)) dark = true
54
+ else if (/light/i.test(cs) && !/dark/i.test(cs)) dark = false
55
+ else if (base === null && window.matchMedia) dark = window.matchMedia('(prefers-color-scheme: dark)').matches
56
+ } catch {}
57
+ const lift = (v, d) => Math.max(0, Math.min(255, v + d))
58
+ const bg = base
59
+ ? 'rgb(' + (dark ? base.map((v) => lift(v, 16)).join(',') : '255,255,255') + ')'
60
+ : (dark ? '#2b3040' : '#ffffff')
61
+ const fg = dark ? '#e8eaf1' : '#1b1f27'
62
+ return {
63
+ dark,
64
+ control: { colorScheme: dark ? 'dark' : 'light', background: bg, color: fg, border: '1px solid rgba(127,127,127,.45)' },
65
+ option: { background: bg, color: fg },
66
+ card: { background: dark ? 'rgba(255,255,255,.045)' : 'rgba(0,0,0,.028)', border: '1px solid ' + (dark ? 'rgba(255,255,255,.09)' : 'rgba(0,0,0,.07)') },
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 微信风格图标:两只叠在一起的聊天气泡(大泡带 3 点、小泡带 2 点)。
72
+ * 纯 SVG 画,不依赖任何外部资源;`sep` 是两泡之间的分隔描边色(取所在卡片底色)。
73
+ */
74
+ function wxBubbles(size, fill, dot, sep) {
75
+ const R = require('react')
76
+ const s = (n) => String(n)
77
+ return R.createElement('svg', { width: size, height: size, viewBox: '0 0 64 64', style: { display: 'block' } },
78
+ R.createElement('ellipse', { cx: 26, cy: 27, rx: 20, ry: 17, fill }),
79
+ R.createElement('circle', { cx: 19, cy: 25, r: 2.4, fill: dot }),
80
+ R.createElement('circle', { cx: 26, cy: 25, r: 2.4, fill: dot }),
81
+ R.createElement('circle', { cx: 33, cy: 25, r: 2.4, fill: dot }),
82
+ R.createElement('ellipse', { cx: 45, cy: 42, rx: 15, ry: 13, fill, stroke: sep || 'transparent', strokeWidth: s(3) }),
83
+ R.createElement('circle', { cx: 40, cy: 41, r: 1.9, fill: dot }),
84
+ R.createElement('circle', { cx: 50, cy: 41, r: 1.9, fill: dot }),
85
+ )
86
+ }
87
+
88
+ /** 注入一次样式(keyframes / hover 这些内联样式表达不了)。 */
89
+ function useWxStyle() {
90
+ const R = require('react')
91
+ R.useEffect(() => {
92
+ try {
93
+ if (document.getElementById('wxbridge-style')) return
94
+ const el = document.createElement('style')
95
+ el.id = 'wxbridge-style'
96
+ el.textContent = [
97
+ '@keyframes wx-pulse{0%{box-shadow:0 0 0 0 rgba(7,193,96,.55)}70%{box-shadow:0 0 0 7px rgba(7,193,96,0)}100%{box-shadow:0 0 0 0 rgba(7,193,96,0)}}',
98
+ '.wxbridge-card{transition:transform .12s ease}',
99
+ '.wxbridge-btn{transition:background .15s ease,border-color .15s ease,transform .1s ease}',
100
+ '.wxbridge-btn:hover{filter:brightness(1.08)}',
101
+ '.wxbridge-btn:active{transform:translateY(1px)}',
102
+ '.wxbridge-bubble{position:relative}',
103
+ '.wxbridge-bubble:after{content:"";position:absolute;left:-5px;top:12px;width:0;height:0;border:6px solid transparent;border-right-color:currentColor;opacity:.001}',
104
+ '.wxbridge-logrow:hover{background:rgba(127,127,127,.08)}',
105
+ ].join('')
106
+ document.head.appendChild(el)
107
+ } catch {}
108
+ }, [])
109
+ }
110
+
111
+ function Panel() {
112
+ const R = require('react')
113
+ const [j, setJ] = R.useState(null)
114
+ const [pair, setPair] = R.useState(null)
115
+ const [err, setErr] = R.useState('')
116
+ const [busy, setBusy] = R.useState('')
117
+ const [note, setNote] = R.useState('')
118
+ const [code, setCode] = R.useState('')
119
+ const [log, setLog] = R.useState([])
120
+ const [showRaw, setShowRaw] = R.useState(false)
121
+ const skin = controlSkin()
122
+ const [presets, setPresets] = R.useState([])
123
+ const [presetSel, setPresetSel] = R.useState('')
124
+ const [presetState, setPresetState] = R.useState(null)
125
+ useWxStyle()
126
+
127
+ R.useEffect(() => {
128
+ let alive = true
129
+ const paint = async () => {
130
+ try {
131
+ const [sr, lr] = await Promise.all([
132
+ fetch('/wxbridge/status', { cache: 'no-store' }),
133
+ fetch('/wxbridge/log', { cache: 'no-store' }),
134
+ ])
135
+ const d = await sr.json()
136
+ const lg = await lr.json()
137
+ if (!alive) return
138
+ setJ(d); setErr(''); setLog(lg.actions || [])
139
+ } catch (e) { if (alive) setErr(String(e && e.message ? e.message : e)) }
140
+ }
141
+ paint()
142
+ const t = setInterval(paint, 2000)
143
+ return () => { alive = false; clearInterval(t) }
144
+ }, [])
145
+
146
+ R.useEffect(() => {
147
+ let alive = true
148
+ fetch('/wxbridge/info', { cache: 'no-store' })
149
+ .then((r) => r.json())
150
+ .then((d) => { if (alive) setPair(d) })
151
+ .catch(() => {})
152
+ return () => { alive = false }
153
+ }, [])
154
+
155
+ const loadPresets = () => fetch('/wxbridge/presets', { cache: 'no-store' })
156
+ .then((r) => r.json())
157
+ .then((d) => { setPresetState(d); setPresets(d.presets || []); setPresetSel(typeof d.selected === 'string' ? d.selected : '') })
158
+ .catch(() => {})
159
+
160
+ R.useEffect(() => { let alive = true; loadPresets().then(() => {}); return () => { alive = false } }, [])
161
+
162
+ const savePreset = async (want) => {
163
+ setBusy('preset')
164
+ try {
165
+ const r = await fetch('/wxbridge/preset', {
166
+ method: 'POST', headers: { 'content-type': 'application/json' },
167
+ body: JSON.stringify({ preset: want }),
168
+ })
169
+ const d = await r.json()
170
+ setPresetSel(typeof d.preset === 'string' ? d.preset : '')
171
+ setNote('已保存手机通道预设:' + (d.preset ? d.preset : '(不指定 · 跟随 DSH 默认预设)') + ' —— 下一条微信任务即生效')
172
+ } catch (e) { setNote('保存失败:' + String(e && e.message ? e.message : e)) } finally { setBusy('') }
173
+ }
174
+
175
+ const pairingActive = !!(pair && pair.pairing && pair.pairing.active)
176
+ const pairingStatus = (pair && pair.pairing && pair.pairing.status) || ''
177
+
178
+ R.useEffect(() => {
179
+ if (!pairingActive || pairingStatus === 'confirmed') return
180
+ let alive = true
181
+ const t = setInterval(async () => {
182
+ try {
183
+ const r = await fetch('/wxbridge/qr/status', { cache: 'no-store' })
184
+ const d = await r.json()
185
+ if (alive && d && d.pairing) setPair((p) => ({ ...(p || {}), pairing: d.pairing }))
186
+ } catch {}
187
+ }, 2500)
188
+ return () => { alive = false; clearInterval(t) }
189
+ }, [pairingActive, pairingStatus])
190
+
191
+ const readAll = async () => {
192
+ try {
193
+ const [sr, lr] = await Promise.all([
194
+ fetch('/wxbridge/status', { cache: 'no-store' }),
195
+ fetch('/wxbridge/log', { cache: 'no-store' }),
196
+ ])
197
+ setJ(await sr.json())
198
+ const lg = await lr.json()
199
+ setLog(lg.actions || [])
200
+ } catch {}
201
+ }
202
+
203
+ const call = async (path, body) => {
204
+ setBusy(path); setNote('')
205
+ try {
206
+ const t0 = Date.now()
207
+ const r = await fetch('/wxbridge/' + path, {
208
+ method: 'POST',
209
+ headers: body ? { 'content-type': 'application/json' } : undefined,
210
+ body: body ? JSON.stringify(body) : undefined,
211
+ })
212
+ const d = await r.json()
213
+ if (d.pairing) setPair((p) => ({ ...(p || {}), pairing: d.pairing }))
214
+ setNote(path + ' → ' + JSON.stringify(d) + '(' + (Date.now() - t0) + 'ms)')
215
+ if (d.pid && d.pid > 0 && (path === 'start' || path === 'restart')) {
216
+ // 启动后回读桥自己的输出:起不来时面板要能直接看到原因(以前 stdio 被丢弃,只剩一个 pid)
217
+ setTimeout(async () => {
218
+ try {
219
+ const lr = await fetch('/wxbridge/bridge-log?lines=25', { cache: 'no-store' })
220
+ const ld = await lr.json()
221
+ setNote((n) => n + '\n--- 桥的启动输出 ---\n' + String(ld.log || '').slice(-1200))
222
+ } catch {}
223
+ }, 1500)
224
+ }
225
+ // 立刻回读,并连拍几次:桥的启动/停止有几百毫秒延迟,只读一次会看不到变化
226
+ await readAll()
227
+ setTimeout(readAll, 400)
228
+ setTimeout(readAll, 1500)
229
+ setTimeout(readAll, 3000)
230
+ } catch (e) { setNote(path + ' 失败:' + String(e && e.message ? e.message : e)) } finally { setBusy('') }
231
+ }
232
+
233
+ const b = (j && j.bridge) || {}
234
+ const stoppedBy = b.manualStop && b.manualStop.at
235
+ const tone = b.pidOk ? (b.fresh ? 'ok' : 'warn') : (stoppedBy ? 'off' : 'bad')
236
+ const TONE = { ok: WX, warn: '#f1c40f', off: '#7f8c8d', bad: '#e74c3c' }
237
+ const dot = TONE[tone]
238
+ const state = b.pidOk
239
+ ? (b.fresh ? '运行中' : '心跳陈旧')
240
+ : (stoppedBy ? '已人工停止' : '未运行')
241
+ const stateHint = b.pidOk ? (b.fresh ? '桥在正常轮询微信消息' : '桥在跑,但心跳已过期') : (stoppedBy ? '自检不会自动拉起,点「启动」恢复' : '点「启动」把桥拉起来')
242
+
243
+ const dim = { color: skin.dark ? '#9aa4b8' : '#6b7280', fontSize: '12px', margin: '0 0 6px 0' }
244
+ const subtle = '1px solid ' + (skin.dark ? 'rgba(255,255,255,.09)' : 'rgba(0,0,0,.07)')
245
+ const mutedBg = skin.dark ? 'rgba(255,255,255,.05)' : 'rgba(0,0,0,.03)'
246
+
247
+ /** 按钮:primary=微信绿实底;danger=红;默认=描边。 */
248
+ const btn = (label, path, primary, body, kind) => R.createElement('button', {
249
+ className: 'wxbridge-btn', key: path, onClick: () => call(path, body), disabled: !!busy,
250
+ style: {
251
+ marginRight: '8px', marginBottom: '6px', padding: '6px 14px', borderRadius: '999px', cursor: busy ? 'default' : 'pointer',
252
+ fontSize: '12px', fontWeight: 500, lineHeight: 1.3,
253
+ border: kind === 'danger' ? '1px solid rgba(231,76,60,.5)' : (primary ? '1px solid ' + WX : subtle),
254
+ background: kind === 'danger' ? 'rgba(231,76,60,.14)' : (primary ? WX : mutedBg),
255
+ color: kind === 'danger' ? (skin.dark ? '#ff9d92' : '#c0392b') : (primary ? '#fff' : 'inherit'),
256
+ opacity: busy && busy !== path ? 0.6 : 1,
257
+ },
258
+ }, busy === path ? '处理中…' : label)
259
+
260
+ /** 小统计卡。 */
261
+ const stat = (label, value, icon) => R.createElement('div', {
262
+ key: label,
263
+ style: { flex: '1 1 92px', minWidth: '92px', padding: '8px 10px', borderRadius: '10px', background: mutedBg, border: subtle },
264
+ },
265
+ R.createElement('div', { style: { fontSize: '11px', color: dim.color, marginBottom: '2px' } }, icon + ' ' + label),
266
+ R.createElement('div', { style: { fontSize: '15px', fontWeight: 600, letterSpacing: '.2px' } }, String(value)),
267
+ )
268
+
269
+ const rows = [
270
+ ['桥状态', state + '(' + (b.phase || 'n/a') + ')'],
271
+ ['进程', b.pid ? ('pid ' + b.pid + '|心跳 ' + (b.ageSec === null ? 'n/a' : b.ageSec + 's 前')) : '无'],
272
+ ['数据目录', b.dataDir || 'n/a'],
273
+ ['默认工作区', b.cwd || 'n/a'],
274
+ ]
275
+
276
+ const pg = (pair && pair.pairing) || null
277
+ const qrNode = pg && pg.active ? R.createElement('div', {
278
+ style: { margin: '10px 0', borderRadius: '12px', overflow: 'hidden', border: subtle },
279
+ },
280
+ R.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 12px', background: WX, color: '#fff', fontSize: '12px', fontWeight: 500 } },
281
+ wxBubbles(18, '#fff', WX, WX),
282
+ R.createElement('span', null, '扫码配对 · ' + (STATUS_TEXT[pg.status] || pg.status || '…'))),
283
+ R.createElement('div', { style: { padding: '12px', display: 'flex', gap: '14px', flexWrap: 'wrap', alignItems: 'flex-start' } },
284
+ pg.qrDataUrl
285
+ ? R.createElement('div', {
286
+ style: { padding: '8px', background: '#fff', borderRadius: '10px', boxShadow: '0 2px 10px rgba(0,0,0,.18)', border: '3px solid ' + WX },
287
+ }, R.createElement('img', { src: pg.qrDataUrl, width: 196, height: 196, alt: '微信扫码配对', style: { display: 'block' } }))
288
+ : R.createElement('p', { style: dim }, '(本机 qrcode 渲染不可用,请用下面的备用链接)'),
289
+ R.createElement('div', { style: { flex: '1 1 180px', minWidth: '180px' } },
290
+ R.createElement('p', { style: { margin: '0 0 6px 0', fontSize: '12px' } }, '用手机「微信 → 扫一扫」扫描左侧二维码'),
291
+ pg.qrContent ? R.createElement('p', { style: { ...dim, wordBreak: 'break-all' } }, '备用链接:' + pg.qrContent) : null,
292
+ pg.status === 'need_verifycode' ? R.createElement('div', { style: { marginTop: '8px' } },
293
+ R.createElement('input', {
294
+ value: code, onChange: (e) => setCode(e.target.value), placeholder: '手机微信显示的数字配对码',
295
+ style: { ...skin.control, padding: '5px 10px', borderRadius: '8px', marginRight: '8px', width: '160px', fontSize: '12px' },
296
+ }),
297
+ R.createElement('button', {
298
+ className: 'wxbridge-btn', onClick: () => call('qr/verify', { code }),
299
+ style: { padding: '6px 12px', borderRadius: '999px', border: '1px solid ' + WX, background: WX, color: '#fff', fontSize: '12px', cursor: 'pointer' },
300
+ }, '提交配对码')
301
+ ) : null,
302
+ pg.error ? R.createElement('p', { style: { ...dim, color: '#e67e22' } }, '提示:' + pg.error) : null,
303
+ pg.status === 'confirmed' && pg.confirmed
304
+ ? R.createElement('p', { style: dim }, '账号 ' + pg.confirmed.accountId + '|用户 ' + pg.confirmed.userId +
305
+ '|凭据已写入 ' + pg.confirmed.credentialFile + '(重启桥后生效)')
306
+ : null,
307
+ R.createElement('div', { style: { marginTop: '10px' } }, btn('停止配对', 'qr/stop'))
308
+ )
309
+ )
310
+ ) : null
311
+
312
+ return R.createElement('div', { style: { padding: '0 0 6px 0' } },
313
+ // ── 头卡:微信绿 + 气泡 logo + 状态胶囊 ──────────────────────────
314
+ R.createElement('div', {
315
+ style: {
316
+ borderRadius: '14px', overflow: 'hidden', marginBottom: '12px',
317
+ background: 'linear-gradient(135deg,' + WX + ' 0%,#0aa86a 58%,#0b8f63 100%)',
318
+ boxShadow: '0 6px 18px rgba(7,193,96,.22)',
319
+ },
320
+ },
321
+ R.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '14px 16px' } },
322
+ R.createElement('div', {
323
+ style: { width: '42px', height: '42px', borderRadius: '12px', background: 'rgba(255,255,255,.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 42px' },
324
+ }, wxBubbles(30, '#fff', WX, WX)),
325
+ R.createElement('div', { style: { flex: 1, minWidth: 0 } },
326
+ R.createElement('div', { style: { color: '#fff', fontSize: '16px', fontWeight: 600, letterSpacing: '.3px' } }, '微信连接'),
327
+ R.createElement('div', { style: { color: 'rgba(255,255,255,.85)', fontSize: '12px', marginTop: '2px' } },
328
+ '手机微信 ↔ 本机 DSH · wxbridge')),
329
+ R.createElement('div', {
330
+ style: { display: 'flex', alignItems: 'center', gap: '7px', padding: '5px 12px', borderRadius: '999px', background: 'rgba(255,255,255,.16)', color: '#fff', fontSize: '12px', flex: '0 0 auto' },
331
+ },
332
+ R.createElement('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: dot, boxShadow: '0 0 0 0 rgba(255,255,255,.5)', animation: tone === 'ok' ? 'wx-pulse 2s infinite' : 'none' } }),
333
+ state)
334
+ ),
335
+ R.createElement('div', { style: { padding: '0 16px 12px 16px', color: 'rgba(255,255,255,.82)', fontSize: '12px' } },
336
+ err ? ('宿主路由读取失败:' + err) : stateHint)
337
+ ),
338
+
339
+ // ── 数据卡 ──────────────────────────────────────────────────────
340
+ R.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px' } },
341
+ stat('轮询', (b.polls === null || b.polls === undefined) ? 'n/a' : b.polls, '📡'),
342
+ stat('会话', (b.peers || 0) + (b.allowedUsers ? ' / ' + b.allowedUsers + ' 人' : ''), '👥'),
343
+ stat('任务', (b.running || 0) + ' / ' + (b.tasks || 0), '🧩'),
344
+ stat('心跳', b.ageSec === null || b.ageSec === undefined ? 'n/a' : b.ageSec + 's', '💚'),
345
+ ),
346
+
347
+ // ── 明细表 ─────────────────────────────────────────────────────
348
+ R.createElement('div', { style: { ...skin.card, borderRadius: '12px', padding: '10px 12px', marginBottom: '10px' } },
349
+ R.createElement('table', { style: { borderCollapse: 'collapse', fontSize: '12px', width: '100%' } },
350
+ R.createElement('tbody', null, rows.map((r) => R.createElement('tr', { key: r[0] },
351
+ R.createElement('td', { style: { padding: '3px 12px 3px 0', color: dim.color, verticalAlign: 'top', whiteSpace: 'nowrap' } }, r[0]),
352
+ R.createElement('td', { style: { padding: '3px 0', wordBreak: 'break-all' } }, String(r[1]))
353
+ )))) ),
354
+
355
+ // ── 操作按钮 ────────────────────────────────────────────────────
356
+ R.createElement('div', { style: { marginBottom: '8px' } },
357
+ btn('启动', 'start', !b.pidOk), btn('停止', 'stop', false, null, 'danger'), btn('重启', 'restart'),
358
+ btn('体检', 'tick'), btn('扫码配对', 'qr/start', true), btn('归组未分组会话', 'scan')),
359
+ R.createElement('p', { style: { ...dim, margin: '0 0 10px 0' } },
360
+ '提示:在手机微信里给机器人发任何文本即可驱动本机;发 /help 看指令(/预设、/model、/sessions、/new、/ws、/status、/task、/cancel)。'),
361
+
362
+ qrNode,
363
+
364
+ // ── 备用登记:微信气泡样式 ───────────────────────────────────────
365
+ pair && pair.token ? R.createElement('div', { style: { marginBottom: '12px' } },
366
+ R.createElement('p', { style: dim }, '备用登记方式(无法扫码时):在手机微信里给机器人发送下面这条'),
367
+ R.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end' } },
368
+ R.createElement('div', {
369
+ style: {
370
+ maxWidth: '86%', padding: '9px 12px', borderRadius: '12px 12px 4px 12px', background: WX, color: '#fff',
371
+ fontSize: '12px', fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Consolas,monospace', wordBreak: 'break-all',
372
+ boxShadow: '0 2px 8px rgba(7,193,96,.22)',
373
+ },
374
+ }, pair.token + ' /help'))
375
+ ) : null,
376
+
377
+ // ── 通道预设 ───────────────────────────────────────────────────
378
+ R.createElement('div', { style: { ...skin.card, borderRadius: '12px', padding: '12px', marginTop: '4px' } },
379
+ R.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '6px' } },
380
+ wxBubbles(16, WX, skin.dark ? '#0d1220' : '#ffffff', 'transparent'),
381
+ R.createElement('span', { style: { fontSize: '13px', fontWeight: 600 } }, '手机通道预设'),
382
+ R.createElement('span', { style: { fontSize: '11px', color: dim.color } }, '(决定手机那条通道用哪套工具 / 提示词 / 技能)')),
383
+ R.createElement('select', {
384
+ value: presetSel, onChange: (e) => setPresetSel(e.target.value),
385
+ style: { ...skin.control, width: '100%', boxSizing: 'border-box', padding: '7px 10px', borderRadius: '8px', fontSize: '12px' },
386
+ }, [R.createElement('option', { key: '__none', value: '', style: skin.option }, '(不指定 · 跟随 DSH 默认预设)')].concat(
387
+ presets.map((pr) => R.createElement('option', { key: pr.id, value: pr.id, style: skin.option },
388
+ pr.name + '(' + pr.id + ')' + (pr.description ? ' · ' + String(pr.description).slice(0, 28) : ''))),
389
+ )),
390
+ R.createElement('div', { style: { marginTop: '8px', display: 'flex', alignItems: 'center' } },
391
+ R.createElement('button', {
392
+ className: 'wxbridge-btn', onClick: () => savePreset(presetSel), disabled: !!busy,
393
+ style: {
394
+ marginRight: '10px', padding: '6px 14px', borderRadius: '999px', fontSize: '12px', fontWeight: 500,
395
+ cursor: busy ? 'default' : 'pointer', border: '1px solid ' + WX, background: WX, color: '#fff',
396
+ },
397
+ }, busy === 'preset' ? '处理中…' : '保存预设'),
398
+ R.createElement('a', {
399
+ href: '#', onClick: (e) => { e.preventDefault(); loadPresets() },
400
+ style: { marginRight: '10px', fontSize: '12px', color: WX, textDecoration: 'none' },
401
+ }, '刷新列表'),
402
+ R.createElement('span', { style: dim },
403
+ presets.length ? ('当前:' + (presetSel || '默认') + '(' + presets.length + ' 个可选)') : '未读到预设名册')
404
+ )
405
+ ),
406
+
407
+ note ? R.createElement('pre', { style: { margin: '10px 0 0 0', padding: '9px 11px', borderRadius: '10px', background: mutedBg, border: subtle, fontSize: '11px', overflow: 'auto', maxHeight: '160px' } }, note) : null,
408
+
409
+ // ── 操作日志 ───────────────────────────────────────────────────
410
+ R.createElement('div', { style: { marginTop: '12px' } },
411
+ R.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: '6px' } },
412
+ R.createElement('span', { style: { fontSize: '12px', color: dim.color } }, '操作日志(宿主半记录,最近 ' + log.length + ' 条)'),
413
+ R.createElement('a', { href: '#', onClick: (e) => { e.preventDefault(); readAll() }, style: { marginLeft: '8px', fontSize: '12px', color: WX, textDecoration: 'none' } }, '刷新')),
414
+ log.length === 0
415
+ ? R.createElement('p', { style: dim }, '暂无记录——点一下上面的按钮试试。')
416
+ : R.createElement('div', { style: { maxHeight: '200px', overflow: 'auto', border: subtle, borderRadius: '10px' } },
417
+ log.map((a, i) => R.createElement('div', {
418
+ key: i, className: 'wxbridge-logrow',
419
+ style: { display: 'flex', gap: '8px', padding: '5px 9px', fontSize: '11px', borderBottom: i === log.length - 1 ? 'none' : subtle, alignItems: 'baseline' },
420
+ },
421
+ R.createElement('span', { style: { color: dim.color, flex: '0 0 58px', fontVariantNumeric: 'tabular-nums' } }, String(a.at || '').slice(11, 19)),
422
+ R.createElement('span', { style: { color: a.ok ? WX : '#e74c3c', flex: '0 0 84px' } }, (a.ok ? '● ' : '● ') + String(a.action || '')),
423
+ R.createElement('span', { style: { wordBreak: 'break-all', opacity: .9 } }, String(a.detail || ''))
424
+ ))
425
+ )
426
+ ),
427
+
428
+ R.createElement('p', { style: { ...dim, marginTop: '10px' } },
429
+ '插件 v' + ((j && j.version) || '?') + '|数据来自 /wxbridge/status(宿主半实时读桥的心跳与状态文件)',
430
+ R.createElement('a', { href: '#', onClick: (e) => { e.preventDefault(); setShowRaw(!showRaw) }, style: { marginLeft: '8px', color: WX, textDecoration: 'none' } }, showRaw ? '收起原始 JSON' : '查看原始 JSON')),
431
+ showRaw ? R.createElement('pre', { style: { margin: '8px 0 0 0', padding: '10px', borderRadius: '10px', background: mutedBg, fontSize: '11px', overflow: 'auto', maxHeight: '200px' } }, JSON.stringify(j || {}, null, 1)) : null
432
+ )
433
+ }
434
+
435
+ exports.name = name;
436
+ exports.inject = inject;
437
+ exports.apply = apply;
438
+ return module.exports;
439
+ }
440
+ });