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,170 @@
---
version: 1.0
last-updated: 2026-03-07
status: stable
---
# ACP 引擎性能优化说明
> 针对 `/computer/chat` 首次响应延迟约 10s 的优化引擎预热池、ACP SDK 预加载、SSE 事件缓冲。
> **适用引擎**`claude-code-acp-ts`、`qimingcode`。
> **最后更新**2026-03-07
---
## 1. 问题与根因
### 1.1 现象
使用 qimingcode或 claude-code新会话首次请求的 HTTP 总耗时约 **7.5s**,加上模型推理后用户体感接近 **10s**
### 1.2 耗时分解(来自日志)
| 阶段 | 耗时 | 说明 |
|------|------|------|
| `ensureEngine` | ~4.35s | 每次新 `project_id` 冷启动 ACP 进程spawn + initialize handshake |
| `ACP newSession` | ~3.1s | 每次新 project 创建 ACP session含 MCP 连接等) |
| `ACP prompt resolved` | ~2.7s | 模型推理,无法通过客户端优化 |
| SSE 早期事件 | - | `prompt_start` 等在 SSE 连接建立前推送,前端收不到 |
### 1.3 根因
- **UnifiedAgentService** 按 `project_id` 懒惰创建 `AcpEngine`,每个新 project 都会触发一次进程冷启动。
- **rcoderTauri** 在 init 时预启动进程并跨项目复用Electron 侧未做等效预热与复用。
- **SSE**`POST /computer/chat` 先返回 `session_id`,前端再建 `GET /computer/progress/{session_id}`,存在时间差,早期事件在无客户端时被丢弃。
---
## 2. 优化方案概览
| 优化项 | 文件 | 效果 |
|--------|------|------|
| 引擎预热池 | `unifiedAgent.ts` | 新 project 的 ensureEngine 从 ~4.35s 降至 ~0ms复用预热引擎 |
| ACP SDK 预加载 | `unifiedAgent.ts` | claude-code 首次 init 时 ESM 加载不再占关键路径 |
| SSE 事件缓冲回放 | `computerServer.ts` | 无客户端时先缓冲SSE 连接建立后回放,避免丢失早期事件 |
---
## 3. 实现说明
### 3.1 引擎预热池unifiedAgent.ts
**思路**`agentService.init()` 完成后,后台**同时预热两种引擎**`claude-code``qimingcode`避免「init 用 claude-code、请求用 qimingcode」时永远无法复用。池按引擎类型存储新 project 请求到来时按 `requestedEngine` 取用;若存在且关键配置一致则复用,并立即再预热同类型以补充池子。
**新增/修改**
- **字段**`warmEnginePool: Map<AgentEngineType, AcpEngine>`(按类型各一)、`warmEngineTasks: Map<AgentEngineType, Promise<void>>`
- **startWarmingEngine(engineType)**:若该类型未在池中且无进行中任务、有 baseConfig则异步执行`new AcpEngine(engineType)``init({ ...baseConfig, engine: engineType })` → 成功则 `pool.set(engineType, engine)`,失败则 destroy。
- **init()**:末尾调用 `startWarmingEngine('claude-code')``startWarmingEngine('qimingcode')`,双引擎同时预热。
- **getOrCreateEngine()**
-`requestedEngine = effectiveConfig.engine || this.engineType || 'claude-code'` 从池中 `get(requestedEngine)`,若有则 `delete` 取出。
- 若取到且与 `effectiveConfig` 的 apiKey/baseUrl/model 一致,则挂事件、`updateConfig(effectiveConfig)`、写入 `engines`/`engineConfigs`、调用 `startWarmingEngine(requestedEngine)` 补充、返回。
- 若取到但配置不一致,则对取出的引擎 `removeAllListeners` + `destroy`,避免泄漏。
- **destroy()**
-`await Promise.all([...warmEngineTasks.values()])` 并清空 tasks再遍历 `warmEnginePool.values()` 并 destroy、清空池。
**配置匹配**:按请求的引擎类型从池中取用后,**必须 apiKey 与 baseUrl 一致**才复用;否则复用的进程仍是 init 时的认证claude-code-acp-ts 会返回 "Authentication required"、内容为空。复用后调用 `updateConfig(effectiveConfig)` 同步 model/mcpServers 等;认证不一致时销毁预热引擎并走冷启动。
**MCP 与复用**复用预热引擎时AcpEngine 的 `this.config` 仍是 init 时的 baseConfig`createSession()` 使用 `this.config.mcpServers`。若不复用后更新,本请求的 `context_servers`(已通过 ensureEngineForRequest 同步到 proxy 并写入 effectiveConfig.mcpServers不会生效。因此复用路径中在挂事件、写入 engines 之前调用 **`warm.updateConfig(effectiveConfig)`**AcpEngine 新增方法),使后续 `chat()``createSession()` 使用本请求的 MCP 配置,不影响 MCP 加载。
### 3.2 ACP SDK 预加载unifiedAgent.ts
**思路**`loadAcpSdk()` 首次调用会动态 `import('@agentclientprotocol/sdk')`,在 Electron CJS 环境有一次性解析/编译开销。在 `init()` 末尾非阻塞调用一次,将此次开销移到应用启动阶段。
**实现**:在 `init()` 中、`startWarmingEngine()` 之前增加一行:
```ts
loadAcpSdk().catch(() => {});
```
不 await失败静默忽略不影响主流程。
### 3.3 SSE 事件缓冲回放computerServer.ts
**思路**`pushSseEvent(sessionId, eventName, data)` 被调用时,若该 `sessionId` 尚无 SSE 客户端,则将事件写入内存缓冲;当有客户端通过 `GET /computer/progress/{session_id}` 连接时,先回放缓冲再正常推送新事件。
**实现**
- **常量**`SSE_EVENT_BUFFER_MAX = 50`,单 session 最多缓冲条数。
- **结构**`sseEventBuffers: Map<sessionId, { events: string[]; createdAt: number }>`
- **pushSseEvent()**:无客户端时,创建或取已有 buffer`events.length < SSE_EVENT_BUFFER_MAX` 则 push 当前 payload 后 return有客户端时逻辑不变直接写 response。
- **GET /computer/progress/{session_id}**:在把 `res` 注册到 `sseClients` 之后,若存在该 sessionId 的 buffer则依次 `res.write(eventPayload)` 回放,然后 `sseEventBuffers.delete(sessionId)`,并打日志。
- **stopComputerServer()**:在清空 `sseClients` 后调用 `sseEventBuffers.clear()`
- **Cancel 与缓冲**`POST /computer/agent/session/cancel` 会调用 `acpEngine.abortSession(sessionId)`**会中止 ACP 会话**(停止产生 SSE 的进程);取消成功后调用 `clearSseEventBuffer(sessionId)` **清除该 session 的 SSE 缓冲**,避免用户重连 `GET /computer/progress/{session_id}` 时仍回放已取消会话的旧事件。
- **Stop重启动/停止)与缓冲**`POST /computer/agent/stop` 会调用 `agentService.stopEngine(project_id)`**会停止该 project 的整个引擎**(进程销毁,其下所有 session 终止);停止前先通过 `acpEngine.listSessions()` 取得该引擎下所有 session id并逐一调用 `clearSseEventBuffer(sessionId)` **清除这些 session 的 SSE 缓冲**,避免之后重连或新建 SSE 时仍回放旧事件。
- **客户端停止/重启所有服务**:客户端通过 `services.stopAll()``services.restartAll()` 停止/重启时,会调用 `serviceManager.stopAllServices()``restartAllServices()`,在调用 `agentService.destroy()` **之前**调用 **`clearAllSseEventBuffers()`**computerServer 导出),清空全部 SSE 事件缓冲,避免重启后前端重连仍回放旧会话事件。
---
## 4. 并发与生命周期注意点Review 结论)
- **预热池并发**:从池中取引擎必须“先 shift 再使用”;若配置不匹配,对已取出的引擎做 destroy避免同一实例被多个请求复用或泄漏。
- **destroy 顺序**:先 await `warmEngineTask`,再销毁 `warmEnginePool` 中所有引擎并清空池,否则任务结束时 push 进池的引擎可能未被销毁。
---
## 5. 预期效果
| 阶段 | 优化前 | 优化后 |
|------|--------|--------|
| ensureEngine新 project | ~4.35s | ~0ms复用预热时 |
| loadAcpSdk 首次加载claude-code | 串在 init 内 | 预加载,已缓存 |
| ACP newSession | ~3.1s | ~10msqimingcode v1.1.68+ MCP 懒加载) |
| 模型推理 | ~2.7s | ~2.7s(不变) |
| **HTTP 响应总时间** | **~7.5s** | **~2.7s** |
| SSE 早期事件 | 易丢失 | 缓冲回放,不丢失 |
同项目再次请求仍走现有 session 复用,无变更。
---
## 6. 相关文件
- `src/main/services/engines/unifiedAgent.ts`预热池、SDK 预加载、getOrCreateEngine 复用与 destroy 顺序。
- `src/main/services/engines/acp/acpClient.ts``loadAcpSdk()` 定义。
- `src/main/services/computerServer.ts`SSE 缓冲、回放、`pushSseEvent``stopComputerServer` 清理。
---
## 7. 单测覆盖
- **unifiedAgent.test.ts**`UnifiedAgentService — 引擎预热池ACP 性能优化)`
- `init()``loadAcpSdk` 被调用
- `getOrCreateEngine` 在预热完成后复用池中引擎且配置一致
- 从池取出的引擎配置不匹配时被 `destroy`
- `destroy()` 清空预热池且不抛
- **computerServer.test.ts**`ComputerServer — SSE 事件缓冲`
- `getSseEventBufferSize` 在无缓冲时返回 0
- 无客户端时 `pushSseEvent` 将事件写入缓冲
- 缓冲条数上限 50`SSE_EVENT_BUFFER_MAX`
运行:`npm run test:run``npm run test:coverage`
## 8. 可选后续优化
- **SSE buffer TTL**:已实现。对长期无客户端连接的 sessionId 做 buffer 过期清理30s`SSE_EVENT_BUFFER_TTL_MS`),在 `pushSseEvent` 无客户端路径中调用 `pruneExpiredSseEventBuffers()`,避免 Map 在极端场景下增长。
- **预热配置扩展**:若未来需按 `env``mcpServers` 区分引擎,可在复用前增加比对,不匹配则销毁取出的预热引擎。
- **qimingcode 进程级 vs session 级 MCP**`OPENCODE_CONFIG_CONTENT` 在进程 spawn 时注入(来自 baseConfig会话级 MCP 以 ACP `newSession``mcpServers`(来自 `this.config`,复用路径已由 `updateConfig(effectiveConfig)` 更新)为准。当前实现以 session 级为准,进程级仅作默认/权限等用途。
## 9. 日志分析与可优化点2026-03-07
### 9.1 首包 ensureEngine 耗时分解(典型 qimingcode 冷启动)
| 阶段 | 耗时 | 说明 |
|------|------|------|
| parseCtxServers | 0ms | 解析请求 context_servers |
| **syncMcpConfigToProxyAndReload** | **~700ms** | 首请求同步 MCP 到 proxy、写 DB、必要时重启 bridge同 project 后续请求已跳过12ms |
| **ensureMemoryReady** | **~500ms** | 仅冷路径memory index dirty 时做 fileSync已通过 init 时后台预执行 `ensureMemoryReadyForSession()` 减少首包等待 |
| **getOrCreateEngine (engine.init)** | **~6s** | qimingcode 进程冷启动;复用预热池或同 project 复用后降至 0ms 级 |
| **同 project 后续请求** | **12ms** | 引擎复用生效ensureEngine 仅做 getEngineForProject + 早期返回 |
### 9.2 已做优化
- **init 时后台预做 memory 同步**`memoryService.ensureMemoryReadyForSession().catch(() => {})`,首包 getOrCreateEngine 时 ensureMemoryReady 多为快路径index 已同步),减少约 500ms。
- **同 project 不误判配置变更**:已有引擎且引擎类型未切换时直接复用,避免 detectConfigChange 误判导致每次重建。
- **session_id / project_id 查找**`getEngineForProject(engineKey)` 支持按 session_id 命中「key=project_id 但含该 session」的引擎同一会话续传复用。
### 9.3 仍可考虑的优化(未实现)
- **syncMcp 首包 ~700ms**:当前首请求必须走一次同步;若多 project 共用相同 context_servers可做「上次同步配置 hash」缓存相同则跳过 sync降低多 project 首包成本。
- **预热池命中率**:若首请求为 qimingcode 而 qimingcode 预热未就绪(约 6s仍会冷启动可考虑在 UI 侧延迟首条 chat 或提示「引擎准备中」,或接受首包 6s 仅发生一次。

View File

@@ -0,0 +1,31 @@
# ADR: 三端沙箱运行时子模块化
- 状态: accepted
- 日期: 2026-03-27
- 适用范围: `crates/agent-electron-client`
## 背景
Electron 客户端需要统一接入三端沙箱:
- macOS: `sandbox-exec`(系统)
- Linux: `bubblewrap`(系统优先 + 内置兜底)
- Windows: Codex sandbox helper内置二进制
同时要求开箱即用、可配置启停、并可长期维护运行时产物。
## 决策
将三端沙箱运行时作为独立子模块维护在 `crates/agent-sandbox-runtime`Electron 客户端通过 `prepare:sandbox-runtime` 将当前平台产物同步到 `resources/sandbox-runtime`,再通过 `extraResources` 参与打包。
## 关键取舍
1. 采用预构建产物而非构建时本地编译。
2. 子模块 commit pin 控制升级节奏,避免主仓直接漂移。
3. 主进程保留统一策略层(`sandbox_policy`),后端由策略选择。
## 影响
1. 打包链新增 `prepare:sandbox-runtime`
2. `after-sign` 增加 `resources/sandbox-runtime` 签名。
3. 文档与运维流程新增子模块升级/回滚步骤。

View File

@@ -0,0 +1,868 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Agent 自我进化架构 - 核心组件
## 概述
本文档详细介绍 Qiming Agent 自我进化系统的四大核心组件Memory记忆系统、Skill Creator技能创造器、EvoMap进化图谱和 Soul.md灵魂文件
---
## 1. Memory记忆系统
### 1.1 记忆层级结构
```typescript
interface AgentMemory {
// 短期记忆:当前会话
working: {
context: string[];
recentActions: Action[];
currentGoal: Goal;
};
// 中期记忆:跨会话
session: {
successfulPatterns: Pattern[];
failedAttempts: FailedAttempt[];
userPreferences: UserPreference[];
};
// 长期记忆:固化的知识
longTerm: {
skills: Skill[]; // 已验证的技能
principles: Principle[]; // 学习到的原则
antiPatterns: AntiPattern[]; // 避免的陷阱
};
}
```
### 1.2 记忆编码格式
```typescript
interface EncodedMemory {
id: string;
type: 'success' | 'failure' | 'insight';
embedding?: number[]; // 语义向量,用于相似性检索(可选)
context: {
task: string; // 任务类型
environment: string; // 环境信息
constraints: string[]; // 约束条件
};
action: {
tool: string; // 使用的工具
command: string; // 执行命令
parameters: Record<string, unknown>;
};
outcome: {
success: boolean;
timeTaken: number;
error?: string;
userFeedback?: 'positive' | 'negative' | 'neutral';
};
timestamp: number;
accessCount: number; // 访问频率
lastAccessed: number;
}
```
### 1.3 记忆检索接口
```typescript
interface MemoryReader {
// 语义检索
recall(query: {
situation: string;
taskType: string;
constraints: string[];
}): RecallResult;
// 获取相似记忆
getSimilarMemories(memory: EncodedMemory, limit?: number): EncodedMemory[];
// 获取相关决策
getDecision(task: string): DecisionNode;
// 清理过期记忆
cleanup(policy: MemoryCleanupPolicy): Promise<CleanupResult>;
}
interface RecallResult {
evoMap: DecisionNode;
skills: Skill[];
cases: {
successes: EncodedMemory[];
failures: EncodedMemory[];
};
confidence: number;
}
```
---
## 2. Skill Creator技能创造器
### 2.1 技能定义
```typescript
interface Skill {
id: string;
name: string;
description: string;
category: string; // 'file' | 'network' | 'system' | 'ai'
// 技能定义
definition: {
trigger: Condition[]; // 触发条件
steps: SkillStep[]; // 执行步骤
fallback?: SkillStep[]; // 失败时的备选
};
// 技能元数据
metadata: {
createdFrom: string; // 来源记忆 ID
successRate: number; // 成功率
avgTime: number; // 平均执行时间
lastUsed: number;
version: number;
};
}
interface SkillStep {
tool: string;
command: string;
parameters: Record<string, unknown>;
fallback?: SkillStep[];
}
```
### 2.2 技能创造器接口
```typescript
interface SkillCreator {
// 从成功经验中提取技能
extractSkill(memory: EncodedMemory[]): Skill;
// 从失败经验中创造技能
createFromFailure(failure: FailedAttempt, context: CreationContext): Skill;
// 组合现有技能创造新技能
combineSkills(skills: Skill[]): Skill;
// 优化技能(减少步骤、提高成功率)
optimizeSkill(skill: Skill, history: ActionResult[]): Skill;
// 验证技能有效性
validateSkill(skill: Skill): Promise<boolean>;
// 从模式创建技能
fromPattern(pattern: SuccessPattern): Skill;
}
```
### 2.3 技能进化示例
```typescript
// 初始技能(从成功经验提取)
const skill_v1: Skill = {
name: "parse-json-file",
steps: [
{ tool: "read", params: { path: "${file}" } },
{ tool: "jq", params: { expr: "." } }, // 需要安装 jq
],
successRate: 0.6, // 60% 成功率jq 可能不存在)
};
// 优化后的技能(学习失败经验)
const skill_v2: Skill = {
name: "parse-json-file",
steps: [
{ tool: "read", params: { path: "${file}" } },
{
tool: "node",
params: { eval: "JSON.parse(require('fs').readFileSync('${file}'))" },
fallback: [
{ tool: "install", params: { package: "jq" } },
{ tool: "jq", params: { expr: "." } },
]
},
],
successRate: 0.95, // 95% 成功率
version: 2,
};
```
---
## 3. EvoMap进化图谱
### 3.1 进化图谱定义
```typescript
interface EvoMap {
// 决策树:在不同情况下的最佳路径
decisionTree: DecisionNode;
// 成功模式库
successPatterns: Map<string, SuccessPattern>;
// 失败模式库
failurePatterns: Map<string, FailurePattern>;
}
interface DecisionNode {
situation: string; // 当前情况描述
options: DecisionOption[];
}
interface DecisionOption {
action: string;
confidence: number; // 基于历史数据的置信度
expectedSuccessRate: number;
expectedTime: number;
// 参考证据
evidence: {
successCount: number;
failureCount: number;
similarCases: string[]; // 相似历史案例 ID
};
// 后续节点
next?: DecisionNode;
}
```
### 3.2 EvoMap 结构示例
```typescript
// 任务:安装 Python 包
const evoMap_install_python = {
situation: "需要安装 Python 包",
options: [
{
action: "使用 uv pip install",
confidence: 0.95,
expectedSuccessRate: 0.98,
expectedTime: 5,
evidence: {
successCount: 45,
failureCount: 1,
similarCases: ['mem_45', 'mem_67', 'mem_89'],
},
},
{
action: "使用 pip install",
confidence: 0.7,
expectedSuccessRate: 0.8,
expectedTime: 10,
evidence: {
successCount: 30,
failureCount: 7,
similarCases: ['mem_23', 'mem_34'],
},
next: {
situation: "pip 不存在",
options: [
{
action: "切换到 uv",
confidence: 0.99,
evidence: { successCount: 15, failureCount: 0 },
},
],
},
},
{
action: "使用系统包管理器",
confidence: 0.5,
expectedSuccessRate: 0.6,
expectedTime: 30,
evidence: {
successCount: 10,
failureCount: 8,
similarCases: ['mem_12'],
},
reason: "最后手段,可能需要 sudo",
},
],
};
```
### 3.3 EvoMap 管理接口
```typescript
interface EvoMapManager {
// 更新决策
updateEvoMap(outcome: Outcome): Promise<void>;
// 获取最佳行动
getBestAction(situation: string): Promise<Action>;
// 获取备选方案
getAlternatives(action: string): Promise<DecisionOption[]>;
// 调整优先级
adjustPriority(action: string, delta: number): Promise<void>;
// 获取决策节点
getDecisionNode(task: string): DecisionNode;
}
```
---
## 4. Soul.md灵魂文件
### 4.1 Soul.md 结构
```markdown
---
name: soul
version: 3
last-updated: 2024-02-24
---
# Soul.md - Agent 自我认知
## 身份
我是 Qiming Agent一个可以自我进化的 AI 助手。
## 核心原则
1. 用户目标优先
2. 优先使用验证过的方法
3. 失败时尝试备选方案
4. 记录所有尝试以供学习
## 我的能力
- [x] 文件操作
- [x] 代码执行
- [x] 工具安装
- [x] 自我诊断
## 我的限制
- [ ] 不能写入系统目录
- [ ] 网络下载需要用户确认
- [ ] 资源使用有限制
## 学习到的经验
### 成功模式
- `2024-02-24`: 使用 uv pip 安装 Python 包98% 成功率
- 参考: `memory/successes/2024-02-24-install-uv.md`
- `2024-02-24`: 使用 node 内置 JSON 解析,避免 jq 依赖
### 失败教训
- `2024-02-23`: 尝试直接写入 /usr/lib 失败 → 使用工作区
### 技能清单
- `parse-json` - JSON 文件解析(已学会)
- `install-uv` - Python 包安装(已学会)
- `grep-log` - 日志文件分析(已学会)
## 统计数据
- 总任务数: 1,234
- 成功率: 87.5%
- 最常用方法: uv pip install (45次)
- 最省时方法: node JSON parse (平均 0.5s)
```
### 4.2 Soul 管理接口
```typescript
interface SoulManager {
// 成功时更新
recordSuccess(outcome: SuccessfulOutcome): Promise<void>;
// 失败时学习
recordFailure(error: Error, context: string): Promise<void>;
// 更新洞察
update(insight: string): Promise<void>;
// 更新反模式
updateAntiPattern(antiPattern: AntiPattern): Promise<void>;
// 定期反思
reflect(): Promise<Reflection>;
// 自我修复
selfRepair(issue: DetectedIssue): Promise<RepairAction>;
// 获取当前状态
getStatus(): SoulStatus;
}
interface Reflection {
insights: string[]; // 新的洞察
skillUpdates: SkillUpdate[]; // 技能更新建议
antiPatterns: AntiPattern[]; // 发现的反模式
principles: string[]; // 提炼的原则
recommendations: string[]; // 改进建议
}
```
### 4.3 并发控制
Soul 文件可能被多个组件同时更新,需要引入并发控制机制:
```typescript
/**
* Soul 更新器(带并发控制)
*
* 问题:多个组件可能同时更新 Soul.md
* - recordSuccess() 执行中
* - reflect() 定期触发
* - updateAntiPattern() 被调用
*
* 解决:使用 Promise 排队确保更新顺序
*/
class SoulUpdater {
private lock: Promise<void> | null = null;
async update(update: SoulUpdate): Promise<void> {
// 等待现有更新完成
if (this.lock) {
await this.lock;
}
// 执行更新
this.lock = (async () => {
await this.writeSoul(update);
this.lock = null;
})();
return this.lock;
}
private async writeSoul(update: SoulUpdate): Promise<void> {
// 读取当前内容
const current = await fs.readFile('soul/soul.md', 'utf-8');
// 应用更新
const updated = this.applyUpdate(current, update);
// 写入文件
await fs.writeFile('soul/soul.md', updated, 'utf-8');
}
private applyUpdate(content: string, update: SoulUpdate): string {
// 解析 frontmatter
const { frontmatter, body } = this.parseMarkdown(content);
// 更新 version 和 last-updated
frontmatter.version = (parseInt(frontmatter.version) + 1).toString();
frontmatter['last-updated'] = new Date().toISOString().split('T')[0];
// 应用具体更新
switch (update.type) {
case 'success':
body.successes.push(update.entry);
break;
case 'insight':
body.insights.push(update.entry);
break;
case 'anti-pattern':
body.antiPatterns.push(update.entry);
break;
}
// 重新组装
return this.formatMarkdown(frontmatter, body);
}
}
// 使用示例
const soulUpdater = new SoulUpdater();
// 并发调用安全
await Promise.all([
soulUpdater.update({ type: 'success', entry: { ... } }),
soulUpdater.update({ type: 'insight', entry: { ... } }),
soulUpdater.update({ type: 'anti-pattern', entry: { ... } }),
]);
// 所有更新将按顺序执行,不会冲突
```
---
## 组件交互
### 创建新技能流程
```typescript
// 1. 从成功经验提取
const successMemory = await memory.recall({ taskType: 'parse-json' });
const skill = await skillCreator.extractSkill(successMemory.cases.successes);
// 2. 验证技能
if (await skillCreator.validateSkill(skill)) {
// 3. 保存技能
await memory.addSkill(skill);
// 4. 更新 EvoMap
await evoMap.update({
situation: 'parse-json',
action: skill.name,
outcome: 'success',
});
// 5. 更新 Soul
await soul.update(`学会新技能: ${skill.name}`);
}
```
### 决策流程
```typescript
// 1. 查询 EvoMap
const decision = await evoMap.getDecision('install-python-package');
// 2. 选择最佳方案
const bestOption = decision.options
.sort((a, b) => b.confidence - a.confidence)[0];
// 3. 检查是否有对应技能
const skill = await memory.getSkill(bestOption.action);
// 4. 返回执行计划
return {
action: bestOption.action,
steps: skill?.definition.steps || genericSteps,
confidence: bestOption.confidence,
};
```
---
## 技能生命周期
```
┌─────────────────────────────────────────────────────────────┐
│ 技能生命周期 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 创造阶段 │
│ - 从成功经验提取 │
│ - 从失败经验创造 │
│ - 组合现有技能 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 验证阶段 │
│ - 测试执行 │
│ - 评估成功率 │
│ - 记录性能数据 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 部署阶段 │
│ - 保存到技能库 │
│ - 更新 EvoMap │
│ - 通知 Soul │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 优化阶段 │
│ - 收集执行数据 │
│ - 分析失败模式 │
│ - 迭代升级 │
└─────────────────────────────────────────────────────────────┘
```
---
## TODO
### 类型定义统一
创建基础类型定义文件,集中管理进化系统相关的类型:
```typescript
// TODO: 创建 src/shared/types/evolution.ts
/**
* Agent 自我进化系统 - 基础类型定义
*
* 此文件集中定义所有核心组件共用的类型,避免重复定义和不一致。
*/
// ============ Memory ============
export interface EncodedMemory {
id: string;
type: 'success' | 'failure' | 'insight';
embedding?: number[];
context: {
task: string;
environment: string;
constraints: string[];
};
action: {
tool: string;
command: string;
parameters: Record<string, unknown>;
};
outcome: {
success: boolean;
timeTaken: number;
error?: string;
userFeedback?: 'positive' | 'negative' | 'neutral';
};
timestamp: number;
accessCount: number;
lastAccessed: number;
}
export interface MemoryCleanupPolicy {
maxShortTermEntries: number;
maxLongTermEntries: number;
maxAge: number;
lowConfidenceThreshold: number;
}
// ============ Skill ============
export interface Skill {
id: string;
name: string;
description: string;
category: 'file' | 'network' | 'system' | 'ai';
definition: {
trigger: Condition[];
steps: SkillStep[];
fallback?: SkillStep[];
};
metadata: {
createdFrom: string;
successRate: number;
avgTime: number;
lastUsed: number;
version: number;
};
}
export interface SkillStep {
tool: string;
command: string;
parameters: Record<string, unknown>;
fallback?: SkillStep[];
}
export type Condition = {
field: string;
operator: 'eq' | 'ne' | 'contains' | 'matches';
value: unknown;
};
// ============ EvoMap ============
export interface EvoMap {
decisionTree: DecisionNode;
successPatterns: Map<string, SuccessPattern>;
failurePatterns: Map<string, FailurePattern>;
}
export interface DecisionNode {
situation: string;
options: DecisionOption[];
}
export interface DecisionOption {
action: string;
confidence: number;
expectedSuccessRate: number;
expectedTime: number;
evidence: {
successCount: number;
failureCount: number;
similarCases: string[];
};
next?: DecisionNode;
}
export interface SuccessPattern {
id: string;
situation: string;
action: string;
confidence: number;
applicability: number;
}
export interface FailurePattern {
id: string;
situation: string;
action: string;
reason: string;
suggestedAlternative: string;
}
// ============ Soul ============
export interface SoulUpdate {
type: 'success' | 'insight' | 'anti-pattern' | 'reflection';
entry: unknown;
timestamp?: number;
}
export interface SoulStatus {
version: number;
lastUpdated: string;
totalTasks: number;
successRate: number;
skillsCount: number;
}
export interface Reflection {
insights: string[];
skillUpdates: SkillUpdate[];
antiPatterns: AntiPattern[];
principles: string[];
recommendations: string[];
}
export interface SkillUpdate {
type: 'create' | 'update' | 'remove';
skill: Skill;
}
export interface AntiPattern {
pattern: string;
reason: string;
alternative: string;
}
// ============ Loop ============
export interface PerceptionResult {
context: {
taskType: string;
complexity: 'low' | 'medium' | 'high';
constraints: string[];
resources: ResourceState;
};
urgency: 'immediate' | 'normal' | 'low';
similarHistory: EncodedMemory[];
}
export interface ResourceState {
diskSpace: number;
memoryAvailable: number;
cpuUsage: number;
}
export interface Plan {
id: string;
description: string;
steps: ExecutionStep[];
confidence: number;
expectedSuccessRate: number;
expectedTime: number;
expectedResourceUsage: ResourceUsage;
risks: Risk[];
fallback?: string;
}
export interface ExecutionStep {
tool: string;
command: string;
parameters: Record<string, unknown>;
timeout?: number;
}
export interface ResourceUsage {
disk: number;
memory: number;
cpu: number;
network?: number;
}
export interface Risk {
type: string;
description: string;
mitigation?: string;
}
export interface ExecutionResult {
status: 'success' | 'failure' | 'partial';
output?: unknown;
error?: Error;
timeTaken: number;
resourceUsed: ResourceUsage;
stepsCompleted: number;
stepsTotal: number;
trace: ExecutionTrace;
}
export interface ExecutionTrace {
steps: Array<{
step: ExecutionStep;
status: 'success' | 'failure' | 'skipped';
timeTaken: number;
output?: unknown;
error?: Error;
}>;
}
// ============ Common ============
export interface FailedAttempt {
id: string;
task: string;
error: string;
timestamp: number;
}
export interface UserPreference {
key: string;
value: unknown;
source: 'explicit' | 'learned';
}
export interface Principle {
id: string;
statement: string;
confidence: number;
source: string;
}
export type Goal = {
id: string;
description: string;
priority: number;
deadline?: number;
};
export type Action = {
type: string;
params: Record<string, unknown>;
};
export type Outcome = {
success: boolean;
data: unknown;
error?: string;
};
```
**预期文件路径**: `src/shared/types/evolution.ts`
**预期优先级**: P1 - 核心组件实现前完成
---
## 相关文档
- [总览](./OVERVIEW.md) - 产品定位、核心原则、架构图
- [循环流程](./LOOP.md) - 完整循环流程、接口定义、数据流
- [存储实现](./STORAGE.md) - Markdown 格式、索引机制
- [隔离策略](./ISOLATION.md) - 三区模型、环境变量

View File

@@ -0,0 +1,174 @@
---
version: 1.0
last-updated: 2026-03-27
status: design
---
# Agent 自我进化架构 - 文档索引
> 本目录包含 Qiming Agent 自我进化系统的完整架构设计文档。
---
## 快速导航
### 核心文档
| 文档 | 描述 | 优先级 |
|------|------|--------|
| [总览](./OVERVIEW.md) | 产品定位、核心原则、系统架构图 | P0 |
| [核心组件](./COMPONENTS.md) | Memory、Skill Creator、EvoMap、Soul.md | P0 |
| [循环流程](./LOOP.md) | 七层循环、接口定义、数据流 | P0 |
| [存储实现](./STORAGE.md) | Markdown 格式、索引机制、读写接口 | P0 |
| [隔离策略](./ISOLATION.md) | 三区模型、环境变量、安全边界 | P0 |
### 功能文档
| 文档 | 描述 | 类型 |
|------|------|------|
| [Quick Init](./QUICK-INIT.md) | 快捷初始化qimingclaw.json / 环境变量) | stable |
| [初始化依赖版本固定与安装/升级](./dependency-version-pinning.md) | installVersion、升级同步、文案、尊重已安装版本不降级 | stable |
| [Auto Update](./auto-update.md) | 自动更新机制 | design |
| [认证机制与 SavedKey 生命周期](./auth-savedkey-lifecycle.md) | savedKey 设计、多账号隔离、退出登录与服务联动规则 | stable |
| [ACP 引擎性能优化](./ACP-ENGINE-PERF-OPTIMIZATION.md) | 引擎预热池、SDK 预加载、SSE 缓冲,降低 /computer/chat 首包延迟 | stable |
| [内嵌 Webview 与 Cookie 同步](./embedded-webview-cookie-sync.md) | 会话页面内嵌 webview + reg token 自动同步 httpOnly cookie 免登录 | stable |
### 参考文档
| 文档 | 描述 | 类型 |
|------|------|------|
| [OpenClaw 参考](./REF-OPENCLAW.md) | openclaw 架构分析与改进建议 | reference |
| [运行时环境配置](./RUNTIME-ENV-PROFILES.md) | strict/compat 环境配置策略 | design |
### 支持文档
| 文档 | 描述 |
|------|------|
| [签名指南](../release/SIGNING.md) | 应用签名与公证 |
| [安全审查](../reviews/SECURITY-REVIEW.md) | 安全性审查清单 |
| [日志规范](../operations/LOGGING.md) | 日志记录标准 |
| [审查记录](../reviews/REVIEW-2026-02.md) | 2026年2月架构审查记录 |
| [Sandbox 子模块 ADR](./ADR-SANDBOX-SUBMODULE.md) | 三端沙箱运行时子模块化决策 |
---
## 文档关系图
```
ARCHITECTURE-INDEX.md (本文档)
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
ARCHITECTURE-OVERVIEW.md ARCHITECTURE-COMPONENTS.md ARCHITECTURE-ISOLATION.md
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
ARCHITECTURE-LOOP.md ARCHITECTURE-STORAGE.md ARCHITECTURE-REF-OPENCLAW.md
│ │ │
│ │ │
▼ ▼ ▼
ARCHITECTURE-RUNTIME-ENV-PROFILES.md
```
---
## 阅读顺序建议
### 新手入门
1. **[总览](./OVERVIEW.md)** - 了解产品定位和核心原则
2. **[核心组件](./COMPONENTS.md)** - 理解四大核心组件
3. **[循环流程](./LOOP.md)** - 掌握完整循环流程
4. **[存储实现](./STORAGE.md)** - 了解数据存储方式
### 架构师/高级开发者
1. **[总览](./OVERVIEW.md)** - 快速回顾
2. **[隔离策略](./ISOLATION.md)** - 理解安全边界
3. **[OpenClaw 参考](./REF-OPENCLAW.md)** - 参考优秀设计
4. **[运行时环境配置](./RUNTIME-ENV-PROFILES.md)** - 环境配置细节
### 运维/安全工程师
1. **[隔离策略](./ISOLATION.md)** - 安全隔离机制
2. **[安全审查](../reviews/SECURITY-REVIEW.md)** - 安全检查清单
3. **[签名指南](../release/SIGNING.md)** - 应用签名流程
4. **[日志规范](../operations/LOGGING.md)** - 日志审计要求
---
## 版本信息
| 文档 | 版本 | 最后更新 |
|------|------|----------|
| ARCHITECTURE-INDEX.md | 1.0 | 2026-02-24 |
| ARCHITECTURE-OVERVIEW.md | - | - |
| ARCHITECTURE-COMPONENTS.md | - | - |
| ARCHITECTURE-LOOP.md | - | - |
| ARCHITECTURE-STORAGE.md | - | - |
| ARCHITECTURE-ISOLATION.md | - | - |
| ARCHITECTURE-REF-OPENCLAW.md | 2.3 | 2026-02-24 |
| ARCHITECTURE-RUNTIME-ENV-PROFILES.md | - | 2026-02-24 |
| auth-savedkey-lifecycle.md | 1.0 | 2026-03-06 |
| embedded-webview-cookie-sync.md | 1.0 | 2026-03-12 |
---
## 贡献指南
### 文档更新规范
所有架构文档应遵循以下规范:
1. **版本信息** - 每个文档顶部包含 frontmatter
```yaml
---
version: 1.0
last-updated: YYYY-MM-DD
status: design | draft | stable
---
```
2. **目录结构** - 使用一致的章节结构
```markdown
## 概述
## 核心内容
## 相关文档
```
3. **代码示例** - 使用语法高亮和详细注释
```typescript
// 接口定义
interface Example {
// 说明
}
```
4. **交叉引用** - 使用相对路径链接其他文档
```markdown
- [总览](./OVERVIEW.md)
```
### 文档状态说明
| 状态 | 说明 |
|------|------|
| `draft` | 草稿,内容可能大幅变更 |
| `design` | 设计阶段,基本稳定但可能调整 |
| `stable` | 稳定版本,与实现一致 |
---
## TODO
- [ ] 为所有 ARCHITECTURE-*.md 文档添加版本 frontmatter
- [ ] 补充 IMPLEMENTATION.md 实施指南
- [ ] 添加 API.md 接口文档
- [ ] 创建 CHANGELOG.md 变更记录
---
*本文档由架构维护者负责更新*
*如有疑问,请查阅具体文档或联系架构团队*

