fix(computer-use): use marker file to detect interpreter changes reliably

The previous fix compared the venv's pyvenv.cfg `home` field with the
custom interpreter's directory, but Python's venv module skips the
wrapping venv when the source interpreter is itself in a venv (conda /
pyenv / hand-built venvs — which is the common case). The recorded
`home` then points to the base Python install, not the user-provided
path, causing the comparison to permanently fail and the UI to keep
showing "venv not ready" even right after a successful rebuild.

Switch to a marker file (~/.claude/.runtime/venv-base-interpreter.txt)
that records the exact `config.pythonPath` used at venv creation time.
Status check compares it verbatim with the current config. Falls back
to "match if both empty" when the marker is missing, which preserves
behavior for legacy users who never set a custom path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-10 20:36:15 +08:00
parent 339d0d860d
commit 65548d3949

View File

@ -38,6 +38,9 @@ const claudeHome = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude')
const runtimeStateRoot = join(claudeHome, '.runtime')
const venvRoot = join(runtimeStateRoot, 'venv')
const installStampPath = join(runtimeStateRoot, 'requirements.sha256')
// 记录上次创建 venv 时所用的 config.pythonPath 原值。读取该文件来判断当前
// venv 是否仍与最新的自定义路径配置一致。
const baseInterpreterMarkerPath = join(runtimeStateRoot, 'venv-base-interpreter.txt')
const isWindows = process.platform === 'win32'
const REQUIREMENTS_CONTENT = isWindows ? REQUIREMENTS_WIN32 : REQUIREMENTS_DARWIN
@ -78,28 +81,28 @@ async function pathExists(target: string): Promise<boolean> {
}
/**
* venv pyvenv.cfg Python venv venv
* `home = ...` venv Python
*
* venv config.pythonPath
*
* Python
*
* marker setup pyvenv.cfg
* Python venv conda/pyenv/ venv
* Python venv venv base
* pyvenv.cfg home base
*
* / true
* marker
* - current === '' venv
* true
* - current !== '' marker
* venv false
*/
async function venvBuiltFromInterpreter(
venvDir: string,
interpreterPath: string,
async function venvBaseInterpreterMatches(
currentCustomPath: string | null | undefined,
): Promise<boolean> {
const current = (currentCustomPath ?? '').trim()
try {
const cfg = await readFile(join(venvDir, 'pyvenv.cfg'), 'utf8')
const homeMatch = cfg.match(/^\s*home\s*=\s*(.+)$/m)
if (!homeMatch) return true
const recordedHome = homeMatch[1].trim()
const expectedHome = path.dirname(interpreterPath)
return recordedHome === expectedHome
const recorded = (await readFile(baseInterpreterMarkerPath, 'utf8')).trim()
return recorded === current
} catch {
return true
return current === ''
}
}
@ -184,12 +187,14 @@ async function checkStatus(): Promise<EnvStatus> {
config.pythonPath,
)
// 仅当用户配置了自定义 Python 路径时才校验现有 venv 是否由该解释器创建。
// 未配置自定义路径的用户不进入此分支effectiveVenvCreated === venvCreated
// 保持原有行为不变。
// 校验现有 venv 是否与当前 config.pythonPath 配置一致:
// - 老用户从未设过自定义路径 → marker 不存在 + current 为空 → 视为匹配,
// 原行为完全不变。
// - 配置变更(设置/切换/清空自定义路径)→ marker 与 current 不一致 →
// effectiveVenvCreated 置为 falseUI 提示需要重新 setup。
let effectiveVenvCreated = venvCreated
if (venvCreated && config.pythonPath) {
const matches = await venvBuiltFromInterpreter(venvRoot, config.pythonPath)
if (venvCreated) {
const matches = await venvBaseInterpreterMatches(config.pythonPath)
if (!matches) effectiveVenvCreated = false
}
@ -260,30 +265,29 @@ async function runSetup(): Promise<SetupResult> {
let venvExists = await pathExists(venvPython)
const config = await loadConfig()
// 仅在用户配置了自定义 Python 路径时才考虑重建:若现有 venv 不是由该解释器
// 创建,则删除以便后续步骤使用新解释器重建。未配置自定义路径时不动现有 venv。
if (venvExists && config.pythonPath) {
const matches = await venvBuiltFromInterpreter(venvRoot, config.pythonPath)
if (!matches) {
try {
await rm(venvRoot, { recursive: true, force: true })
// 同时清除 stamp否则 Step 5 会因 digest 匹配而跳过依赖安装,
// 导致重建出的 venv 没有依赖。
await rm(installStampPath, { force: true })
venvExists = false
steps.push({
name: 'venv_rebuild',
ok: true,
message: '检测到自定义解释器变更,已移除旧虚拟环境以便重建',
})
} catch (err) {
steps.push({
name: 'venv_rebuild',
ok: false,
message: `移除旧虚拟环境失败: ${err}`,
})
return { success: false, steps }
}
// 校验现有 venv 是否与当前 config.pythonPath 配置一致marker 机制详见
// venvBaseInterpreterMatches 注释)。不一致则删除以便后续步骤重建。
// 老用户从未设过自定义路径时此分支不会触发,原行为完全保留。
if (venvExists && !(await venvBaseInterpreterMatches(config.pythonPath))) {
try {
await rm(venvRoot, { recursive: true, force: true })
// 同时清除 stamp否则 Step 5 会因 digest 匹配而跳过依赖安装,
// 导致重建出的 venv 没有依赖。
await rm(installStampPath, { force: true })
await rm(baseInterpreterMarkerPath, { force: true })
venvExists = false
steps.push({
name: 'venv_rebuild',
ok: true,
message: '检测到自定义解释器变更,已移除旧虚拟环境以便重建',
})
} catch (err) {
steps.push({
name: 'venv_rebuild',
ok: false,
message: `移除旧虚拟环境失败: ${err}`,
})
return { success: false, steps }
}
}
@ -351,6 +355,17 @@ async function runSetup(): Promise<SetupResult> {
})
return { success: false, steps }
}
// 记录本次创建 venv 时所用的自定义路径配置,供后续 checkStatus 比对。
try {
await writeFile(baseInterpreterMarkerPath, config.pythonPath ?? '', 'utf8')
} catch (err) {
steps.push({
name: 'venv',
ok: false,
message: `写入虚拟环境标记文件失败: ${err}`,
})
return { success: false, steps }
}
steps.push({ name: 'venv', ok: true, message: '虚拟环境已创建' })
} else {
steps.push({ name: 'venv', ok: true, message: '虚拟环境已存在' })