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.
- package/CHANGELOG.md +532 -513
- package/LICENSE +21 -21
- package/README.md +494 -487
- 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 -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/defectretro.py +43 -0
- 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 +401 -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 +1050 -1022
- package/app/core/knowledge.py +56 -5
- package/app/core/manager.py +2122 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +190 -92
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3546 -3332
- 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 +4808 -4494
- package/app/core/planner.py +37 -32
- 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 -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 +203 -198
- package/app/core/publish/manager.py +692 -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/revisions.py +51 -0
- package/app/core/router.py +119 -81
- package/app/core/runner.py +2065 -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 +315 -73
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +230 -11
- 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 +389 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2465 -2447
- package/app/main.py +2957 -2832
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +14384 -13977
- package/app/ui/artdiff.js +74 -0
- 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 +2766 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +7 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6179 -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/step_runner.py
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""Step 执行器:压缩触发的重试守门(复用已渲染 prompt,不重复组装)。
|
|
3
|
-
|
|
4
|
-
设计稿:docs/migration/02-context-compaction.md §1D。
|
|
5
|
-
参考 dsh packages/core/agent-loop/src/agent.ts:307-378:
|
|
6
|
-
失败重试只在「前进 surface generation」时返回 {kind:'retry'},
|
|
7
|
-
复用已渲染 PromptAssembly,不重跑 pre-step / 组装 / 用户消息准入。
|
|
8
|
-
|
|
9
|
-
Tutti 语境:runner.run_agent 自带 prompt(已渲染);重试的风险不是重复渲染
|
|
10
|
-
(prompt 是同一个字符串),而是「不压缩直接重试」必然再次撑爆。
|
|
11
|
-
本模块的守门:仅当 maybe_compact 真正前进了 surface generation 才重试一次。
|
|
12
|
-
"""
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
|
|
17
|
-
from .compaction import maybe_compact, DEFAULT_PRESSURE_THRESHOLD
|
|
18
|
-
from .error_codes import ErrorCode
|
|
19
|
-
|
|
20
|
-
log = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
# 触发「压缩后重试」的错误码
|
|
23
|
-
_OVERFLOW_CODES = {ErrorCode.CONTEXT_OVERFLOW, ErrorCode.MAX_TOKENS}
|
|
24
|
-
|
|
25
|
-
# 事前预检阈值:最近一次调用的上下文锚点已占本步模型容量九成 → 第一个请求
|
|
26
|
-
# 大概率被供应商拒。只拦「大概率必死」,不提前压缩平时的高压(used() 是累计
|
|
27
|
-
# 口径,比真实上下文偏大;0.8 的响应式阈值语义与此不同,勿混用)。
|
|
28
|
-
_PRECHECK_RATIO = 0.9
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _v2_compaction_tuning():
|
|
32
|
-
"""settings_v2 orchestrator.compaction 微调 → (threshold|None, retain|None)。
|
|
33
|
-
|
|
34
|
-
None = 未配置(走 maybe_compact 的模块默认);读取/注册失败静默回落。
|
|
35
|
-
retain 允许 0(尾部不保留),与 None(未配置)语义不同,勿合并。"""
|
|
36
|
-
try:
|
|
37
|
-
from .settings_schema import get as ss_get, register_default_namespaces
|
|
38
|
-
register_default_namespaces()
|
|
39
|
-
c = (ss_get("orchestrator") or {}).get("compaction") or {}
|
|
40
|
-
th = c.get("pressure_threshold")
|
|
41
|
-
rt = c.get("retain_tail_tokens")
|
|
42
|
-
return (float(th) if th else None,
|
|
43
|
-
int(rt) if rt is not None else None)
|
|
44
|
-
except Exception:
|
|
45
|
-
return None, None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def execute_step(session, run_agent_fn, prompt, *, model: str = "",
|
|
49
|
-
llm_caller=None, retain_tail_tokens=None, **kwargs):
|
|
50
|
-
"""执行一次 step;撑爆时压缩并守门重试一次。
|
|
51
|
-
|
|
52
|
-
事前预检(借鉴 freebuff base-chat 每步容量重估):换将切到小窗口模型后,
|
|
53
|
-
响应式路径要等供应商报 CONTEXT_OVERFLOW 才压缩——报得不干净就直接卡死。
|
|
54
|
-
先用「最近一次调用上下文 vs 本步模型容量」判一次,超阈先压缩再发。
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
session: session_log.Session(记录 replace_generation)
|
|
58
|
-
run_agent_fn: runner.run_agent(或测试替身),签名 (prompt, **kwargs) -> dict
|
|
59
|
-
prompt: 已渲染的提示词(重试时原样复用,不重新渲染)
|
|
60
|
-
model: 本步将要使用的模型名(容量与压力估算都按它算)
|
|
61
|
-
llm_caller: 压缩摘要 LLM(None = 不压缩,永不重试)
|
|
62
|
-
**kwargs: 透传 run_agent_fn
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
(result, retried: bool)
|
|
66
|
-
"""
|
|
67
|
-
v2_th, v2_rt = _v2_compaction_tuning()
|
|
68
|
-
compact_kwargs = {}
|
|
69
|
-
if retain_tail_tokens is not None:
|
|
70
|
-
compact_kwargs["retain_tail_tokens"] = retain_tail_tokens
|
|
71
|
-
elif v2_rt is not None:
|
|
72
|
-
compact_kwargs["retain_tail_tokens"] = v2_rt
|
|
73
|
-
if v2_th is not None:
|
|
74
|
-
compact_kwargs["threshold"] = v2_th
|
|
75
|
-
if llm_caller is not None and model:
|
|
76
|
-
try:
|
|
77
|
-
from .token_meter import token_meter
|
|
78
|
-
ctx = token_meter.last_context(session.run_id)
|
|
79
|
-
cap = token_meter.capacity(model)
|
|
80
|
-
if ctx > 0 and cap > 0 and ctx > cap * _PRECHECK_RATIO:
|
|
81
|
-
gen_p0 = session.replace_generation()
|
|
82
|
-
if maybe_compact(session, model=model, llm_caller=llm_caller,
|
|
83
|
-
run_id=session.run_id, **compact_kwargs) \
|
|
84
|
-
and session.replace_generation() > gen_p0:
|
|
85
|
-
log.info("precheck: last ctx~%d > %.0f%% of cap(%s)=%d, "
|
|
86
|
-
"compacted before step", ctx, _PRECHECK_RATIO * 100,
|
|
87
|
-
model, cap)
|
|
88
|
-
except Exception:
|
|
89
|
-
log.debug("precheck skipped", exc_info=True)
|
|
90
|
-
gen0 = session.replace_generation()
|
|
91
|
-
result = run_agent_fn(prompt, **kwargs)
|
|
92
|
-
code = result.get("error_code") or ""
|
|
93
|
-
if code not in _OVERFLOW_CODES or llm_caller is None:
|
|
94
|
-
return result, False
|
|
95
|
-
# 撑爆 → 尝试压缩
|
|
96
|
-
compacted = maybe_compact(session, model=model, llm_caller=llm_caller,
|
|
97
|
-
run_id=session.run_id, **compact_kwargs)
|
|
98
|
-
gen1 = session.replace_generation()
|
|
99
|
-
if not compacted or gen1 <= gen0:
|
|
100
|
-
# 压缩未发生或未前进 generation:重试必然再次撑爆,不重试
|
|
101
|
-
log.info("overflow but no compaction progress (gen %s->%s), no retry",
|
|
102
|
-
gen0, gen1)
|
|
103
|
-
return result, False
|
|
104
|
-
# 压缩生效:surface 已折叠。**真正替换 CLI 请求上下文**(2026-09-21
|
|
105
|
-
# 清最老待深挖项:此前重试仍发原 prompt + 续旧 resume 会话——摘要只进
|
|
106
|
-
# 审计日志,CLI 看到的上下文一点没小,重试必然再爆):
|
|
107
|
-
# ①重试 prompt 改用 session 派生转录(折叠摘要 + 近尾消息,体量骤降);
|
|
108
|
-
# ②弃 resume(旧会话本体仍是膨胀态,续上就前功尽弃)。
|
|
109
|
-
msgs = []
|
|
110
|
-
try:
|
|
111
|
-
msgs = session.derive_messages()
|
|
112
|
-
except Exception:
|
|
113
|
-
log.debug("derive_messages failed on retry", exc_info=True)
|
|
114
|
-
kwargs2 = dict(kwargs)
|
|
115
|
-
if msgs:
|
|
116
|
-
transcript = "\n\n".join("[%s] %s" % (m.get("role"), m.get("content"))
|
|
117
|
-
for m in msgs)
|
|
118
|
-
if len(transcript) > 24_000:
|
|
119
|
-
transcript = transcript[-24_000:]
|
|
120
|
-
retry_prompt = transcript
|
|
121
|
-
# 本步指令必须在场:被折叠/截断掉就显式补在尾部(缺失指令比重复严重)
|
|
122
|
-
tail = transcript[-2000:]
|
|
123
|
-
if prompt[:400] not in tail:
|
|
124
|
-
retry_prompt = transcript + "\n\n" + prompt
|
|
125
|
-
kwargs2.pop("resume", None)
|
|
126
|
-
log.info("compaction advanced gen %s->%s, retrying with derived "
|
|
127
|
-
"context (%d msgs, resume dropped)", gen0, gen1, len(msgs))
|
|
128
|
-
result2 = run_agent_fn(retry_prompt, **kwargs2)
|
|
129
|
-
else:
|
|
130
|
-
log.info("compaction advanced gen %s->%s but no surface derived, "
|
|
131
|
-
"retrying same prompt", gen0, gen1)
|
|
132
|
-
result2 = run_agent_fn(prompt, **kwargs)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Step 执行器:压缩触发的重试守门(复用已渲染 prompt,不重复组装)。
|
|
3
|
+
|
|
4
|
+
设计稿:docs/migration/02-context-compaction.md §1D。
|
|
5
|
+
参考 dsh packages/core/agent-loop/src/agent.ts:307-378:
|
|
6
|
+
失败重试只在「前进 surface generation」时返回 {kind:'retry'},
|
|
7
|
+
复用已渲染 PromptAssembly,不重跑 pre-step / 组装 / 用户消息准入。
|
|
8
|
+
|
|
9
|
+
Tutti 语境:runner.run_agent 自带 prompt(已渲染);重试的风险不是重复渲染
|
|
10
|
+
(prompt 是同一个字符串),而是「不压缩直接重试」必然再次撑爆。
|
|
11
|
+
本模块的守门:仅当 maybe_compact 真正前进了 surface generation 才重试一次。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from .compaction import maybe_compact, DEFAULT_PRESSURE_THRESHOLD
|
|
18
|
+
from .error_codes import ErrorCode
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# 触发「压缩后重试」的错误码
|
|
23
|
+
_OVERFLOW_CODES = {ErrorCode.CONTEXT_OVERFLOW, ErrorCode.MAX_TOKENS}
|
|
24
|
+
|
|
25
|
+
# 事前预检阈值:最近一次调用的上下文锚点已占本步模型容量九成 → 第一个请求
|
|
26
|
+
# 大概率被供应商拒。只拦「大概率必死」,不提前压缩平时的高压(used() 是累计
|
|
27
|
+
# 口径,比真实上下文偏大;0.8 的响应式阈值语义与此不同,勿混用)。
|
|
28
|
+
_PRECHECK_RATIO = 0.9
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _v2_compaction_tuning():
|
|
32
|
+
"""settings_v2 orchestrator.compaction 微调 → (threshold|None, retain|None)。
|
|
33
|
+
|
|
34
|
+
None = 未配置(走 maybe_compact 的模块默认);读取/注册失败静默回落。
|
|
35
|
+
retain 允许 0(尾部不保留),与 None(未配置)语义不同,勿合并。"""
|
|
36
|
+
try:
|
|
37
|
+
from .settings_schema import get as ss_get, register_default_namespaces
|
|
38
|
+
register_default_namespaces()
|
|
39
|
+
c = (ss_get("orchestrator") or {}).get("compaction") or {}
|
|
40
|
+
th = c.get("pressure_threshold")
|
|
41
|
+
rt = c.get("retain_tail_tokens")
|
|
42
|
+
return (float(th) if th else None,
|
|
43
|
+
int(rt) if rt is not None else None)
|
|
44
|
+
except Exception:
|
|
45
|
+
return None, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def execute_step(session, run_agent_fn, prompt, *, model: str = "",
|
|
49
|
+
llm_caller=None, retain_tail_tokens=None, **kwargs):
|
|
50
|
+
"""执行一次 step;撑爆时压缩并守门重试一次。
|
|
51
|
+
|
|
52
|
+
事前预检(借鉴 freebuff base-chat 每步容量重估):换将切到小窗口模型后,
|
|
53
|
+
响应式路径要等供应商报 CONTEXT_OVERFLOW 才压缩——报得不干净就直接卡死。
|
|
54
|
+
先用「最近一次调用上下文 vs 本步模型容量」判一次,超阈先压缩再发。
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
session: session_log.Session(记录 replace_generation)
|
|
58
|
+
run_agent_fn: runner.run_agent(或测试替身),签名 (prompt, **kwargs) -> dict
|
|
59
|
+
prompt: 已渲染的提示词(重试时原样复用,不重新渲染)
|
|
60
|
+
model: 本步将要使用的模型名(容量与压力估算都按它算)
|
|
61
|
+
llm_caller: 压缩摘要 LLM(None = 不压缩,永不重试)
|
|
62
|
+
**kwargs: 透传 run_agent_fn
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
(result, retried: bool)
|
|
66
|
+
"""
|
|
67
|
+
v2_th, v2_rt = _v2_compaction_tuning()
|
|
68
|
+
compact_kwargs = {}
|
|
69
|
+
if retain_tail_tokens is not None:
|
|
70
|
+
compact_kwargs["retain_tail_tokens"] = retain_tail_tokens
|
|
71
|
+
elif v2_rt is not None:
|
|
72
|
+
compact_kwargs["retain_tail_tokens"] = v2_rt
|
|
73
|
+
if v2_th is not None:
|
|
74
|
+
compact_kwargs["threshold"] = v2_th
|
|
75
|
+
if llm_caller is not None and model:
|
|
76
|
+
try:
|
|
77
|
+
from .token_meter import token_meter
|
|
78
|
+
ctx = token_meter.last_context(session.run_id)
|
|
79
|
+
cap = token_meter.capacity(model)
|
|
80
|
+
if ctx > 0 and cap > 0 and ctx > cap * _PRECHECK_RATIO:
|
|
81
|
+
gen_p0 = session.replace_generation()
|
|
82
|
+
if maybe_compact(session, model=model, llm_caller=llm_caller,
|
|
83
|
+
run_id=session.run_id, **compact_kwargs) \
|
|
84
|
+
and session.replace_generation() > gen_p0:
|
|
85
|
+
log.info("precheck: last ctx~%d > %.0f%% of cap(%s)=%d, "
|
|
86
|
+
"compacted before step", ctx, _PRECHECK_RATIO * 100,
|
|
87
|
+
model, cap)
|
|
88
|
+
except Exception:
|
|
89
|
+
log.debug("precheck skipped", exc_info=True)
|
|
90
|
+
gen0 = session.replace_generation()
|
|
91
|
+
result = run_agent_fn(prompt, **kwargs)
|
|
92
|
+
code = result.get("error_code") or ""
|
|
93
|
+
if code not in _OVERFLOW_CODES or llm_caller is None:
|
|
94
|
+
return result, False
|
|
95
|
+
# 撑爆 → 尝试压缩
|
|
96
|
+
compacted = maybe_compact(session, model=model, llm_caller=llm_caller,
|
|
97
|
+
run_id=session.run_id, **compact_kwargs)
|
|
98
|
+
gen1 = session.replace_generation()
|
|
99
|
+
if not compacted or gen1 <= gen0:
|
|
100
|
+
# 压缩未发生或未前进 generation:重试必然再次撑爆,不重试
|
|
101
|
+
log.info("overflow but no compaction progress (gen %s->%s), no retry",
|
|
102
|
+
gen0, gen1)
|
|
103
|
+
return result, False
|
|
104
|
+
# 压缩生效:surface 已折叠。**真正替换 CLI 请求上下文**(2026-09-21
|
|
105
|
+
# 清最老待深挖项:此前重试仍发原 prompt + 续旧 resume 会话——摘要只进
|
|
106
|
+
# 审计日志,CLI 看到的上下文一点没小,重试必然再爆):
|
|
107
|
+
# ①重试 prompt 改用 session 派生转录(折叠摘要 + 近尾消息,体量骤降);
|
|
108
|
+
# ②弃 resume(旧会话本体仍是膨胀态,续上就前功尽弃)。
|
|
109
|
+
msgs = []
|
|
110
|
+
try:
|
|
111
|
+
msgs = session.derive_messages()
|
|
112
|
+
except Exception:
|
|
113
|
+
log.debug("derive_messages failed on retry", exc_info=True)
|
|
114
|
+
kwargs2 = dict(kwargs)
|
|
115
|
+
if msgs:
|
|
116
|
+
transcript = "\n\n".join("[%s] %s" % (m.get("role"), m.get("content"))
|
|
117
|
+
for m in msgs)
|
|
118
|
+
if len(transcript) > 24_000:
|
|
119
|
+
transcript = transcript[-24_000:]
|
|
120
|
+
retry_prompt = transcript
|
|
121
|
+
# 本步指令必须在场:被折叠/截断掉就显式补在尾部(缺失指令比重复严重)
|
|
122
|
+
tail = transcript[-2000:]
|
|
123
|
+
if prompt[:400] not in tail:
|
|
124
|
+
retry_prompt = transcript + "\n\n" + prompt
|
|
125
|
+
kwargs2.pop("resume", None)
|
|
126
|
+
log.info("compaction advanced gen %s->%s, retrying with derived "
|
|
127
|
+
"context (%d msgs, resume dropped)", gen0, gen1, len(msgs))
|
|
128
|
+
result2 = run_agent_fn(retry_prompt, **kwargs2)
|
|
129
|
+
else:
|
|
130
|
+
log.info("compaction advanced gen %s->%s but no surface derived, "
|
|
131
|
+
"retrying same prompt", gen0, gen1)
|
|
132
|
+
result2 = run_agent_fn(prompt, **kwargs)
|
|
133
133
|
return result2, True
|
package/app/core/store.py
CHANGED
|
@@ -160,6 +160,19 @@ def create_task(payload):
|
|
|
160
160
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
161
161
|
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
162
|
task["timeout_s"] = _task_timeout_s(payload.get("timeout_s"))
|
|
163
|
+
# 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
|
|
164
|
+
# 固化参数已保证「改流程不影响本任务」,digest 再补上可对账的一面——
|
|
165
|
+
# 续跑/重试时若流程已改,flows.flow_drift 能给出明确诊断而非静默换新。
|
|
166
|
+
flow_content_sha = flows_mod.flow_content_sha256(flow)
|
|
167
|
+
task["flow_snapshot"] = {"id": flow["id"], "name": (flow.get("name") or "")[:40],
|
|
168
|
+
"engine": flow["engine"],
|
|
169
|
+
"edited": bool(flow.get("edited")),
|
|
170
|
+
"digest": flow_content_sha[:16],
|
|
171
|
+
"revision_id": "flowrev-" + flow_content_sha[:16],
|
|
172
|
+
"content_sha256": flow_content_sha}
|
|
173
|
+
# 参数修订号(借鉴 WorkDSH 契约纪律 expectedRevision):写接口据此做过期
|
|
174
|
+
# 写入检测。老任务无此键按 1 处理;不带 expected_rev 的调用不受影响。
|
|
175
|
+
task["rev"] = 1
|
|
163
176
|
if flow["engine"] == "direct":
|
|
164
177
|
task["direct_provider_id"] = _text(payload.get("direct_provider_id"),
|
|
165
178
|
"direct_provider_id")[:80]
|
|
@@ -312,8 +325,20 @@ def create_task(payload):
|
|
|
312
325
|
att_ids = payload.get("attachments")
|
|
313
326
|
if isinstance(att_ids, list) and att_ids:
|
|
314
327
|
from . import attachments as att_mod
|
|
315
|
-
|
|
316
|
-
if
|
|
328
|
+
dict_items = [a for a in att_ids if isinstance(a, dict) and a.get("path")]
|
|
329
|
+
if dict_items:
|
|
330
|
+
root = Path(str(wd)).resolve()
|
|
331
|
+
items = []
|
|
332
|
+
for raw in dict_items[:att_mod.MAX_FILES]:
|
|
333
|
+
rel = att_mod.norm_rel(raw)
|
|
334
|
+
fp = (root / rel).resolve() if rel else root / "__invalid__"
|
|
335
|
+
if rel and root in fp.parents and fp.is_file():
|
|
336
|
+
item = dict(raw)
|
|
337
|
+
item["path"] = rel
|
|
338
|
+
items.append(item)
|
|
339
|
+
if not items:
|
|
340
|
+
raise ValueError("附件路径必须位于工作目录的 _attachments/ 内")
|
|
341
|
+
else: # 纯 id 形态 → 从待提交区移入工作目录
|
|
317
342
|
try:
|
|
318
343
|
items = att_mod.commit_to_workdir(
|
|
319
344
|
str(wd), [str(x) for x in att_ids][:att_mod.MAX_FILES])
|
|
@@ -321,6 +346,18 @@ def create_task(payload):
|
|
|
321
346
|
raise ValueError("附件落盘失败: %s" % e)
|
|
322
347
|
if items:
|
|
323
348
|
task["attachments"] = items
|
|
349
|
+
task["asset_refs"] = att_mod.asset_refs(str(wd), items)
|
|
350
|
+
try:
|
|
351
|
+
from . import assets
|
|
352
|
+
for ref in task["asset_refs"]:
|
|
353
|
+
saved = assets.register(str(wd), ref.get("path"),
|
|
354
|
+
owner_type="task", owner_id=task["id"],
|
|
355
|
+
name=ref.get("name"), mime=ref.get("mime"))
|
|
356
|
+
if saved:
|
|
357
|
+
ref.update({k: saved[k] for k in ("asset_id", "revision_id",
|
|
358
|
+
"content_sha256")})
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
324
361
|
task["context"] = att_mod.merge_context(
|
|
325
362
|
task["context"], items, workdir=str(wd))
|
|
326
363
|
with LOCK:
|
|
@@ -554,6 +591,7 @@ def load_all():
|
|
|
554
591
|
# ---------------------------------------------------------------- 运行(含管理操作)
|
|
555
592
|
|
|
556
593
|
_RUN_ESTIMATOR = None
|
|
594
|
+
_RUN_AUDITOR = None
|
|
557
595
|
|
|
558
596
|
|
|
559
597
|
def set_run_estimator(estimator):
|
|
@@ -562,6 +600,110 @@ def set_run_estimator(estimator):
|
|
|
562
600
|
_RUN_ESTIMATOR = estimator if callable(estimator) else None
|
|
563
601
|
|
|
564
602
|
|
|
603
|
+
def set_run_auditor(auditor):
|
|
604
|
+
"""注入 run 终态对账器(usage.audit_run),同样不让存储层依赖用量模块。"""
|
|
605
|
+
global _RUN_AUDITOR
|
|
606
|
+
_RUN_AUDITOR = auditor if callable(auditor) else None
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _execution_snapshot(task):
|
|
610
|
+
"""Capture the redacted execution inputs at run creation time.
|
|
611
|
+
|
|
612
|
+
This is intentionally a snapshot of identities and revisions, never API
|
|
613
|
+
keys or task bodies. Later binding/skill/knowledge changes therefore do
|
|
614
|
+
not rewrite the explanation of an existing run.
|
|
615
|
+
"""
|
|
616
|
+
task = task or {}
|
|
617
|
+
flow_snapshot = dict(task.get("flow_snapshot") or {})
|
|
618
|
+
flow_digest = str(flow_snapshot.get("digest") or "")
|
|
619
|
+
flow_content_sha = str(flow_snapshot.get("content_sha256") or "")
|
|
620
|
+
flow_revision_id = str(flow_snapshot.get("revision_id") or "")
|
|
621
|
+
if not flow_content_sha and len(flow_digest) == 64:
|
|
622
|
+
flow_content_sha = flow_digest
|
|
623
|
+
if not flow_revision_id and flow_digest:
|
|
624
|
+
flow_revision_id = "flowrev-" + flow_digest[:16]
|
|
625
|
+
|
|
626
|
+
def _redact_command(command):
|
|
627
|
+
value = str(command or "")[:300]
|
|
628
|
+
if not value:
|
|
629
|
+
return ""
|
|
630
|
+
pattern = re.compile(
|
|
631
|
+
r"(?i)(api[_-]?key|access[_-]?token|auth(?:orization)?|password|passwd|secret|token)"
|
|
632
|
+
r"(\s*[:=]\s*|\s+)([^\s]+)")
|
|
633
|
+
return pattern.sub(lambda m: m.group(1) + m.group(2) + "<redacted>", value)
|
|
634
|
+
|
|
635
|
+
snapshot = {
|
|
636
|
+
"captured_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
637
|
+
"task_id": task.get("id") or "",
|
|
638
|
+
"flow_revision": flow_digest,
|
|
639
|
+
"flow_revision_id": flow_revision_id,
|
|
640
|
+
"flow_content_sha256": flow_content_sha,
|
|
641
|
+
"flow_snapshot": flow_snapshot,
|
|
642
|
+
"workdir": str(task.get("workdir") or "")[:500],
|
|
643
|
+
"git_rev": str(task.get("git_rev") or "")[:160],
|
|
644
|
+
"verify_command": _redact_command(task.get("verify_command") or ""),
|
|
645
|
+
"quality_gate": {
|
|
646
|
+
"threshold": task.get("threshold"),
|
|
647
|
+
"rubric": list(task.get("rubric") or [])[:8],
|
|
648
|
+
"rounds": task.get("rounds"),
|
|
649
|
+
"best_of": task.get("best_of"),
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
try:
|
|
653
|
+
from . import knowledge, modelhub, skills
|
|
654
|
+
snapshot["skills"] = skills.revision_for_task(task)
|
|
655
|
+
snapshot["knowledge"] = knowledge.revision_for_task(task)
|
|
656
|
+
providers = {str(p.get("id") or ""): p for p in modelhub.providers()}
|
|
657
|
+
bindings = {}
|
|
658
|
+
for agent_id, binding in (modelhub.bindings() or {}).items():
|
|
659
|
+
chain = []
|
|
660
|
+
for item in (binding or {}).get("chain") or []:
|
|
661
|
+
pid = str(item.get("provider_id") or "")
|
|
662
|
+
prov = providers.get(pid) or {}
|
|
663
|
+
try:
|
|
664
|
+
key_ids = [str(k.get("id") or "") for k in modelhub._provider_keys(prov)
|
|
665
|
+
if k.get("id")]
|
|
666
|
+
except Exception:
|
|
667
|
+
key_ids = []
|
|
668
|
+
entry = {
|
|
669
|
+
"provider_id": pid,
|
|
670
|
+
"model": str(item.get("model") or "")[:160],
|
|
671
|
+
"key_ids": key_ids[:8],
|
|
672
|
+
}
|
|
673
|
+
if pid:
|
|
674
|
+
entry.update(modelhub.connection_identity(pid, key_ids[0] if key_ids else ""))
|
|
675
|
+
chain.append(entry)
|
|
676
|
+
bindings[str(agent_id)[:80]] = {
|
|
677
|
+
"difficulty_routing": bool((binding or {}).get("difficulty_routing")),
|
|
678
|
+
"chain": chain,
|
|
679
|
+
}
|
|
680
|
+
snapshot["agent_bindings"] = bindings
|
|
681
|
+
if task.get("direct_provider_id") or task.get("direct_model"):
|
|
682
|
+
direct = {
|
|
683
|
+
"provider_id": str(task.get("direct_provider_id") or "")[:80],
|
|
684
|
+
"model": str(task.get("direct_model") or "")[:160],
|
|
685
|
+
"agent": str(task.get("direct_agent") or "")[:80],
|
|
686
|
+
}
|
|
687
|
+
direct_pid = str(task.get("direct_provider_id") or "")
|
|
688
|
+
direct_prov = providers.get(direct_pid) or {}
|
|
689
|
+
direct_keys = modelhub._provider_keys(direct_prov)
|
|
690
|
+
if direct_pid:
|
|
691
|
+
direct.update(modelhub.connection_identity(
|
|
692
|
+
direct_pid, str((direct_keys[0] if direct_keys else {}).get("id") or "")))
|
|
693
|
+
snapshot["direct"] = direct
|
|
694
|
+
except Exception:
|
|
695
|
+
# A snapshot is diagnostic metadata. A broken optional catalog must
|
|
696
|
+
# never prevent the task from being created.
|
|
697
|
+
snapshot.setdefault("skills", {})
|
|
698
|
+
snapshot.setdefault("knowledge", {})
|
|
699
|
+
snapshot.setdefault("agent_bindings", {})
|
|
700
|
+
return snapshot
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# run 终态集合:与 CAS 收口、状态回填共用同一份定义
|
|
704
|
+
TERMINAL_STATUSES = ("done", "failed", "cancelled", "timeout")
|
|
705
|
+
|
|
706
|
+
|
|
565
707
|
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
566
708
|
run = {
|
|
567
709
|
"id": _new_id("r" if kind == "orchestration" else "m"),
|
|
@@ -578,6 +720,7 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
|
578
720
|
if kind == "orchestration" and task_id:
|
|
579
721
|
task = get_task(task_id) or {}
|
|
580
722
|
run["deadline_at"] = time.time() + _task_timeout_s(task.get("timeout_s"))
|
|
723
|
+
run["execution_snapshot"] = _execution_snapshot(task)
|
|
581
724
|
try:
|
|
582
725
|
eta = _RUN_ESTIMATOR(
|
|
583
726
|
task_type=task.get("type") or "", mode=task.get("mode") or "auto",
|
|
@@ -776,12 +919,14 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
776
919
|
当前状态不等于 expected_status 则拒绝写入并返回 None,
|
|
777
920
|
防止陈旧执行方(被取消的 worker、崩溃恢复前的旧线程)覆盖新状态
|
|
778
921
|
——防御模式「异步状态不是同步状态」。不传则保持原行为。"""
|
|
922
|
+
audit = None
|
|
779
923
|
with LOCK:
|
|
780
924
|
run = _RUNS.get(run_id)
|
|
781
925
|
if not run:
|
|
782
926
|
return None
|
|
783
927
|
if expected_status is not None and run.get("status") != expected_status:
|
|
784
928
|
return None
|
|
929
|
+
prev_status = str(run.get("status") or "")
|
|
785
930
|
for k in ("error", "summary"):
|
|
786
931
|
if isinstance(fields.get(k), str):
|
|
787
932
|
# 错误串多是我们自己拼的 CLI 尾巴(含 ANSI/覆写/乱码墙):
|
|
@@ -803,7 +948,6 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
803
948
|
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
804
949
|
if st == "timeout" else
|
|
805
950
|
"步骤未正常收尾(终态自动恢复)")
|
|
806
|
-
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
807
951
|
# 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
|
|
808
952
|
# 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
|
|
809
953
|
# 写了一小时,任务卡 queued,用户以为卡死连点重试)。
|
|
@@ -813,6 +957,21 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
813
957
|
if task:
|
|
814
958
|
task["status"] = st
|
|
815
959
|
_save_json(paths.TASKS_DIR / (tid + ".json"), task)
|
|
960
|
+
if st in TERMINAL_STATUSES:
|
|
961
|
+
try:
|
|
962
|
+
from . import acceptance
|
|
963
|
+
run["acceptance"] = acceptance.evaluate_run(
|
|
964
|
+
run, (_TASKS.get(run.get("task_id")) if run.get("task_id") else None) or {})
|
|
965
|
+
except Exception:
|
|
966
|
+
# Acceptance reporting is evidence enrichment; it must not
|
|
967
|
+
# prevent the primary run from reaching a terminal state.
|
|
968
|
+
pass
|
|
969
|
+
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
970
|
+
if st in TERMINAL_STATUSES and prev_status not in TERMINAL_STATUSES:
|
|
971
|
+
# 事后对账:只有「非终态 → 终态」这一次翻转才记账,重复写终态
|
|
972
|
+
# (UI 补写、迟到写手)不得二次入账。快照在锁内浅拷,锁外调用
|
|
973
|
+
# 审计器——审计器会回读 store,锁内调用即自锁。
|
|
974
|
+
audit = dict(run)
|
|
816
975
|
if st in ("done", "failed", "cancelled", "timeout"):
|
|
817
976
|
# run_end 钩子(2026-09-22):终态即触发,后台跑副作用型脚本
|
|
818
977
|
# (回写知识库等);失败/超时静默,绝不拖慢收尾路径
|
|
@@ -831,6 +990,11 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
831
990
|
# 「在跑」直到下一次无关 bump(2026-09-22 侧栏假在跑案)。
|
|
832
991
|
# 调用都是步骤级边界,不会形成推送风暴;CAS 拒绝/无此 run 的早退
|
|
833
992
|
# 路径不动版本号。
|
|
993
|
+
if audit is not None and _RUN_AUDITOR is not None:
|
|
994
|
+
try:
|
|
995
|
+
_RUN_AUDITOR(audit)
|
|
996
|
+
except Exception:
|
|
997
|
+
pass # 对账永远不能拖垮终态收尾
|
|
834
998
|
bump_state()
|
|
835
999
|
return run
|
|
836
1000
|
|
|
@@ -1015,6 +1179,29 @@ def run_artifacts(run_id, limit=200, cache=False):
|
|
|
1015
1179
|
running=running, cache=cache)
|
|
1016
1180
|
|
|
1017
1181
|
|
|
1182
|
+
def run_asset_refs(run_id, limit=200):
|
|
1183
|
+
"""Return content-addressed references for this run's output files."""
|
|
1184
|
+
wd, files = run_artifacts(run_id, limit=limit, cache=False)
|
|
1185
|
+
if not wd:
|
|
1186
|
+
return []
|
|
1187
|
+
from . import attachments
|
|
1188
|
+
refs = attachments.asset_refs(wd, [{"path": f.get("name") or f.get("path") or ""}
|
|
1189
|
+
for f in files if isinstance(f, dict)])
|
|
1190
|
+
try:
|
|
1191
|
+
from . import assets
|
|
1192
|
+
run = get_run(run_id) or {}
|
|
1193
|
+
for ref in refs:
|
|
1194
|
+
saved = assets.register(wd, ref.get("path"), owner_type="run",
|
|
1195
|
+
owner_id=run_id, name=ref.get("name"),
|
|
1196
|
+
mime=ref.get("mime"))
|
|
1197
|
+
if saved:
|
|
1198
|
+
ref.update({k: saved[k] for k in ("asset_id", "revision_id",
|
|
1199
|
+
"content_sha256")})
|
|
1200
|
+
except Exception:
|
|
1201
|
+
pass
|
|
1202
|
+
return refs
|
|
1203
|
+
|
|
1204
|
+
|
|
1018
1205
|
def task_step_count(task_id):
|
|
1019
1206
|
"""该任务所有 run 的步骤总数(含进行中)。
|
|
1020
1207
|
|
|
@@ -1506,30 +1693,47 @@ def rename_task(task_id, title):
|
|
|
1506
1693
|
|
|
1507
1694
|
|
|
1508
1695
|
def update_task_params(task_id, patch):
|
|
1509
|
-
"""更新任务编排参数(对话条三件套:mode/thinking/direct_provider_id/direct_model
|
|
1510
|
-
|
|
1696
|
+
"""更新任务编排参数(对话条三件套:mode/thinking/direct_provider_id/direct_model,
|
|
1697
|
+
另有 timeout_s 任务总时限)。
|
|
1698
|
+
只在任务未在跑时生效——运行中改参数不会影响当前执行,静默跳过避免误导。
|
|
1699
|
+
|
|
1700
|
+
修订 CAS(借鉴 WorkDSH 契约纪律 expectedRevision):patch 携带整数
|
|
1701
|
+
expected_rev 时与任务当前 rev 比对,不符=别处已先改——拒绝写入并说明,
|
|
1702
|
+
不做过期覆盖;不带 expected_rev 的旧调用保持原行为。写成功 rev+1,
|
|
1703
|
+
调用方(路由)从 get_task 回读新 rev 交前端续用。"""
|
|
1511
1704
|
if not _valid_id(task_id):
|
|
1512
1705
|
return False, "非法的任务 ID"
|
|
1706
|
+
patch = dict(patch) if isinstance(patch, dict) else {}
|
|
1707
|
+
expected_rev = patch.pop("expected_rev", None)
|
|
1513
1708
|
with LOCK:
|
|
1514
1709
|
task = _TASKS.get(task_id)
|
|
1515
1710
|
if not task:
|
|
1516
1711
|
return False, "任务不存在"
|
|
1517
1712
|
if task.get("status") in ("queued", "running"):
|
|
1518
1713
|
return False, "任务运行中,参数不可改"
|
|
1714
|
+
if expected_rev is not None:
|
|
1715
|
+
try:
|
|
1716
|
+
want_rev = int(expected_rev)
|
|
1717
|
+
cur_rev = int(task.get("rev") or 1)
|
|
1718
|
+
except (TypeError, ValueError):
|
|
1719
|
+
return False, "expected_rev 必须是整数"
|
|
1720
|
+
if want_rev != cur_rev:
|
|
1721
|
+
return False, ("任务参数已被其他窗口修改(当前修订 %d),"
|
|
1722
|
+
"已拒绝过期写入,界面稍后自动同步最新值" % cur_rev)
|
|
1519
1723
|
changed = False
|
|
1520
|
-
mode = str(
|
|
1724
|
+
mode = str(patch.get("mode") or "").strip()
|
|
1521
1725
|
if mode in ("auto", "fast", "expert", "manual"):
|
|
1522
1726
|
if task.get("mode") != mode:
|
|
1523
1727
|
task["mode"] = mode
|
|
1524
1728
|
changed = True
|
|
1525
|
-
thinking = str(
|
|
1729
|
+
thinking = str(patch.get("thinking") or "").strip()
|
|
1526
1730
|
if thinking in ("auto", "low", "standard", "high"):
|
|
1527
1731
|
if task.get("thinking") != thinking:
|
|
1528
1732
|
task["thinking"] = thinking
|
|
1529
1733
|
changed = True
|
|
1530
1734
|
if task.get("engine") == "direct":
|
|
1531
|
-
pv = str(
|
|
1532
|
-
mv = str(
|
|
1735
|
+
pv = str(patch.get("direct_provider_id") or "").strip()[:80]
|
|
1736
|
+
mv = str(patch.get("direct_model") or "").strip()[:160]
|
|
1533
1737
|
if mv and not pv:
|
|
1534
1738
|
return False, "指定对话模型时必须同时指定厂商"
|
|
1535
1739
|
if task.get("direct_provider_id") != pv or task.get("direct_model") != mv:
|
|
@@ -1538,17 +1742,28 @@ def update_task_params(task_id, patch):
|
|
|
1538
1742
|
changed = True
|
|
1539
1743
|
# 执行 CLI 可改/可清(清空 = 回内置智能体或模型绑定);与模型绑定
|
|
1540
1744
|
# 允许共存,运行时 CLI 优先
|
|
1541
|
-
av = str(
|
|
1745
|
+
av = str(patch.get("direct_agent") or "").strip()[:80]
|
|
1542
1746
|
if task.get("direct_agent") != av and (av or task.get("direct_agent")):
|
|
1543
1747
|
task["direct_agent"] = av
|
|
1544
1748
|
changed = True
|
|
1749
|
+
# 任务总时限(秒):建任务时写入,此后靠这里改——下次重试/续跑的
|
|
1750
|
+
# create_run 会按它重新给满预算(2026-09-24 戍边骑奴案:12 章连载
|
|
1751
|
+
# 默认 1 小时在慢链天跑不完,只能人工反复重试)。走 _task_timeout_s
|
|
1752
|
+
# 统一钳制 [1s, 7天],脏值回落默认;空闲时才可改(与三件套同闸)。
|
|
1753
|
+
raw_ts = patch.get("timeout_s")
|
|
1754
|
+
if raw_ts not in (None, ""):
|
|
1755
|
+
new_ts = _task_timeout_s(raw_ts)
|
|
1756
|
+
if task.get("timeout_s") != new_ts:
|
|
1757
|
+
task["timeout_s"] = new_ts
|
|
1758
|
+
changed = True
|
|
1545
1759
|
if changed:
|
|
1760
|
+
task["rev"] = int(task.get("rev") or 1) + 1
|
|
1546
1761
|
_save_json(paths.TASKS_DIR / (task_id + ".json"), task)
|
|
1547
1762
|
bump_state()
|
|
1548
1763
|
return True, ""
|
|
1549
1764
|
|
|
1550
1765
|
|
|
1551
|
-
def add_step(run_id, role, agent_id, agent_label, note="", model=""):
|
|
1766
|
+
def add_step(run_id, role, agent_id, agent_label, note="", model="", provider=""):
|
|
1552
1767
|
with LOCK:
|
|
1553
1768
|
run = _RUNS.get(run_id)
|
|
1554
1769
|
if not run:
|
|
@@ -1560,6 +1775,10 @@ def add_step(run_id, role, agent_id, agent_label, note="", model=""):
|
|
|
1560
1775
|
# 起跑即记解析到的模型:运行中蜂巢卡/步骤列表就能看到「在用哪个模型」,
|
|
1561
1776
|
# 不必等 finish_step 用实际结果覆盖(换将成功后以实际为准)
|
|
1562
1777
|
"model": str(model or "")[:80],
|
|
1778
|
+
# 绑定速记(借鉴 WorkDSH ResolvedExecutionBinding):本步实际用的
|
|
1779
|
+
# provider 链与上游 host——烧错配额/模型漂移直接对账步骤行。
|
|
1780
|
+
# 只记身份不记密钥;老数据无此键,读取一律 .get
|
|
1781
|
+
"provider": str(provider or "")[:120],
|
|
1563
1782
|
"status": "running", "started_at": time.strftime("%H:%M:%S"),
|
|
1564
1783
|
"ended_at": None, "duration_s": None, "exit_code": None,
|
|
1565
1784
|
"summary": "", "log": None, "cost_usd": 0.0, "tokens": 0,
|