/**
* 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 `
${app.name}
${app.description}
`;
}).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: `
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');
});