Skip to content

Phase 5: Windows 适配 #533

@pionxe

Description

@pionxe

Phase5: All-RPC Architecture & Ask Mode Implementation Plan

1. 目标与范围

交付物

# 交付物 可验证标准
1 Ask 模式 Runtime 提供带会话上下文、支持压缩的单轮推理路径(无工具、无预算、无验收),供诊断与 IDM 使用
2 诊断链路 RPC 化 CLI diag 命令和 ptyproxy 通过 Gateway RPC 通信,移除 Unix Domain Socket 信令
3 IDM 改良 IDM 从 gateway.run(完整 ReAct)切换到 gateway.ask,延迟降低,工具权限彻底移除
4 Windows 诊断 neocode diagneocode shell 在 Windows 11 CMD/PowerShell 中原生可用

非目标

  • CLI 管理命令(provider/model/use/config)不改动,维持直接文件读写
  • 不改变 TUI 的主链路
  • 不在本期引入除诊断以外的 Ask 使用场景

2. 关键决策与取舍

决策 1:Ask 模式 = 带会话上下文的单轮推理

为什么不是纯无状态?

IDM 模式下一个诊断会话可能持续 10-20 轮 @ai 交互。如果 Ask 完全无状态,ptyproxy 必须自行管理消息历史并在每次调用时完整传入。这带来两个问题:

  • 历史膨胀后每次请求的 payload 越来越大,IPC 传输开销线性增长
  • ptyproxy 需要感知 compaction 时机并自行裁剪历史——这本质上是 Runtime 层的职责,不应泄漏到 Shell 代理

为什么不是完整 ReAct(Run)?

Run 模式包含预算评估、工具选择、任务验收、checkpoint 等机制,诊断场景不需要这些。对比:

维度 Run (ReAct) Ask (新)
工具定义 注入完整工具 schema 不注入(tool_choice: "none"
Provider 调用 可能多轮(tool call 循环) 严格单轮
预算评估 evaluateTurnBudget() 跳过
任务验收 Acceptance 阶段 跳过
Checkpoint 自动创建 不创建
Todo 跟踪
消息持久化 写入 session 写入 AskSession
上下文压缩 完整 compact(含 transcript) 轻量 summarize(见决策 2)
典型延迟(诊断) 2-5s 0.5-2s

做什么?

  • Runtime 层新增 AskSession——轻量级会话,仅管理消息历史和上下文
  • gateway.ask 接受 session_id:若为空则自动创建 AskSession,若已有则复用
  • 每次 Ask 调用 = 单次 Provider Generate(tool_choice: "none")→ 流式返回
  • AskSession 不参与 checkpoint、不接受 acceptance、不跟踪 Todo

决策 2:Ask 压缩 = 轻量 Summarize(非 Run 的完整 Compact)

为什么不复用 context/compact

Run 模式的 compact 机制设计目标是:

  • 生成结构化 transcript(task_state、verification_profile、goals 等)
  • 保留工具调用和结果
  • 支持从 compact 状态恢复完整上下文
  • 需要额外一次 Provider 调用来生成 summary

Ask 场景不需要这些:没有工具调用需要保留,不需要从 compact 恢复,诊断上下文的语义简单。

做什么?

AskSession 的压缩机制:

  • 触发条件:估算 token 数超过阈值(默认 8000,可配置)
  • 压缩方法:将最早的消息对(user + assistant)替换为摘要段落,保留最近 N 轮(默认 5 轮)
  • 摘要内容:用户问题要点 + AI 回答要点
  • 不做:不生成 transcript 文件、不保存到磁盘、不触发 Provider 调用(纯本地文本操作)
压缩前: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10]
压缩后: [summary("msg1-msg5"), msg6, msg7, msg8, msg9, msg10]

这比完整 compact 快得多(无 Provider 调用),且对于诊断场景足够有效。


决策 3:CLI 管理命令不 RPC 化

为什么?

  • 鸡生蛋问题neocode provider add 等命令需要写配置。如果强制走 RPC,用户必须确保 Gateway 已运行——但添加 provider 很可能是用户的第一步操作
  • 配置真相源是文件系统~/.neocode/config.yaml 是持久化存储。CLI 写文件 + Gateway 启动时读取是最简单的正确方案
  • 收益不成比例:管理命令延迟不敏感(用户手动执行),"状态一致性"对于低频管理操作几乎感知不到

做什么?

  • CLI 管理命令(provider/model/use/config)维持当前直接文件读写
  • 如果 Gateway 正在运行,CLI 写入配置后可通过 gateway.reloadConfig 通知刷新(此 RPC 方法可选,本期不做硬要求)

决策 4:Windows ptyproxy 需要 ConPTY 实现

