Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
## Project Structure & Module Organization
- `cmd/tui/main.go` 是TUI模式的主要入口,负责初始化配置和启动终端用户界面。
- `cmd/server/main.go` 是服务器模式的入口,用于验证服务组装,目前仅作为占位符存在。
- `internal/tui/bootstrap/` 负责TUI启动前准备,包括工作区解析、配置初始化和程序装配。
- `internal/tui/core/` 包含TUI的核心逻辑,实现了Bubble Tea ELM架构(Model-Update-View)。
- `internal/tui/infra/` 包含TUI的基础设施层,负责与后端服务交互。
- `internal/tui/state/` 保存TUI纯状态结构,如聊天历史、窗口尺寸和运行时标记。
- `internal/tui/services/` 提供TUI的本地服务适配层,负责组装后端 service/provider/repository/tools 并暴露统一接口。
- `internal/server/domain/` 定义了核心领域模型和接口,遵循依赖倒置原则。
- `internal/server/service/` 实现了应用服务层,负责编排业务流程。
- `internal/server/infra/provider/` 包含具体的外部服务提供方实现,如ModelScopeProvider。
Expand Down
183 changes: 69 additions & 114 deletions docs/TUI_REFINED_ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,149 +1,104 @@
# NeoCode TUI 增强版架构设计指南 (细化版)
# NeoCode TUI 架构说明

## 核心设计

本架构基于 **TEA (The Elm Architecture)** 模式,遵循“**单向数据流**”与“**逻辑/视图/通信彻底解耦**”的原则。
- **状态驱动**:界面是状态(State)的函数。
- **异步解耦**:所有耗时操作(网络、I/O)必须通过 `tea.Cmd` 异步执行。
- **物理隔离**:TUI 层严禁引用 `internal/server` 目录下的任何非 API 结构。
当前 TUI 仍然基于 Bubble Tea 的 TEA 模式,但重构后职责边界已经从旧版的 `app + infra` 结构调整为 `bootstrap + core + state + components + services`:

- 状态驱动:界面输出由 `core.Model` 持有的状态决定。
- 异步更新:模型响应、工具执行、记忆刷新都通过 `tea.Cmd` 回流到 `Update`。
- 依赖收口:TUI 对后端实现的依赖统一集中在 `internal/tui/services/`,`core` 和 `components` 不直接触碰 `internal/server/...`。
- 本地组装:当前 TUI 默认通过本地 `service + provider + repository + tools` 组装聊天能力,而不是通过独立的 gRPC/HTTP 客户端。

---

## 细化后的六层架构模型
## 五层结构

我们将整个 TUI 客户端细化为六个逻辑层,每个层级有严格的边界限制:
### 入口层 - `cmd/tui/`

### 入口层 (Entry) - `cmd/tui/`
- 解析命令行参数,目前支持 `--workspace`。
- 负责启动前准备:设置终端 UTF-8、准备工作区、交互式检查 API Key、加载配置与人设。
- 调用 `bootstrap.NewProgram(...)` 构建 Bubble Tea Program 并运行。

* **职责**:程序的物理起点。
* **具体任务**:
* 解析命令行启动参数(如 `--debug`, `--config`)。
* 调用 `bootstrap` 层获取初始化好的 `Program` 实例。
* 启动 `tea.NewProgram` 并处理最终的退出错误。
* **禁止**:严禁编写任何业务逻辑或具体的 UI 布局代码。
### 启动层 - `internal/tui/bootstrap/`

### 启动与注入层 (Bootstrap) - `internal/tui/bootstrap/`
- `setup.go` 负责工作区解析、配置文件初始化、API Key 校验前的交互式引导。
- `runtime.go` 负责创建 `services.ChatClient`,再注入 `core.NewModel(...)`。
- 这一层是依赖装配点,后续如果要切换成远端 API 客户端,也应该优先在这里替换实现。

* **职责**:系统的“总装车间”,负责**依赖注入 (DI)**。
* **具体任务**:
* 读取 `config.yaml` 配置文件。
* 实例化 `services` 层(如 `APIClient`, `Logger`)。
* 将这些服务注入到 `app` 层中。
* **关键作用**:通过在这一层注入不同的实现,可以轻松实现“离线测试模式”或“Mock 测试”。
### 状态机层 - `internal/tui/core/`

### 应用逻辑层 (App/Core) - `internal/tui/app/`
- `model.go` 定义顶层 `Model`,聚合 UI 状态、聊天状态、Bubble 组件实例和流式通道。
- `update.go` 负责按键处理、命令解析、消息流更新、工具调用闭环、记忆清理与模型切换。
- `view.go` 负责顶层布局,组合状态栏、聊天区、帮助区、输入区。
- `msg.go` 定义流式输出、工具结果、帮助切换等内部消息。

