@zhin.js/adapter-milky 1.1.0 → 1.1.3

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 CHANGED
@@ -1,5 +1,102 @@
1
1
  # @zhin.js/adapter-milky
2
2
 
3
+ ## 1.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [5b5d270]
8
+ - Updated dependencies [36c2eb9]
9
+ - @zhin.js/im-contract@1.1.2
10
+ - @zhin.js/core@1.1.38
11
+ - @zhin.js/adapter@1.1.15
12
+ - @zhin.js/host-http@1.1.2
13
+ - @zhin.js/feature-kit@1.1.2
14
+ - @zhin.js/skill@1.1.2
15
+
16
+ ## 1.1.2
17
+
18
+ ### Patch Changes
19
+
20
+ - a9e4a40: Derive Command user routes only from explicit `commands/**/*/index.ts(x)` entry paths instead of prepending the plugin owner. Child plugins can now publish top-level static or dynamic commands, while duplicate routes across owners fail during generation construction. Adapter endpoint commands move to explicit paths such as `commands/qq/endpoint/list/index.ts` and are invoked as `qq endpoint list`.
21
+ - 8740059: Standardize TypeScript capabilities on named module directories such as `commands/foo/index.ts`, `middlewares/audit/index.ts`, `handlers/message-receive/index.ts`, `pages/workroom/index.tsx`, and `mcps/filesystem/index.ts`. Only the fixed `index` entry is discovered; sibling files remain private helpers.
22
+
23
+ Command route segments come from directories, while `[name]`, `[[name]]`, `[...name]`, and `[[...name]]` directories declare dynamic parameters. Plugin owners do not enter the route unless their config explicitly sets `commandNamespace`; Endpoint `commandPrefix` remains platform-owned and defaults to an empty string.
24
+
25
+ Migrate the built-in adapters, plugins, examples, generators, migration tooling, hot reload classification, Agent authoring surfaces, documentation, and release artifacts to the explicit entry convention.
26
+
27
+ Make Tool ownership and progressive disclosure explicit across all four supported locations: plugin-public `tools/`, Agent-private `agents/<name>/tools/`, Skill-private `skills/<name>/tools/`, and Agent-Skill-private `agents/<name>/skills/<name>/tools/`. Move adapter and group-suite operations that require domain instructions into their owning Skills so `load_skill` is the only path that unlocks their schemas.
28
+
29
+ Remove the package-root `agent/` convention. Public capabilities now use named package-root directories, schedules use `schedules/<name>/index.ts` or `plugin.ts` injection, MCP connections use `mcps/<name>/index.ts`, Prompt Sections use `prompt-sections/<name>/index.ts`, Agent definitions use `agents/<name>/`, and permission vocabulary is published as `PERMITS.md`.
30
+
31
+ - 25a845a: Unify Skills on `skills/<name>/SKILL.md`, support Agent-private Skills and nested Skill-private Tools, publish and mount existing plugin Skills, add governed Skill metadata and Turn access filtering, unlock only already-admitted same-owner Tools, and make `on-risk` and `once` approval behavior precise.
32
+ - 4380cf9: Colocate adapter command definitions and Agent Skill handlers with their owning capabilities, and provide a shared validated platform permission constructor.
33
+ - 86087cf: Require one canonical expanded Milky endpoint configuration. The protocol no longer reinterprets nested `endpoints` or reads endpoint identity from `process.env`, and reverse WebSocket connection replacement, heartbeat, and cleanup now use the shared endpoint lifecycle.
34
+ - 5c3858e: Make `@zhin.js/im-contract` the explicit zero-dependency owner of transport identities, canonical segments, media guards, conversation facts, stores, and delivery contracts. Platform adapters now read media contracts from that foundation instead of reaching through Core.
35
+ - Updated dependencies [c861789]
36
+ - Updated dependencies [1414ccb]
37
+ - Updated dependencies [743d470]
38
+ - Updated dependencies [62dee52]
39
+ - Updated dependencies [cd54131]
40
+ - Updated dependencies [244e475]
41
+ - Updated dependencies [a9e4a40]
42
+ - Updated dependencies [5855db7]
43
+ - Updated dependencies [ec921d2]
44
+ - Updated dependencies [8740059]
45
+ - Updated dependencies [25a845a]
46
+ - Updated dependencies [103f2c3]
47
+ - Updated dependencies [9110ab8]
48
+ - Updated dependencies [cd2a888]
49
+ - Updated dependencies [eb3227a]
50
+ - Updated dependencies [b853dba]
51
+ - Updated dependencies [e561309]
52
+ - Updated dependencies [d4c6175]
53
+ - Updated dependencies [7a0e1ca]
54
+ - Updated dependencies [103b5d3]
55
+ - Updated dependencies [5a7a7f7]
56
+ - Updated dependencies [b076eae]
57
+ - Updated dependencies [73a24b7]
58
+ - Updated dependencies [2dbbc15]
59
+ - Updated dependencies [1cb1163]
60
+ - Updated dependencies [be3061e]
61
+ - Updated dependencies [75f8332]
62
+ - Updated dependencies [81935e2]
63
+ - Updated dependencies [f9ed01b]
64
+ - Updated dependencies [ac0ab50]
65
+ - Updated dependencies [5140ce1]
66
+ - Updated dependencies [522d75f]
67
+ - Updated dependencies [a7611b3]
68
+ - Updated dependencies [8823044]
69
+ - Updated dependencies [379439b]
70
+ - Updated dependencies [11c9352]
71
+ - Updated dependencies [6b70e46]
72
+ - Updated dependencies [135ac91]
73
+ - Updated dependencies [140cf0f]
74
+ - Updated dependencies [251e4d2]
75
+ - Updated dependencies [203ad34]
76
+ - Updated dependencies [e6c5113]
77
+ - Updated dependencies [e0f6478]
78
+ - Updated dependencies [baef56b]
79
+ - Updated dependencies [da0a8e3]
80
+ - Updated dependencies [2fd8017]
81
+ - Updated dependencies [5c3858e]
82
+ - Updated dependencies [507d602]
83
+ - Updated dependencies [df9f76b]
84
+ - @zhin.js/core@1.1.37
85
+ - @zhin.js/host-http@1.1.1
86
+ - @zhin.js/adapter@1.1.14
87
+ - @zhin.js/feature-kit@1.1.1
88
+ - @zhin.js/skill@1.1.1
89
+ - @zhin.js/logger@1.1.1
90
+ - @zhin.js/im-contract@1.1.1
91
+
92
+ ## 1.1.1
93
+
94
+ ### Patch Changes
95
+
96
+ - Updated dependencies [769e67f]
97
+ - @zhin.js/adapter@1.1.13
98
+ - @zhin.js/core@1.1.36
99
+
3
100
  ## 1.1.0
