Files
TalkingQ_Dashboard/scripts/agent-manage.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

798 lines
35 KiB
JavaScript
Raw Permalink 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.

/**
* 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 = `
<div class="empty-state" style="grid-column:1/-1">
<i data-lucide="search-x" style="width:40px;height:40px;color:var(--text-muted);margin-bottom:12px"></i>
<p style="font-size:15px;font-weight:600;margin-bottom:6px">暂无符合条件的应用</p>
<p>试试调整筛选条件或创建新应用</p>
</div>
<div class="app-card app-card-new" id="createAppCard">
<i data-lucide="plus-circle" style="width:36px;height:36px;color:var(--text-muted);margin-bottom:10px"></i>
<span style="font-size:13px;color:var(--text-muted);font-weight:500">创建新应用</span>
</div>`;
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 `
<div class="app-card" data-id="${app.id}">
<div class="app-card-header">
<div class="app-type-badge" style="background:${tp.bg};color:${tp.color}">
<i data-lucide="${tp.icon}" style="width:13px;height:13px"></i>
<span>${tp.label}</span>
</div>
<span class="badge ${st.cls}">${st.label}</span>
</div>
<div class="app-card-body">
<div class="app-icon" style="background:${tp.bg}">
<i data-lucide="${tp.icon}" style="color:${tp.color};width:24px;height:24px"></i>
</div>
<div class="app-info">
<div class="app-name">${app.name}</div>
<div class="app-desc">${app.description}</div>
</div>
</div>
<div class="app-meta">
<div class="app-meta-item">
<span class="app-meta-label">应用 ID</span>
<span class="app-meta-val monospace">${app.id}</span>
</div>
<div class="app-meta-item">
<span class="app-meta-label">所属公司</span>
<span class="app-meta-val">${getAppCompanyName(app.companyId)}</span>
</div>
<div class="app-meta-item">
<span class="app-meta-label">更新时间</span>
<span class="app-meta-val">${app.updatedAt}</span>
</div>
<div class="app-meta-item">
<span class="app-meta-label">固件版本</span>
<span class="app-meta-val ${app.firmwareConnected ? '' : 'text-muted'}">
${app.firmwareConnected ? app.firmwareVersion : '未接入固件'}
</span>
</div>
</div>
<div class="app-card-actions">
<button class="btn btn-primary app-action-config" data-id="${app.id}">
<i data-lucide="settings"></i> 配置应用
</button>
<div class="app-action-more" data-app-menu>
<button class="btn btn-outline app-action-more-trigger" type="button" data-app-menu-trigger data-id="${app.id}" aria-label="更多操作" aria-expanded="false">
<i data-lucide="more-horizontal"></i>
</button>
<div class="app-action-menu">
<button type="button" class="app-action-menu-item" data-app-action="analytics" data-id="${app.id}">
<i data-lucide="bar-chart-2"></i><span>查看数据</span>
</button>
<button type="button" class="app-action-menu-item" data-app-action="api" data-id="${app.id}">
<i data-lucide="plug"></i><span>API 接入</span>
</button>
<button type="button" class="app-action-menu-item" data-app-action="duplicate" data-id="${app.id}">
<i data-lucide="copy"></i><span>复制应用</span>
</button>
<button type="button" class="app-action-menu-item danger" data-app-action="delete" data-id="${app.id}">
<i data-lucide="trash-2"></i><span>删除应用</span>
</button>
</div>
</div>
</div>
</div>
`;
}).join('') + `
<!-- 创建新应用占位卡 -->
<div class="app-card app-card-new" id="createAppCard">
<i data-lucide="plus-circle" style="width:36px;height:36px;color:var(--text-muted);margin-bottom:10px"></i>
<span style="font-size:13px;color:var(--text-muted);font-weight:500">创建新应用</span>
</div>
`;
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 = `
<div class="page-header">
<div>
<div class="page-title">${currentTypeFilter === 'multimodal' ? '多模态交互应用' : currentTypeFilter === 'voice' ? '语音交互应用' : '应用管理'}</div>
<div class="page-desc">${currentTypeFilter === 'multimodal' ? '管理多模态交互应用,跟踪版本状态与性能数据' : currentTypeFilter === 'voice' ? '管理语音交互应用,跟踪版本状态与性能数据' : '管理多模态交互应用与语音交互应用,跟踪版本状态与性能数据'}</div>
</div>
<div class="page-actions">
<button class="btn btn-outline" id="openAppRecycleBtn">
<i data-lucide="archive-restore"></i> 回收站
<span class="app-recycle-count" id="appRecycleCount">${getRecycledApps().length}</span>
</button>
<button class="btn btn-primary" id="createAppBtn">
<i data-lucide="plus"></i> 创建应用
</button>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-tabs" id="filterTabs">
<button class="filter-tab active" data-filter="all">全部<span class="filter-count">${typeScopedApps.length}</span></button>
<button class="filter-tab" data-filter="draft">草稿<span class="filter-count">${typeScopedApps.filter(a=>a.status==='draft').length}</span></button>
<button class="filter-tab" data-filter="editing">已发布·编辑中<span class="filter-count">${typeScopedApps.filter(a=>a.status==='editing').length}</span></button>
<button class="filter-tab" data-filter="published">已发布<span class="filter-count">${typeScopedApps.filter(a=>a.status==='published').length}</span></button>
</div>
<div class="filter-search">
<i data-lucide="search" style="width:14px;height:14px;color:var(--text-muted)"></i>
<input type="text" placeholder="搜索应用名称 / ID" id="appSearchInput">
</div>
</div>
<!-- 应用网格 -->
<div class="app-grid" id="appGrid"></div>
`;
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: `
<p style="margin-bottom:12px">确定要将应用 <strong>${appName}</strong> 移入回收站吗?</p>
<div class="app-recycle-note">
<i data-lucide="info"></i>
<div>
<strong>保留 ${RECYCLE_RETENTION_DAYS} 天</strong>
<span>移入回收站后应用会从正常列表隐藏,${RECYCLE_RETENTION_DAYS} 天内可恢复;到期后系统自动清理。该操作会在消息通知中提醒管理员。</span>
</div>
</div>
`,
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: `
<div class="app-recycle-panel">
<div class="app-recycle-summary">
<div>
<strong>${recycledApps.length}</strong>
<span>个应用等待清理</span>
</div>
<p>回收站仅保留 ${RECYCLE_RETENTION_DAYS} 天。恢复后应用会回到原公司和原类型列表;彻底删除后前端 mock 中不再可恢复。</p>
</div>
${recycledApps.length === 0 ? `
<div class="empty-state app-recycle-empty">
<i data-lucide="archive-restore" style="width:40px;height:40px;color:var(--text-muted);margin-bottom:12px"></i>
<p style="font-size:15px;font-weight:600;margin-bottom:6px">回收站为空</p>
<p>被删除的交互应用会在这里保留 ${RECYCLE_RETENTION_DAYS} 天。</p>
</div>
` : `
<div class="app-recycle-list">
${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 `
<div class="app-recycle-item" data-recycle-app="${app.id}">
<div class="app-recycle-item-main">
<div class="app-recycle-icon" style="background:${tp.bg};color:${tp.color}">
<i data-lucide="${tp.icon}"></i>
</div>
<div>
<div class="app-recycle-name">${app.name}</div>
<div class="app-recycle-meta">
<span>${tp.label}</span>
<span>${getAppCompanyName(app.companyId)}</span>
<span>ID: ${app.id}</span>
</div>
<div class="app-recycle-time">
删除时间:${deletedAt} · 到期清理:${expireAt} · ${getRecycleRemainingText(app)}
</div>
</div>
</div>
<div class="app-recycle-actions">
<button class="btn btn-outline btn-sm" data-recycle-action="restore" data-id="${app.id}">
<i data-lucide="rotate-ccw"></i> 恢复
</button>
<button class="btn btn-ghost btn-sm danger" data-recycle-action="purge" data-id="${app.id}">
<i data-lucide="trash-2"></i> 彻底删除
</button>
</div>
</div>
`;
}).join('')}
</div>
`}
</div>
`,
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: `<p style="margin-bottom:12px">确定要彻底删除 <strong>${app.name}</strong> 吗?</p><p style="color:var(--danger);font-size:13px">彻底删除后将从回收站移除,前端 mock 中不可恢复。后端接入时应同步删除应用配置或进入异步清理任务。</p>`,
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: `
<div style="margin-bottom:16px">
<div class="form-label" style="margin-bottom:6px">App ID</div>
<div style="display:flex;gap:8px;align-items:center">
<code style="flex:1;background:#f5f5f5;padding:8px 12px;border-radius:6px;font-size:13px;border:1px solid var(--border)">${appId}</code>
<button class="btn btn-outline btn-sm" onclick="copyText('${appId}', '已复制 App ID')">
<i data-lucide="copy"></i>
</button>
</div>
</div>
<div style="margin-bottom:16px">
<div class="form-label" style="margin-bottom:6px">API Key</div>
<div style="display:flex;gap:8px;align-items:center">
<code style="flex:1;background:#f5f5f5;padding:8px 12px;border-radius:6px;font-size:13px;border:1px solid var(--border)">sk-tq-••••••••••••••••••••••••</code>
<button class="btn btn-outline btn-sm" onclick="Toast.info('请在系统设置中管理 API Key')">
<i data-lucide="eye"></i>
</button>
</div>
</div>
<div class="api-note" style="margin-top:0">
<i data-lucide="info"></i>
<div>访问文档:<code>POST https://api.talkingq.com/v1/chat/completions</code>Header 携带 <code>Authorization: Bearer &lt;API_KEY&gt;</code></div>
</div>
`,
});
setTimeout(() => lucide.createIcons(), 100);
};
function showCreateAppModal() {
const defaultType = currentTypeFilter === 'voice' ? 'voice' : 'multimodal';
const selectableCompanies = getSelectableAppCompanies();
const defaultCompanyId = getDefaultAppCompanyId();
createModal({
title: '创建应用',
content: `
<div class="form-group">
<label class="form-label">应用类型<span class="required">*</span></label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:4px">
<label class="app-type-selector ${defaultType === 'multimodal' ? 'selected' : ''}" data-type="multimodal" id="typeMultimodal">
<input type="radio" name="appType" value="multimodal" ${defaultType === 'multimodal' ? 'checked' : ''} style="display:none">
<div style="display:flex;align-items:center;gap:10px">
<div style="width:36px;height:36px;border-radius:8px;background:#f9f0ff;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<i data-lucide="monitor-smartphone" style="color:#722ed1;width:18px;height:18px"></i>
</div>
<div>
<div style="font-size:13px;font-weight:600">多模态交互</div>
<div style="font-size:11px;color:var(--text-muted)">支持图文/语音/视频</div>
</div>
</div>
</label>
<label class="app-type-selector ${defaultType === 'voice' ? 'selected' : ''}" data-type="voice" id="typeVoice">
<input type="radio" name="appType" value="voice" ${defaultType === 'voice' ? 'checked' : ''} style="display:none">
<div style="display:flex;align-items:center;gap:10px">
<div style="width:36px;height:36px;border-radius:8px;background:#e6f7ff;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<i data-lucide="mic" style="color:#1890ff;width:18px;height:18px"></i>
</div>
<div>
<div style="font-size:13px;font-weight:600">语音交互</div>
<div style="font-size:11px;color:var(--text-muted)">纯语音对话场景</div>
</div>
</div>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">所属公司<span class="required">*</span></label>
<select class="form-control" id="newAppCompany">
${selectableCompanies.map(company => `<option value="${company.id}" ${company.id === defaultCompanyId ? 'selected' : ''}>${company.name}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">应用名称<span class="required">*</span></label>
<input type="text" class="form-control" id="newAppName" placeholder="例:科学探索小达人">
</div>
<div class="form-group">
<label class="form-label">应用描述</label>
<textarea class="form-control" id="newAppDesc" placeholder="简要描述应用的功能和目标用户" style="height:72px"></textarea>
</div>
`,
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');
});