msdevflow 0.7.5 → 0.7.6

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 CHANGED
@@ -9,7 +9,7 @@
9
9
  - Claude Code、Codex 或 OpenCode 中至少一个;
10
10
  - Node.js `>=18` 和 npm;
11
11
  - Git;
12
- - Python `>=3.10` 和 pip;
12
+ - Python `>=3.10`,且标准库 `venv` 模块可用;
13
13
  - GitCode 账号;
14
14
  - Chrome 或 Edge(推荐,用于 openLiBing OAuth)。
15
15
 
@@ -31,7 +31,7 @@ npx msdevflow@latest setup
31
31
 
32
32
  1. 把一个 `msd` 核心和十个 `msd-<action>` 路由器作为整套事务原子安装或更新到所需目标;
33
33
  2. 安装或升级官方 `@gitcode-cli/cli@latest`;
34
- 3. 安装受审的 Python Playwright 依赖;
34
+ 3. 创建或修复 msdevflow 受管 Python venv,并在其中安装受审的 Playwright 依赖;
35
35
  4. 验收 Issue、PR、结构化评论、行内评论、JSON、显式仓库和正文文件等 CLI 能力;
36
36
  5. GitCode CLI 未认证时打开官方 Token 创建页面,并让 CLI 自己的终端提示接收 Token。
37
37
 
@@ -67,6 +67,15 @@ npx msdevflow setup --agents-skills-dir "<shared-agent-skills-directory>"
67
67
 
68
68
  `--skills-dir` 未配合 `--targets` 使用时保留旧语义,只安装 Claude Code 目标。没有检测到客户端时,交互运行会要求选择目标;非交互运行必须提供 `--targets`。安装完成后重启或重新加载对应客户端。如果机器已有 Python 版本的 `gitcode`,setup 会保留它,并为 npm CLI 创建 `gitcode-npm` 命令;否则使用 `gitcode`。
69
69
 
70
+ setup 在所有平台使用独立受管 venv,不向系统 Python 安装包:
71
+
72
+ ```text
73
+ Windows: %LOCALAPPDATA%\msdevflow\python
74
+ macOS/Linux: ${XDG_DATA_HOME:-$HOME/.local/share}/msdevflow/python
75
+ ```
76
+
77
+ 可用 `MSDEVFLOW_PYTHON_DIR` 覆盖,值必须是绝对路径或以 `~` 开头;setup 与后续 action 必须使用同一配置。`venv` 属于 CPython 标准库,但精简发行版可能未安装;Debian/Ubuntu 通常需要用户自行安装 `python3-venv`。setup 不执行 sudo,不回退到全局 pip,也不使用 `--break-system-packages`。Windows 官方 Python 和 Homebrew Python 通常已包含 `venv`。
78
+
70
79
  ## 运行与 action 补全
71
80
 
72
81
  从目标仓库目录启动客户端。直接调用核心 `msd` 会运行完整作者 E2E:
package/lib/bootstrap.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  readdirSync,
12
12
  readSync,
13
13
  renameSync,
14
+ rmdirSync,
14
15
  rmSync,
15
16
  unlinkSync,
16
17
  writeFileSync,
@@ -27,6 +28,8 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
27
28
  const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skill");
28
29
  const CORE_SKILL_NAME = "msd";
29
30
  const LEGACY_SKILL_NAME = "msdevflow";
