lark-bridge-mcp 2.2.7 → 2.3.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.
@@ -0,0 +1,12 @@
1
+ export declare function initFileQueue(appId: string): string;
2
+ export declare function getQueueDir(): string;
3
+ export declare function pushToFileQueue(text: string, messageId?: string, source?: string): boolean;
4
+ export declare function claimNextMessage(): string | null;
5
+ export declare function pollFileQueue(timeoutMs: number, intervalMs?: number): Promise<string | null>;
6
+ export declare function pollFileQueueBatch(timeoutMs: number, intervalMs?: number): Promise<string | null>;
7
+ export declare function getQueueLength(): number;
8
+ export declare function getQueueMessages(): {
9
+ index: number;
10
+ preview: string;
11
+ }[];
12
+ export declare function cleanupStaleMessages(): void;
@@ -0,0 +1,164 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ const POLL_INTERVAL_MS = 400;
5
+ const STALE_MESSAGE_MS = 5 * 60 * 1000;
6
+ let queueDir = "";
7
+ export function initFileQueue(appId) {
8
+ const suffix = appId ? appId.slice(-8) : "default";
9
+ queueDir = path.join(os.homedir(), ".lark-bridge-mcp", `queue-${suffix}`);
10
+ if (!fs.existsSync(queueDir))
11
+ fs.mkdirSync(queueDir, { recursive: true });
12
+ return queueDir;
13
+ }
14
+ export function getQueueDir() {
15
+ return queueDir;
16
+ }
17
+ export function pushToFileQueue(text, messageId, source) {
18
+ if (!queueDir || !text?.trim())
19
+ return false;
20
+ const ts = Date.now();
21
+ const id = messageId || `${ts}-${Math.random().toString(36).slice(2, 8)}`;
22
+ const safeId = id.replace(/[^a-zA-Z0-9_-]/g, "_");
23
+ const filename = `${ts}_${safeId}.msg`;
24
+ if (messageId) {
25
+ try {
26
+ const existing = fs.readdirSync(queueDir);
27
+ if (existing.some((f) => f.endsWith(`_${safeId}.msg`) || f.endsWith(`_${safeId}.claimed`))) {
28
+ return false;
29
+ }
30
+ }
31
+ catch { /* ignore */ }
32
+ }
33
+ try {
34
+ const data = JSON.stringify({ text, messageId: id, timestamp: ts, source: source || `pid-${process.pid}` });
35
+ const tmpPath = path.join(queueDir, filename + ".tmp");
36
+ const finalPath = path.join(queueDir, filename);
37
+ fs.writeFileSync(tmpPath, data, "utf-8");
38
+ fs.renameSync(tmpPath, finalPath);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function claimNextMessage() {
46
+ if (!queueDir)
47
+ return null;
48
+ let files;
49
+ try {
50
+ files = fs.readdirSync(queueDir).filter((f) => f.endsWith(".msg")).sort();
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ for (const file of files) {
56
+ const srcPath = path.join(queueDir, file);
57
+ const claimedPath = srcPath.replace(/\.msg$/, ".claimed");
58
+ try {
59
+ fs.renameSync(srcPath, claimedPath);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ try {
65
+ const raw = fs.readFileSync(claimedPath, "utf-8");
66
+ fs.unlinkSync(claimedPath);
67
+ const parsed = JSON.parse(raw);
68
+ return typeof parsed.text === "string" ? parsed.text : raw;
69
+ }
70
+ catch {
71
+ try {
72
+ fs.unlinkSync(claimedPath);
73
+ }
74
+ catch { /* ignore */ }
75
+ continue;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ export function pollFileQueue(timeoutMs, intervalMs = POLL_INTERVAL_MS) {
81
+ return new Promise((resolve) => {
82
+ const immediate = claimNextMessage();
83
+ if (immediate !== null) {
84
+ resolve(immediate);
85
+ return;
86
+ }
87
+ const deadline = Date.now() + timeoutMs;
88
+ const timer = setInterval(() => {
89
+ const msg = claimNextMessage();
90
+ if (msg !== null) {
91
+ clearInterval(timer);
92
+ resolve(msg);
93
+ return;
94
+ }
95
+ if (Date.now() >= deadline) {
96
+ clearInterval(timer);
97
+ resolve(null);
98
+ }
99
+ }, intervalMs);
100
+ timer.unref();
101
+ });
102
+ }
103
+ export async function pollFileQueueBatch(timeoutMs, intervalMs = POLL_INTERVAL_MS) {
104
+ const first = await pollFileQueue(timeoutMs, intervalMs);
105
+ if (first === null)
106
+ return null;
107
+ const messages = [first];
108
+ let extra = claimNextMessage();
109
+ while (extra !== null) {
110
+ messages.push(extra);
111
+ extra = claimNextMessage();
112
+ }
113
+ return messages.join("\n");
114
+ }
115
+ export function getQueueLength() {
116
+ if (!queueDir)
117
+ return 0;
118
+ try {
119
+ return fs.readdirSync(queueDir).filter((f) => f.endsWith(".msg")).length;
120
+ }
121
+ catch {
122
+ return 0;
123
+ }
124
+ }
125
+ export function getQueueMessages() {
126
+ if (!queueDir)
127
+ return [];
128
+ try {
129
+ const files = fs.readdirSync(queueDir).filter((f) => f.endsWith(".msg")).sort();
130
+ return files.map((f, i) => {
131
+ try {
132
+ const raw = fs.readFileSync(path.join(queueDir, f), "utf-8");
133
+ const parsed = JSON.parse(raw);
134
+ return { index: i, preview: (parsed.text ?? "").slice(0, 200) };
135
+ }
136
+ catch {
137
+ return { index: i, preview: "(unreadable)" };
138
+ }
139
+ });
140
+ }
141
+ catch {
142
+ return [];
143
+ }
144
+ }
145
+ export function cleanupStaleMessages() {
146
+ if (!queueDir)
147
+ return;
148
+ const now = Date.now();
149
+ try {
150
+ for (const f of fs.readdirSync(queueDir)) {
151
+ if (!f.endsWith(".claimed") && !f.endsWith(".tmp"))
152
+ continue;
153
+ const filePath = path.join(queueDir, f);
154
+ try {
155
+ const stat = fs.statSync(filePath);
156
+ if (now - stat.mtimeMs > STALE_MESSAGE_MS)
157
+ fs.unlinkSync(filePath);
158
+ }
159
+ catch { /* ignore */ }
160
+ }
161
+ }
162
+ catch { /* ignore */ }
163
+ }
164
+ //# sourceMappingURL=file-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-queue.js","sourceRoot":"","sources":["../src/file-queue.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
- import * as http from "node:http";
4
+ import * as net from "node:net";
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import * as os from "node:os";
8
+ import * as cp from "node:child_process";
8
9
  import * as Lark from "@larksuiteoapi/node-sdk";
10
+ import { initFileQueue, pushToFileQueue, pollFileQueueBatch, cleanupStaleMessages } from "./file-queue.js";
9
11
  // ── stdout 保护:MCP 用 stdio 通信,任何非协议输出都会破坏 JSON-RPC 帧 ──
10
12
  const _origStdoutWrite = process.stdout.write.bind(process.stdout);
11
13
  process.stdout.write = ((chunk, encodingOrCb, cb) => {
@@ -48,38 +50,105 @@ function log(level, ...args) {
48
50
  const msg = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
49
51
  process.stderr.write(`[${localTimestamp()}][${level}] ${msg}\n`);
50
52
  }
51
- // ── 单实例保护(PID 锁)────────────────────────────────
52
- const workspaceDirs = [
53
- process.env.LARK_WORKSPACE_DIR,
54
- process.cwd(),
55
- ].filter(Boolean);
53
+ // ── 单实例保护(PID 锁 + TCP 端口互斥)─────────────────
56
54
  function resolvePidFilePath() {
57
- for (const ws of workspaceDirs) {
58
- const cursorDir = path.join(ws, ".cursor");
59
- if (fs.existsSync(cursorDir))
60
- return path.join(cursorDir, ".lark-mcp.pid");
55
+ const suffix = APP_ID ? `-${APP_ID.slice(-8)}` : "";
56
+ const homeDir = os.homedir();
57
+ const dir = path.join(homeDir, ".lark-bridge-mcp");
58
+ if (!fs.existsSync(dir)) {
59
+ try {
60
+ fs.mkdirSync(dir, { recursive: true });
61
+ }
62
+ catch { /* ignore */ }
61
63
  }
62
- return path.join(os.tmpdir(), ".lark-mcp.pid");
64
+ return path.join(dir, `mcp${suffix}.pid`);
63
65
  }
64
66
  const MCP_PID_FILE = resolvePidFilePath();
67
+ function getSingletonPort() {
68
+ const base = APP_ID || "lark-bridge-default";
69
+ let hash = 0;
70
+ for (let i = 0; i < base.length; i++) {
71
+ hash = ((hash << 5) - hash + base.charCodeAt(i)) | 0;
72
+ }
73
+ return 49152 + (Math.abs(hash) % (65535 - 49152));
74
+ }
75
+ const SINGLETON_PORT = getSingletonPort();
76
+ function forceKillPid(pid) {
77
+ try {
78
+ if (process.platform === "win32") {
79
+ cp.execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore", timeout: 5000 });
80
+ }
81
+ else {
82
+ process.kill(pid, "SIGKILL");
83
+ }
84
+ }
85
+ catch { /* best-effort */ }
86
+ }
87
+ function isProcessAlive(pid) {
88
+ try {
89
+ process.kill(pid, 0);
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
65
96
  function killPreviousInstance() {
66
97
  try {
67
98
  if (!fs.existsSync(MCP_PID_FILE))
68
99
  return;
69
- const oldPid = parseInt(fs.readFileSync(MCP_PID_FILE, "utf-8").trim(), 10);
70
- if (isNaN(oldPid) || oldPid === process.pid)
71
- return;
72
- try {
73
- process.kill(oldPid, 0);
74
- log("WARN", `发现旧 MCP 进程 (PID=${oldPid}),正在终止...`);
75
- process.kill(oldPid);
100
+ const raw = fs.readFileSync(MCP_PID_FILE, "utf-8").trim();
101
+ const pids = raw.split(/[\r\n,]+/).map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n !== process.pid);
102
+ for (const oldPid of pids) {
103
+ if (!isProcessAlive(oldPid))
104
+ continue;
105
+ log("WARN", `发现旧 MCP 进程 (PID=${oldPid}),正在强制终止...`);
106
+ forceKillPid(oldPid);
107
+ setTimeout(() => {
108
+ if (isProcessAlive(oldPid)) {
109
+ log("WARN", `PID=${oldPid} 仍存活,重试...`);
110
+ forceKillPid(oldPid);
111
+ }
112
+ }, 1000);
76
113
  }
77
- catch { /* already dead */ }
78
114
  }
79
115
  catch (e) {
80
116
  log("WARN", `清理旧进程失败: ${e?.message ?? e}`);
81
117
  }
82
118
  }
119
+ async function requestOldInstanceShutdown() {
120
+ return new Promise((resolve) => {
121
+ const client = net.createConnection({ port: SINGLETON_PORT, host: "127.0.0.1" }, () => {
122
+ client.write("shutdown");
123
+ client.end();
124
+ });
125
+ client.on("data", () => { });
126
+ client.on("end", () => { setTimeout(resolve, 500); });
127
+ client.on("error", () => { resolve(); });
128
+ client.setTimeout(3000, () => { client.destroy(); resolve(); });
129
+ });
130
+ }
131
+ let singletonServer = null;
132
+ function startSingletonGuard() {
133
+ singletonServer = net.createServer((socket) => {
134
+ socket.on("data", (data) => {
135
+ if (data.toString().trim() === "shutdown") {
136
+ log("INFO", "收到新实例的 shutdown 指令,即将退出...");
137
+ socket.end("ok");
138
+ gracefulShutdown("singleton-replaced");
139
+ }
140
+ });
141
+ });
142
+ singletonServer.on("error", (err) => {
143
+ if (err.code === "EADDRINUSE") {
144
+ log("WARN", `互斥端口 ${SINGLETON_PORT} 被占用(旧实例仍在监听),已通过 PID 文件兜底`);
145
+ }
146
+ });
147
+ singletonServer.listen(SINGLETON_PORT, "127.0.0.1", () => {
148
+ log("INFO", `单实例守卫已启动 (port=${SINGLETON_PORT})`);
149
+ });
150
+ singletonServer.unref();
151
+ }
83
152
  function writePidFile() {
84
153
  try {
85
154
  const dir = path.dirname(MCP_PID_FILE);
@@ -100,70 +169,26 @@ function writePidFile() {
100
169
  log("WARN", `写入 PID 文件失败: ${e?.message ?? e}`);
101
170
  }
102
171
  }
103
- // ── Daemon HTTP 客户端 ──────────────────────────────────
104
- let daemonBaseUrl = "";
105
- function findDaemonPort() {
106
- const envPort = process.env.LARK_DAEMON_PORT;
107
- if (envPort) {
108
- const p = Number(envPort);
109
- if (p > 0) {
110
- log("INFO", `从 LARK_DAEMON_PORT 环境变量获取端口: ${p}`);
111
- return p;
112
- }
113
- }
114
- for (const ws of workspaceDirs) {
115
- const lockPath = path.join(ws, ".cursor", ".lark-daemon.json");
116
- try {
117
- const raw = fs.readFileSync(lockPath, "utf-8");
118
- const data = JSON.parse(raw);
119
- if (data.port)
120
- return Number(data.port);
121
- }
122
- catch { /* lock not found */ }
123
- }
124
- return null;
125
- }
126
- function httpRequest(method, urlPath, body, timeoutMs = 30_000) {
127
- return new Promise((resolve, reject) => {
128
- const url = new URL(urlPath, daemonBaseUrl);
129
- const payload = body ? JSON.stringify(body) : undefined;
130
- const req = http.request(url, {
131
- method,
132
- headers: payload
133
- ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
134
- : undefined,
135
- timeout: timeoutMs,
136
- }, (res) => {
137
- let data = "";
138
- res.on("data", (c) => (data += c));
139
- res.on("end", () => {
140
- try {
141
- resolve(JSON.parse(data));
142
- }
143
- catch {
144
- resolve(data);
145
- }
146
- });
147
- });
148
- req.on("error", reject);
149
- req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
150
- if (payload)
151
- req.write(payload);
152
- req.end();
153
- });
154
- }
155
- async function pingDaemon(port) {
172
+ // ── 优雅退出 ─────────────────────────────────────────────
173
+ let isShuttingDown = false;
174
+ function gracefulShutdown(reason) {
175
+ if (isShuttingDown)
176
+ return;
177
+ isShuttingDown = true;
178
+ log("INFO", `进程退出中 (reason=${reason}, PID=${process.pid})`);
156
179
  try {
157
- const resp = await httpRequest("GET", `/health`);
158
- return resp?.status === "ok";
180
+ if (singletonServer)
181
+ singletonServer.close();
159
182
  }
160
- catch {
161
- return false;
183
+ catch { /* ignore */ }
184
+ try {
185
+ fs.unlinkSync(MCP_PID_FILE);
162
186
  }
187
+ catch { /* ignore */ }
188
+ setTimeout(() => process.exit(0), 300);
163
189
  }
164
190
  let resolvedTarget = null;
165
191
  let autoOpenId = "";
166
- let embeddedMode = false;
167
192
  const larkClient = new Lark.Client({
168
193
  appId: APP_ID, appSecret: APP_SECRET,
169
194
  appType: Lark.AppType.SelfBuild,
@@ -231,7 +256,8 @@ function getSendTarget() {
231
256
  return { receiveIdType: "open_id", receiveId: autoOpenId };
232
257
  return null;
233
258
  }
234
- async function embeddedSendMessage(text) {
259
+ // ── 发送(始终直接调用 Lark API)──────────────────────────
260
+ async function sendMessage(text) {
235
261
  const target = getSendTarget();
236
262
  if (!target) {
237
263
  log("WARN", "无发送目标");
@@ -248,7 +274,7 @@ async function embeddedSendMessage(text) {
248
274
  log("ERROR", `飞书发送异常: ${e?.message ?? e}`);
249
275
  }
250
276
  }
251
- async function embeddedSendImage(imagePath) {
277
+ async function sendImage(imagePath) {
252
278
  const target = getSendTarget();
253
279
  if (!target) {
254
280
  log("WARN", "无发送目标");
@@ -276,7 +302,7 @@ async function embeddedSendImage(imagePath) {
276
302
  log("ERROR", `发送图片异常: ${e?.message ?? e}`);
277
303
  }
278
304
  }
279
- async function embeddedSendFile(filePath) {
305
+ async function sendFile(filePath) {
280
306
  const target = getSendTarget();
281
307
  if (!target) {
282
308
  log("WARN", "无发送目标");
@@ -305,6 +331,7 @@ async function embeddedSendFile(filePath) {
305
331
  log("ERROR", `发送文件异常: ${e?.message ?? e}`);
306
332
  }
307
333
  }
334
+ // ── 接收(WebSocket → 共享文件队列 → poll 读取)──────────
308
335
  const IMAGE_DOWNLOAD_DIR = path.join(os.tmpdir(), "lark-bridge-images");
309
336
  async function downloadLarkImage(messageId, imageKey) {
310
337
  try {
@@ -383,75 +410,18 @@ async function processIncomingMessage(messageId, messageType, content) {
383
410
  }
384
411
  return parts.join("\n");
385
412
  }
386
- const messageQueue = [];
387
- const pollWaiters = [];
388
- const processedMessageIds = new Set();
389
413
  function pushMessage(content, messageId) {
390
- if (!content || !content.trim()) {
414
+ if (!content?.trim()) {
391
415
  log("WARN", `丢弃空消息 (messageId=${messageId})`);
392
416
  return;
393
417
  }
394
- if (messageId) {
395
- if (processedMessageIds.has(messageId))
396
- return;
397
- processedMessageIds.add(messageId);
398
- if (processedMessageIds.size > 200) {
399
- const first = processedMessageIds.values().next().value;
400
- if (first !== undefined)
401
- processedMessageIds.delete(first);
402
- }
403
- }
404
- log("INFO", `pushMessage: "${content.slice(0, 60)}", waiters=${pollWaiters.length}, queue=${messageQueue.length}`);
405
- while (pollWaiters.length > 0) {
406
- const waiter = pollWaiters.shift();
407
- clearTimeout(waiter.timer);
408
- waiter.resolve(content);
409
- return;
418
+ const written = pushToFileQueue(content, messageId, `mcp-${process.pid}`);
419
+ if (written) {
420
+ log("INFO", `消息已写入共享队列: "${content.slice(0, 60)}" (id=${messageId ?? "none"})`);
410
421
  }
411
- messageQueue.push(content);
412
- }
413
- function drainEmptyMessages() {
414
- while (messageQueue.length > 0 && !messageQueue[0]?.trim()) {
415
- messageQueue.shift();
416
- }
417
- }
418
- function pullMessage(timeoutMs) {
419
- drainEmptyMessages();
420
- if (messageQueue.length > 0) {
421
- const msg = messageQueue.shift();
422
- log("INFO", `pullMessage: 从队列取出 "${msg.slice(0, 40)}", 剩余=${messageQueue.length}`);
423
- return Promise.resolve(msg);
424
- }
425
- log("INFO", `pullMessage: 队列为空, 创建 waiter (timeout=${timeoutMs}ms)`);
426
- return new Promise((resolve) => {
427
- const entry = {
428
- resolve,
429
- timer: setTimeout(() => {
430
- const idx = pollWaiters.indexOf(entry);
431
- if (idx >= 0)
432
- pollWaiters.splice(idx, 1);
433
- log("INFO", `pullMessage: waiter 超时 (${timeoutMs}ms), queue=${messageQueue.length}`);
434
- resolve(null);
435
- }, timeoutMs),
436
- };
437
- pollWaiters.push(entry);
438
- });
439
- }
440
- async function waitForReply(timeoutMs) {
441
- const first = await pullMessage(timeoutMs);
442
- if (first === null)
443
- return null;
444
- const messages = [first];
445
- while (messageQueue.length > 0) {
446
- const next = messageQueue[0];
447
- if (!next?.trim()) {
448
- messageQueue.shift();
449
- continue;
450
- }
451
- messages.push(messageQueue.shift());
422
+ else {
423
+ log("INFO", `消息已跳过(重复或写入失败): id=${messageId ?? "none"}`);
452
424
  }
453
- log("INFO", `waitForReply: 返回 ${messages.length} 条消息, 剩余队列=${messageQueue.length}`);
454
- return messages.join("\n");
455
425
  }
456
426
  function startLarkConnection() {
457
427
  const eventDispatcher = new Lark.EventDispatcher(ENCRYPT_KEY ? { encryptKey: ENCRYPT_KEY } : {}).register({
@@ -491,41 +461,8 @@ function startLarkConnection() {
491
461
  const wsClient = new Lark.WSClient({ appId: APP_ID, appSecret: APP_SECRET, loggerLevel: Lark.LoggerLevel.error });
492
462
  wsClient.start({ eventDispatcher }).then(() => log("INFO", "飞书 WebSocket 连接建立成功")).catch((e) => log("ERROR", `飞书 WebSocket 连接失败: ${e?.message ?? e}`));
493
463
  }
494
- // ── 统一 API(daemon 代理 / 内嵌)─────────────────────────
495
- async function sendMessage(text) {
496
- if (!embeddedMode) {
497
- await httpRequest("POST", "/send", { text });
498
- }
499
- else {
500
- await embeddedSendMessage(text);
501
- }
502
- }
503
- async function sendImage(imagePath) {
504
- if (!embeddedMode) {
505
- await httpRequest("POST", "/send-image", { image_path: imagePath });
506
- }
507
- else {
508
- await embeddedSendImage(imagePath);
509
- }
510
- }
511
- async function sendFile(filePath) {
512
- if (!embeddedMode) {
513
- await httpRequest("POST", "/send-file", { file_path: filePath });
514
- }
515
- else {
516
- await embeddedSendFile(filePath);
517
- }
518
- }
519
- async function pollReply(timeoutMs) {
520
- if (!embeddedMode) {
521
- const httpTimeout = timeoutMs + 10_000;
522
- const resp = await httpRequest("GET", `/poll?timeout=${timeoutMs}`, undefined, httpTimeout);
523
- return resp?.message ?? null;
524
- }
525
- return await waitForReply(timeoutMs);
526
- }
527
464
  // ── MCP Server ──────────────────────────────────────────
528
- const mcpServer = new McpServer({ name: "feishu-cursor-bridge", version: "2.2.7", description: "飞书消息桥接 – 通过飞书与用户沟通" });
465
+ const mcpServer = new McpServer({ name: "feishu-cursor-bridge", version: "2.3.0", description: "飞书消息桥接 – 通过飞书与用户沟通" });
529
466
  mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送消息;传 timeout_seconds 则等待用户回复;两者同时传则先发送再等待。均不传时仅检查待处理消息。", {
530
467
  message: z.string().optional().describe("要发送给用户的消息内容。不传则不发送"),
531
468
  timeout_seconds: z.number().optional().describe("等待用户回复的超时秒数。不传则不等待,立即返回"),
@@ -535,7 +472,7 @@ mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送
535
472
  await sendMessage(message);
536
473
  const timeoutMs = (timeout_seconds && timeout_seconds > 0) ? timeout_seconds * 1000 : 0;
537
474
  if (timeoutMs > 0) {
538
- const reply = await pollReply(timeoutMs);
475
+ const reply = await pollFileQueueBatch(timeoutMs);
539
476
  if (reply === null)
540
477
  return { content: [{ type: "text", text: "[waiting]" }] };
541
478
  return { content: [{ type: "text", text: reply }] };
@@ -550,58 +487,46 @@ mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送
550
487
  mcpServer.tool("send_image", "发送本地图片到飞书。image_path 为本地文件绝对路径。", { image_path: z.string().describe("图片绝对路径") }, async ({ image_path }) => { await sendImage(image_path); return { content: [{ type: "text", text: "图片已发送" }] }; });
551
488
  mcpServer.tool("send_file", "发送本地文件到飞书。file_path 为本地文件绝对路径。", { file_path: z.string().describe("文件绝对路径") }, async ({ file_path }) => { await sendFile(file_path); return { content: [{ type: "text", text: "文件已发送" }] }; });
552
489
  // ── 主函数 ───────────────────────────────────────────────
553
- async function tryConnectDaemon(maxRetries = 3, retryDelayMs = 2000) {
554
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
555
- const port = findDaemonPort();
556
- if (port) {
557
- daemonBaseUrl = `http://127.0.0.1:${port}`;
558
- const alive = await pingDaemon(port);
559
- if (alive) {
560
- embeddedMode = false;
561
- log("INFO", `已连接 daemon (port=${port}),代理模式${attempt > 1 ? ` (第${attempt}次尝试)` : ""}`);
562
- return true;
563
- }
564
- log("WARN", `daemon lock 存在但无响应 (port=${port})${attempt < maxRetries ? ",稍后重试..." : ""}`);
565
- }
566
- else {
567
- log("INFO", `未检测到 daemon${attempt < maxRetries ? ",稍后重试..." : ""}`);
568
- }
569
- if (attempt < maxRetries)
570
- await new Promise((r) => setTimeout(r, retryDelayMs));
571
- }
572
- return false;
573
- }
574
490
  export async function main() {
575
- killPreviousInstance();
576
- writePidFile();
491
+ if (!APP_ID || !APP_SECRET) {
492
+ log("ERROR", "LARK_APP_ID / LARK_APP_SECRET 未配置");
493
+ process.exit(1);
494
+ }
577
495
  log("INFO", "════════════════════════════════════════════════");
578
- log("INFO", `feishu-cursor-bridge MCP v2.2.7 启动 (PID=${process.pid})`);
579
- log("INFO", `workspaceDirs: ${JSON.stringify(workspaceDirs)}`);
496
+ log("INFO", `feishu-cursor-bridge MCP v2.3.0 启动 (PID=${process.pid})`);
497
+ log("INFO", `PID 文件: ${MCP_PID_FILE}`);
498
+ log("INFO", `互斥端口: ${SINGLETON_PORT}`);
580
499
  log("INFO", "════════════════════════════════════════════════");
581
- const hasDaemonPortHint = !!process.env.LARK_DAEMON_PORT;
582
- const connected = await tryConnectDaemon(hasDaemonPortHint ? 5 : 3, 2000);
583
- if (!connected) {
584
- if (hasDaemonPortHint) {
585
- log("ERROR", "应用版模式下 daemon 不可达,MCP 将以纯代理模式运行(无内嵌 WebSocket,避免连接冲突)");
586
- embeddedMode = false;
587
- daemonBaseUrl = `http://127.0.0.1:${process.env.LARK_DAEMON_PORT}`;
588
- }
589
- else {
590
- log("INFO", "降级为内嵌模式");
591
- embeddedMode = true;
592
- }
593
- }
594
- if (embeddedMode) {
595
- if (!APP_ID || !APP_SECRET) {
596
- log("ERROR", "LARK_APP_ID / LARK_APP_SECRET 未配置");
597
- process.exit(1);
598
- }
599
- await initSendTarget();
600
- startLarkConnection();
601
- }
500
+ await requestOldInstanceShutdown();
501
+ killPreviousInstance();
502
+ writePidFile();
503
+ startSingletonGuard();
504
+ const queueDir = initFileQueue(APP_ID);
505
+ log("INFO", `共享文件队列: ${queueDir}`);
506
+ cleanupStaleMessages();
507
+ await initSendTarget();
508
+ startLarkConnection();
602
509
  const transport = new StdioServerTransport();
603
510
  await mcpServer.connect(transport);
604
- log("INFO", `MCP Server 已连接 stdio ✓ (${embeddedMode ? "内嵌" : "代理"}模式)`);
511
+ log("INFO", "MCP Server 已连接 stdio ✓");
512
+ transport.onclose = () => {
513
+ gracefulShutdown("transport-closed");
514
+ };
515
+ process.stdin.on("end", () => {
516
+ gracefulShutdown("stdin-end");
517
+ });
518
+ process.stdin.on("close", () => {
519
+ gracefulShutdown("stdin-close");
520
+ });
521
+ if (process.platform === "win32") {
522
+ const stdinWatchdog = setInterval(() => {
523
+ if (process.stdin.destroyed || !process.stdin.readable) {
524
+ clearInterval(stdinWatchdog);
525
+ gracefulShutdown("stdin-destroyed");
526
+ }
527
+ }, 5000);
528
+ stdinWatchdog.unref();
529
+ }
605
530
  }
606
531
  main().catch((e) => { log("ERROR", `MCP main 异常: ${e?.message ?? e}`); });
607
532
  //# sourceMappingURL=server.js.map