issues.list_issues(issues/model/api.ts:84-105) | query 逐字对齐 lib/service/issues/src/services/issues.rs:142-163 ListIssueQuery:state / author_id / milestone_id / label_id / assignee_id / priority / sort / q / limit / offset,前端发出的 10 个键一个不多一个不少;结构体带 #[serde(deny_unknown_fields)](:143),多传即 400,前端未多传。sort 走 SORT_PARAMS(model/meta.ts:206-212)= created_at/updated_at/priority/due_at/number,与 IssueSort(issues.rs:125-133,rename_all="snake_case")5 个取值完全一致;未知值被 SORT_PARAMS[k] 取成 undefined 后由 query() 丢弃,不会打出 400 |
issues.list_issues 的 priority / milestone_id 多值 | csv() 合并成 a,b,后端 parse_filter_priorities(issues.rs:181-197)按 PRIORITY_KEYS(:224-230 = none/low/medium/high/urgent)逐项校验,与 UI 的 5 个 PRIORITY_KEYS 同拼写;none 兜底走 parse_milestone_filter(:201-)的 include_no_milestone,前端 board-filters.tsx:277 传的就是 none |
issues.list_labels / list_milestones / list_events | 均为 pageOf()(api.ts:54-60 读 items/total/limit/offset),后端三者都是 Page:labels/list_labels.rs:17、milestones/list_milestones.rs:17、events/list_events.rs:16。query 也对齐:ListLabelQuery(labels.rs:82-87 有 is_archived)、ListMilestoneQuery(milestones.rs:63- 有 is_closed) |
列表 limit 未被当数组用(上一轮踩过的坑,全量复查) | 6 个分页端点的消费方全部取 .items,无一处 data?.find/?.map 直接作用在分页对象上:shell-api.ts:47,53,62、repo.ts:23、refs.ts:22,65、releases.ts:17、commits-tab.tsx:90(tags.data?.items)、tags-section.tsx:41、branches-tab.tsx。反向的 5 处 unwrapApi<T[]>(insights.ts:16,20,23、topics.ts:10、remotes.ts:21)对应端点确实返回数组:analysis/languages.rs:12、languages_read.rs:12、contributors.rs:12 均 Vec<…Dto> |
列表 limit(除确认问题 #5 外未越界) | 实测上限 100:GET …/branches?limit=101 与 ?limit=0 → 400 {"message":"limit must be between 1 and 100"},…/releases?limit=101 同。逐项核对取值:ISSUE_PAGE_SIZE=50(use-board-data.ts:21,board-view.tsx:111)、listLabels/listMilestones=100(api.ts:213,269,issues max_pagination=100 见 config/default/issues.json:2)、listEvents=50(api.ts:201)、listReleases=50(releases-tab.tsx:45)、listSpaceRepos=100(repo.ts:25)、fetchSpaceRepos=50/fetchTags=10(shell-api.ts:47,62)、listBranches/listTags 默认 100(refs.ts:19,63)——除 branches-tab.tsx/tags-section.tsx 的 HEAD 值 200 见 #5 外全部 ≤100 |
issues.update_issue 三态(milestone_id / due_at / description) | IssuePatchCmd(model/issue.ts:159-168)字段名与 UpdateIssueCmd(issues.rs:102-123)逐一对应;调用方真的按三态发:issue-detail.tsx:514 { milestone_id: value || null }(选空=null=解除)、:519 { due_at: next }(清空日期=null)。后端 Some(mid_opt) 分支(issues.rs:919-935)确认是 Option<Option<Uuid>> |
issues.update_board 三态 | board-manage-modal.tsx:122-126 发 prefix: … || null、description: … || null,后端 UpdateBoardCmd(boards.rs:50-59)这两字段是 deserialize_opt_opt 的 Option<Option<String>>,null→清空成立;name 每次必发(:123)为 Option<String>,无副作用 |
issues 的 UUID / 看板编号双寻址 | 前端混用但后端两种都认:board-view.tsx:250 打开时传 issueRef(board, issue.number)(CAD-12 或 42),issue-detail.tsx:335 等变更用 issue.id(UUID)。后端 lookup_issue_by_ref(lib/service/issues/src/share/lookup.rs:46-77)依次尝试 Uuid::parse_str → parse::<i32> → PREFIX-N,三种形态皆可;路径参数类型为 String 而非 Uuid(issues.rs:233-238 IssuePath,:30 WatchPath,comments.rs:85,events.rs:42),不会因编号 400 |
issues.create_issue | api.ts:112-128 发的 10 个键对齐 CreateIssueCmd(issues.rs:78-100),含 attachment_ids,null 折成 Option::None 合法 |
issues.bulk_issues | BulkIssuesCmd { action, issue_refs }(issues.rs:1169-1173,action 为 close/reopen)+ 1..=100 校验(:1189);前端无任何调用点(ops.ts:265 注册了,65 处调用里 0 处使用),批量改状态是逐条 close_issue,故无契约风险 |
issues.list_issue_attachments / list_labels_for_issue / list_comments_tree | 前端 listOf()(读数组),后端三者确为 Vec:attachments/list_attachments.rs:16、labels/list_labels_for_issue.rs:16、comments/list_comments_tree.rs:17 |
| 附件三段式(intent → PUT → finalize) | create_issue_attachment 无路径段(ops.ts:271-276 path:0)+ body {space_name, filename, content_type, size};finalize/delete/download 各 1 段(ops.ts:287-310),与 attachments/create_attachment.rs:11、finalize_attachment.rs:12、delete_attachment.rs:11 一致;前端 api.ts:343 手写的下载 URL 也做了 encodeURIComponent |
issues 标签/里程碑 CRUD | create_label 发 {name,color,description},CreateLabelCmd(labels.rs:53-62)匹配;update_label 的 description?: string|null 对应 UpdateLabelCmd.description: Option<Option<String>>(labels.rs:74-75)三态成立;update_milestone 的 due_at: dateApiValue(...)(board-manage-modal.tsx:446)清空时给 null,对应 Option<Option<OffsetDateTime>>(milestones.rs:53-57) |
issues.upsert_issue_watch / repos.upsert_watch | 都只发 { mode },对齐 UpsertWatchCmd { mode }(issues/src/services/watches.rs:25-27、git/src/service/watches.rs:27-29);WatchMode 带 #[serde(rename_all="snake_case")](lib/share/models/src/common/watch_mode.rs:16-19)= unwatched/watching/ignore,与前端 meta.ts:194-204 同拼写 |
repos.list_repos | query 只用 limit/offset/sort,对齐 RepoListQuery(git/src/service/repos.rs:43-48);sort:'name' 实测 200(GET /api/v1/repos/demo?limit=100&sort=name);PageQuery 未知键会 400 已实测:…/branches?limit=10&sort=name → 400 {"error":"Query deserialize error: unknown field \sort`, expected `limit` or `offset`"}` |
repos.create_repo / update_repo 字段名 | create_repo 发的 8 个键与 CreateRepoCmd(repos.rs:203-219)逐一对应(含 parent_dir_id);update_repo 的 description/website/is_private/enable_lfs/default_branch/tab_order/allow_pull_requests 与 UpdateRepoCmd(:221-238)一一对应,无错名(语义问题见确认问题 #4)。features-settings.tsx:67 把 RepoDetail 展开当 body 虽显冗余,但 UpdateRepoCmd 未开 deny_unknown_fields,多余键被忽略,不是 400 |
branches.list_branches / tags.list_tags 响应 | 实测 Page 形状:{"items":[{…}],"total":1,"limit":100,"offset":0};前端 {items,total} 取值正确,total 缺失时兜底 items.length 不会误判 |
tags.create_tag / create_branch / rename_branch body 名 | createTag 发 {name,target,message,annotated},对齐 CreateTagCmd(git/src/service/tags.rs:23-30,annotated: bool 而非 annotate);createBranch 发 {name,start_point},对齐 CreateBranchCmd(service/branches.rs:43-47);renameBranch 发 {new_name},对齐 RenameBranchCmd(handler/branches/rename.rs:14-17) |
tags.list_tags 的 tag_type 判定 | 前端 tags-section.tsx:98 比较 'Annotated',后端 TagType(rpcgit/tags/types.rs:18-22)没有 rename_all,序列化即 "Lightweight" / "Annotated",首字母大写,比较成立(不是 snake_case 陷阱) |
releases 的 assets / 字段名 | 后端 ReleaseDto(git/src/service/releases.rs:21-36)没有 assets 字段,前端也没用(releases-tab.tsx 只用 tag_name/title/num_commits/is_draft/is_prerelease/publisher_name/created_at),无"把不存在的 assets 当数组遍历";sha 与 target 并存,前端不取 sha,无 sha vs id 误用 |
releases.list_releases query | 发 {limit,offset,draft},对齐 ListReleasesQuery(releases.rs:40-50,deny_unknown_fields,含 draft: Option<bool>);实测 ?draft=false → 200,?sort=x → 400 {"error":"Query deserialize error: unknown field \sort`, expected one of `limit`, `offset`, `draft`"},前端确实不发 sort` |
analysis.* | languages_read(GET)与 languages(POST)同路径不同方法,ops.ts:46-49 与 handler/analysis/languages.rs:12(POST 重扫)、languages_read.rs:12(GET 缓存)对应,rescanLanguages 用 POST 正确;两者响应都是 Vec<LanguageStatDto>,前端 unwrapApi<LanguageStat[]> 正确(实测 GET …/analysis/languages 返回数组)。code_activity 的 branch 对齐 CodeActivityQuery(code_activity.rs:11-14)。contributors 无 query、返回 Vec(contributors.rs:17) |
repos topics | add/remove 的 topic_name 为单段路径,路由 /{space_name}/{repo_name}/topics/{topic_name}(routes.rs:287),topic 名不合法含 /,无编码风险;list_repo_topics 后端返回 Vec,前端 unwrapApi<TopicRow[]> 正确 |
| OPS 路径段数与路由段数 | 抽查全部所用 op:get_board/update_board/delete_board=2、list_issues=2、get/update/close/reopen/delete_issue=3、bind/unbind_label=4、branches.*=2 或 3、releases.*=2 或 3、analysis.*=2,与对应路由声明一一吻合;无一处"段数不足"。65 个调用点全部命中 OPS 表,MISSING_OPS 与 ALIASES 无一处兜底,故无 G 类幽灵端点 |