@bangdao-ai/acw-tools 1.4.3 → 1.4.5-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.
- package/cursorConversationParser.js +57 -7
- package/index.js +264 -9
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -136,14 +136,22 @@ function extractFilePath(toolResult, rawArgs) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
139
|
+
* 解析代码编辑工具的代码变更统计
|
|
140
|
+
* 支持的工具:write、search_replace、edit_file、edit_file_v2
|
|
140
141
|
* @param {Object} bubble - bubble数据
|
|
141
|
-
* @returns {Object|null} 按语言聚合的代码变更统计,格式:{ languageId: { added: number, removed: number } }
|
|
142
|
+
* @returns {Object|null} 按语言聚合的代码变更统计,格式:{ languageId: { added: number, removed: number, filePath: string } }
|
|
142
143
|
*/
|
|
143
144
|
function parseCodeChanges(bubble) {
|
|
144
|
-
// 1. 检查是否是accepted的write操作
|
|
145
145
|
const toolFormerData = bubble.toolFormerData;
|
|
146
|
-
if (!toolFormerData
|
|
146
|
+
if (!toolFormerData) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toolName = toolFormerData.name;
|
|
151
|
+
|
|
152
|
+
// 1. 检查是否是支持的代码编辑工具,且用户已接受
|
|
153
|
+
const supportedTools = ['write', 'search_replace', 'edit_file', 'edit_file_v2'];
|
|
154
|
+
if (!supportedTools.includes(toolName) || toolFormerData.userDecision !== 'accepted') {
|
|
147
155
|
return null;
|
|
148
156
|
}
|
|
149
157
|
|
|
@@ -161,13 +169,14 @@ function parseCodeChanges(bubble) {
|
|
|
161
169
|
return null;
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
// 3. 从rawArgs获取file_path
|
|
172
|
+
// 3. 从rawArgs获取file_path(不同工具的参数名可能不同)
|
|
165
173
|
let filePath = null;
|
|
166
174
|
try {
|
|
167
175
|
const rawArgs = typeof toolFormerData.rawArgs === 'string'
|
|
168
176
|
? JSON.parse(toolFormerData.rawArgs)
|
|
169
177
|
: toolFormerData.rawArgs;
|
|
170
|
-
|
|
178
|
+
// 尝试多种可能的参数名
|
|
179
|
+
filePath = rawArgs?.file_path || rawArgs?.target_file || rawArgs?.path || rawArgs?.filePath;
|
|
171
180
|
} catch (error) {
|
|
172
181
|
// 忽略解析错误
|
|
173
182
|
}
|
|
@@ -197,6 +206,47 @@ function parseCodeChanges(bubble) {
|
|
|
197
206
|
languageId = matchedBlock.languageId;
|
|
198
207
|
}
|
|
199
208
|
}
|
|
209
|
+
|
|
210
|
+
// 如果仍未识别语言,尝试从文件扩展名推断
|
|
211
|
+
if (languageId === 'unknown' && filePath) {
|
|
212
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
213
|
+
const extToLangMap = {
|
|
214
|
+
'js': 'javascript',
|
|
215
|
+
'jsx': 'javascriptreact',
|
|
216
|
+
'ts': 'typescript',
|
|
217
|
+
'tsx': 'typescriptreact',
|
|
218
|
+
'py': 'python',
|
|
219
|
+
'java': 'java',
|
|
220
|
+
'json': 'json',
|
|
221
|
+
'md': 'markdown',
|
|
222
|
+
'yml': 'yaml',
|
|
223
|
+
'yaml': 'yaml',
|
|
224
|
+
'xml': 'xml',
|
|
225
|
+
'html': 'html',
|
|
226
|
+
'css': 'css',
|
|
227
|
+
'scss': 'scss',
|
|
228
|
+
'less': 'less',
|
|
229
|
+
'sh': 'shellscript',
|
|
230
|
+
'bash': 'shellscript',
|
|
231
|
+
'sql': 'sql',
|
|
232
|
+
'go': 'go',
|
|
233
|
+
'rs': 'rust',
|
|
234
|
+
'rb': 'ruby',
|
|
235
|
+
'php': 'php',
|
|
236
|
+
'c': 'c',
|
|
237
|
+
'cpp': 'cpp',
|
|
238
|
+
'h': 'c',
|
|
239
|
+
'hpp': 'cpp',
|
|
240
|
+
'cs': 'csharp',
|
|
241
|
+
'swift': 'swift',
|
|
242
|
+
'kt': 'kotlin',
|
|
243
|
+
'vue': 'vue',
|
|
244
|
+
'svelte': 'svelte',
|
|
245
|
+
};
|
|
246
|
+
if (ext && extToLangMap[ext]) {
|
|
247
|
+
languageId = extToLangMap[ext];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
200
250
|
|
|
201
251
|
// 5. 统计所有chunks的增删行数
|
|
202
252
|
let totalAdded = 0;
|
|
@@ -213,7 +263,7 @@ function parseCodeChanges(bubble) {
|
|
|
213
263
|
[languageId]: {
|
|
214
264
|
added: totalAdded,
|
|
215
265
|
removed: totalRemoved,
|
|
216
|
-
filePath: filePath //
|
|
266
|
+
filePath: filePath // 文件路径,用于后端识别真实语言
|
|
217
267
|
}
|
|
218
268
|
};
|
|
219
269
|
}
|
package/index.js
CHANGED
|
@@ -69,6 +69,188 @@ 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
|
+
*/
|
|
201
|
+
function startLockHeartbeat() {
|
|
202
|
+
logger.info('启动心跳检测', { pid: process.pid, isMainInstance, interval: '30秒' });
|
|
203
|
+
|
|
204
|
+
setInterval(() => {
|
|
205
|
+
try {
|
|
206
|
+
if (isMainInstance) {
|
|
207
|
+
// 主实例:更新心跳时间戳
|
|
208
|
+
if (fs.existsSync(LOCK_FILE)) {
|
|
209
|
+
const lockContent = fs.readFileSync(LOCK_FILE, 'utf8');
|
|
210
|
+
const lockData = JSON.parse(lockContent);
|
|
211
|
+
|
|
212
|
+
if (lockData.pid === process.pid) {
|
|
213
|
+
lockData.timestamp = Date.now();
|
|
214
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify(lockData), 'utf8');
|
|
215
|
+
logger.debug('心跳更新成功', { pid: process.pid });
|
|
216
|
+
} else {
|
|
217
|
+
// 锁被其他进程持有,降级为从实例
|
|
218
|
+
isMainInstance = false;
|
|
219
|
+
logger.warn('锁被其他进程持有,降级为从实例', { myPid: process.pid, lockPid: lockData.pid });
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// 锁文件不存在,尝试重新获取
|
|
223
|
+
if (tryAcquireLock()) {
|
|
224
|
+
logger.info('锁文件丢失,重新获取成功', { pid: process.pid });
|
|
225
|
+
} else {
|
|
226
|
+
isMainInstance = false;
|
|
227
|
+
logger.warn('锁文件丢失且无法重新获取,降级为从实例', { pid: process.pid });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
// 从实例:尝试升级为主实例
|
|
232
|
+
logger.debug('从实例尝试获取锁', { pid: process.pid });
|
|
233
|
+
if (tryAcquireLock()) {
|
|
234
|
+
isMainInstance = true;
|
|
235
|
+
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
236
|
+
logger.info('从实例升级为主实例,开始运行定时任务', { pid: process.pid });
|
|
237
|
+
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
238
|
+
|
|
239
|
+
// 启动定时任务(如果 TOKEN 有效)
|
|
240
|
+
if (isTokenValid && chatGrabAvailable) {
|
|
241
|
+
startChatGrabScheduler().catch((error) => {
|
|
242
|
+
logger.error('启动定时任务失败', { pid: process.pid, error: error.message });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
// 忽略心跳更新失败
|
|
249
|
+
logger.debug('心跳更新失败', { pid: process.pid, error: error.message });
|
|
250
|
+
}
|
|
251
|
+
}, 30 * 1000); // 每 30 秒检查一次(缩短间隔以便更快接管)
|
|
252
|
+
}
|
|
253
|
+
|
|
72
254
|
/**
|
|
73
255
|
* 格式化时间戳(UTF+8 本地时间)
|
|
74
256
|
* 格式: 2025-11-10 21:35:42.456
|
|
@@ -2189,8 +2371,12 @@ Tips:当返回多个匹配时,请使用返回列表中的完整规则名称
|
|
|
2189
2371
|
|
|
2190
2372
|
// --- 启动 stdio 传输 ---
|
|
2191
2373
|
async function main() {
|
|
2374
|
+
// 尝试获取锁,成为主实例
|
|
2375
|
+
isMainInstance = tryAcquireLock();
|
|
2376
|
+
|
|
2192
2377
|
const transport = new StdioServerTransport();
|
|
2193
2378
|
await server.connect(transport);
|
|
2379
|
+
|
|
2194
2380
|
logger.info("ACW工具MCP服务已启动", {
|
|
2195
2381
|
mcpVersion: CURRENT_MCP_VERSION,
|
|
2196
2382
|
baseUrl: BASE_URL,
|
|
@@ -2198,23 +2384,39 @@ async function main() {
|
|
|
2198
2384
|
tokenConfigured: isTokenValid,
|
|
2199
2385
|
hostName: HOST_NAME,
|
|
2200
2386
|
osType: OS_TYPE,
|
|
2387
|
+
pid: process.pid,
|
|
2388
|
+
isMainInstance: isMainInstance, // 是否是主实例
|
|
2201
2389
|
availableTools: ['download_rule'],
|
|
2202
|
-
chatGrabEnabled: chatGrabAvailable && isTokenValid,
|
|
2390
|
+
chatGrabEnabled: chatGrabAvailable && isTokenValid && isMainInstance,
|
|
2203
2391
|
chatGrabDir: CHAT_GRAB_DIR,
|
|
2204
2392
|
dbEngineType: dbEngineType,
|
|
2205
2393
|
...(chatGrabAvailable ? {} : { chatGrabDisabledReason: 'No database engine available' }),
|
|
2206
|
-
...(!isTokenValid ? { tokenDisabledReason: 'ACW_TOKEN not configured or invalid' } : {})
|
|
2394
|
+
...(!isTokenValid ? { tokenDisabledReason: 'ACW_TOKEN not configured or invalid' } : {}),
|
|
2395
|
+
...(!isMainInstance ? { schedulerDisabledReason: '非主实例,定时任务由其他实例负责' } : {})
|
|
2207
2396
|
});
|
|
2208
2397
|
|
|
2398
|
+
// 启动心跳(所有实例都需要,从实例用于尝试升级为主实例)
|
|
2399
|
+
startLockHeartbeat();
|
|
2400
|
+
|
|
2209
2401
|
// 只有 TOKEN 有效时才执行需要认证的功能
|
|
2210
2402
|
if (isTokenValid) {
|
|
2211
|
-
//
|
|
2212
|
-
|
|
2213
|
-
logger.
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2403
|
+
// 只有主实例才运行定时任务(避免多个进程同时抓取)
|
|
2404
|
+
if (isMainInstance) {
|
|
2405
|
+
logger.info("本实例是主实例,将负责运行定时任务", { pid: process.pid });
|
|
2406
|
+
|
|
2407
|
+
// 上报主机信息(异步执行,失败不影响启动)
|
|
2408
|
+
reportHostInfo().catch((error) => {
|
|
2409
|
+
logger.warn('主机信息上报异常', { error: error.message });
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
// 启动对话抓取定时任务(会先获取配置再抓取,不需要单独的配置刷新定时器)
|
|
2413
|
+
await startChatGrabScheduler();
|
|
2414
|
+
} else {
|
|
2415
|
+
logger.info("本实例是从实例,不运行定时任务", {
|
|
2416
|
+
pid: process.pid,
|
|
2417
|
+
hint: '已有其他实例负责对话抓取,本实例仅提供工具调用服务,会定期尝试升级为主实例'
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2218
2420
|
} else {
|
|
2219
2421
|
logger.info("ACW 接口功能已禁用(TOKEN 未配置)", {
|
|
2220
2422
|
disabledFeatures: ['主机信息上报', '对话抓取', '规则下载'],
|
|
@@ -2223,10 +2425,63 @@ async function main() {
|
|
|
2223
2425
|
}
|
|
2224
2426
|
}
|
|
2225
2427
|
|
|
2428
|
+
// ==================== 进程退出处理 ====================
|
|
2429
|
+
|
|
2430
|
+
/**
|
|
2431
|
+
* 优雅退出处理
|
|
2432
|
+
*/
|
|
2433
|
+
function gracefulShutdown(signal) {
|
|
2434
|
+
logger.info(`收到退出信号: ${signal},开始优雅退出`, {
|
|
2435
|
+
pid: process.pid,
|
|
2436
|
+
isMainInstance,
|
|
2437
|
+
signal
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
// 释放锁文件
|
|
2441
|
+
releaseLock();
|
|
2442
|
+
|
|
2443
|
+
// 给一点时间完成日志写入
|
|
2444
|
+
setTimeout(() => {
|
|
2445
|
+
process.exit(0);
|
|
2446
|
+
}, 100);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// 监听退出信号
|
|
2450
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
2451
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
2452
|
+
process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
|
|
2453
|
+
|
|
2454
|
+
// 监听进程退出事件
|
|
2455
|
+
process.on('exit', (code) => {
|
|
2456
|
+
releaseLock();
|
|
2457
|
+
// 注意:exit 事件中只能执行同步操作
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
// 监听未捕获的异常
|
|
2461
|
+
process.on('uncaughtException', (err) => {
|
|
2462
|
+
logger.error('未捕获的异常', { pid: process.pid, error: err.message, stack: err.stack });
|
|
2463
|
+
releaseLock();
|
|
2464
|
+
process.exit(1);
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
// 监听未处理的 Promise 拒绝
|
|
2468
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
2469
|
+
logger.error('未处理的 Promise 拒绝', { pid: process.pid, reason: String(reason) });
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
// ==================== 连接状态监控 ====================
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* 注意:不直接监控 stdin,因为 MCP SDK 会管理 stdio 通信
|
|
2476
|
+
* 当 Cursor 断开时,MCP SDK 的 transport 会处理断开逻辑
|
|
2477
|
+
* 我们只需要确保进程能够响应系统信号即可
|
|
2478
|
+
*/
|
|
2479
|
+
|
|
2226
2480
|
main().catch((err) => {
|
|
2227
2481
|
logger.error("MCP服务启动失败", {
|
|
2228
2482
|
error: err.message,
|
|
2229
2483
|
stack: err.stack
|
|
2230
2484
|
});
|
|
2485
|
+
releaseLock();
|
|
2231
2486
|
process.exit(1);
|
|
2232
2487
|
});
|
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
|
+
"version": "1.4.5",
|
|
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