codebee 0.1.18 → 0.1.20
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 +22 -0
- package/README.md +14 -8
- package/app/core/automation.py +19 -0
- package/app/core/backup.py +470 -0
- package/app/core/bookmeta.py +4 -1
- package/app/core/builtin_agent.py +33 -1
- package/app/core/cleanup.py +303 -0
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +22 -4
- package/app/core/knowledge.py +26 -4
- package/app/core/manager.py +25 -1
- package/app/core/market.py +14 -2
- package/app/core/modelhub.py +16 -0
- package/app/core/pipeline.py +136 -13
- package/app/core/planner.py +22 -4
- package/app/core/publish/manager.py +80 -3
- package/app/core/runner.py +127 -7
- package/app/core/selfupdate.py +80 -38
- package/app/core/settings.py +32 -1
- package/app/core/skill_scan.py +86 -0
- package/app/core/store.py +62 -8
- package/app/core/wxdigest.py +710 -0
- package/app/core/zentao.py +516 -50
- package/app/main.py +263 -1
- package/app/pet.py +1323 -0
- package/app/pet_bee.png +0 -0
- package/app/pet_bee_robot.png +0 -0
- package/app/pick_dialog.py +34 -2
- package/app/ui/app.js +11083 -10389
- package/app/ui/i18n.js +170 -9
- package/app/ui/index.html +132 -2
- package/app/ui/style.css +156 -2
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -310,7 +310,8 @@ def _wait_gate(run_id, ev):
|
|
|
310
310
|
return
|
|
311
311
|
if ev is not None and ev.is_set():
|
|
312
312
|
return
|
|
313
|
-
|
|
313
|
+
# ev.wait 睡等:取消置位即刻醒来,不必耗满 1s 轮询间隔
|
|
314
|
+
(ev.wait(1.0) if ev is not None else time.sleep(1.0))
|
|
314
315
|
|
|
315
316
|
|
|
316
317
|
def _binding_dead_msg(agent):
|
|
@@ -501,6 +502,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
501
502
|
"error_code": ErrorCode.ENV_BLOCK, "sid": "",
|
|
502
503
|
"raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
|
|
503
504
|
"model": agent.get("model")}
|
|
505
|
+
usage_recorded = False
|
|
504
506
|
if _compaction_enabled() and not resume:
|
|
505
507
|
# Phase 2(1D):撑爆 → 压缩 → 守门重试;同时把 usage 累进 token_meter(1C)
|
|
506
508
|
session = _get_session(session_run_id)
|
|
@@ -510,6 +512,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
510
512
|
images=images, require_tools=require_tools)
|
|
511
513
|
|
|
512
514
|
def _call(p, **kw):
|
|
515
|
+
nonlocal usage_recorded
|
|
513
516
|
# 模型可见即已记录(§1A 不变量):入参/出参先落 session 日志
|
|
514
517
|
session.append("user_message", {"content": p, "role": role},
|
|
515
518
|
turn_id=str(step["n"]))
|
|
@@ -522,6 +525,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
522
525
|
from .token_meter import token_meter
|
|
523
526
|
token_meter.accumulate(session_run_id, r.get("usage"),
|
|
524
527
|
model=r.get("model") or "")
|
|
528
|
+
usage_recorded = True
|
|
525
529
|
except Exception:
|
|
526
530
|
pass
|
|
527
531
|
return r
|
|
@@ -533,6 +537,15 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
533
537
|
res = runner.run_agent(agent, prompt, workdir=workdir, readonly=readonly,
|
|
534
538
|
timeout=timeout, cancel_event=ev, log_path=str(log_abs),
|
|
535
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
|
|
536
549
|
return res
|
|
537
550
|
|
|
538
551
|
|
|
@@ -632,6 +645,7 @@ __GOAL__
|
|
|
632
645
|
|
|
633
646
|
## 本步指令
|
|
634
647
|
__SUBTASK__
|
|
648
|
+
__FILES__
|
|
635
649
|
|
|
636
650
|
## 背景与上下文
|
|
637
651
|
__CONTEXT__
|
|
@@ -693,6 +707,16 @@ def _verify_hint(task):
|
|
|
693
707
|
return ""
|
|
694
708
|
|
|
695
709
|
|
|
710
|
+
def _files_hint(sub):
|
|
711
|
+
"""计划锚定文件清单(OpenSpec explore 借鉴):计划里带了 files 就明示改动面,
|
|
712
|
+
让实现者知道该动哪些文件、不必全盘摸索。无 files 时返回空串(旧计划兼容)。"""
|
|
713
|
+
files = (sub or {}).get("files")
|
|
714
|
+
if not isinstance(files, list) or not files:
|
|
715
|
+
return ""
|
|
716
|
+
return "\n- 本步涉及文件(计划已锚定,先读再改):" + "、".join(
|
|
717
|
+
"`%s`" % str(f) for f in files[:10])
|
|
718
|
+
|
|
719
|
+
|
|
696
720
|
def _run_verify(run_id, task, workdir, ev):
|
|
697
721
|
"""确定性验证。返回 (verify_pass, ran)。"""
|
|
698
722
|
if not task.get("verify_command"):
|
|
@@ -803,6 +827,7 @@ def _code_bestof(run, task, impl, difficulty, ev):
|
|
|
803
827
|
prompt = (_RUN_CONSTITUTION + CODE_IMPL_PROMPT
|
|
804
828
|
.replace("__GOAL__", task["goal"])
|
|
805
829
|
.replace("__SUBTASK__", sub["detail"] if sub["detail"] else sub["title"])
|
|
830
|
+
.replace("__FILES__", _files_hint(sub))
|
|
806
831
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
807
832
|
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
808
833
|
res = _run_step(run_id, "race%d-impl" % k, agt_b, prompt, wt_path,
|
|
@@ -953,6 +978,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
953
978
|
.replace("__GOAL__", task["goal"])
|
|
954
979
|
.replace("__SUBTASK__",
|
|
955
980
|
sub["detail"] if sub["detail"] else sub["title"])
|
|
981
|
+
.replace("__FILES__", _files_hint(sub))
|
|
956
982
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
957
983
|
.replace("__VERIFY_HINT__", _verify_hint(task))) + prog
|
|
958
984
|
role = "implement" if len(subtasks) == 1 else "implement-%d/%d" % (i + 1, len(subtasks))
|
|
@@ -1396,7 +1422,7 @@ def _pick_reviewer_legacy(agents, impl):
|
|
|
1396
1422
|
|
|
1397
1423
|
# ---------------------------------------------------------------- review 引擎(小说/文档/翻译/调研…通用)
|
|
1398
1424
|
|
|
1399
|
-
NOVEL_DRAFT_PROMPT = """
|
|
1425
|
+
NOVEL_DRAFT_PROMPT = """你是__ROLE__。请在当前工作目录中撰写/修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
1400
1426
|
|
|
1401
1427
|
## 写作任务
|
|
1402
1428
|
__GOAL__
|
|
@@ -1412,6 +1438,69 @@ __RUBRIC__
|
|
|
1412
1438
|
- 只修改 `__FILE__` 这一个文件;保持 Markdown 结构。
|
|
1413
1439
|
- 完成后用 3 句话说明本轮写了什么。"""
|
|
1414
1440
|
|
|
1441
|
+
# review 引擎共用执行骨架,但交付物不能只靠 rubric 猜格式。每个内置类型给出
|
|
1442
|
+
# 最小成品契约,起草和修订都注入;自定义流程继续使用通用回退,避免强加结构。
|
|
1443
|
+
CONTENT_DELIVERY_CONTRACTS = {
|
|
1444
|
+
"novel": ("小说作者", [
|
|
1445
|
+
"遵守用户给定的题材、篇幅、视角和风格;未给出的核心设定不要擅自扩张。",
|
|
1446
|
+
"用场景、行动和对话推进冲突,人物动机与前后因果保持一致。",
|
|
1447
|
+
]),
|
|
1448
|
+
"article": ("平台内容主编", [
|
|
1449
|
+
"标题、开头钩子、正文层级和结尾行动建议要适配目标平台与读者。",
|
|
1450
|
+
"事实、数据和引语不得编造;缺少来源时明确标注待核实。",
|
|
1451
|
+
]),
|
|
1452
|
+
"video_script": ("短视频编导", [
|
|
1453
|
+
"按镜头或时间段写清画面、口播、字幕/音效与预计时长,前 3 秒给出钩子。",
|
|
1454
|
+
"每个画面都应可实际拍摄或制作,结尾给出自然的互动或转化动作。",
|
|
1455
|
+
]),
|
|
1456
|
+
"doc": ("技术文档编辑", [
|
|
1457
|
+
"先明确读者、目的和前置条件,再按可执行步骤组织正文。",
|
|
1458
|
+
"命令、参数、示例与限制必须一致;无法确认的内容明确标注。",
|
|
1459
|
+
]),
|
|
1460
|
+
"translation": ("专业译者与审校", [
|
|
1461
|
+
"忠实保留原文含义、语气、数字、专名、占位符、链接和 Markdown 结构,不增译或漏译。",
|
|
1462
|
+
"术语译法全文一致;歧义或无法确认的专名保留原文并加简短译注。",
|
|
1463
|
+
]),
|
|
1464
|
+
"research": ("研究分析师", [
|
|
1465
|
+
"围绕决策问题组织证据、对比、结论与可执行建议,避免资料堆砌。",
|
|
1466
|
+
"结论必须能回溯到来源;证据不足处明确写出不确定性和验证办法。",
|
|
1467
|
+
]),
|
|
1468
|
+
"speech": ("演讲撰稿人", [
|
|
1469
|
+
"按场合、听众和时长控制篇幅,使用适合现场说出的短句与自然转场。",
|
|
1470
|
+
"开场建立关系,主体围绕一个核心信息展开,结尾给出清晰收束或号召。",
|
|
1471
|
+
]),
|
|
1472
|
+
"weekly_report": ("业务汇报顾问", [
|
|
1473
|
+
"按成果与影响、关键数据、问题阻塞、下步行动(负责人/时间)组织内容。",
|
|
1474
|
+
"只使用用户提供或可核验的数据;缺失数字保留待补项,不虚构业绩。",
|
|
1475
|
+
]),
|
|
1476
|
+
"email": ("商务沟通顾问", [
|
|
1477
|
+
"包含明确主题、称呼、来意、必要背景、请求/下一步和得体落款。",
|
|
1478
|
+
"语气匹配双方关系;日期、承诺、附件与联系人不得凭空补造。",
|
|
1479
|
+
]),
|
|
1480
|
+
"tech_proposal": ("解决方案架构师", [
|
|
1481
|
+
"覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
|
|
1482
|
+
"区分已知事实、假设和待验证项;成本收益给出计算口径而非虚构数字。",
|
|
1483
|
+
]),
|
|
1484
|
+
"resume": ("招聘与简历顾问", [
|
|
1485
|
+
"围绕目标岗位提炼真实经历,用行动、结果和技能关键词表达岗位匹配度。",
|
|
1486
|
+
"不得虚构经历、公司、学历、指标或技术栈;缺少量化数据时保留待补提示。",
|
|
1487
|
+
]),
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _content_role(task):
|
|
1492
|
+
"""返回内置类型的专业角色;自定义 review 流程使用中性角色。"""
|
|
1493
|
+
spec = CONTENT_DELIVERY_CONTRACTS.get(str(task.get("type") or ""))
|
|
1494
|
+
return spec[0] if spec else "内容交付专家"
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _content_contract(task):
|
|
1498
|
+
"""把类型成品约束渲染为稳定提示块;无内置契约时不额外注入。"""
|
|
1499
|
+
spec = CONTENT_DELIVERY_CONTRACTS.get(str(task.get("type") or ""))
|
|
1500
|
+
if not spec:
|
|
1501
|
+
return ""
|
|
1502
|
+
return "\n\n## 本类型交付约束\n" + "\n".join("- " + item for item in spec[1])
|
|
1503
|
+
|
|
1415
1504
|
# 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
|
|
1416
1505
|
# 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采
|
|
1417
1506
|
RESEARCH_APPENDIX = """
|
|
@@ -1421,9 +1510,10 @@ RESEARCH_APPENDIX = """
|
|
|
1421
1510
|
单源信息要标注「仅单一来源」。
|
|
1422
1511
|
- 引用来源在文中用行内链接或脚注标明(域名即可,不编造 URL)。
|
|
1423
1512
|
- 区分「事实」与「观点」:数据/时间/版本号给来源,预测/评价标明是分析。
|
|
1424
|
-
-
|
|
1513
|
+
- 结构硬性要求:报告第一段必须是「**核心结论**」三行以内的要点摘要(结论先行),
|
|
1514
|
+
之后才展开分层论证 → 风险与局限(说明哪些结论证据不足)。"""
|
|
1425
1515
|
|
|
1426
|
-
NOVEL_REVISE_PROMPT = """
|
|
1516
|
+
NOVEL_REVISE_PROMPT = """你是__ROLE__。请根据下方汇总评审意见修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
1427
1517
|
|
|
1428
1518
|
## 原始写作任务
|
|
1429
1519
|
__GOAL__
|
|
@@ -1647,6 +1737,10 @@ def _critique_json(res, dims):
|
|
|
1647
1737
|
if isinstance(gj, dict) and isinstance(gj.get("scores"), dict) and gj.get("scores"):
|
|
1648
1738
|
return gj
|
|
1649
1739
|
prose = runner.scores_from_prose(text, dims)
|
|
1740
|
+
if not prose:
|
|
1741
|
+
# 第四道网(BAML 借鉴):维度名没命中时按「X:N 分」模式泛化抓取——
|
|
1742
|
+
# 自定义 rubric 改了维度措辞而模型用了自己的说法时仍能救回
|
|
1743
|
+
prose = runner.extract_scores_from_text(text)
|
|
1650
1744
|
if prose:
|
|
1651
1745
|
return {"scores": prose, "issues": [], "summary": text[:400]}
|
|
1652
1746
|
return {"scores": {}, "issues": [],
|
|
@@ -2016,9 +2110,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2016
2110
|
use_prompt = prompt
|
|
2017
2111
|
for draft_attempt in range(3):
|
|
2018
2112
|
if draft_attempt:
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2113
|
+
# 30s / 60s 退避;ev.wait 睡等可被取消即刻唤醒
|
|
2114
|
+
if ev is not None:
|
|
2115
|
+
if ev.wait(30 * draft_attempt):
|
|
2116
|
+
break
|
|
2117
|
+
else:
|
|
2118
|
+
time.sleep(30 * draft_attempt)
|
|
2022
2119
|
if draft_attempt and len(prompt) > 12000 and sk_block and sk_block in prompt:
|
|
2023
2120
|
# 长提示词在容量受限通道(讯飞托管 35B 等)上会挂起/秒拒
|
|
2024
2121
|
# ——分层降级:经验库→4K、模块库按模块边界、圣经按二级标题
|
|
@@ -2103,9 +2200,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2103
2200
|
for race_round in range(2):
|
|
2104
2201
|
# 全变体失败(网关突发限流)→ 60s 退避重赛一轮,别一章判死
|
|
2105
2202
|
if race_round:
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2203
|
+
# ev.wait 睡等可被取消即刻唤醒
|
|
2204
|
+
if ev is not None:
|
|
2205
|
+
if ev.wait(60):
|
|
2206
|
+
break
|
|
2207
|
+
else:
|
|
2208
|
+
time.sleep(60)
|
|
2109
2209
|
for kk in range(n_variants):
|
|
2110
2210
|
# 清上一轮残稿:防陈旧半成品被本轮评分误认成新成品
|
|
2111
2211
|
try:
|
|
@@ -2664,13 +2764,15 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2664
2764
|
impl, _ = _pick_implementer(agents, task.get("implementer"))
|
|
2665
2765
|
critics = _pick_critics_manual(agents, task)
|
|
2666
2766
|
else:
|
|
2667
|
-
|
|
2668
|
-
|
|
2767
|
+
task_type = task.get("type") or "novel"
|
|
2768
|
+
impl, route["author"] = router.pick(agents, "implement", task_type, stats)
|
|
2769
|
+
critics, route["critics"] = router.pick_critics(agents, task_type, stats, impl=impl)
|
|
2669
2770
|
if impl is None:
|
|
2670
2771
|
store.update_run(run_id, status="failed", error="没有可用智能体", ended_at=_now())
|
|
2671
2772
|
return
|
|
2672
2773
|
if resume_ctx is not None and mode == "auto":
|
|
2673
|
-
critics, route["critics"] = router.pick_critics(
|
|
2774
|
+
critics, route["critics"] = router.pick_critics(
|
|
2775
|
+
agents, task.get("type") or "novel", stats, impl=impl)
|
|
2674
2776
|
|
|
2675
2777
|
# ---- 规划(小说为模板计划)
|
|
2676
2778
|
_wait_gate(run_id, ev)
|
|
@@ -2714,6 +2816,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2714
2816
|
|
|
2715
2817
|
def _draft_prompt_for(vfile):
|
|
2716
2818
|
p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
|
|
2819
|
+
.replace("__ROLE__", _content_role(task))
|
|
2717
2820
|
.replace("__GOAL__", task["goal"])
|
|
2718
2821
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2719
2822
|
.replace("__RUBRIC__", "、".join(dims) if dims else "(按流程默认维度)"))
|
|
@@ -2723,6 +2826,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2723
2826
|
if is_research:
|
|
2724
2827
|
# 调研报告追加证据链要求(gpt-researcher 借鉴)
|
|
2725
2828
|
p += RESEARCH_APPENDIX
|
|
2829
|
+
p += _content_contract(task)
|
|
2726
2830
|
return p
|
|
2727
2831
|
|
|
2728
2832
|
best_of = max(1, min(3, int(task.get("best_of") or 1)))
|
|
@@ -2822,8 +2926,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2822
2926
|
pass
|
|
2823
2927
|
else:
|
|
2824
2928
|
prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
|
|
2929
|
+
.replace("__ROLE__", _content_role(task))
|
|
2825
2930
|
.replace("__GOAL__", task["goal"])
|
|
2826
2931
|
.replace("__CRITIQUE__", "\n".join(crit_lines)))
|
|
2932
|
+
prompt += _content_contract(task)
|
|
2827
2933
|
_run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
|
|
2828
2934
|
workdir, readonly=False, ev=ev,
|
|
2829
2935
|
resume=resume_ctx["session"] if resume_ctx else None)
|
|
@@ -3008,11 +3114,28 @@ def _write_task_evidence(run_id, task, workdir, summary_lines):
|
|
|
3008
3114
|
for ln in summary_lines:
|
|
3009
3115
|
f.write("- %s\n" % str(ln)[:300])
|
|
3010
3116
|
f.write("\n")
|
|
3117
|
+
_archive_stamp(run_id, task, workdir, summary_lines)
|
|
3011
3118
|
return path
|
|
3012
3119
|
except Exception:
|
|
3013
3120
|
return ""
|
|
3014
3121
|
|
|
3015
3122
|
|
|
3123
|
+
def _archive_stamp(run_id, task, workdir, summary_lines):
|
|
3124
|
+
"""归档戳(借鉴 OpenSpec archive):evidence 落盘后向 spec.md 尾部追加
|
|
3125
|
+
「已完成」快照行——spec 从「意图」升格为「意图+交付记录」的活档案。
|
|
3126
|
+
失败静默。"""
|
|
3127
|
+
try:
|
|
3128
|
+
spec = os.path.join(workdir, ".codebee", "spec.md")
|
|
3129
|
+
if not os.path.isfile(spec):
|
|
3130
|
+
return
|
|
3131
|
+
with open(spec, "a", encoding="utf-8") as f:
|
|
3132
|
+
f.write("\n---\n**✅ 已交付** · %s · run %s\n%s\n" % (
|
|
3133
|
+
_now(), run_id,
|
|
3134
|
+
"\n".join("- %s" % str(ln)[:160] for ln in summary_lines[:5])))
|
|
3135
|
+
except Exception:
|
|
3136
|
+
return
|
|
3137
|
+
|
|
3138
|
+
|
|
3016
3139
|
def _evidence_lines_from_run(run_id, task):
|
|
3017
3140
|
"""从 run 步骤与 verdict 提取证据行(确定性事实,不抄模型输出)。"""
|
|
3018
3141
|
run = store.get_run(run_id) or {}
|
package/app/core/planner.py
CHANGED
|
@@ -12,7 +12,7 @@ import os
|
|
|
12
12
|
import re
|
|
13
13
|
import time
|
|
14
14
|
|
|
15
|
-
from . import modelhub, runner, skills, usage
|
|
15
|
+
from . import knowledge, modelhub, runner, skills, usage
|
|
16
16
|
|
|
17
17
|
MAX_SUBTASKS = 4
|
|
18
18
|
DEFAULT_OUTLINE_TIMEOUT = 900 # 8 章大纲 + 经验包注入是重生成任务,300s 实测不够
|
|
@@ -116,10 +116,16 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
|
|
|
116
116
|
|
|
117
117
|
CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
|
|
118
118
|
并判定任务难度。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
119
|
-
{"difficulty": "easy 或 hard", "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令"}]}
|
|
119
|
+
{"difficulty": "easy 或 hard", "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
|
|
120
120
|
难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
|
|
121
121
|
子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
|
|
122
122
|
|
|
123
|
+
## 先探索再计划(重要,借鉴 OpenSpec explore)
|
|
124
|
+
拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
|
|
125
|
+
detail 必须点名真实存在的文件路径;files 列出该子任务会改动的文件(新建的写目标路径)。
|
|
126
|
+
**没探索过就不要凭想象编路径**——计划的可执行性取决于它对真实代码库的贴合度。
|
|
127
|
+
若工作目录为空(全新项目),files 写计划新建的文件路径。
|
|
128
|
+
|
|
123
129
|
## 开发目标
|
|
124
130
|
__GOAL__
|
|
125
131
|
|
|
@@ -274,6 +280,9 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
274
280
|
wpc = int(serial.get("words_per_chapter") or 2500)
|
|
275
281
|
start = int(serial.get("start_chapter") or 1)
|
|
276
282
|
sk_block, _ = skills.block_for(task)
|
|
283
|
+
kb_block = knowledge.block_for(task)
|
|
284
|
+
if kb_block:
|
|
285
|
+
sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
|
|
277
286
|
prev_title = ""
|
|
278
287
|
if start > 1:
|
|
279
288
|
prev_lines, done, prev_title, prev_tail = _prev_serial_story(task)
|
|
@@ -367,7 +376,10 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
367
376
|
|
|
368
377
|
|
|
369
378
|
def _norm_subtasks(data):
|
|
370
|
-
"""规范化 LLM 计划输出;不合规返回 None。
|
|
379
|
+
"""规范化 LLM 计划输出;不合规返回 None。
|
|
380
|
+
|
|
381
|
+
files 字段(OpenSpec explore 借鉴):计划锚定的真实文件清单,透传给执行步
|
|
382
|
+
让实现者知道改动面(缺失/非法时留空,向后兼容旧计划)。"""
|
|
371
383
|
if not isinstance(data, dict):
|
|
372
384
|
return None
|
|
373
385
|
subs = data.get("subtasks")
|
|
@@ -381,7 +393,13 @@ def _norm_subtasks(data):
|
|
|
381
393
|
detail = str(s.get("detail") or "").strip()
|
|
382
394
|
if not title:
|
|
383
395
|
continue
|
|
384
|
-
|
|
396
|
+
step = {"title": title[:60], "detail": detail[:1500]}
|
|
397
|
+
files = s.get("files")
|
|
398
|
+
if isinstance(files, list):
|
|
399
|
+
clean = [str(f).strip()[:200] for f in files if str(f).strip()][:10]
|
|
400
|
+
if clean:
|
|
401
|
+
step["files"] = clean
|
|
402
|
+
steps.append(step)
|
|
385
403
|
return steps or None
|
|
386
404
|
|
|
387
405
|
|
|
@@ -86,16 +86,56 @@ def view():
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def recover_orphans():
|
|
89
|
-
"""启动收尸:waiting_login / busy 的线程随进程重启死掉,统一改判 error。
|
|
89
|
+
"""启动收尸:waiting_login / busy 的线程随进程重启死掉,统一改判 error。
|
|
90
|
+
|
|
91
|
+
升级自愈:旧版「等扫码窗口一关就冤判超时」留下的 error(error 文案带
|
|
92
|
+
「等待登录超时」)排队后台复核——profile 登录态还在的直接翻 connected,
|
|
93
|
+
别让升级完还挂着旧冤案;attach 不到活实例就维持原样(用户点重连时
|
|
94
|
+
新终审逻辑自会兜住)。"""
|
|
90
95
|
_load()
|
|
91
96
|
n = 0
|
|
97
|
+
stale = []
|
|
92
98
|
for plat in PLATFORMS:
|
|
93
|
-
|
|
99
|
+
s = _st(plat)
|
|
100
|
+
if s.get("status") in ("waiting_login", "busy"):
|
|
94
101
|
_set(plat, status="error", error="上次操作随服务重启中断,请重试")
|
|
95
102
|
n += 1
|
|
103
|
+
elif s.get("status") == "error" and "等待登录超时" in (s.get("error") or ""):
|
|
104
|
+
stale.append(plat)
|
|
105
|
+
if stale:
|
|
106
|
+
threading.Thread(target=_recheck_stale_errors, args=(stale,),
|
|
107
|
+
daemon=True, name="pub-stale-recheck").start()
|
|
96
108
|
return n
|
|
97
109
|
|
|
98
110
|
|
|
111
|
+
def _recheck_stale_errors(plats):
|
|
112
|
+
"""旧版假超时的后台复核:只 attach 还活着的浏览器实例(绝不 launch——
|
|
113
|
+
升级重启时静默弹窗口吓人),登录态复核过了翻 connected;否则不动。"""
|
|
114
|
+
for plat in plats:
|
|
115
|
+
if _st(plat).get("status") != "error":
|
|
116
|
+
continue # 用户已先行操作(重连/断开),别覆盖
|
|
117
|
+
port = _st(plat).get("port") or 0
|
|
118
|
+
if not port:
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
b = Browser.attach(int(port))
|
|
122
|
+
except Exception:
|
|
123
|
+
continue # 实例已死:不 launch,维持原错误
|
|
124
|
+
with LOCK:
|
|
125
|
+
_browsers[plat] = b
|
|
126
|
+
page = b.first_page(create=False)
|
|
127
|
+
if page is None:
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
ok, _u = _check_login(plat, page)
|
|
131
|
+
except Exception:
|
|
132
|
+
continue
|
|
133
|
+
if ok and _st(plat).get("status") == "error":
|
|
134
|
+
_set(plat, status="connected", error="",
|
|
135
|
+
last_login=time.strftime("%m-%d %H:%M"))
|
|
136
|
+
ledger.record(plat, "connect", ok=True)
|
|
137
|
+
|
|
138
|
+
|
|
99
139
|
# ---------------------------------------------------------------- 浏览器会话
|
|
100
140
|
def _profiles_base():
|
|
101
141
|
"""profile 存放根:用户主目录 ~/.codebee/publish_profiles(仓库外)。
|
|
@@ -245,13 +285,31 @@ def connect(plat):
|
|
|
245
285
|
except (BrowserError, Exception):
|
|
246
286
|
pass # 页面被用户关掉等:继续等到超时
|
|
247
287
|
time.sleep(5)
|
|
248
|
-
|
|
288
|
+
_finalize_login_wait(plat)
|
|
249
289
|
|
|
250
290
|
threading.Thread(target=wait_login, daemon=True,
|
|
251
291
|
name="pub-login-%s" % plat).start()
|
|
252
292
|
return True, ""
|
|
253
293
|
|
|
254
294
|
|
|
295
|
+
def _finalize_login_wait(plat):
|
|
296
|
+
"""等扫码超时后的终审:用户可能已在本窗口登录后把窗口关了(profile
|
|
297
|
+
里登录态还在),attach-or-launch 重拉页面再复核一次,别急着冤判
|
|
298
|
+
超时——「明明登录着却报超时」是发布卡死感的最大来源。复核也过不了
|
|
299
|
+
才落超时错误。返回 True 表示复核通过(已判 connected)。"""
|
|
300
|
+
try:
|
|
301
|
+
_b, page = _open_page(plat)
|
|
302
|
+
ok, _u = _check_login(plat, page)
|
|
303
|
+
except Exception:
|
|
304
|
+
ok = False
|
|
305
|
+
if ok:
|
|
306
|
+
_set(plat, status="connected", last_login=time.strftime("%m-%d %H:%M"))
|
|
307
|
+
ledger.record(plat, "connect", ok=True)
|
|
308
|
+
else:
|
|
309
|
+
_set(plat, status="error", error="等待登录超时(15 分钟),请重新点连接")
|
|
310
|
+
return ok
|
|
311
|
+
|
|
312
|
+
|
|
255
313
|
def disconnect(plat):
|
|
256
314
|
b = _browsers.pop(plat, None)
|
|
257
315
|
if b:
|
|
@@ -357,6 +415,25 @@ def create_book_async(task_id, plat, auto_submit=False):
|
|
|
357
415
|
return True, ""
|
|
358
416
|
|
|
359
417
|
|
|
418
|
+
# ---------------------------------------------------------------- 动作:登记已有作品
|
|
419
|
+
def register_book(task_id, plat, title, book_id=""):
|
|
420
|
+
"""人工登记平台已有作品:建书流程没走通、或用户纯手工在平台上建的
|
|
421
|
+
书,补进台账让卡片翻到「已建书·发一章」,避免再点「创建作品」造出
|
|
422
|
+
重复书。只动本地台账不碰浏览器;发章按书名找书,title 必填,
|
|
423
|
+
book_id 选填(有直达 URL 时填)。已登记会覆盖更新(兼纠错口)。"""
|
|
424
|
+
from .. import store
|
|
425
|
+
if plat not in PLATFORMS:
|
|
426
|
+
return False, "未知平台"
|
|
427
|
+
if not store.get_task(task_id):
|
|
428
|
+
return False, "任务不存在"
|
|
429
|
+
title = (title or "").strip()
|
|
430
|
+
if not title:
|
|
431
|
+
return False, "作品名必填(发章按作品名在平台找书)"
|
|
432
|
+
ledger.save_book(task_id, plat, {"book_id": str(book_id or "").strip(),
|
|
433
|
+
"title": title[:120]})
|
|
434
|
+
return True, ""
|
|
435
|
+
|
|
436
|
+
|
|
360
437
|
def _with_tag_steps(steps, groups, values, mod=None):
|
|
361
438
|
"""标签走数据驱动:清单进 values["_tags"]([组名, 标签] 对),由 flow 的
|
|
362
439
|
"tags" 步骤按组切换点选。组显示名映射来自**对应平台模块**的
|