codebee 0.1.65 → 0.1.66

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +520 -513
  2. package/LICENSE +21 -21
  3. package/README.md +494 -487
  4. package/app/core/aiflavor.py +197 -197
  5. package/app/core/artifacts.py +148 -148
  6. package/app/core/attachments.py +532 -532
  7. package/app/core/automation.py +758 -758
  8. package/app/core/backup.py +470 -470
  9. package/app/core/beekeeper.py +183 -183
  10. package/app/core/board.py +209 -209
  11. package/app/core/bookmeta.py +796 -759
  12. package/app/core/bookmeta_catalog.py +180 -180
  13. package/app/core/branching.py +137 -137
  14. package/app/core/builtin_agent.py +1234 -1191
  15. package/app/core/capability.py +144 -144
  16. package/app/core/catalog.py +23 -24
  17. package/app/core/chaptersafe.py +32 -32
  18. package/app/core/cleanup.py +303 -303
  19. package/app/core/compaction.py +201 -186
  20. package/app/core/covergen.py +356 -356
  21. package/app/core/diagnostics.py +114 -114
  22. package/app/core/dispatch.py +173 -173
  23. package/app/core/dispatch_log.py +113 -113
  24. package/app/core/env_scrub.py +85 -85
  25. package/app/core/error_codes.py +220 -65
  26. package/app/core/errorlog.py +137 -137
  27. package/app/core/evaluation.py +174 -0
  28. package/app/core/failure_notify.py +290 -0
  29. package/app/core/flows.py +355 -355
  30. package/app/core/goal_service.py +167 -167
  31. package/app/core/health.py +374 -342
  32. package/app/core/history.py +38 -38
  33. package/app/core/hooks.py +162 -156
  34. package/app/core/jobs.py +1034 -1022
  35. package/app/core/knowledge.py +6 -2
  36. package/app/core/manager.py +2115 -1996
  37. package/app/core/market.py +362 -362
  38. package/app/core/market_remote.py +66 -66
  39. package/app/core/mocks.py +64 -64
  40. package/app/core/modelhub.py +3517 -3332
  41. package/app/core/notify.py +106 -106
  42. package/app/core/paihang.py +109 -109
  43. package/app/core/paths.py +2 -2
  44. package/app/core/pipeline.py +4614 -4494
  45. package/app/core/planner.py +33 -31
  46. package/app/core/portguard.py +111 -111
  47. package/app/core/preview.py +293 -293
  48. package/app/core/publish/__init__.py +7 -7
  49. package/app/core/publish/auto.py +367 -367
  50. package/app/core/publish/browser.py +533 -533
  51. package/app/core/publish/fanqie.py +128 -128
  52. package/app/core/publish/flow.py +432 -406
  53. package/app/core/publish/flows-fanqie-calibrated.json +17 -9
  54. package/app/core/publish/flows-qimao-calibrated.json +6 -2
  55. package/app/core/publish/ledger.py +198 -198
  56. package/app/core/publish/manager.py +623 -590
  57. package/app/core/publish/qimao.py +141 -141
  58. package/app/core/publish/ws.py +139 -139
  59. package/app/core/redact.py +38 -38
  60. package/app/core/repeat_guard.py +129 -124
  61. package/app/core/router.py +119 -81
  62. package/app/core/runner.py +2050 -2042
  63. package/app/core/selfupdate.py +384 -384
  64. package/app/core/session_log.py +161 -161
  65. package/app/core/sessions.py +312 -312
  66. package/app/core/settings.py +149 -149
  67. package/app/core/settings_schema.py +262 -262
  68. package/app/core/share_page.py +114 -114
  69. package/app/core/skill_scan.py +114 -106
  70. package/app/core/skillpacks/fanqie-novel.md +80 -80
  71. package/app/core/skillpacks/market/character-bible.md +66 -66
  72. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  73. package/app/core/skillpacks/market/git-workflow.md +57 -57
  74. package/app/core/skillpacks/market/release-notes.md +72 -72
  75. package/app/core/skillpacks/market/weekly-report.md +71 -71
  76. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  77. package/app/core/skills.py +221 -72
  78. package/app/core/step_runner.py +132 -132
  79. package/app/core/store.py +23 -0
  80. package/app/core/task_compile.py +199 -199
  81. package/app/core/telemetry.py +292 -292
  82. package/app/core/tlsctx.py +64 -64
  83. package/app/core/token_meter.py +147 -147
  84. package/app/core/upstream.py +41 -0
  85. package/app/core/usage.py +114 -3
  86. package/app/core/volumes.py +355 -355
  87. package/app/core/wxdigest.py +858 -858
  88. package/app/core/zentao.py +2447 -2447
  89. package/app/main.py +2838 -2832
  90. package/app/pick_dialog.py +110 -110
  91. package/app/ui/app.js +13994 -13977
  92. package/app/ui/board.css +20 -41
  93. package/app/ui/board.html +162 -162
  94. package/app/ui/board.js +305 -305
  95. package/app/ui/i18n.js +2716 -2711
  96. package/app/ui/icons/bee.svg +79 -79
  97. package/app/ui/index.html +1 -1
  98. package/app/ui/manifest.json +16 -16
  99. package/app/ui/qrcode.js +2297 -2297
  100. package/app/ui/style.css +6107 -6125
  101. package/bin/tutti.js +147 -147
  102. package/package.json +46 -41
  103. package/tools/wx_reader.py +261 -261
  104. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  105. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  106. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  107. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  108. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  109. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  110. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -69,7 +69,7 @@ def _log_streamer(log_path, min_chars=400, min_secs=0.8):
