codebee 0.1.7 → 0.1.9
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 +20 -0
- package/README.md +236 -252
- package/app/core/catalog.py +24 -5
- package/app/core/errorlog.py +179 -0
- package/app/core/flows.py +6 -2
- package/app/core/gitmod.py +45 -1
- package/app/core/health.py +48 -11
- package/app/core/jobs.py +104 -9
- package/app/core/manager.py +21 -0
- package/app/core/modelhub.py +80 -11
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +420 -88
- package/app/core/runner.py +122 -10
- package/app/core/settings.py +9 -2
- package/app/core/step_runner.py +28 -4
- package/app/core/store.py +180 -139
- package/app/core/telemetry.py +291 -0
- package/app/core/token_meter.py +18 -0
- package/app/main.py +162 -16
- package/app/pick_dialog.py +78 -0
- package/app/ui/app.js +1012 -604
- package/app/ui/i18n.js +79 -6
- package/app/ui/index.html +161 -80
- package/app/ui/style.css +838 -67
- package/package.json +1 -1
package/app/core/modelhub.py
CHANGED
|
@@ -270,7 +270,9 @@ def refresh_models(provider_id):
|
|
|
270
270
|
"priority": o.get("priority", 0),
|
|
271
271
|
"hidden": bool(o.get("hidden")),
|
|
272
272
|
# 模态声明同理:用户/预填设过的 image_in 刷新时不能被抹掉
|
|
273
|
-
"image_in": bool(o.get("image_in"))
|
|
273
|
+
"image_in": bool(o.get("image_in")),
|
|
274
|
+
# 手工添加的标记也要透传:它决定刷新时「不在列表里」是否保留
|
|
275
|
+
"manual": bool(o.get("manual"))}
|
|
274
276
|
(hidden if item["hidden"] else existing).append(item)
|
|
275
277
|
else:
|
|
276
278
|
fresh.append({"name": n, "enabled": True,
|
|
@@ -279,6 +281,10 @@ def refresh_models(provider_id):
|
|
|
279
281
|
# 已删除但本次未返回的条目也保留,否则下次拉取会当作新模型“复活”
|
|
280
282
|
hidden += [dict(o) for n, o in old.items()
|
|
281
283
|
if o.get("hidden") and n not in names]
|
|
284
|
+
# 手工添加但厂商列表里没有的模型同样保留(有的网关列表接口调不通或只返回
|
|
285
|
+
# 部分),否则一次成功的刷新会把用户手工接入的模型静默清掉。
|
|
286
|
+
existing += [dict(o) for n, o in old.items()
|
|
287
|
+
if o.get("manual") and not o.get("hidden") and n not in names]
|
|
282
288
|
# 顺序 = 既有启用(保持用户手动排序)+ 新模型(按强弱估分)+ 既有停用 + 已删除墓碑。
|
|
283
289
|
# 启用块始终在停用块之前(顺带自愈历史数据);新模型不能插到启用块前面,
|
|
284
290
|
# 否则会顶掉手动顺序与「#1 即默认模型」语义。
|
|
@@ -305,6 +311,43 @@ def refresh_models(provider_id):
|
|
|
305
311
|
return len(allm), ""
|
|
306
312
|
|
|
307
313
|
|
|
314
|
+
def add_model_manual(provider_id, name):
|
|
315
|
+
"""手工添加模型——厂商列表接口调不通(或只返回部分)时的通路。返回 (可见模型数, 错误)。
|
|
316
|
+
|
|
317
|
+
不要求密钥、不发网络请求:模型能不能用交给「测试连接」和编排时的换将去验证。
|
|
318
|
+
名字已存在且可见 → 不重复加;已存在但是删除墓碑 → 视同恢复。
|
|
319
|
+
打 manual 标记:之后刷新即使厂商列表里没有它也保留,不会被清掉。
|
|
320
|
+
新模型排在启用块末尾(不置顶、不抢 #1 默认模型),不改变既有排序语义。
|
|
321
|
+
"""
|
|
322
|
+
name = (name or "").strip()
|
|
323
|
+
if not name:
|
|
324
|
+
return 0, "模型名不能为空"
|
|
325
|
+
if len(name) > 200 or any(c in name for c in "\r\n\t"):
|
|
326
|
+
return 0, "模型名不合法"
|
|
327
|
+
with _LOCK:
|
|
328
|
+
data = _load()
|
|
329
|
+
prov = _find_prov(data, provider_id)
|
|
330
|
+
if not prov:
|
|
331
|
+
return 0, "供应商不存在"
|
|
332
|
+
models = prov.get("models")
|
|
333
|
+
if models is None:
|
|
334
|
+
models = []
|
|
335
|
+
prov["models"] = models
|
|
336
|
+
target = next((m for m in models if m.get("name") == name), None)
|
|
337
|
+
if target and not target.get("hidden"):
|
|
338
|
+
return len(_ranked(models)), "模型已存在"
|
|
339
|
+
if target:
|
|
340
|
+
target["hidden"] = False
|
|
341
|
+
target["enabled"] = True
|
|
342
|
+
target["manual"] = True
|
|
343
|
+
else:
|
|
344
|
+
models.append({"name": name, "enabled": True,
|
|
345
|
+
"image_in": _auto_image_in(name), "manual": True})
|
|
346
|
+
_promote(models) # 启用块末尾:参与编排但不抢 #1 默认模型
|
|
347
|
+
_save(data)
|
|
348
|
+
return len(_ranked(models)), ""
|
|
349
|
+
|
|
350
|
+
|
|
308
351
|
def refresh_all_async():
|
|
309
352
|
"""后台逐个刷新全部供应商的模型列表(导入后自动触发)。返回供应商数。
|
|
310
353
|
|
|
@@ -1985,6 +2028,23 @@ def _protocol_candidates(prov):
|
|
|
1985
2028
|
if (caps.get(p) or {}).get("base")]
|
|
1986
2029
|
|
|
1987
2030
|
|
|
2031
|
+
def _ensure_wire_candidates(provider_id, prov, model_name=""):
|
|
2032
|
+
"""唯一协议场景(单模型测试/直连对话)的鸡生蛋解除:auto 供应商还没探出
|
|
2033
|
+
wire_caps 时(导入后没探过/上次探挂了),带着手头模型先实测一轮再谈——
|
|
2034
|
+
探到即落盘放行;返回 (候选, 失败原因 note),仍探不出则 ([], note)。"""
|
|
2035
|
+
protos = _protocol_candidates(prov)
|
|
2036
|
+
if protos:
|
|
2037
|
+
return protos, ""
|
|
2038
|
+
note = ""
|
|
2039
|
+
try:
|
|
2040
|
+
_, note = probe_wire_caps(provider_id, prefer_model=model_name)
|
|
2041
|
+
except Exception as e:
|
|
2042
|
+
note = repr(e)[:160]
|
|
2043
|
+
with _LOCK:
|
|
2044
|
+
prov2 = next((p for p in providers() if p.get("id") == provider_id), None)
|
|
2045
|
+
return (_protocol_candidates(prov2) if prov2 else []), (note or "")
|
|
2046
|
+
|
|
2047
|
+
|
|
1988
2048
|
def bindable_protocols(agent_kind_or_id):
|
|
1989
2049
|
"""该 CLI 可绑定的 wire 协议(与 resolve_binding 的 allowed 一致)。
|
|
1990
2050
|
供死链告警/失败文案解释「为什么绑不上」:claude 只认 anthropic,
|
|
@@ -2445,7 +2505,8 @@ def test_provider(provider_id):
|
|
|
2445
2505
|
def test_model(provider_id, model_name, key_id=""):
|
|
2446
2506
|
"""单模型连通性测试:发一条 1 token 的最小对话。返回 {ok, latency_ms, error}。
|
|
2447
2507
|
|
|
2448
|
-
auto 供应商逐条试实测过的 wire
|
|
2508
|
+
auto 供应商逐条试实测过的 wire(显式协议只有一条;还没探过 wire_caps 就
|
|
2509
|
+
先自带一次适配探测,见 _ensure_wire_candidates);多 KEY 供应商逐把试
|
|
2449
2510
|
(指定 key_id 则只测那把)。返回里带 protocol/key_id 说明这次是谁通的。
|
|
2450
2511
|
"""
|
|
2451
2512
|
import time as _t
|
|
@@ -2454,9 +2515,10 @@ def test_model(provider_id, model_name, key_id=""):
|
|
|
2454
2515
|
prov = next((p for p in providers() if p.get("id") == provider_id), None)
|
|
2455
2516
|
if not prov or not prov.get("api_key"):
|
|
2456
2517
|
return {"ok": False, "error": "供应商不存在或未配置密钥"}
|
|
2457
|
-
protos =
|
|
2518
|
+
protos, note = _ensure_wire_candidates(provider_id, prov, model_name)
|
|
2458
2519
|
if not protos:
|
|
2459
|
-
return {"ok": False,
|
|
2520
|
+
return {"ok": False,
|
|
2521
|
+
"error": "没探到可用 wire——地址/密钥/模型名至少一项不通" + ("(%s)" % note if note else "")}
|
|
2460
2522
|
if key_id:
|
|
2461
2523
|
keys = [k for k in _provider_keys(prov) if k["id"] == key_id]
|
|
2462
2524
|
if not keys:
|
|
@@ -2573,13 +2635,16 @@ def _probe_wire_once(base, api_key, target_proto, model, allow_private, timeout=
|
|
|
2573
2635
|
return False, "", last
|
|
2574
2636
|
|
|
2575
2637
|
|
|
2576
|
-
def probe_wire_caps(provider_id):
|
|
2638
|
+
def probe_wire_caps(provider_id, prefer_model=""):
|
|
2577
2639
|
"""适配测试:实测该供应商的可用 wire,存进 wire_caps。返回 (caps, note)。
|
|
2578
2640
|
|
|
2579
2641
|
显式协议的供应商只测「除原生外」的 wire(原生天然可用,不必花请求);
|
|
2580
2642
|
protocol="auto"(导入未指定格式)则把可注入 wire 全测一遍,caps 就是分类
|
|
2581
2643
|
结果。之前通过、本次失败的条目移除(网关两面变动以实测为准)。google 不参与。
|
|
2582
|
-
|
|
2644
|
+
prefer_model 优先作探针(单模型测试/直连对话带着手头模型来探);否则用
|
|
2645
|
+
手动填的模型 > 首个启用模型 > 首个可见模型——探针只是 1 token 连通验证,
|
|
2646
|
+
全停用也能探,wire 能力是供应商级属性。note 是给 UI 的补充说明(失败原因 /
|
|
2647
|
+
未测原因),全通过时为空串。"""
|
|
2583
2648
|
import time as _t
|
|
2584
2649
|
with _LOCK:
|
|
2585
2650
|
prov = next((p for p in providers() if p.get("id") == provider_id), None)
|
|
@@ -2587,9 +2652,12 @@ def probe_wire_caps(provider_id):
|
|
|
2587
2652
|
return {}, "供应商不存在"
|
|
2588
2653
|
if not prov.get("api_key"):
|
|
2589
2654
|
return {}, "该供应商未配置密钥"
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2655
|
+
ranked = _ranked(prov.get("models") or [])
|
|
2656
|
+
model = (prefer_model or prov.get("model") or next(
|
|
2657
|
+
(m.get("name") for m in ranked
|
|
2658
|
+
if m.get("name") and m.get("enabled", True)), ""))
|
|
2659
|
+
if not model:
|
|
2660
|
+
model = next((m.get("name") for m in ranked if m.get("name")), "")
|
|
2593
2661
|
if not model:
|
|
2594
2662
|
return {}, "没有可用模型名——先「获取模型列表」再测"
|
|
2595
2663
|
native = prov.get("protocol")
|
|
@@ -2758,10 +2826,11 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
|
|
|
2758
2826
|
if not prov or not prov.get("api_key"):
|
|
2759
2827
|
return {"ok": False, "text": "", "tokens": 0, "usage": None,
|
|
2760
2828
|
"error": "供应商不存在或未配置密钥"}
|
|
2761
|
-
protos =
|
|
2829
|
+
protos, note = _ensure_wire_candidates(provider_id, prov, model_name)
|
|
2762
2830
|
if not protos:
|
|
2763
2831
|
return {"ok": False, "text": "", "tokens": 0, "usage": None,
|
|
2764
|
-
"error": "
|
|
2832
|
+
"error": "没探到可用 wire——地址/密钥/模型名至少一项不通"
|
|
2833
|
+
+ ("(%s)" % note if note else "")}
|
|
2765
2834
|
# 多 KEY:按「KEY 序 × 协议」展开逐条试。欠费的 KEY 先被跳过(切备用),
|
|
2766
2835
|
# 冷却中的 KEY 一条都不剩时仍按原顺序试——比整家供应商不可用强。
|
|
2767
2836
|
keys = _chain_keys(prov) or [{"key": prov.get("api_key") or "", "id": ""}]
|
package/app/core/paths.py
CHANGED
|
@@ -55,6 +55,7 @@ DATA_DIR = default_data_dir()
|
|
|
55
55
|
TASKS_DIR = DATA_DIR / "tasks"
|
|
56
56
|
RUNS_DIR = DATA_DIR / "runs"
|
|
57
57
|
USAGE_DIR = DATA_DIR / "usage"
|
|
58
|
+
ERRORS_DIR = DATA_DIR / "errors"
|
|
58
59
|
CATALOG_FILE = DATA_DIR / "catalog.json"
|
|
59
60
|
ENABLED_FILE = DATA_DIR / "orchestration.json"
|
|
60
61
|
|