@lmcl/ailo-mcp-sdk 0.0.4 → 0.2.0

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/README.md CHANGED
@@ -40,65 +40,36 @@ server.registerTool(
40
40
  runMcp(server);
41
41
  ```
42
42
 
43
- ### 场景二:纯接收(平台有事件时推送给 AILO)
43
+ ### 场景二:双向通道(接收平台消息 + 工具发消息)
44
44
 
45
45
  ```typescript
46
- import type { BridgeHandler, BridgeMessage } from "@lmcl/ailo-mcp-sdk";
46
+ import type { BridgeHandler, ChannelContext } from "@lmcl/ailo-mcp-sdk";
47
47
  import { runMcpChannel } from "@lmcl/ailo-mcp-sdk";
48
48
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
49
49
 
50
50
  class MyHandler implements BridgeHandler {
51
- private onMessage?: (msg: BridgeMessage) => void;
51
+ private ctx!: ChannelContext;
52
52
 
53
- setOnMessage(fn: (msg: BridgeMessage) => void) {
54
- this.onMessage = fn;
55
- }
56
-
57
- async start() {
53
+ async start(ctx: ChannelContext) {
54
+ this.ctx = ctx;
58
55
  // 启动平台连接(WebSocket / long polling 等)
59
- // 收到消息时:this.onMessage?.({ text, contextTags, attachments? })
56
+ // 收到消息时:await ctx.accept({ text, contextTags, attachments? })
57
+ // 读写持久化数据:await ctx.storage.getData("key") / setData / deleteData
60
58
  }
61
59
 
62
- stop?() {
60
+ async stop() {
63
61
  // 断开平台连接
64
62
  }
65
63
  }
66
64
 
67
65
  const handler = new MyHandler();
68
66
  const server = new McpServer({ name: "channel:my-platform", version: "1.0.0" });
67
+ // 注册出站工具...
69
68
 
70
69
  runMcpChannel({
71
70
  handler,
72
71
  mcpServer: server,
73
- buildChannelPrompt: () => "本通道规则说明(如 @提及格式、ID 格式等)",
74
- });
75
- ```
76
-
77
- ### 场景三:双向收发(既有推送,也有工具)
78
-
79
- 在场景二基础上,给 MCP Server 注册发消息工具即可:
80
-
81
- ```typescript
82
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
83
- import { z } from "zod";
84
-
85
- const server = new McpServer({ name: "channel:my-platform", version: "1.0.0" });
86
-
87
- server.registerTool(
88
- "send_message",
89
- {
90
- description: "发送消息到平台",
91
- inputSchema: { chat_id: z.string(), text: z.string() },
92
- },
93
- async ({ chat_id, text }) => {
94
- // 调用平台 API 或 handler 发消息
95
- return { content: [{ type: "text", text: "已发送" }] };
96
- }
97
- );
98
-
99
- runMcpChannel({
100
- handler: new MyHandler(),
101
- mcpServer: server,
72
+ displayName: "我的平台",
102
73
  buildChannelPrompt: () => "本通道规则说明",
103
74
  });
104
75
  ```
@@ -109,64 +80,36 @@ runMcpChannel({
109
80
 
110
81
  MCP 使用 **stdio** 传输,**stdout 只能输出 JSON-RPC 消息**。任何 `console.log`、第三方库日志写入 stdout 都会破坏协议,导致解析错误。
111
82
 
112
- **本 SDK 已内置保护**:自动拦截 stdout,仅 JSON-RPC 转发到 stdout,其余重定向到 stderr。你可以自由使用 `console.log`、`console.info`、`console.debug`,以及会打日志的第三方库(如飞书 SDK),无需额外处理。
113
-
114
- **入口顺序**:请将 `import { runMcpChannel } from "@lmcl/ailo-mcp-sdk"` 作为入口文件的**首个 import**,确保保护在 dotenv、平台 SDK 等之前生效。若入口结构复杂,可显式第一行写:
83
+ **本 SDK 已内置保护**:自动拦截 stdout,仅 JSON-RPC 转发到 stdout,其余重定向到 stderr。你可以自由使用 `console.log`、`console.info`、`console.debug`,以及会打日志的第三方库,无需额外处理。
115
84
 
116
- ```typescript
117
- import "@lmcl/ailo-mcp-sdk/stdio-guard";
118
- import "dotenv/config";
119
- import { runMcpChannel } from "@lmcl/ailo-mcp-sdk";
120
- ```
85
+ **入口顺序**:请将 `import { runMcpChannel } from "@lmcl/ailo-mcp-sdk"` 作为入口文件的**首个 import**,确保保护在 dotenv、平台 SDK 等之前生效。
121
86
 
122
87
  ---
123
88
 
124
89
  ## BridgeHandler 接口
125
90
 
126
- 通道需实现 `BridgeHandler`,负责平台连接与消息转发:
91
+ 通道需实现 `BridgeHandler`,SDK 保证 `start(ctx)` 调用时 `ctx` 已就绪:
92
+
93
+ | 方法 | 说明 |
94
+ |------|------|
95
+ | `start(ctx)` | 启动平台连接。`ctx.accept(msg)` 投递消息到 AILO,`ctx.storage` 读写持久化数据 |
96
+ | `stop()` | 优雅停止。SDK 在 SIGINT/SIGTERM 时调用 |
97
+
98
+ ### ChannelContext
127
99
 
128
- | 方法 | 必填 | 说明 |
100
+ | 字段 | 类型 | 说明 |
129
101
  |------|------|------|
130
- | `setOnMessage(fn)` | 是 | SDK 注入回调。平台有消息时调用 `fn(msg)`,msg 必须自带 contextTags(时空场) |
131
- | `start()` | | 启动平台连接(WebSocket / long polling 等) |
132
- | `stop?()` | 否 | 断开平台连接,优雅退出时调用 |
133
- | `setDataProvider?(storage)` | 否 | SDK 连接后注入持久化存储,用于通道数据(如外部用户映射) |
102
+ | `accept(msg)` | `(msg: BridgeMessage) => Promise<void>` | 投递入站消息到 AILO。SDK 内部做空消息过滤 |
103
+ | `storage` | `ChannelStorage` | 通道级 KV 存储(`getData`/`setData`/`deleteData`),数据持久化在 AILO |
134
104
 
135
105
  ### BridgeMessage(时空场模型)
136
106
 
137
- 通道自己定义,全在 contextTags 里:
138
-
139
107
  | 字段 | 类型 | 说明 |
140
108
  |------|------|------|
141
- | `text` | string | 消息正文 |
109
+ | `text` | string? | 消息正文 |
142
110
  | `contextTags` | ContextTag[] | 时空场标签,必须 |
143
111
  | `attachments` | Attachment[]? | 附件 |
144
-
145
- ---
146
-
147
- ## 通道持久化数据(可选)
148
-
149
- 通道运行时产生的持久化数据(如外部用户映射)可存于 AILO 本体。实现 `setDataProvider` 后,SDK 会注入带 `getData` / `setData` / `deleteData` 的对象:
150
-
151
- ```typescript
152
- class MyHandler implements BridgeHandler {
153
- private storage: {
154
- getData(key: string): Promise<string | null>;
155
- setData(key: string, value: string): Promise<void>;
156
- deleteData(key: string): Promise<void>;
157
- } | null = null;
158
-
159
- setDataProvider(storage: typeof this.storage) {
160
- this.storage = storage;
161
- }
162
-
163
- async someMethod() {
164
- const v = await this.storage?.getData("key");
165
- await this.storage?.setData("key", "value");
166
- await this.storage?.deleteData("key");
167
- }
168
- }
169
- ```
112
+ | `requiresResponse` | boolean? | 覆盖通道级 defaultRequiresResponse |
170
113
 
171
114
  ---
172
115
 
@@ -176,10 +119,10 @@ class MyHandler implements BridgeHandler {
176
119
  |------|------|------|
177
120
  | `handler` | 是 | BridgeHandler 实例 |
178
121
  | `mcpServer` | 是 | MCP Server 实例(需注册工具,AILO 才能调用) |
122
+ | `displayName` | 是 | 中文通道显示名(如 "飞书") |
179
123
  | `buildChannelPrompt` | 否 | 通道规则提示词,connect 时注册 |
180
- | `channelName` | 否 | 通道名,不传则从 `AILO_MCP_NAME` 读取 |
181
- | `ailoWsUrl` | 否 | AILO WebSocket 地址,不传则从 `AILO_WS_URL` 读取 |
182
- | `ailoToken` | 否 | 认证 Token,不传则从 `AILO_TOKEN` 读取 |
124
+ | `defaultRequiresResponse` | 否 | 默认 true(主动信号) |
125
+ | `channelName` | 否 | 不传则从 `AILO_MCP_NAME` 读取 |
183
126
 
184
127
  ---
185
128
 
@@ -189,21 +132,15 @@ class MyHandler implements BridgeHandler {
189
132
 
190
133
  | 变量 | 说明 |
191
134
  |------|------|
192
- | `AILO_WS_URL` | AILO WebSocket 网关地址。通道与网关不同网络时,需在 MCP 配置的 env 中显式填写 |
135
+ | `AILO_WS_URL` | AILO WebSocket 网关地址 |
193
136
  | `AILO_TOKEN` | 认证 Token |
194
- | `AILO_MCP_NAME` | MCP 名(即通道名),AILO 注入 |
195
- | `AILO_MCP_WORKDIR` | MCP 专属工作目录(绝对路径)。框架拉起 stdio MCP 时创建并注入,通道可在此目录下规划子目录(如 `blobs`、`cache`) |
137
+ | `AILO_MCP_NAME` | MCP 名(即通道名) |
138
+ | `AILO_MCP_WORKDIR` | MCP 专属工作目录(绝对路径) |
196
139
 
197
140
  **获取工作目录**:`import { getWorkDir } from "@lmcl/ailo-mcp-sdk"`,`getWorkDir()` 返回 `AILO_MCP_WORKDIR` 或 `null`。
198
141
 
199
142
  ---
200
143
 
201
- ## 时空场(ContextTags)
202
-
203
- 每条消息必须带 contextTags,通道自己定义。格式:`{ desc, value, core }[]`。`desc` 给 LLM 看,`value` 为标签值,`core` 表示是否参与时空场键。
204
-
205
- ---
206
-
207
144
  ## 类型与导出
208
145
 
209
146
  ```typescript
@@ -211,19 +148,33 @@ import {
211
148
  runMcp,
212
149
  runMcpChannel,
213
150
  getWorkDir,
151
+ AiloClient,
152
+ tagValue,
214
153
  defaultBuildChannelPrompt,
215
154
  } from "@lmcl/ailo-mcp-sdk";
216
155
  import type {
217
156
  BridgeHandler,
218
157
  BridgeMessage,
158
+ ChannelContext,
159
+ ChannelStorage,
219
160
  Attachment,
220
161
  ContextTag,
221
162
  McpChannelConfig,
163
+ AiloClientConfig,
222
164
  } from "@lmcl/ailo-mcp-sdk";
223
165
  ```
224
166
 
225
167
  ---
226
168
 
169
+ ## AiloClient 特性
170
+
171
+ - 指数退避自动重连(1s → 2s → 4s → ... → 60s max)
172
+ - 请求超时(30s,防止 Promise 泄漏)
173
+ - WebSocket 心跳(ping/pong,30s 间隔,10s 超时检测半死连接)
174
+ - 断线时自动清理所有 pending 请求
175
+
176
+ ---
177
+
227
178
  ## 依赖
228
179
 
229
180
  - Node.js >= 18
@@ -1,35 +1,49 @@
1
- import type { BridgeMessage, ContextTag } from "./types.js";
1
+ import type { BridgeMessage, ChannelStorage, ContextTag } from "./types.js";
2
2
  /** 按 kind 查找第一个匹配标签的 value */
3
3
  export declare function tagValue(tags: ContextTag[], kind: string): string;
4
+ export interface AiloClientConfig {
5
+ url: string;
6
+ token: string;
7
+ channel: string;
8
+ displayName: string;
9
+ defaultRequiresResponse?: boolean;
10
+ channelPrompt?: string;
11
+ }
4
12
  /**
5
- * 反向 WebSocket 信号客户端。
6
- *
7
- * 连接 Ailo 网关,connect 时传入 channel、displayName、defaultRequiresResponse,一步完成注册。
8
- * 负责 channel.accept(入站信号投递)。
13
+ * 反向 WebSocket 客户端。
9
14
  *
10
- * 出站(AI 平台)由 MCP stdio 工具处理,不经过此客户端。
15
+ * 连接 Ailo 网关,自动重连(指数退避)、心跳(ping/pong)、请求超时。
16
+ * 实现 ChannelStorage 接口,可直接作为 ctx.storage 使用。
11
17
  */
12
- export declare class AiloClient {
18
+ export declare class AiloClient implements ChannelStorage {
13
19
  private ws;
14
- private url;
15
- private token;
16
- private channel;
17
- private displayName;
18
- private defaultRequiresResponse;
19
- private channelPrompt;
20
- private reconnectTimer;
20
+ private cfg;
21
21
  private reqId;
22
- constructor(url: string, token: string, channel: string, displayName: string, defaultRequiresResponse?: boolean, channelPrompt?: string);
23
- private request;
22
+ private pending;
23
+ private heartbeatTimer;
24
+ private pongTimer;
25
+ private reconnectTimer;
26
+ private reconnectAttempt;
27
+ private intentionalClose;
28
+ constructor(config: AiloClientConfig);
29
+ /** 建立连接并完成握手。首次调用;后续断线由内部自动重连。 */
24
30
  connect(): Promise<void>;
25
- private scheduleReconnect;
26
- sendMessage(msg: BridgeMessage): Promise<{
27
- text?: string;
28
- }>;
29
- /** 简单 KV,数据存 AILO 本体,自动持久化 */
31
+ private dial;
32
+ private handshake;
33
+ private attachHandlers;
34
+ private request;
30
35
  getData(key: string): Promise<string | null>;
31
36
  setData(key: string, value: string): Promise<void>;
32
37
  deleteData(key: string): Promise<void>;
38
+ sendMessage(msg: BridgeMessage): Promise<void>;
39
+ /** 通过 WS 将日志发给 Ailo 代打,不阻塞,失败静默忽略 */
40
+ sendLog(level: "debug" | "info" | "warn" | "error", message: string, data?: Record<string, unknown>): void;
41
+ private startHeartbeat;
42
+ private stopHeartbeat;
43
+ private onDisconnect;
44
+ private rejectAllPending;
45
+ private scheduleReconnect;
46
+ /** 主动关闭连接(不触发自动重连) */
33
47
  close(): void;
34
48
  }
35
49
  //# sourceMappingURL=ailo-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ailo-client.d.ts","sourceRoot":"","sources":["../src/ailo-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,8BAA8B;AAC9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE;AAcD;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,uBAAuB,CAAU;IACzC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,KAAK,CAAK;gBAGhB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,uBAAuB,UAAO,EAC9B,aAAa,SAAK;IAUpB,OAAO,CAAC,OAAO;IAuBf,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAwDxB,OAAO,CAAC,iBAAiB;IAUzB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAc3D,8BAA8B;IACxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQ5C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C,KAAK,IAAI,IAAI;CAUd"}
1
+ {"version":3,"file":"ailo-client.d.ts","sourceRoot":"","sources":["../src/ailo-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAsB5E,8BAA8B;AAC9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,cAAc;IAC/C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,GAAG,CAAmB;IAC9B,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAAS;gBAErB,MAAM,EAAE,gBAAgB;IAIpC,kCAAkC;IAC5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B,OAAO,CAAC,IAAI;IA6BZ,OAAO,CAAC,SAAS;IA+BjB,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,OAAO;IAmBT,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAK5C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9C,qCAAqC;IACrC,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQ1G,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,iBAAiB;IAczB,sBAAsB;IACtB,KAAK,IAAI,IAAI;CAOd"}
@@ -1,4 +1,9 @@
1
1
  import WebSocket from "ws";
2
+ const REQUEST_TIMEOUT_MS = 30_000;
3
+ const HEARTBEAT_INTERVAL_MS = 30_000;
4
+ const HEARTBEAT_TIMEOUT_MS = 10_000;
5
+ const RECONNECT_BASE_MS = 1_000;
6
+ const RECONNECT_MAX_MS = 60_000;
2
7
  /** 按 kind 查找第一个匹配标签的 value */
3
8
  export function tagValue(tags, kind) {
4
9
  for (const t of tags) {
@@ -8,115 +13,142 @@ export function tagValue(tags, kind) {
8
13
  return "";
9
14
  }
10
15
  /**
11
- * 反向 WebSocket 信号客户端。
16
+ * 反向 WebSocket 客户端。
12
17
  *
13
- * 连接 Ailo 网关,connect 时传入 channel、displayName、defaultRequiresResponse,一步完成注册。
14
- * 负责 channel.accept(入站信号投递)。
15
- *
16
- * 出站(AI → 平台)由 MCP stdio 工具处理,不经过此客户端。
18
+ * 连接 Ailo 网关,自动重连(指数退避)、心跳(ping/pong)、请求超时。
19
+ * 实现 ChannelStorage 接口,可直接作为 ctx.storage 使用。
17
20
  */
18
21
  export class AiloClient {
19
22
  ws = null;
20
- url;
21
- token;
22
- channel;
23
- displayName;
24
- defaultRequiresResponse;
25
- channelPrompt;
26
- reconnectTimer = null;
23
+ cfg;
27
24
  reqId = 0;
28
- constructor(url, token, channel, displayName, defaultRequiresResponse = true, channelPrompt = "") {
29
- this.url = url;
30
- this.token = token;
31
- this.channel = channel;
32
- this.displayName = displayName;
33
- this.defaultRequiresResponse = defaultRequiresResponse;
34
- this.channelPrompt = channelPrompt;
25
+ pending = new Map();
26
+ heartbeatTimer = null;
27
+ pongTimer = null;
28
+ reconnectTimer = null;
29
+ reconnectAttempt = 0;
30
+ intentionalClose = false;
31
+ constructor(config) {
32
+ this.cfg = config;
35
33
  }
36
- request(method, params) {
34
+ /** 建立连接并完成握手。首次调用;后续断线由内部自动重连。 */
35
+ async connect() {
36
+ this.intentionalClose = false;
37
+ await this.dial();
38
+ }
39
+ // ── 连接/握手 ──
40
+ dial() {
37
41
  return new Promise((resolve, reject) => {
38
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
39
- reject(new Error("Ailo WebSocket not connected"));
40
- return;
41
- }
42
- const id = `${method}-${++this.reqId}`;
42
+ const ws = new WebSocket(this.cfg.url);
43
+ let settled = false;
44
+ const settle = (ok, err) => {
45
+ if (settled)
46
+ return;
47
+ settled = true;
48
+ ok ? resolve() : reject(err);
49
+ };
50
+ ws.on("open", async () => {
51
+ try {
52
+ await this.handshake(ws);
53
+ this.ws = ws;
54
+ this.reconnectAttempt = 0;
55
+ this.attachHandlers(ws);
56
+ this.startHeartbeat();
57
+ settle(true);
58
+ }
59
+ catch (err) {
60
+ ws.close();
61
+ settle(false, err);
62
+ }
63
+ });
64
+ ws.on("error", (err) => settle(false, err));
65
+ ws.on("close", () => settle(false, new Error("closed before handshake")));
66
+ });
67
+ }
68
+ handshake(ws) {
69
+ const id = `connect-${++this.reqId}`;
70
+ return new Promise((resolve, reject) => {
71
+ const timer = setTimeout(() => {
72
+ ws.off("message", handler);
73
+ reject(new Error("handshake timeout"));
74
+ }, REQUEST_TIMEOUT_MS);
43
75
  const handler = (raw) => {
44
76
  const frame = JSON.parse(raw.toString());
45
77
  if (frame.type === "res" && frame.id === id) {
46
- this.ws?.off("message", handler);
47
- if (frame.ok) {
48
- resolve(frame.payload ?? {});
49
- }
50
- else {
51
- reject(new Error(frame.error?.message ?? `${method} failed`));
52
- }
78
+ clearTimeout(timer);
79
+ ws.off("message", handler);
80
+ frame.ok ? resolve() : reject(new Error(frame.error?.message ?? "connect rejected"));
53
81
  }
54
82
  };
55
- this.ws.on("message", handler);
56
- this.ws.send(JSON.stringify({ type: "req", id, method, params }));
83
+ ws.on("message", handler);
84
+ ws.send(JSON.stringify({
85
+ type: "req", id, method: "connect",
86
+ params: {
87
+ role: "channel",
88
+ token: this.cfg.token,
89
+ channel: this.cfg.channel,
90
+ displayName: this.cfg.displayName,
91
+ defaultRequiresResponse: this.cfg.defaultRequiresResponse ?? true,
92
+ prompt: this.cfg.channelPrompt ?? "",
93
+ },
94
+ }));
57
95
  });
58
96
  }
59
- connect() {
97
+ attachHandlers(ws) {
98
+ ws.on("message", (raw) => {
99
+ const frame = JSON.parse(raw.toString());
100
+ if (frame.type === "res" && frame.id) {
101
+ const req = this.pending.get(frame.id);
102
+ if (!req)
103
+ return;
104
+ this.pending.delete(frame.id);
105
+ clearTimeout(req.timer);
106
+ frame.ok
107
+ ? req.resolve(frame.payload ?? {})
108
+ : req.reject(new Error(frame.error?.message ?? "request failed"));
109
+ }
110
+ });
111
+ ws.on("close", () => this.onDisconnect());
112
+ ws.on("pong", () => {
113
+ if (this.pongTimer) {
114
+ clearTimeout(this.pongTimer);
115
+ this.pongTimer = null;
116
+ }
117
+ });
118
+ }
119
+ // ── 请求/响应 ──
120
+ request(method, params) {
60
121
  return new Promise((resolve, reject) => {
61
- const ws = new WebSocket(this.url);
62
- this.ws = ws;
63
- ws.on("open", async () => {
64
- try {
65
- const id = `connect-${++this.reqId}`;
66
- ws.send(JSON.stringify({
67
- type: "req",
68
- id,
69
- method: "connect",
70
- params: {
71
- role: "channel",
72
- token: this.token,
73
- channel: this.channel,
74
- displayName: this.displayName,
75
- defaultRequiresResponse: this.defaultRequiresResponse,
76
- prompt: this.channelPrompt,
77
- },
78
- }));
79
- await new Promise((res, rej) => {
80
- const onMsg = (raw) => {
81
- const frame = JSON.parse(raw.toString());
82
- if (frame.type === "res" && frame.id === id) {
83
- ws.off("message", onMsg);
84
- if (frame.ok) {
85
- res();
86
- }
87
- else {
88
- rej(new Error(frame.error?.message ?? "connect failed"));
89
- }
90
- }
91
- };
92
- ws.on("message", onMsg);
93
- });
94
- resolve();
95
- }
96
- catch (err) {
97
- reject(err);
98
- }
99
- });
100
- ws.on("close", () => {
101
- this.ws = null;
102
- this.scheduleReconnect(resolve);
103
- });
104
- ws.on("error", (err) => {
105
- reject(err);
106
- });
122
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
123
+ reject(new Error("not connected"));
124
+ return;
125
+ }
126
+ const id = `${method}-${++this.reqId}`;
127
+ const timer = setTimeout(() => {
128
+ this.pending.delete(id);
129
+ reject(new Error(`${method} timeout (${REQUEST_TIMEOUT_MS}ms)`));
130
+ }, REQUEST_TIMEOUT_MS);
131
+ this.pending.set(id, { resolve: resolve, reject, timer });
132
+ this.ws.send(JSON.stringify({ type: "req", id, method, params }));
107
133
  });
108
134
  }
109
- scheduleReconnect(onReconnect) {
110
- if (this.reconnectTimer)
111
- return;
112
- this.reconnectTimer = setTimeout(() => {
113
- this.reconnectTimer = null;
114
- this.connect()
115
- .then(() => onReconnect?.())
116
- .catch(() => this.scheduleReconnect(onReconnect));
117
- }, 3000);
135
+ // ── ChannelStorage ──
136
+ async getData(key) {
137
+ const res = await this.request("channel.data.get", { key });
138
+ return res.found ? (res.value ?? null) : null;
118
139
  }
140
+ async setData(key, value) {
141
+ await this.request("channel.data.set", { key, value });
142
+ }
143
+ async deleteData(key) {
144
+ await this.request("channel.data.delete", { key });
145
+ }
146
+ // ── 消息投递 ──
147
+ // channel.accept 是单向推送,不需要等待响应。机制上不进入 request/pending,发完即返回。
119
148
  sendMessage(msg) {
149
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
150
+ return Promise.reject(new Error("not connected"));
151
+ }
120
152
  const chatId = tagValue(msg.contextTags, "chat_id") || "main";
121
153
  const params = {
122
154
  chatId,
@@ -127,24 +159,78 @@ export class AiloClient {
127
159
  if (msg.requiresResponse !== undefined) {
128
160
  params.requiresResponse = msg.requiresResponse;
129
161
  }
130
- return this.request("channel.accept", params);
162
+ const id = `channel.accept-${++this.reqId}`;
163
+ this.ws.send(JSON.stringify({ type: "req", id, method: "channel.accept", params }));
164
+ return Promise.resolve();
131
165
  }
132
- /** 简单 KV,数据存 AILO 本体,自动持久化 */
133
- async getData(key) {
134
- const res = await this.request("channel.data.get", { key });
135
- return res.found ? (res.value ?? null) : null;
166
+ /** 通过 WS 将日志发给 Ailo 代打,不阻塞,失败静默忽略 */
167
+ sendLog(level, message, data) {
168
+ const params = { level, message };
169
+ if (data && Object.keys(data).length > 0)
170
+ params.data = data;
171
+ this.request("channel.log", params).catch(() => { });
136
172
  }
137
- async setData(key, value) {
138
- await this.request("channel.data.set", { key, value });
173
+ // ── 心跳 ──
174
+ startHeartbeat() {
175
+ this.stopHeartbeat();
176
+ this.heartbeatTimer = setInterval(() => {
177
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
178
+ return;
179
+ this.ws.ping();
180
+ this.pongTimer = setTimeout(() => {
181
+ console.error("[ailo-client] pong timeout, closing");
182
+ this.ws?.terminate();
183
+ }, HEARTBEAT_TIMEOUT_MS);
184
+ }, HEARTBEAT_INTERVAL_MS);
139
185
  }
140
- async deleteData(key) {
141
- await this.request("channel.data.delete", { key });
186
+ stopHeartbeat() {
187
+ if (this.heartbeatTimer) {
188
+ clearInterval(this.heartbeatTimer);
189
+ this.heartbeatTimer = null;
190
+ }
191
+ if (this.pongTimer) {
192
+ clearTimeout(this.pongTimer);
193
+ this.pongTimer = null;
194
+ }
195
+ }
196
+ // ── 断线/重连 ──
197
+ onDisconnect() {
198
+ this.ws = null;
199
+ this.stopHeartbeat();
200
+ this.rejectAllPending(new Error("disconnected"));
201
+ if (!this.intentionalClose)
202
+ this.scheduleReconnect();
203
+ }
204
+ rejectAllPending(err) {
205
+ for (const [, req] of this.pending) {
206
+ clearTimeout(req.timer);
207
+ req.reject(err);
208
+ }
209
+ this.pending.clear();
210
+ }
211
+ scheduleReconnect() {
212
+ if (this.reconnectTimer)
213
+ return;
214
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
215
+ this.reconnectAttempt++;
216
+ console.error(`[ailo-client] reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
217
+ this.reconnectTimer = setTimeout(() => {
218
+ this.reconnectTimer = null;
219
+ this.dial().catch((err) => {
220
+ console.error("[ailo-client] reconnect failed:", err.message);
221
+ this.scheduleReconnect();
222
+ });
223
+ }, delay);
142
224
  }
225
+ /** 主动关闭连接(不触发自动重连) */
143
226
  close() {
227
+ this.intentionalClose = true;
144
228
  if (this.reconnectTimer) {
145
229
  clearTimeout(this.reconnectTimer);
146
230
  this.reconnectTimer = null;
147
231
  }
232
+ this.stopHeartbeat();
233
+ this.rejectAllPending(new Error("client closed"));
148
234
  if (this.ws) {
149
235
  this.ws.close();
150
236
  this.ws = null;
@@ -19,10 +19,7 @@ export interface McpChannelConfig {
19
19
  ailoWsUrl?: string;
20
20
  /** Ailo 网关认证 Token。不传则从 AILO_TOKEN 读取 */
21
21
  ailoToken?: string;
22
- /**
23
- * 构建通道静态提示词(connect 时注册)。
24
- * 逐步废弃:通道指令应迁移到 MCP 工具定义(tool schema description)中。
25
- */
22
+ /** 构建通道静态提示词(connect 时注册)。逐步废弃:通道指令应迁移到 MCP 工具定义中。 */
26
23
  buildChannelPrompt?: () => string;
27
24
  /** 预配置的 MCP Server 实例(已注册好工具) */
28
25
  mcpServer: McpServer;
@@ -38,12 +35,12 @@ export declare function runMcp(mcpServer: McpServer): void;
38
35
  /**
39
36
  * 启动 MCP 通道
40
37
  *
41
- * 通用流程:
38
+ * 流程:
42
39
  * 1. stdio-guard 已在 index 入口加载,console.log 等自动重定向到 stderr
43
40
  * 2. 启动 MCP stdio server(暴露出站工具)
44
- * 3. 建立反向 WebSocket 连接(connect 时传入 channel + prompt,一步完成注册)
45
- * 4. 接线入站:handler.setOnMessage 组装 contextTags → channel.accept
46
- * 5. 启动平台 Handler
41
+ * 3. 组装 ChannelContext(accept + storage)
42
+ * 4. 先做 WS 回连(connect Ailo),确认成功后才有出口
43
+ * 5. 确认回连成功后,再 handler.start(ctx) 启动平台连接(如飞书 WebSocket)
47
44
  * 6. 注册 SIGINT / SIGTERM 优雅退出
48
45
  */
49
46
  export declare function runMcpChannel(config: McpChannelConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,MAAM,CAAC;IAClC,iCAAiC;IACjC,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CASjD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAsF5D"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,YAAY,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,MAAM,MAAM,CAAC;IAClC,iCAAiC;IACjC,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAQjD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAwE5D"}
package/dist/bootstrap.js CHANGED
@@ -10,7 +10,6 @@ export function defaultBuildChannelPrompt() {
10
10
  * stdout 被 MCP stdio 占用,日志自动重定向到 stderr。
11
11
  */
12
12
  export function runMcp(mcpServer) {
13
- // stdio-guard 需在入口 import,runMcp 使用者应 import 自 @lmcl/ailo-mcp-sdk
14
13
  const transport = new StdioServerTransport();
15
14
  mcpServer.connect(transport).then(() => {
16
15
  console.log("[mcp] MCP stdio server started");
@@ -22,12 +21,12 @@ export function runMcp(mcpServer) {
22
21
  /**
23
22
  * 启动 MCP 通道
24
23
  *
25
- * 通用流程:
24
+ * 流程:
26
25
  * 1. stdio-guard 已在 index 入口加载,console.log 等自动重定向到 stderr
27
26
  * 2. 启动 MCP stdio server(暴露出站工具)
28
- * 3. 建立反向 WebSocket 连接(connect 时传入 channel + prompt,一步完成注册)
29
- * 4. 接线入站:handler.setOnMessage 组装 contextTags → channel.accept
30
- * 5. 启动平台 Handler
27
+ * 3. 组装 ChannelContext(accept + storage)
28
+ * 4. 先做 WS 回连(connect Ailo),确认成功后才有出口
29
+ * 5. 确认回连成功后,再 handler.start(ctx) 启动平台连接(如飞书 WebSocket)
31
30
  * 6. 注册 SIGINT / SIGTERM 优雅退出
32
31
  */
33
32
  export function runMcpChannel(config) {
@@ -39,69 +38,62 @@ export function runMcpChannel(config) {
39
38
  console.error("Missing AILO_WS_URL, AILO_TOKEN or AILO_MCP_NAME. Channel must be started by Ailo MCP.");
40
39
  process.exit(1);
41
40
  }
42
- // stdio-guard 已在 index 入口加载,console.log 等已重定向到 stderr
43
41
  const tag = `[${channelName}]`;
44
- const channelPrompt = config.buildChannelPrompt
45
- ? config.buildChannelPrompt()
46
- : defaultBuildChannelPrompt();
47
- const displayName = config.displayName;
42
+ const channelPrompt = config.buildChannelPrompt?.() ?? defaultBuildChannelPrompt();
48
43
  const defaultRequiresResponse = config.defaultRequiresResponse ?? true;
49
- const client = new AiloClient(ailoWsUrl, ailoToken, channelName, displayName, defaultRequiresResponse, channelPrompt);
50
- // 入站:平台 → Ailo(channel.accept),msg 必须自带 contextTags 或内容
51
- // 注意:被动感知信号(requiresResponse=false)允许无 text/attachments,只需有 contextTags
52
- handler.setOnMessage(async (msg) => {
53
- const hasContent = (msg.text?.trim() ?? "") !== "" || (msg.attachments?.length ?? 0) > 0 || msg.contextTags.length > 0;
54
- if (!hasContent) {
55
- console.log(`${tag} skipped (no text, attachments, or contextTags)`);
56
- return;
57
- }
58
- console.log(`${tag} ${(msg.text ?? "").slice(0, 80)}`);
59
- try {
60
- await client.sendMessage(msg);
61
- }
62
- catch (err) {
63
- console.error(`${tag} send to Ailo failed:`, err);
64
- }
44
+ const client = new AiloClient({
45
+ url: ailoWsUrl,
46
+ token: ailoToken,
47
+ channel: channelName,
48
+ displayName: config.displayName,
49
+ defaultRequiresResponse,
50
+ channelPrompt,
65
51
  });
66
- // 优雅退出
67
52
  const shutdown = () => {
68
53
  console.log(`${tag} shutting down...`);
69
- handler.stop?.();
54
+ Promise.resolve(handler.stop()).catch(() => { });
70
55
  client.close();
71
56
  process.exit(0);
72
57
  };
73
58
  process.on("SIGINT", shutdown);
74
59
  process.on("SIGTERM", shutdown);
75
60
  (async () => {
76
- // 1. 启动 MCP stdio server
61
+ // 1. MCP stdio
77
62
  const transport = new StdioServerTransport();
78
63
  await mcpServer.connect(transport);
79
64
  console.log(`${tag} MCP stdio server started`);
80
- // 2. 建立反向 WebSocket 连接
65
+ // 2. ChannelContext: accept + storage + log,一次性就绪
66
+ const ctx = {
67
+ accept: async (msg) => {
68
+ const hasContent = (msg.text?.trim() ?? "") !== ""
69
+ || (msg.attachments?.length ?? 0) > 0
70
+ || msg.contextTags.length > 0;
71
+ if (!hasContent) {
72
+ return;
73
+ }
74
+ await client.sendMessage(msg);
75
+ },
76
+ storage: client,
77
+ log: (level, message, data) => client.sendLog(level, message, data),
78
+ };
79
+ // 3. 先做 WS 回连(连接 Ailo),必须确认成功后才有出口
81
80
  try {
82
81
  await client.connect();
83
- console.log(`${tag} reverse WebSocket connected`);
82
+ console.log(`${tag} Ailo WebSocket connected`);
84
83
  }
85
84
  catch (err) {
86
- console.error(`${tag} reverse WebSocket connect failed:`, err);
85
+ console.error(`${tag} Ailo WebSocket connect failed:`, err);
87
86
  process.exit(1);
88
87
  }
89
- // 3. 注入持久化存储
90
- if (handler.setDataProvider) {
91
- handler.setDataProvider(client);
88
+ // 4. 确认回连成功后,再启动平台连接(如飞书 WebSocket)
89
+ console.log(`${tag} starting handler (platform connection)...`);
90
+ try {
91
+ await handler.start(ctx);
92
+ console.log(`${tag} handler started`);
92
93
  }
93
- // 4. 启动平台 Handler
94
- console.log(`${tag} starting handler...`);
95
- const startResult = handler.start();
96
- if (startResult && typeof startResult.then === "function") {
97
- try {
98
- await startResult;
99
- console.log(`${tag} handler started successfully`);
100
- }
101
- catch (err) {
102
- console.error(`${tag} handler start failed:`, err);
103
- process.exit(1);
104
- }
94
+ catch (err) {
95
+ console.error(`${tag} handler start failed:`, err);
96
+ process.exit(1);
105
97
  }
106
98
  })();
107
99
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import "./stdio-guard.js";
2
- export { AiloClient } from "./ailo-client.js";
2
+ export { AiloClient, tagValue } from "./ailo-client.js";
3
+ export type { AiloClientConfig } from "./ailo-client.js";
3
4
  export { getWorkDir } from "./workdir.js";
4
5
  export { runMcp, runMcpChannel, defaultBuildChannelPrompt } from "./bootstrap.js";
5
6
  export type { McpChannelConfig } from "./bootstrap.js";
6
- export type { Attachment, BridgeHandler, BridgeMessage, ContextTag, } from "./types.js";
7
+ export type { Attachment, BridgeHandler, BridgeMessage, ChannelContext, ChannelStorage, ContextTag, } from "./types.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,EACd,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import "./stdio-guard.js"; // 必须最先:stdout 被 MCP stdio 占用,框架层统一拦截非 JSON-RPC 输出
2
- export { AiloClient } from "./ailo-client.js";
2
+ export { AiloClient, tagValue } from "./ailo-client.js";
3
3
  export { getWorkDir } from "./workdir.js";
4
4
  export { runMcp, runMcpChannel, defaultBuildChannelPrompt } from "./bootstrap.js";
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * 附件类型(图片/音频/视频/文件等)
2
+ * 附件(入站/出站通用)
3
+ *
3
4
  * 入站图片:path/url/base64 三选一,直接使用 LLM 多模态。其他类型:path/url/ref+channel/base64。
4
5
  * 出站:file_path 或 base64 或 url。
5
6
  */
@@ -28,7 +29,7 @@ export type ContextTag = {
28
29
  routing?: boolean;
29
30
  };
30
31
  /**
31
- * 桥接器入站消息(平台 → Ailo)
32
+ * 入站消息(平台 → Ailo)
32
33
  *
33
34
  * 时空场模型:通道自己定义,全在 contextTags 里。
34
35
  */
@@ -39,21 +40,42 @@ export type BridgeMessage = {
39
40
  /** 本条消息是否需要 LLM 响应(覆盖通道级 defaultRequiresResponse) */
40
41
  requiresResponse?: boolean;
41
42
  };
42
- /** setDataProvider 接收的对象,SDK 注入,直接用 get/set/delete 即可 */
43
- type ChannelStorage = {
43
+ /**
44
+ * 通道级 KV 存储(数据持久化在 Ailo 侧)
45
+ */
46
+ export interface ChannelStorage {
44
47
  getData(key: string): Promise<string | null>;
45
48
  setData(key: string, value: string): Promise<void>;
46
49
  deleteData(key: string): Promise<void>;
47
- };
50
+ }
48
51
  /**
49
- * 通道 Handler 统一接口
52
+ * 通道运行时上下文
53
+ *
54
+ * SDK 在调用 handler.start() 时注入,handler 通过 ctx 与 Ailo 交互。
55
+ * 替代旧版 setOnMessage + setDataProvider 的 setter 注入模式——
56
+ * 所有依赖在 start() 一次性就绪,无时序歧义。
57
+ */
58
+ export interface ChannelContext {
59
+ /** 投递入站消息到 Ailo(平台 → Ailo)。SDK 内部做空消息过滤。 */
60
+ accept(msg: BridgeMessage): Promise<void>;
61
+ /** 通道级 KV 存储 */
62
+ storage: ChannelStorage;
63
+ /**
64
+ * 通过 WS 将日志发给 Ailo 代打(MCP 子进程 stdout 被 stdio 占用时使用)。
65
+ * level: "debug" | "info" | "warn" | "error"
66
+ * 不阻塞,失败静默忽略。
67
+ */
68
+ log(level: "debug" | "info" | "warn" | "error", message: string, data?: Record<string, unknown>): void;
69
+ }
70
+ /**
71
+ * 通道 Handler 接口
72
+ *
73
+ * 通道需实现此接口。SDK 保证 start(ctx) 调用时 ctx 已就绪。
50
74
  */
51
75
  export interface BridgeHandler {
52
- setOnMessage(handler: (msg: BridgeMessage) => void | Promise<void>): void;
53
- start(): void | Promise<void>;
54
- stop?(): void;
55
- /** 可选,SDK 连接后注入带 get/set/delete 的对象,直接用 */
56
- setDataProvider?(storage: ChannelStorage): void;
76
+ /** 启动 Handler。ctx 包含与 Ailo 交互所需的一切。 */
77
+ start(ctx: ChannelContext): void | Promise<void>;
78
+ /** 优雅停止。SDK 在 SIGINT/SIGTERM 时调用。 */
79
+ stop(): void | Promise<void>;
57
80
  }
58
- export {};
59
81
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,yDAAyD;AACzD,KAAK,cAAc,GAAG;IACpB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1E,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,eAAe,CAAC,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CACjD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,gBAAgB;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxG;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,qCAAqC;IACrC,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmcl/ailo-mcp-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "description": "Ailo MCP SDK - 通道通过 MCP 注册发送消息能力,用于开发感知通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",