codebee 0.1.64 → 0.1.66

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 (110) hide show
  1. package/CHANGELOG.md +520 -507
  2. package/LICENSE +21 -21
  3. package/README.md +494 -487
  4. package/app/core/aiflavor.py +197 -197
  5. package/app/core/artifacts.py +148 -148
  6. package/app/core/attachments.py +532 -532
  7. package/app/core/automation.py +758 -758
  8. package/app/core/backup.py +470 -470
  9. package/app/core/beekeeper.py +183 -183
  10. package/app/core/board.py +209 -209
  11. package/app/core/bookmeta.py +272 -217
  12. package/app/core/bookmeta_catalog.py +180 -180
  13. package/app/core/branching.py +137 -137
  14. package/app/core/builtin_agent.py +1234 -1191
  15. package/app/core/capability.py +144 -144
  16. package/app/core/catalog.py +23 -24
  17. package/app/core/chaptersafe.py +32 -32
  18. package/app/core/cleanup.py +303 -303
  19. package/app/core/compaction.py +201 -186
  20. package/app/core/covergen.py +356 -356
  21. package/app/core/diagnostics.py +114 -114
  22. package/app/core/dispatch.py +173 -173
  23. package/app/core/dispatch_log.py +113 -113
  24. package/app/core/env_scrub.py +85 -85
  25. package/app/core/error_codes.py +220 -65
  26. package/app/core/errorlog.py +137 -137
  27. package/app/core/evaluation.py +174 -0
  28. package/app/core/failure_notify.py +290 -0
  29. package/app/core/flows.py +355 -355
  30. package/app/core/goal_service.py +167 -167
  31. package/app/core/health.py +374 -342
  32. package/app/core/history.py +38 -38
  33. package/app/core/hooks.py +162 -156
  34. package/app/core/jobs.py +1034 -1022
  35. package/app/core/knowledge.py +6 -2
  36. package/app/core/manager.py +2115 -1996
  37. package/app/core/market.py +362 -362
  38. package/app/core/market_remote.py +66 -66
  39. package/app/core/mocks.py +64 -64
  40. package/app/core/modelhub.py +3517 -3332
  41. package/app/core/notify.py +106 -106
  42. package/app/core/paihang.py +109 -109
  43. package/app/core/paths.py +2 -2
  44. package/app/core/pipeline.py +4614 -4483
  45. package/app/core/planner.py +33 -31
  46. package/app/core/portguard.py +111 -111
  47. package/app/core/preview.py +293 -293
  48. package/app/core/publish/__init__.py +7 -7
  49. package/app/core/publish/auto.py +367 -367
  50. package/app/core/publish/browser.py +533 -533
  51. package/app/core/publish/fanqie.py +128 -128
  52. package/app/core/publish/flow.py +432 -406
  53. package/app/core/publish/flows-fanqie-calibrated.json +17 -9
  54. package/app/core/publish/flows-qimao-calibrated.json +6 -2
  55. package/app/core/publish/ledger.py +198 -198
  56. package/app/core/publish/manager.py +623 -590
  57. package/app/core/publish/qimao.py +141 -141
  58. package/app/core/publish/ws.py +139 -139
  59. package/app/core/redact.py +38 -38
  60. package/app/core/repeat_guard.py +129 -124
  61. package/app/core/router.py +96 -22
  62. package/app/core/runner.py +2050 -2019
  63. package/app/core/selfupdate.py +384 -384
  64. package/app/core/session_log.py +161 -161
  65. package/app/core/sessions.py +312 -312
  66. package/app/core/settings.py +149 -149
  67. package/app/core/settings_schema.py +262 -262
  68. package/app/core/share_page.py +114 -114
  69. package/app/core/skill_scan.py +114 -106
  70. package/app/core/skillpacks/fanqie-novel.md +80 -80
  71. package/app/core/skillpacks/market/character-bible.md +66 -66
  72. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  73. package/app/core/skillpacks/market/git-workflow.md +57 -57
  74. package/app/core/skillpacks/market/release-notes.md +72 -72
  75. package/app/core/skillpacks/market/weekly-report.md +71 -71
  76. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  77. package/app/core/skills.py +221 -72
  78. package/app/core/step_runner.py +132 -132
  79. package/app/core/store.py +23 -0
  80. package/app/core/task_compile.py +199 -199
  81. package/app/core/telemetry.py +292 -292
  82. package/app/core/tlsctx.py +64 -64
  83. package/app/core/token_meter.py +147 -147
  84. package/app/core/upstream.py +41 -0
  85. package/app/core/usage.py +114 -3
  86. package/app/core/volumes.py +355 -355
  87. package/app/core/wxdigest.py +858 -858
  88. package/app/core/zentao.py +2447 -2447
  89. package/app/main.py +2838 -2832
  90. package/app/pick_dialog.py +110 -110
  91. package/app/ui/app.js +13994 -13977
  92. package/app/ui/board.css +20 -41
  93. package/app/ui/board.html +162 -162
  94. package/app/ui/board.js +305 -305
  95. package/app/ui/i18n.js +2716 -2711
  96. package/app/ui/icons/bee.svg +79 -79
  97. package/app/ui/index.html +1 -1
  98. package/app/ui/manifest.json +16 -16
  99. package/app/ui/qrcode.js +2297 -2297
  100. package/app/ui/style.css +6107 -6125
  101. package/bin/tutti.js +147 -147
  102. package/package.json +46 -41
  103. package/tools/wx_reader.py +261 -261
  104. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  105. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  106. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  107. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  108. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  109. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  110. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -1,106 +1,106 @@
