mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:40:50 +08:00
refactor: replace Button with TruncatedButton in CursorModelCards and PluginManagementPage
- Updated the UI components in CursorModelCards and PluginManagementPage to use TruncatedButton for better text handling and display. - Adjusted styles in CursorSettings and PluginManagementPage to ensure proper button layout and responsiveness. - Added new ActionMenu component for handling additional actions in PluginManagementPage. - Enhanced localization files to include new strings for the ActionMenu and TruncatedButton components.
This commit is contained in:
@@ -175,7 +175,12 @@ pub fn run() -> ExitCode {
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
Some(vec![AUTOSTART_ARG]),
|
||||
))?;
|
||||
let config = Config::desktop()?;
|
||||
let config = {
|
||||
let mut config = Config::desktop()?;
|
||||
// 插件的 minAppVersion 按桌面应用版本判定,而不是内嵌 server 库的版本。
|
||||
config.app_version = env!("CARGO_PKG_VERSION").into();
|
||||
config
|
||||
};
|
||||
#[cfg(dev)]
|
||||
let config = {
|
||||
let mut config = config;
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { IconifyIcon } from "@iconify/react/offline";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import Sortable from "sortablejs";
|
||||
import type { Model, PluginModelDescriptor } from "../../shared/api";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Card } from "../../shared/ui/Card";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { chevronDownIcon, chevronRightIcon, claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../../shared/ui/icons";
|
||||
import { TruncatedButton } from "../../shared/ui/TruncatedButton";
|
||||
import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult";
|
||||
import styles from "./CursorSettings.module.scss";
|
||||
|
||||
@@ -147,10 +147,10 @@ function ModelListRow({ model, disabled, testing, result, onTest, onEdit, onDupl
|
||||
</div>
|
||||
<CursorModelTestResult compact state={result} testing={testing} />
|
||||
<div className={styles.modelCardActions}>
|
||||
<Button size="small" disabled={disabled && !testing} onClick={onTest}>{testing ? t("取消测试") : t("测试")}</Button>
|
||||
<Button size="small" disabled={disabled} onClick={onEdit}>{t("编辑")}</Button>
|
||||
<Button size="small" disabled={disabled} onClick={onDuplicate}>{t("复制")}</Button>
|
||||
<Button size="small" className={styles.deleteButton} disabled={disabled} onClick={onDelete}>{t("删除")}</Button>
|
||||
<TruncatedButton size="small" disabled={disabled && !testing} label={testing ? t("取消测试") : t("测试")} onClick={onTest} />
|
||||
<TruncatedButton size="small" disabled={disabled} label={t("编辑")} onClick={onEdit} />
|
||||
<TruncatedButton size="small" disabled={disabled} label={t("复制")} onClick={onDuplicate} />
|
||||
<TruncatedButton size="small" className={styles.deleteButton} disabled={disabled} label={t("删除")} onClick={onDelete} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -170,8 +170,8 @@ function PluginModelRow({ model, disabled, testing, result, onTest, onSettings }
|
||||
</div>
|
||||
<CursorModelTestResult compact state={result} testing={testing} />
|
||||
<div className={styles.modelCardActions}>
|
||||
<Button size="small" disabled={disabled && !testing} onClick={onTest}>{testing ? t("取消测试") : t("测试")}</Button>
|
||||
<Button size="small" disabled={disabled} onClick={onSettings}>{t("设置")}</Button>
|
||||
<TruncatedButton size="small" disabled={disabled && !testing} label={testing ? t("取消测试") : t("测试")} onClick={onTest} />
|
||||
<TruncatedButton size="small" disabled={disabled} label={t("设置")} onClick={onSettings} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -261,10 +261,10 @@ function ModelGrid({
|
||||
<CursorModelTestResult state={result} testing={testing} />
|
||||
</div>
|
||||
<div className={styles.modelCardActions}>
|
||||
<Button size="small" disabled={disabled && !testing} onClick={() => onTest(model)}>{testing ? t("取消测试") : t("测试")}</Button>
|
||||
<Button size="small" disabled={disabled} onClick={() => onEdit(model)}>{t("编辑")}</Button>
|
||||
<Button size="small" disabled={disabled} onClick={() => onDuplicate(model)}>{t("复制")}</Button>
|
||||
<Button size="small" className={styles.deleteButton} disabled={disabled} onClick={() => onDelete(model)}>{t("删除")}</Button>
|
||||
<TruncatedButton size="small" disabled={disabled && !testing} label={testing ? t("取消测试") : t("测试")} onClick={() => onTest(model)} />
|
||||
<TruncatedButton size="small" disabled={disabled} label={t("编辑")} onClick={() => onEdit(model)} />
|
||||
<TruncatedButton size="small" disabled={disabled} label={t("复制")} onClick={() => onDuplicate(model)} />
|
||||
<TruncatedButton size="small" className={styles.deleteButton} disabled={disabled} label={t("删除")} onClick={() => onDelete(model)} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>;
|
||||
|
||||
@@ -183,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);
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border: 1px solid var(--vscode-sideBar-border);
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
.pluginIdentity {
|
||||
@@ -98,8 +98,21 @@
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 7px;
|
||||
|
||||
> button {
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 主操作靠左,"更多"推到行尾,两端对齐。
|
||||
.moreAction {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.gate,
|
||||
|
||||
@@ -3,11 +3,12 @@ import { api, pluginText, type PluginDescriptor, type PluginImportFile, type Plu
|
||||
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 { Icon } from "../../shared/ui/Icon";
|
||||
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";
|
||||
|
||||
@@ -167,43 +168,93 @@ function PluginCard({ plugin, onOpen }: {
|
||||
}
|
||||
};
|
||||
|
||||
return <Card className={styles.pluginCard}>
|
||||
<div className={styles.pluginCardTop}>
|
||||
<span className={styles.pluginIcon}><Icon src={plugin.icon} size="1.75em" /></span>
|
||||
<div className={styles.pluginIdentity}>
|
||||
<span className={styles.pluginName}>{plugin.name}</span>
|
||||
<span className={styles.pluginId}>{subtitle}</span>
|
||||
return (
|
||||
<Card className={styles.pluginCard}>
|
||||
<div className={styles.pluginCardTop}>
|
||||
<img className={styles.pluginIcon} src={plugin.icon} />
|
||||
<div className={styles.pluginIdentity}>
|
||||
<span className={styles.pluginName}>{plugin.name}</span>
|
||||
<span className={styles.pluginId}>{subtitle}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`${styles.stateBadge} ${configured ? styles.stateReady : ""}`}
|
||||
>
|
||||
{configured ? t("已配置") : t("未配置")}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`${styles.stateBadge} ${configured ? styles.stateReady : ""}`}>
|
||||
{configured ? t("已配置") : t("未配置")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.pluginMeta}>
|
||||
<span>{t("{accounts} 个账号 · {models} 个模型", { accounts: accountCount, models: modelCount })}</span>
|
||||
{plugin.author && <span className={styles.pluginAuthor}>{plugin.author}</span>}
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
<Button size="small" variant="primary" onClick={() => onOpen(plugin.id, "add")}>{t("添加账号")}</Button>
|
||||
{configured && <Button size="small" onClick={() => onOpen(plugin.id, "settings")}>{t("账号管理")}</Button>}
|
||||
{importResource && <Button size="small" disabled={importing} onClick={() => importInput.current?.click()}>
|
||||
{importing ? t("正在导入…") : t("批量导入")}
|
||||
</Button>}
|
||||
{exportResource && <Button
|
||||
size="small"
|
||||
onClick={() => void api.openExternalUrl(api.pluginResourceExportUrl(ports.service_port, plugin.id, exportResource.type))}
|
||||
>
|
||||
{t("批量导出")}
|
||||
</Button>}
|
||||
{importResource && <input
|
||||
ref={importInput}
|
||||
type="file"
|
||||
hidden
|
||||
accept={importResource.import?.accept.join(",")}
|
||||
multiple={importResource.import?.multiple ?? false}
|
||||
onChange={(event) => void importFiles(event.target.files)}
|
||||
/>}
|
||||
</div>
|
||||
</Card>;
|
||||
<div className={styles.pluginMeta}>
|
||||
<span>
|
||||
{t("{accounts} 个账号 · {models} 个模型", {
|
||||
accounts: accountCount,
|
||||
models: modelCount,
|
||||
})}
|
||||
</span>
|
||||
<span className={styles.pluginAuthor}>
|
||||
{[`v${plugin.version}`, plugin.author].filter(Boolean).join(" · ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
<TruncatedButton
|
||||
size="small"
|
||||
variant="primary"
|
||||
label={t("添加账号")}
|
||||
onClick={() => onOpen(plugin.id, "add")}
|
||||
/>
|
||||
{configured && (
|
||||
<TruncatedButton
|
||||
size="small"
|
||||
label={t("账号管理")}
|
||||
onClick={() => onOpen(plugin.id, "settings")}
|
||||
/>
|
||||
)}
|
||||
{(importResource || exportResource) && (
|
||||
<span className={styles.moreAction}>
|
||||
<ActionMenu
|
||||
label={t("更多")}
|
||||
items={[
|
||||
...(importResource
|
||||
? [
|
||||
{
|
||||
id: "import",
|
||||
label: importing ? t("正在导入…") : t("批量导入"),
|
||||
disabled: importing,
|
||||
onSelect: () => importInput.current?.click(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(exportResource
|
||||
? [
|
||||
{
|
||||
id: "export",
|
||||
label: t("批量导出"),
|
||||
onSelect: () =>
|
||||
void api.openExternalUrl(
|
||||
api.pluginResourceExportUrl(
|
||||
ports.service_port,
|
||||
plugin.id,
|
||||
exportResource.type,
|
||||
),
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{importResource && (
|
||||
<input
|
||||
ref={importInput}
|
||||
type="file"
|
||||
hidden
|
||||
accept={importResource.import?.accept.join(",")}
|
||||
multiple={importResource.import?.multiple ?? false}
|
||||
onChange={(event) => void importFiles(event.target.files)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeProgressModal({ open, status, starting, onClose }: { open: boolean; status: PluginRuntimeStatus | null; starting: boolean; onClose: () => void }) {
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 251,
|
||||
"line": 259,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -225,7 +225,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 182,
|
||||
"line": 183,
|
||||
"column": 14
|
||||
}
|
||||
]
|
||||
@@ -594,7 +594,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 120,
|
||||
"line": 121,
|
||||
"column": 16
|
||||
}
|
||||
]
|
||||
@@ -638,7 +638,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 154,
|
||||
"line": 155,
|
||||
"column": 23
|
||||
}
|
||||
]
|
||||
@@ -878,7 +878,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 235,
|
||||
"line": 243,
|
||||
"column": 122
|
||||
}
|
||||
]
|
||||
@@ -996,7 +996,7 @@
|
||||
},
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 223,
|
||||
"line": 231,
|
||||
"column": 70
|
||||
},
|
||||
{
|
||||
@@ -1318,6 +1318,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"38844b135cf70dfc": {
|
||||
"source": "更多",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 190,
|
||||
"column": 16
|
||||
}
|
||||
]
|
||||
},
|
||||
"393e1241552b1870": {
|
||||
"source": "请求",
|
||||
"kind": "text",
|
||||
@@ -1510,7 +1522,7 @@
|
||||
},
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 223,
|
||||
"line": 231,
|
||||
"column": 80
|
||||
}
|
||||
]
|
||||
@@ -1522,7 +1534,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 109,
|
||||
"line": 110,
|
||||
"column": 68
|
||||
}
|
||||
]
|
||||
@@ -1786,7 +1798,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 222,
|
||||
"line": 230,
|
||||
"column": 12
|
||||
}
|
||||
]
|
||||
@@ -1810,7 +1822,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 109,
|
||||
"line": 110,
|
||||
"column": 46
|
||||
}
|
||||
]
|
||||
@@ -2152,7 +2164,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 215,
|
||||
"line": 223,
|
||||
"column": 7
|
||||
}
|
||||
]
|
||||
@@ -2682,7 +2694,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 250,
|
||||
"line": 258,
|
||||
"column": 31
|
||||
}
|
||||
]
|
||||
@@ -2788,7 +2800,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 178,
|
||||
"line": 179,
|
||||
"column": 34
|
||||
}
|
||||
]
|
||||
@@ -2812,7 +2824,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 102,
|
||||
"line": 103,
|
||||
"column": 25
|
||||
}
|
||||
]
|
||||
@@ -2842,7 +2854,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 235,
|
||||
"line": 243,
|
||||
"column": 20
|
||||
}
|
||||
]
|
||||
@@ -2958,7 +2970,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 247,
|
||||
"line": 255,
|
||||
"column": 32
|
||||
}
|
||||
]
|
||||
@@ -3131,7 +3143,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 186,
|
||||
"line": 187,
|
||||
"column": 88
|
||||
}
|
||||
]
|
||||
@@ -3167,7 +3179,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 249,
|
||||
"line": 257,
|
||||
"column": 31
|
||||
}
|
||||
]
|
||||
@@ -3302,12 +3314,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 95,
|
||||
"line": 96,
|
||||
"column": 9
|
||||
},
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 217,
|
||||
"line": 225,
|
||||
"column": 9
|
||||
}
|
||||
]
|
||||
@@ -3345,7 +3357,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 77,
|
||||
"line": 78,
|
||||
"column": 11
|
||||
}
|
||||
]
|
||||
@@ -3357,7 +3369,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 121,
|
||||
"line": 122,
|
||||
"column": 14
|
||||
}
|
||||
]
|
||||
@@ -3369,7 +3381,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 248,
|
||||
"line": 256,
|
||||
"column": 30
|
||||
}
|
||||
]
|
||||
@@ -3383,7 +3395,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 156,
|
||||
"line": 157,
|
||||
"column": 17
|
||||
},
|
||||
{
|
||||
@@ -3630,7 +3642,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 78,
|
||||
"line": 79,
|
||||
"column": 11
|
||||
}
|
||||
]
|
||||
@@ -3876,12 +3888,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 100,
|
||||
"line": 101,
|
||||
"column": 7
|
||||
},
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 238,
|
||||
"line": 246,
|
||||
"column": 70
|
||||
}
|
||||
]
|
||||
@@ -3905,7 +3917,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 98,
|
||||
"line": 99,
|
||||
"column": 11
|
||||
}
|
||||
]
|
||||
@@ -4158,7 +4170,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 109,
|
||||
"line": 110,
|
||||
"column": 83
|
||||
}
|
||||
]
|
||||
@@ -4196,7 +4208,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 223,
|
||||
"line": 231,
|
||||
"column": 45
|
||||
}
|
||||
]
|
||||
@@ -4239,7 +4251,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 97,
|
||||
"line": 98,
|
||||
"column": 11
|
||||
}
|
||||
]
|
||||
@@ -4369,7 +4381,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 103,
|
||||
"line": 104,
|
||||
"column": 9
|
||||
}
|
||||
]
|
||||
@@ -4480,8 +4492,8 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 195,
|
||||
"column": 10
|
||||
"line": 200,
|
||||
"column": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -4605,7 +4617,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 239,
|
||||
"line": 247,
|
||||
"column": 44
|
||||
}
|
||||
]
|
||||
@@ -4728,7 +4740,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 178,
|
||||
"line": 179,
|
||||
"column": 23
|
||||
}
|
||||
]
|
||||
@@ -5146,7 +5158,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 187,
|
||||
"line": 188,
|
||||
"column": 90
|
||||
}
|
||||
]
|
||||
@@ -5274,8 +5286,8 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 189,
|
||||
"column": 22
|
||||
"line": 194,
|
||||
"column": 32
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -5286,7 +5298,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 64,
|
||||
"line": 65,
|
||||
"column": 14
|
||||
},
|
||||
{
|
||||
@@ -5391,7 +5403,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 230,
|
||||
"line": 238,
|
||||
"column": 23
|
||||
}
|
||||
]
|
||||
@@ -5403,7 +5415,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 109,
|
||||
"line": 110,
|
||||
"column": 19
|
||||
},
|
||||
{
|
||||
@@ -5432,12 +5444,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 93,
|
||||
"line": 94,
|
||||
"column": 7
|
||||
},
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 246,
|
||||
"line": 254,
|
||||
"column": 29
|
||||
}
|
||||
]
|
||||
@@ -5493,8 +5505,8 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "features/plugins/PluginManagementPage.tsx",
|
||||
"line": 189,
|
||||
"column": 35
|
||||
"line": 194,
|
||||
"column": 45
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"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",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"378bb0eec39fa8a2": "最后一页",
|
||||
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
|
||||
"382f2e3419a02fef": "仅清理详细记录",
|
||||
"38844b135cf70dfc": "更多",
|
||||
"393e1241552b1870": "请求",
|
||||
"398f8e6c6f0a0b97": "继续选择或输入",
|
||||
"39f52eee100131d7": "缓存输入",
|
||||
|
||||
@@ -254,6 +254,7 @@ export interface PluginProviderDescriptor {
|
||||
export interface PluginDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author: string | null;
|
||||
icon: string;
|
||||
providers: PluginProviderDescriptor[];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<HTMLButtonElement>(null);
|
||||
const menu = useRef<HTMLDivElement>(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 <>
|
||||
<Button
|
||||
ref={trigger}
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
aria-haspopup="menu"
|
||||
aria-controls={open ? menuId : undefined}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") close();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<Icon icon={chevronDownIcon} size="1em" className={open ? styles.openIcon : undefined} />
|
||||
</Button>
|
||||
{open && createPortal(
|
||||
<div
|
||||
id={menuId}
|
||||
ref={menu}
|
||||
className={styles.menu}
|
||||
role="menu"
|
||||
style={{ left: position.left, top: position.top }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{items.map((item) => <button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
item.onSelect();
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
@@ -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<HTMLButtonElement> & {
|
||||
}: ComponentProps<"button"> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
}) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useRef, useState, type ComponentProps } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { TooltipTrigger } from "./TooltipTrigger";
|
||||
import styles from "./TruncatedButton.module.scss";
|
||||
|
||||
/**
|
||||
* 文本被省略号截断时才显示完整文案悬浮提示的按钮。
|
||||
* 按钮是 flex 容器,省略号只作用在内层文本 span 上;
|
||||
* 截断在悬停/聚焦时现测——挂载时字体可能未加载,提前测会得到错误结果。
|
||||
*/
|
||||
export function TruncatedButton({ label, ...props }: ComponentProps<typeof Button> & { label: string }) {
|
||||
const element = useRef<HTMLSpanElement>(null);
|
||||
const [truncated, setTruncated] = useState(false);
|
||||
const measure = () => {
|
||||
const text = element.current;
|
||||
if (text) setTruncated(text.scrollWidth > text.clientWidth);
|
||||
};
|
||||
const button = <Button {...props} onPointerEnter={measure} onFocus={measure}>
|
||||
<span ref={element} className={styles.label}>{label}</span>
|
||||
</Button>;
|
||||
return truncated ? <TooltipTrigger label={label}>{button}</TooltipTrigger> : button;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ Plugins implement three capability interfaces defined by the core: **Provider**
|
||||
└── models-<provider>.json # model catalogs persisted by the core
|
||||
```
|
||||
|
||||
Built-in plugins live at `server/plugins/build-in/` (such as `codex-auth`); debug builds discover them automatically, while release builds only read `plugins/installed/`. When the user directory contains a plugin with the same ID, the user directory wins.
|
||||
Built-in plugin sources live at `server/plugins/build-in/` (such as `codex-auth`); they are bundled into the binary and pre-installed into `plugins/installed/` keyed by version. Debug builds load the source directory first so edits take effect immediately.
|
||||
|
||||
## Static manifest
|
||||
|
||||
@@ -30,6 +30,9 @@ Built-in plugins live at `server/plugins/build-in/` (such as `codex-auth`); debu
|
||||
"apiVersion": 1,
|
||||
"id": "com.example.subscription",
|
||||
"name": "Example Subscription",
|
||||
"version": "0.1.0",
|
||||
"author": "@example",
|
||||
"minAppVersion": "0.1.0",
|
||||
"icon": "assets/icon.svg",
|
||||
"entry": "main.ts",
|
||||
"permissions": {
|
||||
@@ -38,7 +41,7 @@ Built-in plugins live at `server/plugins/build-in/` (such as `codex-auth`); debu
|
||||
}
|
||||
```
|
||||
|
||||
`permissions.network` accepts exact hostnames only. All plugin network requests must use HTTPS and hit this allowlist.
|
||||
`permissions.network` accepts exact hostnames only. All plugin network requests must use HTTPS and hit this allowlist. `version` is required; the plugin is ignored when the app version is older than `minAppVersion`. Built-in plugins are pre-installed into `plugins/installed/` keyed by `version`: startup writes nothing when the version matches and resyncs the whole directory (pruning stale files) when it changes.
|
||||
|
||||
## Entry and capabilities
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ icon: Blocks
|
||||
└── models-<provider>.json # 核心持久化的模型目录
|
||||
```
|
||||
|
||||
内置插件位于 `server/plugins/build-in/`(如 `codex-auth`),Debug 构建自动发现;发布构建只读取 `plugins/installed/`。用户目录中存在同 ID 插件时,以用户目录为准。
|
||||
内置插件源码位于 `server/plugins/build-in/`(如 `codex-auth`),随二进制打包并按版本预装进 `plugins/installed/`;Debug 构建下源码目录优先加载,便于热改。
|
||||
|
||||
## 静态清单
|
||||
|
||||
@@ -30,6 +30,9 @@ icon: Blocks
|
||||
"apiVersion": 1,
|
||||
"id": "com.example.subscription",
|
||||
"name": "Example Subscription",
|
||||
"version": "0.1.0",
|
||||
"author": "@example",
|
||||
"minAppVersion": "0.1.0",
|
||||
"icon": "assets/icon.svg",
|
||||
"entry": "main.ts",
|
||||
"permissions": {
|
||||
@@ -38,7 +41,7 @@ icon: Blocks
|
||||
}
|
||||
```
|
||||
|
||||
`permissions.network` 只能包含精确主机名。所有插件网络请求都必须是 HTTPS 且命中该白名单。
|
||||
`permissions.network` 只能包含精确主机名。所有插件网络请求都必须是 HTTPS 且命中该白名单。`version` 必填;应用版本低于 `minAppVersion` 时插件会被忽略。内置插件按 `version` 预装进 `plugins/installed/`:版本一致时启动零写盘,版本变化时整目录同步并清理旧文件。
|
||||
|
||||
## 入口与能力
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"apiVersion": 1,
|
||||
"id": "dev.cursorbyok.examples.codex-auth",
|
||||
"name": "Codex",
|
||||
"version": "0.1.0",
|
||||
"author": "@leookun",
|
||||
"minAppVersion": "0.1.0",
|
||||
"icon": "assets/codex.svg",
|
||||
"entry": "main.ts",
|
||||
"permissions": {
|
||||
|
||||
+5
-1
@@ -39,7 +39,11 @@ impl App {
|
||||
let assets = PromptAssets::embedded()?;
|
||||
let compiler = PromptCompiler::new(assets);
|
||||
let plugin_runtime = PluginRuntime::managed()?;
|
||||
let plugins = PluginRegistry::managed(store.clone(), plugin_runtime.clone())?;
|
||||
let plugins = PluginRegistry::managed(
|
||||
store.clone(),
|
||||
plugin_runtime.clone(),
|
||||
config.app_version.clone(),
|
||||
)?;
|
||||
let provider = std::sync::Arc::new(ProviderRouter::new(
|
||||
store.clone(),
|
||||
plugins.clone(),
|
||||
|
||||
@@ -58,6 +58,8 @@ pub struct Config {
|
||||
pub provider_stream_idle_timeout: Duration,
|
||||
pub console: Option<ConsoleSource>,
|
||||
pub use_persisted_ports: bool,
|
||||
/// 面向用户的应用版本;桌面壳会覆盖为自身版本,用于插件 minAppVersion 门控。
|
||||
pub app_version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -109,6 +111,7 @@ impl Config {
|
||||
provider_stream_idle_timeout: DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT,
|
||||
console,
|
||||
use_persisted_ports: false,
|
||||
app_version: env!("CARGO_PKG_VERSION").into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,6 +125,7 @@ impl Config {
|
||||
provider_stream_idle_timeout: DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT,
|
||||
console: None,
|
||||
use_persisted_ports: true,
|
||||
app_version: env!("CARGO_PKG_VERSION").into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+108
-10
@@ -1,10 +1,10 @@
|
||||
//! Materializes built-in plugins bundled in the binary into the managed dir.
|
||||
use std::path::PathBuf;
|
||||
//! Pre-installs bundled built-in plugins into the user's installed directory.
|
||||
use std::path::Path;
|
||||
|
||||
use super::definition::write_if_changed;
|
||||
use crate::{config, Result};
|
||||
use crate::Result;
|
||||
|
||||
/// 随二进制打包的内置插件文件;发布构建没有源码目录,靠这里落盘。
|
||||
/// 随二进制打包的内置插件文件;发布构建没有源码目录,靠这里预装。
|
||||
const CODEX_AUTH: &[(&str, &str)] = &[
|
||||
(
|
||||
"plugin.json",
|
||||
@@ -57,14 +57,42 @@ const CODEX_AUTH: &[(&str, &str)] = &[
|
||||
),
|
||||
];
|
||||
|
||||
/// 把内置插件写入受管目录并返回该目录,作为插件目录的扫描根之一。
|
||||
pub(super) fn materialize() -> Result<PathBuf> {
|
||||
let root = config::managed_data_dir()?.join("plugins/build-in");
|
||||
write_plugin(&root.join("codex-auth"), CODEX_AUTH)?;
|
||||
Ok(root)
|
||||
const PLUGINS: &[(&str, &[(&str, &str)])] = &[("codex-auth", CODEX_AUTH)];
|
||||
|
||||
/// 把内置插件预装到 installed 目录。manifest 的 version 是缓存键:
|
||||
/// 版本一致时零写盘;版本变化时整目录同步并清理旧版本残留文件。
|
||||
pub(super) fn install(installed: &Path) -> Result<()> {
|
||||
for (name, files) in PLUGINS {
|
||||
let directory = installed.join(name);
|
||||
if disk_version(&directory) == Some(embedded_version(files)?) {
|
||||
continue;
|
||||
}
|
||||
write_plugin(&directory, files)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_plugin(directory: &std::path::Path, files: &[(&str, &str)]) -> Result<()> {
|
||||
fn embedded_version(files: &[(&str, &str)]) -> Result<String> {
|
||||
let manifest = files
|
||||
.iter()
|
||||
.find(|(name, _)| *name == "plugin.json")
|
||||
.map(|(_, content)| *content)
|
||||
.expect("built-in plugin bundles plugin.json");
|
||||
let value: serde_json::Value = serde_json::from_str(manifest)?;
|
||||
value
|
||||
.get("version")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| crate::Error::Config("built-in plugin manifest requires version".into()))
|
||||
}
|
||||
|
||||
fn disk_version(directory: &Path) -> Option<String> {
|
||||
let manifest = std::fs::read_to_string(directory.join("plugin.json")).ok()?;
|
||||
let value: serde_json::Value = serde_json::from_str(&manifest).ok()?;
|
||||
Some(value.get("version")?.as_str()?.to_owned())
|
||||
}
|
||||
|
||||
fn write_plugin(directory: &Path, files: &[(&str, &str)]) -> Result<()> {
|
||||
for (relative, content) in files {
|
||||
let path = directory.join(relative);
|
||||
let parent = path.parent().expect("plugin file path has a parent");
|
||||
@@ -81,5 +109,75 @@ fn write_plugin(directory: &std::path::Path, files: &[(&str, &str)]) -> Result<(
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
}
|
||||
prune_unknown_files(directory, directory, files)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除插件目录中不在嵌入清单里的文件与空目录(旧版本残留)。
|
||||
fn prune_unknown_files(root: &Path, directory: &Path, files: &[(&str, &str)]) -> Result<()> {
|
||||
for entry in std::fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if entry.file_type()?.is_dir() {
|
||||
prune_unknown_files(root, &path, files)?;
|
||||
if std::fs::read_dir(&path)?.next().is_none() {
|
||||
std::fs::remove_dir(&path)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let known = files
|
||||
.iter()
|
||||
.any(|(relative, _)| root.join(relative) == path);
|
||||
if !known {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn embedded_main() -> &'static str {
|
||||
CODEX_AUTH
|
||||
.iter()
|
||||
.find(|(name, _)| *name == "main.ts")
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_is_version_gated_and_syncs_on_version_change() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let plugin = root.path().join("codex-auth");
|
||||
|
||||
install(root.path()).unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(plugin.join("main.ts")).unwrap(),
|
||||
embedded_main()
|
||||
);
|
||||
|
||||
// 版本一致:本地改动与额外文件保持原样,不发生任何写盘。
|
||||
std::fs::write(plugin.join("main.ts"), "edited").unwrap();
|
||||
std::fs::write(plugin.join("stale.ts"), "extra").unwrap();
|
||||
install(root.path()).unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(plugin.join("main.ts")).unwrap(),
|
||||
"edited"
|
||||
);
|
||||
assert!(plugin.join("stale.ts").exists());
|
||||
|
||||
// 版本变化:整目录同步回嵌入内容并清理残留。
|
||||
let manifest = std::fs::read_to_string(plugin.join("plugin.json")).unwrap();
|
||||
let mut value: serde_json::Value = serde_json::from_str(&manifest).unwrap();
|
||||
value["version"] = serde_json::Value::String("0.0.1".into());
|
||||
std::fs::write(plugin.join("plugin.json"), value.to_string()).unwrap();
|
||||
install(root.path()).unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(plugin.join("main.ts")).unwrap(),
|
||||
embedded_main()
|
||||
);
|
||||
assert!(!plugin.join("stale.ts").exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const MAX_ICON_BYTES: u64 = 1024 * 1024;
|
||||
pub struct PluginCatalog {
|
||||
roots: Vec<PathBuf>,
|
||||
definition_loader: PluginDefinitionLoader,
|
||||
app_version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -33,7 +34,7 @@ pub(crate) struct PluginEntry {
|
||||
}
|
||||
|
||||
impl PluginCatalog {
|
||||
pub fn managed() -> Result<Self> {
|
||||
pub fn managed(app_version: String) -> Result<Self> {
|
||||
let installed = config::managed_data_dir()?.join("plugins/installed");
|
||||
fs::create_dir_all(&installed)?;
|
||||
#[cfg(unix)]
|
||||
@@ -41,15 +42,21 @@ impl PluginCatalog {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&installed, fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
// 扫描顺序即优先级:用户安装目录 > 源码内置目录(仅 debug,便于热改)
|
||||
// > 随二进制打包后落盘的内置目录;同 ID 时靠前的覆盖靠后的。
|
||||
let mut roots = vec![installed];
|
||||
// 内置插件按版本预装进 installed;版本一致时不写盘。
|
||||
super::builtin::install(&installed)?;
|
||||
// 扫描顺序即优先级:debug 下源码目录优先,保证内置插件热改生效;
|
||||
// 发布构建只有 installed 一个根。
|
||||
#[cfg(debug_assertions)]
|
||||
roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("plugins/build-in"));
|
||||
roots.push(super::builtin::materialize()?);
|
||||
let roots = vec![
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("plugins/build-in"),
|
||||
installed,
|
||||
];
|
||||
#[cfg(not(debug_assertions))]
|
||||
let roots = vec![installed];
|
||||
Ok(Self {
|
||||
roots,
|
||||
definition_loader: PluginDefinitionLoader::managed()?,
|
||||
app_version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +76,14 @@ impl PluginCatalog {
|
||||
};
|
||||
directories.sort();
|
||||
for directory in directories {
|
||||
match load_plugin(&directory, &self.definition_loader, executable).await {
|
||||
match load_plugin(
|
||||
&directory,
|
||||
&self.definition_loader,
|
||||
executable,
|
||||
&self.app_version,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(entry) => {
|
||||
if plugins.contains_key(&entry.manifest.id) {
|
||||
tracing::warn!(plugin = %entry.manifest.id, path = %directory.display(), "ignoring duplicate plugin");
|
||||
@@ -98,6 +112,7 @@ impl PluginCatalog {
|
||||
let manifest: PluginManifest =
|
||||
serde_json::from_slice(&fs::read(directory.join(MANIFEST_FILE_NAME))?)?;
|
||||
manifest.validate(&directory)?;
|
||||
require_app_version(&manifest, &self.app_version)?;
|
||||
let icon = icon_data_url(&directory, &manifest.icon)?;
|
||||
Ok((manifest, icon))
|
||||
})();
|
||||
@@ -126,14 +141,30 @@ fn child_directories(root: &Path) -> Result<Vec<PathBuf>> {
|
||||
Ok(directories)
|
||||
}
|
||||
|
||||
/// 应用过旧时拒绝加载,让插件的 minAppVersion 声明生效。
|
||||
fn require_app_version(manifest: &PluginManifest, app_version: &str) -> Result<()> {
|
||||
let Some(minimum) = &manifest.min_app_version else {
|
||||
return Ok(());
|
||||
};
|
||||
if super::manifest::version_at_least(app_version, minimum) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Config(format!(
|
||||
"plugin '{}' requires app version {minimum} or newer (current {app_version})",
|
||||
manifest.id
|
||||
)))
|
||||
}
|
||||
|
||||
async fn load_plugin(
|
||||
directory: &Path,
|
||||
loader: &PluginDefinitionLoader,
|
||||
executable: &Path,
|
||||
app_version: &str,
|
||||
) -> Result<PluginEntry> {
|
||||
let manifest: PluginManifest =
|
||||
serde_json::from_slice(&fs::read(directory.join(MANIFEST_FILE_NAME))?)?;
|
||||
manifest.validate(directory)?;
|
||||
require_app_version(&manifest, app_version)?;
|
||||
let icon = icon_data_url(directory, &manifest.icon)?;
|
||||
let entry = directory.join(&manifest.entry).canonicalize()?;
|
||||
let definition = loader.load(executable, directory, &entry).await?;
|
||||
@@ -279,6 +310,7 @@ mod tests {
|
||||
let catalog = PluginCatalog {
|
||||
roots: vec![root],
|
||||
definition_loader: PluginDefinitionLoader::for_test(sdk.path()).unwrap(),
|
||||
app_version: env!("CARGO_PKG_VERSION").into(),
|
||||
};
|
||||
assert!(!catalog.manifests().is_empty());
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ pub const OAUTH2_ADD_METHOD: &str = "oauth2.0";
|
||||
pub struct PluginDescriptor {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub icon: String,
|
||||
pub providers: Vec<PluginProviderDescriptor>,
|
||||
|
||||
@@ -14,8 +14,13 @@ pub struct PluginManifest {
|
||||
pub api_version: u32,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// 插件自身版本;内置插件预装时以它为缓存键决定是否重新落盘。
|
||||
pub version: String,
|
||||
#[serde(default)]
|
||||
pub author: Option<String>,
|
||||
/// 插件要求的最低应用版本;应用过旧时插件被忽略。
|
||||
#[serde(default)]
|
||||
pub min_app_version: Option<String>,
|
||||
pub icon: String,
|
||||
pub entry: String,
|
||||
#[serde(default)]
|
||||
@@ -39,6 +44,12 @@ impl PluginManifest {
|
||||
}
|
||||
validate_id(&self.id, "plugin id")?;
|
||||
required(&self.name, "plugin name")?;
|
||||
parse_version(&self.version)
|
||||
.ok_or_else(|| Error::Config(format!("invalid plugin version: {}", self.version)))?;
|
||||
if let Some(minimum) = &self.min_app_version {
|
||||
parse_version(minimum)
|
||||
.ok_or_else(|| Error::Config(format!("invalid plugin minAppVersion: {minimum}")))?;
|
||||
}
|
||||
validate_entry_path(directory, &self.entry)?;
|
||||
validate_asset_path(directory, &self.icon)?;
|
||||
let mut hosts = HashSet::new();
|
||||
@@ -55,6 +66,23 @@ impl PluginManifest {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 semver 的核心三段(忽略预发布/构建后缀),格式非法返回 None。
|
||||
pub(super) fn parse_version(value: &str) -> Option<(u64, u64, u64)> {
|
||||
let core = value.split(['-', '+']).next()?;
|
||||
let mut parts = core.split('.');
|
||||
let major = parts.next()?.parse().ok()?;
|
||||
let minor = parts.next()?.parse().ok()?;
|
||||
let patch = parts.next()?.parse().ok()?;
|
||||
parts.next().is_none().then_some((major, minor, patch))
|
||||
}
|
||||
|
||||
pub(super) fn version_at_least(actual: &str, minimum: &str) -> bool {
|
||||
match (parse_version(actual), parse_version(minimum)) {
|
||||
(Some(actual), Some(minimum)) => actual >= minimum,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn validate_id(value: &str, label: &str) -> Result<()> {
|
||||
static ID: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||
let expression = ID.get_or_init(|| Regex::new(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$").unwrap());
|
||||
@@ -172,4 +200,14 @@ mod tests {
|
||||
assert!(validate_network_host("example.com:443").is_err());
|
||||
assert!(validate_network_host("example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compares_semver_cores_and_ignores_prerelease_suffixes() {
|
||||
assert_eq!(parse_version("0.1.5-beta.1"), Some((0, 1, 5)));
|
||||
assert_eq!(parse_version("1.2"), None);
|
||||
assert!(version_at_least("0.1.5-beta.1", "0.1.5"));
|
||||
assert!(version_at_least("0.2.0", "0.1.9"));
|
||||
assert!(!version_at_least("0.1.4", "0.1.5"));
|
||||
assert!(!version_at_least("bogus", "0.1.0"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +97,13 @@ pub struct PluginInvocationPlan {
|
||||
}
|
||||
|
||||
impl PluginRegistry {
|
||||
pub fn managed(store: Store, runtime: PluginRuntime) -> Result<Self> {
|
||||
pub fn managed(store: Store, runtime: PluginRuntime, app_version: String) -> Result<Self> {
|
||||
let data = PluginDataStore::managed()?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(RegistryInner {
|
||||
store,
|
||||
runtime,
|
||||
catalog: PluginCatalog::managed()?,
|
||||
catalog: PluginCatalog::managed(app_version)?,
|
||||
state: PluginStateStore::new(data),
|
||||
entries: RwLock::new(None),
|
||||
workers: Mutex::new(HashMap::new()),
|
||||
@@ -122,6 +122,7 @@ impl PluginRegistry {
|
||||
.map(|(manifest, icon)| PluginDescriptor {
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
author: manifest.author,
|
||||
icon,
|
||||
providers: Vec::new(),
|
||||
@@ -625,6 +626,7 @@ impl PluginRegistry {
|
||||
PluginDescriptor {
|
||||
id: plugin_id.clone(),
|
||||
name: entry.manifest.name.clone(),
|
||||
version: entry.manifest.version.clone(),
|
||||
author: entry.manifest.author.clone(),
|
||||
icon: entry.icon.clone(),
|
||||
providers,
|
||||
|
||||
@@ -17,11 +17,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
<<<<<<< HEAD
|
||||
apply_body_allowlist, apply_openai_prompt_cache_key, merge_extra_params,
|
||||
=======
|
||||
apply_openai_prompt_cache_key, map_sse_error, merge_extra_params, provider_event_error,
|
||||
>>>>>>> main
|
||||
apply_body_allowlist, apply_openai_prompt_cache_key, map_sse_error, merge_extra_params,
|
||||
provider_event_error,
|
||||
recorder::recorded_headers,
|
||||
retry::{send_with_retry, Attempt, RetryPolicy},
|
||||
CallRecorder, FinishReason, ModelEvent, Provider, ProviderStream,
|
||||
|
||||
@@ -14,11 +14,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
<<<<<<< HEAD
|
||||
apply_body_allowlist, apply_openai_prompt_cache_key, merge_extra_params,
|
||||
=======
|
||||
apply_openai_prompt_cache_key, map_sse_error, merge_extra_params, provider_event_error,
|
||||
>>>>>>> main
|
||||
apply_body_allowlist, apply_openai_prompt_cache_key, map_sse_error, merge_extra_params,
|
||||
provider_event_error,
|
||||
recorder::recorded_headers,
|
||||
retry::{send_with_retry, Attempt, RetryPolicy},
|
||||
CallRecorder, FinishReason, ModelEvent, Provider, ProviderStream,
|
||||
|
||||
+77
-135
@@ -14,8 +14,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
normalize::NormalizedProvider, AnthropicProvider, CallRecorder, OpenAiChatProvider,
|
||||
OpenAiResponsesProvider, Provider, ProviderStream,
|
||||
normalize::NormalizedProvider, recorder::CancelOnDrop, AnthropicProvider, CallRecorder,
|
||||
OpenAiChatProvider, OpenAiResponsesProvider, Provider, ProviderStream,
|
||||
};
|
||||
|
||||
const BUILTIN_PROVIDER_RETRIES: u32 = 5;
|
||||
@@ -28,11 +28,12 @@ pub struct ProviderRouter {
|
||||
}
|
||||
|
||||
impl ProviderRouter {
|
||||
<<<<<<< HEAD
|
||||
pub fn new(store: Store, plugins: PluginRegistry, request_timeout: Duration) -> Self {
|
||||
=======
|
||||
pub fn new(store: Store, request_timeout: Duration, stream_idle_timeout: Duration) -> Self {
|
||||
>>>>>>> main
|
||||
pub fn new(
|
||||
store: Store,
|
||||
plugins: PluginRegistry,
|
||||
request_timeout: Duration,
|
||||
stream_idle_timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
plugins,
|
||||
@@ -54,87 +55,57 @@ impl Provider for ProviderRouter {
|
||||
let stream_idle_timeout = self.stream_idle_timeout;
|
||||
Box::pin(try_stream! {
|
||||
let selected = invocation.request.model.model_id.clone();
|
||||
<<<<<<< HEAD
|
||||
if selected.starts_with(ADAPTER_ID_PREFIX) {
|
||||
// 插件模型与内置模型走完全相同的流程:Recorder、统一事件、
|
||||
// 规范化包装。资源选择与将来的负载均衡都在插件 Provider 内部。
|
||||
let plan = plugins.plan_model(&selected).await?;
|
||||
let recorder = start_recorder(&store, &invocation, &selected, &plan.model.display_name, ProviderType::Plugin, &plan.request_url, &plan.model.model_id).await?;
|
||||
let _cancel_on_drop = recorder.cancel_on_drop();
|
||||
recorder.request(serde_json::json!({}), &crate::plugin::plugin_llm_request(&invocation)?).await?;
|
||||
let mut routed = invocation.clone();
|
||||
routed.request.model.display_name = Some(plan.model.display_name.clone());
|
||||
if let Some(tokens) = plan.model.context_window_tokens {
|
||||
routed.request.model.context_window_tokens.get_or_insert(tokens);
|
||||
}
|
||||
if let Some(tokens) = plan.model.max_output_tokens {
|
||||
routed.request.model.max_output_tokens.get_or_insert(tokens);
|
||||
}
|
||||
let provider: Arc<dyn Provider> = Arc::new(NormalizedProvider::new(Arc::new(PluginModelProvider {
|
||||
registry: plugins.clone(),
|
||||
})));
|
||||
let mut stream = provider.stream(routed, cancellation.clone());
|
||||
while let Some(item) = stream.next().await {
|
||||
match item {
|
||||
Ok(event) => { recorder.event(&event).await?; yield event; }
|
||||
Err(error) => { recorder.failed(&error).await?; Err(error)?; }
|
||||
=======
|
||||
let model = store
|
||||
.model(&selected)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Provider(format!("unknown model: {selected}")))?;
|
||||
let provider_type = model.provider_type();
|
||||
let request_url = model.request_url()?;
|
||||
model.configure(&mut invocation.request.model);
|
||||
invocation.request.model.extra_params = model.extra_params().clone();
|
||||
invocation.request.model.model_id = model.model_id.clone();
|
||||
let recorder = CallRecorder::start(store.clone(), NewLlmCall {
|
||||
call_id: invocation.call_id.clone(),
|
||||
run_id: invocation.run_id.clone(),
|
||||
conversation_id: invocation.conversation_id.clone(),
|
||||
provider_call_index: invocation.provider_call_index.min(i64::MAX as u64) as i64,
|
||||
model_hash: model.model_hash.clone(),
|
||||
provider_type,
|
||||
provider_url: model.base_url.clone(),
|
||||
request_type: provider_type,
|
||||
request_url: request_url.clone(),
|
||||
model_id: model.model_id.clone(),
|
||||
display_name: model.display_name.clone(),
|
||||
reasoning_effort: invocation.request.model.reasoning.effort.clone(),
|
||||
fast: invocation.request.model.latency == ModelLatency::Fast,
|
||||
message_count: invocation.request.history.len(),
|
||||
tool_count: invocation.request.prompt.tools.len(),
|
||||
detailed: false,
|
||||
}).await?;
|
||||
let _cancel_on_drop = recorder.cancel_on_drop();
|
||||
let config = ProviderConfig {
|
||||
kind: match provider_type {
|
||||
ProviderType::OpenAiChat => ProviderKind::OpenAiChat,
|
||||
ProviderType::OpenAiResponses => ProviderKind::OpenAiResponses,
|
||||
ProviderType::Anthropic => ProviderKind::Anthropic,
|
||||
},
|
||||
request_url,
|
||||
api_key: model.api_key.clone(),
|
||||
custom_headers: if model.custom_headers_enabled {
|
||||
custom_headers(&model.custom_headers)?
|
||||
// 两条分支只负责装配 Recorder 与 Provider 流;
|
||||
// 事件消费(空闲超时看门狗、记录、错误规范化)对两者完全一致。
|
||||
let (recorder, _cancel_on_drop, mut stream): (CallRecorder, CancelOnDrop, ProviderStream) =
|
||||
if selected.starts_with(ADAPTER_ID_PREFIX) {
|
||||
// 插件模型与内置模型走完全相同的流程:资源选择与将来的
|
||||
// 负载均衡都在插件 Provider 内部。
|
||||
let plan = plugins.plan_model(&selected).await?;
|
||||
let recorder = start_recorder(&store, &invocation, &selected, &plan.model.display_name, ProviderType::Plugin, &plan.request_url, &plan.model.model_id).await?;
|
||||
let guard = recorder.cancel_on_drop();
|
||||
recorder.request(serde_json::json!({}), &crate::plugin::plugin_llm_request(&invocation)?).await?;
|
||||
let mut routed = invocation.clone();
|
||||
routed.request.model.display_name = Some(plan.model.display_name.clone());
|
||||
if let Some(tokens) = plan.model.context_window_tokens {
|
||||
routed.request.model.context_window_tokens.get_or_insert(tokens);
|
||||
}
|
||||
if let Some(tokens) = plan.model.max_output_tokens {
|
||||
routed.request.model.max_output_tokens.get_or_insert(tokens);
|
||||
}
|
||||
let provider: Arc<dyn Provider> = Arc::new(NormalizedProvider::new(Arc::new(PluginModelProvider {
|
||||
registry: plugins.clone(),
|
||||
})));
|
||||
(recorder, guard, provider.stream(routed, cancellation.clone()))
|
||||
} else {
|
||||
reqwest::header::HeaderMap::new()
|
||||
},
|
||||
max_output_tokens: model.max_output_tokens(),
|
||||
request_timeout,
|
||||
};
|
||||
let client = crate::network::client_builder(&store)
|
||||
.await?
|
||||
.timeout(config.request_timeout)
|
||||
.build()?;
|
||||
let provider = build_observed(&config, recorder.clone(), client)?;
|
||||
let stream_cancellation = cancellation.clone();
|
||||
let mut stream = provider.stream(invocation, cancellation);
|
||||
let mut routed = invocation.clone();
|
||||
let model = store.model(&selected).await?.ok_or_else(|| Error::Provider(format!("unknown model: {selected}")))?;
|
||||
let provider_type = model.provider_type();
|
||||
let request_url = model.request_url()?;
|
||||
model.configure(&mut routed.request.model);
|
||||
routed.request.model.extra_params = model.extra_params().clone();
|
||||
routed.request.model.model_id = model.model_id.clone();
|
||||
let recorder = start_recorder(&store, &invocation, &model.model_hash, &model.display_name, provider_type, &request_url, &model.model_id).await?;
|
||||
let guard = recorder.cancel_on_drop();
|
||||
let config = ProviderConfig {
|
||||
kind: provider_kind(provider_type),
|
||||
request_url,
|
||||
api_key: model.api_key.clone(),
|
||||
custom_headers: if model.custom_headers_enabled { custom_headers(&model.custom_headers)? } else { reqwest::header::HeaderMap::new() },
|
||||
max_output_tokens: model.max_output_tokens(),
|
||||
request_timeout,
|
||||
retry_count: BUILTIN_PROVIDER_RETRIES,
|
||||
allowed_body_fields: None,
|
||||
};
|
||||
let client = crate::network::client_builder(&store).await?.timeout(request_timeout).build()?;
|
||||
let provider = build_observed(&config, recorder.clone(), client)?;
|
||||
(recorder, guard, provider.stream(routed, cancellation.clone()))
|
||||
};
|
||||
|
||||
let stream_started = std::time::Instant::now();
|
||||
tracing::debug!(
|
||||
model = %selected,
|
||||
provider_type = ?provider_type,
|
||||
request_timeout_ms = config.request_timeout.as_millis() as u64,
|
||||
request_timeout_ms = request_timeout.as_millis() as u64,
|
||||
stream_idle_timeout_ms = stream_idle_timeout.as_millis() as u64,
|
||||
"provider stream created"
|
||||
);
|
||||
@@ -163,26 +134,11 @@ impl Provider for ProviderRouter {
|
||||
event_count += 1;
|
||||
match event {
|
||||
Ok(event) => {
|
||||
let event_name = match &event {
|
||||
super::ModelEvent::Start { .. } => "Start",
|
||||
super::ModelEvent::TextStart => "TextStart",
|
||||
super::ModelEvent::TextDelta(_) => "TextDelta",
|
||||
super::ModelEvent::TextEnd => "TextEnd",
|
||||
super::ModelEvent::ThinkingStart => "ThinkingStart",
|
||||
super::ModelEvent::ThinkingDelta(_) => "ThinkingDelta",
|
||||
super::ModelEvent::ThinkingEnd => "ThinkingEnd",
|
||||
super::ModelEvent::ToolCallStart { .. } => "ToolCallStart",
|
||||
super::ModelEvent::ToolCallArgumentsDelta { .. } => "ToolCallArgsDelta",
|
||||
super::ModelEvent::ToolCallEnd { .. } => "ToolCallEnd",
|
||||
super::ModelEvent::ProviderReplayState(_) => "ReplayState",
|
||||
super::ModelEvent::Usage(_) => "Usage",
|
||||
super::ModelEvent::Done(_) => "Done",
|
||||
};
|
||||
if gap_ms > 5000 {
|
||||
tracing::debug!(
|
||||
gap_ms,
|
||||
elapsed_ms,
|
||||
event = event_name,
|
||||
event = event_name(&event),
|
||||
event_count,
|
||||
"slow gap detected between provider events"
|
||||
);
|
||||
@@ -202,46 +158,32 @@ impl Provider for ProviderRouter {
|
||||
);
|
||||
recorder.failed(&error).await?;
|
||||
Err(error)?;
|
||||
>>>>>>> main
|
||||
}
|
||||
}
|
||||
finish_stream(&recorder, &cancellation).await?;
|
||||
} else {
|
||||
let mut routed = invocation.clone();
|
||||
let model = store.model(&selected).await?.ok_or_else(|| Error::Provider(format!("unknown model: {selected}")))?;
|
||||
let provider_type = model.provider_type();
|
||||
let request_url = model.request_url()?;
|
||||
model.configure(&mut routed.request.model);
|
||||
routed.request.model.extra_params = model.extra_params().clone();
|
||||
routed.request.model.model_id = model.model_id.clone();
|
||||
let recorder = start_recorder(&store, &invocation, &model.model_hash, &model.display_name, provider_type, &request_url, &model.model_id).await?;
|
||||
let _cancel_on_drop = recorder.cancel_on_drop();
|
||||
let config = ProviderConfig {
|
||||
kind: provider_kind(provider_type),
|
||||
request_url,
|
||||
api_key: model.api_key.clone(),
|
||||
custom_headers: if model.custom_headers_enabled { custom_headers(&model.custom_headers)? } else { reqwest::header::HeaderMap::new() },
|
||||
max_output_tokens: model.max_output_tokens(),
|
||||
request_timeout,
|
||||
retry_count: BUILTIN_PROVIDER_RETRIES,
|
||||
allowed_body_fields: None,
|
||||
};
|
||||
let client = crate::network::client_builder(&store).await?.timeout(request_timeout).build()?;
|
||||
let provider = build_observed(&config, recorder.clone(), client)?;
|
||||
let mut stream = provider.stream(routed, cancellation.clone());
|
||||
while let Some(item) = stream.next().await {
|
||||
match item {
|
||||
Ok(event) => { recorder.event(&event).await?; yield event; }
|
||||
Err(error) => { recorder.failed(&error).await?; Err(error)?; }
|
||||
}
|
||||
}
|
||||
finish_stream(&recorder, &cancellation).await?;
|
||||
}
|
||||
finish_stream(&recorder, &cancellation).await?;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn event_name(event: &super::ModelEvent) -> &'static str {
|
||||
match event {
|
||||
super::ModelEvent::Start { .. } => "Start",
|
||||
super::ModelEvent::TextStart => "TextStart",
|
||||
super::ModelEvent::TextDelta(_) => "TextDelta",
|
||||
super::ModelEvent::TextEnd => "TextEnd",
|
||||
super::ModelEvent::ThinkingStart => "ThinkingStart",
|
||||
super::ModelEvent::ThinkingDelta(_) => "ThinkingDelta",
|
||||
super::ModelEvent::ThinkingEnd => "ThinkingEnd",
|
||||
super::ModelEvent::ToolCallStart { .. } => "ToolCallStart",
|
||||
super::ModelEvent::ToolCallArgumentsDelta { .. } => "ToolCallArgsDelta",
|
||||
super::ModelEvent::ToolCallEnd { .. } => "ToolCallEnd",
|
||||
super::ModelEvent::ProviderReplayState(_) => "ReplayState",
|
||||
super::ModelEvent::Usage(_) => "Usage",
|
||||
super::ModelEvent::Done(_) => "Done",
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_recorder(
|
||||
store: &Store,
|
||||
invocation: &ModelInvocation,
|
||||
@@ -311,7 +253,8 @@ fn provider_kind(provider_type: ProviderType) -> ProviderKind {
|
||||
// 内置模型的 provider_type 只来自 ModelType,不可能是插件。
|
||||
ProviderType::Plugin => unreachable!("plugin models never use built-in provider configs"),
|
||||
}
|
||||
=======
|
||||
}
|
||||
|
||||
async fn next_provider_event(
|
||||
stream: &mut ProviderStream,
|
||||
idle_timeout: Duration,
|
||||
@@ -352,7 +295,6 @@ fn root_error_message(error: &(dyn std::error::Error + 'static)) -> String {
|
||||
current = source;
|
||||
}
|
||||
current.to_string()
|
||||
>>>>>>> main
|
||||
}
|
||||
|
||||
fn custom_headers(value: &serde_json::Value) -> Result<reqwest::header::HeaderMap> {
|
||||
|
||||
Reference in New Issue
Block a user