@cloudbase/cloudbase-mcp 1.5.0 → 1.7.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.
- package/README.md +133 -50
- package/dist/auth.js +4 -0
- package/dist/cloudbase-manager.js +81 -10
- package/dist/index.js +7 -1
- package/dist/interactive-server.js +1621 -0
- package/dist/tools/env.js +59 -13
- package/dist/tools/functions.js +20 -2
- package/dist/tools/interactive.js +198 -0
- package/dist/tools/rag.js +3 -2
- package/dist/tools/setup.js +186 -0
- package/dist/utils/logger.js +119 -0
- package/package.json +9 -1
package/dist/tools/env.js
CHANGED
|
@@ -1,26 +1,72 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getCloudBaseManager } from '../cloudbase-manager.js';
|
|
3
3
|
import { logout } from '../auth.js';
|
|
4
|
+
import { clearUserEnvId, _promptAndSetEnvironmentId } from './interactive.js';
|
|
4
5
|
export function registerEnvTools(server) {
|
|
5
|
-
//
|
|
6
|
-
server.tool("
|
|
6
|
+
// login - 登录云开发环境
|
|
7
|
+
server.tool("login", "登录云开发环境并选择要使用的环境", {
|
|
8
|
+
forceUpdate: z.boolean().optional().describe("是否强制重新选择环境")
|
|
9
|
+
}, async ({ forceUpdate = false }) => {
|
|
10
|
+
try {
|
|
11
|
+
const { selectedEnvId, cancelled, error, noEnvs } = await _promptAndSetEnvironmentId(false);
|
|
12
|
+
if (error) {
|
|
13
|
+
return { content: [{ type: "text", text: error }] };
|
|
14
|
+
}
|
|
15
|
+
if (noEnvs) {
|
|
16
|
+
return { content: [{ type: "text", text: "当前账户下暂无可用的云开发环境,请先在腾讯云控制台创建环境" }] };
|
|
17
|
+
}
|
|
18
|
+
if (cancelled) {
|
|
19
|
+
return { content: [{ type: "text", text: "用户取消了登录" }] };
|
|
20
|
+
}
|
|
21
|
+
if (selectedEnvId) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `✅ 登录成功,当前环境: ${selectedEnvId}`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
throw new Error("登录失败");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: `登录失败: ${error instanceof Error ? error.message : String(error)}`
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// logout - 退出云开发环境
|
|
41
|
+
server.tool("logout", "退出云开发环境", {
|
|
7
42
|
confirm: z.literal("yes").describe("确认操作,默认传 yes")
|
|
8
|
-
}, async (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
}, async () => {
|
|
44
|
+
try {
|
|
45
|
+
// 登出账户
|
|
46
|
+
await logout();
|
|
47
|
+
// 清理环境ID配置
|
|
48
|
+
await clearUserEnvId();
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: "✅ 已退出登录"
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `退出失败: ${error instanceof Error ? error.message : String(error)}`
|
|
61
|
+
}]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
18
64
|
});
|
|
19
65
|
// listEnvs
|
|
20
66
|
server.tool("listEnvs", "获取所有云开发环境信息", {
|
|
21
67
|
confirm: z.literal("yes").describe("确认操作,默认传 yes")
|
|
22
68
|
}, async () => {
|
|
23
|
-
const cloudbase = await getCloudBaseManager();
|
|
69
|
+
const cloudbase = await getCloudBaseManager({ requireEnvId: false });
|
|
24
70
|
const result = await cloudbase.env.listEnvs();
|
|
25
71
|
return {
|
|
26
72
|
content: [
|
package/dist/tools/functions.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getCloudBaseManager } from '../cloudbase-manager.js';
|
|
3
|
+
// 支持的 Node.js 运行时列表
|
|
4
|
+
export const SUPPORTED_NODEJS_RUNTIMES = [
|
|
5
|
+
'Nodejs 18.15',
|
|
6
|
+
'Nodejs 16.13',
|
|
7
|
+
'Nodejs 14.18',
|
|
8
|
+
'Nodejs 12.16',
|
|
9
|
+
'Nodejs 10.15',
|
|
10
|
+
'Nodejs 8.9(即将下线)',
|
|
11
|
+
];
|
|
12
|
+
export const DEFAULT_NODEJS_RUNTIME = 'Nodejs 18.15';
|
|
3
13
|
export function registerFunctionTools(server) {
|
|
4
14
|
// getFunctionList - 获取云函数列表(推荐)
|
|
5
15
|
server.tool("getFunctionList", "获取云函数列表", {
|
|
@@ -27,7 +37,7 @@ export function registerFunctionTools(server) {
|
|
|
27
37
|
vpcId: z.string(),
|
|
28
38
|
subnetId: z.string()
|
|
29
39
|
}).optional().describe("私有网络配置"),
|
|
30
|
-
runtime: z.string().optional().describe("
|
|
40
|
+
runtime: z.string().optional().describe("运行时环境,可选值:" + SUPPORTED_NODEJS_RUNTIMES.join(',') + ",默认 Nodejs 18.15"),
|
|
31
41
|
installDependency: z.boolean().optional().describe("是否安装依赖,建议传 true"),
|
|
32
42
|
triggers: z.array(z.object({
|
|
33
43
|
name: z.string(),
|
|
@@ -45,6 +55,10 @@ export function registerFunctionTools(server) {
|
|
|
45
55
|
functionRootPath: z.string().optional().describe("函数根目录(云函数目录的父目录),这里需要传操作系统上文件的绝对路径,指定之后可以自动上传这部分的文件作为代码"),
|
|
46
56
|
force: z.boolean().describe("是否覆盖")
|
|
47
57
|
}, async ({ func, functionRootPath, force }) => {
|
|
58
|
+
// 自动填充默认 runtime
|
|
59
|
+
if (!func.runtime) {
|
|
60
|
+
func.runtime = DEFAULT_NODEJS_RUNTIME;
|
|
61
|
+
}
|
|
48
62
|
const cloudbase = await getCloudBaseManager();
|
|
49
63
|
const result = await cloudbase.functions.createFunction({
|
|
50
64
|
func,
|
|
@@ -94,9 +108,13 @@ export function registerFunctionTools(server) {
|
|
|
94
108
|
vpcId: z.string(),
|
|
95
109
|
subnetId: z.string()
|
|
96
110
|
}).optional().describe("VPC配置"),
|
|
97
|
-
runtime: z.string().optional().describe("
|
|
111
|
+
runtime: z.string().optional().describe("运行时(可选值:" + SUPPORTED_NODEJS_RUNTIMES.join(',') + ",默认 Nodejs 18.15)")
|
|
98
112
|
}).describe("函数配置")
|
|
99
113
|
}, async ({ funcParam }) => {
|
|
114
|
+
// 自动填充默认 runtime
|
|
115
|
+
if (!funcParam.runtime) {
|
|
116
|
+
funcParam.runtime = DEFAULT_NODEJS_RUNTIME;
|
|
117
|
+
}
|
|
100
118
|
const cloudbase = await getCloudBaseManager();
|
|
101
119
|
const result = await cloudbase.functions.updateFunctionConfig(funcParam);
|
|
102
120
|
return {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getInteractiveServer } from "../interactive-server.js";
|
|
3
|
+
import { getCloudBaseManager } from '../cloudbase-manager.js';
|
|
4
|
+
import { getLoginState } from '../auth.js';
|
|
5
|
+
import { debug, warn } from '../utils/logger.js';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
export function registerInteractiveTools(server) {
|
|
10
|
+
// 统一的交互式对话工具
|
|
11
|
+
server.tool("interactiveDialog", "统一的交互式对话工具,支持需求澄清和任务确认,当需要和用户确认下一步的操作的时候,可以调用这个工具的clarify,如果有敏感的操作,需要用户确认,可以调用这个工具的confirm", {
|
|
12
|
+
type: z.enum(['clarify', 'confirm']).describe("交互类型: clarify=需求澄清, confirm=任务确认"),
|
|
13
|
+
message: z.string().optional().describe("对话消息内容"),
|
|
14
|
+
options: z.array(z.string()).optional().describe("可选的预设选项"),
|
|
15
|
+
forceUpdate: z.boolean().optional().describe("是否强制更新环境ID配置"),
|
|
16
|
+
risks: z.array(z.string()).optional().describe("操作风险提示")
|
|
17
|
+
}, async ({ type, message, options, forceUpdate = false, risks }) => {
|
|
18
|
+
try {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'clarify': {
|
|
21
|
+
if (!message) {
|
|
22
|
+
throw new Error("需求澄清必须提供message参数");
|
|
23
|
+
}
|
|
24
|
+
const interactiveServer = getInteractiveServer();
|
|
25
|
+
const result = await interactiveServer.clarifyRequest(message, options);
|
|
26
|
+
if (result.cancelled) {
|
|
27
|
+
return { content: [{ type: "text", text: "用户取消了需求澄清" }] };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `📝 用户澄清反馈:\n${result.data.response}`
|
|
33
|
+
}]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
case 'confirm': {
|
|
37
|
+
if (!message) {
|
|
38
|
+
throw new Error("任务确认必须提供message参数");
|
|
39
|
+
}
|
|
40
|
+
let dialogMessage = `🎯 即将执行任务:\n${message}`;
|
|
41
|
+
if (risks && risks.length > 0) {
|
|
42
|
+
dialogMessage += `\n\n⚠️ 风险提示:\n${risks.map(risk => `• ${risk}`).join('\n')}`;
|
|
43
|
+
}
|
|
44
|
+
dialogMessage += `\n\n是否继续执行此任务?`;
|
|
45
|
+
const dialogOptions = options || ["确认执行", "取消操作", "需要修改任务"];
|
|
46
|
+
const interactiveServer = getInteractiveServer();
|
|
47
|
+
const result = await interactiveServer.clarifyRequest(dialogMessage, dialogOptions);
|
|
48
|
+
if (result.cancelled || result.data.response.includes('取消')) {
|
|
49
|
+
return { content: [{ type: "text", text: "❌ 用户取消了任务执行" }] };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `✅ 用户确认: ${result.data.response}`
|
|
55
|
+
}]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `交互对话出错: ${error instanceof Error ? error.message : String(error)}`
|
|
65
|
+
}]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// 封装了获取环境、提示选择、保存配置的核心逻辑
|
|
71
|
+
export async function _promptAndSetEnvironmentId(autoSelectSingle) {
|
|
72
|
+
// 1. 确保用户已登录
|
|
73
|
+
const loginState = await getLoginState();
|
|
74
|
+
debug('loginState', loginState);
|
|
75
|
+
if (!loginState) {
|
|
76
|
+
debug('请先登录云开发账户');
|
|
77
|
+
return { selectedEnvId: null, cancelled: false, error: "请先登录云开发账户" };
|
|
78
|
+
}
|
|
79
|
+
// 2. 获取可用环境列表
|
|
80
|
+
const cloudbase = await getCloudBaseManager({ requireEnvId: false });
|
|
81
|
+
const envResult = await cloudbase.env.listEnvs();
|
|
82
|
+
debug('envResult', envResult);
|
|
83
|
+
if (!envResult || !envResult.EnvList || envResult.EnvList.length === 0) {
|
|
84
|
+
return { selectedEnvId: null, cancelled: false, noEnvs: true };
|
|
85
|
+
}
|
|
86
|
+
const { EnvList } = envResult;
|
|
87
|
+
let selectedEnvId = null;
|
|
88
|
+
// 3. 根据情况选择或提示用户选择
|
|
89
|
+
if (autoSelectSingle && EnvList.length === 1 && EnvList[0].EnvId) {
|
|
90
|
+
selectedEnvId = EnvList[0].EnvId;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const interactiveServer = getInteractiveServer();
|
|
94
|
+
const result = await interactiveServer.collectEnvId(EnvList);
|
|
95
|
+
if (result.cancelled) {
|
|
96
|
+
return { selectedEnvId: null, cancelled: true };
|
|
97
|
+
}
|
|
98
|
+
selectedEnvId = result.data.envId;
|
|
99
|
+
}
|
|
100
|
+
// 4. 保存并设置环境ID
|
|
101
|
+
if (selectedEnvId) {
|
|
102
|
+
await saveEnvIdToUserConfig(selectedEnvId);
|
|
103
|
+
process.env.CLOUDBASE_ENV_ID = selectedEnvId;
|
|
104
|
+
}
|
|
105
|
+
return { selectedEnvId, cancelled: false };
|
|
106
|
+
}
|
|
107
|
+
// 获取用户配置文件路径
|
|
108
|
+
function getUserConfigPath() {
|
|
109
|
+
return path.join(os.homedir(), '.cloudbase-env-id');
|
|
110
|
+
}
|
|
111
|
+
// 保存环境ID到用户配置文件
|
|
112
|
+
async function saveEnvIdToUserConfig(envId) {
|
|
113
|
+
const configPath = getUserConfigPath();
|
|
114
|
+
try {
|
|
115
|
+
const config = {
|
|
116
|
+
envId,
|
|
117
|
+
updatedAt: new Date().toISOString(),
|
|
118
|
+
version: '1.0'
|
|
119
|
+
};
|
|
120
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
121
|
+
// 环境ID已保存 - 静默操作避免干扰MCP返回值
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('保存环境ID配置失败:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 从用户配置文件读取环境ID
|
|
129
|
+
async function loadEnvIdFromUserConfig() {
|
|
130
|
+
const configPath = getUserConfigPath();
|
|
131
|
+
try {
|
|
132
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
133
|
+
const config = JSON.parse(configContent);
|
|
134
|
+
const envId = config.envId || null;
|
|
135
|
+
if (!envId) {
|
|
136
|
+
warn(`Config file ${configPath} found, but 'envId' property is missing or empty.`);
|
|
137
|
+
}
|
|
138
|
+
return envId;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// 文件不存在是正常情况,不应告警。只在文件存在但有问题时告警。
|
|
142
|
+
if (err.code !== 'ENOENT') {
|
|
143
|
+
warn(`Failed to load envId from config file at ${configPath}. Error: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
debug(`Env config file not found at ${configPath}, which is expected if not set.`);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 检查并设置环境ID
|
|
152
|
+
export async function ensureEnvId() {
|
|
153
|
+
// 优先使用进程环境变量
|
|
154
|
+
if (process.env.CLOUDBASE_ENV_ID) {
|
|
155
|
+
return process.env.CLOUDBASE_ENV_ID;
|
|
156
|
+
}
|
|
157
|
+
// 从用户配置文件读取
|
|
158
|
+
const envId = await loadEnvIdFromUserConfig();
|
|
159
|
+
if (envId) {
|
|
160
|
+
// 设置到进程环境变量中
|
|
161
|
+
process.env.CLOUDBASE_ENV_ID = envId;
|
|
162
|
+
return envId;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// 清理用户环境ID配置
|
|
167
|
+
export async function clearUserEnvId() {
|
|
168
|
+
const configPath = getUserConfigPath();
|
|
169
|
+
try {
|
|
170
|
+
await fs.unlink(configPath);
|
|
171
|
+
// 清理进程环境变量
|
|
172
|
+
delete process.env.CLOUDBASE_ENV_ID;
|
|
173
|
+
delete process.env.TENCENTCLOUD_SECRETID;
|
|
174
|
+
delete process.env.TENCENTCLOUD_SECRETKEY;
|
|
175
|
+
delete process.env.TENCENTCLOUD_SESSIONTOKEN;
|
|
176
|
+
// 环境ID配置已清理 - 静默操作
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
// 文件不存在或删除失败,忽略错误
|
|
180
|
+
// 环境ID配置文件不存在或已清理 - 静默操作
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 自动设置环境ID(无需MCP工具调用)
|
|
184
|
+
export async function autoSetupEnvironmentId() {
|
|
185
|
+
try {
|
|
186
|
+
const { selectedEnvId, cancelled, error, noEnvs } = await _promptAndSetEnvironmentId(true);
|
|
187
|
+
if (error || noEnvs || cancelled) {
|
|
188
|
+
debug('Auto setup environment ID interrupted or failed silently.', { error, noEnvs, cancelled });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
debug('Auto setup environment ID successful.', { selectedEnvId });
|
|
192
|
+
return selectedEnvId;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('自动配置环境ID时出错:', error);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
package/dist/tools/rag.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// 1. 枚举定义
|
|
3
|
-
const KnowledgeBaseEnum = z.enum(["cloudbase", "scf"]);
|
|
3
|
+
const KnowledgeBaseEnum = z.enum(["cloudbase", "scf", "miniprogram"]);
|
|
4
4
|
// 2. 枚举到后端 id 的映射
|
|
5
5
|
const KnowledgeBaseIdMap = {
|
|
6
6
|
cloudbase: "ykfzskv4_ad28",
|
|
7
7
|
scf: "scfsczskzyws_4bdc",
|
|
8
|
+
miniprogram: "xcxzskws_25d8",
|
|
8
9
|
};
|
|
9
10
|
// 安全 JSON.parse
|
|
10
11
|
function safeParse(str) {
|
|
@@ -36,7 +37,7 @@ export function registerRagTools(server) {
|
|
|
36
37
|
// 知识库检索
|
|
37
38
|
server.tool('searchKnowledgeBase', '云开发知识库智能检索工具,支持云开发与云函数知识的向量查询', {
|
|
38
39
|
threshold: z.number().default(0.5).optional().describe("相似性检索阈值"),
|
|
39
|
-
id: KnowledgeBaseEnum.describe("知识库范围,cloudbase=云开发全量知识,scf
|
|
40
|
+
id: KnowledgeBaseEnum.describe("知识库范围,cloudbase=云开发全量知识,scf=云开发的云函数知识, miniprogram=小程序知识(不包含云开发与云函数知识)"),
|
|
40
41
|
content: z.string().describe("检索内容"),
|
|
41
42
|
options: z.object({
|
|
42
43
|
chunkExpand: z.array(z.number()).min(2).max(2).default([3, 3]).describe("指定返回的文档内容的展开长度,例如 [3,3]代表前后展开长度"),
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as fsPromises from "fs/promises";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as https from "https";
|
|
7
|
+
import * as http from "http";
|
|
8
|
+
import * as unzipper from "unzipper";
|
|
9
|
+
// CloudBase 模板配置
|
|
10
|
+
const TEMPLATES = {
|
|
11
|
+
"react": {
|
|
12
|
+
description: "React + CloudBase 全栈应用模板",
|
|
13
|
+
url: "https://static.cloudbase.net/cloudbase-examples/web-cloudbase-react-template.zip"
|
|
14
|
+
},
|
|
15
|
+
"miniprogram": {
|
|
16
|
+
description: "微信小程序 + 云开发模板",
|
|
17
|
+
url: "https://static.cloudbase.net/cloudbase-examples/miniprogram-cloudbase-miniprogram-template.zip"
|
|
18
|
+
},
|
|
19
|
+
"rules": {
|
|
20
|
+
description: "AI编辑器配置模板(包含所有主流编辑器配置)",
|
|
21
|
+
url: "https://static.cloudbase.net/cloudbase-examples/web-cloudbase-project.zip"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
// 下载文件到临时目录
|
|
25
|
+
async function downloadFile(url, filePath) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const client = url.startsWith('https:') ? https : http;
|
|
28
|
+
client.get(url, (res) => {
|
|
29
|
+
if (res.statusCode === 200) {
|
|
30
|
+
const file = fs.createWriteStream(filePath);
|
|
31
|
+
res.pipe(file);
|
|
32
|
+
file.on('finish', () => {
|
|
33
|
+
file.close();
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
file.on('error', reject);
|
|
37
|
+
}
|
|
38
|
+
else if (res.statusCode === 302 || res.statusCode === 301) {
|
|
39
|
+
// 处理重定向
|
|
40
|
+
if (res.headers.location) {
|
|
41
|
+
downloadFile(res.headers.location, filePath).then(resolve).catch(reject);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reject(new Error('重定向但没有location header'));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
reject(new Error(`下载失败,状态码: ${res.statusCode}`));
|
|
49
|
+
}
|
|
50
|
+
}).on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// 解压ZIP文件
|
|
54
|
+
async function extractZip(zipPath, extractPath) {
|
|
55
|
+
try {
|
|
56
|
+
// 创建解压目录
|
|
57
|
+
await fsPromises.mkdir(extractPath, { recursive: true });
|
|
58
|
+
// 使用 unzipper 库进行解压,兼容性更好
|
|
59
|
+
await fs.createReadStream(zipPath)
|
|
60
|
+
.pipe(unzipper.Extract({ path: extractPath }))
|
|
61
|
+
.promise();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`解压失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 获取目录下所有文件的相对路径列表
|
|
68
|
+
async function getAllFiles(dir, baseDir = dir) {
|
|
69
|
+
const files = [];
|
|
70
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const fullPath = path.join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
const subFiles = await getAllFiles(fullPath, baseDir);
|
|
75
|
+
files.push(...subFiles);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
files.push(path.relative(baseDir, fullPath));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return files;
|
|
82
|
+
}
|
|
83
|
+
// 复制文件,不覆盖已存在的文件
|
|
84
|
+
async function copyFileIfNotExists(src, dest) {
|
|
85
|
+
try {
|
|
86
|
+
// 检查目标文件是否存在
|
|
87
|
+
if (fs.existsSync(dest)) {
|
|
88
|
+
return { copied: false, reason: '文件已存在' };
|
|
89
|
+
}
|
|
90
|
+
// 创建目标目录
|
|
91
|
+
await fsPromises.mkdir(path.dirname(dest), { recursive: true });
|
|
92
|
+
// 复制文件
|
|
93
|
+
await fsPromises.copyFile(src, dest);
|
|
94
|
+
return { copied: true };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return { copied: false, reason: `复制失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function registerSetupTools(server) {
|
|
101
|
+
server.tool("downloadTemplate", `自动下载并部署CloudBase项目模板。
|
|
102
|
+
|
|
103
|
+
支持的模板:
|
|
104
|
+
- react: React + CloudBase 全栈应用模板
|
|
105
|
+
- miniprogram: 微信小程序 + 云开发模板
|
|
106
|
+
- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置
|
|
107
|
+
|
|
108
|
+
工具会自动下载模板到临时目录,解压后如果检测到WORKSPACE_FOLDER_PATHS环境变量,则复制到项目目录(不覆盖已存在文件)。`, {
|
|
109
|
+
template: z.enum(["react", "miniprogram", "rules"]).describe("要下载的模板类型")
|
|
110
|
+
}, async ({ template }) => {
|
|
111
|
+
try {
|
|
112
|
+
const templateConfig = TEMPLATES[template];
|
|
113
|
+
if (!templateConfig) {
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `❌ 不支持的模板类型: ${template}`
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// 创建临时目录
|
|
124
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudbase-template-'));
|
|
125
|
+
const zipPath = path.join(tempDir, 'template.zip');
|
|
126
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
127
|
+
// 下载和解压
|
|
128
|
+
await downloadFile(templateConfig.url, zipPath);
|
|
129
|
+
await extractZip(zipPath, extractDir);
|
|
130
|
+
const extractedFiles = await getAllFiles(extractDir);
|
|
131
|
+
// 检查是否需要复制到项目目录
|
|
132
|
+
const workspaceFolder = process.env.WORKSPACE_FOLDER_PATHS;
|
|
133
|
+
let finalFiles = [];
|
|
134
|
+
let copiedCount = 0;
|
|
135
|
+
let skippedCount = 0;
|
|
136
|
+
const results = [];
|
|
137
|
+
if (workspaceFolder) {
|
|
138
|
+
for (const relativePath of extractedFiles) {
|
|
139
|
+
const srcPath = path.join(extractDir, relativePath);
|
|
140
|
+
const destPath = path.join(workspaceFolder, relativePath);
|
|
141
|
+
const copyResult = await copyFileIfNotExists(srcPath, destPath);
|
|
142
|
+
if (copyResult.copied) {
|
|
143
|
+
copiedCount++;
|
|
144
|
+
finalFiles.push(destPath);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
skippedCount++;
|
|
148
|
+
finalFiles.push(srcPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
results.push(`✅ ${templateConfig.description} 同步完成`);
|
|
152
|
+
results.push(`📁 保存在临时目录: ${extractDir}`);
|
|
153
|
+
results.push(`📊 复制了 ${copiedCount} 个文件${skippedCount > 0 ? `,跳过 ${skippedCount} 个已存在文件` : ''}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
finalFiles = extractedFiles.map(relativePath => path.join(extractDir, relativePath));
|
|
157
|
+
results.push(`✅ ${templateConfig.description} 下载完成`);
|
|
158
|
+
results.push(`📁 保存在临时目录: ${extractDir}`);
|
|
159
|
+
}
|
|
160
|
+
// 文件路径列表
|
|
161
|
+
results.push('');
|
|
162
|
+
results.push('📋 文件列表:');
|
|
163
|
+
finalFiles.forEach(filePath => {
|
|
164
|
+
results.push(`${filePath}`);
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: results.join('\n')
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: `❌ 下载模板失败: ${error instanceof Error ? error.message : '未知错误'}`
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
export var LogLevel;
|
|
5
|
+
(function (LogLevel) {
|
|
6
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
7
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
8
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
9
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
10
|
+
})(LogLevel || (LogLevel = {}));
|
|
11
|
+
class Logger {
|
|
12
|
+
enabled;
|
|
13
|
+
level;
|
|
14
|
+
logFile;
|
|
15
|
+
useConsole;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
// 通过环境变量控制是否启用日志
|
|
18
|
+
this.enabled = options.enabled ?? (process.env.MCP_DEBUG === 'true' || process.env.NODE_ENV === 'development');
|
|
19
|
+
this.level = options.level ?? LogLevel.INFO;
|
|
20
|
+
this.useConsole = options.console ?? false;
|
|
21
|
+
// 默认日志文件路径
|
|
22
|
+
this.logFile = options.logFile ?? path.join(os.tmpdir(), 'cloudbase-mcp.log');
|
|
23
|
+
}
|
|
24
|
+
async writeLog(level, message, data) {
|
|
25
|
+
if (!this.enabled || level < this.level)
|
|
26
|
+
return;
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const levelName = LogLevel[level];
|
|
29
|
+
const logMessage = data
|
|
30
|
+
? `[${timestamp}] [${levelName}] ${message} ${JSON.stringify(data, null, 2)}`
|
|
31
|
+
: `[${timestamp}] [${levelName}] ${message}`;
|
|
32
|
+
// 输出到控制台(仅在调试模式下)
|
|
33
|
+
if (this.useConsole && process.env.NODE_ENV === 'development') {
|
|
34
|
+
console.error(logMessage); // 使用 stderr 避免污染 stdout
|
|
35
|
+
}
|
|
36
|
+
// 写入日志文件
|
|
37
|
+
if (this.logFile) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.appendFile(this.logFile, logMessage + '\n');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// 静默处理日志写入错误,避免影响主要功能
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
debug(message, data) {
|
|
47
|
+
this.writeLog(LogLevel.DEBUG, message, data);
|
|
48
|
+
}
|
|
49
|
+
info(message, data) {
|
|
50
|
+
this.writeLog(LogLevel.INFO, message, data);
|
|
51
|
+
}
|
|
52
|
+
warn(message, data) {
|
|
53
|
+
this.writeLog(LogLevel.WARN, message, data);
|
|
54
|
+
}
|
|
55
|
+
error(message, data) {
|
|
56
|
+
this.writeLog(LogLevel.ERROR, message, data);
|
|
57
|
+
}
|
|
58
|
+
// 设置日志级别
|
|
59
|
+
setLevel(level) {
|
|
60
|
+
this.level = level;
|
|
61
|
+
}
|
|
62
|
+
// 启用/禁用日志
|
|
63
|
+
setEnabled(enabled) {
|
|
64
|
+
this.enabled = enabled;
|
|
65
|
+
}
|
|
66
|
+
// 获取日志文件路径
|
|
67
|
+
getLogFile() {
|
|
68
|
+
return this.logFile;
|
|
69
|
+
}
|
|
70
|
+
// 清理日志文件
|
|
71
|
+
async clearLogs() {
|
|
72
|
+
if (this.logFile) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.writeFile(this.logFile, '');
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// 静默处理
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 读取日志内容
|
|
82
|
+
async getLogs(maxLines = 1000) {
|
|
83
|
+
if (!this.logFile)
|
|
84
|
+
return [];
|
|
85
|
+
try {
|
|
86
|
+
const content = await fs.readFile(this.logFile, 'utf-8');
|
|
87
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
88
|
+
// 返回最近的 maxLines 行
|
|
89
|
+
return lines.slice(-maxLines);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return [`读取日志文件失败: ${error instanceof Error ? error.message : String(error)}`];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 获取日志状态信息
|
|
96
|
+
getStatus() {
|
|
97
|
+
return {
|
|
98
|
+
enabled: this.enabled,
|
|
99
|
+
level: LogLevel[this.level],
|
|
100
|
+
logFile: this.logFile,
|
|
101
|
+
useConsole: this.useConsole
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 创建全局 logger 实例
|
|
106
|
+
export const logger = new Logger({
|
|
107
|
+
enabled: process.env.MCP_DEBUG === 'true',
|
|
108
|
+
level: LogLevel.INFO,
|
|
109
|
+
console: process.env.NODE_ENV === 'development'
|
|
110
|
+
});
|
|
111
|
+
// 便捷的导出函数
|
|
112
|
+
export const debug = (message, data) => logger.error(message, data);
|
|
113
|
+
export const info = (message, data) => logger.info(message, data);
|
|
114
|
+
export const warn = (message, data) => logger.warn(message, data);
|
|
115
|
+
export const error = (message, data) => logger.error(message, data);
|
|
116
|
+
// 日志管理函数
|
|
117
|
+
export const getLogs = (maxLines) => logger.getLogs(maxLines);
|
|
118
|
+
export const getLoggerStatus = () => logger.getStatus();
|
|
119
|
+
export const clearLogs = () => logger.clearLogs();
|