codebee 0.1.19 → 0.1.21
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 +13 -0
- package/README.md +21 -14
- package/app/core/automation.py +6 -6
- package/app/core/capability.py +16 -2
- package/app/core/catalog.py +1 -1
- package/app/core/covergen.py +40 -4
- package/app/core/dispatch.py +141 -0
- package/app/core/flows.py +5 -0
- package/app/core/jobs.py +281 -166
- package/app/core/manager.py +30 -23
- package/app/core/market_remote.py +66 -22
- package/app/core/modelhub.py +145 -11
- package/app/core/pipeline.py +197 -35
- package/app/core/router.py +18 -7
- package/app/core/runner.py +2 -1
- package/app/core/selfupdate.py +24 -14
- package/app/core/settings.py +3 -3
- package/app/core/skills.py +43 -0
- package/app/core/store.py +20 -14
- package/app/core/zentao.py +26 -6
- package/app/main.py +20 -12
- package/app/pet.py +2 -2
- package/app/ui/app.js +87 -63
- package/app/ui/i18n.js +22 -3
- package/app/ui/index.html +11 -11
- package/app/ui/style.css +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## v0.1.21(2026-09-21)
|
|
10
|
+
|
|
11
|
+
- 任务默认直接启动,不再进入容量等待队列;达到并发保护上限立即失败并明确提示稍后重试,历史 queued 记录启动时自动接管或收口。
|
|
12
|
+
- 修复取消竞态:编排、管理、自升级、禅道修复在取消后不会复活;自动续跑重启恢复不再受隐藏数量上限影响,并显示明确的续跑时间。
|
|
13
|
+
- 定时任务、重试、续写、答疑和禅道修复统一使用同一套启动失败收口逻辑,避免任务长期停留在待启动状态。
|
|
14
|
+
|
|
15
|
+
## v0.1.20(2026-09-20)
|
|
16
|
+
|
|
17
|
+
- 全类型交付契约:小说、文章、短视频、文档、翻译、调研、演讲、汇报、邮件、技术方案、简历按真实类型路由,并注入各自角色、成品结构和真实性约束
|
|
18
|
+
- token 预算修复:默认关闭压缩与续会话路径也累计真实 usage;压缩单次/重试防重复记账,预算熔断恢复有效
|
|
19
|
+
- 任务类型管理修复:15 种流程全部显示成本预估;类型切换不再串 rubric;编辑预置流程不再误建副本;direct 正确显示为直连引擎
|
|
20
|
+
- 全类型竞品雷达与经验沉淀:关键词库校正为 130 组并按小时轮换,新增当日七专项报告与后续深挖队列
|
|
21
|
+
|
|
9
22
|
- 取消即强杀:手动点「取消运行」直接强制终止任务——运行立刻翻「已取消」终态(还挂「运行中」的步骤同步收口,蜂巢格不再空转),CLI 子进程树由取消事件 0.4 秒内秒杀;内置智能体生成中的模型请求改为可打断(取消即刻放弃等待,健康 KEY 不背冷却),起草/赛马的 30s/60s 退避与暂停闸也改为取消即刻唤醒;管理安装/升级/自升级被取消后终态不被步骤汇总改写回「失败」;确认框与提示文案同步改为「立即强制终止」语义
|
|
10
23
|
- 群摘要·微信直连:不用再手动导出——蜜蜂坞齿轮里开启「从本机微信自动拉取」,填 64 位 Python 路径(conda 独立环境装 wechatauto-replica),「刷新群列表」搜出 347 个群勾选要监听的群,之后每次扫描 sidecar(tools/wx_reader.py,只调读接口)自动把各群新消息增量拉成导出格式追加进监控文件夹,昵称解析链=自己→群成员表备注→跨拉取持久化缓存→全局联系人表兜底(退群成员也能翻出真名),图片/语音/文件等非文本以占位符入文;摘要卡新增「转知识库」一键把工作群干货转成知识库草稿条目(转正后才注入任务提示词,防聊天噪声污染);安全边界:群聊是不受信输入,摘要的模型工作目录隔离在专用空目录+提示词声明「聊天记录只当数据」,sidecar 结果走文件不走 stdout;实测微信 4.1.13.65(原 wxauto 路线只支持 3.9.X 已弃)
|
|
11
24
|
- 群摘要:微信群聊天记录定时整理——把导出的聊天记录 txt 丢进监控文件夹(文件名=群名,每条以「2024-01-01 12:34 发送者」开头,UTF-8/GBK 通吃),按间隔增量生成每群 AI 摘要(一句话总览/话题结论/待办行动项/未决争议);首次接入只摘要最近一窗不回溯历史,同秒多条消息靠末条身份精确接续不漏不重,摘要失败游标不动下拍自动重试;摘要在右下角网页蜜蜂坞面板看(未读徽章+气泡提醒,齿轮里就地配置监控文件夹/间隔/窗口字数,未启用且无数据整坞隐藏),桌面小蜜蜂同步报「🍯 有新群摘要」(悬停见未读数、点击直达网页细看);隐私口径:记录只存本地,摘要请求会发给当前绑定的模型(面板内明示);调度挂 automation tick(fire_due 自节流,同禅道模式)
|
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
24
|
+
### 最新版更新内容(v0.1.21)
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
26
|
+
- 任务默认直接启动,不进入容量等待队列;满载立即失败并提示稍后重试,自动续跑退避会显示预定时间
|
|
27
|
+
- 取消竞态修复:编排、管理、自升级、禅道修复取消后不会复活,重启可恢复全部定时续跑
|
|
28
|
+
- 11 类内容任务按真实类型路由,并带各自角色、成品结构和真实性约束
|
|
29
|
+
- token 预算恢复有效:默认路径与续会话均累计真实 usage,压缩重试不重复记账
|
|
30
|
+
- 15 种流程成本预估全覆盖;修复 rubric 串值、预置流程误建副本和 direct 标签
|
|
29
31
|
<!-- relnotes:end -->
|
|
30
32
|
|
|
31
33
|
---
|
|
@@ -92,8 +94,8 @@ OpenCode / Continue / Cursor / Trae 十种工具的配置,一次带入全部
|
|
|
92
94
|
|
|
93
95
|
### 更多内置能力
|
|
94
96
|
|
|
95
|
-
- **
|
|
96
|
-
|
|
97
|
+
- **15 种任务类型**:直接执行、代码、小说、连载小说、自媒体文章、短视频脚本、
|
|
98
|
+
文档、翻译、扫榜选材、调研报告、演讲稿、工作汇报、商务邮件、技术方案、简历;全部支持自定义流程
|
|
97
99
|
(引擎 / 评审维度 / 阈值 / 轮数 / 提示词覆盖);
|
|
98
100
|
- **质量闸门与裁决**:评审失败≠0 分照过、降级大纲不开写、超时但已落盘的稿件照常
|
|
99
101
|
送评审;需要人拍板的事项进「待裁决」队列(徽章 + 提示音),`tools/bee.py`
|
|
@@ -108,7 +110,7 @@ OpenCode / Continue / Cursor / Trae 十种工具的配置,一次带入全部
|
|
|
108
110
|
跑完整代码链,修完自动合并;双端问题修完我方部分后带修复报告转派另一端负责人,
|
|
109
111
|
非我方转回报告人;测试指错人也按排查结论改派;修复失败自动升级转派;群机器人
|
|
110
112
|
全程播报(设置页「禅道」子页配置);
|
|
111
|
-
-
|
|
113
|
+
- **多任务并发**:任务默认立即启动、无等待队列,并发保护上限默认 12;连载任务超时/中断自动
|
|
112
114
|
断点续跑;
|
|
113
115
|
- **远程访问**:手机同一 WiFi 直开、Tailscale 外网可达、Cloudflare Tunnel 固定
|
|
114
116
|
域名——8 位访问令牌保护,多端实时同步(SSE),单设备控制权防互踩;
|
|
@@ -199,7 +201,8 @@ python tools\bee.py discard <task> :: 否决——删除任务分支(需输
|
|
|
199
201
|
## 任务类型与自定义流程
|
|
200
202
|
|
|
201
203
|
主表单的「类型」下拉即流程列表,点「管理」可新增/编辑自定义流程(引擎、评审维度、
|
|
202
|
-
阈值、轮数、产出文件名、起草/评审提示词覆盖)。内置
|
|
204
|
+
阈值、轮数、产出文件名、起草/评审提示词覆盖)。内置 15 种;禅道工单是自动转成
|
|
205
|
+
`code` 修复任务的集成入口,不另占菜单类型:
|
|
203
206
|
|
|
204
207
|
| 类型 | 引擎 | 流程 |
|
|
205
208
|
|---|---|---|
|
|
@@ -207,13 +210,17 @@ python tools\bee.py discard <task> :: 否决——删除任务分支(需输
|
|
|
207
210
|
| 💻 代码 | code | 实现 → 验证 → 跨厂商评审 → 自动修复/换将 |
|
|
208
211
|
| 📖 小说 | review | 起草 → 多维评审(情节/人物/文笔/节奏/吸引力)→ 修订 → 发布门禁 |
|
|
209
212
|
| 📚 连载小说 | review | 大纲 → 逐章起草/评审/修订 → 全局一致性评审 → 合并成书(断点续跑/继续连载) |
|
|
213
|
+
| 📰 自媒体文章 | review | 平台化标题/开头/结构 → 多维评审 → 修订 → 发布门禁 |
|
|
214
|
+
| 🎬 短视频脚本 | review | 画面 + 口播 + 字幕/音效 → 多维评审 → 修订 → 发布门禁 |
|
|
210
215
|
| 📝 文档 | review | 起草 → 多维评审(准确性/结构/表达/实用)→ 修订 → 门禁 |
|
|
211
216
|
| 🌐 翻译 | review | 起草 → 多维评审(忠实度/流畅度/术语/风格)→ 修订 → 门禁 |
|
|
217
|
+
| 📈 扫榜选材 | direct | 抓七猫 + 番茄公开榜单 → 跨平台题材/人设/切入点洞察 |
|
|
212
218
|
| 🔍 调研报告 | review | 起草 → 多维评审(全面性/深度/论据/结论)→ 修订 → 门禁 |
|
|
213
219
|
| 🎤 演讲稿 | review | 起草 → 多维评审(主题/结构/感染力/语言)→ 修订 → 门禁 |
|
|
214
|
-
|
|
|
220
|
+
| 📅 工作汇报 / ✉️ 商务邮件 / 📋 技术方案 / 📄 简历 | review | 各自专属角色、交付约束与评审维度 |
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
三种引擎:
|
|
223
|
+
- **direct 引擎**:单智能体直接执行目标,保留会话上下文,适合问答、探索和无需评审链的快捷任务;
|
|
217
224
|
- **code 引擎**:实现者在工作目录直接改代码 → 执行验证命令(确定性结果)→ 跨厂商评审
|
|
218
225
|
(结构化 JSON)→ 不通过则自动修复循环(至多 2 轮)→ 仍失败自动换将全量重实现;
|
|
219
226
|
- **review 引擎**:作者起草产出文件 → 多个评审智能体按维度独立打分 + 提问题 →
|
|
@@ -311,10 +318,10 @@ start-public.bat
|
|
|
311
318
|
|
|
312
319
|
## 多任务并发
|
|
313
320
|
|
|
314
|
-
|
|
315
|
-
互不打扰——每个任务独立线程,运行数据按 run
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
任务执行器采用直接启动模式(并发保护上限默认 12,设置页「编排设置」可调 1-12):多个任务同时跑、
|
|
322
|
+
互不打扰——每个任务独立线程,运行数据按 run 隔离,不进入等待队列。达到保护上限时本次请求明确失败并提示稍后重试;并发数调小只影响后续任务,已有任务继续完成。
|
|
323
|
+
连载任务超时/中断会自动断点续跑;重试前按明确时间退避,防止在网关限流墙上连续撞死,
|
|
324
|
+
这段等待不占执行位,也不属于任务容量排队。
|
|
318
325
|
|
|
319
326
|
## 编排设置(编排者模型)
|
|
320
327
|
|
package/app/core/automation.py
CHANGED
|
@@ -292,7 +292,7 @@ def _recover_after_restart():
|
|
|
292
292
|
|
|
293
293
|
def _launch_run(t):
|
|
294
294
|
"""拉起一次真实编排运行:与 main.py 的 /api/tasks 走同一条链路
|
|
295
|
-
(store.create_task → store.create_run → jobs
|
|
295
|
+
(store.create_task → store.create_run → jobs 直接启动),返回 run_id。
|
|
296
296
|
测试可把本函数打成 stub,绝不真调 LLM/CLI。"""
|
|
297
297
|
payload = {"type": t.get("flow") or DEFAULT_FLOW,
|
|
298
298
|
"title": ("%s %s" % (t.get("name") or "定时任务",
|
|
@@ -309,15 +309,15 @@ def _launch_run(t):
|
|
|
309
309
|
"task_id": task["id"]})
|
|
310
310
|
return run["id"]
|
|
311
311
|
except Exception:
|
|
312
|
-
#
|
|
312
|
+
# 启动失败必须收口,不能留下看似仍在启动的任务/run。
|
|
313
313
|
# Keep the public error generic; the detailed traceback stays in the
|
|
314
314
|
# service log and the run record remains useful for diagnostics.
|
|
315
|
-
log.exception("automation:
|
|
315
|
+
log.exception("automation: 运行启动失败 task=%s run=%s",
|
|
316
316
|
(task or {}).get("id"), (run or {}).get("id"))
|
|
317
317
|
if run:
|
|
318
318
|
try:
|
|
319
319
|
store.update_run(run["id"], status="failed",
|
|
320
|
-
error="
|
|
320
|
+
error="任务启动失败,本次未排队,请稍后重试",
|
|
321
321
|
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
322
322
|
except Exception:
|
|
323
323
|
log.exception("automation: 运行失败收口失败 run=%s", run.get("id"))
|
|
@@ -334,7 +334,7 @@ def _launch_run(t):
|
|
|
334
334
|
def _fire(snapshot, now):
|
|
335
335
|
"""触发一个到期任务:拉起运行并推进 last_run/next_run/run_count/last_status。
|
|
336
336
|
once 触发完自动停用。拉起失败只记 error,next_run 照常推进(下个周期重试)。"""
|
|
337
|
-
status = "
|
|
337
|
+
status = "started"
|
|
338
338
|
try:
|
|
339
339
|
_launch_run(snapshot)
|
|
340
340
|
except Exception as e:
|
|
@@ -527,7 +527,7 @@ def run_now(tid):
|
|
|
527
527
|
return None, None
|
|
528
528
|
now = datetime.now()
|
|
529
529
|
run_id = ""
|
|
530
|
-
status = "
|
|
530
|
+
status = "started"
|
|
531
531
|
try:
|
|
532
532
|
with _LOCK:
|
|
533
533
|
snapshot = dict(_TASKS[tid])
|
package/app/core/capability.py
CHANGED
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
15
|
|
|
16
|
+
from .dispatch import TYPE_DIMENSIONS
|
|
17
|
+
|
|
16
18
|
# 能力维度(先固化清单,schema 见设计稿)
|
|
17
19
|
DIMS = ("writing", "coding", "reasoning", "vision")
|
|
18
20
|
|
|
@@ -33,6 +35,9 @@ def classify_task_type(task) -> str:
|
|
|
33
35
|
explicit = str((task or {}).get("task_type") or "").strip().lower()
|
|
34
36
|
if explicit in DIMS:
|
|
35
37
|
return explicit
|
|
38
|
+
preset = str((task or {}).get("type") or "").strip().lower()
|
|
39
|
+
if preset in TYPE_DIMENSIONS:
|
|
40
|
+
return TYPE_DIMENSIONS[preset]
|
|
36
41
|
text = "%s %s" % ((task or {}).get("type") or "",
|
|
37
42
|
(task or {}).get("goal") or "")
|
|
38
43
|
scores = {}
|
|
@@ -92,7 +97,8 @@ def resolve_binding_by_task(task, providers, bindings):
|
|
|
92
97
|
TIER_ORDER = {"budget": 0, "standard": 1, "premium": 2}
|
|
93
98
|
|
|
94
99
|
|
|
95
|
-
def cascade_reorder(agent, tier_of
|
|
100
|
+
def cascade_reorder(agent, tier_of, *, providers=None, pricing=None,
|
|
101
|
+
difficulty="easy", task_type="", role=""):
|
|
96
102
|
"""FrugalGPT 式便宜优先:easy 任务把 call_chain 按 tier 升序稳定重排。
|
|
97
103
|
|
|
98
104
|
agent 来自 modelhub.bind_agent(可能带 call_chain);tier_of(chain_entry)
|
|
@@ -102,6 +108,14 @@ def cascade_reorder(agent, tier_of):
|
|
|
102
108
|
chain = (agent or {}).get("call_chain") or []
|
|
103
109
|
if len(chain) < 2:
|
|
104
110
|
return agent
|
|
111
|
+
if providers is not None and (task_type or role):
|
|
112
|
+
from .dispatch import rank_model_entries
|
|
113
|
+
ranked, _decisions = rank_model_entries(
|
|
114
|
+
chain, {p.get("id"): p for p in providers or []},
|
|
115
|
+
pricing or {}, difficulty, task_type, role)
|
|
116
|
+
out = dict(agent)
|
|
117
|
+
out["call_chain"] = ranked
|
|
118
|
+
return out
|
|
105
119
|
|
|
106
120
|
def key(entry):
|
|
107
121
|
return TIER_ORDER.get(tier_of(entry) or "standard", 1)
|
|
@@ -127,4 +141,4 @@ def make_tier_lookup(providers):
|
|
|
127
141
|
if isinstance(m, dict) and m.get("name") == (entry or {}).get("model"):
|
|
128
142
|
return m.get("tier") or p.get("tier")
|
|
129
143
|
return p.get("tier")
|
|
130
|
-
return tier_of
|
|
144
|
+
return tier_of
|
package/app/core/catalog.py
CHANGED
|
@@ -154,7 +154,7 @@ DEFAULT_CATALOG = [
|
|
|
154
154
|
"note": "DeepSeek 官方 agent harness(dsh,profile 插件架构);无头是「一次性任务」——"
|
|
155
155
|
"答完即退、无交互后续、不支持会话恢复;任务只走位置参数(超长提示词受 Windows "
|
|
156
156
|
"命令行上限约 32k 约束);模型写进 ~/.dsh/settings.yaml 的 agent-default-model.model;"
|
|
157
|
-
"
|
|
157
|
+
"密钥由运行时自动调度或「模型调度(可选)」注入 DEEPSEEK_API_KEY(优先级最高);端点注入 DEEPSEEK_BASE_URL,"
|
|
158
158
|
"但若 settings.yaml 已固定 llm-deepseek.baseURL,则以它为准(settings 高于 env)",
|
|
159
159
|
"detect": {"cli": "dsh"},
|
|
160
160
|
"orch": {"kind": "generic", "command": "dsh",
|
package/app/core/covergen.py
CHANGED
|
@@ -42,17 +42,53 @@ _IMAGE_MODEL_KEYWORDS = ("cogview", "dall-e", "gpt-image", "flux", "kolors",
|
|
|
42
42
|
_SIZES = ("768x1344", "1024x1024") # 竖版优先,方图兜底
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _chapter_facts(workdir, limit=3):
|
|
46
|
+
"""从最新章节抽取视觉事实(借鉴 poster-v2 的 novel fact extraction):
|
|
47
|
+
人物/场景/关键意象的原文片段——封面元素来自真实剧情而非干瘪题材词。
|
|
48
|
+
读不到章节(单稿/未开写)返回空串,提示词回落题材+简介。"""
|
|
49
|
+
if not workdir:
|
|
50
|
+
return ""
|
|
51
|
+
import glob as _glob
|
|
52
|
+
try:
|
|
53
|
+
chapters = sorted(_glob.glob(str(workdir) + "/chapter-*.md"))
|
|
54
|
+
if not chapters:
|
|
55
|
+
return ""
|
|
56
|
+
facts = []
|
|
57
|
+
for p in chapters[-limit:]:
|
|
58
|
+
try:
|
|
59
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
60
|
+
txt = f.read(4000)
|
|
61
|
+
# 取开头(多为场景/人物动作)+ 关键句粗筛
|
|
62
|
+
head = " ".join(txt.split("\n")[1:6])
|
|
63
|
+
if head.strip():
|
|
64
|
+
facts.append(head.strip()[:200])
|
|
65
|
+
except OSError:
|
|
66
|
+
continue
|
|
67
|
+
return "\n".join(facts)[:900]
|
|
68
|
+
except Exception:
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
45
72
|
def _cover_prompt(task):
|
|
46
|
-
"""从任务与建书资料拼图像提示词:场景氛围向,不要文字(平台会自行压字)。
|
|
73
|
+
"""从任务与建书资料拼图像提示词:场景氛围向,不要文字(平台会自行压字)。
|
|
74
|
+
|
|
75
|
+
素材优先级:章节事实抽取(真实剧情的人物/场景/意象)> 题材+简介。
|
|
76
|
+
poster-v2 借鉴:视觉元素来自 novel facts 才有「这是这本书的封面」的辨识度。"""
|
|
47
77
|
bm = ((task.get("book_meta") or {}).get("fanqie") or {}).get("data") or {}
|
|
48
78
|
if not isinstance(bm, dict):
|
|
49
79
|
bm = {}
|
|
50
80
|
title = bm.get("书名") or task.get("title") or ""
|
|
51
81
|
genre = bm.get("类型") or bm.get("分类") or ""
|
|
52
82
|
brief = (bm.get("一句话简介") or bm.get("简介") or task.get("goal") or "")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
facts = _chapter_facts(task.get("workdir") or "")
|
|
84
|
+
base = ("竖版小说封面插画,画面中不要出现任何文字。题材:%s %s。故事梗概:%s。"
|
|
85
|
+
% (genre, title, str(brief)[:300]))
|
|
86
|
+
if facts:
|
|
87
|
+
base += ("\n\n以下是最新的剧情片段(从中提取主要人物形象、标志性场景与核心意象"
|
|
88
|
+
"作为画面主体——让封面一眼可辨「这是这本书」):\n%s" % facts)
|
|
89
|
+
base += ("\n商业网文封面质感:主体人物或核心场景突出,色彩浓郁有冲击力,"
|
|
90
|
+
"构图上方留白便于后期压标题。")
|
|
91
|
+
return base
|
|
56
92
|
|
|
57
93
|
|
|
58
94
|
def _pick_key(prov):
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""统一调度评分:任务画像、CLI 亲和度与模型链排序。
|
|
3
|
+
|
|
4
|
+
本模块只做纯计算,不读写配置和运行状态。调用方先完成协议、启停、健康、
|
|
5
|
+
密钥等硬约束过滤,再把可用候选交给这里评分;相同分数保持用户原顺序。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TYPE_DIMENSIONS = {
|
|
13
|
+
"direct": "reasoning",
|
|
14
|
+
"code": "coding",
|
|
15
|
+
"novel": "writing",
|
|
16
|
+
"serial_novel": "writing",
|
|
17
|
+
"article": "writing",
|
|
18
|
+
"video_script": "writing",
|
|
19
|
+
"doc": "writing",
|
|
20
|
+
"translation": "writing",
|
|
21
|
+
"rank_scan": "reasoning",
|
|
22
|
+
"research": "reasoning",
|
|
23
|
+
"speech": "writing",
|
|
24
|
+
"weekly_report": "writing",
|
|
25
|
+
"email": "writing",
|
|
26
|
+
"tech_proposal": "reasoning",
|
|
27
|
+
"resume": "writing",
|
|
28
|
+
"zentao": "coding",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_KIND_AFFINITY = {
|
|
32
|
+
"coding": {"codex": 10, "aider": 8, "opencode": 7, "qwen": 6,
|
|
33
|
+
"claude": 5, "generic": 2},
|
|
34
|
+
"writing": {"claude": 10, "qwen": 7, "generic": 5, "codex": 3,
|
|
35
|
+
"opencode": 2, "aider": 0},
|
|
36
|
+
"reasoning": {"claude": 9, "codex": 8, "qwen": 6, "opencode": 4,
|
|
37
|
+
"generic": 3, "aider": 1},
|
|
38
|
+
"vision": {"codex": 10, "claude": 8, "qwen": 5, "opencode": 2,
|
|
39
|
+
"generic": 1, "aider": 0},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_TIER_SCORE = {
|
|
43
|
+
"easy": {"budget": 18.0, "standard": 8.0, "premium": -6.0},
|
|
44
|
+
"default": {"budget": 2.0, "standard": 6.0, "premium": 5.0},
|
|
45
|
+
"hard": {"budget": -8.0, "standard": 4.0, "premium": 12.0},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def task_dimension(task_type, role=""):
|
|
50
|
+
"""把预置任务与步骤角色归一为 writing/coding/reasoning/vision。"""
|
|
51
|
+
ttype = str(task_type or "").strip().lower()
|
|
52
|
+
role = str(role or "").strip().lower()
|
|
53
|
+
if ttype in ("writing", "coding", "reasoning", "vision"):
|
|
54
|
+
return ttype
|
|
55
|
+
if role in ("plan", "review", "critique", "selector", "qa") \
|
|
56
|
+
or "review" in role or "critique" in role:
|
|
57
|
+
return "reasoning"
|
|
58
|
+
return TYPE_DIMENSIONS.get(ttype, "reasoning")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def agent_affinity(kind, task_type, role=""):
|
|
62
|
+
"""CLI 类型对任务维度的温和偏好;只作加分,不覆盖健康与历史信号。"""
|
|
63
|
+
dim = task_dimension(task_type, role)
|
|
64
|
+
score = float((_KIND_AFFINITY.get(dim) or {}).get(kind, 0))
|
|
65
|
+
if role == "review" or "critique" in str(role or ""):
|
|
66
|
+
score += {"claude": 4, "codex": 3, "qwen": 2}.get(kind, 0)
|
|
67
|
+
return score, "%s 匹配 %+.1f" % (dim, score)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _model_meta(provider, model):
|
|
71
|
+
for item in (provider or {}).get("models") or []:
|
|
72
|
+
if isinstance(item, dict) and item.get("name") == model:
|
|
73
|
+
return item
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _price_score(pricing, model, difficulty):
|
|
78
|
+
price = (pricing or {}).get(model) or {}
|
|
79
|
+
try:
|
|
80
|
+
# 输出通常比输入贵且更影响整步成本,按 2 倍权重估算。
|
|
81
|
+
blended = max(0.0, float(price.get("in") or 0.0)
|
|
82
|
+
+ 2.0 * float(price.get("out") or 0.0))
|
|
83
|
+
except (TypeError, ValueError):
|
|
84
|
+
return 0.0, "价格未知"
|
|
85
|
+
if blended <= 0:
|
|
86
|
+
return 0.0, "价格未知"
|
|
87
|
+
magnitude = max(0.0, math.log10(blended + 1.0))
|
|
88
|
+
score = -min(18.0, magnitude * (7.0 if difficulty == "easy" else 2.0))
|
|
89
|
+
return score, "估算价 %.3g(%+.1f)" % (blended, score)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def score_model_entry(entry, providers, pricing, difficulty, task_type="", role=""):
|
|
93
|
+
"""给已通过硬约束的模型链条目评分,返回 (score, explanation)。"""
|
|
94
|
+
provider = (providers or {}).get(entry.get("provider_id")) \
|
|
95
|
+
or entry.get("provider") or {}
|
|
96
|
+
model = entry.get("model") or ""
|
|
97
|
+
meta = _model_meta(provider, model)
|
|
98
|
+
try:
|
|
99
|
+
priority = max(1, int(meta.get("priority") or 99))
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
priority = 99
|
|
102
|
+
quality = 0.0 if priority == 99 else max(-8.0, 20.0 - (priority - 1) * 4.0)
|
|
103
|
+
tier = meta.get("tier") or provider.get("tier") or "standard"
|
|
104
|
+
tier_score = (_TIER_SCORE.get(difficulty) or {}).get(tier, 0.0)
|
|
105
|
+
price_score, price_reason = _price_score(pricing, model, difficulty)
|
|
106
|
+
dim = task_dimension(task_type, role)
|
|
107
|
+
strengths = provider.get("strengths") if isinstance(provider.get("strengths"), list) else []
|
|
108
|
+
strength_score = 10.0 if dim in strengths else 0.0
|
|
109
|
+
if difficulty == "easy":
|
|
110
|
+
quality *= 0.25
|
|
111
|
+
elif difficulty != "hard":
|
|
112
|
+
# 普通任务兼顾质量、成本与能力匹配;是否真的重排由调用方决定,
|
|
113
|
+
# 因而手工链仍可保持原顺序,自动推荐则能使用这组平衡分。
|
|
114
|
+
quality *= 0.65
|
|
115
|
+
vision_score = 0.0
|
|
116
|
+
if dim == "vision":
|
|
117
|
+
vision_score = 24.0 if meta.get("image_in") else -24.0
|
|
118
|
+
total = quality + tier_score + price_score + strength_score + vision_score
|
|
119
|
+
reason = ("质量 %+.1f,档位 %s %+.1f,%s,能力 %s %+.1f"
|
|
120
|
+
% (quality, tier, tier_score, price_reason, dim,
|
|
121
|
+
strength_score + vision_score))
|
|
122
|
+
return round(total, 2), reason
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def rank_model_entries(entries, providers, pricing, difficulty,
|
|
126
|
+
task_type="", role="", force=False):
|
|
127
|
+
"""稳定排序模型链并返回脱敏决策明细;default 难度保持人工顺序。"""
|
|
128
|
+
rows = []
|
|
129
|
+
for index, entry in enumerate(entries or []):
|
|
130
|
+
score, reason = score_model_entry(
|
|
131
|
+
entry, providers, pricing, difficulty, task_type, role)
|
|
132
|
+
rows.append((score, index, entry, reason))
|
|
133
|
+
if difficulty in ("easy", "hard") or force:
|
|
134
|
+
rows.sort(key=lambda row: (-row[0], row[1]))
|
|
135
|
+
ranked = [row[2] for row in rows]
|
|
136
|
+
decisions = [{"provider_id": row[2].get("provider_id") or "",
|
|
137
|
+
"provider": (row[2].get("provider") or {}).get("name") or "",
|
|
138
|
+
"model": row[2].get("model") or "",
|
|
139
|
+
"score": row[0], "reason": row[3]}
|
|
140
|
+
for row in rows]
|
|
141
|
+
return ranked, decisions
|
package/app/core/flows.py
CHANGED
|
@@ -187,6 +187,11 @@ def _apply_overrides(base, ov):
|
|
|
187
187
|
f["rounds"] = max(1, min(5, int(v)))
|
|
188
188
|
except Exception:
|
|
189
189
|
pass
|
|
190
|
+
elif k == "best_of":
|
|
191
|
+
try:
|
|
192
|
+
f["best_of"] = max(1, min(3, int(v)))
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
190
195
|
elif k in ("manuscript", "verify_command"):
|
|
191
196
|
s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
|
|
192
197
|
s = re.sub(r"\.{2,}", "_", s).lstrip(".")
|