feat(update): 页面一键更新 + 守护启动脚本
系统配置页新增「版本与更新」卡片,顶栏版本号旁在有新版时亮出提示胶囊。 更新链路:下载发布包 → 比对 SHA256SUMS → -h 冒烟测试 → 暂存 artex.new → 以退出码 75 退出 → 守护脚本重新拉起并完成换装,页面轮询 /api/health 等版本号变化后自动刷新。 新增 start.sh / start.bat 作为正式启动入口,只按退出码决定是否重新拉起 (0 停止 / 75 应用新版 / 其余崩溃退避)。校验、冒烟、换装、回滚全部收在 selfupdate 包里而不写进脚本:这些逻辑在 sh 和 bat 上要写两套,而它们恰恰 最不能出错——换上跑不起来的二进制后守护进程会反复拉起它,只能上机器手工救。 Docker 的 ENTRYPOINT 一并改为该脚本,并由它转发 SIGTERM,否则 docker stop 只发给 PID 1,artex 做不了优雅关闭会被 SIGKILL 硬杀。 失败保护两道闸门互补:无法 exec 由换装前的冒烟挡住,能执行但初始化崩溃 由启动计数兜住(连续 3 次失败自动回滚 artex.old,坏的留作 artex.failed)。 GitHub 查询结果后端缓存 30 分钟(失败 2 分钟)。未认证 API 是每 IP 每小时 60 次,且这 60 次是服务器 IP 上所有用户共用的,顶栏每次整页加载都查一次, 不缓存会很快耗光配额导致真想更新时反而查不动。手动「检查更新」带 force=1 绕过缓存。 安全:只认 GitHub 域名且强制 HTTPS(逐跳校验重定向),发布源写死不可配置; 升级文件挂在可执行文件所在目录而非 CWD(服务化运行时 CWD 可能是 /); 开发构建版本号不可比较,禁用一键更新。 走完整 zip 而非裸二进制,因为现有 Release 的 SHA256SUMS 只覆盖 zip, 这样无需改 CI,也兼容已发布的历史版本。 已知限制:Docker 下只换程序不换镜像(工具链不同步、重建容器会退回镜像 版本);不同步发布包里的 skills/;更新即重启会中断运行中的任务。
This commit is contained in:
1 parent
557317491b
commit
44156a0a4c
22 files changed
+2619
-16
No files matched your search
@@ -14,6 +14,7 @@ import { AccountSwitcher } from "./sidebar/account-switcher";
|
||||
import { LayoutControls } from "./sidebar/layout-controls";
|
||||
import { SearchDialog } from "./sidebar/search-dialog";
|
||||
import { ThemeSwitcher } from "./sidebar/theme-switcher";
|
||||
import { UpdateBadge } from "./update-badge";
|
||||
|
||||
// 任务详情页保持原样:它自带头部/Tabs 与内边距,这里不再叠加全局头部和 padding。
|
||||
function isFullBleed(pathname: string) {
|
||||
@@ -64,6 +65,7 @@ export function MainContent({ children }: { children: ReactNode }) {
|
||||
{version && (
|
||||
<span className="font-medium text-muted-foreground text-xs tabular-nums">版本 · {version}</span>
|
||||
)}
|
||||
<UpdateBadge />
|
||||
<LayoutControls />
|
||||
<ThemeSwitcher />
|
||||
<AccountSwitcher users={[currentUser]} />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { ArrowUpCircleIcon } from "lucide-react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* 顶栏的"有新版本"提示:整页加载时查一次,有更新就在版本号旁边亮出来,
|
||||
* 点击直达系统配置页的「版本与更新」卡片。
|
||||
*
|
||||
* 后端对 GitHub 的查询结果有 30 分钟缓存,所以这里每次挂载都查一次是安全的
|
||||
* ——未认证的 GitHub API 只有 60 次/小时/IP,没有那层缓存的话,多开几个标签页
|
||||
* 就会把配额耗光,之后真想更新反而查不动。
|
||||
*
|
||||
* 查询失败一律静默:顶栏不是报错的地方,用户进设置页点「检查更新」会看到原因。
|
||||
*/
|
||||
export function UpdateBadge() {
|
||||
const [latest, setLatest] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
let alive = true;
|
||||
api
|
||||
.checkUpdate()
|
||||
.then((r) => {
|
||||
// has_update 已经包含了"版本号可比较"的判断,开发构建不会亮这个提示。
|
||||
if (alive && r.has_update && r.latest) setLatest(r.latest.replace(/^v(?=\d)/, ""));
|
||||
})
|
||||
.catch(() => {
|
||||
// 静默:没网 / GitHub 限流都不该在顶栏弹错误。
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!latest) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/system/settings"
|
||||
title={`发现新版本 ${latest},点击前往更新`}
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-primary px-2.5 py-1 font-medium text-primary-foreground text-xs transition-opacity hover:opacity-90"
|
||||
>
|
||||
{/* 呼吸点:顶栏元素很多,纯文字容易被忽略,动效让它一眼可见。 */}
|
||||
<span className="relative flex size-1.5">
|
||||
<span className="absolute inline-flex size-full animate-ping rounded-full bg-primary-foreground opacity-75" />
|
||||
<span className="relative inline-flex size-1.5 rounded-full bg-primary-foreground" />
|
||||
</span>
|
||||
<ArrowUpCircleIcon className="size-3.5" />
|
||||
<span className="hidden sm:inline">新版本 {latest}</span>
|
||||
<span className="sm:hidden">新版本</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
RefreshCwIcon,
|
||||
RotateCcwIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { api, sseUrl } from "@/lib/api";
|
||||
import type { UpdateCheck, UpdateProgress } from "@/lib/types";
|
||||
|
||||
/** 等待新版本上线的最长时间。一次升级要经过三次进程启动(暂存 → 换装 → 新版),
|
||||
* 每次都是秒级,三分钟足够覆盖慢磁盘和 Docker 容器重建。 */
|
||||
const RESTART_TIMEOUT_MS = 180_000;
|
||||
|
||||
function humanSize(n?: number): string {
|
||||
if (!n || n <= 0) return "";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let v = n;
|
||||
let i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
export function UpdateCard() {
|
||||
const [info, setInfo] = React.useState<UpdateCheck | null>(null);
|
||||
const [checking, setChecking] = React.useState(true);
|
||||
const [progress, setProgress] = React.useState<UpdateProgress | null>(null);
|
||||
// 与 progress 分开:暂存完成后进程就没了,SSE 会断,此时要切到轮询 /api/health。
|
||||
const [restarting, setRestarting] = React.useState(false);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
// quiet 同时决定要不要绕过后端缓存:进页面时的自动检查用缓存(顶栏刚查过),
|
||||
// 用户手动点「检查更新」则强制回源,否则刚发布的版本要等缓存过期才看得到。
|
||||
const check = React.useCallback((quiet = false) => {
|
||||
setChecking(true);
|
||||
api
|
||||
.checkUpdate(!quiet)
|
||||
.then((r) => {
|
||||
setInfo(r);
|
||||
if (!quiet) {
|
||||
if (r.error) toast.error("检查更新失败:" + r.error);
|
||||
else if (r.has_update) toast.success(`发现新版本 ${r.latest}`);
|
||||
else if (r.comparable) toast.success("当前已是最新版本");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!quiet) toast.error("检查更新失败:" + (e as Error).message);
|
||||
})
|
||||
.finally(() => setChecking(false));
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
check(true);
|
||||
}, [check]);
|
||||
|
||||
// 轮询 /api/health 直到版本号变化。
|
||||
//
|
||||
// 判据必须是"版本变了"而不是"能连上了":换装过程中旧版本会短暂地重新起来一次
|
||||
// (那一次只负责把 artex.new 换上去然后立刻退出),只看连通性会误判成功。
|
||||
const waitForNewVersion = React.useCallback(async (fromVersion: string) => {
|
||||
setRestarting(true);
|
||||
const deadline = Date.now() + RESTART_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(2000);
|
||||
try {
|
||||
const r = await fetch("/api/health", { cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const j = (await r.json()) as { version?: string };
|
||||
if (j.version && j.version !== fromVersion) {
|
||||
toast.success(`已更新到 ${j.version},正在重新加载页面`);
|
||||
await sleep(800);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 重启窗口内连不上是预期的,继续轮询。
|
||||
}
|
||||
}
|
||||
setRestarting(false);
|
||||
toast.error("等待服务重启超时。请检查后端日志,或确认 artex 是通过 start.sh / start.bat 启动的。");
|
||||
}, []);
|
||||
|
||||
// 订阅更新进度。SSE 不走 Next 的 /api 重写(那层会缓冲,事件推不出来)。
|
||||
const openStream = React.useCallback(
|
||||
(fromVersion: string) => {
|
||||
const es = new EventSource(sseUrl("/api/update/stream"));
|
||||
es.onmessage = (ev) => {
|
||||
let p: UpdateProgress;
|
||||
try {
|
||||
p = JSON.parse(ev.data) as UpdateProgress;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
setProgress(p);
|
||||
if (p.phase === "failed") {
|
||||
es.close();
|
||||
setBusy(false);
|
||||
toast.error("更新失败:" + (p.error || p.message));
|
||||
return;
|
||||
}
|
||||
if (p.phase === "staged") {
|
||||
es.close();
|
||||
void waitForNewVersion(fromVersion);
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
// 进程退出时 SSE 必然断开。如果已经进入等待重启,这属于正常现象,
|
||||
// 交给 /api/health 轮询继续判定即可。
|
||||
es.close();
|
||||
};
|
||||
return es;
|
||||
},
|
||||
[waitForNewVersion],
|
||||
);
|
||||
|
||||
const doUpdate = () => {
|
||||
if (!info) return;
|
||||
const from = info.current;
|
||||
const ok = window.confirm(
|
||||
`确定更新到 ${info.latest}?\n\n` +
|
||||
"更新会重启程序,正在运行的任务会被中断。\n" +
|
||||
(info.mode === "docker"
|
||||
? "\n注意:容器内更新只替换程序本身,不会更新镜像里的 playwright / nmap 等工具链;" +
|
||||
"若新版本依赖新工具,请改用 docker compose pull。"
|
||||
: ""),
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
setBusy(true);
|
||||
setProgress({ phase: "downloading", percent: 0, message: "准备中…" });
|
||||
const es = openStream(from);
|
||||
api.applyUpdate().catch((e) => {
|
||||
es.close();
|
||||
setBusy(false);
|
||||
setProgress(null);
|
||||
toast.error("启动更新失败:" + (e as Error).message);
|
||||
});
|
||||
};
|
||||
|
||||
const doRollback = () => {
|
||||
if (!info) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"回滚到上一版本?\n\n程序会重启,正在运行的任务会被中断。\n注意:数据库结构不会回退,旧版本可能无法识别新版写入的数据。",
|
||||
)
|
||||
)
|
||||
return;
|
||||
const from = info.current;
|
||||
setBusy(true);
|
||||
api
|
||||
.rollbackUpdate()
|
||||
.then(() => {
|
||||
toast.success("已切换到上一版本,正在重启…");
|
||||
void waitForNewVersion(from);
|
||||
})
|
||||
.catch((e) => {
|
||||
setBusy(false);
|
||||
toast.error("回滚失败:" + (e as Error).message);
|
||||
});
|
||||
};
|
||||
|
||||
const phase = progress?.phase;
|
||||
const showProgress = busy || restarting;
|
||||
// 只有下载阶段拿得到真实百分比(按 Content-Length 算)。校验/解压/等待重启都是
|
||||
// 时长不可知的阶段,进度条填满并加个脉冲动画表示"在忙但说不准还要多久"。
|
||||
const downloading = !restarting && phase === "downloading";
|
||||
const pct = downloading ? Math.max(progress?.percent ?? 0, 0) : 100;
|
||||
|
||||
return (
|
||||
// 设置页是多列瀑布流布局,卡片自己负责行间距并禁止跨列断开(见 page.tsx 的注释)。
|
||||
<Card className="mb-4 break-inside-avoid md:mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<DownloadIcon className="size-4" />
|
||||
版本与更新
|
||||
</CardTitle>
|
||||
<CardDescription>从 GitHub 检查并安装新版本。更新会重启程序,正在运行的任务会被中断。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">当前版本</span>
|
||||
<Badge variant="secondary" className="font-mono">
|
||||
{info?.current ?? "…"}
|
||||
</Badge>
|
||||
{info && (
|
||||
<>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{info.os}/{info.arch}
|
||||
</Badge>
|
||||
<Badge variant="outline">{info.mode === "docker" ? "Docker" : "独立程序"}</Badge>
|
||||
</>
|
||||
)}
|
||||
{info?.latest && (
|
||||
<>
|
||||
<span className="text-muted-foreground">最新版本</span>
|
||||
<Badge variant={info.has_update ? "default" : "secondary"} className="font-mono">
|
||||
{info.latest}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
{info?.html_url && (
|
||||
<a
|
||||
href={info.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-4 hover:underline"
|
||||
>
|
||||
更新日志 <ExternalLinkIcon className="size-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{info?.boot_notice && (
|
||||
<p className="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-2 text-xs text-amber-700 dark:text-amber-400">
|
||||
<TriangleAlertIcon className="mt-0.5 size-3.5 shrink-0" />
|
||||
{info.boot_notice}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info?.error && (
|
||||
<p className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-xs text-destructive">
|
||||
<TriangleAlertIcon className="mt-0.5 size-3.5 shrink-0" />
|
||||
无法连接 GitHub:{info.error}
|
||||
{" "}可在上方配置全局代理后重试。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info && !info.comparable && info.reason && <p className="text-xs text-muted-foreground">{info.reason}</p>}
|
||||
|
||||
{info?.has_update && info.asset_available === false && (
|
||||
<p className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-xs text-destructive">
|
||||
<TriangleAlertIcon className="mt-0.5 size-3.5 shrink-0" />
|
||||
{info.latest} 没有提供 {info.os}/{info.arch} 的发布包(缺少 {info.asset}),无法自动更新。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info?.has_update && info.asset_available !== false && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
将下载 <span className="font-mono">{info.asset}</span>
|
||||
{info.size ? `(${humanSize(info.size)})` : ""},校验 SHA256 并冒烟测试后才会替换,失败自动保留当前版本。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info && !info.has_update && info.comparable && !info.error && (
|
||||
<p className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<CheckCircle2Icon className="size-3.5 text-emerald-600" />
|
||||
当前已是最新版本。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info?.mode === "docker" && info.has_update && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Docker 下的更新只替换程序本身,不更新镜像里的 playwright / nmap 等工具链,且
|
||||
<span className="font-mono"> docker compose up -d </span>
|
||||
重建容器后会退回镜像自带的版本。需要连镜像一起升级请执行
|
||||
<span className="font-mono"> docker compose pull artex && docker compose up -d artex</span>。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showProgress && (
|
||||
<div className="space-y-1.5">
|
||||
<Progress value={pct} className={downloading ? undefined : "animate-pulse"} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{restarting ? "正在重启并应用新版本,请稍候(页面会自动刷新)…" : progress?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => check(false)} disabled={checking || busy || restarting}>
|
||||
<RefreshCwIcon className={checking ? "size-4 animate-spin" : "size-4"} />
|
||||
检查更新
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={doUpdate}
|
||||
disabled={busy || restarting || !info?.has_update || info?.asset_available === false}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
{info?.has_update ? `更新到 ${info.latest}` : "立即更新"}
|
||||
</Button>
|
||||
{info?.has_backup && (
|
||||
<Button variant="ghost" size="sm" onClick={doRollback} disabled={busy || restarting}>
|
||||
<RotateCcwIcon className="size-4" />
|
||||
回滚到上一版本
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
一键更新依赖守护脚本重启程序。请通过 <span className="font-mono">start.sh</span>(Windows 为
|
||||
<span className="font-mono"> start.bat</span>)启动 ARTEX;直接运行 artex 本体时,程序退出后不会被自动拉起。
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { api } from "@/lib/api";
|
||||
import { CHAT_SEND_MODE_OPTIONS, type ChatSendMode, setChatSendMode, useChatSendMode } from "@/lib/chat-send-mode";
|
||||
import type { Settings } from "@/lib/types";
|
||||
|
||||
import { UpdateCard } from "./_components/update-card";
|
||||
|
||||
export default function SystemSettingsPage() {
|
||||
const [trafficCapture, setTrafficCapture] = React.useState(false);
|
||||
const [agentTrafficBinding, setAgentTrafficBinding] = React.useState(false);
|
||||
@@ -247,6 +249,8 @@ export default function SystemSettingsPage() {
|
||||
多列则自动按内容高度平衡填充。卡片间距靠 mb 而非 gap——多列布局下
|
||||
column-gap 只管列间距,行间距要由子元素自己给。 */}
|
||||
<div className="columns-1 gap-4 md:gap-6 lg:columns-2">
|
||||
<UpdateCard />
|
||||
|
||||
<Card className="mb-4 break-inside-avoid md:mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
|
||||
+18
-2
@@ -89,6 +89,7 @@ import type {
|
||||
TrafficEvidenceRole,
|
||||
TrafficHost,
|
||||
TrafficResp,
|
||||
UpdateCheck,
|
||||
UsageStats,
|
||||
WorkspaceFile,
|
||||
WorkspaceListing,
|
||||
@@ -619,11 +620,15 @@ export const api = {
|
||||
// 删除漏洞:移除 findings 记录 + 来源探索节点(从发现列表/任务发现 Tab/探索图一并消失)。
|
||||
deleteFinding: (id: string) => del<{ deleted: boolean; id: number }>(`/exploration/findings/${id}`),
|
||||
findingRetests: (id: string) =>
|
||||
get<{ retests: FindingRetest[] }>(`/exploration/findings/${encodeURIComponent(id)}/retests`).then((r) => arr(r.retests)),
|
||||
get<{ retests: FindingRetest[] }>(`/exploration/findings/${encodeURIComponent(id)}/retests`).then((r) =>
|
||||
arr(r.retests),
|
||||
),
|
||||
activeFindingRetests: () =>
|
||||
get<{ retests: ActiveFindingRetest[] }>("/exploration/findings/retests/active").then((r) => arr(r.retests)),
|
||||
startFindingRetest: (id: string, notes: string) =>
|
||||
post<{ retest: FindingRetest; created: boolean }>(`/exploration/findings/${encodeURIComponent(id)}/retests`, { notes }),
|
||||
post<{ retest: FindingRetest; created: boolean }>(`/exploration/findings/${encodeURIComponent(id)}/retests`, {
|
||||
notes,
|
||||
}),
|
||||
deepenFinding: (id: string, description: string) =>
|
||||
post<FindingDeepenResponse>(`/exploration/findings/${id}/deepen`, { description }),
|
||||
intents: (task?: string) => get<TaskNode[]>(`/exploration/intents${tq(task)}`).then(arr),
|
||||
@@ -1123,4 +1128,15 @@ export const api = {
|
||||
// per-agent 绑定 / 轮询 / 中断消耗都覆盖)。
|
||||
tokensByModel: (task: string) =>
|
||||
get<{ models: ModelTokenStat[] }>(`/llm/records/by-model?task=${encodeURIComponent(task)}`),
|
||||
|
||||
// ---- 一键更新 ----
|
||||
// 检查以后端为准:下载是后端做的,浏览器能连 GitHub 而服务器连不上的情况很常见
|
||||
// (服务器在内网、代理只配在浏览器上),那时点更新必然失败。
|
||||
// 后端对 GitHub 的查询结果有 30 分钟缓存(未认证的 GitHub API 是 60 次/小时/IP,
|
||||
// 顶栏每次整页加载都会查一次,不缓存会很快耗光配额)。force=true 强制回源,
|
||||
// 留给用户显式点「检查更新」时用。
|
||||
checkUpdate: (force = false) => get<UpdateCheck>(`/update/check${force ? "?force=1" : ""}`),
|
||||
// 202 即返回,实际下载在后台跑,进度走 /api/update/stream。
|
||||
applyUpdate: () => post<{ ok: boolean; target: string }>(`/update/apply`),
|
||||
rollbackUpdate: () => post<{ ok: boolean }>(`/update/rollback`),
|
||||
};
|
||||
+44
-1
@@ -1288,7 +1288,9 @@ export interface InterceptAudit {
|
||||
input_digest: string;
|
||||
user_message: string;
|
||||
user_truncated?: boolean;
|
||||
context: { kind: string; tool?: string; tool_use_id?: string; text: string; is_error?: boolean; truncated?: boolean }[] | null;
|
||||
context:
|
||||
| { kind: string; tool?: string; tool_use_id?: string; text: string; is_error?: boolean; truncated?: boolean }[]
|
||||
| null;
|
||||
context_truncated?: boolean;
|
||||
captured_at: string;
|
||||
model_fallback?: boolean;
|
||||
@@ -1358,3 +1360,44 @@ export interface FindingTrafficDetail {
|
||||
request: EvidenceBodyPreview;
|
||||
response: EvidenceBodyPreview;
|
||||
}
|
||||
|
||||
/** GET /api/update/check —— 当前版本与 GitHub 最新正式版的比较结果。 */
|
||||
export interface UpdateCheck {
|
||||
/** 当前运行的版本;开发构建为 "dev" 或 git describe 的带后缀形式。 */
|
||||
current: string;
|
||||
/** 运行形态。docker 下换装只作用于容器可写层,重建容器会退回镜像版本。 */
|
||||
mode: "docker" | "binary";
|
||||
os: string;
|
||||
arch: string;
|
||||
repo: string;
|
||||
/** 是否存在可回滚的上一版本(artex.old)。 */
|
||||
has_backup: boolean;
|
||||
/** 本次启动时自更新自举的结论(换装失败 / 已回滚等),无事发生时为空。 */
|
||||
boot_notice?: string;
|
||||
rolled_back?: boolean;
|
||||
/** 查询 GitHub 失败时给出原因,此时下面的字段都不会有。 */
|
||||
error?: string;
|
||||
latest?: string;
|
||||
notes?: string;
|
||||
html_url?: string;
|
||||
published_at?: string;
|
||||
/** 当前平台对应的发布包名,以及该 Release 是否真的带了它。 */
|
||||
asset?: string;
|
||||
asset_available?: boolean;
|
||||
size?: number;
|
||||
has_update?: boolean;
|
||||
/** 双方版本号是否可比较;开发构建为 false,此时禁用一键更新。 */
|
||||
comparable?: boolean;
|
||||
/** comparable 为 false 时的说明。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** /api/update/stream 推送的一条更新进度。 */
|
||||
export interface UpdateProgress {
|
||||
phase: "idle" | "downloading" | "verifying" | "extracting" | "staged" | "failed";
|
||||
/** 仅下载阶段有意义(0-100);其余阶段为 -1。 */
|
||||
percent: number;
|
||||
message: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
}
|
||||
Reference in new issue
Block a user