codebee 0.1.4 → 0.1.6
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 +44 -0
- package/README.md +8 -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 +190 -8
- package/app/core/flows.py +328 -328
- package/app/core/gitmod.py +82 -8
- package/app/core/health.py +55 -1
- package/app/core/history.py +8 -2
- package/app/core/jobs.py +19 -4
- package/app/core/manager.py +159 -1
- package/app/core/modelhub.py +148 -15
- package/app/core/pipeline.py +2392 -2267
- package/app/core/registry.py +4 -0
- package/app/core/router.py +13 -7
- package/app/core/runner.py +194 -21
- package/app/core/selfupdate.py +60 -11
- package/app/core/store.py +4 -2
- package/app/main.py +101 -7
- package/app/ui/app.js +447 -115
- package/app/ui/i18n.js +32 -10
- package/app/ui/icons/bee.svg +79 -0
- package/app/ui/index.html +8 -3
- package/app/ui/style.css +3226 -2764
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,并同步 README 里的
|
|
4
|
+
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
|
+
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
|
+
|
|
7
|
+
## v0.1.6(2026-09-18)
|
|
8
|
+
|
|
9
|
+
- 连载韧性:网关限流/欠费时章稿失败先退避重试同作者(保住连载风格一致),重试穷尽才判死整 run;自动续跑改为延迟 5 分钟入队、上限提升到 3 次
|
|
10
|
+
- 修复:任务分支从游离 HEAD 检出时「切回原分支」原地空转的假成功;游离基线的合并现在显式拒绝并说明
|
|
11
|
+
- kimi 接入:僵尸 kimi 进程自动检测清理;CodeBee 托管配置按 TOML 语义渲染
|
|
12
|
+
- 修复:generic 类 CLI 经 npm .cmd 垫片时提示词被 cmd 重解析截烂(改 node 直启)
|
|
13
|
+
- 界面:新增蜜蜂图标,蜂巢工作台与皮肤视觉精修
|
|
14
|
+
|
|
15
|
+
## v0.1.5(2026-09-16)
|
|
16
|
+
|
|
17
|
+
- 更新提示升级:发现新版本时直接展示「新版本更新内容」,升级后首次打开自动弹一次「本次更新内容」
|
|
18
|
+
- 新增 CHANGELOG.md 并随包分发
|
|
19
|
+
|
|
20
|
+
## v0.1.4(2026-09-16)
|
|
21
|
+
|
|
22
|
+
- 修复:部分安全软件环境下启动卡在「正在启动任务队列」——worker 线程改为首次有任务时再创建,启动期不再创建任何线程
|
|
23
|
+
- 启动任一阶段卡超过 20 秒会自动打印各线程卡点堆栈(就绪后自动关闭),截图即可定位问题
|
|
24
|
+
|
|
25
|
+
## v0.1.3(2026-09-16)
|
|
26
|
+
|
|
27
|
+
- 启动过程逐步骤显示进度与耗时,慢硬盘/杀软冷启动不再像卡死
|
|
28
|
+
- 服务就绪后明确提示「不要关闭本窗口」
|
|
29
|
+
|
|
30
|
+
## v0.1.2(2026-09-16)
|
|
31
|
+
|
|
32
|
+
- 源码仓库地址改为 github.com/Vercel-By-WXP/CodeBee(npm 元数据同步)
|
|
33
|
+
|
|
34
|
+
## v0.1.1(2026-09-16)
|
|
35
|
+
|
|
36
|
+
- 品牌 CodeBee 化:任务分支前缀 `codebee/<任务ID>`(老 `tutti/` 分支仍可合并/丢弃),git 提交身份改为 CodeBee
|
|
37
|
+
- 数据目录:新装落 `%APPDATA%\CodeBee`(macOS/Linux `~/.codebee`),老 Tutti 目录自动沿用零迁移
|
|
38
|
+
- 启动横幅与失败排障指引(多 Python 机器可用 `TUTTI_PYTHON` 切换)
|
|
39
|
+
- README 重写快速开始:npm 安装为一等路径
|
|
40
|
+
|
|
41
|
+
## v0.1.0(2026-09-16)
|
|
42
|
+
|
|
43
|
+
- npm 首发完成:`npm install -g codebee` 一条命令即用,自带自更新
|
|
44
|
+
- 多智能体编排台全家桶:目标拆解/智能路由/执行/验证/跨厂商评审/连载/经验库/插件市场
|
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
|
|
21
21
|
零第三方依赖:纯 Python 标准库(3.8+),本地 Web 界面,数据全部落盘可回放。
|
|
22
22
|
|
|
23
|
+
<!-- relnotes:start -->
|
|
24
|
+
### 最新版更新内容(v0.1.6)
|
|
25
|
+
|
|
26
|
+
- 连载更抗限流:章稿失败先退避重试同作者,自动续跑延迟 5 分钟、上限 3 次
|
|
27
|
+
- 修复任务分支游离 HEAD 的假成功切换与合并;kimi 僵尸进程检测与配置渲染修复
|
|
28
|
+
- 修复 generic CLI 经 npm 垫片时提示词被截烂;界面新增蜜蜂图标与视觉精修
|
|
29
|
+
<!-- relnotes:end -->
|
|
30
|
+
|
|
23
31
|
## 作者侧命令行(可选)
|
|
24
32
|
|
|
25
33
|
不开浏览器也能过裁决队列(本机免令牌):
|
package/app/core/attachments.py
CHANGED
|
@@ -86,6 +86,46 @@ def save_pending(name, data_b64):
|
|
|
86
86
|
return meta
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def norm_rel(a):
|
|
90
|
+
"""把一条附件记录归一成工作目录内相对路径(正斜杠)。
|
|
91
|
+
|
|
92
|
+
附件只会落在 <workdir>/_attachments/ 下;老数据可能是绝对路径或裸文件名,
|
|
93
|
+
统一钉回 _attachments/ 起头的相对位置,供前端「点击查看」直接走
|
|
94
|
+
/api/runs/<id>/file?name= 通道。取不到路径返回空串。"""
|
|
95
|
+
if isinstance(a, dict):
|
|
96
|
+
p = str(a.get("path") or a.get("name") or "")
|
|
97
|
+
else:
|
|
98
|
+
p = str(a or "")
|
|
99
|
+
p = p.replace("\\", "/").strip()
|
|
100
|
+
if not p:
|
|
101
|
+
return ""
|
|
102
|
+
if p.startswith("_attachments/"):
|
|
103
|
+
return p
|
|
104
|
+
return "_attachments/" + p.rsplit("/", 1)[-1]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_pending(fid):
|
|
108
|
+
"""读待提交区附件原文(输入条胶囊点击预览用)。id 是 16 位十六进制
|
|
109
|
+
随机数且只认文件名,猜不中即空。返回 (bytes, mime, name);找不到
|
|
110
|
+
返回 (b"", "", "")。"""
|
|
111
|
+
fid = str(fid or "")
|
|
112
|
+
if not _ID_RE.match(fid):
|
|
113
|
+
return b"", "", ""
|
|
114
|
+
p = _pending_dir() / fid
|
|
115
|
+
if not p.is_file():
|
|
116
|
+
return b"", "", ""
|
|
117
|
+
meta = {}
|
|
118
|
+
try:
|
|
119
|
+
meta = json.loads((_pending_dir() / (fid + ".json")).read_text(encoding="utf-8"))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
try:
|
|
123
|
+
data = p.read_bytes()
|
|
124
|
+
except OSError:
|
|
125
|
+
return b"", "", ""
|
|
126
|
+
return data, str(meta.get("mime") or "application/octet-stream"), _safe_name(meta.get("name") or fid)
|
|
127
|
+
|
|
128
|
+
|
|
89
129
|
_XML_PART_MAX = 20 * 1024 * 1024 # 单个 Office 部件(XML 条目)读入上限
|
|
90
130
|
|
|
91
131
|
|
package/app/core/bookmeta.py
CHANGED
|
@@ -44,20 +44,26 @@ def needs_book_meta(task):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
# 各平台建书表单的字段展示顺序(前端卡片与 Markdown 归档共用)。
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# 番茄官方表单(2026-09 实抓):签约模式=连载模式/完本模式;阅读标签=主分类(必选1)
|
|
48
|
+
# +主题/角色/情节(各≤2);内容标签=情节(≤4)/情感(≤2)/人设(≤4)/世界观(≤1)。
|
|
49
|
+
# 七猫官方表单:一级→二级级联按频道;作品标签四组每组必选 1-3 个。
|
|
49
50
|
FIELD_LABELS = {
|
|
50
51
|
"fanqie": [
|
|
51
52
|
("book_name", "作品名"), ("signing_mode", "签约模式"),
|
|
52
|
-
("target_reader", "目标读者"), ("category", "
|
|
53
|
+
("target_reader", "目标读者"), ("category", "主分类"),
|
|
53
54
|
("tags_theme", "主题标签"), ("tags_role", "角色标签"),
|
|
54
|
-
("tags_plot", "情节标签"),
|
|
55
|
+
("tags_plot", "情节标签"),
|
|
56
|
+
("content_plot", "内容·情节"), ("content_emotion", "内容·情感"),
|
|
57
|
+
("content_character", "内容·人设"), ("content_world", "内容·世界观"),
|
|
58
|
+
("protagonist_1", "主角名1"),
|
|
55
59
|
("protagonist_2", "主角名2"), ("summary", "作品简介"),
|
|
56
60
|
],
|
|
57
61
|
"qimao": [
|
|
58
62
|
("book_name", "作品名称"), ("target_reader", "目标读者"),
|
|
59
63
|
("category_main", "一级分类"), ("category_sub", "二级分类"),
|
|
60
|
-
("
|
|
64
|
+
("tags_style", "风格标签"), ("tags_role", "角色标签"),
|
|
65
|
+
("tags_plot", "情节标签"), ("tags_bg", "背景标签"),
|
|
66
|
+
("protagonist_1", "主角名1"),
|
|
61
67
|
("protagonist_2", "主角名2"), ("status", "作品状态"),
|
|
62
68
|
("summary", "作品简介"),
|
|
63
69
|
],
|
|
@@ -72,24 +78,33 @@ __SKILLS__
|
|
|
72
78
|
__SCHEMA__
|
|
73
79
|
硬性要求:
|
|
74
80
|
- 字段值必须贴合素材内容,不编造素材里没有的设定;简介不超过 500 字,突出卖点与钩子;
|
|
81
|
+
- 每个标签数组字段都必须至少给 1 个、并尽量贴着上限给满,只从对应组的选项表里选;
|
|
82
|
+
任何一组留空数组、漏掉字段或自造表外词都算错误;
|
|
75
83
|
- __OPTIONS_RULE__
|
|
76
84
|
- 题材健康,无违规内容,符合平台签约调性。
|
|
77
85
|
"""
|
|
78
86
|
|
|
79
|
-
#
|
|
80
|
-
FANQIE_MODES = ("
|
|
81
|
-
|
|
82
|
-
FANQIE_SCHEMA = ('{"book_name": "作品名(15字内)", "signing_mode": "
|
|
83
|
-
'"target_reader": "男频 或 女频", "category": "
|
|
84
|
-
'"tags_theme": ["主题标签", …最多
|
|
85
|
-
'"tags_plot": ["情节标签", …最多
|
|
87
|
+
# 签约模式真实选项(番茄建书表单 2026-09 实抓):连载模式 / 完本模式
|
|
88
|
+
FANQIE_MODES = ("连载模式", "完本模式")
|
|
89
|
+
|
|
90
|
+
FANQIE_SCHEMA = ('{"book_name": "作品名(15字内)", "signing_mode": "连载模式 或 完本模式", '
|
|
91
|
+
'"target_reader": "男频 或 女频", "category": "主分类(必选且只能选一个,从下方分类表选)", '
|
|
92
|
+
'"tags_theme": ["主题标签", …最多2个], "tags_role": ["角色标签", …最多2个], '
|
|
93
|
+
'"tags_plot": ["情节标签", …最多2个], '
|
|
94
|
+
'"content_plot": ["内容标签·情节", …最多4个], '
|
|
95
|
+
'"content_emotion": ["内容标签·情感", …最多2个], '
|
|
96
|
+
'"content_character": ["内容标签·人设", …最多4个], '
|
|
97
|
+
'"content_world": ["内容标签·世界观", …最多1个], '
|
|
86
98
|
'"protagonist_1": "主角名1(5字内)", '
|
|
87
99
|
'"protagonist_2": "主角名2(5字内,没有就空串)", "summary": "作品简介(500字内)"}')
|
|
88
100
|
|
|
89
|
-
QIMAO_SCHEMA = ('{"book_name": "作品名称(
|
|
90
|
-
'"category_main": "一级分类(
|
|
101
|
+
QIMAO_SCHEMA = ('{"book_name": "作品名称(18字内)", "target_reader": "男生 或 女生", '
|
|
102
|
+
'"category_main": "一级分类(从该频道一级分类表选一个)", '
|
|
91
103
|
'"category_sub": "二级分类(必须是该一级下的二级)", '
|
|
92
|
-
'"
|
|
104
|
+
'"tags_style": ["风格标签", 必选1-3个], '
|
|
105
|
+
'"tags_role": ["角色标签", 必选1-3个], '
|
|
106
|
+
'"tags_plot": ["情节标签", 必选1-3个], '
|
|
107
|
+
'"tags_bg": ["背景标签", 必选1-3个], '
|
|
93
108
|
'"protagonist_1": "主角名1(5字内)", '
|
|
94
109
|
'"protagonist_2": "主角名2(5字内,没有就空串)", "status": "连载中 或 已完结", '
|
|
95
110
|
'"summary": "作品简介(200字内)"}')
|
|
@@ -105,25 +120,36 @@ def _fq_tables(reader):
|
|
|
105
120
|
cat.FANQIE_MALE_TAGS_ROLE, cat.FANQIE_MALE_TAGS_PLOT)
|
|
106
121
|
|
|
107
122
|
|
|
123
|
+
def _fq_content_tables():
|
|
124
|
+
"""番茄内容标签四组(男女性共用):情节/情感/人设/世界观。"""
|
|
125
|
+
from . import bookmeta_catalog as cat
|
|
126
|
+
return (cat.FANQIE_CONTENT_PLOT, cat.FANQIE_CONTENT_EMOTION,
|
|
127
|
+
cat.FANQIE_CONTENT_CHARACTER, cat.FANQIE_CONTENT_WORLD)
|
|
128
|
+
|
|
129
|
+
|
|
108
130
|
def _fq_options_block(goal):
|
|
109
|
-
"""番茄提示词的真实选项注入:按 goal
|
|
131
|
+
"""番茄提示词的真实选项注入:按 goal 推断读者给出对应分类与标签表。"""
|
|
110
132
|
reader = "女频" if "女" in (goal or "") else "男频"
|
|
111
133
|
cats, theme, role, plot = _fq_tables(reader)
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
c_plot, c_emo, c_char, c_world = _fq_content_tables()
|
|
135
|
+
return ("目标读者(%s)可用的主分类表(必选且只能选一个):%s\n"
|
|
136
|
+
"阅读标签三组各最多选 2 个——主题组:%s\n角色组:%s\n情节组(部分,选最贴合的):%s\n"
|
|
137
|
+
"内容标签四组——情节组(最多4个,部分):%s\n情感组(最多2个):%s\n"
|
|
138
|
+
"人设组(最多4个,部分):%s\n世界观组(最多1个):%s"
|
|
114
139
|
% (reader, "、".join(cats), "、".join(theme), "、".join(role),
|
|
115
|
-
"、".join(plot[:50])))
|
|
140
|
+
"、".join(plot[:50]), "、".join(c_plot[:60]), "、".join(c_emo),
|
|
141
|
+
"、".join(c_char[:60]), "、".join(c_world)))
|
|
116
142
|
|
|
117
143
|
|
|
118
|
-
def _qm_options_block():
|
|
119
|
-
"""
|
|
144
|
+
def _qm_options_block(goal):
|
|
145
|
+
"""七猫提示词的真实选项注入:按频道给级联 + 四组标签池(每组必选 1-3)。"""
|
|
120
146
|
from . import bookmeta_catalog as cat
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
channel = cat.QIMAO_CATS.get("女生" if "女" in (goal or "") else "男生")
|
|
148
|
+
cascade = ["%s(二级:%s)" % (m, "、".join(v)) for m, v in channel.items()]
|
|
149
|
+
pool = ["%s组(必选1-3个):%s" % (g, "、".join(t)) for g, t in cat.QIMAO_TAG_GROUPS.items()]
|
|
150
|
+
return ("一级分类表(选一个):%s\n一级→二级级联(二级必须从所属一级下选):%s\n"
|
|
151
|
+
"作品标签四组:%s"
|
|
152
|
+
% ("、".join(channel), ";".join(cascade), ";".join(pool)))
|
|
127
153
|
|
|
128
154
|
|
|
129
155
|
def _collect_material(task):
|
|
@@ -162,6 +188,7 @@ def _collect_material(task):
|
|
|
162
188
|
|
|
163
189
|
|
|
164
190
|
def _tags(v, limit=6):
|
|
191
|
+
"""(旧字段兜底保留)通用标签列表清洗:去重/去空/截断。"""
|
|
165
192
|
if not isinstance(v, list):
|
|
166
193
|
return []
|
|
167
194
|
out = []
|
|
@@ -219,15 +246,20 @@ def _norm(data, platform, goal=""):
|
|
|
219
246
|
reader = "女频" if "女" in (goal or "") else "男频"
|
|
220
247
|
mode = str(data.get("signing_mode") or "").strip()
|
|
221
248
|
if mode not in FANQIE_MODES:
|
|
222
|
-
mode = "
|
|
249
|
+
mode = "连载模式"
|
|
223
250
|
cats, theme, role, plot = _fq_tables(reader)
|
|
251
|
+
c_plot, c_emo, c_char, c_world = _fq_content_tables()
|
|
224
252
|
# 兼容旧字段:read_tags/content_tags 并入情节/主题组(老数据不丢)
|
|
225
253
|
legacy_read = data.get("read_tags") if isinstance(data.get("read_tags"), list) else []
|
|
226
254
|
return {"book_name": book_name, "signing_mode": mode, "target_reader": reader,
|
|
227
255
|
"category": _in_table(data.get("category"), cats),
|
|
228
|
-
"tags_theme": _filter_table(data.get("tags_theme"), theme),
|
|
229
|
-
"tags_role": _filter_table(data.get("tags_role"), role),
|
|
230
|
-
"tags_plot": _filter_table(data.get("tags_plot"), plot),
|
|
256
|
+
"tags_theme": _filter_table(data.get("tags_theme"), theme, 2),
|
|
257
|
+
"tags_role": _filter_table(data.get("tags_role"), role, 2),
|
|
258
|
+
"tags_plot": _filter_table(data.get("tags_plot"), plot, 2),
|
|
259
|
+
"content_plot": _filter_table(data.get("content_plot"), c_plot, 4),
|
|
260
|
+
"content_emotion": _filter_table(data.get("content_emotion"), c_emo, 2),
|
|
261
|
+
"content_character": _filter_table(data.get("content_character"), c_char, 4),
|
|
262
|
+
"content_world": _filter_table(data.get("content_world"), c_world, 1),
|
|
231
263
|
"legacy_read_tags": _filter_table(legacy_read, plot + theme + role),
|
|
232
264
|
"protagonist_1": _name(data.get("protagonist_1"), 5),
|
|
233
265
|
"protagonist_2": _name(data.get("protagonist_2"), 5),
|
|
@@ -239,14 +271,29 @@ def _norm(data, platform, goal=""):
|
|
|
239
271
|
status = str(data.get("status") or "").strip()
|
|
240
272
|
if status not in ("连载中", "已完结"):
|
|
241
273
|
status = "连载中"
|
|
242
|
-
|
|
274
|
+
channel = cat.QIMAO_CATS.get(reader) or {}
|
|
275
|
+
c_main = _in_table(data.get("category_main"), list(channel))
|
|
243
276
|
c_sub = _name(data.get("category_sub"), 10)
|
|
244
|
-
subs =
|
|
277
|
+
subs = channel.get(c_main)
|
|
245
278
|
if subs and c_sub not in subs:
|
|
246
279
|
c_sub = "" # 级联不匹配清空(二级必须挂在一级下)
|
|
280
|
+
# 作品标签四组(官方每组必选 1-3):逐组过滤 + 截到 3;老数据 tags 并入
|
|
281
|
+
legacy_tags = data.get("tags") if isinstance(data.get("tags"), list) else []
|
|
282
|
+
groups = {}
|
|
283
|
+
for key, gname in (("tags_style", "风格"), ("tags_role", "角色"),
|
|
284
|
+
("tags_plot", "情节"), ("tags_bg", "背景")):
|
|
285
|
+
got = _filter_table(data.get(key), cat.QIMAO_TAG_GROUPS[gname], 3)
|
|
286
|
+
if len(got) < 3 and legacy_tags:
|
|
287
|
+
for t in _filter_table(legacy_tags, cat.QIMAO_TAG_GROUPS[gname], 3):
|
|
288
|
+
if t not in got:
|
|
289
|
+
got.append(t)
|
|
290
|
+
if len(got) >= 3:
|
|
291
|
+
break
|
|
292
|
+
groups[key] = got
|
|
247
293
|
return {"book_name": book_name, "target_reader": reader,
|
|
248
294
|
"category_main": c_main, "category_sub": c_sub,
|
|
249
|
-
"
|
|
295
|
+
"tags_style": groups["tags_style"], "tags_role": groups["tags_role"],
|
|
296
|
+
"tags_plot": groups["tags_plot"], "tags_bg": groups["tags_bg"],
|
|
250
297
|
"protagonist_1": _name(data.get("protagonist_1"), 5),
|
|
251
298
|
"protagonist_2": _name(data.get("protagonist_2"), 5),
|
|
252
299
|
"status": status, "summary": summary}
|
|
@@ -263,27 +310,26 @@ def _template_meta(task, platform, outline):
|
|
|
263
310
|
summary = goal.splitlines()[0][:500] if goal.strip() else ""
|
|
264
311
|
if not summary:
|
|
265
312
|
summary = book_name or "(待补充)" # 全空素材也要有返回,_norm 才不会拒绝
|
|
266
|
-
material =
|
|
267
|
-
try:
|
|
268
|
-
_, bible, _ = store_read_bible(task)
|
|
269
|
-
material += "\n" + (bible or "")
|
|
270
|
-
except Exception:
|
|
271
|
-
pass
|
|
272
|
-
if outline:
|
|
273
|
-
material += "\n" + str(outline.get("book_title") or "")
|
|
313
|
+
material = _material_text(task, outline)
|
|
274
314
|
data = {"book_name": book_name, "summary": summary}
|
|
275
315
|
if platform == "fanqie":
|
|
276
316
|
reader = "女频" if "女" in goal else "男频"
|
|
277
317
|
cats, theme, role, plot = _fq_tables(reader)
|
|
318
|
+
c_plot, c_emo, c_char, c_world = _fq_content_tables()
|
|
278
319
|
data.update({"target_reader": reader,
|
|
279
320
|
"category": _scan_table(material, cats, 1),
|
|
280
|
-
"tags_theme": _scan_table(material, theme,
|
|
281
|
-
"tags_role": _scan_table(material, role,
|
|
282
|
-
"tags_plot": _scan_table(material, plot,
|
|
321
|
+
"tags_theme": _scan_table(material, theme, 2),
|
|
322
|
+
"tags_role": _scan_table(material, role, 2),
|
|
323
|
+
"tags_plot": _scan_table(material, plot, 2),
|
|
324
|
+
"content_plot": _scan_table(material, c_plot, 4),
|
|
325
|
+
"content_emotion": _scan_table(material, c_emo, 2),
|
|
326
|
+
"content_character": _scan_table(material, c_char, 4),
|
|
327
|
+
"content_world": _scan_table(material, c_world, 1)})
|
|
283
328
|
else:
|
|
284
329
|
from . import bookmeta_catalog as cat
|
|
285
|
-
|
|
286
|
-
|
|
330
|
+
reader = "女生" if "女" in goal else "男生"
|
|
331
|
+
data["target_reader"] = reader
|
|
332
|
+
for main, subs in (cat.QIMAO_CATS.get(reader) or {}).items():
|
|
287
333
|
if main in material:
|
|
288
334
|
data["category_main"] = main
|
|
289
335
|
for s in subs: # 二级名撞素材即选(如「宫闱宅斗」)
|
|
@@ -291,8 +337,9 @@ def _template_meta(task, platform, outline):
|
|
|
291
337
|
data["category_sub"] = s
|
|
292
338
|
break
|
|
293
339
|
break
|
|
294
|
-
|
|
295
|
-
|
|
340
|
+
for key, gname in (("tags_style", "风格"), ("tags_role", "角色"),
|
|
341
|
+
("tags_plot", "情节"), ("tags_bg", "背景")):
|
|
342
|
+
data[key] = _scan_table(material, cat.QIMAO_TAG_GROUPS[gname], 3)
|
|
296
343
|
return _norm(data, platform, goal)
|
|
297
344
|
|
|
298
345
|
|
|
@@ -309,6 +356,46 @@ def _scan_table(text, table, limit):
|
|
|
309
356
|
return out
|
|
310
357
|
|
|
311
358
|
|
|
359
|
+
def _material_text(task, outline):
|
|
360
|
+
"""标签词面扫描用的素材文本:goal + 圣经 + 大纲书名(模板兜底同源)。"""
|
|
361
|
+
material = task.get("goal") or ""
|
|
362
|
+
try:
|
|
363
|
+
_, bible, _ = store_read_bible(task)
|
|
364
|
+
material += "\n" + (bible or "")
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
if outline:
|
|
368
|
+
material += "\n" + str(outline.get("book_title") or "")
|
|
369
|
+
return material
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _fill_tag_gaps(task, platform, meta, outline):
|
|
373
|
+
"""llm 归一化后仍为空的标签组:用素材词面扫描补一轮(命中多少算多少)。
|
|
374
|
+
|
|
375
|
+
模型偶尔整组漏给(实测 codex-cli 只填了内容标签前两组);空着用户就得
|
|
376
|
+
手选,扫描命中的是官方表内词,补上好过留白——已有的组不动,不覆盖模型。"""
|
|
377
|
+
material = _material_text(task, outline)
|
|
378
|
+
if not material:
|
|
379
|
+
return meta
|
|
380
|
+
goal = task.get("goal") or ""
|
|
381
|
+
if platform == "fanqie":
|
|
382
|
+
reader = meta.get("target_reader") or ("女频" if "女" in goal else "男频")
|
|
383
|
+
cats, theme, role, plot = _fq_tables(reader)
|
|
384
|
+
c_plot, c_emo, c_char, c_world = _fq_content_tables()
|
|
385
|
+
pairs = [("tags_theme", theme, 2), ("tags_role", role, 2), ("tags_plot", plot, 2),
|
|
386
|
+
("content_plot", c_plot, 4), ("content_emotion", c_emo, 2),
|
|
387
|
+
("content_character", c_char, 4), ("content_world", c_world, 1)]
|
|
388
|
+
else:
|
|
389
|
+
from . import bookmeta_catalog as cat
|
|
390
|
+
pairs = [(k, cat.QIMAO_TAG_GROUPS[g], 3) for k, g in
|
|
391
|
+
(("tags_style", "风格"), ("tags_role", "角色"),
|
|
392
|
+
("tags_plot", "情节"), ("tags_bg", "背景"))]
|
|
393
|
+
for key, table, limit in pairs:
|
|
394
|
+
if not meta.get(key):
|
|
395
|
+
meta[key] = _scan_table(material, table, limit)
|
|
396
|
+
return meta
|
|
397
|
+
|
|
398
|
+
|
|
312
399
|
def store_read_bible(task):
|
|
313
400
|
"""圣经读取的小包装(模板兜底用;异常向上抛由调用方吞掉)。"""
|
|
314
401
|
from . import store
|
|
@@ -339,7 +426,7 @@ def make_book_meta(task, platform, author_agent=None, log_path=None):
|
|
|
339
426
|
sk_block, _ = skills.block_for(task)
|
|
340
427
|
goal = task.get("goal") or ""
|
|
341
428
|
# 真实选项注入:分类/标签表按平台与读者频段给全(模型只能从表里选)
|
|
342
|
-
options = (_fq_options_block(goal) if platform == "fanqie" else _qm_options_block())
|
|
429
|
+
options = (_fq_options_block(goal) if platform == "fanqie" else _qm_options_block(goal))
|
|
343
430
|
prompt = (PROMPT_HEAD.replace("__PLATFORM__", plat_label)
|
|
344
431
|
.replace("__SKILLS__", sk_block)
|
|
345
432
|
.replace("__SCHEMA__", schema)
|
|
@@ -361,7 +448,8 @@ def make_book_meta(task, platform, author_agent=None, log_path=None):
|
|
|
361
448
|
if res["ok"]:
|
|
362
449
|
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
363
450
|
if meta:
|
|
364
|
-
return
|
|
451
|
+
return _fill_tag_gaps(task, platform, meta, outline), \
|
|
452
|
+
"编排者(%s · %s)" % (prov.get("name", prov["id"]), model)
|
|
365
453
|
orch_err = str(res.get("error") or "返回内容无法解析为作品信息")[:200]
|
|
366
454
|
|
|
367
455
|
if author_agent and author_agent.get("mode") == "real" and not template_only():
|
|
@@ -372,7 +460,7 @@ def make_book_meta(task, platform, author_agent=None, log_path=None):
|
|
|
372
460
|
if res["ok"]:
|
|
373
461
|
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
374
462
|
if meta:
|
|
375
|
-
return meta, "llm(%s)" % author_agent["id"]
|
|
463
|
+
return _fill_tag_gaps(task, platform, meta, outline), "llm(%s)" % author_agent["id"]
|
|
376
464
|
orch_err = orch_err or str(res.get("error") or "")[:200]
|
|
377
465
|
|
|
378
466
|
meta = _template_meta(task, platform, outline)
|
|
@@ -428,3 +516,20 @@ def generate_async(task_id, platform, author_agent=None):
|
|
|
428
516
|
if prev.get("status") != "running": # 期间被删/被重置:丢弃结果
|
|
429
517
|
return
|
|
430
518
|
store.set_book_meta(task_id, platform, entry)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def recover_orphans():
|
|
522
|
+
"""启动收尸:后台生成线程随进程重启死掉后,落盘的 running 无人写终态——
|
|
523
|
+
前端永远「生成中」(按钮禁用)+ 接口幂等拒绝,卡死不可自愈。启动时把
|
|
524
|
+
遗留 running 统一改判 failed,交还「重试」按钮。返回改判条数。"""
|
|
525
|
+
from . import store
|
|
526
|
+
n = 0
|
|
527
|
+
for task in store.list_tasks(limit=10 ** 9):
|
|
528
|
+
for plat, entry in (task.get("book_meta") or {}).items():
|
|
529
|
+
if isinstance(entry, dict) and entry.get("status") == "running":
|
|
530
|
+
store.set_book_meta(task["id"], plat, {
|
|
531
|
+
"status": "failed",
|
|
532
|
+
"error": "生成随服务重启中断,请点重试",
|
|
533
|
+
"at": time.strftime("%Y-%m-%d %H:%M:%S")})
|
|
534
|
+
n += 1
|
|
535
|
+
return n
|