codebee 0.1.3 → 0.1.5
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 +36 -0
- package/README.md +9 -0
- package/app/core/attachments.py +40 -0
- package/app/core/bookmeta.py +157 -52
- package/app/core/bookmeta_catalog.py +67 -90
- package/app/core/builtin_agent.py +564 -0
- package/app/core/flows.py +328 -328
- package/app/core/gitmod.py +82 -8
- package/app/core/history.py +8 -2
- package/app/core/jobs.py +448 -424
- package/app/core/manager.py +151 -1
- package/app/core/modelhub.py +86 -4
- package/app/core/pipeline.py +2326 -2164
- package/app/core/router.py +8 -3
- package/app/core/runner.py +185 -21
- package/app/core/selfupdate.py +54 -11
- package/app/core/store.py +4 -2
- package/app/main.py +117 -12
- package/app/ui/app.js +279 -47
- package/app/ui/i18n.js +21 -5
- package/app/ui/index.html +6 -2
- package/app/ui/style.css +3379 -2764
- package/package.json +2 -1
package/app/core/flows.py
CHANGED
|
@@ -1,328 +1,328 @@
|
|
|
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
|
|
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},
|
|
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": "
|
|
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": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
|
|
78
|
-
"manuscript": "report.md",
|
|
79
|
-
"rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
|
|
80
|
-
"threshold": 7.0, "rounds": 2,
|
|
81
|
-
"goal_hint": "调研什么问题、产出给谁用",
|
|
82
|
-
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
83
|
-
{"id": "speech", "name": "演讲稿", "icon": "i-mic", "engine": "review", "builtin": True,
|
|
84
|
-
"manuscript": "speech.md",
|
|
85
|
-
"rubric": ["主题聚焦", "结构", "感染力", "语言风格"],
|
|
86
|
-
"threshold": 7.0, "rounds": 2,
|
|
87
|
-
"goal_hint": "演讲场合 / 听众 / 时长 / 核心信息",
|
|
88
|
-
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
89
|
-
{"id": "weekly_report", "name": "工作汇报", "icon": "i-clip-check", "engine": "review", "builtin": True,
|
|
90
|
-
"manuscript": "weekly.md",
|
|
91
|
-
"rubric": ["重点突出", "数据支撑", "结构清晰", "下一步可执行"],
|
|
92
|
-
"threshold": 7.0, "rounds": 2,
|
|
93
|
-
"goal_hint": "周报/月报/述职?汇报给谁、做了什么(可贴流水账让小队提炼)",
|
|
94
|
-
"note": "周报/月报/述职材料:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
95
|
-
{"id": "email", "name": "商务邮件", "icon": "i-mail", "engine": "review", "builtin": True,
|
|
96
|
-
"manuscript": "email.md",
|
|
97
|
-
"rubric": ["目的明确", "语气得体", "信息完整", "简洁度"],
|
|
98
|
-
"threshold": 7.0, "rounds": 2,
|
|
99
|
-
"goal_hint": "给谁写、要达成什么、对方背景与你们的关系",
|
|
100
|
-
"note": "商务/客户/跨部门邮件:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
101
|
-
{"id": "tech_proposal", "name": "技术方案", "icon": "i-blocks", "engine": "review", "builtin": True,
|
|
102
|
-
"manuscript": "proposal.md",
|
|
103
|
-
"rubric": ["可行性", "方案完整性", "风险识别", "成本与收益"],
|
|
104
|
-
"threshold": 7.0, "rounds": 2,
|
|
105
|
-
"goal_hint": "要解决什么问题、约束条件(工期/技术栈/预算)、给谁评审",
|
|
106
|
-
"note": "技术选型/架构/实施方案:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
107
|
-
{"id": "resume", "name": "简历", "icon": "i-idcard", "engine": "review", "builtin": True,
|
|
108
|
-
"manuscript": "resume.md",
|
|
109
|
-
"rubric": ["真实可信", "岗位匹配", "成果量化", "关键词覆盖", "简洁度"],
|
|
110
|
-
"threshold": 7.0, "rounds": 2,
|
|
111
|
-
"goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
|
|
112
|
-
"note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
|
116
|
-
# 可编辑字段(预置流程的 overrides 与自定义流程共用同一套规范化)。
|
|
117
|
-
# icon 刻意不进来:预置图标跟版本走(老 overrides 里钉的旧 emoji 会被忽略),
|
|
118
|
-
# 自定义流程的 icon 在 upsert_flow 里直接落盘,不走 overrides。
|
|
119
|
-
_EDITABLE = ("name", "goal_hint", "note", "manuscript", "rubric",
|
|
120
|
-
"threshold", "rounds", "serial", "draft_prompt", "critique_prompt",
|
|
121
|
-
"verify_command")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _read():
|
|
125
|
-
try:
|
|
126
|
-
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
127
|
-
return data if isinstance(data, dict) else {}
|
|
128
|
-
except Exception:
|
|
129
|
-
return {}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _write(data):
|
|
133
|
-
_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
-
tmp = _FILE.with_suffix(".tmp")
|
|
135
|
-
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
136
|
-
tmp.replace(_FILE)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _norm_serial(serial):
|
|
140
|
-
if not isinstance(serial, dict) or not serial.get("chapters"):
|
|
141
|
-
return None
|
|
142
|
-
try:
|
|
143
|
-
return {"chapters": max(2, min(20, int(serial["chapters"]))),
|
|
144
|
-
"words_per_chapter": max(500, min(8000, int(serial.get("words_per_chapter") or 2500)))}
|
|
145
|
-
except Exception:
|
|
146
|
-
return None
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _norm_rubric(rubric):
|
|
150
|
-
if isinstance(rubric, str):
|
|
151
|
-
rubric = re.split(r"[,,、\n]+", rubric)
|
|
152
|
-
if not isinstance(rubric, list):
|
|
153
|
-
return None
|
|
154
|
-
out = [str(d).strip() for d in rubric if str(d).strip()][:8]
|
|
155
|
-
return out or None
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _apply_overrides(base, ov):
|
|
159
|
-
"""把 overrides 合并到预置流程上(只认白名单字段)。"""
|
|
160
|
-
f = dict(base)
|
|
161
|
-
if not isinstance(ov, dict):
|
|
162
|
-
return f
|
|
163
|
-
for k in _EDITABLE:
|
|
164
|
-
if k not in ov:
|
|
165
|
-
continue
|
|
166
|
-
v = ov[k]
|
|
167
|
-
if k == "rubric":
|
|
168
|
-
v = _norm_rubric(v)
|
|
169
|
-
if v:
|
|
170
|
-
f["rubric"] = v
|
|
171
|
-
elif k == "serial":
|
|
172
|
-
v = _norm_serial(v)
|
|
173
|
-
if v:
|
|
174
|
-
f["serial"] = v
|
|
175
|
-
else:
|
|
176
|
-
f.pop("serial", None)
|
|
177
|
-
elif k == "threshold":
|
|
178
|
-
try:
|
|
179
|
-
f["threshold"] = max(1.0, min(10.0, float(v)))
|
|
180
|
-
except Exception:
|
|
181
|
-
pass
|
|
182
|
-
elif k == "rounds":
|
|
183
|
-
try:
|
|
184
|
-
f["rounds"] = max(1, min(5, int(v)))
|
|
185
|
-
except Exception:
|
|
186
|
-
pass
|
|
187
|
-
elif k in ("manuscript", "verify_command"):
|
|
188
|
-
s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
|
|
189
|
-
s = re.sub(r"\.{2,}", "_", s).lstrip(".")
|
|
190
|
-
if s:
|
|
191
|
-
f[k] = s
|
|
192
|
-
elif k in ("draft_prompt", "critique_prompt"):
|
|
193
|
-
s = str(v or "").strip()
|
|
194
|
-
if s:
|
|
195
|
-
f[k] = s[:4000]
|
|
196
|
-
else:
|
|
197
|
-
f.pop(k, None)
|
|
198
|
-
else:
|
|
199
|
-
f[k] = str(v or "")[:80]
|
|
200
|
-
return f
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _custom():
|
|
204
|
-
data = _read().get("flows")
|
|
205
|
-
return [f for f in data if isinstance(f, dict) and f.get("id")] if isinstance(data, list) else []
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _overrides():
|
|
209
|
-
ov = _read().get("overrides")
|
|
210
|
-
return ov if isinstance(ov, dict) else {}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def list_flows():
|
|
214
|
-
"""自定义流程在前(便于看到新增),预置流程按定义顺序,均已套用 overrides。"""
|
|
215
|
-
with _LOCK:
|
|
216
|
-
custom, ov = _custom(), _overrides()
|
|
217
|
-
builtin = [_apply_overrides(b, ov.get(b["id"])) for b in BUILTIN_FLOWS]
|
|
218
|
-
for f in builtin:
|
|
219
|
-
f["edited"] = bool(ov.get(f["id"]))
|
|
220
|
-
return custom + builtin
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def get_flow(flow_id):
|
|
224
|
-
for f in list_flows():
|
|
225
|
-
if f["id"] == flow_id:
|
|
226
|
-
return f
|
|
227
|
-
return None
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def upsert_flow(payload):
|
|
231
|
-
"""新增/更新流程。返回 (flow, 错误)。
|
|
232
|
-
|
|
233
|
-
- 预置流程 id:写入 overrides(可编辑、可恢复默认);
|
|
234
|
-
- 新 id:创建自定义流程——**只需 name + engine**,其余字段留空走引擎默认。
|
|
235
|
-
"""
|
|
236
|
-
fid = str(payload.get("id") or "").strip()
|
|
237
|
-
name = str(payload.get("name") or "").strip()
|
|
238
|
-
engine = str(payload.get("engine") or "review").strip()
|
|
239
|
-
if not _ID_RE.match(fid):
|
|
240
|
-
return None, "流程 ID 只能是小写字母开头的字母/数字/-/_(≤32 位)"
|
|
241
|
-
if engine not in ENGINES:
|
|
242
|
-
return None, "engine 必须是 code、review 或 direct"
|
|
243
|
-
builtin_ids = {b["id"] for b in BUILTIN_FLOWS}
|
|
244
|
-
|
|
245
|
-
if fid in builtin_ids:
|
|
246
|
-
base = next(b for b in BUILTIN_FLOWS if b["id"] == fid)
|
|
247
|
-
if name and name != base["name"]:
|
|
248
|
-
pass # 允许改名
|
|
249
|
-
ov = dict(payload)
|
|
250
|
-
ov.pop("id", None)
|
|
251
|
-
ov["engine"] = base["engine"] # 引擎不可改(避免语义错乱)
|
|
252
|
-
merged = _apply_overrides(base, ov)
|
|
253
|
-
with _LOCK:
|
|
254
|
-
data = _read()
|
|
255
|
-
ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
|
|
256
|
-
ovs[fid] = {k: merged[k] for k in _EDITABLE if k in merged}
|
|
257
|
-
data["overrides"] = ovs
|
|
258
|
-
_write(data)
|
|
259
|
-
merged["edited"] = True
|
|
260
|
-
return merged, None
|
|
261
|
-
|
|
262
|
-
if not name:
|
|
263
|
-
return None, "名称不能为空"
|
|
264
|
-
flow = {"id": fid, "name": name[:20], "icon": str(payload.get("icon") or "✨")[:4],
|
|
265
|
-
"engine": engine, "builtin": False,
|
|
266
|
-
"goal_hint": str(payload.get("goal_hint") or "")[:80],
|
|
267
|
-
"note": str(payload.get("note") or "")[:80]}
|
|
268
|
-
if engine == "review":
|
|
269
|
-
dflt = ENGINE_DEFAULTS["review"]
|
|
270
|
-
ms = re.sub(r"[\\/]+", "_", str(payload.get("manuscript") or (fid + ".md"))).strip()
|
|
271
|
-
ms = re.sub(r"\.{2,}", "_", ms).lstrip(".") or (fid + ".md")
|
|
272
|
-
flow["manuscript"] = ms
|
|
273
|
-
flow["rubric"] = _norm_rubric(payload.get("rubric")) or list(dflt["rubric"])
|
|
274
|
-
try:
|
|
275
|
-
flow["threshold"] = max(1.0, min(10.0, float(payload.get("threshold") or dflt["threshold"])))
|
|
276
|
-
except Exception:
|
|
277
|
-
flow["threshold"] = dflt["threshold"]
|
|
278
|
-
try:
|
|
279
|
-
flow["rounds"] = max(1, min(5, int(payload.get("rounds") or dflt["rounds"])))
|
|
280
|
-
except Exception:
|
|
281
|
-
flow["rounds"] = dflt["rounds"]
|
|
282
|
-
serial = _norm_serial(payload.get("serial"))
|
|
283
|
-
if serial:
|
|
284
|
-
flow["serial"] = serial
|
|
285
|
-
for key in ("draft_prompt", "critique_prompt"):
|
|
286
|
-
v = str(payload.get(key) or "").strip()
|
|
287
|
-
if v:
|
|
288
|
-
flow[key] = v[:4000]
|
|
289
|
-
elif engine == "code":
|
|
290
|
-
flow["verify_command"] = str(payload.get("verify_command") or "")[:200]
|
|
291
|
-
# direct:无流程参数(目标+附件即全部输入)
|
|
292
|
-
with _LOCK:
|
|
293
|
-
data = _read()
|
|
294
|
-
flows = data.get("flows") if isinstance(data.get("flows"), list) else []
|
|
295
|
-
flows = [f for f in flows if isinstance(f, dict) and f.get("id") != fid]
|
|
296
|
-
flows.append(flow)
|
|
297
|
-
data["flows"] = flows
|
|
298
|
-
_write(data)
|
|
299
|
-
return flow, None
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def delete_flow(flow_id):
|
|
303
|
-
"""删除自定义流程;预置流程只能「恢复默认」不能删除。"""
|
|
304
|
-
if flow_id in {b["id"] for b in BUILTIN_FLOWS}:
|
|
305
|
-
return "预置流程不可删除(可点「恢复默认」回滚改动)"
|
|
306
|
-
with _LOCK:
|
|
307
|
-
data = _read()
|
|
308
|
-
flows = data.get("flows") if isinstance(data.get("flows"), list) else []
|
|
309
|
-
kept = [f for f in flows if not (isinstance(f, dict) and f.get("id") == flow_id)]
|
|
310
|
-
if len(kept) == len(flows):
|
|
311
|
-
return "流程不存在"
|
|
312
|
-
data["flows"] = kept
|
|
313
|
-
_write(data)
|
|
314
|
-
return None
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def reset_flow(flow_id):
|
|
318
|
-
"""预置流程恢复默认(清掉 overrides)。返回错误或 None。"""
|
|
319
|
-
if flow_id not in {b["id"] for b in BUILTIN_FLOWS}:
|
|
320
|
-
return "只有预置流程支持恢复默认"
|
|
321
|
-
with _LOCK:
|
|
322
|
-
data = _read()
|
|
323
|
-
ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
|
|
324
|
-
if flow_id in ovs:
|
|
325
|
-
ovs.pop(flow_id, None)
|
|
326
|
-
data["overrides"] = ovs
|
|
327
|
-
_write(data)
|
|
328
|
-
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 json
|
|
13
|
+
import re
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
from . import paths
|
|
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},
|
|
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": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
|
|
78
|
+
"manuscript": "report.md",
|
|
79
|
+
"rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
|
|
80
|
+
"threshold": 7.0, "rounds": 2,
|
|
81
|
+
"goal_hint": "调研什么问题、产出给谁用",
|
|
82
|
+
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
83
|
+
{"id": "speech", "name": "演讲稿", "icon": "i-mic", "engine": "review", "builtin": True,
|
|
84
|
+
"manuscript": "speech.md",
|
|
85
|
+
"rubric": ["主题聚焦", "结构", "感染力", "语言风格"],
|
|
86
|
+
"threshold": 7.0, "rounds": 2,
|
|
87
|
+
"goal_hint": "演讲场合 / 听众 / 时长 / 核心信息",
|
|
88
|
+
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
89
|
+
{"id": "weekly_report", "name": "工作汇报", "icon": "i-clip-check", "engine": "review", "builtin": True,
|
|
90
|
+
"manuscript": "weekly.md",
|
|
91
|
+
"rubric": ["重点突出", "数据支撑", "结构清晰", "下一步可执行"],
|
|
92
|
+
"threshold": 7.0, "rounds": 2,
|
|
93
|
+
"goal_hint": "周报/月报/述职?汇报给谁、做了什么(可贴流水账让小队提炼)",
|
|
94
|
+
"note": "周报/月报/述职材料:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
95
|
+
{"id": "email", "name": "商务邮件", "icon": "i-mail", "engine": "review", "builtin": True,
|
|
96
|
+
"manuscript": "email.md",
|
|
97
|
+
"rubric": ["目的明确", "语气得体", "信息完整", "简洁度"],
|
|
98
|
+
"threshold": 7.0, "rounds": 2,
|
|
99
|
+
"goal_hint": "给谁写、要达成什么、对方背景与你们的关系",
|
|
100
|
+
"note": "商务/客户/跨部门邮件:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
101
|
+
{"id": "tech_proposal", "name": "技术方案", "icon": "i-blocks", "engine": "review", "builtin": True,
|
|
102
|
+
"manuscript": "proposal.md",
|
|
103
|
+
"rubric": ["可行性", "方案完整性", "风险识别", "成本与收益"],
|
|
104
|
+
"threshold": 7.0, "rounds": 2,
|
|
105
|
+
"goal_hint": "要解决什么问题、约束条件(工期/技术栈/预算)、给谁评审",
|
|
106
|
+
"note": "技术选型/架构/实施方案:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
107
|
+
{"id": "resume", "name": "简历", "icon": "i-idcard", "engine": "review", "builtin": True,
|
|
108
|
+
"manuscript": "resume.md",
|
|
109
|
+
"rubric": ["真实可信", "岗位匹配", "成果量化", "关键词覆盖", "简洁度"],
|
|
110
|
+
"threshold": 7.0, "rounds": 2,
|
|
111
|
+
"goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
|
|
112
|
+
"note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
|
116
|
+
# 可编辑字段(预置流程的 overrides 与自定义流程共用同一套规范化)。
|
|
117
|
+
# icon 刻意不进来:预置图标跟版本走(老 overrides 里钉的旧 emoji 会被忽略),
|
|
118
|
+
# 自定义流程的 icon 在 upsert_flow 里直接落盘,不走 overrides。
|
|
119
|
+
_EDITABLE = ("name", "goal_hint", "note", "manuscript", "rubric",
|
|
120
|
+
"threshold", "rounds", "serial", "draft_prompt", "critique_prompt",
|
|
121
|
+
"verify_command")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read():
|
|
125
|
+
try:
|
|
126
|
+
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
127
|
+
return data if isinstance(data, dict) else {}
|
|
128
|
+
except Exception:
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _write(data):
|
|
133
|
+
_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
tmp = _FILE.with_suffix(".tmp")
|
|
135
|
+
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
136
|
+
tmp.replace(_FILE)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _norm_serial(serial):
|
|
140
|
+
if not isinstance(serial, dict) or not serial.get("chapters"):
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
return {"chapters": max(2, min(20, int(serial["chapters"]))),
|
|
144
|
+
"words_per_chapter": max(500, min(8000, int(serial.get("words_per_chapter") or 2500)))}
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _norm_rubric(rubric):
|
|
150
|
+
if isinstance(rubric, str):
|
|
151
|
+
rubric = re.split(r"[,,、\n]+", rubric)
|
|
152
|
+
if not isinstance(rubric, list):
|
|
153
|
+
return None
|
|
154
|
+
out = [str(d).strip() for d in rubric if str(d).strip()][:8]
|
|
155
|
+
return out or None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _apply_overrides(base, ov):
|
|
159
|
+
"""把 overrides 合并到预置流程上(只认白名单字段)。"""
|
|
160
|
+
f = dict(base)
|
|
161
|
+
if not isinstance(ov, dict):
|
|
162
|
+
return f
|
|
163
|
+
for k in _EDITABLE:
|
|
164
|
+
if k not in ov:
|
|
165
|
+
continue
|
|
166
|
+
v = ov[k]
|
|
167
|
+
if k == "rubric":
|
|
168
|
+
v = _norm_rubric(v)
|
|
169
|
+
if v:
|
|
170
|
+
f["rubric"] = v
|
|
171
|
+
elif k == "serial":
|
|
172
|
+
v = _norm_serial(v)
|
|
173
|
+
if v:
|
|
174
|
+
f["serial"] = v
|
|
175
|
+
else:
|
|
176
|
+
f.pop("serial", None)
|
|
177
|
+
elif k == "threshold":
|
|
178
|
+
try:
|
|
179
|
+
f["threshold"] = max(1.0, min(10.0, float(v)))
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
elif k == "rounds":
|
|
183
|
+
try:
|
|
184
|
+
f["rounds"] = max(1, min(5, int(v)))
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
elif k in ("manuscript", "verify_command"):
|
|
188
|
+
s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
|
|
189
|
+
s = re.sub(r"\.{2,}", "_", s).lstrip(".")
|
|
190
|
+
if s:
|
|
191
|
+
f[k] = s
|
|
192
|
+
elif k in ("draft_prompt", "critique_prompt"):
|
|
193
|
+
s = str(v or "").strip()
|
|
194
|
+
if s:
|
|
195
|
+
f[k] = s[:4000]
|
|
196
|
+
else:
|
|
197
|
+
f.pop(k, None)
|
|
198
|
+
else:
|
|
199
|
+
f[k] = str(v or "")[:80]
|
|
200
|
+
return f
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _custom():
|
|
204
|
+
data = _read().get("flows")
|
|
205
|
+
return [f for f in data if isinstance(f, dict) and f.get("id")] if isinstance(data, list) else []
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _overrides():
|
|
209
|
+
ov = _read().get("overrides")
|
|
210
|
+
return ov if isinstance(ov, dict) else {}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def list_flows():
|
|
214
|
+
"""自定义流程在前(便于看到新增),预置流程按定义顺序,均已套用 overrides。"""
|
|
215
|
+
with _LOCK:
|
|
216
|
+
custom, ov = _custom(), _overrides()
|
|
217
|
+
builtin = [_apply_overrides(b, ov.get(b["id"])) for b in BUILTIN_FLOWS]
|
|
218
|
+
for f in builtin:
|
|
219
|
+
f["edited"] = bool(ov.get(f["id"]))
|
|
220
|
+
return custom + builtin
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_flow(flow_id):
|
|
224
|
+
for f in list_flows():
|
|
225
|
+
if f["id"] == flow_id:
|
|
226
|
+
return f
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def upsert_flow(payload):
|
|
231
|
+
"""新增/更新流程。返回 (flow, 错误)。
|
|
232
|
+
|
|
233
|
+
- 预置流程 id:写入 overrides(可编辑、可恢复默认);
|
|
234
|
+
- 新 id:创建自定义流程——**只需 name + engine**,其余字段留空走引擎默认。
|
|
235
|
+
"""
|
|
236
|
+
fid = str(payload.get("id") or "").strip()
|
|
237
|
+
name = str(payload.get("name") or "").strip()
|
|
238
|
+
engine = str(payload.get("engine") or "review").strip()
|
|
239
|
+
if not _ID_RE.match(fid):
|
|
240
|
+
return None, "流程 ID 只能是小写字母开头的字母/数字/-/_(≤32 位)"
|
|
241
|
+
if engine not in ENGINES:
|
|
242
|
+
return None, "engine 必须是 code、review 或 direct"
|
|
243
|
+
builtin_ids = {b["id"] for b in BUILTIN_FLOWS}
|
|
244
|
+
|
|
245
|
+
if fid in builtin_ids:
|
|
246
|
+
base = next(b for b in BUILTIN_FLOWS if b["id"] == fid)
|
|
247
|
+
if name and name != base["name"]:
|
|
248
|
+
pass # 允许改名
|
|
249
|
+
ov = dict(payload)
|
|
250
|
+
ov.pop("id", None)
|
|
251
|
+
ov["engine"] = base["engine"] # 引擎不可改(避免语义错乱)
|
|
252
|
+
merged = _apply_overrides(base, ov)
|
|
253
|
+
with _LOCK:
|
|
254
|
+
data = _read()
|
|
255
|
+
ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
|
|
256
|
+
ovs[fid] = {k: merged[k] for k in _EDITABLE if k in merged}
|
|
257
|
+
data["overrides"] = ovs
|
|
258
|
+
_write(data)
|
|
259
|
+
merged["edited"] = True
|
|
260
|
+
return merged, None
|
|
261
|
+
|
|
262
|
+
if not name:
|
|
263
|
+
return None, "名称不能为空"
|
|
264
|
+
flow = {"id": fid, "name": name[:20], "icon": str(payload.get("icon") or "✨")[:4],
|
|
265
|
+
"engine": engine, "builtin": False,
|
|
266
|
+
"goal_hint": str(payload.get("goal_hint") or "")[:80],
|
|
267
|
+
"note": str(payload.get("note") or "")[:80]}
|
|
268
|
+
if engine == "review":
|
|
269
|
+
dflt = ENGINE_DEFAULTS["review"]
|
|
270
|
+
ms = re.sub(r"[\\/]+", "_", str(payload.get("manuscript") or (fid + ".md"))).strip()
|
|
271
|
+
ms = re.sub(r"\.{2,}", "_", ms).lstrip(".") or (fid + ".md")
|
|
272
|
+
flow["manuscript"] = ms
|
|
273
|
+
flow["rubric"] = _norm_rubric(payload.get("rubric")) or list(dflt["rubric"])
|
|
274
|
+
try:
|
|
275
|
+
flow["threshold"] = max(1.0, min(10.0, float(payload.get("threshold") or dflt["threshold"])))
|
|
276
|
+
except Exception:
|
|
277
|
+
flow["threshold"] = dflt["threshold"]
|
|
278
|
+
try:
|
|
279
|
+
flow["rounds"] = max(1, min(5, int(payload.get("rounds") or dflt["rounds"])))
|
|
280
|
+
except Exception:
|
|
281
|
+
flow["rounds"] = dflt["rounds"]
|
|
282
|
+
serial = _norm_serial(payload.get("serial"))
|
|
283
|
+
if serial:
|
|
284
|
+
flow["serial"] = serial
|
|
285
|
+
for key in ("draft_prompt", "critique_prompt"):
|
|
286
|
+
v = str(payload.get(key) or "").strip()
|
|
287
|
+
if v:
|
|
288
|
+
flow[key] = v[:4000]
|
|
289
|
+
elif engine == "code":
|
|
290
|
+
flow["verify_command"] = str(payload.get("verify_command") or "")[:200]
|
|
291
|
+
# direct:无流程参数(目标+附件即全部输入)
|
|
292
|
+
with _LOCK:
|
|
293
|
+
data = _read()
|
|
294
|
+
flows = data.get("flows") if isinstance(data.get("flows"), list) else []
|
|
295
|
+
flows = [f for f in flows if isinstance(f, dict) and f.get("id") != fid]
|
|
296
|
+
flows.append(flow)
|
|
297
|
+
data["flows"] = flows
|
|
298
|
+
_write(data)
|
|
299
|
+
return flow, None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def delete_flow(flow_id):
|
|
303
|
+
"""删除自定义流程;预置流程只能「恢复默认」不能删除。"""
|
|
304
|
+
if flow_id in {b["id"] for b in BUILTIN_FLOWS}:
|
|
305
|
+
return "预置流程不可删除(可点「恢复默认」回滚改动)"
|
|
306
|
+
with _LOCK:
|
|
307
|
+
data = _read()
|
|
308
|
+
flows = data.get("flows") if isinstance(data.get("flows"), list) else []
|
|
309
|
+
kept = [f for f in flows if not (isinstance(f, dict) and f.get("id") == flow_id)]
|
|
310
|
+
if len(kept) == len(flows):
|
|
311
|
+
return "流程不存在"
|
|
312
|
+
data["flows"] = kept
|
|
313
|
+
_write(data)
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def reset_flow(flow_id):
|
|
318
|
+
"""预置流程恢复默认(清掉 overrides)。返回错误或 None。"""
|
|
319
|
+
if flow_id not in {b["id"] for b in BUILTIN_FLOWS}:
|
|
320
|
+
return "只有预置流程支持恢复默认"
|
|
321
|
+
with _LOCK:
|
|
322
|
+
data = _read()
|
|
323
|
+
ovs = data.get("overrides") if isinstance(data.get("overrides"), dict) else {}
|
|
324
|
+
if flow_id in ovs:
|
|
325
|
+
ovs.pop(flow_id, None)
|
|
326
|
+
data["overrides"] = ovs
|
|
327
|
+
_write(data)
|
|
328
|
+
return None
|