@fg-lab/amadeus-qq 1.2.1 → 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.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +55 -53
  3. package/README.zh-CN.md +51 -51
  4. package/REFERENCE.md +49 -49
  5. package/cordis.patch.yml +6 -6
  6. package/docs/DATA_MIGRATION.md +26 -26
  7. package/docs/INSTALL_V1_2.md +22 -22
  8. package/docs/PUBLISHING.md +28 -26
  9. package/docs/RELEASE_CHECKLIST.md +23 -23
  10. package/docs/RELEASE_NOTES.md +38 -38
  11. package/docs/UPGRADE_POLICY.md +26 -26
  12. package/docs/verification/IMAGE_READ_FIX_2026-11-07.md +22 -0
  13. package/docs/verification/LIFE_QQ_BOUNDARY_2026-11-07.md +20 -0
  14. package/lib/client.js +372 -372
  15. package/package.json +94 -91
  16. package/preset.patch.yml +63 -0
  17. package/scripts/symlink-security.mjs +1 -1
  18. package/src/account-profile.js +223 -223
  19. package/src/admin-http.js +361 -361
  20. package/src/audio.js +428 -428
  21. package/src/capabilities.js +785 -785
  22. package/src/client/factory.template.js +82 -82
  23. package/src/client/logic.js +290 -290
  24. package/src/config.js +2 -0
  25. package/src/console/console.css +28 -28
  26. package/src/console/console.js +3 -0
  27. package/src/console/index.html +67 -67
  28. package/src/core.js +21 -4
  29. package/src/files.js +230 -230
  30. package/src/index.js +135 -8
  31. package/src/lease.js +42 -42
  32. package/src/media.js +66 -3
  33. package/src/napcat-action-names.js +186 -186
  34. package/src/native-asr.js +112 -112
  35. package/src/onebot.js +378 -378
  36. package/src/outbound-media.js +309 -309
  37. package/src/owner-workspace.js +394 -394
  38. package/src/permissions.js +20 -0
  39. package/src/preset-guard.mjs +16 -2
  40. package/src/qzone-provider.js +628 -628
  41. package/src/session-provenance.js +74 -74
  42. package/src/setup.js +255 -255
  43. package/src/web-console.js +9 -9
@@ -1,290 +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
- }
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 CHANGED
@@ -39,6 +39,7 @@ export const Config = z.object({
39
39
  groupDefaultMode: z.string().default('safe-chat'),
40
40
  privateDefaultMode: z.string().default('safe-chat'),
41
41
  ownerPrivateIds: z.array(z.string()).default([]),
42
+ lifeReadPrivateIds: z.array(z.string()).default([]),
42
43
  ownerTools: z.array(z.string()).default([]),
43
44
  globalDeny: z.array(z.string()).default([]),
44
45
  accountGrants: z.object({
@@ -46,3 +47,4 @@ export const Config = z.object({
46
47
  }).default({}),
47
48
  }).default({}),
48
49
  })
50
+