codebee 0.1.0 → 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.
@@ -1329,8 +1329,9 @@ def _src_ccswitch():
1329
1329
  except Exception:
1330
1330
  pass
1331
1331
  if skipped:
1332
- out["note"] = "跳过 %d 条(缺地址或格式不识别):%s" % (
1332
+ out["note"] = "跳过 {0} 条(缺地址或格式不识别):{1}".format(
1333
1333
  len(skipped), "、".join(sorted(set(skipped))[:4]))
1334
+ out["note_args"] = [len(skipped), "、".join(sorted(set(skipped))[:4])]
1334
1335
  return out
1335
1336
 
1336
1337
 
@@ -1414,7 +1415,9 @@ def _src_zcode():
1414
1415
  else:
1415
1416
  bad.append(str(p.get("name") or pid))
1416
1417
  if bad:
1417
- out["note"] = "跳过 %d 个(缺合法 baseURL):%s" % (len(bad), "、".join(bad[:4]))
1418
+ out["note"] = "跳过 {0} 个(缺合法 baseURL):{1}".format(
1419
+ len(bad), "、".join(bad[:4]))
1420
+ out["note_args"] = [len(bad), "、".join(bad[:4])]
1418
1421
  return out
1419
1422
 
1420
1423
 
@@ -1697,6 +1700,7 @@ def _collect(fn):
1697
1700
  res.setdefault("providers", [])
1698
1701
  res.setdefault("pricing", {})
1699
1702
  res.setdefault("note", "")
1703
+ res.setdefault("note_args", [])
1700
1704
  res.setdefault("error", "")
1701
1705
  res.setdefault("found", False)
1702
1706
  return res
@@ -1709,11 +1713,12 @@ def sources():
1709
1713
  ps = [p for p in paths() if p]
1710
1714
  found = any(os.path.isfile(p) for p in ps)
1711
1715
  row = {"id": sid, "name": name, "desc": desc, "paths": ps,
1712
- "found": found, "count": 0, "note": "", "error": ""}
1716
+ "found": found, "count": 0, "note": "", "note_args": [], "error": ""}
1713
1717
  if found:
1714
1718
  res = _collect(fn)
1715
1719
  row["count"] = len(res["providers"])
1716
1720
  row["note"] = res["note"]
1721
+ row["note_args"] = res.get("note_args", [])
1717
1722
  row["error"] = res["error"]
1718
1723
  rows.append(row)
1719
1724
  return rows
@@ -1754,7 +1759,8 @@ def import_sources(ids=None):
1754
1759
  out["sources"].append({
1755
1760
  "id": sid, "name": name, "found": res["found"],
1756
1761
  "added": added, "updated": updated, "duplicate": dup,
1757
- "count": added + updated, "note": res["note"], "error": res["error"]})
1762
+ "count": added + updated, "note": res["note"],
1763
+ "note_args": res.get("note_args", []), "error": res["error"]})
1758
1764
  if out["added"] or out["updated"] or out["pricing"]:
1759
1765
  _save(data)
1760
1766
  out["imported"] = out["added"] + out["updated"]
package/app/core/paths.py CHANGED
@@ -4,7 +4,9 @@
4
4
  数据目录选择顺序:
5
5
  1. TUTTI_DATA 环境变量(测试/多实例时整体指到别处,必须在任何模块使用前设置);
6
6
  2. 仓库内 data/ 且已有内容——开发仓库与老安装平滑沿用,升级不动数据;
7
- 3. 用户目录:Windows %APPDATA%\\Tutti,macOS/Linux ~/.tutti
7
+ 3. 用户目录:Windows %APPDATA%\\CodeBee,macOS/Linux ~/.codebee
8
+ 老安装的 %APPDATA%\\Tutti / ~/.tutti 若存在则自动沿用(零迁移),
9
+ 新安装一律落 CodeBee 目录。
8
10
  npm/pip 全局安装的包目录会在 `npm update -g` 时被整体替换,
9
11
  数据绝不能落在包内,故全新安装一律走用户目录。
10
12
  """
@@ -23,16 +25,20 @@ _DATA_SENTINELS = ("catalog.json", "models.json", "orchestration.json", "tasks",
23
25
 
24
26
 
25
27
  def _user_data_dir(platform=None, environ=None, home=None) -> Path:
26
- """全局安装场景的用户数据目录(不创建,仅计算)。home 供测试注入。"""
28
+ """全局安装场景的用户数据目录(不创建,仅计算)。home 供测试注入。
29
+ 新装落 CodeBee;老 Tutti 目录存在则沿用(改名零迁移)。"""
27
30
  platform = platform or sys.platform
28
31
  env = os.environ if environ is None else environ
29
32
  home = Path(home) if home else Path.home()
30
33
  if platform == "win32":
31
34
  appdata = env.get("APPDATA")
32
- if appdata:
33
- return Path(appdata) / "Tutti"
34
- return home / "AppData" / "Roaming" / "Tutti"
35
- return home / ".tutti"
35
+ base = Path(appdata) if appdata else home / "AppData" / "Roaming"
36
+ if (base / "Tutti").exists():
37
+ return base / "Tutti"
38
+ return base / "CodeBee"
39
+ if (home / ".tutti").exists():
40
+ return home / ".tutti"
41
+ return home / ".codebee"
36
42
 
37
43
 
38
44
  def default_data_dir(repo=None, environ=None) -> Path:
@@ -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)