codebee 0.1.65 → 0.1.67

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 (116) hide show
  1. package/CHANGELOG.md +532 -513
  2. package/LICENSE +21 -21
  3. package/README.md +494 -487
  4. package/app/core/acceptance.py +100 -0
  5. package/app/core/aiflavor.py +197 -197
  6. package/app/core/artifacts.py +148 -148
  7. package/app/core/assets.py +110 -0
  8. package/app/core/attachments.py +623 -532
  9. package/app/core/automation.py +758 -758
  10. package/app/core/backup.py +470 -470
  11. package/app/core/beekeeper.py +183 -183
  12. package/app/core/board.py +209 -209
  13. package/app/core/bookmeta.py +930 -759
  14. package/app/core/bookmeta_catalog.py +180 -180
  15. package/app/core/branching.py +137 -137
  16. package/app/core/builtin_agent.py +1234 -1191
  17. package/app/core/capability.py +144 -144
  18. package/app/core/catalog.py +23 -24
  19. package/app/core/chaptersafe.py +32 -32
  20. package/app/core/cleanup.py +303 -303
  21. package/app/core/compaction.py +201 -186
  22. package/app/core/covergen.py +356 -356
  23. package/app/core/defectretro.py +43 -0
  24. package/app/core/diagnostics.py +114 -114
  25. package/app/core/dispatch.py +173 -173
  26. package/app/core/dispatch_log.py +113 -113
  27. package/app/core/env_scrub.py +85 -85
  28. package/app/core/error_codes.py +220 -65
  29. package/app/core/errorlog.py +137 -137
  30. package/app/core/evaluation.py +174 -0
  31. package/app/core/failure_notify.py +290 -0
  32. package/app/core/flows.py +401 -355
  33. package/app/core/goal_service.py +167 -167
  34. package/app/core/health.py +374 -342
  35. package/app/core/history.py +38 -38
  36. package/app/core/hooks.py +162 -156
  37. package/app/core/jobs.py +1050 -1022
  38. package/app/core/knowledge.py +56 -5
  39. package/app/core/manager.py +2122 -1996
  40. package/app/core/market.py +362 -362
  41. package/app/core/market_remote.py +190 -92
  42. package/app/core/mocks.py +64 -64
  43. package/app/core/modelhub.py +3546 -3332
  44. package/app/core/notify.py +106 -106
  45. package/app/core/operations.py +228 -0
  46. package/app/core/paihang.py +138 -109
  47. package/app/core/paths.py +2 -2
  48. package/app/core/pipeline.py +4808 -4494
  49. package/app/core/planner.py +37 -32
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/preview.py +293 -293
  52. package/app/core/publish/__init__.py +7 -7
  53. package/app/core/publish/auto.py +367 -367
  54. package/app/core/publish/browser.py +539 -533
  55. package/app/core/publish/fanqie.py +159 -128
  56. package/app/core/publish/flow.py +440 -406
  57. package/app/core/publish/flows-fanqie-calibrated.json +17 -9
  58. package/app/core/publish/flows-qimao-calibrated.json +6 -2
  59. package/app/core/publish/ledger.py +203 -198
  60. package/app/core/publish/manager.py +692 -590
  61. package/app/core/publish/qimao.py +141 -141
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/redact.py +38 -38
  64. package/app/core/repeat_guard.py +129 -124
  65. package/app/core/revisions.py +51 -0
  66. package/app/core/router.py +119 -81
  67. package/app/core/runner.py +2065 -2042
  68. package/app/core/selfupdate.py +384 -384
  69. package/app/core/session_log.py +161 -161
  70. package/app/core/sessions.py +312 -312
  71. package/app/core/settings.py +149 -149
  72. package/app/core/settings_schema.py +262 -262
  73. package/app/core/share_page.py +114 -114
  74. package/app/core/skill_scan.py +114 -106
  75. package/app/core/skillpacks/fanqie-novel.md +80 -80
  76. package/app/core/skillpacks/market/character-bible.md +66 -66
  77. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  78. package/app/core/skillpacks/market/git-workflow.md +57 -57
  79. package/app/core/skillpacks/market/release-notes.md +72 -72
  80. package/app/core/skillpacks/market/weekly-report.md +71 -71
  81. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  82. package/app/core/skills.py +315 -73
  83. package/app/core/step_runner.py +132 -132
  84. package/app/core/store.py +230 -11
  85. package/app/core/task_compile.py +199 -199
  86. package/app/core/telemetry.py +292 -292
  87. package/app/core/tlsctx.py +64 -64
  88. package/app/core/token_meter.py +147 -147
  89. package/app/core/upstream.py +41 -0
  90. package/app/core/usage.py +114 -3
  91. package/app/core/volumes.py +389 -355
  92. package/app/core/wxdigest.py +858 -858
  93. package/app/core/zentao.py +2465 -2447
  94. package/app/main.py +2957 -2832
  95. package/app/pick_dialog.py +110 -110
  96. package/app/ui/app.js +14384 -13977
  97. package/app/ui/artdiff.js +74 -0
  98. package/app/ui/board.css +20 -41
  99. package/app/ui/board.html +162 -162
  100. package/app/ui/board.js +305 -305
  101. package/app/ui/i18n.js +2766 -2711
  102. package/app/ui/icons/bee.svg +79 -79
  103. package/app/ui/index.html +7 -1
  104. package/app/ui/manifest.json +16 -16
  105. package/app/ui/qrcode.js +2297 -2297
  106. package/app/ui/style.css +6179 -6125
  107. package/bin/tutti.js +147 -147
  108. package/package.json +46 -41
  109. package/tools/wx_reader.py +261 -261
  110. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  111. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  112. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  113. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  114. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  115. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  116. 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)