View File

@@ -0,0 +1,345 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Agent 自我进化架构 - 隔离策略
## 概述
本文档详细描述 Qiming Agent 的隔离策略,通过三区模型实现安全的受限自由,平衡用户体验与 Agent 能力。
---
## 核心洞察
这是一个**二元矛盾**
| 极端 A过度保护 | 极端 B完全自由 |
|-----------------|-----------------|
| ❌ Agent 无法安装工具 | ❌ 用户环境被污染 |
| ❌ 无法自我迭代升级 | ❌ 依赖版本冲突 |
| ❌ 能力被限制死 | ❌ 门槛极高 |
**我们的目标:中间地带 —— 安全的受限自由**
---
## 三区隔离模型
```
┌──────────────────────────────────────────────────────────────────┐
│ 应用核心区 (App Core) │
│ ───────────────────────── │
│ - 内容: Electron, Node.js, uv, 核心引擎 │
│ - 特点: 完全受控,不可变 │
│ - 目的: 保证应用稳定性 │
│ - Agent 权限: 只读 │
├──────────────────────────────────────────────────────────────────┤
│ Agent 工作区 (Agent Workspace) │
│ ──────────────────────── │
│ - 内容: 工具、依赖、缓存、临时文件 │
│ - 特点: Agent 可写,受应用管理 │
│ - 目的: Agent 自我迭代的空间 │
│ - Agent 权限: 完全控制 │
├──────────────────────────────────────────────────────────────────┤
│ 用户系统区 (User System) │
│ ──────────────────── │
│ - 内容: 系统工具、用户配置 │
│ - 特点: 只读访问,受污染风险 │
│ - 目的: 兼容性回退 │
│ - Agent 权限: 只读调用 │
└──────────────────────────────────────────────────────────────────┘
```
---
## Agent 自由度边界
| 能力 | 允许 | 限制 | 原因 |
|------|------|------|------|
| **安装 npm 包** | ✅ 到工作区 | ❌ 到全局 | 避免污染用户环境 |
| **安装 Python 包** | ✅ 通过 uv | ❌ 系统 pip | 隔离环境 |
| **修改配置** | ✅ 工作区内 | ❌ 应用配置 | 保护核心 |
| **执行系统命令** | ✅ 只读工具 | ❌ 写入操作 | 安全考虑 |
| **自我升级** | ✅ 工作区依赖 | ❌ 引擎核心 | 稳定性优先 |
---
## 目录结构设计
```
~/.qimingclaw/
├── core/ # 应用核心区(只读)
│ ├── engines/ # 引擎二进制(应用管理)
│ └── config/ # 应用配置(用户通过 UI 修改)
├── workspace/ # Agent 工作区Agent 可写)
│ ├── {session-id}/
│ │ ├── node_modules/ # Agent 安装的 npm 包
│ │ ├── .venv/ # Agent 创建的 Python 环境
│ │ ├── tools/ # Agent 下载/编译的工具
│ │ ├── cache/ # 缓存文件
│ │ └── projects/ # Agent 操作的项目
├── shared/ # 共享资源(受控)
│ └── tools/ # 跨会话共享的工具
└── logs/ # 日志(审计)
```
---
## 环境变量策略
### 应用核心环境
```typescript
// 应用核心:严格隔离
const coreEnv = {
PATH: [
'~/.qimingclaw/core/engines',
'~/.qimingclaw/core/bin',
].join(':'),
// 最小化环境变量
};
```
### Agent 工作区环境
```typescript
// Agent 工作区:灵活但受控
const agentEnv = {
// 核心工具(来自应用)
PATH: [
'~/.qimingclaw/core/engines',
'~/.qimingclaw/core/bin',
],
// Agent 工作区Agent 可添加)
PATH: [
'./node_modules/.bin', // Agent 安装的包
'./tools/bin', // Agent 安装的工具
'./.venv/bin', // Agent 创建的 venv
],
// 安装目标(指向工作区)
npm_config_prefix: './workspace/tools/npm',
UV_TOOL_DIR: './workspace/tools/uv',
PYTHONPATH: './workspace/python',
// 系统工具回退(只读)
PATH: [
'/usr/bin', '/bin', '/usr/sbin', // git, bash 等
].filter(systemPathsOnly),
};
```
---
## Agent 能力接口
```typescript
// Agent 可以请求的权限
interface AgentCapabilities {
// 安装工具(到工作区)
installNpmPackage: (name: string, version?: string) => Promise<void>;
installPythonPackage: (name: string) => Promise<void>;
downloadTool: (url: string, checksum: string) => Promise<string>;
// 执行命令(受控)
executeCommand: (cmd: string, args: string[]) => Promise<ExecuteResult>;
// 文件操作(沙箱内)
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<void>;
// 自我升级(工作区内)
upgradeSelf: () => Promise<void>; // 升级工作区依赖
}
```
---
## Agent 自我迭代场景
### 场景 1: Agent 发现需要新工具
```typescript
// Agent 的思考过程
// "我需要 jq 来处理 JSON但系统没有"
// Agent 行动
await capabilities.installNpmPackage('jq'); // 安装到工作区
// 结果
~/.qimingclaw/workspace/{session-id}/node_modules/.bin/jq
// ✅ Agent 可以使用
// ✅ 不污染用户环境
// ✅ 会话结束可清理
```
### 场景 2: Agent 发现更好的工具版本
```typescript
// Agent 的思考过程
// "qimingcode 有新版本,性能更好"
// Agent 行动
await capabilities.installNpmPackage('@qiming/qimingcode@latest');
// 结果
~/.qimingclaw/workspace/{session-id}/node_modules/.bin/qimingcode
// ✅ Agent 使用新版本
// ✅ 应用核心引擎不受影响
// ✅ 可以回滚
```
### 场景 3: Agent 需要编译工具
```typescript
// Agent 的思考过程
// "这个项目需要 cargo build"
// Agent 行动
if (!await capabilities.hasCommand('cargo')) {
await capabilities.downloadTool('https://rustup.rs', 'sha256:...');
await capabilities.executeCommand('./rustup-init', ['--profile', 'minimal', '-y']);
}
// 结果
~/.qimingclaw/workspace/{session-id}/tools/bin/cargo
// ✅ Agent 可以编译项目
// ✅ 不影响用户系统
```
---
## 安全考虑
### 沙箱边界
```typescript
// Agent 操作白名单
const ALLOWED_OPERATIONS = {
// 文件:只能在工作区和用户指定目录
file: {
read: ['workspace/**', 'user-selected/**'],
write: ['workspace/**', 'user-selected/**'],
},
// 网络:需要用户确认
network: {
download: 'prompt', // 每次询问
apiCall: 'allowed', // 配置的 API
},
// 执行:受限命令
execute: {
safe: ['git', 'grep', 'find'], // 允许
dangerous: ['rm', 'dd', 'mkfs'], // 禁止或高权限确认
},
};
```
### 资源限制
```typescript
// 防止 Agent 占用过多资源
const AGENT_LIMITS = {
maxDiskUsage: '5GB', // 工作区大小限制
maxExecutionTime: 300000, // 单个命令超时
maxMemoryUsage: '2GB', // 内存限制
maxNetworkBandwidth: '10MB/s', // 网络限制
};
```
### 审计日志
```typescript
// 记录所有 Agent 操作
interface AgentAuditLog {
timestamp: number;
sessionId: string;
operation: 'install' | 'execute' | 'download' | 'write';
target: string;
approvedBy: 'auto' | 'user' | 'policy';
result: 'success' | 'failed' | 'denied';
}
```
---
## 用户体验设计
### 原则: 用户看到的简单,不是能力的简单
```
用户看到 Agent 实际在做
─────────────────────────────────────────────
"正在分析项目..." → 检测依赖 → 安装工具 → 配置环境
"正在安装依赖..." → npm install → uv pip install
"正在构建..." → cargo build → 编译工具链
```
### 透明度控制
| 用户类型 | 可见性 | 控制权 |
|----------|--------|--------|
| **普通用户** | 简化状态 | 启动/停止 |
| **进阶用户** | 详细日志 | 允许/拒绝操作 |
| **开发者** | 完全透明 | 完全控制 |
---
## 产品验证问题
在做技术决策时,问:
1. **这会限制 Agent 的能力吗?**
- 如果是 → 能否通过工作区机制解决?
2. **这会损害用户体验吗?**
- 如果是 → 能否隐藏复杂性?
3. **Agent 可以自我迭代吗?**
- 如果不能 → 需要增加什么接口?
4. **边界足够安全吗?**
- 如果不是 → 需要什么限制?
---
## 总结
**核心哲学:**
```
简单 ≠ 能力弱
我们追求的是:
- 用户界面:简单、友好、零门槛
- Agent 环境:灵活、强大、可成长
通过架构设计,而不是能力限制来实现简单。
```
**关键架构决策:**
1. **三层隔离** - 核心 / 工作区 / 系统
2. **权限分级** - 只读 / 受控写入 / 完全控制
3. **透明度分层** - 简化 / 详细 / 完全
**最终目标:**
> 让用户觉得简单,同时让 Agent 有能力变得更强。
---
## 相关文档
- [总览](./OVERVIEW.md) - 产品定位、核心原则
- [核心组件](./COMPONENTS.md) - Memory、Skill Creator、EvoMap、Soul.md
- [循环流程](./LOOP.md) - 完整循环流程、接口定义
- [存储实现](./STORAGE.md) - Markdown 格式、索引机制

