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
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ install-skills:
@./scripts/install_skills.sh

docs-gateway:
@go run ./scripts/generate_gateway_rpc_examples
@go run -tags gatewaydocgen ./scripts/generate_gateway_rpc_examples.go

docs-gateway-check:
@go run ./scripts/generate_gateway_rpc_examples
@git diff --exit-code -- docs/reference/gateway-rpc-api.md
@go run -tags gatewaydocgen ./scripts/generate_gateway_rpc_examples.go
@git diff --exit-code -- docs/generated/gateway-rpc-examples.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ go run ./cmd/neocode url-dispatch --url "neocode://review?path=README.md"

1. `NEOCODE_GATEWAY_BIN` 显式路径
2. `PATH` 中 `neocode-gateway`
3. 回退当前可执行 `neocode gateway`
3. `PATH` 中 `neocode` 并追加子命令 `gateway`

## 安装脚本

Expand Down
2 changes: 1 addition & 1 deletion docs/gateway-detailed-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ stateDiagram-v2

1. `NEOCODE_GATEWAY_BIN` 显式路径
2. `PATH` 中的 `neocode-gateway`
3. 当前可执行回退 `neocode gateway`
3. `PATH` 中的 `neocode` 并追加子命令 `gateway`

约束:仅允许一次受控回退,失败后返回确定性错误。

