plugin runtime

This commit is contained in:
leookun
2026-08-30 13:35:03 +08:00
parent e5bfcdd202
commit 1609b57433
20 changed files with 1417 additions and 26 deletions
Generated
+22
View File
@@ -1248,6 +1248,7 @@ dependencies = [
"uuid", "uuid",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"x509-parser", "x509-parser",
"zip",
] ]
[[package]] [[package]]
@@ -1929,6 +1930,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
"zlib-rs",
] ]
[[package]] [[package]]
@@ -9079,16 +9081,36 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",
"flate2",
"indexmap 2.14.0", "indexmap 2.14.0",
"memchr", "memchr",
"zopfli",
] ]
[[package]]
name = "zlib-rs"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.3" version = "0.13.3"
+2
View File
@@ -8,6 +8,7 @@ import { CallsPage } from "./features/calls/CallsPage";
import { CallDetailsPage } from "./features/calls/CallDetailsPage"; import { CallDetailsPage } from "./features/calls/CallDetailsPage";
import { CursorSettingsPage } from "./features/models/CursorSettingsPage"; import { CursorSettingsPage } from "./features/models/CursorSettingsPage";
import { HomePage } from "./features/home/HomePage"; import { HomePage } from "./features/home/HomePage";
import { PluginManagementPage } from "./features/plugins/PluginManagementPage";
import { SettingsPage } from "./features/settings/SettingsPage"; import { SettingsPage } from "./features/settings/SettingsPage";
import { useAppStore } from "./shared/store/appStore"; import { useAppStore } from "./shared/store/appStore";
import { updateStore } from "./shared/store/updateStore"; import { updateStore } from "./shared/store/updateStore";
@@ -23,6 +24,7 @@ export function App() {
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
<Route path="calls" element={<CallsPage />} /> <Route path="calls" element={<CallsPage />} />
<Route path="harness/cursor" element={<CursorSettingsPage />} /> <Route path="harness/cursor" element={<CursorSettingsPage />} />
<Route path="plugins" element={<PluginManagementPage />} />
<Route path="settings" element={<SettingsPage />} /> <Route path="settings" element={<SettingsPage />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
@@ -0,0 +1,86 @@
@use "../../styles/typography" as type;
.page {
display: grid;
gap: 16px;
}
.gate {
min-height: 250px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 9px;
color: var(--vscode-descriptionForeground);
text-align: center;
border: 1px dashed var(--vscode-sideBar-border);
border-radius: var(--oa-overlay-radius);
strong {
color: var(--vscode-foreground);
font-size: type.$font-size-base;
}
span {
max-width: 500px;
font-size: type.$font-size-xs;
}
button {
margin-top: 6px;
}
}
.runtimeDetails {
display: grid;
padding: 8px 16px;
> div {
min-height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid var(--vscode-sideBar-border);
&:last-child {
border-bottom: 0;
}
}
span {
color: var(--vscode-descriptionForeground);
font-size: type.$font-size-xs;
}
.ready {
color: var(--vscode-testing-iconPassed, #73c991);
}
}
.progressContent {
display: flex;
flex-direction: column;
gap: 12px;
strong {
font-size: type.$font-size-base;
}
progress {
width: 100%;
height: 8px;
accent-color: var(--vscode-progressBar-background);
}
span {
color: var(--vscode-descriptionForeground);
font-size: type.$font-size-xs;
}
.error {
color: var(--vscode-errorForeground);
overflow-wrap: anywhere;
}
}
@@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from "react";
import type { PluginRuntimePhase, PluginRuntimeStatus } from "../../shared/api";
import { PageContent } from "../../shell/layout/PageContent";
import { appStore, useAppStore } from "../../shared/store/appStore";
import { Button } from "../../shared/ui/Button";
import { Modal } from "../../shared/ui/Modal";
import { TitledCard } from "../../shared/ui/TitledCard";
import styles from "./PluginManagementPage.module.scss";
export function PluginManagementPage() {
const { pluginRuntime } = useAppStore();
const [progressOpen, setProgressOpen] = useState(false);
const [starting, setStarting] = useState(false);
const cancelRequested = useRef(false);
useEffect(() => {
if (!pluginRuntime) void appStore.refreshPluginRuntime();
}, [pluginRuntime]);
useEffect(() => {
if (pluginRuntime?.state !== "initializing") return;
if (!cancelRequested.current) setProgressOpen(true);
const timer = window.setInterval(() => void appStore.refreshPluginRuntime(), 300);
return () => window.clearInterval(timer);
}, [pluginRuntime?.state]);
const initialize = async () => {
if (starting) return;
cancelRequested.current = false;
setStarting(true);
setProgressOpen(true);
const status = await appStore.initializePluginRuntime();
setStarting(false);
if (!status) {
setProgressOpen(false);
} else if (cancelRequested.current && status.state === "initializing") {
void appStore.cancelPluginRuntimeInitialization();
}
};
const closeProgress = () => {
setProgressOpen(false);
cancelRequested.current = true;
if (pluginRuntime?.state === "initializing") {
void appStore.cancelPluginRuntimeInitialization();
}
};
const content = pluginRuntime?.state === "ready"
? <RuntimeReady status={pluginRuntime} />
: <RuntimeGate status={pluginRuntime} starting={starting} onInitialize={() => void initialize()} />;
return <>
<PageContent
title={t("插件管理")}
sections={[{ key: "plugin-runtime", estimatedHeight: 320, content }]}
/>
<RuntimeProgressModal
open={progressOpen}
status={pluginRuntime}
starting={starting}
onClose={closeProgress}
/>
</>;
}
function RuntimeGate({ status, starting, onInitialize }: { status: PluginRuntimeStatus | null; starting: boolean; onInitialize: () => void }) {
const checking = status === null;
const initializing = starting || status?.state === "initializing";
const failed = status?.state === "failed";
const unsupported = status?.state === "unsupported";
const title = checking
? t("正在检查插件运行时")
: failed
? t("插件运行时初始化失败")
: unsupported
? t("当前系统不支持插件运行时")
: t("需要先初始化插件运行时");
const description = failed
? t("请重试初始化")
: unsupported
? status.error ?? t("当前操作系统或 CPU 架构暂不受支持")
: t("初始化将下载并安装插件运行时。");
return <div className={styles.gate}>
<strong>{title}</strong>
<span>{description}</span>
{!unsupported && <Button variant="primary" disabled={checking || initializing} onClick={onInitialize}>
{checking ? t("检查中…") : initializing ? t("初始化中…") : failed ? t("重新初始化插件") : t("初始化插件")}
</Button>}
</div>;
}
function RuntimeReady({ status }: { status: PluginRuntimeStatus }) {
return <div className={styles.page}>
<TitledCard title={t("插件运行时")}>
<div className={styles.runtimeDetails}>
<div><strong>{t("状态")}</strong><span className={styles.ready}>{t("已就绪")}</span></div>
<div><strong>{t("插件运行时版本")}</strong><span>{status.version}</span></div>
<div><strong>{t("运行平台")}</strong><span>{status.target}</span></div>
</div>
</TitledCard>
</div>;
}
function RuntimeProgressModal({ open, status, starting, onClose }: { open: boolean; status: PluginRuntimeStatus | null; starting: boolean; onClose: () => void }) {
const initializing = starting || status?.state === "initializing";
const downloaded = status?.downloaded_bytes ?? 0;
const total = status?.total_bytes ?? null;
const percent = total && total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null;
const stage = status?.state === "ready"
? t("插件运行时初始化完成")
: status?.state === "failed"
? t("插件运行时初始化失败")
: phaseText(status?.phase ?? null);
return <Modal
open={open}
title={t("初始化插件运行时")}
closeLabel={status?.state === "ready" ? t("完成") : initializing ? t("取消") : t("关闭")}
onClose={onClose}
>
<div className={styles.progressContent} aria-live="polite">
<strong>{stage}</strong>
{status?.phase === "downloading" && <>
<progress
aria-label={t("下载进度")}
value={percent ?? undefined}
max={100}
/>
<span>
{total ? t("已下载 {downloaded} / {total}", { downloaded: formatBytes(downloaded), total: formatBytes(total) }) : t("已下载 {downloaded}", { downloaded: formatBytes(downloaded) })}
</span>
</>}
{status?.state === "failed" && <span className={styles.error}>{t("请重试初始化")}</span>}
{status?.state === "ready" && <span>{t("插件运行时 {version} 已安装,可以开始使用插件。", { version: status.version })}</span>}
</div>
</Modal>;
}
function phaseText(phase: PluginRuntimePhase | null) {
switch (phase) {
case "checking": return t("正在检查插件运行时");
case "downloading": return t("正在下载插件运行时");
case "verifying": return t("正在验证插件运行时下载文件");
case "installing": return t("正在安装插件运行时");
case "validating": return t("正在验证插件运行时");
default: return t("正在准备插件运行时");
}
}
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB"];
let value = bytes / 1024;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
return `${value < 10 ? value.toFixed(1) : value.toFixed(0)} ${units[unit]}`;
}
+398 -20
View File
@@ -124,6 +124,18 @@
} }
] ]
}, },
"06619f339fa0ab46": {
"source": "正在准备插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 130,
"column": 21
}
]
},
"076832c1b2de22c3": { "076832c1b2de22c3": {
"source": "缓存写入:{tokens}", "source": "缓存写入:{tokens}",
"kind": "template", "kind": "template",
@@ -752,6 +764,20 @@
} }
] ]
}, },
"2400fbd0aeab9e13": {
"source": "已下载 {downloaded}",
"kind": "template",
"placeholders": [
"downloaded"
],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 114,
"column": 122
}
]
},
"24a0a24864454575": { "24a0a24864454575": {
"source": "已存在,跳过", "source": "已存在,跳过",
"kind": "text", "kind": "text",
@@ -797,7 +823,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 238, "line": 240,
"column": 14 "column": 14
} }
] ]
@@ -883,12 +909,12 @@
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 228, "line": 230,
"column": 20 "column": 20
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 239, "line": 241,
"column": 20 "column": 20
} }
] ]
@@ -993,7 +1019,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 227, "line": 229,
"column": 14 "column": 14
} }
] ]
@@ -1046,6 +1072,23 @@
} }
] ]
}, },
"35fcbd57d58a9394": {
"source": "插件管理",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 37,
"column": 14
},
{
"file": "shell/AppLayout.tsx",
"line": 80,
"column": 46
}
]
},
"36f33adaf0942634": { "36f33adaf0942634": {
"source": "确认", "source": "确认",
"kind": "text", "kind": "text",
@@ -1058,7 +1101,7 @@
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 240, "line": 242,
"column": 21 "column": 21
} }
] ]
@@ -1275,6 +1318,18 @@
} }
] ]
}, },
"3e0b34ddc2121f7d": {
"source": "运行平台",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 81,
"column": 23
}
]
},
"3f6c25aa329163a4": { "3f6c25aa329163a4": {
"source": "原接口路径会追加到此服务地址。", "source": "原接口路径会追加到此服务地址。",
"kind": "text", "kind": "text",
@@ -1308,6 +1363,23 @@
"file": "features/models/CursorSettingsPage.tsx", "file": "features/models/CursorSettingsPage.tsx",
"line": 262, "line": 262,
"column": 80 "column": 80
},
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 102,
"column": 55
}
]
},
"402495402ce333b1": {
"source": "重新初始化插件",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 70,
"column": 68
} }
] ]
}, },
@@ -1541,6 +1613,18 @@
} }
] ]
}, },
"4a861200ad513a3c": {
"source": "初始化插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 100,
"column": 12
}
]
},
"4a8d6841b4023edf": { "4a8d6841b4023edf": {
"source": "确认导入", "source": "确认导入",
"kind": "text", "kind": "text",
@@ -1553,6 +1637,18 @@
} }
] ]
}, },
"4aca6a31090fe2b8": {
"source": "初始化中…",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 70,
"column": 46
}
]
},
"4b458e6e147221d7": { "4b458e6e147221d7": {
"source": "系统会根据请求协议自动追加标准端点路径。", "source": "系统会根据请求协议自动追加标准端点路径。",
"kind": "text", "kind": "text",
@@ -1577,7 +1673,7 @@
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 200, "line": 202,
"column": 67 "column": 67
} }
] ]
@@ -1871,6 +1967,18 @@
} }
] ]
}, },
"5c62e36c152dfc7c": {
"source": "插件运行时初始化完成",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 93,
"column": 7
}
]
},
"5d59857bf039cac9": { "5d59857bf039cac9": {
"source": "Cursor 助手 v{version}", "source": "Cursor 助手 v{version}",
"kind": "template", "kind": "template",
@@ -1904,7 +2012,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 244, "line": 246,
"column": 11 "column": 11
} }
] ]
@@ -1994,6 +2102,11 @@
"file": "features/calls/CallTable.tsx", "file": "features/calls/CallTable.tsx",
"line": 16, "line": 16,
"column": 15 "column": 15
},
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 79,
"column": 23
} }
] ]
}, },
@@ -2028,7 +2141,7 @@
"refs": [ "refs": [
{ {
"file": "shared/api.ts", "file": "shared/api.ts",
"line": 318, "line": 333,
"column": 43 "column": 43
} }
] ]
@@ -2280,12 +2393,12 @@
"refs": [ "refs": [
{ {
"file": "shared/store/appStore.ts", "file": "shared/store/appStore.ts",
"line": 153, "line": 177,
"column": 23 "column": 23
}, },
{ {
"file": "shared/store/appStore.ts", "file": "shared/store/appStore.ts",
"line": 160, "line": 184,
"column": 25 "column": 25
} }
] ]
@@ -2333,7 +2446,7 @@
"refs": [ "refs": [
{ {
"file": "shared/api.ts", "file": "shared/api.ts",
"line": 313, "line": 328,
"column": 43 "column": 43
} }
] ]
@@ -2362,6 +2475,18 @@
} }
] ]
}, },
"7c10d97162c96dbd": {
"source": "正在验证插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 129,
"column": 31
}
]
},
"7cea2f3c46565d29": { "7cea2f3c46565d29": {
"source": "OpenAI 额外参数", "source": "OpenAI 额外参数",
"kind": "text", "kind": "text",
@@ -2388,7 +2513,7 @@
"refs": [ "refs": [
{ {
"file": "App.tsx", "file": "App.tsx",
"line": 50, "line": 52,
"column": 19 "column": 19
} }
] ]
@@ -2448,11 +2573,23 @@
"refs": [ "refs": [
{ {
"file": "shared/api.ts", "file": "shared/api.ts",
"line": 262, "line": 275,
"column": 21 "column": 21
} }
] ]
}, },
"8213941f12320ce1": {
"source": "当前操作系统或 CPU 架构暂不受支持",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 63,
"column": 25
}
]
},
"83c4efccd9a6bf69": { "83c4efccd9a6bf69": {
"source": "连通性测试已取消:成功 {successful},失败 {failed}", "source": "连通性测试已取消:成功 {successful},失败 {failed}",
"kind": "template", "kind": "template",
@@ -2468,6 +2605,21 @@
} }
] ]
}, },
"83e8d0b7aff2b394": {
"source": "已下载 {downloaded} / {total}",
"kind": "template",
"placeholders": [
"downloaded",
"total"
],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 114,
"column": 20
}
]
},
"83fcfb4c1f2c1641": { "83fcfb4c1f2c1641": {
"source": "获取模型", "source": "获取模型",
"kind": "text", "kind": "text",
@@ -2572,6 +2724,18 @@
} }
] ]
}, },
"8911e4f1407d58cb": {
"source": "正在下载插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 126,
"column": 32
}
]
},
"89a101b809be7cfc": { "89a101b809be7cfc": {
"source": "系统会原样使用此地址,不追加或修改请求路径。", "source": "系统会原样使用此地址,不追加或修改请求路径。",
"kind": "text", "kind": "text",
@@ -2649,7 +2813,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 246, "line": 248,
"column": 16 "column": 16
} }
] ]
@@ -2757,6 +2921,18 @@
} }
] ]
}, },
"945fb1c67eca8493": {
"source": "正在安装插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 128,
"column": 31
}
]
},
"946b3ffc02f026c0": { "946b3ffc02f026c0": {
"source": "确定删除这个模型吗?", "source": "确定删除这个模型吗?",
"kind": "text", "kind": "text",
@@ -2824,7 +3000,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 233, "line": 235,
"column": 11 "column": 11
} }
] ]
@@ -2856,6 +3032,23 @@
} }
] ]
}, },
"9db205c6055bacc4": {
"source": "插件运行时初始化失败",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 56,
"column": 9
},
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 95,
"column": 9
}
]
},
"9e356080c56877f8": { "9e356080c56877f8": {
"source": "已关闭静默启动", "source": "已关闭静默启动",
"kind": "text", "kind": "text",
@@ -2880,6 +3073,18 @@
} }
] ]
}, },
"9ed11266ead88f5b": {
"source": "正在验证插件运行时下载文件",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 127,
"column": 30
}
]
},
"9f6fee1aba17a565": { "9f6fee1aba17a565": {
"source": "语言", "source": "语言",
"kind": "text", "kind": "text",
@@ -3069,7 +3274,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 229, "line": 231,
"column": 21 "column": 21
} }
] ]
@@ -3176,6 +3381,18 @@
} }
] ]
}, },
"ab27f80d046f3d7f": {
"source": "已就绪",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 79,
"column": 72
}
]
},
"ab9084a640fbb864": { "ab9084a640fbb864": {
"source": "全不选", "source": "全不选",
"kind": "text", "kind": "text",
@@ -3281,12 +3498,12 @@
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 260, "line": 262,
"column": 64 "column": 64
}, },
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 260, "line": 262,
"column": 125 "column": 125
} }
] ]
@@ -3320,6 +3537,23 @@
} }
] ]
}, },
"b254ff315d861346": {
"source": "请重试初始化",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 61,
"column": 7
},
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 117,
"column": 70
}
]
},
"b4411558b932266f": { "b4411558b932266f": {
"source": "上游类型", "source": "上游类型",
"kind": "text", "kind": "text",
@@ -3332,6 +3566,18 @@
} }
] ]
}, },
"b4c9e08870d41aa2": {
"source": "需要先初始化插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 59,
"column": 11
}
]
},
"b502b1d414664337": { "b502b1d414664337": {
"source": "提示词:{tokens}", "source": "提示词:{tokens}",
"kind": "template", "kind": "template",
@@ -3556,11 +3802,23 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 251, "line": 253,
"column": 24 "column": 24
} }
] ]
}, },
"bda74b5674b6a57d": {
"source": "初始化插件",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 70,
"column": 83
}
]
},
"bf57afd709694b55": { "bf57afd709694b55": {
"source": "概览时间范围", "source": "概览时间范围",
"kind": "text", "kind": "text",
@@ -3587,6 +3845,18 @@
} }
] ]
}, },
"c0b3fbff51ccc40b": {
"source": "完成",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 102,
"column": 45
}
]
},
"c1e98892a77f7a19": { "c1e98892a77f7a19": {
"source": "{count} 条/页", "source": "{count} 条/页",
"kind": "template", "kind": "template",
@@ -3618,6 +3888,18 @@
} }
] ]
}, },
"c54863655e879b36": {
"source": "当前系统不支持插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 58,
"column": 11
}
]
},
"c7ea2c9bc43134bd": { "c7ea2c9bc43134bd": {
"source": "编辑模型", "source": "编辑模型",
"kind": "text", "kind": "text",
@@ -3719,6 +4001,18 @@
} }
] ]
}, },
"cb99f0138b032687": {
"source": "初始化将下载并安装插件运行时。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 64,
"column": 9
}
]
},
"cea1aafe9416de7b": { "cea1aafe9416de7b": {
"source": "请求头", "source": "请求头",
"kind": "text", "kind": "text",
@@ -3905,6 +4199,32 @@
} }
] ]
}, },
"d6e61888b07853ea": {
"source": "高级",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "shell/AppLayout.tsx",
"line": 79,
"column": 29
}
]
},
"d766536c18e8e990": {
"source": "插件运行时 {version} 已安装,可以开始使用插件。",
"kind": "template",
"placeholders": [
"version"
],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 118,
"column": 44
}
]
},
"d86fa42c3848c680": { "d86fa42c3848c680": {
"source": "使用系统代理", "source": "使用系统代理",
"kind": "text", "kind": "text",
@@ -3929,7 +4249,7 @@
"refs": [ "refs": [
{ {
"file": "shell/AppLayout.tsx", "file": "shell/AppLayout.tsx",
"line": 186, "line": 188,
"column": 54 "column": 54
} }
] ]
@@ -4270,6 +4590,18 @@
} }
] ]
}, },
"e7cef7b834f301e7": {
"source": "插件运行时版本",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 80,
"column": 23
}
]
},
"e825a2a42c22380e": { "e825a2a42c22380e": {
"source": "模型类型", "source": "模型类型",
"kind": "text", "kind": "text",
@@ -4451,6 +4783,18 @@
} }
] ]
}, },
"f4f4c81a4d719711": {
"source": "插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 77,
"column": 24
}
]
},
"f4fa9f31ea2ae58d": { "f4fa9f31ea2ae58d": {
"source": "过去一年的 Token 用量日历", "source": "过去一年的 Token 用量日历",
"kind": "text", "kind": "text",
@@ -4575,11 +4919,28 @@
} }
] ]
}, },
"fad86bf65f72c747": {
"source": "下载进度",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 109,
"column": 23
}
]
},
"fb11aa6f29827095": { "fb11aa6f29827095": {
"source": "检查中…", "source": "检查中…",
"kind": "text", "kind": "text",
"placeholders": [], "placeholders": [],
"refs": [ "refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 70,
"column": 19
},
{ {
"file": "features/settings/AppLifecycleSettingsCard.tsx", "file": "features/settings/AppLifecycleSettingsCard.tsx",
"line": 160, "line": 160,
@@ -4599,6 +4960,23 @@
} }
] ]
}, },
"fc22d1ab9ac73c6f": {
"source": "正在检查插件运行时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 54,
"column": 7
},
{
"file": "features/plugins/PluginManagementPage.tsx",
"line": 125,
"column": 29
}
]
},
"fc3947ebe6b2177b": { "fc3947ebe6b2177b": {
"source": "默认 {defaultRate} / 计入创建 {reuseRate}", "source": "默认 {defaultRate} / 计入创建 {reuseRate}",
"kind": "template", "kind": "template",
+28
View File
@@ -7,6 +7,7 @@
"051836569928a9f9": "Edit", "051836569928a9f9": "Edit",
"05468af47054d488": "Connectivity test for {model} succeeded ({duration} ms)", "05468af47054d488": "Connectivity test for {model} succeeded ({duration} ms)",
"0580e0a99a6f1afc": "Artifacts", "0580e0a99a6f1afc": "Artifacts",
"06619f339fa0ab46": "Preparing the plugin runtime",
"076832c1b2de22c3": "Cache write: {tokens}", "076832c1b2de22c3": "Cache write: {tokens}",
"07879e064ae16542": "Estimated output: {tokens}", "07879e064ae16542": "Estimated output: {tokens}",
"07c657ed4747126e": "Anthropic extra parameters", "07c657ed4747126e": "Anthropic extra parameters",
@@ -50,6 +51,7 @@
"22d7895ea5fca72e": "By provider", "22d7895ea5fca72e": "By provider",
"23ae7a90b1b9816d": "Clear scope", "23ae7a90b1b9816d": "Clear scope",
"23e49479e15e6770": "Version {version} is available", "23e49479e15e6770": "Version {version} is available",
"2400fbd0aeab9e13": "Downloaded {downloaded}",
"24a0a24864454575": "Existing, skipped", "24a0a24864454575": "Existing, skipped",
"2555d6c7fbb7e070": "Enter a model ID directly or load models returned by the API.", "2555d6c7fbb7e070": "Enter a model ID directly or load models returned by the API.",
"29585d7193539200": "Current version {version}", "29585d7193539200": "Current version {version}",
@@ -68,6 +70,7 @@
"2f7dec3be28d7597": "{count} selected", "2f7dec3be28d7597": "{count} selected",
"2f9daa828907b93f": "Delete", "2f9daa828907b93f": "Delete",
"346ff60e6c7c5181": "Reading…", "346ff60e6c7c5181": "Reading…",
"35fcbd57d58a9394": "Plugin management",
"36f33adaf0942634": "Confirm", "36f33adaf0942634": "Confirm",
"37125ef2e1d707cb": "Server address or complete request URL, API Key, model name, display name, and note are required", "37125ef2e1d707cb": "Server address or complete request URL, API Key, model name, display name, and note are required",
"378bb0eec39fa8a2": "Last page", "378bb0eec39fa8a2": "Last page",
@@ -85,9 +88,11 @@
"3cfae5728b92b334": "Token usage: {tokens}", "3cfae5728b92b334": "Token usage: {tokens}",
"3d13868593ae4eeb": "Display language", "3d13868593ae4eeb": "Display language",
"3da0bf1610ff5db5": "Recommended", "3da0bf1610ff5db5": "Recommended",
"3e0b34ddc2121f7d": "Runtime platform",
"3f6c25aa329163a4": "The original endpoint path is appended to this service address.", "3f6c25aa329163a4": "The original endpoint path is appended to this service address.",
"3fd118e2ffe0b2b6": "Cancel all tests", "3fd118e2ffe0b2b6": "Cancel all tests",
"3fd47edce45b3603": "Close", "3fd47edce45b3603": "Close",
"402495402ce333b1": "Reinitialize plugins",
"40a08e7cf320ae07": "Clear detailed records?", "40a08e7cf320ae07": "Clear detailed records?",
"4125fc7ba333524c": "Default light", "4125fc7ba333524c": "Default light",
"42655ed8e4108ae2": "Input (non-cached)", "42655ed8e4108ae2": "Input (non-cached)",
@@ -104,7 +109,9 @@
"497c85690c4cc0fc": "No data", "497c85690c4cc0fc": "No data",
"499c729eb09aa2a6": "Context window tokens", "499c729eb09aa2a6": "Context window tokens",
"49be72e6045c007d": "Cancel test", "49be72e6045c007d": "Cancel test",
"4a861200ad513a3c": "Initialize plugin runtime",
"4a8d6841b4023edf": "Confirm import", "4a8d6841b4023edf": "Confirm import",
"4aca6a31090fe2b8": "Initializing…",
"4b458e6e147221d7": "The standard endpoint path is appended automatically for the selected protocol.", "4b458e6e147221d7": "The standard endpoint path is appended automatically for the selected protocol.",
"4d0680f9efaef147": "Unread", "4d0680f9efaef147": "Unread",
"4e30d7c9ed2b0eee": "Not set", "4e30d7c9ed2b0eee": "Not set",
@@ -127,6 +134,7 @@
"5b17f59d33bde39e": "Error: {error}", "5b17f59d33bde39e": "Error: {error}",
"5ba65a74c4e792c5": "By type", "5ba65a74c4e792c5": "By type",
"5c55a67935af8f45": "All", "5c55a67935af8f45": "All",
"5c62e36c152dfc7c": "Plugin runtime initialized",
"5d59857bf039cac9": "Cursor Assistant v{version}", "5d59857bf039cac9": "Cursor Assistant v{version}",
"5f8d556a9c47da3c": "Launch at login disabled", "5f8d556a9c47da3c": "Launch at login disabled",
"5f9acfb945229062": "Are you sure you no longer want to see this ad?", "5f9acfb945229062": "Are you sure you no longer want to see this ad?",
@@ -163,6 +171,7 @@
"7a2229f6a6d330a5": "Open a terminal from the desktop app to install the CA", "7a2229f6a6d330a5": "Open a terminal from the desktop app to install the CA",
"7a3cec4ca715de80": "Call statistics", "7a3cec4ca715de80": "Call statistics",
"7ba2d6728fe2531b": "Confirm clear", "7ba2d6728fe2531b": "Confirm clear",
"7c10d97162c96dbd": "Validating the plugin runtime",
"7cea2f3c46565d29": "OpenAI extra parameters", "7cea2f3c46565d29": "OpenAI extra parameters",
"7d9f043f8f7ab45c": "Version {version} is available in Settings", "7d9f043f8f7ab45c": "Version {version} is available in Settings",
"7e0891860c9e6374": "TAB service address is required", "7e0891860c9e6374": "TAB service address is required",
@@ -170,7 +179,9 @@
"7f3c8312816fe26a": "Refreshing…", "7f3c8312816fe26a": "Refreshing…",
"7f68ebad19ba6bcd": "Check for updates", "7f68ebad19ba6bcd": "Check for updates",
"811a3b22a5a7f2d5": "Unable to connect to the local management service", "811a3b22a5a7f2d5": "Unable to connect to the local management service",
"8213941f12320ce1": "This operating system or CPU architecture is not currently supported",
"83c4efccd9a6bf69": "Connectivity test cancelled: {successful} succeeded, {failed} failed", "83c4efccd9a6bf69": "Connectivity test cancelled: {successful} succeeded, {failed} failed",
"83e8d0b7aff2b394": "Downloaded {downloaded} / {total}",
"83fcfb4c1f2c1641": "Fetch models", "83fcfb4c1f2c1641": "Fetch models",
"842b9f11cdd96bda": "Launch at login", "842b9f11cdd96bda": "Launch at login",
"843ac7e15a5047a7": "Confirm legacy model configuration import", "843ac7e15a5047a7": "Confirm legacy model configuration import",
@@ -178,6 +189,7 @@
"86b7355ec3bd55ef": "Hide API Key", "86b7355ec3bd55ef": "Hide API Key",
"8716e1344b0daddb": "Cursor official", "8716e1344b0daddb": "Cursor official",
"878a8ab176429a86": "View instructions", "878a8ab176429a86": "View instructions",
"8911e4f1407d58cb": "Downloading the plugin runtime",
"89a101b809be7cfc": "This address is used exactly as entered without changing or appending the request path.", "89a101b809be7cfc": "This address is used exactly as entered without changing or appending the request path.",
"8a8542f6964852dc": "Next page", "8a8542f6964852dc": "Next page",
"8b6ff498515bcc2f": "Time", "8b6ff498515bcc2f": "Time",
@@ -192,14 +204,17 @@
"91aaf184cfc17ffd": "Overview", "91aaf184cfc17ffd": "Overview",
"92156a483d4ba248": "Only request, response, and trace attachments are deleted; call summaries, metrics, and configuration are kept.", "92156a483d4ba248": "Only request, response, and trace attachments are deleted; call summaries, metrics, and configuration are kept.",
"940a168911ade998": "Items per page", "940a168911ade998": "Items per page",
"945fb1c67eca8493": "Installing the plugin runtime",
"946b3ffc02f026c0": "Delete this model?", "946b3ffc02f026c0": "Delete this model?",
"966498853d801a52": "TAB connection", "966498853d801a52": "TAB connection",
"9850ed41a5bfbb0c": "{count} selected", "9850ed41a5bfbb0c": "{count} selected",
"997ec8201c2adeda": "Open terminal to install CA", "997ec8201c2adeda": "Open terminal to install CA",
"9b1b7ed518ee401d": "This will open the tutorial in your system browser. Continue?", "9b1b7ed518ee401d": "This will open the tutorial in your system browser. Continue?",
"9c41b3a9e12ac994": "Reasoning effort", "9c41b3a9e12ac994": "Reasoning effort",
"9db205c6055bacc4": "Plugin runtime initialization failed",
"9e356080c56877f8": "Silent start disabled", "9e356080c56877f8": "Silent start disabled",
"9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago", "9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago",
"9ed11266ead88f5b": "Verifying the plugin runtime download",
"9f6fee1aba17a565": "Language", "9f6fee1aba17a565": "Language",
"9fb48101d237ff96": "Last week", "9fb48101d237ff96": "Last week",
"a026f37e613cf48b": "Output Tokens", "a026f37e613cf48b": "Output Tokens",
@@ -220,6 +235,7 @@
"a7617f42f898b2bf": "Use complete request URL", "a7617f42f898b2bf": "Use complete request URL",
"a8036485f9227f2c": "Drag to reorder", "a8036485f9227f2c": "Drag to reorder",
"a98585871c5313ff": "Display name", "a98585871c5313ff": "Display name",
"ab27f80d046f3d7f": "Ready",
"ab9084a640fbb864": "Deselect all", "ab9084a640fbb864": "Deselect all",
"abecab6701177721": "Launch at login enabled", "abecab6701177721": "Launch at login enabled",
"ac58d0f9a3f8d389": "Enter model notes", "ac58d0f9a3f8d389": "Enter model notes",
@@ -230,7 +246,9 @@
"aee88743413144a2": "Refresh", "aee88743413144a2": "Refresh",
"b06325c5660f0c29": "Direct", "b06325c5660f0c29": "Direct",
"b16c3b2ecedd6fe1": "Cursor integration is active. Add a model configuration to use a BYOK model.", "b16c3b2ecedd6fe1": "Cursor integration is active. Add a model configuration to use a BYOK model.",
"b254ff315d861346": "Try initializing again",
"b4411558b932266f": "Provider type", "b4411558b932266f": "Provider type",
"b4c9e08870d41aa2": "Initialize the plugin runtime first",
"b502b1d414664337": "Prompt: {tokens}", "b502b1d414664337": "Prompt: {tokens}",
"b5141d3d19e9a048": "Yes", "b5141d3d19e9a048": "Yes",
"b6725f218ebaef26": "Dock icon shown", "b6725f218ebaef26": "Dock icon shown",
@@ -247,10 +265,13 @@
"bb2b7736433ae867": "Cursor tracing", "bb2b7736433ae867": "Cursor tracing",
"bb7efdcb6af6e805": "Default dark", "bb7efdcb6af6e805": "Default dark",
"bda62ce1d5e4ace9": "Tell us why", "bda62ce1d5e4ace9": "Tell us why",
"bda74b5674b6a57d": "Initialize plugins",
"bf57afd709694b55": "Overview time range", "bf57afd709694b55": "Overview time range",
"bfc01caf9fe0c841": "Cache hit rate {rate}", "bfc01caf9fe0c841": "Cache hit rate {rate}",
"c0b3fbff51ccc40b": "Done",
"c1e98892a77f7a19": "{count} per page", "c1e98892a77f7a19": "{count} per page",
"c3760858cdb6d9f4": "Request body", "c3760858cdb6d9f4": "Request body",
"c54863655e879b36": "Plugin runtime is not supported on this system",
"c7ea2c9bc43134bd": "Edit model", "c7ea2c9bc43134bd": "Edit model",
"c8c14507b2d37395": "Reasoning effort", "c8c14507b2d37395": "Reasoning effort",
"c8df3c14a003bfcd": "Unable to load call details", "c8df3c14a003bfcd": "Unable to load call details",
@@ -258,6 +279,7 @@
"c9b9ae7a61444ab7": "Previous page", "c9b9ae7a61444ab7": "Previous page",
"c9d146d006993cc1": "Cache statistics policy: default ({rate})", "c9d146d006993cc1": "Cache statistics policy: default ({rate})",
"cb2f1709f983d2f4": "Model name", "cb2f1709f983d2f4": "Model name",
"cb99f0138b032687": "Initialization downloads and installs the plugin runtime.",
"cea1aafe9416de7b": "Request headers", "cea1aafe9416de7b": "Request headers",
"cfae1a14d2120c57": "Detailed mode", "cfae1a14d2120c57": "Detailed mode",
"cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.", "cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.",
@@ -271,6 +293,8 @@
"d58c88688e1a949d": "Presets", "d58c88688e1a949d": "Presets",
"d60669bb26a22f5d": "Leave blank to use the default", "d60669bb26a22f5d": "Leave blank to use the default",
"d6b1f203680f5496": "Leave blank to use adaptive thinking", "d6b1f203680f5496": "Leave blank to use adaptive thinking",
"d6e61888b07853ea": "Advanced",
"d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.",
"d86fa42c3848c680": "Use system proxy", "d86fa42c3848c680": "Use system proxy",
"d8c47e9776cf1082": "Main menu", "d8c47e9776cf1082": "Main menu",
"da521d1c1cbd36af": "Authorization is required to install the certificate", "da521d1c1cbd36af": "Authorization is required to install the certificate",
@@ -299,6 +323,7 @@
"e59ae97924d62f01": "First page", "e59ae97924d62f01": "First page",
"e5b9961a0d5242e3": "Port settings saved. Restart the app to apply them.", "e5b9961a0d5242e3": "Port settings saved. Restart the app to apply them.",
"e77e3d58b0dcffaa": "Duration", "e77e3d58b0dcffaa": "Duration",
"e7cef7b834f301e7": "Plugin runtime version",
"e825a2a42c22380e": "Model type", "e825a2a42c22380e": "Model type",
"e828bd3a0151edc2": "The local CA must be trusted by the system", "e828bd3a0151edc2": "The local CA must be trusted by the system",
"e8b1268c1e3610f2": "Existing", "e8b1268c1e3610f2": "Existing",
@@ -313,6 +338,7 @@
"f2bdc88464c51c2e": "Show API Key", "f2bdc88464c51c2e": "Show API Key",
"f4694c46b1e19602": "Final request type", "f4694c46b1e19602": "Final request type",
"f4dcb6a3ceb32247": "Page {page} of {count}", "f4dcb6a3ceb32247": "Page {page} of {count}",
"f4f4c81a4d719711": "Plugin runtime",
"f4fa9f31ea2ae58d": "Token usage calendar for the past year", "f4fa9f31ea2ae58d": "Token usage calendar for the past year",
"f50276449943286c": "End time", "f50276449943286c": "End time",
"f69273dbbebfb3a1": "Format", "f69273dbbebfb3a1": "Format",
@@ -323,8 +349,10 @@
"f9aa11dbb15ce647": "Saturday", "f9aa11dbb15ce647": "Saturday",
"f9b55ca75425161b": "Response content was not recorded. Enable detailed records and try again.", "f9b55ca75425161b": "Response content was not recorded. Enable detailed records and try again.",
"fa5b4b8a751c7d1b": "The local proxy port used by Cursor. Enter 0 to select a random port at startup.", "fa5b4b8a751c7d1b": "The local proxy port used by Cursor. Enter 0 to select a random port at startup.",
"fad86bf65f72c747": "Download progress",
"fb11aa6f29827095": "Checking…", "fb11aa6f29827095": "Checking…",
"fbe8778fa8b9bab5": "Initialize the local CA first", "fbe8778fa8b9bab5": "Initialize the local CA first",
"fc22d1ab9ac73c6f": "Checking the plugin runtime",
"fc3947ebe6b2177b": "Default {defaultRate} / include creation {reuseRate}", "fc3947ebe6b2177b": "Default {defaultRate} / include creation {reuseRate}",
"fcd311fd8ad42462": "Open model list", "fcd311fd8ad42462": "Open model list",
"fd415f8e0097c832": "Cache reads and writes are included in prompt-side statistics.", "fd415f8e0097c832": "Cache reads and writes are included in prompt-side statistics.",
+28
View File
@@ -7,6 +7,7 @@
"051836569928a9f9": "编辑", "051836569928a9f9": "编辑",
"05468af47054d488": "模型 {model} 连通性测试成功({duration} ms)", "05468af47054d488": "模型 {model} 连通性测试成功({duration} ms)",
"0580e0a99a6f1afc": "工件数", "0580e0a99a6f1afc": "工件数",
"06619f339fa0ab46": "正在准备插件运行时",
"076832c1b2de22c3": "缓存写入:{tokens}", "076832c1b2de22c3": "缓存写入:{tokens}",
"07879e064ae16542": "输出推算:{tokens}", "07879e064ae16542": "输出推算:{tokens}",
"07c657ed4747126e": "Anthropic 额外参数", "07c657ed4747126e": "Anthropic 额外参数",
@@ -50,6 +51,7 @@
"22d7895ea5fca72e": "按供应商", "22d7895ea5fca72e": "按供应商",
"23ae7a90b1b9816d": "清理范围", "23ae7a90b1b9816d": "清理范围",
"23e49479e15e6770": "发现新版本 {version}", "23e49479e15e6770": "发现新版本 {version}",
"2400fbd0aeab9e13": "已下载 {downloaded}",
"24a0a24864454575": "已存在,跳过", "24a0a24864454575": "已存在,跳过",
"2555d6c7fbb7e070": "可以直接输入模型标识,也可以读取接口返回的模型列表。", "2555d6c7fbb7e070": "可以直接输入模型标识,也可以读取接口返回的模型列表。",
"29585d7193539200": "当前版本 {version}", "29585d7193539200": "当前版本 {version}",
@@ -68,6 +70,7 @@
"2f7dec3be28d7597": "已选择 {count} 个", "2f7dec3be28d7597": "已选择 {count} 个",
"2f9daa828907b93f": "删除", "2f9daa828907b93f": "删除",
"346ff60e6c7c5181": "读取中…", "346ff60e6c7c5181": "读取中…",
"35fcbd57d58a9394": "插件管理",
"36f33adaf0942634": "确认", "36f33adaf0942634": "确认",
"37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空", "37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
"378bb0eec39fa8a2": "最后一页", "378bb0eec39fa8a2": "最后一页",
@@ -85,9 +88,11 @@
"3cfae5728b92b334": "Token 用量:{tokens}", "3cfae5728b92b334": "Token 用量:{tokens}",
"3d13868593ae4eeb": "界面语言", "3d13868593ae4eeb": "界面语言",
"3da0bf1610ff5db5": "推荐内容", "3da0bf1610ff5db5": "推荐内容",
"3e0b34ddc2121f7d": "运行平台",
"3f6c25aa329163a4": "原接口路径会追加到此服务地址。", "3f6c25aa329163a4": "原接口路径会追加到此服务地址。",
"3fd118e2ffe0b2b6": "取消全部测试", "3fd118e2ffe0b2b6": "取消全部测试",
"3fd47edce45b3603": "关闭", "3fd47edce45b3603": "关闭",
"402495402ce333b1": "重新初始化插件",
"40a08e7cf320ae07": "确定要清理详细记录吗?", "40a08e7cf320ae07": "确定要清理详细记录吗?",
"4125fc7ba333524c": "默认亮色", "4125fc7ba333524c": "默认亮色",
"42655ed8e4108ae2": "输入(非缓存)", "42655ed8e4108ae2": "输入(非缓存)",
@@ -104,7 +109,9 @@
"497c85690c4cc0fc": "暂无数据", "497c85690c4cc0fc": "暂无数据",
"499c729eb09aa2a6": "上下文窗口 Token", "499c729eb09aa2a6": "上下文窗口 Token",
"49be72e6045c007d": "取消测试", "49be72e6045c007d": "取消测试",
"4a861200ad513a3c": "初始化插件运行时",
"4a8d6841b4023edf": "确认导入", "4a8d6841b4023edf": "确认导入",
"4aca6a31090fe2b8": "初始化中…",
"4b458e6e147221d7": "系统会根据请求协议自动追加标准端点路径。", "4b458e6e147221d7": "系统会根据请求协议自动追加标准端点路径。",
"4d0680f9efaef147": "未读", "4d0680f9efaef147": "未读",
"4e30d7c9ed2b0eee": "不设置", "4e30d7c9ed2b0eee": "不设置",
@@ -127,6 +134,7 @@
"5b17f59d33bde39e": "错误:{error}", "5b17f59d33bde39e": "错误:{error}",
"5ba65a74c4e792c5": "按类型", "5ba65a74c4e792c5": "按类型",
"5c55a67935af8f45": "全部", "5c55a67935af8f45": "全部",
"5c62e36c152dfc7c": "插件运行时初始化完成",
"5d59857bf039cac9": "Cursor 助手 v{version}", "5d59857bf039cac9": "Cursor 助手 v{version}",
"5f8d556a9c47da3c": "已关闭开机启动", "5f8d556a9c47da3c": "已关闭开机启动",
"5f9acfb945229062": "你确认不想再看到此广告吗?", "5f9acfb945229062": "你确认不想再看到此广告吗?",
@@ -163,6 +171,7 @@
"7a2229f6a6d330a5": "请在桌面应用中打开终端安装 CA", "7a2229f6a6d330a5": "请在桌面应用中打开终端安装 CA",
"7a3cec4ca715de80": "调用统计", "7a3cec4ca715de80": "调用统计",
"7ba2d6728fe2531b": "确认清理", "7ba2d6728fe2531b": "确认清理",
"7c10d97162c96dbd": "正在验证插件运行时",
"7cea2f3c46565d29": "OpenAI 额外参数", "7cea2f3c46565d29": "OpenAI 额外参数",
"7d9f043f8f7ab45c": "发现新版本 {version},可在设置中安装", "7d9f043f8f7ab45c": "发现新版本 {version},可在设置中安装",
"7e0891860c9e6374": "TAB 服务地址不能为空", "7e0891860c9e6374": "TAB 服务地址不能为空",
@@ -170,7 +179,9 @@
"7f3c8312816fe26a": "刷新中…", "7f3c8312816fe26a": "刷新中…",
"7f68ebad19ba6bcd": "检查更新", "7f68ebad19ba6bcd": "检查更新",
"811a3b22a5a7f2d5": "无法连接本地管理服务", "811a3b22a5a7f2d5": "无法连接本地管理服务",
"8213941f12320ce1": "当前操作系统或 CPU 架构暂不受支持",
"83c4efccd9a6bf69": "连通性测试已取消:成功 {successful},失败 {failed}", "83c4efccd9a6bf69": "连通性测试已取消:成功 {successful},失败 {failed}",
"83e8d0b7aff2b394": "已下载 {downloaded} / {total}",
"83fcfb4c1f2c1641": "获取模型", "83fcfb4c1f2c1641": "获取模型",
"842b9f11cdd96bda": "开机启动", "842b9f11cdd96bda": "开机启动",
"843ac7e15a5047a7": "确认导入旧版模型配置", "843ac7e15a5047a7": "确认导入旧版模型配置",
@@ -178,6 +189,7 @@
"86b7355ec3bd55ef": "隐藏 API Key", "86b7355ec3bd55ef": "隐藏 API Key",
"8716e1344b0daddb": "Cursor 官方", "8716e1344b0daddb": "Cursor 官方",
"878a8ab176429a86": "查看说明", "878a8ab176429a86": "查看说明",
"8911e4f1407d58cb": "正在下载插件运行时",
"89a101b809be7cfc": "系统会原样使用此地址,不追加或修改请求路径。", "89a101b809be7cfc": "系统会原样使用此地址,不追加或修改请求路径。",
"8a8542f6964852dc": "下一页", "8a8542f6964852dc": "下一页",
"8b6ff498515bcc2f": "时间", "8b6ff498515bcc2f": "时间",
@@ -192,14 +204,17 @@
"91aaf184cfc17ffd": "数据概览", "91aaf184cfc17ffd": "数据概览",
"92156a483d4ba248": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。", "92156a483d4ba248": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。",
"940a168911ade998": "每页条数", "940a168911ade998": "每页条数",
"945fb1c67eca8493": "正在安装插件运行时",
"946b3ffc02f026c0": "确定删除这个模型吗?", "946b3ffc02f026c0": "确定删除这个模型吗?",
"966498853d801a52": "TAB 选择", "966498853d801a52": "TAB 选择",
"9850ed41a5bfbb0c": "已选 {count} 项", "9850ed41a5bfbb0c": "已选 {count} 项",
"997ec8201c2adeda": "打开终端安装 CA", "997ec8201c2adeda": "打开终端安装 CA",
"9b1b7ed518ee401d": "将在系统浏览器中打开使用教程,是否继续?", "9b1b7ed518ee401d": "将在系统浏览器中打开使用教程,是否继续?",
"9c41b3a9e12ac994": "思考强度", "9c41b3a9e12ac994": "思考强度",
"9db205c6055bacc4": "插件运行时初始化失败",
"9e356080c56877f8": "已关闭静默启动", "9e356080c56877f8": "已关闭静默启动",
"9e46da6923836182": "如:2026-08-23 09:00、1小时前", "9e46da6923836182": "如:2026-08-23 09:00、1小时前",
"9ed11266ead88f5b": "正在验证插件运行时下载文件",
"9f6fee1aba17a565": "语言", "9f6fee1aba17a565": "语言",
"9fb48101d237ff96": "近一周", "9fb48101d237ff96": "近一周",
"a026f37e613cf48b": "输出 Token", "a026f37e613cf48b": "输出 Token",
@@ -220,6 +235,7 @@
"a7617f42f898b2bf": "使用完整请求地址", "a7617f42f898b2bf": "使用完整请求地址",
"a8036485f9227f2c": "拖动排序", "a8036485f9227f2c": "拖动排序",
"a98585871c5313ff": "显示名称", "a98585871c5313ff": "显示名称",
"ab27f80d046f3d7f": "已就绪",
"ab9084a640fbb864": "全不选", "ab9084a640fbb864": "全不选",
"abecab6701177721": "已开启开机启动", "abecab6701177721": "已开启开机启动",
"ac58d0f9a3f8d389": "请输入模型备注", "ac58d0f9a3f8d389": "请输入模型备注",
@@ -230,7 +246,9 @@
"aee88743413144a2": "刷新", "aee88743413144a2": "刷新",
"b06325c5660f0c29": "直连", "b06325c5660f0c29": "直连",
"b16c3b2ecedd6fe1": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。", "b16c3b2ecedd6fe1": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。",
"b254ff315d861346": "请重试初始化",
"b4411558b932266f": "上游类型", "b4411558b932266f": "上游类型",
"b4c9e08870d41aa2": "需要先初始化插件运行时",
"b502b1d414664337": "提示词:{tokens}", "b502b1d414664337": "提示词:{tokens}",
"b5141d3d19e9a048": "是", "b5141d3d19e9a048": "是",
"b6725f218ebaef26": "已显示 Dock 栏图标", "b6725f218ebaef26": "已显示 Dock 栏图标",
@@ -247,10 +265,13 @@
"bb2b7736433ae867": "Cursor 追踪", "bb2b7736433ae867": "Cursor 追踪",
"bb7efdcb6af6e805": "默认暗色", "bb7efdcb6af6e805": "默认暗色",
"bda62ce1d5e4ace9": "可以告诉我们原因", "bda62ce1d5e4ace9": "可以告诉我们原因",
"bda74b5674b6a57d": "初始化插件",
"bf57afd709694b55": "概览时间范围", "bf57afd709694b55": "概览时间范围",
"bfc01caf9fe0c841": "缓存命中率 {rate}", "bfc01caf9fe0c841": "缓存命中率 {rate}",
"c0b3fbff51ccc40b": "完成",
"c1e98892a77f7a19": "{count} 条/页", "c1e98892a77f7a19": "{count} 条/页",
"c3760858cdb6d9f4": "请求体", "c3760858cdb6d9f4": "请求体",
"c54863655e879b36": "当前系统不支持插件运行时",
"c7ea2c9bc43134bd": "编辑模型", "c7ea2c9bc43134bd": "编辑模型",
"c8c14507b2d37395": "推理强度", "c8c14507b2d37395": "推理强度",
"c8df3c14a003bfcd": "无法加载调用详情", "c8df3c14a003bfcd": "无法加载调用详情",
@@ -258,6 +279,7 @@
"c9b9ae7a61444ab7": "上一页", "c9b9ae7a61444ab7": "上一页",
"c9d146d006993cc1": "缓存统计策略:默认口径({rate})", "c9d146d006993cc1": "缓存统计策略:默认口径({rate})",
"cb2f1709f983d2f4": "模型名称", "cb2f1709f983d2f4": "模型名称",
"cb99f0138b032687": "初始化将下载并安装插件运行时。",
"cea1aafe9416de7b": "请求头", "cea1aafe9416de7b": "请求头",
"cfae1a14d2120c57": "详细模式", "cfae1a14d2120c57": "详细模式",
"cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。", "cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。",
@@ -271,6 +293,8 @@
"d58c88688e1a949d": "常用预设", "d58c88688e1a949d": "常用预设",
"d60669bb26a22f5d": "留空使用默认值", "d60669bb26a22f5d": "留空使用默认值",
"d6b1f203680f5496": "留空使用 adaptive thinking", "d6b1f203680f5496": "留空使用 adaptive thinking",
"d6e61888b07853ea": "高级",
"d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。",
"d86fa42c3848c680": "使用系统代理", "d86fa42c3848c680": "使用系统代理",
"d8c47e9776cf1082": "主菜单", "d8c47e9776cf1082": "主菜单",
"da521d1c1cbd36af": "需要授权安装证书", "da521d1c1cbd36af": "需要授权安装证书",
@@ -299,6 +323,7 @@
"e59ae97924d62f01": "第一页", "e59ae97924d62f01": "第一页",
"e5b9961a0d5242e3": "端口设置已保存,重启软件后生效", "e5b9961a0d5242e3": "端口设置已保存,重启软件后生效",
"e77e3d58b0dcffaa": "耗时", "e77e3d58b0dcffaa": "耗时",
"e7cef7b834f301e7": "插件运行时版本",
"e825a2a42c22380e": "模型类型", "e825a2a42c22380e": "模型类型",
"e828bd3a0151edc2": "需要在系统中信任本地 CA", "e828bd3a0151edc2": "需要在系统中信任本地 CA",
"e8b1268c1e3610f2": "已存在", "e8b1268c1e3610f2": "已存在",
@@ -313,6 +338,7 @@
"f2bdc88464c51c2e": "显示 API Key", "f2bdc88464c51c2e": "显示 API Key",
"f4694c46b1e19602": "最终请求类型", "f4694c46b1e19602": "最终请求类型",
"f4dcb6a3ceb32247": "第 {page} / {count} 页", "f4dcb6a3ceb32247": "第 {page} / {count} 页",
"f4f4c81a4d719711": "插件运行时",
"f4fa9f31ea2ae58d": "过去一年的 Token 用量日历", "f4fa9f31ea2ae58d": "过去一年的 Token 用量日历",
"f50276449943286c": "结束时间", "f50276449943286c": "结束时间",
"f69273dbbebfb3a1": "格式化", "f69273dbbebfb3a1": "格式化",
@@ -323,8 +349,10 @@
"f9aa11dbb15ce647": "周六", "f9aa11dbb15ce647": "周六",
"f9b55ca75425161b": "未记录响应内容,请开启详细记录后重试。", "f9b55ca75425161b": "未记录响应内容,请开启详细记录后重试。",
"fa5b4b8a751c7d1b": "Cursor 使用的本地代理端口;填写 0 时启动时随机选择。", "fa5b4b8a751c7d1b": "Cursor 使用的本地代理端口;填写 0 时启动时随机选择。",
"fad86bf65f72c747": "下载进度",
"fb11aa6f29827095": "检查中…", "fb11aa6f29827095": "检查中…",
"fbe8778fa8b9bab5": "需要先初始化本地 CA", "fbe8778fa8b9bab5": "需要先初始化本地 CA",
"fc22d1ab9ac73c6f": "正在检查插件运行时",
"fc3947ebe6b2177b": "默认 {defaultRate} / 计入创建 {reuseRate}", "fc3947ebe6b2177b": "默认 {defaultRate} / 计入创建 {reuseRate}",
"fcd311fd8ad42462": "打开模型列表", "fcd311fd8ad42462": "打开模型列表",
"fd415f8e0097c832": "缓存读写已计入提示词侧统计。", "fd415f8e0097c832": "缓存读写已计入提示词侧统计。",
+16
View File
@@ -148,6 +148,19 @@ export interface DesktopSettings {
show_dock_icon: boolean; show_dock_icon: boolean;
} }
export type PluginRuntimeState = "uninitialized" | "initializing" | "ready" | "failed" | "unsupported";
export type PluginRuntimePhase = "checking" | "downloading" | "verifying" | "installing" | "validating";
export interface PluginRuntimeStatus {
state: PluginRuntimeState;
version: string;
target: string | null;
phase: PluginRuntimePhase | null;
downloaded_bytes: number;
total_bytes: number | null;
error: string | null;
}
export interface OverviewMetrics { export interface OverviewMetrics {
llm_calls: number; llm_calls: number;
successful_calls: number; successful_calls: number;
@@ -309,6 +322,9 @@ export const api = {
}, },
cursorHarness: () => request<CursorHarnessStatus>("/harness/cursor/status"), cursorHarness: () => request<CursorHarnessStatus>("/harness/cursor/status"),
initializeCursorCa: () => request<CursorHarnessStatus>("/harness/cursor/ca/initialize", { method: "POST" }), initializeCursorCa: () => request<CursorHarnessStatus>("/harness/cursor/ca/initialize", { method: "POST" }),
pluginRuntime: () => request<PluginRuntimeStatus>("/plugins/runtime"),
initializePluginRuntime: () => request<PluginRuntimeStatus>("/plugins/runtime", { method: "POST" }),
cancelPluginRuntimeInitialization: () => request<PluginRuntimeStatus>("/plugins/runtime", { method: "DELETE" }),
openCursorCaInstallTerminal: async (command: string) => { openCursorCaInstallTerminal: async (command: string) => {
if (!packagedDesktop) throw new Error(t("请在桌面应用中打开终端安装 CA")); if (!packagedDesktop) throw new Error(t("请在桌面应用中打开终端安装 CA"));
const { invoke } = await import("@tauri-apps/api/core"); const { invoke } = await import("@tauri-apps/api/core");
+37 -3
View File
@@ -1,5 +1,5 @@
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PortSettings } from "../api"; import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PluginRuntimeStatus, type PortSettings } from "../api";
import { applyTheme, isThemeId, type ThemeId } from "../theme/theme"; import { applyTheme, isThemeId, type ThemeId } from "../theme/theme";
export type AppSnapshot = { export type AppSnapshot = {
@@ -13,6 +13,7 @@ export type AppSnapshot = {
theme: ThemeId; theme: ThemeId;
cursorHarness: CursorHarnessStatus | null; cursorHarness: CursorHarnessStatus | null;
cursorBusy: boolean; cursorBusy: boolean;
pluginRuntime: PluginRuntimeStatus | null;
}; };
const savedTheme = (): ThemeId => { const savedTheme = (): ThemeId => {
@@ -45,6 +46,7 @@ let snapshot: AppSnapshot = {
theme: savedTheme(), theme: savedTheme(),
cursorHarness: null, cursorHarness: null,
cursorBusy: false, cursorBusy: false,
pluginRuntime: null,
}; };
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
@@ -73,15 +75,16 @@ export const appStore = {
async refresh() { async refresh() {
update({ busy: true, error: null }); update({ busy: true, error: null });
try { try {
const [models, calls, overview, settings, ports, cursorHarness] = await Promise.all([ const [models, calls, overview, settings, ports, cursorHarness, pluginRuntime] = await Promise.all([
api.models(), api.models(),
api.calls(), api.calls(),
api.overview(), api.overview(),
api.observability(), api.observability(),
api.ports(), api.ports(),
api.cursorHarness(), api.cursorHarness(),
api.pluginRuntime(),
]); ]);
update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness }); update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness, pluginRuntime });
} catch (cause) { } catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) }); update({ error: cause instanceof Error ? cause.message : String(cause) });
} finally { } finally {
@@ -107,6 +110,37 @@ export const appStore = {
return null; return null;
} finally { update({ cursorBusy: false }); } } finally { update({ cursorBusy: false }); }
}, },
async initializePluginRuntime() {
update({ error: null });
try {
const pluginRuntime = await api.initializePluginRuntime();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async refreshPluginRuntime() {
try {
const pluginRuntime = await api.pluginRuntime();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async cancelPluginRuntimeInitialization() {
try {
const pluginRuntime = await api.cancelPluginRuntimeInitialization();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async setCursorEnabled(enabled: boolean) { async setCursorEnabled(enabled: boolean) {
update({ cursorBusy: true, error: null }); update({ cursorBusy: true, error: null });
try { update({ cursorHarness: await api.setCursorEnabled(enabled) }); } try { update({ cursorHarness: await api.setCursorEnabled(enabled) }); }
+2
View File
@@ -13,6 +13,8 @@ export const flatColorComboChartIcon = icon('<path fill="#00BCD4" d="M37 18h6v24
export const flatColorDataConfigurationIcon = icon('<path fill="#D1C4E9" d="M38 7H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2m0 12H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2m0 12H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2"/><path fill="#607D8B" d="M45.2 38.1c.1-.4.1-.8.1-1.1s0-.8-.1-1.1l2.3-1.7c.2-.2.3-.5.2-.7l-2.3-3.9c-.1-.2-.4-.3-.7-.2l-2.6 1.2c-.6-.5-1.3-.9-2-1.2l-.3-2.9c0-.3-.3-.5-.5-.5h-4.5c-.3 0-.5.2-.5.5l-.3 2.9c-.7.3-1.4.7-2 1.2l-2.6-1.2c-.3-.1-.6 0-.7.2l-2.3 3.9c-.1.2-.1.6.2.7l2.3 1.7c-.1.4-.1.8-.1 1.1s0 .8.1 1.1l-2.3 1.7c-.2.2-.3.5-.2.7l2.3 3.9c.1.2.4.3.7.2l2.6-1.2c.6.5 1.3.9 2 1.2l.3 2.9c0 .3.3.5.5.5h4.5c.3 0 .5-.2.5-.5l.3-2.9c.7-.3 1.4-.7 2-1.2l2.6 1.2c.3.1.6 0 .7-.2l2.3-3.9c.1-.2.1-.6-.2-.7zM37 42.2c-2.9 0-5.2-2.3-5.2-5.2s2.3-5.2 5.2-5.2s5.2 2.3 5.2 5.2s-2.3 5.2-5.2 5.2"/><path fill="#455A64" d="M37 31c-3.3 0-6 2.7-6 6s2.7 6 6 6s6-2.7 6-6s-2.7-6-6-6m0 9c-1.7 0-3-1.3-3-3s1.3-3 3-3s3 1.3 3 3s-1.3 3-3 3"/>', 48, 48); export const flatColorDataConfigurationIcon = icon('<path fill="#D1C4E9" d="M38 7H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2m0 12H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2m0 12H10c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2"/><path fill="#607D8B" d="M45.2 38.1c.1-.4.1-.8.1-1.1s0-.8-.1-1.1l2.3-1.7c.2-.2.3-.5.2-.7l-2.3-3.9c-.1-.2-.4-.3-.7-.2l-2.6 1.2c-.6-.5-1.3-.9-2-1.2l-.3-2.9c0-.3-.3-.5-.5-.5h-4.5c-.3 0-.5.2-.5.5l-.3 2.9c-.7.3-1.4.7-2 1.2l-2.6-1.2c-.3-.1-.6 0-.7.2l-2.3 3.9c-.1.2-.1.6.2.7l2.3 1.7c-.1.4-.1.8-.1 1.1s0 .8.1 1.1l-2.3 1.7c-.2.2-.3.5-.2.7l2.3 3.9c.1.2.4.3.7.2l2.6-1.2c.6.5 1.3.9 2 1.2l.3 2.9c0 .3.3.5.5.5h4.5c.3 0 .5-.2.5-.5l.3-2.9c.7-.3 1.4-.7 2-1.2l2.6 1.2c.3.1.6 0 .7-.2l2.3-3.9c.1-.2.1-.6-.2-.7zM37 42.2c-2.9 0-5.2-2.3-5.2-5.2s2.3-5.2 5.2-5.2s5.2 2.3 5.2 5.2s-2.3 5.2-5.2 5.2"/><path fill="#455A64" d="M37 31c-3.3 0-6 2.7-6 6s2.7 6 6 6s6-2.7 6-6s-2.7-6-6-6m0 9c-1.7 0-3-1.3-3-3s1.3-3 3-3s3 1.3 3 3s-1.3 3-3 3"/>', 48, 48);
export const flatColorSettingsIcon = icon('<path fill="#607D8B" d="M39.6 27.2c.1-.7.2-1.4.2-2.2s-.1-1.5-.2-2.2l4.5-3.2c.4-.3.6-.9.3-1.4L40 10.8c-.3-.5-.8-.7-1.3-.4l-5 2.3c-1.2-.9-2.4-1.6-3.8-2.2L29.4 5c-.1-.5-.5-.9-1-.9h-8.6c-.5 0-1 .4-1 .9l-.5 5.5c-1.4.6-2.7 1.3-3.8 2.2l-5-2.3c-.5-.2-1.1 0-1.3.4l-4.3 7.4c-.3.5-.1 1.1.3 1.4l4.5 3.2c-.1.7-.2 1.4-.2 2.2s.1 1.5.2 2.2L4 30.4c-.4.3-.6.9-.3 1.4L8 39.2c.3.5.8.7 1.3.4l5-2.3c1.2.9 2.4 1.6 3.8 2.2l.5 5.5c.1.5.5.9 1 .9h8.6c.5 0 1-.4 1-.9l.5-5.5c1.4-.6 2.7-1.3 3.8-2.2l5 2.3c.5.2 1.1 0 1.3-.4l4.3-7.4c.3-.5.1-1.1-.3-1.4zM24 35c-5.5 0-10-4.5-10-10s4.5-10 10-10s10 4.5 10 10s-4.5 10-10 10"/><path fill="#455A64" d="M24 13c-6.6 0-12 5.4-12 12s5.4 12 12 12s12-5.4 12-12s-5.4-12-12-12m0 17c-2.8 0-5-2.2-5-5s2.2-5 5-5s5 2.2 5 5s-2.2 5-5 5"/>', 48, 48); export const flatColorSettingsIcon = icon('<path fill="#607D8B" d="M39.6 27.2c.1-.7.2-1.4.2-2.2s-.1-1.5-.2-2.2l4.5-3.2c.4-.3.6-.9.3-1.4L40 10.8c-.3-.5-.8-.7-1.3-.4l-5 2.3c-1.2-.9-2.4-1.6-3.8-2.2L29.4 5c-.1-.5-.5-.9-1-.9h-8.6c-.5 0-1 .4-1 .9l-.5 5.5c-1.4.6-2.7 1.3-3.8 2.2l-5-2.3c-.5-.2-1.1 0-1.3.4l-4.3 7.4c-.3.5-.1 1.1.3 1.4l4.5 3.2c-.1.7-.2 1.4-.2 2.2s.1 1.5.2 2.2L4 30.4c-.4.3-.6.9-.3 1.4L8 39.2c.3.5.8.7 1.3.4l5-2.3c1.2.9 2.4 1.6 3.8 2.2l.5 5.5c.1.5.5.9 1 .9h8.6c.5 0 1-.4 1-.9l.5-5.5c1.4-.6 2.7-1.3 3.8-2.2l5 2.3c.5.2 1.1 0 1.3-.4l4.3-7.4c.3-.5.1-1.1-.3-1.4zM24 35c-5.5 0-10-4.5-10-10s4.5-10 10-10s10 4.5 10 10s-4.5 10-10 10"/><path fill="#455A64" d="M24 13c-6.6 0-12 5.4-12 12s5.4 12 12 12s12-5.4 12-12s-5.4-12-12-12m0 17c-2.8 0-5-2.2-5-5s2.2-5 5-5s5 2.2 5 5s-2.2 5-5 5"/>', 48, 48);
export const flatColorBriefcaseIcon = icon('<path fill="#424242" d="M27 7h-6c-1.7 0-3 1.3-3 3v3h2v-3c0-.6.4-1 1-1h6c.6 0 1 .4 1 1v3h2v-3c0-1.7-1.3-3-3-3"/><path fill="#E65100" d="M40 43H8c-2.2 0-4-1.8-4-4V15c0-2.2 1.8-4 4-4h32c2.2 0 4 1.8 4 4v24c0 2.2-1.8 4-4 4"/><path fill="#FF6E40" d="M40 28H8c-2.2 0-4-1.8-4-4v-9c0-2.2 1.8-4 4-4h32c2.2 0 4 1.8 4 4v9c0 2.2-1.8 4-4 4"/><path fill="#FFF3E0" d="M26 26h-4c-.6 0-1-.4-1-1v-2c0-.6.4-1 1-1h4c.6 0 1 .4 1 1v2c0 .6-.4 1-1 1"/>', 48, 48); // flat-color-icons:briefcase
export const claudeIcon = icon('<path fill="#d97757" d="m50.228 170.321l50.357-28.257l.843-2.463l-.843-1.361h-2.462l-8.426-.518l-28.775-.778l-24.952-1.037l-24.175-1.296l-6.092-1.297L0 125.796l.583-3.759l5.12-3.434l7.324.648l16.202 1.101l24.304 1.685l17.629 1.037l26.118 2.722h4.148l.583-1.685l-1.426-1.037l-1.101-1.037l-25.147-17.045l-27.22-18.017l-14.258-10.37l-7.713-5.25l-3.888-4.925l-1.685-10.758l7-7.713l9.397.649l2.398.648l9.527 7.323l20.35 15.75L94.817 91.9l3.889 3.24l1.555-1.102l.195-.777l-1.75-2.917l-14.453-26.118l-15.425-26.572l-6.87-11.018l-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0l10.63 1.426l4.472 3.888l6.61 15.101l10.694 23.786l16.591 32.34l4.861 9.592l2.592 8.879l.973 2.722h1.685v-1.556l1.36-18.211l2.528-22.36l2.463-28.776l.843-8.1l4.018-9.722l7.971-5.25l6.222 2.981l5.12 7.324l-.713 4.73l-3.046 19.768l-5.962 30.98l-3.889 20.739h2.268l2.593-2.593l10.499-13.934l17.628-22.036l7.778-8.749l9.073-9.657l5.833-4.601h11.018l8.1 12.055l-3.628 12.443l-11.342 14.388l-9.398 12.184l-13.48 18.147l-8.426 14.518l.778 1.166l2.01-.194l30.46-6.481l16.462-2.982l19.637-3.37l8.88 4.148l.971 4.213l-3.5 8.62l-20.998 5.184l-24.628 4.926l-36.682 8.685l-.454.324l.519.648l16.526 1.555l7.065.389h17.304l32.21 2.398l8.426 5.574l5.055 6.805l-.843 5.184l-12.962 6.611l-17.498-4.148l-40.83-9.721l-14-3.5h-1.944v1.167l11.666 11.406l21.387 19.314l26.767 24.887l1.36 6.157l-3.434 4.86l-3.63-.518l-23.526-17.693l-9.073-7.972l-20.545-17.304h-1.36v1.814l4.73 6.935l25.017 37.59l1.296 11.536l-1.814 3.76l-6.481 2.268l-7.13-1.297l-14.647-20.544l-15.1-23.138l-12.185-20.739l-1.49.843l-7.194 77.448l-3.37 3.953l-7.778 2.981l-6.48-4.925l-3.436-7.972l3.435-15.749l4.148-20.544l3.37-16.333l3.046-20.285l1.815-6.74l-.13-.454l-1.49.194l-15.295 20.999l-23.267 31.433l-18.406 19.702l-4.407 1.75l-7.648-3.954l.713-7.064l4.277-6.286l25.47-32.405l15.36-20.092l9.917-11.6l-.065-1.686h-.583L44.07 198.125l-12.055 1.555l-5.185-4.86l.648-7.972l2.463-2.593l20.35-13.999z"/>', 256, 257); export const claudeIcon = icon('<path fill="#d97757" d="m50.228 170.321l50.357-28.257l.843-2.463l-.843-1.361h-2.462l-8.426-.518l-28.775-.778l-24.952-1.037l-24.175-1.296l-6.092-1.297L0 125.796l.583-3.759l5.12-3.434l7.324.648l16.202 1.101l24.304 1.685l17.629 1.037l26.118 2.722h4.148l.583-1.685l-1.426-1.037l-1.101-1.037l-25.147-17.045l-27.22-18.017l-14.258-10.37l-7.713-5.25l-3.888-4.925l-1.685-10.758l7-7.713l9.397.649l2.398.648l9.527 7.323l20.35 15.75L94.817 91.9l3.889 3.24l1.555-1.102l.195-.777l-1.75-2.917l-14.453-26.118l-15.425-26.572l-6.87-11.018l-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0l10.63 1.426l4.472 3.888l6.61 15.101l10.694 23.786l16.591 32.34l4.861 9.592l2.592 8.879l.973 2.722h1.685v-1.556l1.36-18.211l2.528-22.36l2.463-28.776l.843-8.1l4.018-9.722l7.971-5.25l6.222 2.981l5.12 7.324l-.713 4.73l-3.046 19.768l-5.962 30.98l-3.889 20.739h2.268l2.593-2.593l10.499-13.934l17.628-22.036l7.778-8.749l9.073-9.657l5.833-4.601h11.018l8.1 12.055l-3.628 12.443l-11.342 14.388l-9.398 12.184l-13.48 18.147l-8.426 14.518l.778 1.166l2.01-.194l30.46-6.481l16.462-2.982l19.637-3.37l8.88 4.148l.971 4.213l-3.5 8.62l-20.998 5.184l-24.628 4.926l-36.682 8.685l-.454.324l.519.648l16.526 1.555l7.065.389h17.304l32.21 2.398l8.426 5.574l5.055 6.805l-.843 5.184l-12.962 6.611l-17.498-4.148l-40.83-9.721l-14-3.5h-1.944v1.167l11.666 11.406l21.387 19.314l26.767 24.887l1.36 6.157l-3.434 4.86l-3.63-.518l-23.526-17.693l-9.073-7.972l-20.545-17.304h-1.36v1.814l4.73 6.935l25.017 37.59l1.296 11.536l-1.814 3.76l-6.481 2.268l-7.13-1.297l-14.647-20.544l-15.1-23.138l-12.185-20.739l-1.49.843l-7.194 77.448l-3.37 3.953l-7.778 2.981l-6.48-4.925l-3.436-7.972l3.435-15.749l4.148-20.544l3.37-16.333l3.046-20.285l1.815-6.74l-.13-.454l-1.49.194l-15.295 20.999l-23.267 31.433l-18.406 19.702l-4.407 1.75l-7.648-3.954l.713-7.064l4.277-6.286l25.47-32.405l15.36-20.092l9.917-11.6l-.065-1.686h-.583L44.07 198.125l-12.055 1.555l-5.185-4.86l.648-7.972l2.463-2.593l20.35-13.999z"/>', 256, 257);
export const openAiIcon = icon('<path fill="currentColor" d="M20.57 10.18c.45-1.36.3-2.85-.43-4.09a5.03 5.03 0 0 0-5.42-2.42a5.05 5.05 0 0 0-7.14-.38c-.66.59-1.15 1.35-1.43 2.19A4.98 4.98 0 0 0 2.82 7.9c-1.1 1.9-.85 4.29.62 5.91c-.45 1.36-.3 2.85.43 4.09a5.045 5.045 0 0 0 5.43 2.42A5.03 5.03 0 0 0 13.06 22c2.19 0 4.14-1.41 4.81-3.5a4.98 4.98 0 0 0 3.33-2.42a5.04 5.04 0 0 0-.62-5.89Zm-7.52 10.51c-.88 0-1.72-.31-2.4-.87l.12-.07l3.98-2.3c.2-.12.33-.33.33-.57v-5.61l1.68.97s.03.02.03.04v4.65c0 2.07-1.68 3.74-3.75 3.75ZM5 17.25c-.44-.76-.6-1.65-.45-2.51l.12.07l3.99 2.3c.2.12.45.12.65 0l4.87-2.81v1.94s-.01.04-.03.05l-4.03 2.33A3.756 3.756 0 0 1 5 17.25M3.95 8.58a3.7 3.7 0 0 1 1.97-1.64v4.73c0 .23.12.45.32.56l4.85 2.8l-1.68.97h-.06l-4.03-2.32a3.754 3.754 0 0 1-1.37-5.12zm13.83 3.21l-4.86-2.82L14.6 8h.06l4.03 2.33a3.743 3.743 0 0 1 1.37 5.12a3.8 3.8 0 0 1-1.94 1.64v-4.73a.67.67 0 0 0-.34-.56Zm1.68-2.52l-.12-.07l-3.98-2.32a.63.63 0 0 0-.65 0L9.84 9.69V7.75s0-.04.02-.05l4.03-2.32a3.75 3.75 0 0 1 5.12 1.38c.44.76.59 1.64.45 2.51v.02ZM8.93 12.72l-1.68-.97s-.03-.03-.03-.05V7.06c0-2.07 1.68-3.75 3.75-3.74c.87 0 1.72.31 2.39.87l-.12.07l-3.98 2.3c-.2.12-.33.33-.33.57v5.6Zm.91-1.97l2.17-1.25l2.17 1.25v2.5l-2.16 1.25l-2.17-1.25v-2.5Z"/>'); export const openAiIcon = icon('<path fill="currentColor" d="M20.57 10.18c.45-1.36.3-2.85-.43-4.09a5.03 5.03 0 0 0-5.42-2.42a5.05 5.05 0 0 0-7.14-.38c-.66.59-1.15 1.35-1.43 2.19A4.98 4.98 0 0 0 2.82 7.9c-1.1 1.9-.85 4.29.62 5.91c-.45 1.36-.3 2.85.43 4.09a5.045 5.045 0 0 0 5.43 2.42A5.03 5.03 0 0 0 13.06 22c2.19 0 4.14-1.41 4.81-3.5a4.98 4.98 0 0 0 3.33-2.42a5.04 5.04 0 0 0-.62-5.89Zm-7.52 10.51c-.88 0-1.72-.31-2.4-.87l.12-.07l3.98-2.3c.2-.12.33-.33.33-.57v-5.61l1.68.97s.03.02.03.04v4.65c0 2.07-1.68 3.74-3.75 3.75ZM5 17.25c-.44-.76-.6-1.65-.45-2.51l.12.07l3.99 2.3c.2.12.45.12.65 0l4.87-2.81v1.94s-.01.04-.03.05l-4.03 2.33A3.756 3.756 0 0 1 5 17.25M3.95 8.58a3.7 3.7 0 0 1 1.97-1.64v4.73c0 .23.12.45.32.56l4.85 2.8l-1.68.97h-.06l-4.03-2.32a3.754 3.754 0 0 1-1.37-5.12zm13.83 3.21l-4.86-2.82L14.6 8h.06l4.03 2.33a3.743 3.743 0 0 1 1.37 5.12a3.8 3.8 0 0 1-1.94 1.64v-4.73a.67.67 0 0 0-.34-.56Zm1.68-2.52l-.12-.07l-3.98-2.32a.63.63 0 0 0-.65 0L9.84 9.69V7.75s0-.04.02-.05l4.03-2.32a3.75 3.75 0 0 1 5.12 1.38c.44.76.59 1.64.45 2.51v.02ZM8.93 12.72l-1.68-.97s-.03-.03-.03-.05V7.06c0-2.07 1.68-3.75 3.75-3.74c.87 0 1.72.31 2.39.87l-.12.07l-3.98 2.3c-.2.12-.33.33-.33.57v5.6Zm.91-1.97l2.17-1.25l2.17 1.25v2.5l-2.16 1.25l-2.17-1.25v-2.5Z"/>');
// Menu icons intentionally use filled glyphs from different collections so they // Menu icons intentionally use filled glyphs from different collections so they
+4 -2
View File
@@ -13,7 +13,7 @@ import { ConfirmDialog } from "../shared/ui/ConfirmDialog";
import controls from "../shared/ui/Controls.module.scss"; import controls from "../shared/ui/Controls.module.scss";
import { Icon } from "../shared/ui/Icon"; import { Icon } from "../shared/ui/Icon";
import { TooltipTrigger } from "../shared/ui/TooltipTrigger"; import { TooltipTrigger } from "../shared/ui/TooltipTrigger";
import { flatColorAboutIcon, flatColorAreaChartIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../shared/ui/icons"; import { flatColorAboutIcon, flatColorAreaChartIcon, flatColorBriefcaseIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../shared/ui/icons";
import { useMessage } from "../shared/ui/message"; import { useMessage } from "../shared/ui/message";
import { VirtualList } from "../shared/virtual/VirtualList"; import { VirtualList } from "../shared/virtual/VirtualList";
import { useI18n } from "../i18n/store"; import { useI18n } from "../i18n/store";
@@ -27,7 +27,7 @@ type MenuItem =
| { kind: "external"; id: string; label: string; icon: IconifyIcon | string } | { kind: "external"; id: string; label: string; icon: IconifyIcon | string }
| { kind: "group"; label: string }; | { kind: "group"; label: string };
const keptAlivePages = ["/", "/calls", "/settings", "/harness/cursor"]; const keptAlivePages = ["/", "/calls", "/settings", "/harness/cursor", "/plugins"];
const readAdStorageKey = "cursor-byok:read-ad-ids"; const readAdStorageKey = "cursor-byok:read-ad-ids";
const dismissedAdStorageKey = "cursor-byok:dismissed-ad-ids"; const dismissedAdStorageKey = "cursor-byok:dismissed-ad-ids";
const tutorialReadStorageKey = "cursor-byok:tutorial-read"; const tutorialReadStorageKey = "cursor-byok:tutorial-read";
@@ -76,6 +76,8 @@ export function AppLayout() {
{ kind: "page", path: "/harness/cursor", label: t("Cursor 配置"), icon: cursorIconUrl }, { kind: "page", path: "/harness/cursor", label: t("Cursor 配置"), icon: cursorIconUrl },
{ kind: "page", path: "/settings", label: t("系统设置"), icon: flatColorSettingsIcon }, { kind: "page", path: "/settings", label: t("系统设置"), icon: flatColorSettingsIcon },
{ kind: "external", id: "tutorial", label: t("使用教程"), icon: flatColorAboutIcon }, { kind: "external", id: "tutorial", label: t("使用教程"), icon: flatColorAboutIcon },
{ kind: "group", label: t("高级") },
{ kind: "page", path: "/plugins", label: t("插件管理"), icon: flatColorBriefcaseIcon },
]; ];
const openTutorial = useCallback(() => { const openTutorial = useCallback(() => {
+2 -1
View File
@@ -48,7 +48,7 @@ similar = "2"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
thiserror = "2" thiserror = "2"
time = "0.3" time = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "time", "net"] } tokio = { version = "1", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time", "net"] }
tokio-stream = { version = "0.1", features = ["sync"] } tokio-stream = { version = "0.1", features = ["sync"] }
tokio-util = "0.7" tokio-util = "0.7"
tracing = "0.1" tracing = "0.1"
@@ -57,6 +57,7 @@ tower-http = { version = "0.6", features = ["cors", "decompression-gzip", "fs"]
url = "2" url = "2"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
x509-parser = "0.18" x509-parser = "0.18"
zip = { version = "4", default-features = false, features = ["deflate"] }
[build-dependencies] [build-dependencies]
prost-build = "0.13" prost-build = "0.13"
protoc-bin-vendored = "3" protoc-bin-vendored = "3"
+7
View File
@@ -4,6 +4,7 @@ mod calls;
mod harness; mod harness;
mod models; mod models;
mod overview; mod overview;
mod plugins;
mod service; mod service;
mod settings; mod settings;
@@ -136,6 +137,12 @@ pub fn api_router(service: ControlService) -> Router {
) )
.route("/__byok-api__/api/llm-calls", get(calls::list)) .route("/__byok-api__/api/llm-calls", get(calls::list))
.route("/__byok-api__/api/llm-calls/{call_id}", get(calls::detail)) .route("/__byok-api__/api/llm-calls/{call_id}", get(calls::detail))
.route(
"/__byok-api__/api/plugins/runtime",
get(plugins::runtime_status)
.post(plugins::initialize_runtime)
.delete(plugins::cancel_runtime_initialization),
)
.route( .route(
"/__byok-api__/api/settings/observability", "/__byok-api__/api/settings/observability",
get(settings::get).put(settings::update), get(settings::get).put(settings::update),
+24
View File
@@ -0,0 +1,24 @@
//! Exposes plugin runtime initialization and status endpoints.
use axum::{extract::State, Json};
use crate::{plugin::PluginRuntimeStatus, Result};
use super::ControlService;
pub async fn runtime_status(
State(service): State<ControlService>,
) -> Result<Json<PluginRuntimeStatus>> {
Ok(Json(service.plugin_runtime_status()))
}
pub async fn initialize_runtime(
State(service): State<ControlService>,
) -> Result<Json<PluginRuntimeStatus>> {
Ok(Json(service.initialize_plugin_runtime()))
}
pub async fn cancel_runtime_initialization(
State(service): State<ControlService>,
) -> Result<Json<PluginRuntimeStatus>> {
Ok(Json(service.cancel_plugin_runtime_initialization()))
}
+15
View File
@@ -25,6 +25,7 @@ use crate::{
ModelRequest, ModelSpec, ModelType, Overview, ProjectedContent, ProjectedMessage, ModelRequest, ModelSpec, ModelType, Overview, ProjectedContent, ProjectedMessage,
PromptSpec, ProviderType, Role, PromptSpec, ProviderType, Role,
}, },
plugin::{PluginRuntime, PluginRuntimeStatus},
provider::{is_valid_response_event, ModelEvent, Provider}, provider::{is_valid_response_event, ModelEvent, Provider},
store::{ store::{
DesktopSettings, PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage, Store, DesktopSettings, PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage, Store,
@@ -38,6 +39,7 @@ pub struct ControlService {
store: Store, store: Store,
cursor_harness: CursorHarness, cursor_harness: CursorHarness,
provider: Arc<dyn Provider>, provider: Arc<dyn Provider>,
plugin_runtime: PluginRuntime,
model_tests: Arc<Mutex<BTreeMap<String, CancellationToken>>>, model_tests: Arc<Mutex<BTreeMap<String, CancellationToken>>>,
} }
@@ -148,6 +150,7 @@ impl ControlService {
cursor_harness: CursorHarness::new(store.clone())?, cursor_harness: CursorHarness::new(store.clone())?,
store, store,
provider, provider,
plugin_runtime: PluginRuntime::managed()?,
model_tests: Arc::new(Mutex::new(BTreeMap::new())), model_tests: Arc::new(Mutex::new(BTreeMap::new())),
}) })
} }
@@ -156,6 +159,18 @@ impl ControlService {
&self.cursor_harness &self.cursor_harness
} }
pub fn plugin_runtime_status(&self) -> PluginRuntimeStatus {
self.plugin_runtime.status()
}
pub fn initialize_plugin_runtime(&self) -> PluginRuntimeStatus {
self.plugin_runtime.initialize(self.store.clone())
}
pub fn cancel_plugin_runtime_initialization(&self) -> PluginRuntimeStatus {
self.plugin_runtime.cancel_initialization()
}
pub(super) async fn ads( pub(super) async fn ads(
&self, &self,
disabled_ad_ids: Option<&str>, disabled_ad_ids: Option<&str>,
+1
View File
@@ -8,6 +8,7 @@ pub mod error;
pub mod local_app; pub mod local_app;
pub mod model; pub mod model;
pub mod network; pub mod network;
pub mod plugin;
pub mod provider; pub mod provider;
pub mod run; pub mod run;
pub mod search; pub mod search;
+88
View File
@@ -0,0 +1,88 @@
//! Maps supported desktop platforms to pinned Deno release assets.
pub(super) const DENO_VERSION: &str = "2.9.6";
#[derive(Clone, Copy, Debug)]
pub(super) struct RuntimeAsset {
pub target: &'static str,
pub sha256: &'static str,
}
impl RuntimeAsset {
pub fn current() -> Option<Self> {
Self::for_platform(std::env::consts::OS, std::env::consts::ARCH)
}
pub(super) fn for_platform(os: &str, arch: &str) -> Option<Self> {
let (target, sha256) = match (os, arch) {
("macos", "aarch64") => (
"aarch64-apple-darwin",
"213a2f304f04d3c9cb5220669afad138f60a5aab1fe80962abdeb8f35807a472",
),
("macos", "x86_64") => (
"x86_64-apple-darwin",
"7d4524b82bcc557fe020a1a5b56956ed42b992ae5b28026e8ad5d17329533f5f",
),
("windows", "aarch64") => (
"aarch64-pc-windows-msvc",
"acb014afe2299847764e232b4993e162e3946cdeec36603e3f1a0b548cd1ea55",
),
("windows", "x86_64") => (
"x86_64-pc-windows-msvc",
"15e5300b0ba3c3695a7621d90160a746ec9e710228cee639afa9d580f6e3cd11",
),
("linux", "aarch64") => (
"aarch64-unknown-linux-gnu",
"9a46afc6c392c7cd2ff71a31558935545b46408d0e87f7a86908c712721c046e",
),
("linux", "x86_64") => (
"x86_64-unknown-linux-gnu",
"394f07f4da2bebe6ce6f1e7ce0fa16429b29b08c35e3fac3fe25972676dff4b2",
),
_ => return None,
};
Some(Self { target, sha256 })
}
pub fn archive_name(self) -> String {
format!("deno-{}.zip", self.target)
}
pub fn download_url(self) -> String {
format!(
"https://github.com/denoland/deno/releases/download/v{DENO_VERSION}/{}",
self.archive_name()
)
}
pub fn executable_name(self) -> &'static str {
if self.target.contains("windows") {
"deno.exe"
} else {
"deno"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_every_supported_desktop_target() {
let cases = [
("macos", "aarch64", "aarch64-apple-darwin"),
("macos", "x86_64", "x86_64-apple-darwin"),
("windows", "aarch64", "aarch64-pc-windows-msvc"),
("windows", "x86_64", "x86_64-pc-windows-msvc"),
("linux", "aarch64", "aarch64-unknown-linux-gnu"),
("linux", "x86_64", "x86_64-unknown-linux-gnu"),
];
for (os, arch, expected) in cases {
assert_eq!(
RuntimeAsset::for_platform(os, arch).unwrap().target,
expected
);
}
assert!(RuntimeAsset::for_platform("linux", "x86").is_none());
}
}
+268
View File
@@ -0,0 +1,268 @@
//! Downloads, verifies, extracts, and validates a pinned Deno runtime.
use std::{
io,
path::{Path, PathBuf},
process::Stdio,
time::Duration,
};
use futures_util::StreamExt;
use sha2::{Digest, Sha256};
use tokio::io::AsyncWriteExt;
use tokio_util::sync::CancellationToken;
use super::{
asset::{RuntimeAsset, DENO_VERSION},
runtime::PluginRuntimePhase,
};
use crate::{network, store::Store, Error, Result};
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10 * 60);
const VALIDATION_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_ARCHIVE_BYTES: u64 = 128 * 1024 * 1024;
pub(super) async fn install(
root: &Path,
store: &Store,
asset: RuntimeAsset,
cancellation: CancellationToken,
on_progress: impl Fn(PluginRuntimePhase, u64, Option<u64>),
) -> Result<()> {
let paths = RuntimePaths::new(root, asset);
tokio::fs::create_dir_all(&paths.download_dir).await?;
tokio::fs::create_dir_all(&paths.install_dir).await?;
remove_if_exists(&paths.archive).await?;
remove_if_exists(&paths.executable_staging).await?;
remove_if_exists(&paths.ready_marker).await?;
let result = download_and_install(store, asset, &paths, &cancellation, &on_progress).await;
if result.is_err() {
let _ = remove_if_exists(&paths.archive).await;
let _ = remove_if_exists(&paths.executable_staging).await;
let _ = remove_if_exists(&paths.executable).await;
let _ = remove_if_exists(&paths.ready_marker).await;
}
result
}
pub(super) fn runtime_complete(root: &Path, asset: RuntimeAsset) -> bool {
let paths = RuntimePaths::new(root, asset);
paths.executable.is_file() && paths.ready_marker.is_file()
}
async fn download_and_install(
store: &Store,
asset: RuntimeAsset,
paths: &RuntimePaths,
cancellation: &CancellationToken,
on_progress: &impl Fn(PluginRuntimePhase, u64, Option<u64>),
) -> Result<()> {
ensure_not_cancelled(cancellation)?;
let client = network::client(store).await?;
let response = tokio::select! {
_ = cancellation.cancelled() => return Err(Error::Cancelled),
response = client
.get(asset.download_url())
.timeout(DOWNLOAD_TIMEOUT)
.send() => response?,
}
.error_for_status()?;
let total_bytes = response.content_length();
if total_bytes.is_some_and(|size| size > MAX_ARCHIVE_BYTES) {
return Err(Error::Config(
"Deno runtime archive is larger than allowed".into(),
));
}
on_progress(PluginRuntimePhase::Downloading, 0, total_bytes);
let mut archive = tokio::fs::File::create(&paths.archive).await?;
let mut hasher = Sha256::new();
let mut downloaded_bytes = 0_u64;
let mut stream = response.bytes_stream();
loop {
let next = tokio::select! {
_ = cancellation.cancelled() => return Err(Error::Cancelled),
next = stream.next() => next,
};
let Some(chunk) = next else { break };
let chunk = chunk?;
downloaded_bytes = downloaded_bytes.saturating_add(chunk.len() as u64);
if downloaded_bytes > MAX_ARCHIVE_BYTES {
return Err(Error::Config(
"Deno runtime archive is larger than allowed".into(),
));
}
archive.write_all(&chunk).await?;
hasher.update(&chunk);
on_progress(
PluginRuntimePhase::Downloading,
downloaded_bytes,
total_bytes,
);
}
archive.flush().await?;
archive.sync_all().await?;
drop(archive);
ensure_not_cancelled(cancellation)?;
on_progress(PluginRuntimePhase::Verifying, downloaded_bytes, total_bytes);
let actual_hash = hex::encode(hasher.finalize());
if actual_hash != asset.sha256 {
return Err(Error::Config(format!(
"Deno runtime checksum mismatch: expected {}, received {actual_hash}",
asset.sha256
)));
}
on_progress(
PluginRuntimePhase::Installing,
downloaded_bytes,
total_bytes,
);
let archive_path = paths.archive.clone();
let staging_path = paths.executable_staging.clone();
let executable_name = asset.executable_name();
tokio::task::spawn_blocking(move || {
extract_runtime_archive(&archive_path, &staging_path, executable_name)
})
.await
.map_err(|error| Error::Config(format!("Deno extraction task failed: {error}")))??;
ensure_not_cancelled(cancellation)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
tokio::fs::set_permissions(
&paths.executable_staging,
std::fs::Permissions::from_mode(0o700),
)
.await?;
}
remove_if_exists(&paths.executable).await?;
tokio::fs::rename(&paths.executable_staging, &paths.executable).await?;
ensure_not_cancelled(cancellation)?;
on_progress(
PluginRuntimePhase::Validating,
downloaded_bytes,
total_bytes,
);
validate_runtime(&paths.executable, cancellation).await?;
ensure_not_cancelled(cancellation)?;
tokio::fs::write(&paths.ready_marker, format!("deno {DENO_VERSION}\n")).await?;
remove_if_exists(&paths.archive).await?;
tracing::info!(
version = DENO_VERSION,
target = asset.target,
path = %paths.executable.display(),
"plugin runtime initialized"
);
Ok(())
}
struct RuntimePaths {
download_dir: PathBuf,
install_dir: PathBuf,
archive: PathBuf,
executable: PathBuf,
executable_staging: PathBuf,
ready_marker: PathBuf,
}
impl RuntimePaths {
fn new(root: &Path, asset: RuntimeAsset) -> Self {
let download_dir = root.join(".downloads");
let install_dir = root
.join("deno")
.join(format!("v{DENO_VERSION}"))
.join(asset.target);
let executable = install_dir.join(asset.executable_name());
Self {
archive: download_dir.join(format!("{}.part", asset.archive_name())),
executable_staging: install_dir.join(format!("{}.part", asset.executable_name())),
ready_marker: install_dir.join(".ready"),
download_dir,
install_dir,
executable,
}
}
}
fn extract_runtime_archive(archive: &Path, output: &Path, executable_name: &str) -> Result<()> {
let file = std::fs::File::open(archive)?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|error| Error::Config(format!("invalid Deno runtime archive: {error}")))?;
let mut executable = archive
.by_name(executable_name)
.map_err(|error| Error::Config(format!("Deno executable missing from archive: {error}")))?;
let mut destination = std::fs::File::create(output)?;
io::copy(&mut executable, &mut destination)?;
destination.sync_all()?;
Ok(())
}
async fn validate_runtime(executable: &Path, cancellation: &CancellationToken) -> Result<()> {
let mut command = tokio::process::Command::new(executable);
command
.arg("--version")
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true);
let output = tokio::select! {
_ = cancellation.cancelled() => return Err(Error::Cancelled),
result = tokio::time::timeout(VALIDATION_TIMEOUT, command.output()) => {
result.map_err(|_| Error::Config("Deno runtime validation timed out".into()))??
}
};
if !output.status.success() {
return Err(Error::Config(format!(
"Deno runtime validation failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let expected = format!("deno {DENO_VERSION}");
let stdout = String::from_utf8_lossy(&output.stdout);
let version_line = stdout.lines().next().unwrap_or_default().trim();
if version_line != expected && !version_line.starts_with(&format!("{expected} ")) {
return Err(Error::Config(format!(
"unexpected Deno runtime version: {version_line}"
)));
}
Ok(())
}
fn ensure_not_cancelled(cancellation: &CancellationToken) -> Result<()> {
if cancellation.is_cancelled() {
Err(Error::Cancelled)
} else {
Ok(())
}
}
async fn remove_if_exists(path: &Path) -> Result<()> {
match tokio::fs::remove_file(path).await {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uses_versioned_runtime_directory() {
let root = PathBuf::from("/tmp/plugin-runtime");
let asset = super::super::asset::RuntimeAsset::for_platform("macos", "aarch64").unwrap();
let paths = RuntimePaths::new(&root, asset);
assert_eq!(
paths.executable,
root.join("deno")
.join(format!("v{DENO_VERSION}"))
.join(asset.target)
.join("deno")
);
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Owns plugin runtime installation and lifecycle infrastructure.
mod asset;
mod installation;
mod runtime;
pub use runtime::{PluginRuntime, PluginRuntimePhase, PluginRuntimeState, PluginRuntimeStatus};
+221
View File
@@ -0,0 +1,221 @@
//! Tracks Deno runtime readiness and coordinates one initialization task.
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use parking_lot::{Mutex, RwLock};
use serde::Serialize;
use tokio_util::sync::CancellationToken;
use super::{
asset::{RuntimeAsset, DENO_VERSION},
installation,
};
use crate::{config, store::Store, Error, Result};
#[derive(Clone)]
pub struct PluginRuntime {
inner: Arc<PluginRuntimeInner>,
}
struct PluginRuntimeInner {
root: PathBuf,
asset: Option<RuntimeAsset>,
status: RwLock<PluginRuntimeStatus>,
initializing: AtomicBool,
cancellation: Mutex<Option<CancellationToken>>,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PluginRuntimeState {
Uninitialized,
Initializing,
Ready,
Failed,
Unsupported,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PluginRuntimePhase {
Checking,
Downloading,
Verifying,
Installing,
Validating,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct PluginRuntimeStatus {
pub state: PluginRuntimeState,
pub version: String,
pub target: Option<String>,
pub phase: Option<PluginRuntimePhase>,
pub downloaded_bytes: u64,
pub total_bytes: Option<u64>,
pub error: Option<String>,
}
impl PluginRuntimeStatus {
fn uninitialized(asset: RuntimeAsset) -> Self {
Self::new(PluginRuntimeState::Uninitialized, Some(asset))
}
fn ready(asset: RuntimeAsset) -> Self {
Self::new(PluginRuntimeState::Ready, Some(asset))
}
fn unsupported() -> Self {
Self {
state: PluginRuntimeState::Unsupported,
version: DENO_VERSION.into(),
target: None,
phase: None,
downloaded_bytes: 0,
total_bytes: None,
error: Some(format!(
"unsupported platform: {}/{}",
std::env::consts::OS,
std::env::consts::ARCH
)),
}
}
fn new(state: PluginRuntimeState, asset: Option<RuntimeAsset>) -> Self {
Self {
state,
version: DENO_VERSION.into(),
target: asset.map(|value| value.target.into()),
phase: None,
downloaded_bytes: 0,
total_bytes: None,
error: None,
}
}
}
impl PluginRuntime {
pub fn managed() -> Result<Self> {
Self::new(config::managed_data_dir()?.join("plugins").join("runtime"))
}
fn new(root: PathBuf) -> Result<Self> {
std::fs::create_dir_all(&root)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?;
}
let asset = RuntimeAsset::current();
let status = match asset {
Some(asset) if installation::runtime_complete(&root, asset) => {
PluginRuntimeStatus::ready(asset)
}
Some(asset) => PluginRuntimeStatus::uninitialized(asset),
None => PluginRuntimeStatus::unsupported(),
};
Ok(Self {
inner: Arc::new(PluginRuntimeInner {
root,
asset,
status: RwLock::new(status),
initializing: AtomicBool::new(false),
cancellation: Mutex::new(None),
}),
})
}
pub fn status(&self) -> PluginRuntimeStatus {
let mut status = self.inner.status.write();
if status.state == PluginRuntimeState::Ready {
if let Some(asset) = self.inner.asset {
if !installation::runtime_complete(&self.inner.root, asset) {
*status = PluginRuntimeStatus::uninitialized(asset);
}
}
}
status.clone()
}
pub fn initialize(&self, store: Store) -> PluginRuntimeStatus {
let Some(asset) = self.inner.asset else {
return self.status();
};
if installation::runtime_complete(&self.inner.root, asset) {
let ready = PluginRuntimeStatus::ready(asset);
*self.inner.status.write() = ready.clone();
return ready;
}
if self
.inner
.initializing
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return self.status();
}
let mut initializing =
PluginRuntimeStatus::new(PluginRuntimeState::Initializing, Some(asset));
initializing.phase = Some(PluginRuntimePhase::Checking);
*self.inner.status.write() = initializing.clone();
let cancellation = CancellationToken::new();
*self.inner.cancellation.lock() = Some(cancellation.clone());
let runtime = self.clone();
tokio::spawn(async move {
let result = installation::install(
&runtime.inner.root,
&store,
asset,
cancellation,
|phase, downloaded, total| {
runtime.update_progress(phase, downloaded, total);
},
)
.await;
let status = match result {
Ok(()) => PluginRuntimeStatus::ready(asset),
Err(Error::Cancelled) => PluginRuntimeStatus::uninitialized(asset),
Err(error) => {
tracing::error!(%error, target = asset.target, "plugin runtime initialization failed");
let mut failed =
PluginRuntimeStatus::new(PluginRuntimeState::Failed, Some(asset));
failed.error = Some("plugin runtime initialization failed".into());
failed
}
};
*runtime.inner.status.write() = status;
runtime.inner.cancellation.lock().take();
runtime.inner.initializing.store(false, Ordering::Release);
});
initializing
}
pub fn cancel_initialization(&self) -> PluginRuntimeStatus {
if let Some(cancellation) = self.inner.cancellation.lock().as_ref() {
cancellation.cancel();
}
self.status()
}
fn update_progress(
&self,
phase: PluginRuntimePhase,
downloaded_bytes: u64,
total_bytes: Option<u64>,
) {
let mut status = self.inner.status.write();
status.state = PluginRuntimeState::Initializing;
status.phase = Some(phase);
status.downloaded_bytes = downloaded_bytes;
status.total_bytes = total_bytes;
status.error = None;
}
}