@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
@@ -20,7 +20,9 @@
20
20
  * - opencode/big-pickle — Big Pickle
21
21
  * 本适配器请求不传 model 时由 OpenCode 服务端使用上述配置的默认模型。详见 https://opencode.ai/docs/zen
22
22
  */
23
+ import { join, resolve } from "path";
23
24
  import { createOpencodeClient } from "@opencode-ai/sdk";
25
+ import { getOpenbotWorkspaceDir } from "../../agent-dir.js";
24
26
  import { ensureLocalOpencodeRunning } from "./opencode-local-runner.js";
25
27
  /** 单次请求与事件流超时。长任务(如 init 分析大项目、多轮工具调用)可能较久,故设 15 分钟 */
26
28
  const REQUEST_TIMEOUT_MS = 15 * 60 * 1000; // 15 min
@@ -85,6 +87,17 @@ function getOpenCodeConfig(config) {
85
87
  model: { providerID, modelID: modelID || "default" },
86
88
  };
87
89
  }
90
+ /** 与 Claude Code 一致:未显式配置时使用智能体工作区路径 ~/.openbot/workspace/<workspace>/ */
91
+ function getOpencodeWorkingDirectory(config) {
92
+ const custom = config.opencode?.workingDirectory;
93
+ if (typeof custom === "string" && custom.trim()) {
94
+ return resolve(custom.trim());
95
+ }
96
+ const w = config.workspace;
97
+ if (typeof w !== "string" || !w.trim())
98
+ return undefined;
99
+ return join(getOpenbotWorkspaceDir(), w.trim());
100
+ }
88
101
  function buildAuthHeaders(oc) {
89
102
  const user = oc.username || DEFAULT_SERVER_USERNAME;
90
103
  const pass = oc.password ?? "";
@@ -208,7 +221,7 @@ function parseSlashCommand(message) {
208
221
  const args = space >= 0 ? rest.slice(space + 1).trim() : "";
209
222
  return command ? { command, args } : null;
210
223
  }
211
- /** 从 session.prompt / session.command 返回的 parts 提取文本 */
224
+ /** 从 session.prompt / session.command 返回的 parts 提取文本(含所有 type,用于兼容旧逻辑) */
212
225
  function partsToText(parts) {
213
226
  if (!Array.isArray(parts))
214
227
  return "";
@@ -217,6 +230,17 @@ function partsToText(parts) {
217
230
  .filter(Boolean)
218
231
  .join("");
219
232
  }
233
+ /** 仅取 type=text 的 part 拼接为助手正文,与流式时只推 text 保持一致,不包含 reasoning/step-start/step-finish */
234
+ function partsToReplyText(parts) {
235
+ if (!Array.isArray(parts))
236
+ return "";
237
+ return parts
238
+ .filter((p) => p?.type === "text")
239
+ .map((p) => (typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : ""))
240
+ .filter(Boolean)
241
+ .join("")
242
+ .trim();
243
+ }
220
244
  /** 日志用:可序列化对象,避免循环引用、过长字符串和不可序列化字段 */
221
245
  function safeForLog(obj, maxStrLen = 2000) {
222
246
  if (obj === null || obj === undefined)
@@ -348,7 +372,7 @@ async function pollForAssistantMessage(session, sessionId) {
348
372
  const parts = item?.info?.parts ?? item?.parts;
349
373
  if (!Array.isArray(parts))
350
374
  continue;
351
- const text = partsToText(parts);
375
+ const text = partsToReplyText(parts);
352
376
  if (text)
353
377
  return text;
354
378
  // 已有 assistant 但 parts 为空,可能仍在生成,继续轮询
@@ -367,7 +391,7 @@ export const opencodeAdapter = {
367
391
  throw new Error("OpenCode adapter: missing opencode.port or (remote 模式下缺少 address) in agent config");
368
392
  }
369
393
  if (config.opencode?.mode === "local" && config.opencode.port != null) {
370
- await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, config.opencode.workingDirectory?.trim() || undefined);
394
+ await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, getOpencodeWorkingDirectory(config));
371
395
  }
372
396
  const hasPassword = Boolean(oc.password?.trim());
373
397
  const userSignal = options.signal;
@@ -538,15 +562,36 @@ export const opencodeAdapter = {
538
562
  let hadAnyChunk = false;
539
563
  let lastTextLength = 0;
540
564
  let eventCount = 0;
541
- // text 部分按 (messageID, partType) 记录上次已下发的完整内容,避免同一 part 多次相同事件导致前端重复显示
542
- const lastEmittedNonText = new Map();
565
+ // reasoning 只缓存在见到 text session.idle 时再整体输出一次,避免「先部分后完整」重复显示
566
+ const reasoningBuffer = new Map();
567
+ // 正文累积后去掉 [step-start]/[step-finish] 再推送,避免服务端把这类标签混在 text 里
568
+ let textAccumulated = "";
569
+ let lastEmittedCleanLen = 0;
570
+ const stepMarkerRe = /\n?\[step-(?:start|finish)\]\n?/g;
571
+ const emitStrippedText = () => {
572
+ const cleaned = textAccumulated.replace(stepMarkerRe, "");
573
+ const toEmit = cleaned.slice(lastEmittedCleanLen);
574
+ if (toEmit) {
575
+ hadAnyChunk = true;
576
+ callbacks.onChunk(toEmit);
577
+ lastEmittedCleanLen = cleaned.length;
578
+ }
579
+ };
580
+ const flushReasoningForMessage = (messageID) => {
581
+ const raw = reasoningBuffer.get(messageID);
582
+ if (!raw?.trim())
583
+ return;
584
+ reasoningBuffer.delete(messageID);
585
+ const formatted = `\n\n---\nreasoning: ${raw.slice(0, 2000)}${raw.length > 2000 ? "…" : ""}\n---\n\n`;
586
+ hadAnyChunk = true;
587
+ callbacks.onChunk(formatted);
588
+ };
543
589
  try {
544
590
  for await (const event of eventStream) {
545
591
  if (userSignal?.aborted)
546
592
  break;
547
593
  eventCount++;
548
594
  const ev = event;
549
- // 日志:长任务排查时可见 OpenCode 下发了哪些事件类型与 part 类型,便于确认是否有中间状态未回显
550
595
  if (ev.type !== "message.part.updated" || (ev.properties?.part?.type !== "text")) {
551
596
  const partType = ev.properties?.part?.type ?? "(no part)";
552
597
  console.log(`[OpenCode] event #${eventCount} type=${ev.type} part.type=${partType}`);
@@ -558,41 +603,32 @@ export const opencodeAdapter = {
558
603
  if (ev.type === "message.part.updated" && ev.properties?.part?.sessionID === opencodeSessionId) {
559
604
  const part = ev.properties.part;
560
605
  if (userMessageID != null && part.messageID === userMessageID)
561
- continue; // 跳过用户消息的 part
606
+ continue;
562
607
  const partType = part.type ?? "";
563
608
  const delta = ev.properties.delta;
564
609
  const fullText = typeof part?.text === "string" ? part.text : "";
565
610
  if (partType === "text") {
611
+ const msgId = part.messageID ?? "";
612
+ flushReasoningForMessage(msgId);
566
613
  if (typeof delta === "string" && delta) {
567
- hadAnyChunk = true;
568
- callbacks.onChunk(delta);
569
- lastTextLength += delta.length;
614
+ textAccumulated += delta;
615
+ emitStrippedText();
570
616
  }
571
617
  else if (fullText.length > lastTextLength) {
572
- hadAnyChunk = true;
573
- callbacks.onChunk(fullText.slice(lastTextLength));
618
+ textAccumulated = fullText;
574
619
  lastTextLength = fullText.length;
620
+ emitStrippedText();
575
621
  }
576
622
  }
577
- else {
578
- // text(如 reasoning、tool_call 等):向前端回显简短状态;相同 part 相同内容只发一次,避免重复
579
- const key = `${part.messageID ?? ""}-${partType}`;
580
- if (fullText && fullText.trim()) {
581
- const formatted = `\n\n---\n${partType}: ${fullText.slice(0, 500)}${fullText.length > 500 ? "…" : ""}\n---\n\n`;
582
- if (lastEmittedNonText.get(key) === formatted)
583
- continue;
584
- lastEmittedNonText.set(key, formatted);
585
- hadAnyChunk = true;
586
- callbacks.onChunk(formatted);
587
- }
588
- else if (partType) {
589
- // [tool]/[step-start]/[step-finish] 等无正文:每次事件都发(无法区分“同一事件重复”与“多次不同 tool/step”,故不做去重)
590
- hadAnyChunk = true;
591
- callbacks.onChunk(`\n[${partType}]\n`);
592
- }
623
+ else if (partType === "reasoning" && fullText.trim()) {
624
+ const msgId = part.messageID ?? "";
625
+ reasoningBuffer.set(msgId, fullText);
593
626
  }
627
+ // step-start、step-finish、tool_call 等不推给前端
594
628
  }
595
629
  if (ev.type === "session.idle" && ev.properties?.sessionID === opencodeSessionId) {
630
+ for (const msgId of reasoningBuffer.keys())
631
+ flushReasoningForMessage(msgId);
596
632
  console.log(`[OpenCode] session.idle after ${eventCount} events, hadAnyChunk=${hadAnyChunk}`);
597
633
  break;
598
634
  }
@@ -625,7 +661,7 @@ export const opencodeAdapter = {
625
661
  throw new Error("OpenCode adapter: missing opencode.port or (remote 模式下缺少 address) in agent config");
626
662
  }
627
663
  if (config.opencode?.mode === "local" && config.opencode.port != null) {
628
- await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, config.opencode.workingDirectory?.trim() || undefined);
664
+ await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, getOpencodeWorkingDirectory(config));
629
665
  }
630
666
  const hasPassword = oc.password != null && String(oc.password).trim() !== "";
631
667
  const client = createOpencodeClient({
@@ -3,6 +3,7 @@
3
3
  * 并通过 OPENCODE_CONFIG_CONTENT 注入默认模型与端口;可选设置工作目录(cwd)。
4
4
  */
5
5
  import { spawn } from "child_process";
6
+ import { mkdirSync } from "node:fs";
6
7
  import { createInterface } from "readline";
7
8
  import { resolve } from "path";
8
9
  const HEALTH_PATH = "/global/health";
@@ -41,6 +42,14 @@ export async function ensureLocalOpencodeRunning(port, model, workingDirectory)
41
42
  ...process.env,
42
43
  OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
43
44
  };
45
+ if (cwd) {
46
+ try {
47
+ mkdirSync(cwd, { recursive: true });
48
+ }
49
+ catch (e) {
50
+ console.warn("[OpenCode local runner] mkdir cwd failed:", e.message);
51
+ }
52
+ }
44
53
  const child = spawn("opencode", ["serve", "--port", String(port), "--hostname", "127.0.0.1"], {
45
54
  env,
46
55
  stdio: ["ignore", "pipe", "pipe"],
@@ -8,9 +8,11 @@ import { localAdapter } from "./adapters/local-adapter.js";
8
8
  import { cozeAdapter } from "./adapters/coze-adapter.js";
9
9
  import { openclawxAdapter } from "./adapters/openclawx-adapter.js";
10
10
  import { opencodeAdapter } from "./adapters/opencode-adapter.js";
11
+ import { claudeCodeAdapter } from "./adapters/claude-code-adapter.js";
11
12
  registerAgentProxyAdapter(localAdapter);
12
13
  registerAgentProxyAdapter(cozeAdapter);
13
14
  registerAgentProxyAdapter(openclawxAdapter);
14
15
  registerAgentProxyAdapter(opencodeAdapter);
16
+ registerAgentProxyAdapter(claudeCodeAdapter);
15
17
  export { runForChannelStream, runForChannelCollect } from "./run-for-channel.js";
16
18
  export { registerAgentProxyAdapter, getAgentProxyAdapter, listAgentProxyAdapterTypes } from "./registry.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 内置 extension:在 turn_start / turn_end 和 compaction 相关事件时打印 context 用量与 compaction 信息,
3
+ * 便于分析 token 占用。仅打 log,不改变行为。
4
+ * 若 agent-manager 在 session 创建后调用了 setTokenUsageInitialStats,则每轮会打印 systemPrompt/skills/tools/conversation 的估算占比。
5
+ */
6
+ import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
7
+ export interface TokenUsageInitialStats {
8
+ systemPromptEstTokens: number;
9
+ skillsBlockEstTokens: number;
10
+ toolsDefsEstTokens: number;
11
+ }
12
+ /** 由 agent-manager 在 session 创建并算完 systemPrompt/skills/tools 后调用,供本 extension 在 turn 时打印占比 */
13
+ export declare function setTokenUsageInitialStats(compositeKey: string, stats: TokenUsageInitialStats): void;
14
+ export declare function createTokenUsageLogExtensionFactory(compositeKey: string): ExtensionFactory;
@@ -0,0 +1,61 @@
1
+ const LOG_PREFIX = "[token-usage]";
2
+ const initialStatsByKey = new Map();
3
+ /** 由 agent-manager 在 session 创建并算完 systemPrompt/skills/tools 后调用,供本 extension 在 turn 时打印占比 */
4
+ export function setTokenUsageInitialStats(compositeKey, stats) {
5
+ initialStatsByKey.set(compositeKey, stats);
6
+ }
7
+ function logContextBreakdown(phase, totalTokens, compositeKey) {
8
+ const stats = initialStatsByKey.get(compositeKey);
9
+ if (!stats || totalTokens <= 0)
10
+ return;
11
+ const { systemPromptEstTokens, skillsBlockEstTokens, toolsDefsEstTokens } = stats;
12
+ const conversationEst = Math.max(0, totalTokens - systemPromptEstTokens - toolsDefsEstTokens);
13
+ console.log(`${LOG_PREFIX} ${phase} breakdown | total=${totalTokens} systemPrompt≈${systemPromptEstTokens} (含skills≈${skillsBlockEstTokens}) tools≈${toolsDefsEstTokens} conversation≈${conversationEst}`);
14
+ }
15
+ export function createTokenUsageLogExtensionFactory(compositeKey) {
16
+ return (pi) => {
17
+ const on = pi.on.bind(pi);
18
+ on("turn_start", (_event, ctx) => {
19
+ const c = ctx;
20
+ try {
21
+ const usage = c?.getContextUsage?.();
22
+ if (usage && typeof usage.tokens === "number") {
23
+ console.log(`${LOG_PREFIX} turn_start contextUsage.tokens=${usage.tokens} (估算,用于判断是否触发 compaction)`);
24
+ logContextBreakdown("turn_start", usage.tokens, compositeKey);
25
+ }
26
+ }
27
+ catch {
28
+ // ignore
29
+ }
30
+ });
31
+ on("turn_end", (_event, ctx) => {
32
+ const c = ctx;
33
+ try {
34
+ const usage = c?.getContextUsage?.();
35
+ if (usage && typeof usage.tokens === "number") {
36
+ console.log(`${LOG_PREFIX} turn_end contextUsage.tokens=${usage.tokens} (本轮结束后)`);
37
+ logContextBreakdown("turn_end", usage.tokens, compositeKey);
38
+ }
39
+ }
40
+ catch {
41
+ // ignore
42
+ }
43
+ });
44
+ on("session_compact", (event) => {
45
+ const e = event;
46
+ const entry = e?.compactionEntry;
47
+ const tokensBefore = entry?.tokensBefore;
48
+ const summaryLen = typeof entry?.summary === "string" ? entry.summary.length : 0;
49
+ console.log(`${LOG_PREFIX} session_compact 已触发 tokensBefore=${tokensBefore ?? "?"} summaryChars=${summaryLen}`);
50
+ });
51
+ on("auto_compaction_start", (event) => {
52
+ const e = event;
53
+ console.log(`${LOG_PREFIX} auto_compaction_start 已触发 reason=${String(e?.reason ?? "?")}`);
54
+ });
55
+ on("auto_compaction_end", (event) => {
56
+ const e = event;
57
+ const { result, aborted, willRetry, errorMessage } = e ?? {};
58
+ console.log(`${LOG_PREFIX} auto_compaction_end 已结束 aborted=${aborted} willRetry=${willRetry} result=${result != null ? "ok" : "null"}${errorMessage ? ` error=${errorMessage}` : ""}`);
59
+ });
60
+ };
61
+ }
@@ -56,8 +56,8 @@ export interface ChannelsConfig {
56
56
  export type DesktopMcpServerConfig = import("../mcp/index.js").McpServerConfig;
57
57
  /** MCP 标准 JSON 格式(key 为服务器名称),存储与 UI 可读写 */
58
58
  export type DesktopMcpServersStandardFormat = import("../mcp/index.js").McpServersStandardFormat;
59
- /** Agent 执行器类型:local=本机 pi-coding-agent,coze/openclawx/opencode=远程代理 */
60
- export type AgentRunnerType = "local" | "coze" | "openclawx" | "opencode";
59
+ /** Agent 执行器类型:local=本机 pi-coding-agent,coze/openclawx/opencode=远程代理,claude_code=本机 Claude Code CLI */
60
+ export type AgentRunnerType = "local" | "coze" | "openclawx" | "opencode" | "claude_code";
61
61
  /** Coze 站点:国内站 api.coze.cn / 国际站 api.coze.com,凭证不通用 */
62
62
  export type CozeRegion = "cn" | "com";
63
63
  /** 某站点的 Bot 凭证(国内/国际各自独立) */
@@ -90,6 +90,11 @@ export interface AgentOpenClawXConfig {
90
90
  }
91
91
  /** OpenCode 启动模式:local=由本应用按需启动本机服务;remote=连接已运行的远端服务 */
92
92
  export type OpenCodeServerMode = "local" | "remote";
93
+ /** Claude Code CLI 代理配置(当 runnerType 为 claude_code 时使用) */
94
+ export interface AgentClaudeCodeConfig {
95
+ /** 工作目录:Claude Code CLI 执行时的 cwd;留空则使用该智能体工作区路径 */
96
+ workingDirectory?: string;
97
+ }
93
98
  /** OpenCode 代理配置:仅对接 [OpenCode 官方 Server API](https://opencode.ai/docs/server)(Session/Message + HTTP Basic) */
94
99
  export interface AgentOpenCodeConfig {
95
100
  /** 启动模式:local=本应用控制启动并可选设置默认模型;remote=连接已有服务 */
@@ -143,6 +148,8 @@ export interface DesktopAgentConfig {
143
148
  workspace?: string;
144
149
  /** MCP 服务器配置(数组或标准对象格式),创建 Session 时传入并归一化 */
145
150
  mcpServers?: DesktopMcpServerConfig[] | DesktopMcpServersStandardFormat;
151
+ /** MCP 单次返回最大 token;超过则从尾部裁剪;不配置则不限制 */
152
+ mcpMaxResultTokens?: number;
146
153
  /** 自定义系统提示词,会与技能等一起组成最终 systemPrompt */
147
154
  systemPrompt?: string;
148
155
  /** 执行器类型,缺省 local */
@@ -153,8 +160,23 @@ export interface DesktopAgentConfig {
153
160
  openclawx?: AgentOpenClawXConfig;
154
161
  /** OpenCode 代理配置 */
155
162
  opencode?: AgentOpenCodeConfig;
163
+ /** Claude Code CLI 代理配置 */
164
+ claudeCode?: AgentClaudeCodeConfig;
156
165
  /** 是否使用经验(长记忆);默认 true */
157
166
  useLongMemory?: boolean;
167
+ /** 在线搜索:解析后的运行时配置,仅当 runnerType 为 local 时用于注册 web_search 工具 */
168
+ webSearch?: {
169
+ enabled: boolean;
170
+ provider: "brave" | "duck-duck-scrape";
171
+ apiKey?: string;
172
+ timeoutSeconds: number;
173
+ cacheTtlMinutes: number;
174
+ maxResults: number;
175
+ /** 单次搜索返回最大 token;超过则从尾部裁剪;不配置则不限制 */
176
+ maxResultTokens?: number;
177
+ };
178
+ /** 本地模型上下文长度(token 数),仅 runnerType 为 local 时用于启动本地 LLM;默认 32768 */
179
+ contextSize?: number;
158
180
  }
159
181
  /**
160
182
  * 从 config.json 读取缺省智能体 id(defaultAgentId)。
@@ -209,8 +209,8 @@ export async function loadDesktopAgentConfig(agentId) {
209
209
  }
210
210
  }
211
211
  const resolvedAgentId = agentId === "default" ? "default" : agentId;
212
- let provider = config.defaultProvider ?? "deepseek";
213
- let model = config.defaultModel ?? "deepseek-chat";
212
+ let provider = config.defaultProvider ?? "ollama";
213
+ let model = config.defaultModel ?? "qwen3:4b";
214
214
  if (config.defaultModelItemCode && Array.isArray(config.configuredModels)) {
215
215
  const configured = config.configuredModels.find((m) => m.modelItemCode === config.defaultModelItemCode);
216
216
  if (configured) {
@@ -220,8 +220,10 @@ export async function loadDesktopAgentConfig(agentId) {
220
220
  }
221
221
  let workspaceName = resolvedAgentId;
222
222
  let mcpServers;
223
+ let mcpMaxResultTokens;
223
224
  let systemPrompt;
224
225
  let useLongMemory = true;
226
+ let contextSize;
225
227
  if (existsSync(agentsPath)) {
226
228
  try {
227
229
  const raw = await readFile(agentsPath, "utf-8");
@@ -233,6 +235,12 @@ export async function loadDesktopAgentConfig(agentId) {
233
235
  workspaceName = agent.workspace;
234
236
  else if (agent.id)
235
237
  workspaceName = agent.id;
238
+ if (agent.mcpMaxResultTokens != null && typeof agent.mcpMaxResultTokens === "number" && agent.mcpMaxResultTokens > 0) {
239
+ mcpMaxResultTokens = agent.mcpMaxResultTokens;
240
+ }
241
+ if (agent.contextSize != null && typeof agent.contextSize === "number" && agent.contextSize > 0) {
242
+ contextSize = agent.contextSize;
243
+ }
236
244
  if (agent.mcpServers != null) {
237
245
  if (Array.isArray(agent.mcpServers) || (typeof agent.mcpServers === "object" && !Array.isArray(agent.mcpServers))) {
238
246
  mcpServers = agent.mcpServers;
@@ -276,6 +284,19 @@ export async function loadDesktopAgentConfig(agentId) {
276
284
  let coze;
277
285
  let openclawx;
278
286
  let opencode;
287
+ let claudeCode;
288
+ const tw = config.tools?.webSearch;
289
+ const timeoutSeconds = typeof tw?.timeoutSeconds === "number" && tw.timeoutSeconds > 0 ? tw.timeoutSeconds : 15;
290
+ const cacheTtlMinutes = typeof tw?.cacheTtlMinutes === "number" && tw.cacheTtlMinutes >= 0 ? tw.cacheTtlMinutes : 5;
291
+ const maxResultsRaw = typeof tw?.maxResults === "number" ? tw.maxResults : 5;
292
+ const maxResults = Math.min(10, Math.max(1, maxResultsRaw));
293
+ let webSearch = {
294
+ enabled: false,
295
+ provider: "duck-duck-scrape",
296
+ timeoutSeconds,
297
+ cacheTtlMinutes,
298
+ maxResults,
299
+ };
279
300
  if (existsSync(agentsPath)) {
280
301
  try {
281
302
  const rawAgents = await readFile(agentsPath, "utf-8");
@@ -285,9 +306,16 @@ export async function loadDesktopAgentConfig(agentId) {
285
306
  if (agentRow) {
286
307
  if (agentRow.runnerType === "coze" ||
287
308
  agentRow.runnerType === "openclawx" ||
288
- agentRow.runnerType === "opencode") {
309
+ agentRow.runnerType === "opencode" ||
310
+ agentRow.runnerType === "claude_code") {
289
311
  runnerType = agentRow.runnerType;
290
312
  }
313
+ if (agentRow.runnerType === "claude_code") {
314
+ const wd = agentRow.claudeCode?.workingDirectory;
315
+ claudeCode = {
316
+ workingDirectory: typeof wd === "string" && wd.trim() ? wd.trim() : undefined,
317
+ };
318
+ }
291
319
  if (agentRow.coze) {
292
320
  const row = agentRow.coze;
293
321
  const region = row.region === "cn" || row.region === "com" ? row.region : "com";
@@ -353,6 +381,35 @@ export async function loadDesktopAgentConfig(agentId) {
353
381
  }
354
382
  }
355
383
  }
384
+ if (agentRow.webSearch?.enabled === true) {
385
+ let preferredProvider = agentRow.webSearch?.provider === "brave" || agentRow.webSearch?.provider === "duck-duck-scrape"
386
+ ? agentRow.webSearch.provider
387
+ : tw?.defaultProvider === "brave" || tw?.defaultProvider === "duck-duck-scrape"
388
+ ? tw.defaultProvider
389
+ : "duck-duck-scrape";
390
+ let braveKey;
391
+ if (preferredProvider === "brave") {
392
+ braveKey =
393
+ (typeof tw?.providers?.brave?.apiKey === "string" && tw.providers.brave.apiKey.trim()
394
+ ? tw.providers.brave.apiKey.trim()
395
+ : undefined) ??
396
+ (process.env.BRAVE_API_KEY && process.env.BRAVE_API_KEY.trim() ? process.env.BRAVE_API_KEY.trim() : undefined);
397
+ if (!braveKey)
398
+ preferredProvider = "duck-duck-scrape";
399
+ }
400
+ const maxResultTokens = agentRow.webSearch?.maxResultTokens != null && typeof agentRow.webSearch?.maxResultTokens === "number" && agentRow.webSearch.maxResultTokens > 0
401
+ ? agentRow.webSearch.maxResultTokens
402
+ : undefined;
403
+ webSearch = {
404
+ enabled: true,
405
+ provider: preferredProvider,
406
+ apiKey: preferredProvider === "brave" ? braveKey : undefined,
407
+ timeoutSeconds,
408
+ cacheTtlMinutes,
409
+ maxResults,
410
+ maxResultTokens,
411
+ };
412
+ }
356
413
  }
357
414
  }
358
415
  catch {
@@ -365,12 +422,16 @@ export async function loadDesktopAgentConfig(agentId) {
365
422
  apiKey: apiKey ?? undefined,
366
423
  workspace: workspaceName,
367
424
  mcpServers,
425
+ mcpMaxResultTokens,
368
426
  systemPrompt,
369
427
  runnerType,
370
428
  coze,
371
429
  openclawx,
372
430
  opencode,
431
+ claudeCode,
373
432
  useLongMemory,
433
+ webSearch,
434
+ contextSize,
374
435
  };
375
436
  }
376
437
  function ensureDesktopDir() {
@@ -572,12 +633,22 @@ export async function ensureProviderSupportFile() {
572
633
  async function ensureConfigJsonInitialized() {
573
634
  const presetPath = join(getPresetsDir(), "preset-config.json");
574
635
  let presetConfig = {
575
- defaultProvider: "deepseek",
576
- defaultModel: "deepseek-chat",
636
+ defaultProvider: "ollama",
637
+ defaultModel: "qwen3:4b",
577
638
  defaultAgentId: DEFAULT_AGENT_ID,
578
639
  maxAgentSessions: DEFAULT_MAX_AGENT_SESSIONS,
579
- providers: {},
580
- configuredModels: [],
640
+ providers: {
641
+ ollama: { baseUrl: "http://localhost:11434/v1" },
642
+ },
643
+ configuredModels: [
644
+ {
645
+ provider: "ollama",
646
+ modelId: "qwen3:4b",
647
+ type: "llm",
648
+ alias: "Qwen3 4B (本地)",
649
+ modelItemCode: "ollama:qwen3:4b",
650
+ },
651
+ ],
581
652
  };
582
653
  if (existsSync(presetPath)) {
583
654
  try {
@@ -714,6 +785,10 @@ const SYNC_DEFAULTS = {
714
785
  "openai-custom": { baseUrl: "", apiKey: "OPENAI_API_KEY", api: "openai-completions" },
715
786
  nvidia: { baseUrl: "https://integrate.api.nvidia.com/v1", apiKey: "NVIDIA_API_KEY", api: "openai-completions" },
716
787
  kimi: { baseUrl: "https://api.moonshot.cn/v1", apiKey: "MOONSHOT_API_KEY", api: "openai-completions" },
788
+ /** 本地 Ollama,无需真实 API Key */
789
+ ollama: { baseUrl: "http://localhost:11434/v1", apiKey: "OPENAI_API_KEY", api: "openai-completions" },
790
+ /** 内置本地推理(node-llama-cpp),无需 API Key,baseUrl 指向本地子进程服务 */
791
+ local: { baseUrl: "http://127.0.0.1:11435/v1", apiKey: "OPENAI_API_KEY", api: "openai-completions" },
717
792
  };
718
793
  const DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
719
794
  const DEFAULT_CONTEXT_WINDOW = 64000;
@@ -750,10 +825,12 @@ export async function syncDesktopConfigToModelsJson() {
750
825
  const support = await getProviderSupport();
751
826
  const piProviders = {};
752
827
  for (const [providerId, userConfig] of Object.entries(configured)) {
753
- if (!userConfig?.apiKey?.trim())
828
+ // ollama / local 不需要 API Key,其他 provider 必须有 apiKey
829
+ const isNoKeyProvider = providerId === "ollama" || providerId === "local";
830
+ if (!isNoKeyProvider && !userConfig?.apiKey?.trim())
754
831
  continue;
755
832
  const defaults = SYNC_DEFAULTS[providerId] ?? { baseUrl: "", apiKey: "OPENAI_API_KEY", api: "openai-completions" };
756
- const baseUrl = userConfig.baseUrl?.trim() || (support[providerId]?.baseUrl ?? "").trim() || defaults.baseUrl;
833
+ const baseUrl = userConfig?.baseUrl?.trim() || (support[providerId]?.baseUrl ?? "").trim() || defaults.baseUrl;
757
834
  if (!baseUrl)
758
835
  continue;
759
836
  const def = support[providerId];
@@ -783,7 +860,7 @@ export async function syncDesktopConfigToModelsJson() {
783
860
  continue;
784
861
  }
785
862
  piProviders[providerId] = {
786
- name: (userConfig.alias?.trim() || def?.name) || providerId,
863
+ name: (userConfig?.alias?.trim() || def?.name) || providerId,
787
864
  apiKey: defaults.apiKey,
788
865
  api: defaults.api,
789
866
  baseUrl: baseUrl.replace(/\/$/, ""),
@@ -54,4 +54,30 @@ export const DEFAULT_PROVIDER_SUPPORT = {
54
54
  { id: "moonshot-v1-128k", name: "Moonshot 128K", types: ["llm"] },
55
55
  ],
56
56
  },
57
+ /** 本地 Ollama 服务,兼容 OpenAI API;baseUrl 指向本机 Ollama 默认端口 */
58
+ ollama: {
59
+ name: "Ollama (本地)",
60
+ baseUrl: "http://localhost:11434/v1",
61
+ models: [
62
+ { id: "qwen3:4b", name: "Qwen3 4B", types: ["llm"] },
63
+ { id: "qwen3:8b", name: "Qwen3 8B", types: ["llm"] },
64
+ { id: "qwen3:14b", name: "Qwen3 14B", types: ["llm"] },
65
+ { id: "llama3.2:3b", name: "Llama 3.2 3B", types: ["llm"] },
66
+ { id: "llama3.2:1b", name: "Llama 3.2 1B", types: ["llm"] },
67
+ { id: "nomic-embed-text", name: "Nomic Embed Text", types: ["embedding"] },
68
+ ],
69
+ },
70
+ /**
71
+ * 内置本地推理(node-llama-cpp),无需安装 Ollama。
72
+ * baseUrl 指向本地 LLM 子进程服务;模型列表为推荐的 GGUF 模型,可在本地模型管理页面增删。
73
+ * 无需 API Key。
74
+ */
75
+ local: {
76
+ name: "本地推理 (node-llama-cpp)",
77
+ baseUrl: "http://127.0.0.1:11435/v1",
78
+ models: [
79
+ { id: "local-llm", name: "本地 LLM(当前加载)", types: ["llm"] },
80
+ { id: "local-embedding", name: "本地 Embedding(当前加载)", types: ["embedding"] },
81
+ ],
82
+ },
57
83
  };
@@ -0,0 +1 @@
1
+ export { loadExtensionFactories, clearExtensionFactoriesCache } from "./load.js";
@@ -0,0 +1 @@
1
+ export { loadExtensionFactories, clearExtensionFactoriesCache } from "./load.js";
@@ -0,0 +1,11 @@
1
+ import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
2
+ /**
3
+ * 扫描 ~/.openbot/plugins,加载所有已安装的扩展包,返回 ExtensionFactory 数组。
4
+ * 进程内缓存结果;若需重载可调用 clearExtensionFactoriesCache()。
5
+ */
6
+ export declare function loadExtensionFactories(): ExtensionFactory[];
7
+ /**
8
+ * 清除扩展 factory 缓存,下次 loadExtensionFactories() 时会重新扫描并加载。
9
+ * 用于安装/卸载扩展后希望不重启即生效的场景(若调用方在适当时机调用)。
10
+ */
11
+ export declare function clearExtensionFactoriesCache(): void;