codebee 0.1.62 → 0.1.64

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 (101) hide show
  1. package/CHANGELOG.md +507 -490
  2. package/LICENSE +21 -21
  3. package/README.md +487 -486
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/aiflavor.py +197 -197
  12. package/app/core/artifacts.py +148 -0
  13. package/app/core/attachments.py +532 -532
  14. package/app/core/automation.py +758 -758
  15. package/app/core/backup.py +470 -470
  16. package/app/core/beekeeper.py +183 -183
  17. package/app/core/board.py +209 -209
  18. package/app/core/bookmeta.py +215 -19
  19. package/app/core/bookmeta_catalog.py +180 -180
  20. package/app/core/branching.py +137 -137
  21. package/app/core/builtin_agent.py +1191 -1191
  22. package/app/core/capability.py +144 -144
  23. package/app/core/catalog.py +19 -14
  24. package/app/core/chaptersafe.py +32 -32
  25. package/app/core/cleanup.py +303 -303
  26. package/app/core/compaction.py +186 -186
  27. package/app/core/covergen.py +356 -356
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +173 -173
  30. package/app/core/dispatch_log.py +113 -113
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/error_codes.py +64 -64
  33. package/app/core/errorlog.py +137 -137
  34. package/app/core/flows.py +355 -355
  35. package/app/core/goal_service.py +167 -167
  36. package/app/core/health.py +341 -341
  37. package/app/core/history.py +38 -38
  38. package/app/core/hooks.py +156 -156
  39. package/app/core/jobs.py +1022 -1022
  40. package/app/core/manager.py +1996 -1996
  41. package/app/core/market.py +362 -362
  42. package/app/core/market_remote.py +66 -66
  43. package/app/core/mocks.py +64 -64
  44. package/app/core/modelhub.py +3332 -3266
  45. package/app/core/notify.py +106 -106
  46. package/app/core/paihang.py +109 -109
  47. package/app/core/paths.py +2 -2
  48. package/app/core/pipeline.py +4483 -4426
  49. package/app/core/planner.py +27 -27
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/preview.py +293 -293
  52. package/app/core/publish/__init__.py +7 -7
  53. package/app/core/publish/auto.py +367 -367
  54. package/app/core/publish/browser.py +533 -533
  55. package/app/core/publish/fanqie.py +128 -128
  56. package/app/core/publish/flow.py +406 -406
  57. package/app/core/publish/ledger.py +198 -198
  58. package/app/core/publish/manager.py +590 -590
  59. package/app/core/publish/qimao.py +141 -141
  60. package/app/core/publish/ws.py +139 -139
  61. package/app/core/redact.py +38 -38
  62. package/app/core/repeat_guard.py +124 -124
  63. package/app/core/router.py +20 -3
  64. package/app/core/runner.py +2019 -1991
  65. package/app/core/selfupdate.py +384 -384
  66. package/app/core/session_log.py +161 -161
  67. package/app/core/sessions.py +312 -312
  68. package/app/core/settings.py +149 -149
  69. package/app/core/settings_schema.py +262 -262
  70. package/app/core/share_page.py +114 -114
  71. package/app/core/skill_scan.py +106 -106
  72. package/app/core/skillpacks/fanqie-novel.md +80 -80
  73. package/app/core/skillpacks/market/character-bible.md +66 -66
  74. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  75. package/app/core/skillpacks/market/git-workflow.md +57 -57
  76. package/app/core/skillpacks/market/release-notes.md +72 -72
  77. package/app/core/skillpacks/market/weekly-report.md +71 -71
  78. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  79. package/app/core/step_runner.py +132 -132
  80. package/app/core/store.py +13 -70
  81. package/app/core/task_compile.py +199 -199
  82. package/app/core/telemetry.py +292 -292
  83. package/app/core/tlsctx.py +64 -64
  84. package/app/core/token_meter.py +147 -147
  85. package/app/core/volumes.py +355 -355
  86. package/app/core/wxdigest.py +858 -858
  87. package/app/core/zentao.py +2447 -2090
  88. package/app/main.py +2832 -2826
  89. package/app/pick_dialog.py +110 -110
  90. package/app/ui/app.js +13977 -13977
  91. package/app/ui/board.html +162 -162
  92. package/app/ui/board.js +305 -305
  93. package/app/ui/i18n.js +2711 -2711
  94. package/app/ui/icons/bee.svg +79 -79
  95. package/app/ui/index.html +1 -1
  96. package/app/ui/manifest.json +16 -16
  97. package/app/ui/qrcode.js +2297 -2297
  98. package/app/ui/style.css +6125 -6125
  99. package/bin/tutti.js +147 -147
  100. package/package.json +41 -41
  101. package/tools/wx_reader.py +261 -261
package/app/core/store.py CHANGED
@@ -18,7 +18,7 @@ import threading
18
18
  import time
19
19
  from pathlib import Path
20
20
 
21
- from . import paths, runner, volumes
21
+ from . import artifacts, paths, runner, volumes
22
22
 
23
23
  LOCK = threading.RLock()
24
24
  _TASKS = {}
@@ -746,7 +746,7 @@ def task_side(task_id):
746
746
  total_steps = sum(len(r.get("steps") or []) for r in runs)
747
747
  # 成品口径闸:任务一步都没跑出来过(如历次都在检出前失败)→ 工作目录里的
748
748
  # 文件变动全是并行活动的噪音,不算这个任务的成品
749
- wd, arts = run_artifacts(latest["id"], limit=50) if (latest and total_steps > 0) else ("", [])
749
+ wd, arts = run_artifacts(latest["id"], limit=50, cache=True) if (latest and total_steps > 0) else ("", [])
750
750
  return {
751
751
  "task": {k: task.get(k) for k in ("id", "title", "status", "workdir", "git_state",
752
752
  "git_rev")},
@@ -966,41 +966,6 @@ def run_workdir(run_id):
966
966
  return str(task.get("workdir") or "") if task else ""
967
967
 
968
968
 
969
- _SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", ".idea", ".vscode"}
970
-
971
- # 构建产物/依赖缓存目录:里面的文件是工具链再生成的,不是任务成品
972
- _BUILD_DIRS = {
973
- "target", "build", "dist", "out", "bin", "obj", # 通用构建输出
974
- "surefire", "failsafe-reports", "test-output", "reports", # 测试/报告输出
975
- ".next", ".nuxt", ".output", ".gradle", ".gradle-home", # 前端/Gradle
976
- "__MACOSX",
977
- }
978
-
979
- # CLI 自身的历史/评审中间件不属于用户成果。它们通常落在工作目录根部,
980
- # 只按隐藏目录过滤会漏掉 Aider 的 .aider.* 与流程生成的 *_review.json。
981
- _PROCESS_ARTIFACT_NAMES = {
982
- ".aider.chat.history.md", ".aider.input.history", ".aider.input.history.md",
983
- ".aider.tags.cache.v4", ".aider.tags.cache.v3", ".aider.conf.yml",
984
- }
985
- _PROCESS_ARTIFACT_SUFFIXES = ("_review.json", ".review.json")
986
- _PROCESS_ARTIFACT_PREFIXES = ("tutti_prompt_", "_tutti_prompt_")
987
-
988
-
989
- def _is_process_artifact(rel_name):
990
- """判断工作目录文件是否为 CLI/评审过程产物,而非可交付成果。"""
991
- name = str(rel_name or "").replace("\\", "/")
992
- base = name.rsplit("/", 1)[-1].lower()
993
- if base in _PROCESS_ARTIFACT_NAMES:
994
- return True
995
- if base.startswith(".") and base.startswith(".aider"):
996
- return True
997
- if base.endswith(_PROCESS_ARTIFACT_SUFFIXES):
998
- return True
999
- if base.startswith(_PROCESS_ARTIFACT_PREFIXES):
1000
- return True
1001
- return False
1002
-
1003
-
1004
969
  def task_first_start(task_id, fallback=""):
1005
970
  """该任务最早一次运行的开始时间(含回退:任务创建时间 → 指定回退值)。"""
1006
971
  stamps = []
@@ -1019,13 +984,15 @@ def task_first_start(task_id, fallback=""):
1019
984
  return best
1020
985
 
1021
986
 
1022
- def run_artifacts(run_id, limit=200):
987
+ def run_artifacts(run_id, limit=200, cache=False):
1023
988
  """列一次运行的「成品文件」:工作目录里自该任务首次运行以来新产生/修改的文件。
1024
989
 
1025
990
  断点续跑会拆成多条 run,只按本 run 过滤会漏掉早期章节;这里以「任务首跑」
1026
991
  为起点。返回 (workdir, files);files 按 mtime 新→旧,name 为工作目录内
1027
- 相对路径,已跳过 .git / 隐藏目录 / node_modules / target 等构建产物目录,
1028
- 最多 limit 个。
992
+ 相对路径,已跳过 .git / 隐藏目录 / node_modules / target 等构建产物目录。
993
+ cache=True 供 UI 轮询热路径(详情/侧栏/状态快照)复用短 TTL 缓存与在飞
994
+ 合并;发布/预览/知识库等正确性敏感方保持默认,永远现扫。
995
+ 剪枝遍历与过滤实现在 artifacts.py(几万文件目录从秒级降到百毫秒级)。
1029
996
  """
1030
997
  wd = run_workdir(run_id)
1031
998
  if not wd:
@@ -1040,36 +1007,12 @@ def run_artifacts(run_id, limit=200):
1040
1007
  root = Path(wd)
1041
1008
  if not root.is_dir():
1042
1009
  return wd, []
1043
- files = []
1044
- try:
1045
- for p in root.rglob("*"):
1046
- if not p.is_file():
1047
- continue
1048
- if any(part in _SKIP_DIRS for part in p.parts):
1049
- continue
1050
- if any(part in _BUILD_DIRS for part in p.parts[:-1]):
1051
- continue
1052
- # 隐藏目录一律是工具过程文件(.mimocode/.zcode/.claude/.codex…),
1053
- # 不是成品;按前缀通排,免得每来一个新 agent CLI 就补一次白名单
1054
- if any(part.startswith(".") for part in p.parts[:-1]):
1055
- continue
1056
- try:
1057
- st = p.stat()
1058
- except OSError:
1059
- continue
1060
- if st.st_mtime < t0:
1061
- continue
1062
- rel_name = str(p.relative_to(root)).replace("\\", "/")
1063
- if _is_process_artifact(rel_name):
1064
- continue
1065
- files.append({"name": rel_name,
1066
- "size": st.st_size, "mtime": int(st.st_mtime)})
1067
- if len(files) >= 800: # 防超大目录拖垮接口;截断后再排序取最新
1068
- break
1069
- except OSError:
1070
- pass
1071
- files.sort(key=lambda f: -f["mtime"])
1072
- return wd, files[:limit]
1010
+ running = False
1011
+ if cache:
1012
+ task = get_task(run.get("task_id") or "") if run.get("task_id") else None
1013
+ running = bool(task) and task.get("status") in ("queued", "running")
1014
+ return wd, artifacts.scan(str(root), t0, limit=limit,
1015
+ running=running, cache=cache)
1073
1016
 
1074
1017
 
1075
1018
  def task_step_count(task_id):
@@ -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 "标准内容使用单评审短链")}