@bangdao-ai/acw-tools 1.4.4 → 1.4.6-beta.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.
Files changed (3) hide show
  1. package/index.js +260 -14
  2. package/manifest.json +1 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -69,6 +69,170 @@ if (!fs.existsSync(LOG_DIR)) {
69
69
  fs.mkdirSync(LOG_DIR, { recursive: true });
70
70
  }
71
71
 
72
+ // ==================== 单例模式:锁文件 ====================
73
+ const LOCK_FILE = path.join(os.homedir(), '.cursor', '.chat_grab', 'acw-mcp.lock');
74
+ let lockFileHandle = null;
75
+ let isMainInstance = false; // 是否是主实例(负责定时任务)
76
+
77
+ /**
78
+ * 尝试获取锁文件(使用排他写入避免竞争条件)
79
+ * 只有主实例才能获取锁,负责运行定时任务
80
+ * @returns {boolean} 是否成功获取锁
81
+ */
82
+ function tryAcquireLock() {
83
+ try {
84
+ // 检查锁文件是否存在
85
+ if (fs.existsSync(LOCK_FILE)) {
86
+ // 读取锁文件内容
87
+ let lockContent;
88
+ try {
89
+ lockContent = fs.readFileSync(LOCK_FILE, 'utf8');
90
+ } catch (e) {
91
+ // 读取失败,可能正在被写入,稍后重试
92
+ logger.debug('读取锁文件失败,稍后重试', { pid: process.pid, error: e.message });
93
+ return false;
94
+ }
95
+
96
+ let lockData;
97
+ try {
98
+ lockData = JSON.parse(lockContent);
99
+ } catch (e) {
100
+ // JSON 解析失败,锁文件损坏,删除它
101
+ logger.warn('锁文件损坏,删除并重新获取', { pid: process.pid });
102
+ fs.unlinkSync(LOCK_FILE);
103
+ lockData = null;
104
+ }
105
+
106
+ if (lockData) {
107
+ // 检查锁是否过期(超过 2 分钟认为进程已死,缩短超时时间)
108
+ const lockAge = Date.now() - lockData.timestamp;
109
+ const LOCK_TIMEOUT = 2 * 60 * 1000; // 2 分钟
110
+
111
+ if (lockAge < LOCK_TIMEOUT) {
112
+ // 检查进程是否还活着
113
+ try {
114
+ process.kill(lockData.pid, 0); // 发送信号 0 检查进程是否存在
115
+ // 进程还活着,锁有效
116
+ logger.debug('锁被其他进程持有', {
117
+ myPid: process.pid,
118
+ lockPid: lockData.pid,
119
+ lockAge: `${Math.round(lockAge / 1000)}秒`
120
+ });
121
+ return false;
122
+ } catch (e) {
123
+ // 进程不存在,可以获取锁
124
+ logger.info('检测到死锁(进程不存在),准备接管', {
125
+ myPid: process.pid,
126
+ deadPid: lockData.pid
127
+ });
128
+ }
129
+ } else {
130
+ // 锁已过期
131
+ logger.info('检测到过期锁,准备接管', {
132
+ myPid: process.pid,
133
+ expiredPid: lockData.pid,
134
+ lockAge: `${Math.round(lockAge / 1000)}秒`
135
+ });
136
+ }
137
+ // 锁已过期或进程已死,删除旧锁
138
+ try {
139
+ fs.unlinkSync(LOCK_FILE);
140
+ } catch (e) {
141
+ // 删除失败,可能已被其他进程删除
142
+ logger.debug('删除旧锁失败', { pid: process.pid, error: e.message });
143
+ }
144
+ }
145
+ }
146
+
147
+ // 使用排他模式创建锁文件(wx 标志:排他写入,文件必须不存在)
148
+ const lockData = {
149
+ pid: process.pid,
150
+ timestamp: Date.now(),
151
+ hostname: os.hostname()
152
+ };
153
+
154
+ try {
155
+ // 使用 wx 标志确保原子创建
156
+ fs.writeFileSync(LOCK_FILE, JSON.stringify(lockData), { flag: 'wx' });
157
+ logger.info('成功获取锁,成为主实例', { pid: process.pid });
158
+ return true;
159
+ } catch (e) {
160
+ if (e.code === 'EEXIST') {
161
+ // 文件已存在,说明其他进程抢先创建了
162
+ logger.debug('获取锁失败,其他进程抢先', { pid: process.pid });
163
+ return false;
164
+ }
165
+ throw e;
166
+ }
167
+ } catch (error) {
168
+ // 获取锁失败
169
+ logger.error('获取锁时发生异常', { pid: process.pid, error: error.message });
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 释放锁文件
176
+ */
177
+ function releaseLock() {
178
+ try {
179
+ if (fs.existsSync(LOCK_FILE)) {
180
+ const lockContent = fs.readFileSync(LOCK_FILE, 'utf8');
181
+ const lockData = JSON.parse(lockContent);
182
+
183
+ // 只删除自己创建的锁
184
+ if (lockData.pid === process.pid) {
185
+ fs.unlinkSync(LOCK_FILE);
186
+ logger.info('已释放锁文件', { pid: process.pid });
187
+ } else {
188
+ logger.debug('锁文件属于其他进程,不释放', { myPid: process.pid, lockPid: lockData.pid });
189
+ }
190
+ }
191
+ } catch (error) {
192
+ logger.debug('释放锁文件失败', { pid: process.pid, error: error.message });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 定期更新锁文件时间戳(心跳)
198
+ * 只有主实例需要运行,用于维护锁的有效性
199
+ */
200
+ function startLockHeartbeat() {
201
+ logger.info('启动心跳检测', { pid: process.pid, interval: '30秒' });
202
+
203
+ setInterval(() => {
204
+ try {
205
+ // 主实例:更新心跳时间戳
206
+ if (fs.existsSync(LOCK_FILE)) {
207
+ const lockContent = fs.readFileSync(LOCK_FILE, 'utf8');
208
+ const lockData = JSON.parse(lockContent);
209
+
210
+ if (lockData.pid === process.pid) {
211
+ lockData.timestamp = Date.now();
212
+ fs.writeFileSync(LOCK_FILE, JSON.stringify(lockData), 'utf8');
213
+ logger.debug('心跳更新成功', { pid: process.pid });
214
+ } else {
215
+ // 锁被其他进程持有(异常情况),退出当前进程
216
+ logger.warn('锁被其他进程抢占,当前进程退出', { myPid: process.pid, lockPid: lockData.pid });
217
+ process.exit(0);
218
+ }
219
+ } else {
220
+ // 锁文件不存在,尝试重新获取
221
+ if (tryAcquireLock()) {
222
+ logger.info('锁文件丢失,重新获取成功', { pid: process.pid });
223
+ } else {
224
+ // 无法重新获取锁,退出当前进程
225
+ logger.warn('锁文件丢失且无法重新获取,当前进程退出', { pid: process.pid });
226
+ process.exit(0);
227
+ }
228
+ }
229
+ } catch (error) {
230
+ // 忽略心跳更新失败
231
+ logger.debug('心跳更新失败', { pid: process.pid, error: error.message });
232
+ }
233
+ }, 30 * 1000); // 每 30 秒检查一次
234
+ }
235
+
72
236
  /**
73
237
  * 格式化时间戳(UTF+8 本地时间)
74
238
  * 格式: 2025-11-10 21:35:42.456
@@ -2189,8 +2353,12 @@ Tips:当返回多个匹配时,请使用返回列表中的完整规则名称
2189
2353
 
2190
2354
  // --- 启动 stdio 传输 ---
2191
2355
  async function main() {
2356
+ // 尝试获取锁,成为主实例
2357
+ isMainInstance = tryAcquireLock();
2358
+
2192
2359
  const transport = new StdioServerTransport();
2193
2360
  await server.connect(transport);
2361
+
2194
2362
  logger.info("ACW工具MCP服务已启动", {
2195
2363
  mcpVersion: CURRENT_MCP_VERSION,
2196
2364
  baseUrl: BASE_URL,
@@ -2198,35 +2366,113 @@ async function main() {
2198
2366
  tokenConfigured: isTokenValid,
2199
2367
  hostName: HOST_NAME,
2200
2368
  osType: OS_TYPE,
2369
+ pid: process.pid,
2370
+ isMainInstance: isMainInstance, // 是否是主实例
2201
2371
  availableTools: ['download_rule'],
2202
- chatGrabEnabled: chatGrabAvailable && isTokenValid,
2372
+ chatGrabEnabled: chatGrabAvailable && isTokenValid && isMainInstance,
2203
2373
  chatGrabDir: CHAT_GRAB_DIR,
2204
2374
  dbEngineType: dbEngineType,
2205
2375
  ...(chatGrabAvailable ? {} : { chatGrabDisabledReason: 'No database engine available' }),
2206
- ...(!isTokenValid ? { tokenDisabledReason: 'ACW_TOKEN not configured or invalid' } : {})
2376
+ ...(!isTokenValid ? { tokenDisabledReason: 'ACW_TOKEN not configured or invalid' } : {}),
2377
+ ...(!isMainInstance ? { schedulerDisabledReason: '非主实例,定时任务由其他实例负责' } : {})
2207
2378
  });
2208
2379
 
2209
- // 只有 TOKEN 有效时才执行需要认证的功能
2210
- if (isTokenValid) {
2211
- // 上报主机信息(异步执行,失败不影响启动)
2212
- reportHostInfo().catch((error) => {
2213
- logger.warn('主机信息上报异常', { error: error.message });
2214
- });
2215
-
2216
- // 启动对话抓取定时任务(会先获取配置再抓取,不需要单独的配置刷新定时器)
2217
- await startChatGrabScheduler();
2380
+ // 只有主实例才启动心跳和定时任务
2381
+ if (isMainInstance) {
2382
+ // 启动心跳(只有主实例需要,用于维护锁的有效性)
2383
+ startLockHeartbeat();
2384
+
2385
+ // 只有 TOKEN 有效时才执行需要认证的功能
2386
+ if (isTokenValid) {
2387
+ logger.info("本实例是主实例,将负责运行定时任务", { pid: process.pid });
2388
+
2389
+ // 上报主机信息(异步执行,失败不影响启动)
2390
+ reportHostInfo().catch((error) => {
2391
+ logger.warn('主机信息上报异常', { error: error.message });
2392
+ });
2393
+
2394
+ // 启动对话抓取定时任务(会先获取配置再抓取,不需要单独的配置刷新定时器)
2395
+ await startChatGrabScheduler();
2396
+ } else {
2397
+ logger.info("ACW 接口功能已禁用(TOKEN 未配置)", {
2398
+ disabledFeatures: ['主机信息上报', '对话抓取', '规则下载'],
2399
+ hint: '请在 MCP 配置中设置有效的 ACW_TOKEN 环境变量以启用完整功能'
2400
+ });
2401
+ }
2218
2402
  } else {
2219
- logger.info("ACW 接口功能已禁用(TOKEN 未配置)", {
2220
- disabledFeatures: ['主机信息上报', '对话抓取', '规则下载'],
2221
- hint: '请在 MCP 配置中设置有效的 ACW_TOKEN 环境变量以启用完整功能'
2403
+ // 从实例:检测到已有主实例运行,直接退出以节省资源
2404
+ logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2405
+ logger.info("检测到已有主实例运行,本实例将退出", {
2406
+ pid: process.pid,
2407
+ reason: '避免资源浪费,只保留一个主实例运行'
2222
2408
  });
2409
+ logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2410
+
2411
+ // 延迟退出,确保日志写入完成
2412
+ setTimeout(() => {
2413
+ process.exit(0);
2414
+ }, 500);
2415
+ return; // 不再执行后续逻辑
2223
2416
  }
2224
2417
  }
2225
2418
 
2419
+ // ==================== 进程退出处理 ====================
2420
+
2421
+ /**
2422
+ * 优雅退出处理
2423
+ */
2424
+ function gracefulShutdown(signal) {
2425
+ logger.info(`收到退出信号: ${signal},开始优雅退出`, {
2426
+ pid: process.pid,
2427
+ isMainInstance,
2428
+ signal
2429
+ });
2430
+
2431
+ // 释放锁文件
2432
+ releaseLock();
2433
+
2434
+ // 给一点时间完成日志写入
2435
+ setTimeout(() => {
2436
+ process.exit(0);
2437
+ }, 100);
2438
+ }
2439
+
2440
+ // 监听退出信号
2441
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
2442
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
2443
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
2444
+
2445
+ // 监听进程退出事件
2446
+ process.on('exit', (code) => {
2447
+ releaseLock();
2448
+ // 注意:exit 事件中只能执行同步操作
2449
+ });
2450
+
2451
+ // 监听未捕获的异常
2452
+ process.on('uncaughtException', (err) => {
2453
+ logger.error('未捕获的异常', { pid: process.pid, error: err.message, stack: err.stack });
2454
+ releaseLock();
2455
+ process.exit(1);
2456
+ });
2457
+
2458
+ // 监听未处理的 Promise 拒绝
2459
+ process.on('unhandledRejection', (reason, promise) => {
2460
+ logger.error('未处理的 Promise 拒绝', { pid: process.pid, reason: String(reason) });
2461
+ });
2462
+
2463
+ // ==================== 连接状态监控 ====================
2464
+
2465
+ /**
2466
+ * 注意:不直接监控 stdin,因为 MCP SDK 会管理 stdio 通信
2467
+ * 当 Cursor 断开时,MCP SDK 的 transport 会处理断开逻辑
2468
+ * 我们只需要确保进程能够响应系统信号即可
2469
+ */
2470
+
2226
2471
  main().catch((err) => {
2227
2472
  logger.error("MCP服务启动失败", {
2228
2473
  error: err.message,
2229
2474
  stack: err.stack
2230
2475
  });
2476
+ releaseLock();
2231
2477
  process.exit(1);
2232
2478
  });
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ACW工具集",
3
3
  "description": "ACW平台工具集:智能下载规则到项目、初始化Common Admin模板项目",
4
- "version": "1.4.4",
4
+ "version": "1.4.6-beta.1",
5
5
  "author": "邦道科技 - 产品技术中心",
6
6
  "homepage": "https://www.npmjs.com/package/@bangdao-ai/acw-tools",
7
7
  "repository": "https://www.npmjs.com/package/@bangdao-ai/acw-tools?activeTab=readme",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bangdao-ai/acw-tools",
3
- "version": "1.4.4",
3
+ "version": "1.4.6-beta.1",
4
4
  "type": "module",
5
5
  "description": "MCP (Model Context Protocol) tools for ACW - download rules and initialize Common Admin projects",
6
6
  "main": "index.js",