为什么必须本期实现?

  • neocode shell 是诊断功能的核心入口。如果 Windows 上没有 Shell 代理,用户只能使用 neocode diag --error-log 手动传入日志,体验不完整
  • Named Pipe 传输层已经存在(transport/listen_windows.go),传输层不是瓶颈
  • 当前 proxy_windows.go 所有函数返回 errUnsupportedPlatform,这一点必须改变

做什么?

  • 使用 Windows ConPTY API(CreatePseudoConsole / ClosePseudoConsole)实现 proxy_windows.go
  • 支持启动 CMD 和 PowerShell
  • 输出捕获到 Ring Buffer
  • 通过 Named Pipe 连接 Gateway,注册 role=shell
  • 支持 triggerAction 通知和 IDM 模式
  • 详见第 7 节

3. Ask 模式设计

3.1 AskSession 模型

// AskSession 是 Ask 模式的轻量级会话。
type AskSession struct {
    ID        string
    Workdir   string
    Skills    []string            // 已激活的 Skill ID 列表
    Messages  []AskMessage        // 对话历史
    CreatedAt time.Time
    UpdatedAt time.Time
}

type AskMessage struct {
    Role    string // "user" | "assistant"
    Content string
}

区别于 Run 的 agentsession.Session

  • 没有 Todo 列表
  • 没有 ToolCall 记录
  • 没有 Checkpoint 状态
  • 没有 Budget 信息
  • 没有 AgentMode

3.2 RPC 协议

// gateway.ask — 请求
{
  "method": "gateway.ask",
  "params": {
    "session_id": "",                    // 空 = 自动创建 AskSession
    "user_query": "npm install 失败了怎么办?",
    "workdir": "/home/user/project",     // 首次调用时生效
    "skills": ["terminal-diagnosis"]     // 首次调用时生效
  }
}

// gateway.ask — 流式响应事件
// 事件1: ask_chunk (可多次)
{
  "type": "event",
  "event": "ask_chunk",
  "payload": {
    "session_id": "ask-abc123",
    "delta": "根据错误信息分析..."
  }
}

// 事件2: ask_done (一次)
{
  "type": "event",
  "event": "ask_done",
  "payload": {
    "session_id": "ask-abc123",
    "full_response": "完整的回答文本...",
    "compacted": false,                   // 本轮是否触发了压缩
    "usage": {
      "input_tokens": 450,
      "output_tokens": 120
    }
  }
}

// 事件3: ask_error (错误时)
{
  "type": "event",
  "event": "ask_error",
  "payload": {
    "session_id": "ask-abc123",
    "code": "PROVIDER_ERROR",
    "message": "provider returned 429"
  }
}

3.3 Runtime 实现路径

internal/runtime 中新增 AskService(或 Service 新增 Ask 方法):

func (s *Service) Ask(ctx context.Context, input AskInput) error

Ask() 方法的执行流程

1. 解析 session_id
   ├── 空 → 创建新 AskSession(生成 ID,写入 workdir/skills)
   └── 非空 → 加载已有 AskSession

2. 追加用户消息到 AskSession.Messages

3. 上下文构建 (context.BuildAskPrompt)
   ├── System Prompt = 基础指令 + Skill instructions
   ├── 检查 token 估算
   │   ├── 超过阈值 → 轻量压缩(合并早期消息为摘要)
   │   └── 未超过 → 直接使用
   ├── 拼接历史消息
   └── 拼接当前用户问题

4. Provider 调用
   ├── tool_choice: "none"
   ├── stream: true
   └── 事件: ask_chunk (每个 delta)

5. 追加助手回答到 AskSession.Messages

6. 发射 ask_done 事件(含 usage + session_id)

7. 持久化 AskSession(轻量存储,可选)

与 Run() 的代码路径对比

步骤 Run() Ask()
会话加载 acquireSessionLock + LoadSession loadAskSession(无锁,无并发)
Prompt 构建 context.Build() 含工具定义 context.BuildAskPrompt() 不含工具
预算检查 evaluateTurnBudget()
Provider generateStreamingMessage() generateStreamingMessage()(同)
工具执行 executeAssistantToolCalls() 跳过
验收 acceptance.Evaluate() 跳过
Checkpoint MaybeCreateCodeCheckpoint() 跳过
消息保存 sessionStore.SaveSession() saveAskSession()(轻量)

3.4 上下文构建 (BuildAskPrompt)

BuildAskPrompt 输入:
  - askSession  (消息历史)
  - userQuery   (当前问题)
  - skills      (Skill ID 列表)
  - config      (模型配置)

