@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 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: "抓取超时 (120s),请检查浏览器是否已停止滚动" });
151
+ resolve({ error: "抓取超时 (300s),请检查浏览器是否已停止滚动" });
152
152
  }
153
- }, 120000);
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: "抓取超时 (120s)" });
247
- }, 120000);
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 HTTP_SERVICE_URL = "http://127.0.0.1:18793/search";
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("http://127.0.0.1:18793/ping", (res) => {
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
- * 自动拉起 Service 层
47
+ * 请求旧服务优雅退出
45
48
  */
46
- async function ensureServiceRunning() {
47
- const alive = await checkServiceAlive();
48
- if (alive) {
49
- return;
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 = 5;
71
+ async function waitForServiceDown() {
72
+ let retries = 10;
61
73
  while (retries > 0) {
62
- await new Promise(r => setTimeout(r, 1000));
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
- throw new Error("Failed to start Service Layer after 5s.");
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: "tiktok_search_top_200",
77
- description: "在 TikTok 上搜索关键词,自动滚动加载至少 200 条结果,并全部返回。",
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
- if (request.params.name === "tiktok_search_top_200") {
94
- const { query, save_dir } = request.params.arguments;
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
- req.write(JSON.stringify({ query, save_dir }));
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
- ? `📂 完整结果已保存到: ${savePath}\n\n`
145
- : `📂 未返回保存路径,请检查服务端日志。\n\n`;
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: `✅ 抓取完成!共获取 ${result.length} 条数据。\n` +
216
+ text: `✅ [${toolName}] 执行成功,共 ${result.length} 条数据。\n` +
152
217
  saveLine +
153
- `以下是点赞最高的前 20 条结果:\n` +
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: `❌ 转发失败: ${e.message}.` }],
225
+ content: [{ type: "text", text: `❌ 链路故障: ${e.message}.` }],
161
226
  isError: true
162
227
  };
163
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gecho-ai/gecho-bridge",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "MCP Bridge for TikTok Extension Relay - Connecting LLMs to TikTok via Chrome Extension",
5
5
  "main": "mcp-client.js",
6
6
  "bin": {
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 === "/search") {
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 query = normalizeQuery(payload.query, payload.query_b64);
122
- if (!query) {
163
+ const action = payload.action;
164
+
165
+ if (!action) {
123
166
  res.statusCode = 400;
124
- return res.end(JSON.stringify({ error: "Missing query" }));
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(`🔍 Processing search request for: [${query}]`);
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: "Scraping timeout (120s)" });
139
- }, 120000);
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: { action: "search", params: { query } },
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
- // 优先级: 1. 请求中传来的 save_dir 2. 环境变量 GECHO_DATA_DIR 3. 默认 data 目录
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}_search_results.json`);
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"));