@next-open-ai/openclawx 0.8.32 → 0.8.40
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 +55 -47
- package/apps/desktop/renderer/dist/assets/index-BSfTiTKo.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-DgLpQsA-.js +89 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/cli/cli.js +29 -0
- package/dist/cli/extension-cmd.d.ts +15 -0
- package/dist/cli/extension-cmd.js +107 -0
- package/dist/core/agent/agent-dir.d.ts +6 -0
- package/dist/core/agent/agent-dir.js +8 -0
- package/dist/core/agent/agent-manager.d.ts +13 -0
- package/dist/core/agent/agent-manager.js +72 -6
- package/dist/core/agent/proxy/adapters/claude-code-adapter.d.ts +2 -0
- package/dist/core/agent/proxy/adapters/claude-code-adapter.js +186 -0
- package/dist/core/agent/proxy/adapters/local-adapter.js +2 -0
- package/dist/core/agent/proxy/adapters/opencode-adapter.js +65 -29
- package/dist/core/agent/proxy/adapters/opencode-local-runner.js +9 -0
- package/dist/core/agent/proxy/index.js +2 -0
- package/dist/core/agent/token-usage-log-extension.d.ts +14 -0
- package/dist/core/agent/token-usage-log-extension.js +61 -0
- package/dist/core/config/desktop-config.d.ts +22 -2
- package/dist/core/config/desktop-config.js +57 -1
- package/dist/core/extensions/index.d.ts +1 -0
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/load.d.ts +11 -0
- package/dist/core/extensions/load.js +101 -0
- package/dist/core/mcp/adapter.d.ts +4 -2
- package/dist/core/mcp/adapter.js +10 -4
- package/dist/core/mcp/client.d.ts +4 -0
- package/dist/core/mcp/client.js +2 -0
- package/dist/core/mcp/index.d.ts +5 -1
- package/dist/core/mcp/index.js +5 -1
- package/dist/core/mcp/operator.d.ts +19 -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/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/truncate-result.d.ts +14 -0
- package/dist/core/tools/truncate-result.js +27 -0
- package/dist/core/tools/web-search/create-web-search-tool.d.ts +17 -0
- package/dist/core/tools/web-search/create-web-search-tool.js +87 -0
- package/dist/core/tools/web-search/index.d.ts +4 -0
- package/dist/core/tools/web-search/index.js +2 -0
- package/dist/core/tools/web-search/providers/brave.d.ts +2 -0
- package/dist/core/tools/web-search/providers/brave.js +87 -0
- package/dist/core/tools/web-search/providers/duck-duck-scrape.d.ts +2 -0
- package/dist/core/tools/web-search/providers/duck-duck-scrape.js +47 -0
- package/dist/core/tools/web-search/providers/index.d.ts +5 -0
- package/dist/core/tools/web-search/providers/index.js +13 -0
- package/dist/core/tools/web-search/types.d.ts +35 -0
- package/dist/core/tools/web-search/types.js +4 -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 +294 -240
- package/dist/gateway/methods/run-scheduled-task.js +2 -0
- package/dist/gateway/server.js +2 -0
- package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
- package/dist/server/agent-config/agent-config.service.d.ts +15 -3
- package/dist/server/agent-config/agent-config.service.js +18 -0
- package/dist/server/config/config.controller.d.ts +26 -0
- package/dist/server/config/config.service.d.ts +14 -0
- package/package.json +3 -1
- package/presets/preset-agents.json +121 -91
- package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +9 -0
- package/presets/workspaces/office-automation/skills/rpa-helper/SKILL.md +9 -0
- package/presets/workspaces/self-media-bot/skills/self-media-tools/SKILL.md +9 -0
- package/apps/desktop/renderer/dist/assets/index-BGHtXhm3.js +0 -89
- package/apps/desktop/renderer/dist/assets/index-CB2-m4ae.css +0 -10
package/dist/core/mcp/index.d.ts
CHANGED
|
@@ -7,12 +7,16 @@ 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;
|
|
20
|
+
/** 单次 MCP 工具返回最大 token;不配置则不限制 */
|
|
21
|
+
mcpMaxResultTokens?: number;
|
|
18
22
|
}): Promise<ToolDefinition[]>;
|
package/dist/core/mcp/index.js
CHANGED
|
@@ -11,10 +11,14 @@ 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
|
+
maxResultTokens: options.mcpMaxResultTokens,
|
|
23
|
+
});
|
|
20
24
|
}
|
|
@@ -4,11 +4,28 @@
|
|
|
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
|
+
/** 单次 MCP 工具返回最大 token;超过则从尾部裁剪并打日志;不配置则不限制 */
|
|
22
|
+
maxResultTokens?: number;
|
|
23
|
+
}
|
|
7
24
|
/**
|
|
8
25
|
* 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
|
|
9
|
-
* 连接失败或 list_tools 失败的 server
|
|
26
|
+
* 连接失败或 list_tools 失败的 server 会按 options 重试,仍失败则跳过并打日志,不阻塞整体。
|
|
10
27
|
*/
|
|
11
|
-
export declare function getMcpToolDefinitions(serverConfigs: McpServerConfig[]): Promise<ToolDefinition[]>;
|
|
28
|
+
export declare function getMcpToolDefinitions(serverConfigs: McpServerConfig[], options?: GetMcpToolDefinitionsOptions): Promise<ToolDefinition[]>;
|
|
12
29
|
/**
|
|
13
30
|
* 关闭并移除所有缓存的 MCP 客户端(进程退出或显式清理时调用)。
|
|
14
31
|
*/
|
|
@@ -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, options.maxResultTokens);
|
|
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
|
+
}
|
|
@@ -6,3 +6,4 @@ export { createSwitchAgentTool } from "./switch-agent-tool.js";
|
|
|
6
6
|
export { createListAgentsTool } from "./list-agents-tool.js";
|
|
7
7
|
export { createCreateAgentTool } from "./create-agent-tool.js";
|
|
8
8
|
export { createGetBookmarkTagsTool, createSaveBookmarkTool, createAddBookmarkTagTool, } from "./bookmark-tool.js";
|
|
9
|
+
export { createWebSearchTool } from "./web-search/index.js";
|
package/dist/core/tools/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { createSwitchAgentTool } from "./switch-agent-tool.js";
|
|
|
6
6
|
export { createListAgentsTool } from "./list-agents-tool.js";
|
|
7
7
|
export { createCreateAgentTool } from "./create-agent-tool.js";
|
|
8
8
|
export { createGetBookmarkTagsTool, createSaveBookmarkTool, createAddBookmarkTagTool, } from "./bookmark-tool.js";
|
|
9
|
+
export { createWebSearchTool } from "./web-search/index.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具返回结果按 token 估算截断:超过 maxTokens 时从尾部裁剪并打日志。
|
|
3
|
+
* 用于 MCP、web_search 等单次返回可能过大的场景。
|
|
4
|
+
*/
|
|
5
|
+
/** 粗略按字符估算 token(中英混合约 1/3) */
|
|
6
|
+
export declare function estTokensFromChars(chars: number): number;
|
|
7
|
+
/**
|
|
8
|
+
* 若 text 估算 token 超过 maxTokens,则保留前 maxTokens 对应字符并打日志,否则返回原 text。
|
|
9
|
+
* @param text 原始文本
|
|
10
|
+
* @param maxTokens 最大保留 token 数(仅当 > 0 时生效)
|
|
11
|
+
* @param toolLabel 工具标识,用于日志
|
|
12
|
+
* @returns 截断后的文本(可能为原 text)
|
|
13
|
+
*/
|
|
14
|
+
export declare function truncateTextToMaxTokens(text: string, maxTokens: number, toolLabel: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具返回结果按 token 估算截断:超过 maxTokens 时从尾部裁剪并打日志。
|
|
3
|
+
* 用于 MCP、web_search 等单次返回可能过大的场景。
|
|
4
|
+
*/
|
|
5
|
+
const LOG_PREFIX = "[tool-result]";
|
|
6
|
+
/** 粗略按字符估算 token(中英混合约 1/3) */
|
|
7
|
+
export function estTokensFromChars(chars) {
|
|
8
|
+
return Math.ceil(chars / 3);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 若 text 估算 token 超过 maxTokens,则保留前 maxTokens 对应字符并打日志,否则返回原 text。
|
|
12
|
+
* @param text 原始文本
|
|
13
|
+
* @param maxTokens 最大保留 token 数(仅当 > 0 时生效)
|
|
14
|
+
* @param toolLabel 工具标识,用于日志
|
|
15
|
+
* @returns 截断后的文本(可能为原 text)
|
|
16
|
+
*/
|
|
17
|
+
export function truncateTextToMaxTokens(text, maxTokens, toolLabel) {
|
|
18
|
+
if (!text || maxTokens <= 0)
|
|
19
|
+
return text;
|
|
20
|
+
const est = estTokensFromChars(text.length);
|
|
21
|
+
if (est <= maxTokens)
|
|
22
|
+
return text;
|
|
23
|
+
const keepChars = Math.max(1, maxTokens * 3);
|
|
24
|
+
const truncated = text.slice(0, keepChars);
|
|
25
|
+
console.log(`${LOG_PREFIX} ${toolLabel} 返回超限 estTokens=${est} maxTokens=${maxTokens} 已从尾部裁剪,保留约 ${maxTokens} token`);
|
|
26
|
+
return truncated;
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
export interface WebSearchToolOptions {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
provider: "brave" | "duck-duck-scrape";
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
timeoutSeconds?: number;
|
|
7
|
+
cacheTtlMinutes?: number;
|
|
8
|
+
maxResults?: number;
|
|
9
|
+
/** 单次搜索返回内容最大 token;超过则从尾部裁剪并打日志;不配置则不限制 */
|
|
10
|
+
maxResultTokens?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 创建 web_search 工具。
|
|
14
|
+
* - enabled 为 false 时返回 null,调用方不应将工具加入 customTools。
|
|
15
|
+
* - enabled 为 true 但当前 provider 不可用(如 Brave 无 Key)时仍返回工具,执行时空转,返回固定提示。
|
|
16
|
+
*/
|
|
17
|
+
export declare function createWebSearchTool(options: WebSearchToolOptions): ToolDefinition | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getWebSearchProvider } from "./providers/index.js";
|
|
3
|
+
import { truncateTextToMaxTokens } from "../truncate-result.js";
|
|
4
|
+
const WebSearchSchema = Type.Object({
|
|
5
|
+
query: Type.String({
|
|
6
|
+
description: "搜索关键词或问题,例如:当前天气、最新新闻、某概念解释等",
|
|
7
|
+
}),
|
|
8
|
+
count: Type.Optional(Type.Number({
|
|
9
|
+
description: "返回结果条数,1-10,默认 5",
|
|
10
|
+
minimum: 1,
|
|
11
|
+
maximum: 10,
|
|
12
|
+
})),
|
|
13
|
+
country: Type.Optional(Type.String({
|
|
14
|
+
description: "可选,地区代码(如 us、cn),部分 Provider 支持",
|
|
15
|
+
})),
|
|
16
|
+
freshness: Type.Optional(Type.String({
|
|
17
|
+
description: "可选,时间范围(如 pd、pw、pm),部分 Provider 支持",
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
const WEB_SEARCH_NOOP_MESSAGE = "当前在线搜索不可用:所选 Provider 未配置或不可用(如选择了 Brave 但未配置 API Key)。请在设置中配置 Brave API Key,或将智能体在线搜索 Provider 改为 DuckDuckGo。";
|
|
21
|
+
/**
|
|
22
|
+
* 创建 web_search 工具。
|
|
23
|
+
* - enabled 为 false 时返回 null,调用方不应将工具加入 customTools。
|
|
24
|
+
* - enabled 为 true 但当前 provider 不可用(如 Brave 无 Key)时仍返回工具,执行时空转,返回固定提示。
|
|
25
|
+
*/
|
|
26
|
+
export function createWebSearchTool(options) {
|
|
27
|
+
if (!options || options.enabled !== true)
|
|
28
|
+
return null;
|
|
29
|
+
const provider = getWebSearchProvider(options.provider);
|
|
30
|
+
const apiKey = options.provider === "brave" ? options.apiKey : undefined;
|
|
31
|
+
const available = provider.isAvailable({ apiKey });
|
|
32
|
+
const timeoutSeconds = options.timeoutSeconds ?? 15;
|
|
33
|
+
const cacheTtlMinutes = options.cacheTtlMinutes ?? 5;
|
|
34
|
+
const maxResults = options.maxResults ?? 5;
|
|
35
|
+
return {
|
|
36
|
+
name: "web_search",
|
|
37
|
+
label: "Web Search",
|
|
38
|
+
description: "联网搜索:根据查询词从互联网获取最新结果(标题、链接、摘要)。无 API Key 时使用 DuckDuckGo;配置 Brave API Key 后可选用 Brave。在回答需要实时信息、新闻、天气、概念解释等问题前可调用本工具。",
|
|
39
|
+
parameters: WebSearchSchema,
|
|
40
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
41
|
+
if (!available) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: WEB_SEARCH_NOOP_MESSAGE }],
|
|
44
|
+
details: undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const query = (params.query ?? "").trim();
|
|
48
|
+
if (!query) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: "请提供搜索关键词(query)。" }],
|
|
51
|
+
details: undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const count = Math.min(10, Math.max(1, params.count ?? maxResults));
|
|
55
|
+
try {
|
|
56
|
+
const result = await provider.search({
|
|
57
|
+
query,
|
|
58
|
+
count,
|
|
59
|
+
apiKey,
|
|
60
|
+
timeoutSeconds,
|
|
61
|
+
cacheTtlMinutes,
|
|
62
|
+
country: params.country,
|
|
63
|
+
freshness: params.freshness,
|
|
64
|
+
});
|
|
65
|
+
const lines = result.results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}${r.description ? `\n ${r.description}` : ""}`);
|
|
66
|
+
let text = (result.cached ? "[缓存] " : "") +
|
|
67
|
+
`搜索「${result.query}」共 ${result.count} 条(${result.provider}${result.tookMs != null ? `, ${result.tookMs}ms` : ""}):\n\n` +
|
|
68
|
+
(lines.length ? lines.join("\n\n") : "未找到结果。");
|
|
69
|
+
const maxResultTokens = options.maxResultTokens;
|
|
70
|
+
if (typeof maxResultTokens === "number" && maxResultTokens > 0) {
|
|
71
|
+
text = truncateTextToMaxTokens(text, maxResultTokens, "web_search");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text }],
|
|
75
|
+
details: { query: result.query, count: result.count, provider: result.provider },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: `在线搜索失败: ${msg}` }],
|
|
82
|
+
details: undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createWebSearchTool } from "./create-web-search-tool.js";
|
|
2
|
+
export type { WebSearchToolOptions } from "./create-web-search-tool.js";
|
|
3
|
+
export type { WebSearchProviderId, WebSearchResultItem, WebSearchProviderResult, IWebSearchProvider, WebSearchSearchParams, } from "./types.js";
|
|
4
|
+
export { getWebSearchProvider } from "./providers/index.js";
|