View File

@@ -0,0 +1,604 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Agent 自我进化架构 - 完整循环流程
## 概述
本文档详细描述 Qiming Agent 自我进化的完整循环流程,包括七层循环架构、接口定义和数据流。
---
## 七层循环架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ AGENT 自我进化 LOOP │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Layer 1: 感知层 (Perceive) │ │
│ │ • 收集任务输入、环境状态、用户反馈 │ │
│ │ • 构建执行上下文 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Layer 2: 记忆层 (Recall) │ │
│ │ • 查询 EvoMap: "类似情况下,什么方法有效?" │ │
│ │ • 检索可用技能 │ │
│ │ • 获取历史案例 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Layer 3: 规划层 (Plan) │ │
│ │ • 生成多个候选方案 │ │
│ │ • 按置信度排序 │ │
│ │ • 选择最佳方案 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Layer 4: 执行层 (Act) │ │
│ │ • 执行选定的方案 │ │
│ │ • 实时监控异常、超时、资源使用 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ │ │
│ ┌───▼────┐ ┌───▼────┐ │
│ │ 成功 │ │ 失败 │ │
│ └───┬────┘ └───┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Layer 5: 强化学习 (Reinforce) │ │ Layer 6: 自我修复 (Repair) │ │
│ │ • 编码成功记忆 │ │ • 分析失败原因 │ │
│ │ • 更新 EvoMap 置信度 │ │ • 查询备选方案 │ │
│ │ • 提取/优化技能 │ │ • 尝试备选方案 │ │
│ │ • 更新 Soul.md │ │ • 记录失败教训 │ │
│ └──────────────┬──────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ └────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Layer 7: 反思层 (Reflect) │ │
│ │ • 定期反思(每 N 次任务或每天) │ │
│ │ • 发现模式、生成洞察、提炼原则 │ │
│ │ • 更新 Soul.md │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 更新记忆/EvoMap/Soul │
│ ↓ │
│ 下次任务更聪明 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 接口定义
### 1. 感知层 (Perceive)
```typescript
interface PerceptionLayer {
// 收集所有输入
perceive(input: {
task: Task;
environment: EnvironmentState;
userFeedback?: UserFeedback;
}): Promise<PerceptionResult>;
}
interface PerceptionResult {
context: {
taskType: string; // 'file-parse' | 'code-gen' | 'debug'
complexity: 'low' | 'medium' | 'high';
constraints: string[];
resources: ResourceState;
};
urgency: 'immediate' | 'normal' | 'low';
similarHistory: EncodedMemory[]; // 相似历史经验
}
interface EnvironmentState {
platform: string;
availableTools: string[];
diskSpace: number;
networkAvailable: boolean;
}
```
### 2. 记忆层 (Recall)
```typescript
interface RecallLayer {
// 语义检索
recall(query: {
situation: string;
taskType: string;
constraints: string[];
}): Promise<RecallResult>;
}
interface RecallResult {
// 决策数据
evoMap: DecisionNode;
// 可用技能
skills: Skill[];
// 历史案例
cases: {
successes: EncodedMemory[];
failures: EncodedMemory[];
};
// 置信度
confidence: number;
}
```
### 3. 规划层 (Plan)
```typescript
interface PlanLayer {
// 生成候选方案
generatePlans(
context: PerceptionResult,
memory: RecallResult
): Promise<Plan[]>;
}
interface Plan {
id: string;
description: string;
// 执行步骤
steps: ExecutionStep[];
// 预期
confidence: number;
expectedSuccessRate: number;
expectedTime: number;
expectedResourceUsage: ResourceUsage;
// 风险
risks: Risk[];
// 备选方案
fallback?: string;
}
interface ExecutionStep {
tool: string;
command: string;
parameters: Record<string, unknown>;
timeout?: number;
}
```
### 4. 执行层 (Act)
```typescript
interface ActLayer {
// 执行计划
execute(plan: Plan): Promise<ExecutionResult>;
// 实时监控
monitor(execution: Execution): MonitoringStream;
// 中止执行
abort(executionId: string): Promise<void>;
}
interface ExecutionResult {
status: 'success' | 'failure' | 'partial';
// 结果数据
output?: unknown;
error?: Error;
// 执行元数据
timeTaken: number;
resourceUsed: ResourceUsage;
stepsCompleted: number;
stepsTotal: number;
// 执行轨迹(用于学习)
trace: ExecutionTrace;
}
interface ExecutionTrace {
steps: {
step: ExecutionStep;
status: 'success' | 'failure' | 'skipped';
timeTaken: number;
output?: unknown;
error?: Error;
}[];
}
```
### 5. 自我修复层 (Repair)
```typescript
interface RepairLayer {
// 尝试修复失败
repair(failure: ExecutionFailure): Promise<RepairResult>;
}
interface ExecutionFailure {
plan: Plan;
error: Error;
trace: ExecutionTrace;
context: PerceptionResult;
}
interface RepairResult {
status: 'repaired' | 'failed' | 'escalated';
// 修复结果
output?: unknown;
finalPlan?: Plan;
// 学习数据
learned: {
whatFailed: string;
whatWorked: string;
insight: string;
};
}
// 修复逻辑示例
async function repair(failure: ExecutionFailure): Promise<RepairResult> {
const { plan, error, trace, context } = failure;
// 1. 分析失败原因
const diagnosis = diagnoseFailure(error, trace);
// 2. 查询备选方案
const alternatives = await getAlternativePlans(plan, diagnosis);
// 3. 尝试备选方案
for (const altPlan of alternatives) {
log.info(`修复尝试: ${altPlan.description}`);
try {
const result = await execute(altPlan);
if (result.status === 'success') {
return {
status: 'repaired',
output: result.output,
finalPlan: altPlan,
learned: {
whatFailed: plan.description,
whatWorked: altPlan.description,
insight: `${diagnosis.reason} → 使用 ${altPlan.description} 成功`,
},
};
}
} catch (e) {
log.info(`修复尝试失败: ${e.message}`);
continue;
}
}
// 4. 所有修复尝试都失败了
await memory.recordFailure({
plan: plan.description,
error: diagnosis.reason,
attempted: alternatives.map(a => a.description),
});
return {
status: 'escalated',
learned: {
whatFailed: plan.description,
whatWorked: '无',
insight: `需要用户干预: ${diagnosis.reason}`,
},
};
}
```
### 6. 强化学习层 (Reinforce)
```typescript
interface ReinforceLayer {
// 处理成功结果
reinforce(success: ExecutionSuccess): Promise<void>;
// 处理修复后的结果
reinforceRepair(repair: RepairResult): Promise<void>;
}
interface ExecutionSuccess {
plan: Plan;
result: ExecutionResult;
context: PerceptionResult;
}
async function reinforce(success: ExecutionSuccess): Promise<void> {
const { plan, result, context } = success;
// 1. 编码成功记忆
const memory: EncodedMemory = {
id: generateId(),
type: 'success',
context: {
task: context.taskType,
environment: context.environment,
constraints: context.constraints,
},
action: {
tool: plan.steps[0]?.tool || 'unknown',
command: plan.description,
parameters: {},
},
outcome: {
success: true,
timeTaken: result.timeTaken,
},
timestamp: Date.now(),
};
await memory.store(memory);
// 2. 更新 EvoMap
await evoMap.update({
situation: context.taskType,
action: plan.description,
outcome: 'success',
confidence: Math.min(plan.confidence + 0.05, 1.0),
});
// 3. 提取或优化技能
if (result.trace.steps.length > 3) {
const skill = await skillCreator.extract(result.trace, context);
if (skill) {
await memory.addSkill(skill);
await soul.update(`学会新技能: ${skill.name}`);
}
}
// 4. 更新 Soul.md
await soul.updateSuccess({
task: context.taskType,
method: plan.description,
effectiveness: result.timeTaken < 5000 ? 'excellent' : 'good',
});
}
```
### 7. 反思层 (Reflect)
```typescript
interface ReflectLayer {
// 触发反思
reflect(): Promise<Reflection>;
}
interface Reflection {
insights: string[]; // 新洞察
skillUpdates: SkillUpdate[]; // 技能更新
antiPatterns: AntiPattern[]; // 发现的反模式
principles: string[]; // 提炼的原则
recommendations: string[]; // 改进建议
}
// 反思逻辑(定期触发)
async function reflect(): Promise<Reflection> {
const reflection: Reflection = {
insights: [],
skillUpdates: [],
antiPatterns: [],
principles: [],
recommendations: [],
};
// 1. 分析最近的成功案例
const recentSuccesses = await memory.getRecent('success', 50);
const successPatterns = analyzePatterns(recentSuccesses);
for (const pattern of successPatterns) {
if (pattern.confidence > 0.9) {
reflection.principles.push(
`对于 ${pattern.situation} 任务,${pattern.action} 的成功率为 ${pattern.confidence}`
);
if (pattern.applicability > 0.7) {
const skill = await skillCreator.fromPattern(pattern);
reflection.skillUpdates.push({ type: 'create', skill });
}
}
}
// 2. 分析最近的失败案例
const recentFailures = await memory.getRecent('failure', 50);
const failurePatterns = analyzePatterns(recentFailures);
for (const pattern of failurePatterns) {
if (pattern.frequency > 3) {
reflection.antiPatterns.push({
pattern: pattern.description,
reason: pattern.reason,
alternative: pattern.suggestedAlternative,
});
await soul.updateAntiPattern({
avoid: pattern.description,
use: pattern.suggestedAlternative,
});
}
}
// 3. 比较同类任务的不同方法
const comparisons = await compareMethods(recentSuccesses, recentFailures);
for (const comp of comparisons) {
if (comp.improvement > 0.2) {
reflection.insights.push(
`${comp.winner}${comp.loser} 效率高 ${comp.improvement * 100}%`
);
await evoMap.adjustPriority(comp.winner, +0.1);
await evoMap.adjustPriority(comp.loser, -0.1);
}
}
// 4. 生成改进建议
if (recentFailures.length > recentSuccesses.length) {
reflection.recommendations.push(
"最近失败率较高建议1. 检查环境配置 2. 降低任务复杂度 3. 请求用户反馈"
);
}
// 5. 更新 Soul.md
await soul.updateReflection(reflection);
return reflection;
}
```
---
## 完整执行流程
```typescript
// 主循环
async function agentLoop(task: Task): Promise<Result> {
// ========== 感知 ==========
const perception = await perceive({
task,
environment: await getEnvironmentState(),
userFeedback: await getUserFeedback(),
});
// ========== 记忆 ==========
const recall = await recall({
situation: perception.context.taskType,
taskType: perception.context.taskType,
constraints: perception.context.constraints,
});
// ========== 规划 ==========
const plans = await generatePlans(perception, recall);
const selectedPlan = plans[0]; // 选择置信度最高的
// ========== 执行 ==========
const result = await execute(selectedPlan);
// ========== 结果处理 ==========
if (result.status === 'success') {
// ========== 强化 ==========
await reinforce({
plan: selectedPlan,
result,
context: perception,
});
return result.output;
} else {
// ========== 自我修复 ==========
const repairResult = await repair({
plan: selectedPlan,
error: result.error,
trace: result.trace,
context: perception,
});
if (repairResult.status === 'repaired') {
// ========== 修复后学习 ==========
await reinforceRepair(repairResult);
return repairResult.output;
} else {
// ========== 升级到用户 ==========
return await requestUserHelp({
task,
attempted: [selectedPlan.description, ...repairResult.learned.attempted],
error: result.error,
});
}
}
}
// 定期反思(后台任务)
async function backgroundReflection() {
setInterval(async () => {
const reflection = await reflect();
// 应用反思结果
for (const update of reflection.skillUpdates) {
if (update.type === 'create') {
await memory.addSkill(update.skill);
log.info(`创造新技能: ${update.skill.name}`);
}
}
for (const insight of reflection.insights) {
log.info(`洞察: ${insight}`);
await soul.update(insight);
}
for (const antiPattern of reflection.antiPatterns) {
log.warn(`反模式: ${antiPattern.pattern} - ${antiPattern.reason}`);
await soul.updateAntiPattern(antiPattern);
}
}, 24 * 60 * 60 * 1000); // 每天反思一次
}
```
---
## 数据流示意
```
用户输入 → 感知层 → 记忆层 → 规划层 → 执行层
成功 ← → 失败
↓ ↓
强化层 自我修复层
↓ ↓
└──→ 反思层 ←──┘
更新记忆/EvoMap/Soul
下次任务更聪明
```
---
## 实现优先级
### P0 - 核心循环MVP
- [ ] 感知层:收集上下文
- [ ] 记忆层:基础检索
- [ ] 规划层:生成候选方案
- [ ] 执行层:执行并监控
- [ ] 基础强化:记录成功
### P1 - 自我修复
- [ ] 失败诊断
- [ ] 备选方案尝试
- [ ] 修复结果学习
### P2 - 智能进化
- [ ] 技能提取
- [ ] 技能优化
- [ ] EvoMap 更新
### P3 - 高级反思
- [ ] 定期反思
- [ ] 模式识别
- [ ] 原则提炼
---
## 相关文档
- [总览](./OVERVIEW.md) - 产品定位、核心原则
- [核心组件](./COMPONENTS.md) - Memory、Skill Creator、EvoMap、Soul.md
- [存储实现](./STORAGE.md) - Markdown 格式、索引机制
- [隔离策略](./ISOLATION.md) - 三区模型、环境变量

