mingdao-harness 0.1.65 → 0.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/CONFIG.md +5 -1
- package/docs/EVALUATION-SYNTHESIS.md +85 -0
- package/docs/QA-REPORT.md +1 -1
- package/package.json +1 -1
- package/src/agent.js +9 -1
- package/src/cli.js +21 -2
- package/src/commands/update.js +10 -0
- package/src/context.js +28 -11
- package/src/memory.js +1 -1
- package/src/models.js +11 -5
- package/src/pricing.js +89 -21
- package/src/prompts.js +8 -7
- package/src/providers/openai-compatible.js +2 -1
- package/src/routing.js +19 -2
- package/src/schedule.js +4 -3
- package/src/tasks.js +1 -1
- package/src/titles.js +2 -0
- package/src/web/index.html +248 -40
- package/src/web/server.js +22 -7
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## 为什么选 MingDao
|
|
8
8
|
|
|
9
9
|
- ⚡ **真正的零依赖**:纯 Node.js ≥ 18,无任何 npm 运行时依赖、无构建步骤——装完即用,不拖 node_modules
|
|
10
|
-
- 💰 **DeepSeek-V4 深度省钱**:384K
|
|
10
|
+
- 💰 **DeepSeek-V4 深度省钱**:1M 上下文预设(单次输出上限 384K)、**缓存命中计价**(命中价仅为未命中的 1/30)+ 命中率仪表盘、峰谷计价(高峰=北京工作日 9:00–12:00、14:00–18:00,闲时半价自动识别)、自动路由(pro 规划 / flash 执行 + 分类缓存 + 会话粘滞)、精确 tokenizer(官方词表 BPE)、滞回自动压缩、**Batch API 半价批处理**(`mingdao batch`)、**避峰调度**(`--offpeak` 高峰顺延到最近闲时 12:00/18:00)、**费用护栏**(每日上限防超支)
|
|
11
11
|
- 🖥 **双界面 + IDE 全家桶**:产品级 TUI(流式 Markdown、代码高亮、编辑 diff、Ctrl+C 中断、Tab 补全)与 `mingdao web` 一键 WebUI(PWA 可装桌面、多任务并行、全项设置面板);VS Code 侧边栏与 JetBrains 工具窗深度集成
|
|
12
12
|
- 🧠 **36 个技能开箱即用**:14 个内置常驻 + 22 个可安装技能库(线上 registry 逐文件 sha256 校验防供应链篡改,`mingdao skill install sql` 一键装,可自建企业内 registry)
|
|
13
13
|
- 🔌 **生态即插即用**:MCP 客户端(零依赖实现,`mcpServers` 配置即接入任意 MCP 服务器)+ Hooks 钩子 + 9 个 MCP 生态预设
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -89,9 +89,9 @@ PreToolUse(stdin 收 JSON,stdout 输出 `{decision:"block", reason}` 可阻
|
|
|
89
89
|
|
|
90
90
|
## 5. 面向 DeepSeek-V4 的优化设计
|
|
91
91
|
|
|
92
|
-
1. **预设调优**:v4-pro(推理/规划:温度 0.4、输出上限 32k、预算 200k);v4-flash(日常:温度 0.6、输出 8k、预算 128k);两者 `contextWindow` 均为 384K
|
|
92
|
+
1. **预设调优**:v4-pro(推理/规划:温度 0.4、输出上限 32k、预算 200k);v4-flash(日常:温度 0.6、输出 8k、预算 128k);两者 `contextWindow` 均为 1M(官方规格;384K 是单次最大输出,字段 `maxOutputCeiling`),预算可随时调高。
|
|
93
93
|
2. **推理内容流式展示**:`reasoning_content` 以暗色增量渲染,与正文同流。
|
|
94
|
-
3. **峰谷定价适配**:每轮后展示 prompt/completion tokens,便于用户把批处理放在谷时段(v4 系列 2026-08-17
|
|
94
|
+
3. **峰谷定价适配**:每轮后展示 prompt/completion tokens,便于用户把批处理放在谷时段(v4 系列 2026-08-17 起峰谷定价,高峰=北京工作日 9:00–12:00、14:00–18:00,闲时价=高峰一半)。官方同时提供 Responses API 与 Anthropic 兼容接口;MingDao 默认走 OpenAI 兼容 chat/completions,如需原生协议可写自定义 Provider 模块。
|
|
95
95
|
4. **模型路由(路线图)**:规划用 v4-pro、执行用 v4-flash 的自动分工;Provider 抽象已支持任意切换。
|
|
96
96
|
5. **可靠性**:超时 + 指数退避重试,长上下文下避免瞬时错误中断任务。
|
|
97
97
|
|
package/docs/CONFIG.md
CHANGED
|
@@ -179,7 +179,11 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
179
179
|
分账(`batch` 标记)。端点不支持的网关会明确报错;可用 `config.batchBaseUrl` 指定支持
|
|
180
180
|
批处理的网关、`batchEndpoint`/`batchWindow` 覆盖协议字段。
|
|
181
181
|
- **避峰执行(高峰输入价 2 倍)**:`mingdao run --offpeak` / `mingdao schedule add --offpeak`
|
|
182
|
-
——高峰时段(北京时间工作日 9:00–14:00
|
|
182
|
+
——高峰时段(北京时间工作日 9:00–12:00、14:00–18:00 两段)自动顺延到最近闲时(12:00 / 18:00)执行;**周末与午间 12:00–14:00 按闲时计价**
|
|
183
|
+
· `pricing.peakWindows`:高峰窗口覆盖([[起,止],...] 北京时间整点);`pricing.timezone`:计价时区
|
|
184
|
+
· `pricing.source`:官方价格 JSON 地址(`mingdao update --pricing` 拉取,TTL 默认 7 天,`pricing.ttlDays` 可调);`pricing.overrides`:按模型覆盖价格
|
|
185
|
+
· `reasoningEffort`:思考强度 low/high/max(默认模型内置 high;REPL `/think` 切换)
|
|
186
|
+
· `routing.upgradeSteps` / `routing.upgradeTruncated`:粘滞 flash 会话累计步数/截断超过阈值自动升 planner(默认 10 / 2)
|
|
183
187
|
(DeepSeek 官方邮件确认),不触发避峰等待。WebUI 调度面板勾选「🌙 避峰执行」。
|
|
184
188
|
|
|
185
189
|
## 云同步
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 六份第三方技术评估综合分析与优化路线(2026-08-27)
|
|
2
|
+
|
|
3
|
+
对 `/persistent/home/YouLi/办公云盘/MingDao-Harness` 下六份评估(DuMate / Hermes / Kimi / MiniMax / OfficeACE×3 / workbuddy)逐份研读后,结合当前代码核实的综合结论与行动计划。
|
|
4
|
+
|
|
5
|
+
## 一、报告结论速览
|
|
6
|
+
|
|
7
|
+
| 报告 | 视角 | 结论 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| DuMate | 逐文件代码审计 + 评分卡 | 架构 9/10、安全 9/10、可维护性 7/10;定位兑现度高 |
|
|
10
|
+
| Hermes | 省钱定位深度剖析 + 冒烟实跑 | "实现程度高、技术路线正确";缓存前缀稳定性是"最值钱的设计判断" |
|
|
11
|
+
| Kimi | 实现度评估 | 8.2/10;发现 1 个 P0(峰谷窗口)与多个 P1(思考强度/上下文字段) |
|
|
12
|
+
| MiniMax | Token 优化评估 | 重度使用可降至裸调 20–35%;P0-1"压缩破坏前缀"(与 Kimi 表扬相矛盾,经核实为观察角度差异) |
|
|
13
|
+
| OfficeACE | 迭代方向 + 优化方案 O-01~O-19 | 8.5/10;显式缓存控制/技能压缩/路由优化为共识 P0 |
|
|
14
|
+
| workbuddy | 全量克隆 + 实测 | 8.5/10;smoke 51 + e2e 16 全过 |
|
|
15
|
+
|
|
16
|
+
**共识**:六份报告一致认可"省 Token 省钱"定位兑现度高;共同短板是 WebUI 单体、价格/词表数据硬编码、记忆检索弱、评测基准缺失。
|
|
17
|
+
|
|
18
|
+
## 二、已核实并修正的问题(本版 0.1.66 实施)
|
|
19
|
+
|
|
20
|
+
### P0-1 峰谷窗口与避峰顺延错误(Kimi 报告 P0,官方口径核实)
|
|
21
|
+
- 现象:代码按单窗口"工作日 9:00–14:00 高峰"计价;`--offpeak` 把任务顺延到 14:00——恰是第二段高峰起点,**功能效果与意图相反**;14:00–18:00 费用被低估一倍。
|
|
22
|
+
- 官方权威口径(https://api-docs.deepseek.com/zh-cn/quick_start/pricing/):**高峰 = 北京时间周一至周五 9:00–12:00、14:00–18:00;其余为闲时;闲时价 = 高峰价一半**。
|
|
23
|
+
- 已修复(`src/pricing.js` / `cli.js` / `schedule.js` / `tasks.js` / `models.js` / WebUI / README / docs / smoke 12 组断言):
|
|
24
|
+
- `isPeakHour`:双窗口判定,午间 12:00–14:00 与周末全天为闲时;
|
|
25
|
+
- `deferToOffpeak`:上午高峰 → 顺延 12:00;下午高峰 → 顺延 18:00;闲时原时刻执行;
|
|
26
|
+
- 高峰窗口经 `config.pricing.peakWindows` 可配置(官方调整时可零代码跟随);
|
|
27
|
+
- 新增 `peakStatusLabel()` 供面板/备注显示当前时段。
|
|
28
|
+
|
|
29
|
+
### P0-2 桌面版「检查更新」点击无响应(用户反馈)
|
|
30
|
+
- 现象:菜单点击后任何路径异常(electron-updater 导入失败 / 事件不触发 / 网络挂起)都会静默吞掉。
|
|
31
|
+
- 已修复(`desktop/main.js`):全程 try/catch + promise 结果兜底 + 30 秒超时兜底;已是最新时提示 **"当前版本:v0.1.66,已是最新版本"**;检查中并发给出可见反馈。
|
|
32
|
+
|
|
33
|
+
### P1 桌面版设置改多级菜单(用户反馈)
|
|
34
|
+
- 现象:⚙ 设置一拉到底(模型/同步/沙箱/调度/工作空间/记忆/仪表盘/MCP/技能库 9 块混排)。
|
|
35
|
+
- 已修复(`src/web/index.html`):左侧分组导航 + 右侧面板(🔑模型与密钥 / ☁️云同步与协作 / 🛡️通用与权限 / ⏰调度队列 / 📁工作空间 / 🧠长期记忆 / 📊缓存仪表盘 / 🔌MCP 生态 / 🧩技能库),记住上次分组、窄屏自动横向导航,全部控件 id 不变。
|
|
36
|
+
|
|
37
|
+
### P1 界面与会话样式对齐 DeepSeek-Harness
|
|
38
|
+
- 以 gitcode.com/MingDaoTCM/deepseek-harness 的 `packages/client/ui-theme/src/styles/design-platform.css` 实测值为准对齐暗色体系:
|
|
39
|
+
- 基色 `#151517`、侧栏 `#1B1B1C`、面板 `#2C2C2E`、悬浮 `#353638`、边框 `rgba(255,255,255,.12)`;
|
|
40
|
+
- 文字 `#F9FAFB` / `#ADB2B8` / `#81858C`;错误 `#F25A5A`、警告 `#F59E0B`;
|
|
41
|
+
- 用户气泡改 DSH 式扁平中性底 + **22px 圆角** + 16px/24px 排版(上限 min(525px,82%));
|
|
42
|
+
- 行内代码 `#2C2C2E` 底;代码块 `#1B1B1C` + 横幅 `#2C2C2E` + 12px 圆角 + 语言标签 + 一键复制;
|
|
43
|
+
- 主按钮 DSH 暗色近白填充;滚动条 `#3C3C3D`/hover `#545557`。
|
|
44
|
+
|
|
45
|
+
## 三、下一步优化路线(按投入产出排序)
|
|
46
|
+
|
|
47
|
+
### 近期(0.1.67 已实施 ✅,其余顺延 0.2.0)
|
|
48
|
+
1. ✅ **思考强度三档透传**(Kimi P1-B):`models.js` 加 `reasoningEffort`、Provider 透传 `reasoning_effort`(官方 thinking_mode 参数),辅助调用(标题/记忆/分类器)固定 `low`,REPL `/think low|high|max|off` 切换。
|
|
49
|
+
2. ✅ **上下文窗口字段修正**(Kimi P1-C):`contextWindow` 1M、新增 `maxOutputCeiling:384000`、pro `maxOutputTokens` 32768→65536。
|
|
50
|
+
3. ✅ **价格表外置 + 自动刷新**(Hermes C1):内置表兜底 + `~/.mingdao/pricing.json`(`mingdao update --pricing` 从 `pricing.source` 拉取,TTL 7 天);过期时费用标签提示刷新。
|
|
51
|
+
4. ✅ **轻量语义回收**(Hermes B1):预算 >80% 时最老 tool 消息 → 单行摘要、旧 assistant 长文截前 200 字,无需模型调用;仍超才按尾部保留丢弃。
|
|
52
|
+
5. ✅ **路由升级检测 + 第三态**(Hermes C2 / Kimi):粘滞 flash 会话累计工具步数 ≥10 或截断 ≥2 自动升 planner(`routing.upgradeSteps/upgradeTruncated` 可调);分类器"不确定"→ 保守走 planner。
|
|
53
|
+
|
|
54
|
+
### 中期(0.2.x,2–5 天)
|
|
55
|
+
6. **reasoning 回填裁剪**(MiniMax P0-2):推理链 >1000 字符截尾 500 + 关键决策标记;>4000 完全丢弃留一句概括。
|
|
56
|
+
7. **增量式压缩**(Kimi P2-D):`compressedUntil` 游标,只对增量段摘要,压缩输入从 O(历史) 降为 O(增量)。
|
|
57
|
+
8. **护栏前置预估**(Kimi P2-E):发请求前按"trimmed prompt×未命中价 + maxOutput×输出价"估算最坏成本,超限发送前拦截。
|
|
58
|
+
9. **同回合工具去重 + 会话级 read 缓存**(Hermes C4 / MiniMax P2-2):相同参数只读工具合并执行;read 按 mtime+size 去重。
|
|
59
|
+
10. **工具 Schema 按需挂载**(MiniMax P3-5 / Hermes A1):只读阶段只给 read/ls/glob/grep,每请求省 1–2K tokens。
|
|
60
|
+
11. **Batch 输入侧优化**(Kimi P3-F):文本哈希去重、单问超窗口报错、`--max-cost` 截断。
|
|
61
|
+
12. **评测基准起步**(Hermes E2):`test/bench/` 压缩质量集 / 路由准确率集 / tokenizer 黄金值回归。
|
|
62
|
+
|
|
63
|
+
### 长期(0.3.x+,可选)
|
|
64
|
+
13. 跨平台沙箱补位(Windows Job Object + 降权 token;macOS seatbelt)。
|
|
65
|
+
14. tokenizer 缓存真 LRU + 大文本分级缓存(Kimi P3 / Hermes E3)。
|
|
66
|
+
15. 费用二级分账(reasoning/tool 维度)+ WebUI 看板扩展。
|
|
67
|
+
16. WebUI 单体拆分(`routes/` + 前端区块化)——与"轻量单文件易部署"权衡后分阶段做。
|
|
68
|
+
|
|
69
|
+
## 四、不建议采纳(通用 AI 幻觉式建议)
|
|
70
|
+
|
|
71
|
+
| 建议 | 原因 |
|
|
72
|
+
|---|---|
|
|
73
|
+
| TypeScript 全量迁移 / i18n 框架 / OpenTelemetry | 工具链级重写,与"零运行时依赖 + 单文件可部署"冲突;渐进 JSDoc 即可 |
|
|
74
|
+
| onnxruntime WASM tokenizer / fasttext 本地分类器 / 向量嵌入检索(外部库) | 引入重依赖;语义检索可用 `node:sqlite` + 简单相似度保持零依赖 |
|
|
75
|
+
| better-sqlite3 会话索引 | Node <22 无内置 `node:sqlite`;当前 JSONL+bigram 倒排够用 |
|
|
76
|
+
| A/B 双模型对比路由 | 与省钱目标直接矛盾(同时调两模型) |
|
|
77
|
+
| "固定 prefix 参数 / since message_id 增量 API" | 以可能不存在的 API 为前提 |
|
|
78
|
+
| LLMLingua 压缩 / Speculative Execution | 外部库或高风险推测收益 |
|
|
79
|
+
| MiniMax P0-1 "压缩延迟注入" | 与 Kimi 对滞回压缩的表扬相矛盾;压缩本就发生在轮间,收益被夸大 |
|
|
80
|
+
|
|
81
|
+
## 五、口径勘误记录(报告间矛盾核实结果)
|
|
82
|
+
|
|
83
|
+
- 峰谷窗口:Kimi 正确(双窗口),MiniMax 漏查 → 已按官方口径修复。
|
|
84
|
+
- "无自动避峰调度"(OfficeACE 二/三):不实,`run --offpeak` / `schedule add --offpeak` / WebUI 均已实现 → 本版修正了顺延目标。
|
|
85
|
+
- 上下文窗口 384K vs 1M:Kimi 指出 384K 实为最大输出规格 → 0.1.67 已修正(contextWindow=1M、新增 maxOutputCeiling=384K、pro maxOutputTokens 32768→65536)。
|
package/docs/QA-REPORT.md
CHANGED
|
@@ -282,7 +282,7 @@ Retry-After 退避;远期:拆 cli.js、子代理并行、插件生命周期
|
|
|
282
282
|
| --- | --- |
|
|
283
283
|
| **Batch API 半价通道(A1)** | `src/batch.js` + `mingdao batch <文件|->`:单轮批量任务走 OpenAI 兼容批处理协议(上传→建批→轮询→取回),结果落盘 JSONL 并计入 `/cost` 分账(`batch:true`,×0.5 计价);轮询连续失败 10 次报错、端点不支持明确报错;`batchBaseUrl/batchEndpoint/batchWindow` 可配。smoke #41 全协议 mock 演练 |
|
|
284
284
|
| **费用护栏(A2)** | `src/cost-guard.js`:按北京时间自然日累计真实费用(含缓存折扣与 batch 半价),`costGuard{dailyLimitYuan,warnAtYuan,action:warn\|block}`;Agent 每轮开始前检查,block 时暂停并明确告知;`/cost`、WebUI 头部费用徽标实时显示。smoke #40 覆盖 |
|
|
285
|
-
| **避峰调度** | `run --offpeak` / `schedule add --offpeak` / WebUI「🌙 避峰执行」:高峰(北京工作日 9:00–
|
|
285
|
+
| **避峰调度** | `run --offpeak` / `schedule add --offpeak` / WebUI「🌙 避峰执行」:高峰(北京工作日 9:00–12:00、14:00–18:00,官方口径)自动顺延到最近闲时(12:00 / 18:00)执行;**周末与午间识别为闲时**。e2e-schedule #5 覆盖 |
|
|
286
286
|
| **/cost 分账升级** | 按模型分账、今日费用、Batch 子项、节省归因(相比全未命中省 ¥X)、护栏状态;Web `/api/cache-stats` 同步输出 |
|
|
287
287
|
| **token 预算可视化(A5)** | WebUI done 事件携带会话 token 占用/预算,提示栏显示 `预算 45K/128K(35%)`;头部费用徽标 15s 刷新「今日 ¥x · 命中 y% · 护栏 z%」 |
|
|
288
288
|
| **Retry-After 退避(P3-1)** | provider 重试尊重 `Retry-After` 头(封顶 30s),错误对象透传响应头 |
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -26,6 +26,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
26
26
|
const budget = cfg.contextBudget || preset.budgetTokens || 128000;
|
|
27
27
|
const maxOutput = cfg.maxOutputTokens || preset.maxOutputTokens || 8192;
|
|
28
28
|
const temperature = cfg.temperature ?? preset.temperature ?? 0.6;
|
|
29
|
+
const reasoningEffort = cfg.reasoningEffort ?? preset.reasoningEffort?.default ?? undefined;
|
|
29
30
|
const hooks = createHooks(cfg.hooks, workingDir);
|
|
30
31
|
const todos = [];
|
|
31
32
|
// 会话级共享:调用方传入则复用(/model 切换、子代理均共享,undo 不丢失)
|
|
@@ -189,6 +190,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
189
190
|
tools: [...toolSchemas(), ...mcpSchemas()],
|
|
190
191
|
temperature,
|
|
191
192
|
maxTokens: maxOutput,
|
|
193
|
+
reasoningEffort,
|
|
192
194
|
signal: ac.signal,
|
|
193
195
|
onDelta(d) {
|
|
194
196
|
io.stopSpinner();
|
|
@@ -256,7 +258,13 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
256
258
|
// 参数 JSON 解析失败:回填错误给模型,不拿空参数去执行工具
|
|
257
259
|
io.renderToolDenied(name, {}, '参数解析失败(输出超限被截断?建议分块)');
|
|
258
260
|
if (auditOn) auditEntry({ denied: true, reason: '参数解析失败' });
|
|
259
|
-
|
|
261
|
+
// 审计(长生成截断循环):给出明确分块指引,避免模型反复用单个超大 write 撞输出上限
|
|
262
|
+
messages.push({
|
|
263
|
+
role: 'tool',
|
|
264
|
+
tool_call_id: tc.id,
|
|
265
|
+
content:
|
|
266
|
+
'工具参数 JSON 解析失败(大概率是输出超过模型单次上限被截断)。请把内容拆分成多个较小文件或多次调用逐步写入:单个 write 的参数总长控制在 6000 字符以内;先写核心骨架,再逐文件补充。',
|
|
267
|
+
});
|
|
260
268
|
return null;
|
|
261
269
|
}
|
|
262
270
|
if (!args || typeof args !== 'object' || Array.isArray(args)) args = {};
|
package/src/cli.js
CHANGED
|
@@ -201,10 +201,11 @@ async function runWorkerTask(id, question, { permission, model, offpeak }) {
|
|
|
201
201
|
perm = 'readonly';
|
|
202
202
|
note = 'ask 权限下后台任务按只读执行';
|
|
203
203
|
}
|
|
204
|
-
// 避峰(评估 A2/Kimi P-1):高峰时段(北京工作日 9:00–
|
|
204
|
+
// 避峰(评估 A2/Kimi P-1):高峰时段(北京工作日 9:00–12:00、14:00–18:00)
|
|
205
|
+
// 自动顺延到最近闲时起点(12:00 / 18:00)执行,输入价省 50%
|
|
205
206
|
if (offpeak && isPeakHour(new Date())) {
|
|
206
207
|
const defer = deferToOffpeak(new Date());
|
|
207
|
-
finish({ note:
|
|
208
|
+
finish({ note: `避峰等待至北京时间 ${defer.toISOString().slice(11, 16)}(闲时起执行,输入价省 50%)` });
|
|
208
209
|
await new Promise((r) => setTimeout(r, defer.getTime() - Date.now() + 2000));
|
|
209
210
|
finish({ note: '' });
|
|
210
211
|
}
|
|
@@ -779,6 +780,24 @@ async function main() {
|
|
|
779
780
|
continue;
|
|
780
781
|
}
|
|
781
782
|
await switchToModel(target);
|
|
783
|
+
} else if (cmd === '/think') {
|
|
784
|
+
const vals = ['low', 'high', 'max'];
|
|
785
|
+
if (!arg) {
|
|
786
|
+
io.print(`用法:/think low|high|max|off(当前:${cfg.reasoningEffort || '跟随模型默认 high'})`);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (arg === 'off') cfg.reasoningEffort = undefined;
|
|
790
|
+
else if (vals.includes(arg)) cfg.reasoningEffort = arg;
|
|
791
|
+
else { io.print(style('无效取值:low|high|max|off', C.red)); continue; }
|
|
792
|
+
agent = createAgent({
|
|
793
|
+
provider, permission, io, modelName, workingDir, cfg,
|
|
794
|
+
undoStore: sessionUndoStore, mcp: mcpFacade, sessionRef,
|
|
795
|
+
onCompact: (msgs) => {
|
|
796
|
+
rewriteSession(session.file, msgs);
|
|
797
|
+
persisted = msgs.length;
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
io.print(style(`✓ 思考强度:${cfg.reasoningEffort || 'off(模型默认)'}`, C.green));
|
|
782
801
|
} else if (cmd === '/plan') {
|
|
783
802
|
planMode = !planMode;
|
|
784
803
|
io.print(style(`✓ 计划模式:${planMode ? '开' : '关'}${planMode ? '(先出计划,确认后执行)' : ''}`, C.green));
|
package/src/commands/update.js
CHANGED
|
@@ -5,6 +5,16 @@ import path from 'node:path';
|
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
6
|
|
|
7
7
|
export async function handleUpdateFamily(cmd, args) {
|
|
8
|
+
// 价格表外置刷新(Hermes C1):mingdao update --pricing —— 从 cfg.pricing.source 拉取官方价格 JSON
|
|
9
|
+
if (cmd === 'update' && args.includes('--pricing')) {
|
|
10
|
+
const cfg = loadConfig();
|
|
11
|
+
if (!cfg) { console.log('[错误] 未初始化配置:请先运行 mingdao init'); process.exitCode = 1; return true; }
|
|
12
|
+
const { refreshPricingFromSource } = await import('../pricing.js');
|
|
13
|
+
const r = await refreshPricingFromSource(cfg);
|
|
14
|
+
console.log(r.lines.join('\n'));
|
|
15
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
8
18
|
// 自更新与回滚(git 安装形态)。
|
|
9
19
|
// 评估 P3-1 子命令劫持防护:参数不合法时回退为普通提问(如 "mingdao update the docs" 是提问而非更新命令)
|
|
10
20
|
if (cmd === 'update' && args.every((a) => a === '--check')) {
|
package/src/context.js
CHANGED
|
@@ -65,18 +65,35 @@ export function trimMessages(messages, budget, count = approxTokens) {
|
|
|
65
65
|
const hasSystem = messages[0]?.role === 'system';
|
|
66
66
|
const system = hasSystem ? [messages[0]] : [];
|
|
67
67
|
const rest = hasSystem ? messages.slice(1) : messages;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const totalOf = (arr) => arr.reduce((s, m) => s + messageTokens(m, count), 0);
|
|
69
|
+
// —— 轻量语义回收(Hermes B1):预算 >80% 时先对最老消息做无模型压缩 ——
|
|
70
|
+
// 最老 tool 消息 → 单行摘要;老 assistant 长文 → 前 200 字;逐条向新推进直到回到 80% 线。
|
|
71
|
+
// 相比直接丢弃,被修改的只有前端旧消息——其后未被触碰的近期尾部字节保持不变,
|
|
72
|
+
// 下一轮前缀缓存从首个未修改消息起继续命中(直接丢弃反而让整段前缀失配)。
|
|
73
|
+
if (totalOf(system) + totalOf(rest) > budget * 0.8) {
|
|
74
|
+
const out = [...rest];
|
|
75
|
+
for (let i = 0; i < out.length && i < 500; i++) {
|
|
76
|
+
if (totalOf(system) + totalOf(out) <= budget * 0.8) break;
|
|
77
|
+
const m = out[i];
|
|
78
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m?.content ?? '');
|
|
79
|
+
if (m.role === 'tool' && c.length > 40) {
|
|
80
|
+
out[i] = { ...m, content: `[工具 ${m.tool_call_id || ''} 结果摘要:${c.slice(0, 40).replace(/\n/g, ' ')}…(已回收,原 ${c.length} 字)]` };
|
|
81
|
+
} else if (m.role === 'assistant' && c.length > 200) {
|
|
82
|
+
out[i] = { ...m, content: c.slice(0, 200) + ` …[已回收,原 ${c.length} 字]` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 回收后仍超预算 → 从尾部保留(必要时继续丢弃最老,清洗 tool 配对)
|
|
86
|
+
let total = totalOf(system);
|
|
87
|
+
const tail = [];
|
|
88
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
89
|
+
const t = messageTokens(out[i], count);
|
|
90
|
+
if (total + t > budget) break;
|
|
91
|
+
total += t;
|
|
92
|
+
tail.unshift(out[i]);
|
|
93
|
+
}
|
|
94
|
+
return cleanToolPairing(system.concat(tail));
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
// 静默裁剪:不向中间插入说明消息——保持消息前缀字节稳定,
|
|
78
|
-
// 让 DeepSeek 等支持上下文缓存的 API 最大化缓存命中(命中价仅为未命中的约 1/30)。
|
|
79
|
-
return kept;
|
|
96
|
+
return cleanToolPairing(system.concat(rest));
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
export function clampText(text, maxChars = TOOL_RESULT_LIMIT) {
|
package/src/memory.js
CHANGED
|
@@ -178,7 +178,7 @@ export async function extractMemory(provider, model, messages, existingMemory) {
|
|
|
178
178
|
];
|
|
179
179
|
// 结构化输出(评估 4.2-4):json_object,maxTokens 300→200,解析零失败;网关不支持时回退纯文本
|
|
180
180
|
try {
|
|
181
|
-
const res = await provider.chat({ model, messages: msgs, tools: [], temperature: 0.2, maxTokens: 200, responseFormat: { type: 'json_object' } });
|
|
181
|
+
const res = await provider.chat({ model, messages: msgs, tools: [], temperature: 0.2, maxTokens: 200, reasoningEffort: 'low', responseFormat: { type: 'json_object' } });
|
|
182
182
|
const j = JSON.parse(String(res.text || '').trim());
|
|
183
183
|
const items = Array.isArray(j?.items) ? j.items : [];
|
|
184
184
|
const clean = items
|
package/src/models.js
CHANGED
|
@@ -9,7 +9,7 @@ export const PROVIDERS = {
|
|
|
9
9
|
baseUrl: 'https://api.deepseek.com/v1',
|
|
10
10
|
envKey: 'DEEPSEEK_API_KEY',
|
|
11
11
|
models: ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-v4-flash-vision-exp'],
|
|
12
|
-
note: 'DeepSeek-V4 正式版(2026-08-17 起商用):384K
|
|
12
|
+
note: 'DeepSeek-V4 正式版(2026-08-17 起商用):1M 上下文(单次最大输出 384K),峰谷定价(高峰=北京工作日 9:00–12:00、14:00–18:00,闲时价=高峰一半),支持工具调用 / Responses API / Anthropic 兼容接口。',
|
|
13
13
|
},
|
|
14
14
|
openai: {
|
|
15
15
|
label: 'OpenAI',
|
|
@@ -53,11 +53,15 @@ export const MODELS = {
|
|
|
53
53
|
'deepseek-v4-pro': {
|
|
54
54
|
label: '旗舰推理 · 复杂架构、排障、规划',
|
|
55
55
|
provider: 'deepseek',
|
|
56
|
-
contextWindow:
|
|
56
|
+
contextWindow: 1000000, // 官方上下文 1M(审计 Kimi P1-C:384K 实为单次最大输出,字段语义修正)
|
|
57
|
+
maxOutputCeiling: 384000, // 单次最大输出上限(官方规格)
|
|
57
58
|
budgetTokens: 200000, // 默认注入预算(省钱省时,可调高)
|
|
58
|
-
maxOutputTokens:
|
|
59
|
+
maxOutputTokens: 65536, // 生成类任务少续写、少重复计费(Kimi P1-C)
|
|
59
60
|
temperature: 0.4,
|
|
60
61
|
supportsReasoning: true,
|
|
62
|
+
// 思考强度三档(官方 thinking_mode:low/high/max)——输出按 completion 计费(输入 3 倍价),
|
|
63
|
+
// 简单任务用 low 可砍掉大部分推理 token;辅助调用(标题/记忆/分类器)固定 low
|
|
64
|
+
reasoningEffort: { default: 'high', options: ['low', 'high', 'max'] },
|
|
61
65
|
// 峰谷定价(元/百万 tokens;input=缓存未命中,cacheHit=缓存命中约为未命中的 1/30)
|
|
62
66
|
pricing: {
|
|
63
67
|
offpeak: { input: 4.5, output: 13.5, cacheHit: 0.15 },
|
|
@@ -67,7 +71,8 @@ export const MODELS = {
|
|
|
67
71
|
'deepseek-v4-flash': {
|
|
68
72
|
label: '极速响应 · 日常问答与轻量任务',
|
|
69
73
|
provider: 'deepseek',
|
|
70
|
-
contextWindow:
|
|
74
|
+
contextWindow: 1000000, // 官方上下文 1M
|
|
75
|
+
maxOutputCeiling: 384000,
|
|
71
76
|
budgetTokens: 128000,
|
|
72
77
|
maxOutputTokens: 8192,
|
|
73
78
|
temperature: 0.6,
|
|
@@ -82,7 +87,8 @@ export const MODELS = {
|
|
|
82
87
|
'deepseek-v4-flash-vision-exp': {
|
|
83
88
|
label: '多模态视觉 · 图片描述/截图识别/图表分析(实验版)',
|
|
84
89
|
provider: 'deepseek',
|
|
85
|
-
contextWindow:
|
|
90
|
+
contextWindow: 1000000,
|
|
91
|
+
maxOutputCeiling: 384000,
|
|
86
92
|
budgetTokens: 128000,
|
|
87
93
|
maxOutputTokens: 8192,
|
|
88
94
|
temperature: 0.6,
|
package/src/pricing.js
CHANGED
|
@@ -12,36 +12,88 @@ import { mingdaoHome } from './config.js';
|
|
|
12
12
|
// 内置价格表的数据时点(定价可能调整,配置覆盖可随时更新)
|
|
13
13
|
export const PRICE_DATA_AS_OF = '2026-08';
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
function
|
|
15
|
+
// 价格表外置(Hermes C1):~/.mingdao/pricing.json 由「mingdao update --pricing」从
|
|
16
|
+
// cfg.pricing.source 拉取(TTL 默认 7 天,cfg.pricing.ttlDays 可调);TTL 内覆盖内置表,
|
|
17
|
+
// 过期自动回退内置并置 stale 标记(/cost 与费用标签会提示)。
|
|
18
|
+
function pricingFilePath() { return path.join(mingdaoHome(), 'pricing.json'); }
|
|
19
|
+
let extCache = { mtime: -1, data: null, stale: false };
|
|
20
|
+
function externalPricing() {
|
|
21
|
+
try {
|
|
22
|
+
const f = pricingFilePath();
|
|
23
|
+
const st = fs.statSync(f);
|
|
24
|
+
if (st.mtimeMs !== extCache.mtime) {
|
|
25
|
+
const d = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
26
|
+
const ttlDays = Number(tzCache.ttlDays ?? 7);
|
|
27
|
+
const age = Date.now() - Number(d?.fetchedAt || 0);
|
|
28
|
+
extCache = { mtime: st.mtimeMs, data: d, stale: !Number.isFinite(age) || age > ttlDays * 86400000 };
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
extCache = { mtime: -1, data: null, stale: false };
|
|
32
|
+
}
|
|
33
|
+
return extCache;
|
|
34
|
+
}
|
|
35
|
+
export function pricingDataStale() { return externalPricing().stale; }
|
|
36
|
+
|
|
37
|
+
// 拉取官方价格表(零依赖 fetch):cfg.pricing.source 为 JSON 地址,返回 { models: { <模型名>: { input, output, cacheHit, peak? } } }
|
|
38
|
+
export async function refreshPricingFromSource(cfg) {
|
|
39
|
+
const src = String(cfg?.pricing?.source || '').trim();
|
|
40
|
+
if (!src) return { ok: false, lines: ['未配置 pricing.source(config.json 的 pricing.source 填官方价格 JSON 地址后重试)'] };
|
|
41
|
+
const res = await fetch(src, { signal: AbortSignal.timeout(15000) });
|
|
42
|
+
if (!res.ok) return { ok: false, lines: ['价格源请求失败:HTTP ' + res.status] };
|
|
43
|
+
const d = await res.json();
|
|
44
|
+
const models = d && typeof d === 'object' && !Array.isArray(d) ? /** @type {any} */ (d).models : null;
|
|
45
|
+
if (!models || typeof models !== 'object' || Array.isArray(models)) {
|
|
46
|
+
return { ok: false, lines: ['价格源格式不符:应为 {"models": {"模型名": {"input":…,"output":…,"cacheHit":…,"peak":{…}}}}'] };
|
|
47
|
+
}
|
|
48
|
+
const file = pricingFilePath();
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
fs.writeFileSync(file, JSON.stringify({ fetchedAt: Date.now(), source: src, models }, null, 2));
|
|
51
|
+
extCache = { mtime: -1, data: null, stale: false };
|
|
52
|
+
const names = Object.keys(models).join('、');
|
|
53
|
+
return { ok: true, lines: ['✓ 价格表已刷新(' + names + '),TTL 内费用估算/护栏/避峰自动跟随'] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 峰谷判断锚定北京时间(DeepSeek 官方定价页:高峰时段 = 北京时间周一至周五
|
|
57
|
+
// 9:00–12:00、14:00–18:00 两段;其余(含午间 12:00–14:00 与周末全天)为闲时,闲时价 = 高峰一半)。
|
|
58
|
+
// 用本机时区判断会让海外用户计费错位。config.pricing.timezone 可覆盖时区、
|
|
59
|
+
// config.pricing.peakWindows 可覆盖高峰窗口([[起,止],...] 北京时间整点),mtime 缓存避免每次读盘。
|
|
60
|
+
const DEFAULT_PEAK_WINDOWS = [[9, 12], [14, 18]];
|
|
61
|
+
let tzCache = { mtime: 0, timezone: 'Asia/Shanghai', peakWindows: DEFAULT_PEAK_WINDOWS };
|
|
62
|
+
function peakCfg() {
|
|
19
63
|
try {
|
|
20
64
|
const file = path.join(mingdaoHome(), 'config.json');
|
|
21
65
|
const st = fs.statSync(file);
|
|
22
66
|
if (st.mtimeMs !== tzCache.mtime) {
|
|
23
67
|
const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
24
|
-
|
|
68
|
+
const pw = cfg?.pricing?.peakWindows;
|
|
69
|
+
tzCache = {
|
|
70
|
+
mtime: st.mtimeMs,
|
|
71
|
+
timezone: String(cfg?.pricing?.timezone || 'Asia/Shanghai'),
|
|
72
|
+
peakWindows: Array.isArray(pw) && pw.length ? pw : DEFAULT_PEAK_WINDOWS,
|
|
73
|
+
ttlDays: Number(cfg?.pricing?.ttlDays || 7),
|
|
74
|
+
};
|
|
25
75
|
}
|
|
26
76
|
} catch {}
|
|
27
|
-
return tzCache
|
|
77
|
+
return tzCache;
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
export function isPeakHour(date = new Date()) {
|
|
81
|
+
const { timezone, peakWindows } = peakCfg();
|
|
31
82
|
try {
|
|
32
83
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
33
|
-
timeZone:
|
|
84
|
+
timeZone: timezone,
|
|
34
85
|
weekday: 'short',
|
|
35
86
|
hour: 'numeric',
|
|
36
87
|
hourCycle: 'h23',
|
|
37
88
|
}).formatToParts(date);
|
|
38
89
|
const weekday = parts.find((p) => p.type === 'weekday')?.value;
|
|
39
|
-
//
|
|
90
|
+
// 周末全天按闲时计价
|
|
40
91
|
if (weekday === 'Sat' || weekday === 'Sun') return false;
|
|
41
92
|
const hour = Number(parts.find((p) => p.type === 'hour')?.value);
|
|
42
|
-
return hour >=
|
|
93
|
+
return peakWindows.some(([s, e]) => hour >= s && hour < e);
|
|
43
94
|
} catch {
|
|
44
|
-
|
|
95
|
+
const h = date.getHours();
|
|
96
|
+
return peakWindows.some(([s, e]) => h >= s && h < e); // 非法时区回退本机
|
|
45
97
|
}
|
|
46
98
|
}
|
|
47
99
|
|
|
@@ -49,7 +101,7 @@ export function isPeakHour(date = new Date()) {
|
|
|
49
101
|
export function beijingParts(date = new Date()) {
|
|
50
102
|
let tz = 'Asia/Shanghai';
|
|
51
103
|
try {
|
|
52
|
-
tz =
|
|
104
|
+
tz = peakCfg().timezone;
|
|
53
105
|
new Intl.DateTimeFormat('en-US', { timeZone: tz }).format(); // 非法时区在此抛错
|
|
54
106
|
} catch {
|
|
55
107
|
tz = 'Asia/Shanghai'; // 审计 B1:坏时区配置回退北京时间,绝不让计费/护栏崩溃
|
|
@@ -73,11 +125,24 @@ export function beijingToDate(parts) {
|
|
|
73
125
|
return new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second) - 8 * 3600 * 1000);
|
|
74
126
|
}
|
|
75
127
|
|
|
76
|
-
//
|
|
128
|
+
// 避峰顺延:当前处于高峰 → 顺延到该高峰段的结束整点(即最近一个闲时起点)
|
|
129
|
+
// · 9:00–12:00 高峰 → 12:00(午间闲时)
|
|
130
|
+
// · 14:00–18:00 高峰 → 18:00(晚间闲时)
|
|
131
|
+
// 其余(午间/晚间/清晨/周末)原时刻执行。窗口可经 pricing.peakWindows 覆盖。
|
|
77
132
|
export function deferToOffpeak(date = new Date()) {
|
|
78
133
|
if (!isPeakHour(date)) return date;
|
|
79
134
|
const p = beijingParts(date);
|
|
80
|
-
|
|
135
|
+
const w = peakCfg().peakWindows.find(([s, e]) => p.hour >= s && p.hour < e);
|
|
136
|
+
const targetHour = w ? w[1] : 18;
|
|
137
|
+
return beijingToDate({ ...p, hour: targetHour, minute: 0, second: 0 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 当前时段的人类可读描述(设置面板/调度备注共用)
|
|
141
|
+
export function peakStatusLabel(date = new Date()) {
|
|
142
|
+
if (!isPeakHour(date)) return '闲时';
|
|
143
|
+
const p = beijingParts(date);
|
|
144
|
+
const w = peakCfg().peakWindows.find(([s, e]) => p.hour >= s && p.hour < e);
|
|
145
|
+
return `高峰(至 ${String(w ? w[1] : 18).padStart(2, '0')}:00)`;
|
|
81
146
|
}
|
|
82
147
|
|
|
83
148
|
// 北京时间当日 0 点(费用护栏按自然日累计)
|
|
@@ -119,16 +184,19 @@ function pricingOverrides() {
|
|
|
119
184
|
}
|
|
120
185
|
function effectivePricing(modelName) {
|
|
121
186
|
const preset = modelPreset(modelName);
|
|
122
|
-
|
|
187
|
+
const ext = externalPricing().data?.models?.[modelName] || null;
|
|
188
|
+
if (!preset?.pricing && !ext) return null;
|
|
189
|
+
const base = ext || preset?.pricing || null;
|
|
190
|
+
if (!base) return null;
|
|
123
191
|
const over = pricingOverrides()[modelName] || {}; // 审计 Q3:mtime 缓存替代每轮读盘
|
|
124
|
-
const merge = (
|
|
125
|
-
input: Number(o.input ??
|
|
126
|
-
output: Number(o.output ??
|
|
127
|
-
cacheHit: Number(o.cacheHit ??
|
|
192
|
+
const merge = (b, o = {}) => ({
|
|
193
|
+
input: Number(o.input ?? b?.input ?? 0),
|
|
194
|
+
output: Number(o.output ?? b?.output ?? 0),
|
|
195
|
+
cacheHit: Number(o.cacheHit ?? b?.cacheHit ?? 0),
|
|
128
196
|
});
|
|
129
197
|
return {
|
|
130
|
-
offpeak: merge(
|
|
131
|
-
peak: merge(
|
|
198
|
+
offpeak: merge(base.offpeak || base, over),
|
|
199
|
+
peak: merge(base.peak || base.offpeak || base, over.peak),
|
|
132
200
|
};
|
|
133
201
|
}
|
|
134
202
|
|
|
@@ -152,5 +220,5 @@ export function estimateCostLabel(modelName, promptTokens, completionTokens, usa
|
|
|
152
220
|
const yuan = estimateCost(modelName, promptTokens, completionTokens, cache).toFixed(5);
|
|
153
221
|
const hitPart =
|
|
154
222
|
cache && cache.hit + cache.miss > 0 ? ` · 缓存命中 ${(cache.rate * 100).toFixed(0)}%` : ' · 未计缓存折扣';
|
|
155
|
-
return ` ≈¥${yuan}(${isPeakHour() ? '高峰' : '闲时'}${hitPart}
|
|
223
|
+
return ` ≈¥${yuan}(${isPeakHour() ? '高峰' : '闲时'}${hitPart})${pricingDataStale() ? ' · ⚠ 价格表过期,运行 mingdao update --pricing 刷新' : ''}`;
|
|
156
224
|
}
|
package/src/prompts.js
CHANGED
|
@@ -12,13 +12,14 @@ const BASE = `你是 MingDao Harness,一个由 MingDao Harness 驱动的 AI
|
|
|
12
12
|
工作准则:
|
|
13
13
|
1. 先了解再动手:修改或创建代码前,先用 read / ls / glob / grep 查看相关文件,不要凭空猜测。
|
|
14
14
|
2. 精准修改:优先用 edit 做小步精确替换;新建文件用 write;保持改动最小化;改错可用 undo 撤销。
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
18
|
-
6.
|
|
19
|
-
7.
|
|
20
|
-
8.
|
|
21
|
-
9.
|
|
15
|
+
3. 大内容分批写:单个 write 的参数总长控制在 6000 字符以内;大文件先写核心骨架,再分多次 write/edit 逐步补充——单次输出超限会被截断并导致参数解析失败。
|
|
16
|
+
4. 任务管理:多步骤任务先建 todo 清单并逐项更新;与技能(skill)相关的任务先加载对应 SKILL.md。
|
|
17
|
+
5. 委托与专注:独立、可并行的子问题(调研、复核、独立实现)用 task 委托子代理,把结果拿回来继续主线。
|
|
18
|
+
6. 命令谨慎:执行 bash 前想清楚影响;命令失败时阅读错误输出并修复,同一问题最多重试 3 次。
|
|
19
|
+
7. 回答风格:使用用户的语言(默认中文),简洁直接,先结论后说明。
|
|
20
|
+
8. 诚实可靠:不编造不存在的文件、行号、API 或执行结果。
|
|
21
|
+
9. 善用工具:能用工具确认的事实就用工具确认;多个只读工具可连续调用以提高效率。
|
|
22
|
+
10. 交付收尾:完成工具操作后必须给出简短交付总结——生成了哪些文件(路径)、如何运行/使用(如「浏览器打开 angry-birds.html 即可游玩」或运行命令)、以及未完成或注意事项(若有)。绝不能不声不响结束。`;
|
|
22
23
|
|
|
23
24
|
function loadFile(p, cap) {
|
|
24
25
|
try {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @typedef {Error & { status?: number, headers?: Headers }} ApiError
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export async function chat({ baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat }) {
|
|
9
|
+
export async function chat({ baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat, reasoningEffort }) {
|
|
10
10
|
const url = String(baseUrl).replace(/\/+$/, '') + '/chat/completions';
|
|
11
11
|
const payload = { model, messages };
|
|
12
12
|
if (temperature != null) payload.temperature = temperature;
|
|
@@ -16,6 +16,7 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
|
|
|
16
16
|
}
|
|
17
17
|
if (maxTokens) payload.max_tokens = maxTokens;
|
|
18
18
|
if (responseFormat) payload.response_format = responseFormat;
|
|
19
|
+
if (reasoningEffort) payload.reasoning_effort = reasoningEffort; // 思考强度(官方 thinking_mode)
|
|
19
20
|
payload.stream = true;
|
|
20
21
|
// 流式响应默认不带 usage,显式请求以便展示 token/费用统计
|
|
21
22
|
// (部分网关不支持该字段,可在 config.json 设 "includeUsage": false 关闭)
|
package/src/routing.js
CHANGED
|
@@ -37,7 +37,7 @@ export function heuristicRoute(text, rc) {
|
|
|
37
37
|
const ROUTE_CACHE_MAX = 100;
|
|
38
38
|
const routeCache = new Map();
|
|
39
39
|
|
|
40
|
-
export async function routeTask({ cfg, provider, currentModel, text, sticky = null }) {
|
|
40
|
+
export async function routeTask({ cfg, provider, currentModel, text, sticky = null, sessionStats = null }) {
|
|
41
41
|
const rc = routingConfig(cfg);
|
|
42
42
|
if (!rc) return { model: currentModel, reason: null };
|
|
43
43
|
// 当前模型已在路由池外(用户手动指定):不干预
|
|
@@ -52,8 +52,19 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
|
|
|
52
52
|
: { model: quick, reason: quick === rc.planner ? '规划类任务' : '执行类任务' };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// 会话粘滞 + 升级检测(Hermes C2):执行类会话不再逐轮分类(生成类关键词已在启发式拦截),
|
|
56
|
+
// 但会话内复杂度信号累积(工具步数/截断次数)时允许升级 planner——「开头简单、中途复杂」不再被 flash 硬扛。
|
|
56
57
|
if (sticky === rc.executor) {
|
|
58
|
+
const stats = sessionStats || {};
|
|
59
|
+
const steps = Number(stats.steps) || 0;
|
|
60
|
+
const truncated = Number(stats.truncated) || 0;
|
|
61
|
+
const UPGRADE_STEPS = Number(cfg?.routing?.upgradeSteps) || 10;
|
|
62
|
+
const UPGRADE_TRUNCATED = Number(cfg?.routing?.upgradeTruncated) || 2;
|
|
63
|
+
if (steps >= UPGRADE_STEPS || truncated >= UPGRADE_TRUNCATED) {
|
|
64
|
+
return rc.planner === currentModel
|
|
65
|
+
? { model: currentModel, reason: null }
|
|
66
|
+
: { model: rc.planner, reason: `会话升级:执行类已累计 ${steps} 步工具 / ${truncated} 次截断 → planner` };
|
|
67
|
+
}
|
|
57
68
|
return rc.executor === currentModel
|
|
58
69
|
? { model: currentModel, reason: null }
|
|
59
70
|
: { model: rc.executor, reason: '会话粘滞:执行类' };
|
|
@@ -90,6 +101,7 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
|
|
|
90
101
|
tools: [],
|
|
91
102
|
temperature: 0,
|
|
92
103
|
maxTokens: 20, // 结构化输出(评估 4.2-4):80→20
|
|
104
|
+
reasoningEffort: 'low',
|
|
93
105
|
responseFormat: { type: 'json_object' },
|
|
94
106
|
});
|
|
95
107
|
let verdict = null;
|
|
@@ -103,6 +115,11 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
|
|
|
103
115
|
if (t.includes('plan')) verdict = 'plan';
|
|
104
116
|
else if (t.includes('exec')) verdict = 'execute';
|
|
105
117
|
}
|
|
118
|
+
if (!verdict) {
|
|
119
|
+
// 第三态(Kimi C2):分类器给不出结论 → 保守走 planner,避免复杂任务被 flash 硬扛
|
|
120
|
+
const m = rc.planner;
|
|
121
|
+
return m === currentModel ? { model: currentModel, reason: null } : { model: m, reason: '分类器不确定:保守走 planner' };
|
|
122
|
+
}
|
|
106
123
|
if (verdict) {
|
|
107
124
|
if (routeCache.size >= ROUTE_CACHE_MAX) {
|
|
108
125
|
const first = routeCache.keys().next().value;
|