@next-open-ai/openclawx 0.8.36 → 0.8.48

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.
Files changed (78) hide show
  1. package/README.md +60 -42
  2. package/apps/desktop/renderer/dist/assets/index-BHY1xIZQ.css +10 -0
  3. package/apps/desktop/renderer/dist/assets/index-DQxlVuBe.js +93 -0
  4. package/apps/desktop/renderer/dist/index.html +2 -2
  5. package/dist/cli/cli.js +29 -0
  6. package/dist/cli/extension-cmd.d.ts +15 -0
  7. package/dist/cli/extension-cmd.js +107 -0
  8. package/dist/core/agent/agent-dir.d.ts +6 -0
  9. package/dist/core/agent/agent-dir.js +8 -0
  10. package/dist/core/agent/agent-manager.d.ts +13 -0
  11. package/dist/core/agent/agent-manager.js +77 -7
  12. package/dist/core/agent/proxy/adapters/claude-code-adapter.d.ts +2 -0
  13. package/dist/core/agent/proxy/adapters/claude-code-adapter.js +186 -0
  14. package/dist/core/agent/proxy/adapters/local-adapter.js +3 -1
  15. package/dist/core/agent/proxy/adapters/opencode-adapter.js +65 -29
  16. package/dist/core/agent/proxy/adapters/opencode-local-runner.js +9 -0
  17. package/dist/core/agent/proxy/index.js +2 -0
  18. package/dist/core/agent/token-usage-log-extension.d.ts +14 -0
  19. package/dist/core/agent/token-usage-log-extension.js +61 -0
  20. package/dist/core/config/desktop-config.d.ts +24 -2
  21. package/dist/core/config/desktop-config.js +87 -10
  22. package/dist/core/config/provider-support-default.js +26 -0
  23. package/dist/core/extensions/index.d.ts +1 -0
  24. package/dist/core/extensions/index.js +1 -0
  25. package/dist/core/extensions/load.d.ts +11 -0
  26. package/dist/core/extensions/load.js +101 -0
  27. package/dist/core/local-llm-server/index.d.ts +32 -0
  28. package/dist/core/local-llm-server/index.js +126 -0
  29. package/dist/core/local-llm-server/llm-context.d.ts +60 -0
  30. package/dist/core/local-llm-server/llm-context.js +221 -0
  31. package/dist/core/local-llm-server/model-resolve.d.ts +20 -0
  32. package/dist/core/local-llm-server/model-resolve.js +58 -0
  33. package/dist/core/local-llm-server/server.d.ts +1 -0
  34. package/dist/core/local-llm-server/server.js +235 -0
  35. package/dist/core/mcp/adapter.d.ts +4 -2
  36. package/dist/core/mcp/adapter.js +10 -4
  37. package/dist/core/mcp/index.d.ts +2 -0
  38. package/dist/core/mcp/index.js +1 -0
  39. package/dist/core/mcp/operator.d.ts +2 -0
  40. package/dist/core/mcp/operator.js +1 -1
  41. package/dist/core/memory/local-embedding.d.ts +4 -3
  42. package/dist/core/memory/local-embedding.js +43 -3
  43. package/dist/core/tools/index.d.ts +1 -0
  44. package/dist/core/tools/index.js +1 -0
  45. package/dist/core/tools/truncate-result.d.ts +14 -0
  46. package/dist/core/tools/truncate-result.js +27 -0
  47. package/dist/core/tools/web-search/create-web-search-tool.d.ts +17 -0
  48. package/dist/core/tools/web-search/create-web-search-tool.js +87 -0
  49. package/dist/core/tools/web-search/index.d.ts +4 -0
  50. package/dist/core/tools/web-search/index.js +2 -0
  51. package/dist/core/tools/web-search/providers/brave.d.ts +2 -0
  52. package/dist/core/tools/web-search/providers/brave.js +87 -0
  53. package/dist/core/tools/web-search/providers/duck-duck-scrape.d.ts +2 -0
  54. package/dist/core/tools/web-search/providers/duck-duck-scrape.js +47 -0
  55. package/dist/core/tools/web-search/providers/index.d.ts +5 -0
  56. package/dist/core/tools/web-search/providers/index.js +13 -0
  57. package/dist/core/tools/web-search/types.d.ts +35 -0
  58. package/dist/core/tools/web-search/types.js +4 -0
  59. package/dist/gateway/methods/agent-chat.js +74 -42
  60. package/dist/gateway/methods/run-scheduled-task.js +2 -0
  61. package/dist/gateway/server.js +54 -1
  62. package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
  63. package/dist/server/agent-config/agent-config.service.d.ts +17 -3
  64. package/dist/server/agent-config/agent-config.service.js +23 -0
  65. package/dist/server/config/config.controller.d.ts +84 -4
  66. package/dist/server/config/config.controller.js +135 -3
  67. package/dist/server/config/config.module.js +3 -2
  68. package/dist/server/config/config.service.d.ts +14 -0
  69. package/dist/server/config/local-models.service.d.ts +52 -0
  70. package/dist/server/config/local-models.service.js +211 -0
  71. package/package.json +3 -1
  72. package/presets/preset-agents.json +121 -91
  73. package/presets/recommended-local-models.json +42 -0
  74. package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +9 -0
  75. package/presets/workspaces/office-automation/skills/rpa-helper/SKILL.md +9 -0
  76. package/presets/workspaces/self-media-bot/skills/self-media-tools/SKILL.md +9 -0
  77. package/apps/desktop/renderer/dist/assets/index-BGHtXhm3.js +0 -89
  78. package/apps/desktop/renderer/dist/assets/index-CB2-m4ae.css +0 -10
