codebee 0.1.65 → 0.1.67

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.
Files changed (116) hide show
  1. package/CHANGELOG.md +532 -513
  2. package/LICENSE +21 -21
  3. package/README.md +494 -487
  4. package/app/core/acceptance.py +100 -0
  5. package/app/core/aiflavor.py +197 -197
  6. package/app/core/artifacts.py +148 -148
  7. package/app/core/assets.py +110 -0
  8. package/app/core/attachments.py +623 -532
  9. package/app/core/automation.py +758 -758
  10. package/app/core/backup.py +470 -470
  11. package/app/core/beekeeper.py +183 -183
  12. package/app/core/board.py +209 -209
  13. package/app/core/bookmeta.py +930 -759
  14. package/app/core/bookmeta_catalog.py +180 -180
  15. package/app/core/branching.py +137 -137
  16. package/app/core/builtin_agent.py +1234 -1191
  17. package/app/core/capability.py +144 -144
  18. package/app/core/catalog.py +23 -24
  19. package/app/core/chaptersafe.py +32 -32
  20. package/app/core/cleanup.py +303 -303
  21. package/app/core/compaction.py +201 -186
  22. package/app/core/covergen.py +356 -356
  23. package/app/core/defectretro.py +43 -0
  24. package/app/core/diagnostics.py +114 -114
  25. package/app/core/dispatch.py +173 -173
  26. package/app/core/dispatch_log.py +113 -113
  27. package/app/core/env_scrub.py +85 -85
  28. package/app/core/error_codes.py +220 -65
  29. package/app/core/errorlog.py +137 -137
  30. package/app/core/evaluation.py +174 -0
  31. package/app/core/failure_notify.py +290 -0
  32. package/app/core/flows.py +401 -355
  33. package/app/core/goal_service.py +167 -167
  34. package/app/core/health.py +374 -342
  35. package/app/core/history.py +38 -38
  36. package/app/core/hooks.py +162 -156
  37. package/app/core/jobs.py +1050 -1022
  38. package/app/core/knowledge.py +56 -5
  39. package/app/core/manager.py +2122 -1996
  40. package/app/core/market.py +362 -362
  41. package/app/core/market_remote.py +190 -92
  42. package/app/core/mocks.py +64 -64
  43. package/app/core/modelhub.py +3546 -3332
  44. package/app/core/notify.py +106 -106
  45. package/app/core/operations.py +228 -0
  46. package/app/core/paihang.py +138 -109
  47. package/app/core/paths.py +2 -2
  48. package/app/core/pipeline.py +4808 -4494
  49. package/app/core/planner.py +37 -32
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/preview.py +293 -293
  52. package/app/core/publish/__init__.py +7 -7
  53. package/app/core/publish/auto.py +367 -367
  54. package/app/core/publish/browser.py +539 -533
  55. package/app/core/publish/fanqie.py +159 -128
  56. package/app/core/publish/flow.py +440 -406
  57. package/app/core/publish/flows-fanqie-calibrated.json +17 -9
  58. package/app/core/publish/flows-qimao-calibrated.json +6 -2
  59. package/app/core/publish/ledger.py +203 -198
  60. package/app/core/publish/manager.py +692 -590
  61. package/app/core/publish/qimao.py +141 -141
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/redact.py +38 -38
  64. package/app/core/repeat_guard.py +129 -124
  65. package/app/core/revisions.py +51 -0
  66. package/app/core/router.py +119 -81
  67. package/app/core/runner.py +2065 -2042
  68. package/app/core/selfupdate.py +384 -384
  69. package/app/core/session_log.py +161 -161
  70. package/app/core/sessions.py +312 -312
  71. package/app/core/settings.py +149 -149
  72. package/app/core/settings_schema.py +262 -262
  73. package/app/core/share_page.py +114 -114
  74. package/app/core/skill_scan.py +114 -106
  75. package/app/core/skillpacks/fanqie-novel.md +80 -80
  76. package/app/core/skillpacks/market/character-bible.md +66 -66
  77. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  78. package/app/core/skillpacks/market/git-workflow.md +57 -57
  79. package/app/core/skillpacks/market/release-notes.md +72 -72
  80. package/app/core/skillpacks/market/weekly-report.md +71 -71
  81. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  82. package/app/core/skills.py +315 -73
  83. package/app/core/step_runner.py +132 -132
  84. package/app/core/store.py +230 -11
  85. package/app/core/task_compile.py +199 -199
  86. package/app/core/telemetry.py +292 -292
  87. package/app/core/tlsctx.py +64 -64
  88. package/app/core/token_meter.py +147 -147
  89. package/app/core/upstream.py +41 -0
  90. package/app/core/usage.py +114 -3
  91. package/app/core/volumes.py +389 -355
  92. package/app/core/wxdigest.py +858 -858
  93. package/app/core/zentao.py +2465 -2447
  94. package/app/main.py +2957 -2832
  95. package/app/pick_dialog.py +110 -110
  96. package/app/ui/app.js +14384 -13977
  97. package/app/ui/artdiff.js +74 -0
  98. package/app/ui/board.css +20 -41
  99. package/app/ui/board.html +162 -162
  100. package/app/ui/board.js +305 -305
  101. package/app/ui/i18n.js +2766 -2711
  102. package/app/ui/icons/bee.svg +79 -79
  103. package/app/ui/index.html +7 -1
  104. package/app/ui/manifest.json +16 -16
  105. package/app/ui/qrcode.js +2297 -2297
  106. package/app/ui/style.css +6179 -6125
  107. package/bin/tutti.js +147 -147
  108. package/package.json +46 -41
  109. package/tools/wx_reader.py +261 -261
  110. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  111. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  112. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  113. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  114. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  115. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  116. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -24,16 +24,17 @@ 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
