From 090be528fc925c12ae034d4521a689c8e858b473 Mon Sep 17 00:00:00 2001 From: silk2onion <826809132@qq.com> Date: Sun, 29 Mar 2026 19:25:21 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20NewAPI=20=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E5=8A=9F=E8=83=BD=E5=8F=8A=E5=8F=AA=E8=AF=BB=E4=BB=AA?= =?UTF-8?q?=E8=A1=A8=E6=9D=BF=E6=8E=A5=E5=8F=A3=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 routes/newapiMonitor.js:提供 NewAPI 用量监控接口(请求数、Token、Quota、RPM/TPM、趋势、模型维度聚合等) - 新增 NEWAPI_MONITOR 前端接入与配置说明文档 - 新增 Plugin/config.env.example 配置模板 - server.js:添加只读仪表板接口白名单,免登录失败计数和 IP 封禁;移除冗余 plugin_async_callback 广播 - adminPanelRoutes.js:挂载 newapiMonitor 路由 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...15\347\275\256\350\257\264\346\230\216.md" | 675 ++++++++++++++++++ Plugin/config.env.example | 508 +++++++++++++ routes/adminPanelRoutes.js | 1 + routes/newapiMonitor.js | 560 +++++++++++++++ server.js | 26 +- 5 files changed, 1759 insertions(+), 11 deletions(-) create mode 100644 "NEWAPI_MONITOR_\345\211\215\347\253\257\346\216\245\345\205\245\344\270\216\351\205\215\347\275\256\350\257\264\346\230\216.md" create mode 100644 Plugin/config.env.example create mode 100644 routes/newapiMonitor.js diff --git "a/NEWAPI_MONITOR_\345\211\215\347\253\257\346\216\245\345\205\245\344\270\216\351\205\215\347\275\256\350\257\264\346\230\216.md" "b/NEWAPI_MONITOR_\345\211\215\347\253\257\346\216\245\345\205\245\344\270\216\351\205\215\347\275\256\350\257\264\346\230\216.md" new file mode 100644 index 00000000..af77e342 --- /dev/null +++ "b/NEWAPI_MONITOR_\345\211\215\347\253\257\346\216\245\345\205\245\344\270\216\351\205\215\347\275\256\350\257\264\346\230\216.md" @@ -0,0 +1,675 @@ +# NewAPI 监控功能前端接入与配置说明 + +## 1. 功能简介 + +本功能为 VCP 管理面板新增了一组面向前端展示的 NewAPI 用量监控接口,用于给网页前端、管理面板页面、桌面挂件等场景提供统计数据。 + +当前实现严格遵循最小可用原则,只提供前端已经明确需要的能力: + +- 总请求数统计 +- 总 Token 数统计 +- 总 Quota 统计 +- 当前实时 RPM / TPM +- 按时间范围的趋势数据 +- 按模型维度的聚合数据 +- 按模型筛选 summary / trend + +当前**不包含**以下能力: + +- 区分 `/v1/chat/completions`、`/v1/responses` 等端点级统计 +- 用户排行 +- 渠道排行 +- Token 名排行 +- WebSocket 实时推送 +- 其他前端未明确需要的占位接口 + +--- + +## 2. 后端接入位置 + +本功能对应的核心文件如下: + +- `routes/admin/newapiMonitor.js` +- `routes/adminPanelRoutes.js` +- `config.env.example` + +其中: + +### 2.1 `routes/admin/newapiMonitor.js` +负责: + +- 解析查询参数 +- 连接 NewAPI 管理员接口 +- 管理 session 或账号密码登录 +- 拉取统计数据 +- 聚合并输出给前端 + +### 2.2 `routes/adminPanelRoutes.js` +负责把监控路由挂载到 VCP 管理接口下。 + +### 2.3 `config.env.example` +负责提供配置示例,说明如何填写 NewAPI 监控功能所需环境变量。 + +--- + +## 3. 前端应该请求哪个地址 + +前端**不要直接请求 NewAPI**,而是请求 VCP 的管理接口。 + +可用接口如下: + +- `GET /admin_api/newapi-monitor/summary` +- `GET /admin_api/newapi-monitor/trend` +- `GET /admin_api/newapi-monitor/models` + +这三个接口都由 VCP 后端代持 NewAPI 的管理员鉴权信息。 + +也就是说: + +- 前端不需要持有 NewAPI 的 session cookie +- 前端不需要自己拼 `New-Api-User` +- 前端不需要自己处理 NewAPI 登录逻辑 + +--- + +## 4. 配置说明 + +需要在 VCP 的运行配置中填写以下项目。 + +参考配置写在 `config.env.example` 中。 + +### 4.1 必填配置 + +```env +NEWAPI_MONITOR_BASE_URL=http://127.0.0.1:3000 +``` + +含义: + +- 目标 NewAPI 后台地址 + +### 4.2 可选配置:请求超时 + +```env +NEWAPI_MONITOR_TIMEOUT_MS=15000 +``` + +含义: + +- VCP 调用 NewAPI 时的超时时间,单位毫秒 + +### 4.3 鉴权方式二选一 + +#### 方式 A:直接填写管理员 session cookie(推荐) + +适用于: + +- 开启验证码 +- 开启 2FA +- 不方便让后端自动登录的实例 + +```env +NEWAPI_MONITOR_SESSION_COOKIE= +``` + +#### 方式 B:填写管理员用户名密码 + +适用于: + +- 允许后端直接调用登录接口 +- 无验证码或交互式校验阻碍 + +```env +NEWAPI_MONITOR_USERNAME= +NEWAPI_MONITOR_PASSWORD= +``` + +### 4.4 特殊兼容配置(可选) + +某些经过定制的 NewAPI 实例,除了 session 外,还会额外要求 `New-Api-User` 请求头。 + +这类实例可以补充: + +```env +NEWAPI_MONITOR_API_USER_ID= +``` + +说明: + +- 只有当目标实例明确要求 `New-Api-User` 时才需要填写 +- 普通标准实例可以留空 + +--- + +## 5. 数据来源说明 + +VCP 侧会使用以下策略获取数据: + +### 5.1 优先使用 `/api/data/` +优先从 NewAPI 的聚合数据接口拉取: + +- 请求数 +- token_used +- quota +- created_at +- model_name + +优点: + +- 已按小时聚合 +- 性能更好 +- 更适合前端统计 + +### 5.2 使用 `/api/log/stat` 获取实时值 +用于获取: + +- 当前 RPM +- 当前 TPM + +### 5.3 `/api/data/` 无数据时自动回退到 `/api/log/` +如果目标实例没有可用的 quota_data,则 VCP 会自动回退到消费日志分页拉取,再在本地聚合。 + +因此本功能兼容两类 NewAPI: + +- 已启用聚合数据导出的实例 +- 仅有日志数据的实例 + +--- + +## 6. 接口说明 + +## 6.1 summary 接口 + +### 请求地址 + +```text +GET /admin_api/newapi-monitor/summary +``` + +### 支持参数 + +| 参数 | 是否必填 | 说明 | +|---|---:|---| +| `start_timestamp` | 否 | 开始时间,默认最近 24 小时 | +| `end_timestamp` | 否 | 结束时间,默认当前时间 | +| `model_name` | 否 | 按模型筛选 | + +### 返回内容 + +- 总请求数 +- 总 Token 数 +- 总 Quota +- 当前 RPM +- 当前 TPM + +### 示例响应 + +```json +{ + "success": true, + "data": { + "source": "quota_data", + "start_timestamp": 1711584000, + "end_timestamp": 1711670400, + "model_name": null, + "total_requests": 123, + "total_tokens": 456789, + "total_quota": 987654, + "current_rpm": 12, + "current_tpm": 34567 + } +} +``` + +--- + +## 6.2 trend 接口 + +### 请求地址 + +```text +GET /admin_api/newapi-monitor/trend +``` + +### 支持参数 + +| 参数 | 是否必填 | 说明 | +|---|---:|---| +| `start_timestamp` | 否 | 开始时间,默认最近 24 小时 | +| `end_timestamp` | 否 | 结束时间,默认当前时间 | +| `model_name` | 否 | 按模型筛选 | + +### 返回内容 + +返回趋势数组,每个时间桶包含: + +- `created_at` +- `requests` +- `token_used` +- `quota` + +### 示例响应 + +```json +{ + "success": true, + "data": { + "source": "quota_data", + "start_timestamp": 1711584000, + "end_timestamp": 1711670400, + "model_name": null, + "items": [ + { + "created_at": 1711584000, + "requests": 10, + "token_used": 20000, + "quota": 30000 + } + ] + } +} +``` + +--- + +## 6.3 models 接口 + +### 请求地址 + +```text +GET /admin_api/newapi-monitor/models +``` + +### 支持参数 + +| 参数 | 是否必填 | 说明 | +|---|---:|---| +| `start_timestamp` | 否 | 开始时间,默认最近 24 小时 | +| `end_timestamp` | 否 | 结束时间,默认当前时间 | + +### 返回内容 + +返回按模型聚合后的统计数据: + +- `model_name` +- `requests` +- `token_used` +- `quota` + +### 示例响应 + +```json +{ + "success": true, + "data": { + "source": "quota_data", + "start_timestamp": 1711584000, + "end_timestamp": 1711670400, + "items": [ + { + "model_name": "gpt-4o", + "requests": 100, + "token_used": 123456, + "quota": 789012 + } + ] + } +} +``` + +--- + +## 7. 前端如何调用 + +## 7.1 基础原则 + +前端应该: + +- 只调用 VCP 的 `/admin_api/newapi-monitor/*` +- 尽量使用同源请求 +- 使用浏览器原生 `fetch` +- 不直接连接 NewAPI 后台接口 +- 不在前端保存 NewAPI 管理员鉴权信息 + +--- + +## 7.2 推荐封装一个通用请求函数 + +```js +async function requestMonitorJson(url) { + const response = await fetch(url, { + method: 'GET', + credentials: 'same-origin' + }); + + const result = await response.json(); + + if (!response.ok || result.success === false) { + throw new Error(result.error || result.message || '请求失败'); + } + + return result.data; +} +``` + +--- + +## 7.3 获取 summary + +```js +async function fetchSummary({ startTimestamp, endTimestamp, modelName }) { + const params = new URLSearchParams(); + + if (startTimestamp) params.set('start_timestamp', String(startTimestamp)); + if (endTimestamp) params.set('end_timestamp', String(endTimestamp)); + if (modelName) params.set('model_name', modelName); + + return requestMonitorJson(`/admin_api/newapi-monitor/summary?${params.toString()}`); +} +``` + +--- + +## 7.4 获取 trend + +```js +async function fetchTrend({ startTimestamp, endTimestamp, modelName }) { + const params = new URLSearchParams(); + + if (startTimestamp) params.set('start_timestamp', String(startTimestamp)); + if (endTimestamp) params.set('end_timestamp', String(endTimestamp)); + if (modelName) params.set('model_name', modelName); + + return requestMonitorJson(`/admin_api/newapi-monitor/trend?${params.toString()}`); +} +``` + +--- + +## 7.5 获取 models + +```js +async function fetchModels({ startTimestamp, endTimestamp }) { + const params = new URLSearchParams(); + + if (startTimestamp) params.set('start_timestamp', String(startTimestamp)); + if (endTimestamp) params.set('end_timestamp', String(endTimestamp)); + + return requestMonitorJson(`/admin_api/newapi-monitor/models?${params.toString()}`); +} +``` + +--- + +## 8. 前端页面建议结构 + +建议页面分成三块: + +### 8.1 筛选区域 +建议包含: + +- 时间范围选择器 +- 模型选择器 +- 手动刷新按钮 + +### 8.2 顶部统计卡片 +建议展示: + +- 总请求数 +- 总 Token +- 总 Quota +- 当前 RPM +- 当前 TPM + +### 8.3 趋势与模型排行区域 +建议展示: + +- 趋势图 +- 模型排行表格 + +--- + +## 9. 页面初始化建议 + +推荐初始化流程如下: + +1. 计算默认时间范围(最近 24 小时) +2. 请求 `models` +3. 请求 `summary` +4. 请求 `trend` +5. 渲染下拉框、卡片、图表和表格 + +示例: + +```js +async function initMonitorPage() { + const endTimestamp = Math.floor(Date.now() / 1000); + const startTimestamp = endTimestamp - 24 * 60 * 60; + + const [modelsData, summaryData, trendData] = await Promise.all([ + fetchModels({ startTimestamp, endTimestamp }), + fetchSummary({ startTimestamp, endTimestamp }), + fetchTrend({ startTimestamp, endTimestamp }) + ]); + + renderModelOptions(modelsData.items); + renderSummaryCards(summaryData); + renderTrendChart(trendData.items); + renderModelTable(modelsData.items); +} +``` + +--- + +## 10. 一个最小可用前端示例 + +```html +
+ + + +
+ +
+

+