构建步骤:
  1. 加载基础 System Prompt(不含工具定义的最小指令集)
  2. 遍历 skills → 从 Skills Registry 加载 instruction → 拼接
  3. 估算当前总 token 数
     ├── 超阈值 → 执行轻量压缩
     │   ├── 保留最近 N 轮消息
     │   ├── 将早期消息合并为一段摘要
     │   └── 摘要放在 System Prompt 之后、历史消息之前
     └── 未超阈值 → 直接使用
  4. 拼接最终 prompt:
     [System Prompt]
     [Skill Instructions]
     [压缩摘要(如有)]
     [历史消息(最近 N 轮)]
     [当前用户问题]
  5. 转换为 Provider GenerateRequest(tool_choice: "none")

3.5 轻量压缩算法

输入: messages ([]AskMessage), keepLastN (int)
输出: summary (string), kept ([]AskMessage)

算法:
  1. 如果 len(messages)/2 <= keepLastN: 不需要压缩
  2. splitPoint = (len(messages) - keepLastN*2)
  3. oldMessages = messages[:splitPoint]
  4. keptMessages = messages[splitPoint:]
  5. summary = 本地文本模板:
     "之前的对话摘要:用户询问了[提取的关键问题列表],AI 提供了[关键结论]。"
  6. 返回 summary, keptMessages

注意这是本地提取不是模型生成——不调用 Provider。对诊断场景,早期的错误分析上下文可以通过简单拼接保留关键信息。


4. 信令中继 (triggerAction)

4.0 为什么要添加信令中继?

问题的本质:跨进程指令传递。

当前诊断链路中,CLI 进程和 Shell 进程是两个独立的操作系统进程:

CLI 进程 (neocode diag)          Shell 进程 (neocode shell)
     │                                    │
     │  用户按下 Ctrl+G 或运行 diag        │  持有终端 Buffer
     │  但它没有终端 Buffer                │  但它不知道用户想触发诊断
     │                                    │
     └──── 需要一个通信通道 ──────────────┘

CLI 进程知道"用户想触发诊断",Shell 进程持有"终端报错日志"。两者需要协作才能完成诊断。同样的情况也存在于 IDM 进入(neocode diag -i)和自动诊断模式切换(neocode diag auto on/off)。

现状为什么有问题?

当前这个通信通道是 Unix Domain Socket——一个仅 Unix 平台可用的 IPC 机制。它带来三个问题:

  1. 平台锁定:Windows 不支持 Unix Domain Socket,导致 neocode diag 系列命令在 Windows 上完全不可用。这不是"功能受限",而是"完全不存在"——proxy_windows.go 中所有信号函数返回 errUnsupportedPlatform

  2. 双重通信通道:Shell 进程已经通过 GatewayRPCClient 与 Gateway 维持一条 JSON-RPC 长连接(用于执行诊断工具调用),同时又监听一个独立的 Unix Socket 等待 CLI 信号。两条通道做两件事,增加了 Shell 代理的复杂度和故障面

  3. Socket 生命周期管理复杂:Socket 文件基于 PID 命名并放在 ~/.neocode/run/ 目录下。CLI 需要通过 --socket flag、环境变量(NEOCODE_DIAG_SOCKET / NEOCODE_IDM_SOCKET)或扫描目录来发现目标 Socket。进程异常退出时 Socket 文件残留、多个 Shell 实例并存时的歧义、权限问题——这些都是已发生的维护负担

信令中继如何解决?

核心思路:Gateway 是唯一的中介。 Shell 进程和 CLI 进程各自只与 Gateway 通信,Gateway 负责指令路由。

CLI 进程                        Gateway                         Shell 进程
    │                              │                                │
    │-- triggerAction(sess, act)-->│                                │
    │                              │-- 查找路由表                    │
    │                              │   sess + role=shell            │
    │                              │   → 目标连接                   │
    │                              │                                │
    │                              │-- notification(action) ------->│
    │<-- {ok: true} --------------│                                │
    │                              │                                │
    │                              │   [Shell 捕获 Buffer            │
    │                              │    调用 gateway.ask            │
    │                              │    流式渲染结果]                │

三个好处:

  • 平台统一:Gateway 的传输层已经支持 Unix Socket 和 Named Pipe,信令中继天然跨平台
  • 单一通道:Shell 进程只需要维护与 Gateway 的一条 RPC 连接,所有指令(信令 + 诊断推理)都走这条连接
  • 生命周期简化:不再需要 Socket 文件管理。Shell 连接断开时 Gateway 自动清理路由表,CLI 收到明确的"no active shell"错误

为什么必须经由 Gateway 而不是 CLI 直接连 Shell?

如果 CLI 直接通过某种 IPC 连接 Shell 进程:

  • 需要引入新的认证机制(谁有权给 Shell 发指令?)
  • Gateway 已有的连接管理和 ACL 体系完全无法复用
  • CLI 需要知道 Shell 进程的内部地址(又回到 Socket 发现问题)

