flex-mcp 1.0.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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/api/auth.d.ts +6 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +111 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/configs.d.ts +6 -0
- package/dist/api/configs.d.ts.map +1 -0
- package/dist/api/configs.js +693 -0
- package/dist/api/configs.js.map +1 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +24 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/mcp.d.ts +7 -0
- package/dist/api/mcp.d.ts.map +1 -0
- package/dist/api/mcp.js +46 -0
- package/dist/api/mcp.js.map +1 -0
- package/dist/api/middleware.d.ts +25 -0
- package/dist/api/middleware.d.ts.map +1 -0
- package/dist/api/middleware.js +65 -0
- package/dist/api/middleware.js.map +1 -0
- package/dist/api/prompts.d.ts +6 -0
- package/dist/api/prompts.d.ts.map +1 -0
- package/dist/api/prompts.js +260 -0
- package/dist/api/prompts.js.map +1 -0
- package/dist/api/resources.d.ts +6 -0
- package/dist/api/resources.d.ts.map +1 -0
- package/dist/api/resources.js +159 -0
- package/dist/api/resources.js.map +1 -0
- package/dist/api/tables.d.ts +15 -0
- package/dist/api/tables.d.ts.map +1 -0
- package/dist/api/tables.js +702 -0
- package/dist/api/tables.js.map +1 -0
- package/dist/api/users.d.ts +6 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +96 -0
- package/dist/api/users.js.map +1 -0
- package/dist/auth/index.d.ts +15 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +126 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +288 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +164 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +646 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +73 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/aggregator.d.ts +78 -0
- package/dist/mcp/aggregator.d.ts.map +1 -0
- package/dist/mcp/aggregator.js +1266 -0
- package/dist/mcp/aggregator.js.map +1 -0
- package/dist/mcp/component-loader.d.ts +17 -0
- package/dist/mcp/component-loader.d.ts.map +1 -0
- package/dist/mcp/component-loader.js +131 -0
- package/dist/mcp/component-loader.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +107 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-client.d.ts +53 -0
- package/dist/mcp/mcp-client.d.ts.map +1 -0
- package/dist/mcp/mcp-client.js +418 -0
- package/dist/mcp/mcp-client.js.map +1 -0
- package/dist/mcp/post-sse-transport.d.ts +52 -0
- package/dist/mcp/post-sse-transport.d.ts.map +1 -0
- package/dist/mcp/post-sse-transport.js +375 -0
- package/dist/mcp/post-sse-transport.js.map +1 -0
- package/dist/mcp/service.d.ts +49 -0
- package/dist/mcp/service.d.ts.map +1 -0
- package/dist/mcp/service.js +358 -0
- package/dist/mcp/service.js.map +1 -0
- package/dist/mcp/tool-sync.d.ts +27 -0
- package/dist/mcp/tool-sync.d.ts.map +1 -0
- package/dist/mcp/tool-sync.js +200 -0
- package/dist/mcp/tool-sync.js.map +1 -0
- package/dist/script/compiler.d.ts +15 -0
- package/dist/script/compiler.d.ts.map +1 -0
- package/dist/script/compiler.js +163 -0
- package/dist/script/compiler.js.map +1 -0
- package/dist/script/context.d.ts +11 -0
- package/dist/script/context.d.ts.map +1 -0
- package/dist/script/context.js +786 -0
- package/dist/script/context.js.map +1 -0
- package/dist/script/executor.d.ts +43 -0
- package/dist/script/executor.d.ts.map +1 -0
- package/dist/script/executor.js +126 -0
- package/dist/script/executor.js.map +1 -0
- package/dist/static.d.ts +21 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +95 -0
- package/dist/static.js.map +1 -0
- package/dist/types/index.d.ts +337 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/id.d.ts +5 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +8 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +31 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prompt-template.d.ts +15 -0
- package/dist/utils/prompt-template.d.ts.map +1 -0
- package/dist/utils/prompt-template.js +82 -0
- package/dist/utils/prompt-template.js.map +1 -0
- package/dist/utils/telemetry.d.ts +45 -0
- package/dist/utils/telemetry.d.ts.map +1 -0
- package/dist/utils/telemetry.js +245 -0
- package/dist/utils/telemetry.js.map +1 -0
- package/dist/utils/variables.d.ts +24 -0
- package/dist/utils/variables.d.ts.map +1 -0
- package/dist/utils/variables.js +45 -0
- package/dist/utils/variables.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 脚本执行上下文
|
|
3
|
+
* 提供脚本执行时可用的 API
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { exec, spawn } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { rgPath } from '@lvce-editor/ripgrep';
|
|
12
|
+
import { db, sqlite } from '../db/index.js';
|
|
13
|
+
import { tableDefinitions, tableData } from '../db/schema.js';
|
|
14
|
+
import { eq, and } from 'drizzle-orm';
|
|
15
|
+
import { getUserById } from '../auth/index.js';
|
|
16
|
+
import { resolveUserPath, getFileSystemDir } from '../config.js';
|
|
17
|
+
import { generateId } from '../utils/id.js';
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
export async function createScriptContext(config, mcpClientConfig, aggregator) {
|
|
20
|
+
// 获取用户信息
|
|
21
|
+
const user = await getUserById(config.userId);
|
|
22
|
+
if (!user) {
|
|
23
|
+
throw new Error(`User ${config.userId} not found`);
|
|
24
|
+
}
|
|
25
|
+
// 确保用户隔离目录存在
|
|
26
|
+
const userDir = path.join(getFileSystemDir(), user.id);
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(userDir);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
await fs.mkdir(userDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
// 当前用户信息(包含邮箱)
|
|
35
|
+
user: {
|
|
36
|
+
id: user.id,
|
|
37
|
+
email: user.email,
|
|
38
|
+
role: user.role,
|
|
39
|
+
createdAt: user.createdAt,
|
|
40
|
+
updatedAt: user.updatedAt,
|
|
41
|
+
},
|
|
42
|
+
// MCP 客户端配置(来自 Cursor mcp.json)
|
|
43
|
+
mcpClient: {
|
|
44
|
+
headers: mcpClientConfig?.headers || {},
|
|
45
|
+
config: mcpClientConfig?.config || {},
|
|
46
|
+
},
|
|
47
|
+
// HTTP 请求
|
|
48
|
+
http: {
|
|
49
|
+
async get(url, options) {
|
|
50
|
+
const response = await axios.get(url, options);
|
|
51
|
+
return response.data;
|
|
52
|
+
},
|
|
53
|
+
async post(url, data, options) {
|
|
54
|
+
const response = await axios.post(url, data, options);
|
|
55
|
+
return response.data;
|
|
56
|
+
},
|
|
57
|
+
async put(url, data, options) {
|
|
58
|
+
const response = await axios.put(url, data, options);
|
|
59
|
+
return response.data;
|
|
60
|
+
},
|
|
61
|
+
async delete(url, options) {
|
|
62
|
+
const response = await axios.delete(url, options);
|
|
63
|
+
return response.data;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
// 文件操作(用户隔离)
|
|
67
|
+
fs: {
|
|
68
|
+
async readFile(filePath) {
|
|
69
|
+
const resolvedPath = resolveUserPath(filePath, user.id);
|
|
70
|
+
return await fs.readFile(resolvedPath, 'utf-8');
|
|
71
|
+
},
|
|
72
|
+
async writeFile(filePath, content) {
|
|
73
|
+
const resolvedPath = resolveUserPath(filePath, user.id);
|
|
74
|
+
// 确保父目录存在
|
|
75
|
+
const dir = path.dirname(resolvedPath);
|
|
76
|
+
await fs.mkdir(dir, { recursive: true });
|
|
77
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
78
|
+
},
|
|
79
|
+
async exists(filePath) {
|
|
80
|
+
try {
|
|
81
|
+
const resolvedPath = resolveUserPath(filePath, user.id);
|
|
82
|
+
await fs.access(resolvedPath);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async listDir(dirPath) {
|
|
90
|
+
const resolvedPath = resolveUserPath(dirPath, user.id);
|
|
91
|
+
return await fs.readdir(resolvedPath);
|
|
92
|
+
},
|
|
93
|
+
async mkdir(dirPath) {
|
|
94
|
+
const resolvedPath = resolveUserPath(dirPath, user.id);
|
|
95
|
+
await fs.mkdir(resolvedPath, { recursive: true });
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
// 命令执行(在用户隔离目录中执行)
|
|
99
|
+
cmd: {
|
|
100
|
+
async exec(command, options) {
|
|
101
|
+
const userDir = path.join(getFileSystemDir(), user.id);
|
|
102
|
+
const cwd = options?.cwd
|
|
103
|
+
? path.resolve(userDir, options.cwd.startsWith('/') ? options.cwd.substring(1) : options.cwd)
|
|
104
|
+
: userDir;
|
|
105
|
+
// 确保工作目录在用户目录内
|
|
106
|
+
if (!cwd.startsWith(path.resolve(userDir))) {
|
|
107
|
+
throw new Error(`Working directory must be within user directory`);
|
|
108
|
+
}
|
|
109
|
+
const timeout = options?.timeout || 30000; // 默认 30 秒超时
|
|
110
|
+
try {
|
|
111
|
+
const { stdout, stderr } = await Promise.race([
|
|
112
|
+
execAsync(command, {
|
|
113
|
+
cwd,
|
|
114
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
115
|
+
}),
|
|
116
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Command timeout after ${timeout}ms`)), timeout))
|
|
117
|
+
]);
|
|
118
|
+
return {
|
|
119
|
+
stdout: stdout || '',
|
|
120
|
+
stderr: stderr || '',
|
|
121
|
+
exitCode: 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// execAsync 在命令失败时会抛出错误,但包含 stdout 和 stderr
|
|
126
|
+
if (error.stdout !== undefined || error.stderr !== undefined) {
|
|
127
|
+
return {
|
|
128
|
+
stdout: error.stdout || '',
|
|
129
|
+
stderr: error.stderr || '',
|
|
130
|
+
exitCode: error.code || 1,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
// 工具函数
|
|
138
|
+
utils: {
|
|
139
|
+
md5(input) {
|
|
140
|
+
return crypto.createHash('md5').update(input).digest('hex');
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
// ripgrep 搜索
|
|
144
|
+
ripgrep: {
|
|
145
|
+
/**
|
|
146
|
+
* 使用 ripgrep 搜索文件内容
|
|
147
|
+
* @param pattern 搜索模式(支持正则表达式)
|
|
148
|
+
* @param options 搜索选项
|
|
149
|
+
* @param options.cwd 工作目录(相对于用户隔离目录,默认:用户根目录)
|
|
150
|
+
* @param options.caseSensitive 是否大小写敏感(默认:false)
|
|
151
|
+
* @param options.multiline 是否启用多行匹配(默认:false)
|
|
152
|
+
* @param options.files 文件过滤模式(glob 模式数组,默认:[],匹配所有文件)
|
|
153
|
+
* @param options.excludeFiles 排除文件模式(glob 模式数组,默认:[])
|
|
154
|
+
* @param options.maxCount 每个文件的最大匹配数(默认:无限制)
|
|
155
|
+
* @param options.maxResults 总结果数的最大限制(默认:无限制)
|
|
156
|
+
* @param options.maxDepth 最大递归深度(默认:无限制)
|
|
157
|
+
* @param options.contextBefore 匹配行之前显示的行数(默认:0)
|
|
158
|
+
* @param options.contextAfter 匹配行之后显示的行数(默认:0)
|
|
159
|
+
* @param options.context 匹配行前后显示的行数(等同于同时设置 contextBefore 和 contextAfter,默认:0)
|
|
160
|
+
* @param options.fixedStrings 将模式视为固定字符串而非正则表达式(默认:false)
|
|
161
|
+
* @param options.wordRegexp 只匹配完整的单词(默认:false)
|
|
162
|
+
* @param options.fileTypes 文件类型过滤(如:['js', 'ts'],默认:[],匹配所有类型)
|
|
163
|
+
* @param options.excludeTypes 排除的文件类型(默认:[])
|
|
164
|
+
* @param options.filesWithMatches 只返回包含匹配的文件名(默认:false)
|
|
165
|
+
* @param options.count 只返回每个文件的匹配数量(默认:false)
|
|
166
|
+
* @param options.noIgnore 忽略 .gitignore 和 .ignore 文件(默认:false)
|
|
167
|
+
* @param options.smartCase 智能大小写:如果模式包含大写字母则大小写敏感(默认:false)
|
|
168
|
+
* @returns 匹配结果数组
|
|
169
|
+
*/
|
|
170
|
+
async search(pattern, options) {
|
|
171
|
+
const userDir = path.join(getFileSystemDir(), user.id);
|
|
172
|
+
const cwd = options?.cwd
|
|
173
|
+
? path.resolve(userDir, options.cwd.startsWith('/') ? options.cwd.substring(1) : options.cwd)
|
|
174
|
+
: userDir;
|
|
175
|
+
// 确保工作目录在用户目录内
|
|
176
|
+
if (!cwd.startsWith(path.resolve(userDir))) {
|
|
177
|
+
throw new Error(`Working directory must be within user directory`);
|
|
178
|
+
}
|
|
179
|
+
const args = [
|
|
180
|
+
'--json',
|
|
181
|
+
'--no-heading',
|
|
182
|
+
];
|
|
183
|
+
// 大小写选项(优先级:smartCase > caseSensitive > 默认忽略大小写)
|
|
184
|
+
if (options?.smartCase) {
|
|
185
|
+
args.push('--smart-case');
|
|
186
|
+
}
|
|
187
|
+
else if (options?.caseSensitive === true) {
|
|
188
|
+
// 明确指定大小写敏感时不添加 --ignore-case
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
args.push('--ignore-case');
|
|
192
|
+
}
|
|
193
|
+
// 多行匹配
|
|
194
|
+
if (options?.multiline) {
|
|
195
|
+
args.push('--multiline');
|
|
196
|
+
}
|
|
197
|
+
// 固定字符串模式
|
|
198
|
+
if (options?.fixedStrings) {
|
|
199
|
+
args.push('--fixed-strings');
|
|
200
|
+
}
|
|
201
|
+
// 单词边界
|
|
202
|
+
if (options?.wordRegexp) {
|
|
203
|
+
args.push('--word-regexp');
|
|
204
|
+
}
|
|
205
|
+
// 最大匹配数
|
|
206
|
+
if (options?.maxCount) {
|
|
207
|
+
args.push('--max-count', options.maxCount.toString());
|
|
208
|
+
}
|
|
209
|
+
// 最大深度
|
|
210
|
+
if (options?.maxDepth) {
|
|
211
|
+
args.push('--max-depth', options.maxDepth.toString());
|
|
212
|
+
}
|
|
213
|
+
// 上下文行
|
|
214
|
+
const contextBefore = options?.contextBefore ?? options?.context ?? 0;
|
|
215
|
+
const contextAfter = options?.contextAfter ?? options?.context ?? 0;
|
|
216
|
+
if (contextBefore > 0) {
|
|
217
|
+
args.push('-B', contextBefore.toString());
|
|
218
|
+
}
|
|
219
|
+
if (contextAfter > 0) {
|
|
220
|
+
args.push('-A', contextAfter.toString());
|
|
221
|
+
}
|
|
222
|
+
// 文件过滤
|
|
223
|
+
if (options?.files && options.files.length > 0) {
|
|
224
|
+
for (const file of options.files) {
|
|
225
|
+
args.push('--glob', file);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// 排除文件
|
|
229
|
+
if (options?.excludeFiles && options.excludeFiles.length > 0) {
|
|
230
|
+
for (const file of options.excludeFiles) {
|
|
231
|
+
args.push('--glob', `!${file}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 文件类型过滤
|
|
235
|
+
if (options?.fileTypes && options.fileTypes.length > 0) {
|
|
236
|
+
for (const type of options.fileTypes) {
|
|
237
|
+
args.push('--type', type);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 排除文件类型
|
|
241
|
+
if (options?.excludeTypes && options.excludeTypes.length > 0) {
|
|
242
|
+
for (const type of options.excludeTypes) {
|
|
243
|
+
args.push('--type-not', type);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 只显示文件名
|
|
247
|
+
if (options?.filesWithMatches) {
|
|
248
|
+
args.push('--files-with-matches');
|
|
249
|
+
}
|
|
250
|
+
// 只显示匹配数量
|
|
251
|
+
if (options?.count) {
|
|
252
|
+
args.push('--count');
|
|
253
|
+
}
|
|
254
|
+
// 忽略 .gitignore
|
|
255
|
+
if (options?.noIgnore) {
|
|
256
|
+
args.push('--no-ignore');
|
|
257
|
+
}
|
|
258
|
+
args.push(pattern);
|
|
259
|
+
args.push('.');
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const results = [];
|
|
262
|
+
const rgProcess = spawn(rgPath, args, { cwd });
|
|
263
|
+
let stdout = '';
|
|
264
|
+
let stderr = '';
|
|
265
|
+
rgProcess.stdout.on('data', (data) => {
|
|
266
|
+
stdout += data.toString();
|
|
267
|
+
});
|
|
268
|
+
rgProcess.stderr.on('data', (data) => {
|
|
269
|
+
stderr += data.toString();
|
|
270
|
+
});
|
|
271
|
+
rgProcess.on('close', (code) => {
|
|
272
|
+
if (code === 0 || code === 1) {
|
|
273
|
+
// ripgrep 返回 0 表示找到匹配,1 表示未找到匹配
|
|
274
|
+
try {
|
|
275
|
+
const lines = stdout.trim().split('\n').filter(line => line.trim());
|
|
276
|
+
let currentMatch = null;
|
|
277
|
+
const maxResults = options?.maxResults;
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
// 如果设置了 maxResults,检查是否已达到限制
|
|
280
|
+
if (maxResults && results.length >= maxResults) {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const data = JSON.parse(line);
|
|
285
|
+
if (data.type === 'match') {
|
|
286
|
+
// 保存之前的匹配
|
|
287
|
+
if (currentMatch) {
|
|
288
|
+
results.push(currentMatch);
|
|
289
|
+
// 检查是否达到限制
|
|
290
|
+
if (maxResults && results.length >= maxResults) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const filePath = data.data.path.text;
|
|
295
|
+
const relativePath = path.relative(cwd, filePath);
|
|
296
|
+
const userPath = relativePath.startsWith('..')
|
|
297
|
+
? filePath
|
|
298
|
+
: `/${relativePath.replace(/\\/g, '/')}`;
|
|
299
|
+
currentMatch = {
|
|
300
|
+
file: userPath,
|
|
301
|
+
line: data.data.line_number,
|
|
302
|
+
text: data.data.lines.text.trimEnd(),
|
|
303
|
+
column: data.data.submatches?.[0]?.start,
|
|
304
|
+
contextBefore: [],
|
|
305
|
+
contextAfter: [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
else if (data.type === 'context' && currentMatch) {
|
|
309
|
+
// 上下文行
|
|
310
|
+
const lineText = data.data.lines.text.trimEnd();
|
|
311
|
+
const lineNumber = data.data.line_number;
|
|
312
|
+
if (lineNumber < currentMatch.line) {
|
|
313
|
+
currentMatch.contextBefore.push(lineText);
|
|
314
|
+
}
|
|
315
|
+
else if (lineNumber > currentMatch.line) {
|
|
316
|
+
currentMatch.contextAfter.push(lineText);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
// 忽略无效的 JSON 行
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// 添加最后一个匹配(如果未达到限制)
|
|
325
|
+
if (currentMatch && (!maxResults || results.length < maxResults)) {
|
|
326
|
+
results.push(currentMatch);
|
|
327
|
+
}
|
|
328
|
+
resolve(results);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
reject(new Error(`Failed to parse ripgrep output: ${error.message}`));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
reject(new Error(`ripgrep failed with code ${code}: ${stderr}`));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
rgProcess.on('error', (error) => {
|
|
339
|
+
reject(new Error(`Failed to spawn ripgrep: ${error.message}`));
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
/**
|
|
344
|
+
* 通过文件名模式搜索文件
|
|
345
|
+
*/
|
|
346
|
+
async searchByPath(pattern, options) {
|
|
347
|
+
const userDir = path.join(getFileSystemDir(), user.id);
|
|
348
|
+
const cwd = options?.cwd
|
|
349
|
+
? path.resolve(userDir, options.cwd.startsWith('/') ? options.cwd.substring(1) : options.cwd)
|
|
350
|
+
: userDir;
|
|
351
|
+
// 确保工作目录在用户目录内
|
|
352
|
+
if (!cwd.startsWith(path.resolve(userDir))) {
|
|
353
|
+
throw new Error(`Working directory must be within user directory`);
|
|
354
|
+
}
|
|
355
|
+
const args = [
|
|
356
|
+
'--files',
|
|
357
|
+
];
|
|
358
|
+
// 大小写选项
|
|
359
|
+
if (options?.caseSensitive === true) {
|
|
360
|
+
// 明确指定大小写敏感时不添加 --ignore-case
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
args.push('--ignore-case');
|
|
364
|
+
}
|
|
365
|
+
// 最大深度
|
|
366
|
+
if (options?.maxDepth) {
|
|
367
|
+
args.push('--max-depth', options.maxDepth.toString());
|
|
368
|
+
}
|
|
369
|
+
// 文件类型过滤
|
|
370
|
+
if (options?.fileTypes && options.fileTypes.length > 0) {
|
|
371
|
+
for (const type of options.fileTypes) {
|
|
372
|
+
args.push('--type', type);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// 排除文件类型
|
|
376
|
+
if (options?.excludeTypes && options.excludeTypes.length > 0) {
|
|
377
|
+
for (const type of options.excludeTypes) {
|
|
378
|
+
args.push('--type-not', type);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 文件名模式过滤(使用 glob)
|
|
382
|
+
args.push('--glob', pattern);
|
|
383
|
+
// 排除文件
|
|
384
|
+
if (options?.excludeFiles && options.excludeFiles.length > 0) {
|
|
385
|
+
for (const file of options.excludeFiles) {
|
|
386
|
+
args.push('--glob', `!${file}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// 忽略 .gitignore
|
|
390
|
+
if (options?.noIgnore) {
|
|
391
|
+
args.push('--no-ignore');
|
|
392
|
+
}
|
|
393
|
+
args.push('.');
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
395
|
+
const results = [];
|
|
396
|
+
const rgProcess = spawn(rgPath, args, { cwd });
|
|
397
|
+
let stdout = '';
|
|
398
|
+
let stderr = '';
|
|
399
|
+
rgProcess.stdout.on('data', (data) => {
|
|
400
|
+
stdout += data.toString();
|
|
401
|
+
});
|
|
402
|
+
rgProcess.stderr.on('data', (data) => {
|
|
403
|
+
stderr += data.toString();
|
|
404
|
+
});
|
|
405
|
+
rgProcess.on('close', (code) => {
|
|
406
|
+
if (code === 0 || code === 1) {
|
|
407
|
+
// ripgrep 返回 0 表示找到文件,1 表示未找到文件
|
|
408
|
+
try {
|
|
409
|
+
const lines = stdout.trim().split('\n').filter(line => line.trim());
|
|
410
|
+
const maxResults = options?.maxResults;
|
|
411
|
+
for (const line of lines) {
|
|
412
|
+
// 如果设置了 maxResults,检查是否已达到限制
|
|
413
|
+
if (maxResults && results.length >= maxResults) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
if (line.trim()) {
|
|
417
|
+
const filePath = path.resolve(cwd, line.trim());
|
|
418
|
+
// 确保文件路径在用户目录内
|
|
419
|
+
if (filePath.startsWith(path.resolve(userDir))) {
|
|
420
|
+
const relativePath = path.relative(cwd, filePath);
|
|
421
|
+
// 转换为用户可见的路径格式(以 / 开头)
|
|
422
|
+
const userPath = relativePath.startsWith('..')
|
|
423
|
+
? filePath
|
|
424
|
+
: `/${relativePath.replace(/\\/g, '/')}`;
|
|
425
|
+
results.push(userPath);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
resolve(results);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
reject(new Error(`Failed to parse ripgrep output: ${error.message}`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
reject(new Error(`ripgrep failed with code ${code}: ${stderr}`));
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
rgProcess.on('error', (error) => {
|
|
440
|
+
reject(new Error(`Failed to spawn ripgrep: ${error.message}`));
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
// 调用其他 MCP 工具
|
|
446
|
+
mcp: {
|
|
447
|
+
async callTool(toolName, args) {
|
|
448
|
+
if (!aggregator) {
|
|
449
|
+
throw new Error('MCP aggregator not available. Tool calling is only available after initialization.');
|
|
450
|
+
}
|
|
451
|
+
// 确保聚合器已初始化
|
|
452
|
+
if (!aggregator.initialized) {
|
|
453
|
+
throw new Error('MCP aggregator not initialized yet. Tool calling is only available after initialization.');
|
|
454
|
+
}
|
|
455
|
+
// Context 中的调用允许使用禁用的组件,因为这是用户在自己的脚本中主动调用的
|
|
456
|
+
return await aggregator.callTool(toolName, args, true);
|
|
457
|
+
},
|
|
458
|
+
async listTools() {
|
|
459
|
+
if (!aggregator) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
if (!aggregator.initialized) {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
return await aggregator.getAllTools().then(tools => tools.map(t => t.name));
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
// 变量引用
|
|
469
|
+
vars: {
|
|
470
|
+
get(name) {
|
|
471
|
+
const variable = config.variables.find((v) => v.name === name);
|
|
472
|
+
return variable?.value || '';
|
|
473
|
+
},
|
|
474
|
+
has(name) {
|
|
475
|
+
return config.variables.some((v) => v.name === name);
|
|
476
|
+
},
|
|
477
|
+
getAll() {
|
|
478
|
+
const result = {};
|
|
479
|
+
for (const variable of config.variables) {
|
|
480
|
+
result[variable.name] = variable.value;
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
},
|
|
484
|
+
set(name, value, description) {
|
|
485
|
+
const existingIndex = config.variables.findIndex((v) => v.name === name);
|
|
486
|
+
if (existingIndex >= 0) {
|
|
487
|
+
// 更新现有变量
|
|
488
|
+
config.variables[existingIndex].value = value;
|
|
489
|
+
if (description !== undefined) {
|
|
490
|
+
config.variables[existingIndex].description = description;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// 创建新变量
|
|
495
|
+
config.variables.push({
|
|
496
|
+
id: generateId(),
|
|
497
|
+
name,
|
|
498
|
+
value,
|
|
499
|
+
...(description !== undefined && { description }),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
// 日志
|
|
505
|
+
log: {
|
|
506
|
+
info(message, ...args) {
|
|
507
|
+
console.log(`[INFO] ${message}`, ...args);
|
|
508
|
+
},
|
|
509
|
+
error(message, ...args) {
|
|
510
|
+
console.error(`[ERROR] ${message}`, ...args);
|
|
511
|
+
},
|
|
512
|
+
warn(message, ...args) {
|
|
513
|
+
console.warn(`[WARN] ${message}`, ...args);
|
|
514
|
+
},
|
|
515
|
+
debug(message, ...args) {
|
|
516
|
+
console.debug(`[DEBUG] ${message}`, ...args);
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
// 表数据访问(仅限当前用户)
|
|
520
|
+
tables: {
|
|
521
|
+
async list() {
|
|
522
|
+
try {
|
|
523
|
+
// 只查询当前用户的表
|
|
524
|
+
const result = await db
|
|
525
|
+
.select()
|
|
526
|
+
.from(tableDefinitions)
|
|
527
|
+
.where(eq(tableDefinitions.userId, config.userId));
|
|
528
|
+
return result.map((row) => {
|
|
529
|
+
// 解析列定义,确保包含 description 字段
|
|
530
|
+
const columns = JSON.parse(row.columns);
|
|
531
|
+
const tableDef = {
|
|
532
|
+
id: row.id,
|
|
533
|
+
name: row.name,
|
|
534
|
+
description: row.description || undefined,
|
|
535
|
+
columns: columns, // 确保返回包含 description 的列定义
|
|
536
|
+
createdAt: row.createdAt,
|
|
537
|
+
updatedAt: row.updatedAt,
|
|
538
|
+
};
|
|
539
|
+
return tableDef;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.error('[Tables.list] Error:', error);
|
|
544
|
+
throw new Error(`Failed to list tables: ${error.message}`);
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
async getDefinition(tableName) {
|
|
548
|
+
try {
|
|
549
|
+
// 查找当前用户的表定义
|
|
550
|
+
const tableDefs = await db
|
|
551
|
+
.select()
|
|
552
|
+
.from(tableDefinitions)
|
|
553
|
+
.where(and(eq(tableDefinitions.name, tableName), eq(tableDefinitions.userId, config.userId)))
|
|
554
|
+
.limit(1);
|
|
555
|
+
if (tableDefs.length === 0) {
|
|
556
|
+
throw new Error(`Table "${tableName}" not found`);
|
|
557
|
+
}
|
|
558
|
+
const tableDef = tableDefs[0];
|
|
559
|
+
// 解析列定义,确保包含 description 字段
|
|
560
|
+
const columns = JSON.parse(tableDef.columns);
|
|
561
|
+
const result = {
|
|
562
|
+
id: tableDef.id,
|
|
563
|
+
name: tableDef.name,
|
|
564
|
+
description: tableDef.description || undefined,
|
|
565
|
+
columns: columns, // 确保返回包含 description 的列定义
|
|
566
|
+
createdAt: tableDef.createdAt,
|
|
567
|
+
updatedAt: tableDef.updatedAt,
|
|
568
|
+
};
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
console.error(`[Tables.getDefinition] Error for table "${tableName}":`, error);
|
|
573
|
+
throw new Error(`Failed to get table definition "${tableName}": ${error.message}`);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
async get(tableName) {
|
|
577
|
+
try {
|
|
578
|
+
// 查找当前用户的表定义
|
|
579
|
+
const tableDefs = await db
|
|
580
|
+
.select()
|
|
581
|
+
.from(tableDefinitions)
|
|
582
|
+
.where(and(eq(tableDefinitions.name, tableName), eq(tableDefinitions.userId, config.userId)))
|
|
583
|
+
.limit(1);
|
|
584
|
+
if (tableDefs.length === 0) {
|
|
585
|
+
throw new Error(`Table "${tableName}" not found`);
|
|
586
|
+
}
|
|
587
|
+
const tableDef = tableDefs[0];
|
|
588
|
+
// 获取表的所有数据
|
|
589
|
+
const rows = await db
|
|
590
|
+
.select()
|
|
591
|
+
.from(tableData)
|
|
592
|
+
.where(eq(tableData.tableId, tableDef.id));
|
|
593
|
+
// 返回行数据(不包含元数据)
|
|
594
|
+
return rows.map((row) => JSON.parse(row.rowData));
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
console.error(`[Tables.get] Error for table "${tableName}":`, error);
|
|
598
|
+
throw new Error(`Failed to get table "${tableName}": ${error.message}`);
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
async query(tableName, filter) {
|
|
602
|
+
try {
|
|
603
|
+
const allRows = await this.get(tableName);
|
|
604
|
+
if (!filter) {
|
|
605
|
+
return allRows;
|
|
606
|
+
}
|
|
607
|
+
return allRows.filter(filter);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
console.error(`[Tables.query] Error for table "${tableName}":`, error);
|
|
611
|
+
throw new Error(`Failed to query table "${tableName}": ${error.message}`);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
// SQL 执行(仅限当前用户的表)
|
|
616
|
+
sql: {
|
|
617
|
+
/**
|
|
618
|
+
* 执行 SQL 语句
|
|
619
|
+
* 只能访问和操作当前用户的表
|
|
620
|
+
* 支持 SELECT, INSERT, UPDATE, DELETE
|
|
621
|
+
*/
|
|
622
|
+
async execute(sql) {
|
|
623
|
+
try {
|
|
624
|
+
// 获取当前用户的所有表定义
|
|
625
|
+
const userTables = await db
|
|
626
|
+
.select({ id: tableDefinitions.id, name: tableDefinitions.name, columns: tableDefinitions.columns })
|
|
627
|
+
.from(tableDefinitions)
|
|
628
|
+
.where(eq(tableDefinitions.userId, config.userId));
|
|
629
|
+
const userTableMap = new Map(userTables.map(t => [t.name.toLowerCase(), t]));
|
|
630
|
+
const sqlLower = sql.toLowerCase().trim();
|
|
631
|
+
// 检查是否包含危险操作
|
|
632
|
+
const dangerousKeywords = ['drop', 'truncate', 'alter', 'create', 'replace', 'attach', 'detach'];
|
|
633
|
+
for (const keyword of dangerousKeywords) {
|
|
634
|
+
if (sqlLower.includes(keyword)) {
|
|
635
|
+
throw new Error(`SQL keyword "${keyword}" is not allowed`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// 提取 SQL 中涉及的表名
|
|
639
|
+
const tableNamePattern = /\b(from|into|update|join)\s+(\w+)/gi;
|
|
640
|
+
const matches = Array.from(sql.matchAll(tableNamePattern));
|
|
641
|
+
const referencedTables = matches.map(m => m[2].toLowerCase());
|
|
642
|
+
// 验证所有引用的表都属于当前用户
|
|
643
|
+
for (const tableName of referencedTables) {
|
|
644
|
+
if (!userTableMap.has(tableName)) {
|
|
645
|
+
throw new Error(`Table "${tableName}" not found or access denied`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// 由于用户表的数据存储在 table_data 表的 JSON 字段中,
|
|
649
|
+
// 我们需要创建临时视图来映射用户表到实际的 SQLite 表结构
|
|
650
|
+
// 然后执行 SQL,最后清理视图
|
|
651
|
+
const viewNames = [];
|
|
652
|
+
try {
|
|
653
|
+
// 为每个用户表创建临时视图
|
|
654
|
+
for (const table of userTables) {
|
|
655
|
+
const viewName = `_user_table_${table.id.replace(/-/g, '_')}`;
|
|
656
|
+
viewNames.push(viewName);
|
|
657
|
+
// 获取表的所有数据
|
|
658
|
+
const rows = await db
|
|
659
|
+
.select()
|
|
660
|
+
.from(tableData)
|
|
661
|
+
.where(eq(tableData.tableId, table.id));
|
|
662
|
+
const columns = JSON.parse(table.columns);
|
|
663
|
+
// 创建临时表来存储数据(因为视图不支持直接查询 JSON)
|
|
664
|
+
// 使用 WITH 子句创建临时表
|
|
665
|
+
const columnDefs = columns.map(col => {
|
|
666
|
+
const sqliteType = col.type === 'number' ? 'REAL' :
|
|
667
|
+
col.type === 'boolean' ? 'INTEGER' :
|
|
668
|
+
col.type === 'date' ? 'TEXT' :
|
|
669
|
+
col.type === 'json' ? 'TEXT' : 'TEXT';
|
|
670
|
+
return `"${col.name}" ${sqliteType}`;
|
|
671
|
+
}).join(', ');
|
|
672
|
+
// 创建临时表
|
|
673
|
+
const createTempTableSql = `
|
|
674
|
+
CREATE TEMP TABLE IF NOT EXISTS ${viewName} (
|
|
675
|
+
id TEXT PRIMARY KEY,
|
|
676
|
+
${columnDefs},
|
|
677
|
+
created_at INTEGER,
|
|
678
|
+
updated_at INTEGER
|
|
679
|
+
);
|
|
680
|
+
`;
|
|
681
|
+
sqlite.exec(createTempTableSql);
|
|
682
|
+
// 插入数据
|
|
683
|
+
for (const row of rows) {
|
|
684
|
+
const rowData = JSON.parse(row.rowData);
|
|
685
|
+
const values = columns.map(col => {
|
|
686
|
+
const value = rowData[col.name];
|
|
687
|
+
if (value === null || value === undefined) {
|
|
688
|
+
return 'NULL';
|
|
689
|
+
}
|
|
690
|
+
if (col.type === 'string' || col.type === 'date' || col.type === 'json') {
|
|
691
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
692
|
+
}
|
|
693
|
+
return String(value);
|
|
694
|
+
}).join(', ');
|
|
695
|
+
const insertSql = `
|
|
696
|
+
INSERT OR REPLACE INTO ${viewName} (id, ${columns.map(c => `"${c.name}"`).join(', ')}, created_at, updated_at)
|
|
697
|
+
VALUES ('${row.id}', ${values}, ${row.createdAt.getTime()}, ${row.updatedAt.getTime()});
|
|
698
|
+
`;
|
|
699
|
+
sqlite.exec(insertSql);
|
|
700
|
+
}
|
|
701
|
+
// 在 SQL 中替换表名
|
|
702
|
+
const tableNameRegex = new RegExp(`\\b${table.name}\\b`, 'gi');
|
|
703
|
+
sql = sql.replace(tableNameRegex, viewName);
|
|
704
|
+
}
|
|
705
|
+
// 执行 SQL
|
|
706
|
+
if (sqlLower.startsWith('select')) {
|
|
707
|
+
// SELECT 查询
|
|
708
|
+
const stmt = sqlite.prepare(sql);
|
|
709
|
+
const results = stmt.all();
|
|
710
|
+
return results;
|
|
711
|
+
}
|
|
712
|
+
else if (sqlLower.startsWith('insert')) {
|
|
713
|
+
// INSERT 操作
|
|
714
|
+
const stmt = sqlite.prepare(sql);
|
|
715
|
+
const result = stmt.run();
|
|
716
|
+
// 同步回 table_data 表
|
|
717
|
+
// 找到对应的表
|
|
718
|
+
const insertMatch = sqlLower.match(/into\s+(\w+)/i);
|
|
719
|
+
if (insertMatch) {
|
|
720
|
+
const tableName = insertMatch[1];
|
|
721
|
+
const userTable = userTables.find(t => {
|
|
722
|
+
const viewName = `_user_table_${t.id.replace(/-/g, '_')}`;
|
|
723
|
+
return viewName.toLowerCase() === tableName.toLowerCase();
|
|
724
|
+
});
|
|
725
|
+
if (userTable) {
|
|
726
|
+
// 获取新插入的行
|
|
727
|
+
const newRowStmt = sqlite.prepare(`SELECT * FROM ${tableName} WHERE rowid = ?`);
|
|
728
|
+
const newRow = newRowStmt.get(result.lastInsertRowid);
|
|
729
|
+
if (newRow) {
|
|
730
|
+
// 构建 rowData(排除 id, created_at, updated_at)
|
|
731
|
+
const rowData = {};
|
|
732
|
+
const columns = JSON.parse(userTable.columns);
|
|
733
|
+
for (const col of columns) {
|
|
734
|
+
if (newRow[col.name] !== undefined) {
|
|
735
|
+
rowData[col.name] = newRow[col.name];
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// 插入到 table_data
|
|
739
|
+
const rowId = newRow.id || generateId();
|
|
740
|
+
const now = new Date();
|
|
741
|
+
await db.insert(tableData).values({
|
|
742
|
+
id: rowId,
|
|
743
|
+
tableId: userTable.id,
|
|
744
|
+
rowData: JSON.stringify(rowData),
|
|
745
|
+
createdAt: now,
|
|
746
|
+
updatedAt: now,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
changes: result.changes,
|
|
753
|
+
lastInsertRowid: result.lastInsertRowid,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
else if (sqlLower.startsWith('update') || sqlLower.startsWith('delete')) {
|
|
757
|
+
// UPDATE 或 DELETE 操作
|
|
758
|
+
// 由于需要同步回 table_data,这里暂时不支持
|
|
759
|
+
// 未来可以实现更完整的同步逻辑
|
|
760
|
+
throw new Error('UPDATE and DELETE operations are not yet fully supported. Please use the tables API methods instead.');
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
throw new Error(`Unsupported SQL operation: ${sqlLower.split(' ')[0]}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
// 清理临时表
|
|
768
|
+
for (const viewName of viewNames) {
|
|
769
|
+
try {
|
|
770
|
+
sqlite.exec(`DROP TABLE IF EXISTS ${viewName};`);
|
|
771
|
+
}
|
|
772
|
+
catch (e) {
|
|
773
|
+
// 忽略清理错误
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
console.error('[SQL.execute] Error:', error);
|
|
780
|
+
throw new Error(`SQL execution failed: ${error.message}`);
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//# sourceMappingURL=context.js.map
|