Skip to content

feat(step2): 打通 neocode://run 唤醒-会话创建-终端接管闭环并补齐系统级 URL 注册#496

Merged
phantom5099 merged 20 commits into1024XEngineer:mainfrom
pionxe:feat/url-wake-session-hydration-mvp
Apr 28, 2026
Merged

feat(step2): 打通 neocode://run 唤醒-会话创建-终端接管闭环并补齐系统级 URL 注册#496
phantom5099 merged 20 commits into1024XEngineer:mainfrom
pionxe:feat/url-wake-session-hydration-mvp

Conversation

@pionxe
Copy link
Copy Markdown
Collaborator

@pionxe pionxe commented Apr 27, 2026

背景

Step 2 目标是打通完整闭环:

neocode://run 外部触发
-> Gateway 解析与放行
-> Runtime 创建/接管会话并执行 run
-> Dispatcher 拉起终端
-> neocode --session <id> 启动后自动水化会话状态

同时补齐系统级 URL Scheme 注册能力,实现“文档/网页/聊天窗口点击即用”。


变更摘要

本 PR 在不改动核心分层边界的前提下,完成了 Step 2 的关键落地:

  1. 会话创建权下放 Runtime
  • Runtime CreateSession 支持空 session_id 自动生成(session_*)。
  • Gateway 不再生成会话 ID,只负责编排与转发。
  • 新增 RuntimePort.CreateSession(...) 契约并在 bridge 层接入鉴权。
  1. wake.run 闭环编排
  • wake.openUrl(action=run) 路径改为:先 CreateSession,后 Run
  • wake.openUrl ACK 回传最终 session_id(用于后续接管)。
  • 修复 IPC 短连接导致 run 被取消的问题:wake.run 启动改为使用脱离连接取消的上下文(context.WithoutCancel 语义封装)。
  1. Dispatcher 与终端拉起
  • Dispatcher 在收到 session_id 后执行终端拉起命令:neocode --session <id>
  • Windows/macOS 实现终端拉起;Linux 维持明确 not_supported 提示(现阶段不做弹窗终端适配)。
  1. TUI 会话水化与工作区接管
  • 启动参数新增 --session 并贯通到启动装配链路。
  • 启动时支持按 session_id 水化历史消息/标题/Todo/活动会话。
  • 工作区按会话 workdir 接管;路径失效时保留当前工作区并给出告警。
  • 保持既有展示语义:未引入额外的启动期强制 Waiting for agent... 视图改动。
  1. 系统级 URL Scheme 注册(点击即用)
  • 新增 CLI 命令:neocode url-register [--executable <abs-path>]
  • 平台实现:
    • Windows:用户态注册表 HKCU\Software\Classes\neocode
    • macOS:用户态 .app bundle(含 CFBundleURLTypes、可执行脚本、lsregister 刷新)。
    • Linux:用户态 .desktop + xdg-mimeExec ... "%u"
    • 其他平台:not_supported
  • 安装脚本(full flavor)安装后 best-effort 自动执行 url-register(失败仅 warning,不阻断安装)。
  1. 文档与协议同步
  • README 增补 url-register 用法、验证方式、排障建议与手动清理指引。
  • Gateway RPC 文档同步 run 唤醒与 session_id 回传行为。

兼容性与边界说明

  • 本次改动遵循既有分层:TUI -> Gateway -> Runtime,未引入跨层直连。
  • Gateway 职责保持为“编排与转发”,会话 ID 生成归 Runtime。
  • 为避免影响现有启动链路,bridge.LoadSession 的“not found -> upsert”仍保留(已加 TODO,后续在 TUI Submit 显式创建会话后再移除)。
  • Linux 终端弹窗能力仍未实现,不影响 URL 到 Gateway 的唤醒链路。

测试与验证

自动化测试

  • 已补充/更新:
    • URL Scheme 注册(Windows/macOS/Linux/unsupported)单测
    • CLI url-register 参数解析、默认路径、错误透传、跳过 preload/silent check
    • wake.run 编排、session_id 回传、上下文脱离取消回归
    • Runtime CreateSession 空 ID 自生成行为
    • TUI HydrateSession 与工作区接管相关测试
  • 全量回归:
    • go test ./...