@@ -0,0 +1,235 @@
1
+ /**
2
+ * OpenAI 兼容 HTTP 服务(严格对齐 [OpenAI Chat Completions / Embeddings API](https://platform.openai.com/docs/api-reference))。
3
+ * 实现:GET /v1/models;POST /v1/chat/completions(流式/非流式,tool_calls);POST /v1/embeddings。
4
+ * - 错误统一为 { error: { message, type } },流式错误以 SSE 事件发送后结束。
5
+ * - 流式 delta 仅含规范字段:role、content(必为 string)、tool_calls(规范结构),避免客户端解析到未知类型。
6
+ */
7
+ import { createServer } from "node:http";
8
+ import { randomUUID } from "node:crypto";
9
+ import { chatCompletionStream, chatCompletion, getEmbedding, isReady, } from "./llm-context.js";
10
+ const LLM_MODEL_ID = process.env.LOCAL_LLM_MODEL_ID ?? "local-llm";
11
+ const EMB_MODEL_ID = process.env.LOCAL_EMB_MODEL_ID ?? "local-embedding";
12
+ function readBody(req) {
13
+ return new Promise((resolve, reject) => {
14
+ let data = "";
15
+ req.on("data", (chunk) => (data += chunk));
16
+ req.on("end", () => {
17
+ try {
18
+ resolve(data ? JSON.parse(data) : {});
19
+ }
20
+ catch {
21
+ reject(new Error("Invalid JSON body"));
22
+ }
23
+ });
24
+ req.on("error", reject);
25
+ });
26
+ }
27
+ function sendJson(res, status, body) {
28
+ const json = JSON.stringify(body);
29
+ res.writeHead(status, { "Content-Type": "application/json" });
30
+ res.end(json);
31
+ }
32
+ /** OpenAI 规范错误体:{ error: { message, type } } */
33
+ function sendError(res, status, message, type = status >= 500 ? "server_error" : "invalid_request_error") {
34
+ sendJson(res, status, { error: { message: String(message), type } });
35
+ }
36
+ /** 构造 OpenAI 格式的 chat completion 响应对象 */
37
+ function buildCompletionResponse(content, tool_calls, finish_reason, model) {
38
+ const message = { role: "assistant", content: tool_calls ? null : content };
39
+ if (tool_calls?.length)
40
+ message.tool_calls = tool_calls;
41
+ return {
42
+ id: `chatcmpl-${randomUUID()}`,
43
+ object: "chat.completion",
44
+ created: Math.floor(Date.now() / 1000),
45
+ model,
46
+ choices: [{ index: 0, message, finish_reason, logprobs: null }],
47
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
48
+ };
49
+ }
50
+ /** 构造 SSE delta chunk,仅含 OpenAI 流式规范字段,不包含 logprobs 避免下游解析异常 */
51
+ function buildStreamChunk(id, model, delta, finish_reason) {
52
+ const choice = { index: 0, delta, finish_reason };
53
+ const chunk = {
54
+ id,
55
+ object: "chat.completion.chunk",
56
+ created: Math.floor(Date.now() / 1000),
57
+ model,
58
+ choices: [choice],
59
+ };
60
+ return `data: ${JSON.stringify(chunk)}\n\n`;
61
+ }
62
+ async function handleChatCompletions(req, res) {
63
+ let body;
64
+ try {
65
+ body = await readBody(req);
66
+ }
67
+ catch {
68
+ return sendError(res, 400, "Invalid JSON body");
69
+ }
70
+ if (!isReady())
71
+ return sendError(res, 503, "模型尚未加载完成,请稍后重试", "server_error");
72
+ if (!Array.isArray(body.messages)) {
73
+ return sendError(res, 400, "Missing or invalid 'messages' (must be an array)", "invalid_request_error");
74
+ }
75
+ if (body.messages.length === 0) {
76
+ return sendError(res, 400, "'messages' must contain at least one message", "invalid_request_error");
77
+ }
78
+ const messages = body.messages;
79
+ const tools = Array.isArray(body.tools) ? body.tools : [];
80
+ const stream = body.stream === true;
81
+ const model = typeof body.model === "string" && body.model.trim() ? body.model.trim() : LLM_MODEL_ID;
82
+ const abortCtrl = new AbortController();
83
+ req.on("close", () => abortCtrl.abort());
84
+ if (stream) {
85
+ res.writeHead(200, {
86
+ "Content-Type": "text/event-stream",
87
+ "Cache-Control": "no-cache",
88
+ Connection: "keep-alive",
89
+ });
90
+ const id = `chatcmpl-${randomUUID()}`;
91
+ // 首包:role + content 占位,与 DeepSeek 等一致,避免仅 role 时下游对 delta 的严格校验
92
+ res.write(buildStreamChunk(id, model, { role: "assistant", content: "" }, null));
93
+ let pendingToolCalls;
94
+ let finishReason = "stop";
95
+ try {
96
+ await chatCompletionStream(messages, tools, (chunk) => {
97
+ if (abortCtrl.signal.aborted)
98
+ return;
99
+ if (chunk.content != null && chunk.content !== "") {
100
+ const text = typeof chunk.content === "string" ? chunk.content : String(chunk.content);
101
+ res.write(buildStreamChunk(id, model, { content: text }, null));
102
+ }
103
+ if (chunk.tool_calls?.length) {
104
+ pendingToolCalls = chunk.tool_calls;
105
+ }
106
+ if (chunk.finish_reason) {
107
+ finishReason = chunk.finish_reason;
108
+ }
109
+ }, abortCtrl.signal);
110
+ }
111
+ catch (e) {
112
+ if (!abortCtrl.signal.aborted) {
113
+ const errMsg = e instanceof Error ? e.message : String(e);
114
+ const stack = e instanceof Error ? e.stack : undefined;
115
+ console.error("[local-llm] stream error:", errMsg);
116
+ if (stack)
117
+ console.error("[local-llm] stream stack:", stack);
118
+ res.write(`data: ${JSON.stringify({ error: { message: errMsg, type: "server_error" } })}\n\n`);
119
+ }
120
+ res.end();
121
+ return;
122
+ }
123
+ // 若有 tool_calls,按 OpenAI 流式规范发一条 delta(含 index/id/type/function),与 DeepSeek 等一致
124
+ if (pendingToolCalls?.length) {
125
+ const deltaToolCalls = pendingToolCalls.map((tc, i) => ({
126
+ index: i,
127
+ id: typeof tc.id === "string" ? tc.id : `call_${i}`,
128
+ type: "function",
129
+ function: {
130
+ name: typeof tc.function?.name === "string" ? tc.function.name : "",
131
+ arguments: typeof tc.function?.arguments === "string" ? tc.function.arguments : "",
132
+ },
133
+ }));
134
+ res.write(buildStreamChunk(id, model, { tool_calls: deltaToolCalls }, null));
135
+ finishReason = "tool_calls";
136
+ }
137
+ res.write(buildStreamChunk(id, model, {}, finishReason));
138
+ res.write("data: [DONE]\n\n");
139
+ res.end();
140
+ }
141
+ else {
142
+ try {
143
+ const result = await chatCompletion(messages, tools, abortCtrl.signal);
144
+ sendJson(res, 200, buildCompletionResponse(result.content, result.tool_calls, result.finish_reason, model));
145
+ }
146
+ catch (e) {
147
+ const msg = e instanceof Error ? e.message : String(e);
148
+ sendError(res, 500, msg, "server_error");
149
+ }
150
+ }
151
+ }
152
+ async function handleEmbeddings(req, res) {
153
+ let body;
154
+ try {
155
+ body = await readBody(req);
156
+ }
157
+ catch {
158
+ return sendError(res, 400, "Invalid JSON body", "invalid_request_error");
159
+ }
160
+ if (!isReady())
161
+ return sendError(res, 503, "模型尚未加载完成,请稍后重试", "server_error");
162
+ const input = body.input;
163
+ if (input === undefined || input === null) {
164
+ return sendError(res, 400, "Missing 'input' (string or array of strings)", "invalid_request_error");
165
+ }
166
+ const inputs = Array.isArray(input) ? input : [input];
167
+ if (inputs.length === 0 || inputs.some((x) => typeof x !== "string")) {
168
+ return sendError(res, 400, "'input' must be a non-empty string or array of strings", "invalid_request_error");
169
+ }
170
+ try {
171
+ const data = await Promise.all(inputs.map(async (text, i) => ({
172
+ object: "embedding",
173
+ index: i,
174
+ embedding: await getEmbedding(text),
175
+ })));
176
+ sendJson(res, 200, {
177
+ object: "list",
178
+ data,
179
+ model: body.model ?? EMB_MODEL_ID,
180
+ usage: { prompt_tokens: 0, total_tokens: 0 },
181
+ });
182
+ }
183
+ catch (e) {
184
+ const msg = e instanceof Error ? e.message : String(e);
185
+ sendError(res, 500, msg, "server_error");
186
+ }
187
+ }
188
+ function handleModels(_req, res) {
189
+ sendJson(res, 200, {
190
+ object: "list",
191
+ data: [
192
+ { id: LLM_MODEL_ID, object: "model", created: 0, owned_by: "local" },
193
+ { id: EMB_MODEL_ID, object: "model", created: 0, owned_by: "local" },
194
+ ],
195
+ });
196
+ }
197
+ export function createOpenAICompatServer(port) {
198
+ return new Promise((resolve, reject) => {
199
+ const server = createServer(async (req, res) => {
200
+ const url = req.url ?? "";
201
+ const method = req.method ?? "";
202
+ // CORS
203
+ res.setHeader("Access-Control-Allow-Origin", "*");
204
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
205
+ if (method === "OPTIONS") {
206
+ res.writeHead(204);
207
+ res.end();
208
+ return;
209
+ }
210
+ try {
211
+ if (method === "GET" && url === "/v1/models") {
212
+ handleModels(req, res);
213
+ }
214
+ else if (method === "POST" && url === "/v1/chat/completions") {
215
+ await handleChatCompletions(req, res);
216
+ }
217
+ else if (method === "POST" && url === "/v1/embeddings") {
218
+ await handleEmbeddings(req, res);
219
+ }
220
+ else {
221
+ sendError(res, 404, `Not found: ${method} ${url}`, "invalid_request_error");
222
+ }
223
+ }
224
+ catch (e) {
225
+ if (!res.headersSent)
226
+ sendError(res, 500, String(e));
227
+ }
228
+ });
229
+ server.listen(port, "127.0.0.1", () => {
230
+ console.log(`[local-llm] OpenAI 兼容服务已启动: http://127.0.0.1:${port}/v1`);
231
+ resolve();
232
+ });
233
+ server.on("error", reject);
234
+ });
235
+ }
@@ -9,9 +9,11 @@ import type { McpClient } from "./client.js";
9
9
  * @param tool MCP tools/list 返回的项