Gateway 已经在 CLl 和 Shell 之间——它是双方都信任的中立中介,拥有成熟的认证、ACL 和路由基础设施。信令中继只是在这个基础设施上增加一个路由规则。

4.1 对现有客户端(TUI / Web / 桌面端)的影响评估

结论:零影响。 以下是逐客户端分析。

TUI (Bubble Tea)

TUI 与 Gateway 的交互方式:

  • 通过 RemoteRuntimeAdapterGatewayRPCClient 建立 IPC 连接
  • 调用 authenticatebindStreamgateway.run
  • 订阅 gateway.event 流式事件

role 字段:TUI 在 bindStream 时不传 role 参数。Gateway 将其视为 role="" (空角色,等同于"未声明")。StreamRelay 的路由查找使用精确匹配(role="shell"),空角色连接不会被匹配到,因此 TUI 连接永远不会收到 gateway.notification 事件。

triggerAction 方法:TUI 不调用此方法。即便将来 TUI 需要触发 Shell 诊断,也只需在 bindStream 时声明 role="tui" 即可获得权限。

gateway.ask 方法:TUI 不使用。主链路(gateway.run)完全不受影响。

StreamRelay 路由表变更:在 relayBinding 结构体中新增 Role 字段是纯增量变更。已有绑定记录中 Role 为空字符串,不影响现有路由逻辑。

Web (Network Server)

Web 前端通过 HTTP/WebSocket/SSE 连接 Gateway:

  • WebSocket 连接用于流式事件订阅
  • HTTP POST 用于 JSON-RPC 调用(gateway.run 等)

与 TUI 相同:不声明 role,不调用 triggerAction,不使用 gateway.ask零影响。

桌面端 (URL Scheme Daemon)

internal/gateway/adapters/urlscheme/ 下的守护进程仅处理 neocode:// URL scheme 唤醒——它不维持 Shell 连接,不参与诊断链路。零影响。

安全边界

triggerAction 的 ACL 独立于现有的 control-plane ACL:

调用方 role 可调用的 action 说明
cli diagnose, idm_enter, auto_on, auto_off, auto_status CLI 命令触发
tui diagnose, idm_enter TUI 将来可选支持
shell Shell 不能反向发指令
空 (未声明) 向后兼容,无权限

gateway.notification 事件仅发送给精确匹配 session_id + role 的连接,不是广播。Web/TUI 连接即使绑定到同一 session,由于 role 不匹配(空 ≠ "shell"),也不会收到 Shell 的通知事件。

4.2 协议定义

// gateway.experimental.triggerAction — 请求
{
  "method": "gateway.experimental.triggerAction",
  "params": {
    "session_id": "pty-shell-abc",
    "action": "diagnose",              // diagnose | idm_enter | auto_on | auto_off | auto_status
    "payload": {}                      // action 特定数据,可选
  }
}

// gateway.experimental.triggerAction — 响应(同步)
{
  "ok": true,
  "message": "action delivered to shell"
}

// gateway.notification — 服务器推送(发往 role=shell 的连接)
{
  "type": "notification",
  "method": "gateway.notification",
  "params": {
    "action": "diagnose",
    "payload": {},
    "source_session_id": "cli-session-xyz"
  }
}

4.3 连接角色注册

修改 gateway.bindStream,增加 role 参数:

{
  "method": "gateway.bindStream",
  "params": {
    "session_id": "pty-shell-abc",
    "role": "shell"         // 新增: "shell" | "cli" | "tui" (默认)
  }
}

Gateway StreamRelay 路由表扩展:

type relayBinding struct {
    ConnectionID ConnectionID
    SessionID    string
    Role         string   // 新增
    Channel      StreamChannel
}

4.4 triggerAction 执行流程

CLI 进程                     Gateway                      Shell 进程
    |                           |                              |
    |-- triggerAction --------->|                              |
    |   (session_id, "diagnose")|                              |
    |                           |-- 查找路由表                   |
    |                           |   session_id + role="shell"  |
    |                           |   → conn_id=shell-42         |
    |                           |                              |
    |                           |-- notification -------------->|
    |                           |   (action="diagnose")         |
    |<-- {ok: true} ------------|                              |
    |                           |                              |
    |                           |   [Shell 执行诊断]             |
    |                           |<-- gateway.ask -------------|
    |                           |   (user_query=buffer)        |
    |                           |                              |
    |                           |-- ask_chunk events --------->|

4.5 错误语义

场景 响应
目标 session 无 shell 连接 {"ok": false, "message": "no active shell for session X"}
shell 连接存在但诊断进行中 {"ok": false, "message": "shell busy: diagnosis in-flight"}
action 未知 {"ok": false, "message": "unknown action: X"}
送达成功 {"ok": true, "message": "delivered"}

4.6 安全约束

  • triggerAction 仅允许 role=clirole=tui 的连接发起
  • Shell 连接不能调用 triggerAction(避免权限提升)
  • Action 白名单:diagnose, idm_enter, auto_on, auto_off, auto_status

5. IDM 改良

5.1 当前 IDM vs 新 IDM

维度 当前(Run-based) 新(Ask-based)
推理路径 完整 ReAct 循环 单次 Provider Generate
工具权限 工具 schema 存在(模型可见) 完全不注入(tool_choice: "none"
上下文管理 gateway.run 自动管理 session gateway.ask 管理 AskSession
压缩 完整 compact(耗时、生成 transcript) 轻量 summarize(本地,不调 Provider)
典型首响延迟 2-5s 0.5-2s
会话管理 创建临时 Runtime Session → 用完删除 创建 AskSession → 用完删除
输出渲染 glamour Markdown → ANSI 同(保留 glamour 做最终格式化)

5.2 交互流程

用户运行 neocode shell → 自动进入 ptyproxy
ptyproxy 连接 Gateway → authenticate → createSession → bindStream(role=shell)

[用户按 Ctrl+G 或命令以非零退出]
ptyproxy 自动触发诊断:
  → 不需要 triggerAction(自己在 shell 进程内)
  → 直接调用 gateway.ask
     session_id="" (新建 AskSession)
     user_query=<terminal buffer + error log>
     skills=["terminal-diagnosis"]
  → 流式渲染结果到终端

[用户在 IDM 模式中]
用户输入 @ai <问题>
  → ptyproxy 调用 gateway.ask
     session_id=<已有 AskSession ID>
     user_query=<问题>
  → 流式渲染回答
  → AskSession 自动管理上下文 + 压缩

用户输入 exit → 退出 IDM → ptyproxy 调用 gateway.deleteAskSession

5.3 高亮

上下文自动管理:IDM 从几轮到几十轮的对话,ptproxy 不需要感知压缩逻辑。Runtime 在每次 gateway.ask 时自动检查 token 数并决定是否压缩。

工具权限彻底移除:老 IDM 通过 Skill prompt "禁止"工具调用,但工具 schema 仍然注入在 System Prompt 中,模型可能尝试 call。新 IDM 在 Provider 层设置 tool_choice: "none",模型根本不知道工具的存在。

延迟可预期:Ask 模式每次都只是一次 Provider Generate 调用,没有"模型决定调用工具 → 返回 tool call → Runtime 执行 → 模型再总结"的多轮循环。


6. CLI 诊断迁移

6.1 命令重构

# 触发运行中 Shell 的诊断(通过 Gateway 信令中继)
neocode diag

# 直接诊断一段错误日志(不依赖 Shell,Windows 也可用)
neocode diag --error-log "npm install failed: ECONNREFUSED"

# 从 stdin 读取错误日志进行诊断
cat error.log | neocode diag

# 交互式诊断(需 Shell)
neocode diag -i

# 诊断模式开关(需 Shell)
neocode diag auto on|off|status

6.2 实现方式

neocode diag(触发 Shell 诊断)

1. 初始化 GatewayRPCClient(连接 Named Pipe / Unix Socket)
2. authenticate
3. 解析目标 session_id(通过 --session flag / env / 自动发现)
4. gateway.experimental.triggerAction(session_id, "diagnose")
5. 显示结果(触发成功/失败)

neocode diag --error-log(独立诊断):

1. 初始化 GatewayRPCClient
2. authenticate
3. gateway.ask(session_id="", user_query=error_log, skills=["terminal-diagnosis"])
4. 流式输出 ask_chunk → stdout
5. 显示 ask_done 汇总

neocode diag -i(IDM 进入):

1. 初始化 GatewayRPCClient
2. authenticate
3. gateway.experimental.triggerAction(session_id, "idm_enter")
4. Shell 收到通知后进入 IDM 循环

6.3 关键变更点

  • shell_diag_commands.go:移除 sendDiagnoseSignalFnptyproxy.SendDiagnoseSignal(Unix Socket),改为调用 gatewayclient.GatewayRPCClient 的方法
  • 新增 --error-log flag 和 stdin 管道读取
  • 新增 --session flag 用于指定目标 Shell Session

7. Windows ptyproxy (ConPTY)

7.1 目标

使 neocode shell 在 Windows 10+(1809+)上可用,支持:

  • CMD 和 PowerShell
  • 终端输出捕获到 Ring Buffer
  • Gateway RPC 连接(Named Pipe)
  • triggerAction 通知接收
  • IDM 模式
  • 自动诊断(auto mode)

7.2 技术方案

使用 Windows ConPTY API(CreatePseudoConsole / ClosePseudoConsole / ResizePseudoConsole),通过 golang.org/x/sys/windows 的 syscall 封装直接调用。

ConPTY 创建流程

1. 创建双向 Pipe
   ├── inputPipe  (ptyproxy → ConPTY: 用户输入)
   └── outputPipe (ConPTY → ptyproxy: 终端输出)

2. CreatePseudoConsole(size, inputPipe, outputPipe, 0)
   → HPCON (pseudo console handle)

3. 构建 STARTUPINFOEX
   ├── InitializeProcThreadAttributeList
   └── UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, HPCON)

4. CreateProcess(cmd.exe / powershell.exe, ..., EXTENDED_STARTUPINFO_PRESENT)
   → 进程在 ConPTY 中运行

5. 主循环
   ├── goroutine 1: 读取 outputPipe → Ring Buffer + 终端输出
   ├── goroutine 2: 读取用户输入 → 写入 inputPipe
   └── goroutine 3: Gateway RPC 通知监听

7.3 与 Unix ptyproxy 的差异处理

功能 Unix Windows
PTY 创建 github.com/creack/pty ConPTY API(syscall)
终端大小同步 pty.InheritSize + SIGWINCH ResizePseudoConsole + Window Buffer Size Event
原始模式 term.MakeRaw SetConsoleMode (ENABLE_VIRTUAL_TERMINAL_INPUT)
信号转发 SIGINT/SIGTERM → Process.Signal GenerateConsoleCtrlEvent
ANSI 支持 原生 需启用 ENABLE_VIRTUAL_TERMINAL_PROCESSING
Socket 信令 Unix Domain Socket Named Pipe(RPC 通知,同 Unix 改造后)
Gateway 连接 Unix Socket Named Pipe (winio.DialPipe)

7.4 文件结构

internal/ptyproxy/
├── proxy_unix.go          # Unix PTY 实现(现有,改造)
├── proxy_windows.go       # Windows: 移除 errUnsupportedPlatform,实现 ConPTY
├── proxy_windows_conpty.go # ConPTY API 封装
├── proxy_common.go        # 跨平台共享逻辑(Ring Buffer、诊断协调器、IDM)
├── idm_controller.go      # IDM 控制器(去掉 //go:build !windows)
├── diagnosis_coordinator.go # 诊断协调器(去掉 //go:build !windows)
└── ring_buffer.go         # Ring Buffer(现有)

关键变化:

  • idm_controller.go 去掉 //go:build !windows 构建标签
  • diagnosis_coordinator.go 去掉 //go:build !windows 构建标签
  • proxy_common.go 提取 PTY 生命周期管理中的跨平台逻辑

7.5 ConPTY 依赖

使用 golang.org/x/sys/windows 中的 syscall 原语,不引入新的第三方库。需要封装的 API:

//go:build windows

// Syscall 封装
var (
    procCreatePseudoConsole = kernel32.NewProc("CreatePseudoConsole")
    procClosePseudoConsole  = kernel32.NewProc("ClosePseudoConsole")
    procResizePseudoConsole = kernel32.NewProc("ResizePseudoConsole")
)

// CreatePseudoConsole 创建伪控制台
func CreatePseudoConsole(size windows.Coord, input, output windows.Handle, flags uint32) (windows.Handle, error)

// ClosePseudoConsole 关闭伪控制台
func ClosePseudoConsole(hpc windows.Handle) error

// ResizePseudoConsole 调整伪控制台大小
func ResizePseudoConsole(hpc windows.Handle, size windows.Coord) error

PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 常量值: 0x20016(Windows SDK 定义)。


8. 任务清单(含依赖关系)

