@szc-ft/mcp-szcd-client 0.11.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,408 @@
1
+ /**
2
+ * qwen-code.js — Qwen Code 兼容逻辑
3
+ *
4
+ * 导出 setupQwenCode(deps) 供 postinstall.js 调用。
5
+ * deps 需包含: { getMcpServerUrl, getMcpServerName, safeExecSync, isCommandAvailable, ensureDirectory, copyFile, writeFile, SKILL_SOURCE, PROJECT_ROOT, PACKAGE_ROOT }
6
+ *
7
+ * Qwen Code 配置方式(v0.9.0+):
8
+ * - 优先使用 Extension 扩展机制(qwen extensions install)
9
+ * - 扩展目录: ~/.qwen/extensions/szcd-component-helper/
10
+ * - 扩展配置: qwen-extension.json(声明 MCP 服务器、skills、agents、commands)
11
+ * - 回退方案: 直接复制扩展目录到 ~/.qwen/extensions/
12
+ * - 清理旧配置: 移除 settings.json 中的 mcpServers 条目、旧的 skills/ 目录
13
+ *
14
+ * 扩展优势(vs 手动改 settings.json):
15
+ * - 声明式配置,Qwen Code 自动加载 MCP 服务器
16
+ * - 原生支持 skills/agents/commands 目录
17
+ * - 支持 qwen extensions install/uninstall/disable/enable 完整生命周期
18
+ * - 支持 settings(如 MCP_SERVER_URL 环境变量)
19
+ * - contextFileName 自动注入 QWEN.md 上下文
20
+ */
21
+
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import os from "node:os";
25
+ import { execSync } from "node:child_process";
26
+
27
+ // ==================== 路径工具 ====================
28
+
29
+ function getHomeDir() {
30
+ if (os.platform() === "win32") {
31
+ return process.env.USERPROFILE || process.env.HOME || os.homedir();
32
+ }
33
+ return process.env.HOME || os.homedir();
34
+ }
35
+
36
+ function getQwenCodeUserDirectory() {
37
+ const homeDir = getHomeDir();
38
+ if (!homeDir) throw new Error("Home directory not found");
39
+ return path.join(homeDir, ".qwen");
40
+ }
41
+
42
+ function getQwenCodeExtensionsDirectory() {
43
+ return path.join(getQwenCodeUserDirectory(), "extensions");
44
+ }
45
+
46
+ function getQwenCodeSettingsPath() {
47
+ return path.join(getHomeDir(), ".qwen", "settings.json");
48
+ }
49
+
50
+ function getQwenCodeProjectSettingsPath(projectRoot) {
51
+ return path.join(projectRoot, ".qwen", "settings.json");
52
+ }
53
+
54
+ function getExtensionSourcePath(deps) {
55
+ return path.join(deps.PACKAGE_ROOT, "qwen-extension");
56
+ }
57
+
58
+ // ==================== JSON 读写 ====================
59
+
60
+ function readJsonFile(filePath) {
61
+ if (!fs.existsSync(filePath)) return {};
62
+ try {
63
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+
69
+ function writeJsonFile(filePath, data) {
70
+ const dir = path.dirname(filePath);
71
+ if (!fs.existsSync(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ }
74
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
75
+ }
76
+
77
+ // ==================== 旧配置清理 ====================
78
+
79
+ /**
80
+ * 清理 settings.json 中旧的 mcpServers 条目
81
+ * 扩展机制下 MCP 由 qwen-extension.json 声明,settings.json 中不再需要
82
+ */
83
+ function cleanupLegacyMcpConfig(deps) {
84
+ const settingsPath = getQwenCodeSettingsPath();
85
+ const serverName = deps.getMcpServerName();
86
+
87
+ if (!fs.existsSync(settingsPath)) return;
88
+
89
+ const settings = readJsonFile(settingsPath);
90
+ if (!settings.mcpServers || !settings.mcpServers[serverName]) return;
91
+
92
+ delete settings.mcpServers[serverName];
93
+ if (Object.keys(settings.mcpServers).length === 0) {
94
+ delete settings.mcpServers;
95
+ }
96
+
97
+ writeJsonFile(settingsPath, settings);
98
+ console.log(`✓ Cleaned up legacy mcpServers entry in ~/.qwen/settings.json`);
99
+ }
100
+
101
+ /**
102
+ * 清理项目级 .qwen/settings.json 中的旧配置
103
+ * 将 mcpServers 迁移到 _mcpServers_disabled(如果还有的话)
104
+ */
105
+ function cleanupLegacyProjectConfig(deps) {
106
+ const settingsPath = getQwenCodeProjectSettingsPath(deps.PROJECT_ROOT);
107
+ const serverName = deps.getMcpServerName();
108
+
109
+ if (!fs.existsSync(settingsPath)) return;
110
+
111
+ const settings = readJsonFile(settingsPath);
112
+ const disabledKey = "_mcpServers_disabled";
113
+
114
+ if (settings.mcpServers && settings.mcpServers[serverName]) {
115
+ settings[disabledKey] = settings[disabledKey] || {};
116
+ settings[disabledKey][serverName] = settings.mcpServers[serverName];
117
+ delete settings.mcpServers[serverName];
118
+ if (Object.keys(settings.mcpServers).length === 0) {
119
+ delete settings.mcpServers;
120
+ }
121
+ settings._comment =
122
+ "项目级 MCP 已禁用,扩展机制已覆盖。如需恢复,将 _mcpServers_disabled 改回 mcpServers";
123
+ writeJsonFile(settingsPath, settings);
124
+ console.log(`✓ Cleaned up project .qwen/settings.json: migrated to _mcpServers_disabled`);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 清理旧的用户级 skills/ 目录中的手动安装文件
130
+ * 扩展机制下 skills 由扩展目录提供,不再需要手动复制
131
+ */
132
+ function cleanupLegacySkills(deps) {
133
+ const skillsDir = path.join(getQwenCodeUserDirectory(), "skills", deps.getMcpServerName());
134
+ if (fs.existsSync(skillsDir)) {
135
+ try {
136
+ fs.rmSync(skillsDir, { recursive: true, force: true });
137
+ console.log(`✓ Cleaned up legacy skills directory: ${skillsDir}`);
138
+ } catch (e) {
139
+ console.log(`⚠️ Failed to clean up legacy skills: ${e.message}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 清理旧的用户级 agents/ 目录中的手动安装文件
146
+ */
147
+ function cleanupLegacyAgents(deps) {
148
+ const agentsDir = path.join(getQwenCodeUserDirectory(), "agents");
149
+ const serverName = deps.getMcpServerName();
150
+
151
+ if (!fs.existsSync(agentsDir)) return;
152
+
153
+ const agentFiles = fs.readdirSync(agentsDir).filter(
154
+ (f) => f.includes(serverName) && f.endsWith(".md"),
155
+ );
156
+
157
+ for (const f of agentFiles) {
158
+ try {
159
+ fs.unlinkSync(path.join(agentsDir, f));
160
+ console.log(`✓ Cleaned up legacy agent: ${f}`);
161
+ } catch (e) {
162
+ console.log(`⚠️ Failed to clean up agent ${f}: ${e.message}`);
163
+ }
164
+ }
165
+ }
166
+
167
+ // ==================== 扩展安装 ====================
168
+
169
+ /**
170
+ * 方式1: 通过 CLI 安装扩展(推荐)
171
+ */
172
+ function installExtensionViaCLI(deps) {
173
+ if (!deps.isCommandAvailable("qwen")) {
174
+ console.log("⏭️ Skipping Qwen Code CLI extension install: qwen command not found");
175
+ return false;
176
+ }
177
+
178
+ const extensionPath = getExtensionSourcePath(deps);
179
+ const result = deps.safeExecSync(`qwen extensions install "${extensionPath}"`);
180
+
181
+ if (result === true) {
182
+ console.log(`✓ Qwen Code extension installed via CLI: ${deps.getMcpServerName()}`);
183
+ return true;
184
+ }
185
+
186
+ console.warn(`⚠️ Failed to install Qwen Code extension via CLI, falling back to direct copy`);
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * 方式2: 直接复制扩展目录到 ~/.qwen/extensions/
192
+ */
193
+ function installExtensionViaCopy(deps) {
194
+ const sourcePath = getExtensionSourcePath(deps);
195
+ const extName = deps.getMcpServerName();
196
+ const destPath = path.join(getQwenCodeExtensionsDirectory(), extName);
197
+
198
+ if (!fs.existsSync(sourcePath)) {
199
+ console.log(`⚠️ Extension source not found: ${sourcePath}`);
200
+ return false;
201
+ }
202
+
203
+ try {
204
+ // 递归复制目录
205
+ copyDirRecursive(sourcePath, destPath);
206
+ console.log(`✓ Qwen Code extension installed via direct copy: ${destPath}`);
207
+ return true;
208
+ } catch (error) {
209
+ console.log(`⚠️ Failed to copy extension: ${error.message}`);
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 递归复制目录
216
+ */
217
+ function copyDirRecursive(src, dest) {
218
+ if (!fs.existsSync(dest)) {
219
+ fs.mkdirSync(dest, { recursive: true });
220
+ }
221
+
222
+ const entries = fs.readdirSync(src, { withFileTypes: true });
223
+ for (const entry of entries) {
224
+ const srcPath = path.join(src, entry.name);
225
+ const destPath = path.join(dest, entry.name);
226
+
227
+ if (entry.isDirectory()) {
228
+ copyDirRecursive(srcPath, destPath);
229
+ } else {
230
+ fs.copyFileSync(srcPath, destPath);
231
+ }
232
+ }
233
+ }
234
+
235
+ // ==================== MCP URL 同步(供 update-mcp-url.js 调用) ====================
236
+
237
+ /**
238
+ * 不依赖 deps 的 CLI 可用性检测
239
+ */
240
+ function isCommandAvailable(cmd) {
241
+ try {
242
+ execSync(`which ${cmd} 2>/dev/null`, { stdio: "pipe", timeout: 3000 });
243
+ return true;
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * 不依赖 deps 的安全命令执行
251
+ */
252
+ function safeExecSync(command) {
253
+ try {
254
+ execSync(command, { stdio: "pipe", timeout: 10000 });
255
+ return true;
256
+ } catch (e) {
257
+ const stderr = e.stderr?.toString() || "";
258
+ if (stderr.includes("already exists")) return "already_exists";
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * 同步 Qwen Code 配置中的 MCP 服务器 URL
265
+ *
266
+ * 优先更新 Extension 扩展目录中的 qwen-extension.json,
267
+ * 同时更新 settings.json(兜底,给非扩展模式用户使用)。
268
+ *
269
+ * @param {string} targetUrl - 新的 MCP 服务器基础 URL(如 http://localhost:3456)
270
+ * @param {string} serverName - MCP 服务器名称
271
+ */
272
+ export function syncMcpUrl(targetUrl, serverName) {
273
+ const sseUrl = `${targetUrl}/sse`;
274
+
275
+ // 1. CLI 命令同步
276
+ if (isCommandAvailable("qwen")) {
277
+ safeExecSync(`qwen mcp remove --scope user ${serverName} 2>/dev/null`);
278
+ const result = safeExecSync(
279
+ `qwen mcp add --transport sse --scope user ${serverName} ${sseUrl}`,
280
+ );
281
+ if (result === true || result === "already_exists") {
282
+ console.log(`✓ Qwen Code CLI synced: ${sseUrl}`);
283
+ } else {
284
+ console.warn(`⚠️ Qwen Code CLI sync failed (settings.json will be updated directly)`);
285
+ }
286
+ } else {
287
+ console.log("⏭️ Skipping Qwen Code CLI sync: qwen not found");
288
+ }
289
+
290
+ // 2. 更新 Extension 扩展目录中的 qwen-extension.json
291
+ syncExtensionConfig(targetUrl, serverName);
292
+
293
+ // 3. 直接文件操作同步 ~/.qwen/settings.json(兜底)
294
+ syncQwenCodeSettingsDirect(targetUrl, serverName);
295
+ }
296
+
297
+ /**
298
+ * 更新已安装扩展目录中的 qwen-extension.json
299
+ */
300
+ function syncExtensionConfig(targetUrl, serverName) {
301
+ const extDir = path.join(getQwenCodeExtensionsDirectory(), serverName);
302
+ const manifestPath = path.join(extDir, "qwen-extension.json");
303
+
304
+ if (!fs.existsSync(manifestPath)) {
305
+ console.log("⏭️ Skipping extension manifest update: extension not installed");
306
+ return;
307
+ }
308
+
309
+ const manifest = readJsonFile(manifestPath);
310
+ if (!manifest.mcpServers || !manifest.mcpServers[serverName]) {
311
+ console.log("⏭️ Skipping extension manifest update: mcpServers entry not found");
312
+ return;
313
+ }
314
+
315
+ const sseUrl = `${targetUrl}/sse`;
316
+ if (manifest.mcpServers[serverName].url === sseUrl) {
317
+ console.log(`⏭️ Extension qwen-extension.json already up-to-date: ${sseUrl}`);
318
+ return;
319
+ }
320
+
321
+ manifest.mcpServers[serverName].url = sseUrl;
322
+ writeJsonFile(manifestPath, manifest);
323
+ console.log(`✓ Updated extension qwen-extension.json: ${sseUrl}`);
324
+ }
325
+
326
+ /**
327
+ * 直接文件操作同步 ~/.qwen/settings.json(独立于 deps)
328
+ */
329
+ function syncQwenCodeSettingsDirect(targetUrl, serverName) {
330
+ const settingsPath = getQwenCodeSettingsPath();
331
+ const sseUrl = `${targetUrl}/sse`;
332
+
333
+ let settings = {};
334
+ if (fs.existsSync(settingsPath)) {
335
+ try {
336
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
337
+ } catch { /* 忽略 */ }
338
+ }
339
+
340
+ if (!settings.mcpServers) settings.mcpServers = {};
341
+
342
+ const current = settings.mcpServers[serverName];
343
+ if (current && current.type === "sse" && current.url === sseUrl) {
344
+ console.log(`⏭️ Qwen Code ~/.qwen/settings.json already up-to-date: ${sseUrl}`);
345
+ return;
346
+ }
347
+
348
+ settings.mcpServers[serverName] = {
349
+ type: "sse",
350
+ url: sseUrl,
351
+ timeout: 30000,
352
+ };
353
+
354
+ const dir = path.dirname(settingsPath);
355
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
356
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
357
+ console.log(`✓ Updated Qwen Code ~/.qwen/settings.json: ${sseUrl}`);
358
+ }
359
+
360
+ // ==================== 导出 ====================
361
+
362
+ export function setupQwenCode(deps) {
363
+ console.log("\n📦 Setting up Qwen Code extension...\n");
364
+
365
+ // ---- 清理旧配置 ----
366
+ cleanupLegacyMcpConfig(deps);
367
+ cleanupLegacySkills(deps);
368
+ cleanupLegacyAgents(deps);
369
+
370
+ // ---- 项目级清理 ----
371
+ if (deps.PROJECT_ROOT && fs.existsSync(deps.PROJECT_ROOT)) {
372
+ try {
373
+ cleanupLegacyProjectConfig(deps);
374
+ } catch (e) {
375
+ console.log(`⚠️ Failed to clean up project config: ${e.message}`);
376
+ }
377
+ }
378
+
379
+ // ---- 安装扩展 ----
380
+ let extensionInstalled = false;
381
+
382
+ // 优先使用 CLI 安装
383
+ extensionInstalled = installExtensionViaCLI(deps);
384
+
385
+ // CLI 失败则直接复制
386
+ if (!extensionInstalled) {
387
+ extensionInstalled = installExtensionViaCopy(deps);
388
+ }
389
+
390
+ // ---- 结果 ----
391
+ if (extensionInstalled) {
392
+ console.log(`\n✅ Qwen Code extension installed successfully!`);
393
+ console.log(` Extension: ~/.qwen/extensions/${deps.getMcpServerName()}/`);
394
+ console.log(` Restart Qwen Code to activate the extension.`);
395
+ } else {
396
+ console.log(`\n⚠️ Qwen Code extension installation failed.`);
397
+ console.log(` You can manually install: qwen extensions install ${getExtensionSourcePath(deps)}`);
398
+ }
399
+
400
+ return {
401
+ qwenProjectInstalled: extensionInstalled,
402
+ extensionInstalled,
403
+ extensionsDirectory: getQwenCodeExtensionsDirectory(),
404
+ // 保留旧字段兼容 postinstall.js
405
+ skillsDirectory: path.join(getQwenCodeExtensionsDirectory(), deps.getMcpServerName(), "skills"),
406
+ agentsDirectory: path.join(getQwenCodeExtensionsDirectory(), deps.getMcpServerName(), "agents"),
407
+ };
408
+ }