@zhin.js/adapter-milky 3.0.1 → 4.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 +443 -0
- package/README.md +36 -117
- package/adapters/milky.ts +68 -0
- package/lib/endpoint.d.ts +7 -0
- package/lib/endpoint.js +6 -0
- package/lib/index.d.ts +3 -20
- package/lib/index.js +3 -35
- package/lib/milky-agent-deps.d.ts +24 -0
- package/lib/milky-agent-deps.js +30 -0
- package/lib/milky-auth.d.ts +3 -0
- package/lib/milky-auth.js +30 -0
- package/lib/protocol.d.ts +162 -0
- package/lib/protocol.js +337 -0
- package/lib/sse-client.d.ts +20 -0
- package/lib/sse-client.js +85 -0
- package/lib/sse-endpoint.d.ts +50 -0
- package/lib/sse-endpoint.js +261 -0
- package/lib/webhook-endpoint.d.ts +40 -0
- package/lib/webhook-endpoint.js +212 -0
- package/lib/ws-endpoint.d.ts +42 -0
- package/lib/ws-endpoint.js +320 -0
- package/lib/ws-types.d.ts +11 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-endpoint.d.ts +40 -0
- package/lib/wss-endpoint.js +263 -0
- package/package.json +52 -19
- package/plugin.ts +8 -0
- package/schema.json +36 -0
- package/src/endpoint.ts +26 -0
- package/src/index.ts +59 -48
- package/src/milky-agent-deps.ts +53 -0
- package/src/milky-auth.ts +29 -0
- package/src/protocol.ts +491 -0
- package/src/sse-client.ts +106 -0
- package/src/sse-endpoint.ts +318 -0
- package/src/webhook-endpoint.ts +262 -0
- package/src/ws-endpoint.ts +373 -0
- package/src/ws-types.ts +12 -0
- package/src/wss-endpoint.ts +305 -0
- package/lib/adapter.d.ts +0 -37
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -99
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -10
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -48
- package/lib/api.js.map +0 -1
- package/lib/endpoint-sse.d.ts +0 -31
- package/lib/endpoint-sse.d.ts.map +0 -1
- package/lib/endpoint-sse.js +0 -197
- package/lib/endpoint-sse.js.map +0 -1
- package/lib/endpoint-webhook.d.ts +0 -34
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -189
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -35
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -264
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/endpoint-wss.d.ts +0 -34
- package/lib/endpoint-wss.d.ts.map +0 -1
- package/lib/endpoint-wss.js +0 -227
- package/lib/endpoint-wss.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -75
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -35
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -166
- package/lib/utils.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -111
- package/src/api.ts +0 -63
- package/src/endpoint-sse.ts +0 -232
- package/src/endpoint-webhook.ts +0 -225
- package/src/endpoint-ws.ts +0 -300
- package/src/endpoint-wss.ts +0 -272
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -73
- package/src/utils.ts +0 -182
- /package/{skills/milky → agent}/PERMITS.md +0 -0
- /package/{skills/milky/SKILL.md → agent/skills/milky.md} +0 -0
package/src/adapter.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Milky 适配器:单一适配器支持 WS 正向 / SSE / Webhook / 反向 WS,由 config.connection 区分
|
|
3
|
-
*/
|
|
4
|
-
import { formatCompact, Adapter, Plugin, type ISceneManagement, OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL, ONEBOT_KEYBOARD_AI_OUTBOUND_EXTENSION } from 'zhin.js';
|
|
5
|
-
import type { Router } from '@zhin.js/host-router';
|
|
6
|
-
import { MilkyWsClient } from './endpoint-ws.js';
|
|
7
|
-
import { MilkySseClient } from './endpoint-sse.js';
|
|
8
|
-
import { MilkyWebhookEndpoint } from './endpoint-webhook.js';
|
|
9
|
-
import { MilkyWssServer } from './endpoint-wss.js';
|
|
10
|
-
import type {
|
|
11
|
-
MilkyEndpointConfig,
|
|
12
|
-
MilkyWsConfig,
|
|
13
|
-
MilkySseConfig,
|
|
14
|
-
MilkyWebhookConfig,
|
|
15
|
-
MilkyWssConfig,
|
|
16
|
-
} from './types.js';
|
|
17
|
-
|
|
18
|
-
export type MilkyBot = MilkyWsClient | MilkySseClient | MilkyWebhookEndpoint | MilkyWssServer;
|
|
19
|
-
|
|
20
|
-
export class MilkyAdapter extends Adapter<MilkyBot> {
|
|
21
|
-
static override readonly capabilities = ['inbound', 'outbound'] as const;
|
|
22
|
-
static override outboundRichSegmentPolicy = OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL;
|
|
23
|
-
static override interactivePolicy = 'text' as const;
|
|
24
|
-
static override aiOutboundCapabilities = {
|
|
25
|
-
mentions: true,
|
|
26
|
-
richSegments: ['qrcode', 'html', 'markdown', 'tts'],
|
|
27
|
-
interactive: 'text' as const,
|
|
28
|
-
};
|
|
29
|
-
static override aiOutboundExtensions = [ONEBOT_KEYBOARD_AI_OUTBOUND_EXTENSION];
|
|
30
|
-
|
|
31
|
-
#router?: Router;
|
|
32
|
-
|
|
33
|
-
constructor(plugin: Plugin) {
|
|
34
|
-
super(plugin, 'milky', []);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
createEndpoint(config: MilkyEndpointConfig): MilkyBot {
|
|
38
|
-
switch (config.connection) {
|
|
39
|
-
case 'ws':
|
|
40
|
-
return new MilkyWsClient(this, config as MilkyWsConfig);
|
|
41
|
-
case 'sse':
|
|
42
|
-
return new MilkySseClient(this, config as MilkySseConfig);
|
|
43
|
-
case 'webhook':
|
|
44
|
-
if (!this.#router) throw new Error('Milky connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/host-router');
|
|
45
|
-
return new MilkyWebhookEndpoint(this, this.#router, config as MilkyWebhookConfig);
|
|
46
|
-
case 'wss':
|
|
47
|
-
if (!this.#router) throw new Error('Milky connection: wss 需要 router,请安装并在配置中启用 @zhin.js/host-router');
|
|
48
|
-
return new MilkyWssServer(this, this.#router, config as MilkyWssConfig);
|
|
49
|
-
default:
|
|
50
|
-
throw new Error(`Unknown Milky connection: ${(config as MilkyEndpointConfig).connection}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async removeMember(endpointId: string, sceneId: string, userId: string) {
|
|
55
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
56
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
57
|
-
return endpoint.kickMember(Number(sceneId), Number(userId), false);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async muteMember(endpointId: string, sceneId: string, userId: string, duration = 600) {
|
|
61
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
62
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
63
|
-
return endpoint.muteMember(Number(sceneId), Number(userId), duration);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async setSceneMuted(endpointId: string, sceneId: string, enable = true) {
|
|
67
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
68
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
69
|
-
return endpoint.muteAll(Number(sceneId), enable);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async setModerator(endpointId: string, sceneId: string, userId: string, enable = true) {
|
|
73
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
74
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
75
|
-
return endpoint.setAdmin(Number(sceneId), Number(userId), enable);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async setMemberNickname(endpointId: string, sceneId: string, userId: string, nickname: string) {
|
|
79
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
80
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
81
|
-
return endpoint.setCard(Number(sceneId), Number(userId), nickname);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async renameScene(endpointId: string, sceneId: string, name: string) {
|
|
85
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
86
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
87
|
-
return endpoint.setGroupName(Number(sceneId), name);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async listMembers(endpointId: string, sceneId: string) {
|
|
91
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
92
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
93
|
-
const members = await endpoint.getMemberList(Number(sceneId));
|
|
94
|
-
return { members: Array.isArray(members) ? members : [], count: Array.isArray(members) ? members.length : 0 };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async getSceneInfo(endpointId: string, sceneId: string) {
|
|
98
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
99
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
100
|
-
return endpoint.getGroupInfo(Number(sceneId));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async start(): Promise<void> {
|
|
104
|
-
// 同步获取已就绪的 router,或通过 useContext 在 router 挂载后赋值
|
|
105
|
-
this.#router = (this.plugin.inject as (key: string) => Router | undefined)('router');
|
|
106
|
-
(this.plugin.useContext as (key: string, fn: (router: Router) => void) => void)('router', (router: Router) => {
|
|
107
|
-
this.#router = router;
|
|
108
|
-
});
|
|
109
|
-
await super.start();
|
|
110
|
-
}
|
|
111
|
-
}
|
package/src/api.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Milky HTTP API 封装:鉴权 + POST /api/:api
|
|
3
|
-
*/
|
|
4
|
-
import type { MilkyApiResponse } from './types.js';
|
|
5
|
-
|
|
6
|
-
export interface MilkyApiClientOptions {
|
|
7
|
-
baseUrl: string;
|
|
8
|
-
access_token?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** 鉴权:Header Authorization: Bearer {token} 或 URL query access_token=xxx */
|
|
12
|
-
function authHeaders(access_token?: string): Record<string, string> {
|
|
13
|
-
const headers: Record<string, string> = {};
|
|
14
|
-
if (access_token) {
|
|
15
|
-
headers['Authorization'] = `Bearer ${access_token}`;
|
|
16
|
-
}
|
|
17
|
-
return headers;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function authQuery(access_token?: string): string {
|
|
21
|
-
if (!access_token) return '';
|
|
22
|
-
return `access_token=${encodeURIComponent(access_token)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
|
|
27
|
-
* 非 200 或 retcode !== 0 时抛错。
|
|
28
|
-
*/
|
|
29
|
-
export async function callApi<T = unknown>(
|
|
30
|
-
options: MilkyApiClientOptions,
|
|
31
|
-
apiName: string,
|
|
32
|
-
params: Record<string, unknown> = {},
|
|
33
|
-
): Promise<T> {
|
|
34
|
-
const { baseUrl, access_token } = options;
|
|
35
|
-
const url = new URL(`/api/${apiName}`, baseUrl.replace(/\/$/, ''));
|
|
36
|
-
const q = authQuery(access_token);
|
|
37
|
-
if (q) url.search = (url.search ? url.search + '&' : '') + q;
|
|
38
|
-
|
|
39
|
-
const res = await fetch(url.toString(), {
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: {
|
|
42
|
-
'Content-Type': 'application/json',
|
|
43
|
-
...authHeaders(access_token),
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify(Object.keys(params).length ? params : {}),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const text = await res.text();
|
|
49
|
-
let body: MilkyApiResponse<T>;
|
|
50
|
-
try {
|
|
51
|
-
body = JSON.parse(text) as MilkyApiResponse<T>;
|
|
52
|
-
} catch {
|
|
53
|
-
throw new Error(`Milky API ${apiName}: invalid JSON response (${res.status}) ${text.slice(0, 200)}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (res.status !== 200) {
|
|
57
|
-
throw new Error(`Milky API ${apiName}: HTTP ${res.status} ${body.message ?? text}`);
|
|
58
|
-
}
|
|
59
|
-
if (body.retcode !== 0) {
|
|
60
|
-
throw new Error(`Milky API ${apiName}: retcode=${body.retcode} ${body.message ?? ''}`);
|
|
61
|
-
}
|
|
62
|
-
return (body.data ?? {}) as T;
|
|
63
|
-
}
|
package/src/endpoint-sse.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Milky SSE Bot:应用 GET 协议端 /event,Accept: text/event-stream,解析 SSE 的 milky_event
|
|
3
|
-
*/
|
|
4
|
-
import EventSource from 'eventsource';
|
|
5
|
-
import { EventEmitter } from 'events';
|
|
6
|
-
import { formatCompact, Endpoint, Message, segment, SendOptions, expandInteractiveSegmentsInContent,} from 'zhin.js';
|
|
7
|
-
import { callApi } from './api.js';
|
|
8
|
-
import type { MilkySseConfig, MilkyEvent } from './types.js';
|
|
9
|
-
import type { MilkyAdapter } from './adapter.js';
|
|
10
|
-
import {
|
|
11
|
-
formatMilkyMessagePayload,
|
|
12
|
-
parseMessageReceiveData,
|
|
13
|
-
toMilkyOutgoingSegments,
|
|
14
|
-
parseMilkyMessageId,
|
|
15
|
-
} from './utils.js';
|
|
16
|
-
import { fromCanonicalSegments } from './segment-mapper.js';
|
|
17
|
-
|
|
18
|
-
export class MilkySseClient extends EventEmitter implements Endpoint<MilkySseConfig, MilkyEvent> {
|
|
19
|
-
$connected: boolean;
|
|
20
|
-
private es?: EventSource;
|
|
21
|
-
|
|
22
|
-
get logger() {
|
|
23
|
-
return this.adapter.plugin.logger;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
constructor(public adapter: MilkyAdapter, public $config: MilkySseConfig) {
|
|
27
|
-
super();
|
|
28
|
-
this.$connected = false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get $id() {
|
|
32
|
-
return this.$config.name;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private get eventUrl(): string {
|
|
36
|
-
const base = this.$config.baseUrl.replace(/\/$/, '');
|
|
37
|
-
const url = `${base}/event`;
|
|
38
|
-
const token = this.$config.access_token;
|
|
39
|
-
if (token) return `${url}${url.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(token)}`;
|
|
40
|
-
return url;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private apiOptions() {
|
|
44
|
-
return { baseUrl: this.$config.baseUrl, access_token: this.$config.access_token };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async $connect(): Promise<void> {
|
|
48
|
-
const headers: Record<string, string> = { Accept: 'text/event-stream' };
|
|
49
|
-
if (this.$config.access_token) {
|
|
50
|
-
headers['Authorization'] = `Bearer ${this.$config.access_token}`;
|
|
51
|
-
}
|
|
52
|
-
this.es = new EventSource(this.eventUrl, { headers });
|
|
53
|
-
this.$connected = true;
|
|
54
|
-
if (!this.$config.access_token) {
|
|
55
|
-
this.logger.warn(formatCompact({ endpoint: this.$id, ok: false, error: 'missing access_token' }));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
this.es.addEventListener('milky_event', (e: MessageEvent) => {
|
|
59
|
-
try {
|
|
60
|
-
const event = JSON.parse(e.data) as MilkyEvent;
|
|
61
|
-
this.handleEvent(event);
|
|
62
|
-
} catch (err: unknown) {
|
|
63
|
-
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.es.onerror = (err: unknown) => {
|
|
68
|
-
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async $disconnect(): Promise<void> {
|
|
73
|
-
if (this.es) {
|
|
74
|
-
this.es.close();
|
|
75
|
-
this.es = undefined;
|
|
76
|
-
}
|
|
77
|
-
this.$connected = false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
$formatMessage(event: MilkyEvent): Message<MilkyEvent> {
|
|
81
|
-
const data = parseMessageReceiveData(event);
|
|
82
|
-
if (!data) {
|
|
83
|
-
return Message.from(event, {
|
|
84
|
-
$id: '',
|
|
85
|
-
$adapter: 'milky',
|
|
86
|
-
$endpoint: this.$config.name,
|
|
87
|
-
$channel: { id: '', type: 'private' },
|
|
88
|
-
$sender: { id: '', name: '' },
|
|
89
|
-
$content: [],
|
|
90
|
-
$raw: '',
|
|
91
|
-
$timestamp: event.time ?? 0,
|
|
92
|
-
$recall: async () => {},
|
|
93
|
-
$reply: async () => '',
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
const payload = formatMilkyMessagePayload(
|
|
97
|
-
event,
|
|
98
|
-
data,
|
|
99
|
-
(id) => this.$recallMessage(id),
|
|
100
|
-
(channel, content) =>
|
|
101
|
-
this.adapter.sendMessage({
|
|
102
|
-
...channel,
|
|
103
|
-
context: 'milky',
|
|
104
|
-
endpoint: this.$config.name,
|
|
105
|
-
content: content as import('zhin.js').SendContent,
|
|
106
|
-
}),
|
|
107
|
-
'milky',
|
|
108
|
-
this.$config.name,
|
|
109
|
-
);
|
|
110
|
-
return Message.from(event, payload);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private handleEvent(event: MilkyEvent): void {
|
|
114
|
-
const data = parseMessageReceiveData(event);
|
|
115
|
-
if (data) {
|
|
116
|
-
const message = this.$formatMessage(event);
|
|
117
|
-
this.adapter.emit('message.receive', message);
|
|
118
|
-
this.logger.debug(
|
|
119
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
125
|
-
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
126
|
-
const arr = Array.isArray(expanded) ? expanded : [expanded];
|
|
127
|
-
const wire = fromCanonicalSegments(
|
|
128
|
-
arr.map((c) => (typeof c === 'string' ? { type: 'text' as const, data: { text: c } } : c)),
|
|
129
|
-
);
|
|
130
|
-
const message = toMilkyOutgoingSegments(
|
|
131
|
-
wire as (string | { type: string; data?: Record<string, unknown> })[],
|
|
132
|
-
);
|
|
133
|
-
if (options.type === 'group') {
|
|
134
|
-
const result = await callApi(this.apiOptions(), 'send_group_message', {
|
|
135
|
-
group_id: parseInt(options.id, 10),
|
|
136
|
-
message,
|
|
137
|
-
});
|
|
138
|
-
const seq = (result as { message_seq?: number }).message_seq;
|
|
139
|
-
this.logger.debug(`${this.$config.name} send group(${options.id}):${segment.raw(options.content)}`);
|
|
140
|
-
return seq != null ? `group:${options.id}:${seq}` : '';
|
|
141
|
-
}
|
|
142
|
-
if (options.type === 'private') {
|
|
143
|
-
const result = await callApi(this.apiOptions(), 'send_private_message', {
|
|
144
|
-
user_id: parseInt(options.id, 10),
|
|
145
|
-
message,
|
|
146
|
-
});
|
|
147
|
-
const seq = (result as { message_seq?: number }).message_seq;
|
|
148
|
-
this.logger.debug(`${this.$config.name} send private(${options.id}):${segment.raw(options.content)}`);
|
|
149
|
-
return seq != null ? `friend:${options.id}:${seq}` : '';
|
|
150
|
-
}
|
|
151
|
-
throw new Error('Either group or private must be provided');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async $recallMessage(id: string): Promise<void> {
|
|
155
|
-
const parsed = parseMilkyMessageId(id);
|
|
156
|
-
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
157
|
-
if (parsed.message_scene === 'group') {
|
|
158
|
-
await callApi(this.apiOptions(), 'recall_group_message', {
|
|
159
|
-
group_id: parsed.peer_id,
|
|
160
|
-
message_seq: parsed.message_seq,
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
await callApi(this.apiOptions(), 'recall_private_message', {
|
|
164
|
-
user_id: parsed.peer_id,
|
|
165
|
-
message_seq: parsed.message_seq,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
|
|
171
|
-
await callApi(this.apiOptions(), 'kick_group_member', {
|
|
172
|
-
group_id: groupId,
|
|
173
|
-
user_id: userId,
|
|
174
|
-
reject_add_request: rejectAddRequest,
|
|
175
|
-
});
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
|
|
180
|
-
await callApi(this.apiOptions(), 'set_group_member_mute', {
|
|
181
|
-
group_id: groupId,
|
|
182
|
-
user_id: userId,
|
|
183
|
-
duration,
|
|
184
|
-
});
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async muteAll(groupId: number, enable = true): Promise<boolean> {
|
|
189
|
-
await callApi(this.apiOptions(), 'set_group_whole_mute', { group_id: groupId, is_mute: enable });
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
|
|
194
|
-
await callApi(this.apiOptions(), 'set_group_member_admin', {
|
|
195
|
-
group_id: groupId,
|
|
196
|
-
user_id: userId,
|
|
197
|
-
is_set: enable,
|
|
198
|
-
});
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
|
|
203
|
-
await callApi(this.apiOptions(), 'set_group_member_card', {
|
|
204
|
-
group_id: groupId,
|
|
205
|
-
user_id: userId,
|
|
206
|
-
card,
|
|
207
|
-
});
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
|
|
212
|
-
await callApi(this.apiOptions(), 'set_group_member_special_title', {
|
|
213
|
-
group_id: groupId,
|
|
214
|
-
user_id: userId,
|
|
215
|
-
special_title: title,
|
|
216
|
-
});
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async setGroupName(groupId: number, name: string): Promise<boolean> {
|
|
221
|
-
await callApi(this.apiOptions(), 'set_group_name', { group_id: groupId, new_group_name: name });
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async getMemberList(groupId: number): Promise<unknown[]> {
|
|
226
|
-
return callApi(this.apiOptions(), 'get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async getGroupInfo(groupId: number): Promise<unknown> {
|
|
230
|
-
return callApi(this.apiOptions(), 'get_group_info', { group_id: groupId });
|
|
231
|
-
}
|
|
232
|
-
}
|
package/src/endpoint-webhook.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Milky Webhook Bot:无长连接,在 router.post(path) 接收 POST 事件,鉴权后转 Message
|
|
3
|
-
*/
|
|
4
|
-
import { EventEmitter } from 'events';
|
|
5
|
-
import { formatCompact, Endpoint, Message, segment, SendOptions, expandInteractiveSegmentsInContent,} from 'zhin.js';
|
|
6
|
-
import { firstHeader, firstQuery, registerFetchRoute, type Router, type RouterContext } from '@zhin.js/host-router/router';
|
|
7
|
-
import { callApi } from './api.js';
|
|
8
|
-
import type { MilkyWebhookConfig, MilkyEvent } from './types.js';
|
|
9
|
-
import type { MilkyAdapter } from './adapter.js';
|
|
10
|
-
import {
|
|
11
|
-
formatMilkyMessagePayload,
|
|
12
|
-
parseMessageReceiveData,
|
|
13
|
-
toMilkyOutgoingSegments,
|
|
14
|
-
parseMilkyMessageId,
|
|
15
|
-
} from './utils.js';
|
|
16
|
-
import { fromCanonicalSegments } from './segment-mapper.js';
|
|
17
|
-
|
|
18
|
-
function getAccessTokenFromRequest(ctx: RouterContext): string | undefined {
|
|
19
|
-
const auth = firstHeader(ctx, 'authorization');
|
|
20
|
-
if (auth?.startsWith('Bearer ')) return auth.slice(7);
|
|
21
|
-
return firstQuery(ctx, 'access_token');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class MilkyWebhookEndpoint extends EventEmitter implements Endpoint<MilkyWebhookConfig, MilkyEvent> {
|
|
25
|
-
$connected: boolean = true;
|
|
26
|
-
|
|
27
|
-
get logger() {
|
|
28
|
-
return this.adapter.plugin.logger;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
public adapter: MilkyAdapter,
|
|
33
|
-
public router: Router,
|
|
34
|
-
public $config: MilkyWebhookConfig,
|
|
35
|
-
) {
|
|
36
|
-
super();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get $id() {
|
|
40
|
-
return this.$config.name;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async $connect(): Promise<void> {
|
|
44
|
-
const path = this.$config.path.startsWith('/') ? this.$config.path : `/${this.$config.path}`;
|
|
45
|
-
const token = this.$config.access_token;
|
|
46
|
-
registerFetchRoute(this.router, 'POST', path, async (ctx: RouterContext) => {
|
|
47
|
-
const received = getAccessTokenFromRequest(ctx);
|
|
48
|
-
if (token && received !== token) {
|
|
49
|
-
ctx.status = 401;
|
|
50
|
-
ctx.body = { retcode: 401, message: 'Unauthorized' };
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const body = ctx.request.body;
|
|
54
|
-
if (!body || typeof body !== 'object') {
|
|
55
|
-
ctx.status = 400;
|
|
56
|
-
ctx.body = { retcode: 400, message: 'Invalid JSON' };
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const event = body as MilkyEvent;
|
|
60
|
-
this.handleEvent(event);
|
|
61
|
-
ctx.status = 200;
|
|
62
|
-
ctx.body = { retcode: 0 };
|
|
63
|
-
});
|
|
64
|
-
this.logger.info(formatCompact( { op: 'webhook', path }));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async $disconnect(): Promise<void> {
|
|
68
|
-
this.$connected = false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
$formatMessage(event: MilkyEvent): Message<MilkyEvent> {
|
|
72
|
-
const data = parseMessageReceiveData(event);
|
|
73
|
-
if (!data) {
|
|
74
|
-
return Message.from(event, {
|
|
75
|
-
$id: '',
|
|
76
|
-
$adapter: 'milky',
|
|
77
|
-
$endpoint: this.$config.name,
|
|
78
|
-
$channel: { id: '', type: 'private' },
|
|
79
|
-
$sender: { id: '', name: '' },
|
|
80
|
-
$content: [],
|
|
81
|
-
$raw: '',
|
|
82
|
-
$timestamp: event.time ?? 0,
|
|
83
|
-
$recall: async () => {},
|
|
84
|
-
$reply: async () => '',
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const payload = formatMilkyMessagePayload(
|
|
88
|
-
event,
|
|
89
|
-
data,
|
|
90
|
-
(id) => this.$recallMessage(id),
|
|
91
|
-
(channel, content) =>
|
|
92
|
-
this.adapter.sendMessage({
|
|
93
|
-
...channel,
|
|
94
|
-
context: 'milky',
|
|
95
|
-
endpoint: this.$config.name,
|
|
96
|
-
content: content as import('zhin.js').SendContent,
|
|
97
|
-
}),
|
|
98
|
-
'milky',
|
|
99
|
-
this.$config.name,
|
|
100
|
-
);
|
|
101
|
-
return Message.from(event, payload);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private handleEvent(event: MilkyEvent): void {
|
|
105
|
-
const data = parseMessageReceiveData(event);
|
|
106
|
-
if (data) {
|
|
107
|
-
const message = this.$formatMessage(event);
|
|
108
|
-
this.adapter.emit('message.receive', message);
|
|
109
|
-
this.logger.debug(
|
|
110
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private apiOptions() {
|
|
116
|
-
return { baseUrl: this.$config.baseUrl, access_token: this.$config.access_token };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
120
|
-
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
121
|
-
const arr = Array.isArray(expanded) ? expanded : [expanded];
|
|
122
|
-
const wire = fromCanonicalSegments(
|
|
123
|
-
arr.map((c) => (typeof c === 'string' ? { type: 'text' as const, data: { text: c } } : c)),
|
|
124
|
-
);
|
|
125
|
-
const message = toMilkyOutgoingSegments(
|
|
126
|
-
wire as (string | { type: string; data?: Record<string, unknown> })[],
|
|
127
|
-
);
|
|
128
|
-
if (options.type === 'group') {
|
|
129
|
-
const result = await callApi(this.apiOptions(), 'send_group_message', {
|
|
130
|
-
group_id: parseInt(options.id, 10),
|
|
131
|
-
message,
|
|
132
|
-
});
|
|
133
|
-
const seq = (result as { message_seq?: number }).message_seq;
|
|
134
|
-
return seq != null ? `group:${options.id}:${seq}` : '';
|
|
135
|
-
}
|
|
136
|
-
if (options.type === 'private') {
|
|
137
|
-
const result = await callApi(this.apiOptions(), 'send_private_message', {
|
|
138
|
-
user_id: parseInt(options.id, 10),
|
|
139
|
-
message,
|
|
140
|
-
});
|
|
141
|
-
const seq = (result as { message_seq?: number }).message_seq;
|
|
142
|
-
return seq != null ? `friend:${options.id}:${seq}` : '';
|
|
143
|
-
}
|
|
144
|
-
throw new Error('Either group or private must be provided');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async $recallMessage(id: string): Promise<void> {
|
|
148
|
-
const parsed = parseMilkyMessageId(id);
|
|
149
|
-
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
150
|
-
if (parsed.message_scene === 'group') {
|
|
151
|
-
await callApi(this.apiOptions(), 'recall_group_message', {
|
|
152
|
-
group_id: parsed.peer_id,
|
|
153
|
-
message_seq: parsed.message_seq,
|
|
154
|
-
});
|
|
155
|
-
} else {
|
|
156
|
-
await callApi(this.apiOptions(), 'recall_private_message', {
|
|
157
|
-
user_id: parsed.peer_id,
|
|
158
|
-
message_seq: parsed.message_seq,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
|
|
164
|
-
await callApi(this.apiOptions(), 'kick_group_member', {
|
|
165
|
-
group_id: groupId,
|
|
166
|
-
user_id: userId,
|
|
167
|
-
reject_add_request: rejectAddRequest,
|
|
168
|
-
});
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
|
|
173
|
-
await callApi(this.apiOptions(), 'set_group_member_mute', {
|
|
174
|
-
group_id: groupId,
|
|
175
|
-
user_id: userId,
|
|
176
|
-
duration,
|
|
177
|
-
});
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async muteAll(groupId: number, enable = true): Promise<boolean> {
|
|
182
|
-
await callApi(this.apiOptions(), 'set_group_whole_mute', { group_id: groupId, is_mute: enable });
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
|
|
187
|
-
await callApi(this.apiOptions(), 'set_group_member_admin', {
|
|
188
|
-
group_id: groupId,
|
|
189
|
-
user_id: userId,
|
|
190
|
-
is_set: enable,
|
|
191
|
-
});
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
|
|
196
|
-
await callApi(this.apiOptions(), 'set_group_member_card', {
|
|
197
|
-
group_id: groupId,
|
|
198
|
-
user_id: userId,
|
|
199
|
-
card,
|
|
200
|
-
});
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
|
|
205
|
-
await callApi(this.apiOptions(), 'set_group_member_special_title', {
|
|
206
|
-
group_id: groupId,
|
|
207
|
-
user_id: userId,
|
|
208
|
-
special_title: title,
|
|
209
|
-
});
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async setGroupName(groupId: number, name: string): Promise<boolean> {
|
|
214
|
-
await callApi(this.apiOptions(), 'set_group_name', { group_id: groupId, new_group_name: name });
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async getMemberList(groupId: number): Promise<unknown[]> {
|
|
219
|
-
return callApi(this.apiOptions(), 'get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async getGroupInfo(groupId: number): Promise<unknown> {
|
|
223
|
-
return callApi(this.apiOptions(), 'get_group_info', { group_id: groupId });
|
|
224
|
-
}
|
|
225
|
-
}
|