# cli-lib **Repository Path**: tjqm/cli-lib ## Basic Information - **Project Name**: cli-lib - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-15 - **Last Updated**: 2026-05-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Command SDK — AI-友好的命令编排框架 **写一遍业务逻辑,CLI / Web / Cron / gRPC / AI Agent 五点复用。** 三行接入,250+ 单测/基准/fuzz,`go test -race` 全绿,`go vet` 干净。 > **状态:v0.4.0(beta)** — 功能稳定、测试齐全,但在 v1.0 之前 API 仍可能调整(v0.2 → v0.3 → v0.4 均有破坏性改动,详见 [CHANGELOG.md](CHANGELOG.md))。生产用请固定到具体版本号。 > > **License:** [MIT](LICENSE) --- ## 5 秒看懂 ```go import sdk "gitee.com/tjqm/cli-lib/sdk" app := sdk.New("myapp", "My App", sdk.WithConfigPath("cmds.yaml")) // 写一遍 handler app.Handle(1, func(ctx *sdk.Context) error { ctx.Output("status", "healthy") return nil }) // ① CLI: $ myapp run 1-10 // ② HTTP API: POST /api/v1/runs {"expression":"1-10"} // ③ 程序里: app.Exec([]int{1,2,3}, nil) // ④ gRPC handler: app.ExecCtx(ctx, ids, inputs) // ⑤ AI Agent: app.Schema(FormatOpenAI) → function-calling JSON // 或使用 HandleT:struct tag 即 schema,不用在 YAML 里重复定义 sdk.HandleT(2, "抓取日K线", func(ctx *sdk.Context, in DailyIn) (DailyOut, error) { return DailyOut{Count: 120, Source: "akshare"}, nil }) ``` 每个命令在 YAML 里声明元信息: ```yaml commands: 1: { name: ping, title: Ping, description: 健康检查, auto_safe: true } 78: name: publish title: 发布 auto_safe: false # 有副作用 review_gate: true # 需要人工审核 rollback: 79 # 出错时回滚 scenarios: daily: { command: "1-10" } automation: level: review # shadow → live 五级 ``` --- ## 多种入口,同一次注册 | 入口 | 代码 | 典型场景 | |---|---|---| | CLI | `app.Run()` 后用 `myapp run 41` | 运维终端操作 | | **Daemon** | `myapp serve -a :8080` | 长驻服务:HTTP + cron + 健康检查 | | Web | `app.Web()` → gin 路由 | 管理后台 / REST API / **SSE 流式** | | **MCP** | `app.MCP()` | Claude Desktop / Cursor / AI Agent 直接调 | | 程序化 | `app.Exec(ids, inputs)` | cron / gRPC / MQ 消费 | | **带取消** | `app.ExecCtx(ctx, ids, inputs)` | 需要超时控制的调用 | --- ## 安装 ```bash go get gitee.com/tjqm/cli-lib/sdk ``` 最小项目只引 `sdk`。内部 `core/` 包(expr / registry / recorder)可独立引用,但大多数场景不需要。 --- ## 快速开始 ### 1. CLI 工具(30 秒) ```go // main.go package main import sdk "gitee.com/tjqm/cli-lib/sdk" func main() { app := sdk.New("hello", "Hello CLI") app.Handle(1, func(ctx *sdk.Context) error { ctx.Output("msg", "hello world") return nil }) app.Run() } ``` ```bash go run main.go run 1 # → ● Step 1: hello world ... 完成 # outputs: { msg: "hello world" } ``` ### 2. Web API(1 分钟) ```go r := gin.Default() web, _ := app.Web() // 鉴权中间件(gin 原生支持) api := r.Group("/api/v1") api.Use(yourAuthMiddleware) // JWT / API Key / IP 白名单 web.RegisterRoutes(api) r.Run(":8080") ``` ```bash curl -X POST http://localhost:8080/api/v1/runs \ -d '{"expression":"1","inputs":{"date":"2026-05-13"}}' # 同步:直接返回结果 # {"run_id":"...", "status":"done"} # 异步:不阻塞长时间流程 curl -X POST http://localhost:8080/api/v1/runs \ -d '{"expression":"1-54", "async":true}' # {"run_id":"...", "status":"pending"} # 轮询: GET /api/v1/runs/:run_id ``` ### 3. Prometheus 接入(30 秒) ```go app := sdk.New("myapp", "My App", sdk.WithMetrics()) web, _ := app.Web() web.RegisterMetrics(r.Group("/api/v1")) // GET /api/v1/metrics 现在输出: // sdk_steps_total{id="1",name="ping",status="done"} 42 // sdk_steps_duration_seconds{id="1",name="ping"} ... // sdk_runs_active 1 ``` 也支持 `sdk.WithMetricsRegistry(customRegisterer)` 与 gin 生态的 prometheus 中间件共用。 ### 4. 程序化调用(定时任务 / gRPC) ```go // cron c.AddFunc("0 9 * * 1-5", func() { app.ExecScenario("daily", map[string]interface{}{"date": today()}) }) // gRPC(带超时 + 可取消) ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() outputs, err := app.ExecCtx(ctx, ids, req.Inputs) ``` --- ## 类型化注册 (HandleT) — 新写法 旧写法 `app.Handle` 需要同时维护 YAML 里的 `inputs`/`outputs` 和 Go 代码里的 `ctx.InputWithDefault`——两处定义易不同步。HandleT 让 **struct tag 成为 schema 的唯一来源**。 ### 四种注册 API ```go // 有入参有出参 — HandleT sdk.HandleT(2, "日K线", func(ctx *sdk.Context, in DailyIn) (DailyOut, error) { return DailyOut{Count: 120}, nil }) // 有入参无出参 — HandleNoOut sdk.HandleNoOut(31, "选股", func(ctx *sdk.Context, in SelectIn) error { return nil }) // 无入参有出参 — HandleNoIn sdk.HandleNoIn(5, "交易日历", func(ctx *sdk.Context) (CountOut, error) { return CountOut{Count: 242}, nil }) // 无入参无出参 — HandleVoid sdk.HandleVoid(95, "同步守护", func(ctx *sdk.Context) error { return nil }) ``` ### struct tag 推导规则 | tag | 效果 | |---|---| | `json:"field_name"` | Input/Output 字段名 | | `json:"field,omitempty"` | 可选字段 | | `json:"-"` | 跳过(不出现在 schema 中) | | `required:"true"` | 必填,缺失时 Exec 返回错误 | | `default:"值"` | 默认值,自动填充 | | `example:"…"` | 示例值(展示用) | | `hint:"…"` | 字段提示(展示用) | Go 类型自动映射:`string`→string, `int`/`int64`→int, `float64`→float, `bool`→bool, 复杂类型 (slice/map/struct 指针) → string (JSON)。 匿名嵌入 struct 的字段会自动展开,YAML 中同 ID 的旧 inputs/outputs 会被 HandleT 覆盖。 ### 旧写法 ↔ 新写法对比 ```go // ===== 旧写法 ===== app.Handle(1, func(ctx *sdk.Context) error { date := ctx.InputWithDefault("date", "today") // 手工取值 ctx.Output("result", "ok") // 手工写输出 return nil }) // YAML 里还要写: inputs: [{name: date, default: today}], outputs: [result] // ===== 新写法 ===== type MyIn struct { Date string `json:"date" default:"today"` } type MyOut struct { Result string `json:"result"` } HandleT(1, "示例", func(ctx *sdk.Context, in MyIn) (MyOut, error) { return MyOut{Result: "ok"}, nil }) // YAML 里不需要 inputs/outputs——struct 即 schema ``` 新旧写法可混用。已有 `app.Handle` 命令不受影响,新命令可以用 HandleT 省掉 YAML 里的 schema 维护。 > 详见 `examples/handlet_demo/`。 --- ## 安全机制 ### AutomationLevel 五档灰度 ``` shadow → review → draft → publish → live (只看) (人审) (警告) (自动) (全自动) ``` | 等级 | review_gate 命令 | auto_safe=false 命令 | |---|---|---| | shadow | ❌ 硬阻止 | ❌ 硬阻止 | | review | ⚠️ CLI 弹 [y/N] | ❌ 硬阻止 | | draft | ⚠️ CLI 弹 [y/N] | ⚠️ 警告后可跳过 | | publish | 自动通过 | ⚠️ 警告后可跳过 | | live | 自动通过 | 自动通过 | 闸门对 **CLI / Web / 程序化 / MCP 四条入口统一生效**(共用 `checkGate`)。非交互入口(Web async/SSE、`Exec*`、MCP)弹不了 `[y/N]`,对「需人工确认」的命令按交互模式下「拒绝」的语义降级: - `review_gate` 命令 → 返回 `ErrGateBlocked`,终止运行(调用方可 `errors.Is` 判别) - `auto_safe=false` 命令 → 跳过该步,继续后续 也就是说:**想让 cron / AI Agent 自动执行高风险命令,必须显式把 `automation.level` 调到 publish/live** —— level 本身就是那个「可调节的刹车」。 对 AI Agent 的价值:**给 LLM 的自主度加一个可调节的刹车**。开发时 shadow,上线后 review,信任后 live。 ### 实盘安全示例 ```yaml 71: name: live:place_order auto_safe: false review_gate: true ``` ```bash $ myapp run 71 -i symbol=AAPL -i side=buy -i qty=100 ⚠ 审核闸门: 步骤 71 (实盘下单) 描述: 调用券商 API 下单(不可逆动作) 是否批准? [y/N]: N → 阻止。AI 写错策略也下不了单。 ``` --- ## 断点续跑 (Resume) 7 步流程在第 4 步挂了,只重试第 4 步开始: ```bash $ myapp run 41 # step 4 由于外部 API 超时失败 → run.json: step 1-3 done, step 4 failed $ myapp resume 20260513_222132_b0c07053 → 从 step 4 开始,step 1-3 的历史完整保留 ``` 不需要担心幂等问题——前面成功的步骤不会再跑。 --- ## 测试:AI 写单测 3 行起步 ```go import ( "testing" sdk "gitee.com/tjqm/cli-lib/sdk" "gitee.com/tjqm/cli-lib/sdk/testkit" ) func TestMyHandler(t *testing.T) { app := testkit.TestApp(t, ` commands: 1: { name: ping, title: Ping, description: hi, auto_safe: true } `) app.Handle(1, func(ctx *sdk.Context) error { ctx.Output("ok", true) return nil }) out := testkit.MustExec(t, app, []int{1}, nil) // out = {"ok": true} } ``` - `testkit.TestApp(t, yaml)` — 一行构造 App,tempdir 自动清理 - `testkit.SimpleApp(t, "1:a:A", "2:b:B")` — 更短的声明 - `testkit.MustExec(t, app, ids, inputs)` — 失败直接 Fatal 每个 handler 一个测试。AI 看模板照抄,10 秒一个。 --- ## AI 协作工作流 ### 1. 导出工具定义给 LLM ```go tools, _ := app.Schema(sdk.SchemaFormatAnthropic) // 直接传给 Claude API 的 tools 字段 ``` 产出: ```json { "name": "cmd_71_live_place_order", "description": "命令 71: 实盘下单。调用券商 API 下单(不可逆动作)。⚠ review_gate", "input_schema": { "type": "object", "properties": { "symbol": { "type": "string" }, "side": { "type": "string" }, "qty": { "type": "string" } }, "required": ["symbol", "side", "qty"] } } ``` LLM 拿到这些 tool 后,`expression` + `inputs` 就能调命令,比构造嵌套 JSON 嵌套数组少出错。 ### 2. 把 run.json 当复盘数据 每次 run 都自动写 JSON 记录——喂给 AI 分析为什么某个 handler 输出不对: ```bash $ cat reports/runs/.../run.json # 复制 → 粘贴给 AI: # "为什么 step 11 的 rsi 输出 85 但 step 31 的 signal 是 hold?" ``` ### 3. 项目模板 `examples/quant/` 是一个可直接复制的量化骨架,附带 CLAUDE.md 规则: - ID 编号区间约定(数据 1-9、因子 11-29、信号 31-49、回测 51-69、实盘 71-89) - 每个新 handler 复制最近的模板改 - 实盘动作必须 `review_gate: true` - 必须写单测 把这个当脚手架,新项目直接从结构到测试都有模有样。 --- ### 4. MCP Server — 一行接入 Claude Desktop ```go app.MCP() // 所有命令自动变成 MCP tools ``` 然后在 Claude Desktop 的 `claude_desktop_config.json` 中配置: ```json { "mcpServers": { "myapp": { "command": "go", "args": ["run", "/path/to/your/main.go"] } } } ``` Claude Desktop 重启后就能直接发现并使用所有命令: > **用户**: "检查一下系统状态" > **Claude**: 好的,我来调用 system:checkup... > > **用户**: "下单 100 股 AAPL" > **Claude**: ⚠ 这个操作有 review_gate 保护,需要你确认... 每个命令的 YAML inputs、review_gate、auto_safe 等信息自动映射到 MCP tool 的 description 和 inputSchema 中,LLM 不需要额外配置就知道哪些参数必填、哪些操作有风险。 --- ## 表达式速查 | 格式 | 示例 | 结果 | |---|---|---| | 单号 | `41` | [41] | | 列表 | `1,2,3` | [1, 2, 3] | | 范围 | `1-10` | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | | 排除 | `1-10,!7` | [1, 2, 3, 4, 5, 6, 8, 9, 10] | | 组合 | `1,5-8,!7` | [1, 5, 6, 8] | CLI 也支持 `--skip "31-35,!33"` 和 `--from 21 --until 42`。 --- ## 性能设计要点 | 决策 | 理由 | |---|---| | `map[string]interface{}` 而非 `map[string]any` + typed | 与 YAML/JSON 生态兼容,零序列化开销 | | 无反射、无 ORM、无 DI 容器 | 启动速度最快,二进制最小 | | `sync.Mutex` 窄锁(只锁 registry 加载) | 大部分路径无锁竞争 | | gin 集成而非自建 HTTP | 复用 gin 的零分配 router + 成熟生态 | | Recorder 异步写可选(async 模式) | 不影响 HTTP 响应延迟 | | 固定 striped lock(64 条)而非动态 mutex map | 并发 Web 请求内存上限常数 | 已知上限:单步 handler 开销约 2-5 μs(context 创建 + hook 触发 + logger),主要耗时在用户 handler 内部。十万级 QPS 的 CRUD 场景不要用来做主链路——它定位是编排任务,不是高频在线服务。 --- ## 项目结构 ``` ├── sdk/ 对外包,新项目只引这里 │ ├── sdk.go App / Handle / Exec / ExecCtx / Run │ ├── context.go 命令上下文 (IO + logger + cancel) │ ├── options.go 配置选项 │ ├── schema.go AI function-calling schema 导出 │ ├── web.go Gin 集成(REST + SSE + async + 优雅关闭) │ └── testkit/ 测试辅助(TestApp / SimpleApp / MustExec) ├── core/ 内部实现,一般不用直接碰 │ ├── expr/ 表达式解析 │ ├── registry/ YAML 加载 + 复合命令递归 │ └── recorder/ run.json / SQLite 双实现 + Resume + 幂等存储 ├── examples/ 可直接复制的样板 │ ├── quant/ 量化骨架(推荐新人从这里开始) │ ├── demo/ CLI 全功能演示(旧写法 app.Handle) │ ├── handlet_demo/ HandleT 类型化注册演示(新旧对比) │ ├── web/ Web API 演示(含鉴权示例) │ ├── programmatic/ 程序化调用演示 │ ├── mcp/ MCP Server 示例(Claude Desktop 集成) │ ├── newsflow/ eino LLM 真实接入 + 审批闸门 + SSE │ ├── prescription/ 业务领域示例(处方流转) │ └── logtool/ 单文件 CLI 工具示例 ├── config/ 默认配置模板 ├── docs/ 架构与测试指南(architecture.md / testing.md / ports.md) ├── FEATURES.md 功能全览速查(16 个 section) ├── QUICKSTART.md 5 分钟快速上手 ├── CHANGELOG.md 变更记录 └── README.md ``` --- ## 完整 API 速览 ```go // 创建 App app := sdk.New(name, desc, opts...) // 注册 handler(旧写法) app.Handle(id, func(ctx *sdk.Context) error { ... }) // 注册 handler(新写法,struct tag 即 schema) sdk.HandleT[In, Out](id, name, func(ctx *sdk.Context, in In) (Out, error) { ... }) sdk.HandleNoIn[Out](id, name, func(ctx *sdk.Context) (Out, error) { ... }) sdk.HandleNoOut[In](id, name, func(ctx *sdk.Context, in In) error { ... }) sdk.HandleVoid(id, name, func(ctx *sdk.Context) error { ... }) // 同步执行 outputs, err := app.Exec([]int{1, 11}, inputs) // 同步 + 超时/取消 outputs, err := app.ExecCtx(ctx, []int{1, 11}, inputs) // 按表达式 outputs, err := app.ExecExpr("1-10,!5", inputs) outputs, err := app.ExecExprCtx(ctx, "1-10,!5", inputs) // 按场景 outputs, err := app.ExecScenario("daily", inputs) outputs, err := app.ExecScenarioCtx(ctx, "daily", inputs) // 清理过期记录 n, err := app.Cleanup() // Web 集成 web, err := app.Web() web.RegisterRoutes(ginRouter) // /runs(同步+async)、/runs/stream(SSE)、 // /runs/:id/resume、/runs/:id/cancel、/commands… web.RegisterAdmin(ginRouter) // 嵌入管理看板 (GET /) web.RegisterMetrics(ginRouter) // 暴露 /metrics (Prometheus) web.SetEngine(ginEngine) // 设置 engine 引用(供 Serve 优雅关闭) web.Serve(":8080") // 启动 HTTP + 监听信号优雅关闭 web.Close() // 停止后台 goroutine(Serve 时自动调用) // 一站式 daemon(HTTP + cron 调度器 + 健康检查 + 优雅关闭) app.ServeDaemon(":8080") // MCP 服务器(Claude Desktop / Cursor) app.MCP() // AI schema data, _ := app.Schema(sdk.SchemaFormatOpenAI) data, _ := app.Schema(sdk.SchemaFormatAnthropic) // 生命周期 hook app.OnHook(func(a *sdk.App, phase string, ctx *sdk.Context) { // phase: before_step / after_step / after_step_failed }) // Context 方法 ctx.ID() // 当前命令 ID ctx.Command() // 元信息 ctx.Input("key") // 读输入 (interface{}) ctx.InputString("key") // 读输入 (string) ctx.InputWithDefault("key", "fallback") // 带默认 ctx.Bind(&myStruct) // 类型安全:inputs 按 json tag 映射到结构体 ctx.Output("key", val) // 写输出 ctx.OutputFrom(&myStruct) // 类型安全:结构体按 json tag 批量写入 outputs ctx.Outputs() // 所有输出 ctx.Ctx() // context.Context(可取消/超时) ctx.Logger() // *slog.Logger(结构化日志) ctx.IsDryRun() // 是否预演模式 // 配置选项 sdk.WithConfigPath(path) sdk.WithReportDir(dir) sdk.WithVersion("1.0.0") sdk.WithDryRun(true) sdk.WithPassOutput(false) sdk.WithLogger(slogLogger) sdk.WithRequestTimeout(30 * time.Second) sdk.WithAuth(ginMiddleware) // 注入鉴权中间件,所有 Web 路由自动生效 sdk.WithMetrics() // Prometheus metrics(默认 registerer) sdk.WithMetricsRegistry(registerer) // 自定义 registerer sdk.WithStore(store) // 自定义持久化 Store(默认 FileStore) sdk.WithDB("runs.db") // GORM + SQLite 持久化(进程重启不丢) sdk.WithIdempotencyStore(idemStore) // 自定义幂等存储(多副本共享后端) sdk.WithRunTTL(7*24*time.Hour) // 运行记录 TTL(7 天自动删除) sdk.WithCleanupInterval(30*time.Minute) // 后台清理间隔(默认 1h) ``` ### 持久化与多副本 | 选项 | 后端 | 适用 | |---|---|---| | 默认 | `FileStore` — `reports/runs//run.json` | 单实例 / CLI | | `WithDB(dsn)` | GORM + 内嵌 SQLite,进程重启不丢;初始化失败自动降级文件存储 | 单实例长期服务 | | `WithStore(s)` | 任意自定义 `recorder.Store` 实现 | 接 PG / MySQL 等 | 幂等存储默认基于文件(仅单实例有效);设了 `WithDB` 时**自动复用同一 SQLite 连接**,多副本只要共享数据库幂等就跨实例生效。也可用 `WithIdempotencyStore` 注入 Redis 等自定义后端。所有 `runID` 在文件存储层做路径穿越校验。 --- ## 后续规划 ### 已完成(v0.3 / v0.4) - [x] MCP Server — `app.MCP()` - [x] Prometheus metrics — `WithMetrics()` + `/metrics` - [x] Recorder 后端接口化 — `Store` + FileStore + DBStore(SQLite) + `WithStore()` / `WithDB()` - [x] Run 记录 TTL 自动清理 - [x] Idempotency Key — `WithDB` 时自动跨副本生效 - [x] 流式 SSE — `POST /runs/stream` + 程序化 `WithSSEChannel(ch)` - [x] Web Admin 只读仪表盘 — `web.RegisterAdmin(router)` - [x] 命令热加载(fsnotify)— 500ms 去抖 - [x] HandleT 类型化注册 — struct tag 即 schema - [x] DAG 并行执行 + 步骤级 retry/timeout(含 full-jitter 指数退避) - [x] Scenario 级 cron 调度 — `StartScheduler` / `ServeDaemon` - [x] 哨兵错误集中(`errors.Is`)+ registry 引用完整性校验 ### v1.0 (中期) - [ ] 分布式锁(Redis,多实例安全) - [ ] 命名空间(团队隔离) - [ ] 命令版本管理 + 弃用机制 - [ ] Web Admin UI — 完整版(DAG 可视化 + 操作按钮) ### v2.0 (长期) - [ ] 字符串层级 ID (`billing.invoice.generate`) - [ ] Skill 包管理 + 分发 (`skillctl install`) - [ ] 语义搜索(embedding 向量召回) - [ ] 分布式 workflow 引擎(跨进程 retry / saga) --- ## 与其他方案的对比 | | 本 SDK | Temporal | LangChain Tools | 手撸 gin | |---|---|---|---|---| | 三入口复用 | ✅ | ❌ | ❌ | ❌ | | ReviewGate 安全护栏 | ✅ | ❌ | ❌ | ❌ | | 断点续跑 | ✅ | ✅ | ❌ | 需自建 | | AI schema 一键导出 | ✅ | ❌ | ✅ | ❌ | | 学习曲线 | 分钟级 | 周级 | 小时级 | 分钟级 | | 多步骤编排 | ✅ | ✅✅ | ⭐⭐ | 需自建 | | 分布式可靠 | ❌ | ✅✅ | ❌ | 视实现 | | MCP 协议 | ✅ | ❌ | ❌ | ❌ | **定位**:比 Temporal 轻(不需要集群),比 LangChain 严肃(有审计和安全),比手撸 gin 多了编排/回放/护栏。 --- ## 贡献 欢迎提 Issue / PR。 新策略加一份单测(参考 `sdk/testkit/` 模板)、写在 CHANGELOG、跑 `go vet ./... && go test -race ./...`。