remove /api support
This commit is contained in:
@@ -22,7 +22,7 @@ test("panel model kernel renames, overwrites, lists both names, and deletes on t
|
|||||||
});
|
});
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
apiUrl: `http://127.0.0.1:${server.address().port}/api`,
|
apiUrl: `http://127.0.0.1:${server.address().port}`,
|
||||||
adminToken: "test-token"
|
adminToken: "test-token"
|
||||||
};
|
};
|
||||||
const handlers = createModelHandlers({ callServer });
|
const handlers = createModelHandlers({ callServer });
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ $env:B_ADMIN_TOKEN="your-admin-token"
|
|||||||
npm start
|
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
|
### ControlPanel
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd ControlPanel
|
cd ControlPanel
|
||||||
npm install
|
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"
|
$env:B_ADMIN_TOKEN="your-admin-token"
|
||||||
npm run gui
|
npm run gui
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ base_url = os.environ.get(
|
|||||||
).rstrip("/")
|
).rstrip("/")
|
||||||
server_api_url = os.environ.get(
|
server_api_url = os.environ.get(
|
||||||
"REINLOOP_API_URL",
|
"REINLOOP_API_URL",
|
||||||
f"{base_url}/api",
|
base_url,
|
||||||
)
|
)
|
||||||
# Compatibility alias used by existing modules. It points to the ReinLoop
|
# Compatibility alias used by existing modules. It points to the ReinLoop
|
||||||
# Express server API, not a cloud-function endpoint.
|
# Express server API, not a cloud-function endpoint.
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
## 约定
|
## 约定
|
||||||
|
|
||||||
- 业务服务端地址由 `REINLOOP_API_URL` 指定,未设置时使用
|
- 业务服务端地址由 `REINLOOP_API_URL` 指定,未设置时使用
|
||||||
`REINLOOP_SERVER_URL + /api`;默认地址为
|
`REINLOOP_SERVER_URL`;默认地址为
|
||||||
`https://ReinLoop.dominatedconvergence.com/api`。
|
`https://ReinLoop.dominatedconvergence.com`。
|
||||||
- 新许可证的设备标识为 `<company_code>/<production_line_code>`,在代码中通过
|
- 新许可证的设备标识为 `<company_code>/<production_line_code>`,在代码中通过
|
||||||
`api.the_folder` 使用。旧许可证才回退到 `REINLOOP_DEVICE_ID`。
|
`api.the_folder` 使用。旧许可证才回退到 `REINLOOP_DEVICE_ID`。
|
||||||
- 所有服务端业务请求均使用 `POST /api`,通过请求体的 `type` 字段分发。
|
- 所有服务端业务请求均使用 `POST /`,通过请求体的 `type` 字段分发。
|
||||||
- 公司、产线、许可证签发/撤销、模型删除、审核反馈及配置提交均为
|
- 公司、产线、许可证签发/撤销、模型删除、审核反馈及配置提交均为
|
||||||
ControlPanel 管理端能力,客户端不提供对应的管理接口。旧微信云函数和其管理脚本
|
ControlPanel 管理端能力,客户端不提供对应的管理接口。旧微信云函数和其管理脚本
|
||||||
已移除。
|
已移除。
|
||||||
@@ -148,7 +148,7 @@ RL 模式下,`ControlEngine` 使用模型根据流量、当前压力和压力
|
|||||||
|
|
||||||
## 客户端服务端协议
|
## 客户端服务端协议
|
||||||
|
|
||||||
所有业务请求都发送至 `POST /api`。业务成功响应应至少包含 `success: true`。
|
所有业务请求都发送至 `POST /`。业务成功响应应至少包含 `success: true`。
|
||||||
|
|
||||||
| `type` | 请求关键字段 | 用途 |
|
| `type` | 请求关键字段 | 用途 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ def _api_url():
|
|||||||
base_url = os.environ.get(
|
base_url = os.environ.get(
|
||||||
"REINLOOP_SERVER_URL", "https://ReinLoop.dominatedconvergence.com"
|
"REINLOOP_SERVER_URL", "https://ReinLoop.dominatedconvergence.com"
|
||||||
).rstrip("/")
|
).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():
|
def _offline_limit_seconds():
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "data_collector.py"
|
|||||||
def load_data_collector_module():
|
def load_data_collector_module():
|
||||||
api = types.ModuleType("api")
|
api = types.ModuleType("api")
|
||||||
api.base_url = "https://cloud.example"
|
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"
|
api.the_folder = "customer-a/line-1"
|
||||||
requests = types.ModuleType("requests")
|
requests = types.ModuleType("requests")
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ class DeviceHeartbeatTests(unittest.TestCase):
|
|||||||
|
|
||||||
requests_module.post = post
|
requests_module.post = post
|
||||||
api_module = types.ModuleType("api")
|
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"
|
api_module.the_folder = "company/line"
|
||||||
with patch.dict(sys.modules, {"requests": requests_module, "api": api_module}):
|
with patch.dict(sys.modules, {"requests": requests_module, "api": api_module}):
|
||||||
timestamp = HEARTBEAT.heartbeat_device(timeout=7)
|
timestamp = HEARTBEAT.heartbeat_device(timeout=7)
|
||||||
|
|
||||||
self.assertEqual(timestamp, "2026-07-28T00:00:00.000Z")
|
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"
|
"type": "deviceHeartbeat", "deviceId": "company/line"
|
||||||
}, 7)])
|
}, 7)])
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 通用约定
|
## 通用约定
|
||||||
|
|
||||||
业务接口为 `POST /api`。请求与响应均为 JSON,响应包含 `success`。
|
业务接口为 `POST /`。请求与响应均为 JSON,响应包含 `success`。
|
||||||
|
|
||||||
标注为 Admin 的接口需要附加:
|
标注为 Admin 的接口需要附加:
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
| 方法 | 路径 | 用途 |
|
| 方法 | 路径 | 用途 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `POST` | `/api` | 主业务 API |
|
| `POST` | `/` | 主业务 API |
|
||||||
| `POST` | `/upload/:token` | 根据上传凭证接收 multipart 文件 |
|
| `POST` | `/upload/:token` | 根据上传凭证接收 multipart 文件 |
|
||||||
| `GET` | `/files/:fileID` | 下载已存储文件 |
|
| `GET` | `/files/:fileID` | 下载已存储文件 |
|
||||||
| `GET` | `/health` | 服务存活检查 |
|
| `GET` | `/health` | 服务存活检查 |
|
||||||
|
|||||||
@@ -1111,7 +1111,6 @@ function createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
app.post("/", apiHandler);
|
app.post("/", apiHandler);
|
||||||
app.post("/api", apiHandler);
|
|
||||||
|
|
||||||
app.use((error, req, res, next) => {
|
app.use((error, req, res, next) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
+50
-50
@@ -1,51 +1,51 @@
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const { createApp } = require("./app");
|
const { createApp } = require("./app");
|
||||||
const { JsonStore } = require("./store");
|
const { JsonStore } = require("./store");
|
||||||
const { PostgresStore } = require("./postgres-store");
|
const { PostgresStore } = require("./postgres-store");
|
||||||
|
|
||||||
const host = process.env.HOST || "127.0.0.1";
|
const host = process.env.HOST || "127.0.0.1";
|
||||||
const port = Number(process.env.PORT || 3000);
|
const port = Number(process.env.PORT || 3000);
|
||||||
const identificationPurgeIntervalMs = Number(process.env.IDENTIFICATION_PURGE_INTERVAL_MS || 60 * 60 * 1000);
|
const identificationPurgeIntervalMs = Number(process.env.IDENTIFICATION_PURGE_INTERVAL_MS || 60 * 60 * 1000);
|
||||||
const dataDirectory = path.resolve(process.env.DATA_DIR || path.join(__dirname, "..", "data"));
|
const dataDirectory = path.resolve(process.env.DATA_DIR || path.join(__dirname, "..", "data"));
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT 必须是有效端口号");
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT 必须是有效端口号");
|
||||||
const jsonDatabasePath = path.join(dataDirectory, "database.json");
|
const jsonDatabasePath = path.join(dataDirectory, "database.json");
|
||||||
const hasExistingJsonStore = await fs.promises.access(jsonDatabasePath).then(() => true, () => false);
|
const hasExistingJsonStore = await fs.promises.access(jsonDatabasePath).then(() => true, () => false);
|
||||||
if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL && !hasExistingJsonStore) {
|
if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL && !hasExistingJsonStore) {
|
||||||
throw new Error("生产环境必须配置 DATABASE_URL");
|
throw new Error("生产环境必须配置 DATABASE_URL");
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL) {
|
if (process.env.NODE_ENV === "production" && !process.env.DATABASE_URL) {
|
||||||
console.warn("警告: 正在使用既有 JSON 数据库;请尽快迁移到 PostgreSQL");
|
console.warn("警告: 正在使用既有 JSON 数据库;请尽快迁移到 PostgreSQL");
|
||||||
}
|
}
|
||||||
const filesDirectory = path.join(dataDirectory, "files");
|
const filesDirectory = path.join(dataDirectory, "files");
|
||||||
await fs.promises.mkdir(filesDirectory, { recursive: true });
|
await fs.promises.mkdir(filesDirectory, { recursive: true });
|
||||||
const store = process.env.DATABASE_URL
|
const store = process.env.DATABASE_URL
|
||||||
? new PostgresStore(process.env.DATABASE_URL, filesDirectory)
|
? new PostgresStore(process.env.DATABASE_URL, filesDirectory)
|
||||||
: new JsonStore(dataDirectory);
|
: new JsonStore(dataDirectory);
|
||||||
await store.initialize();
|
await store.initialize();
|
||||||
const app = createApp({ store });
|
const app = createApp({ store });
|
||||||
const purgeIdentificationFiles = async () => {
|
const purgeIdentificationFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const purged = await app.locals.purgeExpiredIdentificationFiles();
|
const purged = await app.locals.purgeExpiredIdentificationFiles();
|
||||||
if (purged) console.info(`[retention] purged ${purged} expired identification file(s)`);
|
if (purged) console.info(`[retention] purged ${purged} expired identification file(s)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[retention] identification file purge failed", error);
|
console.error("[retention] identification file purge failed", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await purgeIdentificationFiles();
|
await purgeIdentificationFiles();
|
||||||
const purgeTimer = setInterval(purgeIdentificationFiles, identificationPurgeIntervalMs);
|
const purgeTimer = setInterval(purgeIdentificationFiles, identificationPurgeIntervalMs);
|
||||||
purgeTimer.unref();
|
purgeTimer.unref();
|
||||||
app.listen(port, host, () => {
|
app.listen(port, host, () => {
|
||||||
console.log(`ReinLoop server listening on http://${host}:${port}`);
|
console.log(`ReinLoop server listening on http://${host}:${port}`);
|
||||||
console.log(`API endpoints: http://${host}:${port} and http://${host}:${port}/api`);
|
console.log(`API endpoint: http://${host}:${port}`);
|
||||||
console.log(`Data directory: ${dataDirectory}`);
|
console.log(`Data directory: ${dataDirectory}`);
|
||||||
console.log(`Metadata store: ${process.env.DATABASE_URL ? "PostgreSQL" : "local JSON"}`);
|
console.log(`Metadata store: ${process.env.DATABASE_URL ? "PostgreSQL" : "local JSON"}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
});
|
});
|
||||||
@@ -15,7 +15,7 @@ let licensePrivateKey;
|
|||||||
let licensePublicKeyPath;
|
let licensePublicKeyPath;
|
||||||
|
|
||||||
async function post(payload) {
|
async function post(payload) {
|
||||||
const response = await fetch(`${baseUrl}/api`, {
|
const response = await fetch(baseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
@@ -59,7 +59,7 @@ test("health endpoint reports ready", async () => {
|
|||||||
assert.deepEqual(await response.json(), { success: true, service: "reinloop-server" });
|
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, {
|
const response = await fetch(baseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
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: [] });
|
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 () => {
|
test("legacy data_record endpoint and uploadUserInfo type are unavailable", async () => {
|
||||||
const legacyRoute = await fetch(`${baseUrl}/data_record`, {
|
const legacyRoute = await fetch(`${baseUrl}/data_record`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user