@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 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.0" },
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.data);
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 result;
124
+ let serviceResponse;
128
125
  try {
129
- result = await requestService();
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
- result = await requestService();
131
+ serviceResponse = await requestService();
135
132
  } else {
136
133
  throw firstError;
137
134
  }
138
135
  }
139
136
 
140
- if (result.error) {
141
- return { content: [{ type: "text", text: `❌ 错误: ${result.error}` }], isError: true };
137
+ if (serviceResponse.error) {
138
+ return { content: [{ type: "text", text: `❌ 错误: ${serviceResponse.error}` }], isError: true };
142
139
  }
143
140
 
144
- const top20 = Array.isArray(result) ? result.slice(0, 20) : [];
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: `✅ 抓取完成!共获取 ${Array.isArray(result) ? result.length : 0} 条数据。\n` +
150
- `📂 完整结果已保存在本地 data 目录。\n\n` +
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 => console.error(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.0.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
- "access": "public"
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 { query } = JSON.parse(body);
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
- const dataDir = path.join(__dirname, "..", "data");
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 fixedPath = path.join(dataDir, `${query}_search_results.json`);
110
- fs.writeFileSync(fixedPath, JSON.stringify(result, null, 2), "utf8");
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 }));