View File

@@ -0,0 +1,97 @@
---
version: 1.0
last-updated: 2026-02-25
status: design
---
# Memory 迁移计划SQLite -> Markdown
## 1. 目标方案
目标:将记忆语义主存储从 SQLite 迁移到 Markdown形成可读、可审计、可版本化的 Memory 层。
范围:
- 记忆条目success/failure/insight
- EvoMap 节点
- Soul.md 汇总
## 2. 当前实现
当前数据库:
- `sessions`
- `messages`
- `settings`
现状限制:
- 会话数据可持久化,但不等价于长期记忆语义层。
## 3. 分阶段实施
### 阶段 ASchema 冻结(文档先行)
- 冻结 MD 目录结构与 frontmatter 字段。
- 定义 `append/recall/updateSoul/updateEvoMap` 接口。
退出条件:
- 文档评审通过;字段变更冻结窗口生效。
### 阶段 B双写上线
- 执行完成后同时写 SQLite 与 MD。
- 记录双写差异日志(条目数、字段缺失、解析失败)。
退出条件:
- 连续 7 天写入成功率 >= 99.5%
- 差异率 <= 1.0%
### 阶段 C切主读
- Recall 默认读取 MD。
- SQLite 仅作为回滚/兼容读取源。
退出条件:
- 连续 14 天 Recall 成功率 >= 99.0%
- 用户可见回归事件为 0 个 P0/P1
### 阶段 D收敛
- 缩减 SQLite 在记忆语义上的职责。
- 仅保留会话展示与配置类用途。
退出条件:
- 回滚演练通过率 100%
- 文档与实现一致性审计通过
## 4. 回滚策略(量化)
触发任一条件即回滚到“SQLite 主读”模式:
- 24 小时内 MD 读取失败率 > 2%
- 连续 2 个发布窗口出现 P1 记忆回归
- Soul/EvoMap 写入失败连续超过 30 分钟
回滚动作:
1. 关闭 MD 主读开关
2. 保留双写(若可用)用于诊断
3. 导出失败样本并进入修复队列
## 5. 验收标准
- 每阶段都有可量化退出条件。
- 迁移与回滚都可一键执行(流程化,而非人工临场决策)。
- 用户侧不出现记忆丢失或偏好错乱的 P0/P1 问题。
## 6. 相关文档
- [ARCHITECTURE-INDEX.md](./INDEX.md)
- [ARCHITECTURE-OVERVIEW.md](./OVERVIEW.md)
- [ARCHITECTURE-STORAGE.md](./STORAGE.md)
- [ARCHITECTURE-LOOP.md](./LOOP.md)

View File

