release: v0.2.2 - cross-platform paths, health version, update-check hardening, clean packaging

This commit is contained in:
Aisland
2026-08-25 02:14:02 +08:00
parent d71ac54bb9
commit b6a96405e8
17 changed files with 369 additions and 305 deletions
+6 -17
View File
@@ -474,8 +474,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
var out={};
try{
var st=window.__dshWorktable.splitStore;
var probe=await fetch('/api/worktable/write',{method:'POST',headers:{'content-type':'application/json'},body:'{}'}).catch(function(){return null;});
out.writeRouteReady=!!(probe&&probe.status===400);
var probe=await fetch('/api/worktable/write',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:'C:/Users/SJL/AppData/Local/Temp/wt-edit-test.md',content:'# ORIG'})}).catch(function(){return null;});
out.writeRouteReady=!!(probe&&probe.status===200);
if(out.writeRouteReady){
st.open({id:'t-mdedit',title:'md',top:null,main:[{id:'m1',title:'m',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
await new Promise(function(r){setTimeout(r,400)});
@@ -708,28 +708,17 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 还原临时夹具
try { fs.writeFileSync(TEMP_MD, '# ORIG\n', 'utf8') } catch {}
const errors = events.filter((e) => {
const ent = e.method === 'Log.entryAdded' ? e.params.entry : null;
const txt = ent ? (ent.text || '') + '|' + (ent.url || '') : '';
if (txt.indexOf('/api/worktable/write') >= 0) return false;
return true;
}).filter((e) => {
// 服务端新路由(site/write/workspaces):运行中的服务器未重启前其 404 属预期(重启后移除本过滤)
const ent = e.method === 'Log.entryAdded' ? e.params.entry : null;
const txt = ent ? (ent.text || '') + '|' + (ent.url || '') : '';
if (txt.indexOf('/api/worktable/site') >= 0) return false;
if (txt.indexOf('/api/worktable/workspaces') >= 0) return false;
return true;
}).filter((e) =>
// 严格门禁:不再掩盖任何插件路由错误(site/write/workspaces 曾因旧服务器未重启被过滤,已移除)
const errors = events.filter((e) =>
e.method === 'Runtime.exceptionThrown' ||
(e.method === 'Log.entryAdded' && e.params.entry.level === 'error') ||
(e.method === 'Runtime.consoleAPICalled' && e.params.type === 'error'),
).slice(0, 10).map((e) => e.method === 'Runtime.exceptionThrown'
? 'EXCEPTION: ' + ((e.params.exceptionDetails.exception || {}).description || e.params.exceptionDetails.text)
: 'LOG: ' + (e.params.entry ? e.params.entry.text : '').slice(0, 200));
: 'LOG: ' + (e.params.entry ? e.params.entry.text : '').slice(0, 200) + (e.params.entry && e.params.entry.url ? ' @ ' + e.params.entry.url : ''));
console.log('ERRORS_COUNT:', errors.length);
errors.forEach((x) => console.log(x));
ws.close();
proc.kill();
process.exit(0);
process.exit(errors.length === 0 ? 0 : 1); // 严格门禁:发现错误即非零退出
})().catch((e) => { console.log('SCRIPT_FAIL:', e); proc.kill(); process.exit(1); });
+55
View File
@@ -0,0 +1,55 @@
// pathutil-matrix.cjs — pathutil 路径矩阵定向验证(用 esbuild 把 TS 编译成 CJS 后实测)
const path = require('path');
const os = require('os');
const fs = require('fs');
const { buildSync } = require('../01_content/node_modules/esbuild');
const out = path.join(os.tmpdir(), 'wt-pathutil-' + process.pid + '.cjs');
buildSync({
entryPoints: [path.join(__dirname, '..', '01_content', 'src', 'client', 'pathutil.ts')],
outfile: out,
bundle: true,
platform: 'node',
format: 'cjs',
logLevel: 'silent',
});
const u = require(out);
fs.unlinkSync(out);
let fails = 0;
const eq = (label, got, want) => {
if (got === want) { console.log('PASS ' + label + ' = ' + JSON.stringify(got)); }
else { fails++; console.log('FAIL ' + label + ': got ' + JSON.stringify(got) + ' want ' + JSON.stringify(want)); }
};
// joinPath:Windows 反斜杠风格 / Windows 正斜杠风格 / POSIX / 根目录 / 盘符根 / UNC / 相对
eq("join C:\\base+rel", u.joinPath('C:\\base', 'rel'), 'C:\\base\\rel');
eq("join C:/base+rel", u.joinPath('C:/base', 'rel'), 'C:/base/rel');
eq("join /Users+project", u.joinPath('/Users/name', 'project'), '/Users/name/project');
eq("join /+project", u.joinPath('/', 'project'), '/project');
eq("join C:\\+project", u.joinPath('C:\\', 'project'), 'C:\\project');
eq("join UNC+x", u.joinPath('\\\\server\\share', 'x'), '\\\\server\\share\\x');
eq("join rel+rel", u.joinPath('a/b', 'c'), 'a/b/c');
eq("join trim seps", u.joinPath('C:\\base\\', '\\rel\\'), 'C:\\base\\rel');
// isAbs
eq("isAbs C:\\x", u.isAbs('C:\\x'), true);
eq("isAbs C:/x", u.isAbs('C:/x'), true);
eq("isAbs /x", u.isAbs('/x'), true);
eq("isAbs UNC", u.isAbs('\\\\server\\share'), true);
eq("isAbs rel", u.isAbs('rel/x'), false);
// parentPathOf:POSIX 根 / 盘符根 / 根自身 / UNC / 相对
eq("parent /foo", u.parentPathOf('/foo'), '/');
eq("parent C:\\foo", u.parentPathOf('C:\\foo'), 'C:\\');
eq("parent C:\\", u.parentPathOf('C:\\'), 'C:\\');
eq("parent /", u.parentPathOf('/'), '/');
eq("parent UNC/x", u.parentPathOf('\\\\server\\share\\x'), '\\\\server\\share');
eq("parent a/b", u.parentPathOf('a/b'), 'a');
// basenameOf
eq("base a/b/c.html", u.basenameOf('a/b/c.html'), 'c.html');
eq("base C:\\x\\y.txt", u.basenameOf('C:\\x\\y.txt'), 'y.txt');
console.log(fails === 0 ? 'MATRIX ALL PASS' : 'MATRIX FAILS: ' + fails);
process.exit(fails === 0 ? 0 : 1);
+117
View File
@@ -0,0 +1,117 @@
// probe-update-scenarios.cjs — 更新检查五场景定向验证(CDP Fetch 拦截;fulfill 带 CORS 头)
const http = require('http');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const PORT = 9398;
const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
'--window-size=1200,800', '--force-device-scale-factor=1',
'--remote-debugging-port=' + PORT,
'--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-updscn2',
'about:blank',
], { stdio: 'ignore' });
const getJSON = (p) => new Promise((res, rej) => {
http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d = ''; r.on('data', c => d += c); r.on('end', () => res(JSON.parse(d))); }).on('error', rej);
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const OK_BODY = Buffer.from(JSON.stringify({ tag_name: 'v0.1.0', body: 'scenario test', html_url: 'https://example.com/x' })).toString('base64');
const FAIL_BODY = Buffer.from('{"message":"boom"}').toString('base64');
const HI_BODY = Buffer.from(JSON.stringify({ tag_name: 'v0.2.3', body: 'v0.2.3 release notes', html_url: 'https://example.com/hi' })).toString('base64');
const HDRS = [{ name: 'Content-Type', value: 'application/json' }, { name: 'Access-Control-Allow-Origin', value: '*' }];
let mode = 'success';
let reqCount = 0;
let fulfillCount = 0;
(async () => {
let list = null;
for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); }
const target = list.find((t) => t.type === 'page') || list[0];
const ws = new WebSocket(target.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
ws.on('message', (raw) => {
const m = JSON.parse(String(raw));
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
else if (m.method === 'Fetch.requestPaused') {
reqCount++;
const rid = m.params.requestId;
if (mode === 'pending') {
setTimeout(() => { try { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.continueRequest', params: { requestId: rid } })); } catch {} }, 9000);
} else if (mode === 'fail500') {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 500, responseHeaders: HDRS, body: FAIL_BODY } }));
} else if (mode === 'twothenok') {
if (fulfillCount < 2) { fulfillCount++; ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 500, responseHeaders: HDRS, body: FAIL_BODY } })); }
else { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } })); }
} else if (mode === 'slowok') {
setTimeout(() => { try { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } })); } catch {} }, 2500);
} else if (mode === 'hiok') {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: HI_BODY } }));
} else {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } }));
}
}
});
await new Promise((r) => ws.on('open', r));
await send('Runtime.enable');
await send('Page.enable');
await send('Fetch.enable', { patterns: [{ urlPattern: '*api.github.com*' }] });
const evaluate = async (expression) => {
const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
if (r.result && r.result.exceptionDetails) return { __error: String(r.result.exceptionDetails.text) };
return r.result && r.result.result ? r.result.result.value : undefined;
};
const rowSnap = () => evaluate("(function(){ var r=document.querySelector('.dsh-wt_versionRow'); if(!r) return null; var btn=Array.from(r.querySelectorAll('button')).find(function(b){ return b.textContent.indexOf('检查')>=0 || b.textContent.indexOf('Check')>=0; }); return { text: r.innerText.replace(/\\s+/g,' '), disabled: btn ? btn.disabled : null, label: btn ? btn.textContent.trim() : null }; })()");
const openSettings = async () => {
await evaluate("(function(){ var b=document.querySelector('button[title=\"设置\"]')||document.querySelector('button[title=\"Settings\"]'); if(b) b.click(); })()");
await sleep(500);
};
const clickCheck = async () => {
await evaluate("(function(){ var r=document.querySelector('.dsh-wt_versionRow'); if(!r) return; var btn=Array.from(r.querySelectorAll('button')).find(function(b){ return b.textContent.indexOf('检查')>=0 || b.textContent.indexOf('Check')>=0; }); if(btn) btn.click(); })()");
};
async function scenario(name, opts) {
mode = opts.mode; fulfillCount = 0;
// 预热加载:让首屏自动检查在计数前跑完
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
await sleep(6000);
const rail = await evaluate("!!document.querySelector('.dsh-wt_rail')");
if (rail) { await evaluate("(function(){ var b=document.querySelector('button.hHd-Xa_toggle'); if(b) b.click(); })()"); await sleep(1500); }
await evaluate("localStorage.setItem('dsh.worktable.lastUpdateCheck.v1', String(Date.now())); localStorage.removeItem('dsh.worktable.skipVersion.v1');");
reqCount = 0;
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
await sleep(6000);
const rail2 = await evaluate("!!document.querySelector('.dsh-wt_rail')");
if (rail2) { await evaluate("(function(){ var b=document.querySelector('button.hHd-Xa_toggle'); if(b) b.click(); })()"); await sleep(1500); }
await openSettings();
const before = await rowSnap();
await clickCheck();
if (opts.reentry) { for (let i = 0; i < 4; i++) { await clickCheck(); } }
await sleep(Math.min(900, opts.waitMs));
const mid = await rowSnap();
await sleep(opts.waitMs);
const end = await rowSnap();
console.log('SCENARIO[' + name + '] requests=' + reqCount);
console.log(' mid:', JSON.stringify(mid));
console.log(' end:', JSON.stringify(end));
return { before, mid, end, requests: reqCount };
}
const results = [];
results.push(['1-success', await scenario('1-success', { mode: 'success', waitMs: 4000 })]);
results.push(['2-pending-timeout', await scenario('2-pending-timeout', { mode: 'pending', waitMs: 26000 })]);
results.push(['3-twothenok', await scenario('3-twothenok', { mode: 'twothenok', waitMs: 5000 })]);
results.push(['4-fail-all', await scenario('4-fail-all', { mode: 'fail500', waitMs: 4000 })]);
results.push(['5-reentry', await scenario('5-reentry', { mode: 'slowok', waitMs: 4000, reentry: true })]);
results.push(['6-higher-version', await scenario('6-higher-version', { mode: 'hiok', waitMs: 4000 })]);
const hiState = await evaluate("(function(){ var b=document.querySelector('.dsh-wt_updateBadge'); if(!b) return {badge:false}; b.click(); return {badge:true}; })()");
await sleep(700);
const hiCard = await evaluate("(function(){ var c=document.querySelector('.dsh-wt_updateCard'); if(!c) return {card:false}; return {card:true, text:c.innerText.replace(/\\s+/g,' ')}; })()");
console.log('HI_BADGE:', JSON.stringify(hiState), 'HI_CARD:', JSON.stringify(hiCard));
let pass = true;
const ok = (cond, label) => { if (!cond) { pass = false; console.log(' ❌ ' + label); } else { console.log(' ✓ ' + label); } };
const [, s1] = results[0]; ok(s1.requests >= 1 && /已是最新版本|Up to date/.test(s1.end.text || ''), '成功 → 已是最新版本');
const [, s2] = results[1]; ok(s2.requests === 3 && /上次检查未成功|Last check failed/.test(s2.end.text || ''), 'pending×3 超时 → 失败提示'); ok(s2.mid && s2.mid.disabled === true && /检查中|Checking/.test(s2.mid.label || ''), '检查中按钮禁用+文案');
const [, s3] = results[2]; ok(s3.requests === 3 && /已是最新版本|Up to date/.test(s3.end.text || ''), '两败一成 → 已是最新版本');
const [, s4] = results[3]; ok(s4.requests === 3 && /上次检查未成功|Last check failed/.test(s4.end.text || ''), '三败 → 失败提示');
const [, s5] = results[4]; ok(s5.requests === 1, '慢响应期间连点 5 次仅 1 个 in-flight(防重入)'); ok(s5.mid && s5.mid.disabled === true, '请求进行中按钮保持禁用');
const [, s6] = results[5]; ok(s6.requests >= 1 && hiState && hiState.badge === true && hiCard && hiCard.card === true && /v0\.2\.3/.test(hiCard.text || ''), '更高版本 → 徽标亮起 + 更新卡显示 v0.2.3');
console.log(pass ? 'ALL SCENARIOS PASS' : 'SCENARIO FAILURES PRESENT');
ws.close(); proc.kill(); process.exit(pass ? 0 : 1);
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} process.exit(1); });