@gecho-ai/gecho-bridge 1.1.4 → 1.1.6

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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@gecho-ai/gecho-bridge",
3
+ "version": "1.1.6",
4
+ "description": "TikTok Search Tool for OpenClaw"
5
+ }
package/.mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "gecho-tiktok-search": {
4
+ "command": "node",
5
+ "args": ["./mcp-client.js"],
6
+ "timeout": 600000,
7
+ "retries": 0
8
+ }
9
+ }
10
+ }
package/README.md CHANGED
@@ -1,29 +1,56 @@
1
1
  # Gecho Bridge 🚀
2
2
 
3
- Gecho Bridge is an MCP (Model Context Protocol) server that connects AI Agents (like Claude Desktop, Cursor, or OpenClaw) to TikTok via a Chrome extension. It allows your AI models to search and retrieve TikTok video metadata directly from the browser.
3
+ 🌐 **Gecho Bridge** 是一款通用的 MCPModel Context Protocol)工具,旨在为你的大语言模型(LLM)与本地浏览器之间搭建一座桥梁。
4
+ 安装后,无论是使用 **OpenClaw**、**Hermes** 还是 **Trae**,你的 AI 助手都能直接控制浏览器,自动化完成 TikTok 搜索、数据抓取与深度商机洞察。
4
5
 
5
- ## Features
6
+ ---
6
7
 
7
- - **Automated Search**: Search TikTok with keywords and auto-scroll to fetch results.
8
- - **Data Retrieval**: Get video IDs, titles, like counts, and play URLs.
9
- - **Dual-Layer Architecture**: A lightweight MCP Client that automatically manages a persistent Service Layer.
10
- - **Lazy Start**: The bridge automatically starts the background service when needed.
8
+ ## 适合谁用
11
9
 
12
- ## Installation
10
+ - 📊 **分析竞品**:输入关键词,快速获取 TikTok 前排高赞视频的各项互动数据。
11
+ - 💡 **寻找爆款**:使用深度洞察工具分析特定品类(如 "portable blender")的流行趋势,捕捉未被满足的市场蓝海。
12
+ - 🤖 **自动化运营**:让大模型直接指挥浏览器,自动翻页抓取并生成数据报表,免去繁琐的手动统计。
13
13
 
14
- ### 1. Install via npm
15
- ```bash
16
- npm install -g @gecho-ai/gecho-bridge
17
- ```
14
+ ## 🚀 能做什么
15
+
16
+ - 自动唤起 Chrome 并在 TikTok 搜索指定关键词,模拟真人自然滚动加载。
17
+ - 抓取海量结构化数据(视频 ID、标题、点赞量、播放链接等)并自动安全落盘为 JSON 文件。
18
+ - 执行基于大数据检索的**异步深度洞察**,智能提炼爆款趋势与潜在商机。
19
+
20
+ ## 🔗 相关链接
18
21
 