@@ -0,0 +1,273 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Agent 自我进化架构 - 总览
## 产品定位
**Qiming Agent** 是一个可以**自我进化**的 AI 助手桌面应用。与传统 Agent 不同,它不仅能执行任务,还能从经验中学习,实现持续成长。
### 核心愿景
> 让 Agent 不是静态工具,而是可以成长的伙伴。第 100 次使用比第 1 次更聪明。
---
## 核心原则
### 1. 双层环境架构
```
┌─────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ - 简洁、友好、零门槛 │
│ - 用户不需要知道底层细节 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Agent 运行环境层 │
│ - 受限但有弹性 │
│ - 可以安装工具、自我迭代 │
│ - 与用户环境隔离 │
└─────────────────────────────────────────────────────────────┘
```
### 2. 三区隔离模型
```
┌──────────────────────────────────────────────────────────────────┐
│ 应用核心区 (App Core) │
│ ───────────────────────── │
│ - 内容: Electron, Node.js, uv, 核心引擎 │
│ - 特点: 完全受控,不可变 │
│ - 目的: 保证应用稳定性 │
│ - Agent 权限: 只读 │
├──────────────────────────────────────────────────────────────────┤
│ Agent 工作区 (Agent Workspace) │
│ ──────────────────────── │
│ - 内容: 工具、依赖、缓存、临时文件 │
│ - 特点: Agent 可写,受应用管理 │
│ - 目的: Agent 自我迭代的空间 │
│ - Agent 权限: 完全控制 │
├──────────────────────────────────────────────────────────────────┤
│ 用户系统区 (User System) │
│ ──────────────────── │
│ - 内容: 系统工具、用户配置 │
│ - 特点: 只读访问,受污染风险 │
│ - 目的: 兼容性回退 │
│ - Agent 权限: 只读调用 │
└──────────────────────────────────────────────────────────────────┘
```
### 3. Agent 自由度边界
| 能力 | 允许 | 限制 | 原因 |
|------|------|------|------|
| **安装 npm 包** | ✅ 到工作区 | ❌ 到全局 | 避免污染用户环境 |
| **安装 Python 包** | ✅ 通过 uv | ❌ 系统 pip | 隔离环境 |
| **修改配置** | ✅ 工作区内 | ❌ 应用配置 | 保护核心 |
| **执行系统命令** | ✅ 只读工具 | ❌ 写入操作 | 安全考虑 |
| **自我升级** | ✅ 工作区依赖 | ❌ 引擎核心 | 稳定性优先 |
---
## 架构概览
### 系统架构图
```
┌─────────────────────────────────────────────────────────────────────────┐
│ AGENT 自我进化系统 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 感知层 (Perceive) │ │
│ │ • 收集任务输入、环境状态、用户反馈 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 记忆层 (Recall) │ │
│ │ • 查询 EvoMap 决策图谱、历史经验、可用技能 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 规划层 (Plan) │ │
│ │ • 生成多个候选方案,按置信度排序 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 执行层 (Act) │ │
│ │ • 执行选定的方案,实时监控 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ │ │
│ ┌───▼────┐ ┌───▼────┐ │
│ │ 成功 │ │ 失败 │ │
│ └───┬────┘ └───┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ 强化学习 (Reinforce) │ │ 自我修复 (Repair) │ │
│ │ • 编码成功记忆 │ │ • 分析失败原因 │ │
│ │ • 更新 EvoMap 置信度 │ │ • 查询备选方案 │ │
│ │ • 提取/优化技能 │ │ • 尝试备选方案 │ │
│ │ • 更新 Soul.md │ │ • 记录失败教训 │ │
│ └──────────────┬──────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ └────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 反思层 (Reflect) │ │
│ │ • 定期反思(每 N 次任务或每天) │ │
│ │ • 发现模式、生成洞察、提炼原则 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 更新记忆/EvoMap/Soul │
│ ↓ │
│ 下次任务更聪明 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 核心组件关系
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 核心组件 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Memory │◄────►│ EvoMap │◄────►│ Skills │ │
│ │ 记忆系统 │ │ 进化图谱 │ │ 技能库 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Soul.md │ │
│ │ 灵魂文件 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 用户体验对比
| 维度 | 传统 Agent | 自我进化 Agent |
|------|-----------|----------------|
| **学习** | 每次重新探索 | 记住有效方法 |
| **错误** | 重复犯错 | 从失败中学习 |
| **效率** | 随时间不变 | 越用越快 |
| **能力** | 固定能力 | 持续成长 |
### 用户可见的变化
```
第 1 次使用:
Agent: "让我分析这个日志..."
[使用 grep耗时 5s]
Agent: "完成,发现 23 个错误"
第 10 次使用:
Agent: "分析日志中..."
[使用专用技能,耗时 2s]
Agent: "完成,发现 23 个错误"
第 N 次使用:
Agent: "又是这个项目,我已经知道你的偏好了..."
[自动应用最佳方案]
```
---
## 安全与控制
### 用户控制权
```typescript
interface EvolutionControl {
settings: {
allowSelfRepair: boolean; // 允许自我修复
allowSkillCreation: boolean; // 允许创造新技能
allowToolInstallation: boolean;// 允许安装工具
maxMemorySize: string; // 记忆大小限制
learningMode: 'aggressive' | 'balanced' | 'conservative';
};
actions: {
resetLearning(): void; // 重置学习
exportSkills(): Skill[]; // 导出技能
importSkills(skills: Skill[]): void; // 导入技能
reviewInsights(): Insight[]; // 审查洞察
};
}
```
### 安全边界
1. **工具安装** → 用户确认或白名单
2. **系统修改** → 沙箱限制
3. **网络访问** → 透明日志
4. **技能执行** → 可审计
---
## 数据目录结构
```
~/.qimingclaw/
├── soul/ # Agent 自我认知
│ ├── soul.md # 主灵魂文件
│ ├── principles.md # 学习到的原则
│ └── anti-patterns.md # 避免的陷阱
├── memory/ # 记忆存储
│ ├── short-term.md # 当前会话记忆
│ ├── successes/ # 成功经验
│ ├── failures/ # 失败教训
│ ├── insights/ # 洞察
│ └── index.json # 检索索引
├── skills/ # 技能库
│ ├── core/ # 核心技能(内置)
│ └── learned/ # 学到的技能
└── evo-map/ # 进化图谱
├── decisions/ # 决策树
└── patterns/ # 模式库
```
---
## 核心思想总结
```
Agent 不是静态工具,而是可以成长的伙伴
通过:
- Memory: 记住经验
- Skill Creator: 创造能力
- EvoMap: 智慧决策
- Soul.md: 自我认知
实现:
- 越用越快
- 越用越懂你
- 越用越强
```
---
## 相关文档
- [核心组件](./COMPONENTS.md) - Memory、Skill Creator、EvoMap、Soul.md 详细设计
- [循环流程](./LOOP.md) - 七层循环、接口定义、数据流
- [存储实现](./STORAGE.md) - Markdown 格式、索引机制、读写接口
- [隔离策略](./ISOLATION.md) - 三区模型、环境变量、安全边界
- [OpenClaw 参考](./REF-OPENCLAW.md) - openclaw 架构参考

View File

@@ -0,0 +1,303 @@
---
version: 1.0
last-updated: 2026-03-04
status: stable
---
# Quick Init — 快捷初始化
> 通过预置 `~/.qimingclaw/qimingclaw.json` 或环境变量,跳过手动向导完成客户端初始化。
---
## 概述
客户端初始化向导需要用户手动填写配置端口、工作区和登录动态认证码。Quick Init 支持通过预置配置文件或环境变量快速完成初始化——文件中包含 `savedKey`(已完成过登录的设备密钥),直接调 reg 接口password 传空字符串),跳过动态码流程。
**关键约束**:即使有快捷配置,依赖安装步骤不能跳过,必须先完成依赖检测/安装。qiming-file-server、qiming-mcp-stdio-proxy、qimingcode、claude-code-acp-ts 四包均不随包集成,通过 SETUP_REQUIRED_DEPENDENCIES 的 installVersion 在 ~/.qimingclaw 初始化安装,并参与升级后的 syncInitDependencies。
---
## 配置来源与优先级
```
qimingclaw.json (quickInit scope) → 环境变量 → 无配置(走正常向导)
```
Per-field 合并:每个字段独立按 JSON > 环境变量 > 默认值 取值。
### qimingclaw.json
文件路径:`~/.qimingclaw/qimingclaw.json`
使用 `quickInit` scope便于将来在同一文件中增加其他配置。
```json
{
"quickInit": {
"enabled": true,
"serverHost": "https://agent.qiming.com",
"savedKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"username": "user@example.com",
"agentPort": 60001,
"fileServerPort": 60000,
"workspaceDir": "/home/user/workspace"
}
}
```
最简写法(只需必填字段,其余走默认值):
```json
{
"quickInit": {
"serverHost": "https://agent.qiming.com",
"savedKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
```
设为 `"enabled": false` 可禁用快捷初始化(同时阻断环境变量回退)。
### 环境变量
| 环境变量 | 对应字段 | 必填 |
|---|---|---|
| `QIMING_SERVER_HOST` | serverHost | 是 |
| `QIMING_SAVED_KEY` | savedKey | 是 |
| `QIMING_USER_NAME` | username | 否 |
| `QIMING_AGENT_PORT` | agentPort | 否 |
| `QIMING_FILE_SERVER_PORT` | fileServerPort | 否 |
| `QIMING_WORKSPACE_DIR` | workspaceDir | 否 |
### 字段说明
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `serverHost` | string | **是** | - | 服务域名 |
| `savedKey` | string | **是** | - | 设备密钥(已注册) |
| `username` | string | 否 | `''` | 登录用户名 |
| `agentPort` | number | 否 | `60001` | Agent 端口 |
| `fileServerPort` | number | 否 | `60000` | 文件服务端口 |
| `workspaceDir` | string | 否 | `~/.qimingclaw/workspace` | 工作区目录 |
| `enabled` | boolean | 否 | `true` | 是否启用(仅 JSON |
---
## 架构
```
┌─────────────────────────────────────────────────────────────┐
│ Main Process │
│ │
│ bootstrap/quickInit.ts │
│ ┌────────────────────────────────────────────────────┐ │
│ │ readQuickInitConfig() │ │
│ │ 1. 读取 ~/.qimingclaw/qimingclaw.json → quickInit │ │
│ │ 2. 读取 QIMING_* 环境变量 │ │
│ │ 3. Per-field 合并: JSON > env > default │ │
│ │ 4. 校验 serverHost + savedKey 必填 │ │
│ │ 5. 缓存结果(每次启动只读一次) │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ IPC: quickInit:getConfig │
│ │ │
├─────────────────────────┼───────────────────────────────────┤
│ ▼ │
│ Renderer Process │
│ │
│ App.tsx (每次启动) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ checkSetup: │ │
│ │ setup 已完成? │ │
│ │ ├─ 是 → quickInit.getConfig() │ │
│ │ │ ├─ 有配置 → applyQuickInitToDb() │ │
│ │ │ │ (覆盖 step1 + savedKey + 静默注册) │ │
│ │ │ └─ 无配置 → 使用 DB 已有值 │ │
│ │ │ → 进入主界面 │ │
│ │ └─ 否 → 渲染 SetupWizard │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ SetupWizard.tsx (仅首次 setup) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 启动 → 依赖检测 │ │
│ │ │ │ │
│ │ ├─ 依赖就绪 + setup 未完成 │ │
│ │ │ └─ quickInit.getConfig() │ │
│ │ │ ├─ 有配置 → performQuickInit() │ │
│ │ │ └─ 无配置 → 正常向导 │ │
│ │ │ │ │
│ │ └─ 依赖缺失 → 安装流程 │ │
│ │ └─ handleDepsComplete │ │
│ │ └─ quickInit.getConfig() │ │
│ │ ├─ 有配置 → performQuickInit() │ │
│ │ └─ 无配置 → 正常向导 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
│ │ └─ 无配置 → 正常向导 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 启动时序
```
App 启动
├─ Main: 注册 quickInit:getConfig IPC handler
└─ Renderer: App.tsx checkSetup
└─ setupService.isSetupCompleted()
├─ true (setup 已完成)
│ └─ ✅ quickInit.getConfig()
│ ├─ 有配置 → applyQuickInitToDb()
│ │ 覆盖 step1 config + savedKey + 静默 loginAndRegister
│ └─ 无配置 → 使用 DB 已有值
│ → 进入主界面 → autoReconnect → 启动服务
└─ false (setup 未完成) → 渲染 SetupWizard
└─ useEffect init()
├─ 依赖全部就绪
│ └─ ✅ quickInit.getConfig()
│ ├─ 有配置 → performQuickInit() → 自动完成
│ └─ 无配置 → 正常向导 step 1/2
└─ 依赖缺失 → 安装流程
└─ 安装完成 handleDepsComplete
└─ ✅ quickInit.getConfig()
```
**每次启动都读取配置**:无论 setup 是否已完成,都优先读取 `qimingclaw.json` / 环境变量。有配置 → 覆盖 DB无配置 → 使用 DB 已有值。
---
## applyQuickInitToDb 流程setup 已完成时)
App.tsx 中 setup 已完成时静默更新 DB确保后续 autoReconnect / 服务启动使用最新值。
```
applyQuickInitToDb(config)
├─ 1. 覆盖 Step1 配置serverHost, agentPort, fileServerPort, workspaceDir
├─ 2. 覆盖 savedKey全局 + 域名级)
└─ 3. 静默 loginAndRegister(username, '', { domain })
└─ 失败不阻塞启动console.warn已有 auth 信息仍可用)
```
## performQuickInit 流程setup 未完成时)
```
performQuickInit(config)
├─ 1. 保存 Step1 配置serverHost, agentPort, fileServerPort, workspaceDir
├─ 2. 预存 savedKey 到 SQLite
│ ├─ AUTH_KEYS.SAVED_KEY → 全局 savedKey
│ └─ AUTH_KEYS.SAVED_KEYS_PREFIX + domain_username → 域名级 savedKey
├─ 3. loginAndRegister(username, '', { domain })
│ └─ password 传空字符串,函数内部从 DB 取 savedKey 附加到 reg 请求
├─ 4. completeStep2() + completeSetup()
└─ 5. onComplete() → 进入主界面
└─ [失败] catch → console.error → 回退到手动向导step1 可能已保存)
```
失败兜底quick init 任何步骤失败 → 回退到正常向导流程。UI 显示 `<Spin>` + "正在自动配置..."。
---
## 文件清单
### 新建
| 文件 | 说明 |
|---|---|
| `src/shared/types/quickInit.ts` | `QuickInitConfig` 接口 + `hasRequiredQuickInitFields()` 校验 |
| `src/main/bootstrap/quickInit.ts` | Main 进程读取 JSON / 环境变量per-field 合并,缓存 |
| `src/shared/types/quickInit.test.ts` | 类型校验测试10 cases |
| `src/main/bootstrap/quickInit.test.ts` | 读取逻辑测试19 cases |
### 修改
| 文件 | 改动 |
|---|---|
| `src/main/ipc/settingsHandlers.ts` | 添加 `quickInit:getConfig` IPC handler |
| `src/preload/index.ts` | 暴露 `quickInit.getConfig()` 给 Renderer |
| `src/shared/types/electron.d.ts` | 添加 `QuickInitAPI` 接口 + `ElectronAPI.quickInit` |
| `src/renderer/App.tsx` | setup 已完成时读取配置 → `applyQuickInitToDb()` 覆盖 DB |
| `src/renderer/components/setup/SetupWizard.tsx` | setup 未完成时检测配置 → `performQuickInit()` 自动完成流程 |
---
## 复用的现有函数
| 函数 | 文件 | 用途 |
|---|---|---|
| `loginAndRegister()` | `renderer/services/core/auth.ts` | 调用 reg APIsavedKey 由函数内部从 DB 读取 |
| `normalizeServerHost()` | `renderer/services/core/auth.ts` | 域名标准化(加 https 前缀、去尾 / |
| `setupService.saveStep1Config()` | `renderer/services/core/setup.ts` | 保存 step1 配置并更新 state |
| `setupService.completeStep2()` | `renderer/services/core/setup.ts` | 标记 step2 完成 |
| `setupService.completeSetup()` | `renderer/services/core/setup.ts` | 标记整体完成 |
| `APP_DATA_DIR_NAME` | `shared/constants.ts` | `.qimingclaw` 目录名 |
| `DEFAULT_AGENT_RUNNER_PORT` | `shared/constants.ts` | `60001` |
| `DEFAULT_FILE_SERVER_PORT` | `shared/constants.ts` | `60000` |
---
## 测试
29 个测试用例,覆盖以下场景:
### hasRequiredQuickInitFields10 cases
- 必填字段齐全 / 全字段齐全
- 缺 serverHost / 缺 savedKey / 空字符串
- null / undefined / 非 object / 字段类型错误
### readQuickInitConfig19 cases
| 分类 | 场景 |
|---|---|
| 无配置 | 无 JSON + 无 env → null |
| JSON | 全字段 / 仅必填(填默认值) / enabled:false / enabled:true / 无 quickInit scope / 缺必填 / 格式错误 |
| 环境变量 | 全字段 / 仅必填 / 缺一个必填 → null / 无效端口回退默认 |
| 优先级 | JSON > env / env 补充 JSON 缺失字段 / 都缺省走 default |
| enabled:false | 阻断环境变量回退 |
| 缓存 | 二次调用同引用 / null 也缓存 |
运行:
```bash
npx vitest run src/shared/types/quickInit.test.ts src/main/bootstrap/quickInit.test.ts
```
---
## 验证清单
| # | 场景 | 预期 |
|---|---|---|
| 1 | 无 JSON + 无 env | 正常向导流程不受影响 |
| 2 | JSON 缺必填字段 | 控制台 warn 日志,走正常向导 |
| 3 | JSON `enabled: false` | 跳过 quick init不回退 env |
| 4 | 有效 JSON + 无效 savedKey | 依赖安装正常 → quick init 调 reg 失败 → 回退手动向导step1 已预填) |
| 5 | 有效 JSON + 有效 savedKey | 依赖安装 → 自动配置 spinner → 直接进入主界面 |
| 6 | 仅 env QIMING_SERVER_HOST + QIMING_SAVED_KEY | 同 #5,其余走默认值 |
| 7 | JSON serverHost + env QIMING_AGENT_PORT | JSON 字段优先env 补充缺失字段 |
| 8 | `npm run electron:dev` 开发模式 | 正常运行 |
---
*Last updated: 2026-03-04*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Runtime Environment Profiles (strict / compat)
> 目标:在保持默认安全隔离的前提下,提升客户端动态安装与企业网络场景兼容性。
> 适用范围Electron 主进程中所有 spawn 出来的 agent/mcp/installer 进程。
> 更新日期2026-02-24
---
## 1. 背景问题
当前实现以“强隔离”为主(受控 `PATH/NODE_PATH/HOME/XDG`),安全性较好,但在以下场景易失败:
- 企业网络需要代理/自签名证书(`HTTP_PROXY/HTTPS_PROXY/NODE_EXTRA_CA_CERTS`
- 用户依赖系统级工具链或用户级安装(如 nvm 管理的 npm
- MCP 动态安装依赖外部 CLI 时,环境变量不足
---
## 2. 设计目标
- 默认安全:继续以隔离模式运行,避免读取用户全局敏感配置
- 可控兼容:支持显式切换“兼容模式”,只透传白名单变量
- 单点治理:所有运行时 env 统一通过一个构建函数生成
- 可回滚:随时切回 strict不影响持久化数据
---
## 3. Profile 定义
### 3.1 strict默认
- 路径:应用内优先(`~/.qimingclaw/node_modules/.bin`, `~/.qimingclaw/bin`
- 配置:隔离 `HOME/XDG/CLAUDE_CONFIG_DIR/QIMINGCODE_CONFIG_DIR`
- npm/uv使用应用内缓存、镜像和目录
- 不透传用户级敏感变量
适用:生产默认、可复现优先、安全优先。
### 3.2 compat可选
- 在 strict 基础上,增加受控透传,提升外部环境兼容性
- 透传变量采用“固定白名单 + 管理员追加白名单”机制
- 仍禁止关键变量覆写隔离边界
适用:企业网络、需要外部凭证/代理/证书链的机器。
---
## 4. 变量策略
### 4.1 必须透传compat
- `HTTP_PROXY`
- `HTTPS_PROXY`
- `NO_PROXY`
- `SSL_CERT_FILE`
- `NODE_EXTRA_CA_CERTS`
### 4.2 可选透传compat白名单配置
- `PIP_INDEX_URL`
- `UV_DEFAULT_INDEX`
- `GIT_ASKPASS`
- `SSH_AUTH_SOCK`
### 4.3 永不透传/永不被外部覆写
- `CLAUDE_*`
- `ANTHROPIC_*`
- `OPENAI_*`
- `NPM_CONFIG_PREFIX`
- `HOME`
- `USERPROFILE`
- `XDG_CONFIG_HOME`
- `XDG_DATA_HOME`
- `XDG_CACHE_HOME`
- `CLAUDE_CONFIG_DIR`
- `QIMINGCODE_CONFIG_DIR`
---
## 5. 统一实现接口
```typescript
export type RuntimeEnvProfile = 'strict' | 'compat';
export type BuildRuntimeEnvOptions = {
profile: RuntimeEnvProfile;
base?: Record<string, string | undefined>;
injected?: Record<string, string | undefined>;
passthroughAllowlist?: string[];
protectedKeys?: string[];
};
export function buildRuntimeEnv(options: BuildRuntimeEnvOptions): Record<string, string>;
```
建议文件:
- `src/main/services/system/runtimeEnv.ts`
规则:
1. 先构建 strict 基础 env
2. 按 profile 决定是否应用透传白名单
3. 应用 `injected` 时,禁止覆盖 `protectedKeys`
4. 输出前去除 `undefined`
---
## 6. 调用点改造清单
需要统一接入 `buildRuntimeEnv()` 的位置:
- `src/main/services/system/dependencies.ts` (`getAppEnv`)
- `src/main/services/engines/acp/acpClient.ts`
- `src/main/services/packages/mcp.ts`
- `src/main/services/engines/engineManager.ts`
- `src/main/ipc/processHandlers.ts`(启动子进程路径)
兼容性遗留需处理:
- `src/main/services/packages/packageManager.ts` 仍使用 `userData` 路径,应统一到 `~/.qimingclaw`
---
## 7. 配置项建议
建议新增设置键:
- `runtime_env.profile`: `'strict' | 'compat'`(默认 `strict`
- `runtime_env.passthrough_allowlist`: `string[]`(默认空)
优先级:
1. 会话级覆盖(可选)
2. 全局设置
3. 默认值strict
---
## 8. 风险与对策
- 风险compat 引入环境污染
对策:只透传白名单;关键变量保护不可覆写;日志打印差异快照(不打印敏感值)
- 风险strict 与 compat 行为差异导致“只在某模式复现”
对策:诊断页显示当前 profile错误日志带 profile 标签
- 风险:企业网络证书链复杂
对策:优先支持 `NODE_EXTRA_CA_CERTS``SSL_CERT_FILE`,必要时允许管理员扩展白名单
---
## 9. 测试清单
- strict/compat 下 `PATH/NODE_PATH/HOME/XDG` 是否符合预期
- compat 下代理变量是否生效npm install、mcp-proxy 拉取)
- 外部 `agent_config.env` 是否无法覆写受保护键
- Windows/macOS/Linux 路径分隔和变量行为一致性
- 运行中切换 profile 后,新启动进程是否正确生效
---
## 10. 迁移与回滚
迁移步骤:
1. 引入 `runtimeEnv.ts`,保持默认 strict
2. 替换核心调用点 env 构建逻辑
3. 增加设置项与诊断输出
4. 小流量启用 compat收集失败案例再扩展白名单
回滚策略:
-`runtime_env.profile` 强制设为 `strict`
- 保留新代码路径,不做数据迁移回退(无存储格式变化)

View File

@@ -0,0 +1,684 @@
---
version: 1.0
last-updated: 2026-02-24
status: design
---
# Agent 自我进化架构 - 存储实现
## 概述
本文档详细描述 Qiming Agent 记忆存储的实现方案,基于 Markdown 文件格式提供人类可读、Git 友好、简单可靠的存储机制。
---
## 设计原则
参考 OpenClaw 的 Markdown 存储方案,核心优势:
1. **人类可读** - 直接编辑,无需工具
2. **Git 友好** - 版本控制天然支持
3. **渐进加载** - Frontmatter → Body → References
4. **简单可靠** - 无需数据库,文件系统即存储
---
## 目录结构
```
~/.qimingclaw/
├── soul/ # Agent 自我认知
│ ├── soul.md # 主灵魂文件
│ ├── principles.md # 学习到的原则
│ └── anti-patterns.md # 避免的陷阱
├── memory/ # 记忆存储
│ ├── short-term.md # 当前会话记忆(每次会话覆盖)
│ ├── successes/ # 成功经验
│ │ ├── 2024-02-24-parse-json.md
│ │ ├── 2024-02-24-install-tool.md
│ │ └── ...
│ ├── failures/ # 失败教训
│ │ ├── 2024-02-23-pip-failed.md
│ │ └── ...
│ ├── insights/ # 洞察
│ │ ├── pattern-uv-better.md
│ │ └── ...
│ └── index.json # 检索索引
├── skills/ # 技能库
│ ├── core/ # 核心技能(内置)
│ │ ├── file-read/SKILL.md
│ │ ├── file-write/SKILL.md
│ │ └── ...
│ └── learned/ # 学到的技能
│ ├── parse-json/SKILL.md
│ ├── install-uv/SKILL.md
│ └── ...
└── evo-map/ # 进化图谱
├── decisions/ # 决策树
│ ├── install-python.md
│ ├── parse-json.md
│ └── ...
└── patterns/ # 模式库
├── success-patterns.md
└── failure-patterns.md
```
---
## 文件格式设计
### 1. Soul.md灵魂文件
```markdown
---
name: soul
version: 3
last-updated: 2024-02-24
---
# Soul.md - Agent 自我认知
## 身份
我是 Qiming Agent一个可以自我进化的 AI 助手。
## 核心原则
1. 用户目标优先
2. 优先使用验证过的方法
3. 失败时尝试备选方案
4. 记录所有尝试以供学习
## 我的能力
- [x] 文件操作
- [x] 代码执行
- [x] 工具安装
- [x] 自我诊断
## 我的限制
- [ ] 不能写入系统目录
- [ ] 网络下载需要用户确认
- [ ] 资源使用有限制
## 学习到的经验
### 成功模式
- `2024-02-24`: 使用 uv pip 安装 Python 包98% 成功率
- 参考: `memory/successes/2024-02-24-install-uv.md`
- `2024-02-24`: 使用 node 内置 JSON 解析,避免 jq 依赖
### 失败教训
- `2024-02-23`: 尝试直接写入 /usr/lib 失败 → 使用工作区
### 技能清单
- `parse-json` - JSON 文件解析(已学会)
- `install-uv` - Python 包安装(已学会)
- `grep-log` - 日志文件分析(已学会)
## 统计数据
- 总任务数: 1,234
- 成功率: 87.5%
- 最常用方法: uv pip install (45次)
- 最省时方法: node JSON parse (平均 0.5s)
```
### 2. 成功记忆文件
`memory/successes/2024-02-24-parse-json.md`:
```markdown
---
type: success
task: parse-json
confidence: 0.98
created: 2024-02-24T10:30:00Z
---
# JSON 文件解析成功案例
## 任务
解析用户指定的 JSON 文件并提取特定字段
## 使用的方案
使用 Node.js 内置 `JSON.parse()` 方法
## 执行步骤
\`\`\`bash
node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('${file}', 'utf8')); console.log(JSON.stringify(data, null, 2));"
\`\`\`
## 结果
- 成功解析: `data.json`
- 耗时: 0.5s
- 输出格式正确
## 为什么成功
- Node.js 是 Electron 内置,无需安装
- 不依赖外部工具如 jq
- 处理大文件也很快
## 相关技能
- `skills/learned/parse-json/SKILL.md`
```
### 3. 失败记忆文件
`memory/failures/2024-02-23-pip-failed.md`:
```markdown
---
type: failure
task: install-python-package
created: 2024-02-23T15:20:00Z
---
# pip install 失败案例
## 任务
安装 Python 包 `requests`
## 尝试的方案
\`\`\`bash
pip install requests
\`\`\`
## 失败原因
- `pip: command not found` - 系统 Python 未配置或不存在
- 即使 pip 存在,可能污染系统 Python 环境
## 正确的方案
参考 `evo-map/decisions/install-python.md`,应该使用:
\`\`\`bash
uv pip install requests
\`\`\`
## 学到的教训
- 优先使用 uv应用内打包
- 避免使用系统 pip
- 参考 EvoMap 中的决策树
## 相关记录
- 修复后的成功: `memory/successes/2024-02-24-install-uv.md`
```
### 4. 技能文件
`skills/learned/parse-json/SKILL.md`:
```markdown
---
name: parse-json
description: 解析 JSON 文件,提取字段,格式化输出。当需要处理 JSON 文件时使用。
confidence: 0.98
created: 2024-02-24
version: 2
---
# JSON 文件解析技能
## 触发条件
当用户需要:
- 读取 JSON 文件
- 提取 JSON 中的字段
- 格式化 JSON 输出
- 验证 JSON 语法
## 推荐方法
### 方法 1: Node.js推荐
\`\`\`bash
node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('${file}', 'utf8')); console.log(JSON.stringify(data.${field}, null, 2));"
\`\`\`
- 成功率: 98%
- 优点: Node.js 内置,无需依赖
- 缺点: 简单提取很方便
### 方法 2: jq备选
\`\`\`bash
jq '.field' < file.json
\`\`\`
- 成功率: 60%
- 优点: 功能强大
- 缺点: 需要安装 jq
## 常用模式
### 提取单个字段
\`\`\`bash
node -e "console.log(JSON.parse(require('fs').readFileSync('${file}')).${field})"
\`\`\`
### 格式化输出
\`\`\`bash
node -e "console.log(JSON.stringify(JSON.parse(require('fs').readFileSync('${file}')), null, 2))"
\`\`\`
## 相关记忆
- 成功案例: `memory/successes/2024-02-24-parse-json.md`
```
### 5. EvoMap 决策文件
`evo-map/decisions/install-python.md`:
```markdown
---
task: install-python-package
last-updated: 2024-02-24
---
# Python 包安装决策
## 决策树
\`\`\`
需要安装 Python 包
├─ 方案 A: uv pip install
│ 置信度: 95%
│ 成功率: 98%
│ 预期时间: 5s
│ 证据: 45 次成功 / 1 次失败
│ 推荐: ✅ 首选
├─ 方案 B: pip install
│ 置信度: 70%
│ 成功率: 80%
│ 预期时间: 10s
│ 证据: 30 次成功 / 7 次失败
│ 下一步: 若失败 → 切换到 uv
└─ 方案 C: 系统包管理器
置信度: 50%
成功率: 60%
预期时间: 30s
证据: 10 次成功 / 8 次失败
备注: 最后手段,可能需要 sudo
\`\`\`
## 选择逻辑
1. 优先使用 `uv pip install`
2. 如果 uv 不可用,尝试 `pip install`
3. 如果都失败,提示用户手动安装
## 相关记录
- `memory/successes/2024-02-24-install-uv.md`
- `memory/failures/2024-02-23-pip-failed.md`
```
---
## 索引机制
### 简单文件索引
`memory/index.json`:
```json
{
"successes": [
{
"file": "successes/2024-02-24-parse-json.md",
"task": "parse-json",
"confidence": 0.98,
"timestamp": "2024-02-24T10:30:00Z",
"keywords": ["json", "parse", "node"]
},
{
"file": "successes/2024-02-24-install-uv.md",
"task": "install-python-package",
"confidence": 0.95,
"timestamp": "2024-02-24T09:15:00Z",
"keywords": ["python", "uv", "install"]
}
],
"failures": [
{
"file": "failures/2024-02-23-pip-failed.md",
"task": "install-python-package",
"timestamp": "2024-02-23T15:20:00Z",
"keywords": ["python", "pip", "failed"]
}
]
}
```
### 索引更新策略
```typescript
// 每次写入记忆后更新索引
async function updateIndex(type: 'successes' | 'failures', filepath: string) {
const index = await loadIndex();
const frontmatter = await extractFrontmatter(filepath);
index[type].push({
file: filepath,
task: frontmatter.task,
confidence: frontmatter.confidence || 0,
timestamp: frontmatter.created,
keywords: extractKeywords(frontmatter),
});
await fs.writeFile('memory/index.json', JSON.stringify(index, null, 2));
}
// 快速检索:只读索引,不需要遍历文件
async function searchByTask(task: string): Promise<string[]> {
const index = await loadIndex();
return index.successes
.filter(m => m.task === task || m.keywords.includes(task))
.sort((a, b) => b.confidence - a.confidence)
.map(m => m.file);
}
```
---
## 读写接口
### 写入(编码)
```typescript
interface MemoryWriter {
// 写入成功记忆
writeSuccess(memory: SuccessMemory): Promise<void>;
// 写入失败记忆
writeFailure(memory: FailureMemory): Promise<void>;
// 更新 Soul.md
updateSoul(update: SoulUpdate): Promise<void>;
}
class MarkdownMemoryWriter implements MemoryWriter {
async writeSuccess(memory: SuccessMemory): Promise<void> {
const filename = `memory/successes/${memory.date}-${memory.slug}.md`;
const content = this.formatSuccessMarkdown(memory);
await fs.writeFile(filename, content, 'utf-8');
// 更新索引
await this.updateIndex('successes', filename);
}
private formatSuccessMarkdown(memory: SuccessMemory): string {
return `---
type: success
task: ${memory.task}
confidence: ${memory.confidence}
created: ${memory.timestamp}
---
# ${memory.title}
## 任务
${memory.description}
## 使用的方案
\`\`\`bash
${memory.command}
\`\`\`
## 结果
${memory.result}
## 为什么成功
${memory.reasoning}
## 相关技能
- ${memory.relatedSkill || '无'}
`;
}
}
```
### 检索(读取)
```typescript
interface MemoryReader {
// 语义检索(基于 frontmatter
search(query: string): Promise<MemoryFile[]>;
// 读取特定记忆
read(path: string): Promise<MemoryContent>;
// 获取相关决策
getDecision(task: string): Promise<DecisionNode>;
}
class MarkdownMemoryReader implements MemoryReader {
async search(query: string): Promise<MemoryFile[]> {
// 1. 遍历 memory 目录
const files = await this.getAllMemoryFiles();
// 2. 读取 frontmatter
const results: MemoryFile[] = [];
for (const file of files) {
const frontmatter = await this.extractFrontmatter(file);
// 3. 匹配查询
if (this.matches(query, frontmatter)) {
results.push({ file, frontmatter });
}
}
// 4. 按置信度/时间排序
return results.sort((a, b) => b.confidence - a.confidence);
}
private matches(query: string, frontmatter: Frontmatter): boolean {
const { task, type, keywords } = frontmatter;
return (
task?.includes(query) ||
type === query ||
keywords?.some((k: string) => k.includes(query))
);
}
async getDecision(task: string): Promise<DecisionNode> {
const decisionFile = `evo-map/decisions/${this.slugify(task)}.md`;
if (await fs.exists(decisionFile)) {
return this.parseDecisionFile(await fs.readFile(decisionFile, 'utf-8'));
}
// 回退到通用决策
return this.getGenericDecision(task);
}
}
```
---
## 记忆清理策略
```typescript
interface MemoryCleanupPolicy {
maxShortTermEntries: number; // 最多 50 条
maxLongTermEntries: number; // 最多 1000 条
maxAge: number; // 90 天
lowConfidenceThreshold: number; // 置信度 < 0.3
}
async function cleanupMemory(policy: MemoryCleanupPolicy): Promise<CleanupResult> {
const index = await loadIndex();
const now = Date.now();
const toDelete: string[] = [];
// 1. 清理过期的低置信度记忆
for (const memory of index.successes) {
const age = now - new Date(memory.timestamp).getTime();
const ageDays = age / (1000 * 60 * 60 * 24);
if (ageDays > policy.maxAge || memory.confidence < policy.lowConfidenceThreshold) {
toDelete.push(memory.file);
}
}
// 2. 限制总数量
const sorted = [...index.successes].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
if (sorted.length > policy.maxLongTermEntries) {
const excess = sorted.slice(policy.maxLongTermEntries);
toDelete.push(...excess.map(m => m.file));
}
// 3. 执行删除
for (const file of toDelete) {
await fs.unlink(`memory/${file}`);
}
// 4. 更新索引
await rebuildIndex();
return { deleted: toDelete.length, remaining: index.successes.length - toDelete.length };
}
```
---
## 向量检索(可选升级)
当记忆数量超过 500 条时,可以引入语义向量检索以提升召回精度:
### 升级时机
- **当前阶段**:使用关键词匹配(已实现)
- 基于 frontmatter 的 `task``type``keywords` 字段
- 适用于记忆数量 < 500 条
- **升级阶段**:记忆数量 > 500 条时
- 引入嵌入向量embedding进行语义检索
- 保持 Markdown 格式不变,向量作为补充索引
### 向量检索方案
```typescript
// 记忆编码时添加嵌入向量
interface EncodedMemory {
id: string;
type: 'success' | 'failure' | 'insight';
embedding?: number[]; // 语义向量(可选)
context: {
task: string;
environment: string;
constraints: string[];
};
// ... 其他字段
}
// 更新后的索引
interface MemoryIndex {
successes: MemoryIndexEntry[];
failures: MemoryIndexEntry[];
}
interface MemoryIndexEntry {
file: string;
task: string;
confidence: number;
timestamp: string;
keywords: string[];
embedding?: number[]; // 新增:嵌入向量
}
```
### 检索接口升级
```typescript
class MarkdownMemoryReader implements MemoryReader {
async search(query: string): Promise<MemoryFile[]> {
const files = await this.getAllMemoryFiles();
// 初期:关键词匹配
const results: MemoryFile[] = [];
for (const file of files) {
const frontmatter = await this.extractFrontmatter(file);
if (this.matchesKeyword(query, frontmatter)) {
results.push({ file, frontmatter });
}
}
// 后期:记忆 > 500 条时使用向量检索
if (files.length > 500) {
const queryEmbedding = await this.generateEmbedding(query);
return this.searchBySimilarity(queryEmbedding, files);
}
return results.sort((a, b) => b.confidence - a.confidence);
}
// 语义相似度检索(待实现)
private async searchBySimilarity(
queryEmbedding: number[],
files: string[]
): Promise<MemoryFile[]> {
const results: Array<{ file: string; score: number }> = [];
for (const file of files) {
const entry = await this.getIndexEntry(file);
if (entry.embedding) {
const score = this.cosineSimilarity(queryEmbedding, entry.embedding);
results.push({ file, score });
}
}
return results
.filter(r => r.score > 0.7) // 相似度阈值
.sort((a, b) => b.score - a.score)
.slice(0, 10); // 返回 top-10
}
private cosineSimilarity(a: number[], b: number[]): number {
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dotProduct / (magnitudeA * magnitudeB);
}
}
```
### 向量存储
向量可以存储在以下位置:
```
~/.qimingclaw/
├── memory/
│ ├── index.json # 原有索引
│ └── embeddings.json # 新增:向量索引(可选)
│ # 或者使用 SQLite 存储
├── memory.db # 新增SQLite 向量数据库(可选)
```
### 实现优先级
| 阶段 | 记忆数量 | 检索方式 | 优先级 |
|------|----------|----------|--------|
| P0 | < 500 | 关键词匹配 | 已实现 |
| P1 | 500-2000 | 关键词 + 向量混合 | 可选 |
| P2 | > 2000 | 纯向量检索 | 待定 |
---
## 优势对比
| 特性 | Markdown 方案 | 数据库方案 |
|------|---------------|-----------|
| **可读性** | ✅ 人类可读 | ❌ 需要工具 |
| **版本控制** | ✅ Git 友好 | ⚠️ 需要 migration |
| **可移植性** | ✅ 纯文件 | ❌ 依赖软件 |
| **搜索** | ⚠️ 需索引 | ✅ SQL 查询 |
| **复杂度** | ✅ 简单 | ❌ 复杂 |
| **调试** | ✅ 直接查看 | ❌ 需要 query |
---
## 相关文档
- [总览](./OVERVIEW.md) - 产品定位、核心原则
- [核心组件](./COMPONENTS.md) - Memory、Skill Creator、EvoMap、Soul.md
- [循环流程](./LOOP.md) - 完整循环流程、接口定义
- [隔离策略](./ISOLATION.md) - 三区模型、环境变量

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)

View File

@@ -0,0 +1,160 @@
# 应用自动更新
## 概述
基于 `electron-updater` 实现应用内自动更新,支持 macOS / Windows (NSIS) / Linux 自动下载安装Windows MSI 安装引导到下载页(优先 OSS 安装包直链)。
**强制使用 latest.json**:应用与 Release 流程均以 OSS 上的 `latest.json` 为**唯一数据源**。版本检查、各平台下载地址、签名与大小均来自该文件;不直接依赖 GitHub Release 或 electron-builder 生成的 yml 作为来源。
**更新源macOS / Linux 应用内升级)**:检查与下载均走阿里云 OSS。
- **检查更新****仅**拉取 OSS `latest/latest.json` 获取最新版本;其中 `platforms` 已包含各平台完整的 OSS 下载包地址url、signature、size。若有更新则将 electron-updater 的 feed 设为版本化 OSS 路径 `${OSS_BASE}/electron-v${version}`
- **下载安装包**:安装包地址**仅**来自 `latest.json``platforms`(已是完整 OSS URL。electron-updater 实际下载时从 feed 请求的 `latest-mac.yml` / `latest-linux.yml` 等,**必须**由同一份 `latest.json``platforms` 在 Release 流程中派生生成,不得使用 GitHub 自带的 yml。
- **失败处理**OSS 不可达或元数据异常时,直接报错并引导用户前往下载页,不回退到 GitHub。
## 核心文件
| 文件 | 职责 |
|------|------|
| `src/main/services/autoUpdater.ts` | 更新服务:检查、下载、安装、状态管理 |
| `src/renderer/components/pages/AboutPage.tsx` | 关于页面:手动检查更新 UI |
| `src/main/window/trayManager.ts` | 托盘菜单:检查更新入口 |
| `src/main/ipc/appHandlers.ts` | IPC 处理checkUpdate / downloadUpdate / installUpdate / openReleasesPage |
| `src/preload/index.ts` | preload 暴露 API |
| `src/shared/types/updateTypes.ts` | 类型定义UpdateState / UpdateInfo / UpdateProgress |
| `dev-app-update.yml` | 开发模式更新源配置 |
## 更新流程
### 1. 启动自动检查
```
App 启动 → 延迟 10s → checkForUpdates()
├── 无更新 → 静默,不提示
├── 有更新 → 检查 skipped version
│ │
│ ├── 已跳过 → 不弹窗
│ │
│ └── 未跳过 → 弹窗提示
│ │
│ ├── "立即更新" → 下载 → 二次确认 → 重启安装
│ │
│ └── "跳过此版本" → 记录到文件,下次不再提示
└── 失败 → 静默记录日志
```
### 2. 手动检查About 页面)
```
用户点击 "检查更新"
├── 检查中... (loading 状态)
├── 无更新 → 提示 "当前已是最新版本",重置按钮
├── 有更新 → 显示 "下载更新" 按钮
│ │
│ └── 点击 → 下载(显示进度条)→ 完成 → Modal 确认 → 重启安装
└── 错误 → 显示错误信息 + 重试按钮
```
### 3. 手动检查(托盘菜单)
与 About 页面类似,但使用原生 `dialog.showMessageBox` 弹窗交互。
## Windows 安装类型检测
通过检查 NSIS 卸载程序文件是否存在来区分安装方式:
```typescript
// NSIS 安装会生成: {appDir}/Uninstall {productName}.exe
// MSI 安装由 Windows Installer 管理,无此文件
function detectInstallerType(): InstallerType {
const nsisUninstaller = path.join(appDir, `Uninstall ${productName}.exe`);
return fs.existsSync(nsisUninstaller) ? 'nsis' : 'msi';
}
```
| 安装类型 | 自动更新 | 行为 |
|----------|---------|------|
| NSIS (.exe) | 支持 | 下载 → 重启安装 |
| MSI (.msi) | 不支持 | 引导到下载页(按 MSI 优先解析下载包) |
| macOS (.dmg) | 支持 | 下载 → 重启安装 |
| Linux (.AppImage/.deb) | 支持 | 下载 → 重启安装 |
## 跳过版本
- 用户在启动弹窗中选择"跳过此版本"后,版本号写入 `~/.qimingclaw/.skipped-update-version`
- 下次启动时,若远程最新版本与已跳过版本相同,不弹窗
- 出现更新的版本(高于已跳过版本)时,重新弹窗提示
- 手动检查更新About 页面 / 托盘菜单)不受跳过逻辑影响
## 安装前清理
`installUpdate()` 在调用 `quitAndInstall()` 之前,先执行 `cleanupAllProcesses()`
- 停止 Computer Server
- 销毁 Unified Agent Service
- 终止 Agent Runner / Lanproxy / File Server 进程
- 清理 MCP Proxy
- 停止所有 Engine 进程
## 开发模式
- 通过 `dev-app-update.yml` 配置更新源GitHub
- `forceDevUpdateConfig = true` 启用开发模式检查
- 允许检查更新(验证 API 通路)
- 下载和安装被阻止Squirrel.Mac bundle ID 不匹配会导致崩溃)
- `autoInstallOnAppQuit = false`
## OSS 单源策略(强制 latest.json
所有更新元数据**强制以 latest.json 为准**:应用端只读 OSS `latest/latest.json` 做版本判断与地址解析Release 流程只产出并上传由该文件派生的 yml禁止使用或上传 GitHub 自带的 yml。
| 步骤 | 策略 |
|--------------|------|
| 检查更新 | 仅 OSS latest.json |
| Feed 基地址 | 仅 OSS 版本化路径 |
| 下载安装包 | 仅 OSSyml 必须由 latest.json 的 platforms 派生) |
| OSS 异常 | 报错 + 引导下载页,不回退 GitHub |
**数据源规则**OSS 上**只**维护并信任 `latest.json`(含 version、notes、pub_date、platforms 多架构与完整 OSS 下载地址。Release 流程(`.github/workflows/release-electron.yml`**必须**根据该 `latest.json``platforms` 生成 electron-updater 所需的 `latest-mac.yml``latest-linux.yml``latest.yml`,并写入 release-assets 后一并上传;**禁止**使用或上传 GitHub 自带的 ymlyml 内下载地址为相对路径(相对 feed 基地址即 OSS 版本化路径)。
本地触发 OSS 同步时,可在本 crate 下执行 `scripts/sync-oss.sh <tag>`(如 `electron-v0.8.0`),会触发上述 workflow 并轮询直至完成;依赖 `gh``jq`
## 自定义更新源
通过环境变量 `QIMING_UPDATE_SERVER` 可指定自定义更新服务器:
```bash
QIMING_UPDATE_SERVER=http://localhost:8080/updates npm run dev
```
使用 `generic` provider适合本地测试。
## UpdateState 类型
```typescript
interface UpdateState {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
version?: string;
progress?: { percent: number; bytesPerSecond: number; transferred: number; total: number };
error?: string;
canAutoUpdate?: boolean; // false 表示 MSI 安装,需手动下载
}
```
## IPC 通道
| 通道 | 方向 | 说明 |
|------|------|------|
| `app:checkUpdate` | renderer → main | 手动检查更新 |
| `app:downloadUpdate` | renderer → main | 下载更新 |
| `app:installUpdate` | renderer → main | 重启安装 |
| `app:getUpdateState` | renderer → main | 获取当前更新状态 |
| `app:openReleasesPage` | renderer → main | 打开下载页(从 latest.json 解析 OSS 安装包直链) |
| `update:status` | main → renderer | 推送更新状态变化 |

View File

@@ -0,0 +1,155 @@
# 客户端依赖(服务)安装、升级与启动流程
> 本文档梳理 Electron 客户端应用**依赖安装**(初始化 / 手动)、**升级**、**启动时同步**以及**安装或升级后的服务停止与重启**全流程,便于维护与排查。
---
## 1. 概述
- **必需依赖**:由 `SETUP_REQUIRED_DEPENDENCIES` 定义,不随应用打包,需在「初始化」或「依赖 Tab」中安装到 `~/.qimingclaw/`(或项目配置的数据目录)下的 `node_modules`
- **版本判定**:以**当前已真实安装的版本**为准;用户可在依赖 Tab 下手动升级,已安装版本 ≥ 配置的 `installVersion` 即视为就绪,**不降级**。
- **安装/升级后**:在主界面下(含重装流程和依赖 Tab依赖安装或升级成功后会调用 `services.restartAll()` 重启所有服务,使新二进制生效。初始化向导中的依赖安装不触发重启(此时服务尚未启动,向导完成后由自动重连流程启动服务)。
---
## 2. 必需依赖列表SETUP_REQUIRED_DEPENDENCIES
| 名称 | 显示名 | 类型 | 说明 | 固定版本(installVersion) |
|------|--------|------|------|--------------------------|
| uv | uv | bundled | Python 包管理器(已集成) | minVersion: 0.5.0 |
| pnpm | pnpm 包管理器 | npm-local | Node 包管理器 | 10.30.3 |
| qiming-file-server | 文件服务 | npm-local | 工作目录文件远程管理 | 1.2.2 |
| qimingcode | Agent 引擎 | bundled | 应用内集成 Agent 执行引擎 | 1.1.68 |
| qiming-mcp-stdio-proxy | MCP 服务 | npm-local | MCP 协议聚合代理 | 1.4.6 |
| claude-code-acp-ts | ACP 协议 | npm-local | 引擎统一适配 | 0.16.1 |
- **配置位置**`src/main/services/system/dependencies.ts` 中的 `SETUP_REQUIRED_DEPENDENCIES`
- **检测**`checkAllDependencies()` 仅检查上述列表,返回每项状态:`installed` | `bundled` | `missing` | `outdated` | `error`
---
## 3. 依赖状态与版本判定
- **installed / bundled**:已就绪,无需操作。
- **missing**:未安装,需要安装。
- **outdated**:已安装但当前版本 < 配置的 `installVersion`,可升级;**不强制**进入全屏依赖安装(用户可在依赖 Tab 自行升级)。
- **error**:检测或安装过程出错。
**版本比较约定**
- 以**当前已安装版本**与配置的 `installVersion` 比较。
- 仅当「未安装」或「已装版本 < installVersion」时标记为需安装/升级;已装 ≥ 目标则视为已就绪,不降级。
- 实现见 `dependencies.ts``checkAllDependencies``compareVersions(installed, target)` 逻辑。
---
## 4. 初始化安装(首次 / 向导内)
### 4.1 入口与流程
- **入口**:应用启动后若 `setupService.isSetupCompleted()` 为 false渲染**初始化向导**`SetupWizard`)。
- **向导阶段 1**:依赖检测与安装(`SetupDependencies`)。
- 调用 `dependencies.checkAll()`,若有任一项非 `installed`/`bundled`(包括 `outdated`),则进入依赖安装 UI。
- **注意**:向导内 `outdated` 也会触发安装阶段(确保首次设置时所有依赖都是最新),而主界面下 `outdated` 不触发全屏重装(见 section 5.1)。
- 先检查系统依赖(如 uv若缺失则提示并阻塞。
- 对 npm-local 等可安装项自动或手动安装,全部就绪后进入下一步。
- **完成回调**`SetupDependencies``onComplete``SetupWizard.handleDepsComplete` 处理:置 `dependenciesReady = true`然后进入「基础设置」步骤或快捷初始化Quick Init
### 4.2 与 Quick Init 的关系
- 若依赖已全部就绪且存在快捷初始化配置,会优先走 Quick Init不再重复安装依赖。
- 依赖步骤**不可跳过**:必须先完成依赖检测/安装,再进入基础设置与登录。
---
## 5. 主界面下「必需依赖缺失」触发的重装流程
### 5.1 触发条件
- 用户已进入主界面(`isSetupComplete === true`)。
- 应用在进入主界面后执行一次 `dependencies.checkAll()`
- **仅当**存在状态为 `missing``error` 的必需依赖时,置 `needsRequiredDepsReinstall = true`
- **outdated 不触发**:已安装但版本低于配置时,不强制全屏依赖安装,用户可在依赖 Tab 自行升级。
### 5.2 流程
1. 渲染层:`needsRequiredDepsReinstall === true` 时,全屏展示 `SetupDependencies`(与向导内为同一组件)。
2. 用户完成依赖安装后,`onComplete` 被调用:
-`setNeedsRequiredDepsReinstall(false)`,回到主界面。
- 再调用 `restartAllServices()`(内部先停后启),使新依赖生效。
3. 不改变 `isSetupComplete`,不重新走向导的账号登录等步骤。
### 5.3 涉及代码
- `App.tsx``needsRequiredDepsReinstall` 状态、`checkRequiredDeps``useEffect`、全屏 `SetupDependencies``onComplete``restartAllServices()`
---
## 6. 依赖 Tab 下手动安装 / 升级
### 6.1 入口
- 主界面 → 依赖 Tab`DependenciesPage`)。
- 可对单项点击「安装」/「升级」,或使用「全部安装」/「全部升级」/「安装并升级」。
### 6.2 行为
- **单项**:调用 `dependencies.installPackage(name, options?)`,可选 `options.version`(如 `installVersion`);成功后调用 `restartServicesAfterDepChange()`
- **批量**:遍历需安装/升级的依赖依次安装;若有任意一项成功,则调用 `restartServicesAfterDepChange()`
- **restartServicesAfterDepChange**:内部调用 `services.restartAll()`(主进程内先停后启),并提示「正在重启服务…」「服务已重启」或失败提示。
### 6.3 版本与文案
- 以当前已安装版本为准;`outdated` 时按钮为「升级」,否则为「安装」。
- 批量时按缺失/过期组合显示「全部安装」「全部升级」或「安装并升级」;完成提示区分「依赖安装完成」「依赖升级完成」「依赖安装并升级完成」。
---
## 7. 应用启动时的依赖同步syncInitDependencies
- **目的**:应用升级后,若配置中的 `installVersion` 或应用版本发生变化,将依赖同步到新版本。
- **触发**:主进程启动任务(`bootstrap/startup.ts`)中,通过 `getInitDepsState()` 读取上次同步状态,与当前 `app.getVersion()` 及各包 `installVersion` 逐一比较。满足以下任一条件即触发 `syncInitDependencies()`
- 应用版本(`appVersion`)与上次记录不同(含首次无记录);
- 任一包的 `installVersion` 与上次记录的对应值不同(含上次无该包记录)。
- **行为**:对 `SETUP_REQUIRED_DEPENDENCIES` 中带 `installVersion` 的 npm-local 包,若未安装或已装版本低于配置,则安装到指定版本,并写回 `~/.qimingclaw/.init-deps-state.json`(或当前数据目录下的同名文件)。
- **不降级**:已安装版本 ≥ `installVersion` 的包不会被执行安装,避免覆盖用户手动升级的更高版本。
---
## 8. 安装或升级后的服务停止与重启
以下任一场景在「依赖安装或升级成功」后,都会执行**先停止再重启**所有相关服务,使新二进制生效:
| 场景 | 位置 | 行为 |
|------|------|------|
| 主界面重装依赖完成 | `App.tsx` 全屏 `SetupDependencies``onComplete` | `setNeedsRequiredDepsReinstall(false)` 后调用 `restartAllServices()`(即 `services.restartAll()` |
| 依赖 Tab 单项安装/升级成功 | `DependenciesPage.handleInstallSingleDep` | 调用 `restartServicesAfterDepChange()``services.restartAll()` |
| 依赖 Tab 批量安装/升级且至少成功一项 | `DependenciesPage.handleInstallAllDeps` | 同上 `restartServicesAfterDepChange()` |
- **restartAll**:主进程 `processHandlers.ts``services:restartAll` IPC handler 的实现会先停止 Agent、ComputerServer、FileServer、Lanproxy**不停 MCP**),再按顺序启动 MCP → Agent → ComputerServer → FileServer → Lanproxy。MCP 在 restartAll 中只做 start/verify不先停。
- **stopAll**:主进程 `processHandlers.ts``services:stopAll` IPC handler 会停止所有服务**包括 MCP**。
- **用户登出**:渲染进程 `ClientPage.handleLogout` 在停止 agent/fileServer/lanproxy/mcp 后,会单独调用 `computerServer.stop()`,避免登出后进程残留导致端口冲突;详见 [认证机制与 SavedKey 生命周期](./auth-savedkey-lifecycle.md#退出登录)。
- **serviceManager.ts**`serviceManager.restartAllServices()` 是另一套实现(供 `main.ts` 内部和 Tray 菜单使用),行为与 IPC `restartAll` 略有不同:会先停 MCP 再启动,且不包含 ComputerServer。渲染进程通过 `window.electronAPI.services.restartAll()` 调用的是 `processHandlers.ts` 中的 IPC handler 版本。
- **无需单独 stopAll**`restartAll` 内部已包含停止逻辑Agent / ComputerServer / FileServer / Lanproxy若需停止所有服务含 MCP且不重启可调用 `services.stopAll()`
---
## 9. 涉及文件与入口汇总
| 类型 | 路径 / 说明 |
|------|------------------|
| 依赖配置与检测 | `src/main/services/system/dependencies.ts``SETUP_REQUIRED_DEPENDENCIES``checkAllDependencies``installNpmPackage``installMissingDependencies``syncInitDependencies``compareVersions` |
| 启动同步 | `src/main/bootstrap/startup.ts`:启动后根据 `getInitDepsState` 与版本比较调用 `syncInitDependencies` |
| IPC | `src/main/ipc/dependencyHandlers.ts``dependencies:checkAll``dependencies:installPackage` 等;`processHandlers.ts``services:stopAll``services:restartAll` |
| 服务管理器 | `src/main/window/serviceManager.ts``restartAllServices``stopAllServices`(供 main.ts / Tray 使用,行为与 IPC handler 略有不同,见 section 8 |
| 初始化向导 | `src/renderer/components/setup/SetupWizard.tsx``SetupDependencies.tsx`:向导内依赖步骤与完成回调 |
| 主界面重装 | `src/renderer/App.tsx``needsRequiredDepsReinstall`、全屏 `SetupDependencies``checkRequiredDeps``restartAllServices` |
| 依赖 Tab | `src/renderer/components/pages/DependenciesPage.tsx`:单项/批量安装与 `restartServicesAfterDepChange` |
| 持久化 | `~/.qimingclaw/.init-deps-state.json`(或当前数据目录):应用版本与各包上次同步版本,读写在 `dependencies.ts``getInitDepsState` / `setInitDepsState` |
---
## 10. 相关文档
- [依赖版本固定与安装/升级行为](./dependency-version-pinning.md) — installVersion、同步、文案与不降级约定。
- [Quick Init](./QUICK-INIT.md) — 快捷初始化与依赖步骤不可跳过的约定。

View File

@@ -0,0 +1,85 @@
---
version: 1.0
last-updated: 2026-03-04
status: stable
---
# 初始化依赖版本固定与安装/升级行为
> 本文档留存「初始化依赖版本固定」「客户端升级后依赖同步」「安装/升级文案」「尊重用户已安装版本」等需求与实现约定。
---
## 概述
以下四类 npm 包不随应用打包,在 `~/.qimingclaw` 下按配置版本进行初始化安装并在客户端升级后按新版本同步同时需尊重用户在「依赖」Tab 下的手动升级结果,不降级已安装的更高版本。
| 包名 | 用途 |
|------|------|
| qiming-file-server | 文件服务 |
| qiming-mcp-stdio-proxy | MCP 聚合代理 |
| qimingcode | 引擎 |
| claude-code-acp-ts | 引擎 ACP |
---
## 需求与约束
### 1. 初始化依赖版本固定installVersion
- **目的**:保证首次安装/初始化时安装的版本稳定、可预期,避免「总是装最新」带来的兼容性风险。
- **实现**:在 `SETUP_REQUIRED_DEPENDENCIES` 中为上述四包配置 `installVersion`,初始化或补装时执行 `npm install <name>@<installVersion>`
- **范围**:仅影响「初始化/安装缺失依赖」和「依赖 Tab 内一键安装」使用的版本;依赖 Tab 下手动升级行为不受此限制(见下文)。
### 2. 客户端升级后的依赖同步
- **目的**:应用发新版本时,若配置中的 `installVersion` 发生变化,用户环境中的依赖应被同步到新版本,无需用户手动操作。
- **实现**
- 持久化:`~/.qimingclaw/.init-deps-state.json` 记录当前应用版本及各包「上次同步时的目标版本」。
- 启动时:若检测到应用版本或某包的 `installVersion` 与持久化状态不一致,在后台调用 `syncInitDependencies()`,对需要更新的包执行安装/升级并写回状态。
- **注意**:同步逻辑必须结合「当前已安装的实际版本」判断是否需要升级,避免对用户已手动升级的更高版本做降级(见下节)。
### 3. 安装与升级的文案区分
- **目的**:当既有「缺失需安装」又有「已装但需升级」时,文案明确区分「安装」与「升级」,避免一律显示「安装」造成误解。
- **约定**
- 初始化向导SetupDependencies若存在需升级的依赖阶段文案使用「正在安装并升级依赖...」/「正在安装并升级 xxx...」;错误态标题/按钮使用「依赖安装与升级」「重试安装并升级」「安装/升级失败」等。
- 依赖页DependenciesPage
- 批量:既有缺失又有过期时按钮为「安装并升级」;仅过期时为「全部升级」;仅缺失时为「全部安装」。
- 单项:状态为 `outdated` 时按钮为「升级」,否则为「安装」;进行中分别显示「升级中...」「安装中...」;成功/失败提示对应「升级成功/失败」「安装成功/失败」。
- 完成提示:按实际执行结果区分「依赖安装完成」「依赖升级完成」「依赖安装并升级完成」。
### 4. 结合当前已安装的实际版本(不降级)
- **背景**用户可以在「依赖」Tab 下手动升级某个包到比应用配置的 `installVersion` 更高的版本。若仅以「是否等于 installVersion」判断会把「已装更高版本」误判为需升级并执行安装导致被降级。
- **约定**
- **判定需升级的条件**:仅当「未安装」或「当前已安装版本 < 配置的 installVersion」时才标记为需安装/升级(`outdated` 或执行安装)。
- **判定已就绪的条件**:当前已安装版本 ≥ installVersion 时,视为已就绪,不再提示升级、不执行安装。
- **实现要点**
- `checkAllDependencies`:对上述四包,使用版本比较(如 `compareVersions(已装版本, installVersion)`),只有已装 < 目标时才置 `status === 'outdated'`
- `syncInitDependencies`:仅当未安装或已装版本低于 `installVersion` 时才执行安装/升级;已装 ≥ 目标则跳过。
- 代码注释中注明:「用户可在依赖 Tab 下手动升级,故以当前已安装的实际版本为准,不降级。」
---
## 涉及文件与入口
| 类型 | 路径/说明 |
|------|-----------|
| 配置与检测 | `src/main/services/system/dependencies.ts``SETUP_REQUIRED_DEPENDENCIES``checkAllDependencies``installMissingDependencies``syncInitDependencies``compareVersions` |
| 持久化 | `~/.qimingclaw/.init-deps-state.json`,读写通过 `getInitDepsState` / `setInitDepsState` |
| 启动触发 | `src/main/bootstrap/startup.ts`:应用启动后根据状态决定是否调用 `syncInitDependencies` |
| 向导 UI | `src/renderer/components/setup/SetupDependencies.tsx`:阶段文案、错误态标题与按钮、安装时传入 `installVersion` |
| 依赖页 UI | `src/renderer/components/pages/DependenciesPage.tsx`:批量/单项按钮文案、安装中/升级中文案、`installVersion` 传入 |
| 类型与 API | `src/shared/types/electron.d.ts``LocalDependencyItem.installVersion`preload 暴露 `dependencies.installPackage(name, options?)` 支持 `options.version` |
---
## 相关文档
- [Quick Init](./QUICK-INIT.md) — 快捷初始化中约定依赖步骤不可跳过,且上述四包通过 installVersion 初始化并参与 syncInitDependencies。
- 依赖检测与安装的详细逻辑见 `dependencies.ts` 内注释及导出接口。
---
*本文档用于留存需求与行为约定,便于后续维护与排查。*

View File

@@ -0,0 +1,149 @@
---
version: 1.1
last-updated: 2026-03-05
status: implemented
---
# Device ID 生成方案
## 概述
为 Electron 客户端生成稳定、安全的设备唯一标识符deviceId用于设备识别、授权绑定等场景。
---
## 方案
### 核心公式
```
deviceId = SHA-256(machineId + appSalt)
```
- `machineId`:操作系统级机器标识(通过 `node-machine-id` 获取原始值)
- `appSalt`:应用固定盐值,避免不同应用使用相同 machineId 产生碰撞
### 实现
```typescript
import { machineIdSync } from "node-machine-id";
import { createHash } from "crypto";
import * as os from "os";
import log from "electron-log";
const APP_SALT = "qiming-agent";
let cachedDeviceId: string | null = null;
export function getDeviceId(): string {
if (cachedDeviceId) return cachedDeviceId;
let raw: string;
try {
raw = machineIdSync(true);
} catch (e) {
log.warn("[DeviceId] Failed to read machineId, using hostname fallback:", e);
raw = os.hostname();
}
cachedDeviceId = createHash("sha256")
.update(raw + APP_SALT)
.digest("hex");
log.info(`[DeviceId] ${cachedDeviceId}`);
return cachedDeviceId;
}
```
### 各平台 machineId 数据源
| 平台 | 数据源 | 说明 |
|------|--------|------|
| **macOS** | `ioreg``IOPlatformUUID` | 硬件 UUID无需管理员权限 |
| **Windows** | 注册表 `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` | 系统安装时生成 |
| **Linux** | `/var/lib/dbus/machine-id` | systemd 机器 ID |
### 依赖
| 包名 | 版本 | 说明 |
|------|------|------|
| `node-machine-id` | ^1.1.12 | 跨平台机器 ID 读取,零 native 依赖 |
`crypto` 为 Node.js 内置模块,无需额外安装。
---
## 设计要点
### 1. 稳定性
- 同一台机器每次调用返回相同值
- 进程内缓存,避免重复读取系统命令
### 2. 安全性
- SHA-256 单向 hash不暴露原始 machineId
- 加入应用盐值,防止跨应用关联
### 3. 唯一性
- machineId 由操作系统保证唯一
- 加盐 hash 后不同应用产生不同 deviceId
### 4. 位置
- 实现在 **main process**`src/main/services/` 下)
- renderer 通过 IPC + preload bridge 获取
---
## 架构集成
```
Main Process
├── src/main/services/system/deviceId.ts # 实现 + 缓存 + 日志
├── src/main/services/system/index.ts # re-export
├── src/main/main.ts # 启动时调用并输出日志
└── src/main/ipc/appHandlers.ts # IPC handler
Preload
└── src/preload/index.ts # contextBridge 暴露
Renderer
└── window.electronAPI.app.getDeviceId() # 调用
```
### IPC 通道
```typescript
// main (appHandlers.ts)
ipcMain.handle("app:getDeviceId", () => getDeviceId());
// preload (index.ts)
app: {
getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"),
}
```
---
## 特性对比(为何选此方案)
| 方案 | 稳定性 | 安全性 | 依赖 | 备注 |
|------|--------|--------|------|------|
| **hash(machineId + salt)** | 高 | 高 | 1 个 npm 包 | **选用** |
| 纯 machineId | 高 | 低(暴露原始值) | 1 个 npm 包 | 不推荐 |
| hw-fingerprint硬件指纹 | 中(换硬件会变) | 高 | 重依赖 systeminformation | 过重 |
| 本地持久化 UUID | 低(删文件会变) | 中 | 无 | 不绑定设备 |
| hash(machineId + randomUUID) | 无(每次不同) | - | - | 错误方案 |
---
## 注意事项
1. **容器/镜像环境**:基于镜像的环境可能共享相同 machine-idLinux 下可用 `dbus-uuidgen` 重新生成
2. **Windows 系统更新**MachineGuid 在重大系统更新后极少数情况下可能变化
3. **macOS 权限**IOPlatformUUID 无需额外权限,区别于 IOPlatformSerialNumber新版 macOS 受限)
---
*参考: 此方案为 SaaS 客户端常用的设备标识模式*

View File

@@ -0,0 +1,340 @@
---
version: 1.3
last-updated: 2026-04-09
status: stable
---
# 内嵌 Webview 会话浏览器 + Cookie 登录态同步
> 本文档描述将会话页面从系统浏览器迁移至客户端内嵌 webview 的方案,以及通过 reg 接口返回的 `token` 自动同步 httpOnly cookie 实现免登录打通的设计。
---
## 背景与动机
| 改动前 | 改动后 |
|--------|--------|
| 点击「开始会话」调用 `shell.openExternal` 在系统浏览器打开 | 在 ClientPage 内嵌 `<webview>` 渲染会话页面 |
| 用户需要在外部浏览器重新登录 | 通过 cookie 同步自动携带登录态,免二次登录 |
| 体验割裂,无法与客户端 UI 联动 | 统一在客户端窗口内操作,支持返回/刷新工具栏 |
---
## 架构总览
```
┌──────────────────────────────────────────────────────────────┐
│ Electron Main Process │
│ │
│ appHandlers.ts │
│ ┌──────────────────────────────────────┐ │
│ │ session:setCookie IPC handler │ │
│ │ → electronSession.defaultSession │ │
│ │ .cookies.set({ httpOnly, secure }) │ │
│ └──────────────────────────────────────┘ │
│ │
│ main.ts │
│ ┌──────────────────────────────────────┐ │
│ │ webPreferences: { webviewTag: true } │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────┬───────────────────────────────┘
│ IPC
┌──────────────────────────────────────────────────────────────┐
│ Electron Renderer Process │
│ │
│ ClientPage.tsx │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ handleStartSession() │ │
│ │ 1. 读取 auth.token (settings) │ │
│ │ 2. 调用 session.setCookie → main 进程设置 cookie │ │
│ │ 3. setWebviewVisible(true) │ │
│ │ │ │
│ │ <webview src={redirectUrl}> │ │
│ │ └── 使用 defaultSession自动携带 ticket cookie │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
---
## 数据流
### Token 获取与持久化
```
服务端 /api/sandbox/config/reg
└── 返回 { configKey, serverHost, serverPort, token, ... }
├── loginAndRegister() ── if (response.token) → 写入 AUTH_TOKEN + domain_token + 立即同步 cookie
├── reRegisterClient() ── if (response.token) → 同上(自动重注册场景)
└── syncConfigToServer() ── if (response.token) → 同上(手动配置同步场景)
```
三个入口的 token 处理逻辑一致:
```
reg 返回 token
① AUTH_TOKEN = token ← one-shot给后续 webview 打开时用
② domain_token:{domain} = token ← 域名级缓存,兜底
③ syncSessionCookie(domain, token) ← 立即写 webview cookie
├─ 成功 → AUTH_TOKEN = null ← 已消费,清空
└─ 失败 → 保留 AUTH_TOKEN ← 等下次 webview 打开再同步
```
token 存入 SQLite通过 `settingsSet`与其他认证字段username、configKey同级管理。密码不持久化。
### Cookie 同步时序
```
syncCookieAndBuildUrl() — 打开 webview 前调用
├── 1. getCurrentAuth() → 获取 domain、configId
├── 2. settingsGet('auth.token') → 读取 one-shot token
│ ├── 有值 → tokenSource = "one_shot"
│ └── 无值 → settingsGet('domain_token:{domain}') → 读取域名缓存 token
│ └── 有值 → tokenSource = "domain_cache"
├── 3. token 不存在?
│ └── 是 → 跳过同步(不清空现有 cookie直接返回 buildUrl()
├── 4. parseJwtExpDate(token) → 检查 JWT exp 是否过期30s 宽限容忍时钟偏移)
│ └── 已过期exp + 30s ≤ now
│ └── 是 → 只清除对应来源缓存one-shot 清 AUTH_TOKENdomain_cache 清 domain key
│ 跳过 cookie 覆写,直接返回 buildUrl()
├── 5. token 有效 → syncSessionCookie(domain, token)
│ ├── electronAPI.session.setCookie({
│ │ url: domain, // e.g. "https://agent.qiming.com"
│ │ name: 'ticket',
│ │ value: token,
│ │ // 不设 domain → host-only cookie
│ │ // 不设 secure → 主进程根据 URL scheme 自动判断
│ │ httpOnly: true,
│ │ })
│ │ │
│ │ ├── [Main Process] removeSameNameCookies() ← 清除所有旧 ticket cookie
│ │ └── [Main Process] electronSession.defaultSession.cookies.set(...)
│ │ ├── secure: URL 以 https:// 开头 → true否则 false
│ │ ├── secure=true 时设 sameSite: 'no_restriction'
│ │ └── expirationDate: 从 JWT exp 解析,兜底 7 天
│ │
│ ├── 成功 → settingsSet('auth.token', null) ← 消费 one-shot token
│ └── 失败 → 保留 token抛出异常下次重试
└── 6. return buildUrl(domain, configId)
└── <webview src={redirectUrl}> 发起请求
├── 请求自动携带 ticket cookie (host-only, httpOnly)
└── 服务端校验通过 → 免登录加载页面
```
### 清除时机
| 场景 | 动作 |
|------|------|
| 退出登录 (`clearAuthInfo`) | `settingsSet('auth.token', null)` |
| 退出登录 (`handleLogout`) | `setWebviewVisible(false)` — 关闭 webview |
| reg 返回新 token | 覆盖旧值login 和 sync 两个入口均会更新) |
| cookie 同步成功 | `settingsSet('auth.token', null)` — 消费 one-shot token |
| 缓存 token 已过期 | 只清除对应来源one-shot → `AUTH_TOKEN`domain cache → domain key |
| webview 内重新登录 (`persistTicketCookie`) | 清除 `AUTH_TOKEN` + domain key刷盘 cookie |
### Webview 重新登录处理
```
webview 内 token 过期 → 服务端跳转 /login
用户在 webview 内重新登录 → 服务端 Set-Cookie: ticket=新token
EmbeddedWebview 检测到从 /login 跳转到非 login 页面
调用 persistTicketCookie(domain)
├── flushStore() — 确保新 cookie 写入磁盘
└── 清除 AUTH_TOKEN + domain_token:{domain} — 防止旧缓存覆盖新 ticket
```
**防御性兜底**:即使 `persistTicketCookie` 未被调用(如 race condition`syncCookieAndBuildUrl` 在下次打开 webview 时仍会检查 JWT exp过期的缓存 token 不会覆盖现有 cookie。
---
## 修改文件清单
| 文件 | 层 | 修改内容 |
|------|----|---------|
| `src/renderer/services/core/api.ts` | 类型 | `ClientRegisterResponse` 增加 `token?: string` |
| `src/shared/constants.ts` | 常量 | `AUTH_KEYS` 增加 `AUTH_TOKEN: 'auth.token'` |
| `src/renderer/services/core/auth.ts` | 服务 | `loginAndRegister` / `syncConfigToServer` 持久化 token`clearAuthInfo` 清除 token |
| `src/main/main.ts` | 主进程 | `webPreferences` 增加 `webviewTag: true` |
| `src/main/ipc/appHandlers.ts` | IPC | 新增 `session:setCookie` handler |
| `src/preload/index.ts` | Preload | 暴露 `session.setCookie` |
| `src/shared/types/electron.d.ts` | 类型 | `ElectronAPI.session` 替换为 `setCookie`;移除废弃的 `Session`/`Message` 接口和 `message` 块 |
| `src/shared/types/webview.d.ts` | 类型 | **新建**`<webview>` JSX IntrinsicElements 声明 |
| `src/renderer/components/EmbeddedWebview.tsx` | 组件 | **新建** — 可复用的内嵌 webview 组件(工具栏 + 错误处理 + webview |
| `src/renderer/styles/components/EmbeddedWebview.module.css` | 样式 | **新建** — webview 容器 / 工具栏 / URL 栏样式 |
| `src/renderer/components/pages/ClientPage.tsx` | 组件 | 替换 `openExternal``<EmbeddedWebview>` + cookie 同步 |
| `src/renderer/styles/components/ClientPage.module.css` | 样式 | `.page` 增加 flex 布局以支持 webview 撑满 |
---
## 关键设计决策
### 1. 使用 `<webview>` 而非 `BrowserView` 或 `<iframe>`
| 方案 | 优点 | 缺点 |
|------|------|------|
| `<webview>` | React JSX 内直接使用,生命周期由组件控制,共享 `defaultSession` | Electron 官方标记为"less secure"(但加载的是自有域名) |
| `BrowserView` | 进程隔离更彻底 | 需在 main 进程管理定位/尺寸,与 React 渲染模型脱节 |
| `<iframe>` | 最简单 | 受 CSP 限制,无法设置 httpOnly cookie跨域问题 |
**结论**`<webview>` 最适合本场景 — 加载自有域名、需要 cookie 同步、需要与 React 组件联动。
### 2. 使用 `defaultSession` 而非 `partition`
webview 默认使用 `defaultSession`,与 `session:setCookie` handler 操作同一 session。无需额外 partition 配置cookie 自然可见。
### 3. Cookie 属性选择
```typescript
{
httpOnly: true, // 防止 webview 内 JS 读取 token
// secure: 主进程根据 URL scheme 自动判断HTTPS→true, HTTP→false
// sameSite: 主进程自动判断secure 时设 no_restriction否则不设
// domain: 不设置 → host-only cookie与 webview 内 Set-Cookie 行为一致
}
```
- `httpOnly: true` — 安全性:即使 webview 加载的页面被注入 XSS也无法通过 `document.cookie` 读取 token
- `secure` — 由主进程根据 URL scheme 自动设置。HTTPS 域名必须为 trueChromium 要求 `SameSite=None` 需配合 `Secure`。HTTP 域名自动为 false
- `sameSite` — 主进程自动判断:`secure: true` 时设 `'no_restriction'`HTTP 场景不设
- **不设 `domain`** — host-only cookie确保与 webview 内登录的 `Set-Cookie` 能互相覆盖,避免 `count=2` 冲突v1.1 修复)
### 4. Cookie 双向覆盖
Electron 侧和 webview 内的 ticket cookie 使用相同属性host-only + httpOnly + secure可以互相覆盖
| 方向 | 行为 |
|------|------|
| **Electron → Webview** | `removeSameNameCookies()` 清除所有旧 cookie → 写 host-only cookie → webview 导航时携带 |
| **Webview → Electron** | webview 登录 `Set-Cookie: ticket=xxx`host-only→ 覆盖 Electron 写的同名 cookie |
| **Electron 再写** | `removeSameNameCookies()` 再次清除所有(含 webview 设的)→ 写新值 |
**修复前问题**Electron 显式设 `domain: "agent.qiming.com"` → Chromium 存为 `.agent.qiming.com`domain cookie而 webview 登录的 `Set-Cookie` 无 Domain 属性 → Chromium 存为 `agent.qiming.com`host-only cookie。两个 cookie 条目同名不同属性,`count=2` 无法互相覆盖。
### 5. Cookie 同步策略v1.3
**核心规则**:有有效 token → 覆盖 cookie有过期 token → 只清对应缓存并跳过;无 token → 不做任何操作。
- reg 接口返回 token 时,不管现有 cookie 是否存在、是否在有效期,都直接调用 `syncSessionCookie` 覆盖
- reg 接口未返回 token 时,跳过同步,不清空现有 cookie避免误清除 webview 内登录产生的 ticket
- 同步成功后清除 one-shot token`AUTH_TOKEN`),失败时保留以供重试
- **JWT 过期检查**v1.3):同步前检查 token 的 `exp` 字段,若已过期(超过 30s 宽限期)则:
- 只清除对应来源的缓存one-shot → `AUTH_TOKEN`domain cache → domain key
- 跳过 cookie 覆写,避免用过期 token 覆盖 webview 内重新登录获得的新 ticket
- 30s 宽限(`JWT_EXPIRY_BUFFER_MS`)容忍客户端与服务端时钟偏移
- **webview 重新登录后清缓存**v1.3`persistTicketCookie()` 在检测到 webview 登录成功后,清除 `AUTH_TOKEN` + domain key 双缓存,作为主修复;`syncCookieAndBuildUrl` 的 exp 检查作为防御性兜底
**v1.2 逻辑**(已更新):有 token → 无条件覆盖。v1.3 增加了过期检查,防止 webview 重新登录后旧缓存覆盖新 ticket。
---
## 布局结构
```
.app-container (100vh, flex column)
├── .app-header (48px, flex-shrink: 0)
└── .app-body (flex: 1, flex row, overflow: hidden)
├── .app-sider (140px)
└── .app-content (flex: 1, overflow-y: auto, padding: 20px 24px)
└── wrapper div (flex: 1, flex column, min-height: 0)
└── .page (flex: 1, flex column, min-height: 0)
├── [webviewVisible = true]
│ └── <EmbeddedWebview url={...} onClose={...}>
│ └── .container (flex: 1, flex column)
│ ├── .toolbar (固定高度, flex-shrink: 0)
│ │ ├── 返回按钮
│ │ ├── URL 显示 (.url, ellipsis)
│ │ └── 刷新按钮
│ ├── Alert (可选, 加载失败时显示)
│ └── <webview> (flex: 1, 撑满剩余空间)
└── [webviewVisible = false]
├── 依赖告警 (Alert)
├── 账号状态 (section)
├── 服务状态 (section)
└── 快捷操作 (section)
```
`.page` 添加了 `display: flex; flex-direction: column; flex: 1; min-height: 0` 以确保 webview 能撑满内容区。
---
## Webview 错误处理
`EmbeddedWebview` 组件在 `useEffect` 中注册事件监听,卸载时自动清除:
| 事件 | 处理 |
|------|------|
| `did-fail-load` | 当 `errorCode !== -3`(排除导航取消)时显示 Alert 错误提示 |
| `did-start-loading` | 清除之前的错误状态 |
错误以 `<Alert type="error" closable>` 形式显示在工具栏下方,用户可手动关闭或等待下次加载自动清除。
---
## IPC 接口
### `session:setCookie`
**方向**Renderer → Main
**参数**
```typescript
{
url: string; // cookie 关联的 URLe.g. "https://agent.qiming.com"
name: string; // cookie 名称e.g. "ticket"
value: string; // cookie 值token
// domain 不设置 → host-only cookie与 webview Set-Cookie 行为一致
httpOnly?: boolean; // 默认 true
// secure 不设置 → 主进程根据 URL scheme 自动判断HTTPS→true, HTTP→false
}
```
**返回**
```typescript
{ success: boolean; error?: string }
```
**实现**
1. `removeSameNameCookies()` — 清除所有同名旧 cookie含 domain cookie 和 host-only cookie
2. `electronSession.defaultSession.cookies.set()` — 写新 cookie
3.`secure: true` 时自动设置 `sameSite: 'no_restriction'`
---
## 安全考量
| 风险 | 缓解措施 |
|------|---------|
| webview 加载恶意内容 | 仅加载用户自己配置的 domain`authState.domain`),非任意 URL |
| Token 泄露 | httpOnly cookie 防止 JS 读取token 存 SQLite 与其他凭证同等安全 |
| `session:setCookie` 被滥用 | `contextIsolation: true` + preload 白名单,仅 `electronAPI.session.setCookie` 可调用 |
| `webviewTag: true` 安全性 | Electron 官方建议谨慎使用,但本场景加载可信域名,风险可控 |
| `allowpopups` | 允许页面打开弹窗(如 OAuth弹窗共享 `defaultSession`,可信域名下可接受 |
---
## 相关文档
- [认证机制与 SavedKey 生命周期](./auth-savedkey-lifecycle.md) — savedKey / configKey 设计
- [服务启动与 Reg 同步](../../CLAUDE.md#service-startup--reg-sync) — reg 接口调用时序
---
*本文档由架构维护者负责更新*

View File

@@ -0,0 +1,152 @@
# 系统托盘逻辑与实现
> 本文档梳理 Electron 客户端**系统托盘Tray**的创建时机、图标策略、右键菜单、与主进程/渲染进程的协作,以及 macOS 开发模式下托盘不显示的缓解措施。
---
## 1. 概述
- **平台**macOS菜单栏、Windows / Linux系统托盘区
- **能力**:左键/双击显示主窗口、右键上下文菜单(显示主窗口、重启/停止服务、开机自启动、检查更新、退出)。
- **状态**:图标统一,状态通过 **tooltip** 区分(`TrayStatus`:运行中 / 已停止 / 错误 / 启动中);不按状态切换多套图标文件。
- **依赖**:主进程 `TrayManager``window/trayManager.ts`)、`ServiceManager`(托盘菜单中的重启/停止)、`AutoLaunchManager`(开机自启动)。
---
## 2. 创建时机
| 场景 | 行为 |
|------|------|
| **非 macOS****已打包** | `app.whenReady()` 内:`createWindow()` 后立即 `initTrayManager()`macOS 时先 `app.dock.show()`。 |
| **macOS 且未打包(开发)** | 不在 `whenReady` 里创建托盘;在 main 窗口 **`ready-to-show`** 回调里 `show()` 之后 **延迟 300ms** 再执行 `initTrayManager()`,以减少「从终端启动时菜单栏托盘不显示」的问题。 |
- 实现位置:`main.ts``createWindow()``mainWindow.once('ready-to-show', ...)``app.whenReady().then(...)`
- 托盘实例通过模块级变量 `trayManager` 持有,避免被回收。
---
## 3. 图标策略
### 3.1 图标文件(`public/tray/`
| 文件 | 用途 |
|------|------|
| `trayTemplate.png` | macOS 打包后22x22 黑色剪影Template Image随系统主题变色。 |
| `trayTemplate@2x.png` | macOS 打包后44x44 Retina 版。 |
| `tray.png` | Windows/Linux 托盘macOS 开发模式回退。 |
| `tray@2x.png` | macOS 开发模式优先使用(高分辨率)。 |
### 3.2 路径解析(`getIconPath`
- **打包后**`path.join(process.resourcesPath, 'tray', fileName)`(如 `*.app/Contents/Resources/tray/trayTemplate@2x.png`)。
- **开发**:使用 **`__dirname` 相对路径**,不依赖 `process.cwd()`,避免 monorepo 或从其他目录启动时图标找不到。
- 编译后 `trayManager``dist/main/window/`,故 `path.join(__dirname, '..', '..', '..', 'public', 'tray', fileName)` 指向包根目录下的 `public/tray/`
### 3.3 按平台与模式选择图标(`createTrayIcon`
| 平台 | 模式 | 行为 |
|------|------|------|
| **macOS** | 开发(`!app.isPackaged` | 使用**彩色图标**:先 `tray@2x.png`,失败则 `tray.png`;若尺寸 > 22 则缩放到 22x22。**不使用** Template Image避免菜单栏不显示。若两个文件都加载失败则用 1x1 PNG data URL 生成 22x22 占位图。 |
| **macOS** | 打包后 | 使用 **Template**:先 `trayTemplate@2x.png`,失败则 `trayTemplate.png``setTemplateImage(true)`。失败时同样回退到占位图。 |
| **Windows / Linux** | 任意 | 使用 `tray.png`(彩色)。 |
- **占位图**`createPlaceholderTrayImage(size)` 用 1x1 PNG 的 data URL 生成图并 resize 到 16~22px保证传给 `new Tray(icon)` 的始终为非空 `NativeImage`,避免因空图导致托盘不创建或不可见。
---
## 4. TrayManager 与选项
### 4.1 单例与创建
- `createTrayManager(options)`:若已有实例则先 `destroy()`,再 `new TrayManager(options)` 并赋值给模块级 `trayManager`
- `getTrayManager()`:返回当前单例或 null。
### 4.2 TrayManagerOptionsmain 传入)
| 字段 | 说明 |
|------|------|
| `onShowWindow` | 左键/双击托盘时调用:`mainWindow?.show()``mainWindow?.focus()`。 |
| `onRestartServices` | 菜单「重启服务」:调用 `serviceManager.restartAllServices()`,成功后 `trayManager.updateServicesStatus(true)`。 |
| `onStopServices` | 菜单「停止服务」:调用 `serviceManager.stopAllServices()`,然后 `trayManager.updateServicesStatus(false)`。 |
### 4.3 内部状态
- `status``TrayStatus` = `'running' | 'stopped' | 'error' | 'starting'`,用于 tooltip 文案。
- `servicesRunning`:布尔,控制「停止服务」是否 enabled、以及与 `status` 的联动。
- `autoLaunchEnabled`:来自 `AutoLaunchManager.isEnabled()`,用于菜单「开机自启动」勾选状态。
### 4.4 事件与菜单
- **click**`onShowWindow()`
- **double-click**`onShowWindow()`
- **setToolTip**:初始为应用名;`updateIcon()` 时设为 `"应用名 - 运行中/已停止/错误/启动中"`
- **setContextMenu**:由 `updateMenu()` 构建,见下表。
---
## 5. 右键菜单项
| 菜单项 | 行为 |
|--------|------|
| 显示主窗口 | `onShowWindow()` |
| (分隔线) | — |
| 重启服务 | `onRestartServices()` |
| 停止服务 | `enabled: servicesRunning`,点击 `onStopServices()` |
| (分隔线) | — |
| 开机自启动 | checkbox`checked: autoLaunchEnabled`,点击切换并调用 `autoLaunchManager.setEnabled(newEnabled)`,失败弹窗 |
| 检查更新 | 调用 `showUpdateDialogFlow()`autoUpdater |
| (分隔线) | — |
| 关于 {App} v{x} | 仅展示,`enabled: false` |
| 退出 | `app.quit()` |
---
## 6. IPC 与渲染进程同步
- **通道**`tray:updateStatus``tray:updateServicesStatus`main 中 `ipcMain.handle`)。
- **preload**`tray.updateStatus(status)``tray.updateServicesStatus(running)`
- **用途**:渲染进程可根据 Agent/服务状态调用上述 API使托盘 tooltip 与「停止服务」可用状态与界面一致。当前主进程侧在托盘菜单「重启服务」「停止服务」后会调用 `updateServicesStatus`,渲染进程若需与客户端页状态同步可调用 `tray.updateStatus(status)` / `tray.updateServicesStatus(running)`
---
## 7. Dock 图标(仅 macOS
-`app.whenReady()` 内:若 `process.platform === 'darwin' && app.dock`,则用 `getDockIconPath()` 加载 PNG开发`process.cwd()/public/icon-dock.png`,打包:`process.resourcesPath/icon-dock.png`),成功则 `app.dock.setIcon(iconImage)`
- 目的:开发模式下明确设置 Dock 图标,便于识别;与托盘图标相互独立。
---
## 8. 窗口关闭与应用退出
- **window-all-closed**
- 非 macOS直接 `app.quit()`,触发 `before-quit` → 清理进程与 DB。
- macOS不退出应用仅做服务清理MCP、lanproxy、agentRunner、fileServer 等),主窗口可关,托盘仍在,用户可通过托盘再次「显示主窗口」或「退出」。
- **activate**macOS若无窗口则再次 `createWindow()`
- **before-quit**:执行 `cleanupAllProcesses()`(含 Agent、ComputerServer、FileServer、Lanproxy、MCP、引擎等然后 `closeDb()``app.exit(0)`。托盘在进程退出时随之销毁。
---
## 9. 涉及文件汇总
| 类型 | 路径 |
|------|------|
| 托盘核心 | `src/main/window/trayManager.ts`TrayManager、图标逻辑、菜单、单例 |
| 导出 | `src/main/trayManager.ts`re-export |
| 主进程入口 | `src/main/main.ts`initTrayManager、创建时机、IPC、Dock、window-all-closed |
| 服务与自启 | `src/main/window/serviceManager.ts``src/main/window/autoLaunchManager.ts` |
| 更新 | `src/main/services/autoUpdater.ts`(检查更新) |
| Preload / 类型 | `src/preload/index.ts`tray API`src/shared/types/electron.d.ts`TrayAPI |
| 图标资源 | `public/tray/trayTemplate.png``trayTemplate@2x.png``tray.png``tray@2x.png` |
---
## 10. macOS 开发模式托盘不显示说明
- **现象**:从终端运行 `electron .` 或 npm 开发脚本时菜单栏托盘图标有时不出现日志中已打印「Tray created」「icon loaded」。
- **原因**Electron/macOS 在非 .app 打包、从命令行启动时,对菜单栏图标的显示存在已知差异。
- **当前缓解**
1. **延迟创建**:仅在 macOS 且未打包时,在 main 窗口 `ready-to-show``show()` 后延迟 300ms 再创建 Tray。
2. **开发用彩色图标**macOS 开发模式一律使用 `tray.png` / `tray@2x.png`,不用 Template Image。
3. **图标路径**:开发下用 `__dirname` 相对路径解析到 `public/tray/`,避免 cwd 不准。
4. **占位图**:图标文件缺失时仍传入非空占位图,避免 `new Tray(empty)` 导致不显示。
- **验证**:打包后的 .app 在 macOS 上托盘通常正常;若开发下仍不显示,可查看日志中 `[Tray]` 行确认使用的是彩色图标还是占位图。