日志与运行时调试
当页面检查发现结果不符合预期时,agent-lynx 可以将页面现象与日志、已加载的脚本和 JavaScript 实时值对应起来,确认代码执行到哪里、收到了什么运行时输入。
从点击无响应追到运行时错误
沿用页面检查中的 Tailwind CSS Settings 示例,这次点击 Ocean 后,主题没有切换。为了复现这个故障,我们在本地副本的点击回调中加入了对缺失配置的读取;原示例没有这处错误。
下面的命令与输出采集自 Android 上的 Lynx Explorer。agent 已经连接应用并选定页面,$CLIENT_ID 是 list-clients 返回的目标应用 ID,本次页面的 session 为 2。
点了 Ocean,但主题没有切换,帮我查一下原因。
思路:先找到 Ocean 按钮,启动错误日志监听,再点击一次复现问题。
agent-lynx snapshot找到 Ocean 按钮
# 读取当前页面的元素引用;这里节选主题区域,Ocean 的按钮容器是 @e15。
agent-lynx snapshot --client "$CLIENT_ID" --session 2@e1 [scroll-view] @(207,450) {scrollable}
└─ @e2 [view] @(207,460)
├─ @e3 [view] @(207,70)
│ ├─ @e4 [text] "Settings" @(207,59)
│ └─ @e5 [text] "Manage your account and preferences." @(207,86)
├─ @e6 [view] @(207,177)
│ ├─ @e7 [view] @(207,153)
│ │ ├─ @e8 [text] "Theme" @(207,143)
│ │ └─ @e9 [text] "Switch design tokens at runtime via CSS variables." @(207,167)
│ └─ @e10 [view] @(207,207)
│ ├─ @e11 [view] @(91,206)
│ │ └─ @e12 [text] "Dark" @(91,206)
│ ├─ @e13 [view] @(207,206)
│ │ └─ @e14 [text] "Light" @(208,206)
│ └─ @e15 [view] @(324,206)
│ └─ @e16 [text] "Ocean" @(323,206)agent-lynx get-console --watch捕获两条错误
# 先保持日志监听运行,再执行下一步点击。下面是这次点击产生的两条错误;收到两条后,监听自动结束。
agent-lynx get-console --watch --level error --limit 2 --client "$CLIENT_ID" --session 2[11:25:29.411Z] [error/background] The following error occurred in the JSRuntime:
at file://shared/lynx_core.js:2:10030
at file://shared/lynx_core.js:8:21708
at file://shared/lynx_core.js:8:21934
at file://shared/lynx_core.js:8:5388
at file://view3/.lynx/main/background.js:9055:27
[11:25:29.411Z] [error/background] TypeError: cannot read property 'ocean' of undefined
at <anonymous> (file://view3/.lynx/main/background.js:487:73)
at publishEvent (file://view3/.lynx/main/background.js:9054:27)
at file://shared/lynx_core.js:3:27
at file://shared/lynx_core.js:8:21708
at file://shared/lynx_core.js:8:21934
at file://shared/lynx_core.js:8:5388
at file://view3/.lynx/main/background.js:9055:27agent-lynx tap @e15已复现点击
# 在日志监听期间点击当前快照中的按钮,让同一次操作的错误进入采集窗口。
agent-lynx tap @e15 --client "$CLIENT_ID" --session 2Tapped @e15 at (324,206). Re-run `agent-lynx snapshot` before using refs again.思路:后台 VM 抛出了读取 ocean 的 TypeError。调用栈指向 background.js,先找到这份运行中脚本的 ID。
agent-lynx get-sources匹配到脚本 5
# 列出当前后台 VM 的脚本。app-service.js 也在列表中;本次错误对应的是 .lynx/main/background.js。
agent-lynx get-sources --client "$CLIENT_ID" --session 2[
{
"scriptId": "1",
"url": "file://shared/lynx_core.js"
},
{
"scriptId": "4",
"url": "file://view3/app-service.js"
},
{
"scriptId": "5",
"url": "file://view3/.lynx/main/background.js"
}
]agent-lynx cdp --method Debugger.getScriptSource找到出错的回调
# 读取脚本 5 的真实代码,再用 jq 提取 scriptSource 的第 484–491 行,查看调用栈附近的点击回调。
agent-lynx cdp --thread background --method Debugger.getScriptSource '{"scriptId":"5"}' --client "$CLIENT_ID" --session 2 | jq -r '.scriptSource | split("\n")[483:491][]' $0: themeNames.map((t)=>/*#__PURE__*/ (0,_lynx_js_react_jsx_dev_runtime__rspack_import_0.jsxDEV)(__snapshot_835da_abf5c_2, {
values: [
`flex-1 rounded-md py-2 items-center justify-center border ${currentTheme === t ? "bg-primary border-primary" : "bg-secondary border-border"}`,
()=>setCurrentTheme(lynx.__globalProps.themeNames[t]),
`text-sm font-medium ${currentTheme === t ? "text-primary-foreground" : "text-secondary-foreground"}`
],
$0: t[0].toUpperCase() + t.slice(1)
}, t, false, {思路:回调在调用 setCurrentTheme 前先读取 lynx.__globalProps.themeNames[t]。检查当前后台 VM 中的 themeNames 是否存在。
agent-lynx evaluate字段为 undefined
# 读取报错表达式中的配置字段,不修改页面状态。
agent-lynx evaluate 'lynx.__globalProps.themeNames' --thread background --client "$CLIENT_ID" --session 2{
"result": {
"type": "undefined"
}
}点击触发了事件回调,但回调在读取 lynx.__globalProps.themeNames[t] 时抛出了 TypeError。当前 themeNames 是 undefined,读取它的 ocean 属性失败,因此 setCurrentTheme 没有执行。
这个示例已经通过遍历得到主题名 t,应恢复为 setCurrentTheme(t)。如果业务确实需要宿主配置,则需要先约定并传入该字段,再处理缺失时的行为。
日志将“没有切换”定位到后台 VM 的异常,脚本源码说明了哪次属性读取失败,evaluate 则验证了当前值。脚本列表中可见 app-service.js,而本次调用栈指向 .lynx/main/background.js;应按实际返回的 URL 和脚本 ID 继续查找。
get-console 订阅运行时日志,不保证保留历史记录。这段对话先启动 --watch,在监听期间点击按钮,并在收到两条错误后结束采集。一次短窗口没有输出,不能说明页面从未报错。
从页面元素定位 JSX 和事件处理函数
调用栈给出了运行中脚本的位置,UI source map 则能把页面上的元素对应到编写它的 JSX。继续查看 Ocean 按钮,就能把上一段中的错误回调落到本地源文件上。
这段示例在支持 DOM.getOriginalNodeSourceInfo 的开发版客户端中运行同一份 bundle,构建时启用了 UI source map。$CLIENT_ID 指向这个客户端,本次页面的 session 为 1。
Ocean 按钮对应哪段 JSX?把它的点击处理函数也找出来。
思路:重新读取页面快照,选中 Ocean 的按钮容器,再把这个元素映射到源码。
agent-lynx snapshot找到 Ocean 按钮
# 刷新当前页面的引用;这里节选主题区域,使用包含 Ocean 文本的 @e15 容器。
agent-lynx snapshot --client "$CLIENT_ID" --session 1@e1 [scroll-view] @(207,450) {scrollable}
└─ @e2 [view] @(207,460)
├─ @e3 [view] @(207,70)
│ ├─ @e4 [text] "Settings" @(207,59)
│ └─ @e5 [text] "Manage your account and preferences." @(207,86)
├─ @e6 [view] @(207,177)
│ ├─ @e7 [view] @(207,153)
│ │ ├─ @e8 [text] "Theme" @(207,143)
│ │ └─ @e9 [text] "Switch design tokens at runtime via CSS variables." @(207,167)
│ └─ @e10 [view] @(207,207)
│ ├─ @e11 [view] @(91,206)
│ │ └─ @e12 [text] "Dark" @(91,206)
│ ├─ @e13 [view] @(207,206)
│ │ └─ @e14 [text] "Light" @(208,206)
│ └─ @e15 [view] @(324,206)
│ └─ @e16 [text] "Ocean" @(323,206)agent-lynx get-source-info @e15App.tsx:62:15
# 根据运行时节点和 UI source-map 元数据,解析该按钮的仓库、源文件、行和列。
agent-lynx get-source-info @e15 --client "$CLIENT_ID" --session 1{
"debugMetadataUrl": "http://127.0.0.1:33127/.lynx/main/debug-metadata.json",
"lazyBundleUrl": null,
"nodeId": 82,
"nodeIndex": 713223749,
"tag": "view",
"repo": "lynx-family/lynx-examples",
"source": "/tmp/agent-lynx-real-debugging-20261030/lynx-examples/examples/tailwindcss/src/App.tsx",
"line": 62,
"column": 15,
"status": "mapped",
"reason": null
}思路:映射指向 App.tsx 第 62 行的 view。读取附近的 JSX,查看这个节点绑定的事件处理函数。
Read · examples/tailwindcss/src/App.tsx找到 bindtap
// 读取本地示例副本中的第 61–80 行;第 69 行是本次复现加入的出错回调。
Read examples/tailwindcss/src/App.tsx:61–80 61 {themeNames.map((t) => (
62 <view
63 key={t}
64 className={`flex-1 rounded-md py-2 items-center justify-center border ${
65 currentTheme === t
66 ? "bg-primary border-primary"
67 : "bg-secondary border-border"
68 }`}
69 bindtap={() => setCurrentTheme(lynx.__globalProps.themeNames[t])}
70 >
71 <text
72 className={`text-sm font-medium ${
73 currentTheme === t
74 ? "text-primary-foreground"
75 : "text-secondary-foreground"
76 }`}
77 >
78 {t[0].toUpperCase() + t.slice(1)}
79 </text>
80 </view>Ocean 按钮对应 examples/tailwindcss/src/App.tsx 第 62 行、第 15 列的 view,它由 themeNames.map 创建。第 69 行的 bindtap 绑定了点击处理函数,正是前面读取缺失配置的那段代码。
get-source-info 返回元素的源码位置,agent 再读取对应文件,才能看到事件绑定和业务逻辑。mapped 表示映射成功;运行时生成的包装节点或缺少元数据的页面可能无法得到源码位置。
在同一个目标上串联证据
同一个 session 可以包含 background 和 main 两个 VM。示例日志标为 background,因此后续的脚本请求和表达式求值也指向后台 VM。get-sources 列出的是后台 VM 中的脚本。
脚本 ID 和远程对象 ID 都属于对应的 client、session 和 VM。例如,日志中包含 objectId 时,可以通过 cdp 的 Runtime.getProperties 在同一目标上读取对象属性。页面重载或切换后,应重新获取日志和脚本 ID 再继续排查。
日志与运行时调试能力
日志过滤、输出格式、VM 选择和表达式求值参数见 CLI 参考。需要继续检查组件 props、state 和层级关系时,参见 ReactLynx 组件。