@fg-lab/amadeus-qq 0.0.0-stage → 1.2.2
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 +54 -2
- package/README.zh-CN.md +51 -0
- package/REFERENCE.md +49 -0
- package/assets/market-icon.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/COMPATIBILITY.md +17 -0
- package/docs/DATA_MIGRATION.md +26 -0
- package/docs/INSTALL_V1_2.md +22 -0
- package/docs/PUBLISHING.md +28 -0
- package/docs/RELEASE_CHECKLIST.md +23 -0
- package/docs/RELEASE_NOTES.md +38 -0
- package/docs/UPGRADE_POLICY.md +26 -0
- package/docs/V1_SCOPE.md +33 -0
- package/docs/screenshots/account-browser.png +0 -0
- package/docs/screenshots/account-dark-browser.png +0 -0
- package/docs/screenshots/global-desktop.png +0 -0
- package/docs/screenshots/overview-desktop.png +0 -0
- package/docs/screenshots/product-release-20261031.png +0 -0
- package/docs/screenshots/v1.2-overview.png +0 -0
- package/docs/v1.1/PRESET_MATRIX.md +28 -0
- package/docs/v1.1/PROTOTYPE_DIFF.md +15 -0
- package/docs/v1.1/TEST_REPORT.md +20 -0
- package/docs/v1.1/UPGRADE.md +10 -0
- package/docs/verification/IMAGE_READ_FIX_2026-11-07.md +22 -0
- package/docs/verification/LIFE_QQ_BOUNDARY_2026-11-07.md +20 -0
- package/docs/verification/V1_REAL_NAPCAT_2026-10-27.md +43 -0
- package/lib/client.js +380 -0
- package/package.json +92 -4
- package/preset.patch.yml +166 -0
- package/screenshots.json +6 -0
- package/scripts/build-client.mjs +52 -0
- package/scripts/registry-guard.mjs +12 -0
- package/scripts/release-audit.mjs +59 -0
- package/scripts/symlink-security.mjs +8 -0
- package/src/account-profile.js +223 -0
- package/src/admin-http.js +361 -0
- package/src/audio.js +428 -0
- package/src/capabilities.js +785 -0
- package/src/client/factory.template.js +82 -0
- package/src/client/logic.js +290 -0
- package/src/config.js +50 -0
- package/src/console/console.css +28 -0
- package/src/console/console.js +541 -0
- package/src/console/index.html +67 -0
- package/src/core.js +909 -0
- package/src/files.js +230 -0
- package/src/index.js +994 -0
- package/src/lease.js +42 -0
- package/src/media.js +309 -0
- package/src/napcat-action-names.js +186 -0
- package/src/native-asr.js +112 -0
- package/src/onebot.js +378 -0
- package/src/outbound-media.js +309 -0
- package/src/owner-workspace.js +394 -0
- package/src/permissions.js +270 -0
- package/src/preset-guard.mjs +593 -0
- package/src/qzone-provider.js +628 -0
- package/src/session-provenance.js +74 -0
- package/src/setup.js +255 -0
- package/src/storage.js +580 -0
- package/src/web-console.js +9 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const ROOT = '/api/amadeus-qq'
|
|
4
|
+
const pages = {
|
|
5
|
+
overview: ['总览首页', '连接状态、会话和首次配置。'],
|
|
6
|
+
scenes: ['会话管理', '每个授权群聊或私聊都有独立的 DSH Session 和权限。'],
|
|
7
|
+
features: ['功能中心', '按实际执行路径与 Provider 状态查看 QQ 能力。'],
|
|
8
|
+
account: ['账号与 QQ 空间', '账号级能力由可信 Owner 单独授权。'],
|
|
9
|
+
global: ['全局设置', '选择 Workspace、连接 NapCat 并管理白名单。'],
|
|
10
|
+
diagnostics: ['运行与诊断', '检查实际连接、Session 和权限边界。'],
|
|
11
|
+
}
|
|
12
|
+
const modes = [
|
|
13
|
+
['observe', '仅观察', '读取入站,不调用模型或发送'],
|
|
14
|
+
['safe-chat', '安全聊天', '文字聊天与同一会话续接'],
|
|
15
|
+
['enhanced', '增强对话', '媒体、回应与搜索能力'],
|
|
16
|
+
['autonomous-social', '自主群友', '允许新入站消息触发参与判断'],
|
|
17
|
+
['owner-agent', 'Owner Agent', '仅可信管理员私聊'],
|
|
18
|
+
]
|
|
19
|
+
const groups = [
|
|
20
|
+
['聊天与媒体', ['qq.read', 'qq.send', 'qq.media.read', 'qq.reaction.write', 'qq.poke', 'qq.recall']],
|
|
21
|
+
['Amadeus Life', ['amadeus.life.read']],
|
|
22
|
+
['群管理', ['qq.group.essence', 'qq.group.notice', 'qq.group.card', 'qq.group.ban', 'qq.group.whole_ban', 'qq.group.kick', 'qq.group.name', 'qq.group.admin', 'qq.group.special_title']],
|
|
23
|
+
['Agent 与目录', ['dsh.search', 'qq.owner.workspace', 'qq.owner.tools', 'qq.friends.read', 'qq.groups.read']],
|
|
24
|
+
]
|
|
25
|
+
const behaviorSettings = [
|
|
26
|
+
['activity', '参与活跃度', [['low', '低'], ['medium', '适中'], ['high', '较活跃']]],
|
|
27
|
+
['quietWindowMs', '攒消息静默窗口', [[0, '立即处理'], [5000, '5 秒'], [10000, '10 秒'], [12000, '12 秒'], [15000, '15 秒']]],
|
|
28
|
+
['waitTimeoutMs', '本轮等待续聊上限', [[0, '不等待'], [15000, '15 秒'], [30000, '30 秒'], [60000, '60 秒'], [120000, '120 秒']]],
|
|
29
|
+
['replyCooldownMs', '自主接话冷却间隔', [[0, '关闭'], [10000, '10 秒'], [15000, '15 秒'], [30000, '30 秒'], [60000, '60 秒']]],
|
|
30
|
+
['contextBudget', '近期上下文条数', [[10, '10 条'], [20, '20 条'], [40, '40 条']]],
|
|
31
|
+
]
|
|
32
|
+
const labels = {
|
|
33
|
+
'qq.read': '读取 QQ 消息', 'qq.send': '发送 QQ 消息', 'qq.media.read': '读取媒体附件',
|
|
34
|
+
'qq.reaction.write': '表情回应', 'qq.poke': '戳一戳', 'qq.recall': '撤回消息',
|
|
35
|
+
'qq.group.essence': '群精华', 'qq.group.notice': '群公告', 'qq.group.card': '群名片',
|
|
36
|
+
'qq.group.ban': '禁言成员', 'qq.group.whole_ban': '全员禁言', 'qq.group.kick': '移出成员',
|
|
37
|
+
'qq.group.name': '群名称', 'qq.group.admin': '设置群管理员', 'qq.group.special_title': '群头衔',
|
|
38
|
+
'dsh.search': 'DSH 搜索', 'qq.owner.workspace': 'Owner 文件工作区',
|
|
39
|
+
'qq.owner.tools': 'Owner Host 工具', 'qq.friends.read': '好友目录', 'qq.groups.read': '群目录',
|
|
40
|
+
'amadeus.life.read': '读取本会话 Life 概况',
|
|
41
|
+
'qq.profile.write': '账号资料修改', 'qq.qzone.use': 'QQ 空间统一操作',
|
|
42
|
+
'qq.qzone.read': 'QQ 空间读取', 'qq.qzone.publish': 'QQ 空间发布',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const $ = id => document.getElementById(id)
|
|
46
|
+
const state = { setup: null, runtime: null, selected: '', draft: null, page: 'overview', busy: false }
|
|
47
|
+
const openGroups = new Set()
|
|
48
|
+
|
|
49
|
+
function transportText(value) {
|
|
50
|
+
return { connected: '已连接', connecting: '连接中', disconnected: '未连接', disabled: '已暂停' }[value] ?? '未连接'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function el(tag, className, text) {
|
|
54
|
+
const node = document.createElement(tag)
|
|
55
|
+
if (className) node.className = className
|
|
56
|
+
if (text !== undefined) node.textContent = String(text)
|
|
57
|
+
return node
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function notice(text, bad = false) {
|
|
61
|
+
const node = $('notice')
|
|
62
|
+
node.textContent = text
|
|
63
|
+
node.classList.remove('hide')
|
|
64
|
+
node.classList.toggle('notice-bad', bad)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function api(path, body) {
|
|
68
|
+
const response = await fetch(ROOT + path, {
|
|
69
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
70
|
+
credentials: 'same-origin',
|
|
71
|
+
headers: body === undefined ? {} : { 'content-type': 'application/json', 'x-amadeus-qq': '1' },
|
|
72
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
73
|
+
})
|
|
74
|
+
const payload = await response.json()
|
|
75
|
+
if (!response.ok || payload.ok !== true) {
|
|
76
|
+
const error = new Error(payload.error || '请求未完成')
|
|
77
|
+
error.code = payload.code
|
|
78
|
+
error.status = response.status
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
return payload.data
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function show(page) {
|
|
85
|
+
if (!pages[page]) return
|
|
86
|
+
state.page = page
|
|
87
|
+
for (const [name] of Object.entries(pages)) $('view-' + name).classList.toggle('active', name === page)
|
|
88
|
+
for (const button of document.querySelectorAll('[data-page]')) button.classList.toggle('active', button.dataset.page === page)
|
|
89
|
+
$('page-title').textContent = pages[page][0]
|
|
90
|
+
$('page-subtitle').textContent = pages[page][1]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function splitIds(value) {
|
|
94
|
+
return [...new Set(value.split(/[,,\s]+/).map(x => x.trim()).filter(Boolean))]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sceneRows() {
|
|
98
|
+
const setup = state.setup
|
|
99
|
+
if (!setup?.botId) return []
|
|
100
|
+
const observed = new Map((state.runtime?.scenes ?? []).map(row => [row.key, row]))
|
|
101
|
+
const rows = []
|
|
102
|
+
for (const [kind, ids] of [['group', setup.groups], ['private', setup.privatePeers]]) {
|
|
103
|
+
for (const peerId of ids ?? []) {
|
|
104
|
+
const key = `${setup.botId}:${kind}:${peerId}`
|
|
105
|
+
const row = observed.get(key) ?? { key, scene: { botId: setup.botId, kind, peerId },
|
|
106
|
+
revision: 0, effectiveMode: 'safe-chat', permissions: null, capabilities: [],
|
|
107
|
+
health: { sessionActive: false }, ownerEligible: kind === 'private' && peerId === setup.ownerId }
|
|
108
|
+
rows.push({ ...row, paused: setup.pausedScenes?.includes(key) === true })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return rows
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderOverview() {
|
|
115
|
+
const setup = state.setup ?? {}
|
|
116
|
+
const runtime = state.runtime ?? {}
|
|
117
|
+
const rows = sceneRows()
|
|
118
|
+
const workspace = setup.workspaces?.find(item => item.id === setup.workspaceId)
|
|
119
|
+
$('overview-plugin').textContent = setup.enabled ? '已启用' : '待配置'
|
|
120
|
+
$('overview-transport').textContent = setup.connect === false ? 'Amadeus 已暂停' : `NapCat ${transportText(runtime.health?.transport)}`
|
|
121
|
+
$('overview-scenes').textContent = rows.length
|
|
122
|
+
$('overview-sessions').textContent = rows.filter(row => row.health?.sessionActive).length
|
|
123
|
+
$('overview-workspace').textContent = workspace?.title ?? '未选择'
|
|
124
|
+
$('side-status').textContent = setup.enabled ? '插件已启用' : '等待首次设置'
|
|
125
|
+
$('side-detail').textContent = setup.connect === false ? 'QQ 已交接给其他本地客户端' : runtime.health?.transport === 'connected' ? 'NapCat 已连接' : 'NapCat 未连接或已禁用'
|
|
126
|
+
$('side-bot').textContent = setup.botId || '—'
|
|
127
|
+
$('side-owner').textContent = setup.ownerId || '—'
|
|
128
|
+
$('transport-status').textContent = `NapCat:${transportText(runtime.health?.transport)}`
|
|
129
|
+
const list = $('overview-list')
|
|
130
|
+
list.replaceChildren()
|
|
131
|
+
for (const row of rows.slice(0, 5)) {
|
|
132
|
+
const card = el('div', 'activity-row')
|
|
133
|
+
card.append(el('strong', '', `${row.scene.kind === 'group' ? '群聊' : '私聊'} ${row.scene.peerId}`))
|
|
134
|
+
card.append(el('span', 'tag ' + (row.paused ? 'warn' : 'success'), row.paused ? '已暂停' : row.health?.sessionActive ? 'Session 已绑定' : '正在建立 Session'))
|
|
135
|
+
list.append(card)
|
|
136
|
+
}
|
|
137
|
+
if (!rows.length) list.append(el('p', 'fine', '尚无授权会话。请先完成全局设置。'))
|
|
138
|
+
const flow = $('onboarding')
|
|
139
|
+
flow.replaceChildren()
|
|
140
|
+
for (const [done, title] of [
|
|
141
|
+
[Boolean(setup.workspaceId), '选择 DSH Workspace'],
|
|
142
|
+
[Boolean(setup.botId && setup.ownerId && setup.httpUrl && setup.wsUrl), '填写 Bot、Owner 和 NapCat'],
|
|
143
|
+
[rows.length > 0, '授权至少一个 QQ 会话'],
|
|
144
|
+
[runtime.health?.transport === 'connected', '连接 NapCat'],
|
|
145
|
+
]) {
|
|
146
|
+
const item = el('div', 'check-row')
|
|
147
|
+
item.append(el('span', 'tag ' + (done ? 'success' : 'warn'), done ? '完成' : '待办'), el('b', '', title))
|
|
148
|
+
flow.append(item)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderHandoff() {
|
|
153
|
+
const setup = state.setup
|
|
154
|
+
const health = state.runtime?.health
|
|
155
|
+
const button = $('handoff-button')
|
|
156
|
+
const ready = setup?.enabled === true && Boolean(setup.workspaceId && setup.botId)
|
|
157
|
+
const paused = setup?.connect === false
|
|
158
|
+
const stopped = health?.handoffPaused === true || health?.transport === 'disabled'
|
|
159
|
+
button.disabled = state.busy || !ready
|
|
160
|
+
button.textContent = !ready ? '请先完成首次配置' : paused ? '恢复由 Amadeus 接管'
|
|
161
|
+
: stopped ? '完成暂停配置' : '暂停 Amadeus,交给 AstrBot'
|
|
162
|
+
$('handoff-status').textContent = !ready ? '尚未完成配置'
|
|
163
|
+
: paused && stopped ? '已暂停,可启用 AstrBot'
|
|
164
|
+
: paused ? '正在停止连接;请刷新确认后再启用 AstrBot'
|
|
165
|
+
: stopped ? '运行已暂停,但配置尚未保存;请重试切换'
|
|
166
|
+
: health?.transport === 'connected' ? 'Amadeus 正在接管 QQ'
|
|
167
|
+
: 'Amadeus 已接管,正在等待 NapCat 连接'
|
|
168
|
+
$('handoff-detail').textContent = paused
|
|
169
|
+
? 'Amadeus 不接收新消息,也不会通过 QQ 工具外发。切回前请先停用 AstrBot;暂停期间的消息不会自动补入 Amadeus。'
|
|
170
|
+
: '切换不会删除 QQ Scene 或 DSH Session。暂停前已提交给 NapCat 的操作无法撤回。'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderSetup() {
|
|
174
|
+
const setup = state.setup
|
|
175
|
+
if (!setup) return
|
|
176
|
+
const selector = $('setup-workspace')
|
|
177
|
+
selector.replaceChildren()
|
|
178
|
+
selector.append(el('option', '', '请选择 Workspace'))
|
|
179
|
+
selector.firstChild.value = ''
|
|
180
|
+
for (const workspace of setup.workspaces ?? []) {
|
|
181
|
+
const option = el('option', '', `${workspace.title} · ${workspace.path}`)
|
|
182
|
+
option.value = workspace.id
|
|
183
|
+
selector.append(option)
|
|
184
|
+
}
|
|
185
|
+
selector.value = setup.workspaceId || ''
|
|
186
|
+
$('setup-bot').value = setup.botId || ''
|
|
187
|
+
$('setup-owner').value = setup.ownerId || ''
|
|
188
|
+
$('setup-http').value = setup.httpUrl || ''
|
|
189
|
+
$('setup-ws').value = setup.wsUrl || ''
|
|
190
|
+
$('setup-token').value = ''
|
|
191
|
+
$('token-hint').textContent = setup.tokenConfigured ? '已有 Token。留空可保留。' : '尚未配置 Token;若 NapCat 未启用鉴权可留空。'
|
|
192
|
+
$('setup-groups').value = (setup.groups ?? []).join(', ')
|
|
193
|
+
$('setup-private').value = (setup.privatePeers ?? []).filter(id => id !== setup.ownerId).join(', ')
|
|
194
|
+
$('setup-connect').checked = setup.connect === true
|
|
195
|
+
const denyList = $('setup-global-deny')
|
|
196
|
+
denyList.replaceChildren()
|
|
197
|
+
const catalog = state.runtime?.catalog
|
|
198
|
+
if (!Array.isArray(catalog)) denyList.append(el('p', 'fine', '能力目录暂不可用;保存时保留现有全局拒绝。'))
|
|
199
|
+
else for (const capability of catalog) {
|
|
200
|
+
const label = el('label', 'global-deny-option')
|
|
201
|
+
const checkbox = el('input')
|
|
202
|
+
checkbox.type = 'checkbox'; checkbox.value = capability.id
|
|
203
|
+
checkbox.checked = (setup.globalDeny ?? []).includes(capability.id)
|
|
204
|
+
checkbox.addEventListener('change', () => { $('setup-state').textContent = '有未保存修改' })
|
|
205
|
+
label.append(checkbox, el('span', '', labels[capability.id] ?? capability.id))
|
|
206
|
+
denyList.append(label)
|
|
207
|
+
}
|
|
208
|
+
$('setup-state').textContent = '当前已应用配置'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function selectScene(key) {
|
|
212
|
+
const row = sceneRows().find(item => item.key === key)
|
|
213
|
+
state.selected = row?.key ?? ''
|
|
214
|
+
const mode = row?.permissions?.mode ?? row?.effectiveMode ?? 'safe-chat'
|
|
215
|
+
const preset = state.runtime?.behaviorPresets?.[mode] ?? {}
|
|
216
|
+
const behavior = row?.permissions?.behavior ?? preset
|
|
217
|
+
state.draft = row ? { mode,
|
|
218
|
+
overrides: { ...(row.permissions?.overrides ?? {}) }, behavior: { ...behavior },
|
|
219
|
+
custom: Object.keys(row.permissions?.overrides ?? {}).length > 0 ||
|
|
220
|
+
(row.permissions?.behavior !== undefined && JSON.stringify(behavior) !== JSON.stringify(preset)),
|
|
221
|
+
expectedRevision: row.revision ?? 0 } : null
|
|
222
|
+
renderScenes()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderScenes() {
|
|
226
|
+
const rows = sceneRows()
|
|
227
|
+
const filter = $('scene-filter').value.trim()
|
|
228
|
+
const list = $('scene-list')
|
|
229
|
+
list.replaceChildren()
|
|
230
|
+
for (const row of rows.filter(item => !filter || item.scene.peerId.includes(filter))) {
|
|
231
|
+
const button = el('button', 'scene-choice' + (row.key === state.selected ? ' selected' : ''))
|
|
232
|
+
button.type = 'button'
|
|
233
|
+
button.append(el('strong', '', `${row.scene.kind === 'group' ? '群聊' : '私聊'} ${row.scene.peerId}`),
|
|
234
|
+
el('small', '', row.paused ? '已暂停' : row.health?.sessionActive ? 'Session 已绑定' : '正在建立 Session'))
|
|
235
|
+
button.addEventListener('click', () => selectScene(row.key))
|
|
236
|
+
list.append(button)
|
|
237
|
+
}
|
|
238
|
+
if (!rows.length) list.append(el('p', 'fine', '暂无授权会话。请在全局设置中填写 Bot 和 Owner。'))
|
|
239
|
+
const row = rows.find(item => item.key === state.selected)
|
|
240
|
+
const detail = $('scene-detail')
|
|
241
|
+
detail.replaceChildren()
|
|
242
|
+
if (!row || !state.draft) { detail.append(el('h2', '', '选择会话'), el('p', '', '选择左侧 Scene 查看设置。')); return }
|
|
243
|
+
const scene = row.scene
|
|
244
|
+
const head = el('div', 'sect-head')
|
|
245
|
+
head.append(el('h2', '', `${scene.kind === 'group' ? '群聊' : '私聊'} ${scene.peerId}`), el('span', 'tag', row.paused ? '已暂停' : '已授权'))
|
|
246
|
+
detail.append(head)
|
|
247
|
+
const info = el('div', 'inline-notice')
|
|
248
|
+
info.textContent = row.paused
|
|
249
|
+
? '此 Scene 已暂停,模型不能读取或发送 QQ 消息。恢复后沿用原 Session 绑定。'
|
|
250
|
+
: row.health?.sessionActive
|
|
251
|
+
? `已绑定 DSH Session ${row.sessionId ? `${row.sessionId.slice(0, 10)}…${row.sessionId.slice(-4)}` : ''} · Workspace:${state.setup.workspaces.find(x => x.id === row.workspaceId)?.title ?? '未知'}`
|
|
252
|
+
: '正在建立 DSH Session;若持续未绑定,请检查运行诊断。'
|
|
253
|
+
detail.append(info)
|
|
254
|
+
const actions = el('div', 'scene-actions')
|
|
255
|
+
const pause = el('button', 'button', row.paused ? '恢复会话' : '暂停会话')
|
|
256
|
+
pause.type = 'button'; pause.addEventListener('click', () => void changeScenePause(row, !row.paused))
|
|
257
|
+
const remove = el('button', 'button', '移出白名单')
|
|
258
|
+
remove.type = 'button'; remove.disabled = row.ownerEligible
|
|
259
|
+
remove.addEventListener('click', () => void removeScene(row))
|
|
260
|
+
actions.append(pause, remove)
|
|
261
|
+
detail.append(actions, el('h3', '', '权限预设'))
|
|
262
|
+
const grid = el('div', 'preset-grid')
|
|
263
|
+
for (const [id, label, description] of modes) {
|
|
264
|
+
if (id === 'owner-agent' && !row.ownerEligible) continue
|
|
265
|
+
const option = el('button', 'preset-option' + (!state.draft.custom && state.draft.mode === id ? ' active' : ''))
|
|
266
|
+
option.type = 'button'
|
|
267
|
+
option.append(el('strong', '', label), el('small', '', description))
|
|
268
|
+
option.addEventListener('click', () => { state.draft.mode = id; state.draft.overrides = {};
|
|
269
|
+
state.draft.behavior = { ...(state.runtime?.behaviorPresets?.[id] ?? {}) }; state.draft.custom = false; renderScenes() })
|
|
270
|
+
grid.append(option)
|
|
271
|
+
}
|
|
272
|
+
detail.append(grid)
|
|
273
|
+
detail.append(el('p', 'fine', `当前编辑模式:${state.draft.custom ? '自定义' : modes.find(x => x[0] === state.draft.mode)?.[1] ?? state.draft.mode}。更改权限后自动切换为自定义。`))
|
|
274
|
+
for (const [title, caps] of groups) {
|
|
275
|
+
if (title === '群管理' && scene.kind !== 'group') continue
|
|
276
|
+
const panel = el('details', 'permission-group')
|
|
277
|
+
panel.open = openGroups.has(title)
|
|
278
|
+
panel.addEventListener('toggle', () => { if (panel.open) openGroups.add(title); else openGroups.delete(title) })
|
|
279
|
+
panel.append(el('summary', '', title))
|
|
280
|
+
for (const id of caps) {
|
|
281
|
+
if (id.startsWith('qq.owner') && !row.ownerEligible) continue
|
|
282
|
+
if (['qq.friends.read', 'qq.groups.read'].includes(id) && !row.ownerEligible) continue
|
|
283
|
+
const current = row.capabilities?.find(item => item.id === id)
|
|
284
|
+
const line = el('div', 'permission-row')
|
|
285
|
+
const label = el('label', '', labels[id] ?? id)
|
|
286
|
+
const select = el('select', 'input')
|
|
287
|
+
select.setAttribute('aria-label', `${labels[id] ?? id} 权限`)
|
|
288
|
+
for (const [value, text] of [['inherit', '继承预设'], ['allow', '允许'], ['deny', '拒绝']]) {
|
|
289
|
+
const option = el('option', '', text); option.value = value; select.append(option)
|
|
290
|
+
}
|
|
291
|
+
select.value = state.draft.overrides[id] ?? 'inherit'
|
|
292
|
+
select.addEventListener('change', () => {
|
|
293
|
+
if (select.value === 'inherit') delete state.draft.overrides[id]
|
|
294
|
+
else state.draft.overrides[id] = select.value
|
|
295
|
+
state.draft.custom = true
|
|
296
|
+
renderScenes()
|
|
297
|
+
})
|
|
298
|
+
line.append(label, select, el('small', 'effective-status', current ? (current.effective ? '当前生效' : `当前拒绝 · ${current.reason}`) : '保存后查看生效状态'))
|
|
299
|
+
panel.append(line)
|
|
300
|
+
}
|
|
301
|
+
detail.append(panel)
|
|
302
|
+
}
|
|
303
|
+
const behaviorPanel = el('details', 'permission-group')
|
|
304
|
+
behaviorPanel.open = openGroups.has('连续对话')
|
|
305
|
+
behaviorPanel.addEventListener('toggle', () => { if (behaviorPanel.open) openGroups.add('连续对话'); else openGroups.delete('连续对话') })
|
|
306
|
+
behaviorPanel.append(el('summary', '', '连续对话与行为参数'))
|
|
307
|
+
for (const [key, labelText, options] of behaviorSettings) {
|
|
308
|
+
if (key === 'replyCooldownMs' && scene.kind !== 'group') continue
|
|
309
|
+
const line = el('div', 'permission-row')
|
|
310
|
+
const selector = el('select', 'input')
|
|
311
|
+
selector.setAttribute('aria-label', labelText)
|
|
312
|
+
for (const [value, label] of options) {
|
|
313
|
+
const option = el('option', '', label); option.value = String(value); selector.append(option)
|
|
314
|
+
}
|
|
315
|
+
selector.value = String(state.draft.behavior[key] ?? options[0][0])
|
|
316
|
+
selector.addEventListener('change', () => {
|
|
317
|
+
state.draft.behavior[key] = key === 'activity' ? selector.value : Number(selector.value)
|
|
318
|
+
state.draft.custom = true
|
|
319
|
+
renderScenes()
|
|
320
|
+
})
|
|
321
|
+
line.append(el('label', '', labelText), selector)
|
|
322
|
+
behaviorPanel.append(line)
|
|
323
|
+
}
|
|
324
|
+
detail.append(behaviorPanel)
|
|
325
|
+
const footer = el('div', 'detail-footer')
|
|
326
|
+
const cancel = el('button', 'button', '撤销修改')
|
|
327
|
+
cancel.type = 'button'; cancel.addEventListener('click', () => selectScene(row.key))
|
|
328
|
+
const save = el('button', 'button primary', '保存权限')
|
|
329
|
+
save.type = 'button'; save.disabled = state.busy || !state.setup.enabled || row.paused
|
|
330
|
+
save.addEventListener('click', () => void savePermissions(row))
|
|
331
|
+
footer.append(cancel, save)
|
|
332
|
+
detail.append(footer)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function savePermissions(row) {
|
|
336
|
+
if (state.busy) return
|
|
337
|
+
state.busy = true
|
|
338
|
+
try {
|
|
339
|
+
await api('/scenes/permissions', { scene: row.scene, access: { mode: state.draft.mode,
|
|
340
|
+
overrides: state.draft.overrides, behavior: state.draft.behavior }, expectedRevision: state.draft.expectedRevision })
|
|
341
|
+
notice('会话设置已保存;当前 Session 绑定以页面显示为准。')
|
|
342
|
+
await refresh()
|
|
343
|
+
} catch (error) { notice(error.status === 409 ? '权限已有变化,请刷新后重新编辑。' : `保存失败:${error.message}`, true) }
|
|
344
|
+
finally { state.busy = false }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function changeScenePause(row, paused) {
|
|
348
|
+
try {
|
|
349
|
+
await api('/scenes/pause', { expectedRevision: state.setup.revision, ...row.scene, paused })
|
|
350
|
+
notice(paused ? '会话已暂停。' : '会话已恢复。')
|
|
351
|
+
await refresh()
|
|
352
|
+
} catch (error) { notice(`会话状态未确认:${error.message}`, true); await refresh() }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function removeScene(row) {
|
|
356
|
+
if (!confirm(`确定将 ${row.scene.peerId} 移出白名单?`)) return
|
|
357
|
+
try {
|
|
358
|
+
await api('/scenes/allowlist', { expectedRevision: state.setup.revision, ...row.scene, allowed: false })
|
|
359
|
+
state.selected = ''
|
|
360
|
+
notice('会话已移出白名单。历史记录未删除。')
|
|
361
|
+
await refresh()
|
|
362
|
+
} catch (error) { notice(`移除状态未确认:${error.message}`, true); await refresh() }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderFeatures() {
|
|
366
|
+
const features = [
|
|
367
|
+
['聊天与互动', '受 Scene 权限与 NapCat 能力共同约束', ['qq.read', 'qq.send', 'qq.reaction.write', 'qq.poke', 'qq.recall']],
|
|
368
|
+
['媒体与连续交流', '图片、语音、视频和文件读取;等待、已读、短消息合并与可靠投递由 QQ 内部工具处理', ['qq.media.read']],
|
|
369
|
+
['群管理', '普通预设默认关闭;还需 Bot 实际拥有相应 QQ 群权限', groups[1][1]],
|
|
370
|
+
['Owner 与 Agent', '仅可信 Owner 私聊可授权账号级目录及 Host 工具', groups[2][1]],
|
|
371
|
+
['账号与 QQ 空间', '仅在独立账号页配置;空间读写还受 Provider 与平台支持限制', ['qq.profile.write', 'qq.qzone.use', 'qq.qzone.read', 'qq.qzone.publish']],
|
|
372
|
+
]
|
|
373
|
+
const list = $('feature-list')
|
|
374
|
+
list.replaceChildren()
|
|
375
|
+
const catalog = new Map((state.runtime?.catalog ?? []).map(item => [item.id, item]))
|
|
376
|
+
for (const [name, body, ids] of features) {
|
|
377
|
+
const card = el('div', 'card padded')
|
|
378
|
+
card.append(el('h3', '', name), el('p', '', body))
|
|
379
|
+
const items = el('div', 'cap-list')
|
|
380
|
+
for (const id of ids) {
|
|
381
|
+
const item = el('div')
|
|
382
|
+
const available = catalog.get(id)?.implemented === true
|
|
383
|
+
const status = id.startsWith('qq.qzone.')
|
|
384
|
+
? state.runtime?.providers?.qzone?.configured ? 'Provider 已配置 · 待平台确认' : '需要 QZone Provider'
|
|
385
|
+
: id.startsWith('qq.group.') ? '显式授权 + 群权限' : available ? '源码已接线 · 需授权' : '当前不可用'
|
|
386
|
+
item.append(el('span', '', labels[id] ?? id), el('span', 'tag ' + (available ? 'info' : 'warn'), status))
|
|
387
|
+
items.append(item)
|
|
388
|
+
}
|
|
389
|
+
card.append(items)
|
|
390
|
+
list.append(card)
|
|
391
|
+
}
|
|
392
|
+
const optional = el('div', 'card padded')
|
|
393
|
+
optional.append(el('h3', '', '可选联动'), el('p', '', 'Memory、Life、State 缺失时,QQ 当前聊天功能仍可单独运行。Life 独占长期自主唤醒。'))
|
|
394
|
+
list.append(optional)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function renderAccount() {
|
|
398
|
+
const setup = state.setup ?? {}
|
|
399
|
+
const owner = sceneRows().find(row => row.ownerEligible)
|
|
400
|
+
const ownerAgent = owner?.permissions?.mode === 'owner-agent'
|
|
401
|
+
$('account-status').textContent = ownerAgent
|
|
402
|
+
? `可信 Owner 私聊 ${setup.ownerId} 已选择 Owner Agent。修改资料仍受 QQ 平台拒绝约束。`
|
|
403
|
+
: '先在会话管理中将 Owner 私聊切换为 Owner Agent。'
|
|
404
|
+
$('qzone-status').textContent = state.runtime?.providers?.qzone?.configured
|
|
405
|
+
? 'QZone Provider 当前可用;真实平台动作仍可能被拒绝。'
|
|
406
|
+
: '当前未检测到可用的 QZone Provider。保存配置后请到诊断页核对。'
|
|
407
|
+
$('account-profile').checked = owner?.permissions?.overrides?.['qq.profile.write'] === 'allow'
|
|
408
|
+
$('account-qzone-use').checked = owner?.permissions?.overrides?.['qq.qzone.use'] === 'allow'
|
|
409
|
+
$('account-qzone-enabled').checked = setup.qzoneEnabled === true
|
|
410
|
+
$('account-qzone-publish').checked = setup.qzonePublish === true
|
|
411
|
+
$('account-qzone-url').value = setup.qzoneLoopbackUrl ?? ''
|
|
412
|
+
$('account-qzone-token-env').value = setup.qzoneTokenEnv ?? ''
|
|
413
|
+
for (const id of ['account-profile', 'account-qzone-use']) $(id).disabled = !ownerAgent
|
|
414
|
+
$('account-form').querySelector('[data-account-save="scene"]').disabled = !ownerAgent
|
|
415
|
+
$('account-state').textContent = ownerAgent ? '当前已应用设置' : '空间 Provider 可先配置;Owner 授权需启用 Owner Agent'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderDiagnostics() {
|
|
419
|
+
const setup = state.setup ?? {}
|
|
420
|
+
const runtime = state.runtime ?? {}
|
|
421
|
+
const rows = sceneRows()
|
|
422
|
+
$('diag-plugin').textContent = setup.enabled ? '已启用' : '未启用'
|
|
423
|
+
$('diag-transport').textContent = transportText(runtime.health?.transport)
|
|
424
|
+
$('diag-workspace').textContent = setup.workspaceId ? '已选择' : '缺失'
|
|
425
|
+
$('diag-scenes').textContent = String(rows.length)
|
|
426
|
+
const list = $('diagnostic-list')
|
|
427
|
+
list.replaceChildren()
|
|
428
|
+
for (const [okay, title, description] of [
|
|
429
|
+
[Boolean(setup.workspaceId), 'DSH Workspace', '必须选择现有 Workspace'],
|
|
430
|
+
[Boolean(setup.ownerId && setup.botId), 'Bot 与 Owner', '可信身份只来自 OneBot 与 DSH 绑定'],
|
|
431
|
+
[runtime.health?.transport === 'connected', 'NapCat WebSocket', '连接在线不等于具有发送权限'],
|
|
432
|
+
[rows.some(row => row.health?.sessionActive), 'QQ Session 绑定', '授权后应在所选 Workspace 中创建真实 Session'],
|
|
433
|
+
]) {
|
|
434
|
+
const row = el('div', 'check-row')
|
|
435
|
+
row.append(el('span', 'tag ' + (okay ? 'success' : 'warn'), okay ? '通过' : '待检查'))
|
|
436
|
+
const content = el('div'); content.append(el('b', '', title), el('small', '', description)); row.append(content)
|
|
437
|
+
list.append(row)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function refresh() {
|
|
442
|
+
const [setup, runtime] = await Promise.allSettled([api('/setup'), api('/scenes')])
|
|
443
|
+
if (setup.status === 'fulfilled') state.setup = setup.value
|
|
444
|
+
else notice(`首次设置不可用:${setup.reason.message}`, true)
|
|
445
|
+
if (runtime.status === 'fulfilled') state.runtime = runtime.value
|
|
446
|
+
else notice(`运行状态不可用:${runtime.reason.message}`, true)
|
|
447
|
+
renderOverview(); renderSetup(); renderHandoff(); renderScenes(); renderFeatures(); renderAccount(); renderDiagnostics()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const button of document.querySelectorAll('[data-page]')) button.addEventListener('click', () => show(button.dataset.page))
|
|
451
|
+
$('theme-toggle').addEventListener('click', () => {
|
|
452
|
+
document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'
|
|
453
|
+
})
|
|
454
|
+
$('scene-filter').addEventListener('input', renderScenes)
|
|
455
|
+
$('refresh').addEventListener('click', () => void refresh())
|
|
456
|
+
$('setup-discard').addEventListener('click', renderSetup)
|
|
457
|
+
$('handoff-button').addEventListener('click', async () => {
|
|
458
|
+
if (!state.setup?.enabled || state.busy) return
|
|
459
|
+
const connect = state.setup.connect !== true
|
|
460
|
+
state.busy = true
|
|
461
|
+
renderHandoff()
|
|
462
|
+
try {
|
|
463
|
+
await api('/handoff', { expectedRevision: state.setup.revision, connect })
|
|
464
|
+
notice(connect ? 'Amadeus 已恢复接管;请确认 AstrBot 已停用。' : 'Amadeus 已暂停;请等待运行状态确认后启用 AstrBot。')
|
|
465
|
+
} catch (error) {
|
|
466
|
+
notice(`切换状态未确认:${error.message}。请刷新核对。`, true)
|
|
467
|
+
} finally {
|
|
468
|
+
await refresh()
|
|
469
|
+
state.busy = false
|
|
470
|
+
renderHandoff()
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
for (const field of $('setup-form').querySelectorAll('input,select')) field.addEventListener('input', () => { $('setup-state').textContent = '有未保存修改' })
|
|
474
|
+
$('setup-form').addEventListener('submit', async event => {
|
|
475
|
+
event.preventDefault()
|
|
476
|
+
if (!state.setup || state.busy) return
|
|
477
|
+
state.busy = true
|
|
478
|
+
const body = {
|
|
479
|
+
expectedRevision: state.setup.revision, workspaceId: $('setup-workspace').value,
|
|
480
|
+
botId: $('setup-bot').value.trim(), ownerId: $('setup-owner').value.trim(),
|
|
481
|
+
httpUrl: $('setup-http').value.trim(), wsUrl: $('setup-ws').value.trim(),
|
|
482
|
+
groups: splitIds($('setup-groups').value), privatePeers: splitIds($('setup-private').value),
|
|
483
|
+
connect: $('setup-connect').checked,
|
|
484
|
+
}
|
|
485
|
+
if (Array.isArray(state.runtime?.catalog)) {
|
|
486
|
+
body.globalDeny = [...$('setup-global-deny').querySelectorAll('input:checked')].map(input => input.value)
|
|
487
|
+
}
|
|
488
|
+
if ($('setup-token').value) body.token = $('setup-token').value
|
|
489
|
+
try { await api('/setup', body); notice('全局配置已保存并应用。'); await refresh() }
|
|
490
|
+
catch (error) { notice(`保存状态未确认:${error.message}。请刷新核对。`, true); await refresh() }
|
|
491
|
+
finally { state.busy = false }
|
|
492
|
+
})
|
|
493
|
+
$('account-discard').addEventListener('click', renderAccount)
|
|
494
|
+
for (const field of $('account-form').querySelectorAll('input')) field.addEventListener('input', () => { $('account-state').textContent = '有未保存修改' })
|
|
495
|
+
$('account-form').addEventListener('submit', async event => {
|
|
496
|
+
event.preventDefault()
|
|
497
|
+
if (!state.setup || state.busy) return
|
|
498
|
+
const owner = sceneRows().find(row => row.ownerEligible)
|
|
499
|
+
const target = event.submitter?.dataset.accountSave
|
|
500
|
+
if (!['scene', 'provider'].includes(target)) return
|
|
501
|
+
if (target === 'scene' && owner?.permissions?.mode !== 'owner-agent') {
|
|
502
|
+
notice('请先为 Owner 私聊启用 Owner Agent。', true); return
|
|
503
|
+
}
|
|
504
|
+
state.busy = true
|
|
505
|
+
try {
|
|
506
|
+
if (target === 'scene') {
|
|
507
|
+
const overrides = { ...(owner.permissions?.overrides ?? {}),
|
|
508
|
+
'qq.profile.write': $('account-profile').checked ? 'allow' : 'deny',
|
|
509
|
+
'qq.qzone.use': $('account-qzone-use').checked ? 'allow' : 'deny',
|
|
510
|
+
}
|
|
511
|
+
await api('/scenes/permissions', { scene: owner.scene,
|
|
512
|
+
access: { mode: 'owner-agent', overrides, ...(owner.permissions?.behavior ? { behavior: owner.permissions.behavior } : {}) },
|
|
513
|
+
expectedRevision: owner.revision })
|
|
514
|
+
notice('Owner 授权已保存。')
|
|
515
|
+
} else {
|
|
516
|
+
await api('/account', { expectedRevision: state.setup.revision,
|
|
517
|
+
qzoneEnabled: $('account-qzone-enabled').checked,
|
|
518
|
+
qzonePublish: $('account-qzone-publish').checked,
|
|
519
|
+
qzoneLoopbackUrl: $('account-qzone-url').value.trim(),
|
|
520
|
+
qzoneTokenEnv: $('account-qzone-token-env').value.trim(),
|
|
521
|
+
})
|
|
522
|
+
notice('空间 Provider 与账号级授权已保存。')
|
|
523
|
+
}
|
|
524
|
+
await refresh()
|
|
525
|
+
} catch (error) { notice(`设置未确认:${error.message}。请刷新核对。`, true); await refresh() }
|
|
526
|
+
finally { state.busy = false }
|
|
527
|
+
})
|
|
528
|
+
$('add-scene').addEventListener('click', () => $('add-modal').showModal())
|
|
529
|
+
$('add-cancel').addEventListener('click', () => $('add-modal').close())
|
|
530
|
+
$('add-form').addEventListener('submit', async event => {
|
|
531
|
+
event.preventDefault()
|
|
532
|
+
if (!state.setup?.botId) { notice('请先配置 Bot 账号。', true); return }
|
|
533
|
+
const kind = document.querySelector('input[name="kind"]:checked').value
|
|
534
|
+
const peerId = $('add-peer').value.trim()
|
|
535
|
+
try {
|
|
536
|
+
await api('/scenes/allowlist', { expectedRevision: state.setup.revision, botId: state.setup.botId, kind, peerId, allowed: true })
|
|
537
|
+
$('add-modal').close(); $('add-form').reset(); notice('会话已加入白名单。'); await refresh(); selectScene(`${state.setup.botId}:${kind}:${peerId}`)
|
|
538
|
+
} catch (error) { notice(`添加状态未确认:${error.message}`, true); await refresh() }
|
|
539
|
+
})
|
|
540
|
+
void refresh()
|
|
541
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>Amadeus QQ · 管理控制台</title>
|
|
8
|
+
<link rel="stylesheet" href="console.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="app">
|
|
12
|
+
<aside class="sidebar">
|
|
13
|
+
<div class="brand"><div class="brand-logo">A</div><div><div class="brand-label">Amadeus QQ <span class="brand-tag">v1.2</span></div><div class="brand-sub">与世界一起聊天</div></div></div>
|
|
14
|
+
<div><div class="nav-label">CONSOLE · 管理控制台</div><nav class="nav" aria-label="主导航">
|
|
15
|
+
<button type="button" data-page="overview" class="active">总览首页</button>
|
|
16
|
+
<button type="button" data-page="scenes">会话管理</button>
|
|
17
|
+
<button type="button" data-page="features">功能中心</button>
|
|
18
|
+
<div class="nav-divider"></div>
|
|
19
|
+
<button type="button" data-page="account">账号与 QQ 空间</button>
|
|
20
|
+
<button type="button" data-page="global">全局设置</button>
|
|
21
|
+
<button type="button" data-page="diagnostics">运行与诊断</button>
|
|
22
|
+
</nav></div>
|
|
23
|
+
<div class="sidebar-bottom"><div class="plugin-status"><span class="micro">插件运行状态</span><strong id="side-status">正在读取</strong><p id="side-detail">等待 DSH Owner 鉴权</p><div class="idline"><span>Bot</span><b id="side-bot">—</b></div><div class="idline"><span>Owner</span><b id="side-owner">—</b></div></div></div>
|
|
24
|
+
</aside>
|
|
25
|
+
<main class="main"><div class="main-width">
|
|
26
|
+
<header class="head"><div class="page-lead"><div class="eyebrow">AMADEUS · QQ INTEGRATION</div><h1 id="page-title">总览首页</h1><p id="page-subtitle">连接状态、会话和首次配置。</p></div><div class="head-right"><div class="head-status" id="transport-status">NapCat:正在检查</div><button class="button" id="theme-toggle" type="button">切换主题</button></div></header>
|
|
27
|
+
<div id="notice" class="inline-notice hide" role="status" aria-live="polite"></div>
|
|
28
|
+
|
|
29
|
+
<section id="view-overview" class="view active" aria-label="总览首页">
|
|
30
|
+
<div class="stats-grid">
|
|
31
|
+
<div class="card stat-card"><div class="stat-top">插件状态</div><div class="stat-value" id="overview-plugin">—</div><div class="stat-caption" id="overview-transport">读取中</div></div>
|
|
32
|
+
<div class="card stat-card"><div class="stat-top">已授权会话</div><div class="stat-value" id="overview-scenes">0</div><div class="stat-caption">每个 QQ 场景独立绑定 DSH Session</div></div>
|
|
33
|
+
<div class="card stat-card"><div class="stat-top">实际 Session</div><div class="stat-value" id="overview-sessions">0</div><div class="stat-caption">授权后创建,重启后恢复</div></div>
|
|
34
|
+
<div class="card stat-card"><div class="stat-top">目标工作区</div><div class="stat-value" id="overview-workspace">未选择</div><div class="stat-caption">QQ 会话归属此 DSH Workspace</div></div>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="two-columns"><div class="card padded"><h2>最近会话</h2><div id="overview-list" class="scene-list"></div><button class="button soft" type="button" data-page="scenes">管理会话</button></div><div class="card padded"><h2>首次设置</h2><p>选择 Workspace、配置本机 NapCat、指定唯一 Owner,然后授权群聊或私聊。</p><div id="onboarding" class="check-flow"></div><button class="button primary" type="button" data-page="global">打开全局设置</button></div></div>
|
|
37
|
+
<div class="inline-notice"><b>QQ 与 Life 的边界:</b>QQ 只处理当前聊天和同一 Session 内的等待续接。长期自主唤醒由可选的 Life 插件负责。</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section id="view-scenes" class="view" aria-label="会话管理">
|
|
41
|
+
<div class="scene-layout"><div class="card scene-panel"><div class="sect-head"><h2>授权会话</h2><button class="button mini" id="add-scene" type="button">添加</button></div><input class="input" id="scene-filter" type="search" placeholder="筛选群号或 QQ 号" aria-label="筛选会话"><div id="scene-list" class="scene-list"></div></div>
|
|
42
|
+
<div class="card scene-detail" id="scene-detail"><h2>选择会话</h2><p>选择左侧 Scene 查看权限、Session 绑定和运行状态。</p></div></div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section id="view-features" class="view" aria-label="功能中心"><div class="callout"><div><b>按实际执行条件展示</b><p>源码路径、Provider 可用性和真实 QQ 验收是不同状态;这里的可用性以当前配置为准。</p></div></div><div id="feature-list" class="feature-grid"></div></section>
|
|
46
|
+
|
|
47
|
+
<section id="view-account" class="view" aria-label="账号与 QQ 空间"><div class="callout"><div><b>仅可信 Owner 私聊</b><p>账号资料和 QQ 空间不属于任何群聊预设;请先在 Owner 私聊 Scene 选择 Owner Agent。两组设置分别保存,避免其中一步失败后误以为全部生效。</p></div></div><form id="account-form"><div class="two-columns"><div class="card padded"><h2>账号资料</h2><p>昵称、签名、在线状态和头像均由 Owner 专用工具操作。</p><label class="form-label"><input id="account-profile" type="checkbox"> 允许 Owner 私聊修改账号资料</label><div id="account-status" class="inline-notice"></div></div><div class="card padded"><h2>QQ 空间</h2><p>Provider、账号级授权和 Scene 显式授权会逐层检查。</p><label class="form-label"><input id="account-qzone-enabled" type="checkbox"> 启用 QZone 工具</label><label class="form-label"><input id="account-qzone-publish" type="checkbox"> 授权此 Bot 发布空间内容</label><label class="form-label"><input id="account-qzone-use" type="checkbox"> 允许 Owner 私聊使用 QZone</label><label class="form-label" for="account-qzone-url">可选本机 QZone Provider 地址</label><input class="input" id="account-qzone-url" placeholder="http://127.0.0.1:端口"><label class="form-label" for="account-qzone-token-env">Provider Token 环境变量名</label><input class="input" id="account-qzone-token-env" placeholder="QZONE_ACCESS_TOKEN"><div id="qzone-status" class="inline-notice"></div></div></div><div class="detail-footer"><span id="account-state">当前设置</span><div class="detail-footer-actions"><button class="button" type="button" id="account-discard">撤销修改</button><button class="button primary" type="submit" data-account-save="scene">保存 Owner 授权</button><button class="button primary" type="submit" data-account-save="provider">保存空间配置</button></div></div></form></section>
|
|
48
|
+
|
|
49
|
+
<section id="view-global" class="view" aria-label="全局设置"><div class="callout"><div><b>由 DSH Owner 管理</b><p>配置保存到当前 DSH Profile,Token 不会回显。改动通过 DSH 配置服务应用;连接状态以运行诊断为准。</p></div></div>
|
|
50
|
+
<div class="card padded"><h2>QQ 接管</h2><p>同一个 NapCat 登录可轮流供 Amadeus QQ 和本地 AstrBot 使用。先暂停 Amadeus,看到“已暂停”后再启用 AstrBot;切回时先停用 AstrBot。</p><strong id="handoff-status" role="status">正在读取状态</strong><p class="form-hint" id="handoff-detail">切换不会删除 QQ Scene 或 DSH Session。暂停期间的消息不会自动补入 Amadeus;切换前已提交给 NapCat 的操作无法撤回。</p><button class="button" id="handoff-button" type="button" disabled>正在读取</button></div>
|
|
51
|
+
<form id="setup-form"><div class="form-grid">
|
|
52
|
+
<div class="card form-card"><h3>DSH Workspace</h3><p>所有新建 QQ Session 都会实际加入所选 Workspace。</p><label class="form-label" for="setup-workspace">工作区</label><select class="input" id="setup-workspace" required></select></div>
|
|
53
|
+
<div class="card form-card"><h3>Bot 与 Owner</h3><label class="form-label" for="setup-bot">Bot QQ 号</label><input class="input" id="setup-bot" inputmode="numeric" pattern="[1-9][0-9]*" required><label class="form-label" for="setup-owner">唯一 Owner QQ 号</label><input class="input" id="setup-owner" inputmode="numeric" pattern="[1-9][0-9]*" required></div>
|
|
54
|
+
<div class="card form-card"><h3>本机 NapCat / OneBot</h3><label class="form-label" for="setup-http">HTTP 地址</label><input class="input" id="setup-http" type="url" placeholder="http://127.0.0.1:3000" required><label class="form-label" for="setup-ws">WebSocket 地址</label><input class="input" id="setup-ws" placeholder="ws://127.0.0.1:3001" required><label class="form-label" for="setup-token">Access Token</label><input class="input" id="setup-token" type="password" autocomplete="new-password" placeholder="留空则保留现有 Token"><p class="form-hint" id="token-hint">当前未配置 Token</p></div>
|
|
55
|
+
<div class="card form-card"><h3>默认授权</h3><p>未知群聊和私聊始终拒绝。Owner 私聊会自动加入白名单。</p><label class="form-label" for="setup-groups">授权群号,逗号分隔</label><input class="input" id="setup-groups" placeholder="例如 123456789"><label class="form-label" for="setup-private">授权好友 QQ 号,逗号分隔</label><input class="input" id="setup-private"><label class="form-label"><input id="setup-connect" type="checkbox"> 启用 NapCat 连接</label></div>
|
|
56
|
+
<div class="card form-card"><h3>全局拒绝</h3><p>被勾选的能力对所有 Scene 生效,即使单个 Scene 请求允许也不能执行。此处由 DSH Owner 管理。</p><div id="setup-global-deny" class="global-deny-grid"></div></div>
|
|
57
|
+
</div><div class="detail-footer"><span id="setup-state">尚未修改</span><div class="detail-footer-actions"><button class="button" id="setup-discard" type="button">撤销修改</button><button class="button primary" type="submit">保存并应用</button></div></div></form>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section id="view-diagnostics" class="view" aria-label="运行与诊断"><div class="stats-grid"><div class="card stat-card"><div class="stat-top">插件</div><div class="stat-value" id="diag-plugin">—</div></div><div class="card stat-card"><div class="stat-top">NapCat WebSocket</div><div class="stat-value" id="diag-transport">—</div></div><div class="card stat-card"><div class="stat-top">Workspace</div><div class="stat-value" id="diag-workspace">—</div></div><div class="card stat-card"><div class="stat-top">授权 Scene</div><div class="stat-value" id="diag-scenes">—</div></div></div><div class="card padded"><h2>配置检查</h2><div id="diagnostic-list" class="check-flow"></div><button class="button soft" id="refresh" type="button">重新检查</button></div></section>
|
|
61
|
+
<footer class="footer">Amadeus QQ v1.2.1 · 本地 DSH Owner 控制台</footer>
|
|
62
|
+
</div></main>
|
|
63
|
+
</div>
|
|
64
|
+
<dialog id="add-modal" class="modal" aria-labelledby="add-modal-title"><form id="add-form"><h2 id="add-modal-title">添加 QQ 会话</h2><p>新 Scene 默认安全聊天权限;可以在添加后选择“仅观察”。</p><div class="radio-pair"><label><input type="radio" name="kind" value="group" checked> 群聊</label><label><input type="radio" name="kind" value="private"> 私聊</label></div><label class="form-label" for="add-peer">群号 / QQ 号</label><input class="input" id="add-peer" inputmode="numeric" pattern="[1-9][0-9]*" required><div class="actions"><button class="button" id="add-cancel" type="button">取消</button><button class="button primary" type="submit">添加</button></div></form></dialog>
|
|
65
|
+
<script src="console.js"></script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|