--- name: hurl-test description: Hurl 接口测试生成器。用户描述要测试的接口或业务流程,自动探索代码、确认需求、生成完整的 .hurl 测试文件(含 DTO 驱动的字段完整性断言)。触发词:测试、hurl、写测试、接口测试。 --- # Hurl 接口测试生成器 **用户描述要测试什么,你来读代码、问确认、生成 .hurl 文件。** 适用于任何后端项目(Go / Python / Node / Java 等),不预设框架和目录结构。 --- ## 触发条件 以下情况必须使用本 Skill: - 用户说"测试 XX 接口"、"写 hurl 测试"、"给 XX 加测试" - 用户说"测试 XX 流程"、"测试 XX 的业务逻辑" - 用户说"验证 XX 接口的字段"、"测试接口契约" - 用户提到 hurl、.hurl、接口测试、集成测试、冒烟测试 --- ## 四阶段工作流(必须按顺序执行) ``` Phase 1: 探索 → Phase 2: 确认 → Phase 3: 生成 → Phase 4: 验证 读代码搞清楚 展示给用户确认 输出 .hurl 文件 语法检查 + 试跑 ``` --- ### Phase 1: 探索(Explore) **目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。** #### 1.1 项目画像(首次使用时必须执行,后续复用) 首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜): | 问题 | 怎么找 | |------|--------| | **语言/框架** | 看 go.mod / package.json / requirements.txt / pom.xml | | **路由注册在哪** | 搜索 `router`、`app.Get`、`@GetMapping`、`@app.route` 等关键词 | | **请求/响应 schema 定义在哪** | 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器 | | **统一响应格式是什么** | 找 response helper 文件(如 `response.go`、`response.py`),记录 JSON 结构 | | **认证方式是什么** | 找 auth middleware,确定是 Bearer Token / Cookie / API Key / Basic Auth | | **登录接口是什么** | 找登录 handler,记录路径、请求体、响应中 token 的位置 | | **分页格式是什么** | 找列表接口的响应结构,记录 items/total/page 等字段名 | | **已有 hurl 测试吗** | 搜索 `*.hurl` 文件,复用已有的约定 | 将画像结果**写入 `tests/hurl/.project-profile.md` 文件持久化保存**。 #### 画像持久化(关键机制) **首次使用时**:完成 1.1 探索后,将画像写入 `tests/hurl/.project-profile.md`,格式如下: ```markdown # 项目画像(Hurl 测试自动生成用) ## 技术栈 - 语言: Go 1.25 - 框架: Fiber v2 - ORM: GORM ## 路由定义位置 - 路由注册入口: internal/routes/routes.go - 按模块拆分: internal/routes/{module}.go - 路由注册函数: Register(router, doc, basePath, method, path, handler, spec) ## Schema 定义位置 - DTO 目录: internal/model/dto/ - 命名规则: {module}_dto.go - 字段标签: json / validate / description ## 统一响应格式 {code: int, msg: string, data: any, timestamp: string(RFC3339)} - 成功: code=0, msg="success" - 错误: code!=0 ## 分页格式 {items: [], total: int, page: int, size: int} - 包裹在 data 字段内: $.data.items / $.data.total ## 认证方式 - 后台: POST /api/auth/admin-login → $.data.access_token → Authorization: Bearer {token} - C端: JWT → Authorization: Bearer {token} ## 默认测试账号 - 用户名: admin - 密码: Admin@123456 ## 服务端口 - 默认: 3000 ``` **后续使用时**:检查 `tests/hurl/.project-profile.md` 是否存在: - **存在** → 直接读取,跳过 1.1 的探索步骤,节省时间 - **不存在** → 执行 1.1 完整探索,然后生成此文件 - **用户说"刷新画像"** → 删除旧文件,重新执行 1.1 #### 1.2 找接口定义 根据用户要测的模块,定位路由注册代码,提取: - **HTTP 方法**(GET / POST / PUT / DELETE / PATCH) - **路由路径**(含路径参数格式,如 `/users/:id` 或 `/users/{id}`) - **接口说明**(注释、Summary、装饰器描述) - **是否需要认证** - **请求 schema 类型名**(Input / Request DTO) - **响应 schema 类型名**(Output / Response DTO) #### 1.3 读 schema 定义(DTO / struct / class / type) 定位请求和响应的 schema 定义文件,提取每个字段的: - **字段名**:JSON 序列化后的名称(json tag / @JsonProperty / serializer field) - **语言类型**:string / int / bool / 数组 / 嵌套对象 / 可空等 - **是否必填**:validate tag / required 装饰器 / 非空标注 - **是否可空**:指针类型 / Optional / nullable - **是否参与序列化**:`json:"-"` / @JsonIgnore / exclude - **是否 omitempty**:`json:",omitempty"` / 条件序列化 - **字段描述**:description tag / docstring / 注释 #### 1.4 识别业务依赖 读 service / business logic 层,识别: - 创建操作需要哪些前置数据(如创建订单需要先有商品和用户) - 是否有唯一性约束(如用户名不能重复) - 是否依赖外部服务(支付网关、短信、OAuth 等) - 业务流转逻辑(状态机、级联操作) --- ### Phase 2: 确认(Clarify) **目标:向用户展示发现的内容,确认模糊点。不要闷头生成。** #### 2.1 必须展示的内容 ``` 我梳理了相关代码,发现以下信息: 📋 涉及接口: - [方法] [路径] - [说明](认证: 是/否) - ... 📦 响应字段(基于 {SchemaName}): - [字段名]: [类型] - [说明] - ...(共 N 个字段,将全部生成断言) 🔗 依赖关系: - [创建 X 需要先创建 Y] - ... ⚠️ 特殊情况: - [涉及外部服务 / 文件上传 / 特殊认证等] ``` #### 2.2 按需确认(只问有歧义的) | 场景 | 要问的 | |------|--------| | 流程范围不明确 | "要测到哪一步?" | | 多种用户角色 | "用哪种身份测?" | | 是否测异常 | "需要包含异常 case 吗?(参数校验失败、权限不足等)" | | 是否测数据隔离 | "需要验证不同用户间数据不可见吗?" | | 涉及第三方 | "XX 部分怎么处理?绕过 / 模拟回调 / 跳过?" | | 前置数据来源 | "XX 依赖数据是通过 API 创建还是假设已存在?" | **如果用户说"越完整越好"或"都要"→ 默认全部包含,不再追问。** --- ### Phase 3: 生成(Generate) **目标:生成完整的 .hurl 文件,字段断言基于 schema 代码,不能编造。** #### 3.1 文件头注释 ```hurl # ============================================================ # 测试:{测试名称} # 生成时间:{日期} # 涉及模块:{module1, module2, ...} # 涉及接口:{N} 个 # 断言数量:{N} 条 # 前置条件:{服务运行 + 必要的前置条件} # ============================================================ # 流程: # 1. {步骤描述} # 2. {步骤描述} # ... # ============================================================ ``` #### 3.2 请求生成规则 **认证**: - 根据 Phase 1 画像中的登录接口和 token 位置生成 - token 必须通过 `[Captures]` 捕获,后续请求引用 - 如果是 Cookie 认证,用 `[Cookies]` 或 cookie capture **CRUD 标准模式**: | 操作 | 生成要求 | |------|---------| | **创建(POST)** | capture 返回的 ID;唯一字段用 `{{newUuid}}` 防冲突 | | **查询详情(GET)** | **逐字段断言**(类型 + 值,见 3.3) | | **查询列表(GET)** | 分页结构断言 + items[0] 逐字段断言 | | **修改(PUT/PATCH)** | 修改后**紧跟一个 GET 验证修改生效** | | **删除(DELETE)** | 删除后**紧跟一个 GET 验证已删除** | **业务流程模式**: - 按用户描述的流程顺序编排请求 - 上一步的输出(ID、状态等)通过 `[Captures]` 传给下一步 - 关键步骤加中间状态验证(如创建订单后验证状态为"待支付") #### 3.3 schema 到断言的映射 读到 schema 字段后,按以下规则生成 jsonpath 断言: **通用类型映射(所有语言)**: | Schema 类型特征 | Hurl 断言 | |----------------|-----------| | 字符串(string / str / String) | `isString` | | 整数(int / integer / long / Int) | `isInteger` | | 浮点(float / double / decimal / Float) | `isNumber` | | 布尔(bool / boolean / Boolean) | `isBoolean` | | 数组 / 列表([] / List / Array) | `isList` | | 嵌套对象(struct / class / dict / object) | `isObject`,并递归检查子字段 | | 可空类型(指针 / Optional / nullable) | `exists`(不强制类型,因为可能是 null) | | 不参与序列化(json:"-" / @JsonIgnore / exclude=True) | **跳过,不生成断言** | | 条件序列化(omitempty / if not None) | `exists` 或不生成(取决于场景) | **Go 特定映射**: | Go 类型 | Hurl 断言 | |---------|-----------| | `string` | `isString` | | `int`, `int8/16/32/64`, `uint`, `uint8/16/32/64` | `isInteger` | | `float32`, `float64` | `isNumber` | | `bool` | `isBoolean` | | `[]T` | `isList` | | `*string`, `*int`, `*uint` 等指针 | `exists` | | `time.Time` | `isString`(通常序列化为字符串) | | `map[string]any` | `isObject` | **Python 特定映射(Pydantic / Django / FastAPI)**: | Python 类型 | Hurl 断言 | |------------|-----------| | `str` | `isString` | | `int` | `isInteger` | | `float`, `Decimal` | `isNumber` | | `bool` | `isBoolean` | | `list[T]`, `List[T]` | `isList` | | `Optional[T]`, `T | None` | `exists` | | `dict`, `Dict` | `isObject` | | `datetime`, `date` | `isString` | **TypeScript/JavaScript 特定映射**: | TS/JS 类型 | Hurl 断言 | |-----------|-----------| | `string` | `isString` | | `number`(整数上下文) | `isInteger` | | `number`(通用) | `isNumber` | | `boolean` | `isBoolean` | | `T[]`, `Array` | `isList` | | `T \| null`, `T \| undefined` | `exists` | | `object`, `Record<>` | `isObject` | | `Date` | `isString` | **Java 特定映射**: | Java 类型 | Hurl 断言 | |----------|-----------| | `String` | `isString` | | `Integer`, `Long`, `int`, `long` | `isInteger` | | `Double`, `Float`, `BigDecimal` | `isNumber` | | `Boolean`, `boolean` | `isBoolean` | | `List` | `isList` | | `@Nullable`, `Optional` | `exists` | | `Map` | `isObject` | | `LocalDateTime`, `Instant` | `isString` | #### 3.4 统一响应格式断言 根据 Phase 1 画像中发现的统一响应格式,为**每个成功响应**添加格式断言。 示例:如果项目的统一格式是 `{code, msg, data, timestamp}`: ```hurl [Asserts] jsonpath "$.code" == 0 jsonpath "$.msg" == "success" jsonpath "$.timestamp" isIsoDate ``` 示例:如果项目的格式是 `{status, message, result}`: ```hurl [Asserts] jsonpath "$.status" == "ok" jsonpath "$.message" isString ``` 示例:如果项目无统一包装,直接返回数据: ```hurl [Asserts] # 直接断言业务字段 jsonpath "$.id" isInteger jsonpath "$.name" isString ``` **不要假设响应格式,必须从代码中确认。** #### 3.5 分页断言 根据 Phase 1 画像中发现的分页结构生成。 示例:如果是 `{items, total, page, size}` 格式: ```hurl jsonpath "$.data.items" isList jsonpath "$.data.total" isInteger jsonpath "$.data.total" >= 1 jsonpath "$.data.page" isInteger jsonpath "$.data.size" isInteger # items 内元素逐字段断言 jsonpath "$.data.items[0].{field}" {type_assert} ``` 示例:如果是 `{results, count, next, previous}` 格式(Django 风格): ```hurl jsonpath "$.results" isList jsonpath "$.count" isInteger jsonpath "$.count" >= 1 # results 内元素逐字段断言 jsonpath "$.results[0].{field}" {type_assert} ``` **根据实际代码调整字段名,不硬编码。** #### 3.6 异常 Case 模板 **参数校验失败**: ```hurl # ── 异常:参数校验失败 ── POST {{base_url}}/{path} Authorization: Bearer {{token}} Content-Type: application/json { "required_field": "" } HTTP {expected_error_status} [Asserts] # 断言错误响应格式(根据项目约定调整) ``` > HTTP 状态码根据项目实际返回确定:有的项目错误也返回 200 + 业务错误码,有的返回 400/422。 **未认证访问**: ```hurl # ── 异常:未认证访问 ── GET {{base_url}}/{protected_path} HTTP {expected_unauth_status} ``` **越权访问**(如果用户要求): ```hurl # ── 异常:用户 B 不能访问用户 A 的资源 ── GET {{base_url}}/{path}/{{user_a_resource_id}} Authorization: Bearer {{user_b_token}} HTTP {expected_forbidden_status} ``` #### 3.7 特殊场景处理 | 场景 | 处理策略 | |------|---------| | **短信/邮件验证码** | 建议服务端加 test_mode 开关,固定验证码写入 env 文件;注释提醒用户 | | **第三方支付** | 优先用项目内部支付方式(如钱包支付);如需测回调,直接 POST 回调接口模拟 | | **OAuth 登录(微信/Google/GitHub)** | 建议服务端加 test_mode 支持直接传 openid/email;注释提醒用户 | | **文件上传** | 用 Hurl 的 `[Multipart]` 语法 + testdata 目录下的样本文件 | | **外部 API 依赖** | 只测参数校验和错误响应格式,不断言业务结果;注释说明依赖 | | **WebSocket** | Hurl 不支持,注释说明跳过 | | **异步任务结果** | 用 Hurl 的 `retry` + `retry-interval` 轮询直到状态变更 | 异步轮询示例: ```hurl # 等待异步任务完成(最多重试 10 次,间隔 500ms) GET {{base_url}}/{path}/{{task_id}} Authorization: Bearer {{token}} [Options] retry: 10 retry-interval: 500ms HTTP 200 [Asserts] jsonpath "$.data.status" == "completed" ``` #### 3.8 文件输出 **目录结构**(首次使用时创建,如不存在): ``` tests/hurl/ ├── env/ │ └── dev.env # 环境变量 ├── testdata/ # 测试用的样本文件 ├── flows/ # 业务流程测试 ├── modules/ # 按模块的接口测试 │ └── {module}/ │ └── 01-crud.hurl ├── negative/ # 异常/边界测试 ├── contract/ # 接口契约验证 └── Makefile # 快捷命令 ``` 文件放置规则: | 用户描述 | 输出路径 | |---------|---------| | 测试 XX 流程 / 业务流程 | `tests/hurl/flows/{flow-name}.hurl` | | 测试 XX 模块的 CRUD / 接口 | `tests/hurl/modules/{module}/01-crud.hurl` | | 测试异常/边界/权限 | `tests/hurl/negative/{name}.hurl` | | 测试接口契约/字段对齐 | `tests/hurl/contract/{name}.hurl` | **如果项目已有 hurl 测试目录结构,沿用已有约定,不要另起炉灶。** #### 3.9 env 和 Makefile **env/dev.env**(首次创建时生成,内容基于 Phase 1 画像): ```properties # 服务地址 base_url=http://localhost:{port} # 认证信息(根据项目实际填写) admin_username={默认用户名} admin_password={默认密码} # 测试模式变量(如果有特殊场景) # test_sms_code=888888 # test_openid=test_openid_001 ``` **Makefile**(首次创建时生成): ```makefile SHELL := /bin/bash ENV ?= dev HURL_OPTS := --variables-file env/$(ENV).env --test .PHONY: test test-flows test-modules test-negative report test: ## 运行所有测试 hurl $(HURL_OPTS) . test-flows: ## 运行业务流程测试 hurl $(HURL_OPTS) flows/ test-modules: ## 运行模块接口测试 hurl $(HURL_OPTS) modules/ test-negative: ## 运行异常测试 hurl $(HURL_OPTS) negative/ report: ## 生成 HTML 报告 hurl $(HURL_OPTS) --report-html build/report/ . ``` --- ### Phase 4: 验证(Verify) **目标:确保生成的 .hurl 文件语法正确、可运行。** #### 4.1 语法自检 - [ ] 每个请求之间有空行分隔 - [ ] `[Captures]` 和 `[Asserts]` 拼写正确(大小写敏感) - [ ] 所有 `{{变量}}` 引用都有来源(env 文件定义 或 上游 `[Captures]`) - [ ] JSON body 无尾逗号、格式正确 - [ ] `HTTP {status}` 在请求之后、`[Captures]` / `[Asserts]` 之前 - [ ] 请求和断言之间没有多余空行(`HTTP` 行必须紧跟请求) - [ ] 文件上传路径相对于 hurl 文件位置正确 #### 4.2 运行测试 ```bash hurl --variables-file tests/hurl/env/dev.env --test tests/hurl/{生成的文件} ``` - 服务在跑 → 执行,如有失败分析修正 - 服务没跑 → 跳过,告知用户手动验证命令 --- ## 红线规则 | 规则 | 说明 | |------|------| | **不跳过 Phase 1** | 必须读代码确认接口路径和字段,不能凭记忆或猜测 | | **不跳过 Phase 2** | 必须向用户展示发现的接口和字段,确认后再生成 | | **不编造字段** | 所有断言的字段名必须来自实际 schema 代码 | | **不编造路径** | 所有接口路径必须来自实际路由代码 | | **不遗漏字段** | schema 中每个参与序列化的字段都必须有对应断言 | | **不硬编码 ID** | 所有依赖的 ID 通过 `[Captures]` 从上游请求获取 | | **不假设响应格式** | 统一响应结构必须从代码中确认,不同项目格式不同 | | **唯一值防冲突** | 创建类请求的唯一字段使用 `{{newUuid}}` 或 `{{newDate}}` | | **自给自足** | 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备 | --- ## AI 助手检查清单 生成 .hurl 文件后自检: 1. ✅ 文件头注释包含流程描述和前置条件 2. ✅ 认证步骤正确 capture 了 token / cookie 3. ✅ 所有依赖数据通过 API 链式创建(自给自足) 4. ✅ 每个成功响应断言了项目的统一响应格式 5. ✅ 查询详情接口**逐字段断言**(类型 + 值,基于 schema) 6. ✅ 分页接口断言了分页结构 + 第一条记录的字段 7. ✅ 修改操作后紧跟 GET 验证修改生效 8. ✅ 删除操作后紧跟 GET 验证已删除 9. ✅ 唯一字段使用了 `{{newUuid}}` 10. ✅ `{{变量}}` 引用无悬空(都有 env 或 capture 来源) 11. ✅ 文件放在了正确的目录位置 12. ✅ 特殊场景有明确的处理策略和注释提醒 --- ## 附录:Hurl 语法速查 **生成 .hurl 文件时必须参照本速查,不可凭记忆编造语法。** ### 文件结构 一个 .hurl 文件由多个 entry 组成,每个 entry = 请求 + 可选响应: ``` 请求1 响应1(可选) 请求2 响应2(可选) ``` entry 之间用空行分隔。 ### 请求格式 ```hurl METHOD URL Header1: value1 Header2: value2 [Options] key: value [Query] param1: value1 [Form] field1: value1 [Multipart] file1: file,path/to/file; [BasicAuth] username: password [Cookies] name: value BODY(JSON / XML / multiline string / file) ``` **规则**: - Method + URL 是第一行,必须 - Headers 紧跟 URL 之后(无 section 标记) - Sections(`[Query]`、`[Form]`、`[Options]` 等)顺序任意 - Body 必须在最后 - JSON body 直接写 `{ }` 即可,自动设置 Content-Type: application/json ### 响应格式 ```hurl HTTP {status_code} Header1: expected_value1 [Captures] var_name: jsonpath "$.path" [Asserts] jsonpath "$.field" == "value" ``` **规则**: - `HTTP {status}` 紧跟请求之后(中间不能有空行) - `HTTP *` 表示不检查状态码 - Headers 检查紧跟 HTTP 行之后 - `[Captures]` 和 `[Asserts]` 顺序任意 ### 变量和模板 ```hurl # 引用变量(从 env 文件、命令行或上游 capture 获取) GET {{base_url}}/api/users/{{user_id}} # 内置函数 POST {{base_url}}/api/users { "email": "{{newUuid}}@test.com", "created_at": "{{newDate}}" } ``` 可用函数: - `{{newUuid}}` — 生成 UUID v4 - `{{newDate}}` — 生成 RFC 3339 UTC 时间戳 ### Capture 语法 ```hurl [Captures] # JSONPath token: jsonpath "$.data.access_token" user_id: jsonpath "$.data.id" first_item: jsonpath "$.items[0].name" # Header location: header "Location" # Cookie session: cookie "SESSIONID" # Body(整个响应体作为字符串) full_body: body # Status code code: status # 正则表达式 csrf: regex "name=\"csrf\" value=\"([^\"]+)\"" # 响应时间(毫秒) response_time: duration ``` ### Assert 语法 ```hurl [Asserts] # ── 状态码 ── status == 200 status >= 200 status < 300 # ── JSONPath 断言 ── jsonpath "$.name" == "Alice" # 等于 jsonpath "$.name" != "Bob" # 不等于 jsonpath "$.age" > 18 # 大于 jsonpath "$.age" >= 18 # 大于等于 jsonpath "$.count" < 100 # 小于 jsonpath "$.items" count == 5 # 集合长度 jsonpath "$.name" startsWith "Al" # 前缀 jsonpath "$.name" endsWith "ce" # 后缀 jsonpath "$.name" contains "lic" # 包含 jsonpath "$.date" matches /\\d{4}-\\d{2}-\\d{2}/ # 正则 # ── 类型断言 ── jsonpath "$.name" isString jsonpath "$.age" isInteger jsonpath "$.score" isFloat jsonpath "$.count" isNumber # 整数或浮点 jsonpath "$.active" isBoolean jsonpath "$.items" isList jsonpath "$.meta" isObject jsonpath "$.id" isUuid jsonpath "$.created_at" isIsoDate # RFC 3339 格式 jsonpath "$.field" isEmpty # 空集合 # ── 存在性 ── jsonpath "$.field" exists jsonpath "$.field" not exists # ── 否定 ── jsonpath "$.name" not contains "Bob" jsonpath "$.status" not == "deleted" # ── Header 断言 ── header "Content-Type" contains "application/json" header "X-Request-Id" exists # ── 性能 ── duration < 1000 # 响应时间(毫秒) # ── Body 断言 ── body contains "Hello" bytes count == 1024 ``` ### Options(逐请求配置) ```hurl GET {{base_url}}/api/task/{{task_id}} [Options] retry: 10 # 最大重试次数(-1 = 无限) retry-interval: 500ms # 重试间隔 delay: 2s # 请求前等待 location: true # 跟随重定向 insecure: true # 允许不安全 SSL verbose: true # 输出详细日志 very-verbose: true # 输出更详细日志 skip: true # 跳过此请求 variable: key=value # 定义变量 HTTP 200 ``` ### Multipart 文件上传 ```hurl POST {{base_url}}/api/upload [Multipart] file: file,testdata/sample.xlsx; field1: value1 # 指定 Content-Type file2: file,testdata/data.bin; application/octet-stream ``` ### 运行命令 ```bash # 运行单个文件 hurl --test file.hurl # 带变量文件 hurl --variables-file env/dev.env --test file.hurl # 运行目录下所有 .hurl hurl --test tests/hurl/ # 生成 HTML 报告 hurl --test --report-html build/report/ tests/hurl/ # 生成 JUnit 报告(CI 用) hurl --test --report-junit build/report.xml tests/hurl/ # 并行执行(--test 默认并行,同文件内串行) hurl --test --jobs 4 tests/hurl/ # 指定单个变量 hurl --variable base_url=http://localhost:3000 --test file.hurl # 失败后继续执行 hurl --test --continue-on-error tests/hurl/ ``` ### 常见错误 | 错误 | 原因 | 修正 | |------|------|------| | `HTTP 200` 和请求之间有空行 | 空行会被当作 entry 分隔符 | 删除空行,HTTP 行紧跟请求 | | JSON body 有尾逗号 | Hurl 严格解析 JSON | 删除最后一个逗号 | | `jsonpath` 写成 `json_path` 或 `JsonPath` | 关键字大小写敏感 | 必须小写 `jsonpath` | | `[Captures]` 写成 `[Capture]` | 必须是复数 | `[Captures]`、`[Asserts]`、`[Options]` | | 变量 `{{ var }}` 有空格 | 允许,但建议统一 | `{{var}}` 或 `{{ var }}` 都可以 | | `isIsoDate` 用在非 RFC 3339 格式 | 只认 `YYYY-MM-DDTHH:mm:ss` 格式 | 如果是其他格式用 `matches` | | `file,path;` 路径含 `..` | Hurl 禁止相对父目录 | 用 `--file-root` 或调整路径 |