codebee 0.1.69 → 0.1.71
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 +589 -543
- package/LICENSE +21 -21
- package/README.md +509 -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/browser-final-1440.png +0 -0
- package/app/browser-final-390.png +0 -0
- package/app/browser-google-fallback.png +0 -0
- package/app/browser-google-test.png +0 -0
- package/app/browser-workbench-1440.png +0 -0
- package/app/browser-workbench-390.png +0 -0
- package/app/browser-workspace-google.png +0 -0
- package/app/browser-workspace-smoke.png +0 -0
- package/app/browser-workspace.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- 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 -0
- package/app/core/automation.py +107 -12
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -0
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +935 -930
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1673 -1234
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +7 -1
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- 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 -173
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +11 -2
- package/app/core/errorlog.py +137 -137
- package/app/core/evalbench.py +720 -0
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +234 -9
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +22 -1
- package/app/core/manager.py +2143 -2122
- package/app/core/market_remote.py +10 -0
- package/app/core/mcp_client.py +287 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +185 -2
- package/app/core/notify.py +201 -106
- package/app/core/novel_quality.py +189 -0
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +138 -138
- package/app/core/paths.py +4 -0
- package/app/core/pipeline.py +426 -28
- package/app/core/planner.py +243 -22
- package/app/core/plugins.py +525 -0
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +655 -539
- package/app/core/publish/fanqie.py +165 -159
- package/app/core/publish/flow.py +589 -440
- package/app/core/publish/flows-fanqie-calibrated.json +336 -332
- package/app/core/publish/ledger.py +229 -203
- package/app/core/publish/manager.py +824 -692
- package/app/core/publish/qimao.py +146 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/remotesync.py +121 -0
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2067
- 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.py +42 -4
- package/app/core/settings_schema.py +270 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +122 -122
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- 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/step_runner.py +132 -132
- package/app/core/store.py +30 -8
- 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 -862
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2614 -2465
- package/app/current-browser.png +0 -0
- package/app/main.py +316 -17
- package/app/mcp_server.py +175 -0
- package/app/pet.py +83 -22
- package/app/pick_dialog.py +110 -110
- package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
- package/app/plugins/code-review-helper/skills/review.md +16 -0
- package/app/ui/app.js +1391 -77
- 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 +249 -7
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +277 -13
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +845 -12
- package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +18 -0
- package/bin/tutti.js +147 -147
- package/package.json +47 -46
- package/tools/wx_reader.py +261 -261
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
|
|
22
|
+
from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, novel_quality, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
|
|
23
23
|
from . import builtin_agent
|
|
24
24
|
from . import diagnostics
|
|
25
25
|
from . import paths as paths_mod
|
|
@@ -500,6 +500,24 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
500
500
|
return res
|
|
501
501
|
|
|
502
502
|
|
|
503
|
+
def _create_task_from_builtin(payload):
|
|
504
|
+
"""Create and enqueue a task requested by the direct conversation tool."""
|
|
505
|
+
task = store.create_task(payload)
|
|
506
|
+
run = None
|
|
507
|
+
try:
|
|
508
|
+
run = store.create_run("orchestration", task["title"], task_id=task["id"])
|
|
509
|
+
store.update_task_status(task["id"], "queued")
|
|
510
|
+
jobs.enqueue({"kind": "orchestration", "run_id": run["id"],
|
|
511
|
+
"task_id": task["id"]})
|
|
512
|
+
except Exception:
|
|
513
|
+
if run:
|
|
514
|
+
store.update_run(run["id"], status="failed", error="运行记录初始化失败",
|
|
515
|
+
ended_at=_now())
|
|
516
|
+
store.update_task_status(task["id"], "failed")
|
|
517
|
+
raise
|
|
518
|
+
return {"task_id": task["id"], "run_id": run["id"]}
|
|
519
|
+
|
|
520
|
+
|
|
503
521
|
def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
|
|
504
522
|
followups=False):
|
|
505
523
|
"""内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
|
|
@@ -560,7 +578,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
|
|
|
560
578
|
res = builtin_agent.run(bi, prompt, workdir, timeout=remaining or 180,
|
|
561
579
|
deadline=deadline, cancel_event=ev, log=_log, images=images,
|
|
562
580
|
on_reason=_on_reason, on_stream=_on_stream,
|
|
563
|
-
on_activity=_on_activity
|
|
581
|
+
on_activity=_on_activity,
|
|
582
|
+
task_creator=_create_task_from_builtin)
|
|
564
583
|
if followups and res.get("ok"):
|
|
565
584
|
clean, fups = _parse_followups(res.get("text") or "")
|
|
566
585
|
if fups:
|
|
@@ -610,10 +629,59 @@ def _budget_max_tokens():
|
|
|
610
629
|
return 0
|
|
611
630
|
|
|
612
631
|
|
|
632
|
+
def _budget_cost_caps():
|
|
633
|
+
"""日/月花费硬顶(¥,按台账标定单价折算的 cost 列累计);0 = 不限。
|
|
634
|
+
|
|
635
|
+
返回 (daily_cap, monthly_cap)。读台账失败按 0 处理(闸放行)——
|
|
636
|
+
统计层故障不能把业务锁死。
|
|
637
|
+
"""
|
|
638
|
+
try:
|
|
639
|
+
from .settings_schema import get as ss_get, register_default_namespaces
|
|
640
|
+
register_default_namespaces()
|
|
641
|
+
d = float(ss_get("budget", "daily_cost_yuan") or 0)
|
|
642
|
+
m = float(ss_get("budget", "monthly_cost_yuan") or 0)
|
|
643
|
+
return max(0.0, d), max(0.0, m)
|
|
644
|
+
except Exception:
|
|
645
|
+
return 0.0, 0.0
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _cost_gate_block():
|
|
649
|
+
"""花费硬顶闸:超出日/月上限返回人话报文(ENV_BLOCK 形状),否则 None。
|
|
650
|
+
|
|
651
|
+
与 token 预算闸同一命中形状:只拦「下一步」,允许越过线的当前步完成;
|
|
652
|
+
auto 续跑可在用户调高上限(或次日/次月额度重置)后接手;同因连撞由
|
|
653
|
+
repeat-guard 止损,不会烧钱(被拦步骤零真实调用)。
|
|
654
|
+
"""
|
|
655
|
+
daily_cap, monthly_cap = _budget_cost_caps()
|
|
656
|
+
if daily_cap <= 0 and monthly_cap <= 0:
|
|
657
|
+
return None
|
|
658
|
+
try:
|
|
659
|
+
from .usage import cost_snapshot
|
|
660
|
+
today, month = cost_snapshot()
|
|
661
|
+
except Exception:
|
|
662
|
+
return None
|
|
663
|
+
if daily_cap > 0 and today >= daily_cap:
|
|
664
|
+
return ("已超出每日花费预算:¥%.2f/¥%.2f(可在设置→编排设置→预算 调整,"
|
|
665
|
+
"次日自动恢复),停止后续步骤" % (today, daily_cap))
|
|
666
|
+
if monthly_cap > 0 and month >= monthly_cap:
|
|
667
|
+
return ("已超出每月花费预算:¥%.2f/¥%.2f(可在设置→编排设置→预算 调整,"
|
|
668
|
+
"次月自动恢复),停止后续步骤" % (month, monthly_cap))
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
613
672
|
def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
614
673
|
timeout, resume, step, log_abs, images=None, require_tools=False,
|
|
615
674
|
deadline=None):
|
|
616
675
|
"""真实 CLI 调用:压缩灰度路径或原路径。"""
|
|
676
|
+
# 花费硬顶闸(先于 token 闸:钱比 token 更早见顶):命中直接返回 ENV_BLOCK 形状。
|
|
677
|
+
cost_block = _cost_gate_block()
|
|
678
|
+
if cost_block:
|
|
679
|
+
from .error_codes import ErrorCode
|
|
680
|
+
return {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
|
|
681
|
+
"tokens": 0, "usage": None, "error": cost_block,
|
|
682
|
+
"error_code": ErrorCode.ENV_BLOCK, "sid": "",
|
|
683
|
+
"raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
|
|
684
|
+
"model": agent.get("model")}
|
|
617
685
|
# T2.1 预算闸:已用 token 达到单次 run 上限 → 阻断后续真实调用(ENV_BLOCK)。
|
|
618
686
|
# 只拦「下一步」,允许越过线的当前步完成;auto 续跑可在用户调高预算后接手。
|
|
619
687
|
cap = _budget_max_tokens()
|
|
@@ -709,7 +777,8 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
709
777
|
output=(res.get("text") or ""),
|
|
710
778
|
followups=res.get("followups"),
|
|
711
779
|
# 思考过程(内置智能体流式抓取):落进步骤记录,对话气泡折叠展示
|
|
712
|
-
thinking=res.get("reasoning") or None
|
|
780
|
+
thinking=res.get("reasoning") or None,
|
|
781
|
+
partial=bool(res.get("partial")))
|
|
713
782
|
# 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
|
|
714
783
|
# 用户主动取消不入账——那不是产品问题;detail 只存脱敏后的失败摘录。
|
|
715
784
|
if status in ("failed", "timeout"):
|
|
@@ -927,12 +996,30 @@ def _run_verify(run_id, task, workdir, ev):
|
|
|
927
996
|
return ok, True
|
|
928
997
|
|
|
929
998
|
|
|
999
|
+
def _review_depth_note(diff):
|
|
1000
|
+
"""评审深度随 diff 规模分级(借鉴 pr-af 模型分级:评审投入应随变更量级变化)。
|
|
1001
|
+
纯提示词指引,不改 pass 判定语义:小改动不逼评审员凑字数(省 token),
|
|
1002
|
+
大变更先概览后深看高风险区(防浮于表面)。中等规模不给指引(默认深度)。"""
|
|
1003
|
+
lines = (diff or "").count("\n")
|
|
1004
|
+
if 0 < lines < 40:
|
|
1005
|
+
return ("\n\n## 评审深度指引(小改动快速评审)\n"
|
|
1006
|
+
"本次变更很小(%d 行)。聚焦正确性与验收命令是否真实覆盖改动,"
|
|
1007
|
+
"不展开风格类与假设性建议;确认无 blocker 即可给 pass。" % lines)
|
|
1008
|
+
if lines >= 600:
|
|
1009
|
+
return ("\n\n## 评审深度指引(大变更评审)\n"
|
|
1010
|
+
"本次变更较大(%d 行)。先逐 hunk 概览建立全貌,再对高风险区"
|
|
1011
|
+
"(安全/并发/数据与迁移/公共 API 契约)逐行深看;"
|
|
1012
|
+
"minor 问题汇总一条即可,不逐条展开。" % lines)
|
|
1013
|
+
return ""
|
|
1014
|
+
|
|
1015
|
+
|
|
930
1016
|
def _run_review(run_id, task, workdir, reviewer, ev):
|
|
931
1017
|
diff = _git_diff(workdir)
|
|
932
1018
|
prompt = (CODE_REVIEW_PROMPT
|
|
933
1019
|
.replace("__GOAL__", task["goal"])
|
|
934
1020
|
.replace("__VERIFY__", task.get("verify_command") or "(未配置)")
|
|
935
1021
|
.replace("__DIFF__", diff or "(无法获取 git diff,请综合任务目标谨慎评审)"))
|
|
1022
|
+
prompt += _review_depth_note(diff)
|
|
936
1023
|
if task.get("context"):
|
|
937
1024
|
prompt += "\n\n## 原始背景与附件要求\n" + task["context"]
|
|
938
1025
|
res = _run_step(run_id, "review", reviewer, prompt, workdir, readonly=True, ev=ev,
|
|
@@ -1936,6 +2023,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1936
2023
|
|
|
1937
2024
|
sid = (resume_ctx["session"] if resume_ctx else "") or ""
|
|
1938
2025
|
last_text = ""
|
|
2026
|
+
partial_turn = False
|
|
1939
2027
|
# 追话起跑(/api/runs/<id>/chat → retry_task):信箱已有未消费消息 = 这是对话
|
|
1940
2028
|
# 的下一轮而非首轮。CLI 继承上一轮的会话 id(真的「接着上次聊」),内置智能体
|
|
1941
2029
|
# 靠「上一轮输出(结尾)」块带上下文;同时把首步切成续轮档。
|
|
@@ -2021,6 +2109,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
2021
2109
|
return
|
|
2022
2110
|
turns += 1
|
|
2023
2111
|
last_text = (res.get("text") or "").strip()
|
|
2112
|
+
partial_turn = partial_turn or bool(res.get("partial"))
|
|
2024
2113
|
if impl is not None:
|
|
2025
2114
|
new_sid = _resume_sid(impl, res.get("sid"))
|
|
2026
2115
|
if new_sid:
|
|
@@ -2039,7 +2128,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
2039
2128
|
|
|
2040
2129
|
verdict = {"type": task["type"], "engine": "direct", "pass": True, "mode": mode,
|
|
2041
2130
|
"direct": True, "turns": turns,
|
|
2042
|
-
"impl": "builtin" if bi is not None else impl["id"], "route": route
|
|
2131
|
+
"impl": "builtin" if bi is not None else impl["id"], "route": route,
|
|
2132
|
+
"partial": partial_turn}
|
|
2043
2133
|
impl_label = ("CodeBee(%s · %s)" % (bi["provider_name"], bi["model"])
|
|
2044
2134
|
if bi is not None else impl.get("label"))
|
|
2045
2135
|
report = ["# 直连任务:%s" % task["title"], "",
|
|
@@ -2096,6 +2186,10 @@ CONTENT_DELIVERY_CONTRACTS = {
|
|
|
2096
2186
|
]),
|
|
2097
2187
|
"article": ("平台内容主编", [
|
|
2098
2188
|
"标题、开头钩子、正文层级和结尾行动建议要适配目标平台与读者。",
|
|
2189
|
+
# AIWriteX 借鉴(2026-09-27):公众号/头条/小红书等平台阅读主战场在手机,
|
|
2190
|
+
# 泛泛「适配平台」落不到执行——短段落、小标题分节是这类平台的排版硬需求
|
|
2191
|
+
"面向移动端平台(公众号/头条/小红书等)时按手机阅读节奏排版:"
|
|
2192
|
+
"每段三行以内、用小标题分节、关键结论独立成段便于扫读。",
|
|
2099
2193
|
"事实、数据和引语不得编造;缺少来源时明确标注待核实。",
|
|
2100
2194
|
]),
|
|
2101
2195
|
"video_script": ("短视频编导", [
|
|
@@ -2177,7 +2271,10 @@ def _content_contract(task):
|
|
|
2177
2271
|
# 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
|
|
2178
2272
|
# 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采。
|
|
2179
2273
|
# 来源冲突裁决借鉴 deepresearch-agent 记忆层矛盾检测(Majority/Source-Weight 消解);
|
|
2180
|
-
# 子问题缺口补查借鉴 dzhng/deep-research
|
|
2274
|
+
# 子问题缺口补查借鉴 dzhng/deep-research 迭代循环;
|
|
2275
|
+
# 来源可信度审查与主动找反例借鉴对抗降噪(2026-09-28:Cornell Tech 深研 agent
|
|
2276
|
+
# 可被 UGC 误导内容操纵 / MisKnow-Agent 误导暴露即致错 / ARGUS 误信息注入防御 /
|
|
2277
|
+
# deepresearch-agent Red-Blue 两队互搏——检索降噪多源独立验证后收窄的落地切片)
|
|
2181
2278
|
RESEARCH_APPENDIX = """
|
|
2182
2279
|
|
|
2183
2280
|
## 调研要求(证据链)
|
|
@@ -2188,11 +2285,28 @@ RESEARCH_APPENDIX = """
|
|
|
2188
2285
|
- 来源冲突显式裁决:同一事实多个来源说法不一致时,写明分歧点与各方依据,给出
|
|
2189
2286
|
倾向判断和理由(多数来源一致取多数,权威差异按来源可信度加权)——不各说一半,
|
|
2190
2287
|
也不悄悄只取其一。
|
|
2288
|
+
- 来源先审后用:营销软文、SEO 内容农场、利益相关方的自我背书一律降权,引用时
|
|
2289
|
+
点明其立场;同一说法优先追溯到更原始的出处(转述会走样)。
|
|
2290
|
+
- 结论要经得起反例:关键结论不仅找支持证据,也主动找反对证据;找不到才可写强
|
|
2291
|
+
结论,写完自问「什么证据能推翻它」并在风险与局限里如实回答。
|
|
2191
2292
|
- 缺口驱动补查:起草前先列出报告要回答的子问题清单,逐个核对证据是否到位;
|
|
2192
2293
|
没找到答案的子问题明确标注「证据不足」,不用泛泛而谈填空。
|
|
2193
2294
|
- 结构硬性要求:报告第一段必须是「**核心结论**」三行以内的要点摘要(结论先行),
|
|
2194
2295
|
之后才展开分层论证 → 风险与局限(说明哪些结论证据不足)。"""
|
|
2195
2296
|
|
|
2297
|
+
# 翻译类稿件的追加要求(借鉴 andrewyng/translation-agent 三步法 translate→reflect→improve
|
|
2298
|
+
# 的 glossary 机制):动笔前先提炼术语表、全篇译名以表为准——术语一致性此前只靠
|
|
2299
|
+
# 评审 rubric「术语一致性」事后抓,起草侧无约定,长文译名漂移只能靠修订轮返工。
|
|
2300
|
+
# TransAgents(多智能体文学翻译)的出版社角色分工不借鉴:评审链已有对应物。
|
|
2301
|
+
TRANSLATION_APPENDIX = """
|
|
2302
|
+
|
|
2303
|
+
## 翻译要求(术语表先行)
|
|
2304
|
+
- 动笔前先通读全文,提炼术语表放在稿件开头(`## 术语表`):专有名词(人名/地名/
|
|
2305
|
+
作品名/产品名/机构名)与反复出现的领域术语,逐条给出「源文 → 译名」。
|
|
2306
|
+
- 全篇译名以术语表为准:同一源文术语从头到尾一个译法,不随上下文漂移;业界已有
|
|
2307
|
+
通用译名的从通用,没有的定一个贴切的并全篇保持一致。
|
|
2308
|
+
- 无术语可提炼的短文可省略术语表,译名一致性要求不变。"""
|
|
2309
|
+
|
|
2196
2310
|
NOVEL_REVISE_PROMPT = """你是__ROLE__。请根据下方汇总评审意见修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
2197
2311
|
|
|
2198
2312
|
## 原始写作任务
|
|
@@ -2213,6 +2327,7 @@ NOVEL_CRITIQUE_PROMPT = """你是严格的评审(不要使用任何工具、
|
|
|
2213
2327
|
"summary": "一句话总评"
|
|
2214
2328
|
}
|
|
2215
2329
|
每个维度打 1-10 分(可为小数),宁严勿宽。major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2330
|
+
评审必须逐项回答:叙述语域与视角是否统一;复杂情感是否有动作、五感或生理细节;开篇是否在前 600 字进入具体冲突;本章是否发生局面变化;是否存在可删的重复铺垫或长段解释。
|
|
2216
2331
|
|
|
2217
2332
|
## 待评审稿件
|
|
2218
2333
|
---
|
|
@@ -2395,6 +2510,10 @@ def _ensure_critique_placeholders(tpl):
|
|
|
2395
2510
|
"""自定义评审模板缺占位符时补上,避免稿件内容/维度定义丢失导致盲评。"""
|
|
2396
2511
|
if "__MANUSCRIPT__" not in tpl:
|
|
2397
2512
|
tpl += "\n\n## 待评审稿件\n---\n__MANUSCRIPT__\n---"
|
|
2513
|
+
elif "## 待评审稿件" not in tpl:
|
|
2514
|
+
# 自定义模板可能只放占位符、不提供标题;统一补锚点,保证经验库、
|
|
2515
|
+
# 故事圣经和确定性文本信号仍能插到稿件正文之前。
|
|
2516
|
+
tpl = tpl.replace("__MANUSCRIPT__", "## 待评审稿件\n---\n__MANUSCRIPT__\n---", 1)
|
|
2398
2517
|
if "__DIMKEYS__" not in tpl:
|
|
2399
2518
|
tpl = ("请按维度打分(1-10 分)。\n\n" + tpl)
|
|
2400
2519
|
return tpl
|
|
@@ -2419,6 +2538,7 @@ __VOLUME__
|
|
|
2419
2538
|
- 章节标题:__TITLE__
|
|
2420
2539
|
- 剧情要点:__BEATS__
|
|
2421
2540
|
- 章末钩子:__HOOK__
|
|
2541
|
+
- 本章爽点/情绪爆点:__HIGHLIGHT__(必须按特写镜头写厚:铺垫在前、放大在中、余波在后,用动作/五感/生理反应/环境反馈呈现,禁止一句话带过)
|
|
2422
2542
|
- 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
|
|
2423
2543
|
|
|
2424
2544
|
## 前情提要(此前各章结尾摘录,衔接用)
|
|
@@ -2427,6 +2547,12 @@ __PREV__
|
|
|
2427
2547
|
## 资源账本(道具/伤情/承诺/伏笔的现状登记,写本章前必须核对)
|
|
2428
2548
|
__LEDGER__
|
|
2429
2549
|
|
|
2550
|
+
## 全书文风锚(本章必须遵守;与前文各章保持同一笔调)
|
|
2551
|
+
__STYLE__
|
|
2552
|
+
|
|
2553
|
+
## 签约质量门禁
|
|
2554
|
+
__QUALITY_GATE__
|
|
2555
|
+
|
|
2430
2556
|
- 写完文件后,最终回复只输出一行:`第 __I__ 章完成(约 __WORDS__ 字)`——不要在回复里复述或解释正文。"""
|
|
2431
2557
|
|
|
2432
2558
|
SERIAL_REVISE_PROMPT = """你是一名网文作者。第 __I__ 章没有通过评审,请修订文件 `__FILE__`(直接改写该文件)。文件必须以 UTF-8 编码保存(PowerShell 显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
@@ -2436,11 +2562,25 @@ __GOAL__
|
|
|
2436
2562
|
|
|
2437
2563
|
## 本卷上下文
|
|
2438
2564
|
__VOLUME__
|
|
2565
|
+
## 本章按大纲应完成
|
|
2566
|
+
__BEATS__
|
|
2567
|
+
(本章爽点/情绪爆点:__HIGHLIGHT__)
|
|
2568
|
+
|
|
2439
2569
|
## 本章评审意见
|
|
2440
2570
|
__CRITIQUE__
|
|
2441
2571
|
|
|
2572
|
+
## 全书文风锚(修订后必须仍符合本锚,与前文各章同一笔调)
|
|
2573
|
+
__STYLE__
|
|
2574
|
+
|
|
2575
|
+
## 签约返修原则
|
|
2576
|
+
- 统一叙述语域、句式和视角距离,不在白描、半文半白和网络梗之间跳变。
|
|
2577
|
+
- 复杂情感必须落到动作、五感、生理反应和对话潜台词,禁止只写“很伤心/很愤怒”。
|
|
2578
|
+
- 爽点/情绪爆点按特写镜头写厚:铺垫在前、放大在中、余波在后,禁止一句话带过。
|
|
2579
|
+
- 删除不改变局面的环境、解释和重复叙述;每段至少完成冲突、信息或人物选择之一。
|
|
2580
|
+
- 让主角主动做出选择并推动下一步,补足因果桥,不用旁白硬推剧情。
|
|
2581
|
+
|
|
2442
2582
|
## 要求
|
|
2443
|
-
- 针对性解决所有 major
|
|
2583
|
+
- 针对性解决所有 major 问题(含确定性文本信号指出的问题),保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
|
|
2444
2584
|
|
|
2445
2585
|
SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全书各章已完稿,请从**全书整体**视角评审。
|
|
2446
2586
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
@@ -2449,7 +2589,8 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
|
|
|
2449
2589
|
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2450
2590
|
"summary": "一句话总评:是否达到可签约水平"
|
|
2451
2591
|
}
|
|
2452
|
-
每个维度打 1-10
|
|
2592
|
+
每个维度打 1-10 分,宁严勿宽。重点关注:主线一致性、人物弧光、开篇吸引力、文风统一、情感细腻度、情节推进与节奏控制。
|
|
2593
|
+
若是前 3 章,必须核对黄金一章/黄金三章的切入速度;若发现拖沓、硬推或情绪概括,给出可定位的 major 问题与原文引文。
|
|
2453
2594
|
major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2454
2595
|
|
|
2455
2596
|
## 全书目标
|
|
@@ -2460,6 +2601,25 @@ __GOAL__
|
|
|
2460
2601
|
__MANUSCRIPT__
|
|
2461
2602
|
---"""
|
|
2462
2603
|
|
|
2604
|
+
# 独立签约评估(开篇闸门 + 2万字检查点共用):评分维度保持作者任务口径,
|
|
2605
|
+
# 平台五维在这里按平台真实尺度单独核——驳回模板的高频话术逐条要判定。
|
|
2606
|
+
SERIAL_SIGNING_EVAL_PROMPT = """你是签约评估编辑(不要修改任何文件)。请按免费阅读平台(番茄/七猫)签约评估的口径评审下面的稿件__NOTE__。
|
|
2607
|
+
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2608
|
+
{
|
|
2609
|
+
"scores": {"__DIMKEYS__"},
|
|
2610
|
+
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2611
|
+
"summary": "一句话总评:是否达到可申请签约评估的水平"
|
|
2612
|
+
}
|
|
2613
|
+
每个维度打 1-10 分(可为小数),按平台真实尺度从严。五个维度固定为:开篇吸引力、情节推进、文风统一、情感细腻度、节奏控制。
|
|
2614
|
+
驳回模板的高频话术——文风在风格上不统一(语域/句式跳变)、
|
|
2615
|
+
复杂情感描绘不够细腻(情绪概括无动作细节)、开篇切入点缺乏足够的吸引力(黄金一章未兑现)、情节的推动生硬(因果桥缺失/旁白硬推)、
|
|
2616
|
+
行文拖沓节奏缓慢(重复铺垫、与主线无关的描写)——每一条都要给出明确判定与证据。
|
|
2617
|
+
|
|
2618
|
+
## 待评审稿件
|
|
2619
|
+
---
|
|
2620
|
+
__MANUSCRIPT__
|
|
2621
|
+
---"""
|
|
2622
|
+
|
|
2463
2623
|
|
|
2464
2624
|
def _read_text_any_enc(p):
|
|
2465
2625
|
"""工作区文件读文本:委托 runner.read_text_any_enc(UTF-8 → GBK → replace)。
|
|
@@ -2689,7 +2849,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2689
2849
|
serial = task.get("serial") or {}
|
|
2690
2850
|
n = int(serial.get("chapters") or 8)
|
|
2691
2851
|
wpc = int(serial.get("words_per_chapter") or 2500)
|
|
2852
|
+
# 保留既有任务/流程的评分维度,避免历史任务和外部评审桩因维度漂移失效;
|
|
2853
|
+
# 签约专项维度作为额外硬性检查注入提示词与报告证据。
|
|
2692
2854
|
dims = task.get("rubric") or DEFAULT_RUBRIC
|
|
2855
|
+
signing_dims = novel_quality.rubric_for({"type": "serial_novel"})
|
|
2693
2856
|
threshold = task.get("threshold", 7.0)
|
|
2694
2857
|
threshold_ch = threshold - 0.5 if threshold >= 7.5 else threshold # 单章阈值略放宽 0.5 分
|
|
2695
2858
|
dimkey = ", ".join('"%s": 0' % d for d in dims)
|
|
@@ -2711,12 +2874,16 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2711
2874
|
bible = task["context"] + ("\n\n" + bible if bible else "")
|
|
2712
2875
|
|
|
2713
2876
|
|
|
2714
|
-
def crit_prompt_for(text, note="", event_check=""):
|
|
2877
|
+
def crit_prompt_for(text, note="", event_check="", chapter=None):
|
|
2715
2878
|
tpl = _ensure_critique_placeholders(
|
|
2716
2879
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
2717
2880
|
if note:
|
|
2718
2881
|
tpl = tpl.replace("你是严格的评审",
|
|
2719
2882
|
"你是严格的评审(背景:%s,请结合全书目标评审本章节)" % note, 1)
|
|
2883
|
+
tpl = tpl.replace("## 待评审稿件",
|
|
2884
|
+
"## 签约专项检查维度\n" + "、".join(signing_dims) +
|
|
2885
|
+
"\n请在 issues 中指出这些专项问题的具体证据;它们是评分解释层,"
|
|
2886
|
+
"不替换用户已有评分维度。\n\n## 待评审稿件", 1)
|
|
2720
2887
|
if event_check:
|
|
2721
2888
|
# 逐项目标审稿(借鉴 AI-Novel-Writer v1.1):本章大纲要点逐项核对
|
|
2722
2889
|
tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % event_check, 1)
|
|
@@ -2732,6 +2899,23 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2732
2899
|
# 确定性检测(AI 味/叙事架构/节奏)此前只挂在单稿件评审上,连载逐章
|
|
2733
2900
|
# 从未拿到——逐章节奏与钩子恰恰最需要这条参考线(命中才追加,不扣分)
|
|
2734
2901
|
tpl = aiflavor.inject_into_prompt(tpl, text, task.get("type"))
|
|
2902
|
+
signal = novel_quality.signal_summary(text, chapter=chapter)
|
|
2903
|
+
signal_block = novel_quality.format_signal_summary(signal)
|
|
2904
|
+
if signal_block:
|
|
2905
|
+
tpl = tpl.replace("## 待评审稿件", "## 确定性文本信号\n" + signal_block + "\n\n## 待评审稿件", 1)
|
|
2906
|
+
if style_block:
|
|
2907
|
+
tpl = tpl.replace("## 待评审稿件", style_block + "\n## 待评审稿件", 1)
|
|
2908
|
+
# 跨章风格指纹:本章与此前各章的确定性指标(句长/对白占比/情绪动作密度)
|
|
2909
|
+
# 中位基线比对,漂移超阈时点名——「文风统一」不靠评审模型自觉
|
|
2910
|
+
base_sigs = [cs.get("quality_signals") for cs in chapter_scores
|
|
2911
|
+
if isinstance(cs, dict) and isinstance(cs.get("quality_signals"), dict)]
|
|
2912
|
+
if base_sigs:
|
|
2913
|
+
drift = novel_quality.style_deviation_notes(base_sigs, signal)
|
|
2914
|
+
if drift:
|
|
2915
|
+
tpl = tpl.replace(
|
|
2916
|
+
"## 待评审稿件",
|
|
2917
|
+
"## 风格指纹核查(确定性证据,请计入文风统一)\n- "
|
|
2918
|
+
+ "\n- ".join(drift) + "\n\n## 待评审稿件", 1)
|
|
2735
2919
|
return tpl.replace("__DIMKEYS__", dimkey).replace(
|
|
2736
2920
|
"__MANUSCRIPT__", text or "(稿件为空!)")
|
|
2737
2921
|
|
|
@@ -2786,13 +2970,30 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2786
2970
|
outline.get("source", "?"),
|
|
2787
2971
|
outline.get("book_title") or task["title"], n),
|
|
2788
2972
|
duration_s=0.1 if impl.get("mode") == "mock" else None)
|
|
2973
|
+
# 全书文风锚:续写批次模型没回锚点时回查前批大纲兜底,再渲染成注入块。
|
|
2974
|
+
# 起草/修订模板用占位版本(永远有内容),评审面只在真有锚时注入。
|
|
2975
|
+
if not outline.get("style_anchor") and start > 1:
|
|
2976
|
+
try:
|
|
2977
|
+
_prev_anchor = planner.prev_style_anchor(task)
|
|
2978
|
+
if _prev_anchor:
|
|
2979
|
+
outline["style_anchor"] = _prev_anchor
|
|
2980
|
+
except Exception:
|
|
2981
|
+
pass
|
|
2789
2982
|
store.update_run(run_id, outline=outline)
|
|
2790
2983
|
_check_cancel(ev)
|
|
2791
2984
|
end = start + n - 1 # 本批最后一章的全书章号(全局评审与合并成书覆盖 1..end)
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2985
|
+
|
|
2986
|
+
def render_outline_txt():
|
|
2987
|
+
return "\n".join(
|
|
2988
|
+
"第 %d 章《%s》:%s%s%s" % (start + k, c["title"], c["beats"],
|
|
2989
|
+
("(章末钩子:%s)" % c.get("hook")) if c.get("hook") else "",
|
|
2990
|
+
("(爆点:%s)" % c["highlight"]) if c.get("highlight") else "")
|
|
2991
|
+
for k, c in enumerate(outline["chapters"]))
|
|
2992
|
+
|
|
2993
|
+
outline_txt = render_outline_txt()
|
|
2994
|
+
style_block = novel_quality.style_anchor_block(outline.get("style_anchor"))
|
|
2995
|
+
style_tpl = style_block or ("(大纲未指定文风锚:延续前文章节的叙述视角、句长与语域,"
|
|
2996
|
+
"全批保持同一笔调)\n")
|
|
2796
2997
|
|
|
2797
2998
|
# 分卷规划表(本书级、由章号确定性推导):覆盖到本批末章即可。写成
|
|
2798
2999
|
# 全书顶层字段,UI/报告/发布页直接读,不必重算。
|
|
@@ -2825,6 +3026,56 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2825
3026
|
chapter_scores = [] # [{chapter,title,means,passed,rounds,words}]
|
|
2826
3027
|
issues_all = []
|
|
2827
3028
|
|
|
3029
|
+
# ---- 独立签约评估设施(开篇闸门 + 2万字检查点共用)----
|
|
3030
|
+
def _signing_eval(upto_chapter, note):
|
|
3031
|
+
"""按平台五维对截至 upto_chapter 的稿件做一次签约评估。
|
|
3032
|
+
与任务 rubric 解耦(评分维度保持作者口径,签约五维是解释层/平台口径)。
|
|
3033
|
+
返回 (means, issues, summary);评估失败返回 (None, [], "")——调用方
|
|
3034
|
+
按「未评估」处理,绝不得当 0 分。issues 同时并入 issues_all 供打磨用。"""
|
|
3035
|
+
texts, acc = [], 0
|
|
3036
|
+
for j in range(start, upto_chapter + 1):
|
|
3037
|
+
t = _read_chapter(workdir, j)
|
|
3038
|
+
if not t:
|
|
3039
|
+
continue
|
|
3040
|
+
texts.append("### 第 %d 章\n\n%s" % (j, t.strip()))
|
|
3041
|
+
acc += _wc(t)
|
|
3042
|
+
if acc >= novel_quality.SIGNING_CHECKPOINT_WORDS * 1.2:
|
|
3043
|
+
break
|
|
3044
|
+
if not texts:
|
|
3045
|
+
return None, [], ""
|
|
3046
|
+
signing_dimkey = ", ".join('"%s": 0' % d for d in novel_quality.SIGNING_RUBRIC)
|
|
3047
|
+
prompt = (SERIAL_SIGNING_EVAL_PROMPT
|
|
3048
|
+
.replace("__DIMKEYS__", signing_dimkey)
|
|
3049
|
+
.replace("__NOTE__", note or "")
|
|
3050
|
+
.replace("__MANUSCRIPT__", "\n\n".join(texts)[:26000]))
|
|
3051
|
+
agent = critics[0] if critics else impl
|
|
3052
|
+
res = _run_step(run_id, "signing-eval", modelhub.bind_agent(agent, difficulty),
|
|
3053
|
+
prompt, workdir, readonly=True, ev=ev, timeout=2400,
|
|
3054
|
+
note=("签约评估 %s" % (note or "")).strip())
|
|
3055
|
+
cj = _critique_json(res, novel_quality.SIGNING_RUBRIC)
|
|
3056
|
+
scores = cj.get("scores") or {}
|
|
3057
|
+
means = {}
|
|
3058
|
+
for d in novel_quality.SIGNING_RUBRIC:
|
|
3059
|
+
try:
|
|
3060
|
+
means[d] = float(scores[d])
|
|
3061
|
+
except (KeyError, TypeError, ValueError):
|
|
3062
|
+
pass
|
|
3063
|
+
if not means:
|
|
3064
|
+
return None, [], ""
|
|
3065
|
+
issues = (cj.get("issues") or [])[:6]
|
|
3066
|
+
issues_all.extend({"chapter": upto_chapter, **it} for it in issues)
|
|
3067
|
+
return means, issues, str(cj.get("summary") or "")
|
|
3068
|
+
|
|
3069
|
+
def _signing_record(kind, payload):
|
|
3070
|
+
"""签约评估结果入 run(signing_evals.<kind>),审计与 UI 证据。"""
|
|
3071
|
+
try:
|
|
3072
|
+
latest = store.get_run(run_id) or {}
|
|
3073
|
+
evals = dict(latest.get("signing_evals") or {})
|
|
3074
|
+
evals[kind] = payload
|
|
3075
|
+
store.update_run(run_id, signing_evals=evals)
|
|
3076
|
+
except Exception:
|
|
3077
|
+
pass
|
|
3078
|
+
|
|
2828
3079
|
# ---- 2) 逐章
|
|
2829
3080
|
for k in range(1, n + 1):
|
|
2830
3081
|
i = start + k - 1 # 全书章号:文件名/步骤角色/评分记录都按全书编号
|
|
@@ -2847,17 +3098,22 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2847
3098
|
# 逐项目标审稿(借鉴 AI-Novel-Writer):本章大纲要点随评审下发,评审
|
|
2848
3099
|
# 以 <event_check> 回逐项判定;资源账本(借鉴角色资源账本)以 <ledger>
|
|
2849
3100
|
# 回本章道具/伤情/承诺/伏笔增量,评审达标后追加账本文件供下章注入。
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
3101
|
+
def build_event_block(ch_ent):
|
|
3102
|
+
return (
|
|
3103
|
+
"## 本章大纲核对(逐项目标审稿)\n"
|
|
3104
|
+
"本章按大纲应完成:\n- 剧情要点:%s\n- 章末钩子:%s\n- 爽点/情绪爆点:%s\n"
|
|
3105
|
+
"评审时逐项判定「已完成 / 未完成 / 待核实」,判定必须引用正文证据"
|
|
3106
|
+
"(原文短句或位置),写在回复末尾的 <event_check> 块内(每项一行)。"
|
|
3107
|
+
"「铺垫了但没发生」不算已完成;爆点一句带过(未按特写写厚)不算已完成;"
|
|
3108
|
+
"未完成的项必须反映到对应维度评分。\n"
|
|
3109
|
+
"另在 <event_check> 块之后输出 <ledger> 块(没有新变化就整个省略):"
|
|
3110
|
+
"逐行列出本章新出现或状态变化的 道具/伤情/承诺/伏笔,格式:"
|
|
3111
|
+
"类型|名称|现状(一句话)。\n%s\n"
|
|
3112
|
+
% (ch_ent.get("beats") or "按大纲推进", ch_ent.get("hook") or "留下悬念",
|
|
3113
|
+
ch_ent.get("highlight") or "(大纲未指定,按剧情要点核对是否有一处小冲突/小反转写厚)",
|
|
3114
|
+
vol_review_block))
|
|
3115
|
+
|
|
3116
|
+
event_block = build_event_block(ch)
|
|
2861
3117
|
ev_check_lines = [[]] # 每章重置:第一份非空评审的逐项判定
|
|
2862
3118
|
ledger_lines = [] # 本章全部评审的账本增量并集
|
|
2863
3119
|
ledger_txt = _read_ledger(workdir) # 截至上一章的资源账本(起草注入)
|
|
@@ -2955,7 +3211,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2955
3211
|
"|你的专属评审视角:%s(其他评审会覆盖其余视角,"
|
|
2956
3212
|
"请深挖你的镜头,但所有维度仍需打分)" % lens)
|
|
2957
3213
|
if lens else "",
|
|
2958
|
-
event_check=event_check) + note_extra,
|
|
3214
|
+
event_check=event_check, chapter=i) + note_extra,
|
|
2959
3215
|
workdir, readonly=True, ev=ev,
|
|
2960
3216
|
resume=sids.get(agent["id"]))
|
|
2961
3217
|
results[idx] = {
|
|
@@ -3007,7 +3263,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3007
3263
|
crit_prompt_for(
|
|
3008
3264
|
text,
|
|
3009
3265
|
note="小说第 %d 章" % i,
|
|
3010
|
-
event_check=event_check) + note_extra,
|
|
3266
|
+
event_check=event_check, chapter=i) + note_extra,
|
|
3011
3267
|
workdir, readonly=True, ev=ev)
|
|
3012
3268
|
cj = runner.extract_json(res.get("text") or "")
|
|
3013
3269
|
if isinstance(cj, dict) and isinstance(cj.get("scores"), dict) \
|
|
@@ -3098,6 +3354,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3098
3354
|
.replace("__TITLE__", ch["title"])
|
|
3099
3355
|
.replace("__BEATS__", branch_beats or ch["beats"] or "按大纲推进")
|
|
3100
3356
|
.replace("__HOOK__", branch_hook or ch.get("hook") or "留下悬念")
|
|
3357
|
+
.replace("__HIGHLIGHT__", ch.get("highlight") or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3358
|
+
.replace("__STYLE__", style_tpl)
|
|
3359
|
+
.replace("__QUALITY_GATE__", novel_quality.opening_requirements(i, wpc))
|
|
3101
3360
|
.replace("__WORDS__", str(wpc)))
|
|
3102
3361
|
|
|
3103
3362
|
n_variants = max(1, min(3, int(serial.get("variants") or 1)))
|
|
@@ -3383,6 +3642,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3383
3642
|
store.update_run(run_id, chapter_scores=chapter_scores)
|
|
3384
3643
|
continue
|
|
3385
3644
|
|
|
3645
|
+
# 开篇闸门(仅全书第 1 章、真实作者、可经 serial.opening_gate=false 关闭):
|
|
3646
|
+
# 平台口径「开篇失败=全书判死」,第 1 章必须按签约评估单独核开篇吸引力
|
|
3647
|
+
gate_live = (start == 1 and i == 1 and impl.get("mode") == "real"
|
|
3648
|
+
and bool(serial.get("opening_gate", True)))
|
|
3649
|
+
gate_note = ""
|
|
3650
|
+
gate_opening_ok = None # rnd1 闸门评估是否过线(None=未评估)
|
|
3386
3651
|
for rnd in (1, 2):
|
|
3387
3652
|
text = _read_chapter(workdir, i)
|
|
3388
3653
|
if rnd == 1 and race_cj is not None:
|
|
@@ -3400,6 +3665,36 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3400
3665
|
return
|
|
3401
3666
|
means = means_of(cj_by_agent)
|
|
3402
3667
|
passed = bool(means) and all(v >= threshold_ch for v in means.values())
|
|
3668
|
+
if rnd == 1 and gate_live:
|
|
3669
|
+
s_means, s_issues, s_summary = _signing_eval(
|
|
3670
|
+
i, "(开篇闸门:核第 1 章能否抓住读者)")
|
|
3671
|
+
if s_means is not None:
|
|
3672
|
+
gate_opening_ok = not novel_quality.opening_below(
|
|
3673
|
+
s_means, threshold_ch)
|
|
3674
|
+
_signing_record("opening_gate", {
|
|
3675
|
+
"chapter": i, "means": s_means,
|
|
3676
|
+
"summary": (s_summary or "")[:200],
|
|
3677
|
+
"opening_below_line": not gate_opening_ok})
|
|
3678
|
+
if s_means is not None and not gate_opening_ok:
|
|
3679
|
+
# 平台口径未过线:回大纲层重设计前三章节拍,再按新节拍重写,
|
|
3680
|
+
# 而不是只修文案——切入点不行是结构病(重设计失败回退普通修订)
|
|
3681
|
+
passed = False
|
|
3682
|
+
critique_txt = ";".join(
|
|
3683
|
+
"%s %.1f(签约线 %.1f)" % (d, s_means.get(d, 0.0), threshold_ch)
|
|
3684
|
+
for d in novel_quality.SIGNING_RUBRIC)
|
|
3685
|
+
if s_issues:
|
|
3686
|
+
critique_txt += ";意见:" + ";".join(
|
|
3687
|
+
str(it.get("note") or "")[:80] for it in s_issues[:3])
|
|
3688
|
+
rebuilt = _planner_call(
|
|
3689
|
+
planner.redesign_opening, task, outline=outline,
|
|
3690
|
+
critique=critique_txt, author_agent=impl, workdir=workdir,
|
|
3691
|
+
ev=ev, deadline=_run_deadline(run_id), run_id=run_id)
|
|
3692
|
+
if rebuilt is not None:
|
|
3693
|
+
ch = outline["chapters"][k - 1] # 合并是原位改写,刷新引用
|
|
3694
|
+
event_block = build_event_block(ch) # 逐项核对按新节拍重建
|
|
3695
|
+
outline_txt = render_outline_txt() # 后续章节看到新节拍
|
|
3696
|
+
gate_note = ("- [开篇闸门] 前三章节拍已按签约评估重新设计"
|
|
3697
|
+
"(见「本章按大纲应完成」),请按新节拍对本章做结构性重写,不是小修。")
|
|
3403
3698
|
if passed or rnd == 2:
|
|
3404
3699
|
break
|
|
3405
3700
|
# 第 1 轮不达标 → 修订该章后重评审
|
|
@@ -3409,8 +3704,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3409
3704
|
majors, _unanchored = _anchor_issues(majors, text)
|
|
3410
3705
|
crit_lines = ["- %s:%.1f(章阈值 %.1f)" % (d, means[d], threshold_ch) for d in dims]
|
|
3411
3706
|
crit_lines += _major_lines(majors)
|
|
3707
|
+
if gate_note:
|
|
3708
|
+
crit_lines.insert(0, gate_note)
|
|
3412
3709
|
if _unanchored:
|
|
3413
3710
|
crit_lines.append("- ⚠ %d 条 major 意见的引文未能在稿件中锚定,已降序殿后" % _unanchored)
|
|
3711
|
+
sig_now = novel_quality.signal_summary(text, chapter=i)
|
|
3712
|
+
if sig_now.get("hints"):
|
|
3713
|
+
crit_lines.append("- 确定性复查提示(修订须对应处理):"
|
|
3714
|
+
+ ";".join(sig_now["hints"]))
|
|
3414
3715
|
if rnd == 1 and race_losers and impl.get("mode") == "real":
|
|
3415
3716
|
# 赛马败者精华回收(连载版):一次选择器调用提炼落选稿优点,
|
|
3416
3717
|
# 与评审意见一并喂给首轮修订;失败静默不影响修订
|
|
@@ -3445,6 +3746,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3445
3746
|
.replace("__I__", str(i)).replace("__FILE__", ch_file)
|
|
3446
3747
|
.replace("__GOAL__", task["goal"])
|
|
3447
3748
|
.replace("__VOLUME__", vol_block_for(i))
|
|
3749
|
+
.replace("__BEATS__", ch.get("beats") or "按大纲推进")
|
|
3750
|
+
.replace("__HIGHLIGHT__", ch.get("highlight")
|
|
3751
|
+
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3752
|
+
.replace("__STYLE__", style_tpl)
|
|
3448
3753
|
.replace("__CRITIQUE__", "\n".join(crit_lines))
|
|
3449
3754
|
.replace("__WORDS__", str(wpc)))
|
|
3450
3755
|
prompt = attachments.append_task_context(prompt, task)
|
|
@@ -3462,10 +3767,30 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3462
3767
|
except Exception:
|
|
3463
3768
|
pass
|
|
3464
3769
|
_check_cancel(ev)
|
|
3770
|
+
if gate_live and not (gate_opening_ok and rounds_used == 1):
|
|
3771
|
+
# 闸门终判(rnd1 已过线且未修订时免复核):平台口径的开篇吸引力
|
|
3772
|
+
# 仍低于签约线 → 停批止损。写满 8 章才发现开篇不行太贵,这正是
|
|
3773
|
+
# 《戍边骑奴》被整本驳回的病根。
|
|
3774
|
+
s_means2, _si2, s_sum2 = _signing_eval(
|
|
3775
|
+
i, "(开篇闸门终判:重设计/修订后能否申请签约评估)")
|
|
3776
|
+
_signing_record("opening_gate_final", {
|
|
3777
|
+
"chapter": i, "means": s_means2 or {},
|
|
3778
|
+
"summary": (s_sum2 or "")[:200]})
|
|
3779
|
+
opening_final = (s_means2 or {}).get(novel_quality.OPENING_DIM)
|
|
3780
|
+
if opening_final is not None and float(opening_final) < threshold_ch:
|
|
3781
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3782
|
+
error="开篇闸门:第 1 章签约评估「开篇吸引力」%.1f 低于签约线 %.1f"
|
|
3783
|
+
"(已重设计节拍并修订仍不达标)。开篇失败=全书判死,已停批止损;"
|
|
3784
|
+
"稿件保留,可人工复核或调整目标后重开。"
|
|
3785
|
+
% (float(opening_final), threshold_ch),
|
|
3786
|
+
ended_at=_now())
|
|
3787
|
+
return
|
|
3465
3788
|
cs_new = {"chapter": i, "title": ch["title"], "means": means,
|
|
3466
3789
|
"passed": bool(means) and all(v >= threshold_ch for v in means.values()),
|
|
3467
3790
|
"rounds": rounds_used,
|
|
3468
|
-
"words": _wc(_read_chapter(workdir, i))
|
|
3791
|
+
"words": _wc(_read_chapter(workdir, i)),
|
|
3792
|
+
"quality_signals": novel_quality.signal_summary(
|
|
3793
|
+
_read_chapter(workdir, i), chapter=i)}
|
|
3469
3794
|
_vent = volumes.find(vol_plan, i) if vol_plan else None
|
|
3470
3795
|
if _vent:
|
|
3471
3796
|
cs_new["vol"] = _vent["vol"] # 章节卡按卷分组用
|
|
@@ -3497,10 +3822,70 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3497
3822
|
f.write("- 第%d章《%s》:%s(%d 字,%s)\n" % (
|
|
3498
3823
|
i, ch["title"],
|
|
3499
3824
|
ch.get("beats") or "按大纲推进", chapter_scores[-1]["words"],
|
|
3500
|
-
"%.1f 分" % means.get("情节", 0.0)
|
|
3825
|
+
"%.1f 分" % (means.get("情节", means.get("情节推进", 0.0)))
|
|
3826
|
+
if means else "无评分"))
|
|
3501
3827
|
except Exception:
|
|
3502
3828
|
pass
|
|
3503
3829
|
|
|
3830
|
+
# ---- 2万字签约预评估检查点(平台口径:前 2 万字定签约)。累计字数
|
|
3831
|
+
# 过线时做一次独立签约评估;未达线先重改最弱章一轮再复核一次。
|
|
3832
|
+
# 不中止批次——终局全局评审仍是发布门,这里把问题提前暴露并给一次
|
|
3833
|
+
# 修复机会(全书评审通过记录 signing_checkpoint,只触发一次)。
|
|
3834
|
+
if impl.get("mode") == "real":
|
|
3835
|
+
total_words = sum(int(c.get("words") or 0) for c in chapter_scores)
|
|
3836
|
+
latest_cp = (store.get_run(run_id) or {}).get("signing_checkpoint")
|
|
3837
|
+
if total_words >= novel_quality.SIGNING_CHECKPOINT_WORDS and not latest_cp:
|
|
3838
|
+
cp_means, _cpi, cp_summary = _signing_eval(
|
|
3839
|
+
i, "(2万字签约预评估检查点)")
|
|
3840
|
+
cp = {"at_words": total_words, "chapter": i,
|
|
3841
|
+
"means": cp_means or {},
|
|
3842
|
+
"passed": bool(cp_means) and all(
|
|
3843
|
+
v >= threshold for v in cp_means.values()) if cp_means else None,
|
|
3844
|
+
"summary": (cp_summary or "")[:200]}
|
|
3845
|
+
if cp["passed"] is False:
|
|
3846
|
+
# 未达线:重改最弱章一轮(按签约评估短板),再复核一次
|
|
3847
|
+
weak = _weakest_chapters(chapter_scores, cp_means, threshold, limit=1)
|
|
3848
|
+
short = ";".join(
|
|
3849
|
+
"%s %.1f(签约线 %.1f)" % (d, v, threshold)
|
|
3850
|
+
for d, v in (cp_means or {}).items() if v < threshold)
|
|
3851
|
+
for c in weak:
|
|
3852
|
+
j = c["chapter"]
|
|
3853
|
+
_wch = outline["chapters"][j - start] if start <= j <= end \
|
|
3854
|
+
else ch
|
|
3855
|
+
crit = ("- 2万字签约预评估未达线:%s\n- 全文短板:%s\n"
|
|
3856
|
+
"- 重改要求:优先补签约短板(风格统一/情感特写/节奏删拖沓),"
|
|
3857
|
+
"不得改动剧情主线关键事实。"
|
|
3858
|
+
% (short or "(各维度均未明确出分)", short or "(无)"))
|
|
3859
|
+
res = _run_step(
|
|
3860
|
+
run_id, "polish-cp%d" % j,
|
|
3861
|
+
modelhub.bind_agent(impl, difficulty),
|
|
3862
|
+
(SERIAL_REVISE_PROMPT
|
|
3863
|
+
.replace("__I__", str(j))
|
|
3864
|
+
.replace("__FILE__", "chapter-%02d.md" % j)
|
|
3865
|
+
.replace("__GOAL__", task["goal"])
|
|
3866
|
+
.replace("__VOLUME__", vol_block_for(j))
|
|
3867
|
+
.replace("__BEATS__", _wch.get("beats") or "按大纲推进")
|
|
3868
|
+
.replace("__HIGHLIGHT__", _wch.get("highlight")
|
|
3869
|
+
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3870
|
+
.replace("__STYLE__", style_tpl)
|
|
3871
|
+
.replace("__CRITIQUE__", crit)
|
|
3872
|
+
.replace("__WORDS__", str(wpc))),
|
|
3873
|
+
workdir, readonly=False, ev=ev, timeout=2400,
|
|
3874
|
+
note="2万字检查点:签约预评估未达线,重改最弱章")
|
|
3875
|
+
if not res["ok"]:
|
|
3876
|
+
continue
|
|
3877
|
+
try:
|
|
3878
|
+
branching.mark_stale(workdir, j)
|
|
3879
|
+
except Exception:
|
|
3880
|
+
pass
|
|
3881
|
+
cp_means2, _cpi2, cp_sum2 = _signing_eval(
|
|
3882
|
+
i, "(2万字检查点复核)")
|
|
3883
|
+
if cp_means2:
|
|
3884
|
+
cp["means"] = cp_means2
|
|
3885
|
+
cp["summary"] = (cp_sum2 or "")[:200]
|
|
3886
|
+
cp["passed"] = all(v >= threshold for v in cp_means2.values())
|
|
3887
|
+
store.update_run(run_id, signing_checkpoint=cp)
|
|
3888
|
+
|
|
3504
3889
|
# ---- 3) 全局一致性评审(覆盖 1..end 全书:续写批次必须连同旧章一起查一致性)
|
|
3505
3890
|
full_text = "\n\n".join(_read_chapter(workdir, i) for i in range(1, end + 1))
|
|
3506
3891
|
global_issues = []
|
|
@@ -3622,6 +4007,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3622
4007
|
"\n- 重改要求:优先修全书节奏/衔接问题(章间过渡、信息倾泻、主角主动性),"
|
|
3623
4008
|
"再补本章短板;不得改动既有剧情主线的关键事实。"
|
|
3624
4009
|
% (dims_txt or "(无)", threshold, gj or "(无)"))
|
|
4010
|
+
_psig = novel_quality.signal_summary(_read_chapter(workdir, i), chapter=i)
|
|
4011
|
+
if _psig.get("hints"):
|
|
4012
|
+
crit += ("\n- 确定性复查提示(修订须对应处理):"
|
|
4013
|
+
+ ";".join(_psig["hints"]))
|
|
3625
4014
|
if impl.get("mode") == "mock":
|
|
3626
4015
|
_write_chapter(workdir, i, mocks.draft_manuscript(
|
|
3627
4016
|
{"title": ch["title"], "goal": task["goal"]}, 3))
|
|
@@ -3649,7 +4038,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3649
4038
|
cj = mocks.critique(agent["id"], 3, dims, threshold_ch)
|
|
3650
4039
|
else:
|
|
3651
4040
|
res2 = _run_step(run_id, "critique-c%d" % i, modelhub.bind_agent(agent, difficulty),
|
|
3652
|
-
crit_prompt_for(_read_chapter(workdir, i), note="小说第 %d 章(打磨后)" % i),
|
|
4041
|
+
crit_prompt_for(_read_chapter(workdir, i), note="小说第 %d 章(打磨后)" % i, chapter=i),
|
|
3653
4042
|
workdir, readonly=True, ev=ev)
|
|
3654
4043
|
cj = runner.extract_json(res2.get("text") or "")
|
|
3655
4044
|
if not isinstance(cj, dict) or not isinstance(cj.get("scores"), dict):
|
|
@@ -3777,6 +4166,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3777
4166
|
+ " | ".join("%.1f" % c["means"].get(d, 0.0) for d in dims)
|
|
3778
4167
|
+ " | %.1f | %s | %d | %d |" % (mean, "✓" if c["passed"] else "✗",
|
|
3779
4168
|
c["rounds"], c["words"]))
|
|
4169
|
+
signal_hints = []
|
|
4170
|
+
for c in chapter_scores:
|
|
4171
|
+
for hint in (c.get("quality_signals") or {}).get("hints") or []:
|
|
4172
|
+
signal_hints.append("- 第 %s 章:%s" % (c.get("chapter"), hint))
|
|
4173
|
+
if signal_hints:
|
|
4174
|
+
lines += ["", "## 确定性文本信号复查", ""] + signal_hints[:20]
|
|
3780
4175
|
lines += ["", "## 全局评审(阈值 %.1f)" % threshold, ""]
|
|
3781
4176
|
lines += ["- %s:%.1f" % (d, global_means.get(d, 0.0)) for d in dims]
|
|
3782
4177
|
lines += ["", "## 主要问题", ""]
|
|
@@ -4022,6 +4417,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
4022
4417
|
if is_research:
|
|
4023
4418
|
# 调研报告追加证据链要求(gpt-researcher 借鉴)
|
|
4024
4419
|
p += RESEARCH_APPENDIX
|
|
4420
|
+
if task.get("type") == "translation":
|
|
4421
|
+
# 翻译追加术语表先行要求(andrewyng/translation-agent 借鉴)
|
|
4422
|
+
p += TRANSLATION_APPENDIX
|
|
4025
4423
|
if task.get("type") == "weekly_report":
|
|
4026
4424
|
# 禅道本周素材注入(Weekly Report Generator 借鉴:从工作系统
|
|
4027
4425
|
# 取数入素材)。尽力而为:未配置/不可达静默为空,绝不阻塞起草。
|