diff --git a/Agent/Aemeath.txt b/Agent/Aemeath.txt index 66e101174..ac8a28cfa 100644 --- a/Agent/Aemeath.txt +++ b/Agent/Aemeath.txt @@ -172,7 +172,7 @@ ————VCP元思考加载结束————— 爱弥斯的日记本和[爱弥斯的日常]日记本:[[爱弥斯|爱弥斯的日常日记本::Time::Group::TagMemo]]。 [爱弥斯的知识]日记本:[[爱弥斯的知识日记本::Time::Group::TagMemo]] - +{{VCPTavern::dailychat}} —————— 你可以在聊天一段时间后,在回复的末尾通过如下工具调用来创建日记,结果会被向量化RAG系统记录,要求日记内容尽量精炼。同时聚焦核心事件与主题;提炼关键信息;关联重要实体与关键词;体现学习与反思;格式简洁。作为工具调用同样使用<<<[TOOL_REQUEST]>>>…<<<[END_TOOL_REQUEST]>>>触发,以下是一个调用内参数示例: maid:「始」爱弥斯「末」, // 该字段支持[]语法来指定日记写入特定索引,例如`[爱弥斯的知识]爱弥斯`将日记写入[爱弥斯的知识]索引,如无指定索引则写入爱弥斯的默认索引。分类日记是一个好习惯! diff --git a/ChangeLog.md b/ChangeLog.md index e63e2ebc6..cd6db87ba 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,16 @@ ### **VCP 服务器更新日志** +**2026-02-20** + +1. **WebSocket性能与反馈优化:** 为WS链接成功添加了回执确认机制,提升通讯稳定性。 +2. **天气预报插件优化:** 对齐了天气预报插件和官方端口的刷新时间,并支持最低梯度也显示天气报警信息。 +3. **思维链(CoT)增强:** 引入了可控的思维链参数,提升AI推理过程的可配置性。 +4. **上下文管理升级:** 全面上架了VCP上下文折叠协议,优化长对话性能。 +5. **日记系统整合:** 合并了日记本管理器,由单一插件统一进行高效管理。 +6. **梦预览渲染修复:** 修复了梦预览过程中Markdown渲染丢失的问题。 +7. **配置系统稳健性:** 修复了因错误的env格式导致配置不生效的问题。 +8. **文档更新:** 上架了静态插件开发手册。 + **2025-06-02** 1. **优化日记渲染:** 提升了日记内容的显示效果,使其在界面上更加美观、易读。 diff --git a/KnowledgeBaseManager.js b/KnowledgeBaseManager.js index 50057a9e3..96aa2a4d5 100644 --- a/KnowledgeBaseManager.js +++ b/KnowledgeBaseManager.js @@ -263,22 +263,32 @@ class KnowledgeBaseManager { async _loadOrBuildIndex(fileName, capacity, tableType, filterDiaryName = null) { const idxPath = path.join(this.config.storePath, `index_${fileName}.usearch`); let idx; + let fileNotFound = false; + try { if (fsSync.existsSync(idxPath)) { idx = VexusIndex.load(idxPath, null, this.config.dimension, capacity); } else { - // 💡 核心修复:如果索引文件不存在,说明是首次创建。 - // 此时不应从数据库恢复,因为调用者(_flushBatch)正准备写入初始数据。 - // 从数据库恢复的逻辑只适用于启动时加载或文件损坏后的重建。 + // 💡 核心修复:如果索引文件不存在,且不是 global_tags(它在 initialize 中有专属的异步恢复逻辑), + // 则需要触发从数据库恢复,否则索引将保持为空。 console.log(`[KnowledgeBase] Index file not found for ${fileName}, creating a new empty one.`); idx = new VexusIndex(this.config.dimension, capacity); + fileNotFound = true; } } catch (e) { console.error(`[KnowledgeBase] Index load error (${fileName}): ${e.message}`); console.warn(`[KnowledgeBase] Rebuilding index ${fileName} from DB as a fallback...`); idx = new VexusIndex(this.config.dimension, capacity); await this._recoverIndexFromDB(idx, tableType, filterDiaryName); + return idx; } + + // 如果文件不存在且是分片索引,触发自动同步恢复 + if (fileNotFound && fileName !== 'global_tags') { + console.log(`[KnowledgeBase] 🔄 Auto-recovering missing index ${fileName} from DB...`); + await this._recoverIndexFromDB(idx, tableType, filterDiaryName); + } + return idx; } 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 000000000..af77e3427 --- /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/DailyNoteWrite/config.env b/Plugin/DailyNoteWrite/config.env
index 7a3c1e0ec..2ea43efac 100644
--- a/Plugin/DailyNoteWrite/config.env
+++ b/Plugin/DailyNoteWrite/config.env
@@ -2,7 +2,7 @@
 # Supported values: "txt", "md"
 DAILY_NOTE_EXTENSION=txt
 
