@hupan56/wlkj 2.7.9 → 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 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
- const r = spawnSync(pyCmd, [p, ...args], { cwd: process.cwd(), stdio: "inherit", timeout: timeoutMs, encoding: "utf-8" });
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
- execSync(cmd, { cwd, stdio: "inherit", timeout: 300000 });
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
- execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "inherit", timeout: 20000 });
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)}`);
@@ -743,7 +748,8 @@ function doUpdate() {
743
748
  const diagScript = path.join(cwd, ".qoder", "scripts", "check_mcp_launch.py");
744
749
  if (pyCmd && fs.existsSync(diagScript)) {
745
750
  try {
746
- execSync(`${pyCmd} "${diagScript}"`, { cwd, stdio: "inherit", timeout: 60000 });
751
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
752
+ execSync(`${pyCmd} "${diagScript}"`, { cwd, stdio: "inherit", timeout: 60000, env });
747
753
  } catch (e) {
748
754
  console.log(` [WARN] MCP 实测脚本异常 (不阻塞): ${(e.message || "").slice(0, 80)}`);
749
755
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -24,11 +24,14 @@ MIRRORS = [
24
24
  ('腾讯', 'https://mirrors.cloud.tencent.com/pypi/simple'),
25
25
  ]
26
26
 
27
- # 单次安装超时 (秒), 避免卡死
28
- INSTALL_TIMEOUT = 300
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=INSTALL_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
- # 2. 逐个国内镜像重试
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. 全失败 —— 解析出哪些包没装上 (用 import 验证, 比 parse pip 输出可靠)
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
- pkg_names = [a for a in args if not a.startswith('-')]
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
- r = subprocess.run(cmd, cwd=str(BASE))
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
- r = subprocess.run([sys.executable, str(THIS_DIR / 'install_qoderwork.py')], cwd=str(BASE))
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: