fix: app update check in all modes, add Check for Updates menu, clean upgrade output

- App self-update check no longer skipped in remote mode
- Added "Check for Updates" button in sidebar
- Strip ANSI escape codes from upgrade output
- Collapse upgrade log behind "Show details" on success
- Updated Discord link and added icons to landing page buttons
- Recorded deployment docs in agents.md

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
zhixian
2026-02-19 19:59:35 +09:00
parent e26b53a910
commit 8747d5bf21
5 changed files with 96 additions and 17 deletions

View File

@@ -59,7 +59,28 @@
- `docs/plans/2026-02-15-clawpal-mvp-design.md`(设计)
- `docs/plans/2026-02-15-clawpal-mvp-implementation-plan.md`(计划)
## 7. 安全与风险
## 7. 部署
### 官网clawpal.zhixian.io
使用 Cloudflare Pages Direct Upload 部署,源目录为 `docs/site/`
部署命令:
```bash
npx wrangler pages deploy docs/site --project-name clawpal
```
项目域名:`clawpal.zhixian.io`(也可通过 `clawpal.pages.dev` 访问)。
### 桌面应用 Release
通过 GitHub Actions 自动构建push tag 触发(如 `v0.1.1`
- CI workflow: `.github/workflows/release.yml`
- 构建产物macOS (ARM/x64 .dmg)、Windows (.exe/.msi)、Linux (.deb/.AppImage)
- 需要 `TAURI_SIGNING_PRIVATE_KEY` 等 secrets本地无法打 release bundle
- 发布新版本流程:更新 `package.json` + `src-tauri/Cargo.toml` 版本号 → commit → `git tag vX.Y.Z` → push
## 8. 安全与风险
- 禁止提交明文密钥/配置路径泄露
- 避免大文件和自动生成产物直接提交

View File

@@ -423,6 +423,12 @@
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</symbol>
<symbol id="icon-github" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.295 24 12 24 5.37 18.63 0 12 0"/>
</symbol>
<symbol id="icon-discord" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</symbol>
</svg>
<!-- Nav -->
@@ -449,14 +455,16 @@
<p class="reveal reveal-delay-2">Manage AI agents, models, and configs with a visual interface.<br>Stop editing JSON by hand.</p>
<div class="hero-actions reveal reveal-delay-3">
<a href="#download" class="btn" style="padding:14px 32px;font-size:1rem;border-radius:12px;">
<svg width="18" height="18"><use href="#icon-download"/></svg>
Download
<svg width="16" height="16"><use href="#icon-download"/></svg>
</a>
<a href="https://github.com/zhixianio/clawpal" class="btn btn-outline" style="padding:14px 32px;font-size:1rem;border-radius:12px;" target="_blank">
View on GitHub
<svg width="18" height="18"><use href="#icon-github"/></svg>
GitHub
</a>
<a href="https://discord.gg/4yqf28wf7R" class="btn btn-outline" style="padding:14px 32px;font-size:1rem;border-radius:12px;" target="_blank">
Join Discord
<a href="https://discord.gg/T3nPEG9TZN" class="btn btn-outline" style="padding:14px 32px;font-size:1rem;border-radius:12px;" target="_blank">
<svg width="18" height="18"><use href="#icon-discord"/></svg>
Discord
</a>
</div>
<div class="hero-sub reveal reveal-delay-4">

View File

@@ -1,4 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { Home } from "./pages/Home";
import { Recipes } from "./pages/Recipes";
import { Cook } from "./pages/Cook";
@@ -80,6 +82,30 @@ export function App() {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
// App self-update (manual check from sidebar)
const [checkingAppUpdate, setCheckingAppUpdate] = useState(false);
const handleCheckForUpdates = useCallback(async () => {
setCheckingAppUpdate(true);
try {
const update = await check();
if (update) {
const confirmed = window.confirm(`ClawPal v${update.version} is available. Update and restart now?`);
if (confirmed) {
await update.downloadAndInstall();
await relaunch();
}
} else {
showToast("You're on the latest version");
}
} catch (e) {
console.error("Update check failed:", e);
showToast(`Update check failed: ${e}`, "error");
} finally {
setCheckingAppUpdate(false);
}
}, [showToast]);
const handleInstanceSelect = useCallback((id: string) => {
setActiveInstance(id);
if (id !== "local") {
@@ -285,6 +311,14 @@ export function App() {
>
Settings
</Button>
<Button
variant="ghost"
className="justify-start hover:bg-accent text-muted-foreground"
onClick={handleCheckForUpdates}
disabled={checkingAppUpdate}
>
{checkingAppUpdate ? "Checking..." : "Check for Updates"}
</Button>
</nav>
{/* Dirty config action bar */}

View File

@@ -11,6 +11,11 @@ import {
type Step = "confirm" | "backup" | "upgrading" | "done";
/** Strip ANSI escape codes from terminal output */
function stripAnsi(str: string): string {
return str.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
}
export function UpgradeDialog({
open,
onOpenChange,
@@ -31,6 +36,7 @@ export function UpgradeDialog({
const [output, setOutput] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [showLog, setShowLog] = useState(false);
const reset = () => {
setStep("confirm");
@@ -38,6 +44,7 @@ export function UpgradeDialog({
setOutput("");
setError("");
setLoading(false);
setShowLog(false);
};
const handleClose = (open: boolean) => {
@@ -77,11 +84,12 @@ export function UpgradeDialog({
const result = isRemote
? await api.remoteRunOpenclawUpgrade(instanceId)
: await api.runOpenclawUpgrade();
setOutput(result);
setOutput(stripAnsi(result));
setStep("done");
} catch (e) {
setOutput(String(e));
setOutput(stripAnsi(String(e)));
setError("Upgrade failed. See output below.");
setShowLog(true);
} finally {
setLoading(false);
}
@@ -163,9 +171,19 @@ export function UpgradeDialog({
Upgrade completed successfully.
</p>
{output && (
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap">
{output}
</pre>
<>
<button
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setShowLog(!showLog)}
>
{showLog ? "Hide details" : "Show details"}
</button>
{showLog && (
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap">
{output}
</pre>
)}
</>
)}
</div>
)}

View File

@@ -201,9 +201,8 @@ export function Home({
return () => clearTimeout(timer);
}, [isRemote, isConnected, instanceId]);
// ClawPal app self-update check (local only)
// ClawPal app self-update check
useEffect(() => {
if (isRemote) return;
setAppUpdateChecking(true);
check()
.then((update) => {
@@ -213,10 +212,9 @@ export function Home({
})
.catch((e) => console.error("Failed to check app update:", e))
.finally(() => setAppUpdateChecking(false));
}, [isRemote]);
}, []);
const handleAppUpdate = useCallback(async () => {
if (isRemote) return;
setAppUpdating(true);
setAppUpdateProgress(0);
try {
@@ -242,7 +240,7 @@ export function Home({
setAppUpdating(false);
setAppUpdateProgress(null);
}
}, [isRemote]);
}, []);
const handleDeleteAgent = (agentId: string) => {
if (isRemote && !isConnected) return;
@@ -303,8 +301,8 @@ export function Home({
)}
</div>
{/* ClawPal app self-update (local only) */}
{!isRemote && (appUpdateChecking || appUpdate) && (
{/* ClawPal app self-update */}
{(appUpdateChecking || appUpdate) && (
<>
<span className="text-sm text-muted-foreground">App Update</span>
<div className="flex items-center gap-2 flex-wrap">