chore: 迁入 mock 构建管线脚本(tools/)+ 对齐最新 conversations-ma mock
This commit is contained in:
parent
31d27eba69
commit
aba1aae262
@ -13,7 +13,7 @@
|
|||||||
"total_messages": 185,
|
"total_messages": 185,
|
||||||
"total_tools": 87,
|
"total_tools": 87,
|
||||||
"created_at": "2026-09-02T23:51:53.495751",
|
"created_at": "2026-09-02T23:51:53.495751",
|
||||||
"updated_at": "2026-09-03T00:40:08.788991"
|
"updated_at": "2026-09-03T20:39:59.397333"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_more": false,
|
"has_more": false,
|
||||||
|
|||||||
411
tools/build-mocks.cjs
Normal file
411
tools/build-mocks.cjs
Normal 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 成功渲染为 PDF(233KB,3页)——这是最强的验证: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('✅ 全部完成');
|
||||||
89
tools/check-mocks.cjs
Normal file
89
tools/check-mocks.cjs
Normal 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);
|
||||||
52
tools/rebuild-cat-html.cjs
Normal file
52
tools/rebuild-cat-html.cjs
Normal 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, '字节');
|
||||||
33
tools/serve.cjs
Normal file
33
tools/serve.cjs
Normal 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})`));
|
||||||
Loading…
Reference in New Issue
Block a user