30
31
  import tarfile
31
32
  import tempfile
32
33
  import time
33
- import urllib.error
34
- import urllib.request
35
- import urllib.parse
36
- import zipfile
34
+ import urllib.error
35
+ import urllib.request
36
+ import urllib.parse
37
+ import zipfile
37
38
  from pathlib import Path
38
39
  from urllib.parse import urlparse
39
40
 
@@ -94,8 +95,8 @@ _CAP_DOWNLOAD = 80 * 1024 * 1024
94
95
  _CAP_UNPACKED = 120 * 1024 * 1024
95
96
  _CAP_FILES = 500
96
97
  _CAP_FILE_TEXT = 512 * 1024
97
- _CAP_TOTAL_TEXT = 4 * 1024 * 1024
98
- _MAX_REDIRECTS = 3
98
+ _CAP_TOTAL_TEXT = 4 * 1024 * 1024
99
+ _MAX_REDIRECTS = 3
99
100
 
100
101
 
101
102
  def _cache_dir():
@@ -132,52 +133,52 @@ def assert_public_url(url):
132
133
  return host, port
133
134
 
134
135
 
135
- def _fetch(url, cap=_CAP_MANIFEST):
136
+ def _fetch(url, cap=_CAP_MANIFEST):
136
137
  """带体量上限的 https GET;返回 bytes。
137
138
 
138
139
  两段式网络策略:先走默认通道(Windows 上 urllib 会读注册表系统代理,
139
140
  依赖代理上网的环境靠它),失败再自动直连重试一次(实测本机代理对部分
140
141
  CDN 文件返回 404/篡改,直连正常;反过来需要代理的网络第一段就成功)。
141
142
  体量超限属确定性错误,不重试。"""
