Files
Toolbox/public/tools/ai-lib/ai-lib.js
T

1387 lines
52 KiB
JavaScript
Raw 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.
;(function(){
// 全局错误捕获
try {
const el = id => document.getElementById(id)
// 声明 DOM 变量 (将在 init 中赋值)
let $auth, $app
let $systems, $modules, $notes, $attachments
let $moduleNav, $noteNav
let $systemDetail, $moduleDetail, $noteDetail
let $attachmentFile, $btnUploadAttachment, $uploadProgress
// 移除早期的检查,移至 init
let current = {
system: null, module: null, note: null,
systems: [], modules: [], notes: [], attachments: [],
categories: []
}
const apiBase = location.pathname.startsWith('/tools/ai-lib/') ? '/tools/ai-lib/api/ai-lib' : '/api/ai-lib'
// --- 核心逻辑函数 ---
// 登录
async function login() {
try {
const userIn = el('login-user');
const passIn = el('login-pass');
if (!userIn || !passIn) return;
const user = userIn.value.trim()
const pass = passIn.value
const s = el('auth-status');
if (s) { s.textContent = '正在登录...'; s.style.display = 'block'; }
const res = await fetch(apiBase + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: user, password: pass }),
credentials: 'same-origin'
})
const data = await res.json()
if (!data.ok) throw new Error(data.error || `登录失败 (HTTP ${res.status})`)
$auth.style.display = 'none'
$app.style.display = 'block'
if (s) { s.textContent = ''; s.style.display = 'none'; }
await loadCategories()
await loadSystems()
} catch (e) {
const s = el('auth-status');
if (s) {
s.textContent = String(e.message || e);
s.style.display = 'block';
}
alert('登录出错: ' + (e.message || e));
}
}
// 创建系统树形项
function createSystemTreeItem(sys, expanded = false) {
const treeItem = document.createElement('div')
treeItem.className = 'tree-item'
treeItem.dataset.systemId = sys.id
const header = document.createElement('div')
header.className = `tree-item-header ${expanded ? 'expanded' : ''}`
const toggle = document.createElement('span')
toggle.className = `tree-toggle ${expanded ? 'expanded' : ''}`
toggle.textContent = '▶'
const icon = document.createElement('span')
icon.className = 'system-icon'
icon.textContent = '📦'
const title = document.createElement('span')
title.textContent = sys.name || '未命名系统'
title.style.flex = '1'
title.style.fontWeight = '600'
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnEdit = document.createElement('button')
btnEdit.className = 'btn-icon btn-edit'
btnEdit.textContent = '✏️'
btnEdit.onclick = (e) => { e.stopPropagation(); editSystem(sys.id) }
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete'
btnDelete.textContent = '🗑️'
btnDelete.onclick = (e) => { e.stopPropagation(); deleteSystem(sys.id) }
const btnAddChild = document.createElement('button')
btnAddChild.className = 'btn-icon sm'
btnAddChild.textContent = '✂️'
btnAddChild.title = '新增子模块'
btnAddChild.onclick = async (e) => {
e.stopPropagation();
selectSystem(sys)
await newModule()
}
actions.appendChild(btnEdit)
actions.appendChild(btnDelete)
actions.appendChild(btnAddChild)
header.appendChild(toggle)
header.appendChild(icon)
header.appendChild(title)
header.appendChild(actions)
const content = document.createElement('div')
content.className = `tree-item-content ${expanded ? 'expanded' : ''}`
const childrenContainer = document.createElement('div')
childrenContainer.className = 'tree-item-children'
childrenContainer.innerHTML = '<div style="padding: 8px 16px; color: #888; font-style: italic;">加载中...</div>'
content.appendChild(childrenContainer)
treeItem.appendChild(header)
treeItem.appendChild(content)
// 点击展开/收缩
header.onclick = (e) => {
if (e.target === toggle || e.target === header) {
const isExpanded = header.classList.contains('expanded')
header.classList.toggle('expanded')
content.classList.toggle('expanded')
toggle.classList.toggle('expanded')
if (!isExpanded && content.classList.contains('expanded')) {
// 展开时加载子模块
loadSystemModules(sys.id, childrenContainer)
}
}
}
// 点击系统标题选择系统
title.onclick = (e) => {
e.stopPropagation()
selectSystem(sys)
}
return treeItem
}
// 加载系统模块到树形结构
async function loadSystemModules(systemId, container) {
showLoading('正在加载子模块...')
try {
const res = await fetch(`${apiBase}/modules?system_id=${systemId}`, { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
const modules = data.rows || []
const sys = (current.systems || []).find(s => String(s.id) === String(systemId))
container.innerHTML = ''
if (modules.length === 0) {
container.innerHTML = '<div style="padding: 8px 16px; color: #888; font-style: italic;">暂无子模块</div>'
return
}
modules.forEach(mod => {
const moduleItem = createModuleTreeItem(mod, sys)
container.appendChild(moduleItem)
})
} catch (e) {
console.error('加载子模块失败:', e)
container.innerHTML = '<div style="padding: 8px 16px; color: #f44747; font-style: italic;">加载失败</div>'
} finally { hideLoading() }
}
// 创建子模块树形项(可展开显示开发纪要)
function createModuleTreeItem(mod, sys) {
const wrapper = document.createElement('div')
wrapper.className = 'tree-module-wrapper'
wrapper.dataset.moduleId = mod.id
wrapper.dataset.moduleName = String(mod.name || '未命名模块')
if (sys) {
wrapper.dataset.systemId = String(sys.id)
wrapper.dataset.systemName = String(sys.name || '未命名系统')
}
const header = document.createElement('div')
header.className = 'tree-child-item'
header.dataset.moduleId = mod.id
const toggle = document.createElement('span')
toggle.className = 'tree-toggle'
toggle.textContent = '▶'
const icon = document.createElement('span')
icon.className = 'module-icon'
icon.textContent = '🔧'
const title = document.createElement('span')
title.textContent = mod.name || '未命名模块'
title.style.flex = '1'
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnAddModule = document.createElement('button')
btnAddModule.className = 'btn-icon sm'
btnAddModule.textContent = '➕'
btnAddModule.title = '新增纪要'
btnAddModule.onclick = async (e) => {
e.stopPropagation();
if (sys) selectSystem(sys)
selectModule(mod)
await newNote()
const notesContainer = wrapper.querySelector('.tree-note-list')
if (notesContainer) await loadModuleNotes(mod.id, notesContainer)
}
const btnEdit = document.createElement('button')
btnEdit.className = 'btn-icon btn-edit sm'
btnEdit.textContent = '✏️'
btnEdit.title = '编辑子模块'
btnEdit.onclick = (e) => {
e.stopPropagation();
if (sys) selectSystem(sys)
selectModule(mod)
}
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete sm'
btnDelete.textContent = '🗑️'
btnDelete.title = '删除子模块'
btnDelete.onclick = async (e) => {
e.stopPropagation();
await deleteModule(mod.id)
}
const btnAddChild = document.createElement('button')
btnAddChild.className = 'btn-icon sm'
btnAddChild.textContent = '✂️'
btnAddChild.title = '新增纪要'
btnAddChild.onclick = async (e) => {
e.stopPropagation()
if (sys) selectSystem(sys)
selectModule(mod)
await newNote()
const notesContainer = wrapper.querySelector('.tree-note-list')
if (notesContainer) await loadModuleNotes(mod.id, notesContainer)
}
actions.appendChild(btnAddModule)
actions.appendChild(btnEdit)
actions.appendChild(btnDelete)
actions.appendChild(btnAddChild)
header.appendChild(toggle)
header.appendChild(icon)
header.appendChild(title)
header.appendChild(actions)
const content = document.createElement('div')
content.className = 'tree-item-content'
const notesContainer = document.createElement('div')
notesContainer.className = 'tree-note-list'
notesContainer.innerHTML = '<div>加载中...</div>'
content.appendChild(notesContainer)
wrapper.appendChild(header)
wrapper.appendChild(content)
// 展开/收缩
header.onclick = async (e) => {
if (e.target === header || e.target === toggle) {
const isExpanded = content.classList.contains('expanded')
content.classList.toggle('expanded')
toggle.classList.toggle('expanded')
if (!isExpanded) {
await loadModuleNotes(mod.id, notesContainer)
}
}
}
// 点击标题选择模块
title.onclick = (e) => {
e.stopPropagation()
if (sys) selectSystem(sys)
selectModule(mod)
}
return wrapper
}
// 加载系统列表
async function loadSystems() {
showLoading('正在加载系统列表...')
try {
const res = await fetch(apiBase + '/systems', { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
current.systems = data.rows || []
const filterVal = document.querySelector('input[name="filter-major"]:checked')?.value || ''
const filteredSystems = filterVal
? current.systems.filter(s => s.major_category === filterVal)
: current.systems
if ($systems) {
$systems.innerHTML = ''
if (filteredSystems.length === 0) {
$systems.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">暂无系统,请点击右上角"新增系统"创建</div>'
} else {
const treeView = document.createElement('div')
treeView.className = 'tree-view'
filteredSystems.forEach((sys) => {
const treeItem = createSystemTreeItem(sys, false)
treeView.appendChild(treeItem)
})
$systems.appendChild(treeView)
}
}
// 保持当前导航状态,不要在首次加载时清空已渲染的系统列表
} catch (e) {
console.error('加载系统失败:', e)
alert('加载系统列表失败: ' + e.message)
} finally {
hideLoading()
}
}
// 选择系统
function selectSystem(sys) {
current.system = sys
current.module = null
current.note = null
updatePath(`系统: ${sys.name}`)
if ($systemDetail) $systemDetail.style.display = 'block'
if ($moduleDetail) $moduleDetail.style.display = 'none'
if ($noteDetail) $noteDetail.style.display = 'none'
const elName = el('sys-name'), elIntro = el('sys-intro');
if (elName) elName.value = sys.name || ''
if (elIntro) elIntro.value = sys.intro || ''
const radios = document.querySelectorAll('input[name="sys-major"]')
radios.forEach(r => r.checked = false)
if (sys.major_category) {
const target = document.querySelector(`input[name="sys-major"][value="${sys.major_category}"]`)
if (target) target.checked = true
} else {
// Default for legacy data? Or just leave unchecked?
// User requested "Initial data is 'Trae研发...'" but that's for new systems maybe?
// If editing existing system without category, maybe better to leave unchecked or set default.
// Let's check "Trae研发" if none is set, to encourage categorization.
// const target = document.querySelector(`input[name="sys-major"][value="Trae研发"]`)
// if (target) target.checked = true
// Actually, if it's saved without category, showing a default might be misleading if they don't save.
// But for better UX let's leave it unchecked if empty.
}
if ($moduleNav) $moduleNav.style.display = 'none'
if ($noteNav) $noteNav.style.display = 'none'
loadModules(sys.id)
}
// 加载子模块
async function loadModules(systemId) {
showLoading('正在加载子模块...')
try {
const res = await fetch(`${apiBase}/modules?system_id=${systemId}`, { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
current.modules = data.rows || []
if ($modules) {
$modules.innerHTML = ''
current.modules.forEach(mod => {
const item = document.createElement('div')
item.className = 'module-item'
const content = document.createElement('div')
content.innerHTML = `
<strong>🔧 ${mod.name || '未命名模块'}</strong><br>
<small>${mod.description || '暂无说明'}</small>
`
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnEdit = document.createElement('button')
btnEdit.className = 'btn-icon btn-edit'
btnEdit.textContent = '✏️'
btnEdit.onclick = (e) => { e.stopPropagation(); editModule(mod.id) }
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete'
btnDelete.textContent = '🗑️'
btnDelete.onclick = (e) => { e.stopPropagation(); deleteModule(mod.id) }
actions.appendChild(btnEdit)
actions.appendChild(btnDelete)
item.appendChild(content)
item.appendChild(actions)
item.onclick = () => selectModule(mod)
$modules.appendChild(item)
})
}
} catch (e) {
console.error('加载子模块失败:', e)
alert('加载子模块失败: ' + e.message)
} finally {
hideLoading()
}
}
// 选择子模块
function selectModule(mod) {
current.module = mod
current.note = null
const sysName = (current.system && current.system.name)
|| (() => {
const sid = mod && (mod.system_id || mod.systemId)
const found = (current.systems || []).find(s => String(s.id) === String(sid))
return found ? found.name : '未选择系统'
})()
updatePath(`系统: ${sysName} → 子模块: ${mod.name}`)
if ($systemDetail) $systemDetail.style.display = 'none'
if ($moduleDetail) $moduleDetail.style.display = 'block'
if ($noteDetail) $noteDetail.style.display = 'none'
if (el('mod-name')) el('mod-name').value = mod.name || ''
if (el('mod-desc')) el('mod-desc').value = mod.description || ''
if (el('mod-addr')) el('mod-addr').value = mod.server_addr || ''
if (el('mod-port')) el('mod-port').value = String(mod.server_port || '')
if (el('mod-code')) el('mod-code').value = mod.code_type || ''
if (el('mod-deploy')) el('mod-deploy').value = mod.deploy_method || ''
if (el('mod-soft')) el('mod-soft').value = mod.software_versions || ''
if ($noteNav) $noteNav.style.display = 'none'
loadNotes(mod.id)
}
// 加载模块下的开发纪要到树
async function loadModuleNotes(moduleId, container) {
showLoading('正在加载纪要...')
try {
const res = await fetch(`${apiBase}/notes?module_id=${moduleId}`, { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
const notes = data.rows || []
container.innerHTML = ''
if (notes.length === 0) {
const empty = document.createElement('div')
empty.style.color = '#888'
empty.textContent = '暂无开发纪要'
container.appendChild(empty)
const addRow = document.createElement('div')
addRow.className = 'tree-note-item'
const addBtn = document.createElement('button')
addBtn.className = 'btn-icon sm'
addBtn.textContent = '➕'
addBtn.title = '新增纪要'
addBtn.onclick = async (e) => {
e.stopPropagation();
const mod = (current.modules || []).find(m => String(m.id) === String(moduleId))
if (mod) selectModule(mod)
await newNote()
await loadModuleNotes(moduleId, container)
}
addRow.appendChild(addBtn)
container.appendChild(addRow)
return
}
notes.forEach(note => {
const row = createNoteTreeItem(note, moduleId)
container.appendChild(row)
})
} catch (e) {
container.innerHTML = '<div style="color:#f44747">加载失败</div>'
} finally { hideLoading() }
}
// 创建纪要树形项
function createNoteTreeItem(note, moduleId) {
const row = document.createElement('div')
row.className = 'tree-note-item'
row.dataset.noteId = note.id
row.dataset.moduleId = String(moduleId)
const icon = document.createElement('span')
icon.className = 'note-icon'
icon.textContent = '📝'
const title = document.createElement('span')
const preview = (note.content || '').substring(0, 24)
title.textContent = `${note.note_type} ${preview ? '· ' + preview + '...' : ''}`
title.style.flex = '1'
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnAdd = document.createElement('button')
btnAdd.className = 'btn-icon sm'
btnAdd.textContent = '➕'
btnAdd.title = '新增纪要'
btnAdd.onclick = async (e) => {
e.stopPropagation();
const mod = (current.modules || []).find(m => String(m.id) === String(moduleId))
if (mod) selectModule(mod)
await newNote()
const container = row.parentElement
if (container) await loadModuleNotes(moduleId, container)
}
const btnEdit = document.createElement('button')
btnEdit.className = 'btn-icon btn-edit sm'
btnEdit.textContent = '✏️'
btnEdit.title = '编辑纪要'
btnEdit.onclick = (e) => { e.stopPropagation(); selectNote(note, moduleId) }
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete sm'
btnDelete.textContent = '🗑️'
btnDelete.title = '删除纪要'
btnDelete.onclick = async (e) => {
e.stopPropagation();
await deleteNote(note.id)
const container = row.parentElement
if (container) await loadModuleNotes(moduleId, container)
}
actions.appendChild(btnAdd)
actions.appendChild(btnEdit)
actions.appendChild(btnDelete)
row.appendChild(icon)
row.appendChild(title)
row.appendChild(actions)
row.onclick = (e) => {
e.stopPropagation()
selectNote(note, moduleId)
}
return row
}
// 加载开发纪要
async function loadNotes(moduleId) {
showLoading('正在加载开发纪要...')
try {
const res = await fetch(`${apiBase}/notes?module_id=${moduleId}`, { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
current.notes = data.rows || []
if ($notes) {
$notes.innerHTML = ''
current.notes.forEach(note => {
const item = document.createElement('div')
item.className = 'note-item'
const typeIcon = getNoteTypeIcon(note.note_type)
const content = document.createElement('div')
content.innerHTML = `<strong>${typeIcon} ${note.note_type}: ${note.content?.substring(0, 50) || '无内容'}...</strong>`
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnEdit = document.createElement('button')
btnEdit.className = 'btn-icon btn-edit'
btnEdit.textContent = '✏️'
btnEdit.onclick = (e) => { e.stopPropagation(); editNote(note.id) }
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete'
btnDelete.textContent = '🗑️'
btnDelete.onclick = (e) => { e.stopPropagation(); deleteNote(note.id) }
actions.appendChild(btnEdit)
actions.appendChild(btnDelete)
item.appendChild(content)
item.appendChild(actions)
item.onclick = () => selectNote(note)
$notes.appendChild(item)
})
}
} catch (e) {
console.error('加载纪要失败:', e)
alert('加载纪要失败: ' + e.message)
} finally {
hideLoading()
}
}
// 选择开发纪要
function selectNote(note, moduleId) {
current.note = note
const modIdStr = moduleId ? String(moduleId) : (note && note.module_id ? String(note.module_id) : '')
const wrapper = modIdStr ? document.querySelector(`.tree-module-wrapper[data-module-id="${modIdStr}"]`) : null
const sysName = (current.system && current.system.name) || (wrapper && wrapper.dataset.systemName) || '未选择系统'
const modName = (current.module && current.module.name) || (wrapper && wrapper.dataset.moduleName) || '未选择子模块'
if (!current.module && modIdStr) {
const sysId = wrapper && wrapper.dataset.systemId ? parseInt(wrapper.dataset.systemId, 10) : null
current.module = { id: parseInt(modIdStr, 10), name: modName, system_id: sysId }
}
updatePath(`系统: ${sysName} → 子模块: ${modName} → 纪要: ${note.note_type}`)
if ($systemDetail) $systemDetail.style.display = 'none'
if ($moduleDetail) $moduleDetail.style.display = 'none'
if ($noteDetail) $noteDetail.style.display = 'block'
if (el('note-type')) el('note-type').value = note.note_type || '需求'
if (el('note-content')) el('note-content').value = note.content || ''
// 加载附件
if ($attachments) $attachments.innerHTML = '<div style="color:#888">附件列表加载中...</div>'
loadAttachments(note.id)
// 显示附件上传区域
if ($noteDetail) {
const attachmentSection = $noteDetail.querySelector('.attachment-section')
if (attachmentSection) attachmentSection.style.display = 'block'
}
}
// 加载附件
async function loadAttachments(noteId) {
try {
const res = await fetch(`${apiBase}/attachments?note_id=${noteId}`, { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
const rows = data.rows || []
// 防止竞态:若用户已切换到其他纪要,忽略过期结果
if (current.note && String(current.note.id) !== String(noteId)) return
current.attachments = rows
if ($attachments) {
$attachments.innerHTML = ''
if (rows.length === 0) {
const empty = document.createElement('div')
empty.style.color = '#888'
empty.textContent = '暂无附件'
$attachments.appendChild(empty)
return
}
rows.forEach(att => {
const item = document.createElement('div')
item.className = 'attachment-item'
const content = document.createElement('div')
content.innerHTML = `📎 ${att.filename} (${formatFileSize(att.size)})`
const actions = document.createElement('div')
actions.className = 'item-actions'
const btnDelete = document.createElement('button')
btnDelete.className = 'btn-icon btn-delete'
btnDelete.textContent = '🗑️'
btnDelete.onclick = (e) => { e.stopPropagation(); deleteAttachment(att.id) }
actions.appendChild(btnDelete)
item.appendChild(content)
item.appendChild(actions)
item.onclick = () => downloadAttachment(att)
$attachments.appendChild(item)
})
}
} catch (e) {
if ($attachments) {
$attachments.innerHTML = '<div style="color:#f44747">附件列表加载失败</div>'
}
console.error('加载附件失败:', e)
}
}
// 工具函数
function getNoteTypeIcon(type) {
const icons = { '需求': '📋', '开发': '💻', '部署': '🚀', '维护': '🔧' }
return icons[type] || '📝'
}
function formatFileSize(bytes) {
if (!bytes) return '0 B'
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]
}
function updatePath(text) {
const p = el('current-path')
if (p) p.textContent = text
}
function showLoading(text) {
const overlay = el('loading-overlay')
const msg = el('loading-text')
if (msg && typeof text === 'string' && text) msg.textContent = text
if (overlay) overlay.style.display = 'flex'
}
function hideLoading() {
const overlay = el('loading-overlay')
if (overlay) overlay.style.display = 'none'
}
function resetNavigation() {
if ($moduleNav) $moduleNav.style.display = 'none'
if ($noteNav) $noteNav.style.display = 'none'
if ($systemDetail) $systemDetail.style.display = 'none'
if ($moduleDetail) $moduleDetail.style.display = 'none'
if ($noteDetail) $noteDetail.style.display = 'none'
current.system = null
current.module = null
current.note = null
if ($systems) $systems.innerHTML = '' // 注意:loadSystems 会重新填充
if ($modules) $modules.innerHTML = ''
if ($notes) $notes.innerHTML = ''
if ($attachments) $attachments.innerHTML = ''
updatePath('请选择系统 → 子模块 → 开发纪要')
if ($noteDetail) {
const attachmentSection = $noteDetail.querySelector('.attachment-section')
if (attachmentSection) {
attachmentSection.style.display = 'none'
}
}
}
// 保存系统
async function saveSystem() {
if (!current.system) return
const name = (el('sys-name').value || '').trim()
const intro = (el('sys-intro').value || '').trim()
const major_category = document.querySelector('input[name="sys-major"]:checked')?.value || ''
if (!name || !intro) { alert('系统名称与简介为必填'); return }
const payload = { name, intro, major_category }
showLoading('正在保存系统...')
try {
let res, data
if (current.system && current.system.id) {
res = await fetch(`${apiBase}/systems/${current.system.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
data = await res.json()
if (!data.ok) throw new Error('保存失败')
alert('系统已保存')
await loadSystems()
} else {
res = await fetch(`${apiBase}/systems`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
data = await res.json()
if (!data.ok) throw new Error(data.error || '创建失败')
alert('系统已创建')
await loadSystems()
const created = (current.systems || []).find(s => String(s.id) === String(data.id))
if (created) selectSystem(created)
}
} catch (e) {
alert('保存失败: ' + e.message)
} finally {
hideLoading()
}
}
// 保存子模块
async function saveModule() {
if (!current.system) return
const name = (el('mod-name').value || '').trim()
const description = (el('mod-desc').value || '').trim()
const server_addr = (el('mod-addr').value || '').trim()
const portStr = (el('mod-port').value || '').trim()
const server_port = parseInt(portStr || '0', 10)
const code_type = (el('mod-code').value || '').trim()
const deploy_method = (el('mod-deploy').value || '').trim()
const software_versions = (el('mod-soft').value || '').trim()
if (!name || !description || !server_addr || !portStr || !(server_port > 0) || !code_type || !deploy_method || !software_versions) { alert('子模块所有字段均为必填,端口需为正整数'); return }
const payload = { system_id: current.system.id, name, description, server_addr, server_port, code_type, deploy_method, software_versions }
showLoading('正在保存子模块...')
try {
let res, data
if (current.module && current.module.id) {
payload.id = current.module.id
res = await fetch(`${apiBase}/modules/${current.module.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
} else {
res = await fetch(`${apiBase}/modules`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
}
data = await res.json()
if (!data.ok) throw new Error('保存失败')
alert('子模块已保存')
await loadModules(current.system.id)
// 刷新树形中的当前系统子模块列表
const sysChildren = document.querySelector(`.tree-item[data-system-id="${current.system.id}"] .tree-item-children`)
if (sysChildren) await loadSystemModules(current.system.id, sysChildren)
else {
await loadSystems()
const sysNode = document.querySelector(`.tree-item[data-system-id="${current.system.id}"]`)
if (sysNode) {
const header = sysNode.querySelector('.tree-item-header')
const content = sysNode.querySelector('.tree-item-content')
const children = sysNode.querySelector('.tree-item-children')
const toggle = header && header.querySelector('.tree-toggle')
if (header) header.classList.add('expanded')
if (content) content.classList.add('expanded')
if (toggle) toggle.classList.add('expanded')
if (children) await loadSystemModules(current.system.id, children)
}
}
} catch (e) {
alert('保存失败: ' + e.message)
} finally {
hideLoading()
}
}
// 保存开发纪要
async function saveNote() {
if (!current.module) return
const note_type = (el('note-type').value || '').trim()
const content = (el('note-content').value || '').trim()
if (!note_type || !content) { alert('纪要类型与内容为必填'); return }
const payload = { module_id: current.module.id, note_type, content }
showLoading('正在保存开发纪要...')
try {
let res, data
if (current.note && current.note.id) {
payload.id = current.note.id
res = await fetch(`${apiBase}/notes/${current.note.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
} else {
res = await fetch(`${apiBase}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin'
})
}
data = await res.json()
if (!data.ok) throw new Error('保存失败')
alert('开发纪要已保存')
await loadNotes(current.module.id)
// 刷新树形中的当前模块纪要列表
const notesContainer = document.querySelector(`.tree-module-wrapper[data-module-id="${current.module.id}"] .tree-note-list`)
if (notesContainer) await loadModuleNotes(current.module.id, notesContainer)
} catch (e) {
alert('保存失败: ' + e.message)
} finally {
hideLoading()
}
}
// 新增系统:进入新增模式,不立即提交
async function newSystem() {
current.system = { id: null, name: '', intro: '' }
updatePath('系统: 新增')
if ($systemDetail) $systemDetail.style.display = 'block'
if ($moduleDetail) $moduleDetail.style.display = 'none'
if ($noteDetail) $noteDetail.style.display = 'none'
if (el('sys-name')) el('sys-name').value = ''
if (el('sys-intro')) el('sys-intro').value = ''
const radios = document.querySelectorAll('input[name="sys-major"]')
radios.forEach(r => {
if (r.value === 'Trae研发') r.checked = true
else r.checked = false
})
}
// 新增子模块
async function newModule() {
if (!current.system) return
current.module = null
updatePath(`系统: ${current.system.name} → 子模块: 新增`)
if ($systemDetail) $systemDetail.style.display = 'none'
if ($moduleDetail) $moduleDetail.style.display = 'block'
if ($noteDetail) $noteDetail.style.display = 'none'
if (el('mod-name')) el('mod-name').value = ''
if (el('mod-desc')) el('mod-desc').value = ''
if (el('mod-addr')) el('mod-addr').value = ''
if (el('mod-port')) el('mod-port').value = ''
if (el('mod-code')) el('mod-code').value = ''
if (el('mod-deploy')) el('mod-deploy').value = ''
if (el('mod-soft')) el('mod-soft').value = ''
}
// 新增开发纪要
async function newNote() {
if (!current.module) return
current.note = null
updatePath(`系统: ${current.system ? current.system.name : ''} → 子模块: ${current.module.name} → 纪要: 新增`)
if ($systemDetail) $systemDetail.style.display = 'none'
if ($moduleDetail) $moduleDetail.style.display = 'none'
if ($noteDetail) $noteDetail.style.display = 'block'
if (el('note-type')) el('note-type').value = '需求'
if (el('note-content')) el('note-content').value = ''
const attachmentSection = $noteDetail && $noteDetail.querySelector('.attachment-section')
if (attachmentSection) attachmentSection.style.display = 'none'
}
// 上传附件
async function uploadAttachments() {
if (!$attachmentFile) return;
const files = $attachmentFile.files
if (files.length === 0) {
alert('请选择至少一个文件')
return
}
if (!current.note) {
alert('请先选择开发纪要')
return
}
if ($btnUploadAttachment) $btnUploadAttachment.disabled = true
if ($uploadProgress) $uploadProgress.innerHTML = ''
let successCount = 0
let errorCount = 0
for (let i = 0; i < files.length; i++) {
const file = files[i]
const progressId = 'progress-' + Date.now() + '-' + i
const progressDiv = document.createElement('div')
progressDiv.id = progressId
progressDiv.innerHTML = `
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="flex: 1; font-size: 12px;">📄 ${file.name} (${formatFileSize(file.size)})</span>
<progress value="0" max="100" style="width: 100px; margin: 0 10px;"></progress>
<span style="font-size: 12px;">0%</span>
</div>
`
if ($uploadProgress) $uploadProgress.appendChild(progressDiv)
try {
const url = `${apiBase}/attachments/upload?note_id=${encodeURIComponent(current.note.id)}&filename=${encodeURIComponent(file.name)}&mime=${encodeURIComponent(file.type||'application/octet-stream')}`
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/octet-stream' },
body: file
})
const data = await res.json()
if (!data.ok) throw new Error(data.error || '上传失败')
successCount++
const progressBar = document.querySelector(`#${progressId} progress`)
const progressText = document.querySelector(`#${progressId} span:last-child`)
if (progressBar && progressText) {
progressBar.value = 100
progressText.textContent = '✅ 完成'
}
} catch (e) {
errorCount++
const progressBar = document.querySelector(`#${progressId} progress`)
const progressText = document.querySelector(`#${progressId} span:last-child`)
if (progressBar && progressText) {
progressBar.value = 0
progressText.textContent = '❌ 失败: ' + e.message
progressText.style.color = '#ff6b6b'
}
}
}
if ($btnUploadAttachment) $btnUploadAttachment.disabled = false
if ((successCount > 0 || errorCount > 0) && $uploadProgress) {
const resultDiv = document.createElement('div')
resultDiv.style.marginTop = '16px'
resultDiv.style.padding = '8px'
resultDiv.style.background = successCount > 0 ? '#2b8a3e20' : '#c92a2a20'
resultDiv.style.border = successCount > 0 ? '1px solid #2b8a3e' : '1px solid #c92a2a'
resultDiv.style.borderRadius = '4px'
resultDiv.innerHTML = `
<strong>📊 上传结果:</strong><br>
✅ 成功: ${successCount} 个文件<br>
❌ 失败: ${errorCount} 个文件
`
$uploadProgress.appendChild(resultDiv)
if (successCount > 0) {
await loadAttachments(current.note.id)
}
}
}
// 下载附件
async function downloadAttachment(attachment) {
try {
const res = await fetch(`${apiBase}/attachments/${attachment.id}/download`, {
credentials: 'same-origin'
})
if (!res.ok) throw new Error('下载失败')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = attachment.filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (e) {
alert('下载失败: ' + e.message)
}
}
// CRUD 操作
async function deleteSystem(systemId) {
if (!confirm('确定要删除这个系统吗?此操作将删除所有相关的子模块、纪要和附件!')) return
showLoading('正在删除系统...')
try {
const res = await fetch(`${apiBase}/systems/${systemId}`, {
method: 'DELETE',
credentials: 'same-origin'
})
const data = await res.json()
if (!data.ok) throw new Error('删除失败')
alert('系统已删除')
await loadSystems()
} catch (e) {
alert('删除失败: ' + e.message)
} finally { hideLoading() }
}
async function editSystem(systemId) {
const system = current.systems.find(s => s.id === systemId)
if (system) selectSystem(system)
}
async function deleteModule(moduleId) {
if (!confirm('确定要删除这个子模块吗?此操作将删除所有相关的纪要和附件!')) return
showLoading('正在删除子模块...')
try {
const res = await fetch(`${apiBase}/modules/${moduleId}`, {
method: 'DELETE',
credentials: 'same-origin'
})
const data = await res.json()
if (!data.ok) throw new Error('删除失败')
alert('子模块已删除')
await loadModules(current.system.id)
} catch (e) {
alert('删除失败: ' + e.message)
} finally { hideLoading() }
}
async function editModule(moduleId) {
const module = current.modules.find(m => m.id === moduleId)
if (module) selectModule(module)
}
async function deleteNote(noteId) {
if (!confirm('确定要删除这个开发纪要吗?此操作将删除所有相关的附件!')) return
showLoading('正在删除纪要...')
try {
const res = await fetch(`${apiBase}/notes/${noteId}`, {
method: 'DELETE',
credentials: 'same-origin'
})
const data = await res.json()
if (!data.ok) throw new Error('删除失败')
alert('开发纪要已删除')
await loadNotes(current.module.id)
} catch (e) {
alert('删除失败: ' + e.message)
} finally { hideLoading() }
}
async function editNote(noteId) {
const note = current.notes.find(n => n.id === noteId)
if (note) selectNote(note)
}
async function deleteAttachment(attachmentId) {
if (!confirm('确定要删除这个附件吗?')) return
showLoading('正在删除附件...')
try {
const res = await fetch(`${apiBase}/attachments/${attachmentId}`, {
method: 'DELETE',
credentials: 'same-origin'
})
const data = await res.json()
if (!data.ok) throw new Error('删除失败')
alert('附件已删除')
await loadAttachments(current.note.id)
} catch (e) {
alert('删除失败: ' + e.message)
} finally { hideLoading() }
}
// --- 大类管理逻辑 ---
async function loadCategories() {
try {
const res = await fetch(apiBase + '/categories', { cache: 'no-store', credentials: 'same-origin' })
const data = await res.json()
if (data.ok) {
current.categories = data.rows || []
renderCategories()
}
} catch (e) {
console.error('加载大类失败:', e)
}
}
function renderCategories() {
// 1. 渲染右上角筛选
const filterContainer = el('filter-major-dynamic')
if (filterContainer) {
// 保留当前选中的值
const currentFilter = document.querySelector('input[name="filter-major"]:checked')?.value || ''
filterContainer.innerHTML = current.categories.map(c => `
<label><input type="radio" name="filter-major" value="${c.name}" ${currentFilter === c.name ? 'checked' : ''}> ${c.name}</label>
`).join('')
// 重新绑定筛选事件
const radios = document.querySelectorAll('input[name="filter-major"]')
radios.forEach(r => {
r.onchange = () => {
resetNavigation()
loadSystems()
}
})
}
// 2. 渲染系统详情页的大类单选
const formContainer = el('sys-major-dynamic')
if (formContainer) {
formContainer.innerHTML = current.categories.map(c => `
<label><input type="radio" name="sys-major" value="${c.name}"> ${c.name}</label>
`).join('')
// 如果当前正在编辑系统,恢复选中值
if (current.system && current.system.major_category) {
const r = formContainer.querySelector(`input[value="${current.system.major_category}"]`)
if (r) r.checked = true
}
}
}
async function openCategoryManager() {
let modal = el('category-manager-modal')
if (!modal) {
modal = document.createElement('div')
modal.id = 'category-manager-modal'
modal.className = 'modal-overlay open'
modal.style.zIndex = '2000'
document.body.appendChild(modal)
}
const renderModalContent = () => {
modal.innerHTML = `
<div class="modal" style="max-width: 500px;">
<div class="modal-header">
<h3>🔧 大类管理</h3>
<button class="btn-icon close-modal" onclick="document.getElementById('category-manager-modal').style.display='none'">✖️</button>
</div>
<div class="modal-body">
<div style="display:flex; gap:10px; margin-bottom:15px; border-bottom:1px solid #eee; padding-bottom:15px;">
<input id="new-cat-input" placeholder="输入新大类名称" style="flex:1;">
<button id="btn-add-cat" class="btn">添加</button>
</div>
<div id="cat-list" style="max-height:400px; overflow-y:auto;">
${current.categories.map(c => `
<div style="display:flex; align-items:center; padding:8px 0; border-bottom:1px solid #f5f5f5; justify-content: space-between;">
<span style="font-weight:500;">${c.name}</span>
<div>
<button class="btn-icon sm" onclick="window.aiLib_promptEditCategory('${c.id}', '${c.name}')" title="重命名">✏️</button>
<button class="btn-icon sm btn-delete" onclick="window.aiLib_confirmDeleteCategory('${c.id}', '${c.name}')" title="删除">🗑️</button>
</div>
</div>
`).join('')}
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="document.getElementById('category-manager-modal').style.display='none'">关闭</button>
</div>
</div>
`
setTimeout(() => {
const btnAdd = modal.querySelector('#btn-add-cat')
const input = modal.querySelector('#new-cat-input')
if (btnAdd && input) {
btnAdd.onclick = async () => {
const name = input.value.trim()
if (!name) return alert('请输入名称')
await apiCreateCategory(name)
input.value = ''
renderModalContent()
}
}
}, 0)
}
window.aiLib_promptEditCategory = async (id, oldName) => {
const newName = prompt('请输入新的大类名称:', oldName)
if (newName && newName.trim() && newName !== oldName) {
await apiUpdateCategory(id, newName.trim())
renderModalContent()
}
}
window.aiLib_confirmDeleteCategory = async (id, name) => {
if (confirm(`确定要删除大类“${name}”吗?`)) {
await apiDeleteCategory(id)
renderModalContent()
}
}
renderModalContent()
modal.style.display = 'flex'
}
async function apiCreateCategory(name) {
try {
const res = await fetch(apiBase + '/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
cache: 'no-store'
})
const data = await res.json()
if (!data.ok) throw new Error(data.error)
await loadCategories()
} catch(e) { alert('添加失败: ' + e.message) }
}
async function apiUpdateCategory(id, name) {
try {
const res = await fetch(apiBase + '/categories/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
cache: 'no-store'
})
const data = await res.json()
if (!data.ok) throw new Error(data.error)
await loadCategories()
} catch(e) { alert('更新失败: ' + e.message) }
}
async function apiDeleteCategory(id) {
try {
const res = await fetch(apiBase + '/categories/' + id, {
method: 'DELETE',
cache: 'no-store'
})
const data = await res.json()
if (!data.ok) throw new Error(data.error)
await loadCategories()
} catch(e) { alert('删除失败: ' + e.message) }
}
// 绑定事件
function bindEvents() {
const btnLogin = el('btn-login'); if (btnLogin) btnLogin.onclick = login
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/ai-lib/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) el('login-user').value = d.creds.username
if (d.creds.password) el('login-pass').value = d.creds.password
}
} catch {}
el('login-user').removeAttribute('readonly')
el('login-pass').removeAttribute('readonly')
})()
const btnNewSystem = el('btn-new-system');
if (btnNewSystem) {
btnNewSystem.onclick = newSystem
} else {
console.warn('Button btn-new-system not found in DOM')
}
const btnNewModule = el('btn-new-module'); if (btnNewModule) btnNewModule.onclick = newModule
const btnNewNote = el('btn-new-note'); if (btnNewNote) btnNewNote.onclick = newNote
const btnRefreshTree = el('btn-refresh-tree'); if (btnRefreshTree) btnRefreshTree.onclick = () => loadSystems()
const btnManageCategories = el('btn-manage-categories'); if (btnManageCategories) btnManageCategories.onclick = openCategoryManager
// 已移除详情标题处的新增按钮
const btnSaveSystem = el('btn-save-system'); if (btnSaveSystem) btnSaveSystem.onclick = saveSystem
const btnSaveModule = el('btn-save-module'); if (btnSaveModule) btnSaveModule.onclick = saveModule
const btnSaveNote = el('btn-save-note'); if (btnSaveNote) btnSaveNote.onclick = saveNote
if ($btnUploadAttachment) $btnUploadAttachment.onclick = uploadAttachments
const filterRadios = document.querySelectorAll('input[name="filter-major"]')
filterRadios.forEach(r => {
r.onchange = () => {
resetNavigation()
loadSystems()
}
})
}
async function init() {
console.log('AI Lib initializing...')
// 初始化 DOM 引用
$auth = el('auth')
$app = el('app')
$systems = el('systems')
$modules = el('modules')
$notes = el('notes')
$attachments = el('attachments')
$moduleNav = el('module-nav')
$noteNav = el('note-nav')
$systemDetail = el('system-detail')
$moduleDetail = el('module-detail')
$noteDetail = el('note-detail')
$attachmentFile = el('attachment-file')
$btnUploadAttachment = el('btn-upload-attachment')
$uploadProgress = el('upload-progress')
if (!$auth || !$app || !$systems) {
console.error('Critical elements missing:', { auth: !!$auth, app: !!$app, systems: !!$systems })
alert('系统初始化失败:关键页面元素缺失。请查看控制台了解详情。')
return
}
bindEvents()
const isLoggedIn = await loadCategories()
if (isLoggedIn) {
$auth.style.display = 'none'
$app.style.display = 'block'
await loadSystems()
}
resetNavigation()
console.log('AI Lib initialized successfully.')
// alert('系统脚本已更新并加载 (v2)') // Debug alert
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
} catch (err) {
console.error('Script initialization error:', err);
alert('脚本初始化错误: ' + err.message);
}
})()