lark-bridge-mcp 2.2.8 → 2.3.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/dist/file-queue.d.ts +12 -0
- package/dist/file-queue.js +164 -0
- package/dist/file-queue.js.map +1 -0
- package/dist/server.js +26 -364
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/file-queue.ts +149 -0
- package/src/server.ts +29 -341
|
@@ -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,13 +1,11 @@
|
|
|
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";
|
|
5
|
-
import * as net from "node:net";
|
|
6
4
|
import * as fs from "node:fs";
|
|
7
5
|
import * as path from "node:path";
|
|
8
6
|
import * as os from "node:os";
|
|
9
|
-
import * as cp from "node:child_process";
|
|
10
7
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
8
|
+
import { initFileQueue, pushToFileQueue, pollFileQueueBatch, cleanupStaleMessages } from "./file-queue.js";
|
|
11
9
|
// ── stdout 保护:MCP 用 stdio 通信,任何非协议输出都会破坏 JSON-RPC 帧 ──
|
|
12
10
|
const _origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
13
11
|
process.stdout.write = ((chunk, encodingOrCb, cb) => {
|
|
@@ -50,218 +48,17 @@ function log(level, ...args) {
|
|
|
50
48
|
const msg = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
|
|
51
49
|
process.stderr.write(`[${localTimestamp()}][${level}] ${msg}\n`);
|
|
52
50
|
}
|
|
53
|
-
// ──
|
|
54
|
-
const workspaceDirs = [
|
|
55
|
-
process.env.LARK_WORKSPACE_DIR,
|
|
56
|
-
process.cwd(),
|
|
57
|
-
].filter(Boolean);
|
|
58
|
-
/**
|
|
59
|
-
* PID 文件始终放在用户主目录下,避免因 CWD 不同导致多实例互相看不到。
|
|
60
|
-
* 文件名含 APP_ID 哈希,保证不同应用不冲突。
|
|
61
|
-
*/
|
|
62
|
-
function resolvePidFilePath() {
|
|
63
|
-
const suffix = APP_ID ? `-${APP_ID.slice(-8)}` : "";
|
|
64
|
-
const homeDir = os.homedir();
|
|
65
|
-
const dir = path.join(homeDir, ".lark-bridge-mcp");
|
|
66
|
-
if (!fs.existsSync(dir)) {
|
|
67
|
-
try {
|
|
68
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
-
}
|
|
70
|
-
catch { /* ignore */ }
|
|
71
|
-
}
|
|
72
|
-
return path.join(dir, `mcp${suffix}.pid`);
|
|
73
|
-
}
|
|
74
|
-
const MCP_PID_FILE = resolvePidFilePath();
|
|
75
|
-
/** 计算互斥端口号:基于 APP_ID 的确定性端口,范围 49152-65535 */
|
|
76
|
-
function getSingletonPort() {
|
|
77
|
-
const base = APP_ID || "lark-bridge-default";
|
|
78
|
-
let hash = 0;
|
|
79
|
-
for (let i = 0; i < base.length; i++) {
|
|
80
|
-
hash = ((hash << 5) - hash + base.charCodeAt(i)) | 0;
|
|
81
|
-
}
|
|
82
|
-
return 49152 + (Math.abs(hash) % (65535 - 49152));
|
|
83
|
-
}
|
|
84
|
-
const SINGLETON_PORT = getSingletonPort();
|
|
85
|
-
function forceKillPid(pid) {
|
|
86
|
-
try {
|
|
87
|
-
if (process.platform === "win32") {
|
|
88
|
-
cp.execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore", timeout: 5000 });
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
process.kill(pid, "SIGKILL");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch { /* best-effort */ }
|
|
95
|
-
}
|
|
96
|
-
function isProcessAlive(pid) {
|
|
97
|
-
try {
|
|
98
|
-
process.kill(pid, 0);
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function killPreviousInstance() {
|
|
106
|
-
try {
|
|
107
|
-
if (!fs.existsSync(MCP_PID_FILE))
|
|
108
|
-
return;
|
|
109
|
-
const raw = fs.readFileSync(MCP_PID_FILE, "utf-8").trim();
|
|
110
|
-
const pids = raw.split(/[\r\n,]+/).map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n !== process.pid);
|
|
111
|
-
for (const oldPid of pids) {
|
|
112
|
-
if (!isProcessAlive(oldPid))
|
|
113
|
-
continue;
|
|
114
|
-
log("WARN", `发现旧 MCP 进程 (PID=${oldPid}),正在强制终止...`);
|
|
115
|
-
forceKillPid(oldPid);
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
if (isProcessAlive(oldPid)) {
|
|
118
|
-
log("WARN", `PID=${oldPid} 仍存活,重试 taskkill...`);
|
|
119
|
-
forceKillPid(oldPid);
|
|
120
|
-
}
|
|
121
|
-
}, 1000);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
log("WARN", `清理旧进程失败: ${e?.message ?? e}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/** 通过 TCP 端口通知旧实例退出 */
|
|
129
|
-
async function requestOldInstanceShutdown() {
|
|
130
|
-
return new Promise((resolve) => {
|
|
131
|
-
const client = net.createConnection({ port: SINGLETON_PORT, host: "127.0.0.1" }, () => {
|
|
132
|
-
client.write("shutdown");
|
|
133
|
-
client.end();
|
|
134
|
-
});
|
|
135
|
-
client.on("data", () => { });
|
|
136
|
-
client.on("end", () => { setTimeout(resolve, 500); });
|
|
137
|
-
client.on("error", () => { resolve(); });
|
|
138
|
-
client.setTimeout(3000, () => { client.destroy(); resolve(); });
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
let singletonServer = null;
|
|
142
|
-
/** 启动 TCP 互斥锁服务器 —— 收到 shutdown 立即退出 */
|
|
143
|
-
function startSingletonGuard() {
|
|
144
|
-
singletonServer = net.createServer((socket) => {
|
|
145
|
-
socket.on("data", (data) => {
|
|
146
|
-
if (data.toString().trim() === "shutdown") {
|
|
147
|
-
log("INFO", "收到新实例的 shutdown 指令,即将退出...");
|
|
148
|
-
socket.end("ok");
|
|
149
|
-
gracefulShutdown("singleton-replaced");
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
singletonServer.on("error", (err) => {
|
|
154
|
-
if (err.code === "EADDRINUSE") {
|
|
155
|
-
log("WARN", `互斥端口 ${SINGLETON_PORT} 被占用(旧实例仍在监听),已通过 PID 文件兜底`);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
singletonServer.listen(SINGLETON_PORT, "127.0.0.1", () => {
|
|
159
|
-
log("INFO", `单实例守卫已启动 (port=${SINGLETON_PORT})`);
|
|
160
|
-
});
|
|
161
|
-
singletonServer.unref();
|
|
162
|
-
}
|
|
163
|
-
function writePidFile() {
|
|
164
|
-
try {
|
|
165
|
-
const dir = path.dirname(MCP_PID_FILE);
|
|
166
|
-
if (!fs.existsSync(dir))
|
|
167
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
168
|
-
fs.writeFileSync(MCP_PID_FILE, String(process.pid), "utf-8");
|
|
169
|
-
const cleanup = () => {
|
|
170
|
-
try {
|
|
171
|
-
fs.unlinkSync(MCP_PID_FILE);
|
|
172
|
-
}
|
|
173
|
-
catch { /* best-effort */ }
|
|
174
|
-
};
|
|
175
|
-
process.on("exit", cleanup);
|
|
176
|
-
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
177
|
-
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
178
|
-
}
|
|
179
|
-
catch (e) {
|
|
180
|
-
log("WARN", `写入 PID 文件失败: ${e?.message ?? e}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// ── 优雅退出 ─────────────────────────────────────────────
|
|
51
|
+
// ── 优雅退出(stdio 断开检测,防止僵尸进程)────────────
|
|
184
52
|
let isShuttingDown = false;
|
|
185
53
|
function gracefulShutdown(reason) {
|
|
186
54
|
if (isShuttingDown)
|
|
187
55
|
return;
|
|
188
56
|
isShuttingDown = true;
|
|
189
57
|
log("INFO", `进程退出中 (reason=${reason}, PID=${process.pid})`);
|
|
190
|
-
try {
|
|
191
|
-
if (singletonServer)
|
|
192
|
-
singletonServer.close();
|
|
193
|
-
}
|
|
194
|
-
catch { /* ignore */ }
|
|
195
|
-
try {
|
|
196
|
-
fs.unlinkSync(MCP_PID_FILE);
|
|
197
|
-
}
|
|
198
|
-
catch { /* ignore */ }
|
|
199
58
|
setTimeout(() => process.exit(0), 300);
|
|
200
59
|
}
|
|
201
|
-
// ── Daemon HTTP 客户端 ──────────────────────────────────
|
|
202
|
-
let daemonBaseUrl = "";
|
|
203
|
-
function findDaemonPort() {
|
|
204
|
-
const envPort = process.env.LARK_DAEMON_PORT;
|
|
205
|
-
if (envPort) {
|
|
206
|
-
const p = Number(envPort);
|
|
207
|
-
if (p > 0) {
|
|
208
|
-
log("INFO", `从 LARK_DAEMON_PORT 环境变量获取端口: ${p}`);
|
|
209
|
-
return p;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const ws of workspaceDirs) {
|
|
213
|
-
const lockPath = path.join(ws, ".cursor", ".lark-daemon.json");
|
|
214
|
-
try {
|
|
215
|
-
const raw = fs.readFileSync(lockPath, "utf-8");
|
|
216
|
-
const data = JSON.parse(raw);
|
|
217
|
-
if (data.port)
|
|
218
|
-
return Number(data.port);
|
|
219
|
-
}
|
|
220
|
-
catch { /* lock not found */ }
|
|
221
|
-
}
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
function httpRequest(method, urlPath, body, timeoutMs = 30_000) {
|
|
225
|
-
return new Promise((resolve, reject) => {
|
|
226
|
-
const url = new URL(urlPath, daemonBaseUrl);
|
|
227
|
-
const payload = body ? JSON.stringify(body) : undefined;
|
|
228
|
-
const req = http.request(url, {
|
|
229
|
-
method,
|
|
230
|
-
headers: payload
|
|
231
|
-
? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
|
|
232
|
-
: undefined,
|
|
233
|
-
timeout: timeoutMs,
|
|
234
|
-
}, (res) => {
|
|
235
|
-
let data = "";
|
|
236
|
-
res.on("data", (c) => (data += c));
|
|
237
|
-
res.on("end", () => {
|
|
238
|
-
try {
|
|
239
|
-
resolve(JSON.parse(data));
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
resolve(data);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
req.on("error", reject);
|
|
247
|
-
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
248
|
-
if (payload)
|
|
249
|
-
req.write(payload);
|
|
250
|
-
req.end();
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
async function pingDaemon(port) {
|
|
254
|
-
try {
|
|
255
|
-
const resp = await httpRequest("GET", `/health`);
|
|
256
|
-
return resp?.status === "ok";
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
60
|
let resolvedTarget = null;
|
|
263
61
|
let autoOpenId = "";
|
|
264
|
-
let embeddedMode = false;
|
|
265
62
|
const larkClient = new Lark.Client({
|
|
266
63
|
appId: APP_ID, appSecret: APP_SECRET,
|
|
267
64
|
appType: Lark.AppType.SelfBuild,
|
|
@@ -329,7 +126,8 @@ function getSendTarget() {
|
|
|
329
126
|
return { receiveIdType: "open_id", receiveId: autoOpenId };
|
|
330
127
|
return null;
|
|
331
128
|
}
|
|
332
|
-
|
|
129
|
+
// ── 发送(始终直接调用 Lark API)──────────────────────────
|
|
130
|
+
async function sendMessage(text) {
|
|
333
131
|
const target = getSendTarget();
|
|
334
132
|
if (!target) {
|
|
335
133
|
log("WARN", "无发送目标");
|
|
@@ -346,7 +144,7 @@ async function embeddedSendMessage(text) {
|
|
|
346
144
|
log("ERROR", `飞书发送异常: ${e?.message ?? e}`);
|
|
347
145
|
}
|
|
348
146
|
}
|
|
349
|
-
async function
|
|
147
|
+
async function sendImage(imagePath) {
|
|
350
148
|
const target = getSendTarget();
|
|
351
149
|
if (!target) {
|
|
352
150
|
log("WARN", "无发送目标");
|
|
@@ -374,7 +172,7 @@ async function embeddedSendImage(imagePath) {
|
|
|
374
172
|
log("ERROR", `发送图片异常: ${e?.message ?? e}`);
|
|
375
173
|
}
|
|
376
174
|
}
|
|
377
|
-
async function
|
|
175
|
+
async function sendFile(filePath) {
|
|
378
176
|
const target = getSendTarget();
|
|
379
177
|
if (!target) {
|
|
380
178
|
log("WARN", "无发送目标");
|
|
@@ -403,6 +201,7 @@ async function embeddedSendFile(filePath) {
|
|
|
403
201
|
log("ERROR", `发送文件异常: ${e?.message ?? e}`);
|
|
404
202
|
}
|
|
405
203
|
}
|
|
204
|
+
// ── 接收(WebSocket → 共享文件队列 → poll 读取)──────────
|
|
406
205
|
const IMAGE_DOWNLOAD_DIR = path.join(os.tmpdir(), "lark-bridge-images");
|
|
407
206
|
async function downloadLarkImage(messageId, imageKey) {
|
|
408
207
|
try {
|
|
@@ -481,75 +280,18 @@ async function processIncomingMessage(messageId, messageType, content) {
|
|
|
481
280
|
}
|
|
482
281
|
return parts.join("\n");
|
|
483
282
|
}
|
|
484
|
-
const messageQueue = [];
|
|
485
|
-
const pollWaiters = [];
|
|
486
|
-
const processedMessageIds = new Set();
|
|
487
283
|
function pushMessage(content, messageId) {
|
|
488
|
-
if (!content
|
|
284
|
+
if (!content?.trim()) {
|
|
489
285
|
log("WARN", `丢弃空消息 (messageId=${messageId})`);
|
|
490
286
|
return;
|
|
491
287
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
processedMessageIds.add(messageId);
|
|
496
|
-
if (processedMessageIds.size > 200) {
|
|
497
|
-
const first = processedMessageIds.values().next().value;
|
|
498
|
-
if (first !== undefined)
|
|
499
|
-
processedMessageIds.delete(first);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
log("INFO", `pushMessage: "${content.slice(0, 60)}", waiters=${pollWaiters.length}, queue=${messageQueue.length}`);
|
|
503
|
-
while (pollWaiters.length > 0) {
|
|
504
|
-
const waiter = pollWaiters.shift();
|
|
505
|
-
clearTimeout(waiter.timer);
|
|
506
|
-
waiter.resolve(content);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
messageQueue.push(content);
|
|
510
|
-
}
|
|
511
|
-
function drainEmptyMessages() {
|
|
512
|
-
while (messageQueue.length > 0 && !messageQueue[0]?.trim()) {
|
|
513
|
-
messageQueue.shift();
|
|
288
|
+
const written = pushToFileQueue(content, messageId, `mcp-${process.pid}`);
|
|
289
|
+
if (written) {
|
|
290
|
+
log("INFO", `消息已写入共享队列: "${content.slice(0, 60)}" (id=${messageId ?? "none"})`);
|
|
514
291
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
drainEmptyMessages();
|
|
518
|
-
if (messageQueue.length > 0) {
|
|
519
|
-
const msg = messageQueue.shift();
|
|
520
|
-
log("INFO", `pullMessage: 从队列取出 "${msg.slice(0, 40)}", 剩余=${messageQueue.length}`);
|
|
521
|
-
return Promise.resolve(msg);
|
|
522
|
-
}
|
|
523
|
-
log("INFO", `pullMessage: 队列为空, 创建 waiter (timeout=${timeoutMs}ms)`);
|
|
524
|
-
return new Promise((resolve) => {
|
|
525
|
-
const entry = {
|
|
526
|
-
resolve,
|
|
527
|
-
timer: setTimeout(() => {
|
|
528
|
-
const idx = pollWaiters.indexOf(entry);
|
|
529
|
-
if (idx >= 0)
|
|
530
|
-
pollWaiters.splice(idx, 1);
|
|
531
|
-
log("INFO", `pullMessage: waiter 超时 (${timeoutMs}ms), queue=${messageQueue.length}`);
|
|
532
|
-
resolve(null);
|
|
533
|
-
}, timeoutMs),
|
|
534
|
-
};
|
|
535
|
-
pollWaiters.push(entry);
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
async function waitForReply(timeoutMs) {
|
|
539
|
-
const first = await pullMessage(timeoutMs);
|
|
540
|
-
if (first === null)
|
|
541
|
-
return null;
|
|
542
|
-
const messages = [first];
|
|
543
|
-
while (messageQueue.length > 0) {
|
|
544
|
-
const next = messageQueue[0];
|
|
545
|
-
if (!next?.trim()) {
|
|
546
|
-
messageQueue.shift();
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
messages.push(messageQueue.shift());
|
|
292
|
+
else {
|
|
293
|
+
log("INFO", `消息已跳过(重复或写入失败): id=${messageId ?? "none"}`);
|
|
550
294
|
}
|
|
551
|
-
log("INFO", `waitForReply: 返回 ${messages.length} 条消息, 剩余队列=${messageQueue.length}`);
|
|
552
|
-
return messages.join("\n");
|
|
553
295
|
}
|
|
554
296
|
function startLarkConnection() {
|
|
555
297
|
const eventDispatcher = new Lark.EventDispatcher(ENCRYPT_KEY ? { encryptKey: ENCRYPT_KEY } : {}).register({
|
|
@@ -589,41 +331,8 @@ function startLarkConnection() {
|
|
|
589
331
|
const wsClient = new Lark.WSClient({ appId: APP_ID, appSecret: APP_SECRET, loggerLevel: Lark.LoggerLevel.error });
|
|
590
332
|
wsClient.start({ eventDispatcher }).then(() => log("INFO", "飞书 WebSocket 连接建立成功")).catch((e) => log("ERROR", `飞书 WebSocket 连接失败: ${e?.message ?? e}`));
|
|
591
333
|
}
|
|
592
|
-
// ── 统一 API(daemon 代理 / 内嵌)─────────────────────────
|
|
593
|
-
async function sendMessage(text) {
|
|
594
|
-
if (!embeddedMode) {
|
|
595
|
-
await httpRequest("POST", "/send", { text });
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
await embeddedSendMessage(text);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
async function sendImage(imagePath) {
|
|
602
|
-
if (!embeddedMode) {
|
|
603
|
-
await httpRequest("POST", "/send-image", { image_path: imagePath });
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
await embeddedSendImage(imagePath);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
async function sendFile(filePath) {
|
|
610
|
-
if (!embeddedMode) {
|
|
611
|
-
await httpRequest("POST", "/send-file", { file_path: filePath });
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
await embeddedSendFile(filePath);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async function pollReply(timeoutMs) {
|
|
618
|
-
if (!embeddedMode) {
|
|
619
|
-
const httpTimeout = timeoutMs + 10_000;
|
|
620
|
-
const resp = await httpRequest("GET", `/poll?timeout=${timeoutMs}`, undefined, httpTimeout);
|
|
621
|
-
return resp?.message ?? null;
|
|
622
|
-
}
|
|
623
|
-
return await waitForReply(timeoutMs);
|
|
624
|
-
}
|
|
625
334
|
// ── MCP Server ──────────────────────────────────────────
|
|
626
|
-
const mcpServer = new McpServer({ name: "feishu-cursor-bridge", version: "2.
|
|
335
|
+
const mcpServer = new McpServer({ name: "feishu-cursor-bridge", version: "2.3.1", description: "飞书消息桥接 – 通过飞书与用户沟通" });
|
|
627
336
|
mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送消息;传 timeout_seconds 则等待用户回复;两者同时传则先发送再等待。均不传时仅检查待处理消息。", {
|
|
628
337
|
message: z.string().optional().describe("要发送给用户的消息内容。不传则不发送"),
|
|
629
338
|
timeout_seconds: z.number().optional().describe("等待用户回复的超时秒数。不传则不等待,立即返回"),
|
|
@@ -633,7 +342,7 @@ mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送
|
|
|
633
342
|
await sendMessage(message);
|
|
634
343
|
const timeoutMs = (timeout_seconds && timeout_seconds > 0) ? timeout_seconds * 1000 : 0;
|
|
635
344
|
if (timeoutMs > 0) {
|
|
636
|
-
const reply = await
|
|
345
|
+
const reply = await pollFileQueueBatch(timeoutMs);
|
|
637
346
|
if (reply === null)
|
|
638
347
|
return { content: [{ type: "text", text: "[waiting]" }] };
|
|
639
348
|
return { content: [{ type: "text", text: reply }] };
|
|
@@ -648,68 +357,22 @@ mcpServer.tool("sync_message", "飞书消息同步工具。传 message 则发送
|
|
|
648
357
|
mcpServer.tool("send_image", "发送本地图片到飞书。image_path 为本地文件绝对路径。", { image_path: z.string().describe("图片绝对路径") }, async ({ image_path }) => { await sendImage(image_path); return { content: [{ type: "text", text: "图片已发送" }] }; });
|
|
649
358
|
mcpServer.tool("send_file", "发送本地文件到飞书。file_path 为本地文件绝对路径。", { file_path: z.string().describe("文件绝对路径") }, async ({ file_path }) => { await sendFile(file_path); return { content: [{ type: "text", text: "文件已发送" }] }; });
|
|
650
359
|
// ── 主函数 ───────────────────────────────────────────────
|
|
651
|
-
async function tryConnectDaemon(maxRetries = 3, retryDelayMs = 2000) {
|
|
652
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
653
|
-
const port = findDaemonPort();
|
|
654
|
-
if (port) {
|
|
655
|
-
daemonBaseUrl = `http://127.0.0.1:${port}`;
|
|
656
|
-
const alive = await pingDaemon(port);
|
|
657
|
-
if (alive) {
|
|
658
|
-
embeddedMode = false;
|
|
659
|
-
log("INFO", `已连接 daemon (port=${port}),代理模式${attempt > 1 ? ` (第${attempt}次尝试)` : ""}`);
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
|
-
log("WARN", `daemon lock 存在但无响应 (port=${port})${attempt < maxRetries ? ",稍后重试..." : ""}`);
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
log("INFO", `未检测到 daemon${attempt < maxRetries ? ",稍后重试..." : ""}`);
|
|
666
|
-
}
|
|
667
|
-
if (attempt < maxRetries)
|
|
668
|
-
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
669
|
-
}
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
360
|
export async function main() {
|
|
361
|
+
if (!APP_ID || !APP_SECRET) {
|
|
362
|
+
log("ERROR", "LARK_APP_ID / LARK_APP_SECRET 未配置");
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
673
365
|
log("INFO", "════════════════════════════════════════════════");
|
|
674
|
-
log("INFO", `feishu-cursor-bridge MCP v2.
|
|
675
|
-
log("INFO", `workspaceDirs: ${JSON.stringify(workspaceDirs)}`);
|
|
676
|
-
log("INFO", `PID 文件: ${MCP_PID_FILE}`);
|
|
677
|
-
log("INFO", `互斥端口: ${SINGLETON_PORT}`);
|
|
366
|
+
log("INFO", `feishu-cursor-bridge MCP v2.3.1 启动 (PID=${process.pid})`);
|
|
678
367
|
log("INFO", "════════════════════════════════════════════════");
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
writePidFile();
|
|
685
|
-
startSingletonGuard();
|
|
686
|
-
const hasDaemonPortHint = !!process.env.LARK_DAEMON_PORT;
|
|
687
|
-
const connected = await tryConnectDaemon(hasDaemonPortHint ? 5 : 3, 2000);
|
|
688
|
-
if (!connected) {
|
|
689
|
-
if (hasDaemonPortHint) {
|
|
690
|
-
log("ERROR", "应用版模式下 daemon 不可达,MCP 将以纯代理模式运行(无内嵌 WebSocket,避免连接冲突)");
|
|
691
|
-
embeddedMode = false;
|
|
692
|
-
daemonBaseUrl = `http://127.0.0.1:${process.env.LARK_DAEMON_PORT}`;
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
log("INFO", "降级为内嵌模式");
|
|
696
|
-
embeddedMode = true;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (embeddedMode) {
|
|
700
|
-
if (!APP_ID || !APP_SECRET) {
|
|
701
|
-
log("ERROR", "LARK_APP_ID / LARK_APP_SECRET 未配置");
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
await initSendTarget();
|
|
705
|
-
startLarkConnection();
|
|
706
|
-
}
|
|
368
|
+
const queueDir = initFileQueue(APP_ID);
|
|
369
|
+
log("INFO", `共享文件队列: ${queueDir}`);
|
|
370
|
+
cleanupStaleMessages();
|
|
371
|
+
await initSendTarget();
|
|
372
|
+
startLarkConnection();
|
|
707
373
|
const transport = new StdioServerTransport();
|
|
708
374
|
await mcpServer.connect(transport);
|
|
709
|
-
log("INFO",
|
|
710
|
-
// ── 关键:检测 stdio 断开后强制退出 ──
|
|
711
|
-
// 当 Cursor 关闭会话时 stdin 会关闭,但 Lark WebSocket 保持事件循环存活,
|
|
712
|
-
// 导致进程成为"僵尸"继续抢占飞书消息。
|
|
375
|
+
log("INFO", "MCP Server 已连接 stdio ✓");
|
|
713
376
|
transport.onclose = () => {
|
|
714
377
|
gracefulShutdown("transport-closed");
|
|
715
378
|
};
|
|
@@ -719,7 +382,6 @@ export async function main() {
|
|
|
719
382
|
process.stdin.on("close", () => {
|
|
720
383
|
gracefulShutdown("stdin-close");
|
|
721
384
|
});
|
|
722
|
-
// Windows 下 stdin 可能不触发 end/close,通过 readable 状态轮询兜底
|
|
723
385
|
if (process.platform === "win32") {
|
|
724
386
|
const stdinWatchdog = setInterval(() => {
|
|
725
387
|
if (process.stdin.destroyed || !process.stdin.readable) {
|