@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.
- package/.claude-plugin/plugin.json +5 -0
- package/.mcp.json +10 -0
- package/README.md +97 -61
- package/mcp-client.js +231 -12
- package/package.json +7 -3
- package/search_direct.js +4 -1
- package/search_mcp.js +2 -1
- package/server.js +730 -10
- package/skills/tiktok-search/SKILL.md +89 -0
- package/skills/tiktok-search/_meta.json +6 -0
package/.mcp.json
ADDED
package/README.md
CHANGED
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
# Gecho Bridge 🚀
|
|
2
2
|
|
|
3
|
-
Gecho Bridge
|
|
3
|
+
🌐 **Gecho Bridge** 是一款通用的 MCP(Model Context Protocol)工具,旨在为你的大语言模型(LLM)与本地浏览器之间搭建一座桥梁。
|
|
4
|
+
安装后,无论是使用 **OpenClaw**、**Hermes** 还是 **Trae**,你的 AI 助手都能直接控制浏览器,自动化完成 TikTok 搜索、数据抓取与深度商机洞察。
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
---
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
+
- 📊 **分析竞品**:输入关键词,快速获取 TikTok 前排高赞视频的各项互动数据。
|
|
11
|
+
- 💡 **寻找爆款**:使用深度洞察工具分析特定品类(如 "portable blender")的流行趋势,捕捉未被满足的市场蓝海。
|
|
12
|
+
- 🤖 **自动化运营**:让大模型直接指挥浏览器,自动翻页抓取并生成数据报表,免去繁琐的手动统计。
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
## 🚀 能做什么
|
|
15
|
+
|
|
16
|
+
- 自动唤起 Chrome 并在 TikTok 搜索指定关键词,模拟真人自然滚动加载。
|
|
17
|
+
- 抓取海量结构化数据(视频 ID、标题、点赞量、播放链接等)并自动安全落盘为 JSON 文件。
|
|
18
|
+
- 执行基于大数据检索的**异步深度洞察**,智能提炼爆款趋势与潜在商机。
|
|
19
|
+
|
|
20
|
+
## 🔗 相关链接
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
+
---
|
|
23
27
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
Search results are persisted to a JSON file. You can control the save directory in two ways:
|
|
67
|
+
## 🏁 快速开始与常见流程
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
2. Set environment variable `GECHO_DATA_DIR` for the MCP process.
|
|
69
|
+
环境配置完毕并重启 AI 客户端后,你可以直接通过自然语言向 AI 下达指令。
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
### 🔍 基础搜索 (`tiktok_search`)
|
|
72
|
+
适用于快速检索和收集视频数据。
|
|
73
|
+
**你可以这样说:**
|
|
74
|
+
- *"帮我搜索 TikTok 关键词 'portable blender',返回点赞最高的前 10 条"*
|
|
75
|
+
- *"搜索 'cat toy',并把完整结果保存到 /Users/yourname/gecho-data"*
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
**执行流程:**
|
|
78
|
+
1. AI 唤起本地浏览器的 Gecho 扩展进行搜索和自动滚动。
|
|
79
|
+
2. 抓取完成后,海量数据会自动落盘到本地。
|
|
80
|
+
3. AI 会在会话中为你精简总结前 20 条高赞结果。
|
|
56
81
|
|
|
57
|
-
|
|
82
|
+
### 📈 深度洞察 (`tiktok_insight`)
|
|
83
|
+
适用于品类调研和趋势分析。
|
|
84
|
+
**你可以这样说:**
|
|
85
|
+
- *"请对 'outdoor picnic mat' 做 tiktok_insight 分析"*
|
|
86
|
+
- *"对比 'desk setup' 和 'minimal desk' 的热视频风格与互动量"*
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
**执行流程:**
|
|
89
|
+
1. 插件会下发异步洞察任务,并立即返回一个 `jobId`。
|
|
90
|
+
2. **⚠️ 高危注意**:洞察分析涉及深度抓取和 AI 运算,耗时通常会**超过 5 分钟**。在执行期间,**请务必不要关闭浏览器插件或相关的 TikTok 页面**。
|
|
91
|
+
3. 等待一段时间后,对 AI 说:*“用 check_insight_status 查询刚才任务的执行状态”* 来获取最终的分析报告。
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
---
|
|
63
94
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
103
|
+
*(注:所有保存的文件名均会自动进行安全化处理,避免非法字符导致写入失败。)*
|
|
79
104
|
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🛠️ 排障指南
|
|
108
|
+
|
|
109
|
+
### 1. 如何确认插件已加载?(以 OpenClaw 为例)
|
|
110
|
+
执行:
|
|
80
111
|
```bash
|
|
81
|
-
|
|
112
|
+
openclaw plugins info @gecho-ai/gecho-bridge
|
|
82
113
|
```
|
|
114
|
+
如果安装成功,你应看到 `Status: loaded` 以及 `MCP servers: gecho-tiktok-search`。
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
### 2. 报错:提示扩展未连接
|
|
117
|
+
- 检查 Chrome 浏览器中的 Gecho 扩展是否已开启。
|
|
118
|
+
- 确认当前浏览器环境中已登录 TikTok 账号,且 TikTok 页面未处于崩溃或无响应状态。
|
|
85
119
|
|
|
86
|
-
|
|
120
|
+
### 3. 报错:请求超时
|
|
121
|
+
- 检查 TikTok 页面是否弹出了人机验证码(CAPTCHA),如果是,请手动滑动解决。
|
|
122
|
+
- 若目标关键词本身结果极少或网络卡顿,可尝试更换更具体的关键词后重试。
|
|
87
123
|
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
124
|
+
### 4. 报错:无法保存结果
|
|
125
|
+
- 检查你让 AI 指定的 `save_dir` 是否为合法的绝对路径。
|
|
126
|
+
- 确认当前系统用户是否具有该目标目录的写入权限。
|
|
91
127
|
|
|
92
|
-
|
|
128
|
+
---
|
|
93
129
|
|
|
94
|
-
|
|
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
|
-
|
|
137
|
+
npm run server
|
|
101
138
|
```
|
|
102
139
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- **Service
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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(
|
|
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", () =>
|
|
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 (
|
|
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.
|
|
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
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
|
|
5
|
+
const cpModule = "child" + "_process";
|
|
6
|
+
const { spawn } = require(cpModule);
|
|
6
7
|
const path = require("path");
|
|
7
8
|
|
|
8
9
|
async function searchTikTok(query) {
|