bfi-cli 0.1.107 → 0.1.108
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/dist/mcp-pm/main.mjs +88 -25
- package/package.json +1 -1
package/dist/mcp-pm/main.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { CallToolRequestSchema, CallToolResultSchema, ListToolsRequestSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
5
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
import { openAsBlob } from "node:fs";
|
|
6
7
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
const PM_ORIGIN = "https://bfi.yyuap.com";
|
|
@@ -42,27 +43,50 @@ var PmRemote = class {
|
|
|
42
43
|
}
|
|
43
44
|
async listTools() {
|
|
44
45
|
try {
|
|
45
|
-
return
|
|
46
|
+
return await this.listToolsOnce();
|
|
46
47
|
} catch (e) {
|
|
48
|
+
if (isSessionInvalid(e)) {
|
|
49
|
+
this.client = null;
|
|
50
|
+
try {
|
|
51
|
+
return await this.listToolsOnce();
|
|
52
|
+
} catch (e2) {
|
|
53
|
+
console.error(`[bfi-pm-mcp] 远端工具列表获取失败: ${messageOf(e2)}`);
|
|
54
|
+
this.client = null;
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
47
58
|
console.error(`[bfi-pm-mcp] 远端工具列表获取失败: ${messageOf(e)}`);
|
|
48
59
|
this.client = null;
|
|
49
60
|
return [];
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
async callTool(name, args) {
|
|
53
|
-
const client = await this.ensure();
|
|
54
64
|
try {
|
|
55
|
-
return await
|
|
56
|
-
method: "tools/call",
|
|
57
|
-
params: {
|
|
58
|
-
name,
|
|
59
|
-
arguments: args ?? {}
|
|
60
|
-
}
|
|
61
|
-
}, CallToolResultSchema);
|
|
65
|
+
return await this.callToolOnce(name, args);
|
|
62
66
|
} catch (e) {
|
|
67
|
+
if (isSessionInvalid(e)) {
|
|
68
|
+
this.client = null;
|
|
69
|
+
try {
|
|
70
|
+
return await this.callToolOnce(name, args);
|
|
71
|
+
} catch (e2) {
|
|
72
|
+
return this.handleRemoteFailure(e2);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
63
75
|
return this.handleRemoteFailure(e);
|
|
64
76
|
}
|
|
65
77
|
}
|
|
78
|
+
async listToolsOnce() {
|
|
79
|
+
return (await (await this.ensure()).request({ method: "tools/list" }, ListToolsResultSchema)).tools;
|
|
80
|
+
}
|
|
81
|
+
async callToolOnce(name, args) {
|
|
82
|
+
return (await this.ensure()).request({
|
|
83
|
+
method: "tools/call",
|
|
84
|
+
params: {
|
|
85
|
+
name,
|
|
86
|
+
arguments: args ?? {}
|
|
87
|
+
}
|
|
88
|
+
}, CallToolResultSchema);
|
|
89
|
+
}
|
|
66
90
|
async ensure() {
|
|
67
91
|
if (this.client) return this.client;
|
|
68
92
|
if (!this.credentials) throw new PmError("unauthorized", credentialHint());
|
|
@@ -97,6 +121,9 @@ var PmRemote = class {
|
|
|
97
121
|
function messageOf(e) {
|
|
98
122
|
return e instanceof Error ? e.message : String(e);
|
|
99
123
|
}
|
|
124
|
+
function isSessionInvalid(e) {
|
|
125
|
+
return /session not found|session expired|\b404\b/i.test(messageOf(e));
|
|
126
|
+
}
|
|
100
127
|
const DEFAULT_MAX_MB = 50;
|
|
101
128
|
function resolveUploadRoots() {
|
|
102
129
|
return [...(process.env.BFI_UPLOAD_ROOTS || "").split(path.delimiter).map((s) => s.trim()).filter(Boolean), process.cwd()].map((p) => path.resolve(p));
|
|
@@ -135,20 +162,27 @@ function absoluteUploadUrl(uploadUrl, origin = PM_ORIGIN, pmUrl = process.env.BF
|
|
|
135
162
|
return url.toString();
|
|
136
163
|
}
|
|
137
164
|
async function uploadToTicket(url, file, fetchImpl = fetch) {
|
|
138
|
-
|
|
165
|
+
if (file.size === 0) throw new Error("文件为空,无法上传");
|
|
139
166
|
const form = new FormData();
|
|
140
|
-
|
|
167
|
+
const blob = typeof openAsBlob === "function" ? await openAsBlob(file.realPath) : new Blob([new Uint8Array(await readFile(file.realPath))]);
|
|
168
|
+
form.append("file", blob, file.name);
|
|
169
|
+
const timeoutMs = Number(process.env.BFI_UPLOAD_TIMEOUT_MS || 12e4);
|
|
141
170
|
const resp = await fetchImpl(url, {
|
|
142
171
|
method: "POST",
|
|
143
|
-
body: form
|
|
172
|
+
body: form,
|
|
173
|
+
signal: AbortSignal.timeout(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 12e4)
|
|
144
174
|
});
|
|
145
175
|
const text = await resp.text();
|
|
146
176
|
if (!resp.ok) throw new Error(`附件上传失败 HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
177
|
+
let parsed;
|
|
147
178
|
try {
|
|
148
|
-
|
|
179
|
+
parsed = JSON.parse(text);
|
|
149
180
|
} catch {
|
|
150
181
|
throw new Error(`附件上传响应非 JSON: ${text.slice(0, 200)}`);
|
|
151
182
|
}
|
|
183
|
+
const code = parsed.code;
|
|
184
|
+
if (code !== void 0 && String(code) !== "0" && String(code) !== "200") throw new Error(`附件上传被拒绝(code=${String(code)}): ${String(parsed.msg || text.slice(0, 200))}`);
|
|
185
|
+
return parsed;
|
|
152
186
|
}
|
|
153
187
|
const LOCAL_TOOLS = [{
|
|
154
188
|
name: "attachment_upload",
|
|
@@ -181,6 +215,15 @@ const LOCAL_TOOLS = [{
|
|
|
181
215
|
type: "string",
|
|
182
216
|
description: "评论正文"
|
|
183
217
|
},
|
|
218
|
+
entity_type: {
|
|
219
|
+
type: "string",
|
|
220
|
+
enum: [
|
|
221
|
+
"DEFECT",
|
|
222
|
+
"FEATURE",
|
|
223
|
+
"SUBTASK"
|
|
224
|
+
],
|
|
225
|
+
description: "单据类型,默认 DEFECT"
|
|
226
|
+
},
|
|
184
227
|
file_paths: {
|
|
185
228
|
type: "array",
|
|
186
229
|
items: { type: "string" },
|
|
@@ -238,8 +281,9 @@ function buildServer(remote, fetchImpl) {
|
|
|
238
281
|
const ticket = extractJson(await pm.callTool("get_upload_ticket", { file_name: (fileName || file.name).trim() }));
|
|
239
282
|
const uploadUrl = String(ticket.upload_url || "");
|
|
240
283
|
if (!uploadUrl) throw new PmError("ticket_failed", `签发 ticket 失败: ${JSON.stringify(ticket).slice(0, 200)}`);
|
|
241
|
-
const
|
|
242
|
-
|
|
284
|
+
const response = await uploadToTicket(absoluteUploadUrl(uploadUrl), file, fetchImpl);
|
|
285
|
+
const uploaded = response.data && typeof response.data === "object" ? response.data : response;
|
|
286
|
+
if (!uploaded.aid) throw new PmError("upload_failed", `上传响应缺少 aid: ${JSON.stringify(response).slice(0, 200)}`);
|
|
243
287
|
return {
|
|
244
288
|
...uploaded,
|
|
245
289
|
fileName: uploaded.fileName || file.name,
|
|
@@ -256,17 +300,36 @@ function buildServer(remote, fetchImpl) {
|
|
|
256
300
|
if (!input.object_id) throw new PmError("invalid_params", "object_id 不能为空");
|
|
257
301
|
if (!paths.length) throw new PmError("invalid_params", "file_paths 不能为空");
|
|
258
302
|
const uploads = [];
|
|
259
|
-
for (const p of paths)
|
|
303
|
+
for (const p of paths) try {
|
|
304
|
+
uploads.push(await uploadOne(p));
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return textResult({
|
|
307
|
+
ok: false,
|
|
308
|
+
error_kind: "upload_failed",
|
|
309
|
+
message: `第 ${uploads.length + 1} 个文件上传失败: ${e instanceof Error ? e.message : String(e)}`,
|
|
310
|
+
uploaded: uploads
|
|
311
|
+
}, true);
|
|
312
|
+
}
|
|
260
313
|
const aids = uploads.map((u) => String(u.aid));
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
try {
|
|
315
|
+
return textResult({
|
|
316
|
+
ok: true,
|
|
317
|
+
uploads,
|
|
318
|
+
comment: extractJson(await pm.callTool("add_comment", {
|
|
319
|
+
object_id: String(input.object_id),
|
|
320
|
+
content: input.content ? String(input.content) : "",
|
|
321
|
+
entity_type: input.entity_type ? String(input.entity_type) : "DEFECT",
|
|
322
|
+
attache_ids: aids
|
|
323
|
+
}))
|
|
324
|
+
});
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return textResult({
|
|
327
|
+
ok: false,
|
|
328
|
+
error_kind: e instanceof PmError ? e.errorKind : "internal_error",
|
|
329
|
+
message: `附件已上传,但评论失败: ${e instanceof Error ? e.message : String(e)}`,
|
|
330
|
+
uploaded: uploads
|
|
331
|
+
}, true);
|
|
332
|
+
}
|
|
270
333
|
}
|
|
271
334
|
return await pm.callTool(name, input);
|
|
272
335
|
} catch (e) {
|