@fg-lab/amadeus-qq 0.0.0-stage → 1.2.1
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 +53 -3
- 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 +26 -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/V1_REAL_NAPCAT_2026-10-27.md +43 -0
- package/lib/client.js +380 -0
- package/package.json +91 -6
- package/preset.patch.yml +103 -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 +48 -0
- package/src/console/console.css +28 -0
- package/src/console/console.js +538 -0
- package/src/console/index.html +67 -0
- package/src/core.js +892 -0
- package/src/files.js +230 -0
- package/src/index.js +867 -0
- package/src/lease.js +42 -0
- package/src/media.js +246 -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 +250 -0
- package/src/preset-guard.mjs +579 -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,82 @@
|
|
|
1
|
+
__CLIENT_LOGIC__
|
|
2
|
+
|
|
3
|
+
const PANEL_CSS = `
|
|
4
|
+
[data-amadeus-qq]{box-sizing:border-box;color:var(--dsw-alias-label-primary,#203333);font:13px/1.6 system-ui,sans-serif;max-width:760px}
|
|
5
|
+
[data-amadeus-qq] *{box-sizing:border-box}
|
|
6
|
+
[data-amadeus-qq] h2{font-size:21px;margin:0 0 5px}
|
|
7
|
+
[data-amadeus-qq] p{margin:0 0 14px;opacity:.75}
|
|
8
|
+
[data-amadeus-qq] .summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin:14px 0}
|
|
9
|
+
[data-amadeus-qq] .metric{border:1px solid var(--dsw-alias-border-secondary,#8c8c8c55);border-radius:12px;padding:12px;background:var(--dsw-alias-bg-layer-2,#8c8c8c10)}
|
|
10
|
+
[data-amadeus-qq] .metric small{display:block;opacity:.68;font-size:11px}
|
|
11
|
+
[data-amadeus-qq] .metric strong{display:block;font-size:14px;margin-top:3px;word-break:break-word}
|
|
12
|
+
[data-amadeus-qq] button{border:1px solid #178a69;background:#178a69;color:white;border-radius:9px;padding:8px 14px;font:inherit;cursor:pointer}
|
|
13
|
+
[data-amadeus-qq] button:focus-visible{outline:2px solid #178a69;outline-offset:2px}
|
|
14
|
+
[data-amadeus-qq] .error{color:#ab3535;margin:12px 0}
|
|
15
|
+
@media(max-width:560px){[data-amadeus-qq] .summary{grid-template-columns:1fr}}
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
function Panel() {
|
|
19
|
+
const [setup, setSetup] = React.useState(null)
|
|
20
|
+
const [runtime, setRuntime] = React.useState(null)
|
|
21
|
+
const [error, setError] = React.useState('')
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
let active = true
|
|
24
|
+
const load = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const [a, b] = await Promise.all([
|
|
27
|
+
fetch('/api/amadeus-qq/setup', { credentials: 'same-origin' }),
|
|
28
|
+
fetch('/api/amadeus-qq/scenes', { credentials: 'same-origin' }),
|
|
29
|
+
])
|
|
30
|
+
const [left, right] = await Promise.all([a.json(), b.json()])
|
|
31
|
+
if (!a.ok || !b.ok || !left.ok || !right.ok) throw new Error('请在 DSH Owner 会话中打开管理控制台')
|
|
32
|
+
if (active) { setSetup(left.data); setRuntime(right.data); setError('') }
|
|
33
|
+
} catch (failure) {
|
|
34
|
+
if (active) setError(failure instanceof Error ? failure.message : '状态读取失败')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
void load()
|
|
38
|
+
return () => { active = false }
|
|
39
|
+
}, [])
|
|
40
|
+
const workspace = setup?.workspaces?.find(item => item.id === setup.workspaceId)
|
|
41
|
+
const rows = (setup?.groups?.length ?? 0) + (setup?.privatePeers?.length ?? 0)
|
|
42
|
+
const transportText = { connected: '已连接', connecting: '连接中', disconnected: '未连接', disabled: '已暂停' }[runtime?.health?.transport] ?? '未连接'
|
|
43
|
+
const metric = (label, value) => React.createElement('div', { className: 'metric' },
|
|
44
|
+
React.createElement('small', null, label), React.createElement('strong', null, value))
|
|
45
|
+
return React.createElement('div', { 'data-amadeus-qq': 'true' },
|
|
46
|
+
React.createElement('style', null, PANEL_CSS),
|
|
47
|
+
React.createElement('h2', null, 'Amadeus QQ'),
|
|
48
|
+
React.createElement('p', null, '首次设置和详细权限在受 DSH Owner 保护的本地 Web Console 中完成。'),
|
|
49
|
+
error ? React.createElement('div', { className: 'error', role: 'status' }, error) : null,
|
|
50
|
+
React.createElement('div', { className: 'summary' },
|
|
51
|
+
metric('插件', setup?.enabled ? '已启用' : '待配置'),
|
|
52
|
+
metric('NapCat', transportText),
|
|
53
|
+
metric('Bot / Owner', `${setup?.botId || '—'} / ${setup?.ownerId || '—'}`),
|
|
54
|
+
metric('Workspace / 会话', `${workspace?.title || '未选择'} · ${rows} 个`)),
|
|
55
|
+
React.createElement('button', { type: 'button', onClick: () => window.open('/api/amadeus-qq/console', '_blank', 'noopener,noreferrer') },
|
|
56
|
+
setup?.enabled ? '打开 Web Console' : '开始首次设置'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
inject: ['slots'],
|
|
61
|
+
apply(ctx) {
|
|
62
|
+
let active
|
|
63
|
+
const activate = () => {
|
|
64
|
+
if (active) return active.dispose
|
|
65
|
+
let dispose = () => {}
|
|
66
|
+
try {
|
|
67
|
+
dispose = ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
68
|
+
name: 'settings.section', id: 'amadeus-qq', order: 30, label: () => 'Amadeus QQ',
|
|
69
|
+
}, Panel)) || (() => {})
|
|
70
|
+
} catch (error) { console.error('[amadeus-qq] settings.section unavailable', error) }
|
|
71
|
+
const disposal = () => {
|
|
72
|
+
if (!active || active.dispose !== disposal) return
|
|
73
|
+
active = undefined
|
|
74
|
+
try { dispose() } catch {}
|
|
75
|
+
}
|
|
76
|
+
active = { dispose: disposal }
|
|
77
|
+
return disposal
|
|
78
|
+
}
|
|
79
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => activate(), 'amadeus-qq: settings page')
|
|
80
|
+
else activate()
|
|
81
|
+
},
|
|
82
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Pure client-side helpers, inlined into the browser bundle by build-client.
|
|
2
|
+
// No ESM imports here: the bundle is a lazy-CJS factory with only React injected.
|
|
3
|
+
|
|
4
|
+
export const API_ROOT = '/api/amadeus-qq'
|
|
5
|
+
export const MODE_IDS = Object.freeze([
|
|
6
|
+
'observe', 'safe-chat', 'enhanced', 'autonomous-social', 'owner-agent',
|
|
7
|
+
])
|
|
8
|
+
const OVERRIDE_VALUES = new Set(['inherit', 'allow', 'deny'])
|
|
9
|
+
|
|
10
|
+
export function revisionOf(entry) {
|
|
11
|
+
return entry?.revision ?? 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function draftFromEntry(entry) {
|
|
15
|
+
const overrides = {}
|
|
16
|
+
for (const capability of entry?.capabilities ?? []) {
|
|
17
|
+
if (capability.override === 'allow' || capability.override === 'deny') {
|
|
18
|
+
overrides[capability.id] = capability.override
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
sceneKey: entry?.key ?? '',
|
|
23
|
+
mode: entry?.permissions?.mode ?? entry?.effectiveMode ?? 'safe-chat',
|
|
24
|
+
overrides,
|
|
25
|
+
expectedRevision: revisionOf(entry),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isDraftDirty(entry, draft) {
|
|
30
|
+
if (!entry || !draft) return false
|
|
31
|
+
const base = draftFromEntry(entry)
|
|
32
|
+
if (base.mode !== draft.mode) return true
|
|
33
|
+
const keys = new Set([...Object.keys(base.overrides), ...Object.keys(draft.overrides ?? {})])
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
if ((base.overrides[key] ?? 'inherit') !== (draft.overrides?.[key] ?? 'inherit')) return true
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Decide which draft the editor should show after a Scene row is selected.
|
|
42
|
+
* A dirty draft is never replaced: the operator must save or cancel first, so
|
|
43
|
+
* an unsaved permission change can never be lost by an accidental click.
|
|
44
|
+
*/
|
|
45
|
+
export function nextDraftForSelection(currentEntry, currentDraft, targetEntry) {
|
|
46
|
+
if (currentDraft && isDraftDirty(currentEntry, currentDraft)) {
|
|
47
|
+
return { blocked: true, draft: currentDraft }
|
|
48
|
+
}
|
|
49
|
+
if (!targetEntry) return { blocked: false, draft: null }
|
|
50
|
+
const fresh = { ...draftFromEntry(targetEntry), sceneKey: targetEntry.key }
|
|
51
|
+
return { blocked: false, draft: fresh }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateDraft(entry, draft) {
|
|
55
|
+
if (!entry || !draft) return '未选择场景'
|
|
56
|
+
if (!MODE_IDS.includes(draft.mode)) return '模式无效'
|
|
57
|
+
if (draft.mode === 'owner-agent' && entry.ownerEligible !== true) return 'Owner Agent 仅可用于已配置的可信 Owner 私聊'
|
|
58
|
+
const known = new Set((entry.capabilities ?? []).map(capability => capability.id))
|
|
59
|
+
for (const [id, value] of Object.entries(draft.overrides ?? {})) {
|
|
60
|
+
if (!known.has(id)) return '包含未知能力:' + id
|
|
61
|
+
if (!OVERRIDE_VALUES.has(value)) return '覆盖值无效:' + id
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CAPABILITY_REASON_HINTS = Object.freeze({
|
|
67
|
+
'owner-directory-required': '需在可信 Owner 私聊使用 Owner Agent 模式,并对该能力显式 allow。',
|
|
68
|
+
'global-deny': '宿主全局策略拒绝该能力,Scene 设置无法解除。',
|
|
69
|
+
'account-publish-required': '该 QQ 账号未获授权使用 QQ 空间发布。',
|
|
70
|
+
'scene-deny': '该 Scene 显式拒绝(deny)此能力。',
|
|
71
|
+
'owner-private-required': '仅可信 Owner 私聊的 Owner Agent 模式可用。',
|
|
72
|
+
'owner-tool-required': '宿主未为该 Owner 场景配置此工具。',
|
|
73
|
+
'group-only': '仅群聊 Scene 可用;私聊不适用,与 Owner 权限无关。',
|
|
74
|
+
'qzone-read-explicit': '需在自主社交或可信 Owner Agent 模式显式 allow 才能读取 QZone。',
|
|
75
|
+
'qzone-use-explicit': '需在自主社交或可信 Owner Agent 模式显式 allow 才能使用 QZone。',
|
|
76
|
+
'profile-write-explicit': '账号资料写入仅限可信 Owner 私聊的 Owner Agent 模式并显式 allow。',
|
|
77
|
+
'owner-workspace-required': 'Owner 工作区需可信 Owner 私聊、Owner Agent 模式并显式 allow。',
|
|
78
|
+
mode: '当前模式未包含此能力。',
|
|
79
|
+
'scene-allow': '该 Scene 显式允许,但保存后按最新模式重算。',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fixed Chinese guidance for an implemented-but-denied capability. Uses only
|
|
84
|
+
* known denial reasons; an unknown reason falls back to a generic sentence and
|
|
85
|
+
* never echoes the server value. `Object.hasOwn` keeps names such as
|
|
86
|
+
* `constructor` or `toString` from resolving to inherited Object members.
|
|
87
|
+
*/
|
|
88
|
+
export function capabilityStateHint(capability) {
|
|
89
|
+
if (!capability || capability.implemented !== true || capability.effective === true) return null
|
|
90
|
+
const reason = capability.reason
|
|
91
|
+
return typeof reason === 'string' && Object.hasOwn(CAPABILITY_REASON_HINTS, reason)
|
|
92
|
+
? CAPABILITY_REASON_HINTS[reason]
|
|
93
|
+
: '当前策略拒绝该能力。'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Configuration hints; never a path, token or credential. */
|
|
97
|
+
export function capabilityConfigHint(capability, runtime = {}) {
|
|
98
|
+
if (!capability || capability.implemented !== true) return null
|
|
99
|
+
if (capability.id === 'qq.owner.workspace' && runtime.ownerWorkspace?.configured !== true) {
|
|
100
|
+
return '需要核心用实际 QQ 配置注册专用目录并授权后才可用。'
|
|
101
|
+
}
|
|
102
|
+
if ((capability.id === 'qq.qzone.use' || capability.id === 'qq.qzone.read' || capability.id === 'qq.qzone.publish')
|
|
103
|
+
&& runtime.providers?.qzone?.installed !== true) {
|
|
104
|
+
return '需要配置 QZone loopback Provider 后才可用;当前未观测到接线。'
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildSaveRequest(entry, draft) {
|
|
110
|
+
const overrides = {}
|
|
111
|
+
for (const [id, value] of Object.entries(draft.overrides ?? {})) {
|
|
112
|
+
if (value === 'allow' || value === 'deny') overrides[id] = value
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
scene: {
|
|
116
|
+
botId: entry.scene.botId,
|
|
117
|
+
kind: entry.scene.kind,
|
|
118
|
+
peerId: entry.scene.peerId,
|
|
119
|
+
},
|
|
120
|
+
access: { mode: draft.mode, overrides },
|
|
121
|
+
expectedRevision: draft.expectedRevision,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readPayload(response) {
|
|
126
|
+
let payload
|
|
127
|
+
try {
|
|
128
|
+
payload = await response.json()
|
|
129
|
+
} catch {
|
|
130
|
+
const error = new Error('管理接口返回了无法解析的响应(HTTP ' + response.status + ')')
|
|
131
|
+
error.status = response.status
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok || !payload || payload.ok !== true) {
|
|
135
|
+
const error = new Error(payload && payload.error ? payload.error : 'HTTP ' + response.status)
|
|
136
|
+
error.status = response.status
|
|
137
|
+
if (typeof payload?.code === 'string') error.code = payload.code
|
|
138
|
+
if (response.status === 409) error.conflict = true
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
141
|
+
return payload.data
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* True only when the management surface is genuinely unusable for this page:
|
|
146
|
+
* authentication/owner admission failed, or the Host lacks the management
|
|
147
|
+
* service. Only the explicitly listed policy rejections and session-cancel
|
|
148
|
+
* failures stay editable; any unknown code falls back to its HTTP status so an
|
|
149
|
+
* unrecognized 401/403/503 keeps the editor locked (fail closed).
|
|
150
|
+
*/
|
|
151
|
+
const EDITABLE_FAILURE_CODES = new Set([
|
|
152
|
+
'QQ_OWNER_AGENT_FORBIDDEN',
|
|
153
|
+
'QQ_SCENE_UNAUTHORIZED',
|
|
154
|
+
'QQ_SESSION_CANCEL_FAILED',
|
|
155
|
+
'QQ_SESSION_CANCEL_UNAVAILABLE',
|
|
156
|
+
'QQ_PERMISSION_CONFLICT',
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
export function requiresReadOnly(error) {
|
|
160
|
+
const code = typeof error?.code === 'string' ? error.code : ''
|
|
161
|
+
if (code === 'QQ_AUTH_REQUIRED' || code === 'QQ_MANAGEMENT_UNAVAILABLE') return true
|
|
162
|
+
if (EDITABLE_FAILURE_CODES.has(code)) return false
|
|
163
|
+
const status = Number(error?.status)
|
|
164
|
+
return status === 401 || status === 403 || status === 503
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Fixed Chinese guidance per management failure code; never echoes server internals. */
|
|
168
|
+
export function managementErrorMessage(error) {
|
|
169
|
+
const code = typeof error?.code === 'string' ? error.code : ''
|
|
170
|
+
if (code === 'QQ_PERMISSION_CONFLICT') {
|
|
171
|
+
return '权限已被其他窗口修改(409);请重新加载后再编辑。'
|
|
172
|
+
}
|
|
173
|
+
if (code === 'QQ_AUTH_REQUIRED') {
|
|
174
|
+
return '当前请求没有通过 DSH 认证或 Owner 授权;请从已认证的 DSH 页面访问。'
|
|
175
|
+
}
|
|
176
|
+
if (code === 'QQ_MANAGEMENT_UNAVAILABLE') {
|
|
177
|
+
return '管理接口不可用;QQ 插件可能未启用或缺少 Host 服务。'
|
|
178
|
+
}
|
|
179
|
+
if (code === 'QQ_OWNER_AGENT_FORBIDDEN') {
|
|
180
|
+
return 'Owner Agent 仅可用于已配置的可信 Owner 私聊;当前场景或配置不允许保存该模式。'
|
|
181
|
+
}
|
|
182
|
+
if (code === 'QQ_SCENE_UNAUTHORIZED') {
|
|
183
|
+
return '该场景已不在 QQ 允许列表内;请更新配置并刷新后再操作。'
|
|
184
|
+
}
|
|
185
|
+
if (code === 'QQ_SESSION_CANCEL_FAILED' || code === 'QQ_SESSION_CANCEL_UNAVAILABLE') {
|
|
186
|
+
return '权限可能已写入,但旧 QQ Session 未能安全取消;请刷新确认最新 revision 后再编辑。'
|
|
187
|
+
}
|
|
188
|
+
if (code === 'QQ_DELIVERY_IN_FLIGHT') {
|
|
189
|
+
return '该投递仍在进行中,现在不能标记为失败;请稍后刷新重试。'
|
|
190
|
+
}
|
|
191
|
+
if (code === 'QQ_DELIVERY_NOT_UNKNOWN') {
|
|
192
|
+
return '该投递已不是 unknown 状态;请刷新确认后再操作。'
|
|
193
|
+
}
|
|
194
|
+
if (code === 'QQ_DELIVERY_NOT_FOUND') {
|
|
195
|
+
return '未找到该 actionId 的未知投递;请刷新后确认。'
|
|
196
|
+
}
|
|
197
|
+
if (code === 'QQ_REQUEST_INVALID') {
|
|
198
|
+
return '管理请求无效;请刷新后重试。'
|
|
199
|
+
}
|
|
200
|
+
const status = Number(error?.status)
|
|
201
|
+
if (status === 401 || status === 403) {
|
|
202
|
+
return '当前请求没有通过 DSH 认证或 Owner 授权(HTTP ' + status + ');请从已认证的 DSH 页面访问。'
|
|
203
|
+
}
|
|
204
|
+
if (status === 503) {
|
|
205
|
+
return '管理接口不可用(503);QQ 插件可能未启用或缺少 Host 服务。'
|
|
206
|
+
}
|
|
207
|
+
return 'QQ 管理请求失败' + (Number.isInteger(status) ? '(HTTP ' + status + ')' : '')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function fetchScenes(fetchImpl = fetch) {
|
|
211
|
+
const response = await fetchImpl(API_ROOT + '/scenes', {
|
|
212
|
+
credentials: 'same-origin',
|
|
213
|
+
headers: { 'x-amadeus-qq': '1' },
|
|
214
|
+
})
|
|
215
|
+
return readPayload(response)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function savePermissions(entry, draft, fetchImpl = fetch) {
|
|
219
|
+
const response = await fetchImpl(API_ROOT + '/scenes/permissions', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
credentials: 'same-origin',
|
|
222
|
+
headers: { 'content-type': 'application/json', 'x-amadeus-qq': '1' },
|
|
223
|
+
body: JSON.stringify(buildSaveRequest(entry, draft)),
|
|
224
|
+
})
|
|
225
|
+
return readPayload(response)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Storage key for one operator confirmation input. It is scoped to the Scene
|
|
230
|
+
* and prefixed, so two Scenes with the same actionId never share a draft, and
|
|
231
|
+
* prototype names such as `__proto__` or `constructor` can never be the key.
|
|
232
|
+
* The API still receives the untouched actionId.
|
|
233
|
+
*/
|
|
234
|
+
export function deliveryInputKey(entry, actionId) {
|
|
235
|
+
return 'delivery-confirm:' + String(entry?.key ?? '') + '|' + String(actionId)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Fixed Settings copy for the Owner workspace. It never includes a directory
|
|
240
|
+
* path, token or provider URL; an unwired core keeps the feature "pending".
|
|
241
|
+
*/
|
|
242
|
+
export function ownerWorkspaceSummary(status) {
|
|
243
|
+
const configured = status?.configured === true
|
|
244
|
+
const writes = status?.writes === true
|
|
245
|
+
const maxBytes = Number.isSafeInteger(status?.maxBytes) && status.maxBytes > 0 ? status.maxBytes : 0
|
|
246
|
+
return {
|
|
247
|
+
state: configured ? '已接线' : 'pending(待核心接线)',
|
|
248
|
+
writes: writes ? '可写(逐次审批)' : '只读',
|
|
249
|
+
maxBytes,
|
|
250
|
+
note: configured
|
|
251
|
+
? '目录与写入授权由插件配置决定;本页不显示路径,也不保存配置。实机写入仍需人工审批,尚未完成真实验证。'
|
|
252
|
+
: 'Owner preset 已随包;核心尚未用实际 QQ 配置注册 Owner 工作区(未接线),当前不可读、不可写。',
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Provider labels only; never a root path, loopback URL or token value. */
|
|
257
|
+
export function providerSummary(providers) {
|
|
258
|
+
const stateOf = provider => {
|
|
259
|
+
if (provider?.installed !== true) return '未观测到接线'
|
|
260
|
+
if (provider?.configured !== true) return '已安装,未配置'
|
|
261
|
+
return provider?.enabled === true ? '已配置' : '已安装,未启用'
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
mediaSend: stateOf(providers?.mediaSend),
|
|
265
|
+
qzone: stateOf(providers?.qzone),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Manual reconciliation for one unknown delivery. Only the owner management
|
|
271
|
+
* surface can do this, and the body never carries message content: just the
|
|
272
|
+
* Scene identity, the operator-confirmed actionId and the fixed outcome.
|
|
273
|
+
*/
|
|
274
|
+
export async function resolveUnknownDelivery(entry, actionId, fetchImpl = fetch) {
|
|
275
|
+
const response = await fetchImpl(API_ROOT + '/deliveries/resolve', {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
credentials: 'same-origin',
|
|
278
|
+
headers: { 'content-type': 'application/json', 'x-amadeus-qq': '1' },
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
scene: {
|
|
281
|
+
botId: entry.scene.botId,
|
|
282
|
+
kind: entry.scene.kind,
|
|
283
|
+
peerId: entry.scene.peerId,
|
|
284
|
+
},
|
|
285
|
+
actionId,
|
|
286
|
+
outcome: 'failed',
|
|
287
|
+
}),
|
|
288
|
+
})
|
|
289
|
+
return readPayload(response)
|
|
290
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
export const Config = z.object({
|
|
4
|
+
presetGuard: z.boolean().default(false),
|
|
5
|
+
level: z.natural().max(4).default(1),
|
|
6
|
+
allowSearch: z.boolean().default(false),
|
|
7
|
+
enabled: z.boolean().default(false),
|
|
8
|
+
connect: z.boolean().default(false),
|
|
9
|
+
setupRevision: z.natural().default(0),
|
|
10
|
+
statePath: z.string().default(''),
|
|
11
|
+
mediaRoot: z.string().default(''),
|
|
12
|
+
sendRoot: z.string().default(''),
|
|
13
|
+
qzoneEnabled: z.boolean().default(false),
|
|
14
|
+
qzoneLoopbackUrl: z.string().default(''),
|
|
15
|
+
qzoneTokenEnv: z.string().default(''),
|
|
16
|
+
ownerWorkspaceRoot: z.string().default(''),
|
|
17
|
+
ownerWorkspaceWrites: z.boolean().default(false),
|
|
18
|
+
ownerWorkspaceMaxBytes: z.natural().min(1).max(256 * 1024).default(64 * 1024),
|
|
19
|
+
asrProviderId: z.string().default(''),
|
|
20
|
+
ffmpegPath: z.string().default(''),
|
|
21
|
+
sessionCwd: z.string().default(''),
|
|
22
|
+
workspaceId: z.string().default(''),
|
|
23
|
+
agentPreset: z.string().default(''),
|
|
24
|
+
httpUrl: z.string().default(''),
|
|
25
|
+
wsUrl: z.string().default(''),
|
|
26
|
+
tokenEnv: z.string().default(''),
|
|
27
|
+
token: z.string().role('secret').default(''),
|
|
28
|
+
policy: z.object({
|
|
29
|
+
epoch: z.natural().default(0),
|
|
30
|
+
botIds: z.array(z.string()).default([]),
|
|
31
|
+
groupIds: z.array(z.string()).default([]),
|
|
32
|
+
privateIds: z.array(z.string()).default([]),
|
|
33
|
+
sceneRevisions: z.dict(z.natural()).default({}),
|
|
34
|
+
pausedScenes: z.array(z.string()).default([]),
|
|
35
|
+
triggerNames: z.array(z.string()).default([]),
|
|
36
|
+
}).default({}),
|
|
37
|
+
permissions: z.object({
|
|
38
|
+
defaultMode: z.string().default('safe-chat'),
|
|
39
|
+
groupDefaultMode: z.string().default('safe-chat'),
|
|
40
|
+
privateDefaultMode: z.string().default('safe-chat'),
|
|
41
|
+
ownerPrivateIds: z.array(z.string()).default([]),
|
|
42
|
+
ownerTools: z.array(z.string()).default([]),
|
|
43
|
+
globalDeny: z.array(z.string()).default([]),
|
|
44
|
+
accountGrants: z.object({
|
|
45
|
+
byBotId: z.dict(z.object({ qzonePublish: z.boolean().default(false) })).default({}),
|
|
46
|
+
}).default({}),
|
|
47
|
+
}).default({}),
|
|
48
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
:root{
|
|
2
|
+
--bg:#f5f8fa;--paper:#fff;--ink:#192b39;--ink2:#233748;--muted:#728397;--muted2:#98a8b6;
|
|
3
|
+
--line:#e6edf1;--line-strong:#dae5eb;--soft:#f7fafb;--green:#0b875c;--green-soft:#e8f7ef;--green-ink:#08704e;
|
|
4
|
+
--blue:#2679c7;--blue-soft:#edf5fe;--orange:#d77b20;--orange-soft:#fff3e6;--pink:#a967cc;--pink-soft:#f8f0fc;
|
|
5
|
+
--warning:#a87826;--warning-soft:#fff9e8;--danger:#b85a56;--danger-soft:#fff0ed;
|
|
6
|
+
--shadow:0 12px 34px rgba(22,55,73,.045);--shadow2:0 8px 30px rgba(21,46,69,.08)
|
|
7
|
+
}
|
|
8
|
+
[data-theme="dark"]{
|
|
9
|
+
--bg:#131d25;--paper:#1b2932;--ink:#eaf3f4;--ink2:#d9e5e9;--muted:#a0b4bf;--muted2:#7f97a6;
|
|
10
|
+
--line:#30404b;--line-strong:#405363;--soft:#22333e;--green:#78d6af;--green-soft:#1b4639;--green-ink:#91e6bf;
|
|
11
|
+
--blue:#8fc0f1;--blue-soft:#263f58;--orange:#f4bd80;--orange-soft:#4c392b;--pink:#e3b3f2;--pink-soft:#45314f;
|
|
12
|
+
--warning:#f0cd8c;--warning-soft:#473e2c;--danger:#ecaaa0;--danger-soft:#4b2e30;
|
|
13
|
+
--shadow:0 12px 32px rgba(0,0,0,.13);--shadow2:0 10px 36px rgba(0,0,0,.2)
|
|
14
|
+
}
|
|
15
|
+
*{box-sizing:border-box}html{scrollbar-gutter:stable}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.58 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",Arial,sans-serif;letter-spacing:.002em}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{opacity:.55;cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:2px solid var(--green);outline-offset:2px}a{color:var(--green);text-decoration:none}a:hover{text-decoration:underline}h1,h2,h3,h4,p{margin-top:0}h1{font-size:26px;letter-spacing:-.04em;margin-bottom:3px;line-height:1.35}h2{font-size:19px;letter-spacing:-.025em;margin-bottom:6px}h3{font-size:15px;margin-bottom:6px}p{color:var(--muted)}.icon{width:18px;height:18px;flex-shrink:0;display:inline-block;vertical-align:middle;stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.85}.icon.sm{width:15px;height:15px}.icon.lg{width:22px;height:22px}.app{display:grid;grid-template-columns:220px minmax(0,1fr);min-height:100vh}.sidebar{background:var(--paper);border-right:1px solid var(--line);padding:21px 14px 17px;position:sticky;top:0;height:100vh;display:flex;flex-direction:column;gap:27px;z-index:5}.brand{display:flex;gap:10px;align-items:center;padding:1px 7px}.brand-logo{position:relative;width:37px;height:37px;border-radius:13px;background:linear-gradient(135deg,#159d78,#2764b0);display:grid;place-items:center;color:white;font-weight:900;font-size:17px;letter-spacing:-.1em;box-shadow:0 6px 13px #1c8f9250}.brand-logo:after{content:"";position:absolute;right:1px;top:1px;background:#91e2c0;width:9px;height:9px;border-radius:50%;border:2px solid var(--paper)}.brand-label{font-size:15px;font-weight:800;line-height:1.1;letter-spacing:-.02em}.brand-sub{font-size:10px;color:var(--muted);margin-top:4px}.brand-tag{color:var(--green-ink);background:var(--green-soft);border-radius:100px;font-size:10px;padding:2px 7px;margin-left:4px;font-weight:700}.nav-label{color:var(--muted2);padding:0 12px;font-size:10px;font-weight:800;letter-spacing:.1em;margin-bottom:9px}.nav{display:grid;gap:6px}.nav button{border:0;background:transparent;width:100%;padding:11px 12px;border-radius:11px;text-align:left;display:flex;align-items:center;gap:12px;color:var(--ink2);font-size:13px;font-weight:550}.nav button:hover{background:var(--soft)}.nav button.active{background:var(--green-soft);color:var(--green-ink);font-weight:800}.nav button .icon{opacity:.75}.nav-divider{margin:6px 11px;border-bottom:1px solid var(--line)}.sidebar-bottom{margin-top:auto}.plugin-status{border:1px solid var(--line);border-radius:14px;padding:15px 13px;background:var(--soft)}.plugin-status .micro{font-size:11px;color:var(--muted)}.plugin-status strong{display:flex;align-items:center;gap:7px;margin:9px 0 5px;color:var(--green-ink)}.plugin-status p{font-size:11px;line-height:1.6;margin-bottom:10px}.plugin-status .idline{font-size:11px;display:flex;justify-content:space-between;margin:5px 0}.plugin-status .idline span:first-child{color:var(--muted)}.plugin-status .idline b{font-size:11px}.main{min-width:0;padding:25px 26px 50px}.main-width{max-width:1600px;margin:0 auto}.head{display:flex;justify-content:space-between;align-items:flex-start;gap:22px;margin-bottom:23px}.page-lead .eyebrow{font-size:10px;text-transform:uppercase;font-weight:750;letter-spacing:.17em;color:var(--green-ink);margin-bottom:6px}.page-lead p{font-size:12px;margin:0}.head-right{display:flex;gap:9px;align-items:stretch;flex-wrap:wrap;justify-content:flex-end}.head-status{padding:8px 12px;min-height:50px;background:var(--paper);border:1px solid var(--line);border-radius:12px;display:flex;align-items:center;gap:10px;min-width:137px;box-shadow:var(--shadow)}.head-status .label{font-size:10px;color:var(--muted)}.head-status strong{font-size:12px;display:block;line-height:1.25}.head-status .statusicon{width:31px;height:31px;border-radius:10px;background:var(--blue-soft);color:var(--blue);display:grid;place-items:center}.head-status.online .statusicon{background:var(--green-soft);color:var(--green-ink)}.head-status.online strong{color:var(--green-ink)}.demo-pill{display:inline-flex;align-items:center;padding:4px 9px;border-radius:100px;background:var(--soft);border:1px solid var(--line);font-size:10px;color:var(--muted);white-space:nowrap}.button{border:1px solid var(--line-strong);border-radius:10px;background:var(--paper);font-size:12px;font-weight:730;padding:9px 12px;display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:36px;white-space:nowrap}.button:hover{border-color:#9dcdbb;background:var(--soft)}.button.primary{color:#fff;background:#0b8c61;border-color:#0b8c61;box-shadow:0 4px 12px rgba(0,135,92,.12)}.button.primary:hover{background:#067851}.button.soft{border-color:#cceadb;background:var(--green-soft);color:var(--green-ink)}.button.ghost{background:transparent;border-color:transparent;color:var(--muted)}.button.mini{padding:6px 10px;min-height:29px;font-size:11px}.button.linklike{border:0;color:var(--blue);background:transparent;padding:0}.tag{display:inline-flex;gap:5px;align-items:center;white-space:nowrap;font-size:10px;font-weight:740;padding:3px 8px;background:var(--soft);color:var(--muted);border:1px solid var(--line);border-radius:50px}.tag.success{background:var(--green-soft);border-color:#c7e6d7;color:var(--green-ink)}.tag.info{background:var(--blue-soft);border-color:#d8e6f7;color:var(--blue)}.tag.warn{background:var(--warning-soft);border-color:#f0e3c3;color:var(--warning)}.tag.danger{background:var(--danger-soft);border-color:#ebccc8;color:var(--danger)}.tag.demo{border-style:dashed}.dot{width:7px;height:7px;border-radius:50%;display:inline-block;background:currentColor}.view{display:none;animation:fadein .12s ease}.view.active{display:block}@keyframes fadein{from{opacity:.6;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}.card{background:var(--paper);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow)}.padded{padding:20px}.muted{color:var(--muted)}.micro{font-size:11px}.fine{font-size:12px;color:var(--muted)}.sect-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:22px 0 12px}.sect-head:first-child{margin-top:0}.sect-head h2{font-size:15px;margin:0}.section-sub{font-size:11px;color:var(--muted)}.stats-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:13px}.stat-card{padding:19px;min-height:129px}.stat-top{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:11px}.stat-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:10px;background:var(--green-soft);color:var(--green-ink)}.stat-icon.blue{background:var(--blue-soft);color:var(--blue)}.stat-icon.orange{background:var(--orange-soft);color:var(--orange)}.stat-icon.pink{background:var(--pink-soft);color:var(--pink)}.stat-value{font-size:27px;font-weight:830;letter-spacing:-.05em;line-height:1.4;margin:9px 0 0}.stat-caption{font-size:11px;color:var(--muted)}.two-columns{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(300px,.95fr);gap:15px}.activity-row{display:flex;align-items:center;gap:13px;padding:13px 0;border-top:1px solid var(--line)}.activity-row:first-child{border-top:0}.activity-ico{display:grid;place-items:center;border-radius:11px;background:var(--blue-soft);color:var(--blue);width:34px;height:34px}.activity-content{flex:1;min-width:0}.activity-content strong{display:block;font-size:12px}.activity-content small{display:block;color:var(--muted);font-size:10px}.activity-time{font-size:10px;color:var(--muted2)}.flowline{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}.flowstep{position:relative;border:1px solid var(--line);border-radius:12px;padding:15px 11px;background:var(--soft);min-height:86px}.flowstep span{color:var(--green-ink);font-size:10px;font-weight:800}.flowstep b{display:block;font-size:12px;margin-top:6px}.flowstep small{font-size:10px;color:var(--muted)}.callout{display:flex;align-items:flex-start;gap:11px;background:var(--green-soft);color:var(--green-ink);border:1px solid #cbe8d9;border-radius:12px;padding:13px 14px;font-size:12px}.callout p{color:var(--muted);font-size:11px;margin:4px 0 0}.scene-layout{display:grid;grid-template-columns:277px minmax(0,1fr);gap:15px;align-items:start}.scene-panel{padding:14px;position:sticky;top:15px}.scene-tabs{display:flex;background:var(--soft);padding:4px;border-radius:10px;gap:3px;margin-bottom:13px}.scene-tabs button{flex:1;border:0;border-radius:8px;background:transparent;color:var(--muted);padding:7px;font-size:11px;font-weight:700;white-space:nowrap}.scene-tabs button.active{background:var(--paper);color:var(--green-ink);box-shadow:0 2px 6px #00000008}.scene-tools{display:flex;gap:7px;margin-bottom:12px}.input{min-width:0;width:100%;border:1px solid var(--line-strong);border-radius:9px;background:var(--paper);padding:9px 11px;font-size:12px}.input:focus{border-color:var(--green)}.scene-tools .input{flex:1;width:auto}.scene-list{display:grid;gap:5px}.scene-item{display:flex;align-items:center;width:100%;text-align:left;border:1px solid transparent;background:transparent;gap:10px;padding:11px 9px;border-radius:12px}.scene-item:hover{background:var(--soft)}.scene-item.active{border-color:#a6dbc3;background:var(--green-soft)}.avatar{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;color:var(--orange);background:var(--orange-soft);flex-shrink:0}.avatar.private{background:var(--blue-soft);color:var(--blue)}.scene-info{flex:1;min-width:0}.scene-info b{display:block;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.scene-info small{display:block;font-size:10px;color:var(--muted);line-height:1.5}.scene-item>.icon{color:var(--muted)}.scene-aside-foot{margin:17px 5px 2px;color:var(--muted);font-size:11px}.scene-detail{padding:21px 21px 16px;min-height:760px}.detail-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}.detail-header-main{display:flex;gap:12px;align-items:center}.detail-header-main .avatar{height:43px;width:43px;border-radius:14px}.detail-header h2{font-size:20px;margin-bottom:2px}.detail-header p{font-size:11px;margin:0}.detail-action{display:flex;gap:7px;align-items:center}.divider{height:1px;background:var(--line);margin:20px 0}.mini-title{font-size:11px;color:var(--muted);font-weight:650;margin:0 0 8px}.workspace-banner{display:flex;align-items:center;gap:12px;background:var(--soft);border:1px solid var(--line);border-radius:11px;padding:12px 14px}.workspace-banner .statusicon{height:36px;width:36px;display:grid;place-items:center;border-radius:10px;background:var(--blue-soft);color:var(--blue)}.workspace-banner .grow{flex:1}.workspace-banner b{font-size:12px}.workspace-banner small{display:block;color:var(--muted);font-size:11px;margin-top:3px}.preset-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;gap:8px}.preset-header .small{font-size:10px;color:var(--muted)}.preset-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px}.preset-option{text-align:left;min-height:80px;border:1px solid var(--line);border-radius:11px;background:var(--paper);padding:11px;display:flex;flex-direction:column;gap:4px;position:relative;transition:border-color .15s,background .15s}.preset-option:hover{border-color:#91caae}.preset-option.active{background:var(--green-soft);border-color:#70c49c;box-shadow:0 0 0 1px #70c49c2e}.preset-option .p-head{display:flex;align-items:center;gap:5px}.preset-option b{font-size:12px;white-space:nowrap}.preset-option small{font-size:10px;color:var(--muted);line-height:1.42}.preset-option .check{margin-left:auto;color:var(--green-ink)}.preset-option:not(.active) .check{display:none}.preset-desc{margin-top:10px;padding:10px 12px;border-radius:10px;background:var(--soft);border:1px solid var(--line);font-size:11px;color:var(--muted)}.preset-desc strong{color:var(--ink)}.custom-alert{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:11px;padding:10px 12px;background:var(--warning-soft);border:1px solid #e9dfb9;border-radius:10px;font-size:11px;color:var(--warning)}.state-row{display:flex;align-items:center;justify-content:space-between;margin:20px 0 11px}.state-row strong{font-size:13px}.setting-groups{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.group-card{border:1px solid var(--line);border-radius:12px;background:var(--paper);overflow:hidden}.group-card summary{list-style:none;display:flex;align-items:center;gap:10px;padding:13px;cursor:pointer;min-height:67px}.group-card summary::-webkit-details-marker{display:none}.group-card summary:hover{background:var(--soft)}.group-icon{width:33px;height:33px;display:grid;place-items:center;border-radius:10px;background:var(--blue-soft);color:var(--blue);flex-shrink:0}.group-icon.green{background:var(--green-soft);color:var(--green-ink)}.group-icon.orange{background:var(--orange-soft);color:var(--orange)}.group-icon.pink{background:var(--pink-soft);color:var(--pink)}.group-summary{flex:1;min-width:0}.group-summary b{display:block;font-size:12px}.group-summary small{display:block;font-size:10px;color:var(--muted);white-space:normal}.group-card .arrow{color:var(--muted)}.group-card[open] .arrow{transform:rotate(90deg)}.group-content{padding:1px 13px 12px;border-top:1px solid var(--line)}.toggle-line{display:flex;gap:10px;align-items:center;padding:11px 0;border-bottom:1px solid var(--line)}.toggle-line:last-child{border:0}.toggle-line>div:first-child{flex:1;min-width:0}.toggle-line strong{font-size:11px;display:block}.toggle-line small{font-size:10px;color:var(--muted);display:block;margin-top:3px}.toggle-line.disabled{opacity:.6}.switch{display:flex;align-items:center;position:relative;cursor:pointer;min-width:34px}.switch input{position:absolute;opacity:0;width:36px;height:22px}.switch .track{height:21px;width:37px;display:block;position:relative;background:#c7d3d9;border-radius:999px;transition:background .16s}.switch .track:after{content:"";position:absolute;top:3px;left:3px;height:15px;width:15px;border-radius:50%;background:#fff;transition:left .16s}.switch input:checked+.track{background:#0e9b6a}.switch input:checked+.track:after{left:19px}.switch input:focus-visible+.track{outline:2px solid var(--green);outline-offset:3px}.switch input:disabled+.track{opacity:.52}.select-line{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 0;border-bottom:1px solid var(--line)}.select-line:last-child{border:0}.select-line b{font-size:11px}.select-line small{font-size:10px;color:var(--muted);display:block;margin-top:2px}.select-mini{background:var(--soft);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:11px;max-width:155px}.detail-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:18px;border-top:1px solid var(--line);margin-top:20px}.detail-footer .fine{max-width:530px;font-size:11px}.detail-footer-actions{display:flex;gap:7px;flex-shrink:0}.capability-summary{background:var(--soft);border:1px solid var(--line);border-radius:11px;padding:12px;margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:11px}.capability-summary .soft{color:var(--muted)}.legend{font-size:10px;color:var(--muted)}.feature-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.feature-tile{padding:19px;min-height:184px}.feature-title{display:flex;align-items:center;gap:10px;margin-bottom:12px}.feature-title h3{margin:0}.feature-tile p{font-size:11px;margin:8px 0 12px;line-height:1.7}.feature-foot{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto}.cap-list{display:grid;gap:7px;margin:13px 0}.cap-list div{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:8px 0;border-top:1px dashed var(--line);font-size:11px}.cap-list .tag{flex-shrink:0}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.form-card{padding:20px}.form-card h3{display:flex;gap:8px;align-items:center}.form-card p{font-size:11px;line-height:1.75}.form-label{font-size:11px;font-weight:700;display:block;margin:12px 0 6px}.form-hint{color:var(--muted);font-size:10px;margin:6px 0}.form-row{display:flex;gap:8px;align-items:center}.form-row>*:first-child{flex:1}.status-table{width:100%;border-collapse:collapse;font-size:11px}.status-table td{padding:11px 4px;border-bottom:1px solid var(--line)}.status-table td:last-child{text-align:right}.status-table tr:last-child td{border:0}.status-table td:first-child{color:var(--muted)}.log-line{display:flex;align-items:start;gap:11px;padding:12px 0;border-top:1px solid var(--line);font-size:11px}.log-line:first-child{border:0}.log-time{font-family:ui-monospace,Consolas,monospace;color:var(--muted);white-space:nowrap}.check-flow{display:grid;gap:10px}.check-row{display:flex;align-items:center;gap:10px;padding:11px 13px;border-radius:10px;background:var(--soft);border:1px solid var(--line);font-size:11px}.check-row div{flex:1}.check-row b{display:block;font-size:12px}.check-row small{color:var(--muted)}.test-chip{color:var(--green-ink);font-weight:700;font-size:10px}.global-flags{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.global-flags .tag{font-size:11px}.inline-notice{padding:12px 14px;border:1px solid var(--line);background:var(--soft);border-radius:11px;font-size:11px;color:var(--muted)}.footer{font-size:11px;color:var(--muted2);padding:30px 2px 0;text-align:center}.desktop-frame{max-width:690px;margin:15px auto;border:1px solid var(--line);border-radius:19px;padding:22px;background:var(--paper);box-shadow:var(--shadow2)}.desktop-inner{display:grid;grid-template-columns:160px minmax(0,1fr);gap:22px}.desktop-rail{border-right:1px solid var(--line);padding-right:14px}.desktop-rail div{font-size:12px;padding:10px;border-radius:9px;color:var(--muted)}.desktop-rail div.active{background:var(--green-soft);color:var(--green-ink);font-weight:700}.desktop-content h3{font-size:20px}.desktop-content p{font-size:11px}.desktop-summary{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin:15px 0}.desktop-summary .metric{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--soft)}.desktop-summary small{display:block;font-size:10px;color:var(--muted)}.desktop-summary b{font-size:12px}.demo-hint{display:flex;align-items:center;gap:7px;font-size:10px;color:var(--muted);margin-top:9px}.toast{position:fixed;bottom:22px;right:24px;max-width:390px;border-radius:13px;box-shadow:var(--shadow2);border:1px solid var(--line-strong);background:var(--paper);z-index:99;padding:13px 17px;color:var(--ink);font-size:12px;display:none}.toast.show{display:block}.toast strong{display:block;margin-bottom:3px}.modal{border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:16px;padding:24px;width:min(430px,90vw);box-shadow:var(--shadow2)}.modal::backdrop{background:#16252a80;backdrop-filter:blur(3px)}.modal h2{margin-bottom:8px}.modal p{font-size:12px}.modal .actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.radio-pair{display:flex;gap:8px;margin:13px 0}.radio-pair label{border:1px solid var(--line);border-radius:9px;padding:10px 15px;font-size:12px;flex:1}.radio-pair input{accent-color:#0d9769}.scroll-guard{overflow:auto}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.hide{display:none!important}
|
|
16
|
+
@media(max-width:1300px){.app{grid-template-columns:195px minmax(0,1fr)}.main{padding:22px 18px}.head-status{min-width:117px}.scene-layout{grid-template-columns:238px minmax(0,1fr)}.setting-groups{grid-template-columns:1fr}.preset-grid{grid-template-columns:repeat(3,1fr)}}
|
|
17
|
+
@media(max-width:950px){.sidebar{position:relative;height:auto;min-height:100vh}.head{flex-direction:column}.head-right{justify-content:flex-start}.stats-grid{grid-template-columns:repeat(2,1fr)}.scene-layout{grid-template-columns:1fr}.scene-panel{position:static}.scene-list{grid-template-columns:repeat(2,minmax(0,1fr))}.feature-grid{grid-template-columns:repeat(2,1fr)}.two-columns{grid-template-columns:1fr}.preset-grid{grid-template-columns:repeat(4,1fr)}}
|
|
18
|
+
@media(max-width:650px){.app{display:block}.sidebar{height:auto;min-height:0;position:static;padding:13px 14px;gap:10px}.brand{padding:0}.nav{display:flex;overflow:auto;gap:3px}.nav-label,.sidebar-bottom,.nav-divider{display:none}.nav button{white-space:nowrap;padding:9px}.main{padding:18px 13px 30px}.head-status{min-width:auto}.head-right .head-status{flex:1}.scene-detail{padding:16px 12px}.scene-list{grid-template-columns:1fr}.preset-grid{grid-template-columns:repeat(2,1fr)}.stats-grid,.feature-grid,.form-grid,.setting-groups{grid-template-columns:1fr}.desktop-inner{grid-template-columns:1fr}.desktop-rail{display:none}.detail-footer{align-items:stretch;flex-direction:column}.detail-footer-actions{justify-content:flex-end}.flowline{grid-template-columns:repeat(2,1fr)}}
|
|
19
|
+
/* Explicit dark-preset surfaces for Chromium's dynamic data-theme repaint. */
|
|
20
|
+
html[data-theme="dark"] .preset-option{background-color:#233540;color:#eaf3f4;border-color:#405363}
|
|
21
|
+
html[data-theme="dark"] .preset-option.active{background-color:#1b4639;border-color:#70c49c;color:#eaf3f4}
|
|
22
|
+
html[data-theme="dark"] .preset-option:hover{background-color:#29423e}
|
|
23
|
+
html[data-theme="dark"] .preset-option.active:hover{background-color:#215641}
|
|
24
|
+
|
|
25
|
+
/* Runtime-only controls added to the approved visual language. */
|
|
26
|
+
.view{display:none}.view.active{display:block}.scene-choice{display:grid;gap:3px;width:100%;text-align:left;border:1px solid var(--line);border-radius:11px;padding:12px;background:var(--paper);margin:6px 0}.scene-choice:hover,.scene-choice.selected{border-color:var(--green);background:var(--green-soft)}.scene-choice small{color:var(--muted)}.scene-actions{display:flex;gap:8px;margin:14px 0 21px}.permission-group{border:1px solid var(--line);border-radius:12px;padding:11px 14px;margin:10px 0;background:var(--paper)}.permission-group summary{font-weight:700;cursor:pointer}.permission-row{display:grid;grid-template-columns:minmax(130px,1fr) minmax(145px,180px) minmax(100px,1fr);gap:10px;align-items:center;border-top:1px solid var(--line);padding:8px 0}.permission-row:first-of-type{margin-top:8px}.permission-row small{color:var(--muted);text-align:right}.notice-bad{border-color:#d88;color:#a33}.scene-panel .scene-list{display:block}.scene-detail .preset-grid{margin:12px 0}.scene-detail .preset-option{min-height:76px;text-align:left}.scene-detail .preset-option strong,.scene-detail .preset-option small{display:block}.scene-detail .preset-option small{font-size:10px;color:var(--muted)}.head-status{align-self:center}.form-card .form-label{margin-top:11px}.form-card .input{width:100%}.footer{margin-top:30px}
|
|
27
|
+
@media(max-width:650px){.permission-row{grid-template-columns:1fr 1fr}.permission-row small{grid-column:1 / -1;text-align:left}.scene-actions{flex-wrap:wrap}}
|
|
28
|
+
.global-deny-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.global-deny-option{display:flex;gap:7px;align-items:center;padding:8px;border:1px solid var(--line);border-radius:9px;font-size:11px;background:var(--soft)}.global-deny-option input{accent-color:var(--green)}@media(max-width:650px){.global-deny-grid{grid-template-columns:1fr}}
|