@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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +440 -0
- package/lib/index.js +650 -0
- package/lib/kernel/acp-overlay-chat.yml +26 -0
- package/lib/kernel/acp-overlay.yml +22 -0
- package/lib/kernel/acp-preset-shim.mjs +162 -0
- package/lib/kernel/acp.mjs +232 -0
- package/lib/kernel/bridge.mjs +1341 -0
- package/lib/kernel/keeper.mjs +302 -0
- package/lib/kernel/second-brain-contract.txt +7 -0
- package/lib/pairing.js +225 -0
- package/lib/vendor/qr-svg.cjs +58 -0
- package/lib/vendor/qrcode-core/LICENSE +10 -0
- package/lib/vendor/qrcode-core/NOTICE.md +21 -0
- package/lib/vendor/qrcode-core/alignment-pattern.js +83 -0
- package/lib/vendor/qrcode-core/alphanumeric-data.js +59 -0
- package/lib/vendor/qrcode-core/bit-buffer.js +37 -0
- package/lib/vendor/qrcode-core/bit-matrix.js +65 -0
- package/lib/vendor/qrcode-core/byte-data.js +30 -0
- package/lib/vendor/qrcode-core/dijkstrajs.LICENSE +19 -0
- package/lib/vendor/qrcode-core/dijkstrajs.js +165 -0
- package/lib/vendor/qrcode-core/error-correction-code.js +135 -0
- package/lib/vendor/qrcode-core/error-correction-level.js +50 -0
- package/lib/vendor/qrcode-core/finder-pattern.js +22 -0
- package/lib/vendor/qrcode-core/format-info.js +29 -0
- package/lib/vendor/qrcode-core/galois-field.js +69 -0
- package/lib/vendor/qrcode-core/kanji-data.js +54 -0
- package/lib/vendor/qrcode-core/mask-pattern.js +234 -0
- package/lib/vendor/qrcode-core/mode.js +167 -0
- package/lib/vendor/qrcode-core/numeric-data.js +43 -0
- package/lib/vendor/qrcode-core/package.json +8 -0
- package/lib/vendor/qrcode-core/polynomial.js +62 -0
- package/lib/vendor/qrcode-core/qrcode.js +495 -0
- package/lib/vendor/qrcode-core/reed-solomon-encoder.js +56 -0
- package/lib/vendor/qrcode-core/regex.js +31 -0
- package/lib/vendor/qrcode-core/segments.js +330 -0
- package/lib/vendor/qrcode-core/utils.js +63 -0
- package/lib/vendor/qrcode-core/version-check.js +9 -0
- package/lib/vendor/qrcode-core/version.js +163 -0
- package/package.json +40 -0
- package/scripts/build-client.mjs +35 -0
- 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
|
+
});
|