codebee 0.1.66 → 0.1.68
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 +543 -520
- package/LICENSE +21 -21
- package/README.md +11 -7
- 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 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +623 -532
- package/app/core/automation.py +758 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +930 -796
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1234 -1234
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +42 -0
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -0
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +128 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +23 -23
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +48 -2
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +46 -26
- package/app/core/knowledge.py +57 -3
- package/app/core/manager.py +2122 -2115
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +124 -26
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +51 -3
- package/app/core/notify.py +106 -106
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +138 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +221 -12
- package/app/core/planner.py +6 -1
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +539 -533
- package/app/core/publish/fanqie.py +159 -128
- package/app/core/publish/flow.py +440 -432
- package/app/core/publish/flows-fanqie-calibrated.json +332 -332
- package/app/core/publish/flows-qimao-calibrated.json +7 -3
- package/app/core/publish/ledger.py +203 -198
- package/app/core/publish/manager.py +692 -623
- package/app/core/publish/qimao.py +141 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/revisions.py +51 -0
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2067 -2050
- 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 +149 -149
- package/app/core/settings_schema.py +262 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +122 -114
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- 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/skills.py +109 -2
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +278 -41
- 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/tracing.py +144 -0
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +862 -858
- package/app/core/volumes.py +389 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2465 -2447
- package/app/main.py +2981 -2838
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +14399 -13994
- package/app/ui/artdiff.js +74 -0
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2770 -2716
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +6 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +96 -24
- package/bin/tutti.js +147 -147
- package/package.json +46 -46
- package/tools/wx_reader.py +261 -261
|
@@ -24,6 +24,7 @@ import os
|
|
|
24
24
|
import ipaddress
|
|
25
25
|
import json
|
|
26
26
|
import re
|
|
27
|
+
import secrets
|
|
27
28
|
import shutil
|
|
28
29
|
import socket
|
|
29
30
|
import subprocess
|
|
@@ -343,11 +344,13 @@ def _entries_from_cache(cache=None):
|
|
|
343
344
|
return entries
|
|
344
345
|
|
|
345
346
|
|
|
346
|
-
def view(offset=0, limit=_PAGE_LIMIT, source=None, q=None):
|
|
347
|
+
def view(offset=0, limit=_PAGE_LIMIT, source=None, q=None, installed=None):
|
|
347
348
|
"""外部目录视图(服务端过滤 + 分页)。缓存全在本地,对全量条目过滤再切页
|
|
348
349
|
零成本——搜索/来源筛选不再受「已加载页」限制,滚动翻页逛完全部目录。
|
|
350
|
+
installed=真值时只看已安装(「已安装 N」入口的过滤器);installed_total
|
|
351
|
+
始终是**未过滤前**的全量已装数(徽章计数与过滤条件无关)。
|
|
349
352
|
返回 {sources, entries(本页), total(过滤后全量), offset, has_more,
|
|
350
|
-
categories}。"""
|
|
353
|
+
categories, installed_total}。"""
|
|
351
354
|
cache = _load_cache()
|
|
352
355
|
entries = _entries_from_cache(cache)
|
|
353
356
|
sources = []
|
|
@@ -360,6 +363,9 @@ def view(offset=0, limit=_PAGE_LIMIT, source=None, q=None):
|
|
|
360
363
|
for e in entries:
|
|
361
364
|
if e["category"] and e["category"] not in cats:
|
|
362
365
|
cats.append(e["category"])
|
|
366
|
+
installed_total = sum(1 for e in entries if e["installed"])
|
|
367
|
+
if installed:
|
|
368
|
+
entries = [e for e in entries if e["installed"]]
|
|
363
369
|
if source:
|
|
364
370
|
entries = [e for e in entries if e["source_id"] == source]
|
|
365
371
|
qq = str(q or "").strip().lower()
|
|
@@ -372,7 +378,7 @@ def view(offset=0, limit=_PAGE_LIMIT, source=None, q=None):
|
|
|
372
378
|
page = entries[offset:offset + limit]
|
|
373
379
|
return {"sources": sources, "entries": page, "total": len(entries),
|
|
374
380
|
"offset": offset, "has_more": offset + limit < len(entries),
|
|
375
|
-
"categories": cats}
|
|
381
|
+
"categories": cats, "installed_total": installed_total}
|
|
376
382
|
|
|
377
383
|
|
|
378
384
|
def _fetch_clawhub_catalog():
|
|
@@ -659,7 +665,9 @@ def _safe_extract(zf, dest):
|
|
|
659
665
|
|
|
660
666
|
|
|
661
667
|
def _download_zip(entry, tmp):
|
|
662
|
-
"""下载 zip(校验 sha256)→ 安全解包 →
|
|
668
|
+
"""下载 zip(校验 sha256)→ 安全解包 → 返回 (插件根目录, 完整性说明)。
|
|
669
|
+
完整性:清单带 sha256 且校验通过 = verified;清单没带 = unverified
|
|
670
|
+
(靠 https 传输 + 解包后的纯技能白名单兜底,如实告知不装懂)。"""
|
|
663
671
|
data = _fetch(entry["install"]["url"], cap=_CAP_DOWNLOAD)
|
|
664
672
|
want = (entry["install"].get("sha256") or "").lower()
|
|
665
673
|
if want:
|
|
@@ -667,7 +675,7 @@ def _download_zip(entry, tmp):
|
|
|
667
675
|
if got != want:
|
|
668
676
|
raise ValueError("sha256 校验不符(期望 %s,实际 %s)" % (want[:12], got[:12]))
|
|
669
677
|
dest = _safe_extract(zipfile.ZipFile(io.BytesIO(data)), tmp / "unzip")
|
|
670
|
-
return _plugin_root(dest)
|
|
678
|
+
return _plugin_root(dest), ("verified" if want else "unverified")
|
|
671
679
|
|
|
672
680
|
|
|
673
681
|
def _gh_split(url):
|
|
@@ -870,7 +878,7 @@ def _download_clawhub(entry, tmp):
|
|
|
870
878
|
_CLAWHUB_BASE, q(inst["slug"], safe=""), q(inst["reference"] or inst["slug"], safe=""))
|
|
871
879
|
data = _fetch(url, cap=_CAP_DOWNLOAD)
|
|
872
880
|
dest = _safe_extract(zipfile.ZipFile(io.BytesIO(data)), tmp / "clawhub")
|
|
873
|
-
return _plugin_root(dest)
|
|
881
|
+
return _plugin_root(dest), "unverified"
|
|
874
882
|
|
|
875
883
|
|
|
876
884
|
def _plugin_root(base):
|
|
@@ -961,9 +969,20 @@ def build_files(entry, root):
|
|
|
961
969
|
return out, None, stripped
|
|
962
970
|
|
|
963
971
|
|
|
964
|
-
def
|
|
965
|
-
"""
|
|
966
|
-
|
|
972
|
+
def _fetch_root(entry, tmp):
|
|
973
|
+
"""按来源下载并解包,返回 (插件根目录, 完整性说明)。
|
|
974
|
+
verified=zip 带 sha256 且校验通过;unverified=直链无哈希(https+白名单兜底);
|
|
975
|
+
git-ref/git-head=git 类来源按 ref/HEAD 取树。"""
|
|
976
|
+
kind = entry["install"]["kind"]
|
|
977
|
+
if kind == "zip":
|
|
978
|
+
return _download_zip(entry, tmp)
|
|
979
|
+
if kind == "clawhub":
|
|
980
|
+
return _download_clawhub(entry, tmp)
|
|
981
|
+
return _download_git_any(entry, tmp), \
|
|
982
|
+
("git-ref" if entry["install"].get("ref") else "git-head")
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _resolve_entry(entry_id):
|
|
967
986
|
entries = _entries_from_cache()
|
|
968
987
|
entry = next((e for e in entries if e["id"] == entry_id), None)
|
|
969
988
|
if not entry:
|
|
@@ -972,35 +991,114 @@ def install_remote(entry_id):
|
|
|
972
991
|
return None, entry["block_reason"] or "该插件不适配 CodeBee"
|
|
973
992
|
if entry["install"]["kind"] == "unsupported":
|
|
974
993
|
return None, "来源类型不支持(仅支持 zip 直链、git 子目录与 ClawHub)"
|
|
994
|
+
return entry, None
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
# 预览→确认安装两阶段之间的已检内容缓存:token 一次性、TTL 10 分钟、最多 8 份
|
|
998
|
+
# (files 是纯文本 dict、总量有 _CAP_TOTAL_TEXT 上限,缓存占用有界)。
|
|
999
|
+
_PREVIEW_TTL = 600
|
|
1000
|
+
_PREVIEW_MAX = 8
|
|
1001
|
+
_PREVIEW_CACHE = {}
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _preview_purge():
|
|
1005
|
+
now = time.time()
|
|
1006
|
+
for k in [k for k, v in _PREVIEW_CACHE.items() if now - v["ts"] > _PREVIEW_TTL]:
|
|
1007
|
+
_PREVIEW_CACHE.pop(k, None)
|
|
1008
|
+
while len(_PREVIEW_CACHE) > _PREVIEW_MAX:
|
|
1009
|
+
_PREVIEW_CACHE.pop(min(_PREVIEW_CACHE, key=lambda k: _PREVIEW_CACHE[k]["ts"]))
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _skill_names_from_files(files, entry):
|
|
1013
|
+
"""从转换后的 files 键推技能名:market-<id>.md 是主技能(用条目名),
|
|
1014
|
+
market-<id>__<名>.md 是多技能包里的其余技能(用目录名)。"""
|
|
1015
|
+
names = []
|
|
1016
|
+
for k in files:
|
|
1017
|
+
if "/" in k or not k.endswith(".md") or not k.startswith("market-"):
|
|
1018
|
+
continue
|
|
1019
|
+
base = k[len("market-"):-3]
|
|
1020
|
+
names.append("__" in base and base.split("__", 1)[1] or entry["title"])
|
|
1021
|
+
return sorted(set(names))
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def preview_remote(entry_id):
|
|
1025
|
+
"""安装前预览(先看后装):下载 → 剥离式检查 → 转换,但**不落盘安装**。
|
|
1026
|
+
返回 (预览 dict, 错误)。dict 带 token,确认安装时把 token 传回
|
|
1027
|
+
install_remote 即安装这份已检内容,不重复下载。"""
|
|
1028
|
+
entry, err = _resolve_entry(entry_id)
|
|
1029
|
+
if err:
|
|
1030
|
+
return None, err
|
|
975
1031
|
tmp = Path(tempfile.mkdtemp(prefix="codebee-mkt-"))
|
|
976
1032
|
try:
|
|
977
|
-
|
|
978
|
-
if kind == "zip":
|
|
979
|
-
root = _download_zip(entry, tmp)
|
|
980
|
-
elif kind == "clawhub":
|
|
981
|
-
root = _download_clawhub(entry, tmp)
|
|
982
|
-
else:
|
|
983
|
-
root = _download_git_any(entry, tmp)
|
|
1033
|
+
root, integrity = _fetch_root(entry, tmp)
|
|
984
1034
|
files, blocked, stripped = build_files(entry, root)
|
|
985
1035
|
if blocked:
|
|
986
1036
|
return None, blocked
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1037
|
+
_preview_purge()
|
|
1038
|
+
token = secrets.token_hex(12)
|
|
1039
|
+
_PREVIEW_CACHE[token] = {"entry_id": entry_id, "files": files,
|
|
1040
|
+
"stripped": stripped, "ts": time.time()}
|
|
1041
|
+
file_list = sorted(files.items(), key=lambda kv: kv[0])
|
|
1042
|
+
return {
|
|
1043
|
+
"token": token, "id": entry_id, "title": entry["title"],
|
|
1044
|
+
"source": entry["source_name"], "version": entry["version"],
|
|
1045
|
+
"homepage": entry["homepage"], "integrity": integrity,
|
|
1046
|
+
"skills": _skill_names_from_files(files, entry),
|
|
1047
|
+
"files_total": len(files),
|
|
1048
|
+
"total_chars": sum(len(v) for v in files.values()),
|
|
1049
|
+
"file_list": [{"path": k, "chars": len(v)} for k, v in file_list[:60]],
|
|
1050
|
+
"stripped": sorted(stripped)[:40], "stripped_total": len(stripped),
|
|
1051
|
+
"expires_in": _PREVIEW_TTL,
|
|
1052
|
+
}, None
|
|
996
1053
|
except ValueError as e:
|
|
997
1054
|
return None, str(e)
|
|
998
|
-
except Exception as e:
|
|
1055
|
+
except Exception as e:
|
|
999
1056
|
return None, "下载或解析插件失败: %s" % e
|
|
1000
1057
|
finally:
|
|
1001
1058
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
1002
1059
|
|
|
1003
1060
|
|
|
1061
|
+
def install_remote(entry_id, token=""):
|
|
1062
|
+
"""安装外部目录插件:下载 → 纯技能检查 → 转换 → 复用 market 安装通道。
|
|
1063
|
+
token 非空且命中预览缓存时直接安装那份已检内容(不重复下载);token
|
|
1064
|
+
一次性(装成功即焚,防重放)。返回 (结果 dict, 错误)。"""
|
|
1065
|
+
entry, err = _resolve_entry(entry_id)
|
|
1066
|
+
if err:
|
|
1067
|
+
return None, err
|
|
1068
|
+
cached = None
|
|
1069
|
+
if token:
|
|
1070
|
+
_preview_purge()
|
|
1071
|
+
c = _PREVIEW_CACHE.get(str(token))
|
|
1072
|
+
if c and c["entry_id"] == entry_id:
|
|
1073
|
+
cached = c
|
|
1074
|
+
if cached:
|
|
1075
|
+
files, stripped = cached["files"], cached["stripped"]
|
|
1076
|
+
else:
|
|
1077
|
+
tmp = Path(tempfile.mkdtemp(prefix="codebee-mkt-"))
|
|
1078
|
+
try:
|
|
1079
|
+
root, _integrity = _fetch_root(entry, tmp)
|
|
1080
|
+
files, blocked, stripped = build_files(entry, root)
|
|
1081
|
+
if blocked:
|
|
1082
|
+
return None, blocked
|
|
1083
|
+
except ValueError as e:
|
|
1084
|
+
return None, str(e)
|
|
1085
|
+
except Exception as e: # 网络/解包等意外错误也要兜成用户可读的一句话
|
|
1086
|
+
return None, "下载或解析插件失败: %s" % e
|
|
1087
|
+
finally:
|
|
1088
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
1089
|
+
res, err = market.install_files(entry_id, entry["title"], files, extra={
|
|
1090
|
+
"remote": {"source": entry["source_id"], "name": entry["name"],
|
|
1091
|
+
"version": entry["version"], "homepage": entry["homepage"]},
|
|
1092
|
+
})
|
|
1093
|
+
if err:
|
|
1094
|
+
return None, err
|
|
1095
|
+
if cached:
|
|
1096
|
+
_PREVIEW_CACHE.pop(str(token), None)
|
|
1097
|
+
res["skills"] = sum(1 for k in files if k.endswith(".md") and "/" not in k)
|
|
1098
|
+
res["stripped"] = sorted(stripped)
|
|
1099
|
+
return res, None
|
|
1100
|
+
|
|
1101
|
+
|
|
1004
1102
|
def remove_remote(entry_id):
|
|
1005
1103
|
"""卸载外部插件:记账与标记齐全时走 market 既有卸载;否则报错。"""
|
|
1006
1104
|
return market.remove(entry_id)
|
package/app/core/mocks.py
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""内置演示/测试智能体(mock):不消耗任何配额即可跑通全流程。"""
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _seed(agent_id):
|
|
9
|
-
return int(hashlib.sha256(agent_id.encode("utf-8")).hexdigest()[:6], 16)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def draft_manuscript(task, round_no):
|
|
13
|
-
"""生成一份示例稿件(mock 起草/修订直接落盘的内容)。"""
|
|
14
|
-
title = task.get("title") or "未命名章节"
|
|
15
|
-
goal = task.get("goal") or ""
|
|
16
|
-
return (
|
|
17
|
-
"# {title}\n\n"
|
|
18
|
-
"这是第 {r} 轮 mock 草稿(演示用,不消耗模型配额)。\n\n"
|
|
19
|
-
"## 任务目标\n{goal}\n\n"
|
|
20
|
-
"## 正文\n"
|
|
21
|
-
"夜色沿着山脊线缓慢退去,主角终于看清了石门上的刻痕——那不是文字,"
|
|
22
|
-
"而是一段被反复涂抹又反复显影的名字。他伸手触碰的瞬间,耳边响起的"
|
|
23
|
-
"却是自己昨夜说过的话。{extra}\n\n"
|
|
24
|
-
"(本轮修订关注:{focus})\n"
|
|
25
|
-
).format(
|
|
26
|
-
title=title, r=round_no, goal=goal,
|
|
27
|
-
extra="这一段在第 2 轮修订中补充了人物动机的伏笔。" if round_no >= 2 else "",
|
|
28
|
-
focus="节奏与悬念铺排" if round_no == 1 else "人物弧光与首尾呼应",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def implement_note(task):
|
|
33
|
-
return "(mock 实现)已在目标目录写入 mock-impl.txt 作为变更占位。"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def critique(agent_id, round_no, dims, threshold):
|
|
37
|
-
"""确定性评分:第 1 轮略低于阈值,第 2 轮达标,用于验证发布门禁逻辑。"""
|
|
38
|
-
s = _seed(agent_id)
|
|
39
|
-
scores = {}
|
|
40
|
-
for i, d in enumerate(dims):
|
|
41
|
-
jitter = ((s >> i) % 5) * 0.1
|
|
42
|
-
scores[d] = round(min(9.5, 4.8 + 1.2 * round_no + jitter), 1)
|
|
43
|
-
passed = all(v >= threshold for v in scores.values())
|
|
44
|
-
issues = []
|
|
45
|
-
if round_no == 1:
|
|
46
|
-
issues = [
|
|
47
|
-
{"dim": dims[0], "severity": "major", "note": "开篇信息密度偏低,钩子出现太晚(mock 意见)"},
|
|
48
|
-
{"dim": dims[-1], "severity": "minor", "note": "段落长短交替不够,读感偏平(mock 意见)"},
|
|
49
|
-
]
|
|
50
|
-
return {
|
|
51
|
-
"scores": scores,
|
|
52
|
-
"issues": issues,
|
|
53
|
-
"summary": "mock 总评:第 %d 轮稿件%s发布阈值" % (round_no, "已达到" if passed else "尚未达到"),
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def review(task, verify_passed):
|
|
58
|
-
return {
|
|
59
|
-
"pass": bool(verify_passed),
|
|
60
|
-
"scores": {"正确性": 8.0 if verify_passed else 5.0, "可维护性": 8.0, "安全": 8.5},
|
|
61
|
-
"issues": [] if verify_passed else [
|
|
62
|
-
{"severity": "major", "title": "验证命令未通过", "detail": "请检查测试输出(mock 意见)"}],
|
|
63
|
-
"summary": "mock 代码评审:%s" % ("通过" if verify_passed else "未通过"),
|
|
64
|
-
}
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""内置演示/测试智能体(mock):不消耗任何配额即可跑通全流程。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _seed(agent_id):
|
|
9
|
+
return int(hashlib.sha256(agent_id.encode("utf-8")).hexdigest()[:6], 16)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def draft_manuscript(task, round_no):
|
|
13
|
+
"""生成一份示例稿件(mock 起草/修订直接落盘的内容)。"""
|
|
14
|
+
title = task.get("title") or "未命名章节"
|
|
15
|
+
goal = task.get("goal") or ""
|
|
16
|
+
return (
|
|
17
|
+
"# {title}\n\n"
|
|
18
|
+
"这是第 {r} 轮 mock 草稿(演示用,不消耗模型配额)。\n\n"
|
|
19
|
+
"## 任务目标\n{goal}\n\n"
|
|
20
|
+
"## 正文\n"
|
|
21
|
+
"夜色沿着山脊线缓慢退去,主角终于看清了石门上的刻痕——那不是文字,"
|
|
22
|
+
"而是一段被反复涂抹又反复显影的名字。他伸手触碰的瞬间,耳边响起的"
|
|
23
|
+
"却是自己昨夜说过的话。{extra}\n\n"
|
|
24
|
+
"(本轮修订关注:{focus})\n"
|
|
25
|
+
).format(
|
|
26
|
+
title=title, r=round_no, goal=goal,
|
|
27
|
+
extra="这一段在第 2 轮修订中补充了人物动机的伏笔。" if round_no >= 2 else "",
|
|
28
|
+
focus="节奏与悬念铺排" if round_no == 1 else "人物弧光与首尾呼应",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def implement_note(task):
|
|
33
|
+
return "(mock 实现)已在目标目录写入 mock-impl.txt 作为变更占位。"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def critique(agent_id, round_no, dims, threshold):
|
|
37
|
+
"""确定性评分:第 1 轮略低于阈值,第 2 轮达标,用于验证发布门禁逻辑。"""
|
|
38
|
+
s = _seed(agent_id)
|
|
39
|
+
scores = {}
|
|
40
|
+
for i, d in enumerate(dims):
|
|
41
|
+
jitter = ((s >> i) % 5) * 0.1
|
|
42
|
+
scores[d] = round(min(9.5, 4.8 + 1.2 * round_no + jitter), 1)
|
|
43
|
+
passed = all(v >= threshold for v in scores.values())
|
|
44
|
+
issues = []
|
|
45
|
+
if round_no == 1:
|
|
46
|
+
issues = [
|
|
47
|
+
{"dim": dims[0], "severity": "major", "note": "开篇信息密度偏低,钩子出现太晚(mock 意见)"},
|
|
48
|
+
{"dim": dims[-1], "severity": "minor", "note": "段落长短交替不够,读感偏平(mock 意见)"},
|
|
49
|
+
]
|
|
50
|
+
return {
|
|
51
|
+
"scores": scores,
|
|
52
|
+
"issues": issues,
|
|
53
|
+
"summary": "mock 总评:第 %d 轮稿件%s发布阈值" % (round_no, "已达到" if passed else "尚未达到"),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def review(task, verify_passed):
|
|
58
|
+
return {
|
|
59
|
+
"pass": bool(verify_passed),
|
|
60
|
+
"scores": {"正确性": 8.0 if verify_passed else 5.0, "可维护性": 8.0, "安全": 8.5},
|
|
61
|
+
"issues": [] if verify_passed else [
|
|
62
|
+
{"severity": "major", "title": "验证命令未通过", "detail": "请检查测试输出(mock 意见)"}],
|
|
63
|
+
"summary": "mock 代码评审:%s" % ("通过" if verify_passed else "未通过"),
|
|
64
|
+
}
|
package/app/core/modelhub.py
CHANGED
|
@@ -29,6 +29,7 @@ anthropic/openai 可注入到 CLI;google 仅登记(当前无对应 CLI 可
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
31
|
import copy
|
|
32
|
+
import hashlib
|
|
32
33
|
import ipaddress
|
|
33
34
|
import json
|
|
34
35
|
import os
|
|
@@ -759,6 +760,34 @@ def providers():
|
|
|
759
760
|
return _load().get("providers", [])
|
|
760
761
|
|
|
761
762
|
|
|
763
|
+
def connection_identity(provider_id, key_id="") -> dict:
|
|
764
|
+
"""Resolve provider/credential identity without exposing credential values.
|
|
765
|
+
|
|
766
|
+
A provider definition (protocol + normalized host), provider instance (the
|
|
767
|
+
configured provider id), account/key identity and credential id are kept as
|
|
768
|
+
separate fields for run snapshots. The existing provider store remains the
|
|
769
|
+
source of truth; this helper only supplies stable, redacted references.
|
|
770
|
+
"""
|
|
771
|
+
provider_id = str(provider_id or "").strip()
|
|
772
|
+
key_id = str(key_id or "").strip()
|
|
773
|
+
prov = next((p for p in providers() if str(p.get("id") or "") == provider_id), {})
|
|
774
|
+
from . import upstream
|
|
775
|
+
host = upstream.normalize_upstream(prov.get("base_url") or "")
|
|
776
|
+
protocol = str(prov.get("protocol") or "")[:24]
|
|
777
|
+
definition_key = "%s|%s" % (protocol, host)
|
|
778
|
+
definition_id = "conn-def-" + hashlib.sha256(definition_key.encode("utf-8")).hexdigest()[:16]
|
|
779
|
+
account_id = key_id or "provider-default"
|
|
780
|
+
return {
|
|
781
|
+
"connection_id": provider_id,
|
|
782
|
+
"definition_id": definition_id,
|
|
783
|
+
"instance_id": provider_id,
|
|
784
|
+
"account_id": account_id,
|
|
785
|
+
"credential_id": account_id,
|
|
786
|
+
"protocol": protocol,
|
|
787
|
+
"upstream": host,
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
762
791
|
def bindings():
|
|
763
792
|
with _LOCK:
|
|
764
793
|
return _load().get("bindings", {})
|
|
@@ -2390,9 +2419,19 @@ def bindable_protocols(agent_kind_or_id):
|
|
|
2390
2419
|
return ("openai",)
|
|
2391
2420
|
if agent_kind_or_id in ("claude-code", "claude"):
|
|
2392
2421
|
return ("anthropic",)
|
|
2422
|
+
if agent_kind_or_id in _LOCAL_CRED_ONLY_TARGETS:
|
|
2423
|
+
return ()
|
|
2393
2424
|
return tuple(_BINDABLE_PROTOCOLS)
|
|
2394
2425
|
|
|
2395
2426
|
|
|
2427
|
+
# 只吃 CLI 本机凭据、不吃 Tutti 两种 wire env 的目标:gemini 本尊只认
|
|
2428
|
+
# GOOGLE_API_KEY/GEMINI_API_KEY(Google AI 协议,Tutti 无此协议面);codebuddy
|
|
2429
|
+
# (claude 兼容系)与 trae-agent 的凭据 env 语义未实机验证。注入它们不认的 env
|
|
2430
|
+
# 只会造出「链显示活着、实际走 CLI 默认模型」的静默假绑定——宁可返回空协议集,
|
|
2431
|
+
# 链上条目全跳过、回落 CLI 本机登录态(recommend_binding 同口径,返回 None)。
|
|
2432
|
+
_LOCAL_CRED_ONLY_TARGETS = ("gemini-cli", "codebuddy", "trae-agent")
|
|
2433
|
+
|
|
2434
|
+
|
|
2396
2435
|
def binding_dead_msg(cli_id):
|
|
2397
2436
|
"""死链失败/告警文案(pipeline 死链闸门与本模块 sync 共用):说明为什么
|
|
2398
2437
|
不回落本机默认 + 该 CLI 需要什么协议的供应商。"""
|
|
@@ -2400,12 +2439,15 @@ def binding_dead_msg(cli_id):
|
|
|
2400
2439
|
protos = bindable_protocols(cli_id)
|
|
2401
2440
|
except Exception:
|
|
2402
2441
|
protos = ()
|
|
2403
|
-
|
|
2442
|
+
if not protos:
|
|
2443
|
+
return ("该 CLI 暂不支持绑定链(凭据注入尚未适配,使用 CLI 本机登录态与"
|
|
2444
|
+
"默认模型);请勿为其配置供应商链")
|
|
2445
|
+
hint = "该 CLI 仅接受 %s 协议的已启用供应商;" % "、".join(protos)
|
|
2404
2446
|
return ("绑定链全部失效(链上供应商已停用/删除/无密钥,或模型已停用),"
|
|
2405
2447
|
"本步判失败、不回落 CLI 本机默认——%s请在「模型调度(可选)」页为该 CLI 指定已启用的供应商" % hint)
|
|
2406
2448
|
|
|
2407
2449
|
|
|
2408
|
-
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
2450
|
+
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
2409
2451
|
"""返回 {env:{}, model:..., model_fallbacks:[...], codex_provider:..., call_chain:[...]} 或 None。
|
|
2410
2452
|
|
|
2411
2453
|
call_chain 是跨厂商降级链(唯一真源):每条 {model, env, provider, [codex_provider]},
|
|
@@ -2413,7 +2455,13 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2413
2455
|
→ 整体回落 CLI 默认(None)。纯链条目(无供应商)不注入 env,只传 -m。
|
|
2414
2456
|
难度路由只在无显式链时生效。
|
|
2415
2457
|
"""
|
|
2416
|
-
|
|
2458
|
+
# Local-credential-only CLIs must never expose a binding, including legacy
|
|
2459
|
+
# model-only entries that have no provider_id. The generic runner cannot
|
|
2460
|
+
# inject those model names, so returning a partial chain would make the UI
|
|
2461
|
+
# report a bound model while the CLI silently uses its own default.
|
|
2462
|
+
if agent_kind_or_id in _LOCAL_CRED_ONLY_TARGETS:
|
|
2463
|
+
return None
|
|
2464
|
+
b = _binding_for(agent_kind_or_id)
|
|
2417
2465
|
chain = _binding_chain(b)
|
|
2418
2466
|
provs = {p.get("id"): p for p in providers()}
|
|
2419
2467
|
routing = bool(b.get("difficulty_routing"))
|