Skip to content
Open
22 changes: 18 additions & 4 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
<a href="https://go.dev/">
<img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go&logoColor=white" alt="Go Version" />
</a>
<a href="https://github.com/1024XEngineer/neo-code/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/1024XEngineer/neo-code/ci.yml?branch=main&label=CI" alt="CI Status" />
<a href="https://github.com/1024XEngineer/neo-code">
<img src="https://codecov.io/gh/1024XEngineer/neo-code/branch/main/graph/badge.svg" alt="Codecov Coverage" />
</a>
<a href="https://github.com/1024XEngineer/neo-code/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/1024XEngineer/neo-code?color=97CA00" alt="License" />
<img src="https://img.shields.io/badge/License-MIT-purple?logo=opensourceinitiative&logoColor=white" alt="License MIT" />
</a>
<a href="https://neocode-docs.pages.dev/">
<img src="https://img.shields.io/badge/Docs-Official-1677FF?logo=readthedocs&logoColor=white" alt="Docs" />
Expand All @@ -22,6 +22,7 @@
</a>
</p>


<p align="center">
<a href="https://neocode-docs.pages.dev/en/">Docs</a>
·
Expand Down Expand Up @@ -55,6 +56,8 @@ Core loop:
- Skills system for task-specific behaviors.
- MCP integration via stdio servers.
- Gateway mode with local JSON-RPC / SSE / WebSocket access.
- Feishu Adapter: Webhook and SDK long-connection ingress with live status card updates.
- Local Runner: execute tools on your local machine via WebSocket connection to a cloud Gateway — no inbound ports needed.

---

Expand Down Expand Up @@ -126,14 +129,25 @@ neocode --workdir /path/to/your/project

---

## Gateway / MCP / Skills
## Gateway / MCP / Skills / Runner

Detailed docs are intentionally split out. README keeps entry links:

- Gateway integration and protocol: `docs/guides/gateway-integration-guide.md`
- MCP configuration: `docs/guides/mcp-configuration.md`
- Skills design: `docs/skills-system-design.md`
- Runtime event flow: `docs/runtime-provider-event-flow.md`
- Feishu remote setup: `www/guide/feishu-remote-setup.md`

### CLI Quick Reference

```bash
# Start local runner daemon (connects to cloud Gateway for remote tool execution)
neocode runner --gateway-address "your-gateway.com:8080" --token-file ~/.neocode/auth.json

# Start feishu adapter (SDK mode, no public network required)
neocode feishu-adapter --ingress sdk --gateway-listen "127.0.0.1:8080"
```

---

Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@
<a href="https://go.dev/">
<img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go&logoColor=white" alt="Go Version" />
</a>
<a href="https://github.com/1024XEngineer/neo-code/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/1024XEngineer/neo-code/ci.yml?branch=main&label=CI" alt="CI Status" />
<a href="https://github.com/1024XEngineer/neo-code">
<img src="https://codecov.io/gh/1024XEngineer/neo-code/branch/main/graph/badge.svg" alt="Codecov Coverage" />
</a>
<a href="https://github.com/1024XEngineer/neo-code/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/1024XEngineer/neo-code?color=97CA00" alt="License" />
<img src="https://img.shields.io/badge/License-MIT-purple?logo=opensourceinitiative&logoColor=white" alt="License MIT" />
</a>
<a href="https://neocode-docs.pages.dev/">
<img src="https://img.shields.io/badge/Docs-Official-1677FF?logo=readthedocs&logoColor=white" alt="Docs" />
</a>
<a href="https://neocode-docs.pages.dev/guide/install">
<a href="https://neocode-docs.pages.dev/en/guide/install">
<img src="https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-4EAA25" alt="Platform" />
</a>
</p>


<p align="center">
<a href="https://neocode-docs.pages.dev/">文档</a>
·
Expand Down Expand Up @@ -56,6 +57,7 @@ NeoCode 是一个运行在本地开发环境中的 AI Coding Agent。
- MCP 接入:通过 MCP stdio server 扩展外部工具能力。
- Gateway 模式:通过本地 JSON-RPC / SSE / WebSocket 接口连接桌面端、脚本和第三方客户端。
- Feishu Adapter:支持 Webhook 与 SDK 长连接接入,并用单张状态卡片持续回传 run 状态。
- Local Runner:`neocode runner` 在本机执行工具,通过 WebSocket 主动连接云端 Gateway,无需开放入站端口。

---

Expand Down Expand Up @@ -176,6 +178,21 @@ neocode use <provider> --model <model-id>
neocode use openai --model gpt-4.1
```

#### Local Runner

在本机启动执行守护进程,主动连接云端 Gateway 接收工具执行请求。

```bash
# 启动 runner(默认连接 127.0.0.1:8080)
neocode runner

