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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ URL Scheme 派发骨架命令(EPIC-GW-02A):
go run ./cmd/neocode url-dispatch --url "neocode://review?path=README.md"
```

> `url-dispatch` 当前仅提供参数与命令骨架,实际派发能力在 EPIC-GW-02 实现。
> `url-dispatch` 会将 `neocode://` URL 转发到本地 Gateway,并输出结构化响应。
Comment thread
fennoai[bot] marked this conversation as resolved.
>
> 注意:当前 MVP 版本仅支持 `review` 动作,且必须携带 `path` 参数(如 `neocode://review?path=README.md`);其余动作会在网关侧被拦截拒绝。

设置 API Key 示例(按你使用的 provider 选择):

Expand Down
125 changes: 104 additions & 21 deletions internal/cli/gateway_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"os/signal"
"strings"
Expand All @@ -14,16 +15,22 @@ import (
"github.com/spf13/cobra"

"neo-code/internal/gateway"
"neo-code/internal/gateway/adapters/urlscheme"
)

const (
defaultGatewayLogLevel = "info"
defaultGatewayLogLevel = "info"
fallbackDispatchErrorJSON = `{"status":"error","code":"internal_error","message":"failed to encode or write error output"}`
)

var (
runGatewayCommand = defaultGatewayCommandRunner
runURLDispatchCommand = defaultURLDispatchCommandRunner
newGatewayServer = defaultNewGatewayServer
dispatchURLThroughIPC = urlscheme.Dispatch
exitProcess = os.Exit
writeDispatchError = writeURLDispatchErrorOutput
writeDispatchSuccess = writeURLDispatchSuccessOutput
)

