@next-open-ai/openclawx 0.8.36 → 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 +50 -42
- 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 +68 -5
- 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/index.d.ts +2 -0
- package/dist/core/mcp/index.js +1 -0
- package/dist/core/mcp/operator.d.ts +2 -0
- package/dist/core/mcp/operator.js +1 -1
- 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/methods/agent-chat.js +3 -1
- package/dist/gateway/methods/run-scheduled-task.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
|
@@ -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 =
|
|
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
|
|
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
|
-
//
|
|
542
|
-
const
|
|
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;
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
lastTextLength += delta.length;
|
|
614
|
+
textAccumulated += delta;
|
|
615
|
+
emitStrippedText();
|
|
570
616
|
}
|
|
571
617
|
else if (fullText.length > lastTextLength) {
|
|
572
|
-
|
|
573
|
-
callbacks.onChunk(fullText.slice(lastTextLength));
|
|
618
|
+
textAccumulated = fullText;
|
|
574
619
|
lastTextLength = fullText.length;
|
|
620
|
+
emitStrippedText();
|
|
575
621
|
}
|
|
576
622
|
}
|
|
577
|
-
else {
|
|
578
|
-
|
|
579
|
-
|
|
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
|
|
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,21 @@ 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
|
+
};
|
|
158
178
|
}
|
|
159
179
|
/**
|
|
160
180
|
* 从 config.json 读取缺省智能体 id(defaultAgentId)。
|
|
@@ -220,6 +220,7 @@ 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;
|
|
225
226
|
if (existsSync(agentsPath)) {
|
|
@@ -233,6 +234,9 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
233
234
|
workspaceName = agent.workspace;
|
|
234
235
|
else if (agent.id)
|
|
235
236
|
workspaceName = agent.id;
|
|
237
|
+
if (agent.mcpMaxResultTokens != null && typeof agent.mcpMaxResultTokens === "number" && agent.mcpMaxResultTokens > 0) {
|
|
238
|
+
mcpMaxResultTokens = agent.mcpMaxResultTokens;
|
|
239
|
+
}
|
|
236
240
|
if (agent.mcpServers != null) {
|
|
237
241
|
if (Array.isArray(agent.mcpServers) || (typeof agent.mcpServers === "object" && !Array.isArray(agent.mcpServers))) {
|
|
238
242
|
mcpServers = agent.mcpServers;
|
|
@@ -276,6 +280,19 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
276
280
|
let coze;
|
|
277
281
|
let openclawx;
|
|
278
282
|
let opencode;
|
|
283
|
+
let claudeCode;
|
|
284
|
+
const tw = config.tools?.webSearch;
|
|
285
|
+
const timeoutSeconds = typeof tw?.timeoutSeconds === "number" && tw.timeoutSeconds > 0 ? tw.timeoutSeconds : 15;
|
|
286
|
+
const cacheTtlMinutes = typeof tw?.cacheTtlMinutes === "number" && tw.cacheTtlMinutes >= 0 ? tw.cacheTtlMinutes : 5;
|
|
287
|
+
const maxResultsRaw = typeof tw?.maxResults === "number" ? tw.maxResults : 5;
|
|
288
|
+
const maxResults = Math.min(10, Math.max(1, maxResultsRaw));
|
|
289
|
+
let webSearch = {
|
|
290
|
+
enabled: false,
|
|
291
|
+
provider: "duck-duck-scrape",
|
|
292
|
+
timeoutSeconds,
|
|
293
|
+
cacheTtlMinutes,
|
|
294
|
+
maxResults,
|
|
295
|
+
};
|
|
279
296
|
if (existsSync(agentsPath)) {
|
|
280
297
|
try {
|
|
281
298
|
const rawAgents = await readFile(agentsPath, "utf-8");
|
|
@@ -285,9 +302,16 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
285
302
|
if (agentRow) {
|
|
286
303
|
if (agentRow.runnerType === "coze" ||
|
|
287
304
|
agentRow.runnerType === "openclawx" ||
|
|
288
|
-
agentRow.runnerType === "opencode"
|
|
305
|
+
agentRow.runnerType === "opencode" ||
|
|
306
|
+
agentRow.runnerType === "claude_code") {
|
|
289
307
|
runnerType = agentRow.runnerType;
|
|
290
308
|
}
|
|
309
|
+
if (agentRow.runnerType === "claude_code") {
|
|
310
|
+
const wd = agentRow.claudeCode?.workingDirectory;
|
|
311
|
+
claudeCode = {
|
|
312
|
+
workingDirectory: typeof wd === "string" && wd.trim() ? wd.trim() : undefined,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
291
315
|
if (agentRow.coze) {
|
|
292
316
|
const row = agentRow.coze;
|
|
293
317
|
const region = row.region === "cn" || row.region === "com" ? row.region : "com";
|
|
@@ -353,6 +377,35 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
353
377
|
}
|
|
354
378
|
}
|
|
355
379
|
}
|
|
380
|
+
if (agentRow.webSearch?.enabled === true) {
|
|
381
|
+
let preferredProvider = agentRow.webSearch?.provider === "brave" || agentRow.webSearch?.provider === "duck-duck-scrape"
|
|
382
|
+
? agentRow.webSearch.provider
|
|
383
|
+
: tw?.defaultProvider === "brave" || tw?.defaultProvider === "duck-duck-scrape"
|
|
384
|
+
? tw.defaultProvider
|
|
385
|
+
: "duck-duck-scrape";
|
|
386
|
+
let braveKey;
|
|
387
|
+
if (preferredProvider === "brave") {
|
|
388
|
+
braveKey =
|
|
389
|
+
(typeof tw?.providers?.brave?.apiKey === "string" && tw.providers.brave.apiKey.trim()
|
|
390
|
+
? tw.providers.brave.apiKey.trim()
|
|
391
|
+
: undefined) ??
|
|
392
|
+
(process.env.BRAVE_API_KEY && process.env.BRAVE_API_KEY.trim() ? process.env.BRAVE_API_KEY.trim() : undefined);
|
|
393
|
+
if (!braveKey)
|
|
394
|
+
preferredProvider = "duck-duck-scrape";
|
|
395
|
+
}
|
|
396
|
+
const maxResultTokens = agentRow.webSearch?.maxResultTokens != null && typeof agentRow.webSearch?.maxResultTokens === "number" && agentRow.webSearch.maxResultTokens > 0
|
|
397
|
+
? agentRow.webSearch.maxResultTokens
|
|
398
|
+
: undefined;
|
|
399
|
+
webSearch = {
|
|
400
|
+
enabled: true,
|
|
401
|
+
provider: preferredProvider,
|
|
402
|
+
apiKey: preferredProvider === "brave" ? braveKey : undefined,
|
|
403
|
+
timeoutSeconds,
|
|
404
|
+
cacheTtlMinutes,
|
|
405
|
+
maxResults,
|
|
406
|
+
maxResultTokens,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
356
409
|
}
|
|
357
410
|
}
|
|
358
411
|
catch {
|
|
@@ -365,12 +418,15 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
365
418
|
apiKey: apiKey ?? undefined,
|
|
366
419
|
workspace: workspaceName,
|
|
367
420
|
mcpServers,
|
|
421
|
+
mcpMaxResultTokens,
|
|
368
422
|
systemPrompt,
|
|
369
423
|
runnerType,
|
|
370
424
|
coze,
|
|
371
425
|
openclawx,
|
|
372
426
|
opencode,
|
|
427
|
+
claudeCode,
|
|
373
428
|
useLongMemory,
|
|
429
|
+
webSearch,
|
|
374
430
|
};
|
|
375
431
|
}
|
|
376
432
|
function ensureDesktopDir() {
|
|
@@ -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;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 ~/.openbot/plugins 目录加载通过 openbot extension install 安装的 npm 包,
|
|
3
|
+
* 将每个包的默认导出规范为 ExtensionFactory 并返回,供 AgentManager 注入到 DefaultResourceLoader.extensionFactories。
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getOpenbotPluginsDir } from "../agent/agent-dir.js";
|
|
9
|
+
let cachedFactories = null;
|
|
10
|
+
/**
|
|
11
|
+
* 从插件目录的 package.json 读取 dependencies(及 optionalDependencies)的包名列表。
|
|
12
|
+
* 仅返回在 node_modules 中实际存在的包名。
|
|
13
|
+
*/
|
|
14
|
+
function getInstalledPluginNames(pluginsDir) {
|
|
15
|
+
const pkgPath = join(pluginsDir, "package.json");
|
|
16
|
+
if (!existsSync(pkgPath))
|
|
17
|
+
return [];
|
|
18
|
+
let pkg;
|
|
19
|
+
try {
|
|
20
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const deps = {
|
|
26
|
+
...pkg.dependencies,
|
|
27
|
+
...pkg.optionalDependencies,
|
|
28
|
+
};
|
|
29
|
+
const names = Object.keys(deps || {});
|
|
30
|
+
return names.filter((name) => {
|
|
31
|
+
const dir = join(pluginsDir, "node_modules", name);
|
|
32
|
+
return existsSync(dir);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 将包默认导出规范为 ExtensionFactory:(pi) => void。
|
|
37
|
+
* 插件可导出 (pi) => void 或 () => (pi) => void(工厂),此处统一为 (pi) => void。
|
|
38
|
+
*/
|
|
39
|
+
function toExtensionFactory(fn) {
|
|
40
|
+
if (typeof fn !== "function")
|
|
41
|
+
return null;
|
|
42
|
+
if (fn.length === 1)
|
|
43
|
+
return fn; // (pi) => void
|
|
44
|
+
if (fn.length === 0) {
|
|
45
|
+
const result = fn();
|
|
46
|
+
if (typeof result === "function")
|
|
47
|
+
return result; // () => (pi) => void
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 加载单个插件包,返回 ExtensionFactory 或 null(失败时打日志并返回 null)。
|
|
53
|
+
* 使用 require(pkgName) 从 plugins 目录的 node_modules 解析,以便插件自身依赖正确解析。
|
|
54
|
+
*/
|
|
55
|
+
function loadOnePlugin(pluginsDir, pkgName) {
|
|
56
|
+
const require = createRequire(join(pluginsDir, "package.json"));
|
|
57
|
+
let mod;
|
|
58
|
+
try {
|
|
59
|
+
mod = require(pkgName);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.warn(`[extensions] Failed to load plugin "${pkgName}":`, err);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const def = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
66
|
+
const factory = toExtensionFactory(def);
|
|
67
|
+
if (!factory) {
|
|
68
|
+
console.warn(`[extensions] Plugin "${pkgName}" default export is not a function; skipped.`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return factory;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 扫描 ~/.openbot/plugins,加载所有已安装的扩展包,返回 ExtensionFactory 数组。
|
|
75
|
+
* 进程内缓存结果;若需重载可调用 clearExtensionFactoriesCache()。
|
|
76
|
+
*/
|
|
77
|
+
export function loadExtensionFactories() {
|
|
78
|
+
if (cachedFactories !== null)
|
|
79
|
+
return cachedFactories;
|
|
80
|
+
const pluginsDir = getOpenbotPluginsDir();
|
|
81
|
+
if (!existsSync(pluginsDir)) {
|
|
82
|
+
cachedFactories = [];
|
|
83
|
+
return cachedFactories;
|
|
84
|
+
}
|
|
85
|
+
const names = getInstalledPluginNames(pluginsDir);
|
|
86
|
+
const factories = [];
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const factory = loadOnePlugin(pluginsDir, name);
|
|
89
|
+
if (factory)
|
|
90
|
+
factories.push(factory);
|
|
91
|
+
}
|
|
92
|
+
cachedFactories = factories;
|
|
93
|
+
return factories;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 清除扩展 factory 缓存,下次 loadExtensionFactories() 时会重新扫描并加载。
|
|
97
|
+
* 用于安装/卸载扩展后希望不重启即生效的场景(若调用方在适当时机调用)。
|
|
98
|
+
*/
|
|
99
|
+
export function clearExtensionFactoriesCache() {
|
|
100
|
+
cachedFactories = null;
|
|
101
|
+
}
|
|
@@ -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[];
|
package/dist/core/mcp/adapter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/core/mcp/index.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/core/mcp/index.js
CHANGED
|
@@ -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 数组。
|