fix(gateway): avoid false foreign gateway detection (#176)

This commit is contained in:
晴天
2026-04-02 18:15:52 +08:00
parent dec0bf4d82
commit c6b6707d6c
9 changed files with 91 additions and 53 deletions

View File

@@ -5,6 +5,12 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [0.11.2] - 2026-04-02
### 修复 (Fixes)
- **Gateway 归属误判** — 放宽 Gateway owner 判定,改为按端口 / CLI 路径 / OpenClaw 目录签名识别当前绑定实例,不再因 PID 漂移把当前面板已启动且可正常聊天的 Gateway 误判为“外部实例”;检测到 PID 变化时会自动回写 `gateway-owner.json`修复服务页误报、Dashboard 外部实例提示以及顶部“Gateway 未运行”误报 (fixes #176)
## [0.11.1] - 2026-04-02
### 改进 (Improvements)

View File

@@ -34,7 +34,7 @@
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。内置晴辰助手支持工具调用,晴辰云 AI 接口一键接入。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。支持 11 种语言。",
"url": "https://claw.qt.cool/",
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
"softwareVersion": "0.11.1",
"softwareVersion": "0.11.2",
"author": {
"@type": "Organization",
"name": "晴辰云 QingchenCloud",
@@ -1155,7 +1155,7 @@
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
<div class="container-sm" style="position:relative;z-index:10">
<div class="section-header">
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.11.1 最新版</span></div>
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.11.2 最新版</span></div>
<h2 class="reveal section-title" data-i18n="dl.title"><span class="gradient-text">下载安装</span></h2>
<p class="reveal section-desc" data-i18n="dl.desc">选择你的操作系统,一键下载安装</p>
</div>
@@ -1165,11 +1165,11 @@
<h3>macOS</h3>
<p class="dl-desc" data-i18n="dl.mac.d">支持 Apple Silicon 和 Intel 芯片</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_aarch64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_aarch64.dmg" target="_blank" rel="noopener">
Apple Silicon (M1/M2/M3/M4)
<span class="dl-format">.dmg</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_x64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64.dmg" target="_blank" rel="noopener">
<span data-i18n="dl.mac.intel">Intel 芯片</span>
<span class="dl-format">.dmg</span>
</a>
@@ -1187,15 +1187,15 @@
<h3>Windows</h3>
<p class="dl-desc" data-i18n="dl.win.d">支持 Windows 10 及以上版本</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_x64-setup.exe" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64-setup.exe" target="_blank" rel="noopener">
<span data-i18n="dl.win.exe">安装程序</span>
<span class="dl-format">.exe</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_x64-setup-full.exe" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64-setup-full.exe" target="_blank" rel="noopener">
<span data-i18n="dl.win.full">完整包(含 WebView2</span>
<span class="dl-format">.exe</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_x64_en-US.msi" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64_en-US.msi" target="_blank" rel="noopener">
<span data-i18n="dl.win.msi">MSI 安装包</span>
<span class="dl-format">.msi</span>
</a>
@@ -1206,11 +1206,11 @@
<h3>Linux</h3>
<p class="dl-desc" data-i18n="dl.linux.d">支持主流 Linux 发行版</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_amd64.AppImage" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_amd64.AppImage" target="_blank" rel="noopener">
<span data-i18n="dl.linux.ai">通用版</span>
<span class="dl-format">.AppImage</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.1_amd64.deb" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_amd64.deb" target="_blank" rel="noopener">
Debian / Ubuntu
<span class="dl-format">.deb</span>
</a>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "clawpanel",
"version": "0.11.1",
"version": "0.11.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
"version": "0.11.1",
"version": "0.11.2",
"license": "AGPL-3.0",
"dependencies": {
"@tauri-apps/api": "^2.5.0",

View File

@@ -1,6 +1,6 @@
{
"name": "clawpanel",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",

View File

@@ -2125,7 +2125,7 @@ function currentGatewayOwnerSignature() {
}
}
function isCurrentGatewayOwner(owner, pid = null) {
function matchesCurrentGatewayOwnerSignature(owner) {
if (!owner || owner.startedBy !== 'clawpanel') return false
const current = currentGatewayOwnerSignature()
if (Number(owner.port || 0) !== current.port) return false
@@ -2133,10 +2133,19 @@ function isCurrentGatewayOwner(owner, pid = null) {
const ownerCliPath = canonicalCliPath(owner.cliPath)
if (!ownerCliPath || ownerCliPath !== current.cliPath) return false
if (!owner.openclawDir || path.resolve(owner.openclawDir) !== current.openclawDir) return false
if (pid != null && owner.pid != null && Number(owner.pid) !== Number(pid)) return false
return true
}
function gatewayOwnerPidNeedsRefresh(owner, pid = null) {
if (!matchesCurrentGatewayOwnerSignature(owner)) return false
if (!Number.isInteger(pid) || pid <= 0) return false
return !Number.isInteger(owner?.pid) || Number(owner.pid) !== Number(pid)
}
function isCurrentGatewayOwner(owner, pid = null) {
return matchesCurrentGatewayOwnerSignature(owner)
}
function writeGatewayOwner(pid = null) {
const ownerPath = gatewayOwnerFilePath()
const ownerDir = path.dirname(ownerPath)
@@ -2164,7 +2173,11 @@ function foreignGatewayError(pid = null) {
}
function ensureOwnedGatewayOrThrow(pid = null) {
if (isCurrentGatewayOwner(readGatewayOwner(), pid)) return true
const owner = readGatewayOwner()
if (isCurrentGatewayOwner(owner, pid)) {
if (gatewayOwnerPidNeedsRefresh(owner, pid)) writeGatewayOwner(pid)
return true
}
throw foreignGatewayError(pid)
}
@@ -2753,7 +2766,11 @@ const handlers = {
}
const cliInstalled = !!resolveOpenclawCliPath()
const ownedByCurrentInstance = !!running && isCurrentGatewayOwner(readGatewayOwner(), pid || null)
const owner = readGatewayOwner()
const ownedByCurrentInstance = !!running && isCurrentGatewayOwner(owner, pid || null)
if (ownedByCurrentInstance && gatewayOwnerPidNeedsRefresh(owner, pid || null)) {
writeGatewayOwner(pid || null)
}
const ownership = !running ? 'stopped' : ownedByCurrentInstance ? 'owned' : 'foreign'
return [{ label, running, pid, description: 'OpenClaw Gateway', cli_installed: cliInstalled, ownership, owned_by_current_instance: ownedByCurrentInstance }]

2
src-tauri/Cargo.lock generated
View File

@@ -351,7 +351,7 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.11.1"
version = "0.11.2"
dependencies = [
"base64 0.22.1",
"chrono",

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.11.1"
version = "0.11.2"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]

View File

@@ -91,6 +91,29 @@ fn current_gateway_owner_signature() -> (u16, String, Option<String>) {
)
}
fn matches_current_gateway_owner_signature(owner: &GatewayOwnerRecord) -> bool {
if owner.started_by != "clawpanel" {
return false;
}
let (port, openclaw_dir, cli_path) = current_gateway_owner_signature();
if owner.port != port {
return false;
}
if normalize_owned_path(&owner.openclaw_dir) != openclaw_dir {
return false;
}
let owner_cli_path = owner.cli_path.as_ref().map(normalize_owned_path);
matches!(
(owner_cli_path.as_deref(), cli_path.as_deref()),
(Some(owner_cli), Some(current_cli)) if owner_cli == current_cli
)
}
fn gateway_owner_pid_needs_refresh(owner: &GatewayOwnerRecord, pid: Option<u32>) -> bool {
matches_current_gateway_owner_signature(owner)
&& matches!(pid, Some(current_pid) if owner.pid != Some(current_pid))
}
fn read_gateway_owner() -> Option<GatewayOwnerRecord> {
let content = std::fs::read_to_string(gateway_owner_path()).ok()?;
serde_json::from_str(&content).ok()
@@ -119,35 +142,8 @@ fn clear_gateway_owner() {
let _ = std::fs::remove_file(gateway_owner_path());
}
fn is_current_gateway_owner(owner: &GatewayOwnerRecord, pid: Option<u32>) -> bool {
if owner.started_by != "clawpanel" {
return false;
}
let (port, openclaw_dir, cli_path) = current_gateway_owner_signature();
if owner.port != port {
return false;
}
if normalize_owned_path(&owner.openclaw_dir) != openclaw_dir {
return false;
}
let owner_cli_path = owner.cli_path.as_ref().map(normalize_owned_path);
match (owner_cli_path.as_deref(), cli_path.as_deref()) {
(Some(owner_cli), Some(current_cli)) if owner_cli == current_cli => {}
_ => return false,
}
if let (Some(owner_pid), Some(current_pid)) = (owner.pid, pid) {
if owner_pid != current_pid {
return false;
}
}
true
}
fn is_gateway_owned_by_current_instance(pid: Option<u32>) -> bool {
read_gateway_owner()
.as_ref()
.map(|owner| is_current_gateway_owner(owner, pid))
.unwrap_or(false)
fn is_current_gateway_owner(owner: &GatewayOwnerRecord, _pid: Option<u32>) -> bool {
matches_current_gateway_owner_signature(owner)
}
fn foreign_gateway_error(pid: Option<u32>) -> String {
@@ -162,11 +158,15 @@ fn foreign_gateway_error(pid: Option<u32>) -> String {
}
fn ensure_owned_gateway_or_err(pid: Option<u32>) -> Result<(), String> {
if is_gateway_owned_by_current_instance(pid) {
Ok(())
} else {
Err(foreign_gateway_error(pid))
if let Some(owner) = read_gateway_owner() {
if is_current_gateway_owner(&owner, pid) {
if gateway_owner_pid_needs_refresh(&owner, pid) {
write_gateway_owner(pid)?;
}
return Ok(());
}
}
Err(foreign_gateway_error(pid))
}
async fn current_gateway_runtime(label: &str) -> (bool, Option<u32>) {
@@ -845,6 +845,9 @@ mod platform {
kill_process_tree(pid);
}
}
} else {
// 读不到命令行时,不做假设,避免误杀其他进程
continue;
}
}
}
@@ -1617,7 +1620,19 @@ pub async fn get_services_status() -> Result<Vec<ServiceStatus>, String> {
let mut results = Vec::new();
for label in labels.iter().map(String::as_str) {
let (running, pid) = current_gateway_runtime(label).await;
let owned_by_current_instance = running && is_gateway_owned_by_current_instance(pid);
let owner = read_gateway_owner();
let owned_by_current_instance = running
&& owner
.as_ref()
.map(|record| is_current_gateway_owner(record, pid))
.unwrap_or(false);
if owned_by_current_instance {
if let Some(record) = owner.as_ref() {
if gateway_owner_pid_needs_refresh(record, pid) {
let _ = write_gateway_owner(pid);
}
}
}
let ownership = if !running {
Some("stopped".to_string())
} else if owned_by_current_instance {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
"version": "0.11.1",
"version": "0.11.2",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",