957 lines
36 KiB
JavaScript
957 lines
36 KiB
JavaScript
// ============================================================
|
||
// 状态管理
|
||
// ============================================================
|
||
const state = {
|
||
docs: [],
|
||
page: 1,
|
||
size: 10,
|
||
total: 0,
|
||
currentInstanceId: null,
|
||
history: [],
|
||
historyPage: 1,
|
||
historySize: 10,
|
||
historyTotal: 0,
|
||
pollInterval: null,
|
||
currentTaskId: null,
|
||
genInterfaces: [],
|
||
};
|
||
|
||
// ============================================================
|
||
// DOM 引用
|
||
// ============================================================
|
||
const docList = document.getElementById('docList');
|
||
const listCount = document.getElementById('listCount');
|
||
const pagination = document.getElementById('pagination');
|
||
const pageInfo = document.getElementById('pageInfo');
|
||
const prevPage = document.getElementById('prevPage');
|
||
const nextPage = document.getElementById('nextPage');
|
||
|
||
// 添加文档
|
||
const addDocBtn = document.getElementById('addDocBtn');
|
||
const addDocModal = document.getElementById('addDocModal');
|
||
const addDocModalClose = document.getElementById('addDocModalClose');
|
||
const addCancelBtn = document.getElementById('addCancelBtn');
|
||
const addSubmitBtn = document.getElementById('addSubmitBtn');
|
||
const addForm = document.getElementById('addDocForm');
|
||
const addUploadZone = document.getElementById('addUploadZone');
|
||
const addFileInput = document.getElementById('addFileInput');
|
||
const addFileInfo = document.getElementById('addFileInfo');
|
||
const addFileName = document.getElementById('addFileName');
|
||
const addRemoveFile = document.getElementById('addRemoveFile');
|
||
|
||
// 修改文档
|
||
const editDocModal = document.getElementById('editDocModal');
|
||
const editDocModalClose = document.getElementById('editDocModalClose');
|
||
const editCancelBtn = document.getElementById('editCancelBtn');
|
||
const editSubmitBtn = document.getElementById('editSubmitBtn');
|
||
const editForm = document.getElementById('editDocForm');
|
||
const editInstanceId = document.getElementById('editInstanceId');
|
||
const editDesc = document.getElementById('editDesc');
|
||
const editRefinedDocRaw = document.getElementById('editRefinedDocRaw');
|
||
const editRefinedDocPreview = document.getElementById('editRefinedDocPreview');
|
||
const editInterfaceList = document.getElementById('editInterfaceList');
|
||
const toggleMarkdownView = document.getElementById('toggleMarkdownView');
|
||
|
||
// 生成代码
|
||
const generateModal = document.getElementById('generateModal');
|
||
const generateModalClose = document.getElementById('generateModalClose');
|
||
const genCancelBtn = document.getElementById('genCancelBtn');
|
||
const genSubmitBtn = document.getElementById('genSubmitBtn');
|
||
const genForm = document.getElementById('generateForm');
|
||
const genInstanceId = document.getElementById('genInstanceId');
|
||
const genInterfacesList = document.getElementById('genInterfacesList');
|
||
const genInstanceIdHidden = document.getElementById('genInstanceIdHidden');
|
||
const genCodeType = document.getElementById('genCodeType');
|
||
const genSdkName = document.getElementById('genSdkName');
|
||
const genGenerateType = document.getElementById('genGenerateType');
|
||
const genApiKey = document.getElementById('genApiKey');
|
||
const genBaseUrl = document.getElementById('genBaseUrl');
|
||
const genModel = document.getElementById('genModel');
|
||
const genMsgContent = document.getElementById('genMsgContent');
|
||
const genMesSendToken = document.getElementById('genMesSendToken');
|
||
|
||
// 历史任务
|
||
const historyModal = document.getElementById('historyModal');
|
||
const historyModalClose = document.getElementById('historyModalClose');
|
||
const historyInstanceId = document.getElementById('historyInstanceId');
|
||
const historyList = document.getElementById('historyList');
|
||
const historyPagination = document.getElementById('historyPagination');
|
||
const historyPageInfo = document.getElementById('historyPageInfo');
|
||
const historyPrevPage = document.getElementById('historyPrevPage');
|
||
const historyNextPage = document.getElementById('historyNextPage');
|
||
const refreshHistoryBtn = document.getElementById('refreshHistoryBtn');
|
||
|
||
// 任务详情
|
||
const taskDetailModal = document.getElementById('taskDetailModal');
|
||
const taskDetailModalClose = document.getElementById('taskDetailModalClose');
|
||
const taskDetailId = document.getElementById('taskDetailId');
|
||
const taskDetailContent = document.getElementById('taskDetailContent');
|
||
|
||
// 用量详情
|
||
const usageModal = document.getElementById('usageModal');
|
||
const usageModalClose = document.getElementById('usageModalClose');
|
||
const usageContent = document.getElementById('usageContent');
|
||
|
||
// 刷新
|
||
const refreshListBtn = document.getElementById('refreshListBtn');
|
||
|
||
// ============================================================
|
||
// 工具函数
|
||
// ============================================================
|
||
function formatTime(isoStr) {
|
||
if (!isoStr) return '-';
|
||
const d = new Date(isoStr);
|
||
return d.toLocaleString('zh-CN');
|
||
}
|
||
|
||
function getGenerateTypeLabel(type) {
|
||
const map = {
|
||
1: '客户端 SDK',
|
||
2: '服务端骨架',
|
||
};
|
||
return map[type] || type || '-';
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function parseInterfaces(interfaceStr) {
|
||
if (!interfaceStr) return [];
|
||
try {
|
||
return typeof interfaceStr === 'string' ? JSON.parse(interfaceStr) : interfaceStr;
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 文档列表
|
||
// ============================================================
|
||
async function loadDocs(page = state.page) {
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('page', String(page));
|
||
formData.append('page_size', String(state.size));
|
||
|
||
const response = await fetch('/api/v1/refine/list', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '加载失败');
|
||
}
|
||
|
||
state.docs = data.data.data || [];
|
||
state.total = data.data.total || 0;
|
||
state.page = page;
|
||
|
||
renderDocs();
|
||
renderPagination();
|
||
|
||
} catch (err) {
|
||
console.error('加载列表失败:', err);
|
||
docList.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderDocs() {
|
||
if (state.docs.length === 0) {
|
||
docList.innerHTML = '<div class="list-empty">暂无文档实例,点击「添加文档实例」创建</div>';
|
||
listCount.textContent = '共 0 个';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
state.docs.forEach(doc => {
|
||
const interfaces = parseInterfaces(doc.interface);
|
||
const interfaceTags = interfaces.map(i =>
|
||
`<span class="tag">${i.method || 'POST'} ${escapeHtml(i.summary || '')}</span>`
|
||
).join('');
|
||
|
||
html += `
|
||
<div class="doc-item" data-instance-id="${doc.instance_id}">
|
||
<div class="doc-info">
|
||
<div class="doc-name">${escapeHtml(doc.desc || '未命名')}</div>
|
||
<div class="doc-meta">
|
||
<span>🆔 ${doc.instance_id}</span>
|
||
<span>📅 ${formatTime(doc.created_at)}</span>
|
||
<span>📊 ${interfaces.length} 个接口</span>
|
||
<span>${doc.status === 1 ? '✅ 有效' : '❌ 无效'}</span>
|
||
</div>
|
||
<div class="doc-interfaces">${interfaceTags}</div>
|
||
</div>
|
||
<div class="doc-actions">
|
||
<button class="btn-edit" data-instance-id="${doc.instance_id}">✏️ 修改</button>
|
||
<button class="btn-generate" data-instance-id="${doc.instance_id}">🚀 生成</button>
|
||
<button class="btn-history" data-instance-id="${doc.instance_id}">📋 历史</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
docList.innerHTML = html;
|
||
listCount.textContent = `共 ${state.total} 个`;
|
||
|
||
docList.querySelectorAll('.btn-edit').forEach(btn => {
|
||
btn.addEventListener('click', () => openEditModal(btn.dataset.instanceId));
|
||
});
|
||
docList.querySelectorAll('.btn-generate').forEach(btn => {
|
||
btn.addEventListener('click', () => openGenerateModal(btn.dataset.instanceId));
|
||
});
|
||
docList.querySelectorAll('.btn-history').forEach(btn => {
|
||
btn.addEventListener('click', () => openHistoryModal(btn.dataset.instanceId));
|
||
});
|
||
}
|
||
|
||
function renderPagination() {
|
||
const totalPages = Math.ceil(state.total / state.size);
|
||
if (totalPages <= 1) {
|
||
pagination.style.display = 'none';
|
||
return;
|
||
}
|
||
pagination.style.display = 'flex';
|
||
pageInfo.textContent = `${state.page} / ${totalPages}`;
|
||
prevPage.disabled = state.page <= 1;
|
||
nextPage.disabled = state.page >= totalPages;
|
||
}
|
||
|
||
prevPage.addEventListener('click', () => {
|
||
if (state.page > 1) loadDocs(state.page - 1);
|
||
});
|
||
|
||
nextPage.addEventListener('click', () => {
|
||
const totalPages = Math.ceil(state.total / state.size);
|
||
if (state.page < totalPages) loadDocs(state.page + 1);
|
||
});
|
||
|
||
refreshListBtn.addEventListener('click', () => loadDocs(state.page));
|
||
|
||
// ============================================================
|
||
// 添加文档实例
|
||
// ============================================================
|
||
let addFileData = null;
|
||
|
||
addDocBtn.addEventListener('click', () => {
|
||
addFileData = null;
|
||
addFileInfo.style.display = 'none';
|
||
addUploadZone.style.display = 'block';
|
||
addFileInput.value = '';
|
||
addForm.reset();
|
||
addSubmitBtn.disabled = false;
|
||
addSubmitBtn.querySelector('.btn-text').textContent = '📝 精炼并保存';
|
||
addSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
|
||
addDocModal.style.display = 'flex';
|
||
});
|
||
|
||
addDocModalClose.addEventListener('click', () => { addDocModal.style.display = 'none'; });
|
||
addCancelBtn.addEventListener('click', () => { addDocModal.style.display = 'none'; });
|
||
|
||
addUploadZone.addEventListener('click', () => addFileInput.click());
|
||
addUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); addUploadZone.classList.add('dragover'); });
|
||
addUploadZone.addEventListener('dragleave', () => { addUploadZone.classList.remove('dragover'); });
|
||
addUploadZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
addUploadZone.classList.remove('dragover');
|
||
if (e.dataTransfer.files.length > 0) {
|
||
handleAddFile(e.dataTransfer.files[0]);
|
||
}
|
||
});
|
||
|
||
addFileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) {
|
||
handleAddFile(e.target.files[0]);
|
||
}
|
||
});
|
||
|
||
addRemoveFile.addEventListener('click', () => {
|
||
addFileData = null;
|
||
addFileInfo.style.display = 'none';
|
||
addUploadZone.style.display = 'block';
|
||
addFileInput.value = '';
|
||
});
|
||
|
||
function handleAddFile(file) {
|
||
const validTypes = ['.md', '.txt', '.doc', '.docx', '.pdf'];
|
||
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
||
if (!validTypes.includes(ext)) {
|
||
alert('不支持的文件类型');
|
||
return;
|
||
}
|
||
if (file.size > 50 * 1024 * 1024) {
|
||
alert('文件大小超过 50MB');
|
||
return;
|
||
}
|
||
addFileData = file;
|
||
addFileName.textContent = file.name;
|
||
addFileInfo.style.display = 'flex';
|
||
addUploadZone.style.display = 'none';
|
||
}
|
||
|
||
addForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (!addFileData) {
|
||
alert('请上传文档文件');
|
||
return;
|
||
}
|
||
|
||
const apiKey = document.getElementById('addApiKey').value.trim();
|
||
const baseUrl = document.getElementById('addBaseUrl').value.trim();
|
||
const model = document.getElementById('addModel').value.trim();
|
||
const desc = document.getElementById('addDesc').value.trim();
|
||
|
||
if (!apiKey) { alert('请输入 API Key'); return; }
|
||
if (!baseUrl) { alert('请输入 Base URL'); return; }
|
||
if (!model) { alert('请输入大模型名称'); return; }
|
||
|
||
addSubmitBtn.disabled = true;
|
||
addSubmitBtn.querySelector('.btn-text').textContent = '精炼中...';
|
||
addSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', addFileData);
|
||
formData.append('llm_api_key', apiKey);
|
||
formData.append('llm_base_url', baseUrl);
|
||
formData.append('llm_model', model);
|
||
formData.append('desc', desc || addFileData.name);
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/refine', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '精炼失败');
|
||
}
|
||
|
||
alert('✅ 文档精炼成功!');
|
||
addDocModal.style.display = 'none';
|
||
loadDocs(state.page);
|
||
|
||
} catch (err) {
|
||
alert('❌ ' + err.message);
|
||
} finally {
|
||
addSubmitBtn.disabled = false;
|
||
addSubmitBtn.querySelector('.btn-text').textContent = '📝 精炼并保存';
|
||
addSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 修改文档(接口列表只读展示,Markdown 渲染)
|
||
// ============================================================
|
||
let editInterfaces = [];
|
||
let showRawMarkdown = false;
|
||
|
||
async function openEditModal(instanceId) {
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('page', '1');
|
||
formData.append('page_size', '100');
|
||
formData.append('instance_id', instanceId);
|
||
|
||
const response = await fetch('/api/v1/refine/list', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200 || !data.data.data || data.data.data.length === 0) {
|
||
throw new Error('文档不存在');
|
||
}
|
||
|
||
const doc = data.data.data[0];
|
||
state.currentInstanceId = instanceId;
|
||
editInstanceId.textContent = 'ID: ' + instanceId;
|
||
editDesc.value = doc.desc || '';
|
||
|
||
// 接口列表只读展示
|
||
editInterfaces = parseInterfaces(doc.interface);
|
||
renderEditInterfaces();
|
||
|
||
// Markdown 渲染精炼文档
|
||
const refinedDoc = doc.refined_doc || '';
|
||
editRefinedDocPreview.innerHTML = renderMarkdown(refinedDoc);
|
||
editRefinedDocRaw.value = refinedDoc;
|
||
|
||
// 默认显示预览,隐藏源码
|
||
showRawMarkdown = false;
|
||
editRefinedDocPreview.style.display = 'block';
|
||
editRefinedDocRaw.style.display = 'none';
|
||
if (toggleMarkdownView) {
|
||
toggleMarkdownView.textContent = '✏️ 修改';
|
||
}
|
||
|
||
editDocModal.style.display = 'flex';
|
||
|
||
} catch (err) {
|
||
alert('加载文档失败: ' + err.message);
|
||
}
|
||
}
|
||
|
||
function renderEditInterfaces() {
|
||
const container = editInterfaceList;
|
||
container.innerHTML = '';
|
||
if (!editInterfaces || editInterfaces.length === 0) {
|
||
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">暂无接口</span>';
|
||
return;
|
||
}
|
||
editInterfaces.forEach(iface => {
|
||
const tag = document.createElement('span');
|
||
tag.className = 'tag';
|
||
tag.innerHTML = `
|
||
<span class="method">${escapeHtml(iface.method || 'POST')}</span>
|
||
${escapeHtml(iface.summary || '')}
|
||
`;
|
||
container.appendChild(tag);
|
||
});
|
||
}
|
||
|
||
// Markdown 渲染(支持标题、代码块、表格、列表)
|
||
function renderMarkdown(text) {
|
||
if (!text) return '<span style="color:#9ca3af;">暂无内容</span>';
|
||
|
||
let html = text;
|
||
|
||
// 代码块 ``` ... ```
|
||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
|
||
return `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
|
||
});
|
||
|
||
// 标题
|
||
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
|
||
|
||
// 表格
|
||
html = html.replace(/\|(.+)\|\n\|[-:| ]+\|\n((?:\|.+\|\n?)*)/g, (match, header, rows) => {
|
||
const headers = header.split('|').filter(h => h.trim());
|
||
const headerHtml = `<thead><tr>${headers.map(h => `<th>${h.trim()}</th>`).join('')}</tr></thead>`;
|
||
const bodyRows = rows.split('\n').filter(r => r.trim()).map(row => {
|
||
const cells = row.split('|').filter(c => c.trim());
|
||
return `<tr>${cells.map(c => `<td>${c.trim()}</td>`).join('')}</tr>`;
|
||
});
|
||
return `<table>${headerHtml}<tbody>${bodyRows.join('')}</tbody></table>`;
|
||
});
|
||
|
||
// 列表
|
||
html = html.replace(/^- (.*$)/gm, '<li>$1</li>');
|
||
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
||
|
||
// 粗体
|
||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||
|
||
// 换行
|
||
html = html.replace(/\n/g, '<br>');
|
||
|
||
return html;
|
||
}
|
||
|
||
// 切换 Markdown 源码/预览(修改按钮)
|
||
if (toggleMarkdownView) {
|
||
toggleMarkdownView.addEventListener('click', function() {
|
||
showRawMarkdown = !showRawMarkdown;
|
||
if (showRawMarkdown) {
|
||
editRefinedDocPreview.style.display = 'none';
|
||
editRefinedDocRaw.style.display = 'block';
|
||
this.textContent = '📄 预览';
|
||
} else {
|
||
editRefinedDocPreview.style.display = 'block';
|
||
editRefinedDocRaw.style.display = 'none';
|
||
// 重新渲染预览
|
||
editRefinedDocPreview.innerHTML = renderMarkdown(editRefinedDocRaw.value);
|
||
this.textContent = '✏️ 修改';
|
||
}
|
||
});
|
||
}
|
||
|
||
editDocModalClose.addEventListener('click', () => { editDocModal.style.display = 'none'; });
|
||
editCancelBtn.addEventListener('click', () => { editDocModal.style.display = 'none'; });
|
||
|
||
editForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const refinedDoc = editRefinedDocRaw.value.trim();
|
||
const desc = editDesc.value.trim();
|
||
|
||
editSubmitBtn.disabled = true;
|
||
editSubmitBtn.querySelector('.btn-text').textContent = '保存中...';
|
||
editSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
|
||
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('instance_id', state.currentInstanceId);
|
||
if (refinedDoc) formData.append('refined_doc', refinedDoc);
|
||
if (desc) formData.append('desc', desc);
|
||
|
||
const response = await fetch('/api/v1/refine/update', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '保存失败');
|
||
}
|
||
|
||
alert('✅ 保存成功!');
|
||
editDocModal.style.display = 'none';
|
||
loadDocs(state.page);
|
||
|
||
} catch (err) {
|
||
alert('❌ ' + err.message);
|
||
} finally {
|
||
editSubmitBtn.disabled = false;
|
||
editSubmitBtn.querySelector('.btn-text').textContent = '💾 保存修改';
|
||
editSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 生成代码(接口可勾选,默认全选)
|
||
// ============================================================
|
||
async function openGenerateModal(instanceId) {
|
||
state.currentInstanceId = instanceId;
|
||
genInstanceId.textContent = 'ID: ' + instanceId;
|
||
genInstanceIdHidden.value = instanceId;
|
||
|
||
genCodeType.value = 'go';
|
||
genForm.reset();
|
||
genSubmitBtn.disabled = false;
|
||
genSubmitBtn.querySelector('.btn-text').textContent = '🚀 生成代码';
|
||
genSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
|
||
|
||
// 加载接口列表(可勾选)
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('page', '1');
|
||
formData.append('page_size', '100');
|
||
formData.append('instance_id', instanceId);
|
||
|
||
const response = await fetch('/api/v1/refine/list', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
const container = genInterfacesList;
|
||
container.innerHTML = '';
|
||
|
||
if (data.code === 200 && data.data.data && data.data.data.length > 0) {
|
||
const doc = data.data.data[0];
|
||
const interfaces = parseInterfaces(doc.interface);
|
||
state.genInterfaces = interfaces;
|
||
|
||
if (interfaces.length === 0) {
|
||
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">暂无接口</span>';
|
||
} else {
|
||
// 默认全选
|
||
interfaces.forEach((iface, index) => {
|
||
const label = document.createElement('label');
|
||
label.className = 'checkbox-item checked';
|
||
label.innerHTML = `
|
||
<input type="checkbox" checked data-index="${index}" />
|
||
<span class="method-tag">${escapeHtml(iface.method || 'POST')}</span>
|
||
<span class="summary-text">${escapeHtml(iface.summary || '')}</span>
|
||
`;
|
||
const checkbox = label.querySelector('input[type="checkbox"]');
|
||
checkbox.addEventListener('change', function() {
|
||
label.classList.toggle('checked', this.checked);
|
||
});
|
||
container.appendChild(label);
|
||
});
|
||
}
|
||
} else {
|
||
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">加载接口列表失败</span>';
|
||
}
|
||
} catch (err) {
|
||
console.error('加载接口列表失败:', err);
|
||
genInterfacesList.innerHTML = '<span style="color:#9ca3af;font-size:13px;">加载接口列表失败</span>';
|
||
}
|
||
|
||
generateModal.style.display = 'flex';
|
||
}
|
||
|
||
generateModalClose.addEventListener('click', () => { generateModal.style.display = 'none'; });
|
||
genCancelBtn.addEventListener('click', () => { generateModal.style.display = 'none'; });
|
||
|
||
genForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const instanceId = genInstanceIdHidden.value;
|
||
const sdkName = genSdkName.value.trim();
|
||
const generateType = genGenerateType.value;
|
||
const apiKey = genApiKey.value.trim();
|
||
const baseUrl = genBaseUrl.value.trim();
|
||
const model = genModel.value.trim();
|
||
const msgContent = genMsgContent.value.trim();
|
||
const mesSendToken = genMesSendToken.value.trim();
|
||
|
||
// 获取选中的接口
|
||
const checkboxes = document.querySelectorAll('#genInterfacesList input[type="checkbox"]:checked');
|
||
const selectedInterfaces = [];
|
||
checkboxes.forEach(cb => {
|
||
const idx = parseInt(cb.dataset.index);
|
||
if (state.genInterfaces && state.genInterfaces[idx]) {
|
||
selectedInterfaces.push(state.genInterfaces[idx]);
|
||
}
|
||
});
|
||
|
||
if (selectedInterfaces.length === 0) {
|
||
alert('请至少选择一个接口');
|
||
return;
|
||
}
|
||
|
||
if (!instanceId) { alert('文档实例ID不能为空'); return; }
|
||
if (!sdkName) { alert('请输入仓库名称'); return; }
|
||
if (!apiKey) { alert('请输入 API Key'); return; }
|
||
if (!baseUrl) { alert('请输入 Base URL'); return; }
|
||
if (!model) { alert('请输入大模型名称'); return; }
|
||
|
||
genSubmitBtn.disabled = true;
|
||
genSubmitBtn.querySelector('.btn-text').textContent = '提交中...';
|
||
genSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
|
||
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('instance_id', instanceId);
|
||
formData.append('llm_api_key', apiKey);
|
||
formData.append('llm_base_url', baseUrl);
|
||
formData.append('llm_model', model);
|
||
formData.append('desc', sdkName);
|
||
formData.append('code_type', 'go');
|
||
formData.append('generate_type', generateType);
|
||
formData.append('interfaces', JSON.stringify(selectedInterfaces));
|
||
if (msgContent) formData.append('msg_content', msgContent);
|
||
if (mesSendToken) formData.append('mes_send_token', mesSendToken);
|
||
|
||
const response = await fetch('/api/v1/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '提交失败');
|
||
}
|
||
|
||
const taskId = data.data?.task_id || data.task_id;
|
||
alert(`✅ 任务已提交!任务ID: ${taskId}`);
|
||
generateModal.style.display = 'none';
|
||
loadDocs(state.page);
|
||
|
||
} catch (err) {
|
||
alert('❌ ' + err.message);
|
||
} finally {
|
||
genSubmitBtn.disabled = false;
|
||
genSubmitBtn.querySelector('.btn-text').textContent = '🚀 生成代码';
|
||
genSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 历史任务列表(美化版)
|
||
// ============================================================
|
||
async function openHistoryModal(instanceId) {
|
||
state.currentInstanceId = instanceId;
|
||
historyInstanceId.textContent = 'ID: ' + instanceId;
|
||
state.historyPage = 1;
|
||
historyModal.style.display = 'flex';
|
||
await loadHistory(instanceId, 1);
|
||
}
|
||
|
||
historyModalClose.addEventListener('click', () => { historyModal.style.display = 'none'; });
|
||
refreshHistoryBtn.addEventListener('click', () => {
|
||
if (state.currentInstanceId) {
|
||
loadHistory(state.currentInstanceId, state.historyPage);
|
||
}
|
||
});
|
||
|
||
async function loadHistory(instanceId, page = 1) {
|
||
try {
|
||
const formData = new URLSearchParams();
|
||
formData.append('page', String(page));
|
||
formData.append('page_size', String(state.historySize));
|
||
formData.append('task_id', instanceId);
|
||
|
||
const response = await fetch('/api/v1/tasks/list', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: formData.toString(),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '加载失败');
|
||
}
|
||
|
||
state.history = data.data.data || [];
|
||
state.historyTotal = data.data.total || 0;
|
||
state.historyPage = page;
|
||
|
||
renderHistory();
|
||
|
||
} catch (err) {
|
||
historyList.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderHistory() {
|
||
if (state.history.length === 0) {
|
||
historyList.innerHTML = '<div class="list-empty">暂无历史任务</div>';
|
||
historyPagination.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
state.history.forEach(task => {
|
||
const statusClass = task.task_status === 'completed' ? 'completed' :
|
||
task.task_status === 'failed' ? 'failed' :
|
||
task.task_status === 'pending' ? 'pending' : 'processing';
|
||
const interfaces = parseInterfaces(task.interfaces);
|
||
|
||
let usageSummary = '';
|
||
let usageDetail = null;
|
||
try {
|
||
const ua = typeof task.use_age === 'string' ? JSON.parse(task.use_age) : task.use_age;
|
||
if (ua) {
|
||
usageSummary = `总: ${ua.total_tokens || 0}`;
|
||
usageDetail = ua;
|
||
}
|
||
} catch {}
|
||
|
||
html += `
|
||
<div class="history-item" data-task-id="${task.task_id}" data-usage='${JSON.stringify(usageDetail || {})}'>
|
||
<div class="h-info">
|
||
<div class="h-id">${task.task_id}</div>
|
||
<div class="h-meta">
|
||
<span>📅 ${formatTime(task.created_at)}</span>
|
||
<span>📦 ${escapeHtml(task.desc || '-')}</span>
|
||
<span>🤖 ${escapeHtml(task.llm_model || '-')}</span>
|
||
<span>📊 ${interfaces.length} 个接口</span>
|
||
<span>📋 ${getGenerateTypeLabel(task.generate_type)}</span>
|
||
<span>💻 ${escapeHtml(task.code_type || 'go')}</span>
|
||
${task.repo_url ? `<span>🔗 <a href="${task.repo_url}" target="_blank" onclick="event.stopPropagation();">仓库</a></span>` : ''}
|
||
${usageSummary ? `<span class="usage-badge" onclick="event.stopPropagation();showUsageDetail(this)">📊 ${usageSummary}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="h-status ${statusClass}">${task.task_status}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
historyList.innerHTML = html;
|
||
|
||
historyList.querySelectorAll('.history-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const taskId = item.dataset.taskId;
|
||
openTaskDetail(taskId);
|
||
});
|
||
});
|
||
|
||
const totalPages = Math.ceil(state.historyTotal / state.historySize);
|
||
if (totalPages <= 1) {
|
||
historyPagination.style.display = 'none';
|
||
} else {
|
||
historyPagination.style.display = 'flex';
|
||
historyPageInfo.textContent = `${state.historyPage} / ${totalPages}`;
|
||
historyPrevPage.disabled = state.historyPage <= 1;
|
||
historyNextPage.disabled = state.historyPage >= totalPages;
|
||
}
|
||
}
|
||
|
||
historyPrevPage.addEventListener('click', () => {
|
||
if (state.historyPage > 1 && state.currentInstanceId) {
|
||
loadHistory(state.currentInstanceId, state.historyPage - 1);
|
||
}
|
||
});
|
||
|
||
historyNextPage.addEventListener('click', () => {
|
||
const totalPages = Math.ceil(state.historyTotal / state.historySize);
|
||
if (state.historyPage < totalPages && state.currentInstanceId) {
|
||
loadHistory(state.currentInstanceId, state.historyPage + 1);
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 用量详情弹窗(美化版)
|
||
// ============================================================
|
||
function showUsageDetail(el) {
|
||
const item = el.closest('.history-item');
|
||
if (!item) return;
|
||
const usageData = JSON.parse(item.dataset.usage || '{}');
|
||
const content = usageContent;
|
||
|
||
if (!usageData || Object.keys(usageData).length === 0 || usageData.total_tokens === 0) {
|
||
content.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:20px;">暂无用量数据</div>';
|
||
usageModal.style.display = 'flex';
|
||
return;
|
||
}
|
||
|
||
const promptTokens = usageData.prompt_tokens || 0;
|
||
const completionTokens = usageData.completion_tokens || 0;
|
||
const totalTokens = usageData.total_tokens || 0;
|
||
const detail = usageData.detail || [];
|
||
|
||
let detailHtml = '';
|
||
if (detail.length > 0) {
|
||
detailHtml = `
|
||
<div style="margin-top:16px;">
|
||
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px;">分步详情</div>
|
||
<div class="usage-detail-list">
|
||
${detail.map(d => `
|
||
<div class="usage-detail-item">
|
||
<span class="step-name">${escapeHtml(d.state_name || '未知步骤')}</span>
|
||
<span class="step-tokens">${d.total_tokens || 0} tokens</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<div class="usage-stats">
|
||
<div class="usage-stat-card">
|
||
<div class="stat-value">${totalTokens}</div>
|
||
<div class="stat-label">总 Tokens</div>
|
||
</div>
|
||
<div class="usage-stat-card">
|
||
<div class="stat-value">${promptTokens}</div>
|
||
<div class="stat-label">输入 Tokens <span class="sub">(Prompt)</span></div>
|
||
</div>
|
||
<div class="usage-stat-card">
|
||
<div class="stat-value">${completionTokens}</div>
|
||
<div class="stat-label">输出 Tokens <span class="sub">(Completion)</span></div>
|
||
</div>
|
||
</div>
|
||
${detailHtml}
|
||
`;
|
||
usageModal.style.display = 'flex';
|
||
}
|
||
|
||
usageModalClose.addEventListener('click', () => {
|
||
usageModal.style.display = 'none';
|
||
});
|
||
|
||
// 点击弹窗外关闭
|
||
usageModal.addEventListener('click', (e) => {
|
||
if (e.target === usageModal) {
|
||
usageModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 任务详情
|
||
// ============================================================
|
||
async function openTaskDetail(taskId) {
|
||
taskDetailId.textContent = 'ID: ' + taskId;
|
||
taskDetailContent.innerHTML = '<div class="detail-loading">加载中...</div>';
|
||
taskDetailModal.style.display = 'flex';
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/tasks/${taskId}`);
|
||
const data = await response.json();
|
||
if (data.code !== 200) {
|
||
throw new Error(data.message || '加载失败');
|
||
}
|
||
|
||
const task = data.data?.Task || data.data || {};
|
||
renderTaskDetail(task);
|
||
|
||
} catch (err) {
|
||
taskDetailContent.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderTaskDetail(task) {
|
||
const interfaces = parseInterfaces(task.interfaces);
|
||
const interfaceTags = interfaces.map(i =>
|
||
`<span class="tag">${i.method || 'POST'} ${escapeHtml(i.summary || '')}</span>`
|
||
).join('');
|
||
|
||
let useAge = '';
|
||
try {
|
||
const ua = typeof task.use_age === 'string' ? JSON.parse(task.use_age) : task.use_age;
|
||
if (ua) {
|
||
useAge = `
|
||
<div style="margin-top:6px;font-size:13px;color:#6b7280;">
|
||
总 Tokens: ${ua.total_tokens || 0}
|
||
${ua.detail ? ua.detail.map(d =>
|
||
`<span style="margin-left:12px;">${d.state_name || '未知'}: ${d.total_tokens || 0}</span>`
|
||
).join('') : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
} catch {}
|
||
|
||
taskDetailContent.innerHTML = `
|
||
<div class="detail-row">
|
||
<span class="label">任务ID</span>
|
||
<span class="value">${task.task_id}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">状态</span>
|
||
<span class="value"><span class="h-status ${task.task_status === 'completed' ? 'completed' : task.task_status === 'failed' ? 'failed' : 'processing'}">${task.task_status}</span></span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">仓库名称</span>
|
||
<span class="value">${escapeHtml(task.desc || '-')}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">生成类型</span>
|
||
<span class="value">${getGenerateTypeLabel(task.generate_type)}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">代码类型</span>
|
||
<span class="value">${escapeHtml(task.code_type || 'go')}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">大模型</span>
|
||
<span class="value">${escapeHtml(task.llm_model || '-')}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">接口列表</span>
|
||
<span class="value">${interfaceTags || '-'}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">创建时间</span>
|
||
<span class="value">${formatTime(task.created_at)}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">完成时间</span>
|
||
<span class="value">${formatTime(task.completed_at)}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">代码仓库</span>
|
||
<span class="value">${task.repo_url ? `<a href="${task.repo_url}" target="_blank">${task.repo_url}</a>` : '-'}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">错误信息</span>
|
||
<span class="value" style="color:#dc2626;">${escapeHtml(task.error || '-')}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="label">Token 用量</span>
|
||
<span class="value">${useAge || '-'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
taskDetailModalClose.addEventListener('click', () => { taskDetailModal.style.display = 'none'; });
|
||
|
||
// ============================================================
|
||
// 初始化
|
||
// ============================================================
|
||
loadDocs(1); |