{"components":{"parameters":{"Cursor":{"description":"上一页返回的 next_cursor。用游标而非 offset —— 数据在变,offset 会漏/会重。","in":"query","name":"cursor","schema":{"type":"string"}},"IdempotencyKey":{"description":"同一个 key 重放会返回首次的结果,不会重复创建。**CI 重试是常态**,强烈建议带上\n(例如用 commit sha + job id 拼)。\n","in":"header","name":"Idempotency-Key","schema":{"maxLength":128,"type":"string"}},"Limit":{"in":"query","name":"limit","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}},"PathID":{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}},"responses":{"BadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"请求不合法"},"Conflict":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"状态冲突(如设备被占、run 已终态无法取消)"},"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"不存在,或该 Key 无权访问(两者不作区分,避免探测)"},"PayloadTooLarge":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"包太大"},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"缺失或无效的 API Key"}},"schemas":{"Build":{"properties":{"build_number":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"description":"展示名","type":"string","可改":null},"package_name":{"description":"装机匹配键,由 manifest 解析得来,不可改。","type":"string"},"platform":{"enum":["android","ios"],"type":"string"},"project_id":{"format":"uuid","type":"string"},"size_bytes":{"format":"int64","type":"integer"},"version":{"type":"string"}},"required":["id","project_id","package_name"],"type":"object"},"CaseHealth":{"description":"这条用例的历史战绩(不含本次,排除环境错误那些 run —— 设备离线既不能证明用例好、\n也不能证明用例坏)。用来区分「用例本身有问题」与「回归」。\n\n2026-07-29 全量回归实测:14 条失败里 11 条从未通过;而**曾通过过**的那 3 条,\n正是独立看截图判定为「控件点不中」的那 3 条 —— 两条互不依赖的证据链指向同一批。\n","properties":{"hint":{"description":"给人看的一句话结论;无明确结论时为空","type":"string"},"never_passed":{"description":"样本≥3 且一次没通过 → true。**这是提示不是裁决**:从来没对过的用例,\n这次挂了不可能是\"回归\",更可能是用例本身有问题。样本不足时恒为 false ——\n宁可不提示,不可误提示。\n","type":"boolean"},"passed":{"description":"其中通过的次数","type":"integer"},"runs":{"description":"统计窗口(最近 20 次)内的执行次数,不含本次","type":"integer"}},"required":["runs","passed","never_passed"],"type":"object"},"CreateTestRunRequest":{"properties":{"auto_install":{"default":false,"description":"所选设备未装该包时先装再跑。CI 场景基本都要开。","type":"boolean"},"build_id":{"format":"uuid","type":"string"},"device_selector":{"$ref":"#/components/schemas/DeviceSelector"},"plan_ids":{"description":"要跑的用例;与 `suite` 二选一。\n\n**目前只支持一条。** 发多条会返回 400 而不是只跑第一条 ——\n后者会让一次「只执行了 1/3」的运行报成 PASSED,而调用方从返回体里\n看不出任何异常。要跑多条请用 `suite`(它有顺序/并发/重试配置),\n或每条用例发一次请求。\n","items":{"format":"uuid","type":"string"},"maxItems":1,"type":"array"},"project_id":{"format":"uuid","type":"string"},"queue_timeout_seconds":{"default":0,"description":"无可用设备时最多排队多久(秒);超时报 `device_unavailable`,错误信息里会写明等了多久。\n\n**0 或不传 = 不排队**,挑不到就立刻返回 —— 这是老行为,不给没要求过排队的\n调用方悄悄改变时序。\n\n**上限 280 秒,由服务端强制。** 这是同步 HTTP 调用,网关对本 API 的读超时是\n300 秒;要得再多也兑现不了,等过了头连接会被网关掐断,你看到的会是网络错误\n而不是这里定义的 409。需要更长的排队请在 CI 侧重试。\n","maximum":280,"type":"integer"},"suite":{"description":"套件(方案)名或 id;与 `plan_ids` 二选一。套件按其配置的顺序/并发跑多条用例,\n返回的 id 是【执行 id】,查询用同一个 `GET /test-runs/{id}` —— 对调用方而言\n\"跑一条\"和\"跑一套\"是同一套接口,不必分两条链路写代码。\n","type":"string"},"webhook_url":{"description":"终态回调。载荷带 `X-Shiyu-Signature`(HMAC-SHA256)可验真。\n⚠️ 内网 CI(如自建 Jenkins)我们回调不到——**轮询才是唯一保证路径**,回调只作增强。\n","format":"uri","type":"string"}},"required":["project_id","build_id","device_selector"],"type":"object"},"DeviceSelector":{"description":"按条件要一台设备,而不是写死 id。给了 `device_id` 则忽略其余条件(兼容特殊场景)。\n","properties":{"device_id":{"format":"uuid","type":"string"},"os_version":{"description":"语义化比较,如 \"\u003e=13\"、\"14\"","type":"string"},"platform":{"enum":["android","ios"],"type":"string"},"pool":{"description":"设备池标签","type":"string"}},"required":["platform"],"type":"object"},"Error":{"properties":{"code":{"description":"稳定错误码,**只增不改**,可以据此写判断逻辑。首批:\n`unauthorized` / `forbidden` / `not_found` / `invalid_request` /\n`device_unavailable` / `build_not_installed` / `payload_too_large` /\n`rate_limited` / `conflict`\n","type":"string"},"details":{"additionalProperties":true,"type":"object"},"message":{"description":"人类可读说明;文案可能变","type":"string","别用来做判断":null}},"required":["code","message"],"type":"object"},"Failure":{"properties":{"action":{"description":"tap / input / verify / launch / back / home ...","type":"string"},"intent":{"deprecated":true,"description":"已废弃,与 action 同值。新接入请用 action;本字段在 v1 生命周期内不会移除。","type":"string"},"plan_name":{"type":"string"},"reason":{"description":"一句话根因(AI 失败分析产出)","type":"string"},"screenshot_url":{"description":"失败那一屏,形如 `/api/v1/test-runs/{id}/screenshots/{step}/after` ——\n**用同一把 API Key 直接 GET 即可**。agent 看得到画面才谈得上自主修复。\n","type":"string"},"step_order":{"type":"integer"},"target":{"description":"这一步要操作的元素(自然语言描述)","type":"string"}},"required":["plan_name","step_order"],"type":"object"},"InstallResult":{"properties":{"build_id":{"format":"uuid","type":"string"},"device_id":{"format":"uuid","type":"string"},"message":{"type":"string"},"status":{"enum":["installed","failed"],"type":"string"}},"required":["device_id","build_id","status"],"type":"object"},"Project":{"properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"platform":{"enum":["android","ios","miniprogram"],"type":"string"}},"required":["id","name"],"type":"object"},"TestPlan":{"properties":{"id":{"format":"uuid","type":"string"},"kind":{"enum":["case","suite"],"type":"string"},"name":{"type":"string"},"reset_policy":{"description":"执行前把被测应用带到什么起点。**这会影响设备上的共享状态,不只影响这一条用例。**\n\n- `keep`     什么都不做(最快;依赖设备上已有的登录态等状态)\n- `clear`    `pm clear` 清应用数据 —— **会毁掉登录态**\n- `reinstall` 卸载重装 —— 同上,更彻底\n- `baseline` 恢复到该设备已捕获的「就绪干净」基线(含登录态)\n\n⚠️ **`clear` / `reinstall` 的影响范围是设备,不是这条用例。**\n一批用例里混着 `clear` 和 `keep` 时,`clear` 那条会把登录态洗掉,\n排在它后面的 `keep` 用例随后全部失败,而失败原因看起来与它们自己有关。\n排批次前请检查这个字段。\n","enum":["keep","clear","reinstall","baseline"],"type":"string"},"step_count":{"type":"integer"},"tags":{"items":{"type":"string"},"type":"array"}},"required":["id","name","kind"],"type":"object"},"TestRun":{"properties":{"device":{"description":"实际分配到的设备名","type":"string"},"duration_ms":{"format":"int64","type":"integer"},"error_message":{"description":"status=error 时说明环境哪里不对","type":"string"},"failed":{"type":"integer"},"finished_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"passed":{"type":"integer"},"report_url":{"format":"uri","type":"string"},"started_at":{"format":"date-time","type":"string"},"status":{"description":"`error` = **环境错误**(设备离线、App 停在登录页、截图拿不到),不是用例失败。\nCI 应当把它与 `failed` 区分对待——把环境抖动算成代码问题会让人不信任流水线。\n","enum":["queued","running","passed","failed","error","cancelled","timeout","skipped"],"type":"string"},"total":{"type":"integer"}},"required":["id","status"],"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"description":"`Authorization: Bearer shiyu_\u003ckey\u003e`","scheme":"bearer","type":"http"}}},"info":{"description":"面向 **CI 平台**(GitHub Actions / Jenkins / GitLab CI)与 **coding agent** 的公共接口。\n\n## 认证\n所有端点要求 `Authorization: Bearer shiyu_\u003ckey\u003e`。在控制台「设置 → API Keys」创建。\nKey 绑定组织,可限定项目范围与权限(scopes),可设过期时间。\n\n## 兼容性承诺\n- 字段**只增不删**;破坏性变更走 `/api/v2`,v1 至少维护 12 个月。\n- 错误 `code` 只增不改,可以据此写判断逻辑。\n\n## 长任务\n执行是异步的:`POST /test-runs` 立刻返回,用 `GET /test-runs/{id}` 轮询到终态。\n不提供阻塞式长连接——CI 与企业网络中间件常会砍掉长连接。\n","title":"试玉 Shiyu Public API","version":"1.0.0"},"openapi":"3.0.3","paths":{"/builds":{"post":{"description":"上传后平台会解析 manifest 取出 `package_name` / `version` / `build_number`。\nCI 的典型用法:每次构建产出新包就传一次,再用返回的 `id` 起执行。\n","operationId":"UploadBuild","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"multipart/form-data":{"schema":{"properties":{"file":{"format":"binary","type":"string"},"project_id":{"format":"uuid","type":"string"}},"required":["project_id","file"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Build"}}},"description":"已创建"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}},"summary":"上传应用包(APK / IPA)","tags":["builds"]}},"/builds/{id}":{"get":{"operationId":"GetBuild","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Build"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"tags":["builds"]}},"/installs":{"post":{"description":"装机与执行是**解耦**的:执行时不下载、不安装,只校验\"这台设备上已经有这个包\"——\n这样每次执行都快且可复现。代价是包必须先装。\n\nCI 的两种用法:\n- **每次新包**:`POST /test-runs` 时带 `auto_install: true`,平台自动先装再跑;\n- **同一个包跑多条用例**:先调本端点装一次,后续执行都不必再装。\n\n同步返回:装完(或失败)才返回,不是异步任务。装机耗时取决于包大小与设备,可能数分钟。\n","operationId":"InstallBuild","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"build_id":{"format":"uuid","type":"string"},"device_selector":{"$ref":"#/components/schemas/DeviceSelector"}},"required":["build_id","device_selector"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResult"}}},"description":"已安装"},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}},"summary":"把包装到设备上","tags":["builds"]}},"/projects":{"get":{"operationId":"ListProjects","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"items":{"items":{"$ref":"#/components/schemas/Project"},"type":"array"},"next_cursor":{"description":"还有下一页时给出;为空表示到底了","type":"string"}},"required":["items"],"type":"object"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"列出该 Key 可见的项目","tags":["projects"]}},"/test-plans":{"get":{"operationId":"ListTestPlans","parameters":[{"in":"query","name":"project_id","required":true,"schema":{"format":"uuid","type":"string"}},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TestPlan"},"type":"array"},"next_cursor":{"type":"string"}},"required":["items"],"type":"object"}}},"description":"OK"}},"summary":"列出用例与套件","tags":["test-plans"]}},"/test-runs":{"post":{"description":"**本 API 的核心端点。** 一个动作完成「选包 + 选用例 + 选设备 + (按需装机) + 开跑」。\n\n- 设备用 `device_selector` **按条件**要,不要写死某台设备的 id——设备会离线、会被占用。\n- `auto_install=true` 时,若所选设备尚未安装该包,平台会先装再跑(装机计入准备阶段,不计入用例分数)。\n  CI 每次都是新包,基本都要开这个。\n- 带 `queue_timeout_seconds` 时,无可用设备会**排队**而非立即失败(上限 280 秒,见该字段)。\n","operationId":"CreateTestRun","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTestRunRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRun"}}},"description":"已受理,异步执行中"},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}},"summary":"起一次执行","tags":["test-runs"]}},"/test-runs/{id}":{"get":{"operationId":"GetTestRun","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRun"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"查执行状态与进度","tags":["test-runs"]}},"/test-runs/{id}/allure":{"get":{"operationId":"GetTestRunAllure","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/zip":{"schema":{"format":"binary","type":"string"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Allure 结果包(ZIP)","tags":["test-runs"]}},"/test-runs/{id}/cancel":{"post":{"operationId":"CancelTestRun","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRun"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}},"tags":["test-runs"]}},"/test-runs/{id}/failures":{"get":{"description":"每条失败给出:用例名、失败在第几步、做了什么、根因、**失败那一屏的截图 URL**。\nagent 拿到截图才谈得上自主判断并改代码——只给 pass/fail 是不够的。\n\n响应还带 `case_health` —— 这条用例的历史战绩,用来回答「这次失败更像**用例本身\n有问题**,还是更像**回归**」。⚠️ 它是**提示,不是裁决**:单看一次执行,用例缺陷与\n产品缺陷长得一模一样(元素不在这屏,既可能是用例引用了易变内容,也可能是按钮真被\n删了),只有历史能分开两者。\n","operationId":"ListTestRunFailures","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"case_health":{"$ref":"#/components/schemas/CaseHealth"},"items":{"items":{"$ref":"#/components/schemas/Failure"},"type":"array"}},"required":["items"],"type":"object"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"失败清单(coding agent 主要消费这个)","tags":["test-runs"]}},"/test-runs/{id}/junit":{"get":{"operationId":"GetTestRunJUnit","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"application/xml":{"schema":{"type":"string"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"JUnit XML(给 CI 的报告插件消费)","tags":["test-runs"]}},"/test-runs/{id}/screenshots/{step}/{kind}":{"get":{"description":"`GET /test-runs/{id}/failures` 返回的 `screenshot_url` 就指向这里。\n\n**coding agent 靠它看失败现场** —— 只给 pass/fail 或一句文字根因不够,\n很多失败(点错位置、页面没渲染完、弹窗挡住)只有看画面才判得准。\n","operationId":"GetStepScreenshot","parameters":[{"$ref":"#/components/parameters/PathID"},{"description":"步骤序号(1-based)","in":"path","name":"step","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"before=动作前;after=动作后(失败现场一般看这张)","in":"path","name":"kind","required":true,"schema":{"enum":["before","after"],"type":"string"}}],"responses":{"200":{"content":{"image/jpeg":{"schema":{"format":"binary","type":"string"}}},"description":"JPEG 截图"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"取某一步的截图","tags":["test-runs"]}},"/test-runs/{id}/summary":{"get":{"description":"通过率 + 失败用例 + **每条失败的一句话根因**(平台的 AI 失败分析产出)+ 截图直链。\n传统测试平台给 CI 的是一堆 XML;这里给的是「为什么挂了」。\n","operationId":"GetTestRunSummary","parameters":[{"$ref":"#/components/parameters/PathID"}],"responses":{"200":{"content":{"text/markdown":{"schema":{"type":"string"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"可直接贴进 PR 的 Markdown 摘要","tags":["test-runs"]}}},"security":[{"ApiKeyAuth":[]}],"servers":[{"url":"https://shiyu.quartzchord.com/api/v1"}],"tags":[{"name":"projects"},{"name":"builds"},{"name":"test-plans"},{"name":"test-runs"}]}