CN-Codex 是一个基于 Tauri 2 + React 的 AI 编程助手桌面应用。前端使用 React + Zustand + TailwindCSS,后端使用 Rust(Tauri 框架),内置 Agent 引擎支持多轮对话、工具调用、上下文压缩等能力。
flowchart TB
subgraph frontend [前端 - React + Zustand]
App["App.tsx(根组件)"]
ChatPage["ChatPage(聊天主页)"]
MessageList["MessageList(消息列表)"]
ChatInput["ChatInput(输入框)"]
Sidebar["Sidebar(侧边栏)"]
Settings["SettingsPanel(设置面板)"]
AppStore["appStore(Zustand 主状态)"]
TauriEvents["useTauriEvents(事件桥接)"]
ApiLayer["api/ 层(invoke 封装)"]
end
subgraph backend [后端 - Rust / Tauri]
Commands["Tauri Commands(standalone_*)"]
AgentEngine["AgentEngine(Agent 引擎)"]
ThreadStore["ThreadStore(线程持久化)"]
ToolExecutor["ToolExecutor(工具执行器)"]
Compaction["Compaction(上下文压缩)"]
Adapter["Adapter(LLM 适配层)"]
HookRuntime["HookRuntime(Hook 运行时)"]
UsageDb["UsageDb(用量统计)"]
ConfigManager["ConfigManager(配置管理)"]
end
subgraph external [外部服务]
LLM["LLM API(OpenAI / Anthropic / 火山 / 智谱等)"]
FileSystem["本地文件系统"]
end
App --> TauriEvents
App --> ChatPage
ChatPage --> ChatInput
ChatPage --> MessageList
ChatPage --> ApiLayer
TauriEvents -->|"listen Tauri events"| AppStore
ApiLayer -->|"invoke"| Commands
Commands --> AgentEngine
AgentEngine --> ThreadStore
AgentEngine --> ToolExecutor
AgentEngine --> Compaction
AgentEngine --> Adapter
AgentEngine --> HookRuntime
Adapter -->|"HTTP SSE"| LLM
ToolExecutor --> FileSystem
ThreadStore -->|"JSONL"| FileSystem
UsageDb -->|"SQLite"| FileSystem
cn-codex/
├── src/ # 前端 React 源码
│ ├── main.tsx # React 入口
│ ├── App.tsx # 根组件(布局 + 初始化)
│ ├── api/ # Tauri invoke 封装
│ │ ├── standalone.ts # 核心引擎调用(thread/chat/config)
│ │ ├── app_state.ts # SQLite KV 持久化
│ │ ├── approval.ts # 审批通道
│ │ └── ...
│ ├── stores/ # Zustand 状态管理
│ │ ├── appStore.ts # 主应用状态(~1300 行)
│ │ └── settingsStore.ts # 语言/主题
│ ├── hooks/
│ │ └── useTauriEvents.ts # 后端事件 → store 桥接
│ ├── components/
│ │ ├── chat/ # 聊天 UI
│ │ ├── layout/ # 布局组件
│ │ ├── settings/ # 设置面板
│ │ ├── approval/ # 审批弹窗
│ │ └── common/ # 通用组件
│ ├── types/ # TypeScript 类型定义
│ └── i18n/ # 国际化(中/英)
├── src-tauri/ # Rust 后端
│ ├── src/
│ │ ├── lib.rs # 模块声明 + Tauri 初始化
│ │ ├── agent.rs # Agent 引擎核心
│ │ ├── thread_store.rs # 线程/会话持久化
│ │ ├── compaction.rs # 上下文压缩
│ │ ├── tool_executor.rs # 工具执行器
│ │ ├── standalone.rs # Standalone 命令
│ │ ├── config_system.rs # 配置管理
│ │ ├── hook_runtime.rs # Hook 运行时
│ │ ├── browser_automation.rs # 浏览器自动化
│ │ ├── adapter/ # LLM 适配层
│ │ ├── commands/ # Tauri 命令
│ │ ├── protocol/ # JSON-RPC 协议类型
│ │ └── usage/ # 用量统计
│ ├── Cargo.toml
│ └── tauri.conf.json
├── codey/ # 运行时资源
│ ├── config.toml # 用户配置
│ ├── sessions/ # 会话持久化(JSONL)
│ ├── skills/ # Skill 定义
│ ├── plugins/ # 插件
│ └── memories/ # 记忆存储
├── docs/ # 技术文档
└── publish.bat # Windows 发布脚本
sequenceDiagram
participant User as 用户
participant ChatPage as ChatPage
participant API as api/standalone
participant Backend as standalone_chat
participant Agent as AgentEngine.run_turn
participant LLM as LLM API
participant Events as Tauri Events
participant Store as appStore
User->>ChatPage: 输入消息并发送
ChatPage->>Store: startNewThreadWithMessage()
ChatPage->>API: standaloneChat(threadId, input, mode)
API->>Backend: invoke("standalone_chat")
Backend->>Agent: run_turn(thread_id, input, mode)
Agent->>Events: emit("turn-started")
Events->>Store: setStreaming(true), setStreamingLabel("正在处理请求...")
loop Agent 循环(最多 25 次)
Agent->>LLM: stream_completion(SSE 流式)
LLM-->>Agent: delta chunks
Agent->>Events: emit("agent-message-delta", delta)
Events->>Store: appendStreamingText(delta)
alt LLM 返回工具调用
Agent->>Events: emit("tool-calls-start", calls)
Events->>Store: addMessage(toolCalls), setStreamingLabel("执行命令: ...")
Agent->>Agent: tool_executor.execute()
Agent->>Events: emit("tool-exec-end", result)
Events->>Store: updateToolCallStatus()
else LLM 返回文本消息
Agent->>Agent: 记录 assistant 消息,结束循环
end
end
Agent->>Events: emit("turn-completed")
Events->>Store: addMessage(finalText), setStreaming(false)
Store->>ChatPage: 重渲染显示结果
sequenceDiagram
participant Agent as AgentEngine
participant Hook as HookRuntime
participant Tool as ToolExecutor
participant Events as Tauri Events
participant Store as appStore (前端)
Agent->>Agent: LLM 返回 ToolCalls
Agent->>Hook: HOOK_COMMAND_EXEC(可 block/修改参数)
alt Hook 允许
Agent->>Events: emit("tool-calls-start", [{id, name, arguments}])
Events->>Store: addMessage(toolCalls), setStreamingLabel(活动描述)
loop 每个工具调用
Agent->>Tool: execute(name, arguments)
Tool-->>Agent: 执行结果
Agent->>Events: emit("tool-exec-end", {callId, exitCode, output})
Events->>Store: updateToolCallStatus(success/failed)
end
Agent->>Events: emit("tool-calls-end")
Agent->>Hook: HOOK_POST_TOOL_USE
Agent->>Agent: 将结果加入上下文,继续循环
else Hook 阻止
Agent->>Agent: 跳过此工具调用
end
| 分组 | 字段 | 说明 |
|---|---|---|
| 初始化 | initialized, initError, retryInit |
应用启动状态 |
| 当前会话 | currentThreadId, currentTurnId |
活跃线程和 turn |
| 消息 | messages, streamingText, streamingLabel, isStreaming |
聊天消息和流式状态 |
| 模式 | chatMode(chat/goal), currentGoal |
对话模式 |
| 模型 | configuredModels, activeModelId, providers, activeProviderId |
模型配置 |
| 工作区 | workspaceCwd, configDir, configPath |
路径 |
| 项目/线程 | projects, currentProjectId, threads, threadProjectMap |
多项目管理 |
| UI | showSettings, rightPanelVisible, autoApprove |
界面状态 |
stateDiagram-v2
[*] --> Idle: 初始
Idle --> Processing: turn-started
Processing --> Generating: agent-message-delta(首次)
Processing --> ToolExec: tool-calls-start
Generating --> Organizing: streamingText > 200 字符
Organizing --> Summarizing: streamingText > 800 字符
ToolExec --> Processing: tool-calls-end
Generating --> Idle: turn-completed
Organizing --> Idle: turn-completed
Summarizing --> Idle: turn-completed
ToolExec --> Idle: turn-completed
state Processing {
[*]: streamingLabel = "正在处理请求..."
}
state Generating {
[*]: streamingLabel = "正在生成响应..."
}
state Organizing {
[*]: streamingLabel = "正在组织答案结构..."
}
state Summarizing {
[*]: streamingLabel = "正在汇总信息..."
}
state ToolExec {
[*]: streamingLabel = "执行命令: xxx"
}
Agent 引擎通过以下条件判断一个 turn 是否完成:
- LLM 返回纯文本消息(非工具调用)→ turn 结束
- 循环次数达到上限(25 次)→ 强制结束
- 用户中断(
cancel_flag被设置)→ 中断结束 - Goal 模式下 Goal 完成(agent 标记 goal 为 completed)→ 结束
flowchart LR
TurnStarted["turn-started 事件"] -->|"isStreaming = true"| Streaming["流式状态"]
Streaming -->|"turn-completed 事件"| Done["完成"]
Done -->|"isStreaming = false"| Idle["空闲"]
Streaming -->|"用户点击停止"| Interrupt["standalone_turn_interrupt"]
Interrupt -->|"后端 cancel_flag"| Done
前端通过监听 turn-completed 事件来判断处理完成:
| 事件 | 前端动作 |
|---|---|
turn-started |
isStreaming = true,显示加载动画 |
agent-message-delta |
累积文本,实时显示 |
tool-calls-start |
显示工具调用卡片 |
tool-exec-end |
更新工具状态为成功/失败 |
turn-completed |
isStreaming = false,将累积文本转为正式消息 |
- 后端错误:
standalone_chat返回Err→ 前端 catch → 显示错误 toast - 用户取消:前端调用
standalone_turn_interrupt→ 后端设置cancel_flag+ 中断活跃工具 →turn-completed事件携带中断信息 - Goal 模式错误:自动将 goal 状态恢复为
paused
LLM 的输出天然分为两种:
- 文本内容(assistant message):模型的思考和回复
- 工具调用(function call / tool_use):模型决定执行的操作
Agent 引擎的循环实现了这种交替:
flowchart TD
Start["开始 Turn"] --> BuildContext["构建上下文<br/>(system + history + user)"]
BuildContext --> CallLLM["调用 LLM(流式)"]
CallLLM --> Parse["解析响应"]
Parse -->|"CompletionResult::Message"| EmitText["发射 agent-message-delta<br/>前端实时显示文字"]
Parse -->|"CompletionResult::ToolCalls"| EmitTool["发射 tool-calls-start<br/>前端显示工具卡片"]
EmitText --> EndTurn["结束 Turn"]
EmitTool --> Execute["执行工具"]
Execute --> EmitResult["发射 tool-exec-end<br/>前端更新工具状态"]
EmitResult --> BuildContext
EndTurn --> TurnCompleted["发射 turn-completed"]
// agent.rs 中 stream_completion 的核心逻辑
while let Some(chunk) = stream.next().await {
// 解析 SSE delta
if let Some(content_delta) = parse_delta(&chunk) {
// 实时发射给前端
app_handle.emit("agent-message-delta", json!({ "delta": content_delta }));
full_content.push_str(&content_delta);
}
}前端 useTauriEvents.ts 接收并累积:
listen("agent-message-delta", (e) => {
store.appendStreamingText(e.payload.delta);
// streamingText 实时更新 → MessageList 重渲染
});工具调用以 ChatMessage 的形式存储,包含 toolCalls 数组:
interface ToolCallItem {
id: string;
name: string; // 工具名(如 "shell")
arguments: string; // JSON 参数
status: "running" | "success" | "failed";
displayLabel: string; // 显示文本(如 "ls -la")
output?: string; // 执行输出
}MessageList 对包含 toolCalls 的消息渲染为可折叠的工具卡片 UI。
| 时机 | 条件 |
|---|---|
| Pre-turn | turn 开始前,检查上次 last_single_prompt_tokens > 阈值 |
| Mid-turn | 工具调用后,单次 API prompt_tokens > 阈值 |
| Goal continuation | goal 循环继续前检查 |
| 手动 | 用户输入 /compact |
pub fn compact_threshold(config: &ConfigToml) -> u64 {
// 优先使用显式配置
if let Some(limit) = config.model_auto_compact_token_limit {
return limit;
}
// 否则取模型上下文窗口的 90%
let window = config.model_context_window.unwrap_or(128_000);
(window as f64 * 0.9) as u64
}flowchart TD
Check["检查 prompt_tokens > threshold"] -->|"是"| Start["开始压缩"]
Start --> BuildSummary["构建 summarization 请求<br/>system + history + SUMMARIZATION_PROMPT"]
BuildSummary --> CallLLM["调用 LLM 生成摘要"]
CallLLM --> CollectUser["收集 user 消息<br/>(保留最近 ~20K tokens)"]
CollectUser --> Build["build_compacted_history<br/>= 保留的 user 消息 + summary"]
Build --> Replace["thread_store.replace_messages<br/>重写 JSONL 文件"]
Replace --> Emit["发射 context-compacted 事件"]
每个线程存储为 codey/sessions/{thread_id}.jsonl,每行一个事件:
| 事件类型 | 内容 |
|---|---|
thread_meta |
线程元信息(name、创建时间) |
thread_goal_set |
Goal 设置 |
turn_start |
Turn 开始(id、mode) |
message |
单条消息(role、content) |
turn_end |
Turn 结束(usage、changed_files) |
thread_update |
线程属性更新 |
codey/usage.db 中的 app_state 表存储前端持久化数据:
- 供应商配置、模型列表
- 项目关联、活跃模型
- UI 偏好(auto_approve、theme、locale)
支持多种 LLM API 格式:
| 适配器 | wire_api | 支持的服务 |
|---|---|---|
ChatCompletions |
chat |
OpenAI、火山引擎、智谱、百度 |
Responses |
responses |
OpenAI Responses API |
Anthropic |
anthropic |
Claude |
Google |
google |
Gemini |
所有适配器统一输出 CompletionResult:
enum CompletionResult {
Message {
content: String,
usage: TokenUsage,
},
ToolCalls {
calls: Vec<ToolCallRequest>,
usage: TokenUsage,
},
}| 技术 | 版本 | 用途 |
|---|---|---|
| React | 18.3 | UI 框架 |
| Zustand | latest | 状态管理 |
| TailwindCSS | 4 | 样式 |
| Vite | 6 | 构建工具 |
| TypeScript | 5.7 | 类型系统 |
| react-intl | latest | 国际化 |
| react-markdown | latest | Markdown 渲染 |
| Vitest | 4.1 | 单元测试 |
| 技术 | 版本 | 用途 |
|---|---|---|
| Rust | 1.85+ | 系统语言 |
| Tauri | 2 | 桌面框架 |
| tokio | full | 异步运行时 |
| reqwest | latest | HTTP 客户端 |
| rusqlite | bundled | SQLite |
| serde/serde_json | latest | 序列化 |
| tracing | latest | 日志 |
通过 publish.bat 脚本:
- 清理
publish/目录 - 安装前端依赖(pnpm install)
- Tauri Release 构建(
pnpm tauri build --no-bundle) - 复制产物:
CN-Codex.exe+ DLL - 复制运行时资源:skills、plugins(排除敏感配置)
- 打包 Node.js 便携版(用于 shell 工具)
- 生成可分发的
publish/目录