codebee 0.1.65 → 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.
- package/CHANGELOG.md +520 -513
- package/LICENSE +21 -21
- package/README.md +494 -487
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- 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 +796 -759
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1234 -1191
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +23 -24
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -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 +220 -65
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -0
- package/app/core/failure_notify.py +290 -0
- package/app/core/flows.py +355 -355
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -342
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -156
- package/app/core/jobs.py +1034 -1022
- package/app/core/knowledge.py +6 -2
- package/app/core/manager.py +2115 -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 +3517 -3332
- 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 +4614 -4494
- package/app/core/planner.py +33 -31
- 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 +432 -406
- package/app/core/publish/flows-fanqie-calibrated.json +17 -9
- package/app/core/publish/flows-qimao-calibrated.json +6 -2
- package/app/core/publish/ledger.py +198 -198
- package/app/core/publish/manager.py +623 -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 +129 -124
- package/app/core/router.py +119 -81
- package/app/core/runner.py +2050 -2042
- 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 +114 -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/skills.py +221 -72
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +23 -0
- 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/upstream.py +41 -0
- package/app/core/usage.py +114 -3
- package/app/core/volumes.py +355 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2447 -2447
- package/app/main.py +2838 -2832
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +13994 -13977
- package/app/ui/board.css +20 -41
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2716 -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 +6107 -6125
- package/bin/tutti.js +147 -147
- package/package.json +46 -41
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- 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 +0 -210
- 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/notify.py
CHANGED
|
@@ -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 依赖;置底避免顶部循环导入)
|
package/app/core/paihang.py
CHANGED
|
@@ -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"
|