4
101
 
5
102
  ### Minor Changes
package/README.md CHANGED
@@ -18,11 +18,13 @@ pnpm add @zhin.js/adapter-milky
18
18
 
19
19
  ## Plugin Runtime
20
20
 
21
- - `@zhin.js/adapter` — 约定式 `adapters/milky.ts`(`defineAdapter`)
21
+ - `@zhin.js/adapter` — 约定式 `adapters/milky/index.ts`(`defineAdapter`)
22
22
  - `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
23
23
  - `zhin.js` — `plugin.ts`(`definePlugin`)
24
24
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
25
25
 
26
+ `AdapterIndex` 会把实例默认值与 `endpoints[]` 的逐项覆盖合并;协议层只接收一个已经展开的 endpoint 配置,不再读取嵌套 endpoint 或从进程环境推断 endpoint 身份。
27
+
26
28
  入站:`gateway.receive({ conversation: ConversationRef(kind: "private"|"group", id), message, content, sender, metadata })`
27
29
  出站:`send({ conversation, payload })` → HTTP `send_private_message` / `send_group_message`(payload 已由 gateway/core 渲染;无 segment-mapper)
28
30
 
@@ -47,7 +49,7 @@ plugins:
47
49
  reconnect_interval: 5000
48
50
  heartbeat_interval: 30000
49
51
  endpoints:
50
- - name: milky-bot
52
+ - id: milky-bot
51
53
  baseUrl: "http://127.0.0.1:8080"
52
54
  access_token: "${MILKY_ACCESS_TOKEN}"
53
55
  ```
@@ -77,8 +79,8 @@ plugins:
77
79
 
78
80
  | 类别 | 路径 |
79
81
  |------|------|
80
- | Permit 词汇 | `agent/PERMITS.md` |
81
- | 技能说明 | `agent/skills/milky.md` |
82
+ | Permit 词汇 | `PERMITS.md` |
83
+ | 技能说明 | `skills/milky/SKILL.md` |
82
84
 
83
85
  ## 文档链接
84
86
 
@@ -1,13 +1,13 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  /**
3
- * Convention entry: discover `adapters/milky.ts` → defineAdapter.
3
+ * Convention entry: discover `adapters/milky/index.ts` → defineAdapter.
4
4
  */
5
5
  import { defineAdapter } from 'zhin.js/adapter';
6
6
  import { httpHostToken } from '@zhin.js/host-http';
