@zhin.js/adapter-milky 1.1.17 → 1.1.19
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 +62 -0
- package/README.md +18 -5
- package/adapters/milky/index.js +3 -36
- package/adapters/milky/index.ts +2 -41
- package/commands/milky/endpoint/definition.js +0 -2
- package/commands/milky/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +21 -0
- package/lib/adapter.js +36 -0
- package/lib/client.d.ts +20 -15
- package/lib/client.js +28 -21
- package/lib/console-spec.d.ts +12 -0
- package/lib/console-spec.js +12 -0
- package/lib/console.d.ts +8 -0
- package/lib/console.js +13 -0
- package/lib/endpoint-management.d.ts +2 -2
- package/lib/endpoint-management.js +8 -8
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +15 -14
- package/lib/sse-endpoint.d.ts +5 -10
- package/lib/sse-endpoint.js +52 -33
- package/lib/webhook-endpoint.d.ts +5 -10
- package/lib/webhook-endpoint.js +55 -34
- package/lib/ws-endpoint.d.ts +5 -11
- package/lib/ws-endpoint.js +54 -35
- package/lib/wss-endpoint.d.ts +5 -10
- package/lib/wss-endpoint.js +52 -33
- package/package.json +24 -16
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +3 -5
- package/src/adapter.ts +39 -0
- package/src/client.ts +37 -43
- package/src/console-spec.ts +17 -0
- package/src/console.ts +18 -0
- package/src/endpoint-management.ts +15 -12
- package/src/index.ts +2 -1
- package/src/protocol.ts +18 -12
- package/src/sse-endpoint.ts +45 -31
- package/src/webhook-endpoint.ts +48 -32
- package/src/ws-endpoint.ts +49 -35
- package/src/wss-endpoint.ts +45 -31
- package/lib/milky-runtime-state.d.ts +0 -1
- package/lib/milky-runtime-state.js +0 -6
- package/src/milky-runtime-state.ts +0 -7
package/lib/wss-endpoint.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
5
5
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
6
|
import { verifyMilkyAccessToken } from './milky-auth.js';
|
|
7
7
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
9
9
|
import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
10
10
|
const WS_OPEN = 1;
|
|
11
|
-
export class MilkyWssEndpoint extends
|
|
12
|
-
|
|
11
|
+
export class MilkyWssEndpoint extends MilkyClient {
|
|
12
|
+
[endpointSpec];
|
|
13
|
+
#open = false;
|
|
13
14
|
#logger;
|
|
14
15
|
#options;
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
control = createRecallEndpointControl((id) => this
|
|
16
|
+
#management;
|
|
17
|
+
#call;
|
|
18
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
18
19
|
#ws;
|
|
19
20
|
#wsRelease;
|
|
20
21
|
#clientSocketRelease;
|
|
@@ -22,24 +23,38 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
22
23
|
#connectionTask = Promise.resolve();
|
|
23
24
|
#started = false;
|
|
24
25
|
constructor(options) {
|
|
25
|
-
|
|
26
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
27
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
28
|
+
this.#call = call;
|
|
26
29
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
27
30
|
this.#options = options;
|
|
28
|
-
this.#callApi = options.callApi ?? callApi;
|
|
29
31
|
this.#connectionLifecycle = createEndpointLifecycle({
|
|
30
32
|
name: `${options.config.id}:inbound`,
|
|
31
33
|
reconnect: false,
|
|
32
34
|
});
|
|
33
|
-
this
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
this.#management = createMilkyEndpointManagement({
|
|
36
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
37
|
+
});
|
|
38
|
+
const readTransportState = () => this.#connectionLifecycle.state;
|
|
39
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
40
|
+
name: options.config.id,
|
|
41
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
42
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
43
|
+
send: request => this.#send(request),
|
|
44
|
+
management: this.#management, control: this.#control,
|
|
45
|
+
get transportState() { return readTransportState(); },
|
|
46
|
+
});
|
|
47
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
48
|
+
void this[endpointSpec].events.run(() => {
|
|
49
|
+
if (!this.#open && !this.#started)
|
|
50
|
+
return;
|
|
51
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
52
|
+
if (name === 'event')
|
|
53
|
+
this.#admitRaw(payload);
|
|
54
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
36
55
|
});
|
|
37
|
-
this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
|
|
38
|
-
if (name === 'event')
|
|
39
|
-
this.#admitRaw(payload);
|
|
40
|
-
}, (_name, error) => this.#warnPlatformEvent(error));
|
|
41
56
|
}
|
|
42
|
-
async start() {
|
|
57
|
+
async #start() {
|
|
43
58
|
if (this.#started)
|
|
44
59
|
return;
|
|
45
60
|
this.#started = true;
|
|
@@ -62,8 +77,8 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
62
77
|
path: this.#options.config.path,
|
|
63
78
|
}));
|
|
64
79
|
}
|
|
65
|
-
async stop() {
|
|
66
|
-
this
|
|
80
|
+
async #stop() {
|
|
81
|
+
this.#open = false;
|
|
67
82
|
this.#wsRelease?.();
|
|
68
83
|
this.#wsRelease = undefined;
|
|
69
84
|
await this.#connectionTask;
|
|
@@ -71,10 +86,10 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
71
86
|
this.#ws = undefined;
|
|
72
87
|
this.#started = false;
|
|
73
88
|
}
|
|
74
|
-
async send({ conversation, payload }) {
|
|
89
|
+
async #send({ conversation, payload, signal }) {
|
|
75
90
|
const message = formatOutboundSegments(payload);
|
|
76
91
|
const { action, params } = buildSendAction(conversation, message);
|
|
77
|
-
const data = await
|
|
92
|
+
const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
|
|
78
93
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
79
94
|
this.#logger.debug(formatCompact({
|
|
80
95
|
op: 'milky_send',
|
|
@@ -84,24 +99,26 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
84
99
|
}));
|
|
85
100
|
return messageId;
|
|
86
101
|
}
|
|
87
|
-
async recallMessage(id) {
|
|
102
|
+
async #recallMessage({ id, signal }) {
|
|
88
103
|
const parsed = parseMilkyMessageId(id);
|
|
89
104
|
if (!parsed)
|
|
90
105
|
throw new Error(`Invalid message id: ${id}`);
|
|
91
106
|
if (parsed.message_scene === 'group') {
|
|
92
|
-
await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
108
|
+
group_id: parsed.peer_id,
|
|
109
|
+
message_seq: parsed.message_seq,
|
|
110
|
+
} }), 'recall_group_message');
|
|
96
111
|
}
|
|
97
112
|
else {
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
114
|
+
user_id: parsed.peer_id,
|
|
115
|
+
message_seq: parsed.message_seq,
|
|
116
|
+
} }), 'recall_private_message');
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
#admitRaw(event) {
|
|
120
|
+
if (event.self_id != null)
|
|
121
|
+
this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
105
122
|
const data = parseMessageReceiveData(event);
|
|
106
123
|
if (!data)
|
|
107
124
|
return;
|
|
@@ -111,14 +128,16 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
111
128
|
this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
112
129
|
}
|
|
113
130
|
#admitMessage(data, event) {
|
|
114
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
131
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
115
132
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
116
133
|
const content = formatInboundContent(data);
|
|
117
134
|
const segments = formatInboundSegments(data);
|
|
118
135
|
const audioUrl = extractInboundAudioUrl(data);
|
|
119
136
|
const nickname = senderNickname(data);
|
|
120
137
|
const mentioned = isMentioned(data, event.self_id);
|
|
121
|
-
void this.
|
|
138
|
+
void this[endpointSpec].events.message({
|
|
139
|
+
event,
|
|
140
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
122
141
|
conversation,
|
|
123
142
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
124
143
|
content,
|
|
@@ -152,7 +171,7 @@ export class MilkyWssEndpoint extends ClientEndpoint {
|
|
|
152
171
|
await this.#connectionLifecycle.stop();
|
|
153
172
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
154
173
|
this.#ws = socket;
|
|
155
|
-
const releaseClientSocket = this.
|
|
174
|
+
const releaseClientSocket = this.acceptWebSocket(socket);
|
|
156
175
|
this.#clientSocketRelease = releaseClientSocket;
|
|
157
176
|
lifecycleHandle.onForceClose(() => {
|
|
158
177
|
if (this.#clientSocketRelease === releaseClientSocket) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-milky",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "Zhin.js Milky adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"adapters",
|
|
16
16
|
"commands",
|
|
17
|
+
"pages",
|
|
17
18
|
"plugin.js",
|
|
18
19
|
"schema.json",
|
|
19
20
|
"src",
|
|
@@ -42,13 +43,16 @@
|
|
|
42
43
|
"@imhelper/milky-v1": "1.0.7",
|
|
43
44
|
"imhelper": "1.0.7",
|
|
44
45
|
"ws": "^8.21.1",
|
|
45
|
-
"@zhin.js/adapter": "1.1.
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
48
|
-
"@zhin.js/
|
|
49
|
-
"@zhin.js/
|
|
46
|
+
"@zhin.js/adapter": "1.1.26",
|
|
47
|
+
"@zhin.js/client": "1.1.22",
|
|
48
|
+
"@zhin.js/console-contract": "1.1.3",
|
|
49
|
+
"@zhin.js/core": "1.1.54",
|
|
50
|
+
"@zhin.js/feature-kit": "1.1.10",
|
|
51
|
+
"@zhin.js/host-http": "1.1.12",
|
|
52
|
+
"@zhin.js/im-contract": "1.1.9",
|
|
50
53
|
"@zhin.js/logger": "1.1.2",
|
|
51
|
-
"@zhin.js/
|
|
54
|
+
"@zhin.js/page": "1.1.11",
|
|
55
|
+
"@zhin.js/skill": "1.1.11"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
54
58
|
"@types/node": "^26.1.2",
|
|
@@ -56,17 +60,17 @@
|
|
|
56
60
|
"typescript": "^6.0.3",
|
|
57
61
|
"vitest": "^4.1.10",
|
|
58
62
|
"zod": "^4.4.3",
|
|
59
|
-
"@zhin.js/agent": "1.1.
|
|
60
|
-
"@zhin.js/host-http": "1.1.
|
|
61
|
-
"zhin.js": "1.1.
|
|
63
|
+
"@zhin.js/agent": "1.1.45",
|
|
64
|
+
"@zhin.js/host-http": "1.1.12",
|
|
65
|
+
"zhin.js": "1.1.19"
|
|
62
66
|
},
|
|
63
67
|
"peerDependencies": {
|
|
64
68
|
"zod": "^4.0.0",
|
|
65
|
-
"@zhin.js/adapter": "^1.1.
|
|
66
|
-
"@zhin.js/agent": "^1.1.
|
|
67
|
-
"@zhin.js/command": "^1.1.
|
|
68
|
-
"@zhin.js/core": "^1.1.
|
|
69
|
-
"zhin.js": "^1.1.
|
|
69
|
+
"@zhin.js/adapter": "^1.1.26",
|
|
70
|
+
"@zhin.js/agent": "^1.1.45",
|
|
71
|
+
"@zhin.js/command": "^1.1.12",
|
|
72
|
+
"@zhin.js/core": "^1.1.54",
|
|
73
|
+
"zhin.js": "^1.1.19"
|
|
70
74
|
},
|
|
71
75
|
"peerDependenciesMeta": {
|
|
72
76
|
"@zhin.js/agent": {
|
|
@@ -103,7 +107,7 @@
|
|
|
103
107
|
"features": [
|
|
104
108
|
{
|
|
105
109
|
"package": "@zhin.js/adapter",
|
|
106
|
-
"api": "^
|
|
110
|
+
"api": "^2.0.0"
|
|
107
111
|
},
|
|
108
112
|
{
|
|
109
113
|
"package": "@zhin.js/command",
|
|
@@ -112,6 +116,10 @@
|
|
|
112
116
|
{
|
|
113
117
|
"package": "@zhin.js/skill",
|
|
114
118
|
"api": "^1.0.0"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"package": "@zhin.js/page",
|
|
122
|
+
"api": "^1.0.0"
|
|
115
123
|
}
|
|
116
124
|
],
|
|
117
125
|
"plugins": []
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { definePage } from '@zhin.js/console-contract';
|
|
3
|
+
import { createAdapterConsolePage } from '@zhin.js/client';
|
|
4
|
+
export const meta = definePage({
|
|
5
|
+
title: 'Milky',
|
|
6
|
+
titleI18n: { en: 'Milky' },
|
|
7
|
+
icon: 'Milk',
|
|
8
|
+
order: 68,
|
|
9
|
+
requiredPermissions: ['milky:read'],
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Convention page: `pages/index/index.ts` → `/milky`(插件路径)。
|
|
13
|
+
* 连接状态、连接测试、endpoint 增删;登录待办来自 LoginAssist(ICQQ 等)。
|
|
14
|
+
*/
|
|
15
|
+
export default createAdapterConsolePage('milky');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { definePage } from '@zhin.js/console-contract';
|
|
2
|
+
import { createAdapterConsolePage } from '@zhin.js/client';
|
|
3
|
+
|
|
4
|
+
export const meta = definePage({
|
|
5
|
+
title: 'Milky',
|
|
6
|
+
titleI18n: { en: 'Milky' },
|
|
7
|
+
icon: 'Milk',
|
|
8
|
+
order: 68,
|
|
9
|
+
requiredPermissions: ['milky:read'],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convention page: `pages/index/index.ts` → `/milky`(插件路径)。
|
|
14
|
+
* 连接状态、连接测试、endpoint 增删;登录待办来自 LoginAssist(ICQQ 等)。
|
|
15
|
+
*/
|
|
16
|
+
export default createAdapterConsolePage('milky');
|
package/plugin.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { milkyRuntimeStateToken } from "./lib/milky-runtime-state.js";
|
|
2
|
+
import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
|
|
3
|
+
import { createMilkyConsoleRpc } from "./lib/console.js";
|
|
5
4
|
export default definePlugin({
|
|
6
5
|
name: 'milky',
|
|
7
6
|
metadata: {
|
|
8
7
|
displayName: 'Milky Adapter',
|
|
9
8
|
},
|
|
10
9
|
setup(context) {
|
|
11
|
-
|
|
12
|
-
context.resources.provide(milkyRuntimeStateToken, createEndpointRuntimeState());
|
|
10
|
+
context.resources.provide(pluginConsoleRpcToken, createMilkyConsoleRpc(context.resources, () => context.config.get()));
|
|
13
11
|
},
|
|
14
12
|
});
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
3
|
+
import { callApi, resolveMilkyConfig, type MilkyEndpointConfig } from './protocol.js';
|
|
4
|
+
import { MilkyWsEndpoint, MilkyWssEndpoint, MilkySseEndpoint, MilkyWebhookEndpoint } from './endpoint.js';
|
|
5
|
+
import { readMilkyResponse, type MilkyClientEventMap } from './client.js';
|
|
6
|
+
|
|
7
|
+
type MilkyEndpoint = MilkyWsEndpoint | MilkyWssEndpoint | MilkySseEndpoint | MilkyWebhookEndpoint;
|
|
8
|
+
export class MilkyAdapter extends Adapter<MilkyEndpoint, MilkyEndpointConfig, MilkyClientEventMap> {
|
|
9
|
+
readonly #context: AdapterContext<MilkyEndpointConfig>;
|
|
10
|
+
constructor(context: AdapterContext<MilkyEndpointConfig>) { super(); this.#context = context; }
|
|
11
|
+
override get configurationIdentityFields(): readonly string[] { return ['id']; }
|
|
12
|
+
override get configurationIdentityProbeFields(): readonly string[] { return ['access_token', 'baseUrl']; }
|
|
13
|
+
override get configurationUpdateFields(): readonly string[] {
|
|
14
|
+
return ['connection', 'baseUrl', 'path', 'access_token', 'reconnect_interval', 'heartbeat_interval'];
|
|
15
|
+
}
|
|
16
|
+
protected override validateConfiguration(config: MilkyEndpointConfig): void { resolveMilkyConfig(config); }
|
|
17
|
+
protected override async identifyConfiguration(input: MilkyEndpointConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
18
|
+
const config = resolveMilkyConfig(input);
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
const response = await callApi<{ uin?: unknown }>({ ...config, signal, action: 'get_login_info' });
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const userId = readMilkyResponse<{ uin?: unknown }>(response, 'get_login_info')?.uin;
|
|
23
|
+
if (userId === 0 || userId === '0' || (typeof userId === 'string' && !userId.trim())) return undefined;
|
|
24
|
+
if ((typeof userId !== 'number' && typeof userId !== 'string') || !String(userId).trim()) throw new Error('Milky login identity is unavailable');
|
|
25
|
+
return `qq:${userId}`;
|
|
26
|
+
}
|
|
27
|
+
protected createEndpoint(input: MilkyEndpointConfig, execution: EndpointCreationContext): MilkyEndpoint {
|
|
28
|
+
const config = resolveMilkyConfig(input), id = execution.canonicalId, signal = execution.signal;
|
|
29
|
+
switch (config.connection) {
|
|
30
|
+
case 'wss': return new MilkyWssEndpoint({ id, config, signal, http: this.#context.use(httpHostToken) });
|
|
31
|
+
case 'webhook': return new MilkyWebhookEndpoint({ id, config, signal, http: this.#context.use(httpHostToken) });
|
|
32
|
+
case 'sse': return new MilkySseEndpoint({ id, config, signal });
|
|
33
|
+
case 'ws': return new MilkyWsEndpoint({ id, config, signal });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
declare module '@zhin.js/feature-kit' {
|
|
38
|
+
interface AdapterRegistry { readonly milky: MilkyAdapter; }
|
|
39
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
1
2
|
import {
|
|
2
3
|
MilkyV1Client,
|
|
3
4
|
type MilkyV1Event,
|
|
@@ -9,9 +10,9 @@ import {
|
|
|
9
10
|
type ImHelperEventMap,
|
|
10
11
|
} from 'imhelper';
|
|
11
12
|
import {
|
|
12
|
-
defineEndpointClient,
|
|
13
13
|
forwardEndpointClientEvents,
|
|
14
14
|
type ClientEventPayloads,
|
|
15
|
+
type PendingRpcRequest,
|
|
15
16
|
} from 'zhin.js/adapter';
|
|
16
17
|
import type { ResolvedMilkyConfig, callApi } from './protocol.js';
|
|
17
18
|
|
|
@@ -20,40 +21,44 @@ export { MilkyV1Client as MilkyClient } from '@imhelper/milky-v1';
|
|
|
20
21
|
type MilkyClientEvents = ImHelperEventMap<string, MilkyV1Event, EventMap<string>>;
|
|
21
22
|
export type MilkyClientEventMap = ClientEventPayloads<MilkyClientEvents>;
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
export
|
|
25
|
-
config: ResolvedMilkyConfig
|
|
26
|
-
request: typeof callApi
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
/** One account-bound transport, shared by native SDK and semantic requests. */
|
|
25
|
+
export interface MilkyClientBinding {
|
|
26
|
+
readonly config: ResolvedMilkyConfig;
|
|
27
|
+
readonly request: typeof callApi;
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export type MilkyActionCaller = (request: PendingRpcRequest) => Promise<MilkyV1Response>;
|
|
31
|
+
export function createMilkyActionCaller({ config, request, signal }: MilkyClientBinding): MilkyActionCaller {
|
|
32
|
+
return action => {
|
|
33
|
+
const operationSignal = signal && action.signal ? AbortSignal.any([signal, action.signal]) : signal ?? action.signal;
|
|
34
|
+
if (operationSignal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint request ended before submission', 'not_sent', { cause: operationSignal.reason });
|
|
35
|
+
return request({ baseUrl: config.baseUrl, access_token: config.access_token, ...action, signal: operationSignal });
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function milkyClientOptions({ config, call }: {
|
|
39
|
+
readonly config: ResolvedMilkyConfig;
|
|
40
|
+
readonly call: MilkyActionCaller;
|
|
41
|
+
}): ConstructorParameters<typeof MilkyV1Client>[0] {
|
|
42
|
+
return { baseUrl: config.baseUrl, selfId: config.id, accessToken: config.access_token, receiveMode: 'manual',
|
|
43
|
+
call: (action, params) => call({ action, params }),
|
|
44
|
+
};
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Semantic ports must retain the SDK's status + retcode check when using its shared HTTP caller.
|
|
49
|
+
* A contradictory envelope proves neither success nor rejection.
|
|
50
|
+
* @see https://milky.ntqqrev.org/guide/communication
|
|
51
|
+
*/
|
|
52
|
+
export function readMilkyResponse<T = unknown>(response: unknown, action: string): T | undefined {
|
|
53
|
+
if (response && typeof response === 'object') {
|
|
54
|
+
const result = response as Partial<MilkyV1Response<unknown>>;
|
|
55
|
+
if (result.status === 'ok' && result.retcode === 0) return result.data as T | undefined;
|
|
56
|
+
if (result.status === 'failed' && Number.isSafeInteger(result.retcode) && result.retcode !== 0) {
|
|
57
|
+
throw new EndpointDeliveryError('platform_rejected',
|
|
58
|
+
`Milky API ${action}: retcode=${result.retcode}${typeof result.message === 'string' && result.message ? ` ${result.message}` : ''}`, 'rejected');
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
|
-
|
|
61
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', `Milky API ${action} did not confirm the action result`, 'unknown');
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
const milkyClientEventNames = Object.freeze([
|
|
@@ -67,14 +72,3 @@ export function forwardMilkyClientEvents(
|
|
|
67
72
|
): () => void {
|
|
68
73
|
return forwardEndpointClientEvents(client, milkyClientEventNames, receive);
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
declare module '@zhin.js/feature-kit' {
|
|
72
|
-
interface AdapterClientRegistry {
|
|
73
|
-
readonly milky: {
|
|
74
|
-
readonly client: MilkyV1Client;
|
|
75
|
-
readonly events: MilkyClientEventMap;
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const milkyClient = defineEndpointClient<MilkyV1Client, MilkyClientEventMap>('milky');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky Console 页面元数据(浏览器与服务端共享;不要在这里 import 服务端代码)。
|
|
3
|
+
*/
|
|
4
|
+
import type { AdapterConsoleField } from 'zhin.js/adapter';
|
|
5
|
+
|
|
6
|
+
export const milkyConsoleSpec = {
|
|
7
|
+
adapterKey: 'milky',
|
|
8
|
+
adapterDisplayName: 'Milky',
|
|
9
|
+
fields: [
|
|
10
|
+
{ key: 'baseUrl', required: true, description: 'Milky HTTP API base URL' },
|
|
11
|
+
{ key: 'path', description: 'webhook / reverse-wss 路径', descriptionI18n: { en: 'Webhook / reverse-wss path' } },
|
|
12
|
+
{ key: 'access_token', env: true, description: 'Milky access token' },
|
|
13
|
+
] as readonly AdapterConsoleField[],
|
|
14
|
+
docsUrl: 'https://zhin.js.org/adapters/milky',
|
|
15
|
+
readPermission: 'milky:read',
|
|
16
|
+
writePermission: 'milky:write',
|
|
17
|
+
} as const;
|
package/src/console.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky Console RPC:运行状态 + endpoint 增删(原 `milky endpoint` 聊天命令)。
|
|
3
|
+
*
|
|
4
|
+
* 页面绑定 `pages/index`,读取 / 写入分别要求 `<adapter>:read` / `<adapter>:write`。
|
|
5
|
+
*/
|
|
6
|
+
import { createAdapterConsoleRpc, type AdapterConsoleResources } from 'zhin.js/adapter';
|
|
7
|
+
import type { PluginConsoleRpc } from 'zhin.js';
|
|
8
|
+
import { milkyConsoleSpec } from './console-spec.js';
|
|
9
|
+
|
|
10
|
+
export function createMilkyConsoleRpc(
|
|
11
|
+
_resources: AdapterConsoleResources,
|
|
12
|
+
getConfig: () => unknown,
|
|
13
|
+
): PluginConsoleRpc {
|
|
14
|
+
return createAdapterConsoleRpc({
|
|
15
|
+
...milkyConsoleSpec,
|
|
16
|
+
getConfig,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -9,23 +9,26 @@
|
|
|
9
9
|
* - group → {group_id:number, name: group_name ?? name}
|
|
10
10
|
* - 群成员列表保持 Milky 原生形状,仅保证数组
|
|
11
11
|
*/
|
|
12
|
-
import
|
|
13
|
-
EndpointFriend,
|
|
14
|
-
EndpointGroup,
|
|
15
|
-
EndpointManagement,
|
|
12
|
+
import {
|
|
13
|
+
type EndpointFriend,
|
|
14
|
+
type EndpointGroup,
|
|
15
|
+
type EndpointManagement,
|
|
16
|
+
type EndpointManagementRequest,
|
|
17
|
+
type EndpointGroupRequest,
|
|
18
|
+
type PendingRpcRequest,
|
|
16
19
|
} from 'zhin.js/adapter';
|
|
17
20
|
|
|
18
21
|
/** 管理面只依赖 endpoint 的 callApi(ws/wss/sse/webhook 四种传输各自实现)。 */
|
|
19
22
|
export interface MilkyManagementCaller {
|
|
20
|
-
callApi(
|
|
23
|
+
callApi(request: PendingRpcRequest): Promise<unknown>;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function createMilkyEndpointManagement(
|
|
24
27
|
endpoint: MilkyManagementCaller,
|
|
25
28
|
): EndpointManagement {
|
|
26
29
|
return Object.freeze<EndpointManagement>({
|
|
27
|
-
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
28
|
-
const data = asRecord(await endpoint.callApi('get_friend_list'));
|
|
30
|
+
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
31
|
+
const data = asRecord(await endpoint.callApi({ action: 'get_friend_list', signal: request.signal }));
|
|
29
32
|
return toArray(data.friends).map((value) => {
|
|
30
33
|
const friend = asRecord(value);
|
|
31
34
|
return {
|
|
@@ -35,8 +38,8 @@ export function createMilkyEndpointManagement(
|
|
|
35
38
|
};
|
|
36
39
|
});
|
|
37
40
|
},
|
|
38
|
-
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
39
|
-
const data = asRecord(await endpoint.callApi('get_group_list'));
|
|
41
|
+
async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
|
|
42
|
+
const data = asRecord(await endpoint.callApi({ action: 'get_group_list', signal: request.signal }));
|
|
40
43
|
return toArray(data.groups).map((value) => {
|
|
41
44
|
const group = asRecord(value);
|
|
42
45
|
return {
|
|
@@ -45,10 +48,10 @@ export function createMilkyEndpointManagement(
|
|
|
45
48
|
};
|
|
46
49
|
});
|
|
47
50
|
},
|
|
48
|
-
async listGroupMembers(groupId:
|
|
49
|
-
const data = asRecord(await endpoint.callApi('get_group_member_list', {
|
|
51
|
+
async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
|
|
52
|
+
const data = asRecord(await endpoint.callApi({ action: 'get_group_member_list', signal, params: {
|
|
50
53
|
group_id: toNumberId(groupId, 'group_id'),
|
|
51
|
-
}));
|
|
54
|
+
} }));
|
|
52
55
|
return toArray(data.members);
|
|
53
56
|
},
|
|
54
57
|
});
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
resolveMilkyConfig,
|
|
14
14
|
senderNickname,
|
|
15
15
|
type MilkyApiClientOptions,
|
|
16
|
+
type MilkyApiRequest,
|
|
16
17
|
type MilkyApiResponse,
|
|
17
18
|
type MilkyConfigBase,
|
|
18
19
|
type MilkyEndpointConfig,
|
|
@@ -47,7 +48,6 @@ export {
|
|
|
47
48
|
|
|
48
49
|
export {
|
|
49
50
|
MilkyClient,
|
|
50
|
-
milkyClient,
|
|
51
51
|
type MilkyClientEventMap,
|
|
52
52
|
} from './client.js';
|
|
53
53
|
|
|
@@ -63,3 +63,4 @@ export type {
|
|
|
63
63
|
} from '@imhelper/milky-v1';
|
|
64
64
|
export { ProtocolError } from '@imhelper/milky-v1';
|
|
65
65
|
export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
|
|
66
|
+
export { MilkyAdapter } from './adapter.js';
|
package/src/protocol.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Spec: https://milky.ntqqrev.org/
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
8
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
9
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
10
|
|
|
@@ -104,6 +104,7 @@ export interface MilkyOutgoingSegment {
|
|
|
104
104
|
export interface MilkyApiClientOptions {
|
|
105
105
|
readonly baseUrl: string;
|
|
106
106
|
readonly access_token?: string;
|
|
107
|
+
readonly signal?: AbortSignal;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function normalizeConnection(connection: string | undefined): 'ws' | 'sse' | 'webhook' | 'wss' {
|
|
@@ -191,24 +192,29 @@ function authQuery(access_token?: string): string {
|
|
|
191
192
|
* 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
|
|
192
193
|
* HTTP 层错误抛出;协议响应原样返回,由 Client 公开 call() 保留完整语义。
|
|
193
194
|
*/
|
|
195
|
+
export interface MilkyApiRequest extends MilkyApiClientOptions {
|
|
196
|
+
readonly action: string;
|
|
197
|
+
readonly params?: Record<string, unknown>;
|
|
198
|
+
}
|
|
194
199
|
export async function callApi<T = unknown>(
|
|
195
|
-
options:
|
|
196
|
-
apiName: string,
|
|
197
|
-
params: Record<string, unknown> = {},
|
|
200
|
+
{ action: apiName, params = {}, ...options }: MilkyApiRequest,
|
|
198
201
|
): Promise<MilkyApiResponse<T>> {
|
|
199
202
|
const { baseUrl, access_token } = options;
|
|
200
203
|
const url = new URL(`/api/${apiName}`, baseUrl.replace(/\/$/, ''));
|
|
201
204
|
const q = authQuery(access_token);
|
|
202
205
|
if (q) url.search = (url.search ? `${url.search}&` : '') + q;
|
|
203
206
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
if (options.signal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Milky request ended before submission', 'not_sent', { cause: options.signal.reason });
|
|
208
|
+
const payload = JSON.stringify(Object.keys(params).length ? params : {});
|
|
209
|
+
let res: Response;
|
|
210
|
+
try {
|
|
211
|
+
res = await fetch(url.toString(), {
|
|
212
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders(access_token) },
|
|
213
|
+
body: payload, signal: options.signal,
|
|
214
|
+
});
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'Milky HTTP request outcome is unknown', 'unknown', { cause });
|
|
217
|
+
}
|
|
212
218
|
|
|
213
219
|
const text = await res.text();
|
|
214
220
|
let body: MilkyApiResponse<T>;
|