codebee 0.1.15 → 0.1.16
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/README.md +9 -8
- package/app/core/bookmeta.py +10 -3
- package/app/core/bookmeta_catalog.py +16 -0
- package/app/core/covergen.py +176 -56
- package/app/core/modelhub.py +23 -3
- package/app/core/notify.py +11 -1
- package/app/core/paihang.py +5 -2
- package/app/core/pipeline.py +80 -4
- package/app/core/publish/browser.py +21 -7
- package/app/core/publish/fanqie.py +16 -2
- package/app/core/publish/flow.py +25 -1
- package/app/core/publish/flows-fanqie-calibrated.json +324 -37
- package/app/core/publish/manager.py +25 -9
- package/app/core/publish/qimao.py +18 -3
- package/app/core/runner.py +19 -4
- package/app/core/settings.py +3 -1
- package/app/core/zentao.py +26 -26
- package/app/main.py +20 -0
- package/app/ui/app.js +411 -52
- package/app/ui/i18n.js +77 -7
- package/app/ui/index.html +25 -47
- package/app/ui/style.css +116 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
+
## v0.1.16(2026-09-19)
|
|
8
|
+
|
|
9
|
+
- 帮助中心:首启欢迎引导升级为「左目录 + 右内容」多章帮助中心,七章全就绪——快速上手、模型接入与绑定(多 Key 轮换 / 协议徽章解读 / 报错速查 / 免费档起步)、功能一览、连载创作流程、发布上架、自动化与技能市场、常见问题(9 条踩坑 FAQ)。首启仍只自动弹一次落在「快速上手」,手动打开(设置导航/关于页/Ctrl+K)徽章显示「帮助中心」
|
|
10
|
+
- 就地帮助问号:模型接入、CLI 绑定、自动化、插件市场四个页头新增小「?」,点击直达帮助中心对应章节;供应商空状态与「还没有装好的 CLI」空状态附「看帮助」链接
|
|
11
|
+
|
|
7
12
|
## v0.1.15(2026-09-19)
|
|
8
13
|
|
|
9
14
|
- 禅道集成:定时扫描激活 Bug → 自动建修复任务 → 跑完自动合并 + resolve(fixed) + 报告回写 + 群通知(REST v1,SSRF 守卫;设置页「禅道」子页)
|
package/README.md
CHANGED
|
@@ -21,14 +21,10 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- **可分享报告页**:运行报告一键导出自包含单文件 HTML
|
|
29
|
-
- **引擎调参卡**:设置页直接调压缩阈值/goal 轮数等引擎参数(防并发覆盖)
|
|
30
|
-
- 出站 HTTPS 证书校验加固(修复部分 Mac 上的全外联失败);发布浏览器 profile 迁出仓库目录
|
|
31
|
-
- 群推送(钉钉/飞书/企微自动适配)与「扫榜选材」任务类型见 v0.1.14
|
|
24
|
+
### 最新版更新内容(v0.1.16)
|
|
25
|
+
|
|
26
|
+
- **帮助中心**:首启欢迎引导升级为多章帮助中心——快速上手 / 模型接入与绑定 / 连载创作流程 / 发布上架 / 自动化与技能市场 / 功能一览 / 常见问题七章全就绪;模型接入、CLI 绑定、自动化、插件市场页头有「?」就地问号直达对应章节;设置导航、关于页、Ctrl+K 随时可重开
|
|
27
|
+
- 禅道集成、扫榜选材(双源)、可分享报告页、引擎调参卡见 v0.1.15
|
|
32
28
|
<!-- relnotes:end -->
|
|
33
29
|
|
|
34
30
|
---
|
|
@@ -106,6 +102,11 @@ OpenCode / Continue / Cursor / Trae 十种工具的配置,一次带入全部
|
|
|
106
102
|
- **运行中指挥**:任务跑着也能递话——文字 / 截图 / 附件注入下一轮;
|
|
107
103
|
- **对话模式**:不拆步骤、直接执行,时间线式对话页,随时追话续上下文;
|
|
108
104
|
- **自动化**:定时任务复用真实运行链,到点自动开跑(错过的一次性任务不补跑);
|
|
105
|
+
- **禅道 Bug 自动修复**:多产品档案(每产品配我方端/前后端仓库/负责人),定时扫描
|
|
106
|
+
激活 Bug 先排查定责(模块路由规则 > AI 判端 > 留人工)——我方端自动建修复任务
|
|
107
|
+
跑完整代码链,修完自动合并;双端问题修完我方部分后带修复报告转派另一端负责人,
|
|
108
|
+
非我方转回报告人;测试指错人也按排查结论改派;修复失败自动升级转派;群机器人
|
|
109
|
+
全程播报(设置页「禅道」子页配置);
|
|
109
110
|
- **多任务并发**:worker 池默认 3(1-6 可调),互不打扰;连载任务超时/中断自动
|
|
110
111
|
断点续跑;
|
|
111
112
|
- **远程访问**:手机同一 WiFi 直开、Tailscale 外网可达、Cloudflare Tunnel 固定
|
package/app/core/bookmeta.py
CHANGED
|
@@ -274,9 +274,16 @@ def _norm(data, platform, goal=""):
|
|
|
274
274
|
channel = cat.QIMAO_CATS.get(reader) or {}
|
|
275
275
|
c_main = _in_table(data.get("category_main"), list(channel))
|
|
276
276
|
c_sub = _name(data.get("category_sub"), 10)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
if c_sub and not (c_main and c_sub in (channel.get(c_main) or [])):
|
|
278
|
+
# 一级/二级不自洽(一级空、或二级不属于该频道——如女生专属的
|
|
279
|
+
# 「现实故事」配了男生频道):跨频道反查官方目录纠正,频道与
|
|
280
|
+
# 一级跟着二级走(表单级联的选项集以此为准);全目录都没有
|
|
281
|
+
# 才清空——表单选不出的词不硬填
|
|
282
|
+
loc = cat.qimao_locate_sub(c_sub)
|
|
283
|
+
if loc:
|
|
284
|
+
reader, c_main = loc
|
|
285
|
+
else:
|
|
286
|
+
c_sub = ""
|
|
280
287
|
# 作品标签四组(官方每组必选 1-3):逐组过滤 + 截到 3;老数据 tags 并入
|
|
281
288
|
legacy_tags = data.get("tags") if isinstance(data.get("tags"), list) else []
|
|
282
289
|
groups = {}
|
|
@@ -155,6 +155,22 @@ QIMAO_CATS = {
|
|
|
155
155
|
QIMAO_MAIN_CATEGORIES = [m for ch in QIMAO_CATS.values() for m in ch]
|
|
156
156
|
QIMAO_CASCADE = {m: subs for ch in QIMAO_CATS.values() for m, subs in ch.items()}
|
|
157
157
|
|
|
158
|
+
|
|
159
|
+
def qimao_locate_sub(sub):
|
|
160
|
+
"""二级分类跨频道反查:返回 (频道, 一级分类),目录里没有则 None。
|
|
161
|
+
|
|
162
|
+
级联表单的二级选项集随频道变化,「现实故事」(女生·现实主义)
|
|
163
|
+
等词被编排者配到男生频道时,只能从官方全目录反查出真实归属,
|
|
164
|
+
供规整层纠正 target_reader/category_main 错配。"""
|
|
165
|
+
sub = (sub or "").strip()
|
|
166
|
+
if not sub:
|
|
167
|
+
return None
|
|
168
|
+
for ch, mains in QIMAO_CATS.items():
|
|
169
|
+
for main, subs in mains.items():
|
|
170
|
+
if sub in subs:
|
|
171
|
+
return ch, main
|
|
172
|
+
return None
|
|
173
|
+
|
|
158
174
|
# 七猫·作品标签四组(男女生频道同池),每组必选 1-3 个
|
|
159
175
|
QIMAO_TAG_GROUPS = {
|
|
160
176
|
"风格": ["热血", "轻松", "暗黑", "现实题材", "虐恋", "治愈", "甜宠", "悬疑", "种田", "清穿", "宅斗", "宫斗", "中短篇", "青春", "军事", "爽文", "恐怖", "烧脑", "正剧", "商战", "权谋", "励志", "搞笑", "日常", "短篇", "短故事", "买股文", "第一人称", "纯爱", "快节奏", "反套路", "新疆现实题材", "国安现实题材"],
|
package/app/core/covergen.py
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
"""封面图生成:调供应商图像 API 产出竖版封面插画,curl 直接落盘到运行目录。
|
|
3
3
|
|
|
4
4
|
接口形状:OpenAI 兼容 POST {base}/images/generations(Z.ai cogview 系列、
|
|
5
|
-
多数聚合网关都支持,默认返回图片 URL
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
多数聚合网关都支持,默认返回图片 URL)。端点选取:编排者供应商的 openai 面
|
|
6
|
+
(显式协议或 wire_caps 实测出的另一协议面)优先,其余开了 openai 面的供应商
|
|
7
|
+
按优先级自动跟上——编排者走 anthropic 面不再挡封面。已知主机会补规范图像面
|
|
8
|
+
(智谱 anthropic 面 key 同源,图像只走 /api/paas/v4;实测 face 对图像回
|
|
9
|
+
HTTP 200+失败信封,规范面放前面省一轮空试)。错误信息带响应体真因(聚合
|
|
10
|
+
网关「无可用渠道」不再吞成光秃 HTTPError)。
|
|
11
|
+
图像模型:环境变量
|
|
12
|
+
CODEBEE_IMAGE_MODEL 优先,其次供应商模型清单里认得出的图像模型(关键词),
|
|
13
|
+
否则逐个试默认候选(cogview-3-flash → cogview-4),第一个 2xx 的胜出;
|
|
14
|
+
同一模型先试竖版尺寸再回落方图。
|
|
8
15
|
状态机与建书生成同款(running/done/failed 写任务 cover_gen 字段,
|
|
9
|
-
bump_state 推 SSE
|
|
16
|
+
bump_state 推 SSE)。
|
|
10
17
|
|
|
11
18
|
安全边界:
|
|
12
19
|
- 图像 URL 仅接受 https,解析后 IP 命中私网/环回/链路本地一律拒绝
|
|
@@ -16,16 +23,22 @@ bump_state 推 SSE)。仅支持 openai 协议供应商。
|
|
|
16
23
|
"""
|
|
17
24
|
from __future__ import annotations
|
|
18
25
|
|
|
26
|
+
import json
|
|
19
27
|
import logging
|
|
20
28
|
import os
|
|
21
29
|
import socket
|
|
22
30
|
import threading
|
|
23
31
|
import time
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.request
|
|
24
34
|
from pathlib import Path
|
|
25
35
|
|
|
26
36
|
log = logging.getLogger(__name__)
|
|
27
37
|
|
|
28
38
|
_IMAGE_CANDIDATES = ("cogview-3-flash", "cogview-4")
|
|
39
|
+
# 模型清单里认得出的图像模型关键词(命中即列为候选,保持清单序)
|
|
40
|
+
_IMAGE_MODEL_KEYWORDS = ("cogview", "dall-e", "gpt-image", "flux", "kolors",
|
|
41
|
+
"seedream", "stable-diffusion", "hidream", "imagen")
|
|
29
42
|
_SIZES = ("768x1344", "1024x1024") # 竖版优先,方图兜底
|
|
30
43
|
|
|
31
44
|
|
|
@@ -50,6 +63,87 @@ def _pick_key(prov):
|
|
|
50
63
|
return prov.get("api_key") or ""
|
|
51
64
|
|
|
52
65
|
|
|
66
|
+
# 已知主机的规范图像面:wire_caps 记的 openai 面只保 chat 可用,图像端点在
|
|
67
|
+
# 各家规范路径上(真案:智谱 anthropic 面 key 同源可出图,但 wire_caps 面的
|
|
68
|
+
# /api/anthropic 路径对图像返回 HTTP 200+失败信封,必须走 /api/paas/v4)。
|
|
69
|
+
_CANON_IMAGE_FACE = {
|
|
70
|
+
"open.bigmodel.cn": "https://open.bigmodel.cn/api/paas/v4",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _openai_face(prov):
|
|
75
|
+
"""该供应商可用的 openai 面地址:显式 openai 协议用本体;其余只认 wire_caps
|
|
76
|
+
里实测通过的 openai 面——两协议面的路径不同(Z.ai:/api/anthropic vs
|
|
77
|
+
/api/paas/v4),靠猜必错,与绑定链「只挑实测 wire」同纪律。"""
|
|
78
|
+
proto = str(prov.get("protocol") or "openai")
|
|
79
|
+
base = str(prov.get("base_url") or "").rstrip("/")
|
|
80
|
+
if proto == "openai" and base:
|
|
81
|
+
return base
|
|
82
|
+
cap = (prov.get("wire_caps") or {}).get("openai") or {}
|
|
83
|
+
return str(cap.get("base") or "").rstrip("/")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _image_bases(prov):
|
|
87
|
+
"""该供应商可试的图像端点(去重保序):已知主机的规范图像面在前——
|
|
88
|
+
实测 face 若恰好是 chat 代理路径(智谱 anthropic 面),对图像必是假 200;
|
|
89
|
+
规范面直接出图省一轮空试。无规范表的其余主机仍用实测 face。"""
|
|
90
|
+
from urllib.parse import urlparse
|
|
91
|
+
face = _openai_face(prov)
|
|
92
|
+
host = urlparse(face or str(prov.get("base_url") or "")).hostname or ""
|
|
93
|
+
canon = _CANON_IMAGE_FACE.get(host) or ""
|
|
94
|
+
bases = [b for b in (canon, face) if b]
|
|
95
|
+
seen, out = set(), []
|
|
96
|
+
for b in bases:
|
|
97
|
+
if b not in seen:
|
|
98
|
+
seen.add(b)
|
|
99
|
+
out.append(b)
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _image_models(prov):
|
|
104
|
+
"""图像模型候选序:环境变量 CODEBEE_IMAGE_MODEL > 供应商模型清单里关键词
|
|
105
|
+
命中的图像模型(保持清单序)> 默认候选 cogview(Z.ai 免费档先行)。"""
|
|
106
|
+
models = []
|
|
107
|
+
env_model = os.environ.get("CODEBEE_IMAGE_MODEL", "").strip()
|
|
108
|
+
if env_model:
|
|
109
|
+
models.append(env_model)
|
|
110
|
+
for m in (prov.get("models") or []):
|
|
111
|
+
name = str((m.get("name") if isinstance(m, dict) else m) or "").strip()
|
|
112
|
+
low = name.lower()
|
|
113
|
+
if name and any(k in low for k in _IMAGE_MODEL_KEYWORDS) and name not in models:
|
|
114
|
+
models.append(name)
|
|
115
|
+
models.extend(m for m in _IMAGE_CANDIDATES if m not in models)
|
|
116
|
+
return models
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _candidates():
|
|
120
|
+
"""图像端点候选 [{label, bases, key, allow_private, prov}]:编排者供应商排
|
|
121
|
+
最前,其余启用且配了密钥的供应商按列表优先级跟上;每家可试端点见
|
|
122
|
+
_image_bases(实测 face + 已知主机规范面);同一(端点组+密钥)只收一次。"""
|
|
123
|
+
from . import modelhub
|
|
124
|
+
orch = modelhub.resolve_orchestrator()
|
|
125
|
+
first = [orch[0]] if orch else []
|
|
126
|
+
first_ids = {p.get("id") for p in first}
|
|
127
|
+
rest = [p for p in modelhub.providers() if p.get("id") not in first_ids]
|
|
128
|
+
out, seen = [], set()
|
|
129
|
+
for prov in first + rest:
|
|
130
|
+
if not prov.get("enabled", True) or not prov.get("api_key"):
|
|
131
|
+
continue
|
|
132
|
+
bases = _image_bases(prov)
|
|
133
|
+
key = _pick_key(prov)
|
|
134
|
+
if not bases or not key:
|
|
135
|
+
continue
|
|
136
|
+
sig = (tuple(bases), key)
|
|
137
|
+
if sig in seen:
|
|
138
|
+
continue
|
|
139
|
+
seen.add(sig)
|
|
140
|
+
out.append({"label": prov.get("name") or prov.get("id") or "供应商",
|
|
141
|
+
"bases": bases, "key": key,
|
|
142
|
+
"allow_private": bool(prov.get("allow_private")),
|
|
143
|
+
"prov": prov})
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
53
147
|
def _safe_image_url(url):
|
|
54
148
|
"""SSRF 校验:仅 https;解析主机全部 IP,私网/环回/链路本地/保留段拒绝。"""
|
|
55
149
|
import ipaddress
|
|
@@ -86,75 +180,101 @@ def _curl_to(url, out_path):
|
|
|
86
180
|
raise RuntimeError("下载内容过小或为空")
|
|
87
181
|
|
|
88
182
|
|
|
183
|
+
def _post_images(url, key, body, allow_private, timeout):
|
|
184
|
+
"""图像接口专用 POST:非 2xx 也读响应体真因。_post_json_http 对 HTTPError
|
|
185
|
+
直接返回 (0, None, repr),聚合网关的「无可用渠道」会被吞成光秃
|
|
186
|
+
HTTPError 503;复用同一套 SSRF 校验与出站 TLS 上下文。"""
|
|
187
|
+
from . import modelhub, tlsctx
|
|
188
|
+
_host, herr = modelhub._validate_host(url, allow_private)
|
|
189
|
+
if _host is None:
|
|
190
|
+
return 0, None, herr
|
|
191
|
+
try:
|
|
192
|
+
req = urllib.request.Request(url, method="POST",
|
|
193
|
+
headers={"Authorization": "Bearer " + key,
|
|
194
|
+
"Content-Type": "application/json"},
|
|
195
|
+
data=json.dumps(body).encode("utf-8"))
|
|
196
|
+
with modelhub._opener().open(req, timeout=timeout) as resp:
|
|
197
|
+
return resp.status, json.loads(resp.read(1024 * 1024).decode("utf-8", "replace")), ""
|
|
198
|
+
except urllib.error.HTTPError as e:
|
|
199
|
+
raw = e.read(65536).decode("utf-8", "replace")
|
|
200
|
+
try:
|
|
201
|
+
data = json.loads(raw)
|
|
202
|
+
except Exception:
|
|
203
|
+
data = None
|
|
204
|
+
msg = ""
|
|
205
|
+
if isinstance(data, dict):
|
|
206
|
+
msg = str((data.get("error") or {}).get("message")
|
|
207
|
+
or data.get("message") or data.get("msg") or "")
|
|
208
|
+
if not msg:
|
|
209
|
+
msg = "HTTP %s %s" % (e.code, getattr(e, "reason", "") or "")
|
|
210
|
+
return e.code, data, msg[:300]
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return 0, None, tlsctx.humanize(repr(e)[:300])
|
|
213
|
+
|
|
214
|
+
|
|
89
215
|
def _call_images(base, key, model, prompt, size, allow_private):
|
|
90
216
|
"""POST /images/generations。返回 (data_item dict, 错误串);data_item 含 url 或 b64_json。"""
|
|
91
|
-
from . import builtin_agent
|
|
92
217
|
url = base.rstrip("/") + "/images/generations"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
218
|
+
body = {"model": model, "prompt": prompt, "size": size}
|
|
219
|
+
if os.environ.get("CODEBEE_IMAGE_WATERMARK", "").strip() != "1":
|
|
220
|
+
# 智谱缺省 watermark=true,图上强制「AI生成」显式角标+隐式数字水印;
|
|
221
|
+
# 实测传 false 被 200 接受。CODEBEE_IMAGE_WATERMARK=1 恢复供应商默认。
|
|
222
|
+
body["watermark"] = False
|
|
223
|
+
status, data, err = _post_images(url, key, body, allow_private, 180)
|
|
98
224
|
if status == 0:
|
|
99
225
|
return None, err or "网络错误"
|
|
100
226
|
if 200 <= status < 300:
|
|
227
|
+
if isinstance(data, dict) and data.get("success") is False:
|
|
228
|
+
# 智谱 anthropic 面对未知子路径回 HTTP 200+失败信封,按失败处理
|
|
229
|
+
return None, str(data.get("msg") or "供应商返回失败信封")
|
|
101
230
|
items = (data or {}).get("data") or []
|
|
102
231
|
if items and isinstance(items[0], dict):
|
|
103
232
|
return items[0], ""
|
|
104
233
|
return None, "响应缺少 data[0]"
|
|
105
|
-
|
|
106
|
-
or err or ("HTTP %s" % status)
|
|
107
|
-
return None, last
|
|
234
|
+
return None, err or ("HTTP %s" % status)
|
|
108
235
|
|
|
109
236
|
|
|
110
237
|
def make_cover(run_id, task):
|
|
111
238
|
"""同步生成封面到运行目录。成功/失败返回 entry dict(写任务 cover_gen 用)。"""
|
|
112
|
-
from . import builtin_agent,
|
|
239
|
+
from . import builtin_agent, paths
|
|
113
240
|
try:
|
|
114
|
-
|
|
115
|
-
if not
|
|
116
|
-
raise RuntimeError(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
base = str(prov.get("base_url") or "").rstrip("/")
|
|
121
|
-
key = _pick_key(prov)
|
|
122
|
-
if not base or not key:
|
|
123
|
-
raise RuntimeError("供应商缺少 base_url 或密钥")
|
|
124
|
-
allow_private = bool(prov.get("allow_private"))
|
|
241
|
+
cands = _candidates()
|
|
242
|
+
if not cands:
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
"没有可用的 openai 协议图像接口(扫了全部供应商的 openai 面,"
|
|
245
|
+
"含适配测试实测出的)。可到模型管理页对网关跑一次适配测试补出 "
|
|
246
|
+
"openai 面,或设环境变量 CODEBEE_IMAGE_MODEL 指定图像模型后重试")
|
|
125
247
|
prompt = _cover_prompt(task)
|
|
126
|
-
models = []
|
|
127
|
-
env_model = os.environ.get("CODEBEE_IMAGE_MODEL", "").strip()
|
|
128
|
-
if env_model:
|
|
129
|
-
models.append(env_model)
|
|
130
|
-
models.extend(m for m in _IMAGE_CANDIDATES if m not in models)
|
|
131
248
|
last_err = ""
|
|
132
|
-
for
|
|
133
|
-
for
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
249
|
+
for cand in cands:
|
|
250
|
+
for base in cand["bases"]:
|
|
251
|
+
for model in _image_models(cand["prov"]):
|
|
252
|
+
for size in _SIZES:
|
|
253
|
+
item, err = _call_images(base, cand["key"], model,
|
|
254
|
+
prompt, size, cand["allow_private"])
|
|
255
|
+
if item is None:
|
|
256
|
+
last_err = err
|
|
257
|
+
continue
|
|
258
|
+
img_url = str(item.get("url") or "")
|
|
259
|
+
if not img_url:
|
|
260
|
+
last_err = "该供应商未返回图片 URL(仅内嵌数据),暂不支持"
|
|
261
|
+
continue
|
|
262
|
+
try:
|
|
263
|
+
safe_url = _safe_image_url(img_url)
|
|
264
|
+
except ValueError as e:
|
|
265
|
+
last_err = str(e)
|
|
266
|
+
continue
|
|
267
|
+
out_dir = paths.RUNS_DIR / str(run_id)
|
|
268
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
out = out_dir / "cover.png"
|
|
270
|
+
try:
|
|
271
|
+
_curl_to(safe_url, out)
|
|
272
|
+
except RuntimeError as e:
|
|
273
|
+
last_err = str(e)
|
|
274
|
+
continue
|
|
275
|
+
return {"status": "done", "file": "cover.png", "run_id": str(run_id),
|
|
276
|
+
"provider": cand["label"], "model": model, "size": size,
|
|
277
|
+
"at": time.strftime("%Y-%m-%d %H:%M:%S")}
|
|
158
278
|
raise RuntimeError(last_err or "图像接口无可用模型")
|
|
159
279
|
except Exception as e:
|
|
160
280
|
return {"status": "failed", "error": str(e)[:300],
|
package/app/core/modelhub.py
CHANGED
|
@@ -192,6 +192,18 @@ def _validate_host(url, allow_private):
|
|
|
192
192
|
return host, ""
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
# 「候选列表端点全部 404」的良性提示固定后缀。zcode-plan 等 anthropic 形网关
|
|
196
|
+
# 没有 GET /models:404 与密钥无关、不影响对话调用。调用方用 is_no_list_note()
|
|
197
|
+
# 把它归为预期情况(成功 + 提示),不算失败、不记 KEY 错误。
|
|
198
|
+
_NO_LIST_SUFFIX = ("无模型列表接口(HTTP 404)——"
|
|
199
|
+
"模型以导入/手动添加为准,不影响对话调用")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def is_no_list_note(err):
|
|
203
|
+
"""404≠密钥错≠网络断:网关本来就没有 /models 列表接口,属良性结果。"""
|
|
204
|
+
return bool(err) and err.endswith(_NO_LIST_SUFFIX)
|
|
205
|
+
|
|
206
|
+
|
|
195
207
|
def _fetch_models_http(base_url, api_key, protocol, allow_private=False):
|
|
196
208
|
"""GET {base}/models 拉取模型列表。返回 (names, err)。
|
|
197
209
|
|
|
@@ -199,6 +211,8 @@ def _fetch_models_http(base_url, api_key, protocol, allow_private=False):
|
|
|
199
211
|
google 走 /v1beta/models,返回项形如 {"name": "models/gemini-x"}。
|
|
200
212
|
protocol="auto"(导入时未指定格式)两条 URL 形状都试——anthropic 与 openai
|
|
201
213
|
的取列表路径本就相同,多试 google 那条只是让 auto 名副其实。
|
|
214
|
+
全部候选端点皆 404 → err 携带 _NO_LIST_SUFFIX 的良性提示(is_no_list_note
|
|
215
|
+
判真),不是失败;混入任何真错误(500/401/连不上)则报真错误。
|
|
202
216
|
"""
|
|
203
217
|
import urllib.parse
|
|
204
218
|
base = (base_url or "").rstrip("/")
|
|
@@ -213,6 +227,7 @@ def _fetch_models_http(base_url, api_key, protocol, allow_private=False):
|
|
|
213
227
|
else:
|
|
214
228
|
urls = std
|
|
215
229
|
last_err = ""
|
|
230
|
+
last_404 = "" # 最后一条 404;全部尝试皆 404 时它是良性结果而非失败
|
|
216
231
|
opener = _opener()
|
|
217
232
|
for url in urls:
|
|
218
233
|
host, herr = _validate_host(url, allow_private)
|
|
@@ -248,13 +263,13 @@ def _fetch_models_http(base_url, api_key, protocol, allow_private=False):
|
|
|
248
263
|
if e.code == 404:
|
|
249
264
|
# zcode-plan / open.bigmodel.cn 等 anthropic 形网关没有
|
|
250
265
|
# GET /models——404 与密钥无关,模型以导入/手填为准
|
|
251
|
-
|
|
252
|
-
"模型以导入/手动添加为准,不影响对话调用")
|
|
266
|
+
last_404 = url + " " + _NO_LIST_SUFFIX
|
|
253
267
|
else:
|
|
254
268
|
last_err = tlsctx.humanize("%s → HTTP %s" % (url, e.code))
|
|
255
269
|
except Exception as e:
|
|
256
270
|
last_err = tlsctx.humanize("%s → %r" % (url, e))
|
|
257
|
-
|
|
271
|
+
# 真错误压过良性 404:500/401/连不上任一出现,就不能报「无列表接口」
|
|
272
|
+
return None, last_err or last_404
|
|
258
273
|
|
|
259
274
|
|
|
260
275
|
def refresh_models(provider_id):
|
|
@@ -2533,6 +2548,11 @@ def test_provider(provider_id):
|
|
|
2533
2548
|
note_key_ok(provider_id, kk.get("id") or "")
|
|
2534
2549
|
return {"ok": True, "latency_ms": int((_t.time() - t0) * 1000),
|
|
2535
2550
|
"count": len(names), "error": "", "key_id": kk.get("id") or ""}
|
|
2551
|
+
if is_no_list_note(err):
|
|
2552
|
+
# 网关没有 /models:HTTP 往返已证连通,与 KEY 无关——不计失败、
|
|
2553
|
+
# 不记 KEY 错误;密钥真伪交「单模型测试」的 1 token 真对话去验
|
|
2554
|
+
return {"ok": True, "latency_ms": int((_t.time() - t0) * 1000),
|
|
2555
|
+
"count": 0, "error": "", "note": err, "key_id": kk.get("id") or ""}
|
|
2536
2556
|
last = err
|
|
2537
2557
|
note_key_error(provider_id, kk.get("id") or "", err)
|
|
2538
2558
|
return {"ok": False, "latency_ms": int((_t.time() - t0) * 1000), "error": last}
|
package/app/core/notify.py
CHANGED
|
@@ -71,7 +71,10 @@ def push_run_async(run_id):
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def push_run(run_id):
|
|
74
|
-
"""组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
|
|
74
|
+
"""组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
|
|
75
|
+
|
|
76
|
+
摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
|
|
77
|
+
run_id 供在本机 CodeBee 界面查找)。"""
|
|
75
78
|
hook = _webhook()
|
|
76
79
|
if not hook:
|
|
77
80
|
return False
|
|
@@ -90,6 +93,13 @@ def push_run(run_id):
|
|
|
90
93
|
if v.get("overall") is not None:
|
|
91
94
|
lines.append("综合评分 %.1f(%s)" % (
|
|
92
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
|
|
93
103
|
return push_text("\n".join(lines))
|
|
94
104
|
|
|
95
105
|
|
package/app/core/paihang.py
CHANGED
|
@@ -22,10 +22,13 @@ FANQIE_RANK_URL = "https://fanqienovel.com/rank"
|
|
|
22
22
|
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
23
23
|
"(KHTML, like Gecko) Chrome/126 Safari/537.36")
|
|
24
24
|
|
|
25
|
-
# 页面 UI
|
|
25
|
+
# 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索等
|
|
26
26
|
_NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
|
|
27
27
|
"排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
|
|
28
|
-
"帮助中心", "作家助手", "客服", "登录", "注册"
|
|
28
|
+
"帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
|
|
29
|
+
"搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
|
|
30
|
+
"更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
|
|
31
|
+
"下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于"}
|
|
29
32
|
|
|
30
33
|
_CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
|
|
31
34
|
|
package/app/core/pipeline.py
CHANGED
|
@@ -1472,6 +1472,9 @@ def _plot_modules(workdir):
|
|
|
1472
1472
|
"""剧情模块库(oh-story 拆文沉淀式):工作目录里的 plot-modules.md
|
|
1473
1473
|
(可复用的桥段/冲突/爽点/名场面素材模块),作者手工维护,每章起草与
|
|
1474
1474
|
评审前自动注入。不存在/为空返回 ""——约定式功能,零配置零噪音。"""
|
|
1475
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), MODULES_FILE))
|
|
1476
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
1477
|
+
return ""
|
|
1475
1478
|
try:
|
|
1476
1479
|
txt = _read_text_any_enc(p)[:_MODULES_MAX_CHARS].strip()
|
|
1477
1480
|
except OSError:
|
|
@@ -1482,6 +1485,49 @@ def _plot_modules(workdir):
|
|
|
1482
1485
|
"鼓励化用,不要照抄原句)\n\n" + txt)
|
|
1483
1486
|
|
|
1484
1487
|
|
|
1488
|
+
def _shrink_context_block(sk_block, bible, budget=12000):
|
|
1489
|
+
"""分层上下文降级(长提示词在容量受限通道上会挂起/秒拒,2026-09-17 讯飞实测)。
|
|
1490
|
+
|
|
1491
|
+
四层优先级:故事圣经(最高,设定冲突以它为准)> 剧情模块库 > 经验库 > 大纲/前情
|
|
1492
|
+
(后两者在提示词正文里,永不动)。超预算时按优先级保序截断:
|
|
1493
|
+
- 圣经截断保整段(按二级标题边界,无边界才硬截)
|
|
1494
|
+
- 模块库截断保整模块(按「## 」标题边界)
|
|
1495
|
+
- 经验库直接硬截(条目本身短,损失最小)
|
|
1496
|
+
返回 (新 sk_block, 降级说明)。无降级返回原样。"""
|
|
1497
|
+
total = len(sk_block or "") + len(bible or "")
|
|
1498
|
+
if total <= budget:
|
|
1499
|
+
return sk_block, bible, ""
|
|
1500
|
+
notes = []
|
|
1501
|
+
# 1) 先压经验库到 4K(条目短、损失最小)
|
|
1502
|
+
if len(sk_block or "") > 4000:
|
|
1503
|
+
sk_block = sk_block[:4000] + "\n\n(经验库已因上下文容量限制精简)"
|
|
1504
|
+
notes.append("经验库→4K")
|
|
1505
|
+
if len(sk_block) + len(bible or "") <= budget:
|
|
1506
|
+
return sk_block, bible, ";".join(notes)
|
|
1507
|
+
# 2) 模块库按模块边界截断
|
|
1508
|
+
if bible and "## 剧情模块库" in bible:
|
|
1509
|
+
head, sep, mods = bible.partition("## 剧情模块库")
|
|
1510
|
+
mods = sep + mods
|
|
1511
|
+
keep = mods[:6000]
|
|
1512
|
+
cut = keep.rfind("\n## ")
|
|
1513
|
+
if cut > 200:
|
|
1514
|
+
keep = keep[:cut]
|
|
1515
|
+
bible = head + keep + "\n\n(模块库已因上下文容量限制精简)"
|
|
1516
|
+
notes.append("模块库→边界截断")
|
|
1517
|
+
if len(sk_block) + len(bible) <= budget:
|
|
1518
|
+
return sk_block, bible, ";".join(notes)
|
|
1519
|
+
# 3) 圣经按二级标题边界截断到预算
|
|
1520
|
+
if bible:
|
|
1521
|
+
budget_left = max(2000, budget - len(sk_block or ""))
|
|
1522
|
+
keep = bible[:budget_left]
|
|
1523
|
+
cut = keep.rfind("\n## ")
|
|
1524
|
+
if cut > 500:
|
|
1525
|
+
keep = keep[:cut]
|
|
1526
|
+
bible = keep + "\n\n(圣经已因上下文容量限制精简)"
|
|
1527
|
+
notes.append("圣经→边界截断")
|
|
1528
|
+
return sk_block, bible, ";".join(notes)
|
|
1529
|
+
|
|
1530
|
+
|
|
1485
1531
|
def _critic_lens(critics, agent):
|
|
1486
1532
|
"""该评审的专属视角;单评审/手动指定时不播种(无从轮换,也别稀释注意力)。"""
|
|
1487
1533
|
try:
|
|
@@ -1777,6 +1823,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1777
1823
|
if t:
|
|
1778
1824
|
tails.append("(第 %d 章结尾)…%s" % (j, t[-260:].strip()))
|
|
1779
1825
|
prev = "\n".join(tails) or "(无)"
|
|
1826
|
+
# findings 中期记忆(借鉴 agentmemory 持久记忆):此前各章的关键事实
|
|
1827
|
+
# 追加在 .codebee/findings.md,注入时只取前一章之前的记录(当章发现
|
|
1828
|
+
# 会在当章评审后追加进来)。300+ 章长篇的前情只看近 2 章不够,
|
|
1829
|
+
# findings 填补中期记忆空洞。失败静默。
|
|
1830
|
+
try:
|
|
1831
|
+
fd_p = os.path.join(workdir, ".codebee", "findings.md")
|
|
1832
|
+
if os.path.isfile(fd_p):
|
|
1833
|
+
fd_txt = _read_text_any_enc(fd_p)
|
|
1834
|
+
if fd_txt:
|
|
1835
|
+
# 截到 3000 字防无限膨胀;只在尾部追加时自动增长,注入头固定
|
|
1836
|
+
prev += "\n\n## 此前章节发现摘要\n" + fd_txt[:3000]
|
|
1837
|
+
except Exception:
|
|
1838
|
+
pass
|
|
1780
1839
|
|
|
1781
1840
|
# 评审-修订(每章至多 1 轮修订)
|
|
1782
1841
|
rounds_used = 1
|
|
@@ -1926,10 +1985,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1926
1985
|
time.sleep(30 * draft_attempt) # 30s / 60s 退避
|
|
1927
1986
|
if draft_attempt and len(prompt) > 12000 and sk_block and sk_block in prompt:
|
|
1928
1987
|
# 长提示词在容量受限通道(讯飞托管 35B 等)上会挂起/秒拒
|
|
1929
|
-
#
|
|
1930
|
-
#
|
|
1931
|
-
|
|
1932
|
-
|
|
1988
|
+
# ——分层降级:经验库→4K、模块库按模块边界、圣经按二级标题
|
|
1989
|
+
# 边界,保大纲/前情/本章要点(2026-09-17 七猫实测:全量
|
|
1990
|
+
# 30KB 对讯飞必挂)
|
|
1991
|
+
sk2, bible2, _note = _shrink_context_block(sk_block, bible, budget=12000)
|
|
1992
|
+
use_prompt = prompt.replace(sk_block, sk2).replace(bible, bible2)
|
|
1933
1993
|
res = _run_step(run_id, "draft-c%d" % i, modelhub.bind_agent(impl, difficulty), use_prompt,
|
|
1934
1994
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
1935
1995
|
resume=resume_ctx["session"] if resume_ctx else None,
|
|
@@ -2185,6 +2245,22 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2185
2245
|
"words": _wc(_read_chapter(workdir, i))})
|
|
2186
2246
|
# 每章即时持久化:长篇中断/超时后可断点续跑,不丢已完成章的分数
|
|
2187
2247
|
store.update_run(run_id, chapter_scores=chapter_scores)
|
|
2248
|
+
# findings 沉淀(借鉴 agentmemory 持久记忆):章节标题+要点追加到
|
|
2249
|
+
# .codebee/findings.md——后续章节起草时随圣经/模块库注入,弥补
|
|
2250
|
+
# 前情提要只看近 2 章结尾的中期记忆空洞。失败静默。
|
|
2251
|
+
try:
|
|
2252
|
+
fd_p = os.path.join(workdir, ".codebee", "findings.md")
|
|
2253
|
+
os.makedirs(os.path.dirname(fd_p), exist_ok=True)
|
|
2254
|
+
header_needed = not os.path.isfile(fd_p)
|
|
2255
|
+
with open(fd_p, "a", encoding="utf-8") as f:
|
|
2256
|
+
if header_needed:
|
|
2257
|
+
f.write("# 章节发现(每章评审达标后自动追加,供后续章节参考)\n\n")
|
|
2258
|
+
f.write("- 第%d章《%s》:%s(%d 字,%s)\n" % (
|
|
2259
|
+
i, ch["title"],
|
|
2260
|
+
ch.get("beats") or "按大纲推进", chapter_scores[-1]["words"],
|
|
2261
|
+
"%.1f 分" % means.get("情节", 0.0) if means else "无评分"))
|
|
2262
|
+
except Exception:
|
|
2263
|
+
pass
|
|
2188
2264
|
|
|
2189
2265
|
# ---- 3) 全局一致性评审(覆盖 1..end 全书:续写批次必须连同旧章一起查一致性)
|
|
2190
2266
|
full_text = "\n\n".join(_read_chapter(workdir, i) for i in range(1, end + 1))
|