@gecho-ai/gecho-bridge 1.1.1 → 1.1.3
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/index.js +4 -4
- package/mcp-client.js +90 -25
- package/package.json +1 -1
- package/server.js +68 -12
package/index.js
CHANGED
|
@@ -148,9 +148,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
148
148
|
const timeoutId = setTimeout(() => {
|
|
149
149
|
if (pendingRequests.has(requestId)) {
|
|
150
150
|
pendingRequests.delete(requestId);
|
|
151
|
-
resolve({ error: "抓取超时 (
|
|
151
|
+
resolve({ error: "抓取超时 (300s),请检查浏览器是否已停止滚动" });
|
|
152
152
|
}
|
|
153
|
-
},
|
|
153
|
+
}, 300000);
|
|
154
154
|
|
|
155
155
|
pendingRequests.set(requestId, { resolve, reject, timeoutId });
|
|
156
156
|
|
|
@@ -243,8 +243,8 @@ async function run() {
|
|
|
243
243
|
const result = await new Promise((resolve) => {
|
|
244
244
|
const timeoutId = setTimeout(() => {
|
|
245
245
|
pendingRequests.delete(requestId);
|
|
246
|
-
resolve({ error: "抓取超时 (
|
|
247
|
-
},
|
|
246
|
+
resolve({ error: "抓取超时 (300s)" });
|
|
247
|
+
}, 300000);
|
|
248
248
|
|
|
249
249
|
pendingRequests.set(requestId, { resolve, timeoutId });
|
|
250
250
|
|
package/mcp-client.js
CHANGED
|
@@ -19,7 +19,10 @@ const http = require("http");
|
|
|
19
19
|
const { spawn } = require("child_process");
|
|
20
20
|
const path = require("path");
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const SERVICE_BASE_URL = "http://127.0.0.1:18793";
|
|
23
|
+
const HTTP_SERVICE_URL = `${SERVICE_BASE_URL}/search`;
|
|
24
|
+
const PING_URL = `${SERVICE_BASE_URL}/ping`;
|
|
25
|
+
const SHUTDOWN_URL = `${SERVICE_BASE_URL}/shutdown`;
|
|
23
26
|
const SERVICE_PATH = path.join(__dirname, "server.js");
|
|
24
27
|
|
|
25
28
|
const server = new Server(
|
|
@@ -32,7 +35,7 @@ const server = new Server(
|
|
|
32
35
|
*/
|
|
33
36
|
function checkServiceAlive() {
|
|
34
37
|
return new Promise((resolve) => {
|
|
35
|
-
const req = http.get(
|
|
38
|
+
const req = http.get(PING_URL, (res) => {
|
|
36
39
|
resolve(res.statusCode === 200);
|
|
37
40
|
});
|
|
38
41
|
req.on("error", () => resolve(false));
|
|
@@ -41,31 +44,65 @@ function checkServiceAlive() {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
|
-
*
|
|
47
|
+
* 请求旧服务优雅退出
|
|
45
48
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
function requestShutdown() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const req = http.request(SHUTDOWN_URL, { method: "POST" }, (res) => {
|
|
52
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
53
|
+
});
|
|
54
|
+
req.on("error", () => resolve(false));
|
|
55
|
+
req.setTimeout(1000, () => {
|
|
56
|
+
req.destroy();
|
|
57
|
+
resolve(false);
|
|
58
|
+
});
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
51
62
|
|
|
63
|
+
function startServiceDetached() {
|
|
52
64
|
const child = spawn("node", [SERVICE_PATH], {
|
|
53
65
|
detached: true,
|
|
54
66
|
stdio: "ignore" // 静默启动,不占用当前终端
|
|
55
67
|
});
|
|
56
|
-
|
|
57
68
|
child.unref(); // 让子进程独立运行,父进程退出时不影响它
|
|
69
|
+
}
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
let retries =
|
|
71
|
+
async function waitForServiceDown() {
|
|
72
|
+
let retries = 10;
|
|
61
73
|
while (retries > 0) {
|
|
62
|
-
await new Promise(r => setTimeout(r,
|
|
74
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
75
|
+
if (!(await checkServiceAlive())) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
retries--;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForServiceUp() {
|
|
84
|
+
let retries = 8;
|
|
85
|
+
while (retries > 0) {
|
|
86
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
63
87
|
if (await checkServiceAlive()) {
|
|
64
|
-
return;
|
|
88
|
+
return true;
|
|
65
89
|
}
|
|
66
90
|
retries--;
|
|
67
91
|
}
|
|
68
|
-
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 启动即接管:每次启动 MCP 时先尝试关闭旧服务,再拉起当前服务
|
|
97
|
+
*/
|
|
98
|
+
async function ensureServiceRunning() {
|
|
99
|
+
await requestShutdown();
|
|
100
|
+
await waitForServiceDown();
|
|
101
|
+
startServiceDetached();
|
|
102
|
+
const ready = await waitForServiceUp();
|
|
103
|
+
if (!ready) {
|
|
104
|
+
throw new Error("Failed to start Service Layer.");
|
|
105
|
+
}
|
|
69
106
|
}
|
|
70
107
|
|
|
71
108
|
// 1. 定义工具
|
|
@@ -73,8 +110,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
73
110
|
return {
|
|
74
111
|
tools: [
|
|
75
112
|
{
|
|
76
|
-
name: "
|
|
77
|
-
description: "在 TikTok
|
|
113
|
+
name: "tiktok_search",
|
|
114
|
+
description: "在 TikTok 上搜索关键词,自动滚动加载结果并返回。",
|
|
78
115
|
inputSchema: {
|
|
79
116
|
type: "object",
|
|
80
117
|
properties: {
|
|
@@ -83,6 +120,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
83
120
|
},
|
|
84
121
|
required: ["query"]
|
|
85
122
|
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "tiktok_insight",
|
|
126
|
+
description: "在 TikTok 搜索的基础上进行商机洞察和趋势分析。",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
query: { type: "string", description: "搜索关键词 (例如: '户外野餐垫')" },
|
|
131
|
+
save_dir: { type: "string", description: "可选的保存目录绝对路径 (例如: '/Users/xxx/data')" }
|
|
132
|
+
},
|
|
133
|
+
required: ["query"]
|
|
134
|
+
}
|
|
86
135
|
}
|
|
87
136
|
]
|
|
88
137
|
};
|
|
@@ -90,9 +139,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
90
139
|
|
|
91
140
|
// 2. 转发工具请求到 Service 层
|
|
92
141
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
const toolName = request.params.name;
|
|
143
|
+
const args = request.params.arguments;
|
|
95
144
|
|
|
145
|
+
// 只要是 tiktok_ 开头的工具,都走通用转发逻辑
|
|
146
|
+
if (toolName.startsWith("tiktok_") || toolName.startsWith("x_") || toolName.startsWith("ins_")) {
|
|
96
147
|
try {
|
|
97
148
|
const requestService = () => new Promise((resolve, reject) => {
|
|
98
149
|
const req = http.request(HTTP_SERVICE_URL, {
|
|
@@ -117,7 +168,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
117
168
|
});
|
|
118
169
|
|
|
119
170
|
req.on("error", () => reject(new Error("Service Layer communication error")));
|
|
120
|
-
|
|
171
|
+
|
|
172
|
+
// 【核心改动】:直接将工具名作为 action,所有参数作为 payload 透传
|
|
173
|
+
req.write(JSON.stringify({
|
|
174
|
+
action: toolName,
|
|
175
|
+
...args
|
|
176
|
+
}));
|
|
121
177
|
req.end();
|
|
122
178
|
});
|
|
123
179
|
|
|
@@ -138,26 +194,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
138
194
|
return { content: [{ type: "text", text: `❌ 错误: ${serviceResponse.error}` }], isError: true };
|
|
139
195
|
}
|
|
140
196
|
|
|
141
|
-
const result = serviceResponse.data
|
|
197
|
+
const result = serviceResponse.data;
|
|
198
|
+
if (typeof result === 'object' && result !== null && result.error) {
|
|
199
|
+
return { content: [{ type: "text", text: `❌ 业务错误: ${result.error}` }], isError: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!Array.isArray(result)) {
|
|
203
|
+
return { content: [{ type: "text", text: `❌ 异常: 插件未返回数组格式的结果` }], isError: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
142
206
|
const savePath = serviceResponse.savePath || "";
|
|
143
207
|
const saveLine = savePath
|
|
144
|
-
? `📂
|
|
145
|
-
:
|
|
208
|
+
? `📂 数据已存: ${savePath}\n\n`
|
|
209
|
+
: "";
|
|
210
|
+
|
|
146
211
|
const top20 = result.slice(0, 20);
|
|
147
212
|
return {
|
|
148
213
|
content: [
|
|
149
214
|
{
|
|
150
215
|
type: "text",
|
|
151
|
-
text: `✅
|
|
216
|
+
text: `✅ [${toolName}] 执行成功,共 ${result.length} 条数据。\n` +
|
|
152
217
|
saveLine +
|
|
153
|
-
|
|
218
|
+
`以下是部分结果展示:\n` +
|
|
154
219
|
JSON.stringify(top20, null, 2)
|
|
155
220
|
}
|
|
156
221
|
]
|
|
157
222
|
};
|
|
158
223
|
} catch (e) {
|
|
159
224
|
return {
|
|
160
|
-
content: [{ type: "text", text: `❌
|
|
225
|
+
content: [{ type: "text", text: `❌ 链路故障: ${e.message}.` }],
|
|
161
226
|
isError: true
|
|
162
227
|
};
|
|
163
228
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const HTTP_PORT = 18793;
|
|
|
19
19
|
let extensionSocket = null;
|
|
20
20
|
const pendingRequests = new Map();
|
|
21
21
|
let requestIdCounter = 1;
|
|
22
|
+
let shuttingDown = false;
|
|
22
23
|
|
|
23
24
|
function decodeBase64Utf8(value) {
|
|
24
25
|
try {
|
|
@@ -64,6 +65,36 @@ function toSafeFileName(name) {
|
|
|
64
65
|
return reserved.has(upper) ? `${candidate}_` : candidate;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function gracefulShutdown(reason) {
|
|
69
|
+
if (shuttingDown) return;
|
|
70
|
+
shuttingDown = true;
|
|
71
|
+
console.log(`🛑 Service shutting down: ${reason}`);
|
|
72
|
+
|
|
73
|
+
for (const [_requestId, pending] of pendingRequests) {
|
|
74
|
+
clearTimeout(pending.timeoutId);
|
|
75
|
+
pending.resolve({ error: "Service is shutting down" });
|
|
76
|
+
}
|
|
77
|
+
pendingRequests.clear();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (extensionSocket && extensionSocket.readyState === 1) {
|
|
81
|
+
extensionSocket.close(1001, "service_shutdown");
|
|
82
|
+
}
|
|
83
|
+
} catch (_e) {}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
wss.close(() => {
|
|
87
|
+
server.close(() => process.exit(0));
|
|
88
|
+
});
|
|
89
|
+
} catch (_e) {
|
|
90
|
+
try {
|
|
91
|
+
server.close(() => process.exit(0));
|
|
92
|
+
} catch (__e) {
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
67
98
|
// --- WebSocket Server (与插件通信) ---
|
|
68
99
|
const wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
|
|
69
100
|
|
|
@@ -112,16 +143,28 @@ const server = http.createServer(async (req, res) => {
|
|
|
112
143
|
return res.end(JSON.stringify({ status: "ok" }));
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
if (req.method === "POST" && req.url === "/
|
|
146
|
+
if (req.method === "POST" && req.url === "/shutdown") {
|
|
147
|
+
res.end(JSON.stringify({ status: "ok", message: "shutdown accepted" }));
|
|
148
|
+
setTimeout(() => gracefulShutdown("remote_shutdown"), 20).unref?.();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (req.method === "POST" && (req.url === "/search" || req.url === "/action")) {
|
|
153
|
+
if (shuttingDown) {
|
|
154
|
+
res.statusCode = 503;
|
|
155
|
+
return res.end(JSON.stringify({ error: "Service is shutting down" }));
|
|
156
|
+
}
|
|
157
|
+
|
|
116
158
|
let body = "";
|
|
117
159
|
req.on("data", chunk => { body += chunk; });
|
|
118
160
|
req.on("end", async () => {
|
|
119
161
|
try {
|
|
120
162
|
const payload = JSON.parse(body);
|
|
121
|
-
const
|
|
122
|
-
|
|
163
|
+
const action = payload.action;
|
|
164
|
+
|
|
165
|
+
if (!action) {
|
|
123
166
|
res.statusCode = 400;
|
|
124
|
-
return res.end(JSON.stringify({ error: "Missing
|
|
167
|
+
return res.end(JSON.stringify({ error: "Missing action" }));
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
if (!extensionSocket || extensionSocket.readyState !== 1) {
|
|
@@ -129,20 +172,31 @@ const server = http.createServer(async (req, res) => {
|
|
|
129
172
|
return res.end(JSON.stringify({ error: "Extension not connected" }));
|
|
130
173
|
}
|
|
131
174
|
|
|
132
|
-
console.log(
|
|
175
|
+
console.log(`🚀 Dispatching action: [${action}]`);
|
|
133
176
|
const requestId = `svc-${Date.now()}-${requestIdCounter++}`;
|
|
134
177
|
|
|
135
178
|
const result = await new Promise((resolve) => {
|
|
136
179
|
const timeoutId = setTimeout(() => {
|
|
137
180
|
pendingRequests.delete(requestId);
|
|
138
|
-
resolve({ error:
|
|
139
|
-
},
|
|
181
|
+
resolve({ error: `Scraping timeout (300s) for action: ${action}` });
|
|
182
|
+
}, 300000);
|
|
140
183
|
|
|
141
184
|
pendingRequests.set(requestId, { resolve, timeoutId });
|
|
142
185
|
|
|
186
|
+
// 通用透传逻辑:将 payload 中的所有参数(除去 action)作为 params 传给插件
|
|
187
|
+
const { action: _a, ...params } = payload;
|
|
188
|
+
|
|
189
|
+
// 🐷 兼容层:如果插件版本较旧,识别不了 tiktok_search,则映射回 search
|
|
190
|
+
let finalAction = action;
|
|
191
|
+
if (action === "tiktok_search") finalAction = "search";
|
|
192
|
+
if (action === "tiktok_insight") finalAction = "search";
|
|
193
|
+
|
|
143
194
|
extensionSocket.send(JSON.stringify({
|
|
144
195
|
method: "execute_action",
|
|
145
|
-
params: {
|
|
196
|
+
params: {
|
|
197
|
+
action: finalAction,
|
|
198
|
+
params: params
|
|
199
|
+
},
|
|
146
200
|
requestId: requestId
|
|
147
201
|
}));
|
|
148
202
|
});
|
|
@@ -151,11 +205,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
151
205
|
let savePath = "";
|
|
152
206
|
let saveWarning = "";
|
|
153
207
|
if (Array.isArray(result) && result.length > 0) {
|
|
154
|
-
|
|
155
|
-
const dataDir = payload.save_dir || process.env.GECHO_DATA_DIR || path.join(__dirname, "..", "data");
|
|
208
|
+
const dataDir = payload.save_dir || process.env.GECHO_DATA_DIR || path.join(__dirname, "data");
|
|
156
209
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
157
|
-
const safeName = toSafeFileName(query);
|
|
158
|
-
const fixedPath = path.join(dataDir, `${safeName}
|
|
210
|
+
const safeName = toSafeFileName(params.query || action);
|
|
211
|
+
const fixedPath = path.join(dataDir, `${safeName}_results.json`);
|
|
159
212
|
try {
|
|
160
213
|
fs.writeFileSync(fixedPath, JSON.stringify(result, null, 2), "utf8");
|
|
161
214
|
savePath = fixedPath;
|
|
@@ -180,3 +233,6 @@ server.listen(HTTP_PORT, "127.0.0.1", () => {
|
|
|
180
233
|
console.log(` - WebSocket (Extension): ws://127.0.0.1:${WS_PORT}`);
|
|
181
234
|
console.log(` - HTTP API (Client): http://127.0.0.1:${HTTP_PORT}`);
|
|
182
235
|
});
|
|
236
|
+
|
|
237
|
+
process.on("SIGTERM", () => gracefulShutdown("sigterm"));
|
|
238
|
+
process.on("SIGINT", () => gracefulShutdown("sigint"));
|