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
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
2
|
+
"""建书表单的真实选项目录:官方渠道实抓(2026-09-17)。
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
数据来源(快照见 tools/fanqie_bookmeta.json / tools/qimao_bookmeta.json):
|
|
5
|
+
- 番茄·阅读标签:官方接口
|
|
6
|
+
https://fanqienovel.com/api/author/book/category_list/v0/?gender=1|0(匿名可用),
|
|
7
|
+
并与建书页弹层实抓逐词比对一致。弹层规则:主分类必选且只能选一个,
|
|
8
|
+
主题/角色/情节每组最多两个。男频/女频两套完全独立。
|
|
9
|
+
- 番茄·内容标签:建书页「内容标签」弹层实抓,男女性共用一套;
|
|
10
|
+
情节/人设最多四个、情感最多两个、世界观最多一个。
|
|
11
|
+
- 番茄·签约模式:建书表单真实选项为「连载模式/完本模式」。
|
|
12
|
+
- 七猫:登录态下 book-option 接口实抓
|
|
13
|
+
https://zuozhe.qimao.com/api/pc/v1/book/book-option;频道(男生/女生)→一级→
|
|
14
|
+
二级级联为官方全量;作品标签四组(风格/角色/情节/背景),每组必选 1-3 个。
|
|
10
15
|
|
|
11
16
|
改这些表不影响已保存任务(book_meta 生成时快照)。
|
|
12
17
|
"""
|
|
13
18
|
|
|
19
|
+
# ================= 番茄·阅读标签(category_list API,弹层校验一致) =================
|
|
20
|
+
|
|
14
21
|
# 番茄·男频主分类(gender=1)
|
|
15
22
|
FANQIE_MALE_CATEGORIES = [
|
|
16
23
|
"西方奇幻", "东方仙侠", "科幻末世", "男频衍生", "都市高武", "悬疑灵异",
|
|
@@ -19,7 +26,7 @@ FANQIE_MALE_CATEGORIES = [
|
|
|
19
26
|
"都市修真",
|
|
20
27
|
]
|
|
21
28
|
|
|
22
|
-
#
|
|
29
|
+
# 番茄·男频标签-主题组(最多选 2)
|
|
23
30
|
FANQIE_MALE_TAGS_THEME = [
|
|
24
31
|
"衍生", "仕途", "综影视", "天灾", "第一人称", "赛博朋克",
|
|
25
32
|
"第四天灾", "规则怪谈", "搞笑轻松", "古代", "悬疑", "克苏鲁",
|
|
@@ -29,7 +36,7 @@ FANQIE_MALE_TAGS_THEME = [
|
|
|
29
36
|
"历史", "体育", "武侠",
|
|
30
37
|
]
|
|
31
38
|
|
|
32
|
-
#
|
|
39
|
+
# 番茄·男频标签-角色组(最多选 2)
|
|
33
40
|
FANQIE_MALE_TAGS_ROLE = [
|
|
34
41
|
"多女主", "赘婿", "全能", "大佬", "大小姐", "特工",
|
|
35
42
|
"游戏主播", "神探", "贵族", "皇帝", "单女主", "校花",
|
|
@@ -37,7 +44,7 @@ FANQIE_MALE_TAGS_ROLE = [
|
|
|
37
44
|
"学霸", "天才", "腹黑", "扮猪吃虎",
|
|
38
45
|
]
|
|
39
46
|
|
|
40
|
-
#
|
|
47
|
+
# 番茄·男频标签-情节组(最多选 2)
|
|
41
48
|
FANQIE_MALE_TAGS_PLOT = [
|
|
42
49
|
"都市江湖", "风水秘术", "斩神衍生", "十日衍生", "西游衍生", "公版衍生",
|
|
43
50
|
"红楼衍生", "甄嬛衍生", "如懿衍生", "惊悚游戏", "卡牌", "山海经",
|
|
@@ -63,7 +70,7 @@ FANQIE_FEMALE_CATEGORIES = [
|
|
|
63
70
|
"年代", "种田", "快穿",
|
|
64
71
|
]
|
|
65
72
|
|
|
66
|
-
#
|
|
73
|
+
# 番茄·女频标签-主题组(最多选 2)
|
|
67
74
|
FANQIE_FEMALE_TAGS_THEME = [
|
|
68
75
|
"古言权谋", "悬疑恋爱", "纯爱", "衍生", "仕途", "综影视",
|
|
69
76
|
"天灾", "第一人称", "赛博朋克", "规则怪谈", "搞笑轻松", "古代",
|
|
@@ -72,7 +79,7 @@ FANQIE_FEMALE_TAGS_THEME = [
|
|
|
72
79
|
"幻想言情",
|
|
73
80
|
]
|
|
74
81
|
|
|
75
|
-
#
|
|
82
|
+
# 番茄·女频标签-角色组(最多选 2)
|
|
76
83
|
FANQIE_FEMALE_TAGS_ROLE = [
|
|
77
84
|
"位尊权重", "总裁", "忠犬", "全能", "白切黑", "双学霸",
|
|
78
85
|
"作精", "大佬", "大小姐", "游戏主播", "神探", "贵族",
|
|
@@ -83,7 +90,7 @@ FANQIE_FEMALE_TAGS_ROLE = [
|
|
|
83
90
|
"精灵", "天才", "腹黑", "扮猪吃虎", "团宠",
|
|
84
91
|
]
|
|
85
92
|
|
|
86
|
-
#
|
|
93
|
+
# 番茄·女频标签-情节组(最多选 2)
|
|
87
94
|
FANQIE_FEMALE_TAGS_PLOT = [
|
|
88
95
|
"男二上位", "代嫁代娶", "攻略反派", "风水秘术", "斩神衍生", "十日衍生",
|
|
89
96
|
"西游衍生", "公版衍生", "红楼衍生", "甄嬛衍生", "如懿衍生", "惊悚游戏",
|
|
@@ -103,85 +110,55 @@ FANQIE_FEMALE_TAGS_PLOT = [
|
|
|
103
110
|
"穿越", "二次元", "娱乐圈", "空间", "推理",
|
|
104
111
|
]
|
|
105
112
|
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
]
|
|
113
|
+
# ================= 番茄·内容标签(建书弹层实抓,男女性共用) =================
|
|
114
|
+
|
|
115
|
+
# 内容标签-情节(最多选 4)
|
|
116
|
+
FANQIE_CONTENT_PLOT = ["互撩", "和亲", "独宠", "攻略反派", "寄人篱下", "冷宫", "恋爱日常", "流放", "门阀斗争", "男二上位", "女扮男装", "强强", "深情错付", "失忆", "双向奔赴", "逃婚", "天作之合", "休夫", "休妻", "修罗场", "选秀入宫", "追妻火葬场", "双穿", "随军", "召唤流", "隐居", "异族入侵", "基因改造", "记忆篡改", "觉醒天赋", "时间循环", "封地", "古武传承", "争霸", "赶山赶海", "黑科技", "角色扮演", "家人曝光", "财运亨通", "变身", "经营建设", "挂机", "无后宫", "修改背景", "百鬼夜行", "抱大腿", "保家卫国", "爆装备", "避难所", "抄家", "吃瓜", "抽卡", "带货直播", "代嫁代娶", "古墓探秘", "打怪升级", "电竞", "第四天灾", "囤物资", "夺嫡", "读心", "发育", "捉鬼", "风水秘术", "分家", "复仇", "父凭子贵", "附身", "赶尸", "高考", "黑化", "黄昏恋", "欢喜冤家", "化身", "捡漏", "家长里短", "假结婚", "锦鲤", "基建", "绝不原谅", "绝症", "科举", "慢穿", "胎穿", "群穿", "古穿今", "两界穿梭", "恋综", "聊天群", "灵魂互换", "灵气复苏", "历劫", "历史直播", "美食", "民国传奇", "宝物秘境", "密室逃脱", "末世基建", "逆袭", "虐渣", "破案", "气运之子", "全民觉醒", "全民领主", "全民转职", "权谋", "全球灾变", "全员恶人", "全员火葬场", "驱魔降妖", "入魔", "商战", "升学", "收徒", "双修", "双重生", "逃荒", "推理", "无限流", "无限物资", "一夜情", "游戏入侵", "越狱", "致富", "转职", "追夫火葬场", "宗门大比", "末世种田", "退婚", "直播", "直播穿越", "鉴宝", "荒岛求生", "海洋求生", "豪门世家", "群像", "大逃杀", "多宝", "全民神祇", "公路求生", "全员重生", "游戏制作", "开局辞退", "旁门左道"]
|
|
117
|
+
|
|
118
|
+
# 内容标签-情感(最多选 2)
|
|
119
|
+
FANQIE_CONTENT_EMOTION = ["追夫", "死对头", "年龄差", "暗恋", "暗恋无果", "宠妻", "祈福联姻", "破镜不重圆", "奉子成婚", "破镜重圆", "青梅竹马", "闪婚", "双向暗恋", "相爱相杀", "先婚后爱", "隐婚", "一见钟情", "智性恋", "双宠", "年上", "老夫少妻", "水仙", "宠夫", "双洁", "救赎", "单箭头", "久别重逢", "无固定cp", "无CP", "1v1", "单女主", "无女主", "师徒", "病态依赖", "错位爱情", "姐弟恋", "浪子回头", "强娶豪夺", "日久生情", "家族联姻", "网恋", "人外恋", "替身", "白月光", "初恋", "多CP", "和离", "离婚", "虐恋情深", "女追男", "前任", "未婚夫", "未婚妻", "追妻", "道侣", "夫管严", "妻管严", "妾室"]
|
|
113
120
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
],
|
|
146
|
-
"游戏": [
|
|
147
|
-
"电子竞技", "虚拟网游",
|
|
148
|
-
],
|
|
149
|
-
"游戏竞技": [
|
|
150
|
-
"网游电竞", "运动竞技",
|
|
151
|
-
],
|
|
152
|
-
"体育": [
|
|
153
|
-
"其他竞技", "篮球风云", "足球天下",
|
|
154
|
-
],
|
|
155
|
-
"N次元": [
|
|
156
|
-
"原生幻想", "衍生同人", "轻小说",
|
|
157
|
-
],
|
|
158
|
-
"历史": [
|
|
159
|
-
"架空历史",
|
|
160
|
-
],
|
|
121
|
+
# 内容标签-人设(最多选 4)
|
|
122
|
+
FANQIE_CONTENT_CHARACTER = ["马甲", "女主萌宝", "对照组", "太奶奶", "农女", "商女", "穿成女配", "老太太", "灰姑娘", "真假千金", "豪门千金", "金丝雀", "宠妃", "团宠", "佛子", "夫郎", "鬼夫", "精灵", "九千岁", "名媛", "王妃", "弃妃", "丫鬟", "主母", "总裁", "医妃", "非穿越重生", "皇叔", "位尊权重", "继室", "神豪", "勇者", "战神", "赘婿", "高手下山", "奶爸", "神医", "天才", "师尊", "娇妻", "真假少爷", "废材", "海后", "海王", "萌宝", "高智商", "富二代", "孤儿", "落魄千金", "养女", "暴君", "超凡者", "大魔王", "师兄", "嫡福晋", "恶毒女配", "反派", "高智反派", "贵妃", "后妈", "狐妖", "奸臣", "间谍特工", "老祖宗", "领主", "满级大佬", "美人鱼", "冥王", "魔尊", "女帝", "炮灰", "兽人", "网红", "王爷", "仙尊", "校霸", "邪神", "邪修", "修仙者", "吸血鬼", "玄学大佬", "阴妻", "知青", "阎王", "主神", "宗主", "双学霸", "学霸", "军嫂", "奶妈", "傻女", "职工", "非人类", "网红雇佣兵"]
|
|
123
|
+
|
|
124
|
+
# 内容标签-世界观(最多选 1)
|
|
125
|
+
FANQIE_CONTENT_WORLD = ["女尊", "兽世", "都市江湖", "军旅", "诸天万界", "洪荒", "乡村", "九叔", "规则怪谈", "惊悚游戏", "虫族", "仕途", "穿剧", "穿书", "位面", "港圈", "截教", "70年代", "80年代", "90年代", "港综", "韩娱", "机甲", "抗战", "历史演义", "赛博朋克", "丧尸", "神话复苏", "文娱", "综武", "宋朝", "清朝", "大秦", "三国", "大唐", "明朝", "宝可梦", "奥特曼", "火影", "西游衍生", "封神", "海贼王", "废土", "山海经", "综漫", "综影视", "星际", "四合院", "如懿衍生", "甄嬛衍生", "红楼衍生", "公版衍生", "十日衍生", "斩神衍生", "漫威", "龙珠", "龙族", "王者荣耀", "儒道", "末世冰封"]
|
|
126
|
+
|
|
127
|
+
# ================= 七猫(book-option 接口实抓,官方全量) =================
|
|
128
|
+
|
|
129
|
+
# 七猫·频道→一级→二级级联(男生 11 个一级 / 女生 6 个一级)
|
|
130
|
+
QIMAO_CATS = {
|
|
131
|
+
"男生": {
|
|
132
|
+
"玄幻奇幻": ["东方玄幻", "异世大陆", "诸天万界", "王朝争霸", "西方奇幻"],
|
|
133
|
+
"都市": ["都市高手", "都市高武", "乡村生活", "热血校园", "商战职场", "明星娱乐", "都市生活", "灵气复苏", "官场"],
|
|
134
|
+
"奇闻异事": ["恐怖灵异", "奇门秘术", "侦探推理", "寻宝探险"],
|
|
135
|
+
"武侠仙侠": ["幻想修真", "上古洪荒", "古典仙侠", "武侠幻想", "传统武侠"],
|
|
136
|
+
"军事": ["现代军事", "抗战烽火", "战争幻想", "谍战特工"],
|
|
137
|
+
"历史": ["穿越历史", "架空历史", "历史传记"],
|
|
138
|
+
"游戏": ["虚拟网游", "电子竞技"],
|
|
139
|
+
"科幻": ["未来幻想", "末世危机", "星际机甲"],
|
|
140
|
+
"体育": ["篮球风云", "足球天下", "其他竞技"],
|
|
141
|
+
"N次元": ["原生幻想", "衍生同人", "轻小说"],
|
|
142
|
+
"现实题材": ["现实百态"],
|
|
143
|
+
},
|
|
144
|
+
"女生": {
|
|
145
|
+
"现代言情": ["总裁豪门", "职场情缘", "民国旧影", "娱乐明星", "都市奇幻", "年代重生", "青春校园", "现实生活", "现代悬疑"],
|
|
146
|
+
"古代言情": ["宫闱宅斗", "种田经商", "古代情缘", "远古大陆", "权谋天下", "武侠江湖", "古代悬疑"],
|
|
147
|
+
"幻想言情": ["玄幻仙侠", "异世幻想", "未来科幻", "无限快穿", "末世求生"],
|
|
148
|
+
"游戏竞技": ["运动竞技", "网游电竞"],
|
|
149
|
+
"衍生言情": ["次元衍生", "经典衍生"],
|
|
150
|
+
"现实主义": ["现实故事"],
|
|
151
|
+
},
|
|
161
152
|
}
|
|
162
153
|
|
|
163
|
-
#
|
|
154
|
+
# 兼容并集(男女生一级名互不重叠):老调用方/模板扫描用
|
|
155
|
+
QIMAO_MAIN_CATEGORIES = [m for ch in QIMAO_CATS.values() for m in ch]
|
|
156
|
+
QIMAO_CASCADE = {m: subs for ch in QIMAO_CATS.values() for m, subs in ch.items()}
|
|
157
|
+
|
|
158
|
+
# 七猫·作品标签四组(男女生频道同池),每组必选 1-3 个
|
|
164
159
|
QIMAO_TAG_GROUPS = {
|
|
165
|
-
"风格": [
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
],
|
|
170
|
-
"角色": [
|
|
171
|
-
"战神", "赘婿", "神医", "神豪", "奶爸", "女帝",
|
|
172
|
-
"嫡女", "真千金", "大女主", "兵王", "医生", "主播",
|
|
173
|
-
"总裁", "大佬", "学霸", "团宠", "萌宝", "反派",
|
|
174
|
-
],
|
|
175
|
-
"情节": [
|
|
176
|
-
"系统", "金手指", "签到", "御兽", "重生", "穿越",
|
|
177
|
-
"穿书", "快穿", "逆袭", "开局流", "无敌流", "先婚后爱",
|
|
178
|
-
"追妻火葬场", "破镜重圆", "闪婚", "打脸虐渣", "马甲", "养成",
|
|
179
|
-
],
|
|
180
|
-
"背景": [
|
|
181
|
-
"末世", "无限流", "玄学", "盗墓", "鉴宝", "娱乐圈",
|
|
182
|
-
"电竞", "官场", "架空历史", "三国", "大唐", "大明",
|
|
183
|
-
"大秦", "年代", "民国", "高武", "灵气复苏", "诸天万界",
|
|
184
|
-
"科幻", "赛博朋克", "校园", "都市", "乡村", "海岛",
|
|
185
|
-
"星际", "玄幻", "武侠", "仙侠",
|
|
186
|
-
],
|
|
160
|
+
"风格": ["热血", "轻松", "暗黑", "现实题材", "虐恋", "治愈", "甜宠", "悬疑", "种田", "清穿", "宅斗", "宫斗", "中短篇", "青春", "军事", "爽文", "恐怖", "烧脑", "正剧", "商战", "权谋", "励志", "搞笑", "日常", "短篇", "短故事", "买股文", "第一人称", "纯爱", "快节奏", "反套路", "新疆现实题材", "国安现实题材"],
|
|
161
|
+
"角色": ["兵王", "护花高手", "仙帝", "学生", "赘婿", "神豪", "奶爸", "皇帝", "道士", "女总裁", "校花", "皇后", "王妃", "医妃", "公主", "嫡女", "庶女", "王爷", "太子", "首辅", "丑女", "大叔", "医生", "律师", "吸血鬼", "特工", "法医", "仵作", "女配", "全能", "杀伐果断", "霸道", "偏执", "忠犬", "残疾", "毒舌", "傲娇", "病娇", "萌宝", "龙", "僵尸", "欢喜冤家", "师徒恋", "弃少", "战神", "强强", "魔尊", "团宠", "特种兵", "暴君", "皇叔", "皇子", "摄政王", "奸臣", "世子", "锦衣卫", "太监", "女帝", "明星", "豪门总裁", "老师", "校草", "侦探", "警察", "主播", "少年", "师兄", "领主", "温柔", "呆萌", "可盐可甜", "娇媚", "绿茶", "铁血", "败家子", "弃妇", "女扮男装", "女尊", "女强", "替身", "二婚", "姐姐", "学霸", "天才", "杀手", "将军", "腹黑", "废柴", "反派", "扮猪吃虎", "智商在线", "真假千金", "咸鱼", "锦鲤", "小众职业", "双女主", "后妈", "哥哥", "黑莲花", "黑月光", "白月光", "书生", "糙汉", "老千", "渔女", "丞相", "驸马", "双洁", "作精", "外卖员", "丫鬟", "恶婆婆", "幕后BOSS", "奶团", "大女主", "主母", "妾室", "人间清醒", "无节操", "嘴炮", "痴情", "超A", "高冷", "财迷", "醋王", "万人迷", "戏精", "炮灰", "吃货", "旺夫", "美强惨", "顶流", "小狼狗", "反差萌", "心机", "暖男", "逗比", "毒医", "厨娘", "冰山", "古灵精怪", "宠妻", "黑化", "独宠", "御姐", "佛系", "神医", "真假少爷"],
|
|
162
|
+
"情节": ["鉴宝", "剑道", "透视", "美食", "无敌流", "争霸", "召唤", "盗墓", "快穿", "穿书", "随身空间", "契约婚姻", "娱乐圈", "穿越", "荒岛求生", "家族崛起", "逆袭", "强者回归", "一统天下", "直播", "聊天群", "红包", "御兽", "血脉", "武魂", "丹药", "风水", "青梅竹马", "破镜重圆", "替嫁", "闪婚", "隐婚", "逃婚", "致富", "错嫁", "先婚后爱", "脑洞", "进化", "吞噬", "影视", "无限流", "马甲", "开局流", "签到", "同人", "体育", "官场", "神祇", "修仙", "古穿今", "性转", "HE", "BE", "1V1", "姐弟恋", "相爱相杀", "追妻火葬场", "打脸虐渣", "暗恋", "日久生情", "一见钟情", "带球跑", "职场", "金手指", "异能", "技术流", "全球", "基建", "求生", "丧尸", "机甲", "网游", "游戏", "电竞", "篮球", "足球", "推理", "探险", "怪谈", "长生流", "国术", "古武", "战争", "魔法", "经典衍生", "奇闻异事", "前世今生", "重生", "系统", "复仇", "读心术", "恋综", "双男主", "无CP", "非典型修仙", "双重生", "诡异流", "爱国流", "单元故事", "迪化流", "灵魂互换", "克苏鲁", "稳健流", "扮演", "双向奔赴", "科技", "男二上位", "无女主", "单女主", "离婚后", "逃荒", "国运", "钓鱼", "恋爱", "群穿", "都市脑洞", "玄幻脑洞", "历史脑洞", "港综", "记忆曝光", "囤物资", "玄学", "模拟器", "相亲", "综艺", "规则怪谈", "黑道", "军婚", "谍战", "股海风云", "综武", "全家火葬场", "多女主", "多重人格", "全民", "宠夫", "NBA", "通古今", "全民领主", "草根崛起", "氪金", "荒野求生", "死对头", "天作之合", "婚恋", "双向救赎", "萌宠", "爆红", "失忆", "倒追", "修罗场", "崩人设", "偷听心声", "父子火葬场", "后悔流", "弹幕文", "万物沟通", "影视原著"],
|
|
163
|
+
"背景": ["诸天万界", "三国", "大唐", "水浒", "西游", "漫威", "火影", "海贼王", "英雄联盟", "绝地求生", "王者荣耀", "武侠", "灵气复苏", "神奇宝贝", "奥特曼", "末世", "洪荒", "玄幻", "仙侠", "异界", "西幻", "奇幻", "都市", "年代", "民国", "校园", "古代", "架空历史", "远古", "星际", "历史", "大明", "乡村", "未来世界", "科幻", "精灵鬼怪", "红楼", "封神", "天庭", "卡牌", "神话", "哈利波特", "假面骑士", "龙珠", "游戏衍生", "动漫衍生", "影视衍生", "现代言情", "古代言情", "幻想言情", "高武", "四合院", "兽世", "大秦", "原神", "我的世界", "铠甲勇士", "超神学院", "柯南", "抗战", "宋朝", "赛博朋克", "惊悚世界", "崩坏星穹铁道", "衍生言情", "传统玄幻"],
|
|
187
164
|
}
|
|
@@ -11,20 +11,29 @@
|
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import base64
|
|
15
|
+
import io
|
|
14
16
|
import json
|
|
17
|
+
import mimetypes
|
|
15
18
|
import os
|
|
16
19
|
from pathlib import Path
|
|
17
20
|
|
|
18
|
-
from . import modelhub
|
|
21
|
+
from . import modelhub, runner
|
|
19
22
|
|
|
20
23
|
MAX_TOOL_ITERS = 16 # 单步工具循环上限(防模型打转)
|
|
21
24
|
READ_MAX_BYTES = 64 * 1024 # read_file 单次读取上限
|
|
22
25
|
LIST_MAX_ENTRIES = 200
|
|
26
|
+
MAX_IMAGES = 8 # 单次最多随消息传几张图(请求体防爆)
|
|
27
|
+
IMAGE_MAX_EDGE = 1568 # 长边上限(视觉模型通行建议值,超出等比缩)
|
|
28
|
+
IMAGE_JPEG_BYTES = 3500 * 1024 # 缩放后仍超此大小 → 转 JPEG q85
|
|
29
|
+
IMAGE_MAX_BYTES = 5 * 1024 * 1024 # 单张最终字节硬上限(超出剔除并日志)
|
|
23
30
|
|
|
24
31
|
_SYSTEM_PROMPT = """你是 CodeBee 的内置执行智能体,直接完成用户交代的任务。用户的目标、背景与工作目录内的附件就是全部输入。
|
|
25
32
|
|
|
26
33
|
## 工作方式
|
|
27
34
|
- 需要读文件、看目录、写文件时调用工具;所有路径都是工作目录内的相对路径。
|
|
35
|
+
- read_file 只能读文本文件;图片、压缩包等二进制文件读不了,如实告知用户即可,不要反复尝试。
|
|
36
|
+
- 用户消息中的图片附件会直接出现在对话里,可直接看图作答,无需用工具读取。
|
|
28
37
|
- 产出文件一律 UTF-8 编码。
|
|
29
38
|
- 回答用户的语言与用户一致(默认中文)。直接给结论和内容,不要输出任何机器标记或协议行。"""
|
|
30
39
|
|
|
@@ -90,6 +99,131 @@ def _tool_list_files(workdir, args):
|
|
|
90
99
|
return "\n".join(out) or "(空目录)"
|
|
91
100
|
|
|
92
101
|
|
|
102
|
+
_BIN_MAGIC = (
|
|
103
|
+
(b"\x89PNG\r\n\x1a\n", "PNG 图片"),
|
|
104
|
+
(b"\xff\xd8\xff", "JPEG 图片"),
|
|
105
|
+
(b"GIF8", "GIF 图片"),
|
|
106
|
+
(b"%PDF-", "PDF 文档"),
|
|
107
|
+
(b"PK\x03\x04", "ZIP 压缩包"),
|
|
108
|
+
(b"\x1f\x8b", "GZIP 压缩包"),
|
|
109
|
+
(b"7z\xbc\xaf\x27\x1c", "7z 压缩包"),
|
|
110
|
+
(b"Rar!\x1a\x07", "RAR 压缩包"),
|
|
111
|
+
(b"\x00\x00\x01\x00", "ICO 图标"),
|
|
112
|
+
(b"OggS", "OGG 音频"),
|
|
113
|
+
(b"ID3", "MP3 音频"),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _bin_format(head):
|
|
118
|
+
"""常见二进制魔数 → 人类可读格式名(写进给模型的提示,让它立刻止损)。"""
|
|
119
|
+
if len(head) >= 12 and head[:4] == b"RIFF":
|
|
120
|
+
return {"WEBP": "WEBP 图片", "WAVE": "WAV 音频",
|
|
121
|
+
"AVI ": "AVI 视频"}.get(head[8:12].decode("ascii", "replace"),
|
|
122
|
+
"RIFF 媒体")
|
|
123
|
+
if len(head) >= 8 and head[4:8] == b"ftyp":
|
|
124
|
+
return "MP4/MOV 视频"
|
|
125
|
+
for magic, name in _BIN_MAGIC:
|
|
126
|
+
if head.startswith(magic):
|
|
127
|
+
return name
|
|
128
|
+
return "二进制文件"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _looks_binary(head):
|
|
132
|
+
"""文本判定从宽:NUL 字节即判二进制(git 同款启发式),控制字符占比兜底
|
|
133
|
+
(部分二进制头 8KB 内无 NUL)。"""
|
|
134
|
+
if not head:
|
|
135
|
+
return False
|
|
136
|
+
if b"\x00" in head:
|
|
137
|
+
return True
|
|
138
|
+
sample = head[:8192]
|
|
139
|
+
ctrl = sum(1 for b in sample if b < 32 and b not in (9, 10, 12, 13))
|
|
140
|
+
return ctrl * 20 > len(sample) # >5% 视为二进制
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _trim_partial_utf8(chunk):
|
|
144
|
+
"""头部截断可能把多字节字符切成两半:剥掉尾部不完整序列,否则 UTF-8
|
|
145
|
+
strict 失败会误落 GBK 解出错字(runner.tail_decoded 的头截断对偶)。"""
|
|
146
|
+
i = len(chunk) - 1
|
|
147
|
+
while i >= 0 and i >= len(chunk) - 3 and (chunk[i] & 0xC0) == 0x80:
|
|
148
|
+
i -= 1
|
|
149
|
+
if 0 <= i < len(chunk):
|
|
150
|
+
b = chunk[i]
|
|
151
|
+
need = 4 if (b & 0xF8) == 0xF0 else 3 if (b & 0xF0) == 0xE0 \
|
|
152
|
+
else 2 if (b & 0xC0) == 0xC0 else 1
|
|
153
|
+
if len(chunk) - i < need:
|
|
154
|
+
return chunk[:i]
|
|
155
|
+
return chunk
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _guess_image_mime(path, raw=b""):
|
|
159
|
+
m = mimetypes.guess_type(str(path))[0]
|
|
160
|
+
if m:
|
|
161
|
+
return m
|
|
162
|
+
if raw.startswith(b"\x89PNG"):
|
|
163
|
+
return "image/png"
|
|
164
|
+
if raw.startswith(b"\xff\xd8"):
|
|
165
|
+
return "image/jpeg"
|
|
166
|
+
if raw.startswith(b"GIF8"):
|
|
167
|
+
return "image/gif"
|
|
168
|
+
if raw.startswith(b"RIFF"):
|
|
169
|
+
return "image/webp"
|
|
170
|
+
return "image/png"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _prep_images(paths, log=None):
|
|
174
|
+
"""图片路径 → [(mime, b64)]。Pillow 可用时:长边超 1568px 等比缩(万级像素
|
|
175
|
+
原图既烧 token 又常被网关拒),仍超 3.5MB 转 JPEG q85;Pillow 缺失或解码
|
|
176
|
+
失败时原图直传,单张超 5MB 剔除并日志。最多 MAX_IMAGES 张。"""
|
|
177
|
+
try:
|
|
178
|
+
from PIL import Image
|
|
179
|
+
except Exception:
|
|
180
|
+
Image = None
|
|
181
|
+
out = []
|
|
182
|
+
for p in (paths or [])[:MAX_IMAGES]:
|
|
183
|
+
try:
|
|
184
|
+
raw = Path(p).read_bytes()
|
|
185
|
+
except OSError:
|
|
186
|
+
if log:
|
|
187
|
+
log("[图片] 跳过(读取失败): %s" % os.path.basename(str(p)))
|
|
188
|
+
continue
|
|
189
|
+
mime = _guess_image_mime(p, raw)
|
|
190
|
+
data = raw
|
|
191
|
+
if Image is not None:
|
|
192
|
+
try:
|
|
193
|
+
im = Image.open(io.BytesIO(raw))
|
|
194
|
+
im.load()
|
|
195
|
+
w, h = im.size
|
|
196
|
+
edge = max(w, h, 1)
|
|
197
|
+
if edge > IMAGE_MAX_EDGE:
|
|
198
|
+
r = IMAGE_MAX_EDGE / float(edge)
|
|
199
|
+
im = im.resize((max(1, round(w * r)), max(1, round(h * r))),
|
|
200
|
+
Image.LANCZOS)
|
|
201
|
+
fmt = {"image/png": "PNG", "image/jpeg": "JPEG",
|
|
202
|
+
"image/gif": "GIF", "image/webp": "WEBP"}.get(mime, "PNG")
|
|
203
|
+
buf = io.BytesIO()
|
|
204
|
+
im.save(buf, format=fmt) # GIF 多帧仅存首帧,可接受
|
|
205
|
+
data = buf.getvalue()
|
|
206
|
+
except Exception:
|
|
207
|
+
data = raw # 解码/编码失败退回原始字节
|
|
208
|
+
if (Image is not None and mime != "image/jpeg"
|
|
209
|
+
and len(data) > IMAGE_JPEG_BYTES):
|
|
210
|
+
try: # 仍过大:转 JPEG q85(RGB 拍平 alpha)
|
|
211
|
+
im = Image.open(io.BytesIO(data)).convert("RGB")
|
|
212
|
+
buf = io.BytesIO()
|
|
213
|
+
im.save(buf, format="JPEG", quality=85)
|
|
214
|
+
data = buf.getvalue()
|
|
215
|
+
mime = "image/jpeg"
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
if len(data) > IMAGE_MAX_BYTES:
|
|
219
|
+
if log:
|
|
220
|
+
log("[图片] 剔除(过大 %d MB): %s"
|
|
221
|
+
% (len(data) // (1024 * 1024), os.path.basename(str(p))))
|
|
222
|
+
continue
|
|
223
|
+
out.append((mime, base64.b64encode(data).decode("ascii")))
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
93
227
|
def _tool_read_file(workdir, args):
|
|
94
228
|
rel = str(args.get("path") or "").replace("\\", "/").strip("/")
|
|
95
229
|
if not rel or ".." in Path(rel).parts or any(":" in seg for seg in Path(rel).parts):
|
|
@@ -102,7 +236,25 @@ def _tool_read_file(workdir, args):
|
|
|
102
236
|
return "(文件不存在: %s)" % rel
|
|
103
237
|
data = p.read_bytes()[:READ_MAX_BYTES + 1]
|
|
104
238
|
truncated = len(data) > READ_MAX_BYTES
|
|
105
|
-
|
|
239
|
+
head = data[:READ_MAX_BYTES]
|
|
240
|
+
if not head:
|
|
241
|
+
return "(空文件)"
|
|
242
|
+
if _looks_binary(head):
|
|
243
|
+
try:
|
|
244
|
+
size = p.stat().st_size
|
|
245
|
+
except OSError:
|
|
246
|
+
size = len(head)
|
|
247
|
+
fmt = _bin_format(head)
|
|
248
|
+
if "图片" in fmt:
|
|
249
|
+
return ("(图片文件: %s — %s,%d B,无法按文本读取。用户以附件发来的图片会"
|
|
250
|
+
"直接出现在对话中,无需用工具读取;若需要目录里这张图的内容,"
|
|
251
|
+
"请提示用户把它作为附件发送。)" % (rel, fmt, size))
|
|
252
|
+
return ("(二进制文件,无法按文本读取: %s — %s,%d B。read_file 只支持文本文件;"
|
|
253
|
+
"请如实告知用户该文件内容无法以文本方式查看,不要反复重读。)"
|
|
254
|
+
% (rel, fmt, size))
|
|
255
|
+
if truncated:
|
|
256
|
+
head = _trim_partial_utf8(head)
|
|
257
|
+
text = runner.decode_output(head)
|
|
106
258
|
return ("…(超过 64KB 已截断)\n" if truncated else "") + text
|
|
107
259
|
|
|
108
260
|
|
|
@@ -123,7 +275,7 @@ def _tool_write_file(workdir, args):
|
|
|
123
275
|
TOOLS_SPEC = [
|
|
124
276
|
{"name": "list_files", "description": "列出工作目录(或其子目录)下的文件",
|
|
125
277
|
"args": {"path": "子目录相对路径,留空=根目录"}},
|
|
126
|
-
{"name": "read_file", "description": "读取工作目录内一个文本文件(UTF-8
|
|
278
|
+
{"name": "read_file", "description": "读取工作目录内一个文本文件(UTF-8/GBK 自动识别,超 64KB 截断;图片等二进制文件无法读取)",
|
|
127
279
|
"args": {"path": "文件相对路径"}},
|
|
128
280
|
{"name": "write_file", "description": "把文本内容写入工作目录内一个文件(UTF-8,父目录自动创建)",
|
|
129
281
|
"args": {"path": "文件相对路径", "content": "完整文本内容"}},
|
|
@@ -182,6 +334,13 @@ def _m_openai(m):
|
|
|
182
334
|
if m["role"] == "tool_results":
|
|
183
335
|
return [{"role": "tool", "tool_call_id": cid, "content": text}
|
|
184
336
|
for cid, text in m["tool_results"]]
|
|
337
|
+
imgs = m.get("images") or []
|
|
338
|
+
if imgs: # 多模态:content 变数组,图跟在文本后(role=tool 只收文本,故图只走 user)
|
|
339
|
+
content = [{"type": "text", "text": m.get("content") or ""}]
|
|
340
|
+
content += [{"type": "image_url",
|
|
341
|
+
"image_url": {"url": "data:%s;base64,%s" % (mime, b64)}}
|
|
342
|
+
for mime, b64 in imgs]
|
|
343
|
+
return {"role": "user", "content": content}
|
|
185
344
|
return {"role": "user", "content": m.get("content") or ""}
|
|
186
345
|
|
|
187
346
|
|
|
@@ -198,6 +357,13 @@ def _m_anthropic(m):
|
|
|
198
357
|
return {"role": "user", "content": [
|
|
199
358
|
{"type": "tool_result", "tool_use_id": cid, "content": text}
|
|
200
359
|
for cid, text in m["tool_results"]]}
|
|
360
|
+
imgs = m.get("images") or []
|
|
361
|
+
if imgs:
|
|
362
|
+
blocks = [{"type": "text", "text": m.get("content") or ""}]
|
|
363
|
+
blocks += [{"type": "image",
|
|
364
|
+
"source": {"type": "base64", "media_type": mime, "data": b64}}
|
|
365
|
+
for mime, b64 in imgs]
|
|
366
|
+
return {"role": "user", "content": blocks}
|
|
201
367
|
return {"role": "user", "content": [{"type": "text", "text": m.get("content") or ""}]}
|
|
202
368
|
|
|
203
369
|
|
|
@@ -210,9 +376,15 @@ def _build_request(proto, base, use_key, model, system, msgs, with_tools):
|
|
|
210
376
|
else:
|
|
211
377
|
url = base + "/v1beta/models/%s:generateContent" % model
|
|
212
378
|
headers = {"x-goog-api-key": use_key}
|
|
213
|
-
contents = [
|
|
214
|
-
|
|
215
|
-
|
|
379
|
+
contents = []
|
|
380
|
+
for m in msgs:
|
|
381
|
+
if m["role"] not in ("user", "assistant"):
|
|
382
|
+
continue
|
|
383
|
+
parts = [{"text": m.get("content") or ""}]
|
|
384
|
+
parts += [{"inline_data": {"mime_type": mime, "data": b64}}
|
|
385
|
+
for mime, b64 in (m.get("images") or [])]
|
|
386
|
+
contents.append({"role": ("user" if m["role"] == "user" else "model"),
|
|
387
|
+
"parts": parts})
|
|
216
388
|
body = {"contents": contents,
|
|
217
389
|
"systemInstruction": {"parts": [{"text": system}]},
|
|
218
390
|
"generationConfig": {"maxOutputTokens": 8000}}
|
|
@@ -291,10 +463,11 @@ def _parse_reply(proto, data):
|
|
|
291
463
|
|
|
292
464
|
# ---------------------------------------------------------------- 主循环
|
|
293
465
|
|
|
294
|
-
def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None):
|
|
466
|
+
def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=None):
|
|
295
467
|
"""跑一次内置智能体(内部自带工具循环直到给出最终回答)。
|
|
296
468
|
|
|
297
469
|
bi: resolve() 的返回;prompt: 本轮完整输入(目标/续轮块由 pipeline 拼);
|
|
470
|
+
images: 用户随本轮消息提供的图片绝对路径列表(任务附件/对话追话);
|
|
298
471
|
log: 追加一行日志的回调(步骤日志)。返回 runner 风格统一结果:
|
|
299
472
|
{ok, text, usage, error, model, provider_name, provider_id, iterations, cost_usd}。
|
|
300
473
|
"""
|
|
@@ -302,7 +475,16 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None):
|
|
|
302
475
|
model = bi["model"]
|
|
303
476
|
allow_private = bool(prov.get("allow_private"))
|
|
304
477
|
system = _SYSTEM_PROMPT + "\n\n## 工作目录\n%s" % os.path.abspath(workdir or ".")
|
|
305
|
-
|
|
478
|
+
imgs = _prep_images(images, log) if images else []
|
|
479
|
+
if imgs and not modelhub._model_image_in(prov, model):
|
|
480
|
+
# 能力闸门:模型未声明图片输入就不塞图(网关多半会拒),改为显式告知
|
|
481
|
+
if log:
|
|
482
|
+
log("[图片] 模型 %s 未开启图片输入,%d 张图不随消息发送" % (model, len(imgs)))
|
|
483
|
+
prompt = ("(用户提供了 %d 张图片,但当前模型未开启图片输入;请在回答开头提示用户"
|
|
484
|
+
"到绑定页为该模型开启「图」开关,或改绑多模态模型。本条回答请基于"
|
|
485
|
+
"文字部分完成。)\n\n%s" % (len(imgs), prompt))
|
|
486
|
+
imgs = []
|
|
487
|
+
msgs = [{"role": "user", "content": prompt, "images": imgs}]
|
|
306
488
|
candidates = list(modelhub._protocol_candidates(prov))
|
|
307
489
|
tools_ok = all(proto != "google" for proto, _ in candidates) # google wire 无工具协议
|
|
308
490
|
total_usage = {"input": 0, "cached": 0, "output": 0, "total": 0}
|