@zaofan/dsh-qqbot 0.6.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 +308 -0
- package/README_EN.md +273 -0
- package/client/qqbot-settings.js +1463 -0
- package/cordis.patch.yml +36 -0
- package/dist/api/group-admin.d.ts +112 -0
- package/dist/api/group-admin.d.ts.map +1 -0
- package/dist/api/group-admin.js +179 -0
- package/dist/api/group-admin.js.map +1 -0
- package/dist/channel-tools.d.ts +14 -0
- package/dist/channel-tools.d.ts.map +1 -0
- package/dist/channel-tools.js +880 -0
- package/dist/channel-tools.js.map +1 -0
- package/dist/commands/help.d.ts +11 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/index.d.ts +13 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +29 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/misc.d.ts +14 -0
- package/dist/commands/misc.d.ts.map +1 -0
- package/dist/commands/misc.js +39 -0
- package/dist/commands/misc.js.map +1 -0
- package/dist/commands/model.d.ts +7 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +58 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/session.d.ts +10 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +26 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +24 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/config.d.ts +205 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +203 -0
- package/dist/config.js.map +1 -0
- package/dist/features/chat-ledger.d.ts +53 -0
- package/dist/features/chat-ledger.d.ts.map +1 -0
- package/dist/features/chat-ledger.js +185 -0
- package/dist/features/chat-ledger.js.map +1 -0
- package/dist/features/face-tags.d.ts +17 -0
- package/dist/features/face-tags.d.ts.map +1 -0
- package/dist/features/face-tags.js +79 -0
- package/dist/features/face-tags.js.map +1 -0
- package/dist/features/group-join-request.d.ts +65 -0
- package/dist/features/group-join-request.d.ts.map +1 -0
- package/dist/features/group-join-request.js +190 -0
- package/dist/features/group-join-request.js.map +1 -0
- package/dist/features/history-store.d.ts +8 -0
- package/dist/features/history-store.d.ts.map +1 -0
- package/dist/features/history-store.js +24 -0
- package/dist/features/history-store.js.map +1 -0
- package/dist/features/qq-approval.d.ts +78 -0
- package/dist/features/qq-approval.d.ts.map +1 -0
- package/dist/features/qq-approval.js +165 -0
- package/dist/features/qq-approval.js.map +1 -0
- package/dist/features/schedule-store.d.ts +62 -0
- package/dist/features/schedule-store.d.ts.map +1 -0
- package/dist/features/schedule-store.js +253 -0
- package/dist/features/schedule-store.js.map +1 -0
- package/dist/features/scheduler.d.ts +9 -0
- package/dist/features/scheduler.d.ts.map +1 -0
- package/dist/features/scheduler.js +146 -0
- package/dist/features/scheduler.js.map +1 -0
- package/dist/features/sticker-gate.d.ts +90 -0
- package/dist/features/sticker-gate.d.ts.map +1 -0
- package/dist/features/sticker-gate.js +391 -0
- package/dist/features/sticker-gate.js.map +1 -0
- package/dist/features/sticker-store.d.ts +158 -0
- package/dist/features/sticker-store.d.ts.map +1 -0
- package/dist/features/sticker-store.js +676 -0
- package/dist/features/sticker-store.js.map +1 -0
- package/dist/features/sticker-tagger.d.ts +10 -0
- package/dist/features/sticker-tagger.d.ts.map +1 -0
- package/dist/features/sticker-tagger.js +139 -0
- package/dist/features/sticker-tagger.js.map +1 -0
- package/dist/gateway/bootstrap.d.ts +12 -0
- package/dist/gateway/bootstrap.d.ts.map +1 -0
- package/dist/gateway/bootstrap.js +346 -0
- package/dist/gateway/bootstrap.js.map +1 -0
- package/dist/gateway/debounce.d.ts +40 -0
- package/dist/gateway/debounce.d.ts.map +1 -0
- package/dist/gateway/debounce.js +262 -0
- package/dist/gateway/debounce.js.map +1 -0
- package/dist/gateway/index.d.ts +3 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/middleware-setup.d.ts +12 -0
- package/dist/gateway/middleware-setup.d.ts.map +1 -0
- package/dist/gateway/middleware-setup.js +156 -0
- package/dist/gateway/middleware-setup.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/attachment.d.ts +11 -0
- package/dist/middleware/attachment.d.ts.map +1 -0
- package/dist/middleware/attachment.js +17 -0
- package/dist/middleware/attachment.js.map +1 -0
- package/dist/middleware/media-history.d.ts +30 -0
- package/dist/middleware/media-history.d.ts.map +1 -0
- package/dist/middleware/media-history.js +54 -0
- package/dist/middleware/media-history.js.map +1 -0
- package/dist/middleware/sticker-capture.d.ts +17 -0
- package/dist/middleware/sticker-capture.d.ts.map +1 -0
- package/dist/middleware/sticker-capture.js +97 -0
- package/dist/middleware/sticker-capture.js.map +1 -0
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +9 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/model-resolver.d.ts +88 -0
- package/dist/model/model-resolver.d.ts.map +1 -0
- package/dist/model/model-resolver.js +151 -0
- package/dist/model/model-resolver.js.map +1 -0
- package/dist/model/prefs-store.d.ts +24 -0
- package/dist/model/prefs-store.d.ts.map +1 -0
- package/dist/model/prefs-store.js +126 -0
- package/dist/model/prefs-store.js.map +1 -0
- package/dist/model/settings-reader.d.ts +19 -0
- package/dist/model/settings-reader.d.ts.map +1 -0
- package/dist/model/settings-reader.js +76 -0
- package/dist/model/settings-reader.js.map +1 -0
- package/dist/model/types.d.ts +15 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +5 -0
- package/dist/model/types.js.map +1 -0
- package/dist/session/idle-evictor.d.ts +18 -0
- package/dist/session/idle-evictor.d.ts.map +1 -0
- package/dist/session/idle-evictor.js +31 -0
- package/dist/session/idle-evictor.js.map +1 -0
- package/dist/session/index.d.ts +9 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/session-manager.d.ts +97 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +624 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/session/types.d.ts +130 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/session/workspace-attach.d.ts +22 -0
- package/dist/session/workspace-attach.d.ts.map +1 -0
- package/dist/session/workspace-attach.js +21 -0
- package/dist/session/workspace-attach.js.map +1 -0
- package/dist/setup.d.ts +25 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +198 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/index.d.ts +7 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/scope.d.ts +14 -0
- package/dist/shared/scope.d.ts.map +1 -0
- package/dist/shared/scope.js +12 -0
- package/dist/shared/scope.js.map +1 -0
- package/dist/shared/send-helper.d.ts +11 -0
- package/dist/shared/send-helper.d.ts.map +1 -0
- package/dist/shared/send-helper.js +13 -0
- package/dist/shared/send-helper.js.map +1 -0
- package/dist/shared/utils.d.ts +28 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +143 -0
- package/dist/shared/utils.js.map +1 -0
- package/dist/transport/attachment.d.ts +18 -0
- package/dist/transport/attachment.d.ts.map +1 -0
- package/dist/transport/attachment.js +106 -0
- package/dist/transport/attachment.js.map +1 -0
- package/dist/transport/chunker.d.ts +14 -0
- package/dist/transport/chunker.d.ts.map +1 -0
- package/dist/transport/chunker.js +78 -0
- package/dist/transport/chunker.js.map +1 -0
- package/dist/transport/events.d.ts +80 -0
- package/dist/transport/events.d.ts.map +1 -0
- package/dist/transport/events.js +62 -0
- package/dist/transport/events.js.map +1 -0
- package/dist/transport/inbound.d.ts +8 -0
- package/dist/transport/inbound.d.ts.map +1 -0
- package/dist/transport/inbound.js +292 -0
- package/dist/transport/inbound.js.map +1 -0
- package/dist/transport/index.d.ts +10 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +10 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/inject-rules.d.ts +24 -0
- package/dist/transport/inject-rules.d.ts.map +1 -0
- package/dist/transport/inject-rules.js +72 -0
- package/dist/transport/inject-rules.js.map +1 -0
- package/dist/transport/outbound-buffer.d.ts +56 -0
- package/dist/transport/outbound-buffer.d.ts.map +1 -0
- package/dist/transport/outbound-buffer.js +131 -0
- package/dist/transport/outbound-buffer.js.map +1 -0
- package/dist/transport/outbound.d.ts +30 -0
- package/dist/transport/outbound.d.ts.map +1 -0
- package/dist/transport/outbound.js +139 -0
- package/dist/transport/outbound.js.map +1 -0
- package/dist/transport/rich-media.d.ts +41 -0
- package/dist/transport/rich-media.d.ts.map +1 -0
- package/dist/transport/rich-media.js +104 -0
- package/dist/transport/rich-media.js.map +1 -0
- package/dist/transport/streaming-writer.d.ts +47 -0
- package/dist/transport/streaming-writer.d.ts.map +1 -0
- package/dist/transport/streaming-writer.js +110 -0
- package/dist/transport/streaming-writer.js.map +1 -0
- package/dist/transport/tool-presenter.d.ts +38 -0
- package/dist/transport/tool-presenter.d.ts.map +1 -0
- package/dist/transport/tool-presenter.js +121 -0
- package/dist/transport/tool-presenter.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +106 -0
- package/settings-host.js +927 -0
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
/* qqbot-settings — browser half (hand-written __ModuleLoader__ bundle, no build step).
|
|
2
|
+
* 在设置页新增「QQ 机器人 (im-qqbot)」整页: 读/写 im-qqbot settings ns,
|
|
3
|
+
* 数据经 host 半边同源路由 /api/qqbot-settings/{read,update}。
|
|
4
|
+
*/
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: '@zaofan/dsh-qqbot',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
var React = require('react')
|
|
11
|
+
var h = React.createElement
|
|
12
|
+
var useState = React.useState
|
|
13
|
+
var useEffect = React.useEffect
|
|
14
|
+
|
|
15
|
+
var READ = '/api/qqbot-settings/read'
|
|
16
|
+
var UPDATE = '/api/qqbot-settings/update'
|
|
17
|
+
|
|
18
|
+
var labelStyle = { display: 'block', margin: '6px 0', fontSize: 13 }
|
|
19
|
+
var inputStyle = { marginLeft: 8, padding: '2px 6px', border: '1px solid #8884', borderRadius: 4, background: 'transparent', color: 'inherit', width: 110 }
|
|
20
|
+
var wideStyle = { ...inputStyle, width: 260 }
|
|
21
|
+
var fullStyle = { ...inputStyle, width: '92%', display: 'block', margin: '4px 0 8px' }
|
|
22
|
+
var boxStyle = { border: '1px solid #e5e6eb', borderRadius: 8, padding: '10px 14px 12px', margin: '8px 0', background: '#fff' }
|
|
23
|
+
var sectionTitle = { fontWeight: 600, margin: '10px 0 4px', fontSize: 14, color: '#1f2329' }
|
|
24
|
+
|
|
25
|
+
// ── 迷你设计系统(2026-09-05 专家团定稿; 深色柔和适配宿主, 不刺眼) ──
|
|
26
|
+
// 宿主 dsh web 为深色主题 → 卡片用半透明深色、文字浅色、主色用低饱和紫(贴合 #9775fa 系)
|
|
27
|
+
var T = {
|
|
28
|
+
bg: 'transparent', card: '#ffffff', cardHi: '#fafafa', cardSolid: '#f4f5f8', border: '#e2e5ea', borderLight: '#eef0f3',
|
|
29
|
+
text: '#1f2329', text2: '#4e5560', text3: '#8a919c',
|
|
30
|
+
primary: '#7c6cf0', primary2: '#6b5ae6', primaryBg: '#7c6cf01a', primaryBorder: '#7c6cf04d',
|
|
31
|
+
danger: '#e5484d', dangerBg: '#e5484d14',
|
|
32
|
+
success: '#1f9e4f', successBg: '#1f9e4f14',
|
|
33
|
+
warn: '#d97706', warnBg: '#d9770614',
|
|
34
|
+
radius: 6, radiusLg: 10,
|
|
35
|
+
shadow: 'none',
|
|
36
|
+
fs: 13, fsSm: 12, fsTitle: 14,
|
|
37
|
+
}
|
|
38
|
+
var pageBg = { background: T.bg, borderRadius: T.radiusLg, padding: '12px 16px', minHeight: 300 }
|
|
39
|
+
var card = { background: T.card, border: '1px solid ' + T.border, borderRadius: T.radius }
|
|
40
|
+
function btnStyle(variant, size) {
|
|
41
|
+
var base = {
|
|
42
|
+
border: '1px solid ' + T.border, borderRadius: T.radius, cursor: 'pointer', font: 'inherit',
|
|
43
|
+
padding: size === 'sm' ? '2px 10px' : '4px 14px', fontSize: size === 'sm' ? 12 : 13, lineHeight: '20px', whiteSpace: 'nowrap',
|
|
44
|
+
transition: 'background .15s, border-color .15s, color .15s',
|
|
45
|
+
}
|
|
46
|
+
if (variant === 'primary') return { ...base, background: T.primaryBg, color: T.primary, borderColor: T.primaryBorder, fontWeight: 600 }
|
|
47
|
+
if (variant === 'danger') return { ...base, background: T.dangerBg, color: T.danger, borderColor: '#f7656044', fontWeight: 600 }
|
|
48
|
+
if (variant === 'ghost') return { ...base, background: 'transparent', color: T.text2 }
|
|
49
|
+
return { ...base, background: T.card, color: T.text }
|
|
50
|
+
}
|
|
51
|
+
function SButton(props) {
|
|
52
|
+
return h('button', { className: 'qqs-btn', style: btnStyle(props.variant, props.size), disabled: props.disabled, onClick: props.onClick }, props.children)
|
|
53
|
+
}
|
|
54
|
+
// 状态条: tone=info/danger/success/warn; 深色柔底 + 左圆标; action=可选右侧行动按钮
|
|
55
|
+
function StatusBar(props) {
|
|
56
|
+
var m = { info: [T.primaryBg, T.primary], danger: [T.dangerBg, T.danger], success: [T.successBg, T.success], warn: [T.warnBg, T.warn] }[props.tone || 'info'] || [T.primaryBg, T.primary]
|
|
57
|
+
var icon = props.tone === 'danger' ? '!' : props.tone === 'success' ? '✓' : props.tone === 'warn' ? '⚠' : 'i'
|
|
58
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, background: m[0], border: '1px solid ' + m[1] + '44', borderRadius: T.radius, padding: '6px 12px', margin: '8px 0', fontSize: T.fsSm } },
|
|
59
|
+
h('span', { style: { flexShrink: 0, width: 18, height: 18, borderRadius: '50%', background: m[1], color: '#fff', textAlign: 'center', lineHeight: '18px', fontSize: 11, fontWeight: 700 } }, icon),
|
|
60
|
+
h('span', { style: { flex: 1, color: m[1] } }, props.children),
|
|
61
|
+
props.action ? h(SButton, { variant: props.tone === 'danger' ? 'danger' : 'primary', size: 'sm', onClick: props.action.onClick }, props.action.label) : null)
|
|
62
|
+
}
|
|
63
|
+
// 空状态
|
|
64
|
+
function Empty(props) {
|
|
65
|
+
return h('div', { style: { padding: '36px 0', textAlign: 'center', color: T.text3 } },
|
|
66
|
+
h('div', { style: { fontSize: 34, marginBottom: 8, opacity: .5 } }, props.icon || '🕳'),
|
|
67
|
+
h('div', { style: { fontSize: T.fsSm } }, props.text || '暂无数据'))
|
|
68
|
+
}
|
|
69
|
+
// 简易表格: cols=[{key,title,width?,render?}], rows=[]; 空态走 Empty (深色适配)
|
|
70
|
+
function DataTable(props) {
|
|
71
|
+
var cols = props.cols || []
|
|
72
|
+
var rows = props.rows || []
|
|
73
|
+
if (rows.length === 0) return h(Empty, { text: props.emptyText || '暂无数据' })
|
|
74
|
+
return h('div', { style: { border: '1px solid #dde1e7', borderRadius: T.radius, overflow: 'hidden', background: T.card } },
|
|
75
|
+
h('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: T.fsSm } },
|
|
76
|
+
h('thead', null, h('tr', { style: { background: T.cardHi } },
|
|
77
|
+
cols.map(function (c) { return h('th', { key: c.key, style: { padding: '8px 10px', textAlign: 'left', fontWeight: 600, color: T.text2, borderBottom: '1px solid #dde1e7', width: c.width || undefined } }, c.title) }))),
|
|
78
|
+
h('tbody', null, rows.map(function (r, i) {
|
|
79
|
+
return h('tr', { key: i, style: { background: i % 2 ? T.card : T.cardHi } },
|
|
80
|
+
cols.map(function (c) {
|
|
81
|
+
var v = r[c.key]
|
|
82
|
+
var cell = c.render ? c.render(v, r) : (v === null || v === undefined ? '' : String(v))
|
|
83
|
+
return h('td', { key: c.key, style: { padding: '8px 10px', borderBottom: '1px solid #eef0f3', color: T.text } }, cell)
|
|
84
|
+
}))
|
|
85
|
+
}))))
|
|
86
|
+
}
|
|
87
|
+
// 横排 TabBar: items=[{key,label,badge?}]
|
|
88
|
+
function TabBar(props) {
|
|
89
|
+
return h('div', { style: { display: 'flex', gap: 2, borderBottom: '1px solid ' + T.border, marginBottom: 4 } },
|
|
90
|
+
(props.items || []).map(function (it) {
|
|
91
|
+
var on = it.key === props.value
|
|
92
|
+
return h('div', { key: it.key, onClick: function () { props.onChange && props.onChange(it.key) }, style: { cursor: 'pointer', padding: '8px 14px', fontSize: T.fsSm, color: on ? T.primary : T.text2, fontWeight: on ? 600 : 400, borderBottom: on ? '2px solid ' + T.primary : '2px solid transparent', marginBottom: -1, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
93
|
+
it.label,
|
|
94
|
+
it.badge > 0 ? h('span', { style: { background: T.danger, color: '#fff', borderRadius: 9, padding: '0 6px', fontSize: 11, lineHeight: '16px', fontWeight: 700 } }, it.badge) : null)
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 可搜索选择器(选聊天对象用): 点击弹出候选浮层, 输入即过滤; 选中回调 onPick({scope,id,name})
|
|
99
|
+
function TargetPicker(props) {
|
|
100
|
+
var [open, setOpen] = useState(false)
|
|
101
|
+
var [q, setQ] = useState('')
|
|
102
|
+
var val = props.value || {}
|
|
103
|
+
function cands() {
|
|
104
|
+
var all = props.options || []
|
|
105
|
+
var t = q.trim().toLowerCase()
|
|
106
|
+
if (!t) return all
|
|
107
|
+
return all.filter(function (c) { return (c.name || '').toLowerCase().indexOf(t) >= 0 || String(c.id).toLowerCase().indexOf(t) >= 0 })
|
|
108
|
+
}
|
|
109
|
+
var gs = cands().filter(function (c) { return c.scope === 'group' })
|
|
110
|
+
var cs = cands().filter(function (c) { return c.scope === 'c2c' })
|
|
111
|
+
var label = val.name || (val.targetId ? String(val.targetId).slice(0, 8) : '')
|
|
112
|
+
var sec = { fontSize: 10, color: T.text3, padding: '3px 6px 1px', fontWeight: 600 }
|
|
113
|
+
var item = { cursor: 'pointer', padding: '4px 8px', borderRadius: 4, fontSize: T.fsSm, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
|
114
|
+
function pick(c) { setOpen(false); setQ(''); props.onPick && props.onPick({ scope: c.scope, id: c.id, name: c.name || '' }) }
|
|
115
|
+
return h('div', { style: { position: 'relative', flex: 1, minWidth: 0 } },
|
|
116
|
+
h('div', { onClick: function () { setOpen(!open); setQ('') }, style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 5, padding: '2px 8px', minHeight: 22, border: '1px solid ' + T.border, borderRadius: T.radius, background: T.card, fontSize: T.fsSm, color: label ? T.text : T.text3, userSelect: 'none' } },
|
|
117
|
+
h('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, label ? '『' + label + '』' : '(未选,点此搜索)'),
|
|
118
|
+
h('span', { style: { fontSize: 10, color: T.text3 } }, '▾')),
|
|
119
|
+
open ? h('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 60, minWidth: 250, marginTop: 2, background: T.card, border: '1px solid ' + T.border, borderRadius: T.radius, boxShadow: '0 6px 18px rgba(0,0,0,.12)', padding: 6 } },
|
|
120
|
+
h('input', { className: 'qqs-inp', style: { width: '100%', boxSizing: 'border-box', padding: '3px 8px', marginBottom: 4, fontSize: T.fsSm }, autoFocus: true, placeholder: '搜群名 / 昵称 / ID…', value: q, onChange: function (e) { setQ(e.target.value) } }),
|
|
121
|
+
gs.length + cs.length === 0 ? h('div', { style: { padding: '8px 6px', color: T.text3, fontSize: T.fsSm, textAlign: 'center' } }, '没有匹配的聊天') : null,
|
|
122
|
+
gs.length ? h('div', { style: sec }, '群') : null,
|
|
123
|
+
gs.map(function (c) { return h('div', { key: 'g:' + c.id, style: item, onMouseDown: function (ev) { ev.preventDefault(); pick(c) }, onMouseEnter: function (e) { e.currentTarget.style.background = T.cardHi }, onMouseLeave: function (e) { e.currentTarget.style.background = 'transparent' } }, '👥 ' + (c.name || c.id)) }),
|
|
124
|
+
cs.length ? h('div', { style: sec }, '私聊') : null,
|
|
125
|
+
cs.map(function (c) { return h('div', { key: 'c:' + c.id, style: item, onMouseDown: function (ev) { ev.preventDefault(); pick(c) }, onMouseEnter: function (e) { e.currentTarget.style.background = T.cardHi }, onMouseLeave: function (e) { e.currentTarget.style.background = 'transparent' } }, '💬 ' + (c.name || c.id)) })) : null)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 群聊守则"恢复默认"用: 与主包 dsh-qqbot config.ts 的 DEFAULT_GROUP_PROMPT 保持一致。
|
|
129
|
+
// ⚠️ 若改主包默认文本, 必须同步改这里(唯一同步点)。
|
|
130
|
+
var DEFAULT_GROUP_PROMPT = [
|
|
131
|
+
'【表情包礼仪】你有一座本地表情包库(群友存的图,带标签)。想用表情包回应时先调 list_stickers 搜库,命中后再用 send_media 发出,别发没把握的图。',
|
|
132
|
+
'只在群里正热闹、情绪正浓时,才在回复末尾附 0~1 张:满屏哈哈/笑死(爆笑)、大家"确实/我也是"(共鸣)、有人抛梗你能接住(接梗)、有人当面夸你(被夸)、轻度吐槽可以接治愈/滑稽图。',
|
|
133
|
+
'绝不发:冷场没人接话、正事/技术问答/找资源、吵架互怼中、对方难过或聊严肃事、私聊。',
|
|
134
|
+
'出手前自检,缺一不发:①库里有 9 成贴切的图;②发出来群友会心一笑;③这轮没发过、今天这群没刷过图。',
|
|
135
|
+
'把图删掉话照样完整;一次最多一张;发离谱/冒犯/色气/羞辱人的图=人设崩塌,永远不许。',
|
|
136
|
+
].join('\n')
|
|
137
|
+
|
|
138
|
+
function nv(v) { return typeof v === 'number' && isFinite(v) ? v : 0 }
|
|
139
|
+
function sv(v) { return typeof v === 'string' ? v : '' }
|
|
140
|
+
|
|
141
|
+
function BoolRow(props) {
|
|
142
|
+
return h('label', { style: labelStyle },
|
|
143
|
+
props.label,
|
|
144
|
+
h('input', { className: 'qqs-inp',
|
|
145
|
+
type: 'checkbox', style: inputStyle, checked: !!props.value,
|
|
146
|
+
onChange: (e) => props.onChange(e.target.checked),
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
function NumRow(props) {
|
|
150
|
+
return h('label', { style: labelStyle },
|
|
151
|
+
props.label,
|
|
152
|
+
h('input', { className: 'qqs-inp',
|
|
153
|
+
type: 'number', style: inputStyle, value: nv(props.value),
|
|
154
|
+
onChange: (e) => props.onChange(Number(e.target.value)),
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
function StrRow(props) {
|
|
158
|
+
return h('label', { style: labelStyle },
|
|
159
|
+
props.label,
|
|
160
|
+
h('input', { className: 'qqs-inp', style: props.wide ? wideStyle : inputStyle, value: sv(props.value), onChange: (e) => props.onChange(e.target.value) }))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function RuleEditor(props) {
|
|
164
|
+
var r = props.rule
|
|
165
|
+
function upd(patch) { props.onChange({ ...r, ...patch }) }
|
|
166
|
+
function condUpd(patch) { upd({ conditions: { ...r.conditions, ...patch } }) }
|
|
167
|
+
return h('div', { style: boxStyle },
|
|
168
|
+
h('label', { style: labelStyle },
|
|
169
|
+
h('input', { className: 'qqs-inp',
|
|
170
|
+
type: 'checkbox', checked: !!r.enabled,
|
|
171
|
+
onChange: (e) => upd({ enabled: e.target.checked }),
|
|
172
|
+
}), ' 启用这条提醒 ' + ((r.name || r.id) + (r.name && r.name !== r.id ? ' (' + r.id + ')' : ''))),
|
|
173
|
+
h('label', { style: labelStyle },
|
|
174
|
+
h('input', { className: 'qqs-inp',
|
|
175
|
+
type: 'checkbox', checked: !!r.conditions.hasImage,
|
|
176
|
+
onChange: (e) => condUpd({ hasImage: e.target.checked }),
|
|
177
|
+
}), ' 消息里带图片 ',
|
|
178
|
+
h('input', { className: 'qqs-inp',
|
|
179
|
+
type: 'checkbox', checked: !!r.conditions.hasLink,
|
|
180
|
+
onChange: (e) => condUpd({ hasLink: e.target.checked }),
|
|
181
|
+
}), ' 消息里带链接 '),
|
|
182
|
+
h('label', { style: labelStyle }, ' 按固定写法匹配文字(高手用,留空=不用): ',
|
|
183
|
+
h('input', { className: 'qqs-inp', style: wideStyle, value: sv(r.conditions.contentRegex), placeholder: '如 ^早安', onChange: (e) => condUpd({ contentRegex: e.target.value }) })),
|
|
184
|
+
h('label', { style: labelStyle }, ' 出现这些词就触发(逗号分隔): ',
|
|
185
|
+
h('input', { className: 'qqs-inp', style: wideStyle, value: (r.conditions.contentKeywords || []).join(','), onChange: (e) => condUpd({ contentKeywords: e.target.value.split(/[,,]/).map(function (s) { return s.trim() }).filter(Boolean) }) })),
|
|
186
|
+
h('label', { style: labelStyle }, ' 多个条件怎么算: ',
|
|
187
|
+
h('select', { style: inputStyle, value: r.conditions.matchScope || 'any', onChange: (e) => condUpd({ matchScope: e.target.value }) },
|
|
188
|
+
h('option', { value: 'any' }, '满足一个就行'),
|
|
189
|
+
h('option', { value: 'all' }, '全部都要满足'))),
|
|
190
|
+
h('div', { style: labelStyle }, '要提醒她的内容:'),
|
|
191
|
+
h('textarea', { className: 'qqs-area',
|
|
192
|
+
style: fullStyle, rows: 2, value: sv(r.prompt),
|
|
193
|
+
placeholder: '例:上方含链接,引用前先核实',
|
|
194
|
+
onChange: (e) => upd({ prompt: e.target.value }),
|
|
195
|
+
}),
|
|
196
|
+
h('button', { className: 'qqs-btn', style: { marginTop: 4 }, onClick: () => props.onRemove(r.id) }, '删除这条提醒'))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function SettingsPage(props) {
|
|
200
|
+
var close = props && props.close
|
|
201
|
+
// 多账号: 当前操作账号的 settings 命名空间(主=im-qqbot 不带参, 其它=实例 id)
|
|
202
|
+
var _acctNs = props && props.acct && props.acct.ns && props.acct.ns !== 'im-qqbot' ? String(props.acct.ns) : ''
|
|
203
|
+
var [cfg, setCfg] = useState(null)
|
|
204
|
+
var [rev, setRev] = useState(undefined)
|
|
205
|
+
var [msg, setMsg] = useState('')
|
|
206
|
+
|
|
207
|
+
function load() {
|
|
208
|
+
setMsg('加载中…')
|
|
209
|
+
fetch(READ + (_acctNs ? '?ns=' + encodeURIComponent(_acctNs) : '')).then(function (r) { return r.json() }).then(function (d) {
|
|
210
|
+
if (d && d.value) {
|
|
211
|
+
var v = d.value
|
|
212
|
+
var base = {
|
|
213
|
+
behavior: v.behavior || {},
|
|
214
|
+
sticker: v.sticker || {},
|
|
215
|
+
injectRules: Array.isArray(v.injectRules) ? v.injectRules : [],
|
|
216
|
+
schedule: v.schedule && Array.isArray(v.schedule.targets) ? v.schedule : { targets: [] },
|
|
217
|
+
// groupPrompt 必须读回来, 否则每次保存都会把它清空成 ''
|
|
218
|
+
// undefined(从未设置)→ 显示默认守则; ''(用户明确清空)→ 保持空(无守则)
|
|
219
|
+
groupPrompt: typeof v.groupPrompt === 'string' ? v.groupPrompt : DEFAULT_GROUP_PROMPT,
|
|
220
|
+
enableApprovals: v.enableApprovals === true,
|
|
221
|
+
approvalTimeoutMs: typeof v.approvalTimeoutMs === 'number' ? v.approvalTimeoutMs : 120000,
|
|
222
|
+
groupAdmin: { enabled: v.groupAdmin && v.groupAdmin.enabled === true, owners: Array.isArray(v.groupAdmin && v.groupAdmin.owners) ? v.groupAdmin.owners : [], manageGroup: v.groupAdmin && typeof v.groupAdmin.manageGroup === 'string' ? v.groupAdmin.manageGroup : '', watchJoinRequests: !!(v.groupAdmin && v.groupAdmin.watchJoinRequests), notifyInGroup: v.groupAdmin && v.groupAdmin.notifyInGroup !== false },
|
|
223
|
+
}
|
|
224
|
+
setCfg(base); setRev(d.revision); setMsg('')
|
|
225
|
+
} else { setMsg('读取失败: ' + JSON.stringify(d)) }
|
|
226
|
+
}).catch(function (e) { setMsg('读取异常: ' + e.message) })
|
|
227
|
+
}
|
|
228
|
+
useEffect(function () { load() }, [])
|
|
229
|
+
|
|
230
|
+
if (!cfg) return h('div', null, h('p', null, msg || '加载中…'))
|
|
231
|
+
|
|
232
|
+
var gates = cfg.sticker.gates || {}
|
|
233
|
+
var dbc = cfg.behavior.debounce || {} // 延迟聚合(后端未设置时缺省回落默认值)
|
|
234
|
+
function setBehavior(p) { setCfg(function (c) { return { ...c, behavior: { ...c.behavior, ...p } } }) }
|
|
235
|
+
function setGates(p) { setCfg(function (c) { return { ...c, sticker: { ...c.sticker, gates: { ...(c.sticker.gates || {}), ...p } } } }) }
|
|
236
|
+
function setSticker(p) { setCfg(function (c) { return { ...c, sticker: { ...(c.sticker || {}), ...p } } }) }
|
|
237
|
+
function setGroupAdmin(p) { setCfg(function (c) { return { ...c, groupAdmin: { ...(c.groupAdmin || { enabled: false, owners: [] }), ...p } } }) }
|
|
238
|
+
function setRules(list) { setCfg(function (c) { return { ...c, injectRules: list } }) }
|
|
239
|
+
|
|
240
|
+
function addRule() {
|
|
241
|
+
var list = (cfg.injectRules || []).slice()
|
|
242
|
+
list.push({
|
|
243
|
+
id: 'rule-' + Date.now().toString(36),
|
|
244
|
+
name: '',
|
|
245
|
+
enabled: true,
|
|
246
|
+
conditions: { hasImage: false, hasLink: false, contentRegex: '', contentKeywords: [], matchScope: 'any' },
|
|
247
|
+
prompt: '',
|
|
248
|
+
})
|
|
249
|
+
setRules(list)
|
|
250
|
+
}
|
|
251
|
+
function updRule(i, r) { var list = cfg.injectRules.slice(); list[i] = r; setRules(list) }
|
|
252
|
+
function rmRule(id) { setRules(cfg.injectRules.filter(function (r) { return r.id !== id })) }
|
|
253
|
+
|
|
254
|
+
// gpOverride: 传入字符串 = 用该守则保存(恢复默认用); undefined = 用当前编辑框内容
|
|
255
|
+
function doSave(gpOverride) {
|
|
256
|
+
setMsg('保存中…')
|
|
257
|
+
var patch = {
|
|
258
|
+
behavior: cfg.behavior,
|
|
259
|
+
sticker: {
|
|
260
|
+
gates: cfg.sticker.gates,
|
|
261
|
+
autoTagEnabled: cfg.sticker.autoTagEnabled,
|
|
262
|
+
visionCli: cfg.sticker.visionCli,
|
|
263
|
+
},
|
|
264
|
+
injectRules: cfg.injectRules,
|
|
265
|
+
// 定时唤醒(④)已并入「定时任务」页编辑; 这里原样带过不丢即可
|
|
266
|
+
schedule: cfg.schedule && Array.isArray(cfg.schedule.targets) ? cfg.schedule : { targets: [] },
|
|
267
|
+
groupPrompt: typeof gpOverride === 'string' ? gpOverride : (typeof cfg.groupPrompt === 'string' ? cfg.groupPrompt : ''),
|
|
268
|
+
enableApprovals: cfg.enableApprovals === true,
|
|
269
|
+
approvalTimeoutMs: typeof cfg.approvalTimeoutMs === 'number' ? cfg.approvalTimeoutMs : 120000,
|
|
270
|
+
groupAdmin: { enabled: cfg.groupAdmin && cfg.groupAdmin.enabled === true, owners: Array.isArray(cfg.groupAdmin && cfg.groupAdmin.owners) ? cfg.groupAdmin.owners : [], manageGroup: cfg.groupAdmin && typeof cfg.groupAdmin.manageGroup === 'string' ? cfg.groupAdmin.manageGroup : '', watchJoinRequests: !!(cfg.groupAdmin && cfg.groupAdmin.watchJoinRequests), notifyInGroup: cfg.groupAdmin && cfg.groupAdmin.notifyInGroup !== false },
|
|
271
|
+
}
|
|
272
|
+
fetch(UPDATE, {
|
|
273
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({ patch: patch, expectedRevision: rev, ns: _acctNs || undefined }),
|
|
275
|
+
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
276
|
+
if (d && d.value) {
|
|
277
|
+
var v2 = d.value
|
|
278
|
+
setCfg({
|
|
279
|
+
behavior: v2.behavior || {}, sticker: v2.sticker || {},
|
|
280
|
+
injectRules: Array.isArray(v2.injectRules) ? v2.injectRules : [],
|
|
281
|
+
schedule: v2.schedule && Array.isArray(v2.schedule.targets) ? v2.schedule : { targets: [] },
|
|
282
|
+
groupPrompt: typeof v2.groupPrompt === 'string' ? v2.groupPrompt : (typeof cfg.groupPrompt === 'string' ? cfg.groupPrompt : DEFAULT_GROUP_PROMPT),
|
|
283
|
+
enableApprovals: v2.enableApprovals === true,
|
|
284
|
+
approvalTimeoutMs: typeof v2.approvalTimeoutMs === 'number' ? v2.approvalTimeoutMs : 120000,
|
|
285
|
+
groupAdmin: { enabled: v2.groupAdmin && v2.groupAdmin.enabled === true, owners: Array.isArray(v2.groupAdmin && v2.groupAdmin.owners) ? v2.groupAdmin.owners : [], manageGroup: v2.groupAdmin && typeof v2.groupAdmin.manageGroup === 'string' ? v2.groupAdmin.manageGroup : '', watchJoinRequests: !!(v2.groupAdmin && v2.groupAdmin.watchJoinRequests), notifyInGroup: v2.groupAdmin && v2.groupAdmin.notifyInGroup !== false },
|
|
286
|
+
})
|
|
287
|
+
setRev(d.revision); setMsg('已保存 ✓(live 生效)')
|
|
288
|
+
}
|
|
289
|
+
else if (d && d.error) { setMsg('保存失败: ' + d.error) }
|
|
290
|
+
else { setMsg('保存失败(未知响应)') }
|
|
291
|
+
}).catch(function (e) { setMsg('保存异常: ' + e.message) })
|
|
292
|
+
}
|
|
293
|
+
function save() { doSave(undefined) }
|
|
294
|
+
// 恢复默认守则: 直接把"表情包礼仪"默认文本写回并保存(不需手动粘贴)
|
|
295
|
+
function restoreDefault() { doSave(DEFAULT_GROUP_PROMPT) }
|
|
296
|
+
|
|
297
|
+
var gh = gates.bannedGroups || []
|
|
298
|
+
return h('div', { style: { maxWidth: 640 } },
|
|
299
|
+
h('h2', null, 'QQ 机器人设置'),
|
|
300
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '改完点「保存」立刻生效(不用重启)。看不懂的项放着别动就行;想恢复"不限制"就把数字填 0、勾选取消、列表清空。'),
|
|
301
|
+
|
|
302
|
+
h('div', { style: sectionTitle }, '⓪ 群里她要一直记住的守则'),
|
|
303
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '每轮回复都会带上的一段提醒(所有 QQ 群都生效,不碰你的人格设定)。默认是"表情包礼仪";想让她不主动发图,把这里清空即可(留空=没守则,不会自动填回)。'),
|
|
304
|
+
h('textarea', { className: 'qqs-area',
|
|
305
|
+
style: { ...fullStyle, minHeight: 90 },
|
|
306
|
+
value: sv(cfg.groupPrompt),
|
|
307
|
+
onChange: function (e) { setCfg(function (c) { return { ...c, groupPrompt: e.target.value } }) },
|
|
308
|
+
}),
|
|
309
|
+
h('div', { style: { margin: '0 0 6px' } },
|
|
310
|
+
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12 }, onClick: restoreDefault }, '恢复默认守则(表情包礼仪)')),
|
|
311
|
+
|
|
312
|
+
h('div', { style: sectionTitle }, '① 多久回一次消息'),
|
|
313
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '控制她回复的速度。全填 0 = 来一条回一条(不限制)。'),
|
|
314
|
+
h('div', { style: boxStyle },
|
|
315
|
+
NumRow({ label: '群里没人 @ 她时,隔几秒才回一次(0=每条都回)', value: cfg.behavior.freeIntervalSec, onChange: function (v) { setBehavior({ freeIntervalSec: v }) } }),
|
|
316
|
+
NumRow({ label: '有人 @ 她时,两次回复至少隔几秒(0=随叫随到)', value: cfg.behavior.mentionIntervalSec, onChange: function (v) { setBehavior({ mentionIntervalSec: v }) } }),
|
|
317
|
+
NumRow({ label: '私聊里隔几秒回一次(0=不限制)', value: cfg.behavior.directIntervalSec, onChange: function (v) { setBehavior({ directIntervalSec: v }) } }),
|
|
318
|
+
h('div', { style: { fontSize: 12, color: '#666', margin: '10px 0 2px' } }, '延迟聚合(另一套机制,和上面冷却不冲突): 她收到消息先等一小会儿, 把连发的话攒一起综合回, 免得只回第一句。'),
|
|
319
|
+
BoolRow({ label: '开启延迟聚合(不勾=回到来一条回一条)', value: dbc.enabled !== false, onChange: function (v) { setBehavior({ debounce: { ...dbc, enabled: v } }) } }),
|
|
320
|
+
NumRow({ label: '对方停口几秒后她才开口(默认3;0=不停顿)', value: dbc.silenceSec != null ? dbc.silenceSec : 3, onChange: function (v) { setBehavior({ debounce: { ...dbc, silenceSec: v } }) } }),
|
|
321
|
+
NumRow({ label: '攒满几条立即开口,不等对方停(默认10)', value: dbc.maxMsgs != null ? dbc.maxMsgs : 10, onChange: function (v) { setBehavior({ debounce: { ...dbc, maxMsgs: v } }) } }),
|
|
322
|
+
BoolRow({ label: '有人 @ 她时也走延迟(不勾=@到秒回)', value: dbc.mentionDelayed !== false, onChange: function (v) { setBehavior({ debounce: { ...dbc, mentionDelayed: v } }) } })),
|
|
323
|
+
|
|
324
|
+
h('div', { style: sectionTitle }, '② 发表情包的限制(全默认不限制)'),
|
|
325
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '防止她聊天时表情包刷屏;下面两项是"她主动发图"时才用——群里很热闹才发,冷清就憋着。不想管就保持全 0,也别勾总开关。'),
|
|
326
|
+
h('div', { style: boxStyle },
|
|
327
|
+
BoolRow({ label: '开启表情包限制(不勾=完全不限制)', value: gates.enabled, onChange: function (v) { setGates({ enabled: v }) } }),
|
|
328
|
+
NumRow({ label: '她一次回复最多带几张图(0=不限)', value: gates.perTurnMax, onChange: function (v) { setGates({ perTurnMax: v }) } }),
|
|
329
|
+
NumRow({ label: '每隔"一小段时间"最多发几张(0=不限)', value: gates.maxPerWindow, onChange: function (v) { setGates({ maxPerWindow: v }) } }),
|
|
330
|
+
NumRow({ label: '上面说的"一小段时间"是多长(秒,如600=10分钟)', value: gates.perWindowSec, onChange: function (v) { setGates({ perWindowSec: v }) } }),
|
|
331
|
+
NumRow({ label: '每个群一天最多发几张表情包(0=不限)', value: gates.dailyBudgetPerGroup, onChange: function (v) { setGates({ dailyBudgetPerGroup: v }) } }),
|
|
332
|
+
NumRow({ label: '同一张图多久内不许重复发(小时,0=不查)', value: gates.dupTTLHours, onChange: function (v) { setGates({ dupTTLHours: v }) } }),
|
|
333
|
+
NumRow({ label: '看"最近"多长时间的聊天来判热闹(3600=最近1小时)', value: gates.activityWindowSec, onChange: function (v) { setGates({ activityWindowSec: v }) } }),
|
|
334
|
+
NumRow({ label: '最近这段时间群消息达到几条,她才肯主动发图(0=不管冷不冷都发)', value: gates.activityMinMsgs, onChange: function (v) { setGates({ activityMinMsgs: v }) } }),
|
|
335
|
+
StrRow({ label: '禁止发图的群(填群ID,逗号分隔;一般不用填)', value: gh.join(','), wide: true, onChange: function (v) { setGates({ bannedGroups: v.split(/[,,]/).map(function (s) { return s.trim() }).filter(Boolean) }) } })),
|
|
336
|
+
h('div', { style: boxStyle },
|
|
337
|
+
BoolRow({ label: '新图自动后台识图打标(会花视觉额度;不勾=全靠人工手动补)', value: !!cfg.sticker.autoTagEnabled, onChange: function (v) { setSticker({ autoTagEnabled: v }) } }),
|
|
338
|
+
StrRow({ label: '视觉引擎命令(高级;留空自动探测)', value: sv(cfg.sticker.visionCli), wide: true, onChange: function (v) { setSticker({ visionCli: v }) } })),
|
|
339
|
+
|
|
340
|
+
h('div', { style: sectionTitle }, '③ 自定义小提醒(高级)'),
|
|
341
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '当群友发的消息满足下面条件,就偷偷给机器人加一条要照做的提醒。比如:勾上"含链接",提醒写"引用前先核实"。'),
|
|
342
|
+
(cfg.injectRules || []).map(function (r, i) {
|
|
343
|
+
return h(RuleEditor, { key: r.id, rule: r, onChange: function (nr) { updRule(i, nr) }, onRemove: rmRule })
|
|
344
|
+
}),
|
|
345
|
+
h('button', { className: 'qqs-btn', onClick: addRule }, '+ 添加一条提醒'),
|
|
346
|
+
|
|
347
|
+
h('div', { style: sectionTitle }, '④ 定时唤醒'),
|
|
348
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '已合并到「定时任务」页(顶部 tab)一起编辑——到点主动开口的群/人分组,与她答应你的定时提醒,都在那边管理。'),
|
|
349
|
+
|
|
350
|
+
h('div', { style: sectionTitle }, '⑤ QQ 远程审批(在 QQ 里放行 dsh 权限申请)'),
|
|
351
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '机器人的工具要动"工作区外"的东西时,dsh 会申请权限。开启后审批请求直接发到你的 QQ(私聊/群聊看你从哪发起),回 /approve 验证码 放行、/deny 拒绝——只放行这一次,验证码一次性,只有你能批。'),
|
|
352
|
+
h('div', { style: boxStyle },
|
|
353
|
+
BoolRow({ label: '开启 QQ 远程审批(不勾=保持默认审批方式)', value: cfg.enableApprovals === true, onChange: function (v) { setCfg(function (c) { return { ...c, enableApprovals: v } }) } }),
|
|
354
|
+
NumRow({ label: '审批等待秒数(超时自动拒绝;默认120)', value: Math.round((cfg.approvalTimeoutMs || 120000) / 1000), onChange: function (v) { setCfg(function (c) { return { ...c, approvalTimeoutMs: v * 1000 } }) } })),
|
|
355
|
+
|
|
356
|
+
h('div', { style: { ...card, margin: '10px 0', padding: '12px 16px' } },
|
|
357
|
+
h('div', { style: { fontWeight: 700, fontSize: 13, marginBottom: 2 } }, '⚙ 群管理基础设置'),
|
|
358
|
+
h('p', { style: { fontSize: 12, color: T.text3, margin: '0 0 6px' } }, '开启后 QQ 会话里可用群管理工具; 踢人/成员列表等官方未开放能力会在下方 Tab 里提示等待公测。'),
|
|
359
|
+
BoolRow({ label: '开启 QQ 群管理(不勾=群管理工具不可用)', value: cfg.groupAdmin && cfg.groupAdmin.enabled === true, onChange: function (v) { setGroupAdmin({ enabled: v }) } }),
|
|
360
|
+
StrRow({ label: '允许操作的主人 openid(逗号分隔,可留空=不校验)', value: (cfg.groupAdmin && Array.isArray(cfg.groupAdmin.owners) ? cfg.groupAdmin.owners : []).join(','), wide: true, onChange: function (v) { setGroupAdmin({ owners: v.split(/[,,]/).map(function (s) { return s.trim() }).filter(Boolean) }) } }),
|
|
361
|
+
StrRow({ label: '对话内默认管理群 group_openid(web 对话时群工具用它; QQ 群会话自动用当前群)', value: (cfg.groupAdmin && typeof cfg.groupAdmin.manageGroup === 'string' ? cfg.groupAdmin.manageGroup : ''), wide: true, onChange: function (v) { setGroupAdmin({ manageGroup: v.trim() }) } }),
|
|
362
|
+
BoolRow({ label: '实时接收"入群申请"事件并自动提醒(勾选后需重启才生效)', value: !!(cfg.groupAdmin && cfg.groupAdmin.watchJoinRequests), onChange: function (v) { setGroupAdmin({ watchJoinRequests: v }) } }),
|
|
363
|
+
BoolRow({ label: '收到入群申请时在该群内发提醒消息', value: !(cfg.groupAdmin && cfg.groupAdmin.notifyInGroup === false), onChange: function (v) { setGroupAdmin({ notifyInGroup: v }) } })),
|
|
364
|
+
h(GroupAdminPanel, { key: 'ga-' + (_acctNs || 'main'), ns: _acctNs || undefined }),
|
|
365
|
+
|
|
366
|
+
h('div', { style: { margin: '12px 0' } },
|
|
367
|
+
h('button', { className: 'qqs-btn', style: { marginRight: 8 }, onClick: save }, '保存'),
|
|
368
|
+
h('button', { className: 'qqs-btn', onClick: load }, '放弃修改(重新读取)')),
|
|
369
|
+
msg ? h('p', { style: { fontSize: 13, color: '#2f9e44' } }, msg) : null,
|
|
370
|
+
close ? h('button', { className: 'qqs-btn', onClick: close, style: { float: 'right' } }, '完成') : null)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── ⑥ QQ 群管理(单开大卡片; P3+ 设计系统版: 横排 Tab + 状态条 + 表格 + 空状态 + 预留接口位) ──
|
|
374
|
+
// 能力就绪度: 官方未开放位保留(红条+行动), 不置灰糊弄; 业务名词做 Tab 骨架。
|
|
375
|
+
var GROUP_TABS = [
|
|
376
|
+
{ key: 'join', label: '入群审批', open: true },
|
|
377
|
+
{ key: 'mute', label: '禁言', open: true },
|
|
378
|
+
{ key: 'members', label: '成员信息', open: false, gateHuman: '获取群成员列表: 官方尚未开放(内邀中), 等待公测后自动可用' },
|
|
379
|
+
{ key: 'blacklist', label: '黑名单', open: false, gateHuman: '群黑名单: 官方接口尚未开放, 等待公测后自动可用' },
|
|
380
|
+
]
|
|
381
|
+
function GroupAdminPanel(props) {
|
|
382
|
+
var nsq = props && props.ns ? '?ns=' + encodeURIComponent(props.ns) : ''
|
|
383
|
+
var [accts, setAccts] = useState(null)
|
|
384
|
+
var [gid, setGid] = useState('')
|
|
385
|
+
var [tab, setTab] = useState('join')
|
|
386
|
+
var [joins, setJoins] = useState(null)
|
|
387
|
+
var [mute, setMute] = useState(null)
|
|
388
|
+
var [msg, setMsg] = useState('')
|
|
389
|
+
var [busy, setBusy] = useState('')
|
|
390
|
+
var [bindGid, setBindGid] = useState('')
|
|
391
|
+
var [bindName, setBindName] = useState('')
|
|
392
|
+
var [members, setMembers] = useState(null)
|
|
393
|
+
var [muteSecs, setMuteSecs] = useState('60')
|
|
394
|
+
var [muteTarget, setMuteTarget] = useState('')
|
|
395
|
+
|
|
396
|
+
function loadMembers() {
|
|
397
|
+
if (!gid) return
|
|
398
|
+
fetch('/api/qqbot-settings/group/members_local' + nsq + (nsq ? '&' : '?') + 'gid=' + encodeURIComponent(gid)).then(function (r) { return r.json() }).then(function (d) {
|
|
399
|
+
setMembers(d && d.ok ? (d.members || []) : [])
|
|
400
|
+
}).catch(function () { setMembers([]) })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function loadAccounts() {
|
|
404
|
+
fetch('/api/qqbot-settings/group/accounts' + nsq).then(function (r) { return r.json() }).then(function (d) {
|
|
405
|
+
if (d && d.error) { setMsg('群面板: ' + d.error); setAccts([]); return }
|
|
406
|
+
var list = (d && Array.isArray(d.groups) ? d.groups : []).map(function (g) { return { gid: g.gid, name: g.name || '', from: g.from || '' } })
|
|
407
|
+
setAccts(list)
|
|
408
|
+
if (list.length && !gid) setGid(list[0].gid)
|
|
409
|
+
else if (list.length === 0) { setJoins(null); setMute(null) }
|
|
410
|
+
}).catch(function (e) { setMsg('群列表加载失败: ' + e.message) })
|
|
411
|
+
}
|
|
412
|
+
function refresh() {
|
|
413
|
+
if (!gid) return
|
|
414
|
+
if (tab === 'join') {
|
|
415
|
+
fetch('/api/qqbot-settings/group/join_requests' + nsq + (nsq ? '&' : '?') + 'gid=' + encodeURIComponent(gid)).then(function (r) { return r.json() }).then(function (d) {
|
|
416
|
+
if (d && d.ok) setJoins(d); else setMsg((d && d.err && d.err.human) || '申请列表加载失败')
|
|
417
|
+
}).catch(function (e) { setMsg('申请列表加载异常: ' + e.message) })
|
|
418
|
+
} else if (tab === 'mute') {
|
|
419
|
+
fetch('/api/qqbot-settings/group/mute_state' + nsq + (nsq ? '&' : '?') + 'gid=' + encodeURIComponent(gid)).then(function (r) { return r.json() }).then(function (d) {
|
|
420
|
+
if (d && d.ok) setMute(d); else setMsg((d && d.err && d.err.human) || '禁言状态加载失败')
|
|
421
|
+
}).catch(function (e) { setMsg('禁言状态加载异常: ' + e.message) })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
useEffect(function () { loadAccounts() }, [])
|
|
425
|
+
useEffect(function () { refresh(); loadMembers() }, [gid, tab])
|
|
426
|
+
|
|
427
|
+
function doApprove(mid, op) {
|
|
428
|
+
var reason = op === 'decline' ? window.prompt('拒绝理由(可留空)') : '-'
|
|
429
|
+
if (op === 'decline' && reason === null) return
|
|
430
|
+
setBusy('approve-' + mid)
|
|
431
|
+
fetch('/api/qqbot-settings/group/approve', {
|
|
432
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
433
|
+
body: JSON.stringify(Object.assign({ gid: gid, member_openid: mid, op: op, reason: reason || '' }, props.ns ? { ns: props.ns } : {})),
|
|
434
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
435
|
+
setBusy('')
|
|
436
|
+
setMsg((d && d.msg) || (d && d.err && d.err.human) || '审批失败')
|
|
437
|
+
refresh()
|
|
438
|
+
}).catch(function (e) { setBusy(''); setMsg('审批异常: ' + e.message) })
|
|
439
|
+
}
|
|
440
|
+
function doMute(mid, action, seconds) {
|
|
441
|
+
var secs = action === 'mute' ? Math.round(Number(seconds || 600)) : 0
|
|
442
|
+
setBusy('mute-' + mid)
|
|
443
|
+
fetch('/api/qqbot-settings/group/mute', {
|
|
444
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
445
|
+
body: JSON.stringify(Object.assign({ gid: gid, member_openid: mid, action: action, seconds: secs }, props.ns ? { ns: props.ns } : {})),
|
|
446
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
447
|
+
setBusy('')
|
|
448
|
+
setMsg((d && d.msg) || (d && d.err && d.err.human) || '操作失败')
|
|
449
|
+
refresh()
|
|
450
|
+
}).catch(function (e) { setBusy(''); setMsg('禁言异常: ' + e.message) })
|
|
451
|
+
}
|
|
452
|
+
function doBind() {
|
|
453
|
+
var g = bindGid.trim()
|
|
454
|
+
if (!g) { setMsg('先粘贴 group_openid'); return }
|
|
455
|
+
fetch('/api/qqbot-settings/group/bind', {
|
|
456
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
457
|
+
body: JSON.stringify(Object.assign({ gid: g, name: bindName.trim() }, props.ns ? { ns: props.ns } : {})),
|
|
458
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
459
|
+
if (d && d.ok) { setMsg('已登记群 ✓'); setBindGid(''); setBindName(''); loadAccounts() }
|
|
460
|
+
else setMsg((d && d.error) || '绑定失败')
|
|
461
|
+
}).catch(function (e) { setMsg('绑定异常: ' + e.message) })
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var curTabMeta = null
|
|
465
|
+
GROUP_TABS.forEach(function (t) { if (t.key === tab) curTabMeta = t })
|
|
466
|
+
var groupSel = accts === null ? h('span', { style: { fontSize: T.fsSm, color: T.text3 } }, '群列表加载中…')
|
|
467
|
+
: accts.length === 0 ? h('span', { style: { fontSize: T.fsSm, color: T.warn } }, '暂无群(事件累积/下方登记后出现)')
|
|
468
|
+
: h('select', { className: 'qqs-inp', style: { padding: '3px 8px', maxWidth: 320, border: '1px solid ' + T.border, borderRadius: T.radius }, value: gid, onChange: function (e) { setGid(e.target.value); setMsg('') } },
|
|
469
|
+
accts.map(function (a) {
|
|
470
|
+
return h('option', { key: a.gid, value: a.gid }, (a.name ? a.name + ' · ' : '') + a.gid.slice(0, 12) + '…' + (a.from ? ' [' + a.from + ']' : ''))
|
|
471
|
+
}))
|
|
472
|
+
|
|
473
|
+
var joinCols = [
|
|
474
|
+
{ key: 'username', title: '昵称', width: '18%', render: function (v, r) { return h('span', { style: { fontWeight: 600 } }, v || '(未知)') } },
|
|
475
|
+
{ key: 'source', title: '来源', width: '10%', render: function (v, r) { return h('span', { style: { color: T.text2 } }, v || '-') } },
|
|
476
|
+
{ key: 'verify', title: '验证消息', render: function (v, r) { return h('span', { style: { color: T.text2 } }, v || '-') } },
|
|
477
|
+
{ key: 'risk', title: '风险', width: '18%', render: function (v, r) { return v ? h('span', { style: { color: T.danger } }, '⚠ ' + v) : h('span', { style: { color: T.text3 } }, '-') } },
|
|
478
|
+
{ key: 'op', title: '操作', width: '20%', render: function (v, r) {
|
|
479
|
+
var busying = busy === 'approve-' + r.member_openid
|
|
480
|
+
return h('span', { style: { display: 'inline-flex', gap: 6 } },
|
|
481
|
+
h(SButton, { variant: 'primary', size: 'sm', disabled: !!busy, onClick: function () { doApprove(r.member_openid, 'approve') } }, busying ? '处理中…' : '通过'),
|
|
482
|
+
h(SButton, { variant: 'danger', size: 'sm', disabled: !!busy, onClick: function () { doApprove(r.member_openid, 'decline') } }, '拒绝'))
|
|
483
|
+
} },
|
|
484
|
+
]
|
|
485
|
+
var joinRows = ((joins && joins.ok && joins.list) || []).map(function (j) {
|
|
486
|
+
return {
|
|
487
|
+
username: j.username || '(未知昵称)',
|
|
488
|
+
source: j.apply_source === 'invited' ? '被邀请' : '主动申请',
|
|
489
|
+
verify: (j.verify_info && j.verify_info.verify_message) || (j.verify_info && j.verify_info.method) || '',
|
|
490
|
+
risk: j.risk_tips || '',
|
|
491
|
+
member_openid: j.member_openid,
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
var muteMode = (mute && mute.data && mute.data.global_rule && mute.data.global_rule.mode) || 'none'
|
|
496
|
+
var muteCols = [
|
|
497
|
+
{ key: 'username', title: '成员', width: '30%', render: function (v, r) { return h('span', { style: { fontWeight: 600 } }, v || String(r.member_openid).slice(0, 12)) } },
|
|
498
|
+
{ key: 'expire', title: '禁言至', render: function (v) { return h('span', { style: { color: T.text2 } }, v || '?') } },
|
|
499
|
+
{ key: 'op', title: '操作', width: '18%', render: function (v, r) {
|
|
500
|
+
var busying = busy === 'mute-' + r.member_openid
|
|
501
|
+
return h(SButton, { variant: 'ghost', size: 'sm', disabled: !!busy, onClick: function () { doMute(r.member_openid, 'unmute') } }, busying ? '处理中…' : '解除')
|
|
502
|
+
} },
|
|
503
|
+
]
|
|
504
|
+
var muteRows = (((mute && mute.data && mute.data.members) || [])).map(function (m) { return { username: m.username || '', member_openid: m.member_openid, expire: m.mute_expire_at || '' } })
|
|
505
|
+
|
|
506
|
+
var body = null
|
|
507
|
+
if (curTabMeta && curTabMeta.open === false) {
|
|
508
|
+
body = h('div', null,
|
|
509
|
+
h(StatusBar, { tone: 'danger', action: { label: '去开放平台申请', onClick: function () { window.open('https://q.qq.com', '_blank') } } }, curTabMeta.gateHuman || '该能力官方尚未开放'),
|
|
510
|
+
h('div', { style: { border: '1px dashed ' + T.border, borderRadius: T.radius, padding: '24px', textAlign: 'center', color: T.text3, fontSize: T.fsSm } },
|
|
511
|
+
'此功能位已预留 —— 官方开放接口后此处自动点亮, 无需等待插件更新'))
|
|
512
|
+
} else if (!gid) {
|
|
513
|
+
body = h(Empty, { icon: '👥', text: '先选择或登记一个群, 再查看/操作' })
|
|
514
|
+
} else if (tab === 'join') {
|
|
515
|
+
body = joins === null ? h(Empty, { icon: '⏳', text: '加载中…' })
|
|
516
|
+
: h('div', null,
|
|
517
|
+
h(StatusBar, { tone: 'info' }, '数据来自 QQ 官方接口; 机器人需为群管理员。通过/拒绝将直接生效并写入审计日志。'),
|
|
518
|
+
h(DataTable, { cols: joinCols, rows: joinRows, emptyText: '当前没有待审批的入群申请' }))
|
|
519
|
+
} else if (tab === 'mute') {
|
|
520
|
+
// 禁言页: ①选人禁言(本地成员清单) ②正在禁言中的成员(官方状态)
|
|
521
|
+
var pickList = members === null ? h(Empty, { icon: '⏳', text: '加载成员中…' })
|
|
522
|
+
: members.length === 0 ? h('div', { style: { border: '1px dashed ' + T.border, borderRadius: T.radius, padding: '12px', textAlign: 'center', color: T.text3, fontSize: T.fsSm } },
|
|
523
|
+
'本地暂无成员记录 —— 让群友在群里说句话, 机器人就能记住他们, 之后这里可直接选人禁言(官方成员列表未开放)')
|
|
524
|
+
: h('div', { style: { maxHeight: 260, overflowY: 'auto', border: '1px solid ' + T.border, borderRadius: T.radius } },
|
|
525
|
+
members.map(function (m) {
|
|
526
|
+
var busying = busy === 'mute-' + m.mid
|
|
527
|
+
return h('div', { key: m.mid, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderBottom: '1px solid ' + T.borderLight, fontSize: T.fsSm } },
|
|
528
|
+
h('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (m.name || '(未知名)') + ' · ' + m.mid.slice(0, 10) + '…'),
|
|
529
|
+
h('input', { className: 'qqs-inp', style: { width: 70, padding: '1px 6px', border: '1px solid ' + T.border, borderRadius: T.radius }, type: 'number', min: 1, defaultValue: muteSecs, onChange: function (e) { setMuteSecs(e.target.value) } }),
|
|
530
|
+
h('span', { style: { color: T.text3 } }, '分钟'),
|
|
531
|
+
h(SButton, { variant: 'danger', size: 'sm', disabled: !!busy, onClick: function () { doMute(m.mid, 'mute', Number(muteSecs || 60) * 60) } }, busying ? '处理中…' : '禁言'))
|
|
532
|
+
}))
|
|
533
|
+
body = mute === null ? h(Empty, { icon: '⏳', text: '加载中…' })
|
|
534
|
+
: h('div', null,
|
|
535
|
+
h('div', { style: { fontWeight: 700, fontSize: 13, margin: '10px 0 4px' } }, '① 选择成员禁言'),
|
|
536
|
+
pickList,
|
|
537
|
+
h('div', { style: { fontWeight: 700, fontSize: 13, margin: '14px 0 4px' } }, '② 正在禁言中(官方状态)'),
|
|
538
|
+
h(StatusBar, { tone: 'info' }, '全员禁言: ' + (muteMode === 'always' ? '常开' : muteMode === 'schedule' ? '定时' : '关闭') + ' —— 只能操作普通成员, 群主/管理员不可禁。'),
|
|
539
|
+
h(DataTable, { cols: muteCols, rows: muteRows, emptyText: '没有正在禁言的成员' }))
|
|
540
|
+
} else if (tab === 'members') {
|
|
541
|
+
// 成员信息: 红条(官方未开放)+ 本地兜底名单(见到的发言者)
|
|
542
|
+
var memRows = (members || []).map(function (m) { return { name: m.name || '(未知名)', mid: m.mid, seen: new Date(m.lastSeen).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } })
|
|
543
|
+
var memCols = [
|
|
544
|
+
{ key: 'name', title: '昵称', width: '24%' },
|
|
545
|
+
{ key: 'mid', title: '成员 ID', render: function (v) { return h('span', { style: { color: T.text2, wordBreak: 'break-all' } }, v) } },
|
|
546
|
+
{ key: 'seen', title: '最近发言', width: '22%', render: function (v) { return h('span', { style: { color: T.text3 } }, v) } },
|
|
547
|
+
]
|
|
548
|
+
body = h('div', null,
|
|
549
|
+
h(StatusBar, { tone: 'danger', action: { label: '去开放平台申请', onClick: function () { window.open('https://q.qq.com', '_blank') } } }, '官方"群成员列表"接口尚未开放 —— 下方为本地兜底名单(机器人见过的发言者), 仅用于帮你认人。'),
|
|
550
|
+
h('div', { style: { fontWeight: 700, fontSize: 13, margin: '10px 0 4px' } }, '本地成员清单(机器人见过的)'),
|
|
551
|
+
h(DataTable, { cols: memCols, rows: memRows, emptyText: '暂无记录 —— 群友发言后自动出现' }))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return h('div', { style: { ...card, padding: '4px 0 12px', margin: '12px 0', overflow: 'hidden' } },
|
|
555
|
+
h('div', { style: { padding: '12px 16px', borderBottom: '1px solid ' + T.border, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
|
|
556
|
+
h('span', { style: { fontSize: T.fsTitle, fontWeight: 700 } }, '🛡 群管理'),
|
|
557
|
+
h('span', { style: { fontSize: T.fsSm, color: T.text3 } }, '直连 QQ 官方, 面板操作 = 主人直发'),
|
|
558
|
+
h('span', { style: { flex: 1 } }),
|
|
559
|
+
h('span', { style: { fontSize: T.fsSm, color: T.text2 } }, '目标群'),
|
|
560
|
+
groupSel,
|
|
561
|
+
h(SButton, { variant: 'ghost', size: 'sm', onClick: refresh }, '刷新')),
|
|
562
|
+
h('div', { style: { padding: '0 16px' } },
|
|
563
|
+
h(TabBar, { items: GROUP_TABS.map(function (t) { return { key: t.key, label: t.label + (t.open ? '' : '(待开放)'), badge: t.key === 'join' ? joinRows.length : 0 } }), value: tab, onChange: function (k) { setTab(k); setMsg('') } })),
|
|
564
|
+
h('div', { style: { padding: '0 16px' } }, body),
|
|
565
|
+
h('div', { style: { margin: '12px 16px 0', borderTop: '1px solid ' + T.borderLight, paddingTop: 10, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
566
|
+
h('span', { style: { fontSize: T.fsSm, color: T.text2 } }, '登记群(官方无"我的群列表", 手动粘贴):'),
|
|
567
|
+
h('input', { className: 'qqs-inp', style: { width: 200, padding: '3px 8px', border: '1px solid ' + T.border, borderRadius: T.radius }, placeholder: 'group_openid', value: bindGid, onChange: function (e) { setBindGid(e.target.value) } }),
|
|
568
|
+
h('input', { className: 'qqs-inp', style: { width: 100, padding: '3px 8px', border: '1px solid ' + T.border, borderRadius: T.radius }, placeholder: '备注(可选)', value: bindName, onChange: function (e) { setBindName(e.target.value) } }),
|
|
569
|
+
h(SButton, { variant: 'primary', size: 'sm', onClick: doBind }, '绑定'),
|
|
570
|
+
msg ? h('span', { style: { fontSize: T.fsSm, color: T.success } }, msg) : null))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
var API_LIST = '/api/qqbot-settings/stickers'
|
|
574
|
+
var API_IMG = '/api/qqbot-settings/sticker-img?id='
|
|
575
|
+
var API_BATCH = '/api/qqbot-settings/stickers/batch'
|
|
576
|
+
var API_IMPORT = '/api/qqbot-settings/stickers/import'
|
|
577
|
+
var LAYER_LABEL = { candidate: '待整理', library: '收藏', trash: '回收站' }
|
|
578
|
+
|
|
579
|
+
function Lightbox(props) {
|
|
580
|
+
var it = props.item || {}
|
|
581
|
+
var lbDD = props.storeDir || ''
|
|
582
|
+
var [tagsText, setTagsText] = useState((it.tags || []).join(','))
|
|
583
|
+
var [desc, setDesc] = useState(it.desc || '')
|
|
584
|
+
var [saved, setSaved] = useState('')
|
|
585
|
+
function doBatch(op, extra) {
|
|
586
|
+
fetch(API_BATCH, {
|
|
587
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
588
|
+
body: JSON.stringify(Object.assign({ op: op, ids: [it.id], dataDir: lbDD || undefined }, extra || {})),
|
|
589
|
+
}).then(function (r) { return r.json() }).then(function () { setSaved(op === 'tag' ? '已保存 ✓' : (op === 'promote' ? '已移入收藏 ✓' : '已送回收站 ✓')); props.onChanged && props.onChanged() }).catch(function (e) { setSaved('失败:' + e.message) })
|
|
590
|
+
}
|
|
591
|
+
return h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: function () { props.onClose() } },
|
|
592
|
+
h('div', { className: 'qqs-modal', style: { background: '#22252e', borderRadius: 12, padding: 14, maxWidth: 560, width: '92%', maxHeight: '88vh', overflow: 'auto' }, onClick: function (e) { e.stopPropagation() } },
|
|
593
|
+
h('img', { src: API_IMG + encodeURIComponent(it.id) + (lbDD ? '&dataDir=' + encodeURIComponent(lbDD) : ''), style: { maxWidth: '100%', maxHeight: '46vh', borderRadius: 8, display: 'block', margin: '0 auto', objectFit: 'contain' } }),
|
|
594
|
+
h('p', { style: { fontSize: 12, color: '#999' } }, '图号 ' + it.id.slice(0, 8) + ' · ' + (LAYER_LABEL[it.layer] || it.layer) + ' · 用过 ' + (it.useCount || 0) + ' 次'),
|
|
595
|
+
h('div', { style: { fontSize: 13, margin: '4px 0' } }, '标签(逗号或空格分开都行,自动去重):'),
|
|
596
|
+
h('input', { className: 'qqs-inp', style: { width: '100%', padding: '4px 8px' }, value: tagsText, onChange: function (e) { setTagsText(e.target.value) } }),
|
|
597
|
+
h('div', { style: { fontSize: 13, margin: '6px 0 2px' } }, '描述(一句话说明画面和适合场合):'),
|
|
598
|
+
h('textarea', { className: 'qqs-area', style: { width: '100%', minHeight: 54 }, value: desc, onChange: function (e) { setDesc(e.target.value) } }),
|
|
599
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', margin: '6px 0', fontSize: 12, color: '#aaa' } },
|
|
600
|
+
h('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', direction: 'rtl', textAlign: 'left' } }, it.path || ''),
|
|
601
|
+
h('button', { className: 'qqs-btn', style: { padding: '2px 8px', fontSize: 12 }, onClick: function () { try { navigator.clipboard.writeText(it.path || '').then(function () { setSaved('原路径已复制 ✓') }).catch(function () { setSaved('复制失败') }) } catch (e2) { setSaved('复制失败:' + e2.message) } } }, '复制原路径')),
|
|
602
|
+
h('div', { style: { marginTop: 8, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
603
|
+
h('button', { className: 'qqs-btn', onClick: function () { doBatch('tag', { tags: tagsText.split(/[,,\s]+/).map(function (s) { return s.trim() }).filter(Boolean), desc: desc }) } }, '保存'),
|
|
604
|
+
it.layer !== 'library' ? h('button', { className: 'qqs-btn', onClick: function () { doBatch('promote') } }, '移入收藏') : null,
|
|
605
|
+
it.layer !== 'trash' ? h('button', { className: 'qqs-btn', onClick: function () { doBatch('trash') } }, '送回收站') : null,
|
|
606
|
+
it.layer === 'trash' ? h('button', { className: 'qqs-btn', onClick: function () { doBatch('restore') } }, '恢复') : null,
|
|
607
|
+
saved ? h('span', { style: { fontSize: 12, color: '#2f9e44' } }, saved) : null,
|
|
608
|
+
h('button', { className: 'qqs-btn', onClick: function () { props.onClose() } }, '关闭'))))
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function ImportPanel(props) {
|
|
612
|
+
var ipDD = props.storeDir || ''
|
|
613
|
+
var [text, setText] = useState('')
|
|
614
|
+
var [fileMsg, setFileMsg] = useState('')
|
|
615
|
+
var [result, setResult] = useState('')
|
|
616
|
+
var fileInput = React.useRef(null)
|
|
617
|
+
function doImport(extra) {
|
|
618
|
+
setResult('导入中…')
|
|
619
|
+
fetch(API_IMPORT, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(Object.assign({}, extra, ipDD ? { dataDir: ipDD } : {})) })
|
|
620
|
+
.then(function (r) { return r.json() }).then(function (d) {
|
|
621
|
+
var rs = (d && d.results) || []
|
|
622
|
+
var ok = 0, dup = 0, err = 0, errList = []
|
|
623
|
+
rs.forEach(function (x) { if (x.status === 'new') ok += 1; else if (x.status === 'dup') dup += 1; else { err += 1; if (errList.length < 5) errList.push(x.ref) } })
|
|
624
|
+
setResult('完成: 新增 ' + ok + ' 张, 已有跳过 ' + dup + ' 张' + (err ? ', 失败 ' + err + ' 张(' + errList.join('; ') + ')' : ''))
|
|
625
|
+
}).catch(function (e) { setResult('失败:' + e.message) })
|
|
626
|
+
}
|
|
627
|
+
function pickFiles() {
|
|
628
|
+
var files = fileInput.current && fileInput.current.files ? Array.from(fileInput.current.files).slice(0, 20) : []
|
|
629
|
+
if (!files.length) return
|
|
630
|
+
setFileMsg('读取中 ' + files.length + ' 个文件…')
|
|
631
|
+
var reads = files.map(function (f) {
|
|
632
|
+
return new Promise(function (res) {
|
|
633
|
+
var rd = new FileReader()
|
|
634
|
+
rd.onload = function () {
|
|
635
|
+
var b64 = String(rd.result || '').split(',')[1] || ''
|
|
636
|
+
res({ name: f.name, dataBase64: b64 })
|
|
637
|
+
}
|
|
638
|
+
rd.onerror = function () { res(null) }
|
|
639
|
+
rd.readAsDataURL(f)
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
Promise.all(reads).then(function (list) {
|
|
643
|
+
var filesArr = list.filter(Boolean)
|
|
644
|
+
setFileMsg('')
|
|
645
|
+
doImport({ files: filesArr })
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
return h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: function () { props.onClose() } },
|
|
649
|
+
h('div', { className: 'qqs-modal', style: { background: '#22252e', borderRadius: 12, padding: 14, maxWidth: 520, width: '92%' }, onClick: function (e) { e.stopPropagation() } },
|
|
650
|
+
h('h3', { style: { marginTop: 0 } }, '导入表情包'),
|
|
651
|
+
h('div', null,
|
|
652
|
+
h('input', { className: 'qqs-inp', ref: fileInput, type: 'file', accept: 'image/*', multiple: true }),
|
|
653
|
+
h('button', { className: 'qqs-btn', style: { marginLeft: 8 }, onClick: pickFiles }, '上传所选图片')),
|
|
654
|
+
h('p', { style: { fontSize: 12, color: '#999' } }, '或粘贴每行一个:图片URL 或 电脑上的图片路径'),
|
|
655
|
+
h('textarea', { className: 'qqs-area', style: { width: '100%', minHeight: 70 }, value: text, placeholder: 'https://… 或 D:\\…\\xx.jpg', onChange: function (e) { setText(e.target.value) } }),
|
|
656
|
+
h('div', { style: { margin: '6px 0' } },
|
|
657
|
+
h('button', { className: 'qqs-btn', onClick: function () {
|
|
658
|
+
var lines = text.split('\n').map(function (s) { return s.trim() }).filter(Boolean)
|
|
659
|
+
var urls = lines.filter(function (x) { return /^https?:\/\//i.test(x) })
|
|
660
|
+
var paths = lines.filter(function (x) { return !/^https?:\/\//i.test(x) })
|
|
661
|
+
doImport({ urls: urls, paths: paths })
|
|
662
|
+
} }, '导入粘贴的内容')),
|
|
663
|
+
fileMsg ? h('p', { style: { fontSize: 12 } }, fileMsg) : null,
|
|
664
|
+
result ? h('p', { style: { fontSize: 13, color: '#2f9e44' } }, result) : null,
|
|
665
|
+
h('div', { style: { textAlign: 'right', marginTop: 8 } }, h('button', { className: 'qqs-btn', onClick: function () { props.onClose() } }, '关闭'))))
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function GalleryPage(props) {
|
|
669
|
+
// 多账号: 图库按当前账号 dataDir(各号各库)
|
|
670
|
+
var DD = props && props.acct && props.acct.dataDir ? String(props.acct.dataDir) : ''
|
|
671
|
+
var [items, setItems] = useState([])
|
|
672
|
+
var [layer, setLayer] = useState('')
|
|
673
|
+
var [untagged, setUntagged] = useState(false)
|
|
674
|
+
var [q, setQ] = useState('')
|
|
675
|
+
var [sel, setSel] = useState([])
|
|
676
|
+
var [box, setBox] = useState(null)
|
|
677
|
+
var [imp, setImp] = useState(false)
|
|
678
|
+
var gridRef = React.useRef(null)
|
|
679
|
+
var suppressRef = React.useRef(false)
|
|
680
|
+
var [mq, setMq] = useState(null)
|
|
681
|
+
var selDownRef = React.useRef(false)
|
|
682
|
+
|
|
683
|
+
function onGridDown(e) {
|
|
684
|
+
if (e.button !== 0) return
|
|
685
|
+
if (selDownRef.current) return
|
|
686
|
+
selDownRef.current = true
|
|
687
|
+
try { if (e.target && e.target.closest && e.target.closest('input,textarea,button,select,label,a,[data-sel-card]')) { selDownRef.current = false; return } } catch (e2) {}
|
|
688
|
+
var sx = e.clientX, sy = e.clientY, moved = false
|
|
689
|
+
function move(ev) {
|
|
690
|
+
var dx = ev.clientX - sx, dy = ev.clientY - sy
|
|
691
|
+
if (!moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) moved = true
|
|
692
|
+
if (moved) setMq({ x0: Math.min(sx, ev.clientX), y0: Math.min(sy, ev.clientY), x1: Math.max(sx, ev.clientX), y1: Math.max(sy, ev.clientY) })
|
|
693
|
+
}
|
|
694
|
+
function up(ev) {
|
|
695
|
+
window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); setMq(null); selDownRef.current = false
|
|
696
|
+
if (moved && gridRef.current) {
|
|
697
|
+
suppressRef.current = true
|
|
698
|
+
var r = { x0: Math.min(sx, ev.clientX), y0: Math.min(sy, ev.clientY), x1: Math.max(sx, ev.clientX), y1: Math.max(sy, ev.clientY) }
|
|
699
|
+
var hits = []
|
|
700
|
+
Array.prototype.forEach.call(gridRef.current.children, function (el, i) {
|
|
701
|
+
var b = el.getBoundingClientRect()
|
|
702
|
+
var cx = b.left + b.width / 2, cy = b.top + b.height / 2
|
|
703
|
+
if (cx >= r.x0 && cx <= r.x1 && cy >= r.y0 && cy <= r.y1) { var it = items[i]; if (it) hits.push(it.id) }
|
|
704
|
+
})
|
|
705
|
+
if (hits.length) setSel(function (s) { return ev.shiftKey ? Array.from(new Set(s.concat(hits))) : hits })
|
|
706
|
+
setTimeout(function () { suppressRef.current = false }, 80)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
window.addEventListener('mousemove', move); window.addEventListener('mouseup', up)
|
|
710
|
+
}
|
|
711
|
+
var [msg, setMsg] = useState('')
|
|
712
|
+
|
|
713
|
+
function load(quiet) {
|
|
714
|
+
// 回收站层必须带 includeTrash=1, 否则 queryAll 默认滤掉 trash(既有坑: 回收站一直显示空)
|
|
715
|
+
var url = API_LIST + '?layer=' + encodeURIComponent(layer) + (layer === 'trash' ? '&includeTrash=1' : '') + (untagged ? '&untagged=1' : '') + '&q=' + encodeURIComponent(q) + (DD ? '&dataDir=' + encodeURIComponent(DD) : '')
|
|
716
|
+
fetch(url).then(function (r) { return r.json() }).then(function (d) {
|
|
717
|
+
if (d && Array.isArray(d.items)) {
|
|
718
|
+
setItems(d.items)
|
|
719
|
+
if (!quiet) setSel([])
|
|
720
|
+
}
|
|
721
|
+
}).catch(function (e) { if (!quiet) setMsg('加载失败:' + e.message) })
|
|
722
|
+
}
|
|
723
|
+
useEffect(function () { load(true) }, [layer, untagged, q])
|
|
724
|
+
// 自动刷新: 后台机器人持续收藏/改动时, 页面每 6 秒同步一次(不打断勾选)
|
|
725
|
+
useEffect(function () {
|
|
726
|
+
var t = setInterval(function () { load(true) }, 6000)
|
|
727
|
+
return function () { clearInterval(t) }
|
|
728
|
+
}, [layer, untagged, q])
|
|
729
|
+
|
|
730
|
+
function toggle(id) { setSel(function (s) { return s.includes(id) ? s.filter(function (x) { return x !== id }) : s.concat(id) }) }
|
|
731
|
+
function batch(op, extra) {
|
|
732
|
+
if (!sel.length) return
|
|
733
|
+
fetch(API_BATCH, {
|
|
734
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
735
|
+
body: JSON.stringify(Object.assign({ op: op, ids: sel, dataDir: DD || undefined }, extra || {})),
|
|
736
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
737
|
+
setMsg((d && d.total !== undefined ? '已处理 ' + d.ok + '/' + d.total : '完成') + ' ✓')
|
|
738
|
+
load()
|
|
739
|
+
}).catch(function (e) { setMsg('操作失败:' + e.message) })
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
var pills = [['', '全部'], ['candidate', '待整理'], ['library', '收藏'], ['trash', '回收站']]
|
|
743
|
+
return h('div', { className: 'qqs-panel', onMouseDown: onGridDown, style: { maxWidth: 1080, width: '100%', userSelect: 'none' } },
|
|
744
|
+
h('h2', null, '表情包图库'),
|
|
745
|
+
h('div', { className: 'qqs-toolbar', style: {} },
|
|
746
|
+
pills.map(function (p) {
|
|
747
|
+
return h('button', { className: 'qqs-btn', key: p[0], style: { padding: '3px 10px', fontWeight: layer === p[0] ? 700 : 400 }, onClick: function () { setLayer(p[0]) } }, p[1])
|
|
748
|
+
}),
|
|
749
|
+
h('input', { className: 'qqs-inp', placeholder: '搜标签/描述…', value: q, style: { flex: 1, minWidth: 140 }, onChange: function (e) { setQ(e.target.value) } }),
|
|
750
|
+
h('label', { style: { fontSize: 13, whiteSpace: 'nowrap' } }, h('input', { className: 'qqs-inp', type: 'checkbox', className: 'qqs-cb', checked: untagged, onChange: function (e) { setUntagged(e.target.checked) } }), ' 只看没打标的'),
|
|
751
|
+
h('button', { className: 'qqs-btn', onClick: function () { setImp(true) } }, '导入'),
|
|
752
|
+
h('button', { className: 'qqs-btn', onClick: load }, '刷新')),
|
|
753
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '共 ' + items.length + ' 张。点图看大图并编辑标签/描述;勾选后底部批量操作。'),
|
|
754
|
+
items.length === 0 ? h('p', { style: { color: '#888', padding: 20, textAlign: 'center' } }, '这里还没有图' + (untagged ? '(没有未打标的图了 🎉)' : ',点右上角「导入」或等群里发图自动收藏')) : null,
|
|
755
|
+
h('div', { ref: gridRef, onMouseDown: onGridDown, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(104px,1fr))', gap: 14, padding: 14, userSelect: 'none', cursor: 'crosshair', minHeight: 120 } },
|
|
756
|
+
items.map(function (it) {
|
|
757
|
+
var on = sel.includes(it.id)
|
|
758
|
+
return h('div', { key: it.id, 'data-sel-card': true, className: 'qqs-card', style: { position: 'relative', border: '1px solid ' + (on ? '#2f9e44' : '#8883'), borderRadius: 8, overflow: 'hidden', cursor: 'pointer' }, onClick: function () { if (suppressRef.current) return; setBox(it) } },
|
|
759
|
+
h('img', { src: API_IMG + encodeURIComponent(it.id) + (DD ? '&dataDir=' + encodeURIComponent(DD) : ''), style: { width: '100%', height: 104, objectFit: 'cover', display: 'block' } }),
|
|
760
|
+
h('input', { className: 'qqs-inp', type: 'checkbox', className: 'qqs-cb', checked: on, style: { position: 'absolute', left: 4, top: 4 }, onClick: function (e) { e.stopPropagation(); toggle(it.id) } }),
|
|
761
|
+
it.tags && it.tags.length ? h('span', { style: { position: 'absolute', left: 4, bottom: 2, fontSize: 10, color: '#fff', background: '#0009', borderRadius: 4, padding: '0 4px', maxWidth: '80%', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' } }, it.tags[0]) : null,
|
|
762
|
+
it.layer === 'library' ? h('span', { style: { position: 'absolute', right: 4, bottom: 2, fontSize: 10, color: '#ffd43b' } }, '★') : null,
|
|
763
|
+
it.layer === 'trash' ? h('span', { style: { position: 'absolute', right: 4, bottom: 2, fontSize: 10, color: '#ffa94d' } }, '回收站') : null)
|
|
764
|
+
})),
|
|
765
|
+
h('div', { style: { height: 120, cursor: 'crosshair', userSelect: 'none' } }),
|
|
766
|
+
sel.length ? h('div', { style: { position: 'sticky', bottom: 8, marginTop: 10, background: 'rgba(255,255,255,.86)', border: '1px solid #00000030', borderRadius: 12, padding: '8px 12px', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', boxShadow: '0 6px 22px rgba(0,0,0,.18)', backdropFilter: 'blur(4px)' } },
|
|
767
|
+
h('span', { style: { fontWeight: 800, color: '#171a21' } }, '已选 ' + sel.length + ' 张'),
|
|
768
|
+
layer !== 'trash' ? h('button', { onClick: function () { batch('promote') }, style: { font: 'inherit', cursor: 'pointer', padding: '4px 12px', borderRadius: 9, border: '1px solid #2f9e4499', background: 'rgba(47,158,68,.12)', color: '#1d7a31', fontWeight: 700, fontSize: 13 } }, '移入收藏 ★') : null,
|
|
769
|
+
layer !== 'trash' ? h('button', { onClick: function () { batch('trash') }, style: { font: 'inherit', cursor: 'pointer', padding: '4px 12px', borderRadius: 9, border: '1px solid #e8590c99', background: 'rgba(232,89,12,.12)', color: '#c2450a', fontWeight: 700, fontSize: 13 } }, '送回收站') : null,
|
|
770
|
+
layer === 'trash' ? h('button', { onClick: function () { batch('restore') }, style: { font: 'inherit', cursor: 'pointer', padding: '4px 12px', borderRadius: 9, border: '1px solid #2f9e4499', background: 'rgba(47,158,68,.12)', color: '#1d7a31', fontWeight: 700, fontSize: 13 } }, '恢复') : null,
|
|
771
|
+
h('button', { onClick: function () { setSel([]) }, style: { font: 'inherit', cursor: 'pointer', padding: '4px 12px', borderRadius: 9, border: '1px solid #00000030', background: 'rgba(255,255,255,.7)', color: '#444', fontWeight: 600, fontSize: 13 } }, '取消')) : null,
|
|
772
|
+
mq ? h('div', { style: { position: 'fixed', left: mq.x0, top: mq.y0, width: Math.max(0, mq.x1 - mq.x0), height: Math.max(0, mq.y1 - mq.y0), border: '1px dashed #5b9dff', background: '#5b9dff22', pointerEvents: 'none', zIndex: 500 } }) : null,
|
|
773
|
+
msg ? h('p', { style: { fontSize: 13, color: '#2f9e44' } }, msg) : null,
|
|
774
|
+
box ? h(Lightbox, { item: box, storeDir: DD || undefined, onClose: function () { setBox(null); load() }, onChanged: load }) : null,
|
|
775
|
+
imp ? h(ImportPanel, { storeDir: DD || undefined, onClose: function () { setImp(false); load() } }) : null)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ── 定时任务页(管理 AI 在 QQ 会话里建的定时任务: 一次性/每天) ──
|
|
779
|
+
var API_TIMERS = '/api/qqbot-settings/timers'
|
|
780
|
+
function fmtWhen(job) {
|
|
781
|
+
if (job.kind === 'daily') return '每天 ' + (job.atTime || '??:??')
|
|
782
|
+
if (job.kind === 'once' && job.dueAt) {
|
|
783
|
+
var d = new Date(job.dueAt)
|
|
784
|
+
var p2 = function (n) { return n < 10 ? '0' + n : '' + n }
|
|
785
|
+
return '一次性 · ' + d.getFullYear() + '/' + p2(d.getMonth() + 1) + '/' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes())
|
|
786
|
+
}
|
|
787
|
+
return job.kind === 'once' ? '一次性' : '每天'
|
|
788
|
+
}
|
|
789
|
+
// ── 定时唤醒编辑(原设置页④区, 并入定时任务页): config.schedule.targets 分组 ──
|
|
790
|
+
// 保存走 im-qqbot settings ns 全量 patch(与设置页同构, 防覆盖掉其它字段), live 热更新。
|
|
791
|
+
// 聊天对象数据源(2026-09-05 统一): 群只列"活的"(host 已用官方 info 剔除注销群, 显示真群名);
|
|
792
|
+
// 私聊用台账昵称, 缺名时用本地成员表(同 openid 在群里出现过的昵称)补 —— 不再露裸 id。
|
|
793
|
+
function fetchChatTargets(acct) {
|
|
794
|
+
var ns = acct && acct.ns && acct.ns !== 'im-qqbot' ? String(acct.ns) : ''
|
|
795
|
+
var dd = acct && acct.dataDir ? String(acct.dataDir) : ''
|
|
796
|
+
var qNs = ns ? '?ns=' + encodeURIComponent(ns) : ''
|
|
797
|
+
var qDD = dd ? '?dataDir=' + encodeURIComponent(dd) : ''
|
|
798
|
+
var qM = ns ? ('?ns=' + encodeURIComponent(ns)) : '' // members_local 不带 gid = 全部成员
|
|
799
|
+
return Promise.all([
|
|
800
|
+
fetch('/api/qqbot-settings/group/accounts' + qNs).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
801
|
+
fetch('/api/qqbot-settings/known-chats' + qDD).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
802
|
+
fetch('/api/qqbot-settings/group/members_local' + qM).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
803
|
+
]).then(function (rs) {
|
|
804
|
+
var groups = ((rs[0] && rs[0].groups) || []).map(function (g) {
|
|
805
|
+
return { scope: 'group', id: g.gid, name: g.name || '', lastSeen: g.lastAt || 0, count: 0 }
|
|
806
|
+
})
|
|
807
|
+
var known = ((rs[1] && rs[1].chats) || [])
|
|
808
|
+
// 成员表: mid -> 最近昵称(给私聊/无台账名时补名)
|
|
809
|
+
var nameByMid = {}
|
|
810
|
+
;((rs[2] && rs[2].members) || []).forEach(function (m) {
|
|
811
|
+
if (m.mid && m.name) {
|
|
812
|
+
var old = nameByMid[m.mid]
|
|
813
|
+
if (!old || m.lastSeen > old.ts) nameByMid[m.mid] = { name: m.name, ts: m.lastSeen || 0 }
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
var c2c = []
|
|
817
|
+
var seen = {}
|
|
818
|
+
known.forEach(function (c) {
|
|
819
|
+
if (c.scope !== 'c2c' || !c.id || seen[c.id]) return
|
|
820
|
+
seen[c.id] = 1
|
|
821
|
+
var nm = c.name || (nameByMid[c.id] && nameByMid[c.id].name) || ''
|
|
822
|
+
c2c.push({ scope: 'c2c', id: c.id, name: nm, lastSeen: c.lastSeen || 0, count: c.count || 0 })
|
|
823
|
+
})
|
|
824
|
+
// 台账里没有但成员表有名字的 openid 也补上(私聊候选)
|
|
825
|
+
Object.keys(nameByMid).forEach(function (mid) {
|
|
826
|
+
if (!seen[mid]) { seen[mid] = 1; c2c.push({ scope: 'c2c', id: mid, name: nameByMid[mid].name, lastSeen: nameByMid[mid].ts, count: 0 }) }
|
|
827
|
+
})
|
|
828
|
+
c2c.sort(function (a, b) { return (b.lastSeen || 0) - (a.lastSeen || 0) })
|
|
829
|
+
return { group: groups, c2c: c2c }
|
|
830
|
+
})
|
|
831
|
+
}
|
|
832
|
+
function WakeEditor(props) {
|
|
833
|
+
// 多账号: 定时唤醒按当前账号 ns 读写; 表格=拍平行视图(行=对象×时刻), 存储仍是 targets 分组
|
|
834
|
+
var _wkNs = props && props.acct && props.acct.ns && props.acct.ns !== 'im-qqbot' ? String(props.acct.ns) : ''
|
|
835
|
+
var [cfg, setCfg] = useState(null)
|
|
836
|
+
var [rev, setRev] = useState(undefined)
|
|
837
|
+
var [knownChats, setKnownChats] = useState(null)
|
|
838
|
+
var [msg, setMsg] = useState('')
|
|
839
|
+
// 视图态: 筛选 tab + 对象搜索词(纯视图过滤)
|
|
840
|
+
var [filter, setFilter] = useState('all') // all|group|c2c
|
|
841
|
+
var [search, setSearch] = useState('')
|
|
842
|
+
|
|
843
|
+
function load() {
|
|
844
|
+
fetch(READ + (_wkNs ? '?ns=' + encodeURIComponent(_wkNs) : '')).then(function (r) { return r.json() }).then(function (d) {
|
|
845
|
+
if (d && d.value) { setCfg(d.value); setRev(d.revision); setMsg('') }
|
|
846
|
+
else { setMsg('读取失败: ' + JSON.stringify(d)) }
|
|
847
|
+
}).catch(function (e) { setMsg('读取异常: ' + e.message) })
|
|
848
|
+
}
|
|
849
|
+
useEffect(function () { load() }, [])
|
|
850
|
+
useEffect(function () {
|
|
851
|
+
fetchChatTargets(props.acct).then(function (t) {
|
|
852
|
+
var list = (t.group || []).concat(t.c2c || [])
|
|
853
|
+
setKnownChats(list)
|
|
854
|
+
}).catch(function () {})
|
|
855
|
+
}, [])
|
|
856
|
+
function targets() { return (cfg && cfg.schedule && Array.isArray(cfg.schedule.targets)) ? cfg.schedule.targets : [] }
|
|
857
|
+
function setTargets(list) {
|
|
858
|
+
setCfg(function (c) {
|
|
859
|
+
var base = c || {}
|
|
860
|
+
var s = (base.schedule && typeof base.schedule === 'object') ? base.schedule : {}
|
|
861
|
+
return { ...base, schedule: Object.assign({}, s, { targets: list }) }
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── 拍平行行模型: [{tgIndex, tg, task}] ──
|
|
866
|
+
function rows() {
|
|
867
|
+
var out = []
|
|
868
|
+
targets().forEach(function (tg, ti) {
|
|
869
|
+
;(tg.tasks || []).forEach(function (tt) {
|
|
870
|
+
out.push({ ti: ti, tg: tg, task: tt })
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
return out
|
|
874
|
+
}
|
|
875
|
+
function chatLabelOf(scope, id) {
|
|
876
|
+
var hit = null
|
|
877
|
+
;(knownChats || []).forEach(function (c) { if (c.scope === scope && c.id === id) hit = c })
|
|
878
|
+
if (hit && hit.name) return hit.name
|
|
879
|
+
return id ? (id.length > 14 ? id.slice(0, 6) + '…' + id.slice(-4) : id) : ''
|
|
880
|
+
}
|
|
881
|
+
// 行操作 → 直接改 targets 分组(保持存储模型)
|
|
882
|
+
function patchTask(ti, taskId, patch) {
|
|
883
|
+
var list = targets().slice()
|
|
884
|
+
var tg = { ...list[ti], tasks: (list[ti].tasks || []).map(function (t) { return t.id === taskId ? { ...t, ...patch } : t }) }
|
|
885
|
+
list[ti] = tg
|
|
886
|
+
setTargets(list)
|
|
887
|
+
}
|
|
888
|
+
// 行内改对象: 若该行所在组还挂着其它行(旧数据同组多时刻), 先拆成独立组再改, 保证只影响本行
|
|
889
|
+
function pickObject(r, c) {
|
|
890
|
+
var list = targets().slice()
|
|
891
|
+
var tg = list[r.ti]
|
|
892
|
+
if (!tg) return
|
|
893
|
+
var tasks = (tg.tasks || []).slice()
|
|
894
|
+
var patch = { scope: c.scope, targetId: c.id, name: c.name || '' }
|
|
895
|
+
if (tasks.length <= 1) {
|
|
896
|
+
list[r.ti] = { ...tg, ...patch }
|
|
897
|
+
} else {
|
|
898
|
+
var keep = tasks.filter(function (t) { return t.id !== r.task.id })
|
|
899
|
+
var lone = tasks.filter(function (t) { return t.id === r.task.id })
|
|
900
|
+
var ntg = { ...tg, ...patch, id: 'tg-' + Date.now().toString(36), tasks: lone }
|
|
901
|
+
list.splice(r.ti, 1, { ...tg, tasks: keep }, ntg)
|
|
902
|
+
list = list.filter(function (g) { return (g.tasks || []).length > 0 })
|
|
903
|
+
}
|
|
904
|
+
setTargets(list)
|
|
905
|
+
}
|
|
906
|
+
function rmRow(ti, taskId) {
|
|
907
|
+
var list = targets().slice()
|
|
908
|
+
var tg = { ...list[ti], tasks: (list[ti].tasks || []).filter(function (t) { return t.id !== taskId }) }
|
|
909
|
+
if (tg.tasks.length === 0) list = list.filter(function (_, i) { return i !== ti }) // 空目标组一并删
|
|
910
|
+
else list[ti] = tg
|
|
911
|
+
setTargets(list)
|
|
912
|
+
}
|
|
913
|
+
function addRow() {
|
|
914
|
+
var list = targets().slice()
|
|
915
|
+
// 每行 = 独立目标组(对象列改起来互不牵连); 默认挂"全部"视图, scope 跟随当前筛选类型
|
|
916
|
+
var nt = { id: 't-' + Date.now().toString(36), time: '09:00', enabled: true, prompt: '' }
|
|
917
|
+
var scope = filter === 'c2c' ? 'c2c' : filter === 'group' ? 'group' : 'group'
|
|
918
|
+
var ntg = { id: 'tg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6), name: '', scope: scope, targetId: '', tasks: [nt] }
|
|
919
|
+
list.push(ntg)
|
|
920
|
+
setTargets(list)
|
|
921
|
+
}
|
|
922
|
+
// 保存: 整表(行→分组已实时同步)全量 patch
|
|
923
|
+
function save() {
|
|
924
|
+
setMsg('保存中…')
|
|
925
|
+
var v = cfg || {}
|
|
926
|
+
var patch = {
|
|
927
|
+
behavior: v.behavior || {},
|
|
928
|
+
sticker: {
|
|
929
|
+
gates: (v.sticker && v.sticker.gates) || {},
|
|
930
|
+
autoTagEnabled: !!(v.sticker && v.sticker.autoTagEnabled),
|
|
931
|
+
visionCli: (v.sticker && typeof v.sticker.visionCli === 'string') ? v.sticker.visionCli : '',
|
|
932
|
+
},
|
|
933
|
+
injectRules: Array.isArray(v.injectRules) ? v.injectRules : [],
|
|
934
|
+
schedule: { targets: targets() },
|
|
935
|
+
groupPrompt: typeof v.groupPrompt === 'string' ? v.groupPrompt : DEFAULT_GROUP_PROMPT,
|
|
936
|
+
}
|
|
937
|
+
fetch(UPDATE, {
|
|
938
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
939
|
+
body: JSON.stringify({ patch: patch, expectedRevision: rev, ns: _wkNs || undefined }),
|
|
940
|
+
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
941
|
+
if (d && d.value) { setCfg(d.value); setRev(d.revision); setMsg('已保存 ✓(live 生效)') }
|
|
942
|
+
else if (d && d.error) { setMsg('保存失败: ' + d.error) }
|
|
943
|
+
else { setMsg('保存失败(未知响应)') }
|
|
944
|
+
}).catch(function (e) { setMsg('保存异常: ' + e.message) })
|
|
945
|
+
}
|
|
946
|
+
if (!cfg) return h('p', null, msg || '加载中…')
|
|
947
|
+
|
|
948
|
+
// 筛选行
|
|
949
|
+
var allRows = rows()
|
|
950
|
+
var shown = allRows.filter(function (r) {
|
|
951
|
+
if (filter !== 'all' && r.tg.scope !== filter) return false
|
|
952
|
+
if (search) {
|
|
953
|
+
var q = search.toLowerCase()
|
|
954
|
+
var nm = (chatLabelOf(r.tg.scope, r.tg.targetId) || '').toLowerCase()
|
|
955
|
+
if (nm.indexOf(q) < 0 && String(r.task.prompt || '').toLowerCase().indexOf(q) < 0 && String(r.task.time).indexOf(q) < 0) return false
|
|
956
|
+
}
|
|
957
|
+
return true
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
// 渲染: 表格
|
|
961
|
+
return h('div', null,
|
|
962
|
+
h('p', { style: { fontSize: T.fsSm, color: T.warn, margin: '4px 0 6px' } }, '⚠️ QQ 平台: 主动发私聊需对方 48h 内跟机器人说过话, 超时会拒收(自动降级); 群一般没问题。想验证: 时间设成 1~2 分钟后保存等着看。'),
|
|
963
|
+
// 工具条: 筛选 + 搜索 + 添加行
|
|
964
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', margin: '6px 0' } },
|
|
965
|
+
h(TabBar, {
|
|
966
|
+
items: [
|
|
967
|
+
{ key: 'all', label: '全部(' + allRows.length + ')' },
|
|
968
|
+
{ key: 'group', label: '群' },
|
|
969
|
+
{ key: 'c2c', label: '私聊' },
|
|
970
|
+
], value: filter, onChange: function (k) { setFilter(k); setMsg('') },
|
|
971
|
+
}),
|
|
972
|
+
h('input', { className: 'qqs-inp', style: { flex: 1, minWidth: 120, padding: '3px 8px', border: '1px solid ' + T.border, borderRadius: T.radius }, placeholder: '搜对象 / 描述 / 时间…', value: search, onChange: function (e) { setSearch(e.target.value) } }),
|
|
973
|
+
h(SButton, { variant: 'primary', size: 'sm', onClick: addRow }, '+ 添加一行')),
|
|
974
|
+
// 表格头
|
|
975
|
+
h('div', { style: { display: 'flex', alignItems: 'center', background: T.cardHi, border: '1px solid #dde1e7', borderRadius: T.radius + ' ' + T.radius + ' 0 0', fontWeight: 700, fontSize: T.fsSm, color: T.text2 } },
|
|
976
|
+
h('div', { style: { width: 90, padding: '8px 10px' } }, '时间'),
|
|
977
|
+
h('div', { style: { flex: '1 1 26%', minWidth: 140, padding: '8px 10px' } }, '对象'),
|
|
978
|
+
h('div', { style: { flex: '2 1 38%', minWidth: 160, padding: '8px 10px' } }, '任务描述'),
|
|
979
|
+
h('div', { style: { width: 80, padding: '8px 10px', textAlign: 'center' } }, '每天'),
|
|
980
|
+
h('div', { style: { width: 56, padding: '8px 10px', textAlign: 'center' } }, '')),
|
|
981
|
+
// 行
|
|
982
|
+
shown.length === 0 ? h('div', { style: { border: '1px solid ' + T.border, borderTop: 'none', borderRadius: '0 0 ' + T.radius + ' ' + T.radius, padding: 22, textAlign: 'center', color: T.text3, fontSize: T.fsSm } }, '还没有任务行 —— 点「+ 添加一行」开始配置')
|
|
983
|
+
: shown.map(function (r, idx) {
|
|
984
|
+
var tg = r.tg, tt = r.task
|
|
985
|
+
var nm = chatLabelOf(tg.scope, tg.targetId)
|
|
986
|
+
return h('div', { key: tg.id + ':' + tt.id, style: { display: 'flex', alignItems: 'center', background: idx % 2 ? T.cardHi : '#ffffff', borderBottom: '1px solid #dde1e7', borderLeft: '1px solid #dde1e7', borderRight: '1px solid #dde1e7', fontSize: T.fsSm } },
|
|
987
|
+
// 时间
|
|
988
|
+
h('div', { style: { width: 90, padding: '6px 8px' } },
|
|
989
|
+
h('input', { className: 'qqs-inp', style: { width: 62, padding: '2px 6px', border: '1px solid ' + T.border, borderRadius: T.radius }, value: sv(tt.time), placeholder: '09:00', onChange: function (e) { patchTask(r.ti, tt.id, { time: e.target.value }) } })),
|
|
990
|
+
// 对象: 类型小标 + 可搜索选择器(点击弹出, 输入过滤; 每行独立, 互不牵连)
|
|
991
|
+
h('div', { style: { flex: '1 1 26%', minWidth: 150, padding: '6px 8px', display: 'flex', gap: 4, alignItems: 'center' } },
|
|
992
|
+
h('span', { style: { flexShrink: 0, fontSize: 10, padding: '0 5px', borderRadius: 4, color: '#fff', background: tg.scope === 'c2c' ? T.primary : T.primary2 } }, tg.scope === 'c2c' ? '私' : '群'),
|
|
993
|
+
h(TargetPicker, { value: { scope: tg.scope, targetId: tg.targetId, name: nm }, options: knownChats || [], onPick: function (c) { pickObject(r, c) } })),
|
|
994
|
+
// 描述
|
|
995
|
+
h('div', { style: { flex: '2 1 38%', minWidth: 160, padding: '6px 8px' } },
|
|
996
|
+
h('input', { className: 'qqs-inp', style: { width: '100%', padding: '2px 6px', border: '1px solid ' + T.border, borderRadius: T.radius, fontSize: T.fsSm }, value: sv(tt.prompt), placeholder: '例: 早上好, 提醒大家吃早饭(留空=自由发挥)', onChange: function (e) { patchTask(r.ti, tt.id, { prompt: e.target.value }) } })),
|
|
997
|
+
// 每天 toggle = 启停
|
|
998
|
+
h('div', { style: { width: 80, padding: '6px 8px', textAlign: 'center' } },
|
|
999
|
+
h('input', { type: 'checkbox', checked: !!tt.enabled, onChange: function (e) { patchTask(r.ti, tt.id, { enabled: e.target.checked }) } })),
|
|
1000
|
+
// 删除
|
|
1001
|
+
h('div', { style: { width: 56, padding: '6px 8px', textAlign: 'center' } },
|
|
1002
|
+
h('button', { className: 'qqs-btn', style: { padding: '1px 8px', fontSize: 11, color: T.danger, background: 'transparent', border: 'none', cursor: 'pointer' }, onClick: function () { if (window.confirm('删除这一行?')) rmRow(r.ti, tt.id) } }, '✕')),
|
|
1003
|
+
)
|
|
1004
|
+
}),
|
|
1005
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', margin: '10px 0 2px' } },
|
|
1006
|
+
h(SButton, { variant: 'primary', size: 'sm', onClick: save }, '保存定时唤醒'),
|
|
1007
|
+
h(SButton, { variant: 'ghost', size: 'sm', onClick: load }, '放弃修改'),
|
|
1008
|
+
msg ? h('span', { style: { fontSize: T.fsSm, color: T.success } }, msg) : null))
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function TimersPage(props) {
|
|
1012
|
+
// 多账号: 定时任务按当前账号 schedDir(={cwd}/.qqbot)
|
|
1013
|
+
var _tmSD = props && props.acct && props.acct.schedDir ? String(props.acct.schedDir) : ''
|
|
1014
|
+
var _tmQ = _tmSD ? '?schedDir=' + encodeURIComponent(_tmSD) : ''
|
|
1015
|
+
var [jobs, setJobs] = useState(null)
|
|
1016
|
+
var [msg, setMsg] = useState('')
|
|
1017
|
+
var [busyId, setBusyId] = useState('')
|
|
1018
|
+
// 名称解析(2026-09-05): 群=活群真名(accounts 已剔除注销群), 私聊=台账昵称+成员表补名
|
|
1019
|
+
var [chatTg, setChatTg] = useState({ group: [], c2c: [] })
|
|
1020
|
+
function load() {
|
|
1021
|
+
fetch(API_TIMERS + _tmQ).then(function (r) { return r.json() }).then(function (d) {
|
|
1022
|
+
if (d && Array.isArray(d.jobs)) setJobs(d.jobs.slice().reverse())
|
|
1023
|
+
else setMsg('读取失败: ' + JSON.stringify(d))
|
|
1024
|
+
}).catch(function (e) { setMsg('读取异常: ' + e.message) })
|
|
1025
|
+
}
|
|
1026
|
+
useEffect(function () {
|
|
1027
|
+
load()
|
|
1028
|
+
fetchChatTargets(props.acct).then(function (t) { setChatTg(t) }).catch(function () {})
|
|
1029
|
+
}, [])
|
|
1030
|
+
function nameOf(scope, id) {
|
|
1031
|
+
var nm = ''
|
|
1032
|
+
;(chatTg[scope] || []).forEach(function (x) { if (x.id === id) nm = x.name || nm })
|
|
1033
|
+
return nm
|
|
1034
|
+
}
|
|
1035
|
+
function isGroupAlive(gid) {
|
|
1036
|
+
return (chatTg.group || []).some(function (g) { return g.id === gid })
|
|
1037
|
+
}
|
|
1038
|
+
function act(job, body, okMsg) {
|
|
1039
|
+
if (busyId) return
|
|
1040
|
+
setBusyId(job.id)
|
|
1041
|
+
fetch(API_TIMERS, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(Object.assign({ id: job.id, schedDir: _tmSD || undefined }, body || {})) })
|
|
1042
|
+
.then(function (r) { return r.json() })
|
|
1043
|
+
.then(function (d) {
|
|
1044
|
+
if (d && d.error) setMsg('失败: ' + d.error)
|
|
1045
|
+
else { setMsg(okMsg + ' ✓'); load() }
|
|
1046
|
+
})
|
|
1047
|
+
.catch(function (e) { setMsg('操作失败: ' + e.message) })
|
|
1048
|
+
.then(function () { setBusyId('') })
|
|
1049
|
+
}
|
|
1050
|
+
function who(job) {
|
|
1051
|
+
var scope = job.scope === 'c2c' ? 'c2c' : 'group'
|
|
1052
|
+
var nm = nameOf(scope, job.peerId)
|
|
1053
|
+
if (!nm) return (scope === 'c2c' ? '私聊' : '群聊') + ' · ' + String(job.peerId || '').slice(0, 8)
|
|
1054
|
+
return (scope === 'c2c' ? '私聊 · ' : '群聊 · ') + nm
|
|
1055
|
+
}
|
|
1056
|
+
function chip(kind) {
|
|
1057
|
+
return h('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 999, color: '#fff', background: kind === 'daily' ? '#2f9e44' : '#4d7cfe', whiteSpace: 'nowrap' } },
|
|
1058
|
+
kind === 'daily' ? '每天' : '一次性')
|
|
1059
|
+
}
|
|
1060
|
+
// 左右分栏: 左=定时唤醒(编辑), 右=已安排的定时提醒(开关/删除)——有效利用横向空间
|
|
1061
|
+
var leftCol = h('div', { style: { flex: '1 1 44%', minWidth: 280, maxWidth: 560 } },
|
|
1062
|
+
h('div', { style: { ...card, padding: '10px 14px' } },
|
|
1063
|
+
h('div', { style: { fontWeight: 700, fontSize: 13 } }, '① 定时唤醒(每天到点,让她主动去群/私聊开口)'),
|
|
1064
|
+
h('p', { style: { fontSize: T.fsSm, color: T.text3 } }, '每个群/人一组,下面可加多个时刻。不用记号码——点「选聊天对象」挑见过的就行(群显示真名/人显示昵称)。改完点「保存」。'),
|
|
1065
|
+
h(WakeEditor, { acct: props.acct })))
|
|
1066
|
+
var rightCol = h('div', { style: { flex: '1 1 44%', minWidth: 280 } },
|
|
1067
|
+
h('div', { style: { ...card, padding: '10px 14px' } },
|
|
1068
|
+
h('div', { style: { fontWeight: 700, fontSize: 13 } }, '② 她答应你的定时提醒(在 QQ 里说一声就能建)'),
|
|
1069
|
+
h('p', { style: { fontSize: T.fsSm, color: T.text3 } }, '这些是她在 QQ 里应你要求安排的提醒。这里只管看/开关/删除;一次性到点自动消失。'),
|
|
1070
|
+
jobs === null ? h('p', { style: { fontSize: T.fsSm, color: T.text3 }, padding: 10 }, '加载中…') : null,
|
|
1071
|
+
jobs !== null && jobs.length === 0 ? h('p', { style: { color: '#888', padding: 14, textAlign: 'center', fontSize: T.fsSm } }, '还没有这类提醒。去 QQ 里让她安排一个,就会出现在这里。') : null,
|
|
1072
|
+
(jobs || []).map(function (j) {
|
|
1073
|
+
var disabled = busyId === j.id
|
|
1074
|
+
var deadGroup = j.scope === 'group' && !isGroupAlive(j.peerId)
|
|
1075
|
+
return h('div', { key: j.id, style: { border: '1px solid ' + (deadGroup ? T.danger + '88' : (j.enabled ? T.border : '#ffa94d66')), borderRadius: T.radius, padding: '8px 10px', margin: '6px 0', background: '#fff', opacity: j.enabled ? 1 : 0.72 } },
|
|
1076
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1077
|
+
chip(j.kind),
|
|
1078
|
+
h('span', { style: { fontWeight: 600, fontSize: T.fsSm } }, fmtWhen(j)),
|
|
1079
|
+
h('span', { style: { fontSize: T.fsSm, color: deadGroup ? T.danger : T.text2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 150 } }, deadGroup ? (who(j) + '(群已失效)') : who(j)),
|
|
1080
|
+
h('span', { style: { flex: 1 } }),
|
|
1081
|
+
h('label', { style: { fontSize: T.fsSm, whiteSpace: 'nowrap', color: j.enabled ? T.primary : '#ffa94d' } },
|
|
1082
|
+
h('input', { className: 'qqs-cb', type: 'checkbox', checked: !!j.enabled, disabled: disabled, onChange: function (e) { act(j, { enabled: e.target.checked }, e.target.checked ? '已启用' : '已停用') } }),
|
|
1083
|
+
' ' + (j.enabled ? '启用中' : '已停用')),
|
|
1084
|
+
h('button', { className: 'qqs-btn', disabled: disabled, style: { padding: '2px 8px', fontSize: T.fsSm }, onClick: function () { if (window.confirm('删除这条定时任务?')) act(j, {}, '已删除') } }, '删除')),
|
|
1085
|
+
j.prompt ? h('div', { style: { fontSize: T.fsSm, color: '#cfd3dc', background: '#ffffff0a', border: '1px solid #ffffff14', borderRadius: T.radius, padding: '4px 8px', marginTop: 4, whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, j.prompt) : null)
|
|
1086
|
+
}),
|
|
1087
|
+
msg ? h('p', { style: { fontSize: T.fsSm, color: T.success } }, msg) : null))
|
|
1088
|
+
return h('div', { style: { width: '100%' } },
|
|
1089
|
+
h('div', { style: { display: 'flex', gap: 14, alignItems: 'flex-start', flexWrap: 'wrap' } }, leftCol, rightCol),
|
|
1090
|
+
props && props.close ? h('button', { className: 'qqs-btn', onClick: props.close, style: { float: 'right', marginTop: 6 } }, '完成') : null)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ── 单入口壳: 设置 / 表情包图库 / 定时任务 三个胶囊 tab 页内切换 ──
|
|
1094
|
+
var TAB_KEY = 'qqs-home-tab'
|
|
1095
|
+
var ACCT_KEY = 'qqs-acct'
|
|
1096
|
+
function QqbotHome(props) {
|
|
1097
|
+
var [tab, setTab] = useState(null)
|
|
1098
|
+
var [accts, setAccts] = useState(null)
|
|
1099
|
+
var [acctId, setAcctId] = useState('')
|
|
1100
|
+
var [badges, setBadges] = useState({})
|
|
1101
|
+
// 当前操作账号(缺省=第一个启用的实例, 兼容单账号)
|
|
1102
|
+
function currentAcct() {
|
|
1103
|
+
var list = accts || []
|
|
1104
|
+
if (acctId) { var p = null; list.forEach(function (a) { if (a.id === acctId) p = a }); if (p) return p }
|
|
1105
|
+
var first = null
|
|
1106
|
+
list.forEach(function (a) { if (!first && !a.disabled) first = a })
|
|
1107
|
+
return first || list[0] || null
|
|
1108
|
+
}
|
|
1109
|
+
useEffect(function () {
|
|
1110
|
+
var saved = ''
|
|
1111
|
+
try { saved = localStorage.getItem(TAB_KEY) || '' } catch (e) {}
|
|
1112
|
+
setTab(saved === 'gallery' || saved === 'timers' || saved === 'accounts' ? saved : 'settings')
|
|
1113
|
+
fetch('/api/qqbot-settings/accounts').then(function (r) { return r.json() }).then(function (d) {
|
|
1114
|
+
if (d && Array.isArray(d.instances)) {
|
|
1115
|
+
setAccts(d.instances)
|
|
1116
|
+
var want = ''
|
|
1117
|
+
try { want = localStorage.getItem(ACCT_KEY) || '' } catch (e) {}
|
|
1118
|
+
var found = null
|
|
1119
|
+
d.instances.forEach(function (a) { if (a.id === want) found = a.id })
|
|
1120
|
+
setAcctId(found || '')
|
|
1121
|
+
}
|
|
1122
|
+
}).catch(function () {})
|
|
1123
|
+
}, [])
|
|
1124
|
+
// 角标按当前账号
|
|
1125
|
+
useEffect(function () {
|
|
1126
|
+
var ac = currentAcct()
|
|
1127
|
+
var alive = true
|
|
1128
|
+
var ddq = ac && ac.dataDir ? '&dataDir=' + encodeURIComponent(ac.dataDir) : ''
|
|
1129
|
+
var sdq = ac && ac.schedDir ? '?schedDir=' + encodeURIComponent(ac.schedDir) : ''
|
|
1130
|
+
Promise.all([
|
|
1131
|
+
fetch('/api/qqbot-settings/stickers?untagged=1&layer=candidate' + ddq).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
1132
|
+
fetch(API_TIMERS + sdq).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
1133
|
+
]).then(function (rs) {
|
|
1134
|
+
if (!alive) return
|
|
1135
|
+
var unt = rs[0] && typeof rs[0].total === 'number' ? rs[0].total : 0
|
|
1136
|
+
var on = 0
|
|
1137
|
+
var list = (rs[1] && rs[1].jobs) || []
|
|
1138
|
+
list.forEach(function (j) { if (j.enabled) on += 1 })
|
|
1139
|
+
setBadges({ untagged: unt, timers: on })
|
|
1140
|
+
})
|
|
1141
|
+
return function () { alive = false }
|
|
1142
|
+
}, [accts, acctId])
|
|
1143
|
+
if (!tab) return h('div', null, '…')
|
|
1144
|
+
function choose(t) { setTab(t); try { localStorage.setItem(TAB_KEY, t) } catch (e) {} }
|
|
1145
|
+
function chooseAcct(id) { setAcctId(id); try { localStorage.setItem(ACCT_KEY, id) } catch (e) {} }
|
|
1146
|
+
function badge(n) {
|
|
1147
|
+
return n > 0 ? h('span', { style: { background: '#ff6b6b', color: '#fff', borderRadius: 9, padding: '0 6px', fontSize: 11, lineHeight: '16px', display: 'inline-block', fontWeight: 700, flexShrink: 0 } }, n) : null
|
|
1148
|
+
}
|
|
1149
|
+
var ac = currentAcct()
|
|
1150
|
+
// 传给按账号视图的上下文: 设置按 ns, 图库按 dataDir, 定时按 schedDir
|
|
1151
|
+
var acctx = ac ? { id: ac.id, ns: ac.ns, dataDir: ac.dataDir, schedDir: ac.schedDir } : null
|
|
1152
|
+
// key=账号: 切号即强制子页整页重挂(清掉旧账号 state, 全部重新拉取)
|
|
1153
|
+
var subKey = ac ? ac.id : 'main'
|
|
1154
|
+
var sub = tab === 'gallery' ? h(GalleryPage, { key: subKey, close: props && props.close, acct: acctx })
|
|
1155
|
+
: tab === 'timers' ? h(TimersPage, { key: subKey, close: props && props.close, acct: acctx })
|
|
1156
|
+
: tab === 'accounts' ? h(AccountsPage, { close: props && props.close })
|
|
1157
|
+
: h(SettingsPage, { key: subKey, close: props && props.close, acct: acctx })
|
|
1158
|
+
var acctLine = (accts && accts.length > 1) || (accts && accts.length === 1)
|
|
1159
|
+
return h('div', { style: { width: '100%' } },
|
|
1160
|
+
// ── 一级: 账号选择条(身份带) ──
|
|
1161
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', margin: '2px 0 10px', padding: '6px 10px', border: '1px solid #b197fc55', borderRadius: 12, background: 'linear-gradient(90deg,#b197fc22,#b197fc0a)' } },
|
|
1162
|
+
h('span', { style: { fontSize: 13, fontWeight: 800, color: '#171a21', whiteSpace: 'nowrap' } }, '🤖 正在查看:'),
|
|
1163
|
+
accts && accts.length > 0
|
|
1164
|
+
? h('select', { className: 'qqs-inp', style: { padding: '4px 8px', fontWeight: 700 }, value: ac ? ac.id : '', onChange: function (e) { chooseAcct(e.target.value) } },
|
|
1165
|
+
accts.map(function (a) {
|
|
1166
|
+
return h('option', { key: a.id, value: a.id }, a.id + (a.appId ? ' (AppID ' + a.appId + ')' : '') + (a.disabled ? ' [停用]' : ''))
|
|
1167
|
+
}))
|
|
1168
|
+
: h('span', { style: { fontSize: 13, color: '#888' } }, '无账号… 去「账号与预设」添加'),
|
|
1169
|
+
ac ? h('span', { style: { fontSize: 12, color: '#6d28d9', whiteSpace: 'nowrap' } }, ac.dataDir ? '数据目录: ' + ac.dataDir : '') : null,
|
|
1170
|
+
h('span', { style: { flex: 1 } }),
|
|
1171
|
+
h('span', { style: { fontSize: 12, color: '#8a97ad' } }, '设置/图库/定时 都跟随上面选的机器人')),
|
|
1172
|
+
h('div', { style: { position: 'sticky', top: 0, zIndex: 30, background: 'transparent', padding: '10px 4px 12px', display: 'flex', gap: 10, flexWrap: 'nowrap', alignItems: 'stretch', marginBottom: 12 } },
|
|
1173
|
+
[['settings', '设置', '#ff5d5d'], ['gallery', '表情包图库', '#ffd97a'], ['timers', '定时任务', '#5b9dff'], ['accounts', '账号与预设', '#b197fc']].map(function (t) {
|
|
1174
|
+
var on = tab === t[0]
|
|
1175
|
+
var n = t[0] === 'gallery' ? badges.untagged : t[0] === 'timers' ? badges.timers : 0
|
|
1176
|
+
var col = t[2]
|
|
1177
|
+
return h('button', { key: t[0], onClick: function () { choose(t[0]) },
|
|
1178
|
+
style: {
|
|
1179
|
+
flex: 1, minWidth: 0, cursor: 'pointer', font: 'inherit',
|
|
1180
|
+
color: '#171a21', fontSize: 14, textAlign: 'center',
|
|
1181
|
+
whiteSpace: 'nowrap', padding: '9px 8px', borderRadius: 14,
|
|
1182
|
+
border: '1px solid ' + (on ? col : col + 'a6'),
|
|
1183
|
+
background: 'linear-gradient(165deg,' + col + (on ? '66' : '40') + ',' + col + (on ? '1f' : '14') + ')',
|
|
1184
|
+
boxShadow: on
|
|
1185
|
+
? '0 0 18px ' + col + 'cc, inset 0 0 16px ' + col + '66'
|
|
1186
|
+
: '0 0 10px ' + col + '73, inset 0 0 12px ' + col + '47',
|
|
1187
|
+
fontWeight: on ? 800 : 600,
|
|
1188
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7,
|
|
1189
|
+
transition: 'all .15s', outline: 'none',
|
|
1190
|
+
} },
|
|
1191
|
+
t[1], badge(n))
|
|
1192
|
+
})),
|
|
1193
|
+
sub)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ── 账号与预设(第4 tab): 多机器人账号(appId/secret/预设/cwd) + agent 预设复制管理 ──
|
|
1197
|
+
var API_ACCOUNTS = '/api/qqbot-settings/accounts'
|
|
1198
|
+
var API_PRESETS = '/api/qqbot-settings/presets'
|
|
1199
|
+
var API_BIND = '/api/qqbot-settings/bind'
|
|
1200
|
+
var API_WORKSPACES = '/api/qqbot-settings/workspaces'
|
|
1201
|
+
// appSecret 掩码(借鉴 dsh-qqbot-panel): host 只回显掩码, 留空/掩码保存=保留原值
|
|
1202
|
+
var SECRET_MASK = '********'
|
|
1203
|
+
var PRESET_HINT = '想改人设:点预设卡片「打开文件夹」,编辑里面 agent.cordis.yml —— persona 插件 config.text 那段就是人设正文(改完保存并重启 dsh 生效);preset.yml 的 name 是预设显示名。做新人格:选一个预设点「复制」,填「预设 id」(=文件夹名,字母/数字开头,可含 - _) 和「人设名」(中文随意),保存后自动生成新预设文件夹。'
|
|
1204
|
+
function inputRow(label, node) {
|
|
1205
|
+
return h('label', { style: { display: 'block', margin: '5px 0', fontSize: 13 } }, label, node)
|
|
1206
|
+
}
|
|
1207
|
+
function AccountsPage(props) {
|
|
1208
|
+
var [insts, setInsts] = useState(null)
|
|
1209
|
+
var [presets, setPresets] = useState([])
|
|
1210
|
+
var [root, setRoot] = useState('')
|
|
1211
|
+
var [workspaces, setWorkspaces] = useState([])
|
|
1212
|
+
var [msg, setMsg] = useState('')
|
|
1213
|
+
var [busy, setBusy] = useState('')
|
|
1214
|
+
var [bind, setBind] = useState(null) // {id,url,status,error,iframeUrl}
|
|
1215
|
+
var [copy, setCopy] = useState(null) // 复制弹层: {sourceId}
|
|
1216
|
+
var [copyId, setCopyId] = useState('')
|
|
1217
|
+
var [copyName, setCopyName] = useState('')
|
|
1218
|
+
var pollRef = React.useRef(null)
|
|
1219
|
+
function load(quiet) {
|
|
1220
|
+
Promise.all([
|
|
1221
|
+
fetch(API_ACCOUNTS).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
1222
|
+
fetch(API_PRESETS).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
1223
|
+
fetch(API_WORKSPACES).then(function (r) { return r.json() }).catch(function () { return {} }),
|
|
1224
|
+
]).then(function (rs) {
|
|
1225
|
+
var a = rs[0] || {}
|
|
1226
|
+
if (Array.isArray(a.instances)) setInsts(a.instances)
|
|
1227
|
+
var p = rs[1] || {}
|
|
1228
|
+
if (Array.isArray(p.presets)) { setPresets(p.presets); if (p.root) setRoot(p.root) }
|
|
1229
|
+
var w = rs[2] || {}
|
|
1230
|
+
if (Array.isArray(w.workspaces)) setWorkspaces(w.workspaces)
|
|
1231
|
+
if (!quiet) setMsg('')
|
|
1232
|
+
})
|
|
1233
|
+
}
|
|
1234
|
+
useEffect(function () { load(true) }, [])
|
|
1235
|
+
// 自动填充: 账号没选 preset 时自动带上候选中的第一个(浏览器 select 默认显示 ≠ state 值,
|
|
1236
|
+
// 不真正写入 state 的话保存会丢 preset——主人实测: 不手动切换选择就保存不了)。
|
|
1237
|
+
// 与渲染处同规则算候选(host 没给 hasChannelTools 字段时视全部为候选, 不锁死)。
|
|
1238
|
+
// 候选数量不限: 有候选就把空缺账号填上第一个, 用户仍可手动改选。
|
|
1239
|
+
useEffect(function () {
|
|
1240
|
+
if (!Array.isArray(presets) || presets.length === 0) return
|
|
1241
|
+
if (!Array.isArray(insts) || insts.length === 0) return
|
|
1242
|
+
var know = presets.every(function (p) { return p.hasChannelTools === true || p.hasChannelTools === false })
|
|
1243
|
+
var only = know ? presets.filter(function (p) { return p.hasChannelTools }) : presets
|
|
1244
|
+
if (only.length === 0) return
|
|
1245
|
+
var changed = false
|
|
1246
|
+
var next = insts.map(function (x) {
|
|
1247
|
+
if (x.preset) return x
|
|
1248
|
+
changed = true
|
|
1249
|
+
return Object.assign({}, x, { preset: only[0].id })
|
|
1250
|
+
})
|
|
1251
|
+
if (changed) setInsts(next)
|
|
1252
|
+
}, [insts, presets])
|
|
1253
|
+
useEffect(function () { return function () { if (pollRef.current) clearInterval(pollRef.current) } }, [])
|
|
1254
|
+
function upd(i, patch) { setInsts(function (list) { var n = list.slice(); n[i] = Object.assign({}, n[i], patch); return n }) }
|
|
1255
|
+
function addBlank() {
|
|
1256
|
+
var base = 'im-qqbot'
|
|
1257
|
+
var used = new Set((insts || []).map(function (x) { return x.id }))
|
|
1258
|
+
var n = 2
|
|
1259
|
+
while (used.has(base + '-' + n)) n += 1
|
|
1260
|
+
setInsts(function (list) { return (list || []).concat([{ id: base + '-' + n, appId: '', appSecret: '', preset: '', cwd: '', disabled: false, _new: true }]) })
|
|
1261
|
+
}
|
|
1262
|
+
function addBound(cred) {
|
|
1263
|
+
var base = 'im-qqbot'
|
|
1264
|
+
var used = new Set((insts || []).map(function (x) { return x.id }))
|
|
1265
|
+
var n = 2
|
|
1266
|
+
while (used.has(base + '-' + n)) n += 1
|
|
1267
|
+
setInsts(function (list) { return (list || []).concat([{ id: base + '-' + n, appId: cred.appId, appSecret: cred.appSecret, preset: '', cwd: '', disabled: false, _bound: true }]) })
|
|
1268
|
+
}
|
|
1269
|
+
function save() {
|
|
1270
|
+
if (busy) return
|
|
1271
|
+
setBusy('save'); setMsg('保存中…')
|
|
1272
|
+
var list = (insts || []).filter(function (x) { return x.id && (x.appId || x.appSecret || x.hasSecret || !x._new || x._bound) })
|
|
1273
|
+
// 空账号(全空且没绑定)不保存; 全新手动号若两凭据都空则提示(hasSecret=已保存的账号不算空)
|
|
1274
|
+
var blanks = list.filter(function (x) { return !x.appSecret && !x.hasSecret && !x._bound })
|
|
1275
|
+
if (blanks.length && !window.confirm('有 ' + blanks.length + ' 个账号没填 appSecret(不填的会被保存为只有骨架的实例, 可能启动就弹扫码)。继续?')) { setBusy(''); return }
|
|
1276
|
+
// 保存兜底: preset 仍为空的账号自动填第一个候选(防 effect 未触发/用户未手动选导致保存丢 preset)
|
|
1277
|
+
var knowNow = presets.length > 0 && presets.every(function (p) { return p.hasChannelTools === true || p.hasChannelTools === false })
|
|
1278
|
+
var candNow = knowNow ? presets.filter(function (p) { return p.hasChannelTools }) : presets
|
|
1279
|
+
if (candNow.length > 0) {
|
|
1280
|
+
var autoId = candNow[0].id
|
|
1281
|
+
list = list.map(function (x) { return x.preset ? x : Object.assign({}, x, { preset: autoId }) })
|
|
1282
|
+
}
|
|
1283
|
+
fetch(API_ACCOUNTS + '/save', {
|
|
1284
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1285
|
+
body: JSON.stringify({ instances: list.map(function (x) { return { id: x.id, appId: x.appId, appSecret: x.appSecret, preset: x.preset, cwd: x.cwd, disabled: !!x.disabled } }) }),
|
|
1286
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
1287
|
+
setBusy('')
|
|
1288
|
+
if (d && d.error) setMsg('保存失败: ' + d.error)
|
|
1289
|
+
else setMsg('已保存到 cordis.patch.yml ✓ —— 重启 dsh(web) 后生效')
|
|
1290
|
+
load(true)
|
|
1291
|
+
}).catch(function (e) { setBusy(''); setMsg('保存异常: ' + e.message) })
|
|
1292
|
+
}
|
|
1293
|
+
function removeInst(i) {
|
|
1294
|
+
var list = (insts || []).slice()
|
|
1295
|
+
var one = list[i]
|
|
1296
|
+
if (!window.confirm('删除账号实例 ' + one.id + '? (它的配置会从 cordis.patch.yml 移除)')) return
|
|
1297
|
+
list.splice(i, 1)
|
|
1298
|
+
setInsts(list)
|
|
1299
|
+
}
|
|
1300
|
+
// ── 扫码绑定 ──
|
|
1301
|
+
function startBind() {
|
|
1302
|
+
setBind({ id: '', url: '', status: 'start', error: '' })
|
|
1303
|
+
fetch(API_BIND + '/start', { method: 'POST' }).then(function (r) { return r.json() }).then(function (d) {
|
|
1304
|
+
if (d && d.error) { setBind({ id: '', url: '', status: 'error', error: d.error }); return }
|
|
1305
|
+
var b = { id: d.id, url: d.url, status: 'wait', error: '', iframeUrl: d.url }
|
|
1306
|
+
setBind(b)
|
|
1307
|
+
if (pollRef.current) clearInterval(pollRef.current)
|
|
1308
|
+
pollRef.current = setInterval(function () {
|
|
1309
|
+
fetch(API_BIND + '/poll?id=' + encodeURIComponent(b.id)).then(function (r) { return r.json() }).then(function (p) {
|
|
1310
|
+
if (p.status === 'done') {
|
|
1311
|
+
clearInterval(pollRef.current); pollRef.current = null
|
|
1312
|
+
addBound({ appId: p.appId, appSecret: p.appSecret })
|
|
1313
|
+
setMsg('🎉 扫码绑定成功! 平台下发的 AppID/Secret 已填好, 起个名字保存即可')
|
|
1314
|
+
setBind(null)
|
|
1315
|
+
} else if (p.status === 'error') {
|
|
1316
|
+
clearInterval(pollRef.current); pollRef.current = null
|
|
1317
|
+
setBind({ id: b.id, url: p.url || b.url, status: 'error', error: p.error || '绑定失败', iframeUrl: p.url || b.url })
|
|
1318
|
+
} else {
|
|
1319
|
+
setBind(function (old) { return old && old.id === b.id ? Object.assign({}, old, { url: p.url || old.url, iframeUrl: (p.url && p.url !== old.url) ? p.url : old.iframeUrl }) : old })
|
|
1320
|
+
}
|
|
1321
|
+
}).catch(function () {})
|
|
1322
|
+
}, 2500)
|
|
1323
|
+
}).catch(function (e) { setBind({ id: '', url: '', status: 'error', error: e.message }) })
|
|
1324
|
+
}
|
|
1325
|
+
function cancelBind() {
|
|
1326
|
+
if (bind && bind.id) { try { fetch(API_BIND + '/cancel', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: bind.id }) }) } catch (e) {} }
|
|
1327
|
+
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null }
|
|
1328
|
+
setBind(null)
|
|
1329
|
+
}
|
|
1330
|
+
// ── 预设复制 ──
|
|
1331
|
+
function doCopy() {
|
|
1332
|
+
if (!copy) return
|
|
1333
|
+
setBusy('copy')
|
|
1334
|
+
var url = copy.fromStandard ? API_PRESETS + '/new' : API_PRESETS + '/copy'
|
|
1335
|
+
var payload = copy.fromStandard
|
|
1336
|
+
? { id: copyId.trim(), name: copyName.trim() }
|
|
1337
|
+
: { sourceId: copy.sourceId, newId: copyId.trim(), newName: copyName.trim() }
|
|
1338
|
+
fetch(url, {
|
|
1339
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1340
|
+
body: JSON.stringify(payload),
|
|
1341
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
1342
|
+
setBusy('')
|
|
1343
|
+
if (d && d.error) { setMsg('复制失败: ' + d.error); return }
|
|
1344
|
+
setMsg('✅ 新预设已生成: ' + d.newId + (copy.fromStandard ? ' (复制自标准模式, 已带 QQ 工具)' : '') + '。去「打开文件夹」改人设吧。')
|
|
1345
|
+
setCopy(null); setCopyId(''); setCopyName('')
|
|
1346
|
+
load(true)
|
|
1347
|
+
}).catch(function (e) { setBusy(''); setMsg('复制异常: ' + e.message) })
|
|
1348
|
+
}
|
|
1349
|
+
function openPreset(id) {
|
|
1350
|
+
fetch(API_PRESETS + '/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: id }) })
|
|
1351
|
+
.then(function (r) { return r.json() }).then(function (d) { if (d && d.ok) setMsg('已在本机打开文件夹'); else if (d && d.error) setMsg(d.error) })
|
|
1352
|
+
.catch(function (e) { setMsg('打开失败: ' + e.message) })
|
|
1353
|
+
}
|
|
1354
|
+
var st = { width: '100%', padding: '5px 8px', boxSizing: 'border-box' }
|
|
1355
|
+
return h('div', { style: { maxWidth: 860 } },
|
|
1356
|
+
h('h2', null, '账号与预设'),
|
|
1357
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, '一个 QQ 机器人 = 一个账号实例(AppID/AppSecret)。想同时开几个机器人就加几个实例,每个可以配不同的 agent 预设(人格)与工作目录(各人各数据,不串)。⚠️ appSecret 会明文存在本机 cordis.patch.yml(改前自动备份 .bak),别把配置文件传上网。'),
|
|
1358
|
+
h('div', { style: sectionTitle }, '① 机器人账号(改完点保存, 重启生效)'),
|
|
1359
|
+
(insts === null ? h('p', null, '加载中…') : null),
|
|
1360
|
+
(insts || []).map(function (it, i) {
|
|
1361
|
+
// 账号可选预设: 有 QQ 工具的优先。容错: host 若还没返回 hasChannelTools(旧版未重启), 则不锁死、照常全列, 保证能选能填;
|
|
1362
|
+
// 一旦 host 给全了该字段, 就只列带 QQ 工具(不带行的选了在 QQ 跑不起来)。
|
|
1363
|
+
var knowChannel = presets.length > 0 && presets.every(function (p) { return p.hasChannelTools === true || p.hasChannelTools === false })
|
|
1364
|
+
var usable = knowChannel ? presets.filter(function (p) { return p.hasChannelTools }) : presets.slice()
|
|
1365
|
+
var selInvalid = it.preset && !usable.some(function (p) { return p.id === it.preset })
|
|
1366
|
+
var opts = usable.map(function (p) { return { id: p.id, label: p.name + ' (' + p.id + ')' } })
|
|
1367
|
+
if (selInvalid) opts.unshift({ id: it.preset, label: '⚠️ ' + it.preset + '(不带QQ工具, 无法在QQ用)' })
|
|
1368
|
+
return h('div', { key: it.id, style: { border: '1px solid ' + (it.disabled ? '#ffa94d66' : '#00000026'), borderRadius: 10, padding: '8px 12px', margin: '8px 0', background: 'rgba(255,255,255,.5)' } },
|
|
1369
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1370
|
+
h('span', { style: { fontWeight: 800, color: '#171a21' } }, it._bound ? '📱 扫码绑定' : (it._new ? '🆕 新账号' : '🤖 账号')),
|
|
1371
|
+
h('input', { className: 'qqs-inp', style: Object.assign({ width: 170 }, st), value: it.id, disabled: !it._new && !it._bound, onChange: function (e) { upd(i, { id: e.target.value }) }, placeholder: '实例 id(如 im-qqbot-2)' }),
|
|
1372
|
+
h('label', { style: { fontSize: 13, whiteSpace: 'nowrap' } },
|
|
1373
|
+
h('input', { className: 'qqs-cb', type: 'checkbox', checked: !it.disabled, onChange: function (e) { upd(i, { disabled: !e.target.checked }) } }), ' 启用'),
|
|
1374
|
+
h('button', { className: 'qqs-btn', onClick: function () { removeInst(i) }, style: { marginLeft: 'auto' } }, '删除')),
|
|
1375
|
+
inputRow('AppID: ', h('input', { className: 'qqs-inp', style: Object.assign({ width: 220 }, st), value: it.appId || '', onChange: function (e) { upd(i, { appId: e.target.value }) }, placeholder: '如 1234567890' })),
|
|
1376
|
+
inputRow('AppSecret: ', h('input', { className: 'qqs-inp', type: 'password', style: Object.assign({ width: 320 }, st), value: (it.appSecret === SECRET_MASK || (!it.appSecret && it.hasSecret)) ? '' : (it.appSecret || ''), onChange: function (e) { upd(i, { appSecret: e.target.value }) }, placeholder: (it.hasSecret && !it._new) ? '已保存(留空/掩码=保留原值; 填新值=更换)' : '扫码绑定会自动填; 也可手动填(两个都要填全才不弹码)' })),
|
|
1377
|
+
inputRow('Agent 预设(人格): ', (function () { if (usable.length === 0 && !selInvalid) {
|
|
1378
|
+
return h('span', { className: 'qqs-inp', style: { display: 'inline-block', verticalAlign: 'middle', padding: '5px 10px', fontSize: 12, color: '#e8590c', background: '#fff5f0', borderRadius: 6 } }, '⚠️ 还没有可用的 Agent 预设——请在下方②复制一个(如 whale-girl / whitegirl), 复制会自动带 QQ 工具。')
|
|
1379
|
+
} return h('select', { className: 'qqs-inp', style: { width: 320, padding: '4px 8px' }, value: it.preset || '', onChange: function (e) { upd(i, { preset: e.target.value }) } }, opts.map(function (o) { return h('option', { key: o.id, value: o.id, style: selInvalid && o.id === it.preset ? { color: '#e03131' } : null }, o.label) })) })()),
|
|
1380
|
+
inputRow('工作目录(各账号数据放这, 留空=默认): ', h('input', { className: 'qqs-inp', style: Object.assign({ width: '90%' }, st), value: it.cwd || '', onChange: function (e) { upd(i, { cwd: e.target.value }) }, placeholder: '如 D:\\bots\\二号机' })),
|
|
1381
|
+
(function () {
|
|
1382
|
+
var ws = (workspaces || []).filter(function (w) { return w.path && w.count > 0 })
|
|
1383
|
+
if (ws.length === 0) return null
|
|
1384
|
+
return h('div', { style: { margin: '2px 0 4px', fontSize: 12, color: '#555' } },
|
|
1385
|
+
'📁 已有 dsh 会话的工作区(点选填入上方): ',
|
|
1386
|
+
ws.map(function (w) {
|
|
1387
|
+
return h('button', { key: w.dir, className: 'qqs-btn', style: { fontSize: 11, padding: '1px 8px', margin: '2px 4px 2px 0' }, onClick: function () { upd(i, { cwd: w.path }) } },
|
|
1388
|
+
(w.path.length > 46 ? w.path.slice(0, 43) + '…' : w.path) + ' (' + w.count + ')')
|
|
1389
|
+
}))
|
|
1390
|
+
})())
|
|
1391
|
+
}),
|
|
1392
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', margin: '8px 0' } },
|
|
1393
|
+
h('button', { className: 'qqs-btn', onClick: addBlank }, '+ 添加账号(手动填凭据)'),
|
|
1394
|
+
h('button', { className: 'qqs-btn', onClick: startBind, style: { borderColor: '#b197fc99', color: '#171a21' } }, '📱 扫码绑定新机器人(推荐)'),
|
|
1395
|
+
h('span', { style: { flex: 1 } }),
|
|
1396
|
+
h('button', { className: 'qqs-btn', onClick: save, disabled: busy === 'save' }, '保存账号配置'),
|
|
1397
|
+
h('button', { className: 'qqs-btn', onClick: function () { load() } }, '重新读取')),
|
|
1398
|
+
h('div', { style: { ...sectionTitle, marginTop: 16, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
1399
|
+
'② Agent 预设(人格库)',
|
|
1400
|
+
h('button', { className: 'qqs-btn', style: { fontSize: 12, padding: '2px 10px', borderColor: '#2b8a3e99', color: '#171a21' }, onClick: function () { setCopy({ fromStandard: true, sourceId: 'standard' }); setCopyId(''); setCopyName('') } }, '➕ 从标准模式新建(带QQ工具)')),
|
|
1401
|
+
h('p', { style: { fontSize: 12, color: '#888' } }, PRESET_HINT),
|
|
1402
|
+
(presets.filter(function (p) { return !p.hasChannelTools }).length > 0
|
|
1403
|
+
? h('p', { style: { fontSize: 12, color: '#e8590c', background: '#fff5f0', borderRadius: 6, padding: '6px 10px' } }, '💡 选一个预设点「复制」生成的副本会自动带上 QQ 工具(send_media/发图等), 之后就能在①账号里给机器人选用了。复制源无所谓, 只要是能跑的人格。')
|
|
1404
|
+
: null),
|
|
1405
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(190px,1fr))', gap: 10, margin: '8px 0' } },
|
|
1406
|
+
presets.map(function (p) {
|
|
1407
|
+
return h('div', { key: p.id, style: { border: '1px solid #00000026', borderRadius: 10, padding: '10px 12px', background: 'rgba(255,255,255,.5)' } },
|
|
1408
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1409
|
+
h('span', { style: { fontWeight: 800, color: '#171a21' } }, p.name),
|
|
1410
|
+
h('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 8, color: '#fff', background: p.hasChannelTools ? '#2b8a3e' : '#e8590c' } }, p.hasChannelTools ? 'QQ可用' : '无QQ工具')),
|
|
1411
|
+
h('div', { style: { fontSize: 12, color: '#666', margin: '4px 0' } }, 'id=' + p.id),
|
|
1412
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1413
|
+
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12 }, onClick: function () { setCopy({ sourceId: p.id }); setCopyId(''); setCopyName('') } }, '复制'),
|
|
1414
|
+
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12 }, onClick: function () { openPreset(p.id) } }, '📂 打开文件夹')))
|
|
1415
|
+
})),
|
|
1416
|
+
copy ? h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: function () { if (!busy) setCopy(null) } },
|
|
1417
|
+
h('div', { className: 'qqs-modal', style: { background: '#22252e', color: '#e8e8e8', borderRadius: 12, padding: 14, maxWidth: 460, width: '92%' }, onClick: function (e) { e.stopPropagation() } },
|
|
1418
|
+
h('h3', { style: { marginTop: 0 } }, copy.fromStandard ? '从标准模式新建预设' : '复制预设: ' + copy.sourceId),
|
|
1419
|
+
inputRow('预设 id(=新文件夹名, 字母/数字开头, 可含 - _): ', h('input', { className: 'qqs-inp', style: { width: '100%', boxSizing: 'border-box' }, value: copyId, onChange: function (e) { setCopyId(e.target.value) }, placeholder: '如 my-girl-2' })),
|
|
1420
|
+
inputRow('人设名(显示用, 中文随意): ', h('input', { className: 'qqs-inp', style: { width: '100%', boxSizing: 'border-box' }, value: copyName, onChange: function (e) { setCopyName(e.target.value) }, placeholder: '如 我的二号人格' })),
|
|
1421
|
+
h('div', { style: { textAlign: 'right', marginTop: 10 } },
|
|
1422
|
+
h('button', { className: 'qqs-btn', onClick: function () { if (!busy) setCopy(null) }, style: { marginRight: 8 } }, '取消'),
|
|
1423
|
+
h('button', { className: 'qqs-btn', onClick: doCopy, disabled: busy === 'copy' }, '生成新预设')))) : null,
|
|
1424
|
+
bind ? h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
1425
|
+
h('div', { className: 'qqs-modal', style: { background: '#22252e', color: '#e8e8e8', borderRadius: 12, padding: 14, maxWidth: 620, width: '94%' }, onClick: function (e) { e.stopPropagation() } },
|
|
1426
|
+
h('h3', { style: { marginTop: 0 } }, bind.status === 'error' ? '绑定失败' : '扫码绑定 QQ 机器人'),
|
|
1427
|
+
bind.status === 'error'
|
|
1428
|
+
? h('p', { style: { color: '#ffa94d' } }, '失败: ' + (bind.error || '未知'))
|
|
1429
|
+
: h('div', null,
|
|
1430
|
+
h('p', { style: { fontSize: 13, color: '#cfd3dc' } }, '这个授权页只认「手机 QQ」打开(终端启动时是拿手机扫屏幕上的码 = 同一个链接)。请照做:'),
|
|
1431
|
+
h('ol', { style: { fontSize: 13, color: '#cfd3dc', margin: '4px 0 8px', paddingLeft: 20 } },
|
|
1432
|
+
h('li', null, '点「复制链接」,把链接发到手机 QQ(如发给"文件传输助手")'),
|
|
1433
|
+
h('li', null, '在手机 QQ 里点开链接,按页面提示完成机器人绑定(没有第二个机器人,就在页面里选/建一个)'),
|
|
1434
|
+
h('li', null, '完成后本页自动收到并填好 AppID/AppSecret')),
|
|
1435
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', background: '#ffffff12', border: '1px solid #ffffff26', borderRadius: 8, padding: '6px 8px' } },
|
|
1436
|
+
h('span', { style: { flex: 1, fontSize: 11, color: '#9fb0c9', wordBreak: 'break-all', maxHeight: 60, overflow: 'auto' } }, bind.url),
|
|
1437
|
+
h('button', { className: 'qqs-btn', onClick: function () { try { navigator.clipboard.writeText(bind.url).then(function () { setBind(function (o) { return o ? Object.assign({}, o, { copied: true }) : o }) }).catch(function () {}) } catch (e) {} } }, bind.copied ? '已复制 ✓' : '复制链接')),
|
|
1438
|
+
h('p', { style: { fontSize: 11, color: '#8a97ad' } }, '提示: 二维码内容就是这个链接——手机 QQ 扫电脑屏的码 = 在手机 QQ 里点开它, 效果相同。等待中…(过期会自动刷新, 重新复制最新链接即可)')),
|
|
1439
|
+
h('div', { style: { textAlign: 'right', marginTop: 8 } },
|
|
1440
|
+
h('button', { className: 'qqs-btn', onClick: cancelBind }, '取消绑定')))) : null,
|
|
1441
|
+
msg ? h('p', { style: { fontSize: 13, color: '#1d7a31', fontWeight: 600 } }, msg) : null,
|
|
1442
|
+
props && props.close ? h('button', { className: 'qqs-btn', onClick: props.close, style: { float: 'right', marginTop: 6 } }, '完成') : null)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180deg,#ffffff,#f5f6f8);border:1px solid #d0d5dd;border-radius:9px;padding:5px 13px;cursor:pointer;transition:all .15s}.qqs-btn:hover{background:#ffffff;border-color:#7c6cf0;color:#7c6cf0}.qqs-btn:active{transform:translateY(1px)}.qqs-inp,.qqs-area{font:inherit;color:#1f2329;background:#ffffff;border:1px solid #d0d5dd;border-radius:9px;padding:5px 10px;outline:none;transition:border-color .15s}.qqs-inp::placeholder,.qqs-area::placeholder{color:#9aa1ab}.qqs-inp:focus,.qqs-area:focus{border-color:#7c6cf0;box-shadow:0 0 0 2px #7c6cf033}.qqs-cb{accent-color:#7c6cf0;width:15px;height:15px}.qqs-card{transition:box-shadow .15s,border-color .15s}.qqs-card:hover{box-shadow:0 2px 10px #0002}.qqs-panel{background:linear-gradient(180deg,#ffffff,#f7f8fa);border:1px solid #e2e5ea;border-radius:16px;padding:16px;box-shadow:0 10px 34px #0002}.qqs-toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;background:#f7f8fa;border:1px solid #e2e5ea;border-radius:12px;padding:8px 10px;margin-bottom:10px}.qqs-modal{background:#ffffff;color:#1f2329}"
|
|
1446
|
+
function ensureCss() { try { if (!document.getElementById('qqs-css')) { var st = document.createElement('style'); st.id = 'qqs-css'; st.textContent = QQS_CSS; document.head.appendChild(st) } } catch (e) {} }
|
|
1447
|
+
|
|
1448
|
+
function apply(ctx) {
|
|
1449
|
+
ensureCss()
|
|
1450
|
+
var slots = ctx.slots
|
|
1451
|
+
if (!slots) { console.warn('[qqbot-settings] slots unavailable'); return }
|
|
1452
|
+
slots.inject('settings.section', function () {
|
|
1453
|
+
return slots.register(
|
|
1454
|
+
{ name: 'settings.section', id: 'qqbot', order: 32, label: 'QQ 机器人' },
|
|
1455
|
+
function (props) { return h(QqbotHome, { close: props && props.close }) })
|
|
1456
|
+
})
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
exports.inject = ['slots']
|
|
1460
|
+
exports.apply = apply
|
|
1461
|
+
return module.exports
|
|
1462
|
+
}
|
|
1463
|
+
})
|