diff --git a/.gitignore b/.gitignore index c73f3c5..e8a4a26 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist/ smoke-report/ .playwright-mcp/ .claude/worktrees/ +dist/ diff --git a/claude-specification/project/CLAUDE.md b/claude-specification/project/CLAUDE.md index 0686712..79ebfc2 100644 --- a/claude-specification/project/CLAUDE.md +++ b/claude-specification/project/CLAUDE.md @@ -66,7 +66,7 @@ cora/ ### 命令生成机制 - 命令树在运行时从 OpenAPI Spec 动态生成,**不手写具体 API 命令** -- 资源名由三层推导(详见 `spec/spec-optimization-design.md`): +- 资源名由三层推导: 1. **Path 优先**:path 中有明确子资源段(如 `/pulls/`、`/issues/`)的,优先采用 path 信号 2. **Tag 规范化**:对 tag 做归一化(与 path 交叉验证、同义词映射、单复数统一) 3. **Fallback**:path 最后非参数段 @@ -84,6 +84,8 @@ cora/ - Discourse:注入 `Api-Key` / `Api-Username` 请求头 - Etherpad:注入 `?apikey=` query 参数 - GitCode:注入 `?access_token=` query 参数 +- GitHub:注入 `Authorization: Bearer ` 请求头 +- Jenkins / EUR:注入 `Authorization: Basic ` 请求头 - 认证参数(`Api-Key`、`access_token` 等)不生成 CLI flag,由 executor 自动注入 ### 内置服务 @@ -238,7 +240,6 @@ go run ./cmd/cora -- [flags] # 直接运行 | 日志系统设计 | `spec/logging-design.md` | | API Token 调研 | `spec/api-token-investigation.md` | | CLI 设计参考 | `spec/reference-cli-design-patterns.md` | -| Spec 优化设计 | `spec/spec-optimization-design.md` | --- diff --git a/cmd/cora/main.go b/cmd/cora/main.go index 4ca00ac..b4f62b7 100644 --- a/cmd/cora/main.go +++ b/cmd/cora/main.go @@ -5,17 +5,17 @@ import ( "os" "strings" - "github.com/cncf/cora/internal/output/color" + "github.com/opensourceways/cora/internal/output/color" "github.com/spf13/cobra" - "github.com/cncf/cora/internal/builder" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/executor" - "github.com/cncf/cora/internal/log" - "github.com/cncf/cora/internal/registry" - "github.com/cncf/cora/internal/view" - "github.com/cncf/cora/pkg/errs" + "github.com/opensourceways/cora/internal/builder" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/executor" + "github.com/opensourceways/cora/internal/log" + "github.com/opensourceways/cora/internal/registry" + "github.com/opensourceways/cora/internal/view" + "github.com/opensourceways/cora/pkg/errs" ) func main() { diff --git a/cmd/cora/services_cmd.go b/cmd/cora/services_cmd.go index 8b53ee6..b605650 100644 --- a/cmd/cora/services_cmd.go +++ b/cmd/cora/services_cmd.go @@ -12,7 +12,7 @@ import ( "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" - "github.com/cncf/cora/internal/registry" + "github.com/opensourceways/cora/internal/registry" ) // buildServicesCmd returns the `cora services` command tree. diff --git a/cmd/smoke/main.go b/cmd/smoke/main.go index 1b0c172..4f8e5f7 100644 --- a/cmd/smoke/main.go +++ b/cmd/smoke/main.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/cncf/cora/internal/smoke" + "github.com/opensourceways/cora/internal/smoke" ) func main() { diff --git a/go.mod b/go.mod index ab45336..2555300 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/cncf/cora +module github.com/opensourceways/cora go 1.23.0 diff --git a/go.sum b/go.sum index b2cd504..3a1f633 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= @@ -13,6 +15,8 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= @@ -52,14 +56,13 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= diff --git a/internal/auth/resolver.go b/internal/auth/resolver.go index 15647ac..f55001c 100644 --- a/internal/auth/resolver.go +++ b/internal/auth/resolver.go @@ -8,8 +8,8 @@ import ( "io" "net/http" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/log" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/log" ) // InjectAuth adds authentication credentials to an outgoing request based on @@ -19,6 +19,8 @@ import ( // Etherpad: injects ?apikey= into the request URL's query string. // GitCode: injects ?access_token= into the request URL's query string. // GitHub: injects Authorization: Bearer header. +// Jenkins: injects Authorization: Basic header. +// EUR: injects Authorization: Basic header. // // All providers inject credentials unconditionally when present; the server // ignores them for public endpoints and enforces them for protected ones. diff --git a/internal/auth/resolver_test.go b/internal/auth/resolver_test.go index 673be35..710f5d4 100644 --- a/internal/auth/resolver_test.go +++ b/internal/auth/resolver_test.go @@ -5,7 +5,7 @@ import ( "net/url" "testing" - "github.com/cncf/cora/internal/config" + "github.com/opensourceways/cora/internal/config" ) func newGETRequest(t *testing.T, rawURL string) *http.Request { diff --git a/internal/builder/build_test.go b/internal/builder/build_test.go index a058054..c3fb778 100644 --- a/internal/builder/build_test.go +++ b/internal/builder/build_test.go @@ -9,11 +9,11 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/spf13/cobra" - "github.com/cncf/cora/assets" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/executor" - "github.com/cncf/cora/internal/spec" - "github.com/cncf/cora/internal/view" + "github.com/opensourceways/cora/assets" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/executor" + "github.com/opensourceways/cora/internal/spec" + "github.com/opensourceways/cora/internal/view" ) // ── helpers ────────────────────────────────────────────────────────────────── diff --git a/internal/builder/command.go b/internal/builder/command.go index a87cc34..460b7d0 100644 --- a/internal/builder/command.go +++ b/internal/builder/command.go @@ -21,10 +21,10 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/spf13/cobra" - "github.com/cncf/cora/internal/auth" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/executor" - "github.com/cncf/cora/internal/view" + "github.com/opensourceways/cora/internal/auth" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/executor" + "github.com/opensourceways/cora/internal/view" ) // Build returns a *cobra.Command for the given service, populated with one diff --git a/internal/config/config.go b/internal/config/config.go index 8ca192d..a8c8f1d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,7 @@ import ( "github.com/joho/godotenv" "github.com/spf13/viper" - "github.com/cncf/cora/internal/log" + "github.com/opensourceways/cora/internal/log" ) const ( diff --git a/internal/executor/execute_more_test.go b/internal/executor/execute_more_test.go index b1d3255..91c0cc5 100644 --- a/internal/executor/execute_more_test.go +++ b/internal/executor/execute_more_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/view" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/view" ) func svcConfig(serverURL string) *config.Config { diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 1d0db3c..776cca4 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -12,12 +12,12 @@ import ( "strings" "time" - "github.com/cncf/cora/internal/auth" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/log" - "github.com/cncf/cora/internal/output" - "github.com/cncf/cora/internal/view" - "github.com/cncf/cora/pkg/errs" + "github.com/opensourceways/cora/internal/auth" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/log" + "github.com/opensourceways/cora/internal/output" + "github.com/opensourceways/cora/internal/view" + "github.com/opensourceways/cora/pkg/errs" ) const ( diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index a609fdc..aebd589 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/pkg/errs" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/pkg/errs" ) // cliErr extracts a *errs.CLIError from err; fails the test if not found. diff --git a/internal/output/formatter.go b/internal/output/formatter.go index 9d0b8e4..f342cb5 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -10,8 +10,8 @@ import ( "github.com/olekukonko/tablewriter" "gopkg.in/yaml.v3" - "github.com/cncf/cora/internal/output/color" - "github.com/cncf/cora/internal/view" + "github.com/opensourceways/cora/internal/output/color" + "github.com/opensourceways/cora/internal/view" ) // Print renders raw JSON response bytes in the requested format. diff --git a/internal/output/formatter_test.go b/internal/output/formatter_test.go index 2eea50d..688b2f4 100644 --- a/internal/output/formatter_test.go +++ b/internal/output/formatter_test.go @@ -3,7 +3,7 @@ package output import ( "testing" - "github.com/cncf/cora/internal/view" + "github.com/opensourceways/cora/internal/view" ) // --- extractItems --- diff --git a/internal/registry/builtin.go b/internal/registry/builtin.go index 669b7b8..d215bcf 100644 --- a/internal/registry/builtin.go +++ b/internal/registry/builtin.go @@ -3,9 +3,9 @@ package registry import ( "time" - "github.com/cncf/cora/assets" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/spec" + "github.com/opensourceways/cora/assets" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/spec" ) const ( diff --git a/internal/registry/registry.go b/internal/registry/registry.go index a7f95c9..3a67eb2 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -8,8 +8,8 @@ import ( "github.com/getkin/kin-openapi/openapi3" - "github.com/cncf/cora/internal/config" - "github.com/cncf/cora/internal/spec" + "github.com/opensourceways/cora/internal/config" + "github.com/opensourceways/cora/internal/spec" ) // Entry holds metadata and the spec loader for one backend service. diff --git a/internal/spec/loader.go b/internal/spec/loader.go index 55e6e4d..de67c91 100644 --- a/internal/spec/loader.go +++ b/internal/spec/loader.go @@ -12,8 +12,8 @@ import ( "github.com/getkin/kin-openapi/openapi3" - "github.com/cncf/cora/internal/log" - "github.com/cncf/cora/pkg/errs" + "github.com/opensourceways/cora/internal/log" + "github.com/opensourceways/cora/pkg/errs" ) // Loader loads an OpenAPI spec from a URL or local file, using a local cache diff --git a/internal/view/builtin.go b/internal/view/builtin.go index 989d53e..3cbec3d 100644 --- a/internal/view/builtin.go +++ b/internal/view/builtin.go @@ -253,7 +253,7 @@ var builtinViews = map[string]map[string]ViewConfig{ }, "etherpad": { - "pads/list": { + "pad/list-all-pads": { Columns: []ViewColumn{ {Field: "padID", Label: "Pad ID"}, }, diff --git a/pkg/errs/errors_test.go b/pkg/errs/errors_test.go index 2b90cfb..3ac146f 100644 --- a/pkg/errs/errors_test.go +++ b/pkg/errs/errors_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/cncf/cora/pkg/errs" + "github.com/opensourceways/cora/pkg/errs" ) // --- CLIError.Error() --- diff --git a/spec/spec-optimization-design.md b/spec/spec-optimization-design.md deleted file mode 100644 index e702bac..0000000 --- a/spec/spec-optimization-design.md +++ /dev/null @@ -1,312 +0,0 @@ -# Cora OpenAPI Spec 优化与命令生成增强设计 - -> 目标:通过三层约束(path 优先 + tag 规范化 + verb 增强),减少 CLI 命令生成对 OpenAPI spec 质量的依赖,提高健壮性。 - ---- - -## 一、现状与问题全景 - -### 1.1 两平台 Spec 差异 - -| 维度 | GitHub | GitCode | 影响 | -|------|--------|---------|------| -| 路径格式 | `/repos/{o}/{r}/...` | `/api/v5/repos/{o}/{r}/...` | Priority 3 阈值需兼容 `api/v5/` 前缀 | -| operationId | 语义化:`listRepos`, `getIssue` | 路径编码:`get_api_v5_repos_{o}_{r}_issues` | Priority 2 被跳过,损失 verb 信号 | -| Tag 数量 | 44(细粒度) | 18(粗粒度) | GitCode 每个 tag 下 ops 更多,冲突更多 | -| Tag 命名 | `repos`, `pulls`, `orgs` | `Repositories`, `Pulls`, `Organizations` | 资源名不一致 | -| 多词 Tag | 无 | `Pull Requests`, `AI hub` | kebab 化后产生 `pull-requests`、`ai-hub` | -| Tag 范围 | 一 op 一 tag | 部分 op 错标(如 `GET repos` 标 `Users`) | 资源归类错误 | - -### 1.2 已踩的坑(6 类) - -``` -TAG-NAME "Pull Requests" → resource "pull-requests"(应为 pulls) -TAG-MISPLACE GET repos/{o}/{r} tagged "Users" → 抢走 users get 动词 -TAG-SYNONYM GitHub "repos" vs GitCode "Repositories" → 两端资源名不同 -VERB-PATH POST/PATCH/DELETE 同路径 → assignees-assignees-assignees -VERB-RES verb "issues" vs res "issue" → override 失效 -OPID-STYLE get_api_v5_user → 跳过 Priority 2 → 失去 verb 信号 -``` - ---- - -## 二、设计原则(三层约束) - -**核心原则**:控制信号越靠近 API 结构层,可靠性越高。 - -``` -可靠性: path > parameters > operationId > tags -影响范围: path 决定 URL(错了API不可用)> tags 决定分类(错了API仍可用) -``` - -### Layer 1:Path 优先(结构性约束) - -Path 是 REST API 的结构性定义,不能随意修改。如果 path 已经暗示了资源归属,就应优先于 tag。 - -``` -Rule 1.1: 如果 path 包含 "/repos/{owner}/{repo}/",该 op 只属于 repos 资源 -Rule 1.2: 如果 path 包含 "/{number}/comments",verb 优先使用 "comments" -Rule 1.3: 如果 path 最后一段和 tag 名高度相关,用 path 段命名资源 -``` - -### Layer 2:Tag 规范化(语义性增强) - -Tag 是给人看的分类标签,不是严格的资源名。需要在消费前归一化。 - -``` -Rule 2.1: 多词 tag 提取核心名词 → "Pull Requests" → "PullRequest" -Rule 2.2: 将 tag 和 path 中出现的词交叉验证,优先用 path 中的简写 -Rule 2.3: 单复数归一化 → "Repositories" → "repository" ≈ "repos" -Rule 2.4: 特殊字符清理 → "Oauth2.0" → "oauth2" -``` - -### Layer 3:Verb 推导增强(当前逻辑修复) - -在 Layer 1/2 提供正确 resource 的前提下,完善 verb 推导。 - -``` -Rule 3.1: path-encoded opId 中仍可提取 HTTP method 前缀("get_api_v5_..." → "get") -Rule 3.2: 消歧优先顺序:pathContext → pathSuffix(检查 suffix≠verb)→ HTTP method -Rule 3.3: verb==res override 使用更宽松的匹配(已是 TrimSuffix) -``` - ---- - -## 三、具体修改清单 - -### 3.1 框架代码修改(`internal/builder/command.go`) - -#### 3.1.1 `resourceName()` — Tag 规范化(Layer 2) - -**当前代码:** -```go -func resourceName(op *openapi3.Operation, path string) string { - if len(op.Tags) > 0 { - return strings.ToLower(strings.ReplaceAll(op.Tags[0], " ", "-")) - } - return lastPathSegment(path) -} -``` - -**修改后:** -```go -func resourceName(op *openapi3.Operation, path string) string { - if len(op.Tags) > 0 { - return normalizeTag(op.Tags[0], path) - } - return lastPathSegment(path) -} - -// normalizeTag converts a human-readable tag into a CLI resource name, -// using path segments as a cross-reference for preferred vocabulary. -func normalizeTag(tag string, path string) string { - // Step 1: extract core noun from multi-word tags - // "Pull Requests" → "pull-request", "AI hub" → "ai-hub" - words := strings.Fields(strings.ToLower(tag)) - - // Step 2: kebab-case the tag - candidate := strings.Join(words, "-") - - // Step 3: check if path contains a preferred short form - // e.g. path "/api/v5/repos/.../pulls/..." → prefer "pulls" over "pull-requests" - pathSegs := strings.Split(strings.Trim(path, "/"), "/") - for _, s := range pathSegs { - s = strings.ToLower(s) - if s == "" || strings.HasPrefix(s, "{") || s == "api" || len(s) < 2 { - continue - } - // Skip version segments - if len(s) >= 2 && s[0] == 'v' && isDigit(s[1]) { - continue - } - // Check if path segment is a short synonym for the tag - // "pulls" ↔ "pull-requests", "repos" ↔ "repositories", "orgs" ↔ "organizations" - if isShortSynonym(s, candidate) { - return s - } - } - - return candidate -} -``` - -**影响**:`"Pull Requests"` → 自动识别为 `"pulls"`,不需要手工改 spec tag。 - -#### 3.1.2 `verbName()` — path-encoded opId 提取 method(Layer 3) - -**当前代码**跳过了 Priority 2 对 GitCode opId 的处理。 - -**新增**:在 path-encoded opId 不可用时,仍从 opId 前缀提取 HTTP method 对应的 verb: - -```go -// 在 Priority 2 之后、Priority 3 之前插入: -// Priority 2.5: for path-encoded operationIds (GitCode style), -// extract the HTTP method prefix which is always the first segment. -// "get_api_v5_user" → "get", "post_api_v5_..." → "create". -if isPathEncodedOpID(opID) { - parts := strings.SplitN(strings.ToLower(opID), "_", 2) - if len(parts) > 0 { - switch parts[0] { - case "get": - if hasTrailingParam(path) { return "get" } - return "list" - case "post": - return "create" - case "put", "patch": - return "update" - case "delete": - return "delete" - } - } -} -``` - -**影响**:GitCode 的 `get_api_v5_user` 不再走到 Priority 4 `httpMethodVerb`,而是通过 Priority 2.5 获得 `"list"`。其他 path-encoded opId 同理。 - -#### 3.1.3 `Build()` — Path 优先校验(Layer 1) - -在 `Build()` 中,对每个 operation 做 path 归属校验: - -```go -// Path-priority check: if path strongly implies a different resource, -// override the tag-derived resource. -pathRes := resourceFromPath(e.path) -if pathRes != "" && pathRes != res { - // Path contains a strong resource signal (e.g. "repos/{o}/{r}/issues") - // that conflicts with the tag. Use path-derived resource. - res = pathRes -} -``` - -`resourceFromPath()` 逻辑: - -```go -// resourceFromPath extracts the resource name from the path structure. -// Recognizes patterns like: -// /repos/{o}/{r}/issues/... → "issues" (not "repos") -// /repos/{o}/{r}/pulls/... → "pulls" (not "repos") -// /enterprises/{e}/members/... → "organizations" (maps to orgs tag) -// Returns "" if no clear signal. -func resourceFromPath(path string) string { - // Known sub-resource segments that indicate a different resource group - subResources := map[string]string{ - "issues": "issues", - "pulls": "pulls", - "branches": "branch", - "labels": "labels", - "comments": "comments", - "commits": "commit", - "releases": "release", - "tags": "tag", - "members": "member", - "collaborators": "member", - "webhooks": "webhooks", - } - segs := strings.Split(strings.Trim(path, "/"), "/") - for _, s := range segs { - if res, ok := subResources[strings.ToLower(s)]; ok { - return res - } - } - return "" -} -``` - -**影响**:即使 spec 作者把 `GET repos/{o}/{r}` 标成 `Users` tag,这个 op 也会被强制归到 `repos` 资源(因为 path 里有 `repos/...`)。 - -#### 3.1.4 消歧顺序调整(已是当前代码,不需改) - -当前消歧逻辑已经是:pathContext → pathSuffix(suffix≠verb)→ HTTP method。保持不变。 - -### 3.2 Spec 文件修改 - -#### 3.2.1 GitCode spec(`assets/openapi/gitcode/openapi.json`) - -已经改过的不再重复。仍需修正: - -| # | 端点 | 当前 tag | 修正为 | 原因 | -|---|------|---------|--------|------| -| 1 | `GET /api/v5/repos/{o}/{r}/notifications` | `Users` | `Repositories` | 不应属于 Users | -| 2 | `PUT /api/v5/repos/{o}/{r}/notifications` | `Users` | `Repositories` | 同上 | -| 3 | `GET /api/v5/user/notifications` | `Users` (already) | 保持不变 | 这个是 user 级别的通知 | - -> 注:改完 3.1 后,第 1、2 项即使不改 spec,Layer 1 path 优先校验也会自动修正。但建议还是把 spec 改对。 - -#### 3.2.2 可选:GitHub spec(`assets/openapi/github/api.github.com.json`) - -GitHub spec 质量较高,无需修改。仅作为 tag 规范化测试的参照。 - -### 3.3 CLAUDE.md 修改 - -`claude-specification/project/CLAUDE.md` 的关键架构约定部分需要更新: - -**命令生成机制一节**,将: - -``` -- 资源名 = 操作的第一个 tag(小写 kebab-case) -``` - -替换为: - -``` -- 资源名由三层推导: - 1. Path 优先:path 中有明确子资源段(如 /pulls/、/issues/)的,优先使用 path 段 - 2. Tag 规范化:对 tag 做归一化(多词→连词、与 path 交叉验证、单复数统一) - 3. Fallback:path 最后非参数段 -- 动词按优先级派生:UsingGET 后缀 → 已知动词前缀(GitCode 额外提 method)→ path 结构 → HTTP method fallback -- GitCode 风格的 path-encoded operationId 在 Priority 2.5 提取 HTTP method 前缀 -``` - -**Spec 文件规范一节**,新增: - -``` -## OpenAPI Spec 编写规范 - -添加新服务或在现有 spec 中新增端点时: - -### Tag 规范 -- 使用单数或复数短名词,避免多词组合:`"Pulls"` 而非 `"Pull Requests"` -- 与 GitHub API 保持一致命名:`repos`/`pulls`/`issues`/`users`/`orgs` -- 每个 operation 只标一个 tag(第一个 tag 用作资源名) -- Tag 含义应与 REST 资源概念一致,不应跨资源 - -### 路径中不宜出现的 Tag 映射 -- 路径是 `repos/{o}/{r}/...` 开头 → tag 应为 `Repositories` -- 路径是 `repos/{o}/{r}/pulls/...` → tag 应为 `Pulls`(不是 Repositories) -- 路径是 `repos/{o}/{r}/issues/...` → tag 应为 `Issues`(不是 Repositories) -``` - ---- - -## 四、修改影响评估 - -| 修改 | 影响范围 | 风险 | 收益 | -|------|---------|------|------| -| Tag 规范化 | `resourceName()` | 低:仅在 path 有同义词时改变行为 | 不再需要手工改 spec tag | -| Path-encoded opId 提 method | `verbName()` | 低:仅影响 GitCode opId | GitCode 命令名从 `list-user` → `get` | -| Path 优先校验 | `Build()` | 中:可能改变已有资源归属 | 彻底解决 TAG-MISPLACE 问题 | -| Spec tag 修正 | spec 文件 | 低:不影响 API 调用 | spec 自文档一致性 | -| CLAUDE.md 更新 | 文档 | 无 | 后续开发有规范可循 | - ---- - -## 五、实施顺序 - -``` -Phase 1: 框架修改 - ├── 3.1.1 resourceName() Tag 规范化 - ├── 3.1.2 verbName() path-encoded opId 提 method - ├── 3.1.3 Build() Path 优先校验 - └── 补充测试用例 - -Phase 2: Spec 修正 - └── 3.2.1 GitCode spec 残留 tag 修正 - -Phase 3: 文档更新 - └── 3.3 CLAUDE.md 规范更新 - -Phase 4: 验证 - ├── go test ./... 全量通过 - ├── 两端命令对比输出一致 - └── 已修复 case 无回归 -```