* **职责**:状态机中心,负责调度 `Update` 和 `View`。
* **具体文件**:
* `model.go`: 定义顶层 `Model`,聚合各子模块状态。
* `update.go`: 核心业务逻辑路由器。根据收到的 `tea.Msg` 决定调用哪个 Service 或更新哪个 State。
* `view.go`: 顶层布局管理器。决定 `Header`, `Content`, `Footer` 的排版位置(使用 Lipgloss)。
* `msg.go`: 定义所有自定义消息类型(如 `AIGeneratingMsg`, `SocketErrorMsg`)。
### 纯状态层 - `internal/tui/state/`

### 纯状态层 (State) - `internal/tui/state/`
- `ui_state.go` 保存窗口尺寸、当前模式、自动滚动等纯 UI 状态。
- `chat_state.go` 保存消息历史、当前模型、记忆统计、命令历史、工作区和配置状态。
- 状态层只保留结构体定义,不承载业务流程。

* **职责**:**数据容器**。仅存放纯粹的 Go 结构体。
* **具体任务**:
* `ui_state.go`: 记录 UI 细节(如:窗口宽高、当前焦点在哪个输入框、滚动条位置)。
* `chat_state.go`: 存放当前的聊天历史、AI 思考中的临时文本。
* **准则**:这一层**不含任何方法**,只存放数据,确保状态的可序列化和易测试性。
### 组件与适配层

### 视图组件层 (Components) - `internal/tui/components/`
#### 视图组件 - `internal/tui/components/`

* **职责**:**原子级 UI 渲染器**(“傻瓜组件”)。
* **具体任务**:
* `code_block.go`: 负责代码高亮渲染。
* `status_bar.go`: 负责底部状态栏的样式。
* **原则**:
* **输入**:仅接收基础数据或 State 结构体。
* **输出**:返回渲染好的字符串(`string`)。
* **禁止**:组件内严禁发起任何网络请求或修改全局状态。
- 负责状态栏、输入框、帮助面板、消息列表、代码块高亮等纯渲染逻辑。
- 输入是基础数据或轻量结构,输出是渲染后的字符串。
- 不发起请求,不修改全局状态。

### 服务对接层 (Services) - `internal/tui/services/`
#### 服务适配 - `internal/tui/services/`

* **职责**:**外交部**。负责与后端 Server 或系统环境通信。
* **具体任务**:
* `api_client.go`: 封装对 `internal/server/transport` 的调用。
* `file_service.go`: 处理本地文件的临时读取。
* **原则**:所有方法必须返回 `tea.Cmd` 或在回调中触发 `tea.Msg`。
- `api_client.go` 当前不是网络客户端,而是本地聊天适配器:直接组装 `internal/server/service`、`internal/server/infra/provider`、`internal/server/infra/repository` 和 `internal/server/infra/tools`。
- 同时负责工作区根目录、提供商/模型规范化、工具调用封装、记忆统计等 TUI 依赖的运行时能力。
- `core` 只依赖这里暴露的接口和数据结构,不关心底层是本地实现还是远程实现。

---

## 标准数据流向 (Lifecycle)

以“用户发送消息”为例:
1. **用户按下回车**:`app/update.go` 捕获到按键事件。
2. **更新本地状态**:`update.go` 将用户输入追加到 `state/chat_state.go`,并返回一个 `tea.Cmd` 触发发送请求。
3. **服务调用**:`services/api_client.go` 执行异步 API 调用。
4. **结果反馈**:API 返回结果后,封装成 `APIResponseMsg` 发回给 `app/update.go`。
5. **界面重绘**:`app/view.go` 根据更新后的 `state` 重新生成字符串,渲染到屏幕。



#### 用户视角:一个“发送消息”动作的全层级演变


假设用户在终端输入了 "你好" 并按下 回车键。以下是各层级像齿轮一样咬合转动的过程:
第一阶段:输入捕获 (Entry -> App)
* 用户看到:手指按下回车。
* 层级变化:入口层 (Entry) 的底层的 tea.Program 捕获到操作系统发来的按键信号,并将其包装成一个 KeyMsg
发送给 应用逻辑层 (App) 的 Update 函数。


第二阶段:本地状态更新 (App -> State -> View)
* 层级变化:
1. App (Update):识别出这是回车键。
2. State:Update 把 state.ChatState.InputBuffer 里的 "你好" 提取出来,清空输入框,并把这条消息塞进
state.ChatState.History 数组。
3. View:Runtime 立即调用 View。View 发现 History 里多了一条消息,于是命令 组件层 (Components) 渲染一个新的气泡。
* 用户看到:屏幕上自己发送的 "你好" 瞬间出现在了聊天记录区域,且输入框变空了。(此时 AI
还没说话,但用户感觉响应非常快)


