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.
- package/CHANGELOG.md +532 -513
- package/LICENSE +21 -21
- package/README.md +494 -487
- package/app/core/acceptance.py +100 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +623 -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 +930 -759
- 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/defectretro.py +43 -0
- 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 +401 -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 +1050 -1022
- package/app/core/knowledge.py +56 -5
- package/app/core/manager.py +2122 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +190 -92
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3546 -3332
- package/app/core/notify.py +106 -106
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +138 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +4808 -4494
- package/app/core/planner.py +37 -32
- 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 +539 -533
- package/app/core/publish/fanqie.py +159 -128
- package/app/core/publish/flow.py +440 -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 +203 -198
- package/app/core/publish/manager.py +692 -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/revisions.py +51 -0
- package/app/core/router.py +119 -81
- package/app/core/runner.py +2065 -2042
- 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 +315 -73
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +230 -11
- 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 +389 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2465 -2447
- package/app/main.py +2957 -2832
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +14384 -13977
- package/app/ui/artdiff.js +74 -0
- 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 +2766 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +7 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6179 -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)
|
|
@@ -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 "")
|
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
|