# collect_lineloss.js 从生成到可用的完整排查记录 本文档记录了 `tq-lineloss-report` skill 脚本从初始生成到最终可用的全部排查过程,包括遇到的每个错误、根因分析和修复方法。可作为后续类似 skill 开发的排查模板。 --- ## 背景 ### 架构概览 ``` 用户输入 "兰州公司 月累计 2026-03。。。" │ ▼ sgClaw Rust 进程 ├── 解析指令 → DeterministicExecutionPlan ├── 读取 collect_lineloss.js 脚本 ├── 包装为 IIFE:(function(){ const args = {...}; <脚本内容> })() ├── 调用 sgBrowserExcuteJsCodeByDomain(domain, wrappedJs) │ 注入到浏览器中匹配 domain 的页面执行 ├── 等待回调:脚本通过 callBackJsToCpp 返回 JSON 结果 ├── 解析 artifact JSON → 提取 status/rows/reasons └── 生成 XLSX(Rust 侧)→ 返回 outcome ``` ### 关键差异:原始场景 vs Skill 模式 | 对比项 | 原始场景 (index.html) | Skill 模式 | |--------|----------------------|------------| | 脚本注入方式 | `sgBrowserExcuteJsCode(exactURL, js)` — 精确 URL | `sgBrowserExcuteJsCodeByDomain(domain, js)` — 仅域名匹配 | | 执行页面 | 业务子页面 `/tqLinelossStatis/tqQualifyRateMonitor` | 可能命中父框架页 `/gsllys` | | `window.mac` | 有(Vue 实例,`mounted()` 中 `window.mac = this`) | 无(没有 Vue 实例) | | 导出 Excel | JS 调 `localhost:13313`(本地场景页可访问) | JS 无法调 `localhost:13313`(CORS 阻断) | | 结果回传 | Rust 只需要 `.then()` 回调结果 | 同左,但脚本是 async 函数需 `.then()` 处理 | --- ## 排查时间线 ### 第 1 阶段:基础管道问题 #### 问题 1: `missing_expected_domain` **现象**: `status=blocked reasons=missing_expected_domain` **根因**: Rust 侧 `deterministic_submit.rs` 构造 args 时没有传 `expected_domain` 字段。`derive_expected_domain()` 从 `page_url` 提取 host 时只取了域名不含端口,但传入 args 时 key 不匹配。 **修复**: 确保 `deterministic_submit_args()` 正确插入 `expected_domain` 到 args Map。 **涉及文件**: `src/compat/deterministic_submit.rs` **是否需要重新编译**: 是 --- #### 问题 2: `target_url` 缺少端口号 **现象**: 脚本注入失败或注入到错误页面。 **根因**: `target_url` 被设为 `http://20.76.57.61`(无端口),但实际业务页面在 `http://20.76.57.61:18080/gsllys/...`。`sgBrowserExcuteJsCodeByDomain` 需要能匹配到正确的标签页。 **修复**: 在 `deterministic_submit.rs` 中设置完整 `target_url`: ```rust const LINELLOSS_TARGET_URL: &str = "http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"; ``` **涉及文件**: `src/compat/deterministic_submit.rs` **是否需要重新编译**: 是 --- #### 问题 3: 脚本返回 `{}` 空对象 **现象**: Rust 侧收到的 artifact 是 `{}`,无任何数据。 **根因**: `collect_lineloss.js` 的入口 `buildBrowserEntrypointResult()` 是 `async` 函数,返回 Promise。Rust 侧 `build_eval_js` 包装器原来直接调用 `_s(v)` 发送结果,但 `v` 是一个 Promise 对象,JSON.stringify 后变成 `{}`。 **修复**: 在 `build_eval_js`(`callback_backend.rs`)中增加 Promise 检测: ```rust // 旧代码 "_s(v);" // 新代码 "if(v&&typeof v.then==='function'){v.then(_s).catch(function(){});}else{_s(v);}" ``` 如果返回值是 thenable(Promise),等它 resolve 后再发送回调。 **涉及文件**: `src/browser/callback_backend.rs` 中 `build_eval_js` 函数 **是否需要重新编译**: 是 **教训**: 所有 browser_script skill 如果入口函数是 async(返回 Promise),都需要这个 `.then()` 处理。这是管道层的通用修复。 --- ### 第 2 阶段:页面上下文问题 #### 问题 4: `page_context_unavailable` (mac_missing) **现象**: ``` tq-lineloss-report 国网兰州供电公司 2026-03 status=blocked rows=0 reasons=page_context_unavailable ``` **排查过程**: 1. 在 `validatePageContext` 中添加诊断信息: ```javascript // 临时诊断代码 const diag = 'href=' + href + '|host=' + host + '|port=' + port + '|title=' + title + '|mac=' + hasMac; return { ok: false, reason: 'page_context_unavailable:mac_missing|' + diag }; ``` 2. 页面返回的诊断结果: ``` href=http://20.76.57.61:18080/gsllys host=20.76.57.61 port=18080 title=台区线损大数据分析模块 mac=false ``` **根因**: `sgBrowserExcuteJsCodeByDomain("20.76.57.61")` 匹配到了父框架页 `/gsllys`,而不是业务子页面。`window.mac` 是业务子页面的 Vue 实例,在 `mounted()` 中通过 `window.mac = this` 设置,父框架页没有这个实例。 **关键认知**: 在 Skill 模式下没有 Vue 实例,`window.mac` 检查在架构上就不适用。脚本通过 AJAX 发绝对 URL 请求,不依赖页面本地状态。 **修复**: 删除 `globalThis.mac` 检查,只保留 host 匹配: ```javascript // 修复前 validatePageContext(args) { // ... 含 mac 检查 + 诊断代码 if (!hasMac) { return { ok: false, reason: 'page_context_unavailable:mac_missing|' + diag }; } } // 修复后 validatePageContext(args) { const host = normalizeText(globalThis.location?.hostname); const expected = normalizeText(args.expected_domain); if (!host) { return { ok: false, reason: 'page_context_unavailable' }; } if (host !== expected) { return { ok: false, reason: 'page_context_mismatch' }; } return { ok: true }; }, ``` **涉及文件**: `collect_lineloss.js` — `validatePageContext` 函数 **是否需要重新编译**: 否(JS 文件运行时读取) **排查技巧**: 在 reasons 中拼接诊断信息(href/host/port/title/mac),不需要 F12 console,直接通过 Rust 侧的 summary 输出就能看到。 --- ### 第 3 阶段:API 请求问题 #### 问题 5: `api_query_failed` — 返回 HTML 而非 JSON **现象**: ``` status=error rows=0 reasons=api_query_failed:month_api_failed: SyntaxError: Unexpected token '<', " reject(new Error( `month_api_failed(${xhr.status}): ${String(err)}|body=${String(xhr.responseText || '').substring(0, 200)}` )) }); ``` **涉及文件**: `collect_lineloss.js` — `queryMonthData` 和 `queryWeekData` **是否需要重新编译**: 否 **排查技巧**: 在 error handler 中拼接 `xhr.responseText` 的前 200 字符到 reasons 中。如果看到 ` row[key] !== '') ? row : null; } // 修复后 function normalizeMonthRow(rawRow) { const row = {}; for (const key of MONTH_COLUMNS) { const v = rawRow?.[key]; row[key] = (v === null || v === undefined || v === '') ? '' : String(v).trim(); } return MONTH_COLUMNS.every((key) => row[key] !== '') ? row : null; } ``` **涉及文件**: `collect_lineloss.js` — `normalizeMonthRow` **是否需要重新编译**: 否 **通用规则**: 内网 API 返回的 JSON 中数值字段通常是 `number` 类型而非字符串。行规范化函数必须用 `String(v)` 进行类型转换,不能依赖 `typeof === 'string'` 判断。 --- ### 第 5 阶段:导出问题(架构级) #### 问题 8: 导出永久挂起 **现象**: ``` tq-lineloss-report 国网兰州供电公司 2026-03 status=pl rows=12 ``` 数据采集成功(12 行),但之后永远没有返回,脚本卡死在导出步骤。 **排查过程**: 1. `exportWorkbook` 调用 `fetch('http://localhost:13313/...')` — CORS 阻断 2. 改用 `$.ajax({ crossDomain: true })` — 同样阻断 3. 确认这是浏览器安全模型限制,不是配置问题 **根因**: 脚本运行在远程页面 `http://20.76.57.61:18080` 上,浏览器禁止从远程页面向 `localhost:13313` 发起请求(同源策略 + Mixed Content)。`crossDomain: true` 只是告诉 jQuery 用跨域模式,并不能绕过浏览器安全策略。 原始场景的解决方式:有一个本地场景页面(`localhost` 上的 `index.html`)充当代理,先在远程页面采集数据,再通过 `postMessage` 或回调传回本地页面,由本地页面调用 `localhost:13313`。 Skill 模式没有本地场景页面,因此这种代理机制不存在。 **解决方案**: 将导出逻辑从浏览器 JS 移到 Rust 侧(方案 A2: Rust 本地生成 XLSX)。 **最终架构**: ``` JS (浏览器): 采集数据 → 返回 artifact { rows, column_defs, status } ↓ Rust (本地): 解析 artifact → 提取 rows + column_defs → 生成 XLSX 文件 ``` **具体修改**: 1. **JS 侧**: 删除 `exportWorkbook()`、`writeReportLog()`、`postJson()`、`buildExportPayload()` 等导出相关代码。artifact 中添加 `column_defs` 字段,export 状态设为 `deferred_to_rust`。 2. **Rust 侧**: 新增 `lineloss_xlsx_export.rs`,用 `zip` crate + OpenXML XML 生成 XLSX。在 `deterministic_submit.rs` 中,收到 artifact 后调用 XLSX 生成。 **涉及文件**: - `collect_lineloss.js` — 删除导出代码,添加 `column_defs` - `src/compat/lineloss_xlsx_export.rs` — 新增 - `src/compat/deterministic_submit.rs` — 新增导出集成 - `src/compat/mod.rs` — 注册新模块 **是否需要重新编译**: 是 **通用规则**: 任何从远程页面调用 `localhost` 的操作在 Skill 模式下都不可行。导出/写日志等需要访问本地服务的功能必须放到 Rust 侧实现。 --- ## 排查方法论总结 ### 1. 诊断信息注入模式 脚本运行在浏览器中,无法看 F12 console。唯一的信息通道是 artifact JSON 的 `reasons` 字段。 ```javascript // 在 catch 块中注入详细错误 reasons: ['api_query_failed:' + String(error?.message || error || 'unknown')] // 在规范化失败时注入原始数据摘要 reasons: ['row_normalization_failed:rawRows=' + rawRows.length + '|keys=' + Object.keys(rawRows[0]).join(',')] // 在页面上下文检查中注入环境信息 reason: 'page_context_unavailable:mac_missing|href=' + href + '|host=' + host + '|port=' + port ``` Rust 侧的 summary 输出会包含这些 reasons,直接在日志中可见。 ### 2. 逐层排查顺序 ``` Layer 1: 管道层(Rust) ├── args 是否正确传入?(expected_domain, target_url, org_code 等) ├── 脚本文件是否正确读取? ├── async 返回值是否被正确处理?(.then() 模式) └── 回调是否成功返回? Layer 2: 页面上下文(JS) ├── 脚本注入到了哪个页面?(href, title) ├── 页面是否有需要的全局变量?(window.mac 等) └── domain 匹配是否正确? Layer 3: API 请求(JS) ├── 请求头是否完整?(X-Requested-With) ├── 返回格式是否正确?(JSON vs HTML) └── 返回状态码? Layer 4: 数据处理(JS) ├── API 返回的字段名是否匹配列定义? ├── 字段值类型是否兼容?(number vs string) └── 规范化后是否有有效行? Layer 5: 导出(架构) ├── 是否涉及跨域请求? ├── localhost 是否可达? └── 是否需要 Rust 侧处理? ``` ### 3. 修改后验证检查清单 - [ ] JS 文件语法检查:`node -e "require('./collect_lineloss.js')"` - [ ] 如果改了 Rust 代码:`cargo build` 编译通过 - [ ] `cargo test` 全部通过(排除已知的 pre-existing failures) - [ ] 替换 JS 文件到部署目录 - [ ] 如果改了 Rust:重新部署编译后的 sgclaw 二进制 --- ## 最终文件清单 ### JS 文件: `collect_lineloss.js` **位置**: `D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging/skills/tq-lineloss-report/scripts/collect_lineloss.js` **功能**: 纯数据采集。注入到浏览器,查询线损平台 API,返回结构化 artifact。 **不做的事**: 不调 localhost:13313,不导出 Excel,不写 report log。 ### Rust 文件: 修改清单 | 文件 | 修改内容 | 修改类型 | |------|---------|---------| | `src/browser/callback_backend.rs` | `build_eval_js` 增加 `.then()` 处理 async 返回值 | 管道层通用修复 | | `src/compat/deterministic_submit.rs` | 完整 `target_url`; 解析 artifact 后调 XLSX 导出 | 业务集成 | | `src/compat/lineloss_xlsx_export.rs` | XLSX 生成(zip + OpenXML) | 新增 | | `src/compat/mod.rs` | 注册 `lineloss_xlsx_export` 模块 | 新增 | --- ## 快速复用模板 新建类似 skill 时,直接检查以下要点: 1. **`build_eval_js` 是否支持 async**:入口函数如果是 `async`,确认 `callback_backend.rs` 中有 `.then()` 处理。 2. **`validatePageContext` 不检查页面局部状态**:只检查 host,不检查 `window.mac`、`window.app` 等场景页专属变量。 3. **API 请求必须带 `X-Requested-With: XMLHttpRequest`**:内网 Java 后端的标配。 4. **列定义从原始场景代码精确复制**:找 `cols1`/`cols2` 或表格 `columns` 配置。 5. **`normalizeRow` 用 `String(v)` 而非 `pickFirstNonEmpty`**:API 返回数字不是字符串。 6. **导出不走浏览器,走 Rust 侧**:JS 返回 rows + column_defs,Rust 生成 XLSX。