Compare commits

..

4 Commits

2 changed files with 231 additions and 11 deletions

View File

@@ -136,6 +136,8 @@ xian-favor/
## 版本历史
- v1.7.0 (2026-04-13): 编辑收藏时支持更改类型,动态显示对应字段
- v1.6.0 (2026-04-13): 标签管理添加编辑功能,支持修改标签名称
- v1.5.1 (2026-04-13): 标签管理添加搜索功能,实时过滤标签列表
- v1.5.0 (2026-04-13): 首页添加分页功能每页20条记录
- v1.4.0 (2026-04-13): 首页添加导出按钮一键导出所有收藏为JSON文件

View File

@@ -164,6 +164,82 @@ def get_stats():
return jsonify({'success': True, 'data': stats})
@app.route('/api/ai-process', methods=['POST'])
def ai_process():
"""AI处理文本"""
import requests
data = request.get_json()
text = data.get('text', '').strip()
if not text:
return jsonify({'success': False, 'error': '请输入文本内容'}), 400
# 大模型配置
llm_url = "http://192.168.2.17:19007/v1/chat/completions"
llm_key = "xxxx"
prompt = f"""请分析以下文本内容,识别其类型并提取关键信息。
文本内容:
{text}
请按以下JSON格式返回结果只返回JSON不要其他内容
{
"type": "text/link/column/todo",
"title": "提取的标题(简短概括)",
"content": "主要内容(如果是文本类型)",
"url": "如果是链接或专栏提取URL",
"source": "如果是专栏,提取来源",
"tags": ["相关标签1", "标签2"],
"note": "补充说明或备注",
"status": "如果是待办默认pending",
"priority": "如果是待办默认medium"
}
类型判断规则:
- link: 包含http/https链接且不是专栏订阅地址
- column: 专栏订阅地址或RSS链接
- todo: 包含任务、待办、提醒等关键词
- text: 其他文本内容"""
try:
response = requests.post(
llm_url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {llm_key}"
},
json={
"model": "auto",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3
},
timeout=30
)
if response.status_code != 200:
return jsonify({'success': False, 'error': f'模型调用失败: {response.status_code}'}), 500
result = response.json()
content = result['choices'][0]['message']['content']
# 解析JSON
import json
import re
# 提取JSON部分
json_match = re.search(r'\{.*\}', content, re.DOTALL)
if json_match:
parsed = json.loads(json_match.group())
return jsonify({'success': True, 'data': parsed})
else:
return jsonify({'success': False, 'error': '无法解析模型返回'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/search', methods=['GET'])
def search_items():
"""搜索条目"""
@@ -263,6 +339,9 @@ INDEX_TEMPLATE = '''
<option value="todo">待办</option>
</select>
</div>
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
<i class="bi bi-robot"></i> AI添加
</button>
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
<i class="bi bi-download"></i> 导出
</button>
@@ -432,7 +511,12 @@ INDEX_TEMPLATE = '''
<input type="hidden" id="editId">
<div class="mb-3">
<label class="form-label">类型</label>
<input type="text" id="editType" class="form-control" readonly>
<select id="editType" class="form-select">
<option value="text">📝 文本</option>
<option value="link">🔗 链接</option>
<option value="column">📰 专栏</option>
<option value="todo">✅ 待办</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">标题</label>
@@ -528,6 +612,44 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- AI自动添加模态框 -->
<div class="modal fade" id="aiAddModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-robot"></i> AI自动添加</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">输入文本内容</label>
<textarea id="aiInputText" class="form-control" rows="6" placeholder="粘贴文本、链接、笔记等AI会自动识别并整理..."></textarea>
</div>
<div id="aiResult" style="display:none;">
<hr>
<h6>识别结果:</h6>
<div id="aiResultContent" class="border rounded p-3 bg-light"></div>
</div>
<div id="aiLoading" style="display:none;">
<div class="text-center py-3">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2">AI正在分析...</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="aiProcessBtn" onclick="processAIInput()">
<i class="bi bi-magic"></i> 分析并添加
</button>
<button type="button" class="btn btn-success" id="aiConfirmBtn" style="display:none;" onclick="confirmAIAdd()">
<i class="bi bi-check"></i> 确认添加
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
@@ -555,6 +677,11 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('todoFields').style.display = type === 'todo' ? 'block' : 'none';
});
// 编辑时类型切换
document.getElementById('editType').addEventListener('change', (e) => {
updateEditFieldsByType(e.target.value);
});
// 搜索
document.getElementById('searchInput').addEventListener('input', debounce(loadItems, 300));
@@ -815,14 +942,11 @@ async function openEditModal(id) {
const type = item.type;
document.getElementById('editId').value = id;
document.getElementById('editType').value = getTypeLabel(type);
document.getElementById('editType').value = type;
document.getElementById('editTitle').value = item.title || '';
// 根据类型显示/隐藏字段
document.getElementById('editContentGroup').style.display = type === 'text' ? 'block' : 'none';
document.getElementById('editUrlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
document.getElementById('editSourceGroup').style.display = type === 'column' ? 'block' : 'none';
document.getElementById('editTodoFields').style.display = type === 'todo' ? 'block' : 'none';
updateEditFieldsByType(type);
document.getElementById('editContent').value = item.content || '';
document.getElementById('editUrl').value = item.url || '';
@@ -839,16 +963,21 @@ async function openEditModal(id) {
new bootstrap.Modal(document.getElementById('editModal')).show();
}
// 根据类型更新编辑表单字段显示
function updateEditFieldsByType(type) {
document.getElementById('editContentGroup').style.display = type === 'text' ? 'block' : 'none';
document.getElementById('editUrlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
document.getElementById('editSourceGroup').style.display = type === 'column' ? 'block' : 'none';
document.getElementById('editTodoFields').style.display = type === 'todo' ? 'block' : 'none';
}
// 保存编辑
async function saveEdit() {
const id = document.getElementById('editId').value;
// 获取当前条目的类型
const detailRes = await fetch(`${API_BASE}/items/${currentDetailId}`);
const detailData = await detailRes.json();
const type = detailData.data.type;
const type = document.getElementById('editType').value; // 从下拉框获取新类型
const data = {
type: type, // 包含类型变更
title: document.getElementById('editTitle').value,
content: type === 'text' ? document.getElementById('editContent').value : null,
url: ['link', 'column'].includes(type) ? document.getElementById('editUrl').value : null,
@@ -1120,6 +1249,95 @@ async function exportData() {
URL.revokeObjectURL(url);
}
// AI自动添加
let aiParsedData = null;
function showAIAddModal() {
document.getElementById('aiInputText').value = '';
document.getElementById('aiResult').style.display = 'none';
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').style.display = 'inline-block';
document.getElementById('aiConfirmBtn').style.display = 'none';
aiParsedData = null;
new bootstrap.Modal(document.getElementById('aiAddModal')).show();
}
async function processAIInput() {
const text = document.getElementById('aiInputText').value.trim();
if (!text) {
alert('请输入文本内容');
return;
}
// 显示加载
document.getElementById('aiLoading').style.display = 'block';
document.getElementById('aiProcessBtn').disabled = true;
try {
const res = await fetch(`${API_BASE}/ai-process`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await res.json();
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').disabled = false;
if (!data.success) {
alert('AI分析失败: ' + data.error);
return;
}
aiParsedData = data.data;
// 显示结果
const typeLabels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
let html = `<div><strong>类型:</strong> ${typeLabels[aiParsedData.type] || aiParsedData.type}</div>`;
if (aiParsedData.title) html += `<div><strong>标题:</strong> ${aiParsedData.title}</div>`;
if (aiParsedData.url) html += `<div><strong>URL:</strong> ${aiParsedData.url}</div>`;
if (aiParsedData.content) html += `<div><strong>内容:</strong> ${aiParsedData.content.substring(0, 200)}${aiParsedData.content.length > 200 ? '...' : ''}</div>`;
if (aiParsedData.tags && aiParsedData.tags.length) html += `<div><strong>标签:</strong> ${aiParsedData.tags.join(', ')}</div>`;
if (aiParsedData.note) html += `<div><strong>备注:</strong> ${aiParsedData.note.substring(0, 100)}${aiParsedData.note.length > 100 ? '...' : ''}</div>`;
if (aiParsedData.type === 'todo') {
const statusLabels = { pending: '⏳ 待处理', in_progress: '🔄 进行中', completed: '✅ 已完成' };
const priorityLabels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
html += `<div><strong>状态:</strong> ${statusLabels[aiParsedData.status] || 'pending'}</div>`;
html += `<div><strong>优先级:</strong> ${priorityLabels[aiParsedData.priority] || 'medium'}</div>`;
}
document.getElementById('aiResultContent').innerHTML = html;
document.getElementById('aiResult').style.display = 'block';
document.getElementById('aiProcessBtn').style.display = 'none';
document.getElementById('aiConfirmBtn').style.display = 'inline-block';
} catch (e) {
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').disabled = false;
alert('请求失败: ' + e.message);
}
}
async function confirmAIAdd() {
if (!aiParsedData) return;
const res = await fetch(`${API_BASE}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(aiParsedData)
});
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById('aiAddModal')).hide();
refreshData();
alert('添加成功!');
} else {
const data = await res.json();
alert('添加失败: ' + data.error);
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {