@zhin.js/adapter-napcat 5.0.0 → 5.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 +68 -0
- package/adapters/napcat.js +39 -0
- package/adapters/napcat.ts +11 -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 +15 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/http-endpoint.d.ts +2 -1
- package/lib/http-endpoint.js +2 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/napcat-endpoint-commands.d.ts +1 -0
- package/lib/napcat-endpoint-commands.js +28 -0
- package/lib/napcat-runtime-state.d.ts +1 -0
- package/lib/napcat-runtime-state.js +6 -0
- package/lib/protocol.d.ts +8 -1
- package/lib/protocol.js +68 -2
- package/lib/ws-endpoint.d.ts +2 -1
- package/lib/ws-endpoint.js +43 -43
- package/lib/wss-endpoint.d.ts +2 -1
- package/lib/wss-endpoint.js +2 -0
- package/package.json +20 -14
- package/plugin.js +17 -0
- package/src/endpoint-management.ts +72 -0
- package/src/http-endpoint.ts +3 -1
- package/src/index.ts +1 -0
- package/src/napcat-endpoint-commands.ts +26 -0
- package/src/napcat-runtime-state.ts +7 -0
- package/src/protocol.ts +70 -2
- package/src/ws-endpoint.ts +46 -48
- package/src/wss-endpoint.ts +3 -1
- package/plugin.ts +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# @zhin.js/adapter-napcat
|
|
2
2
|
|
|
3
|
+
## 5.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
|
+
- @zhin.js/agent@1.0.7
|
|
20
|
+
|
|
21
|
+
## 5.0.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- cdf64e7: 多方向审计修复批(8 面 30+ bug):
|
|
26
|
+
|
|
27
|
+
- **安全**:钉钉 webhook 验签绕过修复(缺 timestamp/sign 一律 403 + ±1h 防重放);exec-policy fail-closed(换行/`$(`/反引号拒绝、管道逐段过白名单、env dump 复合拆段);wecom 验签改 timingSafeEqual;config:set 拒绝 `__proto__` 等魔术键、host 键优先防覆写。
|
|
28
|
+
- **P0 功能**:`zhin packages` scoped 包名解析为空导致 rm -rf 风险;命令数字参数解析失败炸断消息链路(dispatch 捕获 continue);TaskQueue 监听器首个事件自摘除导致 assistant 队列挂死(+超时后完成不覆盖终态);DatabaseHost 跨世代共享崩溃(define 幂等 + stop 改进程级);rss 按不存在 id 列删除改业务键。
|
|
29
|
+
- **Runtime/HMR**:native watcher 过滤忽略目录(lib/.zhin 不再触发重载风暴);host 段配置 patch(http.port 等)存在 installResources 时全量重建;capability 目录非 entry 支持文件升级进程重启;documentTransaction 失败回滚。
|
|
30
|
+
- **CLI 写侧**:config/setup 对 toml 静默假成功改统一 config-file(不支持格式报错);doctor/onboard 默认配置改新形态 plugins 映射;schedule add 改 --prompt;migrate engines/中文模板误伤/覆盖前备份。
|
|
31
|
+
- **适配器生命周期**:napcat/milky start 失败竞态与僵尸连接、心跳 close 清理、stop-during-connect settle;slack webhook 二次 writeHead + messageChannelMap LRU。
|
|
32
|
+
- **Host/向导**:logs/stats 与 inbox 查询改 DB 侧 count/orderBy/limit 下推;save-yaml 写入前校验;zhipu/moonshot baseUrl 必填预填;.env 写入转义 + 幂等合并;setup 数据库密码不再明文落 config;60s fetch 超时与 JSON 守卫。
|
|
33
|
+
|
|
34
|
+
- 078e3f7: 架构统一批(AURA):
|
|
35
|
+
|
|
36
|
+
- **EndpointLifecycle 基座**(@zhin.js/adapter 新增 `createEndpointLifecycle`):WS/SSE 端点的 start 失败复位、仅曾 open 才退避重连(指数+jitter 可配)、stop 不重连、PONG 看门狗、定时器集中清理、陈旧事件防叠套;napcat/milky/onebot11/onebot12/satori 已迁移(删除各自手写状态机),从此同类竞态在结构上不可能再犯。
|
|
37
|
+
- **Generation-store**(@zhin.js/plugin-runtime 新增 `createGenerationStore`):模块级运行时状态的一等能力,provide 自动挂 lifecycle 反注册(代际结束自动清理);lottery deps 与 rss db 已迁移,公开 API 兼容。
|
|
38
|
+
- **Resolver 管线收敛**(@zhin.js/runtime):解析规则统一为 local path → workspace → node_modules 单管线;optional 引用对所有 PackageResolutionError 容错(消除 message 前缀补丁)。
|
|
39
|
+
- **工具目录准入统一**(@zhin.js/agent):RegisteredToolSource 与 ExternalToolSource 共用同一 `canAccessTool` 准入(platforms/scopes/permissions/hidden 四元组全链路透传),同名覆盖 warn;AgentToolRegistration 补 platforms/scopes。两条注册通道(静态约定 vs 动态注册)职责边界已文档化。
|
|
40
|
+
|
|
41
|
+
- 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)。
|
|
42
|
+
- 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 注明平台无列表面暂不接。
|
|
43
|
+
- 6cb6152: 统一消息元素通道(UNI-Channel)落地:
|
|
44
|
+
|
|
45
|
+
- **入站契约**:`IncomingMessage.segments`(canonical Segment[],与 content 纯文本视图同源双轨),Message 透传;AI 兜底链路经 `collectSegmentMedia` 把图片/语音/视频/文件 MediaRef 写入会话 extra——多模态输入不再丢失。
|
|
46
|
+
- **出站协商**:`normalizeOutboundPayload` 升级全量 canonical 归一(复用 generic-segment-mapper),html→image 按端点 `segments.outboundMedia` 声明降级(base64 直发 / url-or-text / passthrough 自行物化);`MediaRef.kind` 新增 `'file'` 承载平台不透明引用(file_id/resource_id)。
|
|
47
|
+
- **能力声明**:`defineAdapter.segments` policy(outboundMedia / interactive),三道段门禁复活(探测点改 adapters/\*.ts,豁免名单渐进收敛)。
|
|
48
|
+
- **首批迁移**: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 图片不再静默丢图,失败降级文本)。
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [cdf64e7]
|
|
51
|
+
- Updated dependencies [2d0a159]
|
|
52
|
+
- Updated dependencies [5691aba]
|
|
53
|
+
- Updated dependencies [078e3f7]
|
|
54
|
+
- Updated dependencies [50497a5]
|
|
55
|
+
- Updated dependencies [9c997b2]
|
|
56
|
+
- Updated dependencies [09d4f25]
|
|
57
|
+
- Updated dependencies [43485a9]
|
|
58
|
+
- Updated dependencies [f0ec5ab]
|
|
59
|
+
- Updated dependencies [3e925d0]
|
|
60
|
+
- Updated dependencies [fa66c4c]
|
|
61
|
+
- Updated dependencies [fa66c4c]
|
|
62
|
+
- Updated dependencies [6cb6152]
|
|
63
|
+
- @zhin.js/command@1.0.3
|
|
64
|
+
- @zhin.js/agent@1.0.6
|
|
65
|
+
- @zhin.js/plugin-runtime@1.1.1
|
|
66
|
+
- @zhin.js/host-http@1.0.3
|
|
67
|
+
- zhin.js@5.0.1
|
|
68
|
+
- @zhin.js/adapter@1.1.1
|
|
69
|
+
- @zhin.js/core@1.4.1
|
|
70
|
+
|
|
3
71
|
## 5.0.0
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* Convention entry: discover `adapters/napcat.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 { NapCatHttpEndpoint } from "../lib/http-endpoint.js";
|
|
9
|
+
import { resolveNapCatConfig } from "../lib/protocol.js";
|
|
10
|
+
import { NapCatWsEndpoint } from "../lib/ws-endpoint.js";
|
|
11
|
+
import { NapCatWssEndpoint } from "../lib/wss-endpoint.js";
|
|
12
|
+
import { napcatRuntimeStateToken } from "../lib/napcat-runtime-state.js";
|
|
13
|
+
export { NapCatHttpEndpoint, } from "../lib/http-endpoint.js";
|
|
14
|
+
export { NapCatWsEndpoint, } from "../lib/ws-endpoint.js";
|
|
15
|
+
export { NapCatWssEndpoint, } from "../lib/wss-endpoint.js";
|
|
16
|
+
export default defineAdapter({
|
|
17
|
+
capabilities: ['inbound', 'outbound'],
|
|
18
|
+
// OneBot file 参数原生消费 url / base64:// 媒体;无卡片交互面,交互段降级纯文本。
|
|
19
|
+
segments: {
|
|
20
|
+
outboundMedia: ['url', 'base64'],
|
|
21
|
+
interactive: 'text',
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const config = resolveNapCatConfig(context.config);
|
|
25
|
+
const gateway = context.use(messageGatewayToken);
|
|
26
|
+
// 注册到插件运行时状态(napcat endpoint list 的"运行中"数据源)
|
|
27
|
+
context.use(napcatRuntimeStateToken).endpoints.set(config.name, {
|
|
28
|
+
name: config.name,
|
|
29
|
+
mode: config.connection,
|
|
30
|
+
});
|
|
31
|
+
if (config.connection === 'wss') {
|
|
32
|
+
return new NapCatWssEndpoint({ id: context.id, gateway, http: context.use(httpHostToken), config });
|
|
33
|
+
}
|
|
34
|
+
if (config.connection === 'http') {
|
|
35
|
+
return new NapCatHttpEndpoint({ id: context.id, gateway, http: context.use(httpHostToken), config });
|
|
36
|
+
}
|
|
37
|
+
return new NapCatWsEndpoint({ id: context.id, gateway, config });
|
|
38
|
+
},
|
|
39
|
+
});
|
package/adapters/napcat.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { NapCatHttpEndpoint } from '../src/http-endpoint.js';
|
|
|
8
8
|
import { resolveNapCatConfig, type NapCatAdapterConfig } from '../src/protocol.js';
|
|
9
9
|
import { NapCatWsEndpoint } from '../src/ws-endpoint.js';
|
|
10
10
|
import { NapCatWssEndpoint } from '../src/wss-endpoint.js';
|
|
11
|
+
import { napcatRuntimeStateToken } from '../src/napcat-runtime-state.js';
|
|
11
12
|
|
|
12
13
|
export {
|
|
13
14
|
NapCatHttpEndpoint,
|
|
@@ -25,9 +26,19 @@ export type { NapCatWsSocket, NapCatWsCreateOptions } from '../src/ws-types.js';
|
|
|
25
26
|
|
|
26
27
|
export default defineAdapter<NapCatAdapterConfig>({
|
|
27
28
|
capabilities: ['inbound', 'outbound'],
|
|
29
|
+
// OneBot file 参数原生消费 url / base64:// 媒体;无卡片交互面,交互段降级纯文本。
|
|
30
|
+
segments: {
|
|
31
|
+
outboundMedia: ['url', 'base64'],
|
|
32
|
+
interactive: 'text',
|
|
33
|
+
},
|
|
28
34
|
create(context) {
|
|
29
35
|
const config = resolveNapCatConfig(context.config);
|
|
30
36
|
const gateway = context.use(messageGatewayToken);
|
|
37
|
+
// 注册到插件运行时状态(napcat endpoint list 的"运行中"数据源)
|
|
38
|
+
context.use(napcatRuntimeStateToken).endpoints.set(config.name, {
|
|
39
|
+
name: config.name,
|
|
40
|
+
mode: config.connection,
|
|
41
|
+
});
|
|
31
42
|
if (config.connection === 'wss') {
|
|
32
43
|
return new NapCatWssEndpoint({ id: context.id, gateway, http: context.use(httpHostToken), config });
|
|
33
44
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat endpoint management 语义端口(Console 社交面 RPC 消费,见
|
|
3
|
+
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
|
+
*
|
|
5
|
+
* 归一化取舍:
|
|
6
|
+
* - friend → {user_id:number, nickname, remark: remark ?? ''}
|
|
7
|
+
* - group → {group_id:number, name: group_name ?? name}
|
|
8
|
+
* - 群成员列表保持 OneBot11 原生形状,仅保证数组
|
|
9
|
+
*/
|
|
10
|
+
import type { EndpointManagement } from '@zhin.js/adapter';
|
|
11
|
+
/** 管理面只依赖 endpoint 的 callApi(ws/wss/http 三种传输各自实现)。 */
|
|
12
|
+
export interface NapCatManagementCaller {
|
|
13
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createNapCatEndpointManagement(endpoint: NapCatManagementCaller): EndpointManagement;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createNapCatEndpointManagement(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.nickname ?? ''),
|
|
10
|
+
remark: String(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: toNumberId(groupId, 'group_id'),
|
|
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
|
+
/** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
|
|
41
|
+
function toNumberId(value, label) {
|
|
42
|
+
const n = Number(value);
|
|
43
|
+
if (!Number.isFinite(n) || String(value ?? '').trim() === '') {
|
|
44
|
+
throw new TypeError(`napcat ${label} 必须是数字: ${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
}
|
package/lib/http-endpoint.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 NapCatHttpEndpointOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class NapCatHttpEndpoint implements EndpointInstance {
|
|
14
14
|
#private;
|
|
15
|
+
readonly management: EndpointManagement;
|
|
15
16
|
constructor(options: NapCatHttpEndpointOptions);
|
|
16
17
|
start(): Promise<void>;
|
|
17
18
|
open(): void;
|
package/lib/http-endpoint.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
2
3
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
3
4
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
4
5
|
import { buildSendAction, callNapCatHttpAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
@@ -8,6 +9,7 @@ const logger = getLogger('napcat');
|
|
|
8
9
|
export class NapCatHttpEndpoint {
|
|
9
10
|
#options;
|
|
10
11
|
#inboundDeduper = new InboundMessageDeduper();
|
|
12
|
+
management = createNapCatEndpointManagement(this);
|
|
11
13
|
#callHttpAction;
|
|
12
14
|
#routeReleases = [];
|
|
13
15
|
#open = false;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildSendAction, buildWsConnectOptions, callNapCatHttpAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isMessageEvent, parseSendTarget, resolveNapCatConfig, senderNickname, senderUserId, type MessageSegment, type NapCatActionRequest, type NapCatActionResponse, type NapCatAdapterConfig, type NapCatConfigBase, type NapCatEndpointConfig, type NapCatEvent, type NapCatHttpConfig, type NapCatMessageEvent, type NapCatSender, type NapCatWireSegment, type NapCatWsConfig, type NapCatWssConfig, type ParsedSendTarget, type ResolvedNapCatConfig, } from './protocol.js';
|
|
1
|
+
export { buildSendAction, buildWsConnectOptions, callNapCatHttpAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isMessageEvent, mediaRefToOneBotFile, parseSendTarget, resolveNapCatConfig, senderNickname, senderUserId, type MessageSegment, type NapCatActionRequest, type NapCatActionResponse, type NapCatAdapterConfig, type NapCatConfigBase, type NapCatEndpointConfig, type NapCatEvent, type NapCatHttpConfig, type NapCatMessageEvent, type NapCatSender, type NapCatWireSegment, type NapCatWsConfig, type NapCatWssConfig, type ParsedSendTarget, type ResolvedNapCatConfig, } from './protocol.js';
|
|
2
2
|
export { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
3
3
|
export { getEndpoint, getNapcatAgentDeps, registerNapcatAgentEndpoint, setNapcatAgentDeps, type NapcatAgentDeps, type NapcatAgentEndpoint, } from './napcat-agent-deps.js';
|
|
4
4
|
export { parseOneBotGetMsgResponse } from './onebot-get-msg.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildSendAction, buildWsConnectOptions, callNapCatHttpAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isMessageEvent, parseSendTarget, resolveNapCatConfig, senderNickname, senderUserId, } from './protocol.js';
|
|
1
|
+
export { buildSendAction, buildWsConnectOptions, callNapCatHttpAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, getChannelId, isMessageEvent, mediaRefToOneBotFile, parseSendTarget, resolveNapCatConfig, senderNickname, senderUserId, } from './protocol.js';
|
|
2
2
|
export { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
3
3
|
export { getEndpoint, getNapcatAgentDeps, registerNapcatAgentEndpoint, setNapcatAgentDeps, } from './napcat-agent-deps.js';
|
|
4
4
|
export { parseOneBotGetMsgResponse } from './onebot-get-msg.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const napcatEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `napcat 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 { napcatRuntimeStateToken } from './napcat-runtime-state.js';
|
|
8
|
+
export const napcatEndpointCommands = createEndpointCommands({
|
|
9
|
+
adapterKey: 'napcat',
|
|
10
|
+
adapterDisplayName: 'NapCat',
|
|
11
|
+
fields: [
|
|
12
|
+
{ key: 'url', description: 'NapCat WebSocket URL(connection: ws 必填)' },
|
|
13
|
+
{ key: 'path', description: 'reverse-wss 路径(connection: wss)' },
|
|
14
|
+
{ key: 'http_url', description: 'HTTP API base URL(connection: http 出站)' },
|
|
15
|
+
{ key: 'post_path', description: 'HTTP POST 事件路径(connection: http 入站)' },
|
|
16
|
+
{ key: 'access_token', env: true, description: 'NapCat access token' },
|
|
17
|
+
],
|
|
18
|
+
running: (use) => use(napcatRuntimeStateToken).endpoints.values(),
|
|
19
|
+
describeEntry: (entry) => {
|
|
20
|
+
if (entry.url)
|
|
21
|
+
return `url: ${String(entry.url)}`;
|
|
22
|
+
if (entry.path)
|
|
23
|
+
return `path: ${String(entry.path)}`;
|
|
24
|
+
if (entry.http_url)
|
|
25
|
+
return `http_url: ${String(entry.http_url)}`;
|
|
26
|
+
return '';
|
|
27
|
+
},
|
|
28
|
+
}, defineCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const napcatRuntimeStateToken: import("@zhin.js/plugin-runtime").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `napcat endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
export const napcatRuntimeStateToken = defineEndpointRuntimeStateToken('napcat');
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* No legacy Adapter/Endpoint / segment-mapper.
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
*/
|
|
6
|
+
import { type MediaRef } from '@zhin.js/core';
|
|
6
7
|
/** Transitional legacy endpoint row (`endpoints[]` with `context: napcat`). */
|
|
7
8
|
export interface NapCatLegacyEndpointRow {
|
|
8
9
|
readonly context?: string;
|
|
@@ -125,9 +126,15 @@ export declare function formatInboundContent(ev: NapCatEvent): string;
|
|
|
125
126
|
export declare function senderUserId(ev: NapCatEvent): string;
|
|
126
127
|
/** 显示名(群名片 card 优先于 nickname),没有则 undefined。 */
|
|
127
128
|
export declare function senderNickname(ev: NapCatEvent): string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
|
|
131
|
+
* 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
|
|
132
|
+
*/
|
|
133
|
+
export declare function mediaRefToOneBotFile(media: MediaRef): string;
|
|
128
134
|
/**
|
|
129
135
|
* Wire-encode an already-rendered outbound payload into OneBot message segments.
|
|
130
|
-
*
|
|
136
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
137
|
+
* 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
|
|
131
138
|
*/
|
|
132
139
|
export declare function formatOutboundSegments(payload: unknown): MessageSegment[];
|
|
133
140
|
export declare function buildSendAction(target: string, message: MessageSegment[]): {
|
package/lib/protocol.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* No legacy Adapter/Endpoint / segment-mapper.
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
*/
|
|
6
|
+
import { isMediaRef, mediaRefFromLegacyData } from '@zhin.js/core';
|
|
6
7
|
function normalizeConnection(connection) {
|
|
7
8
|
if (connection === 'wss' || connection === 'http')
|
|
8
9
|
return connection;
|
|
@@ -118,9 +119,74 @@ export function senderNickname(ev) {
|
|
|
118
119
|
const name = ev.sender?.card || ev.sender?.nickname;
|
|
119
120
|
return typeof name === 'string' && name ? name : undefined;
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
|
|
124
|
+
* 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
|
|
125
|
+
*/
|
|
126
|
+
export function mediaRefToOneBotFile(media) {
|
|
127
|
+
if (media.kind === 'base64') {
|
|
128
|
+
return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
|
|
129
|
+
}
|
|
130
|
+
if (media.kind === 'path') {
|
|
131
|
+
return media.value.startsWith('file://') ? media.value : `file://${media.value}`;
|
|
132
|
+
}
|
|
133
|
+
return media.value;
|
|
134
|
+
}
|
|
135
|
+
/** 媒体段 data 里的 canonical-only 字段,不进 OneBot wire。 */
|
|
136
|
+
const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt', 'url', 'base64', 'file', 'mime_type']);
|
|
137
|
+
function oneBotMediaSegment(type, data) {
|
|
138
|
+
const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
|
|
139
|
+
if (!media)
|
|
140
|
+
return { type, data };
|
|
141
|
+
const extra = {};
|
|
142
|
+
for (const [key, value] of Object.entries(data)) {
|
|
143
|
+
if (!MEDIA_DATA_SKIP_KEYS.has(key))
|
|
144
|
+
extra[key] = value;
|
|
145
|
+
}
|
|
146
|
+
return { type, data: { ...extra, file: mediaRefToOneBotFile(media) } };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* canonical Segment → OneBot 11 数组段:
|
|
150
|
+
* - mention → at(`qq: target`);
|
|
151
|
+
* - reply(`message_id`)→ reply(`id`);
|
|
152
|
+
* - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file://);
|
|
153
|
+
* - face 取 `id`;
|
|
154
|
+
* - 其余(NapCat 扩展段、已是 wire 形状的段)原样透传。
|
|
155
|
+
*/
|
|
156
|
+
function canonicalToOneBotSegment(segment) {
|
|
157
|
+
const data = segment.data ?? {};
|
|
158
|
+
switch (segment.type) {
|
|
159
|
+
case 'mention': {
|
|
160
|
+
const target = data.target ?? data.qq ?? data.id;
|
|
161
|
+
if (target == null)
|
|
162
|
+
return { type: segment.type, data };
|
|
163
|
+
return { type: 'at', data: { qq: String(target) } };
|
|
164
|
+
}
|
|
165
|
+
case 'reply': {
|
|
166
|
+
const messageId = data.message_id ?? data.id;
|
|
167
|
+
if (messageId == null)
|
|
168
|
+
return { type: segment.type, data };
|
|
169
|
+
return { type: 'reply', data: { id: String(messageId) } };
|
|
170
|
+
}
|
|
171
|
+
case 'face': {
|
|
172
|
+
if (data.id == null)
|
|
173
|
+
return { type: segment.type, data };
|
|
174
|
+
return { type: 'face', data: { id: data.id } };
|
|
175
|
+
}
|
|
176
|
+
case 'image':
|
|
177
|
+
case 'record':
|
|
178
|
+
case 'video':
|
|
179
|
+
return oneBotMediaSegment(segment.type, data);
|
|
180
|
+
case 'audio':
|
|
181
|
+
return oneBotMediaSegment('record', data);
|
|
182
|
+
default:
|
|
183
|
+
return { type: segment.type, data };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
121
186
|
/**
|
|
122
187
|
* Wire-encode an already-rendered outbound payload into OneBot message segments.
|
|
123
|
-
*
|
|
188
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
189
|
+
* 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
|
|
124
190
|
*/
|
|
125
191
|
export function formatOutboundSegments(payload) {
|
|
126
192
|
if (typeof payload === 'string') {
|
|
@@ -145,7 +211,7 @@ export function formatOutboundSegments(payload) {
|
|
|
145
211
|
segs.push({ type: 'text', data: { text: item } });
|
|
146
212
|
continue;
|
|
147
213
|
}
|
|
148
|
-
segs.push(
|
|
214
|
+
segs.push(canonicalToOneBotSegment(item));
|
|
149
215
|
}
|
|
150
216
|
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
151
217
|
}
|
package/lib/ws-endpoint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
4
4
|
import { type NapCatEvent, type NapCatWsConfig } from './protocol.js';
|
|
@@ -11,6 +11,7 @@ export interface NapCatWsEndpointOptions {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class NapCatWsEndpoint implements EndpointInstance {
|
|
13
13
|
#private;
|
|
14
|
+
readonly management: EndpointManagement;
|
|
14
15
|
constructor(options: NapCatWsEndpointOptions);
|
|
15
16
|
start(): Promise<void>;
|
|
16
17
|
open(): void;
|
package/lib/ws-endpoint.js
CHANGED
|
@@ -2,39 +2,48 @@
|
|
|
2
2
|
* NapCat WS client endpoint — outbound connect to NapCat.
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
|
-
import {
|
|
5
|
+
import { createEndpointLifecycle, } from '@zhin.js/adapter';
|
|
6
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
7
8
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
8
9
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
9
10
|
import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
10
|
-
import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending,
|
|
11
|
+
import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, } from './ws-transport.js';
|
|
11
12
|
const logger = getLogger('napcat');
|
|
12
13
|
export class NapCatWsEndpoint {
|
|
13
14
|
#options;
|
|
14
15
|
#inboundDeduper = new InboundMessageDeduper();
|
|
16
|
+
management = createNapCatEndpointManagement(this);
|
|
17
|
+
#lifecycle;
|
|
15
18
|
#ws;
|
|
16
|
-
#reconnectTimer;
|
|
17
|
-
#heartbeatTimer;
|
|
18
19
|
#requestId = { value: 0 };
|
|
19
20
|
#pending = new Map();
|
|
20
21
|
#open = false;
|
|
21
|
-
#started = false;
|
|
22
|
-
#stopping = false;
|
|
23
22
|
#unregisterAgent;
|
|
24
23
|
constructor(options) {
|
|
25
24
|
this.#options = options;
|
|
25
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
26
|
+
name: options.config.name,
|
|
27
|
+
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
28
|
+
reconnect: {
|
|
29
|
+
initialIntervalMs: options.config.reconnect_interval,
|
|
30
|
+
multiplier: 1,
|
|
31
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
32
|
+
jitterMs: 0,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
26
35
|
}
|
|
27
36
|
async start() {
|
|
28
|
-
if (this.#started)
|
|
37
|
+
if (this.#lifecycle.started)
|
|
29
38
|
return;
|
|
30
|
-
this.#started = true;
|
|
31
|
-
this.#stopping = false;
|
|
32
39
|
this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
|
|
33
40
|
try {
|
|
34
|
-
await this.#connect();
|
|
41
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
35
42
|
}
|
|
36
43
|
catch (err) {
|
|
37
|
-
|
|
44
|
+
// start 失败复位由基座保证;agent 注册/反注册是适配器专有依赖,留在适配器侧
|
|
45
|
+
this.#unregisterAgent?.();
|
|
46
|
+
this.#unregisterAgent = undefined;
|
|
38
47
|
throw err;
|
|
39
48
|
}
|
|
40
49
|
}
|
|
@@ -46,18 +55,9 @@ export class NapCatWsEndpoint {
|
|
|
46
55
|
}
|
|
47
56
|
async stop() {
|
|
48
57
|
this.#open = false;
|
|
49
|
-
this.#
|
|
50
|
-
this.#started = false;
|
|
58
|
+
await this.#lifecycle.stop();
|
|
51
59
|
this.#unregisterAgent?.();
|
|
52
60
|
this.#unregisterAgent = undefined;
|
|
53
|
-
if (this.#reconnectTimer) {
|
|
54
|
-
clearTimeout(this.#reconnectTimer);
|
|
55
|
-
this.#reconnectTimer = undefined;
|
|
56
|
-
}
|
|
57
|
-
if (this.#heartbeatTimer) {
|
|
58
|
-
clearInterval(this.#heartbeatTimer);
|
|
59
|
-
this.#heartbeatTimer = undefined;
|
|
60
|
-
}
|
|
61
61
|
rejectAllPending(this.#pending);
|
|
62
62
|
this.#inboundDeduper.clear();
|
|
63
63
|
if (this.#ws) {
|
|
@@ -69,7 +69,6 @@ export class NapCatWsEndpoint {
|
|
|
69
69
|
}
|
|
70
70
|
this.#ws = undefined;
|
|
71
71
|
}
|
|
72
|
-
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
73
72
|
}
|
|
74
73
|
async send({ target, payload }) {
|
|
75
74
|
const message = formatOutboundSegments(payload);
|
|
@@ -262,7 +261,7 @@ export class NapCatWsEndpoint {
|
|
|
262
261
|
}));
|
|
263
262
|
});
|
|
264
263
|
}
|
|
265
|
-
async #connect() {
|
|
264
|
+
async #connect(handle) {
|
|
266
265
|
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
267
266
|
const create = this.#options.createWebSocket
|
|
268
267
|
?? ((connectUrl, options) => new WebSocket(connectUrl, { headers: options.headers }));
|
|
@@ -270,6 +269,14 @@ export class NapCatWsEndpoint {
|
|
|
270
269
|
let settled = false;
|
|
271
270
|
const ws = create(url, { headers });
|
|
272
271
|
this.#ws = ws;
|
|
272
|
+
handle.onForceClose(() => {
|
|
273
|
+
try {
|
|
274
|
+
ws.close();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* ignore */
|
|
278
|
+
}
|
|
279
|
+
});
|
|
273
280
|
ws.on('open', () => {
|
|
274
281
|
if (settled)
|
|
275
282
|
return;
|
|
@@ -286,9 +293,17 @@ export class NapCatWsEndpoint {
|
|
|
286
293
|
mode: 'ws',
|
|
287
294
|
url: safeUrl,
|
|
288
295
|
}));
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
// stop-during-connect 竞态:已停止则不再武装心跳(基座 stop 已清理定时器)
|
|
297
|
+
if (this.#lifecycle.started) {
|
|
298
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
299
|
+
try {
|
|
300
|
+
ws.ping?.();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
/* ignore */
|
|
304
|
+
}
|
|
305
|
+
}, this.#options.config.heartbeat_interval);
|
|
306
|
+
}
|
|
292
307
|
resolve();
|
|
293
308
|
});
|
|
294
309
|
ws.on('message', (data) => {
|
|
@@ -317,11 +332,12 @@ export class NapCatWsEndpoint {
|
|
|
317
332
|
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
318
333
|
reconnect_ms: this.#options.config.reconnect_interval,
|
|
319
334
|
}));
|
|
335
|
+
// 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
336
|
+
handle.notifyClosed(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
|
|
320
337
|
if (!settled) {
|
|
321
338
|
settled = true;
|
|
322
339
|
reject(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
|
|
323
340
|
}
|
|
324
|
-
this.#scheduleReconnect();
|
|
325
341
|
});
|
|
326
342
|
ws.on('error', (err) => {
|
|
327
343
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -338,20 +354,4 @@ export class NapCatWsEndpoint {
|
|
|
338
354
|
});
|
|
339
355
|
});
|
|
340
356
|
}
|
|
341
|
-
#scheduleReconnect() {
|
|
342
|
-
if (this.#stopping || !this.#started || this.#reconnectTimer)
|
|
343
|
-
return;
|
|
344
|
-
const delay = this.#options.config.reconnect_interval;
|
|
345
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
346
|
-
this.#reconnectTimer = undefined;
|
|
347
|
-
void this.#connect().catch((err) => {
|
|
348
|
-
logger.warn(formatCompact({
|
|
349
|
-
op: 'reconnect',
|
|
350
|
-
endpoint: this.#options.config.name,
|
|
351
|
-
ok: false,
|
|
352
|
-
error: err instanceof Error ? err.message : String(err),
|
|
353
|
-
}));
|
|
354
|
-
});
|
|
355
|
-
}, delay);
|
|
356
|
-
}
|
|
357
357
|
}
|
package/lib/wss-endpoint.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';
|
|
@@ -11,6 +11,7 @@ export interface NapCatWssEndpointOptions {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class NapCatWssEndpoint implements EndpointInstance {
|
|
13
13
|
#private;
|
|
14
|
+
readonly management: EndpointManagement;
|
|
14
15
|
constructor(options: NapCatWssEndpointOptions);
|
|
15
16
|
start(): Promise<void>;
|
|
16
17
|
open(): void;
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { clearInterval } from 'node:timers';
|
|
5
5
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
6
7
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
7
8
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
8
9
|
import { buildSendAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
@@ -12,6 +13,7 @@ const logger = getLogger('napcat');
|
|
|
12
13
|
export class NapCatWssEndpoint {
|
|
13
14
|
#options;
|
|
14
15
|
#inboundDeduper = new InboundMessageDeduper();
|
|
16
|
+
management = createNapCatEndpointManagement(this);
|
|
15
17
|
#ws;
|
|
16
18
|
#wsRelease;
|
|
17
19
|
#heartbeatTimer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-napcat",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"adapters",
|
|
17
|
-
"
|
|
17
|
+
"commands",
|
|
18
|
+
"plugin.js",
|
|
18
19
|
"schema.json",
|
|
19
20
|
"src",
|
|
20
21
|
"lib",
|
|
@@ -42,11 +43,12 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"ws": "^8.21.0",
|
|
45
|
-
"@zhin.js/adapter": "1.1.
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
46
|
+
"@zhin.js/adapter": "1.1.2",
|
|
47
|
+
"@zhin.js/command": "1.0.4",
|
|
48
|
+
"@zhin.js/core": "1.4.2",
|
|
49
|
+
"@zhin.js/host-http": "1.0.3",
|
|
48
50
|
"@zhin.js/logger": "1.0.75",
|
|
49
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
51
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@types/node": "^26.1.0",
|
|
@@ -54,16 +56,16 @@
|
|
|
54
56
|
"typescript": "^6.0.3",
|
|
55
57
|
"vitest": "^4.1.10",
|
|
56
58
|
"zod": "^4.4.3",
|
|
57
|
-
"@zhin.js/agent": "1.0.
|
|
58
|
-
"@zhin.js/host-http": "1.0.
|
|
59
|
+
"@zhin.js/agent": "1.0.7",
|
|
60
|
+
"@zhin.js/host-http": "1.0.3"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
61
63
|
"zod": "^4.0.0",
|
|
62
|
-
"@zhin.js/adapter": "1.1.
|
|
63
|
-
"@zhin.js/core": "1.4.
|
|
64
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
65
|
-
"zhin.js": "5.0.
|
|
66
|
-
"@zhin.js/agent": "1.0.
|
|
64
|
+
"@zhin.js/adapter": "1.1.2",
|
|
65
|
+
"@zhin.js/core": "1.4.2",
|
|
66
|
+
"@zhin.js/plugin-runtime": "1.1.1",
|
|
67
|
+
"zhin.js": "5.0.2",
|
|
68
|
+
"@zhin.js/agent": "1.0.7"
|
|
67
69
|
},
|
|
68
70
|
"peerDependenciesMeta": {
|
|
69
71
|
"zhin.js": {
|
|
@@ -91,13 +93,17 @@
|
|
|
91
93
|
"zhin": {
|
|
92
94
|
"protocol": 1,
|
|
93
95
|
"type": "plugin",
|
|
94
|
-
"entry": "./plugin.
|
|
96
|
+
"entry": "./plugin.js",
|
|
95
97
|
"engine": "^1.0.0",
|
|
96
98
|
"runtime": "trusted",
|
|
97
99
|
"features": [
|
|
98
100
|
{
|
|
99
101
|
"package": "@zhin.js/adapter",
|
|
100
102
|
"api": "^1.0.0"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"package": "@zhin.js/command",
|
|
106
|
+
"api": "^1.0.0"
|
|
101
107
|
}
|
|
102
108
|
],
|
|
103
109
|
"plugins": []
|
package/plugin.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createEndpointRuntimeState } from '@zhin.js/adapter';
|
|
3
|
+
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { registerDefaultScenePlatformPermitChecker } from '@zhin.js/core';
|
|
5
|
+
import { napcatRuntimeStateToken } from "./lib/napcat-runtime-state.js";
|
|
6
|
+
export default definePlugin({
|
|
7
|
+
name: 'napcat',
|
|
8
|
+
metadata: {
|
|
9
|
+
displayName: 'NapCat Adapter',
|
|
10
|
+
},
|
|
11
|
+
setup(context) {
|
|
12
|
+
// 运行中 endpoint 注册表(napcat endpoint list 的"运行中"数据源)
|
|
13
|
+
context.resources.provide(napcatRuntimeStateToken, createEndpointRuntimeState());
|
|
14
|
+
// 平台权限门禁:scene_admin / scene_owner 由 sender role 判定(见各 endpoint admit metadata)
|
|
15
|
+
return registerDefaultScenePlatformPermitChecker('napcat');
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat endpoint management 语义端口(Console 社交面 RPC 消费,见
|
|
3
|
+
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
|
+
*
|
|
5
|
+
* 归一化取舍:
|
|
6
|
+
* - friend → {user_id:number, nickname, remark: remark ?? ''}
|
|
7
|
+
* - group → {group_id:number, name: group_name ?? name}
|
|
8
|
+
* - 群成员列表保持 OneBot11 原生形状,仅保证数组
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
EndpointFriend,
|
|
12
|
+
EndpointGroup,
|
|
13
|
+
EndpointManagement,
|
|
14
|
+
} from '@zhin.js/adapter';
|
|
15
|
+
|
|
16
|
+
/** 管理面只依赖 endpoint 的 callApi(ws/wss/http 三种传输各自实现)。 */
|
|
17
|
+
export interface NapCatManagementCaller {
|
|
18
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createNapCatEndpointManagement(
|
|
22
|
+
endpoint: NapCatManagementCaller,
|
|
23
|
+
): EndpointManagement {
|
|
24
|
+
return Object.freeze<EndpointManagement>({
|
|
25
|
+
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
26
|
+
const data = await endpoint.callApi('get_friend_list');
|
|
27
|
+
return toArray(data).map((value) => {
|
|
28
|
+
const friend = asRecord(value);
|
|
29
|
+
return {
|
|
30
|
+
user_id: toNumberId(friend.user_id, 'user_id'),
|
|
31
|
+
nickname: String(friend.nickname ?? ''),
|
|
32
|
+
remark: String(friend.remark ?? ''),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
37
|
+
const data = await endpoint.callApi('get_group_list');
|
|
38
|
+
return toArray(data).map((value) => {
|
|
39
|
+
const group = asRecord(value);
|
|
40
|
+
return {
|
|
41
|
+
group_id: toNumberId(group.group_id, 'group_id'),
|
|
42
|
+
name: String(group.group_name ?? group.name ?? ''),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
|
|
47
|
+
const data = await endpoint.callApi('get_group_member_list', {
|
|
48
|
+
group_id: toNumberId(groupId, 'group_id'),
|
|
49
|
+
});
|
|
50
|
+
return toArray(data);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
56
|
+
return value !== null && typeof value === 'object'
|
|
57
|
+
? value as Record<string, unknown>
|
|
58
|
+
: {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toArray(value: unknown): readonly unknown[] {
|
|
62
|
+
return Array.isArray(value) ? value : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
|
|
66
|
+
function toNumberId(value: unknown, label: string): number {
|
|
67
|
+
const n = Number(value);
|
|
68
|
+
if (!Number.isFinite(n) || String(value ?? '').trim() === '') {
|
|
69
|
+
throw new TypeError(`napcat ${label} 必须是数字: ${String(value)}`);
|
|
70
|
+
}
|
|
71
|
+
return n;
|
|
72
|
+
}
|
package/src/http-endpoint.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
|
|
3
3
|
*/
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
10
11
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
11
12
|
import {
|
|
12
13
|
InboundMessageDeduper,
|
|
@@ -43,6 +44,7 @@ export interface NapCatHttpEndpointOptions {
|
|
|
43
44
|
export class NapCatHttpEndpoint implements EndpointInstance {
|
|
44
45
|
readonly #options: NapCatHttpEndpointOptions;
|
|
45
46
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
47
|
+
readonly management: EndpointManagement = createNapCatEndpointManagement(this);
|
|
46
48
|
readonly #callHttpAction: typeof callNapCatHttpAction;
|
|
47
49
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
48
50
|
#open = false;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `napcat 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 { napcatRuntimeStateToken } from './napcat-runtime-state.js';
|
|
8
|
+
|
|
9
|
+
export const napcatEndpointCommands = createEndpointCommands({
|
|
10
|
+
adapterKey: 'napcat',
|
|
11
|
+
adapterDisplayName: 'NapCat',
|
|
12
|
+
fields: [
|
|
13
|
+
{ key: 'url', description: 'NapCat WebSocket URL(connection: ws 必填)' },
|
|
14
|
+
{ key: 'path', description: 'reverse-wss 路径(connection: wss)' },
|
|
15
|
+
{ key: 'http_url', description: 'HTTP API base URL(connection: http 出站)' },
|
|
16
|
+
{ key: 'post_path', description: 'HTTP POST 事件路径(connection: http 入站)' },
|
|
17
|
+
{ key: 'access_token', env: true, description: 'NapCat access token' },
|
|
18
|
+
],
|
|
19
|
+
running: (use) => use(napcatRuntimeStateToken).endpoints.values(),
|
|
20
|
+
describeEntry: (entry) => {
|
|
21
|
+
if (entry.url) return `url: ${String(entry.url)}`;
|
|
22
|
+
if (entry.path) return `path: ${String(entry.path)}`;
|
|
23
|
+
if (entry.http_url) return `http_url: ${String(entry.http_url)}`;
|
|
24
|
+
return '';
|
|
25
|
+
},
|
|
26
|
+
}, defineCommand);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `napcat endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
|
|
7
|
+
export const napcatRuntimeStateToken = defineEndpointRuntimeStateToken('napcat');
|
package/src/protocol.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* No legacy Adapter/Endpoint / segment-mapper.
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
*/
|
|
6
|
+
import { isMediaRef, mediaRefFromLegacyData, type MediaRef } from '@zhin.js/core';
|
|
6
7
|
|
|
7
8
|
/** Transitional legacy endpoint row (`endpoints[]` with `context: napcat`). */
|
|
8
9
|
export interface NapCatLegacyEndpointRow {
|
|
@@ -253,9 +254,76 @@ export function senderNickname(ev: NapCatEvent): string | undefined {
|
|
|
253
254
|
return typeof name === 'string' && name ? name : undefined;
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
|
|
259
|
+
* 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
|
|
260
|
+
*/
|
|
261
|
+
export function mediaRefToOneBotFile(media: MediaRef): string {
|
|
262
|
+
if (media.kind === 'base64') {
|
|
263
|
+
return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
|
|
264
|
+
}
|
|
265
|
+
if (media.kind === 'path') {
|
|
266
|
+
return media.value.startsWith('file://') ? media.value : `file://${media.value}`;
|
|
267
|
+
}
|
|
268
|
+
return media.value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 媒体段 data 里的 canonical-only 字段,不进 OneBot wire。 */
|
|
272
|
+
const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt', 'url', 'base64', 'file', 'mime_type']);
|
|
273
|
+
|
|
274
|
+
function oneBotMediaSegment(
|
|
275
|
+
type: 'image' | 'record' | 'video',
|
|
276
|
+
data: Record<string, unknown>,
|
|
277
|
+
): MessageSegment {
|
|
278
|
+
const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
|
|
279
|
+
if (!media) return { type, data };
|
|
280
|
+
const extra: Record<string, unknown> = {};
|
|
281
|
+
for (const [key, value] of Object.entries(data)) {
|
|
282
|
+
if (!MEDIA_DATA_SKIP_KEYS.has(key)) extra[key] = value;
|
|
283
|
+
}
|
|
284
|
+
return { type, data: { ...extra, file: mediaRefToOneBotFile(media) } };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* canonical Segment → OneBot 11 数组段:
|
|
289
|
+
* - mention → at(`qq: target`);
|
|
290
|
+
* - reply(`message_id`)→ reply(`id`);
|
|
291
|
+
* - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file://);
|
|
292
|
+
* - face 取 `id`;
|
|
293
|
+
* - 其余(NapCat 扩展段、已是 wire 形状的段)原样透传。
|
|
294
|
+
*/
|
|
295
|
+
function canonicalToOneBotSegment(segment: NapCatWireSegment): MessageSegment {
|
|
296
|
+
const data = segment.data ?? {};
|
|
297
|
+
switch (segment.type) {
|
|
298
|
+
case 'mention': {
|
|
299
|
+
const target = data.target ?? data.qq ?? data.id;
|
|
300
|
+
if (target == null) return { type: segment.type, data };
|
|
301
|
+
return { type: 'at', data: { qq: String(target) } };
|
|
302
|
+
}
|
|
303
|
+
case 'reply': {
|
|
304
|
+
const messageId = data.message_id ?? data.id;
|
|
305
|
+
if (messageId == null) return { type: segment.type, data };
|
|
306
|
+
return { type: 'reply', data: { id: String(messageId) } };
|
|
307
|
+
}
|
|
308
|
+
case 'face': {
|
|
309
|
+
if (data.id == null) return { type: segment.type, data };
|
|
310
|
+
return { type: 'face', data: { id: data.id } };
|
|
311
|
+
}
|
|
312
|
+
case 'image':
|
|
313
|
+
case 'record':
|
|
314
|
+
case 'video':
|
|
315
|
+
return oneBotMediaSegment(segment.type, data);
|
|
316
|
+
case 'audio':
|
|
317
|
+
return oneBotMediaSegment('record', data);
|
|
318
|
+
default:
|
|
319
|
+
return { type: segment.type, data };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
256
323
|
/**
|
|
257
324
|
* Wire-encode an already-rendered outbound payload into OneBot message segments.
|
|
258
|
-
*
|
|
325
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
326
|
+
* 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
|
|
259
327
|
*/
|
|
260
328
|
export function formatOutboundSegments(payload: unknown): MessageSegment[] {
|
|
261
329
|
if (typeof payload === 'string') {
|
|
@@ -283,7 +351,7 @@ export function formatOutboundSegments(payload: unknown): MessageSegment[] {
|
|
|
283
351
|
segs.push({ type: 'text', data: { text: item } });
|
|
284
352
|
continue;
|
|
285
353
|
}
|
|
286
|
-
segs.push(
|
|
354
|
+
segs.push(canonicalToOneBotSegment(item));
|
|
287
355
|
}
|
|
288
356
|
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
289
357
|
}
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
* NapCat WS client endpoint — outbound connect to NapCat.
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
createEndpointLifecycle,
|
|
7
|
+
type EndpointConnectHandle,
|
|
8
|
+
type EndpointInstance,
|
|
9
|
+
type EndpointLifecycle,
|
|
10
|
+
type EndpointManagement,
|
|
11
|
+
} from '@zhin.js/adapter';
|
|
7
12
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
8
13
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
14
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
15
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
10
16
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
11
17
|
import {
|
|
12
18
|
InboundMessageDeduper,
|
|
@@ -30,7 +36,6 @@ import {
|
|
|
30
36
|
callNapCatWsAction,
|
|
31
37
|
handleNapCatWsMessage,
|
|
32
38
|
rejectAllPending,
|
|
33
|
-
startNapCatHeartbeat,
|
|
34
39
|
} from './ws-transport.js';
|
|
35
40
|
import {
|
|
36
41
|
type NapCatPendingAction,
|
|
@@ -53,29 +58,37 @@ export interface NapCatWsEndpointOptions {
|
|
|
53
58
|
export class NapCatWsEndpoint implements EndpointInstance {
|
|
54
59
|
readonly #options: NapCatWsEndpointOptions;
|
|
55
60
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
61
|
+
readonly management: EndpointManagement = createNapCatEndpointManagement(this);
|
|
62
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
56
63
|
#ws?: NapCatWsSocket;
|
|
57
|
-
#reconnectTimer?: NodeJS.Timeout;
|
|
58
|
-
#heartbeatTimer?: NodeJS.Timeout;
|
|
59
64
|
#requestId = { value: 0 };
|
|
60
65
|
#pending = new Map<string, NapCatPendingAction>();
|
|
61
66
|
#open = false;
|
|
62
|
-
#started = false;
|
|
63
|
-
#stopping = false;
|
|
64
67
|
#unregisterAgent?: () => void;
|
|
65
68
|
|
|
66
69
|
constructor(options: NapCatWsEndpointOptions) {
|
|
67
70
|
this.#options = options;
|
|
71
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
72
|
+
name: options.config.name,
|
|
73
|
+
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
74
|
+
reconnect: {
|
|
75
|
+
initialIntervalMs: options.config.reconnect_interval,
|
|
76
|
+
multiplier: 1,
|
|
77
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
78
|
+
jitterMs: 0,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
async start(): Promise<void> {
|
|
71
|
-
if (this.#started) return;
|
|
72
|
-
this.#started = true;
|
|
73
|
-
this.#stopping = false;
|
|
84
|
+
if (this.#lifecycle.started) return;
|
|
74
85
|
this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
|
|
75
86
|
try {
|
|
76
|
-
await this.#connect();
|
|
87
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
77
88
|
} catch (err) {
|
|
78
|
-
|
|
89
|
+
// start 失败复位由基座保证;agent 注册/反注册是适配器专有依赖,留在适配器侧
|
|
90
|
+
this.#unregisterAgent?.();
|
|
91
|
+
this.#unregisterAgent = undefined;
|
|
79
92
|
throw err;
|
|
80
93
|
}
|
|
81
94
|
}
|
|
@@ -90,18 +103,9 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
90
103
|
|
|
91
104
|
async stop(): Promise<void> {
|
|
92
105
|
this.#open = false;
|
|
93
|
-
this.#
|
|
94
|
-
this.#started = false;
|
|
106
|
+
await this.#lifecycle.stop();
|
|
95
107
|
this.#unregisterAgent?.();
|
|
96
108
|
this.#unregisterAgent = undefined;
|
|
97
|
-
if (this.#reconnectTimer) {
|
|
98
|
-
clearTimeout(this.#reconnectTimer);
|
|
99
|
-
this.#reconnectTimer = undefined;
|
|
100
|
-
}
|
|
101
|
-
if (this.#heartbeatTimer) {
|
|
102
|
-
clearInterval(this.#heartbeatTimer);
|
|
103
|
-
this.#heartbeatTimer = undefined;
|
|
104
|
-
}
|
|
105
109
|
rejectAllPending(this.#pending);
|
|
106
110
|
this.#inboundDeduper.clear();
|
|
107
111
|
if (this.#ws) {
|
|
@@ -112,7 +116,6 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
112
116
|
}
|
|
113
117
|
this.#ws = undefined;
|
|
114
118
|
}
|
|
115
|
-
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
@@ -348,7 +351,7 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
348
351
|
});
|
|
349
352
|
}
|
|
350
353
|
|
|
351
|
-
async #connect(): Promise<void> {
|
|
354
|
+
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
352
355
|
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
353
356
|
const create = this.#options.createWebSocket
|
|
354
357
|
?? ((connectUrl: string, options: NapCatWsCreateOptions) =>
|
|
@@ -358,6 +361,13 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
358
361
|
let settled = false;
|
|
359
362
|
const ws = create(url, { headers });
|
|
360
363
|
this.#ws = ws;
|
|
364
|
+
handle.onForceClose(() => {
|
|
365
|
+
try {
|
|
366
|
+
ws.close();
|
|
367
|
+
} catch {
|
|
368
|
+
/* ignore */
|
|
369
|
+
}
|
|
370
|
+
});
|
|
361
371
|
|
|
362
372
|
ws.on('open', () => {
|
|
363
373
|
if (settled) return;
|
|
@@ -374,13 +384,16 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
374
384
|
mode: 'ws',
|
|
375
385
|
url: safeUrl,
|
|
376
386
|
}));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
// stop-during-connect 竞态:已停止则不再武装心跳(基座 stop 已清理定时器)
|
|
388
|
+
if (this.#lifecycle.started) {
|
|
389
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
390
|
+
try {
|
|
391
|
+
ws.ping?.();
|
|
392
|
+
} catch {
|
|
393
|
+
/* ignore */
|
|
394
|
+
}
|
|
395
|
+
}, this.#options.config.heartbeat_interval);
|
|
396
|
+
}
|
|
384
397
|
resolve();
|
|
385
398
|
});
|
|
386
399
|
|
|
@@ -411,11 +424,12 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
411
424
|
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
412
425
|
reconnect_ms: this.#options.config.reconnect_interval,
|
|
413
426
|
}));
|
|
427
|
+
// 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
428
|
+
handle.notifyClosed(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
|
|
414
429
|
if (!settled) {
|
|
415
430
|
settled = true;
|
|
416
431
|
reject(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
|
|
417
432
|
}
|
|
418
|
-
this.#scheduleReconnect();
|
|
419
433
|
});
|
|
420
434
|
|
|
421
435
|
ws.on('error', (err) => {
|
|
@@ -433,20 +447,4 @@ export class NapCatWsEndpoint implements EndpointInstance {
|
|
|
433
447
|
});
|
|
434
448
|
});
|
|
435
449
|
}
|
|
436
|
-
|
|
437
|
-
#scheduleReconnect(): void {
|
|
438
|
-
if (this.#stopping || !this.#started || this.#reconnectTimer) return;
|
|
439
|
-
const delay = this.#options.config.reconnect_interval;
|
|
440
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
441
|
-
this.#reconnectTimer = undefined;
|
|
442
|
-
void this.#connect().catch((err) => {
|
|
443
|
-
logger.warn(formatCompact({
|
|
444
|
-
op: 'reconnect',
|
|
445
|
-
endpoint: this.#options.config.name,
|
|
446
|
-
ok: false,
|
|
447
|
-
error: err instanceof Error ? err.message : String(err),
|
|
448
|
-
}));
|
|
449
|
-
});
|
|
450
|
-
}, delay);
|
|
451
|
-
}
|
|
452
450
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
|
|
3
3
|
*/
|
|
4
4
|
import { clearInterval } from 'node:timers';
|
|
5
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
10
11
|
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
11
12
|
import {
|
|
12
13
|
InboundMessageDeduper,
|
|
@@ -50,6 +51,7 @@ export interface NapCatWssEndpointOptions {
|
|
|
50
51
|
export class NapCatWssEndpoint implements EndpointInstance {
|
|
51
52
|
readonly #options: NapCatWssEndpointOptions;
|
|
52
53
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
54
|
+
readonly management: EndpointManagement = createNapCatEndpointManagement(this);
|
|
53
55
|
#ws?: NapCatWsSocket;
|
|
54
56
|
#wsRelease?: () => void;
|
|
55
57
|
#heartbeatTimer?: NodeJS.Timeout;
|
package/plugin.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { registerDefaultScenePlatformPermitChecker } from '@zhin.js/core';
|
|
3
|
-
|
|
4
|
-
export default definePlugin({
|
|
5
|
-
name: 'napcat',
|
|
6
|
-
metadata: {
|
|
7
|
-
displayName: 'NapCat Adapter',
|
|
8
|
-
},
|
|
9
|
-
setup() {
|
|
10
|
-
// 平台权限门禁:scene_admin / scene_owner 由 sender role 判定(见各 endpoint admit metadata)
|
|
11
|
-
return registerDefaultScenePlatformPermitChecker('napcat');
|
|
12
|
-
},
|
|
13
|
-
});
|