1
- # -*- coding: utf-8 -*-
2
- """运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
3
- 推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
4
-
5
- webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
6
- 思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
7
- 推送在后台线程、失败只记日志——通知永远不影响任务本身。
8
- """
9
- from __future__ import annotations
10
-
11
- import logging
12
-
13
- log = logging.getLogger(__name__)
14
-
15
-
16
- def _payload_for(webhook, text):
17
- """按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
18
- from urllib.parse import urlparse
19
- host = (urlparse(webhook).hostname or "").lower()
20
- if "dingtalk" in host:
21
- return {"msgtype": "text", "text": {"content": text}}
22
- if "feishu" in host or "larksuite" in host:
23
- return {"msg_type": "text", "content": {"text": text}}
24
- if "weixin" in host or "wechat" in host or "work.weixin" in host:
25
- return {"msgtype": "text", "text": {"content": text}}
26
- if host:
27
- return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
28
- return None
29
-
30
-
31
- def _webhook():
32
- try:
33
- from . import settings
34
- return str(settings.load().get("notify_webhook") or "").strip()
35
- except Exception:
36
- return ""
37
-
38
-
39
- def _post(hook, text):
40
- """curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
41
- from . import runner
42
- import json as _json
43
- body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
44
- r = runner.run_process(
45
- argv=["curl", "-sS", "--max-time", "20",
46
- "-H", "Content-Type: application/json",
47
- "-d", body, hook],
48
- timeout=30)
49
- return bool(r["ok"])
50
-
51
-
52
- def push_text(text):
53
- """推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
54
- hook = _webhook()
55
- if not hook:
56
- return False
57
- if not hook.startswith("https://"):
58
- log.warning("notify webhook 必须是 https")
59
- return False
60
- try:
61
- return _post(hook, text)
62
- except Exception as e:
63
- log.warning("notify push failed: %s", e)
64
- return False
65
-
66
-
67
- def push_run_async(run_id):
68
- """任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
69
- threading.Thread(target=push_run, daemon=True,
70
- name="notify-%s" % run_id, args=(run_id,)).start()
71
-
72
-
73
- def push_run(run_id):
74
- """组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
75
-
76
- 摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
77
- run_id 供在本机 CodeBee 界面查找)。"""
78
- hook = _webhook()
79
- if not hook:
80
- return False
81
- from . import store
82
- run = store.get_run(run_id) or {}
83
- if not run:
84
- return False
85
- status = str(run.get("status") or "")
86
- mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
87
- lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
88
- "cancelled": "已取消"}.get(status, status))]
89
- lines.append("任务:%s" % (run.get("title") or run_id))
90
- if run.get("error"):
91
- lines.append("错误:%s" % str(run["error"])[:200])
92
- v = run.get("verdict") or {}
93
- if v.get("overall") is not None:
94
- lines.append("综合评分 %.1f(%s)" % (
95
- float(v["overall"]), "达标" if v.get("publishable") else "未达标"))
96
- try:
97
- from .settings import load as _sload
98
- base = str(_sload().get("notify_base_url") or "").strip().rstrip("/")
99
- if base:
100
- lines.append("📄 详情:%s/api/runs/%s/share" % (base, run_id))
101
- except Exception:
102
- pass
103
- return push_text("\n".join(lines))
104
-
105
-
106
- import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
1
+ # -*- coding: utf-8 -*-
2
+ """运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
3
+ 推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
4
+
5
+ webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
6
+ 思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
7
+ 推送在后台线程、失败只记日志——通知永远不影响任务本身。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def _payload_for(webhook, text):
17
+ """按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
18
+ from urllib.parse import urlparse
19
+ host = (urlparse(webhook).hostname or "").lower()
20
+ if "dingtalk" in host:
21
+ return {"msgtype": "text", "text": {"content": text}}
22
+ if "feishu" in host or "larksuite" in host:
23
+ return {"msg_type": "text", "content": {"text": text}}
24
+ if "weixin" in host or "wechat" in host or "work.weixin" in host:
25
+ return {"msgtype": "text", "text": {"content": text}}
26
+ if host:
27
+ return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
28
+ return None
29
+
30
+
31
+ def _webhook():
32
+ try:
33
+ from . import settings
34
+ return str(settings.load().get("notify_webhook") or "").strip()
35
+ except Exception:
36
+ return ""
37
+
38
+
39
+ def _post(hook, text):
40
+ """curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
41
+ from . import runner
42
+ import json as _json
43
+ body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
44
+ r = runner.run_process(
45
+ argv=["curl", "-sS", "--max-time", "20",
46
+ "-H", "Content-Type: application/json",
47
+ "-d", body, hook],
48
+ timeout=30)
49
+ return bool(r["ok"])
50
+
51
+
52
+ def push_text(text):
53
+ """推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
54
+ hook = _webhook()
55
+ if not hook:
56
+ return False
57
+ if not hook.startswith("https://"):
58
+ log.warning("notify webhook 必须是 https")
59
+ return False
60
+ try:
61
+ return _post(hook, text)
62
+ except Exception as e:
63
+ log.warning("notify push failed: %s", e)
64
+ return False
65
+
66
+
67
+ def push_run_async(run_id):
68
+ """任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
69
+ threading.Thread(target=push_run, daemon=True,
70
+ name="notify-%s" % run_id, args=(run_id,)).start()
71
+
72
+
73
+ def push_run(run_id):
74
+ """组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
75
+
76
+ 摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
77
+ run_id 供在本机 CodeBee 界面查找)。"""
78
+ hook = _webhook()
79
+ if not hook:
80
+ return False
81
+ from . import store
82
+ run = store.get_run(run_id) or {}
83
+ if not run:
84
+ return False
85
+ status = str(run.get("status") or "")
86
+ mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
87
+ lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
88
+ "cancelled": "已取消"}.get(status, status))]
89
+ lines.append("任务:%s" % (run.get("title") or run_id))
90
+ if run.get("error"):
91
+ lines.append("错误:%s" % str(run["error"])[:200])
92
+ v = run.get("verdict") or {}
93
+ if v.get("overall") is not None:
94
+ lines.append("综合评分 %.1f(%s)" % (
95
+ float(v["overall"]), "达标" if v.get("publishable") else "未达标"))
96
+ try:
97
+ from .settings import load as _sload
98
+ base = str(_sload().get("notify_base_url") or "").strip().rstrip("/")
99
+ if base:
100
+ lines.append("📄 详情:%s/api/runs/%s/share" % (base, run_id))
101
+ except Exception:
102
+ pass
103
+ return push_text("\n".join(lines))
104
+
105
+
106
+ import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
@@ -1,109 +1,109 @@
1
- # -*- coding: utf-8 -*-
2
- """扫榜选材:抓取公开排行榜页,产出选题分析素材。
3
-
4
- 数据源(双源聚合,任一失败不影响另一源):
5
- - 七猫:www.qimao.com/paihang/(公开可抓,2026-09-19 实测 200/106KB)
6
- - 番茄 Web 版:fanqienovel.com/rank(公开可抓,题材分类+书名在
7
- 中文 a 标签里;App 端接口有 SecuritySign/X-Argus 签名——那才是难路,
8
- Web 版榜单页不需要签名,别走弯路)
9
-
10
- 下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
11
- 宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
12
- """
13
- from __future__ import annotations
14
-
15
- import logging
16
- import re
17
-
18
- log = logging.getLogger(__name__)
19
-
20
- QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
21
- FANQIE_RANK_URL = "https://fanqienovel.com/rank"
22
- _UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
23
- "(KHTML, like Gecko) Chrome/126 Safari/537.36")
24
-
25
- # 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索等
26
- _NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
27
- "排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
28
- "帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
29
- "搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
30
- "更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
31
- "下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于"}
32
-
33
- _CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
34
-
35
-
36
- def _curl_text(url):
37
- """curl 子进程抓页面文本;失败返回空串。"""
38
- from . import runner
39
- try:
40
- r = runner.run_process(
41
- argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
42
- timeout=30)
43
- except Exception:
44
- return ""
45
- if not r.get("ok"):
46
- return ""
47
- return r.get("stdout") or ""
48
-
49
-
50
- def _extract_cjk(html, limit):
51
- """从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
52
- items, seen = [], set()
53
- for t in _CJK_RE.findall(html or ""):
54
- if t in _NOISE or t in seen:
55
- continue
56
- seen.add(t)
57
- items.append(t)
58
- if len(items) >= limit:
59
- break
60
- return items
61
-
62
-
63
- def fetch_qimao_rank(limit=30):
64
- """七猫排行榜:返回元素列表,抓取失败返回 []。"""
65
- return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
66
-
67
-
68
- def fetch_fanqie_rank(limit=30):
69
- """番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
70
- return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
71
-
72
-
73
- def fetch_rank_items(limit=40):
74
- """双源聚合:七猫+番茄各抓一份(去重合并,七猫在前)。
75
-
76
- 两源都失败才返回 []——单源挂了另一源仍可用,扫榜永不因单点挡任务。"""
77
- qm = fetch_qimao_rank(limit)
78
- fq = fetch_fanqie_rank(limit)
79
- merged, seen = [], set(qm)
80
- for t in fq:
81
- if t not in seen:
82
- seen.add(t)
83
- merged.append(t)
84
- return qm + merged
85
-
86
-
87
- def rank_scan_prompt(goal):
88
- """组装扫榜选材分析提示词。双源都空返回 None(调用方回落直连提示词)。"""
89
- qm = fetch_qimao_rank(30)
90
- fq = fetch_fanqie_rank(30)
91
- qm = [t for t in qm if t not in fq] # 双源重合的只留七猫份
92
- if not qm and not fq:
93
- return None
94
- blocks = []
95
- if qm:
96
- blocks.append("### 七猫排行榜素材\n" + "、".join(qm))
97
- if fq:
98
- blocks.append("### 番茄排行榜素材\n" + "、".join(fq))
99
- material = "\n\n".join(blocks)
100
- return (
101
- "你是网文选题分析师。以下是刚刚抓取的两个平台排行榜页面的书目与分类素材:\n\n"
102
- "## 榜单元素材\n%s\n\n"
103
- "## 用户想写的方向\n%s\n\n"
104
- "## 你的产出(Markdown 报告)\n"
105
- "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——双平台都热说明是真风口)\n"
106
- "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
107
- "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
108
- "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
109
- % (material, goal or "(用户未指定方向,按大盘热门分析)"))
1
+ # -*- coding: utf-8 -*-
2
+ """扫榜选材:抓取公开排行榜页,产出选题分析素材。
3
+
4
+ 数据源(双源聚合,任一失败不影响另一源):
5
+ - 七猫:www.qimao.com/paihang/(公开可抓,2026-09-19 实测 200/106KB)
6
+ - 番茄 Web 版:fanqienovel.com/rank(公开可抓,题材分类+书名在
7
+ 中文 a 标签里;App 端接口有 SecuritySign/X-Argus 签名——那才是难路,
8
+ Web 版榜单页不需要签名,别走弯路)
9
+
10
+ 下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
11
+ 宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import re
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
21
+ FANQIE_RANK_URL = "https://fanqienovel.com/rank"
22
+ _UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
23
+ "(KHTML, like Gecko) Chrome/126 Safari/537.36")
24
+
25
+ # 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索等
26
+ _NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
27
+ "排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
28
+ "帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
29
+ "搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
30
+ "更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
31
+ "下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于"}
32
+
33
+ _CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
34
+
35
+
36
+ def _curl_text(url):
37
+ """curl 子进程抓页面文本;失败返回空串。"""
38
+ from . import runner
39
+ try:
40
+ r = runner.run_process(
41
+ argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
42
+ timeout=30)
43
+ except Exception:
44
+ return ""
45
+ if not r.get("ok"):
46
+ return ""
47
+ return r.get("stdout") or ""
48
+
49
+
50
+ def _extract_cjk(html, limit):
51
+ """从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
52
+ items, seen = [], set()
53
+ for t in _CJK_RE.findall(html or ""):
54
+ if t in _NOISE or t in seen:
55
+ continue
56
+ seen.add(t)
57
+ items.append(t)
58
+ if len(items) >= limit:
59
+ break
60
+ return items
61
+
62
+
63
+ def fetch_qimao_rank(limit=30):
64
+ """七猫排行榜:返回元素列表,抓取失败返回 []。"""
65
+ return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
66
+
67
+
68
+ def fetch_fanqie_rank(limit=30):
69
+ """番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
70
+ return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
71
+
72
+
73
+ def fetch_rank_items(limit=40):
74
+ """双源聚合:七猫+番茄各抓一份(去重合并,七猫在前)。
75
+
76
+ 两源都失败才返回 []——单源挂了另一源仍可用,扫榜永不因单点挡任务。"""
77
+ qm = fetch_qimao_rank(limit)
78
+ fq = fetch_fanqie_rank(limit)
79
+ merged, seen = [], set(qm)
80
+ for t in fq:
81
+ if t not in seen:
82
+ seen.add(t)
83
+ merged.append(t)
84
+ return qm + merged
85
+
86
+
87
+ def rank_scan_prompt(goal):
88
+ """组装扫榜选材分析提示词。双源都空返回 None(调用方回落直连提示词)。"""
89
+ qm = fetch_qimao_rank(30)
90
+ fq = fetch_fanqie_rank(30)
91
+ qm = [t for t in qm if t not in fq] # 双源重合的只留七猫份
92
+ if not qm and not fq:
93
+ return None
94
+ blocks = []
95
+ if qm:
96
+ blocks.append("### 七猫排行榜素材\n" + "、".join(qm))
97
+ if fq:
98
+ blocks.append("### 番茄排行榜素材\n" + "、".join(fq))
99
+ material = "\n\n".join(blocks)
100
+ return (
101
+ "你是网文选题分析师。以下是刚刚抓取的两个平台排行榜页面的书目与分类素材:\n\n"
102
+ "## 榜单元素材\n%s\n\n"
103
+ "## 用户想写的方向\n%s\n\n"
104
+ "## 你的产出(Markdown 报告)\n"
105
+ "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——双平台都热说明是真风口)\n"
106
+ "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
107
+ "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
108
+ "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
109
+ % (material, goal or "(用户未指定方向,按大盘热门分析)"))
package/app/core/paths.py CHANGED
@@ -54,8 +54,8 @@ def default_data_dir(repo=None, environ=None) -> Path:
54
54
  DATA_DIR = default_data_dir()
55
55
  TASKS_DIR = DATA_DIR / "tasks"
56
56
  RUNS_DIR = DATA_DIR / "runs"
57
- USAGE_DIR = DATA_DIR / "usage"
58
- ERRORS_DIR = DATA_DIR / "errors"
57
+ USAGE_DIR = DATA_DIR / "usage"
58
+ ERRORS_DIR = DATA_DIR / "errors"
59
59
  PUBLISH_DIR = DATA_DIR / "publish" # 一键发布:台账/作品登记/流程选择器覆盖/浏览器 profile
60
60
  CATALOG_FILE = DATA_DIR / "catalog.json"
61
61
  ENABLED_FILE = DATA_DIR / "orchestration.json"