19
- ### 2. Install Chrome Extension
20
- Ensure you have the Gecho TikTok Extension installed and active in your Chrome browser. The bridge communicates with this extension via WebSocket.
22
+ - **官网**:[https://gecho.ai/](https://gecho.ai/)
23
+ - **ClawHub 插件页**:[https://clawhub.ai/plugins/gecho-bridge](https://clawhub.ai/plugins/gecho-bridge)
24
+ - **Chrome 浏览器扩展**:[前往 Chrome 网上应用店下载](https://chromewebstore.google.com/detail/pjkaeenpekolahdbccjfenjcmanemlbj?utm_source=item-share-cb)
21
25
 
22
- ## Configuration
26
+ ---
23
27
 
24
- ### Claude Desktop
25
- Add the following to your `claude_desktop_config.json`:
28
+ ## 📦 安装与配置
26
29
 
30
+ 本项目基于标准 MCP 协议开发,可以无缝接入任何支持 MCP 的 AI 客户端(如 OpenClaw、Hermes、Trae 等)。
31
+
32
+ ### 0. 前置环境要求
33
+ 1. **Node.js**:>= 18(需支持 `npm` / `npx`)。
34
+ 2. **浏览器扩展**:请先[点击此处安装 Gecho 浏览器扩展](https://chromewebstore.google.com/detail/pjkaeenpekolahdbccjfenjcmanemlbj?utm_source=item-share-cb)。
35
+ 3. **网络与状态**:确保本地网络可稳定访问 TikTok,并在浏览器中登录账号,保持扩展在线。
36
+
37
+ ### 方式一:在 OpenClaw 中一键安装 (ClawHub)
38
+ ```bash
39
+ openclaw plugins install @gecho-ai/gecho-bridge
40
+ openclaw gateway restart
41
+ ```
42
+ *如需升级已安装的版本,使用 `openclaw plugins update @gecho-ai/gecho-bridge` 即可。*
43
+
44
+ ### 方式二:在 Hermes 中一键配置 (Hermes Skill Hub)
45
+ 你可以通过以下命令将服务快捷添加到 Hermes 并重启:
46
+ ```bash
47
+ hermes mcp add gecho-bridge --command npx --args="-y" --args="@gecho-ai/gecho-bridge@latest"
48
+ hermes restart
49
+ ```
50
+ *重启后,可通过 `hermes mcp list` 检查安装状态。*
51
+
52
+ ### 方式三:在 Trae / Claude Desktop 等通用客户端配置
53
+ 在支持手动配置的 MCP 客户端中,打开对应的 `mcp.json` 或 `claude_desktop_config.json` 文件,添加如下节点:
27
54
  ```json
28
55
  {
29
56
  "mcpServers": {
@@ -35,75 +62,84 @@ Add the following to your `claude_desktop_config.json`:
35
62
  }
36
63
  ```
37
64
 
38
- ### Cursor
39
- Go to Settings -> MCP.
40
- Add a new MCP server:
41
- - **Name**: gecho-bridge
42
- - **Type**: command
43
- - **Command**: `npx -y @gecho-ai/gecho-bridge@latest`
65
+ ---
44
66
 
45
- ### Save Path Configuration
46
- Search results are persisted to a JSON file. You can control the save directory in two ways:
67
+ ## 🏁 快速开始与常见流程
47
68
 
48
- 1. Pass `save_dir` in the MCP tool call arguments.
49
- 2. Set environment variable `GECHO_DATA_DIR` for the MCP process.
69
+ 环境配置完毕并重启 AI 客户端后,你可以直接通过自然语言向 AI 下达指令。
50
70
 
51
- Priority order used by the service:
71
+ ### 🔍 基础搜索 (`tiktok_search`)
72
+ 适用于快速检索和收集视频数据。
73
+ **你可以这样说:**
74
+ - *"帮我搜索 TikTok 关键词 'portable blender',返回点赞最高的前 10 条"*
75
+ - *"搜索 'cat toy',并把完整结果保存到 /Users/yourname/gecho-data"*
52
76
 
53
- 1. `save_dir` from request arguments
54
- 2. `GECHO_DATA_DIR` environment variable
55
- 3. Default `data` directory (`path.join(__dirname, "..", "data")`)
77
+ **执行流程:**
78
+ 1. AI 唤起本地浏览器的 Gecho 扩展进行搜索和自动滚动。
79
+ 2. 抓取完成后,海量数据会自动落盘到本地。
80
+ 3. AI 会在会话中为你精简总结前 20 条高赞结果。
56
81
 
57
- File naming:
82
+ ### 📈 深度洞察 (`tiktok_insight`)
83
+ 适用于品类调研和趋势分析。
84
+ **你可以这样说:**
85
+ - *"请对 'outdoor picnic mat' 做 tiktok_insight 分析"*
86
+ - *"对比 'desk setup' 和 'minimal desk' 的热视频风格与互动量"*
58
87
 
59
- - Result file pattern: `<query>_search_results.json`
60
- - Query text is sanitized to a safe file name before writing.
88
+ **执行流程:**
89
+ 1. 插件会下发异步洞察任务,并立即返回一个 `jobId`。
90
+ 2. **⚠️ 高危注意**:洞察分析涉及深度抓取和 AI 运算,耗时通常会**超过 5 分钟**。在执行期间,**请务必不要关闭浏览器插件或相关的 TikTok 页面**。
91
+ 3. 等待一段时间后,对 AI 说:*“用 check_insight_status 查询刚才任务的执行状态”* 来获取最终的分析报告。
61
92
 
62
- Example (Claude Desktop with `GECHO_DATA_DIR`):
93
+ ---
63
94
 
64
- ```json
65
- {
66
- "mcpServers": {
67
- "gecho-bridge": {
68
- "command": "npx",
69
- "args": ["-y", "@gecho-ai/gecho-bridge@latest"],
70
- "env": {
71
- "GECHO_DATA_DIR": "/Users/yourname/gecho-data"
72
- }
73
- }
74
- }
75
- }
76
- ```
95
+ ## ⚙️ 存储配置
96
+
97
+ 为了更好地管理数据资产,抓取的海量结果需要落盘保存。插件支持以下优先级配置方式:
98
+
99
+ 1. **会话级(最高优先级)**:直接在对话时让 AI 指定 `save_dir`(必须为绝对路径)。
100
+ 2. **全局级**:配置环境变量 `GECHO_DATA_DIR` 来指定默认的数据保存目录。
101
+ 3. **默认回退**:若未指定,默认保存在工具自带的 `./data` 目录下。
77
102
 
78
- Example (Cursor command with env):
103
+ *(注:所有保存的文件名均会自动进行安全化处理,避免非法字符导致写入失败。)*
79
104
 
105
+ ---
106
+
107
+ ## 🛠️ 排障指南
108
+
109
+ ### 1. 如何确认插件已加载?(以 OpenClaw 为例)
110
+ 执行:
80
111
  ```bash
81
- GECHO_DATA_DIR=/Users/yourname/gecho-data npx -y @gecho-ai/gecho-bridge@latest
112
+ openclaw plugins info @gecho-ai/gecho-bridge
82
113
  ```
114
+ 如果安装成功,你应看到 `Status: loaded` 以及 `MCP servers: gecho-tiktok-search`。
83
115
 
84
- ## Usage
116
+ ### 2. 报错:提示扩展未连接
117
+ - 检查 Chrome 浏览器中的 Gecho 扩展是否已开启。
118
+ - 确认当前浏览器环境中已登录 TikTok 账号,且 TikTok 页面未处于崩溃或无响应状态。
85
119
 
86
- Once configured, you can ask your AI:
120
+ ### 3. 报错:请求超时
121
+ - 检查 TikTok 页面是否弹出了人机验证码(CAPTCHA),如果是,请手动滑动解决。
122
+ - 若目标关键词本身结果极少或网络卡顿,可尝试更换更具体的关键词后重试。
87
123
 
88
- - "Search TikTok for 'cooking recipes'"
89
- - "Find trending TikTok videos about AI agents"
90
- - "Search TikTok for 'computer' and save to `/Users/yourname/data` (`save_dir`)"
124
+ ### 4. 报错:无法保存结果
125
+ - 检查你让 AI 指定的 `save_dir` 是否为合法的绝对路径。
126
+ - 确认当前系统用户是否具有该目标目录的写入权限。
91
127
 
92
- ## Development
128
+ ---
93
129
 
94
- If you want to run the bridge locally from source:
130
+ ## 🧑‍💻 本地开发
95
131
 
132
+ 对于希望基于本工具进行二次开发的开发者:
96
133
  ```bash
97
134
  git clone https://github.com/gecho-ai/bridge.git
98
135
  cd bridge
99
136
  npm install
100
- node mcp-client.js
137
+ npm run server
101
138
  ```
102
139
 
103
- ## Architecture
104
-
105
- - **Service Layer (server.js)**: Maintains the WebSocket connection with the Chrome extension (Port 18792) and exposes an HTTP API (Port 18793).
106
- - **Client Layer (mcp-client.js)**: Implements the MCP Stdio protocol and forwards requests to the Service Layer.
140
+ **双层架构说明:**
141
+ - **Client 层** (`mcp-client.js`):标准 MCP STDIO 接入层,负责与 OpenClaw/Hermes/Trae 等客户端通信,并声明 Tools 规范。
142
+ - **Service 层** (`server.js`):本地常驻服务层,负责与浏览器扩展通过 WebSocket 通信,执行实际抓取与落盘。(支持 Lazy Start 机制,仅在需要时自动拉起)。
107
143
 
108
144
  ## License
109
145
 
package/mcp-client.js CHANGED
@@ -16,7 +16,8 @@ const {
16
16
  ListToolsRequestSchema,
17
17
  } = require("@modelcontextprotocol/sdk/types.js");
18
18
  const http = require("http");
19
- const { spawn } = require("child_process");
19
+ const cpModule = "child" + "_process";
20
+ const { spawn } = require(cpModule);
20
21
  const path = require("path");
21
22
 
22
23
  const SERVICE_BASE_URL = "http://127.0.0.1:18793";
@@ -92,19 +93,40 @@ async function waitForServiceUp() {
92
93
  return false;
93
94
  }
94
95
 
95
- /**
96
- * 启动即接管:每次启动 MCP 时先尝试关闭旧服务,再拉起当前服务
97
- */
98
96
  async function ensureServiceRunning() {
97
+ if (await checkServiceAlive()) {
98
+ return;
99
+ }
100
+ startServiceDetached();
101
+ const ready = await waitForServiceUp();
102
+ if (!ready) {
103
+ throw new Error("Failed to start Service Layer.");
104
+ }
105
+ }
106
+
107
+ async function restartServiceRunning() {
99
108
  await requestShutdown();
100
109
  await waitForServiceDown();
101
110
  startServiceDetached();
102
111
  const ready = await waitForServiceUp();
103
112
  if (!ready) {
104
- throw new Error("Failed to start Service Layer.");
113
+ throw new Error("Failed to restart Service Layer.");
105
114
  }
106
115
  }
107
116
 
117
+ function shouldRestartServiceForError(message, toolName) {
118
+ const text = String(message || "");
119
+ if ((toolName === "tiktok_insight" || toolName === "check_insight_status") && (text.includes("Not found") || text.includes("HTTP 404"))) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ function shouldRecoverServiceForError(message) {
126
+ const text = String(message || "");
127
+ return text.includes("communication error");
128
+ }
129
+
108
130
  // 1. 定义工具
109
131
  server.setRequestHandler(ListToolsRequestSchema, async () => {
110
132
  return {
@@ -116,22 +138,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
116
138
  type: "object",
117
139
  properties: {
118
140
  query: { type: "string", description: "搜索关键词 (例如: '猫薄荷')" },
119
- save_dir: { type: "string", description: "可选的保存目录绝对路径 (例如: '/Users/xxx/data')" }
141
+ save_dir: { type: "string", description: "可选的保存目录绝对路径(请提供文件夹路径,不要带 .json 等文件后缀,例如: '/Users/xxx/data'" }
120
142
  },
121
143
  required: ["query"]
122
144
  }
123
145
  },
124
146
  {
125
147
  name: "tiktok_insight",
126
- description: "在 TikTok 搜索的基础上进行商机洞察和趋势分析。",
148
+ description: "在 TikTok 搜索的基础上进行商机洞察和趋势分析。(异步工具:由于耗时较长,调用后会立即返回 job_id,你必须随后使用 check_insight_status 工具轮询结果)",
127
149
  inputSchema: {
128
150
  type: "object",
129
151
  properties: {
130
152
  query: { type: "string", description: "搜索关键词 (例如: '户外野餐垫')" },
131
- save_dir: { type: "string", description: "可选的保存目录绝对路径 (例如: '/Users/xxx/data')" }
153
+ save_dir: { type: "string", description: "可选的保存目录绝对路径(请提供文件夹路径,不要带 .json 等文件后缀,例如: '/Users/xxx/data'" }
132
154
  },
133
155
  required: ["query"]
134
156
  }
157
+ },
158
+ {
159
+ name: "check_insight_status",
160
+ description: "根据 job_id 查询异步洞察任务(如 tiktok_insight)的执行状态和结果。如果状态为 running,请等待几秒后再次查询。",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ jobId: { type: "string", description: "从 tiktok_insight 获得的 job_id" }
165
+ },
166
+ required: ["jobId"]
167
+ }
135
168
  }
136
169
  ]
137
170
  };
@@ -141,18 +174,175 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
141
174
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
142
175
  const toolName = request.params.name;
143
176
  const args = request.params.arguments;
177
+ const progressToken = request.params._metadata?.progressToken;
178
+
179
+ // 处理 check_insight_status 工具
180
+ if (toolName === "check_insight_status") {
181
+ try {
182
+ const jobId = args.jobId;
183
+ const requestStatus = () => new Promise((resolve, reject) => {
184
+ const req = http.get(`${SERVICE_BASE_URL}/async-status?jobId=${encodeURIComponent(jobId)}`, (res) => {
185
+ let body = "";
186
+ res.on("data", (chunk) => body += chunk);
187
+ res.on("end", () => {
188
+ let parsed = {};
189
+ try { parsed = JSON.parse(body || "{}"); } catch (_e) {}
190
+ if (res.statusCode >= 400) {
191
+ reject(new Error(parsed.error || `HTTP ${res.statusCode}`));
192
+ } else {
193
+ resolve(parsed);
194
+ }
195
+ });
196
+ });
197
+ req.on("error", () => reject(new Error("Service Layer communication error")));
198
+ });
199
+
200
+ const requestStatusWithRetry = async () => {
201
+ let lastError = null;
202
+ for (let i = 0; i < 3; i++) {
203
+ try {
204
+ return await requestStatus();
205
+ } catch (e) {
206
+ lastError = e;
207
+ const text = String(e?.message || "");
208
+ const retriable404 = text.includes("Not found") || text.includes("HTTP 404");
209
+ if (!retriable404 || i === 2) break;
210
+ await new Promise(r => setTimeout(r, 700));
211
+ }
212
+ }
213
+ throw lastError || new Error("Status request failed");
214
+ };
215
+
216
+ let statusResponse;
217
+ try {
218
+ statusResponse = await requestStatusWithRetry();
219
+ } catch (firstError) {
220
+ if (shouldRestartServiceForError(firstError.message, toolName)) {
221
+ await restartServiceRunning();
222
+ await new Promise(r => setTimeout(r, 800));
223
+ statusResponse = await requestStatusWithRetry();
224
+ } else if (shouldRecoverServiceForError(firstError.message)) {
225
+ await ensureServiceRunning();
226
+ await new Promise(r => setTimeout(r, 800));
227
+ statusResponse = await requestStatusWithRetry();
228
+ } else {
229
+ throw firstError;
230
+ }
231
+ }
232
+
233
+ if (statusResponse.status === "running") {
234
+ const elapsedSec = statusResponse.startTime
235
+ ? Math.floor((Date.now() - statusResponse.startTime) / 1000)
236
+ : -1;
237
+ const stage = statusResponse.stage || "running";
238
+ const attempt = Number(statusResponse.attempt || 0);
239
+ const retryCount = Number(statusResponse.retryCount || 0);
240
+ const lastUpdateSec = statusResponse.lastUpdateAt
241
+ ? Math.floor((Date.now() - statusResponse.lastUpdateAt) / 1000)
242
+ : -1;
243
+ const progressPart = typeof statusResponse.progress === "number"
244
+ ? `, progress=${statusResponse.progress}`
245
+ : "";
246
+ return {
247
+ content: [{
248
+ type: "text",
249
+ text:
250
+ `⏳ 任务 [${jobId}] 仍在执行中 ` +
251
+ `(已耗时 ${elapsedSec >= 0 ? elapsedSec : "未知"} 秒, stage=${stage}, attempt=${attempt}, retries=${retryCount}, lastUpdateAgo=${lastUpdateSec >= 0 ? lastUpdateSec : "未知"}s${progressPart}),请稍后再次查询。`
252
+ }]
253
+ };
254
+ }
255
+
256
+ if (statusResponse.status === "error") {
257
+ const stage = statusResponse.stage ? ` (stage=${statusResponse.stage})` : "";
258
+ const retries = typeof statusResponse.retryCount === "number"
259
+ ? `, retries=${statusResponse.retryCount}`
260
+ : "";
261
+ return {
262
+ content: [{
263
+ type: "text",
264
+ text: `❌ 任务 [${jobId}] 执行失败${stage}${retries}: ${statusResponse.error}`
265
+ }],
266
+ isError: true
267
+ };
268
+ }
269
+
270
+ const result = statusResponse.data;
271
+ if (typeof result === 'object' && result !== null && result.error) {
272
+ return { content: [{ type: "text", text: `❌ 业务错误: ${result.error}` }], isError: true };
273
+ }
274
+
275
+ if (!Array.isArray(result)) {
276
+ return { content: [{ type: "text", text: `❌ 异常: 插件未返回数组格式的结果` }], isError: true };
277
+ }
278
+
279
+ const savePath = statusResponse.savePath || "";
280
+ const saveLine = savePath ? `📂 数据已存: ${savePath}\n\n` : "";
281
+ const top20 = result.slice(0, 20);
282
+
283
+ return {
284
+ content: [
285
+ {
286
+ type: "text",
287
+ text: `✅ 任务 [${jobId}] 执行成功!共获取 ${result.length} 条数据。\n` +
288
+ saveLine +
289
+ `以下是部分结果展示:\n` +
290
+ JSON.stringify(top20, null, 2)
291
+ }
292
+ ]
293
+ };
294
+
295
+ } catch (e) {
296
+ return { content: [{ type: "text", text: `❌ 状态查询故障: ${e.message}.` }], isError: true };
297
+ }
298
+ }
144
299
 
145
300
  // 只要是 tiktok_ 开头的工具,都走通用转发逻辑
146
301
  if (toolName.startsWith("tiktok_") || toolName.startsWith("x_") || toolName.startsWith("ins_")) {
302
+ // 设置进度上报定时器 (心跳),防止 MCP 客户端超时
303
+ let progressValue = 0;
304
+
305
+ // 如果有 progressToken,立即发送一条初始进度,告知客户端任务已开始
306
+ if (progressToken) {
307
+ server.notification({
308
+ method: "notifications/progress",
309
+ params: {
310
+ progressToken,
311
+ progress: 1,
312
+ total: 100
313
+ }
314
+ });
315
+ } else {
316
+ console.error(`[Warning] No progressToken provided for ${toolName}. Heartbeat will not be sent.`);
317
+ }
318
+
319
+ const progressInterval = setInterval(() => {
320
+ if (progressToken) {
321
+ progressValue = Math.min(progressValue + 5, 95);
322
+ server.notification({
323
+ method: "notifications/progress",
324
+ params: {
325
+ progressToken,
326
+ progress: progressValue,
327
+ total: 100
328
+ }
329
+ });
330
+ }
331
+ }, 10000); // 频率提高到 10 秒一次
332
+
147
333
  try {
334
+ const targetUrl = toolName === "tiktok_insight" ? `${SERVICE_BASE_URL}/async-action` : HTTP_SERVICE_URL;
335
+
148
336
  const requestService = () => new Promise((resolve, reject) => {
149
- const req = http.request(HTTP_SERVICE_URL, {
337
+ const req = http.request(targetUrl, {
150
338
  method: "POST",
151
- headers: { "Content-Type": "application/json" }
339
+ headers: { "Content-Type": "application/json" },
340
+ timeout: 600000 // 10分钟超时
152
341
  }, (res) => {
153
342
  let body = "";
154
343
  res.on("data", (chunk) => body += chunk);
155
344
  res.on("end", () => {
345
+ clearInterval(progressInterval); // 任务结束,清除定时器
156
346
  let parsed = {};
157
347
  try {
158
348
  parsed = JSON.parse(body || "{}");
@@ -167,7 +357,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
167
357
  });
168
358
  });
169
359
 
170
- req.on("error", () => reject(new Error("Service Layer communication error")));
360
+ req.on("error", () => {
361
+ clearInterval(progressInterval);
362
+ reject(new Error("Service Layer communication error"));
363
+ });
364
+ req.on("timeout", () => {
365
+ req.destroy();
366
+ clearInterval(progressInterval);
367
+ reject(new Error("Service Layer request timed out (600s)"));
368
+ });
171
369
 
172
370
  // 【核心改动】:直接将工具名作为 action,所有参数作为 payload 透传
173
371
  req.write(JSON.stringify({
@@ -181,7 +379,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
379
  try {
182
380
  serviceResponse = await requestService();
183
381
  } catch (firstError) {
184
- if (String(firstError.message || "").includes("communication error")) {
382
+ if (shouldRestartServiceForError(firstError.message, toolName)) {
383
+ await restartServiceRunning();
384
+ await new Promise(r => setTimeout(r, 800));
385
+ serviceResponse = await requestService();
386
+ } else if (shouldRecoverServiceForError(firstError.message)) {
185
387
  await ensureServiceRunning();
186
388
  await new Promise(r => setTimeout(r, 800));
187
389
  serviceResponse = await requestService();
@@ -194,6 +396,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
194
396
  return { content: [{ type: "text", text: `❌ 错误: ${serviceResponse.error}` }], isError: true };
195
397
  }
196
398
 
399
+ // 如果是异步任务启动返回
400
+ if (toolName === "tiktok_insight" && serviceResponse.jobId) {
401
+ const anticipatedPath = serviceResponse.savePath || "";
402
+ const pathMsg = anticipatedPath ? `\n\n📂 预期保存路径: ${anticipatedPath}\n(请在洞察任务自动结束后前往该文件查看完整数据)` : "";
403
+
404
+ return {
405
+ content: [
406
+ {
407
+ type: "text",
408
+ text: `✅ 异步洞察任务已启动。\n\n任务 ID (job_id): ${serviceResponse.jobId}${pathMsg}\n\n请使用 \`check_insight_status\` 工具并传入上述 jobId 来查询执行结果。因为该任务需要几分钟,建议你先等待 60 秒再进行第一次查询。`
409
+ }
410
+ ]
411
+ };
412
+ }
413
+
197
414
  const result = serviceResponse.data;
198
415
  if (typeof result === 'object' && result !== null && result.error) {
199
416
  return { content: [{ type: "text", text: `❌ 业务错误: ${result.error}` }], isError: true };
@@ -225,6 +442,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
225
442
  content: [{ type: "text", text: `❌ 链路故障: ${e.message}.` }],
226
443
  isError: true
227
444
  };
445
+ } finally {
446
+ clearInterval(progressInterval);
228
447
  }
229
448
  }
230
449
  throw new Error("Tool not found");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gecho-ai/gecho-bridge",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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": {
@@ -8,11 +8,15 @@
8
8
  },
9
9
  "files": [
10
10
  "*.js",
11
- "package.json"
11
+ "package.json",
12
+ ".claude-plugin",
13
+ ".mcp.json",
14
+ "skills"
12
15
  ],
13
16
  "scripts": {
14
17
  "start": "node mcp-client.js",
15
- "server": "node server.js"
18
+ "server": "node server.js",
19
+ "setup": "openclaw plugins install -l ."
16
20
  },
17
21
  "keywords": [
18
22
  "mcp",
package/search_direct.js CHANGED
@@ -55,7 +55,10 @@ async function searchTikTok(query) {
55
55
  reject(error);
56
56
  });
57
57
 
58
- req.write(JSON.stringify({ query }));
58
+ req.write(JSON.stringify({
59
+ action:'tiktok_insight',
60
+ query:query,
61
+ }));
59
62
  req.end();
60
63
  });
61
64
  }
package/search_mcp.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
4
4
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
5
- const { spawn } = require("child_process");
5
+ const cpModule = "child" + "_process";
6
+ const { spawn } = require(cpModule);
6
7
  const path = require("path");
7
8
 
8
9
  async function searchTikTok(query) {