codebee 0.1.102 → 0.1.103
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 +5 -0
- package/LICENSE +21 -21
- package/README.md +4 -8
- 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/acceptance.py +100 -100
- package/app/core/acp_client.py +457 -457
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backend_profiles.py +188 -188
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -102
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/command_auth.py +302 -302
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +1 -1
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/eval_matrix.py +91 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -33
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/knowledge_pipeline.py +86 -86
- package/app/core/manager.py +2385 -2385
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -181
- package/app/core/openapi.py +217 -217
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +2 -2
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/project_memory.py +206 -206
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +659 -659
- package/app/core/publish/chaprows.py +138 -138
- package/app/core/publish/flows-fanqie-calibrated.json +442 -442
- package/app/core/publish/manager.py +1185 -1185
- package/app/core/publish/qimao.py +193 -193
- package/app/core/publish/volumes.py +108 -108
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +201 -201
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/sandbox_runtime.py +95 -95
- 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 +215 -215
- package/app/core/settings_schema.py +282 -282
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- 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/story_tracking.py +275 -275
- 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/tool_outputs.py +107 -107
- package/app/core/tracing.py +207 -207
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -89
- package/app/core/wxdigest.py +865 -865
- package/app/core/zentao.py +2692 -2692
- package/app/mcp_server.py +244 -244
- package/app/output/_createbook_body.json +1 -0
- package/app/output/_probe_fq_click.py +50 -0
- package/app/output/_probe_fq_form.py +42 -0
- package/app/output/_probe_fq_modal.py +55 -0
- package/app/output/_probe_fq_points.py +71 -0
- package/app/output/_probe_fq_submit.py +34 -0
- package/app/output/_probe_fq_watch.py +62 -0
- package/app/output/workbench-clean-preview.jpg +0 -0
- package/app/output/workbench-reference-render.png +0 -0
- package/app/output/workbench-shell.png +0 -0
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +2 -3
- package/app/ui/artdiff.js +74 -74
- package/app/ui/i18n.js +0 -1
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/bin/tutti.js +147 -147
- package/package.json +1 -1
- package/tools/wx_reader.py +261 -261
package/app/core/paihang.py
CHANGED
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""扫榜选材:抓取公开排行榜页,产出选题分析素材。
|
|
3
|
-
|
|
4
|
-
数据源(四源聚合,任一失败不影响其余源;2026-09-24 实测全 200):
|
|
5
|
-
- 七猫:www.qimao.com/paihang/(106KB)
|
|
6
|
-
- 番茄 Web 版:fanqienovel.com/rank(题材分类+书名在中文 a 标签里;
|
|
7
|
-
App 端接口有 SecuritySign/X-Argus 签名——那才是难路,Web 版榜单页
|
|
8
|
-
不需要签名,别走弯路)
|
|
9
|
-
- 起点移动版:m.qidian.com/rank(106KB,直接出书名;主站 www.qidian.com
|
|
10
|
-
被 WAF 拦 202/209B,移动页不需要签名)
|
|
11
|
-
- 纵横:www.zongheng.com/rank(65KB,书名+统计标签混排,靠噪音词过滤)
|
|
12
|
-
|
|
13
|
-
下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
|
|
14
|
-
宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import logging
|
|
19
|
-
import re
|
|
20
|
-
|
|
21
|
-
log = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
|
|
24
|
-
FANQIE_RANK_URL = "https://fanqienovel.com/rank"
|
|
25
|
-
QIDIAN_M_RANK_URL = "https://m.qidian.com/rank/"
|
|
26
|
-
ZONGHENG_RANK_URL = "https://www.zongheng.com/rank/"
|
|
27
|
-
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
28
|
-
"(KHTML, like Gecko) Chrome/126 Safari/537.36")
|
|
29
|
-
|
|
30
|
-
# 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索、
|
|
31
|
-
# 榜单 tab 与统计标签(起点移动版 tab 名、纵横月票/人气/点击等)
|
|
32
|
-
_NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
|
|
33
|
-
"排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
|
|
34
|
-
"帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
|
|
35
|
-
"搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
|
|
36
|
-
"更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
|
|
37
|
-
"下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于",
|
|
38
|
-
"月票榜", "阅读榜", "书友榜", "推荐榜", "签约榜", "新人榜",
|
|
39
|
-
"畅销榜", "大家都在搜", "男生小说排行榜", "女生小说排行榜",
|
|
40
|
-
"男频阅读榜", "女频阅读榜", "男频新书榜", "番茄小说",
|
|
41
|
-
"登录后获得更多特色功能", "安装起点读书客户端", "看更多正版好书",
|
|
42
|
-
"月票", "人气", "点击", "万字", "推荐", "收藏", "鲜花", "打赏", "推荐票"}
|
|
43
|
-
|
|
44
|
-
_CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _curl_text(url):
|
|
48
|
-
"""curl 子进程抓页面文本;失败返回空串。"""
|
|
49
|
-
from . import runner
|
|
50
|
-
try:
|
|
51
|
-
r = runner.run_process(
|
|
52
|
-
argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
|
|
53
|
-
timeout=30)
|
|
54
|
-
except Exception:
|
|
55
|
-
return ""
|
|
56
|
-
if not r.get("ok"):
|
|
57
|
-
return ""
|
|
58
|
-
return r.get("stdout") or ""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _extract_cjk(html, limit):
|
|
62
|
-
"""从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
|
|
63
|
-
items, seen = [], set()
|
|
64
|
-
for t in _CJK_RE.findall(html or ""):
|
|
65
|
-
if t in _NOISE or t in seen:
|
|
66
|
-
continue
|
|
67
|
-
seen.add(t)
|
|
68
|
-
items.append(t)
|
|
69
|
-
if len(items) >= limit:
|
|
70
|
-
break
|
|
71
|
-
return items
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def fetch_qimao_rank(limit=30):
|
|
75
|
-
"""七猫排行榜:返回元素列表,抓取失败返回 []。"""
|
|
76
|
-
return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def fetch_fanqie_rank(limit=30):
|
|
80
|
-
"""番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
|
|
81
|
-
return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def fetch_qidian_rank(limit=30):
|
|
85
|
-
"""起点移动版榜单:返回元素列表,抓取失败返回 []。"""
|
|
86
|
-
return _extract_cjk(_curl_text(QIDIAN_M_RANK_URL), limit)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def fetch_zongheng_rank(limit=30):
|
|
90
|
-
"""纵横排行榜:返回元素列表,抓取失败返回 []。"""
|
|
91
|
-
return _extract_cjk(_curl_text(ZONGHENG_RANK_URL), limit)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# 源清单:顺序即素材拼接顺序(七猫/番茄为元老源保持在前)。
|
|
95
|
-
# 存函数名、调用期 globals() 解析——mock.patch 换模块属性才能生效
|
|
96
|
-
# (存函数引用会把定义期原函数钉死,测试会穿透打真网络)。
|
|
97
|
-
_SOURCES = (
|
|
98
|
-
("七猫排行榜素材", "fetch_qimao_rank"),
|
|
99
|
-
("番茄排行榜素材", "fetch_fanqie_rank"),
|
|
100
|
-
("起点排行榜素材", "fetch_qidian_rank"),
|
|
101
|
-
("纵横排行榜素材", "fetch_zongheng_rank"),
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def fetch_rank_items(limit=40):
|
|
106
|
-
"""四源聚合:各抓一份(跨源去重合并,按源清单顺序)。
|
|
107
|
-
|
|
108
|
-
全部失败才返回 []——单源挂了其余源仍可用,扫榜永不因单点挡任务。"""
|
|
109
|
-
merged, seen = [], set()
|
|
110
|
-
for _label, fname in _SOURCES:
|
|
111
|
-
for t in globals()[fname](limit):
|
|
112
|
-
if t not in seen:
|
|
113
|
-
seen.add(t)
|
|
114
|
-
merged.append(t)
|
|
115
|
-
return merged
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def rank_scan_prompt(goal):
|
|
119
|
-
"""组装扫榜选材分析提示词。四源全空返回 None(调用方回落直连提示词)。
|
|
120
|
-
|
|
121
|
-
报告契约含逐条 A-E 跨平台证据分级(借鉴 career-ops 的分级报告交付形态):
|
|
122
|
-
分级只认素材实际出现情况——抑制单源孤证被模型放大成「大热题材」。"""
|
|
123
|
-
blocks, seen = [], set()
|
|
124
|
-
for label, fname in _SOURCES:
|
|
125
|
-
items = [t for t in globals()[fname](30) if t not in seen]
|
|
126
|
-
seen.update(items)
|
|
127
|
-
if items:
|
|
128
|
-
blocks.append("### %s\n%s" % (label, "、".join(items)))
|
|
129
|
-
if not blocks:
|
|
130
|
-
return None
|
|
131
|
-
material = "\n\n".join(blocks)
|
|
132
|
-
return (
|
|
133
|
-
"你是网文选题分析师。以下是刚刚抓取的各平台排行榜页面的书目与分类素材:\n\n"
|
|
134
|
-
"## 榜单元素材\n%s\n\n"
|
|
135
|
-
"## 用户想写的方向\n%s\n\n"
|
|
136
|
-
"## 你的产出(Markdown 报告)\n"
|
|
137
|
-
"报告第一行给图例(一行):「证据分级:A=三平台及以上重合,B=两平台重合,"
|
|
138
|
-
"C=单平台多次出现,D=单平台仅一次,E=素材无直接证据、系推断」。\n"
|
|
139
|
-
"1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
|
|
140
|
-
"2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
|
|
141
|
-
"3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
|
|
142
|
-
"以上每一项结论末尾标注【证据X】(X 为 A-E 之一)。分级只看素材里真实出现的"
|
|
143
|
-
"情况:组合条目按其组成元素的较低档取值,拿不准就降一档、不升级。\n"
|
|
144
|
-
"直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
|
|
145
|
-
% (material, goal or "(用户未指定方向,按大盘热门分析)"))
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""扫榜选材:抓取公开排行榜页,产出选题分析素材。
|
|
3
|
+
|
|
4
|
+
数据源(四源聚合,任一失败不影响其余源;2026-09-24 实测全 200):
|
|
5
|
+
- 七猫:www.qimao.com/paihang/(106KB)
|
|
6
|
+
- 番茄 Web 版:fanqienovel.com/rank(题材分类+书名在中文 a 标签里;
|
|
7
|
+
App 端接口有 SecuritySign/X-Argus 签名——那才是难路,Web 版榜单页
|
|
8
|
+
不需要签名,别走弯路)
|
|
9
|
+
- 起点移动版:m.qidian.com/rank(106KB,直接出书名;主站 www.qidian.com
|
|
10
|
+
被 WAF 拦 202/209B,移动页不需要签名)
|
|
11
|
+
- 纵横:www.zongheng.com/rank(65KB,书名+统计标签混排,靠噪音词过滤)
|
|
12
|
+
|
|
13
|
+
下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
|
|
14
|
+
宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
|
|
24
|
+
FANQIE_RANK_URL = "https://fanqienovel.com/rank"
|
|
25
|
+
QIDIAN_M_RANK_URL = "https://m.qidian.com/rank/"
|
|
26
|
+
ZONGHENG_RANK_URL = "https://www.zongheng.com/rank/"
|
|
27
|
+
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
28
|
+
"(KHTML, like Gecko) Chrome/126 Safari/537.36")
|
|
29
|
+
|
|
30
|
+
# 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索、
|
|
31
|
+
# 榜单 tab 与统计标签(起点移动版 tab 名、纵横月票/人气/点击等)
|
|
32
|
+
_NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
|
|
33
|
+
"排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
|
|
34
|
+
"帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
|
|
35
|
+
"搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
|
|
36
|
+
"更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
|
|
37
|
+
"下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于",
|
|
38
|
+
"月票榜", "阅读榜", "书友榜", "推荐榜", "签约榜", "新人榜",
|
|
39
|
+
"畅销榜", "大家都在搜", "男生小说排行榜", "女生小说排行榜",
|
|
40
|
+
"男频阅读榜", "女频阅读榜", "男频新书榜", "番茄小说",
|
|
41
|
+
"登录后获得更多特色功能", "安装起点读书客户端", "看更多正版好书",
|
|
42
|
+
"月票", "人气", "点击", "万字", "推荐", "收藏", "鲜花", "打赏", "推荐票"}
|
|
43
|
+
|
|
44
|
+
_CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _curl_text(url):
|
|
48
|
+
"""curl 子进程抓页面文本;失败返回空串。"""
|
|
49
|
+
from . import runner
|
|
50
|
+
try:
|
|
51
|
+
r = runner.run_process(
|
|
52
|
+
argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
|
|
53
|
+
timeout=30)
|
|
54
|
+
except Exception:
|
|
55
|
+
return ""
|
|
56
|
+
if not r.get("ok"):
|
|
57
|
+
return ""
|
|
58
|
+
return r.get("stdout") or ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_cjk(html, limit):
|
|
62
|
+
"""从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
|
|
63
|
+
items, seen = [], set()
|
|
64
|
+
for t in _CJK_RE.findall(html or ""):
|
|
65
|
+
if t in _NOISE or t in seen:
|
|
66
|
+
continue
|
|
67
|
+
seen.add(t)
|
|
68
|
+
items.append(t)
|
|
69
|
+
if len(items) >= limit:
|
|
70
|
+
break
|
|
71
|
+
return items
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fetch_qimao_rank(limit=30):
|
|
75
|
+
"""七猫排行榜:返回元素列表,抓取失败返回 []。"""
|
|
76
|
+
return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def fetch_fanqie_rank(limit=30):
|
|
80
|
+
"""番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
|
|
81
|
+
return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fetch_qidian_rank(limit=30):
|
|
85
|
+
"""起点移动版榜单:返回元素列表,抓取失败返回 []。"""
|
|
86
|
+
return _extract_cjk(_curl_text(QIDIAN_M_RANK_URL), limit)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fetch_zongheng_rank(limit=30):
|
|
90
|
+
"""纵横排行榜:返回元素列表,抓取失败返回 []。"""
|
|
91
|
+
return _extract_cjk(_curl_text(ZONGHENG_RANK_URL), limit)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# 源清单:顺序即素材拼接顺序(七猫/番茄为元老源保持在前)。
|
|
95
|
+
# 存函数名、调用期 globals() 解析——mock.patch 换模块属性才能生效
|
|
96
|
+
# (存函数引用会把定义期原函数钉死,测试会穿透打真网络)。
|
|
97
|
+
_SOURCES = (
|
|
98
|
+
("七猫排行榜素材", "fetch_qimao_rank"),
|
|
99
|
+
("番茄排行榜素材", "fetch_fanqie_rank"),
|
|
100
|
+
("起点排行榜素材", "fetch_qidian_rank"),
|
|
101
|
+
("纵横排行榜素材", "fetch_zongheng_rank"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fetch_rank_items(limit=40):
|
|
106
|
+
"""四源聚合:各抓一份(跨源去重合并,按源清单顺序)。
|
|
107
|
+
|
|
108
|
+
全部失败才返回 []——单源挂了其余源仍可用,扫榜永不因单点挡任务。"""
|
|
109
|
+
merged, seen = [], set()
|
|
110
|
+
for _label, fname in _SOURCES:
|
|
111
|
+
for t in globals()[fname](limit):
|
|
112
|
+
if t not in seen:
|
|
113
|
+
seen.add(t)
|
|
114
|
+
merged.append(t)
|
|
115
|
+
return merged
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rank_scan_prompt(goal):
|
|
119
|
+
"""组装扫榜选材分析提示词。四源全空返回 None(调用方回落直连提示词)。
|
|
120
|
+
|
|
121
|
+
报告契约含逐条 A-E 跨平台证据分级(借鉴 career-ops 的分级报告交付形态):
|
|
122
|
+
分级只认素材实际出现情况——抑制单源孤证被模型放大成「大热题材」。"""
|
|
123
|
+
blocks, seen = [], set()
|
|
124
|
+
for label, fname in _SOURCES:
|
|
125
|
+
items = [t for t in globals()[fname](30) if t not in seen]
|
|
126
|
+
seen.update(items)
|
|
127
|
+
if items:
|
|
128
|
+
blocks.append("### %s\n%s" % (label, "、".join(items)))
|
|
129
|
+
if not blocks:
|
|
130
|
+
return None
|
|
131
|
+
material = "\n\n".join(blocks)
|
|
132
|
+
return (
|
|
133
|
+
"你是网文选题分析师。以下是刚刚抓取的各平台排行榜页面的书目与分类素材:\n\n"
|
|
134
|
+
"## 榜单元素材\n%s\n\n"
|
|
135
|
+
"## 用户想写的方向\n%s\n\n"
|
|
136
|
+
"## 你的产出(Markdown 报告)\n"
|
|
137
|
+
"报告第一行给图例(一行):「证据分级:A=三平台及以上重合,B=两平台重合,"
|
|
138
|
+
"C=单平台多次出现,D=单平台仅一次,E=素材无直接证据、系推断」。\n"
|
|
139
|
+
"1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
|
|
140
|
+
"2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
|
|
141
|
+
"3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
|
|
142
|
+
"以上每一项结论末尾标注【证据X】(X 为 A-E 之一)。分级只看素材里真实出现的"
|
|
143
|
+
"情况:组合条目按其组成元素的较低档取值,拿不准就降一档、不升级。\n"
|
|
144
|
+
"直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
|
|
145
|
+
% (material, goal or "(用户未指定方向,按大盘热门分析)"))
|
package/app/core/pipeline.py
CHANGED
|
@@ -2311,8 +2311,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
2311
2311
|
if first:
|
|
2312
2312
|
prompt = ""
|
|
2313
2313
|
if task.get("type") == "rank_scan" and bi is not None:
|
|
2314
|
-
# 扫榜选材(借鉴 oh-story
|
|
2315
|
-
# AI 做选题洞察;抓取失败回落普通直连提示词
|
|
2314
|
+
# 扫榜选材(借鉴 oh-story 扫榜):抓七猫/番茄/起点/纵横四平台排行榜
|
|
2315
|
+
# 公开数据注入,AI 做选题洞察;抓取失败回落普通直连提示词
|
|
2316
2316
|
prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
|
|
2317
2317
|
if task.get("type") == "defect_retro" and bi is not None:
|
|
2318
2318
|
# 缺陷复盘(借鉴 test-defect-retrospective):三视角分析框架
|
package/app/core/portguard.py
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""启动端口清场(用户拍板 2026-09-21):端口被占时先清场再启动。
|
|
3
|
-
|
|
4
|
-
- **自家旧实例**(命令行含本包 main.py 完整路径,或 npm 包内 app/main.py)
|
|
5
|
-
→ 杀树(TerminateProcess 直杀)——升级/重启最常见的占用者就是没退干净
|
|
6
|
-
的 CodeBee 自己;控制台进程对温和信号(WM_CLOSE)无反应,温和关不掉
|
|
7
|
-
正是用户「旧进程太难杀」的痛点,所以自家实例直接强杀。
|
|
8
|
-
- **别人的进程** → 只报告占用者,不发送任何信号;启动流程不得替用户
|
|
9
|
-
关闭可能正在开发中的服务。
|
|
10
|
-
- 系统/自身进程拒关(portscan 内置护栏)。
|
|
11
|
-
"""
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import logging
|
|
15
|
-
import os
|
|
16
|
-
import shlex
|
|
17
|
-
import subprocess
|
|
18
|
-
|
|
19
|
-
from . import portscan, runner
|
|
20
|
-
|
|
21
|
-
log = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
PS_EXE = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def proc_cmdline(pid):
|
|
27
|
-
"""读进程命令行;失败返回空串。仅用于确认旧 CodeBee 实例身份。"""
|
|
28
|
-
try:
|
|
29
|
-
pid = int(pid)
|
|
30
|
-
except (TypeError, ValueError):
|
|
31
|
-
return ""
|
|
32
|
-
if pid <= 0:
|
|
33
|
-
return ""
|
|
34
|
-
if os.name != "nt":
|
|
35
|
-
try:
|
|
36
|
-
with open("/proc/%d/cmdline" % pid, "rb") as fh:
|
|
37
|
-
return fh.read().decode("utf-8", "replace").replace("\x00", " ").strip()
|
|
38
|
-
except Exception:
|
|
39
|
-
return ""
|
|
40
|
-
try:
|
|
41
|
-
r = subprocess.run(
|
|
42
|
-
[PS_EXE, "-NoProfile", "-Command",
|
|
43
|
-
"(Get-CimInstance Win32_Process -Filter 'ProcessId = %d').CommandLine" % pid],
|
|
44
|
-
capture_output=True, timeout=10)
|
|
45
|
-
return r.stdout.decode("utf-8", "replace").strip() if r.returncode == 0 else ""
|
|
46
|
-
except Exception:
|
|
47
|
-
return ""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _command_tokens(cmdline):
|
|
51
|
-
try:
|
|
52
|
-
return [token.strip().strip('"').strip("'")
|
|
53
|
-
for token in shlex.split(str(cmdline or ""), posix=False)]
|
|
54
|
-
except (TypeError, ValueError):
|
|
55
|
-
return []
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def is_own_instance(cmdline, main_script, port=None):
|
|
59
|
-
"""按启动器、脚本独立参数和端口精确确认 CodeBee 实例。"""
|
|
60
|
-
tokens = _command_tokens(cmdline)
|
|
61
|
-
if len(tokens) < 2:
|
|
62
|
-
return False
|
|
63
|
-
launcher = os.path.splitext(os.path.basename(tokens[0]))[0].lower()
|
|
64
|
-
if launcher not in ("python", "python3", "py"):
|
|
65
|
-
return False
|
|
66
|
-
script = ""
|
|
67
|
-
for token in tokens[1:]:
|
|
68
|
-
if token.startswith("-"):
|
|
69
|
-
continue
|
|
70
|
-
script = token
|
|
71
|
-
break
|
|
72
|
-
if not script or not os.path.isabs(script):
|
|
73
|
-
return False
|
|
74
|
-
actual = os.path.normcase(os.path.normpath(os.path.abspath(script)))
|
|
75
|
-
expected = os.path.normcase(os.path.normpath(os.path.abspath(str(main_script))))
|
|
76
|
-
packaged = actual.replace("\\", "/").lower().endswith(
|
|
77
|
-
"/node_modules/codebee/app/main.py")
|
|
78
|
-
if actual != expected and not packaged:
|
|
79
|
-
return False
|
|
80
|
-
if port is None:
|
|
81
|
-
return True
|
|
82
|
-
try:
|
|
83
|
-
wanted = str(int(port))
|
|
84
|
-
except (TypeError, ValueError):
|
|
85
|
-
return False
|
|
86
|
-
explicit_port = None
|
|
87
|
-
for i, token in enumerate(tokens):
|
|
88
|
-
if token == "--port" and i + 1 < len(tokens):
|
|
89
|
-
explicit_port = tokens[i + 1]
|
|
90
|
-
elif token.startswith("--port="):
|
|
91
|
-
explicit_port = token.split("=", 1)[1]
|
|
92
|
-
# argparse 默认端口可不出现在旧实例参数中;非默认端口必须显式相符。
|
|
93
|
-
return ((explicit_port == wanted) if explicit_port is not None
|
|
94
|
-
else wanted == "8765")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def clear_stale_port(port, main_script):
|
|
98
|
-
"""清掉占用端口的进程。返回 (是否清掉, 人话说明)。"""
|
|
99
|
-
holders = [p for p in portscan.listening_ports() if p.get("port") == int(port)]
|
|
100
|
-
if not holders:
|
|
101
|
-
return True, ""
|
|
102
|
-
pid = int(holders[0].get("pid") or 0)
|
|
103
|
-
if pid <= 4 or pid == os.getpid():
|
|
104
|
-
return False, "占用者是系统进程/自身,拒绝清理"
|
|
105
|
-
if is_own_instance(proc_cmdline(pid), main_script, port=port):
|
|
106
|
-
try:
|
|
107
|
-
runner._kill_tree(pid)
|
|
108
|
-
return True, "已结束旧实例 PID %d" % pid
|
|
109
|
-
except Exception as e:
|
|
110
|
-
return False, "旧实例 PID %d 清理失败 %s" % (pid, str(e)[:80])
|
|
111
|
-
return False, "占用者非 CodeBee(PID %d),未自动关闭" % pid
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""启动端口清场(用户拍板 2026-09-21):端口被占时先清场再启动。
|
|
3
|
+
|
|
4
|
+
- **自家旧实例**(命令行含本包 main.py 完整路径,或 npm 包内 app/main.py)
|
|
5
|
+
→ 杀树(TerminateProcess 直杀)——升级/重启最常见的占用者就是没退干净
|
|
6
|
+
的 CodeBee 自己;控制台进程对温和信号(WM_CLOSE)无反应,温和关不掉
|
|
7
|
+
正是用户「旧进程太难杀」的痛点,所以自家实例直接强杀。
|
|
8
|
+
- **别人的进程** → 只报告占用者,不发送任何信号;启动流程不得替用户
|
|
9
|
+
关闭可能正在开发中的服务。
|
|
10
|
+
- 系统/自身进程拒关(portscan 内置护栏)。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import shlex
|
|
17
|
+
import subprocess
|
|
18
|
+
|
|
19
|
+
from . import portscan, runner
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
PS_EXE = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def proc_cmdline(pid):
|
|
27
|
+
"""读进程命令行;失败返回空串。仅用于确认旧 CodeBee 实例身份。"""
|
|
28
|
+
try:
|
|
29
|
+
pid = int(pid)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return ""
|
|
32
|
+
if pid <= 0:
|
|
33
|
+
return ""
|
|
34
|
+
if os.name != "nt":
|
|
35
|
+
try:
|
|
36
|
+
with open("/proc/%d/cmdline" % pid, "rb") as fh:
|
|
37
|
+
return fh.read().decode("utf-8", "replace").replace("\x00", " ").strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
return ""
|
|
40
|
+
try:
|
|
41
|
+
r = subprocess.run(
|
|
42
|
+
[PS_EXE, "-NoProfile", "-Command",
|
|
43
|
+
"(Get-CimInstance Win32_Process -Filter 'ProcessId = %d').CommandLine" % pid],
|
|
44
|
+
capture_output=True, timeout=10)
|
|
45
|
+
return r.stdout.decode("utf-8", "replace").strip() if r.returncode == 0 else ""
|
|
46
|
+
except Exception:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _command_tokens(cmdline):
|
|
51
|
+
try:
|
|
52
|
+
return [token.strip().strip('"').strip("'")
|
|
53
|
+
for token in shlex.split(str(cmdline or ""), posix=False)]
|
|
54
|
+
except (TypeError, ValueError):
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_own_instance(cmdline, main_script, port=None):
|
|
59
|
+
"""按启动器、脚本独立参数和端口精确确认 CodeBee 实例。"""
|
|
60
|
+
tokens = _command_tokens(cmdline)
|
|
61
|
+
if len(tokens) < 2:
|
|
62
|
+
return False
|
|
63
|
+
launcher = os.path.splitext(os.path.basename(tokens[0]))[0].lower()
|
|
64
|
+
if launcher not in ("python", "python3", "py"):
|
|
65
|
+
return False
|
|
66
|
+
script = ""
|
|
67
|
+
for token in tokens[1:]:
|
|
68
|
+
if token.startswith("-"):
|
|
69
|
+
continue
|
|
70
|
+
script = token
|
|
71
|
+
break
|
|
72
|
+
if not script or not os.path.isabs(script):
|
|
73
|
+
return False
|
|
74
|
+
actual = os.path.normcase(os.path.normpath(os.path.abspath(script)))
|
|
75
|
+
expected = os.path.normcase(os.path.normpath(os.path.abspath(str(main_script))))
|
|
76
|
+
packaged = actual.replace("\\", "/").lower().endswith(
|
|
77
|
+
"/node_modules/codebee/app/main.py")
|
|
78
|
+
if actual != expected and not packaged:
|
|
79
|
+
return False
|
|
80
|
+
if port is None:
|
|
81
|
+
return True
|
|
82
|
+
try:
|
|
83
|
+
wanted = str(int(port))
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
return False
|
|
86
|
+
explicit_port = None
|
|
87
|
+
for i, token in enumerate(tokens):
|
|
88
|
+
if token == "--port" and i + 1 < len(tokens):
|
|
89
|
+
explicit_port = tokens[i + 1]
|
|
90
|
+
elif token.startswith("--port="):
|
|
91
|
+
explicit_port = token.split("=", 1)[1]
|
|
92
|
+
# argparse 默认端口可不出现在旧实例参数中;非默认端口必须显式相符。
|
|
93
|
+
return ((explicit_port == wanted) if explicit_port is not None
|
|
94
|
+
else wanted == "8765")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def clear_stale_port(port, main_script):
|
|
98
|
+
"""清掉占用端口的进程。返回 (是否清掉, 人话说明)。"""
|
|
99
|
+
holders = [p for p in portscan.listening_ports() if p.get("port") == int(port)]
|
|
100
|
+
if not holders:
|
|
101
|
+
return True, ""
|
|
102
|
+
pid = int(holders[0].get("pid") or 0)
|
|
103
|
+
if pid <= 4 or pid == os.getpid():
|
|
104
|
+
return False, "占用者是系统进程/自身,拒绝清理"
|
|
105
|
+
if is_own_instance(proc_cmdline(pid), main_script, port=port):
|
|
106
|
+
try:
|
|
107
|
+
runner._kill_tree(pid)
|
|
108
|
+
return True, "已结束旧实例 PID %d" % pid
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return False, "旧实例 PID %d 清理失败 %s" % (pid, str(e)[:80])
|
|
111
|
+
return False, "占用者非 CodeBee(PID %d),未自动关闭" % pid
|