10
10
  * @param client 已连接的 McpClient,用于 callTool
11
11
  * @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
12
+ * @param maxResultTokens 可选,单次返回最大 token;超过则从尾部裁剪并打日志;不配置则不限制
12
13
  */
13
- export declare function mcpToolToToolDefinition(tool: McpTool, client: McpClient, serverId?: string): ToolDefinition;
14
+ export declare function mcpToolToToolDefinition(tool: McpTool, client: McpClient, serverId?: string, maxResultTokens?: number): ToolDefinition;
14
15
  /**
15
16
  * 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
17
+ * @param maxResultTokens 可选,单次返回最大 token;不配置则不限制
16
18
  */
17
- export declare function mcpToolsToToolDefinitions(tools: McpTool[], client: McpClient, serverId?: string): ToolDefinition[];
19
+ export declare function mcpToolsToToolDefinitions(tools: McpTool[], client: McpClient, serverId?: string, maxResultTokens?: number): ToolDefinition[];
@@ -2,6 +2,7 @@
2
2
  * MCP Tool 转为 pi-coding-agent ToolDefinition 的适配层。
3
3
  */
4
4
  import { Type } from "@sinclair/typebox";
5
+ import { truncateTextToMaxTokens } from "../tools/truncate-result.js";
5
6
  /** 通用参数:MCP 工具接受任意 JSON 对象作为 arguments */
