codebee 0.1.65 → 0.1.67

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 (116) hide show
  1. package/CHANGELOG.md +532 -513
  2. package/LICENSE +21 -21
  3. package/README.md +494 -487
  4. package/app/core/acceptance.py +100 -0
  5. package/app/core/aiflavor.py +197 -197
  6. package/app/core/artifacts.py +148 -148
  7. package/app/core/assets.py +110 -0
  8. package/app/core/attachments.py +623 -532
  9. package/app/core/automation.py +758 -758
  10. package/app/core/backup.py +470 -470
  11. package/app/core/beekeeper.py +183 -183
  12. package/app/core/board.py +209 -209
  13. package/app/core/bookmeta.py +930 -759
  14. package/app/core/bookmeta_catalog.py +180 -180
  15. package/app/core/branching.py +137 -137
  16. package/app/core/builtin_agent.py +1234 -1191
  17. package/app/core/capability.py +144 -144
  18. package/app/core/catalog.py +23 -24
  19. package/app/core/chaptersafe.py +32 -32
  20. package/app/core/cleanup.py +303 -303
  21. package/app/core/compaction.py +201 -186
  22. package/app/core/covergen.py +356 -356
  23. package/app/core/defectretro.py +43 -0
  24. package/app/core/diagnostics.py +114 -114
  25. package/app/core/dispatch.py +173 -173
  26. package/app/core/dispatch_log.py +113 -113
  27. package/app/core/env_scrub.py +85 -85
  28. package/app/core/error_codes.py +220 -65
  29. package/app/core/errorlog.py +137 -137
  30. package/app/core/evaluation.py +174 -0
  31. package/app/core/failure_notify.py +290 -0
  32. package/app/core/flows.py +401 -355
  33. package/app/core/goal_service.py +167 -167
  34. package/app/core/health.py +374 -342
  35. package/app/core/history.py +38 -38
  36. package/app/core/hooks.py +162 -156
  37. package/app/core/jobs.py +1050 -1022
  38. package/app/core/knowledge.py +56 -5
  39. package/app/core/manager.py +2122 -1996
  40. package/app/core/market.py +362 -362
  41. package/app/core/market_remote.py +190 -92
  42. package/app/core/mocks.py +64 -64
  43. package/app/core/modelhub.py +3546 -3332
  44. package/app/core/notify.py +106 -106
  45. package/app/core/operations.py +228 -0
  46. package/app/core/paihang.py +138 -109
  47. package/app/core/paths.py +2 -2
  48. package/app/core/pipeline.py +4808 -4494
  49. package/app/core/planner.py +37 -32
  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 +539 -533
  55. package/app/core/publish/fanqie.py +159 -128
  56. package/app/core/publish/flow.py +440 -406
  57. package/app/core/publish/flows-fanqie-calibrated.json +17 -9
  58. package/app/core/publish/flows-qimao-calibrated.json +6 -2
  59. package/app/core/publish/ledger.py +203 -198
  60. package/app/core/publish/manager.py +692 -590
  61. package/app/core/publish/qimao.py +141 -141
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/redact.py +38 -38
  64. package/app/core/repeat_guard.py +129 -124
  65. package/app/core/revisions.py +51 -0
  66. package/app/core/router.py +119 -81
  67. package/app/core/runner.py +2065 -2042
  68. package/app/core/selfupdate.py +384 -384
  69. package/app/core/session_log.py +161 -161
  70. package/app/core/sessions.py +312 -312
  71. package/app/core/settings.py +149 -149
  72. package/app/core/settings_schema.py +262 -262
  73. package/app/core/share_page.py +114 -114
  74. package/app/core/skill_scan.py +114 -106
  75. package/app/core/skillpacks/fanqie-novel.md +80 -80
  76. package/app/core/skillpacks/market/character-bible.md +66 -66
  77. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  78. package/app/core/skillpacks/market/git-workflow.md +57 -57
  79. package/app/core/skillpacks/market/release-notes.md +72 -72
  80. package/app/core/skillpacks/market/weekly-report.md +71 -71
  81. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  82. package/app/core/skills.py +315 -73
  83. package/app/core/step_runner.py +132 -132
  84. package/app/core/store.py +230 -11
  85. package/app/core/task_compile.py +199 -199
  86. package/app/core/telemetry.py +292 -292
  87. package/app/core/tlsctx.py +64 -64
  88. package/app/core/token_meter.py +147 -147
  89. package/app/core/upstream.py +41 -0
  90. package/app/core/usage.py +114 -3
  91. package/app/core/volumes.py +389 -355
  92. package/app/core/wxdigest.py +858 -858
  93. package/app/core/zentao.py +2465 -2447
  94. package/app/main.py +2957 -2832
  95. package/app/pick_dialog.py +110 -110
  96. package/app/ui/app.js +14384 -13977
  97. package/app/ui/artdiff.js +74 -0
  98. package/app/ui/board.css +20 -41
  99. package/app/ui/board.html +162 -162
  100. package/app/ui/board.js +305 -305
  101. package/app/ui/i18n.js +2766 -2711
  102. package/app/ui/icons/bee.svg +79 -79
  103. package/app/ui/index.html +7 -1
  104. package/app/ui/manifest.json +16 -16
  105. package/app/ui/qrcode.js +2297 -2297
  106. package/app/ui/style.css +6179 -6125
  107. package/bin/tutti.js +147 -147
  108. package/package.json +46 -41
  109. package/tools/wx_reader.py +261 -261
  110. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  111. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  112. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  113. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  114. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  115. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  116. package/app/.playwright-cli/run-detail-after.png +0 -0
