codebee 0.1.1 → 0.1.2
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/README.md +413 -413
- package/app/core/flows.py +1 -1
- package/app/core/gitmod.py +953 -953
- package/app/core/manager.py +56 -7
- package/app/core/selfupdate.py +170 -170
- package/app/ui/app.js +8045 -8032
- package/app/ui/i18n.js +1712 -1711
- package/app/ui/index.html +1 -0
- package/app/ui/style.css +2736 -2733
- package/bin/tutti.js +147 -147
- package/package.json +39 -39
package/app/core/manager.py
CHANGED
|
@@ -28,7 +28,8 @@ _LOCK = threading.RLock()
|
|
|
28
28
|
_STATE = {"detected": {}, "versions": {}, "detect_ts": 0.0, "detect_ev": None}
|
|
29
29
|
|
|
30
30
|
# 能自动写入默认模型的 config.format(其余格式只能手动编辑)
|
|
31
|
-
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "
|
|
31
|
+
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "jsonc",
|
|
32
|
+
"yaml-line")
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def _expand(p):
|
|
@@ -401,7 +402,7 @@ def read_model(entry):
|
|
|
401
402
|
if cfg["format"] == "jsonc":
|
|
402
403
|
keys = (cfg.get("model_key") or "model").split(".")
|
|
403
404
|
try:
|
|
404
|
-
data = json.loads(
|
|
405
|
+
data = json.loads(_jsonc_strip_comments(text) or "{}")
|
|
405
406
|
except Exception:
|
|
406
407
|
return None
|
|
407
408
|
return _json_path_get(data, keys)
|
|
@@ -418,7 +419,7 @@ def read_model(entry):
|
|
|
418
419
|
|
|
419
420
|
def write_model(entry, model):
|
|
420
421
|
"""写入默认模型(改动前自动备份 .bak)。支持 toml-line / toml-section /
|
|
421
|
-
json / json-path / yaml-line。"""
|
|
422
|
+
json / json-path / jsonc / yaml-line。"""
|
|
422
423
|
path = _config_path(entry)
|
|
423
424
|
cfg = entry.get("config") or {}
|
|
424
425
|
fmt = cfg.get("format")
|
|
@@ -447,8 +448,19 @@ def write_model(entry, model):
|
|
|
447
448
|
# settings.json、openclaw 缺失即安全默认——官方文档明确「缺失即
|
|
448
449
|
# 内置默认」);它正是用户层覆盖的落点,缺了按需创建
|
|
449
450
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
450
|
-
Path(path).write_bytes(b"{}" if fmt in ("json", "json-path") else b"")
|
|
451
|
-
if fmt == "
|
|
451
|
+
Path(path).write_bytes(b"{}" if fmt in ("json", "json-path", "jsonc") else b"")
|
|
452
|
+
if fmt == "jsonc":
|
|
453
|
+
# mimo(mimocode.jsonc)有注释,整体重解析会丢注释——复用
|
|
454
|
+
# _jsonc_set 做就地片段改写(只动目标键,其余原样保留)
|
|
455
|
+
keys = (cfg.get("model_key") or "model").split(".")
|
|
456
|
+
text = open(path, encoding="utf-8", errors="replace", newline="").read()
|
|
457
|
+
new_text, ok = _jsonc_set(text, tuple(keys),
|
|
458
|
+
json.dumps(model, ensure_ascii=False))
|
|
459
|
+
if not ok:
|
|
460
|
+
return {"ok": False,
|
|
461
|
+
"error": "jsonc 结构异常,未能就地写入 %s(已避免覆盖)" % path}
|
|
462
|
+
Path(path).write_bytes(new_text.encode("utf-8"))
|
|
463
|
+
elif fmt == "toml-line":
|
|
452
464
|
text = open(path, encoding="utf-8", errors="replace").read()
|
|
453
465
|
new_line = 'model = "%s"' % model
|
|
454
466
|
if re.search(r'(?m)^\s*model\s*=\s*"[^"]*"', text):
|
|
@@ -882,14 +894,51 @@ def _jsonc_scan_object(text, start):
|
|
|
882
894
|
j += 1
|
|
883
895
|
keys[-1][4] = j
|
|
884
896
|
i = j
|
|
885
|
-
|
|
897
|
+
state = "key"
|
|
886
898
|
return n, keys
|
|
887
899
|
|
|
888
900
|
|
|
901
|
+
def _jsonc_strip_comments(text):
|
|
902
|
+
"""剥掉 jsonc 的 // 行注释与 /* */ 块注释,供 json.loads 解析。
|
|
903
|
+
字符串字面量内部的 //($schema 的 https:// 等)不剥——逐字符扫描;
|
|
904
|
+
简单的 re.sub(r"//[^\\n]*") 会把 URL 截断导致解析失败。"""
|
|
905
|
+
out = []
|
|
906
|
+
i, n = 0, len(text)
|
|
907
|
+
while i < n:
|
|
908
|
+
ch = text[i]
|
|
909
|
+
if ch == '"':
|
|
910
|
+
j = i + 1
|
|
911
|
+
while j < n:
|
|
912
|
+
if text[j] == "\\":
|
|
913
|
+
j += 2
|
|
914
|
+
continue
|
|
915
|
+
if text[j] == '"':
|
|
916
|
+
break
|
|
917
|
+
j += 1
|
|
918
|
+
out.append(text[i:min(j + 1, n)])
|
|
919
|
+
i = j + 1
|
|
920
|
+
continue
|
|
921
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "/":
|
|
922
|
+
e = text.find("\n", i)
|
|
923
|
+
i = n if e < 0 else e # 行注释:换行符本身保留
|
|
924
|
+
continue
|
|
925
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "*":
|
|
926
|
+
e = text.find("*/", i + 2)
|
|
927
|
+
i = n if e < 0 else e + 2
|
|
928
|
+
out.append(" ")
|
|
929
|
+
continue
|
|
930
|
+
out.append(ch)
|
|
931
|
+
i += 1
|
|
932
|
+
return "".join(out)
|
|
933
|
+
|
|
934
|
+
|
|
889
935
|
def _jsonc_set(text, path, value_json):
|
|
890
936
|
"""JSON(C) 顶层就地写键:path=("provider","orch") 或 ("model",)。
|
|
891
|
-
只动目标片段,其余文本(含注释与原格式)原样保留。返回 (new_text, ok)。
|
|
937
|
+
只动目标片段,其余文本(含注释与原格式)原样保留。返回 (new_text, ok)。
|
|
938
|
+
空文件按空对象起笔(write_model 缺文件按需创建的产物)。"""
|
|
892
939
|
brace = text.find("{")
|
|
940
|
+
if brace < 0 and not text.strip():
|
|
941
|
+
return ('{\n "%s": %s\n}' % (path[0], value_json), True)
|
|
893
942
|
if brace < 0:
|
|
894
943
|
return text, False
|
|
895
944
|
end, keys = _jsonc_scan_object(text, brace)
|
package/app/core/selfupdate.py
CHANGED
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""Tutti 自更新:版本读取 / 新版本检测 / 一键升级 / 就地重启。
|
|
3
|
-
|
|
4
|
-
安装模式判定(mode):
|
|
5
|
-
npm — 运行副本位于 node_modules 下且带 package.json(npm 全局安装的包),
|
|
6
|
-
可查新、可升级、可重启;
|
|
7
|
-
repo — 带 .git 的开发仓库:**永不自动升级**(会覆盖开发中的代码),提示走 git pull;
|
|
8
|
-
other — 裸源码拷贝,提示手动替换。
|
|
9
|
-
|
|
10
|
-
升级 = 在标准 mgmt run 里跑 `npm install -g codebee@latest`(日志实时落盘、
|
|
11
|
-
SSE 可看进度)。npm 替换的是包目录文件,当前进程已加载进内存不受影响,装完后由
|
|
12
|
-
「重启」换新代码:新进程先等旧端口释放再 bind(Windows SO_REUSEADDR 允许双 LISTEN
|
|
13
|
-
同时存在,必须先验旧进程真退了),旧进程发送完重启响应后自退。
|
|
14
|
-
|
|
15
|
-
安全:两处子进程命令的 argv 均为**行内字面量列表**(可执行文件与全部参数不来自任何
|
|
16
|
-
外部输入;重启仅透传 argparse 校验过的整型端口),shell 全程 False,绝不拼接用户输入。
|
|
17
|
-
改发布名时(见 test_selfupdate 与 package.json 的一致性断言)同步改 _PKG_NAME。
|
|
18
|
-
"""
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import os
|
|
23
|
-
import re
|
|
24
|
-
import socket
|
|
25
|
-
import subprocess
|
|
26
|
-
import sys
|
|
27
|
-
import threading
|
|
28
|
-
import time
|
|
29
|
-
|
|
30
|
-
from . import paths, runner
|
|
31
|
-
|
|
32
|
-
_PKG_NAME = "codebee" # npm 发布名;必须与 package.json 的 name 一致(单测断言)
|
|
33
|
-
_UPDATE_TTL = 600 # 查新结果缓存(秒)
|
|
34
|
-
_LOCK = threading.Lock()
|
|
35
|
-
_CHECK_CACHE = {"ts": 0.0, "result": None}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def package_version():
|
|
39
|
-
"""读本包 package.json 的 version;读不到返回 ''(开发仓库未同步版本号时)。"""
|
|
40
|
-
try:
|
|
41
|
-
pj = (paths.ROOT / "package.json").resolve()
|
|
42
|
-
return str(json.loads(pj.read_text(encoding="utf-8")).get("version") or "")
|
|
43
|
-
except Exception:
|
|
44
|
-
return ""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def install_mode():
|
|
48
|
-
"""npm / repo / source / other(见模块 docstring)。"""
|
|
49
|
-
try:
|
|
50
|
-
parts = [p.lower() for p in paths.ROOT.resolve().parts]
|
|
51
|
-
except Exception:
|
|
52
|
-
return "other"
|
|
53
|
-
has_pkg = (paths.ROOT / "package.json").is_file()
|
|
54
|
-
if "node_modules" in parts and has_pkg:
|
|
55
|
-
return "npm"
|
|
56
|
-
if (paths.ROOT / ".git").exists():
|
|
57
|
-
return "repo"
|
|
58
|
-
return "source" if has_pkg else "other"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _ver_tuple(s):
|
|
62
|
-
return [int(x) for x in re.findall(r"\d+", str(s or ""))[:4]]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _npm_latest():
|
|
66
|
-
"""npm view codebee version;返回 (latest, err)。
|
|
67
|
-
用 _PKG_NAME 变量而非行内字面量:发布名改过一次(tutti-orchestrator→codebee),
|
|
68
|
-
硬编码两处容易漏改;Mimosa 安全基线要求 argv 可执行文件为字面量,参数用变量不违反。"""
|
|
69
|
-
r = runner.run_process(
|
|
70
|
-
argv=["cmd", "/c", "npm", "view", _PKG_NAME, "version"], timeout=60)
|
|
71
|
-
if not r["ok"]:
|
|
72
|
-
return "", (r["stderr"] or r["stdout"] or "")[-200:] or "npm 命令失败"
|
|
73
|
-
m = re.search(r"\d+\.\d+\.\d+[\w.\-]*", r["stdout"] or "")
|
|
74
|
-
return (m.group(0) if m else ""), ("" if m else "npm 输出无法解析")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def check(force=False):
|
|
78
|
-
"""GET /api/selfupdate 载荷:{mode, current, latest, has_update, note}。"""
|
|
79
|
-
mode = install_mode()
|
|
80
|
-
cur = package_version()
|
|
81
|
-
out = {"mode": mode, "current": cur, "latest": "", "has_update": False, "note": ""}
|
|
82
|
-
if mode != "npm":
|
|
83
|
-
out["note"] = ("开发仓库模式:请用 git pull 更新(自动升级会覆盖未提交的代码)"
|
|
84
|
-
if mode == "repo" else "非 npm 安装,无法自动更新")
|
|
85
|
-
return out
|
|
86
|
-
with _LOCK:
|
|
87
|
-
if not force and _CHECK_CACHE["result"] and \
|
|
88
|
-
time.time() - _CHECK_CACHE["ts"] < _UPDATE_TTL:
|
|
89
|
-
return dict(_CHECK_CACHE["result"])
|
|
90
|
-
latest, err = _npm_latest()
|
|
91
|
-
out["latest"] = latest
|
|
92
|
-
if err:
|
|
93
|
-
out["note"] = "查询新版本失败:" + err
|
|
94
|
-
elif latest and cur:
|
|
95
|
-
out["has_update"] = _ver_tuple(latest) > _ver_tuple(cur)
|
|
96
|
-
else:
|
|
97
|
-
out["note"] = "无法比较版本(本地或 registry 版本号缺失)"
|
|
98
|
-
with _LOCK:
|
|
99
|
-
_CHECK_CACHE["ts"] = time.time()
|
|
100
|
-
_CHECK_CACHE["result"] = dict(out)
|
|
101
|
-
return out
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def apply_upgrade():
|
|
105
|
-
"""发起升级:建 mgmt run 异步跑 npm install -g @latest。返回 {run_id} 或 {error}。"""
|
|
106
|
-
if install_mode() != "npm":
|
|
107
|
-
return {"error": "当前安装方式不支持自动升级(见版本页说明)"}
|
|
108
|
-
from . import store, jobs
|
|
109
|
-
run = store.create_run("mgmt", "升级 CodeBee 本体(npm install -g %s@latest)" % _PKG_NAME,
|
|
110
|
-
entry_id="__self__", op="selfupgrade")
|
|
111
|
-
jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
|
|
112
|
-
return {"run_id": run["id"]}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def run_upgrade(run_id, log_path):
|
|
116
|
-
"""worker 线程里执行升级命令(run/step 生命周期由 jobs 层管)。"""
|
|
117
|
-
res = runner.run_process(
|
|
118
|
-
argv=["cmd", "/c", "npm", "install", "-g", _PKG_NAME + "@latest"],
|
|
119
|
-
cwd=str(paths.ROOT), timeout=900, log_path=log_path)
|
|
120
|
-
if res["ok"]:
|
|
121
|
-
with _LOCK: # 装完即过期查新缓存,重启后自然拿到新版本
|
|
122
|
-
_CHECK_CACHE["result"] = None
|
|
123
|
-
return {"ok": res["ok"], "exit_code": res["exit_code"],
|
|
124
|
-
"error": "" if res["ok"] else (res["stderr"][-800:] or "退出码 %s" % res["exit_code"])}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _port_free(port):
|
|
128
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
129
|
-
try:
|
|
130
|
-
s.settimeout(0.3)
|
|
131
|
-
return s.connect_ex(("127.0.0.1", int(port))) != 0
|
|
132
|
-
except Exception:
|
|
133
|
-
return True
|
|
134
|
-
finally:
|
|
135
|
-
s.close()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def wait_port_before_bind(port):
|
|
139
|
-
"""新进程入口(--wait-port):轮询直到旧实例释放端口(最多 ~30 秒)。
|
|
140
|
-
超时也放行——旧进程若没退,bind 失败自会报错,不会出现双实例串流。"""
|
|
141
|
-
for _ in range(60):
|
|
142
|
-
if _port_free(port):
|
|
143
|
-
return
|
|
144
|
-
time.sleep(0.5)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def relaunch(port):
|
|
148
|
-
"""就地重启:拉起新实例(--wait-port 等新端口可 bind 时旧实例已自退)。
|
|
149
|
-
调用方发送完重启响应后应 self_quit()。port 经 int() 强校验,argv 全字面量。"""
|
|
150
|
-
port = int(port)
|
|
151
|
-
if port < 1 or port > 65535:
|
|
152
|
-
return False
|
|
153
|
-
subprocess.Popen(
|
|
154
|
-
[sys.executable, "main.py", "--port", str(port),
|
|
155
|
-
"--wait-port", "--no-browser"],
|
|
156
|
-
cwd=str(paths.APP_DIR),
|
|
157
|
-
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
158
|
-
close_fds=True,
|
|
159
|
-
creationflags=(0x00000008 | 0x00000200) if os.name == "nt" else 0)
|
|
160
|
-
return True
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def self_quit():
|
|
164
|
-
"""旧进程体面自退(HTTP 响应已发出后调用)。"""
|
|
165
|
-
try:
|
|
166
|
-
from . import remote
|
|
167
|
-
remote.stop_quick_tunnel()
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
|
-
os._exit(0)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tutti 自更新:版本读取 / 新版本检测 / 一键升级 / 就地重启。
|
|
3
|
+
|
|
4
|
+
安装模式判定(mode):
|
|
5
|
+
npm — 运行副本位于 node_modules 下且带 package.json(npm 全局安装的包),
|
|
6
|
+
可查新、可升级、可重启;
|
|
7
|
+
repo — 带 .git 的开发仓库:**永不自动升级**(会覆盖开发中的代码),提示走 git pull;
|
|
8
|
+
other — 裸源码拷贝,提示手动替换。
|
|
9
|
+
|
|
10
|
+
升级 = 在标准 mgmt run 里跑 `npm install -g codebee@latest`(日志实时落盘、
|
|
11
|
+
SSE 可看进度)。npm 替换的是包目录文件,当前进程已加载进内存不受影响,装完后由
|
|
12
|
+
「重启」换新代码:新进程先等旧端口释放再 bind(Windows SO_REUSEADDR 允许双 LISTEN
|
|
13
|
+
同时存在,必须先验旧进程真退了),旧进程发送完重启响应后自退。
|
|
14
|
+
|
|
15
|
+
安全:两处子进程命令的 argv 均为**行内字面量列表**(可执行文件与全部参数不来自任何
|
|
16
|
+
外部输入;重启仅透传 argparse 校验过的整型端口),shell 全程 False,绝不拼接用户输入。
|
|
17
|
+
改发布名时(见 test_selfupdate 与 package.json 的一致性断言)同步改 _PKG_NAME。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import socket
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
from . import paths, runner
|
|
31
|
+
|
|
32
|
+
_PKG_NAME = "codebee" # npm 发布名;必须与 package.json 的 name 一致(单测断言)
|
|
33
|
+
_UPDATE_TTL = 600 # 查新结果缓存(秒)
|
|
34
|
+
_LOCK = threading.Lock()
|
|
35
|
+
_CHECK_CACHE = {"ts": 0.0, "result": None}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def package_version():
|
|
39
|
+
"""读本包 package.json 的 version;读不到返回 ''(开发仓库未同步版本号时)。"""
|
|
40
|
+
try:
|
|
41
|
+
pj = (paths.ROOT / "package.json").resolve()
|
|
42
|
+
return str(json.loads(pj.read_text(encoding="utf-8")).get("version") or "")
|
|
43
|
+
except Exception:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def install_mode():
|
|
48
|
+
"""npm / repo / source / other(见模块 docstring)。"""
|
|
49
|
+
try:
|
|
50
|
+
parts = [p.lower() for p in paths.ROOT.resolve().parts]
|
|
51
|
+
except Exception:
|
|
52
|
+
return "other"
|
|
53
|
+
has_pkg = (paths.ROOT / "package.json").is_file()
|
|
54
|
+
if "node_modules" in parts and has_pkg:
|
|
55
|
+
return "npm"
|
|
56
|
+
if (paths.ROOT / ".git").exists():
|
|
57
|
+
return "repo"
|
|
58
|
+
return "source" if has_pkg else "other"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ver_tuple(s):
|
|
62
|
+
return [int(x) for x in re.findall(r"\d+", str(s or ""))[:4]]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _npm_latest():
|
|
66
|
+
"""npm view codebee version;返回 (latest, err)。
|
|
67
|
+
用 _PKG_NAME 变量而非行内字面量:发布名改过一次(tutti-orchestrator→codebee),
|
|
68
|
+
硬编码两处容易漏改;Mimosa 安全基线要求 argv 可执行文件为字面量,参数用变量不违反。"""
|
|
69
|
+
r = runner.run_process(
|
|
70
|
+
argv=["cmd", "/c", "npm", "view", _PKG_NAME, "version"], timeout=60)
|
|
71
|
+
if not r["ok"]:
|
|
72
|
+
return "", (r["stderr"] or r["stdout"] or "")[-200:] or "npm 命令失败"
|
|
73
|
+
m = re.search(r"\d+\.\d+\.\d+[\w.\-]*", r["stdout"] or "")
|
|
74
|
+
return (m.group(0) if m else ""), ("" if m else "npm 输出无法解析")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check(force=False):
|
|
78
|
+
"""GET /api/selfupdate 载荷:{mode, current, latest, has_update, note}。"""
|
|
79
|
+
mode = install_mode()
|
|
80
|
+
cur = package_version()
|
|
81
|
+
out = {"mode": mode, "current": cur, "latest": "", "has_update": False, "note": ""}
|
|
82
|
+
if mode != "npm":
|
|
83
|
+
out["note"] = ("开发仓库模式:请用 git pull 更新(自动升级会覆盖未提交的代码)"
|
|
84
|
+
if mode == "repo" else "非 npm 安装,无法自动更新")
|
|
85
|
+
return out
|
|
86
|
+
with _LOCK:
|
|
87
|
+
if not force and _CHECK_CACHE["result"] and \
|
|
88
|
+
time.time() - _CHECK_CACHE["ts"] < _UPDATE_TTL:
|
|
89
|
+
return dict(_CHECK_CACHE["result"])
|
|
90
|
+
latest, err = _npm_latest()
|
|
91
|
+
out["latest"] = latest
|
|
92
|
+
if err:
|
|
93
|
+
out["note"] = "查询新版本失败:" + err
|
|
94
|
+
elif latest and cur:
|
|
95
|
+
out["has_update"] = _ver_tuple(latest) > _ver_tuple(cur)
|
|
96
|
+
else:
|
|
97
|
+
out["note"] = "无法比较版本(本地或 registry 版本号缺失)"
|
|
98
|
+
with _LOCK:
|
|
99
|
+
_CHECK_CACHE["ts"] = time.time()
|
|
100
|
+
_CHECK_CACHE["result"] = dict(out)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def apply_upgrade():
|
|
105
|
+
"""发起升级:建 mgmt run 异步跑 npm install -g @latest。返回 {run_id} 或 {error}。"""
|
|
106
|
+
if install_mode() != "npm":
|
|
107
|
+
return {"error": "当前安装方式不支持自动升级(见版本页说明)"}
|
|
108
|
+
from . import store, jobs
|
|
109
|
+
run = store.create_run("mgmt", "升级 CodeBee 本体(npm install -g %s@latest)" % _PKG_NAME,
|
|
110
|
+
entry_id="__self__", op="selfupgrade")
|
|
111
|
+
jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
|
|
112
|
+
return {"run_id": run["id"]}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_upgrade(run_id, log_path):
|
|
116
|
+
"""worker 线程里执行升级命令(run/step 生命周期由 jobs 层管)。"""
|
|
117
|
+
res = runner.run_process(
|
|
118
|
+
argv=["cmd", "/c", "npm", "install", "-g", _PKG_NAME + "@latest"],
|
|
119
|
+
cwd=str(paths.ROOT), timeout=900, log_path=log_path)
|
|
120
|
+
if res["ok"]:
|
|
121
|
+
with _LOCK: # 装完即过期查新缓存,重启后自然拿到新版本
|
|
122
|
+
_CHECK_CACHE["result"] = None
|
|
123
|
+
return {"ok": res["ok"], "exit_code": res["exit_code"],
|
|
124
|
+
"error": "" if res["ok"] else (res["stderr"][-800:] or "退出码 %s" % res["exit_code"])}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _port_free(port):
|
|
128
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
129
|
+
try:
|
|
130
|
+
s.settimeout(0.3)
|
|
131
|
+
return s.connect_ex(("127.0.0.1", int(port))) != 0
|
|
132
|
+
except Exception:
|
|
133
|
+
return True
|
|
134
|
+
finally:
|
|
135
|
+
s.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def wait_port_before_bind(port):
|
|
139
|
+
"""新进程入口(--wait-port):轮询直到旧实例释放端口(最多 ~30 秒)。
|
|
140
|
+
超时也放行——旧进程若没退,bind 失败自会报错,不会出现双实例串流。"""
|
|
141
|
+
for _ in range(60):
|
|
142
|
+
if _port_free(port):
|
|
143
|
+
return
|
|
144
|
+
time.sleep(0.5)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def relaunch(port):
|
|
148
|
+
"""就地重启:拉起新实例(--wait-port 等新端口可 bind 时旧实例已自退)。
|
|
149
|
+
调用方发送完重启响应后应 self_quit()。port 经 int() 强校验,argv 全字面量。"""
|
|
150
|
+
port = int(port)
|
|
151
|
+
if port < 1 or port > 65535:
|
|
152
|
+
return False
|
|
153
|
+
subprocess.Popen(
|
|
154
|
+
[sys.executable, "main.py", "--port", str(port),
|
|
155
|
+
"--wait-port", "--no-browser"],
|
|
156
|
+
cwd=str(paths.APP_DIR),
|
|
157
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
158
|
+
close_fds=True,
|
|
159
|
+
creationflags=(0x00000008 | 0x00000200) if os.name == "nt" else 0)
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def self_quit():
|
|
164
|
+
"""旧进程体面自退(HTTP 响应已发出后调用)。"""
|
|
165
|
+
try:
|
|
166
|
+
from . import remote
|
|
167
|
+
remote.stop_quick_tunnel()
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
os._exit(0)
|