69
69
  return cb
70
70
 
71
71
 
72
- def _outline_timeout():
72
+ def _outline_timeout():
73
73
  """大纲/规划的 CLI 兜底超时(秒):env TUTTI_OUTLINE_TIMEOUT(秒)优先,
74
74
  其次设置 orchestrator.outline_timeout_s,缺省 900。"""
75
75
  try:
@@ -86,16 +86,16 @@ def _outline_timeout():
86
86
  return v
87
87
  except Exception:
88
88
  pass
89
- return DEFAULT_OUTLINE_TIMEOUT
90
-
91
-
92
- def _deadline_timeout(deadline, requested):
93
- if deadline is None:
94
- return requested
95
- try:
96
- return max(1, min(float(requested), deadline - time.monotonic()))
97
- except (TypeError, ValueError):
98
- return requested
89
+ return DEFAULT_OUTLINE_TIMEOUT
90
+
91
+
92
+ def _deadline_timeout(deadline, requested):
93
+ if deadline is None:
94
+ return requested
95
+ try:
96
+ return max(1, min(float(requested), deadline - time.monotonic()))
97
+ except (TypeError, ValueError):
98
+ return requested
99
99
 
100
100
 
101
101
  def _log_usage(source, role, task, res, agent=None, tool="", model="", provider="",
@@ -113,7 +113,7 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
113
113
  cost_usd=float((res or {}).get("cost_usd") or 0.0),
114
114
  usage=(res or {}).get("usage"))
115
115
  # 告警模块:编排者直连调用的成功/失败上报(provider_id 供「禁用厂商」定位)
116
- if provider:
116
+ if provider and not runner.attempt_cancelled(res):
117
117
  from . import health
118
118
  if (res or {}).get("ok"):
119
119
  health.report_success(provider)
@@ -355,8 +355,8 @@ def book_volume_plan(task, outline, upto):
355
355
  return volumes.merge_titles(plan, named)
356
356
 
357
357
 
