Skip to content

Commit b8fcd9a

Browse files
committed
feat: add relay service config workspace
1 parent 515075e commit b8fcd9a

17 files changed

Lines changed: 1191 additions & 26 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
name: gettokens-domain-engineering
3+
description: Technical engineering for GetTokens. Covers the Accounts domain (auth files, API keys, quota rules), UI system (Swiss-industrial aesthetic, theme, l10n), frontend debugging (inspectors, logs), and CLIProxyAPI fork maintenance.
4+
---
5+
6+
# GetTokens Domain & Engineering
7+
8+
This skill unifies the technical rules for building, styling, and debugging GetTokens.
9+
10+
## 1. Accounts Domain & Unified Inventory
11+
- **Model**: Unified `AccountRecord` combining auth files (sidecar), API keys (local store), and Codex quota (projected telemetry).
12+
- **Rules**:
13+
- Keep `provider` and `credentialSource` separate.
14+
- Uniqueness is by asset (e.g., `auth-file:<name>`).
15+
- Do not fetch accounts until sidecar is `ready`.
16+
- Reload from Wails after create/delete instead of hand-merging state.
17+
- `codex api key` lives in local storage under `~/.config/gettokens-data/codex-api-keys/`, not in `auth-dir`.
18+
- `AccountsPage` is assembly-only; heavy UI lives under `frontend/src/pages/accounts/`, and data/actions live in hook modules.
19+
20+
## 2. Relay Service Config Boundary
21+
- **Model**: Relay service client keys are sidecar top-level `api-keys`, not upstream provider assets such as `codex-api-key`.
22+
- **Rules**:
23+
- Never use account-pool `api-key` assets as the Status page relay key source.
24+
- Status/relay configuration must read and write sidecar `api-keys` through Wails + management API.
25+
- Relay key editing may be multi-value; preserve order, trim blanks, and deduplicate exact duplicates.
26+
- Relay endpoint previews should expose `localhost`, hostname, and LAN IP forms when available.
27+
- If relay config is meant for LAN clients, sidecar bind host must not be restricted to `127.0.0.1`.
28+
29+
## 3. Quota Rules
30+
- **Path**: `AccountsPage` -> `GetCodexQuota` -> Wails -> `POST /v0/management/api-call`.
31+
- **Logic**: CLIProxyAPI injects token via `auth_index` for target `chatgpt.com/backend-api/wham/usage`.
32+
- **Debugging**: Verify both Wails debug events and CLIProxyAPI token resolution.
33+
- **Time**: Relative reset countdown must use raw unix seconds (`resetAtUnix`). Do not re-parse `resetLabel` for countdown logic, because display labels lose seconds and drift into false `0s`.
34+
35+
## 4. UI System & Visual Thesis
36+
- **Aesthetic**: Swiss-industrial (black/white/gray, thick borders, hard shadows, monospace).
37+
- **Themes**: Support `system`, `light`, and `dark`. Ensure `--bg-main` and `--bg-surface` are distinct in dark mode.
38+
- **l10n**: Add new copy to both `zh.json` and `en.json`. Default is Chinese.
39+
- **Controls**: Use segmented controls for discrete settings.
40+
41+
## 5. Frontend Debugging & Inspection
42+
- **Tools**: Use `@linhey/react-debug-inspector` in `main.tsx` (dev-only).
43+
- **Config**: Use `createViteDebugInspectorPlugin()` in `vite.config.js` for stable JSX metadata.
44+
- **Workflow**: Prove handler -> bridge call -> backend response. Use `data-collaboration-id` for markers.
45+
46+
## 6. CLIProxyAPI Fork Maintenance
47+
- **Remotes**: `origin` (linhay), `upstream` (router-for-me).
48+
- **Workflow**: Sync upstream -> patch maintenance branch -> rebuild sidecar -> replace binary in `GetTokens.app`.
49+
- **Binary**: Sidecar binary lives at `build/bin/GetTokens.app/Contents/MacOS/cli-proxy-api`.
50+
51+
## 7. Build Metadata & Version Boundary
52+
- **Rule**: Keep `Version` for updater comparison and release/tag semantics. Do not reuse it for UI-only date labels.
53+
- **Display**: Use a separate `ReleaseLabel` for UI surfaces such as Sidebar build/version badges.
54+
- **Format**: `ReleaseLabel` uses `YYYY.MM.DD.HH`.
55+
- **Injection**: Inject `ReleaseLabel` at release build time via `-ldflags`, and keep the generation timezone explicit.
56+
- **Fallback**: Development builds may derive a local fallback label in the frontend, but release builds must prefer the injected value.
57+
58+
## Acceptance Checklist
59+
- Accounts and API keys survive restart and render correctly.
60+
- UI maintains visual consistency and legibility across themes.
61+
- Debug helpers are guarded by dev-only checks.
62+
- CLIProxyAPI patches are committed to the fork and reflected in the runtime binary.
63+
- Build metadata does not couple UI display labels to updater version comparison.

