chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
---
version: 1.1
last-updated: 2026-03-19
status: stable
---
# 认证机制与 SavedKey 生命周期
> 本文档说明 Qiming Agent 客户端的认证设计,重点描述 `savedKey` 的作用、存储结构、完整生命周期,以及退出登录与服务启动的联动逻辑。
---
## 核心概念
### 两类认证凭证
| 凭证 | 存储键 | 说明 | 退出登录后 |
|------|--------|------|-----------|
| `configKey` | `auth.config_key` | 当前会话的登录态 token从服务端 `/register` 接口获取 | **清除** |
| `savedKey` | `auth.saved_key` | 设备级注册凭证("记住我" token与 configKey 值相同,但跨登录会话持久化 | **保留** |
> **重要**`savedKey` 与 `configKey` 值相同(均为服务端返回的 `response.configKey`),区别在于持久化策略不同。`savedKey` 的存在代表"这台设备已完成注册,可以免密重新认证"。
### 多账号隔离存储
`savedKey``domain + username` 维度分别存储,支持同一设备多账号切换:
```
auth.saved_key ← 全局快速访问(最后一次登录的账号)
auth.saved_keys.<domain>_<username> ← 域名+用户级持久化(每个账号独立)
```
**键名与迁移**:存储键中的 `<domain>``normalizeDomain()`auth.ts生成hostname 或规范化字符串。若未来调整域名规范化规则如大小写、去端口、trailing slash需考虑旧键兼容或提供迁移逻辑避免已存账号无法匹配。
---
## 完整生命周期
### 第一次登录(全新设备/账号)
```
用户输入 domain + username + password
└── getSavedKey(domain, username)
└── 未找到 → savedKey = undefined
└── registerClient({ username, password, savedKey: undefined })
└── 服务端返回 { configKey, ... }
├── setConfigKey(configKey)
│ └── auth.config_key = configKey
└── setSavedKey(configKey, domain, username)
├── auth.saved_keys.<domain>_<username> = configKey
└── auth.saved_key = configKey
```
### 退出登录
```
用户点击"退出登录"ClientPage.tsx → handleLogout
├── 1. 停止所有运行中的服务(按顺序)
│ ├── agent.destroy()
│ ├── fileServer.stop()
│ ├── lanproxy.stop()
│ ├── mcp.stop()
│ └── computerServer.stop() ← 登出时单独停止,避免进程残留与端口占用
└── 2. logout() → clearAuthInfo()
├── auth.username = null
├── auth.config_key = null ← 登录态清除
├── auth.user_info = null
├── auth.online_status = null
└── auth.saved_key = 保留 ← 设备凭证不清除,下次可免密重连
```
> **注意**:密码从不持久化保存。登录时用户输入的密码仅用于本次认证请求,不存入数据库。
### 重新打开 App有 savedKey自动重连
```
App 启动 → autoReconnectApp.tsx
├── 检查 loginStartedRef.current内存变量
│ └── 已由登录流程启动 → 跳过(同一会话内有效)
├── 检查 setupJustCompleted
│ └── 向导刚完成 → 直接启动服务(不走重连)
└── 读取 auth.saved_key
├── 不存在 → 跳过(首次使用,需手动登录)
└── 存在 → syncConfigToServer({ suppressToast: true })
├── 成功(服务端接受 savedKey
│ ├── 更新 auth.config_key新 token
│ ├── 更新 auth.saved_key续期
│ ├── 更新 auth.online_status / user_info
│ └── startServicesSequentially(
│ ['mcpProxy', 'agent', 'fileServer', 'lanproxy']
│ )
└── 失败(网络断开 / token 过期 / 账号被封)
└── 服务不启动,停留在登录页
```
### 用户再次登录(已有 savedKey 的账号)
```
用户输入 domain + username + password
└── getSavedKey(domain, username)
└── 找到 → savedKey = "<持久化的 configKey>"
└── registerClient({ username, password, savedKey })
└── 服务端识别为老设备,直接续期返回新 configKey
```
### 切换账号(不同 domain 或 username
```
用户输入 domainB + userB与上次不同
└── getSavedKey(domainB, userB)
└── 未找到 → savedKey = undefined
└── registerClient({ username, password, savedKey: undefined })
└── 服务端视为全新设备注册
```
---
## 服务与登录状态的联动规则
| 场景 | 服务行为 |
|------|---------|
| 当前会话退出登录 | 立即停止所有运行中的服务 |
| 重启 App有 savedKey重连成功 | 自动启动全部服务mcpProxy → agent → fileServer → lanproxy|
| 重启 App有 savedKey重连失败 | 服务不启动,停留在登录页 |
| 重启 App无 savedKey | 服务不启动,停留在登录页 |
| 手动登录成功 | 由 `onComplete` 触发服务启动(不走 autoReconnect|
> **服务启动顺序固定**`mcpProxy` 必须先于 `agent`,因为 Agent 初始化时需要连接 MCP Proxy 注入 mcpServers。
---
## 相关代码位置
| 逻辑 | 文件 | 关键函数/位置 |
|------|------|--------------|
| savedKey 存取 | `src/renderer/services/core/auth.ts` | `getSavedKey()` / `setSavedKey()` L86-100 |
| 退出登录清理 | `src/renderer/services/core/auth.ts` | `clearAuthInfo()` L131-138 |
| 停止服务+登出 | `src/renderer/components/pages/ClientPage.tsx` | `handleLogout()` L171-200 |
| 自动重连逻辑 | `src/renderer/App.tsx` | `autoReconnect` effect L297-349 |
| 静默重新认证 | `src/renderer/services/core/auth.ts` | `syncConfigToServer()` L383-458 |
| 注册/登录 | `src/renderer/services/core/auth.ts` | `loginAndRegister()` L209-314 |
---
## 相关文档
- [依赖与服务生命周期](./dependency-and-services-lifecycle.md)
- [存储实现](./STORAGE.md)
- [安全审查](../reviews/SECURITY-REVIEW.md)