Midscene AI 规划与元素定位缓存源码分析¶
分析版本:Midscene 1.10.5,基线提交
c4c33f8b分析日期:2026-07-16 范围:CoreTaskCache、aiAct规划缓存、元素定位缓存、Web XPath 适配、缓存策略与失效机制
1. 结论摘要¶
Midscene 的缓存不是通用 HTTP 缓存或模型响应缓存,而是面向自动化执行的两类结构化缓存:
- 规划缓存(plan cache):以
aiActprompt 和 action context 为键,保存模型生成的 YAML workflow。命中后跳过本次规划模型调用,直接执行缓存的步骤。 - 定位缓存(locate cache):以元素描述 prompt 为键,保存由页面接口生成的 element cache feature。当前实际实现是 Web XPath;命中后先在当前 DOM 中验证 XPath,再跳过视觉定位模型调用。
查询类能力 aiQuery、aiBoolean、aiAssert、aiWaitFor 等不缓存返回值,因为结果应反映当前页面状态。
对 Android 最重要的结论是:
- Android 可以使用 规划缓存,前提是当前 planning adapter 允许缓存。
- Android 当前不能使用 元素定位缓存,因为
AndroidDevice没有实现cacheFeatureForPoint()和rectMatchesCacheFeature()。 - 命中规划缓存不代表完全不调用 AI。缓存 YAML 中的
aiTap、aiInput等仍可能触发 Android 视觉定位。
缓存文件是明文 YAML,没有 TTL、应用版本、页面 URL、设备类型或模型版本维度,也没有跨进程文件锁。它适合稳定流程加速,但不能当成长期正确性的保证。
2. 总体架构¶
flowchart TD
Config["Agent cache 配置"]
Normalize["validate + processCacheConfig"]
TaskCache["TaskCache"]
File["midscene_run/cache/id.cache.yaml"]
Act["agent.aiAct"]
PlanMatch["matchPlanCache"]
CachedYaml["执行缓存 YAML"]
Planner["规划模型"]
PlanWrite["写入 plan record"]
Locate["TaskBuilder Locate"]
LocateMatch["matchLocateCache"]
Validate["rectMatchesCacheFeature"]
LocateAI["视觉定位模型"]
Feature["cacheFeatureForPoint"]
LocateWrite["写入 locate record"]
Config --> Normalize --> TaskCache
File --> TaskCache
TaskCache --> File
Act --> PlanMatch
TaskCache --> PlanMatch
PlanMatch -->|可用| CachedYaml
PlanMatch -->|未命中或不可用| Planner --> PlanWrite --> TaskCache
CachedYaml --> Locate
Planner --> Locate
Locate --> LocateMatch
TaskCache --> LocateMatch
LocateMatch --> Validate
Validate -->|有效| Locate
Validate -->|失效| LocateAI
LocateMatch -->|未命中| LocateAI
LocateAI --> Feature --> LocateWrite --> TaskCache
核心源码:
TaskCache:文件、匹配、更新、失效和清理。Agent.aiAct():规划缓存入口和失败回退。TaskBuilder.createLocateTask():定位缓存读取、验证和写入。matchElementFromCache():把 element cache feature 恢复为逻辑坐标矩形。PuppeteerBasePage:XPath feature 生成和验证。
3. 配置入口与策略¶
3.1 Agent 配置¶
Core 类型定义为:
直接创建 Agent 时必须显式提供 id:
validateAgentCacheInput() 会拒绝:
- 直接使用
cache: true。 - 对象配置没有 id。
- 空白
cacheDir。 - 非字符串或未知 strategy。
YAML Player 和 Playwright Fixture 会先根据文件名或测试 id 补齐 id,因此它们可以向用户暴露 cache: true。这一层自动补齐发生在 Agent 构造之前,Core Agent 自身仍要求明确 id。
3.2 新旧配置优先级¶
processCacheConfig() 的优先级是:
- 显式
cache: false:禁用。 - 显式 cache object:使用新配置。
cache: true:由 CLI/Fixture 上层补 id;直接 Agent 会在验证阶段拒绝。- 未配置
cache:只有同时设置旧cacheId和MIDSCENE_CACHE=true才启用兼容模式。 - 其他情况:不创建
TaskCache。
旧 cacheId + MIDSCENE_CACHE 仅用于向后兼容,新代码应使用 cache: { id }。
3.3 三种策略¶
| 策略 | 启动时读文件 | 自动使用缓存 | 自动写文件 | 手动 flushCache() |
|---|---|---|---|---|
read-write |
是 | 是 | 是 | 可以 |
read-only |
是 | 是 | 否,只更新内存 | 可以,并会写盘 |
write-only |
否 | 否 | 是 | 可以 |
stateDiagram-v2
[*] --> Disabled: cache=false / 未配置
[*] --> ReadWrite: strategy=read-write 或省略
[*] --> ReadOnly: strategy=read-only
[*] --> WriteOnly: strategy=write-only
ReadWrite --> ReadWrite: 命中、更新、立即写盘
ReadOnly --> ReadOnly: 命中、仅内存更新
ReadOnly --> Persisted: 手动 flushCache
WriteOnly --> WriteOnly: 不读取,每次调用 AI 并写盘
Disabled --> Disabled: 每次走正常 AI 流程
read-only 不是操作系统级只读。自动更新不会写文件,但公开的 agent.flushCache() 会直接调用 flushCacheToFile(),因此手动 flush 可以覆盖原文件。
write-only 启动时完全不读取已有文件,内存从空 caches 开始;第一次产生记录时会用本次内存内容重写目标文件。它适合重建缓存,不适合在已有文件上做追加式并发采集。
4. 缓存文件和数据结构¶
4.1 默认路径¶
默认路径由 getMidsceneRunSubDir('cache') 生成:
可以通过以下方式改变位置:
MIDSCENE_RUN_DIR:改变整个 Midscene 运行目录。cache.cacheDir:只改变当前 TaskCache 目录。TaskCache内部构造参数cacheFilePath:测试和底层调用可直接指定完整文件路径。
浏览器和 Worker 环境没有文件系统,cacheFilePath 为 undefined。此时可以在当前实例内维护内存记录,但无法跨刷新/进程持久化。
4.2 YAML 格式¶
顶层只有:
midsceneVersion。cacheId。caches数组。
没有创建时间、最后使用时间、应用版本、Git commit、URL、包名、设备型号、模型配置或过期时间。
写盘时 plan records 会排在 locate records 前面;同类型记录保持原相对顺序。内存数组不会因为写盘排序而改变,但下次加载会按文件顺序重新建立顺序。
4.3 规划记录¶
prompt 可以是普通字符串或带图片的 multimodal prompt。匹配使用 isDeepStrictEqual(),图片 URL、选项或上下文任一字段变化都会导致 miss。
4.4 定位记录¶
ElementCacheFeature 是平台开放的通用 object。当前 Web 实现保存 xpaths: string[]。老格式顶层 xpaths 会在命中时迁移到 cache.xpaths,下一次更新时清除旧字段。
5. 缓存匹配规则¶
5.1 精确 prompt 匹配¶
matchCache() 从文件加载时的原始记录范围内顺序查找:
它不比较:
- 当前页面截图或 DOM hash。
- URL、Activity、包名或页面 route。
- 模型名称和模型版本。
- Git commit、APK 版本或测试环境。
因此 cache id 必须由调用方承担“场景隔离”责任。同一句“点击提交按钮”如果在多个页面共用同一 cache id,规划缓存可能在错误页面直接执行。
5.2 action context 是规划键的一部分¶
aiAct 先调用 buildPromptWithContext()。存在 context 时,实际键变成:
context 的空白会 trim,但正文、换行和大小写仍精确参与匹配。定位 prompt 不自动附加 aiActContext,它使用具体 Locate task 的 prompt。
5.3 一条记录每次实例只能消费一次¶
matchedCacheIndices 记录 type + prompt + index。同一条记录在一个 TaskCache 实例中只能命中一次。
如果同一流程中同一个 prompt 出现三次,缓存文件需要按发生顺序保存三条记录;只有一条记录时,第一次命中,后两次 miss。这样可以让列表中多个“删除”按钮分别对应不同 XPath,而不是无限复用第一条记录。
5.4 本次新写记录不能立即命中¶
构造时保存 cacheOriginalLength。匹配循环只扫描 [0, cacheOriginalLength),本次运行追加的记录不会被本次运行再次读取。
这防止以下反馈环:
新记录只有下一个 Agent/TaskCache 实例重新从文件加载后才可命中。
6. 规划缓存链路¶
6.1 命中与执行¶
sequenceDiagram
participant Caller as agent.aiAct
participant Cache as TaskCache
participant Runner as TaskExecutor
participant Model as Planning Model
participant File as cache.yaml
Caller->>Cache: matchPlanCache(prompt + context)
alt 命中且 YAML 有非空 flow
Cache-->>Caller: cacheUsable=true
Caller->>Runner: loadYamlFlowAsPlanning()
Caller->>Runner: runYaml(cached workflow)
alt 缓存流程成功
Runner-->>Caller: 完成,不调用规划模型
else 缓存流程失败
Runner-->>Caller: throw
Caller->>Model: 从当前页面重新规划
Model-->>Caller: fallback flow/result
Caller->>Cache: 将原记录更新为 flow: []
Cache->>File: 写盘
end
else 未命中、禁用或 YAML 不可用
Caller->>Model: 完整规划
Model-->>Caller: yamlFlow
Caller->>Cache: append 或更新原记录
Cache->>File: 写盘
end
6.2 何时允许 plan cache¶
必须同时满足:
- Agent 已创建
TaskCache。 - 当前调用没有
cacheable: false。 - planning adapter 的
cacheEnabled=true。 - 缓存记录未在本实例中使用过。
- YAML 可以解析,并且至少一个 task 的
flow非空。
标准 planning adapter 默认允许缓存;custom planning adapter 默认关闭,除非 adapter 明确启用。AutoGLM 和 UI-TARS 当前显式配置 cacheEnabled=false,见 auto-glm/adapter.ts 和 ui-tars/adapter.ts。
6.3 不可用 YAML¶
matchPlanCache() 会把以下记录标为 cacheUsable=false:
yamlWorkflow为空或只有空白。- YAML 无法解析。
- 没有 tasks flow。
- 所有 flow 都为空数组。
记录仍返回给调用方,并被标记为已消费。正常规划成功后,updateOrAppendCacheRecord() 通过该 MatchCacheResult.updateFn 原地修复记录,不会追加重复项。
6.4 缓存 workflow 执行失败¶
缓存 YAML 可能执行了一半后失败,例如“关闭偶现弹窗”步骤在本次不存在。此时页面已经被前半段动作改变,fallback planning 得到的只是**从中间状态继续执行**的 flow,不能安全作为下次从初始页面执行的完整缓存。
因此 Agent.aiAct() 不保存 fallback flow,而是把命中的 plan record 改成空 flow。下一次新实例读取时,它被视为不可用并重新生成完整计划。
这是有意的保守策略,不是“缓存更新失败”。
该修复动作发生在 fallback planning 正常返回之后。如果缓存 YAML 失败,并且随后的 fallback planning 也直接抛错,aiAct() 会在执行到 cache update 前退出,旧 plan record 仍可能留在文件中;下一次新 Agent 仍可能再次尝试它。
7. 元素定位缓存链路¶
7.1 定位优先级¶
TaskBuilder 的定位顺序是:
- planning 直接返回的 bbox,且未启用 deepLocate。
- 用户显式传入的 XPath。
- 已缓存的 element feature。
- AI locate。
flowchart TD
Start["Locate task"] --> Plan{"Plan 有 bbox 且非 deepLocate?"}
Plan -->|是| PlanHit["使用 plan bbox"]
Plan -->|否| XPath{"用户传入 xpath 可解析?"}
XPath -->|是| XPathHit["使用显式 xpath"]
XPath -->|否| Cache{"有未消费 locate record?"}
Cache -->|否| AI["调用 AI locate"]
Cache -->|是| Validate["rectMatchesCacheFeature"]
Validate -->|有效| CacheHit["使用缓存 rect"]
Validate -->|异常或无 rect| AI
AI --> Feature{"cacheFeatureForPoint 可用且非空?"}
PlanHit --> Feature
Feature -->|是| Write["更新/追加 locate record"]
Feature -->|否| Done["只返回定位结果"]
Write --> Done
XPathHit --> Done
CacheHit --> Done
7.2 Web XPath 如何生成¶
Web 页面定位成功后,cacheFeatureForPoint():
- 把命中元素中心点转换为页面逻辑坐标。
- 可选调用
AiJudgeOrderSensitive()判断描述是否依赖顺序,例如“第二个按钮”。 - 在页面注入 element inspector。
- 调用
getXpathsByPoint()生成一个或多个候选 XPath。 - 过滤空值并保存为
{ xpaths }。
首次写定位缓存不一定减少模型调用:判断 order-sensitive 本身可能产生一次额外 AI 请求。收益主要体现在后续运行。
7.3 命中时验证¶
rectMatchesCacheFeature() 按顺序尝试候选 XPath:
- 在当前 DOM 中查询 XPath。
- 读取 element info 和 rect。
- 第一个有 rect 的候选即命中。
- 全部失败则抛错,Core 回退 AI locate。
返回的是逻辑坐标 rect,Core 再按截图缩放比例转换为截图坐标。缓存本身不保存旧屏幕绝对坐标。
7.4 缓存失效后修复¶
有两类失效:
- 验证时失效:XPath 找不到元素。当前 Locate task 立即调用 AI,并通过已有
MatchCacheResult原地更新该记录。 - 验证成功但动作结果错误:XPath 找到了一个 rect,但点错元素或未完成目标,随后触发 replanning。
第二类更容易污染缓存。Midscene 的处理流程是:
stateDiagram-v2
[*] --> Loaded: 从 YAML 加载 locate record
Loaded --> Consumed: prompt 精确匹配
Consumed --> Validated: XPath 返回 rect
Validated --> Stable: 动作成功且流程继续
Validated --> Stale: 动作失败或目标未完成并 replan
Stale --> Relocated: 同 prompt 再次走 AI locate
Relocated --> Replaced: 原 index 原地替换
Replaced --> [*]: 写回 YAML
Loaded --> Invalid: XPath 验证失败
Invalid --> Replaced: AI locate 后通过 updateFn 更新
invalidateFailedCacheHitLocates() 在 replan 前扫描本轮 hitBy.from === Cache 的 Locate task,并调用 markLocateCacheStale()。下一次同 prompt 的 AI locate 不会追加到末尾,而会替换刚才被拒绝的 index,避免错误老记录永远排在前面反复命中。
正常重复 prompt 不会被误替换。只有“已经消费并显式标记 stale”的记录才允许原地替换;其他 miss 一律 append。
7.5 Android 为什么没有 locate cache¶
Core 的定位缓存是能力探测式设计,要求设备 interface 实现:
当前仓库中只有 Puppeteer 页面和 Chrome Extension 页面实现这两个方法。Android 没有稳定 DOM/XPath,也没有提供其他 element feature,因此:
matchElementFromCache()发现缺少rectMatchesCacheFeature后返回 miss。- 定位成功后缺少
cacheFeatureForPoint,不会写 locate record。 - Android cache YAML 通常只有 plan records。
如果未来 Android 要支持定位缓存,需要设计 UIAutomator resource-id、accessibility node path、文本/类名组合及二次验证,不能直接复用 Web XPath。
8. cacheable 的作用域¶
cacheable: false 是单次 API 级开关:
对 aiAct:
- 不读取 plan cache。
- 不写 plan cache。
- 该值继续传给本轮生成的 Locate subtasks,使其不使用/写入定位 feature。
对单独 locate/action:buildDetailedLocateParam() 默认 cacheable=true,显式 false 后 matchElementFromCache() 不使用记录,TaskBuilder 也不写新 feature。
当前实现会在 cacheable guard 之前调用 matchLocateCache(),所以一条记录可能被标记为“本实例已消费”,随后因 cacheable=false 没有实际使用。这意味着:
- 同一 Agent 后续相同 prompt 不能再命中该条记录。
cleanUnused可能把它视作本次使用过的旧记录并保留。
这不影响当前调用绕过缓存的结果,但属于缓存统计和同实例后续行为的实现边界。
查询和断言任务没有对应 cache read/write 分支;即使 Agent 开启缓存,它们仍调用模型。
9. 自动写入、手动清理和版本兼容¶
9.1 自动写入¶
read-write/write-only 模式每次 append、update 或 stale replacement 都调用 flushCacheToFile():
- 确保目录存在。
- 复制并排序 records。
yaml.dump({ lineWidth: -1 })。writeFileSync()重写整个文件。
这不是增量 append,也不是临时文件加 rename 的原子写入。
9.2 手动清理¶
清理规则:
- 保留本次运行命中的原始记录。
- 保留本次运行新增的记录。
- 删除从文件加载但本次未命中的记录。
- write-only 因不读取缓存而跳过清理。
- read-only 允许通过手动 flush 清理并覆盖文件。
清理是显式操作,不会在 Agent destroy 时自动执行。没有 TTL、LRU 或按文件大小自动淘汰。
9.3 版本检查¶
加载 YAML 时会检查 midsceneVersion:
- 低于
0.16.10且不是 beta 字符串时拒绝使用。 - 旧
.json文件存在时给出迁移警告。 - YAML 解析、字段读取或 semver 检查异常时整体按 cache miss 处理。
- 加载成功后只把内存中的
midsceneVersion更新为当前版本。
它没有严格 schema migration,也不拒绝高于当前版本的文件。版本通过不代表 plan/action schema 一定兼容,真正执行失败时仍依赖 plan fallback。
10. 报告和调试可观测性¶
命中缓存的 task 会设置:
规划缓存还保存 hitBy.context.yamlString。Report App 在侧栏显示 Cache 标签,在详情页展示命中的 YAML,见 sidebar/index.tsx 和 detail-side/index.tsx。
调试日志:
重点日志包括:
- cache file 是否加载成功和记录数量。
- prompt 是否命中、命中 index。
- plan flow 是否为空/无效。
- element feature 是否验证成功。
- stale record 是否被标记和替换。
- 自动/手动写盘是否成功。
报告只能证明某个 task 标记为 Cache,不能证明整个 aiAct 完全没有模型调用。应同时检查 task usage 和 locate subtasks。
11. 性能与 Token 收益¶
11.1 能省掉什么¶
| 命中类型 | 通常省掉 | 仍可能发生 |
|---|---|---|
| plan cache | planning 模型调用 | YAML 内的 locate、query、assert、动态 replan |
| locate cache | locate 视觉模型调用 | DOM XPath 验证、动作执行、后续 planning |
| 两者都命中 | planning + locate 调用 | 页面操作和状态等待 |
Android 只有 plan cache,因此收益取决于缓存 YAML 的内容。若每一步仍是自然语言 aiTap,定位 token 仍会消耗;若 planning 结果自带可直接使用的 bbox,但页面布局变化,坐标复用又有稳定性风险。
11.2 本地开销¶
- 每次自动更新都同步序列化并重写整个 YAML 文件。
- 缓存记录很多时,
writeFileSync()会阻塞当前 Node 事件循环。 - 匹配是从头线性扫描,复杂度约为 O(record count)。
- prompt object 使用 deep strict equality,图片型大对象会增加比较成本。
当前缓存文件一般较小,所以实现偏简单。平台化到大量 Journey、多个设备时,应避免所有任务共享一个巨大 cache id。
12. 并发和 CI 边界¶
TaskCache 没有:
- 文件锁。
- 进程间合并。
- revision/CAS 检查。
- 原子临时文件 rename。
- 并发写队列。
两个 Agent/Runner 同时使用同一路径时:
sequenceDiagram
participant A as Runner A
participant B as Runner B
participant F as cache.yaml
A->>F: 启动时读取版本 V0
B->>F: 启动时读取版本 V0
A->>A: 追加 record A
B->>B: 追加 record B
A->>F: writeFileSync(V0 + A)
B->>F: writeFileSync(V0 + B)
Note over F: 最终 A 可能丢失,last writer wins
CI 建议:
- cache id 至少包含项目、Journey/测试用例和稳定环境标识。
- 并行 job 不写同一个 cache file。
- 正式回归使用 read-only,缓存生成使用独立 write-only job。
- 生成 job 完成后,通过 MR 审核 cache diff,再进入正式分支。
- 不要让失败测试自动覆盖共享缓存;只在测试通过后手动 flush/发布 artifact。
13. 正确性与安全边界¶
13.1 无页面环境维度¶
缓存键主要是 prompt。页面变化依赖“执行失败后回退”被动发现,因此存在两类风险:
- plan 在错误页面仍能成功执行一部分副作用,然后才失败。
- XPath 在新 DOM 中仍能找到 rect,但语义已经变了。
高风险操作如支付、删除、提现不应只依赖旧 plan cache。至少需要执行前后断言、环境隔离和测试账号。
13.2 明文敏感信息¶
YAML 可能包含:
- 用户 prompt 和 action context。
aiInput的输入值。- 页面文本和 XPath。
- 测试账号或业务数据。
文件没有加密或脱敏。不要把真实密码、token、验证码、支付信息写进可缓存 prompt/workflow。
13.3 缓存文件具有“可执行配置”属性¶
plan record 的 YAML 会直接交给 runYaml()。如果项目支持 custom action,恶意修改缓存可能触发具有副作用的动作。缓存文件应像测试脚本一样接受代码审查,不能无条件信任外部 artifact。
13.4 路径输入必须可信¶
cacheId 会替换 Windows 文件名非法字符和空格,但源码明确保留 /、\,也没有移除 ..。配合 path.join() 时,包含 ../ 的 id 可以把文件写出预期 cache 子目录。
因此:
- 不要把用户输入、仓库名、分支名原样作为 cache id。
- 平台侧应限制 id 为安全字符集,例如
[A-Za-z0-9._-]+,并拒绝..。 cacheDir应由服务端配置,不允许普通测试用户提交任意绝对路径。
14. 已知局限与适用建议¶
14.1 已知局限¶
- 无 TTL 和自动环境指纹。
- 无跨进程并发安全。
- 文件整包同步重写。
- Android/iOS 等无 DOM 平台缺少 locate cache adapter。
- custom planner 默认不支持 plan cache。
- fallback 只能在执行时发现陈旧 plan,可能已经产生部分副作用。
- 缓存 plan 和 fallback planning 连续失败时,旧 plan 可能来不及写成空 flow。
cacheable=false的 locate record 仍可能被内部 bookkeeping 消费。- 版本检查只提供最低版本门槛,不是完整 schema 兼容保证。
14.2 推荐场景¶
- 页面结构和数据稳定的回归测试。
- 重复执行的长
aiAct流程。 - Web DOM 稳定且 XPath 可验证的元素定位。
- CI 中经过审核的只读缓存。
14.3 不推荐场景¶
- 生产真实账号的支付、提现、删除操作。
- 页面高度动态、A/B 实验频繁或多租户结构不同。
- 多 Runner 并发写同一 cache id。
- 依赖 Canvas/WebGL/跨域 iframe/closed Shadow DOM 的 Web 元素定位。
- 期望缓存后完全不配置 AI 服务;缓存失效和未缓存任务仍需要模型。
15. 测试覆盖¶
Core 和 Web Integration 已覆盖多数关键行为:
task-cache-empty-flow.test.ts:空/非法 YAML 不可用和原地修复。ai-act-plan-cache-fallback.test.ts:缓存 plan 失败后禁用旧 flow。task-cache-poisoning.test.ts:错误 locate record stale replacement。bbox-locate-cache.test.ts:plan bbox、AI locate 和 locate cache 更新。aiaction-cacheable.test.ts:cacheable向 locate subtasks 传播。agent-cache-config.test.ts:id、strategy 和 cacheDir 校验。task-cache.test.ts:YAML、顺序、策略、清理、旧格式和长文件名。
当前明显缺少:
- 多进程/多 Agent 同文件并发写测试。
- 原子写入中途进程退出恢复测试。
- cache id 路径穿越拒绝测试;当前行为是保留 path separator。
- Android plan cache 端到端 token/耗时基准。
- 应用版本、模型版本变化后的自动失效策略测试。
16. 关键源码索引¶
| 模块 | 文件 | 责任 |
|---|---|---|
| 缓存主体 | task-cache.ts |
YAML、匹配、写入、stale、清理 |
| 配置校验 | cache-config.ts |
id、strategy、cacheDir |
| 兼容配置 | utils.ts |
新 cache 与旧环境变量优先级 |
| 规划缓存 | agent.ts |
plan match、YAML 执行和 fallback |
| action/replan | tasks.ts |
缓存定位失败后的 stale 标记 |
| 定位缓存 | task-builder.ts |
定位优先级、feature 读写 |
| feature 验证 | agent/utils.ts |
cache feature -> rect |
| Web XPath | base-page.ts |
XPath 生成和恢复 |
| Web cache helper | cache-helper.ts |
XPath 清洗、顺序敏感判断 |
| planning 策略 | planning.ts |
adapter cacheEnabled 默认值 |
| 报告标记 | sidebar/index.tsx |
Cache 标签 |