@@ -133,6 +133,8 @@ CODE_PLAN_PROMPT = """你是技术负责人。请先判定任务难度,再按
133
133
  子任务拆分:easy 一律只拆 1 个子任务——一个子任务把目标完整做完并自检通过即收尾,
134
134
  禁止拆出独立的核查/联调/收尾子任务;只有 hard 才允许多个子任务(至多 __N__ 个),
135
135
  粒度要可独立验证,且最后一个子任务必须包含整体联调/收尾。
136
+ hard 也不做任务没要的防御性扩展子任务(缓存/重试/监控/通用抽象层)——
137
+ 发现真实风险写进对应子任务的 detail 里备注,由人决定要不要做。
136
138
 
137
139
  ## 先探索再计划(重要,借鉴 OpenSpec explore)
138
140
  拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
@@ -248,7 +250,8 @@ def _norm_chapters(data, n, vol_per=0, vol_start=1, vol_plan=None):
248
250
  # 分卷:只收本批真正用到的卷的卷名/卷弧光(模型可能多写或乱写卷号,一律按
249
251
  # 本批章号范围过滤)。卷边界不在这里——它由 volumes.build_plan 从章号推导。
250
252
  if vol_plan:
251
- named = volumes.norm_volumes(data.get("volumes"), vol_per, vol_start, n)
253
+ named = volumes.norm_volumes(data.get("volumes"), vol_per, vol_start, n,
254
+ plan=vol_plan)
252
255
  if named:
253
256
  res["volumes"] = named
254
257
  return res
@@ -355,8 +358,8 @@ def book_volume_plan(task, outline, upto):
355
358
  return volumes.merge_titles(plan, named)
356
359
 
357
360
 
358
- def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
359
- deadline=None):
361
+ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
362
+ deadline=None, run_id=None):
360
363
  """连载大纲:编排者 API 优先 → 作者 CLI → 模板。返回 {book_title, chapters:[{title,beats,hook}]}。
361
364
 
362
365
  续写批次(serial.start_chapter > 1):改用续写大纲提示词,注入前情大纲与
@@ -368,7 +371,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
368
371
  n = int(serial.get("chapters") or 8)
369
372
  wpc = int(serial.get("words_per_chapter") or 2500)
370
373
  start = int(serial.get("start_chapter") or 1)
371
- sk_block, _ = skills.block_for(task)
374
+ sk_block, _ = skills.block_for(task, run_id=run_id)
372
375
  kb_block = knowledge.block_for(task)
373
376
  if kb_block:
374
377
  sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
@@ -453,8 +456,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
453
456
  # prompt 逐字节相同,命中省一次全量规划调用。第 2 次尝试故意
454
457
  # 旁路缓存——首试 ok 但 JSON 不合法时,重试要的是「再抽一次样
455
458
  # 本」而不是同一份坏文本。