31
+ const PYTHON_RUNTIME_MARKER = ".msdevflow-python-runtime";
32
+ const PYTHON_RUNTIME_MARKER_CONTENT = "managed-by=msdevflow\n";
30
33
  export const ACTIONS = [
31
34
  "discover",
32
35
  "create-issue",
@@ -946,6 +949,176 @@ function environmentHome(environment, platform) {
946
949
  return environment.HOME || homedir();
947
950
  }
948
951
 
952
+ function pythonRuntimeAt(directory, python, platform) {
953
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
954
+ return {
955
+ baseExecutable: python.executable,
956
+ baseVersion: python.version,
957
+ directory,
958
+ executable: pathApi.join(
959
+ directory,
960
+ platform === "win32" ? "Scripts" : "bin",
961
+ platform === "win32" ? "python.exe" : "python",
962
+ ),
963
+ marker: pathApi.join(directory, PYTHON_RUNTIME_MARKER),
964
+ platform,
965
+ };
966
+ }
967
+
968
+ function expandHomePath(value, home, platform, variable) {
969
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
970
+ let expanded = value;
971
+ if (value === "~") {
972
+ expanded = home;
973
+ } else {
974
+ const homeRelative = platform === "win32" ? /^~[\\/]/ : /^~\//;
975
+ if (homeRelative.test(value)) {
976
+ expanded = pathApi.join(home, value.slice(2));
977
+ }
978
+ }
979
+ if (!pathApi.isAbsolute(expanded) && !path.isAbsolute(expanded)) {
980
+ throw new BootstrapError(`${variable} must be an absolute path or start with ~.`, 2);
981
+ }
982
+ return expanded;
983
+ }
984
+
985
+ export function pythonRuntimeDetails(
986
+ python,
987
+ environment = process.env,
988
+ platform = process.platform,
989
+ ) {
990
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
991
+ const home = environmentHome(environment, platform);
992
+ const configured = environment.MSDEVFLOW_PYTHON_DIR;
993
+ const directory = configured
994
+ ? expandHomePath(configured, home, platform, "MSDEVFLOW_PYTHON_DIR")
995
+ : platform === "win32"
996
+ ? pathApi.join(environment.LOCALAPPDATA || pathApi.join(home, "AppData", "Local"), "msdevflow", "python")
997
+ : pathApi.join(
998
+ environment.XDG_DATA_HOME
999
+ ? expandHomePath(environment.XDG_DATA_HOME, home, platform, "XDG_DATA_HOME")
1000
+ : pathApi.join(home, ".local", "share"),
1001
+ "msdevflow",
1002
+ "python",
1003
+ );
1004
+ return pythonRuntimeAt(directory, python, platform);
1005
+ }
1006
+
1007
+ function validatePythonRuntime(runtime, run) {
1008
+ let marker;
1009
+ try {
1010
+ const metadata = lstatSync(runtime.directory);
1011
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
1012
+ throw new BootstrapError(
1013
+ `Refusing to use a Python runtime path not owned by msdevflow: ${runtime.directory}`,
1014
+ 3,
1015
+ );
1016
+ }
1017
+ marker = readOptionalFile(
1018
+ runtime.marker,
1019
+ `Refusing to use an invalid msdevflow Python runtime marker: ${runtime.marker}`,
1020
+ );
1021
+ } catch (error) {
1022
+ if (error?.code === "ENOENT") {
1023
+ return "absent";
1024
+ }
1025
+ throw error;
1026
+ }
1027
+ if (marker !== PYTHON_RUNTIME_MARKER_CONTENT) {
1028
+ throw new BootstrapError(
1029
+ `Refusing to use a Python runtime directory not owned by msdevflow: ${runtime.directory}`,
1030
+ 3,
1031
+ );
1032
+ }
1033
+ let descriptor;
1034
+ try {
1035
+ descriptor = openSync(runtime.executable, "r");
1036
+ } catch (error) {
1037
+ if (error?.code === "ENOENT") {
1038
+ return "repair";
1039
+ }
1040
+ throw error;
1041
+ } finally {
1042
+ if (descriptor !== undefined) {
1043
+ closeSync(descriptor);
1044
+ }
1045
+ }
1046
+ if (runOptional(run, {
1047
+ command: runtime.executable,
1048
+ args: ["-m", "pip", "--version"],
1049
+ }).status !== 0) {
1050
+ return "repair";
1051
+ }
1052
+ return "current";
1053
+ }
1054
+
1055
+ function writePythonRuntimeMarker(runtime) {
1056
+ const existing = readOptionalFile(
1057
+ runtime.marker,
1058
+ `Refusing to replace an invalid msdevflow Python runtime marker: ${runtime.marker}`,
1059
+ );
1060
+ if (existing === PYTHON_RUNTIME_MARKER_CONTENT) {
1061
+ return;
1062
+ }
1063
+ if (existing !== null) {
1064
+ throw new BootstrapError(
1065
+ `Refusing to replace an unrelated Python runtime marker: ${runtime.marker}`,
1066
+ 3,
1067
+ );
1068
+ }
1069
+ writeFileSync(runtime.marker, PYTHON_RUNTIME_MARKER_CONTENT, { encoding: "utf8", flag: "wx" });
1070
+ }
1071
+
1072
+ async function ensurePythonRuntime(runtime, status, runLong, run) {
1073
+ if (validatePythonRuntime(runtime, run) !== status) {
1074
+ throw new BootstrapError(
1075
+ `Managed Python runtime changed after confirmation: ${runtime.directory}`,
1076
+ 3,
1077
+ );
1078
+ }
1079
+ if (status === "absent") {
1080
+ mkdirSync(path.dirname(runtime.directory), { recursive: true });
1081
+ try {
1082
+ mkdirSync(runtime.directory);
1083
+ } catch (error) {
1084
+ if (error?.code === "EEXIST") {
1085
+ throw new BootstrapError(
1086
+ `Managed Python runtime changed after confirmation: ${runtime.directory}`,
1087
+ 3,
1088
+ );
1089
+ }
1090
+ throw error;
1091
+ }
1092
+ try {
1093
+ writePythonRuntimeMarker(runtime);
1094
+ } catch (error) {
1095
+ try {
1096
+ rmdirSync(runtime.directory);
1097
+ } catch {
1098
+ // Preserve the marker failure as the actionable error.
1099
+ }
1100
+ throw error;
1101
+ }
1102
+ }
1103
+ if (status !== "current") {
1104
+ try {
1105
+ await runLong({
1106
+ command: runtime.baseExecutable,
1107
+ args: ["-m", "venv", runtime.directory],
1108
+ });
1109
+ } catch (error) {
1110
+ throw new BootstrapError(
1111
+ `Python venv is unavailable for ${runtime.baseExecutable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup. ${error.message}`,
1112
+ 2,
1113
+ );
1114
+ }
1115
+ }
1116
+ verifyFile(
1117
+ runtime.executable,
1118
+ `Managed Python runtime was not created correctly: ${runtime.executable}`,
1119
+ );
1120
+ }
1121
+
949
1122
  export function gitcodeInstallDetails(
950
1123
  classification,
951
1124
  environment = process.env,
@@ -993,11 +1166,11 @@ export function gitcodeInstallDetails(
993
1166
  };
994
1167
  }