358
- def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
359
- deadline=None):
358
+ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
359
+ deadline=None, run_id=None):
360
360
  """连载大纲:编排者 API 优先 → 作者 CLI → 模板。返回 {book_title, chapters:[{title,beats,hook}]}。
361
361
 
362
362
  续写批次(serial.start_chapter > 1):改用续写大纲提示词,注入前情大纲与
@@ -368,7 +368,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
368
368
  n = int(serial.get("chapters") or 8)
369
369
  wpc = int(serial.get("words_per_chapter") or 2500)
370
370
  start = int(serial.get("start_chapter") or 1)
371
- sk_block, _ = skills.block_for(task)
371
+ sk_block, _ = skills.block_for(task, run_id=run_id)
372
372
  kb_block = knowledge.block_for(task)
373
373
  if kb_block:
374
374
  sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
@@ -453,8 +453,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
453
453
  # prompt 逐字节相同,命中省一次全量规划调用。第 2 次尝试故意
454
454
  # 旁路缓存——首试 ok 但 JSON 不合法时,重试要的是「再抽一次样
455
455
  # 本」而不是同一份坏文本。
456
- res = modelhub.chat(prov["id"], model, prompt,
457
- max_tokens=16000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
456
+ res = modelhub.chat(prov["id"], model, prompt,
457
+ max_tokens=16000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
458
458
  reasoning_effort=_reasoning_effort(task),
459
459
  cache_ttl=3600 if _attempt == 1 else 0)
460
460
  cb.flush()
@@ -483,8 +483,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
483
483
  _append_log(log_path, "===== 作者 CLI(%s)=====" % author_agent.get("id", "?"))
484
484
  res = runner.run_agent(modelhub.bind_agent(author_agent), prompt,
485
485
  workdir=workdir or task.get("workdir"), readonly=True,
486
- timeout=_deadline_timeout(deadline, _outline_timeout()),
487
- deadline=deadline, cancel_event=ev,
486
+ timeout=_deadline_timeout(deadline, _outline_timeout()),
487
+ deadline=deadline, cancel_event=ev,
488
488
  log_path=log_path)
489
489
  _log_usage("outline", "outline", task, res, agent=author_agent)
490
490
  if not res["ok"]:
@@ -602,13 +602,13 @@ def _reasoning_effort(task):
602
602
  return "medium"
603
603
 
604
604
 
605
- def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None,
606
- deadline=None):
605
+ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None,
606
+ deadline=None):
607
607
  """代码任务计划:编排者 API 优先 → CLI 智能体 → 单步模板。"""
608
608
  orch = _orchestrator()
609
609
  if orch:
610
- plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path,
611
- deadline=deadline)
610
+ plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path,
611
+ deadline=deadline)
612
612
  if plan:
613
613
  return plan
614
614
  if planner_agent is None:
@@ -620,8 +620,8 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
620
620
  .replace("__CONTEXT__", task.get("context") or "(无)")
621
621
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)"))
622
622
  res = runner.run_agent(planner_agent, prompt, workdir=workdir, readonly=True,
623
- timeout=_deadline_timeout(deadline, 300), deadline=deadline,
624
- cancel_event=ev, resume=resume,
623
+ timeout=_deadline_timeout(deadline, 300), deadline=deadline,
624
+ cancel_event=ev, resume=resume,
625
625
  log_path=log_path)
626
626
  _log_usage("plan", "plan", task, res, agent=planner_agent)
627
627
  data = runner.extract_json(res.get("text") or "")
@@ -638,7 +638,7 @@ def _fallback_code_plan(task, note):
638
638
  "steps": [{"title": "实现任务", "detail": task["goal"]}]}
639
639
 
640
640
 
641
- def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
641
+ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
642
642
  cb = _log_streamer(log_path)
643
643
  # §07 T2.2:代码计划同任务同 prompt(断点续跑/重试),TTL 1h 精确缓存
644
644
  res = modelhub.chat(prov["id"], model,
@@ -646,12 +646,13 @@ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
646
646
  .replace("__GOAL__", task["goal"])
647
647
  .replace("__CONTEXT__", task.get("context") or "(无)")
648
648
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
649
- max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
649
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
650
650
  reasoning_effort=_reasoning_effort(task),
651
651
  cache_ttl=3600)
652
652
  cb.flush()
653
653
  _log_usage("plan", "plan", task, res, model=model,
654
- provider=prov.get("name", prov.get("id", "")))
654
+ provider=prov.get("name", prov.get("id", "")),
655
+ provider_id=prov.get("id", ""))
655
656
  if not res["ok"]:
656
657
  _append_log(log_path, "编排者计划(%s · %s)失败:%s" % (
657
658
  prov.get("name", prov["id"]), model, (res.get("error") or "未知错误")[:300]))
@@ -666,7 +667,7 @@ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
666
667
  "workflow": _plan_workflow(data)}
667
668
 
668
669
 
669
- def make_review_outline(task, deadline=None):
670
+ def make_review_outline(task, deadline=None):
670
671
  """review 类任务:编排者产出写作大纲(失败返回 None,起草退回无大纲)。"""
671
672
  orch = _orchestrator()
672
673
  if not orch:
@@ -676,11 +677,12 @@ def make_review_outline(task, deadline=None):
676
677
  (REVIEW_OUTLINE_PROMPT
677
678
  .replace("__GOAL__", task["goal"])
678
679
  .replace("__CONTEXT__", task.get("context") or "(无)")),
679
- max_tokens=8000, timeout=_deadline_timeout(deadline, 300),
680
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300),
680
681
  reasoning_effort=_reasoning_effort(task),
681
682
  cache_ttl=3600) # §07 T2.2:同任务重试幂等,TTL 1h
682
683
  _log_usage("outline", "outline", task, res, model=model,
683
- provider=prov.get("name", prov.get("id", "")))
684
+ provider=prov.get("name", prov.get("id", "")),
685
+ provider_id=prov.get("id", ""))
684
686
  if not res["ok"]:
685
687
  return None
686
688
  data = runner.extract_json(res.get("text") or "")
@@ -1,111 +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
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