diff --git a/admin.html b/admin.html index 5bb3b0f..9c819c8 100644 --- a/admin.html +++ b/admin.html @@ -21,134 +21,288 @@ .section-card { margin-bottom: 16px; } + .top-nav { + position: sticky; + top: 0; + z-index: 1100; + background: #fff; + border-bottom: 1px solid #ebeef5; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + .top-nav-inner { + max-width: 1200px; + margin: 0 auto; + padding: 10px 16px; + } .actions { display: flex; gap: 8px; - flex-wrap: wrap; + } + @media (max-width: 1200px) { + .actions { + flex-wrap: wrap; + } } .small-text { color: #909399; font-size: 12px; } + .table-pagination { + margin-top: 12px; + text-align: right; + } -
- -
基础配置
- - - - - - - - - - - - - - - - - - - - - - - -
- - -
- 路由配置(routes) - 新增一行 +
+
+
+
+ 保存配置到 config.json + 重载服务内存配置 + 刷新页面数据 +
- +
+
+ + + +
+ 路由配置(routes) + 新增路由 +
+ + + + - + + + + +
- 说明:如果路由以 # 开头,会作为注释保留,不参与 mock 命中。 + 说明:关闭“启用”后会以 # 注释路由,不参与 mock 命中;切换后请点击“保存配置到 config.json”生效到文件。
-
+ +
- -
动态新增接口配置文件
- - + + +
+ Mock 数据配置 + 新增 Mock 文件 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + - - + - - + + + + + + + + + + + + + + + + 取消 + + {{ routeDialog.mode === "edit" ? "保存修改" : "创建路由+文件" }} + + + + + + - - - - 创建路由+文件 - - - -
- 保存配置到 config.json - 重载服务内存配置 - 刷新页面数据 -
-
+ + 取消 + 保存 Mock 文件 + +
+
@@ -158,6 +312,17 @@ el: "#app", data: function () { return { + activeMainTab: "routes", + apiList: [], + mockFiles: [], + routePagination: { + currentPage: 1, + pageSize: 10, + }, + mockFilePagination: { + currentPage: 1, + pageSize: 10, + }, form: { config: { mockEnabled: true, @@ -171,22 +336,78 @@ }, routes: [], }, - newRoute: { - template: "custom", - route: "", - filePath: "mock/", - fileContent: "", - overwrite: false, + routeDialog: { + visible: false, + mode: "create", + editingIndex: -1, + form: { + apiName: "", + selectedApiRoute: "", + originalRoute: "", + originalRawRoute: "", + route: "", + filePath: "", + enabled: true, + }, + }, + mockFileDialog: { + visible: false, + mode: "create", + form: { + filePath: "mock/", + content: "", + }, }, }; }, - created: function () { - this.loadConfig(); + created: async function () { + await this.loadApiList(); + await this.loadMockFiles(); + await this.loadConfig(); }, methods: { + getPreviewText: function (content) { + var text = String(content || "").replace(/\s+/g, " ").trim(); + if (!text) return "(空文件)"; + return text.length > 120 ? text.slice(0, 120) + "..." : text; + }, + getPagedData: function (list, pagination) { + var source = Array.isArray(list) ? list : []; + var pageSize = pagination.pageSize || 10; + var currentPage = pagination.currentPage || 1; + var maxPage = Math.max(1, Math.ceil(source.length / pageSize)); + if (currentPage > maxPage) { + currentPage = maxPage; + pagination.currentPage = currentPage; + } + var start = (currentPage - 1) * pageSize; + return source.slice(start, start + pageSize); + }, + handleRoutePageChange: function (page) { + this.routePagination.currentPage = page; + }, + handleMockFilePageChange: function (page) { + this.mockFilePagination.currentPage = page; + }, + findApiByRoute: function (route) { + return (this.apiList || []).find(function (item) { + return item.route === route; + }); + }, toRouteArray: function (routesObj) { - return Object.keys(routesObj || {}).map(function (route) { - return { route: route, filePath: routesObj[route] }; + var self = this; + return Object.keys(routesObj || {}).map(function (rawRoute) { + var enabled = !rawRoute.startsWith("#"); + var route = enabled ? rawRoute : rawRoute.replace(/^#+/, ""); + var matched = self.findApiByRoute(route); + return { + rawRoute: rawRoute, + route: route, + filePath: routesObj[rawRoute], + apiName: matched ? matched.name : "", + selectedApiRoute: matched ? matched.route : "", + enabled: enabled, + }; }); }, toRouteObject: function (routeArray) { @@ -195,16 +416,162 @@ var route = (item.route || "").trim(); var filePath = (item.filePath || "").trim(); if (route && filePath) { - obj[route] = filePath; + var routeKey = item.enabled === false ? "#" + route : route; + obj[routeKey] = filePath; } }); return obj; }, - addRouteRow: function () { - this.form.routes.push({ route: "", filePath: "" }); - }, removeRoute: function (index) { - this.form.routes.splice(index, 1); + var actualIndex = + (this.routePagination.currentPage - 1) * this.routePagination.pageSize + + index; + this.form.routes.splice(actualIndex, 1); + }, + getDefaultRouteForm: function () { + return { + apiName: "", + selectedApiRoute: "", + originalRoute: "", + originalRawRoute: "", + route: "", + filePath: "", + enabled: true, + }; + }, + isApiListLocked: function () { + return !!this.routeDialog.form.selectedApiRoute; + }, + onSelectApiRoute: function (route) { + var selected = route ? this.findApiByRoute(route) : null; + if (!selected) { + this.routeDialog.form.selectedApiRoute = ""; + return; + } + this.routeDialog.form.selectedApiRoute = selected.route; + this.routeDialog.form.route = selected.route; + this.routeDialog.form.apiName = selected.name; + }, + openRouteDialogForCreate: function () { + this.routeDialog.mode = "create"; + this.routeDialog.editingIndex = -1; + this.routeDialog.form = this.getDefaultRouteForm(); + this.routeDialog.visible = true; + }, + openRouteDialogForEdit: function (index) { + var actualIndex = + (this.routePagination.currentPage - 1) * this.routePagination.pageSize + + index; + var item = + this.form.routes[actualIndex] || { route: "", filePath: "mock/" }; + this.routeDialog.mode = "edit"; + this.routeDialog.editingIndex = actualIndex; + var matched = this.findApiByRoute(item.route || ""); + this.routeDialog.form = { + apiName: matched ? matched.name : item.apiName || "", + selectedApiRoute: matched ? matched.route : "", + originalRoute: item.route || "", + originalRawRoute: item.rawRoute || item.route || "", + route: item.route || "", + filePath: item.filePath || "mock/", + enabled: item.enabled !== false, + }; + this.routeDialog.visible = true; + }, + getDefaultMockFileForm: function () { + return { + filePath: "mock/", + content: "", + }; + }, + openMockFileDialogForCreate: function () { + this.mockFileDialog.mode = "create"; + this.mockFileDialog.form = this.getDefaultMockFileForm(); + this.mockFileDialog.visible = true; + }, + openMockFileDialogForEdit: function (item) { + this.mockFileDialog.mode = "edit"; + this.mockFileDialog.form = { + filePath: item.filePath || "mock/", + content: String(item.content || ""), + }; + this.mockFileDialog.visible = true; + }, + loadMockFiles: async function () { + try { + var resp = await fetch("/__mock-files"); + var data = await resp.json(); + if (!resp.ok || data.success === false) { + throw new Error(data.error || "加载 mock 文件失败"); + } + this.mockFiles = Array.isArray(data.list) ? data.list : []; + this.mockFilePagination.currentPage = 1; + } catch (err) { + this.mockFiles = []; + this.$message.error("加载 mock 文件失败: " + err.message); + } + }, + submitMockFileDialog: async function () { + if (!this.mockFileDialog.form.filePath) { + this.$message.error("Mock 文件路径不能为空"); + return; + } + try { + var resp = await fetch("/__mock-files", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + filePath: this.mockFileDialog.form.filePath, + content: this.mockFileDialog.form.content, + }), + }); + var data = await resp.json(); + if (!resp.ok || data.success === false) { + throw new Error(data.error || "保存 mock 文件失败"); + } + this.$message.success("Mock 文件已保存"); + this.mockFileDialog.visible = false; + await this.loadMockFiles(); + } catch (err) { + this.$message.error("保存 mock 文件失败: " + err.message); + } + }, + removeMockFile: async function (item) { + try { + await this.$confirm( + "确认删除 " + item.filePath + " 吗?删除后引用该文件的路由需要重新选择。", + "提示", + { type: "warning" }, + ); + var resp = await fetch("/__mock-files", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filePath: item.filePath }), + }); + var data = await resp.json(); + if (!resp.ok || data.success === false) { + throw new Error(data.error || "删除 mock 文件失败"); + } + this.$message.success("Mock 文件已删除"); + await this.loadMockFiles(); + } catch (err) { + if (err !== "cancel") { + this.$message.error("删除 mock 文件失败: " + err.message); + } + } + }, + loadApiList: async function () { + try { + var resp = await fetch("/__api-list"); + var data = await resp.json(); + if (!resp.ok || data.success === false) { + throw new Error(data.error || "加载接口列表失败"); + } + this.apiList = Array.isArray(data.list) ? data.list : []; + } catch (err) { + this.apiList = []; + this.$message.error("加载接口列表失败: " + err.message); + } }, loadConfig: async function () { try { @@ -212,6 +579,7 @@ var data = await resp.json(); this.form.config = Object.assign({}, this.form.config, data.config || {}); this.form.routes = this.toRouteArray(data.routes || {}); + this.routePagination.currentPage = 1; } catch (err) { this.$message.error("加载配置失败: " + err.message); } @@ -249,42 +617,47 @@ this.$message.error("重载失败: " + err.message); } }, - createRouteAndFile: async function () { + submitRouteDialog: async function () { + if (!this.routeDialog.form.route) { + this.$message.error("请求路径不能为空"); + return; + } + if (!this.routeDialog.form.filePath) { + this.$message.error("请选择 Mock 文件"); + return; + } + var payload = Object.assign({}, this.routeDialog.form, { + useExistingFile: true, + }); try { var resp = await fetch("/__routes", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify( - Object.assign({}, this.newRoute, { - useExistingFile: this.newRoute.template === "basicError", - }), - ), + body: JSON.stringify(payload), }); var data = await resp.json(); if (!resp.ok || data.success === false) { throw new Error(data.error || "创建失败"); } - this.$message.success("接口配置文件创建成功"); - this.newRoute.template = "custom"; - this.newRoute.route = ""; - this.newRoute.filePath = "mock/"; - this.newRoute.fileContent = ""; - this.newRoute.overwrite = false; - this.loadConfig(); + this.$message.success( + this.routeDialog.mode === "edit" + ? "路由修改成功" + : "接口配置文件创建成功", + ); + this.routeDialog.visible = false; + await this.loadMockFiles(); + await this.loadConfig(); } catch (err) { this.$message.error("创建失败: " + err.message); } }, - applyTemplate: function (template) { - if (template === "basicError") { - this.newRoute.filePath = "mock/basic-error.json"; - this.newRoute.fileContent = - '{"code": -1, "success": false, "msg":"失败"}'; - this.newRoute.overwrite = false; - return; - } - this.newRoute.filePath = "mock/"; - this.newRoute.fileContent = ""; + }, + computed: { + pagedRoutes: function () { + return this.getPagedData(this.form.routes, this.routePagination); + }, + pagedMockFiles: function () { + return this.getPagedData(this.mockFiles, this.mockFilePagination); }, }, }); diff --git a/index.api.ts b/index.api.ts index 539815d..9530dc1 100644 --- a/index.api.ts +++ b/index.api.ts @@ -6,6 +6,8 @@ import * as zlib from "zlib"; // 配置文件路径 const CONFIG_FILE = path.join(__dirname, "config.json"); +const API_LIST_FILE = path.join(__dirname, "mock", "api-list.json"); +const MOCK_DIR = path.join(__dirname, "mock"); // 定义类型 interface RouteConfig { @@ -31,6 +33,16 @@ interface ConfigFile { config: AppConfig; } +interface ApiListItem { + name: string; + route: string; +} + +interface MockFileItem { + filePath: string; + content: string; +} + // 存储当前的路由配置 let MOCK_ROUTES: RouteConfig = {}; let RAW_ROUTES: RouteConfig = {}; @@ -160,6 +172,93 @@ function saveConfigFile(nextConfig: ConfigFile): void { CONFIG = nextConfig.config || CONFIG; } +function loadApiList(): ApiListItem[] { + if (!fs.existsSync(API_LIST_FILE)) { + return []; + } + try { + const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim(); + if (!text) { + return []; + } + const parsed = JSON.parse(text); + if (!Array.isArray(parsed)) { + return []; + } + const seenRoutes = new Set(); + const list: ApiListItem[] = []; + for (const item of parsed) { + const route = String(item?.route || "").trim(); + const name = String(item?.name || "").trim(); + if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) { + continue; + } + seenRoutes.add(route); + list.push({ name, route }); + } + return list; + } catch (error) { + console.warn("[ADMIN] 读取 api-list.json 失败:", error); + return []; + } +} + +function normalizeMockFilePath(filePath: string): string { + const trimmed = filePath.trim().replace(/\\/g, "/"); + if (!trimmed) { + throw new Error("filePath is required"); + } + const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`; + if (path.isAbsolute(relative)) { + throw new Error("filePath must be a relative path"); + } + return relative; +} + +function resolveMockFullPath(relativePath: string): string { + const normalized = normalizeMockFilePath(relativePath); + const fullPath = path.resolve(__dirname, normalized); + const mockRoot = path.resolve(MOCK_DIR); + if (!fullPath.startsWith(mockRoot)) { + throw new Error("filePath is invalid"); + } + if (path.basename(fullPath) === "api-list.json") { + throw new Error("api-list.json is read-only in this panel"); + } + return fullPath; +} + +function walkMockFiles(dir: string, baseDir: string, result: string[]): void { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkMockFiles(fullPath, baseDir, result); + continue; + } + if (!entry.isFile()) continue; + if (entry.name === "api-list.json") continue; + const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/"); + result.push(`mock/${relative}`); + } +} + +function loadMockFiles(): MockFileItem[] { + if (!fs.existsSync(MOCK_DIR)) { + return []; + } + const files: string[] = []; + walkMockFiles(MOCK_DIR, MOCK_DIR, files); + files.sort((a, b) => a.localeCompare(b)); + return files.map((filePath) => { + const fullPath = path.join(__dirname, filePath); + return { + filePath, + content: fs.readFileSync(fullPath, "utf-8"), + }; + }); +} + function readBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; @@ -375,28 +474,48 @@ const proxyServer = http.createServer((clientReq, clientRes) => { readBody(clientReq) .then((bodyText) => { const body = bodyText ? JSON.parse(bodyText) : {}; - const route = String(body.route || "").trim(); + let route = String(body.route || "").trim(); const filePath = String(body.filePath || "").trim(); const fileContent = String(body.fileContent || ""); const overwrite = body.overwrite === true; const template = String(body.template || "").trim(); + const apiName = String(body.apiName || "").trim(); + const selectedApiRoute = String(body.selectedApiRoute || "").trim(); + const originalRoute = String(body.originalRoute || "").trim(); + const originalRawRoute = String(body.originalRawRoute || "").trim(); + const enabled = body.enabled !== false; const useExistingFile = body.useExistingFile === true || template === "basicError"; + const apiList = loadApiList(); + const selectedApi = selectedApiRoute + ? apiList.find((item) => item.route === selectedApiRoute) + : undefined; + const originalApi = originalRoute + ? apiList.find((item) => item.route === originalRoute) + : undefined; + + // 来自 api-list 的接口路由不可在新增/修改时变更 + if (selectedApi) { + if (route && route !== selectedApi.route) { + throw new Error("api-list route cannot be changed"); + } + if (apiName && apiName !== selectedApi.name) { + throw new Error("api-list name cannot be changed"); + } + route = selectedApi.route; + } + if (originalApi && route !== originalApi.route) { + throw new Error("api-list route cannot be changed"); + } + if (originalApi && apiName && apiName !== originalApi.name) { + throw new Error("api-list name cannot be changed"); + } if (!route.startsWith("/")) { throw new Error("route must start with '/'"); } - if (!filePath) { - throw new Error("filePath is required"); - } - if (path.isAbsolute(filePath)) { - throw new Error("filePath must be a relative path"); - } - - const fullPath = path.join(__dirname, filePath); - if (!fullPath.startsWith(__dirname)) { - throw new Error("filePath is invalid"); - } + const normalizedFilePath = normalizeMockFilePath(filePath); + const fullPath = resolveMockFullPath(normalizedFilePath); if (useExistingFile) { if (!fs.existsSync(fullPath)) { throw new Error("mock file does not exist"); @@ -412,7 +531,21 @@ const proxyServer = http.createServer((clientReq, clientRes) => { } const nextConfig = buildCurrentConfigFile(); - nextConfig.routes[route] = filePath; + const nextRouteKey = enabled ? route : `#${route}`; + const oldRouteCandidates = new Set(); + if (originalRawRoute) { + oldRouteCandidates.add(originalRawRoute); + } + if (originalRoute) { + oldRouteCandidates.add(originalRoute); + oldRouteCandidates.add(`#${originalRoute}`); + } + oldRouteCandidates.forEach((key) => { + if (key && key !== nextRouteKey) { + delete nextConfig.routes[key]; + } + }); + nextConfig.routes[nextRouteKey] = normalizedFilePath; saveConfigFile(nextConfig); clientRes.writeHead(200, { "Content-Type": "application/json" }); @@ -421,7 +554,111 @@ const proxyServer = http.createServer((clientReq, clientRes) => { success: true, message: "Route and mock file created successfully", route, - filePath, + routeKey: nextRouteKey, + filePath: normalizedFilePath, + enabled, + }), + ); + }) + .catch((error) => { + clientRes.writeHead(400, { "Content-Type": "application/json" }); + clientRes.end( + JSON.stringify({ + success: false, + error: (error as Error).message, + }), + ); + }); + return; + } + + if (requestPath === "/__api-list") { + if (clientReq.method !== "GET") { + clientRes.writeHead(405, { + "Content-Type": "application/json", + Allow: "GET", + }); + clientRes.end( + JSON.stringify({ + success: false, + error: "Method Not Allowed", + allow: ["GET"], + }), + ); + return; + } + clientRes.writeHead(200, { "Content-Type": "application/json" }); + clientRes.end( + JSON.stringify({ + success: true, + list: loadApiList(), + }), + ); + return; + } + + if (requestPath === "/__mock-files") { + if ( + clientReq.method !== "GET" && + clientReq.method !== "POST" && + clientReq.method !== "DELETE" + ) { + clientRes.writeHead(405, { + "Content-Type": "application/json", + Allow: "GET, POST, DELETE", + }); + clientRes.end( + JSON.stringify({ + success: false, + error: "Method Not Allowed", + allow: ["GET", "POST", "DELETE"], + }), + ); + return; + } + + if (clientReq.method === "GET") { + clientRes.writeHead(200, { "Content-Type": "application/json" }); + clientRes.end( + JSON.stringify({ + success: true, + list: loadMockFiles(), + }), + ); + return; + } + + readBody(clientReq) + .then((bodyText) => { + const body = bodyText ? JSON.parse(bodyText) : {}; + const filePath = String(body.filePath || "").trim(); + const fullPath = resolveMockFullPath(filePath); + const normalizedPath = normalizeMockFilePath(filePath); + + if (clientReq.method === "POST") { + const content = String(body.content || ""); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content, "utf-8"); + clientRes.writeHead(200, { "Content-Type": "application/json" }); + clientRes.end( + JSON.stringify({ + success: true, + filePath: normalizedPath, + }), + ); + return; + } + + // DELETE + if (!fs.existsSync(fullPath)) { + throw new Error("mock file does not exist"); + } + fs.unlinkSync(fullPath); + clientRes.writeHead(200, { "Content-Type": "application/json" }); + clientRes.end( + JSON.stringify({ + success: true, + filePath: normalizedPath, }), ); }) diff --git a/mock/api-list.json b/mock/api-list.json new file mode 100644 index 0000000..5581cdb --- /dev/null +++ b/mock/api-list.json @@ -0,0 +1,60 @@ +[ + { "name": "登录/注册发送验证码", "route": "/api1/authentication/sms/send" }, + { "name": "绑定手机号发送验证码", "route": "/api1/authentication/bind/send" }, + { "name": "账号注销", "route": "/api1/account/revoke" }, + { "name": "绑定手机号", "route": "/api1/account/bind" }, + { "name": "认证授权刷新接口", "route": "/api1/oauth2/token" }, + { "name": "一键登录阿里云授权码", "route": "/api1/authentication/common" }, + + { "name": "更新用户信息", "route": "/api2/user/update" }, + { "name": "获取用户信息", "route": "/api2/user" }, + { "name": "我的链接", "route": "/api2/link" }, + { "name": "获取字典类型", "route": "/api2/dict/types" }, + { "name": "获取字典项", "route": "/api2/dict/" }, + { "name": "获取设备列表", "route": "/api2/device/list" }, + { "name": "根据类型获取设备列表", "route": "/api2/device/type/list" }, + { "name": "查询历史设备列表", "route": "/api2/device/history/list" }, + { "name": "绑定设备", "route": "/api2/device/save" }, + { "name": "切换设备", "route": "/api2/device/switch" }, + { "name": "用机人主设备", "route": "/api2/device/master" }, + { "name": "解绑设备", "route": "/api2/device/unbind" }, + { "name": "获取设备信息", "route": "/api2/device/one" }, + { "name": "获取设备店铺", "route": "/api2/device/store" }, + { "name": "获取设备图片", "route": "/api2/device/bind/img" }, + { "name": "查询设备报告日期", "route": "/api2/device/report/date" }, + { "name": "查询是否有报告页", "route": "/api2/device/reports" }, + + { "name": "查询使用教程列表", "route": "/api2/course/list" }, + { "name": "查询白脸教程列表", "route": "/api2/course/white/face/list" }, + { "name": "获取省份城市列表", "route": "/api2/sys/provinces" }, + { "name": "制氧机报告单天", "route": "/api2/oxygenerator/report/day" }, + { "name": "制氧机报告多天", "route": "/api2/oxygenerator/report/multi/day" }, + { "name": "血氧仪报告单天", "route": "/api2/oximeter/report/day" }, + { "name": "血氧仪报告多天", "route": "/api2/oximeter/report/multi/day" }, + { "name": "呼吸机报告单天", "route": "/api2/ventilator/report/day" }, + { "name": "呼吸机报告多天", "route": "/api2/ventilator/report/multi/day" }, + { "name": "血氧仪健康报告单天", "route": "/api2/oximeter/health/report/day" }, + { "name": "血氧仪健康报告多天", "route": "/api2/oximeter/health/report/multi/day" }, + { "name": "血氧仪报告详情", "route": "/api2/oximeter/report/detail" }, + { "name": "首页判断", "route": "/api2/home/flag" }, + { "name": "首页Banner", "route": "/api2/home/ad/list" }, + { "name": "首页卡片信息", "route": "/api2/home/card/info" }, + { "name": "修改首页卡片", "route": "/api2/home/card/update" }, + { "name": "协议列表", "route": "/api2/protocol/list" }, + { "name": "同意协议", "route": "/api2/protocol/agree" }, + { "name": "协议是否更新", "route": "/api2/protocol/has/update" }, + + { "name": "扫码获取报告", "route": "/api2/qr/scan" }, + { "name": "查询QR报告详情", "route": "/api2/qr/one" }, + { "name": "查询QR报告列表", "route": "/api2/qr/page" }, + { "name": "获取报告数量", "route": "/api2/qr/count" }, + + { "name": "检查健康自测人数", "route": "/api2/self/check/head/count" }, + { "name": "健康自测结果", "route": "/api2/self/check/save" }, + + { "name": "科普分类列表", "route": "/api2/health/category/list" }, + { "name": "科普列表", "route": "/api2/health/service/list" }, + { "name": "科普详情", "route": "/api2/health/service/getById" }, + + { "name": "deepSeek流式接口", "route": "/api2/deepSeek/stream" } +]