142
- assert_public_url(url)
143
-
144
- class _NoRedirect(urllib.request.HTTPRedirectHandler):
145
- def redirect_request(self, req, fp, code, msg, headers, newurl):
146
- return None
147
-
148
- def _close_quietly(resp):
149
- try:
150
- resp.close()
151
- except Exception:
152
- pass
153
-
154
- def _open(opener, target):
155
- """逐跳 GET;重定向目标在发起下一跳前重新过 SSRF 网关。"""
156
- current = target
157
- for hop in range(_MAX_REDIRECTS + 1):
158
- assert_public_url(current)
159
- req = urllib.request.Request(current, headers={
160
- "User-Agent": "CodeBee-Market/1.0",
161
- "Accept": "*/*",
162
- })
163
- try:
164
- resp = opener.open(req, timeout=30)
165
- code = getattr(resp, "status", getattr(resp, "code", 200))
166
- except urllib.error.HTTPError as e:
167
- code, resp = e.code, e
168
- if code not in (301, 302, 303, 307, 308):
169
- if isinstance(resp, urllib.error.HTTPError):
170
- raise resp
171
- return resp
172
- location = resp.headers.get("Location")
173
- if not location:
174
- _close_quietly(resp)
175
- raise ValueError("重定向缺少 Location: %s" % current)
176
- _close_quietly(resp)
177
- current = urllib.parse.urljoin(current, location)
178
- if hop >= _MAX_REDIRECTS:
179
- raise ValueError("重定向次数超过上限: %s" % target)
180
- raise ValueError("重定向失败: %s" % target)
143
+ assert_public_url(url)
144
+
145
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
146
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
147
+ return None
148
+
149
+ def _close_quietly(resp):
150
+ try:
151
+ resp.close()
152
+ except Exception:
153
+ pass
154
+
155
+ def _open(opener, target):
156
+ """逐跳 GET;重定向目标在发起下一跳前重新过 SSRF 网关。"""
157
+ current = target
158
+ for hop in range(_MAX_REDIRECTS + 1):
159
+ assert_public_url(current)
160
+ req = urllib.request.Request(current, headers={
161
+ "User-Agent": "CodeBee-Market/1.0",
162
+ "Accept": "*/*",
163
+ })
164
+ try:
165
+ resp = opener.open(req, timeout=30)
166
+ code = getattr(resp, "status", getattr(resp, "code", 200))
167
+ except urllib.error.HTTPError as e:
168
+ code, resp = e.code, e
169
+ if code not in (301, 302, 303, 307, 308):
170
+ if isinstance(resp, urllib.error.HTTPError):
171
+ raise resp
172
+ return resp
173
+ location = resp.headers.get("Location")
174
+ if not location:
175
+ _close_quietly(resp)
176
+ raise ValueError("重定向缺少 Location: %s" % current)
177
+ _close_quietly(resp)
178
+ current = urllib.parse.urljoin(current, location)
179
+ if hop >= _MAX_REDIRECTS:
180
+ raise ValueError("重定向次数超过上限: %s" % target)
181
+ raise ValueError("重定向失败: %s" % target)
181
182
 
182
183
  def _read(open_fn):
183
184
  with open_fn() as resp:
@@ -192,17 +193,17 @@ def _fetch(url, cap=_CAP_MANIFEST):
192
193
  chunks.append(chunk)
193
194
  return b"".join(chunks)
194
195
 