type gatewayCommandOptions struct {
Expand All @@ -36,6 +43,20 @@ type urlDispatchCommandOptions struct {
ListenAddress string
}

type urlDispatchSuccessOutput struct {
Status string `json:"status"`
ListenAddress string `json:"listen_address"`
Action string `json:"action"`
RequestID string `json:"request_id,omitempty"`
Payload any `json:"payload,omitempty"`
}

type urlDispatchErrorOutput struct {
Status string `json:"status"`
Code string `json:"code"`
Message string `json:"message"`
}

type gatewayServer interface {
ListenAddress() string
Serve(ctx context.Context, runtimePort gateway.RuntimePort) error
Expand Down Expand Up @@ -114,10 +135,11 @@ func newURLDispatchCommand() *cobra.Command {
options := &urlDispatchCommandOptions{}

cmd := &cobra.Command{
Use: "url-dispatch [url]",
Short: "Dispatch a neocode:// URL to gateway (skeleton)",
SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
Use: "url-dispatch [url]",
Short: "Dispatch a neocode:// URL to gateway",
SilenceUsage: true,
SilenceErrors: true,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
urlValue := strings.TrimSpace(options.URL)
if urlValue == "" && len(args) == 1 {
Expand All @@ -131,36 +153,97 @@ func newURLDispatchCommand() *cobra.Command {
return err
}

return runURLDispatchCommand(cmd.Context(), urlDispatchCommandOptions{
dispatchErr := runURLDispatchCommand(cmd.Context(), urlDispatchCommandOptions{
URL: normalizedURL,
ListenAddress: strings.TrimSpace(options.ListenAddress),
})
if dispatchErr != nil {
Comment thread
fennoai[bot] marked this conversation as resolved.
exitProcess(1)
return nil
}
return nil
},
}

cmd.Flags().StringVar(&options.URL, "url", "", "neocode:// URL to dispatch")
cmd.Flags().StringVar(&options.ListenAddress, "listen", "", "gateway listen address override (reserved for EPIC-GW-02)")
cmd.Flags().StringVar(&options.ListenAddress, "listen", "", "gateway listen address override")

return cmd
}

// defaultURLDispatchCommandRunner 提供 url-dispatch 的默认骨架行为,明确告知后续步骤接管实现。
func defaultURLDispatchCommandRunner(_ context.Context, _ urlDispatchCommandOptions) error {
return errors.New("url-dispatch is not implemented yet (planned in EPIC-GW-02)")
// defaultURLDispatchCommandRunner 执行 URL 唤醒请求并将结果以结构化 JSON 输出。
func defaultURLDispatchCommandRunner(ctx context.Context, options urlDispatchCommandOptions) error {
result, err := dispatchURLThroughIPC(ctx, urlscheme.DispatchRequest{
RawURL: options.URL,
ListenAddress: options.ListenAddress,
})
if err != nil {
writeErr := writeDispatchError(os.Stderr, err)
if writeErr != nil {
_ = writeURLDispatchFallbackErrorOutput(os.Stderr)
}
exitProcess(1)
return nil
}

if err := writeDispatchSuccess(os.Stdout, result); err != nil {
writeErr := writeDispatchError(os.Stderr, err)
if writeErr != nil {
_ = writeURLDispatchFallbackErrorOutput(os.Stderr)
}
exitProcess(1)
return nil
}
return nil
}

// normalizeDispatchURL 校验并标准化 url-dispatch 输入,确保只接受 neocode scheme
// normalizeDispatchURL url-dispatch 输入做最小归一化,详细校验交由 dispatcher 完成
func normalizeDispatchURL(rawURL string) (string, error) {
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return "", fmt.Errorf("invalid --url %q: %w", rawURL, err)
normalized := strings.TrimSpace(rawURL)
if normalized == "" {
return "", errors.New("missing required --url or positional <url>")
}
if !strings.EqualFold(strings.TrimSpace(parsed.Scheme), "neocode") {
return "", fmt.Errorf("invalid --url scheme %q: must be neocode", parsed.Scheme)
}
if strings.TrimSpace(parsed.Host) == "" {
return "", errors.New("invalid --url: missing action host")
return normalized, nil
}

// writeURLDispatchSuccessOutput 将 url-dispatch 成功结果输出为结构化 JSON。
func writeURLDispatchSuccessOutput(writer io.Writer, result urlscheme.DispatchResult) error {
return encodeJSONLine(writer, urlDispatchSuccessOutput{
Status: "ok",
ListenAddress: result.ListenAddress,
Action: string(result.Response.Action),
RequestID: result.Response.RequestID,
Payload: result.Response.Payload,
})
}

// writeURLDispatchErrorOutput 将 url-dispatch 错误结果输出为结构化 JSON。
func writeURLDispatchErrorOutput(writer io.Writer, err error) error {
code := "internal_error"
message := err.Error()

var dispatchErr *urlscheme.DispatchError
if errors.As(err, &dispatchErr) {
code = dispatchErr.Code
message = dispatchErr.Message
}

return parsed.String(), nil
return encodeJSONLine(writer, urlDispatchErrorOutput{
Status: "error",
Code: code,
Message: message,
})
}

// writeURLDispatchFallbackErrorOutput 在结构化错误输出失败时提供兜底 JSON,避免命令静默退出。
func writeURLDispatchFallbackErrorOutput(writer io.Writer) error {
_, err := fmt.Fprintln(writer, fallbackDispatchErrorJSON)
return err
}

// encodeJSONLine 将对象编码为单行 JSON,并写入目标输出流。
func encodeJSONLine(writer io.Writer, payload any) error {
encoder := json.NewEncoder(writer)
encoder.SetEscapeHTML(false)
return encoder.Encode(payload)
}
20 changes: 20 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

var launchRootProgram = defaultRootProgramLauncher
var newRootProgram = app.NewProgram
var runGlobalPreload = defaultGlobalPreload

// GlobalFlags 描述 CLI 根命令当前支持的全局参数。
type GlobalFlags struct {
Expand All @@ -35,6 +36,12 @@ func NewRootCommand() *cobra.Command {
Short: "NeoCode coding agent",
SilenceUsage: true,
Args: cobra.NoArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if shouldSkipGlobalPreload(cmd) {
return nil
}
return runGlobalPreload(cmd.Context())
},
RunE: func(cmd *cobra.Command, args []string) error {
flags.Workdir = strings.TrimSpace(settings.GetString("workdir"))
return launchRootProgram(cmd.Context(), app.BootstrapOptions{
Expand Down Expand Up @@ -75,3 +82,16 @@ func defaultRootProgramLauncher(ctx context.Context, opts app.BootstrapOptions)
_, err = program.Run()
return err
}

// defaultGlobalPreload 预留给根命令全局预加载逻辑,默认不执行重度配置加载。
func defaultGlobalPreload(_ context.Context) error {
return nil
}

// shouldSkipGlobalPreload 判断当前命令是否应跳过全局预加载逻辑。
func shouldSkipGlobalPreload(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(cmd.Name()), "url-dispatch")
}
Loading
Loading