codebee 0.1.23 → 0.1.24
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 +16 -0
- package/README.md +20 -4
- package/app/core/attachments.py +117 -4
- package/app/core/dispatch.py +34 -5
- package/app/core/dispatch_log.py +113 -0
- package/app/core/errorlog.py +1 -43
- package/app/core/jobs.py +30 -4
- package/app/core/modelhub.py +2 -0
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +119 -7
- package/app/core/portguard.py +111 -0
- package/app/core/portscan.py +188 -188
- package/app/core/redact.py +38 -0
- package/app/core/router.py +34 -8
- package/app/core/runner.py +15 -8
- package/app/core/selfupdate.py +86 -27
- package/app/core/store.py +2 -4
- package/app/core/usage.py +226 -24
- package/app/main.py +52 -21
- package/app/pet.py +34 -11
- package/app/ui/app.js +48 -1
- package/app/ui/i18n.js +2 -0
- package/app/ui/index.html +1 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, catalog, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
|
|
22
|
+
from . import aiflavor, catalog, dispatch_log, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
|
|
23
23
|
from . import builtin_agent
|
|
24
24
|
from . import diagnostics
|
|
25
25
|
from . import paths as paths_mod
|
|
@@ -60,6 +60,20 @@ def _task_images(task, workdir):
|
|
|
60
60
|
return []
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _refresh_attachment_context(task, workdir):
|
|
64
|
+
"""为新旧任务统一生成附件正文上下文;失败时保留原上下文。"""
|
|
65
|
+
if not task.get("attachments"):
|
|
66
|
+
return task
|
|
67
|
+
try:
|
|
68
|
+
from . import attachments as att_mod
|
|
69
|
+
out = dict(task)
|
|
70
|
+
out["context"] = att_mod.merge_context(
|
|
71
|
+
task.get("context") or "", task.get("attachments") or [], workdir=workdir)
|
|
72
|
+
return out
|
|
73
|
+
except Exception:
|
|
74
|
+
return task
|
|
75
|
+
|
|
76
|
+
|
|
63
77
|
def _ms_name(raw):
|
|
64
78
|
name = re.sub(r"[\\/\x00]+", "_", str(raw or "")).strip()
|
|
65
79
|
name = re.sub(r"\.{2,}", "_", name).lstrip(".")
|
|
@@ -253,6 +267,7 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
253
267
|
lines.append("本步为评审步骤:请把上述用户意见作为评分依据之一,"
|
|
254
268
|
"在相应维度的分数与 issues 中明确体现(引用用户原话)。")
|
|
255
269
|
imgs = []
|
|
270
|
+
attachment_paths = []
|
|
256
271
|
for m in msgs:
|
|
257
272
|
stamp = m.get("created_at") or ""
|
|
258
273
|
sender = m.get("sender") or "用户"
|
|
@@ -261,6 +276,7 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
261
276
|
else "- [%s %s](附件指令,见下方文件)" % (stamp, sender))
|
|
262
277
|
for rel in (m.get("attachments") or []):
|
|
263
278
|
rel = str(rel)
|
|
279
|
+
attachment_paths.append(rel)
|
|
264
280
|
low = rel.lower()
|
|
265
281
|
if low.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")):
|
|
266
282
|
ap = os.path.join(workdir or "", rel) if workdir else rel
|
|
@@ -271,6 +287,13 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
271
287
|
lines.append(" · 图片附件:%s" % rel)
|
|
272
288
|
else:
|
|
273
289
|
lines.append(" · 文件附件:%s(位于工作目录,可直接读取)" % rel)
|
|
290
|
+
if attachment_paths:
|
|
291
|
+
try:
|
|
292
|
+
from . import attachments as att_mod
|
|
293
|
+
items = att_mod.items_from_paths(attachment_paths, workdir)
|
|
294
|
+
lines.append(att_mod.context_block(items, workdir=workdir))
|
|
295
|
+
except Exception:
|
|
296
|
+
lines.append("附件预读失败:执行者必须逐个打开上述路径,无法读取时明确说明。")
|
|
274
297
|
return "\n".join(lines), imgs
|
|
275
298
|
|
|
276
299
|
|
|
@@ -609,6 +632,9 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
|
|
|
609
632
|
try:
|
|
610
633
|
run = store.get_run(run_id) or {}
|
|
611
634
|
task = store.get_task(run.get("task_id")) if run.get("task_id") else None
|
|
635
|
+
provider = res.get("provider") or agent.get("provider") or {}
|
|
636
|
+
provider_id = provider.get("id") if isinstance(provider, dict) else ""
|
|
637
|
+
provider_name = provider.get("name") if isinstance(provider, dict) else ""
|
|
612
638
|
usage.record(
|
|
613
639
|
source=source, run_id=run_id, step=step,
|
|
614
640
|
task_id=run.get("task_id") or "",
|
|
@@ -617,13 +643,14 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
|
|
|
617
643
|
agent_label=agent.get("label", ""),
|
|
618
644
|
tool=agent.get("kind", ""),
|
|
619
645
|
model=res.get("model") or "",
|
|
646
|
+
provider=res.get("provider_id") or provider_id or provider_name or "",
|
|
620
647
|
ok=bool(res.get("ok")),
|
|
621
648
|
duration_s=float(res.get("raw", {}).get("duration") or 0.0),
|
|
622
649
|
cost_usd=float(res.get("cost_usd") or 0.0),
|
|
623
650
|
usage=res.get("usage"))
|
|
624
651
|
# 告警模块:CLI 调用成功/失败上报(provider 名与 usage 台账一致)
|
|
625
652
|
from . import health
|
|
626
|
-
prov = agent.get("provider") or {}
|
|
653
|
+
prov = res.get("provider") or agent.get("provider") or {}
|
|
627
654
|
prov_name = (prov.get("name") if isinstance(prov, dict) else "") or ""
|
|
628
655
|
if prov_name:
|
|
629
656
|
if res.get("ok"):
|
|
@@ -663,6 +690,9 @@ __GOAL__
|
|
|
663
690
|
## 未通过的原因
|
|
664
691
|
__ISSUES__
|
|
665
692
|
|
|
693
|
+
## 原始背景与附件
|
|
694
|
+
__CONTEXT__
|
|
695
|
+
|
|
666
696
|
## 要求
|
|
667
697
|
- 只针对上述问题修复;不要无关重构。
|
|
668
698
|
- 完成后用 2-3 句话说明改了什么。
|
|
@@ -688,6 +718,9 @@ __GOAL__
|
|
|
688
718
|
## 验收命令
|
|
689
719
|
__VERIFY__
|
|
690
720
|
|
|
721
|
+
## 原始背景与附件要求
|
|
722
|
+
__CONTEXT__
|
|
723
|
+
|
|
691
724
|
## 变更内容(git diff,若为空表示无法获取)
|
|
692
725
|
__DIFF__
|
|
693
726
|
"""
|
|
@@ -746,6 +779,7 @@ def _run_review(run_id, task, workdir, reviewer, ev):
|
|
|
746
779
|
prompt = (CODE_REVIEW_PROMPT
|
|
747
780
|
.replace("__GOAL__", task["goal"])
|
|
748
781
|
.replace("__VERIFY__", task.get("verify_command") or "(未配置)")
|
|
782
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
749
783
|
.replace("__DIFF__", diff or "(无法获取 git diff,请综合任务目标谨慎评审)"))
|
|
750
784
|
res = _run_step(run_id, "review", reviewer, prompt, workdir, readonly=True, ev=ev,
|
|
751
785
|
images=_task_images(task, workdir))
|
|
@@ -904,6 +938,31 @@ def _record_actual_route(run_id, task, agents, stats, implementer,
|
|
|
904
938
|
store.update_run(run_id, route_plan={
|
|
905
939
|
"task": spec, "implement": impl_plan, "review": review_plan,
|
|
906
940
|
})
|
|
941
|
+
task_id = task.get("id") or (store.get_run(run_id) or {}).get("task_id") or ""
|
|
942
|
+
difficulty = (task.get("difficulty") or "auto")
|
|
943
|
+
for plan in (impl_plan, review_plan):
|
|
944
|
+
dispatch_log.record_event(
|
|
945
|
+
run_id=run_id, task_id=task_id, task_type=spec.get("type") or "",
|
|
946
|
+
difficulty=difficulty, role=plan.get("role") or "",
|
|
947
|
+
phase="selected", selected=plan.get("selected") or "",
|
|
948
|
+
participants=plan.get("participants") or (),
|
|
949
|
+
candidates=plan.get("candidates") or (),
|
|
950
|
+
fallback=plan.get("fallback") or (),
|
|
951
|
+
selection_reason=plan.get("selection_reason") or "")
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _record_dispatch_completed(run_id, task, result, verify_pass=None, review_pass=None):
|
|
955
|
+
"""记录运行终态,供调度回放与线上指标复盘使用。"""
|
|
956
|
+
try:
|
|
957
|
+
spec = task.get("_compiled_spec") or task_compile.compile_task(task)
|
|
958
|
+
dispatch_log.record_event(
|
|
959
|
+
run_id=run_id,
|
|
960
|
+
task_id=task.get("id") or (store.get_run(run_id) or {}).get("task_id") or "",
|
|
961
|
+
task_type=spec.get("type") or "", difficulty=task.get("difficulty") or "auto",
|
|
962
|
+
role="", phase="completed", result=result,
|
|
963
|
+
verify_pass=verify_pass, review_pass=review_pass)
|
|
964
|
+
except Exception:
|
|
965
|
+
pass
|
|
907
966
|
|
|
908
967
|
|
|
909
968
|
def _run_code(run, task, agents, ev, stats, mode):
|
|
@@ -1080,8 +1139,9 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1080
1139
|
return False
|
|
1081
1140
|
|
|
1082
1141
|
def review_and_score():
|
|
1083
|
-
review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
|
|
1084
1142
|
verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
|
|
1143
|
+
# 先让确定性验证落盘,再执行模型评审;终态判断会同时使用两份证据。
|
|
1144
|
+
review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
|
|
1085
1145
|
return review_json, verify_pass, verify_ran
|
|
1086
1146
|
|
|
1087
1147
|
attempt_note = route.get("implementer", "") if mode == "auto" else ""
|
|
@@ -1099,6 +1159,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1099
1159
|
prompt = (CODE_FIX_PROMPT
|
|
1100
1160
|
.replace("__GOAL__", task["goal"])
|
|
1101
1161
|
.replace("__ISSUES__", issues_txt)
|
|
1162
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
1102
1163
|
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
1103
1164
|
res = _run_step(run_id, "fix-r%d" % round_no, modelhub.bind_agent(impl, difficulty),
|
|
1104
1165
|
prompt, workdir, readonly=False, ev=ev,
|
|
@@ -1218,6 +1279,7 @@ __GOAL__
|
|
|
1218
1279
|
__CONTEXT__
|
|
1219
1280
|
|
|
1220
1281
|
## 要求
|
|
1282
|
+
- 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
|
|
1221
1283
|
- 能改直接改、能写直接写(限本工作目录内),产出文件一律 UTF-8 编码(PowerShell 写文件显式 -Encoding UTF8)。
|
|
1222
1284
|
- 正文直接交代结果与答案:不要写「本轮做了什么」这类开场总结,也不要「回复:」这类引导词。
|
|
1223
1285
|
- 回复的最后一行单独输出一行交代结果:
|
|
@@ -1247,6 +1309,7 @@ __GOAL__
|
|
|
1247
1309
|
__CONTEXT__
|
|
1248
1310
|
|
|
1249
1311
|
## 要求
|
|
1312
|
+
- 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
|
|
1250
1313
|
- 能改直接改、能写直接写(用工具,限本工作目录内),产出文件一律 UTF-8 编码。
|
|
1251
1314
|
- 完成后直接给用户结论与答案,需要时顺带交代产出/修改了哪些文件;不要写「本轮做了什么」这类开场白。
|
|
1252
1315
|
__FOLLOWUPS__"""
|
|
@@ -1408,6 +1471,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1408
1471
|
# 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
|
|
1409
1472
|
# AI 做选题洞察;抓取失败回落普通直连提示词
|
|
1410
1473
|
prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
|
|
1474
|
+
if prompt and task.get("context"):
|
|
1475
|
+
prompt += "\n\n## 用户背景与附件\n" + task["context"]
|
|
1411
1476
|
if not prompt:
|
|
1412
1477
|
if bi is not None:
|
|
1413
1478
|
prompt = (BUILTIN_DIRECT_PROMPT
|
|
@@ -1598,6 +1663,9 @@ __GOAL__
|
|
|
1598
1663
|
## 评审汇总(各维度均分与主要问题)
|
|
1599
1664
|
__CRITIQUE__
|
|
1600
1665
|
|
|
1666
|
+
## 原始背景与附件
|
|
1667
|
+
__CONTEXT__
|
|
1668
|
+
|
|
1601
1669
|
## 要求
|
|
1602
1670
|
- 针对性改进所有 major 问题;保持既定风格与设定。
|
|
1603
1671
|
- 完成后用 3 句话说明本轮改了什么。"""
|
|
@@ -1738,6 +1806,8 @@ def _ensure_critique_placeholders(tpl):
|
|
|
1738
1806
|
tpl += "\n\n## 待评审稿件\n---\n__MANUSCRIPT__\n---"
|
|
1739
1807
|
if "__DIMKEYS__" not in tpl:
|
|
1740
1808
|
tpl = ("请按维度打分(1-10 分)。\n\n" + tpl)
|
|
1809
|
+
if "__CONTEXT__" not in tpl:
|
|
1810
|
+
tpl += "\n\n## 原始任务背景与附件参考\n__CONTEXT__"
|
|
1741
1811
|
return tpl
|
|
1742
1812
|
|
|
1743
1813
|
|
|
@@ -1774,6 +1844,9 @@ __GOAL__
|
|
|
1774
1844
|
## 本章评审意见
|
|
1775
1845
|
__CRITIQUE__
|
|
1776
1846
|
|
|
1847
|
+
## 原始背景与附件
|
|
1848
|
+
__CONTEXT__
|
|
1849
|
+
|
|
1777
1850
|
## 要求
|
|
1778
1851
|
- 针对性解决所有 major 问题,保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
|
|
1779
1852
|
|
|
@@ -1789,6 +1862,9 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
|
|
|
1789
1862
|
## 全书目标
|
|
1790
1863
|
__GOAL__
|
|
1791
1864
|
|
|
1865
|
+
## 原始背景与附件
|
|
1866
|
+
__CONTEXT__
|
|
1867
|
+
|
|
1792
1868
|
## 全文
|
|
1793
1869
|
---
|
|
1794
1870
|
__MANUSCRIPT__
|
|
@@ -1976,8 +2052,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1976
2052
|
kb = knowledge.block_for(task)
|
|
1977
2053
|
if kb:
|
|
1978
2054
|
tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
|
|
1979
|
-
return tpl.replace("__DIMKEYS__", dimkey)
|
|
1980
|
-
|
|
2055
|
+
return (tpl.replace("__DIMKEYS__", dimkey)
|
|
2056
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2057
|
+
.replace("__MANUSCRIPT__", text or "(稿件为空!)"))
|
|
1981
2058
|
|
|
1982
2059
|
# ---- 1) 大纲(断点续跑时直接继承上一遍,保证全书结构一致)
|
|
1983
2060
|
inherit = run.get("inherit") or {}
|
|
@@ -2175,8 +2252,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2175
2252
|
else:
|
|
2176
2253
|
# stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
|
|
2177
2254
|
sk_block, _ = skills.block_for(task, stable_order=True)
|
|
2178
|
-
|
|
2179
|
-
|
|
2255
|
+
if bible:
|
|
2256
|
+
sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
|
|
2257
|
+
if task.get("context"):
|
|
2258
|
+
task_ctx = "## 任务背景与附件\n" + task["context"]
|
|
2259
|
+
sk_block = (sk_block + "\n\n" + task_ctx) if sk_block else task_ctx
|
|
2180
2260
|
kb_block = knowledge.block_for(task)
|
|
2181
2261
|
if kb_block:
|
|
2182
2262
|
sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
|
|
@@ -2497,6 +2577,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2497
2577
|
.replace("__I__", str(i)).replace("__FILE__", ch_file)
|
|
2498
2578
|
.replace("__GOAL__", task["goal"])
|
|
2499
2579
|
.replace("__CRITIQUE__", "\n".join(crit_lines))
|
|
2580
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2500
2581
|
.replace("__WORDS__", str(wpc)))
|
|
2501
2582
|
_run_step(run_id, "revise-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
|
|
2502
2583
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
@@ -2553,6 +2634,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2553
2634
|
res = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
|
|
2554
2635
|
(gtpl.replace("__DIMKEYS__", dimkey)
|
|
2555
2636
|
.replace("__GOAL__", task["goal"])
|
|
2637
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2556
2638
|
.replace("__MANUSCRIPT__", full_text[:60000])),
|
|
2557
2639
|
workdir, readonly=True, ev=ev, timeout=2400)
|
|
2558
2640
|
gj = _critique_json(res, dims)
|
|
@@ -3009,6 +3091,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3009
3091
|
crit_prompt = (_ensure_critique_placeholders(
|
|
3010
3092
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
3011
3093
|
.replace("__DIMKEYS__", dimkey)
|
|
3094
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
3012
3095
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
3013
3096
|
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
3014
3097
|
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
@@ -3076,6 +3159,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3076
3159
|
prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
|
|
3077
3160
|
.replace("__ROLE__", _content_role(task))
|
|
3078
3161
|
.replace("__GOAL__", task["goal"])
|
|
3162
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
3079
3163
|
.replace("__CRITIQUE__", "\n".join(crit_lines)))
|
|
3080
3164
|
prompt += _content_contract(task)
|
|
3081
3165
|
_run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
|
|
@@ -3405,6 +3489,7 @@ def execute_run(run_id):
|
|
|
3405
3489
|
task_spec_summary=task_compile.summary(task_spec),
|
|
3406
3490
|
difficulty=task_spec["difficulty"])
|
|
3407
3491
|
task = dict(task)
|
|
3492
|
+
task = _refresh_attachment_context(task, task.get("workdir") or "")
|
|
3408
3493
|
task["_compiled_spec"] = task_spec
|
|
3409
3494
|
# 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
|
|
3410
3495
|
# 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
|
|
@@ -3515,6 +3600,33 @@ def execute_run(run_id):
|
|
|
3515
3600
|
except Exception:
|
|
3516
3601
|
pass
|
|
3517
3602
|
finally:
|
|
3603
|
+
# 全类型统一写调度终态与质量反馈。调用成功只代表传输可靠;真正用于
|
|
3604
|
+
# 在线推荐的成功率以 verify/review/publishable 等验收结果为准。
|
|
3605
|
+
try:
|
|
3606
|
+
final_run = store.get_run(run_id) or {}
|
|
3607
|
+
final_status = final_run.get("status") or ""
|
|
3608
|
+
if final_status in ("done", "failed"):
|
|
3609
|
+
final_verdict = final_run.get("verdict") or {}
|
|
3610
|
+
if "pass" in final_verdict:
|
|
3611
|
+
quality_ok = bool(final_verdict.get("pass"))
|
|
3612
|
+
elif "publishable" in final_verdict:
|
|
3613
|
+
quality_ok = bool(final_verdict.get("publishable"))
|
|
3614
|
+
else:
|
|
3615
|
+
quality_ok = final_status == "done"
|
|
3616
|
+
_record_dispatch_completed(
|
|
3617
|
+
run_id, task, "passed" if quality_ok else "failed",
|
|
3618
|
+
verify_pass=final_verdict.get("verify_pass"),
|
|
3619
|
+
review_pass=final_verdict.get(
|
|
3620
|
+
"review_pass", final_verdict.get("publishable")))
|
|
3621
|
+
final_agent = (((final_run.get("route_plan") or {})
|
|
3622
|
+
.get("implement") or {}).get("selected") or "")
|
|
3623
|
+
if final_agent.startswith("builtin:"):
|
|
3624
|
+
final_agent = "builtin"
|
|
3625
|
+
usage.record_quality_for_run(run_id, quality_ok, agent=final_agent)
|
|
3626
|
+
elif final_status == "cancelled":
|
|
3627
|
+
_record_dispatch_completed(run_id, task, "cancelled")
|
|
3628
|
+
except Exception:
|
|
3629
|
+
pass
|
|
3518
3630
|
# 任务分支收尾(git_rev 隔离链的第二半):先只读快照本 run 的全部变更
|
|
3519
3631
|
# 落 run 记录供人审,再把产物提交到 tutti/<task-id> 并切回原分支。
|
|
3520
3632
|
# 放 finally:done/failed/cancelled/异常一律保存现场;收尾自身绝不抛错,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""启动端口清场(用户拍板 2026-09-21):端口被占时先清场再启动。
|
|
3
|
+
|
|
4
|
+
- **自家旧实例**(命令行含本包 main.py 完整路径,或 npm 包内 app/main.py)
|
|
5
|
+
→ 杀树(TerminateProcess 直杀)——升级/重启最常见的占用者就是没退干净
|
|
6
|
+
的 CodeBee 自己;控制台进程对温和信号(WM_CLOSE)无反应,温和关不掉
|
|
7
|
+
正是用户「旧进程太难杀」的痛点,所以自家实例直接强杀。
|
|
8
|
+
- **别人的进程** → 只报告占用者,不发送任何信号;启动流程不得替用户
|
|
9
|
+
关闭可能正在开发中的服务。
|
|
10
|
+
- 系统/自身进程拒关(portscan 内置护栏)。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import shlex
|
|
17
|
+
import subprocess
|
|
18
|
+
|
|
19
|
+
from . import portscan, runner
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
PS_EXE = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def proc_cmdline(pid):
|
|
27
|
+
"""读进程命令行;失败返回空串。仅用于确认旧 CodeBee 实例身份。"""
|
|
28
|
+
try:
|
|
29
|
+
pid = int(pid)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return ""
|
|
32
|
+
if pid <= 0:
|
|
33
|
+
return ""
|
|
34
|
+
if os.name != "nt":
|
|
35
|
+
try:
|
|
36
|
+
with open("/proc/%d/cmdline" % pid, "rb") as fh:
|
|
37
|
+
return fh.read().decode("utf-8", "replace").replace("\x00", " ").strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
return ""
|
|
40
|
+
try:
|
|
41
|
+
r = subprocess.run(
|
|
42
|
+
[PS_EXE, "-NoProfile", "-Command",
|
|
43
|
+
"(Get-CimInstance Win32_Process -Filter 'ProcessId = %d').CommandLine" % pid],
|
|
44
|
+
capture_output=True, timeout=10)
|
|
45
|
+
return r.stdout.decode("utf-8", "replace").strip() if r.returncode == 0 else ""
|
|
46
|
+
except Exception:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _command_tokens(cmdline):
|
|
51
|
+
try:
|
|
52
|
+
return [token.strip().strip('"').strip("'")
|
|
53
|
+
for token in shlex.split(str(cmdline or ""), posix=False)]
|
|
54
|
+
except (TypeError, ValueError):
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_own_instance(cmdline, main_script, port=None):
|
|
59
|
+
"""按启动器、脚本独立参数和端口精确确认 CodeBee 实例。"""
|
|
60
|
+
tokens = _command_tokens(cmdline)
|
|
61
|
+
if len(tokens) < 2:
|
|
62
|
+
return False
|
|
63
|
+
launcher = os.path.splitext(os.path.basename(tokens[0]))[0].lower()
|
|
64
|
+
if launcher not in ("python", "python3", "py"):
|
|
65
|
+
return False
|
|
66
|
+
script = ""
|
|
67
|
+
for token in tokens[1:]:
|
|
68
|
+
if token.startswith("-"):
|
|
69
|
+
continue
|
|
70
|
+
script = token
|
|
71
|
+
break
|
|
72
|
+
if not script or not os.path.isabs(script):
|
|
73
|
+
return False
|
|
74
|
+
actual = os.path.normcase(os.path.normpath(os.path.abspath(script)))
|
|
75
|
+
expected = os.path.normcase(os.path.normpath(os.path.abspath(str(main_script))))
|
|
76
|
+
packaged = actual.replace("\\", "/").lower().endswith(
|
|
77
|
+
"/node_modules/codebee/app/main.py")
|
|
78
|
+
if actual != expected and not packaged:
|
|
79
|
+
return False
|
|
80
|
+
if port is None:
|
|
81
|
+
return True
|
|
82
|
+
try:
|
|
83
|
+
wanted = str(int(port))
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
return False
|
|
86
|
+
explicit_port = None
|
|
87
|
+
for i, token in enumerate(tokens):
|
|
88
|
+
if token == "--port" and i + 1 < len(tokens):
|
|
89
|
+
explicit_port = tokens[i + 1]
|
|
90
|
+
elif token.startswith("--port="):
|
|
91
|
+
explicit_port = token.split("=", 1)[1]
|
|
92
|
+
# argparse 默认端口可不出现在旧实例参数中;非默认端口必须显式相符。
|
|
93
|
+
return ((explicit_port == wanted) if explicit_port is not None
|
|
94
|
+
else wanted == "8765")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def clear_stale_port(port, main_script):
|
|
98
|
+
"""清掉占用端口的进程。返回 (是否清掉, 人话说明)。"""
|
|
99
|
+
holders = [p for p in portscan.listening_ports() if p.get("port") == int(port)]
|
|
100
|
+
if not holders:
|
|
101
|
+
return True, ""
|
|
102
|
+
pid = int(holders[0].get("pid") or 0)
|
|
103
|
+
if pid <= 4 or pid == os.getpid():
|
|
104
|
+
return False, "占用者是系统进程/自身,拒绝清理"
|
|
105
|
+
if is_own_instance(proc_cmdline(pid), main_script, port=port):
|
|
106
|
+
try:
|
|
107
|
+
runner._kill_tree(pid)
|
|
108
|
+
return True, "已结束旧实例 PID %d" % pid
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return False, "旧实例 PID %d 清理失败 %s" % (pid, str(e)[:80])
|
|
111
|
+
return False, "占用者非 CodeBee(PID %d),未自动关闭" % pid
|