手工验收建议

  1. 执行 neocode url-register
  2. 在浏览器访问 neocode://run?prompt=写一个简单的HTTP服务器
  3. 预期:唤醒成功 -> 终端拉起(Win/macOS)-> neocode --session <id> 接管 -> run 正常开始
  4. Linux 预期:唤醒到 dispatch/gateway 正常,终端弹窗若不支持则给出明确提示

风险与回滚

  • 风险主要在平台级注册差异与本机环境工具缺失(如 xdg-mimelsregister)。
  • 回滚策略:
    • 代码层回滚到本 PR 前版本;
    • 系统注册可按 README 提供的手动清理步骤恢复。

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed in four passes (code quality, performance, security, docs). I found two actionable issues worth fixing before relying on this flow in production.

if d.launchTerminalFn == nil {
return DispatchResult{}, newDispatchError(ErrorCodeInternal, "terminal launcher is unavailable")
}
launchCommand := fmt.Sprintf("neocode --session %s", sessionID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium – brittle launch command: this hard-codes neocode from PATH (neocode --session ...). In the URL-scheme flow, dispatch may be invoked via an absolute executable path that is not on PATH, so the new terminal can open but fail with command not found. Consider launching with the resolved current executable path (or carrying forward the registered absolute path) instead of a bare command name.


// escapeDoubleQuotedShellLiteral 转义双引号 shell 字符串中的反斜杠与引号,避免路径截断。
func escapeDoubleQuotedShellLiteral(value string) string {
escaped := strings.ReplaceAll(value, `\`, `\\`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High – shell injection/expansion risk in generated launcher script: escapeDoubleQuotedShellLiteral escapes only \\ and ", but values embedded in double-quoted shell strings still expand $... and backticks. A crafted executable path can change command behavior when the script runs. Please also escape $ and ` (or avoid shell interpolation by writing a script that does not embed untrusted text directly).

@pionxe
Copy link
Copy Markdown
Collaborator Author

pionxe commented Apr 28, 2026

TUI会话水化是什么意思

可以想象一块干瘪的海绵,当它吸满水后,才具有了擦拭桌子的真实弹性和功能,这过程就是水化。
HydrateSession方法,就是把网关里的历史数据(水),精准地注入到 TUI 那个空壳的状态模型(海绵)里。

Comment thread internal/app/bootstrap.go
return nil, nil, err
}
if sessionID := strings.TrimSpace(opts.SessionID); sessionID != "" {
if err := tuiApp.HydrateSession(ctx, sessionID); err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里有点迷惑,为什么app层会主动调用tui的业务逻辑

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot 请你解析app层、cli层、gateway层以及tui层的职责是什么,他们之间的依赖关系是怎么样的,这里bootstrap.go调用update.go的逻辑是否合理?

Copy link
Copy Markdown

@fennoai fennoai Bot Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论

这个调用整体是合理的,但有一个可优化点:app 层现在直接调用了 tui.App 的具体方法(HydrateSession),耦合略高;从职责上看不越层,但从依赖形态上看可以进一步收敛成启动参数/Init 命令。

分层职责(按当前代码实现)

  1. app 层(internal/app
  • 职责:启动装配与依赖注入。
  • 证据:NewProgram 负责组装配置、远程 runtime 适配器、TUI 实例与资源清理(internal/app/bootstrap.go:243-279)。
  • 不承载协议路由,不执行工具业务。
  1. cli 层(internal/cli
  • 职责:命令入口、参数解析、进程生命周期、把命令请求路由到 app/gateway。
  • 证据:根命令解析 --workdir/--session,调用 app.NewPrograminternal/cli/root.go:68-84);gateway 子命令负责启动网关与 URL register/dispatch(internal/cli/gateway_commands.go)。
  1. gateway 层(internal/gateway
  • 职责:协议边界与转发编排,向下通过 RuntimePort 调 runtime。
  • 证据:handleWakeOpenURLFrame 中先 CreateSessionRun,并做鉴权与 ACK 编排(internal/gateway/bootstrap.go:111-166)。
  1. tui 层(internal/tui
  • 职责:事件消费 + UI 状态渲染与交互。
  • 证据:HydrateSession 通过 runtime.LoadSession 拉会话快照并同步前端状态(internal/tui/core/app/update.go:1450-1477);TUI 只依赖 tui/services.Runtime 契约(internal/tui/services/runtime_contract.go:14-27)。

依赖关系(当前)

cli -> app -> tui/services.Runtime(适配器) -> gateway -> runtime

补充:gateway 侧 runtime 接入通过 bridge 实现(internal/cli/gateway_runtime_bridge.go),符合“通过契约接线”的方向。

关于“bootstrap.go 调用 update.go 逻辑是否合理”

合理,原因如下:

  1. update.go 只是文件名,HydrateSession 本质是 tui.App 的启动期状态装载 API,不是“运行期输入事件处理”专属逻辑。
  2. 调用时机在 program.Run() 前,属于启动装配流程的一部分(internal/app/bootstrap.go:266-273)。
  3. 该调用没有绕开 Runtime/Gateway,仍通过 a.runtime.LoadSession 取数据(internal/tui/core/app/update.go:1460),没有跨层直连 provider/tool。

需要注意的点(非 blocker):

  1. app 层当前依赖 tui.App 具体方法,而非纯启动契约;若后续希望进一步解耦,可把 hydration 变成 NewWith... 的 options 或 Init cmd,由 TUI 内部在启动时自行执行。

View job run

@1024XEngineer 1024XEngineer deleted a comment from fennoai Bot Apr 28, 2026
// LoadSession 加载指定会话详情。
LoadSession(ctx context.Context, input LoadSessionInput) (Session, error)
// CreateSession 创建并返回可用会话标识。
CreateSession(ctx context.Context, input CreateSessionInput) (string, error)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xgopilot 在这里createsession接口函数的定义是否必须的?

Copy link
Copy Markdown

@fennoai fennoai Bot Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论

在这个 PR 的目标下,RuntimePort.CreateSession(...) 是有必要的(不是绝对唯一方案,但在现有契约下是最小且清晰的方案)。

原因:

  1. wake.openUrl(action=run) 需要在 ACK 里回传最终 session_id,用于后续 neocode --session <id> 接管。
  2. 现有 RuntimePort.Run(...) 是“受理即返回”的异步接口(仅返回 error),不会同步返回 session_id
  3. 会话 ID 生成职责已经下放到 Runtime(而不是 Gateway),因此 Gateway 不能自己生成 ID,只能通过 Runtime 明确创建/获取。

对应代码链路:

  • internal/gateway/bootstrap.go:142 先调用 runtimePort.CreateSession(...)
  • internal/gateway/bootstrap.go:150 再发起 handleRunFrame(...)
  • internal/gateway/contracts.go:309 定义 RuntimePort.CreateSession(...)

可选替代(如果后续要简化接口)

可以把 Run 改为同步返回 acceptedSessionID(例如 Run(...) (RunAccepted, error)),这样可删除独立 CreateSession。但这会影响现有 gateway/runtime 运行契约和调用方,改动面更大。

本次我只做了分析答复,没有改代码。

View job run

pionxe added 17 commits April 28, 2026 19:51
- root 命令树移除 url-register/url-dispatch,并清理对应 preload/update 特判
- dispatcher 删除原始 URL 解析入口,仅保留 DispatchWakeIntent
- 删除跨平台 RegisterURLScheme 实现与相关测试
- protocol 删除 ParseNeoCodeURL/ParseError,保留 WakeIntent 与 WakeAction 支持集
- 重写/调整 urlscheme 与 cli 相关测试以覆盖新链路
- 安装脚本移除 url-register 自动调用,仅保留 daemon install
- 网关 RPC 示例生成器将 wake.raw_url 样例改为 http://neocode:18921
- README 与网关相关文档移除 neocode:// 与 url-* 命令说明
- 更新升级指引中的静默更新跳过策略描述
@pionxe pionxe force-pushed the feat/url-wake-session-hydration-mvp branch from a45853e to de34f90 Compare April 28, 2026 12:06
@phantom5099
Copy link
Copy Markdown
Collaborator

@xgopilot 给我详细的补充下这个PR实际上都做了什么,具体修改了什么内容

@fennoai
Copy link
Copy Markdown

fennoai Bot commented Apr 28, 2026

这个 PR 实际做了什么(按链路拆解)

你这条 PR 现在的实际内容,已经不只是 neocode://run,而是把“URL 唤醒 + 会话接管”扩展成了两条入口并行:

  1. neocode://... 协议入口(wake IPC)
  2. http://neocode:<port>/run|/review 入口(HTTP daemon)

核心闭环已经落到代码里:外部唤醒 -> Gateway wake.openUrl -> Runtime 会话创建/运行 -> 终端拉起 -> neocode --session 启动后水化并可自动提交。

关键改动(模块维度)

  1. Runtime 会话创建下放
  • internal/runtime/runtime.goCreateSession 支持空 ID 自动生成(session_*),不再要求上层先造 ID。
  • internal/gateway/contracts.go:新增 RuntimePort.CreateSession(...) 契约。
  • internal/cli/gateway_runtime_bridge.go:bridge 接入 CreateSession,并保留 LoadSession not found -> upsert 兼容路径(带 TODO)。
  1. Gateway wake.run / wake.review 编排收敛
  • internal/gateway/handlers/wake.gorun/review 参数校验改为区分“首次触发”和“按 session_id 恢复”。
  • internal/gateway/rpc_dispatch.go:对 IPC 来源的 wake.openUrl 走 ACL 放行逻辑,避免二次鉴权阻塞唤醒。
  1. Dispatcher 从“仅转发”变成“转发 + 终端接管”
  • internal/gateway/adapters/urlscheme/dispatcher.go
    • 入口改为 DispatchWakeIntent(不再依赖 URL 二次解析)
    • ACK 必须拿到 session_id
    • 收到 run/review ACK 后执行终端拉起命令
    • 首次点击会拼 --wake-input-b64,恢复点击只用 --session
    • Linux 终端能力返回 not_supported 明确错误
  • internal/gateway/launcher/terminal_*.go:新增 Win/macOS/Linux 三平台终端拉起实现(Linux 当前明确不支持自动弹终端)。
  1. TUI 启动接管能力补齐
  • internal/cli/root.go:新增 --session 与隐藏参数 --wake-input-b64,透传到 bootstrap。
  • internal/app/bootstrap.go:启动阶段先 HydrateSession(session_id),再解析 wake-input-b64 注入一次性启动输入。
  • internal/tui/core/app/update.go
    • 新增 HydrateSession:水化历史消息/标题/todo/活动会话
    • 新增 ConfigureStartupWakeInput + startupWakeSubmitMsg:启动后自动走正常 submit 链路
    • 会话 workdir 接管时,路径失效给 warning 并保留当前 workdir
  1. CLI 命令面有明显重构(这是本 PR 的“大变化”)
  • 删除了旧的 url-dispatch 子命令(internal/cli/gateway_commands.go)。
  • 新增 daemon 命令组(internal/cli/daemon_commands.go):
    • daemon serve:起本地 HTTP daemon
    • daemon install/uninstall/status:自启动安装与状态
    • daemon encode run/review:生成可点击唤醒链接
  1. HTTP daemon 新增(比 PR 描述多出来的一条线)
  • internal/gateway/adapters/urlscheme/daemon.go:新增 /run/review/healthz 路由,将 HTTP 请求映射为 WakeIntent 后派发到 gateway。
  • daemon_autostart_{windows,darwin,linux,stub}.go:用户态自启动配置实现。
  • 支持 session_id/workdir/prompt/path 参数校验与透传。
  1. 系统注册与安装脚本联动
  • URL/平台命令错误映射与可执行路径校验收敛到 registry_common.go
  • 安装脚本 scripts/install.sh / scripts/install.ps1 增加 best-effort 注册/安装动作(失败告警不阻断)。
  1. 文档同步
  • README.md 增补唤醒使用与排障。
  • 新增 docs/guides/http-daemon-wake-user-guide.md,并同步到 www/guide/... 与 vitepress 侧栏。
  • Gateway RPC 文档与示例同步 wake 行为变更。

一句话总结“实际落地结果”

这版 PR 已把“外部点击 -> 会话生成/接管 -> 终端启动 -> TUI 自动恢复并可直接执行”的链路跑通;并且在实现上从早期“单纯 URL dispatch”演进成了“wake IPC + HTTP daemon 双入口 + 平台自启动/注册”的完整可用方案。

View job run

@phantom5099 phantom5099 merged commit 67b0108 into 1024XEngineer:main Apr 28, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants