-
+
{draft.model.type === "openai" &&
;
- if (!state) return
{t("未测试")}
;
- if (state.status === "cancelled") return
{t("测试已取消")}
;
+export function CursorModelTestResult({ state, testing = false, compact = false }: {
+ state?: CursorModelTestState;
+ testing?: boolean;
+ /** 列表行内的紧凑徽标形态:未测试时不渲染,详情放入悬浮提示。 */
+ compact?: boolean;
+}) {
+ if (testing) {
+ return compact
+ ?
{t("测试中…")}
+ :
{t("测试中…")}
;
+ }
+ if (!state) {
+ return compact ? null :
{t("未测试")}
;
+ }
+ if (state.status === "cancelled") {
+ return compact
+ ?
{t("测试已取消")}
+ :
{t("测试已取消")}
;
+ }
const success = state.status === "success";
const summary = success
@@ -29,6 +44,15 @@ export function CursorModelTestResult({ state, testing = false }: { state?: Curs
})
: t("测试失败:{error}", { error: state.error });
+ if (compact) {
+ return
+
+ {success ? `${formatSpeed(state.result.tokens_per_second)} tokens/s` : t("测试失败")}
+
+
+ ;
+ }
+
return
{summary}
diff --git a/apps/desktop/src/features/models/CursorSettings.module.scss b/apps/desktop/src/features/models/CursorSettings.module.scss
index 7dbe326..a96a8d3 100644
--- a/apps/desktop/src/features/models/CursorSettings.module.scss
+++ b/apps/desktop/src/features/models/CursorSettings.module.scss
@@ -70,19 +70,61 @@
gap: 20px;
padding-top: 10px;
}
-.modelGroup {
- display: flex;
- flex-direction: column;
- gap: 10px;
+.groupCard {
+ padding: 4px 12px 8px;
}
-.modelGroupHeader {
+.groupToggle {
+ width: 100%;
display: flex;
align-items: center;
- gap: 6px;
+ gap: 8px;
+ padding: 8px 2px;
color: var(--vscode-foreground);
+ background: none;
+ border: none;
font-size: type.$font-size-base;
- opacity: 0.6;
- padding-left: 6px;
+ cursor: pointer;
+}
+.groupLabel {
+ flex: 1;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.modelList {
+ display: flex;
+ flex-direction: column;
+}
+.modelRow {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px 12px;
+ padding: 10px 2px;
+ border-top: 1px solid var(--vscode-sideBar-border);
+}
+.modelRowName {
+ min-width: 0;
+ flex: 1 1 200px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ .modelRowNameText,
+ .modelRowModelId {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .modelRowNameText {
+ color: var(--vscode-foreground);
+ font-size: type.$font-size-base;
+ }
+ .modelRowModelId {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-2xs;
+ }
}
.modelCard {
position: relative;
@@ -141,9 +183,15 @@
}
.modelCardActions {
display: flex;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
justify-content: flex-end;
gap: 8px;
+
+ // 空间不足时按钮收缩显示省略号,而不是换行。
+ > button {
+ min-width: 0;
+ flex: 0 1 auto;
+ }
}
.deleteButton:hover {
color: var(--vscode-errorForeground, #f48771);
diff --git a/apps/desktop/src/features/models/CursorSettingsPage.tsx b/apps/desktop/src/features/models/CursorSettingsPage.tsx
index 62aca4f..33deb24 100644
--- a/apps/desktop/src/features/models/CursorSettingsPage.tsx
+++ b/apps/desktop/src/features/models/CursorSettingsPage.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { api, type Model, type ModelInput } from "../../shared/api";
+import { useNavigate } from "react-router-dom";
+import { api, configuredPluginModels, type Model, type ModelInput } from "../../shared/api";
import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "./CursorGates";
import { CursorModelCards, cursorModelGroups, type CursorModelGrouping } from "./CursorModelCards";
import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "./CursorModelEditor";
@@ -18,7 +19,8 @@ import { PageActions } from "../../shell/PageActions";
import { appStore, useAppStore } from "../../shared/store/appStore";
export function CursorSettingsPage() {
- const { models, cursorHarness, cursorBusy } = useAppStore();
+ const { models, cursorHarness, cursorBusy, plugins } = useAppStore();
+ const navigate = useNavigate();
const message = useMessage();
const [draft, setDraft] = useState
(null);
const [editing, setEditing] = useState(null);
@@ -34,6 +36,11 @@ export function CursorSettingsPage() {
const [grouping, setGrouping] = useState("flat");
const activeModelTests = useRef(new Map());
const caReady = cursorHarness?.ca === "ready";
+ const pluginModels = configuredPluginModels(plugins);
+ const testTargets = [
+ ...models.map((model) => ({ model_hash: model.model_hash, display_name: model.display_name })),
+ ...pluginModels.map((model) => ({ model_hash: model.id, display_name: model.displayName })),
+ ];
const providerGroups = cursorModelGroups(models, "provider");
const typeGroups = cursorModelGroups(models, "type");
const canGroupByProvider = providerGroups.length > 1;
@@ -64,6 +71,7 @@ export function CursorSettingsPage() {
setEditing(model);
setModelOptions([model.model_id]);
setDraft({
+ providerId: `builtin/${model.type}`,
model: modelInput(model),
openAIExtraParamsText: JSON.stringify(model.openai_extra_params, null, 2),
customHeadersText: JSON.stringify(model.custom_headers, null, 2),
@@ -120,7 +128,7 @@ export function CursorSettingsPage() {
const cancelAllModelTests = async () => {
await Promise.all([...activeModelTests.current.keys()].map((modelHash) => cancelModelTest(modelHash)));
};
- const testModel = async (model: Model, notify = true): Promise<"success" | "failure" | "cancelled"> => {
+ const testModel = async (model: { model_hash: string; display_name: string }, notify = true): Promise<"success" | "failure" | "cancelled"> => {
if (activeModelTests.current.has(model.model_hash)) {
await cancelModelTest(model.model_hash);
return "cancelled";
@@ -167,17 +175,17 @@ export function CursorSettingsPage() {
await appStore.refresh();
};
const testAllModels = async () => {
- if (!models.length || batchTesting) return;
+ if (!testTargets.length || batchTesting) return;
setBatchTesting(true);
try {
- const results = await Promise.all(models.map((model) => testModel(model, false)));
+ const results = await Promise.all(testTargets.map((model) => testModel(model, false)));
const successful = results.filter((result) => result === "success").length;
const failed = results.filter((result) => result === "failure").length;
const cancelled = results.filter((result) => result === "cancelled").length;
message(cancelled > 0
? t("连通性测试已取消:成功 {successful},失败 {failed}", { successful, failed })
: failed === 0
- ? t("全部 {count} 个模型连通性测试成功", { count: models.length })
+ ? t("全部 {count} 个模型连通性测试成功", { count: testTargets.length })
: t("连通性测试完成:成功 {successful},失败 {failed}", { successful, failed }),
{ duration: failed === 0 && cancelled === 0 ? 2400 : 5000 });
} finally {
@@ -208,6 +216,7 @@ export function CursorSettingsPage() {
const list = void duplicateModel(model)}
onDelete={setDeleting}
+ onTestPluginModel={(model) => void testModel({ model_hash: model.id, display_name: model.displayName })}
+ onPluginSettings={() => navigate("/plugins")}
onReorder={reorderModels}
/>;
@@ -239,12 +250,13 @@ export function CursorSettingsPage() {
const editorTestState = editing ? modelTestResults.get(editing.model_hash) : undefined;
const editorTesting = Boolean(editing && testingModelHashes.has(editing.model_hash));
const activeGroups = grouping === "provider" ? providerGroups : typeGroups;
+ const pluginSectionHeight = pluginModels.length > 0 ? 60 + pluginModels.length * 56 : 0;
const estimatedModelHeight = grouping === "flat"
- ? Math.max(380, Math.ceil(models.length / 3) * 196)
- : Math.max(380, activeGroups.reduce((height, group) => height + Math.ceil(group.models.length / 3) * 196 + 34, 0) + Math.max(0, activeGroups.length - 1) * 20);
+ ? Math.max(380, Math.ceil(models.length / 3) * 196 + pluginSectionHeight)
+ : Math.max(380, activeGroups.reduce((height, group) => height + 60 + group.models.length * 56, 0) + Math.max(0, activeGroups.length - 1) * 20 + pluginSectionHeight);
return <>
- {models.length > 0 &&
+ {testTargets.length > 0 &&
{canGroupByProvider && }
@@ -253,8 +265,8 @@ export function CursorSettingsPage() {
}
-
- : undefined} busy={cursorBusy || savingAndTesting} onClose={() => { if (editing && editorTesting) void cancelModelTest(editing.model_hash); setDraft(null); setEditing(null); }} onSubmit={() => void save()} secondaryAction={}>
+
+ : undefined} busy={cursorBusy || savingAndTesting} onClose={() => { if (editing && editorTesting) void cancelModelTest(editing.model_hash); setDraft(null); setEditing(null); }} onSubmit={() => void save()} submitLabel={t("保存")} secondaryAction={}>
{draft && <>
void discover()} />
>}
diff --git a/apps/desktop/src/features/plugins/PluginManagementPage.module.scss b/apps/desktop/src/features/plugins/PluginManagementPage.module.scss
new file mode 100644
index 0000000..10a3e0b
--- /dev/null
+++ b/apps/desktop/src/features/plugins/PluginManagementPage.module.scss
@@ -0,0 +1,195 @@
+@use "../../styles/typography" as type;
+
+.pluginGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 12px;
+ padding-top: 10px;
+}
+
+.pluginCard {
+ min-width: 0;
+ min-height: 150px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 18px;
+}
+
+.pluginCardTop {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.pluginIcon {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 42px;
+ height: 42px;
+
+ border-radius: 10px;
+
+}
+
+.pluginIdentity {
+ min-width: 0;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.pluginName,
+.pluginId {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.pluginName {
+ color: var(--vscode-foreground);
+ font-size: type.$font-size-base;
+ font-weight: 600;
+}
+
+.pluginId {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-2xs;
+}
+
+.stateBadge {
+ flex: 0 0 auto;
+ align-self: flex-start;
+ padding: 3px 8px;
+ color: var(--vscode-descriptionForeground);
+ border: 1px solid var(--vscode-sideBar-border);
+ border-radius: 999px;
+ font-size: type.$font-size-2xs;
+ white-space: nowrap;
+}
+
+.stateReady {
+ color: var(--vscode-testing-iconPassed, #73c991);
+ background: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 10%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 28%, transparent);
+}
+
+.pluginMeta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-2xs;
+}
+
+.pluginAuthor {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ opacity: 0.85;
+}
+
+.cardActions {
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ gap: 7px;
+
+ > button {
+ min-width: 0;
+ flex: 0 1 auto;
+ }
+}
+
+
+// 主操作靠左,"更多"推到行尾,两端对齐。
+.moreAction {
+ display: flex;
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.gate,
+.empty {
+ 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;
+ }
+}
+
+.gate button {
+ margin-top: 6px;
+}
+
+.progressBar {
+ width: 100%;
+ height: 8px;
+ overflow: hidden;
+ background: color-mix(in srgb, var(--vscode-foreground) 10%, transparent);
+ border-radius: 999px;
+}
+
+// 反向斜纹(-45°)+ 无限反向滚动;未知总量时以 100% 宽度作不确定态。
+.progressFill {
+ height: 100%;
+ background-color: var(--vscode-progressBar-background, #0e70c0);
+ background-image: linear-gradient(
+ -45deg,
+ rgb(255 255 255 / 24%) 25%,
+ transparent 25% 50%,
+ rgb(255 255 255 / 24%) 50% 75%,
+ transparent 75%
+ );
+ background-size: 24px 24px;
+ border-radius: 999px;
+ transition: width 160ms ease;
+ animation: progress-stripes 0.7s linear infinite;
+}
+
+@keyframes progress-stripes {
+ to {
+ background-position: -24px 0;
+ }
+}
+
+.progressContent {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ strong {
+ font-size: type.$font-size-base;
+ }
+
+ span {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+
+ .error {
+ color: var(--vscode-errorForeground);
+ overflow-wrap: anywhere;
+ }
+}
diff --git a/apps/desktop/src/features/plugins/PluginManagementPage.tsx b/apps/desktop/src/features/plugins/PluginManagementPage.tsx
new file mode 100644
index 0000000..a1ca992
--- /dev/null
+++ b/apps/desktop/src/features/plugins/PluginManagementPage.tsx
@@ -0,0 +1,324 @@
+import { useEffect, useRef, useState } from "react";
+import { api, pluginText, type PluginDescriptor, type PluginImportFile, type PluginRuntimePhase, type PluginRuntimeStatus } from "../../shared/api";
+import { useI18n } from "../../i18n/store";
+import { PageContent } from "../../shell/layout/PageContent";
+import { appStore, useAppStore } from "../../shared/store/appStore";
+import { ActionMenu } from "../../shared/ui/ActionMenu";
+import { Button } from "../../shared/ui/Button";
+import { Card } from "../../shared/ui/Card";
+import { Modal } from "../../shared/ui/Modal";
+import { useMessage } from "../../shared/ui/message";
+import { TruncatedButton } from "../../shared/ui/TruncatedButton";
+import { PluginAddPanel, PluginSettingsPanel } from "./PluginResourcePanels";
+import styles from "./PluginManagementPage.module.scss";
+
+export function PluginManagementPage() {
+ const { pluginRuntime, plugins } = useAppStore();
+ const [progressOpen, setProgressOpen] = useState(false);
+ const [starting, setStarting] = useState(false);
+ const [selected, setSelected] = useState<{ pluginId: string; mode: "add" | "settings" } | null>(null);
+ const cancelRequested = useRef(false);
+ const selectedPlugin = selected ? plugins.find((plugin) => plugin.id === selected.pluginId) ?? null : null;
+
+ 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"
+ ? setSelected({ pluginId, mode })} />
+ : void initialize()} />;
+ const estimatedHeight = plugins.length > 0
+ ? Math.max(320, Math.ceil(plugins.length / 3) * 180)
+ : 320;
+
+ return <>
+
+
+ setSelected(null)}
+ >
+ {selected?.mode === "add" && selectedPlugin && setSelected(null)} />}
+ {selected?.mode === "settings" && selectedPlugin && }
+
+ >;
+}
+
+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
+ {title}
+ {description}
+ {!unsupported && }
+
;
+}
+
+function PluginCards({ plugins, onOpen }: {
+ plugins: PluginDescriptor[];
+ onOpen: (pluginId: string, mode: "add" | "settings") => void;
+}) {
+ if (plugins.length === 0) {
+ return
+ {t("还没有安装插件")}
+ {t("安装插件后会显示在这里。")}
+
;
+ }
+ return
+ {plugins.map((plugin) =>
)}
+
;
+}
+
+function PluginCard({ plugin, onOpen }: {
+ plugin: PluginDescriptor;
+ onOpen: (pluginId: string, mode: "add" | "settings") => void;
+}) {
+ const { locale } = useI18n();
+ const { ports } = useAppStore();
+ const message = useMessage();
+ const importInput = useRef(null);
+ const [importing, setImporting] = useState(false);
+ const configured = plugin.providers.some((provider) => provider.configured);
+ const accountCount = plugin.resources.reduce((count, resource) => count + resource.resources.length, 0);
+ const modelCount = plugin.providers.reduce((count, provider) => count + provider.models.length, 0);
+ const subtitle = plugin.providers.map((provider) => pluginText(provider.displayName, locale)).join(" · ") || plugin.id;
+ const importResource = plugin.resources.find((resource) => resource.import);
+ const exportResource = plugin.resources.find((resource) => resource.resources.length > 0);
+
+ const importFiles = async (files: FileList | null) => {
+ if (!files?.length || !importResource) return;
+ setImporting(true);
+ try {
+ const entries: PluginImportFile[] = await Promise.all(
+ [...files].map(async (file) => ({ name: file.name, content: await file.text() })),
+ );
+ const result = await api.importPluginResources(plugin.id, importResource.type, entries);
+ await appStore.refreshPlugins();
+ const summary = t("导入完成:新增 {added},更新 {updated}", { added: result.added, updated: result.updated });
+ if (result.modelSyncError) {
+ message(t("账号已保存,但同步模型失败:{error}", { error: result.modelSyncError }), { duration: 5000 });
+ } else if (result.warnings.length > 0) {
+ message(`${summary} · ${result.warnings.join("; ")}`, { duration: 5000 });
+ } else {
+ message(summary);
+ }
+ } catch (cause) {
+ message(cause instanceof Error ? cause.message : String(cause), { duration: 5000 });
+ } finally {
+ setImporting(false);
+ if (importInput.current) importInput.current.value = "";
+ }
+ };
+
+ return (
+
+
+

+
+ {plugin.name}
+ {subtitle}
+
+
+ {configured ? t("已配置") : t("未配置")}
+
+
+
+
+ {t("{accounts} 个账号 · {models} 个模型", {
+ accounts: accountCount,
+ models: modelCount,
+ })}
+
+
+ {[`v${plugin.version}`, plugin.author].filter(Boolean).join(" · ")}
+
+
+
+
onOpen(plugin.id, "add")}
+ />
+ {configured && (
+ onOpen(plugin.id, "settings")}
+ />
+ )}
+ {(importResource || exportResource) && (
+
+ importInput.current?.click(),
+ },
+ ]
+ : []),
+ ...(exportResource
+ ? [
+ {
+ id: "export",
+ label: t("批量导出"),
+ onSelect: () =>
+ void api.openExternalUrl(
+ api.pluginResourceExportUrl(
+ ports.service_port,
+ plugin.id,
+ exportResource.type,
+ ),
+ ),
+ },
+ ]
+ : []),
+ ]}
+ />
+
+ )}
+ {importResource && (
+ void importFiles(event.target.files)}
+ />
+ )}
+
+
+ );
+}
+
+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
+
+
{stage}
+ {status?.phase === "downloading" && <>
+
+
+ {total ? t("已下载 {downloaded} / {total}", { downloaded: formatBytes(downloaded), total: formatBytes(total) }) : t("已下载 {downloaded}", { downloaded: formatBytes(downloaded) })}
+
+ >}
+ {status?.state === "failed" &&
{t("请重试初始化")}}
+ {status?.state === "ready" &&
{t("插件运行时 {version} 已安装,可以开始使用插件。", { version: status.version })}}
+
+ ;
+}
+
+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]}`;
+}
diff --git a/apps/desktop/src/features/plugins/PluginResourcePanels.module.scss b/apps/desktop/src/features/plugins/PluginResourcePanels.module.scss
new file mode 100644
index 0000000..23d1fb2
--- /dev/null
+++ b/apps/desktop/src/features/plugins/PluginResourcePanels.module.scss
@@ -0,0 +1,156 @@
+@use "../../styles/typography" as type;
+
+.panel {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.methodCard {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 16px;
+
+ > span {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.actions,
+.toolbar,
+.pagination {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.deviceCode {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ small {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+
+ button {
+ padding: 6px 10px;
+ color: var(--vscode-foreground);
+ background: var(--vscode-textCodeBlock-background);
+ border: 1px solid var(--vscode-sideBar-border);
+ border-radius: 4px;
+ font-family: var(--vscode-editor-font-family);
+ letter-spacing: 0.08em;
+ cursor: pointer;
+ }
+
+ button.copy {
+ padding: 6px 2px;
+ color: var(--vscode-textLink-foreground);
+ background: none;
+ border: none;
+ font-family: inherit;
+ letter-spacing: normal;
+ font-size: type.$font-size-xs;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
+
+.fileButton {
+ align-self: flex-start;
+ padding: 5px 10px;
+ color: var(--vscode-button-foreground);
+ background: var(--vscode-button-background);
+ border-radius: 4px;
+ font-size: type.$font-size-xs;
+ cursor: pointer;
+
+ input {
+ display: none;
+ }
+}
+
+.toolbar {
+ flex-wrap: wrap;
+
+ input {
+ min-width: 180px;
+ flex: 1 1 220px;
+ }
+}
+
+.resourceSection {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.resourceList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.providerRow,
+.resourceRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px;
+
+ > div:first-child {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ }
+
+ span {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.ready {
+ color: var(--vscode-testing-iconPassed, #73c991) !important;
+}
+
+.cooling {
+ color: var(--vscode-editorWarning-foreground, #cca700) !important;
+}
+
+.invalid {
+ color: var(--vscode-errorForeground, #f48771) !important;
+}
+
+.success {
+ color: var(--vscode-testing-iconPassed, #73c991);
+ font-size: type.$font-size-xs;
+}
+
+.empty {
+ padding: 24px;
+ color: var(--vscode-descriptionForeground);
+ text-align: center;
+}
+
+.pagination {
+ justify-content: center;
+
+ span {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.error {
+ color: var(--vscode-errorForeground, #f48771);
+ font-size: type.$font-size-xs;
+}
diff --git a/apps/desktop/src/features/plugins/PluginResourcePanels.tsx b/apps/desktop/src/features/plugins/PluginResourcePanels.tsx
new file mode 100644
index 0000000..2c4f902
--- /dev/null
+++ b/apps/desktop/src/features/plugins/PluginResourcePanels.tsx
@@ -0,0 +1,288 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import {
+ api,
+ pluginText,
+ type PluginAddMethod,
+ type PluginDescriptor,
+ type PluginOAuthBegin,
+ type PluginProviderDescriptor,
+ type PluginResourceDescriptor,
+ type PluginResourceView,
+} from "../../shared/api";
+import { useI18n } from "../../i18n/store";
+import { appStore } from "../../shared/store/appStore";
+import { Button } from "../../shared/ui/Button";
+import { Card } from "../../shared/ui/Card";
+import { FormField, TextInput } from "../../shared/ui/FormControls";
+import styles from "./PluginResourcePanels.module.scss";
+
+const PAGE_SIZE = 10;
+
+export function PluginAddPanel({ plugin, onConfigured }: { plugin: PluginDescriptor; onConfigured: () => void }) {
+ return
+ {plugin.resources.map((resource) => )}
+ {plugin.resources.length === 0 && {t("该插件不需要添加资源")}}
+
;
+}
+
+function ResourceAddSection({ plugin, resource, onConfigured }: {
+ plugin: PluginDescriptor;
+ resource: PluginResourceDescriptor;
+ onConfigured: () => void;
+}) {
+ return <>
+ {resource.add.map((method) => )}
+ >;
+}
+
+function OAuthMethodCard({ pluginId, resourceType, method, onConfigured }: {
+ pluginId: string;
+ resourceType: string;
+ method: PluginAddMethod;
+ onConfigured: () => void;
+}) {
+ const { locale } = useI18n();
+ const [status, setStatus] = useState<"idle" | "starting" | "polling" | "success" | "error">("idle");
+ const [begun, setBegun] = useState(null);
+ const [error, setError] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const stopped = useRef(false);
+
+ const copyCode = async (code: string) => {
+ await api.copyCursorText(code).catch(() => undefined);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 2000);
+ };
+
+ useEffect(() => () => { stopped.current = true; }, []);
+
+ useEffect(() => {
+ if (!begun || status !== "polling") return;
+ let timer = 0;
+ const poll = async (intervalMs: number) => {
+ if (stopped.current) return;
+ try {
+ const result = await api.pluginOAuthPoll(begun.sessionId);
+ if (stopped.current) return;
+ if (result.status === "pending") {
+ timer = window.setTimeout(() => void poll(result.pollIntervalMs), Math.max(1000, result.pollIntervalMs));
+ return;
+ }
+ if (result.status === "completed") {
+ await appStore.refreshPlugins();
+ if (result.modelSyncError) {
+ setStatus("error");
+ setError(t("账号已保存,但同步模型失败:{error}", { error: result.modelSyncError }));
+ return;
+ }
+ setStatus("success");
+ onConfigured();
+ return;
+ }
+ setStatus("error");
+ setError(result.message || t("授权被拒绝或已失败。"));
+ } catch (cause) {
+ if (stopped.current) return;
+ setError(errorText(cause));
+ timer = window.setTimeout(() => void poll(intervalMs), Math.max(1000, intervalMs));
+ }
+ };
+ timer = window.setTimeout(() => void poll(begun.pollIntervalMs), Math.max(1000, begun.pollIntervalMs));
+ return () => window.clearTimeout(timer);
+ }, [begun, onConfigured, status]);
+
+ const start = async () => {
+ setStatus("starting");
+ setError(null);
+ try {
+ const next = await api.pluginOAuthBegin(pluginId, resourceType, method.id);
+ setBegun(next);
+ setStatus("polling");
+ await api.copyCursorText(next.userCode).catch(() => undefined);
+ await api.openExternalUrl(next.verificationUrlComplete || next.verificationUrl);
+ } catch (cause) {
+ setStatus("error");
+ setError(errorText(cause));
+ }
+ };
+
+ return
+ {pluginText(method.displayName, locale)}
+ {method.description && {pluginText(method.description, locale)}}
+ {begun && status === "polling" &&
+ {t("设备验证码")}
+
+
+
}
+
+
+ {begun && status === "polling" && }
+
+ {status === "success" && {t("账号已保存,模型目录已同步。")}}
+ {error && {error}}
+ ;
+}
+
+export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) {
+ const [busy, setBusy] = useState(null);
+ const [error, setError] = useState(null);
+
+ const run = async (key: string, task: () => Promise) => {
+ setBusy(key);
+ setError(null);
+ try {
+ await task();
+ await appStore.refreshPlugins();
+ } catch (cause) {
+ setError(errorText(cause));
+ } finally {
+ setBusy(null);
+ }
+ };
+
+ return
+ {plugin.providers.map((provider) =>
void run(`sync:${provider.id}`, async () => {
+ await api.syncPluginModels(plugin.id, provider.id);
+ })}
+ />)}
+ {plugin.resources.map((resource) => void run(`refresh:${item.id}`, async () => {
+ await api.refreshPluginResource(plugin.id, resource.type, item.id);
+ })}
+ onDelete={(item) => void run(`delete:${item.id}`, async () => {
+ await api.deletePluginResource(plugin.id, resource.type, item.id);
+ })}
+ />)}
+ {error && {error}}
+ ;
+}
+
+function ProviderRow({ provider, busy, syncing, onSync }: {
+ provider: PluginProviderDescriptor;
+ busy: boolean;
+ syncing: boolean;
+ onSync: () => void;
+}) {
+ const { locale } = useI18n();
+ return
+
+ {pluginText(provider.displayName, locale)}
+
+ {provider.providerType}
+ {" · "}
+ {provider.models.length > 0 ? t("{count} 个模型", { count: provider.models.length }) : t("尚未同步模型")}
+ {" · "}
+ {provider.configured ? t("可调用") : t("未就绪")}
+
+
+ {provider.hasModels && }
+ ;
+}
+
+function ResourceList({ resource, busy, onRefresh, onDelete }: {
+ resource: PluginResourceDescriptor;
+ busy: boolean;
+ onRefresh: (item: PluginResourceView) => void;
+ onDelete: (item: PluginResourceView) => void;
+}) {
+ const { locale } = useI18n();
+ const [query, setQuery] = useState("");
+ const [page, setPage] = useState(1);
+ const filtered = useMemo(
+ () => resource.resources.filter((item) => item.displayName.toLowerCase().includes(query.trim().toLowerCase())),
+ [resource.resources, query],
+ );
+ const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
+ const visible = filtered.slice((Math.min(page, pageCount) - 1) * PAGE_SIZE, Math.min(page, pageCount) * PAGE_SIZE);
+
+ useEffect(() => setPage(1), [query]);
+
+ return
+
+ {resource.resources.length > PAGE_SIZE &&
+ setQuery(event.target.value)} />
+
}
+
+ {visible.map((item) => onRefresh(item)}
+ onDelete={() => onDelete(item)}
+ />)}
+ {visible.length === 0 && {t("还没有资源,请先添加。")}}
+
+ {pageCount > 1 &&
+
+ {t("第 {page} / {total} 页", { page: Math.min(page, pageCount), total: pageCount })}
+
+
}
+
+ ;
+}
+
+function ResourceRow({ item, canRefresh, disabled, onRefresh, onDelete }: {
+ item: PluginResourceView;
+ canRefresh: boolean;
+ disabled: boolean;
+ onRefresh: () => void;
+ onDelete: () => void;
+}) {
+ const { locale } = useI18n();
+ return
+
+ {item.displayName}
+ {item.description && {pluginText(item.description, locale)}}
+ {item.metrics.map((metric) =>
+ {metric.unit === "percent"
+ ? t("{label} 剩余 {percent}%", { label: pluginText(metric.label, locale), percent: Math.round(metric.value) })
+ : `${pluginText(metric.label, locale)}: ${metric.value}`}
+ )}
+
+
+
+ {canRefresh && }
+
+
+ ;
+}
+
+function StateBadge({ state }: { state: PluginResourceView["state"] }) {
+ if (state.status === "cooling") {
+ return {t("冷却中")};
+ }
+ if (state.status === "invalid") {
+ return {t("已失效")};
+ }
+ return {t("可用")};
+}
+
+function errorText(cause: unknown) {
+ return cause instanceof Error ? cause.message : String(cause);
+}
diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json
index e78a3bc..c3f45b4 100644
--- a/apps/desktop/src/i18n/generated/catalog.json
+++ b/apps/desktop/src/i18n/generated/catalog.json
@@ -33,11 +33,25 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 142,
+ "line": 151,
"column": 40
}
]
},
+ "023810003eb4563d": {
+ "source": "{count} 个模型",
+ "kind": "template",
+ "placeholders": [
+ "count"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 196,
+ "column": 39
+ }
+ ]
+ },
"028a4de61bff743d": {
"source": "普通输入:{tokens} × ${price}/1M = {cost}",
"kind": "template",
@@ -54,6 +68,21 @@
}
]
},
+ "028c60a8a8e30a1b": {
+ "source": "第 {page} / {total} 页",
+ "kind": "template",
+ "placeholders": [
+ "page",
+ "total"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 243,
+ "column": 16
+ }
+ ]
+ },
"03ff62ab4b818492": {
"source": "缓存写入:{tokens} × ${price}/1M = {cost}",
"kind": "template",
@@ -77,8 +106,13 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 154,
- "column": 85
+ "line": 151,
+ "column": 64
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 265,
+ "column": 70
},
{
"file": "features/settings/ProxySettingsCard.tsx",
@@ -107,7 +141,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 134,
+ "line": 142,
"column": 27
}
]
@@ -124,6 +158,18 @@
}
]
},
+ "06619f339fa0ab46": {
+ "source": "正在准备插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 310,
+ "column": 21
+ }
+ ]
+ },
"076832c1b2de22c3": {
"source": "缓存写入:{tokens}",
"kind": "template",
@@ -159,16 +205,31 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 172,
+ "line": 181,
"column": 16
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 284,
+ "line": 296,
"column": 73
}
]
},
+ "08791ba06e7441de": {
+ "source": "{accounts} 个账号 · {models} 个模型",
+ "kind": "template",
+ "placeholders": [
+ "accounts",
+ "models"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 187,
+ "column": 12
+ }
+ ]
+ },
"092b520558eff5f2": {
"source": "未测试",
"kind": "text",
@@ -176,8 +237,8 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 14,
- "column": 105
+ "line": 24,
+ "column": 112
}
]
},
@@ -188,7 +249,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 295,
+ "line": 307,
"column": 89
}
]
@@ -221,6 +282,18 @@
}
]
},
+ "0bbb2c0ce279d6d5": {
+ "source": "尚未同步模型",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 196,
+ "column": 93
+ }
+ ]
+ },
"0c70665b6eb65f1a": {
"source": "否",
"kind": "text",
@@ -345,8 +418,13 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 13,
- "column": 109
+ "line": 20,
+ "column": 66
+ },
+ {
+ "file": "features/models/CursorModelTestResult.tsx",
+ "line": 21,
+ "column": 95
}
]
},
@@ -369,12 +447,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 159,
+ "line": 168,
"column": 16
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 294,
+ "line": 306,
"column": 36
}
]
@@ -398,7 +476,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 252,
+ "line": 264,
"column": 211
}
]
@@ -434,7 +512,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 144,
+ "line": 153,
"column": 286
}
]
@@ -446,12 +524,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 133,
+ "line": 142,
"column": 59
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 133,
+ "line": 142,
"column": 124
}
]
@@ -463,22 +541,22 @@
"refs": [
{
"file": "features/models/CursorGates.tsx",
- "line": 38,
+ "line": 39,
"column": 77
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 255,
+ "line": 267,
"column": 51
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 255,
+ "line": 267,
"column": 130
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 257,
+ "line": 269,
"column": 74
}
]
@@ -509,6 +587,18 @@
}
]
},
+ "18165f8865eacc91": {
+ "source": "还没有安装插件",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 121,
+ "column": 16
+ }
+ ]
+ },
"19658d9fa9aa8de4": {
"source": "安装中…",
"kind": "text",
@@ -538,6 +628,33 @@
}
]
},
+ "1a60c9eb3cf1dbb5": {
+ "source": "导入完成:新增 {added},更新 {updated}",
+ "kind": "template",
+ "placeholders": [
+ "added",
+ "updated"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 155,
+ "column": 23
+ }
+ ]
+ },
+ "1aa65c55c6cc6163": {
+ "source": "设备验证码",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 124,
+ "column": 15
+ }
+ ]
+ },
"1ae6b0a0f8266382": {
"source": "关闭窗口",
"kind": "text",
@@ -562,7 +679,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 265,
+ "line": 277,
"column": 52
}
]
@@ -661,17 +778,17 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 148,
+ "line": 157,
"column": 49
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 150,
+ "line": 159,
"column": 50
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 153,
+ "line": 162,
"column": 50
}
]
@@ -685,7 +802,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 288,
+ "line": 300,
"column": 89
}
]
@@ -721,7 +838,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 250,
+ "line": 262,
"column": 134
}
]
@@ -752,6 +869,20 @@
}
]
},
+ "2400fbd0aeab9e13": {
+ "source": "已下载 {downloaded}",
+ "kind": "template",
+ "placeholders": [
+ "downloaded"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 294,
+ "column": 122
+ }
+ ]
+ },
"24a0a24864454575": {
"source": "已存在,跳过",
"kind": "text",
@@ -771,7 +902,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 144,
+ "line": 153,
"column": 42
}
]
@@ -797,7 +928,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 238,
+ "line": 240,
"column": 14
}
]
@@ -821,16 +952,28 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 155,
+ "line": 164,
"column": 27
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 287,
+ "line": 299,
"column": 186
}
]
},
+ "2cbc58108d78b06c": {
+ "source": "等待网页端确认授权中…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 132,
+ "column": 73
+ }
+ ]
+ },
"2cd0f3be8738a86c": {
"source": "取消",
"kind": "text",
@@ -843,7 +986,7 @@
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 265,
+ "line": 277,
"column": 76
},
{
@@ -851,6 +994,11 @@
"line": 58,
"column": 20
},
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 274,
+ "column": 70
+ },
{
"file": "features/settings/ProxySettingsCard.tsx",
"line": 32,
@@ -878,17 +1026,17 @@
},
{
"file": "shared/ui/Modal.tsx",
- "line": 40,
+ "line": 41,
"column": 191
},
{
"file": "shell/AppLayout.tsx",
- "line": 228,
+ "line": 230,
"column": 20
},
{
"file": "shell/AppLayout.tsx",
- "line": 239,
+ "line": 241,
"column": 20
}
]
@@ -931,7 +1079,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 145,
+ "line": 154,
"column": 42
}
]
@@ -945,7 +1093,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 301,
+ "line": 313,
"column": 70
}
]
@@ -993,7 +1141,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 227,
+ "line": 229,
"column": 14
}
]
@@ -1019,13 +1167,64 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 156,
- "column": 119
+ "line": 153,
+ "column": 96
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 267,
+ "column": 102
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 265,
+ "line": 277,
"column": 99
+ },
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 271,
+ "column": 68
+ }
+ ]
+ },
+ "2fe5a8d0eee9f14c": {
+ "source": "已失效",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 281,
+ "column": 81
+ }
+ ]
+ },
+ "303c30f301514250": {
+ "source": "搜索资源",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 228,
+ "column": 32
+ },
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 228,
+ "column": 56
+ }
+ ]
+ },
+ "32896fdaaaa4c106": {
+ "source": "账号已保存,模型目录已同步。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 136,
+ "column": 64
}
]
},
@@ -1036,7 +1235,7 @@
"refs": [
{
"file": "features/models/CursorGates.tsx",
- "line": 39,
+ "line": 40,
"column": 101
},
{
@@ -1058,7 +1257,7 @@
},
{
"file": "shell/AppLayout.tsx",
- "line": 240,
+ "line": 242,
"column": 21
}
]
@@ -1070,7 +1269,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 286,
+ "line": 298,
"column": 123
}
]
@@ -1119,6 +1318,18 @@
}
]
},
+ "38844b135cf70dfc": {
+ "source": "更多",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 213,
+ "column": 22
+ }
+ ]
+ },
"393e1241552b1870": {
"source": "请求",
"kind": "text",
@@ -1294,7 +1505,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 252,
+ "line": 264,
"column": 197
}
]
@@ -1306,8 +1517,25 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 262,
+ "line": 274,
"column": 80
+ },
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 274,
+ "column": 80
+ }
+ ]
+ },
+ "402495402ce333b1": {
+ "source": "重新初始化插件",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 110,
+ "column": 68
}
]
},
@@ -1402,7 +1630,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 143,
+ "line": 151,
"column": 27
}
]
@@ -1433,6 +1661,18 @@
}
]
},
+ "48a3bf87eb254591": {
+ "source": "开始登录",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 132,
+ "column": 92
+ }
+ ]
+ },
"48b970b568a7f8f9": {
"source": "代理设置",
"kind": "text",
@@ -1514,12 +1754,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 148,
+ "line": 157,
"column": 25
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 287,
+ "line": 299,
"column": 34
}
]
@@ -1531,13 +1771,35 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 153,
- "column": 107
+ "line": 150,
+ "column": 86
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 173,
+ "column": 86
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 264,
+ "column": 92
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 257,
- "column": 681
+ "line": 269,
+ "column": 703
+ }
+ ]
+ },
+ "4a861200ad513a3c": {
+ "source": "初始化插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 273,
+ "column": 12
}
]
},
@@ -1553,6 +1815,18 @@
}
]
},
+ "4aca6a31090fe2b8": {
+ "source": "初始化中…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 110,
+ "column": 46
+ }
+ ]
+ },
"4b458e6e147221d7": {
"source": "系统会根据请求协议自动追加标准端点路径。",
"kind": "text",
@@ -1560,7 +1834,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 139,
+ "line": 148,
"column": 145
}
]
@@ -1577,11 +1851,23 @@
},
{
"file": "shell/AppLayout.tsx",
- "line": 200,
+ "line": 202,
"column": 67
}
]
},
+ "4d99c976beb8827e": {
+ "source": "可用",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 283,
+ "column": 42
+ }
+ ]
+ },
"4e30d7c9ed2b0eee": {
"source": "不设置",
"kind": "text",
@@ -1589,7 +1875,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 197,
+ "line": 206,
"column": 41
}
]
@@ -1647,7 +1933,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 180,
+ "line": 188,
"column": 13
}
]
@@ -1707,7 +1993,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 205,
+ "line": 213,
"column": 47
}
]
@@ -1731,7 +2017,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 255,
+ "line": 267,
"column": 63
}
]
@@ -1743,17 +2029,17 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 150,
+ "line": 159,
"column": 27
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 153,
+ "line": 162,
"column": 27
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 287,
+ "line": 299,
"column": 83
}
]
@@ -1777,7 +2063,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 146,
+ "line": 155,
"column": 69
}
]
@@ -1842,7 +2128,7 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 20,
+ "line": 35,
"column": 7
}
]
@@ -1854,7 +2140,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 251,
+ "line": 263,
"column": 122
}
]
@@ -1871,6 +2157,18 @@
}
]
},
+ "5c62e36c152dfc7c": {
+ "source": "插件运行时初始化完成",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 266,
+ "column": 7
+ }
+ ]
+ },
"5d59857bf039cac9": {
"source": "Cursor 助手 v{version}",
"kind": "template",
@@ -1904,7 +2202,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 244,
+ "line": 246,
"column": 11
}
]
@@ -2016,8 +2314,18 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 155,
- "column": 90
+ "line": 152,
+ "column": 64
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 266,
+ "column": 70
+ },
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 127,
+ "column": 30
}
]
},
@@ -2028,7 +2336,7 @@
"refs": [
{
"file": "shared/api.ts",
- "line": 318,
+ "line": 486,
"column": 43
}
]
@@ -2059,7 +2367,7 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 22,
+ "line": 37,
"column": 7
}
]
@@ -2083,7 +2391,7 @@
"refs": [
{
"file": "features/models/CursorGates.tsx",
- "line": 35,
+ "line": 36,
"column": 14
}
]
@@ -2155,7 +2463,7 @@
"refs": [
{
"file": "features/models/CursorGates.tsx",
- "line": 39,
+ "line": 40,
"column": 113
}
]
@@ -2167,7 +2475,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 77,
+ "line": 79,
"column": 47
}
]
@@ -2191,8 +2499,18 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 153,
- "column": 119
+ "line": 150,
+ "column": 98
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 173,
+ "column": 98
+ },
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 264,
+ "column": 104
}
]
},
@@ -2268,7 +2586,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 249,
+ "line": 261,
"column": 103
}
]
@@ -2280,12 +2598,12 @@
"refs": [
{
"file": "shared/store/appStore.ts",
- "line": 153,
+ "line": 208,
"column": 23
},
{
"file": "shared/store/appStore.ts",
- "line": 160,
+ "line": 215,
"column": 25
}
]
@@ -2302,6 +2620,18 @@
}
]
},
+ "77c9e582e85583af": {
+ "source": "测试失败",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/models/CursorModelTestResult.tsx",
+ "line": 50,
+ "column": 80
+ }
+ ]
+ },
"788db1cfec2a3db5": {
"source": "主题",
"kind": "text",
@@ -2333,7 +2663,7 @@
"refs": [
{
"file": "shared/api.ts",
- "line": 313,
+ "line": 481,
"column": 43
}
]
@@ -2362,6 +2692,18 @@
}
]
},
+ "7c10d97162c96dbd": {
+ "source": "正在验证插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 309,
+ "column": 31
+ }
+ ]
+ },
"7cea2f3c46565d29": {
"source": "OpenAI 额外参数",
"kind": "text",
@@ -2369,12 +2711,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 166,
+ "line": 175,
"column": 16
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 282,
+ "line": 294,
"column": 67
}
]
@@ -2388,7 +2730,7 @@
"refs": [
{
"file": "App.tsx",
- "line": 50,
+ "line": 52,
"column": 19
}
]
@@ -2441,6 +2783,33 @@
}
]
},
+ "802b0faf0ceb513e": {
+ "source": "{label} 剩余 {percent}%",
+ "kind": "template",
+ "placeholders": [
+ "label",
+ "percent"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 264,
+ "column": 13
+ }
+ ]
+ },
+ "80a57e03f0717f91": {
+ "source": "未配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 182,
+ "column": 36
+ }
+ ]
+ },
"811a3b22a5a7f2d5": {
"source": "无法连接本地管理服务",
"kind": "text",
@@ -2448,11 +2817,23 @@
"refs": [
{
"file": "shared/api.ts",
- "line": 262,
+ "line": 418,
"column": 21
}
]
},
+ "8213941f12320ce1": {
+ "source": "当前操作系统或 CPU 架构暂不受支持",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 103,
+ "column": 25
+ }
+ ]
+ },
"83c4efccd9a6bf69": {
"source": "连通性测试已取消:成功 {successful},失败 {failed}",
"kind": "template",
@@ -2463,11 +2844,26 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 178,
+ "line": 186,
"column": 11
}
]
},
+ "83e8d0b7aff2b394": {
+ "source": "已下载 {downloaded} / {total}",
+ "kind": "template",
+ "placeholders": [
+ "downloaded",
+ "total"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 294,
+ "column": 20
+ }
+ ]
+ },
"83fcfb4c1f2c1641": {
"source": "获取模型",
"kind": "text",
@@ -2475,7 +2871,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 144,
+ "line": 153,
"column": 298
}
]
@@ -2521,15 +2917,15 @@
}
]
},
- "86b7355ec3bd55ef": {
- "source": "隐藏 API Key",
+ "86de7c4ee8fa7689": {
+ "source": "同步模型",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "shared/ui/FormControls.tsx",
- "line": 15,
- "column": 81
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 202,
+ "column": 31
}
]
},
@@ -2572,6 +2968,18 @@
}
]
},
+ "8911e4f1407d58cb": {
+ "source": "正在下载插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 306,
+ "column": 32
+ }
+ ]
+ },
"89a101b809be7cfc": {
"source": "系统会原样使用此地址,不追加或修改请求路径。",
"kind": "text",
@@ -2579,7 +2987,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 139,
+ "line": 148,
"column": 115
}
]
@@ -2589,6 +2997,11 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 244,
+ "column": 110
+ },
{
"file": "shared/ui/Pagination.tsx",
"line": 26,
@@ -2613,6 +3026,18 @@
}
]
},
+ "8cbcf741e727dbf7": {
+ "source": "模型配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "shell/AppLayout.tsx",
+ "line": 75,
+ "column": 29
+ }
+ ]
+ },
"8ccaf87ddb9ca3f4": {
"source": "旧版配置",
"kind": "text",
@@ -2649,7 +3074,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 246,
+ "line": 248,
"column": 16
}
]
@@ -2661,25 +3086,20 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 133,
+ "line": 142,
"column": 76
}
]
},
- "8ea973394446abba": {
- "source": "Cursor 配置",
+ "8f6f8d979c981ced": {
+ "source": "已复制",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "features/models/CursorSettingsPage.tsx",
- "line": 256,
- "column": 25
- },
- {
- "file": "shell/AppLayout.tsx",
- "line": 76,
- "column": 53
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 127,
+ "column": 19
}
]
},
@@ -2704,7 +3124,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 302,
+ "line": 314,
"column": 87
}
]
@@ -2733,6 +3153,18 @@
}
]
},
+ "91af6e57e7453fbe": {
+ "source": "添加账号",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 200,
+ "column": 18
+ }
+ ]
+ },
"92156a483d4ba248": {
"source": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。",
"kind": "text",
@@ -2757,6 +3189,18 @@
}
]
},
+ "945fb1c67eca8493": {
+ "source": "正在安装插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 308,
+ "column": 31
+ }
+ ]
+ },
"946b3ffc02f026c0": {
"source": "确定删除这个模型吗?",
"kind": "text",
@@ -2764,11 +3208,23 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 265,
+ "line": 277,
"column": 250
}
]
},
+ "954ec984cd4f49d1": {
+ "source": "正在同步…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 202,
+ "column": 18
+ }
+ ]
+ },
"966498853d801a52": {
"source": "TAB 选择",
"kind": "text",
@@ -2824,11 +3280,23 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 233,
+ "line": 235,
"column": 11
}
]
},
+ "9b9bc9cd7c76406f": {
+ "source": "打开授权网页",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 134,
+ "column": 147
+ }
+ ]
+ },
"9c41b3a9e12ac994": {
"source": "思考强度",
"kind": "text",
@@ -2846,16 +3314,33 @@
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 154,
+ "line": 163,
"column": 27
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 154,
+ "line": 163,
"column": 58
}
]
},
+ "9db205c6055bacc4": {
+ "source": "插件运行时初始化失败",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 96,
+ "column": 9
+ },
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 268,
+ "column": 9
+ }
+ ]
+ },
"9e356080c56877f8": {
"source": "已关闭静默启动",
"kind": "text",
@@ -2880,6 +3365,63 @@
}
]
},
+ "9ebeab8c4532d671": {
+ "source": "{name} 账号管理",
+ "kind": "template",
+ "placeholders": [
+ "name"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 78,
+ "column": 11
+ }
+ ]
+ },
+ "9ec4caa5fe43b8e3": {
+ "source": "安装插件后会显示在这里。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 122,
+ "column": 14
+ }
+ ]
+ },
+ "9ed11266ead88f5b": {
+ "source": "正在验证插件运行时下载文件",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 307,
+ "column": 30
+ }
+ ]
+ },
+ "9ef7da883941091c": {
+ "source": "账号已保存,但同步模型失败:{error}",
+ "kind": "template",
+ "placeholders": [
+ "error"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 157,
+ "column": 17
+ },
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 86,
+ "column": 22
+ }
+ ]
+ },
"9f6fee1aba17a565": {
"source": "语言",
"kind": "text",
@@ -2937,11 +3479,23 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 189,
+ "line": 197,
"column": 22
}
]
},
+ "a12ee6a3e98a29c2": {
+ "source": "隐藏敏感内容",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "shared/ui/FormControls.tsx",
+ "line": 15,
+ "column": 81
+ }
+ ]
+ },
"a1a42cd9b16e2162": {
"source": "应用设置",
"kind": "text",
@@ -2976,6 +3530,11 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "features/models/CursorSettingsPage.tsx",
+ "line": 269,
+ "column": 432
+ },
{
"file": "features/settings/ProxySettingsCard.tsx",
"line": 33,
@@ -2993,7 +3552,7 @@
},
{
"file": "shared/ui/Modal.tsx",
- "line": 40,
+ "line": 41,
"column": 214
}
]
@@ -3057,7 +3616,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 117,
+ "line": 125,
"column": 15
}
]
@@ -3069,7 +3628,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 229,
+ "line": 231,
"column": 21
}
]
@@ -3091,6 +3650,20 @@
}
]
},
+ "a66e11477dcc97c1": {
+ "source": "添加 {name} 账号",
+ "kind": "template",
+ "placeholders": [
+ "name"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 79,
+ "column": 11
+ }
+ ]
+ },
"a693d69af48bfe48": {
"source": "保存并测试",
"kind": "text",
@@ -3098,8 +3671,8 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 257,
- "column": 693
+ "line": 269,
+ "column": 715
}
]
},
@@ -3120,7 +3693,7 @@
},
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 34,
+ "line": 58,
"column": 86
}
]
@@ -3132,7 +3705,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 140,
+ "line": 149,
"column": 61
}
]
@@ -3144,12 +3717,12 @@
"refs": [
{
"file": "features/models/CursorModelCards.tsx",
- "line": 135,
+ "line": 246,
"column": 113
},
{
"file": "features/models/CursorModelCards.tsx",
- "line": 135,
+ "line": 246,
"column": 131
}
]
@@ -3171,7 +3744,7 @@
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 145,
+ "line": 154,
"column": 25
}
]
@@ -3207,7 +3780,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 146,
+ "line": 155,
"column": 118
}
]
@@ -3279,14 +3852,19 @@
"line": 88,
"column": 89
},
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 270,
+ "column": 84
+ },
{
"file": "shell/AppLayout.tsx",
- "line": 260,
+ "line": 262,
"column": 64
},
{
"file": "shell/AppLayout.tsx",
- "line": 260,
+ "line": 262,
"column": 125
}
]
@@ -3315,11 +3893,28 @@
"refs": [
{
"file": "features/models/CursorGates.tsx",
- "line": 36,
+ "line": 37,
"column": 12
}
]
},
+ "b254ff315d861346": {
+ "source": "请重试初始化",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 101,
+ "column": 7
+ },
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 297,
+ "column": 70
+ }
+ ]
+ },
"b4411558b932266f": {
"source": "上游类型",
"kind": "text",
@@ -3332,6 +3927,18 @@
}
]
},
+ "b4c9e08870d41aa2": {
+ "source": "需要先初始化插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 99,
+ "column": 11
+ }
+ ]
+ },
"b502b1d414664337": {
"source": "提示词:{tokens}",
"kind": "template",
@@ -3430,7 +4037,7 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 27,
+ "line": 42,
"column": 50
}
]
@@ -3442,7 +4049,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 262,
+ "line": 274,
"column": 103
}
]
@@ -3500,11 +4107,23 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 145,
+ "line": 154,
"column": 99
}
]
},
+ "ba6403d22876d626": {
+ "source": "冷却中",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 278,
+ "column": 81
+ }
+ ]
+ },
"baff6c144180b185": {
"source": "连通性测试完成:成功 {successful},失败 {failed}",
"kind": "template",
@@ -3515,7 +4134,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 181,
+ "line": 189,
"column": 13
}
]
@@ -3556,11 +4175,23 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 251,
+ "line": 253,
"column": 24
}
]
},
+ "bda74b5674b6a57d": {
+ "source": "初始化插件",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 110,
+ "column": 83
+ }
+ ]
+ },
"bf57afd709694b55": {
"source": "概览时间范围",
"kind": "text",
@@ -3587,6 +4218,18 @@
}
]
},
+ "c0b3fbff51ccc40b": {
+ "source": "完成",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 274,
+ "column": 45
+ }
+ ]
+ },
"c1e98892a77f7a19": {
"source": "{count} 条/页",
"kind": "template",
@@ -3618,6 +4261,30 @@
}
]
},
+ "c54863655e879b36": {
+ "source": "当前系统不支持插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 98,
+ "column": 11
+ }
+ ]
+ },
+ "c6e7e1a9da356efc": {
+ "source": "还没有资源,请先添加。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 239,
+ "column": 66
+ }
+ ]
+ },
"c7ea2c9bc43134bd": {
"source": "编辑模型",
"kind": "text",
@@ -3625,7 +4292,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 257,
+ "line": 269,
"column": 62
}
]
@@ -3637,12 +4304,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 151,
+ "line": 160,
"column": 27
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 151,
+ "line": 160,
"column": 58
}
]
@@ -3676,6 +4343,11 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 242,
+ "column": 102
+ },
{
"file": "shared/ui/Pagination.tsx",
"line": 25,
@@ -3714,11 +4386,23 @@
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 144,
+ "line": 153,
"column": 25
}
]
},
+ "cb99f0138b032687": {
+ "source": "初始化将下载并安装插件运行时。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 104,
+ "column": 9
+ }
+ ]
+ },
"cea1aafe9416de7b": {
"source": "请求头",
"kind": "text",
@@ -3784,7 +4468,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 155,
+ "line": 164,
"column": 50
}
]
@@ -3818,6 +4502,18 @@
}
]
},
+ "d2fcdde81f06645c": {
+ "source": "批量导出",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 229,
+ "column": 32
+ }
+ ]
+ },
"d34335433395cd3a": {
"source": "登录系统后自动启动 Cursor BYOK。",
"kind": "text",
@@ -3837,7 +4533,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 139,
+ "line": 148,
"column": 70
}
]
@@ -3849,13 +4545,25 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 257,
- "column": 653
+ "line": 269,
+ "column": 675
},
{
"file": "shared/ui/Modal.tsx",
- "line": 98,
- "column": 135
+ "line": 99,
+ "column": 153
+ }
+ ]
+ },
+ "d507652243a2151e": {
+ "source": "显示敏感内容",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "shared/ui/FormControls.tsx",
+ "line": 15,
+ "column": 95
}
]
},
@@ -3871,6 +4579,18 @@
}
]
},
+ "d59e47070f7f358e": {
+ "source": "可调用",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 198,
+ "column": 32
+ }
+ ]
+ },
"d60669bb26a22f5d": {
"source": "留空使用默认值",
"kind": "text",
@@ -3878,17 +4598,17 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 148,
+ "line": 157,
"column": 121
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 150,
+ "line": 159,
"column": 122
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 153,
+ "line": 162,
"column": 122
}
]
@@ -3900,11 +4620,25 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 155,
+ "line": 164,
"column": 137
}
]
},
+ "d766536c18e8e990": {
+ "source": "插件运行时 {version} 已安装,可以开始使用插件。",
+ "kind": "template",
+ "placeholders": [
+ "version"
+ ],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 298,
+ "column": 44
+ }
+ ]
+ },
"d86fa42c3848c680": {
"source": "使用系统代理",
"kind": "text",
@@ -3929,7 +4663,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 186,
+ "line": 188,
"column": 54
}
]
@@ -3941,7 +4675,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 263,
+ "line": 275,
"column": 47
}
]
@@ -3965,12 +4699,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 107,
+ "line": 110,
"column": 88
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 146,
+ "line": 155,
"column": 54
}
]
@@ -3982,8 +4716,13 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 15,
- "column": 127
+ "line": 28,
+ "column": 63
+ },
+ {
+ "file": "features/models/CursorModelTestResult.tsx",
+ "line": 29,
+ "column": 92
}
]
},
@@ -4011,6 +4750,18 @@
}
]
},
+ "de8184da1ef88d03": {
+ "source": "已配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 182,
+ "column": 25
+ }
+ ]
+ },
"dea7749c4cd77e6d": {
"source": "总请求 Token 包含提示词和模型输出。",
"kind": "text",
@@ -4040,10 +4791,20 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "features/models/CursorModelCards.tsx",
+ "line": 174,
+ "column": 64
+ },
{
"file": "features/settings/SettingsPage.tsx",
"line": 316,
"column": 30
+ },
+ {
+ "file": "shell/AppLayout.tsx",
+ "line": 77,
+ "column": 29
}
]
},
@@ -4088,6 +4849,18 @@
}
]
},
+ "e049096ab5614581": {
+ "source": "该插件不需要添加资源",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 29,
+ "column": 71
+ }
+ ]
+ },
"e0fae77446a389a3": {
"source": "速度:{speed} tokens/s",
"kind": "template",
@@ -4097,7 +4870,7 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 19,
+ "line": 34,
"column": 7
}
]
@@ -4145,7 +4918,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 201,
+ "line": 209,
"column": 26
}
]
@@ -4162,6 +4935,30 @@
}
]
},
+ "e231f1f3428d1c93": {
+ "source": "正在申请授权码…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 132,
+ "column": 34
+ }
+ ]
+ },
+ "e24096c81b1a8af4": {
+ "source": "授权被拒绝或已失败。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 94,
+ "column": 36
+ }
+ ]
+ },
"e24ebe4a866d69bf": {
"source": "测试失败:{error}",
"kind": "template",
@@ -4171,7 +4968,7 @@
"refs": [
{
"file": "features/models/CursorModelTestResult.tsx",
- "line": 30,
+ "line": 45,
"column": 7
}
]
@@ -4207,7 +5004,7 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 139,
+ "line": 148,
"column": 54
}
]
@@ -4253,6 +5050,18 @@
}
]
},
+ "e5c84c9aa7826566": {
+ "source": "未就绪",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginResourcePanels.tsx",
+ "line": 198,
+ "column": 43
+ }
+ ]
+ },
"e77e3d58b0dcffaa": {
"source": "耗时",
"kind": "text",
@@ -4277,12 +5086,12 @@
"refs": [
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 129,
+ "line": 141,
"column": 25
},
{
"file": "features/models/CursorModelEditor.tsx",
- "line": 129,
+ "line": 141,
"column": 55
}
]
@@ -4354,11 +5163,23 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 263,
+ "line": 275,
"column": 77
}
]
},
+ "eba54690937bc532": {
+ "source": "账号管理",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 206,
+ "column": 20
+ }
+ ]
+ },
"ed31fbb483ee1b0a": {
"source": "操作",
"kind": "text",
@@ -4371,7 +5192,7 @@
},
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 248,
+ "line": 260,
"column": 69
}
]
@@ -4383,7 +5204,7 @@
"refs": [
{
"file": "features/models/CursorSettingsPage.tsx",
- "line": 262,
+ "line": 274,
"column": 53
}
]
@@ -4412,18 +5233,6 @@
}
]
},
- "f2bdc88464c51c2e": {
- "source": "显示 API Key",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "shared/ui/FormControls.tsx",
- "line": 15,
- "column": 99
- }
- ]
- },
"f4694c46b1e19602": {
"source": "最终请求类型",
"kind": "text",
@@ -4487,6 +5296,35 @@
}
]
},
+ "f6dc1b1641600dd0": {
+ "source": "正在导入…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 219,
+ "column": 44
+ }
+ ]
+ },
+ "f78265089144369a": {
+ "source": "插件配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 65,
+ "column": 14
+ },
+ {
+ "file": "shell/AppLayout.tsx",
+ "line": 78,
+ "column": 46
+ }
+ ]
+ },
"f78413c36d36f090": {
"source": "默认跟随操作系统;不支持的系统语言使用英文。当前:{language}",
"kind": "template",
@@ -4575,11 +5413,28 @@
}
]
},
+ "fad86bf65f72c747": {
+ "source": "下载进度",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 283,
+ "column": 23
+ }
+ ]
+ },
"fb11aa6f29827095": {
"source": "检查中…",
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 110,
+ "column": 19
+ },
{
"file": "features/settings/AppLifecycleSettingsCard.tsx",
"line": 160,
@@ -4599,6 +5454,23 @@
}
]
},
+ "fc22d1ab9ac73c6f": {
+ "source": "正在检查插件运行时",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 94,
+ "column": 7
+ },
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 305,
+ "column": 29
+ }
+ ]
+ },
"fc3947ebe6b2177b": {
"source": "默认 {defaultRate} / 计入创建 {reuseRate}",
"kind": "template",
@@ -4643,6 +5515,18 @@
}
]
},
+ "fd77192739703811": {
+ "source": "批量导入",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "features/plugins/PluginManagementPage.tsx",
+ "line": 219,
+ "column": 57
+ }
+ ]
+ },
"fdc4cabc370fa3f7": {
"source": "暂无选项",
"kind": "text",
@@ -4662,7 +5546,7 @@
"refs": [
{
"file": "features/home/HomePage.tsx",
- "line": 148,
+ "line": 155,
"column": 25
}
]
@@ -4674,7 +5558,7 @@
"refs": [
{
"file": "shell/AppLayout.tsx",
- "line": 78,
+ "line": 80,
"column": 48
}
]
diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json
index 921a806..aa941c2 100644
--- a/apps/desktop/src/i18n/locales/en-US.json
+++ b/apps/desktop/src/i18n/locales/en-US.json
@@ -2,18 +2,23 @@
"0006d696d8e1ec28": "New",
"00929f23850e4ff0": "Successful calls: {count}",
"01f3e69a5a9b2c9b": "The key required to access the model service.",
+ "023810003eb4563d": "{count} models",
"028a4de61bff743d": "Regular input: {tokens} × ${price}/1M = {cost}",
+ "028c60a8a8e30a1b": "Page {page} / {total}",
"03ff62ab4b818492": "Cache write: {tokens} × ${price}/1M = {cost}",
"051836569928a9f9": "Edit",
"05468af47054d488": "Connectivity test for {model} succeeded ({duration} ms)",
"0580e0a99a6f1afc": "Artifacts",
+ "06619f339fa0ab46": "Preparing the plugin runtime",
"076832c1b2de22c3": "Cache write: {tokens}",
"07879e064ae16542": "Estimated output: {tokens}",
"07c657ed4747126e": "Anthropic extra parameters",
+ "08791ba06e7441de": "{accounts} accounts · {models} models",
"092b520558eff5f2": "Not tested",
"099008ea7a42ebd1": "All custom Header values must be strings",
"09ebc2643631ba25": "Estimated value",
"0b96da34f6fbdd3b": "Cache read: {tokens} × ${price}/1M = {cost}",
+ "0bbb2c0ce279d6d5": "Models not synced yet",
"0c70665b6eb65f1a": "No",
"0c72229b7db0e1a9": "Model output",
"0d2dab3d62eb73d6": "All statistics cleared",
@@ -33,8 +38,11 @@
"168e845a86bc3703": "Add model",
"16d0d7e2b332af72": "Total calls: {count}",
"1813d362a82fd437": "Maximize window",
+ "18165f8865eacc91": "No plugins installed",
"19658d9fa9aa8de4": "Installing…",
"1a3f0617d6de8e52": "Username",
+ "1a60c9eb3cf1dbb5": "Import finished: {added} added, {updated} updated",
+ "1aa65c55c6cc6163": "Device code",
"1ae6b0a0f8266382": "Close window",
"1b5932b8946d2d68": "Delete model",
"1b7d5b1a9315fc64": "Calculating…",
@@ -50,12 +58,14 @@
"22d7895ea5fca72e": "By provider",
"23ae7a90b1b9816d": "Clear scope",
"23e49479e15e6770": "Version {version} is available",
+ "2400fbd0aeab9e13": "Downloaded {downloaded}",
"24a0a24864454575": "Existing, skipped",
"2555d6c7fbb7e070": "Enter a model ID directly or load models returned by the API.",
"29585d7193539200": "Current version {version}",
"29fbbef32a6eb58b": "Do not show this ad again",
"2a2773134a829016": "Aggregated from historical LLM calls; in-progress calls are excluded.",
"2caeaec539e78898": "Thinking budget tokens",
+ "2cbc58108d78b06c": "Waiting for browser authorization…",
"2cd0f3be8738a86c": "Cancel",
"2d30c2a98ebb5278": "Current: {rate}",
"2eb2bf7c6597ab9a": "Detailed records",
@@ -67,12 +77,16 @@
"2f7ba5fd1d12f7f9": "Open the tutorial?",
"2f7dec3be28d7597": "{count} selected",
"2f9daa828907b93f": "Delete",
+ "2fe5a8d0eee9f14c": "Invalid",
+ "303c30f301514250": "Search resources",
+ "32896fdaaaa4c106": "Account saved and the model catalog is synced.",
"346ff60e6c7c5181": "Reading…",
"36f33adaf0942634": "Confirm",
"37125ef2e1d707cb": "Server address or complete request URL, API Key, model name, display name, and note are required",
"378bb0eec39fa8a2": "Last page",
"37cb98ff4d5dcfcc": "Successful {successful} / failed {failed}",
"382f2e3419a02fef": "Only clear detailed records",
+ "38844b135cf70dfc": "More",
"393e1241552b1870": "Request",
"398f8e6c6f0a0b97": "Continue selecting or typing",
"39f52eee100131d7": "Cached input",
@@ -88,6 +102,7 @@
"3f6c25aa329163a4": "The original endpoint path is appended to this service address.",
"3fd118e2ffe0b2b6": "Cancel all tests",
"3fd47edce45b3603": "Close",
+ "402495402ce333b1": "Reinitialize plugins",
"40a08e7cf320ae07": "Clear detailed records?",
"4125fc7ba333524c": "Default light",
"42655ed8e4108ae2": "Input (non-cached)",
@@ -97,6 +112,7 @@
"461d6a57900c2ed7": "Connectivity test failed: {error}",
"470049252e54de6a": "Success rate: {rate}",
"47d1c20aa017ff05": "Hide the main window on startup and keep only the tray icon.",
+ "48a3bf87eb254591": "Start sign-in",
"48b970b568a7f8f9": "Proxy settings",
"48d8db17bae06246": "{count} total",
"492042ed1fdc29ed": "Version {version} is ready to install",
@@ -104,9 +120,12 @@
"497c85690c4cc0fc": "No data",
"499c729eb09aa2a6": "Context window tokens",
"49be72e6045c007d": "Cancel test",
+ "4a861200ad513a3c": "Initialize plugin runtime",
"4a8d6841b4023edf": "Confirm import",
+ "4aca6a31090fe2b8": "Initializing…",
"4b458e6e147221d7": "The standard endpoint path is appended automatically for the selected protocol.",
"4d0680f9efaef147": "Unread",
+ "4d99c976beb8827e": "Ready",
"4e30d7c9ed2b0eee": "Not set",
"4eafa9e925b30bcd": "Custom",
"51d04bc3d286f018": "Last calendar day",
@@ -127,6 +146,7 @@
"5b17f59d33bde39e": "Error: {error}",
"5ba65a74c4e792c5": "By type",
"5c55a67935af8f45": "All",
+ "5c62e36c152dfc7c": "Plugin runtime initialized",
"5d59857bf039cac9": "Cursor Assistant v{version}",
"5f8d556a9c47da3c": "Launch at login disabled",
"5f9acfb945229062": "Are you sure you no longer want to see this ad?",
@@ -158,53 +178,71 @@
"72644ec4389da2f7": "Default layout",
"736c9dc2a04c65fd": "The model configuration changed. Refresh and try again.",
"7392e20d61abaa07": "Also store complete requests and streamed responses; by default only timing, status, and usage are stored.",
+ "77c9e582e85583af": "Test failed",
"788db1cfec2a3db5": "Theme",
"7995087e5a3dfe66": "Restore window",
"7a2229f6a6d330a5": "Open a terminal from the desktop app to install the CA",
"7a3cec4ca715de80": "Call statistics",
"7ba2d6728fe2531b": "Confirm clear",
+ "7c10d97162c96dbd": "Validating the plugin runtime",
"7cea2f3c46565d29": "OpenAI extra parameters",
"7d9f043f8f7ab45c": "Version {version} is available in Settings",
"7e0891860c9e6374": "TAB service address is required",
"7e7df68f2a82e09e": "Importing the same configuration again will not create duplicate models. Existing models are skipped automatically.",
"7f3c8312816fe26a": "Refreshing…",
"7f68ebad19ba6bcd": "Check for updates",
+ "802b0faf0ceb513e": "{label}: {percent}% left",
+ "80a57e03f0717f91": "Not configured",
"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",
+ "83e8d0b7aff2b394": "Downloaded {downloaded} / {total}",
"83fcfb4c1f2c1641": "Fetch models",
"842b9f11cdd96bda": "Launch at login",
"843ac7e15a5047a7": "Confirm legacy model configuration import",
"864597982c308d72": "Silent start enabled",
- "86b7355ec3bd55ef": "Hide API Key",
+ "86de7c4ee8fa7689": "Sync models",
"8716e1344b0daddb": "Cursor official",
"878a8ab176429a86": "View instructions",
+ "8911e4f1407d58cb": "Downloading the plugin runtime",
"89a101b809be7cfc": "This address is used exactly as entered without changing or appending the request path.",
"8a8542f6964852dc": "Next page",
"8b6ff498515bcc2f": "Time",
+ "8cbcf741e727dbf7": "Models",
"8ccaf87ddb9ca3f4": "Legacy configuration",
"8d0c47eb9eac2d34": "Call type",
"8df48894086d6fbd": "Reason (optional)",
"8e2d04638a11a7cb": "Only determines the request and response format; it does not change the request URL.",
- "8ea973394446abba": "Cursor Configuration",
+ "8f6f8d979c981ced": "Copied",
"8f9b0d6cc477d334": "Choose how Cursor connects to TAB endpoints.",
"90800c48a1dd0655": "{label} must be a JSON object",
"919cb0ce0c8db4e7": "Leave blank to keep the current password",
"91aaf184cfc17ffd": "Overview",
+ "91af6e57e7453fbe": "Add account",
"92156a483d4ba248": "Only request, response, and trace attachments are deleted; call summaries, metrics, and configuration are kept.",
"940a168911ade998": "Items per page",
+ "945fb1c67eca8493": "Installing the plugin runtime",
"946b3ffc02f026c0": "Delete this model?",
+ "954ec984cd4f49d1": "Syncing…",
"966498853d801a52": "TAB connection",
"9850ed41a5bfbb0c": "{count} selected",
"997ec8201c2adeda": "Open terminal to install CA",
"9b1b7ed518ee401d": "This will open the tutorial in your system browser. Continue?",
+ "9b9bc9cd7c76406f": "Open authorization page",
"9c41b3a9e12ac994": "Reasoning effort",
+ "9db205c6055bacc4": "Plugin runtime initialization failed",
"9e356080c56877f8": "Silent start disabled",
"9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago",
+ "9ebeab8c4532d671": "{name} accounts",
+ "9ec4caa5fe43b8e3": "Installed plugins will appear here.",
+ "9ed11266ead88f5b": "Verifying the plugin runtime download",
+ "9ef7da883941091c": "The account was saved, but model sync failed: {error}",
"9f6fee1aba17a565": "Language",
"9fb48101d237ff96": "Last week",
"a026f37e613cf48b": "Output Tokens",
"a03a1a0cb35414f8": " must be an integer from 0 to 65535",
"a0c42c24e74f8380": "{name} Copy",
+ "a12ee6a3e98a29c2": "Hide sensitive content",
"a1a42cd9b16e2162": "Application",
"a1b8c98f29374a2f": "Silent start",
"a3030bf8f16dc63c": "Save",
@@ -215,6 +253,7 @@
"a4d222236dc1003d": "Failed to cancel test: {error}",
"a5fb6189a8ad011d": "Open tutorial",
"a621ab606db2a11f": "Password",
+ "a66e11477dcc97c1": "Add {name} account",
"a693d69af48bfe48": "Save and test",
"a748cc074f78de00": "View details",
"a7617f42f898b2bf": "Use complete request URL",
@@ -230,7 +269,9 @@
"aee88743413144a2": "Refresh",
"b06325c5660f0c29": "Direct",
"b16c3b2ecedd6fe1": "Cursor integration is active. Add a model configuration to use a BYOK model.",
+ "b254ff315d861346": "Try initializing again",
"b4411558b932266f": "Provider type",
+ "b4c9e08870d41aa2": "Initialize the plugin runtime first",
"b502b1d414664337": "Prompt: {tokens}",
"b5141d3d19e9a048": "Yes",
"b6725f218ebaef26": "Dock icon shown",
@@ -243,14 +284,19 @@
"b9670c85a4ab939e": "Route",
"b9af2de88d903be7": "Proxy address",
"ba5865fbc734e672": "For example: Primary model",
+ "ba6403d22876d626": "Cooling down",
"baff6c144180b185": "Connectivity tests completed: {successful} succeeded, {failed} failed",
"bb2b7736433ae867": "Cursor tracing",
"bb7efdcb6af6e805": "Default dark",
"bda62ce1d5e4ace9": "Tell us why",
+ "bda74b5674b6a57d": "Initialize plugins",
"bf57afd709694b55": "Overview time range",
"bfc01caf9fe0c841": "Cache hit rate {rate}",
+ "c0b3fbff51ccc40b": "Done",
"c1e98892a77f7a19": "{count} per page",
"c3760858cdb6d9f4": "Request body",
+ "c54863655e879b36": "Plugin runtime is not supported on this system",
+ "c6e7e1a9da356efc": "No resources yet. Add one first.",
"c7ea2c9bc43134bd": "Edit model",
"c8c14507b2d37395": "Reasoning effort",
"c8df3c14a003bfcd": "Unable to load call details",
@@ -258,6 +304,7 @@
"c9b9ae7a61444ab7": "Previous page",
"c9d146d006993cc1": "Cache statistics policy: default ({rate})",
"cb2f1709f983d2f4": "Model name",
+ "cb99f0138b032687": "Initialization downloads and installs the plugin runtime.",
"cea1aafe9416de7b": "Request headers",
"cfae1a14d2120c57": "Detailed mode",
"cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.",
@@ -265,12 +312,16 @@
"d1251cd752d4ec25": "Leave blank to use adaptive thinking.",
"d1a3d72618d1ed27": "All call summaries, detailed content, and trace records will be deleted. Model configuration, CA, and application settings are unaffected. This action cannot be undone.",
"d2d648bd1c94b7f9": "Authentication",
+ "d2fcdde81f06645c": "Bulk export",
"d34335433395cd3a": "Start Cursor BYOK automatically after signing in.",
"d3716cc5a2f5a810": "Server address",
"d3d21191f32e79a5": "Processing…",
+ "d507652243a2151e": "Show sensitive content",
"d58c88688e1a949d": "Presets",
+ "d59e47070f7f358e": "Callable",
"d60669bb26a22f5d": "Leave blank to use the default",
"d6b1f203680f5496": "Leave blank to use adaptive thinking",
+ "d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.",
"d86fa42c3848c680": "Use system proxy",
"d8c47e9776cf1082": "Main menu",
"da521d1c1cbd36af": "Authorization is required to install the certificate",
@@ -279,18 +330,22 @@
"db340a9896306d08": "Test cancelled",
"dbd3596e4a86f3c2": "Configured models",
"ddde16f8839da3ce": "Total requests",
+ "de8184da1ef88d03": "Configured",
"dea7749c4cd77e6d": "Total request Tokens include the prompt and model output.",
"df1baa9f706d970b": "To add",
"df3d58c7d84b85f2": "Settings",
"df8b71c74d9b8478": "Response stream",
"dfb802238b38fbd4": "Enabled",
"e025f1ff71996425": "Set",
+ "e049096ab5614581": "This plugin does not need any resources.",
"e0fae77446a389a3": "Speed: {speed} tokens/s",
"e1295adecbb77755": "Close ad",
"e14115de7f7c5795": "Token usage over the past year",
"e14f20d572c02611": "Provider call sequence",
"e17a5b9c90cda6ab": "Model duplicated",
"e18516550b9a5105": "No usage",
+ "e231f1f3428d1c93": "Requesting an authorization code…",
+ "e24096c81b1a8af4": "Authorization was denied or failed.",
"e24ebe4a866d69bf": "Test failed: {error}",
"e25bf3f419bb68f0": "Call history",
"e3fee05f688708b4": "LLM calls",
@@ -298,6 +353,7 @@
"e5043c7a2b408271": "Last 10 minutes",
"e59ae97924d62f01": "First page",
"e5b9961a0d5242e3": "Port settings saved. Restart the app to apply them.",
+ "e5c84c9aa7826566": "Not ready",
"e77e3d58b0dcffaa": "Duration",
"e825a2a42c22380e": "Model type",
"e828bd3a0151edc2": "The local CA must be trusted by the system",
@@ -306,16 +362,18 @@
"eb11e2df1d8ae387": "Provider URL",
"eb1be07f2ca6e506": "Estimated using Claude Opus 4.7 pricing.",
"eb77492c9f76a7e1": "The install command has been copied. Click “Open terminal”, paste it into the terminal, and enter your password when prompted.",
+ "eba54690937bc532": "Manage accounts",
"ed31fbb483ee1b0a": "Actions",
"edc70de18c6da1a6": "Install local CA",
"ee239f3943293f87": "Sunday",
"ee6b89a6a740a4c4": "If a port is occupied, a new random port is selected and saved automatically. Restart the app after changing these settings.",
- "f2bdc88464c51c2e": "Show API Key",
"f4694c46b1e19602": "Final request type",
"f4dcb6a3ceb32247": "Page {page} of {count}",
"f4fa9f31ea2ae58d": "Token usage calendar for the past year",
"f50276449943286c": "End time",
"f69273dbbebfb3a1": "Format",
+ "f6dc1b1641600dd0": "Importing…",
+ "f78265089144369a": "Plugins",
"f78413c36d36f090": "Uses the operating system language by default; unsupported languages fall back to English. Current: {language}",
"f85537d1fd2ef6f6": "Custom overview filters",
"f95ea7f4c063eea7": "Disabled",
@@ -323,11 +381,14 @@
"f9aa11dbb15ce647": "Saturday",
"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.",
+ "fad86bf65f72c747": "Download progress",
"fb11aa6f29827095": "Checking…",
"fbe8778fa8b9bab5": "Initialize the local CA first",
+ "fc22d1ab9ac73c6f": "Checking the plugin runtime",
"fc3947ebe6b2177b": "Default {defaultRate} / include creation {reuseRate}",
"fcd311fd8ad42462": "Open model list",
"fd415f8e0097c832": "Cache reads and writes are included in prompt-side statistics.",
+ "fd77192739703811": "Bulk import",
"fdc4cabc370fa3f7": "No options",
"fea405f9b01d1416": "Summary",
"fec45092945f8790": "User guide",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json
index e6e5f1e..9aff95e 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.json
+++ b/apps/desktop/src/i18n/locales/zh-CN.json
@@ -2,18 +2,23 @@
"0006d696d8e1ec28": "新增",
"00929f23850e4ff0": "成功调用:{count}",
"01f3e69a5a9b2c9b": "访问模型服务所需的密钥。",
+ "023810003eb4563d": "{count} 个模型",
"028a4de61bff743d": "普通输入:{tokens} × ${price}/1M = {cost}",
+ "028c60a8a8e30a1b": "第 {page} / {total} 页",
"03ff62ab4b818492": "缓存写入:{tokens} × ${price}/1M = {cost}",
"051836569928a9f9": "编辑",
"05468af47054d488": "模型 {model} 连通性测试成功({duration} ms)",
"0580e0a99a6f1afc": "工件数",
+ "06619f339fa0ab46": "正在准备插件运行时",
"076832c1b2de22c3": "缓存写入:{tokens}",
"07879e064ae16542": "输出推算:{tokens}",
"07c657ed4747126e": "Anthropic 额外参数",
+ "08791ba06e7441de": "{accounts} 个账号 · {models} 个模型",
"092b520558eff5f2": "未测试",
"099008ea7a42ebd1": "自定义 Headers 的值必须都是字符串",
"09ebc2643631ba25": "价值估算",
"0b96da34f6fbdd3b": "缓存读取:{tokens} × ${price}/1M = {cost}",
+ "0bbb2c0ce279d6d5": "尚未同步模型",
"0c70665b6eb65f1a": "否",
"0c72229b7db0e1a9": "模型输出",
"0d2dab3d62eb73d6": "全部统计数据已清理",
@@ -33,8 +38,11 @@
"168e845a86bc3703": "添加模型",
"16d0d7e2b332af72": "总调用:{count}",
"1813d362a82fd437": "最大化窗口",
+ "18165f8865eacc91": "还没有安装插件",
"19658d9fa9aa8de4": "安装中…",
"1a3f0617d6de8e52": "用户名",
+ "1a60c9eb3cf1dbb5": "导入完成:新增 {added},更新 {updated}",
+ "1aa65c55c6cc6163": "设备验证码",
"1ae6b0a0f8266382": "关闭窗口",
"1b5932b8946d2d68": "删除模型",
"1b7d5b1a9315fc64": "计算中…",
@@ -50,12 +58,14 @@
"22d7895ea5fca72e": "按供应商",
"23ae7a90b1b9816d": "清理范围",
"23e49479e15e6770": "发现新版本 {version}",
+ "2400fbd0aeab9e13": "已下载 {downloaded}",
"24a0a24864454575": "已存在,跳过",
"2555d6c7fbb7e070": "可以直接输入模型标识,也可以读取接口返回的模型列表。",
"29585d7193539200": "当前版本 {version}",
"29fbbef32a6eb58b": "不再显示此广告",
"2a2773134a829016": "按历史 LLM 调用记录汇总,进行中的调用不计入。",
"2caeaec539e78898": "思考预算 Token",
+ "2cbc58108d78b06c": "等待网页端确认授权中…",
"2cd0f3be8738a86c": "取消",
"2d30c2a98ebb5278": "当前:{rate}",
"2eb2bf7c6597ab9a": "详细记录",
@@ -67,12 +77,16 @@
"2f7ba5fd1d12f7f9": "打开使用教程?",
"2f7dec3be28d7597": "已选择 {count} 个",
"2f9daa828907b93f": "删除",
+ "2fe5a8d0eee9f14c": "已失效",
+ "303c30f301514250": "搜索资源",
+ "32896fdaaaa4c106": "账号已保存,模型目录已同步。",
"346ff60e6c7c5181": "读取中…",
"36f33adaf0942634": "确认",
"37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
"378bb0eec39fa8a2": "最后一页",
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
"382f2e3419a02fef": "仅清理详细记录",
+ "38844b135cf70dfc": "更多",
"393e1241552b1870": "请求",
"398f8e6c6f0a0b97": "继续选择或输入",
"39f52eee100131d7": "缓存输入",
@@ -88,6 +102,7 @@
"3f6c25aa329163a4": "原接口路径会追加到此服务地址。",
"3fd118e2ffe0b2b6": "取消全部测试",
"3fd47edce45b3603": "关闭",
+ "402495402ce333b1": "重新初始化插件",
"40a08e7cf320ae07": "确定要清理详细记录吗?",
"4125fc7ba333524c": "默认亮色",
"42655ed8e4108ae2": "输入(非缓存)",
@@ -97,6 +112,7 @@
"461d6a57900c2ed7": "连通性测试失败:{error}",
"470049252e54de6a": "成功占比:{rate}",
"47d1c20aa017ff05": "开机启动时不显示主窗口,仅保留系统托盘图标。",
+ "48a3bf87eb254591": "开始登录",
"48b970b568a7f8f9": "代理设置",
"48d8db17bae06246": "共 {count} 条",
"492042ed1fdc29ed": "版本 {version} 可以安装",
@@ -104,9 +120,12 @@
"497c85690c4cc0fc": "暂无数据",
"499c729eb09aa2a6": "上下文窗口 Token",
"49be72e6045c007d": "取消测试",
+ "4a861200ad513a3c": "初始化插件运行时",
"4a8d6841b4023edf": "确认导入",
+ "4aca6a31090fe2b8": "初始化中…",
"4b458e6e147221d7": "系统会根据请求协议自动追加标准端点路径。",
"4d0680f9efaef147": "未读",
+ "4d99c976beb8827e": "可用",
"4e30d7c9ed2b0eee": "不设置",
"4eafa9e925b30bcd": "自定义",
"51d04bc3d286f018": "近1自然日",
@@ -127,6 +146,7 @@
"5b17f59d33bde39e": "错误:{error}",
"5ba65a74c4e792c5": "按类型",
"5c55a67935af8f45": "全部",
+ "5c62e36c152dfc7c": "插件运行时初始化完成",
"5d59857bf039cac9": "Cursor 助手 v{version}",
"5f8d556a9c47da3c": "已关闭开机启动",
"5f9acfb945229062": "你确认不想再看到此广告吗?",
@@ -158,53 +178,71 @@
"72644ec4389da2f7": "默认平铺",
"736c9dc2a04c65fd": "模型配置已发生变化,请刷新后重试",
"7392e20d61abaa07": "额外保存完整请求和流响应;默认只保存时间、状态与用量。",
+ "77c9e582e85583af": "测试失败",
"788db1cfec2a3db5": "主题",
"7995087e5a3dfe66": "还原窗口",
"7a2229f6a6d330a5": "请在桌面应用中打开终端安装 CA",
"7a3cec4ca715de80": "调用统计",
"7ba2d6728fe2531b": "确认清理",
+ "7c10d97162c96dbd": "正在验证插件运行时",
"7cea2f3c46565d29": "OpenAI 额外参数",
"7d9f043f8f7ab45c": "发现新版本 {version},可在设置中安装",
"7e0891860c9e6374": "TAB 服务地址不能为空",
"7e7df68f2a82e09e": "重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。",
"7f3c8312816fe26a": "刷新中…",
"7f68ebad19ba6bcd": "检查更新",
+ "802b0faf0ceb513e": "{label} 剩余 {percent}%",
+ "80a57e03f0717f91": "未配置",
"811a3b22a5a7f2d5": "无法连接本地管理服务",
+ "8213941f12320ce1": "当前操作系统或 CPU 架构暂不受支持",
"83c4efccd9a6bf69": "连通性测试已取消:成功 {successful},失败 {failed}",
+ "83e8d0b7aff2b394": "已下载 {downloaded} / {total}",
"83fcfb4c1f2c1641": "获取模型",
"842b9f11cdd96bda": "开机启动",
"843ac7e15a5047a7": "确认导入旧版模型配置",
"864597982c308d72": "已开启静默启动",
- "86b7355ec3bd55ef": "隐藏 API Key",
+ "86de7c4ee8fa7689": "同步模型",
"8716e1344b0daddb": "Cursor 官方",
"878a8ab176429a86": "查看说明",
+ "8911e4f1407d58cb": "正在下载插件运行时",
"89a101b809be7cfc": "系统会原样使用此地址,不追加或修改请求路径。",
"8a8542f6964852dc": "下一页",
"8b6ff498515bcc2f": "时间",
+ "8cbcf741e727dbf7": "模型配置",
"8ccaf87ddb9ca3f4": "旧版配置",
"8d0c47eb9eac2d34": "调用类型",
"8df48894086d6fbd": "原因(可选)",
"8e2d04638a11a7cb": "只决定请求与响应的格式,不会改变请求地址。",
- "8ea973394446abba": "Cursor 配置",
+ "8f6f8d979c981ced": "已复制",
"8f9b0d6cc477d334": "控制 Cursor TAB 相关接口的连接方式。",
"90800c48a1dd0655": "{label} 必须是 JSON 对象",
"919cb0ce0c8db4e7": "留空表示保留当前密码",
"91aaf184cfc17ffd": "数据概览",
+ "91af6e57e7453fbe": "添加账号",
"92156a483d4ba248": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。",
"940a168911ade998": "每页条数",
+ "945fb1c67eca8493": "正在安装插件运行时",
"946b3ffc02f026c0": "确定删除这个模型吗?",
+ "954ec984cd4f49d1": "正在同步…",
"966498853d801a52": "TAB 选择",
"9850ed41a5bfbb0c": "已选 {count} 项",
"997ec8201c2adeda": "打开终端安装 CA",
"9b1b7ed518ee401d": "将在系统浏览器中打开使用教程,是否继续?",
+ "9b9bc9cd7c76406f": "打开授权网页",
"9c41b3a9e12ac994": "思考强度",
+ "9db205c6055bacc4": "插件运行时初始化失败",
"9e356080c56877f8": "已关闭静默启动",
"9e46da6923836182": "如:2026-08-23 09:00、1小时前",
+ "9ebeab8c4532d671": "{name} 账号管理",
+ "9ec4caa5fe43b8e3": "安装插件后会显示在这里。",
+ "9ed11266ead88f5b": "正在验证插件运行时下载文件",
+ "9ef7da883941091c": "账号已保存,但同步模型失败:{error}",
"9f6fee1aba17a565": "语言",
"9fb48101d237ff96": "近一周",
"a026f37e613cf48b": "输出 Token",
"a03a1a0cb35414f8": "必须是 0–65535 之间的整数",
"a0c42c24e74f8380": "{name} 副本",
+ "a12ee6a3e98a29c2": "隐藏敏感内容",
"a1a42cd9b16e2162": "应用设置",
"a1b8c98f29374a2f": "静默启动",
"a3030bf8f16dc63c": "保存",
@@ -215,6 +253,7 @@
"a4d222236dc1003d": "取消测试失败:{error}",
"a5fb6189a8ad011d": "打开教程",
"a621ab606db2a11f": "密码",
+ "a66e11477dcc97c1": "添加 {name} 账号",
"a693d69af48bfe48": "保存并测试",
"a748cc074f78de00": "查看详情",
"a7617f42f898b2bf": "使用完整请求地址",
@@ -230,7 +269,9 @@
"aee88743413144a2": "刷新",
"b06325c5660f0c29": "直连",
"b16c3b2ecedd6fe1": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。",
+ "b254ff315d861346": "请重试初始化",
"b4411558b932266f": "上游类型",
+ "b4c9e08870d41aa2": "需要先初始化插件运行时",
"b502b1d414664337": "提示词:{tokens}",
"b5141d3d19e9a048": "是",
"b6725f218ebaef26": "已显示 Dock 栏图标",
@@ -243,14 +284,19 @@
"b9670c85a4ab939e": "路由",
"b9af2de88d903be7": "代理地址",
"ba5865fbc734e672": "例如:主力模型",
+ "ba6403d22876d626": "冷却中",
"baff6c144180b185": "连通性测试完成:成功 {successful},失败 {failed}",
"bb2b7736433ae867": "Cursor 追踪",
"bb7efdcb6af6e805": "默认暗色",
"bda62ce1d5e4ace9": "可以告诉我们原因",
+ "bda74b5674b6a57d": "初始化插件",
"bf57afd709694b55": "概览时间范围",
"bfc01caf9fe0c841": "缓存命中率 {rate}",
+ "c0b3fbff51ccc40b": "完成",
"c1e98892a77f7a19": "{count} 条/页",
"c3760858cdb6d9f4": "请求体",
+ "c54863655e879b36": "当前系统不支持插件运行时",
+ "c6e7e1a9da356efc": "还没有资源,请先添加。",
"c7ea2c9bc43134bd": "编辑模型",
"c8c14507b2d37395": "推理强度",
"c8df3c14a003bfcd": "无法加载调用详情",
@@ -258,6 +304,7 @@
"c9b9ae7a61444ab7": "上一页",
"c9d146d006993cc1": "缓存统计策略:默认口径({rate})",
"cb2f1709f983d2f4": "模型名称",
+ "cb99f0138b032687": "初始化将下载并安装插件运行时。",
"cea1aafe9416de7b": "请求头",
"cfae1a14d2120c57": "详细模式",
"cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。",
@@ -265,12 +312,16 @@
"d1251cd752d4ec25": "留空时使用 adaptive thinking。",
"d1a3d72618d1ed27": "所有调用汇总、详细内容和追踪记录都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
"d2d648bd1c94b7f9": "认证",
+ "d2fcdde81f06645c": "批量导出",
"d34335433395cd3a": "登录系统后自动启动 Cursor BYOK。",
"d3716cc5a2f5a810": "服务器地址",
"d3d21191f32e79a5": "处理中…",
+ "d507652243a2151e": "显示敏感内容",
"d58c88688e1a949d": "常用预设",
+ "d59e47070f7f358e": "可调用",
"d60669bb26a22f5d": "留空使用默认值",
"d6b1f203680f5496": "留空使用 adaptive thinking",
+ "d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。",
"d86fa42c3848c680": "使用系统代理",
"d8c47e9776cf1082": "主菜单",
"da521d1c1cbd36af": "需要授权安装证书",
@@ -279,18 +330,22 @@
"db340a9896306d08": "测试已取消",
"dbd3596e4a86f3c2": "配置模型",
"ddde16f8839da3ce": "总请求",
+ "de8184da1ef88d03": "已配置",
"dea7749c4cd77e6d": "总请求 Token 包含提示词和模型输出。",
"df1baa9f706d970b": "将新增",
"df3d58c7d84b85f2": "设置",
"df8b71c74d9b8478": "响应流",
"dfb802238b38fbd4": "已启用",
"e025f1ff71996425": "已设置",
+ "e049096ab5614581": "该插件不需要添加资源",
"e0fae77446a389a3": "速度:{speed} tokens/s",
"e1295adecbb77755": "关闭广告",
"e14115de7f7c5795": "过去一年的 Token 用量",
"e14f20d572c02611": "上游调用序号",
"e17a5b9c90cda6ab": "模型已复制",
"e18516550b9a5105": "无用量",
+ "e231f1f3428d1c93": "正在申请授权码…",
+ "e24096c81b1a8af4": "授权被拒绝或已失败。",
"e24ebe4a866d69bf": "测试失败:{error}",
"e25bf3f419bb68f0": "调用详细",
"e3fee05f688708b4": "LLM 调用",
@@ -298,6 +353,7 @@
"e5043c7a2b408271": "近10分钟",
"e59ae97924d62f01": "第一页",
"e5b9961a0d5242e3": "端口设置已保存,重启软件后生效",
+ "e5c84c9aa7826566": "未就绪",
"e77e3d58b0dcffaa": "耗时",
"e825a2a42c22380e": "模型类型",
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
@@ -306,16 +362,18 @@
"eb11e2df1d8ae387": "上游地址",
"eb1be07f2ca6e506": "按 Claude Opus 4.7 价格估算。",
"eb77492c9f76a7e1": "安装命令已自动复制。点击“打开终端”,将命令粘贴到终端中执行,并按提示输入密码。",
+ "eba54690937bc532": "账号管理",
"ed31fbb483ee1b0a": "操作",
"edc70de18c6da1a6": "安装本地 CA",
"ee239f3943293f87": "周日",
"ee6b89a6a740a4c4": "端口被占用时会自动选择新的随机端口并保存。修改后需要重启软件才会生效。",
- "f2bdc88464c51c2e": "显示 API Key",
"f4694c46b1e19602": "最终请求类型",
"f4dcb6a3ceb32247": "第 {page} / {count} 页",
"f4fa9f31ea2ae58d": "过去一年的 Token 用量日历",
"f50276449943286c": "结束时间",
"f69273dbbebfb3a1": "格式化",
+ "f6dc1b1641600dd0": "正在导入…",
+ "f78265089144369a": "插件配置",
"f78413c36d36f090": "默认跟随操作系统;不支持的系统语言使用英文。当前:{language}",
"f85537d1fd2ef6f6": "自定义概览筛选",
"f95ea7f4c063eea7": "未启用",
@@ -323,11 +381,14 @@
"f9aa11dbb15ce647": "周六",
"f9b55ca75425161b": "未记录响应内容,请开启详细记录后重试。",
"fa5b4b8a751c7d1b": "Cursor 使用的本地代理端口;填写 0 时启动时随机选择。",
+ "fad86bf65f72c747": "下载进度",
"fb11aa6f29827095": "检查中…",
"fbe8778fa8b9bab5": "需要先初始化本地 CA",
+ "fc22d1ab9ac73c6f": "正在检查插件运行时",
"fc3947ebe6b2177b": "默认 {defaultRate} / 计入创建 {reuseRate}",
"fcd311fd8ad42462": "打开模型列表",
"fd415f8e0097c832": "缓存读写已计入提示词侧统计。",
+ "fd77192739703811": "批量导入",
"fdc4cabc370fa3f7": "暂无选项",
"fea405f9b01d1416": "概览",
"fec45092945f8790": "使用教程",
diff --git a/apps/desktop/src/shared/api.ts b/apps/desktop/src/shared/api.ts
index 5a249f0..03d8e22 100644
--- a/apps/desktop/src/shared/api.ts
+++ b/apps/desktop/src/shared/api.ts
@@ -148,6 +148,162 @@ export interface DesktopSettings {
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;
+}
+
+/** 插件提供的显示文本:纯字符串或 locale → 文本映射。 */
+export type PluginLocalizedText = string | Record;
+
+export function pluginText(value: PluginLocalizedText | null | undefined, locale: string): string {
+ if (!value) return "";
+ if (typeof value === "string") return value;
+ if (value[locale]) return value[locale];
+ const language = locale.split("-")[0].toLowerCase();
+ for (const [key, text] of Object.entries(value)) {
+ const normalized = key.toLowerCase();
+ if (normalized === language || normalized.startsWith(`${language}-`)) return text;
+ }
+ return value["en-US"] ?? value["en"] ?? Object.values(value)[0] ?? "";
+}
+
+export interface PluginResourceState {
+ status: "ready" | "cooling" | "invalid";
+ retryAtMs?: number | null;
+ message?: string | null;
+}
+
+export interface PluginResourceMetric {
+ id: string;
+ label: PluginLocalizedText;
+ unit: "percent" | "count";
+ value: number;
+ resetAtMs?: number | null;
+}
+
+export interface PluginResourceView {
+ id: string;
+ state: PluginResourceState;
+ displayName: string;
+ description: PluginLocalizedText | null;
+ metrics: PluginResourceMetric[];
+ createdAtMs: number;
+}
+
+export interface PluginAddMethod {
+ type: "oauth2.0";
+ id: string;
+ displayName: PluginLocalizedText;
+ description: PluginLocalizedText | null;
+}
+
+export interface PluginImportDescriptor {
+ displayName: PluginLocalizedText;
+ description: PluginLocalizedText | null;
+ accept: string[];
+ multiple: boolean;
+}
+
+export interface PluginResourceDescriptor {
+ type: string;
+ displayName: PluginLocalizedText;
+ add: PluginAddMethod[];
+ import: PluginImportDescriptor | null;
+ canRefresh: boolean;
+ canRemove: boolean;
+ resources: PluginResourceView[];
+}
+
+export interface PluginModelDescriptor {
+ id: string;
+ pluginId: string;
+ pluginName: string;
+ providerId: string;
+ modelId: string;
+ displayName: string;
+ description: string | null;
+ icon: string;
+ providerType: string;
+ contextWindowTokens: number | null;
+ maxOutputTokens: number | null;
+ thinking: boolean;
+ images: boolean;
+}
+
+export interface PluginProviderDescriptor {
+ id: string;
+ pluginId: string;
+ displayName: PluginLocalizedText;
+ description: PluginLocalizedText | null;
+ providerType: string;
+ resourceType: string | null;
+ hasModels: boolean;
+ configured: boolean;
+ models: PluginModelDescriptor[];
+}
+
+export interface PluginDescriptor {
+ id: string;
+ name: string;
+ version: string;
+ author: string | null;
+ icon: string;
+ providers: PluginProviderDescriptor[];
+ resources: PluginResourceDescriptor[];
+}
+
+export interface PluginOAuthBegin {
+ sessionId: string;
+ userCode: string;
+ verificationUrl: string;
+ verificationUrlComplete: string | null;
+ expiresAtMs: number;
+ pollIntervalMs: number;
+}
+
+export type PluginOAuthPoll =
+ | { status: "pending"; pollIntervalMs: number }
+ | { status: "completed"; added: number; updated: number; modelSyncError: string | null }
+ | { status: "denied"; message: string | null }
+ | { status: "failed"; message: string };
+
+export interface PluginImportFile {
+ name: string;
+ content: string;
+}
+
+export interface PluginImportResult {
+ added: number;
+ updated: number;
+ warnings: string[];
+ modelSyncError: string | null;
+}
+
+export type ConfiguredModel =
+ | { kind: "builtin"; id: string; name: string; builtin: Model }
+ | { kind: "plugin"; id: string; name: string; plugin: PluginModelDescriptor };
+
+export function configuredPluginModels(plugins: PluginDescriptor[]): PluginModelDescriptor[] {
+ return plugins.flatMap((plugin) =>
+ plugin.providers.flatMap((provider) => provider.configured ? provider.models : []));
+}
+
+export function configuredModels(models: Model[], plugins: PluginDescriptor[]): ConfiguredModel[] {
+ return [
+ ...models.map((model): ConfiguredModel => ({ kind: "builtin", id: model.model_hash, name: model.display_name, builtin: model })),
+ ...configuredPluginModels(plugins).map((model): ConfiguredModel => ({ kind: "plugin", id: model.id, name: model.displayName, plugin: model })),
+ ];
+}
+
export interface OverviewMetrics {
llm_calls: number;
successful_calls: number;
@@ -295,8 +451,8 @@ export const api = {
importV0049Models: () => request("/models/import-v0049", { method: "POST" }),
updateModel: (hash: string, model: ModelInput) => request(`/models/${hash}`, { method: "PUT", body: JSON.stringify(model) }),
deleteModel: (hash: string) => request(`/models/${hash}`, { method: "DELETE" }),
- testModel: (hash: string, testId: string, signal?: AbortSignal) => request(`/models/${hash}/test/${encodeURIComponent(testId)}`, { method: "POST", signal }),
- cancelModelTest: (hash: string, testId: string) => request(`/models/${hash}/test/${encodeURIComponent(testId)}`, { method: "DELETE" }),
+ testModel: (hash: string, testId: string, signal?: AbortSignal) => request(`/models/${encodeURIComponent(hash)}/test/${encodeURIComponent(testId)}`, { method: "POST", signal }),
+ cancelModelTest: (hash: string, testId: string) => request(`/models/${encodeURIComponent(hash)}/test/${encodeURIComponent(testId)}`, { method: "DELETE" }),
overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[] }) => {
const params = new URLSearchParams();
if (filter) {
@@ -309,6 +465,18 @@ export const api = {
},
cursorHarness: () => request("/harness/cursor/status"),
initializeCursorCa: () => request("/harness/cursor/ca/initialize", { method: "POST" }),
+ plugins: () => request("/plugins"),
+ pluginOAuthBegin: (pluginId: string, resourceType: string, methodId: string) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/add/${encodeURIComponent(methodId)}/begin`, { method: "POST" }),
+ pluginOAuthPoll: (sessionId: string, signal?: AbortSignal) => request(`/plugins/oauth/${encodeURIComponent(sessionId)}/poll`, { method: "POST", signal }),
+ importPluginResources: (pluginId: string, resourceType: string, files: PluginImportFile[]) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/import`, { method: "POST", body: JSON.stringify(files) }),
+ refreshPluginResource: (pluginId: string, resourceType: string, resourceId: string) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/refresh`, { method: "POST" }),
+ deletePluginResource: (pluginId: string, resourceType: string, resourceId: string) => request(`/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}`, { method: "DELETE" }),
+ syncPluginModels: (pluginId: string, providerId: string) => request<{ models: number }>(`/plugins/${encodeURIComponent(pluginId)}/providers/${encodeURIComponent(providerId)}/models/sync`, { method: "POST" }),
+ pluginResourceExportUrl: (servicePort: number, pluginId: string, resourceType: string) => `http://127.0.0.1:${servicePort}${API_ROOT}/plugins/${encodeURIComponent(pluginId)}/resources/${encodeURIComponent(resourceType)}/export`,
+ removePluginConfiguration: (pluginId: string) => request(`/plugins/${encodeURIComponent(pluginId)}`, { method: "DELETE" }),
+ pluginRuntime: () => request("/plugins/runtime"),
+ initializePluginRuntime: () => request("/plugins/runtime", { method: "POST" }),
+ cancelPluginRuntimeInitialization: () => request("/plugins/runtime", { method: "DELETE" }),
openCursorCaInstallTerminal: async (command: string) => {
if (!packagedDesktop) throw new Error(t("请在桌面应用中打开终端安装 CA"));
const { invoke } = await import("@tauri-apps/api/core");
diff --git a/apps/desktop/src/shared/store/appStore.ts b/apps/desktop/src/shared/store/appStore.ts
index 9a6f192..05e706a 100644
--- a/apps/desktop/src/shared/store/appStore.ts
+++ b/apps/desktop/src/shared/store/appStore.ts
@@ -1,5 +1,5 @@
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 PluginDescriptor, type PluginRuntimeStatus, type PortSettings } from "../api";
import { applyTheme, isThemeId, type ThemeId } from "../theme/theme";
export type AppSnapshot = {
@@ -13,6 +13,8 @@ export type AppSnapshot = {
theme: ThemeId;
cursorHarness: CursorHarnessStatus | null;
cursorBusy: boolean;
+ pluginRuntime: PluginRuntimeStatus | null;
+ plugins: PluginDescriptor[];
};
const savedTheme = (): ThemeId => {
@@ -45,6 +47,8 @@ let snapshot: AppSnapshot = {
theme: savedTheme(),
cursorHarness: null,
cursorBusy: false,
+ pluginRuntime: null,
+ plugins: [],
};
const listeners = new Set<() => void>();
@@ -73,15 +77,17 @@ export const appStore = {
async refresh() {
update({ busy: true, error: null });
try {
- const [models, calls, overview, settings, ports, cursorHarness] = await Promise.all([
+ const [models, calls, overview, settings, ports, cursorHarness, pluginRuntime, plugins] = await Promise.all([
api.models(),
api.calls(),
api.overview(),
api.observability(),
api.ports(),
api.cursorHarness(),
+ api.pluginRuntime(),
+ api.plugins(),
]);
- update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness });
+ update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness, pluginRuntime, plugins });
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
} finally {
@@ -107,6 +113,55 @@ export const appStore = {
return null;
} 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 wasReady = snapshot.pluginRuntime?.state === "ready";
+ const pluginRuntime = await api.pluginRuntime();
+ update({ pluginRuntime });
+ if (!wasReady && pluginRuntime.state === "ready") {
+ const plugins = await api.plugins();
+ update({ plugins });
+ }
+ 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 refreshPlugins() {
+ try {
+ update({ plugins: await api.plugins() });
+ } catch (cause) {
+ update({ error: cause instanceof Error ? cause.message : String(cause) });
+ }
+ },
+ async removePluginConfiguration(pluginId: string) {
+ await perform(async () => {
+ await api.removePluginConfiguration(pluginId);
+ update({ plugins: await api.plugins() });
+ });
+ },
async setCursorEnabled(enabled: boolean) {
update({ cursorBusy: true, error: null });
try { update({ cursorHarness: await api.setCursorEnabled(enabled) }); }
diff --git a/apps/desktop/src/shared/ui/ActionMenu.module.scss b/apps/desktop/src/shared/ui/ActionMenu.module.scss
new file mode 100644
index 0000000..3eb10b8
--- /dev/null
+++ b/apps/desktop/src/shared/ui/ActionMenu.module.scss
@@ -0,0 +1,41 @@
+@use "../../styles/typography" as type;
+
+.menu {
+ position: fixed;
+ z-index: 14000;
+ min-width: 132px;
+ overflow: hidden;
+ padding: 4px;
+ background: var(--vscode-dropdown-background);
+ border: 1px solid var(--vscode-dropdown-border);
+ border-radius: 6px;
+ box-shadow: var(--oa-dropdown-shadow);
+
+ button {
+ width: 100%;
+ min-height: 30px;
+ display: block;
+ padding: 5px 8px;
+ color: var(--vscode-dropdown-foreground);
+ text-align: left;
+ background: transparent;
+ border: 0;
+ border-radius: 4px;
+ font-size: type.$font-size-xs;
+ white-space: nowrap;
+
+ &:hover:not(:disabled) {
+ background: var(--vscode-list-activeSelectionBackground);
+ color: var(--vscode-list-activeSelectionForeground);
+ }
+
+ &:disabled {
+ color: var(--vscode-descriptionForeground);
+ cursor: not-allowed;
+ }
+ }
+}
+
+.openIcon {
+ transform: rotate(180deg);
+}
diff --git a/apps/desktop/src/shared/ui/ActionMenu.tsx b/apps/desktop/src/shared/ui/ActionMenu.tsx
new file mode 100644
index 0000000..c335e46
--- /dev/null
+++ b/apps/desktop/src/shared/ui/ActionMenu.tsx
@@ -0,0 +1,99 @@
+import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
+import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { Button } from "./Button";
+import { Icon } from "./Icon";
+import { chevronDownIcon } from "./icons";
+import styles from "./ActionMenu.module.scss";
+
+export type ActionMenuItem = {
+ id: string;
+ label: string;
+ disabled?: boolean;
+ onSelect: () => void;
+};
+
+/** 触发器 + 动作列表的下拉菜单,用于容纳卡片上的次要操作。 */
+export function ActionMenu({ label, items, disabled }: {
+ label: string;
+ items: ActionMenuItem[];
+ disabled?: boolean;
+}) {
+ const trigger = useRef(null);
+ const menu = useRef(null);
+ const menuId = useId();
+ const [open, setOpen] = useState(false);
+ const [position, setPosition] = useState({ left: 0, top: 0 });
+
+ useLayoutEffect(() => {
+ if (!open || !trigger.current || !menu.current) return;
+ return autoUpdate(trigger.current, menu.current, () =>
+ void computePosition(trigger.current!, menu.current!, {
+ placement: "bottom-end",
+ middleware: [offset(5), flip({ padding: 10 }), shift({ padding: 10 })],
+ }).then(({ x, y }) => setPosition({ left: x, top: y })));
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const outside = (event: PointerEvent) => {
+ if (!trigger.current?.contains(event.target as Node) && !menu.current?.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+ document.addEventListener("pointerdown", outside);
+ return () => document.removeEventListener("pointerdown", outside);
+ }, [open]);
+
+ const close = () => {
+ setOpen(false);
+ trigger.current?.focus();
+ };
+
+ return <>
+
+ {open && createPortal(
+ ,
+ document.body,
+ )}
+ >;
+}
diff --git a/apps/desktop/src/shared/ui/Button.tsx b/apps/desktop/src/shared/ui/Button.tsx
index 3b9d73a..d40b667 100644
--- a/apps/desktop/src/shared/ui/Button.tsx
+++ b/apps/desktop/src/shared/ui/Button.tsx
@@ -1,4 +1,4 @@
-import type { ButtonHTMLAttributes } from "react";
+import type { ComponentProps } from "react";
import controls from "./Controls.module.scss";
export type ButtonVariant = "primary" | "secondary";
@@ -10,7 +10,7 @@ export function Button({
className,
type = "button",
...props
-}: ButtonHTMLAttributes & {
+}: ComponentProps<"button"> & {
variant?: ButtonVariant;
size?: ButtonSize;
}) {
diff --git a/apps/desktop/src/shared/ui/Controls.module.scss b/apps/desktop/src/shared/ui/Controls.module.scss
index e7df9e1..e2a5aea 100644
--- a/apps/desktop/src/shared/ui/Controls.module.scss
+++ b/apps/desktop/src/shared/ui/Controls.module.scss
@@ -8,6 +8,7 @@
background: var(--vscode-button-background);
border: 0;
border-radius: 5px;
+ &:disabled { opacity: .5; cursor: not-allowed; }
}
.secondary {
diff --git a/apps/desktop/src/shared/ui/FormControls.tsx b/apps/desktop/src/shared/ui/FormControls.tsx
index 143d6c9..60bd316 100644
--- a/apps/desktop/src/shared/ui/FormControls.tsx
+++ b/apps/desktop/src/shared/ui/FormControls.tsx
@@ -12,7 +12,7 @@ export function SecretTextInput({ className, ...props }: InputHTMLAttributes
- ;
diff --git a/apps/desktop/src/shared/ui/Modal.tsx b/apps/desktop/src/shared/ui/Modal.tsx
index b25e0ee..9c38521 100644
--- a/apps/desktop/src/shared/ui/Modal.tsx
+++ b/apps/desktop/src/shared/ui/Modal.tsx
@@ -21,6 +21,7 @@ type ModalProps = {
secondaryAction?: ReactNode;
closeLabel?: string;
submitLabel?: string;
+ submitDisabled?: boolean;
};
const focusableSelector = [
@@ -37,7 +38,7 @@ function focusableElements(root: HTMLElement) {
.filter((element) => element.getClientRects().length > 0);
}
-export function Modal({ id, open, title, children, banner, busy, wide, fullHeight, role = "dialog", ariaDescribedBy, initialFocus = "first", onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存") }: ModalProps) {
+export function Modal({ id, open, title, children, banner, busy, wide, fullHeight, role = "dialog", ariaDescribedBy, initialFocus = "first", onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存"), submitDisabled = false }: ModalProps) {
const dialog = useRef
(null);
const submitButton = useRef(null);
const closeRef = useRef(onClose);
@@ -95,7 +96,7 @@ export function Modal({ id, open, title, children, banner, busy, wide, fullHeigh
, document.body);
diff --git a/apps/desktop/src/shared/ui/Select.tsx b/apps/desktop/src/shared/ui/Select.tsx
index bf67ccc..899d3cb 100644
--- a/apps/desktop/src/shared/ui/Select.tsx
+++ b/apps/desktop/src/shared/ui/Select.tsx
@@ -7,7 +7,7 @@ import { Icon, type IconProps } from "./Icon";
import { checkIcon, chevronDownIcon } from "./icons";
import styles from "./Select.module.scss";
-export type SelectOption = { value: string; label: string; icon?: IconProps["icon"] };
+export type SelectOption = { value: string; label: string; icon?: IconProps["icon"]; iconSrc?: string };
export function Select({ value, options, disabled, ariaLabel, onChange }: { value: string; options: SelectOption[]; disabled?: boolean; ariaLabel: string; onChange: (value: string) => void }) {
const button = useRef