798 lines
35 KiB
JavaScript
798 lines
35 KiB
JavaScript
/**
|
||
* 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 <API_KEY></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');
|
||
});
|