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.
@@ -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).replace(
1980
- "__MANUSCRIPT__", text or "(稿件为空!)")
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
- if bible:
2179
- sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
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