Files
admin 19101acd65 init: workbuddy_skills 重建,仅收录 session-mechanism
- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件)
- 附 .gitignore(产物 + 本机凭据)
- 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库)
- 本提交为孤儿提交(父提交为空),历史自此重新开始
2026-10-05 14:13:24 +08:00

236 lines
14 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.
// 真跑 render():把 board.html 的页面脚本放进 vm 的**全局作用域**执行,
// 这样顶层 `function render(){}` 会挂到 sandbox 对象上,能真调到。
// ⛔ 上次用 `new Function()` 是假绿 —— 函数关在局部,取到 undefined,等于没跑。
const fs = require('fs'), http = require('http'), vm = require('vm');
const path = require('path');
const P = process.env.BOARD_HTML || path.join(__dirname, 'board.html');
const SNAP_URL = process.env.BOARD_SNAP || 'http://127.0.0.1:20099/board.json';
// 🔴 2026-10-05:支持**盘上快照**(自测/离线用)—— ⛔ 不必起服务,⛔ 不依赖网络。
const SNAP_FILE = process.env.BOARD_SNAP_FILE || '';
// 分母口径断言可被外部指定(自测会传期望值;不传则只报数不判)
const WANT_PASS = process.env.BOARD_WANT_PASS || '';
const html = fs.readFileSync(P, 'utf8');
const blocks = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map(m => m[1]);
const code = blocks.join('\n;\n');
const els = {};
function mkEl(id) {
const e = {
id, hidden: false, textContent: '', innerHTML: '', style: {}, children: [],
dataset: {}, // ← 页面用 dataset.base 存静态文案
classList: { toggle(){}, add(){}, remove(){}, contains(){ return false; } },
appendChild(c){ this.children.push(c); return c; },
insertBefore(c){ this.children.push(c); return c; },
setAttribute(){}, getAttribute(){ return null; }, addEventListener(){},
removeAttribute(){}, focus(){}, blur(){}, click(){},
querySelector(){ return null; }, querySelectorAll(){ return []; },
getBoundingClientRect(){ return {width:900,height:500,top:0,left:0}; },
getContext(){ const noop=()=>{};
return {clearRect:noop,fillRect:noop,beginPath:noop,moveTo:noop,lineTo:noop,
stroke:noop,fill:noop,arc:noop,fillText:noop,save:noop,restore:noop,
translate:noop,scale:noop,closePath:noop,setLineDash:noop,bezierCurveTo:noop,
measureText:()=>({width:10})}; },
parentNode: null, offsetWidth: 900, offsetHeight: 500, offsetTop: 0,
};
return e;
}
const document = {
getElementById(id){ return els[id] || (els[id] = mkEl(id)); },
createElement(t){ return mkEl('__'+t); },
createElementNS(){ return mkEl('__ns'); },
querySelector(){ return null; }, querySelectorAll(){ return []; },
addEventListener(){}, removeEventListener(){},
body: mkEl('body'), documentElement: mkEl('html'), head: mkEl('head'),
createTextNode(t){ return {textContent:t}; },
};
let rafQ = [];
const sandbox = {
document, console,
window: null,
navigator: { userAgent: 'node' },
location: { protocol: 'http:', hash: '', search: '', href: 'http://127.0.0.1:20099/' },
requestAnimationFrame: fn => { rafQ.push(fn); return rafQ.length; },
cancelAnimationFrame: () => {},
setTimeout: () => 0, // ⛔ 不开轮询
clearTimeout: () => {},
setInterval: () => 0,
clearInterval: () => {},
fetch: () => Promise.reject(new Error('probe: fetch disabled')),
localStorage: { getItem(){return null;}, setItem(){}, removeItem(){} },
matchMedia: () => ({ matches:false, addEventListener(){} }),
addEventListener(){}, removeEventListener(){},
alert(){}, getComputedStyle: () => ({ getPropertyValue: () => '' }),
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
sandbox.self = sandbox;
let bootErr = null;
try {
vm.createContext(sandbox);
vm.runInContext(code, sandbox, { filename: 'board.html', timeout: 15000 });
} catch (e) { bootErr = e; }
console.log('脚本装载 :', bootErr ? ('❌ ' + bootErr.message) : '✅ OK');
console.log('render 类型 :', typeof sandbox.render);
console.log('renderProject :', typeof sandbox.renderProject);
console.log('paintScope :', typeof sandbox.paintScope);
console.log('');
function run(snap) {
console.log('快照顶层键数 :', Object.keys(snap).length);
console.log('顶层有 acc? :', Object.prototype.hasOwnProperty.call(snap,'acc'));
console.log('goal.acceptance 条数:', Object.keys((snap.goal||{}).acceptance||{}).length);
console.log('');
let err=null, out='';
// 清掉桩里 fetch reject 留下的残留(那是**测试桩**造成的,⛔ 不是页面 bug)
if (els['err']) els['err'].textContent = '';
try {
const rp = sandbox.renderProject || sandbox.render;
out = rp(snap) || ''; // ← 原来炸的就是这条
} catch(e){ err = e; }
const errEl = els['err'];
// 🔴 renderProject 直接写 `#proj`(⛔ 不是返回值、⛔ 也不是 #project)
// ⚠️ 2026-10-05:**目标 id / 归属判据 / 工作区**那几项落在**右上角 `?` 的 `#projTip`** 里,
// ⛔ 不在 `#proj` ⇒ 只读 `#proj` 会漏掉它们(实测"目标 id 没渲染"就是这么假红的)。
const _tip = (els['projTip'] && els['projTip'].innerHTML) || '';
const outHtml = (out || (els['proj'] ? (els['proj'].innerHTML||'') : '')) + '\n' + _tip;
console.log('render() 抛异常:', err ? ('❌ ' + err.message) : '✅ 无');
if (err) console.log(' 位置:', (err.stack||'').split('\n')[1]);
console.log('#err 文案 :', errEl && errEl.textContent ? ('❌ ' + errEl.textContent.slice(0,160)) : '✅ 空(未报错)');
console.log('#proj 产物长度 :', outHtml.length);
console.log('产物含 accRow :', outHtml.indexOf('accRow') >= 0 ? '✅' : '❌');
console.log('产物含 完成情况:', outHtml.indexOf('完成情况') >= 0 ? '✅' : '❌');
const m = outHtml.match(/(\d+)\s*(?:\/|/)\s*(\d+)\s*通过/);
console.log('通过率数值 :', m ? m[0] : '(未渲染出)');
/* 🔴🔴 2026-10-05 加:**静默错文案**这一族(⛔ 不抛异常,只画错值 ⇒ 上面那些断言全绿漏过)。
用户报障逐字:「看板中目标还是空的 本项目 目标(未声明主题)(未声明目标)」。
真因=目标数据在 `d.project` / `d.goal` 下,而代码裸写 `d.short` / `d.criteria` / `d.id` …
⇒ 取到 `undefined` ⇒ **悄悄**显示兜底文案与 '—',⛔ 一句异常都不抛。
🔴 判据=**拿快照里的真值去比对渲染产物**("没炸" ≠ "画对了")。 */
let silent = 0;
['(未声明主题)', '(未声明目标)'].forEach((t) => {
if (outHtml.indexOf(t) >= 0) { console.log('兜底文案 : ❌ 出现了 ' + t + ' ⇒ 取值层级错'); silent++; }
});
if (!silent) console.log('兜底文案 : ✅ 未出现');
const _pj = snap.project || {}, _gj = snap.goal || {};
const _title = _gj.title || _pj.title;
/* 🔴🔴 2026-10-05 **「主题真值」口径已改**(用户逐字:「目标 vibe-product 提取 …
改为 vibe-product 目标:提取 …」)⇒ 目标行**不再**渲染 `goal.short`(主题简称),
改为渲染【所属工作区真实目录名 `ws_name`】+【目标全称 `goal.title`】。
⚠️ 旧断言查的是 `goal.short` ⇒ 本区格(`goal.short='本机协作'` ≠ `ws_name='ai1net-dsh-server'`)
会**假红**;而 peer 格恰好两者同字面 ⇒ **假绿**。⇒ 旧断言已失效,⛔ 不许留。
✅ 新判据=**双查**:pill 必须画出 `ws_name`,标题必须画出 `title`(各查一次,互不遮掩)。 */
const _wsn = String(snap.ws_name || '').trim();
if (_wsn) {
if (outHtml.indexOf(_wsn) >= 0) console.log('工作区真值 : ✅ 渲染出「' + _wsn + '」');
else { console.log('工作区真值 : ❌ 快照有 ws_name「' + _wsn + '」却没渲染出来'); silent++; }
} else {
console.log('工作区真值 : ⚠️ 快照没有 ws_name(后端没给?目标行 pill 会退化成「—」)');
}
if (_title) {
if (outHtml.indexOf(_title) >= 0) console.log('目标真值 : ✅ 渲染出(' + _title.slice(0, 24) + '…)');
else { console.log('目标真值 : ❌ 快照有 title 却没渲染出来'); silent++; }
}
/* 🔴🔴 2026-10-05 加:**目标行必须是「目标 [工作区] 标题」这个形态**(用户逐字:
「vibe-product 目标:提取 …」)。
⛔ 判据是**产物形态**,不是"两个值分别出现过" —— 两个值都渲染了,但顺序/标签错了
(比如 pill 跑到标题后面、或「目标」标签丢了)同样不合用户口径。
✅ 真跑:拿**真产物**,去标签后**按令牌顺序**咬住 `目标` → `ws_name` → 标题。
🔴🔴 坑(本轮实测):⛔ **不能**用 `/<\/?[^>]*>/g` 去标签 —— 标题里带 `https://…`
时,正则会把 `<` 到下一个 `>` 之间整段吃掉(`//www.tiaoyue.com/ 的设计风格…` 全没了)
⇒ `indexOf(标题)` 恒 -1 ⇒ **假红**("标题没渲染"其实渲染了)。
⇒ ✅ 只剥**本文件已知的真实标签**(`<b>`/`<span …>`/`<div …>` 之类),
⛔ 不用宽泛的 `<[^>]*>`。 */
if (_wsn && _title) {
const _flat = outHtml
.replace(/<\/?(?:b|i|u|em|strong|span|div|code|ul|li|a|br|p|small|sup|sub|svg|path|canvas)(?:\s[^<>]*)?\/?>/gi, ' ')
.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
.replace(/\s+/g, '');
const _needle = _title.replace(/\s+/g, '');
const _iLead = _flat.indexOf('目标'), _iWs = _flat.indexOf(_wsn), _iTi = _flat.indexOf(_needle);
if (_iLead >= 0 && _iWs > _iLead && _iTi > _iWs) {
console.log('目标行形态 : ✅ 目标 → ' + _wsn + ' → 标题(顺序正确)');
} else {
console.log('目标行形态 : ❌ 期望「目标 → 工作区 → 标题」,实得 idx(目标)=' + _iLead
+ ' idx(工作区)=' + _iWs + ' idx(标题)=' + _iTi);
silent++;
}
}
if (_pj.id) {
if (outHtml.indexOf(String(_pj.id)) >= 0) console.log('目标 id : ✅ ' + _pj.id);
else { console.log('目标 id : ❌ 没渲染(criteria/id 取错层级)'); silent++; }
}
/* 🔴🔴 2026-10-05 加:**「任务类别」整行必须不在版面上**(用户逐字:
「任务类别(声明任务类别)这个没用就删除 看着烦」)。
⚠️ 判据是「**版面上没有**」而不是「函数里没有」—— 所以拿 `renderProject()` 的**真产物**判,
⛔ 不 grep 源码(源码里 `'任务类别'` 字样还在:`?` 提示里那句「任务类别的来源」**必须保留**)。
✅ 同时钉住「`?` 提示里那条来源仍在」⇒ 防**删过头**(把该留的信息一起删了)。 */
let dashOk = true;
if (outHtml.indexOf('>任务类别<') >= 0 || outHtml.indexOf('(未声明任务类别)') >= 0
|| outHtml.indexOf('class="topics"') >= 0) {
console.log('任务类别行 : ❌ 版面上还有(用户已要求删除)');
dashOk = false;
} else {
console.log('任务类别行 : ✅ 版面已无该行(按用户口径删除)');
}
if (_tip.indexOf('任务类别的来源') >= 0) {
console.log('类别信息留存 : ✅ `?` 提示里仍有「任务类别的来源」(⛔ 没删过头)');
} else {
console.log('类别信息留存 : ❌ `?` 提示里也没有了 ⇒ 删过头(类别来源无处可查)');
dashOk = false;
}
/* 🔴🔴🔴 2026-10-05 加:**主会话 chip 不许画成「⚠ 无主会话」**(用户报障逐字:
「peer 工作区的主会话还是找不到对应的吗」)。
⛔ 判据不是"源码里有没有这个词" —— 源码**必然**有那句兜底文案(`board.html:1353`),
所以只能查**渲染产物**:快照的 `project.main_by_topic` 有值 ⇒ 产物里就该有 id8;
只有**取不到**时才允许出现「无主会话」。
✅ 双向断言:**快照有值 ⇒ 产物不许说"无主会话"**;**快照确实空 ⇒ 必须说"无主会话"**
(⛔ 否则把兜底整个删掉也能"绿",那是假绿)。 */
const _mbt = ((snap.project || {}).main_by_topic) || {};
const _hasMain = Object.keys(_mbt).some(k => {
const v = _mbt[k];
return (typeof v === 'object' && v && v.sid) || (typeof v === 'string' && v);
});
const _said = outHtml.indexOf('无主会话') >= 0;
if (_hasMain && _said) {
console.log('主会话渲染 : ❌ 快照有 `main_by_topic` 却仍画「无主会话」');
dashOk = false;
} else if (!_hasMain && !_said) {
console.log('主会话渲染 : ❌ 快照无主会话,产物也没提「无主会话」(兜底被删?)');
dashOk = false;
} else {
console.log('主会话渲染 : ✅ ' + (_hasMain ? '快照有主会话 ⇒ 产物未画兜底文案'
: '快照确无主会话 ⇒ 如实画了兜底文案'));
}
console.log('静默错文案 :', silent ? ('❌ ' + silent + ' 项') : '✅ 无');
let ok = !err && !(errEl && errEl.textContent)
&& outHtml.indexOf('accRow') >= 0 && outHtml.indexOf('完成情况') >= 0
&& (typeof sandbox.renderProject==='function')
&& silent === 0 && dashOk;
if (WANT_PASS && (!m || m[0].replace(/\s/g,'') !== WANT_PASS.replace(/\s/g,''))) {
console.log('期望通过率 : ❌ 应得 ' + WANT_PASS + ',实得 ' + (m?m[0]:'—'));
ok = false;
} else if (WANT_PASS) {
console.log('期望通过率 : ✅ ' + WANT_PASS);
}
console.log('\n' + (ok ? '✅ 真跑通了(render 真被调用、零异常、#proj 真渲染出完成情况、无静默错文案)' : '❌ 仍有问题'));
process.exit(ok ? 0 : 1);
}
// 快照来源两选一:盘上文件(自测/离线)优先,否则打活服务。
if (SNAP_FILE) {
let raw;
try { raw = fs.readFileSync(SNAP_FILE, 'utf8'); }
catch (e) { console.log('读盘上快照失败:', e.message); process.exit(1); }
run(JSON.parse(raw));
} else {
http.get(SNAP_URL, res => {
let buf = ''; res.on('data', c => buf += c);
res.on('end', () => { run(JSON.parse(buf)); });
}).on('error', e => { console.log('取快照失败:', e.message); process.exit(1); });
}