Merge pull request #412 from ProtectCookies/main

This commit is contained in:
leokun
2026-09-04 15:45:51 +08:00
16 changed files with 369 additions and 52 deletions
@@ -49,8 +49,8 @@ pub(crate) fn raise_open_file_limit() -> io::Result<OpenFileLimit> {
}
Ok(OpenFileLimit {
previous: previous as u64,
effective: effective.rlim_cur as u64,
hard: effective.rlim_max as u64,
previous,
effective: effective.rlim_cur,
hard: effective.rlim_max,
})
}
+16 -3
View File
@@ -1,8 +1,16 @@
use std::process::ExitCode;
#[cfg(target_os = "windows")]
use std::{
fs::{self, OpenOptions},
io::{Cursor, Read, Write},
path::{Path, PathBuf},
process::{Command, ExitCode},
io::Write,
path::PathBuf,
process::Command,
};
#[cfg(any(target_os = "windows", test))]
use std::{
io::{Cursor, Read},
path::Path,
};
use serde::Serialize;
@@ -14,8 +22,10 @@ use tauri_plugin_updater::UpdaterExt;
#[cfg(target_os = "windows")]
mod replacement;
#[cfg(target_os = "windows")]
const PORTABLE_UPDATE_ENDPOINT: &str =
"https://github.com/leookun/cursor-byok/releases/latest/download/portable-latest.json";
#[cfg(any(target_os = "windows", test))]
const WINDOWS_PAYLOAD_NAME: &str = "cursor-byok-desktop.exe";
#[derive(Serialize)]
@@ -176,6 +186,7 @@ async fn wait_for_replacement_start(
result
}
#[cfg(target_os = "windows")]
fn ensure_target_writable(target: &Path) -> std::io::Result<()> {
let parent = target
.parent()
@@ -193,6 +204,7 @@ fn ensure_target_writable(target: &Path) -> std::io::Result<()> {
fs::remove_file(probe)
}
#[cfg(any(target_os = "windows", test))]
fn extract_windows_payload(bytes: &[u8]) -> Result<Vec<u8>, String> {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
.map_err(|error| format!("failed to open ZIP: {error}"))?;
@@ -222,6 +234,7 @@ fn extract_windows_payload(bytes: &[u8]) -> Result<Vec<u8>, String> {
Ok(payload)
}
#[cfg(target_os = "windows")]
fn stage_payload(target: &Path, payload: &[u8]) -> std::io::Result<PathBuf> {
let directory = tempfile::Builder::new()
.prefix("cursor-byok-portable-update-")
+29 -4
View File
@@ -5,7 +5,7 @@ import { DailyTokenUsageChart } from "./charts/DailyTokenUsageChart";
import { HomeMetrics } from "./metrics/HomeMetrics";
import { PageContent } from "../../shell/layout/PageContent";
import type { VirtualPageSection } from "../../shell/layout/VirtualPage";
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "./overview/OverviewTimeRangeFilter";
import { OverviewTimeRangeFilter, type OverviewRangePreset, type QuickPreset } from "./overview/OverviewTimeRangeFilter";
import { PageActions } from "../../shell/PageActions";
import { appStore, useAppStore } from "../../shared/store/appStore";
import { formatTimeInput, parseTimeInput } from "../../shared/utils/parseTimeInput";
@@ -27,7 +27,9 @@ function presetRange(preset: Exclude<OverviewRangePreset, "custom">, now = new D
start.setMonth(start.getMonth() - 1);
return { startMs: start.getTime(), endMs };
}
const duration = preset === "ten-minutes" ? 10 * 60_000 : preset === "hour" ? 60 * 60_000 : 7 * 24 * 60 * 60_000;
const duration = preset === "ten-minutes" ? 10 * 60_000
: preset === "hour" ? 60 * 60_000
: 7 * 24 * 60 * 60_000;
return { startMs: now.getTime() - duration, endMs };
}
@@ -35,6 +37,8 @@ export function HomePage() {
const { overview, busy, models, plugins } = useAppStore();
const { locale } = useI18n();
const [preset, setPreset] = useState<OverviewRangePreset>("month");
const [quick, setQuick] = useState<QuickPreset | null>(null);
const [fourHourBucket, setFourHourBucket] = useState<number | undefined>(undefined);
const [customRange, setCustomRange] = useState<TimeRange | null>(null);
const [customOpen, setCustomOpen] = useState(false);
const [customStart, setCustomStart] = useState("");
@@ -53,13 +57,14 @@ export function HomePage() {
void api.overview({
...selectedRange,
modelHashes: appliedModels,
bucketMs: fourHourBucket,
}).then((next) => {
if (active) setRangeOverview(next);
}).finally(() => {
if (active) setRangeBusy(false);
});
return () => { active = false; };
}, [preset, customRange, overview, refreshVersion, appliedModels]);
}, [preset, customRange, overview, refreshVersion, appliedModels, fourHourBucket]);
const filteredOverview = rangeOverview ?? overview;
const dailyTokenUsage = filteredOverview.token_usage_series.map((bucket) => ({
@@ -96,9 +101,26 @@ export function HomePage() {
if (startMs === null || endMs === null || startMs >= endMs) return;
setCustomRange({ startMs, endMs });
setAppliedModels(selectedModels);
setQuick(null);
setFourHourBucket(undefined);
setPreset("custom");
setCustomOpen(false);
};
const selectQuick = (durationMs: number, bucketMs?: number) => {
const endMs = Date.now();
setCustomRange({ startMs: endMs - durationMs, endMs });
setAppliedModels(selectedModels);
setQuick(durationMs === 4 * 60 * 60_000 ? "four-hours" : "twenty-four-hours");
setFourHourBucket(durationMs === 4 * 60 * 60_000 ? bucketMs : undefined);
setPreset("custom");
setCustomOpen(false);
};
const selectPreset = (value: Exclude<OverviewRangePreset, "custom">) => {
setPreset(value);
setQuick(null);
setFourHourBucket(undefined);
setCustomOpen(false);
};
const refresh = async () => {
await appStore.refresh();
setRefreshVersion((version) => version + 1);
@@ -146,13 +168,16 @@ export function HomePage() {
return <>
<PageActions><OverviewTimeRangeFilter
value={preset}
quick={quick}
fourHourBucket={fourHourBucket}
customOpen={customOpen}
customStart={customStart}
customEnd={customEnd}
modelOptions={modelOptions}
selectedModels={selectedModels}
busy={busy || rangeBusy}
onSelect={(value) => { setPreset(value); setCustomOpen(false); }}
onSelect={selectPreset}
onQuickSelect={selectQuick}
onCustomOpenChange={openCustom}
onCustomStartChange={setCustomStart}
onCustomEndChange={setCustomEnd}
@@ -76,3 +76,50 @@
justify-content: flex-end;
gap: 6px;
}
.quickPresets {
display: flex;
gap: 6px;
button {
height: 26px;
padding: 0 10px;
color: var(--vscode-descriptionForeground);
background: var(--vscode-button-secondaryBackground, var(--vscode-sideBar-background));
border: 1px solid var(--vscode-sideBar-border);
border-radius: 4px;
font-size: type.$font-size-xs;
white-space: nowrap;
&:hover { color: var(--vscode-foreground); background: var(--vscode-list-hoverBackground); }
&[aria-pressed="true"] { color: var(--vscode-list-activeSelectionForeground); background: var(--vscode-list-activeSelectionBackground); border-color: transparent; }
}
}
.granularityMenu {
position: fixed;
z-index: 15000;
min-width: 96px;
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: 28px;
display: block;
padding: 4px 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 { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); }
&[aria-pressed="true"] { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); }
}
}
@@ -10,9 +10,12 @@ import { refreshIcon } from "../../../shared/ui/icons";
import styles from "./OverviewTimeRangeFilter.module.scss";
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
export type QuickPreset = "four-hours" | "twenty-four-hours";
export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: {
export function OverviewTimeRangeFilter({ value, quick, fourHourBucket, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onQuickSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: {
value: OverviewRangePreset;
quick: QuickPreset | null;
fourHourBucket: number | undefined;
customOpen: boolean;
customStart: string;
customEnd: string;
@@ -20,6 +23,7 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
selectedModels: string[];
busy: boolean;
onSelect: (value: Exclude<OverviewRangePreset, "custom">) => void;
onQuickSelect: (durationMs: number, bucketMs?: number) => void;
onCustomOpenChange: (open: boolean) => void;
onCustomStartChange: (value: string) => void;
onCustomEndChange: (value: string) => void;
@@ -34,10 +38,25 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
{ value: "week", label: t("近一周") },
{ value: "month", label: t("近一个月") },
];
const quickPresets: Array<{ value: QuickPreset; label: string }> = [
{ value: "four-hours", label: t("近4小时") },
{ value: "twenty-four-hours", label: t("近24小时") },
];
const granularityOptions = [
{ bucketMs: 60_000, label: t("1分钟") },
{ bucketMs: 15 * 60_000, label: t("15分钟") },
{ bucketMs: 30 * 60_000, label: t("30分钟") },
{ bucketMs: 60 * 60_000, label: t("1小时") },
];
const customButton = useRef<HTMLButtonElement>(null);
const popover = useRef<HTMLDivElement>(null);
const fourHourButton = useRef<HTMLButtonElement>(null);
const granularityMenu = useRef<HTMLDivElement>(null);
const popoverId = useId();
const granularityMenuId = useId();
const [position, setPosition] = useState({ left: 0, top: 0, width: 300, maxHeight: 480 });
const [menuPosition, setMenuPosition] = useState({ left: 0, top: 0 });
const [granularityOpen, setGranularityOpen] = useState(false);
useLayoutEffect(() => {
if (!customOpen || !customButton.current || !popover.current) return;
@@ -53,11 +72,28 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
}).then(({ x, y }) => setPosition((current) => ({ ...current, left: x, top: y }))));
}, [customOpen]);
useLayoutEffect(() => {
if (!granularityOpen || !fourHourButton.current || !granularityMenu.current) return;
return autoUpdate(fourHourButton.current, granularityMenu.current, () =>
void computePosition(fourHourButton.current!, granularityMenu.current!, {
placement: "right-start",
middleware: [offset(4), flip({ padding: 10 }), shift({ padding: 10 })],
}).then(({ x, y }) => setMenuPosition({ left: x, top: y })));
}, [granularityOpen]);
useEffect(() => {
if (!customOpen) setGranularityOpen(false);
}, [customOpen]);
useEffect(() => {
if (!customOpen) return;
const closeOutside = (event: PointerEvent) => {
const target = event.target as Node;
if (!customButton.current?.contains(target) && !popover.current?.contains(target)) onCustomOpenChange(false);
if (
!customButton.current?.contains(target)
&& !popover.current?.contains(target)
&& !granularityMenu.current?.contains(target)
) onCustomOpenChange(false);
};
document.addEventListener("pointerdown", closeOutside);
return () => document.removeEventListener("pointerdown", closeOutside);
@@ -66,6 +102,10 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
const parsedStart = parseTimeInput(customStart);
const parsedEnd = parseTimeInput(customEnd);
const customValid = parsedStart !== null && parsedEnd !== null && parsedStart < parsedEnd;
const closeGranularity = () => {
setGranularityOpen(false);
fourHourButton.current?.focus();
};
return <div className={styles.root} aria-label={t("概览时间范围")}>
<div className={styles.presets}>
@@ -98,11 +138,37 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
if (granularityOpen) {
closeGranularity();
return;
}
onCustomOpenChange(false);
customButton.current?.focus();
}
}}
>
<div className={styles.quickPresets} aria-label={t("快捷时间范围")}>
{quickPresets.map((preset) => preset.value === "four-hours"
? <button
key={preset.value}
ref={fourHourButton}
type="button"
aria-haspopup="menu"
aria-controls={granularityOpen ? granularityMenuId : undefined}
aria-expanded={granularityOpen}
aria-pressed={quick === "four-hours"}
onClick={() => setGranularityOpen((current) => !current)}
>{preset.label}</button>
: <button
key={preset.value}
type="button"
aria-pressed={quick === "twenty-four-hours"}
onClick={() => {
setGranularityOpen(false);
onQuickSelect(24 * 60 * 60_000);
}}
>{preset.label}</button>)}
</div>
<label><span>{t("开始时间")}</span><input type="text" placeholder={t("如:2026-08-23 09:00、1小时前")} value={customStart} onChange={(event) => onCustomStartChange(event.target.value)} /></label>
<label><span>{t("结束时间")}</span><input type="text" placeholder={t("如:现在、2026-08-23 18:00")} value={customEnd} onChange={(event) => onCustomEndChange(event.target.value)} /></label>
<div className={styles.filterRow}><ModelSelect mode="multiple" label={t("模型")} value={selectedModels} options={modelOptions} onChange={onSelectedModelsChange} /></div>
@@ -111,5 +177,31 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
<button type="button" className={controls.primary} disabled={!customValid} onClick={onCustomApply}>{t("应用")}</button>
</div>
</div>, document.body)}
{granularityOpen && customOpen && createPortal(<div
id={granularityMenuId}
ref={granularityMenu}
className={styles.granularityMenu}
role="menu"
aria-label={t("显示粒度")}
style={menuPosition}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.stopPropagation();
event.preventDefault();
closeGranularity();
}
}}
>
{granularityOptions.map((option) => <button
key={option.bucketMs}
type="button"
role="menuitem"
aria-pressed={quick === "four-hours" && fourHourBucket === option.bucketMs}
onClick={() => {
setGranularityOpen(false);
onQuickSelect(4 * 60 * 60_000, option.bucketMs);
}}
>{option.label}</button>)}
</div>, document.body)}
</div>;
}
+116 -20
View File
@@ -428,6 +428,18 @@
}
]
},
"100fad4a0b3ab781": {
"source": "近24小时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 43,
"column": 42
}
]
},
"105a9082c346f958": {
"source": "测试中…",
"kind": "text",
@@ -1015,7 +1027,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 110,
"line": 176,
"column": 105
},
{
@@ -1615,6 +1627,18 @@
}
]
},
"4256064de039de89": {
"source": "显示粒度",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 185,
"column": 19
}
]
},
"42655ed8e4108ae2": {
"source": "输入(非缓存)",
"kind": "text",
@@ -1944,7 +1968,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 86,
"line": 126,
"column": 9
},
{
@@ -1969,6 +1993,18 @@
}
]
},
"4f0f45b0312ca4ba": {
"source": "1小时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 49,
"column": 37
}
]
},
"51d04bc3d286f018": {
"source": "近1自然日",
"kind": "text",
@@ -1976,7 +2012,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 32,
"line": 36,
"column": 30
}
]
@@ -2002,7 +2038,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 107,
"line": 173,
"column": 70
}
]
@@ -2324,6 +2360,18 @@
}
]
},
"609640f72d422b57": {
"source": "快捷时间范围",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 150,
"column": 56
}
]
},
"61a4c7bac12dc125": {
"source": "异常调用:{count}",
"kind": "template",
@@ -2388,7 +2436,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 111,
"line": 177,
"column": 109
}
]
@@ -2422,7 +2470,7 @@
"refs": [
{
"file": "shared/api.ts",
"line": 486,
"line": 487,
"column": 43
}
]
@@ -2573,7 +2621,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 106,
"line": 172,
"column": 21
}
]
@@ -2749,7 +2797,7 @@
"refs": [
{
"file": "shared/api.ts",
"line": 481,
"line": 482,
"column": 43
}
]
@@ -3008,6 +3056,18 @@
}
]
},
"859757ea792a73da": {
"source": "30分钟",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 48,
"column": 37
}
]
},
"864597982c308d72": {
"source": "已开启静默启动",
"kind": "text",
@@ -3506,7 +3566,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 106,
"line": 172,
"column": 70
}
]
@@ -3587,7 +3647,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 34,
"line": 38,
"column": 29
}
]
@@ -4019,12 +4079,12 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 88,
"line": 128,
"column": 28
},
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 88,
"line": 128,
"column": 89
},
{
@@ -4425,7 +4485,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 70,
"line": 110,
"column": 51
}
]
@@ -4559,7 +4619,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 108,
"line": 174,
"column": 77
}
]
@@ -4682,7 +4742,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 35,
"line": 39,
"column": 30
}
]
@@ -4937,7 +4997,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 31,
"line": 35,
"column": 29
}
]
@@ -4976,6 +5036,18 @@
}
]
},
"dbaa1c565cb1f108": {
"source": "15分钟",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 47,
"column": 37
}
]
},
"dbd3596e4a86f3c2": {
"source": "配置模型",
"kind": "text",
@@ -5283,7 +5355,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 33,
"line": 37,
"column": 36
}
]
@@ -5375,6 +5447,18 @@
}
]
},
"e85373502c372776": {
"source": "1分钟",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 46,
"column": 32
}
]
},
"e8b1268c1e3610f2": {
"source": "已存在",
"kind": "text",
@@ -5558,7 +5642,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 107,
"line": 173,
"column": 21
}
]
@@ -5618,6 +5702,18 @@
}
]
},
"f784b165bd3fcb0e": {
"source": "近4小时",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 42,
"column": 35
}
]
},
"f85537d1fd2ef6f6": {
"source": "自定义概览筛选",
"kind": "text",
@@ -5625,7 +5721,7 @@
"refs": [
{
"file": "features/home/overview/OverviewTimeRangeFilter.tsx",
"line": 96,
"line": 136,
"column": 19
}
]
@@ -5832,7 +5928,7 @@
"refs": [
{
"file": "features/home/HomePage.tsx",
"line": 163,
"line": 188,
"column": 25
}
]
+8
View File
@@ -27,6 +27,7 @@
"0e41f8e3d59ec47b": "Storage management",
"0e67021ebf0a3580": "Import complete: added {imported} models and skipped {skipped} existing models",
"0ec1e85b0c3cfa65": "Call details",
"100fad4a0b3ab781": "Last 24 hours",
"105a9082c346f958": "Testing…",
"124be3f86f197802": "Token usage",
"12ae77e6202d063e": "Custom Headers",
@@ -107,6 +108,7 @@
"402495402ce333b1": "Reinitialize plugins",
"40a08e7cf320ae07": "Clear detailed records?",
"4125fc7ba333524c": "Default light",
"4256064de039de89": "Display granularity",
"42655ed8e4108ae2": "Input (non-cached)",
"42a1d9e5b037c210": "Bytes",
"42aa8e01e98c0d8c": "Total duration",
@@ -130,6 +132,7 @@
"4d99c976beb8827e": "Ready",
"4e30d7c9ed2b0eee": "Not set",
"4eafa9e925b30bcd": "Custom",
"4f0f45b0312ca4ba": "1 hour",
"51d04bc3d286f018": "Last calendar day",
"51de3bcec137ab1b": "Connectivity tests succeeded for all {count} models",
"5228358a6db59fe7": "For example: now, 2026-08-23 18:00",
@@ -156,6 +159,7 @@
"5f9acfb945229062": "Are you sure you no longer want to see this ad?",
"5fd2ec5a6e9b654c": "Total: {cost}",
"6078a681a306930d": "Cache write",
"609640f72d422b57": "Quick time ranges",
"61a4c7bac12dc125": "Failed calls: {count}",
"61c7d1f758b647d5": "Call information",
"621f63a5f08384ac": "Cache read: {tokens}",
@@ -205,6 +209,7 @@
"842b9f11cdd96bda": "Launch at login",
"843ac7e15a5047a7": "Confirm legacy model configuration import",
"844b8cc8dff7c1d8": "Default",
"859757ea792a73da": "30 minutes",
"864597982c308d72": "Silent start enabled",
"86de7c4ee8fa7689": "Sync models",
"8716e1344b0daddb": "Cursor official",
@@ -342,6 +347,7 @@
"da7ae985487c38e6": "Last hour",
"daede9881787abe7": "Notes",
"db340a9896306d08": "Test cancelled",
"dbaa1c565cb1f108": "15 minutes",
"dbd3596e4a86f3c2": "Configured models",
"ddde16f8839da3ce": "Total requests",
"de8184da1ef88d03": "Configured",
@@ -372,6 +378,7 @@
"e77e3d58b0dcffaa": "Duration",
"e825a2a42c22380e": "Model type",
"e828bd3a0151edc2": "The local CA must be trusted by the system",
"e85373502c372776": "1 minute",
"e8b1268c1e3610f2": "Existing",
"ea26b760e930a7ca": "Call observability",
"eb11e2df1d8ae387": "Provider URL",
@@ -391,6 +398,7 @@
"f6dc1b1641600dd0": "Importing…",
"f78265089144369a": "Plugins",
"f78413c36d36f090": "Uses the operating system language by default; unsupported languages fall back to English. Current: {language}",
"f784b165bd3fcb0e": "Last 4 hours",
"f85537d1fd2ef6f6": "Custom overview filters",
"f95ea7f4c063eea7": "Disabled",
"f9615d05d8e18595": "Clear {label} filter",
+8
View File
@@ -27,6 +27,7 @@
"0e41f8e3d59ec47b": "存储管理",
"0e67021ebf0a3580": "导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型",
"0ec1e85b0c3cfa65": "调用详情",
"100fad4a0b3ab781": "近24小时",
"105a9082c346f958": "测试中…",
"124be3f86f197802": "Token 消耗",
"12ae77e6202d063e": "自定义 Headers",
@@ -107,6 +108,7 @@
"402495402ce333b1": "重新初始化插件",
"40a08e7cf320ae07": "确定要清理详细记录吗?",
"4125fc7ba333524c": "默认亮色",
"4256064de039de89": "显示粒度",
"42655ed8e4108ae2": "输入(非缓存)",
"42a1d9e5b037c210": "字节数",
"42aa8e01e98c0d8c": "总耗时",
@@ -130,6 +132,7 @@
"4d99c976beb8827e": "可用",
"4e30d7c9ed2b0eee": "不设置",
"4eafa9e925b30bcd": "自定义",
"4f0f45b0312ca4ba": "1小时",
"51d04bc3d286f018": "近1自然日",
"51de3bcec137ab1b": "全部 {count} 个模型连通性测试成功",
"5228358a6db59fe7": "如:现在、2026-08-23 18:00",
@@ -156,6 +159,7 @@
"5f9acfb945229062": "你确认不想再看到此广告吗?",
"5fd2ec5a6e9b654c": "合计:{cost}",
"6078a681a306930d": "缓存写入",
"609640f72d422b57": "快捷时间范围",
"61a4c7bac12dc125": "异常调用:{count}",
"61c7d1f758b647d5": "调用信息",
"621f63a5f08384ac": "缓存读取:{tokens}",
@@ -205,6 +209,7 @@
"842b9f11cdd96bda": "开机启动",
"843ac7e15a5047a7": "确认导入旧版模型配置",
"844b8cc8dff7c1d8": "默认",
"859757ea792a73da": "30分钟",
"864597982c308d72": "已开启静默启动",
"86de7c4ee8fa7689": "同步模型",
"8716e1344b0daddb": "Cursor 官方",
@@ -342,6 +347,7 @@
"da7ae985487c38e6": "近1小时",
"daede9881787abe7": "备注",
"db340a9896306d08": "测试已取消",
"dbaa1c565cb1f108": "15分钟",
"dbd3596e4a86f3c2": "配置模型",
"ddde16f8839da3ce": "总请求",
"de8184da1ef88d03": "已配置",
@@ -372,6 +378,7 @@
"e77e3d58b0dcffaa": "耗时",
"e825a2a42c22380e": "模型类型",
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
"e85373502c372776": "1分钟",
"e8b1268c1e3610f2": "已存在",
"ea26b760e930a7ca": "调用观测",
"eb11e2df1d8ae387": "上游地址",
@@ -391,6 +398,7 @@
"f6dc1b1641600dd0": "正在导入…",
"f78265089144369a": "插件配置",
"f78413c36d36f090": "默认跟随操作系统;不支持的系统语言使用英文。当前:{language}",
"f784b165bd3fcb0e": "近4小时",
"f85537d1fd2ef6f6": "自定义概览筛选",
"f95ea7f4c063eea7": "未启用",
"f9615d05d8e18595": "清除{label}筛选",
+2 -1
View File
@@ -453,12 +453,13 @@ export const api = {
deleteModel: (hash: string) => request<void>(`/models/${hash}`, { method: "DELETE" }),
testModel: (hash: string, testId: string, signal?: AbortSignal) => request<ModelConnectivityResult>(`/models/${encodeURIComponent(hash)}/test/${encodeURIComponent(testId)}`, { method: "POST", signal }),
cancelModelTest: (hash: string, testId: string) => request<void>(`/models/${encodeURIComponent(hash)}/test/${encodeURIComponent(testId)}`, { method: "DELETE" }),
overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[] }) => {
overview: (filter?: { startMs: number; endMs: number; modelHashes?: string[]; bucketMs?: number }) => {
const params = new URLSearchParams();
if (filter) {
params.set("start_ms", String(filter.startMs));
params.set("end_ms", String(filter.endMs));
if (filter.modelHashes?.length) params.set("model_hashes", JSON.stringify(filter.modelHashes));
if (filter.bucketMs) params.set("bucket_ms", String(filter.bucketMs));
}
const query = params.toString();
return request<Overview>(`/overview${query ? `?${query}` : ""}`);
+5 -1
View File
@@ -169,7 +169,11 @@
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
-webkit-app-region: drag;
// Page actions (e.g. the overview time-range bar and the floating refresh
// button) must not act as a window drag handle; the dedicated drag layer in
// AppHeader owns window dragging. Otherwise these controls get dragged and
// double-click maximizes the window.
-webkit-app-region: no-drag;
}
.actions {
+7 -1
View File
@@ -16,6 +16,7 @@ pub struct OverviewRange {
start_ms: Option<i64>,
end_ms: Option<i64>,
model_hashes: Option<String>,
bucket_ms: Option<i64>,
}
pub async fn get(
@@ -24,7 +25,12 @@ pub async fn get(
) -> Result<Json<Overview>> {
Ok(Json(
service
.overview(range.start_ms, range.end_ms, range.model_hashes.as_deref())
.overview(
range.start_ms,
range.end_ms,
range.model_hashes.as_deref(),
range.bucket_ms,
)
.await?,
))
}
+4 -1
View File
@@ -329,8 +329,11 @@ impl ControlService {
start_ms: Option<i64>,
end_ms: Option<i64>,
model_hashes: Option<&str>,
bucket_ms: Option<i64>,
) -> Result<Overview> {
self.store.overview(start_ms, end_ms, model_hashes).await
self.store
.overview(start_ms, end_ms, model_hashes, bucket_ms)
.await
}
pub async fn create_models(&self, models: &[ModelConfigInput]) -> Result<Vec<ModelConfig>> {
+2 -2
View File
@@ -172,9 +172,9 @@ impl ConversationRuntime {
break;
}
TransportCommand::RunFinished { generation, finish } => {
if !current
if current
.as_ref()
.is_some_and(|current| current.id == generation)
.is_none_or(|current| current.id != generation)
{
continue;
}
+1 -5
View File
@@ -88,11 +88,7 @@ async fn generate_local(
}
let model_id = settings.model_id.trim();
ensure_configured_model(registry, model_id).await?;
let invocation = build_invocation(
&settings,
model_id,
build_user_content(&request, &diffs),
);
let invocation = build_invocation(&settings, model_id, build_user_content(&request, &diffs));
let provider = registry.conversations().dependencies().provider.clone();
let generated = generate(
provider,
+1 -1
View File
@@ -452,7 +452,7 @@ mod tests {
.await
.unwrap();
let overview = store
.overview(None, None, Some(&format!("[\"{plugin_model}\"]")))
.overview(None, None, Some(&format!("[\"{plugin_model}\"]")), None)
.await
.unwrap();
assert_eq!(overview.metrics.llm_calls, 1);
+26 -8
View File
@@ -15,6 +15,7 @@ use super::Store;
const OVERVIEW_DAYS: u64 = 365;
const MAX_RANGE_BUCKETS: i64 = 60;
const MAX_EXPLICIT_BUCKETS: i64 = 1440;
const MINUTE_MS: i64 = 60_000;
const HOUR_MS: i64 = 60 * MINUTE_MS;
const DAY_MS: i64 = 24 * HOUR_MS;
@@ -25,6 +26,7 @@ impl Store {
start_ms: Option<i64>,
end_ms: Option<i64>,
model_hashes: Option<&str>,
bucket_ms: Option<i64>,
) -> Result<Overview> {
let call_row = sqlx::query(
"SELECT
@@ -84,7 +86,7 @@ impl Store {
};
let (token_usage_granularity, bucket_ms, series_start_ms, bucket_count) =
token_usage_buckets(start_ms, end_ms);
token_usage_buckets(start_ms, end_ms, bucket_ms);
let rows = sqlx::query(&format!(
"SELECT
(created_at_ms / {bucket_ms}) * {bucket_ms} AS bucket_start_ms,
@@ -146,20 +148,36 @@ impl Store {
fn token_usage_buckets(
start_ms: Option<i64>,
end_ms: Option<i64>,
requested_bucket_ms: Option<i64>,
) -> (TokenUsageGranularity, i64, i64, i64) {
if let (Some(start_ms), Some(end_ms)) = (start_ms, end_ms) {
let duration_ms = end_ms.saturating_sub(start_ms).max(1);
let (granularity, bucket_ms) = if duration_ms <= HOUR_MS {
(TokenUsageGranularity::Minute, MINUTE_MS)
} else if duration_ms <= MAX_RANGE_BUCKETS * HOUR_MS {
(TokenUsageGranularity::Hour, HOUR_MS)
let explicit_bucket_ms = requested_bucket_ms.filter(|bucket_ms| *bucket_ms >= MINUTE_MS);
let bucket_ms = explicit_bucket_ms.unwrap_or({
if duration_ms <= HOUR_MS {
MINUTE_MS
} else if duration_ms <= MAX_RANGE_BUCKETS * HOUR_MS {
HOUR_MS
} else {
DAY_MS
}
});
let granularity = if bucket_ms < HOUR_MS {
TokenUsageGranularity::Minute
} else if bucket_ms < DAY_MS {
TokenUsageGranularity::Hour
} else {
(TokenUsageGranularity::Day, DAY_MS)
TokenUsageGranularity::Day
};
let max_buckets = if explicit_bucket_ms.is_some() {
MAX_EXPLICIT_BUCKETS
} else {
MAX_RANGE_BUCKETS
};
let last_bucket_ms = end_ms.saturating_sub(1).div_euclid(bucket_ms) * bucket_ms;
let first_bucket_ms = start_ms.div_euclid(bucket_ms) * bucket_ms;
let bucket_count = ((last_bucket_ms - first_bucket_ms).div_euclid(bucket_ms) + 1)
.clamp(1, MAX_RANGE_BUCKETS);
let bucket_count =
((last_bucket_ms - first_bucket_ms).div_euclid(bucket_ms) + 1).clamp(1, max_buckets);
let series_start_ms =
last_bucket_ms.saturating_sub((bucket_count - 1).saturating_mul(bucket_ms));
return (granularity, bucket_ms, series_start_ms, bucket_count);