vite-plugin-aipanel 1.2.0-beta.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 +72 -0
- package/es/client/vue-devtools-bridge.d.ts +58 -0
- package/es/client/vue-devtools-bridge.mjs +150 -0
- package/es/core/injector.d.ts +2 -0
- package/es/core/injector.mjs +15 -0
- package/es/core/mcp-chrome.d.ts +56 -0
- package/es/core/mcp-chrome.mjs +133 -0
- package/es/core/mcp-proxy.d.ts +23 -0
- package/es/core/mcp-proxy.mjs +212 -0
- package/es/core/mcp-tools.d.ts +15 -0
- package/es/core/mcp-tools.mjs +671 -0
- package/es/core/provider-loader.d.ts +19 -0
- package/es/core/provider-loader.mjs +30 -0
- package/es/core/proxy-server.d.ts +12 -0
- package/es/core/proxy-server.mjs +148 -0
- package/es/core/service.d.ts +39 -0
- package/es/core/service.mjs +267 -0
- package/es/endpoints/context.d.ts +3 -0
- package/es/endpoints/context.mjs +106 -0
- package/es/endpoints/index.d.ts +9 -0
- package/es/endpoints/index.mjs +30 -0
- package/es/endpoints/logs.d.ts +9 -0
- package/es/endpoints/logs.mjs +86 -0
- package/es/endpoints/mcp.d.ts +6 -0
- package/es/endpoints/mcp.mjs +610 -0
- package/es/endpoints/sessions.d.ts +3 -0
- package/es/endpoints/sessions.mjs +62 -0
- package/es/endpoints/sse.d.ts +3 -0
- package/es/endpoints/sse.mjs +44 -0
- package/es/endpoints/start.d.ts +3 -0
- package/es/endpoints/start.mjs +21 -0
- package/es/endpoints/types.d.ts +34 -0
- package/es/endpoints/types.mjs +0 -0
- package/es/endpoints/vue-devtools.d.ts +9 -0
- package/es/endpoints/vue-devtools.mjs +107 -0
- package/es/endpoints/warmup.d.ts +3 -0
- package/es/endpoints/warmup.mjs +42 -0
- package/es/endpoints/widget.d.ts +3 -0
- package/es/endpoints/widget.mjs +36 -0
- package/es/env.d.ts +1 -0
- package/es/index.d.ts +22 -0
- package/es/index.mjs +342 -0
- package/es/utils/format-bridge.d.ts +9 -0
- package/es/utils/format-bridge.mjs +80 -0
- package/es/utils/paths.d.ts +3 -0
- package/es/utils/paths.mjs +45 -0
- package/es/utils/system.d.ts +3 -0
- package/es/utils/system.mjs +76 -0
- package/lib/client/vue-devtools-bridge.cjs +150 -0
- package/lib/client/vue-devtools-bridge.d.ts +58 -0
- package/lib/client.css +2 -0
- package/lib/client.js +6950 -0
- package/lib/core/injector.cjs +34 -0
- package/lib/core/injector.d.ts +2 -0
- package/lib/core/mcp-chrome.cjs +161 -0
- package/lib/core/mcp-chrome.d.ts +56 -0
- package/lib/core/mcp-proxy.cjs +244 -0
- package/lib/core/mcp-proxy.d.ts +23 -0
- package/lib/core/mcp-tools.cjs +694 -0
- package/lib/core/mcp-tools.d.ts +15 -0
- package/lib/core/provider-loader.cjs +53 -0
- package/lib/core/provider-loader.d.ts +19 -0
- package/lib/core/proxy-server.cjs +181 -0
- package/lib/core/proxy-server.d.ts +12 -0
- package/lib/core/service.cjs +289 -0
- package/lib/core/service.d.ts +39 -0
- package/lib/endpoints/context.cjs +129 -0
- package/lib/endpoints/context.d.ts +3 -0
- package/lib/endpoints/index.cjs +58 -0
- package/lib/endpoints/index.d.ts +9 -0
- package/lib/endpoints/logs.cjs +107 -0
- package/lib/endpoints/logs.d.ts +9 -0
- package/lib/endpoints/mcp.cjs +624 -0
- package/lib/endpoints/mcp.d.ts +6 -0
- package/lib/endpoints/sessions.cjs +85 -0
- package/lib/endpoints/sessions.d.ts +3 -0
- package/lib/endpoints/sse.cjs +67 -0
- package/lib/endpoints/sse.d.ts +3 -0
- package/lib/endpoints/start.cjs +44 -0
- package/lib/endpoints/start.d.ts +3 -0
- package/lib/endpoints/types.cjs +15 -0
- package/lib/endpoints/types.d.ts +34 -0
- package/lib/endpoints/vue-devtools.cjs +132 -0
- package/lib/endpoints/vue-devtools.d.ts +9 -0
- package/lib/endpoints/warmup.cjs +65 -0
- package/lib/endpoints/warmup.d.ts +3 -0
- package/lib/endpoints/widget.cjs +69 -0
- package/lib/endpoints/widget.d.ts +3 -0
- package/lib/env.d.ts +1 -0
- package/lib/index.cjs +361 -0
- package/lib/index.d.ts +22 -0
- package/lib/utils/format-bridge.cjs +102 -0
- package/lib/utils/format-bridge.d.ts +9 -0
- package/lib/utils/paths.cjs +80 -0
- package/lib/utils/paths.d.ts +3 -0
- package/lib/utils/system.cjs +110 -0
- package/lib/utils/system.d.ts +3 -0
- package/package.json +68 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getProcessLogBuffer
|
|
3
|
+
} from "@aipanel/core/node";
|
|
4
|
+
import { RequestContext, createLogger } from "@aipanel/core/node";
|
|
5
|
+
import { LOGS_API_PATH } from "@aipanel/core";
|
|
6
|
+
const log = createLogger("Endpoints:Logs");
|
|
7
|
+
function setupLogsEndpoint(server) {
|
|
8
|
+
server.middlewares.use(LOGS_API_PATH, async (req, res) => {
|
|
9
|
+
const reqCtx = new RequestContext(req.method || "GET", LOGS_API_PATH);
|
|
10
|
+
res.setHeader("Content-Type", "application/json");
|
|
11
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
12
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS");
|
|
13
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
14
|
+
if (req.method === "OPTIONS") {
|
|
15
|
+
res.writeHead(200);
|
|
16
|
+
res.end();
|
|
17
|
+
reqCtx.end(200);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const buffer = getProcessLogBuffer();
|
|
21
|
+
if (req.method === "GET") {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(req.url || "", `http://${req.headers.host || "localhost"}`);
|
|
24
|
+
const levelParam = url.searchParams.get("level");
|
|
25
|
+
const limitParam = url.searchParams.get("limit");
|
|
26
|
+
const sourceParam = url.searchParams.get("source");
|
|
27
|
+
const sinceParam = url.searchParams.get("since");
|
|
28
|
+
const options = {};
|
|
29
|
+
if (levelParam) {
|
|
30
|
+
const levels = levelParam.split(",").map((l) => l.trim());
|
|
31
|
+
options.level = levels.length === 1 ? levels[0] : levels;
|
|
32
|
+
}
|
|
33
|
+
if (limitParam) {
|
|
34
|
+
const limit = parseInt(limitParam, 10);
|
|
35
|
+
if (limit > 0 && limit <= 1e3) {
|
|
36
|
+
options.limit = limit;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (sourceParam) {
|
|
40
|
+
options.source = sourceParam;
|
|
41
|
+
}
|
|
42
|
+
if (sinceParam) {
|
|
43
|
+
options.since = sinceParam;
|
|
44
|
+
}
|
|
45
|
+
const logs = buffer.getLogs(options);
|
|
46
|
+
log.debug("Logs requested", {
|
|
47
|
+
params: { level: levelParam, limit: limitParam, source: sourceParam, since: sinceParam },
|
|
48
|
+
resultCount: logs.length,
|
|
49
|
+
bufferSize: buffer.size()
|
|
50
|
+
});
|
|
51
|
+
res.writeHead(200);
|
|
52
|
+
res.end(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
logs,
|
|
55
|
+
meta: {
|
|
56
|
+
total: buffer.size(),
|
|
57
|
+
returned: logs.length,
|
|
58
|
+
filters: options
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
reqCtx.end(200);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
log.error("Failed to get logs", { error: e });
|
|
65
|
+
res.writeHead(500);
|
|
66
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
67
|
+
reqCtx.error(e);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (req.method === "DELETE") {
|
|
72
|
+
buffer.clear();
|
|
73
|
+
log.debug("Log buffer cleared");
|
|
74
|
+
res.writeHead(200);
|
|
75
|
+
res.end(JSON.stringify({ success: true, message: "Log buffer cleared" }));
|
|
76
|
+
reqCtx.end(200);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.writeHead(405);
|
|
80
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
81
|
+
reqCtx.end(405);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
setupLogsEndpoint
|
|
86
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import type { LogFileConfig, PageContext } from "@aipanel/core";
|
|
3
|
+
import { MCP_API_PATH } from "@aipanel/core";
|
|
4
|
+
import { McpProxy } from "../core/mcp-proxy";
|
|
5
|
+
export { MCP_API_PATH };
|
|
6
|
+
export declare function setupMcpEndpoint(server: ViteDevServer, mcp: McpProxy, getPageContext: () => PageContext, logFiles?: LogFileConfig[]): void;
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { MCP_API_PATH, VUE_DEVTOOLS_ACTIONS, sleep } from "@aipanel/core";
|
|
2
|
+
import {
|
|
3
|
+
createLogger,
|
|
4
|
+
getProcessLogBuffer,
|
|
5
|
+
readLogFileTail
|
|
6
|
+
} from "@aipanel/core/node";
|
|
7
|
+
import {
|
|
8
|
+
parseListPages,
|
|
9
|
+
resolveChromePageId,
|
|
10
|
+
getProjectOrigins,
|
|
11
|
+
isProjectPage,
|
|
12
|
+
validatePageId
|
|
13
|
+
} from "../core/mcp-chrome.mjs";
|
|
14
|
+
import { CUSTOM_TOOLS } from "../core/mcp-tools.mjs";
|
|
15
|
+
import { executeAction } from "./vue-devtools.mjs";
|
|
16
|
+
import { findGitRoot } from "../utils/system.mjs";
|
|
17
|
+
const log = createLogger("McpEndpoint");
|
|
18
|
+
function setupMcpEndpoint(server, mcp, getPageContext, logFiles = []) {
|
|
19
|
+
const projectRoot = findGitRoot(process.cwd());
|
|
20
|
+
server.middlewares.use(async (req, res, next) => {
|
|
21
|
+
if (!req.url?.startsWith(MCP_API_PATH)) return next();
|
|
22
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
23
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
24
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
|
|
25
|
+
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
26
|
+
if (req.method === "OPTIONS") {
|
|
27
|
+
res.writeHead(204);
|
|
28
|
+
res.end();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (req.method === "GET") {
|
|
32
|
+
handleGetSse(req, res, mcp);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (req.method === "DELETE") {
|
|
36
|
+
res.writeHead(200);
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (req.method === "POST") {
|
|
41
|
+
await handlePost(
|
|
42
|
+
req,
|
|
43
|
+
res,
|
|
44
|
+
mcp,
|
|
45
|
+
getProjectOrigins(server),
|
|
46
|
+
getPageContext,
|
|
47
|
+
logFiles,
|
|
48
|
+
projectRoot
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
next();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function handleGetSse(req, res, mcp) {
|
|
56
|
+
res.writeHead(200, {
|
|
57
|
+
"Content-Type": "text/event-stream",
|
|
58
|
+
"Cache-Control": "no-cache",
|
|
59
|
+
Connection: "keep-alive",
|
|
60
|
+
"Mcp-Session-Id": mcp.sessionId
|
|
61
|
+
});
|
|
62
|
+
res.write(":ok\n\n");
|
|
63
|
+
const keepAlive = setInterval(() => res.write(":ping\n\n"), 15e3);
|
|
64
|
+
req.on("close", () => clearInterval(keepAlive));
|
|
65
|
+
}
|
|
66
|
+
async function handlePost(req, res, mcp, projectOrigins, getPageContext, logFiles, projectRoot) {
|
|
67
|
+
try {
|
|
68
|
+
const body = await readBody(req);
|
|
69
|
+
if (!body) {
|
|
70
|
+
res.writeHead(400);
|
|
71
|
+
res.end("Empty body");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { method, id } = tryParseRequest(body);
|
|
75
|
+
log.debug("MCP request", { method, body: body.substring(0, 150) });
|
|
76
|
+
switch (method) {
|
|
77
|
+
case "tools/list":
|
|
78
|
+
return handleToolsList(res, id, mcp.sessionId, logFiles);
|
|
79
|
+
case "tools/call":
|
|
80
|
+
return await handleToolsCall(
|
|
81
|
+
res,
|
|
82
|
+
id,
|
|
83
|
+
body,
|
|
84
|
+
mcp,
|
|
85
|
+
projectOrigins,
|
|
86
|
+
getPageContext,
|
|
87
|
+
logFiles,
|
|
88
|
+
projectRoot
|
|
89
|
+
);
|
|
90
|
+
default:
|
|
91
|
+
return await handleForward(res, body, mcp);
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
log.debug("MCP POST error", { error: e.message });
|
|
95
|
+
sendMcpJson(res, 500, {
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
error: { code: -32603, message: e.message }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function handleToolsList(res, id, sessionId, logFiles) {
|
|
102
|
+
const tools = [...CUSTOM_TOOLS, ...buildServiceLogTools(logFiles)];
|
|
103
|
+
sendMcpJson(res, 200, { jsonrpc: "2.0", id, result: { tools } }, sessionId);
|
|
104
|
+
}
|
|
105
|
+
function handleToolsCall(res, id, body, mcp, projectOrigins, getPageContext, logFiles, projectRoot) {
|
|
106
|
+
const params = tryParseParams(body);
|
|
107
|
+
const toolName = params?.name;
|
|
108
|
+
const args = params?.arguments ?? {};
|
|
109
|
+
if (toolName === "chrome-devtools_current_page") {
|
|
110
|
+
return handleGetPageContext(res, id, mcp, projectOrigins, getPageContext);
|
|
111
|
+
}
|
|
112
|
+
if (toolName?.startsWith("vue-devtools_")) {
|
|
113
|
+
return handleVueDevtoolsTool(res, id, mcp, projectOrigins, toolName, args);
|
|
114
|
+
}
|
|
115
|
+
if (toolName === "logs-devtools_vite_logs") {
|
|
116
|
+
return handleViteLogsTool(res, id, args, mcp.sessionId);
|
|
117
|
+
}
|
|
118
|
+
if (toolName && isServiceLogTool(toolName, logFiles)) {
|
|
119
|
+
return handleServiceLogsTool(res, id, toolName, args, logFiles, projectRoot, mcp.sessionId);
|
|
120
|
+
}
|
|
121
|
+
if (toolName?.startsWith("chrome-devtools_")) {
|
|
122
|
+
const mapped = toolName.slice("chrome-devtools_".length);
|
|
123
|
+
if (toolName === "chrome-devtools_list_pages") {
|
|
124
|
+
return handleListPages(res, id, mcp, projectOrigins, getPageContext);
|
|
125
|
+
}
|
|
126
|
+
if (toolName === "chrome-devtools_new_page") {
|
|
127
|
+
return handleNewPage(res, id, mcp, args, projectOrigins, getPageContext);
|
|
128
|
+
}
|
|
129
|
+
return handleDevTool(res, id, mcp, mapped, args, projectOrigins, toolName);
|
|
130
|
+
}
|
|
131
|
+
sendMcpError(res, id, -32601, `Tool not found: ${toolName}`, mcp.sessionId);
|
|
132
|
+
}
|
|
133
|
+
async function resolveProjectPages(mcp, projectOrigins, getPageContext) {
|
|
134
|
+
const listResult = await mcp.callChromeDevTool("list_pages", {});
|
|
135
|
+
if (listResult?.error || !listResult?.result) {
|
|
136
|
+
throw new Error(listResult?.error?.message ?? "\u65E0\u6CD5\u83B7\u53D6 Chrome \u9875\u9762\u5217\u8868");
|
|
137
|
+
}
|
|
138
|
+
const text = listResult?.result?.content?.[0]?.text;
|
|
139
|
+
const allPages = text ? parseListPages(text) : [];
|
|
140
|
+
const filtered = allPages.filter((p) => isProjectPage(p.url, projectOrigins));
|
|
141
|
+
log.debug("Chrome pages", { total: allPages.length, urls: allPages.map((p) => p.url) });
|
|
142
|
+
log.debug("project origins", { origins: projectOrigins });
|
|
143
|
+
log.debug("filtered pages", { count: filtered.length, pageIds: filtered.map((p) => p.pageId) });
|
|
144
|
+
let activePageId = null;
|
|
145
|
+
let activePageError;
|
|
146
|
+
if (filtered.length > 0) {
|
|
147
|
+
const pc = getPageContext();
|
|
148
|
+
const chromeSelectedPageId = allPages.find((p) => p.selected)?.pageId;
|
|
149
|
+
const resolved = await resolveChromePageId(
|
|
150
|
+
mcp,
|
|
151
|
+
pc.url,
|
|
152
|
+
pc.title,
|
|
153
|
+
projectOrigins,
|
|
154
|
+
pc.sessionId,
|
|
155
|
+
filtered,
|
|
156
|
+
chromeSelectedPageId
|
|
157
|
+
);
|
|
158
|
+
activePageId = resolved.ok ? resolved.pageId : null;
|
|
159
|
+
if (!resolved.ok) activePageError = resolved.error;
|
|
160
|
+
}
|
|
161
|
+
return { filtered, activePageId, activePageError };
|
|
162
|
+
}
|
|
163
|
+
async function confirmOpenProjectPages(mcp, projectOrigins, getPageContext) {
|
|
164
|
+
let pages = [];
|
|
165
|
+
let ok = false;
|
|
166
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
167
|
+
try {
|
|
168
|
+
({ filtered: pages } = await resolveProjectPages(mcp, projectOrigins, getPageContext));
|
|
169
|
+
ok = true;
|
|
170
|
+
break;
|
|
171
|
+
} catch {
|
|
172
|
+
if (attempt < 2) await sleep(500);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!ok) return null;
|
|
176
|
+
if (pages.length === 0) {
|
|
177
|
+
await sleep(500);
|
|
178
|
+
try {
|
|
179
|
+
({ filtered: pages } = await resolveProjectPages(mcp, projectOrigins, getPageContext));
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return pages;
|
|
184
|
+
}
|
|
185
|
+
async function handleListPages(res, id, mcp, projectOrigins, getPageContext) {
|
|
186
|
+
try {
|
|
187
|
+
const { filtered, activePageId } = await resolveProjectPages(
|
|
188
|
+
mcp,
|
|
189
|
+
projectOrigins,
|
|
190
|
+
getPageContext
|
|
191
|
+
);
|
|
192
|
+
if (filtered.length === 0) {
|
|
193
|
+
sendMcpResult(res, id, "\u6682\u65E0\u9879\u76EE\u9875\u9762\uFF0C\u8BF7\u5148\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u672C\u5730\u5F00\u53D1\u9875\u9762", mcp.sessionId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const pageList = filtered.map((p) => ({
|
|
197
|
+
pageId: p.pageId,
|
|
198
|
+
url: p.url,
|
|
199
|
+
title: p.title,
|
|
200
|
+
active: activePageId != null ? p.pageId === activePageId : false,
|
|
201
|
+
selected: p.selected
|
|
202
|
+
}));
|
|
203
|
+
sendMcpResult(res, id, JSON.stringify(pageList, null, 2), mcp.sessionId);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
log.debug("handleListPages error", { error: e.message });
|
|
206
|
+
sendMcpError(res, id, -32603, `MCP \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function handleGetPageContext(res, id, mcp, projectOrigins, getPageContext) {
|
|
210
|
+
try {
|
|
211
|
+
const { filtered, activePageId, activePageError } = await resolveProjectPages(
|
|
212
|
+
mcp,
|
|
213
|
+
projectOrigins,
|
|
214
|
+
getPageContext
|
|
215
|
+
);
|
|
216
|
+
if (filtered.length === 0) {
|
|
217
|
+
sendMcpResult(res, id, "\u6682\u65E0\u9879\u76EE\u9875\u9762\uFF0C\u8BF7\u5148\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u672C\u5730\u5F00\u53D1\u9875\u9762", mcp.sessionId);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const pc = getPageContext();
|
|
221
|
+
if (activePageId == null) {
|
|
222
|
+
sendMcpResult(
|
|
223
|
+
res,
|
|
224
|
+
id,
|
|
225
|
+
`\u65E0\u6CD5\u5B9A\u4F4D\u5F53\u524D\u9875\u9762\u5BF9\u5E94\u7684 pageId\uFF1A${activePageError ?? "\u672A\u5339\u914D\u5230\u5DF2\u6253\u5F00\u7684\u9875\u9762"}
|
|
226
|
+
|
|
227
|
+
\u5F53\u524D\u9875\u9762\u4E0A\u4E0B\u6587\uFF1A${pc.title} (${pc.url})
|
|
228
|
+
|
|
229
|
+
\u8BF7\u5237\u65B0\u76EE\u6807\u9875\u9762\u540E\u91CD\u8BD5\uFF1B\u6216\u4F7F\u7528 chrome-devtools_list_pages \u83B7\u53D6\u9875\u9762 ID \u540E\u76F4\u63A5\u64CD\u4F5C\u3002`,
|
|
230
|
+
mcp.sessionId
|
|
231
|
+
);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const activePage = filtered.find((p) => p.pageId === activePageId);
|
|
235
|
+
sendMcpResult(
|
|
236
|
+
res,
|
|
237
|
+
id,
|
|
238
|
+
JSON.stringify(
|
|
239
|
+
{
|
|
240
|
+
url: activePage?.url ?? pc.url,
|
|
241
|
+
title: activePage?.title ?? pc.title,
|
|
242
|
+
pageId: activePageId
|
|
243
|
+
},
|
|
244
|
+
null,
|
|
245
|
+
2
|
|
246
|
+
),
|
|
247
|
+
mcp.sessionId
|
|
248
|
+
);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
log.debug("handleGetPageContext error", { error: e.message });
|
|
251
|
+
sendMcpError(res, id, -32603, `MCP \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function handleNewPage(res, id, mcp, args, projectOrigins, getPageContext) {
|
|
255
|
+
try {
|
|
256
|
+
const url = args["url"];
|
|
257
|
+
if (typeof url !== "string" || url.trim().length === 0) {
|
|
258
|
+
sendMcpError(res, id, -32e3, "\u7F3A\u5C11 url \u53C2\u6570\uFF0C\u8BF7\u63D0\u4F9B\u8981\u6253\u5F00\u7684\u9875\u9762 URL", mcp.sessionId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!isProjectPage(url, projectOrigins)) {
|
|
262
|
+
sendMcpError(
|
|
263
|
+
res,
|
|
264
|
+
id,
|
|
265
|
+
-32e3,
|
|
266
|
+
`\u4E0D\u5141\u8BB8\u6253\u5F00\u975E\u672C\u9879\u76EE\u9875\u9762: ${url}\u3002\u4EC5\u5141\u8BB8\u6253\u5F00\u9879\u76EE\u9875\u9762 (${projectOrigins.join(", ")})`,
|
|
267
|
+
mcp.sessionId
|
|
268
|
+
);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const projectPages = await confirmOpenProjectPages(mcp, projectOrigins, getPageContext);
|
|
272
|
+
if (projectPages === null) {
|
|
273
|
+
sendMcpError(
|
|
274
|
+
res,
|
|
275
|
+
id,
|
|
276
|
+
-32e3,
|
|
277
|
+
`\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u9879\u76EE\u9875\u9762\u72B6\u6001\uFF0C\u4E3A\u907F\u514D\u91CD\u590D\u6253\u5F00\uFF0C\u8BF7\u786E\u8BA4 Chrome DevTools \u5DF2\u8FDE\u63A5\u540E\u91CD\u8BD5`,
|
|
278
|
+
mcp.sessionId
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (projectPages.length > 0) {
|
|
283
|
+
const existing = projectPages.map((p) => `- ${p.title} (${p.url}) [pageId: ${p.pageId}]`).join("\n");
|
|
284
|
+
sendMcpResult(
|
|
285
|
+
res,
|
|
286
|
+
id,
|
|
287
|
+
`\u5F53\u524D\u9879\u76EE\u5DF2\u6709\u6253\u5F00\u7684\u9875\u9762\uFF0C\u65E0\u9700\u91CD\u590D\u6253\u5F00\u3002
|
|
288
|
+
|
|
289
|
+
\u5DF2\u6253\u5F00\u7684\u9875\u9762\uFF1A
|
|
290
|
+
${existing}
|
|
291
|
+
|
|
292
|
+
\u5982\u9700\u64CD\u4F5C\u73B0\u6709\u9875\u9762\uFF0C\u8BF7\u4F7F\u7528 chrome-devtools_list_pages \u83B7\u53D6\u9875\u9762 ID\uFF0C\u518D\u914D\u5408\u5176\u4ED6 chrome-devtools_* \u5DE5\u5177\u4F7F\u7528\u3002`,
|
|
293
|
+
mcp.sessionId
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const forwardBody = JSON.stringify({
|
|
298
|
+
jsonrpc: "2.0",
|
|
299
|
+
id,
|
|
300
|
+
method: "tools/call",
|
|
301
|
+
params: { name: "new_page", arguments: { ...args, background: true } }
|
|
302
|
+
});
|
|
303
|
+
const responseText = await mcp.forward(forwardBody);
|
|
304
|
+
log.debug("MCP response", { tool: "new_page", response: responseText.substring(0, 100) });
|
|
305
|
+
sendMcpJson(res, 200, responseText, mcp.sessionId);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
log.debug("handleNewPage error", { error: e.message });
|
|
308
|
+
sendMcpError(res, id, -32603, `MCP \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function handleDevTool(res, id, mcp, mapped, args, projectOrigins, toolName) {
|
|
312
|
+
try {
|
|
313
|
+
const pageId = args["pageId"];
|
|
314
|
+
if (typeof pageId !== "number") {
|
|
315
|
+
sendMcpError(res, id, -32e3, "\u7F3A\u5C11 pageId \u53C2\u6570\uFF0C\u8BF7\u5148\u83B7\u53D6\u9875\u9762 ID", mcp.sessionId);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const validation = await validatePageId(mcp, pageId, projectOrigins);
|
|
319
|
+
log.debug("handleDevTool validation", {
|
|
320
|
+
pageId,
|
|
321
|
+
projectPages: validation.projectPages.length,
|
|
322
|
+
projectPageIds: validation.projectPages.map((p) => p.pageId),
|
|
323
|
+
origins: projectOrigins,
|
|
324
|
+
isValid: validation.valid
|
|
325
|
+
});
|
|
326
|
+
if (!validation.valid) {
|
|
327
|
+
sendMcpError(res, id, -32e3, validation.error, mcp.sessionId);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (toolName === "chrome-devtools_navigate_page" && args["type"] === "url") {
|
|
331
|
+
const targetUrl = args["url"];
|
|
332
|
+
if (typeof targetUrl === "string" && targetUrl.length > 0) {
|
|
333
|
+
if (!isProjectPage(targetUrl, projectOrigins)) {
|
|
334
|
+
sendMcpError(
|
|
335
|
+
res,
|
|
336
|
+
id,
|
|
337
|
+
-32e3,
|
|
338
|
+
`\u4E0D\u5141\u8BB8\u8DF3\u8F6C\u5230\u975E\u672C\u9879\u76EE\u9875\u9762: ${targetUrl}\u3002\u4EC5\u5141\u8BB8\u5BFC\u822A\u5230\u9879\u76EE\u9875\u9762 (${projectOrigins.join(", ")})`,
|
|
339
|
+
mcp.sessionId
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
await mcp.callChromeDevTool("select_page", { pageId, bringToFront: false });
|
|
346
|
+
const forwardArgs = { ...args };
|
|
347
|
+
delete forwardArgs["pageId"];
|
|
348
|
+
const forwardBody = JSON.stringify({
|
|
349
|
+
jsonrpc: "2.0",
|
|
350
|
+
id,
|
|
351
|
+
method: "tools/call",
|
|
352
|
+
params: { name: mapped, arguments: forwardArgs }
|
|
353
|
+
});
|
|
354
|
+
const responseText = await mcp.forward(forwardBody);
|
|
355
|
+
log.debug("MCP response", { mapped, response: responseText.substring(0, 100) });
|
|
356
|
+
sendMcpJson(res, 200, responseText, mcp.sessionId);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
log.debug("handleDevTool error", { error: e.message, mapped });
|
|
359
|
+
sendMcpError(res, id, -32603, `MCP \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const VUE_DEVTOOLS_TOOL_ACTIONS = {
|
|
363
|
+
"vue-devtools_get_apps": VUE_DEVTOOLS_ACTIONS.GET_APPS,
|
|
364
|
+
"vue-devtools_set_active_app": VUE_DEVTOOLS_ACTIONS.TOGGLE_APP,
|
|
365
|
+
"vue-devtools_get_component_tree": VUE_DEVTOOLS_ACTIONS.GET_COMPONENT_TREE,
|
|
366
|
+
"vue-devtools_get_component_state": VUE_DEVTOOLS_ACTIONS.GET_COMPONENT_STATE,
|
|
367
|
+
"vue-devtools_get_component_render_code": VUE_DEVTOOLS_ACTIONS.GET_COMPONENT_RENDER_CODE,
|
|
368
|
+
"vue-devtools_get_current_route": VUE_DEVTOOLS_ACTIONS.GET_ROUTER_INFO,
|
|
369
|
+
"vue-devtools_get_routes": VUE_DEVTOOLS_ACTIONS.GET_ROUTER_INFO
|
|
370
|
+
};
|
|
371
|
+
async function handleVueDevtoolsTool(res, id, mcp, projectOrigins, toolName, args) {
|
|
372
|
+
try {
|
|
373
|
+
const action = VUE_DEVTOOLS_TOOL_ACTIONS[toolName];
|
|
374
|
+
if (!action) {
|
|
375
|
+
sendMcpError(res, id, -32601, `Tool not found: ${toolName}`, mcp.sessionId);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const result = await executeAction(action, args, mcp, projectOrigins);
|
|
379
|
+
switch (toolName) {
|
|
380
|
+
case "vue-devtools_set_active_app":
|
|
381
|
+
sendMcpResult(res, id, `\u5DF2\u5207\u6362\u5230\u5E94\u7528 ${args["appId"]}`, mcp.sessionId);
|
|
382
|
+
return;
|
|
383
|
+
case "vue-devtools_get_current_route":
|
|
384
|
+
case "vue-devtools_get_routes": {
|
|
385
|
+
const parsed = typeof result === "string" ? parseJsonSafe(result) : result;
|
|
386
|
+
const data = parsed ?? {};
|
|
387
|
+
const value = toolName === "vue-devtools_get_current_route" ? data.currentRoute ?? null : data.routes ?? null;
|
|
388
|
+
sendMcpResult(res, id, JSON.stringify(value), mcp.sessionId);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
case "vue-devtools_get_component_render_code":
|
|
392
|
+
sendMcpResult(
|
|
393
|
+
res,
|
|
394
|
+
id,
|
|
395
|
+
typeof result === "string" ? result : JSON.stringify(result),
|
|
396
|
+
mcp.sessionId
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
default:
|
|
400
|
+
sendMcpResult(res, id, JSON.stringify(result), mcp.sessionId);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
} catch (e) {
|
|
404
|
+
log.debug("handleVueDevtoolsTool error", { error: e.message });
|
|
405
|
+
sendMcpError(res, id, -32603, `Vue DevTools \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function parseJsonSafe(text) {
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(text);
|
|
411
|
+
} catch {
|
|
412
|
+
return text;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function handleViteLogsTool(res, id, args, sessionId) {
|
|
416
|
+
const { level, limit, source } = args;
|
|
417
|
+
const buffer = getProcessLogBuffer();
|
|
418
|
+
const logs = buffer.getLogs({
|
|
419
|
+
level: parseProcessLevelFilter(level),
|
|
420
|
+
limit: typeof limit === "number" && limit >= 1 ? limit : 50,
|
|
421
|
+
source: typeof source === "string" && source ? source : void 0
|
|
422
|
+
});
|
|
423
|
+
if (logs.length === 0) {
|
|
424
|
+
sendMcpResult(
|
|
425
|
+
res,
|
|
426
|
+
id,
|
|
427
|
+
`\u5F53\u524D\u6CA1\u6709\u7B26\u5408\u6761\u4EF6\u7684\u65E5\u5FD7\uFF08\u7F13\u51B2\u533A\u5171 ${buffer.size()} \u6761\uFF09\u3002
|
|
428
|
+
|
|
429
|
+
\u5EFA\u8BAE\uFF1A
|
|
430
|
+
- \u4E0D\u6307\u5B9A\u53C2\u6570\u83B7\u53D6\u6240\u6709\u65E5\u5FD7
|
|
431
|
+
- \u4F7F\u7528 level=error,warn \u83B7\u53D6\u9519\u8BEF\u548C\u8B66\u544A`,
|
|
432
|
+
sessionId
|
|
433
|
+
);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const formattedLogs = logs.map((entry) => {
|
|
437
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
438
|
+
const levelIcon = entry.level === "error" ? "\u274C" : entry.level === "warn" ? "\u26A0\uFE0F" : entry.level === "info" ? "\u2139\uFE0F" : "";
|
|
439
|
+
return `${time} ${levelIcon} ${entry.message}`;
|
|
440
|
+
}).join("\n");
|
|
441
|
+
sendMcpResult(
|
|
442
|
+
res,
|
|
443
|
+
id,
|
|
444
|
+
`Vite \u5F00\u53D1\u670D\u52A1\u5668\u65E5\u5FD7\uFF08${logs.length}/${buffer.size()} \u6761\uFF09\uFF1A
|
|
445
|
+
|
|
446
|
+
${formattedLogs}`,
|
|
447
|
+
sessionId
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
function parseProcessLevelFilter(level) {
|
|
451
|
+
if (typeof level !== "string" || !level.trim()) return void 0;
|
|
452
|
+
const levels = level.split(",").map((l) => l.trim()).filter(Boolean);
|
|
453
|
+
return levels.length ? levels : void 0;
|
|
454
|
+
}
|
|
455
|
+
function buildServiceLogTools(logFiles) {
|
|
456
|
+
return logFiles.map((cfg) => ({
|
|
457
|
+
name: `logs-devtools_${cfg.name}_logs`,
|
|
458
|
+
description: `\u83B7\u53D6 ${cfg.name} \u7684\u65E5\u5FD7\u3002
|
|
459
|
+
|
|
460
|
+
**\u4F55\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177**\uFF1A
|
|
461
|
+
${cfg.description}
|
|
462
|
+
|
|
463
|
+
**\u65E5\u5FD7\u5185\u5BB9**\uFF1A
|
|
464
|
+
- \u6765\u81EA\u65E5\u5FD7\u6587\u4EF6 ${cfg.path} \u7684\u5B9E\u65F6\u65E5\u5FD7
|
|
465
|
+
- \u9ED8\u8BA4\u8FD4\u56DE\u6700\u8FD1 200 \u884C\u65E5\u5FD7`,
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
level: {
|
|
470
|
+
type: "string",
|
|
471
|
+
description: "\u65E5\u5FD7\u7EA7\u522B\u8FC7\u6EE4\uFF1Aerror(\u9519\u8BEF)\u3001warn(\u8B66\u544A)\u3001info(\u4FE1\u606F)\u3002\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF0C\u5982 'error,warn'"
|
|
472
|
+
},
|
|
473
|
+
limit: {
|
|
474
|
+
type: "integer",
|
|
475
|
+
minimum: 1,
|
|
476
|
+
maximum: 200,
|
|
477
|
+
default: 50,
|
|
478
|
+
description: "\u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 50\uFF0C\u6700\u5927 200"
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
function isServiceLogTool(toolName, logFiles) {
|
|
485
|
+
return logFiles.some((cfg) => `logs-devtools_${cfg.name}_logs` === toolName);
|
|
486
|
+
}
|
|
487
|
+
async function handleServiceLogsTool(res, id, toolName, args, logFiles, projectRoot, sessionId) {
|
|
488
|
+
try {
|
|
489
|
+
const cfg = logFiles.find((c) => `logs-devtools_${c.name}_logs` === toolName);
|
|
490
|
+
if (!cfg) {
|
|
491
|
+
sendMcpError(res, id, -32601, `Tool not found: ${toolName}`, sessionId);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const { level, limit } = args;
|
|
495
|
+
const requestedLimit = typeof limit === "number" ? limit : 50;
|
|
496
|
+
const entries = await readLogFileTail({
|
|
497
|
+
name: cfg.name,
|
|
498
|
+
filePath: cfg.path,
|
|
499
|
+
projectRoot,
|
|
500
|
+
lines: Math.max(requestedLimit * 3, 500),
|
|
501
|
+
level: parseFileLevelFilter(level),
|
|
502
|
+
limit: requestedLimit
|
|
503
|
+
});
|
|
504
|
+
if (entries.length === 0) {
|
|
505
|
+
sendMcpResult(
|
|
506
|
+
res,
|
|
507
|
+
id,
|
|
508
|
+
`\u5F53\u524D\u6CA1\u6709\u7B26\u5408\u6761\u4EF6\u7684\u65E5\u5FD7\u3002
|
|
509
|
+
|
|
510
|
+
\u5EFA\u8BAE\uFF1A
|
|
511
|
+
- \u4E0D\u6307\u5B9A\u53C2\u6570\u83B7\u53D6\u6240\u6709\u65E5\u5FD7
|
|
512
|
+
- \u4F7F\u7528 level=error,warn \u83B7\u53D6\u9519\u8BEF\u548C\u8B66\u544A`,
|
|
513
|
+
sessionId
|
|
514
|
+
);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const formattedLogs = entries.map((entry) => {
|
|
518
|
+
const levelIcon = entry.level === "error" ? "\u274C" : entry.level === "warn" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
519
|
+
return `${levelIcon} ${entry.message}`;
|
|
520
|
+
}).join("\n");
|
|
521
|
+
sendMcpResult(
|
|
522
|
+
res,
|
|
523
|
+
id,
|
|
524
|
+
`${cfg.name} \u65E5\u5FD7\uFF08${entries.length} \u6761\uFF09\uFF1A
|
|
525
|
+
|
|
526
|
+
${formattedLogs}`,
|
|
527
|
+
sessionId
|
|
528
|
+
);
|
|
529
|
+
} catch (e) {
|
|
530
|
+
log.debug("handleServiceLogsTool error", { error: e.message });
|
|
531
|
+
sendMcpError(res, id, -32603, `\u8BFB\u53D6\u65E5\u5FD7\u5931\u8D25: ${e.message}`, sessionId);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function parseFileLevelFilter(level) {
|
|
535
|
+
if (typeof level !== "string" || !level.trim()) return void 0;
|
|
536
|
+
const levels = level.split(",").map((l) => l.trim()).filter(Boolean);
|
|
537
|
+
return levels.length ? levels : void 0;
|
|
538
|
+
}
|
|
539
|
+
async function handleForward(res, body, mcp) {
|
|
540
|
+
try {
|
|
541
|
+
const responseText = await mcp.forward(body);
|
|
542
|
+
sendMcpJson(res, 200, responseText, mcp.sessionId);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
log.debug("handleForward error", { error: e.message });
|
|
545
|
+
sendMcpError(res, null, -32603, `MCP \u8C03\u7528\u5931\u8D25: ${e.message}`, mcp.sessionId);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function sendMcpResult(res, id, text, sessionId) {
|
|
549
|
+
sendMcpJson(
|
|
550
|
+
res,
|
|
551
|
+
200,
|
|
552
|
+
{
|
|
553
|
+
jsonrpc: "2.0",
|
|
554
|
+
id,
|
|
555
|
+
result: { content: [{ type: "text", text }] }
|
|
556
|
+
},
|
|
557
|
+
sessionId
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
function sendMcpError(res, id, code, message, sessionId) {
|
|
561
|
+
sendMcpJson(
|
|
562
|
+
res,
|
|
563
|
+
200,
|
|
564
|
+
{
|
|
565
|
+
jsonrpc: "2.0",
|
|
566
|
+
id,
|
|
567
|
+
error: { code, message }
|
|
568
|
+
},
|
|
569
|
+
sessionId
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
function sendMcpJson(res, statusCode, body, sessionId) {
|
|
573
|
+
const headers = { "Content-Type": "application/json" };
|
|
574
|
+
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
575
|
+
res.writeHead(statusCode, headers);
|
|
576
|
+
res.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
577
|
+
}
|
|
578
|
+
function readBody(req, maxSize = 1024 * 1024) {
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
580
|
+
let body = "";
|
|
581
|
+
req.on("data", (chunk) => {
|
|
582
|
+
body += chunk.toString();
|
|
583
|
+
if (body.length > maxSize) {
|
|
584
|
+
req.destroy();
|
|
585
|
+
reject(new Error("Request body too large"));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
req.on("end", () => resolve(body));
|
|
589
|
+
req.on("error", reject);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
function tryParseRequest(body) {
|
|
593
|
+
try {
|
|
594
|
+
const data = JSON.parse(body);
|
|
595
|
+
return { method: data.method || "unknown", id: data.id ?? null };
|
|
596
|
+
} catch {
|
|
597
|
+
return { method: "unknown", id: null };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function tryParseParams(body) {
|
|
601
|
+
try {
|
|
602
|
+
return JSON.parse(body).params ?? null;
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
export {
|
|
608
|
+
MCP_API_PATH,
|
|
609
|
+
setupMcpEndpoint
|
|
610
|
+
};
|