@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.
- package/dist/ailo-client.d.ts +35 -19
- package/dist/ailo-client.d.ts.map +1 -1
- package/dist/ailo-client.js +179 -97
- package/dist/bootstrap.d.ts +8 -7
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +40 -44
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +39 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ailo-client.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
13
|
-
private token;
|
|
14
|
-
private channel;
|
|
15
|
-
private channelPrompt;
|
|
16
|
-
private reconnectTimer;
|
|
20
|
+
private cfg;
|
|
17
21
|
private reqId;
|
|
18
|
-
|
|
19
|
-
private
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
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"}
|
package/dist/ailo-client.js
CHANGED
|
@@ -1,126 +1,138 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
|
|
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.
|
|
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
|
|
13
|
-
*
|
|
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
|
-
|
|
20
|
-
token;
|
|
21
|
-
channel;
|
|
22
|
-
channelPrompt;
|
|
23
|
-
reconnectTimer = null;
|
|
23
|
+
cfg;
|
|
24
24
|
reqId = 0;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
+
// ── 连接/握手 ──
|
|
40
|
+
dial() {
|
|
55
41
|
return new Promise((resolve, reject) => {
|
|
56
|
-
const ws = new WebSocket(this.url);
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
ws
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
60
|
+
ws.close();
|
|
61
|
+
settle(false, err);
|
|
93
62
|
}
|
|
94
63
|
});
|
|
95
|
-
ws.on("
|
|
96
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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;
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -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.
|
|
42
|
-
* 5.
|
|
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;
|
package/dist/bootstrap.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
30
|
-
* 5.
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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.
|
|
65
|
+
// 2. Ailo WebSocket
|
|
78
66
|
try {
|
|
79
67
|
await client.connect();
|
|
80
|
-
console.log(`${tag}
|
|
68
|
+
console.log(`${tag} Ailo WebSocket connected`);
|
|
81
69
|
}
|
|
82
70
|
catch (err) {
|
|
83
|
-
console.error(`${tag}
|
|
71
|
+
console.error(`${tag} Ailo WebSocket connect failed:`, err);
|
|
84
72
|
process.exit(1);
|
|
85
73
|
}
|
|
86
|
-
// 3.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
*
|
|
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
|
-
|
|
26
|
+
kind: string;
|
|
22
27
|
value: string;
|
|
23
|
-
|
|
28
|
+
streamKey: boolean;
|
|
29
|
+
routing?: boolean;
|
|
24
30
|
};
|
|
25
31
|
/**
|
|
26
|
-
*
|
|
32
|
+
* 入站消息(平台 → Ailo)
|
|
27
33
|
*
|
|
28
34
|
* 时空场模型:通道自己定义,全在 contextTags 里。
|
|
29
35
|
*/
|
|
30
36
|
export type BridgeMessage = {
|
|
31
|
-
text
|
|
37
|
+
text?: string;
|
|
32
38
|
contextTags: ContextTag[];
|
|
33
39
|
attachments?: Attachment[];
|
|
40
|
+
/** 本条消息是否需要 LLM 响应(覆盖通道级 defaultRequiresResponse) */
|
|
41
|
+
requiresResponse?: boolean;
|
|
34
42
|
};
|
|
35
|
-
/**
|
|
36
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
46
|
-
start(): void | Promise<void>;
|
|
47
|
-
|
|
48
|
-
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|