Files
new-api/scripts/README.md
lollipopkit🏳️‍⚧️ 155f67e960 (jsrt) opt.: script load strategy
2025-07-16 12:46:08 +08:00

239 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# JavaScript Runtime Scripts
本目录包含 JavaScript Runtime 中间件使用的脚本文件。
## 脚本加载
- 系统会自动读取 `scripts/` 目录下的所有 `.js` 文件
- 脚本按文件名字母顺序加载
- 建议使用数字前缀来控制加载顺序(如:`01_utils.js`, `02_pre_process.js`
- 所有脚本会被合并到一个 JavaScript 运行时环境中
## 配置
通过环境变量配置:
- `JS_RUNTIME_ENABLED=true` - 启用 JavaScript Runtime
- `JS_SCRIPT_DIR=scripts/` - 脚本目录路径
- `JS_MAX_VM_COUNT=8` - 最大虚拟机数量
- `JS_SCRIPT_TIMEOUT=5s` - 脚本执行超时时间
- `JS_FETCH_TIMEOUT=10s` - HTTP 请求超时时间
更多的详细配置可以在 `.env.example` 文件中找到,并在实际使用时重命名为 `.env`
## 必需的函数
脚本中必须定义以下两个函数:
### 1. preProcessRequest(req)
在请求被转发到后端 API 之前调用。
**参数:**
- `req`: 请求对象,包含 `method`, `url`, `headers`, `body` 等属性
**返回值:**
返回一个对象,可包含以下属性:
- `block`: boolean - 是否阻止请求继续执行
- `statusCode`: number - 阻止请求时返回的状态码
- `message`: string - 阻止请求时返回的错误消息
- `headers`: object - 要修改或添加的请求头
- `body`: any - 修改后的请求体
### 2. postProcessResponse(req, res)
在响应返回给客户端之前调用。
**参数:**
- `req`: 原始请求对象
- `res`: 响应对象,包含 `statusCode`, `headers`, `body` 等属性
**返回值:**
返回一个对象,可包含以下属性:
- `statusCode`: number - 修改后的状态码
- `headers`: object - 要修改或添加的响应头
- `body`: string - 修改后的响应体
## 可用的全局对象和函数
- `console.log()`, `console.error()`, `console.warn()` - 日志输出
- `JSON.parse()`, `JSON.stringify()` - JSON 处理
- `fetch(url, options)` - HTTP 请求
- `db` - 主数据库连接
- `logdb` - 日志数据库连接
- `setTimeout(fn, delay)` - 定时器
## 示例脚本
参考现有的示例脚本:
- `01_utils.js` - 工具函数
- `02_pre_process.js` - 请求预处理
- `03_post_process.js` - 响应后处理
## 使用示例
```js
// 例子:基于数据库的速率限制
if (req.url.includes("/v1/chat/completions")) {
try {
// Check recent requests from this IP
var recentRequests = db.query(
"SELECT COUNT(*) as count FROM logs WHERE created_at > ? AND ip = ?",
Math.floor(Date.now() / 1000) - 60, // last minute
req.remoteIP
);
if (recentRequests && recentRequests.length > 0 && recentRequests[0].count > 10) {
console.log("速率限制 IP:", req.remoteIP);
return {
block: true,
statusCode: 429,
message: "超过速率限制"
};
}
} catch (e) {
console.error("Ratelimit 数据库错误:", e);
}
}
// 例子:修改请求
if (req.url.includes("/chat/completions")) {
try {
var bodyObj = req.body;
let firstMsg = { // 需要新建一个对象,不能修改原有对象
role: "user",
content: "喵呜🐱~嘻嘻"
};
bodyObj.messages[0] = firstMsg;
console.log("Modified first message:", JSON.stringify(firstMsg));
console.log("Modified body:", JSON.stringify(bodyObj));
return {
body: bodyObj,
headers: {
...req.headers,
"X-Modified-Body": "true"
}
};
} catch (e) {
console.error("Failed to modify request body:", {
message: e.message,
stack: e.stack,
bodyType: typeof req.body,
url: req.url
});
}
}
// 例子:读取最近一条日志,新增 jsrt 日志,并输出日志总数
try {
// 1. 读取最近一条日志
var recentLogs = logdb.query(
"SELECT id, user_id, username, content, created_at FROM logs ORDER BY id DESC LIMIT 1"
);
var recentLog = null;
if (recentLogs && recentLogs.length > 0) {
recentLog = recentLogs[0];
console.log("最近一条日志:", JSON.stringify(recentLog));
}
// 2. 新增一条 jsrt 日志
var currentTimestamp = Math.floor(Date.now() / 1000);
var jsrtLogContent = "JSRT 预处理中间件执行 - " + req.URL + " - " + new Date().toISOString();
var insertResult = logdb.exec(
"INSERT INTO logs (user_id, username, created_at, type, content) VALUES (?, ?, ?, ?, ?)",
req.UserID || 0,
req.Username || "jsrt-system",
currentTimestamp,
4, // LogTypeSystem
jsrtLogContent
);
if (insertResult.error) {
console.error("插入 JSRT 日志失败:", insertResult.error);
} else {
console.log("成功插入 JSRT 日志,影响行数:", insertResult.rowsAffected);
}
// 3. 输出日志总数
var totalLogsResult = logdb.query("SELECT COUNT(*) as total FROM logs");
var totalLogs = 0;
if (totalLogsResult && totalLogsResult.length > 0) {
totalLogs = totalLogsResult[0].total;
}
console.log("当前日志总数:", totalLogs);
console.log("JSRT 日志管理示例执行完成");
} catch (e) {
console.error("JSRT 日志管理示例执行失败:", {
message: e.message,
stack: e.stack,
url: req.URL
});
}
// 例子:使用 fetch 调用外部 API
if (req.url.includes("/api/uptime/status")) {
try {
// 使用 httpbin.org/ip 测试 fetch 功能
var response = fetch("https://httpbin.org/ip", {
method: "GET",
timeout: 5, // 5秒超时
headers: {
"User-Agent": "OneAPI-JSRT/1.0"
}
});
if (response.Error.length === 0) {
// 解析响应体
var ipData = JSON.parse(response.Body);
// 可以根据获取到的 IP 信息进行后续处理
if (ipData.origin) {
console.log("外部 IP 地址:", ipData.origin);
// 示例:记录 IP 信息到数据库
var currentTimestamp = Math.floor(Date.now() / 1000);
var logContent = "Fetch 示例 - 外部 IP: " + ipData.origin + " - " + new Date().toISOString();
var insertResult = logdb.exec(
"INSERT INTO logs (user_id, username, created_at, type, content) VALUES (?, ?, ?, ?, ?)",
0,
"jsrt-fetch",
currentTimestamp,
4, // LogTypeSystem
logContent
);
if (insertResult.error) {
console.error("记录 IP 信息失败:", insertResult.error);
} else {
console.log("成功记录 IP 信息到数据库");
}
}
} else {
console.error("Fetch 失败 ", response.Status, " ", response.Error);
}
} catch (e) {
console.error("Fetch 失败:", {
message: e.message,
stack: e.stack,
url: req.url
});
}
}
```
## 管理接口
### 重新加载脚本
```bash
curl -X POST http://host:port/api/jsrt/reload \
-H 'Content-Type: application/json' \
-H 'Authorization Bearer <admin_token>'
```
## 故障排除
- 查看服务日志中的 JavaScript 相关错误信息
- 使用 `console.log()` 调试脚本逻辑
- 确保 JavaScript 语法正确(不支持所有 ES6+ 特性)