195
- try:
196
- # 默认 opener 仍读取系统代理,但显式禁用自动重定向。
197
- opener = urllib.request.build_opener(
198
- _NoRedirect(), urllib.request.HTTPSHandler(context=tlsctx.context()))
199
- return _read(lambda: _open(opener, url))
200
- except (urllib.error.HTTPError, urllib.error.URLError):
201
- # 直连重试:ProxyHandler({}) 显式清空代理;每一跳仍经过 SSRF 校验。
202
- opener = urllib.request.build_opener(
203
- _NoRedirect(), urllib.request.ProxyHandler({}),
204
- urllib.request.HTTPSHandler(context=tlsctx.context()))
205
- return _read(lambda: _open(opener, url))
196
+ try:
197
+ # 默认 opener 仍读取系统代理,但显式禁用自动重定向。
198
+ opener = urllib.request.build_opener(
199
+ _NoRedirect(), urllib.request.HTTPSHandler(context=tlsctx.context()))
200
+ return _read(lambda: _open(opener, url))
201
+ except (urllib.error.HTTPError, urllib.error.URLError):
202
+ # 直连重试:ProxyHandler({}) 显式清空代理;每一跳仍经过 SSRF 校验。
203
+ opener = urllib.request.build_opener(
204
+ _NoRedirect(), urllib.request.ProxyHandler({}),
205
+ urllib.request.HTTPSHandler(context=tlsctx.context()))
206
+ return _read(lambda: _open(opener, url))
206
207
 
207
208
 
208
209
  # ---------------------------------------------------------------- 清单解析
@@ -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):
@@ -810,22 +818,22 @@ def _gh_fetch_files(entry, tmp):
810
818
  raise ValueError("jsdelivr 拉取失败: %s/%s(%s)" % (owner, repo, last_err))
811
819
 
812
820
 
813
- def _download_git(entry, tmp):
821
+ def _download_git(entry, tmp):
814
822
  """git 浅克隆取子目录(Anthropic 生态的 git-subdir 来源)。"""
815
823
  if shutil.which("git") is None:
816
824
  raise ValueError("本机没有 git,无法安装 git-subdir 来源的插件")
817
- inst = entry["install"]
818
- # 非 GitHub 来源没有 codeload/jsdelivr 的安全收口,必须在 clone 前
819
- # 复用同一公网 HTTPS 网关,避免 git 自己跟随重定向或访问内网。
820
- assert_public_url(inst.get("url") or "")
825
+ inst = entry["install"]
826
+ # 非 GitHub 来源没有 codeload/jsdelivr 的安全收口,必须在 clone 前
827
+ # 复用同一公网 HTTPS 网关,避免 git 自己跟随重定向或访问内网。
828
+ assert_public_url(inst.get("url") or "")
821
829
  dst = tmp / "git"
822
830
  sub = str(inst.get("path") or "").replace("\\", "/")
823
831
  if sub.startswith("/") or ":" in sub or ".." in _rel_parts(sub):
824
832
  raise ValueError("插件子目录路径可疑: %s" % inst.get("path"))
825
- # git 会自行跟随 HTTP 重定向,而重定向目标无法再经过 assert_public_url;
826
- # ��装来源宁可明确失败,也不能让 clone 跳进内网。
827
- cmd = ["git", "-c", "http.followRedirects=false", "clone",
828
- "--depth", "1", "--single-branch", "--quiet"]
833
+ # git 会自行跟随 HTTP 重定向,而重定向目标无法再经过 assert_public_url;
834
+ # ��装来源宁可明确失败,也不能让 clone 跳进内网。
835
+ cmd = ["git", "-c", "http.followRedirects=false", "clone",
836
+ "--depth", "1", "--single-branch", "--quiet"]
829
837
  if inst.get("ref"):
830
838
  cmd += ["--branch", inst["ref"]]
831
839
  cmd += [inst["url"], str(dst)]
@@ -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 install_remote(entry_id):
965
- """安装外部目录插件:下载 → 纯技能检查 → 转换 → 复用 market 安装通道。
966
- 返回 (结果 dict, 错误)。"""
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
- kind = entry["install"]["kind"]
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
- res, err = market.install_files(entry_id, entry["title"], files, extra={
988
- "remote": {"source": entry["source_id"], "name": entry["name"],
989
- "version": entry["version"], "homepage": entry["homepage"]},
990
- })
991
- if err:
992
- return None, err
993
- res["skills"] = sum(1 for k in files if k.endswith(".md") and "/" not in k)
994
- res["stripped"] = sorted(stripped)
995
- return res, None
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
+ }