Skip to content

Commit 1289071

Browse files
authored
Merge pull request #391 from pionxe/main
feat(architecture): [EPIC-INT-01C] 斩断直连实现纯净架构,引入网关无感自动拉起与高可用加固
2 parents 8aaa5de + 9bc0579 commit 1289071

44 files changed

Lines changed: 2558 additions & 803 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
本文件是本仓库的 AI 协作规则。任何 AI 在本项目中进行改写、续写、重构、修复、补测试或补文档时,都应优先遵守本文件。
44

55
## 1. 任务目标
6-
- 本仓库的目标是实现 `NeoCode Coding Agent MVP`
7-
- 当前主链路必须始终围绕以下闭环保持可用:
8-
`用户输入 -> Agent 推理 -> 调用工具 -> 获取结果 -> 继续推理 -> UI 展示`
6+
- 本仓库的目标是实现 `NeoCode Coding Agent`
7+
- 系统已完成控制面与数据面解耦,当前主链路必须始终围绕以下闭环保持可用:
8+
`用户输入(TUI) -> 网关中继(Gateway) -> Agent推理(Runtime) -> 调用工具(Tools) -> 结果回传 -> UI展示`
99
- 做改动时,优先保证主链路可运行、模块边界清晰、实现可验证。
1010

1111
## 2. 最高优先级规则
1212
- 不要为了“可能兼容旧版本”破坏当前架构;若新设计已确定,优先直接切换到新实现。
1313
- 不允许过度设计、过度包装
1414
- 项目中可能存在语义不清的地方,必须要谨慎分析
15-
- 不要跨层直连;新功能默认沿 `TUI -> Runtime -> Provider / Tool Manager` 主链路设计。
15+
- **强制编码准则 (防乱码)**:所有文件的读取、修改、重写操作必须强制使用标准 **UTF-8 (无 BOM)** 编码。严禁使用破坏多字节字符的正则替换;严禁在输出中文注释时出现截断或混入 GBK 等其他编码。发现乱码先修编码再修逻辑。
16+
- 不要跨层直连;新功能默认沿 `TUI -> Gateway -> Runtime -> Provider / Tool Manager` 主链路设计。
1617
- 不要把模型厂商差异泄漏到 `runtime``tui` 或上层调用方。
1718
- 不要在 `runtime``tui` 里直接写工具执行逻辑;所有可被模型调用的能力必须进入 `internal/tools`
1819
- 不要把会话状态、消息历史、工具调用记录散落到 UI;这些状态优先由 `runtime` 管理。
@@ -23,13 +24,14 @@
2324

2425
### 3.1 关键目录
2526
- `cmd/neocode`:CLI 入口。
26-
- `internal/app`:应用装配与 bootstrap,负责连接 config、provider、tools、runtime、tui。
27-
- `internal/config`:配置模型、YAML 加载、环境变量管理、配置校验和并发安全访问。
28-
- `internal/provider`:provider 抽象、领域模型和各厂商适配器。
29-
- `internal/runtime`:ReAct 主循环、事件流、Prompt 编排、token 累积与自动压缩触发。
30-
- `internal/session`:会话领域模型、存储抽象与 JSON 持久化实现。
27+
- `internal/app`:应用装配与 bootstrap,负责组装 Gateway、Runtime、TUI 等组件。
28+
- `internal/config`:配置模型、YAML 加载、环境变量管理及校验。
29+
- `internal/tui`:纯 UI 渲染层、Bubble Tea 状态机。仅负责消费事件并展示,不存业务状态。
30+
- `internal/gateway`:协议路由中枢。负责 IPC/网络监听、JSON-RPC 归一化、ACL 鉴权和流式事件中继。
31+
- `internal/runtime`:业务大脑。负责 ReAct 循环、事件流、Prompt 编排、Token 累积与压缩触发。不接触 UI。
32+
- `internal/provider`:各厂商模型适配器、请求组装与流式响应解析。
33+
- `internal/session`:会话领域模型、存储抽象与 JSON/SQLite 持久化。
3134
- `internal/tools`:工具契约、注册表、参数校验和具体工具实现。
32-
- `internal/tui`:Bubble Tea 状态机、渲染层、Slash Command 和事件桥接。
3335
- `docs`:架构、配置、事件流、会话持久化等说明文档。
3436

