@hupan56/wlkj 2.7.8 → 2.7.10
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/bin/cli.js +41 -4
- package/package.json +1 -1
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_mcp_launch.py +183 -0
- package/templates/qoder/scripts/common/pip_install.py +43 -10
- package/templates/qoder/scripts/setup.py +19 -3
package/bin/cli.js
CHANGED
|
@@ -59,7 +59,9 @@ function runScript(scriptName, args = [], timeoutMs = 600000) {
|
|
|
59
59
|
const pyCmd = detectPyCmd();
|
|
60
60
|
if (!pyCmd) { console.log("Python 未安装。跑: npx @hupan56/wlkj install-env"); process.exit(1); }
|
|
61
61
|
const { spawnSync } = require("child_process");
|
|
62
|
-
|
|
62
|
+
// 强制 UTF-8 环境 (Windows cmd 默认 GBK, 会导致中文输出乱码)
|
|
63
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
64
|
+
const r = spawnSync(pyCmd, [p, ...args], { cwd: process.cwd(), stdio: "inherit", timeout: timeoutMs, encoding: "utf-8", env });
|
|
63
65
|
process.exit(r.status || 0);
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -304,7 +306,9 @@ async function doInit(name, roleArg) {
|
|
|
304
306
|
try {
|
|
305
307
|
const cmd = `python "${setupPath}" ${setupArgs.map(a => `"${a}"`).join(" ")}`;
|
|
306
308
|
console.log(` 运行: setup.py ${setupArgs.join(" ")}`);
|
|
307
|
-
|
|
309
|
+
// 强制 UTF-8 环境 (Windows cmd 默认 GBK, 会导致中文输出乱码)
|
|
310
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
311
|
+
execSync(cmd, { cwd, stdio: "inherit", timeout: 300000, env });
|
|
308
312
|
} catch (e) {
|
|
309
313
|
console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
|
|
310
314
|
console.log(` 可重新运行: npx @hupan56/wlkj init ${name || "<你的名字>"}`);
|
|
@@ -345,7 +349,8 @@ async function doInit(name, roleArg) {
|
|
|
345
349
|
const instScript = path.join(cwd, ".qoder", "scripts", "install_qoderwork.py");
|
|
346
350
|
if (fs.existsSync(instScript)) {
|
|
347
351
|
try {
|
|
348
|
-
|
|
352
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
353
|
+
execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "inherit", timeout: 20000, env });
|
|
349
354
|
console.log(` [OK] mcp.json 已生成 (选装了依赖的Python, MCP 不会黄)`);
|
|
350
355
|
} catch (e) {
|
|
351
356
|
console.log(` [WARN] MCP 配置未完全成功 (不阻塞): ${(e.message || "").slice(0, 80)}`);
|
|
@@ -557,6 +562,9 @@ function doUpdate() {
|
|
|
557
562
|
console.log(` 根文档 + requirements.txt: 已更新`);
|
|
558
563
|
|
|
559
564
|
// === 2.5. git pull 拉最新团队数据 (kg.duckdb/kg_db.duckdb/PRD等在git里) ===
|
|
565
|
+
// git pull 失败会导致 kg.duckdb 拿不到 → kg MCP 无数据(但仍能连)。
|
|
566
|
+
// 失败原因常见: 未配 git 身份 / 远程仓库需要认证 / 网络问题。
|
|
567
|
+
// 这里给出清晰诊断, 不让"git pull 失败"变成无声的静默跳过。
|
|
560
568
|
try {
|
|
561
569
|
const { execSync: _es } = require("child_process");
|
|
562
570
|
const pullR = _es("git pull --no-rebase 2>&1", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
@@ -568,7 +576,22 @@ function doUpdate() {
|
|
|
568
576
|
console.log(` git: 已拉取最新`);
|
|
569
577
|
}
|
|
570
578
|
} catch (e) {
|
|
571
|
-
|
|
579
|
+
// git pull 失败诊断: 区分"没配git身份"/"远程要认证"/"网络问题"
|
|
580
|
+
const errMsg = String(e.message || "");
|
|
581
|
+
const stdout = String(e.stdout || "");
|
|
582
|
+
const combined = errMsg + " " + stdout;
|
|
583
|
+
if (combined.includes("Authentication failed") || combined.includes("Permission denied") ||
|
|
584
|
+
combined.includes("could not read Username") || combined.includes("不支持密码")) {
|
|
585
|
+
console.log(` git: ⚠️ 认证失败 — kg.duckdb 等团队数据拉不到`);
|
|
586
|
+
console.log(` 原因: git 身份/凭证未配。MCP 能连但无数据。`);
|
|
587
|
+
console.log(` 修复: npx @hupan56/wlkj init <你的名字> (交互式配 git 账号)`);
|
|
588
|
+
} else if (combined.includes("not a git repository") || combined.includes("not a git dir")) {
|
|
589
|
+
console.log(` git: 非git仓库 — 用 team_sync 拉团队数据`);
|
|
590
|
+
console.log(` 修复: npx @hupan56/wlkj init <你的名字>`);
|
|
591
|
+
} else {
|
|
592
|
+
console.log(` git: 跳过 — ${(errMsg).slice(0, 60)}`);
|
|
593
|
+
console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
|
|
594
|
+
}
|
|
572
595
|
}
|
|
573
596
|
|
|
574
597
|
// === 3. 补装 pip 依赖 (必须在刷 mcp.json 之前!)
|
|
@@ -718,6 +741,20 @@ function doUpdate() {
|
|
|
718
741
|
}
|
|
719
742
|
} catch (e) { /* 刷新失败不阻塞升级 */ }
|
|
720
743
|
|
|
744
|
+
// === 6.5. MCP 启动实测 (发 initialize 看哪个起不来) ===
|
|
745
|
+
// 这是"根治黄色"的关键一步: 配完 mcp.json 后立即模拟 QoderWork 发 initialize,
|
|
746
|
+
// 看每个 MCP 是 OK / DISABLED(绿) / CRASHED(黄)。有问题当场报, 不让用户重启后才发现。
|
|
747
|
+
console.log(`\n--- MCP 启动实测 (模拟 QoderWork 连接) ---`);
|
|
748
|
+
const diagScript = path.join(cwd, ".qoder", "scripts", "check_mcp_launch.py");
|
|
749
|
+
if (pyCmd && fs.existsSync(diagScript)) {
|
|
750
|
+
try {
|
|
751
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
752
|
+
execSync(`${pyCmd} "${diagScript}"`, { cwd, stdio: "inherit", timeout: 60000, env });
|
|
753
|
+
} catch (e) {
|
|
754
|
+
console.log(` [WARN] MCP 实测脚本异常 (不阻塞): ${(e.message || "").slice(0, 80)}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
721
758
|
// === 7. 自检: 关键文件到底齐不齐 ===
|
|
722
759
|
console.log(`\n--- 自检 (关键文件验证) ---`);
|
|
723
760
|
const checks = [
|
package/package.json
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
check_mcp_launch.py - 诊断 MCP 为什么黄色
|
|
5
|
+
|
|
6
|
+
逐个模拟 mcp_launcher.py 启动每个 MCP, 发送 initialize 请求,
|
|
7
|
+
看哪个 MCP 启动失败/崩溃/不响应。输出明确的诊断结论。
|
|
8
|
+
|
|
9
|
+
用法: python .qoder/scripts/check_mcp_launch.py
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
18
|
+
BASE = SCRIPTS_DIR.parent.parent # repo root
|
|
19
|
+
|
|
20
|
+
MCP_NAMES = ['kg', 'mysql', 'zentao', 'lanhu']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_python_with(mod):
|
|
24
|
+
"""找一个能 import mod 的 python."""
|
|
25
|
+
for py in [sys.executable, 'python', 'python3']:
|
|
26
|
+
try:
|
|
27
|
+
r = subprocess.run([py, '-c', f'import {mod}'],
|
|
28
|
+
capture_output=True, timeout=8)
|
|
29
|
+
if r.returncode == 0:
|
|
30
|
+
return py
|
|
31
|
+
except Exception:
|
|
32
|
+
continue
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_mcp(name):
|
|
37
|
+
"""测试一个 MCP 能否正常响应 initialize."""
|
|
38
|
+
launcher = SCRIPTS_DIR / 'mcp_launcher.py'
|
|
39
|
+
if not launcher.is_file():
|
|
40
|
+
return {'name': name, 'status': 'ERROR',
|
|
41
|
+
'detail': f'launcher 不存在: {launcher}'}
|
|
42
|
+
|
|
43
|
+
init_req = json.dumps({
|
|
44
|
+
'jsonrpc': '2.0', 'id': 1, 'method': 'initialize',
|
|
45
|
+
'params': {'capabilities': {}, 'protocolVersion': '2024-11-05',
|
|
46
|
+
'clientInfo': {'name': 'diag', 'version': '1.0'}}
|
|
47
|
+
}) + '\n'
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
r = subprocess.run(
|
|
51
|
+
[sys.executable, str(launcher), name],
|
|
52
|
+
input=init_req,
|
|
53
|
+
capture_output=True, text=True, timeout=10,
|
|
54
|
+
cwd=str(BASE), encoding='utf-8', errors='replace'
|
|
55
|
+
)
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
return {'name': name, 'status': 'TIMEOUT',
|
|
58
|
+
'detail': '启动后 10s 无响应 (可能卡在等 stdin)'}
|
|
59
|
+
except Exception as e:
|
|
60
|
+
return {'name': name, 'status': 'ERROR',
|
|
61
|
+
'detail': f'启动异常: {e}'}
|
|
62
|
+
|
|
63
|
+
stdout = r.stdout or ''
|
|
64
|
+
stderr = r.stderr or ''
|
|
65
|
+
|
|
66
|
+
# 解析 stdout 找 initialize 响应
|
|
67
|
+
init_ok = False
|
|
68
|
+
disabled = False
|
|
69
|
+
reason = ''
|
|
70
|
+
for line in stdout.split('\n'):
|
|
71
|
+
line = line.strip()
|
|
72
|
+
if not line:
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
resp = json.loads(line)
|
|
76
|
+
if resp.get('id') == 1 and 'result' in resp:
|
|
77
|
+
init_ok = True
|
|
78
|
+
si = resp['result'].get('serverInfo', {})
|
|
79
|
+
ver = si.get('version', '')
|
|
80
|
+
if 'disabled' in ver:
|
|
81
|
+
disabled = True
|
|
82
|
+
reason = si.get('reason', '')
|
|
83
|
+
break
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if init_ok and not disabled:
|
|
88
|
+
return {'name': name, 'status': 'OK',
|
|
89
|
+
'detail': '正常启动, 工具可用'}
|
|
90
|
+
elif init_ok and disabled:
|
|
91
|
+
return {'name': name, 'status': 'DISABLED',
|
|
92
|
+
'detail': f'以"未启用"模式启动 (绿但0工具): {reason}'}
|
|
93
|
+
elif r.returncode != 0:
|
|
94
|
+
# 进程崩溃了
|
|
95
|
+
return {'name': name, 'status': 'CRASHED',
|
|
96
|
+
'detail': f'进程退出(rc={r.returncode})。stderr:\n{stderr[-500:]}'}
|
|
97
|
+
else:
|
|
98
|
+
return {'name': name, 'status': 'NO_RESPONSE',
|
|
99
|
+
'detail': f'没崩溃但没响应 initialize。stdout:\n{stdout[-300:]}\nstderr:\n{stderr[-300:]}'}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
print('=' * 56)
|
|
104
|
+
print('MCP 启动诊断')
|
|
105
|
+
print(f'仓库: {BASE}')
|
|
106
|
+
print(f'Python: {sys.executable} ({sys.version.split()[0]})')
|
|
107
|
+
print('=' * 56)
|
|
108
|
+
|
|
109
|
+
# 基础检查
|
|
110
|
+
print('\n--- 环境检查 ---')
|
|
111
|
+
for mod, label in [('duckdb', 'kg 依赖'), ('pymysql', 'mysql 依赖'),
|
|
112
|
+
('requests', 'zentao 依赖'), ('yaml', '配置解析')]:
|
|
113
|
+
try:
|
|
114
|
+
__import__(mod)
|
|
115
|
+
print(f' [OK] {mod} ({label})')
|
|
116
|
+
except ImportError:
|
|
117
|
+
py = find_python_with(mod)
|
|
118
|
+
if py:
|
|
119
|
+
print(f' [WARN] {mod} 在当前Python没有, 但 {py} 有')
|
|
120
|
+
else:
|
|
121
|
+
print(f' [MISS] {mod} ({label}) — pip install {mod}')
|
|
122
|
+
|
|
123
|
+
kg_path = BASE / 'data' / 'index' / 'kg.duckdb'
|
|
124
|
+
print(f'\n kg.duckdb: {"存在" if kg_path.is_file() else "缺失 (git pull 失败? 非管理员跑 team_sync pull)"}')
|
|
125
|
+
|
|
126
|
+
# mcp.json 检查
|
|
127
|
+
home = Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
|
|
128
|
+
mcp_file = home / '.qoderwork' / 'mcp.json'
|
|
129
|
+
print(f'\n mcp.json: {mcp_file}')
|
|
130
|
+
if mcp_file.is_file():
|
|
131
|
+
try:
|
|
132
|
+
cfg = json.loads(mcp_file.read_text(encoding='utf-8'))
|
|
133
|
+
servers = cfg.get('mcpServers', {})
|
|
134
|
+
for srv in ['qoder-knowledge-graph', 'qoder-mysql', 'qoder-zentao', 'lanhu']:
|
|
135
|
+
entry = servers.get(srv)
|
|
136
|
+
if entry:
|
|
137
|
+
cmd = entry.get('command', '?')
|
|
138
|
+
args = entry.get('args', [])
|
|
139
|
+
has_launcher = any('mcp_launcher.py' in str(a) for a in args)
|
|
140
|
+
print(f' {srv}: command={cmd!r} launcher={"是" if has_launcher else "否(旧模式)"}')
|
|
141
|
+
else:
|
|
142
|
+
print(f' {srv}: 未注册')
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f' [ERR] mcp.json 解析失败: {e}')
|
|
145
|
+
else:
|
|
146
|
+
print(f' [MISS] mcp.json 不存在 (跑 npx @hupan56/wlkj update)')
|
|
147
|
+
|
|
148
|
+
# 逐个测试 MCP
|
|
149
|
+
print('\n--- MCP 启动测试 (模拟 QoderWork 发 initialize) ---')
|
|
150
|
+
results = []
|
|
151
|
+
for name in MCP_NAMES:
|
|
152
|
+
print(f'\n 测试 {name}...')
|
|
153
|
+
r = test_mcp(name)
|
|
154
|
+
results.append(r)
|
|
155
|
+
icon = {'OK': '🟢', 'DISABLED': '🟢(无工具)', 'CRASHED': '🔴',
|
|
156
|
+
'TIMEOUT': '🟡', 'NO_RESPONSE': '🟡', 'ERROR': '🔴'}
|
|
157
|
+
print(f' {icon.get(r["status"], "?")} {r["status"]}: {r["name"]}')
|
|
158
|
+
if r['status'] not in ('OK', 'DISABLED'):
|
|
159
|
+
print(f' 详情: {r["detail"]}')
|
|
160
|
+
|
|
161
|
+
# 总结
|
|
162
|
+
print('\n' + '=' * 56)
|
|
163
|
+
print('诊断结论:')
|
|
164
|
+
ok = sum(1 for r in results if r['status'] in ('OK', 'DISABLED'))
|
|
165
|
+
bad = sum(1 for r in results if r['status'] not in ('OK', 'DISABLED'))
|
|
166
|
+
if bad == 0:
|
|
167
|
+
print(f' 全部 MCP 启动正常 ({ok}/{len(results)})。')
|
|
168
|
+
print(' 如果 QoderWork 仍显示黄色: 重启 QoderWork 或新建对话。')
|
|
169
|
+
print(' (mcp.json 改动需要重启才生效)')
|
|
170
|
+
else:
|
|
171
|
+
print(f' {bad} 个 MCP 启动有问题:')
|
|
172
|
+
for r in results:
|
|
173
|
+
if r['status'] not in ('OK', 'DISABLED'):
|
|
174
|
+
print(f' - {r["name"]}: {r["status"]}')
|
|
175
|
+
print('\n 常见原因:')
|
|
176
|
+
print(' CRASHED: Python 版本不兼容(如3.14) 或 脚本语法错误 → 看详情里的 stderr')
|
|
177
|
+
print(' TIMEOUT: 脚本卡住了 → 查 DuckDB 文件锁 或 网络等待')
|
|
178
|
+
print(' NO_RESPONSE: stdout 被 print 污染 → 看 stdout 里的非 JSON 内容')
|
|
179
|
+
print('=' * 56)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == '__main__':
|
|
183
|
+
main()
|
|
@@ -24,11 +24,14 @@ MIRRORS = [
|
|
|
24
24
|
('腾讯', 'https://mirrors.cloud.tencent.com/pypi/simple'),
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
|
|
27
|
+
# 超时策略 (秒):
|
|
28
|
+
# 默认源(PyPI): 国内常被拦/超慢, 给短超时 45s, 够判定"通不通", 不让用户干等。
|
|
29
|
+
# 国内镜像: 给足 300s, 真正下载大包(tree-sitter/playwright wheels)需要时间。
|
|
30
|
+
DEFAULT_TIMEOUT = 45
|
|
31
|
+
MIRROR_TIMEOUT = 300
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
def _run_pip(args, index_url=None, timeout=
|
|
34
|
+
def _run_pip(args, index_url=None, timeout=MIRROR_TIMEOUT):
|
|
32
35
|
"""跑一次 pip, 返回 (returncode, stdout, stderr)。
|
|
33
36
|
index_url 非 None 时加 -i 指定镜像源。
|
|
34
37
|
用 trusted-host 避免某些镜像的 SSL 证书警告阻断。
|
|
@@ -63,26 +66,43 @@ def install_with_fallback(args):
|
|
|
63
66
|
missing: 仍缺失的包 (ok=True 时为空)
|
|
64
67
|
tried: 尝试过的源列表 [(name, success)]
|
|
65
68
|
hint: 失败时的手动命令
|
|
69
|
+
|
|
70
|
+
判断成功以 _verify_packages(import 验证)为准, 不只看 returncode:
|
|
71
|
+
理由: 某些间接依赖(如 greenlet 在无编译器的环境)编译失败会让
|
|
72
|
+
pip 返回 rc=1, 但核心包其实都已装好(already satisfied)。
|
|
73
|
+
只信 rc=1 会误判失败 → 浪费时间去试镜像 → 仍"失败"吓到用户。
|
|
74
|
+
用 import 验证核心包才是真相。
|
|
66
75
|
"""
|
|
67
76
|
tried = []
|
|
68
77
|
|
|
69
|
-
# 1. 默认源
|
|
70
|
-
rc, out, err = _run_pip(args)
|
|
78
|
+
# 1. 默认源 (短超时: 国内常被拦, 不让用户干等 5 分钟)
|
|
79
|
+
rc, out, err = _run_pip(args, timeout=DEFAULT_TIMEOUT)
|
|
71
80
|
tried.append(('默认源(PyPI)', rc == 0))
|
|
72
81
|
if rc == 0:
|
|
73
82
|
return {'ok': True, 'source': 'default', 'missing': [], 'tried': tried,
|
|
74
83
|
'hint': None}
|
|
75
84
|
|
|
76
|
-
#
|
|
85
|
+
# 1.5 默认源 rc≠0 —— 先 import 验证, 核心包都在就算成功 (跳过无谓的镜像重试)
|
|
86
|
+
# 典型场景: greenlet 编译失败 → rc=1, 但 duckdb/pymysql/PyYAML 都 already satisfied
|
|
87
|
+
missing = _verify_packages(args)
|
|
88
|
+
if not missing:
|
|
89
|
+
return {'ok': True, 'source': 'default', 'missing': [], 'tried': tried,
|
|
90
|
+
'hint': None}
|
|
91
|
+
|
|
92
|
+
# 2. 核心包真有缺失 —— 才逐个试国内镜像
|
|
77
93
|
for name, url in MIRRORS:
|
|
78
94
|
rc, out, err = _run_pip(args, index_url=url)
|
|
79
95
|
tried.append((name, rc == 0))
|
|
80
96
|
if rc == 0:
|
|
81
97
|
return {'ok': True, 'source': name, 'missing': [], 'tried': tried,
|
|
82
98
|
'hint': None}
|
|
99
|
+
# 每个镜像失败后也 import 验证一下 (镜像可能装上了核心包, 只是间接依赖仍编译失败)
|
|
100
|
+
missing = _verify_packages(args)
|
|
101
|
+
if not missing:
|
|
102
|
+
return {'ok': True, 'source': name, 'missing': [], 'tried': tried,
|
|
103
|
+
'hint': None}
|
|
83
104
|
|
|
84
|
-
# 3. 全失败 ——
|
|
85
|
-
missing = _verify_packages(args)
|
|
105
|
+
# 3. 全失败 —— 返回缺失清单 + 手动命令
|
|
86
106
|
hint = _build_manual_hint(missing, tried)
|
|
87
107
|
return {'ok': False, 'source': None, 'missing': missing,
|
|
88
108
|
'tried': tried, 'hint': hint}
|
|
@@ -93,8 +113,21 @@ def _verify_packages(args):
|
|
|
93
113
|
只处理 -r requirements.txt 和直接包名两种情况。
|
|
94
114
|
返回仍 import 失败的 pip 包名列表。
|
|
95
115
|
"""
|
|
96
|
-
# 直接包名: args 里不带 -
|
|
97
|
-
|
|
116
|
+
# 直接包名: args 里不带 - 开头的项, 但要排除 -r 后面跟的文件路径
|
|
117
|
+
# (否则 "requirements.txt" 会被误当成包名去 import)
|
|
118
|
+
skip_next = False
|
|
119
|
+
pkg_names = []
|
|
120
|
+
for a in args:
|
|
121
|
+
if skip_next:
|
|
122
|
+
skip_next = False
|
|
123
|
+
continue
|
|
124
|
+
if a in ('-r', '--requirement', '-c', '--constraint', '-e', '--editable'):
|
|
125
|
+
skip_next = True # 下一个参数是文件路径, 不是包名
|
|
126
|
+
continue
|
|
127
|
+
if a.startswith('-r') and len(a) > 2: # -rFILE 紧凑写法
|
|
128
|
+
continue
|
|
129
|
+
if not a.startswith('-'):
|
|
130
|
+
pkg_names.append(a)
|
|
98
131
|
if '-r' in args:
|
|
99
132
|
# 从 requirements.txt 读包名 (取每行第一个 token, 去 >= 等版本约束)
|
|
100
133
|
try:
|
|
@@ -65,11 +65,20 @@ def ask(prompt, default='y'):
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def run(cmd, **kwargs):
|
|
68
|
-
"""跑子进程, 返回 CompletedProcess。encoding 容错。
|
|
68
|
+
"""跑子进程, 返回 CompletedProcess。encoding 容错。
|
|
69
|
+
强制 UTF-8 环境 (Windows cmd 默认 GBK, 不设会导致中文乱码):
|
|
70
|
+
- encoding='utf-8': 父进程解码子进程 stdout 的方式
|
|
71
|
+
- env PYTHONIOENCODING=utf-8: 让子进程 Python 自己也用 UTF-8 输出
|
|
72
|
+
- env PYTHONUTF8=1: Python 3.7+ UTF-8 mode, 彻底解决
|
|
73
|
+
"""
|
|
69
74
|
kwargs.setdefault('capture_output', True)
|
|
70
75
|
kwargs.setdefault('text', True)
|
|
71
76
|
kwargs.setdefault('encoding', 'utf-8')
|
|
72
77
|
kwargs.setdefault('errors', 'replace')
|
|
78
|
+
env = dict(kwargs.pop('env', None) or os.environ)
|
|
79
|
+
env.setdefault('PYTHONIOENCODING', 'utf-8')
|
|
80
|
+
env.setdefault('PYTHONUTF8', '1')
|
|
81
|
+
kwargs['env'] = env
|
|
73
82
|
return subprocess.run(cmd, **kwargs)
|
|
74
83
|
|
|
75
84
|
|
|
@@ -223,7 +232,11 @@ def detect_name(explicit=None):
|
|
|
223
232
|
def run_doctor(name, role):
|
|
224
233
|
print('\n--- 初始化 (init_doctor) ---')
|
|
225
234
|
cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
|
|
226
|
-
|
|
235
|
+
# 透传 stdio (用户能看到实时输出) + 强制 UTF-8 环境 (解决 cmd GBK 乱码)
|
|
236
|
+
env = dict(os.environ)
|
|
237
|
+
env['PYTHONIOENCODING'] = 'utf-8'
|
|
238
|
+
env['PYTHONUTF8'] = '1'
|
|
239
|
+
r = subprocess.run(cmd, cwd=str(BASE), env=env)
|
|
227
240
|
# init_doctor 返回 0=健康, 1=有未解决问题
|
|
228
241
|
if r.returncode == 0:
|
|
229
242
|
ok('init_doctor 完成, 环境健康')
|
|
@@ -565,7 +578,10 @@ def offer_qoderwork(skip=False):
|
|
|
565
578
|
return
|
|
566
579
|
|
|
567
580
|
if ask('检测到 QoderWork, 安装技能 (软链到 ~/.qoderwork/skills/)?', 'y'):
|
|
568
|
-
|
|
581
|
+
env = dict(os.environ)
|
|
582
|
+
env['PYTHONIOENCODING'] = 'utf-8'
|
|
583
|
+
env['PYTHONUTF8'] = '1'
|
|
584
|
+
r = subprocess.run([sys.executable, str(THIS_DIR / 'install_qoderwork.py')], cwd=str(BASE), env=env)
|
|
569
585
|
if r.returncode == 0:
|
|
570
586
|
ok('QoderWork 技能已安装')
|
|
571
587
|
else:
|