codebee 0.1.49 → 0.1.51
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 +31 -0
- package/README.md +4 -150
- package/app/core/aiflavor.py +14 -0
- package/app/core/builtin_agent.py +567 -74
- package/app/core/jobs.py +170 -42
- package/app/core/knowledge.py +432 -405
- package/app/core/manager.py +100 -10
- package/app/core/market.py +27 -1
- package/app/core/modelhub.py +53 -5
- package/app/core/pipeline.py +193 -77
- package/app/core/planner.py +14 -5
- package/app/core/preview.py +293 -0
- package/app/core/router.py +189 -104
- package/app/core/runner.py +370 -144
- package/app/core/settings.py +11 -4
- package/app/core/skills.py +36 -12
- package/app/core/store.py +74 -2
- package/app/core/zentao.py +189 -30
- package/app/main.py +95 -7
- package/app/ui/app.js +13256 -12876
- package/app/ui/i18n.js +97 -34
- package/app/ui/index.html +55 -12
- package/app/ui/style.css +217 -26
- package/package.json +1 -1
package/app/core/jobs.py
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
"""任务执行器:直接启动、无等待队列地执行编排任务与管理操作。
|
|
3
3
|
|
|
4
4
|
每个 job 一条独立线程,run/step 数据按 run_id 隔离,store 层有全局锁。
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
设置中的并发数是保护上限:有空位就立即启动。满载时 orchestration 任务
|
|
6
|
+
(用户/自动化任务)回滚 queued 排队等下一个空位——补跑 Timer 到点重试,
|
|
7
|
+
封顶 BUSY_WAIT_MAX 拍,超时才判失败(满载秒判失败会逼用户原话重发,
|
|
8
|
+
侧栏堆同题重复任务,2026-09-22 纸枪对话实案);升级排水与其余 kind
|
|
9
|
+
仍明确失败并提示稍后重试。并发保护保护的是本机资源(CLI 子进程数),
|
|
10
|
+
与各 API 厂商的限流无关——厂商侧 429 由 runner 冷却换将兜底。direct
|
|
11
|
+
对话(内置智能体直连 API,本机几乎零开销)走独立轻量池,不与编排任务
|
|
12
|
+
抢全局槽。安装/升级失败时自动触发 AI 诊断修复:
|
|
7
13
|
由真实智能体读取失败日志与本机环境给出修正命令;仅当命令命中白名单前缀
|
|
8
14
|
(npm/winget/brew/pip 安装类,按平台取对应渠道)才自动执行,否则把建议命令
|
|
9
15
|
记录在运行记录里等人工确认。
|
|
@@ -22,12 +28,16 @@ _started = False
|
|
|
22
28
|
_alive = 0 # 已获执行位、尚未结束的 job 数
|
|
23
29
|
_restart_drain = False # 升级重启前原子停止接单;不排队、不打断已运行任务
|
|
24
30
|
_target = 12 # 并发保护上限(settings.max_concurrent_jobs)
|
|
31
|
+
_chat_alive = 0 # 轻量池:direct 对话在跑数(独立于全局池计数)
|
|
25
32
|
_pool_lock = threading.Lock()
|
|
26
33
|
_idle_cond = threading.Condition(_pool_lock)
|
|
27
34
|
_timer_lock = threading.Lock()
|
|
28
35
|
_deferred_timers = {}
|
|
29
36
|
_seq = 0
|
|
30
|
-
MAX_POOL =
|
|
37
|
+
MAX_POOL = 24
|
|
38
|
+
CHAT_POOL = 4 # 轻量池上限:direct 对话直连 API 本机开销极小,4 个足够
|
|
39
|
+
BUSY_WAIT_S = 15.0 # 满载排队后的补跑轮询间隔(秒);测试可调小
|
|
40
|
+
BUSY_WAIT_MAX = 40 # 补跑拍数封顶:15s × 40 = 10 分钟,超时才判失败
|
|
31
41
|
|
|
32
42
|
|
|
33
43
|
class JobsBusyError(RuntimeError):
|
|
@@ -80,7 +90,7 @@ def _repair_command_allowed(cmd, platform=None):
|
|
|
80
90
|
|
|
81
91
|
|
|
82
92
|
def configure(max_workers):
|
|
83
|
-
"""设置并发保护上限(1-
|
|
93
|
+
"""设置并发保护上限(1-24);只影响后续启动,不中断已运行任务。"""
|
|
84
94
|
global _target
|
|
85
95
|
_target = max(1, min(MAX_POOL, int(max_workers)))
|
|
86
96
|
return _target
|
|
@@ -112,23 +122,33 @@ def start_worker():
|
|
|
112
122
|
configure(12)
|
|
113
123
|
|
|
114
124
|
|
|
115
|
-
def
|
|
116
|
-
"""
|
|
125
|
+
def _job_is_light(run):
|
|
126
|
+
"""direct 对话走轻量池:内置智能体直连模型 API,本机无 CLI 子进程,
|
|
127
|
+
开销与编排任务(每步起一个 CLI)差几个量级,不该抢全局槽。判定按
|
|
128
|
+
run 所属任务的 type,入口侧零改动(自动化/webhook 等新入口自动跟随)。
|
|
129
|
+
run 缺失或类型未知时保守走全局池。direct 极少数回落 CLI 的场景,轻量
|
|
130
|
+
池上限也只有 CHAT_POOL 个,本机压力可控。"""
|
|
131
|
+
try:
|
|
132
|
+
task_id = (run or {}).get("task_id")
|
|
133
|
+
if not task_id:
|
|
134
|
+
return False
|
|
135
|
+
from . import store
|
|
136
|
+
task = store.get_task(task_id)
|
|
137
|
+
return str((task or {}).get("type") or "") == "direct"
|
|
138
|
+
except Exception:
|
|
139
|
+
return False
|
|
117
140
|
|
|
118
|
-
返回前先把持久化 run 从 queued 原子切到 running,因此 HTTP/自动化调用方
|
|
119
|
-
不会看到“已接受但仍排队”。满载、重复 run、线程创建失败都会明确抛错;
|
|
120
|
-
满载与启动失败还会就地把 run 收口为 failed,任何入口都不会留下僵尸。
|
|
121
|
-
"""
|
|
122
|
-
global _alive, _seq
|
|
123
|
-
if not _started:
|
|
124
|
-
start_worker()
|
|
125
|
-
if not isinstance(job, dict):
|
|
126
|
-
raise ValueError("job 必须是对象")
|
|
127
|
-
run_id = job.get("run_id")
|
|
128
|
-
if not run_id:
|
|
129
|
-
raise ValueError("job.run_id 必填")
|
|
130
141
|
|
|
142
|
+
def _try_start_once(job):
|
|
143
|
+
"""CAS 认领持久化 run → 占并发位 → 起执行线程,一次直接启动尝试。
|
|
144
|
+
|
|
145
|
+
返回 True=已起跑;False=并发位满载未启动(run 停在 running,由调用方
|
|
146
|
+
决定回滚排队还是收口失败)。run 已非 queued(被取消/接管/收口)时抛
|
|
147
|
+
DuplicateJobError;线程创建失败就地收口后原样上抛。升级排水在这里视作
|
|
148
|
+
满载的一种(返回 False),快速失败还是排队的语义由调用方区分。"""
|
|
149
|
+
global _alive, _chat_alive, _seq
|
|
131
150
|
from . import store
|
|
151
|
+
run_id = job.get("run_id")
|
|
132
152
|
current = store.get_run(run_id)
|
|
133
153
|
if current:
|
|
134
154
|
# 先 CAS 认领持久化 run,再碰并发位。若先占位,两个调用可能分别看到
|
|
@@ -142,55 +162,95 @@ def enqueue(job):
|
|
|
142
162
|
(run_id, latest.get("status")))
|
|
143
163
|
cancel_event_for(run_id)
|
|
144
164
|
|
|
145
|
-
# CAS
|
|
146
|
-
# 回写 alive、扩容循环一次造出几十条 worker 的竞态。
|
|
165
|
+
# CAS 认领后检查并发保护位。占位在 Thread.start 前递增,消除旧实现中线程尚未
|
|
166
|
+
# 回写 alive、扩容循环一次造出几十条 worker 的竞态。direct 对话占轻量池,
|
|
167
|
+
# 其余任务占全局池;job["light"] 随副本传给执行线程,结束时按原池释放。
|
|
168
|
+
light = _job_is_light(current)
|
|
147
169
|
with _pool_lock:
|
|
148
170
|
if _restart_drain:
|
|
149
|
-
|
|
150
|
-
elif
|
|
151
|
-
|
|
171
|
+
busy = True
|
|
172
|
+
elif light:
|
|
173
|
+
busy = _chat_alive >= CHAT_POOL
|
|
174
|
+
if not busy:
|
|
175
|
+
_chat_alive += 1
|
|
176
|
+
job["light"] = True
|
|
152
177
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
busy = _alive >= _target
|
|
179
|
+
if not busy:
|
|
180
|
+
_alive += 1
|
|
181
|
+
if not busy:
|
|
155
182
|
_seq += 1
|
|
156
183
|
seq = _seq
|
|
157
|
-
if
|
|
184
|
+
if busy:
|
|
158
185
|
CANCELS.pop(run_id, None)
|
|
159
|
-
|
|
160
|
-
message = "服务正在完成升级重启;本次未排队,请稍后重试"
|
|
161
|
-
else:
|
|
162
|
-
message = "当前运行任务已达并发保护上限(%d);本次未排队,请稍后重试" % busy_limit
|
|
163
|
-
_close_unstarted(job, message,
|
|
164
|
-
statuses=("running",))
|
|
165
|
-
raise JobsBusyError(message)
|
|
186
|
+
return False
|
|
166
187
|
|
|
167
188
|
try:
|
|
168
189
|
threading.Thread(target=_run_job, args=(dict(job),),
|
|
169
190
|
name="job-direct-%d" % seq, daemon=True).start()
|
|
170
191
|
except Exception:
|
|
171
|
-
_release_slot()
|
|
192
|
+
_release_slot(job)
|
|
172
193
|
CANCELS.pop(run_id, None)
|
|
173
194
|
_close_unstarted(job, "任务执行线程启动失败;本次未排队,请稍后重试",
|
|
174
195
|
statuses=("queued", "running"))
|
|
175
196
|
raise
|
|
197
|
+
return True
|
|
176
198
|
|
|
199
|
+
|
|
200
|
+
def enqueue(job):
|
|
201
|
+
"""立即为 job 预留执行位并启动独立线程;从不进入内存等待队列。
|
|
202
|
+
|
|
203
|
+
返回前先把持久化 run 从 queued 原子切到 running,因此 HTTP/自动化调用方
|
|
204
|
+
不会看到“已接受但仍排队”。满载时 orchestration 任务转排队等待空位(对
|
|
205
|
+
调用方视作受理成功,见 _requeue_await_slot);重复 run、线程创建失败、
|
|
206
|
+
升级排水满载都会明确抛错,并就地把 run 收口为 failed,任何入口都不会
|
|
207
|
+
留下无人认领的僵尸。
|
|
208
|
+
"""
|
|
209
|
+
if not _started:
|
|
210
|
+
start_worker()
|
|
211
|
+
if not isinstance(job, dict):
|
|
212
|
+
raise ValueError("job 必须是对象")
|
|
213
|
+
run_id = job.get("run_id")
|
|
214
|
+
if not run_id:
|
|
215
|
+
raise ValueError("job.run_id 必填")
|
|
216
|
+
|
|
217
|
+
if not _try_start_once(job):
|
|
218
|
+
if _restart_drain:
|
|
219
|
+
# 升级排水:接单通道已在关闭,排队只会排进一次注定重启的进程——
|
|
220
|
+
# 照旧快速失败。
|
|
221
|
+
message = "服务正在完成升级重启;本次未排队,请稍后重试"
|
|
222
|
+
_close_unstarted(job, message,
|
|
223
|
+
statuses=("running",))
|
|
224
|
+
raise JobsBusyError(message)
|
|
225
|
+
if str(job.get("kind") or "") != "orchestration" or not _requeue_await_slot(job):
|
|
226
|
+
# mgmt 与回滚失败(run 已被取消/收口)维持快速失败原语义;
|
|
227
|
+
# orchestration 已转排队,对调用方视作受理成功。
|
|
228
|
+
message = ("当前运行任务已达并发保护上限(%d);本次未排队,请稍后重试"
|
|
229
|
+
% _target)
|
|
230
|
+
_close_unstarted(job, message,
|
|
231
|
+
statuses=("running",))
|
|
232
|
+
raise JobsBusyError(message)
|
|
177
233
|
return True
|
|
178
234
|
|
|
179
235
|
|
|
180
|
-
def _release_slot():
|
|
181
|
-
|
|
236
|
+
def _release_slot(job=None):
|
|
237
|
+
"""释放执行位:按 job 的 light 标记归还对应池,两池全空才唤醒等待方。"""
|
|
238
|
+
global _alive, _chat_alive
|
|
182
239
|
with _idle_cond:
|
|
183
|
-
|
|
184
|
-
|
|
240
|
+
if job is not None and job.get("light"):
|
|
241
|
+
_chat_alive = max(0, _chat_alive - 1)
|
|
242
|
+
else:
|
|
243
|
+
_alive = max(0, _alive - 1)
|
|
244
|
+
if _alive == 0 and _chat_alive == 0:
|
|
185
245
|
_idle_cond.notify_all()
|
|
186
246
|
|
|
187
247
|
|
|
188
248
|
def wait_for_idle(timeout=10):
|
|
189
|
-
"""
|
|
249
|
+
"""测试/停机辅助:等待所有直接执行任务(含轻量池)结束。"""
|
|
190
250
|
import time as _t
|
|
191
251
|
end = _t.time() + max(0, float(timeout))
|
|
192
252
|
with _idle_cond:
|
|
193
|
-
while _alive:
|
|
253
|
+
while _alive or _chat_alive:
|
|
194
254
|
left = end - _t.time()
|
|
195
255
|
if left <= 0:
|
|
196
256
|
return False
|
|
@@ -228,6 +288,72 @@ def _close_unstarted(job, message, statuses=("queued",)):
|
|
|
228
288
|
pass
|
|
229
289
|
|
|
230
290
|
|
|
291
|
+
def _requeue_await_slot(job, waits_left=None):
|
|
292
|
+
"""满载转排队:run 回滚 queued 并挂补跑 Timer,等下一个并发空位。
|
|
293
|
+
|
|
294
|
+
只服务 orchestration 任务(用户/自动化任务)。enqueue 起手已把 run CAS
|
|
295
|
+
成 running,这里再 CAS 回 queued(清 started_at,别让「排队中」顶着起跑
|
|
296
|
+
时间),并同步任务状态回排队。返回 True=已转排队(调用方视作受理成功,
|
|
297
|
+
不再向调用方抛忙);False=回滚失败(run 已被取消/收口),走原快速失败
|
|
298
|
+
路径。取消/重启/超时的收口:
|
|
299
|
+
- 用户取消 → run 已非 running,回滚 CAS 落空,或补跑 enqueue 撞
|
|
300
|
+
DuplicateJobError,链条自然断掉;
|
|
301
|
+
- 服务重启 → Timer 随进程消失,启动时 requeue_pending() 接管 queued 记录;
|
|
302
|
+
- 等满 BUSY_WAIT_MAX 拍仍无空位 → 判失败并给出可读原因。
|
|
303
|
+
"""
|
|
304
|
+
run_id = job.get("run_id")
|
|
305
|
+
if waits_left is None:
|
|
306
|
+
waits_left = BUSY_WAIT_MAX
|
|
307
|
+
try:
|
|
308
|
+
from . import store
|
|
309
|
+
changed = store.update_run(run_id, expected_status="running",
|
|
310
|
+
status="queued", started_at="")
|
|
311
|
+
if changed is None:
|
|
312
|
+
return False
|
|
313
|
+
if job.get("task_id"):
|
|
314
|
+
try:
|
|
315
|
+
store.update_task_status(job["task_id"], "queued")
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
except Exception:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
def _fire():
|
|
322
|
+
# 补跑心跳:直接尝试一次启动(不走 enqueue 的排队分支,否则每拍都会
|
|
323
|
+
# 带着全新重试额度重新入队,封顶永远打不中)。仍满载则续下一拍。
|
|
324
|
+
with _timer_lock:
|
|
325
|
+
_deferred_timers.pop(run_id, None)
|
|
326
|
+
try:
|
|
327
|
+
started = _try_start_once(job)
|
|
328
|
+
except DuplicateJobError:
|
|
329
|
+
return # 已被取消/接管/收口:补跑链到此为止
|
|
330
|
+
except Exception:
|
|
331
|
+
return # 线程启动失败等异常路径已就地收口,不续棒
|
|
332
|
+
if started:
|
|
333
|
+
return
|
|
334
|
+
if _restart_drain:
|
|
335
|
+
_close_unstarted(job, "服务正在完成升级重启;本次未排队,请稍后重试",
|
|
336
|
+
statuses=("running",))
|
|
337
|
+
elif waits_left > 0:
|
|
338
|
+
_requeue_await_slot(job, waits_left - 1)
|
|
339
|
+
else:
|
|
340
|
+
_close_unstarted(job, "并发位已满,排队等待超时仍未起跑,请稍后重试",
|
|
341
|
+
statuses=("running",))
|
|
342
|
+
|
|
343
|
+
with _timer_lock:
|
|
344
|
+
if run_id in _deferred_timers:
|
|
345
|
+
return True # 已有补跑在等:别叠 Timer(防与续跑退避串台)
|
|
346
|
+
timer = threading.Timer(max(0.0, float(BUSY_WAIT_S)), _fire)
|
|
347
|
+
timer.daemon = True
|
|
348
|
+
_deferred_timers[run_id] = timer
|
|
349
|
+
try:
|
|
350
|
+
timer.start()
|
|
351
|
+
except Exception:
|
|
352
|
+
_deferred_timers.pop(run_id, None)
|
|
353
|
+
return False
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
|
|
231
357
|
def _schedule_enqueue(job, delay_s):
|
|
232
358
|
"""按明确截止时间延迟启动;同一 run 只保留一个 Timer。"""
|
|
233
359
|
run_id = job.get("run_id")
|
|
@@ -342,7 +468,7 @@ def _run_job(job):
|
|
|
342
468
|
notify.push_run_async(run_id)
|
|
343
469
|
except Exception:
|
|
344
470
|
pass
|
|
345
|
-
_release_slot()
|
|
471
|
+
_release_slot(job)
|
|
346
472
|
|
|
347
473
|
|
|
348
474
|
def cancel(run_id):
|
|
@@ -648,7 +774,9 @@ def requeue_pending(limit=10, max_age_s=None):
|
|
|
648
774
|
|
|
649
775
|
def workers_info():
|
|
650
776
|
with _pool_lock:
|
|
651
|
-
return {"target": _target, "alive": _alive,
|
|
777
|
+
return {"target": _target, "alive": _alive,
|
|
778
|
+
"chat_alive": _chat_alive, "chat_pool": CHAT_POOL,
|
|
779
|
+
"queued": 0,
|
|
652
780
|
"available": 0 if _restart_drain else max(0, _target - _alive),
|
|
653
781
|
"mode": "restart-drain" if _restart_drain else "direct"}
|
|
654
782
|
|