fix(runtime): onGenerateRoute pathElements 越界 RangeError

- buy-service/renew-minioffice 加长度检查 (pathElements.length >= 4/3)
- 根因: Flutter web 深链接渐进式路径解析, onGenerateRoute 被调用多次,
  pathElements 递增 ([,buy-service] -> [,buy-service,1] -> 完整),
  前几次路径短但 pathElements[1]已匹配 -> int.parse([2])/[3] 越界
- 直接访问/刷新 /buy-service/x/y, /renew-minioffice/x 时崩溃
- 恢复 buy_service 实验代码 (data 校验 + mounted 保留)
This commit is contained in:
2026-08-02 00:13:33 +08:00
parent af8934fe7f
commit 901e42a41b
13 changed files with 192 additions and 5 deletions

16
tools/e2e/diag_ctx.js Normal file
View File

@@ -0,0 +1,16 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(8000);
for (let i=0;i<logs.length;i++){
if(logs[i].startsWith('CK_ERROR')){console.log(logs[i].slice(0,120));
const ctx=logs.slice(i+1,i+4).find(l=>l.startsWith('CK_CTX'));
if(ctx) console.log(' CTX:', ctx.slice(0,150));
}
}
await browser.close();
})();

13
tools/e2e/diag_deep.js Normal file
View File

@@ -0,0 +1,13 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(7000);
const ctx = logs.filter(l => l.startsWith('CK_CTX'));
if (ctx.length) console.log(ctx[0].slice(0, 600));
else console.log('无 CK_CTX');
await browser.close();
})();

14
tools/e2e/diag_exp.js Normal file
View File

@@ -0,0 +1,14 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(7000);
const range = logs.filter(l => l.includes('RangeError'));
const err = logs.filter(l => l.includes('CK_BUY_ERR'));
console.log('RangeError 数:', range.length, err.length ? '| CK_BUY_ERR:'+err[0].slice(0,100) : '');
if (range.length) console.log(range[0].slice(0,120));
await browser.close();
})();

15
tools/e2e/diag_paths.js Normal file
View File

@@ -0,0 +1,15 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(7000);
console.log('=== path elements 打印(看是否有短路径)===');
logs.filter(l => l.includes('path elements')).forEach(l => console.log(' ' + l.slice(0,80)));
console.log('=== RangeError 之前的最后几条 console ===');
const idx = logs.findIndex(l => l.includes('RangeError'));
if (idx >= 0) logs.slice(Math.max(0,idx-4), idx).forEach(l => console.log(' ' + l.slice(0,90)));
await browser.close();
})();

14
tools/e2e/diag_routes.js Normal file
View File

@@ -0,0 +1,14 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
for (const route of ['/download', '/contact-stores', '/me', '/orders', '/buy-service/1/ministore']) {
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
let range = 0;
page.on('console', m => { if (m.text().includes('RangeError')) range++; });
await page.goto('http://127.0.0.1:8099'+route, { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(6000);
console.log(`${range>0?'✗ '+range+' RangeError':'✓ OK'} ${route}`);
await page.close();
}
await browser.close();
})();

13
tools/e2e/diag_seq.js Normal file
View File

@@ -0,0 +1,13 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(7000);
const seq = logs.filter(l => l.startsWith('CK_BS_') || l.includes('RangeError'));
console.log('时序:');
seq.slice(0,10).forEach(l => console.log(' ' + l.slice(0,90)));
await browser.close();
})();

19
tools/e2e/diag_stack.js Normal file
View File

@@ -0,0 +1,19 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(7000);
const stack = logs.filter(l => l.startsWith('CK_STACK'));
if (stack.length) {
// 找 .dart 源文件帧(非 main.dart.js
const dartFrames = stack[0].split('\n').filter(l => l.includes('.dart') && !l.includes('main.dart.js'));
console.log('源文件帧:', dartFrames.length ? dartFrames.slice(0,5).join('\n ') : '(无,全是 main.dart.js)');
// 也看前 3 个 main.dart.js 帧
const jsFrames = stack[0].split('\n').filter(l => l.includes('main.dart.js')).slice(0,4);
console.log('main.dart.js 帧:', jsFrames.join('\n '));
}
await browser.close();
})();

11
tools/e2e/diag_then.js Normal file
View File

@@ -0,0 +1,11 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
const logs = [];
page.on('console', m => logs.push(m.text()));
await page.goto('http://127.0.0.1:8099/buy-service/1/ministore', { waitUntil: 'load' }).catch(()=>{});
await page.waitForTimeout(8000);
logs.filter(l => l.includes('CK_BUY_THEN') || l.includes('RangeError') || l.includes('CK_ERROR')).slice(0,5).forEach(l => console.log(l.slice(0,160)));
await browser.close();
})();

46
tools/e2e/main.dart.js Normal file
View File

@@ -0,0 +1,46 @@
"use strict";
// A map containing the URLs for the bootstrap scripts in debug.
let _scriptUrls = {
"mapper": "stack_trace_mapper.js",
"requireJs": "require.js"
};
// Create a TrustedTypes policy so we can attach Scripts...
let _ttPolicy;
if (window.trustedTypes) {
_ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", {
createScriptURL: (url) => {
let scriptUrl = _scriptUrls[url];
if (!scriptUrl) {
console.error("Unknown Flutter Web bootstrap resource!", url);
}
return scriptUrl;
}
});
}
// Creates a TrustedScriptURL for a given `scriptName`.
// See `_scriptUrls` and `_ttPolicy` above.
function getTTScriptUrl(scriptName) {
let defaultUrl = _scriptUrls[scriptName];
return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl;
}
// Attach source mapping.
var mapperEl = document.createElement("script");
mapperEl.defer = true;
mapperEl.async = false;
mapperEl.src = getTTScriptUrl("mapper");
document.head.appendChild(mapperEl);
// Attach require JS.
var requireEl = document.createElement("script");
requireEl.defer = true;
requireEl.async = false;
requireEl.src = getTTScriptUrl("requireJs");
// This attribute tells require JS what to load as main (defined below).
requireEl.setAttribute("data-main", "main_module.bootstrap");
document.head.appendChild(requireEl);

View File

@@ -9,7 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright": "^1.62.1"
"playwright": "^1.62.1",
"source-map": "^0.8.0"
}
},
"node_modules/fsevents": {
@@ -55,6 +56,15 @@
"engines": {
"node": ">=20"
}
},
"node_modules/source-map": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz",
"integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
}
}
}

View File

@@ -11,6 +11,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"playwright": "^1.62.1"
"playwright": "^1.62.1",
"source-map": "^0.8.0"
}
}

15
tools/e2e/parse_map.js Normal file
View File

@@ -0,0 +1,15 @@
const fs = require('fs');
const { SourceMapConsumer } = require('source-map');
(async () => {
const mapJson = await (await fetch('http://127.0.0.1:8099/main.dart.js.map')).text().catch(()=>null);
if (!mapJson) { console.log('无 sourcemap'); return; }
const smc = await new SourceMapConsumer(mapJson);
// 解析 299:254 (line:col)
const pos = smc.originalPositionFor({ line: 299, column: 254 });
console.log('299:254 →', pos.source, pos.line + ':' + pos.column, '|', pos.name);
// 也试 299 的前后几行
for (const c of [200, 100, 50]) {
const p = smc.originalPositionFor({ line: 299, column: c });
if (p.source) console.log(`299:${c}`, p.source.split('/').slice(-2).join('/') + ':' + p.line);
}
})();