@gecho-ai/gecho-bridge 1.0.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 ADDED
@@ -0,0 +1,70 @@
1
+ # Gecho Bridge 🚀
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.
4
+
5
+ ## Features
6
+
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.
11
+
12
+ ## Installation
13
+
14
+ ### 1. Install via npm
15
+ ```bash
16
+ npm install -g @gecho-ai/gecho-bridge
17
+ ```
18
+
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.
21
+
22
+ ## Configuration
23
+
24
+ ### Claude Desktop
25
+ Add the following to your `claude_desktop_config.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "gecho-bridge": {
31
+ "command": "npx",
32
+ "args": ["-y", "@gecho-ai/gecho-bridge@latest"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
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`
44
+
45
+ ## Usage
46
+
47
+ Once configured, you can ask your AI:
48
+
49
+ - "Search TikTok for 'cooking recipes'"
50
+ - "Find trending TikTok videos about AI agents"
51
+
52
+ ## Development
53
+
54
+ If you want to run the bridge locally from source:
55
+
56
+ ```bash
57
+ git clone https://github.com/gecho-ai/bridge.git
58
+ cd bridge
59
+ npm install
60
+ node mcp-client.js
61
+ ```
62
+
63
+ ## Architecture
64
+
65
+ - **Service Layer (server.js)**: Maintains the WebSocket connection with the Chrome extension (Port 18792) and exposes an HTTP API (Port 18793).
66
+ - **Client Layer (mcp-client.js)**: Implements the MCP Stdio protocol and forwards requests to the Service Layer.
67
+
68
+ ## License
69
+
70
+ MIT
package/index.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 🐷 1.1 MCP 标准格式: 引入 MCP SDK 核心类。StdioServerTransport 是最通用的通信方式。
4
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ } = require("@modelcontextprotocol/sdk/types.js");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ // 🐷 1.2 桥接技术栈: 引入 WebSocketServer,用于与 Chrome 扩展建立双向通信。
14
+ const { WebSocketServer } = require("ws");
15
+
16
+ // 🐷 1.3 桥接逻辑: 初始化 WebSocket 服务器变量。
17
+ const WS_PORT = 18792;
18
+ let extensionSocket = null;
19
+ let wss = null;
20
+
21
+ // 🐷 1.4 改造点: 增加一个 Map 来存储等待中的请求回调,实现“请求-响应”模式。
22
+ // 参考了 mcp-chrome-bridge/dist/native-messaging-host.js [L114] 的设计。
23
+ const pendingRequests = new Map();
24
+ let requestIdCounter = 1;
25
+
26
+ /**
27
+ * 🐷 1.4.1 异步等待插件连接的辅助函数
28
+ * 逻辑: 每隔 500ms 检查一次 extensionSocket,最多等待 timeoutMs。
29
+ * 解决问题: MCP 启动瞬间插件可能还在重连空窗期。
30
+ */
31
+ async function waitForExtension(timeoutMs = 10000) {
32
+ const start = Date.now();
33
+ while (Date.now() - start < timeoutMs) {
34
+ if (extensionSocket && extensionSocket.readyState === 1) {
35
+ return true;
36
+ }
37
+ await new Promise(resolve => setTimeout(resolve, 500));
38
+ }
39
+ return false;
40
+ }
41
+
42
+ function initWss() {
43
+ if (wss) return wss;
44
+
45
+ wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
46
+
47
+ wss.on("error", (err) => {
48
+ if (err.code === "EADDRINUSE") {
49
+ console.error(`\n❌ 错误: 端口 ${WS_PORT} 已被占用!`);
50
+ console.error(`这通常是因为另一个 tiktok_bridge 进程正在运行。`);
51
+ console.error(`请执行以下命令清理残留进程后重试:`);
52
+ console.error(`lsof -i :${WS_PORT} | grep LISTEN | awk '{print $2}' | xargs kill -9\n`);
53
+ } else {
54
+ console.error("WebSocket Server Error:", err);
55
+ }
56
+ process.exit(1);
57
+ });
58
+
59
+ wss.on("connection", (ws) => {
60
+ console.error("Chrome Extension connected to Bridge!");
61
+ extensionSocket = ws;
62
+
63
+ ws.on("message", (message) => {
64
+ try {
65
+ const parsed = JSON.parse(message);
66
+ // 🐷 1.4.2 响应匹配逻辑: 使用 requestId 确保结果精准回传给对应的 MCP 请求。
67
+ // 参考了 mcp-chrome-bridge/dist/native-messaging-host.js [L54] 的 handleMessage 逻辑。
68
+ if (parsed.method === "action_result" && parsed.requestId) {
69
+ console.error(`🐷 收到插件抓取的数据结果 (ID: ${parsed.requestId})`);
70
+ const pending = pendingRequests.get(parsed.requestId);
71
+ if (pending) {
72
+ clearTimeout(pending.timeoutId);
73
+ pending.resolve(parsed.data);
74
+ pendingRequests.delete(parsed.requestId);
75
+ }
76
+ }
77
+ } catch (e) {
78
+ console.error("Error parsing extension message:", e);
79
+ }
80
+ });
81
+
82
+ ws.on("close", () => {
83
+ console.error("Chrome Extension disconnected.");
84
+ extensionSocket = null;
85
+ });
86
+ });
87
+ return wss;
88
+ }
89
+
90
+ // 🐷 2.1 MCP 标准格式: 创建 MCP Server 实例,定义服务器名称和版本。
91
+ const server = new Server(
92
+ {
93
+ name: "tiktok-bridge",
94
+ version: "1.0.0",
95
+ },
96
+ {
97
+ capabilities: {
98
+ // 🐷 2.2 MCP 标准格式: 声明支持“工具 (Tools)”功能。
99
+ tools: {},
100
+ },
101
+ }
102
+ );
103
+
104
+ // 🐷 3.1 Skills 标准格式: 定义工具列表。这是 AI 模型能理解的“技能”描述。
105
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
106
+ return {
107
+ tools: [
108
+ {
109
+ // 🐷 3.2 Skills 标准格式: 工具名称和描述。
110
+ name: "tiktok_search_top_200",
111
+ description: "在 TikTok 上搜索关键词,自动滚动加载至少 200 条结果,并全部返回。",
112
+ // 🐷 3.3 Skills 标准格式: 输入参数 Schema。
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ query: {
117
+ type: "string",
118
+ description: "搜索关键词 (例如: '猫薄荷')"
119
+ }
120
+ },
121
+ required: ["query"]
122
+ }
123
+ }
124
+ ]
125
+ };
126
+ });
127
+
128
+ // 🐷 4.1 MCP 标准格式: 处理具体的工具调用请求。
129
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
130
+ if (request.params.name === "tiktok_search_top_200") {
131
+ const { query } = request.params.arguments;
132
+
133
+ // 🐷 4.2 桥接逻辑: 参考 mcp-chrome-bridge/dist/server/index.js [L48] 的逻辑,先确保连接就绪。
134
+ // 增加 10s 的异步等待窗口,防止插件重连期间直接报错。
135
+ const isConnected = await waitForExtension(10000);
136
+ if (!isConnected) {
137
+ return {
138
+ content: [{ type: "text", text: "错误: Chrome 插件未连接,请确保插件已开启并显示 ON。" }],
139
+ isError: true
140
+ };
141
+ }
142
+
143
+ // 🐷 4.3 改造点: 发送带 requestId 的指令并等待插件回传。
144
+ // 参考了 mcp-chrome-bridge/dist/native-messaging-host.js [L114] 的设计。
145
+ const result = await new Promise((resolve, reject) => {
146
+ const requestId = `mcp-${Date.now()}-${requestIdCounter++}`;
147
+
148
+ const timeoutId = setTimeout(() => {
149
+ if (pendingRequests.has(requestId)) {
150
+ pendingRequests.delete(requestId);
151
+ resolve({ error: "抓取超时 (120s),请检查浏览器是否已停止滚动" });
152
+ }
153
+ }, 120000);
154
+
155
+ pendingRequests.set(requestId, { resolve, reject, timeoutId });
156
+
157
+ const payload = {
158
+ method: "execute_action",
159
+ params: { action: "search", params: { query } },
160
+ requestId: requestId // 🐷 关键: 发送请求 ID
161
+ };
162
+ extensionSocket.send(JSON.stringify(payload));
163
+ });
164
+
165
+ // 🐷 4.5 改造点: 将抓取到的 200+ 条完整数据持久化存储到本地文件夹
166
+ let savedPath = "";
167
+ if (Array.isArray(result) && result.length > 0) {
168
+ const dataDir = path.join(__dirname, "..", "data");
169
+ if (!fs.existsSync(dataDir)) {
170
+ fs.mkdirSync(dataDir, { recursive: true });
171
+ }
172
+
173
+ // 1. 生成固定文件名 (适配 OpenClaw 预期)
174
+ const fixedFilename = `${query}_search_results.json`;
175
+ const fixedPath = path.join(dataDir, fixedFilename);
176
+ fs.writeFileSync(fixedPath, JSON.stringify(result, null, 2), "utf8");
177
+
178
+ // 2. 生成带时间戳的备份
179
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
180
+ const backupFilename = `${query}_${timestamp}.json`;
181
+ const backupPath = path.join(dataDir, backupFilename);
182
+ fs.writeFileSync(backupPath, JSON.stringify(result, null, 2), "utf8");
183
+
184
+ savedPath = fixedPath;
185
+ console.error(`🐷 数据已保存至: ${fixedPath} (备份: ${backupFilename})`);
186
+ }
187
+
188
+ // 🐷 4.6 MCP 标准格式: 返回抓取到的前 20 条高赞视频给 AI,并告知存储路径。
189
+ const top20 = Array.isArray(result) ? result.slice(0, 20) : [];
190
+
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: `✅ 抓取完成!共获取 ${Array.isArray(result) ? result.length : 0} 条数据。\n` +
196
+ `📂 原始数据已存储至: ${savedPath}\n\n` +
197
+ `以下是点赞最高的前 20 条结果:\n` +
198
+ JSON.stringify(top20, null, 2)
199
+ }
200
+ ]
201
+ };
202
+ }
203
+
204
+ throw new Error("Tool not found");
205
+ });
206
+
207
+ // 🐷 5.1 启动模式检测: 支持 MCP 模式和 CLI 直接调用模式
208
+ // 参考了 mcp-chrome-bridge/dist/cli.js 的思路,支持多模式启动。
209
+ async function run() {
210
+ const args = process.argv.slice(2);
211
+ let toolName = "";
212
+ let query = "cat";
213
+
214
+ for (let i = 0; i < args.length; i++) {
215
+ if (args[i] === "--tool" && i + 1 < args.length) {
216
+ toolName = args[i + 1];
217
+ i++;
218
+ } else if (args[i] === "--query" && i + 1 < args.length) {
219
+ query = args[i + 1];
220
+ i++;
221
+ } else if (i === 0 && !args[i].startsWith("--")) {
222
+ toolName = args[i];
223
+ } else if (i === 1 && !args[i].startsWith("--")) {
224
+ query = args[i];
225
+ }
226
+ }
227
+
228
+ const isCliMode = toolName === "tiktok_search_top_200" || toolName === "search";
229
+
230
+ if (isCliMode) {
231
+ // 🐷 CLI 模式: 简化后的逻辑,复用 pendingRequests。
232
+ console.error(`🐷 CLI 模式启动: 正在搜索 [${query}]...`);
233
+ initWss();
234
+
235
+ // 等待插件连接
236
+ const isConnected = await waitForExtension(15000);
237
+ if (!isConnected) {
238
+ console.error("❌ 错误: 插件未连接,请确保插件已开启。");
239
+ process.exit(1);
240
+ }
241
+
242
+ const requestId = `cli-${Date.now()}`;
243
+ const result = await new Promise((resolve) => {
244
+ const timeoutId = setTimeout(() => {
245
+ pendingRequests.delete(requestId);
246
+ resolve({ error: "抓取超时 (120s)" });
247
+ }, 120000);
248
+
249
+ pendingRequests.set(requestId, { resolve, timeoutId });
250
+
251
+ extensionSocket.send(JSON.stringify({
252
+ method: "execute_action",
253
+ params: { action: "search", params: { query } },
254
+ requestId: requestId
255
+ }));
256
+ });
257
+
258
+ // 保存结果并退出
259
+ if (Array.isArray(result)) {
260
+ const dataDir = path.join(__dirname, "..", "data");
261
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
262
+ const filePath = path.join(dataDir, `${query}_search_results.json`);
263
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
264
+ console.error(`🐷 数据已保存至: ${filePath}`);
265
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
266
+ } else {
267
+ console.error("❌ 抓取失败:", result.error || "未知错误");
268
+ }
269
+ process.exit(0);
270
+ } else {
271
+ // 🐷 标准 MCP 模式 (STDIO)
272
+ initWss();
273
+ const transport = new StdioServerTransport();
274
+ await server.connect(transport);
275
+ console.error("🐷 TikTok Bridge MCP Server 已启动 (STDIO 模式)");
276
+ }
277
+ }
278
+
279
+ // 🐷 修改后的启动入口
280
+ run().catch((error) => {
281
+ console.error("Status: Error");
282
+ console.error("Error details:", error);
283
+ process.exit(1);
284
+ });
package/mcp-client.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Client 层 (Client Layer) - 2.0 自动增强版
5
+ * 职责:
6
+ * 1. 提供标准的 MCP Stdio 协议接口。
7
+ * 2. [新增] 启动时自动检查 Service 层是否在线。
8
+ * 3. [新增] 若 Service 不在线,自动静默启动 server.js (Lazy Start)。
9
+ * 4. 转发工具请求到 Service 层。
10
+ */
11
+
12
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
13
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
14
+ const {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } = require("@modelcontextprotocol/sdk/types.js");
18
+ const http = require("http");
19
+ const { spawn } = require("child_process");
20
+ const path = require("path");
21
+
22
+ const HTTP_SERVICE_URL = "http://127.0.0.1:18793/search";
23
+ const SERVICE_PATH = path.join(__dirname, "server.js");
24
+
25
+ const server = new Server(
26
+ { name: "tiktok-bridge-client", version: "1.1.0" },
27
+ { capabilities: { tools: {} } }
28
+ );
29
+
30
+ /**
31
+ * 检查服务是否在线
32
+ */
33
+ function checkServiceAlive() {
34
+ return new Promise((resolve) => {
35
+ const req = http.get("http://127.0.0.1:18793/ping", (res) => {
36
+ resolve(res.statusCode === 200);
37
+ });
38
+ req.on("error", () => resolve(false));
39
+ req.end();
40
+ });
41
+ }
42
+
43
+ /**
44
+ * 自动拉起 Service 层
45
+ */
46
+ async function ensureServiceRunning() {
47
+ const alive = await checkServiceAlive();
48
+ if (alive) {
49
+ console.error("🐷 Service Layer is already running.");
50
+ return;
51
+ }
52
+
53
+ console.error("🐷 Service Layer not found. Starting it automatically...");
54
+
55
+ const child = spawn("node", [SERVICE_PATH], {
56
+ detached: true,
57
+ stdio: "ignore" // 静默启动,不占用当前终端
58
+ });
59
+
60
+ child.unref(); // 让子进程独立运行,父进程退出时不影响它
61
+
62
+ // 等待服务启动成功
63
+ let retries = 5;
64
+ while (retries > 0) {
65
+ await new Promise(r => setTimeout(r, 1000));
66
+ if (await checkServiceAlive()) {
67
+ console.error("✅ Service Layer started successfully.");
68
+ return;
69
+ }
70
+ retries--;
71
+ }
72
+ throw new Error("Failed to start Service Layer after 5s.");
73
+ }
74
+
75
+ // 1. 定义工具
76
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
77
+ return {
78
+ tools: [
79
+ {
80
+ name: "tiktok_search_top_200",
81
+ description: "在 TikTok 上搜索关键词,自动滚动加载至少 200 条结果,并全部返回。",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ query: { type: "string", description: "搜索关键词 (例如: '猫薄荷')" }
86
+ },
87
+ required: ["query"]
88
+ }
89
+ }
90
+ ]
91
+ };
92
+ });
93
+
94
+ // 2. 转发工具请求到 Service 层
95
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
96
+ if (request.params.name === "tiktok_search_top_200") {
97
+ const { query } = request.params.arguments;
98
+
99
+ try {
100
+ const requestService = () => new Promise((resolve, reject) => {
101
+ const req = http.request(HTTP_SERVICE_URL, {
102
+ method: "POST",
103
+ headers: { "Content-Type": "application/json" }
104
+ }, (res) => {
105
+ let body = "";
106
+ res.on("data", (chunk) => body += chunk);
107
+ res.on("end", () => {
108
+ let parsed = {};
109
+ try {
110
+ parsed = JSON.parse(body || "{}");
111
+ } catch (_e) {
112
+ parsed = {};
113
+ }
114
+ if (res.statusCode >= 400) {
115
+ reject(new Error(parsed.error || `HTTP ${res.statusCode}`));
116
+ } else {
117
+ resolve(parsed.data);
118
+ }
119
+ });
120
+ });
121
+
122
+ req.on("error", () => reject(new Error("Service Layer communication error")));
123
+ req.write(JSON.stringify({ query }));
124
+ req.end();
125
+ });
126
+
127
+ let result;
128
+ try {
129
+ result = await requestService();
130
+ } catch (firstError) {
131
+ if (String(firstError.message || "").includes("communication error")) {
132
+ await ensureServiceRunning();
133
+ await new Promise(r => setTimeout(r, 800));
134
+ result = await requestService();
135
+ } else {
136
+ throw firstError;
137
+ }
138
+ }
139
+
140
+ if (result.error) {
141
+ return { content: [{ type: "text", text: `❌ 错误: ${result.error}` }], isError: true };
142
+ }
143
+
144
+ const top20 = Array.isArray(result) ? result.slice(0, 20) : [];
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: `✅ 抓取完成!共获取 ${Array.isArray(result) ? result.length : 0} 条数据。\n` +
150
+ `📂 完整结果已保存在本地 data 目录。\n\n` +
151
+ `以下是点赞最高的前 20 条结果:\n` +
152
+ JSON.stringify(top20, null, 2)
153
+ }
154
+ ]
155
+ };
156
+ } catch (e) {
157
+ return {
158
+ content: [{ type: "text", text: `❌ 转发失败: ${e.message}.` }],
159
+ isError: true
160
+ };
161
+ }
162
+ }
163
+ throw new Error("Tool not found");
164
+ });
165
+
166
+ // 3. 启动
167
+ async function main() {
168
+ try {
169
+ await ensureServiceRunning();
170
+ const transport = new StdioServerTransport();
171
+ await server.connect(transport);
172
+ console.error("🐷 TikTok Bridge MCP Client is running (STDIO)");
173
+ } catch (e) {
174
+ console.error(`❌ Client initialization failed: ${e.message}`);
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@gecho-ai/gecho-bridge",
3
+ "version": "1.0.0",
4
+ "description": "MCP Bridge for TikTok Extension Relay - Connecting LLMs to TikTok via Chrome Extension",
5
+ "main": "mcp-client.js",
6
+ "bin": {
7
+ "gecho-bridge": "mcp-client.js"
8
+ },
9
+ "files": [
10
+ "*.js",
11
+ "package.json"
12
+ ],
13
+ "scripts": {
14
+ "start": "node mcp-client.js",
15
+ "server": "node server.js"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "tiktok",
20
+ "bridge",
21
+ "automation",
22
+ "llm",
23
+ "gecho"
24
+ ],
25
+ "author": "Gecho AI",
26
+ "license": "MIT",
27
+ "type": "commonjs",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.27.1",
30
+ "ws": "^8.19.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ const http = require('http');
4
+
5
+ async function searchTikTok(query) {
6
+ console.log(`🔍 正在搜索 TikTok: "${query}"...`);
7
+
8
+ return new Promise((resolve, reject) => {
9
+ const req = http.request('http://127.0.0.1:18793/search', {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json'
13
+ }
14
+ }, (res) => {
15
+ let data = '';
16
+
17
+ res.on('data', (chunk) => {
18
+ data += chunk;
19
+ });
20
+
21
+ res.on('end', () => {
22
+ try {
23
+ const result = JSON.parse(data);
24
+ console.log(`✅ 搜索完成!状态码: ${res.statusCode}`);
25
+
26
+ if (res.statusCode >= 400) {
27
+ console.error(`❌ 错误: ${result.error || '未知错误'}`);
28
+ reject(new Error(result.error || `HTTP ${res.statusCode}`));
29
+ } else {
30
+ console.log(`📊 获取到 ${Array.isArray(result.data) ? result.data.length : 0} 条结果`);
31
+
32
+ // 显示前5条结果
33
+ if (Array.isArray(result.data) && result.data.length > 0) {
34
+ console.log('\n📈 点赞最高的前5条结果:');
35
+ result.data.slice(0, 5).forEach((item, index) => {
36
+ console.log(`${index + 1}. ${item.title || '无标题'}`);
37
+ console.log(` 作者: ${item.author || '未知'}`);
38
+ console.log(` 点赞: ${item.likes || 0} | 评论: ${item.comments || 0}`);
39
+ console.log(` 链接: ${item.url || '无链接'}`);
40
+ console.log('');
41
+ });
42
+ }
43
+
44
+ resolve(result);
45
+ }
46
+ } catch (error) {
47
+ console.error('❌ 解析响应失败:', error);
48
+ reject(error);
49
+ }
50
+ });
51
+ });
52
+
53
+ req.on('error', (error) => {
54
+ console.error('❌ 请求失败:', error.message);
55
+ reject(error);
56
+ });
57
+
58
+ req.write(JSON.stringify({ query }));
59
+ req.end();
60
+ });
61
+ }
62
+
63
+ // 执行搜索
64
+ const query = process.argv[2] || '麦香鱼';
65
+ searchTikTok(query).catch(console.error);
package/search_mcp.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
4
+ const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
5
+ const { spawn } = require("child_process");
6
+ const path = require("path");
7
+
8
+ async function searchTikTok(query) {
9
+ console.log(`🔍 正在搜索 TikTok: "${query}"...`);
10
+
11
+ // 启动 MCP 客户端进程
12
+ const mcpClient = spawn("node", [path.join(__dirname, "mcp-client.js")], {
13
+ stdio: ["pipe", "pipe", "pipe"] // stdin, stdout, stderr
14
+ });
15
+
16
+ // 创建 MCP 客户端
17
+ const client = new Client(
18
+ { name: "tiktok-search-client", version: "1.0.0" },
19
+ { capabilities: {} }
20
+ );
21
+
22
+ // 使用 stdio 传输
23
+ const transport = new StdioClientTransport({
24
+ command: "node",
25
+ args: [path.join(__dirname, "mcp-client.js")]
26
+ });
27
+
28
+ try {
29
+ // 连接
30
+ await client.connect(transport);
31
+ console.log("✅ 已连接到 TikTok Bridge MCP 客户端");
32
+
33
+ // 列出可用工具
34
+ const tools = await client.request({
35
+ method: "tools/list",
36
+ params: {}
37
+ });
38
+ console.log("📋 可用工具:", tools.tools.map(t => t.name));
39
+
40
+ // 调用搜索工具
41
+ const result = await client.request({
42
+ method: "tools/call",
43
+ params: {
44
+ name: "tiktok_search_top_200",
45
+ arguments: { query }
46
+ }
47
+ });
48
+
49
+ console.log("✅ 搜索完成!");
50
+ console.log("📊 结果:", JSON.stringify(result, null, 2));
51
+
52
+ // 断开连接
53
+ await client.close();
54
+ mcpClient.kill();
55
+
56
+ return result;
57
+
58
+ } catch (error) {
59
+ console.error("❌ 搜索失败:", error);
60
+ mcpClient.kill();
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ // 执行搜索
66
+ const query = process.argv[2] || "麦香鱼";
67
+ searchTikTok(query).catch(console.error);
package/server.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Service 层 (Service Layer)
5
+ * 职责:
6
+ * 1. 维持与 Chrome 扩展的 WebSocket 连接 (端口 18792)。
7
+ * 2. 暴露 HTTP API (端口 18793) 供 Client 层调用。
8
+ * 3. 负责数据的抓取、清洗和本地持久化。
9
+ */
10
+
11
+ const { WebSocketServer } = require("ws");
12
+ const http = require("http");
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const WS_PORT = 18792;
17
+ const HTTP_PORT = 18793;
18
+
19
+ let extensionSocket = null;
20
+ const pendingRequests = new Map();
21
+ let requestIdCounter = 1;
22
+
23
+ // --- WebSocket Server (与插件通信) ---
24
+ const wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
25
+
26
+ wss.on("connection", (ws) => {
27
+ console.log("✅ Chrome Extension connected to Service Layer");
28
+ extensionSocket = ws;
29
+
30
+ ws.on("message", (message) => {
31
+ try {
32
+ const parsed = JSON.parse(message);
33
+ if (parsed.method === "action_result" && parsed.requestId) {
34
+ console.log(`📩 Received result from extension (ID: ${parsed.requestId})`);
35
+ const pending = pendingRequests.get(parsed.requestId);
36
+ if (pending) {
37
+ clearTimeout(pending.timeoutId);
38
+ pending.resolve(parsed.data);
39
+ pendingRequests.delete(parsed.requestId);
40
+ }
41
+ }
42
+ } catch (e) {
43
+ console.error("Error parsing extension message:", e);
44
+ }
45
+ });
46
+
47
+ ws.on("close", () => {
48
+ console.log("❌ Chrome Extension disconnected");
49
+ extensionSocket = null;
50
+ });
51
+ });
52
+
53
+ wss.on("error", (err) => {
54
+ if (err.code === "EADDRINUSE") {
55
+ console.error(`❌ WS Port ${WS_PORT} is in use!`);
56
+ process.exit(1);
57
+ } else {
58
+ console.error("WS Error:", err);
59
+ }
60
+ });
61
+
62
+ // --- HTTP Server (供 Client 层调用) ---
63
+ const server = http.createServer(async (req, res) => {
64
+ res.setHeader("Content-Type", "application/json");
65
+
66
+ // 健康检查接口
67
+ if (req.method === "GET" && req.url === "/ping") {
68
+ return res.end(JSON.stringify({ status: "ok" }));
69
+ }
70
+
71
+ if (req.method === "POST" && req.url === "/search") {
72
+ let body = "";
73
+ req.on("data", chunk => { body += chunk; });
74
+ req.on("end", async () => {
75
+ try {
76
+ const { query } = JSON.parse(body);
77
+ if (!query) {
78
+ res.statusCode = 400;
79
+ return res.end(JSON.stringify({ error: "Missing query" }));
80
+ }
81
+
82
+ if (!extensionSocket || extensionSocket.readyState !== 1) {
83
+ res.statusCode = 503;
84
+ return res.end(JSON.stringify({ error: "Extension not connected" }));
85
+ }
86
+
87
+ console.log(`🔍 Processing search request for: [${query}]`);
88
+ const requestId = `svc-${Date.now()}-${requestIdCounter++}`;
89
+
90
+ const result = await new Promise((resolve) => {
91
+ const timeoutId = setTimeout(() => {
92
+ pendingRequests.delete(requestId);
93
+ resolve({ error: "Scraping timeout (120s)" });
94
+ }, 120000);
95
+
96
+ pendingRequests.set(requestId, { resolve, timeoutId });
97
+
98
+ extensionSocket.send(JSON.stringify({
99
+ method: "execute_action",
100
+ params: { action: "search", params: { query } },
101
+ requestId: requestId
102
+ }));
103
+ });
104
+
105
+ // 持久化存储
106
+ if (Array.isArray(result) && result.length > 0) {
107
+ const dataDir = path.join(__dirname, "..", "data");
108
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
109
+ const fixedPath = path.join(dataDir, `${query}_search_results.json`);
110
+ fs.writeFileSync(fixedPath, JSON.stringify(result, null, 2), "utf8");
111
+ }
112
+
113
+ res.end(JSON.stringify({ success: true, data: result }));
114
+ } catch (e) {
115
+ res.statusCode = 500;
116
+ res.end(JSON.stringify({ error: e.message }));
117
+ }
118
+ });
119
+ } else {
120
+ res.statusCode = 404;
121
+ res.end(JSON.stringify({ error: "Not found" }));
122
+ }
123
+ });
124
+
125
+ server.listen(HTTP_PORT, "127.0.0.1", () => {
126
+ console.log(`🚀 TikTok Bridge Service Layer is running:`);
127
+ console.log(` - WebSocket (Extension): ws://127.0.0.1:${WS_PORT}`);
128
+ console.log(` - HTTP API (Client): http://127.0.0.1:${HTTP_PORT}`);
129
+ });