package main import ( "flag" "fmt" "os" "strings" "time" "github.com/linguo2625469/workbuddy2api-panel/internal/syncer" ) // usageHead 帮助文本前半段。 const usageHead = `wb2api-launcher —— WorkBuddy2API 独立启动器(同步凭证 + 拉起网关 + 后台对账) 用法: wb2api-launcher [标志] 常用: wb2api-launcher 同步一次 → 启动网关 → 后台每 5 分钟对账 wb2api-launcher -status 只看两边差异,不启动网关(有差异退出码 2) wb2api-launcher -sync-only 同步一次就退出 wb2api-launcher -no-sync 离线启动(跳过同步,用本地已有凭证) wb2api-launcher -no-watch 不起后台对账 wb2api-launcher -mode pull 只拉不推 wb2api-launcher -version 打印版本 标志: ` // usageTail 帮助文本后半段。 const usageTail = ` 退出码: 0 正常(网关退出时原样透出它的退出码) 1 启动阶段失败 2 -status 模式下存在未同步的差异 配置查找: config.json 默认取启动器同目录;相对路径一律以 config.json 所在目录为基准, 网关子进程的工作目录也会被切到那里 —— 保证"同步写的目录"就是"网关读的目录"。 ` // parseFlags 解析命令行。返回 (选项, 是否只打印版本, error)。 func parseFlags(args []string) (options, bool, error) { var ( opt options showVersion bool modeStr string statusOnly bool ) fs := flag.NewFlagSet("wb2api-launcher", flag.ContinueOnError) fs.SetOutput(os.Stderr) fs.Usage = func() { fmt.Fprint(os.Stderr, usageHead) fs.PrintDefaults() fmt.Fprint(os.Stderr, usageTail) } fs.StringVar(&opt.configPath, "config", "", "配置文件路径(默认:启动器同目录的 config.json)") fs.StringVar(&opt.serverBin, "server", "", "网关二进制路径(默认:同目录的 wb2api(.exe),再找 PATH)") fs.StringVar(&modeStr, "mode", "sync", "同步方向:sync / pull / push") fs.BoolVar(&statusOnly, "status", false, "只看差异不写入,也不启动网关(等价 -mode sync -dry-run -sync-only)") fs.BoolVar(&opt.syncOnly, "sync-only", false, "同步一次后退出,不启动网关") fs.BoolVar(&opt.noSync, "no-sync", false, "跳过同步,直接启动网关") fs.BoolVar(&opt.noWatch, "no-watch", false, "不起后台定时对账") fs.DurationVar(&opt.interval, "interval", 5*time.Minute, "后台对账间隔") fs.DurationVar(&opt.shutdownGrace, "shutdown-grace", 10*time.Second, "退出时等待网关优雅收尾的宽限期") fs.BoolVar(&opt.quiet, "quiet", false, "减少输出(仍会打印同步结果与告警)") // 同步参数覆盖(不写 config.json 也能跑) fs.StringVar(&opt.bucket, "bucket", "", "HF 桶 ID,如 a3216/wb2api-storage") fs.StringVar(&opt.token, "token", "", "HF token(更推荐写进 config.json 的 sync.token_file)") fs.StringVar(&opt.endpoint, "endpoint", "", "HF 端点(默认 https://huggingface.co)") fs.StringVar(&opt.include, "include-data", "", "额外同步的 data/ 文件名,逗号分隔(默认只同步凭证)") fs.StringVar(&opt.writer, "writer", "", "写入通道:auto / hf / relay") fs.StringVar(&opt.relayURL, "relay-url", "", "中转网关地址(relay 写入通道用)") fs.StringVar(&opt.restart, "restart-space", "", "推了新凭证后重启该 Space,如 a3216/gcli2api") fs.StringVar(&opt.caBundle, "ca-bundle", "", "额外 CA 证书包(PEM);本机跑抓包代理时给 hf CLI 用") fs.BoolVar(&opt.dryRun, "dry-run", false, "只展示将要做什么,不写任何东西") fs.BoolVar(&showVersion, "version", false, "打印版本后退出") if err := fs.Parse(args); err != nil { if err == flag.ErrHelp { // -h 走 Usage(已经打印过了),按成功退出。 return opt, true, nil } return opt, false, err } if rest := fs.Args(); len(rest) > 0 { return opt, false, fmt.Errorf("未知参数: %s(用 -h 看帮助)", strings.Join(rest, " ")) } if showVersion { return opt, true, nil } mode, reportOnly, err := syncer.ParseCommand(modeStr) if err != nil { return opt, false, err } opt.mode = mode // modeStr 写 "status" 时 ParseCommand 已告知只报告。 opt.reportOnly = reportOnly // -status 等价于「按 sync 算一遍,但不落地、不启动网关」。 if statusOnly { opt.mode = syncer.ModeSync opt.reportOnly = true opt.syncOnly = true } // 只报告就不可能写入:把 dry-run 也定死,避免出现自相矛盾的组合。 if opt.reportOnly { opt.dryRun = true } return opt, false, nil }