Expand Down
24 changes: 12 additions & 12 deletions docs/gateway-rpc-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ type BindStreamParams struct {
{
"jsonrpc": "2.0",
"id": "bind-1",
"result": {
"type": "ack",
"action": "bind_stream",
"request_id": "bind-1",
"session_id": "sess-1",
"run_id": "run-1",
"result": {
"type": "ack",
"action": "bind_stream",
"request_id": "bind-1",
"session_id": "sess-1",
"run_id": "run-1",
"payload": {
"message": "stream binding updated",
"channel": "ws"
Expand Down Expand Up @@ -182,12 +182,12 @@ type RunParams struct {
{
"jsonrpc": "2.0",
"id": "run-req-1",
"result": {
"type": "ack",
"action": "run",
"request_id": "run-req-1",
"session_id": "sess-1",
"run_id": "run-1",
"result": {
"type": "ack",
"action": "run",
"request_id": "run-req-1",
"session_id": "sess-1",
"run_id": "run-1",
"payload": {
"message": "run accepted"
}
Expand Down
14 changes: 13 additions & 1 deletion internal/gateway/adapters/urlscheme/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,9 @@ func (d *Dispatcher) launchGateway(ctx context.Context, listenAddress string, re
LaunchMode: spec.LaunchMode,
ResolvedExec: spec.Executable,
})
if err := startGatewayFn(spec); err != nil {
launchSpec := spec
launchSpec.Args = buildGatewayLaunchArgs(spec.Args, listenAddress)
if err := startGatewayFn(launchSpec); err != nil {
d.emitLaunchDecisionLog(launchDecisionLogEntry{
RequestID: requestID,
Method: string(protocol.MethodWakeOpenURL),
Expand Down Expand Up @@ -415,6 +417,16 @@ func (d *Dispatcher) launchGateway(ctx context.Context, listenAddress string, re
return nil
}

// buildGatewayLaunchArgs 构造自动拉起参数,确保子进程监听地址与调度重拨地址一致。
func buildGatewayLaunchArgs(baseArgs []string, listenAddress string) []string {
args := append([]string(nil), baseArgs...)
normalizedListenAddress := strings.TrimSpace(listenAddress)
if normalizedListenAddress == "" {
return args
}
return append(args, "--listen", normalizedListenAddress)
}

// waitGatewayReady 在单次回退窗口内轮询网关连通性,超时后返回确定性错误。
func (d *Dispatcher) waitGatewayReady(ctx context.Context, listenAddress string) error {
nowFn := d.nowFn
Expand Down
26 changes: 25 additions & 1 deletion internal/gateway/adapters/urlscheme/dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"net"
"os"
"reflect"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -1282,11 +1283,13 @@ func TestDispatcherLaunchGatewayBranches(t *testing.T) {
})

t.Run("start gateway failed", func(t *testing.T) {
var capturedSpec launcher.LaunchSpec
dispatcher := &Dispatcher{
resolveLaunchSpecFn: func() (launcher.LaunchSpec, error) {
return launcher.LaunchSpec{LaunchMode: launcher.LaunchModePathBinary, Executable: "/tmp/neocode-gateway"}, nil
},
startGatewayFn: func(launcher.LaunchSpec) error {
startGatewayFn: func(spec launcher.LaunchSpec) error {
capturedSpec = spec
return errors.New("start failed")
},
}
Expand All @@ -1295,6 +1298,9 @@ func TestDispatcherLaunchGatewayBranches(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "start failed") {
t.Fatalf("expected start failed error, got %v", err)
}
if !reflect.DeepEqual(capturedSpec.Args, []string{"--listen", "stub://gateway"}) {
t.Fatalf("launch args = %#v, want %#v", capturedSpec.Args, []string{"--listen", "stub://gateway"})
}
})
}

Expand Down Expand Up @@ -1497,6 +1503,24 @@ func TestDispatcherEmitLaunchDecisionLogNilGuards(t *testing.T) {
dispatcher.emitLaunchDecisionLog(launchDecisionLogEntry{})
}

func TestBuildGatewayLaunchArgs(t *testing.T) {
t.Run("appends listen argument when provided", func(t *testing.T) {
got := buildGatewayLaunchArgs([]string{"gateway"}, " unix:///tmp/neocode.sock ")
want := []string{"gateway", "--listen", "unix:///tmp/neocode.sock"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("buildGatewayLaunchArgs() = %#v, want %#v", got, want)
}
})

t.Run("keeps base args when listen is empty", func(t *testing.T) {
got := buildGatewayLaunchArgs([]string{"gateway"}, " ")
want := []string{"gateway"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("buildGatewayLaunchArgs() = %#v, want %#v", got, want)
}
})
}

type cancelOnWriteErrorConn struct {
stubDispatchConn
cancel context.CancelFunc
Expand Down
17 changes: 6 additions & 11 deletions internal/gateway/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const (
LaunchModeExplicitPath = "explicit_path"
// LaunchModePathBinary 表示命中 PATH 中的 neocode-gateway。
LaunchModePathBinary = "path_neocode_gateway"
// LaunchModeFallbackSubcommand 表示回退到当前可执行的 gateway 子命令。
// LaunchModeFallbackSubcommand 表示回退到 PATH 中 neocode 的 gateway 子命令。
LaunchModeFallbackSubcommand = "fallback_neocode_gateway_subcommand"
)

Expand All @@ -31,9 +31,9 @@ type ResolveOptions struct {
}

// ResolveGatewayLaunchSpec 解析网关可执行发现顺序:
// 显式路径(NEOCODE_GATEWAY_BIN) > PATH(neocode-gateway) > 当前可执行 + gateway 子命令。
// 显式路径(NEOCODE_GATEWAY_BIN) > PATH(neocode-gateway) > PATH(neocode) + gateway 子命令。
func ResolveGatewayLaunchSpec(options ResolveOptions) (LaunchSpec, error) {
return resolveGatewayLaunchSpecWithDeps(options, exec.LookPath, os.Executable)
return resolveGatewayLaunchSpecWithDeps(options, exec.LookPath)
}

// StartDetachedGateway 以非阻塞方式拉起网关进程并释放父进程句柄。
Expand All @@ -55,7 +55,6 @@ func StartDetachedGateway(spec LaunchSpec) error {
func resolveGatewayLaunchSpecWithDeps(
options ResolveOptions,
lookPathFn func(string) (string, error),
executableFn func() (string, error),
) (LaunchSpec, error) {
resolveByLookup := func(binary string) (string, error) {
resolved, err := lookPathFn(strings.TrimSpace(binary))
Expand Down Expand Up @@ -84,18 +83,14 @@ func resolveGatewayLaunchSpecWithDeps(
}, nil
}

currentExecutable, err := executableFn()
resolvedFallbackExecutable, err := resolveByLookup("neocode")
if err != nil {
return LaunchSpec{}, fmt.Errorf("resolve current executable: %w", err)
}
trimmedCurrentExecutable := strings.TrimSpace(currentExecutable)
if trimmedCurrentExecutable == "" {
return LaunchSpec{}, fmt.Errorf("resolve current executable: empty executable path")
return LaunchSpec{}, err
}

return LaunchSpec{
LaunchMode: LaunchModeFallbackSubcommand,
Executable: trimmedCurrentExecutable,
Executable: resolvedFallbackExecutable,
Args: []string{"gateway"},
}, nil
}
37 changes: 16 additions & 21 deletions internal/gateway/launcher/launcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) {
}
return "", errors.New("unexpected lookup")
},
func() (string, error) {
return "/usr/local/bin/neocode", nil
},
)
if err != nil {
t.Fatalf("resolveGatewayLaunchSpecWithDeps() error = %v", err)
Expand All @@ -47,9 +44,6 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) {
}
return "", errors.New("unexpected lookup")
},
func() (string, error) {
return "/usr/local/bin/neocode", nil
},
)
if err != nil {
t.Fatalf("resolveGatewayLaunchSpecWithDeps() error = %v", err)
Expand All @@ -65,14 +59,18 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) {
}
})

t.Run("fallback to current executable subcommand", func(t *testing.T) {
t.Run("fallback to neocode subcommand", func(t *testing.T) {
spec, err := resolveGatewayLaunchSpecWithDeps(
ResolveOptions{},
func(string) (string, error) {
return "", errors.New("not found")
},
func() (string, error) {
return "/usr/local/bin/neocode", nil
func(binary string) (string, error) {
switch binary {
case "neocode-gateway":
return "", errors.New("not found")
case "neocode":
return "/usr/local/bin/neocode", nil
default:
return "", errors.New("unexpected lookup")
}
},
)
if err != nil {
Expand All @@ -95,23 +93,20 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) {
func(string) (string, error) {
return "", errors.New("missing")
},
func() (string, error) {
return "/usr/local/bin/neocode", nil
},
)
if err == nil {
t.Fatal("expected explicit lookup error")
}
})

t.Run("fallback fails when current executable unavailable", func(t *testing.T) {
t.Run("fallback fails when neocode is unavailable", func(t *testing.T) {
_, err := resolveGatewayLaunchSpecWithDeps(
ResolveOptions{},
func(string) (string, error) {
return "", errors.New("not found")
},
func() (string, error) {
return "", errors.New("unavailable")
func(binary string) (string, error) {
if binary == "neocode-gateway" || binary == "neocode" {
return "", errors.New("not found")
}
return "", errors.New("unexpected lookup")
},
)
if err == nil {
Expand Down
Loading