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
package/app/core/modelhub.py
CHANGED
|
@@ -114,6 +114,22 @@ def _normalize_ids(data):
|
|
|
114
114
|
_LITE = ("mini", "flash", "lite", "nano", "small", "tiny", "8b", "7b", "4b")
|
|
115
115
|
_HEAVY = ("opus", "pro", "max", "ultra", "plus", "heavy", "codex")
|
|
116
116
|
|
|
117
|
+
# 图片输入命名惯例:vision/omni 词段、glm-4v 式「数字+v」段;vl 子串另判
|
|
118
|
+
# (qwen-vl / internvl2 / cogvlm 等视觉家族的通用命名根,文本模型名含 vl 的极罕见)
|
|
119
|
+
_IMAGE_IN_RE = re.compile(
|
|
120
|
+
r"(?:^|[-_.])(?:vision|visual|omni)(?:[-_.0-9]|$)|[-_.]\dv(?:[-_.]|$)")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _auto_image_in(name):
|
|
124
|
+
"""按模型名启发式预填「支持图片输入」。claude/gemini 全系原生多模态直接标;
|
|
125
|
+
其余只认显式命名(vision 词段、vl 子串、数字+v),宁缺勿滥——漏标的明模型
|
|
126
|
+
用户手动打开即可,误标只是运行时网关显式报错、开关改回即恢复。
|
|
127
|
+
仅用于新模型的初始声明。"""
|
|
128
|
+
n = (name or "").lower()
|
|
129
|
+
if n.startswith(("claude", "gemini")):
|
|
130
|
+
return True
|
|
131
|
+
return bool(_IMAGE_IN_RE.search(n)) or "vl" in n
|
|
132
|
+
|
|
117
133
|
|
|
118
134
|
def _auto_priority(name):
|
|
119
135
|
"""按模型名启发式估强弱:分越高越强(优先级越靠前)。仅用于新模型的初始排序。"""
|
|
@@ -251,11 +267,14 @@ def refresh_models(provider_id):
|
|
|
251
267
|
# 保留 hidden:用户删掉的模型刷新时不能被重新带回
|
|
252
268
|
item = {"name": n, "enabled": bool(o.get("enabled", True)),
|
|
253
269
|
"priority": o.get("priority", 0),
|
|
254
|
-
"hidden": bool(o.get("hidden"))
|
|
270
|
+
"hidden": bool(o.get("hidden")),
|
|
271
|
+
# 模态声明同理:用户/预填设过的 image_in 刷新时不能被抹掉
|
|
272
|
+
"image_in": bool(o.get("image_in"))}
|
|
255
273
|
(hidden if item["hidden"] else existing).append(item)
|
|
256
274
|
else:
|
|
257
275
|
fresh.append({"name": n, "enabled": True,
|
|
258
|
-
"auto": _auto_priority(n)
|
|
276
|
+
"auto": _auto_priority(n),
|
|
277
|
+
"image_in": _auto_image_in(n)})
|
|
259
278
|
# 已删除但本次未返回的条目也保留,否则下次拉取会当作新模型“复活”
|
|
260
279
|
hidden += [dict(o) for n, o in old.items()
|
|
261
280
|
if o.get("hidden") and n not in names]
|
|
@@ -435,6 +454,7 @@ def model_ops(provider_id, names, op):
|
|
|
435
454
|
if changed:
|
|
436
455
|
_promote(models, newly_enabled) # 启用的置顶,停用的退到启用块之后
|
|
437
456
|
_save(data)
|
|
457
|
+
sync_binding_alerts() # 模型启停/恢复 → 绑定链死活立即刷新告警
|
|
438
458
|
return changed, ""
|
|
439
459
|
|
|
440
460
|
|
|
@@ -454,6 +474,7 @@ def _restore_all(provider_id):
|
|
|
454
474
|
m["enabled"] = True
|
|
455
475
|
_promote(models, names) # 恢复 = 重新启用:同样置顶
|
|
456
476
|
_save(data)
|
|
477
|
+
sync_binding_alerts()
|
|
457
478
|
return None
|
|
458
479
|
|
|
459
480
|
|
|
@@ -729,15 +750,48 @@ def note_key_ok(provider_id, key_id):
|
|
|
729
750
|
with _LOCK:
|
|
730
751
|
data = _load()
|
|
731
752
|
prov = next((p for p in data.get("providers", []) if p.get("id") == provider_id), None)
|
|
732
|
-
if not prov
|
|
753
|
+
if not prov:
|
|
733
754
|
return
|
|
734
|
-
target = next((k for k in prov
|
|
755
|
+
target = next((k for k in (prov.get("keys") or []) if str(k.get("id")) == str(key_id)), None)
|
|
735
756
|
if target is None or not target.get("last_error"):
|
|
736
757
|
return
|
|
737
758
|
target.pop("last_error", None)
|
|
738
759
|
_save(data)
|
|
739
760
|
|
|
740
761
|
|
|
762
|
+
def _is_codex_target(target):
|
|
763
|
+
return (target or "").strip().lower() in ("codex-cli", "codex", "codex-code")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def note_codex_wire_dead(provider_id, minutes=30):
|
|
767
|
+
"""codex 撞上 wire 不兼容的供应商 → 供应商级冷却(自动绕开的记账位)。
|
|
768
|
+
|
|
769
|
+
codex 0.154 起只支持 responses wire(chat 被官方移除),讯飞等只有
|
|
770
|
+
chat completions 的 MaaS 每次 404/启动即拒。runner 按 404+no Route
|
|
771
|
+
matched / wire_api no longer supported 特征自动调用本函数;链展开与
|
|
772
|
+
路由绑定加分据此自动绕开,无需人工改绑定(2026-09-17 mo-so 实测)。
|
|
773
|
+
"""
|
|
774
|
+
import time as _t
|
|
775
|
+
if not provider_id:
|
|
776
|
+
return
|
|
777
|
+
with _LOCK:
|
|
778
|
+
data = _load()
|
|
779
|
+
prov = next((p for p in data.get("providers", []) if p.get("id") == provider_id), None)
|
|
780
|
+
if not prov:
|
|
781
|
+
return
|
|
782
|
+
prov["codex_wire_dead_until"] = _t.time() + max(1, int(minutes)) * 60
|
|
783
|
+
_save(data)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def codex_wire_blocked(prov):
|
|
787
|
+
"""该供应商是否处于 codex wire 不兼容冷却期。"""
|
|
788
|
+
import time as _t
|
|
789
|
+
try:
|
|
790
|
+
return float((prov or {}).get("codex_wire_dead_until") or 0) > _t.time()
|
|
791
|
+
except Exception:
|
|
792
|
+
return False
|
|
793
|
+
|
|
794
|
+
|
|
741
795
|
def _is_private_host(url):
|
|
742
796
|
"""主机是私网/环回 IP 字面量或 localhost 时返回 True(自动放行内网网关)。"""
|
|
743
797
|
import urllib.parse
|
|
@@ -873,6 +927,8 @@ def providers_op(ids, op):
|
|
|
873
927
|
not (p.get("id") in sel and p.get("enabled", True)),
|
|
874
928
|
not bool(p.get("enabled", True))))
|
|
875
929
|
_save(data)
|
|
930
|
+
if changed or op == "delete":
|
|
931
|
+
sync_binding_alerts() # 厂商启停/删除 → 绑定链死活立即刷新告警
|
|
876
932
|
return changed, ("" if changed else "所选供应商已是目标状态")
|
|
877
933
|
|
|
878
934
|
|
|
@@ -1813,6 +1869,14 @@ def _model_bindable(prov, model):
|
|
|
1813
1869
|
return True
|
|
1814
1870
|
|
|
1815
1871
|
|
|
1872
|
+
def _model_image_in(prov, model):
|
|
1873
|
+
"""模型是否声明支持图片输入:models[] 条目 image_in;缺省/查不到=False(纯文本)。"""
|
|
1874
|
+
for m in ((prov or {}).get("models") or []):
|
|
1875
|
+
if isinstance(m, dict) and m.get("name") == model:
|
|
1876
|
+
return bool(m.get("image_in"))
|
|
1877
|
+
return False
|
|
1878
|
+
|
|
1879
|
+
|
|
1816
1880
|
def bind_agent(agent, difficulty="default"):
|
|
1817
1881
|
"""按绑定生成应用了供应商/模型覆盖的 agent 副本;无绑定时原样返回。"""
|
|
1818
1882
|
r = resolve_binding(agent.get("id"), difficulty) or resolve_binding(agent.get("kind"), difficulty)
|
|
@@ -1907,6 +1971,56 @@ def _protocol_candidates(prov):
|
|
|
1907
1971
|
if (caps.get(p) or {}).get("base")]
|
|
1908
1972
|
|
|
1909
1973
|
|
|
1974
|
+
def bindable_protocols(agent_kind_or_id):
|
|
1975
|
+
"""该 CLI 可绑定的 wire 协议(与 resolve_binding 的 allowed 一致)。
|
|
1976
|
+
供死链告警/失败文案解释「为什么绑不上」:claude 只认 anthropic,
|
|
1977
|
+
codex/dsh 只认 openai,其余开放双协议(含 wire 适配)。"""
|
|
1978
|
+
if _deepseek_env_target(agent_kind_or_id) or agent_kind_or_id in ("codex-cli", "codex"):
|
|
1979
|
+
return ("openai",)
|
|
1980
|
+
if agent_kind_or_id in ("claude-code", "claude"):
|
|
1981
|
+
return ("anthropic",)
|
|
1982
|
+
return tuple(_BINDABLE_PROTOCOLS)
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def binding_dead_msg(cli_id):
|
|
1986
|
+
"""死链失败/告警文案(pipeline 死链闸门与本模块 sync 共用):说明为什么
|
|
1987
|
+
不回落本机默认 + 该 CLI 需要什么协议的供应商。"""
|
|
1988
|
+
try:
|
|
1989
|
+
protos = bindable_protocols(cli_id)
|
|
1990
|
+
except Exception:
|
|
1991
|
+
protos = ()
|
|
1992
|
+
hint = ("该 CLI 仅接受 %s 协议的已启用供应商;" % "、".join(protos)) if protos else ""
|
|
1993
|
+
return ("绑定链全部失效(链上供应商已停用/删除/无密钥,或模型已停用),"
|
|
1994
|
+
"本步判失败、不回落 CLI 本机默认——%s请在「CLI 绑定」页为该 CLI 绑定已启用的供应商" % hint)
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
def sync_binding_alerts():
|
|
1998
|
+
"""厂商/模型启停、删除、恢复后立即重评估各 CLI 绑定链的静态死链告警:
|
|
1999
|
+
恢复的当场解除、新死的当场亮起——不必等下一次步骤执行才发现
|
|
2000
|
+
(2026-09-17 起与死链硬失败闸门配套)。幂等:health 侧静默/恢复语义不变。"""
|
|
2001
|
+
from . import health
|
|
2002
|
+
try:
|
|
2003
|
+
binds = bindings()
|
|
2004
|
+
except Exception:
|
|
2005
|
+
return
|
|
2006
|
+
for cli_id in sorted(binds.keys()):
|
|
2007
|
+
b = binds.get(cli_id) or {}
|
|
2008
|
+
if not _binding_chain(b):
|
|
2009
|
+
continue # 没配过链的 CLI 不归静态告警管(执行层闸门在跑时兜)
|
|
2010
|
+
try:
|
|
2011
|
+
r = resolve_binding(cli_id)
|
|
2012
|
+
ok = bool(r and r.get("call_chain"))
|
|
2013
|
+
except Exception:
|
|
2014
|
+
ok = False
|
|
2015
|
+
try:
|
|
2016
|
+
if ok:
|
|
2017
|
+
health.report_binding_ok(cli_id)
|
|
2018
|
+
else:
|
|
2019
|
+
health.report_binding_dead(cli_id, binding_dead_msg(cli_id))
|
|
2020
|
+
except Exception:
|
|
2021
|
+
pass # 告警是尽力而为的旁路:persist 失败(如目录不可用)不拖累操作本身
|
|
2022
|
+
|
|
2023
|
+
|
|
1910
2024
|
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
1911
2025
|
"""返回 {env:{}, model:..., model_fallbacks:[...], codex_provider:..., call_chain:[...]} 或 None。
|
|
1912
2026
|
|
|
@@ -1921,17 +2035,11 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
1921
2035
|
provs = {p.get("id"): p for p in providers()}
|
|
1922
2036
|
routing = bool(b.get("difficulty_routing"))
|
|
1923
2037
|
tier = difficulty if difficulty in ("easy", "hard") else None
|
|
1924
|
-
# dsh
|
|
1925
|
-
# anthropic
|
|
1926
|
-
|
|
1927
|
-
# 2026-09-15
|
|
1928
|
-
|
|
1929
|
-
# 「codex 拿到 ANTHROPIC_* env 却缺 ORCH_API_KEY」这类必然失败的组合
|
|
1930
|
-
# (症状:Missing environment variable: ORCH_API_KEY)。
|
|
1931
|
-
if agent_kind_or_id in ("codex-cli", "codex"):
|
|
1932
|
-
allowed = ("openai",)
|
|
1933
|
-
elif agent_kind_or_id in ("claude-code", "claude"):
|
|
1934
|
-
allowed = ("anthropic",)
|
|
2038
|
+
# 协议必须匹配(bindable_protocols):dsh 只吃 OpenAI 兼容端点,codex 只吃
|
|
2039
|
+
# openai wire(codex_provider 机制),claude 只吃 anthropic wire——混着注入
|
|
2040
|
+
# 会产生「codex 拿到 ANTHROPIC_* env 却缺 ORCH_API_KEY」这类必然失败的组合
|
|
2041
|
+
# (2026-09-15 连载验收实测,症状:Missing environment variable: ORCH_API_KEY)。
|
|
2042
|
+
allowed = bindable_protocols(agent_kind_or_id)
|
|
1935
2043
|
|
|
1936
2044
|
if chain:
|
|
1937
2045
|
entries = []
|
|
@@ -1958,6 +2066,10 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
1958
2066
|
continue # 原生协议与适配过的 wire 都不匹配:跳过
|
|
1959
2067
|
if prov.get("name") in down_set:
|
|
1960
2068
|
continue # 健康监测判定 down:跳过,省掉无效等待
|
|
2069
|
+
if _is_codex_target(agent_kind_or_id) and (ep[2] == "chat" or codex_wire_blocked(prov)):
|
|
2070
|
+
continue # codex 0.154+ 只讲 responses wire:chat-only 供应商在起跑前
|
|
2071
|
+
# 就剔除(此前撞了才冷却 30 分钟,每轮白烧一次注定失败的
|
|
2072
|
+
# 尝试——2026-09-17 续4 连载 c35 实测)
|
|
1961
2073
|
if model and not _model_bindable(prov, model):
|
|
1962
2074
|
continue # 模型被停用/删除:该条跳过(2026-09-15 告警弹框「禁用该模型」)
|
|
1963
2075
|
# 多 KEY:同一厂商按 KEY 展开成多条,顺序即调用顺序。欠费的 KEY 被
|
|
@@ -1996,6 +2108,9 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
1996
2108
|
ep = _entry_endpoint(prov, allowed)
|
|
1997
2109
|
if not ep:
|
|
1998
2110
|
return None # google 只登记;dsh 只接受 OpenAI 兼容端点;未适配的不硬塞
|
|
2111
|
+
if _is_codex_target(agent_kind_or_id) and (ep[2] == "chat" or codex_wire_blocked(prov)):
|
|
2112
|
+
return None # codex 0.154+ 只讲 responses:chat-only 供应商直接判不可绑
|
|
2113
|
+
# (解析为空 → 死链闸门/路由降权接手,不浪费 CLI 尝试)
|
|
1999
2114
|
names = [m["name"] for m in _enabled_models(prov)]
|
|
2000
2115
|
model = prov.get("model_" + tier) or "" if (routing and tier) else ""
|
|
2001
2116
|
if not model:
|
|
@@ -2173,6 +2288,24 @@ def reorder_models(provider_id, ordered_names):
|
|
|
2173
2288
|
return None
|
|
2174
2289
|
|
|
2175
2290
|
|
|
2291
|
+
def set_model_caps(provider_id, name, image_in):
|
|
2292
|
+
"""声明单个模型的模态能力(当前仅 image_in 图片输入)。返回错误文案或 None。"""
|
|
2293
|
+
with _LOCK:
|
|
2294
|
+
data = _load()
|
|
2295
|
+
prov = next((p for p in data.get("providers", []) if p.get("id") == provider_id), None)
|
|
2296
|
+
if not prov:
|
|
2297
|
+
return "供应商不存在"
|
|
2298
|
+
target = next((m for m in (prov.get("models") or [])
|
|
2299
|
+
if m.get("name") == name), None)
|
|
2300
|
+
if target is None:
|
|
2301
|
+
return "模型不存在: %s" % name
|
|
2302
|
+
image_in = bool(image_in)
|
|
2303
|
+
if bool(target.get("image_in")) != image_in: # 有差异才落盘
|
|
2304
|
+
target["image_in"] = image_in
|
|
2305
|
+
_save(data)
|
|
2306
|
+
return None
|
|
2307
|
+
|
|
2308
|
+
|
|
2176
2309
|
def _post_json_http(url, headers, body, allow_private, timeout=20):
|
|
2177
2310
|
"""带 SSRF 防护的 POST。返回 (status, json_obj|None, err)。"""
|
|
2178
2311
|
import urllib.parse
|