codebee 0.1.65 → 0.1.66
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 +520 -513
- package/LICENSE +21 -21
- package/README.md +494 -487
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/attachments.py +532 -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 +796 -759
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1234 -1191
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +23 -24
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -186
- package/app/core/covergen.py +356 -356
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +113 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +220 -65
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -0
- package/app/core/failure_notify.py +290 -0
- package/app/core/flows.py +355 -355
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -342
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -156
- package/app/core/jobs.py +1034 -1022
- package/app/core/knowledge.py +6 -2
- package/app/core/manager.py +2115 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +66 -66
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3517 -3332
- package/app/core/notify.py +106 -106
- package/app/core/paihang.py +109 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +4614 -4494
- package/app/core/planner.py +33 -31
- 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 +533 -533
- package/app/core/publish/fanqie.py +128 -128
- package/app/core/publish/flow.py +432 -406
- package/app/core/publish/flows-fanqie-calibrated.json +17 -9
- package/app/core/publish/flows-qimao-calibrated.json +6 -2
- package/app/core/publish/ledger.py +198 -198
- package/app/core/publish/manager.py +623 -590
- 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 -124
- package/app/core/router.py +119 -81
- package/app/core/runner.py +2050 -2042
- 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 +114 -106
- 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 +221 -72
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +23 -0
- 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/upstream.py +41 -0
- package/app/core/usage.py +114 -3
- package/app/core/volumes.py +355 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2447 -2447
- package/app/main.py +2838 -2832
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +13994 -13977
- package/app/ui/board.css +20 -41
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2716 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +1 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6107 -6125
- package/bin/tutti.js +147 -147
- package/package.json +46 -41
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- 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 +0 -210
- 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/error_codes.py
CHANGED
|
@@ -1,65 +1,220 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""错误码体系:把 runner 失败归一成可枚举的 ErrorCode。
|
|
3
|
-
|
|
4
|
-
设计稿:docs/migration/01-defense-patterns.md §5D + §2D。
|
|
5
|
-
参考 dsh docs/subsystems/workflow.zh.md:108-120 WorkflowErrorCode。
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from enum import Enum
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""错误码体系:把 runner 失败归一成可枚举的 ErrorCode。
|
|
3
|
+
|
|
4
|
+
设计稿:docs/migration/01-defense-patterns.md §5D + §2D。
|
|
5
|
+
参考 dsh docs/subsystems/workflow.zh.md:108-120 WorkflowErrorCode。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorCode(str, Enum):
|
|
14
|
+
"""所有 runner 失败的可枚举分类。
|
|
15
|
+
|
|
16
|
+
字段值是 JSON 友好的字符串,方便持久化到 store / 跨进程传输。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# === fatal:脚本/参数错,必须人工修 ===
|
|
20
|
+
VENDOR_ERROR = "VENDOR_ERROR" # vendor 内部崩溃(exit != 0)
|
|
21
|
+
VENDOR_REFUSAL = "VENDOR_REFUSAL" # 明确拒绝(claude is_error / qwen refusal)
|
|
22
|
+
PARSE_FAIL = "PARSE_FAIL" # 输出无法解析为预期 schema
|
|
23
|
+
ENV_BLOCK = "ENV_BLOCK" # approval 拒 / 策略阻断
|
|
24
|
+
SIGNAL_IGNORED = "SIGNAL_IGNORED" # 发了 SIGTERM/任务杀不掉
|
|
25
|
+
|
|
26
|
+
# === item:可重试 ===
|
|
27
|
+
TIMEOUT = "TIMEOUT"
|
|
28
|
+
CANCELLED = "CANCELLED"
|
|
29
|
+
MAX_TOKENS = "MAX_TOKENS" # 模型撑爆
|
|
30
|
+
EMPTY = "EMPTY" # 空输出
|
|
31
|
+
NETWORK = "NETWORK" # 连接失败
|
|
32
|
+
UPSTREAM_SERVER = "UPSTREAM_SERVER" # 上游 5xx / 服务端异常
|
|
33
|
+
MISSING_CREDENTIAL = "MISSING_CREDENTIAL" # 当前路由没有可用 API key
|
|
34
|
+
AUTH = "AUTH" # 凭据认证失败(401)
|
|
35
|
+
FORBIDDEN = "FORBIDDEN" # 上游拒绝当前账号/模型/请求(403)
|
|
36
|
+
QUOTA = "QUOTA" # 欠费、余额或调用额度耗尽
|
|
37
|
+
RATE_LIMIT = "RATE_LIMIT" # 短时速率/并发限制(429)
|
|
38
|
+
LOCAL_STATE = "LOCAL_STATE" # 本地 CLI 状态/锁冲突
|
|
39
|
+
CONTEXT_OVERFLOW = "CONTEXT_OVERFLOW" # 触发压缩(与 MAX_TOKENS 不同)
|
|
40
|
+
|
|
41
|
+
# === ok ===
|
|
42
|
+
# "" 表示成功,不设枚举值
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# 哪些码视为 fatal(job 终止,不进入重试队列)
|
|
46
|
+
FATAL_CODES = frozenset({
|
|
47
|
+
ErrorCode.VENDOR_ERROR,
|
|
48
|
+
ErrorCode.VENDOR_REFUSAL,
|
|
49
|
+
ErrorCode.PARSE_FAIL,
|
|
50
|
+
ErrorCode.ENV_BLOCK,
|
|
51
|
+
ErrorCode.SIGNAL_IGNORED,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_MISSING_CREDENTIAL = (
|
|
56
|
+
"no api key", "missing api key", "api key not found", "api key is required",
|
|
57
|
+
"missing environment variable", "missing env var",
|
|
58
|
+
)
|
|
59
|
+
_AUTH = (
|
|
60
|
+
"invalid api key", "invalid_api_key", "api key is invalid", "incorrect api key",
|
|
61
|
+
"api key not valid", "api key 不可用", "鉴权失败", "认证失败",
|
|
62
|
+
"unauthorized", "unauthorised", "authentication failed", "authentication error",
|
|
63
|
+
"authentication_error", "invalid token", "invalid_token", "http 401",
|
|
64
|
+
"status code 401",
|
|
65
|
+
)
|
|
66
|
+
_FORBIDDEN = ("forbidden", "request not allowed", "permission denied", "access denied")
|
|
67
|
+
_RATE_LIMIT = ("rate limit", "rate_limit", "too many requests", "http 429", "status code 429",
|
|
68
|
+
"concurrent limit", "concurrency limit", "too many concurrent",
|
|
69
|
+
"并发限制", "并发超限", "限流")
|
|
70
|
+
_QUOTA = ("insufficient balance", "insufficient quota", "insufficient credit",
|
|
71
|
+
"quota", "balance is insufficient", "credit balance", "billing", "arrears",
|
|
72
|
+
"payment required", "http 402", "status code 402", "欠费", "余额", "额度",
|
|
73
|
+
"usage limit", "limit exceeded", "quota exhausted")
|
|
74
|
+
_SERVER = ("unexpected server error", "internal server error", "server error",
|
|
75
|
+
"overloaded", "temporarily", "unavailable", "service unavailable",
|
|
76
|
+
"internal error", "no available channel", "unknown model")
|
|
77
|
+
_NETWORK = ("stream disconnected", "stream closed", "connection reset", "connection aborted",
|
|
78
|
+
"broken pipe", "socket hang up", "econnrefused", "enotfound", "getaddrinfo",
|
|
79
|
+
"name resolution", "network error", "network is unreachable", "reqwest error stream",
|
|
80
|
+
"error sending request for url", "no route to host", "connection error",
|
|
81
|
+
"reset by peer", "channel is closed", "name or service not known",
|
|
82
|
+
"输出停滞")
|
|
83
|
+
_LOCAL_STATE = ("database is locked", "database locked", "sqlite busy",
|
|
84
|
+
"unable to open database file")
|
|
85
|
+
_TRANSIENT_HINTS = ("stream closed before response.completed", "output stall", "timed out",
|
|
86
|
+
"timeout", "output stalled", "ENOTFOUND", "Reconnecting...",
|
|
87
|
+
"initialize", "[1211]")
|
|
88
|
+
_REFUSAL = ("can't help with this", "couldn't help with this", '"stop_reason":"refusal"',
|
|
89
|
+
"content policy", "content filtering")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def error_code_value(code):
|
|
93
|
+
"""Return the stable wire value for ErrorCode or a string-like code."""
|
|
94
|
+
if isinstance(code, ErrorCode):
|
|
95
|
+
return code.value
|
|
96
|
+
return str(code or "")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def classify_error_text(error):
|
|
100
|
+
"""Map a vendor/CLI error message to the shared failure taxonomy.
|
|
101
|
+
|
|
102
|
+
Return ``None`` when text does not provide a reliable signal. Cancellation and
|
|
103
|
+
timeout flags still take precedence in the caller because text may be noisy.
|
|
104
|
+
More specific credential/permission/quota signals are checked before generic
|
|
105
|
+
server and transport wording.
|
|
106
|
+
"""
|
|
107
|
+
text = str(error or "").lower()
|
|
108
|
+
if not text:
|
|
109
|
+
return None
|
|
110
|
+
if re.search(r"(?:http(?:/\d(?:\.\d)?)?\s*|status(?:\s+code)?\s*[:=]?\s*)401\b", text):
|
|
111
|
+
return ErrorCode.AUTH
|
|
112
|
+
# An explicit HTTP 403 is authoritative even when the provider describes the
|
|
113
|
+
# permission problem with credential wording (for example, "invalid API key
|
|
114
|
+
# for this model"). Do not cool down the key as an authentication failure.
|
|
115
|
+
if (re.search(r"(?:http(?:/\d(?:\.\d)?)?\s*|status(?:\s+code)?\s*[:=]?\s*|"
|
|
116
|
+
r"code\s*[:=]?\s*)403\b", text)
|
|
117
|
+
or re.search(r"(?:^|:\s*)403(?=$|[\s:{])", text)):
|
|
118
|
+
return ErrorCode.FORBIDDEN
|
|
119
|
+
if any(marker in text for marker in _MISSING_CREDENTIAL):
|
|
120
|
+
return ErrorCode.MISSING_CREDENTIAL
|
|
121
|
+
if (any(marker in text for marker in _AUTH)
|
|
122
|
+
or re.search(r"(?:http(?:/\d(?:\.\d)?)?\s*|status(?:\s+code)?\s*[:=]?\s*)401\b", text)):
|
|
123
|
+
return ErrorCode.AUTH
|
|
124
|
+
if any(marker in text for marker in _FORBIDDEN):
|
|
125
|
+
return ErrorCode.FORBIDDEN
|
|
126
|
+
if (re.search(r"(?:http(?:/\d(?:\.\d)?)?\s*|status(?:\s+code)?\s*[:=]?\s*)429\b", text)
|
|
127
|
+
or any(marker in text for marker in _RATE_LIMIT)):
|
|
128
|
+
return ErrorCode.RATE_LIMIT
|
|
129
|
+
if (re.search(r"(?:http(?:/\d(?:\.\d)?)?\s*|status(?:\s+code)?\s*[:=]?\s*)402\b", text)
|
|
130
|
+
or any(marker in text for marker in _QUOTA)):
|
|
131
|
+
return ErrorCode.QUOTA
|
|
132
|
+
if any(marker in text for marker in _LOCAL_STATE):
|
|
133
|
+
return ErrorCode.LOCAL_STATE
|
|
134
|
+
if (re.search(r"(?<!\d)5\d\d(?!\d)", text)
|
|
135
|
+
or any(marker in text for marker in _SERVER)):
|
|
136
|
+
return ErrorCode.UPSTREAM_SERVER
|
|
137
|
+
if ("timed out" in text or "timeout" in text or "deadline exceeded" in text
|
|
138
|
+
or re.search(r"(?<!\d)408(?!\d)", text)):
|
|
139
|
+
return ErrorCode.TIMEOUT
|
|
140
|
+
if any(marker in text for marker in _NETWORK):
|
|
141
|
+
return ErrorCode.NETWORK
|
|
142
|
+
if any(marker in text for marker in _REFUSAL):
|
|
143
|
+
return ErrorCode.VENDOR_REFUSAL
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_auth_error(error):
|
|
148
|
+
return classify_error_text(error) == ErrorCode.AUTH
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_forbidden_error(error):
|
|
152
|
+
return classify_error_text(error) == ErrorCode.FORBIDDEN
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_quota_error(error):
|
|
156
|
+
return classify_error_text(error) in (ErrorCode.QUOTA, ErrorCode.RATE_LIMIT)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_transient_error(error):
|
|
160
|
+
text = str(error or "").lower()
|
|
161
|
+
return classify_error_text(text) in (
|
|
162
|
+
ErrorCode.NETWORK, ErrorCode.UPSTREAM_SERVER, ErrorCode.TIMEOUT,
|
|
163
|
+
ErrorCode.QUOTA, ErrorCode.RATE_LIMIT, ErrorCode.LOCAL_STATE,
|
|
164
|
+
) or any(marker.lower() in text for marker in _TRANSIENT_HINTS)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_rate_limited_error(error):
|
|
168
|
+
return classify_error_text(error) == ErrorCode.RATE_LIMIT
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_SAFE_ERROR_SUMMARIES = {
|
|
172
|
+
ErrorCode.VENDOR_ERROR: "供应商或 CLI 调用失败(未识别具体原因)",
|
|
173
|
+
ErrorCode.VENDOR_REFUSAL: "上游拒绝了本次请求",
|
|
174
|
+
ErrorCode.PARSE_FAIL: "模型响应无法解析",
|
|
175
|
+
ErrorCode.ENV_BLOCK: "本机环境或执行策略阻断",
|
|
176
|
+
ErrorCode.SIGNAL_IGNORED: "子进程未响应终止信号",
|
|
177
|
+
ErrorCode.TIMEOUT: "调用超时",
|
|
178
|
+
ErrorCode.CANCELLED: "调用已取消",
|
|
179
|
+
ErrorCode.MAX_TOKENS: "模型输出达到 token 上限",
|
|
180
|
+
ErrorCode.EMPTY: "模型未返回内容",
|
|
181
|
+
ErrorCode.NETWORK: "网络连接或数据流中断",
|
|
182
|
+
ErrorCode.UPSTREAM_SERVER: "上游服务端异常",
|
|
183
|
+
ErrorCode.MISSING_CREDENTIAL: "所选路由未找到可用 API 凭据",
|
|
184
|
+
ErrorCode.AUTH: "API 凭据认证失败",
|
|
185
|
+
ErrorCode.FORBIDDEN: "上游拒绝账号、模型或请求权限",
|
|
186
|
+
ErrorCode.QUOTA: "上游余额或调用额度不足",
|
|
187
|
+
ErrorCode.RATE_LIMIT: "上游触发速率或并发限制",
|
|
188
|
+
ErrorCode.LOCAL_STATE: "CLI 本地状态或数据库锁冲突",
|
|
189
|
+
ErrorCode.CONTEXT_OVERFLOW: "输入上下文超出模型容量",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def safe_error_summary(code):
|
|
194
|
+
"""A content-free summary suitable for durable diagnostics and telemetry."""
|
|
195
|
+
try:
|
|
196
|
+
value = ErrorCode(code)
|
|
197
|
+
except (ValueError, TypeError):
|
|
198
|
+
value = ErrorCode.VENDOR_ERROR
|
|
199
|
+
return _SAFE_ERROR_SUMMARIES.get(value, _SAFE_ERROR_SUMMARIES[ErrorCode.VENDOR_ERROR])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def is_fatal(code) -> bool:
|
|
203
|
+
"""code 可以是 ErrorCode 或字符串(如来自 dict.get('error_code'))。"""
|
|
204
|
+
if not code:
|
|
205
|
+
return False
|
|
206
|
+
try:
|
|
207
|
+
return ErrorCode(code) in FATAL_CODES
|
|
208
|
+
except ValueError:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def is_retryable(code) -> bool:
|
|
213
|
+
"""item 类(可重试)。"""
|
|
214
|
+
if not code:
|
|
215
|
+
return False
|
|
216
|
+
try:
|
|
217
|
+
ec = ErrorCode(code)
|
|
218
|
+
return ec not in FATAL_CODES and ec != ErrorCode.CANCELLED
|
|
219
|
+
except ValueError:
|
|
220
|
+
return False
|
package/app/core/errorlog.py
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""错误台账:把分散在各处的失败(步骤失败/超时/看门狗击杀、CLI 启动失败)
|
|
3
|
-
统一落成结构化记录——「客户遇到了问题我们根本不知道」的第一块拼图。
|
|
4
|
-
|
|
5
|
-
落盘:data/errors/errors-YYYYMM.jsonl(按月分文件,append-only,一行一记录)。
|
|
6
|
-
与 usage.py 同一套纪律:
|
|
7
|
-
- record() 任何异常都吞掉——埋点失败绝不能影响任务执行;
|
|
8
|
-
- 落盘前先脱敏(scrub_text):密钥/令牌/绝对路径/超长文本一概不进台账。
|
|
9
|
-
|
|
10
|
-
隐私红线(上传侧在 telemetry.py 再兜一道底):
|
|
11
|
-
1. API KEY / 凭据绝不入台账;
|
|
12
|
-
2. 任务正文 / 章节内容(用户版权内容)绝不入台账——detail 只允许失败原因摘录;
|
|
13
|
-
3. 绝对路径剥成相对片段。
|
|
14
|
-
"""
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import threading
|
|
19
|
-
import time
|
|
20
|
-
import uuid
|
|
21
|
-
|
|
22
|
-
from . import paths
|
|
23
|
-
from .redact import scrub_text
|
|
24
|
-
|
|
25
|
-
LOCK = threading.RLock()
|
|
26
|
-
|
|
27
|
-
FIELDS = ("id", "ts", "day", "category", "reason", "detail", "provider",
|
|
28
|
-
"model", "tool", "role", "run_id", "task_id", "step",
|
|
29
|
-
"exit_code", "app_version", "os")
|
|
30
|
-
|
|
31
|
-
# detail 摘录上限(字符):失败原因足够,不要变成日志搬运
|
|
32
|
-
DETAIL_LIMIT = 600
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# ---------------------------------------------------------------- 脱敏
|
|
36
|
-
|
|
37
|
-
def _coerce_str(v, limit):
|
|
38
|
-
return str(v or "")[:limit]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _os_tag():
|
|
42
|
-
import sys
|
|
43
|
-
return {"win32": "windows", "darwin": "macos"}.get(sys.platform, sys.platform)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _version():
|
|
47
|
-
try:
|
|
48
|
-
from . import selfupdate
|
|
49
|
-
return str(selfupdate.package_version() or "dev")
|
|
50
|
-
except Exception:
|
|
51
|
-
return "dev"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def record(category="", reason="", detail="", provider="", model="", tool="",
|
|
55
|
-
role="", run_id="", task_id="", step=0, exit_code=None, source=""):
|
|
56
|
-
"""追加一条错误记录。缺省字段留空;任何异常都吞掉(埋点不拖垮业务)。
|
|
57
|
-
|
|
58
|
-
category: step / run / launch / service(枚举放宽,未知值原样入库)
|
|
59
|
-
reason: 失败原因码(error_codes.ErrorCode 的字符串值优先,如 TIMEOUT)
|
|
60
|
-
detail: 失败摘录(内部先过 scrub_text,绝不存原文)
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
rec = {
|
|
64
|
-
"id": "%s-%s" % (time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
65
|
-
uuid.uuid4().hex[:8]),
|
|
66
|
-
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
67
|
-
"day": time.strftime("%Y-%m-%d"),
|
|
68
|
-
"category": _coerce_str(category, 24) or "step",
|
|
69
|
-
"reason": _coerce_str(reason, 40) or "UNKNOWN",
|
|
70
|
-
"detail": scrub_text(detail),
|
|
71
|
-
"provider": _coerce_str(provider, 60),
|
|
72
|
-
"model": _coerce_str(model, 80),
|
|
73
|
-
"tool": _coerce_str(tool, 24),
|
|
74
|
-
"role": _coerce_str(role, 40),
|
|
75
|
-
"run_id": _coerce_str(run_id, 64),
|
|
76
|
-
"task_id": _coerce_str(task_id, 64),
|
|
77
|
-
"step": int(step or 0),
|
|
78
|
-
"exit_code": exit_code,
|
|
79
|
-
"app_version": _version()[:24],
|
|
80
|
-
"os": _os_tag(),
|
|
81
|
-
}
|
|
82
|
-
line = json.dumps(rec, ensure_ascii=False)
|
|
83
|
-
day = rec["day"]
|
|
84
|
-
with LOCK:
|
|
85
|
-
paths.ERRORS_DIR.mkdir(parents=True, exist_ok=True)
|
|
86
|
-
with open(paths.ERRORS_DIR / ("errors-%s.jsonl" % day[:7].replace("-", "")),
|
|
87
|
-
"a", encoding="utf-8") as f:
|
|
88
|
-
f.write(line + "\n")
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def iter_records(days=0):
|
|
94
|
-
"""按时间范围读取台账(days=0 表示全部)。返回按写入顺序的记录列表。"""
|
|
95
|
-
out = []
|
|
96
|
-
try:
|
|
97
|
-
files = sorted(paths.ERRORS_DIR.glob("errors-*.jsonl")) if paths.ERRORS_DIR.is_dir() else []
|
|
98
|
-
except Exception:
|
|
99
|
-
return out
|
|
100
|
-
since_day = ""
|
|
101
|
-
if days:
|
|
102
|
-
import datetime
|
|
103
|
-
since_day = (datetime.date.today()
|
|
104
|
-
- datetime.timedelta(days=int(days) - 1)).isoformat()
|
|
105
|
-
for p in files:
|
|
106
|
-
try:
|
|
107
|
-
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
108
|
-
line = line.strip()
|
|
109
|
-
if not line.startswith("{"):
|
|
110
|
-
continue
|
|
111
|
-
try:
|
|
112
|
-
r = json.loads(line)
|
|
113
|
-
except Exception:
|
|
114
|
-
continue
|
|
115
|
-
if not isinstance(r, dict):
|
|
116
|
-
continue
|
|
117
|
-
if since_day and str(r.get("day", "")) < since_day:
|
|
118
|
-
continue
|
|
119
|
-
out.append(r)
|
|
120
|
-
except Exception:
|
|
121
|
-
continue
|
|
122
|
-
return out
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def pending_since(cursor, limit=200):
|
|
126
|
-
"""取 cursor(上次上传到的记录 id)之后的记录,最多 limit 条。
|
|
127
|
-
|
|
128
|
-
id 以时间开头、uuid 尾巴保证唯一且字典序≈时间序;返回 (records, new_cursor)。
|
|
129
|
-
"""
|
|
130
|
-
try:
|
|
131
|
-
recs = [r for r in iter_records(0) if str(r.get("id") or "") > str(cursor or "")]
|
|
132
|
-
recs.sort(key=lambda r: str(r.get("id") or ""))
|
|
133
|
-
batch = recs[:max(1, int(limit))]
|
|
134
|
-
new_cursor = str(batch[-1].get("id")) if batch else str(cursor or "")
|
|
135
|
-
return batch, new_cursor
|
|
136
|
-
except Exception:
|
|
137
|
-
return [], str(cursor or "")
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""错误台账:把分散在各处的失败(步骤失败/超时/看门狗击杀、CLI 启动失败)
|
|
3
|
+
统一落成结构化记录——「客户遇到了问题我们根本不知道」的第一块拼图。
|
|
4
|
+
|
|
5
|
+
落盘:data/errors/errors-YYYYMM.jsonl(按月分文件,append-only,一行一记录)。
|
|
6
|
+
与 usage.py 同一套纪律:
|
|
7
|
+
- record() 任何异常都吞掉——埋点失败绝不能影响任务执行;
|
|
8
|
+
- 落盘前先脱敏(scrub_text):密钥/令牌/绝对路径/超长文本一概不进台账。
|
|
9
|
+
|
|
10
|
+
隐私红线(上传侧在 telemetry.py 再兜一道底):
|
|
11
|
+
1. API KEY / 凭据绝不入台账;
|
|
12
|
+
2. 任务正文 / 章节内容(用户版权内容)绝不入台账——detail 只允许失败原因摘录;
|
|
13
|
+
3. 绝对路径剥成相对片段。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
from . import paths
|
|
23
|
+
from .redact import scrub_text
|
|
24
|
+
|
|
25
|
+
LOCK = threading.RLock()
|
|
26
|
+
|
|
27
|
+
FIELDS = ("id", "ts", "day", "category", "reason", "detail", "provider",
|
|
28
|
+
"model", "tool", "role", "run_id", "task_id", "step",
|
|
29
|
+
"exit_code", "app_version", "os")
|
|
30
|
+
|
|
31
|
+
# detail 摘录上限(字符):失败原因足够,不要变成日志搬运
|
|
32
|
+
DETAIL_LIMIT = 600
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------- 脱敏
|
|
36
|
+
|
|
37
|
+
def _coerce_str(v, limit):
|
|
38
|
+
return str(v or "")[:limit]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _os_tag():
|
|
42
|
+
import sys
|
|
43
|
+
return {"win32": "windows", "darwin": "macos"}.get(sys.platform, sys.platform)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _version():
|
|
47
|
+
try:
|
|
48
|
+
from . import selfupdate
|
|
49
|
+
return str(selfupdate.package_version() or "dev")
|
|
50
|
+
except Exception:
|
|
51
|
+
return "dev"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def record(category="", reason="", detail="", provider="", model="", tool="",
|
|
55
|
+
role="", run_id="", task_id="", step=0, exit_code=None, source=""):
|
|
56
|
+
"""追加一条错误记录。缺省字段留空;任何异常都吞掉(埋点不拖垮业务)。
|
|
57
|
+
|
|
58
|
+
category: step / run / launch / service(枚举放宽,未知值原样入库)
|
|
59
|
+
reason: 失败原因码(error_codes.ErrorCode 的字符串值优先,如 TIMEOUT)
|
|
60
|
+
detail: 失败摘录(内部先过 scrub_text,绝不存原文)
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
rec = {
|
|
64
|
+
"id": "%s-%s" % (time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
65
|
+
uuid.uuid4().hex[:8]),
|
|
66
|
+
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
67
|
+
"day": time.strftime("%Y-%m-%d"),
|
|
68
|
+
"category": _coerce_str(category, 24) or "step",
|
|
69
|
+
"reason": _coerce_str(reason, 40) or "UNKNOWN",
|
|
70
|
+
"detail": scrub_text(detail),
|
|
71
|
+
"provider": _coerce_str(provider, 60),
|
|
72
|
+
"model": _coerce_str(model, 80),
|
|
73
|
+
"tool": _coerce_str(tool, 24),
|
|
74
|
+
"role": _coerce_str(role, 40),
|
|
75
|
+
"run_id": _coerce_str(run_id, 64),
|
|
76
|
+
"task_id": _coerce_str(task_id, 64),
|
|
77
|
+
"step": int(step or 0),
|
|
78
|
+
"exit_code": exit_code,
|
|
79
|
+
"app_version": _version()[:24],
|
|
80
|
+
"os": _os_tag(),
|
|
81
|
+
}
|
|
82
|
+
line = json.dumps(rec, ensure_ascii=False)
|
|
83
|
+
day = rec["day"]
|
|
84
|
+
with LOCK:
|
|
85
|
+
paths.ERRORS_DIR.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
with open(paths.ERRORS_DIR / ("errors-%s.jsonl" % day[:7].replace("-", "")),
|
|
87
|
+
"a", encoding="utf-8") as f:
|
|
88
|
+
f.write(line + "\n")
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def iter_records(days=0):
|
|
94
|
+
"""按时间范围读取台账(days=0 表示全部)。返回按写入顺序的记录列表。"""
|
|
95
|
+
out = []
|
|
96
|
+
try:
|
|
97
|
+
files = sorted(paths.ERRORS_DIR.glob("errors-*.jsonl")) if paths.ERRORS_DIR.is_dir() else []
|
|
98
|
+
except Exception:
|
|
99
|
+
return out
|
|
100
|
+
since_day = ""
|
|
101
|
+
if days:
|
|
102
|
+
import datetime
|
|
103
|
+
since_day = (datetime.date.today()
|
|
104
|
+
- datetime.timedelta(days=int(days) - 1)).isoformat()
|
|
105
|
+
for p in files:
|
|
106
|
+
try:
|
|
107
|
+
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line.startswith("{"):
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
r = json.loads(line)
|
|
113
|
+
except Exception:
|
|
114
|
+
continue
|
|
115
|
+
if not isinstance(r, dict):
|
|
116
|
+
continue
|
|
117
|
+
if since_day and str(r.get("day", "")) < since_day:
|
|
118
|
+
continue
|
|
119
|
+
out.append(r)
|
|
120
|
+
except Exception:
|
|
121
|
+
continue
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def pending_since(cursor, limit=200):
|
|
126
|
+
"""取 cursor(上次上传到的记录 id)之后的记录,最多 limit 条。
|
|
127
|
+
|
|
128
|
+
id 以时间开头、uuid 尾巴保证唯一且字典序≈时间序;返回 (records, new_cursor)。
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
recs = [r for r in iter_records(0) if str(r.get("id") or "") > str(cursor or "")]
|
|
132
|
+
recs.sort(key=lambda r: str(r.get("id") or ""))
|
|
133
|
+
batch = recs[:max(1, int(limit))]
|
|
134
|
+
new_cursor = str(batch[-1].get("id")) if batch else str(cursor or "")
|
|
135
|
+
return batch, new_cursor
|
|
136
|
+
except Exception:
|
|
137
|
+
return [], str(cursor or "")
|