- build-mocks.cjs:从真实对话导出演示 mock 数据 - check-mocks.cjs:mock 数据完整性校验 - rebuild-cat-html.cjs / serve.cjs:分类页重建与本地预览服务
412 lines
25 KiB
JavaScript
412 lines
25 KiB
JavaScript
#!/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('✅ 全部完成');
|