@next-open-ai/openbot 0.6.6 → 0.6.16
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/LICENSE +21 -0
- package/README.md +197 -138
- package/apps/desktop/renderer/dist/assets/index-BxqMW-uy.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-DJs-wX3R.js +89 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/core/agent/agent-manager.d.ts +14 -6
- package/dist/core/agent/agent-manager.js +63 -23
- package/dist/core/agent/proxy/adapters/coze-adapter.d.ts +2 -0
- package/dist/core/agent/proxy/adapters/coze-adapter.js +406 -0
- package/dist/core/agent/proxy/adapters/local-adapter.d.ts +2 -0
- package/dist/core/agent/proxy/adapters/local-adapter.js +93 -0
- package/dist/core/agent/proxy/adapters/openclawx-adapter.d.ts +2 -0
- package/dist/core/agent/proxy/adapters/openclawx-adapter.js +115 -0
- package/dist/core/agent/proxy/adapters/opencode-adapter.d.ts +11 -0
- package/dist/core/agent/proxy/adapters/opencode-adapter.js +750 -0
- package/dist/core/agent/proxy/adapters/opencode-free-models.d.ts +20 -0
- package/dist/core/agent/proxy/adapters/opencode-free-models.js +14 -0
- package/dist/core/agent/proxy/adapters/opencode-local-runner.d.ts +5 -0
- package/dist/core/agent/proxy/adapters/opencode-local-runner.js +86 -0
- package/dist/core/agent/proxy/index.d.ts +3 -0
- package/dist/core/agent/proxy/index.js +16 -0
- package/dist/core/agent/proxy/registry.d.ts +7 -0
- package/dist/core/agent/proxy/registry.js +13 -0
- package/dist/core/agent/proxy/run-for-channel.d.ts +3 -0
- package/dist/core/agent/proxy/run-for-channel.js +31 -0
- package/dist/core/agent/proxy/types.d.ts +30 -0
- package/dist/core/agent/proxy/types.js +1 -0
- package/dist/core/agent/run.js +1 -1
- package/dist/core/config/agent-reload-pending.d.ts +9 -0
- package/dist/core/config/agent-reload-pending.js +66 -0
- package/dist/core/config/desktop-config.d.ts +122 -3
- package/dist/core/config/desktop-config.js +289 -26
- package/dist/core/inbound-message-preprocess.d.ts +27 -0
- package/dist/core/inbound-message-preprocess.js +96 -0
- package/dist/core/mcp/client.d.ts +4 -0
- package/dist/core/mcp/client.js +2 -0
- package/dist/core/mcp/config.d.ts +14 -3
- package/dist/core/mcp/config.js +68 -3
- package/dist/core/mcp/index.d.ts +8 -6
- package/dist/core/mcp/index.js +6 -3
- package/dist/core/mcp/operator.d.ts +17 -2
- package/dist/core/mcp/operator.js +97 -30
- package/dist/core/mcp/transport/index.d.ts +4 -0
- package/dist/core/mcp/transport/index.js +6 -1
- package/dist/core/mcp/transport/stdio.d.ts +6 -0
- package/dist/core/mcp/transport/stdio.js +22 -1
- package/dist/core/mcp/types.d.ts +18 -0
- package/dist/core/memory/compaction-extension.d.ts +4 -3
- package/dist/core/memory/compaction-extension.js +6 -14
- package/dist/core/memory/embedding-types.d.ts +10 -0
- package/dist/core/memory/embedding-types.js +5 -0
- package/dist/core/memory/embedding.d.ts +2 -1
- package/dist/core/memory/embedding.js +38 -6
- package/dist/core/memory/index.js +3 -0
- package/dist/core/memory/local-embedding-llama.d.ts +13 -0
- package/dist/core/memory/local-embedding-llama.js +76 -0
- package/dist/core/memory/local-embedding.d.ts +10 -0
- package/dist/core/memory/local-embedding.js +29 -0
- package/dist/core/memory/persist-compaction-on-close.d.ts +14 -0
- package/dist/core/memory/persist-compaction-on-close.js +32 -0
- package/dist/core/session-outlet/index.d.ts +19 -0
- package/dist/core/session-outlet/index.js +33 -0
- package/dist/core/session-outlet/outlet.d.ts +15 -0
- package/dist/core/session-outlet/outlet.js +49 -0
- package/dist/core/session-outlet/types.d.ts +35 -0
- package/dist/core/session-outlet/types.js +5 -0
- package/dist/core/tools/bookmark-tool.d.ts +4 -0
- package/dist/core/tools/bookmark-tool.js +59 -3
- package/dist/core/tools/index.d.ts +2 -1
- package/dist/core/tools/index.js +2 -1
- package/dist/core/tools/memory-recall-tool.d.ts +6 -0
- package/dist/core/tools/memory-recall-tool.js +77 -0
- package/dist/gateway/channel/adapters/dingtalk.d.ts +11 -0
- package/dist/gateway/channel/adapters/dingtalk.js +190 -0
- package/dist/gateway/channel/adapters/telegram.d.ts +14 -0
- package/dist/gateway/channel/adapters/telegram.js +208 -0
- package/dist/gateway/channel/adapters/wechat.d.ts +24 -0
- package/dist/gateway/channel/adapters/wechat.js +205 -0
- package/dist/gateway/channel/channel-core.d.ts +1 -0
- package/dist/gateway/channel/channel-core.js +101 -51
- package/dist/gateway/channel/run-agent.d.ts +2 -4
- package/dist/gateway/channel/run-agent.js +13 -125
- package/dist/gateway/channel/types.d.ts +8 -4
- package/dist/gateway/methods/agent-cancel.d.ts +3 -1
- package/dist/gateway/methods/agent-cancel.js +16 -2
- package/dist/gateway/methods/agent-chat.d.ts +4 -0
- package/dist/gateway/methods/agent-chat.js +309 -118
- package/dist/gateway/methods/run-scheduled-task.js +7 -7
- package/dist/gateway/proxy-run-abort.d.ts +6 -0
- package/dist/gateway/proxy-run-abort.js +39 -0
- package/dist/gateway/server.js +103 -12
- package/dist/server/agent-config/agent-config.controller.d.ts +2 -2
- package/dist/server/agent-config/agent-config.controller.js +8 -4
- package/dist/server/agent-config/agent-config.module.js +3 -1
- package/dist/server/agent-config/agent-config.service.d.ts +67 -6
- package/dist/server/agent-config/agent-config.service.js +75 -3
- package/dist/server/agents/agents.controller.d.ts +16 -0
- package/dist/server/agents/agents.controller.js +62 -1
- package/dist/server/agents/agents.service.js +1 -1
- package/dist/server/bootstrap.js +9 -2
- package/dist/server/config/config.controller.d.ts +31 -2
- package/dist/server/config/config.controller.js +14 -0
- package/dist/server/config/config.module.js +2 -2
- package/dist/server/config/config.service.d.ts +14 -1
- package/dist/server/config/config.service.js +1 -0
- package/dist/server/workspace/workspace.service.d.ts +7 -0
- package/dist/server/workspace/workspace.service.js +16 -0
- package/package.json +9 -2
- package/presets/preset-agents.json +94 -0
- package/presets/preset-config.json +11 -0
- package/presets/preset-providers.json +173 -0
- package/presets/workspaces/code-assistant/skills/code-review/SKILL.md +19 -0
- package/presets/workspaces/code-assistant/skills/code-runner/SKILL.md +21 -0
- package/presets/workspaces/code-assistant/skills/git-helper/SKILL.md +29 -0
- package/presets/workspaces/creator-assistant/skills/.gitkeep +0 -0
- package/presets/workspaces/creator-assistant/skills/creator-tools/SKILL.md +15 -0
- package/presets/workspaces/doc-assistant/skills/doc-processor/SKILL.md +21 -0
- package/presets/workspaces/download-assistant/skills/downloader/SKILL.md +20 -0
- package/presets/workspaces/file-assistant/skills/file-converter/SKILL.md +21 -0
- package/presets/workspaces/file-assistant/skills/file-organizer/SKILL.md +17 -0
- package/presets/workspaces/file-assistant/skills/file-search/SKILL.md +22 -0
- package/presets/workspaces/morning-briefing/skills/news-fetcher/SKILL.md +16 -0
- package/presets/workspaces/morning-briefing/skills/web-summarizer/SKILL.md +20 -0
- package/presets/workspaces/news-assistant/skills/news-fetcher/SKILL.md +16 -0
- package/presets/workspaces/news-assistant/skills/web-summarizer/SKILL.md +20 -0
- package/apps/desktop/renderer/dist/assets/index-BNuvb6Ay.css +0 -10
- package/apps/desktop/renderer/dist/assets/index-DvQjslfT.js +0 -89
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<link
|
|
12
12
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap"
|
|
13
13
|
rel="stylesheet">
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DJs-wX3R.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BxqMW-uy.css">
|
|
16
16
|
</head>
|
|
17
17
|
|
|
18
18
|
<body>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { McpServerConfig } from "../mcp/index.js";
|
|
2
|
+
import type { McpServerConfig, McpServersStandardFormat } from "../mcp/index.js";
|
|
3
3
|
import type { Skill } from "./skills.js";
|
|
4
4
|
export interface AgentManagerOptions {
|
|
5
5
|
agentDir?: string;
|
|
@@ -14,6 +14,8 @@ export declare class AgentManager {
|
|
|
14
14
|
private sessions;
|
|
15
15
|
/** 每个 session 最后被使用的时间戳,用于 LRU 淘汰 */
|
|
16
16
|
private sessionLastActiveAt;
|
|
17
|
+
/** 每个 SessionAgent 当前最新的 compaction summary,关闭会话时写入向量库(infotype: compaction) */
|
|
18
|
+
private sessionLatestCompactionSummary;
|
|
17
19
|
private agentDir;
|
|
18
20
|
private workspaceDir;
|
|
19
21
|
private skillPaths;
|
|
@@ -57,16 +59,22 @@ export declare class AgentManager {
|
|
|
57
59
|
apiKey?: string;
|
|
58
60
|
maxSessions?: number;
|
|
59
61
|
targetAgentId?: string;
|
|
60
|
-
mcpServers?: McpServerConfig[];
|
|
62
|
+
mcpServers?: McpServerConfig[] | McpServersStandardFormat;
|
|
61
63
|
/** 自定义系统提示词(来自 agent 配置),会与技能等一起组成最终 systemPrompt */
|
|
62
64
|
systemPrompt?: string;
|
|
65
|
+
/** 是否使用长记忆(memory_recall/save_experience);默认 true */
|
|
66
|
+
useLongMemory?: boolean;
|
|
63
67
|
}): Promise<AgentSession>;
|
|
64
68
|
/** 按复合 key 获取(key = sessionId + "::" + agentId) */
|
|
65
69
|
getSession(compositeKey: string): AgentSession | undefined;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
+
/** 按业务 sessionId 查找一个 Session(取最近活跃的),用于 agent.cancel 等 */
|
|
71
|
+
getSessionBySessionId(sessionId: string): AgentSession | undefined;
|
|
72
|
+
/** 删除一个 Agent Session(传入复合 key);关闭前将本 session 最新 compaction summary 写入向量库 */
|
|
73
|
+
deleteSession(compositeKey: string): Promise<boolean>;
|
|
74
|
+
/** 按业务 sessionId 删除该会话下所有 agent 的 Core Session(如删除会话时);关闭前将各 session 最新 compaction 写入向量库 */
|
|
75
|
+
deleteSessionsByBusinessId(sessionId: string): Promise<void>;
|
|
76
|
+
/** 按 agentId 删除该智能体下所有 Session(配置更新后使旧会话失效,下次请求会用新配置建新会话) */
|
|
77
|
+
deleteSessionsByAgentId(agentId: string): Promise<void>;
|
|
70
78
|
clearAll(): void;
|
|
71
79
|
}
|
|
72
80
|
export declare const agentManager: AgentManager;
|
|
@@ -2,8 +2,9 @@ import { createAgentSession, AuthStorage, DefaultResourceLoader, ModelRegistry,
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { createCompactionMemoryExtensionFactory } from "../memory/compaction-extension.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { addMemory } from "../memory/index.js";
|
|
6
|
+
import { persistStoredCompactionForSession, persistStoredCompactionForBusinessSession, } from "../memory/persist-compaction-on-close.js";
|
|
7
|
+
import { createBrowserTool, createSaveExperienceTool, createMemoryRecallTool, createInstallSkillTool, createSwitchAgentTool, createListAgentsTool, createCreateAgentTool, createGetBookmarkTagsTool, createSaveBookmarkTool, createAddBookmarkTagTool } from "../tools/index.js";
|
|
7
8
|
/** Agent Session 缓存 key:sessionId + "::" + agentId,同一业务 session 下不同 agent 各自一个 Core Session */
|
|
8
9
|
const COMPOSITE_KEY_SEP = "::";
|
|
9
10
|
function toCompositeKey(sessionId, agentId) {
|
|
@@ -24,6 +25,8 @@ export class AgentManager {
|
|
|
24
25
|
sessions = new Map();
|
|
25
26
|
/** 每个 session 最后被使用的时间戳,用于 LRU 淘汰 */
|
|
26
27
|
sessionLastActiveAt = new Map();
|
|
28
|
+
/** 每个 SessionAgent 当前最新的 compaction summary,关闭会话时写入向量库(infotype: compaction) */
|
|
29
|
+
sessionLatestCompactionSummary = new Map();
|
|
27
30
|
agentDir;
|
|
28
31
|
workspaceDir;
|
|
29
32
|
skillPaths = [];
|
|
@@ -89,10 +92,18 @@ You have access to a \`browser\` tool for web automation:
|
|
|
89
92
|
|
|
90
93
|
Use refs from snapshots (e.g., @e1) for reliable element selection.
|
|
91
94
|
For downloads, provide either a direct URL or a selector to click.`;
|
|
95
|
+
const experienceMemoryDesc = `
|
|
96
|
+
## Long-term memory
|
|
97
|
+
|
|
98
|
+
- **save_experience**: Store summaries in long-term memory. **Call at most once per conversation**, and **only after all tasks are fully completed** (as the last step before ending your reply). **Do not call it for very simple dialogue tasks** (e.g. single-turn Q&A, greetings, trivial questions). When you do call it, briefly summarize the main outcomes, conclusions, or reusable points in one \`save_experience\` call.
|
|
99
|
+
- **memory_recall**: Retrieve relevant memories by semantic search. **When the user asks about past work, decisions, dates, people, preferences, todos, complex tasks, scheduled tasks, or anything that may need past experience**, call \`memory_recall\` first with a suitable query, then answer using the recalled content. Do not inject any history or memory into your replies unless you have just retrieved it via \`memory_recall\` for that question.`;
|
|
100
|
+
const terminologyNote = "【术语】本系统中「智能体」「助手」「专家」均指同一概念(Agent),可互换使用。用户说切换助手/专家或提到某个助手/专家时,即指切换或使用对应智能体。";
|
|
92
101
|
const parts = [
|
|
102
|
+
terminologyNote,
|
|
93
103
|
"You are a helpful assistant. When users ask about skills, explain what skills are available.",
|
|
94
104
|
browserToolDesc,
|
|
95
105
|
skillsBlock,
|
|
106
|
+
experienceMemoryDesc,
|
|
96
107
|
].filter(Boolean);
|
|
97
108
|
return parts.join("\n\n");
|
|
98
109
|
}
|
|
@@ -106,27 +117,25 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
106
117
|
const systemPrompt = this.buildSystemPrompt(loadedSkills);
|
|
107
118
|
return { systemPrompt, skills: loadedSkills };
|
|
108
119
|
}
|
|
109
|
-
createResourceLoader(workspaceDir, sessionId,
|
|
120
|
+
createResourceLoader(workspaceDir, sessionId, customAgentPrompt, identity, onUpdateLatestCompaction) {
|
|
110
121
|
const loader = new DefaultResourceLoader({
|
|
111
122
|
cwd: workspaceDir,
|
|
112
123
|
agentDir: this.agentDir,
|
|
113
124
|
noSkills: true, // Disable SDK's built-in skills logic to take full control
|
|
114
125
|
additionalSkillPaths: this.resolveSkillPaths(workspaceDir),
|
|
115
|
-
extensionFactories: sessionId
|
|
126
|
+
extensionFactories: sessionId && onUpdateLatestCompaction
|
|
127
|
+
? [createCompactionMemoryExtensionFactory(sessionId, onUpdateLatestCompaction)]
|
|
128
|
+
: [],
|
|
116
129
|
systemPromptOverride: (base) => {
|
|
117
130
|
const loadedSkills = loader.getSkills().skills;
|
|
118
|
-
|
|
131
|
+
const basePrompt = this.buildSystemPrompt(loadedSkills);
|
|
119
132
|
const withCustom = customAgentPrompt && customAgentPrompt.trim()
|
|
120
133
|
? customAgentPrompt.trim() + "\n\n" + basePrompt
|
|
121
134
|
: basePrompt;
|
|
122
|
-
|
|
135
|
+
return identity && identity.agentId
|
|
123
136
|
? `[Session identity] You are the agent with ID: ${identity.agentId}, workspace: ${identity.workspace || identity.agentId}. When asked which agent you are, answer according to this identity.\n\n` +
|
|
124
137
|
withCustom
|
|
125
138
|
: withCustom;
|
|
126
|
-
if (compactionBlock?.trim()) {
|
|
127
|
-
return withIdentity + "\n\n" + compactionBlock.trim();
|
|
128
|
-
}
|
|
129
|
-
return withIdentity;
|
|
130
139
|
},
|
|
131
140
|
});
|
|
132
141
|
return loader;
|
|
@@ -182,7 +191,7 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
193
|
if (oldestId != null) {
|
|
185
|
-
this.deleteSession(oldestId);
|
|
194
|
+
await this.deleteSession(oldestId);
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
const workspaceRoot = getOpenbotWorkspaceDir();
|
|
@@ -238,8 +247,7 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
238
247
|
return process.env.OPENAI_API_KEY;
|
|
239
248
|
return process.env.OPENAI_API_KEY;
|
|
240
249
|
});
|
|
241
|
-
const
|
|
242
|
-
const loader = this.createResourceLoader(sessionWorkspaceDir, sessionId, compactionBlock, options.systemPrompt, { agentId, workspace: workspaceName });
|
|
250
|
+
const loader = this.createResourceLoader(sessionWorkspaceDir, sessionId, options.systemPrompt, { agentId, workspace: workspaceName }, (summary) => this.sessionLatestCompactionSummary.set(compositeKey, summary));
|
|
243
251
|
await loader.reload();
|
|
244
252
|
const coreTools = {
|
|
245
253
|
read: createReadTool(sessionWorkspaceDir),
|
|
@@ -250,16 +258,22 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
250
258
|
grep: createGrepTool(sessionWorkspaceDir),
|
|
251
259
|
ls: createLsTool(sessionWorkspaceDir),
|
|
252
260
|
};
|
|
253
|
-
const
|
|
261
|
+
const useLongMemory = options.useLongMemory !== false;
|
|
262
|
+
const mcpTools = await createMcpToolsForSession({
|
|
263
|
+
mcpServers: options.mcpServers,
|
|
264
|
+
sessionId,
|
|
265
|
+
});
|
|
254
266
|
const customTools = [
|
|
255
267
|
createBrowserTool(sessionWorkspaceDir),
|
|
256
268
|
createSaveExperienceTool(sessionId),
|
|
269
|
+
createMemoryRecallTool(useLongMemory),
|
|
257
270
|
createInstallSkillTool(options.targetAgentId ?? agentId),
|
|
258
271
|
createSwitchAgentTool(sessionId),
|
|
259
272
|
createListAgentsTool(),
|
|
260
273
|
createCreateAgentTool(),
|
|
261
274
|
createGetBookmarkTagsTool(),
|
|
262
275
|
createSaveBookmarkTool(),
|
|
276
|
+
createAddBookmarkTagTool(),
|
|
263
277
|
...mcpTools,
|
|
264
278
|
];
|
|
265
279
|
const { session } = await createAgentSession({
|
|
@@ -285,24 +299,50 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
285
299
|
getSession(compositeKey) {
|
|
286
300
|
return this.sessions.get(compositeKey);
|
|
287
301
|
}
|
|
288
|
-
/**
|
|
289
|
-
|
|
302
|
+
/** 按业务 sessionId 查找一个 Session(取最近活跃的),用于 agent.cancel 等 */
|
|
303
|
+
getSessionBySessionId(sessionId) {
|
|
304
|
+
const prefix = sessionId + COMPOSITE_KEY_SEP;
|
|
305
|
+
let bestKey;
|
|
306
|
+
let bestAt = 0;
|
|
307
|
+
for (const key of this.sessions.keys()) {
|
|
308
|
+
if (!key.startsWith(prefix))
|
|
309
|
+
continue;
|
|
310
|
+
const at = this.sessionLastActiveAt.get(key) ?? 0;
|
|
311
|
+
if (at >= bestAt) {
|
|
312
|
+
bestAt = at;
|
|
313
|
+
bestKey = key;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return bestKey != null ? this.sessions.get(bestKey) : undefined;
|
|
317
|
+
}
|
|
318
|
+
/** 删除一个 Agent Session(传入复合 key);关闭前将本 session 最新 compaction summary 写入向量库 */
|
|
319
|
+
async deleteSession(compositeKey) {
|
|
320
|
+
await persistStoredCompactionForSession(this.sessionLatestCompactionSummary, compositeKey, addMemory);
|
|
290
321
|
this.sessionLastActiveAt.delete(compositeKey);
|
|
291
322
|
return this.sessions.delete(compositeKey);
|
|
292
323
|
}
|
|
293
|
-
/** 按业务 sessionId 删除该会话下所有 agent 的 Core Session
|
|
294
|
-
deleteSessionsByBusinessId(sessionId) {
|
|
324
|
+
/** 按业务 sessionId 删除该会话下所有 agent 的 Core Session(如删除会话时);关闭前将各 session 最新 compaction 写入向量库 */
|
|
325
|
+
async deleteSessionsByBusinessId(sessionId) {
|
|
295
326
|
const prefix = sessionId + COMPOSITE_KEY_SEP;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
327
|
+
const keysToProcess = Array.from(this.sessions.keys()).filter((k) => k.startsWith(prefix));
|
|
328
|
+
await persistStoredCompactionForBusinessSession(this.sessionLatestCompactionSummary, keysToProcess, sessionId, addMemory);
|
|
329
|
+
for (const key of keysToProcess) {
|
|
330
|
+
this.sessionLastActiveAt.delete(key);
|
|
331
|
+
this.sessions.delete(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** 按 agentId 删除该智能体下所有 Session(配置更新后使旧会话失效,下次请求会用新配置建新会话) */
|
|
335
|
+
async deleteSessionsByAgentId(agentId) {
|
|
336
|
+
const suffix = COMPOSITE_KEY_SEP + agentId;
|
|
337
|
+
const keysToProcess = Array.from(this.sessions.keys()).filter((k) => k.endsWith(suffix));
|
|
338
|
+
for (const key of keysToProcess) {
|
|
339
|
+
await this.deleteSession(key);
|
|
301
340
|
}
|
|
302
341
|
}
|
|
303
342
|
clearAll() {
|
|
304
343
|
this.sessions.clear();
|
|
305
344
|
this.sessionLastActiveAt.clear();
|
|
345
|
+
this.sessionLatestCompactionSummary.clear();
|
|
306
346
|
}
|
|
307
347
|
}
|
|
308
348
|
// Singleton for easy access (e.g., from Gateway)
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
const COZE_ENDPOINT_COM = "https://api.coze.com";
|
|
2
|
+
const COZE_ENDPOINT_CN = "https://api.coze.cn";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
4
|
+
function getCozeConfig(config) {
|
|
5
|
+
const coze = config.coze;
|
|
6
|
+
if (!coze?.botId?.trim() || !coze?.apiKey?.trim()) {
|
|
7
|
+
console.warn(`[Coze] getCozeConfig: missing coze config (hasCoze=${!!coze}, hasBotId=${!!coze?.botId?.trim()}, hasApiKey=${!!coze?.apiKey?.trim()})`);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const explicitEndpoint = coze.endpoint?.trim();
|
|
11
|
+
const region = coze.region === "cn" ? "cn" : "com";
|
|
12
|
+
const defaultBase = region === "cn" ? COZE_ENDPOINT_CN : COZE_ENDPOINT_COM;
|
|
13
|
+
const baseUrl = (explicitEndpoint || defaultBase).replace(/\/$/, "");
|
|
14
|
+
const apiKey = coze.apiKey.trim();
|
|
15
|
+
console.log(`[Coze] getCozeConfig: botId=${coze.botId.trim()}, region=${region}, baseUrl=${baseUrl}`);
|
|
16
|
+
return { botId: coze.botId.trim(), apiKey, baseUrl, region };
|
|
17
|
+
}
|
|
18
|
+
/** 国内站常见内部事件:不当作正文展示 */
|
|
19
|
+
const COZE_INTERNAL_MSG_TYPES = new Set([
|
|
20
|
+
"empty result",
|
|
21
|
+
"generate_answer_finish",
|
|
22
|
+
]);
|
|
23
|
+
/** 若字符串明显是内部协议 JSON(不应展示给用户),返回 true */
|
|
24
|
+
function looksLikeInternalJson(s) {
|
|
25
|
+
const t = s.trim();
|
|
26
|
+
if (t.startsWith("{")) {
|
|
27
|
+
if (t.includes('"msg_type"') || t.includes("from_module") || t.includes("generate_answer_finish"))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/** 从 Coze 流式 SSE 的 data 行 JSON 中解析出文本 delta(兼容国际站/国内站 v3) */
|
|
33
|
+
function parseCozeStreamLine(data) {
|
|
34
|
+
const trimmed = data.trim();
|
|
35
|
+
if (!trimmed || trimmed === "[DONE]" || trimmed === "[done]")
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(trimmed);
|
|
39
|
+
const msgType = parsed.msg_type;
|
|
40
|
+
if (msgType != null && COZE_INTERNAL_MSG_TYPES.has(String(msgType).toLowerCase()))
|
|
41
|
+
return null;
|
|
42
|
+
const d = parsed.data;
|
|
43
|
+
if (d && typeof d.content === "string") {
|
|
44
|
+
if (!looksLikeInternalJson(d.content))
|
|
45
|
+
return d.content;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (d && typeof d.delta === "string") {
|
|
49
|
+
const v = d.delta;
|
|
50
|
+
if (!looksLikeInternalJson(v))
|
|
51
|
+
return v;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (d && d.delta?.content != null) {
|
|
55
|
+
const v = String(d.delta.content);
|
|
56
|
+
if (!looksLikeInternalJson(v))
|
|
57
|
+
return v;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const dataMsg = d?.message;
|
|
61
|
+
if (dataMsg && typeof dataMsg.content === "string") {
|
|
62
|
+
if (!looksLikeInternalJson(dataMsg.content))
|
|
63
|
+
return dataMsg.content;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const msg = parsed.message;
|
|
67
|
+
if (msg && typeof msg.content === "string") {
|
|
68
|
+
if (!looksLikeInternalJson(msg.content))
|
|
69
|
+
return msg.content;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof parsed.content === "string") {
|
|
73
|
+
const v = parsed.content;
|
|
74
|
+
if (!looksLikeInternalJson(v))
|
|
75
|
+
return v;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (typeof parsed.delta === "string") {
|
|
79
|
+
const v = parsed.delta;
|
|
80
|
+
if (!looksLikeInternalJson(v))
|
|
81
|
+
return v;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (parsed.delta?.content != null) {
|
|
85
|
+
const v = String(parsed.delta.content);
|
|
86
|
+
if (!looksLikeInternalJson(v))
|
|
87
|
+
return v;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.type === "answer" && typeof parsed.content === "string") {
|
|
91
|
+
const v = parsed.content;
|
|
92
|
+
if (!looksLikeInternalJson(v))
|
|
93
|
+
return v;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (typeof parsed.answer === "string") {
|
|
97
|
+
const v = parsed.answer;
|
|
98
|
+
if (!looksLikeInternalJson(v))
|
|
99
|
+
return v;
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (typeof parsed.text === "string") {
|
|
103
|
+
const v = parsed.text;
|
|
104
|
+
if (!looksLikeInternalJson(v))
|
|
105
|
+
return v;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (d && typeof d.text === "string") {
|
|
109
|
+
const v = d.text;
|
|
110
|
+
if (!looksLikeInternalJson(v))
|
|
111
|
+
return v;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const parts = (msg ?? dataMsg)?.parts;
|
|
115
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
116
|
+
const text = parts.map((p) => (p?.content != null ? String(p.content) : "")).join("");
|
|
117
|
+
if (text && !looksLikeInternalJson(text))
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Coze 业务错误码:code !== 0 时抛出,提示用户检查配置(如 API Key) */
|
|
127
|
+
function throwIfCozeError(data) {
|
|
128
|
+
const code = data.code;
|
|
129
|
+
if (code === undefined || code === 0)
|
|
130
|
+
return;
|
|
131
|
+
const msg = data.msg || "Unknown error";
|
|
132
|
+
if (code === 4101) {
|
|
133
|
+
throw new Error("Coze 返回 4101:当前填写的 API Key 被 Coze 拒绝(已传递,但认证失败)。请检查:1) 必须使用「个人访问令牌」Personal Access Token,在 Coze 开放平台「个人中心 - API 密钥」创建,不要用 Bot ID 或其它 token;2) 国际站 (api.coze.com) 与国内站 (api.coze.cn) 的 Key 不通用,Endpoint 需与 Key 所属站点一致;3) Key 可能已过期或被撤销,请重新生成并保存。参考:https://www.coze.com/docs/developer_guides/authentication");
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Coze API 错误 (code=${code}): ${msg}`);
|
|
136
|
+
}
|
|
137
|
+
/** 从 Coze 非流式 JSON 响应中解析出助手回复文本 */
|
|
138
|
+
function parseCozeCollectResponse(data) {
|
|
139
|
+
const dataObj = data.data;
|
|
140
|
+
if (dataObj) {
|
|
141
|
+
const msg = dataObj.message;
|
|
142
|
+
if (msg && typeof msg.content === "string")
|
|
143
|
+
return msg.content.trim();
|
|
144
|
+
if (typeof dataObj.content === "string")
|
|
145
|
+
return dataObj.content.trim();
|
|
146
|
+
const messages = dataObj.messages;
|
|
147
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
148
|
+
const last = messages[messages.length - 1];
|
|
149
|
+
if (last && typeof last.content === "string")
|
|
150
|
+
return last.content.trim();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const msg = data.message;
|
|
154
|
+
if (msg && typeof msg.content === "string")
|
|
155
|
+
return msg.content.trim();
|
|
156
|
+
if (typeof data.content === "string")
|
|
157
|
+
return data.content.trim();
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
export const cozeAdapter = {
|
|
161
|
+
type: "coze",
|
|
162
|
+
async runStream(options, config, callbacks) {
|
|
163
|
+
const cozeConfig = getCozeConfig(config);
|
|
164
|
+
if (!cozeConfig) {
|
|
165
|
+
throw new Error("Coze adapter: missing coze.botId or coze.apiKey in agent config");
|
|
166
|
+
}
|
|
167
|
+
const { sessionId, message } = options;
|
|
168
|
+
const url = `${cozeConfig.baseUrl}/v3/chat?bot_id=${encodeURIComponent(cozeConfig.botId)}`;
|
|
169
|
+
const body = {
|
|
170
|
+
conversation_id: sessionId,
|
|
171
|
+
bot_id: cozeConfig.botId,
|
|
172
|
+
user_id: `channel:${sessionId}`,
|
|
173
|
+
stream: true,
|
|
174
|
+
auto_save_history: true,
|
|
175
|
+
additional_messages: [{ role: "user", content: message, content_type: "text" }],
|
|
176
|
+
};
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
179
|
+
const userSignal = options.signal;
|
|
180
|
+
if (userSignal) {
|
|
181
|
+
if (userSignal.aborted)
|
|
182
|
+
controller.abort();
|
|
183
|
+
else
|
|
184
|
+
userSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
console.log(`[Coze] POST ${url} (stream=true)`);
|
|
188
|
+
const res = await fetch(url, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${cozeConfig.apiKey}`,
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(body),
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
});
|
|
197
|
+
console.log(`[Coze] Response status=${res.status} content-type=${res.headers.get("content-type") ?? "(none)"}`);
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
const text = await res.text();
|
|
200
|
+
throw new Error(`Coze API error ${res.status}: ${text}`);
|
|
201
|
+
}
|
|
202
|
+
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
|
203
|
+
// Coze 在 token 错误等情况下仍返回 200 + application/json,需先按 JSON 解析并检查 code
|
|
204
|
+
if (contentType.includes("application/json")) {
|
|
205
|
+
const text = await res.text();
|
|
206
|
+
try {
|
|
207
|
+
const data = JSON.parse(text);
|
|
208
|
+
throwIfCozeError(data);
|
|
209
|
+
const fullText = parseCozeCollectResponse(data);
|
|
210
|
+
if (fullText)
|
|
211
|
+
callbacks.onChunk(fullText);
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
if (e?.message?.startsWith("Coze API") || e?.message?.startsWith("Coze API Key"))
|
|
215
|
+
throw e;
|
|
216
|
+
throw new Error(`Coze 返回了 JSON 但解析失败: ${e?.message ?? e}`);
|
|
217
|
+
}
|
|
218
|
+
callbacks.onTurnEnd?.();
|
|
219
|
+
callbacks.onDone();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const reader = res.body?.getReader();
|
|
223
|
+
if (!reader) {
|
|
224
|
+
console.log(`[Coze] No stream reader, reading body as text`);
|
|
225
|
+
const text = await res.text();
|
|
226
|
+
if (text)
|
|
227
|
+
callbacks.onChunk(text);
|
|
228
|
+
callbacks.onTurnEnd?.();
|
|
229
|
+
callbacks.onDone();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const decoder = new TextDecoder();
|
|
233
|
+
let buffer = "";
|
|
234
|
+
let hadAnyChunk = false;
|
|
235
|
+
const rawDataSamples = [];
|
|
236
|
+
let lastEvent = "";
|
|
237
|
+
let streamAccumulated = "";
|
|
238
|
+
while (true) {
|
|
239
|
+
const { done, value } = await reader.read();
|
|
240
|
+
if (done)
|
|
241
|
+
break;
|
|
242
|
+
buffer += decoder.decode(value, { stream: true });
|
|
243
|
+
const lines = buffer.replace(/\r\n/g, "\n").split("\n");
|
|
244
|
+
buffer = lines.pop() || "";
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
const trimmedLine = line.trim();
|
|
247
|
+
if (trimmedLine.startsWith("event:")) {
|
|
248
|
+
lastEvent = trimmedLine.slice(6).trim();
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!trimmedLine.startsWith("data:"))
|
|
252
|
+
continue;
|
|
253
|
+
const raw = trimmedLine.slice(5).trim();
|
|
254
|
+
if (!raw)
|
|
255
|
+
continue;
|
|
256
|
+
if (rawDataSamples.length < 5)
|
|
257
|
+
rawDataSamples.push(lastEvent + " -> " + raw.slice(0, 150));
|
|
258
|
+
const e = lastEvent.toLowerCase();
|
|
259
|
+
const isMessageEvent = e.includes("message") || e.includes("delta") || e.includes("answer") || e.includes("chunk") || lastEvent === "";
|
|
260
|
+
const content = isMessageEvent ? parseCozeStreamLine(raw) : null;
|
|
261
|
+
if (content) {
|
|
262
|
+
if (content === streamAccumulated)
|
|
263
|
+
continue;
|
|
264
|
+
if (streamAccumulated.length > 0 && streamAccumulated.startsWith(content))
|
|
265
|
+
continue;
|
|
266
|
+
if (streamAccumulated.length > 0 && content.length >= 10 && streamAccumulated.endsWith(content))
|
|
267
|
+
continue;
|
|
268
|
+
if (content.startsWith(streamAccumulated)) {
|
|
269
|
+
const suffix = content.slice(streamAccumulated.length);
|
|
270
|
+
if (suffix) {
|
|
271
|
+
streamAccumulated = content;
|
|
272
|
+
hadAnyChunk = true;
|
|
273
|
+
callbacks.onChunk(suffix);
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
streamAccumulated += content;
|
|
278
|
+
hadAnyChunk = true;
|
|
279
|
+
callbacks.onChunk(content);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 若 SSE 流中没有任何可解析的 content,尝试:1) buffer 为 data: 行;2) buffer 为整段 JSON(非流式返回)
|
|
284
|
+
if (!hadAnyChunk && buffer.trim()) {
|
|
285
|
+
const dataPart = buffer.startsWith("data: ") ? buffer.slice(6) : buffer;
|
|
286
|
+
const content = parseCozeStreamLine(dataPart);
|
|
287
|
+
if (content) {
|
|
288
|
+
callbacks.onChunk(content);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
const asJson = JSON.parse(buffer.trim());
|
|
293
|
+
const fullText = parseCozeCollectResponse(asJson);
|
|
294
|
+
if (fullText)
|
|
295
|
+
callbacks.onChunk(fullText);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// ignore
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!hadAnyChunk) {
|
|
303
|
+
const sample = buffer.trim().slice(0, 300);
|
|
304
|
+
console.log(`[Coze] Stream ended with no parsed chunks. Buffer sample: ${sample || "(empty)"}`);
|
|
305
|
+
if (rawDataSamples.length > 0) {
|
|
306
|
+
console.log(`[Coze] First SSE data lines sample: ${rawDataSamples.join(" | ")}`);
|
|
307
|
+
}
|
|
308
|
+
// 可能是 Coze 返回的 JSON 错误体(如 4101 token 无效),先检查再决定是否回退
|
|
309
|
+
try {
|
|
310
|
+
const asJson = JSON.parse(buffer.trim());
|
|
311
|
+
throwIfCozeError(asJson);
|
|
312
|
+
const fullText = parseCozeCollectResponse(asJson);
|
|
313
|
+
if (fullText)
|
|
314
|
+
callbacks.onChunk(fullText);
|
|
315
|
+
}
|
|
316
|
+
catch (parseErr) {
|
|
317
|
+
if (parseErr?.message?.startsWith("Coze API") || parseErr?.message?.startsWith("Coze API Key"))
|
|
318
|
+
throw parseErr;
|
|
319
|
+
// 流式未解析到内容,回退到非流式接口
|
|
320
|
+
try {
|
|
321
|
+
console.log(`[Coze] Stream had no parsed chunks, trying runCollect fallback`);
|
|
322
|
+
const fallbackText = await this.runCollect(options, config);
|
|
323
|
+
if (fallbackText) {
|
|
324
|
+
callbacks.onChunk(fallbackText);
|
|
325
|
+
console.log(`[Coze] Fallback runCollect returned ${fallbackText.length} chars`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (collectErr) {
|
|
329
|
+
if (collectErr?.message?.startsWith("Coze API") || collectErr?.message?.startsWith("Coze API Key"))
|
|
330
|
+
throw collectErr;
|
|
331
|
+
console.warn(`[Coze] Fallback runCollect failed:`, collectErr?.message ?? collectErr);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
callbacks.onTurnEnd?.();
|
|
336
|
+
callbacks.onDone();
|
|
337
|
+
console.log(`[Coze] runStream finished`);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
console.error(`[Coze] runStream error:`, err?.message ?? err);
|
|
341
|
+
// 请求超时或网络错误时,尝试非流式接口兜底
|
|
342
|
+
try {
|
|
343
|
+
const fallbackText = await this.runCollect(options, config);
|
|
344
|
+
if (fallbackText) {
|
|
345
|
+
callbacks.onChunk(fallbackText);
|
|
346
|
+
callbacks.onTurnEnd?.();
|
|
347
|
+
callbacks.onDone();
|
|
348
|
+
console.log(`[Coze] runStream recovered via runCollect`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (_) {
|
|
353
|
+
// ignore
|
|
354
|
+
}
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
clearTimeout(timeoutId);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
async runCollect(options, config) {
|
|
362
|
+
const cozeConfig = getCozeConfig(config);
|
|
363
|
+
if (!cozeConfig) {
|
|
364
|
+
throw new Error("Coze adapter: missing coze.botId or coze.apiKey in agent config");
|
|
365
|
+
}
|
|
366
|
+
const { sessionId, message } = options;
|
|
367
|
+
const url = `${cozeConfig.baseUrl}/v3/chat?bot_id=${encodeURIComponent(cozeConfig.botId)}`;
|
|
368
|
+
const body = {
|
|
369
|
+
conversation_id: sessionId,
|
|
370
|
+
bot_id: cozeConfig.botId,
|
|
371
|
+
user_id: `channel:${sessionId}`,
|
|
372
|
+
stream: false,
|
|
373
|
+
auto_save_history: true,
|
|
374
|
+
additional_messages: [{ role: "user", content: message, content_type: "text" }],
|
|
375
|
+
};
|
|
376
|
+
const controller = new AbortController();
|
|
377
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
378
|
+
try {
|
|
379
|
+
const res = await fetch(url, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
Authorization: `Bearer ${cozeConfig.apiKey}`,
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify(body),
|
|
386
|
+
signal: controller.signal,
|
|
387
|
+
});
|
|
388
|
+
const raw = await res.text();
|
|
389
|
+
if (!res.ok)
|
|
390
|
+
throw new Error(`Coze API error ${res.status}: ${raw}`);
|
|
391
|
+
let data;
|
|
392
|
+
try {
|
|
393
|
+
data = JSON.parse(raw);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return "(无文本回复)";
|
|
397
|
+
}
|
|
398
|
+
throwIfCozeError(data);
|
|
399
|
+
const content = parseCozeCollectResponse(data);
|
|
400
|
+
return content || "(无文本回复)";
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
clearTimeout(timeoutId);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
};
|