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.
- package/CHANGELOG.md +507 -490
- package/LICENSE +21 -21
- package/README.md +487 -486
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -0
- package/app/core/attachments.py +532 -532
- package/app/core/automation.py +758 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +215 -19
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1191 -1191
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +19 -14
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +186 -186
- package/app/core/covergen.py +356 -356
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +113 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +64 -64
- package/app/core/errorlog.py +137 -137
- package/app/core/flows.py +355 -355
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +341 -341
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +156 -156
- package/app/core/jobs.py +1022 -1022
- package/app/core/manager.py +1996 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +66 -66
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3332 -3266
- package/app/core/notify.py +106 -106
- package/app/core/paihang.py +109 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +4483 -4426
- package/app/core/planner.py +27 -27
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +533 -533
- package/app/core/publish/fanqie.py +128 -128
- package/app/core/publish/flow.py +406 -406
- package/app/core/publish/ledger.py +198 -198
- package/app/core/publish/manager.py +590 -590
- package/app/core/publish/qimao.py +141 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +124 -124
- package/app/core/router.py +20 -3
- package/app/core/runner.py +2019 -1991
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +149 -149
- package/app/core/settings_schema.py +262 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +106 -106
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +13 -70
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/volumes.py +355 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2447 -2090
- package/app/main.py +2832 -2826
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +13977 -13977
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2711 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +1 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6125 -6125
- package/bin/tutti.js +147 -147
- package/package.json +41 -41
- package/tools/wx_reader.py +261 -261
package/app/core/bookmeta.py
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
用户在详情页「成果」分区点按钮触发(不做自动生成),后台线程调编排者模型
|
|
5
5
|
提炼 大纲 + 故事圣经 + 已有章节 的精华,按平台表单字段归一化后存任务
|
|
6
6
|
book_meta 字段,同时在工作目录落一份 Markdown 归档。降级链与大纲一致:
|
|
7
|
-
编排者 API → 作者 CLI →
|
|
7
|
+
编排者 API → 作者 CLI → 模板兜底(模板只做搬运归纳,必填字段仍保证可建书)。
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import os
|
|
12
|
-
import
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
PLATFORMS = {
|
|
@@ -25,7 +26,8 @@ def template_only():
|
|
|
25
26
|
"""只走模板兜底(跳过编排者/作者 CLI):env TUTTI_BOOKMETA_TEMPLATE_ONLY=1。
|
|
26
27
|
|
|
27
28
|
离线环境、不想为开书资料再花一次模型调用、以及测试要确定性输出时使用;
|
|
28
|
-
|
|
29
|
+
产出仍是结构完整的字段,书名/简介直接取大纲与目标,平台必填字段由生成阶段
|
|
30
|
+
的官方目录兜底补齐。"""
|
|
29
31
|
return str(os.environ.get("TUTTI_BOOKMETA_TEMPLATE_ONLY") or "").strip() in ("1", "true", "yes")
|
|
30
32
|
|
|
31
33
|
|
|
@@ -376,7 +378,7 @@ def _material_text(task, outline):
|
|
|
376
378
|
return material
|
|
377
379
|
|
|
378
380
|
|
|
379
|
-
def _fill_tag_gaps(task, platform, meta, outline):
|
|
381
|
+
def _fill_tag_gaps(task, platform, meta, outline):
|
|
380
382
|
"""llm 归一化后仍为空的标签组:用素材词面扫描补一轮(命中多少算多少)。
|
|
381
383
|
|
|
382
384
|
模型偶尔整组漏给(实测 codex-cli 只填了内容标签前两组);空着用户就得
|
|
@@ -400,7 +402,137 @@ def _fill_tag_gaps(task, platform, meta, outline):
|
|
|
400
402
|
for key, table, limit in pairs:
|
|
401
403
|
if not meta.get(key):
|
|
402
404
|
meta[key] = _scan_table(material, table, limit)
|
|
403
|
-
return meta
|
|
405
|
+
return meta
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _material_names(material):
|
|
409
|
+
"""从圣经/大纲/章节里提取明确标注的人物名。
|
|
410
|
+
|
|
411
|
+
只接受带人物语义的短语(如“女主:阿禾”或“男主叫陆沉”),避免把
|
|
412
|
+
普通句子里的名词误当成主角。返回顺序去重后的最多两个名字。
|
|
413
|
+
"""
|
|
414
|
+
text = str(material or "")
|
|
415
|
+
out = []
|
|
416
|
+
patterns = (
|
|
417
|
+
r"(?:主角|主人公|男主(?:角)?|女主(?:角)?|男一|女一|姓名|名字)"
|
|
418
|
+
r"\s*(?:是|叫|为|:|:)\s*([\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z·]{1,7})",
|
|
419
|
+
r"人物\s*[::]\s*([^\n。;;]{2,40})",
|
|
420
|
+
)
|
|
421
|
+
for pat in patterns:
|
|
422
|
+
for m in re.finditer(pat, text):
|
|
423
|
+
raw = m.group(1)
|
|
424
|
+
chunks = re.split(r"[、,,/和及与]|\s+", raw)
|
|
425
|
+
for chunk in chunks:
|
|
426
|
+
name = re.sub(r"[^\u4e00-\u9fffA-Za-z·]", "", chunk)
|
|
427
|
+
if 2 <= len(name) <= 8 and name not in out:
|
|
428
|
+
out.append(name[:5])
|
|
429
|
+
if len(out) >= 2:
|
|
430
|
+
return out
|
|
431
|
+
return out
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _pick_option(material, table, keywords=()):
|
|
435
|
+
"""从官方选项表中按素材命中词选择一个,完全命中不到时用首项。"""
|
|
436
|
+
text = str(material or "")
|
|
437
|
+
for item in table:
|
|
438
|
+
if len(item) >= 2 and item in text:
|
|
439
|
+
return item
|
|
440
|
+
for key, item in keywords:
|
|
441
|
+
if key and key in text and item in table:
|
|
442
|
+
return item
|
|
443
|
+
return table[0] if table else ""
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _has_value(value):
|
|
447
|
+
"""判断建书字段是否真正有值(兼容列表和历史占位文本)。"""
|
|
448
|
+
placeholders = {"待补充", "(待补充)", "(待补充)"}
|
|
449
|
+
if isinstance(value, list):
|
|
450
|
+
return any((text := str(x or "").strip()) and text not in placeholders
|
|
451
|
+
for x in value)
|
|
452
|
+
text = str(value or "").strip()
|
|
453
|
+
return bool(text) and text not in placeholders
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _fill_required_fields(task, platform, meta, outline, material):
|
|
457
|
+
"""生成阶段的必填字段闸门。
|
|
458
|
+
|
|
459
|
+
``_norm`` 仍保持“表外值清空”的纯校验语义,供旧数据修复和单测使用;
|
|
460
|
+
这里仅在真正交付作品信息前补齐平台建书表单的必填项,避免 UI 展示一整
|
|
461
|
+
屏“待补充”而无法直接复制建书。所有分类/标签仍来自官方目录。
|
|
462
|
+
"""
|
|
463
|
+
from . import bookmeta_catalog as cat
|
|
464
|
+
|
|
465
|
+
material = str(material or "")
|
|
466
|
+
names = _material_names(material)
|
|
467
|
+
female_words = ("女频", "女生", "女主", "言情", "古言", "恋爱")
|
|
468
|
+
female = any(x in ((task.get("goal") or "") + material) for x in female_words)
|
|
469
|
+
defaults = ("沈知微", "顾砚川", "林知夏") if female else ("陆沉", "苏晚", "程野")
|
|
470
|
+
if not _has_value(meta.get("protagonist_1")):
|
|
471
|
+
meta["protagonist_1"] = names[0] if names else defaults[0]
|
|
472
|
+
if not _has_value(meta.get("protagonist_2")):
|
|
473
|
+
candidate = names[1] if len(names) > 1 else defaults[1]
|
|
474
|
+
if candidate == meta["protagonist_1"]:
|
|
475
|
+
candidate = next((name for name in defaults if name != meta["protagonist_1"]), defaults[2])
|
|
476
|
+
meta["protagonist_2"] = candidate
|
|
477
|
+
|
|
478
|
+
if platform == "fanqie":
|
|
479
|
+
reader = meta.get("target_reader") or ("女频" if female else "男频")
|
|
480
|
+
if reader not in ("男频", "女频"):
|
|
481
|
+
reader = "女频" if female else "男频"
|
|
482
|
+
meta["target_reader"] = reader
|
|
483
|
+
cats, theme, role, plot = _fq_tables(reader)
|
|
484
|
+
c_plot, c_emo, c_char, c_world = _fq_content_tables()
|
|
485
|
+
if meta.get("category") not in cats:
|
|
486
|
+
meta["category"] = _pick_option(
|
|
487
|
+
material, cats,
|
|
488
|
+
(("戍边", "古风世情"), ("边关", "古风世情"),
|
|
489
|
+
("军", "抗战谍战"), ("悬疑", "女频悬疑" if female else "悬疑脑洞"),
|
|
490
|
+
("种田", "种田"), ("都市", "都市脑洞")))
|
|
491
|
+
groups = (("tags_theme", theme, 2), ("tags_role", role, 2),
|
|
492
|
+
("tags_plot", plot, 2), ("content_plot", c_plot, 4),
|
|
493
|
+
("content_emotion", c_emo, 2), ("content_character", c_char, 4),
|
|
494
|
+
("content_world", c_world, 1))
|
|
495
|
+
else:
|
|
496
|
+
reader = meta.get("target_reader") or ("女生" if female else "男生")
|
|
497
|
+
if reader not in ("男生", "女生"):
|
|
498
|
+
reader = "女生" if female else "男生"
|
|
499
|
+
meta["target_reader"] = reader
|
|
500
|
+
channel = cat.QIMAO_CATS.get(reader) or cat.QIMAO_CATS["男生"]
|
|
501
|
+
main = meta.get("category_main")
|
|
502
|
+
if main not in channel:
|
|
503
|
+
main = _pick_option(
|
|
504
|
+
material, list(channel),
|
|
505
|
+
(("戍边", "古代言情"), ("边关", "古代言情"),
|
|
506
|
+
("古代", "古代言情"), ("军事", "军事"),
|
|
507
|
+
("军", "军事"), ("都市", "都市"),
|
|
508
|
+
("玄幻", "玄幻奇幻"), ("悬疑", "奇闻异事")))
|
|
509
|
+
meta["category_main"] = main
|
|
510
|
+
subs = channel.get(main) or []
|
|
511
|
+
sub = meta.get("category_sub")
|
|
512
|
+
if sub not in subs:
|
|
513
|
+
located = cat.qimao_locate_sub(sub)
|
|
514
|
+
if located:
|
|
515
|
+
reader, main = located
|
|
516
|
+
meta["target_reader"] = reader
|
|
517
|
+
meta["category_main"] = main
|
|
518
|
+
subs = cat.QIMAO_CATS[reader][main]
|
|
519
|
+
else:
|
|
520
|
+
sub = ""
|
|
521
|
+
if sub not in subs:
|
|
522
|
+
meta["category_sub"] = _pick_option(material, subs)
|
|
523
|
+
groups = (("tags_style", cat.QIMAO_TAG_GROUPS["风格"], 3),
|
|
524
|
+
("tags_role", cat.QIMAO_TAG_GROUPS["角色"], 3),
|
|
525
|
+
("tags_plot", cat.QIMAO_TAG_GROUPS["情节"], 3),
|
|
526
|
+
("tags_bg", cat.QIMAO_TAG_GROUPS["背景"], 3))
|
|
527
|
+
for key, table, limit in groups:
|
|
528
|
+
values = meta.get(key)
|
|
529
|
+
if not isinstance(values, list):
|
|
530
|
+
values = []
|
|
531
|
+
values = [str(v).strip() for v in values if str(v or "").strip() in table]
|
|
532
|
+
if not values:
|
|
533
|
+
values = [_pick_option(material, table)]
|
|
534
|
+
meta[key] = values[:limit]
|
|
535
|
+
return meta
|
|
404
536
|
|
|
405
537
|
|
|
406
538
|
def store_read_bible(task):
|
|
@@ -455,11 +587,12 @@ def make_book_meta(task, platform, author_agent=None, log_path=None):
|
|
|
455
587
|
planner._log_usage("bookmeta", "bookmeta", task, res, model=model,
|
|
456
588
|
provider=prov.get("name", prov.get("id", "")),
|
|
457
589
|
provider_id=prov.get("id", ""))
|
|
458
|
-
if res["ok"]:
|
|
459
|
-
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
460
|
-
if meta:
|
|
461
|
-
|
|
462
|
-
|
|
590
|
+
if res["ok"]:
|
|
591
|
+
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
592
|
+
if meta:
|
|
593
|
+
meta = _fill_tag_gaps(task, platform, meta, outline)
|
|
594
|
+
return _fill_required_fields(task, platform, meta, outline, material), \
|
|
595
|
+
"编排者(%s · %s)" % (prov.get("name", prov["id"]), model)
|
|
463
596
|
orch_err = str(res.get("error") or "返回内容无法解析为作品信息")[:200]
|
|
464
597
|
|
|
465
598
|
if author_agent and author_agent.get("mode") == "real" and not template_only():
|
|
@@ -467,14 +600,16 @@ def make_book_meta(task, platform, author_agent=None, log_path=None):
|
|
|
467
600
|
workdir=task.get("workdir"), readonly=True,
|
|
468
601
|
timeout=BOOKMETA_TIMEOUT, log_path=log_path)
|
|
469
602
|
planner._log_usage("bookmeta", "bookmeta", task, res, agent=author_agent)
|
|
470
|
-
if res["ok"]:
|
|
471
|
-
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
472
|
-
if meta:
|
|
473
|
-
|
|
603
|
+
if res["ok"]:
|
|
604
|
+
meta = _norm(runner.extract_json(res.get("text") or ""), platform, goal)
|
|
605
|
+
if meta:
|
|
606
|
+
meta = _fill_tag_gaps(task, platform, meta, outline)
|
|
607
|
+
return _fill_required_fields(task, platform, meta, outline, material), "llm(%s)" % author_agent["id"]
|
|
474
608
|
orch_err = orch_err or str(res.get("error") or "")[:200]
|
|
475
609
|
|
|
476
|
-
meta = _template_meta(task, platform, outline)
|
|
477
|
-
meta
|
|
610
|
+
meta = _template_meta(task, platform, outline)
|
|
611
|
+
meta = _fill_required_fields(task, platform, meta, outline, material)
|
|
612
|
+
meta["source"] = ("模板兜底(编排者不可用:%s)" % orch_err) if orch_err else "模板兜底"
|
|
478
613
|
return meta, meta["source"]
|
|
479
614
|
|
|
480
615
|
|
|
@@ -528,7 +663,7 @@ def generate_async(task_id, platform, author_agent=None):
|
|
|
528
663
|
store.set_book_meta(task_id, platform, entry)
|
|
529
664
|
|
|
530
665
|
|
|
531
|
-
def recover_orphans():
|
|
666
|
+
def recover_orphans():
|
|
532
667
|
"""启动收尸:后台生成线程随进程重启死掉后,落盘的 running 无人写终态——
|
|
533
668
|
前端永远「生成中」(按钮禁用)+ 接口幂等拒绝,卡死不可自愈。启动时把
|
|
534
669
|
遗留 running 统一改判 failed,交还「重试」按钮。返回改判条数。"""
|
|
@@ -542,4 +677,65 @@ def recover_orphans():
|
|
|
542
677
|
"error": "生成随服务重启中断,请点重试",
|
|
543
678
|
"at": time.strftime("%Y-%m-%d %H:%M:%S")})
|
|
544
679
|
n += 1
|
|
545
|
-
return n
|
|
680
|
+
return n
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def repair_existing():
|
|
684
|
+
"""补齐历史已生成记录中的必填字段。
|
|
685
|
+
|
|
686
|
+
旧版本允许分类/主角为空并把结果标成 done;只要字段缺失就按当前官方
|
|
687
|
+
目录和已有素材补一次,已有非空字段保持不变。返回修复条数,供启动日志
|
|
688
|
+
和升级诊断使用。
|
|
689
|
+
"""
|
|
690
|
+
from . import store
|
|
691
|
+
repaired = 0
|
|
692
|
+
for task in store.list_tasks(limit=10 ** 9):
|
|
693
|
+
book_meta = task.get("book_meta") or {}
|
|
694
|
+
for platform in PLATFORMS:
|
|
695
|
+
entry = book_meta.get(platform) or {}
|
|
696
|
+
if entry.get("status") != "done" or not isinstance(entry.get("data"), dict):
|
|
697
|
+
continue
|
|
698
|
+
data = dict(entry["data"])
|
|
699
|
+
if platform == "fanqie":
|
|
700
|
+
reader = data.get("target_reader")
|
|
701
|
+
cats, _, _, _ = _fq_tables(reader if reader in ("男频", "女频") else "男频")
|
|
702
|
+
required_ok = (
|
|
703
|
+
data.get("category") in cats
|
|
704
|
+
and all(_has_value(data.get(key)) for key in (
|
|
705
|
+
"tags_theme", "tags_role", "tags_plot", "content_plot",
|
|
706
|
+
"content_emotion", "content_character", "content_world"))
|
|
707
|
+
and _has_value(data.get("protagonist_1"))
|
|
708
|
+
and _has_value(data.get("protagonist_2")))
|
|
709
|
+
else:
|
|
710
|
+
from . import bookmeta_catalog as cat
|
|
711
|
+
reader = data.get("target_reader")
|
|
712
|
+
channel = cat.QIMAO_CATS.get(reader) or {}
|
|
713
|
+
main = data.get("category_main")
|
|
714
|
+
required_ok = (
|
|
715
|
+
main in channel
|
|
716
|
+
and data.get("category_sub") in (channel.get(main) or [])
|
|
717
|
+
and all(_has_value(data.get(key)) for key in (
|
|
718
|
+
"tags_style", "tags_role", "tags_plot", "tags_bg"))
|
|
719
|
+
and _has_value(data.get("protagonist_1"))
|
|
720
|
+
and _has_value(data.get("protagonist_2")))
|
|
721
|
+
if required_ok:
|
|
722
|
+
continue
|
|
723
|
+
try:
|
|
724
|
+
material, outline = _collect_material(task)
|
|
725
|
+
fixed = _fill_required_fields(task, platform, data, outline, material)
|
|
726
|
+
if fixed == entry["data"]:
|
|
727
|
+
continue
|
|
728
|
+
payload = dict(entry)
|
|
729
|
+
payload["data"] = fixed
|
|
730
|
+
payload["source"] = (str(entry.get("source") or "") + ";已补齐必填字段").lstrip(";")
|
|
731
|
+
store.set_book_meta(task["id"], platform, payload)
|
|
732
|
+
try:
|
|
733
|
+
fp = Path(task.get("workdir") or "") / PLATFORMS[platform]["file"]
|
|
734
|
+
if fp.parent.is_dir():
|
|
735
|
+
fp.write_text(render_markdown(task, platform, fixed), encoding="utf-8")
|
|
736
|
+
except OSError:
|
|
737
|
+
pass
|
|
738
|
+
repaired += 1
|
|
739
|
+
except Exception:
|
|
740
|
+
continue
|
|
741
|
+
return repaired
|