/** * TalkingQ Platform - Application Management Page * 应用管理:多模态交互应用 / 语音交互应用 */ function renderAgentManagePage(container, typeFilter = 'all') { const RECYCLE_RETENTION_DAYS = 7; const RECYCLE_RETENTION_MS = RECYCLE_RETENTION_DAYS * 24 * 60 * 60 * 1000; const fallbackCompanies = [ { id: 'company_a', name: '智慧星科技' }, { id: 'company_b', name: '未来玩具厂' }, { id: 'company_c', name: '童趣电子' }, ]; const appCompanyFallbacks = { 'app-001': 'company_a', 'app-002': 'company_b', 'app-003': 'company_c', 'app-004': 'company_c', }; function getSelectableAppCompanies() { const source = window.companies?.length ? window.companies : fallbackCompanies; return source.filter(company => company.id !== 'all'); } function getAppCompanyName(companyId) { return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : (getSelectableAppCompanies().find(company => company.id === companyId)?.name || companyId || '未知公司'); } function getDefaultAppCompanyId() { const companies = getSelectableAppCompanies(); const currentCompanyId = window.__getCurrentCompanyId ? window.__getCurrentCompanyId() : window.currentCompany; if (currentCompanyId && currentCompanyId !== 'all' && companies.some(company => company.id === currentCompanyId)) { return currentCompanyId; } return companies[0]?.id || 'company_a'; } function normalizeAppCompany(app) { const companies = getSelectableAppCompanies(); const mappedCompanyId = appCompanyFallbacks[app.id]; const fallbackCompanyId = companies.some(company => company.id === mappedCompanyId) ? mappedCompanyId : getDefaultAppCompanyId(); const companyId = companies.some(company => company.id === app.companyId) ? app.companyId : fallbackCompanyId; app.companyId = companyId; app.companyName = getAppCompanyName(companyId); return app; } // 模拟应用数据 const defaultApps = [ { id: 'app-001', type: 'multimodal', name: '小Q 故事机器人', companyId: 'company_a', status: 'published', // published | editing | draft firmwareVersion: 'v2.3.1', firmwareConnected: true, updatedAt: '2026-05-13 10:24:38', description: '为 3-10 岁儿童设计的互动故事讲述应用', dau: 8420 }, { id: 'app-002', type: 'voice', name: '英语学习助手', companyId: 'company_b', status: 'editing', firmwareVersion: 'v2.0.2', firmwareConnected: true, updatedAt: '2026-05-12 15:09:07', description: '基于对话的英语口语训练应用', dau: 3120 }, { id: 'app-003', type: 'voice', name: '科学百科探索者', companyId: 'company_c', status: 'draft', firmwareVersion: null, firmwareConnected: false, updatedAt: '2026-05-10 08:52:11', description: '引导儿童探索科学知识的问答应用', dau: 0 }, { id: 'app-004', type: 'multimodal', name: '数学小课堂', companyId: 'company_c', status: 'published', firmwareVersion: 'v1.8.2', firmwareConnected: true, updatedAt: '2026-05-08 17:33:55', description: '趣味数学题与可视化解析', dau: 2180 }, ]; if (!window.__agentManagedApps) { window.__agentManagedApps = defaultApps.map(app => normalizeAppCompany({ ...app })); } window.__agentManagedApps.forEach(normalizeAppCompany); let apps = window.__agentManagedApps; let currentFilter = 'all'; let currentTypeFilter = typeFilter || window.__appTypeFilter || 'all'; window.__appTypeFilter = 'all'; let searchText = ''; function getNowText() { return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'); } function getActiveApps() { return apps.filter(app => !app.deletedAt); } function getRecycledApps() { return apps .filter(app => app.deletedAt) .sort((a, b) => new Date(b.deletedAt) - new Date(a.deletedAt)); } function getRecycleExpireAt(app) { const deletedTime = new Date(app.deletedAt || Date.now()).getTime(); return new Date(deletedTime + RECYCLE_RETENTION_MS); } function getRecycleRemainingText(app) { const remainingMs = getRecycleExpireAt(app).getTime() - Date.now(); if (remainingMs <= 0) return '待自动清理'; const days = Math.floor(remainingMs / (24 * 60 * 60 * 1000)); const hours = Math.ceil((remainingMs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); if (days <= 0) return `${Math.max(hours, 1)} 小时后清理`; return `${days} 天 ${hours} 小时后清理`; } function pushAppLifecycleNotification({ title, detail, app, severity = 'info', metric = '应用生命周期' }) { window.__addRuntimeNotification?.({ eventType: 'agent.lifecycle', id: `agent-lifecycle-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`, title, detail, objectName: app?.name || '交互应用', metric, severity, actionPage: app?.type === 'voice' ? 'agent-manage-voice' : app?.type === 'multimodal' ? 'agent-manage-multimodal' : 'agent-manage', actionText: '查看应用', }); } function cleanupExpiredRecycledApps({ notify = true } = {}) { const now = Date.now(); const expiredApps = apps.filter(app => app.deletedAt && getRecycleExpireAt(app).getTime() <= now); if (!expiredApps.length) return; API.agents.cleanupRecycleBin().catch(error => { console.warn('Recycle bin cleanup failed in mock mode', error); }); window.__agentManagedApps = apps = apps.filter(app => !app.deletedAt || getRecycleExpireAt(app).getTime() > now); if (notify) { Toast.info(`系统已自动清理 ${expiredApps.length} 个超过 ${RECYCLE_RETENTION_DAYS} 天的回收站应用`, 1800); pushAppLifecycleNotification({ title: `回收站已自动清理 ${expiredApps.length} 个过期应用`, detail: `系统按 ${RECYCLE_RETENTION_DAYS} 天保留策略清理了:${expiredApps.map(app => app.name).join('、')}。后端接入后该操作应写入审计日志,并触发站内消息。`, app: expiredApps[0], severity: 'warning', metric: `${RECYCLE_RETENTION_DAYS} 天保留到期`, }); } } cleanupExpiredRecycledApps({ notify: true }); function getStatusLabel(status) { const map = { published: { label: '已发布', cls: 'badge-success' }, editing: { label: '已发布·编辑中', cls: 'badge-warning' }, draft: { label: '草稿', cls: 'badge-default' }, }; return map[status] || { label: status, cls: 'badge-default' }; } function getTypeInfo(type) { if (type === 'multimodal') return { label: '多模态交互', icon: 'monitor-smartphone', color: '#722ed1', bg: '#f9f0ff' }; return { label: '语音交互', icon: 'mic', color: '#1890ff', bg: '#e6f7ff' }; } function renderApps() { cleanupExpiredRecycledApps({ notify: false }); const recycleCountEl = document.getElementById('appRecycleCount'); if (recycleCountEl) recycleCountEl.textContent = getRecycledApps().length; const filtered = getActiveApps().filter(a => { const matchFilter = currentFilter === 'all' || a.status === currentFilter; const matchType = currentTypeFilter === 'all' || a.type === currentTypeFilter; const matchSearch = !searchText || a.name.includes(searchText) || a.id.includes(searchText); return matchFilter && matchType && matchSearch; }); const grid = document.getElementById('appGrid'); if (!grid) return; updateFilterCounts(); if (filtered.length === 0) { grid.innerHTML = `

暂无符合条件的应用

试试调整筛选条件或创建新应用

创建新应用
`; lucide.createIcons({ elements: [grid] }); document.getElementById('createAppCard')?.addEventListener('click', () => { showCreateAppModal(); }); return; } grid.innerHTML = filtered.map(app => { const st = getStatusLabel(app.status); const tp = getTypeInfo(app.type); return `
${tp.label}
${st.label}
${app.name}
${app.description}
应用 ID ${app.id}
所属公司 ${getAppCompanyName(app.companyId)}
更新时间 ${app.updatedAt}
固件版本 ${app.firmwareConnected ? app.firmwareVersion : '未接入固件'}
`; }).join('') + `
创建新应用
`; lucide.createIcons({ elements: [grid] }); // 绑定配置按钮 grid.querySelectorAll('.app-action-config').forEach(btn => { btn.addEventListener('click', () => { const appId = btn.dataset.id; // 传递选中的 appId window.__selectedAppId = appId; navigateTo('agent-config'); }); }); // 绑定新建卡片 document.getElementById('createAppCard')?.addEventListener('click', () => { showCreateAppModal(); }); grid.querySelectorAll('[data-app-menu-trigger]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const menu = btn.closest('[data-app-menu]'); const isOpen = menu?.classList.contains('open'); grid.querySelectorAll('[data-app-menu].open').forEach(item => { item.classList.remove('open'); item.querySelector('[data-app-menu-trigger]')?.setAttribute('aria-expanded', 'false'); }); if (!isOpen) { menu?.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); } }); }); grid.querySelectorAll('[data-app-action]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const action = btn.dataset.appAction; const appId = btn.dataset.id; const app = apps.find(item => item.id === appId); if (!app) return; btn.closest('[data-app-menu]')?.classList.remove('open'); if (action === 'analytics') { const companyId = normalizeAppCompany(app).companyId; const role = window.__getUserRole ? window.__getUserRole() : 'admin'; const allowedCompanyIds = window.__getAllowedCompanyIds ? window.__getAllowedCompanyIds(role) : ['all']; if (!allowedCompanyIds.includes(companyId)) { Toast.warning('当前角色无权查看该公司数据'); return; } if (window.switchCompany) { window.switchCompany(companyId); } else { window.currentCompany = companyId; window.__syncCompanySelectorValue?.(companyId); } window.__dashboardAppScope = { id: app.id, name: app.name, type: app.type, companyId, companyName: getAppCompanyName(companyId) }; navigateTo('dashboard'); } else if (action === 'api') { window.showApiIntegrationModal(app.id, app.name); } else if (action === 'duplicate') { window.duplicateApp(app.id); } else if (action === 'delete') { window.deleteApp(app.id, app.name); } }); }); } function getTypeScopedApps() { return getActiveApps().filter(a => currentTypeFilter === 'all' || a.type === currentTypeFilter); } function updateFilterCounts() { const scoped = getTypeScopedApps(); document.querySelectorAll('.filter-tab').forEach(tab => { const filter = tab.dataset.filter; const countEl = tab.querySelector('.filter-count'); if (!countEl) return; countEl.textContent = filter === 'all' ? scoped.length : scoped.filter(app => app.status === filter).length; }); } function setActiveFilter(filter) { currentFilter = filter; document.querySelectorAll('.filter-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.filter === filter); }); updateFilterCounts(); } function clearAppSearch() { searchText = ''; const input = document.getElementById('appSearchInput'); if (input) input.value = ''; } function revealCreatedApp(app) { clearAppSearch(); if (currentTypeFilter !== 'all' && currentTypeFilter !== app.type) { window.__appTypeFilter = app.type; setTimeout(() => navigateTo(app.type === 'voice' ? 'agent-manage-voice' : 'agent-manage-multimodal'), 0); return; } setActiveFilter('all'); renderApps(); } const typeScopedApps = getTypeScopedApps(); container.innerHTML = `
`; lucide.createIcons({ elements: [container] }); renderApps(); // 筛选 tab 切换 document.querySelectorAll('.filter-tab').forEach(tab => { tab.addEventListener('click', () => { currentFilter = tab.dataset.filter; document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderApps(); }); }); // 搜索 document.getElementById('appSearchInput')?.addEventListener('input', (e) => { searchText = e.target.value.trim(); renderApps(); }); // 顶部创建按钮 document.getElementById('createAppBtn')?.addEventListener('click', showCreateAppModal); document.getElementById('openAppRecycleBtn')?.addEventListener('click', showRecycleBinModal); if (window.__appMenuCloseHandler) { document.removeEventListener('click', window.__appMenuCloseHandler); } window.__appMenuCloseHandler = (event) => { if (event.target.closest?.('[data-app-menu]')) return; container.querySelectorAll('[data-app-menu].open').forEach(menu => { menu.classList.remove('open'); menu.querySelector('[data-app-menu-trigger]')?.setAttribute('aria-expanded', 'false'); }); }; document.addEventListener('click', window.__appMenuCloseHandler); // ─── 工具函数 ──────────────────────────────────────────────── window.duplicateApp = function(appId) { const app = apps.find(a => a.id === appId && !a.deletedAt); if (!app) return; const newApp = { ...app, id: 'app-' + String(Date.now()).slice(-3), name: app.name + '(副本)', status: 'draft', firmwareConnected: false, firmwareVersion: null, updatedAt: new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'), dau: 0, deletedAt: null, deletedBy: null, }; apps.push(newApp); revealCreatedApp(newApp); Toast.success(`已复制"${app.name}"为草稿`); }; window.deleteApp = function(appId, appName) { const app = apps.find(a => a.id === appId && !a.deletedAt); if (!app) return; createModal({ title: '移入回收站', content: `

确定要将应用 ${appName} 移入回收站吗?

保留 ${RECYCLE_RETENTION_DAYS} 天 移入回收站后应用会从正常列表隐藏,${RECYCLE_RETENTION_DAYS} 天内可恢复;到期后系统自动清理。该操作会在消息通知中提醒管理员。
`, confirmText: '移入回收站', onConfirm: async () => { try { await API.agents.delete(appId); } catch (error) { Toast.error('应用移入回收站失败,请稍后重试'); return false; } app.deletedAt = new Date().toISOString(); app.deletedBy = sessionStorage.getItem('talkingq_user') || 'Admin'; app.updatedAt = getNowText(); renderApps(); Toast.success(`应用"${appName}"已移入回收站,${RECYCLE_RETENTION_DAYS} 天后自动清理`); pushAppLifecycleNotification({ title: `应用「${appName}」已移入回收站`, detail: `应用「${appName}」已从正常应用列表隐藏,将在 ${getRecycleExpireAt(app).toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-')} 后自动清理。${RECYCLE_RETENTION_DAYS} 天内可在应用管理回收站恢复。`, app, severity: 'warning', metric: `${RECYCLE_RETENTION_DAYS} 天后自动清理`, }); } }); }; function showRecycleBinModal() { cleanupExpiredRecycledApps({ notify: true }); const recycledApps = getRecycledApps(); const modal = createModal({ title: `应用回收站(保留 ${RECYCLE_RETENTION_DAYS} 天)`, size: 'large', showCancel: false, confirmText: '关闭', content: `
${recycledApps.length} 个应用等待清理

回收站仅保留 ${RECYCLE_RETENTION_DAYS} 天。恢复后应用会回到原公司和原类型列表;彻底删除后前端 mock 中不再可恢复。

${recycledApps.length === 0 ? `

回收站为空

被删除的交互应用会在这里保留 ${RECYCLE_RETENTION_DAYS} 天。

` : `
${recycledApps.map(app => { const tp = getTypeInfo(app.type); const deletedAt = new Date(app.deletedAt).toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'); const expireAt = getRecycleExpireAt(app).toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'); return `
${app.name}
${tp.label} ${getAppCompanyName(app.companyId)} ID: ${app.id}
删除时间:${deletedAt} · 到期清理:${expireAt} · ${getRecycleRemainingText(app)}
`; }).join('')}
`}
`, onConfirm: () => true, }); setTimeout(() => { lucide.createIcons({ elements: [modal.overlay] }); modal.overlay.querySelectorAll('[data-recycle-action]').forEach(btn => { btn.addEventListener('click', async (event) => { event.preventDefault(); event.stopPropagation(); const app = apps.find(item => item.id === btn.dataset.id && item.deletedAt); if (!app) return; if (btn.dataset.recycleAction === 'restore') { btn.disabled = true; try { await API.agents.restore(app.id); } catch (error) { Toast.error('应用恢复失败,请稍后重试'); btn.disabled = false; return; } app.deletedAt = null; app.deletedBy = null; app.updatedAt = getNowText(); modal.close(); renderApps(); Toast.success(`应用"${app.name}"已恢复`); pushAppLifecycleNotification({ title: `应用「${app.name}」已从回收站恢复`, detail: `应用「${app.name}」已恢复到 ${getAppCompanyName(app.companyId)} 的${getTypeInfo(app.type).label}列表,可继续配置、发布和查看数据。`, app, severity: 'success', metric: '已恢复', }); } if (btn.dataset.recycleAction === 'purge') { createModal({ title: '彻底删除应用', content: `

确定要彻底删除 ${app.name} 吗?

彻底删除后将从回收站移除,前端 mock 中不可恢复。后端接入时应同步删除应用配置或进入异步清理任务。

`, confirmText: '彻底删除', onConfirm: async () => { try { await API.agents.purge(app.id); } catch (error) { Toast.error('应用彻底删除失败,请稍后重试'); return false; } const deletedName = app.name; const deletedApp = { ...app }; window.__agentManagedApps = apps = apps.filter(item => item.id !== app.id); modal.close(); renderApps(); Toast.success(`应用"${deletedName}"已彻底删除`); pushAppLifecycleNotification({ title: `应用「${deletedName}」已彻底删除`, detail: `应用「${deletedName}」已从回收站中彻底删除。后端接入后建议保留审计日志,并异步清理配置、运行记录索引和关联缓存。`, app: deletedApp, severity: 'critical', metric: '不可恢复', }); } }); } }); }); }, 50); } window.showApiIntegrationModal = function(appId, appName) { createModal({ title: `API 接入 — ${appName}`, content: `
App ID
${appId}
API Key
sk-tq-••••••••••••••••••••••••
访问文档:POST https://api.talkingq.com/v1/chat/completions,Header 携带 Authorization: Bearer <API_KEY>
`, }); setTimeout(() => lucide.createIcons(), 100); }; function showCreateAppModal() { const defaultType = currentTypeFilter === 'voice' ? 'voice' : 'multimodal'; const selectableCompanies = getSelectableAppCompanies(); const defaultCompanyId = getDefaultAppCompanyId(); createModal({ title: '创建应用', content: `
`, onConfirm: () => { const name = document.getElementById('newAppName')?.value.trim(); const desc = document.getElementById('newAppDesc')?.value.trim(); const type = document.querySelector('input[name="appType"]:checked')?.value || 'multimodal'; const companyId = document.getElementById('newAppCompany')?.value; if (!name) { Toast.warning('请填写应用名称'); return false; } if (!companyId) { Toast.warning('请选择所属公司'); return false; } const newApp = { id: 'app-' + String(Date.now()).slice(-4), type, name, companyId, companyName: getAppCompanyName(companyId), description: desc || '暂无描述', status: 'draft', firmwareVersion: null, firmwareConnected: false, updatedAt: new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'), dau: 0, }; apps.push(newApp); revealCreatedApp(newApp); Toast.success(`应用"${name}"创建成功`); }, confirmText: '创建', }); // 等 DOM 渲染后绑定事件 setTimeout(() => { lucide.createIcons(); document.querySelectorAll('.app-type-selector').forEach(el => { el.addEventListener('click', () => { document.querySelectorAll('.app-type-selector').forEach(s => s.classList.remove('selected')); el.classList.add('selected'); el.querySelector('input[type=radio]').checked = true; }); }); }, 50); } } registerPage('agent-manage', function(container) { renderAgentManagePage(container, window.__appTypeFilter || 'all'); }); window.__agentManageRender = renderAgentManagePage; registerPage('agent-manage-multimodal', function(container) { renderAgentManagePage(container, 'multimodal'); }); registerPage('agent-manage-voice', function(container) { renderAgentManagePage(container, 'voice'); });