@zhin.js/adapter-satori 5.0.13 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +20 -1
- package/adapters/satori.js +1 -7
- package/adapters/satori.ts +1 -7
- package/lib/client.d.ts +18 -0
- package/lib/client.js +21 -0
- package/lib/endpoint.d.ts +13 -22
- package/lib/endpoint.js +73 -54
- package/lib/index.d.ts +5 -1
- package/lib/index.js +3 -1
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +1 -1
- package/lib/satori-endpoint-commands.d.ts +1 -1
- package/lib/webhook.d.ts +2 -4
- package/lib/webhook.js +5 -30
- package/package.json +14 -11
- package/src/client.ts +57 -0
- package/src/endpoint.ts +94 -73
- package/src/index.ts +17 -1
- package/src/protocol.ts +1 -1
- package/src/webhook.ts +6 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @zhin.js/adapter-satori
|
|
2
2
|
|
|
3
|
+
## 6.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4e8117c: Expose the published imhelper protocol Clients from every Endpoint, preserve native action responses, and route host-owned HTTP, WebSocket, and raw ingress through the Client APIs. Add the shared `ClientEndpoint` deep base so concrete transports inherit one open-gated Client event bridge, and move reverse WebSocket lifecycle and heartbeat cleanup onto the common lifecycle state machine.
|
|
8
|
+
- 1fc78bc: Unify native platform Client access behind the literal `adapter` discriminant. Handlers infer both native events and Clients, while command, inbound/outbound middleware, and both Agent tool authoring surfaces expose the exact operation-scoped Client through a lazy `$client` getter. Definitions without `adapter` keep `$client` typed as `unknown`, and runtime dispatch rejects adapter mismatches before resolving the Client. Bundled platform tools now use this single path instead of model-provided endpoint ids and adapter-specific dependency wrappers. Every adapter registers one Client/EventMap contract, and protocol adapters including NapCat, Milky, OneBot and Satori now produce transport-independent Client objects rather than letting Endpoint instances impersonate Clients.
|
|
9
|
+
- 2ca9a52: Upgrade the protocol Clients to imhelper 1.0.7, keep receiver ownership exclusively in Endpoint through manual mode, expose structured protocol errors, and support canonical non-message Client events.
|
|
10
|
+
- Updated dependencies [4e8117c]
|
|
11
|
+
- Updated dependencies [54bfd6b]
|
|
12
|
+
- Updated dependencies [12025ee]
|
|
13
|
+
- Updated dependencies [09b14d6]
|
|
14
|
+
- Updated dependencies [1fc78bc]
|
|
15
|
+
- @zhin.js/adapter@1.2.1
|
|
16
|
+
- @zhin.js/core@1.5.14
|
|
17
|
+
- @zhin.js/host-http@1.0.13
|
|
18
|
+
- @zhin.js/command@1.0.16
|
|
19
|
+
- @zhin.js/logger@1.0.77
|
|
20
|
+
- zhin.js@6.0.14
|
|
21
|
+
- @zhin.js/feature-kit@1.0.13
|
|
22
|
+
|
|
23
|
+
## 6.0.0
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- f2c532f: Expose exact per-Endpoint message operations through one validated Adapter capability model, route Core control calls through declared active capabilities, and connect existing recall, edit, reaction, and typing implementations across platform adapters.
|
|
28
|
+
- Updated dependencies [b10d058]
|
|
29
|
+
- Updated dependencies [f2c532f]
|
|
30
|
+
- @zhin.js/host-http@1.0.12
|
|
31
|
+
- @zhin.js/adapter@1.2.0
|
|
32
|
+
- @zhin.js/core@1.5.13
|
|
33
|
+
- zhin.js@6.0.13
|
|
34
|
+
|
|
3
35
|
## 5.0.13
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ pnpm add @zhin.js/adapter-satori
|
|
|
22
22
|
## Plugin Runtime
|
|
23
23
|
|
|
24
24
|
- `@zhin.js/adapter` — 约定式 `adapters/satori.ts`(`defineAdapter`)
|
|
25
|
-
- `@zhin.js/core` — `
|
|
25
|
+
- `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
|
|
26
26
|
- `zhin.js` — `plugin.ts`(`definePlugin`)
|
|
27
27
|
- `@zhin.js/host-http` — Webhook 模式需 `httpHostToken` 注册 POST 路由
|
|
28
28
|
- 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`(`baseUrl` / `token` / …)
|
|
@@ -32,6 +32,16 @@ pnpm add @zhin.js/adapter-satori
|
|
|
32
32
|
|
|
33
33
|
入站 `metadata.mentioned`:消息 content 中 `<at id="…"/>` 元素的 id 等于登录 selfId(READY/事件 `login.user.id`)时置 `true`。
|
|
34
34
|
|
|
35
|
+
每个 Endpoint 的 `$client` 是 `@imhelper/satori-v1` 的 `SatoriV1Client`。业务代码直接调用
|
|
36
|
+
`$client.call(resource, method, params)` 或其公开便捷方法;Client 负责协议事件变换,Endpoint
|
|
37
|
+
只负责 Satori IDENTIFY/心跳、Webhook 鉴权和 Zhin 投影。
|
|
38
|
+
|
|
39
|
+
## 前置条件
|
|
40
|
+
|
|
41
|
+
1. 准备兼容 Satori 的服务端,记录 API Base、平台标识和用户标识。
|
|
42
|
+
2. 正向 WS 需 Zhin 可达 Satori 服务;Webhook 需服务端可达 Zhin HTTP Host。
|
|
43
|
+
3. 若服务启用鉴权,在两端配置相同的 Bearer token。
|
|
44
|
+
|
|
35
45
|
## 最小配置
|
|
36
46
|
|
|
37
47
|
```yaml
|
|
@@ -89,3 +99,12 @@ SDK 会向 `path` 发送 POST,请求头 `Satori-Opcode: 0` 表示事件;适
|
|
|
89
99
|
- [介绍](https://satori.chat/zh-CN/introduction.html)
|
|
90
100
|
- [总览](https://satori.chat/zh-CN/protocol/overview.html)
|
|
91
101
|
- [API](https://satori.chat/zh-CN/protocol/api.html)
|
|
102
|
+
|
|
103
|
+
## 故障排查
|
|
104
|
+
|
|
105
|
+
| 现象 | 排查 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| WS IDENTIFY 失败 | 检查 `baseUrl`、平台/用户标识与 token |
|
|
108
|
+
| Webhook 无事件 | 确认路径、`Satori-Opcode: 0` 与 HTTP Host |
|
|
109
|
+
| 401 | 检查 API、WS IDENTIFY 与 Webhook 使用的 token 是否一致 |
|
|
110
|
+
| 发送目标错误 | 核对 `channel_id` 与 Conversation kind;DIRECT 才映射为 private |
|
package/adapters/satori.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/satori.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
7
|
import { SatoriWebhookEndpoint, SatoriWsEndpoint } from "../lib/endpoint.js";
|
|
9
8
|
import { resolveSatoriConfig, } from "../lib/protocol.js";
|
|
@@ -11,6 +10,7 @@ import { satoriRuntimeStateToken } from "../lib/satori-runtime-state.js";
|
|
|
11
10
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint } from "../lib/endpoint.js";
|
|
12
11
|
export default defineAdapter({
|
|
13
12
|
capabilities: ['inbound', 'outbound'],
|
|
13
|
+
operations: ['recall'],
|
|
14
14
|
// Satori 协议 img/file 元素消费 url 与 base64 内联数据;无卡片交互面,交互段降级纯文本。
|
|
15
15
|
segments: {
|
|
16
16
|
outboundMedia: ['url', 'base64'],
|
|
@@ -18,8 +18,6 @@ export default defineAdapter({
|
|
|
18
18
|
},
|
|
19
19
|
create(context) {
|
|
20
20
|
const config = resolveSatoriConfig(context.config);
|
|
21
|
-
const gateway = context.use(messageGatewayToken);
|
|
22
|
-
const sideEvents = context.use(sideEventGatewayToken);
|
|
23
21
|
// 注册到插件运行时状态(satori.endpoint list 的"运行中"数据源)
|
|
24
22
|
context.use(satoriRuntimeStateToken).endpoints.set(config.id, {
|
|
25
23
|
id: config.id,
|
|
@@ -28,16 +26,12 @@ export default defineAdapter({
|
|
|
28
26
|
if (config.connection === 'webhook') {
|
|
29
27
|
return new SatoriWebhookEndpoint({
|
|
30
28
|
id: context.id,
|
|
31
|
-
gateway,
|
|
32
|
-
sideEvents,
|
|
33
29
|
http: context.use(httpHostToken),
|
|
34
30
|
config,
|
|
35
31
|
});
|
|
36
32
|
}
|
|
37
33
|
return new SatoriWsEndpoint({
|
|
38
34
|
id: context.id,
|
|
39
|
-
gateway,
|
|
40
|
-
sideEvents,
|
|
41
35
|
config,
|
|
42
36
|
});
|
|
43
37
|
},
|
package/adapters/satori.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/satori.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
5
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
6
|
import { SatoriWebhookEndpoint, SatoriWsEndpoint } from '../src/endpoint.js';
|
|
8
7
|
import {
|
|
@@ -22,6 +21,7 @@ export type {
|
|
|
22
21
|
|
|
23
22
|
export default defineAdapter<SatoriAdapterConfig>({
|
|
24
23
|
capabilities: ['inbound', 'outbound'],
|
|
24
|
+
operations: ['recall'],
|
|
25
25
|
// Satori 协议 img/file 元素消费 url 与 base64 内联数据;无卡片交互面,交互段降级纯文本。
|
|
26
26
|
segments: {
|
|
27
27
|
outboundMedia: ['url', 'base64'],
|
|
@@ -29,8 +29,6 @@ export default defineAdapter<SatoriAdapterConfig>({
|
|
|
29
29
|
},
|
|
30
30
|
create(context) {
|
|
31
31
|
const config = resolveSatoriConfig(context.config);
|
|
32
|
-
const gateway = context.use(messageGatewayToken);
|
|
33
|
-
const sideEvents = context.use(sideEventGatewayToken);
|
|
34
32
|
// 注册到插件运行时状态(satori.endpoint list 的"运行中"数据源)
|
|
35
33
|
context.use(satoriRuntimeStateToken).endpoints.set(config.id, {
|
|
36
34
|
id: config.id,
|
|
@@ -39,16 +37,12 @@ export default defineAdapter<SatoriAdapterConfig>({
|
|
|
39
37
|
if (config.connection === 'webhook') {
|
|
40
38
|
return new SatoriWebhookEndpoint({
|
|
41
39
|
id: context.id,
|
|
42
|
-
gateway,
|
|
43
|
-
sideEvents,
|
|
44
40
|
http: context.use(httpHostToken),
|
|
45
41
|
config,
|
|
46
42
|
});
|
|
47
43
|
}
|
|
48
44
|
return new SatoriWsEndpoint({
|
|
49
45
|
id: context.id,
|
|
50
|
-
gateway,
|
|
51
|
-
sideEvents,
|
|
52
46
|
config,
|
|
53
47
|
});
|
|
54
48
|
},
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SatoriV1Client, type SatoriV1Event } from '@imhelper/satori-v1';
|
|
2
|
+
import { type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
3
|
+
import { type ClientEventPayloads } from 'zhin.js/adapter';
|
|
4
|
+
import type { ResolvedSatoriConfig, SatoriApiOptions, callSatoriApi } from './protocol.js';
|
|
5
|
+
export { SatoriV1Client as SatoriClient } from '@imhelper/satori-v1';
|
|
6
|
+
type SatoriClientEvents = ImHelperEventMap<string, SatoriV1Event, EventMap<string>>;
|
|
7
|
+
export type SatoriClientEventMap = ClientEventPayloads<SatoriClientEvents>;
|
|
8
|
+
export declare function createSatoriEndpointClient(config: ResolvedSatoriConfig, request: typeof callSatoriApi, apiOptions: () => SatoriApiOptions): SatoriV1Client;
|
|
9
|
+
export declare function forwardSatoriClientEvents(client: SatoriV1Client, receive: (name: string, payload: unknown) => void): () => void;
|
|
10
|
+
declare module '@zhin.js/feature-kit' {
|
|
11
|
+
interface AdapterClientRegistry {
|
|
12
|
+
readonly satori: {
|
|
13
|
+
readonly client: SatoriV1Client;
|
|
14
|
+
readonly events: SatoriClientEventMap;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export declare const satoriClient: import("@zhin.js/adapter").EndpointClientToken<SatoriV1Client, ClientEventPayloads<SatoriClientEvents>>;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SatoriV1Client, } from '@imhelper/satori-v1';
|
|
2
|
+
import { EventFactory } from 'imhelper';
|
|
3
|
+
import { defineEndpointClient, forwardEndpointClientEvents, } from 'zhin.js/adapter';
|
|
4
|
+
export { SatoriV1Client as SatoriClient } from '@imhelper/satori-v1';
|
|
5
|
+
export function createSatoriEndpointClient(config, request, apiOptions) {
|
|
6
|
+
return new SatoriV1Client({
|
|
7
|
+
baseUrl: config.baseUrl,
|
|
8
|
+
selfId: config.id,
|
|
9
|
+
accessToken: config.token,
|
|
10
|
+
receiveMode: 'manual',
|
|
11
|
+
call: (resource, method, params) => request(apiOptions(), resource, method, params ?? {}),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const satoriClientEventNames = Object.freeze([
|
|
15
|
+
...EventFactory.getSupportedEventTypes(),
|
|
16
|
+
'event',
|
|
17
|
+
]);
|
|
18
|
+
export function forwardSatoriClientEvents(client, receive) {
|
|
19
|
+
return forwardEndpointClientEvents(client, satoriClientEventNames, receive);
|
|
20
|
+
}
|
|
21
|
+
export const satoriClient = defineEndpointClient('satori');
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,60 +1,51 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
1
2
|
/**
|
|
2
3
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
3
4
|
*/
|
|
4
|
-
import { type
|
|
5
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
5
|
+
import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
|
|
6
6
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
7
7
|
import type { CapabilityId } from 'zhin.js';
|
|
8
|
-
import { callSatoriApi, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig
|
|
8
|
+
import { callSatoriApi, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig } from './protocol.js';
|
|
9
|
+
import { type SatoriClient } from './client.js';
|
|
9
10
|
import { type CreateSatoriWebSocket } from './ws.js';
|
|
10
11
|
export type SatoriApiCaller = typeof callSatoriApi;
|
|
11
12
|
export interface SatoriWsEndpointOptions {
|
|
12
13
|
readonly id: CapabilityId;
|
|
13
|
-
readonly gateway: MessageGateway;
|
|
14
|
-
readonly sideEvents?: SideEventGateway;
|
|
15
14
|
readonly config: ResolvedSatoriWsConfig;
|
|
16
15
|
readonly createWebSocket?: CreateSatoriWebSocket;
|
|
17
16
|
readonly callApi?: SatoriApiCaller;
|
|
18
17
|
}
|
|
19
|
-
export declare class SatoriWsEndpoint
|
|
18
|
+
export declare class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
|
|
20
19
|
#private;
|
|
20
|
+
readonly client: SatoriClient;
|
|
21
21
|
readonly management: EndpointManagement;
|
|
22
|
+
readonly control: EndpointControl;
|
|
22
23
|
constructor(options: SatoriWsEndpointOptions);
|
|
23
24
|
start(): Promise<void>;
|
|
24
|
-
open(): void;
|
|
25
|
-
close(): void;
|
|
26
25
|
stop(): Promise<void>;
|
|
27
26
|
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
28
|
-
|
|
29
|
-
admit(body: SatoriEventBody): void;
|
|
30
|
-
recall(id: string): Promise<void>;
|
|
31
|
-
/** Test helper: inject a READY login without a live socket. */
|
|
32
|
-
setLogin(login: SatoriLogin): void;
|
|
27
|
+
recall(id: string, fallbackChannelId?: string): Promise<void>;
|
|
33
28
|
}
|
|
34
29
|
export interface SatoriWebhookEndpointOptions {
|
|
35
30
|
readonly id: CapabilityId;
|
|
36
|
-
readonly gateway: MessageGateway;
|
|
37
|
-
readonly sideEvents?: SideEventGateway;
|
|
38
31
|
readonly http: HttpHost;
|
|
39
32
|
readonly config: ResolvedSatoriWebhookConfig;
|
|
40
33
|
readonly callApi?: SatoriApiCaller;
|
|
41
34
|
}
|
|
42
|
-
export declare class SatoriWebhookEndpoint
|
|
35
|
+
export declare class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
|
|
43
36
|
#private;
|
|
37
|
+
readonly client: SatoriClient;
|
|
44
38
|
readonly management: EndpointManagement;
|
|
39
|
+
readonly control: EndpointControl;
|
|
45
40
|
constructor(options: SatoriWebhookEndpointOptions);
|
|
46
41
|
/** Used by webhook handler. */
|
|
47
42
|
get isOpen(): boolean;
|
|
48
43
|
get config(): ResolvedSatoriWebhookConfig;
|
|
44
|
+
acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
49
45
|
start(): Promise<void>;
|
|
50
|
-
open(): void;
|
|
51
|
-
close(): void;
|
|
52
46
|
stop(): Promise<void>;
|
|
53
47
|
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
54
|
-
|
|
55
|
-
recall(id: string): Promise<void>;
|
|
56
|
-
/** Test helper: inject login without a live webhook push. */
|
|
57
|
-
setLogin(login: SatoriLogin): void;
|
|
48
|
+
recall(id: string, fallbackChannelId?: string): Promise<void>;
|
|
58
49
|
}
|
|
59
50
|
export type SatoriManagementApi = (resource: string, method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
60
51
|
/**
|
package/lib/endpoint.js
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
3
3
|
*/
|
|
4
|
-
import { createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
4
|
+
import { ClientEndpoint, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
5
5
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
6
|
import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, satoriInboundConversation, } from './protocol.js';
|
|
7
7
|
import { registerSatoriWebhookRoutes } from './webhook.js';
|
|
8
|
+
import { createSatoriEndpointClient, forwardSatoriClientEvents } from './client.js';
|
|
8
9
|
import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
9
|
-
export class SatoriWsEndpoint {
|
|
10
|
+
export class SatoriWsEndpoint extends ClientEndpoint {
|
|
11
|
+
client;
|
|
10
12
|
#logger;
|
|
11
13
|
#options;
|
|
12
14
|
#lifecycle;
|
|
13
15
|
#ws = null;
|
|
14
16
|
#login;
|
|
15
17
|
#lastSn;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
management;
|
|
19
|
+
control = Object.freeze({
|
|
20
|
+
recall: (message) => this.recall(message.id, message.conversation.id),
|
|
21
|
+
});
|
|
18
22
|
constructor(options) {
|
|
23
|
+
super();
|
|
19
24
|
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
20
25
|
this.#options = options;
|
|
26
|
+
this.client = createSatoriEndpointClient(options.config, options.callApi ?? callSatoriApi, () => this.#apiOptions());
|
|
27
|
+
this.management = createSatoriEndpointManagement((resource, method, params) => this.client.call(resource, method, params));
|
|
28
|
+
this.bindClientEvents((receive) => forwardSatoriClientEvents(this.client, receive), (name, payload) => {
|
|
29
|
+
if (name === 'event')
|
|
30
|
+
this.#admitRaw(payload);
|
|
31
|
+
}, (name, error) => this.#warnPlatformEvent(name, error));
|
|
21
32
|
const { config } = options;
|
|
22
33
|
this.#lifecycle = createEndpointLifecycle({
|
|
23
34
|
name: config.id,
|
|
@@ -53,21 +64,15 @@ export class SatoriWsEndpoint {
|
|
|
53
64
|
throw err;
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
|
-
open() {
|
|
57
|
-
this.#open = true;
|
|
58
|
-
}
|
|
59
|
-
close() {
|
|
60
|
-
this.#open = false;
|
|
61
|
-
}
|
|
62
67
|
async stop() {
|
|
63
|
-
this
|
|
68
|
+
this.close();
|
|
64
69
|
// 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
|
|
65
70
|
await this.#lifecycle.stop();
|
|
66
71
|
this.#ws = null;
|
|
67
72
|
}
|
|
68
73
|
async send({ conversation, payload }) {
|
|
69
74
|
const content = formatSatoriOutbound(payload);
|
|
70
|
-
const result = await this
|
|
75
|
+
const result = await this.client.call('message', 'create', {
|
|
71
76
|
channel_id: conversation.id,
|
|
72
77
|
content,
|
|
73
78
|
});
|
|
@@ -80,10 +85,7 @@ export class SatoriWsEndpoint {
|
|
|
80
85
|
}));
|
|
81
86
|
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
82
87
|
}
|
|
83
|
-
|
|
84
|
-
admit(body) {
|
|
85
|
-
if (!this.#open)
|
|
86
|
-
return;
|
|
88
|
+
#admitRaw(body) {
|
|
87
89
|
if (body.login && !this.#login)
|
|
88
90
|
this.#login = body.login;
|
|
89
91
|
if (!isMessageEvent(body))
|
|
@@ -93,7 +95,7 @@ export class SatoriWsEndpoint {
|
|
|
93
95
|
const sender = resolveInboundSender(body);
|
|
94
96
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
95
97
|
const mentioned = isSelfMentioned(body, selfId);
|
|
96
|
-
void this
|
|
98
|
+
void this.emit('message.receive', {
|
|
97
99
|
conversation,
|
|
98
100
|
message: { conversation, id: body.message.id },
|
|
99
101
|
content,
|
|
@@ -114,16 +116,19 @@ export class SatoriWsEndpoint {
|
|
|
114
116
|
}));
|
|
115
117
|
});
|
|
116
118
|
}
|
|
117
|
-
async recall(id) {
|
|
119
|
+
async recall(id, fallbackChannelId) {
|
|
118
120
|
const { channelId, messageId } = parseMessageRef(id);
|
|
119
|
-
await this
|
|
120
|
-
channel_id: channelId,
|
|
121
|
+
await this.client.call('message', 'delete', {
|
|
122
|
+
channel_id: channelId || fallbackChannelId || '',
|
|
121
123
|
message_id: messageId,
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
#warnPlatformEvent(name, error) {
|
|
127
|
+
this.#logger.warn(formatCompact({
|
|
128
|
+
op: 'satori_platform_event_failed',
|
|
129
|
+
event: name,
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
}));
|
|
127
132
|
}
|
|
128
133
|
async #connect(handle) {
|
|
129
134
|
const { config } = this.#options;
|
|
@@ -206,11 +211,25 @@ export class SatoriWsEndpoint {
|
|
|
206
211
|
if (signal.op === SatoriOpcode.PONG) {
|
|
207
212
|
// 喂狗:复位基座看门狗计数
|
|
208
213
|
this.#lifecycle.notifyHeartbeatAck();
|
|
214
|
+
void this.emitPlatform('pong', signal).catch((error) => {
|
|
215
|
+
this.#logger.warn(formatCompact({
|
|
216
|
+
op: 'satori_platform_event_failed',
|
|
217
|
+
event: 'pong',
|
|
218
|
+
error: error instanceof Error ? error.message : String(error),
|
|
219
|
+
}));
|
|
220
|
+
});
|
|
209
221
|
return;
|
|
210
222
|
}
|
|
211
223
|
if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
|
|
212
224
|
const logins = signal.body.logins;
|
|
213
225
|
this.#login = logins[0];
|
|
226
|
+
void this.emitPlatform('ready', signal).catch((error) => {
|
|
227
|
+
this.#logger.warn(formatCompact({
|
|
228
|
+
op: 'satori_platform_event_failed',
|
|
229
|
+
event: 'ready',
|
|
230
|
+
error: error instanceof Error ? error.message : String(error),
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
214
233
|
if (!this.#login?.platform || !this.#login?.user?.id) {
|
|
215
234
|
this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
216
235
|
}
|
|
@@ -219,7 +238,7 @@ export class SatoriWsEndpoint {
|
|
|
219
238
|
if (signal.op === SatoriOpcode.EVENT && signal.body) {
|
|
220
239
|
if (typeof signal.body.sn === 'number')
|
|
221
240
|
this.#lastSn = signal.body.sn;
|
|
222
|
-
this.
|
|
241
|
+
this.client.ingest(signal.body);
|
|
223
242
|
}
|
|
224
243
|
}
|
|
225
244
|
#sendSignal(op, body) {
|
|
@@ -235,30 +254,39 @@ export class SatoriWsEndpoint {
|
|
|
235
254
|
token: this.#options.config.token,
|
|
236
255
|
};
|
|
237
256
|
}
|
|
238
|
-
#api(resource, method, params) {
|
|
239
|
-
const call = this.#options.callApi ?? callSatoriApi;
|
|
240
|
-
return call(this.#apiOptions(), resource, method, params);
|
|
241
|
-
}
|
|
242
257
|
}
|
|
243
|
-
export class SatoriWebhookEndpoint {
|
|
258
|
+
export class SatoriWebhookEndpoint extends ClientEndpoint {
|
|
259
|
+
client;
|
|
244
260
|
#logger;
|
|
245
261
|
#options;
|
|
246
262
|
#login;
|
|
247
263
|
#routeReleases = [];
|
|
248
|
-
#open = false;
|
|
249
264
|
#started = false;
|
|
250
|
-
management
|
|
265
|
+
management;
|
|
266
|
+
control = Object.freeze({
|
|
267
|
+
recall: (message) => this.recall(message.id, message.conversation.id),
|
|
268
|
+
});
|
|
251
269
|
constructor(options) {
|
|
270
|
+
super();
|
|
252
271
|
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
253
272
|
this.#options = options;
|
|
273
|
+
this.client = createSatoriEndpointClient(options.config, options.callApi ?? callSatoriApi, () => this.#apiOptions());
|
|
274
|
+
this.management = createSatoriEndpointManagement((resource, method, params) => this.client.call(resource, method, params));
|
|
275
|
+
this.bindClientEvents((receive) => forwardSatoriClientEvents(this.client, receive), (name, payload) => {
|
|
276
|
+
if (name === 'event')
|
|
277
|
+
this.#admitRaw(payload);
|
|
278
|
+
}, (name, error) => this.#warnPlatformEvent(name, error));
|
|
254
279
|
}
|
|
255
280
|
/** Used by webhook handler. */
|
|
256
281
|
get isOpen() {
|
|
257
|
-
return this
|
|
282
|
+
return this.clientEventsOpen;
|
|
258
283
|
}
|
|
259
284
|
get config() {
|
|
260
285
|
return this.#options.config;
|
|
261
286
|
}
|
|
287
|
+
async acceptHttp(request, response) {
|
|
288
|
+
await this.client.acceptHttp(request, response);
|
|
289
|
+
}
|
|
262
290
|
async start() {
|
|
263
291
|
if (this.#started)
|
|
264
292
|
return;
|
|
@@ -280,14 +308,8 @@ export class SatoriWebhookEndpoint {
|
|
|
280
308
|
path: this.#options.config.path,
|
|
281
309
|
}));
|
|
282
310
|
}
|
|
283
|
-
open() {
|
|
284
|
-
this.#open = true;
|
|
285
|
-
}
|
|
286
|
-
close() {
|
|
287
|
-
this.#open = false;
|
|
288
|
-
}
|
|
289
311
|
async stop() {
|
|
290
|
-
this
|
|
312
|
+
this.close();
|
|
291
313
|
for (const release of this.#routeReleases.splice(0))
|
|
292
314
|
release();
|
|
293
315
|
this.#started = false;
|
|
@@ -297,7 +319,7 @@ export class SatoriWebhookEndpoint {
|
|
|
297
319
|
}
|
|
298
320
|
async send({ conversation, payload }) {
|
|
299
321
|
const content = formatSatoriOutbound(payload);
|
|
300
|
-
const result = await this
|
|
322
|
+
const result = await this.client.call('message', 'create', {
|
|
301
323
|
channel_id: conversation.id,
|
|
302
324
|
content,
|
|
303
325
|
});
|
|
@@ -310,9 +332,7 @@ export class SatoriWebhookEndpoint {
|
|
|
310
332
|
}));
|
|
311
333
|
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
312
334
|
}
|
|
313
|
-
|
|
314
|
-
if (!this.#open)
|
|
315
|
-
return;
|
|
335
|
+
#admitRaw(body) {
|
|
316
336
|
if (body.login && !this.#login)
|
|
317
337
|
this.#login = body.login;
|
|
318
338
|
if (!isMessageEvent(body))
|
|
@@ -322,7 +342,7 @@ export class SatoriWebhookEndpoint {
|
|
|
322
342
|
const sender = resolveInboundSender(body);
|
|
323
343
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
324
344
|
const mentioned = isSelfMentioned(body, selfId);
|
|
325
|
-
void this
|
|
345
|
+
void this.emit('message.receive', {
|
|
326
346
|
conversation,
|
|
327
347
|
message: { conversation, id: body.message.id },
|
|
328
348
|
content,
|
|
@@ -343,16 +363,19 @@ export class SatoriWebhookEndpoint {
|
|
|
343
363
|
}));
|
|
344
364
|
});
|
|
345
365
|
}
|
|
346
|
-
async recall(id) {
|
|
366
|
+
async recall(id, fallbackChannelId) {
|
|
347
367
|
const { channelId, messageId } = parseMessageRef(id);
|
|
348
|
-
await this
|
|
349
|
-
channel_id: channelId,
|
|
368
|
+
await this.client.call('message', 'delete', {
|
|
369
|
+
channel_id: channelId || fallbackChannelId || '',
|
|
350
370
|
message_id: messageId,
|
|
351
371
|
});
|
|
352
372
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
#warnPlatformEvent(name, error) {
|
|
374
|
+
this.#logger.warn(formatCompact({
|
|
375
|
+
op: 'satori_platform_event_failed',
|
|
376
|
+
event: name,
|
|
377
|
+
error: error instanceof Error ? error.message : String(error),
|
|
378
|
+
}));
|
|
356
379
|
}
|
|
357
380
|
#apiOptions() {
|
|
358
381
|
return {
|
|
@@ -362,10 +385,6 @@ export class SatoriWebhookEndpoint {
|
|
|
362
385
|
token: this.#options.config.token,
|
|
363
386
|
};
|
|
364
387
|
}
|
|
365
|
-
#api(resource, method, params) {
|
|
366
|
-
const call = this.#options.callApi ?? callSatoriApi;
|
|
367
|
-
return call(this.#apiOptions(), resource, method, params);
|
|
368
|
-
}
|
|
369
388
|
}
|
|
370
389
|
function isPrivateChannelType(body) {
|
|
371
390
|
const channel = body.channel ?? body.message?.channel;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriAdapterConfig, type SatoriApiOptions, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, type CreateSatoriWebSocket, type SatoriApiCaller, type SatoriWebhookEndpointOptions, type SatoriWsEndpointOptions, type SatoriWsSocket, } from './endpoint.js';
|
|
3
|
-
export { handleSatoriWebhookRequest,
|
|
3
|
+
export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, type SatoriWebhookHandler, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
5
|
+
export { SatoriClient, satoriClient, type SatoriClientEventMap, } from './client.js';
|
|
6
|
+
export type { SatoriAdapterConfig as ImHelperSatoriAdapterConfig, SatoriActionUrlResolver, SatoriCall, SatoriV1ClientConfig, SatoriV1Event, SatoriV1Response, } from '@imhelper/satori-v1';
|
|
7
|
+
export { ProtocolError } from '@imhelper/satori-v1';
|
|
8
|
+
export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, } from './endpoint.js';
|
|
3
|
-
export { handleSatoriWebhookRequest,
|
|
3
|
+
export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
5
|
+
export { SatoriClient, satoriClient, } from './client.js';
|
|
6
|
+
export { ProtocolError } from '@imhelper/satori-v1';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -121,7 +121,7 @@ export declare function buildWsUrl(baseUrl: string, token?: string): string;
|
|
|
121
121
|
* @see https://satori.chat/en-US/protocol/api.html
|
|
122
122
|
*/
|
|
123
123
|
export declare function callSatoriApi<T = unknown>(options: SatoriApiOptions, resource: string, method: string, params?: Record<string, unknown>): Promise<T>;
|
|
124
|
-
/** Build inbound text for
|
|
124
|
+
/** Build inbound text for OutboundMessageService.receive. */
|
|
125
125
|
export declare function formatInboundContent(body: SatoriEventBody & {
|
|
126
126
|
message: SatoriMessage;
|
|
127
127
|
}): string;
|
package/lib/protocol.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function callSatoriApi(options, resource, method, params = {}) {
|
|
|
108
108
|
throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
/** Build inbound text for
|
|
111
|
+
/** Build inbound text for OutboundMessageService.receive. */
|
|
112
112
|
export function formatInboundContent(body) {
|
|
113
113
|
const content = body.message.content ?? '';
|
|
114
114
|
return typeof content === 'string' ? content : String(content);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const satoriEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
|
|
1
|
+
export declare const satoriEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
3
|
-
import { type ResolvedSatoriWebhookConfig
|
|
3
|
+
import { type ResolvedSatoriWebhookConfig } from './protocol.js';
|
|
4
4
|
export interface SatoriWebhookHandler {
|
|
5
5
|
readonly config: ResolvedSatoriWebhookConfig;
|
|
6
6
|
readonly isOpen: boolean;
|
|
7
|
-
|
|
8
|
-
setLogin(login: SatoriLogin): void;
|
|
7
|
+
acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
9
8
|
}
|
|
10
9
|
export declare function registerSatoriWebhookRoutes(http: HttpHost, handler: SatoriWebhookHandler): HttpRouteRegistration[];
|
|
11
10
|
export declare function handleSatoriWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: SatoriWebhookHandler): Promise<void>;
|
|
12
11
|
export declare function resolveSatoriOpcode(request: IncomingMessage): number | undefined;
|
|
13
12
|
export declare function verifySatoriToken(token: string | undefined, request: IncomingMessage): boolean;
|
|
14
|
-
export declare function readRequestBody(request: IncomingMessage): Promise<string>;
|
package/lib/webhook.js
CHANGED
|
@@ -26,24 +26,13 @@ export async function handleSatoriWebhookRequest(request, response, handler) {
|
|
|
26
26
|
response.end(JSON.stringify({ message: 'OK' }));
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
body = JSON.parse(raw);
|
|
29
|
+
if (handler.isOpen) {
|
|
30
|
+
await handler.acceptHttp(request, response);
|
|
33
31
|
}
|
|
34
|
-
|
|
35
|
-
response.writeHead(
|
|
36
|
-
response.end(JSON.stringify({
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (opcode === SatoriOpcode.EVENT && handler.isOpen) {
|
|
40
|
-
handler.admit(body);
|
|
41
|
-
}
|
|
42
|
-
else if (opcode === SatoriOpcode.META && body.login && handler.isOpen) {
|
|
43
|
-
handler.setLogin(body.login);
|
|
32
|
+
else {
|
|
33
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
34
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
44
35
|
}
|
|
45
|
-
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
46
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
47
36
|
}
|
|
48
37
|
catch (error) {
|
|
49
38
|
logger.error('Satori webhook error:', error);
|
|
@@ -69,17 +58,3 @@ export function verifySatoriToken(token, request) {
|
|
|
69
58
|
const actual = Buffer.from(auth, 'utf8');
|
|
70
59
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
71
60
|
}
|
|
72
|
-
export async function readRequestBody(request) {
|
|
73
|
-
const chunks = [];
|
|
74
|
-
let size = 0;
|
|
75
|
-
for await (const chunk of request) {
|
|
76
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
77
|
-
size += buffer.length;
|
|
78
|
-
if (size > 1_048_576) {
|
|
79
|
-
request.destroy();
|
|
80
|
-
throw new Error('Request body exceeds 1MB');
|
|
81
|
-
}
|
|
82
|
-
chunks.push(buffer);
|
|
83
|
-
}
|
|
84
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
85
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-satori",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Zhin.js Satori adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -38,26 +38,29 @@
|
|
|
38
38
|
"directory": "plugins/adapters/satori"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@imhelper/satori-v1": "1.0.7",
|
|
42
|
+
"imhelper": "1.0.7",
|
|
41
43
|
"ws": "^8.21.1",
|
|
42
|
-
"@zhin.js/adapter": "1.1
|
|
43
|
-
"@zhin.js/core": "1.5.
|
|
44
|
-
"@zhin.js/
|
|
44
|
+
"@zhin.js/adapter": "1.2.1",
|
|
45
|
+
"@zhin.js/core": "1.5.14",
|
|
46
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
47
|
+
"@zhin.js/host-http": "1.0.13",
|
|
45
48
|
"@zhin.js/im-contract": "1.0.4",
|
|
46
|
-
"@zhin.js/logger": "1.0.
|
|
49
|
+
"@zhin.js/logger": "1.0.77"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@types/node": "^26.1.2",
|
|
50
53
|
"@types/ws": "^8.18.1",
|
|
51
54
|
"typescript": "^6.0.3",
|
|
52
55
|
"vitest": "^4.1.10",
|
|
53
|
-
"@zhin.js/host-http": "1.0.
|
|
54
|
-
"zhin.js": "6.0.
|
|
56
|
+
"@zhin.js/host-http": "1.0.13",
|
|
57
|
+
"zhin.js": "6.0.14"
|
|
55
58
|
},
|
|
56
59
|
"peerDependencies": {
|
|
57
|
-
"@zhin.js/adapter": "1.1
|
|
58
|
-
"@zhin.js/command": "1.0.
|
|
59
|
-
"@zhin.js/core": "1.5.
|
|
60
|
-
"zhin.js": "6.0.
|
|
60
|
+
"@zhin.js/adapter": "1.2.1",
|
|
61
|
+
"@zhin.js/command": "1.0.16",
|
|
62
|
+
"@zhin.js/core": "1.5.14",
|
|
63
|
+
"zhin.js": "6.0.14"
|
|
61
64
|
},
|
|
62
65
|
"peerDependenciesMeta": {
|
|
63
66
|
"@zhin.js/command": {
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SatoriV1Client,
|
|
3
|
+
type SatoriV1Event,
|
|
4
|
+
} from '@imhelper/satori-v1';
|
|
5
|
+
import { EventFactory, type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
6
|
+
import {
|
|
7
|
+
defineEndpointClient,
|
|
8
|
+
forwardEndpointClientEvents,
|
|
9
|
+
type ClientEventPayloads,
|
|
10
|
+
} from 'zhin.js/adapter';
|
|
11
|
+
import type {
|
|
12
|
+
ResolvedSatoriConfig,
|
|
13
|
+
SatoriApiOptions,
|
|
14
|
+
callSatoriApi,
|
|
15
|
+
} from './protocol.js';
|
|
16
|
+
|
|
17
|
+
export { SatoriV1Client as SatoriClient } from '@imhelper/satori-v1';
|
|
18
|
+
|
|
19
|
+
type SatoriClientEvents = ImHelperEventMap<string, SatoriV1Event, EventMap<string>>;
|
|
20
|
+
export type SatoriClientEventMap = ClientEventPayloads<SatoriClientEvents>;
|
|
21
|
+
|
|
22
|
+
export function createSatoriEndpointClient(
|
|
23
|
+
config: ResolvedSatoriConfig,
|
|
24
|
+
request: typeof callSatoriApi,
|
|
25
|
+
apiOptions: () => SatoriApiOptions,
|
|
26
|
+
): SatoriV1Client {
|
|
27
|
+
return new SatoriV1Client({
|
|
28
|
+
baseUrl: config.baseUrl,
|
|
29
|
+
selfId: config.id,
|
|
30
|
+
accessToken: config.token,
|
|
31
|
+
receiveMode: 'manual',
|
|
32
|
+
call: (resource, method, params) => request(apiOptions(), resource, method, params ?? {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const satoriClientEventNames = Object.freeze([
|
|
37
|
+
...EventFactory.getSupportedEventTypes<string>(),
|
|
38
|
+
'event',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function forwardSatoriClientEvents(
|
|
42
|
+
client: SatoriV1Client,
|
|
43
|
+
receive: (name: string, payload: unknown) => void,
|
|
44
|
+
): () => void {
|
|
45
|
+
return forwardEndpointClientEvents(client, satoriClientEventNames, receive);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module '@zhin.js/feature-kit' {
|
|
49
|
+
interface AdapterClientRegistry {
|
|
50
|
+
readonly satori: {
|
|
51
|
+
readonly client: SatoriV1Client;
|
|
52
|
+
readonly events: SatoriClientEventMap;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const satoriClient = defineEndpointClient<SatoriV1Client, SatoriClientEventMap>('satori');
|
package/src/endpoint.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
1
2
|
/**
|
|
2
3
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
3
4
|
*/
|
|
4
5
|
import {
|
|
6
|
+
ClientEndpoint,
|
|
5
7
|
createEndpointLifecycle,
|
|
6
8
|
type EndpointChannel,
|
|
7
9
|
type EndpointConnectHandle,
|
|
10
|
+
type EndpointControl,
|
|
8
11
|
type EndpointGroup,
|
|
9
|
-
type EndpointInstance,
|
|
10
12
|
type EndpointLifecycle,
|
|
11
13
|
type EndpointManagement,
|
|
12
14
|
type EndpointSendRequest,
|
|
13
15
|
} from 'zhin.js/adapter';
|
|
14
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
15
16
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
16
17
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
17
18
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
type SatoriSignal,
|
|
37
38
|
} from './protocol.js';
|
|
38
39
|
import { registerSatoriWebhookRoutes } from './webhook.js';
|
|
40
|
+
import { createSatoriEndpointClient, forwardSatoriClientEvents, type SatoriClient } from './client.js';
|
|
39
41
|
import {
|
|
40
42
|
WS_OPEN,
|
|
41
43
|
defaultCreateWebSocket,
|
|
@@ -47,14 +49,13 @@ export type SatoriApiCaller = typeof callSatoriApi;
|
|
|
47
49
|
|
|
48
50
|
export interface SatoriWsEndpointOptions {
|
|
49
51
|
readonly id: CapabilityId;
|
|
50
|
-
readonly gateway: MessageGateway;
|
|
51
|
-
readonly sideEvents?: SideEventGateway;
|
|
52
52
|
readonly config: ResolvedSatoriWsConfig;
|
|
53
53
|
readonly createWebSocket?: CreateSatoriWebSocket;
|
|
54
54
|
readonly callApi?: SatoriApiCaller;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export class SatoriWsEndpoint
|
|
57
|
+
export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
|
|
58
|
+
readonly client: SatoriClient;
|
|
58
59
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
59
60
|
|
|
60
61
|
readonly #options: SatoriWsEndpointOptions;
|
|
@@ -62,14 +63,30 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
62
63
|
#ws: SatoriWsSocket | null = null;
|
|
63
64
|
#login: SatoriLogin | undefined;
|
|
64
65
|
#lastSn: number | undefined;
|
|
65
|
-
|
|
66
|
-
readonly
|
|
67
|
-
(
|
|
68
|
-
);
|
|
66
|
+
readonly management: EndpointManagement;
|
|
67
|
+
readonly control: EndpointControl = Object.freeze<EndpointControl>({
|
|
68
|
+
recall: (message) => this.recall(message.id, message.conversation.id),
|
|
69
|
+
});
|
|
69
70
|
|
|
70
71
|
constructor(options: SatoriWsEndpointOptions) {
|
|
72
|
+
super();
|
|
71
73
|
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
72
74
|
this.#options = options;
|
|
75
|
+
this.client = createSatoriEndpointClient(
|
|
76
|
+
options.config,
|
|
77
|
+
options.callApi ?? callSatoriApi,
|
|
78
|
+
() => this.#apiOptions(),
|
|
79
|
+
);
|
|
80
|
+
this.management = createSatoriEndpointManagement(
|
|
81
|
+
(resource, method, params) => this.client.call(resource, method, params),
|
|
82
|
+
);
|
|
83
|
+
this.bindClientEvents(
|
|
84
|
+
(receive) => forwardSatoriClientEvents(this.client, receive),
|
|
85
|
+
(name, payload) => {
|
|
86
|
+
if (name === 'event') this.#admitRaw(payload as SatoriEventBody);
|
|
87
|
+
},
|
|
88
|
+
(name, error) => this.#warnPlatformEvent(name, error),
|
|
89
|
+
);
|
|
73
90
|
const { config } = options;
|
|
74
91
|
this.#lifecycle = createEndpointLifecycle({
|
|
75
92
|
name: config.id,
|
|
@@ -105,16 +122,8 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
open(): void {
|
|
109
|
-
this.#open = true;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
close(): void {
|
|
113
|
-
this.#open = false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
125
|
async stop(): Promise<void> {
|
|
117
|
-
this
|
|
126
|
+
this.close();
|
|
118
127
|
// 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
|
|
119
128
|
await this.#lifecycle.stop();
|
|
120
129
|
this.#ws = null;
|
|
@@ -122,7 +131,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
122
131
|
|
|
123
132
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
124
133
|
const content = formatSatoriOutbound(payload);
|
|
125
|
-
const result = await this
|
|
134
|
+
const result = await this.client.call('message', 'create', {
|
|
126
135
|
channel_id: conversation.id,
|
|
127
136
|
content,
|
|
128
137
|
});
|
|
@@ -136,9 +145,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
136
145
|
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
admit(body: SatoriEventBody): void {
|
|
141
|
-
if (!this.#open) return;
|
|
148
|
+
#admitRaw(body: SatoriEventBody): void {
|
|
142
149
|
if (body.login && !this.#login) this.#login = body.login;
|
|
143
150
|
if (!isMessageEvent(body)) return;
|
|
144
151
|
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
@@ -146,7 +153,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
146
153
|
const sender = resolveInboundSender(body);
|
|
147
154
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
148
155
|
const mentioned = isSelfMentioned(body, selfId);
|
|
149
|
-
void this
|
|
156
|
+
void this.emit('message.receive', {
|
|
150
157
|
conversation,
|
|
151
158
|
message: { conversation, id: body.message.id },
|
|
152
159
|
content,
|
|
@@ -168,17 +175,20 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
168
175
|
});
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
async recall(id: string): Promise<void> {
|
|
178
|
+
async recall(id: string, fallbackChannelId?: string): Promise<void> {
|
|
172
179
|
const { channelId, messageId } = parseMessageRef(id);
|
|
173
|
-
await this
|
|
174
|
-
channel_id: channelId,
|
|
180
|
+
await this.client.call('message', 'delete', {
|
|
181
|
+
channel_id: channelId || fallbackChannelId || '',
|
|
175
182
|
message_id: messageId,
|
|
176
183
|
});
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
#warnPlatformEvent(name: string, error: unknown): void {
|
|
187
|
+
this.#logger.warn(formatCompact({
|
|
188
|
+
op: 'satori_platform_event_failed',
|
|
189
|
+
event: name,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
}));
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
@@ -265,11 +275,25 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
265
275
|
if (signal.op === SatoriOpcode.PONG) {
|
|
266
276
|
// 喂狗:复位基座看门狗计数
|
|
267
277
|
this.#lifecycle.notifyHeartbeatAck();
|
|
278
|
+
void this.emitPlatform('pong', signal).catch((error) => {
|
|
279
|
+
this.#logger.warn(formatCompact({
|
|
280
|
+
op: 'satori_platform_event_failed',
|
|
281
|
+
event: 'pong',
|
|
282
|
+
error: error instanceof Error ? error.message : String(error),
|
|
283
|
+
}));
|
|
284
|
+
});
|
|
268
285
|
return;
|
|
269
286
|
}
|
|
270
287
|
if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
|
|
271
288
|
const logins = signal.body.logins as SatoriLogin[];
|
|
272
289
|
this.#login = logins[0];
|
|
290
|
+
void this.emitPlatform('ready', signal).catch((error) => {
|
|
291
|
+
this.#logger.warn(formatCompact({
|
|
292
|
+
op: 'satori_platform_event_failed',
|
|
293
|
+
event: 'ready',
|
|
294
|
+
error: error instanceof Error ? error.message : String(error),
|
|
295
|
+
}));
|
|
296
|
+
});
|
|
273
297
|
if (!this.#login?.platform || !this.#login?.user?.id) {
|
|
274
298
|
this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
275
299
|
}
|
|
@@ -277,7 +301,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
277
301
|
}
|
|
278
302
|
if (signal.op === SatoriOpcode.EVENT && signal.body) {
|
|
279
303
|
if (typeof signal.body.sn === 'number') this.#lastSn = signal.body.sn;
|
|
280
|
-
this.
|
|
304
|
+
this.client.ingest(signal.body as Parameters<SatoriClient['ingest']>[0]);
|
|
281
305
|
}
|
|
282
306
|
}
|
|
283
307
|
|
|
@@ -295,51 +319,62 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
295
319
|
};
|
|
296
320
|
}
|
|
297
321
|
|
|
298
|
-
#api(
|
|
299
|
-
resource: string,
|
|
300
|
-
method: string,
|
|
301
|
-
params: Record<string, unknown>,
|
|
302
|
-
): Promise<unknown> {
|
|
303
|
-
const call = this.#options.callApi ?? callSatoriApi;
|
|
304
|
-
return call(this.#apiOptions(), resource, method, params);
|
|
305
|
-
}
|
|
306
322
|
}
|
|
307
323
|
|
|
308
324
|
export interface SatoriWebhookEndpointOptions {
|
|
309
325
|
readonly id: CapabilityId;
|
|
310
|
-
readonly gateway: MessageGateway;
|
|
311
|
-
readonly sideEvents?: SideEventGateway;
|
|
312
326
|
readonly http: HttpHost;
|
|
313
327
|
readonly config: ResolvedSatoriWebhookConfig;
|
|
314
328
|
readonly callApi?: SatoriApiCaller;
|
|
315
329
|
}
|
|
316
330
|
|
|
317
|
-
export class SatoriWebhookEndpoint
|
|
331
|
+
export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
|
|
332
|
+
readonly client: SatoriClient;
|
|
318
333
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
319
334
|
|
|
320
335
|
readonly #options: SatoriWebhookEndpointOptions;
|
|
321
336
|
#login: SatoriLogin | undefined;
|
|
322
337
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
323
|
-
#open = false;
|
|
324
338
|
#started = false;
|
|
325
|
-
readonly management: EndpointManagement
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
readonly management: EndpointManagement;
|
|
340
|
+
readonly control: EndpointControl = Object.freeze<EndpointControl>({
|
|
341
|
+
recall: (message) => this.recall(message.id, message.conversation.id),
|
|
342
|
+
});
|
|
328
343
|
|
|
329
344
|
constructor(options: SatoriWebhookEndpointOptions) {
|
|
345
|
+
super();
|
|
330
346
|
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
331
347
|
this.#options = options;
|
|
348
|
+
this.client = createSatoriEndpointClient(
|
|
349
|
+
options.config,
|
|
350
|
+
options.callApi ?? callSatoriApi,
|
|
351
|
+
() => this.#apiOptions(),
|
|
352
|
+
);
|
|
353
|
+
this.management = createSatoriEndpointManagement(
|
|
354
|
+
(resource, method, params) => this.client.call(resource, method, params),
|
|
355
|
+
);
|
|
356
|
+
this.bindClientEvents(
|
|
357
|
+
(receive) => forwardSatoriClientEvents(this.client, receive),
|
|
358
|
+
(name, payload) => {
|
|
359
|
+
if (name === 'event') this.#admitRaw(payload as SatoriEventBody);
|
|
360
|
+
},
|
|
361
|
+
(name, error) => this.#warnPlatformEvent(name, error),
|
|
362
|
+
);
|
|
332
363
|
}
|
|
333
364
|
|
|
334
365
|
/** Used by webhook handler. */
|
|
335
366
|
get isOpen(): boolean {
|
|
336
|
-
return this
|
|
367
|
+
return this.clientEventsOpen;
|
|
337
368
|
}
|
|
338
369
|
|
|
339
370
|
get config(): ResolvedSatoriWebhookConfig {
|
|
340
371
|
return this.#options.config;
|
|
341
372
|
}
|
|
342
373
|
|
|
374
|
+
async acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
375
|
+
await this.client.acceptHttp(request, response);
|
|
376
|
+
}
|
|
377
|
+
|
|
343
378
|
async start(): Promise<void> {
|
|
344
379
|
if (this.#started) return;
|
|
345
380
|
this.#started = true;
|
|
@@ -361,16 +396,8 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
361
396
|
}));
|
|
362
397
|
}
|
|
363
398
|
|
|
364
|
-
open(): void {
|
|
365
|
-
this.#open = true;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
close(): void {
|
|
369
|
-
this.#open = false;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
399
|
async stop(): Promise<void> {
|
|
373
|
-
this
|
|
400
|
+
this.close();
|
|
374
401
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
375
402
|
this.#started = false;
|
|
376
403
|
this.#logger.debug(formatCompact({
|
|
@@ -380,7 +407,7 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
380
407
|
|
|
381
408
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
382
409
|
const content = formatSatoriOutbound(payload);
|
|
383
|
-
const result = await this
|
|
410
|
+
const result = await this.client.call('message', 'create', {
|
|
384
411
|
channel_id: conversation.id,
|
|
385
412
|
content,
|
|
386
413
|
});
|
|
@@ -394,8 +421,7 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
394
421
|
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
395
422
|
}
|
|
396
423
|
|
|
397
|
-
|
|
398
|
-
if (!this.#open) return;
|
|
424
|
+
#admitRaw(body: SatoriEventBody): void {
|
|
399
425
|
if (body.login && !this.#login) this.#login = body.login;
|
|
400
426
|
if (!isMessageEvent(body)) return;
|
|
401
427
|
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
@@ -403,7 +429,7 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
403
429
|
const sender = resolveInboundSender(body);
|
|
404
430
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
405
431
|
const mentioned = isSelfMentioned(body, selfId);
|
|
406
|
-
void this
|
|
432
|
+
void this.emit('message.receive', {
|
|
407
433
|
conversation,
|
|
408
434
|
message: { conversation, id: body.message.id },
|
|
409
435
|
content,
|
|
@@ -425,17 +451,20 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
425
451
|
});
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
async recall(id: string): Promise<void> {
|
|
454
|
+
async recall(id: string, fallbackChannelId?: string): Promise<void> {
|
|
429
455
|
const { channelId, messageId } = parseMessageRef(id);
|
|
430
|
-
await this
|
|
431
|
-
channel_id: channelId,
|
|
456
|
+
await this.client.call('message', 'delete', {
|
|
457
|
+
channel_id: channelId || fallbackChannelId || '',
|
|
432
458
|
message_id: messageId,
|
|
433
459
|
});
|
|
434
460
|
}
|
|
435
461
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
462
|
+
#warnPlatformEvent(name: string, error: unknown): void {
|
|
463
|
+
this.#logger.warn(formatCompact({
|
|
464
|
+
op: 'satori_platform_event_failed',
|
|
465
|
+
event: name,
|
|
466
|
+
error: error instanceof Error ? error.message : String(error),
|
|
467
|
+
}));
|
|
439
468
|
}
|
|
440
469
|
|
|
441
470
|
#apiOptions(): SatoriApiOptions {
|
|
@@ -447,14 +476,6 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
447
476
|
};
|
|
448
477
|
}
|
|
449
478
|
|
|
450
|
-
#api(
|
|
451
|
-
resource: string,
|
|
452
|
-
method: string,
|
|
453
|
-
params: Record<string, unknown>,
|
|
454
|
-
): Promise<unknown> {
|
|
455
|
-
const call = this.#options.callApi ?? callSatoriApi;
|
|
456
|
-
return call(this.#apiOptions(), resource, method, params);
|
|
457
|
-
}
|
|
458
479
|
}
|
|
459
480
|
|
|
460
481
|
function isPrivateChannelType(body: SatoriEventBody): boolean {
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,6 @@ export {
|
|
|
35
35
|
|
|
36
36
|
export {
|
|
37
37
|
handleSatoriWebhookRequest,
|
|
38
|
-
readRequestBody,
|
|
39
38
|
registerSatoriWebhookRoutes,
|
|
40
39
|
resolveSatoriOpcode,
|
|
41
40
|
verifySatoriToken,
|
|
@@ -46,3 +45,20 @@ export {
|
|
|
46
45
|
WS_OPEN,
|
|
47
46
|
defaultCreateWebSocket,
|
|
48
47
|
} from './ws.js';
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
SatoriClient,
|
|
51
|
+
satoriClient,
|
|
52
|
+
type SatoriClientEventMap,
|
|
53
|
+
} from './client.js';
|
|
54
|
+
|
|
55
|
+
export type {
|
|
56
|
+
SatoriAdapterConfig as ImHelperSatoriAdapterConfig,
|
|
57
|
+
SatoriActionUrlResolver,
|
|
58
|
+
SatoriCall,
|
|
59
|
+
SatoriV1ClientConfig,
|
|
60
|
+
SatoriV1Event,
|
|
61
|
+
SatoriV1Response,
|
|
62
|
+
} from '@imhelper/satori-v1';
|
|
63
|
+
export { ProtocolError } from '@imhelper/satori-v1';
|
|
64
|
+
export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
|
package/src/protocol.ts
CHANGED
|
@@ -222,7 +222,7 @@ export async function callSatoriApi<T = unknown>(
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
/** Build inbound text for
|
|
225
|
+
/** Build inbound text for OutboundMessageService.receive. */
|
|
226
226
|
export function formatInboundContent(body: SatoriEventBody & { message: SatoriMessage }): string {
|
|
227
227
|
const content = body.message.content ?? '';
|
|
228
228
|
return typeof content === 'string' ? content : String(content);
|
package/src/webhook.ts
CHANGED
|
@@ -8,8 +8,6 @@ import { getLogger } from '@zhin.js/logger';
|
|
|
8
8
|
import {
|
|
9
9
|
SatoriOpcode,
|
|
10
10
|
type ResolvedSatoriWebhookConfig,
|
|
11
|
-
type SatoriEventBody,
|
|
12
|
-
type SatoriLogin,
|
|
13
11
|
} from './protocol.js';
|
|
14
12
|
|
|
15
13
|
const logger = getLogger('satori');
|
|
@@ -17,8 +15,7 @@ const logger = getLogger('satori');
|
|
|
17
15
|
export interface SatoriWebhookHandler {
|
|
18
16
|
readonly config: ResolvedSatoriWebhookConfig;
|
|
19
17
|
readonly isOpen: boolean;
|
|
20
|
-
|
|
21
|
-
setLogin(login: SatoriLogin): void;
|
|
18
|
+
acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
export function registerSatoriWebhookRoutes(
|
|
@@ -50,22 +47,12 @@ export async function handleSatoriWebhookRequest(
|
|
|
50
47
|
response.end(JSON.stringify({ message: 'OK' }));
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
59
|
-
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (opcode === SatoriOpcode.EVENT && handler.isOpen) {
|
|
63
|
-
handler.admit(body);
|
|
64
|
-
} else if (opcode === SatoriOpcode.META && body.login && handler.isOpen) {
|
|
65
|
-
handler.setLogin(body.login);
|
|
50
|
+
if (handler.isOpen) {
|
|
51
|
+
await handler.acceptHttp(request, response);
|
|
52
|
+
} else {
|
|
53
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
54
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
66
55
|
}
|
|
67
|
-
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
68
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
69
56
|
} catch (error) {
|
|
70
57
|
logger.error('Satori webhook error:', error);
|
|
71
58
|
if (!response.headersSent) {
|
|
@@ -90,18 +77,3 @@ export function verifySatoriToken(token: string | undefined, request: IncomingMe
|
|
|
90
77
|
const actual = Buffer.from(auth, 'utf8');
|
|
91
78
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
92
79
|
}
|
|
93
|
-
|
|
94
|
-
export async function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
95
|
-
const chunks: Buffer[] = [];
|
|
96
|
-
let size = 0;
|
|
97
|
-
for await (const chunk of request) {
|
|
98
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
99
|
-
size += buffer.length;
|
|
100
|
-
if (size > 1_048_576) {
|
|
101
|
-
request.destroy();
|
|
102
|
-
throw new Error('Request body exceeds 1MB');
|
|
103
|
-
}
|
|
104
|
-
chunks.push(buffer);
|
|
105
|
-
}
|
|
106
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
107
|
-
}
|