Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/zentao-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ export interface ZentaoFileReadResult {
fileID: number;
fileType: string;
mimeType: string;
encoding: "base64";
data: string;
data: Buffer;
size: number;
}
5 changes: 2 additions & 3 deletions packages/zentao-api/src/zentao-legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ export default class ZentaoLegacy extends Zentao {
}

/**
* 读取附件或图片文件,并返回 base64 编码内容
* 读取附件或图片文件,并返回原始二进制内容
*
* @param params 读取参数,其中 `params.fileID` 为文件 ID,`params.fileType` 为文件扩展名
* @returns 文件读取结果
Expand All @@ -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,
};
Expand Down
5 changes: 2 additions & 3 deletions packages/zentao-api/test/zentao-legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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(
Expand Down
57 changes: 55 additions & 2 deletions packages/zentao-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -58,6 +62,8 @@ import {
getString,
type ZentaoCommandArgs,
} from "./utils";

import type { ZentaoFileReadResult } from "@acehubert/zentao-api";
import type {
ZentaoMcpOptions,
BugType,
Expand All @@ -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 {
Expand Down Expand Up @@ -540,7 +573,7 @@ const tools: ZentaoTool[] = [
{
name: "zentao_file",
supportVersions: [],
description: "文件读取。支持:读取附件/图片内容,返回 base64 编码结果",
description: "文件读取。支持:读取附件/图片内容,保存到系统临时目录并返回文件路径",
inputSchema: {
type: "object",
properties: {
Expand Down Expand Up @@ -1127,6 +1160,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
libID,
docID,
moduleID,
fileID,
fileType,
title,
content,
keywords,
Expand All @@ -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;
Expand Down Expand Up @@ -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}` }],
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 2 additions & 9 deletions packages/zentao-mcp/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ZentaoFileReadResult } from "@acehubert/zentao-api";

export interface ZentaoClientBaseOptions {
url?: string;
account?: string;
Expand Down Expand Up @@ -196,15 +198,6 @@ export interface ApiResponse<T = unknown> {
message?: string;
}

export interface ZentaoFileReadResult {
fileID: number;
fileType: string;
mimeType: string;
encoding: "base64";
data: string;
size: number;
}

export type TestCaseType =
| "feature"
| "performance"
Expand Down
3 changes: 2 additions & 1 deletion packages/zentao-mcp/src/zentao-clients/client-legacy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ZentaoLegacy } from "@acehubert/zentao-api";

import type { ZentaoFileReadResult } from "@acehubert/zentao-api";
import type {
Bug,
CloseBugParams,
Expand All @@ -20,7 +22,6 @@ import type {
TestCaseListResponse,
User,
ZentaoClientConfig,
ZentaoFileReadResult,
ZentaoClientVersion,
IZentaoClient,
} from "../types";
Expand Down
5 changes: 3 additions & 2 deletions packages/zentao-mcp/src/zentao-clients/client-v1.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,11 +23,9 @@ import type {
TestCaseListResponse,
User,
ZentaoClientConfig,
ZentaoFileReadResult,
ZentaoClientVersion,
IZentaoClient,
} from "../types";
import { ZentaoClientLegacy } from "./client-legacy";

/**
* MCP v1 客户端适配器。
Expand Down
3 changes: 2 additions & 1 deletion packages/zentao-mcp/src/zentao-clients/client-v2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ZentaoV2 } from "@acehubert/zentao-api";

import type { ZentaoFileReadResult } from "@acehubert/zentao-api";
import type {
Bug,
CloseBugParams,
Expand All @@ -20,7 +22,6 @@ import type {
TestCaseListResponse,
User,
ZentaoClientConfig,
ZentaoFileReadResult,
ZentaoClientVersion,
IZentaoClient,
} from "../types";
Expand Down