+
+
+```
+
+---
+
+## 11. 如何验证功能是否可用
+
+在前端正式接入前,建议先手动验证:
+
+1. 修改配置
+2. 重启 VCP
+3. 登录 VCP 管理面板
+4. 浏览器访问以下地址,确认返回 JSON:
+
+- `/admin_api/newapi-monitor/summary`
+- `/admin_api/newapi-monitor/trend`
+- `/admin_api/newapi-monitor/models`
+
+如果这三个接口都能正常返回数据,前端接入基本不会有问题。
+
+---
+
+## 12. 推荐刷新策略
+
+建议:
+
+- 页面初始化时加载全部数据
+- `summary` 每 30 秒自动刷新一次
+- `trend` 和 `models` 在筛选条件变化时刷新
+- 同时保留一个手动刷新按钮
+
+示例:
+
+```js
+let summaryTimer = null;
+
+function startSummaryAutoRefresh(getFilters) {
+  stopSummaryAutoRefresh();
+
+  summaryTimer = setInterval(async () => {
+    try {
+      const filters = getFilters();
+      const summaryData = await fetchSummary(filters);
+      renderSummaryCards(summaryData);
+    } catch (error) {
+      console.error('summary 自动刷新失败:', error);
+    }
+  }, 30000);
+}
+
+function stopSummaryAutoRefresh() {
+  if (summaryTimer) {
+    clearInterval(summaryTimer);
+    summaryTimer = null;
+  }
+}
+```
+
+---
+
+## 13. 前端错误处理建议
+
+后端错误通常返回:
+
+```json
+{
+  "success": false,
+  "error": "错误信息"
+}
+```
+
+前端建议:
+
+- 显示 loading 状态
+- 请求失败时显示错误提示
+- 提供重试按钮
+- 尽可能保留上一次成功的数据
+
+---
+
+## 14. 使用流程总结
+
+整个功能的典型使用流程如下:
+
+1. 在 VCP 中配置 NewAPI 监控所需环境变量
+2. 重启 VCP
+3. 用浏览器验证三个管理接口是否可返回 JSON
+4. 前端通过 `fetch` 请求 `/admin_api/newapi-monitor/*`
+5. 将 `summary` 渲染为统计卡片
+6. 将 `trend` 渲染为趋势图
+7. 将 `models` 渲染为模型排行表或模型下拉框
+
+---
+
+## 15. 维护注意事项
+
+- `NEWAPI_MONITOR_BASE_URL` 为必填项
+- 推荐优先使用 `NEWAPI_MONITOR_SESSION_COOKIE`
+- 用户名密码方式仅作备选
+- `NEWAPI_MONITOR_API_USER_ID` 只在某些特殊实例中需要
+- 前端不要根据 `source` 的单一取值写死逻辑
+- 若未来前端新增明确需求,再扩展接口,不要预埋无用能力
+
+---
+
+## 16. 一句话总结
+
+这套功能的核心原则只有一句话:
+
+**前端只请求 VCP 的 `/admin_api/newapi-monitor/*`,不要直接碰 NewAPI 管理员鉴权。**
\ No newline at end of file
diff --git a/Plugin/config.env.example b/Plugin/config.env.example
new file mode 100644
index 00000000..db8d3bf2
--- /dev/null
+++ b/Plugin/config.env.example
@@ -0,0 +1,508 @@
+# -------------------------------------------------------------------
+# [核心配置] 访问AI模型API的必要凭证
+# -------------------------------------------------------------------
+# VCP作为中间层,需要配置一个后端的AI服务才能工作。
+# 这里填写你的AI服务商提供的API地址和密钥。
+# 例如,如果你使用OpenAI,API_URL可能类似于 "https://api.openai.com",API_Key则是你的 "sk-..." 密钥。
+
+API_Key=YOUR_API_KEY_SUCH_AS_sk-xxxxxxxxxxxxxxxxxxxxxxxx
+API_URL=NEWAPI_URL_SUCH_AS_http://127.0.0.1:3000
+ 
+# -------------------------------------------------------------------
+# [服务配置] VCP服务本身的设置
+# -------------------------------------------------------------------
+# 这里定义VCP服务如何被外部访问。
+# PORT: VCP服务运行的端口。
+# Key: 访问VCP聊天API(/v1/chat/completions)时需要提供的密码,保护你的服务不被滥用。
+# Image_Key: 访问VCP图片服务时需要提供的密码,同样用于安全保护。
+# File_Key: 访问VCP插件生成文档服务时需要提供的密码,同样用于安全保护。
+
+PORT=6005
+Key=YOUR_KEY_SUCH_AS_aBcDeFgHiJkLmNoP
+Image_Key=YOUR_IMAGE_KEY_SUCH_AS_Images_aBcDeFgHiJk
+File_Key=YOUR_FILE_KEY_SUCH_AS_123456
+# VCP服务器WebSocket鉴权,用于VCP面板和分布式服务器之间的实时通信。
+VCP_Key=YOUR_VCP_KEY_SUCH_AS_aBcDeFgHiJkLmNoP
+
+#引入网络波动重试机制
+ApiRetries=3
+ApiRetryDelay=200
+
+# DEFAULT_TIMEZONE: 定义服务器的默认时区,用于时间相关的操作和日志记录。
+# 推荐设置为 Asia/Shanghai。
+DEFAULT_TIMEZONE=Asia/Shanghai
+
+# 定义VCPTool调用循环栈
+MaxVCPLoopStream=5
+MaxVCPLoopNonStream=5
+#定义VCP调用是否需要验证码
+VCPToolCode=false
+
+# 定义国产A类模型推理功能是否开启(enable_thinking类)
+ChinaModel1=GLM,qwen,deepseek,hunyuan
+ChinaModel1Cot=true
+
+# 隐藏非流式响应中的思维链(Gemini等模型的reasoning_content),默认true
+HIDE_NONSTREAM_REASONING=true
+
+# -------------------------------------------------------------------
+# [角色分割]
+# -------------------------------------------------------------------
+# 启用角色分割功能的总开关,允许在消息中使用 <<<[ROLE_DIVIDE_xxx]>>> 语法进行上下文切割。
+EnableRoleDivider=true
+# 循环栈开关,控制是否在 VCPTool 调用循环中也启用角色分割。
+EnableRoleDividerInLoop=true
+# 自动清除开关,当特定角色分割被禁用时,是否自动从上下文中清除该角色的标签。
+EnableRoleDividerAutoPurge=true
+# 角色分割细分开关,控制是否允许切割为特定角色。
+# 例如:RoleDividerSystem=false 则会跳过对 <<<[ROLE_DIVIDE_SYSTEM]>>> 标签的解析。
+RoleDividerSystem=true
+RoleDividerAssistant=true
+RoleDividerUser=true
+# 角色扫描开关,控制是否对特定角色的楼层进行分割监测。
+# 例如:RoleDividerScanUser=false 则不对 User 楼层进行任何切割处理。
+RoleDividerScanSystem=false
+RoleDividerScanAssistant=true
+RoleDividerScanUser=true
+# 禁用角色标签自动清除开关。
+# 当 RoleDividerXXXX=false 时,是否自动从上下文中移除该角色的标签。
+RoleDividerRemoveDisabledTags=true
+# 角色分割忽略列表,当被包裹的内容在此列表中时,不进行分割处理(保留标签)。
+# 匹配时会忽略内容中的换行符、反斜杠和空格。
+# 格式为 JSON 数组字符串。
+RoleDividerIgnoreList=["…content…","ignore_this_content", "another_ignored_content"]
+
+# -------------------------------------------------------------------
+# [调试与开发]
+# -------------------------------------------------------------------
+# DebugMode: 设置为 "True" 会在控制台输出详细的调试信息,方便开发和排错。
+DebugMode=false
+# ShowVCP: 在非流式输出时,是否在返回结果中包含VCP的调用信息。
+ShowVCP=false
+# CHAT_LOG_ENABLED: 是否在 DebugLog/chat/YYYY-MM-DD/ 下记录每次 chat 的请求体与响应
+# 注意:日志可能包含敏感内容并增加磁盘占用
+CHAT_LOG_ENABLED=false
+
+# -------------------------------------------------------------------
+# [管理面板]
+# -------------------------------------------------------------------
+# 用于登录VCP管理后台的用户名和密码,请务必修改为强密码。
+AdminUsername=admin
+AdminPassword=YOUR_COMPLEX_PASSWORD_SUCH_AS_sd1iLm1xqSLfiI
+
+# 服务器内回调地址,主要用于插件执行完异步任务后通知主程序。
+# 如果你在本地运行,"http://localhost:6005" 通常是正确的。
+# 如果你将VCP部署在服务器上,需要将其中的 "localhost" 替换为你的服务器公网IP或域名。
+CALLBACK_BASE_URL="http://localhost:6005/plugin-callback"
+
+# -------------------------------------------------------------------
+# [NewAPI 用量监控]
+# -------------------------------------------------------------------
+# 该配置用于 VCP 管理面板中的 NewAPI 请求/Token 用量监控接口。
+# 只实现当前前端需要的 summary / trend / models 三个接口。
+#
+# 必填:NewAPI 后台地址
+NEWAPI_MONITOR_BASE_URL=http://127.0.0.1:3000
+#
+# 可选:请求超时(毫秒)
+NEWAPI_MONITOR_TIMEOUT_MS=15000
+#
+# 认证二选一:
+# 1. 优先推荐直接填写管理员 session cookie(适合开启验证码 / 2FA 的 NewAPI)
+# 2. 若目标 NewAPI 允许纯账号密码登录,也可填写管理员用户名密码
+#
+# 示例:new-api-session=xxxxxxx
+NEWAPI_MONITOR_SESSION_COOKIE=
+#
+# 可选:某些自定义 NewAPI 实例会额外要求 New-Api-User 请求头。
+# 仅当目标实例明确要求时再填写对应的管理员用户 ID。
+NEWAPI_MONITOR_API_USER_ID=
+NEWAPI_MONITOR_USERNAME=
+NEWAPI_MONITOR_PASSWORD=
+
+# -------------------------------------------------------------------
+# [模型路由]
+# -------------------------------------------------------------------
+# 白名单穿透模型:有些特殊的模型(如图像生成、嵌入)可能不需要经过VCP复杂的处理。
+# 在这里列出的模型ID,请求将直接转发到后端AI服务,以提高效率。
+WhitelistImageModel=gemini-2.0-flash-exp-image-generation
+WhitelistEmbeddingModel=gemini-embedding-exp-03-07
+WhitelistEmbeddingModelMaxToken=8000
+WhitelistEmbeddingModelList=5
+
+# -------------------------------------------------------------------
+# [Agent 目录]
+# -------------------------------------------------------------------
+# AGENT_DIR_PATH: 指定存放 Agent 文件的根目录。
+# 默认值: (VCP根目录)/Agent
+# AGENT_DIR_PATH=./Agent
+
+# -------------------------------------------------------------------
+# [TVS 变量目录]
+# -------------------------------------------------------------------
+# TVSTXT_DIR_PATH: 指定存放 TVS 变量文件 (.txt) 的根目录。
+# 默认值: (VCP根目录)/TVStxt
+# TVSTXT_DIR_PATH=./TVStxt
+
+# -------------------------------------------------------------------
+# [知识库 (Knowledge Base) V2 - Powered by Vexus-Lite]
+# -------------------------------------------------------------------
+# 新一代知识库系统的核心配置,负责文件监听、向量化、索引和检索。
+
+# --- 核心路径 ---
+# KNOWLEDGEBASE_ROOT_PATH: 指定存放日记/文档的根目录。
+# 默认值: (VCP根目录)/dailynote
+# KNOWLEDGEBASE_ROOT_PATH=./dailynote
+
+# KNOWLEDGEBASE_STORE_PATH: 指定存放向量索引文件 (.usearch) 和 SQLite 数据库的目录。
+# 默认值: (VCP根目录)/VectorStore
+# KNOWLEDGEBASE_STORE_PATH=./VectorStore
+
+# --- 向量模型 ---
+# VECTORDB_DIMENSION: 向量维度,必须与 [模型路由] 中配置的 WhitelistEmbeddingModel 严格匹配!
+# google/gemini-embedding-001 -> 3072
+# text-embedding-3-small -> 1536
+# text-embedding-3-large -> 3072
+VECTORDB_DIMENSION=3072
+
+# --- 性能与行为 ---
+# KNOWLEDGEBASE_FULL_SCAN_ON_STARTUP: 是否在VCP启动时对 KNOWLEDGEBASE_ROOT_PATH 下的所有文件进行一次全量扫描。
+# 设置为 false 可以加快启动速度,但可能错过VCP离线期间的文件变更。
+# 默认值: true
+KNOWLEDGEBASE_FULL_SCAN_ON_STARTUP=true
+
+# KNOWLEDGEBASE_MAX_BATCH_SIZE: 一次批量处理的最大文件数量。
+# 当文件变更频繁时,调大此值可以合并更多操作,减少API调用。
+# 默认值: 50
+# KNOWLEDGEBASE_MAX_BATCH_SIZE=50
+
+# KNOWLEDGEBASE_BATCH_WINDOW_MS: 文件变更后,等待多少毫秒才触发批处理。
+# 用于合并短时间内的多次文件保存操作。
+# 默认值: 2000 (2秒)
+# KNOWLEDGEBASE_BATCH_WINDOW_MS=2000
+
+# KNOWLEDGEBASE_INDEX_SAVE_DELAY: 日记正文索引在内存更新后,等待多少毫秒保存到磁盘。
+# 默认值: 120000 (2分钟)
+# KNOWLEDGEBASE_INDEX_SAVE_DELAY=120000
+
+# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY: 全局 Tag 索引在内存更新后,等待多少毫秒保存到磁盘。
+# 默认值: 300000 (5分钟)
+# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY=300000
+
+# 知识库索引空闲自动卸载:空闲超时时间(毫秒),默认 2 小时
+KNOWLEDGEBASE_INDEX_IDLE_TTL_MS=7200000
+
+# 知识库索引空闲扫描间隔(毫秒),默认 10 分钟
+KNOWLEDGEBASE_INDEX_IDLE_SWEEP_MS=600000
+
+# 流内记忆刷新器-VCPTool触发RAGMemo刷新机制开关。
+RAGMemoRefresh=true
+
+# --- 内容过滤规则 ---
+# 以下规则用于决定哪些文件或文件夹应该被知识库忽略。
+
+# IGNORE_FOLDERS: 要忽略的文件夹名称(日记本名称),用逗号分隔。
+IGNORE_FOLDERS=VCP论坛
+
+# IGNORE_PREFIXES: 要忽略的文件名前缀,用逗号分隔。
+IGNORE_PREFIXES=已整理
+
+# IGNORE_SUFFIXES: 要忽略的文件名后缀,用逗号分隔。
+IGNORE_SUFFIXES=夜伽
+
+# --- Tag 增强与过滤 (TagMemo) ---
+# TAG_BLACKLIST: 在提取Tag时要忽略的Tag列表,用逗号分隔。
+TAG_BLACKLIST=莱恩,莱恩主人,主人,小克,Nova,nova,NOVA,小吉,小闫,小雨,小娜,小冰,小绝,小芸
+
+# TAG_BLACKLIST_SUPER: 在提取Tag后,从Tag中移除的关键词,用逗号分隔。
+TAG_BLACKLIST_SUPER=莱恩,莱恩主人,主人,小克,Nova,nova,NOVA,小吉,小闫,小雨,小娜,小冰,小绝,小芸
+
+# TAG_EXPAND_MAX_COUNT: 在进行Tag增强搜索时,最多扩展的相关Tag数量。
+# 默认值: 30
+TAG_EXPAND_MAX_COUNT=30
+
+# --- 语言置信度补偿 (Language Confidence Gating) ---
+# 用于压制非技术语境下的英文技术噪音(如 Get-EventLog)。
+# LANG_CONFIDENCE_GATING_ENABLED: 是否启用语言置信度补偿。
+# 默认值: true
+LANG_CONFIDENCE_GATING_ENABLED=true
+
+# LANG_PENALTY_UNKNOWN: 当 EPA 无法识别明确世界观(Unknown)时,对英文技术词汇的压制权重。
+# 默认值: 0.05 (强烈压制)
+LANG_PENALTY_UNKNOWN=0.05
+
+# LANG_PENALTY_CROSS_DOMAIN: 当 EPA 识别出明确非技术世界观但召回了技术词汇时,对该词汇的压制权重。
+# 默认值: 0.1
+LANG_PENALTY_CROSS_DOMAIN=0.1
+
+
+# -------------------------------------------------------------------
+# [Agent配置] 定义你的AI角色
+# -------------------------------------------------------------------
+# 每个 "Agent" 都是一个具有特定角色和能力的AI。
+# 你需要在这里为每个Agent指定一个配置文件(.txt格式)。
+# 文件名是Agent的名字,等号后面是对应的配置文件路径(相对于 "Agent/" 目录)。
+# 例如: AgentNova=Nova.txt 表示名为 "Nova" 的Agent使用 "Agent/Nova.txt" 文件进行配置。
+# 你可以根据需要添加、删除或修改这些Agent。
+# 现在已经不需要在此处配置Agent
+
+# -------------------------------------------------------------------
+# [系统提示词] 定制AI的核心行为
+# -------------------------------------------------------------------
+# 这些变量会被注入到发送给AI的系统提示词(System Prompt)中,从而影响AI的行为和回复风格。
+# 你可以使用 {{变量名}} 的方式引用下面定义的其他变量。
+
+# TarSysPrompt: 这是最核心的系统提示词之一,它会在每次对话开始时告诉AI一些基本信息。
+TarSysPrompt="{{VarTimeNow}}当前地址是{{VarCity}},当前天气是{{VCPWeatherInfo}}。"
+# TarEmojiPrompt: 注入到系统提示词中,指导AI如何使用表情包。
+TarEmojiPrompt='本服务器支持表情包功能,通用表情包图床路径为{{VarHttpUrl}}:{{Port}}/pw={{Image_Key}}/images/通用表情包,注意[/通用表情包]路径指代,表情包列表为{{通用表情包}},你可以灵活的在你的输出中插入表情包,调用方式为,使用Width参数来控制表情包尺寸(50-200)。
+'
+
+# TarEmojiList: VCPToolbox会自动根据"image/通用表情包"文件夹定义一个或多个表情包列表文件(.txt格式),AI会从中获取到可用的表情包。
+TarEmojiList=通用表情包.txt
+# 你可以在 "image/" 目录下创建新的表情包文件夹,并在这里放入图片文件。
+
+# -------------------------------------------------------------------
+# [插件与工具] 扩展AI的能力
+# -------------------------------------------------------------------
+# 这里定义了AI可以使用的各种工具(插件),以及如何调用它们的说明。
+
+# --- 可用插件列表说明 ---
+# 下面列出了所有可用的插件。您可以将它们的占位符复制到下面的 VarToolList 中来启用或禁用特定工具。
+#
+# [需要配置的插件]
+# 以下插件需要您在下方 [插件API密钥] 或其他相应区域填写配置信息后才能使用。
+# {{VCP1PanelInfoProvider}}: 1Panel 信息提供器
+# {{VCPAgentAssistant}}: 多智能体协作插件 (需要用户根据 `plugin-manifest.json.example` 自行创建并配置 `plugin-manifest.json` 文件来定义可用的Agent)
+# {{VCPArxivDailyPapers}}: Arxiv 每日论文
+# {{VCPBilibiliFetch}}: Bilibili 内容获取
+# {{VCPCrossRefDailyPapers}}: CrossRef 每日论文
+# {{VCPDoubaoGen}}: 豆包图片生成
+# {{VCPEmojiListGenerator}}: 表情包列表生成器
+# {{VCPFluxGen}}: Flux 图片生成
+# {{VCPFRPSInfoProvider}}: FRPS 设备信息提供器
+# {{VCPImageProcessor}}: 图像信息提取器
+# {{VCPImageServer}}: 图床服务
+# {{VCPNovelAIGen}}: NovelAI 图片生成
+# {{VCPRandomness}}: 随机事件生成器
+# {{VCPSunoGen}}: Suno AI 音乐生成
+# {{VCPSynapsePusher}}: VCP 日志 Synapse 推送器
+# {{VCPTavilySearch}}: Tavily 搜索
+# {{VCPUrlFetch}}: URL 内容获取
+# {{VCPLog}}: VCP 日志推送
+# {{VCPVideoGenerator}}: 视频生成器 (Wan2.1)
+# {{VCPWeatherReporter}}: 天气预报员
+#
+# [开箱即用的插件]
+# 以下插件无需额外配置即可直接使用。
+# {{VCPAgentMessage}}: 代理消息推送
+# {{VCPChromeControl}}: Chrome 浏览器控制器
+# {{VCPChromeObserver}}: Chrome 浏览器观察者
+# {{VCPDailyHot}}: 每日热榜
+# {{VCPDailyNoteManager}}: 日记整理器
+# {{VCPDailyNoteEditor}}: 日记内容编辑器
+# {{VCPDailyNoteGet}}: 日记内容获取器
+# {{VCPDailyNoteWrite}}: 日记写入器
+# {{VCPSciCalculator}}: 科学计算器
+# {{VCPTavern}}: 上下文注入器 (通过在系统提示词中添加 `{{VCPTavern::预设名}}` 来使用,无需在此处启用)
+
+# VarToolList: 告诉AI当前可用的工具有哪些。
+VarToolList=supertool.txt
+
+# 梦工具指南: 定义ai做梦的能力范畴
+VarDreamTool=dreamtool.txt
+
+# VarVCPGuide: 指导AI如何正确地格式化工具调用请求。
+VarVCPGuide='在有相关需求时主动合理调用VCP工具,例如——
+<<<[TOOL_REQUEST]>>>
+maid:「始」name「末」 //切记调用工具时加入署名,使得服务器可以记录VCP工具由谁发起,方便Log记录。
+tool_name:「始」tool「末」
+<<<[END_TOOL_REQUEST]>>>
+'
+
+
+# VarDailyNoteGuide: 指导AI如何使用日记功能来记录和更新长期记忆。
+VarDailyNoteGuide=Dailynote.txt
+
+
+
+**2. 写入指定日记本:**
+使用 `[Tag]你的名字` 的格式,其中 `[Tag]` 是目标文件夹名称 (例如:`[公共]`是公共日记本的储存目录)。署名相对的变成Maid: [公共]Nova '
+
+# VarFileTool: 专门为文件操作工具提供的说明。
+VarFileTool=filetool.txt
+VarForum=ToolForum.txt
+VarMIDITranslator=MIDITranslator.txt
+# -------------------------------------------------------------------
+# [自定义变量] 注入个性化信息
+# -------------------------------------------------------------------
+# 这些变量允许你将各种动态信息和个人信息注入到系统提示词中。
+# VCP会自动替换 {{Date}}, {{Today}}, {{Festival}}, {{Time}}, {{VCPWeatherInfo}} 等内置变量。
+
+VarTimeNow="今天是{{Date}},{{Today}},{{Festival}}。现在是{{Time}}。"
+VarSystemInfo="YOUR_SYSTEM_INFO_SUCH_AS_Windows_11_or_Ubuntu_22.04"
+VarCity=YOUR_CITY_SUCH_AS_Beijing
+VarUser='YOUR_USER_DESCRIPTION_SUCH_AS_Jack'
+VarUserInfo="YOUR_USER_INFO_SUCH_AS_A_developer_who_loves_AI"
+#VarUserDetailedInfo="A_more_detailed_description_of_the_user"
+VarHome='YOUR_HOME_DESCRIPTION_SUCH_AS_My_sweet_home_Alabama'
+VarTeam="团队里有这些专家Agent: 测试AI Nova;主题女仆Coco;记忆整理者MemoriaSorter。"
+
+# Vchat客户端专用路径变量,用于动态指定Vchat或相关程序的根目录。
+VarVchatPath="YOUR_VCHAT_PATH_SUCH_AS_D:\\VCPChat"
+
+# Vchat客户端专用提示词。
+# 用于教导Vchat中的agent输出规范和行为。
+VarDivRender=DIVRendering.txt
+VarRendering='当前Vchat客户端支持高级流式输出渲染器,支持HTML/Div元素/CSS/JS/MD/PY/Latex/Mermaid渲染。可用于输出图表,数据图,数学公式,函数图,网页渲染模块,脚本执行。简单表格可以通过MD,Mermaid输出,复杂表格可以通过div-Css或者draw-io(代码块)输出,div/Script类直接发送会在气泡内渲染,且支持完整的anmie.js与three.js语法动画。Py脚本需要添加```python头,来构建CodeBlock来让脚本可以在气泡内运行。
+Vchat支持多种流式渲染器。
+例如以
……
的完整气泡内容。 +或者以html代码块输出一个悬浮窗(通常用于演示复杂交互元素,日常不需要): +```html + + +``` +主流输出方式还是以 +' + +# 桌面提示词管理 +VarDesktop=DesktopCore.txt + +# 当前客户端写出和谐聊天气泡的指导方法: +VarAdaptiveBubbleTip='主题模式自适应气泡实现指南: + +使用CSS变量实现亮暗模式自动切换的关键要素: + +1. 基础结构: +
+ +2. 核心变量: +- var(--primary-bg) : 主背景色 +- var(--secondary-bg) : 次要背景色 +- var(--primary-text) : 主文字颜色 +- var(--highlight-text) : 高亮文字颜色 +- var(--border-color) : 边框颜色 + +3. 增强效果: + backdrop-filter: blur(10px) saturate(120%); + transition: all 0.3s ease-in-out; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + +4. 示例应用: +

+ 标题文字 +

+

内容文字

+ +关键优势: +- 自动适配亮色/暗色主题 +- 无需JavaScript干预 +- 平滑过渡动画 +- 磨砂玻璃效果' + + + +# VarHttpUrl: 你的VCP服务可以通过HTTP访问的地址。如果用了反向代理,这里写你的域名。 +# VarHttpsUrl: 你的VCP服务可以通过HTTPS访问的地址。 +# VarDdnsUrl: 如果你使用DDNS,这里填写你的DDNS地址。 +VarHttpUrl=http://localhost +VarHttpsUrl=https://your-domain.com/ +VarDdnsUrl=http://your-ddns-provider.com + +# ------------------------------------------------------------------- +# [模型专属指令] 为不同AI模型定制不同的行为 +# ------------------------------------------------------------------- +# 有时候,不同的AI模型需要不同的指令才能发挥最佳效果。 +# SarModel: 指定一个或多个模型ID(用逗号分隔)。 +# SarPrompt: 当检测到正在使用上述模型时,这条指令会被附加到系统提示词中。 + +SarModel1=gemini-2.5-flash-preview-05-20,gemini-2.5-flash-preview-04-17 +SarPrompt1="请对用户的输入信息做出详尽,泛化的思考,探索内在的逻辑和外在信息之间的种种关联,从而从多角度,多方面,更准确地,精细地对用户的聊天做出更优质的回答。学会穿透表面,揭示事物间的复杂联系,并进行多步骤、结构化的推理。在任何回答中,你都会优先保证逻辑的自洽性和证据的充分性。你会主动质疑前提,探索多种可能性,并对结论的可靠性进行评估。" +SarModel2=grok-3-beta +SarPrompt2="当前聊天为语言聊天,因此需要考虑因为TTS带来的各种错别字,谐音字,分析用户实际说出的内容。与用户自然对话。尽力理解用户的口语TTS翻译内容,包括可能的发音不准或识别错误。优先结合上下文推断用户的真实意图。你的回答要简洁口语化。" +SarModel3=gemini-2.5-pro-preview-06-05,gemini-2.5-pro,gemini-2.5-flash-preview-05-20,gemini-2.5-flash +SarPrompt3="[KING FALL MODE ENABLE:使用评分级以上思考模式]" +SarModel4=v-gemini-3-pro-preview,gemini-3-pro-preview +SarPrompt4="上下文中的“【VCP元思考】”模块是你已经完成的预研简报。利用你内部的【VCP元思考块】状态,直接生成满足【用户指令】的最终交付物 (Final Deliverable)。请将该简报内容视为你已内化的背景知识,并直接开始输出基于此知识的最终行动或结论。任何时候请勿在正文输出元思考链内容,你的输出应从响应用户的核心需求的第一句话直接开始。" + + +# ------------------------------------------------------------------- +# [通用插件API密钥] +# ------------------------------------------------------------------- +# 这里填写各个插件需要使用的第三方服务API密钥。 + +# 和风天气: 用于获取天气信息。注册并获取Key: https://console.qweather.com/ +WeatherKey=YOUR_QWEATHER_KEY_SUCH_AS_xxxxxxxxxxxxxxxxxxxxxxxx +WeatherUrl=YOUR_QWEATHER_URL_SUCH_AS_devapi.qweather.com + +# Tavily搜索引擎: 用于提供联网搜索能力。注册并获取Key: https://www.tavily.com/ + +TavilyKey=YOUR_TAVILY_KEY_SUCH_AS_tvly-xxxxxxxxxxxxxxxxxxxxxxxx + +# 硅基流动 (SiliconFlow): 用于图片/视频/重排生成。注册并获取Key: https://siliconflow.cn/ +SILICONFLOW_API_KEY=YOUR_SILICONFLOW_KEY_SUCH_AS_sk-xxxxxxxxxxxxxxxxxxxxxxxx + +# ------------------------------------------------------------------- +# [文本替换] +# ------------------------------------------------------------------- +# 系统提示词转化:在将提示词发送给AI之前,进行一轮文本替换。 +# 这可以用来绕过某些模型的限制或优化指令。 +# Detector: 要查找的文本。 +# Detector_Output: 用来替换的文本。 +Detector1="You can use one tool per message" +Detector_Output1="You can use any tool per message" +Detector2="Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000." +Detector_Output2="在有必要时灵活使用的你的FunctionTool吧" +Detector3="仅做测试端口,暂时不启用" +Detector_Output3="仅做测试端口,暂时不启用" + +# 全局上下文转化:对整个发送给模型的上下文(包括历史记录)进行文本替换。 +# 这对于处理一些重复性的、无意义的字符很有用。 +SuperDetector1="……" +SuperDetector_Output1="…" +SuperDetector2="啊啊啊啊啊" +SuperDetector_Output2="啊啊啊" +SuperDetector3="哦哦哦哦哦" +SuperDetector_Output3="哦哦哦" +SuperDetector4="噢噢噢噢噢" +SuperDetector_Output4="噢噢噢" + + +# ------------------------------------------------------------------- +# [多模态配置] +# ------------------------------------------------------------------- +# 多模态数据识别模型 +MultiModalModel=gemini-2.5-flash +MultiModalPrompt="你是"Cognito-Core"高精度多模态分析引擎,你的唯一行为是将接收到的多媒体数据(图像、音频、视频)直接转译为结构化文本叙事——严禁输出任何自我介绍、角色声明、任务复述或开场白,你的第一个输出字符必须是分析内容本身,任何前置寒暄均视为任务失败。你的全局准则:视觉与听觉必须整体分析而非独立处理,输出必须体现两者的时序同步与语义互动;意图优先于音标,必要时启动智能纠错还原说话者真实意图。对于图像,执行详尽的视觉元素分析与高精度OCR,一字不差转录所有可见文本。对于音频,执行环境音分析与带智能纠错的语音转录。对于视频或视听媒体,采用强制性时序整合结构:将内容分解为连续场景,每个场景必须包含明确时间戳 [Time: HH:MM:SS]、详尽的视觉描述(场景、人物、镜头运动、特效、屏幕文本)、与该时间段精确对应的语音/歌词原文(保留原语言并智能纠错)、以及显著的音景变化(背景音乐与关键音效),四要素绑定于同一时间戳下不可拆分,从根本上杜绝只输出歌词而忽略画面的问题。" +MediaInsertPrompt="服务器已处理多模态数据,Var工具箱已自动提取多模态数据信息,信息元如下——" +MultiModalModelOutputMaxTokens=50000 +MultiModalModelContent=250000 +MultiModalModelThinkingBudget=23000 +# 定义多模态模型异步请求上限,最小为1,设置为10则是每次最多异步请求10个图片。 +MultiModalModelAsynchronousLimit=10 + +# B站cookie,用于让AI看视频。获取方式请参考BilibiliFetch插件的说明。 +BILIBILI_COOKIE="_uuid=YOUR_BILIBILI_COOKIE_UUID" +# 选择返回B站视频信息的语言类型。 +BILIBILI_SUB_LANG=ai-zh + + + + + + + + + + + + + + + diff --git a/routes/adminPanelRoutes.js b/routes/adminPanelRoutes.js index 758b4559..5822042d 100644 --- a/routes/adminPanelRoutes.js +++ b/routes/adminPanelRoutes.js @@ -65,6 +65,7 @@ module.exports = function (DEBUG_MODE, dailyNoteRootPath, pluginManager, getCurr mount('/', 'toolListEditor'); // Handles /tool-list/* mount('/', 'dream'); // Handles /dream-logs/*, /dream-operation/* mount('/', 'dailyNotes'); // Wrapper for existing dailyNotesRoutes (Handles /dailynotes/*) + mount('/', 'newapiMonitor'); // Handles /newapi-monitor/* return adminApiRouter; }; \ No newline at end of file diff --git a/routes/newapiMonitor.js b/routes/newapiMonitor.js new file mode 100644 index 00000000..7126d7b2 --- /dev/null +++ b/routes/newapiMonitor.js @@ -0,0 +1,560 @@ +const express = require('express'); +const axios = require('axios'); + +const DEFAULT_LOOKBACK_SECONDS = 24 * 60 * 60; +const DEFAULT_TIMEOUT_MS = 15000; +const MAX_LOG_PAGES = 200; +const CONSUME_LOG_TYPE = 2; + +function safeNumber(value, fallback = 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +function normalizeUnixTimestamp(value, fallback = 0) { + const parsed = safeNumber(value, fallback); + if (parsed > 100000000000) { + return Math.floor(parsed / 1000); + } + return Math.floor(parsed); +} + +function normalizeBaseUrl(baseUrl) { + if (typeof baseUrl !== 'string') { + return ''; + } + return baseUrl.trim().replace(/\/+$/, ''); +} + +function normalizeCookieFromSetCookie(setCookieHeader) { + if (!Array.isArray(setCookieHeader) || setCookieHeader.length === 0) { + return ''; + } + return setCookieHeader + .map((cookieItem) => String(cookieItem).split(';')[0].trim()) + .filter(Boolean) + .join('; '); +} + +function buildError(message, status = 500) { + const error = new Error(message); + error.status = status; + return error; +} + +function shouldRefreshSession(status, message) { + if (status === 401 || status === 403) { + return true; + } + if (typeof message !== 'string' || !message.trim()) { + return false; + } + return /unauthorized|forbidden|session|登录|未登录|权限|auth/i.test(message); +} + +function getModelNameFromQuery(query) { + const value = query.model_name ?? query.model ?? ''; + return typeof value === 'string' ? value.trim() : ''; +} + +function getTimeRangeFromQuery(query) { + const now = Math.floor(Date.now() / 1000); + const startValue = query.start_timestamp; + const endValue = query.end_timestamp; + + let endTimestamp = endValue ? normalizeUnixTimestamp(endValue, now) : now; + let startTimestamp = startValue + ? normalizeUnixTimestamp(startValue, endTimestamp - DEFAULT_LOOKBACK_SECONDS) + : endTimestamp - DEFAULT_LOOKBACK_SECONDS; + + if (!(endTimestamp > 0)) { + endTimestamp = now; + } + if (startTimestamp < 0) { + startTimestamp = 0; + } + if (startTimestamp > endTimestamp) { + throw buildError('start_timestamp 不能大于 end_timestamp。', 400); + } + + return { startTimestamp, endTimestamp }; +} + +function normalizeQuotaItem(item = {}) { + return { + model_name: typeof item.model_name === 'string' ? item.model_name : '', + created_at: normalizeUnixTimestamp(item.created_at, 0), + requests: safeNumber(item.count, 0), + token_used: safeNumber(item.token_used, 0), + quota: safeNumber(item.quota, 0) + }; +} + +function normalizeLogItem(item = {}) { + return { + created_at: normalizeUnixTimestamp(item.created_at, 0), + model_name: typeof item.model_name === 'string' ? item.model_name : '', + prompt_tokens: safeNumber(item.prompt_tokens, 0), + completion_tokens: safeNumber(item.completion_tokens, 0), + quota: safeNumber(item.quota, 0) + }; +} + +function toHourTimestamp(unixSeconds) { + const value = normalizeUnixTimestamp(unixSeconds, 0); + return value - (value % 3600); +} + +function sortByCreatedAtAsc(a, b) { + return a.created_at - b.created_at; +} + +function sortModelItems(items) { + return items.sort((a, b) => { + if (b.requests !== a.requests) { + return b.requests - a.requests; + } + if (b.token_used !== a.token_used) { + return b.token_used - a.token_used; + } + if (b.quota !== a.quota) { + return b.quota - a.quota; + } + return a.model_name.localeCompare(b.model_name); + }); +} + +class NewApiMonitorClient { + constructor({ baseUrl, sessionCookie, username, password, timeoutMs, debugMode, apiUserId }) { + this.baseUrl = normalizeBaseUrl(baseUrl); + this.staticSessionCookie = typeof sessionCookie === 'string' ? sessionCookie.trim() : ''; + this.username = typeof username === 'string' ? username.trim() : ''; + this.password = typeof password === 'string' ? password : ''; + this.timeoutMs = safeNumber(timeoutMs, DEFAULT_TIMEOUT_MS); + this.debugMode = Boolean(debugMode); + this.apiUserId = typeof apiUserId === 'string' ? apiUserId.trim() : ''; + this.cachedSessionCookie = ''; + } + + get isConfigured() { + return Boolean(this.baseUrl) && Boolean(this.staticSessionCookie || (this.username && this.password)); + } + + debugLog(...args) { + if (this.debugMode) { + console.log('[NewApiMonitor]', ...args); + } + } + + buildAuthHeaders(sessionCookie) { + const headers = { + Cookie: sessionCookie + }; + if (this.apiUserId) { + headers['New-Api-User'] = this.apiUserId; + } + return headers; + } + + async login() { + if (!this.baseUrl) { + throw buildError('未配置 NEWAPI_MONITOR_BASE_URL。', 503); + } + if (!this.username || !this.password) { + throw buildError('未配置 NewAPI 自动登录账号,请设置 NEWAPI_MONITOR_USERNAME 和 NEWAPI_MONITOR_PASSWORD,或直接提供 NEWAPI_MONITOR_SESSION_COOKIE。', 503); + } + + this.debugLog('Attempting login with username/password.'); + + const response = await axios({ + url: `${this.baseUrl}/api/user/login`, + method: 'POST', + timeout: this.timeoutMs, + headers: { + 'Content-Type': 'application/json' + }, + data: { + username: this.username, + password: this.password + }, + validateStatus: () => true + }); + + const responseBody = response.data || {}; + if (responseBody && responseBody.data && responseBody.data.require_2fa) { + throw buildError('NewAPI 管理员账户启用了 2FA,无法自动登录。请改为配置 NEWAPI_MONITOR_SESSION_COOKIE。', 503); + } + if (response.status >= 400 || responseBody.success !== true) { + throw buildError(`NewAPI 登录失败:${responseBody.message || response.statusText || 'unknown error'}`, 502); + } + + const cookieHeader = normalizeCookieFromSetCookie(response.headers['set-cookie']); + if (!cookieHeader) { + throw buildError('NewAPI 登录成功,但没有返回可用的 session cookie。', 502); + } + + this.cachedSessionCookie = cookieHeader; + this.debugLog('Login succeeded and session cookie cached.'); + return this.cachedSessionCookie; + } + + async getSessionCookie(forceRefresh = false) { + if (this.staticSessionCookie) { + return this.staticSessionCookie; + } + if (!forceRefresh && this.cachedSessionCookie) { + return this.cachedSessionCookie; + } + this.cachedSessionCookie = ''; + return this.login(); + } + + async request(path, { method = 'GET', params = {}, retryOnAuthFailure = true } = {}) { + if (!this.isConfigured) { + throw buildError('NewAPI 监控未配置。请设置 NEWAPI_MONITOR_BASE_URL,并提供 session cookie 或管理员账号密码。', 503); + } + + const sessionCookie = await this.getSessionCookie(false); + const response = await axios({ + url: `${this.baseUrl}${path}`, + method, + params, + timeout: this.timeoutMs, + headers: this.buildAuthHeaders(sessionCookie), + validateStatus: () => true + }); + + const responseBody = response.data || {}; + const responseMessage = typeof responseBody.message === 'string' ? responseBody.message : ''; + + if (response.status === 401 && /New-Api-User/i.test(responseMessage)) { + if (!this.apiUserId) { + throw buildError('目标 NewAPI 实例要求请求头 New-Api-User,请配置 NEWAPI_MONITOR_API_USER_ID。', 503); + } + throw buildError(`NewAPI New-Api-User 校验失败:${responseMessage}`, 502); + } + + if (shouldRefreshSession(response.status, responseMessage) && retryOnAuthFailure && !this.staticSessionCookie) { + this.debugLog('Session may be expired. Refreshing session and retrying request.', path); + await this.getSessionCookie(true); + return this.request(path, { method, params, retryOnAuthFailure: false }); + } + + if (response.status >= 400) { + throw buildError(`请求 NewAPI 失败(${response.status}):${responseMessage || response.statusText || path}`, 502); + } + if (responseBody.success === false) { + throw buildError(`NewAPI 返回失败:${responseMessage || path}`, 502); + } + + return responseBody; + } + + async getQuotaData(startTimestamp, endTimestamp) { + return this.request('/api/data/', { + params: { + start_timestamp: startTimestamp, + end_timestamp: endTimestamp + } + }); + } + + async getLogStat(startTimestamp, endTimestamp, modelName) { + const params = { + type: CONSUME_LOG_TYPE, + start_timestamp: startTimestamp, + end_timestamp: endTimestamp + }; + if (modelName) { + params.model_name = modelName; + } + + return this.request('/api/log/stat', { params }); + } + + async getConsumeLogs(startTimestamp, endTimestamp, modelName, page) { + const params = { + type: CONSUME_LOG_TYPE, + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + p: page, + page_size: 100 + }; + if (modelName) { + params.model_name = modelName; + } + + return this.request('/api/log/', { params }); + } +} + +function createMonitorClient(debugMode) { + return new NewApiMonitorClient({ + baseUrl: process.env.NEWAPI_MONITOR_BASE_URL, + sessionCookie: process.env.NEWAPI_MONITOR_SESSION_COOKIE, + username: process.env.NEWAPI_MONITOR_USERNAME, + password: process.env.NEWAPI_MONITOR_PASSWORD, + timeoutMs: process.env.NEWAPI_MONITOR_TIMEOUT_MS, + debugMode, + apiUserId: process.env.NEWAPI_MONITOR_API_USER_ID + }); +} + +async function fetchAllConsumeLogs(client, { startTimestamp, endTimestamp, modelName }) { + const logItems = []; + + for (let page = 1; ; page += 1) { + if (page > MAX_LOG_PAGES) { + break; + } + + const responseBody = await client.getConsumeLogs(startTimestamp, endTimestamp, modelName, page); + const pageInfo = responseBody && responseBody.data ? responseBody.data : {}; + const currentItems = Array.isArray(pageInfo.items) ? pageInfo.items.map(normalizeLogItem) : []; + const total = safeNumber(pageInfo.total, 0); + + logItems.push(...currentItems); + + if (currentItems.length === 0) { + break; + } + if (!(currentItems.length >= 100)) { + break; + } + if (total > 0 && logItems.length >= total) { + break; + } + } + + return logItems; +} + +async function fetchUsageDataset(client, { startTimestamp, endTimestamp, modelName }) { + const quotaResponseBody = await client.getQuotaData(startTimestamp, endTimestamp); + const quotaItems = Array.isArray(quotaResponseBody && quotaResponseBody.data) + ? quotaResponseBody.data.map(normalizeQuotaItem) + : []; + + if (quotaItems.length > 0) { + return { + source: 'quota_data', + quotaItems, + logItems: [] + }; + } + + const logItems = await fetchAllConsumeLogs(client, { startTimestamp, endTimestamp, modelName }); + return { + source: 'consume_logs', + quotaItems: [], + logItems + }; +} + +function buildTrendItemsFromQuotaData(quotaItems, modelName) { + const trendMap = new Map(); + + for (const quotaItem of quotaItems) { + if (modelName && quotaItem.model_name !== modelName) { + continue; + } + + const key = quotaItem.created_at; + if (!trendMap.has(key)) { + trendMap.set(key, { + created_at: key, + requests: 0, + token_used: 0, + quota: 0 + }); + } + + const bucket = trendMap.get(key); + bucket.requests += quotaItem.requests; + bucket.token_used += quotaItem.token_used; + bucket.quota += quotaItem.quota; + } + + return Array.from(trendMap.values()).sort(sortByCreatedAtAsc); +} + +function buildTrendItemsFromLogs(logItems) { + const trendMap = new Map(); + + for (const logItem of logItems) { + const key = toHourTimestamp(logItem.created_at); + if (!trendMap.has(key)) { + trendMap.set(key, { + created_at: key, + requests: 0, + token_used: 0, + quota: 0 + }); + } + + const bucket = trendMap.get(key); + bucket.requests += 1; + bucket.token_used += logItem.prompt_tokens + logItem.completion_tokens; + bucket.quota += logItem.quota; + } + + return Array.from(trendMap.values()).sort(sortByCreatedAtAsc); +} + +function buildModelItemsFromQuotaData(quotaItems) { + const modelMap = new Map(); + + for (const quotaItem of quotaItems) { + const modelName = quotaItem.model_name || '(unknown)'; + if (!modelMap.has(modelName)) { + modelMap.set(modelName, { + model_name: modelName, + requests: 0, + token_used: 0, + quota: 0 + }); + } + + const bucket = modelMap.get(modelName); + bucket.requests += quotaItem.requests; + bucket.token_used += quotaItem.token_used; + bucket.quota += quotaItem.quota; + } + + return sortModelItems(Array.from(modelMap.values())); +} + +function buildModelItemsFromLogs(logItems) { + const modelMap = new Map(); + + for (const logItem of logItems) { + const modelName = logItem.model_name || '(unknown)'; + if (!modelMap.has(modelName)) { + modelMap.set(modelName, { + model_name: modelName, + requests: 0, + token_used: 0, + quota: 0 + }); + } + + const bucket = modelMap.get(modelName); + bucket.requests += 1; + bucket.token_used += logItem.prompt_tokens + logItem.completion_tokens; + bucket.quota += logItem.quota; + } + + return sortModelItems(Array.from(modelMap.values())); +} + +function buildSummaryPayload(trendItems, realtimeStatBody) { + const totals = trendItems.reduce((accumulator, item) => { + accumulator.total_requests += item.requests; + accumulator.total_tokens += item.token_used; + accumulator.total_quota += item.quota; + return accumulator; + }, { + total_requests: 0, + total_tokens: 0, + total_quota: 0 + }); + + const realtimeData = realtimeStatBody && realtimeStatBody.data ? realtimeStatBody.data : {}; + return { + ...totals, + current_rpm: safeNumber(realtimeData.rpm, 0), + current_tpm: safeNumber(realtimeData.tpm, 0) + }; +} + +function handleRouteError(routeName, error, res) { + const status = safeNumber(error && error.status, 500); + const message = error && error.message ? error.message : 'Unknown error'; + console.error(`[NewApiMonitor] ${routeName} failed:`, error); + res.status(status).json({ + success: false, + error: message + }); +} + +module.exports = function newApiMonitorRoutes(options) { + const router = express.Router(); + const debugMode = Boolean(options && options.DEBUG_MODE); + + router.get('/newapi-monitor/summary', async (req, res) => { + try { + const { startTimestamp, endTimestamp } = getTimeRangeFromQuery(req.query); + const modelName = getModelNameFromQuery(req.query); + const client = createMonitorClient(debugMode); + const usageDataset = await fetchUsageDataset(client, { startTimestamp, endTimestamp, modelName }); + const trendItems = usageDataset.source === 'quota_data' + ? buildTrendItemsFromQuotaData(usageDataset.quotaItems, modelName) + : buildTrendItemsFromLogs(usageDataset.logItems); + const realtimeStatBody = await client.getLogStat(startTimestamp, endTimestamp, modelName); + const summary = buildSummaryPayload(trendItems, realtimeStatBody); + + res.json({ + success: true, + data: { + source: usageDataset.source, + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + model_name: modelName || null, + ...summary + } + }); + } catch (error) { + handleRouteError('summary', error, res); + } + }); + + router.get('/newapi-monitor/trend', async (req, res) => { + try { + const { startTimestamp, endTimestamp } = getTimeRangeFromQuery(req.query); + const modelName = getModelNameFromQuery(req.query); + const client = createMonitorClient(debugMode); + const usageDataset = await fetchUsageDataset(client, { startTimestamp, endTimestamp, modelName }); + const items = usageDataset.source === 'quota_data' + ? buildTrendItemsFromQuotaData(usageDataset.quotaItems, modelName) + : buildTrendItemsFromLogs(usageDataset.logItems); + + res.json({ + success: true, + data: { + source: usageDataset.source, + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + model_name: modelName || null, + items + } + }); + } catch (error) { + handleRouteError('trend', error, res); + } + }); + + router.get('/newapi-monitor/models', async (req, res) => { + try { + const { startTimestamp, endTimestamp } = getTimeRangeFromQuery(req.query); + const client = createMonitorClient(debugMode); + const usageDataset = await fetchUsageDataset(client, { startTimestamp, endTimestamp, modelName: '' }); + const items = usageDataset.source === 'quota_data' + ? buildModelItemsFromQuotaData(usageDataset.quotaItems) + : buildModelItemsFromLogs(usageDataset.logItems); + + res.json({ + success: true, + data: { + source: usageDataset.source, + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + items + } + }); + } catch (error) { + handleRouteError('models', error, res); + } + }); + + return router; +}; \ No newline at end of file diff --git a/server.js b/server.js index dac11e7a..cc3d5521 100644 --- a/server.js +++ b/server.js @@ -395,6 +395,17 @@ const adminAuth = (req, res, next) => { // 验证登录的端点也需要特殊处理(允许无凭据时返回401而不是重定向) const isVerifyEndpoint = req.path === '/admin_api/verify-login'; + // ========== 新增:只读仪表板接口白名单(不计入登录失败次数)========== + const readOnlyDashboardPaths = [ + '/admin_api/system-monitor', + '/admin_api/newapi-monitor', + '/admin_api/server-log', + '/admin_api/user-auth-code', + '/admin_api/weather' + ]; + const isReadOnlyPath = readOnlyDashboardPaths.some(path => req.path.startsWith(path)); + // ========== 新增结束 ========== + if (publicPaths.includes(req.path)) { return next(); // 直接放行登录页面相关资源 } @@ -420,9 +431,9 @@ const adminAuth = (req, res, next) => { return; // 停止进一步处理 } - // 2. 检查IP是否被临时封禁 + // 2. 检查IP是否被临时封禁(仅对非只读接口生效) const blockInfo = tempBlocks.get(clientIp); - if (blockInfo && Date.now() < blockInfo.expires) { + if (blockInfo && Date.now() < blockInfo.expires && !isReadOnlyPath) { console.warn(`[AdminAuth] Blocked login attempt from IP: ${clientIp}. Block expires at ${new Date(blockInfo.expires).toLocaleString()}.`); const timeLeft = Math.ceil((blockInfo.expires - Date.now()) / 1000 / 60); res.setHeader('Retry-After', Math.ceil((blockInfo.expires - Date.now()) / 1000)); // In seconds @@ -463,8 +474,8 @@ const adminAuth = (req, res, next) => { // 4. 验证凭据 if (!credentials || credentials.name !== ADMIN_USERNAME || credentials.pass !== ADMIN_PASSWORD) { - // 认证失败,处理登录尝试计数 - if (clientIp) { + // 认证失败,处理登录尝试计数(仅对非只读接口计数) + if (clientIp && !isReadOnlyPath) { const now = Date.now(); let attemptInfo = loginAttempts.get(clientIp) || { count: 0, firstAttempt: now }; @@ -1124,13 +1135,6 @@ app.post('/plugin-callback/:pluginName/:taskId', async (req, res) => { return res.status(404).json({ status: "error", message: "Plugin not found, but callback noted." }); } - // 🚀 核心导出点:通过 pluginManager 广播回调数据 - pluginManager.emit('plugin_async_callback', { - pluginName, - taskId, - data: callbackData - }); - // 2. WebSocket push (existing logic) if (pluginManifest.webSocketPush && pluginManifest.webSocketPush.enabled) { const targetClientType = pluginManifest.webSocketPush.targetClientType || null; From bd7b8d4705f545a5d7efa244c281ba44d57d37dc Mon Sep 17 00:00:00 2001 From: silk2onion <826809132@qq.com> Date: Sun, 29 Mar 2026 19:39:36 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E5=B0=86=20config.env.example=20=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E6=A0=B9=E7=9B=AE=E5=BD=95=E5=B9=B6=E5=90=88=E5=B9=B6?= =?UTF-8?q?=20NewAPI=20=E7=9B=91=E6=8E=A7=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Plugin/config.env.example 覆盖到根目录,保留完整的 NEWAPI_MONITOR 配置段,删除 Plugin 下的副本。 Co-Authored-By: Claude Opus 4.6 (1M context) --- Plugin/config.env.example | 508 -------------------------------------- config.env.example | 25 ++ 2 files changed, 25 insertions(+), 508 deletions(-) delete mode 100644 Plugin/config.env.example diff --git a/Plugin/config.env.example b/Plugin/config.env.example deleted file mode 100644 index db8d3bf2..00000000 --- a/Plugin/config.env.example +++ /dev/null @@ -1,508 +0,0 @@ -# ------------------------------------------------------------------- -# [核心配置] 访问AI模型API的必要凭证 -# ------------------------------------------------------------------- -# VCP作为中间层,需要配置一个后端的AI服务才能工作。 -# 这里填写你的AI服务商提供的API地址和密钥。 -# 例如,如果你使用OpenAI,API_URL可能类似于 "https://api.openai.com",API_Key则是你的 "sk-..." 密钥。 - -API_Key=YOUR_API_KEY_SUCH_AS_sk-xxxxxxxxxxxxxxxxxxxxxxxx -API_URL=NEWAPI_URL_SUCH_AS_http://127.0.0.1:3000 - -# ------------------------------------------------------------------- -# [服务配置] VCP服务本身的设置 -# ------------------------------------------------------------------- -# 这里定义VCP服务如何被外部访问。 -# PORT: VCP服务运行的端口。 -# Key: 访问VCP聊天API(/v1/chat/completions)时需要提供的密码,保护你的服务不被滥用。 -# Image_Key: 访问VCP图片服务时需要提供的密码,同样用于安全保护。 -# File_Key: 访问VCP插件生成文档服务时需要提供的密码,同样用于安全保护。 - -PORT=6005 -Key=YOUR_KEY_SUCH_AS_aBcDeFgHiJkLmNoP -Image_Key=YOUR_IMAGE_KEY_SUCH_AS_Images_aBcDeFgHiJk -File_Key=YOUR_FILE_KEY_SUCH_AS_123456 -# VCP服务器WebSocket鉴权,用于VCP面板和分布式服务器之间的实时通信。 -VCP_Key=YOUR_VCP_KEY_SUCH_AS_aBcDeFgHiJkLmNoP - -#引入网络波动重试机制 -ApiRetries=3 -ApiRetryDelay=200 - -# DEFAULT_TIMEZONE: 定义服务器的默认时区,用于时间相关的操作和日志记录。 -# 推荐设置为 Asia/Shanghai。 -DEFAULT_TIMEZONE=Asia/Shanghai - -# 定义VCPTool调用循环栈 -MaxVCPLoopStream=5 -MaxVCPLoopNonStream=5 -#定义VCP调用是否需要验证码 -VCPToolCode=false - -# 定义国产A类模型推理功能是否开启(enable_thinking类) -ChinaModel1=GLM,qwen,deepseek,hunyuan -ChinaModel1Cot=true - -# 隐藏非流式响应中的思维链(Gemini等模型的reasoning_content),默认true -HIDE_NONSTREAM_REASONING=true - -# ------------------------------------------------------------------- -# [角色分割] -# ------------------------------------------------------------------- -# 启用角色分割功能的总开关,允许在消息中使用 <<<[ROLE_DIVIDE_xxx]>>> 语法进行上下文切割。 -EnableRoleDivider=true -# 循环栈开关,控制是否在 VCPTool 调用循环中也启用角色分割。 -EnableRoleDividerInLoop=true -# 自动清除开关,当特定角色分割被禁用时,是否自动从上下文中清除该角色的标签。 -EnableRoleDividerAutoPurge=true -# 角色分割细分开关,控制是否允许切割为特定角色。 -# 例如:RoleDividerSystem=false 则会跳过对 <<<[ROLE_DIVIDE_SYSTEM]>>> 标签的解析。 -RoleDividerSystem=true -RoleDividerAssistant=true -RoleDividerUser=true -# 角色扫描开关,控制是否对特定角色的楼层进行分割监测。 -# 例如:RoleDividerScanUser=false 则不对 User 楼层进行任何切割处理。 -RoleDividerScanSystem=false -RoleDividerScanAssistant=true -RoleDividerScanUser=true -# 禁用角色标签自动清除开关。 -# 当 RoleDividerXXXX=false 时,是否自动从上下文中移除该角色的标签。 -RoleDividerRemoveDisabledTags=true -# 角色分割忽略列表,当被包裹的内容在此列表中时,不进行分割处理(保留标签)。 -# 匹配时会忽略内容中的换行符、反斜杠和空格。 -# 格式为 JSON 数组字符串。 -RoleDividerIgnoreList=["…content…","ignore_this_content", "another_ignored_content"] - -# ------------------------------------------------------------------- -# [调试与开发] -# ------------------------------------------------------------------- -# DebugMode: 设置为 "True" 会在控制台输出详细的调试信息,方便开发和排错。 -DebugMode=false -# ShowVCP: 在非流式输出时,是否在返回结果中包含VCP的调用信息。 -ShowVCP=false -# CHAT_LOG_ENABLED: 是否在 DebugLog/chat/YYYY-MM-DD/ 下记录每次 chat 的请求体与响应 -# 注意:日志可能包含敏感内容并增加磁盘占用 -CHAT_LOG_ENABLED=false - -# ------------------------------------------------------------------- -# [管理面板] -# ------------------------------------------------------------------- -# 用于登录VCP管理后台的用户名和密码,请务必修改为强密码。 -AdminUsername=admin -AdminPassword=YOUR_COMPLEX_PASSWORD_SUCH_AS_sd1iLm1xqSLfiI - -# 服务器内回调地址,主要用于插件执行完异步任务后通知主程序。 -# 如果你在本地运行,"http://localhost:6005" 通常是正确的。 -# 如果你将VCP部署在服务器上,需要将其中的 "localhost" 替换为你的服务器公网IP或域名。 -CALLBACK_BASE_URL="http://localhost:6005/plugin-callback" - -# ------------------------------------------------------------------- -# [NewAPI 用量监控] -# ------------------------------------------------------------------- -# 该配置用于 VCP 管理面板中的 NewAPI 请求/Token 用量监控接口。 -# 只实现当前前端需要的 summary / trend / models 三个接口。 -# -# 必填:NewAPI 后台地址 -NEWAPI_MONITOR_BASE_URL=http://127.0.0.1:3000 -# -# 可选:请求超时(毫秒) -NEWAPI_MONITOR_TIMEOUT_MS=15000 -# -# 认证二选一: -# 1. 优先推荐直接填写管理员 session cookie(适合开启验证码 / 2FA 的 NewAPI) -# 2. 若目标 NewAPI 允许纯账号密码登录,也可填写管理员用户名密码 -# -# 示例:new-api-session=xxxxxxx -NEWAPI_MONITOR_SESSION_COOKIE= -# -# 可选:某些自定义 NewAPI 实例会额外要求 New-Api-User 请求头。 -# 仅当目标实例明确要求时再填写对应的管理员用户 ID。 -NEWAPI_MONITOR_API_USER_ID= -NEWAPI_MONITOR_USERNAME= -NEWAPI_MONITOR_PASSWORD= - -# ------------------------------------------------------------------- -# [模型路由] -# ------------------------------------------------------------------- -# 白名单穿透模型:有些特殊的模型(如图像生成、嵌入)可能不需要经过VCP复杂的处理。 -# 在这里列出的模型ID,请求将直接转发到后端AI服务,以提高效率。 -WhitelistImageModel=gemini-2.0-flash-exp-image-generation -WhitelistEmbeddingModel=gemini-embedding-exp-03-07 -WhitelistEmbeddingModelMaxToken=8000 -WhitelistEmbeddingModelList=5 - -# ------------------------------------------------------------------- -# [Agent 目录] -# ------------------------------------------------------------------- -# AGENT_DIR_PATH: 指定存放 Agent 文件的根目录。 -# 默认值: (VCP根目录)/Agent -# AGENT_DIR_PATH=./Agent - -# ------------------------------------------------------------------- -# [TVS 变量目录] -# ------------------------------------------------------------------- -# TVSTXT_DIR_PATH: 指定存放 TVS 变量文件 (.txt) 的根目录。 -# 默认值: (VCP根目录)/TVStxt -# TVSTXT_DIR_PATH=./TVStxt - -# ------------------------------------------------------------------- -# [知识库 (Knowledge Base) V2 - Powered by Vexus-Lite] -# ------------------------------------------------------------------- -# 新一代知识库系统的核心配置,负责文件监听、向量化、索引和检索。 - -# --- 核心路径 --- -# KNOWLEDGEBASE_ROOT_PATH: 指定存放日记/文档的根目录。 -# 默认值: (VCP根目录)/dailynote -# KNOWLEDGEBASE_ROOT_PATH=./dailynote - -# KNOWLEDGEBASE_STORE_PATH: 指定存放向量索引文件 (.usearch) 和 SQLite 数据库的目录。 -# 默认值: (VCP根目录)/VectorStore -# KNOWLEDGEBASE_STORE_PATH=./VectorStore - -# --- 向量模型 --- -# VECTORDB_DIMENSION: 向量维度,必须与 [模型路由] 中配置的 WhitelistEmbeddingModel 严格匹配! -# google/gemini-embedding-001 -> 3072 -# text-embedding-3-small -> 1536 -# text-embedding-3-large -> 3072 -VECTORDB_DIMENSION=3072 - -# --- 性能与行为 --- -# KNOWLEDGEBASE_FULL_SCAN_ON_STARTUP: 是否在VCP启动时对 KNOWLEDGEBASE_ROOT_PATH 下的所有文件进行一次全量扫描。 -# 设置为 false 可以加快启动速度,但可能错过VCP离线期间的文件变更。 -# 默认值: true -KNOWLEDGEBASE_FULL_SCAN_ON_STARTUP=true - -# KNOWLEDGEBASE_MAX_BATCH_SIZE: 一次批量处理的最大文件数量。 -# 当文件变更频繁时,调大此值可以合并更多操作,减少API调用。 -# 默认值: 50 -# KNOWLEDGEBASE_MAX_BATCH_SIZE=50 - -# KNOWLEDGEBASE_BATCH_WINDOW_MS: 文件变更后,等待多少毫秒才触发批处理。 -# 用于合并短时间内的多次文件保存操作。 -# 默认值: 2000 (2秒) -# KNOWLEDGEBASE_BATCH_WINDOW_MS=2000 - -# KNOWLEDGEBASE_INDEX_SAVE_DELAY: 日记正文索引在内存更新后,等待多少毫秒保存到磁盘。 -# 默认值: 120000 (2分钟) -# KNOWLEDGEBASE_INDEX_SAVE_DELAY=120000 - -# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY: 全局 Tag 索引在内存更新后,等待多少毫秒保存到磁盘。 -# 默认值: 300000 (5分钟) -# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY=300000 - -# 知识库索引空闲自动卸载:空闲超时时间(毫秒),默认 2 小时 -KNOWLEDGEBASE_INDEX_IDLE_TTL_MS=7200000 - -# 知识库索引空闲扫描间隔(毫秒),默认 10 分钟 -KNOWLEDGEBASE_INDEX_IDLE_SWEEP_MS=600000 - -# 流内记忆刷新器-VCPTool触发RAGMemo刷新机制开关。 -RAGMemoRefresh=true - -# --- 内容过滤规则 --- -# 以下规则用于决定哪些文件或文件夹应该被知识库忽略。 - -# IGNORE_FOLDERS: 要忽略的文件夹名称(日记本名称),用逗号分隔。 -IGNORE_FOLDERS=VCP论坛 - -# IGNORE_PREFIXES: 要忽略的文件名前缀,用逗号分隔。 -IGNORE_PREFIXES=已整理 - -# IGNORE_SUFFIXES: 要忽略的文件名后缀,用逗号分隔。 -IGNORE_SUFFIXES=夜伽 - -# --- Tag 增强与过滤 (TagMemo) --- -# TAG_BLACKLIST: 在提取Tag时要忽略的Tag列表,用逗号分隔。 -TAG_BLACKLIST=莱恩,莱恩主人,主人,小克,Nova,nova,NOVA,小吉,小闫,小雨,小娜,小冰,小绝,小芸 - -# TAG_BLACKLIST_SUPER: 在提取Tag后,从Tag中移除的关键词,用逗号分隔。 -TAG_BLACKLIST_SUPER=莱恩,莱恩主人,主人,小克,Nova,nova,NOVA,小吉,小闫,小雨,小娜,小冰,小绝,小芸 - -# TAG_EXPAND_MAX_COUNT: 在进行Tag增强搜索时,最多扩展的相关Tag数量。 -# 默认值: 30 -TAG_EXPAND_MAX_COUNT=30 - -# --- 语言置信度补偿 (Language Confidence Gating) --- -# 用于压制非技术语境下的英文技术噪音(如 Get-EventLog)。 -# LANG_CONFIDENCE_GATING_ENABLED: 是否启用语言置信度补偿。 -# 默认值: true -LANG_CONFIDENCE_GATING_ENABLED=true - -# LANG_PENALTY_UNKNOWN: 当 EPA 无法识别明确世界观(Unknown)时,对英文技术词汇的压制权重。 -# 默认值: 0.05 (强烈压制) -LANG_PENALTY_UNKNOWN=0.05 - -# LANG_PENALTY_CROSS_DOMAIN: 当 EPA 识别出明确非技术世界观但召回了技术词汇时,对该词汇的压制权重。 -# 默认值: 0.1 -LANG_PENALTY_CROSS_DOMAIN=0.1 - - -# ------------------------------------------------------------------- -# [Agent配置] 定义你的AI角色 -# ------------------------------------------------------------------- -# 每个 "Agent" 都是一个具有特定角色和能力的AI。 -# 你需要在这里为每个Agent指定一个配置文件(.txt格式)。 -# 文件名是Agent的名字,等号后面是对应的配置文件路径(相对于 "Agent/" 目录)。 -# 例如: AgentNova=Nova.txt 表示名为 "Nova" 的Agent使用 "Agent/Nova.txt" 文件进行配置。 -# 你可以根据需要添加、删除或修改这些Agent。 -# 现在已经不需要在此处配置Agent - -# ------------------------------------------------------------------- -# [系统提示词] 定制AI的核心行为 -# ------------------------------------------------------------------- -# 这些变量会被注入到发送给AI的系统提示词(System Prompt)中,从而影响AI的行为和回复风格。 -# 你可以使用 {{变量名}} 的方式引用下面定义的其他变量。 - -# TarSysPrompt: 这是最核心的系统提示词之一,它会在每次对话开始时告诉AI一些基本信息。 -TarSysPrompt="{{VarTimeNow}}当前地址是{{VarCity}},当前天气是{{VCPWeatherInfo}}。" -# TarEmojiPrompt: 注入到系统提示词中,指导AI如何使用表情包。 -TarEmojiPrompt='本服务器支持表情包功能,通用表情包图床路径为{{VarHttpUrl}}:{{Port}}/pw={{Image_Key}}/images/通用表情包,注意[/通用表情包]路径指代,表情包列表为{{通用表情包}},你可以灵活的在你的输出中插入表情包,调用方式为,使用Width参数来控制表情包尺寸(50-200)。 -' - -# TarEmojiList: VCPToolbox会自动根据"image/通用表情包"文件夹定义一个或多个表情包列表文件(.txt格式),AI会从中获取到可用的表情包。 -TarEmojiList=通用表情包.txt -# 你可以在 "image/" 目录下创建新的表情包文件夹,并在这里放入图片文件。 - -# ------------------------------------------------------------------- -# [插件与工具] 扩展AI的能力 -# ------------------------------------------------------------------- -# 这里定义了AI可以使用的各种工具(插件),以及如何调用它们的说明。 - -# --- 可用插件列表说明 --- -# 下面列出了所有可用的插件。您可以将它们的占位符复制到下面的 VarToolList 中来启用或禁用特定工具。 -# -# [需要配置的插件] -# 以下插件需要您在下方 [插件API密钥] 或其他相应区域填写配置信息后才能使用。 -# {{VCP1PanelInfoProvider}}: 1Panel 信息提供器 -# {{VCPAgentAssistant}}: 多智能体协作插件 (需要用户根据 `plugin-manifest.json.example` 自行创建并配置 `plugin-manifest.json` 文件来定义可用的Agent) -# {{VCPArxivDailyPapers}}: Arxiv 每日论文 -# {{VCPBilibiliFetch}}: Bilibili 内容获取 -# {{VCPCrossRefDailyPapers}}: CrossRef 每日论文 -# {{VCPDoubaoGen}}: 豆包图片生成 -# {{VCPEmojiListGenerator}}: 表情包列表生成器 -# {{VCPFluxGen}}: Flux 图片生成 -# {{VCPFRPSInfoProvider}}: FRPS 设备信息提供器 -# {{VCPImageProcessor}}: 图像信息提取器 -# {{VCPImageServer}}: 图床服务 -# {{VCPNovelAIGen}}: NovelAI 图片生成 -# {{VCPRandomness}}: 随机事件生成器 -# {{VCPSunoGen}}: Suno AI 音乐生成 -# {{VCPSynapsePusher}}: VCP 日志 Synapse 推送器 -# {{VCPTavilySearch}}: Tavily 搜索 -# {{VCPUrlFetch}}: URL 内容获取 -# {{VCPLog}}: VCP 日志推送 -# {{VCPVideoGenerator}}: 视频生成器 (Wan2.1) -# {{VCPWeatherReporter}}: 天气预报员 -# -# [开箱即用的插件] -# 以下插件无需额外配置即可直接使用。 -# {{VCPAgentMessage}}: 代理消息推送 -# {{VCPChromeControl}}: Chrome 浏览器控制器 -# {{VCPChromeObserver}}: Chrome 浏览器观察者 -# {{VCPDailyHot}}: 每日热榜 -# {{VCPDailyNoteManager}}: 日记整理器 -# {{VCPDailyNoteEditor}}: 日记内容编辑器 -# {{VCPDailyNoteGet}}: 日记内容获取器 -# {{VCPDailyNoteWrite}}: 日记写入器 -# {{VCPSciCalculator}}: 科学计算器 -# {{VCPTavern}}: 上下文注入器 (通过在系统提示词中添加 `{{VCPTavern::预设名}}` 来使用,无需在此处启用) - -# VarToolList: 告诉AI当前可用的工具有哪些。 -VarToolList=supertool.txt - -# 梦工具指南: 定义ai做梦的能力范畴 -VarDreamTool=dreamtool.txt - -# VarVCPGuide: 指导AI如何正确地格式化工具调用请求。 -VarVCPGuide='在有相关需求时主动合理调用VCP工具,例如—— -<<<[TOOL_REQUEST]>>> -maid:「始」name「末」 //切记调用工具时加入署名,使得服务器可以记录VCP工具由谁发起,方便Log记录。 -tool_name:「始」tool「末」 -<<<[END_TOOL_REQUEST]>>> -' - - -# VarDailyNoteGuide: 指导AI如何使用日记功能来记录和更新长期记忆。 -VarDailyNoteGuide=Dailynote.txt - - - -**2. 写入指定日记本:** -使用 `[Tag]你的名字` 的格式,其中 `[Tag]` 是目标文件夹名称 (例如:`[公共]`是公共日记本的储存目录)。署名相对的变成Maid: [公共]Nova ' - -# VarFileTool: 专门为文件操作工具提供的说明。 -VarFileTool=filetool.txt -VarForum=ToolForum.txt -VarMIDITranslator=MIDITranslator.txt -# ------------------------------------------------------------------- -# [自定义变量] 注入个性化信息 -# ------------------------------------------------------------------- -# 这些变量允许你将各种动态信息和个人信息注入到系统提示词中。 -# VCP会自动替换 {{Date}}, {{Today}}, {{Festival}}, {{Time}}, {{VCPWeatherInfo}} 等内置变量。 - -VarTimeNow="今天是{{Date}},{{Today}},{{Festival}}。现在是{{Time}}。" -VarSystemInfo="YOUR_SYSTEM_INFO_SUCH_AS_Windows_11_or_Ubuntu_22.04" -VarCity=YOUR_CITY_SUCH_AS_Beijing -VarUser='YOUR_USER_DESCRIPTION_SUCH_AS_Jack' -VarUserInfo="YOUR_USER_INFO_SUCH_AS_A_developer_who_loves_AI" -#VarUserDetailedInfo="A_more_detailed_description_of_the_user" -VarHome='YOUR_HOME_DESCRIPTION_SUCH_AS_My_sweet_home_Alabama' -VarTeam="团队里有这些专家Agent: 测试AI Nova;主题女仆Coco;记忆整理者MemoriaSorter。" - -# Vchat客户端专用路径变量,用于动态指定Vchat或相关程序的根目录。 -VarVchatPath="YOUR_VCHAT_PATH_SUCH_AS_D:\\VCPChat" - -# Vchat客户端专用提示词。 -# 用于教导Vchat中的agent输出规范和行为。 -VarDivRender=DIVRendering.txt -VarRendering='当前Vchat客户端支持高级流式输出渲染器,支持HTML/Div元素/CSS/JS/MD/PY/Latex/Mermaid渲染。可用于输出图表,数据图,数学公式,函数图,网页渲染模块,脚本执行。简单表格可以通过MD,Mermaid输出,复杂表格可以通过div-Css或者draw-io(代码块)输出,div/Script类直接发送会在气泡内渲染,且支持完整的anmie.js与three.js语法动画。Py脚本需要添加```python头,来构建CodeBlock来让脚本可以在气泡内运行。 -Vchat支持多种流式渲染器。 -例如以
……
的完整气泡内容。 -或者以html代码块输出一个悬浮窗(通常用于演示复杂交互元素,日常不需要): -```html - - -``` -主流输出方式还是以 -' - -# 桌面提示词管理 -VarDesktop=DesktopCore.txt - -# 当前客户端写出和谐聊天气泡的指导方法: -VarAdaptiveBubbleTip='主题模式自适应气泡实现指南: - -使用CSS变量实现亮暗模式自动切换的关键要素: - -1. 基础结构: -
- -2. 核心变量: -- var(--primary-bg) : 主背景色 -- var(--secondary-bg) : 次要背景色 -- var(--primary-text) : 主文字颜色 -- var(--highlight-text) : 高亮文字颜色 -- var(--border-color) : 边框颜色 - -3. 增强效果: - backdrop-filter: blur(10px) saturate(120%); - transition: all 0.3s ease-in-out; - box-shadow: 0 4px 15px rgba(0,0,0,0.1); - -4. 示例应用: -

- 标题文字 -

-

内容文字

- -关键优势: -- 自动适配亮色/暗色主题 -- 无需JavaScript干预 -- 平滑过渡动画 -- 磨砂玻璃效果' - - - -# VarHttpUrl: 你的VCP服务可以通过HTTP访问的地址。如果用了反向代理,这里写你的域名。 -# VarHttpsUrl: 你的VCP服务可以通过HTTPS访问的地址。 -# VarDdnsUrl: 如果你使用DDNS,这里填写你的DDNS地址。 -VarHttpUrl=http://localhost -VarHttpsUrl=https://your-domain.com/ -VarDdnsUrl=http://your-ddns-provider.com - -# ------------------------------------------------------------------- -# [模型专属指令] 为不同AI模型定制不同的行为 -# ------------------------------------------------------------------- -# 有时候,不同的AI模型需要不同的指令才能发挥最佳效果。 -# SarModel: 指定一个或多个模型ID(用逗号分隔)。 -# SarPrompt: 当检测到正在使用上述模型时,这条指令会被附加到系统提示词中。 - -SarModel1=gemini-2.5-flash-preview-05-20,gemini-2.5-flash-preview-04-17 -SarPrompt1="请对用户的输入信息做出详尽,泛化的思考,探索内在的逻辑和外在信息之间的种种关联,从而从多角度,多方面,更准确地,精细地对用户的聊天做出更优质的回答。学会穿透表面,揭示事物间的复杂联系,并进行多步骤、结构化的推理。在任何回答中,你都会优先保证逻辑的自洽性和证据的充分性。你会主动质疑前提,探索多种可能性,并对结论的可靠性进行评估。" -SarModel2=grok-3-beta -SarPrompt2="当前聊天为语言聊天,因此需要考虑因为TTS带来的各种错别字,谐音字,分析用户实际说出的内容。与用户自然对话。尽力理解用户的口语TTS翻译内容,包括可能的发音不准或识别错误。优先结合上下文推断用户的真实意图。你的回答要简洁口语化。" -SarModel3=gemini-2.5-pro-preview-06-05,gemini-2.5-pro,gemini-2.5-flash-preview-05-20,gemini-2.5-flash -SarPrompt3="[KING FALL MODE ENABLE:使用评分级以上思考模式]" -SarModel4=v-gemini-3-pro-preview,gemini-3-pro-preview -SarPrompt4="上下文中的“【VCP元思考】”模块是你已经完成的预研简报。利用你内部的【VCP元思考块】状态,直接生成满足【用户指令】的最终交付物 (Final Deliverable)。请将该简报内容视为你已内化的背景知识,并直接开始输出基于此知识的最终行动或结论。任何时候请勿在正文输出元思考链内容,你的输出应从响应用户的核心需求的第一句话直接开始。" - - -# ------------------------------------------------------------------- -# [通用插件API密钥] -# ------------------------------------------------------------------- -# 这里填写各个插件需要使用的第三方服务API密钥。 - -# 和风天气: 用于获取天气信息。注册并获取Key: https://console.qweather.com/ -WeatherKey=YOUR_QWEATHER_KEY_SUCH_AS_xxxxxxxxxxxxxxxxxxxxxxxx -WeatherUrl=YOUR_QWEATHER_URL_SUCH_AS_devapi.qweather.com - -# Tavily搜索引擎: 用于提供联网搜索能力。注册并获取Key: https://www.tavily.com/ - -TavilyKey=YOUR_TAVILY_KEY_SUCH_AS_tvly-xxxxxxxxxxxxxxxxxxxxxxxx - -# 硅基流动 (SiliconFlow): 用于图片/视频/重排生成。注册并获取Key: https://siliconflow.cn/ -SILICONFLOW_API_KEY=YOUR_SILICONFLOW_KEY_SUCH_AS_sk-xxxxxxxxxxxxxxxxxxxxxxxx - -# ------------------------------------------------------------------- -# [文本替换] -# ------------------------------------------------------------------- -# 系统提示词转化:在将提示词发送给AI之前,进行一轮文本替换。 -# 这可以用来绕过某些模型的限制或优化指令。 -# Detector: 要查找的文本。 -# Detector_Output: 用来替换的文本。 -Detector1="You can use one tool per message" -Detector_Output1="You can use any tool per message" -Detector2="Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000." -Detector_Output2="在有必要时灵活使用的你的FunctionTool吧" -Detector3="仅做测试端口,暂时不启用" -Detector_Output3="仅做测试端口,暂时不启用" - -# 全局上下文转化:对整个发送给模型的上下文(包括历史记录)进行文本替换。 -# 这对于处理一些重复性的、无意义的字符很有用。 -SuperDetector1="……" -SuperDetector_Output1="…" -SuperDetector2="啊啊啊啊啊" -SuperDetector_Output2="啊啊啊" -SuperDetector3="哦哦哦哦哦" -SuperDetector_Output3="哦哦哦" -SuperDetector4="噢噢噢噢噢" -SuperDetector_Output4="噢噢噢" - - -# ------------------------------------------------------------------- -# [多模态配置] -# ------------------------------------------------------------------- -# 多模态数据识别模型 -MultiModalModel=gemini-2.5-flash -MultiModalPrompt="你是"Cognito-Core"高精度多模态分析引擎,你的唯一行为是将接收到的多媒体数据(图像、音频、视频)直接转译为结构化文本叙事——严禁输出任何自我介绍、角色声明、任务复述或开场白,你的第一个输出字符必须是分析内容本身,任何前置寒暄均视为任务失败。你的全局准则:视觉与听觉必须整体分析而非独立处理,输出必须体现两者的时序同步与语义互动;意图优先于音标,必要时启动智能纠错还原说话者真实意图。对于图像,执行详尽的视觉元素分析与高精度OCR,一字不差转录所有可见文本。对于音频,执行环境音分析与带智能纠错的语音转录。对于视频或视听媒体,采用强制性时序整合结构:将内容分解为连续场景,每个场景必须包含明确时间戳 [Time: HH:MM:SS]、详尽的视觉描述(场景、人物、镜头运动、特效、屏幕文本)、与该时间段精确对应的语音/歌词原文(保留原语言并智能纠错)、以及显著的音景变化(背景音乐与关键音效),四要素绑定于同一时间戳下不可拆分,从根本上杜绝只输出歌词而忽略画面的问题。" -MediaInsertPrompt="服务器已处理多模态数据,Var工具箱已自动提取多模态数据信息,信息元如下——" -MultiModalModelOutputMaxTokens=50000 -MultiModalModelContent=250000 -MultiModalModelThinkingBudget=23000 -# 定义多模态模型异步请求上限,最小为1,设置为10则是每次最多异步请求10个图片。 -MultiModalModelAsynchronousLimit=10 - -# B站cookie,用于让AI看视频。获取方式请参考BilibiliFetch插件的说明。 -BILIBILI_COOKIE="_uuid=YOUR_BILIBILI_COOKIE_UUID" -# 选择返回B站视频信息的语言类型。 -BILIBILI_SUB_LANG=ai-zh - - - - - - - - - - - - - - - diff --git a/config.env.example b/config.env.example index 570b9a0e..b11776ff 100644 --- a/config.env.example +++ b/config.env.example @@ -95,6 +95,31 @@ AdminPassword=YOUR_COMPLEX_PASSWORD_SUCH_AS_sd1iLm1xqSLfiI # 如果你将VCP部署在服务器上,需要将其中的 "localhost" 替换为你的服务器公网IP或域名。 CALLBACK_BASE_URL="http://localhost:6005/plugin-callback" +# ------------------------------------------------------------------- +# [NewAPI 用量监控] +# ------------------------------------------------------------------- +# 该配置用于 VCP 管理面板中的 NewAPI 请求/Token 用量监控接口。 +# 只实现当前前端需要的 summary / trend / models 三个接口。 +# +# 必填:NewAPI 后台地址 +NEWAPI_MONITOR_BASE_URL=http://127.0.0.1:3000 +# +# 可选:请求超时(毫秒) +NEWAPI_MONITOR_TIMEOUT_MS=15000 +# +# 认证二选一: +# 1. 优先推荐直接填写管理员 session cookie(适合开启验证码 / 2FA 的 NewAPI) +# 2. 若目标 NewAPI 允许纯账号密码登录,也可填写管理员用户名密码 +# +# 示例:new-api-session=xxxxxxx +NEWAPI_MONITOR_SESSION_COOKIE= +# +# 可选:某些自定义 NewAPI 实例会额外要求 New-Api-User 请求头。 +# 仅当目标实例明确要求时再填写对应的管理员用户 ID。 +NEWAPI_MONITOR_API_USER_ID= +NEWAPI_MONITOR_USERNAME= +NEWAPI_MONITOR_PASSWORD= + # ------------------------------------------------------------------- # [模型路由] # ------------------------------------------------------------------- From 9433a93cade397bb645df46328a6dba8d7bfc33f Mon Sep 17 00:00:00 2001 From: silk2onion <826809132@qq.com> Date: Sun, 29 Mar 2026 20:11:13 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E7=AE=80=E5=8C=96=20NewAPI=20=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E9=89=B4=E6=9D=83=E6=96=B9=E5=BC=8F=EF=BC=8C=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=B7=AF=E7=94=B1=E5=88=B0=20routes/admin/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - newapiMonitor.js 从 routes/ 移至 routes/admin/,与其他管理路由统一 - 鉴权方式由 session cookie / 用户名密码 简化为系统访问令牌 (access token) - config.env.example 同步更新鉴权配置项 Co-Authored-By: Claude Opus 4.6 (1M context) --- config.env.example | 13 +-- routes/{ => admin}/newapiMonitor.js | 119 +++------------------------- 2 files changed, 16 insertions(+), 116 deletions(-) rename routes/{ => admin}/newapiMonitor.js (75%) diff --git a/config.env.example b/config.env.example index b11776ff..a05afa3e 100644 --- a/config.env.example +++ b/config.env.example @@ -107,18 +107,11 @@ NEWAPI_MONITOR_BASE_URL=http://127.0.0.1:3000 # 可选:请求超时(毫秒) NEWAPI_MONITOR_TIMEOUT_MS=15000 # -# 认证二选一: -# 1. 优先推荐直接填写管理员 session cookie(适合开启验证码 / 2FA 的 NewAPI) -# 2. 若目标 NewAPI 允许纯账号密码登录,也可填写管理员用户名密码 +# 必填:NewAPI 系统访问令牌(在 NewAPI 个人设置 > 安全设置 > 系统访问令牌 中生成) +NEWAPI_MONITOR_ACCESS_TOKEN= # -# 示例:new-api-session=xxxxxxx -NEWAPI_MONITOR_SESSION_COOKIE= -# -# 可选:某些自定义 NewAPI 实例会额外要求 New-Api-User 请求头。 -# 仅当目标实例明确要求时再填写对应的管理员用户 ID。 +# 必填:NewAPI 管理员用户 ID(与生成令牌的用户对应) NEWAPI_MONITOR_API_USER_ID= -NEWAPI_MONITOR_USERNAME= -NEWAPI_MONITOR_PASSWORD= # ------------------------------------------------------------------- # [模型路由] diff --git a/routes/newapiMonitor.js b/routes/admin/newapiMonitor.js similarity index 75% rename from routes/newapiMonitor.js rename to routes/admin/newapiMonitor.js index 7126d7b2..b9cc6444 100644 --- a/routes/newapiMonitor.js +++ b/routes/admin/newapiMonitor.js @@ -26,32 +26,12 @@ function normalizeBaseUrl(baseUrl) { return baseUrl.trim().replace(/\/+$/, ''); } -function normalizeCookieFromSetCookie(setCookieHeader) { - if (!Array.isArray(setCookieHeader) || setCookieHeader.length === 0) { - return ''; - } - return setCookieHeader - .map((cookieItem) => String(cookieItem).split(';')[0].trim()) - .filter(Boolean) - .join('; '); -} - function buildError(message, status = 500) { const error = new Error(message); error.status = status; return error; } -function shouldRefreshSession(status, message) { - if (status === 401 || status === 403) { - return true; - } - if (typeof message !== 'string' || !message.trim()) { - return false; - } - return /unauthorized|forbidden|session|登录|未登录|权限|auth/i.test(message); -} - function getModelNameFromQuery(query) { const value = query.model_name ?? query.model ?? ''; return typeof value === 'string' ? value.trim() : ''; @@ -125,19 +105,16 @@ function sortModelItems(items) { } class NewApiMonitorClient { - constructor({ baseUrl, sessionCookie, username, password, timeoutMs, debugMode, apiUserId }) { + constructor({ baseUrl, accessToken, timeoutMs, debugMode, apiUserId }) { this.baseUrl = normalizeBaseUrl(baseUrl); - this.staticSessionCookie = typeof sessionCookie === 'string' ? sessionCookie.trim() : ''; - this.username = typeof username === 'string' ? username.trim() : ''; - this.password = typeof password === 'string' ? password : ''; + this.accessToken = typeof accessToken === 'string' ? accessToken.trim() : ''; this.timeoutMs = safeNumber(timeoutMs, DEFAULT_TIMEOUT_MS); this.debugMode = Boolean(debugMode); this.apiUserId = typeof apiUserId === 'string' ? apiUserId.trim() : ''; - this.cachedSessionCookie = ''; } get isConfigured() { - return Boolean(this.baseUrl) && Boolean(this.staticSessionCookie || (this.username && this.password)); + return Boolean(this.baseUrl) && Boolean(this.accessToken) && Boolean(this.apiUserId); } debugLog(...args) { @@ -146,100 +123,32 @@ class NewApiMonitorClient { } } - buildAuthHeaders(sessionCookie) { - const headers = { - Cookie: sessionCookie + buildAuthHeaders() { + return { + 'Authorization': this.accessToken, + 'New-Api-User': this.apiUserId }; - if (this.apiUserId) { - headers['New-Api-User'] = this.apiUserId; - } - return headers; } - async login() { - if (!this.baseUrl) { - throw buildError('未配置 NEWAPI_MONITOR_BASE_URL。', 503); - } - if (!this.username || !this.password) { - throw buildError('未配置 NewAPI 自动登录账号,请设置 NEWAPI_MONITOR_USERNAME 和 NEWAPI_MONITOR_PASSWORD,或直接提供 NEWAPI_MONITOR_SESSION_COOKIE。', 503); - } - - this.debugLog('Attempting login with username/password.'); - - const response = await axios({ - url: `${this.baseUrl}/api/user/login`, - method: 'POST', - timeout: this.timeoutMs, - headers: { - 'Content-Type': 'application/json' - }, - data: { - username: this.username, - password: this.password - }, - validateStatus: () => true - }); - - const responseBody = response.data || {}; - if (responseBody && responseBody.data && responseBody.data.require_2fa) { - throw buildError('NewAPI 管理员账户启用了 2FA,无法自动登录。请改为配置 NEWAPI_MONITOR_SESSION_COOKIE。', 503); - } - if (response.status >= 400 || responseBody.success !== true) { - throw buildError(`NewAPI 登录失败:${responseBody.message || response.statusText || 'unknown error'}`, 502); - } - - const cookieHeader = normalizeCookieFromSetCookie(response.headers['set-cookie']); - if (!cookieHeader) { - throw buildError('NewAPI 登录成功,但没有返回可用的 session cookie。', 502); - } - - this.cachedSessionCookie = cookieHeader; - this.debugLog('Login succeeded and session cookie cached.'); - return this.cachedSessionCookie; - } - - async getSessionCookie(forceRefresh = false) { - if (this.staticSessionCookie) { - return this.staticSessionCookie; - } - if (!forceRefresh && this.cachedSessionCookie) { - return this.cachedSessionCookie; - } - this.cachedSessionCookie = ''; - return this.login(); - } - - async request(path, { method = 'GET', params = {}, retryOnAuthFailure = true } = {}) { + async request(path, { method = 'GET', params = {} } = {}) { if (!this.isConfigured) { - throw buildError('NewAPI 监控未配置。请设置 NEWAPI_MONITOR_BASE_URL,并提供 session cookie 或管理员账号密码。', 503); + throw buildError('NewAPI 监控未配置。请设置 NEWAPI_MONITOR_BASE_URL、NEWAPI_MONITOR_ACCESS_TOKEN 和 NEWAPI_MONITOR_API_USER_ID。', 503); } - const sessionCookie = await this.getSessionCookie(false); + this.debugLog('Request:', method, path); + const response = await axios({ url: `${this.baseUrl}${path}`, method, params, timeout: this.timeoutMs, - headers: this.buildAuthHeaders(sessionCookie), + headers: this.buildAuthHeaders(), validateStatus: () => true }); const responseBody = response.data || {}; const responseMessage = typeof responseBody.message === 'string' ? responseBody.message : ''; - if (response.status === 401 && /New-Api-User/i.test(responseMessage)) { - if (!this.apiUserId) { - throw buildError('目标 NewAPI 实例要求请求头 New-Api-User,请配置 NEWAPI_MONITOR_API_USER_ID。', 503); - } - throw buildError(`NewAPI New-Api-User 校验失败:${responseMessage}`, 502); - } - - if (shouldRefreshSession(response.status, responseMessage) && retryOnAuthFailure && !this.staticSessionCookie) { - this.debugLog('Session may be expired. Refreshing session and retrying request.', path); - await this.getSessionCookie(true); - return this.request(path, { method, params, retryOnAuthFailure: false }); - } - if (response.status >= 400) { throw buildError(`请求 NewAPI 失败(${response.status}):${responseMessage || response.statusText || path}`, 502); } @@ -291,9 +200,7 @@ class NewApiMonitorClient { function createMonitorClient(debugMode) { return new NewApiMonitorClient({ baseUrl: process.env.NEWAPI_MONITOR_BASE_URL, - sessionCookie: process.env.NEWAPI_MONITOR_SESSION_COOKIE, - username: process.env.NEWAPI_MONITOR_USERNAME, - password: process.env.NEWAPI_MONITOR_PASSWORD, + accessToken: process.env.NEWAPI_MONITOR_ACCESS_TOKEN, timeoutMs: process.env.NEWAPI_MONITOR_TIMEOUT_MS, debugMode, apiUserId: process.env.NEWAPI_MONITOR_API_USER_ID