From 6330b99860bb3fb2a5e6c4631050454f810a93e1 Mon Sep 17 00:00:00 2001 From: Epifnne Date: Fri, 31 Jul 2026 14:19:48 +0800 Subject: [PATCH] remove /api support --- .../test/model-handlers.integration.test.js | 2 +- README.md | 4 +- ReinLoop/api.py | 2 +- ReinLoop/features.md | 8 +- ReinLoop/license_utils.py | 2 +- ReinLoop/tests/test_data_collector.py | 2 +- ReinLoop/tests/test_device_heartbeat.py | 4 +- server/features.md | 4 +- server/src/app.js | 1 - server/src/server.js | 100 +++++++++--------- server/test/server.test.js | 13 ++- 11 files changed, 75 insertions(+), 67 deletions(-) diff --git a/ControlPanel/test/model-handlers.integration.test.js b/ControlPanel/test/model-handlers.integration.test.js index 7c1a4fb..291eb23 100644 --- a/ControlPanel/test/model-handlers.integration.test.js +++ b/ControlPanel/test/model-handlers.integration.test.js @@ -22,7 +22,7 @@ test("panel model kernel renames, overwrites, lists both names, and deletes on t }); const credentials = { - apiUrl: `http://127.0.0.1:${server.address().port}/api`, + apiUrl: `http://127.0.0.1:${server.address().port}`, adminToken: "test-token" }; const handlers = createModelHandlers({ callServer }); diff --git a/README.md b/README.md index 8d05f44..8b51561 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ $env:B_ADMIN_TOKEN="your-admin-token" npm start ``` -默认服务地址为 `http://127.0.0.1:3000`,健康检查为 `http://127.0.0.1:3000/health`。业务请求可使用根路径或 `/api`。跨设备部署时,应使用服务器局域网 IP 或 HTTPS 域名,而不是 `127.0.0.1`。 +默认服务地址为 `http://127.0.0.1:3000`,健康检查为 `http://127.0.0.1:3000/health`。业务请求统一使用根路径。跨设备部署时,应使用服务器局域网 IP 或 HTTPS 域名,而不是 `127.0.0.1`。 ### ControlPanel ```powershell cd ControlPanel npm install -$env:REINLOOP_API_URL="http://server-address:3000/api" +$env:REINLOOP_API_URL="http://server-address:3000" $env:B_ADMIN_TOKEN="your-admin-token" npm run gui ``` diff --git a/ReinLoop/api.py b/ReinLoop/api.py index 097c4ba..b10c257 100644 --- a/ReinLoop/api.py +++ b/ReinLoop/api.py @@ -11,7 +11,7 @@ base_url = os.environ.get( ).rstrip("/") server_api_url = os.environ.get( "REINLOOP_API_URL", - f"{base_url}/api", + base_url, ) # Compatibility alias used by existing modules. It points to the ReinLoop # Express server API, not a cloud-function endpoint. diff --git a/ReinLoop/features.md b/ReinLoop/features.md index f0d00e6..e63ee84 100644 --- a/ReinLoop/features.md +++ b/ReinLoop/features.md @@ -5,11 +5,11 @@ ## 约定 - 业务服务端地址由 `REINLOOP_API_URL` 指定,未设置时使用 - `REINLOOP_SERVER_URL + /api`;默认地址为 - `https://ReinLoop.dominatedconvergence.com/api`。 + `REINLOOP_SERVER_URL`;默认地址为 + `https://ReinLoop.dominatedconvergence.com`。 - 新许可证的设备标识为 `/`,在代码中通过 `api.the_folder` 使用。旧许可证才回退到 `REINLOOP_DEVICE_ID`。 -- 所有服务端业务请求均使用 `POST /api`,通过请求体的 `type` 字段分发。 +- 所有服务端业务请求均使用 `POST /`,通过请求体的 `type` 字段分发。 - 公司、产线、许可证签发/撤销、模型删除、审核反馈及配置提交均为 ControlPanel 管理端能力,客户端不提供对应的管理接口。旧微信云函数和其管理脚本 已移除。 @@ -148,7 +148,7 @@ RL 模式下,`ControlEngine` 使用模型根据流量、当前压力和压力 ## 客户端服务端协议 -所有业务请求都发送至 `POST /api`。业务成功响应应至少包含 `success: true`。 +所有业务请求都发送至 `POST /`。业务成功响应应至少包含 `success: true`。 | `type` | 请求关键字段 | 用途 | | --- | --- | --- | diff --git a/ReinLoop/license_utils.py b/ReinLoop/license_utils.py index df7e0d8..ceee6d5 100644 --- a/ReinLoop/license_utils.py +++ b/ReinLoop/license_utils.py @@ -276,7 +276,7 @@ def _api_url(): base_url = os.environ.get( "REINLOOP_SERVER_URL", "https://ReinLoop.dominatedconvergence.com" ).rstrip("/") - return os.environ.get("REINLOOP_API_URL", f"{base_url}/api") + return os.environ.get("REINLOOP_API_URL", base_url) def _offline_limit_seconds(): diff --git a/ReinLoop/tests/test_data_collector.py b/ReinLoop/tests/test_data_collector.py index f6156b1..2451701 100644 --- a/ReinLoop/tests/test_data_collector.py +++ b/ReinLoop/tests/test_data_collector.py @@ -14,7 +14,7 @@ MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "data_collector.py" def load_data_collector_module(): api = types.ModuleType("api") api.base_url = "https://cloud.example" - api.data_record_url = "https://cloud.example/api" + api.data_record_url = "https://cloud.example" api.the_folder = "customer-a/line-1" requests = types.ModuleType("requests") diff --git a/ReinLoop/tests/test_device_heartbeat.py b/ReinLoop/tests/test_device_heartbeat.py index ed8687d..66588a1 100644 --- a/ReinLoop/tests/test_device_heartbeat.py +++ b/ReinLoop/tests/test_device_heartbeat.py @@ -30,12 +30,12 @@ class DeviceHeartbeatTests(unittest.TestCase): requests_module.post = post api_module = types.ModuleType("api") - api_module.data_record_url = "https://server.example/api" + api_module.data_record_url = "https://server.example" api_module.the_folder = "company/line" with patch.dict(sys.modules, {"requests": requests_module, "api": api_module}): timestamp = HEARTBEAT.heartbeat_device(timeout=7) self.assertEqual(timestamp, "2026-07-28T00:00:00.000Z") - self.assertEqual(calls, [("https://server.example/api", { + self.assertEqual(calls, [("https://server.example", { "type": "deviceHeartbeat", "deviceId": "company/line" }, 7)]) \ No newline at end of file diff --git a/server/features.md b/server/features.md index 15413c8..af24b53 100644 --- a/server/features.md +++ b/server/features.md @@ -2,7 +2,7 @@ ## 通用约定 -业务接口为 `POST /api`。请求与响应均为 JSON,响应包含 `success`。 +业务接口为 `POST /`。请求与响应均为 JSON,响应包含 `success`。 标注为 Admin 的接口需要附加: @@ -16,7 +16,7 @@ | 方法 | 路径 | 用途 | | --- | --- | --- | -| `POST` | `/api` | 主业务 API | +| `POST` | `/` | 主业务 API | | `POST` | `/upload/:token` | 根据上传凭证接收 multipart 文件 | | `GET` | `/files/:fileID` | 下载已存储文件 | | `GET` | `/health` | 服务存活检查 | diff --git a/server/src/app.js b/server/src/app.js index c5581db..68b8b79 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1111,7 +1111,6 @@ function createApp({ } }; app.post("/", apiHandler); - app.post("/api", apiHandler); app.use((error, req, res, next) => { console.error(error); diff --git a/server/src/server.js b/server/src/server.js index fd35523..ec53a33 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,51 +1,51 @@ -const path = require("node:path"); -const fs = require("node:fs"); -const { createApp } = require("./app"); -const { JsonStore } = require("./store"); -const { PostgresStore } = require("./postgres-store"); - -const host = process.env.HOST || "127.0.0.1"; -const port = Number(process.env.PORT || 3000); -const identificationPurgeIntervalMs = Number(process.env.IDENTIFICATION_PURGE_INTERVAL_MS || 60 * 60 * 1000); -const dataDirectory = path.resolve(process.env.DATA_DIR || path.join(__dirname, "..", "data")); - -async function main() { - if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT 必须是有效端口号"); - const jsonDatabasePath = path.join(dataDirectory, "database.json"); - const hasExistingJsonStore = await fs.promises.access(jsonDatabasePath).then(() => true, () => false); - if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL && !hasExistingJsonStore) { - throw new Error("生产环境必须配置 DATABASE_URL"); - } - if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL) { - console.warn("警告: 正在使用既有 JSON 数据库;请尽快迁移到 PostgreSQL"); - } - const filesDirectory = path.join(dataDirectory, "files"); - await fs.promises.mkdir(filesDirectory, { recursive: true }); - const store = process.env.DATABASE_URL - ? new PostgresStore(process.env.DATABASE_URL, filesDirectory) - : new JsonStore(dataDirectory); - await store.initialize(); - const app = createApp({ store }); - const purgeIdentificationFiles = async () => { - try { - const purged = await app.locals.purgeExpiredIdentificationFiles(); - if (purged) console.info(`[retention] purged ${purged} expired identification file(s)`); - } catch (error) { - console.error("[retention] identification file purge failed", error); - } - }; - await purgeIdentificationFiles(); - const purgeTimer = setInterval(purgeIdentificationFiles, identificationPurgeIntervalMs); - purgeTimer.unref(); - app.listen(port, host, () => { - console.log(`ReinLoop server listening on http://${host}:${port}`); - console.log(`API endpoints: http://${host}:${port} and http://${host}:${port}/api`); - console.log(`Data directory: ${dataDirectory}`); - console.log(`Metadata store: ${process.env.DATABASE_URL ? "PostgreSQL" : "local JSON"}`); - }); -} - -main().catch((error) => { - console.error(error.message); - process.exitCode = 1; +const path = require("node:path"); +const fs = require("node:fs"); +const { createApp } = require("./app"); +const { JsonStore } = require("./store"); +const { PostgresStore } = require("./postgres-store"); + +const host = process.env.HOST || "127.0.0.1"; +const port = Number(process.env.PORT || 3000); +const identificationPurgeIntervalMs = Number(process.env.IDENTIFICATION_PURGE_INTERVAL_MS || 60 * 60 * 1000); +const dataDirectory = path.resolve(process.env.DATA_DIR || path.join(__dirname, "..", "data")); + +async function main() { + if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT 必须是有效端口号"); + const jsonDatabasePath = path.join(dataDirectory, "database.json"); + const hasExistingJsonStore = await fs.promises.access(jsonDatabasePath).then(() => true, () => false); + if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL && !hasExistingJsonStore) { + throw new Error("生产环境必须配置 DATABASE_URL"); + } + if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL) { + console.warn("警告: 正在使用既有 JSON 数据库;请尽快迁移到 PostgreSQL"); + } + const filesDirectory = path.join(dataDirectory, "files"); + await fs.promises.mkdir(filesDirectory, { recursive: true }); + const store = process.env.DATABASE_URL + ? new PostgresStore(process.env.DATABASE_URL, filesDirectory) + : new JsonStore(dataDirectory); + await store.initialize(); + const app = createApp({ store }); + const purgeIdentificationFiles = async () => { + try { + const purged = await app.locals.purgeExpiredIdentificationFiles(); + if (purged) console.info(`[retention] purged ${purged} expired identification file(s)`); + } catch (error) { + console.error("[retention] identification file purge failed", error); + } + }; + await purgeIdentificationFiles(); + const purgeTimer = setInterval(purgeIdentificationFiles, identificationPurgeIntervalMs); + purgeTimer.unref(); + app.listen(port, host, () => { + console.log(`ReinLoop server listening on http://${host}:${port}`); + console.log(`API endpoint: http://${host}:${port}`); + console.log(`Data directory: ${dataDirectory}`); + console.log(`Metadata store: ${process.env.DATABASE_URL ? "PostgreSQL" : "local JSON"}`); + }); +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; }); \ No newline at end of file diff --git a/server/test/server.test.js b/server/test/server.test.js index 5000abb..696d98c 100644 --- a/server/test/server.test.js +++ b/server/test/server.test.js @@ -15,7 +15,7 @@ let licensePrivateKey; let licensePublicKeyPath; async function post(payload) { - const response = await fetch(`${baseUrl}/api`, { + const response = await fetch(baseUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) @@ -59,7 +59,7 @@ test("health endpoint reports ready", async () => { assert.deepEqual(await response.json(), { success: true, service: "reinloop-server" }); }); -test("root endpoint accepts API requests without the /api suffix", async () => { +test("root endpoint accepts API requests", async () => { const response = await fetch(baseUrl, { method: "POST", headers: { "content-type": "application/json" }, @@ -69,6 +69,15 @@ test("root endpoint accepts API requests without the /api suffix", async () => { assert.deepEqual(await response.json(), { success: true, companies: [] }); }); +test("legacy /api endpoint is unavailable", async () => { + const response = await fetch(`${baseUrl}/api`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "listOrganizations", adminToken: "test-token" }) + }); + assert.equal(response.status, 404); +}); + test("legacy data_record endpoint and uploadUserInfo type are unavailable", async () => { const legacyRoute = await fetch(`${baseUrl}/data_record`, { method: "POST",