995
1168
 
996
- function readOptionalFile(file) {
1169
+ function readOptionalFile(file, invalidMessage = `Refusing to overwrite unrelated wrapper: ${file}`) {
997
1170
  try {
998
1171
  const metadata = lstatSync(file);
999
1172
  if (metadata.isSymbolicLink() || !metadata.isFile()) {
1000
- throw new BootstrapError(`Refusing to overwrite unrelated wrapper: ${file}`, 3);
1173
+ throw new BootstrapError(invalidMessage, 3);
1001
1174
  }
1002
1175
  return readFileSync(file, "utf8");
1003
1176
  } catch (error) {
@@ -1142,7 +1315,11 @@ function printPlan(plan, write) {
1142
1315
  write(` Wrapper target: ${plan.gitcodeInstall.cliTarget}`);
1143
1316
  }
1144
1317
  write(` npm command: ${formatCommand(plan.installInvocation)}`);
1145
- write(` Python: ${plan.python.executable} (${plan.python.version.join(".")})`);
1318
+ write(` Base Python: ${plan.python.executable} (${plan.python.version.join(".")})`);
1319
+ write(` Managed Python runtime: ${plan.pythonRuntime.directory} (${plan.pythonRuntime.status})`);
1320
+ if (plan.pythonRuntime.status === "absent") {
1321
+ write(` Python venv command: ${formatCommand(plan.venvInvocation)}`);
1322
+ }
1146
1323
  write(` Python dependency command: ${formatCommand(plan.pipInvocation)}`);
1147
1324
  write(" Browser binary download: disabled");
1148
1325
  write(` GitCode authentication command when needed: ${plan.gitcodeInstall.workflowCommand} auth login --web`);
@@ -1259,8 +1436,9 @@ function textResult(result, write) {
1259
1436
  if (result.gitcode.authentication.username) {
1260
1437
  write(` Username: ${result.gitcode.authentication.username}`);
1261
1438
  }
1262
- write(` Python: ${result.python.executable} ${result.python.version}`);
1263
- write(" Playwright Python package: installed");
1439
+ write(` Base Python: ${result.python.baseExecutable} ${result.python.baseVersion}`);
1440
+ write(` Managed Python runtime: ${result.python.executable}`);
1441
+ write(" Playwright Python package: installed in managed runtime");
1264
1442
  write(" Chromium: not downloaded");
1265
1443
  }
1266
1444
 
@@ -1334,9 +1512,27 @@ export async function runSetup(options, dependencies = {}) {
1334
1512
  );
1335
1513
 
1336
1514
  const python = detectPython(run, platform);
1337
- run({ command: python.executable, args: ["-m", "pip", "--version"] });
1338
- const pipInvocation = {
1515
+ const venvCapability = runOptional(run, {
1339
1516
  command: python.executable,
1517
+ args: ["-m", "venv", "--help"],
1518
+ });
1519
+ if (venvCapability.status !== 0) {
1520
+ throw new BootstrapError(
1521
+ `Python venv is unavailable for ${python.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
1522
+ 2,
1523
+ );
1524
+ }
1525
+ const configuredPythonRuntime = pythonRuntimeDetails(python, environment, platform);
1526
+ const pythonRuntime = dependencies.pythonRuntimeDir
1527
+ ? pythonRuntimeAt(dependencies.pythonRuntimeDir, python, platform)
1528
+ : configuredPythonRuntime;
1529
+ pythonRuntime.status = validatePythonRuntime(pythonRuntime, run);
1530
+ const venvInvocation = {
1531
+ command: python.executable,
1532
+ args: ["-m", "venv", pythonRuntime.directory],
1533
+ };
1534
+ const pipInvocation = {
1535
+ command: pythonRuntime.executable,
1340
1536
  args: ["-m", "pip", "install", "-r", layouts[0].bundledPythonRequirements],
1341
1537
  };
1342
1538
  const plan = {
@@ -1347,6 +1543,8 @@ export async function runSetup(options, dependencies = {}) {
1347
1543
  installInvocation,
1348
1544
  gitcodeInstall,
1349
1545
  python,
1546
+ pythonRuntime,
1547
+ venvInvocation,
1350
1548
  pipInvocation,
1351
1549
  };
1352
1550
  printPlan(plan, writePlan);
@@ -1365,6 +1563,20 @@ export async function runSetup(options, dependencies = {}) {
1365
1563
  validateWrapper(gitcodeInstall, platform);
1366
1564
  }
1367
1565
 
1566
+ await progress.run(
1567
+ pythonRuntime.status === "absent"
1568
+ ? "Creating the managed Python runtime"
1569
+ : pythonRuntime.status === "repair"
1570
+ ? "Repairing the managed Python runtime"
1571
+ : "Validating the managed Python runtime",
1572
+ () => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong, run),
1573
+ );
1574
+ await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
1575
+ await progress.run(
1576
+ "Verifying the Playwright Python package",
1577
+ () => runLong({ command: pythonRuntime.executable, args: ["-c", "import playwright.sync_api"] }),
1578
+ );
1579
+
1368
1580
  const installed = await progress.run(
1369
1581
  "Installing the official GitCode CLI",
1370
1582
  () => installGitcode(plan, runLong, run, platform),
@@ -1375,12 +1587,6 @@ export async function runSetup(options, dependencies = {}) {
1375
1587
  );
1376
1588
  gitcode.command = installed.command;
1377
1589
  gitcode.executable = installed.executable;
1378
-
1379
- await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
1380
- await progress.run(
1381
- "Verifying the Playwright Python package",
1382
- () => runLong({ command: python.executable, args: ["-c", "import playwright.sync_api"] }),
1383
- );
1384
1590
  gitcode.authentication = await authenticateGitcode(
1385
1591
  installed.executable,
1386
1592
  gitcode.authentication,
@@ -1411,8 +1617,10 @@ export async function runSetup(options, dependencies = {}) {
1411
1617
  skills,
1412
1618
  gitcode,
1413
1619
  python: {
1414
- executable: python.executable,
1415
- version: python.version.join("."),
1620
+ baseExecutable: python.executable,
1621
+ baseVersion: python.version.join("."),
1622
+ runtimeDirectory: pythonRuntime.directory,
1623
+ executable: pythonRuntime.executable,
1416
1624
  playwright: "installed",
1417
1625
  chromium: "not-downloaded",
1418
1626
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -146,7 +146,7 @@ PR:https://gitcode.com/Ascend/example/pulls/456
146
146
  本地仓库:D:\work\example
147
147
  ```
148
148
 
149
- CI 通过后立即停止,不处理 feedback 或 merge。识别为 openLiBing 且只读 API 返回 401/403 时,可自动安装 Python Playwright 依赖并打开已有的系统 Chrome/Edge 或 Playwright Chromium,认证成功后继续本 action;不会自动下载浏览器,浏览器不可用时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。可用 `autonomous-ci` 只授权同一 PR 上根因明确的最小 CI 修复循环。
149
+ CI 通过后立即停止,不处理 feedback 或 merge。识别为 openLiBing 且只读 API 返回 401/403 时,使用 setup 创建的受管 Python 运行时和其中的 Playwright 打开已有的系统 Chrome/Edge 或 Playwright Chromium,认证成功后继续本 action;运行时不可用时停止并要求运行 `npx msdevflow@latest setup`,不会在 action 内调用 pip,也不会自动下载浏览器。浏览器不可用时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。可用 `autonomous-ci` 只授权同一 PR 上根因明确的最小 CI 修复循环。
150
150
 
151
151
  ### 只验证 openLiBing OAuth
152
152
 
@@ -155,7 +155,7 @@ CI 通过后立即停止,不处理 feedback 或 merge。识别为 openLiBing
155
155
  本地仓库:D:\work\example
156
156
  ```
157
157
 
158
- 本 action 从当前工作区唯一识别 canonical,只在该仓库内按“当前分支关联 PR → 最近开放 PR → 最近合入 PR”的顺序选择一个包含 openLiBing run 的验证目标,不要求用户提供 PR。它可自动安装 Python Playwright 依赖,并使用已有的系统 Chrome/Edge 或 Playwright Chromium打开可见窗口;不会自动下载浏览器。用户亲自完成 GitCode 登录和授权。浏览器不可用时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`;人工浏览器登录不等于验证完成。只有 OAuth 后对固定 run 的真实只读请求成功才完成,随后立即停止,不进入 `ci`。
158
+ 本 action 从当前工作区唯一识别 canonical,只在该仓库内按“当前分支关联 PR → 最近开放 PR → 最近合入 PR”的顺序选择一个包含 openLiBing run 的验证目标,不要求用户提供 PR。它使用 setup 创建的受管 Python 运行时和其中的 Playwright,并使用已有的系统 Chrome/Edge 或 Playwright Chromium 打开可见窗口;运行时不可用时要求运行 `npx msdevflow@latest setup`,不会在 action 内调用 pip,也不会自动下载浏览器。用户亲自完成 GitCode 登录和授权。浏览器不可用时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`;人工浏览器登录不等于验证完成。只有 OAuth 后对固定 run 的真实只读请求成功才完成,随后立即停止,不进入 `ci`。
159
159
 
160
160
  它不读取或输出 GitCode Token。openLiBing Token 只驻留当前 Python 进程,固定显示为 `redacted`;持久化的只有受管专用 profile 中的 GitCode 浏览器登录态。
161
161
 
@@ -241,13 +241,23 @@ npx msdevflow@latest setup
241
241
 
242
242
  `setup` 是幂等的:内容一致时保持 `current`,内容不同时原子更新。
243
243
 
244
- `setup` 默认检测 PATH 中的 Claude Code、Codex 和 OpenCode,先展示客户端、物理 skill 目标、安装或更新状态、GitCode CLI 归属、registry、安装模式、Python 依赖命令和认证方式;用户确认后,在同一次运行中:
244
+ `setup` 默认检测 PATH 中的 Claude Code、Codex 和 OpenCode,先展示客户端、物理 skill 目标、安装或更新状态、GitCode CLI 归属、registry、安装模式、受管 Python 运行时、依赖命令和认证方式;用户确认后,在同一次运行中:
245
245
 
246
- 1. npm 包内的 `skill/msd` 核心和十个 `skill/msd-<action>` 路由器原子安装或更新整套入口;
247
- 2. 安装或升级官方 GitCode npm CLI;
248
- 3. 安装内置 `scripts/requirements.txt` 中的 Python Playwright 包;
246
+ 1. 创建、修复或复用 msdevflow 受管 Python venv;
247
+ 2. 用受管 Python 安装内置 `scripts/requirements.txt` 中的 Playwright 包;
248
+ 3. 安装或升级官方 GitCode npm CLI;
249
249
  4. 验收 workflow 所需 schema/API;
250
- 5. CLI 未认证时运行选定命令的 `auth login --web`。
250
+ 5. CLI 未认证时运行选定命令的 `auth login --web`;
251
+ 6. 从 npm 包内的 `skill/msd` 核心和十个 `skill/msd-<action>` 路由器原子安装或更新整套入口。
252
+
253
+ 受管运行时默认位于:
254
+
255
+ ```text
256
+ Windows: %LOCALAPPDATA%\msdevflow\python
257
+ macOS/Linux: ${XDG_DATA_HOME:-$HOME/.local/share}/msdevflow/python
258
+ ```
259
+
260
+ 可用 `MSDEVFLOW_PYTHON_DIR` 覆盖,值必须是绝对路径或以 `~` 开头;setup 与后续 action 必须使用同一配置。`venv` 属于 CPython 标准库,但精简发行版可能缺失;Debian/Ubuntu 通常需要用户自行安装 `python3-venv`。setup 不执行 sudo,不回退到全局 pip,也不使用 `--break-system-packages`。来源不明或 marker 损坏的运行时目录不会被接管。
251
261
 
252
262
  客户端到物理目录的映射:
253
263
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  不要把一个仓库的 `compile`、label 或机器人协议迁移到另一个仓库。
13
13
 
14
- 若评论/文档明确为 openLiBing,按需加载 [openlibing-ci.md](openlibing-ci.md);先用 GitCode 评论定位 run/job,再尝试只读 detail/log API。401/403 时,显式 `action=ci` 和完整 E2E 都直接进入安全 OAuth 子流程,允许自动安装 Python Playwright 依赖并打开已有的系统 Chrome/Edge 或 Playwright Chromium,但不自动下载浏览器;成功后恢复原 CI 诊断,浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`。不得把 GitCode Actions 当作 openLiBing。
14
+ 若评论/文档明确为 openLiBing,按需加载 [openlibing-ci.md](openlibing-ci.md);先用 GitCode 评论定位 run/job,再尝试只读 detail/log API。401/403 时,显式 `action=ci` 和完整 E2E 都直接进入安全 OAuth 子流程,使用 setup 创建的受管 Python 运行时和其中的 Playwright,并打开已有的系统 Chrome/Edge 或 Playwright Chromium;action 内不调用 pip,也不自动下载浏览器。受管运行时不可用时要求运行 `npx msdevflow@latest setup`;成功后恢复原 CI 诊断,浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`。不得把 GitCode Actions 当作 openLiBing。
15
15
 
16
16
  记录适配器:
17
17
 
@@ -42,7 +42,7 @@ CLI 缺失、版本低于推荐下限或所需 schema 不存在时,当前 acti
42
42
  npx msdevflow setup
43
43
  ```
44
44
 
45
- 独立 setup 会在同一次确认后安装或更新 npm 包内置的 `msdevflow` skill,安装或升级官方 npm 包 `@gitcode-cli/cli@latest`,保留已有 Python `gitcode`,默认安装内置 `scripts/requirements.txt` 中的 Playwright,并验收 workflow 所需 schema/API。它不下载 Playwright Chromium。若 CLI 未认证,setup 会启动 `<gitcode-command> auth login --web` 的官方浏览器流程;凭证由 GitCode CLI 自己接收和保存,setup 不读取、打印或转存 Token,也不要求用户把 Token 传给 Agent。workflow 不在 action 内隐式安装依赖。setup 成功后重新探测环境并恢复原 action;不要 fallback 到可能安装旧实现的 PyPI `gitcode-cli`。
45
+ 独立 setup 会在同一次确认后安装或更新 npm 包内置的 `msd` Skill 套件,安装或升级官方 npm 包 `@gitcode-cli/cli@latest`,保留已有 Python `gitcode`,创建或修复 msdevflow 受管 Python venv,在其中安装 `scripts/requirements.txt` Playwright,并验收 workflow 所需 schema/API。它不修改系统 Python、不使用 `--break-system-packages`、不回退到全局 pip,也不下载 Playwright Chromium。若 Python 发行版缺少 `venv`,setup 明确阻断;Debian/Ubuntu 通常需要用户自行安装 `python3-venv`。若 CLI 未认证,setup 会启动 `<gitcode-command> auth login --web` 的官方浏览器流程;凭证由 GitCode CLI 自己接收和保存,setup 不读取、打印或转存 Token,也不要求用户把 Token 传给 Agent。workflow 不在 action 内隐式安装依赖。setup 成功后重新探测环境并恢复原 action;不要 fallback 到可能安装旧实现的 PyPI `gitcode-cli`。
46
46
 
47
47
  ## 能力降级顺序
48
48
 
@@ -20,7 +20,14 @@
20
20
 
21
21
  ### 执行和完成证据
22
22
 
23
- setup 默认安装 `scripts/requirements.txt` 中的 Python Playwright 依赖;脚本在依赖缺失时也可从同一清单受控恢复,但不下载 Playwright Chromium。脚本优先使用系统 Chrome/Edge,其次使用当前 Playwright 环境中已经存在的 Chromium。然后运行:
23
+ setup 在所有平台创建或修复一个 msdevflow 受管 Python venv,并在其中安装 `scripts/requirements.txt` Playwright;脚本进入 OAuth 流程时自动重启到该受管解释器。受管运行时或 Playwright 不可用时要求运行 `npx msdevflow@latest setup`,脚本自身不调用 pip,不修改系统 Python,也不下载 Playwright Chromium。脚本优先使用系统 Chrome/Edge,其次使用受管 Playwright 环境中已经存在的 Chromium。然后运行:
24
+
25
+ ```text
26
+ Windows: %LOCALAPPDATA%\msdevflow\python
27
+ macOS/Linux: ${XDG_DATA_HOME:-$HOME/.local/share}/msdevflow/python
28
+ ```
29
+
30
+ 可用 `MSDEVFLOW_PYTHON_DIR` 覆盖,值必须是绝对路径或以 `~` 开头;setup 与运行 action 时必须保持一致。
24
31
 
25
32
  ```bash
26
33
  python <skill-dir>/scripts/openlibing_ci.py login-check \
@@ -85,7 +92,7 @@ python <skill-dir>/scripts/openlibing_ci.py diagnose \
85
92
  python <skill-dir>/scripts/openlibing_ci.py diagnose ... --oauth
86
93
  ```
87
94
 
88
- `--oauth` 需要 Python Playwright,以及已有的系统 Chrome/Edge 或当前 Playwright 环境中已经存在的 Chromium。脚本可从 `scripts/requirements.txt` 自动安装受审依赖,但不会执行 `playwright install chromium`;浏览器可用时打开本机可见窗口让用户自己完成 GitCode 登录/授权,浏览器不可用时只输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。捕获的 openLiBing token 只驻留当前 Python 进程内,不打印、不落盘。
95
+ `--oauth` 需要 setup 已准备好的受管 Python 运行时及其中的 Playwright,以及已有的系统 Chrome/Edge 或受管 Playwright 环境中已经存在的 Chromium。脚本不调用 pip,也不会执行 `playwright install chromium`;受管运行时或依赖缺失时停止并要求运行 `npx msdevflow@latest setup`。浏览器可用时打开本机可见窗口让用户自己完成 GitCode 登录/授权,浏览器不可用时只输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。捕获的 openLiBing token 只驻留当前 Python 进程内,不打印、不落盘。
89
96
 
90
97
  默认使用专用持久浏览器 profile:
91
98
 
@@ -90,7 +90,7 @@ Suggested next action: ci
90
90
  4. 分类当前改动、连带失败、canonical 基线、基础设施、权限或证据不足;
91
91
  5. guided 模式确认修复;`autonomous-ci` 只在已授权最小范围内自动修改、验证、新 commit、push 和重触发;
92
92
  6. 新 push 后获取新 head,旧 head 结果失效;
93
- 7. openLiBing detail/log 返回 401/403 时,按 [openlibing-ci.md](openlibing-ci.md) 自动安装缺失的 Python Playwright 依赖,并使用已有的系统 Chrome/Edge 或 Playwright Chromium 打开可见浏览器;不自动下载浏览器。用户完成 GitCode 登录/授权且固定 run 验证成功后恢复当前 CI 诊断;浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`;
93
+ 7. openLiBing detail/log 返回 401/403 时,按 [openlibing-ci.md](openlibing-ci.md) 使用 setup 创建的受管 Python 运行时和其中的 Playwright,并用已有的系统 Chrome/Edge 或 Playwright Chromium 打开可见浏览器;action 内不调用 pip,也不自动下载浏览器。受管运行时不可用时要求运行 `npx msdevflow@latest setup`;用户完成 GitCode 登录/授权且固定 run 验证成功后恢复当前 CI 诊断,浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`;
94
94
  8. 循环至当前 head 全绿或形成证据充分的 blocker。
95
95
 
96
96
  重试不能代替根因分析。CI 修复不得扩大 Issue 范围;发现产品实现缺失或方案错误时返回 `blocked` 并建议 `action=develop`,不得把 CI action 变成补开发流程。
@@ -100,7 +100,7 @@ Agent 创建或修改的 Issue/PR 正文,以及发布的 Issue 评论、PR 普
100
100
 
101
101
  `autonomous-ci` 只授权在同一 PR 的 CI 修复循环中:修改与根因直接相关的代码、运行门禁、创建新 commit、push 同一 source branch、重触发 CI。它不授权扩大需求、force push、降低测试、处理 feedback、review、approve 或 merge。
102
102
 
103
- 显式 `openlibing-auth`、显式 `ci` 和完整 E2E 均授权在需要 openLiBing OAuth 时自动安装配套 Python Playwright 依赖并打开可见浏览器,但不授权下载 Playwright Chromium。优先使用系统 Chrome/Edge,其次使用已存在的 Playwright Chromium;均不可用或 Playwright 无法启动时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。该链接只供人工浏览器访问,不能证明当前进程已认证或固定 run 已验证。该授权不允许 Agent 代替用户填写 GitCode 凭证、点击授权同意、读取浏览器 Cookie 值或导出任何 Token。
103
+ 显式 `openlibing-auth`、显式 `ci` 和完整 E2E 均授权在需要 openLiBing OAuth 时使用 setup 创建的受管 Python 运行时和其中的 Playwright 打开可见浏览器,但不授权在 action 内调用 pip,也不授权下载 Playwright Chromium。受管运行时不可用时要求用户运行 `npx msdevflow@latest setup`。优先使用系统 Chrome/Edge,其次使用已存在的 Playwright Chromium;均不可用或 Playwright 无法启动时输出可复制的 OAuth 链接并停止为 `blocked: browser-required`。该链接只供人工浏览器访问,不能证明当前进程已认证或固定 run 已验证。该授权不允许 Agent 代替用户填写 GitCode 凭证、点击授权同意、读取浏览器 Cookie 值或导出任何 Token。
104
104
 
105
105
  ## Guided 检查点
106
106
 
@@ -8,7 +8,6 @@ import os
8
8
  import re
9
9
  import shutil
10
10
  import stat
11
- import subprocess
12
11
  import sys
13
12
  import time
14
13
  import urllib.error
@@ -79,6 +78,59 @@ LEGACY_PROFILE_MARKERS = {
79
78
  },
80
79
  }
81
80
  PROFILE_METADATA = "session.json"
81
+ PYTHON_RUNTIME_MARKER = ".msdevflow-python-runtime"
82
+ PYTHON_RUNTIME_MARKER_CONTENT = "managed-by=msdevflow\n"
83
+
84
+
85
+ def configured_directory(value: str, variable: str) -> Path:
86
+ selected = Path(value).expanduser()
87
+ if not selected.is_absolute():
88
+ raise OpenLibingAuthRequired(
89
+ f"{variable} 必须是绝对路径或以 ~ 开头。请运行 npx msdevflow@latest setup。"
90
+ )
91
+ return selected
92
+
93
+
94
+ def default_python_runtime_dir() -> Path:
95
+ configured = os.getenv("MSDEVFLOW_PYTHON_DIR")
96
+ if configured:
97
+ return configured_directory(configured, "MSDEVFLOW_PYTHON_DIR")
98
+ if os.name == "nt":
99
+ local_app_data = os.getenv("LOCALAPPDATA")
100
+ root = (
101
+ configured_directory(local_app_data, "LOCALAPPDATA")
102
+ if local_app_data
103
+ else Path.home() / "AppData" / "Local"
104
+ )
105
+ return root / "msdevflow" / "python"
106
+ data_home = os.getenv("XDG_DATA_HOME")
107
+ root = (
108
+ configured_directory(data_home, "XDG_DATA_HOME")
109
+ if data_home
110
+ else Path.home() / ".local" / "share"
111
+ )
112
+ return root / "msdevflow" / "python"
113
+
114
+
115
+ def managed_python_executable(runtime_dir: Path | None = None) -> Path:
116
+ selected = (runtime_dir or default_python_runtime_dir()).expanduser()
117
+ executable = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
118
+ marker = selected / PYTHON_RUNTIME_MARKER
119
+ try:
120
+ owned = (
121
+ selected.is_dir()
122
+ and not selected.is_symlink()
123
+ and marker.is_file()
124
+ and not marker.is_symlink()
125
+ and marker.read_text(encoding="utf-8") == PYTHON_RUNTIME_MARKER_CONTENT
126
+ )
127
+ except OSError:
128
+ owned = False
129
+ if not owned or not executable.is_file():
130
+ raise OpenLibingAuthRequired(
131
+ f"msdevflow 受管 Python 运行时不可用:{selected}。请运行 npx msdevflow@latest setup。"
132
+ )
133
+ return executable
82
134
 
83
135
 
84
136
  def profile_marker(profile_dir: Path) -> Path:
@@ -181,29 +233,18 @@ def clear_openlibing_storage(context: Any, page: Any, base_url: str) -> None:
181
233
  raise OpenLibingAuthRequired("无法从持久 profile 清除 openLiBing Cookie,已拒绝保存会话。")
182
234
 
183
235
 
184
- def oauth_requirements_file() -> Path:
185
- return Path(__file__).with_name("requirements.txt")
186
-
187
-
188
236
  def ensure_playwright() -> None:
237
+ executable = managed_python_executable()
238
+ active_prefix = os.path.normcase(str(Path(sys.prefix).resolve()))
239
+ managed_prefix = os.path.normcase(str(default_python_runtime_dir().resolve()))
240
+ if active_prefix != managed_prefix:
241
+ os.execv(str(executable), [str(executable), str(Path(__file__).resolve()), *sys.argv[1:]])
189
242
  try:
190
243
  importlib.import_module("playwright.sync_api")
191
- return
192
- except ImportError:
193
- requirements = oauth_requirements_file()
194
- if not requirements.is_file():
195
- raise OpenLibingAuthRequired("缺少 openLiBing OAuth 依赖清单,无法自动安装 Playwright。")
196
- result = subprocess.run(
197
- [sys.executable, "-m", "pip", "install", "-r", str(requirements)],
198
- check=False,
199
- )
200
- if result.returncode != 0:
201
- raise OpenLibingAuthRequired("自动安装 Playwright 失败。")
202
- importlib.invalidate_caches()
203
- try:
204
- importlib.import_module("playwright.sync_api")
205
- except ImportError as error:
206
- raise OpenLibingAuthRequired("Playwright 安装完成但当前 Python 无法导入。") from error
244
+ except ImportError as error:
245
+ raise OpenLibingAuthRequired(
246
+ "受管 Python 运行时缺少 Playwright。请运行 npx msdevflow@latest setup。"
247
+ ) from error
207
248
 
208
249
 
209
250
  def playwright_chromium_path() -> Path: