fix: 重构版本源检测逻辑 + standalone 目录集中化 + Linux 平台检测补全 (#161)

* fix: Windows 版本检测错误——优先从活跃 CLI 读取版本,修复源判断和包检查顺序

- get_local_version() Windows 块新增活跃 CLI 优先检测(与 macOS 一致),
  避免残留的 standalone 汉化版目录被优先扫描到
- detect_installed_source() 修复 standalone 被错误映射为 official(应为 chinese)
- read_version_from_installation() 根据 classify_cli_source 动态决定
  package.json 检查顺序,避免硬编码汉化版优先导致官方版用户看到错误版本

🤖 Generated with [Qoder][https://qoder.com]

* fix: Windows 版本检测忽略残留文件,仅当 CLI 二进制存在时才读取版本

standalone 目录和 npm 全局目录中可能存在卸载后的残留 node_modules/
package.json,导致面板误判 OpenClaw 已安装并显示错误版本号。
现在在读取版本前先检查 openclaw.cmd 是否实际存在。

🤖 Generated with [Qoder][https://qoder.com]

* fix: 重构版本源检测逻辑,修复跨源切换后显示旧源的问题

- 新增 detect_source_from_cmd_shim() 通过读取 Windows .cmd shim 内容判断
  npm 包归属,替代不可靠的文件系统残留目录扫描
- 重写 detect_installed_source() Windows 检测块,优先使用 shim 内容信号
- upgrade_openclaw_inner() 跨源切换时清理 standalone 安装目录
- get_local_version() npm 段改用 shim 内容判断活跃包
- build_enhanced_path() 三平台添加 standalone 安装目录,避免 dashboard 超时
- 所有平台 fallback 从 "official" 改为 "unknown",支持非面板安装场景
- 前端 dashboard/about 支持 official/chinese/unknown 三源显示
- 新增 unknownSource i18n key(中/英/繁/日/韩)

🤖 Generated with [Qoder][https://qoder.com]

* fix: 移除 config.rs 末尾多余的闭合括号,修复编译错误

🤖 Generated with [Qoder][https://qoder.com]

* fix: 移除 config.rs 末尾重复的 configure_git_https 和 invalidate_path_cache 定义

🤖 Generated with [Qoder][https://qoder.com]

* refactor: standalone 目录集中化、unsafe set_var 适配、Linux 平台检测补全

- 提升 all_standalone_dirs() / standalone_install_dir() 为 pub(crate),
  作为全局唯一的 standalone 路径来源
- build_enhanced_path() 三平台块改为调用 config::all_standalone_dirs()
- service.rs 三平台 candidate 函数改为调用 super::config::all_standalone_dirs()
- utils.rs common_non_windows_cli_candidates() 改为调用集中函数
- std::env::set_var 包裹 unsafe 块,附 SAFETY 注释,适配 Rust 1.83+
- 补全 Linux detect_installed_source(): CLI 路径分类 -> symlink -> standalone -> npm list
- 补全 Linux get_local_version(): 活跃 CLI -> standalone VERSION -> symlink package.json

🤖 Generated with [Qoder][https://qoder.com]

* fix: service.rs 模块路径修正 super::config -> crate::commands::config

mod platform 内部 super 指向 service 模块而非 commands,
需要完整路径 crate::commands::config 才能访问 all_standalone_dirs()

🤖 Generated with [Qoder][https://qoder.com]

---------

Co-authored-by: SEVENTEEN-TAN <SEVENTEEN-TAN@users.noreply.github.com>
This commit is contained in:
SEVENTEEN-TAN
2026-03-30 22:50:11 +08:00
committed by GitHub
parent 61bfd56865
commit 87d7c227d8
8 changed files with 253 additions and 81 deletions

View File

@@ -14,6 +14,7 @@ export default {
checkingUpdate: _('检查更新中...', 'Checking for updates...', '檢查更新中...', '更新を確認中...', '업데이트 확인 중...', 'Đang kiểm tra cập nhật...', 'Verificando actualizaciones...', 'Verificando atualizações...', 'Проверка обновлений...', 'Vérification des mises à jour...', 'Suche nach Updates...'),
official: _('原版', 'Official', '', '公式', '공식', 'Chính thức', 'Oficial', 'Oficial', 'Официальный', 'Officiel', 'Offiziell'),
chinese: _('汉化版', 'Chinese', '漢化版', '中国語版', '중국어판'),
unknownSource: _('未知来源', 'Unknown Source', '未知來源', '不明なソース', '알 수 없는 출처'),
policyAhead: _('检测到你本地安装的是高于推荐稳定版的 {current},可能存在接口、事件或配置兼容性问题。建议回退到 {recommended};如果你要继续使用高版本,请自行验证兼容性并关注 issue / release。', 'Your local installation {current} is ahead of the recommended stable version. There may be API, event, or config compatibility issues. Consider rolling back to {recommended}; if you want to keep the newer version, verify compatibility yourself and watch issues/releases.', '檢測到你本地安裝的是高於推薦穩定版的 {current},可能存在介面、事件或設定相容性問題。建議回退到 {recommended};如果你要繼續使用高版本,請自行驗證相容性並關注 issue / release。'),
policyDefault: _('当前面板默认只保证推荐稳定版的兼容性;如果你要尝试其他版本或预览版,请自行验证兼容性。若希望面板尽快支持最新版特性,欢迎提交 issue 告诉我们。', 'This panel only guarantees compatibility with the recommended stable version. If you want to try other versions or previews, verify compatibility yourself. Submit an issue if you want us to support the latest version sooner.', '目前面板預設只保證推薦穩定版的相容性;如果你要尝試其他版本或預覽版,請自行驗證相容性。若希望面板尽快支援最新版特性,欢迎提交 issue 告诉我們。'),
notInstalled: _('未安装', 'Not installed', '未安裝', '未インストール', '미설치', 'Chưa cài đặt', 'No instalado', 'Não instalado', 'Не установлен', 'Non installé', 'Nicht installiert'),

View File

@@ -9,6 +9,7 @@ export default {
versionLabel: _('版本', 'Version', '', 'バージョン', '버전', 'Phiên bản', 'Versión', 'Versão', 'Версия'),
versionOfficial: _('官方', 'Official', '', '公式', '공식'),
versionChinese: _('汉化', 'Chinese', '漢化', '中国語版', '중국어판'),
versionUnknownSource: _('未知来源', 'Unknown Source', '未知來源', '不明なソース', '알 수 없는 출처'),
versionUnknown: _('版本信息未获取', 'Version info unavailable', '版本資訊未取得', 'バージョン情報未取得', '버전 정보 없음'),
versionAhead: _('当前版本高于推荐稳定版 {version},可能不稳定', 'Current version is ahead of recommended stable {version}, may be unstable', '目前版本高於推薦穩定版 {version},可能不穩定', '現在のバージョンは推奨安定版 {version} より新しく、不安定な可能性があります', '현재 버전이 권장 안정 버전 {version}보다 높아 불안정할 수 있습니다'),
versionStable: _('稳定版 {version}', 'Stable {version}', '穩定版 {version}', '安定版 {version}', '안정 버전 {version}'),

View File

@@ -83,7 +83,7 @@ async function loadData(page) {
checkHotUpdate(cards, panelVersion)
const isInstalled = !!version.current
const sourceLabel = version.source === 'official' ? t('about.official') : t('about.chinese')
const sourceLabel = version.source === 'official' ? t('about.official') : version.source === 'chinese' ? t('about.chinese') : t('about.unknownSource')
const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)'
const hasRecommended = !!version.recommended
const aheadOfRecommended = isInstalled && hasRecommended && !!version.ahead_of_recommended
@@ -250,18 +250,18 @@ async function showVersionPicker(page, currentVersion) {
if (!targetVer || targetVer === '') { hintEl.textContent = ''; confirmBtn.disabled = true; return }
const targetTag = select.selectedIndex === 0 ? t('about.tagRecommended') : t('about.tagNeedTest')
const sameSource = targetSource === (currentVersion.source === 'official' ? 'official' : 'chinese')
const sameSource = targetSource === currentVersion.source
if (!isInstalled) {
confirmBtn.textContent = t('about.btnInstall')
hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : t('about.chinese'), ver: targetVer, tag: targetTag })
hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource'), ver: targetVer, tag: targetTag })
confirmBtn.disabled = false
return
}
if (!sameSource) {
confirmBtn.textContent = t('about.btnSwitch')
hintEl.innerHTML = `${t('about.hintCurrent')}: <strong>${currentVersion.source === 'official' ? t('about.official') : t('about.chinese')} ${currentVersion.current}</strong> → <strong>${targetSource === 'official' ? t('about.official') : t('about.chinese')} ${targetVer}</strong>${targetTag}`
hintEl.innerHTML = `${t('about.hintCurrent')}: <strong>${currentVersion.source === 'official' ? t('about.official') : currentVersion.source === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${currentVersion.current}</strong> → <strong>${targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${targetVer}</strong>${targetTag}`
confirmBtn.disabled = false
return
}
@@ -310,7 +310,7 @@ async function showVersionPicker(page, currentVersion) {
const versions = showNightly ? allVersions : (stable.length > 0 ? stable : allVersions)
const nightlyCount = allVersions.length - stable.length
select.innerHTML = versions.map((v, idx) => {
const isCurrent = isInstalled && v === currentVersion.current && source === (currentVersion.source === 'official' ? 'official' : 'chinese')
const isCurrent = isInstalled && v === currentVersion.current && source === currentVersion.source
return `<option value="${v}">${v}${idx === 0 ? ` (${t('about.recommended')})` : ''}${isCurrent ? ` (${t('about.current')})` : ''}</option>`
}).join('')
// nightly 切换提示

View File

@@ -186,7 +186,7 @@ function renderStatCards(page, services, version, agents, config) {
</div>
<div class="stat-card">
<div class="stat-card-header">
<span class="stat-card-label">${t('dashboard.versionLabel')} · ${version.source === 'official' ? t('dashboard.versionOfficial') : t('dashboard.versionChinese')}</span>
<span class="stat-card-label">${t('dashboard.versionLabel')} · ${version.source === 'official' ? t('dashboard.versionOfficial') : version.source === 'chinese' ? t('dashboard.versionChinese') : t('dashboard.versionUnknownSource')}</span>
</div>
<div class="stat-card-value">${version.current || t('common.unknown')}</div>
<div class="stat-card-meta">${versionMeta}</div>