mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:40:50 +08:00
feat(api): enhance statistics storage management with scope options
- Introduced a new type `StatisticsStorageScope` to specify the scope for clearing statistics. - Updated the `clearStatisticsStorage` API method to accept a scope parameter, allowing for selective clearing of detailed records or all statistics. - Modified the demo API to handle the new scope parameter appropriately. - Updated the SettingsPage component to include a selection for clearing scope, enhancing user control over statistics management. - Added new translations for the updated messages related to statistics clearing in both English and Chinese.
This commit is contained in:
@@ -116,6 +116,8 @@ export interface StatisticsStorage {
|
||||
trace_count: number;
|
||||
}
|
||||
|
||||
export type StatisticsStorageScope = "details" | "all";
|
||||
|
||||
export type ProxyMode = "system" | "custom";
|
||||
|
||||
export interface ProxySettings {
|
||||
@@ -330,7 +332,7 @@ export const api = {
|
||||
ports: () => request<PortSettings>("/settings/ports"),
|
||||
setPorts: (settings: PortSettings) => request<PortSettings>("/settings/ports", { method: "PUT", body: JSON.stringify(settings) }),
|
||||
statisticsStorage: () => request<StatisticsStorage>("/settings/storage/statistics"),
|
||||
clearStatisticsStorage: () => request<StatisticsStorage>("/settings/storage/statistics", { method: "DELETE" }),
|
||||
clearStatisticsStorage: (scope: StatisticsStorageScope) => request<StatisticsStorage>("/settings/storage/statistics", { method: "DELETE", body: JSON.stringify({ scope }) }),
|
||||
proxySettings: () => request<ProxySettings>("/settings/proxy"),
|
||||
setProxySettings: (settings: ProxySettingsInput) => request<ProxySettings>("/settings/proxy", { method: "PUT", body: JSON.stringify(settings) }),
|
||||
tabSettings: () => request<TabSettings>("/settings/tab"),
|
||||
|
||||
@@ -138,7 +138,10 @@ export function installDemoApi() {
|
||||
}
|
||||
if (path === "/settings/storage/statistics" && method === "GET") return json(storage);
|
||||
if (path === "/settings/storage/statistics") {
|
||||
storage = { bytes: 0, call_count: 0, trace_count: 0 };
|
||||
const scope = (body as { scope?: string } | null)?.scope ?? "details";
|
||||
storage = scope === "all"
|
||||
? { bytes: 0, call_count: 0, trace_count: 0 }
|
||||
: { ...storage, bytes: 0 };
|
||||
return json(storage);
|
||||
}
|
||||
if (path === "/settings/proxy" && method === "GET") return json(proxySettings);
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 180,
|
||||
"line": 185,
|
||||
"column": 82
|
||||
}
|
||||
]
|
||||
@@ -169,18 +169,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"08d742566ffc9fa3": {
|
||||
"source": "确定要清理所有统计数据吗?",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 289,
|
||||
"column": 16
|
||||
}
|
||||
]
|
||||
},
|
||||
"092b520558eff5f2": {
|
||||
"source": "未测试",
|
||||
"kind": "text",
|
||||
@@ -277,6 +265,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"0d2dab3d62eb73d6": {
|
||||
"source": "全部统计数据已清理",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 94,
|
||||
"column": 38
|
||||
}
|
||||
]
|
||||
},
|
||||
"0d5e2bdb15579fc4": {
|
||||
"source": "消息数",
|
||||
"kind": "text",
|
||||
@@ -301,7 +301,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 272,
|
||||
"line": 277,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -574,7 +574,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 276,
|
||||
"line": 281,
|
||||
"column": 60
|
||||
}
|
||||
]
|
||||
@@ -600,17 +600,17 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 63,
|
||||
"line": 64,
|
||||
"column": 42
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 185,
|
||||
"line": 190,
|
||||
"column": 22
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 212,
|
||||
"line": 217,
|
||||
"column": 58
|
||||
}
|
||||
]
|
||||
@@ -697,7 +697,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 250,
|
||||
"line": 255,
|
||||
"column": 43
|
||||
}
|
||||
]
|
||||
@@ -709,7 +709,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 118,
|
||||
"line": 119,
|
||||
"column": 15
|
||||
}
|
||||
]
|
||||
@@ -726,6 +726,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"23ae7a90b1b9816d": {
|
||||
"source": "清理范围",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 304,
|
||||
"column": 24
|
||||
}
|
||||
]
|
||||
},
|
||||
"23e49479e15e6770": {
|
||||
"source": "发现新版本 {version}",
|
||||
"kind": "template",
|
||||
@@ -871,12 +883,12 @@
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 176,
|
||||
"line": 181,
|
||||
"column": 81
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 291,
|
||||
"line": 296,
|
||||
"column": 22
|
||||
}
|
||||
]
|
||||
@@ -945,7 +957,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 142,
|
||||
"line": 143,
|
||||
"column": 15
|
||||
}
|
||||
]
|
||||
@@ -1029,7 +1041,7 @@
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 235,
|
||||
"line": 240,
|
||||
"column": 27
|
||||
}
|
||||
]
|
||||
@@ -1095,6 +1107,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"382f2e3419a02fef": {
|
||||
"source": "仅清理详细记录",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 306,
|
||||
"column": 42
|
||||
}
|
||||
]
|
||||
},
|
||||
"393e1241552b1870": {
|
||||
"source": "请求",
|
||||
"kind": "text",
|
||||
@@ -1155,7 +1179,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 283,
|
||||
"line": 288,
|
||||
"column": 14
|
||||
}
|
||||
]
|
||||
@@ -1179,7 +1203,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 235,
|
||||
"line": 240,
|
||||
"column": 39
|
||||
}
|
||||
]
|
||||
@@ -1229,12 +1253,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 242,
|
||||
"line": 247,
|
||||
"column": 22
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 248,
|
||||
"line": 253,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -1287,6 +1311,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"40a08e7cf320ae07": {
|
||||
"source": "确定要清理详细记录吗?",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 158,
|
||||
"column": 66
|
||||
}
|
||||
]
|
||||
},
|
||||
"4125fc7ba333524c": {
|
||||
"source": "默认亮色",
|
||||
"kind": "text",
|
||||
@@ -1294,7 +1330,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 267,
|
||||
"line": 272,
|
||||
"column": 52
|
||||
}
|
||||
]
|
||||
@@ -1647,7 +1683,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 174,
|
||||
"line": 179,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -1729,7 +1765,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 228,
|
||||
"line": 233,
|
||||
"column": 78
|
||||
}
|
||||
]
|
||||
@@ -1775,7 +1811,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 232,
|
||||
"line": 237,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -1835,18 +1871,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"5cae248525cb9140": {
|
||||
"source": "所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 298,
|
||||
"column": 14
|
||||
}
|
||||
]
|
||||
},
|
||||
"5d59857bf039cac9": {
|
||||
"source": "Cursor 助手 v{version}",
|
||||
"kind": "template",
|
||||
@@ -1856,7 +1880,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "layouts/AppHeader.tsx",
|
||||
"line": 31,
|
||||
"line": 34,
|
||||
"column": 18
|
||||
}
|
||||
]
|
||||
@@ -2004,7 +2028,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "api.ts",
|
||||
"line": 315,
|
||||
"line": 317,
|
||||
"column": 43
|
||||
}
|
||||
]
|
||||
@@ -2112,6 +2136,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"68102220092c1f0f": {
|
||||
"source": "详细记录已清理",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 94,
|
||||
"column": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
"68ad603fafe4e0d6": {
|
||||
"source": "导入旧版配置",
|
||||
"kind": "text",
|
||||
@@ -2261,7 +2297,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 164,
|
||||
"line": 169,
|
||||
"column": 16
|
||||
}
|
||||
]
|
||||
@@ -2273,7 +2309,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 259,
|
||||
"line": 264,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -2297,7 +2333,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "api.ts",
|
||||
"line": 310,
|
||||
"line": 312,
|
||||
"column": 43
|
||||
}
|
||||
]
|
||||
@@ -2321,7 +2357,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 292,
|
||||
"line": 297,
|
||||
"column": 23
|
||||
}
|
||||
]
|
||||
@@ -2364,23 +2400,11 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 136,
|
||||
"line": 137,
|
||||
"column": 83
|
||||
}
|
||||
]
|
||||
},
|
||||
"7e1f06318e80c3af": {
|
||||
"source": "统计数据已清理",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 93,
|
||||
"column": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"7e7df68f2a82e09e": {
|
||||
"source": "重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。",
|
||||
"kind": "text",
|
||||
@@ -2424,7 +2448,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "api.ts",
|
||||
"line": 259,
|
||||
"line": 261,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -2596,7 +2620,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 231,
|
||||
"line": 236,
|
||||
"column": 22
|
||||
}
|
||||
]
|
||||
@@ -2709,6 +2733,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"92156a483d4ba248": {
|
||||
"source": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 161,
|
||||
"column": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
"940a168911ade998": {
|
||||
"source": "每页条数",
|
||||
"kind": "text",
|
||||
@@ -2851,7 +2887,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 239,
|
||||
"line": 244,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -2887,7 +2923,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 56,
|
||||
"line": 57,
|
||||
"column": 34
|
||||
}
|
||||
]
|
||||
@@ -2957,7 +2993,7 @@
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 177,
|
||||
"line": 182,
|
||||
"column": 133
|
||||
}
|
||||
]
|
||||
@@ -3349,6 +3385,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"b67e408132f4dbd7": {
|
||||
"source": "确定要清理全部统计数据吗?",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 158,
|
||||
"column": 45
|
||||
}
|
||||
]
|
||||
},
|
||||
"b75a46aad3e7c132": {
|
||||
"source": "非缓存输入:{tokens}",
|
||||
"kind": "template",
|
||||
@@ -3370,7 +3418,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 275,
|
||||
"line": 280,
|
||||
"column": 22
|
||||
}
|
||||
]
|
||||
@@ -3496,7 +3544,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 267,
|
||||
"line": 272,
|
||||
"column": 40
|
||||
}
|
||||
]
|
||||
@@ -3695,12 +3743,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 162,
|
||||
"line": 167,
|
||||
"column": 22
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 168,
|
||||
"line": 173,
|
||||
"column": 20
|
||||
}
|
||||
]
|
||||
@@ -3712,7 +3760,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 199,
|
||||
"line": 204,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -3741,6 +3789,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"d1a3d72618d1ed27": {
|
||||
"source": "所有调用汇总、详细内容和追踪记录都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 160,
|
||||
"column": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
"d2d648bd1c94b7f9": {
|
||||
"source": "认证",
|
||||
"kind": "text",
|
||||
@@ -3982,7 +4042,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 306,
|
||||
"line": 316,
|
||||
"column": 30
|
||||
}
|
||||
]
|
||||
@@ -4188,7 +4248,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 69,
|
||||
"line": 70,
|
||||
"column": 17
|
||||
}
|
||||
]
|
||||
@@ -4258,7 +4318,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 159,
|
||||
"line": 164,
|
||||
"column": 26
|
||||
}
|
||||
]
|
||||
@@ -4347,7 +4407,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 218,
|
||||
"line": 223,
|
||||
"column": 16
|
||||
}
|
||||
]
|
||||
@@ -4436,7 +4496,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 243,
|
||||
"line": 248,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -4510,7 +4570,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 186,
|
||||
"line": 191,
|
||||
"column": 21
|
||||
}
|
||||
]
|
||||
@@ -4619,6 +4679,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"fec7210590309465": {
|
||||
"source": "清理全部统计数据",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 307,
|
||||
"column": 38
|
||||
}
|
||||
]
|
||||
},
|
||||
"ff509c9ba052a21c": {
|
||||
"source": "保存中…",
|
||||
"kind": "text",
|
||||
@@ -4636,7 +4708,7 @@
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 177,
|
||||
"line": 182,
|
||||
"column": 121
|
||||
}
|
||||
]
|
||||
@@ -4660,17 +4732,17 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 64,
|
||||
"line": 65,
|
||||
"column": 46
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 198,
|
||||
"line": 203,
|
||||
"column": 22
|
||||
},
|
||||
{
|
||||
"file": "pages/SettingsPage.tsx",
|
||||
"line": 213,
|
||||
"line": 218,
|
||||
"column": 58
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
"076832c1b2de22c3": "Cache write: {tokens}",
|
||||
"07879e064ae16542": "Estimated output: {tokens}",
|
||||
"07c657ed4747126e": "Anthropic extra parameters",
|
||||
"08d742566ffc9fa3": "Clear all statistics?",
|
||||
"092b520558eff5f2": "Not tested",
|
||||
"099008ea7a42ebd1": "All custom Header values must be strings",
|
||||
"09ebc2643631ba25": "Estimated value",
|
||||
"0b96da34f6fbdd3b": "Cache read: {tokens} × ${price}/1M = {cost}",
|
||||
"0c70665b6eb65f1a": "No",
|
||||
"0c72229b7db0e1a9": "Model output",
|
||||
"0d2dab3d62eb73d6": "All statistics cleared",
|
||||
"0d5e2bdb15579fc4": "Messages",
|
||||
"0e41f8e3d59ec47b": "Storage management",
|
||||
"0e67021ebf0a3580": "Import complete: added {imported} models and skipped {skipped} existing models",
|
||||
@@ -48,6 +48,7 @@
|
||||
"217cfe7db1e3d10a": "Use system language",
|
||||
"22c6b4eb4caee6ae": "Proxy settings saved",
|
||||
"22d7895ea5fca72e": "By provider",
|
||||
"23ae7a90b1b9816d": "Clear scope",
|
||||
"23e49479e15e6770": "Version {version} is available",
|
||||
"24a0a24864454575": "Existing, skipped",
|
||||
"2555d6c7fbb7e070": "Enter a model ID directly or load models returned by the API.",
|
||||
@@ -71,6 +72,7 @@
|
||||
"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",
|
||||
"393e1241552b1870": "Request",
|
||||
"398f8e6c6f0a0b97": "Continue selecting or typing",
|
||||
"39f52eee100131d7": "Cached input",
|
||||
@@ -86,6 +88,7 @@
|
||||
"3f6c25aa329163a4": "The original endpoint path is appended to this service address.",
|
||||
"3fd118e2ffe0b2b6": "Cancel all tests",
|
||||
"3fd47edce45b3603": "Close",
|
||||
"40a08e7cf320ae07": "Clear detailed records?",
|
||||
"4125fc7ba333524c": "Default light",
|
||||
"42655ed8e4108ae2": "Input (non-cached)",
|
||||
"42a1d9e5b037c210": "Bytes",
|
||||
@@ -124,7 +127,6 @@
|
||||
"5b17f59d33bde39e": "Error: {error}",
|
||||
"5ba65a74c4e792c5": "By type",
|
||||
"5c55a67935af8f45": "All",
|
||||
"5cae248525cb9140": "All call records and detailed traces will be deleted. Model configuration, CA, and application settings are unaffected. This action cannot be undone.",
|
||||
"5d59857bf039cac9": "Cursor Assistant v{version}",
|
||||
"5f8d556a9c47da3c": "Launch at login disabled",
|
||||
"5f9acfb945229062": "Are you sure you no longer want to see this ad?",
|
||||
@@ -144,6 +146,7 @@
|
||||
"65a6318e07ec1e07": "Tools",
|
||||
"65cb9a7b4f620b6b": "Prompt {tokens}",
|
||||
"680680288a6d2ad2": "Show in Dock",
|
||||
"68102220092c1f0f": "Detailed records cleared",
|
||||
"68ad603fafe4e0d6": "Import legacy configuration",
|
||||
"68ea5dd4d7af20e6": "System settings",
|
||||
"6a9906c79f26c0ba": "Start time",
|
||||
@@ -163,7 +166,6 @@
|
||||
"7cea2f3c46565d29": "OpenAI extra parameters",
|
||||
"7d9f043f8f7ab45c": "Version {version} is available in Settings",
|
||||
"7e0891860c9e6374": "TAB service address is required",
|
||||
"7e1f06318e80c3af": "Statistics cleared",
|
||||
"7e7df68f2a82e09e": "Importing the same configuration again will not create duplicate models. Existing models are skipped automatically.",
|
||||
"7f3c8312816fe26a": "Refreshing…",
|
||||
"7f68ebad19ba6bcd": "Check for updates",
|
||||
@@ -188,6 +190,7 @@
|
||||
"90800c48a1dd0655": "{label} must be a JSON object",
|
||||
"919cb0ce0c8db4e7": "Leave blank to keep the current password",
|
||||
"91aaf184cfc17ffd": "Overview",
|
||||
"92156a483d4ba248": "Only request, response, and trace attachments are deleted; call summaries, metrics, and configuration are kept.",
|
||||
"940a168911ade998": "Items per page",
|
||||
"946b3ffc02f026c0": "Delete this model?",
|
||||
"966498853d801a52": "TAB connection",
|
||||
@@ -231,6 +234,7 @@
|
||||
"b502b1d414664337": "Prompt: {tokens}",
|
||||
"b5141d3d19e9a048": "Yes",
|
||||
"b6725f218ebaef26": "Dock icon shown",
|
||||
"b67e408132f4dbd7": "Clear all statistics?",
|
||||
"b75a46aad3e7c132": "Non-cached input: {tokens}",
|
||||
"b79354009c614ae9": "Statistics",
|
||||
"b86967982067d295": " (estimated)",
|
||||
@@ -259,6 +263,7 @@
|
||||
"cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.",
|
||||
"d0bfccc77315d887": "Last month",
|
||||
"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",
|
||||
"d34335433395cd3a": "Start Cursor BYOK automatically after signing in.",
|
||||
"d3716cc5a2f5a810": "Server address",
|
||||
@@ -326,6 +331,7 @@
|
||||
"fdc4cabc370fa3f7": "No options",
|
||||
"fea405f9b01d1416": "Summary",
|
||||
"fec45092945f8790": "User guide",
|
||||
"fec7210590309465": "Clear all statistics",
|
||||
"ff509c9ba052a21c": "Saving…",
|
||||
"ff66b1c7e010bbc2": "Paste the authorization command into the terminal and enter your password, then click the button below",
|
||||
"ff673195f05fb056": "Service port"
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
"076832c1b2de22c3": "缓存写入:{tokens}",
|
||||
"07879e064ae16542": "输出推算:{tokens}",
|
||||
"07c657ed4747126e": "Anthropic 额外参数",
|
||||
"08d742566ffc9fa3": "确定要清理所有统计数据吗?",
|
||||
"092b520558eff5f2": "未测试",
|
||||
"099008ea7a42ebd1": "自定义 Headers 的值必须都是字符串",
|
||||
"09ebc2643631ba25": "价值估算",
|
||||
"0b96da34f6fbdd3b": "缓存读取:{tokens} × ${price}/1M = {cost}",
|
||||
"0c70665b6eb65f1a": "否",
|
||||
"0c72229b7db0e1a9": "模型输出",
|
||||
"0d2dab3d62eb73d6": "全部统计数据已清理",
|
||||
"0d5e2bdb15579fc4": "消息数",
|
||||
"0e41f8e3d59ec47b": "存储管理",
|
||||
"0e67021ebf0a3580": "导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型",
|
||||
@@ -48,6 +48,7 @@
|
||||
"217cfe7db1e3d10a": "跟随系统",
|
||||
"22c6b4eb4caee6ae": "代理设置已保存",
|
||||
"22d7895ea5fca72e": "按供应商",
|
||||
"23ae7a90b1b9816d": "清理范围",
|
||||
"23e49479e15e6770": "发现新版本 {version}",
|
||||
"24a0a24864454575": "已存在,跳过",
|
||||
"2555d6c7fbb7e070": "可以直接输入模型标识,也可以读取接口返回的模型列表。",
|
||||
@@ -71,6 +72,7 @@
|
||||
"37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
|
||||
"378bb0eec39fa8a2": "最后一页",
|
||||
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
|
||||
"382f2e3419a02fef": "仅清理详细记录",
|
||||
"393e1241552b1870": "请求",
|
||||
"398f8e6c6f0a0b97": "继续选择或输入",
|
||||
"39f52eee100131d7": "缓存输入",
|
||||
@@ -86,6 +88,7 @@
|
||||
"3f6c25aa329163a4": "原接口路径会追加到此服务地址。",
|
||||
"3fd118e2ffe0b2b6": "取消全部测试",
|
||||
"3fd47edce45b3603": "关闭",
|
||||
"40a08e7cf320ae07": "确定要清理详细记录吗?",
|
||||
"4125fc7ba333524c": "默认亮色",
|
||||
"42655ed8e4108ae2": "输入(非缓存)",
|
||||
"42a1d9e5b037c210": "字节数",
|
||||
@@ -124,7 +127,6 @@
|
||||
"5b17f59d33bde39e": "错误:{error}",
|
||||
"5ba65a74c4e792c5": "按类型",
|
||||
"5c55a67935af8f45": "全部",
|
||||
"5cae248525cb9140": "所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||||
"5d59857bf039cac9": "Cursor 助手 v{version}",
|
||||
"5f8d556a9c47da3c": "已关闭开机启动",
|
||||
"5f9acfb945229062": "你确认不想再看到此广告吗?",
|
||||
@@ -144,6 +146,7 @@
|
||||
"65a6318e07ec1e07": "工具数",
|
||||
"65cb9a7b4f620b6b": "提示词 {tokens}",
|
||||
"680680288a6d2ad2": "在 Dock 栏显示",
|
||||
"68102220092c1f0f": "详细记录已清理",
|
||||
"68ad603fafe4e0d6": "导入旧版配置",
|
||||
"68ea5dd4d7af20e6": "系统设置",
|
||||
"6a9906c79f26c0ba": "开始时间",
|
||||
@@ -163,7 +166,6 @@
|
||||
"7cea2f3c46565d29": "OpenAI 额外参数",
|
||||
"7d9f043f8f7ab45c": "发现新版本 {version},可在设置中安装",
|
||||
"7e0891860c9e6374": "TAB 服务地址不能为空",
|
||||
"7e1f06318e80c3af": "统计数据已清理",
|
||||
"7e7df68f2a82e09e": "重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。",
|
||||
"7f3c8312816fe26a": "刷新中…",
|
||||
"7f68ebad19ba6bcd": "检查更新",
|
||||
@@ -188,6 +190,7 @@
|
||||
"90800c48a1dd0655": "{label} 必须是 JSON 对象",
|
||||
"919cb0ce0c8db4e7": "留空表示保留当前密码",
|
||||
"91aaf184cfc17ffd": "数据概览",
|
||||
"92156a483d4ba248": "仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。",
|
||||
"940a168911ade998": "每页条数",
|
||||
"946b3ffc02f026c0": "确定删除这个模型吗?",
|
||||
"966498853d801a52": "TAB 选择",
|
||||
@@ -231,6 +234,7 @@
|
||||
"b502b1d414664337": "提示词:{tokens}",
|
||||
"b5141d3d19e9a048": "是",
|
||||
"b6725f218ebaef26": "已显示 Dock 栏图标",
|
||||
"b67e408132f4dbd7": "确定要清理全部统计数据吗?",
|
||||
"b75a46aad3e7c132": "非缓存输入:{tokens}",
|
||||
"b79354009c614ae9": "统计数据",
|
||||
"b86967982067d295": "(估算)",
|
||||
@@ -259,6 +263,7 @@
|
||||
"cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。",
|
||||
"d0bfccc77315d887": "近一个月",
|
||||
"d1251cd752d4ec25": "留空时使用 adaptive thinking。",
|
||||
"d1a3d72618d1ed27": "所有调用汇总、详细内容和追踪记录都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||||
"d2d648bd1c94b7f9": "认证",
|
||||
"d34335433395cd3a": "登录系统后自动启动 Cursor BYOK。",
|
||||
"d3716cc5a2f5a810": "服务器地址",
|
||||
@@ -326,6 +331,7 @@
|
||||
"fdc4cabc370fa3f7": "暂无选项",
|
||||
"fea405f9b01d1416": "概览",
|
||||
"fec45092945f8790": "使用教程",
|
||||
"fec7210590309465": "清理全部统计数据",
|
||||
"ff509c9ba052a21c": "保存中…",
|
||||
"ff66b1c7e010bbc2": "请在终端中粘贴授权命令并输入密码,完成后点击下方按钮",
|
||||
"ff673195f05fb056": "服务端口"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type TabSettings } from "../api";
|
||||
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type StatisticsStorageScope, type TabSettings } from "../api";
|
||||
import { PageContent } from "../components/layout/PageContent";
|
||||
import { LegacyModelImport } from "../components/models/LegacyModelImport";
|
||||
import { AppLifecycleSettingsCard } from "../components/settings/AppLifecycleSettingsCard";
|
||||
@@ -27,6 +27,7 @@ export function SettingsPage() {
|
||||
const [savingPorts, setSavingPorts] = useState(false);
|
||||
const [storage, setStorage] = useState<StatisticsStorage | null>(null);
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const [clearScope, setClearScope] = useState<StatisticsStorageScope>("details");
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [outboundProxy, setOutboundProxy] = useState<ProxySettings | null>(null);
|
||||
const [proxyDraft, setProxyDraft] = useState<ProxySettingsInput>({ mode: "system", address: "", auth_enabled: false, username: "", password: "" });
|
||||
@@ -87,10 +88,10 @@ export function SettingsPage() {
|
||||
const clearStorage = async () => {
|
||||
try {
|
||||
setClearing(true);
|
||||
setStorage(await api.clearStatisticsStorage());
|
||||
setStorage(await api.clearStatisticsStorage(clearScope));
|
||||
setConfirmClear(false);
|
||||
await appStore.refresh();
|
||||
message(t("统计数据已清理"));
|
||||
message(clearScope === "all" ? t("全部统计数据已清理") : t("详细记录已清理"));
|
||||
} catch (cause) {
|
||||
message(cause instanceof Error ? cause.message : String(cause));
|
||||
} finally {
|
||||
@@ -154,6 +155,10 @@ export function SettingsPage() {
|
||||
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; }
|
||||
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
||||
};
|
||||
const clearTitle = clearScope === "all" ? t("确定要清理全部统计数据吗?") : t("确定要清理详细记录吗?");
|
||||
const clearDescription = clearScope === "all"
|
||||
? t("所有调用汇总、详细内容和追踪记录都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。")
|
||||
: t("仅删除请求、响应和追踪附件等详细内容,保留调用汇总、统计指标和配置。");
|
||||
const content = (
|
||||
<div className={styles.page}>
|
||||
<TitledCard title={t("调用观测")}>
|
||||
@@ -278,7 +283,7 @@ export function SettingsPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={styles.textButton}
|
||||
onClick={() => setConfirmClear(true)}
|
||||
onClick={() => { setClearScope("details"); setConfirmClear(true); }}
|
||||
>
|
||||
{t("清理存储空间")}
|
||||
</button>
|
||||
@@ -286,7 +291,7 @@ export function SettingsPage() {
|
||||
</TitledCard>
|
||||
<ConfirmDialog
|
||||
open={confirmClear}
|
||||
title={t("确定要清理所有统计数据吗?")}
|
||||
title={clearTitle}
|
||||
busy={clearing}
|
||||
cancelLabel={t("取消")}
|
||||
confirmLabel={t("确认清理")}
|
||||
@@ -294,11 +299,16 @@ export function SettingsPage() {
|
||||
onConfirm={() => void clearStorage()}
|
||||
>
|
||||
<div className={styles.confirmContent}>
|
||||
<small>
|
||||
{t(
|
||||
"所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||||
)}
|
||||
</small>
|
||||
<Select
|
||||
value={clearScope}
|
||||
ariaLabel={t("清理范围")}
|
||||
options={[
|
||||
{ value: "details", label: t("仅清理详细记录") },
|
||||
{ value: "all", label: t("清理全部统计数据") },
|
||||
]}
|
||||
onChange={(value) => setClearScope(value as StatisticsStorageScope)}
|
||||
/>
|
||||
<small>{clearDescription}</small>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
||||
@@ -565,6 +565,10 @@ impl ControlService {
|
||||
self.store.clear_statistics_storage().await
|
||||
}
|
||||
|
||||
pub async fn clear_all_statistics_storage(&self) -> Result<StatisticsStorage> {
|
||||
self.store.clear_all_statistics_storage().await
|
||||
}
|
||||
|
||||
pub async fn proxy_settings(&self) -> Result<ProxySettings> {
|
||||
self.store.proxy_settings().await
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::Result;
|
||||
use axum::{extract::State, Json};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::store::{
|
||||
DesktopSettings, PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage,
|
||||
TabSettings,
|
||||
StatisticsStorageScope, TabSettings,
|
||||
};
|
||||
|
||||
use super::{ControlService, ObservabilitySettings};
|
||||
@@ -36,8 +37,20 @@ pub async fn get_storage(State(service): State<ControlService>) -> Result<Json<S
|
||||
|
||||
pub async fn clear_storage(
|
||||
State(service): State<ControlService>,
|
||||
input: Option<Json<ClearStorageInput>>,
|
||||
) -> Result<Json<StatisticsStorage>> {
|
||||
Ok(Json(service.clear_statistics_storage().await?))
|
||||
let scope = input.map(|Json(input)| input.scope).unwrap_or_default();
|
||||
let storage = match scope {
|
||||
StatisticsStorageScope::Details => service.clear_statistics_storage().await?,
|
||||
StatisticsStorageScope::All => service.clear_all_statistics_storage().await?,
|
||||
};
|
||||
Ok(Json(storage))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ClearStorageInput {
|
||||
#[serde(default)]
|
||||
pub scope: StatisticsStorageScope,
|
||||
}
|
||||
|
||||
pub async fn get_proxy(State(service): State<ControlService>) -> Result<Json<ProxySettings>> {
|
||||
|
||||
@@ -64,6 +64,7 @@ impl CursorActor {
|
||||
};
|
||||
match command {
|
||||
CursorCommand::Abort => {
|
||||
handle.mark_conversation_cancelled();
|
||||
handle.cancel();
|
||||
}
|
||||
CursorCommand::Finished => {
|
||||
@@ -76,6 +77,24 @@ impl CursorActor {
|
||||
Some(pb::agent_client_message::Message::RunRequest(
|
||||
request,
|
||||
)) => {
|
||||
if let Some(conversation_id) =
|
||||
request.conversation_id.as_deref()
|
||||
{
|
||||
if let Err(error) =
|
||||
handle.set_conversation_id(conversation_id)
|
||||
{
|
||||
tracing::error!(
|
||||
request_id = handle.request_id(),
|
||||
%error,
|
||||
"invalid Cursor conversation id"
|
||||
);
|
||||
let _ =
|
||||
crate::cursor::lifecycle::fail(&handle, &error);
|
||||
let _ =
|
||||
handle.command(CursorCommand::Finished).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some((results, runtime_actions, dependencies)) =
|
||||
run_resources.take()
|
||||
{
|
||||
@@ -357,8 +376,12 @@ impl CursorActor {
|
||||
) => match action.action {
|
||||
Some(
|
||||
pb::conversation_action::Action::UserMessageAction(_),
|
||||
)
|
||||
| Some(pb::conversation_action::Action::CancelAction(_)) => {
|
||||
) => {
|
||||
handle.mark_conversation_cancelled();
|
||||
handle.cancel();
|
||||
}
|
||||
Some(pb::conversation_action::Action::CancelAction(_)) => {
|
||||
handle.mark_conversation_cancelled();
|
||||
handle.cancel();
|
||||
}
|
||||
Some(
|
||||
|
||||
@@ -43,6 +43,33 @@ impl DecodedAppend {
|
||||
request.conversation_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn is_background_task_completion(&self) -> bool {
|
||||
let Some(agent::agent_client_message::Message::RunRequest(request)) =
|
||||
self.message.message.as_ref()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
request
|
||||
.action
|
||||
.as_ref()
|
||||
.and_then(|action| action.action.as_ref()),
|
||||
Some(agent::conversation_action::Action::BackgroundTaskCompletionAction(_))
|
||||
)
|
||||
}
|
||||
|
||||
fn is_runtime_cancellation(&self) -> bool {
|
||||
matches!(
|
||||
self.message.message.as_ref(),
|
||||
Some(agent::agent_client_message::Message::ConversationAction(action))
|
||||
if matches!(
|
||||
action.action.as_ref(),
|
||||
Some(agent::conversation_action::Action::CancelAction(_))
|
||||
| Some(agent::conversation_action::Action::UserMessageAction(_))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn trace_metadata(&self) -> serde_json::Value {
|
||||
let Some(message) = self.message.message.as_ref() else {
|
||||
return serde_json::json!({
|
||||
@@ -163,7 +190,29 @@ pub async fn append(
|
||||
request: DecodedAppend,
|
||||
parent: Option<CursorParent>,
|
||||
) -> Result<ai::BidiAppendResponse> {
|
||||
if let Some(conversation_id) = request.conversation_id() {
|
||||
if request.is_background_task_completion()
|
||||
&& registry.conversation_cancelled(conversation_id)
|
||||
{
|
||||
tracing::info!(
|
||||
request_id = %request.request_id,
|
||||
%conversation_id,
|
||||
"dropping background task completion for cancelled conversation"
|
||||
);
|
||||
return Ok(ai::BidiAppendResponse {});
|
||||
}
|
||||
if !request.is_background_task_completion() {
|
||||
registry.clear_conversation_cancelled(conversation_id);
|
||||
}
|
||||
}
|
||||
let handle = registry.get_or_create(&request.request_id).await?;
|
||||
if let Some(conversation_id) = request.conversation_id() {
|
||||
handle.set_conversation_id(conversation_id)?;
|
||||
}
|
||||
if request.is_runtime_cancellation() {
|
||||
handle.mark_conversation_cancelled();
|
||||
handle.cancel();
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
handle.set_parent(parent)?;
|
||||
}
|
||||
@@ -225,4 +274,21 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(decoded.model_id(), Some("grok-4.6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_background_task_completion_actions() {
|
||||
let decoded = decode(&encoded(agent::AgentRunRequest {
|
||||
action: Some(agent::ConversationAction {
|
||||
action: Some(
|
||||
agent::conversation_action::Action::BackgroundTaskCompletionAction(
|
||||
agent::BackgroundTaskCompletionAction::default(),
|
||||
),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(decoded.is_background_task_completion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
@@ -29,6 +29,8 @@ pub struct CursorSessionHandle {
|
||||
commands: mpsc::Sender<CursorCommand>,
|
||||
output: Arc<OutputHub>,
|
||||
cancellation: CancellationToken,
|
||||
conversation_id: Arc<OnceLock<String>>,
|
||||
cancelled_conversations: Arc<parking_lot::Mutex<HashSet<String>>>,
|
||||
parent: Arc<OnceLock<CursorParent>>,
|
||||
trace: Option<CursorTraceRecorder>,
|
||||
}
|
||||
@@ -43,6 +45,35 @@ impl CursorSessionHandle {
|
||||
pub fn request_id(&self) -> &str {
|
||||
&self.request_id
|
||||
}
|
||||
pub fn set_conversation_id(&self, conversation_id: &str) -> Result<()> {
|
||||
if conversation_id.is_empty() {
|
||||
return Err(crate::Error::Protocol(
|
||||
"Cursor conversation id is required".into(),
|
||||
));
|
||||
}
|
||||
if self
|
||||
.conversation_id
|
||||
.get()
|
||||
.is_some_and(|current| current != conversation_id)
|
||||
{
|
||||
return Err(crate::Error::Protocol(format!(
|
||||
"conflicting conversation ids for request {}",
|
||||
self.request_id
|
||||
)));
|
||||
}
|
||||
let _ = self.conversation_id.set(conversation_id.into());
|
||||
Ok(())
|
||||
}
|
||||
pub fn conversation_id(&self) -> Option<&str> {
|
||||
self.conversation_id.get().map(String::as_str)
|
||||
}
|
||||
pub fn mark_conversation_cancelled(&self) {
|
||||
if let Some(conversation_id) = self.conversation_id() {
|
||||
self.cancelled_conversations
|
||||
.lock()
|
||||
.insert(conversation_id.to_owned());
|
||||
}
|
||||
}
|
||||
pub fn subscribe(&self) -> mpsc::UnboundedReceiver<Bytes> {
|
||||
self.output.subscribe()
|
||||
}
|
||||
@@ -160,6 +191,7 @@ struct RegistryInner {
|
||||
store: Store,
|
||||
provider: Arc<dyn Provider>,
|
||||
compiler: PromptCompiler,
|
||||
cancelled_conversations: Arc<parking_lot::Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -188,6 +220,7 @@ impl CursorSessionRegistry {
|
||||
store,
|
||||
provider,
|
||||
compiler,
|
||||
cancelled_conversations: Arc::new(parking_lot::Mutex::new(HashSet::new())),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -205,6 +238,8 @@ impl CursorSessionRegistry {
|
||||
commands,
|
||||
output,
|
||||
cancellation,
|
||||
conversation_id: Arc::new(OnceLock::new()),
|
||||
cancelled_conversations: self.inner.cancelled_conversations.clone(),
|
||||
parent: Arc::new(OnceLock::new()),
|
||||
trace,
|
||||
};
|
||||
@@ -262,6 +297,20 @@ impl CursorSessionRegistry {
|
||||
.contains_key(request_id)
|
||||
}
|
||||
|
||||
pub(crate) fn conversation_cancelled(&self, conversation_id: &str) -> bool {
|
||||
self.inner
|
||||
.cancelled_conversations
|
||||
.lock()
|
||||
.contains(conversation_id)
|
||||
}
|
||||
|
||||
pub(crate) fn clear_conversation_cancelled(&self, conversation_id: &str) {
|
||||
self.inner
|
||||
.cancelled_conversations
|
||||
.lock()
|
||||
.remove(conversation_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_route(&self, request_id: &str) -> CursorRoute {
|
||||
loop {
|
||||
let changed = self.inner.route_changed.notified();
|
||||
|
||||
@@ -49,7 +49,8 @@ impl Provider for AnthropicProvider {
|
||||
let recorder = self.recorder.clone();
|
||||
Box::pin(try_stream! {
|
||||
let ModelInvocation { call_id, request, .. } = invocation;
|
||||
let messages = anthropic_messages(&request.history)?;
|
||||
let mut messages = anthropic_messages(&request.history)?;
|
||||
mark_cache_breakpoint(&mut messages);
|
||||
let max_tokens = request.model.max_output_tokens.or(config.max_output_tokens)
|
||||
.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS);
|
||||
let mut body = json!({
|
||||
@@ -372,6 +373,24 @@ fn anthropic_messages(messages: &[ProjectedMessage]) -> Result<Vec<Value>> {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn mark_cache_breakpoint(messages: &mut [Value]) {
|
||||
for message in messages {
|
||||
let Some(content) = message.get_mut("content").and_then(Value::as_array_mut) else {
|
||||
continue;
|
||||
};
|
||||
for block in content {
|
||||
let kind = block.get("type").and_then(Value::as_str);
|
||||
if matches!(kind, Some("thinking" | "redacted_thinking")) {
|
||||
continue;
|
||||
}
|
||||
if let Some(block) = block.as_object_mut() {
|
||||
block.insert("cache_control".into(), json!({"type": "ephemeral"}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn anthropic_parts(role: &Role, parts: &[ContentPart]) -> Result<Vec<Value>> {
|
||||
parts
|
||||
.iter()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Storage accounting and cleanup for disposable observability data.
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
@@ -13,6 +13,14 @@ pub struct StatisticsStorage {
|
||||
pub trace_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StatisticsStorageScope {
|
||||
#[default]
|
||||
Details,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub async fn statistics_storage(&self) -> Result<StatisticsStorage> {
|
||||
let (bytes, call_count, trace_count) = sqlx::query_as::<_, (i64, i64, i64)>(
|
||||
@@ -56,6 +64,15 @@ impl Store {
|
||||
pub async fn clear_statistics_storage(&self) -> Result<StatisticsStorage> {
|
||||
let _write = self.writes.lock().await;
|
||||
let mut transaction = self.pool.begin().await?;
|
||||
Self::clear_detail_storage_tx(&mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
self.statistics_storage().await
|
||||
}
|
||||
|
||||
pub async fn clear_all_statistics_storage(&self) -> Result<StatisticsStorage> {
|
||||
let _write = self.writes.lock().await;
|
||||
let mut transaction = self.pool.begin().await?;
|
||||
Self::clear_trace_artifacts_tx(&mut transaction).await?;
|
||||
sqlx::query("DELETE FROM llm_calls")
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@@ -65,6 +82,59 @@ impl Store {
|
||||
transaction.commit().await?;
|
||||
self.statistics_storage().await
|
||||
}
|
||||
|
||||
async fn clear_detail_storage_tx(
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
) -> Result<()> {
|
||||
sqlx::query("DELETE FROM llm_call_requests")
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM llm_call_response_chunks")
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
Self::clear_trace_artifacts_tx(transaction).await
|
||||
}
|
||||
|
||||
async fn clear_trace_artifacts_tx(
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
"CREATE TEMP TABLE IF NOT EXISTS clear_statistics_blob_ids(
|
||||
blob_id BLOB PRIMARY KEY
|
||||
)",
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM clear_statistics_blob_ids")
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO clear_statistics_blob_ids(blob_id)
|
||||
SELECT blob_id FROM cursor_run_trace_artifacts",
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM cursor_run_trace_artifacts")
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"DELETE FROM blobs
|
||||
WHERE blob_id IN (SELECT blob_id FROM clear_statistics_blob_ids)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM cursor_run_trace_artifacts a WHERE a.blob_id = blobs.blob_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM blob_edges e
|
||||
WHERE e.parent_blob_id = blobs.blob_id OR e.child_blob_id = blobs.blob_id
|
||||
)",
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
sqlx::query("DROP TABLE clear_statistics_blob_ids")
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -73,7 +143,7 @@ mod tests {
|
||||
use crate::model::{ModelConfigInput, ModelType, OPENAI_CHAT_ENDPOINT};
|
||||
|
||||
#[tokio::test]
|
||||
async fn clears_observability_without_removing_configuration() {
|
||||
async fn clears_detail_storage_without_removing_configuration() {
|
||||
let store = Store::connect("sqlite::memory:").await.unwrap();
|
||||
store
|
||||
.create_model(&ModelConfigInput {
|
||||
@@ -106,12 +176,16 @@ mod tests {
|
||||
|
||||
assert!(store.statistics_storage().await.unwrap().bytes > 0);
|
||||
let cleared = store.clear_statistics_storage().await.unwrap();
|
||||
assert_eq!(cleared.bytes, 0);
|
||||
assert_eq!(cleared.call_count, 0);
|
||||
assert_eq!(cleared.call_count, 1);
|
||||
assert_eq!(cleared.trace_count, 0);
|
||||
assert!(cleared.bytes > 0);
|
||||
assert!(store.llm_call_request("call-1").await.unwrap().is_none());
|
||||
assert!(store.llm_call_chunks("call-1").await.unwrap().is_empty());
|
||||
let model_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM model_configs")
|
||||
.fetch_one(store.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(model_count, 1);
|
||||
|
||||
store
|
||||
@@ -127,5 +201,11 @@ mod tests {
|
||||
.record_llm_chunk("call-1", 0, 1, b"data", true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(store.llm_call_request("call-1").await.unwrap().is_some());
|
||||
assert_eq!(store.llm_call_chunks("call-1").await.unwrap().len(), 1);
|
||||
let cleared = store.clear_all_statistics_storage().await.unwrap();
|
||||
assert_eq!(cleared.bytes, 0);
|
||||
assert_eq!(cleared.call_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use cursor_server::{
|
||||
cursor::{
|
||||
bidi_append::{self, DecodedAppend},
|
||||
connect,
|
||||
prompting::{PromptAssets, PromptCompiler},
|
||||
proto::agent::v1 as pb,
|
||||
@@ -20,6 +21,45 @@ use prost::Message;
|
||||
const FOLLOW_UP: &str = "Perform any necessary follow-up actions in response to the subagent completion above. If no follow-up work is needed, no further action is required. If you mention an agent or subagent in your response, link it with the `[Name](id)` Don't use generic label such as `[agent]`, `[worker]`, or `[subagent]`.";
|
||||
const SHELL_FOLLOW_UP: &str = "Briefly inform the user about the task result and perform any follow-up actions (if needed). If there's no follow-ups needed, don't explicitly say that.";
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancelled_conversation_drops_late_background_completion() {
|
||||
let (_directory, store) = fixtures::temp_store().await;
|
||||
let provider = fake_provider::FakeProvider::default();
|
||||
let assets = PromptAssets::load(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("prompt/cursor")
|
||||
.as_path(),
|
||||
)
|
||||
.unwrap();
|
||||
let registry = CursorSessionRegistry::new(
|
||||
store,
|
||||
Arc::new(provider.clone()),
|
||||
PromptCompiler::new(assets),
|
||||
Default::default(),
|
||||
);
|
||||
let active = registry.get_or_create("cancelled-request").await.unwrap();
|
||||
active.set_conversation_id("parent-conversation").unwrap();
|
||||
active.mark_conversation_cancelled();
|
||||
|
||||
bidi_append::append(
|
||||
®istry,
|
||||
DecodedAppend {
|
||||
request_id: "late-completion".into(),
|
||||
seqno: 1,
|
||||
message: completion_run(
|
||||
"child-id",
|
||||
"cancelled-parent-run",
|
||||
pb::ConversationStateStructure::default(),
|
||||
),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(provider.requests().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_subagent_completion_starts_a_simulated_parent_turn() {
|
||||
let (_directory, store) = fixtures::temp_store().await;
|
||||
|
||||
@@ -672,7 +672,17 @@ async fn anthropic_raw_stream_uses_explicit_and_default_token_limits() {
|
||||
server.abort();
|
||||
|
||||
assert_eq!(body["max_tokens"], 1234);
|
||||
assert!(body.get("cache_control").is_none());
|
||||
assert_eq!(
|
||||
body["messages"][0]["content"][0]["cache_control"]["type"],
|
||||
"ephemeral"
|
||||
);
|
||||
assert_eq!(default_body["max_tokens"], 65_000);
|
||||
assert!(default_body.get("cache_control").is_none());
|
||||
assert_eq!(
|
||||
default_body["messages"][0]["content"][0]["cache_control"]["type"],
|
||||
"ephemeral"
|
||||
);
|
||||
assert!(body.get("service_tier").is_none());
|
||||
assert_eq!(body["messages"][0]["content"][1]["type"], "image");
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user