@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,223 +1,223 @@
1
- import { createHash } from 'node:crypto'
2
- import path from 'node:path'
3
- import { snapshotControlledImage } from './outbound-media.js'
4
-
5
- const STATUS_VALUES = new Set([10, 30, 40, 50, 60, 70])
6
- const MAX_SIGNATURE = 200
7
- const MAX_NICKNAME = 36
8
-
9
- function profileError(code) {
10
- const error = new Error(code)
11
- error.code = code
12
- return error
13
- }
14
-
15
- function cleanText(value, max, code) {
16
- if (typeof value !== 'string') throw profileError(code)
17
- const text = value.replace(/[\u0000-\u001f\u007f]/g, '').trim()
18
- if (text.length === 0 || text.length > max) throw profileError(code)
19
- return text
20
- }
21
-
22
- function decimalId(value) {
23
- const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value
24
- if (typeof id !== 'string' || !/^[1-9]\d{0,19}$/.test(id)) throw profileError('QQ_PROFILE_RESPONSE_INVALID')
25
- return id
26
- }
27
-
28
- function responseInvalid() {
29
- return profileError('QQ_PROFILE_RESPONSE_INVALID')
30
- }
31
-
32
- function profileFingerprint(scene, action, payload) {
33
- return createHash('sha256')
34
- .update(JSON.stringify({
35
- account: scene.botId,
36
- scene: scene.peerId,
37
- action,
38
- payload,
39
- }))
40
- .digest('hex')
41
- }
42
-
43
- export function createAccountProfile({ core, transport, authorize, ledger, sendRoot, tempRoot }) {
44
- if (typeof authorize !== 'function') throw profileError('QQ_PROFILE_UNAUTHORIZED')
45
- if (!transport || typeof transport.action !== 'function') throw profileError('QQ_PROFILE_UNAVAILABLE')
46
-
47
- async function check(sessionId, action, write) {
48
- let scene
49
- try {
50
- scene = core.authorizedScene(sessionId)
51
- } catch {
52
- throw profileError('QQ_PROFILE_UNAUTHORIZED')
53
- }
54
- if (write && scene.kind !== 'private') throw profileError('QQ_PROFILE_OWNER_REQUIRED')
55
- let allowed = false
56
- try {
57
- allowed = await authorize({ action, write, sessionId, scene }) === true
58
- } catch {
59
- allowed = false
60
- }
61
- if (!allowed) throw profileError('QQ_PROFILE_UNAUTHORIZED')
62
- return scene
63
- }
64
-
65
- async function call(action, params, write) {
66
- let data
67
- try {
68
- data = await transport.action(action, params, 10_000)
69
- } catch (error) {
70
- if (!write) throw profileError('QQ_PROFILE_UNAVAILABLE')
71
- if (error?.code === 'ONEBOT_REJECTED') throw profileError('QQ_PROFILE_REJECTED')
72
- throw profileError('QQ_PROFILE_UNKNOWN')
73
- }
74
- if (data === null || data === undefined) {
75
- if (write) return {}
76
- throw responseInvalid()
77
- }
78
- if (typeof data !== 'object') throw responseInvalid()
79
- return data
80
- }
81
-
82
- async function loginInfo(sessionId, write = false) {
83
- const data = await call('get_login_info', {}, write)
84
- return { userId: decimalId(data.user_id), nickname: typeof data.nickname === 'string' ? data.nickname.slice(0, MAX_NICKNAME) : '' }
85
- }
86
-
87
- async function strangerInfo(sessionId, userId, write = false) {
88
- const data = await call('get_stranger_info', { user_id: userId, no_cache: true }, write)
89
- return {
90
- longNick: typeof data.long_nick === 'string' ? data.long_nick.slice(0, MAX_SIGNATURE) : '',
91
- status: Number.isFinite(Number(data.status)) ? Number(data.status) : null,
92
- }
93
- }
94
-
95
- async function userStatus(userId, write = false) {
96
- const data = await call('nc_get_user_status', { user_id: userId }, write)
97
- const status = Number(data.status)
98
- const extStatus = Number(data.ext_status)
99
- if (!Number.isSafeInteger(status) || !Number.isSafeInteger(extStatus)) throw responseInvalid()
100
- return { status, extStatus }
101
- }
102
-
103
- async function readProfileInternal(sessionId, write = false) {
104
- const login = await loginInfo(sessionId, write)
105
- const stranger = await strangerInfo(sessionId, login.userId, write)
106
- const online = await userStatus(login.userId, write)
107
- return {
108
- userId: login.userId,
109
- nickname: login.nickname,
110
- signature: stranger.longNick,
111
- status: online.status,
112
- extStatus: online.extStatus,
113
- }
114
- }
115
-
116
- async function writeWithLedger(sessionId, action, payload, actionId, verify) {
117
- const scene = await check(sessionId, action, true)
118
- if (!ledger || typeof ledger.run !== 'function') throw profileError('QQ_PROFILE_LEDGER_REQUIRED')
119
- if (typeof actionId !== 'string' || !/^[\w-]{1,100}$/.test(actionId)) throw profileError('QQ_PROFILE_ACTION_ID_INVALID')
120
- const fingerprint = profileFingerprint(scene, action, payload)
121
- return ledger.run({
122
- actionId,
123
- fingerprint,
124
- action,
125
- sessionId,
126
- sceneKey: `${scene.botId}:${scene.kind}:${scene.peerId}`,
127
- execute: async () => {
128
- await call(action, payload, true)
129
- try {
130
- await check(sessionId, action, true)
131
- } catch {
132
- // The platform write already happened; do not report a clean failure.
133
- throw profileError('QQ_PROFILE_UNKNOWN')
134
- }
135
- let profile
136
- try {
137
- profile = await readProfileInternal(sessionId, true)
138
- if (!(await verify(profile))) throw profileError('QQ_PROFILE_UNKNOWN')
139
- } catch (error) {
140
- if (error?.code === 'QQ_PROFILE_UNKNOWN') throw error
141
- throw profileError('QQ_PROFILE_UNKNOWN')
142
- }
143
- try {
144
- await check(sessionId, action, true)
145
- } catch {
146
- throw profileError('QQ_PROFILE_UNKNOWN')
147
- }
148
- return profile
149
- },
150
- })
151
- }
152
-
153
- return Object.freeze({
154
- async readProfile({ sessionId }) {
155
- await check(sessionId, 'get_login_info', false)
156
- const profile = await readProfileInternal(sessionId)
157
- await check(sessionId, 'get_login_info', false)
158
- return profile
159
- },
160
-
161
- async setSignature({ sessionId, signature, actionId }) {
162
- const value = cleanText(signature, MAX_SIGNATURE, 'QQ_PROFILE_SIGNATURE_INVALID')
163
- return writeWithLedger(sessionId, 'set_self_longnick', { longNick: value }, actionId,
164
- profile => profile.signature === value)
165
- },
166
-
167
- async setNickname({ sessionId, nickname, actionId }) {
168
- const value = cleanText(nickname, MAX_NICKNAME, 'QQ_PROFILE_NICKNAME_INVALID')
169
- return writeWithLedger(sessionId, 'set_qq_profile', { nickname: value }, actionId,
170
- profile => profile.nickname === value)
171
- },
172
-
173
- async setOnlineStatus({ sessionId, status, extStatus, batteryStatus, actionId }) {
174
- if (!Number.isSafeInteger(status) || !STATUS_VALUES.has(status)) throw profileError('QQ_PROFILE_STATUS_INVALID')
175
- if (!Number.isSafeInteger(extStatus) || extStatus < 0 || extStatus > 9999) throw profileError('QQ_PROFILE_STATUS_INVALID')
176
- if (!Number.isSafeInteger(batteryStatus) || batteryStatus < 0 || batteryStatus > 100) throw profileError('QQ_PROFILE_STATUS_INVALID')
177
- return writeWithLedger(sessionId, 'set_online_status', {
178
- status, ext_status: extStatus, battery_status: batteryStatus,
179
- }, actionId, profile => profile.status === status && profile.extStatus === extStatus)
180
- },
181
-
182
- async setAvatar({ sessionId, path: relativePath, actionId }) {
183
- if (typeof sendRoot !== 'string' || !path.isAbsolute(sendRoot)) {
184
- throw profileError('QQ_PROFILE_AVATAR_ROOT_INVALID')
185
- }
186
- const scene = await check(sessionId, 'set_qq_avatar', true)
187
- if (!ledger || typeof ledger.run !== 'function') throw profileError('QQ_PROFILE_LEDGER_REQUIRED')
188
- if (typeof actionId !== 'string' || !/^[\w-]{1,100}$/.test(actionId)) throw profileError('QQ_PROFILE_ACTION_ID_INVALID')
189
-
190
- let snapshot
191
- try {
192
- snapshot = await snapshotControlledImage({ sendRoot, relativePath, tempRoot })
193
- } catch {
194
- throw profileError('QQ_PROFILE_AVATAR_INVALID')
195
- }
196
- const fingerprint = profileFingerprint(scene, 'set_qq_avatar', {
197
- sha256: snapshot.sha256,
198
- mediaType: snapshot.mediaType,
199
- })
200
- try {
201
- return await ledger.run({
202
- actionId,
203
- fingerprint,
204
- action: 'set_qq_avatar',
205
- sessionId,
206
- sceneKey: `${scene.botId}:${scene.kind}:${scene.peerId}`,
207
- execute: async () => {
208
- await call('set_qq_avatar', { file: snapshot.path }, true)
209
- try {
210
- await check(sessionId, 'set_qq_avatar', true)
211
- } catch {
212
- // The platform accepted the write; a later revocation is unknown.
213
- throw profileError('QQ_PROFILE_UNKNOWN')
214
- }
215
- return { status: 'confirmed', mediaType: snapshot.mediaType, bytes: snapshot.bytes }
216
- },
217
- })
218
- } finally {
219
- await snapshot.cleanup()
220
- }
221
- },
222
- })
223
- }
1
+ import { createHash } from 'node:crypto'
2
+ import path from 'node:path'
3
+ import { snapshotControlledImage } from './outbound-media.js'
4
+
5
+ const STATUS_VALUES = new Set([10, 30, 40, 50, 60, 70])
6
+ const MAX_SIGNATURE = 200
7
+ const MAX_NICKNAME = 36
8
+
9
+ function profileError(code) {
10
+ const error = new Error(code)
11
+ error.code = code
12
+ return error
13
+ }
14
+
15
+ function cleanText(value, max, code) {
16
+ if (typeof value !== 'string') throw profileError(code)
17
+ const text = value.replace(/[\u0000-\u001f\u007f]/g, '').trim()
18
+ if (text.length === 0 || text.length > max) throw profileError(code)
19
+ return text
20
+ }
21
+
22
+ function decimalId(value) {
23
+ const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value
24
+ if (typeof id !== 'string' || !/^[1-9]\d{0,19}$/.test(id)) throw profileError('QQ_PROFILE_RESPONSE_INVALID')
25
+ return id
26
+ }
27
+
28
+ function responseInvalid() {
29
+ return profileError('QQ_PROFILE_RESPONSE_INVALID')
30
+ }
31
+
32
+ function profileFingerprint(scene, action, payload) {
33
+ return createHash('sha256')
34
+ .update(JSON.stringify({
35
+ account: scene.botId,
36
+ scene: scene.peerId,
37
+ action,
38
+ payload,
39
+ }))
40
+ .digest('hex')
41
+ }
42
+
43
+ export function createAccountProfile({ core, transport, authorize, ledger, sendRoot, tempRoot }) {
44
+ if (typeof authorize !== 'function') throw profileError('QQ_PROFILE_UNAUTHORIZED')
45
+ if (!transport || typeof transport.action !== 'function') throw profileError('QQ_PROFILE_UNAVAILABLE')
46
+
47
+ async function check(sessionId, action, write) {
48
+ let scene
49
+ try {
50
+ scene = core.authorizedScene(sessionId)
51
+ } catch {
52
+ throw profileError('QQ_PROFILE_UNAUTHORIZED')
53
+ }
54
+ if (write && scene.kind !== 'private') throw profileError('QQ_PROFILE_OWNER_REQUIRED')
55
+ let allowed = false
56
+ try {
57
+ allowed = await authorize({ action, write, sessionId, scene }) === true
58
+ } catch {
59
+ allowed = false
60
+ }
61
+ if (!allowed) throw profileError('QQ_PROFILE_UNAUTHORIZED')
62
+ return scene
63
+ }
64
+
65
+ async function call(action, params, write) {
66
+ let data
67
+ try {
68
+ data = await transport.action(action, params, 10_000)
69
+ } catch (error) {
70
+ if (!write) throw profileError('QQ_PROFILE_UNAVAILABLE')
71
+ if (error?.code === 'ONEBOT_REJECTED') throw profileError('QQ_PROFILE_REJECTED')
72
+ throw profileError('QQ_PROFILE_UNKNOWN')
73
+ }
74
+ if (data === null || data === undefined) {
75
+ if (write) return {}
76
+ throw responseInvalid()
77
+ }
78
+ if (typeof data !== 'object') throw responseInvalid()
79
+ return data
80
+ }
81
+
82
+ async function loginInfo(sessionId, write = false) {
83
+ const data = await call('get_login_info', {}, write)
84
+ return { userId: decimalId(data.user_id), nickname: typeof data.nickname === 'string' ? data.nickname.slice(0, MAX_NICKNAME) : '' }
85
+ }
86
+
87
+ async function strangerInfo(sessionId, userId, write = false) {
88
+ const data = await call('get_stranger_info', { user_id: userId, no_cache: true }, write)
89
+ return {
90
+ longNick: typeof data.long_nick === 'string' ? data.long_nick.slice(0, MAX_SIGNATURE) : '',
91
+ status: Number.isFinite(Number(data.status)) ? Number(data.status) : null,
92
+ }
93
+ }
94
+
95
+ async function userStatus(userId, write = false) {
96
+ const data = await call('nc_get_user_status', { user_id: userId }, write)
97
+ const status = Number(data.status)
98
+ const extStatus = Number(data.ext_status)
99
+ if (!Number.isSafeInteger(status) || !Number.isSafeInteger(extStatus)) throw responseInvalid()
100
+ return { status, extStatus }
101
+ }
102
+
103
+ async function readProfileInternal(sessionId, write = false) {
104
+ const login = await loginInfo(sessionId, write)
105
+ const stranger = await strangerInfo(sessionId, login.userId, write)
106
+ const online = await userStatus(login.userId, write)
107
+ return {
108
+ userId: login.userId,
109
+ nickname: login.nickname,
110
+ signature: stranger.longNick,
111
+ status: online.status,
112
+ extStatus: online.extStatus,
113
+ }
114
+ }
115
+
116
+ async function writeWithLedger(sessionId, action, payload, actionId, verify) {
117
+ const scene = await check(sessionId, action, true)
118
+ if (!ledger || typeof ledger.run !== 'function') throw profileError('QQ_PROFILE_LEDGER_REQUIRED')
119
+ if (typeof actionId !== 'string' || !/^[\w-]{1,100}$/.test(actionId)) throw profileError('QQ_PROFILE_ACTION_ID_INVALID')
120
+ const fingerprint = profileFingerprint(scene, action, payload)
121
+ return ledger.run({
122
+ actionId,
123
+ fingerprint,
124
+ action,
125
+ sessionId,
126
+ sceneKey: `${scene.botId}:${scene.kind}:${scene.peerId}`,
127
+ execute: async () => {
128
+ await call(action, payload, true)
129
+ try {
130
+ await check(sessionId, action, true)
131
+ } catch {
132
+ // The platform write already happened; do not report a clean failure.
133
+ throw profileError('QQ_PROFILE_UNKNOWN')
134
+ }
135
+ let profile
136
+ try {
137
+ profile = await readProfileInternal(sessionId, true)
138
+ if (!(await verify(profile))) throw profileError('QQ_PROFILE_UNKNOWN')
139
+ } catch (error) {
140
+ if (error?.code === 'QQ_PROFILE_UNKNOWN') throw error
141
+ throw profileError('QQ_PROFILE_UNKNOWN')
142
+ }
143
+ try {
144
+ await check(sessionId, action, true)
145
+ } catch {
146
+ throw profileError('QQ_PROFILE_UNKNOWN')
147
+ }
148
+ return profile
149
+ },
150
+ })
151
+ }
152
+
153
+ return Object.freeze({
154
+ async readProfile({ sessionId }) {
155
+ await check(sessionId, 'get_login_info', false)
156
+ const profile = await readProfileInternal(sessionId)
157
+ await check(sessionId, 'get_login_info', false)
158
+ return profile
159
+ },
160
+
161
+ async setSignature({ sessionId, signature, actionId }) {
162
+ const value = cleanText(signature, MAX_SIGNATURE, 'QQ_PROFILE_SIGNATURE_INVALID')
163
+ return writeWithLedger(sessionId, 'set_self_longnick', { longNick: value }, actionId,
164
+ profile => profile.signature === value)
165
+ },
166
+
167
+ async setNickname({ sessionId, nickname, actionId }) {
168
+ const value = cleanText(nickname, MAX_NICKNAME, 'QQ_PROFILE_NICKNAME_INVALID')
169
+ return writeWithLedger(sessionId, 'set_qq_profile', { nickname: value }, actionId,
170
+ profile => profile.nickname === value)
171
+ },
172
+
173
+ async setOnlineStatus({ sessionId, status, extStatus, batteryStatus, actionId }) {
174
+ if (!Number.isSafeInteger(status) || !STATUS_VALUES.has(status)) throw profileError('QQ_PROFILE_STATUS_INVALID')
175
+ if (!Number.isSafeInteger(extStatus) || extStatus < 0 || extStatus > 9999) throw profileError('QQ_PROFILE_STATUS_INVALID')
176
+ if (!Number.isSafeInteger(batteryStatus) || batteryStatus < 0 || batteryStatus > 100) throw profileError('QQ_PROFILE_STATUS_INVALID')
177
+ return writeWithLedger(sessionId, 'set_online_status', {
178
+ status, ext_status: extStatus, battery_status: batteryStatus,
179
+ }, actionId, profile => profile.status === status && profile.extStatus === extStatus)
180
+ },
181
+
182
+ async setAvatar({ sessionId, path: relativePath, actionId }) {
183
+ if (typeof sendRoot !== 'string' || !path.isAbsolute(sendRoot)) {
184
+ throw profileError('QQ_PROFILE_AVATAR_ROOT_INVALID')
185
+ }
186
+ const scene = await check(sessionId, 'set_qq_avatar', true)
187
+ if (!ledger || typeof ledger.run !== 'function') throw profileError('QQ_PROFILE_LEDGER_REQUIRED')
188
+ if (typeof actionId !== 'string' || !/^[\w-]{1,100}$/.test(actionId)) throw profileError('QQ_PROFILE_ACTION_ID_INVALID')
189
+
190
+ let snapshot
191
+ try {
192
+ snapshot = await snapshotControlledImage({ sendRoot, relativePath, tempRoot })
193
+ } catch {
194
+ throw profileError('QQ_PROFILE_AVATAR_INVALID')
195
+ }
196
+ const fingerprint = profileFingerprint(scene, 'set_qq_avatar', {
197
+ sha256: snapshot.sha256,
198
+ mediaType: snapshot.mediaType,
199
+ })
200
+ try {
201
+ return await ledger.run({
202
+ actionId,
203
+ fingerprint,
204
+ action: 'set_qq_avatar',
205
+ sessionId,
206
+ sceneKey: `${scene.botId}:${scene.kind}:${scene.peerId}`,
207
+ execute: async () => {
208
+ await call('set_qq_avatar', { file: snapshot.path }, true)
209
+ try {
210
+ await check(sessionId, 'set_qq_avatar', true)
211
+ } catch {
212
+ // The platform accepted the write; a later revocation is unknown.
213
+ throw profileError('QQ_PROFILE_UNKNOWN')
214
+ }
215
+ return { status: 'confirmed', mediaType: snapshot.mediaType, bytes: snapshot.bytes }
216
+ },
217
+ })
218
+ } finally {
219
+ await snapshot.cleanup()
220
+ }
221
+ },
222
+ })
223
+ }