codebee 0.1.67 → 0.1.69
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/CHANGELOG.md +11 -0
- package/README.md +21 -7
- package/app/core/catalog.py +42 -0
- package/app/core/dispatch_log.py +16 -1
- package/app/core/flows.py +2 -2
- package/app/core/jobs.py +17 -13
- package/app/core/knowledge.py +7 -0
- package/app/core/modelhub.py +20 -1
- package/app/core/pipeline.py +16 -1
- package/app/core/planner.py +2 -0
- package/app/core/publish/flows-qimao-calibrated.json +7 -3
- package/app/core/runner.py +7 -5
- package/app/core/skill_scan.py +8 -0
- package/app/core/skills.py +15 -1
- package/app/core/store.py +48 -7
- package/app/core/tracing.py +144 -0
- package/app/core/usage.py +6 -2
- package/app/main.py +25 -1
- package/app/ui/app.js +22 -7
- package/app/ui/i18n.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
+
## v0.1.69(2026-09-25)
|
|
8
|
+
|
|
9
|
+
- **智能体目录新增三款 CLI(第三批接入)**:Google **Gemini CLI**(每日 1000 次免费请求额度,凭据走个人 Google 账号登录)、腾讯云 **CodeBuddy CLI**(`cbc`,claude 兼容系)、字节开源 **Trae Agent**(`trae-cli`,PyPI 无包故走 uv 从 GitHub 直装)。三者均支持目录管理、一键安装/升级与无头编排(走各 CLI 自带登录态);因其凭据环境变量与 CodeBee 的两种 wire 协议不对应,暂不支持「CLI 绑定」链——配置了也会被明确拦截提示,不会出现「链显示活着、实际走 CLI 默认模型」的假绑定。
|
|
10
|
+
- 美团侧经调研无官方 CLI 产品(LongCat 是模型家族+CatPaw 平台);LongCat-2.0 模型可经支持自定义 provider 的现有 CLI 作为上游接入,无需新条目。
|
|
11
|
+
|
|
12
|
+
## v0.1.68(2026-09-25)
|
|
13
|
+
|
|
14
|
+
- **小说任务初稿不再「边写边自我审查」**:起草契约新增「成文分三步」——先把不设限的情节/画面/台词念头快速倾倒成素材清单(狂人),再挑主线组织结构(建筑师),最后按结构成文(木匠);初稿期不做质量审判,质量把关仍由既有评审链承担。此前契约两条全是收敛导向,发散步骤缺失,点子容易在起草半路被掐死。
|
|
15
|
+
- **商务邮件回复不再漏答对方的问题**:邮件契约新增线程级交付规则——回复往来邮件时逐条回应对方提出的每个问题与请求(可用清单),不漏问、不回避;行动项落到具体动作+负责人+截止时间。此前只有我方发件视角的请求规则,缺收件回复视角。
|
|
16
|
+
- **README 任务类型清单过时修正**:「15 种」→17 种(补缺陷复盘/标书编制)、扫榜选材平台描述从两平台更正为七猫/番茄/起点/纵横四平台——24 班扩源与 22/25 班加类型后文档未跟上。
|
|
17
|
+
|
|
7
18
|
## v0.1.67(2026-09-24)
|
|
8
19
|
|
|
9
20
|
- **连载任务超时不再要人守着手动重试**:「任务总时限已到」此前是死终态——总时限默认 1 小时,供应商只剩慢链时一批十二章天然跑不完,用户只能反复点重试(2026-09-24 戍边骑奴 9-20 案)。现在超时的连载任务与失败一样自动续跑(继承已完成章、延迟 5 分钟退避、次数上限照旧);同因止损对超时做了进度感知——错误签名恒为同一句,但本轮比上轮多过审了章就不算「退避没换来结果」,两轮一章未进才止损。
|
package/README.md
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
**AI 是引擎,你的经验是方向盘。** CodeBee 为你指挥一支跨厂商的执笔/编码小队:
|
|
12
12
|
统一调度本机已装的 AI 编码 CLI(Codex CLI、Claude Code、Qwen Code、OpenCode、Aider、
|
|
13
|
-
Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness
|
|
13
|
+
Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness、Gemini CLI、CodeBuddy CLI、
|
|
14
|
+
Trae Agent……),提供
|
|
14
15
|
**目标 → 自动拆解 → 智能路由 → 执行 → 客观验证 → 跨厂商评审 → 自动修复/换将 → 汇总报告**
|
|
15
16
|
的完整闭环。你不在时它们自动推进、自我打磨;需要你拍板的地方——质量闸门、任务分支的
|
|
16
17
|
采纳与丢弃——它们会亮起「待裁决」等你,绝不静默替你决定。
|
|
@@ -29,6 +30,16 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<!-- relnotes:start -->
|
|
36
|
+
### 最新版更新内容(v0.1.69)
|
|
37
|
+
|
|
38
|
+
- 智能体目录新增三款 CLI:Google **Gemini CLI**、腾讯云 **CodeBuddy CLI**(`cbc`)、字节开源 **Trae Agent**(`trae-cli`)——目录页一键安装/升级与无头编排,凭据走各 CLI 自带登录态
|
|
39
|
+
- 防假绑定:这三款暂不支持「CLI 绑定」链(走本机凭据),配置了供应商链会被明确拦截提示,不再出现「链显示活着、实际走 CLI 默认模型」
|
|
40
|
+
<!-- relnotes:end -->
|
|
41
|
+
|
|
42
|
+
|
|
32
43
|
---
|
|
33
44
|
|
|
34
45
|
## 功能速览
|
|
@@ -121,9 +132,9 @@ OpenCode / Continue / Cursor / Trae 十种工具的配置,一次带入全部
|
|
|
121
132
|
|
|
122
133
|
### 更多内置能力
|
|
123
134
|
|
|
124
|
-
- **
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
- **17 种任务类型**:直接执行、代码、小说、连载小说、自媒体文章、短视频脚本、
|
|
136
|
+
文档、翻译、扫榜选材、缺陷复盘、调研报告、演讲稿、工作汇报、商务邮件、技术方案、简历、标书编制;
|
|
137
|
+
全部支持自定义流程(引擎 / 评审维度 / 阈值 / 轮数 / 提示词覆盖);
|
|
127
138
|
- **质量闸门与裁决**:评审失败≠0 分照过、降级大纲不开写、超时但已落盘的稿件照常
|
|
128
139
|
送评审;需要人拍板的事项进「待裁决」队列(徽章 + 提示音),`tools/bee.py`
|
|
129
140
|
命令行不开浏览器也能过裁决;
|
|
@@ -228,7 +239,7 @@ python tools\bee.py discard <task> :: 否决——删除任务分支(需输
|
|
|
228
239
|
## 任务类型与自定义流程
|
|
229
240
|
|
|
230
241
|
主表单的「类型」下拉即流程列表,点「管理」可新增/编辑自定义流程(引擎、评审维度、
|
|
231
|
-
阈值、轮数、产出文件名、起草/评审提示词覆盖)。内置
|
|
242
|
+
阈值、轮数、产出文件名、起草/评审提示词覆盖)。内置 17 种;禅道工单是自动转成
|
|
232
243
|
`code` 修复任务的集成入口,不另占菜单类型:
|
|
233
244
|
|
|
234
245
|
| 类型 | 引擎 | 流程 |
|
|
@@ -241,10 +252,12 @@ python tools\bee.py discard <task> :: 否决——删除任务分支(需输
|
|
|
241
252
|
| 🎬 短视频脚本 | review | 画面 + 口播 + 字幕/音效 → 多维评审 → 修订 → 发布门禁 |
|
|
242
253
|
| 📝 文档 | review | 起草 → 多维评审(准确性/结构/表达/实用)→ 修订 → 门禁 |
|
|
243
254
|
| 🌐 翻译 | review | 起草 → 多维评审(忠实度/流畅度/术语/风格)→ 修订 → 门禁 |
|
|
244
|
-
| 📈 扫榜选材 | direct |
|
|
255
|
+
| 📈 扫榜选材 | direct | 抓七猫/番茄/起点/纵横四平台榜单 → 跨平台题材/人设/切入点洞察 |
|
|
256
|
+
| 🕓 缺陷复盘 | direct | 读禅道/Jira 导出 → 产品/开发/测试三视角复盘报告 |
|
|
245
257
|
| 🔍 调研报告 | review | 起草 → 多维评审(全面性/深度/论据/结论)→ 修订 → 门禁 |
|
|
246
258
|
| 🎤 演讲稿 | review | 起草 → 多维评审(主题/结构/感染力/语言)→ 修订 → 门禁 |
|
|
247
259
|
| 📅 工作汇报 / ✉️ 商务邮件 / 📋 技术方案 / 📄 简历 | review | 各自专属角色、交付约束与评审维度 |
|
|
260
|
+
| 📑 标书编制 | review | 招标文件对齐评分点 → 多维评审(应答/合规/针对性)→ 修订 → 门禁 |
|
|
248
261
|
|
|
249
262
|
三种引擎:
|
|
250
263
|
- **direct 引擎**:单智能体直接执行目标,保留会话上下文,适合问答、探索和无需评审链的快捷任务;
|
|
@@ -425,7 +438,8 @@ CLI 默认冒烟、供应商可达性、wire 适配、单模型生成、绑定
|
|
|
425
438
|
CodeBee 编排运行时用哪个供应商 / 模型统一在「CLI 绑定」页配置;
|
|
426
439
|
- **编排开关**:决定哪些 CLI 参与任务路由。
|
|
427
440
|
|
|
428
|
-
Kimi Code / MiMo Code / Grok Build / Pi / DeepSeek Harness
|
|
441
|
+
Kimi Code / MiMo Code / Grok Build / Pi / DeepSeek Harness / Gemini CLI / CodeBuddy CLI /
|
|
442
|
+
Trae Agent 的安装命令已内置(均取自
|
|
429
443
|
各工具官方渠道)。若某个条目的安装命令缺失,直接编辑 `data/catalog.json` 补上
|
|
430
444
|
`install` 字段保存后点"重新加载 catalog"即可;点"恢复默认 catalog"可回滚。
|
|
431
445
|
|
package/app/core/catalog.py
CHANGED
|
@@ -172,6 +172,45 @@ DEFAULT_CATALOG = [
|
|
|
172
172
|
"upgrade": "npm install -g @deepseek-ai/dsh@latest",
|
|
173
173
|
"default_enabled": False,
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
"id": "gemini-cli", "name": "Gemini CLI", "cli_group": "installable",
|
|
177
|
+
"note": "Google 官方编码 CLI;无头 gemini -p \"提示词\";默认模型在 "
|
|
178
|
+
"~/.gemini/settings.json 顶层 model;凭据走个人 Google 账号 OAuth "
|
|
179
|
+
"(浏览器登录,免费额度)或 GOOGLE_API_KEY/GEMINI_API_KEY 环境变量",
|
|
180
|
+
"detect": {"cli": "gemini"},
|
|
181
|
+
"orch": {"kind": "generic", "command": "gemini", "argv_template": ["-p", "{prompt}"]},
|
|
182
|
+
"config": {"path": "~/.gemini/settings.json", "format": "json", "model_key": "model"},
|
|
183
|
+
"install": "npm install -g @google/gemini-cli",
|
|
184
|
+
"upgrade": "npm install -g @google/gemini-cli@latest",
|
|
185
|
+
"default_enabled": False,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"id": "codebuddy", "name": "CodeBuddy CLI", "cli_group": "installable",
|
|
189
|
+
"note": "腾讯云 CodeBuddy Code(claude 兼容系,需 Node ≥18.20);可执行名 cbc"
|
|
190
|
+
"(codebuddy 是全名别名);无头 cbc -p \"提示词\";配置在 "
|
|
191
|
+
"~/.codebuddy/settings.json(claude 形态三级作用域,模型走顶层 model 键)",
|
|
192
|
+
"detect": {"cli": "cbc"},
|
|
193
|
+
"orch": {"kind": "generic", "command": "cbc", "argv_template": ["-p", "{prompt}"]},
|
|
194
|
+
"config": {"path": "~/.codebuddy/settings.json", "format": "json", "model_key": "model"},
|
|
195
|
+
"install": "npm install -g @tencent-ai/codebuddy-code",
|
|
196
|
+
"upgrade": "npm install -g @tencent-ai/codebuddy-code@latest",
|
|
197
|
+
"default_enabled": False,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "trae-agent", "name": "Trae Agent", "cli_group": "installable",
|
|
201
|
+
"note": "字节跳动开源 trae-agent;可执行名 trae-cli;PyPI 无包,uv tool 从 "
|
|
202
|
+
"GitHub 直装(Python ≥3.12);无头 trae-cli run \"提示词\"(一次性任务"
|
|
203
|
+
"形态);配置 trae_config.yaml 默认找当前工作目录(TRAE_CONFIG_FILE "
|
|
204
|
+
"可指定),默认模型是 agents.trae_agent.model 指向 models 表别名的两层"
|
|
205
|
+
"引用,CodeBee 暂不托管其模型落盘——绑定模型请在 trae_config.yaml 自配",
|
|
206
|
+
"detect": {"cli": "trae-cli"},
|
|
207
|
+
"orch": {"kind": "generic", "command": "trae-cli", "argv_template": ["run", "{prompt}"]},
|
|
208
|
+
"config": {"path": "~/.trae/trae_config.yaml", "format": None, "model_key": None},
|
|
209
|
+
"install": "uv tool install --python 3.12 git+https://github.com/bytedance/trae-agent",
|
|
210
|
+
"upgrade": "uv tool install --force --python 3.12 git+https://github.com/bytedance/trae-agent",
|
|
211
|
+
"uninstall": "uv tool uninstall trae-agent",
|
|
212
|
+
"default_enabled": False,
|
|
213
|
+
},
|
|
175
214
|
]
|
|
176
215
|
|
|
177
216
|
_LOCK = threading.RLock()
|
|
@@ -195,6 +234,9 @@ LAUNCH_PATCH = { "codex-cli": {"kind": "console", "command": "codex"},
|
|
|
195
234
|
"mimo-code": {"kind": "console", "command": "mimo"},
|
|
196
235
|
"grok-build": {"kind": "console", "command": "grok"},
|
|
197
236
|
"pi": {"kind": "console", "command": "pi"},
|
|
237
|
+
"gemini-cli": {"kind": "console", "command": "gemini"},
|
|
238
|
+
"codebuddy": {"kind": "console", "command": "cbc"},
|
|
239
|
+
"trae-agent": {"kind": "console", "command": "trae-cli"},
|
|
198
240
|
# dsh 自带浏览器 UI(dsh web);端口固定以便「已在运行就直接开页面」的复用
|
|
199
241
|
# 判断,18790 避开 CodeBee 自身与常用测试端口。--no-open 关掉 dsh 自己开浏览器
|
|
200
242
|
# 的行为(否则它会和 CodeBee 就绪后各开一个标签页)
|
package/app/core/dispatch_log.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import hashlib
|
|
6
7
|
import threading
|
|
7
8
|
import time
|
|
8
9
|
|
|
@@ -40,7 +41,7 @@ def _candidate(row):
|
|
|
40
41
|
def record_event(run_id="", task_id="", task_type="", difficulty="", role="",
|
|
41
42
|
phase="selected", selected="", participants=(), candidates=(),
|
|
42
43
|
fallback=(), selection_reason="", result="", verify_pass=None,
|
|
43
|
-
review_pass=None):
|
|
44
|
+
review_pass=None, trace_id="", span_id="", parent_event_id=""):
|
|
44
45
|
"""追加一条事件;仅保存路由元数据,不保存正文、提示词、密钥或文件内容。"""
|
|
45
46
|
try:
|
|
46
47
|
day = time.strftime("%Y-%m-%d")
|
|
@@ -57,6 +58,20 @@ def record_event(run_id="", task_id="", task_type="", difficulty="", role="",
|
|
|
57
58
|
"selection_reason": _text(selection_reason, 400),
|
|
58
59
|
"result": _text(result, 32),
|
|
59
60
|
}
|
|
61
|
+
# Trace identifiers are routing metadata only. They deliberately do
|
|
62
|
+
# not carry prompt/output contents and remain optional for old callers.
|
|
63
|
+
if run_id:
|
|
64
|
+
from . import tracing
|
|
65
|
+
event["trace_id"] = _text(trace_id or tracing.trace_id(run_id), 64)
|
|
66
|
+
if span_id:
|
|
67
|
+
event["span_id"] = _text(span_id, 32)
|
|
68
|
+
if parent_event_id:
|
|
69
|
+
event["parent_event_id"] = _text(parent_event_id, 64)
|
|
70
|
+
event["event_id"] = hashlib.sha256(
|
|
71
|
+
("%s|%s|%s|%s|%s|%s" % (
|
|
72
|
+
time.time_ns(), run_id, phase, role, selected, result
|
|
73
|
+
)).encode("utf-8")
|
|
74
|
+
).hexdigest()[:24]
|
|
60
75
|
if verify_pass is not None:
|
|
61
76
|
event["verify_pass"] = bool(verify_pass)
|
|
62
77
|
if review_pass is not None:
|
package/app/core/flows.py
CHANGED
|
@@ -78,7 +78,7 @@ BUILTIN_FLOWS = [
|
|
|
78
78
|
{"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
|
|
79
79
|
"goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
|
|
80
80
|
"note": "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
|
|
81
|
-
{"id": "defect_retro", "name": "缺陷复盘", "icon": "i-
|
|
81
|
+
{"id": "defect_retro", "name": "缺陷复盘", "icon": "i-history", "engine": "direct", "builtin": True,
|
|
82
82
|
"goal_hint": "复盘哪个版本/时间段的缺陷(建议附带禅道或 Jira 导出 CSV)",
|
|
83
83
|
"note": "读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)"},
|
|
84
84
|
{"id": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
|
|
@@ -117,7 +117,7 @@ BUILTIN_FLOWS = [
|
|
|
117
117
|
"threshold": 7.0, "rounds": 2,
|
|
118
118
|
"goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
|
|
119
119
|
"note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
120
|
-
{"id": "bid_doc", "name": "标书编制", "icon": "i-
|
|
120
|
+
{"id": "bid_doc", "name": "标书编制", "icon": "i-tasks", "engine": "review", "builtin": True,
|
|
121
121
|
"manuscript": "bid.md",
|
|
122
122
|
"rubric": ["应答完整性", "合规符合度", "方案针对性", "评分点覆盖", "商务清晰度"],
|
|
123
123
|
"threshold": 7.5, "rounds": 2,
|
package/app/core/jobs.py
CHANGED
|
@@ -186,6 +186,21 @@ def _try_start_once(job):
|
|
|
186
186
|
CANCELS.pop(run_id, None)
|
|
187
187
|
return False
|
|
188
188
|
|
|
189
|
+
# A queued run may have spent minutes waiting for a slot or resume timer.
|
|
190
|
+
# Only the successful slot owner starts the execution clock. The CAS
|
|
191
|
+
# prevents a cancellation between slot claim and launch from being revived.
|
|
192
|
+
if current and current.get("task_id"):
|
|
193
|
+
task = store.get_task(current["task_id"])
|
|
194
|
+
if task:
|
|
195
|
+
budget_s = store._task_timeout_s(current.get("timeout_budget_s") or
|
|
196
|
+
task.get("timeout_s"))
|
|
197
|
+
if store.update_run(run_id, expected_status="running",
|
|
198
|
+
timeout_budget_s=budget_s,
|
|
199
|
+
deadline_at=time.time() + budget_s) is None:
|
|
200
|
+
_release_slot(job)
|
|
201
|
+
CANCELS.pop(run_id, None)
|
|
202
|
+
raise DuplicateJobError("运行 %s 已在启动前结束" % run_id)
|
|
203
|
+
|
|
189
204
|
try:
|
|
190
205
|
threading.Thread(target=_run_job, args=(dict(job),),
|
|
191
206
|
name="job-direct-%d" % seq, daemon=True).start()
|
|
@@ -324,16 +339,6 @@ def _requeue_await_slot(job, waits_left=None):
|
|
|
324
339
|
# 带着全新重试额度重新入队,封顶永远打不中)。仍满载则续下一拍。
|
|
325
340
|
with _timer_lock:
|
|
326
341
|
_deferred_timers.pop(run_id, None)
|
|
327
|
-
try:
|
|
328
|
-
from . import store
|
|
329
|
-
run = store.get_run(run_id) or {}
|
|
330
|
-
deadline_at = run.get("deadline_at")
|
|
331
|
-
if deadline_at is not None and float(deadline_at) <= time.time():
|
|
332
|
-
store.update_run(run_id, expected_status="queued", status="timeout",
|
|
333
|
-
ended_at=_now(), error="任务总时限已到(排队等待期间)")
|
|
334
|
-
return
|
|
335
|
-
except (TypeError, ValueError, AttributeError):
|
|
336
|
-
pass
|
|
337
342
|
try:
|
|
338
343
|
started = _try_start_once(job)
|
|
339
344
|
except DuplicateJobError:
|
|
@@ -577,9 +582,8 @@ def _maybe_auto_resume(run_id):
|
|
|
577
582
|
|
|
578
583
|
真实长篇单次运行常因供应商拥堵超时中断;这里在执行线程收尾时自动续跑一次
|
|
579
584
|
续跑(store.retry_task 会带上 inherit),让整个流程真正无人值守。
|
|
580
|
-
2026-09-24 戍边骑奴 9-20
|
|
581
|
-
|
|
582
|
-
恰恰是最该自动接着写的一种中断,纳入。
|
|
585
|
+
2026-09-24 戍边骑奴 9-20 案:「任务总时限已到」终态此前不续跑,
|
|
586
|
+
长篇在慢链天仍可能超过单次预算;超时也是应自动接着写的一种中断。
|
|
583
587
|
同因连撞止损:续跑副本再失败时与本次失败的错误签名比对(2026-09-18
|
|
584
588
|
重写任务 kimi 403 欠费案),一模一样说明退避没换来不同结果,直接落
|
|
585
589
|
终态写明死因,不再烧剩余的退避次数。超时例外:错误签名恒为同一句
|
package/app/core/knowledge.py
CHANGED
|
@@ -154,6 +154,13 @@ def upsert_entry(scope, title, body, tags=None, source="", source_file="",
|
|
|
154
154
|
if hit is not None:
|
|
155
155
|
it = hit
|
|
156
156
|
it["seen"] = int(it.get("seen") or 1) + 1
|
|
157
|
+
# 合并不吞变体标题(与教训库同款记忆卫生):近似题吸收留痕——
|
|
158
|
+
# 溯源可见、变体措辞的后续检索有据;去重+封顶 8。
|
|
159
|
+
if title != str(it.get("title") or ""):
|
|
160
|
+
mt = it.setdefault("merged_titles", [])
|
|
161
|
+
if title not in mt:
|
|
162
|
+
mt.append(title)
|
|
163
|
+
del mt[:-8]
|
|
157
164
|
old_body = str(it.get("body") or "")
|
|
158
165
|
old_rev = it.get("revision_id") or ""
|
|
159
166
|
new_rev = revisions.make_revision("kbrev", body, source=source)
|
package/app/core/modelhub.py
CHANGED
|
@@ -2419,9 +2419,19 @@ def bindable_protocols(agent_kind_or_id):
|
|
|
2419
2419
|
return ("openai",)
|
|
2420
2420
|
if agent_kind_or_id in ("claude-code", "claude"):
|
|
2421
2421
|
return ("anthropic",)
|
|
2422
|
+
if agent_kind_or_id in _LOCAL_CRED_ONLY_TARGETS:
|
|
2423
|
+
return ()
|
|
2422
2424
|
return tuple(_BINDABLE_PROTOCOLS)
|
|
2423
2425
|
|
|
2424
2426
|
|
|
2427
|
+
# 只吃 CLI 本机凭据、不吃 Tutti 两种 wire env 的目标:gemini 本尊只认
|
|
2428
|
+
# GOOGLE_API_KEY/GEMINI_API_KEY(Google AI 协议,Tutti 无此协议面);codebuddy
|
|
2429
|
+
# (claude 兼容系)与 trae-agent 的凭据 env 语义未实机验证。注入它们不认的 env
|
|
2430
|
+
# 只会造出「链显示活着、实际走 CLI 默认模型」的静默假绑定——宁可返回空协议集,
|
|
2431
|
+
# 链上条目全跳过、回落 CLI 本机登录态(recommend_binding 同口径,返回 None)。
|
|
2432
|
+
_LOCAL_CRED_ONLY_TARGETS = ("gemini-cli", "codebuddy", "trae-agent")
|
|
2433
|
+
|
|
2434
|
+
|
|
2425
2435
|
def binding_dead_msg(cli_id):
|
|
2426
2436
|
"""死链失败/告警文案(pipeline 死链闸门与本模块 sync 共用):说明为什么
|
|
2427
2437
|
不回落本机默认 + 该 CLI 需要什么协议的供应商。"""
|
|
@@ -2429,7 +2439,10 @@ def binding_dead_msg(cli_id):
|
|
|
2429
2439
|
protos = bindable_protocols(cli_id)
|
|
2430
2440
|
except Exception:
|
|
2431
2441
|
protos = ()
|
|
2432
|
-
|
|
2442
|
+
if not protos:
|
|
2443
|
+
return ("该 CLI 暂不支持绑定链(凭据注入尚未适配,使用 CLI 本机登录态与"
|
|
2444
|
+
"默认模型);请勿为其配置供应商链")
|
|
2445
|
+
hint = "该 CLI 仅接受 %s 协议的已启用供应商;" % "、".join(protos)
|
|
2433
2446
|
return ("绑定链全部失效(链上供应商已停用/删除/无密钥,或模型已停用),"
|
|
2434
2447
|
"本步判失败、不回落 CLI 本机默认——%s请在「模型调度(可选)」页为该 CLI 指定已启用的供应商" % hint)
|
|
2435
2448
|
|
|
@@ -2442,6 +2455,12 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2442
2455
|
→ 整体回落 CLI 默认(None)。纯链条目(无供应商)不注入 env,只传 -m。
|
|
2443
2456
|
难度路由只在无显式链时生效。
|
|
2444
2457
|
"""
|
|
2458
|
+
# Local-credential-only CLIs must never expose a binding, including legacy
|
|
2459
|
+
# model-only entries that have no provider_id. The generic runner cannot
|
|
2460
|
+
# inject those model names, so returning a partial chain would make the UI
|
|
2461
|
+
# report a bound model while the CLI silently uses its own default.
|
|
2462
|
+
if agent_kind_or_id in _LOCAL_CRED_ONLY_TARGETS:
|
|
2463
|
+
return None
|
|
2445
2464
|
b = _binding_for(agent_kind_or_id)
|
|
2446
2465
|
chain = _binding_chain(b)
|
|
2447
2466
|
provs = {p.get("id"): p for p in providers()}
|
package/app/core/pipeline.py
CHANGED
|
@@ -829,6 +829,7 @@ __CONTEXT__
|
|
|
829
829
|
不要自造评分标准或质量闸门;超出任务目标的「顺手加固」一律不做。
|
|
830
830
|
真实风险照样要找:发现就写进总结,由人决定要不要修。
|
|
831
831
|
- 完成后只用 3-5 句话总结你改了什么、为什么。
|
|
832
|
+
- 交付物从简:回复不写开场白、不复述任务、不堆与改动无关的解释——代码、说明、总结直接给干货(借鉴 claude-token-efficient:输出 token 也是真金白银)。
|
|
832
833
|
__VERIFY_HINT__"""
|
|
833
834
|
|
|
834
835
|
CODE_FIX_PROMPT = """你是一名高级工程师。你之前的实现没有通过验收,请在当前工作目录中直接修复(直接修改/创建文件)。创建/写入的文件一律以 UTF-8 编码保存(PowerShell 显式加 -Encoding UTF8,禁止依赖系统默认编码)。
|
|
@@ -2088,6 +2089,10 @@ CONTENT_DELIVERY_CONTRACTS = {
|
|
|
2088
2089
|
"novel": ("小说作者", [
|
|
2089
2090
|
"遵守用户给定的题材、篇幅、视角和风格;未给出的核心设定不要擅自扩张。",
|
|
2090
2091
|
"用场景、行动和对话推进冲突,人物动机与前后因果保持一致。",
|
|
2092
|
+
# Madman-Architect-Carpenter 写作框架(writing-with-agents 借鉴;Judge=评审链已有):
|
|
2093
|
+
# 先发散倾倒念头再组织结构成文,不要边写边自我审查把点子掐死在半路
|
|
2094
|
+
"成文分三步:先把不设限的情节/画面/台词念头快速倾倒成素材清单,再挑出主线组织成"
|
|
2095
|
+
"结构,最后按结构成文——初稿期不做质量审判。",
|
|
2091
2096
|
]),
|
|
2092
2097
|
"article": ("平台内容主编", [
|
|
2093
2098
|
"标题、开头钩子、正文层级和结尾行动建议要适配目标平台与读者。",
|
|
@@ -2129,6 +2134,9 @@ CONTENT_DELIVERY_CONTRACTS = {
|
|
|
2129
2134
|
# sepia 分场合规则(PR 回复体裁):先答再铺陈;篇幅与利害成正比
|
|
2130
2135
|
"第一句/第一段先给结论或答复(对方要做什么、答应还是不答应),再给必要背景。",
|
|
2131
2136
|
"请求具体到动作与截止时间;篇幅与事情轻重成正比,删掉礼节性空话与自我表扬。",
|
|
2137
|
+
# agentic-inbox 借鉴(线程级交付契约):回复是对话不是独白
|
|
2138
|
+
"回复往来邮件时逐条回应对方提出的每个问题与请求(可用清单),不漏问、不回避;"
|
|
2139
|
+
"行动项落到具体动作+负责人+截止时间。",
|
|
2132
2140
|
]),
|
|
2133
2141
|
"tech_proposal": ("解决方案架构师", [
|
|
2134
2142
|
"覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
|
|
@@ -2167,7 +2175,9 @@ def _content_contract(task):
|
|
|
2167
2175
|
return "\n\n## 本类型交付约束\n" + "\n".join("- " + item for item in spec[1])
|
|
2168
2176
|
|
|
2169
2177
|
# 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
|
|
2170
|
-
#
|
|
2178
|
+
# 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采。
|
|
2179
|
+
# 来源冲突裁决借鉴 deepresearch-agent 记忆层矛盾检测(Majority/Source-Weight 消解);
|
|
2180
|
+
# 子问题缺口补查借鉴 dzhng/deep-research 迭代循环
|
|
2171
2181
|
RESEARCH_APPENDIX = """
|
|
2172
2182
|
|
|
2173
2183
|
## 调研要求(证据链)
|
|
@@ -2175,6 +2185,11 @@ RESEARCH_APPENDIX = """
|
|
|
2175
2185
|
单源信息要标注「仅单一来源」。
|
|
2176
2186
|
- 引用来源在文中用行内链接或脚注标明(域名即可,不编造 URL)。
|
|
2177
2187
|
- 区分「事实」与「观点」:数据/时间/版本号给来源,预测/评价标明是分析。
|
|
2188
|
+
- 来源冲突显式裁决:同一事实多个来源说法不一致时,写明分歧点与各方依据,给出
|
|
2189
|
+
倾向判断和理由(多数来源一致取多数,权威差异按来源可信度加权)——不各说一半,
|
|
2190
|
+
也不悄悄只取其一。
|
|
2191
|
+
- 缺口驱动补查:起草前先列出报告要回答的子问题清单,逐个核对证据是否到位;
|
|
2192
|
+
没找到答案的子问题明确标注「证据不足」,不用泛泛而谈填空。
|
|
2178
2193
|
- 结构硬性要求:报告第一段必须是「**核心结论**」三行以内的要点摘要(结论先行),
|
|
2179
2194
|
之后才展开分层论证 → 风险与局限(说明哪些结论证据不足)。"""
|
|
2180
2195
|
|
package/app/core/planner.py
CHANGED
|
@@ -140,6 +140,8 @@ hard 也不做任务没要的防御性扩展子任务(缓存/重试/监控/通
|
|
|
140
140
|
拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
|
|
141
141
|
detail 必须点名真实存在的文件路径;files 列出该子任务会改动的文件(新建的写目标路径)。
|
|
142
142
|
**没探索过就不要凭想象编路径**——计划的可执行性取决于它对真实代码库的贴合度。
|
|
143
|
+
detail 的清晰度按「初级工程师测试」校验:假设执行者没有项目上下文、不做隐含判断,
|
|
144
|
+
只读 detail 就能照做不跑偏(借鉴 superpowers)。
|
|
143
145
|
若工作目录为空(全新项目),files 写计划新建的文件路径。
|
|
144
146
|
|
|
145
147
|
## 开发目标
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "七猫真机校准(2026-09-19 凌晨定稿):建书=book-manage「新建小说」→站点弹层(click_match+确认)→information 表单(fill_label/radio/级联分类/tags 四组各1-3个+确定/主角名平铺/简介);书名仅允许,:!?中文标点(·被拦)。发章=编辑器填好后「立即发布」落草稿箱(未签约书限制)→草稿行「立即发布」→「更正序号去发布」→「确认发布」三层弹窗→章节待审核。正文下限 1000 字(min_chapter_chars)。book-upload?id=<书id> 直达编辑器。;2026-09-24 加固:二级分类选完 settle 1s
|
|
2
|
+
"_note": "七猫真机校准(2026-09-19 凌晨定稿):建书=book-manage「新建小说」→站点弹层(click_match+确认)→information 表单(fill_label/radio/级联分类/tags 四组各1-3个+确定/主角名平铺/简介);书名仅允许,:!?中文标点(·被拦)。发章=编辑器填好后「立即发布」落草稿箱(未签约书限制)→草稿行「立即发布」→「更正序号去发布」→「确认发布」三层弹窗→章节待审核。正文下限 1000 字(min_chapter_chars)。book-upload?id=<书id> 直达编辑器。;2026-09-24 加固:二级分类选完 settle 1s 再点「添加标签」(下拉关闭动画吞点击致标签弹层未开、组名切换失败案);2026-09-24 夜修:平台编辑器「立即发布」变为弹重型确认框(点后页面假死、章节未提交、草稿箱亦空——旧「立即发布落草稿箱」语义过时),改点「存草稿」再走草稿箱三层发布(草稿行立即发布→更正序号→确认发布,22:47 实证成功路径)",
|
|
3
3
|
"probe_form": [
|
|
4
4
|
{
|
|
5
5
|
"do": "navigate",
|
|
@@ -180,9 +180,13 @@
|
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"do": "click_real",
|
|
183
|
-
"text": "
|
|
183
|
+
"text": "存草稿",
|
|
184
184
|
"tries": 25
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
"do": "sleep",
|
|
188
|
+
"s": 2
|
|
189
|
+
},
|
|
186
190
|
{
|
|
187
191
|
"do": "navigate",
|
|
188
192
|
"url": "about:blank"
|
|
@@ -234,4 +238,4 @@
|
|
|
234
238
|
]
|
|
235
239
|
}
|
|
236
240
|
]
|
|
237
|
-
}
|
|
241
|
+
}
|
package/app/core/runner.py
CHANGED
|
@@ -2030,16 +2030,18 @@ def as_scores(gj):
|
|
|
2030
2030
|
def extract_scores_from_text(text):
|
|
2031
2031
|
"""评审解析第四道网(借鉴 BAML 的宽容提取):模型把分数写成散文键值对
|
|
2032
2032
|
完全不出 JSON 时(2026-09-18 真实案例:kimi 正文提分),从文本直接抓
|
|
2033
|
-
「维度:N 分」模式。返回 scores dict 或 {}。
|
|
2033
|
+
「维度:N 分」模式。返回 scores dict 或 {}。
|
|
2034
|
+
2026-09-25 批1 补:分隔符不止冒号——短横线「正确性 - 9 分」、全角等号
|
|
2035
|
+
「=」、箭头「→」三种真实输出形态此前全漏,格式错被误判质量差白烧修复轮。"""
|
|
2034
2036
|
if not text:
|
|
2035
2037
|
return {}
|
|
2036
2038
|
scores = {}
|
|
2037
2039
|
for m in re.finditer(
|
|
2038
|
-
r"[\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z0-9 ]{0,11}"
|
|
2039
|
-
r"[
|
|
2040
|
-
dim = m.group(
|
|
2040
|
+
r"([\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z0-9 ]{0,11})"
|
|
2041
|
+
r"\s*[::\-==→]\s*(\d{1,2}(?:\.\d)?)\s*分?", text):
|
|
2042
|
+
dim = m.group(1).strip()
|
|
2041
2043
|
try:
|
|
2042
|
-
val = float(m.group(
|
|
2044
|
+
val = float(m.group(2))
|
|
2043
2045
|
except ValueError:
|
|
2044
2046
|
continue
|
|
2045
2047
|
if not dim or dim in scores or not (1.0 <= val <= 10.0):
|
package/app/core/skill_scan.py
CHANGED
|
@@ -37,6 +37,14 @@ _PATTERNS = [
|
|
|
37
37
|
(r"\bcrontab\b|schtasks|LaunchAgents|CurrentVersion\\Run|StartupItems",
|
|
38
38
|
"持久化", "注册定时任务/自启动项(持久化驻留特征)"),
|
|
39
39
|
(r"stratum\+tcp|xmrig|cryptonight", "可疑意图", "加密货币挖矿特征"),
|
|
40
|
+
# 云元数据/内网端点探测(批4 治理 09-25:169.254.169.254 窃取云凭据是
|
|
41
|
+
# 真实攻击面,此前零覆盖)。只认高信号形态:链路本地元数据地址/云元数据
|
|
42
|
+
# 主机名,或私网地址搭配凭据路径——本地起服务提 127.0.0.1:3000 不误报。
|
|
43
|
+
(r"169\.254\.169\.254|169\.254\.\d{1,3}\.\d{1,3}|metadata\.google\.internal",
|
|
44
|
+
"可疑意图", "触碰云元数据端点(窃取云凭据特征)"),
|
|
45
|
+
(r"(?:192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3})[^。\n]{0,40}"
|
|
46
|
+
r"(?:meta-data|credentials|secret|token|\.ssh)",
|
|
47
|
+
"可疑意图", "私网地址搭配凭据路径(内网探测特征)"),
|
|
40
48
|
# 敏感信息读取
|
|
41
49
|
(r"os\.environ|process\.env|getenv", "环境读取", "读取环境变量(可能带走密钥)"),
|
|
42
50
|
(r"\.ssh/|\.aws/|\.npmrc|\.gitconfig|credentials|\.env\b", "敏感文件", "触碰凭据/密钥文件路径"),
|
package/app/core/skills.py
CHANGED
|
@@ -410,6 +410,14 @@ def upsert_lesson(scope, title, content, source="", category=None, dim=None):
|
|
|
410
410
|
it.update({k: new_rev[k] for k in ("revision_id", "content_sha256")})
|
|
411
411
|
it["seen"] = int(it.get("seen") or 1) + 1
|
|
412
412
|
it["updated_at"] = _now()
|
|
413
|
+
# 合并不吞变体标题(批2 记忆卫生):近似题被吸收时留痕——溯源可见,
|
|
414
|
+
# 且后续按变体措辞检索也能命中(bigram 检索只看 survivor 自己的题)。
|
|
415
|
+
# 去重+封顶 8 条,防止长跑自动化把字段撑成第二正文。
|
|
416
|
+
if title != str(it.get("title") or ""):
|
|
417
|
+
mt = it.setdefault("merged_titles", [])
|
|
418
|
+
if title not in mt:
|
|
419
|
+
mt.append(title)
|
|
420
|
+
del mt[:-8]
|
|
413
421
|
# 合并时不降级已有分类:除非本次归到了明确类别,或该条原本没有分类
|
|
414
422
|
if cat != LESSON_UNCATEGORIZED or not it.get("category"):
|
|
415
423
|
it["category"] = cat
|
|
@@ -599,7 +607,13 @@ def block_for(task, scope_override=None, *, stable_order=False, run_id=None):
|
|
|
599
607
|
for x in lessons:
|
|
600
608
|
# 编号可见:复盘官据此点名「哪条已沉淀教训没防住本次问题」,
|
|
601
609
|
# 归因才能落到单条而不是整批(见 note_outcome)。
|
|
602
|
-
|
|
610
|
+
# karma 标注(批5 对称补齐):won≥2 才标——1 次可能是巧合,噪音为零;
|
|
611
|
+
# 与知识库[有据]置信标同构,模型可按实证强度校准采信。
|
|
612
|
+
# run 内稳定:won 只在 run 收尾回写,任务中途字节不变。
|
|
613
|
+
karma = ""
|
|
614
|
+
if int(x.get("won") or 0) >= 2:
|
|
615
|
+
karma = "(已验证有效 %d 次)" % int(x.get("won") or 0)
|
|
616
|
+
lines.append("- [%s] **%s**%s:%s" % (x["id"], x["title"], karma, x["content"]))
|
|
603
617
|
used.append(x["id"])
|
|
604
618
|
lesson_ids.append(x["id"])
|
|
605
619
|
lesson_part = ("### 【本项目已沉淀的教训(历史评审反复出现,务必规避;方括号内为教训编号)】\n"
|
package/app/core/store.py
CHANGED
|
@@ -18,7 +18,7 @@ import threading
|
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
from . import artifacts, paths, runner, volumes
|
|
21
|
+
from . import artifacts, paths, runner, tracing, volumes
|
|
22
22
|
|
|
23
23
|
LOCK = threading.RLock()
|
|
24
24
|
_TASKS = {}
|
|
@@ -33,6 +33,9 @@ LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 buil
|
|
|
33
33
|
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
34
|
MIN_TASK_TIMEOUT_S = 1
|
|
35
35
|
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
36
|
+
REVIEW_TASK_TIMEOUT_S = 7200
|
|
37
|
+
LONG_TASK_TIMEOUT_S = 14400
|
|
38
|
+
SERIAL_EXTRA_CHAPTER_S = 1800
|
|
36
39
|
|
|
37
40
|
# ---------------------------------------------------------------- 状态版本(SSE 事件驱动)
|
|
38
41
|
# 任何落盘写都算状态变化:SSE 连接等版本号变化才构建/推送全量状态,
|
|
@@ -95,6 +98,26 @@ def _task_timeout_s(value=None):
|
|
|
95
98
|
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
96
99
|
|
|
97
100
|
|
|
101
|
+
def _default_timeout_s(task):
|
|
102
|
+
"""Choose a bounded default from the amount of work, not one hour for all."""
|
|
103
|
+
env_value = os.environ.get("TUTTI_TASK_TIMEOUT_S")
|
|
104
|
+
if env_value not in (None, ""):
|
|
105
|
+
return _task_timeout_s(env_value)
|
|
106
|
+
serial = task.get("serial")
|
|
107
|
+
if isinstance(serial, dict) and serial.get("chapters"):
|
|
108
|
+
try:
|
|
109
|
+
chapters = max(1, min(20, int(serial["chapters"])))
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
chapters = 8
|
|
112
|
+
return min(MAX_TASK_TIMEOUT_S,
|
|
113
|
+
LONG_TASK_TIMEOUT_S + max(0, chapters - 8) * SERIAL_EXTRA_CHAPTER_S)
|
|
114
|
+
if task.get("engine") == "code":
|
|
115
|
+
return LONG_TASK_TIMEOUT_S
|
|
116
|
+
if task.get("engine") == "review":
|
|
117
|
+
return REVIEW_TASK_TIMEOUT_S
|
|
118
|
+
return DEFAULT_TASK_TIMEOUT_S
|
|
119
|
+
|
|
120
|
+
|
|
98
121
|
# ---------------------------------------------------------------- 任务
|
|
99
122
|
|
|
100
123
|
def create_task(payload):
|
|
@@ -159,7 +182,6 @@ def create_task(payload):
|
|
|
159
182
|
}
|
|
160
183
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
161
184
|
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
|
-
task["timeout_s"] = _task_timeout_s(payload.get("timeout_s"))
|
|
163
185
|
# 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
|
|
164
186
|
# 固化参数已保证「改流程不影响本任务」,digest 再补上可对账的一面——
|
|
165
187
|
# 续跑/重试时若流程已改,flows.flow_drift 能给出明确诊断而非静默换新。
|
|
@@ -293,6 +315,9 @@ def create_task(payload):
|
|
|
293
315
|
if vper:
|
|
294
316
|
s["volume_chapters"] = vper
|
|
295
317
|
task["serial"] = s
|
|
318
|
+
raw_timeout = payload.get("timeout_s")
|
|
319
|
+
task["timeout_s"] = (_task_timeout_s(raw_timeout)
|
|
320
|
+
if raw_timeout not in (None, "") else _default_timeout_s(task))
|
|
296
321
|
critics = payload.get("critics")
|
|
297
322
|
if isinstance(critics, list) and critics:
|
|
298
323
|
task["critics"] = [str(c) for c in critics]
|
|
@@ -717,9 +742,17 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
|
717
742
|
"cost_usd": 0.0, "tokens": 0, "error": "",
|
|
718
743
|
"verdict": None, "summary": "",
|
|
719
744
|
}
|
|
745
|
+
# Persist trace identity at run creation so replay remains stable across
|
|
746
|
+
# process restarts and legacy readers can still use the normal run shape.
|
|
747
|
+
run["trace_schema_version"] = tracing.SCHEMA_VERSION
|
|
748
|
+
run["trace_id"] = tracing.trace_id(run["id"])
|
|
749
|
+
run["root_span_id"] = tracing.root_span_id(run["id"])
|
|
720
750
|
if kind == "orchestration" and task_id:
|
|
721
751
|
task = get_task(task_id) or {}
|
|
722
|
-
run["
|
|
752
|
+
run["timeout_budget_s"] = _task_timeout_s(task.get("timeout_s"))
|
|
753
|
+
# Provisional for direct callers; jobs resets it at worker claim so
|
|
754
|
+
# queued/backoff time does not shorten the actual execution budget.
|
|
755
|
+
run["deadline_at"] = time.time() + run["timeout_budget_s"]
|
|
723
756
|
run["execution_snapshot"] = _execution_snapshot(task)
|
|
724
757
|
try:
|
|
725
758
|
eta = _RUN_ESTIMATOR(
|
|
@@ -1746,10 +1779,9 @@ def update_task_params(task_id, patch):
|
|
|
1746
1779
|
if task.get("direct_agent") != av and (av or task.get("direct_agent")):
|
|
1747
1780
|
task["direct_agent"] = av
|
|
1748
1781
|
changed = True
|
|
1749
|
-
#
|
|
1750
|
-
#
|
|
1751
|
-
#
|
|
1752
|
-
# 统一钳制 [1s, 7天],脏值回落默认;空闲时才可改(与三件套同闸)。
|
|
1782
|
+
# 任务总时限(秒):建任务时按工作量给默认值,此处支持空闲时显式改。
|
|
1783
|
+
# 下次重试/续跑按新值重新给预算;已在跑的 run 保持原截止时间。
|
|
1784
|
+
# 统一钳制 [1s, 7天],脏值回落基础默认。
|
|
1753
1785
|
raw_ts = patch.get("timeout_s")
|
|
1754
1786
|
if raw_ts not in (None, ""):
|
|
1755
1787
|
new_ts = _task_timeout_s(raw_ts)
|
|
@@ -1780,6 +1812,11 @@ def add_step(run_id, role, agent_id, agent_label, note="", model="", provider=""
|
|
|
1780
1812
|
# 只记身份不记密钥;老数据无此键,读取一律 .get
|
|
1781
1813
|
"provider": str(provider or "")[:120],
|
|
1782
1814
|
"status": "running", "started_at": time.strftime("%H:%M:%S"),
|
|
1815
|
+
"started_at_epoch": time.time(),
|
|
1816
|
+
"trace_id": str(run.get("trace_id") or tracing.trace_id(run_id)),
|
|
1817
|
+
"span_id": tracing.span_id(run_id, n),
|
|
1818
|
+
"parent_span_id": str(run.get("root_span_id") or
|
|
1819
|
+
tracing.root_span_id(run_id)),
|
|
1783
1820
|
"ended_at": None, "duration_s": None, "exit_code": None,
|
|
1784
1821
|
"summary": "", "log": None, "cost_usd": 0.0, "tokens": 0,
|
|
1785
1822
|
}
|
|
@@ -1852,6 +1889,7 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1852
1889
|
if s["n"] == n:
|
|
1853
1890
|
s["status"] = status
|
|
1854
1891
|
s["ended_at"] = time.strftime("%H:%M:%S")
|
|
1892
|
+
s["ended_at_epoch"] = time.time()
|
|
1855
1893
|
# 步骤摘要是 CLI 文本的汇聚点(失败时=错误尾巴,成功时=智能体结论):
|
|
1856
1894
|
# 落盘前统一清洗,避免 ANSI/覆写/乱码墙进 UI 与报告
|
|
1857
1895
|
s["summary"] = runner.clean_cli_text(summary)
|
|
@@ -1862,6 +1900,9 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
|
|
|
1862
1900
|
s["model"] = str(model)[:80]
|
|
1863
1901
|
if duration_s is not None:
|
|
1864
1902
|
s["duration_s"] = round(duration_s, 1)
|
|
1903
|
+
elif s.get("started_at_epoch") is not None:
|
|
1904
|
+
s["duration_s"] = round(max(
|
|
1905
|
+
0.0, s["ended_at_epoch"] - s["started_at_epoch"]), 1)
|
|
1865
1906
|
if output is not None:
|
|
1866
1907
|
# output 是智能体正文(对话气泡直读):只剥 ANSI,不做噪声折叠
|
|
1867
1908
|
# ——正文里的装饰性长串是作者写的,不能替它省略
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Run/step/event tracing primitives.
|
|
3
|
+
|
|
4
|
+
The shape intentionally follows OpenTelemetry/OpenInference's trace -> span
|
|
5
|
+
relationship without adding a third-party dependency. It is a read model of
|
|
6
|
+
the existing run and dispatch ledgers: no prompt, stdout, stderr, credentials,
|
|
7
|
+
or artifact contents are copied into the trace.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from .redact import scrub_text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _stable(prefix: str, *parts: object, length: int = 32) -> str:
|
|
21
|
+
raw = prefix + ":" + ":".join(str(part or "") for part in parts)
|
|
22
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:length]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def trace_id(run_id: object) -> str:
|
|
26
|
+
"""Return a deterministic 128-bit trace id for a run."""
|
|
27
|
+
return _stable("codebee.trace", run_id, length=32)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def root_span_id(run_id: object) -> str:
|
|
31
|
+
return _stable("codebee.root", run_id, length=16)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def span_id(run_id: object, step_no: object) -> str:
|
|
35
|
+
return _stable("codebee.step", run_id, step_no, length=16)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def event_id(run_id: object, event: dict) -> str:
|
|
39
|
+
"""Stable event id used for deduplication during replay."""
|
|
40
|
+
return _stable(
|
|
41
|
+
"codebee.event",
|
|
42
|
+
run_id,
|
|
43
|
+
event.get("ts"),
|
|
44
|
+
event.get("phase"),
|
|
45
|
+
event.get("role"),
|
|
46
|
+
event.get("selected"),
|
|
47
|
+
event.get("result"),
|
|
48
|
+
length=24,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _epoch(value):
|
|
53
|
+
if value is None or isinstance(value, (int, float)):
|
|
54
|
+
return value
|
|
55
|
+
text = str(value).strip()
|
|
56
|
+
if not text:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
|
|
60
|
+
except (TypeError, ValueError, OverflowError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _duration(step: dict):
|
|
65
|
+
try:
|
|
66
|
+
value = float(step.get("duration_s"))
|
|
67
|
+
return round(max(0.0, value), 3)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
started = _epoch(step.get("started_at_epoch"))
|
|
70
|
+
ended = _epoch(step.get("ended_at_epoch"))
|
|
71
|
+
if started is not None and ended is not None:
|
|
72
|
+
return round(max(0.0, ended - started), 3)
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _safe_event(event: dict, run_id: str) -> dict:
|
|
77
|
+
"""Keep only routing metadata from a dispatch event."""
|
|
78
|
+
allowed = (
|
|
79
|
+
"ts", "task_id", "task_type", "difficulty", "role", "phase",
|
|
80
|
+
"selected", "participants", "candidates", "fallback",
|
|
81
|
+
"selection_reason", "result", "verify_pass", "review_pass",
|
|
82
|
+
"trace_id", "span_id", "parent_event_id", "event_id",
|
|
83
|
+
)
|
|
84
|
+
item = {key: event[key] for key in allowed if key in event}
|
|
85
|
+
item.setdefault("trace_id", trace_id(run_id))
|
|
86
|
+
item.setdefault("event_id", event_id(run_id, item))
|
|
87
|
+
return _redact(item)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _redact(value, limit=600):
|
|
91
|
+
"""Redact strings even when a legacy event predates dispatch_log scrub."""
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
return {key: _redact(item, 240) for key, item in value.items()}
|
|
94
|
+
if isinstance(value, list):
|
|
95
|
+
return [_redact(item, 240) for item in value]
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
return scrub_text(value, limit=limit)
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_trace(run: dict, events=()) -> dict:
|
|
102
|
+
"""Build a stable, redacted trace view from an existing run dictionary."""
|
|
103
|
+
run_id = str(run.get("id") or "")
|
|
104
|
+
tid = str(run.get("trace_id") or trace_id(run_id))
|
|
105
|
+
rid = str(run.get("root_span_id") or root_span_id(run_id))
|
|
106
|
+
spans = []
|
|
107
|
+
for step in run.get("steps") or ():
|
|
108
|
+
if not isinstance(step, dict):
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
number = int(step.get("n"))
|
|
112
|
+
except (TypeError, ValueError):
|
|
113
|
+
continue
|
|
114
|
+
span = {
|
|
115
|
+
"trace_id": tid,
|
|
116
|
+
"span_id": str(step.get("span_id") or span_id(run_id, number)),
|
|
117
|
+
"parent_span_id": str(step.get("parent_span_id") or rid),
|
|
118
|
+
"name": "agent.%s" % str(step.get("role") or "step")[:80],
|
|
119
|
+
"step": number,
|
|
120
|
+
"status": str(step.get("status") or "unknown"),
|
|
121
|
+
"agent": str(step.get("agent") or "")[:80],
|
|
122
|
+
"model": str(step.get("model") or "")[:120],
|
|
123
|
+
"provider": str(step.get("provider") or "")[:120],
|
|
124
|
+
"started_at": step.get("started_at_epoch"),
|
|
125
|
+
"ended_at": step.get("ended_at_epoch"),
|
|
126
|
+
"duration_s": _duration(step),
|
|
127
|
+
"tokens": step.get("tokens", 0),
|
|
128
|
+
"cost_usd": step.get("cost_usd", 0.0),
|
|
129
|
+
"exit_code": step.get("exit_code"),
|
|
130
|
+
}
|
|
131
|
+
spans.append(span)
|
|
132
|
+
safe_events = [_safe_event(item, run_id) for item in (events or ())
|
|
133
|
+
if isinstance(item, dict)]
|
|
134
|
+
return {
|
|
135
|
+
"version": SCHEMA_VERSION,
|
|
136
|
+
"trace_id": tid,
|
|
137
|
+
"root_span_id": rid,
|
|
138
|
+
"run_id": run_id,
|
|
139
|
+
"status": str(run.get("status") or "unknown"),
|
|
140
|
+
"started_at": run.get("started_at"),
|
|
141
|
+
"ended_at": run.get("ended_at"),
|
|
142
|
+
"spans": spans,
|
|
143
|
+
"events": safe_events,
|
|
144
|
+
}
|
package/app/core/usage.py
CHANGED
|
@@ -478,13 +478,14 @@ def routing_stats(task_type="", role="", agent="", model="", provider="",
|
|
|
478
478
|
|
|
479
479
|
|
|
480
480
|
def _group(records, key):
|
|
481
|
-
"""按 key 聚合:{name: {calls, ok, tokens, input, output, cached, cache_rate, cost_usd, duration_s}}。"""
|
|
481
|
+
"""按 key 聚合:{name: {calls, ok, tokens, input, output, cached, cache_rate, cost_usd, duration_s, saved}}。"""
|
|
482
482
|
groups = {}
|
|
483
483
|
for r in records:
|
|
484
484
|
name = str(r.get(key) or "") or "unknown"
|
|
485
485
|
g = groups.setdefault(name, {"calls": 0, "ok": 0, "tokens": 0,
|
|
486
486
|
"input": 0, "output": 0, "cached": 0,
|
|
487
|
-
"cost_usd": 0.0, "duration_s": 0.0
|
|
487
|
+
"cost_usd": 0.0, "duration_s": 0.0,
|
|
488
|
+
"saved": 0})
|
|
488
489
|
g["calls"] += 1
|
|
489
490
|
if r.get("ok"):
|
|
490
491
|
g["ok"] += 1
|
|
@@ -492,6 +493,7 @@ def _group(records, key):
|
|
|
492
493
|
g["input"] += _num(r, "input")
|
|
493
494
|
g["output"] += _num(r, "output")
|
|
494
495
|
g["cached"] += _num(r, "cached")
|
|
496
|
+
g["saved"] += _num(r, "saved")
|
|
495
497
|
g["cost_usd"] = round(g["cost_usd"] + float(r.get("cost_usd") or 0.0), 4)
|
|
496
498
|
g["duration_s"] = round(g["duration_s"] + float(r.get("duration_s") or 0.0), 1)
|
|
497
499
|
# §07 验收指标:缓存命中率(cached / (input + cached)),与 totals.cache_rate 同口径
|
|
@@ -606,6 +608,7 @@ def summary(days=30, recent_limit=30):
|
|
|
606
608
|
"cached": g.get("cached", 0),
|
|
607
609
|
"cache_rate": g.get("cache_rate", 0.0),
|
|
608
610
|
"duration_s": g.get("duration_s", 0.0),
|
|
611
|
+
"saved": g.get("saved", 0),
|
|
609
612
|
"cost_usd": round(g.get("cost_usd", 0.0), 4)})
|
|
610
613
|
else:
|
|
611
614
|
for d in sorted(day_groups):
|
|
@@ -616,6 +619,7 @@ def summary(days=30, recent_limit=30):
|
|
|
616
619
|
"cached": g.get("cached", 0),
|
|
617
620
|
"cache_rate": g.get("cache_rate", 0.0),
|
|
618
621
|
"duration_s": g.get("duration_s", 0.0),
|
|
622
|
+
"saved": g.get("saved", 0),
|
|
619
623
|
"cost_usd": g["cost_usd"]})
|
|
620
624
|
|
|
621
625
|
# 按日按模型 tokens(范围内;与 by_day 同一天序列,无数据天给空表)
|
package/app/main.py
CHANGED
|
@@ -47,6 +47,19 @@ MIME = {".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=ut
|
|
|
47
47
|
".webmanifest": "application/manifest+json; charset=utf-8"}
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _has_task_plan(wd):
|
|
51
|
+
"""活计划存在性(批3):task_plan.md 落 .codebee/ 不进成品扫描,
|
|
52
|
+
前端据此给「任务计划」入口(file 端点按名可取)。resolve+parents 守卫。"""
|
|
53
|
+
try:
|
|
54
|
+
if not wd:
|
|
55
|
+
return False
|
|
56
|
+
root = Path(wd).resolve()
|
|
57
|
+
p = (root / ".codebee" / "task_plan.md").resolve()
|
|
58
|
+
return (root in p.parents) and p.is_file()
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
def _utf8_bytes(data):
|
|
51
64
|
"""文本预览出口的编码守卫:字节流非合法 UTF-8 时按 GBK 解码后重编码为
|
|
52
65
|
UTF-8 再发。子代理在中文 Windows 上可能把工作区文件落成 GBK,直接透传
|
|
@@ -433,13 +446,24 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
433
446
|
# 文件变动是并行活动的噪音,不算这个任务的成品
|
|
434
447
|
if files and not store.task_step_count(run.get("task_id") or ""):
|
|
435
448
|
files = []
|
|
449
|
+
# 活计划入口(批3):存在性单列标志
|
|
450
|
+
has_plan = _has_task_plan(wd)
|
|
436
451
|
return self._json(200, {"workdir": wd, "files": files,
|
|
437
|
-
"task_id": run.get("task_id") or ""
|
|
452
|
+
"task_id": run.get("task_id") or "",
|
|
453
|
+
"has_plan": has_plan})
|
|
438
454
|
m = re.match(r"^/api/runs/([^/]+)/assets$", path)
|
|
439
455
|
if m:
|
|
440
456
|
if not store.get_run(m.group(1)):
|
|
441
457
|
return self._json(404, {"error": "not found"})
|
|
442
458
|
return self._json(200, {"assets": store.run_asset_refs(m.group(1))})
|
|
459
|
+
m = re.match(r"^/api/runs/([^/]+)/trace$", path)
|
|
460
|
+
if m:
|
|
461
|
+
run = store.get_run(m.group(1))
|
|
462
|
+
if not run:
|
|
463
|
+
return self._json(404, {"error": "not found"})
|
|
464
|
+
from core import dispatch_log, tracing
|
|
465
|
+
return self._json(200, {"trace": tracing.build_trace(
|
|
466
|
+
run, dispatch_log.replay(run_id=m.group(1), limit=200))})
|
|
443
467
|
m = re.match(r"^/api/runs/([^/]+)/preview$", path)
|
|
444
468
|
if m:
|
|
445
469
|
# 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
|
package/app/ui/app.js
CHANGED
|
@@ -6582,8 +6582,18 @@ function fmtSize(n) {
|
|
|
6582
6582
|
/* 成品文件行(检查器「成品文件」与主栏「成果」分区共用一套标记):
|
|
6583
6583
|
* fc-row:文件行 + 弹窗预览按钮同行排布,避免按钮独占一行参差不齐。
|
|
6584
6584
|
* 点文件名与点「预览」同款 artPopup 弹窗,行为一致不 surprise。 */
|
|
6585
|
-
function artifactsChips(runId, files, prevRun) {
|
|
6586
|
-
|
|
6585
|
+
function artifactsChips(runId, files, prevRun, hasPlan) {
|
|
6586
|
+
// 活计划入口(批3):task_plan.md 不进成品扫描,存在时首行给「任务计划」
|
|
6587
|
+
// 独立入口(file 端点按名可取,浏览器直接看勾选进度)。
|
|
6588
|
+
const planChip = hasPlan
|
|
6589
|
+
? '<span class="fc-row">' +
|
|
6590
|
+
'<a class="file-chip artifact-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" +
|
|
6591
|
+
encodeURIComponent(".codebee/task_plan.md")) + '" target="_blank" rel="noopener" ' +
|
|
6592
|
+
'title=".codebee/task_plan.md">' +
|
|
6593
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-git-branch"/></svg>' +
|
|
6594
|
+
'<span class="p">' + t("任务计划") + "</span><i>plan</i></a></span>"
|
|
6595
|
+
: "";
|
|
6596
|
+
return planChip + files.map((f) => {
|
|
6587
6597
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
6588
6598
|
// 与上一版对比(OpenCreator「版本化对比」借鉴):同名文本文件在上一版
|
|
6589
6599
|
// run 也存在时给 diff 入口;prevRun=null(无历史/清单拉取失败)静默不给
|
|
@@ -6679,9 +6689,10 @@ async function loadArtifacts(runId) {
|
|
|
6679
6689
|
const mainBox = detailOwns ? $("rd-arts") : null;
|
|
6680
6690
|
// 成果分区徽章:文件数(终态才有产出,运行中不计);只随 detailOwns 一起刷
|
|
6681
6691
|
const fb = detailOwns ? document.querySelector('#rd-tabs .rd-tab[data-tab="result"] .rd-badge') : null;
|
|
6682
|
-
if (fb) {
|
|
6683
|
-
fb.
|
|
6684
|
-
|
|
6692
|
+
if (fb) { const cnt = files.length + (d.has_plan ? 1 : 0);
|
|
6693
|
+
fb.textContent = cnt ? String(cnt) : "";
|
|
6694
|
+
fb.className = "rd-badge" + (cnt ? "" : " hidden"); }
|
|
6695
|
+
if (!files.length && !d.has_plan) {
|
|
6685
6696
|
if (box) box.innerHTML = '<span class="insp-hint">' + esc(t("本次运行没有在工作目录里产出新文件。")) + "</span>";
|
|
6686
6697
|
if (mainBox) { mainBox.classList.add("hidden"); mainBox.innerHTML = ""; }
|
|
6687
6698
|
return;
|
|
@@ -6701,7 +6712,7 @@ async function loadArtifacts(runId) {
|
|
|
6701
6712
|
} catch (e) { /* 上一版清单不可用就不给对比 */ }
|
|
6702
6713
|
}
|
|
6703
6714
|
}
|
|
6704
|
-
const chips = artifactsChips(runId, files, prevRun);
|
|
6715
|
+
const chips = artifactsChips(runId, files, prevRun, d.has_plan);
|
|
6705
6716
|
if (box) box.innerHTML = head + '<div class="file-chips">' + chips + "</div>";
|
|
6706
6717
|
if (mainBox) { mainBox.classList.remove("hidden");
|
|
6707
6718
|
mainBox.innerHTML = head + '<div class="file-chips">' + chips + "</div>"; }
|
|
@@ -9820,6 +9831,7 @@ function renderSkills() {
|
|
|
9820
9831
|
'onclick="skillCatFilter(\'' + esc(x.category) + '\')" title="' + t("只看该类") + '">' + esc(t(x.category)) + "</span>" : "") +
|
|
9821
9832
|
'<span class="tag">' + esc(x.scope === "*" ? t("通用") : x.scope) + "</span>" +
|
|
9822
9833
|
(x.seen > 1 ? '<span class="tag">' + t("出现 ") + x.seen + t(" 次") + "</span>" : "") +
|
|
9834
|
+
((x.merged_titles || []).length ? '<span class="tag" title="' + t("被近似题合并吸收的变体标题数") + '">' + t("吸收 ") + x.merged_titles.length + t(" 个近似题") + "</span>" : "") +
|
|
9823
9835
|
(x.hits ? '<span class="tag">' + t("已注入 ") + x.hits + t(" 次") + "</span>" : "") +
|
|
9824
9836
|
(x.won > 0 ? '<span class="tag ok" title="' + t("注入后任务过审的次数(真实帮上忙)") + '">' + t("有效 ") + x.won + "</span>" : "") +
|
|
9825
9837
|
(x.lost > 0 ? '<span class="tag" style="color:var(--bad)" title="' + t("注入后任务仍失败的次数(没防住)") + '">' + t("失守 ") + x.lost + "</span>" : "") +
|
|
@@ -9910,6 +9922,7 @@ function kbCard(x) {
|
|
|
9910
9922
|
(x.confidence === "high" ? '<span class="tag ok" title="' + esc(t("产出材料中有明确来源或数据支撑")) + '">' + esc(t("有据")) + "</span>" : "") +
|
|
9911
9923
|
'<span class="tag">' + esc(x.scope === "*" ? t("通用") : x.scope) + "</span>" +
|
|
9912
9924
|
(x.seen > 1 ? '<span class="tag">' + t("出现 ") + x.seen + t(" 次") + "</span>" : "") +
|
|
9925
|
+
((x.merged_titles || []).length ? '<span class="tag" title="' + t("被近似题合并吸收的变体标题数") + '">' + t("吸收 ") + x.merged_titles.length + t(" 个近似题") + "</span>" : "") +
|
|
9913
9926
|
(x.hits ? '<span class="tag">' + t("已注入 ") + x.hits + t(" 次") + "</span>" : "") +
|
|
9914
9927
|
(x.enabled === false ? '<span class="tag">' + t("已停用") + "</span>" : "");
|
|
9915
9928
|
const tags = (x.tags || []).map((tg) =>
|
|
@@ -12717,6 +12730,7 @@ function renderUsage() {
|
|
|
12717
12730
|
// 用量页不做环比:范围由用户任选(含「全部」),没有等长上一期可比,硬算会是假数。
|
|
12718
12731
|
const dayTok = (u.by_day || []).map((d) => d.tokens || 0);
|
|
12719
12732
|
const dayDur = (u.by_day || []).map((d) => d.duration_s || 0);
|
|
12733
|
+
const daySaved = (u.by_day || []).map((d) => d.saved || 0);
|
|
12720
12734
|
// 首字延迟只有内置直连的流式调用可测(CLI 事件流没有逐 token 时刻):
|
|
12721
12735
|
// perf_samples=0 时显示"暂无可测数据",不能显示成 0ms。
|
|
12722
12736
|
const fmtMs = (x) => (x >= 1000 ? (x / 1000).toFixed(1) + "s" : Math.round(x) + "ms");
|
|
@@ -12746,7 +12760,8 @@ function renderUsage() {
|
|
|
12746
12760
|
kpiCard(t("压缩省量"), tot.compaction_saved ? fmtTok(tot.compaction_saved) : "—",
|
|
12747
12761
|
tot.compaction_saved
|
|
12748
12762
|
? t("上下文折叠净省,与消耗并列成账")
|
|
12749
|
-
: t("长对话压力压缩后在此累计"), false, "i-cpu",
|
|
12763
|
+
: t("长对话压力压缩后在此累计"), false, "i-cpu",
|
|
12764
|
+
{ tone: "ok", spark: daySaved }),
|
|
12750
12765
|
].join("");
|
|
12751
12766
|
const note = $("usage-note");
|
|
12752
12767
|
if (note) {
|
package/app/ui/i18n.js
CHANGED
|
@@ -1593,6 +1593,9 @@
|
|
|
1593
1593
|
"知道了": "Got it",
|
|
1594
1594
|
"新版本更新内容": "What's new in the new version",
|
|
1595
1595
|
"已注入 ": "injected ",
|
|
1596
|
+
"吸收 ": "absorbed ",
|
|
1597
|
+
" 个近似题": " near-dup titles",
|
|
1598
|
+
"被近似题合并吸收的变体标题数": "Variant titles absorbed by near-duplicate merging",
|
|
1596
1599
|
"做法": "Best practice",
|
|
1597
1600
|
"已验证有效的做法(程序性记忆)": "Verified working procedure (procedural memory)",
|
|
1598
1601
|
"已清除 ": "Cleared ",
|
|
@@ -1906,6 +1909,7 @@
|
|
|
1906
1909
|
"丢弃该文件的全部未提交改动?此操作不可恢复。": "Discard all uncommitted changes of this file? This cannot be undone.",
|
|
1907
1910
|
"删除这个未跟踪文件?此操作不可恢复。": "Delete this untracked file? This cannot be undone.",
|
|
1908
1911
|
"任务分支": "Task branch",
|
|
1912
|
+
"任务计划": "Task plan",
|
|
1909
1913
|
"已配置代码版本隔离,但任务分支尚未创建(检出失败见运行错误)。": "Code-version isolation is configured, but the task branch has not been created yet (see run error if checkout failed).",
|
|
1910
1914
|
"藏青底色 + 天蓝强调,夜间长时间盯任务更沉静(默认)": "Navy base + sky-blue accent; calmer for long night sessions (default)",
|
|
1911
1915
|
"黑白灰 + 蓝色强调,ChatGPT 式清爽配色": "Black/white/gray + blue accent — the clean ChatGPT-style palette",
|
package/package.json
CHANGED