app.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type CodexQuotaWindow struct {
5959
Label string `json:"label"`
6060
RemainingPercent *int `json:"remainingPercent,omitempty"`
6161
ResetLabel string `json:"resetLabel"`
62+
ResetAtUnix int64 `json:"resetAtUnix,omitempty"`
6263
}
6364

6465
type CodexQuotaResponse struct {
@@ -95,6 +96,18 @@ type CreateCodexAPIKeyInput struct {
9596
ExcludedModels []string `json:"excludedModels,omitempty"`
9697
}
9798

99+
type RelayServiceConfig struct {
100+
APIKeys []string `json:"apiKeys"`
101+
Endpoints []RelayServiceEndpoint `json:"endpoints"`
102+
}
103+
104+
type RelayServiceEndpoint struct {
105+
ID string `json:"id"`
106+
Kind string `json:"kind"`
107+
Host string `json:"host"`
108+
BaseURL string `json:"baseUrl"`
109+
}
110+
98111
func NewApp() *App {
99112
return &App{
100113
core: wailsapp.New(Version, ReleaseLabel, GitHubRepo),
@@ -117,10 +130,14 @@ func (a *App) GetVersion() string {
117130
return a.core.GetVersion()
118131
}
119132

120-
121133
func (a *App) GetReleaseLabel() string {
122134
return a.core.GetReleaseLabel()
123135
}
136+
137+
func (a *App) CanApplyUpdate() bool {
138+
return a.core.CanApplyUpdate()
139+
}
140+
124141
func (a *App) CheckUpdate() (*updater.ReleaseInfo, error) {
125142
return a.core.CheckUpdate()
126143
}
@@ -208,6 +225,7 @@ func (a *App) GetCodexQuota(name string) (*CodexQuotaResponse, error) {
208225
Label: window.Label,
209226
RemainingPercent: window.RemainingPercent,
210227
ResetLabel: window.ResetLabel,
228+
ResetAtUnix: window.ResetAtUnix,
211229
})
212230
}
213231

@@ -230,6 +248,42 @@ func (a *App) ListAccounts() ([]AccountRecord, error) {
230248
return records, nil
231249
}
232250

251+
func (a *App) GetRelayServiceConfig() (*RelayServiceConfig, error) {
252+
result, err := a.core.GetRelayServiceConfig()
253+
if err != nil {
254+
return nil, err
255+
}
256+
257+
return &RelayServiceConfig{
258+
APIKeys: append([]string(nil), result.APIKeys...),
259+
Endpoints: mapRelayServiceEndpoints(result.Endpoints),
260+
}, nil
261+
}
262+
263+
func (a *App) UpdateRelayServiceAPIKey(apiKey string) (*RelayServiceConfig, error) {
264+
result, err := a.core.UpdateRelayServiceAPIKey(apiKey)
265+
if err != nil {
266+
return nil, err
267+
}
268+
269+
return &RelayServiceConfig{
270+
APIKeys: append([]string(nil), result.APIKeys...),
271+
Endpoints: mapRelayServiceEndpoints(result.Endpoints),
272+
}, nil
273+
}
274+
275+
func (a *App) UpdateRelayServiceAPIKeys(apiKeys []string) (*RelayServiceConfig, error) {
276+
result, err := a.core.UpdateRelayServiceAPIKeys(apiKeys)
277+
if err != nil {
278+
return nil, err
279+
}
280+
281+
return &RelayServiceConfig{
282+
APIKeys: append([]string(nil), result.APIKeys...),
283+
Endpoints: mapRelayServiceEndpoints(result.Endpoints),
284+
}, nil
285+
}
286+
233287
func (a *App) CreateCodexAPIKey(input CreateCodexAPIKeyInput) error {
234288
return a.core.CreateCodexAPIKey(wailsapp.CreateCodexAPIKeyInput{
235289
APIKey: input.APIKey,
@@ -266,3 +320,16 @@ func mapAccountRecord(record accountsdomain.AccountRecord) AccountRecord {
266320
LocalOnly: record.LocalOnly,
267321
}
268322
}
323+
324+
func mapRelayServiceEndpoints(items []wailsapp.RelayServiceEndpoint) []RelayServiceEndpoint {
325+
endpoints := make([]RelayServiceEndpoint, 0, len(items))
326+
for _, item := range items {
327+
endpoints = append(endpoints, RelayServiceEndpoint{
328+
ID: item.ID,
329+
Kind: item.Kind,
330+
Host: item.Host,
331+
BaseURL: item.BaseURL,
332+
})
333+
}
334+
return endpoints
335+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# 2026-04-26 Relay Service Config Boundary
2+
3+
## 背景
4+
5+
本轮会话围绕 `StatusPage` 的“后端中转服务配置”反复收敛,最终明确了一条之前容易混淆的边界:
6+
7+
1. 聚合服务对客户端暴露的 key
8+
2. 聚合服务内部使用的上游供应商 key
9+
10+
这两者不是同一种资产,也不应该共用同一套页面取数逻辑。
11+
12+
## 结论
13+
14+
### 1. relay service key 的事实源
15+
16+
`StatusPage` 上展示给用户复制的 `auth.json``OPENAI_API_KEY`,事实源必须是 sidecar 顶层 `api-keys`
17+
18+
原因:
19+
20+
1. 顶层 `api-keys` 才是客户端访问聚合服务时携带的鉴权 key
21+
2. `codex-api-key` / `gemini-api-key` / `claude-api-key` 等是聚合服务内部路由到上游供应商时使用的凭据池
22+
3. 把上游供应商 key 直接外露给客户端,会破坏“聚合服务作为统一入口”的职责边界
23+
24+
因此:
25+
26+
- 状态页不能再从账号池 `api-key` 资产推导 relay key
27+
- relay key 的读写必须走 sidecar management API:`/v0/management/api-keys`
28+
29+
### 2. 账号池 API Key 与 relay key 的关系
30+
31+
账号池中的 `api-key` 资产,当前继续表示“上游供应商资产”。
32+
33+
例如:
34+
35+
- `codex-api-key`
36+
- `openai-compatibility`
37+
- 其他 provider 对应的兼容配置
38+
39+
这些资产用于:
40+
41+
1. 供应商配置工作台预览
42+
2. sidecar 上游路由
43+
3. provider 级配置复制
44+
45+
这些资产不用于:
46+
47+
1. 状态页的客户端接入 key
48+
2. 局域网客户端访问聚合服务的统一入口配置
49+
50+
### 3. relay key 允许多值
51+
52+
sidecar 顶层 `api-keys` 原生支持列表,因此状态页不能再只建模为单个字符串。
53+
54+
当前规则:
55+
56+
1. 状态页以“每行一个 key”的方式维护 relay key 列表
57+
2. 保存时做 trim
58+
3. 保存时去重,但保持输入顺序
59+
4. 预览配置时允许选择其中任意一条 key
60+
61+
### 4. 局域网访问的 host 边界
62+
63+
如果状态页要给出局域网客户端可直接使用的配置,仅展示 `127.0.0.1` 是不够的。
64+
65+
当前规则:
66+
67+
1. sidecar bind host 不再限制为 `127.0.0.1`
68+
2. 状态页同时展示三类可访问地址:
69+
- `localhost`
70+
- 主机名
71+
- 局域网 IP
72+
3. 配置预览时允许在这些地址之间切换
73+
74+
注意:
75+
76+
1. management API 仍然通过 management key 保护
77+
2. 本轮没有把 `remote-management.allow-remote` 打开给外部设备管理使用
78+
3. 本轮目标是让业务入口可被局域网访问,不是开放远程管理面板
79+
80+
## 实现落点
81+
82+
- sidecar 配置写入:`internal/sidecar/manager.go`
83+
- relay service 读写聚合:`internal/wailsapp/relay_service.go`
84+
- management API client:`internal/cliproxyapi/client.go`
85+
- 状态页配置 UI:`frontend/src/pages/StatusPage.tsx`
86+
87+
## 后续建议
88+
89+
1. 如果后续需要按团队或终端分发不同 client key,可以继续沿用顶层 `api-keys` 多值模型
90+
2. 如果需要更强的客户端接入治理,再考虑补“key 标签 / 备注 / 启停状态”,而不是回退到复用上游 provider 资产
91+
3. 如果局域网访问成为正式能力,建议后续补一条真实桌面端验收:另一台设备使用主机名或内网 IP 连通 `/v1`

docs-linhay/memory/2026-04-26.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,34 @@
55
- `frontend/src/utils/version.ts` 将 Sidebar 版本展示统一格式化为 `YYYY.MM.DD.HH`
66
- `Sidebar` 底部文案改为 `VERSION`,并展示格式化后的发布日期标签。
77
- Release 工作流在构建阶段按 `Asia/Shanghai` 时区注入 `ReleaseLabel`
8+
9+
## Release Prep & Auto Update
10+
- 首发版本建议采用 `v0.1.0`:当前仓库无历史 tag,且前端版本号已是 `0.1.0`
11+
- 自动升级继续沿用 `go-selfupdate`,不重写更新框架;本轮补的是 release 资产分层、universal 选择配置和设置页入口。
12+
- GitHub Release workflow 现在同时产出下载安装包与 updater 原始资产:安装包给用户下载,`.tar.gz` 原始二进制给自更新框架消费。
13+
- Windows 安装包重命名为 `GetTokens_windows_amd64_installer.exe`,避免被 `go-selfupdate` 误选。
14+
- Settings 页面新增检查更新入口;Windows / Linux 保持 `ApplyUpdate`,macOS 改为打开 release 页面安装,避免对已签名 `.app` 做 bundle 内原地替换。
15+
- `internal/updater` 增加显式版本比较 helper,修复“同版本 release 也会被误报为可更新”的假阳性问题。
16+
- macOS release workflow 调整为“先签名并公证 `.app`,再生成并公证 DMG”;不能先打 DMG 再签 app,否则 DMG 内仍然是未签名 app。
17+
18+
## Accounts Domain Boundary Refresh
19+
- `AccountsPage` 已正式拆成页面装配层,重型 UI 与状态逻辑下沉到 `frontend/src/pages/accounts/``useAccountsPageState.ts`;后续账号池迭代不要再回到单文件大页。
20+
- 统一账号池仍以 `AccountRecord` 为唯一前端模型;`provider``credentialSource` 继续分离表达。
21+
- `codex api key` 的事实源固定为本地目录 `~/.config/gettokens-data/codex-api-keys/`,而不是 `auth-dir` 或 sidecar `config.yaml`
22+
23+
## Codex Quota Reset Time Rule
24+
- 相对倒计时必须使用原始 unix 秒字段 `resetAtUnix` 计算,`resetLabel` 只做展示。
25+
- 直接反解析 `resetLabel` 会因为秒级精度丢失导致临近重置时误显示 `0s`
26+
- 这条规则已经在 Go 额度窗口 DTO 和前端 quota helper 上收敛,并补了测试。
27+
- 会话整理结论:该边界已上升为项目级可复用模式,已同步写入 `gettokens-domain-engineering``docs-linhay/dev/20260426-release-label-version-boundary.md`;不需要更新 `AGENTS.md`
28+
29+
## Provider Config Setup
30+
- 新建 `docs-linhay/spaces/20260426-provider-config-setup/README.md`,将“API Key 复制 / 供应商配置片段 / 一键复制配置 / 缺失值补填”收敛为独立需求空间。
31+
- `ApiKeyDetailModal` 新增配置工作台:支持分别复制 `API KEY``BASE URL``PREFIX`,并根据当前 provider 生成配置片段,一键复制整段配置。
32+
- 缺失字段不阻塞面板使用,允许用户先在详情面板补填后再复制配置;当前实现为前端工作台,不扩展为完整 provider 管理后台。
33+
- `StatusPage` 补充“后端中转服务配置”卡片,直接给出指向本地 sidecar 端口的最小 `auth.json` 示例,仅保留 `auth_mode``OPENAI_API_KEY``base_url` 三个有用字段。
34+
- 聚合服务自己的客户端 API KEY 与上游 `codex-api-key` 资产分离:状态页读取的是 sidecar 顶层 `api-keys`,并支持通过管理接口热更新保存,不再错误复用账号池中的上游供应商 API KEY。
35+
- 聚合服务客户端 API KEY 现支持多值维护;状态页以“每行一个 key”的形式编辑并保存,同时可选择任意 key 生成配置预览。
36+
- sidecar 默认绑定全接口而不是 `127.0.0.1`,状态页会同步展示 `localhost`、主机名和局域网 IP 形式的访问地址,方便局域网设备接入。
37+
- 本轮验证完成:`npm run typecheck``npm run build``docs-linhay/scripts/check-docs.sh`;未运行额外自动化 UI/E2E 测试,因为前端仓库当前未接入对应测试基建。
38+
- 会话整理结论:`relay service key` 与上游 provider key 的边界已上升为项目级可复用模式,已同步写入 `gettokens-domain-engineering``docs-linhay/dev/20260426-relay-service-config-boundary.md`;本轮不需要更新 `AGENTS.md`
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 供应商配置与一键复制配置
2+
3+
## 背景
4+
当前账号池中的 API Key 详情面板只展示基础字段,缺少直接复制 `API KEY` / `BASE URL` 的入口,也没有针对当前 provider 的配置片段生成能力。
5+
同时状态页缺少一份可直接指向本地 sidecar 聚合中转服务的最小 `auth.json` 配置示例,也没有入口查看或修改聚合服务自己的客户端 `api-keys`,更不能直接给出主机名 / 局域网地址形式的访问配置。
6+
7+
## 目标
8+
为 API Key 详情面板补齐一个可直接交付给用户的配置工作台:
9+
- 单独复制 `API KEY` / `BASE URL` / `PREFIX`
10+
- 生成当前 provider 对应的配置片段
11+
- 提供一键复制配置按钮
12+
- 当字段缺失时允许用户在面板内补填后再复制
13+
- 在状态页展示本地后端中转服务可用的最小 `auth.json` 配置示例
14+
- 支持在状态页查看并修改聚合服务自己的客户端 API KEY
15+
- 支持多客户端 API KEY 管理,并展示 `localhost` / `hostname` / `LAN IP` 三类访问地址
16+
17+
## 范围
18+
- `frontend/src/pages/accounts/ApiKeyDetailModal.tsx`
19+
- `frontend/src/pages/accounts/helpers.ts`
20+
- `frontend/src/pages/StatusPage.tsx`
21+
- `frontend/src/locales/zh.json`
22+
- `frontend/src/locales/en.json`
23+
24+
## 非目标
25+
- 本轮不扩展成完整的 provider 管理后台
26+
- 本轮不新增新的账号类型或新的侧边导航入口
27+
- 本轮不处理 auth file 详情的 provider 配置生成
28+
29+
## 验收标准
30+
- 打开 API Key 详情面板后,可以分别复制 `API KEY``BASE URL``PREFIX`
31+
- 面板中可看到基于当前 provider 生成的配置片段
32+
- 点击一键复制按钮后,可复制整段配置
33+
-`API KEY``BASE URL` 为空时,用户可以先补填再复制
34+
- 状态页可直接复制一份指向本地 sidecar 端口的最小 `auth.json` 示例
35+
- 状态页可查看并保存聚合服务自己的客户端 API KEY,而不是复用上游供应商 API KEY 资产
36+
- 状态页支持一次维护多条客户端 API KEY,并可选择任意一条 key 与任意一个访问地址生成配置
37+
- sidecar 默认绑定全接口,保证局域网内设备可通过主机名或内网 IP 访问
38+
- 中英文文案同步更新
39+
- `frontend``typecheck``build` 通过
40+
41+
## 相关链接
42+
- [账号池 Space](/Users/linhey/Desktop/linhay-open-sources/GetTokens/docs-linhay/spaces/account-pool/README.md)
43+
- [ApiKeyDetailModal](/Users/linhey/Desktop/linhay-open-sources/GetTokens/frontend/src/pages/accounts/ApiKeyDetailModal.tsx)
44+
45+
## 当前状态
46+
- 状态:in_progress
47+
- 最近更新:2026-04-26
48+
- 补充:状态页新增“后端中转服务配置”卡片,直接读取 sidecar 顶层 `api-keys` 作为聚合服务自己的客户端 API KEY,并支持多 key 编辑、主机名展示与局域网地址选择

0 commit comments

Comments
 (0)