@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
package/lib/client.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@fg-lab/amadeus-qq',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
// Pure client-side helpers, inlined into the browser bundle by build-client.
|
|
7
|
+
// No ESM imports here: the bundle is a lazy-CJS factory with only React injected.
|
|
8
|
+
|
|
9
|
+
const API_ROOT = '/api/amadeus-qq'
|
|
10
|
+
const MODE_IDS = Object.freeze([
|
|
11
|
+
'observe', 'safe-chat', 'enhanced', 'autonomous-social', 'owner-agent',
|
|
12
|
+
])
|
|
13
|
+
const OVERRIDE_VALUES = new Set(['inherit', 'allow', 'deny'])
|
|
14
|
+
|
|
15
|
+
function revisionOf(entry) {
|
|
16
|
+
return entry?.revision ?? 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function draftFromEntry(entry) {
|
|
20
|
+
const overrides = {}
|
|
21
|
+
for (const capability of entry?.capabilities ?? []) {
|
|
22
|
+
if (capability.override === 'allow' || capability.override === 'deny') {
|
|
23
|
+
overrides[capability.id] = capability.override
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
sceneKey: entry?.key ?? '',
|
|
28
|
+
mode: entry?.permissions?.mode ?? entry?.effectiveMode ?? 'safe-chat',
|
|
29
|
+
overrides,
|
|
30
|
+
expectedRevision: revisionOf(entry),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isDraftDirty(entry, draft) {
|
|
35
|
+
if (!entry || !draft) return false
|
|
36
|
+
const base = draftFromEntry(entry)
|
|
37
|
+
if (base.mode !== draft.mode) return true
|
|
38
|
+
const keys = new Set([...Object.keys(base.overrides), ...Object.keys(draft.overrides ?? {})])
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if ((base.overrides[key] ?? 'inherit') !== (draft.overrides?.[key] ?? 'inherit')) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide which draft the editor should show after a Scene row is selected.
|
|
47
|
+
* A dirty draft is never replaced: the operator must save or cancel first, so
|
|
48
|
+
* an unsaved permission change can never be lost by an accidental click.
|
|
49
|
+
*/
|
|
50
|
+
function nextDraftForSelection(currentEntry, currentDraft, targetEntry) {
|
|
51
|
+
if (currentDraft && isDraftDirty(currentEntry, currentDraft)) {
|
|
52
|
+
return { blocked: true, draft: currentDraft }
|
|
53
|
+
}
|
|
54
|
+
if (!targetEntry) return { blocked: false, draft: null }
|
|
55
|
+
const fresh = { ...draftFromEntry(targetEntry), sceneKey: targetEntry.key }
|
|
56
|
+
return { blocked: false, draft: fresh }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateDraft(entry, draft) {
|
|
60
|
+
if (!entry || !draft) return '未选择场景'
|
|
61
|
+
if (!MODE_IDS.includes(draft.mode)) return '模式无效'
|
|
62
|
+
if (draft.mode === 'owner-agent' && entry.ownerEligible !== true) return 'Owner Agent 仅可用于已配置的可信 Owner 私聊'
|
|
63
|
+
const known = new Set((entry.capabilities ?? []).map(capability => capability.id))
|
|
64
|
+
for (const [id, value] of Object.entries(draft.overrides ?? {})) {
|
|
65
|
+
if (!known.has(id)) return '包含未知能力:' + id
|
|
66
|
+
if (!OVERRIDE_VALUES.has(value)) return '覆盖值无效:' + id
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CAPABILITY_REASON_HINTS = Object.freeze({
|
|
72
|
+
'owner-directory-required': '需在可信 Owner 私聊使用 Owner Agent 模式,并对该能力显式 allow。',
|
|
73
|
+
'global-deny': '宿主全局策略拒绝该能力,Scene 设置无法解除。',
|
|
74
|
+
'account-publish-required': '该 QQ 账号未获授权使用 QQ 空间发布。',
|
|
75
|
+
'scene-deny': '该 Scene 显式拒绝(deny)此能力。',
|
|
76
|
+
'owner-private-required': '仅可信 Owner 私聊的 Owner Agent 模式可用。',
|
|
77
|
+
'owner-tool-required': '宿主未为该 Owner 场景配置此工具。',
|
|
78
|
+
'group-only': '仅群聊 Scene 可用;私聊不适用,与 Owner 权限无关。',
|
|
79
|
+
'qzone-read-explicit': '需在自主社交或可信 Owner Agent 模式显式 allow 才能读取 QZone。',
|
|
80
|
+
'qzone-use-explicit': '需在自主社交或可信 Owner Agent 模式显式 allow 才能使用 QZone。',
|
|
81
|
+
'profile-write-explicit': '账号资料写入仅限可信 Owner 私聊的 Owner Agent 模式并显式 allow。',
|
|
82
|
+
'owner-workspace-required': 'Owner 工作区需可信 Owner 私聊、Owner Agent 模式并显式 allow。',
|
|
83
|
+
mode: '当前模式未包含此能力。',
|
|
84
|
+
'scene-allow': '该 Scene 显式允许,但保存后按最新模式重算。',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fixed Chinese guidance for an implemented-but-denied capability. Uses only
|
|
89
|
+
* known denial reasons; an unknown reason falls back to a generic sentence and
|
|
90
|
+
* never echoes the server value. `Object.hasOwn` keeps names such as
|
|
91
|
+
* `constructor` or `toString` from resolving to inherited Object members.
|
|
92
|
+
*/
|
|
93
|
+
function capabilityStateHint(capability) {
|
|
94
|
+
if (!capability || capability.implemented !== true || capability.effective === true) return null
|
|
95
|
+
const reason = capability.reason
|
|
96
|
+
return typeof reason === 'string' && Object.hasOwn(CAPABILITY_REASON_HINTS, reason)
|
|
97
|
+
? CAPABILITY_REASON_HINTS[reason]
|
|
98
|
+
: '当前策略拒绝该能力。'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Configuration hints; never a path, token or credential. */
|
|
102
|
+
function capabilityConfigHint(capability, runtime = {}) {
|
|
103
|
+
if (!capability || capability.implemented !== true) return null
|
|
104
|
+
if (capability.id === 'qq.owner.workspace' && runtime.ownerWorkspace?.configured !== true) {
|
|
105
|
+
return '需要核心用实际 QQ 配置注册专用目录并授权后才可用。'
|
|
106
|
+
}
|
|
107
|
+
if ((capability.id === 'qq.qzone.use' || capability.id === 'qq.qzone.read' || capability.id === 'qq.qzone.publish')
|
|
108
|
+
&& runtime.providers?.qzone?.installed !== true) {
|
|
109
|
+
return '需要配置 QZone loopback Provider 后才可用;当前未观测到接线。'
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildSaveRequest(entry, draft) {
|
|
115
|
+
const overrides = {}
|
|
116
|
+
for (const [id, value] of Object.entries(draft.overrides ?? {})) {
|
|
117
|
+
if (value === 'allow' || value === 'deny') overrides[id] = value
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
scene: {
|
|
121
|
+
botId: entry.scene.botId,
|
|
122
|
+
kind: entry.scene.kind,
|
|
123
|
+
peerId: entry.scene.peerId,
|
|
124
|
+
},
|
|
125
|
+
access: { mode: draft.mode, overrides },
|
|
126
|
+
expectedRevision: draft.expectedRevision,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function readPayload(response) {
|
|
131
|
+
let payload
|
|
132
|
+
try {
|
|
133
|
+
payload = await response.json()
|
|
134
|
+
} catch {
|
|
135
|
+
const error = new Error('管理接口返回了无法解析的响应(HTTP ' + response.status + ')')
|
|
136
|
+
error.status = response.status
|
|
137
|
+
throw error
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok || !payload || payload.ok !== true) {
|
|
140
|
+
const error = new Error(payload && payload.error ? payload.error : 'HTTP ' + response.status)
|
|
141
|
+
error.status = response.status
|
|
142
|
+
if (typeof payload?.code === 'string') error.code = payload.code
|
|
143
|
+
if (response.status === 409) error.conflict = true
|
|
144
|
+
throw error
|
|
145
|
+
}
|
|
146
|
+
return payload.data
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* True only when the management surface is genuinely unusable for this page:
|
|
151
|
+
* authentication/owner admission failed, or the Host lacks the management
|
|
152
|
+
* service. Only the explicitly listed policy rejections and session-cancel
|
|
153
|
+
* failures stay editable; any unknown code falls back to its HTTP status so an
|
|
154
|
+
* unrecognized 401/403/503 keeps the editor locked (fail closed).
|
|
155
|
+
*/
|
|
156
|
+
const EDITABLE_FAILURE_CODES = new Set([
|
|
157
|
+
'QQ_OWNER_AGENT_FORBIDDEN',
|
|
158
|
+
'QQ_SCENE_UNAUTHORIZED',
|
|
159
|
+
'QQ_SESSION_CANCEL_FAILED',
|
|
160
|
+
'QQ_SESSION_CANCEL_UNAVAILABLE',
|
|
161
|
+
'QQ_PERMISSION_CONFLICT',
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
function requiresReadOnly(error) {
|
|
165
|
+
const code = typeof error?.code === 'string' ? error.code : ''
|
|
166
|
+
if (code === 'QQ_AUTH_REQUIRED' || code === 'QQ_MANAGEMENT_UNAVAILABLE') return true
|
|
167
|
+
if (EDITABLE_FAILURE_CODES.has(code)) return false
|
|
168
|
+
const status = Number(error?.status)
|
|
169
|
+
return status === 401 || status === 403 || status === 503
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Fixed Chinese guidance per management failure code; never echoes server internals. */
|
|
173
|
+
function managementErrorMessage(error) {
|
|
174
|
+
const code = typeof error?.code === 'string' ? error.code : ''
|
|
175
|
+
if (code === 'QQ_PERMISSION_CONFLICT') {
|
|
176
|
+
return '权限已被其他窗口修改(409);请重新加载后再编辑。'
|
|
177
|
+
}
|
|
178
|
+
if (code === 'QQ_AUTH_REQUIRED') {
|
|
179
|
+
return '当前请求没有通过 DSH 认证或 Owner 授权;请从已认证的 DSH 页面访问。'
|
|
180
|
+
}
|
|
181
|
+
if (code === 'QQ_MANAGEMENT_UNAVAILABLE') {
|
|
182
|
+
return '管理接口不可用;QQ 插件可能未启用或缺少 Host 服务。'
|
|
183
|
+
}
|
|
184
|
+
if (code === 'QQ_OWNER_AGENT_FORBIDDEN') {
|
|
185
|
+
return 'Owner Agent 仅可用于已配置的可信 Owner 私聊;当前场景或配置不允许保存该模式。'
|
|
186
|
+
}
|
|
187
|
+
if (code === 'QQ_SCENE_UNAUTHORIZED') {
|
|
188
|
+
return '该场景已不在 QQ 允许列表内;请更新配置并刷新后再操作。'
|
|
189
|
+
}
|
|
190
|
+
if (code === 'QQ_SESSION_CANCEL_FAILED' || code === 'QQ_SESSION_CANCEL_UNAVAILABLE') {
|
|
191
|
+
return '权限可能已写入,但旧 QQ Session 未能安全取消;请刷新确认最新 revision 后再编辑。'
|
|
192
|
+
}
|
|
193
|
+
if (code === 'QQ_DELIVERY_IN_FLIGHT') {
|
|
194
|
+
return '该投递仍在进行中,现在不能标记为失败;请稍后刷新重试。'
|
|
195
|
+
}
|
|
196
|
+
if (code === 'QQ_DELIVERY_NOT_UNKNOWN') {
|
|
197
|
+
return '该投递已不是 unknown 状态;请刷新确认后再操作。'
|
|
198
|
+
}
|
|
199
|
+
if (code === 'QQ_DELIVERY_NOT_FOUND') {
|
|
200
|
+
return '未找到该 actionId 的未知投递;请刷新后确认。'
|
|
201
|
+
}
|
|
202
|
+
if (code === 'QQ_REQUEST_INVALID') {
|
|
203
|
+
return '管理请求无效;请刷新后重试。'
|
|
204
|
+
}
|
|
205
|
+
const status = Number(error?.status)
|
|
206
|
+
if (status === 401 || status === 403) {
|
|
207
|
+
return '当前请求没有通过 DSH 认证或 Owner 授权(HTTP ' + status + ');请从已认证的 DSH 页面访问。'
|
|
208
|
+
}
|
|
209
|
+
if (status === 503) {
|
|
210
|
+
return '管理接口不可用(503);QQ 插件可能未启用或缺少 Host 服务。'
|
|
211
|
+
}
|
|
212
|
+
return 'QQ 管理请求失败' + (Number.isInteger(status) ? '(HTTP ' + status + ')' : '')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function fetchScenes(fetchImpl = fetch) {
|
|
216
|
+
const response = await fetchImpl(API_ROOT + '/scenes', {
|
|
217
|
+
credentials: 'same-origin',
|
|
218
|
+
headers: { 'x-amadeus-qq': '1' },
|
|
219
|
+
})
|
|
220
|
+
return readPayload(response)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function savePermissions(entry, draft, fetchImpl = fetch) {
|
|
224
|
+
const response = await fetchImpl(API_ROOT + '/scenes/permissions', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
credentials: 'same-origin',
|
|
227
|
+
headers: { 'content-type': 'application/json', 'x-amadeus-qq': '1' },
|
|
228
|
+
body: JSON.stringify(buildSaveRequest(entry, draft)),
|
|
229
|
+
})
|
|
230
|
+
return readPayload(response)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Storage key for one operator confirmation input. It is scoped to the Scene
|
|
235
|
+
* and prefixed, so two Scenes with the same actionId never share a draft, and
|
|
236
|
+
* prototype names such as `__proto__` or `constructor` can never be the key.
|
|
237
|
+
* The API still receives the untouched actionId.
|
|
238
|
+
*/
|
|
239
|
+
function deliveryInputKey(entry, actionId) {
|
|
240
|
+
return 'delivery-confirm:' + String(entry?.key ?? '') + '|' + String(actionId)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fixed Settings copy for the Owner workspace. It never includes a directory
|
|
245
|
+
* path, token or provider URL; an unwired core keeps the feature "pending".
|
|
246
|
+
*/
|
|
247
|
+
function ownerWorkspaceSummary(status) {
|
|
248
|
+
const configured = status?.configured === true
|
|
249
|
+
const writes = status?.writes === true
|
|
250
|
+
const maxBytes = Number.isSafeInteger(status?.maxBytes) && status.maxBytes > 0 ? status.maxBytes : 0
|
|
251
|
+
return {
|
|
252
|
+
state: configured ? '已接线' : 'pending(待核心接线)',
|
|
253
|
+
writes: writes ? '可写(逐次审批)' : '只读',
|
|
254
|
+
maxBytes,
|
|
255
|
+
note: configured
|
|
256
|
+
? '目录与写入授权由插件配置决定;本页不显示路径,也不保存配置。实机写入仍需人工审批,尚未完成真实验证。'
|
|
257
|
+
: 'Owner preset 已随包;核心尚未用实际 QQ 配置注册 Owner 工作区(未接线),当前不可读、不可写。',
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Provider labels only; never a root path, loopback URL or token value. */
|
|
262
|
+
function providerSummary(providers) {
|
|
263
|
+
const stateOf = provider => {
|
|
264
|
+
if (provider?.installed !== true) return '未观测到接线'
|
|
265
|
+
if (provider?.configured !== true) return '已安装,未配置'
|
|
266
|
+
return provider?.enabled === true ? '已配置' : '已安装,未启用'
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
mediaSend: stateOf(providers?.mediaSend),
|
|
270
|
+
qzone: stateOf(providers?.qzone),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Manual reconciliation for one unknown delivery. Only the owner management
|
|
276
|
+
* surface can do this, and the body never carries message content: just the
|
|
277
|
+
* Scene identity, the operator-confirmed actionId and the fixed outcome.
|
|
278
|
+
*/
|
|
279
|
+
async function resolveUnknownDelivery(entry, actionId, fetchImpl = fetch) {
|
|
280
|
+
const response = await fetchImpl(API_ROOT + '/deliveries/resolve', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
credentials: 'same-origin',
|
|
283
|
+
headers: { 'content-type': 'application/json', 'x-amadeus-qq': '1' },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
scene: {
|
|
286
|
+
botId: entry.scene.botId,
|
|
287
|
+
kind: entry.scene.kind,
|
|
288
|
+
peerId: entry.scene.peerId,
|
|
289
|
+
},
|
|
290
|
+
actionId,
|
|
291
|
+
outcome: 'failed',
|
|
292
|
+
}),
|
|
293
|
+
})
|
|
294
|
+
return readPayload(response)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
const PANEL_CSS = `
|
|
299
|
+
[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}
|
|
300
|
+
[data-amadeus-qq] *{box-sizing:border-box}
|
|
301
|
+
[data-amadeus-qq] h2{font-size:21px;margin:0 0 5px}
|
|
302
|
+
[data-amadeus-qq] p{margin:0 0 14px;opacity:.75}
|
|
303
|
+
[data-amadeus-qq] .summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin:14px 0}
|
|
304
|
+
[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)}
|
|
305
|
+
[data-amadeus-qq] .metric small{display:block;opacity:.68;font-size:11px}
|
|
306
|
+
[data-amadeus-qq] .metric strong{display:block;font-size:14px;margin-top:3px;word-break:break-word}
|
|
307
|
+
[data-amadeus-qq] button{border:1px solid #178a69;background:#178a69;color:white;border-radius:9px;padding:8px 14px;font:inherit;cursor:pointer}
|
|
308
|
+
[data-amadeus-qq] button:focus-visible{outline:2px solid #178a69;outline-offset:2px}
|
|
309
|
+
[data-amadeus-qq] .error{color:#ab3535;margin:12px 0}
|
|
310
|
+
@media(max-width:560px){[data-amadeus-qq] .summary{grid-template-columns:1fr}}
|
|
311
|
+
`
|
|
312
|
+
|
|
313
|
+
function Panel() {
|
|
314
|
+
const [setup, setSetup] = React.useState(null)
|
|
315
|
+
const [runtime, setRuntime] = React.useState(null)
|
|
316
|
+
const [error, setError] = React.useState('')
|
|
317
|
+
React.useEffect(() => {
|
|
318
|
+
let active = true
|
|
319
|
+
const load = async () => {
|
|
320
|
+
try {
|
|
321
|
+
const [a, b] = await Promise.all([
|
|
322
|
+
fetch('/api/amadeus-qq/setup', { credentials: 'same-origin' }),
|
|
323
|
+
fetch('/api/amadeus-qq/scenes', { credentials: 'same-origin' }),
|
|
324
|
+
])
|
|
325
|
+
const [left, right] = await Promise.all([a.json(), b.json()])
|
|
326
|
+
if (!a.ok || !b.ok || !left.ok || !right.ok) throw new Error('请在 DSH Owner 会话中打开管理控制台')
|
|
327
|
+
if (active) { setSetup(left.data); setRuntime(right.data); setError('') }
|
|
328
|
+
} catch (failure) {
|
|
329
|
+
if (active) setError(failure instanceof Error ? failure.message : '状态读取失败')
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
void load()
|
|
333
|
+
return () => { active = false }
|
|
334
|
+
}, [])
|
|
335
|
+
const workspace = setup?.workspaces?.find(item => item.id === setup.workspaceId)
|
|
336
|
+
const rows = (setup?.groups?.length ?? 0) + (setup?.privatePeers?.length ?? 0)
|
|
337
|
+
const transportText = { connected: '已连接', connecting: '连接中', disconnected: '未连接', disabled: '已暂停' }[runtime?.health?.transport] ?? '未连接'
|
|
338
|
+
const metric = (label, value) => React.createElement('div', { className: 'metric' },
|
|
339
|
+
React.createElement('small', null, label), React.createElement('strong', null, value))
|
|
340
|
+
return React.createElement('div', { 'data-amadeus-qq': 'true' },
|
|
341
|
+
React.createElement('style', null, PANEL_CSS),
|
|
342
|
+
React.createElement('h2', null, 'Amadeus QQ'),
|
|
343
|
+
React.createElement('p', null, '首次设置和详细权限在受 DSH Owner 保护的本地 Web Console 中完成。'),
|
|
344
|
+
error ? React.createElement('div', { className: 'error', role: 'status' }, error) : null,
|
|
345
|
+
React.createElement('div', { className: 'summary' },
|
|
346
|
+
metric('插件', setup?.enabled ? '已启用' : '待配置'),
|
|
347
|
+
metric('NapCat', transportText),
|
|
348
|
+
metric('Bot / Owner', `${setup?.botId || '—'} / ${setup?.ownerId || '—'}`),
|
|
349
|
+
metric('Workspace / 会话', `${workspace?.title || '未选择'} · ${rows} 个`)),
|
|
350
|
+
React.createElement('button', { type: 'button', onClick: () => window.open('/api/amadeus-qq/console', '_blank', 'noopener,noreferrer') },
|
|
351
|
+
setup?.enabled ? '打开 Web Console' : '开始首次设置'))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
inject: ['slots'],
|
|
356
|
+
apply(ctx) {
|
|
357
|
+
let active
|
|
358
|
+
const activate = () => {
|
|
359
|
+
if (active) return active.dispose
|
|
360
|
+
let dispose = () => {}
|
|
361
|
+
try {
|
|
362
|
+
dispose = ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
363
|
+
name: 'settings.section', id: 'amadeus-qq', order: 30, label: () => 'Amadeus QQ',
|
|
364
|
+
}, Panel)) || (() => {})
|
|
365
|
+
} catch (error) { console.error('[amadeus-qq] settings.section unavailable', error) }
|
|
366
|
+
const disposal = () => {
|
|
367
|
+
if (!active || active.dispose !== disposal) return
|
|
368
|
+
active = undefined
|
|
369
|
+
try { dispose() } catch {}
|
|
370
|
+
}
|
|
371
|
+
active = { dispose: disposal }
|
|
372
|
+
return disposal
|
|
373
|
+
}
|
|
374
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => activate(), 'amadeus-qq: settings page')
|
|
375
|
+
else activate()
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@fg-lab/amadeus-qq",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@fg-lab/amadeus-qq",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Amadeus QQ continuous interaction plugin for DeepSeek Harness",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./preset-guard": "./src/preset-guard.mjs",
|
|
11
|
+
"./client": "./lib/client.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/*.js",
|
|
16
|
+
"src/*.mjs",
|
|
17
|
+
"src/console",
|
|
18
|
+
"src/client",
|
|
19
|
+
"lib",
|
|
20
|
+
"scripts",
|
|
21
|
+
"cordis.patch.yml",
|
|
22
|
+
"preset.patch.yml",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"docs/RELEASE_NOTES.md",
|
|
26
|
+
"docs/COMPATIBILITY.md",
|
|
27
|
+
"docs/V1_SCOPE.md",
|
|
28
|
+
"docs/v1.1",
|
|
29
|
+
"docs/screenshots",
|
|
30
|
+
"docs/verification/V1_REAL_NAPCAT_2026-10-27.md",
|
|
31
|
+
"docs/UPGRADE_POLICY.md",
|
|
32
|
+
"docs/DATA_MIGRATION.md",
|
|
33
|
+
"docs/RELEASE_CHECKLIST.md",
|
|
34
|
+
"docs/INSTALL_V1_2.md",
|
|
35
|
+
"scripts/release-audit.mjs",
|
|
36
|
+
"screenshots.json",
|
|
37
|
+
"assets/market-icon.png",
|
|
38
|
+
"README.zh-CN.md",
|
|
39
|
+
"REFERENCE.md",
|
|
40
|
+
"docs/PUBLISHING.md"
|
|
41
|
+
],
|
|
42
|
+
"dsh": {
|
|
43
|
+
"manifestVersion": 1,
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": [
|
|
46
|
+
"./cordis.patch.yml",
|
|
47
|
+
"./preset.patch.yml"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"ws": "^8.18.3"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "node scripts/build-client.mjs",
|
|
62
|
+
"test": "node --test tests/*.test.mjs",
|
|
63
|
+
"verify:release": "node scripts/release-audit.mjs"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22.13.0",
|
|
67
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/jeskoky-pixel/amadeus-qq.git"
|
|
72
|
+
},
|
|
73
|
+
"homepage": "https://github.com/jeskoky-pixel/amadeus-qq#readme",
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/jeskoky-pixel/amadeus-qq/issues"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"keywords": [
|
|
81
|
+
"dsh-plugin",
|
|
82
|
+
"amadeus",
|
|
83
|
+
"qq"
|
|
84
|
+
],
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
90
|
+
}
|
|
91
|
+
}
|
package/preset.patch.yml
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Dedicated DSH Agent preset shipped by the same @fg-lab/amadeus-qq package.
|
|
2
|
+
# It declares only the QQ platform operation contract (tool boundary) and no
|
|
3
|
+
# persona or character card. The guard lives with the preset, so a persisted QQ
|
|
4
|
+
# Session keeps its tool boundary even when the connector is disabled or absent.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: preset-amadeus-qq
|
|
7
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
8
|
+
config:
|
|
9
|
+
id: amadeus-qq
|
|
10
|
+
name: Amadeus QQ
|
|
11
|
+
description: Restricted continuous QQ conversation Session
|
|
12
|
+
order: 20
|
|
13
|
+
plugins:
|
|
14
|
+
- id: qq-tool-boundary
|
|
15
|
+
# Mount the package root here. DSH's preset registry resolves the
|
|
16
|
+
# package root reliably; a package subpath export imports under
|
|
17
|
+
# plain Node but leaves this row "never started" in the registry.
|
|
18
|
+
name: '@fg-lab/amadeus-qq'
|
|
19
|
+
config:
|
|
20
|
+
presetGuard: true
|
|
21
|
+
- id: preset-amadeus-qq-search
|
|
22
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
23
|
+
config:
|
|
24
|
+
id: amadeus-qq-search
|
|
25
|
+
name: Amadeus QQ Search
|
|
26
|
+
description: Restricted QQ conversation with the Host web search boundary
|
|
27
|
+
order: 21
|
|
28
|
+
plugins:
|
|
29
|
+
- id: qq-web-search
|
|
30
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
31
|
+
config:
|
|
32
|
+
search: true
|
|
33
|
+
fetch: false
|
|
34
|
+
searchTimeoutMs: 60000
|
|
35
|
+
- id: qq-search-boundary
|
|
36
|
+
name: '@fg-lab/amadeus-qq'
|
|
37
|
+
config:
|
|
38
|
+
presetGuard: true
|
|
39
|
+
allowSearch: true
|
|
40
|
+
- id: preset-amadeus-qq-media
|
|
41
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
42
|
+
config:
|
|
43
|
+
id: amadeus-qq-media
|
|
44
|
+
name: Amadeus QQ Media
|
|
45
|
+
description: Restricted QQ conversation with an enhanced media read boundary
|
|
46
|
+
order: 22
|
|
47
|
+
plugins:
|
|
48
|
+
- id: qq-media-boundary
|
|
49
|
+
name: '@fg-lab/amadeus-qq'
|
|
50
|
+
config:
|
|
51
|
+
presetGuard: true
|
|
52
|
+
level: 2
|
|
53
|
+
- id: preset-amadeus-qq-media-search
|
|
54
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
55
|
+
config:
|
|
56
|
+
id: amadeus-qq-media-search
|
|
57
|
+
name: Amadeus QQ Media Search
|
|
58
|
+
description: Restricted QQ conversation with enhanced media read and Host web search
|
|
59
|
+
order: 23
|
|
60
|
+
plugins:
|
|
61
|
+
- id: qq-media-web-search
|
|
62
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
63
|
+
config:
|
|
64
|
+
search: true
|
|
65
|
+
fetch: false
|
|
66
|
+
searchTimeoutMs: 60000
|
|
67
|
+
- id: qq-media-search-boundary
|
|
68
|
+
name: '@fg-lab/amadeus-qq'
|
|
69
|
+
config:
|
|
70
|
+
presetGuard: true
|
|
71
|
+
level: 2
|
|
72
|
+
allowSearch: true
|
|
73
|
+
- id: preset-amadeus-qq-owner
|
|
74
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
75
|
+
config:
|
|
76
|
+
id: amadeus-qq-owner
|
|
77
|
+
name: Amadeus QQ Owner
|
|
78
|
+
description: Restricted Owner workspace; selected only for a trusted Owner Agent Scene
|
|
79
|
+
order: 24
|
|
80
|
+
plugins:
|
|
81
|
+
- id: qq-owner-boundary
|
|
82
|
+
# Level 4 guards the QQ tool surface. The Owner workspace tool is
|
|
83
|
+
# mounted only after an operator supplies ownerWorkspaceRoot; until
|
|
84
|
+
# then Settings shows it as pending and no file tool exists.
|
|
85
|
+
name: '@fg-lab/amadeus-qq'
|
|
86
|
+
config:
|
|
87
|
+
presetGuard: true
|
|
88
|
+
level: 4
|
|
89
|
+
- id: owner-tool-pwsh
|
|
90
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
91
|
+
- id: owner-tool-fs
|
|
92
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
93
|
+
- id: owner-tool-fs-search
|
|
94
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
95
|
+
config:
|
|
96
|
+
sampleOverCapGlobResults: false
|
|
97
|
+
- id: owner-tool-jobs
|
|
98
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
99
|
+
- id: owner-tool-web
|
|
100
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
101
|
+
config:
|
|
102
|
+
search: true
|
|
103
|
+
fetch: true
|
package/screenshots.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the browser half as a DSH lazy-CJS module.
|
|
3
|
+
* The template is plain JavaScript (no ESM import/export); this script wraps it
|
|
4
|
+
* in the official ModuleLoader format and verifies the result parses.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import vm from 'node:vm'
|
|
10
|
+
|
|
11
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
12
|
+
const templatePath = join(root, 'src', 'client', 'factory.template.js')
|
|
13
|
+
const logicPath = join(root, 'src', 'client', 'logic.js')
|
|
14
|
+
const outputPath = join(root, 'lib', 'client.js')
|
|
15
|
+
|
|
16
|
+
const template = readFileSync(templatePath, 'utf8')
|
|
17
|
+
const logic = readFileSync(logicPath, 'utf8').replace(/^export\s+/gm, '')
|
|
18
|
+
for (const source of [template, logic]) {
|
|
19
|
+
if (/^\s*(import|export)\s/m.test(source)) {
|
|
20
|
+
throw new Error('client factory must not contain ESM import/export statements')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const body = template.replace('__CLIENT_LOGIC__', logic)
|
|
25
|
+
if (body.includes('__CLIENT_LOGIC__')) throw new Error('client logic placeholder was not replaced')
|
|
26
|
+
|
|
27
|
+
const bundle = `window.__ModuleLoader__.load({
|
|
28
|
+
id: '@fg-lab/amadeus-qq',
|
|
29
|
+
factory: (require) => {
|
|
30
|
+
const React = require('react');
|
|
31
|
+
const h = React.createElement;
|
|
32
|
+
${body}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
new vm.Script(bundle, { filename: 'lib/client.js' })
|
|
38
|
+
|
|
39
|
+
for (const marker of [
|
|
40
|
+
'settings.section',
|
|
41
|
+
"id: 'amadeus-qq'",
|
|
42
|
+
"/api/amadeus-qq",
|
|
43
|
+
"credentials: 'same-origin'",
|
|
44
|
+
'打开 Web Console',
|
|
45
|
+
'开始首次设置',
|
|
46
|
+
]) {
|
|
47
|
+
if (!bundle.includes(marker)) throw new Error(`client bundle is missing required marker: ${marker}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mkdirSync(dirname(outputPath), { recursive: true })
|
|
51
|
+
writeFileSync(outputPath, bundle, 'utf8')
|
|
52
|
+
process.stdout.write(`built lib/client.js (${Buffer.byteLength(bundle)} bytes)\n`)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
const pkg=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8'));
|
|
5
|
+
assert.match(pkg.name,/^@fg-lab\/amadeus-(qq|memory|life|state|persona)$/);
|
|
6
|
+
assert.match(pkg.version,/^\d+\.\d+\.\d+$/);
|
|
7
|
+
assert.notEqual(pkg.private,true,'Private packages cannot be published');
|
|
8
|
+
assert.equal(pkg.publishConfig?.access,'public');
|
|
9
|
+
const response=await fetch('https://registry.npmjs.org/'+encodeURIComponent(pkg.name)+'/'+encodeURIComponent(pkg.version),{signal:AbortSignal.timeout(20000)});
|
|
10
|
+
if(response.status===200)throw new Error('This npm version already exists; never overwrite or reuse it.');
|
|
11
|
+
if(response.status!==404)throw new Error('Registry availability could not be verified (HTTP '+response.status+'); stop publishing.');
|
|
12
|
+
console.log('Registry preflight: target version does not exist. Scope authorization still requires npm authentication.');
|