6
7
  const McpToolParamsSchema = Type.Record(Type.String(), Type.Any());
7
8
  /**
@@ -9,8 +10,9 @@ const McpToolParamsSchema = Type.Record(Type.String(), Type.Any());
9
10
  * @param tool MCP tools/list 返回的项
10
11
  * @param client 已连接的 McpClient,用于 callTool
11
12
  * @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
13
+ * @param maxResultTokens 可选,单次返回最大 token;超过则从尾部裁剪并打日志;不配置则不限制
12
14
  */
13
- export function mcpToolToToolDefinition(tool, client, serverId) {
15
+ export function mcpToolToToolDefinition(tool, client, serverId, maxResultTokens) {
14
16
  const name = serverId ? `${serverId}_${tool.name}` : tool.name;
15
17
  const description = (tool.description ?? "").trim() || `MCP tool: ${tool.name}`;
16
18
  return {
@@ -22,10 +24,13 @@ export function mcpToolToToolDefinition(tool, client, serverId) {
22
24
  const args = params && typeof params === "object" ? params : {};
23
25
  try {
24
26
  const result = await client.callTool(tool.name, args);
25
- const text = result.content
27
+ let text = result.content
26
28
  ?.filter((c) => c.type === "text")
27
29
  .map((c) => c.text)
28
30
  .join("\n") ?? (result.isError ? "MCP 调用返回错误" : "");
31
+ if (typeof maxResultTokens === "number" && maxResultTokens > 0) {
32
+ text = truncateTextToMaxTokens(text, maxResultTokens, `MCP ${name}`);
33
+ }
29
34
  return {
30
35
  content: [{ type: "text", text }],
31
36
  details: result.isError ? { isError: true } : undefined,
@@ -43,7 +48,8 @@ export function mcpToolToToolDefinition(tool, client, serverId) {
43
48
  }
44
49
  /**
45
50
  * 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
51
+ * @param maxResultTokens 可选,单次返回最大 token;不配置则不限制
46
52
  */
47
- export function mcpToolsToToolDefinitions(tools, client, serverId) {
48
- return tools.map((t) => mcpToolToToolDefinition(t, client, serverId));
53
+ export function mcpToolsToToolDefinitions(tools, client, serverId, maxResultTokens) {
54
+ return tools.map((t) => mcpToolToToolDefinition(t, client, serverId, maxResultTokens));
49
55
  }
@@ -17,4 +17,6 @@ export { mcpToolToToolDefinition, mcpToolsToToolDefinitions } from "./adapter.js
17
17
  export declare function createMcpToolsForSession(options: {
18
18
  mcpServers?: McpServerConfig[] | McpServersStandardFormat;
19
19
  sessionId?: string;
20
+ /** 单次 MCP 工具返回最大 token;不配置则不限制 */
21
+ mcpMaxResultTokens?: number;
20
22
  }): Promise<ToolDefinition[]>;
@@ -19,5 +19,6 @@ export async function createMcpToolsForSession(options) {
19
19
  return [];
20
20
  return getMcpToolDefinitions(configs, {
21
21
  sessionId: options.sessionId,
22
+ maxResultTokens: options.mcpMaxResultTokens,
22
23
  });
23
24
  }
@@ -18,6 +18,8 @@ export interface GetMcpToolDefinitionsOptions {
18
18
  initRetryDelayMs?: number;
19
19
  /** 会话 ID,用于经全局 sendSessionMessage 推送 MCP 进度系统消息 */
20
20
  sessionId?: string;
21
+ /** 单次 MCP 工具返回最大 token;超过则从尾部裁剪并打日志;不配置则不限制 */
22
+ maxResultTokens?: number;
21
23
  }
22
24
  /**
23
25
  * 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
@@ -113,7 +113,7 @@ export async function getMcpToolDefinitions(serverConfigs, options = {}) {
113
113
  try {
114
114
  const tools = await client.listTools();
115
115
  const serverId = `mcp${i}`;
116
- const definitions = mcpToolsToToolDefinitions(tools, client, serverId);
116
+ const definitions = mcpToolsToToolDefinitions(tools, client, serverId, options.maxResultTokens);
117
117
  allTools.push(...definitions);
118
118
  toolsListed = true;
119
119
  break;
@@ -1,10 +1,11 @@
1
1
  /**
2
- * 本地 embedding:仅 node-llama-cpp (GGUF)。不可用时返回 null,由上层决定是否使用在线 RAG。
2
+ * 本地 embedding:优先走本地 LLM 子进程服务(/v1/embeddings),
3
+ * 不可用时回退到 node-llama-cpp 直接加载(GGUF)。
3
4
  */
4
5
  import type { IEmbeddingProvider } from "./embedding-types.js";
5
6
  export declare function getLocalEmbeddingUnavailableReason(): string | null;
6
7
  /**
7
- * 获取本地 embedding 提供方(懒加载,失败后不再重试)。
8
- * 仅使用 node-llama-cpp (GGUF)。不可用时返回 null。
8
+ * 获取本地 embedding 提供方(懒加载)。
9
+ * 优先使用本地 LLM 子进程服务;不可用时回退到 node-llama-cpp 直接加载。
9
10
  */
10
11
  export declare function getLocalEmbeddingProvider(): Promise<IEmbeddingProvider | null>;
@@ -5,19 +5,59 @@ let envLogged = false;
5
5
  export function getLocalEmbeddingUnavailableReason() {
6
6
  return getLocalEmbeddingLlamaUnavailableReason();
7
7
  }
8
+ /** 通过本地 LLM 子进程服务的 /v1/embeddings 接口获取向量 */
9
+ function createLocalServerEmbeddingProvider(baseUrl) {
10
+ return {
11
+ name: "local-llm-server",
12
+ async embed(text) {
13
+ try {
14
+ const res = await fetch(`${baseUrl}/embeddings`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json", Authorization: "Bearer local" },
17
+ body: JSON.stringify({ input: text }),
18
+ signal: AbortSignal.timeout(30_000),
19
+ });
20
+ if (!res.ok)
21
+ return null;
22
+ const data = await res.json();
23
+ const vec = data?.data?.[0]?.embedding;
24
+ return Array.isArray(vec) && vec.length > 0 ? vec : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ },
30
+ };
31
+ }
8
32
  /**
9
- * 获取本地 embedding 提供方(懒加载,失败后不再重试)。
10
- * 仅使用 node-llama-cpp (GGUF)。不可用时返回 null。
33
+ * 获取本地 embedding 提供方(懒加载)。
34
+ * 优先使用本地 LLM 子进程服务;不可用时回退到 node-llama-cpp 直接加载。
11
35
  */
12
36
  export async function getLocalEmbeddingProvider() {
13
37
  if (cached)
14
38
  return cached;
39
+ // 优先:本地 LLM 子进程服务
40
+ const localBaseUrl = process.env.LOCAL_LLM_BASE_URL;
41
+ if (localBaseUrl) {
42
+ const serverProvider = createLocalServerEmbeddingProvider(localBaseUrl);
43
+ // 快速探测服务是否可用
44
+ const testVec = await serverProvider.embed("test");
45
+ if (testVec !== null) {
46
+ cached = serverProvider;
47
+ if (!envLogged) {
48
+ envLogged = true;
49
+ console.log("[RAG embedding] 使用本地 LLM 子进程服务");
50
+ }
51
+ return cached;
52
+ }
53
+ }
54
+ // 回退:node-llama-cpp 直接加载
15
55
  const provider = await getLocalEmbeddingLlamaProvider(getRagLocalModelPathSync());
16
56
  if (provider) {
17
57
  cached = provider;
18
58
  if (!envLogged) {
19
59
  envLogged = true;
20
- console.warn("[RAG embedding] 本地模型使用 node-llama-cpp (GGUF)");
60
+ console.log("[RAG embedding] 使用 node-llama-cpp (GGUF) 直接加载");
21
61
  }
22
62
  return cached;
23
63
  }
@@ -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";
@@ -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";
@@ -0,0 +1,2 @@
1
+ export { createWebSearchTool } from "./create-web-search-tool.js";
2
+ export { getWebSearchProvider } from "./providers/index.js";
@@ -0,0 +1,2 @@
1
+ import type { IWebSearchProvider } from "../types.js";
2
+ export declare const braveProvider: IWebSearchProvider;