第三阶段:发起异步请求 (App -> Services)
* 层级变化:
1. App (Update):在更新完本地状态的同时,返回一个 tea.Cmd。这个命令指向 服务层 (Services) 的 SendToAI 函数。
2. Services:在后台偷偷发起网络请求,把 "你好" 发给后端的 Go Server。
## 当前数据流

以“用户输入一条消息并触发工具调用”为例:

第四阶段:AI 响应回流 (Services -> App -> State -> View)
* 层级变化:
1. Services:收到后端返回的 AI 回复(如 "你好!我是 NeoCode"),将其包装成一个 AIResponseMsg 投递回App
2. App (Update):收到这个消息,再次修改 State,把 AI 的话加入 History
3. View:Runtime 再次触发 View 重绘
* 用户看到:屏幕上刷新出了 AI 的回复
1. 用户在输入框编辑内容,`core/update.go` 处理按键并更新 `textarea` 状态。
2. 按下 `F5` 或 `F8` 后,`handleSubmit()` 将用户消息写入 `chat_state`,然后触发 `streamResponse(...)`。
3. `services.ChatClient.Chat(...)` 启动本地聊天服务,流式返回模型输出
4. `StreamChunkMsg` 持续追加 assistant 内容;`StreamDoneMsg` 在流结束时检查最后一条 assistant 消息是否是工具调用 JSON
5. 若检测到 `{"tool":"...","params":{...}}`,TUI 会通过 `services.ExecuteToolCall(...)` 执行工具,并把工具结果重新注入为 system 上下文
6. 模型基于新的上下文继续生成,直到得到最终自然语言回复

---

## 细化后的目录结构
## 当前目录结构

```text
internal/tui/
├── bootstrap/ # 依赖装配 (Runtime 构造器)
│ └── runtime.go
├── app/ # 状态机核心 (TEA 循环)
│ ├── model.go # 顶层模型定义
│ ├── update.go # 消息分发逻辑
│ ├── view.go # 顶层布局 (Layout)
│ ├── msg.go # 消息类型定义
│ └── keymap.go # 快捷键配置
├── state/ # 纯状态定义 (数据结构)
│ ├── ui_state.go # 窗口、焦点等状态
│ └── chat_state.go # 聊天记录等数据
├── components/ # 纯 UI 组件 (Lipgloss 渲染)
│ ├── code_block.go # 代码块组件
│ ├── input_box.go # 输入框增强
│ └── status_bar.go # 状态栏组件
└── services/ # 外部适配器 (API/I/O)
├── api_client.go # 后端通信
└── config_svc.go # 配置读取
├── bootstrap/ # 启动准备与依赖装配
│ ├── runtime.go
│ └── setup.go
├── components/ # 纯渲染组件
│ ├── code_block.go
│ ├── help.go
│ ├── input_box.go
│ ├── message_list.go
│ └── statusbar.go
├── core/ # Bubble Tea 状态机
│ ├── model.go
│ ├── msg.go
│ ├── update.go
│ └── view.go
├── services/ # 本地服务适配与运行时能力
│ ├── api_client.go
│ └── runtime_services.go
└── state/ # 纯状态定义
├── chat_state.go
└── ui_state.go
```

---

## 开发守则 (Constraints)
## 约束建议

1. **禁止跨层修改**:`components` 里的代码绝对不能修改 `state` 里的数据,必须通过 `Update` 函数统一处理。
2. **样式与逻辑分离**:所有的颜色、边距(Lipgloss 样式)应在 `components` 中定义,`app/view.go` 仅负责大框架的拼装。
3. **零后端业务依赖**:TUI 只能依赖 `api/proto` 中定义的结构。如果后端修改了业务逻辑,只要 API 不变,TUI 代码应保持一行不改。
4. **异步命令化**:任何可能超过 10ms 的操作(读取大文件、调用 AI)必须封装在 `services` 层返回 `tea.Cmd`。
1. `core` 不直接引用 `internal/server/...`,所有后端能力统一经 `services` 暴露。
2. `components` 只做渲染,不做状态修改和副作用。
3. `state` 只放结构体,避免把流程控制重新塞回状态层。
4. 与模型、工具、工作区、记忆相关的新能力,优先落在 `services` 或 `bootstrap`,不要堆进 `cmd/tui/main.go`。
5. 如果未来切换到远端 API,优先替换 `services.ChatClient` 实现,尽量不改 `core` 的 Update/View 逻辑。
24 changes: 13 additions & 11 deletions docs/detailed_architecture_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,33 @@

