ai-browser 0.1.0
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 +130 -0
- package/dist/agent/agent-loop.d.ts +35 -0
- package/dist/agent/agent-loop.d.ts.map +1 -0
- package/dist/agent/agent-loop.js +406 -0
- package/dist/agent/agent-loop.js.map +1 -0
- package/dist/agent/api-client.d.ts +12 -0
- package/dist/agent/api-client.d.ts.map +1 -0
- package/dist/agent/api-client.js +59 -0
- package/dist/agent/api-client.js.map +1 -0
- package/dist/agent/config.d.ts +10 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +10 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +84 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/prompt.d.ts +2 -0
- package/dist/agent/prompt.d.ts.map +1 -0
- package/dist/agent/prompt.js +64 -0
- package/dist/agent/prompt.js.map +1 -0
- package/dist/agent/tools.d.ts +3 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +166 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/agent/types.d.ts +57 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/api/errors.d.ts +24 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +33 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/mcp-sse.d.ts +4 -0
- package/dist/api/mcp-sse.d.ts.map +1 -0
- package/dist/api/mcp-sse.js +45 -0
- package/dist/api/mcp-sse.js.map +1 -0
- package/dist/api/routes.d.ts +4 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +628 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/browser/BrowserManager.d.ts +26 -0
- package/dist/browser/BrowserManager.d.ts.map +1 -0
- package/dist/browser/BrowserManager.js +82 -0
- package/dist/browser/BrowserManager.js.map +1 -0
- package/dist/browser/CookieStore.d.ts +20 -0
- package/dist/browser/CookieStore.d.ts.map +1 -0
- package/dist/browser/CookieStore.js +77 -0
- package/dist/browser/CookieStore.js.map +1 -0
- package/dist/browser/SessionManager.d.ts +41 -0
- package/dist/browser/SessionManager.d.ts.map +1 -0
- package/dist/browser/SessionManager.js +146 -0
- package/dist/browser/SessionManager.js.map +1 -0
- package/dist/browser/actions.d.ts +7 -0
- package/dist/browser/actions.d.ts.map +1 -0
- package/dist/browser/actions.js +110 -0
- package/dist/browser/actions.js.map +1 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/cli/mcp-stdio.d.ts +3 -0
- package/dist/cli/mcp-stdio.d.ts.map +1 -0
- package/dist/cli/mcp-stdio.js +34 -0
- package/dist/cli/mcp-stdio.js.map +1 -0
- package/dist/cli/server.d.ts +3 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +75 -0
- package/dist/cli/server.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/browser-mcp-server.d.ts +8 -0
- package/dist/mcp/browser-mcp-server.d.ts.map +1 -0
- package/dist/mcp/browser-mcp-server.js +276 -0
- package/dist/mcp/browser-mcp-server.js.map +1 -0
- package/dist/semantic/ContentExtractor.d.ts +23 -0
- package/dist/semantic/ContentExtractor.d.ts.map +1 -0
- package/dist/semantic/ContentExtractor.js +119 -0
- package/dist/semantic/ContentExtractor.js.map +1 -0
- package/dist/semantic/ElementCollector.d.ts +15 -0
- package/dist/semantic/ElementCollector.d.ts.map +1 -0
- package/dist/semantic/ElementCollector.js +133 -0
- package/dist/semantic/ElementCollector.js.map +1 -0
- package/dist/semantic/ElementMatcher.d.ts +13 -0
- package/dist/semantic/ElementMatcher.d.ts.map +1 -0
- package/dist/semantic/ElementMatcher.js +84 -0
- package/dist/semantic/ElementMatcher.js.map +1 -0
- package/dist/semantic/IframeHandler.d.ts +17 -0
- package/dist/semantic/IframeHandler.d.ts.map +1 -0
- package/dist/semantic/IframeHandler.js +72 -0
- package/dist/semantic/IframeHandler.js.map +1 -0
- package/dist/semantic/ModelAdapter.d.ts +26 -0
- package/dist/semantic/ModelAdapter.d.ts.map +1 -0
- package/dist/semantic/ModelAdapter.js +47 -0
- package/dist/semantic/ModelAdapter.js.map +1 -0
- package/dist/semantic/PageAnalyzer.d.ts +15 -0
- package/dist/semantic/PageAnalyzer.d.ts.map +1 -0
- package/dist/semantic/PageAnalyzer.js +131 -0
- package/dist/semantic/PageAnalyzer.js.map +1 -0
- package/dist/semantic/RegionDetector.d.ts +8 -0
- package/dist/semantic/RegionDetector.d.ts.map +1 -0
- package/dist/semantic/RegionDetector.js +53 -0
- package/dist/semantic/RegionDetector.js.map +1 -0
- package/dist/semantic/StateTracker.d.ts +24 -0
- package/dist/semantic/StateTracker.d.ts.map +1 -0
- package/dist/semantic/StateTracker.js +111 -0
- package/dist/semantic/StateTracker.js.map +1 -0
- package/dist/semantic/index.d.ts +9 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +9 -0
- package/dist/semantic/index.js.map +1 -0
- package/dist/types/enums.d.ts +23 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/enums.js +26 -0
- package/dist/types/enums.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/semantic.d.ts +13 -0
- package/dist/types/semantic.d.ts.map +1 -0
- package/dist/types/semantic.js +2 -0
- package/dist/types/semantic.js.map +1 -0
- package/dist/types/state.d.ts +15 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +2 -0
- package/dist/types/state.js.map +1 -0
- package/dist/types/structures.d.ts +28 -0
- package/dist/types/structures.d.ts.map +1 -0
- package/dist/types/structures.js +2 -0
- package/dist/types/structures.js.map +1 -0
- package/package.json +52 -0
- package/public/agent.html +932 -0
- package/public/index.html +752 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
2
|
+
import { CookieStore } from '../browser/CookieStore.js';
|
|
3
|
+
import { createBrowserMcpServer } from '../mcp/browser-mcp-server.js';
|
|
4
|
+
export function registerMcpSseRoutes(app, sessionManager) {
|
|
5
|
+
const cookieStore = new CookieStore();
|
|
6
|
+
// sessionId -> SSEServerTransport
|
|
7
|
+
const transports = new Map();
|
|
8
|
+
// GET /mcp/sse — 建立 SSE 连接
|
|
9
|
+
app.get('/mcp/sse', (request, reply) => {
|
|
10
|
+
reply.hijack();
|
|
11
|
+
const transport = new SSEServerTransport('/mcp/message', reply.raw);
|
|
12
|
+
const mcpServer = createBrowserMcpServer(sessionManager, cookieStore);
|
|
13
|
+
const sessionId = transport.sessionId;
|
|
14
|
+
transports.set(sessionId, transport);
|
|
15
|
+
transport.onclose = () => {
|
|
16
|
+
transports.delete(sessionId);
|
|
17
|
+
mcpServer.close().catch(() => { });
|
|
18
|
+
};
|
|
19
|
+
mcpServer.connect(transport).catch((err) => {
|
|
20
|
+
app.log.error('MCP SSE connect error:', err);
|
|
21
|
+
transports.delete(sessionId);
|
|
22
|
+
});
|
|
23
|
+
request.raw.on('close', () => {
|
|
24
|
+
transports.delete(sessionId);
|
|
25
|
+
transport.close().catch(() => { });
|
|
26
|
+
mcpServer.close().catch(() => { });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
// POST /mcp/message?sessionId=xxx — 接收 MCP 客户端消息
|
|
30
|
+
app.post('/mcp/message', async (request, reply) => {
|
|
31
|
+
const sessionId = request.query.sessionId;
|
|
32
|
+
if (!sessionId) {
|
|
33
|
+
reply.status(400).send({ error: 'sessionId query parameter required' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const transport = transports.get(sessionId);
|
|
37
|
+
if (!transport) {
|
|
38
|
+
reply.status(404).send({ error: 'SSE session not found' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// 使用 raw req/res 调用 handlePostMessage
|
|
42
|
+
await transport.handlePostMessage(request.raw, reply.raw, request.body);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=mcp-sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-sse.js","sourceRoot":"","sources":["../../src/api/mcp-sse.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAE7E,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAEtE,MAAM,UAAU,oBAAoB,CAClC,GAAoB,EACpB,cAA8B;IAE9B,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IAEtC,kCAAkC;IAClC,MAAM,UAAU,GAAG,IAAI,GAAG,EAA8B,CAAC;IAEzD,2BAA2B;IAC3B,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QACrC,KAAK,CAAC,MAAM,EAAE,CAAC;QAEf,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QACpE,MAAM,SAAS,GAAG,sBAAsB,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QAEtE,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;QACtC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAErC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;YACvB,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC7B,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC;QAEF,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACzC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;YAC7C,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3B,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC7B,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAClC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,iDAAiD;IACjD,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChD,MAAM,SAAS,GAAI,OAAO,CAAC,KAAa,CAAC,SAAmB,CAAC;QAC7D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,sCAAsC;QACtC,MAAM,SAAS,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmBrD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,cAAc,EAAE,cAAc,QA8rB/B"}
|
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { executeAction } from '../browser/actions.js';
|
|
3
|
+
import { ElementCollector, PageAnalyzer, RegionDetector, ContentExtractor, IframeHandler, ElementMatcher, } from '../semantic/index.js';
|
|
4
|
+
import { ApiError, ErrorCode } from './errors.js';
|
|
5
|
+
import { BrowsingAgent } from '../agent/agent-loop.js';
|
|
6
|
+
import { createBrowserMcpServer } from '../mcp/browser-mcp-server.js';
|
|
7
|
+
import { CookieStore } from '../browser/CookieStore.js';
|
|
8
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
9
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
10
|
+
const MAX_BATCH_ACTIONS = 50;
|
|
11
|
+
export function registerRoutes(app, sessionManager) {
|
|
12
|
+
const elementCollector = new ElementCollector();
|
|
13
|
+
const pageAnalyzer = new PageAnalyzer();
|
|
14
|
+
const regionDetector = new RegionDetector();
|
|
15
|
+
const contentExtractor = new ContentExtractor();
|
|
16
|
+
const iframeHandler = new IframeHandler();
|
|
17
|
+
const elementMatcher = new ElementMatcher();
|
|
18
|
+
// 健康检查
|
|
19
|
+
app.get('/health', async () => {
|
|
20
|
+
return { status: 'healthy', version: '0.1.0' };
|
|
21
|
+
});
|
|
22
|
+
// 内存使用情况(用于测试监控)
|
|
23
|
+
app.get('/v1/memory', async () => {
|
|
24
|
+
const mem = process.memoryUsage();
|
|
25
|
+
return {
|
|
26
|
+
rss: mem.rss,
|
|
27
|
+
heapUsed: mem.heapUsed,
|
|
28
|
+
heapTotal: mem.heapTotal,
|
|
29
|
+
external: mem.external,
|
|
30
|
+
arrayBuffers: mem.arrayBuffers,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
// 服务信息
|
|
34
|
+
app.get('/v1/info', async () => {
|
|
35
|
+
return {
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
capabilities: ['semantic', 'action'],
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
// 创建会话
|
|
41
|
+
app.post('/v1/sessions', async (request) => {
|
|
42
|
+
const { options } = request.body || {};
|
|
43
|
+
const session = await sessionManager.create(options);
|
|
44
|
+
return {
|
|
45
|
+
sessionId: session.id,
|
|
46
|
+
status: 'created',
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
// 获取会话详情
|
|
50
|
+
app.get('/v1/sessions/:sessionId', async (request) => {
|
|
51
|
+
const { sessionId } = request.params;
|
|
52
|
+
const session = sessionManager.get(sessionId);
|
|
53
|
+
if (!session) {
|
|
54
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
55
|
+
}
|
|
56
|
+
const tabs = sessionManager.listTabs(sessionId);
|
|
57
|
+
return {
|
|
58
|
+
sessionId: session.id,
|
|
59
|
+
status: 'active',
|
|
60
|
+
activeTabId: session.activeTabId,
|
|
61
|
+
tabs: tabs.map(t => ({ id: t.id, url: t.url })),
|
|
62
|
+
createdAt: session.createdAt,
|
|
63
|
+
lastActivityAt: session.lastActivityAt,
|
|
64
|
+
expiresAt: session.expiresAt,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
// 关闭会话
|
|
68
|
+
app.delete('/v1/sessions/:sessionId', async (request) => {
|
|
69
|
+
const { sessionId } = request.params;
|
|
70
|
+
const closed = await sessionManager.close(sessionId);
|
|
71
|
+
if (!closed) {
|
|
72
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
73
|
+
}
|
|
74
|
+
return { success: true };
|
|
75
|
+
});
|
|
76
|
+
// ========== Tab管理API ==========
|
|
77
|
+
// 创建新Tab
|
|
78
|
+
app.post('/v1/sessions/:sessionId/tabs', async (request) => {
|
|
79
|
+
const { sessionId } = request.params;
|
|
80
|
+
const { url } = request.body;
|
|
81
|
+
const session = sessionManager.get(sessionId);
|
|
82
|
+
if (!session) {
|
|
83
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
84
|
+
}
|
|
85
|
+
// URL验证(如果提供了URL)
|
|
86
|
+
if (url) {
|
|
87
|
+
if (typeof url !== 'string') {
|
|
88
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'URL must be a string', 400);
|
|
89
|
+
}
|
|
90
|
+
let parsedUrl;
|
|
91
|
+
try {
|
|
92
|
+
parsedUrl = new URL(url);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Invalid URL format', 400);
|
|
96
|
+
}
|
|
97
|
+
const allowedProtocols = ['http:', 'https:', 'file:'];
|
|
98
|
+
if (!allowedProtocols.includes(parsedUrl.protocol)) {
|
|
99
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Only http/https/file URLs allowed', 400);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let tab;
|
|
103
|
+
try {
|
|
104
|
+
tab = await sessionManager.createTab(sessionId);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, err.message, 400);
|
|
108
|
+
}
|
|
109
|
+
if (!tab) {
|
|
110
|
+
throw new ApiError(ErrorCode.INTERNAL_ERROR, 'Failed to create tab', 500);
|
|
111
|
+
}
|
|
112
|
+
// 如果提供了URL,直接导航
|
|
113
|
+
if (url) {
|
|
114
|
+
await tab.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
115
|
+
tab.url = tab.page.url();
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
tabId: tab.id,
|
|
119
|
+
url: tab.url,
|
|
120
|
+
title: await tab.page.title(),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
// 列出所有Tab
|
|
124
|
+
app.get('/v1/sessions/:sessionId/tabs', async (request) => {
|
|
125
|
+
const { sessionId } = request.params;
|
|
126
|
+
const session = sessionManager.get(sessionId);
|
|
127
|
+
if (!session) {
|
|
128
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
129
|
+
}
|
|
130
|
+
const tabs = sessionManager.listTabs(sessionId);
|
|
131
|
+
const tabInfos = await Promise.all(tabs.map(async (t) => ({
|
|
132
|
+
id: t.id,
|
|
133
|
+
url: t.page.url(),
|
|
134
|
+
title: await t.page.title(),
|
|
135
|
+
isActive: t.id === session.activeTabId,
|
|
136
|
+
})));
|
|
137
|
+
return { tabs: tabInfos, activeTabId: session.activeTabId };
|
|
138
|
+
});
|
|
139
|
+
// 关闭Tab
|
|
140
|
+
app.delete('/v1/sessions/:sessionId/tabs/:tabId', async (request) => {
|
|
141
|
+
const { sessionId, tabId } = request.params;
|
|
142
|
+
const closed = await sessionManager.closeTab(sessionId, tabId);
|
|
143
|
+
if (!closed) {
|
|
144
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
145
|
+
}
|
|
146
|
+
return { success: true };
|
|
147
|
+
});
|
|
148
|
+
// 切换活动Tab
|
|
149
|
+
app.post('/v1/sessions/:sessionId/tabs/:tabId/activate', async (request) => {
|
|
150
|
+
const { sessionId, tabId } = request.params;
|
|
151
|
+
const success = sessionManager.switchTab(sessionId, tabId);
|
|
152
|
+
if (!success) {
|
|
153
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
154
|
+
}
|
|
155
|
+
return { success: true, activeTabId: tabId };
|
|
156
|
+
});
|
|
157
|
+
// 页面导航(支持指定tabId)
|
|
158
|
+
app.post('/v1/sessions/:sessionId/navigate', async (request) => {
|
|
159
|
+
const { sessionId } = request.params;
|
|
160
|
+
const { url, tabId } = request.body;
|
|
161
|
+
// URL验证
|
|
162
|
+
if (!url || typeof url !== 'string') {
|
|
163
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'URL is required', 400);
|
|
164
|
+
}
|
|
165
|
+
let parsedUrl;
|
|
166
|
+
try {
|
|
167
|
+
parsedUrl = new URL(url);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Invalid URL format', 400);
|
|
171
|
+
}
|
|
172
|
+
const allowedProtocols = ['http:', 'https:', 'file:'];
|
|
173
|
+
if (!allowedProtocols.includes(parsedUrl.protocol)) {
|
|
174
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Only http/https/file URLs allowed', 400);
|
|
175
|
+
}
|
|
176
|
+
const session = sessionManager.get(sessionId);
|
|
177
|
+
if (!session) {
|
|
178
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
179
|
+
}
|
|
180
|
+
// 获取目标Tab(默认使用活动Tab)
|
|
181
|
+
const tab = tabId
|
|
182
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
183
|
+
: sessionManager.getActiveTab(sessionId);
|
|
184
|
+
if (!tab) {
|
|
185
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await tab.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err.name === 'TimeoutError') {
|
|
192
|
+
throw new ApiError(ErrorCode.PAGE_LOAD_TIMEOUT, `Navigation timeout: ${url}`, 504);
|
|
193
|
+
}
|
|
194
|
+
throw new ApiError(ErrorCode.ACTION_FAILED, err.message || 'Navigation failed', 502);
|
|
195
|
+
}
|
|
196
|
+
tab.url = tab.page.url();
|
|
197
|
+
sessionManager.updateActivity(sessionId);
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
tabId: tab.id,
|
|
201
|
+
page: {
|
|
202
|
+
url: tab.page.url(),
|
|
203
|
+
title: await tab.page.title(),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
// 获取语义元素(支持tabId参数)
|
|
208
|
+
app.get('/v1/sessions/:sessionId/semantic', async (request) => {
|
|
209
|
+
const { sessionId } = request.params;
|
|
210
|
+
const { tabId } = request.query;
|
|
211
|
+
const session = sessionManager.get(sessionId);
|
|
212
|
+
if (!session) {
|
|
213
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
214
|
+
}
|
|
215
|
+
const tab = tabId
|
|
216
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
217
|
+
: sessionManager.getActiveTab(sessionId);
|
|
218
|
+
if (!tab) {
|
|
219
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
220
|
+
}
|
|
221
|
+
const [elements, analysis, regions] = await Promise.all([
|
|
222
|
+
elementCollector.collect(tab.page),
|
|
223
|
+
pageAnalyzer.analyze(tab.page),
|
|
224
|
+
regionDetector.detect(tab.page),
|
|
225
|
+
]);
|
|
226
|
+
sessionManager.updateActivity(sessionId);
|
|
227
|
+
return {
|
|
228
|
+
tabId: tab.id,
|
|
229
|
+
page: {
|
|
230
|
+
url: tab.page.url(),
|
|
231
|
+
title: await tab.page.title(),
|
|
232
|
+
type: analysis.pageType,
|
|
233
|
+
summary: analysis.summary,
|
|
234
|
+
},
|
|
235
|
+
elements,
|
|
236
|
+
regions,
|
|
237
|
+
intents: analysis.intents,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
// 执行操作(支持tabId)
|
|
241
|
+
app.post('/v1/sessions/:sessionId/action', async (request) => {
|
|
242
|
+
const { sessionId } = request.params;
|
|
243
|
+
const { action, elementId, value, tabId } = request.body;
|
|
244
|
+
if (!action || typeof action !== 'string') {
|
|
245
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Action is required', 400);
|
|
246
|
+
}
|
|
247
|
+
const session = sessionManager.get(sessionId);
|
|
248
|
+
if (!session) {
|
|
249
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
250
|
+
}
|
|
251
|
+
const tab = tabId
|
|
252
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
253
|
+
: sessionManager.getActiveTab(sessionId);
|
|
254
|
+
if (!tab) {
|
|
255
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await executeAction(tab.page, action, elementId, value);
|
|
259
|
+
tab.url = tab.page.url();
|
|
260
|
+
sessionManager.updateActivity(sessionId);
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
tabId: tab.id,
|
|
264
|
+
page: {
|
|
265
|
+
url: tab.page.url(),
|
|
266
|
+
title: await tab.page.title(),
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
throw new ApiError(ErrorCode.ACTION_FAILED, err.message || 'Action failed', 400);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// 批量操作(支持tabId)
|
|
275
|
+
app.post('/v1/sessions/:sessionId/actions', async (request) => {
|
|
276
|
+
const { sessionId } = request.params;
|
|
277
|
+
const { actions, tabId } = request.body;
|
|
278
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
279
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Actions array required', 400);
|
|
280
|
+
}
|
|
281
|
+
if (actions.length > MAX_BATCH_ACTIONS) {
|
|
282
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, `Max ${MAX_BATCH_ACTIONS} actions allowed`, 400);
|
|
283
|
+
}
|
|
284
|
+
const session = sessionManager.get(sessionId);
|
|
285
|
+
if (!session) {
|
|
286
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
287
|
+
}
|
|
288
|
+
const tab = tabId
|
|
289
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
290
|
+
: sessionManager.getActiveTab(sessionId);
|
|
291
|
+
if (!tab) {
|
|
292
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
293
|
+
}
|
|
294
|
+
const results = [];
|
|
295
|
+
for (const act of actions) {
|
|
296
|
+
if (!act || typeof act.action !== 'string') {
|
|
297
|
+
results.push({ success: false, error: 'Invalid action object' });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await executeAction(tab.page, act.action, act.elementId, act.value);
|
|
302
|
+
results.push({ success: true });
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
results.push({ success: false, error: err.message });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
tab.url = tab.page.url();
|
|
309
|
+
sessionManager.updateActivity(sessionId);
|
|
310
|
+
return {
|
|
311
|
+
results,
|
|
312
|
+
tabId: tab.id,
|
|
313
|
+
page: {
|
|
314
|
+
url: tab.page.url(),
|
|
315
|
+
title: await tab.page.title(),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
// 内容提取(支持tabId)
|
|
320
|
+
app.get('/v1/sessions/:sessionId/content', async (request) => {
|
|
321
|
+
const { sessionId } = request.params;
|
|
322
|
+
const { tabId } = request.query;
|
|
323
|
+
const session = sessionManager.get(sessionId);
|
|
324
|
+
if (!session) {
|
|
325
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
326
|
+
}
|
|
327
|
+
const tab = tabId
|
|
328
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
329
|
+
: sessionManager.getActiveTab(sessionId);
|
|
330
|
+
if (!tab) {
|
|
331
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
332
|
+
}
|
|
333
|
+
const content = await contentExtractor.extract(tab.page);
|
|
334
|
+
sessionManager.updateActivity(sessionId);
|
|
335
|
+
return { tabId: tab.id, ...content };
|
|
336
|
+
});
|
|
337
|
+
// iframe信息(支持tabId)
|
|
338
|
+
app.get('/v1/sessions/:sessionId/frames', async (request) => {
|
|
339
|
+
const { sessionId } = request.params;
|
|
340
|
+
const { tabId } = request.query;
|
|
341
|
+
const session = sessionManager.get(sessionId);
|
|
342
|
+
if (!session) {
|
|
343
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
344
|
+
}
|
|
345
|
+
const tab = tabId
|
|
346
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
347
|
+
: sessionManager.getActiveTab(sessionId);
|
|
348
|
+
if (!tab) {
|
|
349
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
350
|
+
}
|
|
351
|
+
const frames = await iframeHandler.detectFrames(tab.page);
|
|
352
|
+
sessionManager.updateActivity(sessionId);
|
|
353
|
+
return { tabId: tab.id, frames };
|
|
354
|
+
});
|
|
355
|
+
// 模糊匹配元素(支持tabId)
|
|
356
|
+
app.post('/v1/sessions/:sessionId/match', async (request) => {
|
|
357
|
+
const { sessionId } = request.params;
|
|
358
|
+
const { query, limit, tabId } = request.body;
|
|
359
|
+
if (!query) {
|
|
360
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Query required', 400);
|
|
361
|
+
}
|
|
362
|
+
const session = sessionManager.get(sessionId);
|
|
363
|
+
if (!session) {
|
|
364
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
365
|
+
}
|
|
366
|
+
const tab = tabId
|
|
367
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
368
|
+
: sessionManager.getActiveTab(sessionId);
|
|
369
|
+
if (!tab) {
|
|
370
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
371
|
+
}
|
|
372
|
+
const elements = await elementCollector.collect(tab.page);
|
|
373
|
+
const candidates = elementMatcher.findByQuery(elements, query, limit || 5);
|
|
374
|
+
sessionManager.updateActivity(sessionId);
|
|
375
|
+
return {
|
|
376
|
+
tabId: tab.id,
|
|
377
|
+
query,
|
|
378
|
+
candidates: candidates.map((c) => ({
|
|
379
|
+
id: c.element.id,
|
|
380
|
+
label: c.element.label,
|
|
381
|
+
type: c.element.type,
|
|
382
|
+
score: c.score,
|
|
383
|
+
matchReason: c.matchReason,
|
|
384
|
+
})),
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
// 并发获取多个Tab的内容(AI批量浏览场景)
|
|
388
|
+
app.post('/v1/sessions/:sessionId/tabs/batch-content', async (request) => {
|
|
389
|
+
const { sessionId } = request.params;
|
|
390
|
+
const { tabIds } = request.body;
|
|
391
|
+
const session = sessionManager.get(sessionId);
|
|
392
|
+
if (!session) {
|
|
393
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
394
|
+
}
|
|
395
|
+
const tabs = tabIds
|
|
396
|
+
? tabIds.map((id) => sessionManager.getTab(sessionId, id)).filter(Boolean)
|
|
397
|
+
: sessionManager.listTabs(sessionId);
|
|
398
|
+
const results = await Promise.all(tabs.map(async (tab) => {
|
|
399
|
+
try {
|
|
400
|
+
const [content, elements] = await Promise.all([
|
|
401
|
+
contentExtractor.extract(tab.page),
|
|
402
|
+
elementCollector.collect(tab.page),
|
|
403
|
+
]);
|
|
404
|
+
return {
|
|
405
|
+
tabId: tab.id,
|
|
406
|
+
url: tab.page.url(),
|
|
407
|
+
title: await tab.page.title(),
|
|
408
|
+
content,
|
|
409
|
+
elementCount: elements.length,
|
|
410
|
+
success: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
return {
|
|
415
|
+
tabId: tab.id,
|
|
416
|
+
success: false,
|
|
417
|
+
error: err.message,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}));
|
|
421
|
+
return { results };
|
|
422
|
+
});
|
|
423
|
+
// 在页面中执行JavaScript(用于精确提取数据)
|
|
424
|
+
app.post('/v1/sessions/:sessionId/evaluate', async (request) => {
|
|
425
|
+
const { sessionId } = request.params;
|
|
426
|
+
const { expression, tabId } = request.body;
|
|
427
|
+
if (!expression || typeof expression !== 'string') {
|
|
428
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Expression is required', 400);
|
|
429
|
+
}
|
|
430
|
+
const session = sessionManager.get(sessionId);
|
|
431
|
+
if (!session) {
|
|
432
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
433
|
+
}
|
|
434
|
+
const tab = tabId
|
|
435
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
436
|
+
: sessionManager.getActiveTab(sessionId);
|
|
437
|
+
if (!tab) {
|
|
438
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const result = await tab.page.evaluate(expression);
|
|
442
|
+
sessionManager.updateActivity(sessionId);
|
|
443
|
+
return { success: true, tabId: tab.id, result };
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
throw new ApiError(ErrorCode.ACTION_FAILED, err.message || 'Evaluate failed', 400);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
// 等待指定时间(配合动态内容加载)
|
|
450
|
+
app.post('/v1/sessions/:sessionId/wait', async (request) => {
|
|
451
|
+
const { sessionId } = request.params;
|
|
452
|
+
const { milliseconds, selector, tabId } = request.body;
|
|
453
|
+
const session = sessionManager.get(sessionId);
|
|
454
|
+
if (!session) {
|
|
455
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
456
|
+
}
|
|
457
|
+
const tab = tabId
|
|
458
|
+
? sessionManager.getTab(sessionId, tabId)
|
|
459
|
+
: sessionManager.getActiveTab(sessionId);
|
|
460
|
+
if (!tab) {
|
|
461
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Tab not found', 404);
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
if (selector) {
|
|
465
|
+
await tab.page.waitForSelector(selector, { timeout: milliseconds || 10000 });
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
await new Promise(r => setTimeout(r, Math.min(milliseconds || 1000, 30000)));
|
|
469
|
+
}
|
|
470
|
+
sessionManager.updateActivity(sessionId);
|
|
471
|
+
return { success: true, tabId: tab.id };
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
throw new ApiError(ErrorCode.ACTION_FAILED, err.message || 'Wait failed', 400);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// ========== Screenshot API ==========
|
|
478
|
+
app.get('/v1/sessions/:sessionId/screenshot', async (request) => {
|
|
479
|
+
const { sessionId } = request.params;
|
|
480
|
+
const session = sessionManager.get(sessionId);
|
|
481
|
+
if (!session) {
|
|
482
|
+
throw new ApiError(ErrorCode.SESSION_NOT_FOUND, 'Session not found', 404);
|
|
483
|
+
}
|
|
484
|
+
const tab = sessionManager.getActiveTab(sessionId);
|
|
485
|
+
if (!tab) {
|
|
486
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'No active tab', 404);
|
|
487
|
+
}
|
|
488
|
+
const base64 = await tab.page.screenshot({ type: 'png', encoding: 'base64' });
|
|
489
|
+
return { image: `data:image/png;base64,${base64}` };
|
|
490
|
+
});
|
|
491
|
+
// ========== Agent API ==========
|
|
492
|
+
const MAX_CONCURRENT_AGENTS = 5;
|
|
493
|
+
const AGENT_HARD_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
494
|
+
const AGENT_CLEANUP_DELAY = 60 * 1000; // 60s after done
|
|
495
|
+
const runningAgents = new Map();
|
|
496
|
+
// 进程级 CookieStore,跨 agent 共享,保持登录状态
|
|
497
|
+
const cookieStore = new CookieStore();
|
|
498
|
+
app.post('/v1/agent/run', async (request) => {
|
|
499
|
+
const { task, apiKey, baseURL, model, messages, maxIterations, headless } = request.body;
|
|
500
|
+
if (!task || typeof task !== 'string') {
|
|
501
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'task is required', 400);
|
|
502
|
+
}
|
|
503
|
+
// Concurrency limit
|
|
504
|
+
const activeCount = [...runningAgents.values()].filter(e => !e.finished).length;
|
|
505
|
+
if (activeCount >= MAX_CONCURRENT_AGENTS) {
|
|
506
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, `Max ${MAX_CONCURRENT_AGENTS} concurrent agents`, 429);
|
|
507
|
+
}
|
|
508
|
+
// Create MCP Server + InMemoryTransport + MCP Client
|
|
509
|
+
const mcpHeadless = headless !== undefined ? { headless: headless } : {};
|
|
510
|
+
const mcpServer = createBrowserMcpServer(sessionManager, cookieStore, mcpHeadless);
|
|
511
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
512
|
+
await mcpServer.connect(serverTransport);
|
|
513
|
+
const mcpClient = new Client({ name: 'agent', version: '0.1.0' });
|
|
514
|
+
await mcpClient.connect(clientTransport);
|
|
515
|
+
const agent = new BrowsingAgent({
|
|
516
|
+
apiKey: apiKey || undefined,
|
|
517
|
+
baseURL: baseURL || undefined,
|
|
518
|
+
model: model || undefined,
|
|
519
|
+
mcpClient,
|
|
520
|
+
maxIterations: maxIterations || undefined,
|
|
521
|
+
initialMessages: messages || undefined,
|
|
522
|
+
});
|
|
523
|
+
const agentId = randomUUID();
|
|
524
|
+
const entry = { agent, buffer: [], finished: false };
|
|
525
|
+
// Buffer all events so SSE client can replay them on connect
|
|
526
|
+
// Skip duplicate done events (hard timeout may race with agent completion)
|
|
527
|
+
agent.on('event', (event) => {
|
|
528
|
+
if (entry.finished && event.type === 'done')
|
|
529
|
+
return;
|
|
530
|
+
entry.buffer.push(event);
|
|
531
|
+
if (event.type === 'done') {
|
|
532
|
+
entry.finished = true;
|
|
533
|
+
if (entry.cleanupTimer)
|
|
534
|
+
clearTimeout(entry.cleanupTimer);
|
|
535
|
+
entry.cleanupTimer = setTimeout(() => runningAgents.delete(agentId), AGENT_CLEANUP_DELAY);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
runningAgents.set(agentId, entry);
|
|
539
|
+
// Hard timeout: force cleanup if agent runs too long
|
|
540
|
+
entry.cleanupTimer = setTimeout(async () => {
|
|
541
|
+
if (!entry.finished) {
|
|
542
|
+
entry.finished = true;
|
|
543
|
+
entry.buffer.push({ type: 'done', success: false, error: 'Agent timeout', iterations: 0 });
|
|
544
|
+
agent.emit('event', { type: 'done', success: false, error: 'Agent timeout', iterations: 0 });
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
await mcpClient.close();
|
|
548
|
+
}
|
|
549
|
+
catch { }
|
|
550
|
+
try {
|
|
551
|
+
await mcpServer.close();
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
setTimeout(() => runningAgents.delete(agentId), AGENT_CLEANUP_DELAY);
|
|
555
|
+
}, AGENT_HARD_TIMEOUT);
|
|
556
|
+
// Fire-and-forget with error handling + MCP cleanup
|
|
557
|
+
agent.run(task).catch((err) => {
|
|
558
|
+
if (!entry.finished) {
|
|
559
|
+
entry.finished = true;
|
|
560
|
+
const errEvent = { type: 'done', success: false, error: err.message, iterations: 0 };
|
|
561
|
+
entry.buffer.push(errEvent);
|
|
562
|
+
agent.emit('event', errEvent);
|
|
563
|
+
}
|
|
564
|
+
}).finally(async () => {
|
|
565
|
+
try {
|
|
566
|
+
await mcpClient.close();
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
try {
|
|
570
|
+
await mcpServer.close();
|
|
571
|
+
}
|
|
572
|
+
catch { }
|
|
573
|
+
});
|
|
574
|
+
return { agentId };
|
|
575
|
+
});
|
|
576
|
+
app.get('/v1/agent/:agentId/events', (request, reply) => {
|
|
577
|
+
const { agentId } = request.params;
|
|
578
|
+
const entry = runningAgents.get(agentId);
|
|
579
|
+
if (!entry) {
|
|
580
|
+
reply.status(404).send({ error: { code: 'INVALID_REQUEST', message: 'Agent not found' } });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
reply.hijack();
|
|
584
|
+
reply.raw.writeHead(200, {
|
|
585
|
+
'Content-Type': 'text/event-stream',
|
|
586
|
+
'Cache-Control': 'no-cache',
|
|
587
|
+
'Connection': 'keep-alive',
|
|
588
|
+
});
|
|
589
|
+
const onEvent = (event) => {
|
|
590
|
+
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
591
|
+
if (event.type === 'done') {
|
|
592
|
+
reply.raw.end();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
// Replay buffered events first (synchronous, no events can interleave)
|
|
596
|
+
for (const event of entry.buffer) {
|
|
597
|
+
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
598
|
+
}
|
|
599
|
+
// If agent already finished, close stream immediately
|
|
600
|
+
if (entry.finished) {
|
|
601
|
+
reply.raw.end();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// Only now attach live listener for future events
|
|
605
|
+
entry.agent.on('event', onEvent);
|
|
606
|
+
request.raw.on('close', () => {
|
|
607
|
+
entry.agent.removeListener('event', onEvent);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
// ========== Agent Input (ask_human response) ==========
|
|
611
|
+
app.post('/v1/agent/:agentId/input', async (request) => {
|
|
612
|
+
const { agentId: aid } = request.params;
|
|
613
|
+
const { requestId, response } = request.body;
|
|
614
|
+
if (!requestId || !response || typeof response !== 'object') {
|
|
615
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'requestId and response are required', 400);
|
|
616
|
+
}
|
|
617
|
+
const entry = runningAgents.get(aid);
|
|
618
|
+
if (!entry) {
|
|
619
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'Agent not found', 404);
|
|
620
|
+
}
|
|
621
|
+
const resolved = entry.agent.resolveInput(requestId, response);
|
|
622
|
+
if (!resolved) {
|
|
623
|
+
throw new ApiError(ErrorCode.INVALID_REQUEST, 'No pending input with this requestId', 400);
|
|
624
|
+
}
|
|
625
|
+
return { success: true };
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
//# sourceMappingURL=routes.js.map
|