# 指定远程 Gateway 地址和 token
neocode runner --gateway-address "your-gateway.com:8080" --token-file ~/.neocode/auth.json

# 指定 Runner 名称与工作目录
neocode runner --runner-name "我的本机" --workdir /path/to/project
```

### 6. Shell 诊断代理

用于进入代理 shell、初始化 shell integration、手动触发诊断和控制自动诊断模式。
Expand Down
49 changes: 48 additions & 1 deletion docs/guides/feishu-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- 会话与运行 ID 保持实现一致:
- `session_id = "feishu_" + stableHash(chat_id)`
- `run_id = "feishu_" + stableHash(message_id)`
- #557 只新增 SDK 入站,不包含 #555 Local Runner 主动长连。
- #557 新增 SDK 入站#555 新增 Local Runner 主动长连(工具在 Runner 本机执行)

## 2. 事件执行顺序

Expand Down Expand Up @@ -105,3 +105,50 @@ SDK 模式下不要求公网回调地址,不要求 `adapter.listen/event_path/
- 默认启用签名校验(Webhook);
- 日志不会输出 `app_secret`、签名密钥、gateway token、Authorization 等敏感信息;
- 用户侧只回关键状态(受理、权限请求、完成、失败),不暴露内部堆栈和控制面细节。

## 9. Local Runner 远程工具执行(#555)

Runner 是部署在用户本机的执行守护进程,通过 WebSocket 主动连接云端 Gateway,接收工具执行请求并在本机完成。

```
飞书消息 -> Feishu Adapter (cloud) -> Gateway (cloud) -> WebSocket -> Local Runner (本机)
↑ 主动出站连接
```

### 9.1 启动 Runner

```bash
neocode runner \
--gateway-address "your-gateway:8080" \
--token-file ~/.neocode/auth.json \
--runner-name "我的本机" \
--workdir /path/to/project
```

Runner 启动后会主动连接 Gateway,注册自身并保持心跳。当飞书消息触发工具调用时,Gateway 将工具请求推送到 Runner 本机执行。

### 9.2 参数说明

| 参数 | 必填 | 默认值 | 说明 |
|------|:---:|--------|------|
| `--gateway-address` | 否 | `127.0.0.1:8080` | Gateway WebSocket 地址 |
| `--token-file` | 否 | — | Gateway 认证 token 文件路径 |
| `--runner-id` | 否 | 本机 hostname | Runner 唯一标识 |
| `--runner-name` | 否 | — | 人类可读的 Runner 名称 |
| `--workdir` | 否 | 当前目录 | Runner 工作目录 |

### 9.3 安全模型

- Runner 端验证 CapabilityToken(HMAC-SHA256 签名、TTL、AllowedTools、AllowedPaths)
- 支持 Workdir Allowlist 限制可访问路径
- 所有工具在 Runner 本机执行,结果通过 Gateway 回传飞书

### 9.4 错误翻译

当 Runner 不可用或权限不足时,Feishu Adapter 会将错误码翻译为用户可读消息:

| 错误码 | 飞书消息 |
|--------|----------|
| `runner_offline` | 本机 Runner 未连接,请在电脑上启动 `neocode runner` |
| `capability_denied` | 权限不足:当前能力令牌不允许此操作 |
| `tool_execution_failed` | 工具执行失败:{详情} |
6 changes: 4 additions & 2 deletions docs/guides/modelscope-provider-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
2. 打开登录页:<https://www.modelscope.cn/>
3. 打开 Token 页:<https://www.modelscope.cn/my/access/token>
4. 在 TUI 引导面板粘贴 token 并提交校验
5. 打开阿里云绑定页完成账号绑定:<https://www.modelscope.cn/my/settings/account>

如果返回认证或权限类错误,会自动回退并打开阿里云认证页:
<https://www.modelscope.cn/my/settings/account>
> **注意**:步骤 5 的阿里云账号绑定是必须步骤。ModelScope API 依赖阿里云账号体系进行鉴权与计费,
> 未绑定将导致 API 调用返回认证错误。如果 token 校验时提前检测到认证问题,
> TUI 会自动打开绑定页引导完成。

## 安全说明

Expand Down
42 changes: 42 additions & 0 deletions internal/cli/gateway_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"neo-code/internal/config"
"neo-code/internal/gateway"
gatewayauth "neo-code/internal/gateway/auth"
agentruntime "neo-code/internal/runtime"
"neo-code/internal/webassets"
)

Expand Down Expand Up @@ -238,6 +239,15 @@ func startGatewayServer(ctx context.Context, options gatewayCommandOptions, stat
Metrics: metrics,
})

