Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ff74bc6a8 | ||
|
|
66ac94d5b1 | ||
|
|
fd7998e43c | ||
|
|
44e2d8057a | ||
|
|
d200b3791d | ||
|
|
279e6bb07c | ||
|
|
88f258f184 | ||
|
|
c983124288 | ||
|
|
6447fd998a | ||
|
|
5cc2401ce2 | ||
|
|
5a0bc2e0e9 | ||
|
|
6a3570a95a | ||
|
|
921bcfd247 | ||
|
|
e27b989002 | ||
|
|
e874d68b79 | ||
|
|
befb18d9c3 | ||
|
|
d0033a768b | ||
|
|
59379f1f72 | ||
|
|
370f120b8a | ||
|
|
1ab86c1851 | ||
|
|
d015d1c33b | ||
|
|
2c32dad271 | ||
|
|
85241a0413 | ||
|
|
1fcb78ad95 | ||
|
|
edc28de86b | ||
|
|
b2610c562e | ||
|
|
318545cebd | ||
|
|
2369d101ec | ||
|
|
80604bdf97 | ||
|
|
c4031d4074 | ||
|
|
b15e149b9d | ||
|
|
15f2280e07 | ||
|
|
71ddf71ec1 | ||
|
|
cde4d55fd2 | ||
|
|
217f984112 | ||
|
|
2b54014cf9 | ||
|
|
6b65a4ae31 | ||
|
|
8ca62e933b | ||
|
|
c711690f90 | ||
|
|
ed300cc92e | ||
|
|
eca063e424 | ||
|
|
e4515dc4e0 |
@@ -206,7 +206,7 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
node scripts/release/generate-legacy-update.mjs \
|
||||
node .github/scripts/generate-legacy-update.mjs \
|
||||
--version "${VERSION}" \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--assets-dir legacy-update \
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
-H "Accept: application/octet-stream" \
|
||||
"repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \
|
||||
> tauri-update/latest.json
|
||||
node scripts/release/normalize-tauri-update.mjs \
|
||||
node .github/scripts/normalize-tauri-update.mjs \
|
||||
--manifest tauri-update/latest.json \
|
||||
--release tauri-update/release.json \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
|
||||
@@ -7,3 +7,19 @@
|
||||
- Prefer established, well-maintained libraries when they reduce overall complexity or improve reliability. Do not reimplement common functionality without a clear reason.
|
||||
- Lean on the dependencies already in the project before writing your own implementation or adding packages. Do not assume a library lacks a capability without checking its documentation and types.
|
||||
- Make architectural decisions for the long term. Do not accept a stopgap that only works for now and is meant to be replaced later.
|
||||
|
||||
## Communication
|
||||
|
||||
- Lead with the conclusion. Keep responses direct and omit filler, repeated context, generic explanations, and narration of obvious steps.
|
||||
- Match the level requested by the user: discuss architecture as architecture, behavior as behavior, and code details only when they materially support the answer.
|
||||
- For architecture, refactoring, and module-boundary discussions, communicate primarily with annotated directory trees and ASCII architecture/data-flow diagrams instead of long prose.
|
||||
- In directory trees, annotate every relevant directory and file with its single responsibility. When discussing code size, include line counts in the comments:
|
||||
- use measured line counts for existing code;
|
||||
- use clearly marked approximate targets such as `≈300 lines` for proposed code;
|
||||
- state whether tests, generated code, and blank lines are excluded.
|
||||
- Show the complete main execution path, including inputs, ownership boundaries, runtime loops, persistence, outputs, and extension points. Make the direction of data and control flow explicit.
|
||||
- Clearly separate the current structure from the proposed structure. Explicitly list modules that move, merge, split, or are deleted.
|
||||
- Use the user's vocabulary consistently. Do not introduce new domain terms when existing plain-language terms are sufficient; when an implementation name must be mentioned, distinguish it from the architecture concept.
|
||||
- For stateful or concurrent behavior, show timing, ownership, state boundaries, and before/during/after behavior explicitly. Distinguish continuation within the same lifecycle from creation of a new lifecycle.
|
||||
- Prefer compact trees, flow diagrams, state matrices, and mappings when they communicate the structure more clearly than paragraphs.
|
||||
- Tie recommendations to the actual repository structure and code. Do not present a speculative target architecture as if it already exists.
|
||||
|
||||
@@ -97,6 +97,28 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
|
||||
dependencies = [
|
||||
"async-fs",
|
||||
"async-net",
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.5",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.2"
|
||||
@@ -186,6 +208,17 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -215,6 +248,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
@@ -1128,10 +1172,11 @@ checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "cursor-byok-desktop"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5-beta.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"cursor-server",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -1145,6 +1190,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
@@ -1520,6 +1566,15 @@ dependencies = [
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlib"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.8.2"
|
||||
@@ -4555,6 +4610,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
@@ -5196,6 +5257,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -6289,6 +6374,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symlink"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -7262,6 +7353,19 @@ dependencies = [
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-appender"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"symlink",
|
||||
"thiserror 2.0.20",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
@@ -7636,6 +7740,12 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
@@ -7852,6 +7962,7 @@ dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
@@ -7910,6 +8021,8 @@ version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -9028,6 +9141,7 @@ dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow 1.0.4",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = [
|
||||
"server",
|
||||
"apps/desktop/src-tauri",
|
||||
"crates/semble-core",
|
||||
"benchmarks/semble",
|
||||
"support/benchmarks/semble",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY apps/desktop/src-tauri/src/ apps/desktop/src-tauri/src/
|
||||
COPY apps/desktop/src-tauri/capabilities/ apps/desktop/src-tauri/capabilities/
|
||||
COPY apps/desktop/src-tauri/icons/ apps/desktop/src-tauri/icons/
|
||||
COPY apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/tauri.conf.json
|
||||
COPY scripts/cursor-proto/proto/ scripts/cursor-proto/proto/
|
||||
COPY protocols/cursor/ protocols/cursor/
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/src/target \
|
||||
|
||||
@@ -30,6 +30,13 @@ build-docs:
|
||||
build-server:
|
||||
cargo build --release --package cursor-server --bin cursor-server
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
$(LOCAL_TAURI_SIGNING_KEY):
|
||||
@powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path '$(dir $@)' | Out-Null; & '$(CURDIR)/apps/desktop/node_modules/.bin/tauri.cmd' signer generate --ci --write-keys '$@'"
|
||||
|
||||
build-desktop: $(LOCAL_TAURI_SIGNING_KEY)
|
||||
@node -e "const { spawnSync } = require('node:child_process'); const result = spawnSync(process.execPath, ['node_modules/@tauri-apps/cli/tauri.js', 'build', '--bundles', 'nsis'], { cwd: 'apps/desktop', stdio: 'inherit', env: { ...process.env, TAURI_SIGNING_PRIVATE_KEY: process.argv[1], TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' } }); process.exit(result.status ?? 1)" "$(LOCAL_TAURI_SIGNING_KEY)"
|
||||
else
|
||||
$(LOCAL_TAURI_SIGNING_KEY):
|
||||
@install -d -m 700 "$(dir $@)"
|
||||
@apps/desktop/node_modules/.bin/tauri signer generate --ci --write-keys "$@" >/dev/null
|
||||
@@ -37,6 +44,7 @@ $(LOCAL_TAURI_SIGNING_KEY):
|
||||
|
||||
build-desktop: $(LOCAL_TAURI_SIGNING_KEY)
|
||||
TAURI_SIGNING_PRIVATE_KEY="$(LOCAL_TAURI_SIGNING_KEY)" TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" npm --prefix apps/desktop run tauri:build
|
||||
endif
|
||||
|
||||
build-docker:
|
||||
docker build --tag cursor-byok:local .
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<div align="center">
|
||||
|
||||
# cursor-byok
|
||||
|
||||
cursor-byok 是一个运行在本机的 Cursor 模型网关,帮助你在 Cursor 中使用自己配置的模型服务。
|
||||
|
||||
[English README](./README.md) · [使用指南](https://docs.leokun.cn) · [下载](https://github.com/leookun/cursor-byok/releases/latest) · [提交问题](https://github.com/leookun/cursor-byok/issues)
|
||||
|
||||
[](https://github.com/leookun/cursor-byok/releases/latest)
|
||||
[](https://github.com/leookun/cursor-byok/releases)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/leookun/cursor-byok/releases/latest)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 项目简介
|
||||
|
||||
cursor-byok 是一个开源的本地模型网关。它在你的设备上运行服务,接收 Cursor 发出的 Agent 请求,将请求转发到你配置的模型服务,并尽可能保留 Cursor Agent 的工具调用、Skills、MCP 和多轮对话能力。
|
||||
|
||||
你可以连接兼容 OpenAI 或 Anthropic 协议的服务,自定义服务地址、模型 ID、API Key 和请求参数,也可以使用 Cursor 平台默认选项之外的模型通道。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> cursor-byok 免费且开源,但你连接的模型服务商可能会按用量收费。本项目是独立项目,与 Cursor 或其开发者没有关联,也未获得其认可。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- **自定义模型通道**:配置自己的 API 地址、凭据和模型 ID。
|
||||
- **多种 API 协议**:支持 OpenAI Responses API、OpenAI Chat Completions API 和 Anthropic Messages API 兼容服务。
|
||||
- **模型管理**:添加、复制、编辑、排序模型配置,并批量测试连接。
|
||||
- **连接性能测试**:查看首字延迟、生成速度、总耗时和原始服务商响应。
|
||||
- **Agent 工作流**:继续使用工具调用、Skills、MCP 和多轮对话。
|
||||
- **会话指标**:查看 Token 用量、缓存命中率、对话轮次和估算价值。
|
||||
- **TAB 补全服务**:在公益服务、官方直连和自定义服务之间选择连接方式。
|
||||
- **跨平台运行**:支持 macOS、Windows 和 Linux。
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 从 [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest) 下载适合你操作系统的最新版本。
|
||||
2. 启动 cursor-byok,打开 **Cursor 配置**,按提示初始化本地 CA(证书颁发机构)。
|
||||
3. 在模型设置中添加模型,填写服务地址、API Key 和模型名称,然后保存并运行 **测试**。
|
||||
4. 确认测试通过后,保持 cursor-byok 运行。
|
||||
5. **首次升级 Cursor 或首次配置模型后,完全退出并重新启动 Cursor,然后新开一个对话**。在模型列表中选择已配置的模型,开始使用 Agent。
|
||||
|
||||
完整的安装步骤、配置说明和常见问题,请参阅[中文使用指南](https://docs.leokun.cn/zh/docs)。
|
||||
|
||||
> [!TIP]
|
||||
> 首次升级 Cursor 或首次完成配置后,必须完全退出并重新启动 Cursor,再新开一个对话。配置前已经打开的对话不会加载新连接;使用自定义模型时,请在模型列表中手动选择该模型,不要选择 **Auto**。
|
||||
|
||||
## 模型配置
|
||||
|
||||
每个模型配置都是独立的上游通道,可以单独设置服务商、协议、凭据和生成参数。
|
||||
|
||||

|
||||
|
||||
### 类型与协议选择
|
||||
|
||||
| 模型系列 | 模型类型 | 请求协议 |
|
||||
| --- | --- | --- |
|
||||
| Claude 系列 | **Anthropic** | Messages API |
|
||||
| GPT / OpenAI 系列 | **OpenAI** | **Responses API** |
|
||||
| 其他模型 | **OpenAI** | **Chat Completions API** |
|
||||
|
||||
GPT 系列建议使用 **Responses API**。如果使用 Chat Completions,可能无法保留提示词缓存,导致速度变慢和费用增加。
|
||||
|
||||
### 常用字段
|
||||
|
||||
- **模型类型**:选择 OpenAI 或 Anthropic,决定上游接口格式。
|
||||
- **请求协议**:OpenAI 类型需要继续选择 Responses API 或 Chat Completions API。
|
||||
- **服务器地址**:可以填写服务商基础地址,让应用按协议追加标准端点,也可以填写完整请求 URL 并原样使用。
|
||||
- **API Key**:填写上游服务要求的访问密钥。密钥保存在本机,用于发送模型请求。
|
||||
- **模型名称**:填写服务商接口接受的模型标识,也可以使用 **获取模型** 读取服务商返回的模型列表。
|
||||
- **显示名称**:Cursor 模型列表中显示的名称,不会改变发送给上游的模型标识。
|
||||
- **备注**:显示在 Cursor 的模型说明中。
|
||||
|
||||
还可以根据模型能力设置上下文窗口 Token、最大输出 Token、推理或思考强度、自定义 Headers,以及 OpenAI 或 Anthropic 的额外参数。自定义 Headers 和额外参数必须是 JSON 对象,只应填写服务商明确支持的字段。
|
||||
|
||||
保存配置后运行 **测试**,确认地址、协议、API Key、模型标识和流式响应都正常,再在 Cursor 中使用该模型。
|
||||
|
||||
## TAB 补全服务
|
||||
|
||||
Cursor 的 Tab 补全由独立的 TAB 服务处理,不经过模型通道。你可以在 **系统设置 → TAB 设置** 中选择以下模式:
|
||||
|
||||
- **使用公益服务(默认)**:使用项目作者部署的公共服务,无需额外配置。
|
||||
- **直连**:直接连接当前 Cursor 账号对应的官方 TAB 服务,适合账号拥有官方额度的情况。
|
||||
- **自定义**:自行部署 [`cursor-tab-server`](https://github.com/leookun/cursor-byok/tree/archive/v0.0.49/cursor-tab-server),然后填写 TAB 服务地址。
|
||||
|
||||
修改 TAB 设置后,建议重启 Cursor 并新开一个对话,确保新的连接方式生效。
|
||||
|
||||
## 与官方账号并存
|
||||
|
||||
新版设计支持 cursor-byok 与 Cursor 官方服务并存:
|
||||
|
||||
- 直接在 Cursor 中登录自己的账号。如果之前使用旧版生成的 fake 账户,请先退出该账户,再登录自己的账号。
|
||||
- 账号拥有官方额度时,官方模型和本地模型可以随时切换混用。
|
||||
- **Auto 只使用官方模型**,不会自动使用你配置的本地模型。账号没有官方额度时,请手动选择自己配置的模型。
|
||||
- 插件、代码库索引等 Cursor 功能可以继续使用。
|
||||
|
||||
## 数据流转
|
||||
|
||||
```text
|
||||
Cursor 客户端
|
||||
│
|
||||
│ Agent 请求与工具结果
|
||||
▼
|
||||
cursor-byok 本地服务
|
||||
│
|
||||
│ OpenAI / Anthropic 兼容请求
|
||||
▼
|
||||
你配置的模型 API
|
||||
```
|
||||
|
||||
API Key、模型配置和应用设置保存在本机。模型请求仍会发送到你选择的上游服务商,请根据对应服务商的隐私政策和计费规则使用。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
cursor-byok/
|
||||
├── apps/
|
||||
│ ├── desktop/
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── features/ # 首页、模型、调用记录与设置
|
||||
│ │ │ ├── shell/ # 窗口框架、页面布局与广告外壳
|
||||
│ │ │ ├── shared/ # UI、虚拟列表、状态、API 与平台能力
|
||||
│ │ │ ├── i18n/ # 本地化运行时与语言目录
|
||||
│ │ │ └── styles/ # 全局主题与排版令牌
|
||||
│ │ └── src-tauri/ # Tauri 桌面生命周期
|
||||
│ └── docs/ # Next.js 与 Fumadocs 中文/英文文档站
|
||||
├── server/
|
||||
│ ├── src/
|
||||
│ │ ├── cursor/ # Cursor 协议适配
|
||||
│ │ ├── run/ # 通用 Agent Runtime 与内部 Port
|
||||
│ │ ├── provider/ # 模型供应商适配
|
||||
│ │ ├── model/ # 聚合领域模型
|
||||
│ │ ├── store/ # SQLite Repository
|
||||
│ │ ├── control/ # 管理面 API
|
||||
│ │ ├── harness/ # Cursor 本地集成
|
||||
│ │ └── search/ # Web 与 Semble 搜索接入
|
||||
│ ├── prompt/
|
||||
│ └── migrations/
|
||||
├── crates/
|
||||
│ └── semble-core/ # 本地代码索引与搜索核心库
|
||||
├── protocols/
|
||||
│ └── cursor/ # Cursor 协议定义的唯一来源
|
||||
├── support/ # 仓库辅助设施
|
||||
│ ├── cursor-protocol-extractor/ # Cursor 协议提取与 Go 代码生成工具
|
||||
│ ├── cursor-capture/ # Cursor 协议抓取和调试工具
|
||||
│ └── benchmarks/ # 代码搜索和索引基准测试
|
||||
├── images/ # README 展示图片
|
||||
├── Cargo.toml # Rust 工作区配置
|
||||
└── Makefile # 常用开发、检查和构建命令
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Rust 工具链和 Cargo
|
||||
- Node.js 与 npm
|
||||
- Tauri 2 的系统构建依赖
|
||||
- Docker(仅在构建 Docker 镜像时需要)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm --prefix apps/desktop install
|
||||
npm --prefix apps/docs install
|
||||
```
|
||||
|
||||
### 启动开发环境
|
||||
|
||||
启动桌面前端:
|
||||
|
||||
```bash
|
||||
make dev-web
|
||||
```
|
||||
|
||||
启动桌面应用:
|
||||
|
||||
```bash
|
||||
make dev-desktop
|
||||
```
|
||||
|
||||
启动文档站:
|
||||
|
||||
```bash
|
||||
make dev-docs
|
||||
```
|
||||
|
||||
文档站默认地址为 <http://localhost:3000>。
|
||||
|
||||
### 检查与构建
|
||||
|
||||
运行完整检查:
|
||||
|
||||
```bash
|
||||
make check
|
||||
```
|
||||
|
||||
分别构建各部分:
|
||||
|
||||
```bash
|
||||
make build-web # 构建桌面前端
|
||||
make build-server # 构建 Rust 本地服务
|
||||
make build-docs # 构建文档站
|
||||
make build-desktop # 构建 Tauri 桌面安装包
|
||||
make build-docker # 构建 Docker 镜像
|
||||
```
|
||||
|
||||
文档内容位于 `apps/docs/content/docs` 和 `apps/docs/content/blog`。修改文档侧边栏时同步更新 `apps/docs/content/docs/meta.json`。
|
||||
|
||||
## 路线图
|
||||
|
||||
项目将继续改进模型兼容性、Agent 工具、本地运行稳定性和自托管体验,并探索支持更多 IDE、聊天和 Agent 工作流。
|
||||
|
||||
计划与进展请参阅[发布路线图](https://github.com/leookun/cursor-byok/discussions/32)。
|
||||
|
||||
## 社区与反馈
|
||||
|
||||
- [中文使用指南](https://docs.leokun.cn/zh/docs)
|
||||
- [GitHub Issues](https://github.com/leookun/cursor-byok/issues)
|
||||
- [Telegram 社区](https://t.me/cursor_byok)
|
||||
- QQ 群:`1095916242`、`1094411438`、`1095918002`、`1094419321`
|
||||
|
||||
提交问题时,请附上操作系统、cursor-byok 版本、模型类型、请求协议、已脱敏的服务地址、错误信息和复现步骤。请勿公开 API Key 或其他凭据。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request。提交代码前请先阅读项目中的开发说明,并运行 `make check` 确认格式、测试和前端构建检查通过。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 [MIT License](./LICENSE) 开源。
|
||||
@@ -17,9 +17,9 @@ cursor-byok is a local implementation of Cursor's backend.
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -45,15 +45,16 @@ You can connect OpenAI- and Anthropic-compatible services, customize endpoints,
|
||||
1. Download the latest build for your platform from [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest).
|
||||
2. Launch cursor-byok, open **Model Settings**, and enter the endpoint, API key, and model ID.
|
||||
3. Test the model configuration. Once it passes, return to the dashboard and start the service.
|
||||
4. Open Cursor, select the configured model, and start using Agent.
|
||||
4. Test the model configuration. Once it passes, return to the dashboard and start the service.
|
||||
5. After upgrading Cursor or configuring a model for the first time, quit Cursor completely and restart it, then start a new conversation and select the configured model.
|
||||
|
||||
For complete installation steps, system configuration, and troubleshooting, see the [User Guide](https://docs.leokun.cn).
|
||||
For complete installation steps, system configuration, and Frequently Asked Questions, see the [User Guide](https://docs.leokun.cn).
|
||||
|
||||
## Model Management
|
||||
|
||||
Model configurations support both OpenAI and Anthropic API protocols. Each model channel can independently define its context window, maximum output tokens, reasoning effort, custom headers, and additional request parameters.
|
||||
|
||||

|
||||

|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#141414" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
@@ -11,9 +10,20 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #141414;
|
||||
background: var(--boot-bg, #141414);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// 在应用样式加载前按主题参数铺底色,避免切换 poster 时闪出错误颜色
|
||||
(function () {
|
||||
var light = new URLSearchParams(location.search).get("theme") === "default-light";
|
||||
document.documentElement.style.setProperty("--boot-bg", light ? "#f5f5f5" : "#141414");
|
||||
var meta = document.createElement("meta");
|
||||
meta.name = "theme-color";
|
||||
meta.content = light ? "#f5f5f5" : "#141414";
|
||||
document.head.appendChild(meta);
|
||||
})();
|
||||
</script>
|
||||
<title>Cursor BYOK 产品演示</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cursor-byok-desktop",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cursor-byok-desktop",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5-beta.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cursor-byok-desktop",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5-beta.1",
|
||||
"description": "Cursor BYOK desktop management application",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cursor-byok-desktop"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5-beta.1"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
cursor-server = { path = "../../../server" }
|
||||
rfd = "0.15"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
@@ -26,5 +27,6 @@ tauri-plugin-updater = "2"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
tokio-util = "0.7"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
url = "2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
process::Command,
|
||||
process::{Command, ExitCode},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Mutex,
|
||||
@@ -19,7 +19,6 @@ use tauri::{
|
||||
};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[cfg(dev)]
|
||||
use cursor_server::config::ConsoleSource;
|
||||
@@ -27,6 +26,7 @@ use cursor_server::{App, Config, Result};
|
||||
|
||||
#[cfg(not(dev))]
|
||||
use crate::frontend;
|
||||
use crate::startup::{self, StartupDiagnostics};
|
||||
use crate::tray;
|
||||
|
||||
pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
|
||||
@@ -141,16 +141,23 @@ fn create_main_window(
|
||||
builder.build()
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
|
||||
pub fn run() -> ExitCode {
|
||||
let diagnostics = match StartupDiagnostics::initialize() {
|
||||
Ok(diagnostics) => diagnostics,
|
||||
Err(error) => {
|
||||
startup::report_logging_failure(error.as_ref());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
tracing::info!(
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
os = std::env::consts::OS,
|
||||
architecture = std::env::consts::ARCH,
|
||||
log_directory = %diagnostics.log_directory().display(),
|
||||
"desktop starting"
|
||||
);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "cursor_server=info".into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![open_terminal_with_command])
|
||||
@@ -218,8 +225,14 @@ pub fn run() {
|
||||
tray::create(app)?;
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("failed to build Cursor BYOK desktop app");
|
||||
.build(tauri::generate_context!());
|
||||
let app = match app {
|
||||
Ok(app) => app,
|
||||
Err(error) => {
|
||||
diagnostics.report_fatal(&error);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
app.run(|app, event| match event {
|
||||
RunEvent::WindowEvent {
|
||||
@@ -264,4 +277,6 @@ pub fn run() {
|
||||
} => tray::show_main_window(app),
|
||||
_ => {}
|
||||
});
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod desktop;
|
||||
#[cfg(not(dev))]
|
||||
mod frontend;
|
||||
mod startup;
|
||||
mod tray;
|
||||
|
||||
pub use desktop::run;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
cursor_byok_desktop::run();
|
||||
fn main() -> std::process::ExitCode {
|
||||
cursor_byok_desktop::run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Captures startup diagnostics before the database and desktop runtime initialize.
|
||||
use std::{error::Error, path::PathBuf};
|
||||
|
||||
use rfd::{MessageButtons, MessageDialog, MessageLevel};
|
||||
use tracing_appender::{
|
||||
non_blocking::WorkerGuard,
|
||||
rolling::{RollingFileAppender, Rotation},
|
||||
};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
const LOG_DIRECTORY_NAME: &str = "logs";
|
||||
const LOG_FILE_PREFIX: &str = "cursor-byok";
|
||||
const LOG_FILE_SUFFIX: &str = "log";
|
||||
const RETAINED_LOG_FILES: usize = 15;
|
||||
|
||||
type BoxError = Box<dyn Error + Send + Sync>;
|
||||
|
||||
pub(crate) struct StartupDiagnostics {
|
||||
log_directory: PathBuf,
|
||||
_writer_guard: WorkerGuard,
|
||||
}
|
||||
|
||||
impl StartupDiagnostics {
|
||||
pub(crate) fn initialize() -> Result<Self, BoxError> {
|
||||
let log_directory = cursor_server::config::managed_data_dir()?.join(LOG_DIRECTORY_NAME);
|
||||
std::fs::create_dir_all(&log_directory)?;
|
||||
|
||||
let file_appender = RollingFileAppender::builder()
|
||||
.rotation(Rotation::DAILY)
|
||||
.filename_prefix(LOG_FILE_PREFIX)
|
||||
.filename_suffix(LOG_FILE_SUFFIX)
|
||||
.max_log_files(RETAINED_LOG_FILES)
|
||||
.build(&log_directory)?;
|
||||
let (file_writer, writer_guard) = tracing_appender::non_blocking(file_appender);
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "cursor_byok_desktop=info,cursor_server=info".into());
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(file_writer),
|
||||
)
|
||||
.try_init()?;
|
||||
|
||||
Ok(Self {
|
||||
log_directory,
|
||||
_writer_guard: writer_guard,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn log_directory(&self) -> &std::path::Path {
|
||||
&self.log_directory
|
||||
}
|
||||
|
||||
pub(crate) fn report_fatal(&self, error: &(dyn Error + 'static)) {
|
||||
let details = error_chain(error);
|
||||
tracing::error!(
|
||||
error = %details,
|
||||
log_directory = %self.log_directory.display(),
|
||||
"desktop failed to start"
|
||||
);
|
||||
show_fatal_dialog(&details, Some(&self.log_directory));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn report_logging_failure(error: &(dyn Error + 'static)) {
|
||||
let details = error_chain(error);
|
||||
eprintln!("Cursor BYOK failed to initialize logging: {details}");
|
||||
show_fatal_dialog(&details, None);
|
||||
}
|
||||
|
||||
fn show_fatal_dialog(details: &str, log_directory: Option<&std::path::Path>) {
|
||||
let log_guidance = match log_directory {
|
||||
Some(directory) => format!(
|
||||
"日志目录 / Log directory:\n{}\n\n请将最新的日志文件发送给开发者。\nPlease send the latest log file to the developer.",
|
||||
directory.display()
|
||||
),
|
||||
None => "日志系统也未能启动,因此没有生成日志文件。\nLogging also failed to initialize, so no log file was created."
|
||||
.to_owned(),
|
||||
};
|
||||
let description = format!(
|
||||
"Cursor BYOK 无法启动 / failed to start.\n\n错误 / Error:\n{details}\n\n{log_guidance}"
|
||||
);
|
||||
|
||||
let _ = MessageDialog::new()
|
||||
.set_level(MessageLevel::Error)
|
||||
.set_title("Cursor BYOK 启动失败 / Startup Error")
|
||||
.set_description(description)
|
||||
.set_buttons(MessageButtons::Ok)
|
||||
.show();
|
||||
}
|
||||
|
||||
fn error_chain(error: &(dyn Error + 'static)) -> String {
|
||||
let mut details = error.to_string();
|
||||
let mut source = error.source();
|
||||
while let Some(cause) = source {
|
||||
details.push_str("\nCaused by: ");
|
||||
details.push_str(&cause.to_string());
|
||||
source = cause.source();
|
||||
}
|
||||
details
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OuterError(std::io::Error);
|
||||
|
||||
impl fmt::Display for OuterError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("database initialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for OuterError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fatal_report_includes_the_complete_error_chain() {
|
||||
let error = OuterError(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"database file is read-only",
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
error_chain(&error),
|
||||
"database initialization failed\nCaused by: database file is read-only"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Cursor BYOK",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5-beta.1",
|
||||
"identifier": "dev.cursorbyok.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
import { MessageProvider } from "./components/ui/MessageProvider";
|
||||
import { useMessage } from "./components/ui/message";
|
||||
import { AppFrame } from "./layouts/AppFrame";
|
||||
import { AppLayout } from "./layouts/AppLayout";
|
||||
import { CallsPage } from "./pages/CallsPage";
|
||||
import { CallDetailsPage } from "./pages/CallDetailsPage";
|
||||
import { CursorSettingsPage } from "./pages/CursorSettingsPage";
|
||||
import { HomePage } from "./pages/HomePage";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
import { useAppStore } from "./store/appStore";
|
||||
import { updateStore } from "./store/updateStore";
|
||||
import { MessageProvider } from "./shared/ui/MessageProvider";
|
||||
import { useMessage } from "./shared/ui/message";
|
||||
import { AppFrame } from "./shell/AppFrame";
|
||||
import { AppLayout } from "./shell/AppLayout";
|
||||
import { CallsPage } from "./features/calls/CallsPage";
|
||||
import { CallDetailsPage } from "./features/calls/CallDetailsPage";
|
||||
import { CursorSettingsPage } from "./features/models/CursorSettingsPage";
|
||||
import { HomePage } from "./features/home/HomePage";
|
||||
import { SettingsPage } from "./features/settings/SettingsPage";
|
||||
import { useAppStore } from "./shared/store/appStore";
|
||||
import { updateStore } from "./shared/store/updateStore";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ProxySettings,
|
||||
StatisticsStorage,
|
||||
TabSettings,
|
||||
} from "../api";
|
||||
} from "../shared/api";
|
||||
|
||||
const API_ROOT = "/__byok-api__/api";
|
||||
const FIXED_NOW = Date.UTC(2026, 7, 27, 8, 0, 0);
|
||||
@@ -55,6 +55,7 @@ const calls: LlmCall[] = Array.from({ length: 24 }, (_, index) => {
|
||||
finish_reason: failed ? null : "stop",
|
||||
created_at_ms: FIXED_NOW - index * 3 * 60_000,
|
||||
ttfb_ms: 210 + index * 13,
|
||||
ttfr_ms: 290 + index * 15,
|
||||
ttft_ms: 370 + index * 17,
|
||||
duration_ms: failed ? 812 : 1_420 + index * 71,
|
||||
input_tokens: 4_800 + index * 337,
|
||||
@@ -116,7 +117,7 @@ export function installDemoApi() {
|
||||
}
|
||||
if (path === "/models/import-v0049") return json({ imported: 0, skipped: 0, total: 0 });
|
||||
if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) && method === "POST") {
|
||||
return json({ duration_ms: 1_284, first_text_ms: 418, output_tokens: 42, tokens_per_second: 38.6, tokens_estimated: false, output: "Mock connectivity test passed." });
|
||||
return json({ duration_ms: 1_284, first_valid_response_ms: 418, output_tokens: 42, tokens_per_second: 38.6, tokens_estimated: false, output: "Mock connectivity test passed." });
|
||||
}
|
||||
if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) || /^\/models\/[^/]+$/.test(path)) {
|
||||
return method === "DELETE" ? empty() : json(models[0]);
|
||||
@@ -138,7 +139,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);
|
||||
@@ -243,8 +247,9 @@ function createSeries(count: number, step: number, end: number): OverviewTokenUs
|
||||
const wave = 0.72 + ((index * 17) % 31) / 50;
|
||||
return {
|
||||
bucket_start_ms: end - (count - index) * step,
|
||||
input_tokens: Math.round(18_000 * wave),
|
||||
cache_read_tokens: Math.round(62_000 * wave),
|
||||
// input : cache_read = 1 : 99,使默认口径缓存命中率恰为 99%
|
||||
input_tokens: Math.round(800 * wave),
|
||||
cache_read_tokens: Math.round(79_200 * wave),
|
||||
cache_write_tokens: Math.round(7_500 * wave),
|
||||
output_tokens: Math.round(12_500 * wave),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { installDemoApi } from "./api";
|
||||
|
||||
installDemoApi();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const locale = params.get("locale") === "en-US" ? "en-US" : "zh-CN";
|
||||
const theme = params.get("theme") === "default-light" ? "default-light" : "default-dark";
|
||||
|
||||
document.documentElement.dataset.platform = "macos";
|
||||
localStorage.setItem("cursor-byok.locale", "zh-CN");
|
||||
localStorage.setItem("cursor-byok.theme", "default-dark");
|
||||
localStorage.setItem("cursor-byok.locale", locale);
|
||||
localStorage.setItem("cursor-byok.theme", theme);
|
||||
|
||||
void import("../index");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../styles/typography" as type;
|
||||
@use "../../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
section { min-width: 0; }
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CallDetail } from "../api";
|
||||
import { JsonEditor } from "./ui/JsonEditor";
|
||||
import { Tabs, type TabItem } from "./ui/Tabs";
|
||||
import type { CallDetail } from "../../shared/api";
|
||||
import { JsonEditor } from "../../shared/ui/JsonEditor";
|
||||
import { Tabs, type TabItem } from "../../shared/ui/Tabs";
|
||||
import styles from "./CallDetails.module.scss";
|
||||
|
||||
const show = (value: string | number | null) => value ?? "-";
|
||||
@@ -32,6 +32,7 @@ export function CallDetails({ detail }: { detail: CallDetail }) {
|
||||
["Created At", `${call.created_at_ms} · ${new Date(call.created_at_ms).toLocaleString()}`],
|
||||
[t("耗时"), timing(call.duration_ms)],
|
||||
["TTFB", timing(call.ttfb_ms)],
|
||||
["TTFR", timing(call.ttfr_ms)],
|
||||
["TTFT", timing(call.ttft_ms)],
|
||||
["Input Token", show(call.input_tokens)],
|
||||
["Output Token", show(call.output_tokens)],
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../styles/typography" as type;
|
||||
@use "../../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
width: 100%;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { api, type CallDetail } from "../api";
|
||||
import { CallDetails } from "../components/CallDetails";
|
||||
import { TitledCard } from "../components/ui/TitledCard";
|
||||
import { api, type CallDetail } from "../../shared/api";
|
||||
import { CallDetails } from "./CallDetails";
|
||||
import { TitledCard } from "../../shared/ui/TitledCard";
|
||||
import styles from "./CallDetailsPage.module.scss";
|
||||
import { ScrollableContent } from "../components/virtual/ScrollableContent";
|
||||
import { ScrollableContent } from "../../shared/virtual/ScrollableContent";
|
||||
|
||||
export function CallDetailsPage() {
|
||||
const { callId = "" } = useParams();
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../styles/typography" as type;
|
||||
@use "../../styles/typography" as type;
|
||||
|
||||
.status {
|
||||
padding: 3px 7px;
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { LlmCall } from "../api";
|
||||
import controls from "./ui/Controls.module.scss";
|
||||
import { DataTable, type DataTableColumn } from "./ui/DataTable";
|
||||
import { Icon } from "./ui/Icon";
|
||||
import { TooltipTrigger } from "./ui/TooltipTrigger";
|
||||
import { eyeIcon } from "./ui/icons";
|
||||
import type { LlmCall } from "../../shared/api";
|
||||
import controls from "../../shared/ui/Controls.module.scss";
|
||||
import { DataTable, type DataTableColumn } from "../../shared/ui/DataTable";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
|
||||
import { eyeIcon } from "../../shared/ui/icons";
|
||||
import styles from "./CallTable.module.scss";
|
||||
|
||||
const value = (input: string | number | null) => input ?? "-";
|
||||
@@ -87,6 +87,11 @@ export function CallTable({ calls, onDetails }: { calls: LlmCall[]; onDetails: (
|
||||
header: "TTFB",
|
||||
render: (call) => milliseconds(call.ttfb_ms),
|
||||
},
|
||||
{
|
||||
key: "ttfr",
|
||||
header: "TTFR",
|
||||
render: (call) => milliseconds(call.ttfr_ms),
|
||||
},
|
||||
{
|
||||
key: "ttft",
|
||||
header: "TTFT",
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from "react";
|
||||
import { CallTable } from "./CallTable";
|
||||
import { PageContent } from "../../shell/layout/PageContent";
|
||||
import { appStore, useAppStore } from "../../shared/store/appStore";
|
||||
import styles from "./CallsPage.module.scss";
|
||||
|
||||
const CALL_REFRESH_INTERVAL_MS = 2_000;
|
||||
|
||||
export function CallsPage() {
|
||||
const { calls } = useAppStore();
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const refreshCalls = () => {
|
||||
if (!disposed && document.visibilityState === "visible") {
|
||||
void appStore.refreshCalls();
|
||||
}
|
||||
};
|
||||
|
||||
refreshCalls();
|
||||
const interval = window.setInterval(refreshCalls, CALL_REFRESH_INTERVAL_MS);
|
||||
window.addEventListener("focus", refreshCalls);
|
||||
document.addEventListener("visibilitychange", refreshCalls);
|
||||
return () => {
|
||||
disposed = true;
|
||||
window.clearInterval(interval);
|
||||
window.removeEventListener("focus", refreshCalls);
|
||||
document.removeEventListener("visibilitychange", refreshCalls);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const content = <div className={styles.page}><CallTable calls={calls} onDetails={(call) => void appStore.openCallDetails(call.call_id)} /></div>;
|
||||
return <PageContent fixed title={t("调用")} contentClassName={styles.pageContent} sections={[{ key: "calls", estimatedHeight: 720, content }]} />;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type Overview } from "../api";
|
||||
import { ContributionCalendarChart } from "../components/charts/ContributionCalendarChart";
|
||||
import { DailyTokenUsageChart } from "../components/charts/DailyTokenUsageChart";
|
||||
import { HomeMetrics } from "../components/metrics/HomeMetrics";
|
||||
import { PageContent } from "../components/layout/PageContent";
|
||||
import type { VirtualPageSection } from "../components/layout/VirtualPage";
|
||||
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "../components/overview/OverviewTimeRangeFilter";
|
||||
import { PageActions } from "../layouts/PageActions";
|
||||
import { appStore, useAppStore } from "../store/appStore";
|
||||
import { formatTimeInput, parseTimeInput } from "../utils/parseTimeInput";
|
||||
import { claudeIcon, openAiIcon } from "../components/ui/icons";
|
||||
import { api, type Overview } from "../../shared/api";
|
||||
import { ContributionCalendarChart } from "./charts/ContributionCalendarChart";
|
||||
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 { PageActions } from "../../shell/PageActions";
|
||||
import { appStore, useAppStore } from "../../shared/store/appStore";
|
||||
import { formatTimeInput, parseTimeInput } from "../../shared/utils/parseTimeInput";
|
||||
import { claudeIcon, openAiIcon } from "../../shared/ui/icons";
|
||||
|
||||
type TimeRange = { startMs: number; endMs: number };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../styles/typography" as type;
|
||||
@use "../../../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
width: 100%;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { init, Rect, type ElementEvent } from "zrender";
|
||||
import type { Locale } from "../../i18n/runtime";
|
||||
import { useI18n } from "../../i18n/store";
|
||||
import { Tooltip, type TooltipAnchor } from "../ui/Tooltip";
|
||||
import type { Locale } from "../../../i18n/runtime";
|
||||
import { useI18n } from "../../../i18n/store";
|
||||
import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip";
|
||||
import styles from "./ContributionCalendarChart.module.scss";
|
||||
|
||||
export type ContributionDay = {
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../styles/typography" as type;
|
||||
@use "../../../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
--daily-token-tooltip-font-size: #{type.$font-size-2xs};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EChartsCoreOption } from "echarts/core";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { TokenUsageGranularity } from "../../api";
|
||||
import { formatCompactInteger } from "../../utils/numberFormat";
|
||||
import type { TokenUsageGranularity } from "../../../shared/api";
|
||||
import { formatCompactInteger } from "../../../shared/utils/numberFormat";
|
||||
import { EChart } from "./EChart";
|
||||
import styles from "./DailyTokenUsageChart.module.scss";
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { EChartsCoreOption } from "echarts/core";
|
||||
import type { LlmCall } from "../../api";
|
||||
import type { LlmCall } from "../../../shared/api";
|
||||
import { EChart } from "./EChart";
|
||||
|
||||
export function LatencyChart({ calls }: { calls: LlmCall[] }) {
|
||||
const points = calls.slice(0, 20).reverse();
|
||||
const option: EChartsCoreOption = {
|
||||
animationDuration: 280,
|
||||
color: ["#d79a62", "#ad84cf"],
|
||||
color: ["#d79a62", "#ad84cf", "#72a8d8"],
|
||||
grid: { top: 22, right: 18, bottom: 30, left: 48 },
|
||||
legend: { top: 0, right: 8, textStyle: { color: "#999" } },
|
||||
tooltip: { trigger: "axis" },
|
||||
xAxis: { type: "category", data: points.map((call) => new Date(call.created_at_ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })), axisLabel: { color: "#888" }, axisLine: { lineStyle: { color: "#5555" } } },
|
||||
yAxis: { type: "value", axisLabel: { color: "#888", formatter: "{value} ms" }, splitLine: { lineStyle: { color: "#8882" } } },
|
||||
series: [
|
||||
{ name: "TTFR", type: "line", smooth: true, symbol: "none", data: points.map((call) => call.ttfr_ms ?? 0) },
|
||||
{ name: "TTFT", type: "line", smooth: true, symbol: "none", data: points.map((call) => call.ttft_ms ?? 0) },
|
||||
{ name: t("总耗时"), type: "line", smooth: true, symbol: "none", data: points.map((call) => call.duration_ms ?? 0) },
|
||||
],
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EChartsCoreOption } from "echarts/core";
|
||||
import type { LlmCall } from "../../api";
|
||||
import type { LlmCall } from "../../../shared/api";
|
||||
import { EChart } from "./EChart";
|
||||
|
||||
export function TokenTrendChart({ calls }: { calls: LlmCall[] }) {
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../styles/typography" as type;
|
||||
@use "../../../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../styles/typography" as type;
|
||||
@use "../../../styles/typography" as type;
|
||||
|
||||
.scroller {
|
||||
width: 100%;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { formatCompactInteger, formatInteger } from "../../utils/numberFormat";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { Tooltip, type TooltipAnchor } from "../ui/Tooltip";
|
||||
import { informationOutlineIcon } from "../ui/icons";
|
||||
import { formatCompactInteger, formatInteger } from "../../../shared/utils/numberFormat";
|
||||
import { Icon } from "../../../shared/ui/Icon";
|
||||
import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip";
|
||||
import { informationOutlineIcon } from "../../../shared/ui/icons";
|
||||
import { CacheHitRateChart } from "./CacheHitRateChart";
|
||||
import styles from "./HomeMetrics.module.scss";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../styles/typography" as type;
|
||||
@use "../../../styles/typography" as type;
|
||||
|
||||
.root,
|
||||
.presets {
|
||||
@@ -1,12 +1,12 @@
|
||||
import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom";
|
||||
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseTimeInput } from "../../utils/parseTimeInput";
|
||||
import controls from "../ui/Controls.module.scss";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { MultiSelect, type MultiSelectOption } from "../ui/MultiSelect";
|
||||
import { TooltipTrigger } from "../ui/TooltipTrigger";
|
||||
import { refreshIcon } from "../ui/icons";
|
||||
import { parseTimeInput } from "../../../shared/utils/parseTimeInput";
|
||||
import controls from "../../../shared/ui/Controls.module.scss";
|
||||
import { Icon } from "../../../shared/ui/Icon";
|
||||
import { MultiSelect, type MultiSelectOption } from "../../../shared/ui/MultiSelect";
|
||||
import { TooltipTrigger } from "../../../shared/ui/TooltipTrigger";
|
||||
import { refreshIcon } from "../../../shared/ui/icons";
|
||||
import styles from "./OverviewTimeRangeFilter.module.scss";
|
||||
|
||||
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import { useAppStore } from "../../store/appStore";
|
||||
import controls from "../ui/Controls.module.scss";
|
||||
import { useAppStore } from "../../shared/store/appStore";
|
||||
import controls from "../../shared/ui/Controls.module.scss";
|
||||
import styles from "./CursorSettings.module.scss";
|
||||
|
||||
const CaReady = createContext(false);
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { IconifyIcon } from "@iconify/react/offline";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Sortable from "sortablejs";
|
||||
import type { Model } from "../../api";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Card } from "../ui/Card";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../ui/icons";
|
||||
import type { Model } from "../../shared/api";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Card } from "../../shared/ui/Card";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../../shared/ui/icons";
|
||||
import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult";
|
||||
import styles from "./CursorSettings.module.scss";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ModelInput, ModelType } from "../../api";
|
||||
import { defaultCustomHeadersText } from "../../utils/modelDefaults";
|
||||
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Checkbox } from "../ui/Checkbox";
|
||||
import { FormField, SecretTextInput, TextInput } from "../ui/FormControls";
|
||||
import { JsonEditor } from "../ui/JsonEditor";
|
||||
import { Combobox, Select } from "../ui/Select";
|
||||
import { Switch } from "../ui/Switch";
|
||||
import { claudeIcon, openAiIcon } from "../ui/icons";
|
||||
import type { ModelInput, ModelType } from "../../shared/api";
|
||||
import { defaultCustomHeadersText } from "../../shared/utils/modelDefaults";
|
||||
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../shared/utils/modelPresets";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Checkbox } from "../../shared/ui/Checkbox";
|
||||
import { FormField, SecretTextInput, TextInput } from "../../shared/ui/FormControls";
|
||||
import { JsonEditor } from "../../shared/ui/JsonEditor";
|
||||
import { Combobox, Select } from "../../shared/ui/Select";
|
||||
import { Switch } from "../../shared/ui/Switch";
|
||||
import { claudeIcon, openAiIcon } from "../../shared/ui/icons";
|
||||
import { CursorPresetChips } from "./CursorPresetChips";
|
||||
import styles from "./CursorSettings.module.scss";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ModelConnectivityResult } from "../../api";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { TooltipTrigger } from "../ui/TooltipTrigger";
|
||||
import { informationOutlineIcon } from "../ui/icons";
|
||||
import type { ModelConnectivityResult } from "../../shared/api";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
|
||||
import { informationOutlineIcon } from "../../shared/ui/icons";
|
||||
import styles from "./CursorModelTestResult.module.scss";
|
||||
|
||||
export type CursorModelTestState =
|
||||
@@ -21,7 +21,7 @@ export function CursorModelTestResult({ state, testing = false }: { state?: Curs
|
||||
const detail = success
|
||||
? t("速度 {speed} tokens/s · 首字 {firstText} ms · 总耗时 {duration} ms · 输出 {tokens} tokens{estimated} · 返回:{output}", {
|
||||
speed: formatSpeed(state.result.tokens_per_second),
|
||||
firstText: state.result.first_text_ms ?? "--",
|
||||
firstText: state.result.first_valid_response_ms ?? "--",
|
||||
duration: state.result.duration_ms,
|
||||
tokens: state.result.output_tokens,
|
||||
estimated: state.result.tokens_estimated ? t("(估算)") : "",
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ModelType } from "../../api";
|
||||
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets";
|
||||
import type { ModelType } from "../../shared/api";
|
||||
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../shared/utils/modelPresets";
|
||||
import styles from "./CursorPresetChips.module.scss";
|
||||
|
||||
/** 常用服务商预设:点击按当前协议类型自动填充对应端点与默认模型 */
|
||||
@@ -1,21 +1,21 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api, type Model, type ModelInput } from "../api";
|
||||
import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "../components/cursor/CursorGates";
|
||||
import { CursorModelCards, cursorModelGroups, type CursorModelGrouping } from "../components/cursor/CursorModelCards";
|
||||
import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "../components/cursor/CursorModelEditor";
|
||||
import { CursorModelTestResult, type CursorModelTestState } from "../components/cursor/CursorModelTestResult";
|
||||
import styles from "../components/cursor/CursorSettings.module.scss";
|
||||
import { PageContent } from "../components/layout/PageContent";
|
||||
import { LegacyModelImport } from "../components/models/LegacyModelImport";
|
||||
import { ConfirmDialog } from "../components/ui/ConfirmDialog";
|
||||
import controls from "../components/ui/Controls.module.scss";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { Modal } from "../components/ui/Modal";
|
||||
import { TooltipTrigger } from "../components/ui/TooltipTrigger";
|
||||
import { addIcon } from "../components/ui/icons";
|
||||
import { useMessage } from "../components/ui/message";
|
||||
import { PageActions } from "../layouts/PageActions";
|
||||
import { appStore, useAppStore } from "../store/appStore";
|
||||
import { api, type Model, type ModelInput } from "../../shared/api";
|
||||
import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "./CursorGates";
|
||||
import { CursorModelCards, cursorModelGroups, type CursorModelGrouping } from "./CursorModelCards";
|
||||
import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "./CursorModelEditor";
|
||||
import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult";
|
||||
import styles from "./CursorSettings.module.scss";
|
||||
import { PageContent } from "../../shell/layout/PageContent";
|
||||
import { LegacyModelImport } from "./LegacyModelImport";
|
||||
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
|
||||
import controls from "../../shared/ui/Controls.module.scss";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { Modal } from "../../shared/ui/Modal";
|
||||
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
|
||||
import { addIcon } from "../../shared/ui/icons";
|
||||
import { useMessage } from "../../shared/ui/message";
|
||||
import { PageActions } from "../../shell/PageActions";
|
||||
import { appStore, useAppStore } from "../../shared/store/appStore";
|
||||
|
||||
export function CursorSettingsPage() {
|
||||
const { models, cursorHarness, cursorBusy } = useAppStore();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { api, type LegacyModelImportPreview } from "../../api";
|
||||
import { appStore } from "../../store/appStore";
|
||||
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
||||
import { useMessage } from "../ui/message";
|
||||
import { api, type LegacyModelImportPreview } from "../../shared/api";
|
||||
import { appStore } from "../../shared/store/appStore";
|
||||
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
|
||||
import { useMessage } from "../../shared/ui/message";
|
||||
import styles from "./LegacyModelImport.module.scss";
|
||||
|
||||
type LegacyModelImportControl = {
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
writeAutostart,
|
||||
writeDockIconVisibility,
|
||||
writeSilentStart,
|
||||
} from "../../native/appLifecycle";
|
||||
import { updateStore, useUpdateStore } from "../../store/updateStore";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Switch } from "../ui/Switch";
|
||||
import { TitledCard } from "../ui/TitledCard";
|
||||
import { useMessage } from "../ui/message";
|
||||
} from "../../shared/native/appLifecycle";
|
||||
import { updateStore, useUpdateStore } from "../../shared/store/updateStore";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Switch } from "../../shared/ui/Switch";
|
||||
import { TitledCard } from "../../shared/ui/TitledCard";
|
||||
import { useMessage } from "../../shared/ui/message";
|
||||
import styles from "./AppLifecycleSettingsCard.module.scss";
|
||||
|
||||
export function AppLifecycleSettingsCard() {
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ProxySettings, ProxySettingsInput } from "../../api";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Checkbox } from "../ui/Checkbox";
|
||||
import { TextInput } from "../ui/FormControls";
|
||||
import { Select } from "../ui/Select";
|
||||
import { TitledCard } from "../ui/TitledCard";
|
||||
import type { ProxySettings, ProxySettingsInput } from "../../shared/api";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Checkbox } from "../../shared/ui/Checkbox";
|
||||
import { TextInput } from "../../shared/ui/FormControls";
|
||||
import { Select } from "../../shared/ui/Select";
|
||||
import { TitledCard } from "../../shared/ui/TitledCard";
|
||||
import styles from "./ProxySettingsCard.module.scss";
|
||||
|
||||
export function ProxySettingsCard({
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../styles/typography" as type;
|
||||
@use "../../styles/typography" as type;
|
||||
|
||||
.page {
|
||||
display: grid;
|
||||
@@ -41,6 +41,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textButton {
|
||||
padding: 4px;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover { color: var(--vscode-textLink-activeForeground); }
|
||||
}
|
||||
|
||||
.importRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -59,15 +68,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textButton {
|
||||
padding: 4px;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover { color: var(--vscode-textLink-activeForeground); }
|
||||
}
|
||||
|
||||
.confirmContent {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -155,6 +155,7 @@
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.themeActions .selected {
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type TabSettings } from "../api";
|
||||
import { PageContent } from "../components/layout/PageContent";
|
||||
import { LegacyModelImport } from "../components/models/LegacyModelImport";
|
||||
import { AppLifecycleSettingsCard } from "../components/settings/AppLifecycleSettingsCard";
|
||||
import { ProxySettingsCard } from "../components/settings/ProxySettingsCard";
|
||||
import { TabSettingsCard } from "../components/settings/TabSettingsCard";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { Checkbox } from "../components/ui/Checkbox";
|
||||
import { ConfirmDialog } from "../components/ui/ConfirmDialog";
|
||||
import { FormField, TextInput } from "../components/ui/FormControls";
|
||||
import { Select } from "../components/ui/Select";
|
||||
import { TitledCard } from "../components/ui/TitledCard";
|
||||
import { setLocalePreference, useI18n, type LocalePreference } from "../i18n/store";
|
||||
import { useMessage } from "../components/ui/message";
|
||||
import { appStore, useAppStore } from "../store/appStore";
|
||||
import { themeOptions } from "../theme/theme";
|
||||
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type StatisticsStorageScope, type TabSettings } from "../../shared/api";
|
||||
import { PageContent } from "../../shell/layout/PageContent";
|
||||
import { LegacyModelImport } from "../models/LegacyModelImport";
|
||||
import { AppLifecycleSettingsCard } from "./AppLifecycleSettingsCard";
|
||||
import { ProxySettingsCard } from "./ProxySettingsCard";
|
||||
import { TabSettingsCard } from "./TabSettingsCard";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { Checkbox } from "../../shared/ui/Checkbox";
|
||||
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
|
||||
import { FormField, TextInput } from "../../shared/ui/FormControls";
|
||||
import { Select } from "../../shared/ui/Select";
|
||||
import { TitledCard } from "../../shared/ui/TitledCard";
|
||||
import { setLocalePreference, useI18n, type LocalePreference } from "../../i18n/store";
|
||||
import { useMessage } from "../../shared/ui/message";
|
||||
import { appStore, useAppStore } from "../../shared/store/appStore";
|
||||
import { themeOptions } from "../../shared/theme/theme";
|
||||
import styles from "./SettingsPage.module.scss";
|
||||
|
||||
export function SettingsPage() {
|
||||
@@ -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>
|
||||
@@ -1,10 +1,10 @@
|
||||
import cursorIconUrl from "../../assets/icons/cursor.svg";
|
||||
import type { TabMode, TabSettings } from "../../api";
|
||||
import { Button } from "../ui/Button";
|
||||
import { TextInput } from "../ui/FormControls";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { Select } from "../ui/Select";
|
||||
import { TitledCard } from "../ui/TitledCard";
|
||||
import cursorIconUrl from "../../shared/assets/icons/cursor.svg";
|
||||
import type { TabMode, TabSettings } from "../../shared/api";
|
||||
import { Button } from "../../shared/ui/Button";
|
||||
import { TextInput } from "../../shared/ui/FormControls";
|
||||
import { Icon } from "../../shared/ui/Icon";
|
||||
import { Select } from "../../shared/ui/Select";
|
||||
import { TitledCard } from "../../shared/ui/TitledCard";
|
||||
import styles from "./TabSettingsCard.module.scss";
|
||||
|
||||
export function TabSettingsCard({
|
||||
@@ -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": "服务端口"
|
||||
|
||||
@@ -2,8 +2,8 @@ import ReactDOM from "react-dom/client";
|
||||
import "../node_modules/monaco-editor/min/vs/editor/editor.main.css";
|
||||
import { I18nRoot } from "./i18n/I18nRoot";
|
||||
import { initializeI18n } from "./i18n/store";
|
||||
import { appStore } from "./store/appStore";
|
||||
import { applyTheme } from "./theme/theme";
|
||||
import { appStore } from "./shared/store/appStore";
|
||||
import { applyTheme } from "./shared/theme/theme";
|
||||
import "./styles/globals.scss";
|
||||
|
||||
initializeI18n();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { CallTable } from "../components/CallTable";
|
||||
import { PageContent } from "../components/layout/PageContent";
|
||||
import { appStore, useAppStore } from "../store/appStore";
|
||||
import styles from "./CallsPage.module.scss";
|
||||
|
||||
export function CallsPage() {
|
||||
const { calls } = useAppStore();
|
||||
const content = <div className={styles.page}><CallTable calls={calls} onDetails={(call) => void appStore.openCallDetails(call.call_id)} /></div>;
|
||||
return <PageContent fixed title={t("调用")} contentClassName={styles.pageContent} sections={[{ key: "calls", estimatedHeight: 720, content }]} />;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AdRuntime } from "./components/ads/types";
|
||||
import type { Locale } from "./i18n/runtime";
|
||||
import type { AdRuntime } from "../shell/ads/types";
|
||||
import type { Locale } from "../i18n/runtime";
|
||||
|
||||
export type ModelType = "openai" | "anthropic";
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface LegacyModelImportResult {
|
||||
|
||||
export interface ModelConnectivityResult {
|
||||
duration_ms: number;
|
||||
first_text_ms: number | null;
|
||||
first_valid_response_ms: number | null;
|
||||
output_tokens: number;
|
||||
tokens_per_second: number;
|
||||
tokens_estimated: boolean;
|
||||
@@ -116,6 +116,8 @@ export interface StatisticsStorage {
|
||||
trace_count: number;
|
||||
}
|
||||
|
||||
export type StatisticsStorageScope = "details" | "all";
|
||||
|
||||
export type ProxyMode = "system" | "custom";
|
||||
|
||||
export interface ProxySettings {
|
||||
@@ -195,6 +197,7 @@ export interface LlmCall {
|
||||
created_at_ms: number;
|
||||
ttfb_ms: number | null;
|
||||
ttft_ms: number | null;
|
||||
ttfr_ms: number | null;
|
||||
duration_ms: number | null;
|
||||
input_tokens: number | null;
|
||||
output_tokens: number | null;
|
||||
@@ -330,7 +333,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"),
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 711 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -177,6 +177,14 @@ export const appStore = {
|
||||
}
|
||||
},
|
||||
|
||||
async refreshCalls() {
|
||||
try {
|
||||
update({ calls: await api.calls() });
|
||||
} catch (cause) {
|
||||
update({ error: cause instanceof Error ? cause.message : String(cause) });
|
||||
}
|
||||
},
|
||||
|
||||
async openCallDetails(callId: string) {
|
||||
await perform(() => api.openCallDetails(callId));
|
||||
},
|
||||