diff --git a/packages/zentao-api/src/types.ts b/packages/zentao-api/src/types.ts index 3a26ea2..f6682fe 100644 --- a/packages/zentao-api/src/types.ts +++ b/packages/zentao-api/src/types.ts @@ -227,7 +227,6 @@ export interface ZentaoFileReadResult { fileID: number; fileType: string; mimeType: string; - encoding: "base64"; - data: string; + data: Buffer; size: number; } diff --git a/packages/zentao-api/src/zentao-legacy.ts b/packages/zentao-api/src/zentao-legacy.ts index 0ff582e..c4cc4fb 100644 --- a/packages/zentao-api/src/zentao-legacy.ts +++ b/packages/zentao-api/src/zentao-legacy.ts @@ -1455,7 +1455,7 @@ export default class ZentaoLegacy extends Zentao { } /** - * 读取附件或图片文件,并返回 base64 编码内容。 + * 读取附件或图片文件,并返回原始二进制内容。 * * @param params 读取参数,其中 `params.fileID` 为文件 ID,`params.fileType` 为文件扩展名 * @returns 文件读取结果 @@ -1474,8 +1474,7 @@ export default class ZentaoLegacy extends Zentao { fileID: params.fileID, fileType: params.fileType, mimeType: this.resolveFileMimeType(params.fileType), - encoding: "base64", - data: buffer.toString("base64"), + data: buffer, size: buffer.byteLength, } satisfies ZentaoFileReadResult, }; diff --git a/packages/zentao-api/test/zentao-legacy.test.ts b/packages/zentao-api/test/zentao-legacy.test.ts index 0ebf58e..a47eb70 100644 --- a/packages/zentao-api/test/zentao-legacy.test.ts +++ b/packages/zentao-api/test/zentao-legacy.test.ts @@ -677,7 +677,7 @@ describe("ZentaoLegacy", () => { expectNthRequest(3, "?m=doc&f=ajaxGetDoc&docID=101&version=0&t=json"); }); - it("readFile 应返回 base64 编码文件内容及 MIME 类型", async () => { + it("readFile 应返回 Buffer 文件内容及 MIME 类型", async () => { mockLoginSuccessOnce(); const raw = Uint8Array.from([72, 101, 108, 108, 111]).buffer; mockedAxios.request.mockResolvedValueOnce({ @@ -694,8 +694,7 @@ describe("ZentaoLegacy", () => { fileID: 9, fileType: "txt", mimeType: "text/plain", - encoding: "base64", - data: Buffer.from("Hello").toString("base64"), + data: Buffer.from("Hello"), size: 5, } satisfies ZentaoFileReadResult); expect(mockedAxios.request).toHaveBeenNthCalledWith( diff --git a/packages/zentao-mcp/src/index.ts b/packages/zentao-mcp/src/index.ts index 4d32b80..a41e8ba 100644 --- a/packages/zentao-mcp/src/index.ts +++ b/packages/zentao-mcp/src/index.ts @@ -45,6 +45,10 @@ import { ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; +import { randomUUID } from "node:crypto"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import dotenv from "dotenv"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; @@ -58,6 +62,8 @@ import { getString, type ZentaoCommandArgs, } from "./utils"; + +import type { ZentaoFileReadResult } from "@acehubert/zentao-api"; import type { ZentaoMcpOptions, BugType, @@ -82,6 +88,33 @@ export function getZentaoMcpOptions(args: ZentaoCommandArgs): ZentaoMcpOptions { }; } +function normalizeFileExtension(fileType: string): string { + const normalized = fileType.trim().toLowerCase().replace(/^\.+/, ""); + return normalized || "bin"; +} + +async function saveFileToTempDirectory(file: ZentaoFileReadResult): Promise<{ + fileID: number; + fileType: string; + mimeType: string; + size: number; + filePath: string; +}> { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zentao-mcp-")); + const fileName = `zentao-file-${file.fileID}-${randomUUID()}.${normalizeFileExtension(file.fileType)}`; + const filePath = path.join(tempDir, fileName); + + await fs.writeFile(filePath, file.data); + + return { + fileID: file.fileID, + fileType: file.fileType, + mimeType: file.mimeType, + size: file.size, + filePath, + }; +} + /** 命令参数优先,未传入时回退到环境变量。 */ function resolveZentaoMcpOptions(options: ZentaoMcpOptions): ZentaoMcpOptions { return { @@ -540,7 +573,7 @@ const tools: ZentaoTool[] = [ { name: "zentao_file", supportVersions: [], - description: "文件读取。支持:读取附件/图片内容,返回 base64 编码结果", + description: "文件读取。支持:读取附件/图片内容,保存到系统临时目录并返回文件路径", inputSchema: { type: "object", properties: { @@ -1127,6 +1160,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { libID, docID, moduleID, + fileID, + fileType, title, content, keywords, @@ -1141,6 +1176,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { libID?: number; docID?: number; moduleID?: number; + fileID?: number; + fileType?: string; title?: string; content?: string; keywords?: string; @@ -1270,6 +1307,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } break; + case "readFile": + if (!fileID) { + return { + content: [{ type: "text", text: "缺少必要参数: fileID(文件 ID)" }], + isError: true, + }; + } + if (!fileType) { + return { + content: [{ type: "text", text: "缺少必要参数: fileType(文件类型)" }], + isError: true, + }; + } + result = await saveFileToTempDirectory(await zentaoClient.readFile(fileID, fileType)); + break; + default: return { content: [{ type: "text", text: `未知操作类型: ${action}` }], @@ -1301,7 +1354,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { isError: true, }; } - result = await zentaoClient.readFile(fileID, fileType); + result = await saveFileToTempDirectory(await zentaoClient.readFile(fileID, fileType)); break; default: diff --git a/packages/zentao-mcp/src/types.ts b/packages/zentao-mcp/src/types.ts index c00e621..c475c54 100644 --- a/packages/zentao-mcp/src/types.ts +++ b/packages/zentao-mcp/src/types.ts @@ -1,3 +1,5 @@ +import type { ZentaoFileReadResult } from "@acehubert/zentao-api"; + export interface ZentaoClientBaseOptions { url?: string; account?: string; @@ -196,15 +198,6 @@ export interface ApiResponse { message?: string; } -export interface ZentaoFileReadResult { - fileID: number; - fileType: string; - mimeType: string; - encoding: "base64"; - data: string; - size: number; -} - export type TestCaseType = | "feature" | "performance" diff --git a/packages/zentao-mcp/src/zentao-clients/client-legacy.ts b/packages/zentao-mcp/src/zentao-clients/client-legacy.ts index d3e43b4..0fcb3fd 100644 --- a/packages/zentao-mcp/src/zentao-clients/client-legacy.ts +++ b/packages/zentao-mcp/src/zentao-clients/client-legacy.ts @@ -1,4 +1,6 @@ import { ZentaoLegacy } from "@acehubert/zentao-api"; + +import type { ZentaoFileReadResult } from "@acehubert/zentao-api"; import type { Bug, CloseBugParams, @@ -20,7 +22,6 @@ import type { TestCaseListResponse, User, ZentaoClientConfig, - ZentaoFileReadResult, ZentaoClientVersion, IZentaoClient, } from "../types"; diff --git a/packages/zentao-mcp/src/zentao-clients/client-v1.ts b/packages/zentao-mcp/src/zentao-clients/client-v1.ts index 4e6aa40..9f71f17 100644 --- a/packages/zentao-mcp/src/zentao-clients/client-v1.ts +++ b/packages/zentao-mcp/src/zentao-clients/client-v1.ts @@ -1,4 +1,7 @@ import { ZentaoV1 } from "@acehubert/zentao-api"; +import { ZentaoClientLegacy } from "./client-legacy"; + +import type { ZentaoFileReadResult } from "@acehubert/zentao-api"; import type { Bug, CloseBugParams, @@ -20,11 +23,9 @@ import type { TestCaseListResponse, User, ZentaoClientConfig, - ZentaoFileReadResult, ZentaoClientVersion, IZentaoClient, } from "../types"; -import { ZentaoClientLegacy } from "./client-legacy"; /** * MCP v1 客户端适配器。 diff --git a/packages/zentao-mcp/src/zentao-clients/client-v2.ts b/packages/zentao-mcp/src/zentao-clients/client-v2.ts index b40df54..205f42e 100644 --- a/packages/zentao-mcp/src/zentao-clients/client-v2.ts +++ b/packages/zentao-mcp/src/zentao-clients/client-v2.ts @@ -1,4 +1,6 @@ import { ZentaoV2 } from "@acehubert/zentao-api"; + +import type { ZentaoFileReadResult } from "@acehubert/zentao-api"; import type { Bug, CloseBugParams, @@ -20,7 +22,6 @@ import type { TestCaseListResponse, User, ZentaoClientConfig, - ZentaoFileReadResult, ZentaoClientVersion, IZentaoClient, } from "../types";