@lmcl/ailo-mcp-sdk 0.0.3 → 0.1.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.
@@ -1,31 +1,47 @@
1
- import type { BridgeMessage } from "./types.js";
1
+ import type { BridgeMessage, ChannelStorage, ContextTag } from "./types.js";
2
+ /** 按 kind 查找第一个匹配标签的 value */
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
+ }
2
12
  /**
3
- * 反向 WebSocket 信号客户端。
4
- *
5
- * 连接 Ailo 网关,connect 时一并传入 channel 与 prompt,一步完成注册。
6
- * 负责 channel.accept(入站信号投递)。
13
+ * 反向 WebSocket 客户端。
7
14
  *
8
- * 出站(AI 平台)由 MCP stdio 工具处理,不经过此客户端。
15
+ * 连接 Ailo 网关,自动重连(指数退避)、心跳(ping/pong)、请求超时。
16
+ * 实现 ChannelStorage 接口,可直接作为 ctx.storage 使用。
9
17
  */
10
- export declare class AiloClient {
18
+ export declare class AiloClient implements ChannelStorage {
11
19
  private ws;
12
- private url;
13
- private token;
14
- private channel;
15
- private channelPrompt;
16
- private reconnectTimer;
20
+ private cfg;
17
21
  private reqId;
18
- constructor(url: string, token: string, channel: string, channelPrompt?: string);
19
- 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
+ /** 建立连接并完成握手。首次调用;后续断线由内部自动重连。 */
20
30
  connect(): Promise<void>;
21
- private scheduleReconnect;
22
- sendMessage(msg: BridgeMessage): Promise<{
23
- text?: string;
24
- }>;
25
- /** 简单 KV,数据存 AILO 本体,自动持久化 */
31
+ private dial;
32
+ private handshake;
33
+ private attachHandlers;
34
+ private request;
26
35
  getData(key: string): Promise<string | null>;
27
36
  setData(key: string, value: string): Promise<void>;
28
37
  deleteData(key: string): Promise<void>;
38
+ sendMessage(msg: BridgeMessage): Promise<void>;
39
+ private startHeartbeat;
40
+ private stopHeartbeat;
41
+ private onDisconnect;
42
+ private rejectAllPending;
43
+ private scheduleReconnect;
44
+ /** 主动关闭连接(不触发自动重连) */
29
45
  close(): void;
30
46
  }
31
47
  //# 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,EAAc,MAAM,YAAY,CAAC;AAqB5D;;;;;;;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,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,KAAK,CAAK;gBAEN,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,SAAK;IAO3E,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;IAU3D,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;IAM5C,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9C,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,126 +1,138 @@
1
1
  import WebSocket from "ws";
2
- function tagValue(tags, desc) {
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;
7
+ /** 按 kind 查找第一个匹配标签的 value */
8
+ export function tagValue(tags, kind) {
3
9
  for (const t of tags) {
4
- if (t.desc === desc)
10
+ if (t.kind === kind)
5
11
  return t.value;
6
12
  }
7
13
  return "";
8
14
  }
9
15
  /**
10
- * 反向 WebSocket 信号客户端。
16
+ * 反向 WebSocket 客户端。
11
17
  *
12
- * 连接 Ailo 网关,connect 时一并传入 channel 与 prompt,一步完成注册。
13
- * 负责 channel.accept(入站信号投递)。
14
- *
15
- * 出站(AI → 平台)由 MCP stdio 工具处理,不经过此客户端。
18
+ * 连接 Ailo 网关,自动重连(指数退避)、心跳(ping/pong)、请求超时。
19
+ * 实现 ChannelStorage 接口,可直接作为 ctx.storage 使用。
16
20
  */
17
21
  export class AiloClient {
18
22
  ws = null;
19
- url;
20
- token;
21
- channel;
22
- channelPrompt;
23
- reconnectTimer = null;
23
+ cfg;
24
24
  reqId = 0;
25
- constructor(url, token, channel, channelPrompt = "") {
26
- this.url = url;
27
- this.token = token;
28
- this.channel = channel;
29
- 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;
30
33
  }
31
- request(method, params) {
32
- return new Promise((resolve, reject) => {
33
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
34
- reject(new Error("Ailo WebSocket not connected"));
35
- return;
36
- }
37
- const id = `${method}-${++this.reqId}`;
38
- const handler = (raw) => {
39
- const frame = JSON.parse(raw.toString());
40
- if (frame.type === "res" && frame.id === id) {
41
- this.ws?.off("message", handler);
42
- if (frame.ok) {
43
- resolve(frame.payload ?? {});
44
- }
45
- else {
46
- reject(new Error(frame.error?.message ?? `${method} failed`));
47
- }
48
- }
49
- };
50
- this.ws.on("message", handler);
51
- this.ws.send(JSON.stringify({ type: "req", id, method, params }));
52
- });
34
+ /** 建立连接并完成握手。首次调用;后续断线由内部自动重连。 */
35
+ async connect() {
36
+ this.intentionalClose = false;
37
+ await this.dial();
53
38
  }
54
- connect() {
39
+ // ── 连接/握手 ──
40
+ dial() {
55
41
  return new Promise((resolve, reject) => {
56
- const ws = new WebSocket(this.url);
57
- this.ws = ws;
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
+ };
58
50
  ws.on("open", async () => {
59
51
  try {
60
- const id = `connect-${++this.reqId}`;
61
- ws.send(JSON.stringify({
62
- type: "req",
63
- id,
64
- method: "connect",
65
- params: {
66
- role: "channel",
67
- token: this.token,
68
- channel: this.channel,
69
- prompt: this.channelPrompt,
70
- capabilities: ["text", "media"],
71
- direction: "bidirectional",
72
- },
73
- }));
74
- await new Promise((res, rej) => {
75
- const onMsg = (raw) => {
76
- const frame = JSON.parse(raw.toString());
77
- if (frame.type === "res" && frame.id === id) {
78
- ws.off("message", onMsg);
79
- if (frame.ok) {
80
- res();
81
- }
82
- else {
83
- rej(new Error(frame.error?.message ?? "connect failed"));
84
- }
85
- }
86
- };
87
- ws.on("message", onMsg);
88
- });
89
- resolve();
52
+ await this.handshake(ws);
53
+ this.ws = ws;
54
+ this.reconnectAttempt = 0;
55
+ this.attachHandlers(ws);
56
+ this.startHeartbeat();
57
+ settle(true);
90
58
  }
91
59
  catch (err) {
92
- reject(err);
60
+ ws.close();
61
+ settle(false, err);
93
62
  }
94
63
  });
95
- ws.on("close", () => {
96
- this.ws = null;
97
- this.scheduleReconnect(resolve);
98
- });
99
- ws.on("error", (err) => {
100
- reject(err);
101
- });
64
+ ws.on("error", (err) => settle(false, err));
65
+ ws.on("close", () => settle(false, new Error("closed before handshake")));
102
66
  });
103
67
  }
104
- scheduleReconnect(onReconnect) {
105
- if (this.reconnectTimer)
106
- return;
107
- this.reconnectTimer = setTimeout(() => {
108
- this.reconnectTimer = null;
109
- this.connect()
110
- .then(() => onReconnect?.())
111
- .catch(() => this.scheduleReconnect(onReconnect));
112
- }, 3000);
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);
75
+ const handler = (raw) => {
76
+ const frame = JSON.parse(raw.toString());
77
+ if (frame.type === "res" && frame.id === id) {
78
+ clearTimeout(timer);
79
+ ws.off("message", handler);
80
+ frame.ok ? resolve() : reject(new Error(frame.error?.message ?? "connect rejected"));
81
+ }
82
+ };
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
+ }));
95
+ });
113
96
  }
114
- sendMessage(msg) {
115
- const chatId = tagValue(msg.contextTags, "会话") || "main";
116
- return this.request("channel.accept", {
117
- chatId,
118
- text: msg.text,
119
- contextTags: msg.contextTags,
120
- attachments: msg.attachments ?? [],
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
+ }
121
117
  });
122
118
  }
123
- /** 简单 KV,数据存 AILO 本体,自动持久化 */
119
+ // ── 请求/响应 ──
120
+ request(method, params) {
121
+ return new Promise((resolve, reject) => {
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 }));
133
+ });
134
+ }
135
+ // ── ChannelStorage ──
124
136
  async getData(key) {
125
137
  const res = await this.request("channel.data.get", { key });
126
138
  return res.found ? (res.value ?? null) : null;
@@ -131,11 +143,81 @@ export class AiloClient {
131
143
  async deleteData(key) {
132
144
  await this.request("channel.data.delete", { key });
133
145
  }
146
+ // ── 消息投递 ──
147
+ sendMessage(msg) {
148
+ const chatId = tagValue(msg.contextTags, "chat_id") || "main";
149
+ const params = {
150
+ chatId,
151
+ text: msg.text ?? "",
152
+ contextTags: msg.contextTags,
153
+ attachments: msg.attachments ?? [],
154
+ };
155
+ if (msg.requiresResponse !== undefined) {
156
+ params.requiresResponse = msg.requiresResponse;
157
+ }
158
+ return this.request("channel.accept", params).then(() => { });
159
+ }
160
+ // ── 心跳 ──
161
+ startHeartbeat() {
162
+ this.stopHeartbeat();
163
+ this.heartbeatTimer = setInterval(() => {
164
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
165
+ return;
166
+ this.ws.ping();
167
+ this.pongTimer = setTimeout(() => {
168
+ console.error("[ailo-client] pong timeout, closing");
169
+ this.ws?.terminate();
170
+ }, HEARTBEAT_TIMEOUT_MS);
171
+ }, HEARTBEAT_INTERVAL_MS);
172
+ }
173
+ stopHeartbeat() {
174
+ if (this.heartbeatTimer) {
175
+ clearInterval(this.heartbeatTimer);
176
+ this.heartbeatTimer = null;
177
+ }
178
+ if (this.pongTimer) {
179
+ clearTimeout(this.pongTimer);
180
+ this.pongTimer = null;
181
+ }
182
+ }
183
+ // ── 断线/重连 ──
184
+ onDisconnect() {
185
+ this.ws = null;
186
+ this.stopHeartbeat();
187
+ this.rejectAllPending(new Error("disconnected"));
188
+ if (!this.intentionalClose)
189
+ this.scheduleReconnect();
190
+ }
191
+ rejectAllPending(err) {
192
+ for (const [, req] of this.pending) {
193
+ clearTimeout(req.timer);
194
+ req.reject(err);
195
+ }
196
+ this.pending.clear();
197
+ }
198
+ scheduleReconnect() {
199
+ if (this.reconnectTimer)
200
+ return;
201
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
202
+ this.reconnectAttempt++;
203
+ console.error(`[ailo-client] reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
204
+ this.reconnectTimer = setTimeout(() => {
205
+ this.reconnectTimer = null;
206
+ this.dial().catch((err) => {
207
+ console.error("[ailo-client] reconnect failed:", err.message);
208
+ this.scheduleReconnect();
209
+ });
210
+ }, delay);
211
+ }
212
+ /** 主动关闭连接(不触发自动重连) */
134
213
  close() {
214
+ this.intentionalClose = true;
135
215
  if (this.reconnectTimer) {
136
216
  clearTimeout(this.reconnectTimer);
137
217
  this.reconnectTimer = null;
138
218
  }
219
+ this.stopHeartbeat();
220
+ this.rejectAllPending(new Error("client closed"));
139
221
  if (this.ws) {
140
222
  this.ws.close();
141
223
  this.ws = null;
@@ -9,16 +9,17 @@ import type { BridgeHandler } from "./types.js";
9
9
  export interface McpChannelConfig {
10
10
  /** MCP 名称(如 channel:feishu),connect 时与 token 一起发送供服务端校验。不传则从 AILO_MCP_NAME 读取 */
11
11
  channelName?: string;
12
+ /** 中文通道显示名(如 "飞书"),握手时锁定,框架自动注入 TagChannel */
13
+ displayName: string;
14
+ /** 通道默认行为:true=主动信号(触发 LLM 处理),false=被动感知(仅记录)。默认 true */
15
+ defaultRequiresResponse?: boolean;
12
16
  /** 平台 Handler 实例(需实现 BridgeHandler 接口) */
13
17
  handler: BridgeHandler;
14
18
  /** Ailo WebSocket 网关地址。不传则从 AILO_WS_URL 读取 */
15
19
  ailoWsUrl?: string;
16
20
  /** Ailo 网关认证 Token。不传则从 AILO_TOKEN 读取 */
17
21
  ailoToken?: string;
18
- /**
19
- * 构建通道静态提示词(connect 时注册)。
20
- * 连接时调用一次,注册该通道的特殊规则。
21
- */
22
+ /** 构建通道静态提示词(connect 时注册)。逐步废弃:通道指令应迁移到 MCP 工具定义中。 */
22
23
  buildChannelPrompt?: () => string;
23
24
  /** 预配置的 MCP Server 实例(已注册好工具) */
24
25
  mcpServer: McpServer;
@@ -34,12 +35,12 @@ export declare function runMcp(mcpServer: McpServer): void;
34
35
  /**
35
36
  * 启动 MCP 通道
36
37
  *
37
- * 通用流程:
38
+ * 流程:
38
39
  * 1. stdio-guard 已在 index 入口加载,console.log 等自动重定向到 stderr
39
40
  * 2. 启动 MCP stdio server(暴露出站工具)
40
41
  * 3. 建立反向 WebSocket 连接(connect 时传入 channel + prompt,一步完成注册)
41
- * 4. 接线入站:handler.setOnMessage → 组装 contextTags channel.accept
42
- * 5. 启动平台 Handler
42
+ * 4. 组装 ChannelContext(accept + storage)
43
+ * 5. handler.start(ctx) —— handler 拿到一切就绪的上下文
43
44
  * 6. 注册 SIGINT / SIGTERM 优雅退出
44
45
  */
45
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,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,CAkF5D"}
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,CA0E5D"}
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
27
  * 3. 建立反向 WebSocket 连接(connect 时传入 channel + prompt,一步完成注册)
29
- * 4. 接线入站:handler.setOnMessage → 组装 contextTags channel.accept
30
- * 5. 启动平台 Handler
28
+ * 4. 组装 ChannelContext(accept + storage)
29
+ * 5. handler.start(ctx) —— handler 拿到一切就绪的上下文
31
30
  * 6. 注册 SIGINT / SIGTERM 优雅退出
32
31
  */
33
32
  export function runMcpChannel(config) {
@@ -39,66 +38,63 @@ 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 client = new AiloClient(ailoWsUrl, ailoToken, channelName, channelPrompt);
48
- // 入站:平台 → Ailo(channel.accept),msg 必须自带 contextTags
49
- handler.setOnMessage(async (msg) => {
50
- const hasContent = msg.text.trim() !== "" || (msg.attachments?.length ?? 0) > 0;
51
- if (!hasContent) {
52
- console.log(`${tag} skipped (no text or attachments)`);
53
- return;
54
- }
55
- console.log(`${tag} ${msg.text.slice(0, 80)}`);
56
- try {
57
- await client.sendMessage(msg);
58
- }
59
- catch (err) {
60
- console.error(`${tag} send to Ailo failed:`, err);
61
- }
42
+ const channelPrompt = config.buildChannelPrompt?.() ?? defaultBuildChannelPrompt();
43
+ const defaultRequiresResponse = config.defaultRequiresResponse ?? true;
44
+ const client = new AiloClient({
45
+ url: ailoWsUrl,
46
+ token: ailoToken,
47
+ channel: channelName,
48
+ displayName: config.displayName,
49
+ defaultRequiresResponse,
50
+ channelPrompt,
62
51
  });
63
- // 优雅退出
64
52
  const shutdown = () => {
65
53
  console.log(`${tag} shutting down...`);
66
- handler.stop?.();
54
+ Promise.resolve(handler.stop()).catch(() => { });
67
55
  client.close();
68
56
  process.exit(0);
69
57
  };
70
58
  process.on("SIGINT", shutdown);
71
59
  process.on("SIGTERM", shutdown);
72
60
  (async () => {
73
- // 1. 启动 MCP stdio server
61
+ // 1. MCP stdio
74
62
  const transport = new StdioServerTransport();
75
63
  await mcpServer.connect(transport);
76
64
  console.log(`${tag} MCP stdio server started`);
77
- // 2. 建立反向 WebSocket 连接
65
+ // 2. Ailo WebSocket
78
66
  try {
79
67
  await client.connect();
80
- console.log(`${tag} reverse WebSocket connected`);
68
+ console.log(`${tag} Ailo WebSocket connected`);
81
69
  }
82
70
  catch (err) {
83
- console.error(`${tag} reverse WebSocket connect failed:`, err);
71
+ console.error(`${tag} Ailo WebSocket connect failed:`, err);
84
72
  process.exit(1);
85
73
  }
86
- // 3. 注入持久化存储
87
- if (handler.setDataProvider) {
88
- handler.setDataProvider(client);
89
- }
90
- // 4. 启动平台 Handler
74
+ // 3. ChannelContext: accept + storage,一次性就绪
75
+ const ctx = {
76
+ accept: async (msg) => {
77
+ const hasContent = (msg.text?.trim() ?? "") !== ""
78
+ || (msg.attachments?.length ?? 0) > 0
79
+ || msg.contextTags.length > 0;
80
+ if (!hasContent) {
81
+ console.log(`${tag} skipped (empty message)`);
82
+ return;
83
+ }
84
+ console.log(`${tag} ${(msg.text ?? "").slice(0, 80)}`);
85
+ await client.sendMessage(msg);
86
+ },
87
+ storage: client,
88
+ };
89
+ // 4. Start handler
91
90
  console.log(`${tag} starting handler...`);
92
- const startResult = handler.start();
93
- if (startResult && typeof startResult.then === "function") {
94
- try {
95
- await startResult;
96
- console.log(`${tag} handler started successfully`);
97
- }
98
- catch (err) {
99
- console.error(`${tag} handler start failed:`, err);
100
- process.exit(1);
101
- }
91
+ try {
92
+ await handler.start(ctx);
93
+ console.log(`${tag} handler started`);
94
+ }
95
+ catch (err) {
96
+ console.error(`${tag} handler start failed:`, err);
97
+ process.exit(1);
102
98
  }
103
99
  })();
104
100
  }
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
  */
@@ -15,38 +16,60 @@ export type Attachment = {
15
16
  file_path?: string;
16
17
  };
17
18
  /**
18
- * 时空场标签:desc+value+core,全中文。
19
+ * 时空场标签。
20
+ *
21
+ * kind: 受控词汇(channel/participant/group/conv_type/location/device/modality/chat_id/sender_id)
22
+ * streamKey: 参与 stream_key 推导(标识事件流归属)
23
+ * routing: 仅路由用途——不嵌入向量,不展示在历史邮戳
19
24
  */
20
25
  export type ContextTag = {
21
- desc: string;
26
+ kind: string;
22
27
  value: string;
23
- core: boolean;
28
+ streamKey: boolean;
29
+ routing?: boolean;
24
30
  };
25
31
  /**
26
- * 桥接器入站消息(平台 → Ailo)
32
+ * 入站消息(平台 → Ailo)
27
33
  *
28
34
  * 时空场模型:通道自己定义,全在 contextTags 里。
29
35
  */
30
36
  export type BridgeMessage = {
31
- text: string;
37
+ text?: string;
32
38
  contextTags: ContextTag[];
33
39
  attachments?: Attachment[];
40
+ /** 本条消息是否需要 LLM 响应(覆盖通道级 defaultRequiresResponse) */
41
+ requiresResponse?: boolean;
34
42
  };
35
- /** setDataProvider 接收的对象,SDK 注入,直接用 get/set/delete 即可 */
36
- type ChannelStorage = {
43
+ /**
44
+ * 通道级 KV 存储(数据持久化在 Ailo 侧)
45
+ */
46
+ export interface ChannelStorage {
37
47
  getData(key: string): Promise<string | null>;
38
48
  setData(key: string, value: string): Promise<void>;
39
49
  deleteData(key: string): Promise<void>;
40
- };
50
+ }
41
51
  /**
42
- * 通道 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
+ /**
65
+ * 通道 Handler 接口
66
+ *
67
+ * 通道需实现此接口。SDK 保证 start(ctx) 调用时 ctx 已就绪。
43
68
  */
44
69
  export interface BridgeHandler {
45
- setOnMessage(handler: (msg: BridgeMessage) => void | Promise<void>): void;
46
- start(): void | Promise<void>;
47
- stop?(): void;
48
- /** 可选,SDK 连接后注入带 get/set/delete 的对象,直接用 */
49
- setDataProvider?(storage: ChannelStorage): void;
70
+ /** 启动 Handler。ctx 包含与 Ailo 交互所需的一切。 */
71
+ start(ctx: ChannelContext): void | Promise<void>;
72
+ /** 优雅停止。SDK 在 SIGINT/SIGTERM 时调用。 */
73
+ stop(): void | Promise<void>;
50
74
  }
51
- export {};
52
75
  //# 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;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,UAAU,EAAE,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;CACzB;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.3",
3
+ "version": "0.1.0",
4
4
  "description": "Ailo MCP SDK - 通道通过 MCP 注册发送消息能力,用于开发感知通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",