@zhin.js/adapter-milky 1.1.18 → 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.
@@ -1,20 +1,21 @@
1
1
  /**
2
2
  * Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
3
3
  */
4
- import { ClientEndpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
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 { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents } from './client.js';
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 ClientEndpoint {
12
- client;
11
+ export class MilkyWssEndpoint extends MilkyClient {
12
+ [endpointSpec];
13
+ #open = false;
13
14
  #logger;
14
15
  #options;
15
- #callApi;
16
- management;
17
- control = createRecallEndpointControl((id) => this.recallMessage(id));
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
- super();
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.client = createMilkyEndpointClient(options.config, this.#callApi);
34
- this.management = createMilkyEndpointManagement({
35
- callApi: (action, params) => callMilkyClient(this.client, action, params),
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.close();
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 callMilkyClient(this.client, action, params);
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 callMilkyClient(this.client, 'recall_group_message', {
93
- group_id: parsed.peer_id,
94
- message_seq: parsed.message_seq,
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 callMilkyClient(this.client, 'recall_private_message', {
99
- user_id: parsed.peer_id,
100
- message_seq: parsed.message_seq,
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.emit('message.receive', {
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.client.acceptWebSocket(socket);
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.18",
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",
@@ -43,16 +43,16 @@
43
43
  "@imhelper/milky-v1": "1.0.7",
44
44
  "imhelper": "1.0.7",
45
45
  "ws": "^8.21.1",
46
- "@zhin.js/adapter": "1.1.25",
47
- "@zhin.js/client": "1.1.21",
46
+ "@zhin.js/adapter": "1.1.26",
47
+ "@zhin.js/client": "1.1.22",
48
48
  "@zhin.js/console-contract": "1.1.3",
49
- "@zhin.js/core": "1.1.53",
50
- "@zhin.js/feature-kit": "1.1.9",
51
- "@zhin.js/host-http": "1.1.11",
49
+ "@zhin.js/core": "1.1.54",
50
+ "@zhin.js/feature-kit": "1.1.10",
51
+ "@zhin.js/host-http": "1.1.12",
52
52
  "@zhin.js/im-contract": "1.1.9",
53
53
  "@zhin.js/logger": "1.1.2",
54
- "@zhin.js/page": "1.1.10",
55
- "@zhin.js/skill": "1.1.10"
54
+ "@zhin.js/page": "1.1.11",
55
+ "@zhin.js/skill": "1.1.11"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^26.1.2",
@@ -60,17 +60,17 @@
60
60
  "typescript": "^6.0.3",
61
61
  "vitest": "^4.1.10",
62
62
  "zod": "^4.4.3",
63
- "@zhin.js/agent": "1.1.44",
64
- "@zhin.js/host-http": "1.1.11",
65
- "zhin.js": "1.1.18"
63
+ "@zhin.js/agent": "1.1.45",
64
+ "@zhin.js/host-http": "1.1.12",
65
+ "zhin.js": "1.1.19"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "zod": "^4.0.0",
69
- "@zhin.js/adapter": "^1.1.25",
70
- "@zhin.js/agent": "^1.1.44",
71
- "@zhin.js/command": "^1.1.11",
72
- "@zhin.js/core": "^1.1.53",
73
- "zhin.js": "^1.1.18"
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"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@zhin.js/agent": {
@@ -107,7 +107,7 @@
107
107
  "features": [
108
108
  {
109
109
  "package": "@zhin.js/adapter",
110
- "api": "^1.0.0"
110
+ "api": "^2.0.0"
111
111
  },
112
112
  {
113
113
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
- import { milkyRuntimeStateToken } from "./lib/milky-runtime-state.js";
5
3
  import { createMilkyConsoleRpc } from "./lib/console.js";
6
4
  export default definePlugin({
7
5
  name: 'milky',
@@ -9,8 +7,6 @@ export default definePlugin({
9
7
  displayName: 'Milky Adapter',
10
8
  },
11
9
  setup(context) {
12
- // 运行中 endpoint 注册表(milky endpoint list 的"运行中"数据源)
13
- context.resources.provide(milkyRuntimeStateToken, createEndpointRuntimeState());
14
10
  context.resources.provide(pluginConsoleRpcToken, createMilkyConsoleRpc(context.resources, () => context.config.get()));
15
11
  },
16
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
- /** Construct the exact public imhelper Client without handing it transport ownership. */
24
- export function createMilkyEndpointClient(
25
- config: ResolvedMilkyConfig,
26
- request: typeof callApi,
27
- ): MilkyV1Client {
28
- return new MilkyV1Client({
29
- baseUrl: config.baseUrl,
30
- selfId: config.id,
31
- accessToken: config.access_token,
32
- receiveMode: 'manual',
33
- call: (action, params) => request(
34
- {
35
- baseUrl: config.baseUrl,
36
- access_token: config.access_token,
37
- },
38
- action,
39
- params,
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
- /** Use the Client's complete protocol response while keeping Endpoint internals data-oriented. */
45
- export async function callMilkyClient<T = unknown>(
46
- client: MilkyV1Client,
47
- action: string,
48
- params?: Record<string, unknown>,
49
- ): Promise<T | undefined> {
50
- const response: MilkyV1Response<T> = await client.call<T>(action, params);
51
- if (response.status !== 'ok') {
52
- throw new Error(
53
- `Milky API ${action}: retcode=${response.retcode}${response.message ? ` ${response.message}` : ''}`,
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
- return response.data;
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');
package/src/console.ts CHANGED
@@ -5,17 +5,14 @@
5
5
  */
6
6
  import { createAdapterConsoleRpc, type AdapterConsoleResources } from 'zhin.js/adapter';
7
7
  import type { PluginConsoleRpc } from 'zhin.js';
8
- import { milkyRuntimeStateToken } from './milky-runtime-state.js';
9
8
  import { milkyConsoleSpec } from './console-spec.js';
10
9
 
11
10
  export function createMilkyConsoleRpc(
12
- resources: AdapterConsoleResources,
11
+ _resources: AdapterConsoleResources,
13
12
  getConfig: () => unknown,
14
13
  ): PluginConsoleRpc {
15
- const state = resources.use(milkyRuntimeStateToken);
16
14
  return createAdapterConsoleRpc({
17
15
  ...milkyConsoleSpec,
18
- running: () => state.endpoints.values(),
19
16
  getConfig,
20
- }, resources);
17
+ });
21
18
  }
@@ -9,23 +9,26 @@
9
9
  * - group → {group_id:number, name: group_name ?? name}
10
10
  * - 群成员列表保持 Milky 原生形状,仅保证数组
11
11
  */
12
- import type {
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(action: string, params?: Record<string, unknown>): Promise<unknown>;
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: string): Promise<readonly unknown[]> {
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: MilkyApiClientOptions,
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
- const res = await fetch(url.toString(), {
205
- method: 'POST',
206
- headers: {
207
- 'Content-Type': 'application/json',
208
- ...authHeaders(access_token),
209
- },
210
- body: JSON.stringify(Object.keys(params).length ? params : {}),
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>;