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.
@@ -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 (await (await this.ensure()).request({ method: "tools/list" }, ListToolsResultSchema)).tools;
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 client.request({
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
- const data = await readFile(file.realPath);
165
+ if (file.size === 0) throw new Error("文件为空,无法上传");
139
166
  const form = new FormData();
140
- form.append("file", new Blob([new Uint8Array(data)]), file.name);
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
- return JSON.parse(text);
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 uploaded = await uploadToTicket(absoluteUploadUrl(uploadUrl), file, fetchImpl);
242
- if (!uploaded.aid) throw new PmError("upload_failed", `上传响应缺少 aid: ${JSON.stringify(uploaded).slice(0, 200)}`);
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) uploads.push(await uploadOne(p));
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
- return textResult({
262
- ok: true,
263
- uploads,
264
- comment: extractJson(await pm.callTool("add_comment", {
265
- object_id: String(input.object_id),
266
- content: input.content ? String(input.content) : "",
267
- attache_ids: aids
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.107",
3
+ "version": "0.1.108",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {