@next-open-ai/openclawx 0.8.32 → 0.8.36
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 +7 -7
- package/dist/core/agent/agent-manager.js +4 -1
- package/dist/core/mcp/client.d.ts +4 -0
- package/dist/core/mcp/client.js +2 -0
- package/dist/core/mcp/index.d.ts +3 -1
- package/dist/core/mcp/index.js +4 -1
- package/dist/core/mcp/operator.d.ts +17 -2
- package/dist/core/mcp/operator.js +97 -30
- package/dist/core/mcp/transport/index.d.ts +4 -0
- package/dist/core/mcp/transport/index.js +6 -1
- package/dist/core/mcp/transport/stdio.d.ts +6 -0
- package/dist/core/mcp/transport/stdio.js +22 -1
- package/dist/core/session-outlet/index.d.ts +19 -0
- package/dist/core/session-outlet/index.js +33 -0
- package/dist/core/session-outlet/outlet.d.ts +15 -0
- package/dist/core/session-outlet/outlet.js +49 -0
- package/dist/core/session-outlet/types.d.ts +35 -0
- package/dist/core/session-outlet/types.js +5 -0
- package/dist/gateway/channel/channel-core.d.ts +1 -0
- package/dist/gateway/channel/channel-core.js +91 -70
- package/dist/gateway/methods/agent-cancel.js +3 -0
- package/dist/gateway/methods/agent-chat.d.ts +4 -0
- package/dist/gateway/methods/agent-chat.js +292 -240
- package/dist/gateway/server.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| **使用指南** | [CLI 使用](docs/zh/guides/cli-usage.md) | 命令行对话、登录、模型与技能、开机自启 |
|
|
22
22
|
| | [桌面端使用](docs/zh/guides/desktop-usage.md) | Desktop 安装与启动、智能体/会话/技能/设置;对话内 `//` 指令查询与切换智能体 |
|
|
23
23
|
| | [Web 与 Gateway](docs/zh/guides/gateway-web.md) | 启动网关、端口与路径、Web 端连接 |
|
|
24
|
-
| | [使用场景](docs/zh/guides/usage-scenarios.md) |
|
|
24
|
+
| | [使用场景](docs/zh/guides/usage-scenarios.md) | 整理下载目录、创建/切换智能体、B站下载助手、安装技能、MCP、定时任务等 |
|
|
25
25
|
| **配置** | [配置概览](docs/zh/configuration/config-overview.md) | 配置目录、config.json 与 agents.json |
|
|
26
26
|
| | [智能体配置](docs/zh/configuration/agents.md) | 本机/Coze/OpenClawX 执行方式与模型 |
|
|
27
27
|
| | [通道配置](docs/zh/configuration/channels.md) | 飞书、钉钉、Telegram 启用与配置项 |
|
|
@@ -75,10 +75,10 @@ docs/
|
|
|
75
75
|
| **长期记忆** | 向量存储(Vectra)+ 本地嵌入,支持经验总结与会话压缩(compaction) |
|
|
76
76
|
| **多端接入** | CLI、WebSocket 网关、Electron 桌面端,同一套 Agent 核心;各端技术栈见下方「各端技术栈」 |
|
|
77
77
|
| **多通道接入** | 飞书、钉钉、Telegram 等 IM 通道,Gateway 根据配置注册;入站经统一格式进 Agent,回复经通道回传 |
|
|
78
|
-
| **代理模式** | 智能体执行方式可选 **本机** / **Coze** / **OpenClawX** / **OpenCode**;本机使用当前模型与 Skills
|
|
79
|
-
| **Coze 接入** | 支持 Coze 国内站(api.coze.cn)与国际站(api.coze.com);按站点分别配置 Bot ID 与 Access Token(PAT/OAuth/JWT),桌面端与通道均可选用 Coze
|
|
80
|
-
| **OpenClawX 多节点协作** | 可将智能体代理到另一台 OpenClawX 实例(baseUrl + 可选 API Key
|
|
81
|
-
| **OpenCode 代理** | 可将智能体代理至 [OpenCode](https://opencode.ai/) 官方 Server(本地 `opencode serve` 或远程);支持流式回复、斜杠指令 `/init`、`/undo`、`/redo`、`/share`、`/help`,与 TUI
|
|
78
|
+
| **代理模式** | 智能体执行方式可选 **本机** / **Coze** / **OpenClawX** / **OpenCode**;本机使用当前模型与 Skills,**代理模式下本机 0 Token 消耗**,推理与消息处理在对方平台完成 |
|
|
79
|
+
| **Coze 接入** | 支持 Coze 国内站(api.coze.cn)与国际站(api.coze.com);按站点分别配置 Bot ID 与 Access Token(PAT/OAuth/JWT),桌面端与通道均可选用 Coze 智能体;**0 Token 消耗**,适合 Coze 侧大量消息与长对话场景 |
|
|
80
|
+
| **OpenClawX 多节点协作** | 可将智能体代理到另一台 OpenClawX 实例(baseUrl + 可选 API Key),实现多节点分工、负载与协作;本机 0 Token 消耗 |
|
|
81
|
+
| **OpenCode 代理** | 可将智能体代理至 [OpenCode](https://opencode.ai/) 官方 Server(本地 `opencode serve` 或远程);支持流式回复、斜杠指令 `/init`、`/undo`、`/redo`、`/share`、`/help`,与 TUI 使用方式一致;**0 Token 消耗**,适合 OpenCode 侧大量代码与长上下文能力 |
|
|
82
82
|
| **MCP** | 已支持 [MCP](https://modelcontextprotocol.io/)(Model Context Protocol):智能体可配置 stdio/SSE 两种连接方式,按智能体绑定 MCP 服务器,会话内自动加载对应工具,降低 Token 消耗与大模型幻觉 |
|
|
83
83
|
| **RPA(影刀)** | 通过 MCP 可接入影刀 RPA:在智能体 MCP 配置中添加 [yingdao-mcp-server](https://www.npmjs.com/package/yingdao-mcp-server)(命令 `npx -y yingdao-mcp-server`,可选 env 如 `RPA_MODEL`、`SHADOWBOT_PATH`、`USER_FOLDER`),即可在对话中调用影刀自动化能力 |
|
|
84
84
|
|
|
@@ -217,7 +217,7 @@ docker compose up -d
|
|
|
217
217
|
docker compose -f deploy/docker-compose.yaml up -d
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
默认使用镜像 `ccr.ccs.tencentyun.com/windwithlife/openclawx:latest`。若需指定版本,可修改 `deploy/docker-compose.yaml` 中 `image` 的 tag(如 `0.8.28` 或 CI 生成的 `build-ci-openbot-<BUILD_NUMBER>`)。
|
|
220
|
+
默认使用镜像 `ccr.ccs.tencentyun.com/windwithlife/openclawx:latest`。若需指定版本,可修改 `deploy/docker-compose.yaml` 中 `image` 的 tag(如 `0.8.32`、`0.8.28`、`0.8.26` 或 CI 生成的 `build-ci-openbot-<BUILD_NUMBER>`)。
|
|
221
221
|
|
|
222
222
|
### 方式二:本地构建并运行(开发/无 CI 时)
|
|
223
223
|
|
|
@@ -396,7 +396,7 @@ openclawx gateway --port 38080
|
|
|
396
396
|
|
|
397
397
|
### 2.4.1 代理模式与多节点协作
|
|
398
398
|
|
|
399
|
-
智能体除在本机运行(**local**)外,可配置为**代理模式**,将对话转发至 Coze 或另一台 OpenClawX
|
|
399
|
+
智能体除在本机运行(**local**)外,可配置为**代理模式**,将对话转发至 Coze、OpenCode 或另一台 OpenClawX,实现生态接入与多节点协作。**代理智能体为本机 0 Token 消耗模式**:推理与消息处理均在对方平台完成,本机仅做转发与展示,不占用本机模型 API 的 Token。特别适合 **Coze**、**OpenCode** 等具备大量消息、长上下文或代码协作能力的平台,在桌面端与通道中直接使用其能力而无需消耗本机配额。
|
|
400
400
|
|
|
401
401
|
| 模式 | 说明 | 配置要点 |
|
|
402
402
|
|------|------|----------|
|
|
@@ -259,7 +259,10 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
259
259
|
ls: createLsTool(sessionWorkspaceDir),
|
|
260
260
|
};
|
|
261
261
|
const useLongMemory = options.useLongMemory !== false;
|
|
262
|
-
const mcpTools = await createMcpToolsForSession({
|
|
262
|
+
const mcpTools = await createMcpToolsForSession({
|
|
263
|
+
mcpServers: options.mcpServers,
|
|
264
|
+
sessionId,
|
|
265
|
+
});
|
|
263
266
|
const customTools = [
|
|
264
267
|
createBrowserTool(sessionWorkspaceDir),
|
|
265
268
|
createSaveExperienceTool(sessionId),
|
|
@@ -7,6 +7,10 @@ import type { McpServerConfig } from "./types.js";
|
|
|
7
7
|
export interface McpClientOptions {
|
|
8
8
|
initTimeoutMs?: number;
|
|
9
9
|
requestTimeoutMs?: number;
|
|
10
|
+
/** stdio:初始化失败时的重试次数(默认 1) */
|
|
11
|
+
initRetries?: number;
|
|
12
|
+
/** stdio:初始化重试间隔毫秒(默认 3000) */
|
|
13
|
+
initRetryDelayMs?: number;
|
|
10
14
|
}
|
|
11
15
|
export declare class McpClient {
|
|
12
16
|
private transport;
|
package/dist/core/mcp/client.js
CHANGED
|
@@ -16,6 +16,8 @@ export class McpClient {
|
|
|
16
16
|
this.transport = createTransport(configOrTransport, {
|
|
17
17
|
initTimeoutMs: options.initTimeoutMs,
|
|
18
18
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
19
|
+
initRetries: options.initRetries,
|
|
20
|
+
initRetryDelayMs: options.initRetryDelayMs,
|
|
19
21
|
});
|
|
20
22
|
}
|
|
21
23
|
}
|
package/dist/core/mcp/index.d.ts
CHANGED
|
@@ -7,12 +7,14 @@ import type { McpServerConfig, McpServersStandardFormat } from "./types.js";
|
|
|
7
7
|
export type { McpServerConfig, McpServerConfigStdio, McpServerConfigSse, McpServerConfigStandardEntry, McpServersStandardFormat, McpTool, } from "./types.js";
|
|
8
8
|
export { resolveMcpServersForSession, standardFormatToArray, arrayToStandardFormat, stdioConfigKey, sseConfigKey, mcpConfigKey, } from "./config.js";
|
|
9
9
|
export { McpClient } from "./client.js";
|
|
10
|
-
export { getMcpToolDefinitions, shutdownMcpClients } from "./operator.js";
|
|
10
|
+
export { getMcpToolDefinitions, shutdownMcpClients, type GetMcpToolDefinitionsOptions } from "./operator.js";
|
|
11
11
|
export { mcpToolToToolDefinition, mcpToolsToToolDefinitions } from "./adapter.js";
|
|
12
12
|
/**
|
|
13
13
|
* 根据会话选项中的 mcpServers 配置,返回该会话可用的 MCP 工具(ToolDefinition 数组)。
|
|
14
14
|
* 支持数组或标准 JSON 对象格式;在 AgentManager.getOrCreateSession 中调用,并入 customTools。
|
|
15
|
+
* 若提供 sessionId,MCP 连接/重试进度将经全局 sendSessionMessage 以系统消息推送。
|
|
15
16
|
*/
|
|
16
17
|
export declare function createMcpToolsForSession(options: {
|
|
17
18
|
mcpServers?: McpServerConfig[] | McpServersStandardFormat;
|
|
19
|
+
sessionId?: string;
|
|
18
20
|
}): Promise<ToolDefinition[]>;
|
package/dist/core/mcp/index.js
CHANGED
|
@@ -11,10 +11,13 @@ export { mcpToolToToolDefinition, mcpToolsToToolDefinitions } from "./adapter.js
|
|
|
11
11
|
/**
|
|
12
12
|
* 根据会话选项中的 mcpServers 配置,返回该会话可用的 MCP 工具(ToolDefinition 数组)。
|
|
13
13
|
* 支持数组或标准 JSON 对象格式;在 AgentManager.getOrCreateSession 中调用,并入 customTools。
|
|
14
|
+
* 若提供 sessionId,MCP 连接/重试进度将经全局 sendSessionMessage 以系统消息推送。
|
|
14
15
|
*/
|
|
15
16
|
export async function createMcpToolsForSession(options) {
|
|
16
17
|
const configs = resolveMcpServersForSession(options.mcpServers);
|
|
17
18
|
if (configs.length === 0)
|
|
18
19
|
return [];
|
|
19
|
-
return getMcpToolDefinitions(configs
|
|
20
|
+
return getMcpToolDefinitions(configs, {
|
|
21
|
+
sessionId: options.sessionId,
|
|
22
|
+
});
|
|
20
23
|
}
|
|
@@ -4,11 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { McpServerConfig } from "./types.js";
|
|
6
6
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
/** getMcpToolDefinitions 的可选参数:超时、重试与 sessionId(用于经 sendSessionMessage 推送进度) */
|
|
8
|
+
export interface GetMcpToolDefinitionsOptions {
|
|
9
|
+
/** 连接或 list_tools 失败时的重试次数(不含首次,默认 1) */
|
|
10
|
+
connectRetries?: number;
|
|
11
|
+
/** 重试间隔(毫秒,默认 3000) */
|
|
12
|
+
retryDelayMs?: number;
|
|
13
|
+
/** stdio 初始化超时(毫秒) */
|
|
14
|
+
initTimeoutMs?: number;
|
|
15
|
+
/** stdio 初始化重试次数(默认 1) */
|
|
16
|
+
initRetries?: number;
|
|
17
|
+
/** stdio 初始化重试间隔(毫秒,默认 3000) */
|
|
18
|
+
initRetryDelayMs?: number;
|
|
19
|
+
/** 会话 ID,用于经全局 sendSessionMessage 推送 MCP 进度系统消息 */
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
}
|
|
7
22
|
/**
|
|
8
23
|
* 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
|
|
9
|
-
* 连接失败或 list_tools 失败的 server
|
|
24
|
+
* 连接失败或 list_tools 失败的 server 会按 options 重试,仍失败则跳过并打日志,不阻塞整体。
|
|
10
25
|
*/
|
|
11
|
-
export declare function getMcpToolDefinitions(serverConfigs: McpServerConfig[]): Promise<ToolDefinition[]>;
|
|
26
|
+
export declare function getMcpToolDefinitions(serverConfigs: McpServerConfig[], options?: GetMcpToolDefinitionsOptions): Promise<ToolDefinition[]>;
|
|
12
27
|
/**
|
|
13
28
|
* 关闭并移除所有缓存的 MCP 客户端(进程退出或显式清理时调用)。
|
|
14
29
|
*/
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { mcpConfigKey } from "./config.js";
|
|
6
6
|
import { McpClient } from "./client.js";
|
|
7
7
|
import { mcpToolsToToolDefinitions } from "./adapter.js";
|
|
8
|
+
import { sendSessionMessage } from "../session-outlet/index.js";
|
|
8
9
|
/** 按配置键缓存的客户端 */
|
|
9
10
|
const clientCache = new Map();
|
|
10
11
|
function configLabel(config) {
|
|
@@ -12,53 +13,119 @@ function configLabel(config) {
|
|
|
12
13
|
return config.command;
|
|
13
14
|
return config.url;
|
|
14
15
|
}
|
|
16
|
+
/** 用于系统消息展示的 MCP 名称:stdio 优先用首参(如 akshare-tools),否则 command;sse 用 URL 主机或路径末段 */
|
|
17
|
+
function mcpDisplayName(config) {
|
|
18
|
+
if (config.transport === "stdio") {
|
|
19
|
+
const args = config.args;
|
|
20
|
+
const first = args?.[0];
|
|
21
|
+
if (typeof first === "string" && first.trim() && !first.includes("/") && !first.includes("\\")) {
|
|
22
|
+
return first.trim();
|
|
23
|
+
}
|
|
24
|
+
return config.command;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(config.url);
|
|
28
|
+
const host = u.hostname || u.pathname?.replace(/\/$/, "").split("/").pop() || "MCP";
|
|
29
|
+
return host;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return config.url;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function sleep(ms) {
|
|
36
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
37
|
+
}
|
|
15
38
|
/**
|
|
16
39
|
* 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
|
|
17
|
-
* 连接失败或 list_tools 失败的 server
|
|
40
|
+
* 连接失败或 list_tools 失败的 server 会按 options 重试,仍失败则跳过并打日志,不阻塞整体。
|
|
18
41
|
*/
|
|
19
|
-
export async function getMcpToolDefinitions(serverConfigs) {
|
|
42
|
+
export async function getMcpToolDefinitions(serverConfigs, options = {}) {
|
|
43
|
+
const connectRetries = options.connectRetries ?? 1;
|
|
44
|
+
const retryDelayMs = options.retryDelayMs ?? 3_000;
|
|
45
|
+
const sessionId = options.sessionId;
|
|
46
|
+
const clientOptions = {
|
|
47
|
+
initTimeoutMs: options.initTimeoutMs,
|
|
48
|
+
initRetries: options.initRetries,
|
|
49
|
+
initRetryDelayMs: options.initRetryDelayMs,
|
|
50
|
+
};
|
|
51
|
+
const emitProgress = (displayMessage, phase, detail) => {
|
|
52
|
+
if (sessionId) {
|
|
53
|
+
sendSessionMessage(sessionId, {
|
|
54
|
+
type: "system",
|
|
55
|
+
code: "mcp.progress",
|
|
56
|
+
payload: { phase, message: displayMessage, serverLabel: detail },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
20
60
|
const allTools = [];
|
|
21
61
|
for (let i = 0; i < serverConfigs.length; i++) {
|
|
22
62
|
const config = serverConfigs[i];
|
|
23
63
|
const key = mcpConfigKey(config);
|
|
64
|
+
const label = configLabel(config);
|
|
65
|
+
const displayName = mcpDisplayName(config);
|
|
24
66
|
let client = clientCache.get(key);
|
|
25
|
-
if (!client) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
if (!client || !client.isConnected) {
|
|
68
|
+
if (client) {
|
|
69
|
+
clientCache.delete(key);
|
|
70
|
+
try {
|
|
71
|
+
await client.close();
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
30
74
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
emitProgress(`${displayName} MCP connecting`, "connecting");
|
|
76
|
+
let connected = false;
|
|
77
|
+
for (let attempt = 0; attempt <= connectRetries; attempt++) {
|
|
78
|
+
if (attempt > 0) {
|
|
79
|
+
emitProgress(`${displayName} MCP retrying (${attempt + 1}/${connectRetries + 1})`, "retrying", "connect");
|
|
80
|
+
await sleep(retryDelayMs);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
client = new McpClient(config, clientOptions);
|
|
84
|
+
await client.connect();
|
|
85
|
+
clientCache.set(key, client);
|
|
86
|
+
connected = true;
|
|
87
|
+
emitProgress(`${displayName} MCP ready`, "ready");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (client)
|
|
92
|
+
try {
|
|
93
|
+
await client.close();
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
if (attempt === connectRetries) {
|
|
97
|
+
console.warn(`[mcp] 连接失败 (${label}):`, err instanceof Error ? err.message : err);
|
|
98
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
emitProgress(`${displayName} MCP failed: ${errMsg}`, "skipped", errMsg);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
34
102
|
}
|
|
103
|
+
if (!connected)
|
|
104
|
+
continue;
|
|
35
105
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
106
|
+
client = clientCache.get(key);
|
|
107
|
+
let toolsListed = false;
|
|
108
|
+
for (let attempt = 0; attempt <= connectRetries; attempt++) {
|
|
109
|
+
if (attempt > 0) {
|
|
110
|
+
emitProgress(`${displayName} MCP retrying list tools (${attempt + 1}/${connectRetries + 1})`, "retrying", "list_tools");
|
|
111
|
+
await sleep(retryDelayMs);
|
|
40
112
|
}
|
|
41
|
-
catch { }
|
|
42
|
-
const newClient = new McpClient(config);
|
|
43
113
|
try {
|
|
44
|
-
await
|
|
45
|
-
|
|
46
|
-
|
|
114
|
+
const tools = await client.listTools();
|
|
115
|
+
const serverId = `mcp${i}`;
|
|
116
|
+
const definitions = mcpToolsToToolDefinitions(tools, client, serverId);
|
|
117
|
+
allTools.push(...definitions);
|
|
118
|
+
toolsListed = true;
|
|
119
|
+
break;
|
|
47
120
|
}
|
|
48
121
|
catch (err) {
|
|
49
|
-
|
|
50
|
-
|
|
122
|
+
if (attempt === connectRetries) {
|
|
123
|
+
console.warn(`[mcp] list_tools 失败 (${label}):`, err instanceof Error ? err.message : err);
|
|
124
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
emitProgress(`${displayName} MCP list tools failed: ${errMsg}`, "skipped", errMsg);
|
|
126
|
+
}
|
|
51
127
|
}
|
|
52
128
|
}
|
|
53
|
-
try {
|
|
54
|
-
const tools = await client.listTools();
|
|
55
|
-
const serverId = `mcp${i}`;
|
|
56
|
-
const definitions = mcpToolsToToolDefinitions(tools, client, serverId);
|
|
57
|
-
allTools.push(...definitions);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
console.warn(`[mcp] list_tools 失败 (${configLabel(config)}):`, err instanceof Error ? err.message : err);
|
|
61
|
-
}
|
|
62
129
|
}
|
|
63
130
|
return allTools;
|
|
64
131
|
}
|
|
@@ -2,6 +2,10 @@ import type { McpServerConfig, IMcpTransport } from "../types.js";
|
|
|
2
2
|
export interface TransportOptions {
|
|
3
3
|
initTimeoutMs?: number;
|
|
4
4
|
requestTimeoutMs?: number;
|
|
5
|
+
/** stdio:初始化失败时的重试次数(默认 1) */
|
|
6
|
+
initRetries?: number;
|
|
7
|
+
/** stdio:初始化重试间隔毫秒(默认 3000) */
|
|
8
|
+
initRetryDelayMs?: number;
|
|
5
9
|
}
|
|
6
10
|
/**
|
|
7
11
|
* 根据配置创建对应的传输层实例。
|
|
@@ -5,7 +5,12 @@ import { SseTransport } from "./sse.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export function createTransport(config, options) {
|
|
7
7
|
if (config.transport === "stdio") {
|
|
8
|
-
return new StdioTransport(config,
|
|
8
|
+
return new StdioTransport(config, {
|
|
9
|
+
initTimeoutMs: options?.initTimeoutMs,
|
|
10
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
11
|
+
initRetries: options?.initRetries,
|
|
12
|
+
initRetryDelayMs: options?.initRetryDelayMs,
|
|
13
|
+
});
|
|
9
14
|
}
|
|
10
15
|
if (config.transport === "sse") {
|
|
11
16
|
return new SseTransport(config, options);
|
|
@@ -8,12 +8,18 @@ export interface StdioTransportOptions {
|
|
|
8
8
|
initTimeoutMs?: number;
|
|
9
9
|
/** 单次请求超时(毫秒) */
|
|
10
10
|
requestTimeoutMs?: number;
|
|
11
|
+
/** 初始化失败时的重试次数(不含首次,默认 1,即最多 2 次尝试) */
|
|
12
|
+
initRetries?: number;
|
|
13
|
+
/** 初始化重试间隔(毫秒,默认 3000) */
|
|
14
|
+
initRetryDelayMs?: number;
|
|
11
15
|
}
|
|
12
16
|
export declare class StdioTransport {
|
|
13
17
|
private process;
|
|
14
18
|
private config;
|
|
15
19
|
private initTimeoutMs;
|
|
16
20
|
private requestTimeoutMs;
|
|
21
|
+
private initRetries;
|
|
22
|
+
private initRetryDelayMs;
|
|
17
23
|
private nextId;
|
|
18
24
|
private pending;
|
|
19
25
|
private buffer;
|
|
@@ -8,6 +8,8 @@ export class StdioTransport {
|
|
|
8
8
|
config;
|
|
9
9
|
initTimeoutMs;
|
|
10
10
|
requestTimeoutMs;
|
|
11
|
+
initRetries;
|
|
12
|
+
initRetryDelayMs;
|
|
11
13
|
nextId = 1;
|
|
12
14
|
pending = new Map();
|
|
13
15
|
buffer = "";
|
|
@@ -15,6 +17,8 @@ export class StdioTransport {
|
|
|
15
17
|
this.config = config;
|
|
16
18
|
this.initTimeoutMs = options.initTimeoutMs ?? 10_000;
|
|
17
19
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
20
|
+
this.initRetries = options.initRetries ?? 1;
|
|
21
|
+
this.initRetryDelayMs = options.initRetryDelayMs ?? 3_000;
|
|
18
22
|
}
|
|
19
23
|
/** 启动子进程并完成 MCP initialize 握手 */
|
|
20
24
|
async start() {
|
|
@@ -43,7 +47,24 @@ export class StdioTransport {
|
|
|
43
47
|
this.rejectAll(new Error(`MCP process exited: code=${code} signal=${signal}`));
|
|
44
48
|
this.process = null;
|
|
45
49
|
});
|
|
46
|
-
|
|
50
|
+
const maxAttempts = 1 + this.initRetries;
|
|
51
|
+
let lastErr = null;
|
|
52
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
await this.initialize();
|
|
55
|
+
lastErr = null;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
lastErr = e instanceof Error ? e : new Error(String(e));
|
|
60
|
+
const isTransient = lastErr.message.includes("timeout") || lastErr.message.includes("MCP process exited");
|
|
61
|
+
if (!isTransient || attempt >= maxAttempts) {
|
|
62
|
+
throw lastErr;
|
|
63
|
+
}
|
|
64
|
+
console.warn(`[mcp stdio] initialize 超时或失败,${this.initRetryDelayMs}ms 后重试 (${attempt}/${maxAttempts}):`, lastErr.message);
|
|
65
|
+
await new Promise((r) => setTimeout(r, this.initRetryDelayMs));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
47
68
|
}
|
|
48
69
|
flushLines() {
|
|
49
70
|
const lines = this.buffer.split("\n");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息统一出口:各模块仅需 sessionId + 消息即可通过统一出口准确送达当前会话对应的 Web/Desktop 或通道。
|
|
3
|
+
*/
|
|
4
|
+
import type { SessionMessageInput, ISessionOutlet } from "./types.js";
|
|
5
|
+
export type { SessionMessage, SessionMessageInput, SessionMessageConsumer, ISessionOutlet } from "./types.js";
|
|
6
|
+
export { SessionOutlet } from "./outlet.js";
|
|
7
|
+
/**
|
|
8
|
+
* 获取当前使用的出口实例(Gateway 启动时通过 setSessionOutlet 注入)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function getSessionOutlet(): ISessionOutlet | null;
|
|
11
|
+
/**
|
|
12
|
+
* 设置全局出口实例;传 null 可清空(测试用)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function setSessionOutlet(outlet: ISessionOutlet | null): void;
|
|
15
|
+
/**
|
|
16
|
+
* 解耦 API:任意模块携带 sessionId 即可发送会话消息,由统一出口路由到正确端。
|
|
17
|
+
* 若未设置出口或 sessionId 为空,则静默忽略。
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendSessionMessage(sessionId: string, message: Omit<SessionMessageInput, "sessionId">): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息统一出口:各模块仅需 sessionId + 消息即可通过统一出口准确送达当前会话对应的 Web/Desktop 或通道。
|
|
3
|
+
*/
|
|
4
|
+
export { SessionOutlet } from "./outlet.js";
|
|
5
|
+
let defaultOutlet = null;
|
|
6
|
+
/**
|
|
7
|
+
* 获取当前使用的出口实例(Gateway 启动时通过 setSessionOutlet 注入)。
|
|
8
|
+
*/
|
|
9
|
+
export function getSessionOutlet() {
|
|
10
|
+
return defaultOutlet;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 设置全局出口实例;传 null 可清空(测试用)。
|
|
14
|
+
*/
|
|
15
|
+
export function setSessionOutlet(outlet) {
|
|
16
|
+
defaultOutlet = outlet;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 解耦 API:任意模块携带 sessionId 即可发送会话消息,由统一出口路由到正确端。
|
|
20
|
+
* 若未设置出口或 sessionId 为空,则静默忽略。
|
|
21
|
+
*/
|
|
22
|
+
export function sendSessionMessage(sessionId, message) {
|
|
23
|
+
if (!sessionId || !message)
|
|
24
|
+
return;
|
|
25
|
+
const outlet = getSessionOutlet();
|
|
26
|
+
if (!outlet)
|
|
27
|
+
return;
|
|
28
|
+
outlet.emit(sessionId, {
|
|
29
|
+
type: message.type,
|
|
30
|
+
code: message.code,
|
|
31
|
+
payload: message.payload ?? {},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息出口实现:按 sessionId 维护消费者集合,emit 时扇出到该会话所有消费者。
|
|
3
|
+
*/
|
|
4
|
+
import type { SessionMessage, SessionMessageConsumer } from "./types.js";
|
|
5
|
+
export declare class SessionOutlet {
|
|
6
|
+
private readonly consumersBySession;
|
|
7
|
+
/**
|
|
8
|
+
* 向指定会话注册消费者;返回取消注册函数。
|
|
9
|
+
*/
|
|
10
|
+
registerConsumer(sessionId: string, consumer: SessionMessageConsumer): () => void;
|
|
11
|
+
/**
|
|
12
|
+
* 向指定会话发送消息;将 sessionId、timestamp 注入后扇出到该会话所有消费者。
|
|
13
|
+
*/
|
|
14
|
+
emit(sessionId: string, message: Omit<SessionMessage, "sessionId" | "timestamp">): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息出口实现:按 sessionId 维护消费者集合,emit 时扇出到该会话所有消费者。
|
|
3
|
+
*/
|
|
4
|
+
export class SessionOutlet {
|
|
5
|
+
consumersBySession = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* 向指定会话注册消费者;返回取消注册函数。
|
|
8
|
+
*/
|
|
9
|
+
registerConsumer(sessionId, consumer) {
|
|
10
|
+
let set = this.consumersBySession.get(sessionId);
|
|
11
|
+
if (!set) {
|
|
12
|
+
set = new Set();
|
|
13
|
+
this.consumersBySession.set(sessionId, set);
|
|
14
|
+
}
|
|
15
|
+
set.add(consumer);
|
|
16
|
+
return () => {
|
|
17
|
+
const s = this.consumersBySession.get(sessionId);
|
|
18
|
+
if (s) {
|
|
19
|
+
s.delete(consumer);
|
|
20
|
+
if (s.size === 0)
|
|
21
|
+
this.consumersBySession.delete(sessionId);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 向指定会话发送消息;将 sessionId、timestamp 注入后扇出到该会话所有消费者。
|
|
27
|
+
*/
|
|
28
|
+
emit(sessionId, message) {
|
|
29
|
+
const full = {
|
|
30
|
+
...message,
|
|
31
|
+
sessionId,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
const set = this.consumersBySession.get(sessionId);
|
|
35
|
+
if (!set || set.size === 0)
|
|
36
|
+
return;
|
|
37
|
+
for (const consumer of set) {
|
|
38
|
+
try {
|
|
39
|
+
const result = consumer.send(full);
|
|
40
|
+
if (result && typeof result.catch === "function") {
|
|
41
|
+
result.catch((err) => console.warn("[SessionOutlet] consumer.send rejected:", err));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.warn("[SessionOutlet] consumer.send threw:", err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息出口:统一消息类型与消费者接口。
|
|
3
|
+
* 所有发给各端(Web/Desktop、通道)的会话消息经此抽象,由出口按 sessionId 路由到已注册的消费者。
|
|
4
|
+
*/
|
|
5
|
+
/** 消息类型:对话内容 vs 系统消息(MCP 进度、// 命令结果等) */
|
|
6
|
+
export type SessionMessageType = "chat" | "system";
|
|
7
|
+
/** 系统消息子类型,便于各端区分展示 */
|
|
8
|
+
export type SessionMessageCode = "agent.chunk" | "agent.tool" | "turn_end" | "message_complete" | "agent_end" | "conversation_end" | "mcp.progress" | "command.result";
|
|
9
|
+
/**
|
|
10
|
+
* 统一会话消息:出口只认 sessionId + 本结构,不关心具体传输。
|
|
11
|
+
*/
|
|
12
|
+
export interface SessionMessage {
|
|
13
|
+
type: SessionMessageType;
|
|
14
|
+
code?: SessionMessageCode;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
timestamp?: number;
|
|
18
|
+
}
|
|
19
|
+
/** 发送时调用方可不带 sessionId,由出口注入 */
|
|
20
|
+
export type SessionMessageInput = Omit<SessionMessage, "sessionId"> & {
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 消费者接口:各端(Web、通道)注册后,出口将消息推送给 send(message)。
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionMessageConsumer {
|
|
27
|
+
send(message: SessionMessage): void | Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 出口接口:支持注册消费者与发送消息,便于注入与测试。
|
|
31
|
+
*/
|
|
32
|
+
export interface ISessionOutlet {
|
|
33
|
+
emit(sessionId: string, message: Omit<SessionMessage, "sessionId" | "timestamp">): void;
|
|
34
|
+
registerConsumer(sessionId: string, consumer: SessionMessageConsumer): () => void;
|
|
35
|
+
}
|