3537
### 3.2 模块职责

README.md

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,11 @@ $env:QINIU_API_KEY = "your_key_here"
100100
go run ./cmd/neocode --workdir /path/to/workspace
101101
```
102102

103-
运行模式切换(默认 `local`
103+
Gateway 转发与自动拉起说明
104104

105-
```bash
106-
go run ./cmd/neocode --runtime-mode local
107-
go run ./cmd/neocode --runtime-mode gateway
108-
```
109-
110-
说明:
111-
112-
- `--runtime-mode` 仅影响当前进程,不会回写 `config.yaml`
113-
- `gateway` 模式会通过本地 Gateway(优先 IPC)转发 runtime 请求与事件流
114-
- 若 Gateway 不可达或握手失败会直接报错退出(Fail Fast),不会自动回退到 `local`
105+
- `neocode` 默认通过本地 Gateway(优先 IPC)转发 runtime 请求与事件流
106+
- 启动时会先探测本地网关;若未运行会自动后台拉起并等待就绪(无感)
107+
- 若自动拉起后仍不可达或握手失败,会直接报错退出(Fail Fast)
115108

116109
### 4) 首次使用与常用命令
117110
- `/help`:查看命令帮助
@@ -144,7 +137,7 @@ go run ./cmd/neocode --runtime-mode gateway
144137

145138
- API Key 通过环境变量注入,不写入 `config.yaml`
146139
- `--workdir` 只影响当前运行,不会回写到配置文件
147-
- `--runtime-mode` 默认 `local`,用于灰度切换到 `gateway` 模式
140+
- TUI 默认通过 Gateway 连接 runtime,启动时会自动探测并在必要时后台拉起网关
148141

149142
详细配置请参考:[docs/guides/configuration.md](docs/guides/configuration.md)
150143

docs/guides/configuration.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,20 @@ $env:GEMINI_API_KEY = "AI..."
246246

247247
## CLI 运行参数覆盖
248248

249-
工作目录与运行模式都不写入 `config.yaml`,只通过启动参数覆盖:
249+
工作目录不写入 `config.yaml`,只通过启动参数覆盖:
250250

251251
```bash
252252
go run ./cmd/neocode --workdir /path/to/workspace
253-
go run ./cmd/neocode --runtime-mode local
254-
go run ./cmd/neocode --runtime-mode gateway
255253
```
256254

257255
说明:
258256

259257
- `--workdir` 只影响本次进程
260258
- 不会回写到 `config.yaml`
261259
- 工具根目录与 session 隔离都会使用该工作区
262-
- `--runtime-mode` 默认为 `local`,可切换为 `gateway`
263-
- `gateway` 模式会通过本地 Gateway(优先 IPC)转发 runtime 请求
264-
- 连接或握手失败会直接退出(Fail Fast),不会自动回退到 `local`
260+
- TUI 默认通过本地 Gateway(优先 IPC)转发 runtime 请求
261+
- 启动时会先探测本地网关;若未运行会自动后台拉起并等待就绪
262+
- 若自动拉起后仍连接或握手失败会直接退出(Fail Fast)
265263

266264
## 常见错误
267265

internal/app/bootstrap.go

Lines changed: 84 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package app
22

33
import (
44
"context"
5-
"errors"
65
"log"
76
"path/filepath"
87
"strings"
@@ -36,13 +35,6 @@ import (
3635

3736
const utf8CodePage = 65001
3837

39-
const (
40-
// RuntimeModeLocal 表示继续使用进程内 runtime 直连模式。
41-
RuntimeModeLocal = "local"
42-
// RuntimeModeGateway 表示通过 Gateway JSON-RPC 转发 runtime 调用。
43-
RuntimeModeGateway = "gateway"
44-
)
45-
4638
var (
4739
setConsoleOutputCodePage = platformSetConsoleOutputCodePage
4840
setConsoleInputCodePage = platformSetConsoleInputCodePage
@@ -60,19 +52,24 @@ var (
6052

6153
// BootstrapOptions 描述应用启动时可注入的运行时选项。
6254
type BootstrapOptions struct {
63-
Workdir string
64-
RuntimeMode string
55+
Workdir string
6556
}
6657

6758
type memoExtractorScheduler interface {
6859
ScheduleWithExtractor(sessionID string, messages []providertypes.Message, extractor memo.Extractor)
6960
}
7061

7162
type runtimeWithClose interface {
72-
agentruntime.Runtime
63+
services.Runtime
7364
Close() error
7465
}
7566

67+
type bootstrapSharedBundle struct {
68+
Config config.Config
69+
ConfigManager *config.Manager
70+
ProviderSelection *configstate.Service
71+
}
72+
7673
func newMemoExtractorAdapter(
7774
factory agentruntime.ProviderFactory,
7875
cm *config.Manager,
@@ -129,35 +126,13 @@ func EnsureConsoleUTF8() {
129126
_ = setConsoleInputCodePage(utf8CodePage)
130127
}
131128

132-
// BuildRuntime 构建 CLI 与 TUI 共用的运行时依赖
133-
func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, error) {
134-
runtimeMode, err := resolveBootstrapRuntimeMode(opts.RuntimeMode)
129+
// BuildGatewayServerDeps 构建 Gateway 服务端运行时依赖,包含 runtime/tool/session 全栈能力
130+
func BuildGatewayServerDeps(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, error) {
131+
sharedDeps, providerRegistry, modelCatalogs, err := BuildSharedConfigDeps(ctx, opts)
135132
if err != nil {
136133
return RuntimeBundle{}, err
137134
}
138-
139-
defaultCfg, err := bootstrapDefaultConfig(opts)
140-
if err != nil {
141-
return RuntimeBundle{}, err
142-
}
143-
144-
loader := config.NewLoader("", defaultCfg)
145-
manager := config.NewManager(loader)
146-
if _, err := manager.Load(ctx); err != nil {
147-
return RuntimeBundle{}, err
148-
}
149-
150-
providerRegistry, err := builtin.NewRegistry()
151-
if err != nil {
152-
return RuntimeBundle{}, err
153-
}
154-
modelCatalogs := providercatalog.NewService(manager.BaseDir(), providerRegistry, nil)
155-
providerSelection := configstate.NewService(manager, providerRegistry, modelCatalogs)
156-
if _, err := providerSelection.EnsureSelection(ctx); err != nil {
157-
return RuntimeBundle{}, err
158-
}
159-
160-
cfg := manager.Get()
135+
cfg := sharedDeps.Config
161136

162137
toolRegistry, toolsCleanup, err := buildToolRegistry(cfg)
163138
if err != nil {
@@ -184,7 +159,7 @@ func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, er
184159

185160
// Session Store 绑定到启动时的 workdir 哈希分桶,整个应用生命周期内不可变。
186161
// 这意味着所有会话都归属到启动时指定的项目目录下,运行时不会因配置变更而迁移存储位置。
187-
sessionStore = agentsession.NewStore(loader.BaseDir(), cfg.Workdir)
162+
sessionStore = agentsession.NewStore(sharedDeps.ConfigManager.BaseDir(), cfg.Workdir)
188163

189164
// 启动时自动清理过期会话,避免数据库无限膨胀。
190165
if _, err := cleanupExpiredSessions(ctx, sessionStore, agentsession.DefaultSessionMaxAge); err != nil {
@@ -197,7 +172,7 @@ func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, er
197172
var contextBuilder agentcontext.Builder = agentcontext.NewBuilderWithToolPoliciesAndSummarizers(toolRegistry, toolRegistry)
198173
var memoSvc *memo.Service
199174
if cfg.Memo.Enabled {
200-
memoStore := memo.NewFileStore(loader.BaseDir(), cfg.Workdir)
175+
memoStore := memo.NewFileStore(sharedDeps.ConfigManager.BaseDir(), cfg.Workdir)
201176
memoSource := memo.NewContextSource(memoStore)
202177
var sourceInvl func()
203178
if invalidator, ok := memoSource.(interface{ InvalidateCache() }); ok {
@@ -212,15 +187,15 @@ func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, er
212187
}
213188

214189
runtimeSvc := agentruntime.NewWithFactory(
215-
manager,
190+
sharedDeps.ConfigManager,
216191
toolManager,
217192
sessionStore,
218193
providerRegistry,
219194
contextBuilder,
220195
)
221196
runtimeSvc.SetSessionAssetStore(sessionStore)
222197
runtimeSvc.SetUserInputPreparer(agentruntime.NewSessionInputPreparer(sessionStore, sessionStore))
223-
runtimeSvc.SetSkillsRegistry(buildSkillsRegistry(ctx, loader.BaseDir()))
198+
runtimeSvc.SetSkillsRegistry(buildSkillsRegistry(ctx, sharedDeps.ConfigManager.BaseDir()))
224199
runtimeSvc.SetAutoCompactThresholdResolver(runtimeAutoCompactThresholdResolverFunc(
225200
func(ctx context.Context, cfg config.Config) (int, error) {
226201
resolution, err := configstate.ResolveAutoCompactThreshold(ctx, cfg, modelCatalogs)
@@ -235,55 +210,109 @@ func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, er
235210
if memoSvc != nil && cfg.Memo.AutoExtract {
236211
runtimeSvc.SetMemoExtractor(newMemoExtractorAdapter(
237212
providerRegistry,
238-
manager,
213+
sharedDeps.ConfigManager,
239214
memo.NewAutoExtractor(nil, memoSvc, time.Duration(cfg.Memo.ExtractTimeoutSec)*time.Second),
240215
))
241216
}
242217

243218
runtimeImpl := agentruntime.Runtime(runtimeSvc)
244219
closeFns := []func() error{toolsCleanup, sessionStore.Close}
245-
if runtimeMode == RuntimeModeGateway {
246-
remoteRuntime, remoteErr := newRemoteRuntimeAdapter(services.RemoteRuntimeAdapterOptions{})
247-
if remoteErr != nil {
248-
return RuntimeBundle{}, remoteErr
249-
}
250-
runtimeImpl = remoteRuntime
251-
closeFns = append([]func() error{remoteRuntime.Close}, closeFns...)
252-
}
253220

254221
needCleanup = false
255222

256223
closeBundle := combineRuntimeClosers(closeFns...)
257224

258225
return RuntimeBundle{
259226
Config: cfg,
260-
ConfigManager: manager,
227+
ConfigManager: sharedDeps.ConfigManager,
261228
Runtime: runtimeImpl,
262-
ProviderSelection: providerSelection,
229+
ProviderSelection: sharedDeps.ProviderSelection,
263230
MemoService: memoSvc,
264231
Close: closeBundle,
265232
}, nil
266233
}
267234

235+
// BuildRuntime 兼容旧入口,内部转发到 BuildGatewayServerDeps。
236+
func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, error) {
237+
return BuildGatewayServerDeps(ctx, opts)
238+
}
239+
268240
// NewProgram 基于共享运行时依赖构建并返回 TUI 程序,同时返回退出时应调用的资源清理函数。
269241
func NewProgram(ctx context.Context, opts BootstrapOptions) (*tea.Program, func() error, error) {
270-
bundle, err := BuildRuntime(ctx, opts)
242+
bundle, err := BuildTUIClientDeps(ctx, opts)
271243
if err != nil {
272244
return nil, nil, err
273245
}
274246

275-
tuiApp, err := newTUIWithMemo(&bundle.Config, bundle.ConfigManager, bundle.Runtime, bundle.ProviderSelection, bundle.MemoService)
247+
tuiRuntime, err := newRemoteRuntimeAdapter(services.RemoteRuntimeAdapterOptions{})
276248
if err != nil {
277249
if bundle.Close != nil {
278250
_ = bundle.Close()
279251
}
280252
return nil, nil, err
281253
}
254+
cleanup := combineRuntimeClosers(tuiRuntime.Close, bundle.Close)
255+
256+
tuiApp, err := newTUIWithMemo(&bundle.Config, bundle.ConfigManager, tuiRuntime, bundle.ProviderSelection, bundle.MemoService)
257+
if err != nil {
258+
if cleanup != nil {
259+
_ = cleanup()
260+
}
261+
return nil, nil, err
262+
}
282263
return tea.NewProgram(
283264
tuiApp,
284265
tea.WithAltScreen(),
285266
tea.WithMouseCellMotion(),
286-
), bundle.Close, nil
267+
), cleanup, nil
268+
}
269+
270+
// BuildSharedConfigDeps 统一构建共享配置依赖:配置、Provider 注册与当前选择服务。
271+
func BuildSharedConfigDeps(
272+
ctx context.Context,
273+
opts BootstrapOptions,
274+
) (bootstrapSharedBundle, agentruntime.ProviderFactory, *providercatalog.Service, error) {
275+
defaultCfg, err := bootstrapDefaultConfig(opts)
276+
if err != nil {
277+
return bootstrapSharedBundle{}, nil, nil, err
278+
}
279+
280+
loader := config.NewLoader("", defaultCfg)
281+
manager := config.NewManager(loader)
282+
if _, err := manager.Load(ctx); err != nil {
283+
return bootstrapSharedBundle{}, nil, nil, err
284+
}
285+
286+
providerRegistry, err := builtin.NewRegistry()
287+
if err != nil {
288+
return bootstrapSharedBundle{}, nil, nil, err
289+
}
290+
modelCatalogs := providercatalog.NewService(manager.BaseDir(), providerRegistry, nil)
291+
providerSelection := configstate.NewService(manager, providerRegistry, modelCatalogs)
292+
if _, err := providerSelection.EnsureSelection(ctx); err != nil {
293+
return bootstrapSharedBundle{}, nil, nil, err
294+
}
295+
296+
return bootstrapSharedBundle{
297+
Config: manager.Get(),
298+
ConfigManager: manager,
299+
ProviderSelection: providerSelection,
300+
}, providerRegistry, modelCatalogs, nil
301+
}
302+
303+
// BuildTUIClientDeps 构建 TUI 客户端依赖,仅保留配置与 Provider 选择,不创建本地 runtime/tool 栈。
304+
func BuildTUIClientDeps(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, error) {
305+
sharedDeps, _, _, err := BuildSharedConfigDeps(ctx, opts)
306+
if err != nil {
307+
return RuntimeBundle{}, err
308+
}
309+
return RuntimeBundle{
310+
Config: sharedDeps.Config,
311+
ConfigManager: sharedDeps.ConfigManager,
312+
ProviderSelection: sharedDeps.ProviderSelection,
313+
MemoService: nil,
314+
Close: nil,
315+
}, nil
287316
}
288317

289318
// bootstrapDefaultConfig 负责计算本次启动应使用的默认配置快照。
@@ -307,20 +336,6 @@ func resolveBootstrapWorkdir(workdir string) (string, error) {
307336
return agentsession.ResolveExistingDir(workdir)
308337
}
309338

310-
// resolveBootstrapRuntimeMode 归一化并校验 runtime 运行模式。
311-
func resolveBootstrapRuntimeMode(mode string) (string, error) {
312-
normalized := strings.ToLower(strings.TrimSpace(mode))
313-
if normalized == "" {
314-
return RuntimeModeLocal, nil
315-
}
316-
switch normalized {
317-
case RuntimeModeLocal, RuntimeModeGateway:
318-
return normalized, nil
319-
default:
320-
return "", errors.New("bootstrap: runtime mode must be local or gateway")
321-
}
322-
}
323-
324339
func buildToolRegistry(cfg config.Config) (*tools.Registry, func() error, error) {
325340
toolRegistry := tools.NewRegistry()
326341
toolRegistry.Register(filesystem.New(cfg.Workdir))

0 commit comments

Comments
 (0)