@next-open-ai/openbot 0.1.1
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 +212 -0
- package/dist/agent/agent-dir.d.ts +14 -0
- package/dist/agent/agent-dir.js +75 -0
- package/dist/agent/agent-manager.d.ts +61 -0
- package/dist/agent/agent-manager.js +257 -0
- package/dist/agent/config-manager.d.ts +25 -0
- package/dist/agent/config-manager.js +84 -0
- package/dist/agent/desktop-config.d.ts +15 -0
- package/dist/agent/desktop-config.js +91 -0
- package/dist/agent/run.d.ts +26 -0
- package/dist/agent/run.js +65 -0
- package/dist/agent/skills.d.ts +20 -0
- package/dist/agent/skills.js +86 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +168 -0
- package/dist/gateway/backend-url.d.ts +2 -0
- package/dist/gateway/backend-url.js +11 -0
- package/dist/gateway/clients.d.ts +5 -0
- package/dist/gateway/clients.js +4 -0
- package/dist/gateway/connection-handler.d.ts +6 -0
- package/dist/gateway/connection-handler.js +48 -0
- package/dist/gateway/desktop-config.d.ts +7 -0
- package/dist/gateway/desktop-config.js +25 -0
- package/dist/gateway/index.d.ts +3 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/message-handler.d.ts +5 -0
- package/dist/gateway/message-handler.js +65 -0
- package/dist/gateway/methods/agent-cancel.d.ts +10 -0
- package/dist/gateway/methods/agent-cancel.js +17 -0
- package/dist/gateway/methods/agent-chat.d.ts +8 -0
- package/dist/gateway/methods/agent-chat.js +194 -0
- package/dist/gateway/methods/connect.d.ts +8 -0
- package/dist/gateway/methods/connect.js +15 -0
- package/dist/gateway/methods/install-skill-from-path.d.ts +13 -0
- package/dist/gateway/methods/install-skill-from-path.js +48 -0
- package/dist/gateway/methods/run-scheduled-task.d.ts +13 -0
- package/dist/gateway/methods/run-scheduled-task.js +164 -0
- package/dist/gateway/server.d.ts +10 -0
- package/dist/gateway/server.js +268 -0
- package/dist/gateway/types.d.ts +76 -0
- package/dist/gateway/types.js +1 -0
- package/dist/gateway/utils.d.ts +22 -0
- package/dist/gateway/utils.js +67 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/memory/build-summary.d.ts +6 -0
- package/dist/memory/build-summary.js +27 -0
- package/dist/memory/compaction-extension.d.ts +6 -0
- package/dist/memory/compaction-extension.js +23 -0
- package/dist/memory/embedding.d.ts +10 -0
- package/dist/memory/embedding.js +22 -0
- package/dist/memory/index.d.ts +29 -0
- package/dist/memory/index.js +66 -0
- package/dist/memory/types.d.ts +16 -0
- package/dist/memory/types.js +1 -0
- package/dist/memory/vector-store.d.ts +15 -0
- package/dist/memory/vector-store.js +65 -0
- package/dist/server/agent-config/agent-config.controller.d.ts +30 -0
- package/dist/server/agent-config/agent-config.controller.js +83 -0
- package/dist/server/agent-config/agent-config.module.d.ts +2 -0
- package/dist/server/agent-config/agent-config.module.js +19 -0
- package/dist/server/agent-config/agent-config.service.d.ts +34 -0
- package/dist/server/agent-config/agent-config.service.js +171 -0
- package/dist/server/agents/agents.controller.d.ts +41 -0
- package/dist/server/agents/agents.controller.js +120 -0
- package/dist/server/agents/agents.gateway.d.ts +21 -0
- package/dist/server/agents/agents.gateway.js +103 -0
- package/dist/server/agents/agents.module.d.ts +2 -0
- package/dist/server/agents/agents.module.js +20 -0
- package/dist/server/agents/agents.service.d.ts +63 -0
- package/dist/server/agents/agents.service.js +167 -0
- package/dist/server/app.module.d.ts +2 -0
- package/dist/server/app.module.js +36 -0
- package/dist/server/auth/auth.controller.d.ts +20 -0
- package/dist/server/auth/auth.controller.js +64 -0
- package/dist/server/auth/auth.module.d.ts +2 -0
- package/dist/server/auth/auth.module.js +19 -0
- package/dist/server/config/config.controller.d.ts +51 -0
- package/dist/server/config/config.controller.js +81 -0
- package/dist/server/config/config.module.d.ts +2 -0
- package/dist/server/config/config.module.js +19 -0
- package/dist/server/config/config.service.d.ts +34 -0
- package/dist/server/config/config.service.js +91 -0
- package/dist/server/database/database.module.d.ts +2 -0
- package/dist/server/database/database.module.js +18 -0
- package/dist/server/database/database.service.d.ts +11 -0
- package/dist/server/database/database.service.js +137 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +18 -0
- package/dist/server/skills/skills.controller.d.ts +63 -0
- package/dist/server/skills/skills.controller.js +194 -0
- package/dist/server/skills/skills.module.d.ts +2 -0
- package/dist/server/skills/skills.module.js +22 -0
- package/dist/server/skills/skills.service.d.ts +63 -0
- package/dist/server/skills/skills.service.js +324 -0
- package/dist/server/tasks/tasks.controller.d.ts +52 -0
- package/dist/server/tasks/tasks.controller.js +163 -0
- package/dist/server/tasks/tasks.module.d.ts +2 -0
- package/dist/server/tasks/tasks.module.js +22 -0
- package/dist/server/tasks/tasks.service.d.ts +84 -0
- package/dist/server/tasks/tasks.service.js +313 -0
- package/dist/server/usage/usage.controller.d.ts +12 -0
- package/dist/server/usage/usage.controller.js +46 -0
- package/dist/server/usage/usage.module.d.ts +2 -0
- package/dist/server/usage/usage.module.js +19 -0
- package/dist/server/usage/usage.service.d.ts +21 -0
- package/dist/server/usage/usage.service.js +55 -0
- package/dist/server/users/users.controller.d.ts +35 -0
- package/dist/server/users/users.controller.js +69 -0
- package/dist/server/users/users.module.d.ts +2 -0
- package/dist/server/users/users.module.js +19 -0
- package/dist/server/users/users.service.d.ts +39 -0
- package/dist/server/users/users.service.js +140 -0
- package/dist/server/workspace/workspace.controller.d.ts +24 -0
- package/dist/server/workspace/workspace.controller.js +132 -0
- package/dist/server/workspace/workspace.module.d.ts +2 -0
- package/dist/server/workspace/workspace.module.js +21 -0
- package/dist/server/workspace/workspace.service.d.ts +25 -0
- package/dist/server/workspace/workspace.service.js +103 -0
- package/dist/tools/browser-tool.d.ts +10 -0
- package/dist/tools/browser-tool.js +362 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/install-skill-tool.d.ts +9 -0
- package/dist/tools/install-skill-tool.js +77 -0
- package/dist/tools/save-experience-tool.d.ts +5 -0
- package/dist/tools/save-experience-tool.js +54 -0
- package/package.json +80 -0
- package/skills/agent-browser/SKILL.md +207 -0
- package/skills/agent-browser/references/authentication.md +202 -0
- package/skills/agent-browser/references/commands.md +259 -0
- package/skills/agent-browser/references/proxy-support.md +188 -0
- package/skills/agent-browser/references/session-management.md +193 -0
- package/skills/agent-browser/references/snapshot-refs.md +194 -0
- package/skills/agent-browser/references/video-recording.md +173 -0
- package/skills/agent-browser/templates/authenticated-session.sh +97 -0
- package/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/skills/agent-browser/templates/form-automation.sh +62 -0
- package/skills/find-skills/SKILL.md +140 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".openbot", "desktop");
|
|
5
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
6
|
+
const DEFAULT_MAX_AGENT_SESSIONS = 5;
|
|
7
|
+
/**
|
|
8
|
+
* 读取桌面全局配置(与 Nest ConfigService 使用同一 config.json)。
|
|
9
|
+
* Gateway 进程内使用,用于获取 maxAgentSessions 等。
|
|
10
|
+
*/
|
|
11
|
+
export function getDesktopConfig() {
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
14
|
+
return { maxAgentSessions: DEFAULT_MAX_AGENT_SESSIONS };
|
|
15
|
+
}
|
|
16
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
17
|
+
const data = JSON.parse(content);
|
|
18
|
+
const max = data.maxAgentSessions;
|
|
19
|
+
const maxAgentSessions = typeof max === "number" && max > 0 ? Math.floor(max) : DEFAULT_MAX_AGENT_SESSIONS;
|
|
20
|
+
return { maxAgentSessions };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { maxAgentSessions: DEFAULT_MAX_AGENT_SESSIONS };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { parseMessage, send, createErrorResponse, createSuccessResponse } from "./utils.js";
|
|
2
|
+
import { handleConnect } from "./methods/connect.js";
|
|
3
|
+
import { handleAgentChat } from "./methods/agent-chat.js";
|
|
4
|
+
import { handleAgentCancel } from "./methods/agent-cancel.js";
|
|
5
|
+
/**
|
|
6
|
+
* Handle incoming WebSocket message
|
|
7
|
+
*/
|
|
8
|
+
export async function handleMessage(client, data) {
|
|
9
|
+
const message = parseMessage(data);
|
|
10
|
+
if (!message) {
|
|
11
|
+
send(client.ws, createErrorResponse("unknown", "Invalid message format"));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Only handle request messages
|
|
15
|
+
if (message.type !== "request") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const request = message;
|
|
19
|
+
const { id, method, params } = request;
|
|
20
|
+
console.log(`Received request: ${method} (id: ${id})`);
|
|
21
|
+
try {
|
|
22
|
+
let result;
|
|
23
|
+
switch (method) {
|
|
24
|
+
case "connect":
|
|
25
|
+
result = await handleConnect(client, params || {});
|
|
26
|
+
break;
|
|
27
|
+
case "agent.chat":
|
|
28
|
+
// Check if client is authenticated
|
|
29
|
+
if (!client.authenticated) {
|
|
30
|
+
throw new Error("Not authenticated. Call 'connect' first.");
|
|
31
|
+
}
|
|
32
|
+
result = await handleAgentChat(client, params || {});
|
|
33
|
+
break;
|
|
34
|
+
case "agent.cancel":
|
|
35
|
+
if (!client.authenticated) {
|
|
36
|
+
throw new Error("Not authenticated. Call 'connect' first.");
|
|
37
|
+
}
|
|
38
|
+
result = await handleAgentCancel(client, params || {});
|
|
39
|
+
break;
|
|
40
|
+
case "subscribe_session":
|
|
41
|
+
// Handle session subscription
|
|
42
|
+
// Since handleAgentChat manages its own subscription per request,
|
|
43
|
+
// this might be for listening to async updates.
|
|
44
|
+
// For now, we just acknowledge it to prevent client errors.
|
|
45
|
+
// A more robust implementation would hook into agentManager.
|
|
46
|
+
console.log(`Client ${client.id} subscribed to session ${params.sessionId}`);
|
|
47
|
+
client.sessionId = params.sessionId; // Store session ID on client
|
|
48
|
+
result = { status: "subscribed", sessionId: params.sessionId };
|
|
49
|
+
break;
|
|
50
|
+
case "unsubscribe_session":
|
|
51
|
+
console.log(`Client ${client.id} unsubscribed from session`);
|
|
52
|
+
delete client.sessionId;
|
|
53
|
+
result = { status: "unsubscribed" };
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unknown method: ${method}`);
|
|
57
|
+
}
|
|
58
|
+
// Send success response
|
|
59
|
+
send(client.ws, createSuccessResponse(id, result));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`Error handling ${method}:`, error);
|
|
63
|
+
send(client.ws, createErrorResponse(id, error.message || "Internal error"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GatewayClient } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handle agent.cancel: abort the current turn for the given session.
|
|
4
|
+
* Uses pi-coding-agent's session.abort() to stop the running agent and wait until idle.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handleAgentCancel(client: GatewayClient, params: {
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
status: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { agentManager } from "../../agent/agent-manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handle agent.cancel: abort the current turn for the given session.
|
|
4
|
+
* Uses pi-coding-agent's session.abort() to stop the running agent and wait until idle.
|
|
5
|
+
*/
|
|
6
|
+
export async function handleAgentCancel(client, params) {
|
|
7
|
+
const sessionId = params?.sessionId ?? client.sessionId;
|
|
8
|
+
if (!sessionId) {
|
|
9
|
+
throw new Error("No session ID available");
|
|
10
|
+
}
|
|
11
|
+
const session = agentManager.getSession(sessionId);
|
|
12
|
+
if (!session) {
|
|
13
|
+
return { status: "no_session" };
|
|
14
|
+
}
|
|
15
|
+
await session.abort();
|
|
16
|
+
return { status: "aborted" };
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GatewayClient, AgentChatParams } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handle agent chat request with streaming support
|
|
4
|
+
*/
|
|
5
|
+
export declare function handleAgentChat(client: GatewayClient, params: AgentChatParams): Promise<{
|
|
6
|
+
status: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { agentManager } from "../../agent/agent-manager.js";
|
|
2
|
+
import { getExperienceContextForUserMessage } from "../../memory/index.js";
|
|
3
|
+
import { send, createEvent } from "../utils.js";
|
|
4
|
+
import { connectedClients } from "../clients.js";
|
|
5
|
+
import { getDesktopConfig } from "../desktop-config.js";
|
|
6
|
+
import { getBackendBaseUrl } from "../backend-url.js";
|
|
7
|
+
/**
|
|
8
|
+
* Report token usage to Desktop Server (persist to DB). No-op if backend URL not set.
|
|
9
|
+
*/
|
|
10
|
+
async function reportTokenUsage(sessionId, source, tokens, options) {
|
|
11
|
+
const base = getBackendBaseUrl();
|
|
12
|
+
if (!base)
|
|
13
|
+
return;
|
|
14
|
+
const url = `${base.replace(/\/$/, "")}/server-api/usage`;
|
|
15
|
+
await fetch(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
sessionId,
|
|
20
|
+
source,
|
|
21
|
+
taskId: options?.taskId ?? undefined,
|
|
22
|
+
executionId: options?.executionId ?? undefined,
|
|
23
|
+
promptTokens: tokens.promptTokens,
|
|
24
|
+
completionTokens: tokens.completionTokens,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Broadcast message to all clients subscribed to a session
|
|
30
|
+
*/
|
|
31
|
+
function broadcastToSession(sessionId, message) {
|
|
32
|
+
for (const client of connectedClients) {
|
|
33
|
+
if (client.sessionId === sessionId) {
|
|
34
|
+
send(client.ws, message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Handle agent chat request with streaming support
|
|
40
|
+
*/
|
|
41
|
+
export async function handleAgentChat(client, params) {
|
|
42
|
+
const { message, sessionId, targetAgentId } = params;
|
|
43
|
+
if (!message || !message.trim()) {
|
|
44
|
+
throw new Error("Message is required");
|
|
45
|
+
}
|
|
46
|
+
// Use client's session ID if not provided
|
|
47
|
+
const targetSessionId = sessionId || client.sessionId;
|
|
48
|
+
if (!targetSessionId) {
|
|
49
|
+
throw new Error("No session ID available");
|
|
50
|
+
}
|
|
51
|
+
console.log(`Agent chat request for session ${targetSessionId}: ${message.substring(0, 50)}...`);
|
|
52
|
+
return handleAgentChatInner(client, targetSessionId, message, targetAgentId);
|
|
53
|
+
}
|
|
54
|
+
async function handleAgentChatInner(client, targetSessionId, message, targetAgentId) {
|
|
55
|
+
// 通过 sessionId 获取归属的 agentId,再通过 agentId 获取该智能体配置的 provider/model,用于创建 Agent Session
|
|
56
|
+
let workspace = "default";
|
|
57
|
+
let provider;
|
|
58
|
+
let modelId;
|
|
59
|
+
let sessionType;
|
|
60
|
+
let sessionAgentId = "default";
|
|
61
|
+
let apiKey;
|
|
62
|
+
const base = getBackendBaseUrl();
|
|
63
|
+
if (base) {
|
|
64
|
+
try {
|
|
65
|
+
const sessionRes = await fetch(`${base.replace(/\/$/, "")}/server-api/agents/sessions/${targetSessionId}`);
|
|
66
|
+
if (sessionRes.ok) {
|
|
67
|
+
const sessionData = (await sessionRes.json());
|
|
68
|
+
sessionType = sessionData?.data?.type;
|
|
69
|
+
sessionAgentId = sessionData?.data?.agentId ?? "default";
|
|
70
|
+
const agentRes = await fetch(`${base.replace(/\/$/, "")}/server-api/agent-config/${encodeURIComponent(sessionAgentId)}`);
|
|
71
|
+
if (agentRes.ok) {
|
|
72
|
+
const agentData = (await agentRes.json());
|
|
73
|
+
const agent = agentData?.data;
|
|
74
|
+
if (agent?.workspace)
|
|
75
|
+
workspace = agent.workspace;
|
|
76
|
+
if (agent?.provider)
|
|
77
|
+
provider = agent.provider;
|
|
78
|
+
if (agent?.model)
|
|
79
|
+
modelId = agent.model;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 从桌面端全局配置读取当前 provider 的 API Key(设置里配置的会生效)
|
|
83
|
+
const configRes = await fetch(`${base.replace(/\/$/, "")}/server-api/config`);
|
|
84
|
+
if (configRes.ok) {
|
|
85
|
+
const configData = (await configRes.json());
|
|
86
|
+
const prov = provider ?? "deepseek";
|
|
87
|
+
const key = configData?.data?.providers?.[prov]?.apiKey;
|
|
88
|
+
if (key && typeof key === "string" && key.trim())
|
|
89
|
+
apiKey = key.trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.warn("[agent-chat] Failed to fetch session/agent config, using default:", e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// system / scheduled:每次对话前先删再建,对话结束马上关闭,节省资源
|
|
97
|
+
const isEphemeralSession = sessionType === "system" || sessionType === "scheduled";
|
|
98
|
+
if (isEphemeralSession) {
|
|
99
|
+
agentManager.deleteSession(targetSessionId);
|
|
100
|
+
}
|
|
101
|
+
// system 会话用请求里的 targetAgentId;chat/scheduled 用 session 对应的 agentId 传给 install_skill
|
|
102
|
+
const effectiveTargetAgentId = sessionType === "system" ? targetAgentId : sessionAgentId;
|
|
103
|
+
const { maxAgentSessions } = getDesktopConfig();
|
|
104
|
+
let session;
|
|
105
|
+
try {
|
|
106
|
+
session = await agentManager.getOrCreateSession(targetSessionId, {
|
|
107
|
+
workspace,
|
|
108
|
+
provider,
|
|
109
|
+
modelId,
|
|
110
|
+
apiKey,
|
|
111
|
+
maxSessions: maxAgentSessions,
|
|
112
|
+
targetAgentId: effectiveTargetAgentId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const msg = err?.message ?? String(err);
|
|
117
|
+
if (msg.includes("No API key") || msg.includes("API key")) {
|
|
118
|
+
const prov = provider ?? "deepseek";
|
|
119
|
+
throw new Error(`未配置 ${prov} 的 API Key。请在桌面端「设置」-「模型/API」中配置,或运行:openbot login ${prov} <你的API Key>`);
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
// Set up event listener for streaming
|
|
124
|
+
const unsubscribe = session.subscribe((event) => {
|
|
125
|
+
// console.log(`Agent event received: ${event.type}`); // Reduce noise
|
|
126
|
+
let wsMessage = null;
|
|
127
|
+
if (event.type === "message_update") {
|
|
128
|
+
const update = event;
|
|
129
|
+
if (update.assistantMessageEvent && update.assistantMessageEvent.type === "text_delta") {
|
|
130
|
+
wsMessage = createEvent("agent.chunk", { text: update.assistantMessageEvent.delta });
|
|
131
|
+
}
|
|
132
|
+
else if (update.assistantMessageEvent && update.assistantMessageEvent.type === "thinking_delta") {
|
|
133
|
+
wsMessage = createEvent("agent.chunk", { text: update.assistantMessageEvent.delta, isThinking: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (event.type === "tool_execution_start") {
|
|
137
|
+
wsMessage = createEvent("agent.tool", {
|
|
138
|
+
type: "start",
|
|
139
|
+
toolCallId: event.toolCallId,
|
|
140
|
+
toolName: event.toolName,
|
|
141
|
+
args: event.args
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else if (event.type === "tool_execution_end") {
|
|
145
|
+
wsMessage = createEvent("agent.tool", {
|
|
146
|
+
type: "end",
|
|
147
|
+
toolCallId: event.toolCallId,
|
|
148
|
+
toolName: event.toolName,
|
|
149
|
+
result: event.result,
|
|
150
|
+
isError: event.isError
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (event.type === "turn_end") {
|
|
154
|
+
// Explicit completion event required for frontend to stop loading state
|
|
155
|
+
// Only send on turn_end to avoid duplicate empty messages from message_end
|
|
156
|
+
wsMessage = createEvent("message_complete", { sessionId: targetSessionId, content: "" });
|
|
157
|
+
// Record token usage for this turn (message may have usage.input / usage.output)
|
|
158
|
+
const usage = event.message?.usage;
|
|
159
|
+
if (usage) {
|
|
160
|
+
const promptTokens = Number(usage.input ?? usage.input_tokens ?? 0) || 0;
|
|
161
|
+
const completionTokens = Number(usage.output ?? usage.output_tokens ?? 0) || 0;
|
|
162
|
+
if (promptTokens > 0 || completionTokens > 0) {
|
|
163
|
+
reportTokenUsage(targetSessionId, "chat", { promptTokens, completionTokens }).catch((err) => console.error("[agent-chat] reportTokenUsage failed:", err));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (wsMessage) {
|
|
168
|
+
broadcastToSession(targetSessionId, wsMessage);
|
|
169
|
+
}
|
|
170
|
+
if (event.type === "turn_end" && isEphemeralSession) {
|
|
171
|
+
agentManager.deleteSession(targetSessionId);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
const experienceBlock = await getExperienceContextForUserMessage();
|
|
176
|
+
const userMessageToSend = experienceBlock.trim().length > 0
|
|
177
|
+
? `${experienceBlock}\n\n用户问题:\n${message}`
|
|
178
|
+
: message;
|
|
179
|
+
// 若 agent 正在流式输出,deliverAs: 'followUp' 将本条消息排队,避免抛出 "Agent is already processing"
|
|
180
|
+
await session.sendUserMessage(userMessageToSend, { deliverAs: "followUp" });
|
|
181
|
+
console.log(`Agent chat completed for session ${targetSessionId}`);
|
|
182
|
+
return {
|
|
183
|
+
status: "completed",
|
|
184
|
+
sessionId: targetSessionId,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error(`Error in agent chat:`, error);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
unsubscribe();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Handle client connection request
|
|
4
|
+
*/
|
|
5
|
+
export async function handleConnect(client, params) {
|
|
6
|
+
// Mark client as authenticated
|
|
7
|
+
client.authenticated = true;
|
|
8
|
+
// Use provided session ID or generate new one
|
|
9
|
+
client.sessionId = params.sessionId || randomUUID();
|
|
10
|
+
console.log(`Client ${client.id} connected with session ${client.sessionId}`);
|
|
11
|
+
return {
|
|
12
|
+
sessionId: client.sessionId || "",
|
|
13
|
+
status: "connected",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface InstallFromPathBody {
|
|
2
|
+
path: string;
|
|
3
|
+
scope?: "global" | "workspace";
|
|
4
|
+
workspace?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallFromPathResult {
|
|
7
|
+
success: true;
|
|
8
|
+
data: {
|
|
9
|
+
installDir: string;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function handleInstallSkillFromPath(body: InstallFromPathBody): Promise<InstallFromPathResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在 Gateway 层处理 POST /server-api/skills/install-from-path,
|
|
3
|
+
* 将本地技能目录复制到全局或工作区 skills,不依赖 Nest 路由,保证桌面端稳定可用。
|
|
4
|
+
*/
|
|
5
|
+
import { cp, mkdir, realpath, rm, stat } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { basename, join, resolve } from "path";
|
|
8
|
+
import { getOpenbotAgentDir, getOpenbotWorkspaceDir } from "../../agent/agent-dir.js";
|
|
9
|
+
const SKILL_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
10
|
+
function getGlobalSkillsDir() {
|
|
11
|
+
return join(getOpenbotAgentDir(), "skills");
|
|
12
|
+
}
|
|
13
|
+
function getWorkspaceSkillsDir(workspaceName) {
|
|
14
|
+
return join(getOpenbotWorkspaceDir(), workspaceName, "skills");
|
|
15
|
+
}
|
|
16
|
+
export async function handleInstallSkillFromPath(body) {
|
|
17
|
+
const localPath = (body?.path ?? "").trim();
|
|
18
|
+
if (!localPath) {
|
|
19
|
+
throw new Error("path is required");
|
|
20
|
+
}
|
|
21
|
+
const pathToUse = resolve(localPath);
|
|
22
|
+
if (!existsSync(pathToUse)) {
|
|
23
|
+
throw new Error("本地路径不存在");
|
|
24
|
+
}
|
|
25
|
+
const pathStat = await stat(pathToUse);
|
|
26
|
+
if (!pathStat.isDirectory()) {
|
|
27
|
+
throw new Error("请选择技能目录");
|
|
28
|
+
}
|
|
29
|
+
const skillMdPath = join(pathToUse, "SKILL.md");
|
|
30
|
+
if (!existsSync(skillMdPath)) {
|
|
31
|
+
throw new Error("该目录下未找到 SKILL.md,不是有效的技能目录");
|
|
32
|
+
}
|
|
33
|
+
const scope = body?.scope ?? "global";
|
|
34
|
+
const workspaceName = body?.workspace ?? "default";
|
|
35
|
+
const targetDir = scope === "workspace" ? getWorkspaceSkillsDir(workspaceName) : getGlobalSkillsDir();
|
|
36
|
+
const baseName = basename(pathToUse) || "skill";
|
|
37
|
+
if (!baseName || !SKILL_NAME_REGEX.test(baseName)) {
|
|
38
|
+
throw new Error("技能目录名须为英文、数字、下划线或连字符");
|
|
39
|
+
}
|
|
40
|
+
const destPath = join(targetDir, baseName);
|
|
41
|
+
await mkdir(targetDir, { recursive: true });
|
|
42
|
+
if (existsSync(destPath)) {
|
|
43
|
+
await rm(destPath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
const srcResolved = await realpath(pathToUse).catch(() => pathToUse);
|
|
46
|
+
await cp(srcResolved, destPath, { recursive: true });
|
|
47
|
+
return { success: true, data: { installDir: targetDir, name: baseName } };
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
2
|
+
export interface RunScheduledTaskBody {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
message: string;
|
|
5
|
+
workspace: string;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
backendBaseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run a scheduled task: configure workspace, send message to agent, collect response, POST back to Nest.
|
|
11
|
+
* 执行完成后关闭并移除 AgentSession,避免空悬占用资源。
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleRunScheduledTask(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { agentManager } from "../../agent/agent-manager.js";
|
|
2
|
+
import { getExperienceContextForUserMessage } from "../../memory/index.js";
|
|
3
|
+
async function reportTokenUsage(backendBaseUrl, sessionId, source, tokens, options) {
|
|
4
|
+
const url = `${backendBaseUrl.replace(/\/$/, "")}/server-api/usage`;
|
|
5
|
+
await fetch(url, {
|
|
6
|
+
method: "POST",
|
|
7
|
+
headers: { "Content-Type": "application/json" },
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
sessionId,
|
|
10
|
+
source,
|
|
11
|
+
taskId: options?.taskId ?? undefined,
|
|
12
|
+
executionId: options?.executionId ?? undefined,
|
|
13
|
+
promptTokens: tokens.promptTokens,
|
|
14
|
+
completionTokens: tokens.completionTokens,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function readBody(req) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
22
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
23
|
+
req.on("error", reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run a scheduled task: configure workspace, send message to agent, collect response, POST back to Nest.
|
|
28
|
+
* 执行完成后关闭并移除 AgentSession,避免空悬占用资源。
|
|
29
|
+
*/
|
|
30
|
+
export async function handleRunScheduledTask(req, res) {
|
|
31
|
+
if (req.method !== "POST") {
|
|
32
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
33
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readBody(req);
|
|
39
|
+
body = JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
43
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { sessionId, message, workspace, backendBaseUrl, taskId } = body;
|
|
47
|
+
if (!sessionId || !message || !workspace) {
|
|
48
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
49
|
+
res.end(JSON.stringify({ error: "sessionId, message, workspace required" }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// 与对话一致:仅通过 sessionId 获取 session,用 session.agentId 拉取 agent 配置与 API Key;未取到时用 "default",不依赖 body.workspace
|
|
53
|
+
let resolvedWorkspace = "default";
|
|
54
|
+
let provider;
|
|
55
|
+
let modelId;
|
|
56
|
+
let apiKey;
|
|
57
|
+
let sessionAgentId = "default";
|
|
58
|
+
const base = (backendBaseUrl ?? "").replace(/\/$/, "");
|
|
59
|
+
if (base) {
|
|
60
|
+
try {
|
|
61
|
+
const sessionRes = await fetch(`${base}/server-api/agents/sessions/${encodeURIComponent(sessionId)}`);
|
|
62
|
+
if (sessionRes.ok) {
|
|
63
|
+
const sessionData = (await sessionRes.json());
|
|
64
|
+
sessionAgentId = sessionData?.data?.agentId ?? "default";
|
|
65
|
+
if (sessionData?.data?.workspace)
|
|
66
|
+
resolvedWorkspace = sessionData.data.workspace;
|
|
67
|
+
}
|
|
68
|
+
const agentRes = await fetch(`${base}/server-api/agent-config/${encodeURIComponent(sessionAgentId)}`);
|
|
69
|
+
if (agentRes.ok) {
|
|
70
|
+
const agentData = (await agentRes.json());
|
|
71
|
+
const agent = agentData?.data;
|
|
72
|
+
if (agent?.workspace)
|
|
73
|
+
resolvedWorkspace = agent.workspace;
|
|
74
|
+
if (agent?.provider)
|
|
75
|
+
provider = agent.provider;
|
|
76
|
+
if (agent?.model)
|
|
77
|
+
modelId = agent.model;
|
|
78
|
+
}
|
|
79
|
+
const configRes = await fetch(`${base}/server-api/config`);
|
|
80
|
+
if (configRes.ok) {
|
|
81
|
+
const configData = (await configRes.json());
|
|
82
|
+
const prov = provider ?? "deepseek";
|
|
83
|
+
const key = configData?.data?.providers?.[prov]?.apiKey;
|
|
84
|
+
if (key && typeof key === "string" && key.trim())
|
|
85
|
+
apiKey = key.trim();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.warn("[run-scheduled-task] Failed to fetch session/agent/config, using default:", e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const session = await agentManager.getOrCreateSession(sessionId, {
|
|
94
|
+
workspace: resolvedWorkspace,
|
|
95
|
+
provider,
|
|
96
|
+
modelId,
|
|
97
|
+
apiKey,
|
|
98
|
+
});
|
|
99
|
+
let assistantContent = "";
|
|
100
|
+
let turnPromptTokens = 0;
|
|
101
|
+
let turnCompletionTokens = 0;
|
|
102
|
+
const unsubscribe = session.subscribe((event) => {
|
|
103
|
+
if (event.type === "message_update" && event.assistantMessageEvent) {
|
|
104
|
+
const ev = event.assistantMessageEvent;
|
|
105
|
+
if (ev.type === "text_delta" && ev.delta) {
|
|
106
|
+
assistantContent += ev.delta;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (event.type === "turn_end") {
|
|
110
|
+
const usage = event.message?.usage;
|
|
111
|
+
if (usage) {
|
|
112
|
+
turnPromptTokens += Number(usage.input ?? usage.input_tokens ?? 0) || 0;
|
|
113
|
+
turnCompletionTokens += Number(usage.output ?? usage.output_tokens ?? 0) || 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const experienceBlock = await getExperienceContextForUserMessage();
|
|
118
|
+
const userMessageToSend = experienceBlock.trim().length > 0
|
|
119
|
+
? `${experienceBlock}\n\n用户问题:\n${message}`
|
|
120
|
+
: message;
|
|
121
|
+
// 定时任务复用同一 session:若上次执行未结束会报 "Agent is already processing"。先等待空闲再发,避免并发。
|
|
122
|
+
const idleTimeoutMs = 10 * 60 * 1000;
|
|
123
|
+
const pollMs = 2000;
|
|
124
|
+
let waited = 0;
|
|
125
|
+
while (session.isStreaming && waited < idleTimeoutMs) {
|
|
126
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
127
|
+
waited += pollMs;
|
|
128
|
+
}
|
|
129
|
+
if (session.isStreaming) {
|
|
130
|
+
throw new Error("Session still busy after waiting; try again later.");
|
|
131
|
+
}
|
|
132
|
+
await session.sendUserMessage(userMessageToSend);
|
|
133
|
+
unsubscribe();
|
|
134
|
+
if (backendBaseUrl && assistantContent !== undefined) {
|
|
135
|
+
const url = `${backendBaseUrl.replace(/\/$/, "")}/server-api/agents/sessions/${encodeURIComponent(sessionId)}/messages`;
|
|
136
|
+
await fetch(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({ role: "assistant", content: assistantContent }),
|
|
140
|
+
}).catch((err) => console.error("[run-scheduled-task] POST assistant message failed:", err));
|
|
141
|
+
}
|
|
142
|
+
if (backendBaseUrl && (turnPromptTokens > 0 || turnCompletionTokens > 0)) {
|
|
143
|
+
reportTokenUsage(backendBaseUrl, sessionId, "scheduled_task", {
|
|
144
|
+
promptTokens: turnPromptTokens,
|
|
145
|
+
completionTokens: turnCompletionTokens,
|
|
146
|
+
}, { taskId }).catch((err) => console.error("[run-scheduled-task] reportTokenUsage failed:", err));
|
|
147
|
+
}
|
|
148
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
149
|
+
res.end(JSON.stringify({ success: true, sessionId, assistantContent: assistantContent ?? "" }));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error("[run-scheduled-task] error:", error);
|
|
153
|
+
const msg = error?.message ?? String(error);
|
|
154
|
+
const friendlyError = msg.includes("No API key") || msg.includes("API key")
|
|
155
|
+
? "未配置大模型 API Key。请在桌面端「设置」-「模型配置」中为当前智能体选择 Provider 并保存,并确保在「Provider 配置」中已填写对应 API Key。"
|
|
156
|
+
: msg || "Internal server error";
|
|
157
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
158
|
+
res.end(JSON.stringify({ success: false, error: friendlyError }));
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// 执行完成(成功或失败)后立即关闭并移除该 AgentSession,避免空悬占用内存/连接等资源
|
|
162
|
+
agentManager.deleteSession(sessionId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { type Server } from "http";
|
|
3
|
+
/**
|
|
4
|
+
* Start WebSocket gateway server
|
|
5
|
+
*/
|
|
6
|
+
export declare function startGatewayServer(port?: number): Promise<{
|
|
7
|
+
httpServer: Server;
|
|
8
|
+
wss: WebSocketServer;
|
|
9
|
+
close: () => Promise<void>;
|
|
10
|
+
}>;
|