@fineorg/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/README.md +354 -0
- package/dist/index-http.d.ts +1 -0
- package/dist/index-http.js +2425 -0
- package/dist/index-sse.d.ts +1 -0
- package/dist/index-sse.js +2242 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1981 -0
- package/package.json +72 -0
|
@@ -0,0 +1,2242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
4
|
+
import { readFileSync, createWriteStream } from 'fs';
|
|
5
|
+
import path, { join } from 'path';
|
|
6
|
+
import { config as config$1 } from 'dotenv';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { timingSafeEqual } from 'crypto';
|
|
9
|
+
import { readdir, stat, unlink, mkdir } from 'fs/promises';
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import axios2 from 'axios';
|
|
12
|
+
import axiosRetry from 'axios-retry';
|
|
13
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
14
|
+
|
|
15
|
+
config$1({ path: path.resolve(process.cwd(), ".env") });
|
|
16
|
+
var packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
17
|
+
var packageVersion = "0.0.0";
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
20
|
+
packageVersion = pkg.version || packageVersion;
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
var rawConfig = {
|
|
24
|
+
version: packageVersion,
|
|
25
|
+
mcp: {
|
|
26
|
+
authEnabled: process.env.MCP_AUTH_ENABLED !== "false",
|
|
27
|
+
authToken: process.env.MCP_AUTH_TOKEN,
|
|
28
|
+
allowedClients: process.env.MCP_ALLOWED_CLIENTS,
|
|
29
|
+
http: {
|
|
30
|
+
port: Number.parseInt(process.env.MCP_HTTP_PORT || "3040", 10),
|
|
31
|
+
host: process.env.MCP_HTTP_HOST || "127.0.0.1"
|
|
32
|
+
},
|
|
33
|
+
sse: {
|
|
34
|
+
port: Number.parseInt(process.env.MCP_SSE_PORT || "3041", 10),
|
|
35
|
+
host: process.env.MCP_SSE_HOST || "127.0.0.1"
|
|
36
|
+
},
|
|
37
|
+
maxSessions: Number.parseInt(process.env.MCP_MAX_SESSIONS || "100", 10),
|
|
38
|
+
sessionTimeoutMs: Number.parseInt(process.env.MCP_SESSION_TIMEOUT || String(30 * 60 * 1e3), 10),
|
|
39
|
+
maxBodySize: Number.parseInt(process.env.MCP_MAX_BODY_SIZE || String(1 * 1024 * 1024), 10),
|
|
40
|
+
maxDiffSize: Number.parseInt(process.env.MCP_MAX_DIFF_SIZE || String(2 * 1024 * 1024), 10),
|
|
41
|
+
rateLimitWindowMs: Number.parseInt(process.env.MCP_RATE_LIMIT_WINDOW || "60000", 10),
|
|
42
|
+
rateLimitMax: Number.parseInt(process.env.MCP_RATE_LIMIT_MAX || "60", 10),
|
|
43
|
+
trustProxy: process.env.MCP_TRUST_PROXY === "true"
|
|
44
|
+
},
|
|
45
|
+
jira: {
|
|
46
|
+
host: process.env.JIRA_HOST,
|
|
47
|
+
username: process.env.JIRA_USERNAME,
|
|
48
|
+
token: process.env.JIRA_TOKEN
|
|
49
|
+
},
|
|
50
|
+
bitbucket: {
|
|
51
|
+
host: process.env.BITBUCKET_HOST,
|
|
52
|
+
username: process.env.BITBUCKET_USERNAME,
|
|
53
|
+
password: process.env.BITBUCKET_PASSWORD,
|
|
54
|
+
token: process.env.BITBUCKET_TOKEN
|
|
55
|
+
},
|
|
56
|
+
feishuProject: {
|
|
57
|
+
projectKey: process.env.FEISHU_PROJECT_KEY,
|
|
58
|
+
pluginId: process.env.FEISHU_PROJECT_PLUGIN_ID,
|
|
59
|
+
pluginSecret: process.env.FEISHU_PROJECT_PLUGIN_SECRET,
|
|
60
|
+
useVirtualToken: process.env.FEISHU_PROJECT_USE_VIRTUAL_TOKEN === "true",
|
|
61
|
+
userKey: process.env.FEISHU_PROJECT_USER_KEY
|
|
62
|
+
},
|
|
63
|
+
feishuOpen: {
|
|
64
|
+
appId: process.env.FEISHU_OPEN_APP_ID,
|
|
65
|
+
appSecret: process.env.FEISHU_OPEN_APP_SECRET
|
|
66
|
+
},
|
|
67
|
+
confluence: {
|
|
68
|
+
host: process.env.CONFLUENCE_HOST,
|
|
69
|
+
token: process.env.CONFLUENCE_TOKEN
|
|
70
|
+
},
|
|
71
|
+
server: {
|
|
72
|
+
logDir: process.env.LOG_DIR || "./logs",
|
|
73
|
+
logLevel: process.env.LOG_LEVEL || "INFO"
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var configSchema = z.object({
|
|
77
|
+
version: z.string(),
|
|
78
|
+
mcp: z.object({
|
|
79
|
+
authEnabled: z.boolean(),
|
|
80
|
+
authToken: z.string().optional(),
|
|
81
|
+
allowedClients: z.string().optional(),
|
|
82
|
+
http: z.object({
|
|
83
|
+
port: z.number().int().min(1).max(65535),
|
|
84
|
+
host: z.string()
|
|
85
|
+
}),
|
|
86
|
+
sse: z.object({
|
|
87
|
+
port: z.number().int().min(1).max(65535),
|
|
88
|
+
host: z.string()
|
|
89
|
+
}),
|
|
90
|
+
maxSessions: z.number().int().min(1),
|
|
91
|
+
sessionTimeoutMs: z.number().int().min(1),
|
|
92
|
+
maxBodySize: z.number().int().min(1),
|
|
93
|
+
maxDiffSize: z.number().int().min(1),
|
|
94
|
+
rateLimitWindowMs: z.number().int().min(1e3),
|
|
95
|
+
rateLimitMax: z.number().int().min(1),
|
|
96
|
+
trustProxy: z.boolean()
|
|
97
|
+
}),
|
|
98
|
+
jira: z.object({
|
|
99
|
+
host: z.string().optional(),
|
|
100
|
+
username: z.string().optional(),
|
|
101
|
+
token: z.string().optional()
|
|
102
|
+
}),
|
|
103
|
+
bitbucket: z.object({
|
|
104
|
+
host: z.string().optional(),
|
|
105
|
+
username: z.string().optional(),
|
|
106
|
+
password: z.string().optional(),
|
|
107
|
+
token: z.string().optional()
|
|
108
|
+
}),
|
|
109
|
+
feishuProject: z.object({
|
|
110
|
+
projectKey: z.string().optional(),
|
|
111
|
+
pluginId: z.string().optional(),
|
|
112
|
+
pluginSecret: z.string().optional(),
|
|
113
|
+
useVirtualToken: z.boolean(),
|
|
114
|
+
userKey: z.string().optional()
|
|
115
|
+
}),
|
|
116
|
+
feishuOpen: z.object({
|
|
117
|
+
appId: z.string().optional(),
|
|
118
|
+
appSecret: z.string().optional()
|
|
119
|
+
}),
|
|
120
|
+
confluence: z.object({
|
|
121
|
+
host: z.string().optional(),
|
|
122
|
+
token: z.string().optional()
|
|
123
|
+
}),
|
|
124
|
+
server: z.object({
|
|
125
|
+
logDir: z.string(),
|
|
126
|
+
logLevel: z.string()
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
var config = configSchema.parse(rawConfig);
|
|
130
|
+
var hasJiraConfig = () => !!(config.jira.host && config.jira.username && config.jira.token);
|
|
131
|
+
var hasBitbucketAuth = () => !!(config.bitbucket.token || config.bitbucket.username && config.bitbucket.password);
|
|
132
|
+
var hasConfluenceConfig = () => !!(config.confluence.host && config.confluence.token);
|
|
133
|
+
var hasFeishuProjectAuth = () => !!(config.feishuProject.pluginId && config.feishuProject.pluginSecret);
|
|
134
|
+
var hasFeishuOpenAuth = () => !!(config.feishuOpen.appId && config.feishuOpen.appSecret);
|
|
135
|
+
function validateConfig() {
|
|
136
|
+
const errors = [];
|
|
137
|
+
const jiraFields = [config.jira.host, config.jira.username, config.jira.token];
|
|
138
|
+
const jiraCount = jiraFields.filter(Boolean).length;
|
|
139
|
+
if (jiraCount > 0 && jiraCount < jiraFields.length) {
|
|
140
|
+
errors.push("Jira \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E JIRA_HOST, JIRA_USERNAME, JIRA_TOKEN");
|
|
141
|
+
}
|
|
142
|
+
if (config.bitbucket.host && !hasBitbucketAuth()) {
|
|
143
|
+
errors.push(
|
|
144
|
+
"Bitbucket \u9700\u8981\u914D\u7F6E\u8BA4\u8BC1\u65B9\u5F0F\uFF1ABITBUCKET_TOKEN \u6216 (BITBUCKET_USERNAME + BITBUCKET_PASSWORD)"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (config.confluence.host && !config.confluence.token) {
|
|
148
|
+
errors.push(
|
|
149
|
+
"Confluence \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E CONFLUENCE_HOST \u548C CONFLUENCE_TOKEN"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (config.feishuProject.projectKey) {
|
|
153
|
+
if (!hasFeishuProjectAuth()) {
|
|
154
|
+
errors.push(
|
|
155
|
+
"\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u914D\u7F6E FEISHU_PROJECT_PLUGIN_ID + FEISHU_PROJECT_PLUGIN_SECRET"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (config.feishuProject.useVirtualToken && !config.feishuProject.userKey) {
|
|
159
|
+
errors.push(
|
|
160
|
+
"\u98DE\u4E66\u9879\u76EE\u4F7F\u7528\u865A\u62DF Token (FEISHU_PROJECT_USE_VIRTUAL_TOKEN=true) \u4F46\u672A\u914D\u7F6E FEISHU_PROJECT_USER_KEY"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const feishuOpenFields = [config.feishuOpen.appId, config.feishuOpen.appSecret];
|
|
165
|
+
const feishuOpenCount = feishuOpenFields.filter(Boolean).length;
|
|
166
|
+
if (feishuOpenCount > 0 && feishuOpenCount < feishuOpenFields.length) {
|
|
167
|
+
errors.push(
|
|
168
|
+
"\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E FEISHU_OPEN_APP_ID + FEISHU_OPEN_APP_SECRET"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
throw new Error(`\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25:
|
|
173
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
var platformAvailability = {
|
|
177
|
+
bitbucket: Boolean(config.bitbucket.host && hasBitbucketAuth()),
|
|
178
|
+
jira: hasJiraConfig(),
|
|
179
|
+
confluence: hasConfluenceConfig(),
|
|
180
|
+
feishu: Boolean(config.feishuProject.projectKey && hasFeishuProjectAuth()),
|
|
181
|
+
feishuOpen: hasFeishuOpenAuth()
|
|
182
|
+
};
|
|
183
|
+
var LOG_LEVEL_PRIORITY = {
|
|
184
|
+
DEBUG: 0,
|
|
185
|
+
INFO: 1,
|
|
186
|
+
WARN: 2,
|
|
187
|
+
ERROR: 3
|
|
188
|
+
};
|
|
189
|
+
var currentLogLevel = (() => {
|
|
190
|
+
const envLevel = config.server.logLevel?.toUpperCase();
|
|
191
|
+
if (envLevel && envLevel in LOG_LEVEL_PRIORITY) {
|
|
192
|
+
return envLevel;
|
|
193
|
+
}
|
|
194
|
+
return "INFO";
|
|
195
|
+
})();
|
|
196
|
+
var shouldLog = (level) => {
|
|
197
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLogLevel];
|
|
198
|
+
};
|
|
199
|
+
var LOG_DIR = config.server.logDir;
|
|
200
|
+
var LOG_RETENTION_DAYS = 90;
|
|
201
|
+
var timestampFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
202
|
+
timeZone: "Asia/Shanghai",
|
|
203
|
+
year: "numeric",
|
|
204
|
+
month: "2-digit",
|
|
205
|
+
day: "2-digit",
|
|
206
|
+
hour: "2-digit",
|
|
207
|
+
minute: "2-digit",
|
|
208
|
+
second: "2-digit",
|
|
209
|
+
hour12: false
|
|
210
|
+
});
|
|
211
|
+
var dateFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
212
|
+
timeZone: "Asia/Shanghai",
|
|
213
|
+
year: "numeric",
|
|
214
|
+
month: "2-digit",
|
|
215
|
+
day: "2-digit"
|
|
216
|
+
});
|
|
217
|
+
var formatTimestamp = () => {
|
|
218
|
+
const now = /* @__PURE__ */ new Date();
|
|
219
|
+
const timeString = timestampFormatter.format(now).replace(/\//g, "-");
|
|
220
|
+
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
221
|
+
return `${timeString}.${ms}`;
|
|
222
|
+
};
|
|
223
|
+
var getCurrentDateString = () => {
|
|
224
|
+
const now = /* @__PURE__ */ new Date();
|
|
225
|
+
const parts = dateFormatter.formatToParts(now);
|
|
226
|
+
const year = parts.find((p) => p.type === "year")?.value;
|
|
227
|
+
const month = parts.find((p) => p.type === "month")?.value;
|
|
228
|
+
const day = parts.find((p) => p.type === "day")?.value;
|
|
229
|
+
return `${year}-${month}-${day}`;
|
|
230
|
+
};
|
|
231
|
+
var EXACT_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
232
|
+
"password",
|
|
233
|
+
"token",
|
|
234
|
+
"secret",
|
|
235
|
+
"authorization",
|
|
236
|
+
"cookie",
|
|
237
|
+
"credential",
|
|
238
|
+
"credentials",
|
|
239
|
+
"bearer"
|
|
240
|
+
]);
|
|
241
|
+
var SENSITIVE_SUFFIXES = [
|
|
242
|
+
"password",
|
|
243
|
+
"token",
|
|
244
|
+
"secret",
|
|
245
|
+
"apikey",
|
|
246
|
+
"api_key",
|
|
247
|
+
"privatekey",
|
|
248
|
+
"private_key",
|
|
249
|
+
"databaseurl",
|
|
250
|
+
"database_url",
|
|
251
|
+
"connectionstring",
|
|
252
|
+
"connection_string"
|
|
253
|
+
];
|
|
254
|
+
var MAX_SANITIZE_DEPTH = 5;
|
|
255
|
+
var sanitizeSensitiveData = (data, depth = 0) => {
|
|
256
|
+
if (data === null || data === void 0) {
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
260
|
+
return "[MAX_DEPTH_REACHED]";
|
|
261
|
+
}
|
|
262
|
+
if (typeof data === "string") {
|
|
263
|
+
if (data.startsWith("Bearer ")) {
|
|
264
|
+
return "Bearer [REDACTED]";
|
|
265
|
+
}
|
|
266
|
+
if (data.startsWith("Basic ")) {
|
|
267
|
+
return "Basic [REDACTED]";
|
|
268
|
+
}
|
|
269
|
+
if (data.includes("://") && data.includes("@")) {
|
|
270
|
+
try {
|
|
271
|
+
const url = new URL(data);
|
|
272
|
+
if (url.password) {
|
|
273
|
+
url.password = "***REDACTED***";
|
|
274
|
+
}
|
|
275
|
+
return url.toString();
|
|
276
|
+
} catch {
|
|
277
|
+
return data.replace(/:([^:@]+)@/, ":***REDACTED***@");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return data;
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(data)) {
|
|
283
|
+
return data.map((item) => sanitizeSensitiveData(item, depth + 1));
|
|
284
|
+
}
|
|
285
|
+
if (typeof data === "object") {
|
|
286
|
+
if (data instanceof Error) {
|
|
287
|
+
return {
|
|
288
|
+
message: sanitizeSensitiveData(data.message, depth + 1),
|
|
289
|
+
stack: data.stack,
|
|
290
|
+
name: data.name
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const sanitized = {};
|
|
294
|
+
for (const [key, value] of Object.entries(data)) {
|
|
295
|
+
const keyLower = key.toLowerCase();
|
|
296
|
+
const isSensitive = EXACT_SENSITIVE_KEYS.has(keyLower) || SENSITIVE_SUFFIXES.some((suffix) => keyLower.endsWith(suffix));
|
|
297
|
+
if (isSensitive) {
|
|
298
|
+
sanitized[key] = "***REDACTED***";
|
|
299
|
+
} else {
|
|
300
|
+
sanitized[key] = sanitizeSensitiveData(value, depth + 1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return sanitized;
|
|
304
|
+
}
|
|
305
|
+
return data;
|
|
306
|
+
};
|
|
307
|
+
var ensureLogDir = async () => {
|
|
308
|
+
try {
|
|
309
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var logDirReady = false;
|
|
314
|
+
var makeStreamFactory = (prefix) => {
|
|
315
|
+
let stream = null;
|
|
316
|
+
let date = "";
|
|
317
|
+
return (dateStr) => {
|
|
318
|
+
try {
|
|
319
|
+
if (stream && date === dateStr) return stream;
|
|
320
|
+
stream?.end();
|
|
321
|
+
stream = createWriteStream(join(LOG_DIR, `${prefix}-${dateStr}.log`), {
|
|
322
|
+
flags: "a",
|
|
323
|
+
encoding: "utf-8"
|
|
324
|
+
});
|
|
325
|
+
stream.on("error", (err) => {
|
|
326
|
+
console.error(`[logger] write stream error: ${err.message}`);
|
|
327
|
+
});
|
|
328
|
+
date = dateStr;
|
|
329
|
+
return stream;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
var getOrCreateLogStream = makeStreamFactory("mcp");
|
|
336
|
+
var getOrCreateErrorStream = makeStreamFactory("error");
|
|
337
|
+
var writeToFile = async (logMessage) => {
|
|
338
|
+
try {
|
|
339
|
+
if (!logDirReady) {
|
|
340
|
+
await ensureLogDir();
|
|
341
|
+
logDirReady = true;
|
|
342
|
+
}
|
|
343
|
+
const dateStr = getCurrentDateString();
|
|
344
|
+
const stream = getOrCreateLogStream(dateStr);
|
|
345
|
+
stream?.write(`${logMessage}
|
|
346
|
+
`);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var writeToErrorFile = async (logMessage) => {
|
|
351
|
+
try {
|
|
352
|
+
if (!logDirReady) {
|
|
353
|
+
await ensureLogDir();
|
|
354
|
+
logDirReady = true;
|
|
355
|
+
}
|
|
356
|
+
const dateStr = getCurrentDateString();
|
|
357
|
+
const stream = getOrCreateErrorStream(dateStr);
|
|
358
|
+
stream?.write(`${logMessage}
|
|
359
|
+
`);
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var cleanupOldLogs = async () => {
|
|
364
|
+
try {
|
|
365
|
+
await ensureLogDir();
|
|
366
|
+
const files = await readdir(LOG_DIR);
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
369
|
+
for (const file of files) {
|
|
370
|
+
if (!file.startsWith("mcp-") && !file.startsWith("error-") || !file.endsWith(".log")) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const filePath = join(LOG_DIR, file);
|
|
374
|
+
const fileStat = await stat(filePath);
|
|
375
|
+
const fileAge = now - fileStat.mtime.getTime();
|
|
376
|
+
if (fileAge > retentionMs) {
|
|
377
|
+
await unlink(filePath);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var log = (level, message, data) => {
|
|
384
|
+
if (!shouldLog(level)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const timestamp = formatTimestamp();
|
|
388
|
+
let logMessage = `[${timestamp}] [${level}] ${message}`;
|
|
389
|
+
if (data !== void 0) {
|
|
390
|
+
try {
|
|
391
|
+
const sanitizedData = sanitizeSensitiveData(data);
|
|
392
|
+
let dataStr = "";
|
|
393
|
+
if (sanitizedData instanceof Error) {
|
|
394
|
+
const errorObj = {
|
|
395
|
+
message: sanitizedData.message,
|
|
396
|
+
stack: sanitizedData.stack?.replace(/\n/g, "\\n"),
|
|
397
|
+
name: sanitizedData.name
|
|
398
|
+
};
|
|
399
|
+
if ("code" in sanitizedData) {
|
|
400
|
+
errorObj.code = sanitizedData.code;
|
|
401
|
+
}
|
|
402
|
+
dataStr = JSON.stringify(errorObj);
|
|
403
|
+
} else if (typeof sanitizedData === "object" && sanitizedData !== null) {
|
|
404
|
+
const dataObj = {};
|
|
405
|
+
for (const [key, value] of Object.entries(sanitizedData)) {
|
|
406
|
+
try {
|
|
407
|
+
if (typeof value === "function" || value === void 0) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (value instanceof Error) {
|
|
411
|
+
dataObj[key] = {
|
|
412
|
+
message: value.message,
|
|
413
|
+
stack: value.stack?.replace(/\n/g, "\\n"),
|
|
414
|
+
name: value.name
|
|
415
|
+
};
|
|
416
|
+
} else if (typeof value === "string") {
|
|
417
|
+
dataObj[key] = value.replace(/\n/g, "\\n");
|
|
418
|
+
} else {
|
|
419
|
+
dataObj[key] = value;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
dataObj[key] = `[Unserializable: ${typeof value}]`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
dataStr = JSON.stringify(dataObj);
|
|
426
|
+
} else {
|
|
427
|
+
dataStr = String(sanitizedData);
|
|
428
|
+
}
|
|
429
|
+
logMessage += ` ${dataStr}`;
|
|
430
|
+
} catch {
|
|
431
|
+
logMessage += ` [Data type: ${typeof data}, Message: ${String(data).substring(0, 100)}]`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
console.error(logMessage);
|
|
435
|
+
writeToFile(logMessage).catch(() => {
|
|
436
|
+
});
|
|
437
|
+
if (level === "ERROR") {
|
|
438
|
+
writeToErrorFile(logMessage).catch(() => {
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
function initLogger() {
|
|
443
|
+
cleanupOldLogs().catch(() => {
|
|
444
|
+
});
|
|
445
|
+
scheduleCleanup();
|
|
446
|
+
}
|
|
447
|
+
var CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
448
|
+
var cleanupTimer;
|
|
449
|
+
var scheduleCleanup = () => {
|
|
450
|
+
cleanupTimer = setInterval(() => {
|
|
451
|
+
cleanupOldLogs().catch(() => {
|
|
452
|
+
});
|
|
453
|
+
}, CLEANUP_INTERVAL_MS);
|
|
454
|
+
cleanupTimer.unref();
|
|
455
|
+
};
|
|
456
|
+
var logRequestError = (message, context) => {
|
|
457
|
+
const enhancedData = {
|
|
458
|
+
type: "HTTP_REQUEST_ERROR",
|
|
459
|
+
...context
|
|
460
|
+
};
|
|
461
|
+
log("ERROR", message, enhancedData);
|
|
462
|
+
};
|
|
463
|
+
var logger = {
|
|
464
|
+
debug: (message, data) => log("DEBUG", message, data),
|
|
465
|
+
info: (message, data) => log("INFO", message, data),
|
|
466
|
+
warn: (message, data) => log("WARN", message, data),
|
|
467
|
+
error: (message, data) => log("ERROR", message, data),
|
|
468
|
+
requestError: logRequestError
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/mcp/mcpAuth.ts
|
|
472
|
+
var cachedAllowedClients;
|
|
473
|
+
var cachedAllowedClientsSource;
|
|
474
|
+
function getParsedAllowedClients() {
|
|
475
|
+
const source = config.mcp.allowedClients;
|
|
476
|
+
if (cachedAllowedClientsSource === source && cachedAllowedClients !== void 0) {
|
|
477
|
+
return cachedAllowedClients;
|
|
478
|
+
}
|
|
479
|
+
cachedAllowedClientsSource = source;
|
|
480
|
+
cachedAllowedClients = source ? source.split(",").map((c) => c.trim().toLowerCase()).filter((c) => c.length > 0) : null;
|
|
481
|
+
return cachedAllowedClients;
|
|
482
|
+
}
|
|
483
|
+
var McpAuthError = class extends Error {
|
|
484
|
+
constructor(message) {
|
|
485
|
+
super(message);
|
|
486
|
+
this.name = "McpAuthError";
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function validateMcpStartup() {
|
|
490
|
+
if (!config.mcp.authEnabled) {
|
|
491
|
+
logger.info("MCP Server \u8BA4\u8BC1\u672A\u542F\u7528\uFF0C\u5141\u8BB8\u6240\u6709\u5BA2\u6237\u7AEF\u8BBF\u95EE");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!config.mcp.authToken) {
|
|
495
|
+
throw new McpAuthError(
|
|
496
|
+
"MCP_AUTH_TOKEN \u672A\u914D\u7F6E\u3002\u8BF7\u4F7F\u7528 'openssl rand -hex 32' \u751F\u6210\u4EE4\u724C\u5E76\u6DFB\u52A0\u5230 .env \u6587\u4EF6\u4E2D"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (config.mcp.authToken.length < 32) {
|
|
500
|
+
throw new McpAuthError(
|
|
501
|
+
`MCP_AUTH_TOKEN \u957F\u5EA6\u4E0D\u8DB3\uFF08\u81F3\u5C11\u9700\u8981 32 \u5B57\u7B26\uFF09\u3002\u8BF7\u4F7F\u7528 'openssl rand -hex 32' \u751F\u6210\u65B0\u4EE4\u724C`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
logger.info("MCP Server \u8BA4\u8BC1\u5DF2\u542F\u7528", {
|
|
505
|
+
hasValidToken: true,
|
|
506
|
+
hasAllowedClients: !!config.mcp.allowedClients
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function validateAuthToken(authToken) {
|
|
510
|
+
if (!config.mcp.authEnabled) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (!authToken) {
|
|
514
|
+
logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u672A\u63D0\u4F9B\u8BA4\u8BC1\u4EE4\u724C");
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
if (!config.mcp.authToken) {
|
|
518
|
+
logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const expected = Buffer.from(config.mcp.authToken);
|
|
522
|
+
const received = Buffer.from(authToken);
|
|
523
|
+
if (expected.length !== received.length) {
|
|
524
|
+
logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (!timingSafeEqual(expected, received)) {
|
|
528
|
+
logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
function validateClient(clientName) {
|
|
534
|
+
if (!config.mcp.authEnabled) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
const allowedClients = getParsedAllowedClients();
|
|
538
|
+
if (!allowedClients) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
if (!clientName) {
|
|
542
|
+
logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u672A\u63D0\u4F9B\u5BA2\u6237\u7AEF\u540D\u79F0");
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const isAllowed = allowedClients.includes(clientName.toLowerCase());
|
|
546
|
+
if (!isAllowed) {
|
|
547
|
+
logger.warn("MCP \u5BA2\u6237\u7AEF\u4E0D\u5728\u767D\u540D\u5355\u4E2D", {
|
|
548
|
+
clientName
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return isAllowed;
|
|
552
|
+
}
|
|
553
|
+
function validateHttpAuth(req) {
|
|
554
|
+
if (!config.mcp.authEnabled) return true;
|
|
555
|
+
const authHeader = req.headers.authorization;
|
|
556
|
+
const token = typeof authHeader === "string" ? authHeader.replace(/^Bearer\s+/i, "") : void 0;
|
|
557
|
+
if (!validateAuthToken(token)) {
|
|
558
|
+
logger.warn("HTTP \u8FDE\u63A5\u8BA4\u8BC1\u5931\u8D25", {
|
|
559
|
+
ip: req.socket.remoteAddress,
|
|
560
|
+
authHeader: authHeader ? "present" : "missing"
|
|
561
|
+
});
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const clientHeader = req.headers["x-mcp-client"];
|
|
565
|
+
const clientName = typeof clientHeader === "string" ? clientHeader : void 0;
|
|
566
|
+
if (!validateClient(clientName)) {
|
|
567
|
+
logger.warn("HTTP \u8FDE\u63A5\u5BA2\u6237\u7AEF\u9A8C\u8BC1\u5931\u8D25", {
|
|
568
|
+
ip: req.socket.remoteAddress,
|
|
569
|
+
clientName: clientName || "missing"
|
|
570
|
+
});
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
function getMcpAuthStatus() {
|
|
576
|
+
return {
|
|
577
|
+
authEnabled: config.mcp.authEnabled,
|
|
578
|
+
hasAuthToken: !!config.mcp.authToken,
|
|
579
|
+
tokenLengthSufficient: (config.mcp.authToken?.length || 0) >= 32,
|
|
580
|
+
hasAllowedClients: !!config.mcp.allowedClients
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/types/result.ts
|
|
585
|
+
function success(data) {
|
|
586
|
+
return { success: true, data };
|
|
587
|
+
}
|
|
588
|
+
function failure(error) {
|
|
589
|
+
return { success: false, error };
|
|
590
|
+
}
|
|
591
|
+
function extractHttpErrorDetail(error) {
|
|
592
|
+
const detail = {};
|
|
593
|
+
if (!(error instanceof Error)) {
|
|
594
|
+
detail.raw = String(error);
|
|
595
|
+
return detail;
|
|
596
|
+
}
|
|
597
|
+
detail.message = error.message;
|
|
598
|
+
if (axios2.isAxiosError(error)) {
|
|
599
|
+
detail.axiosCode = error.code;
|
|
600
|
+
if (error.config) {
|
|
601
|
+
detail.requestMethod = error.config.method;
|
|
602
|
+
detail.requestUrl = error.config.url;
|
|
603
|
+
detail.requestBaseURL = error.config.baseURL;
|
|
604
|
+
}
|
|
605
|
+
if (error.response) {
|
|
606
|
+
detail.httpStatus = error.response.status;
|
|
607
|
+
detail.httpStatusText = error.response.statusText;
|
|
608
|
+
extractResponseBody(error.response.data, detail);
|
|
609
|
+
}
|
|
610
|
+
return detail;
|
|
611
|
+
}
|
|
612
|
+
const err = error;
|
|
613
|
+
if (err.response && typeof err.response === "object") {
|
|
614
|
+
const resp = err.response;
|
|
615
|
+
detail.httpStatus = resp.status;
|
|
616
|
+
if (resp.data && typeof resp.data === "object") {
|
|
617
|
+
extractResponseBody(resp.data, detail);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (err.config && typeof err.config === "object") {
|
|
621
|
+
const cfg = err.config;
|
|
622
|
+
detail.requestUrl = cfg.url;
|
|
623
|
+
detail.requestMethod = cfg.method;
|
|
624
|
+
}
|
|
625
|
+
if (err.code) detail.axiosCode = err.code;
|
|
626
|
+
return detail;
|
|
627
|
+
}
|
|
628
|
+
function extractResponseBody(data, detail) {
|
|
629
|
+
if (typeof data === "string") {
|
|
630
|
+
detail.responseBody = data.slice(0, 500);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (typeof data === "object" && data !== null) {
|
|
634
|
+
const d = data;
|
|
635
|
+
const body = {};
|
|
636
|
+
for (const key of ["message", "error", "errors", "code", "msg", "log_id", "troubleshooter"]) {
|
|
637
|
+
if (d[key] !== void 0) body[key] = d[key];
|
|
638
|
+
}
|
|
639
|
+
if (Object.keys(body).length > 0) {
|
|
640
|
+
detail.responseBody = body;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/services/platforms/BasePlatformClient.ts
|
|
646
|
+
var RETRY_ERROR_CODES = ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ENETUNREACH"];
|
|
647
|
+
var BasePlatformClient = class {
|
|
648
|
+
api;
|
|
649
|
+
platformName;
|
|
650
|
+
constructor(platformName, config2) {
|
|
651
|
+
this.platformName = platformName;
|
|
652
|
+
this.api = this.buildAxiosInstance(config2);
|
|
653
|
+
logger.debug(`${platformName} \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316`, { baseURL: config2.baseURL });
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 构建 axios 实例(含认证和重试配置)
|
|
657
|
+
*/
|
|
658
|
+
buildAxiosInstance(config2) {
|
|
659
|
+
const { baseURL, auth, timeout = 3e4, retries = 3 } = config2;
|
|
660
|
+
const headers = {};
|
|
661
|
+
let axiosAuth;
|
|
662
|
+
if (auth.type === "bearer") {
|
|
663
|
+
headers.Authorization = `Bearer ${auth.token}`;
|
|
664
|
+
} else if (auth.type === "basic") {
|
|
665
|
+
axiosAuth = {
|
|
666
|
+
username: auth.username,
|
|
667
|
+
password: auth.password
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const instance = axios2.create({
|
|
671
|
+
baseURL,
|
|
672
|
+
headers,
|
|
673
|
+
auth: axiosAuth,
|
|
674
|
+
timeout
|
|
675
|
+
});
|
|
676
|
+
this.setupRetry(instance, retries);
|
|
677
|
+
return instance;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* 配置自动重试机制
|
|
681
|
+
*/
|
|
682
|
+
setupRetry(instance, retries) {
|
|
683
|
+
axiosRetry(instance, {
|
|
684
|
+
retries,
|
|
685
|
+
retryCondition: (error) => {
|
|
686
|
+
return axios2.isAxiosError(error) && (!error.response || error.response.status >= 500 || RETRY_ERROR_CODES.includes(error.code || ""));
|
|
687
|
+
},
|
|
688
|
+
onRetry: (retryCount, error) => {
|
|
689
|
+
logger.warn(`${this.platformName} API \u8BF7\u6C42\u91CD\u8BD5`, {
|
|
690
|
+
retryCount,
|
|
691
|
+
...extractHttpErrorDetail(error)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* 创建额外的 API 实例(共享相同的认证和重试配置)
|
|
698
|
+
*/
|
|
699
|
+
createApiInstance(config2) {
|
|
700
|
+
return this.buildAxiosInstance(config2);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* 包装 API 调用,统一错误处理
|
|
704
|
+
*/
|
|
705
|
+
async wrapApiCall(operation, apiCall) {
|
|
706
|
+
const startTime = Date.now();
|
|
707
|
+
try {
|
|
708
|
+
logger.debug(`${this.platformName}: ${operation}`);
|
|
709
|
+
const data = await apiCall();
|
|
710
|
+
logger.debug(`${this.platformName}: ${operation} \u6210\u529F`, {
|
|
711
|
+
durationMs: Date.now() - startTime
|
|
712
|
+
});
|
|
713
|
+
return success(data);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
const durationMs = Date.now() - startTime;
|
|
716
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
717
|
+
logger.error(`${this.platformName}: ${operation} \u5931\u8D25`, {
|
|
718
|
+
durationMs,
|
|
719
|
+
...extractHttpErrorDetail(error)
|
|
720
|
+
});
|
|
721
|
+
return failure(
|
|
722
|
+
new Error(`${this.platformName} ${operation} \u5931\u8D25: ${errorMessage}`)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* 获取平台名称
|
|
728
|
+
*/
|
|
729
|
+
getPlatformName() {
|
|
730
|
+
return this.platformName;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/services/platforms/BitbucketClient.ts
|
|
735
|
+
var BitbucketClient = class extends BasePlatformClient {
|
|
736
|
+
/**
|
|
737
|
+
* Patch API 使用独立实例,因为 Bitbucket Server 的 REST API 和 Patch API
|
|
738
|
+
* 使用不同的路径前缀(/rest/api/latest vs /rest/patch/1.0)。
|
|
739
|
+
* 合并为单实例需要在每次调用时传入完整路径,增加出错风险,故保持分离。
|
|
740
|
+
*/
|
|
741
|
+
patchApi;
|
|
742
|
+
constructor(config2) {
|
|
743
|
+
if (!config2.baseUrl) {
|
|
744
|
+
throw new Error("Bitbucket baseUrl is required");
|
|
745
|
+
}
|
|
746
|
+
if (!config2.token && !(config2.username && config2.password)) {
|
|
747
|
+
throw new Error("Either token or username/password is required for Bitbucket authentication");
|
|
748
|
+
}
|
|
749
|
+
const auth = config2.token ? { type: "bearer", token: config2.token } : { type: "basic", username: config2.username, password: config2.password };
|
|
750
|
+
super("Bitbucket", {
|
|
751
|
+
baseURL: `${config2.baseUrl}/rest/api/latest`,
|
|
752
|
+
auth,
|
|
753
|
+
timeout: 3e4
|
|
754
|
+
});
|
|
755
|
+
this.patchApi = this.createApiInstance({
|
|
756
|
+
baseURL: `${config2.baseUrl}/rest/patch/1.0`,
|
|
757
|
+
auth,
|
|
758
|
+
timeout: 3e4
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
async getPullRequestDiff(params, contextLines = 10) {
|
|
762
|
+
const { project, repository, prId } = params;
|
|
763
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR diff (${project}/${repository}#${prId})`, async () => {
|
|
764
|
+
const response = await this.api.get(
|
|
765
|
+
`/projects/${project}/repos/${repository}/pull-requests/${prId}/diff`,
|
|
766
|
+
{
|
|
767
|
+
params: { contextLines },
|
|
768
|
+
headers: { Accept: "text/plain" }
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
return response.data;
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
async getPullRequestPatch(params) {
|
|
775
|
+
const { project, repository, prId } = params;
|
|
776
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR patch (${project}/${repository}#${prId})`, async () => {
|
|
777
|
+
const response = await this.patchApi.get(
|
|
778
|
+
`/projects/${project}/repos/${repository}/pull-requests/${prId}/patch`
|
|
779
|
+
);
|
|
780
|
+
return response.data;
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async getPullRequest(params) {
|
|
784
|
+
const { project, repository, prId } = params;
|
|
785
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR \u8BE6\u60C5 (${project}/${repository}#${prId})`, async () => {
|
|
786
|
+
const response = await this.api.get(
|
|
787
|
+
`/projects/${project}/repos/${repository}/pull-requests/${prId}`
|
|
788
|
+
);
|
|
789
|
+
return response.data;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async getPullRequests(params) {
|
|
793
|
+
const { project, repository, state = "OPEN", limit = 100 } = params;
|
|
794
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR \u5217\u8868 (${project}/${repository})`, async () => {
|
|
795
|
+
const response = await this.api.get(`/projects/${project}/repos/${repository}/pull-requests`, {
|
|
796
|
+
params: { state, limit }
|
|
797
|
+
});
|
|
798
|
+
return response.data.values;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async getPullRequestChanges(params) {
|
|
802
|
+
const { project, repository, prId } = params;
|
|
803
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR \u53D8\u66F4\u6587\u4EF6 (${project}/${repository}#${prId})`, async () => {
|
|
804
|
+
const response = await this.api.get(
|
|
805
|
+
`/projects/${project}/repos/${repository}/pull-requests/${prId}/changes`,
|
|
806
|
+
{
|
|
807
|
+
params: { limit: 1e3 }
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
const files = response.data.values.map((change) => {
|
|
811
|
+
return change.path?.toString || "";
|
|
812
|
+
}).filter(Boolean);
|
|
813
|
+
return files;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
async getPullRequestCommitsCount(params) {
|
|
817
|
+
const { project, repository, prId } = params;
|
|
818
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR \u63D0\u4EA4\u6570\u91CF (${project}/${repository}#${prId})`, async () => {
|
|
819
|
+
const response = await this.api.get(
|
|
820
|
+
`/projects/${project}/repos/${repository}/pull-requests/${prId}/commits`,
|
|
821
|
+
{
|
|
822
|
+
params: { limit: 1 }
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
return response.data.size || 0;
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
async createInlineComment(project, repository, prId, file, line, text) {
|
|
829
|
+
return this.wrapApiCall(`\u521B\u5EFA\u884C\u5185\u8BC4\u8BBA (${project}/${repository}#${prId})`, async () => {
|
|
830
|
+
const url = `/projects/${project}/repos/${repository}/pull-requests/${prId}/comments`;
|
|
831
|
+
const requestBody = {
|
|
832
|
+
text,
|
|
833
|
+
anchor: {
|
|
834
|
+
diffType: "EFFECTIVE",
|
|
835
|
+
path: file,
|
|
836
|
+
line,
|
|
837
|
+
lineType: "ADDED"
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
const response = await this.api.post(url, requestBody);
|
|
841
|
+
return response.status === 201;
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
async getPullRequestComments(params) {
|
|
845
|
+
const { project, repository, prId } = params;
|
|
846
|
+
return this.wrapApiCall(`\u83B7\u53D6 PR \u8BC4\u8BBA (${project}/${repository}#${prId})`, async () => {
|
|
847
|
+
const url = `/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`;
|
|
848
|
+
const response = await this.api.get(url, {
|
|
849
|
+
params: { limit: 100 }
|
|
850
|
+
});
|
|
851
|
+
if (response.data && response.data.values) {
|
|
852
|
+
const comments = response.data.values.filter((activity) => activity.action === "COMMENTED" && activity.comment).map((activity) => activity.comment);
|
|
853
|
+
return comments;
|
|
854
|
+
}
|
|
855
|
+
return [];
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async createPullRequest(params) {
|
|
859
|
+
const { title, description, from, to } = params;
|
|
860
|
+
return this.wrapApiCall(`\u521B\u5EFA Pull Request (${to.project}/${to.repository})`, async () => {
|
|
861
|
+
const response = await this.api.post(
|
|
862
|
+
`/projects/${to.project}/repos/${to.repository}/pull-requests`,
|
|
863
|
+
{
|
|
864
|
+
title,
|
|
865
|
+
description: description || "",
|
|
866
|
+
fromRef: {
|
|
867
|
+
id: `refs/heads/${from.branch}`,
|
|
868
|
+
repository: {
|
|
869
|
+
slug: from.repository,
|
|
870
|
+
project: {
|
|
871
|
+
key: from.project
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
toRef: {
|
|
876
|
+
id: `refs/heads/${to.branch}`,
|
|
877
|
+
repository: {
|
|
878
|
+
slug: to.repository,
|
|
879
|
+
project: {
|
|
880
|
+
key: to.project
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
return response.data;
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
function createBitbucketClient() {
|
|
891
|
+
if (!config.bitbucket.host) return null;
|
|
892
|
+
const hasToken = !!config.bitbucket.token;
|
|
893
|
+
const hasBasicAuth = !!(config.bitbucket.username && config.bitbucket.password);
|
|
894
|
+
if (!hasToken && !hasBasicAuth) return null;
|
|
895
|
+
return new BitbucketClient({
|
|
896
|
+
baseUrl: config.bitbucket.host,
|
|
897
|
+
token: config.bitbucket.token,
|
|
898
|
+
username: config.bitbucket.username,
|
|
899
|
+
password: config.bitbucket.password
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/services/platforms/ConfluenceClient.ts
|
|
904
|
+
var ConfluenceClient = class extends BasePlatformClient {
|
|
905
|
+
host;
|
|
906
|
+
constructor(host, token) {
|
|
907
|
+
super("Confluence", {
|
|
908
|
+
baseURL: `${host}/rest/api`,
|
|
909
|
+
auth: { type: "bearer", token },
|
|
910
|
+
timeout: 3e4
|
|
911
|
+
});
|
|
912
|
+
this.host = host;
|
|
913
|
+
this.api.defaults.headers.common["Content-Type"] = "application/json";
|
|
914
|
+
}
|
|
915
|
+
async getPage(pageId) {
|
|
916
|
+
return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId}`, async () => {
|
|
917
|
+
const response = await this.api.get(`/content/${pageId}`, {
|
|
918
|
+
params: {
|
|
919
|
+
expand: "body.storage,version,history,space"
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
const page = response.data;
|
|
923
|
+
return {
|
|
924
|
+
id: page.id,
|
|
925
|
+
type: page.type,
|
|
926
|
+
title: page.title,
|
|
927
|
+
space: {
|
|
928
|
+
key: page.space?.key,
|
|
929
|
+
name: page.space?.name
|
|
930
|
+
},
|
|
931
|
+
version: {
|
|
932
|
+
number: page.version?.number,
|
|
933
|
+
when: page.version?.when,
|
|
934
|
+
by: page.version?.by?.displayName
|
|
935
|
+
},
|
|
936
|
+
body: {
|
|
937
|
+
storage: page.body?.storage?.value
|
|
938
|
+
},
|
|
939
|
+
history: {
|
|
940
|
+
createdBy: page.history?.createdBy?.displayName,
|
|
941
|
+
createdDate: page.history?.createdDate
|
|
942
|
+
},
|
|
943
|
+
webUrl: new URL(page._links?.webui ?? "", this.host).toString()
|
|
944
|
+
};
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async getChildPages(pageId, options = {}) {
|
|
948
|
+
const { limit = 50, expand = "version,space" } = options;
|
|
949
|
+
return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId} \u7684\u5B50\u9875\u9762`, async () => {
|
|
950
|
+
const response = await this.api.get(
|
|
951
|
+
`/content/${pageId}/child/page`,
|
|
952
|
+
{
|
|
953
|
+
params: {
|
|
954
|
+
limit,
|
|
955
|
+
expand
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
);
|
|
959
|
+
const childPages = response.data.results.map((child) => ({
|
|
960
|
+
id: child.id,
|
|
961
|
+
type: child.type,
|
|
962
|
+
title: child.title,
|
|
963
|
+
status: child.status,
|
|
964
|
+
space: {
|
|
965
|
+
key: child.space?.key,
|
|
966
|
+
name: child.space?.name
|
|
967
|
+
},
|
|
968
|
+
version: {
|
|
969
|
+
number: child.version?.number,
|
|
970
|
+
when: child.version?.when,
|
|
971
|
+
by: child.version?.by?.displayName
|
|
972
|
+
},
|
|
973
|
+
webUrl: new URL(child._links?.webui ?? "", this.host).toString(),
|
|
974
|
+
position: child.extensions?.position
|
|
975
|
+
}));
|
|
976
|
+
return {
|
|
977
|
+
parentPageId: pageId,
|
|
978
|
+
totalChildren: response.data.size,
|
|
979
|
+
start: response.data.start,
|
|
980
|
+
limit: response.data.limit,
|
|
981
|
+
children: childPages
|
|
982
|
+
};
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async getPageComments(pageId, options = {}) {
|
|
986
|
+
const { limit = 50 } = options;
|
|
987
|
+
return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId} \u7684\u8BC4\u8BBA`, async () => {
|
|
988
|
+
const response = await this.api.get(
|
|
989
|
+
`/content/${pageId}/child/comment`,
|
|
990
|
+
{
|
|
991
|
+
params: {
|
|
992
|
+
limit,
|
|
993
|
+
expand: "body.storage,version,extensions.inlineProperties"
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
const comments = response.data.results.map((comment) => ({
|
|
998
|
+
id: comment.id,
|
|
999
|
+
body: comment.body?.storage?.value,
|
|
1000
|
+
author: comment.version?.by?.displayName,
|
|
1001
|
+
createdAt: comment.version?.when,
|
|
1002
|
+
location: comment.extensions?.location
|
|
1003
|
+
}));
|
|
1004
|
+
return {
|
|
1005
|
+
pageId,
|
|
1006
|
+
totalComments: response.data.size,
|
|
1007
|
+
start: response.data.start,
|
|
1008
|
+
limit: response.data.limit,
|
|
1009
|
+
comments
|
|
1010
|
+
};
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
function createConfluenceClient() {
|
|
1015
|
+
const { host, token } = config.confluence;
|
|
1016
|
+
if (!host || !token) return null;
|
|
1017
|
+
return new ConfluenceClient(host, token);
|
|
1018
|
+
}
|
|
1019
|
+
var FEISHU_PREFIX_MAP = {
|
|
1020
|
+
m: "story",
|
|
1021
|
+
f: "issue",
|
|
1022
|
+
g: "assignment"
|
|
1023
|
+
};
|
|
1024
|
+
function parseFeishuWorkItemId(workItemId) {
|
|
1025
|
+
const urlMatch = workItemId.match(/project\.feishu\.cn\/[^/]+\/([^/]+)\/detail\/(\d+)/);
|
|
1026
|
+
if (urlMatch) {
|
|
1027
|
+
return { type: urlMatch[1], id: urlMatch[2] };
|
|
1028
|
+
}
|
|
1029
|
+
const prefixMatch = workItemId.match(/^([a-zA-Z]+)-(\d+)$/);
|
|
1030
|
+
if (prefixMatch) {
|
|
1031
|
+
const prefix = prefixMatch[1].toLowerCase();
|
|
1032
|
+
return { type: FEISHU_PREFIX_MAP[prefix] || prefix, id: prefixMatch[2] };
|
|
1033
|
+
}
|
|
1034
|
+
if (/^\d+$/.test(workItemId)) {
|
|
1035
|
+
return { type: "issue", id: workItemId };
|
|
1036
|
+
}
|
|
1037
|
+
throw new Error(`\u65E0\u6548\u7684\u98DE\u4E66\u5DE5\u4F5C\u9879 ID: ${workItemId.slice(0, 100)}`);
|
|
1038
|
+
}
|
|
1039
|
+
var FeishuClient = class extends BasePlatformClient {
|
|
1040
|
+
projectKey;
|
|
1041
|
+
pluginId;
|
|
1042
|
+
pluginSecret;
|
|
1043
|
+
useVirtualToken;
|
|
1044
|
+
userKey;
|
|
1045
|
+
pluginToken = "";
|
|
1046
|
+
pluginTokenExpireTimestamp = 0;
|
|
1047
|
+
tokenRefreshPromise = null;
|
|
1048
|
+
tokenApi;
|
|
1049
|
+
constructor(cfg = {}) {
|
|
1050
|
+
super("Feishu", {
|
|
1051
|
+
baseURL: "https://project.feishu.cn/open_api",
|
|
1052
|
+
auth: { type: "none" },
|
|
1053
|
+
timeout: 3e4
|
|
1054
|
+
});
|
|
1055
|
+
this.projectKey = cfg.projectKey;
|
|
1056
|
+
this.pluginId = cfg.pluginId;
|
|
1057
|
+
this.pluginSecret = cfg.pluginSecret;
|
|
1058
|
+
this.useVirtualToken = cfg.useVirtualToken ?? false;
|
|
1059
|
+
this.userKey = cfg.userKey;
|
|
1060
|
+
this.tokenApi = axios2.create({ timeout: 3e4 });
|
|
1061
|
+
this.api.interceptors.request.use(async (reqConfig) => {
|
|
1062
|
+
const token = await this.getPluginToken();
|
|
1063
|
+
if (token) {
|
|
1064
|
+
reqConfig.headers["X-PLUGIN-TOKEN"] = token;
|
|
1065
|
+
}
|
|
1066
|
+
if (this.userKey) {
|
|
1067
|
+
reqConfig.headers["X-USER-KEY"] = this.userKey;
|
|
1068
|
+
}
|
|
1069
|
+
reqConfig.headers["Content-Type"] = "application/json; charset=utf-8";
|
|
1070
|
+
return reqConfig;
|
|
1071
|
+
});
|
|
1072
|
+
this.api.interceptors.response.use(
|
|
1073
|
+
(response) => response,
|
|
1074
|
+
async (error) => {
|
|
1075
|
+
const { response, config: reqConfig } = error;
|
|
1076
|
+
if ((response?.status === 401 || response?.status === 403) && reqConfig && !reqConfig._retried) {
|
|
1077
|
+
this.pluginToken = "";
|
|
1078
|
+
this.pluginTokenExpireTimestamp = 0;
|
|
1079
|
+
reqConfig._retried = true;
|
|
1080
|
+
logger.warn("\u98DE\u4E66 API \u8BA4\u8BC1\u5931\u8D25\uFF0C\u5237\u65B0 token \u540E\u91CD\u8BD5", { status: response?.status });
|
|
1081
|
+
return this.api.request(reqConfig);
|
|
1082
|
+
}
|
|
1083
|
+
return Promise.reject(error);
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
async getPluginToken() {
|
|
1088
|
+
if (!this.pluginId || !this.pluginSecret) {
|
|
1089
|
+
logger.warn("\u98DE\u4E66\u9879\u76EE\u8BA4\u8BC1\u51ED\u8BC1\u7F3A\u5931 (\u9700\u8981 Plugin ID/Secret \u6216 User Token)");
|
|
1090
|
+
return "";
|
|
1091
|
+
}
|
|
1092
|
+
if (this.pluginToken && Date.now() < this.pluginTokenExpireTimestamp) {
|
|
1093
|
+
return this.pluginToken;
|
|
1094
|
+
}
|
|
1095
|
+
if (this.tokenRefreshPromise) {
|
|
1096
|
+
return this.tokenRefreshPromise;
|
|
1097
|
+
}
|
|
1098
|
+
this.tokenRefreshPromise = this.refreshPluginToken();
|
|
1099
|
+
try {
|
|
1100
|
+
return await this.tokenRefreshPromise;
|
|
1101
|
+
} finally {
|
|
1102
|
+
this.tokenRefreshPromise = null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async refreshPluginToken() {
|
|
1106
|
+
try {
|
|
1107
|
+
const url = "https://project.feishu.cn/open_api/authen/plugin_token";
|
|
1108
|
+
const tokenType = this.useVirtualToken ? 1 : 0;
|
|
1109
|
+
const tokenTypeName = this.useVirtualToken ? "virtual_plugin_token" : "plugin_access_token";
|
|
1110
|
+
logger.debug(`\u8BF7\u6C42\u98DE\u4E66 Plugin Token (type=${tokenType}, ${tokenTypeName})`);
|
|
1111
|
+
const response = await this.tokenApi.post(url, {
|
|
1112
|
+
plugin_id: this.pluginId,
|
|
1113
|
+
plugin_secret: this.pluginSecret,
|
|
1114
|
+
type: tokenType
|
|
1115
|
+
});
|
|
1116
|
+
const { error: errorInfo, data: authData } = response.data;
|
|
1117
|
+
if (errorInfo && errorInfo.code !== 0) {
|
|
1118
|
+
logger.error("\u83B7\u53D6 Plugin Token \u5931\u8D25", {
|
|
1119
|
+
code: errorInfo.code,
|
|
1120
|
+
msg: errorInfo.msg,
|
|
1121
|
+
tokenType: tokenTypeName
|
|
1122
|
+
});
|
|
1123
|
+
return "";
|
|
1124
|
+
}
|
|
1125
|
+
this.pluginToken = authData?.token || "";
|
|
1126
|
+
if (!this.pluginToken) {
|
|
1127
|
+
logger.error("Plugin Token \u4E0D\u5B58\u5728\u4E8E\u54CD\u5E94\u4E2D");
|
|
1128
|
+
return "";
|
|
1129
|
+
}
|
|
1130
|
+
const expiresIn = authData?.expire_time || 7200;
|
|
1131
|
+
this.pluginTokenExpireTimestamp = Date.now() + (expiresIn - 60) * 1e3;
|
|
1132
|
+
logger.info("\u6210\u529F\u83B7\u53D6\u98DE\u4E66\u9879\u76EE Plugin Token", {
|
|
1133
|
+
tokenType: tokenTypeName,
|
|
1134
|
+
expiresIn
|
|
1135
|
+
});
|
|
1136
|
+
return this.pluginToken;
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
logger.error("\u83B7\u53D6\u98DE\u4E66\u9879\u76EE Plugin Token \u5931\u8D25", { error });
|
|
1139
|
+
return "";
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async getWorkItemComments(workItemTypeKey, workItemId) {
|
|
1143
|
+
if (!this.projectKey || !this.userKey) {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
logger.debug(`\u5C1D\u8BD5\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA`, {
|
|
1148
|
+
workItemId,
|
|
1149
|
+
workItemTypeKey
|
|
1150
|
+
});
|
|
1151
|
+
const response = await this.api.get(
|
|
1152
|
+
`/${this.projectKey}/work_item/${workItemTypeKey}/${workItemId}/comments`
|
|
1153
|
+
);
|
|
1154
|
+
if (response.data.err_code !== 0) {
|
|
1155
|
+
logger.warn(`\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA\u5931\u8D25`, {
|
|
1156
|
+
err_code: response.data.err_code,
|
|
1157
|
+
err_msg: response.data.err_msg,
|
|
1158
|
+
workItemId
|
|
1159
|
+
});
|
|
1160
|
+
return [];
|
|
1161
|
+
}
|
|
1162
|
+
const comments = response.data.data || [];
|
|
1163
|
+
const { pagination } = response.data;
|
|
1164
|
+
logger.info(`\u6210\u529F\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA`, {
|
|
1165
|
+
workItemId,
|
|
1166
|
+
commentCount: comments.length,
|
|
1167
|
+
total: pagination?.total,
|
|
1168
|
+
pageNum: pagination?.page_num,
|
|
1169
|
+
pageSize: pagination?.page_size
|
|
1170
|
+
});
|
|
1171
|
+
return comments.map((comment) => ({
|
|
1172
|
+
id: String(comment.id),
|
|
1173
|
+
content: comment.content,
|
|
1174
|
+
workItemId: String(comment.work_item_id),
|
|
1175
|
+
workItemTypeKey: comment.work_item_type_key,
|
|
1176
|
+
createdAt: comment.created_at,
|
|
1177
|
+
operator: comment.operator
|
|
1178
|
+
}));
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1181
|
+
const axiosError = error;
|
|
1182
|
+
logger.error(`\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA\u5931\u8D25 (\u53EF\u80FD\u4E0D\u652F\u6301\u8BE5 API)`, {
|
|
1183
|
+
workItemId,
|
|
1184
|
+
error: errorMsg,
|
|
1185
|
+
status: axiosError.response?.status
|
|
1186
|
+
});
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async getWorkItem(workItemId) {
|
|
1191
|
+
if (!this.projectKey) {
|
|
1192
|
+
return failure(new Error("\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u7F3A\u5931 (\u9700\u8981 FEISHU_PROJECT_KEY)"));
|
|
1193
|
+
}
|
|
1194
|
+
if (!this.userKey) {
|
|
1195
|
+
return failure(new Error("\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u7F3A\u5931 (\u9700\u8981 FEISHU_PROJECT_USER_KEY)"));
|
|
1196
|
+
}
|
|
1197
|
+
const { type: workItemTypeKey, id: actualId } = parseFeishuWorkItemId(workItemId);
|
|
1198
|
+
logger.debug("\u89E3\u6790\u98DE\u4E66\u5DE5\u4F5C\u9879 ID", {
|
|
1199
|
+
input: workItemId,
|
|
1200
|
+
type: workItemTypeKey,
|
|
1201
|
+
id: actualId
|
|
1202
|
+
});
|
|
1203
|
+
return this.wrapApiCall(`\u83B7\u53D6\u5DE5\u4F5C\u9879 ${actualId}`, async () => {
|
|
1204
|
+
const response = await this.api.post(
|
|
1205
|
+
`/${this.projectKey}/work_item/${workItemTypeKey}/query`,
|
|
1206
|
+
{
|
|
1207
|
+
work_item_ids: [Number.parseInt(actualId)]
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
if (response.data.err_code !== 0) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`\u98DE\u4E66 API \u9519\u8BEF (err_code=${response.data.err_code}): ${response.data.err_msg}`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
const items = response.data.data;
|
|
1216
|
+
if (!items || items.length === 0) {
|
|
1217
|
+
throw new Error(`\u98DE\u4E66\u5DE5\u4F5C\u9879\u4E0D\u5B58\u5728 (${actualId})`);
|
|
1218
|
+
}
|
|
1219
|
+
const workItem = items[0];
|
|
1220
|
+
let description = "";
|
|
1221
|
+
const descField = workItem.fields?.find((f) => f.field_key === "description");
|
|
1222
|
+
if (descField && "field_value" in descField) {
|
|
1223
|
+
description = String(descField.field_value || "");
|
|
1224
|
+
}
|
|
1225
|
+
const comments = await this.getWorkItemComments(workItemTypeKey, actualId);
|
|
1226
|
+
return {
|
|
1227
|
+
projectKey: this.projectKey,
|
|
1228
|
+
id: String(workItem.id),
|
|
1229
|
+
name: workItem.name || "",
|
|
1230
|
+
description,
|
|
1231
|
+
work_item_type_key: workItem.work_item_type_key || workItemTypeKey,
|
|
1232
|
+
work_item_type_name: workItem.work_item_type_name || workItem.work_item_type_key || workItemTypeKey,
|
|
1233
|
+
comments
|
|
1234
|
+
};
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
function createFeishuClient() {
|
|
1239
|
+
const { projectKey, pluginId, pluginSecret } = config.feishuProject;
|
|
1240
|
+
if (!projectKey || !pluginId || !pluginSecret) return null;
|
|
1241
|
+
return new FeishuClient({
|
|
1242
|
+
projectKey,
|
|
1243
|
+
pluginId,
|
|
1244
|
+
pluginSecret,
|
|
1245
|
+
useVirtualToken: config.feishuProject.useVirtualToken,
|
|
1246
|
+
userKey: config.feishuProject.userKey
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
var FEISHU_OPEN_DOC_TYPE_MAP = {
|
|
1250
|
+
docx: "docx",
|
|
1251
|
+
doc: "doc",
|
|
1252
|
+
sheet: "sheet",
|
|
1253
|
+
sheets: "sheet",
|
|
1254
|
+
wiki: "docx",
|
|
1255
|
+
base: "file",
|
|
1256
|
+
slides: "slides"
|
|
1257
|
+
};
|
|
1258
|
+
function parseFeishuDocumentId(input) {
|
|
1259
|
+
const urlMatch = input.match(/\.feishu\.cn\/([a-z]+)\/([A-Za-z0-9]+)/);
|
|
1260
|
+
if (urlMatch) {
|
|
1261
|
+
const rawType = urlMatch[1];
|
|
1262
|
+
const fileType = FEISHU_OPEN_DOC_TYPE_MAP[rawType] || "docx";
|
|
1263
|
+
return { documentId: urlMatch[2], fileType, isWiki: rawType === "wiki" };
|
|
1264
|
+
}
|
|
1265
|
+
if (/^[A-Za-z0-9]{6,}$/.test(input)) {
|
|
1266
|
+
return { documentId: input, fileType: "docx", isWiki: false };
|
|
1267
|
+
}
|
|
1268
|
+
throw new Error(`\u65E0\u6548\u7684\u98DE\u4E66\u6587\u6863\u6807\u8BC6: ${input.slice(0, 100)}`);
|
|
1269
|
+
}
|
|
1270
|
+
function extractReplyText(reply) {
|
|
1271
|
+
return (reply.content?.elements || []).filter((el) => el.type === "text_run" && el.text_run?.text).map((el) => el.text_run.text).join("");
|
|
1272
|
+
}
|
|
1273
|
+
var PARAGRAPH_BLOCK_TYPE = 2;
|
|
1274
|
+
var sdkLogger = {
|
|
1275
|
+
error: (...msg) => logger.error("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
|
|
1276
|
+
warn: (...msg) => logger.warn("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
|
|
1277
|
+
info: (...msg) => logger.info("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
|
|
1278
|
+
debug: (...msg) => logger.debug("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
|
|
1279
|
+
trace: (...msg) => logger.debug("FeishuSDK:trace", { detail: msg.length === 1 ? msg[0] : msg })
|
|
1280
|
+
};
|
|
1281
|
+
var FeishuOpenClient = class {
|
|
1282
|
+
client;
|
|
1283
|
+
constructor(cfg) {
|
|
1284
|
+
this.client = new lark.Client({
|
|
1285
|
+
appId: cfg.appId,
|
|
1286
|
+
appSecret: cfg.appSecret,
|
|
1287
|
+
loggerLevel: lark.LoggerLevel.warn,
|
|
1288
|
+
logger: sdkLogger
|
|
1289
|
+
});
|
|
1290
|
+
logger.info("FeishuOpen \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316", { appId: cfg.appId });
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* 通过 wiki token 获取实际的 document_id 和文档类型
|
|
1294
|
+
* wiki 页面的 token 与 document_id 不同,需要先解析
|
|
1295
|
+
*/
|
|
1296
|
+
async resolveWikiToken(wikiToken) {
|
|
1297
|
+
const res = await this.client.wiki.space.getNode({
|
|
1298
|
+
params: { token: wikiToken }
|
|
1299
|
+
});
|
|
1300
|
+
if (res.code !== 0) {
|
|
1301
|
+
throw new Error(`\u89E3\u6790 Wiki token \u5931\u8D25 (code=${res.code}): ${res.msg}`);
|
|
1302
|
+
}
|
|
1303
|
+
const node = res.data?.node;
|
|
1304
|
+
if (!node?.obj_token) {
|
|
1305
|
+
throw new Error("\u89E3\u6790 Wiki token \u6210\u529F\u4F46\u672A\u8FD4\u56DE obj_token");
|
|
1306
|
+
}
|
|
1307
|
+
return { objToken: node.obj_token, objType: node.obj_type ?? "docx" };
|
|
1308
|
+
}
|
|
1309
|
+
async getDocumentContent(documentId) {
|
|
1310
|
+
const res = await this.client.docx.document.rawContent({
|
|
1311
|
+
path: { document_id: documentId }
|
|
1312
|
+
});
|
|
1313
|
+
if (res.code !== 0) {
|
|
1314
|
+
throw new Error(`\u83B7\u53D6\u6587\u6863\u5185\u5BB9\u5931\u8D25 (code=${res.code}): ${res.msg}`);
|
|
1315
|
+
}
|
|
1316
|
+
return res.data?.content ?? "";
|
|
1317
|
+
}
|
|
1318
|
+
async getDocumentComments(fileToken, fileType) {
|
|
1319
|
+
const comments = [];
|
|
1320
|
+
let pageToken;
|
|
1321
|
+
for (let page = 0; page < 5; page++) {
|
|
1322
|
+
try {
|
|
1323
|
+
const res = await this.client.drive.fileComment.list({
|
|
1324
|
+
path: { file_token: fileToken },
|
|
1325
|
+
params: {
|
|
1326
|
+
file_type: fileType,
|
|
1327
|
+
page_size: 50,
|
|
1328
|
+
...pageToken ? { page_token: pageToken } : {}
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
if (res.code !== 0) {
|
|
1332
|
+
logger.warn("\u83B7\u53D6\u6587\u6863\u8BC4\u8BBA\u5931\u8D25", { code: res.code, msg: res.msg, fileToken });
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
for (const item of res.data?.items ?? []) {
|
|
1336
|
+
const replies = item.reply_list?.replies ?? [];
|
|
1337
|
+
const content = replies.map(extractReplyText).filter(Boolean).join("\n");
|
|
1338
|
+
if (content && item.comment_id) {
|
|
1339
|
+
comments.push({
|
|
1340
|
+
commentId: item.comment_id,
|
|
1341
|
+
content,
|
|
1342
|
+
userId: item.user_id ?? "unknown",
|
|
1343
|
+
createTime: item.create_time ?? 0,
|
|
1344
|
+
isSolved: item.is_solved ?? false
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (!res.data?.has_more) break;
|
|
1349
|
+
pageToken = res.data.page_token;
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
logger.warn("\u83B7\u53D6\u6587\u6863\u8BC4\u8BBA\u5931\u8D25\uFF08\u53EF\u80FD\u65E0\u6743\u9650\uFF09", {
|
|
1352
|
+
fileToken,
|
|
1353
|
+
...extractHttpErrorDetail(error)
|
|
1354
|
+
});
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return comments;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* 将纯文本按段落拆分为飞书文档 block 结构
|
|
1362
|
+
*/
|
|
1363
|
+
textToBlocks(content) {
|
|
1364
|
+
return content.split("\n").map((line) => ({
|
|
1365
|
+
block_type: PARAGRAPH_BLOCK_TYPE,
|
|
1366
|
+
paragraph: {
|
|
1367
|
+
elements: [{ text_run: { content: line } }]
|
|
1368
|
+
}
|
|
1369
|
+
}));
|
|
1370
|
+
}
|
|
1371
|
+
async createDocument(title, content, folderToken) {
|
|
1372
|
+
try {
|
|
1373
|
+
const createRes = await this.client.docx.document.create({
|
|
1374
|
+
data: {
|
|
1375
|
+
title,
|
|
1376
|
+
...folderToken ? { folder_token: folderToken } : {}
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
if (createRes.code !== 0) {
|
|
1380
|
+
throw new Error(`\u521B\u5EFA\u6587\u6863\u5931\u8D25 (code=${createRes.code}): ${createRes.msg}`);
|
|
1381
|
+
}
|
|
1382
|
+
const documentId = createRes.data?.document?.document_id;
|
|
1383
|
+
if (!documentId) {
|
|
1384
|
+
throw new Error("\u521B\u5EFA\u6587\u6863\u6210\u529F\u4F46\u672A\u8FD4\u56DE document_id");
|
|
1385
|
+
}
|
|
1386
|
+
if (content.trim()) {
|
|
1387
|
+
const blocks = this.textToBlocks(content);
|
|
1388
|
+
const writeRes = await this.client.docx.documentBlockChildren.create({
|
|
1389
|
+
path: { document_id: documentId, block_id: documentId },
|
|
1390
|
+
data: { children: blocks, index: 0 }
|
|
1391
|
+
});
|
|
1392
|
+
if (writeRes.code !== 0) {
|
|
1393
|
+
logger.warn("\u6587\u6863\u5DF2\u521B\u5EFA\u4F46\u5199\u5165\u5185\u5BB9\u5931\u8D25", {
|
|
1394
|
+
documentId,
|
|
1395
|
+
code: writeRes.code,
|
|
1396
|
+
msg: writeRes.msg
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const url = `https://feishu.cn/docx/${documentId}`;
|
|
1401
|
+
logger.info("\u6210\u529F\u521B\u5EFA\u98DE\u4E66\u6587\u6863", { documentId, title });
|
|
1402
|
+
return success({ documentId, title, url });
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
const detail = extractHttpErrorDetail(error);
|
|
1405
|
+
logger.error("FeishuOpen: \u521B\u5EFA\u6587\u6863\u5931\u8D25", { ...detail, title });
|
|
1406
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1407
|
+
return failure(new Error(`FeishuOpen \u521B\u5EFA\u6587\u6863\u5931\u8D25: ${msg}`));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
async updateDocument(documentInput, content) {
|
|
1411
|
+
const parsed = parseFeishuDocumentId(documentInput);
|
|
1412
|
+
let { documentId } = parsed;
|
|
1413
|
+
try {
|
|
1414
|
+
if (parsed.isWiki) {
|
|
1415
|
+
const wiki = await this.resolveWikiToken(documentId);
|
|
1416
|
+
documentId = wiki.objToken;
|
|
1417
|
+
}
|
|
1418
|
+
const childrenRes = await this.client.docx.documentBlockChildren.get({
|
|
1419
|
+
path: { document_id: documentId, block_id: documentId },
|
|
1420
|
+
params: { page_size: 500 }
|
|
1421
|
+
});
|
|
1422
|
+
if (childrenRes.code !== 0) {
|
|
1423
|
+
throw new Error(`\u83B7\u53D6\u6587\u6863\u5B50\u5757\u5931\u8D25 (code=${childrenRes.code}): ${childrenRes.msg}`);
|
|
1424
|
+
}
|
|
1425
|
+
const items = childrenRes.data?.items ?? [];
|
|
1426
|
+
if (items.length > 0) {
|
|
1427
|
+
const deleteRes = await this.client.docx.documentBlockChildren.batchDelete({
|
|
1428
|
+
path: { document_id: documentId, block_id: documentId },
|
|
1429
|
+
data: { start_index: 0, end_index: items.length }
|
|
1430
|
+
});
|
|
1431
|
+
if (deleteRes.code !== 0) {
|
|
1432
|
+
throw new Error(`\u5220\u9664\u6587\u6863\u5B50\u5757\u5931\u8D25 (code=${deleteRes.code}): ${deleteRes.msg}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (content.trim()) {
|
|
1436
|
+
const blocks = this.textToBlocks(content);
|
|
1437
|
+
const writeRes = await this.client.docx.documentBlockChildren.create({
|
|
1438
|
+
path: { document_id: documentId, block_id: documentId },
|
|
1439
|
+
data: { children: blocks, index: 0 }
|
|
1440
|
+
});
|
|
1441
|
+
if (writeRes.code !== 0) {
|
|
1442
|
+
throw new Error(`\u5199\u5165\u6587\u6863\u5185\u5BB9\u5931\u8D25 (code=${writeRes.code}): ${writeRes.msg}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const url = `https://feishu.cn/docx/${documentId}`;
|
|
1446
|
+
logger.info("\u6210\u529F\u66F4\u65B0\u98DE\u4E66\u6587\u6863", { documentId });
|
|
1447
|
+
return success({ documentId, url });
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
const detail = extractHttpErrorDetail(error);
|
|
1450
|
+
logger.error(`FeishuOpen: \u66F4\u65B0\u6587\u6863 ${documentId} \u5931\u8D25`, detail);
|
|
1451
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1452
|
+
return failure(new Error(`FeishuOpen \u66F4\u65B0\u6587\u6863 ${documentId} \u5931\u8D25: ${msg}`));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async getDocument(documentInput) {
|
|
1456
|
+
const parsed = parseFeishuDocumentId(documentInput);
|
|
1457
|
+
let { documentId } = parsed;
|
|
1458
|
+
let { fileType } = parsed;
|
|
1459
|
+
logger.debug("\u89E3\u6790\u98DE\u4E66\u6587\u6863\u6807\u8BC6", { input: documentInput, documentId, fileType });
|
|
1460
|
+
try {
|
|
1461
|
+
if (parsed.isWiki) {
|
|
1462
|
+
const wiki = await this.resolveWikiToken(documentId);
|
|
1463
|
+
logger.debug("Wiki token \u5DF2\u89E3\u6790", { wikiToken: documentId, objToken: wiki.objToken, objType: wiki.objType });
|
|
1464
|
+
documentId = wiki.objToken;
|
|
1465
|
+
fileType = wiki.objType;
|
|
1466
|
+
}
|
|
1467
|
+
const content = await this.getDocumentContent(documentId);
|
|
1468
|
+
const comments = await this.getDocumentComments(documentId, fileType);
|
|
1469
|
+
return success({
|
|
1470
|
+
documentId,
|
|
1471
|
+
fileType,
|
|
1472
|
+
content,
|
|
1473
|
+
comments: comments.length > 0 ? comments : void 0
|
|
1474
|
+
});
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
const detail = extractHttpErrorDetail(error);
|
|
1477
|
+
logger.error(`FeishuOpen: \u83B7\u53D6\u6587\u6863 ${documentId} \u5931\u8D25`, detail);
|
|
1478
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1479
|
+
return failure(new Error(`FeishuOpen \u83B7\u53D6\u6587\u6863 ${documentId} \u5931\u8D25: ${msg}`));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
// src/services/platforms/JiraClient.ts
|
|
1485
|
+
var JiraClient = class extends BasePlatformClient {
|
|
1486
|
+
constructor(host, username, token) {
|
|
1487
|
+
super("Jira", {
|
|
1488
|
+
baseURL: host,
|
|
1489
|
+
auth: { type: "basic", username, password: token },
|
|
1490
|
+
timeout: 3e4
|
|
1491
|
+
});
|
|
1492
|
+
this.api.defaults.headers.common["Accept"] = "application/json";
|
|
1493
|
+
}
|
|
1494
|
+
async getIssue(issueKey) {
|
|
1495
|
+
return this.wrapApiCall(`\u83B7\u53D6\u4EFB\u52A1 ${issueKey}`, async () => {
|
|
1496
|
+
const url = `/rest/api/2/issue/${issueKey}?fields=summary,description,status,issuetype,comment`;
|
|
1497
|
+
const response = await this.api.get(url);
|
|
1498
|
+
return response.data;
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
function createJiraClient() {
|
|
1503
|
+
const { host, username, token } = config.jira;
|
|
1504
|
+
if (!host || !username || !token) return null;
|
|
1505
|
+
return new JiraClient(host, username, token);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/services/mcpService.ts
|
|
1509
|
+
function toToolResult(result) {
|
|
1510
|
+
if (!result.success) {
|
|
1511
|
+
return {
|
|
1512
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
1513
|
+
isError: true
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
const toolResult = {
|
|
1517
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1518
|
+
};
|
|
1519
|
+
if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
|
|
1520
|
+
toolResult.structuredContent = result.data;
|
|
1521
|
+
}
|
|
1522
|
+
return toolResult;
|
|
1523
|
+
}
|
|
1524
|
+
function truncateDiff(diffContent, maxSize, context) {
|
|
1525
|
+
const byteLength = Buffer.byteLength(diffContent, "utf-8");
|
|
1526
|
+
if (byteLength <= maxSize) {
|
|
1527
|
+
return diffContent;
|
|
1528
|
+
}
|
|
1529
|
+
const originalSizeMB = (byteLength / 1024 / 1024).toFixed(2);
|
|
1530
|
+
const maxSizeMB = (maxSize / 1024 / 1024).toFixed(2);
|
|
1531
|
+
logger.warn("Diff \u5185\u5BB9\u8FC7\u5927\uFF0C\u5DF2\u622A\u65AD", {
|
|
1532
|
+
project: context.project,
|
|
1533
|
+
repository: context.repository,
|
|
1534
|
+
prId: context.prId,
|
|
1535
|
+
originalSize: byteLength,
|
|
1536
|
+
maxSize,
|
|
1537
|
+
originalSizeMB,
|
|
1538
|
+
maxSizeMB
|
|
1539
|
+
});
|
|
1540
|
+
const buf = Buffer.from(diffContent, "utf-8");
|
|
1541
|
+
let cutoff = Math.min(maxSize, buf.length);
|
|
1542
|
+
const truncatedBuf = buf.subarray(0, cutoff);
|
|
1543
|
+
const lastNewline = truncatedBuf.lastIndexOf(10);
|
|
1544
|
+
if (lastNewline !== -1) {
|
|
1545
|
+
cutoff = lastNewline;
|
|
1546
|
+
} else {
|
|
1547
|
+
while (cutoff > 0 && (buf[cutoff] & 192) === 128) {
|
|
1548
|
+
cutoff--;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const truncatedContent = buf.subarray(0, cutoff).toString("utf-8");
|
|
1552
|
+
const warningMessage = `\u26A0\uFE0F Diff \u5185\u5BB9\u8FC7\u5927 (${originalSizeMB}MB)\uFF0C\u5DF2\u622A\u65AD\u81F3 ${maxSizeMB}MB
|
|
1553
|
+
\u5B8C\u6574 diff \u5171 ${byteLength.toLocaleString()} \u5B57\u8282\uFF0C\u5F53\u524D\u663E\u793A\u524D ${cutoff.toLocaleString()} \u5B57\u8282
|
|
1554
|
+
\u5EFA\u8BAE\uFF1A\u4F7F\u7528\u66F4\u5C0F\u7684 contextLines \u53C2\u6570\u6216\u76F4\u63A5\u5728 Bitbucket \u4E2D\u67E5\u770B
|
|
1555
|
+
|
|
1556
|
+
${"=".repeat(80)}
|
|
1557
|
+
|
|
1558
|
+
`;
|
|
1559
|
+
return `${warningMessage + truncatedContent}
|
|
1560
|
+
|
|
1561
|
+
${"=".repeat(80)}
|
|
1562
|
+
|
|
1563
|
+
\u26A0\uFE0F \u5185\u5BB9\u5DF2\u622A\u65AD\uFF0C\u5269\u4F59 ${(byteLength - cutoff).toLocaleString()} \u5B57\u8282\u672A\u663E\u793A
|
|
1564
|
+
`;
|
|
1565
|
+
}
|
|
1566
|
+
var McpService = class _McpService {
|
|
1567
|
+
bitbucketClient;
|
|
1568
|
+
jiraClient;
|
|
1569
|
+
confluenceClient;
|
|
1570
|
+
feishuClient;
|
|
1571
|
+
feishuOpenClient;
|
|
1572
|
+
constructor(bitbucketClient = null, jiraClient = null, confluenceClient = null, feishuClient = null, feishuOpenClient = null) {
|
|
1573
|
+
this.bitbucketClient = bitbucketClient;
|
|
1574
|
+
this.jiraClient = jiraClient;
|
|
1575
|
+
this.confluenceClient = confluenceClient;
|
|
1576
|
+
this.feishuClient = feishuClient;
|
|
1577
|
+
this.feishuOpenClient = feishuOpenClient;
|
|
1578
|
+
}
|
|
1579
|
+
requireClient(client, name) {
|
|
1580
|
+
if (!client) throw new Error(`${name}\u5BA2\u6237\u7AEF\u672A\u914D\u7F6E`);
|
|
1581
|
+
return client;
|
|
1582
|
+
}
|
|
1583
|
+
requireBitbucket() {
|
|
1584
|
+
return this.requireClient(this.bitbucketClient, "Bitbucket");
|
|
1585
|
+
}
|
|
1586
|
+
requireJira() {
|
|
1587
|
+
return this.requireClient(this.jiraClient, "Jira");
|
|
1588
|
+
}
|
|
1589
|
+
requireConfluence() {
|
|
1590
|
+
return this.requireClient(this.confluenceClient, "Confluence");
|
|
1591
|
+
}
|
|
1592
|
+
requireFeishu() {
|
|
1593
|
+
return this.requireClient(this.feishuClient, "\u98DE\u4E66");
|
|
1594
|
+
}
|
|
1595
|
+
requireFeishuOpen() {
|
|
1596
|
+
return this.requireClient(this.feishuOpenClient, "\u98DE\u4E66\u5F00\u653E\u5E73\u53F0");
|
|
1597
|
+
}
|
|
1598
|
+
static create() {
|
|
1599
|
+
const bitbucket = createBitbucketClient();
|
|
1600
|
+
const jira = createJiraClient();
|
|
1601
|
+
const confluence = createConfluenceClient();
|
|
1602
|
+
const feishu = createFeishuClient();
|
|
1603
|
+
const feishuOpen = config.feishuOpen.appId && config.feishuOpen.appSecret ? new FeishuOpenClient({ appId: config.feishuOpen.appId, appSecret: config.feishuOpen.appSecret }) : null;
|
|
1604
|
+
const platforms = {
|
|
1605
|
+
bitbucket: !!bitbucket,
|
|
1606
|
+
jira: !!jira,
|
|
1607
|
+
confluence: !!confluence,
|
|
1608
|
+
feishu: !!feishu,
|
|
1609
|
+
feishuOpen: !!feishuOpen
|
|
1610
|
+
};
|
|
1611
|
+
logger.info("\u5E73\u53F0\u5BA2\u6237\u7AEF\u521D\u59CB\u5316\u5B8C\u6210", platforms);
|
|
1612
|
+
return new _McpService(bitbucket, jira, confluence, feishu, feishuOpen);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* 获取 PR diff(含截断逻辑)
|
|
1616
|
+
* 保留在 McpService 中因为有独立的业务逻辑
|
|
1617
|
+
*/
|
|
1618
|
+
async getDiff(params, contextLines = 10, maxSize = config.mcp.maxDiffSize) {
|
|
1619
|
+
const client = this.requireBitbucket();
|
|
1620
|
+
const result = await client.getPullRequestDiff(params, contextLines);
|
|
1621
|
+
if (!result.success) {
|
|
1622
|
+
return toToolResult(result);
|
|
1623
|
+
}
|
|
1624
|
+
const diffContent = truncateDiff(result.data, maxSize, params);
|
|
1625
|
+
return {
|
|
1626
|
+
content: [{ type: "text", text: diffContent }]
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
var pullRequestSchema = z.object({
|
|
1631
|
+
id: z.number(),
|
|
1632
|
+
title: z.string(),
|
|
1633
|
+
description: z.string().optional(),
|
|
1634
|
+
state: z.string(),
|
|
1635
|
+
author: z.object({ displayName: z.string().optional() }).optional(),
|
|
1636
|
+
fromRef: z.object({ displayId: z.string().optional() }).optional(),
|
|
1637
|
+
toRef: z.object({ displayId: z.string().optional() }).optional()
|
|
1638
|
+
});
|
|
1639
|
+
var jiraIssueSchema = z.object({
|
|
1640
|
+
key: z.string(),
|
|
1641
|
+
fields: z.object({
|
|
1642
|
+
summary: z.string(),
|
|
1643
|
+
description: z.string().optional(),
|
|
1644
|
+
status: z.object({ name: z.string() }),
|
|
1645
|
+
issuetype: z.object({ name: z.string() }),
|
|
1646
|
+
comment: z.object({
|
|
1647
|
+
comments: z.array(
|
|
1648
|
+
z.object({
|
|
1649
|
+
body: z.string(),
|
|
1650
|
+
author: z.object({ displayName: z.string() }),
|
|
1651
|
+
created: z.string()
|
|
1652
|
+
})
|
|
1653
|
+
)
|
|
1654
|
+
}).optional()
|
|
1655
|
+
})
|
|
1656
|
+
});
|
|
1657
|
+
var confluencePageSchema = z.object({
|
|
1658
|
+
id: z.string(),
|
|
1659
|
+
type: z.string(),
|
|
1660
|
+
title: z.string(),
|
|
1661
|
+
space: z.object({ key: z.string().optional(), name: z.string().optional() }),
|
|
1662
|
+
version: z.object({
|
|
1663
|
+
number: z.number().optional(),
|
|
1664
|
+
when: z.string().optional(),
|
|
1665
|
+
by: z.string().optional()
|
|
1666
|
+
}),
|
|
1667
|
+
body: z.object({ storage: z.string().optional() }),
|
|
1668
|
+
history: z.object({
|
|
1669
|
+
createdBy: z.string().optional(),
|
|
1670
|
+
createdDate: z.string().optional()
|
|
1671
|
+
}),
|
|
1672
|
+
webUrl: z.string()
|
|
1673
|
+
});
|
|
1674
|
+
var confluenceChildPageSchema = z.object({
|
|
1675
|
+
id: z.string(),
|
|
1676
|
+
type: z.string(),
|
|
1677
|
+
title: z.string(),
|
|
1678
|
+
status: z.string(),
|
|
1679
|
+
space: z.object({ key: z.string().optional(), name: z.string().optional() }),
|
|
1680
|
+
version: z.object({
|
|
1681
|
+
number: z.number().optional(),
|
|
1682
|
+
when: z.string().optional(),
|
|
1683
|
+
by: z.string().optional()
|
|
1684
|
+
}),
|
|
1685
|
+
webUrl: z.string(),
|
|
1686
|
+
position: z.string().optional()
|
|
1687
|
+
});
|
|
1688
|
+
var confluenceChildPagesResultSchema = z.object({
|
|
1689
|
+
parentPageId: z.string(),
|
|
1690
|
+
totalChildren: z.number(),
|
|
1691
|
+
start: z.number(),
|
|
1692
|
+
limit: z.number(),
|
|
1693
|
+
children: z.array(confluenceChildPageSchema)
|
|
1694
|
+
});
|
|
1695
|
+
var confluenceCommentSchema = z.object({
|
|
1696
|
+
id: z.string(),
|
|
1697
|
+
body: z.string().optional(),
|
|
1698
|
+
author: z.string().optional(),
|
|
1699
|
+
createdAt: z.string().optional(),
|
|
1700
|
+
location: z.string().optional()
|
|
1701
|
+
});
|
|
1702
|
+
var confluenceCommentsResultSchema = z.object({
|
|
1703
|
+
pageId: z.string(),
|
|
1704
|
+
totalComments: z.number(),
|
|
1705
|
+
start: z.number(),
|
|
1706
|
+
limit: z.number(),
|
|
1707
|
+
comments: z.array(confluenceCommentSchema)
|
|
1708
|
+
});
|
|
1709
|
+
var feishuCommentSchema = z.object({
|
|
1710
|
+
id: z.string(),
|
|
1711
|
+
content: z.string(),
|
|
1712
|
+
workItemId: z.string(),
|
|
1713
|
+
workItemTypeKey: z.string(),
|
|
1714
|
+
createdAt: z.number(),
|
|
1715
|
+
operator: z.string()
|
|
1716
|
+
});
|
|
1717
|
+
var feishuWorkItemSchema = z.object({
|
|
1718
|
+
projectKey: z.string(),
|
|
1719
|
+
id: z.string(),
|
|
1720
|
+
name: z.string(),
|
|
1721
|
+
description: z.string().optional(),
|
|
1722
|
+
work_item_type_key: z.string(),
|
|
1723
|
+
work_item_type_name: z.string().optional(),
|
|
1724
|
+
comments: z.array(feishuCommentSchema).optional()
|
|
1725
|
+
});
|
|
1726
|
+
var feishuDocCommentSchema = z.object({
|
|
1727
|
+
commentId: z.string(),
|
|
1728
|
+
content: z.string(),
|
|
1729
|
+
userId: z.string(),
|
|
1730
|
+
createTime: z.number(),
|
|
1731
|
+
isSolved: z.boolean()
|
|
1732
|
+
});
|
|
1733
|
+
var feishuDocumentSchema = z.object({
|
|
1734
|
+
documentId: z.string(),
|
|
1735
|
+
fileType: z.string(),
|
|
1736
|
+
content: z.string(),
|
|
1737
|
+
comments: z.array(feishuDocCommentSchema).optional()
|
|
1738
|
+
});
|
|
1739
|
+
var feishuDocumentCreateResultSchema = z.object({
|
|
1740
|
+
documentId: z.string(),
|
|
1741
|
+
title: z.string(),
|
|
1742
|
+
url: z.string()
|
|
1743
|
+
});
|
|
1744
|
+
var feishuDocumentUpdateResultSchema = z.object({
|
|
1745
|
+
documentId: z.string(),
|
|
1746
|
+
url: z.string()
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// src/mcp/tools/types.ts
|
|
1750
|
+
function defineTool(name, config2, handler) {
|
|
1751
|
+
return { name, config: config2, handler };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/mcp/tools/bitbucket-tools.ts
|
|
1755
|
+
function defineBitbucketTools() {
|
|
1756
|
+
return [
|
|
1757
|
+
defineTool(
|
|
1758
|
+
"get_pull_request",
|
|
1759
|
+
{
|
|
1760
|
+
description: "Get pull request details from Bitbucket",
|
|
1761
|
+
inputSchema: z.object({
|
|
1762
|
+
project: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Bitbucket project key"),
|
|
1763
|
+
repository: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Repository slug"),
|
|
1764
|
+
prId: z.number().int().positive().describe("Pull request ID")
|
|
1765
|
+
}),
|
|
1766
|
+
outputSchema: pullRequestSchema
|
|
1767
|
+
},
|
|
1768
|
+
async (args, service) => {
|
|
1769
|
+
const client = service.requireBitbucket();
|
|
1770
|
+
return toToolResult(await client.getPullRequest(args));
|
|
1771
|
+
}
|
|
1772
|
+
),
|
|
1773
|
+
defineTool(
|
|
1774
|
+
"get_diff",
|
|
1775
|
+
{
|
|
1776
|
+
description: "Get pull request diff from Bitbucket",
|
|
1777
|
+
inputSchema: z.object({
|
|
1778
|
+
project: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Bitbucket project key"),
|
|
1779
|
+
repository: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Repository slug"),
|
|
1780
|
+
prId: z.number().int().positive().describe("Pull request ID"),
|
|
1781
|
+
contextLines: z.number().int().nonnegative().describe("Number of context lines").optional()
|
|
1782
|
+
})
|
|
1783
|
+
},
|
|
1784
|
+
async ({ project, repository, prId, contextLines }, service) => {
|
|
1785
|
+
return service.getDiff({ project, repository, prId }, contextLines);
|
|
1786
|
+
}
|
|
1787
|
+
)
|
|
1788
|
+
];
|
|
1789
|
+
}
|
|
1790
|
+
function defineConfluenceTools() {
|
|
1791
|
+
return [
|
|
1792
|
+
defineTool(
|
|
1793
|
+
"get_confluence_page",
|
|
1794
|
+
{
|
|
1795
|
+
description: "Get Confluence page content by page ID",
|
|
1796
|
+
inputSchema: z.object({
|
|
1797
|
+
pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID")
|
|
1798
|
+
}),
|
|
1799
|
+
outputSchema: confluencePageSchema
|
|
1800
|
+
},
|
|
1801
|
+
async ({ pageId }, service) => {
|
|
1802
|
+
const client = service.requireConfluence();
|
|
1803
|
+
return toToolResult(await client.getPage(pageId));
|
|
1804
|
+
}
|
|
1805
|
+
),
|
|
1806
|
+
defineTool(
|
|
1807
|
+
"get_confluence_child_pages",
|
|
1808
|
+
{
|
|
1809
|
+
description: "Get child pages of a Confluence page",
|
|
1810
|
+
inputSchema: z.object({
|
|
1811
|
+
pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID"),
|
|
1812
|
+
limit: z.number().int().positive().max(200).describe("Maximum number of child pages to return (default: 50)").optional(),
|
|
1813
|
+
expand: z.string().regex(/^[a-zA-Z.,]+$/).describe("Fields to expand (default: version,space)").optional()
|
|
1814
|
+
}),
|
|
1815
|
+
outputSchema: confluenceChildPagesResultSchema
|
|
1816
|
+
},
|
|
1817
|
+
async ({ pageId, limit, expand }, service) => {
|
|
1818
|
+
const client = service.requireConfluence();
|
|
1819
|
+
return toToolResult(await client.getChildPages(pageId, { limit, expand }));
|
|
1820
|
+
}
|
|
1821
|
+
),
|
|
1822
|
+
defineTool(
|
|
1823
|
+
"get_confluence_page_comments",
|
|
1824
|
+
{
|
|
1825
|
+
description: "Get comments on a Confluence page",
|
|
1826
|
+
inputSchema: z.object({
|
|
1827
|
+
pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID"),
|
|
1828
|
+
limit: z.number().int().positive().max(200).describe("Maximum number of comments to return (default: 50)").optional()
|
|
1829
|
+
}),
|
|
1830
|
+
outputSchema: confluenceCommentsResultSchema
|
|
1831
|
+
},
|
|
1832
|
+
async ({ pageId, limit }, service) => {
|
|
1833
|
+
const client = service.requireConfluence();
|
|
1834
|
+
return toToolResult(await client.getPageComments(pageId, { limit }));
|
|
1835
|
+
}
|
|
1836
|
+
)
|
|
1837
|
+
];
|
|
1838
|
+
}
|
|
1839
|
+
function defineFeishuOpenTools() {
|
|
1840
|
+
return [
|
|
1841
|
+
defineTool(
|
|
1842
|
+
"get_feishu_document",
|
|
1843
|
+
{
|
|
1844
|
+
description: "Get Feishu cloud document content and comments. Supports URL or document token.",
|
|
1845
|
+
inputSchema: z.object({
|
|
1846
|
+
documentId: z.string().describe(
|
|
1847
|
+
"Document identifier. Formats: (1) URL: https://xxx.feishu.cn/docx/ABC123DEF456; (2) Token: ABC123DEF456"
|
|
1848
|
+
)
|
|
1849
|
+
}),
|
|
1850
|
+
outputSchema: feishuDocumentSchema
|
|
1851
|
+
},
|
|
1852
|
+
async ({ documentId }, service) => {
|
|
1853
|
+
const client = service.requireFeishuOpen();
|
|
1854
|
+
return toToolResult(await client.getDocument(documentId));
|
|
1855
|
+
}
|
|
1856
|
+
),
|
|
1857
|
+
defineTool(
|
|
1858
|
+
"create_feishu_document",
|
|
1859
|
+
{
|
|
1860
|
+
description: "Create a new Feishu cloud document with title and content.",
|
|
1861
|
+
inputSchema: z.object({
|
|
1862
|
+
title: z.string().min(1).describe("Document title"),
|
|
1863
|
+
content: z.string().describe("Document body content (plain text, one paragraph per line)"),
|
|
1864
|
+
folderToken: z.string().describe("Folder token to create the document in (optional)").optional()
|
|
1865
|
+
}),
|
|
1866
|
+
outputSchema: feishuDocumentCreateResultSchema
|
|
1867
|
+
},
|
|
1868
|
+
async ({ title, content, folderToken }, service) => {
|
|
1869
|
+
const client = service.requireFeishuOpen();
|
|
1870
|
+
return toToolResult(await client.createDocument(title, content, folderToken));
|
|
1871
|
+
}
|
|
1872
|
+
),
|
|
1873
|
+
defineTool(
|
|
1874
|
+
"update_feishu_document",
|
|
1875
|
+
{
|
|
1876
|
+
description: "Update an existing Feishu cloud document content. Replaces all existing content with new content.",
|
|
1877
|
+
inputSchema: z.object({
|
|
1878
|
+
documentId: z.string().describe(
|
|
1879
|
+
"Document identifier. Formats: (1) URL: https://xxx.feishu.cn/docx/ABC123DEF456 or https://xxx.feishu.cn/wiki/ABC123; (2) Token: ABC123DEF456"
|
|
1880
|
+
),
|
|
1881
|
+
content: z.string().describe("New document body content (plain text, one paragraph per line). Replaces all existing content.")
|
|
1882
|
+
}),
|
|
1883
|
+
outputSchema: feishuDocumentUpdateResultSchema
|
|
1884
|
+
},
|
|
1885
|
+
async ({ documentId, content }, service) => {
|
|
1886
|
+
const client = service.requireFeishuOpen();
|
|
1887
|
+
return toToolResult(await client.updateDocument(documentId, content));
|
|
1888
|
+
}
|
|
1889
|
+
)
|
|
1890
|
+
];
|
|
1891
|
+
}
|
|
1892
|
+
function defineFeishuTools() {
|
|
1893
|
+
return [
|
|
1894
|
+
defineTool(
|
|
1895
|
+
"get_feishu_work_item",
|
|
1896
|
+
{
|
|
1897
|
+
description: "Get Feishu project work item details. Supports URL, prefixed ID, or pure number.",
|
|
1898
|
+
inputSchema: z.object({
|
|
1899
|
+
workItemId: z.string().describe(
|
|
1900
|
+
"Work item identifier. Formats: (1) URL: https://project.feishu.cn/example/issue/detail/12345678; (2) Prefixed: f-12345678, m-1234567890; (3) Number: 12345678"
|
|
1901
|
+
)
|
|
1902
|
+
}),
|
|
1903
|
+
outputSchema: feishuWorkItemSchema
|
|
1904
|
+
},
|
|
1905
|
+
async ({ workItemId }, service) => {
|
|
1906
|
+
const client = service.requireFeishu();
|
|
1907
|
+
return toToolResult(await client.getWorkItem(workItemId));
|
|
1908
|
+
}
|
|
1909
|
+
)
|
|
1910
|
+
];
|
|
1911
|
+
}
|
|
1912
|
+
function defineJiraTools() {
|
|
1913
|
+
return [
|
|
1914
|
+
defineTool(
|
|
1915
|
+
"get_jira_issue",
|
|
1916
|
+
{
|
|
1917
|
+
description: "Get Jira issue details",
|
|
1918
|
+
inputSchema: z.object({
|
|
1919
|
+
issueKey: z.string().min(1).regex(/^[A-Z][A-Z0-9_]+-\d+$/).describe("Jira issue key (e.g., PROJ-123)")
|
|
1920
|
+
}),
|
|
1921
|
+
outputSchema: jiraIssueSchema
|
|
1922
|
+
},
|
|
1923
|
+
async ({ issueKey }, service) => {
|
|
1924
|
+
const client = service.requireJira();
|
|
1925
|
+
return toToolResult(await client.getIssue(issueKey));
|
|
1926
|
+
}
|
|
1927
|
+
)
|
|
1928
|
+
];
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/mcp/tools/index.ts
|
|
1932
|
+
var cachedDefinitions = null;
|
|
1933
|
+
function collectToolDefinitions() {
|
|
1934
|
+
if (cachedDefinitions) return cachedDefinitions;
|
|
1935
|
+
const definitions = [];
|
|
1936
|
+
if (platformAvailability.bitbucket) {
|
|
1937
|
+
const tools = defineBitbucketTools();
|
|
1938
|
+
definitions.push(...tools);
|
|
1939
|
+
logger.debug("\u5DF2\u6CE8\u518C Bitbucket \u5DE5\u5177", { tools: tools.map((t) => t.name) });
|
|
1940
|
+
}
|
|
1941
|
+
if (platformAvailability.jira) {
|
|
1942
|
+
const tools = defineJiraTools();
|
|
1943
|
+
definitions.push(...tools);
|
|
1944
|
+
logger.debug("\u5DF2\u6CE8\u518C Jira \u5DE5\u5177", { tools: tools.map((t) => t.name) });
|
|
1945
|
+
}
|
|
1946
|
+
if (platformAvailability.confluence) {
|
|
1947
|
+
const tools = defineConfluenceTools();
|
|
1948
|
+
definitions.push(...tools);
|
|
1949
|
+
logger.debug("\u5DF2\u6CE8\u518C Confluence \u5DE5\u5177", { tools: tools.map((t) => t.name) });
|
|
1950
|
+
}
|
|
1951
|
+
if (platformAvailability.feishu) {
|
|
1952
|
+
const tools = defineFeishuTools();
|
|
1953
|
+
definitions.push(...tools);
|
|
1954
|
+
logger.debug("\u5DF2\u6CE8\u518C Feishu \u5DE5\u5177", { tools: tools.map((t) => t.name) });
|
|
1955
|
+
}
|
|
1956
|
+
if (platformAvailability.feishuOpen) {
|
|
1957
|
+
const tools = defineFeishuOpenTools();
|
|
1958
|
+
definitions.push(...tools);
|
|
1959
|
+
logger.debug("\u5DF2\u6CE8\u518C FeishuOpen \u5DE5\u5177", { tools: tools.map((t) => t.name) });
|
|
1960
|
+
}
|
|
1961
|
+
cachedDefinitions = definitions;
|
|
1962
|
+
return definitions;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/mcp/server-factory.ts
|
|
1966
|
+
async function safeToolCall(toolName, handler) {
|
|
1967
|
+
const startTime = Date.now();
|
|
1968
|
+
logger.debug("MCP \u5DE5\u5177\u8C03\u7528\u5F00\u59CB", { toolName });
|
|
1969
|
+
try {
|
|
1970
|
+
const result = await handler();
|
|
1971
|
+
const durationMs = Date.now() - startTime;
|
|
1972
|
+
logger.debug("MCP \u5DE5\u5177\u8C03\u7528\u5B8C\u6210", {
|
|
1973
|
+
toolName,
|
|
1974
|
+
durationMs,
|
|
1975
|
+
isError: result.isError ?? false
|
|
1976
|
+
});
|
|
1977
|
+
return result;
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
const durationMs = Date.now() - startTime;
|
|
1980
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1981
|
+
logger.error("MCP \u5DE5\u5177\u8C03\u7528\u5F02\u5E38", { toolName, durationMs, error: errorMessage });
|
|
1982
|
+
const safeMessage = errorMessage.includes("://") ? `${toolName} \u8C03\u7528\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u53C2\u6570\u6216\u7A0D\u540E\u91CD\u8BD5` : errorMessage;
|
|
1983
|
+
return {
|
|
1984
|
+
content: [{ type: "text", text: `Error: ${safeMessage}` }],
|
|
1985
|
+
isError: true
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
function createMcpServer(mcpService) {
|
|
1990
|
+
const service = mcpService ?? McpService.create();
|
|
1991
|
+
const server = new McpServer(
|
|
1992
|
+
{
|
|
1993
|
+
name: "@fineorg/mcp-server",
|
|
1994
|
+
version: config.version
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
capabilities: {
|
|
1998
|
+
tools: {}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
);
|
|
2002
|
+
for (const tool of collectToolDefinitions()) {
|
|
2003
|
+
server.registerTool(
|
|
2004
|
+
tool.name,
|
|
2005
|
+
tool.config,
|
|
2006
|
+
(args) => safeToolCall(tool.name, () => tool.handler(args, service))
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
return server;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// src/mcp/session-manager.ts
|
|
2013
|
+
var sharedService;
|
|
2014
|
+
function getSharedService() {
|
|
2015
|
+
if (!sharedService) sharedService = McpService.create();
|
|
2016
|
+
return sharedService;
|
|
2017
|
+
}
|
|
2018
|
+
function isSessionLimitReached(currentSize) {
|
|
2019
|
+
return currentSize >= config.mcp.maxSessions;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/mcp/startup.ts
|
|
2023
|
+
var SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2024
|
+
function bootstrapServer() {
|
|
2025
|
+
try {
|
|
2026
|
+
initLogger();
|
|
2027
|
+
validateConfig();
|
|
2028
|
+
logger.info("\u914D\u7F6E\u9A8C\u8BC1\u901A\u8FC7");
|
|
2029
|
+
validateMcpStartup();
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
logger.error("\u542F\u52A8\u9A8C\u8BC1\u5931\u8D25", {
|
|
2032
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2033
|
+
});
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function logStartupInfo(transport) {
|
|
2038
|
+
const authStatus = getMcpAuthStatus();
|
|
2039
|
+
const baseInfo = {
|
|
2040
|
+
authEnabled: authStatus.authEnabled,
|
|
2041
|
+
hasAllowedClients: authStatus.hasAllowedClients
|
|
2042
|
+
};
|
|
2043
|
+
{
|
|
2044
|
+
const transportConfig = config.mcp.sse ;
|
|
2045
|
+
baseInfo.host = transportConfig.host;
|
|
2046
|
+
baseInfo.port = transportConfig.port;
|
|
2047
|
+
}
|
|
2048
|
+
logger.info(`MCP Server (${transport}) \u542F\u52A8`, baseInfo);
|
|
2049
|
+
}
|
|
2050
|
+
function handleHealthCheck(res, transport) {
|
|
2051
|
+
const health = {
|
|
2052
|
+
status: "ok",
|
|
2053
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2054
|
+
transport
|
|
2055
|
+
};
|
|
2056
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2057
|
+
res.end(JSON.stringify(health));
|
|
2058
|
+
}
|
|
2059
|
+
function registerGracefulShutdown(httpServer, cleanup, transport) {
|
|
2060
|
+
const shutdown = async () => {
|
|
2061
|
+
logger.info(`\u6B63\u5728\u5173\u95ED MCP Server (${transport})`);
|
|
2062
|
+
await cleanup();
|
|
2063
|
+
httpServer.close((error) => {
|
|
2064
|
+
if (error) {
|
|
2065
|
+
logger.error("\u5173\u95ED HTTP \u670D\u52A1\u5668\u5931\u8D25", {
|
|
2066
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2067
|
+
});
|
|
2068
|
+
process.exit(1);
|
|
2069
|
+
}
|
|
2070
|
+
logger.info(`MCP Server (${transport}) \u5DF2\u5173\u95ED`);
|
|
2071
|
+
process.exit(0);
|
|
2072
|
+
});
|
|
2073
|
+
setTimeout(() => {
|
|
2074
|
+
logger.warn("\u5F3A\u5236\u9000\u51FA MCP Server");
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}, 1e4);
|
|
2077
|
+
};
|
|
2078
|
+
process.on("SIGINT", shutdown);
|
|
2079
|
+
process.on("SIGTERM", shutdown);
|
|
2080
|
+
}
|
|
2081
|
+
function startSessionCleanup(sessions2, transport) {
|
|
2082
|
+
return setInterval(async () => {
|
|
2083
|
+
const now = Date.now();
|
|
2084
|
+
let cleaned = 0;
|
|
2085
|
+
for (const [sessionId, session] of sessions2.entries()) {
|
|
2086
|
+
if (now - session.lastActivity > config.mcp.sessionTimeoutMs) {
|
|
2087
|
+
try {
|
|
2088
|
+
await session.close();
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
logger.error("\u6E05\u7406\u8FC7\u671F\u4F1A\u8BDD\u5931\u8D25", {
|
|
2091
|
+
sessionId,
|
|
2092
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
sessions2.delete(sessionId);
|
|
2096
|
+
cleaned++;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (cleaned > 0) {
|
|
2100
|
+
logger.info(`\u6E05\u7406\u8FC7\u671F ${transport} \u4F1A\u8BDD`, {
|
|
2101
|
+
cleaned,
|
|
2102
|
+
remaining: sessions2.size
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}, SESSION_CLEANUP_INTERVAL_MS);
|
|
2106
|
+
}
|
|
2107
|
+
function runMain(main) {
|
|
2108
|
+
bootstrapServer();
|
|
2109
|
+
main().catch((error) => {
|
|
2110
|
+
logger.error("MCP Server \u53D1\u751F\u672A\u6355\u83B7\u9519\u8BEF", { error });
|
|
2111
|
+
process.exit(1);
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/index-sse.ts
|
|
2116
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
2117
|
+
function handleSSEConnection(req, res) {
|
|
2118
|
+
if (!validateHttpAuth(req)) {
|
|
2119
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
2120
|
+
res.end("Unauthorized: Invalid or missing authentication token");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (isSessionLimitReached(sessions.size)) {
|
|
2124
|
+
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
2125
|
+
res.end("Too many sessions");
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const transport = new SSEServerTransport("/sse", res);
|
|
2129
|
+
const server = createMcpServer(getSharedService());
|
|
2130
|
+
sessions.set(transport.sessionId, {
|
|
2131
|
+
transport,
|
|
2132
|
+
server,
|
|
2133
|
+
lastActivity: Date.now(),
|
|
2134
|
+
close: () => server.close()
|
|
2135
|
+
});
|
|
2136
|
+
logger.info("SSE \u5BA2\u6237\u7AEF\u5DF2\u8FDE\u63A5", {
|
|
2137
|
+
sessionId: transport.sessionId,
|
|
2138
|
+
ip: req.socket.remoteAddress,
|
|
2139
|
+
totalSessions: sessions.size
|
|
2140
|
+
});
|
|
2141
|
+
server.connect(transport).catch((error) => {
|
|
2142
|
+
logger.error("SSE \u670D\u52A1\u5668\u8FDE\u63A5\u5931\u8D25", {
|
|
2143
|
+
sessionId: transport.sessionId,
|
|
2144
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2145
|
+
});
|
|
2146
|
+
});
|
|
2147
|
+
res.on("close", () => {
|
|
2148
|
+
sessions.delete(transport.sessionId);
|
|
2149
|
+
server.close().catch((error) => {
|
|
2150
|
+
logger.error("\u5173\u95ED\u670D\u52A1\u5668\u5931\u8D25", {
|
|
2151
|
+
sessionId: transport.sessionId,
|
|
2152
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2153
|
+
});
|
|
2154
|
+
});
|
|
2155
|
+
logger.info("SSE \u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00", {
|
|
2156
|
+
sessionId: transport.sessionId,
|
|
2157
|
+
totalSessions: sessions.size
|
|
2158
|
+
});
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
async function handlePostMessage(req, res, url) {
|
|
2162
|
+
if (!validateHttpAuth(req)) {
|
|
2163
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
2164
|
+
res.end("Unauthorized: Invalid or missing authentication token");
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
2168
|
+
if (!sessionId) {
|
|
2169
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2170
|
+
res.end("Bad Request: Missing sessionId parameter");
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
const session = sessions.get(sessionId);
|
|
2174
|
+
if (!session) {
|
|
2175
|
+
logger.warn("POST \u8BF7\u6C42\u7684\u4F1A\u8BDD\u4E0D\u5B58\u5728", { sessionId });
|
|
2176
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2177
|
+
res.end("Not Found: Session not found");
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
session.lastActivity = Date.now();
|
|
2181
|
+
logger.info("MCP \u8BF7\u6C42\u5DF2\u63A5\u6536", {
|
|
2182
|
+
sessionId,
|
|
2183
|
+
method: url.searchParams.get("method") || "unknown"
|
|
2184
|
+
});
|
|
2185
|
+
try {
|
|
2186
|
+
await session.transport.handlePostMessage(req, res);
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
logger.error("\u5904\u7406 POST \u6D88\u606F\u5931\u8D25", {
|
|
2189
|
+
sessionId,
|
|
2190
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2191
|
+
});
|
|
2192
|
+
if (!res.headersSent) {
|
|
2193
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2194
|
+
res.end("Internal Server Error");
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
runMain(async () => {
|
|
2199
|
+
logStartupInfo("SSE");
|
|
2200
|
+
const cleanupTimer2 = startSessionCleanup(sessions, "SSE");
|
|
2201
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
2202
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
2203
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
2204
|
+
const url = new URL(req.url, "http://localhost");
|
|
2205
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
2206
|
+
handleSSEConnection(req, res);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (req.method === "POST" && url.pathname === "/sse") {
|
|
2210
|
+
await handlePostMessage(req, res, url);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
2214
|
+
handleHealthCheck(res, "sse");
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2218
|
+
res.end("Not Found");
|
|
2219
|
+
});
|
|
2220
|
+
httpServer.listen(config.mcp.sse.port, config.mcp.sse.host, () => {
|
|
2221
|
+
logger.info("MCP Server (SSE) \u5DF2\u542F\u52A8", {
|
|
2222
|
+
url: `http://${config.mcp.sse.host}:${config.mcp.sse.port}`,
|
|
2223
|
+
sseEndpoint: "/sse",
|
|
2224
|
+
healthEndpoint: "/health"
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
registerGracefulShutdown(httpServer, async () => {
|
|
2228
|
+
clearInterval(cleanupTimer2);
|
|
2229
|
+
for (const [sessionId, session] of sessions.entries()) {
|
|
2230
|
+
try {
|
|
2231
|
+
await session.server.close();
|
|
2232
|
+
logger.debug("\u4F1A\u8BDD\u5DF2\u5173\u95ED", { sessionId });
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
logger.error("\u5173\u95ED\u4F1A\u8BDD\u5931\u8D25", {
|
|
2235
|
+
sessionId,
|
|
2236
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
sessions.clear();
|
|
2241
|
+
}, "SSE");
|
|
2242
|
+
});
|