codebee 0.1.67 → 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 -532
- 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 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- 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 -930
- 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 -43
- 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 +2 -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 +29 -25
- package/app/core/knowledge.py +7 -0
- package/app/core/manager.py +2122 -2122
- package/app/core/market.py +362 -362
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +22 -3
- package/app/core/notify.py +106 -106
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +138 -138
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +16 -1
- package/app/core/planner.py +2 -0
- 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 -539
- package/app/core/publish/fanqie.py +159 -159
- package/app/core/publish/flow.py +440 -440
- 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 -203
- package/app/core/publish/manager.py +692 -692
- 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/router.py +398 -398
- package/app/core/runner.py +2067 -2065
- 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 +15 -1
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +75 -34
- 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 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2465 -2465
- package/app/main.py +2981 -2957
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +14399 -14384
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2770 -2766
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +16 -16
- package/bin/tutti.js +147 -147
- package/package.json +46 -46
- package/tools/wx_reader.py +261 -261
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
|
@@ -2419,9 +2419,19 @@ def bindable_protocols(agent_kind_or_id):
|
|
|
2419
2419
|
return ("openai",)
|
|
2420
2420
|
if agent_kind_or_id in ("claude-code", "claude"):
|
|
2421
2421
|
return ("anthropic",)
|
|
2422
|
+
if agent_kind_or_id in _LOCAL_CRED_ONLY_TARGETS:
|
|
2423
|
+
return ()
|
|
2422
2424
|
return tuple(_BINDABLE_PROTOCOLS)
|
|
2423
2425
|
|
|
2424
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
|
+
|
|
2425
2435
|
def binding_dead_msg(cli_id):
|
|
2426
2436
|
"""死链失败/告警文案(pipeline 死链闸门与本模块 sync 共用):说明为什么
|
|
2427
2437
|
不回落本机默认 + 该 CLI 需要什么协议的供应商。"""
|
|
@@ -2429,12 +2439,15 @@ def binding_dead_msg(cli_id):
|
|
|
2429
2439
|
protos = bindable_protocols(cli_id)
|
|
2430
2440
|
except Exception:
|
|
2431
2441
|
protos = ()
|
|
2432
|
-
|
|
2442
|
+
if not protos:
|
|
2443
|
+
return ("该 CLI 暂不支持绑定链(凭据注入尚未适配,使用 CLI 本机登录态与"
|
|
2444
|
+
"默认模型);请勿为其配置供应商链")
|
|
2445
|
+
hint = "该 CLI 仅接受 %s 协议的已启用供应商;" % "、".join(protos)
|
|
2433
2446
|
return ("绑定链全部失效(链上供应商已停用/删除/无密钥,或模型已停用),"
|
|
2434
2447
|
"本步判失败、不回落 CLI 本机默认——%s请在「模型调度(可选)」页为该 CLI 指定已启用的供应商" % hint)
|
|
2435
2448
|
|
|
2436
2449
|
|
|
2437
|
-
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
2450
|
+
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
2438
2451
|
"""返回 {env:{}, model:..., model_fallbacks:[...], codex_provider:..., call_chain:[...]} 或 None。
|
|
2439
2452
|
|
|
2440
2453
|
call_chain 是跨厂商降级链(唯一真源):每条 {model, env, provider, [codex_provider]},
|
|
@@ -2442,7 +2455,13 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2442
2455
|
→ 整体回落 CLI 默认(None)。纯链条目(无供应商)不注入 env,只传 -m。
|
|
2443
2456
|
难度路由只在无显式链时生效。
|
|
2444
2457
|
"""
|
|
2445
|
-
|
|
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)
|
|
2446
2465
|
chain = _binding_chain(b)
|
|
2447
2466
|
provs = {p.get("id"): p for p in providers()}
|
|
2448
2467
|
routing = bool(b.get("difficulty_routing"))
|
package/app/core/notify.py
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
|
|
3
|
-
推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
|
|
4
|
-
|
|
5
|
-
webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
|
|
6
|
-
思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
|
|
7
|
-
推送在后台线程、失败只记日志——通知永远不影响任务本身。
|
|
8
|
-
"""
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import logging
|
|
12
|
-
|
|
13
|
-
log = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _payload_for(webhook, text):
|
|
17
|
-
"""按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
|
|
18
|
-
from urllib.parse import urlparse
|
|
19
|
-
host = (urlparse(webhook).hostname or "").lower()
|
|
20
|
-
if "dingtalk" in host:
|
|
21
|
-
return {"msgtype": "text", "text": {"content": text}}
|
|
22
|
-
if "feishu" in host or "larksuite" in host:
|
|
23
|
-
return {"msg_type": "text", "content": {"text": text}}
|
|
24
|
-
if "weixin" in host or "wechat" in host or "work.weixin" in host:
|
|
25
|
-
return {"msgtype": "text", "text": {"content": text}}
|
|
26
|
-
if host:
|
|
27
|
-
return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
|
|
28
|
-
return None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _webhook():
|
|
32
|
-
try:
|
|
33
|
-
from . import settings
|
|
34
|
-
return str(settings.load().get("notify_webhook") or "").strip()
|
|
35
|
-
except Exception:
|
|
36
|
-
return ""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _post(hook, text):
|
|
40
|
-
"""curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
|
|
41
|
-
from . import runner
|
|
42
|
-
import json as _json
|
|
43
|
-
body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
|
|
44
|
-
r = runner.run_process(
|
|
45
|
-
argv=["curl", "-sS", "--max-time", "20",
|
|
46
|
-
"-H", "Content-Type: application/json",
|
|
47
|
-
"-d", body, hook],
|
|
48
|
-
timeout=30)
|
|
49
|
-
return bool(r["ok"])
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def push_text(text):
|
|
53
|
-
"""推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
|
|
54
|
-
hook = _webhook()
|
|
55
|
-
if not hook:
|
|
56
|
-
return False
|
|
57
|
-
if not hook.startswith("https://"):
|
|
58
|
-
log.warning("notify webhook 必须是 https")
|
|
59
|
-
return False
|
|
60
|
-
try:
|
|
61
|
-
return _post(hook, text)
|
|
62
|
-
except Exception as e:
|
|
63
|
-
log.warning("notify push failed: %s", e)
|
|
64
|
-
return False
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def push_run_async(run_id):
|
|
68
|
-
"""任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
|
|
69
|
-
threading.Thread(target=push_run, daemon=True,
|
|
70
|
-
name="notify-%s" % run_id, args=(run_id,)).start()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def push_run(run_id):
|
|
74
|
-
"""组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
|
|
75
|
-
|
|
76
|
-
摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
|
|
77
|
-
run_id 供在本机 CodeBee 界面查找)。"""
|
|
78
|
-
hook = _webhook()
|
|
79
|
-
if not hook:
|
|
80
|
-
return False
|
|
81
|
-
from . import store
|
|
82
|
-
run = store.get_run(run_id) or {}
|
|
83
|
-
if not run:
|
|
84
|
-
return False
|
|
85
|
-
status = str(run.get("status") or "")
|
|
86
|
-
mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
|
|
87
|
-
lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
|
|
88
|
-
"cancelled": "已取消"}.get(status, status))]
|
|
89
|
-
lines.append("任务:%s" % (run.get("title") or run_id))
|
|
90
|
-
if run.get("error"):
|
|
91
|
-
lines.append("错误:%s" % str(run["error"])[:200])
|
|
92
|
-
v = run.get("verdict") or {}
|
|
93
|
-
if v.get("overall") is not None:
|
|
94
|
-
lines.append("综合评分 %.1f(%s)" % (
|
|
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
|
|
103
|
-
return push_text("\n".join(lines))
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""运行结果群推送(借鉴 agency-orchestrator 的 --notify):任务跑完把结果摘要
|
|
3
|
+
推到钉钉/飞书/企业微信群机器人,配合定时自动化就是「AI 团队每天定点交活」。
|
|
4
|
+
|
|
5
|
+
webhook 一个地址全包——按域名自动适配三种机器人格式(同 agency-orchestrator
|
|
6
|
+
思路);也接受任意 https 地址(按钉钉 text 形状发,自建 n8n 等自选)。
|
|
7
|
+
推送在后台线程、失败只记日志——通知永远不影响任务本身。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _payload_for(webhook, text):
|
|
17
|
+
"""按 webhook 域名适配群机器人消息体。返回 None = 无法识别的地址。"""
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
host = (urlparse(webhook).hostname or "").lower()
|
|
20
|
+
if "dingtalk" in host:
|
|
21
|
+
return {"msgtype": "text", "text": {"content": text}}
|
|
22
|
+
if "feishu" in host or "larksuite" in host:
|
|
23
|
+
return {"msg_type": "text", "content": {"text": text}}
|
|
24
|
+
if "weixin" in host or "wechat" in host or "work.weixin" in host:
|
|
25
|
+
return {"msgtype": "text", "text": {"content": text}}
|
|
26
|
+
if host:
|
|
27
|
+
return {"msgtype": "text", "text": {"content": text}} # 未知域名按钉钉形状
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _webhook():
|
|
32
|
+
try:
|
|
33
|
+
from . import settings
|
|
34
|
+
return str(settings.load().get("notify_webhook") or "").strip()
|
|
35
|
+
except Exception:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _post(hook, text):
|
|
40
|
+
"""curl 子进程 POST(Python 不经手响应体);网络失败只记日志。"""
|
|
41
|
+
from . import runner
|
|
42
|
+
import json as _json
|
|
43
|
+
body = _json.dumps(_payload_for(hook, text), ensure_ascii=False)
|
|
44
|
+
r = runner.run_process(
|
|
45
|
+
argv=["curl", "-sS", "--max-time", "20",
|
|
46
|
+
"-H", "Content-Type: application/json",
|
|
47
|
+
"-d", body, hook],
|
|
48
|
+
timeout=30)
|
|
49
|
+
return bool(r["ok"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def push_text(text):
|
|
53
|
+
"""推一条文本到群。配置了 webhook 才推;失败返回 False 不抛错。"""
|
|
54
|
+
hook = _webhook()
|
|
55
|
+
if not hook:
|
|
56
|
+
return False
|
|
57
|
+
if not hook.startswith("https://"):
|
|
58
|
+
log.warning("notify webhook 必须是 https")
|
|
59
|
+
return False
|
|
60
|
+
try:
|
|
61
|
+
return _post(hook, text)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
log.warning("notify push failed: %s", e)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def push_run_async(run_id):
|
|
68
|
+
"""任务收尾后异步推送结果摘要(jobs 层调用;绝不阻塞/影响任务)。"""
|
|
69
|
+
threading.Thread(target=push_run, daemon=True,
|
|
70
|
+
name="notify-%s" % run_id, args=(run_id,)).start()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def push_run(run_id):
|
|
74
|
+
"""组装 run 结果摘要并推送(webhook 未配置时静默跳过)。
|
|
75
|
+
|
|
76
|
+
摘要末尾附分享页路径(notify_base_url 设置非空时拼完整链接,否则只给
|
|
77
|
+
run_id 供在本机 CodeBee 界面查找)。"""
|
|
78
|
+
hook = _webhook()
|
|
79
|
+
if not hook:
|
|
80
|
+
return False
|
|
81
|
+
from . import store
|
|
82
|
+
run = store.get_run(run_id) or {}
|
|
83
|
+
if not run:
|
|
84
|
+
return False
|
|
85
|
+
status = str(run.get("status") or "")
|
|
86
|
+
mark = {"done": "✅", "failed": "❌", "cancelled": "⚪"}.get(status, "🔔")
|
|
87
|
+
lines = ["%s CodeBee 任务%s" % (mark, {"done": "完成", "failed": "失败",
|
|
88
|
+
"cancelled": "已取消"}.get(status, status))]
|
|
89
|
+
lines.append("任务:%s" % (run.get("title") or run_id))
|
|
90
|
+
if run.get("error"):
|
|
91
|
+
lines.append("错误:%s" % str(run["error"])[:200])
|
|
92
|
+
v = run.get("verdict") or {}
|
|
93
|
+
if v.get("overall") is not None:
|
|
94
|
+
lines.append("综合评分 %.1f(%s)" % (
|
|
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
|
|
103
|
+
return push_text("\n".join(lines))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
import threading # noqa: E402 (push_run_async 依赖;置底避免顶部循环导入)
|