codebee 0.1.10 → 0.1.11
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 +14 -0
- package/README.md +8 -6
- package/app/core/aiflavor.py +51 -0
- package/app/core/automation.py +9 -0
- package/app/core/gitmod.py +4 -0
- package/app/core/jobs.py +115 -13
- package/app/core/manager.py +49 -0
- package/app/core/modelhub.py +10 -1
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +282 -1
- package/app/core/publish/__init__.py +7 -0
- package/app/core/publish/auto.py +367 -0
- package/app/core/publish/browser.py +410 -0
- package/app/core/publish/fanqie.py +98 -0
- package/app/core/publish/flow.py +281 -0
- package/app/core/publish/ledger.py +198 -0
- package/app/core/publish/manager.py +427 -0
- package/app/core/publish/qimao.py +97 -0
- package/app/core/publish/ws.py +139 -0
- package/app/core/settings.py +18 -3
- package/app/core/store.py +24 -2
- package/app/main.py +174 -0
- package/app/ui/app.js +325 -26
- package/app/ui/i18n.js +4 -1
- package/app/ui/index.html +5 -3
- package/app/ui/style.css +64 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
+
## v0.1.11(2026-09-18)
|
|
8
|
+
|
|
9
|
+
- **连载发布闭环(主轨)**:番茄/七猫作家后台 CDP 直操通道(连接扫码/建书/发章/表单探测/截图存证)+ 详情页出版台面板;发布台账幂等(章号去重)+ 三道护栏(每日上限/连续失败暂停/人工确认闸——默认填好表单停,提交权留给用户)
|
|
10
|
+
- **定时自动发布(P2.5)**:任务级 task.auto_publish 到点触发批量发布;直发模式需 flows 校准文件解锁;批量发布按章号顺序、失败即停
|
|
11
|
+
- 代码任务 Best-of-N:单路失败后 worktree 隔离赛马兜底,验证通过+改动最小者胜(借鉴 orca)
|
|
12
|
+
- 需求拷问采访态:goal 模糊时先问 1-3 个澄清问题再开跑(借鉴 grill-me)
|
|
13
|
+
- 任务队列双病根根治:job 蒸发看门狗自愈 + 排队回填;同因连撞不再重复退避付费;失败退避等待在运行行/运行详情/执行结果/运行记录处显示「将于 HH:MM 自动续跑」,不再笼统显示排队中
|
|
14
|
+
- 运行前配置同步防线:每次真实派发自动把绑定链首落进 CLI 自家配置——绑定换供应商后不再撞上旧 CLI 配置空跑(opencode 钉死旧端点案)
|
|
15
|
+
- 评审新增「AI 味」确定性检测:译制腔/套话密度统计随评审下发参考线(借鉴 oh-story,命中才提示、评审官结合上下文判断)
|
|
16
|
+
- 任务规格文件化:任何任务在工作目录留 .codebee/spec.md(目标/参数/验证命令),随任务分支版本化、复盘续跑可见
|
|
17
|
+
- Git 变更视图豁免 .codebee/ 元数据目录,面板只看任务产出
|
|
18
|
+
- 报告区失败/空报告/无报告分态可见提示,不再静默留白
|
|
19
|
+
- 新增类型菜单成本预估行、步骤卡「实际派发模型」徽章、建 PR 自定义标题(承接 v0.1.10 未列项)
|
|
20
|
+
|
|
7
21
|
## v0.1.10(2026-09-18)
|
|
8
22
|
|
|
9
23
|
- Git 工作台「建 PR」支持自定义标题:提交信息框有内容时自动兼作 PR 标题(一处输入两用)
|
package/README.md
CHANGED
|
@@ -21,12 +21,14 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
24
|
+
### 最新版更新内容(v0.1.11)
|
|
25
|
+
|
|
26
|
+
- **连载发布闭环**:番茄/七猫作家后台一键发布——连接扫码后自动建书/发章(默认填好表单人工确认提交),出版台面板+发布台账+每日上限/连续失败暂停护栏
|
|
27
|
+
- **定时自动发布**:任务到点自动批量发章,直发模式需流程校准文件解锁
|
|
28
|
+
- 代码任务 Best-of-N 赛马兜底;goal 模糊时先问 1-3 个澄清问题再开跑
|
|
29
|
+
- 任务队列自愈(看门狗+排队回填);同因连撞不再重复退避付费,退避期明示「将于 HH:MM 自动续跑」
|
|
30
|
+
- 运行前自动同步 CLI 配置:绑定换供应商后不再撞上旧配置空跑
|
|
31
|
+
- 评审新增「AI 味」参考线;任务留 .codebee/spec.md 规格档案;报告区空态可见
|
|
30
32
|
<!-- relnotes:end -->
|
|
31
33
|
|
|
32
34
|
---
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""AI 味确定性检测(借鉴 oh-story-claudecode 的去AI味思路)。
|
|
3
|
+
|
|
4
|
+
模型评审是主观分,这里补一道客观参考线:统计译制腔/套话高频词的
|
|
5
|
+
密度,产出报告行注入评审提示词,让评审官把「套话密度」纳入评分
|
|
6
|
+
参考。只做参考不做门禁——「仿佛」「缓缓」在正常小说里也常见,
|
|
7
|
+
误伤风险由评审官(能读懂上下文)兜底,脚本只负责指认位置与密度。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# 译制腔/套话词表(按误导性排序,持续运维)
|
|
12
|
+
AI_PHRASES = (
|
|
13
|
+
"空气仿佛凝固", "眸中闪过一丝", "嘴角勾起一抹", "心中一动",
|
|
14
|
+
"值得注意的是", "总而言之", "综上所述", "不难发现",
|
|
15
|
+
"在这一刻", "仿佛在诉说着", "无声地诉说着", "见证着",
|
|
16
|
+
"深深地看了一眼", "似乎在", "空气仿佛", "一抹微笑",
|
|
17
|
+
"不禁", "仿佛",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# 密度告警线(每千字命中次数):超过即提示评审官重点关注
|
|
21
|
+
ALERT_PER_KILO = 8.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def analyze(text):
|
|
25
|
+
"""统计套话命中。返回 {hits: {短语: 次数}, per_kilo: 每千字密度, alert: bool}。"""
|
|
26
|
+
text = text or ""
|
|
27
|
+
total = len(text)
|
|
28
|
+
hits = {}
|
|
29
|
+
for p in AI_PHRASES:
|
|
30
|
+
p = p.strip()
|
|
31
|
+
if not p:
|
|
32
|
+
continue
|
|
33
|
+
n = text.count(p)
|
|
34
|
+
if n:
|
|
35
|
+
hits[p] = n
|
|
36
|
+
n_hits = sum(hits.values())
|
|
37
|
+
per_kilo = round(n_hits * 1000.0 / total, 2) if total else 0.0
|
|
38
|
+
return {"hits": hits, "per_kilo": per_kilo, "alert": per_kilo >= ALERT_PER_KILO}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def report_line(text):
|
|
42
|
+
"""生成注入评审提示词的一行报告;无命中返回空串。"""
|
|
43
|
+
r = analyze(text)
|
|
44
|
+
if not r["hits"]:
|
|
45
|
+
return ""
|
|
46
|
+
top = "、".join("「%s」×%d" % (k, v)
|
|
47
|
+
for k, v in sorted(r["hits"].items(), key=lambda x: -x[1])[:8])
|
|
48
|
+
line = "- [AI味检测] 确定性统计:套话密度 %.1f/千字(%s)。" % (r["per_kilo"], top)
|
|
49
|
+
if r["alert"]:
|
|
50
|
+
line += "密度超过告警线 %.0f/千字,请重点评审译制腔与套话问题。" % ALERT_PER_KILO
|
|
51
|
+
return line
|
package/app/core/automation.py
CHANGED
|
@@ -378,6 +378,15 @@ def _tick():
|
|
|
378
378
|
_fire(dict(t), now)
|
|
379
379
|
except Exception:
|
|
380
380
|
log.exception("automation: tick 处理任务 %s 异常,已跳过", tid)
|
|
381
|
+
# 定时发布联动(P2.5):任务级 auto_publish 标记到点触发批量发布。
|
|
382
|
+
# 发布逻辑全在 publish/auto(护栏/幂等/单飞),这里只当调度宿主。
|
|
383
|
+
try:
|
|
384
|
+
from .publish import auto as _pub_auto
|
|
385
|
+
n = _pub_auto.fire_due()
|
|
386
|
+
if n:
|
|
387
|
+
log.info("automation: 定时发布触发 %d 个任务", n)
|
|
388
|
+
except Exception:
|
|
389
|
+
log.debug("automation: 定时发布检查跳过", exc_info=True)
|
|
381
390
|
|
|
382
391
|
|
|
383
392
|
def _loop():
|
package/app/core/gitmod.py
CHANGED
|
@@ -321,6 +321,10 @@ def collect_changes(workdir, max_diff=30000):
|
|
|
321
321
|
continue
|
|
322
322
|
if xy == "??" and _attach_path(path):
|
|
323
323
|
continue
|
|
324
|
+
# .codebee/ 是本工具的任务元数据目录(spec.md 等):随任务分支提交版本化,
|
|
325
|
+
# 但不算产品变更——面板/裁决视图只看任务产出,过程文件是噪音
|
|
326
|
+
if path == ".codebee" or path.startswith(".codebee/") or path.startswith('" .codebee'):
|
|
327
|
+
continue
|
|
324
328
|
files.append({"status": _status_code(xy), "path": path})
|
|
325
329
|
if xy == "??":
|
|
326
330
|
untracked.append(path)
|
package/app/core/jobs.py
CHANGED
|
@@ -20,6 +20,12 @@ _alive = 0 # 活跃 worker 线程数
|
|
|
20
20
|
_target = 3 # 目标并发数(settings.max_concurrent_jobs)
|
|
21
21
|
_pool_lock = threading.Lock()
|
|
22
22
|
_seq = 0
|
|
23
|
+
MAX_POOL = 12 # 并发上限:每个 job 只是拉起 CLI 子进程,跨任务并行无共享
|
|
24
|
+
# 资源(同任务单飞另有守卫),照竞品(emdash/munder-difflin
|
|
25
|
+
# 高并行环境)放开到 12
|
|
26
|
+
WATCHDOG_INTERVAL_S = 60 # 队列看门狗巡检周期
|
|
27
|
+
WATCHDOG_STALE_S = 120 # queued 超过该秒数视为掉队(正常入队到被拿起 ≤5s)
|
|
28
|
+
_watchdog_started = False
|
|
23
29
|
|
|
24
30
|
AI_REPAIR_PROMPT = """你是环境工程师。在 Windows 上执行下面的安装命令失败了,请诊断原因并给出修正命令。
|
|
25
31
|
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
@@ -48,9 +54,9 @@ def _repair_command_allowed(cmd):
|
|
|
48
54
|
|
|
49
55
|
|
|
50
56
|
def configure(max_workers):
|
|
51
|
-
"""设置目标并发数(1-
|
|
57
|
+
"""设置目标并发数(1-12):扩容立即补线程,缩容由空闲线程自行退出。"""
|
|
52
58
|
global _target
|
|
53
|
-
_target = max(1, min(
|
|
59
|
+
_target = max(1, min(MAX_POOL, int(max_workers)))
|
|
54
60
|
if _started:
|
|
55
61
|
_resize()
|
|
56
62
|
return _target
|
|
@@ -97,14 +103,37 @@ def enqueue(job):
|
|
|
97
103
|
|
|
98
104
|
|
|
99
105
|
def _ensure_workers():
|
|
100
|
-
"""队列里积压超过空闲 worker 数时补线程(惰性扩容,替代启动期预建)。
|
|
106
|
+
"""队列里积压超过空闲 worker 数时补线程(惰性扩容,替代启动期预建)。
|
|
107
|
+
看门狗线程同样惰性起:与 worker 一样不在启动期 Thread.start()(杀软
|
|
108
|
+
挂起新线程的真实装机案例,见 start_worker 注释),首队到达时一起补。"""
|
|
109
|
+
global _watchdog_started
|
|
101
110
|
with _pool_lock:
|
|
102
111
|
pending = _QUEUE.qsize()
|
|
103
112
|
need = max(_target, 1) - _alive + pending
|
|
113
|
+
if not _watchdog_started:
|
|
114
|
+
_watchdog_started = True
|
|
115
|
+
threading.Thread(target=_watchdog, name="job-watchdog",
|
|
116
|
+
daemon=True).start()
|
|
104
117
|
if need > 0:
|
|
105
118
|
_resize()
|
|
106
119
|
|
|
107
120
|
|
|
121
|
+
def _watchdog():
|
|
122
|
+
"""队列看门狗:周期把卡死的 queued 编排运行补回队列。
|
|
123
|
+
|
|
124
|
+
job 队列在内存里,任何一次入队丢失(2026-09-18 实案:r-20260918-211920
|
|
125
|
+
排队 1 小时无人接手、进程未重启则启动补队永远不跑)都会让 UI 永远
|
|
126
|
+
「排队中」。这里每分钟自愈一次;重复入队由 worker 出队守卫(非 queued
|
|
127
|
+
跳过)与同任务单飞守卫兜底,幂等。巡检自身异常绝不退出。"""
|
|
128
|
+
import time as _t
|
|
129
|
+
while True:
|
|
130
|
+
_t.sleep(WATCHDOG_INTERVAL_S)
|
|
131
|
+
try:
|
|
132
|
+
requeue_pending(limit=6, max_age_s=WATCHDOG_STALE_S)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
108
137
|
def cancel(run_id):
|
|
109
138
|
ev = CANCELS.get(run_id)
|
|
110
139
|
if ev:
|
|
@@ -155,11 +184,26 @@ def _task_active_run(task_id, exclude_run_id=None):
|
|
|
155
184
|
return None
|
|
156
185
|
|
|
157
186
|
|
|
187
|
+
def _err_signature(err):
|
|
188
|
+
"""错误串的稳定特征:剥掉章号/引用号/数字等易变片段,只留病根文本。
|
|
189
|
+
同因连撞判定用——退避重跑后错误若还长得一样,说明墙没动(欠费/配置死),
|
|
190
|
+
再多次退避也只是重复付费。"""
|
|
191
|
+
import re
|
|
192
|
+
s = str(err or "")
|
|
193
|
+
s = re.sub(r"第\s*\d+\s*章", "第N章", s)
|
|
194
|
+
s = re.sub(r"err_[0-9a-fA-F]+", "err_X", s)
|
|
195
|
+
s = re.sub(r"\d+", "N", s)
|
|
196
|
+
return re.sub(r"\s+", " ", s).strip()[:400]
|
|
197
|
+
|
|
198
|
+
|
|
158
199
|
def _maybe_auto_resume(run_id):
|
|
159
200
|
"""连载任务失败自动续跑:继承已完成章继续,最多 AUTO_RESUME_MAX 次。
|
|
160
201
|
|
|
161
202
|
真实长篇单次运行常因供应商拥堵超时中断;这里在 worker 收尾时自动重排一次
|
|
162
203
|
续跑(store.retry_task 会带上 inherit),让整个流程真正无人值守。
|
|
204
|
+
同因连撞止损:续跑副本再失败时与本次失败的错误签名比对(2026-09-18
|
|
205
|
+
重写任务 kimi 403 欠费案),一模一样说明退避没换来不同结果,直接落
|
|
206
|
+
终态写明死因,不再烧剩余的退避次数。
|
|
163
207
|
"""
|
|
164
208
|
try:
|
|
165
209
|
from . import store
|
|
@@ -173,6 +217,16 @@ def _maybe_auto_resume(run_id):
|
|
|
173
217
|
return False # 用户主动取消的运行绝不自动续跑
|
|
174
218
|
if int(run.get("auto_resumes") or 0) >= AUTO_RESUME_MAX:
|
|
175
219
|
return False
|
|
220
|
+
prev_id = run.get("auto_resumed_from")
|
|
221
|
+
if prev_id:
|
|
222
|
+
prev = store.get_run(prev_id)
|
|
223
|
+
sig_now = _err_signature(run.get("error"))
|
|
224
|
+
if (prev and sig_now and prev.get("error")
|
|
225
|
+
and sig_now == _err_signature(prev.get("error"))):
|
|
226
|
+
store.update_run(run_id, auto_resume_stopped="same_cause",
|
|
227
|
+
error=(run.get("error") or "")
|
|
228
|
+
+ "|自动续跑止损:连续两次失败原因相同,不再重试")
|
|
229
|
+
return False
|
|
176
230
|
if _task_active_run(task["id"], exclude_run_id=run_id):
|
|
177
231
|
return False # 同任务已有运行排队/在跑:再排副本只会与之撞车
|
|
178
232
|
ok, err, new_run = store.retry_task(task["id"])
|
|
@@ -282,11 +336,43 @@ def _yield_duplicate(run_id):
|
|
|
282
336
|
return False
|
|
283
337
|
|
|
284
338
|
|
|
285
|
-
def
|
|
286
|
-
"""
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
339
|
+
def _age_s(run, now=None):
|
|
340
|
+
"""run 已创建多少秒(created_at 解析失败返回 0:宁可早补,别误判卡死)。"""
|
|
341
|
+
import time as _t
|
|
342
|
+
now = now if now is not None else _t.time()
|
|
343
|
+
try:
|
|
344
|
+
ts = _t.mktime(_t.strptime(run.get("created_at") or "", "%Y-%m-%d %H:%M:%S"))
|
|
345
|
+
except Exception:
|
|
346
|
+
return 0.0
|
|
347
|
+
return max(0.0, now - ts)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _in_resume_backoff(run, now=None):
|
|
351
|
+
"""续跑副本是否还在退避等待窗口内(resume_enqueue_at 未到点)。
|
|
352
|
+
|
|
353
|
+
到点时间由 _maybe_auto_resume 落在 run 上,Timer 到点才入队;巡检/补队
|
|
354
|
+
若无视它直接重排,等于把 300s 网关退避窗口烧掉(立即重排撞同一堵墙)。"""
|
|
355
|
+
import time as _t
|
|
356
|
+
now = now if now is not None else _t.time()
|
|
357
|
+
at = str(run.get("resume_enqueue_at") or "")
|
|
358
|
+
if not at:
|
|
359
|
+
return False
|
|
360
|
+
try:
|
|
361
|
+
return _t.mktime(_t.strptime(at, "%Y-%m-%d %H:%M:%S")) > now
|
|
362
|
+
except Exception:
|
|
363
|
+
return False # 解析失败按「不在退避」处理:宁可入队,别让副本永远卡死
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def requeue_pending(limit=10, max_age_s=None):
|
|
367
|
+
"""把遗留的 queued 编排运行重新入队(启动补队与运行期巡检共用)。
|
|
368
|
+
|
|
369
|
+
队列在内存里,进程一死排队项就没人管了(2026-09-18 七猫 r-162724
|
|
370
|
+
排队僵尸案:续跑副本 created 后服务重启,Timer 随进程蒸发,运行永远
|
|
371
|
+
停在「排队中」)。同任务已有在跑/排队的不重复补。
|
|
372
|
+
|
|
373
|
+
max_age_s:巡检模式只补「卡了超过该秒数」的,刚入队的正常排队不掺和;
|
|
374
|
+
None(启动模式)全量补。resume_enqueue_at 未到点的续跑副本两种模式都
|
|
375
|
+
跳过——重启不该把退避窗口烧掉。返回补队条数。"""
|
|
290
376
|
try:
|
|
291
377
|
from . import store
|
|
292
378
|
except Exception:
|
|
@@ -300,6 +386,10 @@ def requeue_pending(limit=10):
|
|
|
300
386
|
continue
|
|
301
387
|
if not run.get("task_id") or not _recent(run):
|
|
302
388
|
continue
|
|
389
|
+
if _in_resume_backoff(run):
|
|
390
|
+
continue # 退避窗口内的续跑副本:到点 Timer 自会入队
|
|
391
|
+
if max_age_s is not None and _age_s(run) < max_age_s:
|
|
392
|
+
continue
|
|
303
393
|
if _task_active_run(run["task_id"], exclude_run_id=run["id"]):
|
|
304
394
|
continue # 同任务已有更活跃的运行,别再排一份
|
|
305
395
|
_QUEUE.put({"kind": "orchestration", "run_id": run["id"],
|
|
@@ -326,13 +416,15 @@ def _worker():
|
|
|
326
416
|
run_id = job.get("run_id")
|
|
327
417
|
ev = cancel_event_for(run_id) if run_id else threading.Event()
|
|
328
418
|
try:
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
419
|
+
# 出队后状态闸:非 queued 一律跳过。排队期取消的(cancel 已
|
|
420
|
+
# 直接落终态)不再进流水线;running/done/failed 的是看门狗
|
|
421
|
+
# 重排/双入队产生的重复副本——另一 worker 已在跑或已跑完,
|
|
422
|
+
# 再 execute_run 会把同一运行执行两次。查不到的 run(测试
|
|
423
|
+
# mock)不拦,保持原行为。
|
|
332
424
|
if run_id:
|
|
333
425
|
from . import store
|
|
334
426
|
r0 = store.get_run(run_id)
|
|
335
|
-
if r0 and r0.get("status")
|
|
427
|
+
if r0 and r0.get("status") != "queued":
|
|
336
428
|
continue # task_done 由 finally 统一收口,不能在此重复
|
|
337
429
|
if job.get("kind") == "orchestration" and _yield_duplicate(run_id):
|
|
338
430
|
continue # 同任务单飞:续跑副本让位(已落 cancelled)
|
|
@@ -365,7 +457,17 @@ def _worker():
|
|
|
365
457
|
|
|
366
458
|
def workers_info():
|
|
367
459
|
with _pool_lock:
|
|
368
|
-
return {"target": _target, "alive": _alive}
|
|
460
|
+
return {"target": _target, "alive": _alive, "queued": _QUEUE.qsize()}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _drain_test_queue():
|
|
464
|
+
"""测试辅助:清空内存队列并结清未决 join(生产代码勿调)。"""
|
|
465
|
+
while True:
|
|
466
|
+
try:
|
|
467
|
+
_QUEUE.get_nowait()
|
|
468
|
+
_QUEUE.task_done()
|
|
469
|
+
except Exception:
|
|
470
|
+
return
|
|
369
471
|
|
|
370
472
|
|
|
371
473
|
def _now():
|
package/app/core/manager.py
CHANGED
|
@@ -1429,6 +1429,55 @@ def _sync_launch_model(entry, binding):
|
|
|
1429
1429
|
return notes
|
|
1430
1430
|
|
|
1431
1431
|
|
|
1432
|
+
# kind(runner 调用族)→ catalog 条目 id:flow/编排智能体的 id 常是业务名
|
|
1433
|
+
# (draft-c12 等),CLI 身份在 kind 里;目录型智能体的 id 就是条目 id。
|
|
1434
|
+
_KIND_ENTRY = {"codex": "codex-cli", "claude": "claude-code",
|
|
1435
|
+
"qwen": "qwencode", "opencode": "opencode", "aider": "aider"}
|
|
1436
|
+
_RUNTIME_SYNC = {"fps": {}, "lock": threading.Lock()}
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def sync_runtime_config(agent):
|
|
1440
|
+
"""运行前防线:把「CLI 绑定」页当前链首落进该 CLI 自家配置——与一键打开
|
|
1441
|
+
同一套 _sync_launch_model 写盘,不改任何语义。
|
|
1442
|
+
|
|
1443
|
+
绑定换供应商后 CLI 自家配置不会自愈,而同步过去只发生在一键打开:
|
|
1444
|
+
2026-09-18 opencode 钉死讯飞 404 旧端点案——绑定早切到云知声,起跑撞旧
|
|
1445
|
+
配置 2 秒 UnknownError,3 次续跑全灭。bind_agent 入口调用本函数即全覆盖
|
|
1446
|
+
(死链补位也在覆盖内:_dead_binding_substitute 内部同样走 bind_agent)。
|
|
1447
|
+
|
|
1448
|
+
指纹缓存:同绑定(模型/供应商/端点/KEY/codex 段)只写一次盘,步骤级高频
|
|
1449
|
+
调用近零成本,也不与用户手工编辑配置打架(绑定没变就不动);无绑定或
|
|
1450
|
+
条目无配置段直接返回(测试空转,绝不碰真实家目录);任何失败静默放过
|
|
1451
|
+
——防线不拦运行,配置错误会在步骤错误里现形。"""
|
|
1452
|
+
try:
|
|
1453
|
+
cand = [agent.get("id"), _KIND_ENTRY.get(agent.get("kind") or "")]
|
|
1454
|
+
entry = None
|
|
1455
|
+
for x in cand:
|
|
1456
|
+
if x:
|
|
1457
|
+
entry = catalog.by_id(x)
|
|
1458
|
+
if entry:
|
|
1459
|
+
break
|
|
1460
|
+
if entry is None or not (entry.get("config") or {}).get("path"):
|
|
1461
|
+
return
|
|
1462
|
+
from . import modelhub
|
|
1463
|
+
binding = modelhub.resolve_binding(entry["id"]) or {}
|
|
1464
|
+
model = (binding.get("model") or "").strip()
|
|
1465
|
+
env = binding.get("env") or {}
|
|
1466
|
+
if not model and not env:
|
|
1467
|
+
return # 没配链或链全死:没有可落的目标,绝不动用户配置
|
|
1468
|
+
# 指纹取解析结果整体:env 里带端点+KEY(provider 字典不含原始 KEY,
|
|
1469
|
+
# 换 KEY 必须可见),codex 段单独入纹
|
|
1470
|
+
fp = (model, repr(sorted(env.items())),
|
|
1471
|
+
repr(binding.get("codex_provider") or "")[:300])
|
|
1472
|
+
with _RUNTIME_SYNC["lock"]:
|
|
1473
|
+
if _RUNTIME_SYNC["fps"].get(entry["id"]) == fp:
|
|
1474
|
+
return
|
|
1475
|
+
_sync_launch_model(entry, binding)
|
|
1476
|
+
_RUNTIME_SYNC["fps"][entry["id"]] = fp
|
|
1477
|
+
except Exception:
|
|
1478
|
+
pass
|
|
1479
|
+
|
|
1480
|
+
|
|
1432
1481
|
def launch(entry, open_browser=True):
|
|
1433
1482
|
"""一键打开:web 类后台起服务并自动开浏览器;console 类新开终端窗口跑交互 TUI。
|
|
1434
1483
|
|
package/app/core/modelhub.py
CHANGED
|
@@ -1930,7 +1930,16 @@ def bind_agent(agent, difficulty="default"):
|
|
|
1930
1930
|
"""按绑定生成应用了供应商/模型覆盖的 agent 副本;无绑定时原样返回。
|
|
1931
1931
|
|
|
1932
1932
|
binding_configured:该 CLI 是否配过绑定链(配没配与解析结果分开带出——
|
|
1933
|
-
执行层死链闸门只对「配过但全死」判失败,没配过的回落 CLI 本机默认)。
|
|
1933
|
+
执行层死链闸门只对「配过但全死」判失败,没配过的回落 CLI 本机默认)。
|
|
1934
|
+
|
|
1935
|
+
所有真实派发都经此函数(含死链补位),入口顺带做运行前配置同步防线:
|
|
1936
|
+
绑定换供应商后 CLI 自家配置不会自愈(2026-09-18 opencode 钉死讯飞旧端点
|
|
1937
|
+
案),这里把当前链首落进 CLI 自家配置。失败静默,不拦派发。"""
|
|
1938
|
+
try:
|
|
1939
|
+
from . import manager
|
|
1940
|
+
manager.sync_runtime_config(agent)
|
|
1941
|
+
except Exception:
|
|
1942
|
+
pass
|
|
1934
1943
|
rid = agent.get("id")
|
|
1935
1944
|
b = bindings().get(rid) or bindings().get(
|
|
1936
1945
|
"codex-cli" if rid == "codex" else "claude-code") or {}
|
package/app/core/paths.py
CHANGED
|
@@ -56,6 +56,7 @@ TASKS_DIR = DATA_DIR / "tasks"
|
|
|
56
56
|
RUNS_DIR = DATA_DIR / "runs"
|
|
57
57
|
USAGE_DIR = DATA_DIR / "usage"
|
|
58
58
|
ERRORS_DIR = DATA_DIR / "errors"
|
|
59
|
+
PUBLISH_DIR = DATA_DIR / "publish" # 一键发布:台账/作品登记/流程选择器覆盖/浏览器 profile
|
|
59
60
|
CATALOG_FILE = DATA_DIR / "catalog.json"
|
|
60
61
|
ENABLED_FILE = DATA_DIR / "orchestration.json"
|
|
61
62
|
|