7
- import { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, } from "../lib/endpoint.js";
8
- import { resolveMilkyConfig, } from "../lib/protocol.js";
9
- import { milkyRuntimeStateToken } from "../lib/milky-runtime-state.js";
10
- export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, } from "../lib/endpoint.js";
7
+ import { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, } from "../../lib/endpoint.js";
8
+ import { resolveMilkyConfig, } from "../../lib/protocol.js";
9
+ import { milkyRuntimeStateToken } from "../../lib/milky-runtime-state.js";
10
+ export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, } from "../../lib/endpoint.js";
11
11
  export default defineAdapter({
12
12
  capabilities: ['inbound', 'outbound'],
13
13
  operations: ['recall'],
@@ -18,7 +18,7 @@ export default defineAdapter({
18
18
  },
19
19
  create(context) {
20
20
  const config = resolveMilkyConfig(context.config);
21
- // 注册到插件运行时状态(milky.endpoint list 的"运行中"数据源)
21
+ // 注册到插件运行时状态(milky endpoint list 的"运行中"数据源)
22
22
  context.use(milkyRuntimeStateToken).endpoints.set(config.id, {
23
23
  id: config.id,
24
24
  mode: config.connection,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Convention entry: discover `adapters/milky.ts` → defineAdapter.
2
+ * Convention entry: discover `adapters/milky/index.ts` → defineAdapter.
3
3
  */
4
4
  import { defineAdapter } from 'zhin.js/adapter';
5
5
  import { httpHostToken } from '@zhin.js/host-http';
@@ -8,19 +8,19 @@ import {
8
8
  MilkyWebhookEndpoint,
9
9
  MilkyWssEndpoint,
10
10
  MilkyWsEndpoint,
11
- } from '../src/endpoint.js';
11
+ } from '../../src/endpoint.js';
12
12
  import {
13
13
  resolveMilkyConfig,
14
- type MilkyAdapterConfig,
15
- } from '../src/protocol.js';
16
- import { milkyRuntimeStateToken } from '../src/milky-runtime-state.js';
14
+ type MilkyEndpointConfig,
15
+ } from '../../src/protocol.js';
16
+ import { milkyRuntimeStateToken } from '../../src/milky-runtime-state.js';
17
17
 
18
18
  export {
19
19
  MilkySseEndpoint,
20
20
  MilkyWebhookEndpoint,
21
21
  MilkyWssEndpoint,
22
22
  MilkyWsEndpoint,
23
- } from '../src/endpoint.js';
23
+ } from '../../src/endpoint.js';
24
24
  export type {
25
25
  CreateMilkySseStream,
26
26
  MilkySseEndpointOptions,
@@ -29,9 +29,9 @@ export type {
29
29
  MilkyWsEndpointOptions,
30
30
  MilkyWsCreateOptions,
31
31
  MilkyWsSocket,
32
- } from '../src/endpoint.js';
32
+ } from '../../src/endpoint.js';
33
33
 
34
- export default defineAdapter<MilkyAdapterConfig>({
34
+ export default defineAdapter<MilkyEndpointConfig>({
35
35
  capabilities: ['inbound', 'outbound'],
36
36
  operations: ['recall'],
37
37
  // Milky 协议资源 uri 消费 http(s):// 与 base64:// 形式;无卡片交互面,交互段降级纯文本。
@@ -41,7 +41,7 @@ export default defineAdapter<MilkyAdapterConfig>({
41
41
  },
42
42
  create(context) {
43
43
  const config = resolveMilkyConfig(context.config);
44
- // 注册到插件运行时状态(milky.endpoint list 的"运行中"数据源)
44
+ // 注册到插件运行时状态(milky endpoint list 的"运行中"数据源)
45
45
  context.use(milkyRuntimeStateToken).endpoints.set(config.id, {
46
46
  id: config.id,
47
47
  mode: config.connection,
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { milkyEndpointCommands } from "../../../lib/milky-endpoint-commands.js";
2
+ import { milkyEndpointCommands } from '../../definition.js';
3
3
  export default milkyEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { milkyEndpointCommands } from '../../definition.js';
2
+
3
+ export default milkyEndpointCommands.add;
@@ -1,10 +1,11 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
1
2
  /**
2
- * `milky.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
- * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
3
+ * `milky endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
4
+ * commands/milky/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
5
  */
5
6
  import { createEndpointCommands } from 'zhin.js/adapter';
6
7
  import { defineCommand } from 'zhin.js/command';
7
- import { milkyRuntimeStateToken } from './milky-runtime-state.js';
8
+ import { milkyRuntimeStateToken } from "../../../lib/milky-runtime-state.js";
8
9
  export const milkyEndpointCommands = createEndpointCommands({
9
10
  adapterKey: 'milky',
10
11
  adapterDisplayName: 'Milky',
@@ -1,10 +1,10 @@
1
1
  /**
2
- * `milky.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
- * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
2
+ * `milky endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/milky/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
4
  */
5
5
  import { createEndpointCommands } from 'zhin.js/adapter';
6
6
  import { defineCommand } from 'zhin.js/command';
7
- import { milkyRuntimeStateToken } from './milky-runtime-state.js';
7
+ import { milkyRuntimeStateToken } from '../../../src/milky-runtime-state.js';
8
8
 
9
9
  export const milkyEndpointCommands = createEndpointCommands({
10
10
  adapterKey: 'milky',
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { milkyEndpointCommands } from "../../lib/milky-endpoint-commands.js";
2
+ import { milkyEndpointCommands } from '../definition.js';
3
3
  export default milkyEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { milkyEndpointCommands } from '../definition.js';
2
+
3
+ export default milkyEndpointCommands.list;
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { milkyEndpointCommands } from "../../../lib/milky-endpoint-commands.js";
2
+ import { milkyEndpointCommands } from '../../definition.js';
3
3
  export default milkyEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { milkyEndpointCommands } from '../../definition.js';
2
+
3
+ export default milkyEndpointCommands.remove;
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatOutboundSegments, isMentioned, isMessageReceiveEvent, milkyInboundConversation, parseMessageReceiveData, resolveMilkyConfig, senderNickname, type MilkyAdapterConfig, type MilkyApiClientOptions, type MilkyApiResponse, type MilkyConfigBase, type MilkyEndpointConfig, type MilkyEvent, type MilkyIncomingMessage, type MilkyIncomingSegment, type MilkyOutgoingSegment, type MilkySseConfig, type MilkyWebhookConfig, type MilkyWireSegment, type MilkyWsConfig, type MilkyWssConfig, type ResolvedMilkyConfig, } from './protocol.js';
1
+ export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatOutboundSegments, isMentioned, isMessageReceiveEvent, milkyInboundConversation, parseMessageReceiveData, resolveMilkyConfig, senderNickname, type MilkyApiClientOptions, type MilkyApiResponse, type MilkyConfigBase, type MilkyEndpointConfig, type MilkyEvent, type MilkyIncomingMessage, type MilkyIncomingSegment, type MilkyOutgoingSegment, type MilkySseConfig, type MilkyWebhookConfig, type MilkyWireSegment, type MilkyWsConfig, type MilkyWssConfig, type ResolvedMilkyConfig, } from './protocol.js';
2
2
  export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, consumeSseBuffer, openSseStream, verifyMilkyAccessToken, type CreateMilkySseStream, type MilkySseEndpointOptions, type MilkyWebhookEndpointOptions, type MilkyWssEndpointOptions, type MilkyWsCreateOptions, type MilkyWsEndpointOptions, type MilkyWsSocket, } from './endpoint.js';
3
3
  export { MilkyClient, milkyClient, type MilkyClientEventMap, } from './client.js';
4
4
  export type { MilkyAdapterConfig as ImHelperMilkyAdapterConfig, MilkyActionUrlResolver, MilkyCall, MilkyMessageReceiveEvent, MilkyMessageRecallEvent, MilkyV1ClientConfig, MilkyV1Event, MilkyV1Response, } from '@imhelper/milky-v1';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Milky 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `milky.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `milky endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
  export const milkyRuntimeStateToken = defineEndpointRuntimeStateToken('milky');
package/lib/protocol.d.ts CHANGED
@@ -3,30 +3,17 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://milky.ntqqrev.org/
5
5
  */
6
+ import { type ConversationRef } from '@zhin.js/im-contract';
6
7
  import type { Segment } from '@zhin.js/core/runtime';
7
- import type { ConversationRef } from '@zhin.js/im-contract';
8
- /** Transitional legacy endpoint row (`endpoints[]` with `context: milky`). */
9
- export interface MilkyLegacyEndpointRow {
10
- readonly context?: string;
8
+ /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
9
+ export interface MilkyEndpointConfig {
11
10
  readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
12
- readonly id?: string;
13
- readonly baseUrl?: string;
14
- readonly access_token?: string;
15
- readonly path?: string;
16
- readonly reconnect_interval?: number;
17
- readonly heartbeat_interval?: number;
18
- }
19
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
20
- export interface MilkyAdapterConfig {
21
- readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
22
- readonly id?: string;
23
- readonly baseUrl?: string;
11
+ readonly id: string;
12
+ readonly baseUrl: string;
24
13
  readonly access_token?: string;
25
14
  readonly path?: string;
26
15
  readonly reconnect_interval?: number;
27
16
  readonly heartbeat_interval?: number;
28
- /** Transitional: legacy root `endpoints[]` with `context: milky`. */
29
- readonly endpoints?: ReadonlyArray<MilkyLegacyEndpointRow>;
30
17
  }
31
18
  export interface MilkyConfigBase {
32
19
  readonly context: 'milky';
@@ -57,7 +44,6 @@ export interface MilkyWssConfig extends MilkyConfigBase {
57
44
  readonly heartbeat_interval: number;
58
45
  }
59
46
  export type ResolvedMilkyConfig = MilkyWsConfig | MilkySseConfig | MilkyWebhookConfig | MilkyWssConfig;
60
- export type MilkyEndpointConfig = ResolvedMilkyConfig;
61
47
  export interface MilkyApiResponse<T = unknown> {
62
48
  status: 'ok' | 'failed';
63
49
  retcode: number;
@@ -108,7 +94,7 @@ export interface MilkyApiClientOptions {
108
94
  readonly baseUrl: string;
109
95
  readonly access_token?: string;
110
96
  }
111
- export declare function resolveMilkyConfig(config?: MilkyAdapterConfig): ResolvedMilkyConfig;
97
+ export declare function resolveMilkyConfig(config: MilkyEndpointConfig): ResolvedMilkyConfig;
112
98
  /**
113
99
  * 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
114
100
  * HTTP 层错误抛出;协议响应原样返回,由 Client 公开 call() 保留完整语义。
package/lib/protocol.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://milky.ntqqrev.org/
5
5
  */
6
- import { isMediaRef } from '@zhin.js/core';
6
+ import { isMediaRef } from '@zhin.js/im-contract';
7
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
8
8
  const logger = getLogger('milky');
9
9
  function normalizeConnection(connection) {
@@ -11,18 +11,11 @@ function normalizeConnection(connection) {
11
11
  return connection;
12
12
  return 'ws';
13
13
  }
14
- export function resolveMilkyConfig(config = {}) {
15
- const entry = config.endpoints?.find((item) => item.context === 'milky');
16
- const connection = normalizeConnection(config.connection ?? entry?.connection);
17
- const id = (typeof config.id === 'string' && config.id)
18
- || (typeof entry?.id === 'string' && entry.id)
19
- || process.env.MILKY_BOT_NAME
20
- || 'milky-bot';
21
- const baseUrl = config.baseUrl ?? entry?.baseUrl;
22
- if (!baseUrl) {
23
- throw new TypeError('Milky requires baseUrl (plugins.<key>.baseUrl or endpoints with context: milky)');
24
- }
25
- const access_token = config.access_token ?? entry?.access_token;
14
+ export function resolveMilkyConfig(config) {
15
+ const connection = normalizeConnection(config.connection);
16
+ const id = requiredEndpointField(config.id, 'id');
17
+ const baseUrl = requiredEndpointField(config.baseUrl, 'baseUrl');
18
+ const access_token = optionalEndpointField(config.access_token);
26
19
  const base = {
27
20
  context: 'milky',
28
21
  id,
@@ -33,38 +26,42 @@ export function resolveMilkyConfig(config = {}) {
33
26
  return {
34
27
  ...base,
35
28
  connection: 'ws',
36
- reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
37
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
29
+ reconnect_interval: config.reconnect_interval ?? 5000,
30
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
38
31
  };
39
32
  }
40
33
  if (connection === 'sse') {
41
34
  return {
42
35
  ...base,
43
36
  connection: 'sse',
44
- reconnect_interval: config.reconnect_interval
45
- ?? entry?.reconnect_interval
46
- ?? 5_000,
37
+ reconnect_interval: config.reconnect_interval ?? 5_000,
47
38
  };
48
39
  }
49
40
  if (connection === 'webhook') {
50
- const path = config.path ?? entry?.path;
51
- if (!path)
52
- throw new TypeError('Milky connection:webhook requires path');
41
+ const path = requiredEndpointField(config.path, 'path');
53
42
  return { ...base, connection: 'webhook', path };
54
43
  }
55
44
  if (connection === 'wss') {
56
- const path = config.path ?? entry?.path;
57
- if (!path)
58
- throw new TypeError('Milky connection:wss requires path');
45
+ const path = requiredEndpointField(config.path, 'path');
59
46
  return {
60
47
  ...base,
61
48
  connection: 'wss',
62
49
  path,
63
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
50
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
64
51
  };
65
52
  }
66
53
  throw new TypeError(`Unknown Milky connection: ${String(connection)}`);
67
54
  }
55
+ function requiredEndpointField(value, field) {
56
+ const resolved = optionalEndpointField(value);
57
+ if (!resolved) {
58
+ throw new TypeError(`Milky endpoint requires a non-empty ${field}`);
59
+ }
60
+ return resolved;
61
+ }
62
+ function optionalEndpointField(value) {
63
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
64
+ }
68
65
  /** 鉴权:Header Authorization: Bearer {token} 或 URL query access_token=xxx */
69
66
  function authHeaders(access_token) {
70
67
  const headers = {};
@@ -18,16 +18,17 @@ export class MilkyWssEndpoint extends ClientEndpoint {
18
18
  #ws;
19
19
  #wsRelease;
20
20
  #clientSocketRelease;
21
- #lifecycle;
21
+ #connectionLifecycle;
22
+ #connectionTask = Promise.resolve();
23
+ #started = false;
22
24
  constructor(options) {
23
25
  super();
24
26
  this.#logger = getAdapterLogger('milky', options.config.id);
25
27
  this.#options = options;
26
28
  this.#callApi = options.callApi ?? callApi;
27
- this.#lifecycle = createEndpointLifecycle({
28
- name: options.config.id,
29
+ this.#connectionLifecycle = createEndpointLifecycle({
30
+ name: `${options.config.id}:inbound`,
29
31
  reconnect: false,
30
- heartbeat: { intervalMs: options.config.heartbeat_interval },
31
32
  });
32
33
  this.client = createMilkyEndpointClient(options.config, this.#callApi);
33
34
  this.management = createMilkyEndpointManagement({
@@ -39,23 +40,19 @@ export class MilkyWssEndpoint extends ClientEndpoint {
39
40
  }, (_name, error) => this.#warnPlatformEvent(error));
40
41
  }
41
42
  async start() {
42
- await this.#lifecycle.start(async (lifecycleHandle) => {
43
- const handle = this.#options.http.ws(this.#options.config.path);
44
- this.#wsRelease = handle.onConnection((connection) => {
45
- this.#acceptConnection(connection);
46
- });
47
- lifecycleHandle.onForceClose(() => {
48
- this.#wsRelease?.();
49
- this.#wsRelease = undefined;
50
- this.#clientSocketRelease?.();
51
- this.#clientSocketRelease = undefined;
52
- try {
53
- this.#ws?.close();
54
- }
55
- catch {
56
- /* ignore */
57
- }
58
- this.#ws = undefined;
43
+ if (this.#started)
44
+ return;
45
+ this.#started = true;
46
+ const handle = this.#options.http.ws(this.#options.config.path);
47
+ this.#wsRelease = handle.onConnection((connection) => {
48
+ this.#connectionTask = this.#connectionTask
49
+ .then(() => this.#acceptConnection(connection))
50
+ .catch((error) => {
51
+ this.#logger.warn(formatCompact({
52
+ op: 'wss_connection_failed',
53
+ endpoint: this.#options.config.id,
54
+ error: error instanceof Error ? error.message : String(error),
55
+ }));
59
56
  });
60
57
  });
61
58
  this.#logger.info(formatCompact({
@@ -67,7 +64,12 @@ export class MilkyWssEndpoint extends ClientEndpoint {
67
64
  }
68
65
  async stop() {
69
66
  this.close();
70
- await this.#lifecycle.stop();
67
+ this.#wsRelease?.();
68
+ this.#wsRelease = undefined;
69
+ await this.#connectionTask;
70
+ await this.#connectionLifecycle.stop();
71
+ this.#ws = undefined;
72
+ this.#started = false;
71
73
  }
72
74
  async send({ conversation, payload }) {
73
75
  const message = formatOutboundSegments(payload);
@@ -141,39 +143,52 @@ export class MilkyWssEndpoint extends ClientEndpoint {
141
143
  }));
142
144
  });
143
145
  }
144
- #acceptConnection(connection) {
146
+ async #acceptConnection(connection) {
145
147
  if (!verifyMilkyAccessToken(this.#options.config.access_token, connection.request)) {
146
148
  connection.socket.close(4003, 'Unauthorized');
147
149
  return;
148
150
  }
149
151
  const socket = connection.socket;
150
- if (this.#ws) {
151
- try {
152
- this.#ws.close();
153
- }
154
- catch {
155
- /* ignore */
156
- }
157
- }
158
- this.#ws = socket;
159
- this.#lifecycle.startHeartbeat(() => {
160
- try {
161
- if (this.#ws?.readyState === WS_OPEN)
162
- this.#ws.ping?.();
163
- }
164
- catch {
165
- /* ignore */
166
- }
167
- });
168
- this.#clientSocketRelease?.();
169
- this.#clientSocketRelease = this.client.acceptWebSocket(socket);
170
- socket.on('close', () => {
171
- this.#clientSocketRelease?.();
172
- this.#clientSocketRelease = undefined;
173
- if (this.#ws === socket) {
174
- this.#ws = undefined;
175
- this.#lifecycle.stopHeartbeat();
176
- }
152
+ await this.#connectionLifecycle.stop();
153
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
154
+ this.#ws = socket;
155
+ const releaseClientSocket = this.client.acceptWebSocket(socket);
156
+ this.#clientSocketRelease = releaseClientSocket;
157
+ lifecycleHandle.onForceClose(() => {
158
+ if (this.#clientSocketRelease === releaseClientSocket) {
159
+ releaseClientSocket();
160
+ this.#clientSocketRelease = undefined;
161
+ }
162
+ try {
163
+ socket.close();
164
+ }
165
+ catch {
166
+ /* ignore */
167
+ }
168
+ if (this.#ws === socket)
169
+ this.#ws = undefined;
170
+ });
171
+ this.#connectionLifecycle.startHeartbeat(() => {
172
+ try {
173
+ if (socket.readyState === WS_OPEN)
174
+ socket.ping?.();
175
+ }
176
+ catch {
177
+ /* ignore */
178
+ }
179
+ }, this.#options.config.heartbeat_interval);
180
+ socket.on('message', () => {
181
+ this.#connectionLifecycle.notifyHeartbeatAck();
182
+ });
183
+ socket.on('close', () => {
184
+ if (this.#clientSocketRelease === releaseClientSocket) {
185
+ releaseClientSocket();
186
+ this.#clientSocketRelease = undefined;
187
+ }
188
+ if (this.#ws === socket)
189
+ this.#ws = undefined;
190
+ lifecycleHandle.notifyClosed(new Error('Milky reverse WebSocket closed'));
191
+ });
177
192
  });
178
193
  this.#logger.debug(formatCompact({
179
194
  endpoint: this.#options.config.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-milky",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "description": "Zhin.js Milky adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,9 +19,10 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "agent",
22
+ "skills",
23
23
  "README.md",
24
- "CHANGELOG.md"
24
+ "CHANGELOG.md",
25
+ "PERMITS.md"
25
26
  ],
26
27
  "keywords": [
27
28
  "zhin",
@@ -42,12 +43,13 @@
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.12",
46
- "@zhin.js/core": "1.1.35",
47
- "@zhin.js/feature-kit": "1.1.0",
48
- "@zhin.js/host-http": "1.1.0",
49
- "@zhin.js/im-contract": "1.1.0",
50
- "@zhin.js/logger": "1.1.0"
46
+ "@zhin.js/adapter": "1.1.15",
47
+ "@zhin.js/im-contract": "1.1.2",
48
+ "@zhin.js/logger": "1.1.1",
49
+ "@zhin.js/skill": "1.1.2",
50
+ "@zhin.js/core": "1.1.38",
51
+ "@zhin.js/feature-kit": "1.1.2",
52
+ "@zhin.js/host-http": "1.1.2"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@types/node": "^26.1.2",
@@ -55,17 +57,17 @@
55
57
  "typescript": "^6.0.3",
56
58
  "vitest": "^4.1.10",
57
59
  "zod": "^4.4.3",
58
- "@zhin.js/agent": "1.1.23",
59
- "@zhin.js/host-http": "1.1.0",
60
- "zhin.js": "1.1.0"
60
+ "@zhin.js/host-http": "1.1.2",
61
+ "zhin.js": "1.1.3",
62
+ "@zhin.js/agent": "1.1.27"
61
63
  },
62
64
  "peerDependencies": {
63
65
  "zod": "^4.0.0",
64
- "@zhin.js/adapter": "^1.1.12",
65
- "@zhin.js/agent": "^1.1.23",
66
- "@zhin.js/command": "^1.1.0",
67
- "@zhin.js/core": "^1.1.35",
68
- "zhin.js": "^1.1.0"
66
+ "@zhin.js/adapter": "^1.1.15",
67
+ "@zhin.js/agent": "^1.1.27",
68
+ "@zhin.js/command": "^1.1.2",
69
+ "@zhin.js/core": "^1.1.38",
70
+ "zhin.js": "^1.1.3"
69
71
  },
70
72
  "peerDependenciesMeta": {
71
73
  "@zhin.js/agent": {
@@ -107,6 +109,10 @@
107
109
  {
108
110
  "package": "@zhin.js/command",
109
111
  "api": "^1.0.0"
112
+ },
113
+ {
114
+ "package": "@zhin.js/skill",
115
+ "api": "^1.0.0"
110
116
  }
111
117
  ],
112
118
  "plugins": []
package/plugin.js CHANGED
@@ -8,7 +8,7 @@ export default definePlugin({
8
8
  displayName: 'Milky Adapter',
9
9
  },
10
10
  setup(context) {
11
- // 运行中 endpoint 注册表(milky.endpoint list 的"运行中"数据源)
11
+ // 运行中 endpoint 注册表(milky endpoint list 的"运行中"数据源)
12
12
  context.resources.provide(milkyRuntimeStateToken, createEndpointRuntimeState());
13
13
  },
14
14
  });
package/schema.json CHANGED
@@ -42,10 +42,11 @@
42
42
  },
43
43
  "endpoints": {
44
44
  "type": "array",
45
+ "minItems": 1,
45
46
  "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
46
47
  "items": {
47
48
  "type": "object",
48
- "additionalProperties": true,
49
+ "additionalProperties": false,
49
50
  "properties": {
50
51
  "master": {
51
52
  "type": [
@@ -65,6 +66,15 @@
65
66
  },
66
67
  "description": "本 endpoint 的 trusted 列表"
67
68
  },
69
+ "connection": {
70
+ "type": "string",
71
+ "enum": [
72
+ "ws",
73
+ "sse",
74
+ "webhook",
75
+ "wss"
76
+ ]
77
+ },
68
78
  "baseUrl": {
69
79
  "type": "string",
70
80
  "description": "Milky HTTP API base URL (required); WS event path is derived as ws(s)://host/event"
@@ -77,6 +87,15 @@
77
87
  "type": "string",
78
88
  "description": "Milky access token"
79
89
  },
90
+ "reconnect_interval": {
91
+ "type": "number"
92
+ },
93
+ "heartbeat_interval": {
94
+ "type": "number"
95
+ },
96
+ "commandPrefix": {
97
+ "type": "string"
98
+ },
80
99
  "id": {
81
100
  "type": "string",
82
101
  "description": "Milky bot name"
package/src/index.ts CHANGED
@@ -12,7 +12,6 @@ export {
12
12
  parseMessageReceiveData,
13
13
  resolveMilkyConfig,
14
14
  senderNickname,
15
- type MilkyAdapterConfig,
16
15
  type MilkyApiClientOptions,
17
16
  type MilkyApiResponse,
18
17
  type MilkyConfigBase,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Milky 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `milky.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `milky endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -4,36 +4,21 @@
4
4
  * Spec: https://milky.ntqqrev.org/
5
5
  */
6
6
 
7
- import { isMediaRef } from '@zhin.js/core';
7
+ import { isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
8
8
  import type { Segment } from '@zhin.js/core/runtime';
9
- import type { ConversationRef } from '@zhin.js/im-contract';
10
9
  import { formatCompact, getLogger } from '@zhin.js/logger';
11
10
 
12
11
  const logger = getLogger('milky');
13
12
 
14
- /** Transitional legacy endpoint row (`endpoints[]` with `context: milky`). */
15
- export interface MilkyLegacyEndpointRow {
16
- readonly context?: string;
13
+ /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
14
+ export interface MilkyEndpointConfig {
17
15
  readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
18
- readonly id?: string;
19
- readonly baseUrl?: string;
20
- readonly access_token?: string;
21
- readonly path?: string;
22
- readonly reconnect_interval?: number;
23
- readonly heartbeat_interval?: number;
24
- }
25
-
26
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
27
- export interface MilkyAdapterConfig {
28
- readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
29
- readonly id?: string;
30
- readonly baseUrl?: string;
16
+ readonly id: string;
17
+ readonly baseUrl: string;
31
18
  readonly access_token?: string;
32
19
  readonly path?: string;
33
20
  readonly reconnect_interval?: number;
34
21
  readonly heartbeat_interval?: number;
35
- /** Transitional: legacy root `endpoints[]` with `context: milky`. */
36
- readonly endpoints?: ReadonlyArray<MilkyLegacyEndpointRow>;
37
22
  }
38
23
 
39
24
  export interface MilkyConfigBase {
@@ -74,7 +59,6 @@ export type ResolvedMilkyConfig =
74
59
  | MilkySseConfig
75
60
  | MilkyWebhookConfig
76
61
  | MilkyWssConfig;
77
- export type MilkyEndpointConfig = ResolvedMilkyConfig;
78
62
 
79
63
  export interface MilkyApiResponse<T = unknown> {
80
64
  status: 'ok' | 'failed';
@@ -127,20 +111,11 @@ function normalizeConnection(connection: string | undefined): 'ws' | 'sse' | 'we
127
111
  return 'ws';
128
112
  }
129
113
 
130
- export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMilkyConfig {
131
- const entry = config.endpoints?.find((item) => item.context === 'milky');
132
- const connection = normalizeConnection(config.connection ?? entry?.connection);
133
- const id = (typeof config.id === 'string' && config.id)
134
- || (typeof entry?.id === 'string' && entry.id)
135
- || process.env.MILKY_BOT_NAME
136
- || 'milky-bot';
137
- const baseUrl = config.baseUrl ?? entry?.baseUrl;
138
- if (!baseUrl) {
139
- throw new TypeError(
140
- 'Milky requires baseUrl (plugins.<key>.baseUrl or endpoints with context: milky)',
141
- );
142
- }
143
- const access_token = config.access_token ?? entry?.access_token;
114
+ export function resolveMilkyConfig(config: MilkyEndpointConfig): ResolvedMilkyConfig {
115
+ const connection = normalizeConnection(config.connection);
116
+ const id = requiredEndpointField(config.id, 'id');
117
+ const baseUrl = requiredEndpointField(config.baseUrl, 'baseUrl');
118
+ const access_token = optionalEndpointField(config.access_token);
144
119
  const base = {
145
120
  context: 'milky' as const,
146
121
  id,
@@ -152,8 +127,8 @@ export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMil
152
127
  return {
153
128
  ...base,
154
129
  connection: 'ws',
155
- reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
156
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
130
+ reconnect_interval: config.reconnect_interval ?? 5000,
131
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
157
132
  };
158
133
  }
159
134
 
@@ -161,32 +136,43 @@ export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMil
161
136
  return {
162
137
  ...base,
163
138
  connection: 'sse',
164
- reconnect_interval: config.reconnect_interval
165
- ?? entry?.reconnect_interval
166
- ?? 5_000,
139
+ reconnect_interval: config.reconnect_interval ?? 5_000,
167
140
  };
168
141
  }
169
142
 
170
143
  if (connection === 'webhook') {
171
- const path = config.path ?? entry?.path;
172
- if (!path) throw new TypeError('Milky connection:webhook requires path');
144
+ const path = requiredEndpointField(config.path, 'path');
173
145
  return { ...base, connection: 'webhook', path };
174
146
  }
175
147
 
176
148
  if (connection === 'wss') {
177
- const path = config.path ?? entry?.path;
178
- if (!path) throw new TypeError('Milky connection:wss requires path');
149
+ const path = requiredEndpointField(config.path, 'path');
179
150
  return {
180
151
  ...base,
181
152
  connection: 'wss',
182
153
  path,
183
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
154
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
184
155
  };
185
156
  }
186
157
 
187
158
  throw new TypeError(`Unknown Milky connection: ${String(connection)}`);
188
159
  }
189
160
 
161
+ function requiredEndpointField(
162
+ value: unknown,
163
+ field: 'id' | 'baseUrl' | 'path',
164
+ ): string {
165
+ const resolved = optionalEndpointField(value);
166
+ if (!resolved) {
167
+ throw new TypeError(`Milky endpoint requires a non-empty ${field}`);
168
+ }
169
+ return resolved;
170
+ }
171
+
172
+ function optionalEndpointField(value: unknown): string | undefined {
173
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
174
+ }
175
+
190
176
  /** 鉴权:Header Authorization: Bearer {token} 或 URL query access_token=xxx */
191
177
  function authHeaders(access_token?: string): Record<string, string> {
192
178
  const headers: Record<string, string> = {};
@@ -56,17 +56,18 @@ export class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
56
56
  #ws?: MilkyWsSocket;
57
57
  #wsRelease?: () => void;
58
58
  #clientSocketRelease?: () => void;
59
- readonly #lifecycle: EndpointLifecycle;
59
+ readonly #connectionLifecycle: EndpointLifecycle;
60
+ #connectionTask = Promise.resolve();
61
+ #started = false;
60
62
 
61
63
  constructor(options: MilkyWssEndpointOptions) {
62
64
  super();
63
65
  this.#logger = getAdapterLogger('milky', options.config.id);
64
66
  this.#options = options;
65
67
  this.#callApi = options.callApi ?? callApi;
66
- this.#lifecycle = createEndpointLifecycle({
67
- name: options.config.id,
68
+ this.#connectionLifecycle = createEndpointLifecycle({
69
+ name: `${options.config.id}:inbound`,
68
70
  reconnect: false,
69
- heartbeat: { intervalMs: options.config.heartbeat_interval },
70
71
  });
71
72
  this.client = createMilkyEndpointClient(options.config, this.#callApi);
72
73
  this.management = createMilkyEndpointManagement({
@@ -82,23 +83,19 @@ export class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
82
83
  }
83
84
 
84
85
  async start(): Promise<void> {
85
- await this.#lifecycle.start(async (lifecycleHandle) => {
86
- const handle = this.#options.http.ws(this.#options.config.path);
87
- this.#wsRelease = handle.onConnection((connection) => {
88
- this.#acceptConnection(connection);
89
- });
90
- lifecycleHandle.onForceClose(() => {
91
- this.#wsRelease?.();
92
- this.#wsRelease = undefined;
93
- this.#clientSocketRelease?.();
94
- this.#clientSocketRelease = undefined;
95
- try {
96
- this.#ws?.close();
97
- } catch {
98
- /* ignore */
99
- }
100
- this.#ws = undefined;
101
- });
86
+ if (this.#started) return;
87
+ this.#started = true;
88
+ const handle = this.#options.http.ws(this.#options.config.path);
89
+ this.#wsRelease = handle.onConnection((connection) => {
90
+ this.#connectionTask = this.#connectionTask
91
+ .then(() => this.#acceptConnection(connection))
92
+ .catch((error) => {
93
+ this.#logger.warn(formatCompact({
94
+ op: 'wss_connection_failed',
95
+ endpoint: this.#options.config.id,
96
+ error: error instanceof Error ? error.message : String(error),
97
+ }));
98
+ });
102
99
  });
103
100
  this.#logger.info(formatCompact({
104
101
  op: 'listen',
@@ -110,7 +107,12 @@ export class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
110
107
 
111
108
  async stop(): Promise<void> {
112
109
  this.close();
113
- await this.#lifecycle.stop();
110
+ this.#wsRelease?.();
111
+ this.#wsRelease = undefined;
112
+ await this.#connectionTask;
113
+ await this.#connectionLifecycle.stop();
114
+ this.#ws = undefined;
115
+ this.#started = false;
114
116
  }
115
117
 
116
118
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
@@ -187,36 +189,47 @@ export class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
187
189
  });
188
190
  }
189
191
 
190
- #acceptConnection(connection: WsConnection): void {
192
+ async #acceptConnection(connection: WsConnection): Promise<void> {
191
193
  if (!verifyMilkyAccessToken(this.#options.config.access_token, connection.request)) {
192
194
  connection.socket.close(4003, 'Unauthorized');
193
195
  return;
194
196
  }
195
197
  const socket = connection.socket as unknown as MilkyWsSocket;
196
- if (this.#ws) {
197
- try {
198
- this.#ws.close();
199
- } catch {
200
- /* ignore */
201
- }
202
- }
203
- this.#ws = socket;
204
- this.#lifecycle.startHeartbeat(() => {
205
- try {
206
- if (this.#ws?.readyState === WS_OPEN) this.#ws.ping?.();
207
- } catch {
208
- /* ignore */
209
- }
210
- });
211
- this.#clientSocketRelease?.();
212
- this.#clientSocketRelease = this.client.acceptWebSocket(socket);
213
- socket.on('close', () => {
214
- this.#clientSocketRelease?.();
215
- this.#clientSocketRelease = undefined;
216
- if (this.#ws === socket) {
217
- this.#ws = undefined;
218
- this.#lifecycle.stopHeartbeat();
219
- }
198
+ await this.#connectionLifecycle.stop();
199
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
200
+ this.#ws = socket;
201
+ const releaseClientSocket = this.client.acceptWebSocket(socket);
202
+ this.#clientSocketRelease = releaseClientSocket;
203
+ lifecycleHandle.onForceClose(() => {
204
+ if (this.#clientSocketRelease === releaseClientSocket) {
205
+ releaseClientSocket();
206
+ this.#clientSocketRelease = undefined;
207
+ }
208
+ try {
209
+ socket.close();
210
+ } catch {
211
+ /* ignore */
212
+ }
213
+ if (this.#ws === socket) this.#ws = undefined;
214
+ });
215
+ this.#connectionLifecycle.startHeartbeat(() => {
216
+ try {
217
+ if (socket.readyState === WS_OPEN) socket.ping?.();
218
+ } catch {
219
+ /* ignore */
220
+ }
221
+ }, this.#options.config.heartbeat_interval);
222
+ socket.on('message', () => {
223
+ this.#connectionLifecycle.notifyHeartbeatAck();
224
+ });
225
+ socket.on('close', () => {
226
+ if (this.#clientSocketRelease === releaseClientSocket) {
227
+ releaseClientSocket();
228
+ this.#clientSocketRelease = undefined;
229
+ }
230
+ if (this.#ws === socket) this.#ws = undefined;
231
+ lifecycleHandle.notifyClosed(new Error('Milky reverse WebSocket closed'));
232
+ });
220
233
  });
221
234
  this.#logger.debug(formatCompact({
222
235
  endpoint: this.#options.config.id,
@@ -1,65 +0,0 @@
1
- ---
2
- name: milky
3
- platforms:
4
- - milky
5
- description: >-
6
- Milky 协议群管理能力。当用户在 Milky 协议的 QQ 群中请求踢人、禁言、设管理员、
7
- 改名片/群名或查成员/群信息时使用。即使用户没有提到 Milky,只要 Endpoint 平台是
8
- Milky 协议且涉及群管理操作,就应触发。仅有昵称时必须先查成员列表获取 user_id。
9
- keywords:
10
- - milky
11
- - adapter:milky
12
- - 群管理
13
- - 禁言
14
- - 踢人
15
- - 管理员
16
- - list_members
17
- tags:
18
- - group
19
- - management
20
- - im
21
- tools:
22
- - milky_kick_member
23
- - milky_mute_member
24
- - milky_mute_all
25
- - milky_set_admin
26
- - milky_set_nickname
27
- - milky_set_group_name
28
- - milky_list_members
29
- - milky_get_group_info
30
- ---
31
-
32
- # Milky 群管理技能
33
-
34
- Milky 是 QQ 生态中的一种协议实现,提供标准群管理工具集。
35
-
36
- ## 核心原则
37
-
38
- ### 先查后操作
39
-
40
- 只有昵称时,先 `milky_list_members` 获取 QQ 号再操作。
41
-
42
- **Example:**
43
- ```
44
- 用户: 禁言那个叫"阿强"的
45
- 步骤1: milky_list_members → 找到 user_id = 654321
46
- 步骤2: milky_mute_member(user_id=654321, duration=600)
47
- ```
48
-
49
- ## 工具详解
50
-
51
- | 工具 | 用途 | 权限 | 关键参数 |
52
- |------|------|------|----------|
53
- | `milky_kick_member` | 踢出成员 | admin | `user_id` |
54
- | `milky_mute_member` | 禁言/解禁 | admin | `user_id`, `duration`(秒,**0=解禁**) |
55
- | `milky_mute_all` | 全员禁言/解除 | admin | `enable` |
56
- | `milky_set_admin` | 设/取消管理员 | owner | `user_id`, `enable` |
57
- | `milky_set_nickname` | 改群名片 | admin | `user_id`, `nickname` |
58
- | `milky_set_group_name` | 改群名 | admin | `name` |
59
- | `milky_list_members` | 成员列表 | user | — |
60
- | `milky_get_group_info` | 群信息 | user | — |
61
-
62
- ## 易错点
63
-
64
- 1. **禁言 duration=0 是解禁**,不是永久禁言。
65
- 2. **管理员设置需要群主权限**(owner),管理员无法设置其他管理员。
@@ -1,3 +0,0 @@
1
- import { milkyEndpointCommands } from '../../../src/milky-endpoint-commands.js';
2
-
3
- export default milkyEndpointCommands.add;
@@ -1,3 +0,0 @@
1
- import { milkyEndpointCommands } from '../../src/milky-endpoint-commands.js';
2
-
3
- export default milkyEndpointCommands.list;
@@ -1,3 +0,0 @@
1
- import { milkyEndpointCommands } from '../../../src/milky-endpoint-commands.js';
2
-
3
- export default milkyEndpointCommands.remove;
@@ -1 +0,0 @@
1
- export declare const milkyEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
File without changes