@fg-lab/amadeus-qq 0.0.0-stage → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/README.zh-CN.md +51 -0
- package/REFERENCE.md +49 -0
- package/assets/market-icon.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/COMPATIBILITY.md +17 -0
- package/docs/DATA_MIGRATION.md +26 -0
- package/docs/INSTALL_V1_2.md +22 -0
- package/docs/PUBLISHING.md +28 -0
- package/docs/RELEASE_CHECKLIST.md +23 -0
- package/docs/RELEASE_NOTES.md +38 -0
- package/docs/UPGRADE_POLICY.md +26 -0
- package/docs/V1_SCOPE.md +33 -0
- package/docs/screenshots/account-browser.png +0 -0
- package/docs/screenshots/account-dark-browser.png +0 -0
- package/docs/screenshots/global-desktop.png +0 -0
- package/docs/screenshots/overview-desktop.png +0 -0
- package/docs/screenshots/product-release-20261031.png +0 -0
- package/docs/screenshots/v1.2-overview.png +0 -0
- package/docs/v1.1/PRESET_MATRIX.md +28 -0
- package/docs/v1.1/PROTOTYPE_DIFF.md +15 -0
- package/docs/v1.1/TEST_REPORT.md +20 -0
- package/docs/v1.1/UPGRADE.md +10 -0
- package/docs/verification/IMAGE_READ_FIX_2026-11-07.md +22 -0
- package/docs/verification/LIFE_QQ_BOUNDARY_2026-11-07.md +20 -0
- package/docs/verification/V1_REAL_NAPCAT_2026-10-27.md +43 -0
- package/lib/client.js +380 -0
- package/package.json +92 -4
- package/preset.patch.yml +166 -0
- package/screenshots.json +6 -0
- package/scripts/build-client.mjs +52 -0
- package/scripts/registry-guard.mjs +12 -0
- package/scripts/release-audit.mjs +59 -0
- package/scripts/symlink-security.mjs +8 -0
- package/src/account-profile.js +223 -0
- package/src/admin-http.js +361 -0
- package/src/audio.js +428 -0
- package/src/capabilities.js +785 -0
- package/src/client/factory.template.js +82 -0
- package/src/client/logic.js +290 -0
- package/src/config.js +50 -0
- package/src/console/console.css +28 -0
- package/src/console/console.js +541 -0
- package/src/console/index.html +67 -0
- package/src/core.js +909 -0
- package/src/files.js +230 -0
- package/src/index.js +994 -0
- package/src/lease.js +42 -0
- package/src/media.js +309 -0
- package/src/napcat-action-names.js +186 -0
- package/src/native-asr.js +112 -0
- package/src/onebot.js +378 -0
- package/src/outbound-media.js +309 -0
- package/src/owner-workspace.js +394 -0
- package/src/permissions.js +270 -0
- package/src/preset-guard.mjs +593 -0
- package/src/qzone-provider.js +628 -0
- package/src/session-provenance.js +74 -0
- package/src/setup.js +255 -0
- package/src/storage.js +580 -0
- package/src/web-console.js +9 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# v1.0 真实 NapCat/QQ 与隔离 DSH 验收(2026-10-27)
|
|
2
|
+
|
|
3
|
+
## 环境和安全边界
|
|
4
|
+
|
|
5
|
+
- 候选:`codex/m0-baseline`,原 Draft PR #1。隔离 DSH Desktop Profile 安装 QA 包;真实 NapCatQQ Desktop 使用独立 `NCD_DATA_ROOT` 和测试 Bot。生产 DSH Profile、QQ Bridge、Memory/Life 数据未作写入。
|
|
6
|
+
- 用户授权的外发对象包括一个指定测试私聊,以及一个仅含 Owner QQ 与测试 Bot 的授权群;测试期间未向其他群或个人发送。头像、签名和 QQ 空间操作仅作用于登录的测试账号。
|
|
7
|
+
- 真实测试结束后,QQ 插件已停用、隔离 Profile 配置恢复 `connect:false`;测试 NapCat Desktop、BootMain 和 QQ 进程已停止。原 SnowLuma Bot 配置在测试前后哈希一致。Life 受控外发与 Memory 停用 UI 检查使用独立隔离环境,不改变生产配置。
|
|
8
|
+
- NapCat 数据根独立,但 QQ 客户端媒体缓存仍共用系统 `Documents/Tencent Files`;不把此环境描述为客户端级完全隔离。私聊正文、消息 ID、空间 tid、Token、Cookie 和头像原件只保留在忽略的本地 QA 目录,不进入 Git 或安装包。
|
|
9
|
+
|
|
10
|
+
## 实际结果
|
|
11
|
+
|
|
12
|
+
| 场景 | 证据级别与结果 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| 连续交互与 Owner | 隔离官方 DSH 模型实际调用 QQ 工具及原生 `pwsh`、`read`,完成本机只读任务;可信 Owner 私聊可见,普通 Scene 的可见性和执行期授权有回归。原 M1 多阶段发送、等待续接、撤权及硬重启见既有隔离 Desktop 报告。 |
|
|
15
|
+
| 真实入站 | NapCat `get_login_info` 成功;收到指定私聊的真实文字、图片和 `record`;`get_msg`/`get_record` 结构与插件读取路径核对。 |
|
|
16
|
+
| QQ 原生转写 | `fetch_ptt_text` 对真实语音首次暂时失败,随后返回非空文本。隔离 DSH 模型经 `qq_transcribe_message_record_native` 获得转写及正确原消息、发送者和私聊 Scene;硬重启后关联仍在。失败路径不编造转写。 |
|
|
17
|
+
| 真实外发 | 隔离 DSH 模型向授权私聊发送文字、图片、MP3 语音,插件返回 `delivered` 与消息 ID;NapCat `get_msg` 读回对应 `text`、`image`、`record` 段。 |
|
|
18
|
+
| 群聊发送 | 在仅含 Owner 与测试 Bot 的授权群,真实隔离模型调用 @+face、回复、回应、戳一戳和群附件发送工具,均返回 delivered;NapCat `get_msg` 复核到 @、face、reply 段,群文件根目录确认一个上传文件。 |
|
|
19
|
+
| 群附件回执 | NapCat 群文件列表会轮换 `file_id`,最终代码改用稳定元数据/计数并在歧义时保留 unknown。最终包与隔离 Desktop 重启后,新群附件返回 delivered,平台恰有一个匹配文件,回执经 `get_group_file_url` 解析;列表 ID 与上传时不同。旧的 unknown 上传没有被重试或改写。 |
|
|
20
|
+
| 账号资料 | 模型工具修改测试号签名和头像成功;签名读回,头像经平台外部接口读回测试图。两者已恢复原内容。头像经 QQ 平台重编码,恢复后视觉一致但字节哈希不同。 |
|
|
21
|
+
| QQ 空间 | `qq_qzone` 用 `audience:self` 发布文字和图片说说,均返回 tid;两个测试 tid 随后通过 NapCat 删除。其他可见范围及互动动作未作真实平台验收。 |
|
|
22
|
+
| 重启持久性 | 隔离 DSH 硬重启后复用相同私聊和空间 `actionId`,返回 `repeated:true`;私聊历史中测试文字只有一条。 |
|
|
23
|
+
| 管理 Web Console | 最终包在隔离 Desktop 的真实浏览器中加载,显示授权群/私聊、Bot 与已观察 QQ ID 的场景图;节点点击、筛选和重置均可用,权限编辑器保留,owner admission 与 CSP 不变。停用 Memory Bundle 后同一 UI 仍加载并显示授权 Scene 拓扑,确认运行时不依赖 Memory;Memory 自身实体 UI 是独立 Mneme 视图,本轮隔离库为空,未绘制新的语义图数据。窄屏由横向滚动及更窄时的列表回退处理,未做移动端视觉回归。 |
|
|
24
|
+
|
|
25
|
+
## 收尾质量修复与同 Host 联动
|
|
26
|
+
|
|
27
|
+
- QQ Connector 的全局执行 guard 曾将 Owner Preset 已开放的动态注册工具再次拒绝。新增失败回归后修复:只有当前绑定、仍处于 Owner Agent 模式的可信管理员私聊,才能调用 Host 中已注册的非 QQ 工具;`run_code`、未知工具、普通私聊和群聊仍拒绝。隔离官方 DSH 中,全新合成 Owner Session 实际调用 `life_intent_create` 成功;旧 Owner Scene 经管理 API 降为 Safe Chat 后,旧 Session 再调用 `web_fetch` 得到执行期拒绝。Owner 的 `web_fetch` 在撤权前也完成一次真实 Host 只读调用。该隔离 Host 未安装 GitHub 工具,因此不宣称 GitHub 实调。
|
|
28
|
+
- QQ 与 Memory 在同一隔离官方 DSH Host 中,使用合成标记和真实绑定的群/私聊 Session 验收 runtime context。精确查询命中了私聊标记,未观察到其他 Scene 的注入;初始宽泛私聊查询未召回该标记,因此不宣称通用 Memory 检索完备。原始 Session ID、标记及记忆内容只留在忽略的私有 QA 目录。
|
|
29
|
+
- Life 1.0.1 候选代码在同一隔离 Host 使用独立数据库与唯一 Owner Session 允许名单。模型工具创建的 intent 持久化了 `life-tool:session:<当前 Owner Session>` 授权来源;管理员对同一 allowlist Owner Session 与授权 QQ 私聊开启 `externalMessageSessionIds` 后,Life wake 执行,模型在该 Scene 恰好一次调用 `qq_send_message`,QQ 工具返回 delivered,NapCat `get_msg` 与指定 Owner 好友历史确认了 Bot 的私有文字。首次尝试曾暴露错误的 provenance 检查且没有发送任何 QQ 消息;Life 源码随后改为读取持久 intent,而不是 wake/binding 授权。关闭 opt-in 后,另一条全新工具授权 intent 的 wake 使用显式非发送提示,模型零次 QQ 发送。该 opt-in 只改变提示指导,不是 Life 级外部工具硬闸门;QQ Scene 的执行权限仍是权威。最终 1.0.1 版本包已在同一隔离 Desktop 安装并硬重启,健康状态正常;完整外发行为测试使用相同运行时代码的独立 QA 包,未重复外发。
|
|
30
|
+
- 入站 `get_image` 现在限制平台响应体大小,并仅接受受控本地路径或 canonical、有界的内联 base64 图片;`get_file` 超限路径也有回归。内联图片分支为单元/模拟证据,未声称真实 NapCat 返回过该形态。
|
|
31
|
+
- QA 收尾后 QQ 和 Life Bundle 均停用,隔离 Profile 恢复 `connect:false` 且无 Life 配置行,QQ 合成状态恢复测试前备份;测试 NapCat 仍停止。受控外发与 Memory 停用 UI 检查另在隔离环境完成,未改生产;生产 DSH、SnowLuma、QQ Bridge、Memory/Life 数据未修改。
|
|
32
|
+
|
|
33
|
+
## 代码门禁与安装产物
|
|
34
|
+
|
|
35
|
+
- `npm test`:264 通过、0 失败、4 跳过。四个跳过项均因本机 Windows 无法创建符号链接,不是外部服务缺失。
|
|
36
|
+
- `npm run build`、打包与 `git diff --check` 通过。
|
|
37
|
+
- `npm pack` 的脱敏产物不含私有 QA 状态和真实凭据;包哈希与安装记录以交付消息为准。同代码包已在隔离 DSH Desktop 安装并经硬重启加载;最终包安装后 QQ 连接保持关闭。
|
|
38
|
+
|
|
39
|
+
## 尚未验证及部署建议
|
|
40
|
+
|
|
41
|
+
- Memory 的 Scene 隔离在本轮只验证了合成标记下的精确命中/无跨 Scene 注入,宽泛查询未召回,不能宣称检索完备。State 没有稳定运行源码,仅验证可选降级。Life 受控外发已有一次完整证据,但 opt-in 只改变提示指导、不是 Life 级硬闸门;长期自治与任意 QQ 目标仍未验证。
|
|
42
|
+
- GitHub 工具、QQ 空间互动、其他可见范围、移动端视觉回归和长时间真实平台稳定性未逐项实测;对应功能保留源码及单元/模拟证据。群发送、@/face/reply、回应、戳一戳和群附件回执已有授权群实机证据。
|
|
43
|
+
- 生产准备应先备份独立 QQ 状态文件,逐项确认 bot/owner/Scene 白名单、NapCat Token、媒体根目录及权限上限,再对授权私聊/小群灰度。群附件注意 NapCat 会轮换 `file_id`,以稳定元数据和文件计数核对;unknown 投递只人工对账,不自动盲重试。回滚时停用 QQ Bundle、恢复上一安装包和隔离备份状态;不改动生产 QQ/Memory/Life 数据。Life 受控外发只能经管理员 opt-in、授权 QQ Scene 和明确目标使用,QQ 执行权限仍是权威;opt-in 不是硬性禁止层。
|
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,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fg-lab/amadeus-qq",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.2.2",
|
|
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/verification/IMAGE_READ_FIX_2026-11-07.md",
|
|
32
|
+
"docs/verification/LIFE_QQ_BOUNDARY_2026-11-07.md",
|
|
33
|
+
"docs/UPGRADE_POLICY.md",
|
|
34
|
+
"docs/DATA_MIGRATION.md",
|
|
35
|
+
"docs/RELEASE_CHECKLIST.md",
|
|
36
|
+
"docs/INSTALL_V1_2.md",
|
|
37
|
+
"scripts/release-audit.mjs",
|
|
38
|
+
"screenshots.json",
|
|
39
|
+
"assets/market-icon.png",
|
|
40
|
+
"README.zh-CN.md",
|
|
41
|
+
"REFERENCE.md",
|
|
42
|
+
"docs/PUBLISHING.md"
|
|
43
|
+
],
|
|
44
|
+
"dsh": {
|
|
45
|
+
"manifestVersion": 1,
|
|
46
|
+
"bundle": {
|
|
47
|
+
"patch": [
|
|
48
|
+
"./cordis.patch.yml",
|
|
49
|
+
"./preset.patch.yml"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"platform": "web",
|
|
54
|
+
"inject": [
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"ws": "^8.18.3"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "node scripts/build-client.mjs",
|
|
64
|
+
"test": "node --test tests/*.test.mjs",
|
|
65
|
+
"verify:release": "node scripts/release-audit.mjs"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22.13.0",
|
|
69
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0"
|
|
70
|
+
},
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "git+https://github.com/jeskoky-pixel/amadeus-qq.git"
|
|
74
|
+
},
|
|
75
|
+
"homepage": "https://github.com/jeskoky-pixel/amadeus-qq#readme",
|
|
76
|
+
"bugs": {
|
|
77
|
+
"url": "https://github.com/jeskoky-pixel/amadeus-qq/issues"
|
|
78
|
+
},
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public"
|
|
81
|
+
},
|
|
82
|
+
"keywords": [
|
|
83
|
+
"dsh-plugin",
|
|
84
|
+
"amadeus",
|
|
85
|
+
"qq"
|
|
86
|
+
],
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
89
|
+
},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|