384 lines
18 KiB
JavaScript
384 lines
18 KiB
JavaScript
/* 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-<id>.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、/<conv_id>)回退到演示页;真实文件放行
|
||
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-<id>.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);
|
||
}
|