@gecho-ai/gecho-bridge 1.0.1 → 1.1.1
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 +40 -0
- package/mcp-client.js +20 -20
- package/package.json +2 -2
- package/server.js +60 -7
package/README.md
CHANGED
|
@@ -42,12 +42,52 @@ Add a new MCP server:
|
|
|
42
42
|
- **Type**: command
|
|
43
43
|
- **Command**: `npx -y @gecho-ai/gecho-bridge@latest`
|
|
44
44
|
|
|
45
|
+
### Save Path Configuration
|
|
46
|
+
Search results are persisted to a JSON file. You can control the save directory in two ways:
|
|
47
|
+
|
|
48
|
+
1. Pass `save_dir` in the MCP tool call arguments.
|
|
49
|
+
2. Set environment variable `GECHO_DATA_DIR` for the MCP process.
|
|
50
|
+
|
|
51
|
+
Priority order used by the service:
|
|
52
|
+
|
|
53
|
+
1. `save_dir` from request arguments
|
|
54
|
+
2. `GECHO_DATA_DIR` environment variable
|
|
55
|
+
3. Default `data` directory (`path.join(__dirname, "..", "data")`)
|
|
56
|
+
|
|
57
|
+
File naming:
|
|
58
|
+
|
|
59
|
+
- Result file pattern: `<query>_search_results.json`
|
|
60
|
+
- Query text is sanitized to a safe file name before writing.
|
|
61
|
+
|
|
62
|
+
Example (Claude Desktop with `GECHO_DATA_DIR`):
|
|
63
|
+
|
|
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
|
+
```
|
|
77
|
+
|
|
78
|
+
Example (Cursor command with env):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
GECHO_DATA_DIR=/Users/yourname/gecho-data npx -y @gecho-ai/gecho-bridge@latest
|
|
82
|
+
```
|
|
83
|
+
|
|
45
84
|
## Usage
|
|
46
85
|
|
|
47
86
|
Once configured, you can ask your AI:
|
|
48
87
|
|
|
49
88
|
- "Search TikTok for 'cooking recipes'"
|
|
50
89
|
- "Find trending TikTok videos about AI agents"
|
|
90
|
+
- "Search TikTok for 'computer' and save to `/Users/yourname/data` (`save_dir`)"
|
|
51
91
|
|
|
52
92
|
## Development
|
|
53
93
|
|
package/mcp-client.js
CHANGED
|
@@ -23,7 +23,7 @@ const HTTP_SERVICE_URL = "http://127.0.0.1:18793/search";
|
|
|
23
23
|
const SERVICE_PATH = path.join(__dirname, "server.js");
|
|
24
24
|
|
|
25
25
|
const server = new Server(
|
|
26
|
-
{ name: "tiktok-bridge-client", version: "1.1.
|
|
26
|
+
{ name: "tiktok-bridge-client", version: "1.1.1" },
|
|
27
27
|
{ capabilities: { tools: {} } }
|
|
28
28
|
);
|
|
29
29
|
|
|
@@ -46,12 +46,9 @@ function checkServiceAlive() {
|
|
|
46
46
|
async function ensureServiceRunning() {
|
|
47
47
|
const alive = await checkServiceAlive();
|
|
48
48
|
if (alive) {
|
|
49
|
-
console.error("🐷 Service Layer is already running.");
|
|
50
49
|
return;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
console.error("🐷 Service Layer not found. Starting it automatically...");
|
|
54
|
-
|
|
55
52
|
const child = spawn("node", [SERVICE_PATH], {
|
|
56
53
|
detached: true,
|
|
57
54
|
stdio: "ignore" // 静默启动,不占用当前终端
|
|
@@ -64,7 +61,6 @@ async function ensureServiceRunning() {
|
|
|
64
61
|
while (retries > 0) {
|
|
65
62
|
await new Promise(r => setTimeout(r, 1000));
|
|
66
63
|
if (await checkServiceAlive()) {
|
|
67
|
-
console.error("✅ Service Layer started successfully.");
|
|
68
64
|
return;
|
|
69
65
|
}
|
|
70
66
|
retries--;
|
|
@@ -82,7 +78,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
82
78
|
inputSchema: {
|
|
83
79
|
type: "object",
|
|
84
80
|
properties: {
|
|
85
|
-
query: { type: "string", description: "搜索关键词 (例如: '猫薄荷')" }
|
|
81
|
+
query: { type: "string", description: "搜索关键词 (例如: '猫薄荷')" },
|
|
82
|
+
save_dir: { type: "string", description: "可选的保存目录绝对路径 (例如: '/Users/xxx/data')" }
|
|
86
83
|
},
|
|
87
84
|
required: ["query"]
|
|
88
85
|
}
|
|
@@ -94,7 +91,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
94
91
|
// 2. 转发工具请求到 Service 层
|
|
95
92
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
96
93
|
if (request.params.name === "tiktok_search_top_200") {
|
|
97
|
-
const { query } = request.params.arguments;
|
|
94
|
+
const { query, save_dir } = request.params.arguments;
|
|
98
95
|
|
|
99
96
|
try {
|
|
100
97
|
const requestService = () => new Promise((resolve, reject) => {
|
|
@@ -114,40 +111,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
114
111
|
if (res.statusCode >= 400) {
|
|
115
112
|
reject(new Error(parsed.error || `HTTP ${res.statusCode}`));
|
|
116
113
|
} else {
|
|
117
|
-
resolve(parsed
|
|
114
|
+
resolve(parsed); // 返回整个响应对象
|
|
118
115
|
}
|
|
119
116
|
});
|
|
120
117
|
});
|
|
121
118
|
|
|
122
119
|
req.on("error", () => reject(new Error("Service Layer communication error")));
|
|
123
|
-
req.write(JSON.stringify({ query }));
|
|
120
|
+
req.write(JSON.stringify({ query, save_dir }));
|
|
124
121
|
req.end();
|
|
125
122
|
});
|
|
126
123
|
|
|
127
|
-
let
|
|
124
|
+
let serviceResponse;
|
|
128
125
|
try {
|
|
129
|
-
|
|
126
|
+
serviceResponse = await requestService();
|
|
130
127
|
} catch (firstError) {
|
|
131
128
|
if (String(firstError.message || "").includes("communication error")) {
|
|
132
129
|
await ensureServiceRunning();
|
|
133
130
|
await new Promise(r => setTimeout(r, 800));
|
|
134
|
-
|
|
131
|
+
serviceResponse = await requestService();
|
|
135
132
|
} else {
|
|
136
133
|
throw firstError;
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
if (
|
|
141
|
-
return { content: [{ type: "text", text: `❌ 错误: ${
|
|
137
|
+
if (serviceResponse.error) {
|
|
138
|
+
return { content: [{ type: "text", text: `❌ 错误: ${serviceResponse.error}` }], isError: true };
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
const
|
|
141
|
+
const result = serviceResponse.data || [];
|
|
142
|
+
const savePath = serviceResponse.savePath || "";
|
|
143
|
+
const saveLine = savePath
|
|
144
|
+
? `📂 完整结果已保存到: ${savePath}\n\n`
|
|
145
|
+
: `📂 未返回保存路径,请检查服务端日志。\n\n`;
|
|
146
|
+
const top20 = result.slice(0, 20);
|
|
145
147
|
return {
|
|
146
148
|
content: [
|
|
147
149
|
{
|
|
148
150
|
type: "text",
|
|
149
|
-
text: `✅ 抓取完成!共获取 ${
|
|
150
|
-
|
|
151
|
+
text: `✅ 抓取完成!共获取 ${result.length} 条数据。\n` +
|
|
152
|
+
saveLine +
|
|
151
153
|
`以下是点赞最高的前 20 条结果:\n` +
|
|
152
154
|
JSON.stringify(top20, null, 2)
|
|
153
155
|
}
|
|
@@ -169,11 +171,9 @@ async function main() {
|
|
|
169
171
|
await ensureServiceRunning();
|
|
170
172
|
const transport = new StdioServerTransport();
|
|
171
173
|
await server.connect(transport);
|
|
172
|
-
console.error("🐷 TikTok Bridge MCP Client is running (STDIO)");
|
|
173
174
|
} catch (e) {
|
|
174
|
-
console.error(`❌ Client initialization failed: ${e.message}`);
|
|
175
175
|
process.exit(1);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
main().catch(e =>
|
|
179
|
+
main().catch(e => {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gecho-ai/gecho-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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": {
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"ws": "^8.19.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
|
-
|
|
33
|
+
"access": "public"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/server.js
CHANGED
|
@@ -20,6 +20,50 @@ let extensionSocket = null;
|
|
|
20
20
|
const pendingRequests = new Map();
|
|
21
21
|
let requestIdCounter = 1;
|
|
22
22
|
|
|
23
|
+
function decodeBase64Utf8(value) {
|
|
24
|
+
try {
|
|
25
|
+
return Buffer.from(String(value || ""), "base64").toString("utf8");
|
|
26
|
+
} catch (_e) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeQuery(rawQuery, queryB64) {
|
|
32
|
+
const queryText = String(rawQuery || "").trim();
|
|
33
|
+
const decodedFromB64 = decodeBase64Utf8(queryB64).trim();
|
|
34
|
+
if (decodedFromB64) {
|
|
35
|
+
if (!queryText || /^[??]+$/.test(queryText)) {
|
|
36
|
+
return decodedFromB64;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!queryText) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (/%[0-9A-Fa-f]{2}/.test(queryText)) {
|
|
44
|
+
return decodeURIComponent(queryText);
|
|
45
|
+
}
|
|
46
|
+
} catch (_e) {}
|
|
47
|
+
return queryText;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toSafeFileName(name) {
|
|
51
|
+
const value = String(name || "").trim();
|
|
52
|
+
const replaced = value
|
|
53
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_")
|
|
54
|
+
.replace(/[. ]+$/g, "")
|
|
55
|
+
.slice(0, 80);
|
|
56
|
+
const fallback = `query_${Date.now()}`;
|
|
57
|
+
const candidate = replaced || fallback;
|
|
58
|
+
const upper = candidate.toUpperCase();
|
|
59
|
+
const reserved = new Set([
|
|
60
|
+
"CON","PRN","AUX","NUL",
|
|
61
|
+
"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9",
|
|
62
|
+
"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"
|
|
63
|
+
]);
|
|
64
|
+
return reserved.has(upper) ? `${candidate}_` : candidate;
|
|
65
|
+
}
|
|
66
|
+
|
|
23
67
|
// --- WebSocket Server (与插件通信) ---
|
|
24
68
|
const wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
|
|
25
69
|
|
|
@@ -61,7 +105,7 @@ wss.on("error", (err) => {
|
|
|
61
105
|
|
|
62
106
|
// --- HTTP Server (供 Client 层调用) ---
|
|
63
107
|
const server = http.createServer(async (req, res) => {
|
|
64
|
-
res.setHeader("Content-Type", "application/json");
|
|
108
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
65
109
|
|
|
66
110
|
// 健康检查接口
|
|
67
111
|
if (req.method === "GET" && req.url === "/ping") {
|
|
@@ -73,7 +117,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
73
117
|
req.on("data", chunk => { body += chunk; });
|
|
74
118
|
req.on("end", async () => {
|
|
75
119
|
try {
|
|
76
|
-
const
|
|
120
|
+
const payload = JSON.parse(body);
|
|
121
|
+
const query = normalizeQuery(payload.query, payload.query_b64);
|
|
77
122
|
if (!query) {
|
|
78
123
|
res.statusCode = 400;
|
|
79
124
|
return res.end(JSON.stringify({ error: "Missing query" }));
|
|
@@ -103,14 +148,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
103
148
|
});
|
|
104
149
|
|
|
105
150
|
// 持久化存储
|
|
151
|
+
let savePath = "";
|
|
152
|
+
let saveWarning = "";
|
|
106
153
|
if (Array.isArray(result) && result.length > 0) {
|
|
107
|
-
|
|
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");
|
|
108
156
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
109
|
-
const
|
|
110
|
-
|
|
157
|
+
const safeName = toSafeFileName(query);
|
|
158
|
+
const fixedPath = path.join(dataDir, `${safeName}_search_results.json`);
|
|
159
|
+
try {
|
|
160
|
+
fs.writeFileSync(fixedPath, JSON.stringify(result, null, 2), "utf8");
|
|
161
|
+
savePath = fixedPath;
|
|
162
|
+
} catch (e) {
|
|
163
|
+
saveWarning = e.message;
|
|
164
|
+
}
|
|
111
165
|
}
|
|
112
|
-
|
|
113
|
-
res.end(JSON.stringify({ success: true, data: result }));
|
|
166
|
+
res.end(JSON.stringify({ success: true, data: result, savePath, saveWarning }));
|
|
114
167
|
} catch (e) {
|
|
115
168
|
res.statusCode = 500;
|
|
116
169
|
res.end(JSON.stringify({ error: e.message }));
|