bfi-cli 0.1.106 → 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,7 +121,10 @@ var PmRemote = class {
97
121
  function messageOf(e) {
98
122
  return e instanceof Error ? e.message : String(e);
99
123
  }
100
- const DEFAULT_MAX_MB = 100;
124
+ function isSessionInvalid(e) {
125
+ return /session not found|session expired|\b404\b/i.test(messageOf(e));
126
+ }
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));
103
130
  }
@@ -124,24 +151,38 @@ async function resolveLocalFile(filePath, roots = resolveUploadRoots(), maxBytes
124
151
  size: info.size
125
152
  };
126
153
  }
127
- function absoluteUploadUrl(uploadUrl, origin = PM_ORIGIN) {
128
- return new URL(uploadUrl, origin).toString();
154
+ function absoluteUploadUrl(uploadUrl, origin = PM_ORIGIN, pmUrl = process.env.BFI_PM_URL || "https://bfi.yyuap.com/bfi-api/bficli/api/v1/mcp/mcp") {
155
+ const url = new URL(uploadUrl, origin);
156
+ try {
157
+ const base = new URL(pmUrl);
158
+ const idx = base.pathname.indexOf("/bficli/api/");
159
+ const prefix = idx > 0 ? base.pathname.slice(0, idx) : "";
160
+ if (prefix && url.pathname.startsWith("/bficli/") && !url.pathname.startsWith(prefix + "/")) url.pathname = prefix + url.pathname;
161
+ } catch {}
162
+ return url.toString();
129
163
  }
130
164
  async function uploadToTicket(url, file, fetchImpl = fetch) {
131
- const data = await readFile(file.realPath);
165
+ if (file.size === 0) throw new Error("文件为空,无法上传");
132
166
  const form = new FormData();
133
- 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);
134
170
  const resp = await fetchImpl(url, {
135
171
  method: "POST",
136
- body: form
172
+ body: form,
173
+ signal: AbortSignal.timeout(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 12e4)
137
174
  });
138
175
  const text = await resp.text();
139
176
  if (!resp.ok) throw new Error(`附件上传失败 HTTP ${resp.status}: ${text.slice(0, 200)}`);
177
+ let parsed;
140
178
  try {
141
- return JSON.parse(text);
179
+ parsed = JSON.parse(text);
142
180
  } catch {
143
181
  throw new Error(`附件上传响应非 JSON: ${text.slice(0, 200)}`);
144
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;
145
186
  }
146
187
  const LOCAL_TOOLS = [{
147
188
  name: "attachment_upload",
@@ -174,6 +215,15 @@ const LOCAL_TOOLS = [{
174
215
  type: "string",
175
216
  description: "评论正文"
176
217
  },
218
+ entity_type: {
219
+ type: "string",
220
+ enum: [
221
+ "DEFECT",
222
+ "FEATURE",
223
+ "SUBTASK"
224
+ ],
225
+ description: "单据类型,默认 DEFECT"
226
+ },
177
227
  file_paths: {
178
228
  type: "array",
179
229
  items: { type: "string" },
@@ -231,8 +281,9 @@ function buildServer(remote, fetchImpl) {
231
281
  const ticket = extractJson(await pm.callTool("get_upload_ticket", { file_name: (fileName || file.name).trim() }));
232
282
  const uploadUrl = String(ticket.upload_url || "");
233
283
  if (!uploadUrl) throw new PmError("ticket_failed", `签发 ticket 失败: ${JSON.stringify(ticket).slice(0, 200)}`);
234
- const uploaded = await uploadToTicket(absoluteUploadUrl(uploadUrl), file, fetchImpl);
235
- 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)}`);
236
287
  return {
237
288
  ...uploaded,
238
289
  fileName: uploaded.fileName || file.name,
@@ -249,17 +300,36 @@ function buildServer(remote, fetchImpl) {
249
300
  if (!input.object_id) throw new PmError("invalid_params", "object_id 不能为空");
250
301
  if (!paths.length) throw new PmError("invalid_params", "file_paths 不能为空");
251
302
  const uploads = [];
252
- 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
+ }
253
313
  const aids = uploads.map((u) => String(u.aid));
254
- return textResult({
255
- ok: true,
256
- uploads,
257
- comment: extractJson(await pm.callTool("add_comment", {
258
- object_id: String(input.object_id),
259
- content: input.content ? String(input.content) : "",
260
- attache_ids: aids
261
- }))
262
- });
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
+ }
263
333
  }
264
334
  return await pm.callTool(name, input);
265
335
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.106",
3
+ "version": "0.1.108",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {