/* Astrion 官网演示 —— Service Worker:API 静态 mock * * 真实前端构建产物原样运行,本 SW 拦截数据层: * - /api/* → mock/*.json 静态响应 * - /socket.io/* → 503(前端自动降级 REST 轮询) * - 导航请求 → demo.html(支撑 history 路由刷新) * - 其余 → 放行 */ const DEMO_PAGE = '/demo.html'; const CACHE = 'astrion-demo-v28'; /* ── 静态响应体 ── */ const MODELS = { success: true, items: [ { context_window: 1048576, description: 'Kimi-K3', fast_only: false, max_output_tokens: 64000, model_key: 'Kimi-K3', multimodal: 'image,video', name: 'Kimi-K3', supports_reasoning_effort: true, supports_thinking: true, thinking_only: false, visible: true }, { context_window: 1000000, description: 'DeepSeek-V4-Pro', fast_only: false, max_output_tokens: 384000, model_key: 'DeepSeek-V4-Pro', multimodal: 'none', name: 'DeepSeek-V4-Pro', supports_reasoning_effort: true, supports_thinking: true, thinking_only: false, visible: true }, { context_window: 1000000, description: 'DeepSeek-V4-Flash', fast_only: false, max_output_tokens: 384000, model_key: 'DeepSeek-V4-Flash', multimodal: 'none', name: 'DeepSeek-V4-Flash', supports_reasoning_effort: true, supports_thinking: true, thinking_only: false, visible: true }, { context_window: 262144, description: 'GLM-5.2', fast_only: false, max_output_tokens: 64072, model_key: 'GLM-5.2', multimodal: 'none', name: 'GLM-5.2', supports_reasoning_effort: true, supports_thinking: true, thinking_only: false, visible: true }, { context_window: 1000000, description: 'MiniMax-M3', fast_only: false, max_output_tokens: 131072, model_key: 'MiniMax-M3', multimodal: 'none', name: 'MiniMax-M3', supports_reasoning_effort: false, supports_thinking: true, thinking_only: false, visible: true } ] }; const SESSION_STATUS = { success: true, session: { host_mode: true, logged_in: true, role: 'admin', run_mode: 'thinking', thinking_mode: true, username: 'host' } }; // 工作区列表为静态数据;「当前工作区」是会话态——select 接口会改它, // 列表与 select 响应都从它动态组装;否则 current_workspace_id 永远钉死, // 前端切换后 toast/分组高亮会拿错工作区(实测弹窗恒显示「日常使用」)。 const WS_LIST = [ { workspace_id: 'ws-daily', label: '日常使用', path: '/Users/demo/daily', running_task_count: 0 }, { workspace_id: 'ws-starraid', label: 'StarRaid', path: '/Users/demo/starraid', running_task_count: 0 }, { workspace_id: 'ws-dev', label: '开发实战', path: '/Users/demo/dev', running_task_count: 0 } ]; // 初始与 demo 默认落点(弹幕调研对话所属工作区)一致 let currentWorkspaceId = 'ws-starraid'; function buildWorkspaces() { return { success: true, data: { current_workspace_id: currentWorkspaceId, default_workspace_id: 'ws-daily', workspaces: WS_LIST.map((w) => ({ ...w, is_current: w.workspace_id === currentWorkspaceId })) } }; } const UI_BLOCKS = { block_compress_conversation: false, block_conversation_review: false, block_file_manager: false, block_focus_panel: false, block_personal_space: false, block_realtime_terminal: false, block_token_panel: false, block_tool_toggle: false, block_upload: false, block_virtual_monitor: false, collapse_workspace: false }; const STATUS = { success: true, version: '2026-08-23', host_mode: true, container: { username: 'demo', mode: 'host', workspace_path: '/Users/demo/daily', running: true }, admin_policy: { disabled_models: [], forced_category_states: {}, ui_blocks: UI_BLOCKS, version: '2026-08-23' } }; const PERSONALIZATION = { success: true, data: { theme: 'dark', tool_intent_enabled: true, group_sidebar_by_workspace: true, block_display_mode: 'minimal', compact_message_display: 'brief', communication_style: 'auto', conversation_continuity: 'medium', agents_md_auto_inject: true, auto_deep_compress_enabled: true, deep_compress_trigger_tokens: 200000, auto_generate_title: true, auto_shallow_compress_enabled: false }, context_compression_settings: { context_window_tokens: 256000, deep_trigger_tokens: 200000, shallow_keep_recent_tools: 15, shallow_keep_user_turn_tools: 3, shallow_max_replace_per_round: 10, shallow_trigger_tokens: 1000, shallow_trigger_tool_calls_interval: 10 } }; const WORK_MODE = { success: true, mode: 'execute', options: ['plan', 'ask', 'execute'], permission_mode: 'unrestricted', execution_mode: 'sandbox' }; const PERMISSION_MODE = { success: true, mode: 'unrestricted', options: ['readonly', 'approval', 'auto_approval', 'unrestricted'], pending_mode: null }; const EXECUTION_MODE = { success: true, enabled: true, options: ['sandbox', 'direct'], pending_mode: null, state: { default_mode: 'sandbox', mode: 'sandbox' } }; const NETWORK_PERMISSION = { success: true, enabled: true, mode: 'restricted', options: ['restricted', 'full', 'none'], pending_mode: null }; const TUTORIAL = { success: true, data: { applicable: false, should_prompt: false, tutorial_completed: true, username: 'host' } }; // 宿主机模式不统计用量:record_model_call/record_search_call 直接 return,quotas count 恒为 0、 // window_start/reset_at 为 null(get_quota_snapshot 行为),limit 为配置默认值。勿编造非 0 数字。 const USAGE = { success: true, data: { quotas: { fast: { count: 0, limit: 200, window_start: null, reset_at: null }, thinking: { count: 0, limit: 200, window_start: null, reset_at: null }, search: { count: 0, limit: 20, window_start: null, reset_at: null } }, role: 'user' } }; // ── Token 统计 mock(右下角 token 菜单/抽屉数据源)── // 数据为每条对话的真实 token_statistics,由 build-mocks.cjs 导出为 mock/tokens-.json。 // 当前上下文走 /api/conversations/{id}/tokens;统计区走 /token-statistics。上限 200k 由 // PERSONALIZATION 的 deep_compress_trigger_tokens 决定(深度压缩阈值)。 const EMPTY_OK = { success: true, data: {} }; const EMPTY_LIST = { success: true, data: [] }; const GIT_SUMMARY = { success: true, data: { available: false } }; function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } }); } /* ── 安装:预缓存演示页 ── */ self.addEventListener('install', (e) => { e.waitUntil(caches.open(CACHE).then((c) => c.add(DEMO_PAGE)).then(() => self.skipWaiting())); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys() .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) .then(() => self.clients.claim()) ); }); /* ── 请求路由 ── */ self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); if (url.origin !== self.location.origin) return; // 导航请求:仅前端对话路由(/new、/)回退到演示页;真实文件放行 if (e.request.mode === 'navigate') { const isAppRoute = /^\/(?:conv_)?\d{8}_\d{6}_\d{3}$/.test(url.pathname) || url.pathname === '/new'; if (isAppRoute) { e.respondWith(caches.match(DEMO_PAGE).then((r) => r || fetch(DEMO_PAGE))); } return; } // socket.io → 断连(前端降级 REST 轮询) if (url.pathname.startsWith('/socket.io/')) { e.respondWith(new Response('demo: realtime disabled', { status: 503 })); return; } if (!url.pathname.startsWith('/api/')) return; // 静态资源放行 e.respondWith(routeApi(url, e.request)); }); // ── 模拟流式回复:演示站发送消息时,返回固定文案「实际功能请部署后体验」逐字流式吐出 ── // 协议对齐真实后端:POST /api/tasks 创建任务;GET /api/tasks/{id}?from=N 增量拉事件 // (text_start → 逐字 text_chunk → text_end → task_complete,status running→succeeded)。 // 逐字 100ms(用户拍板):前端轮询周期 250ms,每轮稳定拿 2~3 字,节奏快而匀。 const DEMO_REPLY = '实际功能请部署后体验'; const DEMO_TASK_EVENT_INTERVAL_MS = 100; let demoTask = null; // { taskId, convId, startTs, events };SW 休眠后丢失,重发即可 function buildDemoReplyEvents() { const evts = [{ idx: 0, type: 'text_start', data: {} }]; Array.from(DEMO_REPLY).forEach((ch, i) => { evts.push({ idx: i + 1, type: 'text_chunk', data: { content: ch } }); }); evts.push({ idx: DEMO_REPLY.length + 1, type: 'text_end', data: { full_content: DEMO_REPLY } }); evts.push({ idx: DEMO_REPLY.length + 2, type: 'task_complete', data: { has_running_sub_agents: false, has_running_background_commands: false, has_running_multi_agent: false } }); return evts; } async function routeApi(url, req) { const p = url.pathname; const method = req ? req.method : 'GET'; // 子智能体活动详情 const actMatch = p.match(/^\/api\/sub_agents\/(sub_[\w-]+)\/activity$/); if (actMatch) { const r = await fetch(`/mock/activity-${actMatch[1]}.json`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); return json({ success: true, data: { status: 'completed', entries: [] } }); } // 子智能体列表 if (p === '/api/sub_agents') { const convId = url.searchParams.get('conversation_id') || ''; const r = await fetch(`/mock/sub-agents-${convId}.json`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); return json({ success: true, data: [] }); } // 多智能体活跃实例:猫咪对话(conv_20260902_235153_495)有 4 个实例,其余对话为空。 // 这是 QuickDock 子智能体窗口在多智能体对话下的唯一数据源,钉空会让快捷窗口无法展开。 if (p === '/api/multiagent/active_sub_agents') { const convId = url.searchParams.get('conversation_id') || ''; const r = await fetch(`/mock/ma-agents-${convId}.json`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); return json({ success: true, agents: [] }); } // 待办 / 后台命令(演示对话为空) // 待办列表:有 todo mock 的对话(如猫咪)返回真实内容,否则空——QuickDock 待办窗口数据源 if (p === '/api/todo-list') { const convId = url.searchParams.get('conversation_id') || ''; if (convId) { const r = await fetch(`/mock/todo-${convId}.json`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } return json({ success: true, data: null }); } if (p === '/api/background_commands') return json({ success: true, data: [] }); // 文件预览/下载(对话内文件卡片、下载链接) if (p === '/api/file/content') { const name = (url.searchParams.get('path') || '').split('/').pop(); // 二进制/富文本资源放 mock/files/(按扩展名给 MIME) const ext = (name.split('.').pop() || '').toLowerCase(); const MIME = { pdf: 'application/pdf', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', html: 'text/html; charset=utf-8', png: 'image/png', jpg: 'image/jpeg', }; if (MIME[ext]) { const rb = await fetch(`/mock/files/${name}`); if (rb.ok) return new Response(await rb.arrayBuffer(), { headers: { 'Content-Type': MIME[ext] } }); } // 文本资源(mock/file-*) const r = await fetch(`/mock/file-${name}`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); return new Response('演示环境没有该文件', { status: 404 }); } // 消息内嵌媒体(view_image 截图等):mock/media/{sha}.png const mediaMatch = p.match(/^\/api\/conversations\/media\/(.+)$/); if (mediaMatch) { const id = decodeURIComponent(mediaMatch[1]).replace(/^sha256:/, ''); const r = await fetch(`/mock/media/${id}.png`); if (r.ok) return new Response(await r.arrayBuffer(), { headers: { 'Content-Type': 'image/png' } }); return new Response('演示环境没有该媒体', { status: 404 }); } // 对话 token:当前上下文 / 统计区(均读真实导出的 mock/tokens-.json) const tokensMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/tokens$/); if (tokensMatch) { const r = await fetch(`/mock/tokens-${tokensMatch[1]}.json`); const d = r.ok ? await r.json() : null; return json({ success: true, data: { total_tokens: (d && d.current_context_tokens) || 0 } }); } const tokenStatsMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/token-statistics$/); if (tokenStatsMatch) { const r = await fetch(`/mock/tokens-${tokenStatsMatch[1]}.json`); if (r.ok) return json({ success: true, data: await r.json() }); return json({ success: true, data: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, total_cached_input_tokens: 0, cache_exempt_input_tokens: 0, current_context_tokens: 0 } }); } // 对话 bootstrap const bootMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/bootstrap$/); if (bootMatch) { const r = await fetch(`/mock/bootstrap-${bootMatch[1]}.json`); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); return json({ success: false, error: '对话不存在' }, 404); } // 版本控制状态:必须显式 mock!前端 fetchVersioningStatus 会用 payload.host_mode // 覆盖全局 versioningHostMode;若落到兜底 EMPTY_OK(data:{}),host_mode=undefined // 会把 host 模式打回 false,分组视图立刻消失且 persist 进 localStorage 持续作恶。 const versioningMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/versioning$/); if (versioningMatch) { return json({ success: true, data: { host_mode: true, enabled: false, mode: 'overwrite' } }); } // 对话列表(支持 workspace_id 过滤:按 WORKSPACES 的 path 匹配对话 project_path) if (p === '/api/conversations') { const ma = url.searchParams.get('multi_agent_mode') === '1'; const r = await fetch(`/mock/conversations-${ma ? 'ma' : 'normal'}.json`); const wsId = url.searchParams.get('workspace_id'); if (!wsId) { return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } const all = await r.json(); const ws = WS_LIST.find((w) => w.workspace_id === wsId); const list = (all.data.conversations || []).filter((c) => !ws || c.project_path === ws.path); return json({ success: true, data: { conversations: list, has_more: false, limit: 50, offset: 0, total: list.length } }); } // 各状态端点 if (p === '/api/session-status') return json(SESSION_STATUS); if (p === '/api/csrf-token') return json({ success: true, csrf_token: 'demo' }); if (p === '/api/v1/models') return json(MODELS); if (p === '/api/host/workspaces/select') { const target = url.searchParams.get('workspace_id') || ''; const ws = WS_LIST.find((w) => w.workspace_id === target); if (ws) currentWorkspaceId = ws.workspace_id; // 故意不返回 project_path:前端 toast 取值顺序是 project_path || 工作区名 || id, // 演示区路径(/Users/demo/xxx)没有展示价值,缺省后 toast 显示工作区名称。 return json({ success: true, data: { current_workspace_id: currentWorkspaceId, default_workspace_id: 'ws-daily', workspace_label: ws ? ws.label : '' } }); } if (p === '/api/host/workspaces') return json(buildWorkspaces()); if (p === '/api/status') return json(STATUS); if (p === '/api/personalization') return json(PERSONALIZATION); if (p === '/api/work-mode') return json(WORK_MODE); if (p === '/api/permission-mode') return json(PERMISSION_MODE); if (p === '/api/execution-mode') return json(EXECUTION_MODE); if (p === '/api/network-permission') return json(NETWORK_PERMISSION); if (p === '/api/tutorial-status') return json(TUTORIAL); if (p === '/api/usage') return json(USAGE); if (p === '/api/project/git-summary') return json(GIT_SUMMARY); if (p === '/api/health') return json({ status: 'ok' }); if (p === '/api/focused') return json({}); // 发送消息 → 创建模拟任务(前端随后按 task_id 轮询事件) if (p === '/api/tasks' && method === 'POST') { const body = await req.json().catch(() => ({})); demoTask = { taskId: `demo_task_${Date.now()}`, convId: String(body.conversation_id || ''), startTs: Date.now(), events: buildDemoReplyEvents(), }; return json({ success: true, data: { task_id: demoTask.taskId, status: 'running', created_at: new Date().toISOString() } }); } // 模拟任务轮询:按经过时间逐步放事件,放完后 status=succeeded(前端停止轮询) const demoTaskMatch = p.match(/^\/api\/tasks\/(demo_task_\d+)$/); if (demoTaskMatch) { if (!demoTask || demoTask.taskId !== demoTaskMatch[1]) { return json({ success: false, error: '任务不存在或已过期' }, 404); } const from = Math.max(0, Number(url.searchParams.get('from') || 0)); const visible = Math.min(demoTask.events.length, Math.floor((Date.now() - demoTask.startTs) / DEMO_TASK_EVENT_INTERVAL_MS)); const done = visible >= demoTask.events.length; return json({ success: true, data: { task_id: demoTask.taskId, status: done ? 'succeeded' : 'running', events: demoTask.events.slice(from, visible), next_offset: visible, conversation_id: demoTask.convId, updated_at: new Date().toISOString(), } }); } if (p === '/api/tasks' || p === '/api/terminals') return json(EMPTY_LIST); if (p === '/api/tool-settings') return json({ success: true, categories: [] }); if (p === '/api/effective-policy') return json({ success: true, data: { categories: {} } }); if (p === '/api/project-storage') return json(EMPTY_OK); if (p === '/api/input-draft') return json(EMPTY_OK); // 兜底 return json(EMPTY_OK); }