## 3. 💡 深度解析:为什么需要 API 和 Transport?(举个例子)

很多同学会问:我直接在 TUI 里调 Service 不行吗?为什么要搞这么复杂
很多同学会问:我直接在 TUI 里调 Service 不行吗?为什么还要分层

**我们举个“点快餐”的例子:**

1. **api/proto (菜单标准)**:
- 菜单上规定:点“巨无霸”必须说明“是否加生菜”和“份数”。
- 这就是 `api/proto` 的作用。它规定了外界(TUI)和内部(Service)说话的**统一格式**。没有它,外界乱点餐(发了一堆乱七八糟的数据),厨师(Service)就没法干活
- 在 NeoCode 里,`api/proto` 更像未来远程化时会用到的菜单标准;当前本地 TUI 默认并不通过它发请求,但它仍然适合作为未来 HTTP/gRPC 接口的契约来源

2. **Transport (传菜员)**:
- 假设今天你在店里吃(TUI 和 Server 在同一个程序里),传菜员直接把单子递给厨师
2. **Transport / Adapter (传菜员)**:
- 假设今天你在店里吃(TUI 和 Server 在同一个程序里),传菜员可以就在后厨门口,把单子直接递给厨师
- 假设明天你用手机点外卖(TUI 在你手机上,Server 在云端),传菜员就要通过网络把单子传过去。
- **Transport 层就是这个传菜员**。它屏蔽了“怎么传”的细节。Service 永远只需要安心做菜,不需要管单子是从柜台递进来的,还是从外卖平台发过来的
- 当前 TUI 的这个“传菜员”主要落在 `internal/tui/services/`:它把本地 `service/provider/repository/tools` 组装成统一的聊天客户端接口,屏蔽底层到底是本地调用还是未来远程调用

**现阶段的意义**:
虽然目前项目是“本地直接跑”,但我们把这两个层级架设好,是为了以后如果我们想做一个**网页版**、**手机 App 版**或者**VSCode 插件版**,我们只需要增加一个新的 `Transport` 实现(比如 HTTP 或 gRPC),而核心的 `Service`(大脑逻辑)一行代码都不用改!
虽然目前项目是“本地直接跑”,但我们依然保留了 `transport` 和 `api/proto` 的演进空间。以后如果想做**网页版**、**手机 App 版**或者**VSCode 插件版**,可以新增远端 Adapter/Transport,而尽量保持现有 `service` 逻辑不变。

---

## 4. 客户端架构 (TUI 端)

- **位置**:`internal/tui/`
- **模式**:基于 Bubble Tea 的 Model-Update-View (MUV) 模式。
- **Core (`core/`)**: 状态机。记录“当前 AI 是在思考还是在输入”。
- **Infra (`infra/`)**: 客户端的“通讯员”。负责通过网络打电话给 Server 层。
- **Components (`components/`)**: UI 零件。如彩色的进度条、代码高亮块。
- **Bootstrap (`bootstrap/`)**:启动前准备,负责工作区、配置文件和 API Key 引导。
- **Core (`core/`)**:状态机。处理按键、命令、流式响应、工具调用闭环。
- **State (`state/`)**:纯状态结构,如消息历史、窗口尺寸、当前模型、记忆统计。
- **Services (`services/`)**:客户端的适配层。当前以本地组装方式调用后端 service/provider/repository/tools。
- **Components (`components/`)**:UI 零件,如状态栏、帮助面板、消息列表、代码高亮块。

---

Expand All @@ -95,7 +97,7 @@
│ │ ├── infra/ # 【重要】具体工具的实现 (调AI、写文件、跑命令)
│ │ ├── service/ # 【核心】编排逻辑 (AI如何思考、如何连贯动作)
│ │ └── transport/ # 【柜台】处理外界请求的入口
│ └── tui/ # 界面展示逻辑
│ └── tui/ # 终端界面与本地适配层
└── data/ # 记忆数据库
```

Expand All @@ -113,6 +115,6 @@
---

## 7. 核心开发守则
1. **禁止跨层调用**:TUI 绝对不能绕过 Service 直接去读文件
1. **禁止跨层调用**:`core` 和 `components` 不能绕过 `tui/services` 直接依赖后端实现或工具实现
2. **依赖倒置**:Service 只依赖 Domain 里的接口,不依赖 Infra 里的具体实现。
3. **安全第一**:所有 `infra/tools` 里的命令执行,必须经过安全过滤。
Loading