codebee 0.1.74 → 0.1.77
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 +628 -607
- package/LICENSE +21 -21
- package/README.md +507 -508
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +1036 -921
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -190
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -387
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +22 -0
- package/app/core/manager.py +2232 -2232
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +189 -189
- package/app/core/openapi.py +154 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +189 -18
- package/app/core/planner.py +88 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/fanqie.py +165 -165
- package/app/core/publish/flow.py +640 -640
- package/app/core/publish/flows-fanqie-calibrated.json +336 -336
- package/app/core/publish/manager.py +1118 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2238 -2225
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skills.py +1 -1
- package/app/core/step_runner.py +132 -132
- package/app/core/story_tracking.py +275 -241
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -1035
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +59 -0
- package/app/mcp_server.py +244 -244
- package/app/output/_createbook_body.json +1 -0
- package/app/output/_probe_fq_click.py +50 -0
- package/app/output/_probe_fq_form.py +42 -0
- package/app/output/_probe_fq_modal.py +55 -0
- package/app/output/_probe_fq_points.py +71 -0
- package/app/output/_probe_fq_submit.py +34 -0
- package/app/output/_probe_fq_watch.py +62 -0
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +108 -12
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +15 -0
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +4 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +7086 -7070
- package/bin/tutti.js +147 -147
- package/package.json +48 -48
- package/tools/wx_reader.py +261 -261
package/app/core/pipeline.py
CHANGED
|
@@ -2521,6 +2521,20 @@ def _ensure_critique_placeholders(tpl):
|
|
|
2521
2521
|
|
|
2522
2522
|
# ---------------------------------------------------------------- 连载引擎(长篇小说:逐章打磨)
|
|
2523
2523
|
|
|
2524
|
+
def serial_locked_chapter_block(workdir, chapter):
|
|
2525
|
+
"""本章锁定章纲注入块(起草/修订共用):工作目录章纲/*ch-<N>.md 存在时
|
|
2526
|
+
以最高优先级随提示词下发——批次大纲可能被继承链带偏,锁定章纲才是作者
|
|
2527
|
+
逐章确认过的事实标准(2026-10-03 不周山 rogue 大纲实案的治本闸)。
|
|
2528
|
+
无章纲返回 ""(老书零噪音)。"""
|
|
2529
|
+
text = planner.locked_chapter_outline_text(workdir, chapter)
|
|
2530
|
+
if not text:
|
|
2531
|
+
return ""
|
|
2532
|
+
return ("## 锁定章纲(作者已确认,最高优先级)\n"
|
|
2533
|
+
"本章标题、视角(POV)、时间地点、事件节拍与章末落点必须逐项对齐下方章纲;"
|
|
2534
|
+
"若与其他要点冲突,以锁定章纲为准;其后章目的伏笔揭示位不得提前烧毁。\n\n"
|
|
2535
|
+
+ text)
|
|
2536
|
+
|
|
2537
|
+
|
|
2524
2538
|
SERIAL_CHAPTER_PROMPT = """你是一名网文作者(写作规范见下方经验库)。本书信息如下,请先完整读完再执行末尾的「本章任务」。
|
|
2525
2539
|
|
|
2526
2540
|
__SKILLS__
|
|
@@ -2539,6 +2553,7 @@ __VOLUME__
|
|
|
2539
2553
|
- 剧情要点:__BEATS__
|
|
2540
2554
|
- 章末钩子:__HOOK__
|
|
2541
2555
|
- 本章爽点/情绪爆点:__HIGHLIGHT__(必须按特写镜头写厚:铺垫在前、放大在中、余波在后,用动作/五感/生理反应/环境反馈呈现,禁止一句话带过)
|
|
2556
|
+
__LOCKED_CH__
|
|
2542
2557
|
- 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
|
|
2543
2558
|
|
|
2544
2559
|
## 前情提要(此前各章结尾摘录,衔接用)
|
|
@@ -2565,6 +2580,7 @@ __VOLUME__
|
|
|
2565
2580
|
## 本章按大纲应完成
|
|
2566
2581
|
__BEATS__
|
|
2567
2582
|
(本章爽点/情绪爆点:__HIGHLIGHT__)
|
|
2583
|
+
__LOCKED_CH__
|
|
2568
2584
|
|
|
2569
2585
|
## 本章评审意见
|
|
2570
2586
|
__CRITIQUE__
|
|
@@ -2832,6 +2848,97 @@ def _weakest_chapters(chapter_scores, global_means, threshold, limit=2):
|
|
|
2832
2848
|
return cand[:limit]
|
|
2833
2849
|
|
|
2834
2850
|
|
|
2851
|
+
_ARCHIVE_DIR = "已成稿"
|
|
2852
|
+
_CH_MENTION_RE = re.compile(r"第\s*(\d{1,4})\s*(?:[—–\-~至到]\s*(\d{1,4})\s*)?章")
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def _archived_preface(workdir, start):
|
|
2856
|
+
"""前文回退:续写批次的前作章文件被归档成单文件(发布后 chapter-NN.md
|
|
2857
|
+
不在场)时,从「已成稿/」目录找回前文全文,供全书评审与合并成书使用。
|
|
2858
|
+
任一前文章节文件仍在场则视为未归档,返回空串(正常路径零影响)。"""
|
|
2859
|
+
if start <= 1:
|
|
2860
|
+
return ""
|
|
2861
|
+
for i in range(1, start):
|
|
2862
|
+
if _read_chapter(workdir, i):
|
|
2863
|
+
return ""
|
|
2864
|
+
adir = os.path.join(str(workdir), _ARCHIVE_DIR)
|
|
2865
|
+
try:
|
|
2866
|
+
names = sorted(os.listdir(adir))
|
|
2867
|
+
except OSError:
|
|
2868
|
+
return ""
|
|
2869
|
+
parts = []
|
|
2870
|
+
for name in names:
|
|
2871
|
+
if not name.lower().endswith((".md", ".txt")):
|
|
2872
|
+
continue
|
|
2873
|
+
if "目录" in name: # 阅读目录/章节目录是索引不是正文
|
|
2874
|
+
continue
|
|
2875
|
+
p = os.path.join(adir, name)
|
|
2876
|
+
if not os.path.isfile(p):
|
|
2877
|
+
continue
|
|
2878
|
+
try:
|
|
2879
|
+
t = _read_text_any_enc(p)
|
|
2880
|
+
except Exception:
|
|
2881
|
+
continue
|
|
2882
|
+
if t and t.strip():
|
|
2883
|
+
parts.append(t.strip())
|
|
2884
|
+
return "\n\n".join(parts)
|
|
2885
|
+
|
|
2886
|
+
|
|
2887
|
+
def _full_manuscript(workdir, start, end, cap=60000):
|
|
2888
|
+
"""全书评审文本:本批 start..end 全文保底在场,前文(章文件缺失时回退
|
|
2889
|
+
归档)用剩余预算从尾部截取——越靠近本批的前文对一致性越要紧。直拼再
|
|
2890
|
+
[:cap] 会把预算整段喂给前文、把本批(真正的评审对象)截掉。"""
|
|
2891
|
+
new_text = "\n\n".join(t for t in (_read_chapter(workdir, i)
|
|
2892
|
+
for i in range(start, end + 1)) if t)
|
|
2893
|
+
old_text = "\n\n".join(t for t in (_read_chapter(workdir, i)
|
|
2894
|
+
for i in range(1, start)) if t)
|
|
2895
|
+
if not old_text and start > 1:
|
|
2896
|
+
old_text = _archived_preface(workdir, start)
|
|
2897
|
+
budget = cap - len(new_text) - 2
|
|
2898
|
+
if budget <= 0:
|
|
2899
|
+
return new_text[:cap]
|
|
2900
|
+
if len(old_text) > budget:
|
|
2901
|
+
cut_mark = "……(前文较长,仅保留紧邻本批的尾部)……\n\n"
|
|
2902
|
+
keep = budget - len(cut_mark)
|
|
2903
|
+
old_text = cut_mark + (old_text[-keep:] if keep > 0 else "")
|
|
2904
|
+
return (old_text + "\n\n" + new_text)[:cap] if old_text else new_text[:cap]
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
def _chapters_named_in(issues, lo=1, hi=None):
|
|
2908
|
+
"""评审 issue 文本里点名的章号集合(「第22章」「第26—28章」都认)。
|
|
2909
|
+
用于把全局一致性 major 落到具体章;范围外的章号丢弃。"""
|
|
2910
|
+
out = set()
|
|
2911
|
+
for it in issues or []:
|
|
2912
|
+
if not isinstance(it, dict):
|
|
2913
|
+
continue
|
|
2914
|
+
for m in _CH_MENTION_RE.finditer(str(it.get("note") or it.get("issue") or "")):
|
|
2915
|
+
a = int(m.group(1))
|
|
2916
|
+
b = int(m.group(2)) if m.group(2) else a
|
|
2917
|
+
if b < a:
|
|
2918
|
+
a, b = b, a
|
|
2919
|
+
for c in range(a, b + 1):
|
|
2920
|
+
if c >= lo and (hi is None or c <= hi):
|
|
2921
|
+
out.add(c)
|
|
2922
|
+
return out
|
|
2923
|
+
|
|
2924
|
+
|
|
2925
|
+
def _dedup_issues(items, key_len=60):
|
|
2926
|
+
"""跨轮评审 issues 去重:同章同维度同要点(归一化前缀)只保留首条。
|
|
2927
|
+
全局评审每轮重跑都会把上一轮的问题原样或换个说法再列一遍,不去重会把
|
|
2928
|
+
同一处问题计成三四条 major,门禁报告数字虚高。"""
|
|
2929
|
+
seen, out = set(), []
|
|
2930
|
+
for x in items or []:
|
|
2931
|
+
if not isinstance(x, dict):
|
|
2932
|
+
continue
|
|
2933
|
+
k = (str(x.get("chapter")), str(x.get("dim")),
|
|
2934
|
+
re.sub(r"\s", "", str(x.get("note") or ""))[:key_len])
|
|
2935
|
+
if k in seen:
|
|
2936
|
+
continue
|
|
2937
|
+
seen.add(k)
|
|
2938
|
+
out.append(x)
|
|
2939
|
+
return out
|
|
2940
|
+
|
|
2941
|
+
|
|
2835
2942
|
def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route, resume_ctx, difficulty):
|
|
2836
2943
|
"""连载流水线:大纲 → 逐章起草/评审/修订 → 全局一致性评审 → 合并成书。"""
|
|
2837
2944
|
import json as _json
|
|
@@ -3410,6 +3517,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3410
3517
|
.replace("__TITLE__", ch["title"])
|
|
3411
3518
|
.replace("__BEATS__", branch_beats or ch["beats"] or "按大纲推进")
|
|
3412
3519
|
.replace("__HOOK__", branch_hook or ch.get("hook") or "留下悬念")
|
|
3520
|
+
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
3413
3521
|
.replace("__HIGHLIGHT__", ch.get("highlight") or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3414
3522
|
.replace("__STYLE__", style_tpl)
|
|
3415
3523
|
.replace("__QUALITY_GATE__", novel_quality.opening_requirements(i, wpc))
|
|
@@ -3804,6 +3912,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3804
3912
|
.replace("__GOAL__", task["goal"])
|
|
3805
3913
|
.replace("__VOLUME__", vol_block_for(i))
|
|
3806
3914
|
.replace("__BEATS__", ch.get("beats") or "按大纲推进")
|
|
3915
|
+
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
3807
3916
|
.replace("__HIGHLIGHT__", ch.get("highlight")
|
|
3808
3917
|
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3809
3918
|
.replace("__STYLE__", style_tpl)
|
|
@@ -3881,6 +3990,16 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3881
3990
|
store.update_run(run_id, story_tracking_warning=str(exc)[:300])
|
|
3882
3991
|
except Exception:
|
|
3883
3992
|
pass
|
|
3993
|
+
# 旧账换新账(章内修订回路):本章终评达标 → 用终轮评审的 issues 换掉
|
|
3994
|
+
# 第 1 轮的旧账。不换的话「修订后已达标」的章仍顶着第 1 轮 majors,
|
|
3995
|
+
# 最终 publishable 被陈账永远压着 False。放在 story_tracking 取材之后:
|
|
3996
|
+
# 事实沉淀仍吃得到修订前的完整意见,门禁只认终轮评判。
|
|
3997
|
+
if cs_new["passed"] and cj_by_agent:
|
|
3998
|
+
issues_all[:] = [x for x in issues_all if x.get("chapter") != i]
|
|
3999
|
+
for _cj in cj_by_agent.values():
|
|
4000
|
+
if isinstance(_cj, dict):
|
|
4001
|
+
issues_all.extend({"chapter": i, **it}
|
|
4002
|
+
for it in (_cj.get("issues") or [])[:6])
|
|
3884
4003
|
# 资源账本(借鉴角色资源账本):评审提出的道具/伤情/承诺/伏笔增量
|
|
3885
4004
|
# 追加到 .codebee/resource-ledger.md,下一章起草时注入,防跨章穿帮。
|
|
3886
4005
|
# 失败静默——账本是增强不是硬依赖。
|
|
@@ -3948,6 +4067,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3948
4067
|
.replace("__GOAL__", task["goal"])
|
|
3949
4068
|
.replace("__VOLUME__", vol_block_for(j))
|
|
3950
4069
|
.replace("__BEATS__", _wch.get("beats") or "按大纲推进")
|
|
4070
|
+
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, j))
|
|
3951
4071
|
.replace("__HIGHLIGHT__", _wch.get("highlight")
|
|
3952
4072
|
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3953
4073
|
.replace("__STYLE__", style_tpl)
|
|
@@ -3973,16 +4093,17 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3973
4093
|
store.update_run(run_id, signing_checkpoint=cp)
|
|
3974
4094
|
|
|
3975
4095
|
# ---- 3) 全局一致性评审(覆盖 1..end 全书:续写批次必须连同旧章一起查一致性)
|
|
3976
|
-
full_text =
|
|
4096
|
+
full_text = _full_manuscript(workdir, start, end)
|
|
3977
4097
|
global_issues = []
|
|
3978
4098
|
global_review_results = []
|
|
3979
4099
|
|
|
3980
4100
|
def run_global_round(agent_list):
|
|
3981
|
-
"""一轮全局评审:返回 (出分评审数,
|
|
4101
|
+
"""一轮全局评审:返回 (出分评审数, 按维累计分, 有效评审, 本轮 issues)。
|
|
4102
|
+
失败/不可解析不得当成低分计入。
|
|
3982
4103
|
|
|
3983
4104
|
多评审并发(2026-09-22,同章级评审并发):各自读同一份全书文本、互不依赖,
|
|
3984
4105
|
串行只是把等待时间叠起来。共享结构仍在 join 后按原顺序合并,结果与串行一致。"""
|
|
3985
|
-
gmeans_acc, scored, valid_reviews = {}, 0, []
|
|
4106
|
+
gmeans_acc, scored, valid_reviews, round_issues = {}, 0, [], []
|
|
3986
4107
|
results = {}
|
|
3987
4108
|
# 绑定解析提前到主线程(线程内不做会写盘的 bind_agent)
|
|
3988
4109
|
bound = [modelhub.bind_agent(a, difficulty) for a in agent_list]
|
|
@@ -4038,14 +4159,15 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4038
4159
|
if not errors:
|
|
4039
4160
|
valid_reviews.append({"id": agent_list[idx].get("id") or "reviewer-%d" % idx,
|
|
4040
4161
|
"scores": scores})
|
|
4041
|
-
|
|
4162
|
+
round_issues.extend({"chapter": "全书", **it} for it in (gj.get("issues") or [])[:8])
|
|
4042
4163
|
for d in dims:
|
|
4043
4164
|
v = scores.get(d) if not errors else partial_scores.get(d)
|
|
4044
4165
|
if v is not None:
|
|
4045
4166
|
gmeans_acc.setdefault(d, []).append(float(v))
|
|
4046
|
-
return scored, gmeans_acc, valid_reviews
|
|
4167
|
+
return scored, gmeans_acc, valid_reviews, round_issues
|
|
4047
4168
|
|
|
4048
|
-
gscored, gmeans_acc, global_review_results = run_global_round(critics)
|
|
4169
|
+
gscored, gmeans_acc, global_review_results, _gi = run_global_round(critics)
|
|
4170
|
+
global_issues.extend(_gi)
|
|
4049
4171
|
# 「评不上」≠「评了低分」:全局评审全挂时先从其它真实智能体补位(对齐章级
|
|
4050
4172
|
# 评审者级 fallback);补位后仍零分则判 run 失败——「无法评审」绝不能当成
|
|
4051
4173
|
# 「全局评审未通过」去盖「未达标」章(2026-09-18 假未达标案:codex 绑定链
|
|
@@ -4055,8 +4177,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4055
4177
|
tried = {a.get("id") for a in critics}
|
|
4056
4178
|
for spare in [a for a in (agents or [])
|
|
4057
4179
|
if a.get("mode") == "real" and a.get("id") not in tried][:2]:
|
|
4058
|
-
sc, acc, spare_reviews = run_global_round([spare])
|
|
4180
|
+
sc, acc, spare_reviews, _sgi = run_global_round([spare])
|
|
4059
4181
|
global_review_results.extend(spare_reviews)
|
|
4182
|
+
global_issues.extend(_sgi)
|
|
4060
4183
|
for d, xs in acc.items():
|
|
4061
4184
|
gmeans_acc.setdefault(d, []).extend(xs)
|
|
4062
4185
|
gscored += sc
|
|
@@ -4083,9 +4206,17 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4083
4206
|
while (not global_pass) and polish_rounds < 2 and chapter_scores and global_means:
|
|
4084
4207
|
polish_rounds += 1
|
|
4085
4208
|
weak = _weakest_chapters(chapter_scores, global_means, threshold, limit=2)
|
|
4209
|
+
# 全局一致性 major 点名的章优先进入打磨面:脱纲/主线漂移类问题的病灶章
|
|
4210
|
+
# 单章分数往往全部达标(章级评审只看局部),只按分数选章永远选不中
|
|
4211
|
+
# (2026-10-03 不周山守村人实案:21–28 章整体脱纲、章分全 8+,两轮
|
|
4212
|
+
# 打磨重改的全是达标章,全局分纹丝不动)。
|
|
4213
|
+
named = _chapters_named_in(global_issues, lo=start, hi=end)
|
|
4214
|
+
if named:
|
|
4215
|
+
weak = ([c for c in chapter_scores if c.get("chapter") in named]
|
|
4216
|
+
+ [c for c in weak if c.get("chapter") not in named])[:2]
|
|
4086
4217
|
if not weak:
|
|
4087
4218
|
break
|
|
4088
|
-
note = "自动打磨第 %d
|
|
4219
|
+
note = "自动打磨第 %d 轮:全局评审未过,重改 %s" % (
|
|
4089
4220
|
polish_rounds, "、".join("第 %d 章" % c["chapter"] for c in weak))
|
|
4090
4221
|
pstep, _ = store.add_step(run_id, "polish-r%d" % polish_rounds, impl["id"],
|
|
4091
4222
|
impl.get("label"), note=note)
|
|
@@ -4098,10 +4229,24 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4098
4229
|
for d in dims if float((c.get("means") or {}).get(d, 0.0)) < threshold)
|
|
4099
4230
|
gj = ";".join("%s %.1f" % (d, s2) for d, s2 in global_means.items()
|
|
4100
4231
|
if float(s2) < threshold)
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4232
|
+
# 本章被全局评审点名时附上原话,并放开「不得改主线」约束——脱纲章
|
|
4233
|
+
# 唯一的修法就是按大纲推翻自创情节,保事实微调恰恰修不动
|
|
4234
|
+
ch_named = [x for x in global_issues
|
|
4235
|
+
if i in _chapters_named_in([x], lo=i, hi=i)]
|
|
4236
|
+
if ch_named:
|
|
4237
|
+
crit = ("- 本章维度不达标:%s(阈值 %.1f)\n- 全书一致性评审指出的短板:%s"
|
|
4238
|
+
"\n- 全书评审对本章的点名(逐条对照处理):\n%s"
|
|
4239
|
+
"\n- 重改要求:若现稿剧情与「本章按大纲应完成」脱轨(评审已点名),"
|
|
4240
|
+
"以本章大纲要点为准重写剧情,可推翻现稿自创的情节线;"
|
|
4241
|
+
"本书其余各章的既成事实仍不得改动。"
|
|
4242
|
+
% (dims_txt or "(无)", threshold, gj or "(无)",
|
|
4243
|
+
"\n".join("- [%s] %s" % (x.get("dim"), str(x.get("note") or "")[:220])
|
|
4244
|
+
for x in ch_named[:4])))
|
|
4245
|
+
else:
|
|
4246
|
+
crit = ("- 本章维度不达标:%s(阈值 %.1f)\n- 全书一致性评审指出的短板:%s"
|
|
4247
|
+
"\n- 重改要求:优先修全书节奏/衔接问题(章间过渡、信息倾泻、主角主动性),"
|
|
4248
|
+
"再补本章短板;不得改动既有剧情主线的关键事实。"
|
|
4249
|
+
% (dims_txt or "(无)", threshold, gj or "(无)"))
|
|
4105
4250
|
_psig = novel_quality.signal_summary(_read_chapter(workdir, i), chapter=i)
|
|
4106
4251
|
if _psig.get("hints"):
|
|
4107
4252
|
crit += ("\n- 确定性复查提示(修订须对应处理):"
|
|
@@ -4114,7 +4259,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4114
4259
|
.replace("__I__", str(i)).replace("__FILE__", "chapter-%02d.md" % i)
|
|
4115
4260
|
.replace("__GOAL__", task["goal"])
|
|
4116
4261
|
.replace("__VOLUME__", vol_block_for(i))
|
|
4262
|
+
.replace("__BEATS__", ch.get("beats") or "按大纲推进")
|
|
4263
|
+
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
4264
|
+
.replace("__HIGHLIGHT__", ch.get("highlight")
|
|
4265
|
+
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
4117
4266
|
.replace("__CRITIQUE__", crit)
|
|
4267
|
+
.replace("__STYLE__", style_tpl)
|
|
4118
4268
|
.replace("__WORDS__", str(wpc)))
|
|
4119
4269
|
prompt = attachments.append_task_context(prompt, task)
|
|
4120
4270
|
res = _run_step(run_id, "polish-c%d" % i, modelhub.bind_agent(impl, difficulty),
|
|
@@ -4158,6 +4308,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4158
4308
|
c2["polished"] = True
|
|
4159
4309
|
c2["words"] = _wc(_read_chapter(workdir, i))
|
|
4160
4310
|
fixed.append(i)
|
|
4311
|
+
if repolished:
|
|
4312
|
+
# 旧账换新账(章级):该章旧 issues 是对打磨前稿件的评判,重写
|
|
4313
|
+
# 已落盘并重评,陈账不撤会同全局陈账一起永远压着 publishable
|
|
4314
|
+
issues_all[:] = [x for x in issues_all if x.get("chapter") != i]
|
|
4315
|
+
for _cj in cj_by_agent.values():
|
|
4316
|
+
if isinstance(_cj, dict):
|
|
4317
|
+
issues_all.extend({"chapter": i, **it}
|
|
4318
|
+
for it in (_cj.get("issues") or [])[:6])
|
|
4161
4319
|
store.update_run(run_id, chapter_scores=chapter_scores)
|
|
4162
4320
|
_check_cancel(ev)
|
|
4163
4321
|
if not fixed:
|
|
@@ -4173,8 +4331,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4173
4331
|
# 重评全书一致性(同一评审闭包;本轮全挂则保留上一轮结论——评审链挂了
|
|
4174
4332
|
# 不代表书变差,不能拿「无法评审」覆盖真实分数)
|
|
4175
4333
|
try:
|
|
4176
|
-
full_text =
|
|
4177
|
-
gscored2, gmeans_acc2, global_review_results2 = run_global_round(critics)
|
|
4334
|
+
full_text = _full_manuscript(workdir, start, end)
|
|
4335
|
+
gscored2, gmeans_acc2, global_review_results2, gi2 = run_global_round(critics)
|
|
4178
4336
|
except BaseException:
|
|
4179
4337
|
store.finish_step(run_id, pstep["n"], "failed",
|
|
4180
4338
|
summary="打磨后全书重评异常中止,已落盘章稿不受影响")
|
|
@@ -4185,6 +4343,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4185
4343
|
global_pass = _all_ge(global_means, threshold)
|
|
4186
4344
|
global_review = quality_gate.aggregate_reviews(
|
|
4187
4345
|
global_review_results, dims, threshold=threshold)
|
|
4346
|
+
# 旧账换新账:上一轮的 majors 是对打磨前稿件的评判,重评出了新分
|
|
4347
|
+
# 却不换 issues,重写修好了也永远 publishable=False(陈账死闸)
|
|
4348
|
+
global_issues[:] = gi2
|
|
4188
4349
|
store.finish_step(run_id, pstep["n"], "done" if global_pass else "failed",
|
|
4189
4350
|
summary="重改 %s;打磨后全局 %s(%s)" % (
|
|
4190
4351
|
"、".join("第 %d 章" % x for x in fixed),
|
|
@@ -4197,6 +4358,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4197
4358
|
ms_name = _ms_name(task.get("manuscript"))
|
|
4198
4359
|
book_title = outline.get("book_title") or task["title"]
|
|
4199
4360
|
parts = ["# %s" % book_title, ""]
|
|
4361
|
+
# 前文归档回退:前作章文件已被归档(chapter-NN.md 不在场)时,合并成品
|
|
4362
|
+
# 不能只剩本批——否则续写批次的「成书」会静默丢掉此前全部章节
|
|
4363
|
+
arch_preface = _archived_preface(workdir, start) if start > 1 else ""
|
|
4364
|
+
if arch_preface:
|
|
4365
|
+
parts.append("## 前文(第 1–%d 章 · 归档稿)" % (start - 1))
|
|
4366
|
+
parts.append("")
|
|
4367
|
+
parts.append(arch_preface)
|
|
4368
|
+
parts.append("")
|
|
4200
4369
|
# 分卷标题:卷首章之前插「第 X 卷 《卷名》」分隔(全书 1..end 都插,续写
|
|
4201
4370
|
# 批次合并时上一批的卷标题也一并补上,不会只在首批出现)
|
|
4202
4371
|
vol_heads = {}
|
|
@@ -4217,13 +4386,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4217
4386
|
f.write("\n".join(parts))
|
|
4218
4387
|
total_words = _wc("\n".join(parts))
|
|
4219
4388
|
store.finish_step(run_id, step["n"], "done",
|
|
4220
|
-
summary="已合并 %d 章为 %s(约 %d 字%s)" % (
|
|
4389
|
+
summary="已合并 %d 章为 %s(约 %d 字%s%s)" % (
|
|
4221
4390
|
n, ms_name, total_words,
|
|
4222
|
-
",分 %d 卷" % len(vol_heads) if vol_heads else ""
|
|
4391
|
+
",分 %d 卷" % len(vol_heads) if vol_heads else "",
|
|
4392
|
+
",前文 1–%d 章取自归档" % (start - 1) if arch_preface else ""),
|
|
4223
4393
|
duration_s=0.1)
|
|
4224
4394
|
|
|
4225
4395
|
chapters_pass = all(c["passed"] for c in chapter_scores)
|
|
4226
|
-
major_issues = [x for x in issues_all + global_issues
|
|
4396
|
+
major_issues = [x for x in _dedup_issues(list(issues_all) + list(global_issues))
|
|
4227
4397
|
if isinstance(x, dict) and x.get("severity") == "major"]
|
|
4228
4398
|
publishable = bool(chapters_pass and global_pass and not major_issues)
|
|
4229
4399
|
signing_checkpoint = (store.get_run(run_id) or {}).get("signing_checkpoint") or {}
|
|
@@ -4288,7 +4458,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4288
4458
|
lines += ["", "## 全局评审(阈值 %.1f)" % threshold, ""]
|
|
4289
4459
|
lines += ["- %s:%.1f" % (d, global_means.get(d, 0.0)) for d in dims]
|
|
4290
4460
|
lines += ["", "## 主要问题", ""]
|
|
4291
|
-
majors = [x for x in issues_all + global_issues
|
|
4461
|
+
majors = [x for x in _dedup_issues(list(issues_all) + list(global_issues))
|
|
4462
|
+
if x.get("severity") == "major"][:12]
|
|
4292
4463
|
if majors:
|
|
4293
4464
|
lines.extend("- [第%s章][%s] %s" % (str(x.get("chapter", "?")), x.get("dim", "?"),
|
|
4294
4465
|
str(x.get("note", ""))[:150]) for x in majors)
|
package/app/core/planner.py
CHANGED
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
import time
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from . import knowledge, modelhub, runner, skills, usage, volumes
|
|
16
17
|
|
|
@@ -283,6 +284,8 @@ __SKILLS__
|
|
|
283
284
|
硬性要求:
|
|
284
285
|
- 第 1 章直接衔接前文(见下方前情),不得跳线、不得重启设定、不得复述前文;
|
|
285
286
|
- 主线沿既有脉络推进,新冲突尽量从已埋伏笔中生长,人物性格与前文一致;
|
|
287
|
+
- 若下方给出「锁定章纲」:本批各章 title/beats/hook 必须逐章从对应章纲提炼对齐
|
|
288
|
+
(标题沿用、视角不换、节拍不裁并、伏笔揭示位不提前),不得虚构章纲之外的主线事件;
|
|
286
289
|
- 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账;
|
|
287
290
|
- 复杂情感必须落到触发事件、动作/生理反应和潜台词,不能用一句概括代替;
|
|
288
291
|
- 延续既定叙述视角和语言锚点(见下方既定文风锚),删掉重复复述,避免节奏变慢;
|
|
@@ -295,6 +298,9 @@ __STYLE_ANCHOR__
|
|
|
295
298
|
## 前情大纲(已完成章节,章号为全书章号)
|
|
296
299
|
__PREV_OUTLINE__
|
|
297
300
|
|
|
301
|
+
## 锁定章纲(本批各章已确认的章纲,最高优先级;为空则按前情自然规划)
|
|
302
|
+
__LOCKED_OUTLINE__
|
|
303
|
+
|
|
298
304
|
## 最新一章结尾(衔接锚点)
|
|
299
305
|
__PREV_TAIL__
|
|
300
306
|
|
|
@@ -480,6 +486,86 @@ def book_volume_plan(task, outline, upto):
|
|
|
480
486
|
return volumes.merge_titles(plan, named)
|
|
481
487
|
|
|
482
488
|
|
|
489
|
+
_CH_OUTLINE_FILE_RE = re.compile(r"ch-(\d{1,4})\.md$", re.IGNORECASE)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _read_workdir_text(p, cap):
|
|
493
|
+
"""章纲/卷纲文件读文本:UTF-8 → GBK → replace(同 _prev_serial_story 纪律,
|
|
494
|
+
CLI 子代理在中文 Windows 上可能把文件落成 GBK)。"""
|
|
495
|
+
try:
|
|
496
|
+
b = p.read_bytes()
|
|
497
|
+
except OSError:
|
|
498
|
+
return ""
|
|
499
|
+
for enc in ("utf-8", "gbk"):
|
|
500
|
+
try:
|
|
501
|
+
return b.decode(enc)[:cap].strip()
|
|
502
|
+
except UnicodeDecodeError:
|
|
503
|
+
continue
|
|
504
|
+
return b.decode("utf-8", "replace")[:cap].strip()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def locked_chapter_outline_text(workdir, chapter, cap=3000):
|
|
508
|
+
"""工作目录里本章的锁定章纲全文(章纲/*ch-<N>.md,按文件名章号精确匹配)。
|
|
509
|
+
逐章章纲是作者逐章确认过的事实标准——大纲生成、起草、修订三面都应以它为
|
|
510
|
+
最高优先级,否则「起草跟批次大纲、评审跟章纲」会永久互斥(2026-10-03
|
|
511
|
+
不周山 21-28 章 rogue 大纲实案)。无匹配文件返回 ""(老书零噪音)。"""
|
|
512
|
+
if not workdir or chapter is None:
|
|
513
|
+
return ""
|
|
514
|
+
try:
|
|
515
|
+
base = Path(str(workdir)) / "章纲"
|
|
516
|
+
if not base.is_dir():
|
|
517
|
+
return ""
|
|
518
|
+
hits = []
|
|
519
|
+
for p in base.iterdir():
|
|
520
|
+
if not p.is_file():
|
|
521
|
+
continue
|
|
522
|
+
m = _CH_OUTLINE_FILE_RE.search(p.name)
|
|
523
|
+
if m and int(m.group(1)) == int(chapter):
|
|
524
|
+
hits.append(p)
|
|
525
|
+
except OSError:
|
|
526
|
+
return ""
|
|
527
|
+
if not hits:
|
|
528
|
+
return ""
|
|
529
|
+
hits.sort(key=lambda p: p.name) # 同章号多份时取文件名序最大(卷号更高者)
|
|
530
|
+
return _read_workdir_text(hits[-1], cap)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def locked_serial_outline_block(task, start, end, per_cap=2500, total_cap=16000):
|
|
534
|
+
"""续写大纲生成的锁定章纲注入块:本批 [start, end] 逐章汇总章纲全文。
|
|
535
|
+
一份章纲都没有时回退 大纲/*.md(截断兜底,至少锚住卷纲方向);两者皆无
|
|
536
|
+
返回 ""(零噪音)。"""
|
|
537
|
+
workdir = str(task.get("workdir") or "")
|
|
538
|
+
parts, used = [], 0
|
|
539
|
+
if workdir:
|
|
540
|
+
for n in range(int(start), int(end) + 1):
|
|
541
|
+
if used >= total_cap:
|
|
542
|
+
break
|
|
543
|
+
t = locked_chapter_outline_text(workdir, n, cap=per_cap)
|
|
544
|
+
if t:
|
|
545
|
+
head = "### 第 %d 章锁定章纲" % n
|
|
546
|
+
parts.append(head + "\n" + t)
|
|
547
|
+
used += len(head) + len(t) + 2
|
|
548
|
+
if parts:
|
|
549
|
+
return "\n\n".join(parts)[:total_cap]
|
|
550
|
+
if workdir:
|
|
551
|
+
try:
|
|
552
|
+
base = Path(workdir) / "大纲"
|
|
553
|
+
if base.is_dir():
|
|
554
|
+
buf, used = [], 0
|
|
555
|
+
for p in sorted(base.glob("*.md")):
|
|
556
|
+
if used >= total_cap:
|
|
557
|
+
break
|
|
558
|
+
t = _read_workdir_text(p, min(4000, total_cap - used))
|
|
559
|
+
if t:
|
|
560
|
+
buf.append("### %s\n%s" % (p.name, t))
|
|
561
|
+
used += len(t) + len(p.name) + 8
|
|
562
|
+
if buf:
|
|
563
|
+
return "\n\n".join(buf)[:total_cap]
|
|
564
|
+
except OSError:
|
|
565
|
+
pass
|
|
566
|
+
return ""
|
|
567
|
+
|
|
568
|
+
|
|
483
569
|
def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
|
|
484
570
|
deadline=None, run_id=None):
|
|
485
571
|
"""连载大纲:编排者 API 优先 → 作者 CLI → 模板。返回 {book_title, chapters:[{title,beats,hook}]}。
|
|
@@ -520,6 +606,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
520
606
|
else:
|
|
521
607
|
vol_block, vol_key = "", ""
|
|
522
608
|
if start > 1:
|
|
609
|
+
locked_block = locked_serial_outline_block(task, start, start + n - 1)
|
|
523
610
|
prompt = (SERIAL_CONTINUE_OUTLINE_PROMPT
|
|
524
611
|
.replace("__SKILLS__", sk_block)
|
|
525
612
|
.replace("__DONE__", str(done))
|
|
@@ -532,6 +619,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
532
619
|
or "(前文未记录文风锚,请依据前情大纲与最新一章结尾自行归纳,"
|
|
533
620
|
"归纳后全批保持一致)")
|
|
534
621
|
.replace("__PREV_OUTLINE__", prev_lines or "(无大纲记录,请依据下方最新一章结尾与小说目标衔接)")
|
|
622
|
+
.replace("__LOCKED_OUTLINE__", locked_block or "(无)")
|
|
535
623
|
.replace("__PREV_TAIL__", prev_tail or "(无)")
|
|
536
624
|
.replace("__GOAL__", task["goal"])
|
|
537
625
|
.replace("__CONTEXT__", task.get("context") or "(无)"))
|