runnerRegistry := gateway.NewRunnerRegistry(logger)
runnerToolManager := gateway.NewRunnerToolManager(
runnerRegistry,
relay,
nil, // capability signer: nil allows execution without token for MVP
30*time.Second,
logger,
)

runtimePort, closeRuntimePort, err := buildGatewayRuntimePort(signalContext, options.Workdir)
if err != nil {
return fmt.Errorf("initialize gateway runtime: %w", err)
Expand All @@ -248,6 +258,9 @@ func startGatewayServer(ctx context.Context, options gatewayCommandOptions, stat
}
}()

// 注入 Runner 工具分发器到 runtime,使 ReAct 循环中的工具调用可以通过 runner 执行
injectRunnerDispatcherIntoRuntime(runtimePort, runnerToolManager)

idleCloser := newGatewayIdleShutdownController(logger, cancelRuntime)
defer idleCloser.close()

Expand Down Expand Up @@ -294,6 +307,8 @@ func startGatewayServer(ctx context.Context, options gatewayCommandOptions, stat
AllowedOrigins: gatewayConfig.Security.AllowOrigins,
StaticFileDir: staticFileDir,
StaticFileFS: staticFileFS,
RunnerRegistry: runnerRegistry,
RunnerToolManager: runnerToolManager,
ConnectionCountChanged: func(active int) {
idleCloser.observe(active)
},
Expand Down Expand Up @@ -479,6 +494,33 @@ func defaultNewGatewayNetworkServer(options gateway.NetworkServerOptions) (gatew
return gateway.NewNetworkServer(options)
}

// injectRunnerDispatcherIntoRuntime 将 RunnerToolManager 注入到多工作区 runtime 的所有 bundle 中,
// 使 ReAct 循环中的工具调用可以通过 runner 远程执行。
func injectRunnerDispatcherIntoRuntime(runtimePort gateway.RuntimePort, runnerToolManager *gateway.RunnerToolManager) {
if runtimePort == nil || runnerToolManager == nil {
return
}

mw, ok := runtimePort.(*gateway.MultiWorkspaceRuntime)
if !ok {
return
}

dispatcher := gateway.NewRunnerToolDispatcher(runnerToolManager)

mw.InjectRunnerDispatcher(func(port gateway.RuntimePort) {
bridge, ok := port.(*gatewayRuntimePortBridge)
if !ok {
return
}
svc, ok := bridge.runtime.(*agentruntime.Service)
if !ok {
return
}
svc.SetRunnerToolDispatcher(dispatcher)
})
}

// encodeJSONLine 将对象编码为单行 JSON,并写入目标输出流。
func encodeJSONLine(writer io.Writer, payload any) error {
encoder := json.NewEncoder(writer)
Expand Down
27 changes: 27 additions & 0 deletions internal/cli/gateway_commands_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package cli

import (
"reflect"
"testing"

"github.com/spf13/cobra"

"neo-code/internal/gateway"
agentruntime "neo-code/internal/runtime"
)

func TestNormalizeGatewayLogLevel(t *testing.T) {
Expand Down Expand Up @@ -67,3 +71,26 @@ func TestMustReadInheritedWorkdir(t *testing.T) {
}
})
}

func TestInjectRunnerDispatcherIntoRuntime(t *testing.T) {
injectRunnerDispatcherIntoRuntime(nil, nil)
injectRunnerDispatcherIntoRuntime(&gatewayRuntimePortBridge{}, nil)
injectRunnerDispatcherIntoRuntime(&gatewayRuntimePortBridge{}, &gateway.RunnerToolManager{})

nonServiceBridge := &gatewayRuntimePortBridge{runtime: &runtimeStub{}}
multiNonService := gateway.NewMultiWorkspaceRuntime(nil, "", nil)
multiNonService.PreloadWorkspaceBundle("non-service", nonServiceBridge, func() error { return nil })
injectRunnerDispatcherIntoRuntime(multiNonService, &gateway.RunnerToolManager{})

service := &agentruntime.Service{}
bridge := &gatewayRuntimePortBridge{runtime: service}
multi := gateway.NewMultiWorkspaceRuntime(nil, "", nil)
multi.PreloadWorkspaceBundle("default", bridge, func() error { return nil })

injectRunnerDispatcherIntoRuntime(multi, &gateway.RunnerToolManager{})

field := reflect.ValueOf(service).Elem().FieldByName("runnerToolDispatcher")
if !field.IsValid() || field.IsNil() {
t.Fatal("runnerToolDispatcher was not injected")
}
}
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func NewRootCommand() *cobra.Command {
cmd.AddCommand(
newGatewayCommand(),
newFeishuAdapterCommand(),
newRunnerCommand(),
newWebCommand(),
newDaemonCommand(),
newShellCommand(),
Expand Down
Loading
Loading