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/app/core/pipeline.py
CHANGED
|
@@ -321,7 +321,7 @@ def _binding_dead_msg(agent):
|
|
|
321
321
|
return modelhub.binding_dead_msg(agent.get("id") or "")
|
|
322
322
|
except Exception:
|
|
323
323
|
return ("绑定链全部失效,本步判失败、不回落 CLI 本机默认——"
|
|
324
|
-
"
|
|
324
|
+
"请在「模型调度(可选)」页为该 CLI 指定已启用的供应商")
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
|
|
@@ -502,6 +502,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
502
502
|
"error_code": ErrorCode.ENV_BLOCK, "sid": "",
|
|
503
503
|
"raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
|
|
504
504
|
"model": agent.get("model")}
|
|
505
|
+
usage_recorded = False
|
|
505
506
|
if _compaction_enabled() and not resume:
|
|
506
507
|
# Phase 2(1D):撑爆 → 压缩 → 守门重试;同时把 usage 累进 token_meter(1C)
|
|
507
508
|
session = _get_session(session_run_id)
|
|
@@ -511,6 +512,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
511
512
|
images=images, require_tools=require_tools)
|
|
512
513
|
|
|
513
514
|
def _call(p, **kw):
|
|
515
|
+
nonlocal usage_recorded
|
|
514
516
|
# 模型可见即已记录(§1A 不变量):入参/出参先落 session 日志
|
|
515
517
|
session.append("user_message", {"content": p, "role": role},
|
|
516
518
|
turn_id=str(step["n"]))
|
|
@@ -523,6 +525,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
523
525
|
from .token_meter import token_meter
|
|
524
526
|
token_meter.accumulate(session_run_id, r.get("usage"),
|
|
525
527
|
model=r.get("model") or "")
|
|
528
|
+
usage_recorded = True
|
|
526
529
|
except Exception:
|
|
527
530
|
pass
|
|
528
531
|
return r
|
|
@@ -534,6 +537,15 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
534
537
|
res = runner.run_agent(agent, prompt, workdir=workdir, readonly=readonly,
|
|
535
538
|
timeout=timeout, cancel_event=ev, log_path=str(log_abs),
|
|
536
539
|
resume=resume, images=images, require_tools=require_tools)
|
|
540
|
+
# 默认关闭压缩和 resume 都走直通分支,也必须把真实 usage 送进预算表;否则
|
|
541
|
+
# 下一步永远看到 used=0,max_tokens_per_run 只是一个无效设置。
|
|
542
|
+
if not usage_recorded:
|
|
543
|
+
try:
|
|
544
|
+
from .token_meter import token_meter
|
|
545
|
+
token_meter.accumulate(session_run_id, res.get("usage"),
|
|
546
|
+
model=res.get("model") or "")
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
537
549
|
return res
|
|
538
550
|
|
|
539
551
|
|
|
@@ -895,12 +907,18 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
895
907
|
else:
|
|
896
908
|
impl, route["implementer"] = router.pick(agents, "implement", "code", stats)
|
|
897
909
|
if impl is None:
|
|
898
|
-
store.update_run(run_id,
|
|
910
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
911
|
+
error="没有可用智能体", ended_at=_now())
|
|
899
912
|
return
|
|
900
913
|
|
|
901
914
|
# ---- 规划
|
|
902
915
|
if mode == "auto":
|
|
903
916
|
_wait_gate(run_id, ev)
|
|
917
|
+
# 项目记忆注入(借鉴 agentmemory 持久记忆):同工作目录此前代码任务留下的
|
|
918
|
+
# 架构事实,让规划器不再对代码库一无所知
|
|
919
|
+
_pm = _read_project_memory(workdir)
|
|
920
|
+
if _pm:
|
|
921
|
+
task = dict(task, context=((task.get("context") or "") + "\n\n" + _pm)[:8000])
|
|
904
922
|
plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
|
|
905
923
|
note=route.get("implementer", ""))
|
|
906
924
|
plan = planner.make_code_plan(_steered_task(run_id, task),
|
|
@@ -949,8 +967,13 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
949
967
|
register_default_namespaces()
|
|
950
968
|
if ss_get("cascade", "enabled"):
|
|
951
969
|
from . import capability
|
|
970
|
+
mh_data = modelhub._load()
|
|
952
971
|
agt_b = capability.cascade_reorder(
|
|
953
|
-
agt_b, capability.make_tier_lookup(modelhub.providers())
|
|
972
|
+
agt_b, capability.make_tier_lookup(modelhub.providers()),
|
|
973
|
+
providers=modelhub.providers(),
|
|
974
|
+
pricing=mh_data.get("pricing") or {},
|
|
975
|
+
difficulty=difficulty, task_type=task.get("type") or "code",
|
|
976
|
+
role="implement")
|
|
954
977
|
except Exception:
|
|
955
978
|
pass
|
|
956
979
|
for i, sub in enumerate(subtasks):
|
|
@@ -1021,7 +1044,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1021
1044
|
res_err = "实现步骤失败(无其他真实 CLI 可换将): %s" % res.get("error")
|
|
1022
1045
|
else:
|
|
1023
1046
|
res_err = "实现步骤失败: %s" % res.get("error")
|
|
1024
|
-
store.update_run(run_id,
|
|
1047
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1048
|
+
error=res_err, ended_at=_now())
|
|
1025
1049
|
return False
|
|
1026
1050
|
|
|
1027
1051
|
def review_and_score():
|
|
@@ -1117,7 +1141,21 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1117
1141
|
lines += ["", "## 评审总评", "", review_json.get("summary", ""), ""]
|
|
1118
1142
|
store.write_report(run_id, "\n".join(lines))
|
|
1119
1143
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1120
|
-
|
|
1144
|
+
# 项目记忆沉淀(借鉴 agentmemory 持久记忆):代码任务成功后提取架构事实
|
|
1145
|
+
# (改动文件/验收结果/修复轮数),追加到 .codebee/project-memory.md——
|
|
1146
|
+
# 同目录后续 code 任务规划前自动注入,让编排者「知道这个代码库的脾气」
|
|
1147
|
+
try:
|
|
1148
|
+
_diff = _git_diff(workdir)
|
|
1149
|
+
_files_touched = sorted(set(re.findall(
|
|
1150
|
+
r"(?:^|\n)diff --git a/(\S+) b/(\S+)", _diff or "")))
|
|
1151
|
+
_touched_str = "、".join(sorted(set(b for _, b in _files_touched)))[:500] if _files_touched else ""
|
|
1152
|
+
_mem_lines = ["改动文件:%s" % (_touched_str or "(无 diff)"),
|
|
1153
|
+
"验收:%s" % ("通过" if verify_pass else "未通过"),
|
|
1154
|
+
"修复轮数:%d" % (len(repairs) - 1)]
|
|
1155
|
+
_write_project_memory(task, workdir, _mem_lines)
|
|
1156
|
+
except Exception:
|
|
1157
|
+
pass
|
|
1158
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
1121
1159
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
1122
1160
|
"通过" if overall_pass else "未通过",
|
|
1123
1161
|
"通过" if verify_pass else "未通过",
|
|
@@ -1285,7 +1323,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1285
1323
|
else:
|
|
1286
1324
|
impl, route["implementer"] = router.pick(agents, "implement", task["type"], stats)
|
|
1287
1325
|
if impl is None and bi is None:
|
|
1288
|
-
store.update_run(run_id,
|
|
1326
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1327
|
+
error="没有可用智能体", ended_at=_now())
|
|
1289
1328
|
return
|
|
1290
1329
|
difficulty = task.get("difficulty") or "default"
|
|
1291
1330
|
step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
|
|
@@ -1359,7 +1398,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1359
1398
|
readonly=False, ev=ev, note=note,
|
|
1360
1399
|
resume=sid or None, images=images)
|
|
1361
1400
|
if not res["ok"]:
|
|
1362
|
-
store.update_run(run_id, status="failed",
|
|
1401
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1363
1402
|
error="执行失败: %s" % res.get("error"), ended_at=_now())
|
|
1364
1403
|
return
|
|
1365
1404
|
turns += 1
|
|
@@ -1391,7 +1430,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1391
1430
|
report += ["## 最近一轮输出", "", last_text[-5000:], ""]
|
|
1392
1431
|
store.write_report(run_id, "\n".join(report))
|
|
1393
1432
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1394
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
1433
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
1395
1434
|
summary="直连完成(%d 轮):%s" % (turns, last_text[:160]),
|
|
1396
1435
|
ended_at=_now())
|
|
1397
1436
|
|
|
@@ -1410,7 +1449,7 @@ def _pick_reviewer_legacy(agents, impl):
|
|
|
1410
1449
|
|
|
1411
1450
|
# ---------------------------------------------------------------- review 引擎(小说/文档/翻译/调研…通用)
|
|
1412
1451
|
|
|
1413
|
-
NOVEL_DRAFT_PROMPT = """
|
|
1452
|
+
NOVEL_DRAFT_PROMPT = """你是__ROLE__。请在当前工作目录中撰写/修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
1414
1453
|
|
|
1415
1454
|
## 写作任务
|
|
1416
1455
|
__GOAL__
|
|
@@ -1426,6 +1465,69 @@ __RUBRIC__
|
|
|
1426
1465
|
- 只修改 `__FILE__` 这一个文件;保持 Markdown 结构。
|
|
1427
1466
|
- 完成后用 3 句话说明本轮写了什么。"""
|
|
1428
1467
|
|
|
1468
|
+
# review 引擎共用执行骨架,但交付物不能只靠 rubric 猜格式。每个内置类型给出
|
|
1469
|
+
# 最小成品契约,起草和修订都注入;自定义流程继续使用通用回退,避免强加结构。
|
|
1470
|
+
CONTENT_DELIVERY_CONTRACTS = {
|
|
1471
|
+
"novel": ("小说作者", [
|
|
1472
|
+
"遵守用户给定的题材、篇幅、视角和风格;未给出的核心设定不要擅自扩张。",
|
|
1473
|
+
"用场景、行动和对话推进冲突,人物动机与前后因果保持一致。",
|
|
1474
|
+
]),
|
|
1475
|
+
"article": ("平台内容主编", [
|
|
1476
|
+
"标题、开头钩子、正文层级和结尾行动建议要适配目标平台与读者。",
|
|
1477
|
+
"事实、数据和引语不得编造;缺少来源时明确标注待核实。",
|
|
1478
|
+
]),
|
|
1479
|
+
"video_script": ("短视频编导", [
|
|
1480
|
+
"按镜头或时间段写清画面、口播、字幕/音效与预计时长,前 3 秒给出钩子。",
|
|
1481
|
+
"每个画面都应可实际拍摄或制作,结尾给出自然的互动或转化动作。",
|
|
1482
|
+
]),
|
|
1483
|
+
"doc": ("技术文档编辑", [
|
|
1484
|
+
"先明确读者、目的和前置条件,再按可执行步骤组织正文。",
|
|
1485
|
+
"命令、参数、示例与限制必须一致;无法确认的内容明确标注。",
|
|
1486
|
+
]),
|
|
1487
|
+
"translation": ("专业译者与审校", [
|
|
1488
|
+
"忠实保留原文含义、语气、数字、专名、占位符、链接和 Markdown 结构,不增译或漏译。",
|
|
1489
|
+
"术语译法全文一致;歧义或无法确认的专名保留原文并加简短译注。",
|
|
1490
|
+
]),
|
|
1491
|
+
"research": ("研究分析师", [
|
|
1492
|
+
"围绕决策问题组织证据、对比、结论与可执行建议,避免资料堆砌。",
|
|
1493
|
+
"结论必须能回溯到来源;证据不足处明确写出不确定性和验证办法。",
|
|
1494
|
+
]),
|
|
1495
|
+
"speech": ("演讲撰稿人", [
|
|
1496
|
+
"按场合、听众和时长控制篇幅,使用适合现场说出的短句与自然转场。",
|
|
1497
|
+
"开场建立关系,主体围绕一个核心信息展开,结尾给出清晰收束或号召。",
|
|
1498
|
+
]),
|
|
1499
|
+
"weekly_report": ("业务汇报顾问", [
|
|
1500
|
+
"按成果与影响、关键数据、问题阻塞、下步行动(负责人/时间)组织内容。",
|
|
1501
|
+
"只使用用户提供或可核验的数据;缺失数字保留待补项,不虚构业绩。",
|
|
1502
|
+
]),
|
|
1503
|
+
"email": ("商务沟通顾问", [
|
|
1504
|
+
"包含明确主题、称呼、来意、必要背景、请求/下一步和得体落款。",
|
|
1505
|
+
"语气匹配双方关系;日期、承诺、附件与联系人不得凭空补造。",
|
|
1506
|
+
]),
|
|
1507
|
+
"tech_proposal": ("解决方案架构师", [
|
|
1508
|
+
"覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
|
|
1509
|
+
"区分已知事实、假设和待验证项;成本收益给出计算口径而非虚构数字。",
|
|
1510
|
+
]),
|
|
1511
|
+
"resume": ("招聘与简历顾问", [
|
|
1512
|
+
"围绕目标岗位提炼真实经历,用行动、结果和技能关键词表达岗位匹配度。",
|
|
1513
|
+
"不得虚构经历、公司、学历、指标或技术栈;缺少量化数据时保留待补提示。",
|
|
1514
|
+
]),
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
def _content_role(task):
|
|
1519
|
+
"""返回内置类型的专业角色;自定义 review 流程使用中性角色。"""
|
|
1520
|
+
spec = CONTENT_DELIVERY_CONTRACTS.get(str(task.get("type") or ""))
|
|
1521
|
+
return spec[0] if spec else "内容交付专家"
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def _content_contract(task):
|
|
1525
|
+
"""把类型成品约束渲染为稳定提示块;无内置契约时不额外注入。"""
|
|
1526
|
+
spec = CONTENT_DELIVERY_CONTRACTS.get(str(task.get("type") or ""))
|
|
1527
|
+
if not spec:
|
|
1528
|
+
return ""
|
|
1529
|
+
return "\n\n## 本类型交付约束\n" + "\n".join("- " + item for item in spec[1])
|
|
1530
|
+
|
|
1429
1531
|
# 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
|
|
1430
1532
|
# 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采
|
|
1431
1533
|
RESEARCH_APPENDIX = """
|
|
@@ -1438,7 +1540,7 @@ RESEARCH_APPENDIX = """
|
|
|
1438
1540
|
- 结构硬性要求:报告第一段必须是「**核心结论**」三行以内的要点摘要(结论先行),
|
|
1439
1541
|
之后才展开分层论证 → 风险与局限(说明哪些结论证据不足)。"""
|
|
1440
1542
|
|
|
1441
|
-
NOVEL_REVISE_PROMPT = """
|
|
1543
|
+
NOVEL_REVISE_PROMPT = """你是__ROLE__。请根据下方汇总评审意见修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
1442
1544
|
|
|
1443
1545
|
## 原始写作任务
|
|
1444
1546
|
__GOAL__
|
|
@@ -1811,7 +1913,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1811
1913
|
# 历史遗留:降级/模板大纲被继承时,真实任务宁可中止重生成,也不按空模板写全书
|
|
1812
1914
|
if (outline.get("degraded") or outline.get("source") == "template") \
|
|
1813
1915
|
and impl.get("mode") != "mock":
|
|
1814
|
-
store.update_run(run_id, status="failed",
|
|
1916
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1815
1917
|
error="继承的大纲为降级模板(无真实情节),已中止以重新生成大纲",
|
|
1816
1918
|
ended_at=_now())
|
|
1817
1919
|
return
|
|
@@ -1834,7 +1936,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1834
1936
|
store.finish_step(run_id, outline_step["n"], "failed",
|
|
1835
1937
|
summary="大纲降级:%s" % (outline.get("degraded_reason") or "编排者不可用"),
|
|
1836
1938
|
duration_s=None)
|
|
1837
|
-
store.update_run(run_id, status="failed",
|
|
1939
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1838
1940
|
error="%s,已中止以免按空模板写全书"
|
|
1839
1941
|
% (outline.get("degraded_reason") or "编排者不可用"),
|
|
1840
1942
|
ended_at=_now())
|
|
@@ -2103,7 +2205,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2103
2205
|
time.sleep(3) # 落盘竞态宽限:CLI 崩溃退出前写的文件可能晚于
|
|
2104
2206
|
good, txt = _chapter_state() # 退出检查零点几秒才可见(c34 实测)
|
|
2105
2207
|
if not good:
|
|
2106
|
-
store.update_run(run_id, status="failed",
|
|
2208
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2107
2209
|
error="第 %d 章起草失败: %s" % (i, (res or {}).get("error")), ended_at=_now())
|
|
2108
2210
|
return
|
|
2109
2211
|
if not res["ok"]:
|
|
@@ -2187,7 +2289,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2187
2289
|
if scored_variants:
|
|
2188
2290
|
break
|
|
2189
2291
|
if not scored_variants:
|
|
2190
|
-
store.update_run(run_id, status="failed",
|
|
2292
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2191
2293
|
error="第 %d 章赛马全部变体起草失败" % i, ended_at=_now())
|
|
2192
2294
|
return
|
|
2193
2295
|
scored_variants.sort(key=lambda v: (-v["avg"], v["variant"]))
|
|
@@ -2198,7 +2300,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2198
2300
|
os.replace(os.path.join(workdir, win["file"]),
|
|
2199
2301
|
os.path.join(workdir, ch_file))
|
|
2200
2302
|
except OSError as e:
|
|
2201
|
-
store.update_run(run_id, status="failed",
|
|
2303
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2202
2304
|
error="第 %d 章赛马收卷失败: %r" % (i, e), ended_at=_now())
|
|
2203
2305
|
return
|
|
2204
2306
|
for v in scored_variants[1:]:
|
|
@@ -2246,7 +2348,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2246
2348
|
if not scored:
|
|
2247
2349
|
# 「评不上」≠「评了 0 分」:全部评审失败时中止本轮,
|
|
2248
2350
|
# 让自动续跑换个时机重试,而不是以 0 分误判章稿质量。
|
|
2249
|
-
store.update_run(run_id, status="failed",
|
|
2351
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2250
2352
|
error="第 %d 章评审全部失败(评审模型不可用或输出不可解析),"
|
|
2251
2353
|
"已中止以免以 0 分误判质量" % i, ended_at=_now())
|
|
2252
2354
|
return
|
|
@@ -2377,7 +2479,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2377
2479
|
if gscored:
|
|
2378
2480
|
break
|
|
2379
2481
|
if not gscored:
|
|
2380
|
-
store.update_run(run_id, status="failed",
|
|
2482
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2381
2483
|
error="全局一致性评审全部失败(评审模型不可用或输出不可解析),"
|
|
2382
2484
|
"已中止以免把「无法评审」误判为「未达标」。"
|
|
2383
2485
|
"各章稿件已全部落盘,修复评审链后续跑可直接收尾",
|
|
@@ -2546,7 +2648,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2546
2648
|
lines.append("(无 major 问题)")
|
|
2547
2649
|
store.write_report(run_id, "\n".join(lines))
|
|
2548
2650
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2549
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
2651
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
2550
2652
|
summary="连载任务%s(%s,约 %d 字,综合 %.1f)" % (
|
|
2551
2653
|
"达标" if publishable else "未达标", scope_txt,
|
|
2552
2654
|
total_words, overall),
|
|
@@ -2689,13 +2791,16 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2689
2791
|
impl, _ = _pick_implementer(agents, task.get("implementer"))
|
|
2690
2792
|
critics = _pick_critics_manual(agents, task)
|
|
2691
2793
|
else:
|
|
2692
|
-
|
|
2693
|
-
|
|
2794
|
+
task_type = task.get("type") or "novel"
|
|
2795
|
+
impl, route["author"] = router.pick(agents, "implement", task_type, stats)
|
|
2796
|
+
critics, route["critics"] = router.pick_critics(agents, task_type, stats, impl=impl)
|
|
2694
2797
|
if impl is None:
|
|
2695
|
-
store.update_run(run_id,
|
|
2798
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2799
|
+
error="没有可用智能体", ended_at=_now())
|
|
2696
2800
|
return
|
|
2697
2801
|
if resume_ctx is not None and mode == "auto":
|
|
2698
|
-
critics, route["critics"] = router.pick_critics(
|
|
2802
|
+
critics, route["critics"] = router.pick_critics(
|
|
2803
|
+
agents, task.get("type") or "novel", stats, impl=impl)
|
|
2699
2804
|
|
|
2700
2805
|
# ---- 规划(小说为模板计划)
|
|
2701
2806
|
_wait_gate(run_id, ev)
|
|
@@ -2739,6 +2844,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2739
2844
|
|
|
2740
2845
|
def _draft_prompt_for(vfile):
|
|
2741
2846
|
p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
|
|
2847
|
+
.replace("__ROLE__", _content_role(task))
|
|
2742
2848
|
.replace("__GOAL__", task["goal"])
|
|
2743
2849
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2744
2850
|
.replace("__RUBRIC__", "、".join(dims) if dims else "(按流程默认维度)"))
|
|
@@ -2748,6 +2854,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2748
2854
|
if is_research:
|
|
2749
2855
|
# 调研报告追加证据链要求(gpt-researcher 借鉴)
|
|
2750
2856
|
p += RESEARCH_APPENDIX
|
|
2857
|
+
p += _content_contract(task)
|
|
2751
2858
|
return p
|
|
2752
2859
|
|
|
2753
2860
|
best_of = max(1, min(3, int(task.get("best_of") or 1)))
|
|
@@ -2768,7 +2875,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2768
2875
|
resume=resume_ctx["session"] if resume_ctx else None,
|
|
2769
2876
|
images=_task_images(task, workdir))
|
|
2770
2877
|
if not draft_res["ok"]:
|
|
2771
|
-
store.update_run(run_id,
|
|
2878
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2879
|
+
error="起草失败: %s" % draft_res.get("error"),
|
|
2772
2880
|
ended_at=_now())
|
|
2773
2881
|
return
|
|
2774
2882
|
|
|
@@ -2847,8 +2955,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2847
2955
|
pass
|
|
2848
2956
|
else:
|
|
2849
2957
|
prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
|
|
2958
|
+
.replace("__ROLE__", _content_role(task))
|
|
2850
2959
|
.replace("__GOAL__", task["goal"])
|
|
2851
2960
|
.replace("__CRITIQUE__", "\n".join(crit_lines)))
|
|
2961
|
+
prompt += _content_contract(task)
|
|
2852
2962
|
_run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
|
|
2853
2963
|
workdir, readonly=False, ev=ev,
|
|
2854
2964
|
resume=resume_ctx["session"] if resume_ctx else None)
|
|
@@ -2898,7 +3008,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2898
3008
|
lines += ["", "## 稿件位置", "", "`%s`" % ms_path, ""]
|
|
2899
3009
|
store.write_report(run_id, "\n".join(lines))
|
|
2900
3010
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2901
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
3011
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
2902
3012
|
summary="评审任务%s(综合 %.1f)" % ("达标" if publishable else "未达标", overall),
|
|
2903
3013
|
ended_at=_now())
|
|
2904
3014
|
|
|
@@ -2943,13 +3053,13 @@ def _run_serial_qa(run, task, agents, ev):
|
|
|
2943
3053
|
res = _run_step(run_id, "qa", modelhub.bind_agent(agent, "default"),
|
|
2944
3054
|
prompt, workdir, readonly=True, ev=ev, timeout=1200)
|
|
2945
3055
|
if res.get("ok") and (res.get("text") or "").strip():
|
|
2946
|
-
store.update_run(run_id, status="done", ended_at=_now(),
|
|
3056
|
+
store.update_run(run_id, expected_status="running", status="done", ended_at=_now(),
|
|
2947
3057
|
verdict={"qa": True,
|
|
2948
3058
|
"answered_by": agent.get("id")})
|
|
2949
3059
|
return
|
|
2950
3060
|
errors.append("%s:%s" % (agent.get("id"),
|
|
2951
3061
|
(res.get("error") or "无输出")[:120]))
|
|
2952
|
-
store.update_run(run_id, status="failed", ended_at=_now(),
|
|
3062
|
+
store.update_run(run_id, expected_status="running", status="failed", ended_at=_now(),
|
|
2953
3063
|
error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
|
|
2954
3064
|
|
|
2955
3065
|
|
|
@@ -2970,6 +3080,43 @@ def _read_constitution(workdir):
|
|
|
2970
3080
|
"与其他要求冲突时以宪章为准)\n\n" + txt + "\n\n")
|
|
2971
3081
|
|
|
2972
3082
|
|
|
3083
|
+
def _write_project_memory(task, workdir, lines):
|
|
3084
|
+
"""项目记忆持久化(借鉴 agentmemory):代码任务成功后把架构事实追加到
|
|
3085
|
+
.codebee/project-memory.md——同目录后续 code 任务规划前自动注入,
|
|
3086
|
+
让编排者「知道这个代码库的脾气」而非每次从零摸索。失败静默。"""
|
|
3087
|
+
if not lines:
|
|
3088
|
+
return ""
|
|
3089
|
+
try:
|
|
3090
|
+
pm = os.path.join(workdir, ".codebee", "project-memory.md")
|
|
3091
|
+
os.makedirs(os.path.dirname(pm), exist_ok=True)
|
|
3092
|
+
header_needed = not os.path.isfile(pm)
|
|
3093
|
+
with open(pm, "a", encoding="utf-8") as f:
|
|
3094
|
+
if header_needed:
|
|
3095
|
+
f.write("# 项目记忆(每次代码任务完成后自动追加,供后续任务参考)\n\n")
|
|
3096
|
+
f.write("### %s · %s\n" % (task.get("title") or "", _now()))
|
|
3097
|
+
for ln in lines:
|
|
3098
|
+
f.write("- %s\n" % str(ln)[:300])
|
|
3099
|
+
f.write("\n")
|
|
3100
|
+
return pm
|
|
3101
|
+
except Exception:
|
|
3102
|
+
return ""
|
|
3103
|
+
|
|
3104
|
+
|
|
3105
|
+
def _read_project_memory(workdir, cap=4000):
|
|
3106
|
+
"""读取项目记忆供规划提示词注入。超出上限截断到最新条目。"""
|
|
3107
|
+
p = os.path.join(workdir or "", ".codebee", "project-memory.md")
|
|
3108
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
3109
|
+
return ""
|
|
3110
|
+
try:
|
|
3111
|
+
txt = _read_text_any_enc(p)[:cap].strip()
|
|
3112
|
+
except OSError:
|
|
3113
|
+
return ""
|
|
3114
|
+
if not txt:
|
|
3115
|
+
return ""
|
|
3116
|
+
return ("## 项目记忆(此前代码任务在此工作目录留下的架构事实,"
|
|
3117
|
+
"规划时优先参考)\n\n" + txt + "\n\n")
|
|
3118
|
+
|
|
3119
|
+
|
|
2973
3120
|
def _write_task_spec(task, workdir):
|
|
2974
3121
|
"""任务规格落盘 .codebee/spec.md(借鉴 agent-orchestrator 的 .spec/PROMPT.md 与
|
|
2975
3122
|
planning-with-files 的文件化计划):任务定义随工作目录留存、随任务分支版本化,
|
|
@@ -3120,10 +3267,18 @@ def execute_run(run_id):
|
|
|
3120
3267
|
error="排队期间被取消")
|
|
3121
3268
|
return
|
|
3122
3269
|
task = store.get_task(run.get("task_id"))
|
|
3123
|
-
|
|
3270
|
+
# 生产 enqueue 已完成 queued→running 认领;测试/兼容调用也可能直接从
|
|
3271
|
+
# queued 进入。按初读状态做 CAS 起跑确认:取消若恰好落在读取之后,当前
|
|
3272
|
+
# 状态已是 cancelled,写入会失败并立即退出,不产生 Git/文件副作用。
|
|
3273
|
+
initial_status = run.get("status")
|
|
3274
|
+
if initial_status not in ("queued", "running"):
|
|
3275
|
+
return
|
|
3276
|
+
if store.update_run(run_id, expected_status=initial_status, status="running",
|
|
3277
|
+
started_at=_now()) is None:
|
|
3278
|
+
return
|
|
3124
3279
|
if task is None:
|
|
3125
|
-
store.update_run(run_id,
|
|
3126
|
-
ended_at=_now())
|
|
3280
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3281
|
+
error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
|
|
3127
3282
|
return
|
|
3128
3283
|
# 代码版本检出:任务指定了基线版本时,先检出任务分支 tutti/<task-id> 再跑流水线。
|
|
3129
3284
|
# 显式意图不容静默降级——仓库缺失/脏工作区/引用不存在一律中止运行并报错,
|
|
@@ -3134,8 +3289,8 @@ def execute_run(run_id):
|
|
|
3134
3289
|
ok, err, gitinfo = gitmod.prepare_checkout(
|
|
3135
3290
|
task["workdir"], task["git_rev"], task["id"])
|
|
3136
3291
|
if not ok:
|
|
3137
|
-
store.update_run(run_id,
|
|
3138
|
-
ended_at=_now())
|
|
3292
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3293
|
+
error="代码版本检出失败:%s" % err, ended_at=_now())
|
|
3139
3294
|
return
|
|
3140
3295
|
git_ctx = gitinfo
|
|
3141
3296
|
store.update_run(run_id, git=gitinfo)
|
|
@@ -3159,6 +3314,11 @@ def execute_run(run_id):
|
|
|
3159
3314
|
if extra:
|
|
3160
3315
|
agents.append(extra)
|
|
3161
3316
|
stats = history.agent_stats()
|
|
3317
|
+
# 运行级任务画像:所有后续 bind_agent 调用共享同一预置类型,
|
|
3318
|
+
# 模型级联因此覆盖 direct/code/review/serial/translation 等全部引擎。
|
|
3319
|
+
for _agent in agents:
|
|
3320
|
+
if isinstance(_agent, dict):
|
|
3321
|
+
_agent["_dispatch_task_type"] = task.get("type") or "direct"
|
|
3162
3322
|
mode = task.get("mode") or ("manual" if task.get("implementer") else "auto")
|
|
3163
3323
|
store.update_run(run_id, mode=mode)
|
|
3164
3324
|
# engine 决定流水线:code=实现/验证/评审/修复;review=起草/多维评审/修订/门禁;
|
|
@@ -3191,7 +3351,8 @@ def execute_run(run_id):
|
|
|
3191
3351
|
impl, route["author"] = router.pick(agents, "implement", task["type"], stats)
|
|
3192
3352
|
critics, route["critics"] = router.pick_critics(agents, task["type"], stats, impl=impl)
|
|
3193
3353
|
if impl is None:
|
|
3194
|
-
store.update_run(run_id,
|
|
3354
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3355
|
+
error="没有可用智能体", ended_at=_now())
|
|
3195
3356
|
return
|
|
3196
3357
|
if resume_ctx is not None and mode == "auto":
|
|
3197
3358
|
critics, route["critics"] = router.pick_critics(agents, task["type"], stats, impl=impl)
|
|
@@ -3201,11 +3362,12 @@ def execute_run(run_id):
|
|
|
3201
3362
|
else:
|
|
3202
3363
|
_run_content_review(run, task, agents, ev, stats, mode)
|
|
3203
3364
|
except Cancelled:
|
|
3204
|
-
store.update_run(run_id,
|
|
3365
|
+
store.update_run(run_id, expected_status="running",
|
|
3366
|
+
status="cancelled", ended_at=_now())
|
|
3205
3367
|
except Exception as e:
|
|
3206
3368
|
import traceback
|
|
3207
|
-
store.update_run(run_id,
|
|
3208
|
-
ended_at=_now())
|
|
3369
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3370
|
+
error=repr(e)[:500], ended_at=_now())
|
|
3209
3371
|
try:
|
|
3210
3372
|
err_path = store.run_dir(run_id) / "error.log"
|
|
3211
3373
|
if _inside(str(store.run_dir(run_id).parent), str(err_path)):
|
package/app/core/router.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""智能路由:能力基线 × 历史胜率 × 角色约束 → 选智能体,并给出可解释的理由。"""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from . import history
|
|
5
|
+
from . import dispatch, history
|
|
6
6
|
|
|
7
7
|
# 各类智能体的能力基线(0-100)。真实 CLI 里官方双雄最高。
|
|
8
8
|
CAPABILITY = {
|
|
@@ -13,15 +13,19 @@ CAPABILITY = {
|
|
|
13
13
|
MAX_REPAIR_ROUNDS = 2 # 自动修复循环上限
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _binding_bonus(agent_id):
|
|
16
|
+
def _binding_bonus(agent_id, dispatch_mode=False):
|
|
17
17
|
"""绑定链可用性加分/减分:链上有可用条目 +8,解析为空 -25。2026-09-16 实测:
|
|
18
18
|
静态能力基线让配额烧干的 codex 永远压过健康备用 CLI,绑定空的 CLI 更是连
|
|
19
19
|
用户配置的模型都没用上——先按「能不能按配置跑起来」校准。2026-09-17 起
|
|
20
20
|
空链步骤在 pipeline 直接判失败(不再静默回落本机默认),此处只管排序。"""
|
|
21
21
|
try:
|
|
22
22
|
from . import modelhub
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
pref = modelhub._binding_for(agent_id)
|
|
24
|
+
configured = bool(modelhub._binding_chain(pref) or pref.get("provider_id"))
|
|
25
|
+
if not configured:
|
|
26
|
+
return 0.0 if dispatch_mode else -25.0
|
|
27
|
+
b = modelhub.resolve_binding(agent_id)
|
|
28
|
+
return 8.0 if (b and b.get("call_chain")) else -25.0
|
|
25
29
|
except Exception:
|
|
26
30
|
return 0.0
|
|
27
31
|
|
|
@@ -45,14 +49,20 @@ def score(agent, role, ttype, stats=None):
|
|
|
45
49
|
(封顶 -45,足以盖过历史加分),满额后仅在没有其他选择时才会被选中。"""
|
|
46
50
|
stats = stats or {}
|
|
47
51
|
base = CAPABILITY.get(agent.get("kind"), 60)
|
|
48
|
-
|
|
52
|
+
use_dispatch = bool(agent.get("_dispatch_task_type") or
|
|
53
|
+
agent.get("dispatch_enabled"))
|
|
54
|
+
bb = _binding_bonus(agent.get("id"), dispatch_mode=use_dispatch)
|
|
49
55
|
btxt = ""
|
|
50
56
|
if bb > 0:
|
|
51
57
|
btxt = ",绑定链可用(+%s)" % bb
|
|
52
58
|
elif bb < 0:
|
|
53
59
|
btxt = ",绑定链为空:相关步骤将判失败(%s)" % bb
|
|
54
60
|
hb = _history_bonus(stats, agent.get("id"), ttype)
|
|
55
|
-
|
|
61
|
+
# 保持公开 score() 的历史绝对分值;运行级候选由 pipeline 标记画像后
|
|
62
|
+
# 才启用能力亲和度,避免旧插件/测试调用被新权重悄然改变。
|
|
63
|
+
affinity, affinity_txt = (dispatch.agent_affinity(agent.get("kind"), ttype, role)
|
|
64
|
+
if use_dispatch else (0.0, "兼容模式"))
|
|
65
|
+
total = base + bb + hb + affinity
|
|
56
66
|
hs = (stats.get(agent.get("id")) or {}).get(ttype)
|
|
57
67
|
htxt = (",历史 %d/%d 胜(%s)" % (hs["wins"], hs["runs"], "%+.1f" % hb)) if hs else ",无历史记录"
|
|
58
68
|
quota_txt = ""
|
|
@@ -69,7 +79,8 @@ def score(agent, role, ttype, stats=None):
|
|
|
69
79
|
if penalty:
|
|
70
80
|
total += penalty
|
|
71
81
|
quota_txt = ",本小时 %d/%d tokens(%s)" % (used, quota, penalty)
|
|
72
|
-
return total, "能力基线 %d%s%s%s,总分 %s" % (
|
|
82
|
+
return total, "能力基线 %d,%s%s%s%s,总分 %s" % (
|
|
83
|
+
base, affinity_txt, btxt, htxt, quota_txt, round(total, 1))
|
|
73
84
|
|
|
74
85
|
|
|
75
86
|
def pick(agents, role, ttype, stats=None, exclude=()):
|
package/app/core/runner.py
CHANGED
|
@@ -76,7 +76,8 @@ def _npm_shim_bypass(argv):
|
|
|
76
76
|
and str(argv[2]).lower().endswith((".cmd", ".bat")):
|
|
77
77
|
shim = argv[2]
|
|
78
78
|
try:
|
|
79
|
-
|
|
79
|
+
with open(shim, encoding="utf-8", errors="replace") as fh:
|
|
80
|
+
text = fh.read()
|
|
80
81
|
except Exception:
|
|
81
82
|
return argv
|
|
82
83
|
m = re.search(r'%_prog%"\s+"?%dp0%(\\[^"\n]+?\.(?:mjs|js))"?', text)
|
package/app/core/selfupdate.py
CHANGED
|
@@ -176,16 +176,15 @@ def apply_upgrade():
|
|
|
176
176
|
try:
|
|
177
177
|
jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
|
|
178
178
|
except Exception:
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
log.exception("selfupdate: 升级任务入队失败 run=%s", run["id"])
|
|
179
|
+
# run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
|
|
180
|
+
log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
|
|
182
181
|
try:
|
|
183
182
|
store.update_run(run["id"], status="failed",
|
|
184
|
-
error="
|
|
183
|
+
error="升级任务启动失败,本次未排队,请稍后重试",
|
|
185
184
|
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
186
185
|
except Exception:
|
|
187
186
|
log.exception("selfupdate: 升级运行失败收口失败 run=%s", run["id"])
|
|
188
|
-
return {"error": "
|
|
187
|
+
return {"error": "升级任务启动失败,本次未排队,请稍后重试", "run_id": run["id"]}
|
|
189
188
|
return {"run_id": run["id"]}
|
|
190
189
|
|
|
191
190
|
|
|
@@ -210,23 +209,34 @@ def _log_note(log_path, text):
|
|
|
210
209
|
pass
|
|
211
210
|
|
|
212
211
|
|
|
213
|
-
def run_upgrade(run_id, log_path):
|
|
212
|
+
def run_upgrade(run_id, log_path, cancel_event=None):
|
|
214
213
|
"""worker 线程里执行升级命令(run/step 生命周期由 jobs 层管)。
|
|
215
214
|
|
|
216
215
|
包目录被其他进程占用(EBUSY/EPERM:打开包目录的资源管理器/终端窗口、
|
|
217
216
|
杀毒或索引扫描)是升级失败的最常见原因,且多为暂时性——自动重试
|
|
218
217
|
_RETRY_DELAYS 轮,仍败则给人话结论(原始 npm 输出在步骤日志里可查)。"""
|
|
219
|
-
res = {}
|
|
220
|
-
for attempt, delay in enumerate((0,) + _RETRY_DELAYS):
|
|
221
|
-
if
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
218
|
+
res = {}
|
|
219
|
+
for attempt, delay in enumerate((0,) + _RETRY_DELAYS):
|
|
220
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
221
|
+
return {"ok": False, "exit_code": None, "error": "用户主动取消",
|
|
222
|
+
"cancelled": True}
|
|
223
|
+
if delay:
|
|
224
|
+
_log_note(log_path, "目录被占用(EBUSY/EPERM),%d 秒后自动重试(第 %d/%d 次)"
|
|
225
|
+
% (delay, attempt, len(_RETRY_DELAYS)))
|
|
226
|
+
if cancel_event is not None and cancel_event.wait(delay):
|
|
227
|
+
return {"ok": False, "exit_code": None, "error": "用户主动取消",
|
|
228
|
+
"cancelled": True}
|
|
229
|
+
if cancel_event is None:
|
|
230
|
+
time.sleep(delay)
|
|
231
|
+
res = runner.run_process(
|
|
226
232
|
argv=_npm_argv("install", "-g", _PKG_NAME + "@latest"),
|
|
227
233
|
# Windows 上 npm 换版本靠把包目录整体改名(codebee → .codebee-xxx);
|
|
228
234
|
# cwd 若落在本包内,目录被自身进程占用,rename 必报 EBUSY——钉在包外
|
|
229
|
-
cwd=str(Path.home()), timeout=900, log_path=log_path
|
|
235
|
+
cwd=str(Path.home()), timeout=900, log_path=log_path,
|
|
236
|
+
cancel_event=cancel_event)
|
|
237
|
+
if res.get("cancelled"):
|
|
238
|
+
return {"ok": False, "exit_code": res.get("exit_code"),
|
|
239
|
+
"error": "用户主动取消", "cancelled": True}
|
|
230
240
|
if res["ok"] or not _locked_error(res):
|
|
231
241
|
break
|
|
232
242
|
if res["ok"]:
|