@zhin.js/adapter-onebot12 4.0.0 → 4.0.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/CHANGELOG.md +66 -0
- package/adapters/onebot12.js +54 -0
- package/adapters/onebot12.ts +12 -0
- package/commands/endpoint/add/[name:string].js +3 -0
- package/commands/endpoint/add/[name:string].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[name:string].js +3 -0
- package/commands/endpoint/remove/[name:string].ts +3 -0
- package/lib/endpoint-management.d.ts +16 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/onebot12-endpoint-commands.d.ts +1 -0
- package/lib/onebot12-endpoint-commands.js +25 -0
- package/lib/onebot12-runtime-state.d.ts +1 -0
- package/lib/onebot12-runtime-state.js +6 -0
- package/lib/protocol.d.ts +21 -1
- package/lib/protocol.js +161 -2
- package/lib/webhook.d.ts +4 -1
- package/lib/webhook.js +30 -8
- package/lib/ws-endpoint.d.ts +4 -1
- package/lib/ws-endpoint.js +65 -65
- package/lib/wss-endpoint.d.ts +4 -1
- package/lib/wss-endpoint.js +24 -2
- package/package.json +18 -12
- package/plugin.js +14 -0
- package/src/endpoint-management.ts +73 -0
- package/src/index.ts +4 -0
- package/src/onebot12-endpoint-commands.ts +24 -0
- package/src/onebot12-runtime-state.ts +7 -0
- package/src/protocol.ts +179 -2
- package/src/webhook.ts +40 -12
- package/src/ws-endpoint.ts +73 -65
- package/src/wss-endpoint.ts +30 -2
- package/plugin.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# @zhin.js/adapter-onebot12
|
|
2
2
|
|
|
3
|
+
## 4.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d5cd4aa: Publish Plugin Runtime entry points and convention modules as JavaScript so
|
|
8
|
+
installed npm plugins load on Node without TypeScript stripping. Workspace
|
|
9
|
+
development continues to prefer TypeScript sources for local HMR.
|
|
10
|
+
|
|
11
|
+
Remove the unconsumed legacy game hub APIs from game-kit; game navigation and
|
|
12
|
+
records now use ordinary convention commands owned by the game hub plugin.
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [d5cd4aa]
|
|
15
|
+
- @zhin.js/command@1.0.4
|
|
16
|
+
- zhin.js@5.0.2
|
|
17
|
+
- @zhin.js/adapter@1.1.2
|
|
18
|
+
- @zhin.js/core@1.4.2
|
|
19
|
+
|
|
20
|
+
## 4.0.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 5691aba: 第二轮全量审计修复批(8 面 ~60 bug):
|
|
25
|
+
|
|
26
|
+
- **安全**:email 附件路径穿越修复(basename + downloadPath 约束);lark/telegram/satori webhook 鉴权(缺密钥告警、timingSafeEqual、±5min 时效窗、chat_type 修正);onebot wss/webhook 缺 token 告警;qq webhook 改原始字节验签;renderJsx/JSX 转义注入修复;console runtime token 401 死循环。
|
|
27
|
+
- **P0 功能**:sandbox 多 endpoint 解析 + WS 路径隔离;short-url expand(undici opaqueredirect)改 follow;AI 压缩摘要失败不再静默丢历史(熔断恢复生效);console-ui 实时推送事件名归一化 + IndexedDB schema 对齐;process-monitor 热重载不再误判崩溃。
|
|
28
|
+
- **生命周期**:email IMAP 断线重连 + 在飞锁;onebot11/12 start 失败清理;line replyToken TTL + push 兜底;wechat-mp token 过期重试 + MsgId 去重;weixin-ilink buf 推进/防抖写盘/媒体 TTL/QR abort;satori PONG 看门狗;退避自毁修复。
|
|
29
|
+
- **游戏**:text-adventure 终局 restart 复活 + requires 服务端校验;tic-tac-toe PvP 占用/restart/队列清理/TTL;idiom-chain/word-riddle 闲聊不扣失误;别名中间件不劫持普通聊天。
|
|
30
|
+
- **共享库**:schema falsy 默认值/date/tuple/union 修复;database parseCondition Date/未知操作符、sqlite TEXT 往返、query 分派、belongsToMany 方言、migration dry-run;schedule DST 回拨死循环、重复 id 去重、flush 串行化;game-kit fallback 编号/onboarding 提示/尾缀边界/活引用拷贝。
|
|
31
|
+
- **渲染语音**:fetch 全部超时 + 渲染并发闸;sanitizeHtml form 保文本;STT 扩展名映射 + 删临时文件;TTS 未知 provider 报错;emojiCache LRU 负缓存/fontCache style/clearFonts 恢复;register 错误分类收窄。
|
|
32
|
+
|
|
33
|
+
- 078e3f7: 架构统一批(AURA):
|
|
34
|
+
|
|
35
|
+
- **EndpointLifecycle 基座**(@zhin.js/adapter 新增 `createEndpointLifecycle`):WS/SSE 端点的 start 失败复位、仅曾 open 才退避重连(指数+jitter 可配)、stop 不重连、PONG 看门狗、定时器集中清理、陈旧事件防叠套;napcat/milky/onebot11/onebot12/satori 已迁移(删除各自手写状态机),从此同类竞态在结构上不可能再犯。
|
|
36
|
+
- **Generation-store**(@zhin.js/plugin-runtime 新增 `createGenerationStore`):模块级运行时状态的一等能力,provide 自动挂 lifecycle 反注册(代际结束自动清理);lottery deps 与 rss db 已迁移,公开 API 兼容。
|
|
37
|
+
- **Resolver 管线收敛**(@zhin.js/runtime):解析规则统一为 local path → workspace → node_modules 单管线;optional 引用对所有 PackageResolutionError 容错(消除 message 前缀补丁)。
|
|
38
|
+
- **工具目录准入统一**(@zhin.js/agent):RegisteredToolSource 与 ExternalToolSource 共用同一 `canAccessTool` 准入(platforms/scopes/permissions/hidden 四元组全链路透传),同名覆盖 warn;AgentToolRegistration 补 platforms/scopes。两条注册通道(静态约定 vs 动态注册)职责边界已文档化。
|
|
39
|
+
|
|
40
|
+
- 9c997b2: 通用 endpoint 管理命令套件:`@zhin.js/adapter` 新增 `createEndpointCommands(spec, defineCommand)`——`<adapter> endpoint list / add <name> key=value... / remove <name>` 三件套,含 kv 解析、`.env` 凭据派生(`<ADAPTER>_<NAME>_<FIELD>`)、yaml 写回保留注释、master 权限门禁(通用 `isEndpointOperator`)、自定义 bindFlow 钩子。QQ 迁移至套件(行为与扫码绑定流程不变);napcat / onebot11 / onebot12 / milky / slack / telegram 接入(字段对齐各自 schema,features 补 @zhin.js/command)。
|
|
41
|
+
- 09d4f25: Console 社交读取面(management 语义端口)多平台落地:napcat/onebot11/onebot12/milky(好友+群+群成员,OneBot 标准动作);discord/kook/satori(guild+频道+成员,分页聚合,id 保精度留字符串);slack(workspace 成员+public channels+conversations.members);line(群/room 成员分页+profile 回退);wechat-mp(followers openid);weixin-ilink(context_token 对端推导);lark(chats+members 全分页)。`EndpointFriend.user_id`/`EndpointGroup.group_id` 放宽为 `number | string`(雪花 id 不丢精度)。telegram/wecom/dingtalk/github/email/sandbox 注明平台无列表面暂不接。
|
|
42
|
+
- 6cb6152: 统一消息元素通道(UNI-Channel)落地:
|
|
43
|
+
|
|
44
|
+
- **入站契约**:`IncomingMessage.segments`(canonical Segment[],与 content 纯文本视图同源双轨),Message 透传;AI 兜底链路经 `collectSegmentMedia` 把图片/语音/视频/文件 MediaRef 写入会话 extra——多模态输入不再丢失。
|
|
45
|
+
- **出站协商**:`normalizeOutboundPayload` 升级全量 canonical 归一(复用 generic-segment-mapper),html→image 按端点 `segments.outboundMedia` 声明降级(base64 直发 / url-or-text / passthrough 自行物化);`MediaRef.kind` 新增 `'file'` 承载平台不透明引用(file_id/resource_id)。
|
|
46
|
+
- **能力声明**:`defineAdapter.segments` policy(outboundMedia / interactive),三道段门禁复活(探测点改 adapters/\*.ts,豁免名单渐进收敛)。
|
|
47
|
+
- **首批迁移**:icqq 全保真出入站(CQ ↔ canonical,quote→reply 段);milky/telegram/discord 入站媒体段恢复(附件/贴纸/callback action);napcat/onebot11/onebot12 出站 canonical→OneBot 数组段;wechat-mp/wecom `/cgi-bin/media/upload` 与 lark `/im/v1/images` 上传通路(base64/URL 图片不再静默丢图,失败降级文本)。
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [cdf64e7]
|
|
50
|
+
- Updated dependencies [2d0a159]
|
|
51
|
+
- Updated dependencies [5691aba]
|
|
52
|
+
- Updated dependencies [078e3f7]
|
|
53
|
+
- Updated dependencies [50497a5]
|
|
54
|
+
- Updated dependencies [9c997b2]
|
|
55
|
+
- Updated dependencies [09d4f25]
|
|
56
|
+
- Updated dependencies [43485a9]
|
|
57
|
+
- Updated dependencies [f0ec5ab]
|
|
58
|
+
- Updated dependencies [3e925d0]
|
|
59
|
+
- Updated dependencies [fa66c4c]
|
|
60
|
+
- Updated dependencies [fa66c4c]
|
|
61
|
+
- Updated dependencies [6cb6152]
|
|
62
|
+
- @zhin.js/command@1.0.3
|
|
63
|
+
- @zhin.js/plugin-runtime@1.1.1
|
|
64
|
+
- @zhin.js/host-http@1.0.3
|
|
65
|
+
- zhin.js@5.0.1
|
|
66
|
+
- @zhin.js/adapter@1.1.1
|
|
67
|
+
- @zhin.js/core@1.4.1
|
|
68
|
+
|
|
3
69
|
## 4.0.0
|
|
4
70
|
|
|
5
71
|
### Patch Changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* Convention entry: discover `adapters/onebot12.ts` → defineAdapter.
|
|
4
|
+
*/
|
|
5
|
+
import { defineAdapter } from '@zhin.js/adapter';
|
|
6
|
+
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
7
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
|
+
import { OneBot12WebhookEndpoint } from "../lib/webhook.js";
|
|
9
|
+
import { OneBot12WsEndpoint } from "../lib/ws-endpoint.js";
|
|
10
|
+
import { OneBot12WssEndpoint } from "../lib/wss-endpoint.js";
|
|
11
|
+
import { resolveOneBot12Config, } from "../lib/protocol.js";
|
|
12
|
+
import { onebot12RuntimeStateToken } from "../lib/onebot12-runtime-state.js";
|
|
13
|
+
export { OneBot12WebhookEndpoint } from "../lib/webhook.js";
|
|
14
|
+
export { OneBot12WsEndpoint } from "../lib/ws-endpoint.js";
|
|
15
|
+
export { OneBot12WssEndpoint } from "../lib/wss-endpoint.js";
|
|
16
|
+
export default defineAdapter({
|
|
17
|
+
capabilities: ['inbound', 'outbound'],
|
|
18
|
+
// 媒体段经 upload_file 物化为 file_id(spec 正式投递);上传失败降级扩展字段透传。
|
|
19
|
+
// 无卡片交互面,交互段降级纯文本。
|
|
20
|
+
segments: {
|
|
21
|
+
outboundMedia: ['url', 'path', 'base64', 'upload'],
|
|
22
|
+
interactive: 'text',
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const config = resolveOneBot12Config(context.config);
|
|
26
|
+
const gateway = context.use(messageGatewayToken);
|
|
27
|
+
// 注册到插件运行时状态(onebot12 endpoint list 的"运行中"数据源)
|
|
28
|
+
context.use(onebot12RuntimeStateToken).endpoints.set(config.name, {
|
|
29
|
+
name: config.name,
|
|
30
|
+
mode: config.connection,
|
|
31
|
+
});
|
|
32
|
+
if (config.connection === 'webhook') {
|
|
33
|
+
return new OneBot12WebhookEndpoint({
|
|
34
|
+
id: context.id,
|
|
35
|
+
gateway,
|
|
36
|
+
http: context.use(httpHostToken),
|
|
37
|
+
config,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (config.connection === 'wss') {
|
|
41
|
+
return new OneBot12WssEndpoint({
|
|
42
|
+
id: context.id,
|
|
43
|
+
gateway,
|
|
44
|
+
http: context.use(httpHostToken),
|
|
45
|
+
config,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return new OneBot12WsEndpoint({
|
|
49
|
+
id: context.id,
|
|
50
|
+
gateway,
|
|
51
|
+
config,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
package/adapters/onebot12.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveOneBot12Config,
|
|
12
12
|
type OneBot12AdapterConfig,
|
|
13
13
|
} from '../src/protocol.js';
|
|
14
|
+
import { onebot12RuntimeStateToken } from '../src/onebot12-runtime-state.js';
|
|
14
15
|
|
|
15
16
|
export { OneBot12WebhookEndpoint } from '../src/webhook.js';
|
|
16
17
|
export type { OneBot12WebhookEndpointOptions } from '../src/webhook.js';
|
|
@@ -22,9 +23,20 @@ export type { OneBot12WsSocket, OneBot12WsCreateOptions } from '../src/ws-types.
|
|
|
22
23
|
|
|
23
24
|
export default defineAdapter<OneBot12AdapterConfig>({
|
|
24
25
|
capabilities: ['inbound', 'outbound'],
|
|
26
|
+
// 媒体段经 upload_file 物化为 file_id(spec 正式投递);上传失败降级扩展字段透传。
|
|
27
|
+
// 无卡片交互面,交互段降级纯文本。
|
|
28
|
+
segments: {
|
|
29
|
+
outboundMedia: ['url', 'path', 'base64', 'upload'],
|
|
30
|
+
interactive: 'text',
|
|
31
|
+
},
|
|
25
32
|
create(context) {
|
|
26
33
|
const config = resolveOneBot12Config(context.config);
|
|
27
34
|
const gateway = context.use(messageGatewayToken);
|
|
35
|
+
// 注册到插件运行时状态(onebot12 endpoint list 的"运行中"数据源)
|
|
36
|
+
context.use(onebot12RuntimeStateToken).endpoints.set(config.name, {
|
|
37
|
+
name: config.name,
|
|
38
|
+
mode: config.connection,
|
|
39
|
+
});
|
|
28
40
|
if (config.connection === 'webhook') {
|
|
29
41
|
return new OneBot12WebhookEndpoint({
|
|
30
42
|
id: context.id,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 endpoint management 语义端口(Console 社交面 RPC 消费,见
|
|
3
|
+
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
|
+
*
|
|
5
|
+
* 动作名对齐 OB12 规范:get_friend_list / get_group_list / get_group_member_list。
|
|
6
|
+
* 归一化取舍(OB12 id 为字符串,收敛为数字):
|
|
7
|
+
* - friend → {user_id:number, nickname: user_name, remark: user_displayname ?? ''}
|
|
8
|
+
* - group → {group_id:number, name: group_name ?? name}
|
|
9
|
+
* - 群成员列表保持 OB12 原生形状,仅保证数组
|
|
10
|
+
*/
|
|
11
|
+
import type { EndpointManagement } from '@zhin.js/adapter';
|
|
12
|
+
/** 管理面只依赖 endpoint 的 callApi(ws/wss/webhook 三种传输各自实现)。 */
|
|
13
|
+
export interface OneBot12ManagementCaller {
|
|
14
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createOneBot12EndpointManagement(endpoint: OneBot12ManagementCaller): EndpointManagement;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createOneBot12EndpointManagement(endpoint) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
async listFriends() {
|
|
4
|
+
const data = await endpoint.callApi('get_friend_list');
|
|
5
|
+
return toArray(data).map((value) => {
|
|
6
|
+
const friend = asRecord(value);
|
|
7
|
+
return {
|
|
8
|
+
user_id: toNumberId(friend.user_id, 'user_id'),
|
|
9
|
+
nickname: String(friend.user_name ?? friend.nickname ?? ''),
|
|
10
|
+
remark: String(friend.user_displayname ?? friend.remark ?? ''),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
async listGroups() {
|
|
15
|
+
const data = await endpoint.callApi('get_group_list');
|
|
16
|
+
return toArray(data).map((value) => {
|
|
17
|
+
const group = asRecord(value);
|
|
18
|
+
return {
|
|
19
|
+
group_id: toNumberId(group.group_id, 'group_id'),
|
|
20
|
+
name: String(group.group_name ?? group.name ?? ''),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
async listGroupMembers(groupId) {
|
|
25
|
+
const data = await endpoint.callApi('get_group_member_list', {
|
|
26
|
+
group_id: groupId,
|
|
27
|
+
});
|
|
28
|
+
return toArray(data);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function asRecord(value) {
|
|
33
|
+
return value !== null && typeof value === 'object'
|
|
34
|
+
? value
|
|
35
|
+
: {};
|
|
36
|
+
}
|
|
37
|
+
function toArray(value) {
|
|
38
|
+
return Array.isArray(value) ? value : [];
|
|
39
|
+
}
|
|
40
|
+
/** OB12 id 是字符串;QQ 系实现均为数字串,统一收敛为数字。 */
|
|
41
|
+
function toNumberId(value, label) {
|
|
42
|
+
const n = Number(value);
|
|
43
|
+
if (!Number.isFinite(n) || String(value ?? '').trim() === '') {
|
|
44
|
+
throw new TypeError(`onebot12 ${label} 必须是数字: ${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, parseSendTarget, resolveOneBot12Config, senderNickname, senderUserId, type OneBot12ActionRequest, type OneBot12ActionResponse, type OneBot12AdapterConfig, type OneBot12ConfigBase, type OneBot12EndpointConfig, type OneBot12Event, type OneBot12HttpOptions, type OneBot12Segment, type OneBot12Self, type OneBot12WebhookConfig, type OneBot12WireSegment, type OneBot12WsConfig, type OneBot12WssConfig, type ParsedSendTarget, type ResolvedOneBot12Config, } from './protocol.js';
|
|
1
|
+
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, mediaRefToOneBot12Fields, mediaRefToOneBot12UploadParams, parseSendTarget, resolveOneBot12Config, senderNickname, senderUserId, uploadOneBot12MediaSegments, type OneBot12ActionRequest, type OneBot12ActionResponse, type OneBot12AdapterConfig, type OneBot12CallAction, type OneBot12ConfigBase, type OneBot12EndpointConfig, type OneBot12Event, type OneBot12HttpOptions, type OneBot12Segment, type OneBot12Self, type OneBot12WebhookConfig, type OneBot12WireSegment, type OneBot12WsConfig, type OneBot12WssConfig, type ParsedSendTarget, type ResolvedOneBot12Config, } from './protocol.js';
|
|
2
2
|
export { OneBot12WebhookEndpoint, type OneBot12WebhookEndpointOptions, } from './webhook.js';
|
|
3
3
|
export { OneBot12WsEndpoint, type OneBot12WsEndpointOptions, } from './ws-endpoint.js';
|
|
4
4
|
export { OneBot12WssEndpoint, type OneBot12WssEndpointOptions, } from './wss-endpoint.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, parseSendTarget, resolveOneBot12Config, senderNickname, senderUserId, } from './protocol.js';
|
|
1
|
+
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, mediaRefToOneBot12Fields, mediaRefToOneBot12UploadParams, parseSendTarget, resolveOneBot12Config, senderNickname, senderUserId, uploadOneBot12MediaSegments, } from './protocol.js';
|
|
2
2
|
export { OneBot12WebhookEndpoint, } from './webhook.js';
|
|
3
3
|
export { OneBot12WsEndpoint, } from './ws-endpoint.js';
|
|
4
4
|
export { OneBot12WssEndpoint, } from './wss-endpoint.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const onebot12EndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `onebot12 endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*/
|
|
5
|
+
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
6
|
+
import { defineCommand } from '@zhin.js/command';
|
|
7
|
+
import { onebot12RuntimeStateToken } from './onebot12-runtime-state.js';
|
|
8
|
+
export const onebot12EndpointCommands = createEndpointCommands({
|
|
9
|
+
adapterKey: 'onebot12',
|
|
10
|
+
adapterDisplayName: 'OneBot 12',
|
|
11
|
+
fields: [
|
|
12
|
+
{ key: 'url', description: 'OneBot 实现 WebSocket URL(connection: ws 必填)' },
|
|
13
|
+
{ key: 'path', description: 'HTTP/WS 路径(webhook / reverse-wss)' },
|
|
14
|
+
{ key: 'api_url', description: 'HTTP action 端点(connection: webhook 出站必填)' },
|
|
15
|
+
{ key: 'access_token', env: true, description: 'OneBot access token' },
|
|
16
|
+
],
|
|
17
|
+
running: (use) => use(onebot12RuntimeStateToken).endpoints.values(),
|
|
18
|
+
describeEntry: (entry) => {
|
|
19
|
+
if (entry.url)
|
|
20
|
+
return `url: ${String(entry.url)}`;
|
|
21
|
+
if (entry.path)
|
|
22
|
+
return `path: ${String(entry.path)}`;
|
|
23
|
+
return '';
|
|
24
|
+
},
|
|
25
|
+
}, defineCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const onebot12RuntimeStateToken: import("@zhin.js/plugin-runtime").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `onebot12 endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
export const onebot12RuntimeStateToken = defineEndpointRuntimeStateToken('onebot12');
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
* Spec: https://12.onebot.dev/
|
|
5
5
|
*/
|
|
6
|
+
import { type MediaRef } from '@zhin.js/core';
|
|
6
7
|
/** Transitional legacy endpoint row (`endpoints[]` with `context: onebot12`). */
|
|
7
8
|
export interface OneBot12LegacyEndpointRow {
|
|
8
9
|
readonly context?: string;
|
|
@@ -132,11 +133,30 @@ export declare function senderNickname(ev: OneBot12Event): string | undefined;
|
|
|
132
133
|
* 新 Plugin Runtime 的 Message.content 为纯文本,mention 信息只能经 metadata.mentioned 传递。
|
|
133
134
|
*/
|
|
134
135
|
export declare function isBotMentioned(ev: OneBot12Event): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* canonical MediaRef → OneBot 12 媒体字段(扩展字段降级形状)。
|
|
138
|
+
* spec 正式投递形状是 `file_id`(先 upload_file 物化,见
|
|
139
|
+
* {@link uploadOneBot12MediaSegments});上传失败时按常见扩展字段
|
|
140
|
+
* 降级输出:url → `url`、base64 → `data`、本地路径 → `path`。
|
|
141
|
+
*/
|
|
142
|
+
export declare function mediaRefToOneBot12Fields(media: MediaRef): Record<string, unknown>;
|
|
135
143
|
/**
|
|
136
144
|
* Wire-encode an already-rendered outbound payload into OneBot 12 message segments.
|
|
137
|
-
*
|
|
145
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
146
|
+
* 旧 wire 形状(mention / 裸 `{url,file,base64}`)保持兼容透传。
|
|
138
147
|
*/
|
|
139
148
|
export declare function formatOutboundSegments(payload: unknown): OneBot12Segment[];
|
|
149
|
+
/** 端点动作调用签名(WS echo 请求 / webhook api_url HTTP)。 */
|
|
150
|
+
export type OneBot12CallAction = (action: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
151
|
+
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
152
|
+
export declare function mediaRefToOneBot12UploadParams(segmentType: string, data: Record<string, unknown>, media: MediaRef): Record<string, unknown> | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* 出站媒体段物化:image/voice/audio/video/file 段的 MediaRef(url/base64/path)
|
|
155
|
+
* 先经 `upload_file` 换 file_id(spec 正式投递形状),再交给
|
|
156
|
+
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
157
|
+
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
158
|
+
*/
|
|
159
|
+
export declare function uploadOneBot12MediaSegments(payload: unknown, callAction: OneBot12CallAction, onUploadFailed?: (error: unknown) => void): Promise<unknown>;
|
|
140
160
|
export declare function buildSendMessageParams(target: string, message: OneBot12Segment[]): Record<string, unknown>;
|
|
141
161
|
/**
|
|
142
162
|
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
package/lib/protocol.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
* Spec: https://12.onebot.dev/
|
|
5
5
|
*/
|
|
6
|
+
import { isMediaRef, mediaRefFromLegacyData } from '@zhin.js/core';
|
|
6
7
|
export function resolveOneBot12Config(config = {}) {
|
|
7
8
|
const entry = config.endpoints?.find((item) => item.context === 'onebot12');
|
|
8
9
|
const connection = config.connection
|
|
@@ -142,9 +143,82 @@ export function isBotMentioned(ev) {
|
|
|
142
143
|
return false;
|
|
143
144
|
return ev.message.some((seg) => seg.type === 'mention' && String(seg.data?.['user_id'] ?? '') === selfId);
|
|
144
145
|
}
|
|
146
|
+
/** OneBot 12 携带媒体的段类型。 */
|
|
147
|
+
const ONEBOT12_MEDIA_TYPES = new Set(['image', 'voice', 'audio', 'video', 'file']);
|
|
148
|
+
/** 媒体段 data 里的 canonical-only / legacy 媒体字段,归一后不重复进 wire。 */
|
|
149
|
+
const MEDIA_DATA_SKIP_KEYS = new Set([
|
|
150
|
+
'media', 'alt', 'mime_type', 'url', 'file', 'base64', 'data', 'path',
|
|
151
|
+
]);
|
|
152
|
+
/**
|
|
153
|
+
* canonical MediaRef → OneBot 12 媒体字段(扩展字段降级形状)。
|
|
154
|
+
* spec 正式投递形状是 `file_id`(先 upload_file 物化,见
|
|
155
|
+
* {@link uploadOneBot12MediaSegments});上传失败时按常见扩展字段
|
|
156
|
+
* 降级输出:url → `url`、base64 → `data`、本地路径 → `path`。
|
|
157
|
+
*/
|
|
158
|
+
export function mediaRefToOneBot12Fields(media) {
|
|
159
|
+
if (media.kind === 'base64') {
|
|
160
|
+
const value = media.value.startsWith('base64://')
|
|
161
|
+
? media.value.slice('base64://'.length)
|
|
162
|
+
: media.value;
|
|
163
|
+
return { data: value };
|
|
164
|
+
}
|
|
165
|
+
if (media.kind === 'path') {
|
|
166
|
+
return { path: media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value };
|
|
167
|
+
}
|
|
168
|
+
return { url: media.value };
|
|
169
|
+
}
|
|
170
|
+
function oneBot12MediaExtra(data) {
|
|
171
|
+
const extra = {};
|
|
172
|
+
for (const [key, value] of Object.entries(data)) {
|
|
173
|
+
if (!MEDIA_DATA_SKIP_KEYS.has(key))
|
|
174
|
+
extra[key] = value;
|
|
175
|
+
}
|
|
176
|
+
return extra;
|
|
177
|
+
}
|
|
178
|
+
function oneBot12MediaSegment(type, data) {
|
|
179
|
+
// 已物化为 file_id 的段是 spec 正式形状,原样透传。
|
|
180
|
+
if (typeof data.file_id === 'string' && data.file_id)
|
|
181
|
+
return { type, data };
|
|
182
|
+
const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
|
|
183
|
+
if (!media)
|
|
184
|
+
return { type, data };
|
|
185
|
+
return { type, data: { ...oneBot12MediaExtra(data), ...mediaRefToOneBot12Fields(media) } };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* canonical Segment → OneBot 12 数组段:
|
|
189
|
+
* - mention → mention(`user_id: target`,`target: 'all'` → mention_all);
|
|
190
|
+
* - reply(`message_id`)→ reply(`message_id`);
|
|
191
|
+
* - image / voice / audio / video / file 的 MediaRef → url/data/path 扩展字段;
|
|
192
|
+
* - 其余(已是 wire 形状的段、平台扩展段)原样透传。
|
|
193
|
+
*/
|
|
194
|
+
function canonicalToOneBotSegment(segment) {
|
|
195
|
+
const data = segment.data ?? {};
|
|
196
|
+
switch (segment.type) {
|
|
197
|
+
case 'mention': {
|
|
198
|
+
const target = data.target ?? data.user_id ?? data.id;
|
|
199
|
+
if (target == null)
|
|
200
|
+
return { type: segment.type, data };
|
|
201
|
+
if (String(target) === 'all')
|
|
202
|
+
return { type: 'mention_all', data: {} };
|
|
203
|
+
return { type: 'mention', data: { user_id: String(target) } };
|
|
204
|
+
}
|
|
205
|
+
case 'reply': {
|
|
206
|
+
const messageId = data.message_id ?? data.id;
|
|
207
|
+
if (messageId == null)
|
|
208
|
+
return { type: segment.type, data };
|
|
209
|
+
return { type: 'reply', data: { message_id: String(messageId) } };
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
if (ONEBOT12_MEDIA_TYPES.has(segment.type)) {
|
|
213
|
+
return oneBot12MediaSegment(segment.type, data);
|
|
214
|
+
}
|
|
215
|
+
return { type: segment.type, data };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
145
218
|
/**
|
|
146
219
|
* Wire-encode an already-rendered outbound payload into OneBot 12 message segments.
|
|
147
|
-
*
|
|
220
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
221
|
+
* 旧 wire 形状(mention / 裸 `{url,file,base64}`)保持兼容透传。
|
|
148
222
|
*/
|
|
149
223
|
export function formatOutboundSegments(payload) {
|
|
150
224
|
if (typeof payload === 'string') {
|
|
@@ -169,10 +243,94 @@ export function formatOutboundSegments(payload) {
|
|
|
169
243
|
segs.push({ type: 'text', data: { text: item } });
|
|
170
244
|
continue;
|
|
171
245
|
}
|
|
172
|
-
segs.push(
|
|
246
|
+
segs.push(canonicalToOneBotSegment(item));
|
|
173
247
|
}
|
|
174
248
|
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
175
249
|
}
|
|
250
|
+
/** upload_file 文件名:优先段 data.name/filename,URL/路径取 basename,再按 mime 给默认名。 */
|
|
251
|
+
function oneBot12UploadName(segmentType, data, media) {
|
|
252
|
+
const named = data.name ?? data.filename;
|
|
253
|
+
if (typeof named === 'string' && named)
|
|
254
|
+
return named;
|
|
255
|
+
if (media.kind === 'url') {
|
|
256
|
+
try {
|
|
257
|
+
const base = new URL(media.value).pathname.split('/').filter(Boolean).pop();
|
|
258
|
+
if (base)
|
|
259
|
+
return base;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (media.kind === 'path') {
|
|
266
|
+
const base = media.value.split(/[\\/]/).filter(Boolean).pop();
|
|
267
|
+
if (base)
|
|
268
|
+
return base;
|
|
269
|
+
}
|
|
270
|
+
const ext = media.mime_type?.split('/')[1]?.split(';')[0];
|
|
271
|
+
return `${segmentType}.${ext || 'bin'}`;
|
|
272
|
+
}
|
|
273
|
+
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
274
|
+
export function mediaRefToOneBot12UploadParams(segmentType, data, media) {
|
|
275
|
+
const name = oneBot12UploadName(segmentType, data, media);
|
|
276
|
+
if (media.kind === 'url')
|
|
277
|
+
return { type: 'url', name, url: media.value };
|
|
278
|
+
if (media.kind === 'path') {
|
|
279
|
+
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
280
|
+
return { type: 'path', name, path };
|
|
281
|
+
}
|
|
282
|
+
if (media.kind === 'base64') {
|
|
283
|
+
const value = media.value.startsWith('base64://')
|
|
284
|
+
? media.value.slice('base64://'.length)
|
|
285
|
+
: media.value;
|
|
286
|
+
return { type: 'data', name, data: value };
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
async function uploadOneMediaSegment(item, callAction, onUploadFailed) {
|
|
291
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
292
|
+
return item;
|
|
293
|
+
const segment = item;
|
|
294
|
+
if (typeof segment.type !== 'string' || !ONEBOT12_MEDIA_TYPES.has(segment.type))
|
|
295
|
+
return item;
|
|
296
|
+
const data = segment.data ?? {};
|
|
297
|
+
if (typeof data.file_id === 'string' && data.file_id)
|
|
298
|
+
return item;
|
|
299
|
+
const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
|
|
300
|
+
if (!media)
|
|
301
|
+
return item;
|
|
302
|
+
// kind=file:平台不透明引用(OB12 file_id 复投),直接物化为 file_id。
|
|
303
|
+
if (media.kind === 'file') {
|
|
304
|
+
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: media.value } };
|
|
305
|
+
}
|
|
306
|
+
const params = mediaRefToOneBot12UploadParams(segment.type, data, media);
|
|
307
|
+
if (!params)
|
|
308
|
+
return item;
|
|
309
|
+
try {
|
|
310
|
+
const result = await callAction('upload_file', params);
|
|
311
|
+
if (result && typeof result.file_id === 'string' && result.file_id) {
|
|
312
|
+
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: result.file_id } };
|
|
313
|
+
}
|
|
314
|
+
throw new Error('upload_file 响应缺少 file_id');
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
// 上传失败降级:保留原段,由 formatOutboundSegments 走扩展字段(url/data/path)
|
|
318
|
+
onUploadFailed?.(error);
|
|
319
|
+
return item;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 出站媒体段物化:image/voice/audio/video/file 段的 MediaRef(url/base64/path)
|
|
324
|
+
* 先经 `upload_file` 换 file_id(spec 正式投递形状),再交给
|
|
325
|
+
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
326
|
+
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
327
|
+
*/
|
|
328
|
+
export async function uploadOneBot12MediaSegments(payload, callAction, onUploadFailed) {
|
|
329
|
+
if (Array.isArray(payload)) {
|
|
330
|
+
return Promise.all(payload.map((item) => uploadOneMediaSegment(item, callAction, onUploadFailed)));
|
|
331
|
+
}
|
|
332
|
+
return uploadOneMediaSegment(payload, callAction, onUploadFailed);
|
|
333
|
+
}
|
|
176
334
|
export function buildSendMessageParams(target, message) {
|
|
177
335
|
const parsed = parseSendTarget(target);
|
|
178
336
|
const params = {
|
|
@@ -208,6 +366,7 @@ export async function callOneBot12Action(options, action, params = {}, echo) {
|
|
|
208
366
|
method: 'POST',
|
|
209
367
|
headers,
|
|
210
368
|
body: JSON.stringify(body),
|
|
369
|
+
signal: AbortSignal.timeout(30_000),
|
|
211
370
|
});
|
|
212
371
|
const text = await res.text();
|
|
213
372
|
if (res.status === 401)
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
1
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
@@ -12,6 +12,7 @@ export interface OneBot12WebhookEndpointOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
14
14
|
#private;
|
|
15
|
+
readonly management: EndpointManagement;
|
|
15
16
|
constructor(options: OneBot12WebhookEndpointOptions);
|
|
16
17
|
start(): Promise<void>;
|
|
17
18
|
open(): void;
|
|
@@ -21,5 +22,7 @@ export declare class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
21
22
|
readonly target: string;
|
|
22
23
|
readonly payload: unknown;
|
|
23
24
|
}): Promise<string>;
|
|
25
|
+
/** Public API for management surface / callers;webhook 模式走 api_url。 */
|
|
26
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
24
27
|
admit(ev: OneBot12Event): void;
|
|
25
28
|
}
|
package/lib/webhook.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
-
import {
|
|
2
|
+
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
3
|
+
import { buildSendMessageParams, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, isBotMentioned, isMessageEvent, senderNickname, senderUserId, uploadOneBot12MediaSegments, } from './protocol.js';
|
|
3
4
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
4
5
|
const logger = getLogger('onebot12');
|
|
5
6
|
export class OneBot12WebhookEndpoint {
|
|
6
7
|
#options;
|
|
8
|
+
management = createOneBot12EndpointManagement(this);
|
|
7
9
|
#callAction;
|
|
8
10
|
#routeReleases = [];
|
|
9
11
|
#open = false;
|
|
@@ -16,6 +18,15 @@ export class OneBot12WebhookEndpoint {
|
|
|
16
18
|
if (this.#started)
|
|
17
19
|
return;
|
|
18
20
|
this.#started = true;
|
|
21
|
+
if (!this.#options.config.access_token) {
|
|
22
|
+
// webhook 模式未配 access_token 时任何 POST 都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
23
|
+
logger.warn(formatCompact({
|
|
24
|
+
endpoint: this.#options.config.name,
|
|
25
|
+
mode: 'webhook',
|
|
26
|
+
ok: false,
|
|
27
|
+
error: 'missing access_token',
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
19
30
|
this.#setupRoutes();
|
|
20
31
|
logger.info(formatCompact({
|
|
21
32
|
op: 'listen',
|
|
@@ -38,14 +49,16 @@ export class OneBot12WebhookEndpoint {
|
|
|
38
49
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
39
50
|
}
|
|
40
51
|
async send({ target, payload }) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const materialized = await uploadOneBot12MediaSegments(payload, (action, params) => this.callApi(action, params), (error) => {
|
|
53
|
+
logger.warn(formatCompact({
|
|
54
|
+
op: 'onebot12_upload_failed',
|
|
55
|
+
endpoint: this.#options.config.name,
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
const message = formatOutboundSegments(materialized);
|
|
46
60
|
const params = buildSendMessageParams(target, message);
|
|
47
|
-
const
|
|
48
|
-
const data = resp.data;
|
|
61
|
+
const data = await this.callApi('send_message', params);
|
|
49
62
|
const messageId = data?.message_id ?? '';
|
|
50
63
|
logger.debug(formatCompact({
|
|
51
64
|
op: 'onebot12_send',
|
|
@@ -55,6 +68,15 @@ export class OneBot12WebhookEndpoint {
|
|
|
55
68
|
}));
|
|
56
69
|
return messageId;
|
|
57
70
|
}
|
|
71
|
+
/** Public API for management surface / callers;webhook 模式走 api_url。 */
|
|
72
|
+
async callApi(action, params = {}) {
|
|
73
|
+
const apiUrl = this.#options.config.api_url;
|
|
74
|
+
if (!apiUrl) {
|
|
75
|
+
throw new Error('OneBot12 connection:webhook requires api_url for outbound api');
|
|
76
|
+
}
|
|
77
|
+
const resp = await this.#callAction({ url: apiUrl, access_token: this.#options.config.access_token }, action, params);
|
|
78
|
+
return resp.data;
|
|
79
|
+
}
|
|
58
80
|
admit(ev) {
|
|
59
81
|
if (!this.#open || !isMessageEvent(ev))
|
|
60
82
|
return;
|