astrion-website/demo/sw.js

384 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Astrion 官网演示 —— Service WorkerAPI 静态 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 直接 returnquotas count 恒为 0、
// window_start/reset_at 为 nullget_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_completestatus 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_OKdata:{}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);
}