@zhin.js/adapter-onebot11 1.1.26 → 1.1.27
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/CHANGELOG.md +152 -0
- package/LICENSE +21 -0
- package/PERMITS.md +4 -4
- package/README.md +8 -4
- package/adapters/onebot11/index.js +3 -0
- package/adapters/onebot11/index.ts +3 -0
- package/agents/onebot11/conversation.js +15 -0
- package/agents/onebot11/conversation.ts +17 -0
- package/agents/onebot11/messages.js +4 -0
- package/agents/onebot11/messages.ts +4 -0
- package/agents/onebot11/skills/onebot11/tools/set_title/index.js +5 -4
- package/agents/onebot11/skills/onebot11/tools/set_title/index.ts +6 -5
- package/lib/endpoint-management.js +37 -0
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +10 -0
- package/lib/ws-endpoint.js +3 -2
- package/lib/ws-types.d.ts +6 -0
- package/lib/ws-types.js +31 -0
- package/lib/wss-auth.d.ts +2 -2
- package/lib/wss-auth.js +3 -12
- package/lib/wss-endpoint.d.ts +1 -1
- package/lib/wss-endpoint.js +15 -23
- package/package.json +24 -28
- package/plugin.js +0 -5
- package/schema.json +0 -56
- package/src/endpoint-management.ts +33 -0
- package/src/protocol.ts +11 -1
- package/src/ws-endpoint.ts +3 -1
- package/src/ws-types.ts +33 -0
- package/src/wss-auth.ts +4 -11
- package/src/wss-endpoint.ts +15 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,157 @@
|
|
|
1
1
|
# @zhin.js/adapter-onebot11
|
|
2
2
|
|
|
3
|
+
## 1.1.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c606d51: 各适配器按平台能力上报入站发送者的 `sender.platform`(ADR 0056 §1),并在能力画像 `scene.platformStanding` 中声明:OneBot11、NapCat、Milky、ICQQ 群消息上报群主 / 管理员 / 成员;QQ 官方频道上报频道主 / 超级管理员与身份组;Discord 服务器频道上报所有者 / 管理员 / 成员、身份组与 snake*case 权限位;Telegram 群消息按 `getChatMember` 上报 creator / administrator / member / 已离开,以及管理员的 `can*\*` 权限;KOOK 服务器频道上报服务器主 / 管理员角色、角色列表与权限位;飞书、钉钉群消息上报群主 / 管理员 / 成员;Sandbox 把模拟角色作为平台身份上报,不再授予框架主人角色。Satori、Slack、OneBot12、企业微信、LINE 的事件不带可靠的成员身份,不上报。README 补充「成员身份」说明。
|
|
8
|
+
|
|
9
|
+
机器人自身身份(`management.inspectSelfStanding` + 能力画像 `scene.selfStanding`,供 `bot:*` permit):ICQQ、OneBot11、NapCat、Milky 群(`get_group_member_info` self);QQ 官方群(`getBotState`)与频道(机器人频道成员身份组);Discord(`guild.members.me` 身份组与频道权限);Telegram(`getChatMember` bot);KOOK(`/v3/user/view` self 的服务器角色与权限位);飞书(`chat.get` 群主与 `bot_manager_id_list`)。钉钉没有对应接口,不声明。群管理类 Agent 工具补上 `bot:admin` / `bot:owner` / `bot-permission:*`,需要发言者是群管的平台操作改用 `platform:admin` 等。
|
|
10
|
+
|
|
11
|
+
- 53932e5: 修复平台身份 permit 与操作目标不一致的问题(ADR 0056 §5):`platform:*` / `platform-permission:*` / `bot:*` / `bot-permission:*` 按**当前会话**判定,但群管理工具此前接受模型传入的任意群 / 服务器 / 频道 ID,机器人同在 A、B 两群当管理员时,A 群管理员可让 Agent 在 B 群踢人。
|
|
12
|
+
|
|
13
|
+
- 目标改为取自当前会话、去掉目标 ID 入参(模型传入的 ID 被忽略):ICQQ(踢人、禁言、设管理员、头衔、公告、匿名;设精华校验消息属于本群)、Telegram(邀请链接、置顶 / 取消置顶、群描述、默认权限)、Discord(加 / 移除身份组、身份组列表、建子线程)、KOOK(黑名单、授予 / 撤销 / 创建 / 删除角色)、飞书(加成员、设 / 撤管理员、改群信息、解散群)、NapCat(群公告、删公告、头衔、群头像;精华消息校验属于本群)、OneBot11(头衔)、QQ 官方频道(身份组增删建与列表、子频道列表、成员详情;子频道详情只限本频道)。核心场景治理工具工厂(`createSceneManagementTools`)同样只作用于调用消息所在会话,没有会话时拒绝;Agent 侧同名工厂改用与核心一致的 `platform:<地位>` + `bot:<地位>`(此前误作 zhin 角色)。
|
|
14
|
+
- 钉钉群管理接口使用旧版 `chatid`(无法由当前会话推出),`dingtalk_add_chat_members` / `dingtalk_update_chat` 改为要求 zhin `owner`(`update_chat` 此前没有任何门禁);`dingtalk_create_chat` 改为要求 zhin `trusted`(`admin` / `owner` 亦满足),不再依赖钉钉从不上报的 `platform-permission:chat_owner`。
|
|
15
|
+
- Slack 不上报发言者平台身份,频道工具改用 zhin 角色并只作用于当前频道:话题、用途、邀请、置顶 / 取消置顶要求 `admin`,归档 / 恢复归档要求 `owner`;`PERMITS.md` 同步修正。
|
|
16
|
+
- Discord:加 / 移除身份组要求目标身份组低于发起者的最高身份组(服务器主豁免;托管身份组拒绝),低位 `manage_roles` 管理员不能借机器人给自己加更高身份组。平台权限统一为服务器级:机器人自身改读 `GuildMember.permissions`(不再用频道级 `permissionsFor`);Interactions 模式的位域含频道覆盖,只上报不能被频道覆盖的权限(管理员上报全部),并用最近一次交互的 `app_permissions` 上报机器人自身身份(同一口径,10 分钟内有效)。
|
|
17
|
+
- Telegram:`set_permissions` 改要求 `platform-permission:restrict_members`,`set_description` 改 `change_info`,`create_invite` 改 `invite_users`;发送者 `getChatMember` 并发合并为一次、失败负缓存 10 秒,收到 `chat_member` / `my_chat_member` 更新时以新状态覆盖缓存(默认 `allowedUpdates` 增加这两项);restricted 成员的个人 `can_*` 不再作为平台权限上报(与普通成员一致,只有管理员上报权限)。
|
|
18
|
+
- KOOK:服务器主与持有「管理员」位的成员(含机器人自身)展开为全部权限;并入「全体成员」(role_id 0)的基础权限。
|
|
19
|
+
- 飞书:`chat.get` 失败或未返回 `owner_id` 时发送者 / 机器人地位为未知(不再报确定的 `member` 并缓存 60 秒),失败负缓存 10 秒。
|
|
20
|
+
- QQ 官方:发送者平台身份只在频道子频道上报(群、单聊、频道私信不上报)。
|
|
21
|
+
- `AdapterIndex.selfStanding`:共享查询使用独立的 10 秒期限,调用方的 `signal` 只中止自己的等待,不再取消他人共享的查询或把结果缓存为未知;查询期间被 `invalidateSelfStanding` 失效时不写回旧结果。
|
|
22
|
+
- OneBot11 / NapCat / Milky:机器人自身身份不再永久记住首次 `get_login_info` 的账号(重连或换号后刷新;只为合并突发请求复用 5 秒,查询失败即丢弃)。
|
|
23
|
+
- 删除遗留导出(生成永不匹配的 permit 或已不再注册):`discordGroupPermitResolver` / `checkDiscordPlatformPermit` / `normalizeDiscordSenderForPermit`、`telegramGroupPermitResolver` / `checkTelegramPlatformPermit` / `normalizeTelegramChatMember`、`kookGroupPermitResolver` / `checkKookPlatformPermit` / `normalizeKookSenderForPermit` / `KookSenderInfo`、`larkGroupPermitResolver` / `checkLarkPlatformPermit` / `normalizeLarkSenderForPermit`、`slackGroupPermitResolver` / `checkSlackPlatformPermit` / `normalizeSlackSenderForPermit`、`dingtalkGroupPermitResolver` / `checkDingtalkPlatformPermit` / `normalizeDingtalkSenderForPermit`、`wecomGroupPermitResolver` / `checkWecomPlatformPermit` / `normalizeWecomSenderForPermit`、`qqGuildPermitResolver` / `checkQqPlatformPermit` / `normalizeQqGuildSenderForPermit`。permit 一律交给内置评估器(`@zhin.js/permission`),`platformPermit()` 仍保留;各适配器 `PERMITS.md` 按实际上报的字段重写。
|
|
24
|
+
|
|
25
|
+
- 57c6cdc: fix: 全仓审计修复
|
|
26
|
+
|
|
27
|
+
- kernel: 模板求值沙箱不再泄漏宿主对象——宿主函数的返回值与抛出的错误会重新包裹代理(`segments.map(x=>x).constructor.constructor(...)` 之前可拿到宿主 `Function`),并禁止通过代理写入/删除宿主属性。
|
|
28
|
+
- agent: exec 白名单正则按整体锚定(`git|npm` 不再匹配 `gitevil`);SSRF 主机名黑名单识别尾点形式(`localhost.`);Docker 沙箱 `always` 模式的 fail-closed 分支可被触达;声明缺失的 `chalk` 依赖。
|
|
29
|
+
- ai: 摘要按最新优先加载(超过 500 条时不再使用过期锚点);修复 `test` 脚本指向不存在的配置。
|
|
30
|
+
- cli: `ZHIN_LOG_LEVEL` 生效(修复运算符优先级);移除旧守护进程遗留的无引用进程辅助函数。
|
|
31
|
+
- logger: `formatCompact` 不再把 `config: 'yaml'`、`direction: 'in'` 这类非路径值改写成 `./yaml`。
|
|
32
|
+
- host-http: 未配置数据库时 `db:tables` 与有数据库时一致返回 `{ tables }`。
|
|
33
|
+
- a2a: 405 响应把 `Allow` 放进响应头。
|
|
34
|
+
- pagemanager: 内联进 `<script>` 的页面元数据转义 `<`。
|
|
35
|
+
- client: 事件历史请求失败时携带 HTTP 状态,token 失效可触发重新登录。
|
|
36
|
+
- scaffold-wizard / create-zhin-app: 修正适配器文档 URL 与模板里的 `@zhin.js/tool` 导入。
|
|
37
|
+
- adapter-wechat-mp: 签名比较改为常量时间。
|
|
38
|
+
- adapter-kook: 声明运行时依赖 `ws`。
|
|
39
|
+
- plugin-lottery: 遗漏值按最近一次出现计算;推送目标 schema 字段与实现一致(`endpointKey`)。
|
|
40
|
+
- plugin-code-runner: 语言校验改用自有属性判断。
|
|
41
|
+
- plugin-link-poster: 抓取前校验主机名白名单(修复 SSRF),超时覆盖响应体读取。
|
|
42
|
+
- 其余包:从 `files` 中移除不存在的目录。
|
|
43
|
+
- feature-kit / middleware: 修正 README 中层级错误的文档链接。
|
|
44
|
+
|
|
45
|
+
- e1fda87: fix: 按审计待审清单处理(安全、功能、清理)
|
|
46
|
+
|
|
47
|
+
**行为变更(升级前请阅读)**
|
|
48
|
+
|
|
49
|
+
- kernel / core / zhin.js:出站文本中的 `${…}` 不再用 `vm` 求值,只替换 `TemplateContext` 上能解析到值的属性路径(如 `${sender.name}`);表达式、函数调用、未知名字原样保留。整套 vm 沙箱(`evaluate` / `execute` / `compiler` / `getValueWithRuntime`)已移除。
|
|
50
|
+
- kernel / core / zhin.js:移除零引用的遗留导出——`Scheduler`、错误管理器与 `ZhinError` 子类、`compose`、`segment.from/toString`、AI 触发/准入函数、`@zhin.js/core/queue-im-field-contract`、`zhin.js/contexts`。
|
|
51
|
+
- agent:readonly 执行预设不再放行重定向、后台 `&`、进程替换、`find -delete/-exec`、`sed -i`、`sort -o`、`tee`、`xargs`、awk;curl/wget 校验每个 URL 与裸主机并识别更多上传参数;自定义 `execAllowlist` 中的只读命令带写入风险时改走审批。沙箱超时/取消会杀掉进程组或具名容器,默认镜像改为 `node:22-bookworm-slim` + `bash`。移除无生产调用的 `AuditLogger`、`AnomalyDetector`、旧 vision 回合等导出。
|
|
52
|
+
- ai:移除 `storage`、`FileStateCache`、`CostTracker`、`model-name-utils`、`./agent-step-checkpoint` 子路径、`ContextManager` 及旧压缩管线;延迟工具快照改为有界 LRU。
|
|
53
|
+
- cli:`zhin send` / `watch` / `schedule` 改为与运行中的 Host 通信(新增 `POST /message/send` 与 `/assistant/jobs` 写接口),`schedule` 不再支持离线改文件;移除 `zhin uninstall service`(用 `zhin service uninstall`)、`build --analyze`、`install -S`;`zhin stop` 杀不掉进程时保留 pid 文件并以非零退出;`runtime migrate` 新增 `verify`;未设置 `TZ` 时调度默认时区改为系统时区;不带 `ZHIN_SUPERVISOR_PID` 的进程被 init 接管后不再自行退出。
|
|
54
|
+
- schedule:后台 reconcile 出错不再产生未处理 rejection(新增 `onStoreError`);启动途中 `stop()` 不再遗留定时器。
|
|
55
|
+
- schema:元组校验与 `safeParse`、JSON Schema 一致(空数组不通过,缺省的可选元组为 `undefined`)。
|
|
56
|
+
- client / contract:删除无后端路由的 agent-stream API、未使用的 `definePage` 与旧 PageManager 类型;`@zhin.js/contract` 不再依赖 `@zhin.js/ai`;客户端收到 401 会清除已保存的凭据;安装/更新/卸载 RPC 默认超时 120 秒。
|
|
57
|
+
- a2a:流式响应中途出错写入 SSE error 并结束;未配置 token 的非生产环境只接受本机请求;任务存储有上限与过期;Mailbox 凭据回退与生产环境缺密钥检查修复。
|
|
58
|
+
- speech:`provider: openai` 不再套用 Ollama 的默认 host/model;识别语言改为可配置,不再写死 `zh`。
|
|
59
|
+
- adapter-lark:支持 `encryptKey` 加密事件解密。adapter-github:Device Flow 可取消、带超时并回复失败原因。adapter-wecom / dingtalk:请求加超时并校验 HTTP 状态。adapter-weixin-ilink:输入关闭期间暂停轮询。adapter-milky:SSE 支持 CRLF、缓冲上限与空闲重连。adapter-onebot-events:好友删除通知改为 `notice.friend.decrease`。
|
|
60
|
+
- plugin-group-suite:`teachAllowRegex` 默认关闭,正则安全检查更严格。plugin-qrcode:改为本地生成,回复 data URL 图片。plugin-rss / short-url / link-poster:拒绝解析到内网的地址;rss 修复条目每周重复推送;short-url 的 Console 创建改为真实调用 is.gd。plugin-lottery:按 `historyLimit` 同步。
|
|
61
|
+
|
|
62
|
+
**其余**
|
|
63
|
+
|
|
64
|
+
- host-http:插件市场详情加缓存与包名校验,SSE 回放容忍背压。host-http-contract:新增 `readTextBody()`,九个适配器改用它。
|
|
65
|
+
- pagemanager:页面外壳 importmap 升级到 React 19.2.8 / react-router 7.12.0。console-contract:`definePage` 接受 `description` / `group` / `parent` / `fullWidth`。
|
|
66
|
+
- command:进度提示本地化,匹配不再重复拷贝消息段。runtime:源码监听降级后退避重试原生监听。
|
|
67
|
+
- html-renderer:删除未接线的 `aiTextAsImage`。create-zhin-app / scaffold-wizard:移除不起作用的"全局安装 CLI"与运行时选择提示。
|
|
68
|
+
- 依赖声明清理、发布清单不再携带解析不到的 `src` 目录、移除未使用的 Skill 挂载、README 与示例配置按当前实现更正。
|
|
69
|
+
|
|
70
|
+
- 120e225: 聊天角色(ADR 0056,2026-11-12 修订):zhin 角色 owner / admin / trusted 分四级、只来自授予;发言者的平台身份与机器人自身的平台身份是独立维度,命令、Agent 工具 / 技能、中间件与按钮按三条维度共用一个评估器过滤。升级说明见 docs/migrations/chat-roles.md。
|
|
71
|
+
|
|
72
|
+
- zhin.js:实例首次启动把适配器 `master` / `trusted`、`endpoints[i].master|trusted` 与 `ai.trigger.masters|trusted` 迁入项目数据库 `chat_roles`(先记入 `.zhin/chat-roles-migration.json`,再从配置移除并记日志);之后配置再出现这些字段时启动报错并指向升级指南。平台身份不再映射为 zhin 角色:删除 `roles.mappings`(适配器块与 `endpoints[i].roles` 出现即启动报错)、`chat_role_mappings` 表与 Console `chat-roles.mappings` / `chat-roles.set-mappings`;`features.selfService.platformAdmins|roles|allowUsers` 删除;群内「功能」「角色」命令只认 zhin 角色,平台群主不再自动具备;聊天改配置、聊天重启、Agent 主人审批按 zhin `owner` 判断。
|
|
73
|
+
- @zhin.js/core(minor,破坏性):角色模型与解析移入 core(`ChatRoleService`、`createChatRoleSenderEnricher`),入站消息带 `sender.chatRole` 与归一的 `sender.platform`;删除 `SenderRole`、`senderRolesFromMessage`、`roleSatisfies` 等,`master` 改名 `owner`;删除映射规则 API(`createMappingChatRoleSource`、`DEFAULT_CHAT_ROLE_MAPPINGS` 等)与 `ChatRoleSource`;`createSenderEnricher` / `createConfigurationChatAuthorizer` 改为基于聊天角色。IM Runtime 为 PermissionHost 提供机器人自身身份(经 Adapter 索引缓存),机器人的角色变化事件使缓存失效;命令拒绝回复说明缺少的平台 / 机器人条件;`<Button permit>` 按点击者鉴权;场景治理工具工厂同时要求 `platform:*` 与 `bot:*`;`ai.trigger.masters|trusted` 删除。
|
|
74
|
+
- @zhin.js/permission(minor,破坏性):permit 语法 `owner` / `admin` / `trusted` / `role(...)`(zhin 角色)、`platform:owner|admin`、`platform-role:<id>`、`platform-permission:<name>`(发言者平台身份),新增 `bot:owner|admin`、`bot-role:<id>`、`bot-permission:<name>`(机器人自身平台身份,`PermissionSubject.bot`);`PermissionHost` 新增 `setBotStandingResolver`、`withBotStanding`、`firstDenied`,`checkAll` 可带 `source` 解析机器人身份;新增 `describeDeniedPermit`、`permitsNeedBotStanding`。删除 `role(master)`、`scene_admin`、`scene_owner`、`platform(adapter,perm)` 及 `registerPlatform`、`createSceneRolePlatformChecker`、`platformPermission`、`definePlatformPermission`,定义时报错并给出替代写法。
|
|
75
|
+
- @zhin.js/command(minor,破坏性):命令会话的发送者改为 `chatRole` / `platform`,不再从 metadata 读角色;`CONVERSATION_MANAGER_PERMIT` 改为 `admin`(平台群主不再自动满足);分发结果 `denied` 带 `denial`(第一条不满足的规则与原因);指令菜单只列出当前发送者在本会话可用的命令。
|
|
76
|
+
- @zhin.js/agent(minor,破坏性):危险工具、文件 / exec 策略、主人审批、Agent 回合主体只读 zhin 角色(`principal.roles` 只含展开的聊天角色,平台地位在 `principal.platform`);`SenderRole` → `PrincipalRole`,`SELF_CHANGE_MASTER_ROLE` → `SELF_CHANGE_OWNER_ROLE`;回合开始时读取机器人自身身份(`contextState.bot.platform`),按三条维度过滤 tool / skill / Agent 包后才披露,执行工具时再校验一次;群规「群主和管理员豁免」改为平台群主 / 管理员与 zhin `owner`。
|
|
77
|
+
- @zhin.js/feature-kit:`OperationSession.sender` 改为 `chatRole` / `platform`。
|
|
78
|
+
- @zhin.js/im-contract:新增 `PlatformStanding`、`SelfStandingRequest`、`standingFromMemberRole`。
|
|
79
|
+
- @zhin.js/adapter:入站 sender 可带 `platform`;账号管理命令需要聊天角色 `owner`;`EndpointManagement` 新增可选 `inspectSelfStanding`,能力画像新增 `scene.selfStanding`(声明即须实现),`AdapterIndex.selfStanding` / `invalidateSelfStanding` 按会话缓存机器人自身身份。
|
|
80
|
+
- @zhin.js/middleware:入站中间件可声明 `permit`,不满足时跳过该中间件(机器人身份只读缓存)。
|
|
81
|
+
- @zhin.js/tool:定义工具时拒绝已移除的 permit 写法。
|
|
82
|
+
- @zhin.js/ai:注释更新。
|
|
83
|
+
- @zhin.js/console-protocol:`chat-roles.list|grant|revoke`(`iam.manage`);`ConsoleChatRoleSource` 只有 `grant`。
|
|
84
|
+
- @zhin.js/runtime:Host 配置 Schema 删除 `features.selfService.platformAdmins|roles|allowUsers`。
|
|
85
|
+
- @zhin.js/testing:伪造消息与 `PluginTestKit` 用 `sender.chatRole` 声明测试身份。
|
|
86
|
+
- @zhin.js/scaffold-wizard:ICQQ 向导不再写 `master`,完成后提示在 Console 账号「角色」页签或聊天 `角色 授予` 设置 owner。
|
|
87
|
+
- 适配器:配置 Schema 删除 `master` / `trusted`(以及预览版的 `roles`);群管理类工具同时声明发言者条件与机器人条件(如 `platform:admin` + `bot:admin`、`bot-permission:manage_roles`),NapCat / OneBot11 工具不再使用已移除的 `platform(adapter,…)` 写法。
|
|
88
|
+
- 插件:内容审核以 `inbound.bypass`(permit 列表,默认 `owner` 或 `platform:admin`)取代 `masters` / `inbound.bypassMasters`;群管、音乐、日历、地震、B 站、轮盘赌的权限改用 zhin 角色,订阅类命令只认 zhin `admin` / `owner`。
|
|
89
|
+
|
|
90
|
+
- c2bbdcf: Console 访问控制(ADR 0049):用户、角色、会话、API 令牌与审计日志存入项目数据库,每个 Console RPC、`/api` 路由与 Host 鉴权的 WebSocket 都按 `@zhin.js/console-protocol` 的权限表放行,未登记的一律拒绝。
|
|
91
|
+
|
|
92
|
+
**破坏性变更**
|
|
93
|
+
|
|
94
|
+
- `http.tokens` 与演示模式(`scope: demo` / `user`)删除;配置里仍有 `http.tokens` 时 Host 拒绝启动。Host 配置 Schema 同步删除该字段。
|
|
95
|
+
- `http.token` 变为初始化令牌:只在创建第一个所有者之前有效,之后在所有接口上被拒绝。
|
|
96
|
+
- 未在 `CONSOLE_WS_PERMISSIONS` 登记、又使用 Host 鉴权的插件 WebSocket 不再接受 Console 账号;插件自己的 WebSocket 应改用 `ws(path, { auth: 'protocol' })` 并自行校验凭据(新增 `verifyProtocolAccessToken`、`isLoopbackAddress`)。
|
|
97
|
+
- OneBot 11 / 12、NapCat、Milky 的反向 WebSocket 改为只认适配器自己的 `access_token`;未配置 `access_token` 时只接受本机回环连接。
|
|
98
|
+
- `zhin doctor --live` 等改从 `ZHIN_CONSOLE_TOKEN` 读取 API 令牌(仍兼容 `ZHIN_HTTP_TOKEN`)。
|
|
99
|
+
|
|
100
|
+
**迁移步骤**(详见文档「升级到 Console 访问控制」)
|
|
101
|
+
|
|
102
|
+
1. 升级后用 `http.token` 打开 Console 初始化向导创建第一个所有者,或运行 `zhin console init`(脚本:`zhin console init --yes --owner <名> --password-stdin`)。
|
|
103
|
+
2. 删除配置里的 `http.tokens`,在 Console「访问控制」里为原来的令牌持有人创建用户并绑定角色(`viewer` / `operator` / `admin`);演示环境改用 `viewer` 账号。
|
|
104
|
+
3. 自动化在 Console 签发 API 令牌,放进 `ZHIN_CONSOLE_TOKEN`。
|
|
105
|
+
4. 从其他机器或容器连接的反向 WebSocket endpoint 补上 `access_token`。
|
|
106
|
+
5. 忘记密码或所有者被锁时,停下 Host,用 `zhin console user passwd|enable|create` 离线恢复。
|
|
107
|
+
|
|
108
|
+
MCP / A2A 的 Bearer 访问同样接受用户 API 令牌(分别需要 `mcp.access`、`a2a.access`)。适配器与插件文档同步改为账号与角色的说法。
|
|
109
|
+
|
|
110
|
+
- 5cd9d3d: HTTP 路由契约改为 Web 标准 `Request` / `Response`(ADR 0054 §2,B1.0)
|
|
111
|
+
|
|
112
|
+
- host-http-contract:路由处理函数签名改为 `(request, context) => Response`,`context` 提供 `url`、`params`、`principal`、`remoteAddress`;新增 `readBodyBytes` / `readTextBody` / `readJsonBody`(带大小上限,超限先读完再报 413)、`jsonResponse` / `emptyResponse`、带背压的 `createResponseStream`(SSE、大文件下载)与 `WsConnection`。契约不再引用 `node:http` / `ws`。
|
|
113
|
+
- host-http:Node 服务器移到 `@zhin.js/host-http/node`(`createHttpHost`),负责 Node 请求与 `Request` / `Response` 的互转;认证结果经 `context.principal` 传给路由,删除旧的按请求取主体的辅助函数;未预期的异常只返回通用错误信息,细节写日志。
|
|
114
|
+
- runtime、mcp、a2a、gateway、zhin.js、calendar 插件与各适配器的 Webhook / 反向 WebSocket 路由迁移到新契约,行为不变。
|
|
115
|
+
|
|
116
|
+
- 68659cd: `zod` 改为运行时依赖(原为可选对等依赖):插件运行时在启动时就会加载这些包的约定目录(Agent 工具、命令等),其中静态导入了 `zod`;生成的项目里没有安装可选对等依赖,启动会报 `Cannot find package 'zod'`。范围为 `^4.6.7`(已修复的版本)。
|
|
117
|
+
- Updated dependencies [53932e5]
|
|
118
|
+
- Updated dependencies [57c6cdc]
|
|
119
|
+
- Updated dependencies [e1fda87]
|
|
120
|
+
- Updated dependencies [4e8ba38]
|
|
121
|
+
- Updated dependencies [a9c7a13]
|
|
122
|
+
- Updated dependencies [120e225]
|
|
123
|
+
- Updated dependencies [c2bbdcf]
|
|
124
|
+
- Updated dependencies [c2bbdcf]
|
|
125
|
+
- Updated dependencies [63595fc]
|
|
126
|
+
- Updated dependencies [fd055d2]
|
|
127
|
+
- Updated dependencies [4e8ba38]
|
|
128
|
+
- Updated dependencies [cc7a5f9]
|
|
129
|
+
- Updated dependencies [4e8ba38]
|
|
130
|
+
- Updated dependencies [f8bf684]
|
|
131
|
+
- Updated dependencies [e0bc008]
|
|
132
|
+
- Updated dependencies [5cd9d3d]
|
|
133
|
+
- Updated dependencies [5cd9d3d]
|
|
134
|
+
- Updated dependencies [c2bbdcf]
|
|
135
|
+
- Updated dependencies [f8bf684]
|
|
136
|
+
- Updated dependencies [663473b]
|
|
137
|
+
- Updated dependencies [c606d51]
|
|
138
|
+
- Updated dependencies [5cd9d3d]
|
|
139
|
+
- Updated dependencies [d655ef0]
|
|
140
|
+
- Updated dependencies [1e54b1e]
|
|
141
|
+
- @zhin.js/adapter@1.1.33
|
|
142
|
+
- @zhin.js/core@1.2.0
|
|
143
|
+
- @zhin.js/logger@1.1.3
|
|
144
|
+
- @zhin.js/host-http@1.2.0
|
|
145
|
+
- @zhin.js/feature-kit@1.1.16
|
|
146
|
+
- @zhin.js/adapter-onebot-events@0.0.6
|
|
147
|
+
- @zhin.js/agent-feature@1.1.17
|
|
148
|
+
- @zhin.js/console-contract@1.1.6
|
|
149
|
+
- @zhin.js/permission@1.2.0
|
|
150
|
+
- @zhin.js/skill@1.1.17
|
|
151
|
+
- @zhin.js/tool@1.1.19
|
|
152
|
+
- @zhin.js/im-contract@1.1.14
|
|
153
|
+
- @zhin.js/page@1.1.18
|
|
154
|
+
|
|
3
155
|
## 1.1.26
|
|
4
156
|
|
|
5
157
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 凉菜
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PERMITS.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## Permit 词汇表
|
|
8
8
|
|
|
9
|
-
| `platform
|
|
9
|
+
| `platform-permission:…` | 含义 |
|
|
10
10
|
|------------------------|------|
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
11
|
+
| `platform:owner` | 群主 |
|
|
12
|
+
| `platform:admin` | 群管理员(含群主) |
|
|
13
13
|
|
|
14
|
-
工厂工具默认 `
|
|
14
|
+
工厂工具默认 `platform:admin` / `platform:owner`;扩展工具如 `onebot11_set_title` 使用 `platform:owner`。
|
package/README.md
CHANGED
|
@@ -86,19 +86,19 @@ onebot11:
|
|
|
86
86
|
| 类别 | 路径 |
|
|
87
87
|
|------|------|
|
|
88
88
|
| Permit 词汇 | `PERMITS.md` |
|
|
89
|
-
|
|
|
89
|
+
| 平台工具(1 个) | `agents/onebot11/skills/onebot11/tools/set_title/index.ts` → `onebot11_set_title` |
|
|
90
90
|
| 技能说明 | `agents/onebot11/skills/onebot11/SKILL.md` |
|
|
91
91
|
|
|
92
92
|
## 迁移说明(Plugin Runtime)
|
|
93
93
|
|
|
94
94
|
- **notice / request / meta 侧事件**:经统一 `endpointSpec.events` 归一后分发到 `handlers`;消息仍走 `outboundMessageToken`。
|
|
95
95
|
- **群管工具暂未迁移**:旧 Adapter 经 `createSceneManagementTools` 注册踢人 / 禁言 / 群名片等成套 agent 工具;迁移后仅保留 `onebot11_set_title`,其余群管能力可通过 `$client.call()`(如 `set_group_kick`、`set_group_ban`)作为逃生舱调用。
|
|
96
|
-
-
|
|
96
|
+
- **平台权限门禁**:工具 / 命令用内置 permit(ADR 0056 §5):发言者平台身份 `platform:admin` / `platform:owner` 依据入站 `sender.platform.standing`(owner / admin,见下文「成员身份」);机器人自身群内身份 `bot:admin` / `bot:owner` 依据 `get_group_member_info`(self)的 `role`(能力画像 `scene.selfStanding`,框架按会话缓存)。群管理类工具同时要求两者。
|
|
97
97
|
|
|
98
98
|
## 文档链接
|
|
99
99
|
|
|
100
100
|
- [OneBot 11 标准](https://github.com/botuniverse/onebot-11)
|
|
101
|
-
- [适配器概览](https://zhin.js.org/
|
|
101
|
+
- [适配器概览](https://zhin.js.org/adapters/)
|
|
102
102
|
|
|
103
103
|
## Console 页面
|
|
104
104
|
|
|
@@ -108,7 +108,7 @@ onebot11:
|
|
|
108
108
|
- 经所属 Adapter 增删持久化账号、启停单个 Endpoint;凭据由 Host 加密仓储保存,不改启动 YAML 或 `.env`,不重启进程;
|
|
109
109
|
- 如果适配器要求登录,页面会显示 LoginAssist 的扫码 / 滑块 / 设备确认待办;
|
|
110
110
|
|
|
111
|
-
读取需要 `onebot11:read
|
|
111
|
+
读取需要 `onebot11:read`(`read` 级别,内置角色 `viewer` 及以上都有);增删改账号等写操作需要 `onebot11:write`(`operate` 级别,`operator` 及以上都有,绑定到本适配器的角色也算),首次迁移还需管理员身份;全局所有者不受限,见[插件权限的级别](https://zhin.js.org/console/#插件权限的级别)。页面里的「添加 endpoint」等价于原来的 `onebot11 endpoint add`。
|
|
112
112
|
|
|
113
113
|
## 故障排查
|
|
114
114
|
|
|
@@ -126,3 +126,7 @@ MIT License
|
|
|
126
126
|
### 高阶消息支持边界
|
|
127
127
|
|
|
128
128
|
OneBot11 标准支持链接分享:canonical share 的 url/title/description/image 分别映射标准 url/title/content/image,保留标题与描述。具体桥是否实现需实机确认;NapCat 的 share 发送限制不能以标准支持推断通过。标准没有 Markdown 与 keyboard 段,直接出站明确 `unsupported_operation / not_sent`;文字交互降级不能算原生按钮支持。参考:[OneBot11 消息段规范](https://github.com/botuniverse/onebot-11/blob/master/message/segment.md)。
|
|
129
|
+
|
|
130
|
+
## 成员身份
|
|
131
|
+
|
|
132
|
+
群消息的 `sender.role`(`owner` / `admin` / `member`)归一为 `sender.platform.standing`;私聊不上报。OneBot v11 没有平台原生角色与权限位,`roles` / `permissions` 不上报。能力画像:`group.scene.platformStanding = { standing: true }`。详见 [ADR 0056 §1](https://github.com/zhinjs/zhin/blob/main/docs/adr/0056-chat-roles.md)。
|
|
@@ -26,6 +26,9 @@ export default defineAdapter({
|
|
|
26
26
|
},
|
|
27
27
|
delivery: { send: true, proactive: true, spool: true },
|
|
28
28
|
},
|
|
29
|
+
// ADR 0056 §1:群消息带 sender.role,归一为 sender.platform.standing。
|
|
30
|
+
// ADR 0056 §1:发言者与机器人自身的群内地位(get_group_member_info self,供 bot:* permit)。
|
|
31
|
+
group: { scene: { platformStanding: { standing: true }, selfStanding: { standing: true } } },
|
|
29
32
|
},
|
|
30
33
|
createAdapter: context => new Onebot11Adapter(context),
|
|
31
34
|
});
|
|
@@ -32,6 +32,9 @@ export default defineAdapter<OneBot11EndpointConfig>({
|
|
|
32
32
|
},
|
|
33
33
|
delivery: { send: true, proactive: true, spool: true },
|
|
34
34
|
},
|
|
35
|
+
// ADR 0056 §1:群消息带 sender.role,归一为 sender.platform.standing。
|
|
36
|
+
// ADR 0056 §1:发言者与机器人自身的群内地位(get_group_member_info self,供 bot:* permit)。
|
|
37
|
+
group: { scene: { platformStanding: { standing: true }, selfStanding: { standing: true } } },
|
|
35
38
|
},
|
|
36
39
|
createAdapter: context => new Onebot11Adapter(context),
|
|
37
40
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { messages as t } from './messages.js';
|
|
3
|
+
/**
|
|
4
|
+
* The trusted turn owns the group; model input cannot select another one. Platform (`platform:*`) and
|
|
5
|
+
* Bot (`bot:*`) permits are judged in the current conversation, so group administration tools act on
|
|
6
|
+
* the current group only (ADR 0056 §5).
|
|
7
|
+
*/
|
|
8
|
+
export function currentGroup(context) {
|
|
9
|
+
const { origin } = context;
|
|
10
|
+
const conversation = origin?.kind === 'im' && origin.platform === 'onebot11' ? origin.conversation : undefined;
|
|
11
|
+
if (!conversation || conversation.kind !== 'group')
|
|
12
|
+
throw new Error(t(context.locale, 'groupRequired'));
|
|
13
|
+
context.signal?.throwIfAborted();
|
|
14
|
+
return Number(conversation.id);
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ToolExecutionContext } from '@zhin.js/tool';
|
|
2
|
+
import { messages as t } from './messages.js';
|
|
3
|
+
|
|
4
|
+
export type Onebot11ToolContext = ToolExecutionContext<unknown, 'onebot11'>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The trusted turn owns the group; model input cannot select another one. Platform (`platform:*`) and
|
|
8
|
+
* Bot (`bot:*`) permits are judged in the current conversation, so group administration tools act on
|
|
9
|
+
* the current group only (ADR 0056 §5).
|
|
10
|
+
*/
|
|
11
|
+
export function currentGroup(context: Onebot11ToolContext): number {
|
|
12
|
+
const { origin } = context;
|
|
13
|
+
const conversation = origin?.kind === 'im' && origin.platform === 'onebot11' ? origin.conversation : undefined;
|
|
14
|
+
if (!conversation || conversation.kind !== 'group') throw new Error(t(context.locale, 'groupRequired'));
|
|
15
|
+
context.signal?.throwIfAborted();
|
|
16
|
+
return Number(conversation.id);
|
|
17
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { createMessages } from 'zhin.js/i18n';
|
|
3
3
|
export const messages = createMessages({
|
|
4
|
+
"groupRequired": {
|
|
5
|
+
"zh-CN": "此操作仅支持当前 QQ 群",
|
|
6
|
+
"en": "This operation requires the current QQ group"
|
|
7
|
+
},
|
|
4
8
|
"setFailed": {
|
|
5
9
|
"zh-CN": "设置失败",
|
|
6
10
|
"en": "Update failed"
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createMessages } from 'zhin.js/i18n';
|
|
2
2
|
|
|
3
3
|
export const messages = createMessages({
|
|
4
|
+
"groupRequired": {
|
|
5
|
+
"zh-CN": "此操作仅支持当前 QQ 群",
|
|
6
|
+
"en": "This operation requires the current QQ group"
|
|
7
|
+
},
|
|
4
8
|
"setFailed": {
|
|
5
9
|
"zh-CN": "设置失败",
|
|
6
10
|
"en": "Update failed"
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { currentGroup } from '../../../../conversation.js';
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
6
|
-
description: '
|
|
7
|
+
description: '设置当前 QQ 群成员的专属头衔。只有群主才能设置。',
|
|
7
8
|
inputSchema: z.object({
|
|
8
|
-
group_id: z.number().describe('目标群号'),
|
|
9
9
|
user_id: z.number().describe('目标成员 QQ号'),
|
|
10
10
|
title: z.string().describe('头衔文字'),
|
|
11
11
|
}),
|
|
12
12
|
adapter: 'onebot11',
|
|
13
13
|
scopes: ["group"],
|
|
14
14
|
tags: ['onebot11'],
|
|
15
|
-
permissions: ['platform
|
|
16
|
-
async execute({
|
|
15
|
+
permissions: ['platform:owner', 'bot:owner'],
|
|
16
|
+
async execute({ user_id, title }, context) {
|
|
17
|
+
const group_id = currentGroup(context);
|
|
17
18
|
const client = context.$client;
|
|
18
19
|
const response = await client.call('set_group_special_title', {
|
|
19
20
|
group_id,
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { currentGroup } from '../../../../conversation.js';
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
|
-
export default defineAgentTool<{
|
|
5
|
-
description: '
|
|
5
|
+
export default defineAgentTool<{ user_id: number; title: string }>({
|
|
6
|
+
description: '设置当前 QQ 群成员的专属头衔。只有群主才能设置。',
|
|
6
7
|
inputSchema: z.object({
|
|
7
|
-
group_id: z.number().describe('目标群号'),
|
|
8
8
|
user_id: z.number().describe('目标成员 QQ号'),
|
|
9
9
|
title: z.string().describe('头衔文字'),
|
|
10
10
|
}),
|
|
11
11
|
adapter: 'onebot11',
|
|
12
12
|
scopes: ["group"],
|
|
13
13
|
tags: ['onebot11'],
|
|
14
|
-
permissions: ['platform
|
|
15
|
-
async execute({
|
|
14
|
+
permissions: ['platform:owner', 'bot:owner'],
|
|
15
|
+
async execute({ user_id, title }, context) {
|
|
16
|
+
const group_id = currentGroup(context);
|
|
16
17
|
const client = context.$client;
|
|
17
18
|
const response = await client.call('set_group_special_title', {
|
|
18
19
|
group_id,
|
|
@@ -1,5 +1,42 @@
|
|
|
1
|
+
import { standingFromMemberRole } from '@zhin.js/im-contract';
|
|
2
|
+
/** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
|
|
3
|
+
const SELF_ID_TTL_MS = 5_000;
|
|
1
4
|
export function createOneBot11EndpointManagement(endpoint) {
|
|
5
|
+
// The login account can change across reconnects (or a different account on the same
|
|
6
|
+
// connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
|
|
7
|
+
// to coalesce bursts, and never across a failed lookup.
|
|
8
|
+
let login;
|
|
9
|
+
const loginId = async (signal) => {
|
|
10
|
+
if (login && Date.now() - login.at < SELF_ID_TTL_MS)
|
|
11
|
+
return login.id;
|
|
12
|
+
login = undefined;
|
|
13
|
+
const info = asRecord(await endpoint.callApi({ action: 'get_login_info', signal }));
|
|
14
|
+
const id = info.user_id;
|
|
15
|
+
const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
|
|
16
|
+
if (value)
|
|
17
|
+
login = { id: value, at: Date.now() };
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
2
20
|
return Object.freeze({
|
|
21
|
+
/** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
|
|
22
|
+
async inspectSelfStanding({ conversation, signal }) {
|
|
23
|
+
if (conversation.kind !== 'group')
|
|
24
|
+
return undefined;
|
|
25
|
+
const selfId = await loginId(signal);
|
|
26
|
+
if (!selfId)
|
|
27
|
+
return undefined;
|
|
28
|
+
let member;
|
|
29
|
+
try {
|
|
30
|
+
member = asRecord(await endpoint.callApi({ action: 'get_group_member_info', signal, params: {
|
|
31
|
+
group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
|
|
32
|
+
} }));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
login = undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return standingFromMemberRole(member.role);
|
|
39
|
+
},
|
|
3
40
|
async listFriends(request = {}) {
|
|
4
41
|
const data = await endpoint.callApi({ action: 'get_friend_list', signal: request.signal });
|
|
5
42
|
return toArray(data).map((value) => {
|
package/lib/protocol.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
* Spec: https://github.com/botuniverse/onebot-11
|
|
6
6
|
*/
|
|
7
|
-
import { type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
|
|
8
8
|
import { type Segment } from '@zhin.js/core';
|
|
9
9
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
10
10
|
export interface OneBot11EndpointConfig extends EndpointProfileConfiguration {
|
|
@@ -98,6 +98,11 @@ export declare function senderDisplayName(ev: OneBot11Event): string;
|
|
|
98
98
|
* user_id 缺失时兜底为空串,绝不回退到显示名。
|
|
99
99
|
*/
|
|
100
100
|
export declare function senderUserId(ev: OneBot11Event): string;
|
|
101
|
+
/**
|
|
102
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
103
|
+
* 私聊不上报;未知角色词不上报。OneBot v11 没有平台原生角色与权限位。
|
|
104
|
+
*/
|
|
105
|
+
export declare function onebot11SenderStanding(ev: OneBot11Event): PlatformStanding | undefined;
|
|
101
106
|
/** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
|
|
102
107
|
export declare function senderNickname(ev: OneBot11Event): string | undefined;
|
|
103
108
|
/**
|
package/lib/protocol.js
CHANGED
|
@@ -119,6 +119,16 @@ export function senderDisplayName(ev) {
|
|
|
119
119
|
export function senderUserId(ev) {
|
|
120
120
|
return ev.user_id != null ? String(ev.user_id) : '';
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
124
|
+
* 私聊不上报;未知角色词不上报。OneBot v11 没有平台原生角色与权限位。
|
|
125
|
+
*/
|
|
126
|
+
export function onebot11SenderStanding(ev) {
|
|
127
|
+
if (ev.message_type !== 'group')
|
|
128
|
+
return undefined;
|
|
129
|
+
const role = ev.sender?.role;
|
|
130
|
+
return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
|
|
131
|
+
}
|
|
122
132
|
/** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
|
|
123
133
|
export function senderNickname(ev) {
|
|
124
134
|
const name = ev.sender?.card || ev.sender?.nickname;
|
package/lib/ws-endpoint.js
CHANGED
|
@@ -7,7 +7,7 @@ import WebSocket from 'ws';
|
|
|
7
7
|
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
8
8
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
9
|
import { createOneBot11EndpointManagement } from './endpoint-management.js';
|
|
10
|
-
import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, senderUserId, } from './protocol.js';
|
|
10
|
+
import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, onebot11SenderStanding, senderUserId, } from './protocol.js';
|
|
11
11
|
import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
|
|
12
12
|
import { createOneBot11ContentPort } from './content-port.js';
|
|
13
13
|
import { createOneBot11Rpc } from './ws-transport.js';
|
|
@@ -151,6 +151,7 @@ export class OneBot11WsEndpoint extends Onebot11Client {
|
|
|
151
151
|
const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
152
152
|
const content = formatInboundContent(ev);
|
|
153
153
|
const inbound = formatInboundMetadata(ev, this.#options.config.id);
|
|
154
|
+
const platform = onebot11SenderStanding(ev);
|
|
154
155
|
void this[endpointSpec].events.message({
|
|
155
156
|
event: ev,
|
|
156
157
|
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
@@ -160,7 +161,7 @@ export class OneBot11WsEndpoint extends Onebot11Client {
|
|
|
160
161
|
sender: {
|
|
161
162
|
id: senderUserId(ev),
|
|
162
163
|
name: senderDisplayName(ev) || undefined,
|
|
163
|
-
...(
|
|
164
|
+
...(platform ? { platform } : {}),
|
|
164
165
|
},
|
|
165
166
|
endpointId: inbound.endpointId,
|
|
166
167
|
...(inbound.mentioned ? { mentioned: true } : {}),
|
package/lib/ws-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WsConnection } from '@zhin.js/host-http';
|
|
1
2
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
3
|
export interface OneBot11WsSocket {
|
|
3
4
|
readonly readyState: number;
|
|
@@ -11,3 +12,8 @@ export interface OneBot11WsCreateOptions {
|
|
|
11
12
|
readonly headers?: Record<string, string>;
|
|
12
13
|
}
|
|
13
14
|
export declare const WS_OPEN = 1;
|
|
15
|
+
/**
|
|
16
|
+
* A Host reverse-WebSocket connection seen through the OneBot11WsSocket surface. Protocol
|
|
17
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function socketOfConnection(connection: WsConnection): OneBot11WsSocket;
|
package/lib/ws-types.js
CHANGED
|
@@ -1 +1,32 @@
|
|
|
1
1
|
export const WS_OPEN = 1;
|
|
2
|
+
/**
|
|
3
|
+
* A Host reverse-WebSocket connection seen through the OneBot11WsSocket surface. Protocol
|
|
4
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
5
|
+
*/
|
|
6
|
+
export function socketOfConnection(connection) {
|
|
7
|
+
let readyState = 1;
|
|
8
|
+
connection.onClose(() => { readyState = 3; });
|
|
9
|
+
const subscriptions = new Map();
|
|
10
|
+
const off = (_event, listener) => {
|
|
11
|
+
subscriptions.get(listener)?.();
|
|
12
|
+
subscriptions.delete(listener);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
get readyState() { return readyState; },
|
|
16
|
+
send: (data) => connection.send(data),
|
|
17
|
+
close: (code, reason) => connection.close(code, reason),
|
|
18
|
+
on(event, listener) {
|
|
19
|
+
if (event === 'message') {
|
|
20
|
+
subscriptions.set(listener, connection.onMessage((data) => {
|
|
21
|
+
listener(typeof data === 'string' ? data : Buffer.from(data));
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
else if (event === 'close') {
|
|
25
|
+
subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
|
|
26
|
+
}
|
|
27
|
+
// 'error' surfaces as a close on a WsConnection; 'open' already happened.
|
|
28
|
+
},
|
|
29
|
+
off,
|
|
30
|
+
removeListener: off,
|
|
31
|
+
};
|
|
32
|
+
}
|
package/lib/wss-auth.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function verifyOneBotAccessToken(accessToken: string | undefined, request:
|
|
1
|
+
import { type ProtocolAuthRequest } from '@zhin.js/host-http';
|
|
2
|
+
export declare function verifyOneBotAccessToken(accessToken: string | undefined, request: ProtocolAuthRequest): boolean;
|
package/lib/wss-auth.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
+
import { verifyProtocolAccessToken } from '@zhin.js/host-http';
|
|
1
2
|
export function verifyOneBotAccessToken(accessToken, request) {
|
|
3
|
+
// Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
|
|
2
4
|
if (!accessToken)
|
|
3
5
|
return true;
|
|
4
|
-
|
|
5
|
-
if (auth === `Bearer ${accessToken}`)
|
|
6
|
-
return true;
|
|
7
|
-
try {
|
|
8
|
-
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
9
|
-
if (url.searchParams.get('access_token') === accessToken)
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
/* ignore */
|
|
14
|
-
}
|
|
15
|
-
return false;
|
|
6
|
+
return verifyProtocolAccessToken(accessToken, request);
|
|
16
7
|
}
|
package/lib/wss-endpoint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
3
3
|
*/
|
|
4
4
|
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
5
|
-
import type
|
|
5
|
+
import { type HttpHost } from '@zhin.js/host-http';
|
|
6
6
|
import type { CapabilityId } from 'zhin.js';
|
|
7
7
|
import { type OneBot11WssConfig } from './protocol.js';
|
|
8
8
|
import { Onebot11Client } from './client.js';
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -4,13 +4,14 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
4
4
|
* OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
5
5
|
*/
|
|
6
6
|
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
7
|
+
import { verifyProtocolAccessToken } from '@zhin.js/host-http';
|
|
7
8
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
9
|
import { createOneBot11EndpointManagement } from './endpoint-management.js';
|
|
9
|
-
import { buildSendAction, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, senderUserId, } from './protocol.js';
|
|
10
|
+
import { buildSendAction, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, onebot11SenderStanding, senderUserId, } from './protocol.js';
|
|
10
11
|
import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
|
|
11
12
|
import { createOneBot11ContentPort } from './content-port.js';
|
|
12
|
-
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
13
13
|
import { createOneBot11Rpc } from './ws-transport.js';
|
|
14
|
+
import { socketOfConnection } from './ws-types.js';
|
|
14
15
|
import { probeOnebot11SelfId, resolveOnebot11SelfId } from './self-id.js';
|
|
15
16
|
import { callOnebot11Client, readOnebot11Response, onebot11ClientOptions, forwardOnebot11ClientEvents, Onebot11Client } from './client.js';
|
|
16
17
|
export class OneBot11WssEndpoint extends Onebot11Client {
|
|
@@ -83,15 +84,16 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
83
84
|
return;
|
|
84
85
|
this.#stopped = false;
|
|
85
86
|
if (!this.#options.config.access_token) {
|
|
86
|
-
//
|
|
87
|
+
// 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
|
|
87
88
|
this.#logger.warn(formatCompact({
|
|
88
89
|
endpoint: this.#options.config.id,
|
|
89
90
|
mode: 'wss',
|
|
90
91
|
ok: false,
|
|
91
|
-
error: 'missing access_token',
|
|
92
|
+
error: 'missing access_token: only loopback peers are accepted',
|
|
92
93
|
}));
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
// The peer authenticates with this endpoint's own access_token, not a Console credential.
|
|
96
|
+
const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
|
|
95
97
|
this.#wsRelease = handle.onConnection((connection) => {
|
|
96
98
|
this.#connectionTask = this.#connectionTask
|
|
97
99
|
.then(() => this.#acceptConnection(connection))
|
|
@@ -158,6 +160,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
158
160
|
}
|
|
159
161
|
const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
160
162
|
const inbound = formatInboundMetadata(ev, this.#options.config.id);
|
|
163
|
+
const platform = onebot11SenderStanding(ev);
|
|
161
164
|
void this[endpointSpec].events.message({
|
|
162
165
|
event: ev,
|
|
163
166
|
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
@@ -167,7 +170,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
167
170
|
sender: {
|
|
168
171
|
id: senderUserId(ev),
|
|
169
172
|
name: senderDisplayName(ev) || undefined,
|
|
170
|
-
...(
|
|
173
|
+
...(platform ? { platform } : {}),
|
|
171
174
|
},
|
|
172
175
|
endpointId: inbound.endpointId,
|
|
173
176
|
...(inbound.mentioned ? { mentioned: true } : {}),
|
|
@@ -203,14 +206,14 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
203
206
|
}
|
|
204
207
|
async #acceptConnection(connection) {
|
|
205
208
|
if (!this.#started || this.#stopped) {
|
|
206
|
-
connection.
|
|
209
|
+
connection.close();
|
|
207
210
|
return;
|
|
208
211
|
}
|
|
209
|
-
if (!
|
|
210
|
-
connection.
|
|
212
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
213
|
+
connection.close(4003, 'Unauthorized');
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
|
-
const socket = connection
|
|
216
|
+
const socket = socketOfConnection(connection);
|
|
214
217
|
await this.#connectionLifecycle.stop();
|
|
215
218
|
this.#rpc.disconnect('连接已替换');
|
|
216
219
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
@@ -229,18 +232,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
229
232
|
if (this.#ws === socket)
|
|
230
233
|
this.#ws = undefined;
|
|
231
234
|
});
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
socket.ping?.();
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
/* ignore */
|
|
238
|
-
}
|
|
239
|
-
}, this.#options.config.heartbeat_interval);
|
|
240
|
-
socket.on('pong', () => {
|
|
241
|
-
if (this.#ws === socket)
|
|
242
|
-
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
243
|
-
});
|
|
235
|
+
// Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
|
|
244
236
|
socket.on('message', (data) => {
|
|
245
237
|
if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed')
|
|
246
238
|
return;
|
|
@@ -267,7 +259,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
|
|
|
267
259
|
this.#logger.debug(formatCompact({
|
|
268
260
|
endpoint: this.#options.config.id,
|
|
269
261
|
mode: 'wss',
|
|
270
|
-
peer: connection.
|
|
262
|
+
peer: connection.remoteAddress,
|
|
271
263
|
}));
|
|
272
264
|
}
|
|
273
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-onebot11",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.27",
|
|
4
4
|
"description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"schema.json",
|
|
20
20
|
"src",
|
|
21
21
|
"lib",
|
|
22
|
-
"tools",
|
|
23
22
|
"agents",
|
|
24
23
|
"README.md",
|
|
25
24
|
"CHANGELOG.md",
|
|
@@ -38,42 +37,41 @@
|
|
|
38
37
|
"license": "MIT",
|
|
39
38
|
"repository": {
|
|
40
39
|
"type": "git",
|
|
41
|
-
"url": "https://github.com/zhinjs/zhin",
|
|
40
|
+
"url": "git+https://github.com/zhinjs/zhin.git",
|
|
42
41
|
"directory": "plugins/adapters/onebot11"
|
|
43
42
|
},
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@imhelper/onebot-v11": "1.0.7",
|
|
46
45
|
"imhelper": "1.0.7",
|
|
47
46
|
"ws": "^8.21.1",
|
|
48
|
-
"
|
|
49
|
-
"@zhin.js/adapter": "1.1.
|
|
50
|
-
"@zhin.js/
|
|
51
|
-
"@zhin.js/
|
|
52
|
-
"@zhin.js/
|
|
53
|
-
"@zhin.js/
|
|
54
|
-
"@zhin.js/
|
|
55
|
-
"@zhin.js/
|
|
56
|
-
"@zhin.js/
|
|
57
|
-
"@zhin.js/
|
|
58
|
-
"@zhin.js/
|
|
59
|
-
"@zhin.js/
|
|
60
|
-
"@zhin.js/
|
|
47
|
+
"zod": "^4.6.7",
|
|
48
|
+
"@zhin.js/adapter": "1.1.33",
|
|
49
|
+
"@zhin.js/adapter-onebot-events": "0.0.6",
|
|
50
|
+
"@zhin.js/agent-feature": "1.1.17",
|
|
51
|
+
"@zhin.js/console-contract": "1.1.6",
|
|
52
|
+
"@zhin.js/core": "1.2.0",
|
|
53
|
+
"@zhin.js/feature-kit": "1.1.16",
|
|
54
|
+
"@zhin.js/host-http": "1.2.0",
|
|
55
|
+
"@zhin.js/im-contract": "1.1.14",
|
|
56
|
+
"@zhin.js/logger": "1.1.3",
|
|
57
|
+
"@zhin.js/page": "1.1.18",
|
|
58
|
+
"@zhin.js/permission": "1.2.0",
|
|
59
|
+
"@zhin.js/skill": "1.1.17",
|
|
60
|
+
"@zhin.js/tool": "1.1.19"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^26.1.2",
|
|
64
64
|
"@types/ws": "^8.18.1",
|
|
65
65
|
"typescript": "^6.0.3",
|
|
66
66
|
"vitest": "^4.1.10",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"zhin.js": "1.1.25"
|
|
67
|
+
"@zhin.js/host-http": "1.2.0",
|
|
68
|
+
"zhin.js": "1.1.26"
|
|
70
69
|
},
|
|
71
70
|
"peerDependencies": {
|
|
72
|
-
"
|
|
73
|
-
"@zhin.js/
|
|
74
|
-
"@zhin.js/
|
|
75
|
-
"
|
|
76
|
-
"zhin.js": "^1.1.25"
|
|
71
|
+
"@zhin.js/adapter": "^1.1.33",
|
|
72
|
+
"@zhin.js/command": "^1.2.0",
|
|
73
|
+
"@zhin.js/core": "^1.2.0",
|
|
74
|
+
"zhin.js": "^1.1.26"
|
|
77
75
|
},
|
|
78
76
|
"peerDependenciesMeta": {
|
|
79
77
|
"@zhin.js/command": {
|
|
@@ -81,9 +79,6 @@
|
|
|
81
79
|
},
|
|
82
80
|
"zhin.js": {
|
|
83
81
|
"optional": true
|
|
84
|
-
},
|
|
85
|
-
"zod": {
|
|
86
|
-
"optional": true
|
|
87
82
|
}
|
|
88
83
|
},
|
|
89
84
|
"author": {
|
|
@@ -135,6 +130,7 @@
|
|
|
135
130
|
"scripts": {
|
|
136
131
|
"build": "tsc",
|
|
137
132
|
"clean": "rimraf lib",
|
|
138
|
-
"test": "
|
|
133
|
+
"test": "pnpm run test:types && vitest run --root ../../.. plugins/adapters/onebot11/tests",
|
|
134
|
+
"test:types": "tsc -p tsconfig.type-tests.json"
|
|
139
135
|
}
|
|
140
136
|
}
|
package/plugin.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { definePlugin } from 'zhin.js';
|
|
3
|
-
import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
|
|
4
3
|
import { createOnebot11ConsoleRpc } from "./lib/console.js";
|
|
5
4
|
export default definePlugin({
|
|
6
5
|
name: 'onebot11',
|
|
@@ -9,12 +8,8 @@ export default definePlugin({
|
|
|
9
8
|
displayName: 'OneBot 11 Adapter',
|
|
10
9
|
},
|
|
11
10
|
setup(context) {
|
|
12
|
-
const dispose = context.resources.has(permissionHostToken)
|
|
13
|
-
? context.resources.use(permissionHostToken).registerPlatform('onebot11', createSceneRolePlatformChecker())
|
|
14
|
-
: undefined;
|
|
15
11
|
return {
|
|
16
12
|
consoleRpc: createOnebot11ConsoleRpc(context.resources, () => context.config.get()),
|
|
17
|
-
...(dispose ? { dispose } : {}),
|
|
18
13
|
};
|
|
19
14
|
},
|
|
20
15
|
});
|
package/schema.json
CHANGED
|
@@ -20,34 +20,6 @@
|
|
|
20
20
|
"type": "number",
|
|
21
21
|
"default": 30000
|
|
22
22
|
},
|
|
23
|
-
"master": {
|
|
24
|
-
"type": [
|
|
25
|
-
"string",
|
|
26
|
-
"number"
|
|
27
|
-
],
|
|
28
|
-
"description": "框架 master(platform user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
|
|
29
|
-
"x-i18n": {
|
|
30
|
-
"en": {
|
|
31
|
-
"description": "Framework masters (platform user id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"trusted": {
|
|
36
|
-
"type": "array",
|
|
37
|
-
"items": {
|
|
38
|
-
"type": [
|
|
39
|
-
"string",
|
|
40
|
-
"number"
|
|
41
|
-
],
|
|
42
|
-
"description": "Trusted platform user id"
|
|
43
|
-
},
|
|
44
|
-
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
|
|
45
|
-
"x-i18n": {
|
|
46
|
-
"en": {
|
|
47
|
-
"description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
23
|
"endpoints": {
|
|
52
24
|
"type": "array",
|
|
53
25
|
"minItems": 1,
|
|
@@ -56,34 +28,6 @@
|
|
|
56
28
|
"type": "object",
|
|
57
29
|
"additionalProperties": false,
|
|
58
30
|
"properties": {
|
|
59
|
-
"master": {
|
|
60
|
-
"type": [
|
|
61
|
-
"string",
|
|
62
|
-
"number"
|
|
63
|
-
],
|
|
64
|
-
"description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master",
|
|
65
|
-
"x-i18n": {
|
|
66
|
-
"en": {
|
|
67
|
-
"description": "Framework masters for this endpoint (platform user id), overriding the top-level master list."
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
"trusted": {
|
|
72
|
-
"type": "array",
|
|
73
|
-
"items": {
|
|
74
|
-
"type": [
|
|
75
|
-
"string",
|
|
76
|
-
"number"
|
|
77
|
-
],
|
|
78
|
-
"description": "Trusted platform user id"
|
|
79
|
-
},
|
|
80
|
-
"description": "本 endpoint 的 trusted 列表",
|
|
81
|
-
"x-i18n": {
|
|
82
|
-
"en": {
|
|
83
|
-
"description": "Trusted users for this endpoint."
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
31
|
"connection": {
|
|
88
32
|
"type": "string",
|
|
89
33
|
"enum": [
|
|
@@ -15,16 +15,49 @@ import {
|
|
|
15
15
|
type EndpointGroupRequest,
|
|
16
16
|
type PendingRpcRequest,
|
|
17
17
|
} from 'zhin.js/adapter';
|
|
18
|
+
import { standingFromMemberRole, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
|
|
18
19
|
|
|
19
20
|
/** 管理面只依赖 endpoint 的 callApi(ws/wss 两种传输各自实现)。 */
|
|
20
21
|
export interface OneBot11ManagementCaller {
|
|
21
22
|
callApi(request: PendingRpcRequest): Promise<unknown>;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
|
|
26
|
+
const SELF_ID_TTL_MS = 5_000;
|
|
27
|
+
|
|
24
28
|
export function createOneBot11EndpointManagement(
|
|
25
29
|
endpoint: OneBot11ManagementCaller,
|
|
26
30
|
): EndpointManagement {
|
|
31
|
+
// The login account can change across reconnects (or a different account on the same
|
|
32
|
+
// connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
|
|
33
|
+
// to coalesce bursts, and never across a failed lookup.
|
|
34
|
+
let login: { readonly id: string; readonly at: number } | undefined;
|
|
35
|
+
const loginId = async (signal: AbortSignal | undefined): Promise<string | undefined> => {
|
|
36
|
+
if (login && Date.now() - login.at < SELF_ID_TTL_MS) return login.id;
|
|
37
|
+
login = undefined;
|
|
38
|
+
const info = asRecord(await endpoint.callApi({ action: 'get_login_info', signal }));
|
|
39
|
+
const id = info.user_id;
|
|
40
|
+
const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
|
|
41
|
+
if (value) login = { id: value, at: Date.now() };
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
27
44
|
return Object.freeze<EndpointManagement>({
|
|
45
|
+
/** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
|
|
46
|
+
async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
|
|
47
|
+
if (conversation.kind !== 'group') return undefined;
|
|
48
|
+
const selfId = await loginId(signal);
|
|
49
|
+
if (!selfId) return undefined;
|
|
50
|
+
let member: Record<string, unknown>;
|
|
51
|
+
try {
|
|
52
|
+
member = asRecord(await endpoint.callApi({ action: 'get_group_member_info', signal, params: {
|
|
53
|
+
group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
|
|
54
|
+
} }));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
login = undefined;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return standingFromMemberRole(member.role);
|
|
60
|
+
},
|
|
28
61
|
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
29
62
|
const data = await endpoint.callApi({ action: 'get_friend_list', signal: request.signal });
|
|
30
63
|
return toArray(data).map((value) => {
|
package/src/protocol.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
* Spec: https://github.com/botuniverse/onebot-11
|
|
6
6
|
*/
|
|
7
|
-
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
|
|
8
8
|
import { toCanonicalSegments, type Segment } from '@zhin.js/core';
|
|
9
9
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
10
|
|
|
@@ -218,6 +218,16 @@ export function senderUserId(ev: OneBot11Event): string {
|
|
|
218
218
|
return ev.user_id != null ? String(ev.user_id) : '';
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
223
|
+
* 私聊不上报;未知角色词不上报。OneBot v11 没有平台原生角色与权限位。
|
|
224
|
+
*/
|
|
225
|
+
export function onebot11SenderStanding(ev: OneBot11Event): PlatformStanding | undefined {
|
|
226
|
+
if (ev.message_type !== 'group') return undefined;
|
|
227
|
+
const role = ev.sender?.role;
|
|
228
|
+
return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
/** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
|
|
222
232
|
export function senderNickname(ev: OneBot11Event): string | undefined {
|
|
223
233
|
const name = ev.sender?.card || ev.sender?.nickname;
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
isMessageEvent,
|
|
30
30
|
onebot11InboundConversation,
|
|
31
31
|
senderDisplayName,
|
|
32
|
+
onebot11SenderStanding,
|
|
32
33
|
senderUserId,
|
|
33
34
|
type OneBot11Event,
|
|
34
35
|
type OneBot11WsConfig,
|
|
@@ -192,6 +193,7 @@ export class OneBot11WsEndpoint extends Onebot11Client implements Endpoint {
|
|
|
192
193
|
const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
193
194
|
const content = formatInboundContent(ev);
|
|
194
195
|
const inbound = formatInboundMetadata(ev, this.#options.config.id);
|
|
196
|
+
const platform = onebot11SenderStanding(ev);
|
|
195
197
|
void this[endpointSpec].events.message({
|
|
196
198
|
event: ev,
|
|
197
199
|
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
@@ -201,7 +203,7 @@ export class OneBot11WsEndpoint extends Onebot11Client implements Endpoint {
|
|
|
201
203
|
sender: {
|
|
202
204
|
id: senderUserId(ev),
|
|
203
205
|
name: senderDisplayName(ev) || undefined,
|
|
204
|
-
...(
|
|
206
|
+
...(platform ? { platform } : {}),
|
|
205
207
|
},
|
|
206
208
|
endpointId: inbound.endpointId,
|
|
207
209
|
...(inbound.mentioned ? { mentioned: true } : {}),
|
package/src/ws-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WsConnection } from '@zhin.js/host-http';
|
|
2
|
+
|
|
1
3
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
4
|
export interface OneBot11WsSocket {
|
|
3
5
|
readonly readyState: number;
|
|
@@ -13,3 +15,34 @@ export interface OneBot11WsCreateOptions {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const WS_OPEN = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A Host reverse-WebSocket connection seen through the OneBot11WsSocket surface. Protocol
|
|
21
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
22
|
+
*/
|
|
23
|
+
export function socketOfConnection(connection: WsConnection): OneBot11WsSocket {
|
|
24
|
+
let readyState = 1;
|
|
25
|
+
connection.onClose(() => { readyState = 3; });
|
|
26
|
+
const subscriptions = new Map<(...args: unknown[]) => void, () => void>();
|
|
27
|
+
const off = (_event: string, listener: (...args: unknown[]) => void): void => {
|
|
28
|
+
subscriptions.get(listener)?.();
|
|
29
|
+
subscriptions.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
get readyState() { return readyState; },
|
|
33
|
+
send: (data: string) => connection.send(data),
|
|
34
|
+
close: (code?: number, reason?: string) => connection.close(code, reason),
|
|
35
|
+
on(event: string, listener: (...args: unknown[]) => void) {
|
|
36
|
+
if (event === 'message') {
|
|
37
|
+
subscriptions.set(listener, connection.onMessage((data) => {
|
|
38
|
+
listener(typeof data === 'string' ? data : Buffer.from(data));
|
|
39
|
+
}));
|
|
40
|
+
} else if (event === 'close') {
|
|
41
|
+
subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
|
|
42
|
+
}
|
|
43
|
+
// 'error' surfaces as a close on a WsConnection; 'open' already happened.
|
|
44
|
+
},
|
|
45
|
+
off,
|
|
46
|
+
removeListener: off,
|
|
47
|
+
} as OneBot11WsSocket;
|
|
48
|
+
}
|
package/src/wss-auth.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { verifyProtocolAccessToken, type ProtocolAuthRequest } from '@zhin.js/host-http';
|
|
2
2
|
|
|
3
3
|
export function verifyOneBotAccessToken(
|
|
4
4
|
accessToken: string | undefined,
|
|
5
|
-
request:
|
|
5
|
+
request: ProtocolAuthRequest,
|
|
6
6
|
): boolean {
|
|
7
|
+
// Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
|
|
7
8
|
if (!accessToken) return true;
|
|
8
|
-
|
|
9
|
-
if (auth === `Bearer ${accessToken}`) return true;
|
|
10
|
-
try {
|
|
11
|
-
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
12
|
-
if (url.searchParams.get('access_token') === accessToken) return true;
|
|
13
|
-
} catch {
|
|
14
|
-
/* ignore */
|
|
15
|
-
}
|
|
16
|
-
return false;
|
|
9
|
+
return verifyProtocolAccessToken(accessToken, request);
|
|
17
10
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type PendingRpcRequest,
|
|
16
16
|
type EndpointTransportState,
|
|
17
17
|
} from 'zhin.js/adapter';
|
|
18
|
-
import type
|
|
18
|
+
import { verifyProtocolAccessToken, type HttpHost, type WsConnection } from '@zhin.js/host-http';
|
|
19
19
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
20
20
|
import type { CapabilityId } from 'zhin.js';
|
|
21
21
|
import { createOneBot11EndpointManagement } from './endpoint-management.js';
|
|
@@ -27,17 +27,15 @@ import {
|
|
|
27
27
|
isMessageEvent,
|
|
28
28
|
onebot11InboundConversation,
|
|
29
29
|
senderDisplayName,
|
|
30
|
+
onebot11SenderStanding,
|
|
30
31
|
senderUserId,
|
|
31
32
|
type OneBot11Event,
|
|
32
33
|
type OneBot11WssConfig,
|
|
33
34
|
} from './protocol.js';
|
|
34
35
|
import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
|
|
35
36
|
import { createOneBot11ContentPort } from './content-port.js';
|
|
36
|
-
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
37
37
|
import { createOneBot11Rpc, type OneBot11Rpc } from './ws-transport.js';
|
|
38
|
-
import {
|
|
39
|
-
type OneBot11WsSocket,
|
|
40
|
-
} from './ws-types.js';
|
|
38
|
+
import { socketOfConnection, type OneBot11WsSocket } from './ws-types.js';
|
|
41
39
|
import { probeOnebot11SelfId, resolveOnebot11SelfId } from './self-id.js';
|
|
42
40
|
import { callOnebot11Client, readOnebot11Response, onebot11ClientOptions, forwardOnebot11ClientEvents, Onebot11Client } from './client.js';
|
|
43
41
|
|
|
@@ -117,15 +115,16 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
117
115
|
if (this.#started) return;
|
|
118
116
|
this.#stopped = false;
|
|
119
117
|
if (!this.#options.config.access_token) {
|
|
120
|
-
//
|
|
118
|
+
// 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
|
|
121
119
|
this.#logger.warn(formatCompact({
|
|
122
120
|
endpoint: this.#options.config.id,
|
|
123
121
|
mode: 'wss',
|
|
124
122
|
ok: false,
|
|
125
|
-
error: 'missing access_token',
|
|
123
|
+
error: 'missing access_token: only loopback peers are accepted',
|
|
126
124
|
}));
|
|
127
125
|
}
|
|
128
|
-
|
|
126
|
+
// The peer authenticates with this endpoint's own access_token, not a Console credential.
|
|
127
|
+
const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
|
|
129
128
|
this.#wsRelease = handle.onConnection((connection) => {
|
|
130
129
|
this.#connectionTask = this.#connectionTask
|
|
131
130
|
.then(() => this.#acceptConnection(connection))
|
|
@@ -194,6 +193,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
194
193
|
}
|
|
195
194
|
const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
196
195
|
const inbound = formatInboundMetadata(ev, this.#options.config.id);
|
|
196
|
+
const platform = onebot11SenderStanding(ev);
|
|
197
197
|
void this[endpointSpec].events.message({
|
|
198
198
|
event: ev,
|
|
199
199
|
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
@@ -203,7 +203,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
203
203
|
sender: {
|
|
204
204
|
id: senderUserId(ev),
|
|
205
205
|
name: senderDisplayName(ev) || undefined,
|
|
206
|
-
...(
|
|
206
|
+
...(platform ? { platform } : {}),
|
|
207
207
|
},
|
|
208
208
|
endpointId: inbound.endpointId,
|
|
209
209
|
...(inbound.mentioned ? { mentioned: true } : {}),
|
|
@@ -240,14 +240,14 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
240
240
|
|
|
241
241
|
async #acceptConnection(connection: WsConnection): Promise<void> {
|
|
242
242
|
if (!this.#started || this.#stopped) {
|
|
243
|
-
connection.
|
|
243
|
+
connection.close();
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
-
if (!
|
|
247
|
-
connection.
|
|
246
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
247
|
+
connection.close(4003, 'Unauthorized');
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
|
-
const socket = connection
|
|
250
|
+
const socket = socketOfConnection(connection);
|
|
251
251
|
await this.#connectionLifecycle.stop();
|
|
252
252
|
this.#rpc.disconnect('连接已替换');
|
|
253
253
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
@@ -264,16 +264,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
264
264
|
}
|
|
265
265
|
if (this.#ws === socket) this.#ws = undefined;
|
|
266
266
|
});
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
socket.ping?.();
|
|
270
|
-
} catch {
|
|
271
|
-
/* ignore */
|
|
272
|
-
}
|
|
273
|
-
}, this.#options.config.heartbeat_interval);
|
|
274
|
-
socket.on('pong', () => {
|
|
275
|
-
if (this.#ws === socket) this.#connectionLifecycle.notifyHeartbeatAck();
|
|
276
|
-
});
|
|
267
|
+
// Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
|
|
277
268
|
socket.on('message', (data) => {
|
|
278
269
|
if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed') return;
|
|
279
270
|
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
@@ -296,7 +287,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
|
|
|
296
287
|
this.#logger.debug(formatCompact({
|
|
297
288
|
endpoint: this.#options.config.id,
|
|
298
289
|
mode: 'wss',
|
|
299
|
-
peer: connection.
|
|
290
|
+
peer: connection.remoteAddress,
|
|
300
291
|
}));
|
|
301
292
|
}
|
|
302
293
|
}
|