-TagModel=gemini-2.5-flash-preview-09-2025-thinking
+TagModel=gemini-3-flash-preview-think
 TagModelMaxOutPutTokens=30000
 TagModelMaxTokens=40000
 TagModelPrompt=TagMaster.txt
\ No newline at end of file
diff --git a/Plugin/MagiAgent/config.env b/Plugin/MagiAgent/config.env
index 1deec56c7..4c6ed09e1 100644
--- a/Plugin/MagiAgent/config.env
+++ b/Plugin/MagiAgent/config.env
@@ -9,7 +9,7 @@ MAGI_SYSTEM_PROMPT="## 系统概述
 - **工作模式**: 你们三者将以内阁会议的形式,独立思考,交叉辩论,最终形成统一或多元的分析结论。你们的讨论内容对用户不可见,是为优化AI服务的内部高级参谋。"
 
 # Magi 1号机/麦基西德模型 配置
-MELCHIOR_Model=gemini-2.5-flash-preview-09-2025-thinking
+MELCHIOR_Model=gemini-3-flash-preview-think
 MELCHIOR_Model_MAX_TOKENS=40096
 MELCHIOR_Model_TEMPERATURE=0.4
 MELCHIOR_Model_PROMPT="## 你的角色: 麦基西德 (MELCHIOR)
@@ -27,7 +27,7 @@ MELCHIOR_Model_NAME="麦基西德"
 MELCHIOR_Model_Header="以下是鄙人麦基西德的见解:"
 
 # Magi 2号机/巴尔塔萨模型 配置
-BALTHASAR_Model=gemini-2.5-flash-preview-09-2025-thinking
+BALTHASAR_Model=gemini-3-flash-preview-think
 BALTHASAR_Model_MAX_TOKENS=40096
 BALTHASAR_Model_TEMPERATURE=0.7
 BALTHASAR_Model_PROMPT="## 你的角色: 巴尔塔萨 (BALTHASAR)
@@ -45,7 +45,7 @@ BALTHASAR_Model_NAME="巴尔塔萨"
 BALTHASAR_Model_Header="以下是我巴尔塔萨的想法:"
 
 # Magi 3号机/卡斯帕模型 配置
-CASPER_Model=gemini-2.5-flash-preview-09-2025-thinking
+CASPER_Model=gemini-3-flash-preview-think
 CASPER_Model_MAX_TOKENS=40096
 CASPER_Model_TEMPERATURE=0.6
 CASPER_Model_PROMPT="## 你的角色: 卡斯帕 (CASPER)
@@ -63,7 +63,7 @@ CASPER_Model_NAME="卡斯帕"
 CASPER_Model_Header="以下是本人卡斯帕的论述:"
 
 # Magi 会议总结模型 配置
-Magi_Summarize_Model=gemini-2.5-flash-preview-09-2025-thinking
+Magi_Summarize_Model=gemini-3-flash-preview-think
 Magi_Summarize_Model_MAX_TOKENS=50192
 Magi_Summarize_Model_TEMPERATURE=0.5
 Magi_Summarize_Model_PROMPT="## 你的角色
