codebee 0.1.95 → 0.1.97

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +246 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. package/app/output/_probe_fq_watch.py +0 -62
@@ -1,199 +1,199 @@
1
- # -*- coding: utf-8 -*-
2
- """任务编译器:把用户任务和预置流程编译成统一、可审计的运行规格。"""
3
- from __future__ import annotations
4
-
5
- from collections.abc import Mapping
6
-
7
- from . import dispatch, flows
8
-
9
- SCHEMA_VERSION = 1
10
- DIFFICULTIES = ("easy", "default", "hard")
11
-
12
-
13
- def _difficulty(task, flow):
14
- explicit = str((task or {}).get("difficulty") or "").strip().lower()
15
- if explicit in DIFFICULTIES:
16
- return explicit, "用户指定"
17
- try:
18
- threshold = float((task or {}).get("threshold")
19
- if (task or {}).get("threshold") is not None
20
- else (flow or {}).get("threshold") or 7.0)
21
- except (TypeError, ValueError):
22
- threshold = 7.0
23
- if threshold >= 8.5:
24
- return "hard", "高质量门槛"
25
- if threshold <= 6.0:
26
- return "easy", "快速交付门槛"
27
- if str((task or {}).get("type") or "").lower() in (
28
- "research", "tech_proposal", "rank_scan"):
29
- return "hard", "研究/方案类默认需要深度"
30
- return "default", "标准难度"
31
-
32
-
33
- def _capabilities(task, dimension, engine):
34
- caps = [dimension]
35
- if engine == "code" or (task or {}).get("verify_command"):
36
- caps.extend(["filesystem", "verification"])
37
- if (task or {}).get("serial"):
38
- caps.extend(["long_context", "continuity"])
39
- if (task or {}).get("attachments"):
40
- caps.append("attachments")
41
- return list(dict.fromkeys(caps))
42
-
43
-
44
- def compile_task(task):
45
- """返回统一任务规格;输入缺失或字段异常时保持可编排的安全兜底。"""
46
- raw = dict(task) if isinstance(task, Mapping) else {}
47
- ttype = str(raw.get("type") or "direct").strip().lower()
48
- flow = flows.get_flow(ttype) or {}
49
- engine = str(raw.get("engine") or flow.get("engine") or
50
- ("code" if ttype == "code" else "review")).strip()
51
- if engine not in flows.ENGINES:
52
- engine = flow.get("engine") or ("code" if ttype == "code" else "review")
53
- difficulty, difficulty_reason = _difficulty(raw, flow)
54
- dimension = dispatch.task_dimension(ttype, raw.get("role") or "")
55
- rubric = raw.get("rubric") or flow.get("rubric") or []
56
- if isinstance(rubric, str):
57
- rubric = [x.strip() for x in rubric.replace(",", ",").split(",") if x.strip()]
58
- elif not isinstance(rubric, (list, tuple)):
59
- rubric = flow.get("rubric") or []
60
- rubric = [str(x).strip() for x in rubric if str(x).strip()][:8]
61
- deliverable = str(raw.get("manuscript") or flow.get("manuscript") or "").strip()
62
- serial = raw.get("serial") if isinstance(raw.get("serial"), dict) else None
63
- return {
64
- "schema_version": SCHEMA_VERSION,
65
- "type": ttype,
66
- "title": str(raw.get("title") or "")[:120],
67
- "goal": str(raw.get("goal") or "")[:2000],
68
- "engine": engine,
69
- "dimension": dimension,
70
- "difficulty": difficulty,
71
- "difficulty_reason": difficulty_reason,
72
- "thinking": (str(raw.get("thinking") or "standard").lower()
73
- if str(raw.get("thinking") or "standard").lower()
74
- in ("auto", "low", "standard", "high") else "auto"),
75
- "capabilities": _capabilities(raw, dimension, engine),
76
- "deliverable": deliverable,
77
- "quality_dimensions": rubric,
78
- "serial": serial,
79
- "explicit_agent": str(raw.get("implementer") or "").strip(),
80
- "has_verification": bool(raw.get("verify_command")),
81
- }
82
-
83
-
84
- def summary(spec):
85
- """给运行日志/UI 的短说明,不包含用户正文。"""
86
- spec = spec or {}
87
- return "%s/%s/%s(能力:%s)" % (
88
- spec.get("type") or "direct", spec.get("engine") or "direct",
89
- spec.get("difficulty") or "default",
90
- "、".join(spec.get("capabilities") or []) or "通用")
91
-
92
-
93
- def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
94
- """把难度、验收能力和规划建议收敛成代码任务的实际步骤策略。
95
-
96
- 自动模式允许低风险任务走短链;手动模式与高风险任务保留完整质量门禁。
97
- 规划器只能在安全边界内减少步骤,不能让无确定性验证的任务跳过评审。
98
- """
99
- task = task if isinstance(task, Mapping) else {}
100
- rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
101
- rec = rec if isinstance(rec, Mapping) else {}
102
- has_verify = bool(str(task.get("verify_command") or "").strip())
103
- risk_text = " ".join(str(task.get(k) or "") for k in ("title", "goal", "context")).lower()
104
- high_risk = bool(any(k in risk_text for k in (
105
- "安全", "权限", "鉴权", "认证", "加密", "密钥", "注入", "迁移", "数据库",
106
- "并发", "竞态", "支付", "架构", "公共接口", "public api", "公共 api",
107
- "breaking change", "security", "permission", "auth", "migration",
108
- "concurrency", "architecture")))
109
- fast_mode = mode == "fast"
110
- expert_mode = mode == "expert"
111
- easy_auto = (mode == "auto" and difficulty == "easy") or fast_mode
112
-
113
- review_required = True
114
- max_repairs = 2
115
- allow_switch = True
116
- reason = "复杂或手动任务使用完整质量门禁"
117
- if easy_auto:
118
- review_required = high_risk or not has_verify
119
- max_repairs = 1
120
- allow_switch = False
121
- reason = ("低风险任务以确定性验证代替模型评审"
122
- if has_verify else "低风险任务无验证命令,保留一次模型评审")
123
- if isinstance(rec.get("review_required"), bool):
124
- review_required = high_risk or rec["review_required"] or not has_verify
125
- try:
126
- if "max_repair_rounds" in rec:
127
- max_repairs = max(1 if not has_verify else 0,
128
- min(1, int(rec["max_repair_rounds"])))
129
- except (TypeError, ValueError):
130
- pass
131
- elif expert_mode or (mode == "auto" and difficulty == "hard"):
132
- review_required = True
133
- max_repairs = 2
134
- allow_switch = True
135
- try:
136
- if "max_repair_rounds" in rec:
137
- max_repairs = max(1, min(2, int(rec["max_repair_rounds"])))
138
- except (TypeError, ValueError):
139
- pass
140
- if isinstance(rec.get("allow_switch"), bool):
141
- allow_switch = rec["allow_switch"]
142
- reason = "复杂任务保留规划、验证与模型评审"
143
-
144
- return {
145
- "planning": "llm" if planned else "inline",
146
- "implementation_steps": len((plan or {}).get("steps") or []) or 1,
147
- "verification": has_verify,
148
- "review_required": review_required,
149
- "reviewers": 1 if review_required else 0,
150
- "max_repair_rounds": max_repairs,
151
- "allow_switch": allow_switch,
152
- "reason": reason,
153
- }
154
-
155
-
156
- def content_workflow(task, difficulty, plan=None, mode="auto"):
157
- """为非连载内容任务生成动态大纲/评审/修订策略。"""
158
- task = task if isinstance(task, Mapping) else {}
159
- ttype = str(task.get("type") or "doc")
160
- requested_rounds = max(1, min(5, int(task.get("rounds") or 2)))
161
- rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
162
- rec = rec if isinstance(rec, Mapping) else {}
163
- light_types = {"email", "weekly_report", "translation"}
164
- deep_types = {"novel", "research", "tech_proposal"}
165
-
166
- if mode == "fast":
167
- return {"outline": False, "reviewers": 1, "review_rounds": 1,
168
- "reason": "快速模式省略独立大纲,仅保留一次快速评审"}
169
- if mode == "expert":
170
- return {"outline": True, "reviewers": 2,
171
- "review_rounds": min(requested_rounds, 3),
172
- "reason": "专家模式启用完整大纲、多评审与修订门禁"}
173
- if mode != "auto":
174
- return {"outline": True, "reviewers": 0,
175
- "review_rounds": requested_rounds,
176
- "reason": "手动模式尊重用户指定的评审组与轮数"}
177
-
178
- if ttype in light_types and difficulty != "hard":
179
- return {"outline": False, "reviewers": 1, "review_rounds": 1,
180
- "reason": "轻量内容省略独立大纲,只做一次快速评审"}
181
-
182
- outline = ttype in deep_types or difficulty == "hard"
183
- reviewers = 2 if outline else 1
184
- review_rounds = min(requested_rounds, 2 if outline else 1)
185
- try:
186
- if "reviewers" in rec:
187
- reviewers = max(1, min(2, int(rec["reviewers"])))
188
- if "review_rounds" in rec:
189
- review_rounds = max(1, min(requested_rounds, 3,
190
- int(rec["review_rounds"])))
191
- except (TypeError, ValueError):
192
- pass
193
- # 深度类型至少保留一轮评审,但允许规划器把双评审降成一名;高门槛强制双评审。
194
- if float(task.get("threshold") or 7.0) >= 8.5:
195
- reviewers = 2
196
- return {"outline": outline, "reviewers": reviewers,
197
- "review_rounds": review_rounds,
198
- "reason": ("深度内容保留大纲和多轮质量门禁" if outline
199
- else "标准内容使用单评审短链")}
1
+ # -*- coding: utf-8 -*-
2
+ """任务编译器:把用户任务和预置流程编译成统一、可审计的运行规格。"""
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from . import dispatch, flows
8
+
9
+ SCHEMA_VERSION = 1
10
+ DIFFICULTIES = ("easy", "default", "hard")
11
+
12
+
13
+ def _difficulty(task, flow):
14
+ explicit = str((task or {}).get("difficulty") or "").strip().lower()
15
+ if explicit in DIFFICULTIES:
16
+ return explicit, "用户指定"
17
+ try:
18
+ threshold = float((task or {}).get("threshold")
19
+ if (task or {}).get("threshold") is not None
20
+ else (flow or {}).get("threshold") or 7.0)
21
+ except (TypeError, ValueError):
22
+ threshold = 7.0
23
+ if threshold >= 8.5:
24
+ return "hard", "高质量门槛"
25
+ if threshold <= 6.0:
26
+ return "easy", "快速交付门槛"
27
+ if str((task or {}).get("type") or "").lower() in (
28
+ "research", "tech_proposal", "rank_scan"):
29
+ return "hard", "研究/方案类默认需要深度"
30
+ return "default", "标准难度"
31
+
32
+
33
+ def _capabilities(task, dimension, engine):
34
+ caps = [dimension]
35
+ if engine == "code" or (task or {}).get("verify_command"):
36
+ caps.extend(["filesystem", "verification"])
37
+ if (task or {}).get("serial"):
38
+ caps.extend(["long_context", "continuity"])
39
+ if (task or {}).get("attachments"):
40
+ caps.append("attachments")
41
+ return list(dict.fromkeys(caps))
42
+
43
+
44
+ def compile_task(task):
45
+ """返回统一任务规格;输入缺失或字段异常时保持可编排的安全兜底。"""
46
+ raw = dict(task) if isinstance(task, Mapping) else {}
47
+ ttype = str(raw.get("type") or "direct").strip().lower()
48
+ flow = flows.get_flow(ttype) or {}
49
+ engine = str(raw.get("engine") or flow.get("engine") or
50
+ ("code" if ttype == "code" else "review")).strip()
51
+ if engine not in flows.ENGINES:
52
+ engine = flow.get("engine") or ("code" if ttype == "code" else "review")
53
+ difficulty, difficulty_reason = _difficulty(raw, flow)
54
+ dimension = dispatch.task_dimension(ttype, raw.get("role") or "")
55
+ rubric = raw.get("rubric") or flow.get("rubric") or []
56
+ if isinstance(rubric, str):
57
+ rubric = [x.strip() for x in rubric.replace(",", ",").split(",") if x.strip()]
58
+ elif not isinstance(rubric, (list, tuple)):
59
+ rubric = flow.get("rubric") or []
60
+ rubric = [str(x).strip() for x in rubric if str(x).strip()][:8]
61
+ deliverable = str(raw.get("manuscript") or flow.get("manuscript") or "").strip()
62
+ serial = raw.get("serial") if isinstance(raw.get("serial"), dict) else None
63
+ return {
64
+ "schema_version": SCHEMA_VERSION,
65
+ "type": ttype,
66
+ "title": str(raw.get("title") or "")[:120],
67
+ "goal": str(raw.get("goal") or "")[:2000],
68
+ "engine": engine,
69
+ "dimension": dimension,
70
+ "difficulty": difficulty,
71
+ "difficulty_reason": difficulty_reason,
72
+ "thinking": (str(raw.get("thinking") or "standard").lower()
73
+ if str(raw.get("thinking") or "standard").lower()
74
+ in ("auto", "low", "standard", "high") else "auto"),
75
+ "capabilities": _capabilities(raw, dimension, engine),
76
+ "deliverable": deliverable,
77
+ "quality_dimensions": rubric,
78
+ "serial": serial,
79
+ "explicit_agent": str(raw.get("implementer") or "").strip(),
80
+ "has_verification": bool(raw.get("verify_command")),
81
+ }
82
+
83
+
84
+ def summary(spec):
85
+ """给运行日志/UI 的短说明,不包含用户正文。"""
86
+ spec = spec or {}
87
+ return "%s/%s/%s(能力:%s)" % (
88
+ spec.get("type") or "direct", spec.get("engine") or "direct",
89
+ spec.get("difficulty") or "default",
90
+ "、".join(spec.get("capabilities") or []) or "通用")
91
+
92
+
93
+ def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
94
+ """把难度、验收能力和规划建议收敛成代码任务的实际步骤策略。
95
+
96
+ 自动模式允许低风险任务走短链;手动模式与高风险任务保留完整质量门禁。
97
+ 规划器只能在安全边界内减少步骤,不能让无确定性验证的任务跳过评审。
98
+ """
99
+ task = task if isinstance(task, Mapping) else {}
100
+ rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
101
+ rec = rec if isinstance(rec, Mapping) else {}
102
+ has_verify = bool(str(task.get("verify_command") or "").strip())
103
+ risk_text = " ".join(str(task.get(k) or "") for k in ("title", "goal", "context")).lower()
104
+ high_risk = bool(any(k in risk_text for k in (
105
+ "安全", "权限", "鉴权", "认证", "加密", "密钥", "注入", "迁移", "数据库",
106
+ "并发", "竞态", "支付", "架构", "公共接口", "public api", "公共 api",
107
+ "breaking change", "security", "permission", "auth", "migration",
108
+ "concurrency", "architecture")))
109
+ fast_mode = mode == "fast"
110
+ expert_mode = mode == "expert"
111
+ easy_auto = (mode == "auto" and difficulty == "easy") or fast_mode
112
+
113
+ review_required = True
114
+ max_repairs = 2
115
+ allow_switch = True
116
+ reason = "复杂或手动任务使用完整质量门禁"
117
+ if easy_auto:
118
+ review_required = high_risk or not has_verify
119
+ max_repairs = 1
120
+ allow_switch = False
121
+ reason = ("低风险任务以确定性验证代替模型评审"
122
+ if has_verify else "低风险任务无验证命令,保留一次模型评审")
123
+ if isinstance(rec.get("review_required"), bool):
124
+ review_required = high_risk or rec["review_required"] or not has_verify
125
+ try:
126
+ if "max_repair_rounds" in rec:
127
+ max_repairs = max(1 if not has_verify else 0,
128
+ min(1, int(rec["max_repair_rounds"])))
129
+ except (TypeError, ValueError):
130
+ pass
131
+ elif expert_mode or (mode == "auto" and difficulty == "hard"):
132
+ review_required = True
133
+ max_repairs = 2
134
+ allow_switch = True
135
+ try:
136
+ if "max_repair_rounds" in rec:
137
+ max_repairs = max(1, min(2, int(rec["max_repair_rounds"])))
138
+ except (TypeError, ValueError):
139
+ pass
140
+ if isinstance(rec.get("allow_switch"), bool):
141
+ allow_switch = rec["allow_switch"]
142
+ reason = "复杂任务保留规划、验证与模型评审"
143
+
144
+ return {
145
+ "planning": "llm" if planned else "inline",
146
+ "implementation_steps": len((plan or {}).get("steps") or []) or 1,
147
+ "verification": has_verify,
148
+ "review_required": review_required,
149
+ "reviewers": 1 if review_required else 0,
150
+ "max_repair_rounds": max_repairs,
151
+ "allow_switch": allow_switch,
152
+ "reason": reason,
153
+ }
154
+
155
+
156
+ def content_workflow(task, difficulty, plan=None, mode="auto"):
157
+ """为非连载内容任务生成动态大纲/评审/修订策略。"""
158
+ task = task if isinstance(task, Mapping) else {}
159
+ ttype = str(task.get("type") or "doc")
160
+ requested_rounds = max(1, min(5, int(task.get("rounds") or 2)))
161
+ rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
162
+ rec = rec if isinstance(rec, Mapping) else {}
163
+ light_types = {"email", "weekly_report", "translation"}
164
+ deep_types = {"novel", "research", "tech_proposal"}
165
+
166
+ if mode == "fast":
167
+ return {"outline": False, "reviewers": 1, "review_rounds": 1,
168
+ "reason": "快速模式省略独立大纲,仅保留一次快速评审"}
169
+ if mode == "expert":
170
+ return {"outline": True, "reviewers": 2,
171
+ "review_rounds": min(requested_rounds, 3),
172
+ "reason": "专家模式启用完整大纲、多评审与修订门禁"}
173
+ if mode != "auto":
174
+ return {"outline": True, "reviewers": 0,
175
+ "review_rounds": requested_rounds,
176
+ "reason": "手动模式尊重用户指定的评审组与轮数"}
177
+
178
+ if ttype in light_types and difficulty != "hard":
179
+ return {"outline": False, "reviewers": 1, "review_rounds": 1,
180
+ "reason": "轻量内容省略独立大纲,只做一次快速评审"}
181
+
182
+ outline = ttype in deep_types or difficulty == "hard"
183
+ reviewers = 2 if outline else 1
184
+ review_rounds = min(requested_rounds, 2 if outline else 1)
185
+ try:
186
+ if "reviewers" in rec:
187
+ reviewers = max(1, min(2, int(rec["reviewers"])))
188
+ if "review_rounds" in rec:
189
+ review_rounds = max(1, min(requested_rounds, 3,
190
+ int(rec["review_rounds"])))
191
+ except (TypeError, ValueError):
192
+ pass
193
+ # 深度类型至少保留一轮评审,但允许规划器把双评审降成一名;高门槛强制双评审。
194
+ if float(task.get("threshold") or 7.0) >= 8.5:
195
+ reviewers = 2
196
+ return {"outline": outline, "reviewers": reviewers,
197
+ "review_rounds": review_rounds,
198
+ "reason": ("深度内容保留大纲和多轮质量门禁" if outline
199
+ else "标准内容使用单评审短链")}