chore(website-demo): 新增 mock 构建/校验/本地预览工具脚本

- build-mocks.cjs:从真实对话导出演示 mock 数据
- check-mocks.cjs:mock 数据完整性校验
- rebuild-cat-html.cjs / serve.cjs:分类页重建与本地预览服务
This commit is contained in:
JOJO 2026-09-03 21:54:58 +08:00
parent bd2cf0f56d
commit 2f0b970060
4 changed files with 585 additions and 0 deletions

View File

@ -0,0 +1,411 @@
#!/usr/bin/env node
/**
* 官网演示 mock 构建脚本一次性含人工手术逻辑
* 输入用户真实对话 JSON~/.astrion
* 输出website-design/exp/mock/ 下的 bootstrap/列表/activity/媒体 mock
* 手术内容
* - word vlm_analyze失败+ send_qq_file私人工具+ 预览图生成段改标题/首尾文案todo 4 项改 3
* - cat 3 sleep 超时等待含对应思考文案续接其余不动
* - 全量路径改写 /Users/jojo/... /Users/demo/...剥离 edit_summary
*/
const fs = require('fs');
const path = require('path');
const EXP = path.join(__dirname, '..', 'exp');
const MOCK = path.join(EXP, 'mock');
const MEDIA_OUT = path.join(MOCK, 'media');
const MEDIA_STORE = '/Users/jojo/.astrion/astrion/host/data/conversations/media_store';
const SUB_TASKS_DIR = '/Users/jojo/.astrion/astrion/host/data/sub_agent_tasks';
const CONVS = {
car: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-6/conv_20260903_003950_896.json',
id: 'conv_20260903_003950_896', oldBase: '/Users/jojo/Desktop/游戏', newBase: '/Users/demo/daily', ma: false,
model: 'DeepSeek-V4-Flash', // 真实模型(演示站修正用户库里的 Flah 笔误)
},
word: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-3/conv_20260825_210916_394.json',
id: 'conv_20260825_210916_394', oldBase: '/Users/jojo/Desktop/整理资料', newBase: '/Users/demo/daily', ma: false,
title: '整理周会速记为 Word',
},
plane: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-8/conv_20260825_222209_771.json',
id: 'conv_20260825_222209_771', oldBase: '/Users/jojo/Desktop/个人网站', newBase: '/Users/demo/starraid', ma: false,
},
cat: {
src: '/Users/jojo/.astrion/astrion/host/mutiagents/conversations/workspace/conv_20260902_235153_495.json',
id: 'conv_20260902_235153_495', oldBase: '/Users/jojo/Desktop/语音', newBase: '/Users/demo/dev', ma: true,
},
};
// ── 通用工具 ──────────────────────────────────────────────
function makeSanitizer(oldBase, newBase) {
return function sanitize(s) {
if (typeof s !== 'string') return s;
return s
.split(oldBase).join(newBase)
.replace(/\/Users\/jojo/g, '/Users/demo')
.replace(/\bjojo\b/g, 'demo');
};
}
function deepSanitize(v, san) {
if (typeof v === 'string') return san(v);
if (Array.isArray(v)) return v.map((x) => deepSanitize(x, san));
if (v && typeof v === 'object') {
const o = {};
for (const [k, val] of Object.entries(v)) o[k] = deepSanitize(val, san);
return o;
}
return v;
}
const MSG_KEYS = ['role', 'content', 'reasoning_content', 'timestamp', 'message_id', 'metadata', 'tool_calls', 'tool_call_id', 'name'];
const META_KEYS = ['message_source', 'visibility', 'starts_work', 'work_timer', 'model_key', 'tool_payload', 'files', 'hidden', 'source', 'citations', 'runtime_injected', 'inline', 'is_auto_generated', 'auto_message_type', 'multi_agent_message', 'multi_agent_display_name', 'multi_agent_subtype', 'runtime_guidance', 'runtime_guidance_original', 'tool_image_path', 'media_refs'];
// 演示站 MODELS 只列 Kimi-K3消息里的模型名统一归一纯展示字段
const DEMO_MODEL = 'Kimi-K3';
function transformMessage(m, san, demoModel) {
const o = {};
for (const k of MSG_KEYS) if (m[k] !== undefined) o[k] = m[k];
if (o.metadata) {
const meta = {};
for (const k of META_KEYS) if (m.metadata[k] !== undefined) meta[k] = m.metadata[k];
if (meta.model_key) meta.model_key = demoModel;
o.metadata = meta;
}
return deepSanitize(o, san);
}
function replaceOnce(haystack, needle, replacement, label) {
if (!haystack || !haystack.includes(needle)) throw new Error(`手术失败:未找到文本 [${label}]`);
return haystack.split(needle).join(replacement);
}
function replaceOnceRe(haystack, re, replacement, label) {
if (!haystack || !re.test(haystack)) throw new Error(`手术失败:正则未命中 [${label}]`);
return haystack.replace(re, replacement);
}
// ── word 手术 ──────────────────────────────────────────────
function wordSurgery(msgs) {
const cut = new Set();
const idxOf = (pred, label) => {
const i = msgs.findIndex(pred);
if (i < 0) throw new Error('word: 未找到 ' + label);
return i;
};
const cutPair = (iTool, label) => {
if (msgs[iTool].role !== 'tool' || msgs[iTool - 1].role !== 'assistant') throw new Error('word: 配对异常 ' + label);
cut.add(iTool); cut.add(iTool - 1);
};
// Quick Look 缩略图生成对preview png 只服务被剪掉的 vlm
cutPair(idxOf((m) => m.role === 'tool' && typeof m.content === 'string' && m.content.includes('Quick Look thumbnails'), '缩略图结果'), '缩略图');
// ls 预览图对
cutPair(idxOf((m) => m.role === 'tool' && typeof m.content === 'string' && m.content.includes('_preview_page1.png'), '预览图ls'), '预览图ls');
// vlm_analyze 对(失败调用)
cutPair(idxOf((m) => m.role === 'tool' && m.name === 'vlm_analyze', 'vlm结果'), 'vlm');
// send_qq_file 结果(其 assistant 只摘 tool_call保留 todo 调用)
cut.add(idxOf((m) => m.role === 'tool' && m.name === 'send_qq_file', 'qq结果'));
// 收尾 todo 对("进度 4/4"
cutPair(idxOf((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 4/4'), '收尾todo'), '收尾todo');
const kept = msgs.filter((_, i) => !cut.has(i));
if (msgs.length - kept.length !== 9) throw new Error('word: 应剪 9 条,实际 ' + (msgs.length - kept.length));
// 首条用户消息去掉"然后发给我"
const u0 = kept.find((m) => m.role === 'user' && String(m.content).includes('发给我'));
u0.content = replaceOnce(u0.content, '帮我整理这份文件为word然后发给我', '帮我整理这份文件为word', '首条用户消息');
// todo_create 所在 assistant改思考 + 重写 todo 参数4 项 → 3 项,去发送)
const todoAsst = kept.find((m) => (m.tool_calls || []).some((t) => t.function.name === 'todo_create'));
todoAsst.reasoning_content = replaceOnce(todoAsst.reasoning_content,
'5. 发送给用户(用户说"发给我"——可以用 send_qq_file 发送到 QQ 群,这是用户的习惯;也可以提供下载链接)',
'5. 交付给用户(提供下载链接,并转一份 PDF 用文件卡片直接预览)', '思考-交付方式1');
todoAsst.reasoning_content = replaceOnceRe(todoAsst.reasoning_content,
/用户说"发给我",考虑到有 send_qq_file 工具[\s\S]*?同时提供下载链接。/,
'交付方式:生成 Word 后提供工作区下载链接,并转换一份 PDF 用文件卡片预览,方便直接查看。', '思考-交付方式2');
todoAsst.reasoning_content = replaceOnce(todoAsst.reasoning_content, '5. 发送\n', '5. 交付\n', '思考-交付方式3');
for (const t of todoAsst.tool_calls) {
if (t.function.name !== 'todo_create') continue;
const args = JSON.parse(t.function.arguments);
args.overview = '将周会速记整理为结构化 Word 会议纪要';
args.tasks = args.tasks.filter((x) => !x.title.includes('发送'));
if (args.tasks.length !== 3) throw new Error('word: todo 应为 3 项');
t.function.arguments = JSON.stringify(args);
}
// todo_create 结果
const todoRes = kept.find((m) => m.role === 'tool' && m.name === 'todo_create');
todoRes.content = '已创建 TODO将周会速记整理为结构化 Word 会议纪要(共 3 项)';
// 清理/交付 assistant原 vlm 失败后的那条重写思考与正文rm 命令去掉预览图
const cleanup = kept.find((m) => m.role === 'assistant' && String(m.content).includes('VLM 配置缺失'));
cleanup.reasoning_content = '三层验证已经足够:\n1. docx-js 生成成功\n2. zip 结构完整,所有 XML 可解析\n3. LibreOffice 成功渲染为 PDF233KB3页——这是最强的验证LibreOffice 能正常打开并渲染,说明文档在 Word/WPS 中也能正常打开\n\n中文字体我用了宋体/微软雅黑eastAsia这是标准做法LibreOffice 渲染成功说明字体回退正常。\n\n现在收尾\n1. 删除临时脚本 _gen_minutes.js\n2. 更新待办\n3. 交付:提供下载链接 + 文件预览\n\n先清理临时文件。';
cleanup.content = '三重验证都已通过zip 结构完整、XML 全部可解析、LibreOffice 成功渲染 3 页 PDF文档可用性已确认。清理临时文件';
for (const t of cleanup.tool_calls) {
t.function.arguments = t.function.arguments.replace('rm -f _gen_minutes.js .astrion/user_upload/_preview_page1.png &&', 'rm -f _gen_minutes.js &&');
}
// send_qq_file 所在 assistant摘掉该 tool_call保留 todo_update_task
const deliver = kept.find((m) => (m.tool_calls || []).some((t) => t.function.name === 'send_qq_file'));
deliver.tool_calls = deliver.tool_calls.filter((t) => t.function.name !== 'send_qq_file');
// 其 todo 结果文案3/4 → 全部完成 3/3
const todoRes2 = kept.find((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 3/4'));
todoRes2.content = '所有任务已完成。;进度 3/3';
// 最终总结:去掉 QQ 发送表述
const final = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('搞定!'));
final.content = replaceOnce(final.content, '搞定!文件已整理成 Word 并通过 QQ 发送,同时也保留在工作区,可随时下载:',
'搞定!文件已整理成 Word保存在工作区可随时下载', '最终总结正文');
final.reasoning_content = replaceOnce(final.reasoning_content, '文件已发送到 QQ + 提供下载链接。', '提供下载链接与文件预览。', '最终总结思考');
// todo 进度 1/4 → 1/3任务数从 4 改为 3
const todoRes1 = kept.find((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 1/4'));
if (todoRes1) todoRes1.content = String(todoRes1.content).replace('进度 1/4', '进度 1/3');
// 其余 assistant 思考中的发送/QQ 残留(首条用户消息已改,相关思考同步改)
const r2 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('可能是通过 QQ 或下载链接'));
r2.reasoning_content = replaceOnce(r2.reasoning_content, '整理成 Word 文档,然后发给他。', '整理成 Word 文档。', '思考2-开头');
r2.reasoning_content = replaceOnce(r2.reasoning_content, '4. 发送给用户(可能是通过 QQ 或下载链接)', '4. 交付给用户(提供下载链接,并转一份 PDF 预览)', '思考2-交付');
r2.reasoning_content = replaceOnceRe(r2.reasoning_content, /用户说"发给我",工作区里有 send_qq_file 工具[\s\S]*?(如果那是用户习惯的方式)。/, '交付方式:生成 Word 文件后提供下载链接,并转换一份 PDF 用文件卡片预览,方便直接查看。', '思考2-发给我');
const r8 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('验证通过后删除脚本'));
r8.reasoning_content = replaceOnce(r8.reasoning_content, '验证通过后删除脚本,然后 send_qq_file 发送 + 提供下载链接。', '验证通过后删除脚本,然后提供下载链接与文件预览。', '思考8-交付');
r8.reasoning_content = replaceOnceRe(r8.reasoning_content, /用户说"整理这份文件为word然后发给我",重点是发给他。/, '用户要的是整理成 Word。', '思考8-发给我');
const r17 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('send_qq_file 到 QQ 群'));
r17.reasoning_content = replaceOnceRe(r17.reasoning_content, /然后:\n1\. 删除临时脚本 _gen_minutes\.js\n2\. 发送给用户[\s\S]*?同时提供下载链接。/, '然后:\n1. 删除临时脚本 _gen_minutes.js\n2. 交付给用户:提供下载链接 + 文件预览卡片', '思考17-交付');
// 残留扫描(硬失败,确保清理干净)
const rest = JSON.stringify(kept);
for (const bad of ['send_qq_file', 'vlm_analyze', 'VLM', 'QQ', '发给我', '发给他', '视觉模型']) {
if (rest.includes(bad)) throw new Error('word 残留关键词: ' + bad);
}
return kept;
}
// ── plane 手术:卡片按 620px 基准设计,演示区实际渲染 580px两侧标注会被裁
// 注入等比缩放脚本(卡片内外同为 3:4按宽缩放即满填。仅改 mock不动原对话。 ──
function planeSurgery(msgs) {
const FIT = [
'<' + 'script>',
'(function () {',
" var DESIGN_W = 620;",
" var card = document.querySelector('.ps-card');",
' if (!card) return;',
' function fit() {',
' var w = document.documentElement.clientWidth;',
' var s = Math.min(1, w / DESIGN_W);',
" document.body.style.overflow = 'hidden';",
" card.style.width = DESIGN_W + 'px';",
" card.style.minHeight = (DESIGN_W / 0.75) + 'px';",
" card.style.marginLeft = ((w - DESIGN_W) / 2) + 'px';",
" card.style.transform = 'scale(' + s + ')';",
" card.style.transformOrigin = 'top center';",
" document.body.style.height = (DESIGN_W / 0.75 * s) + 'px';",
' }',
' fit();',
" window.addEventListener('resize', fit);",
'})();',
'</scr' + 'ipt>',
].join('\n');
const m = msgs.find((x) => x.role === 'assistant' && String(x.content).includes('</show_html>'));
if (!m) throw new Error('plane: 未找到 show_html 卡片');
m.content = m.content.replace('</show_html>', '\n' + FIT + '\n</show_html>');
return msgs;
}
// ── cat 手术:只剪超时等待对,续接思考 ──────────────────────
function catSurgery(msgs) {
const cut = new Set();
msgs.forEach((m, i) => {
if (m.role === 'tool' && m.name === 'sleep' && String(m.content).includes('sleep 失败')) {
const a = msgs[i - 1];
if (!a || a.role !== 'assistant' || (a.tool_calls || []).length !== 1 || a.tool_calls[0].function.name !== 'sleep') {
throw new Error('cat: 超时配对异常 @' + i);
}
cut.add(i); cut.add(i - 1);
}
});
if (cut.size !== 6) throw new Error('cat: 应剪 3 对6 条),实际 ' + cut.size);
const kept = msgs.filter((_, i) => !cut.has(i));
// 续接 1状态检查的思考原"超时了"
const s1 = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('5 分钟没有输出'));
s1.reasoning_content = replaceOnce(s1.reasoning_content, 'UI Operator_1 超时了5分钟无输出。', 'UI Operator_1 有一阵没有新输出了。', 'cat续接1');
// 续接 2第二次状态检查的思考无正文
const s2 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).startsWith('又超时了'));
s2.reasoning_content = replaceOnce(s2.reasoning_content, '又超时了。再查一下状态', '还是没有新输出。再查一下状态', 'cat续接2');
// 续接 3直接检查工作区
const s3 = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('连续超时'));
s3.reasoning_content = replaceOnce(s3.reasoning_content, '连续两次超时了。', '等了挺久还没有新动静。', 'cat续接3思考');
s3.content = replaceOnce(s3.content, '连续超时,我直接检查一下工作区', '等了一阵还没新动静,我直接检查一下工作区', 'cat续接3正文');
const rest = JSON.stringify(kept);
for (const bad of ['sleep 失败', '超时']) {
if (rest.includes(bad)) console.warn('⚠️ cat 残留关键词:', bad);
}
return kept;
}
// ── 媒体导出 ──────────────────────────────────────────────
function exportMedia(keptMsgs, san) {
fs.mkdirSync(MEDIA_OUT, { recursive: true });
const seen = new Map();
for (const m of keptMsgs) {
for (const ref of (m.metadata && m.metadata.media_refs) || []) {
if (ref && ref.sha256 && !seen.has(ref.sha256)) seen.set(ref.sha256, ref);
}
}
let n = 0;
for (const [sha, ref] of seen) {
const ext = ref.mime_type === 'image/png' ? '.png' : ref.mime_type === 'image/jpeg' ? '.jpg' : '';
if (!ext) { console.warn('⚠️ 未知媒体类型', ref.mime_type); continue; }
const src = path.join(MEDIA_STORE, 'blobs', sha.slice(0, 2), sha);
const dst = path.join(MEDIA_OUT, sha + ext);
if (fs.existsSync(src)) fs.copyFileSync(src, dst);
else if (fs.existsSync(src + ext)) fs.copyFileSync(src + ext, dst);
else { console.warn('⚠️ 媒体 blob 缺失:', sha.slice(0, 12)); continue; }
n++;
}
console.log('媒体导出:', n, '个');
}
// ── activity mock猫咪的 4 个子智能体)────────────────────
function truncateArgs(v) {
if (typeof v === 'string') return v.length > 300 ? v.slice(0, 300) + '…' : v;
if (Array.isArray(v)) return v.map(truncateArgs);
if (v && typeof v === 'object') {
const o = {};
for (const [k, val] of Object.entries(v)) o[k] = truncateArgs(val);
return o;
}
return v;
}
function buildActivityMocks(san) {
const taskIds = ['sub_1_1788364427_985302', 'sub_2_1788364429_91688a', 'sub_3_1788364431_74db3e', 'sub_4_1788364765_5d6208'];
// 多智能体实例快照activity 状态与实例列表共用的真值来源
const MA_SNAP = (JSON.parse(fs.readFileSync('/Users/jojo/.astrion/astrion/host/data/sub_agents.json', 'utf8')).multi_agent_states || {})['conv_20260902_235153_495'];
const byTask = {};
for (const inst of Object.values((MA_SNAP && MA_SNAP.agents) || {})) byTask[inst.task_id] = inst;
const list = [];
for (const tid of taskIds) {
const dir = path.join(SUB_TASKS_DIR, tid);
const entries = fs.readFileSync(path.join(dir, 'progress.jsonl'), 'utf8')
.split('\n').filter(Boolean)
.map((l) => JSON.parse(l))
.filter((e) => e.type === 'progress')
.map((e) => deepSanitize({ ...e, args: truncateArgs(e.args || {}) }, san));
// status 必须还原实例真实状态idle/terminated——写死 completed 会让前端在查看
// 详情时把多智能体实例错误显示为「已完成」(单智能体语义),轮询后才跳回。
const instStatus = (byTask[tid] && byTask[tid].status) || 'completed';
fs.writeFileSync(path.join(MOCK, `activity-${tid}.json`), JSON.stringify({ success: true, data: { status: instStatus, entries } }));
const out = JSON.parse(fs.readFileSync(path.join(dir, 'output.json'), 'utf8'));
const lastTool = entries.length ? entries[entries.length - 1].tool : null;
const ts = tid.split('_');
list.push({
task_id: tid, status: 'completed',
summary: san((out.summary || '').slice(0, 60)),
last_tool: lastTool, conversation_id: 'conv_20260902_235153_495',
created_at: Number(ts[2]) / 1000,
});
}
fs.writeFileSync(path.join(MOCK, 'sub-agents-conv_20260902_235153_495.json'), JSON.stringify({ success: true, data: list }, null, 1));
// 多智能体实例列表(/api/multiagent/active_sub_agents猫咪对话 QuickDock 子智能体窗口的数据源。
// 字段对齐 MultiAgentInstance.to_dict() + 后端接口补充的 last_tool/current_context_tokens。
const maAgents = taskIds.map((tid) => {
const inst = byTask[tid] || {};
const base = list.find((x) => x.task_id === tid) || {};
return {
agent_id: inst.agent_id ?? null,
role_id: inst.role_id || '',
display_name: inst.display_name || tid,
task_id: tid,
status: inst.status || 'idle',
summary: san(String(inst.summary || base.summary || '').slice(0, 60)),
created_at: inst.created_at || base.created_at,
last_output: san(String(inst.last_output || '').slice(0, 300)),
conversation_id: 'conv_20260902_235153_495',
last_tool: base.last_tool || null,
current_context_tokens: 0,
};
});
fs.writeFileSync(path.join(MOCK, 'ma-agents-conv_20260902_235153_495.json'), JSON.stringify({ success: true, agents: maAgents }, null, 1));
console.log('activity mock: 4 个子智能体已导出');
}
// ── 主流程 ──────────────────────────────────────────────
const RUNNING = { has_running_background_commands: false, has_running_multi_agent: false, has_running_sub_agents: false, is_main_running: false, is_truly_active: false, main_task_id: null, main_task_type: null };
const listEntries = { normal: [], ma: [] };
for (const [name, cfg] of Object.entries(CONVS)) {
const raw = JSON.parse(fs.readFileSync(cfg.src, 'utf8'));
const san = makeSanitizer(cfg.oldBase, cfg.newBase);
const demoModel = cfg.model || DEMO_MODEL;
let msgs = raw.messages.map((m) => transformMessage(m, san, demoModel));
const before = msgs.length;
if (name === 'word') msgs = wordSurgery(msgs);
if (name === 'plane') msgs = planeSurgery(msgs);
if (name === 'cat') msgs = catSurgery(msgs);
const title = cfg.title || raw.title;
const meta = {
title: san(title),
model_key: demoModel,
thinking_mode: true,
run_mode: 'thinking',
multi_agent_mode: !!cfg.ma,
permission_mode: 'unrestricted',
execution_mode: 'sandbox',
network_permission: '',
messages_count: msgs.length,
};
// 真实 edited_files 随 bootstrap 还原QuickDock 文件记录窗口);真实 todo_list 导出独立
// mock 文件(/api/todo-list 按 conversation_id 读取QuickDock 待办窗口)。此前均钉空/钉 null。
const rawEdited = (raw.metadata && Array.isArray(raw.metadata.edited_files)) ? raw.metadata.edited_files : [];
const editedFiles = rawEdited
.filter((it) => it && typeof it === 'object' && it.path)
.map((it) => ({ path: san(String(it.path)), op: it.op || 'edit', ts: it.ts || '' }));
const boot = { success: true, data: { conversation_id: cfg.id, edited_files: editedFiles, messages: msgs, meta, running: RUNNING } };
fs.writeFileSync(path.join(MOCK, `bootstrap-${cfg.id}.json`), JSON.stringify(boot));
if (raw.todo_list && Array.isArray(raw.todo_list.tasks) && raw.todo_list.tasks.length) {
fs.writeFileSync(path.join(MOCK, `todo-${cfg.id}.json`), JSON.stringify({ success: true, data: deepSanitize(raw.todo_list, san) }));
}
// 真实 token 统计导出独立 mock/tokens 与 /token-statistics 按 conversation_id 读取)。
// 字段名与对话文件一致total_input_tokens 等),前端 resource store 直接消费;
// 老对话无 total_cached_input_tokens/cache_exempt_input_tokens 字段,前端 `|| 0` 兜底。
if (raw.token_statistics && typeof raw.token_statistics === 'object') {
fs.writeFileSync(path.join(MOCK, `tokens-${cfg.id}.json`), JSON.stringify(raw.token_statistics, null, 1));
}
const tools = msgs.filter((m) => m.role === 'tool').length;
console.log(`${name}: ${before}${msgs.length} 条消息,工具消息 ${tools}`);
exportMedia(msgs, san);
listEntries[cfg.ma ? 'ma' : 'normal'].push({
id: cfg.id, title: meta.title, project_path: cfg.newBase, project_relative_path: null,
multi_agent_mode: !!cfg.ma, thinking_mode: true, status: 'active',
total_messages: msgs.length, total_tools: tools,
created_at: raw.created_at, updated_at: raw.updated_at,
});
}
// 保留原有的弹幕调研对话(不动),插入 normal 列表头部
const oldList = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-normal.json'), 'utf8'));
const research = oldList.data.conversations.find((c) => c.id === 'conv_20260823_122836_778');
// 弹幕调研对话不在上方循环里,单独导出真实 token 统计(老格式:无 cached/exempt 字段)
const danmakuRaw = JSON.parse(fs.readFileSync('/Users/jojo/.astrion/astrion/host/data/conversations/me/conv_20260823_122836_778.json', 'utf8'));
if (danmakuRaw.token_statistics) {
fs.writeFileSync(path.join(MOCK, 'tokens-conv_20260823_122836_778.json'), JSON.stringify(danmakuRaw.token_statistics, null, 1));
}
const normal = [research, ...listEntries.normal.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))];
fs.writeFileSync(path.join(MOCK, 'conversations-normal.json'),
JSON.stringify({ success: true, data: { conversations: normal, has_more: false, limit: 20, offset: 0, total: normal.length } }, null, 1));
fs.writeFileSync(path.join(MOCK, 'conversations-ma.json'),
JSON.stringify({ success: true, data: { conversations: listEntries.ma, has_more: false, limit: 20, offset: 0, total: listEntries.ma.length } }, null, 1));
console.log('列表更新normal', normal.length, '条 / ma', listEntries.ma.length, '条');
buildActivityMocks(makeSanitizer('/Users/jojo/Desktop/语音', '/Users/demo/dev'));
console.log('✅ 全部完成');

View File

@ -0,0 +1,89 @@
#!/usr/bin/env node
/* mock 一致性自检列表↔bootstrap↔资源↔媒体↔activity 全部交叉核对 */
const fs = require('fs');
const path = require('path');
const MOCK = path.join(__dirname, '..', 'exp', 'mock');
let errors = 0;
const bad = (msg) => { console.error('❌', msg); errors++; };
const ok = (msg) => console.log('✓', msg);
// 1. 列表 ↔ bootstrap
const normal = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-normal.json'), 'utf8')).data.conversations;
const ma = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-ma.json'), 'utf8')).data.conversations;
const all = [...normal, ...ma];
console.log(`列表: normal ${normal.length} 条, ma ${ma.length}`);
for (const c of all) {
const f = path.join(MOCK, `bootstrap-${c.id}.json`);
if (!fs.existsSync(f)) { bad(`缺 bootstrap: ${c.id}`); continue; }
const b = JSON.parse(fs.readFileSync(f, 'utf8'));
if (b.data.messages.length !== c.total_messages) bad(`${c.id} 列表消息数 ${c.total_messages} != bootstrap ${b.data.messages.length}`);
if (b.data.meta.title !== c.title) bad(`${c.id} 标题不一致: ${c.title} vs ${b.data.meta.title}`);
if (b.data.meta.multi_agent_mode !== c.multi_agent_mode) bad(`${c.id} MA 标记不一致`);
ok(`${c.title} (${c.id})`);
}
// 2. bootstrap 内部引用核对
const filesDir = fs.readdirSync(path.join(MOCK, 'files'));
const fileMocks = fs.readdirSync(MOCK).filter((f) => f.startsWith('file-'));
const mediaDir = fs.readdirSync(path.join(MOCK, 'media'));
let mediaRefs = 0, showFiles = 0, downloads = 0;
for (const c of all) {
const f = path.join(MOCK, `bootstrap-${c.id}.json`);
if (!fs.existsSync(f)) continue;
const b = JSON.parse(fs.readFileSync(f, 'utf8'));
for (const m of b.data.messages) {
const text = typeof m.content === 'string' ? m.content : '';
// show_file 卡片
for (const mm of text.matchAll(/<show_file\s+path="([^"]+)"/g)) {
showFiles++;
const name = mm[1].split('/').pop();
if (!filesDir.includes(name) && !fileMocks.includes('file-' + name)) bad(`${c.id} show_file 缺资源: ${name}`);
}
// download:// 链接
for (const mm of text.matchAll(/download:\/\/([^)\s"]+)/g)) {
downloads++;
const name = mm[1].split('/').pop();
if (!filesDir.includes(name) && !fileMocks.includes('file-' + name)) bad(`${c.id} download 缺资源: ${name}`);
}
// user_upload 引用(上传附件)
for (const mm of text.matchAll(/\.astrion\/user_upload\/([^\s"`)]+)/g)) {
const name = mm[1];
if (!fileMocks.includes('file-' + name)) bad(`${c.id} user_upload 缺资源: ${name}`);
}
// media_refs
for (const ref of (m.metadata && m.metadata.media_refs) || []) {
mediaRefs++;
if (!mediaDir.includes(ref.sha256 + '.png')) bad(`${c.id} 缺媒体: ${ref.sha256.slice(0, 12)}`);
}
// tool_calls 与 tool 结果配对
}
// tool_call 配对校验
const callIds = new Set();
for (const m of b.data.messages) for (const t of m.tool_calls || []) callIds.add(t.id);
for (const m of b.data.messages) {
if (m.role === 'tool' && m.tool_call_id && !callIds.has(m.tool_call_id)) bad(`${c.id} 孤儿 tool 消息: ${m.name} (${m.tool_call_id})`);
}
}
ok(`资源引用: show_file ${showFiles}, download ${downloads}, media_refs ${mediaRefs}`);
// 3. 隐私扫描:全 mock 不得出现 /Users/jojo 或裸 jojo
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(path.join(d, e.name)) : [path.join(d, e.name)]);
for (const f of walk(MOCK)) {
if (!/\.(json|txt|md)$/.test(f)) continue;
const s = fs.readFileSync(f, 'utf8');
if (s.includes('/Users/jojo')) bad(`${path.basename(f)} 残留 /Users/jojo`);
if (/\bjojo\b/.test(s)) bad(`${path.basename(f)} 残留 jojo`);
}
ok('隐私扫描完成');
// 4. activity mock
const subList = JSON.parse(fs.readFileSync(path.join(MOCK, 'sub-agents-conv_20260902_235153_495.json'), 'utf8')).data;
for (const t of subList) {
const f = path.join(MOCK, `activity-${t.task_id}.json`);
if (!fs.existsSync(f)) { bad(`缺 activity: ${t.task_id}`); continue; }
const a = JSON.parse(fs.readFileSync(f, 'utf8'));
ok(`activity ${t.task_id}: ${a.data.entries.length} 条事件`);
}
console.log(errors ? `\n${errors} 个问题` : '\n✅ 全部通过');
process.exit(errors ? 1 : 0);

View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* 重建猫咪对话的 cat-cafe/index.html
* 子智能体 write_file 全文 + 主对话两次 edit_file太阳修复按钮修复
* 输出到 cache/website-mock-review/assets/cat-cafe/index.html
*/
const fs = require('fs');
const path = require('path');
const SUB = '/Users/jojo/.astrion/astrion/host/data/sub_agent_tasks/sub_4_1788364765_5d6208/conversation.json';
const MAIN = '/Users/jojo/.astrion/astrion/host/mutiagents/conversations/workspace/conv_20260902_235153_495.json';
const OUT = '/Users/jojo/Desktop/agents/正在修复中/agents/cache/website-mock-review/assets/cat-cafe/index.html';
// 1. 取子智能体的 write_file 全文
const sub = JSON.parse(fs.readFileSync(SUB, 'utf8'));
const msgs = sub.messages || sub;
let html = null;
for (const m of msgs) {
for (const tc of m.tool_calls || []) {
if (tc.function.name === 'write_file') {
const args = JSON.parse(tc.function.arguments);
if (args.file_path && args.file_path.includes('cat-cafe')) html = args.content;
}
}
}
if (!html) throw new Error('未找到 cat-cafe 的 write_file');
console.log('write_file 全文:', html.length, '字符');
// 2. 按主对话顺序应用 edit_file
const main = JSON.parse(fs.readFileSync(MAIN, 'utf8'));
let edits = [];
for (const m of main.messages) {
for (const tc of m.tool_calls || []) {
if (tc.function.name === 'edit_file') {
const args = JSON.parse(tc.function.arguments);
if ((args.file_path || '').includes('cat-cafe')) edits.push(args);
}
}
}
console.log('待应用 edit_file:', edits.length, '个');
for (const [i, e] of edits.entries()) {
for (const [j, r] of (e.replacements || []).entries()) {
const cnt = html.split(r.old_string).length - 1;
console.log(` edit ${i + 1}.${j + 1}: 匹配 ${cnt}`);
if (cnt !== 1) throw new Error('匹配数异常,重建不保真');
html = html.split(r.old_string).join(r.new_string);
}
}
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, html);
console.log('已写出:', OUT, fs.statSync(OUT).size, '字节');

View File

@ -0,0 +1,33 @@
#!/usr/bin/env node
/* 极简静态文件服务器(官网预览用,替代沙箱内不可用的 python3 -m http.server */
const http = require('http');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', 'exp');
const PORT = Number(process.argv[2]) || 8898;
const MIME = {
html: 'text/html; charset=utf-8', js: 'text/javascript', mjs: 'text/javascript',
css: 'text/css', json: 'application/json; charset=utf-8', png: 'image/png',
jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml',
webp: 'image/webp', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
pdf: 'application/pdf', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
txt: 'text/plain; charset=utf-8', md: 'text/plain; charset=utf-8', ico: 'image/x-icon',
mp4: 'video/mp4', webm: 'video/webm',
};
http.createServer((req, res) => {
try {
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
if (p.endsWith('/')) p += 'index.html';
const file = path.normalize(path.join(ROOT, p));
if (!file.startsWith(ROOT)) { res.writeHead(403); return res.end(); }
fs.readFile(file, (err, data) => {
if (err) { res.writeHead(404); return res.end('not found'); }
const ext = path.extname(file).slice(1).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
} catch (e) { res.writeHead(500); res.end(String(e)); }
}).listen(PORT, '127.0.0.1', () => console.log(`preview: http://127.0.0.1:${PORT}/ (root: ${ROOT})`));