456
- res = modelhub.chat(prov["id"], model, prompt,
457
- max_tokens=16000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
459
+ res = modelhub.chat(prov["id"], model, prompt,
460
+ max_tokens=16000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
458
461
  reasoning_effort=_reasoning_effort(task),
459
462
  cache_ttl=3600 if _attempt == 1 else 0)
460
463
  cb.flush()
@@ -483,8 +486,8 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
483
486
  _append_log(log_path, "===== 作者 CLI(%s)=====" % author_agent.get("id", "?"))
484
487
  res = runner.run_agent(modelhub.bind_agent(author_agent), prompt,
485
488
  workdir=workdir or task.get("workdir"), readonly=True,
486
- timeout=_deadline_timeout(deadline, _outline_timeout()),
487
- deadline=deadline, cancel_event=ev,
489
+ timeout=_deadline_timeout(deadline, _outline_timeout()),
490
+ deadline=deadline, cancel_event=ev,
488
491
  log_path=log_path)
489
492
  _log_usage("outline", "outline", task, res, agent=author_agent)
490
493
  if not res["ok"]:
@@ -602,13 +605,13 @@ def _reasoning_effort(task):
602
605
  return "medium"
603
606
 
604
607
 
605
- def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None,
606
- deadline=None):
608
+ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None,
609
+ deadline=None):
607
610
  """代码任务计划:编排者 API 优先 → CLI 智能体 → 单步模板。"""
608
611
  orch = _orchestrator()
609
612
  if orch:
610
- plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path,
611
- deadline=deadline)
613
+ plan = _orch_code_plan(task, orch[0], orch[1], log_path=log_path,
614
+ deadline=deadline)
612
615
  if plan:
613
616
  return plan
614
617
  if planner_agent is None:
@@ -620,8 +623,8 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
620
623
  .replace("__CONTEXT__", task.get("context") or "(无)")
621
624
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)"))
622
625
  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,
626
+ timeout=_deadline_timeout(deadline, 300), deadline=deadline,
627
+ cancel_event=ev, resume=resume,
625
628
  log_path=log_path)
626
629
  _log_usage("plan", "plan", task, res, agent=planner_agent)
627
630
  data = runner.extract_json(res.get("text") or "")
@@ -638,7 +641,7 @@ def _fallback_code_plan(task, note):
638
641
  "steps": [{"title": "实现任务", "detail": task["goal"]}]}
639
642
 
640
643
 
641
- def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
644
+ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
642
645
  cb = _log_streamer(log_path)
643
646
  # §07 T2.2:代码计划同任务同 prompt(断点续跑/重试),TTL 1h 精确缓存
644
647
  res = modelhub.chat(prov["id"], model,
@@ -646,12 +649,13 @@ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
646
649
  .replace("__GOAL__", task["goal"])
647
650
  .replace("__CONTEXT__", task.get("context") or "(无)")
648
651
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
649
- max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
652
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
650
653
  reasoning_effort=_reasoning_effort(task),
651
654
  cache_ttl=3600)
652
655
  cb.flush()
653
656
  _log_usage("plan", "plan", task, res, model=model,
654
- provider=prov.get("name", prov.get("id", "")))
657
+ provider=prov.get("name", prov.get("id", "")),
658
+ provider_id=prov.get("id", ""))
655
659
  if not res["ok"]:
656
660
  _append_log(log_path, "编排者计划(%s · %s)失败:%s" % (
657
661
  prov.get("name", prov["id"]), model, (res.get("error") or "未知错误")[:300]))
@@ -666,7 +670,7 @@ def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
666
670
  "workflow": _plan_workflow(data)}
667
671
 
668
672
 
669
- def make_review_outline(task, deadline=None):
673
+ def make_review_outline(task, deadline=None):
670
674
  """review 类任务:编排者产出写作大纲(失败返回 None,起草退回无大纲)。"""
671
675
  orch = _orchestrator()
672
676
  if not orch:
@@ -676,11 +680,12 @@ def make_review_outline(task, deadline=None):
676
680
  (REVIEW_OUTLINE_PROMPT
677
681
  .replace("__GOAL__", task["goal"])
678
682
  .replace("__CONTEXT__", task.get("context") or "(无)")),
679
- max_tokens=8000, timeout=_deadline_timeout(deadline, 300),
683
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 300),
680
684
  reasoning_effort=_reasoning_effort(task),
681
685
  cache_ttl=3600) # §07 T2.2:同任务重试幂等,TTL 1h
682
686
  _log_usage("outline", "outline", task, res, model=model,
683
- provider=prov.get("name", prov.get("id", "")))
687
+ provider=prov.get("name", prov.get("id", "")),
688
+ provider_id=prov.get("id", ""))
684
689
  if not res["ok"]:
685
690
  return None
686
691
  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