Files
OpenClaw Agent 2329955e32 baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。

此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。

当前状态(提交前已实测):
- index.html 内联薪酬算法已删除,改调 WHTPayroll
- shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用
- 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装

未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、
运行时 sqlite 数据与日志、时间戳备份、.trash。
2026-09-05 14:35:00 +08:00

104 lines
4.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 实测:点击右下角「设置」Tab -> 设置抽屉是否弹出
const { spawn } = require('child_process');
const fs = require('fs');
const CHROME = '/usr/bin/chromium';
const OUT_DIR = __dirname;
const URL = 'http://127.0.0.1:9522/index.html';
const chrome = spawn(CHROME, [
'--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
'--remote-debugging-port=9226', `--user-data-dir=${OUT_DIR}/.chrome-test2`,
'--window-size=420,900','about:blank'
], { stdio: ['ignore','ignore','pipe'] });
const stderr = [];
chrome.stderr.on('data', d => stderr.push(d.toString()));
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function getWsUrl() {
for (let i = 0; i < 40; i++) {
try {
const tabs = await (await fetch('http://127.0.0.1:9226/json/list')).json();
if (tabs.length) return tabs[0].webSocketDebuggerUrl;
} catch (e) {}
await sleep(250);
}
throw new Error('CDP 连接失败: ' + stderr.join('').slice(-400));
}
(async () => {
try {
const wsUrl = await getWsUrl();
const ws = new WebSocket(wsUrl);
let id = 0; const pending = new Map(); const logs = [];
let openRes; const openP = new Promise(r => openRes = r);
ws.onopen = openRes;
ws.onmessage = ev => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
else if (m.method === 'Runtime.consoleAPICalled') logs.push('C ' + (m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
else if (m.method === 'Runtime.exceptionThrown') logs.push('EXC ' + JSON.stringify(m.params.exceptionDetails));
};
await openP;
const send = (method, params={}) => new Promise(res => { const mid=++id; pending.set(mid,res); ws.send(JSON.stringify({id:mid,method,params})); });
const ev = async (expr) => {
const r = await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
if (r.result && r.result.exceptionDetails) return { __err: JSON.stringify(r.result.exceptionDetails) };
return r.result && r.result.result && r.result.result.value;
};
await send('Runtime.enable'); await send('Page.enable');
await send('Page.navigate', { url: URL }); await sleep(2200);
// 定位底部 tab 按钮(文字为「设置」的 <button>)
const probe = await ev(`
(function(){
const btns = Array.from(document.querySelectorAll('nav button'));
const info = btns.map(b => ({ t: (b.innerText||'').trim(), rect: JSON.stringify({x:b.getBoundingClientRect().left,y:b.getBoundingClientRect().top,w:b.getBoundingClientRect().width,h:b.getBoundingClientRect().height}), htmlOk: b.outerHTML.slice(0,100) }));
// 命中最上层元素(判断按钮是否被其他元素遮挡)
const sBtn = btns.find(b => (b.innerText||'').trim() === '设置');
let hitTop = null;
if (sBtn) {
const r = sBtn.getBoundingClientRect();
const cx = r.left + r.width/2, cy = r.top + r.height/2;
const topEl = document.elementFromPoint(cx, cy);
hitTop = topEl ? { tag: topEl.tagName, cls: topEl.className && String(topEl.className).slice(0,80) } : null;
}
return { navBtnCount: btns.length, btns: info, settingsBtnHitTest: hitTop };
})()
`);
console.log('底部导航按钮:', JSON.stringify(probe, null, 2));
// 点击「设置」tab
const clickSetup = await ev(`
new Promise(r => {
const btn = Array.from(document.querySelectorAll('nav button')).find(b => (b.innerText||'').trim() === '设置');
if (!btn) return r({ err: 'no settings btn' });
btn.click();
setTimeout(() => {
const z50 = document.querySelectorAll('.z-50').length;
const overlay = document.querySelector('.fixed.inset-0.z-50');
r({
z50,
overlayPresent: !!overlay,
overlayText: overlay ? (overlay.innerText||'').replace(/\\s+/g,' ').slice(0,60) : null,
bodyHasJiangchou: document.body.innerText.includes('薪酬规则')
});
}, 450);
})
`);
console.log('点击设置 Tab 后:', JSON.stringify(clickSetup, null, 2));
const shot = await send('Page.captureScreenshot', { format: 'png' });
fs.writeFileSync(OUT_DIR + '/tab_settings_click.png', Buffer.from(shot.result.data, 'base64'));
console.log('截图 tab_settings_click.png');
console.log('\n=== 控制台 ===');
console.log(logs.length ? logs.join('\n') : '(无)');
ws.close(); chrome.kill(); process.exit(0);
} catch (e) {
console.error('FAILED:', e.message); console.error(stderr.join('').slice(-400));
try { chrome.kill(); } catch(_){} process.exit(1);
}
})();