diff --git a/docs/roadmap.md b/docs/roadmap.md index 2454eb9ae..45d5222c6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ ## 当前总目标 -推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file`,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,下一步重点是真实 Runner adapter。 +推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file`,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补 `agenthub-runner-mock` preset 入口,下一步重点是真实 Runner adapter。 ## 路线图分层 @@ -26,7 +26,7 @@ - [x] M1 客户端本地链路:Desktop Shell + Local Edge + Mock Runner + smoke test。 - [ ] M2 Edge 本地数据层:Project / Thread / Run / Item / EventStore。最小内存实现已在 PR #30,message/item 写入链路、Runner lifecycle 边界、store 接口边界、轻量 JSON 文件持久化实现和 `--store-file` 启动参数已补齐,SQLite 仍是后续可选评估项。 -- [ ] M3 真实 Runner:CLI Agent 进程、取消、日志、错误映射。本地进程 executor、本地进程工作目录边界和 generic adapter profile / 命令模板最小层已补齐,但还不是 Claude Code / Codex / OpenCode 的完整 adapter。 +- [ ] M3 真实 Runner:CLI Agent 进程、取消、日志、错误映射。本地进程 executor、本地进程工作目录边界、generic adapter profile / 命令模板最小层和仓库自带 mock Runner preset 已补齐,但还不是 Claude Code / Codex / OpenCode 的完整 adapter。 - [ ] M4 Workspace 能力:worktree、diff、preview、artifact、approval。 - [ ] M5 Hub 协作链路:Edge-Hub sync、远程查看、远程审批。 @@ -34,7 +34,7 @@ - 前端:从 Mock 数据过渡到真实 REST / WebSocket client,承接 UI 同学设计。 - 后端:实现 Hub Server、Edge-Hub 通信、账号/群聊/同步/中继能力。 -- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 和 `feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,后续继续做真实 Runner adapter。 +- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 和 `feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 增加可测试的 `agenthub-runner-mock` preset 入口,后续继续做真实 Runner adapter。 ## 验收门槛 @@ -53,6 +53,7 @@ - [x] 增加 Edge 本地进程 executor 边界,支持 stdout/stderr、成功、失败和取消事件映射。 - [x] 为 Edge 本地进程 executor 增加工作目录配置边界;这只是本地进程 workdir 能力,不是完整真实 Runner adapter。 - [x] 为 Edge 本地进程 executor 增加 generic adapter profile / 命令模板最小层,支持从 Run 上下文展开 args/env。 +- [x] 为 Edge 启动入口增加 `--runner-profile agenthub-runner-mock` preset,默认使用仓库自带 Runner mock CLI。 - [ ] 将 Runner 真正接入 Edge Run lifecycle,替换 handler 内置 mock flow。 - [ ] M2 完成后归档或更新 `docs/client-roadmap.md`,避免路线图重复。 - [ ] 为 Runner 真实 CLI adapter 规划最小测试夹具。 diff --git a/docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md b/docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md new file mode 100644 index 000000000..0d95856e6 --- /dev/null +++ b/docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md @@ -0,0 +1,35 @@ +# feat/client-runner-profile-preset-delicious233 路线图 + +最后更新:2026-05-23 + +## 当前目标 + +- [x] 为 Edge 启动入口增加可测试的 `agenthub-runner-mock` Runner profile preset。 + +## 写入范围 + +- `edge-server/cmd/agenthub-edge/` +- `runner/README.md` +- `docs/roadmap.md` +- `docs/roadmaps/client.md` +- `docs/roadmaps/branches/feat-client-runner-profile-preset-delicious233.md` + +## 已完成 + +- [x] 新增 `--runner-profile` CLI 参数,默认空值保持原有内置 MockExecutor 行为。 +- [x] 支持 `agenthub-runner-mock` preset,默认生成 `agenthub-runner --mock`。 +- [x] 支持 `--runner-command` 覆盖 command,同时保留 preset 默认参数。 +- [x] 保留用户追加的 `--runner-arg`、`--runner-env` 和 `--runner-workdir`。 +- [x] 对未知 profile 返回清晰配置错误。 +- [x] 修复 review 反馈:明确 `--runner-command` 只接受单个可执行入口,并补 profile + env 未知占位符回归测试。 + +## 下一步 + +- [ ] 后续分支继续接入真实 Runner adapter,不在本分支接 Claude Code / Codex / OpenCode。 + +## 验收 + +- [x] `git diff --check` +- [x] `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('api/openapi.yaml').read_text(encoding='utf-8')); print('yaml ok')"` +- [x] `cd edge-server; go test -count=1 ./...` +- [x] `cd runner; go test -count=1 ./...` diff --git a/docs/roadmaps/client.md b/docs/roadmaps/client.md index 9849a7f15..813a2a21d 100644 --- a/docs/roadmaps/client.md +++ b/docs/roadmaps/client.md @@ -12,7 +12,7 @@ ## 当前目标 -推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,后续继续补真实 Runner adapter。 +推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补仓库自带 mock Runner preset 入口,后续继续补真实 Runner adapter。 ## 近期任务 @@ -31,6 +31,7 @@ - [x] 增加可测试的本地进程 executor,覆盖 stdout/stderr 输出、正常退出、非零退出、取消和重复启动。 - [x] 增加本地进程工作目录配置,覆盖构造期目录验证和子进程实际运行目录;这不是完整 Claude Code / Codex / OpenCode adapter。 - [x] 增加 generic adapter profile / 命令模板最小层,覆盖 args/env 的 Run 占位符展开、未知占位符错误、固定 args 兼容和 workdir 不回退。 +- [x] 增加 `--runner-profile agenthub-runner-mock`,覆盖默认 command、command override、用户 args/env 追加和未知 profile 错误。 - [ ] 将真实 Runner adapter 接入 Edge Run lifecycle。 - [ ] 细化 Project / Thread / Item 的 OpenAPI 响应 schema。 diff --git a/edge-server/cmd/agenthub-edge/main.go b/edge-server/cmd/agenthub-edge/main.go index f5874777e..5a40ee0b8 100644 --- a/edge-server/cmd/agenthub-edge/main.go +++ b/edge-server/cmd/agenthub-edge/main.go @@ -15,6 +15,7 @@ import ( type config struct { Addr string StoreFile string + RunnerProfile string RunnerCommand string RunnerArgs repeatedString RunnerEnv repeatedString @@ -23,6 +24,8 @@ type config struct { type repeatedString []string +const runnerProfileAgentHubMock = "agenthub-runner-mock" + func (v *repeatedString) String() string { return fmt.Sprint([]string(*v)) } @@ -72,13 +75,17 @@ func buildConfig(args []string) (config, error) { cfg := config{} fs.StringVar(&cfg.Addr, "addr", "127.0.0.1:3210", "listen address") fs.StringVar(&cfg.StoreFile, "store-file", "", "JSON store snapshot file path") - fs.StringVar(&cfg.RunnerCommand, "runner-command", "", "local process command to execute for each run; empty uses the mock executor") + fs.StringVar(&cfg.RunnerProfile, "runner-profile", "", "runner profile preset; supported: "+runnerProfileAgentHubMock) + fs.StringVar(&cfg.RunnerCommand, "runner-command", "", "local process executable to run for each run; empty uses the built-in mock executor only when --runner-profile is empty") fs.StringVar(&cfg.RunnerWorkDir, "runner-workdir", "", "working directory for --runner-command; empty inherits the edge process working directory") fs.Var(&cfg.RunnerArgs, "runner-arg", "argument passed to --runner-command; may be repeated") fs.Var(&cfg.RunnerEnv, "runner-env", "environment variable passed to --runner-command as KEY=VALUE; may be repeated") if err := fs.Parse(args); err != nil { return config{}, err } + if err := applyRunnerProfile(&cfg); err != nil { + return config{}, err + } cfg.RunnerCommand = strings.TrimSpace(cfg.RunnerCommand) if cfg.RunnerCommand == "" && len(cfg.RunnerArgs) > 0 { return config{}, fmt.Errorf("--runner-arg requires --runner-command") @@ -98,6 +105,23 @@ func buildConfig(args []string) (config, error) { return cfg, nil } +func applyRunnerProfile(cfg *config) error { + cfg.RunnerProfile = strings.TrimSpace(cfg.RunnerProfile) + if cfg.RunnerProfile == "" { + return nil + } + switch cfg.RunnerProfile { + case runnerProfileAgentHubMock: + if strings.TrimSpace(cfg.RunnerCommand) == "" { + cfg.RunnerCommand = "agenthub-runner" + } + cfg.RunnerArgs = append(repeatedString{"--mock"}, cfg.RunnerArgs...) + default: + return fmt.Errorf("unknown --runner-profile %q; supported values: agenthub-runner-mock", cfg.RunnerProfile) + } + return nil +} + func newStoreFromConfig(cfg config) (store.Repository, error) { if cfg.StoreFile == "" { return store.New(), nil diff --git a/edge-server/cmd/agenthub-edge/main_test.go b/edge-server/cmd/agenthub-edge/main_test.go index 58e594e5d..1a873a0d2 100644 --- a/edge-server/cmd/agenthub-edge/main_test.go +++ b/edge-server/cmd/agenthub-edge/main_test.go @@ -21,6 +21,9 @@ func TestBuildConfigDefaultsToMemoryStore(t *testing.T) { if cfg.StoreFile != "" { t.Fatalf("StoreFile = %q, want empty", cfg.StoreFile) } + if cfg.RunnerProfile != "" { + t.Fatalf("RunnerProfile = %q, want empty", cfg.RunnerProfile) + } if cfg.RunnerCommand != "" { t.Fatalf("RunnerCommand = %q, want empty", cfg.RunnerCommand) } @@ -70,6 +73,85 @@ func TestBuildConfigParsesStoreFile(t *testing.T) { } } +func TestBuildConfigAppliesRunnerProfilePreset(t *testing.T) { + cfg, err := buildConfig([]string{"--runner-profile", "agenthub-runner-mock"}) + if err != nil { + t.Fatalf("buildConfig returned error: %v", err) + } + + if cfg.RunnerProfile != "agenthub-runner-mock" { + t.Fatalf("RunnerProfile = %q, want preset name", cfg.RunnerProfile) + } + if cfg.RunnerCommand != "agenthub-runner" { + t.Fatalf("RunnerCommand = %q, want preset command", cfg.RunnerCommand) + } + if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("RunnerArgs = %#v, want %#v", got, want) + } +} + +func TestBuildConfigRunnerProfileAllowsCommandOverride(t *testing.T) { + cfg, err := buildConfig([]string{ + "--runner-profile", "agenthub-runner-mock", + "--runner-command", "custom-runner", + }) + if err != nil { + t.Fatalf("buildConfig returned error: %v", err) + } + + if cfg.RunnerCommand != "custom-runner" { + t.Fatalf("RunnerCommand = %q, want custom command", cfg.RunnerCommand) + } + if got, want := []string(cfg.RunnerArgs), []string{"--mock"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("RunnerArgs = %#v, want %#v", got, want) + } +} + +func TestBuildConfigRunnerProfilePreservesUserArgOrder(t *testing.T) { + cfg, err := buildConfig([]string{ + "--runner-profile", "agenthub-runner-mock", + "--runner-arg", "--addr=127.0.0.1:0", + }) + if err != nil { + t.Fatalf("buildConfig returned error: %v", err) + } + + if got, want := []string(cfg.RunnerArgs), []string{"--mock", "--addr=127.0.0.1:0"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("RunnerArgs = %#v, want %#v", got, want) + } +} + +func TestBuildConfigRunnerProfileValidatesUserEnvTemplate(t *testing.T) { + cfg, err := buildConfig([]string{ + "--runner-profile", "agenthub-runner-mock", + "--runner-env", "PROFILE_RUN={{run.id}}", + }) + if err != nil { + t.Fatalf("buildConfig returned error: %v", err) + } + + if got, want := []string(cfg.RunnerEnv), []string{"PROFILE_RUN={{run.id}}"}; strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("RunnerEnv = %#v, want %#v", got, want) + } +} + +func TestBuildConfigRunnerProfileRejectsInvalidUserEnvTemplate(t *testing.T) { + _, err := buildConfig([]string{ + "--runner-profile", "agenthub-runner-mock", + "--runner-env", "BAD={{unknown}}", + }) + if err == nil || !strings.Contains(err.Error(), "--runner-env") || !strings.Contains(err.Error(), "unknown placeholder") { + t.Fatalf("buildConfig error = %v, want runner env unknown placeholder error", err) + } +} + +func TestBuildConfigRejectsUnknownRunnerProfile(t *testing.T) { + _, err := buildConfig([]string{"--runner-profile", "missing-profile"}) + if err == nil || !strings.Contains(err.Error(), "unknown --runner-profile") { + t.Fatalf("buildConfig error = %v, want unknown runner profile error", err) + } +} + func TestBuildConfigRejectsUnexpectedArguments(t *testing.T) { _, err := buildConfig([]string{"unexpected"}) if err == nil || !strings.Contains(err.Error(), "unexpected positional arguments") { diff --git a/runner/README.md b/runner/README.md index ffd49f58f..207a123a2 100644 --- a/runner/README.md +++ b/runner/README.md @@ -19,6 +19,22 @@ go build ./cmd/agenthub-runner go run ./cmd/agenthub-runner --mock ``` +### 通过 Edge Runner profile 启动 + +开发 Edge 本地进程接入时,可以用仓库自带 mock Runner preset: + +```powershell +agenthub-edge --runner-profile agenthub-runner-mock +``` + +需要指定本地构建产物或包装脚本时,可以覆盖 command;preset 的 `--mock` 参数仍会保留,后续 `--runner-arg` 会继续追加: + +```powershell +agenthub-edge --runner-profile agenthub-runner-mock --runner-command agenthub-runner --runner-arg --addr=127.0.0.1:0 +``` + +`--runner-command` 只能填写单个可执行文件名或可执行文件路径,不能填写 `go run ./cmd/agenthub-runner` 这类整串 shell 命令。需要走 `go run` 时,后续应先补 command + args 分离能力,或临时使用脚本 / 包装可执行文件。 + ### 可用参数 | 参数 | 默认值 | 说明 |