chore: initialize qiming workspace repository
This commit is contained in:
@@ -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 都会触发一次进程冷启动。
|
||||
- **rcoder(Tauri)** 在 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 | ~10ms(qimingcode 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 后续请求已跳过(1–2ms) |
|
||||
| **ensureMemoryReady** | **~500ms** | 仅冷路径:memory index dirty 时做 fileSync;已通过 init 时后台预执行 `ensureMemoryReadyForSession()` 减少首包等待 |
|
||||
| **getOrCreateEngine (engine.init)** | **~6s** | qimingcode 进程冷启动;复用预热池或同 project 复用后降至 0ms 级 |
|
||||
| **同 project 后续请求** | **1–2ms** | 引擎复用生效,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 仅发生一次。
|
||||
@@ -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. 文档与运维流程新增子模块升级/回滚步骤。
|
||||
@@ -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) - 三区模型、环境变量
|
||||
@@ -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 变更记录
|
||||
|
||||
---
|
||||
|
||||
*本文档由架构维护者负责更新*
|
||||
*如有疑问,请查阅具体文档或联系架构团队*
|
||||
@@ -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 格式、索引机制
|
||||
@@ -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) - 三区模型、环境变量
|
||||
@@ -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. 分阶段实施
|
||||
|
||||
### 阶段 A:Schema 冻结(文档先行)
|
||||
|
||||
- 冻结 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)
|
||||
@@ -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 架构参考
|
||||
@@ -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 API,savedKey 由函数内部从 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 个测试用例,覆盖以下场景:
|
||||
|
||||
### hasRequiredQuickInitFields(10 cases)
|
||||
|
||||
- 必填字段齐全 / 全字段齐全
|
||||
- 缺 serverHost / 缺 savedKey / 空字符串
|
||||
- null / undefined / 非 object / 字段类型错误
|
||||
|
||||
### readQuickInitConfig(19 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
@@ -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`
|
||||
- 保留新代码路径,不做数据迁移回退(无存储格式变化)
|
||||
|
||||
@@ -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) - 三区模型、环境变量
|
||||
@@ -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 启动 → autoReconnect(App.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)
|
||||
@@ -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 版本化路径 |
|
||||
| 下载安装包 | 仅 OSS(yml 必须由 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 自带的 yml;yml 内下载地址为相对路径(相对 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 | 推送更新状态变化 |
|
||||
@@ -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) — 快捷初始化与依赖步骤不可跳过的约定。
|
||||
@@ -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` 内注释及导出接口。
|
||||
|
||||
---
|
||||
|
||||
*本文档用于留存需求与行为约定,便于后续维护与排查。*
|
||||
@@ -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-id,Linux 下可用 `dbus-uuidgen` 重新生成
|
||||
2. **Windows 系统更新**:MachineGuid 在重大系统更新后极少数情况下可能变化
|
||||
3. **macOS 权限**:IOPlatformUUID 无需额外权限,区别于 IOPlatformSerialNumber(新版 macOS 受限)
|
||||
|
||||
---
|
||||
|
||||
*参考: 此方案为 SaaS 客户端常用的设备标识模式*
|
||||
@@ -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_TOKEN,domain_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 域名必须为 true(Chromium 要求 `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 关联的 URL(e.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 接口调用时序
|
||||
|
||||
---
|
||||
|
||||
*本文档由架构维护者负责更新*
|
||||
@@ -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 TrayManagerOptions(main 传入)
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `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]` 行确认使用的是彩色图标还是占位图。
|
||||
Reference in New Issue
Block a user