package/app/core/flows.py CHANGED
@@ -1,355 +1,401 @@
1
- # -*- coding: utf-8 -*-
2
- """流程注册表重写:预置流程可编辑/可恢复默认 + 自定义流程极简配置。
3
-
4
- 设计:
5
- - 预置流程(code/novel/serial_novel/doc/...)**可编辑**(阈值/轮数/维度/章节数/
6
- 提示词),改动存 data/flows.json 的 overrides;「恢复默认」一键回滚;
7
- - 自定义流程**只需名称 + 引擎**:其余全部有合理默认(产出文件名按 id 生成、
8
- 维度用引擎默认、阈值 7.0、轮数 2);进阶项留空即默认,不阻塞创建。
9
- """
10
- from __future__ import annotations
11
-
12
- import json
13
- import re
14
- import threading
15
-
16
- from . import paths, volumes
17
-
18
- _LOCK = threading.RLock()
19
- _FILE = paths.DATA_DIR / "flows.json"
20
-
21
- ENGINES = ("code", "review", "direct")
22
-
23
- # 引擎默认参数:自定义流程留空时的兜底
24
- ENGINE_DEFAULTS = {
25
- "review": {"manuscript": "output.md", "rubric": ["内容", "结构", "表达"],
26
- "threshold": 7.0, "rounds": 2, "best_of": 1},
27
- "code": {"verify_command": ""},
28
- # direct:无参数——目标+附件即全部输入,跑完即止
29
- }
30
-
31
- # icon 约定:"i-*" = 前端精灵表单色线性图标(随日/夜主题黑白);其他值(emoji)原样显示,
32
- # 供自定义流程兜底。预置图标不进 overrides(见 _EDITABLE),保证升级后老数据也拿到新图标。
33
- BUILTIN_FLOWS = [
34
- {"id": "direct", "name": "直接执行", "icon": "i-chat", "engine": "direct", "builtin": True,
35
- "goal_hint": "让 AI 直接做什么(一句话,可带附件)",
36
- "note": "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
37
- {"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
38
- "goal_hint": "要实现/修复什么(一句话)",
39
- "note": "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将"},
40
- {"id": "novel", "name": "小说", "icon": "i-book-open", "engine": "review", "builtin": True,
41
- "manuscript": "manuscript.md",
42
- "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
43
- "threshold": 7.0, "rounds": 2,
44
- "goal_hint": "写什么(题材 / 篇幅 / 风格)",
45
- "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
46
- {"id": "serial_novel", "name": "连载小说", "icon": "i-library", "engine": "review", "builtin": True,
47
- "manuscript": "manuscript.md",
48
- "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
49
- "threshold": 7.0, "rounds": 2,
50
- "serial": {"chapters": 8, "words_per_chapter": 2500},
51
- "goal_hint": "题材/受众/卖点 + 总字数(例:2 万字都市女频,可签约平台)",
52
- "note": "大纲 → 逐章起草 → 每章多维评审修订 → 全局一致性评审 → 合并(可断点续跑)"},
53
- {"id": "article", "name": "自媒体文章", "icon": "i-news", "engine": "review", "builtin": True,
54
- "manuscript": "article.md",
55
- "rubric": ["选题与标题", "开头吸引力", "结构节奏", "平台适配", "传播性"],
56
- "threshold": 7.0, "rounds": 2,
57
- "goal_hint": "什么主题、投哪个平台(公众号/头条/知乎)、给谁看",
58
- "note": "公众号/头条风格文章:起草 → 多维评审 → 修订循环 → 发布门禁"},
59
- {"id": "video_script", "name": "短视频脚本", "icon": "i-clapper", "engine": "review", "builtin": True,
60
- "manuscript": "script.md",
61
- "rubric": ["黄金3秒钩子", "节奏密度", "口播流畅", "画面可执行", "互动引导"],
62
- "threshold": 7.0, "rounds": 2,
63
- "goal_hint": "什么主题、多长时长、投哪个平台(抖音/B站/视频号)",
64
- "note": "分镜 + 口播脚本:起草 → 多维评审 → 修订循环 → 发布门禁"},
65
- {"id": "doc", "name": "文档", "icon": "i-file-text", "engine": "review", "builtin": True,
66
- "manuscript": "document.md",
67
- "rubric": ["准确性", "结构清晰", "表达流畅", "实用价值"],
68
- "threshold": 7.0, "rounds": 2,
69
- "goal_hint": "要写什么文档、给谁看",
70
- "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
71
- {"id": "translation", "name": "翻译", "icon": "i-lang", "engine": "review", "builtin": True,
72
- "manuscript": "translation.md",
73
- "rubric": ["忠实度", "流畅度", "术语一致性", "风格贴合"],
74
- "threshold": 7.0, "rounds": 2,
75
- "goal_hint": "翻译什么(源文本位置 / 目标语言 / 要求)",
76
- "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
77
- {"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
78
- "goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
79
- "note": "抓取七猫+番茄排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
80
- {"id": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
81
- "manuscript": "report.md",
82
- "rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
83
- "threshold": 7.0, "rounds": 2,
84
- "goal_hint": "调研什么问题、产出给谁用",
85
- "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
86
- {"id": "speech", "name": "演讲稿", "icon": "i-mic", "engine": "review", "builtin": True,
87
- "manuscript": "speech.md",
88
- "rubric": ["主题聚焦", "结构", "感染力", "语言风格"],
89
- "threshold": 7.0, "rounds": 2,
90
- "goal_hint": "演讲场合 / 听众 / 时长 / 核心信息",
91
- "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
92
- {"id": "weekly_report", "name": "工作汇报", "icon": "i-clip-check", "engine": "review", "builtin": True,
93
- "manuscript": "weekly.md",
94
- "rubric": ["重点突出", "数据支撑", "结构清晰", "下一步可执行"],
95
- "threshold": 7.0, "rounds": 2,
96
- "goal_hint": "周报/月报/述职?汇报给谁、做了什么(可贴流水账让小队提炼)",
97
- "note": "周报/月报/述职材料:起草 → 多维评审 → 修订循环 → 发布门禁"},
98
- {"id": "email", "name": "商务邮件", "icon": "i-mail", "engine": "review", "builtin": True,
99
- "manuscript": "email.md",
100
- "rubric": ["目的明确", "语气得体", "信息完整", "简洁度"],
101
- "threshold": 7.0, "rounds": 2,
102
- "goal_hint": "给谁写、要达成什么、对方背景与你们的关系",
103
- "note": "商务/客户/跨部门邮件:起草 → 多维评审 → 修订循环 → 发布门禁"},
104
- {"id": "tech_proposal", "name": "技术方案", "icon": "i-blocks", "engine": "review", "builtin": True,
105
- "manuscript": "proposal.md",
106
- "rubric": ["可行性", "方案完整性", "风险识别", "成本与收益"],
107
- "threshold": 7.0, "rounds": 2,
108
- "goal_hint": "要解决什么问题、约束条件(工期/技术栈/预算)、给谁评审",
109
- "note": "技术选型/架构/实施方案:起草 → 多维评审 → 修订循环 → 发布门禁"},
110
- {"id": "resume", "name": "简历", "icon": "i-idcard", "engine": "review", "builtin": True,
111
- "manuscript": "resume.md",
112
- "rubric": ["真实可信", "岗位匹配", "成果量化", "关键词覆盖", "简洁度"],
113
- "threshold": 7.0, "rounds": 2,
114
- "goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
115
- "note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
116
- {"id": "bid_doc", "name": "标书编制", "icon": "i-file-search", "engine": "review", "builtin": True,
117
- "manuscript": "bid.md",
118
- "rubric": ["应答完整性", "合规符合度", "方案针对性", "评分点覆盖", "商务清晰度"],
119
- "threshold": 7.5, "rounds": 2,
120
- "goal_hint": "招标文件(可作附件)+ 标段/包件 + 我方资质与业绩亮点",
121
- "note": "标书/投标文件:起草 → 多维评审 → 修订循环 → 发布门禁"},
122
- ]
123
-
124
- _ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
125
- # 可编辑字段(预置流程的 overrides 与自定义流程共用同一套规范化)。
126
- # icon 刻意不进来:预置图标跟版本走(老 overrides 里钉的旧 emoji 会被忽略),
127
- # 自定义流程的 icon 在 upsert_flow 里直接落盘,不走 overrides。
128
- _EDITABLE = ("name", "goal_hint", "note", "manuscript", "rubric",
129
- "threshold", "rounds", "serial", "draft_prompt", "critique_prompt",
130
- "verify_command", "best_of")
131
-
132
-
133
- def _read():
134
- try:
135
- data = json.loads(_FILE.read_text(encoding="utf-8"))
136
- return data if isinstance(data, dict) else {}
137
- except Exception:
138
- return {}
139
-
140
-
141
- def _write(data):
142
- _FILE.parent.mkdir(parents=True, exist_ok=True)
143
- tmp = _FILE.with_suffix(".tmp")
144
- tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
145
- tmp.replace(_FILE)
146
-
147
-
148
- def _norm_serial(serial):
149
- if not isinstance(serial, dict) or not serial.get("chapters"):
150
- return None
151
- try:
152
- out = {"chapters": max(2, min(20, int(serial["chapters"]))),
153
- "words_per_chapter": max(500, min(8000, int(serial.get("words_per_chapter") or 2500)))}
154
- except Exception:
155
- return None
156
- # 分卷(可选):显式卷表优先,其次每卷章数。流程模板里可以预置,
157
- # 任务级 payload 仍可覆盖/关闭(0 = 不分卷)。
158
- spec = volumes.norm_spec(serial.get("volumes"))
159
- if spec:
160
- out["volumes"] = spec
161
- per = volumes.norm_per(serial.get("volume_chapters"))
162
- if per:
163
- out["volume_chapters"] = per
164
- return out
165
-
166
-
167
- def _norm_rubric(rubric):
168
- if isinstance(rubric, str):
169
- rubric = re.split(r"[,,、\n]+", rubric)
170
- if not isinstance(rubric, list):
171
- return None
172
- out = [str(d).strip() for d in rubric if str(d).strip()][:8]
173
- return out or None
174
-
175
-
176
- def _apply_overrides(base, ov):
177
- """把 overrides 合并到预置流程上(只认白名单字段)。"""
178
- f = dict(base)
179
- if not isinstance(ov, dict):
180
- return f
181
- for k in _EDITABLE:
182
- if k not in ov:
183
- continue
184
- v = ov[k]
185
- if k == "rubric":
186
- v = _norm_rubric(v)
187
- if v:
188
- f["rubric"] = v
189
- elif k == "serial":
190
- v = _norm_serial(v)
191
- if v:
192
- f["serial"] = v
193
- else:
194
- f.pop("serial", None)
195
- elif k == "threshold":
196
- try:
197
- f["threshold"] = max(1.0, min(10.0, float(v)))
198
- except Exception:
199
- pass
200
- elif k == "rounds":
201
- try:
202
- f["rounds"] = max(1, min(5, int(v)))
203
- except Exception:
204
- pass
205
- elif k == "best_of":
206
- try:
207
- f["best_of"] = max(1, min(3, int(v)))
208
- except Exception:
209
- pass
210
- elif k in ("manuscript", "verify_command"):
211
- s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
212
- s = re.sub(r"\.{2,}", "_", s).lstrip(".")
213
- if s:
214
- f[k] = s
215
- elif k in ("draft_prompt", "critique_prompt"):
216
- s = str(v or "").strip()
217
- if s:
218
- f[k] = s[:4000]
219
- else:
220
- f.pop(k, None)
221
- else:
222
- f[k] = str(v or "")[:80]
223
- return f
224
-
225
-
226
- def _custom():
227
- data = _read().get("flows")
228
- return [f for f in data if isinstance(f, dict) and f.get("id")] if isinstance(data, list) else []
229
-
230
-
231
- def _overrides():
232
- ov = _read().get("overrides")
233
- return ov if isinstance(ov, dict) else {}
234
-
235
-
236
- def list_flows():
237
- """自定义流程在前(便于看到新增),预置流程按定义顺序,均已套用 overrides。"""
238
- with _LOCK:
239
- custom, ov = _custom(), _overrides()
240
- builtin = [_apply_overrides(b, ov.get(b["id"])) for b in BUILTIN_FLOWS]
241
- for f in builtin:
242
- f["edited"] = bool(ov.get(f["id"]))
243
- return custom + builtin
244
-
245
-
246
- def get_flow(flow_id):
247
- for f in list_flows():
248
- if f["id"] == flow_id:
249
- return f
250
- return None
251
-
252
-
253
- def upsert_flow(payload):
254
- """新增/更新流程。返回 (flow, 错误)。
255
-
256
- - 预置流程 id:写入 overrides(可编辑、可恢复默认);
257
- - 新 id:创建自定义流程——**只需 name + engine**,其余字段留空走引擎默认。
258
- """
259
- fid = str(payload.get("id") or "").strip()
260
- name = str(payload.get("name") or "").strip()
261
- engine = str(payload.get("engine") or "review").strip()
262
- if not _ID_RE.match(fid):
263
- return None, "流程 ID 只能是小写字母开头的字母/数字/-/_(≤32 位)"
264
- if engine not in ENGINES:
265
- return None, "engine 必须是 code、review 或 direct"
266
- builtin_ids = {b["id"] for b in BUILTIN_FLOWS}
267
-
268
- if fid in builtin_ids:
269
- base = next(b for b in BUILTIN_FLOWS if b["id"] == fid)
270
- if name and name != base["name"]:
271
- pass # 允许改名
272
- ov = dict(payload)
273
- ov.pop("id", None)
274
- ov["engine"] = base["engine"] # 引擎不可改(避免语义错乱)
275
- merged = _apply_overrides(base, ov)
276
- with _LOCK:
277
- data = _read()
278
- ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
279
- ovs[fid] = {k: merged[k] for k in _EDITABLE if k in merged}
280
- data["overrides"] = ovs
281
- _write(data)
282
- merged["edited"] = True
283
- return merged, None
284
-
285
- if not name:
286
- return None, "名称不能为空"
287
- flow = {"id": fid, "name": name[:20], "icon": str(payload.get("icon") or "✨")[:4],
288
- "engine": engine, "builtin": False,
289
- "goal_hint": str(payload.get("goal_hint") or "")[:80],
290
- "note": str(payload.get("note") or "")[:80]}
291
- if engine == "review":
292
- dflt = ENGINE_DEFAULTS["review"]
293
- ms = re.sub(r"[\\/]+", "_", str(payload.get("manuscript") or (fid + ".md"))).strip()
294
- ms = re.sub(r"\.{2,}", "_", ms).lstrip(".") or (fid + ".md")
295
- flow["manuscript"] = ms
296
- flow["rubric"] = _norm_rubric(payload.get("rubric")) or list(dflt["rubric"])
297
- try:
298
- flow["threshold"] = max(1.0, min(10.0, float(payload.get("threshold") or dflt["threshold"])))
299
- except Exception:
300
- flow["threshold"] = dflt["threshold"]
301
- try:
302
- flow["rounds"] = max(1, min(5, int(payload.get("rounds") or dflt["rounds"])))
303
- except Exception:
304
- flow["rounds"] = dflt["rounds"]
305
- try:
306
- flow["best_of"] = max(1, min(3, int(payload.get("best_of") or dflt.get("best_of") or 1)))
307
- except Exception:
308
- flow["best_of"] = 1
309
- serial = _norm_serial(payload.get("serial"))
310
- if serial:
311
- flow["serial"] = serial
312
- for key in ("draft_prompt", "critique_prompt"):
313
- v = str(payload.get(key) or "").strip()
314
- if v:
315
- flow[key] = v[:4000]
316
- elif engine == "code":
317
- flow["verify_command"] = str(payload.get("verify_command") or "")[:200]
318
- # direct:无流程参数(目标+附件即全部输入)
319
- with _LOCK:
320
- data = _read()
321
- flows = data.get("flows") if isinstance(data.get("flows"), list) else []
322
- flows = [f for f in flows if isinstance(f, dict) and f.get("id") != fid]
323
- flows.append(flow)
324
- data["flows"] = flows
325
- _write(data)
326
- return flow, None
327
-
328
-
329
- def delete_flow(flow_id):
330
- """删除自定义流程;预置流程只能「恢复默认」不能删除。"""
331
- if flow_id in {b["id"] for b in BUILTIN_FLOWS}:
332
- return "预置流程不可删除(可点「恢复默认」回滚改动)"
333
- with _LOCK:
334
- data = _read()
335
- flows = data.get("flows") if isinstance(data.get("flows"), list) else []
336
- kept = [f for f in flows if not (isinstance(f, dict) and f.get("id") == flow_id)]
337
- if len(kept) == len(flows):
338
- return "流程不存在"
339
- data["flows"] = kept
340
- _write(data)
341
- return None
342
-
343
-
344
- def reset_flow(flow_id):
345
- """预置流程恢复默认(清掉 overrides)。返回错误或 None。"""
346
- if flow_id not in {b["id"] for b in BUILTIN_FLOWS}:
347
- return "只有预置流程支持恢复默认"
348
- with _LOCK:
349
- data = _read()
350
- ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
351
- if flow_id in ovs:
352
- ovs.pop(flow_id, None)
353
- data["overrides"] = ovs
354
- _write(data)
355
- return None
1
+ # -*- coding: utf-8 -*-
2
+ """流程注册表重写:预置流程可编辑/可恢复默认 + 自定义流程极简配置。
3
+
4
+ 设计:
5
+ - 预置流程(code/novel/serial_novel/doc/...)**可编辑**(阈值/轮数/维度/章节数/
6
+ 提示词),改动存 data/flows.json 的 overrides;「恢复默认」一键回滚;
7
+ - 自定义流程**只需名称 + 引擎**:其余全部有合理默认(产出文件名按 id 生成、
8
+ 维度用引擎默认、阈值 7.0、轮数 2);进阶项留空即默认,不阻塞创建。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import re
15
+ import threading
16
+
17
+ from . import paths, volumes
18
+
19
+ _LOCK = threading.RLock()
20
+ _FILE = paths.DATA_DIR / "flows.json"
21
+
22
+ ENGINES = ("code", "review", "direct")
23
+
24
+ # 引擎默认参数:自定义流程留空时的兜底
25
+ ENGINE_DEFAULTS = {
26
+ "review": {"manuscript": "output.md", "rubric": ["内容", "结构", "表达"],
27
+ "threshold": 7.0, "rounds": 2, "best_of": 1},
28
+ "code": {"verify_command": ""},
29
+ # direct:无参数——目标+附件即全部输入,跑完即止
30
+ }
31
+
32
+ # icon 约定:"i-*" = 前端精灵表单色线性图标(随日/夜主题黑白);其他值(emoji)原样显示,
33
+ # 供自定义流程兜底。预置图标不进 overrides(见 _EDITABLE),保证升级后老数据也拿到新图标。
34
+ BUILTIN_FLOWS = [
35
+ {"id": "direct", "name": "直接执行", "icon": "i-chat", "engine": "direct", "builtin": True,
36
+ "goal_hint": "让 AI 直接做什么(一句话,可带附件)",
37
+ "note": "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
38
+ {"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
39
+ "goal_hint": "要实现/修复什么(一句话)",
40
+ "note": "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将"},
41
+ {"id": "novel", "name": "小说", "icon": "i-book-open", "engine": "review", "builtin": True,
42
+ "manuscript": "manuscript.md",
43
+ "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
44
+ "threshold": 7.0, "rounds": 2,
45
+ "goal_hint": "写什么(题材 / 篇幅 / 风格)",
46
+ "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
47
+ {"id": "serial_novel", "name": "连载小说", "icon": "i-library", "engine": "review", "builtin": True,
48
+ "manuscript": "manuscript.md",
49
+ "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
50
+ "threshold": 7.0, "rounds": 2,
51
+ "serial": {"chapters": 8, "words_per_chapter": 2500},
52
+ "goal_hint": "题材/受众/卖点 + 总字数(例:2 万字都市女频,可签约平台)",
53
+ "note": "大纲 → 逐章起草 → 每章多维评审修订 → 全局一致性评审 → 合并(可断点续跑)"},
54
+ {"id": "article", "name": "自媒体文章", "icon": "i-news", "engine": "review", "builtin": True,
55
+ "manuscript": "article.md",
56
+ "rubric": ["选题与标题", "开头吸引力", "结构节奏", "平台适配", "传播性"],
57
+ "threshold": 7.0, "rounds": 2,
58
+ "goal_hint": "什么主题、投哪个平台(公众号/头条/知乎)、给谁看",
59
+ "note": "公众号/头条风格文章:起草 → 多维评审 → 修订循环 → 发布门禁"},
60
+ {"id": "video_script", "name": "短视频脚本", "icon": "i-clapper", "engine": "review", "builtin": True,
61
+ "manuscript": "script.md",
62
+ "rubric": ["黄金3秒钩子", "节奏密度", "口播流畅", "画面可执行", "互动引导"],
63
+ "threshold": 7.0, "rounds": 2,
64
+ "goal_hint": "什么主题、多长时长、投哪个平台(抖音/B站/视频号)",
65
+ "note": "分镜 + 口播脚本:起草 → 多维评审 → 修订循环 → 发布门禁"},
66
+ {"id": "doc", "name": "文档", "icon": "i-file-text", "engine": "review", "builtin": True,
67
+ "manuscript": "document.md",
68
+ "rubric": ["准确性", "结构清晰", "表达流畅", "实用价值"],
69
+ "threshold": 7.0, "rounds": 2,
70
+ "goal_hint": "要写什么文档、给谁看",
71
+ "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
72
+ {"id": "translation", "name": "翻译", "icon": "i-lang", "engine": "review", "builtin": True,
73
+ "manuscript": "translation.md",
74
+ "rubric": ["忠实度", "流畅度", "术语一致性", "风格贴合"],
75
+ "threshold": 7.0, "rounds": 2,
76
+ "goal_hint": "翻译什么(源文本位置 / 目标语言 / 要求)",
77
+ "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
78
+ {"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
79
+ "goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
80
+ "note": "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
81
+ {"id": "defect_retro", "name": "缺陷复盘", "icon": "i-clipboard", "engine": "direct", "builtin": True,
82
+ "goal_hint": "复盘哪个版本/时间段的缺陷(建议附带禅道或 Jira 导出 CSV)",
83
+ "note": "读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)"},
84
+ {"id": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
85
+ "manuscript": "report.md",
86
+ "rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
87
+ "threshold": 7.0, "rounds": 2,
88
+ "goal_hint": "调研什么问题、产出给谁用",
89
+ "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
90
+ {"id": "speech", "name": "演讲稿", "icon": "i-mic", "engine": "review", "builtin": True,
91
+ "manuscript": "speech.md",
92
+ "rubric": ["主题聚焦", "结构", "感染力", "语言风格"],
93
+ "threshold": 7.0, "rounds": 2,
94
+ "goal_hint": "演讲场合 / 听众 / 时长 / 核心信息",
95
+ "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
96
+ {"id": "weekly_report", "name": "工作汇报", "icon": "i-clip-check", "engine": "review", "builtin": True,
97
+ "manuscript": "weekly.md",
98
+ "rubric": ["重点突出", "数据支撑", "结构清晰", "下一步可执行"],
99
+ "threshold": 7.0, "rounds": 2,
100
+ "goal_hint": "周报/月报/述职?汇报给谁、做了什么(可贴流水账让小队提炼)",
101
+ "note": "周报/月报/述职材料:起草 → 多维评审 → 修订循环 → 发布门禁"},
102
+ {"id": "email", "name": "商务邮件", "icon": "i-mail", "engine": "review", "builtin": True,
103
+ "manuscript": "email.md",
104
+ "rubric": ["目的明确", "语气得体", "信息完整", "简洁度"],
105
+ "threshold": 7.0, "rounds": 2,
106
+ "goal_hint": "给谁写、要达成什么、对方背景与你们的关系",
107
+ "note": "商务/客户/跨部门邮件:起草 → 多维评审 → 修订循环 → 发布门禁"},
108
+ {"id": "tech_proposal", "name": "技术方案", "icon": "i-blocks", "engine": "review", "builtin": True,
109
+ "manuscript": "proposal.md",
110
+ "rubric": ["可行性", "方案完整性", "风险识别", "成本与收益"],
111
+ "threshold": 7.0, "rounds": 2,
112
+ "goal_hint": "要解决什么问题、约束条件(工期/技术栈/预算)、给谁评审",
113
+ "note": "技术选型/架构/实施方案:起草 → 多维评审 → 修订循环 → 发布门禁"},
114
+ {"id": "resume", "name": "简历", "icon": "i-idcard", "engine": "review", "builtin": True,
115
+ "manuscript": "resume.md",
116
+ "rubric": ["真实可信", "岗位匹配", "成果量化", "关键词覆盖", "简洁度"],
117
+ "threshold": 7.0, "rounds": 2,
118
+ "goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
119
+ "note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
120
+ {"id": "bid_doc", "name": "标书编制", "icon": "i-file-search", "engine": "review", "builtin": True,
121
+ "manuscript": "bid.md",
122
+ "rubric": ["应答完整性", "合规符合度", "方案针对性", "评分点覆盖", "商务清晰度"],
123
+ "threshold": 7.5, "rounds": 2,
124
+ "goal_hint": "招标文件(可作附件)+ 标段/包件 + 我方资质与业绩亮点",
125
+ "note": "标书/投标文件:起草 → 多维评审 → 修订循环 → 发布门禁"},
126
+ ]
127
+
128
+ _ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
129
+ # 可编辑字段(预置流程的 overrides 与自定义流程共用同一套规范化)。
130
+ # icon 刻意不进来:预置图标跟版本走(老 overrides 里钉的旧 emoji 会被忽略),
131
+ # 自定义流程的 icon 在 upsert_flow 里直接落盘,不走 overrides。
132
+ _EDITABLE = ("name", "goal_hint", "note", "manuscript", "rubric",
133
+ "threshold", "rounds", "serial", "draft_prompt", "critique_prompt",
134
+ "verify_command", "best_of")
135
+
136
+
137
+ def _read():
138
+ try:
139
+ data = json.loads(_FILE.read_text(encoding="utf-8"))
140
+ return data if isinstance(data, dict) else {}
141
+ except Exception:
142
+ return {}
143
+
144
+
145
+ def _write(data):
146
+ _FILE.parent.mkdir(parents=True, exist_ok=True)
147
+ tmp = _FILE.with_suffix(".tmp")
148
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
149
+ tmp.replace(_FILE)
150
+
151
+
152
+ def _norm_serial(serial):
153
+ if not isinstance(serial, dict) or not serial.get("chapters"):
154
+ return None
155
+ try:
156
+ out = {"chapters": max(2, min(20, int(serial["chapters"]))),
157
+ "words_per_chapter": max(500, min(8000, int(serial.get("words_per_chapter") or 2500)))}
158
+ except Exception:
159
+ return None
160
+ # 分卷(可选):显式卷表优先,其次每卷章数。流程模板里可以预置,
161
+ # 任务级 payload 仍可覆盖/关闭(0 = 不分卷)。
162
+ spec = volumes.norm_spec(serial.get("volumes"))
163
+ if spec:
164
+ out["volumes"] = spec
165
+ per = volumes.norm_per(serial.get("volume_chapters"))
166
+ if per:
167
+ out["volume_chapters"] = per
168
+ return out
169
+
170
+
171
+ def _norm_rubric(rubric):
172
+ if isinstance(rubric, str):
173
+ rubric = re.split(r"[,,、\n]+", rubric)
174
+ if not isinstance(rubric, list):
175
+ return None
176
+ out = [str(d).strip() for d in rubric if str(d).strip()][:8]
177
+ return out or None
178
+
179
+
180
+ def _apply_overrides(base, ov):
181
+ """把 overrides 合并到预置流程上(只认白名单字段)。"""
182
+ f = dict(base)
183
+ if not isinstance(ov, dict):
184
+ return f
185
+ for k in _EDITABLE:
186
+ if k not in ov:
187
+ continue
188
+ v = ov[k]
189
+ if k == "rubric":
190
+ v = _norm_rubric(v)
191
+ if v:
192
+ f["rubric"] = v
193
+ elif k == "serial":
194
+ v = _norm_serial(v)
195
+ if v:
196
+ f["serial"] = v
197
+ else:
198
+ f.pop("serial", None)
199
+ elif k == "threshold":
200
+ try:
201
+ f["threshold"] = max(1.0, min(10.0, float(v)))
202
+ except Exception:
203
+ pass
204
+ elif k == "rounds":
205
+ try:
206
+ f["rounds"] = max(1, min(5, int(v)))
207
+ except Exception:
208
+ pass
209
+ elif k == "best_of":
210
+ try:
211
+ f["best_of"] = max(1, min(3, int(v)))
212
+ except Exception:
213
+ pass
214
+ elif k in ("manuscript", "verify_command"):
215
+ s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
216
+ s = re.sub(r"\.{2,}", "_", s).lstrip(".")
217
+ if s:
218
+ f[k] = s
219
+ elif k in ("draft_prompt", "critique_prompt"):
220
+ s = str(v or "").strip()
221
+ if s:
222
+ f[k] = s[:4000]
223
+ else:
224
+ f.pop(k, None)
225
+ else:
226
+ f[k] = str(v or "")[:80]
227
+ return f
228
+
229
+
230
+ def _custom():
231
+ data = _read().get("flows")
232
+ return [f for f in data if isinstance(f, dict) and f.get("id")] if isinstance(data, list) else []
233
+
234
+
235
+ def _overrides():
236
+ ov = _read().get("overrides")
237
+ return ov if isinstance(ov, dict) else {}
238
+
239
+
240
+ def list_flows():
241
+ """自定义流程在前(便于看到新增),预置流程按定义顺序,均已套用 overrides。"""
242
+ with _LOCK:
243
+ custom, ov = _custom(), _overrides()
244
+ builtin = [_apply_overrides(b, ov.get(b["id"])) for b in BUILTIN_FLOWS]
245
+ for f in builtin:
246
+ f["edited"] = bool(ov.get(f["id"]))
247
+ return custom + builtin
248
+
249
+
250
+ def get_flow(flow_id):
251
+ for f in list_flows():
252
+ if f["id"] == flow_id:
253
+ return f
254
+ return None
255
+
256
+
257
+ # 参与 digest 的语义字段:会进运行链的参数与提示词。icon/note/goal_hint/name 是
258
+ # 纯展示位——措辞变化不影响执行语义,不应制造「修订漂移」噪音。
259
+ _DIGEST_FIELDS = ("id", "engine", "manuscript", "rubric", "threshold", "rounds",
260
+ "best_of", "serial", "draft_prompt", "critique_prompt",
261
+ "verify_command")
262
+
263
+
264
+ def flow_content_sha256(flow):
265
+ """Return the full semantic content hash for an immutable flow revision."""
266
+ if not isinstance(flow, dict):
267
+ return ""
268
+ basis = {k: flow.get(k) for k in _DIGEST_FIELDS}
269
+ blob = json.dumps(basis, ensure_ascii=False, sort_keys=True, default=str)
270
+ return hashlib.sha256(blob.encode("utf-8")).hexdigest()
271
+
272
+
273
+ def flow_digest(flow):
274
+ """流程定义的内容指纹(sha256 前 16 位)——「这个任务当时用的是哪一版流程」
275
+ 的对账真源(借鉴 WorkDSH ADR-0010 发布后不可变修订)。"""
276
+ return flow_content_sha256(flow)[:16]
277
+
278
+
279
+ def flow_drift(task):
280
+ """任务流程修订漂移检测:任务创建时钉的 digest vs 当前流程定义。
281
+
282
+ 任务参数在创建时已固化(改流程不影响固化字段),但 task_compile 对历史
283
+ 任务缺失字段(threshold/rubric/manuscript)仍回落当前流程定义——续跑/
284
+ 重试因此可能静默吃到新流程。这里只诊断不裁定:返回
285
+ {"flow", "pinned", "current"} 或 None(无快照/流程已删/无漂移)。"""
286
+ snap = (task or {}).get("flow_snapshot") or {}
287
+ pinned = str(snap.get("digest") or "")
288
+ if not pinned:
289
+ return None
290
+ cur = get_flow(str((task or {}).get("type") or ""))
291
+ if not cur:
292
+ return None
293
+ current = flow_digest(cur)
294
+ if not current or current == pinned:
295
+ return None
296
+ return {"flow": cur.get("id") or "", "pinned": pinned, "current": current}
297
+
298
+
299
+ def upsert_flow(payload):
300
+ """新增/更新流程。返回 (flow, 错误)。
301
+
302
+ - 预置流程 id:写入 overrides(可编辑、可恢复默认);
303
+ - 新 id:创建自定义流程——**只需 name + engine**,其余字段留空走引擎默认。
304
+ """
305
+ fid = str(payload.get("id") or "").strip()
306
+ name = str(payload.get("name") or "").strip()
307
+ engine = str(payload.get("engine") or "review").strip()
308
+ if not _ID_RE.match(fid):
309
+ return None, "流程 ID 只能是小写字母开头的字母/数字/-/_(≤32 位)"
310
+ if engine not in ENGINES:
311
+ return None, "engine 必须是 code、review 或 direct"
312
+ builtin_ids = {b["id"] for b in BUILTIN_FLOWS}
313
+
314
+ if fid in builtin_ids:
315
+ base = next(b for b in BUILTIN_FLOWS if b["id"] == fid)
316
+ if name and name != base["name"]:
317
+ pass # 允许改名
318
+ ov = dict(payload)
319
+ ov.pop("id", None)
320
+ ov["engine"] = base["engine"] # 引擎不可改(避免语义错乱)
321
+ merged = _apply_overrides(base, ov)
322
+ with _LOCK:
323
+ data = _read()
324
+ ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
325
+ ovs[fid] = {k: merged[k] for k in _EDITABLE if k in merged}
326
+ data["overrides"] = ovs
327
+ _write(data)
328
+ merged["edited"] = True
329
+ return merged, None
330
+
331
+ if not name:
332
+ return None, "名称不能为空"
333
+ flow = {"id": fid, "name": name[:20], "icon": str(payload.get("icon") or "✨")[:4],
334
+ "engine": engine, "builtin": False,
335
+ "goal_hint": str(payload.get("goal_hint") or "")[:80],
336
+ "note": str(payload.get("note") or "")[:80]}
337
+ if engine == "review":
338
+ dflt = ENGINE_DEFAULTS["review"]
339
+ ms = re.sub(r"[\\/]+", "_", str(payload.get("manuscript") or (fid + ".md"))).strip()
340
+ ms = re.sub(r"\.{2,}", "_", ms).lstrip(".") or (fid + ".md")
341
+ flow["manuscript"] = ms
342
+ flow["rubric"] = _norm_rubric(payload.get("rubric")) or list(dflt["rubric"])
343
+ try:
344
+ flow["threshold"] = max(1.0, min(10.0, float(payload.get("threshold") or dflt["threshold"])))
345
+ except Exception:
346
+ flow["threshold"] = dflt["threshold"]
347
+ try:
348
+ flow["rounds"] = max(1, min(5, int(payload.get("rounds") or dflt["rounds"])))
349
+ except Exception:
350
+ flow["rounds"] = dflt["rounds"]
351
+ try:
352
+ flow["best_of"] = max(1, min(3, int(payload.get("best_of") or dflt.get("best_of") or 1)))
353
+ except Exception:
354
+ flow["best_of"] = 1
355
+ serial = _norm_serial(payload.get("serial"))
356
+ if serial:
357
+ flow["serial"] = serial
358
+ for key in ("draft_prompt", "critique_prompt"):
359
+ v = str(payload.get(key) or "").strip()
360
+ if v:
361
+ flow[key] = v[:4000]
362
+ elif engine == "code":
363
+ flow["verify_command"] = str(payload.get("verify_command") or "")[:200]
364
+ # direct:无流程参数(目标+附件即全部输入)
365
+ with _LOCK:
366
+ data = _read()
367
+ flows = data.get("flows") if isinstance(data.get("flows"), list) else []
368
+ flows = [f for f in flows if isinstance(f, dict) and f.get("id") != fid]
369
+ flows.append(flow)
370
+ data["flows"] = flows
371
+ _write(data)
372
+ return flow, None
373
+
374
+
375
+ def delete_flow(flow_id):
376
+ """删除自定义流程;预置流程只能「恢复默认」不能删除。"""
377
+ if flow_id in {b["id"] for b in BUILTIN_FLOWS}:
378
+ return "预置流程不可删除(可点「恢复默认」回滚改动)"
379
+ with _LOCK:
380
+ data = _read()
381
+ flows = data.get("flows") if isinstance(data.get("flows"), list) else []
382
+ kept = [f for f in flows if not (isinstance(f, dict) and f.get("id") == flow_id)]
383
+ if len(kept) == len(flows):
384
+ return "流程不存在"
385
+ data["flows"] = kept
386
+ _write(data)
387
+ return None
388
+
389
+
390
+ def reset_flow(flow_id):
391
+ """预置流程恢复默认(清掉 overrides)。返回错误或 None。"""
392
+ if flow_id not in {b["id"] for b in BUILTIN_FLOWS}:
393
+ return "只有预置流程支持恢复默认"
394
+ with _LOCK:
395
+ data = _read()
396
+ ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
397
+ if flow_id in ovs:
398
+ ovs.pop(flow_id, None)
399
+ data["overrides"] = ovs
400
+ _write(data)
401
+ return None