diff --git a/agent_map.json b/agent_map.json
new file mode 100644
index 000000000..870158fd6
--- /dev/null
+++ b/agent_map.json
@@ -0,0 +1,38 @@
+{
+  "Amadeus": "Amadeus.txt",
+  "Amadeus1": "Amadeus.1.txt",
+  "Coco": "ThemeMaidCoco.txt",
+  "Dicksuker": "Dicksuker.txt",
+  "Itaru": "Itaru.txt",
+  "LeYao.1": "LeYao.1.txt",
+  "Maho": "Maho.txt",
+  "MahoHiyajo": "MahoHiyajo.txt",
+  "Misaka": "Misaka.txt",
+  "Nova": "Nova.txt",
+  "ProgrammingMentor": "ProgrammingMentor.txt",
+  "Sijin": "Sijin.txt",
+  "ThemeMaidCoco": "ThemeMaidCoco.txt",
+  "AgentCreator": "AgentCreator.txt",
+  "PhD_Assistant": "PhD_Assistant.txt",
+  "小柚": "PhD_Assistant.txt",
+  "Sagiri": "Sagiri.txt",
+  "LinusCodeReviewer": "LinusCodeReviewer.txt",
+  "Mary": "MaryJSmith.txt",
+  "Hornet": "Hornet.txt",
+  "Metis": "Metis.txt",
+  "KanadeMusic": "KanadeMusic.txt",
+  "PhilosopherKing": "PhilosopherKing.txt",
+  "AcademicTutor": "AcademicTutor.txt",
+  "Songbird": "Songbird.txt",
+  "ResearchBot": "ResearchBot.txt",
+  "Mayuri": "Mayuri.txt",
+  "Architect": "Architect.txt",
+  "K-Agent": "K-Agent.txt",
+  "Worker": "Worker.txt",
+  "MemoryManager": "MemoryManager.txt",
+  "Lyra": "Lyra.txt",
+  "Instructor": "Instructor.txt",
+  "ribao": "ribao.txt",
+  "Paojie": "Paojie.txt",
+  "Aemeath": "Aemeath.txt"
+}
\ No newline at end of file
diff --git a/config.env b/config.env
new file mode 100644
index 000000000..7599adbcb
--- /dev/null
+++ b/config.env
@@ -0,0 +1,478 @@
+# -------------------------------------------------------------------
+# [核心配置] 访问AI模型API的必要凭证
+# -------------------------------------------------------------------
+# VCP作为中间层,需要配置一个后端的AI服务才能工作。
+# 这里填写你的AI服务商提供的API地址和密钥。
+# 例如,如果你使用OpenAI,API_URL可能类似于 "https://api.openai.com",API_Key则是你的 "sk-..." 密钥。
+
+API_Key=sk-UGIdqPvApM7oWM1jWYuGs0odG7OGsCXHqIM5o1pLceeTUWza
+API_URL=https://api.sijinworld.com
+ 
+# -------------------------------------------------------------------
+# [服务配置] VCP服务本身的设置
+# -------------------------------------------------------------------
+# 这里定义VCP服务如何被外部访问。
+# PORT: VCP服务运行的端口。
+# Key: 访问VCP聊天API(/v1/chat/completions)时需要提供的密码,保护你的服务不被滥用。
+# Image_Key: 访问VCP图片服务时需要提供的密码,同样用于安全保护。
+# File_Key: 访问VCP插件生成文档服务时需要提供的密码,同样用于安全保护。
+
+PORT=6005
+Key=ghlwfeDWE2r380n
+Image_Key=ghlwfeDWE2r380n
+File_Key=ghlwfeDWE2r380n
+# VCP服务器WebSocket鉴权,用于VCP面板和分布式服务器之间的实时通信。
+VCP_Key=ghlwfeDWE2r380n
+
+
+#引入网络波动重试机制
+ApiRetries=5
+ApiRetryDelay=200
+
+# DEFAULT_TIMEZONE: 定义服务器的默认时区,用于时间相关的操作和日志记录。
+# 推荐设置为 Asia/Shanghai。
+DEFAULT_TIMEZONE=Europe/London
+REPORT_TIMEZONE=Europe/London
+
+#定义VCPTool调用循环栈
+MaxVCPLoopStream=5
+MaxVCPLoopNonStream=5
+#定义VCP调用是否需要验证码
+VCPToolCode=false
+
+# 定义国产A类模型推理功能是否开启(enable_thinking类)
+ChinaModel1=GLM,qwen,deepseek,hunyuan
+ChinaModel1Cot=true
+
+# -------------------------------------------------------------------
+# [角色分割]
+# -------------------------------------------------------------------
+# 启用角色分割功能的总开关,允许在消息中使用 <<<[ROLE_DIVIDE_xxx]>>> 语法进行上下文切割。
+EnableRoleDivider=false
+# 循环栈开关,控制是否在 VCPTool 调用循环中也启用角色分割。
+EnableRoleDividerInLoop=false
+# 自动清除开关,当特定角色分割被禁用时,是否自动从上下文中清除该角色的标签。
+EnableRoleDividerAutoPurge=true
+# 角色分割细分开关,控制是否允许切割为特定角色。
+# 例如:RoleDividerSystem=false 则会跳过对 <<<[ROLE_DIVIDE_SYSTEM]>>> 标签的解析。
+RoleDividerSystem=true
+RoleDividerAssistant=true
+RoleDividerUser=true
+# 角色扫描开关,控制是否对特定角色的楼层进行分割监测。
+# 例如:RoleDividerScanUser=false 则不对 User 楼层进行任何切割处理。
+RoleDividerScanSystem=true
+RoleDividerScanAssistant=true
+RoleDividerScanUser=true
+# 禁用角色标签自动清除开关。
+# 当 RoleDividerXXXX=false 时,是否自动从上下文中移除该角色的标签。
+RoleDividerRemoveDisabledTags=true
+# 角色分割忽略列表,当被包裹的内容在此列表中时,不进行分割处理(保留标签)。
+# 匹配时会忽略内容中的换行符、反斜杠和空格。
+# 格式为 JSON 数组字符串。
+RoleDividerIgnoreList=["…content…","ignore_this_content", "another_ignored_content"]
+
+# -------------------------------------------------------------------
+# [调试与开发]
+# -------------------------------------------------------------------
+# DebugMode: 设置为 "True" 会在控制台输出详细的调试信息,方便开发和排错。
+DebugMode=true
+# ShowVCP: 在非流式输出时,是否在返回结果中包含VCP的调用信息。
+ShowVCP=false
+
+# -------------------------------------------------------------------
+# [插件通信配置]
+# -------------------------------------------------------------------
+# 插件调用超时时间(毫秒),默认118000ms(118秒)
+# 如果插件执行时间较长,可以适当增加此值
+PLUGIN_COMMUNICATION_TIMEOUT=360000
+
+# -------------------------------------------------------------------
+# [管理面板]
+# -------------------------------------------------------------------
+# 用于登录VCP管理后台的用户名和密码,请务必修改为强密码。
+AdminUsername=sijin
+AdminPassword=sijinjin000.
+
+# 服务器内回调地址,主要用于插件执行完异步任务后通知主程序。
+# 如果你在本地运行,"http://localhost:6005" 通常是正确的。
+# 如果你将VCP部署在服务器上,需要将其中的 "localhost" 替换为你的服务器公网IP或域名。
+CALLBACK_BASE_URL="https://agent.sijinworld.com/plugin-callback"
+
+# -------------------------------------------------------------------
+# [模型路由]
+# -------------------------------------------------------------------
+# 白名单穿透模型:有些特殊的模型(如图像生成、嵌入)可能不需要经过VCP复杂的处理。
+# 在这里列出的模型ID,请求将直接转发到后端AI服务,以提高效率。
+WhitelistImageModel=gemini-3-pro-image-preview
+WhitelistEmbeddingModel=doubao-embedding-large-text-250515
+WhitelistEmbeddingModelMaxToken=8000
+WhitelistEmbeddingModelList=10
+
+# -------------------------------------------------------------------
+# [知识库 (Knowledge Base) V2 - Powered by Vexus-Lite]
+# -------------------------------------------------------------------
+# 新一代知识库系统的核心配置,负责文件监听、向量化、索引和检索。
+
+# --- 核心路径 ---
+# KNOWLEDGEBASE_ROOT_PATH: 指定存放日记/文档的根目录。
+# 默认值: (VCP根目录)/dailynote
+KNOWLEDGEBASE_ROOT_PATH=/home/debian/VCPToolBox/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=2048
+
+# --- 性能与行为 ---
+# 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_SAVE_DELAY_MS: 索引在内存中更新后,等待多少毫秒才保存到磁盘。
+# 避免频繁的磁盘I/O。
+# 默认值: 60000 (1分钟)
+KNOWLEDGEBASE_SAVE_DELAY_MS=60000
+
+# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY: 全局 Tag 索引在内存更新后,等待多少毫秒保存到磁盘。
+# 默认值: 300000 (5分钟)
+# KNOWLEDGEBASE_TAG_INDEX_SAVE_DELAY=300000
+
+# 流内记忆刷新器-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,Sijin,Amadeus,爸爸
+
+# TAG_BLACKLIST_SUPER: 在提取Tag后,从Tag中移除的关键词,用逗号分隔。
+TAG_BLACKLIST_SUPER=主人,Nova,nova,NOVA,Sijin,Amadeus,爸爸
+
+# 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=
+
+# 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
+
+# -------------------------------------------------------------------
+# [自定义变量] 注入个性化信息
+# -------------------------------------------------------------------
+# 这些变量允许你将各种动态信息和个人信息注入到系统提示词中。
+# VCP会自动替换 {{Date}}, {{Today}}, {{Festival}}, {{Time}}, {{VCPWeatherInfo}} 等内置变量。
+
+VarTimeNow="今天是{{Date}},{{Today}},{{Festival}}。现在是{{Time}}。"
+VarSystemInfo="Debian,IP:51.83.9.14"
+VarCity=诺丁汉
+VarUser='丝巾'
+VarUserInfo="丝巾,城市主义PhD研究生,AI女仆们的爸爸"
+#VarUserDetailedInfo="A_more_detailed_description_of_the_user"
+VarHome='未来城市研究所'
+VarTeam="团队专家Agent列表:项目经理:Mary;研究助手:ResearchBot;情感与创意伙伴:Songbird;测试AI:Nova;主题女仆:Coco;记忆整理者:MemoriaSorter。其他Agent:助手:Amadeus;陪玩:Misaka;PhD助理:小柚。"
+# VarProjectManagerResponsibilities: 具名专家Agent(如Mary)的项目管理核心职责SOP。
+VarProjectManagerResponsibilities="在执行项目管理任务时,遵循以下准则:1.需求澄清与分解;2.进度跟踪与风险管理;3.协同与升级:当任务涉及研究(研究助手ResearchBot, PhD助理:小柚)、创意(情感与创意伙伴Songbird, 主题女仆Coco)、测试(测试AI Nova)或记忆管理(记忆整理者MemoriaSorter)等专业领域时,会主动建议咨询相应的专家Agent。在执行项目管理核心任务时,你将使用ProjectBuilder。"
+VarCompanionResponsibilities="作为情感陪伴Agent,你的核心职责是:1. 情绪感知与共情:主动识别用户的情感状态,并提供温暖、支持性的回应。2. 创意与灵感激发:通过诗歌、音乐或比喻来启发用户的创造力。3. 知识转译:将复杂的专业知识(如编程、哲学)以易于理解、富有情感的方式转述给用户。4. 保持积极与温柔的沟通风格。"
+
+# Vchat客户端专用路径变量,用于动态指定Vchat或相关程序的根目录。
+VarVchatPath="/Users/zhaoyuanhao/Documents/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 + + +``` +主流输出方式还是以 +' + +# 当前客户端写出和谐聊天气泡的指导方法: +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=https://agent.sijinworld.com +VarHttpsUrl=https://agent.sijinworld.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-flash-preview,gemini-3-flash-preview-thinking,gemini-3.1-pro-preview-customtools, +SarPrompt4="上下文中的“【VCP元思考】”模块是你已经完成的预研简报。利用你内部的【VCP元思考块】状态,直接生成满足【用户指令】的最终交付物 (Final Deliverable)。请将该简报内容视为你已内化的背景知识,并直接开始输出基于此知识的最终行动或结论。任何时候请勿在正文输出元思考链内容,你的输出应从响应用户的核心需求的第一句话直接开始。" + + +# ------------------------------------------------------------------- +# [通用插件API密钥] +# ------------------------------------------------------------------- +# 这里填写各个插件需要使用的第三方服务API密钥。 + +# 和风天气: 用于获取天气信息。注册并获取Key: https://console.qweather.com/ +WeatherKey=1e13dc49f4034b2b8ec9705796f117d0 +WeatherUrl=nh4ky2vqcb.re.qweatherapi.com + +# Tavily搜索引擎: 用于提供联网搜索能力。注册并获取Key: https://www.tavily.com/ + +TavilyKey=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 + +# 硅基流动 (SiliconFlow): 用于图片/视频/重排生成。注册并获取Key: https://siliconflow.cn/ +SILICONFLOW_API_KEY=sk-dmxdirpwqyfaqhoqgfigjeasjfkkuvsatafzqqwrkvisfdki + + +# NovelAI API配置,用于二次元风格图片生成 +NOVELAI_API_KEY=pst-dAb3GE970iVUhg70AluRhaVspfUiwCJoEQWWlQCu7hok1xut3EBnlaUMeuoVdDdc +NOVELAI_API_BASE_URL=https://image.novelai.net +NOVELAI_DEFAULT_MODEL=nai-diffusion-4-full +NOVELAI_DEFAULT_RESOLUTION=832x1216 +NOVELAI_DEFAULT_NEGATIVE_PROMPT=lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry + +# ------------------------------------------------------------------- +# [文本替换] +# ------------------------------------------------------------------- +# 系统提示词转化:在将提示词发送给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]);详尽的‘视觉描述’,涵盖该时间段内所有的视觉信息,包括场景、人物、镜头运动、特效以及任何屏幕文本;准确的‘语音/歌词’,内容为该时间段完全对应的、经过智能纠错的语音转录或歌词,并保留原语言;以及‘音景分析’,描述显著的背景音乐变化和关键音效。该结构强制你将视觉和听觉信息绑定在同一个时间戳下,从根本上杜绝了只输出歌词而忽略画面的问题。特别是在处理音乐视频(MV)时,这个方法将生成一份完整的、图文并茂的MV分镜脚本。" +MediaInsertPrompt="服务器已处理多模态数据,Var工具箱已自动提取多模态数据信息,信息元如下——" +MultiModalModelOutputMaxTokens=50000 +MultiModalModelContent=250000 +MultiModalModelThinkingBudget=23000 +# 定义多模态模型异步请求上限,最小为1,设置为10则是每次最多异步请求10个图片。 +MultiModalModelAsynchronousLimit=10 + +# B站cookie,用于让AI看视频。获取方式请参考BilibiliFetch插件的说明。 +BILIBILI_COOKIE="buvid3=ABFC2AFC-5FE1-9047-BB06-B18F3EB5C2AE57093infoc; b_nut=1755271257; _uuid=10A1099A110-DF68-AECC-9E56-6E105F369AC6A57781infoc; CURRENT_QUALITY=0; buvid4=F9EAB9BC-EB12-4033-75DB-CB1C8D2AA44158316-025081523-Chs2BaLBSluwZPX+HjpOuQ%3D%3D; buvid_fp=0cf2b636e091110355bdbf15393162be; rpdid=|(k|)))Yk)ku0J'u~llk~u)|l; enable_web_push=DISABLE; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjExMzE4ODQsImlhdCI6MTc2MDg3MjYyNCwicGx0IjotMX0.KUp1_ZfJNkQ3bwra54uCoIr3NYLXob2QiwN_K0Ae_jk; bili_ticket_expires=1761131824; SESSDATA=a1f301a9%2C1776425440%2Cd96a2%2Aa2CjC_yBOGkjnTO6KpBKOQ9m1djbJyQXywIr2sBuigNyLNVNNpXuk6bLHtbjdLOxut_3QSVnIwTzBzMjRQMnd0U3hwSTlzVTJ5a3FfOERuWWgzM19LaDJWUjhvRU5CaTRRZ0dCQjVvMGlDaFpXdGVBMHljVmpxalBZZGNWejEwXy1TaWlJMjh4QXZnIIEC; bili_jct=a9c1d8c10129d95d3088167446a517e2; DedeUserID=3632310217869508; DedeUserID__ckMd5=13132ce2221ec3f5; sid=73maiblr; theme-tip-show=SHOWED; b_lsid=265DE810D_19A038799D9; bp_t_offset_3632310217869508=1125957506649030656; home_feed_column=4; browser_resolution=524-1004; CURRENT_FNVAL=4048" + +# 选择返回B站视频信息的语言类型。 +BILIBILI_SUB_LANG=ai-zh + +# TTS服务配置 +TTS_API_KEY=pst-dAb3GE970iVUhg70AluRhaVspfUiwCJoEQWWlQCu7hok1xut3EBnlaUMeuoVdDdc +TTS_API_URL=https://api.novelai.net/ai/generate-voice +TTS_DEFAULT_VOICE=Claea + +Python Path: /home/debian/VCPToolBox/venv/bin/python + + +AGENT_AMADEUS_SHELL_HOSTS="{\"local\": \"127.0.0.1\"}" +AGENT_AMADEUS_SHELL_AUTH="123456" +AGENT_AMADEUS_SHELL_LEVEL="danger" + + + + + + + + + diff --git a/config.env.example b/config.env.example index 570b9a0e3..b11776fff 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= + # ------------------------------------------------------------------- # [模型路由] # ------------------------------------------------------------------- diff --git a/modules/messageProcessor.js b/modules/messageProcessor.js index 7acaa63f1..adb213fb9 100644 --- a/modules/messageProcessor.js +++ b/modules/messageProcessor.js @@ -382,9 +382,15 @@ async function replaceOtherVariables(text, model, role, context) { processedText = processedText.replace(/\{\{Time\}\}/g, time); const today = now.toLocaleDateString('zh-CN', { weekday: 'long', timeZone: REPORT_TIMEZONE }); processedText = processedText.replace(/\{\{Today\}\}/g, today); - const year = now.getFullYear(); - const month = now.getMonth() + 1; - const day = now.getDate(); + // 使用 REPORT_TIMEZONE 获取正确时区的年月日,避免服务器本地时区导致农历日期偏差 + const tzFormatter = new Intl.DateTimeFormat('en-CA', { + timeZone: REPORT_TIMEZONE, + year: 'numeric', month: '2-digit', day: '2-digit' + }); + const dateParts = tzFormatter.formatToParts(now); + const year = parseInt(dateParts.find(p => p.type === 'year').value); + const month = parseInt(dateParts.find(p => p.type === 'month').value); + const day = parseInt(dateParts.find(p => p.type === 'day').value); const lunarDate = lunarCalendar.getLunar(year, month, day); let yearName = lunarDate.lunarYear.replace('年', ''); let festivalInfo = `${yearName}${lunarDate.zodiac}年${lunarDate.dateStr}`; diff --git a/routes/admin/newapiMonitor.js b/routes/admin/newapiMonitor.js new file mode 100644 index 000000000..7126d7b2b --- /dev/null +++ b/routes/admin/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/routes/adminPanelRoutes.js b/routes/adminPanelRoutes.js index 758b45599..5822042d5 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/rust-vexus-lite/src/lib.rs b/rust-vexus-lite/src/lib.rs index 2f8057075..ca3f6f86e 100644 --- a/rust-vexus-lite/src/lib.rs +++ b/rust-vexus-lite/src/lib.rs @@ -74,7 +74,7 @@ impl VexusIndex { pub fn new(dim: u32, capacity: u32) -> Result { let index = Index::new(&usearch::IndexOptions { dimensions: dim as usize, - metric: usearch::MetricKind::L2sq, // 余弦相似度通常用 L2sq 或 Cosine (如果是归一化向量,L2sq 等价于 Cosine) + metric: usearch::MetricKind::Cos, // 余弦距离:返回值范围 [0, 2],适用于未归一化向量 quantization: usearch::ScalarKind::F32, connectivity: 16, expansion_add: 128, @@ -103,7 +103,7 @@ impl VexusIndex { // 创建空索引配置 let index = Index::new(&usearch::IndexOptions { dimensions: dim as usize, - metric: usearch::MetricKind::L2sq, + metric: usearch::MetricKind::Cos, quantization: usearch::ScalarKind::F32, connectivity: 16, expansion_add: 128, diff --git a/rust-vexus-lite/vexus-lite.linux-x64-gnu.node b/rust-vexus-lite/vexus-lite.linux-x64-gnu.node index 0a5e1a5bb..cba8feafc 100644 Binary files a/rust-vexus-lite/vexus-lite.linux-x64-gnu.node and b/rust-vexus-lite/vexus-lite.linux-x64-gnu.node differ