codebee 0.1.64 → 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.
- package/CHANGELOG.md +520 -507
- package/LICENSE +21 -21
- package/README.md +494 -487
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/attachments.py +532 -532
- package/app/core/automation.py +758 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +272 -217
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1234 -1191
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +23 -24
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -186
- package/app/core/covergen.py +356 -356
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +113 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +220 -65
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -0
- package/app/core/failure_notify.py +290 -0
- package/app/core/flows.py +355 -355
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -342
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -156
- package/app/core/jobs.py +1034 -1022
- package/app/core/knowledge.py +6 -2
- package/app/core/manager.py +2115 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +66 -66
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3517 -3332
- package/app/core/notify.py +106 -106
- package/app/core/paihang.py +109 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +4614 -4483
- package/app/core/planner.py +33 -31
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +533 -533
- package/app/core/publish/fanqie.py +128 -128
- package/app/core/publish/flow.py +432 -406
- package/app/core/publish/flows-fanqie-calibrated.json +17 -9
- package/app/core/publish/flows-qimao-calibrated.json +6 -2
- package/app/core/publish/ledger.py +198 -198
- package/app/core/publish/manager.py +623 -590
- package/app/core/publish/qimao.py +141 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -124
- package/app/core/router.py +96 -22
- package/app/core/runner.py +2050 -2019
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +149 -149
- package/app/core/settings_schema.py +262 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +114 -106
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skills.py +221 -72
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +23 -0
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/upstream.py +41 -0
- package/app/core/usage.py +114 -3
- package/app/core/volumes.py +355 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2447 -2447
- package/app/main.py +2838 -2832
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +13994 -13977
- package/app/ui/board.css +20 -41
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2716 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +1 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6107 -6125
- package/bin/tutti.js +147 -147
- package/package.json +46 -41
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
package/app/core/planner.py
CHANGED
|
@@ -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 "")
|
package/app/core/portguard.py
CHANGED
|
@@ -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
|