Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dev-notes/
/.npm-cache/
/Library/
/.tmp-qclaw-user-data/
.qclaw-dev.pid
/.codex-skill-build/
/AGENTS.md
.agents/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('openclaw-download-fallbacks', () => {
const commands = buildOpenClawManualInstallCommands()

expect(commands).toHaveLength(OPENCLAW_NPM_REGISTRY_MIRRORS.length)
expect(commands[0]).toContain('openclaw@2026.3.24')
expect(commands[0]).toContain('openclaw@2026.3.28')
expect(commands.every((command) => !command.includes('openclaw@latest'))).toBe(true)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ describe('openclaw upgrade compatibility', () => {
expect(detectOpenClawVersionBand('2026.3.24')).toBe('openclaw_2026_3_23_to_2026_3_24')
})

it('marks unaudited future versions as conservative mode', () => {
it('treats newer versions as same-generation upgrade when ALLOW_LATEST is enabled', () => {
const assessment = assessOpenClawUpgradeCompatibility({
currentVersion: '2026.4.1',
previousVersion: '2026.3.22',
assessedAt: '2026-03-23T10:00:00.000Z',
})

expect(assessment.status).toBe('unknown_future_version')
expect(assessment.conservativeMode).toBe(true)
expect(assessment.warningCodes).toEqual(['version_unknown_future'])
// ALLOW_LATEST_OPENCLAW_VERSION=true:新版本不再进入保守模式
expect(assessment.status).toBe('upgrade_detected')
expect(assessment.conservativeMode).toBe(false)
expect(assessment.currentBand).toBe('openclaw_2026_3_25_to_2026_3_28')
})

it('captures upgrade transitions for later reconcile flows', () => {
Expand Down
31 changes: 26 additions & 5 deletions electron/main/__tests__/openclaw-upgrade-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { OPENCLAW_NPM_REGISTRY_MIRRORS } from '../openclaw-download-fallbacks'
import { setDynamicTargetVersion } from '../../../src/shared/openclaw-version-policy'

const TEST_HOME = process.env.HOME || '/Users/test'
const fs = process.getBuiltinModule('node:fs/promises') as typeof import('node:fs/promises')
Expand Down Expand Up @@ -212,6 +213,8 @@ describe('openclaw upgrade service', () => {
failures: [],
},
})
// 重置动态目标版本
setDynamicTargetVersion(null)
})

it('marks custom out-of-range installs as manual_block against the pinned target', async () => {
Expand Down Expand Up @@ -241,13 +244,13 @@ describe('openclaw upgrade service', () => {
const result = await checkOpenClawUpgrade()

expect(result.currentVersion).toBe('1.0.0')
expect(result.targetVersion).toBe('2026.3.24')
expect(result.targetVersion).toBe('2026.3.28')
expect(result.policyState).toBe('below_min')
expect(result.enforcement).toBe('manual_block')
expect(result.targetAction).toBe('upgrade')
expect(result.canAutoUpgrade).toBe(false)
expect(result.errorCode).toBe('manual_only')
expect(result.manualHint).toContain('2026.3.24')
expect(result.manualHint).toContain('最新版本')
})

it('allows supported-range installs to continue without depending on latest lookup', async () => {
Expand Down Expand Up @@ -280,7 +283,7 @@ describe('openclaw upgrade service', () => {
expect(result.policyState).toBe('supported_not_target')
expect(result.enforcement).toBe('optional_upgrade')
expect(result.targetAction).toBe('upgrade')
expect(result.targetVersion).toBe('2026.3.24')
expect(result.targetVersion).toBe('2026.3.28')
expect(result.errorCode).toBeUndefined()
})

Expand Down Expand Up @@ -317,7 +320,7 @@ describe('openclaw upgrade service', () => {
expect(result.blocksContinue).toBe(false)
expect(result.canAutoUpgrade).toBe(false)
expect(result.errorCode).toBeUndefined()
expect(result.manualHint).toContain('2026.3.24')
expect(result.manualHint).toContain('最新版本')
})

it('manual-blocks installs whose version string cannot be parsed safely', async () => {
Expand Down Expand Up @@ -795,7 +798,7 @@ describe('openclaw upgrade service', () => {
})
checkOpenClawLatestVersionMock.mockResolvedValue({
ok: true,
latestVersion: '2026.3.22',
latestVersion: '2026.3.24',
checkedAt: '2026-03-24T10:00:00.000Z',
source: 'npm-registry',
})
Expand Down Expand Up @@ -975,6 +978,12 @@ describe('openclaw upgrade service', () => {
)

gatewayHealthMock.mockResolvedValue({ running: false, raw: '{}' })
checkOpenClawLatestVersionMock.mockResolvedValue({
ok: true,
latestVersion: '2026.3.24',
checkedAt: '2026-03-18T10:00:00.000Z',
source: 'npm-registry',
})
discoverOpenClawInstallationsMock.mockResolvedValue({
candidates: [
{
Expand Down Expand Up @@ -1070,6 +1079,12 @@ describe('openclaw upgrade service', () => {
await fs.writeFile(path.join(backupArchive, 'credentials', 'token.json'), '{"token":"restored"}', 'utf8')

gatewayHealthMock.mockResolvedValue({ running: false, raw: '{}' })
checkOpenClawLatestVersionMock.mockResolvedValue({
ok: true,
latestVersion: '2026.3.24',
checkedAt: '2026-03-18T10:00:00.000Z',
source: 'npm-registry',
})
discoverOpenClawInstallationsMock.mockResolvedValue({
candidates: [
{
Expand Down Expand Up @@ -1170,6 +1185,12 @@ describe('openclaw upgrade service', () => {
)

gatewayHealthMock.mockResolvedValue({ running: false, raw: '{}' })
checkOpenClawLatestVersionMock.mockResolvedValue({
ok: true,
latestVersion: '2026.3.24',
checkedAt: '2026-03-18T10:00:00.000Z',
source: 'npm-registry',
})
discoverOpenClawInstallationsMock.mockResolvedValue({
candidates: [
{
Expand Down
9 changes: 8 additions & 1 deletion electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { tryNormalizeProcessCwd } from './runtime-working-directory'
import { revealWindow, showOrCreateWindow } from './window-lifecycle'
import { reloadGatewayForConfigChange } from './gateway-lifecycle-controller'
import { sanitizeNodeOptionsForElectron } from './node-options'
import { initVersionAdapter, cleanupVersionAdapter } from './openclaw-version-adapter-integration'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const OPEN_CONTACT_MODAL_CHANNEL = 'app:open-contact-modal'
Expand Down Expand Up @@ -264,7 +265,10 @@ function createTray() {
})
}

app.whenReady().then(() => {
app.whenReady().then(async () => {
// 初始化版本适配器
await initVersionAdapter()

registerIpcHandlers()
createWindow()
createTray()
Expand All @@ -285,6 +289,9 @@ app.on('before-quit', (event) => {
.then(() => undefined)
.catch(() => undefined)
.finally(() => {
// 清理版本适配器
cleanupVersionAdapter()

appExitCleanupDone = true
appExitCleanupPromise = null
app.quit()
Expand Down
72 changes: 70 additions & 2 deletions electron/main/openclaw-upgrade-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
UpgradeCompatibilityAssessment,
UpgradeCompatibilityStatus,
} from '../../src/shared/gateway-runtime-reconcile-state'
import { getVersionAdapter } from '../../src/shared/openclaw-version-policy'
import { ALLOW_LATEST_OPENCLAW_VERSION } from '../../src/shared/openclaw-version-policy'

interface ParsedOpenClawVersion {
major: number
Expand Down Expand Up @@ -52,12 +54,46 @@ export function detectOpenClawVersionBand(version: string | null | undefined): O
const normalized = normalizeOpenClawVersion(version)
if (!normalized) return 'unknown'

// 如果设置了版本适配器,使用适配器的状态判断
const adapter = getVersionAdapter()
if (adapter && adapter.getVersion() === normalized) {
const adapterStatus = adapter.getStatus()

// 如果适配器标记为supported或degraded,认为是支持的版本
if (adapterStatus === 'supported' || adapterStatus === 'degraded') {
// 使用精确的版本范围检查
if (compareOpenClawVersions(normalized, '2026.3.25') >= 0 && compareOpenClawVersions(normalized, '2026.3.28') <= 0) {
return 'openclaw_2026_3_25_to_2026_3_28'
}
if (compareOpenClawVersions(normalized, '2026.3.23') >= 0 && compareOpenClawVersions(normalized, '2026.3.24') <= 0) {
return 'openclaw_2026_3_23_to_2026_3_24'
}
if (compareOpenClawVersions(normalized, '2026.3.22') === 0) {
return 'openclaw_2026_3_22'
}
// 对于未知的支持版本,返回unknown_future
return 'unknown_future'
}

// 如果是experimental,返回unknown_future但不触发保守模式
if (adapterStatus === 'experimental') {
return 'unknown_future'
}
}

// 原有逻辑(保持向后兼容)
if (compareOpenClawVersions(normalized, '2026.3.7') < 0) return 'pre_2026_3_7'
if (compareOpenClawVersions(normalized, '2026.3.11') <= 0) return 'openclaw_2026_3_7_to_2026_3_11'
if (compareOpenClawVersions(normalized, '2026.3.13') <= 0) return 'openclaw_2026_3_12_to_2026_3_13'
if (compareOpenClawVersions(normalized, '2026.3.21') <= 0) return 'openclaw_2026_3_14_to_2026_3_21'
if (compareOpenClawVersions(normalized, '2026.3.22') === 0) return 'openclaw_2026_3_22'
if (compareOpenClawVersions(normalized, '2026.3.24') <= 0) return 'openclaw_2026_3_23_to_2026_3_24'
if (compareOpenClawVersions(normalized, '2026.3.28') <= 0) return 'openclaw_2026_3_25_to_2026_3_28'

// 新版本:当 ALLOW_LATEST 启用时,将新版本视为同代而非 unknown_future
if (ALLOW_LATEST_OPENCLAW_VERSION) {
return 'openclaw_2026_3_25_to_2026_3_28'
}
return 'unknown_future'
}

Expand All @@ -67,7 +103,26 @@ function resolveCompatibilityStatus(
currentBand: OpenClawVersionBand
): UpgradeCompatibilityStatus {
if (!currentVersion) return 'unknown_current_version'
if (currentBand === 'unknown_future') return 'unknown_future_version'

// 如果设置了版本适配器,使用适配器的状态判断
const adapter = getVersionAdapter()
if (adapter && adapter.getVersion() === currentVersion) {
const adapterStatus = adapter.getStatus()

// 如果适配器标记为supported或degraded,不触发unknown_future_version
if (adapterStatus === 'supported' || adapterStatus === 'degraded' || adapterStatus === 'experimental') {
if (!previousVersion) return 'first_observed'

const comparison = compareOpenClawVersions(currentVersion, previousVersion)
if (comparison > 0) return 'upgrade_detected'
if (comparison < 0) return 'downgrade_detected'
return 'steady_state'
}
}

// 原有逻辑(保持向后兼容)
// 当 ALLOW_LATEST 启用时,unknown_future 不再触发保守模式
if (currentBand === 'unknown_future' && !ALLOW_LATEST_OPENCLAW_VERSION) return 'unknown_future_version'
if (!previousVersion) return 'first_observed'

const comparison = compareOpenClawVersions(currentVersion, previousVersion)
Expand Down Expand Up @@ -191,13 +246,26 @@ export function assessOpenClawUpgradeCompatibility(params: {
const warningCodes = buildWarningCodes(currentVersion, previousVersion, status, currentBand)
const assessedAt = String(params.assessedAt || new Date().toISOString())

// 判断是否进入保守模式
// 新策略:只有在适配器明确标记为unsupported时才进入保守模式
let conservativeMode = status === 'unknown_current_version' || status === 'unknown_future_version'

const adapter = getVersionAdapter()
if (adapter && currentVersion && adapter.getVersion() === currentVersion) {
const adapterStatus = adapter.getStatus()
// 如果适配器不是unsupported,不进入保守模式
if (adapterStatus !== 'unsupported') {
conservativeMode = false
}
}

return {
status,
currentVersion,
currentBand,
previousVersion,
previousBand,
conservativeMode: status === 'unknown_current_version' || status === 'unknown_future_version',
conservativeMode,
warningCodes,
summary: buildSummary(status, currentVersion, previousVersion, currentBand, warningCodes),
assessedAt,
Expand Down
33 changes: 26 additions & 7 deletions electron/main/openclaw-upgrade-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
isStrictOpenClawPolicyVersion,
resolveOpenClawVersionEnforcement,
supportsPinnedOpenClawCorrection,
setDynamicTargetVersion,
getEffectiveTargetVersion,
} from '../../src/shared/openclaw-version-policy'
import { MAIN_RUNTIME_POLICY } from './runtime-policy'
import { checkOpenClaw, gatewayHealth, gatewayStart, readConfig, runCli, runDirect, runDoctor, runShell, writeConfig } from './cli'
Expand Down Expand Up @@ -67,17 +69,18 @@ function resolveActiveCandidate(candidates: OpenClawInstallCandidate[]): OpenCla

function buildManualUpgradeHint(candidate: OpenClawInstallCandidate | null): string | undefined {
if (!candidate) return undefined
const targetDesc = '最新版本'
if (!isStrictOpenClawPolicyVersion(candidate.version)) {
return '当前 OpenClaw 版本号无法可靠解析。为避免误改现有安装,请先确认 `openclaw --version` 输出正常,并手动切换到 2026.3.24 后再回到 Qclaw。'
return `当前 OpenClaw 版本号无法可靠解析。为避免误改现有安装,请先确认 \`openclaw --version\` 输出正常,并手动切换到${targetDesc}后再回到 Qclaw。`
}
if (candidate.installSource === 'homebrew' && !supportsPinnedOpenClawCorrection(candidate.installSource, candidate)) {
return '当前 OpenClaw 由 Homebrew 管理,程序内无法安全回退到 2026.3.24。请先在 Homebrew 环境中手动切换到 2026.3.24;若当前 Homebrew 源无法提供该版本,请先移除 brew 安装后,再让 Qclaw 重新安装 2026.3.24。'
return `当前 OpenClaw 由 Homebrew 管理,程序内无法安全升级。请先在 Homebrew 环境中手动升级到${targetDesc};若当前 Homebrew 源无法提供该版本,请先移除 brew 安装后,再让 Qclaw 重新安装${targetDesc}。`
}
if (candidate.installSource === 'custom') {
return '当前 OpenClaw 来自自定义路径,程序内无法安全改写。请在原安装位置或原包管理器中手动安装 2026.3.24,并确认 PATH 指向该版本后再回到 Qclaw。'
return `当前 OpenClaw 来自自定义路径,程序内无法安全改写。请在原安装位置或原包管理器中手动安装${targetDesc},并确认 PATH 指向该版本后再回到 Qclaw。`
}
if (candidate.installSource === 'unknown') {
return '当前 OpenClaw 安装来源无法识别。为避免误改系统环境,请先确认 which openclaw 对应的实际路径,并将 PATH 切换到 2026.3.24,或移除该版本后让 Qclaw 重新安装。'
return `当前 OpenClaw 安装来源无法识别。为避免误改系统环境,请先确认 which openclaw 对应的实际路径,并将 PATH 切换到${targetDesc},或移除该版本后让 Qclaw 重新安装。`
}
return undefined
}
Expand Down Expand Up @@ -687,6 +690,18 @@ async function runSourceAwareUpgrade(
}

export async function checkOpenClawUpgrade(): Promise<OpenClawUpgradeCheckResult> {
// 先查询 npm latest 版本,更新动态目标
try {
const { checkOpenClawLatestVersion } = await import('./openclaw-latest-version-service')
const latestResult = await checkOpenClawLatestVersion()
if (latestResult.ok && latestResult.latestVersion) {
setDynamicTargetVersion(latestResult.latestVersion)
}
} catch {
// 查询失败不阻断,回退到 PINNED_OPENCLAW_VERSION
}

const effectiveTarget = getEffectiveTargetVersion()
const discovery = await discoverOpenClawInstallations()
const activeCandidate = resolveActiveCandidate(discovery.candidates)
const health = activeCandidate ? await gatewayHealth().catch(() => ({ running: false, raw: '' })) : { running: false, raw: '' }
Expand All @@ -697,7 +712,7 @@ export async function checkOpenClawUpgrade(): Promise<OpenClawUpgradeCheckResult
ok: false,
activeCandidate: null,
currentVersion: null,
targetVersion: PINNED_OPENCLAW_VERSION,
targetVersion: effectiveTarget,
latestCheck: null,
policyState: null,
enforcement: null,
Expand Down Expand Up @@ -725,19 +740,23 @@ export async function checkOpenClawUpgrade(): Promise<OpenClawUpgradeCheckResult
warnings.push(manualHint)
}

// 判断是否已是最新:当前版本 >= 有效目标版本
const isUpToDate = policy.policyState === 'supported_target' ||
(activeCandidate.version && compareLooseVersions(activeCandidate.version, effectiveTarget) >= 0)

return {
ok: !policy.blocksContinue,
activeCandidate,
currentVersion: activeCandidate.version || null,
targetVersion: policy.targetVersion,
targetVersion: policy.targetVersion || effectiveTarget,
latestCheck: null,
policyState: policy.policyState,
enforcement: policy.enforcement,
targetAction: policy.targetAction,
blocksContinue: policy.blocksContinue,
canSelfHeal: policy.canSelfHeal,
canAutoUpgrade,
upToDate: policy.policyState === 'supported_target',
upToDate: Boolean(isUpToDate),
gatewayRunning: Boolean(health.running),
warnings,
manualHint,
Expand Down
Loading
Loading