Phase 5a: Ask 模式基础 [阻塞 5b/5c/5d]

  • P5a-1: 协议与类型定义

    • internal/gateway/protocol/:定义 MethodGatewayAsk 常量、AskParamsAskResult
    • internal/gateway/types.go:新增 FrameActionAskFrameActionDeleteAskSession
    • 事件类型:ask_chunkask_doneask_error
  • P5a-2: AskSession 模型

    • internal/runtime/ask_session.go:定义 AskSession 结构体
    • internal/runtime/ask_store.go:AskSession 持久化(简单文件或 SQLite,与 agent session 隔离)
  • P5a-3: 上下文构建

    • internal/context/ask_prompt.go:实现 BuildAskPrompt()
      • 不含工具定义的 System Prompt
      • Skill instruction 注入
      • 轻量压缩触发与执行
  • P5a-4: Runtime Ask 实现

    • internal/runtime/ask.go:实现 Service.Ask(ctx, AskInput) error
      • AskSession 加载/创建
      • 消息追加
      • BuildAskPrompt 调用
      • Provider Generate(tool_choice: "none"
      • 回答追加 + 持久化
      • 事件发射
  • P5a-5: Gateway Ask dispatch

    • internal/gateway/contracts.goRuntimePort 接口新增 Ask()
    • internal/gateway/rpc_dispatch.go:注册 gateway.ask handler
    • internal/gateway/stream_relay.go:确保 ask_chunk/done 事件流式路由
  • P5a-6: 诊断 Skill 标准化

    • idm_controller.go 中的 terminalDiagnosisSkillMarkdown 常量提取为 Skill 文件
    • 确保 Skill 在 ~/.neocode/skills/terminal-diagnosis/SKILL.md 可被 Skills Registry 加载
  • P5a-7: 客户端支持

    • internal/gateway/client/gateway_rpc_client.go:新增 Ask() 方法

Phase 5b: 信令 RPC 化 [依赖 5a]

  • P5b-1: triggerAction 协议

    • internal/gateway/protocol/:定义 MethodGatewayExperimentalTriggerActionTriggerActionParamsTriggerActionResult
    • internal/gateway/types.go:新增 FrameActionTriggerAction
    • 定义 action 枚举常量:ActionDiagnoseActionIDMEnterActionAutoOnActionAutoOffActionAutoStatus
  • P5b-2: StreamRelay 角色管理

    • internal/gateway/connection_context.goConnectionRegistration 增加 Role 字段
    • internal/gateway/stream_relay.gobindStream handler 解析 role 参数
    • 新增查询方法:FindConnectionsBySessionAndRole(sessionID, role)
  • P5b-3: triggerAction dispatch

    • internal/gateway/rpc_dispatch.go:注册 gateway.experimental.triggerAction handler
    • 实现查找 → 下发 notification 逻辑
    • 安全校验:仅 role=cli|tui 的连接可发起
  • P5b-4: ptyproxy Unix 适配

    • internal/ptyproxy/proxy_unix.go
      • 移除 listenDiagSocket / listenIDMSocket / serveDiagSocket / serveIDMSocket
      • 新增 Gateway RPC 通知监听 goroutine
      • RunManualShell 启动时执行 bindStream(role="shell")
      • 收到 diagnose / idm_enter / auto_on / auto_off 通知 → 执行对应逻辑
  • P5b-5: 废弃 Socket 信令文件

    • 删除 internal/ptyproxy/ipc_protocol.go(IPC 指令结构体)
    • 删除 internal/ptyproxy/socket_paths.go(Socket 路径管理)
    • 清理 proxy_windows.go 中的 stub(SendDiagnoseSignal 等函数不再需要)
    • 清理 CLI 层的 socket 路径解析函数(resolveDiagSocketPath 等)

Phase 5c: IDM 切换到 Ask [依赖 5a, 5b]

  • P5c-1: IDM Ask 会话管理

    • internal/ptyproxy/idm_controller.go
      • 新增 askSessionID 字段(跟踪当前 AskSession)
      • Enter() 时创建 AskSession(调用 gateway.createAskSession 或首次 gateway.ask
      • Exit() 时删除 AskSession
  • P5c-2: 替换 sendAIMessage

    • gateway.rungateway.ask
    • 移除 bindStream / run / cancel / createSession / deleteSession / activateSessionSkill 调用
    • 流式处理改为接收 ask_chunk + ask_done 事件
  • P5c-3: 移除构建标签

    • idm_controller.go:移除 //go:build !windows
    • diagnosis_coordinator.go:移除 //go:build !windows
    • 这些组件的逻辑是平台无关的

Phase 5d: CLI 诊断迁移 [依赖 5a, 5b]

  • P5d-1: diag 命令重构

    • shell_diag_commands.go
      • defaultDiagCommandRunner → 改用 gatewayclient.GatewayRPCClient + triggerAction
      • 新增 --error-log flag
      • 新增 stdin 管道输入检测
      • 新增 --session flag
  • P5d-2: 直接诊断路径

    • neocode diag --error-log "..."gateway.ask(不依赖 Shell)
    • stdin 管道 → gateway.ask
    • 这些路径在 Windows 上立即可用
  • P5d-3: 移除旧依赖

    • CLI 不再直接引用 ptyproxy.SendDiagnoseSignal / ptyproxy.SendIDMEnterSignal 等函数
    • 这些函数随 P5b-5 一并清理

Phase 5e: Windows ptyproxy [依赖 5b, 5c]

  • P5e-1: ConPTY API 封装

    • internal/ptyproxy/proxy_windows_conpty.go
      • CreatePseudoConsole / ClosePseudoConsole / ResizePseudoConsole syscall 封装
      • startProcessInConPty(shellPath, workdir, hpc) (*os.Process, error)
  • P5e-2: Windows PTY 主循环

    • internal/ptyproxy/proxy_windows.go
      • 实现 RunManualShell()(替换 errUnsupportedPlatform
      • ConPTY 创建 → 进程启动 → I/O 泵 → 信号处理
      • 终端输出捕获 → Ring Buffer
      • Gateway RPC 连接(Named Pipe)→ authenticate → bindStream(role=shell)
      • 通知监听 goroutine
  • P5e-3: 跨平台共享逻辑提取

    • internal/ptyproxy/proxy_common.go(可选)或直接在现有文件中减少重复
    • newDiagnosisCoordinatorconsumeDiagSignalsidmController 等已在 5c-3 中移除构建标签,直接复用
  • P5e-4: Windows 终端设置

    • 启用 ENABLE_VIRTUAL_TERMINAL_PROCESSING 确保 ANSI 转义序列正常工作
    • 启用 ENABLE_VIRTUAL_TERMINAL_INPUT 处理鼠标和特殊键
    • 窗口大小变更监听

Phase 5f: 测试与验证 [依赖全部前序]

  • P5f-1: Ask 模式单元测试

    • Ask() 正常路径 / 空输入 / 空 session_id(自动创建)
    • 多轮对话 → token 超阈值 → 压缩触发验证
    • Provider 错误 / 超时 / 流式中断 / 429 限流
    • Skill 注入验证
  • P5f-2: triggerAction 集成测试

    • Shell role 注册 → 下发 diagnose → 验证通知到达
    • Shell 连接断开 → triggerAction 返回错误
    • 非 cli/tui role 发起 triggerAction → ACL 拒绝
  • P5f-3: IDM Ask 对比测试

    • 同一错误日志,gateway.run vs gateway.ask 延迟对比
    • 同一错误日志,Token 消耗对比
    • 多轮对话 → 压缩后回答质量评估
  • P5f-4: 跨进程联动测试

    • 终端 A:neocode shell(Unix)
    • 终端 B:neocode diag
    • 验证诊断结果在终端 A 中正确渲染
  • P5f-5: Windows 专项测试

    • Windows 11:neocode shell 启动 CMD / PowerShell
    • neocode diag --error-log "..." 通过 Named Pipe 获取诊断
    • neocode diag 触发 Shell 诊断(通过 triggerAction)
    • IDM 模式功能验证
  • P5f-6: 回归测试

    • TUI 主链路完整(neocode → TUI → Gateway → Runtime → Tools)
    • CLI 管理命令无影响(provider list, model list, use
    • 现有诊断功能(Unix ptyproxy)无退化

9. 风险与缓解

风险 概率 影响 缓解措施
Ask 压缩过于激进导致上下文丢失关键信息 保留最近 5 轮完整消息 + 摘要包含关键结论;阈值可配置
triggerAction 通知在 Shell 连接瞬时断开时丢失 triggerAction 返回送达确认;CLI 端收到 no shell 错误后明确提示用户
ConPTY 在旧版 Windows (< 1809) 不可用 启动时检测 OS 版本,给出明确的升级提示
IDM 切换到 Ask 后回答质量下降(缺少工具上下文) 诊断场景本身不需要工具;Skill prompt 质量是主要变量,P5a-6 中标准化
Named Pipe ACL 与杀毒软件冲突 复用现有 SDDL ACL(已验证),必要时提供 --acl-mode 覆盖

10. 里程碑

里程碑 内容 可验证标准
M1: Ask 就绪 P5a 全部完成 gateway.ask 可通过 RPC 调用,返回流式回答;AskSession 支持多轮上下文 + 自动压缩
M2: 信令就绪 P5b 全部完成 Shell 连接通过 Gateway RPC 收到 diagnose 通知;Unix Socket 代码全部移除
M3: IDM 切换 P5c 全部完成 IDM @ai 通过 Ask 模式回答;工具 schema 完全不注入
M4: CLI 就绪 P5d 全部完成 neocode diag --error-log 在任何平台可用
M5: Windows 就绪 P5e 全部完成 neocode shell 在 Windows 11 CMD/PowerShell 中运行
M6: 交付 P5f 全部完成 全平台测试通过,Socket 代码清理干净

11. 不变约束

  • 主链路不破坏TUI → Gateway → Runtime → Provider → Tools 闭环保持
  • 不分层接线:ptyproxy 不直接调用 Provider;上下文构建不走 CLI 层
  • 工具定义隔离:Ask 模式下 System Prompt 不含工具定义;Provider 层 tool_choice: "none"
  • 配置安全:不硬编码路径/密钥/提示词;配置通过 config 注入
  • 向后兼容neocode diag CLI 接口不变(仅底层实现变);neocode shell CLI 接口不变
  • 构建标签规范:平台特定代码使用 //go:build 标签;跨平台逻辑不加标签

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions