feat: AI自动添加功能,智能识别文本类型并整理数据
This commit is contained in:
@@ -164,6 +164,82 @@ def get_stats():
|
|||||||
return jsonify({'success': True, 'data': 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'])
|
@app.route('/api/search', methods=['GET'])
|
||||||
def search_items():
|
def search_items():
|
||||||
"""搜索条目"""
|
"""搜索条目"""
|
||||||
@@ -263,6 +339,9 @@ INDEX_TEMPLATE = '''
|
|||||||
<option value="todo">待办</option>
|
<option value="todo">待办</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
|
||||||
<i class="bi bi-download"></i> 导出
|
<i class="bi bi-download"></i> 导出
|
||||||
</button>
|
</button>
|
||||||
@@ -533,6 +612,44 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
@@ -1132,6 +1249,95 @@ async function exportData() {
|
|||||||
URL.revokeObjectURL(url);
|
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) {
|
function debounce(fn, delay) {
|
||||||
let timer;
|
let timer;
|
||||||
return function(...args) {
|
return function(...args) {
|
||||||
|
|||||||
Reference in New Issue
Block a user