@@ -132,6 +132,23 @@ def create_tag():
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/tags/<int:tag_id> ' , methods = [ ' PUT ' ] )
def update_tag ( tag_id ) :
""" 更新标签 """
data = request . get_json ( )
name = data . get ( ' name ' , ' ' ) . strip ( )
if not name :
return jsonify ( { ' success ' : False , ' error ' : ' 标签名不能为空 ' } ) , 400
try :
if db . update_tag ( tag_id , name ) :
return jsonify ( { ' success ' : True , ' data ' : { ' id ' : tag_id , ' name ' : name } } )
return jsonify ( { ' success ' : False , ' error ' : ' 标签不存在或名称已存在 ' } ) , 404
except Exception as e :
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/tags/<int:tag_id> ' , methods = [ ' DELETE ' ] )
def delete_tag ( tag_id ) :
""" 删除标签 """
@@ -147,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 ( ) :
""" 搜索条目 """
@@ -162,6 +255,157 @@ def search_items():
return jsonify ( { ' success ' : True , ' data ' : items } )
# ============ 邮箱管理 API ============
@app.route ( ' /api/emails ' , methods = [ ' GET ' ] )
def list_emails ( ) :
""" 列出所有邮箱 """
emails = db . list_emails ( )
return jsonify ( { ' success ' : True , ' data ' : emails } )
@app.route ( ' /api/emails ' , methods = [ ' POST ' ] )
def create_email ( ) :
""" 创建邮箱 """
data = request . get_json ( )
email_addr = data . get ( ' email ' , ' ' ) . strip ( )
if not email_addr :
return jsonify ( { ' success ' : False , ' error ' : ' 邮箱地址不能为空 ' } ) , 400
# 验证邮箱格式
import re
if not re . match ( r ' ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+ \ .[a-zA-Z0-9-.]+$ ' , email_addr ) :
return jsonify ( { ' success ' : False , ' error ' : ' 邮箱格式不正确 ' } ) , 400
try :
email_id = db . create_email ( email_addr , data . get ( ' name ' ) )
return jsonify ( { ' success ' : True , ' data ' : { ' id ' : email_id , ' email ' : email_addr } } ) , 201
except Exception as e :
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/emails/<int:email_id> ' , methods = [ ' PUT ' ] )
def update_email ( email_id ) :
""" 更新邮箱 """
data = request . get_json ( )
try :
if db . update_email ( email_id , email = data . get ( ' email ' ) , name = data . get ( ' name ' ) ) :
email = db . get_email ( email_id )
return jsonify ( { ' success ' : True , ' data ' : email } )
return jsonify ( { ' success ' : False , ' error ' : ' 邮箱不存在或地址已存在 ' } ) , 404
except Exception as e :
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/emails/<int:email_id> ' , methods = [ ' DELETE ' ] )
def delete_email ( email_id ) :
""" 删除邮箱 """
if db . delete_email ( email_id ) :
return jsonify ( { ' success ' : True } )
return jsonify ( { ' success ' : False , ' error ' : ' 邮箱不存在 ' } ) , 404
@app.route ( ' /api/send-email ' , methods = [ ' POST ' ] )
def send_email ( ) :
""" 发送收藏内容到邮箱 """
data = request . get_json ( )
item_id = data . get ( ' item_id ' )
email_addr = data . get ( ' email ' , ' ' ) . strip ( )
if not item_id or not email_addr :
return jsonify ( { ' success ' : False , ' error ' : ' 缺少参数 ' } ) , 400
# 获取收藏内容
item = db . get_item ( item_id )
if not item :
return jsonify ( { ' success ' : False , ' error ' : ' 收藏不存在 ' } ) , 404
# 如果是新邮箱,自动保存
import re
if re . match ( r ' ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+ \ .[a-zA-Z0-9-.]+$ ' , email_addr ) :
db . create_email ( email_addr )
# 构建邮件内容
type_labels = { ' text ' : ' 文本笔记 ' , ' link ' : ' 链接收藏 ' , ' column ' : ' 专栏订阅 ' , ' todo ' : ' 待办事项 ' }
subject = f " 【Xian Favor】 { item [ ' title ' ] or type_labels . get ( item [ ' type ' ] , ' 收藏 ' ) } "
body_lines = [
f " 类型: { type_labels . get ( item [ ' type ' ] , item [ ' type ' ] ) } " ,
f " 标题: { item [ ' title ' ] or ' (无标题) ' } " ,
" "
]
if item [ ' url ' ] :
body_lines . append ( f " 链接: { item [ ' url ' ] } " )
body_lines . append ( " " )
if item [ ' content ' ] :
body_lines . append ( " 内容: " )
body_lines . append ( item [ ' content ' ] )
body_lines . append ( " " )
if item [ ' source ' ] :
body_lines . append ( f " 来源: { item [ ' source ' ] } " )
body_lines . append ( " " )
if item [ ' type ' ] == ' todo ' :
status_labels = { ' pending ' : ' 待处理 ' , ' in_progress ' : ' 进行中 ' , ' completed ' : ' 已完成 ' }
priority_labels = { ' low ' : ' 低 ' , ' medium ' : ' 中 ' , ' high ' : ' 高 ' , ' urgent ' : ' 紧急 ' }
body_lines . append ( f " 状态: { status_labels . get ( item [ ' status ' ] , item [ ' status ' ] ) } " )
body_lines . append ( f " 优先级: { priority_labels . get ( item [ ' priority ' ] , item [ ' priority ' ] ) } " )
if item [ ' due_date ' ] :
body_lines . append ( f " 截止日期: { item [ ' due_date ' ] } " )
body_lines . append ( " " )
if item [ ' tags ' ] :
body_lines . append ( f " 标签: { ' , ' . join ( item [ ' tags ' ] ) } " )
body_lines . append ( " " )
if item [ ' note ' ] :
body_lines . append ( " 详情/备注: " )
body_lines . append ( item [ ' note ' ] )
body_lines . append ( " " )
body_lines . append ( f " 创建时间: { item [ ' created_at ' ] } " )
body_lines . append ( " --- " )
body_lines . append ( " 来自 Xian Favor 收藏系统 " )
body = " \n " . join ( body_lines )
# 调用邮件发送技能
try :
import smtplib
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
# SMTP配置( 从环境变量或配置文件获取)
smtp_host = os . environ . get ( ' SMTP_HOST ' , ' smtp.exmail.qq.com ' )
smtp_port = int ( os . environ . get ( ' SMTP_PORT ' , 465 ) )
smtp_user = os . environ . get ( ' SMTP_USER ' , ' wlq@tphai.com ' )
smtp_pass = os . environ . get ( ' SMTP_PASS ' , ' ' )
if not smtp_pass :
return jsonify ( { ' success ' : False , ' error ' : ' SMTP密码未配置, 请设置环境变量 SMTP_PASS ' } ) , 500
msg = MIMEMultipart ( )
msg [ ' From ' ] = smtp_user
msg [ ' To ' ] = email_addr
msg [ ' Subject ' ] = subject
msg . attach ( MIMEText ( body , ' plain ' , ' utf-8 ' ) )
with smtplib . SMTP_SSL ( smtp_host , smtp_port ) as server :
server . login ( smtp_user , smtp_pass )
server . sendmail ( smtp_user , email_addr , msg . as_string ( ) )
return jsonify ( { ' success ' : True , ' message ' : f ' 已发送到 { email_addr } ' } )
except Exception as e :
return jsonify ( { ' success ' : False , ' error ' : f ' 发送失败: { str ( e ) } ' } ) , 500
# ============ Web 页面 ============
@app.route ( ' / ' )
@@ -229,6 +473,7 @@ INDEX_TEMPLATE = '''
<a href= " # " data-filter= " completed " ><i class= " bi bi-check-circle " ></i> 已完成</a>
<hr class= " border-secondary " >
<a href= " # " onclick= " showTagManager(); return false; " ><i class= " bi bi-tags " ></i> 标签管理</a>
<a href= " # " onclick= " showEmailManager(); return false; " ><i class= " bi bi-envelope " ></i> 邮箱管理</a>
</nav>
</div>
@@ -246,6 +491,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>
@@ -415,7 +663,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>
@@ -486,10 +739,18 @@ INDEX_TEMPLATE = '''
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<div class= " mb-3 " >
<div class= " input-group " >
<input type= " text " id= " newTagName " class= " form-control " placeholder= " 新标签名称 " >
<button class= " btn btn-primary " onclick= " createTag() " ><i class=" bi bi-plus " ></i> 创建</button >
<div class= " row mb-3" >
<div class= " col " >
<div class= " input-group " >
<input type= " text " id= " tagSearch " class= " form-control " placeholder= " 搜索标签... " >
<button class= " btn btn-outline-secondary " onclick= " loadTagManagerList() " ><i class= " bi bi-search " ></i></button>
</div>
</div>
<div class= " col " >
<div class= " input-group " >
<input type= " text " id= " newTagName " class= " form-control " placeholder= " 新标签名称 " >
<button class= " btn btn-primary " onclick= " createTag() " ><i class= " bi bi-plus " ></i> 创建</button>
</div>
</div>
</div>
<div id= " tagListContainer " >
@@ -503,6 +764,112 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 邮箱管理模态框 -->
<div class= " modal fade " id= " emailManagerModal " tabindex= " -1 " >
<div class= " modal-dialog modal-lg " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " ><i class= " bi bi-envelope " ></i> 邮箱管理</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<div class= " row mb-3 " >
<div class= " col-6 " >
<input type= " text " id= " newEmailAddr " class= " form-control " placeholder= " 邮箱地址 " >
</div>
<div class= " col-4 " >
<input type= " text " id= " newEmailName " class= " form-control " placeholder= " 备注名称(可选) " >
</div>
<div class= " col-2 " >
<button class= " btn btn-primary w-100 " onclick= " createEmail() " ><i class= " bi bi-plus " ></i> 添加</button>
</div>
</div>
<div id= " emailListContainer " >
<!-- 动态填充 -->
</div>
</div>
<div class= " modal-footer " >
<button type= " button " class= " btn btn-secondary " data-bs-dismiss= " modal " >关闭</button>
</div>
</div>
</div>
</div>
<!-- 发送邮件模态框 -->
<div class= " modal fade " id= " sendEmailModal " tabindex= " -1 " >
<div class= " modal-dialog " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " ><i class= " bi bi-envelope " ></i> 发送到邮箱</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<input type= " hidden " id= " sendEmailItemId " >
<div class= " mb-3 " >
<label class= " form-label " >选择已有邮箱</label>
<select id= " sendEmailSelect " class= " form-select " >
<option value= " " >-- 选择邮箱 --</option>
</select>
</div>
<div class= " mb-3 " >
<label class= " form-label " >或输入新邮箱</label>
<input type= " email " id= " sendEmailInput " class= " form-control " placeholder= " 输入邮箱地址 " >
</div>
<div id= " sendEmailLoading " style= " display:none; " >
<div class= " text-center py-2 " >
<div class= " spinner-border text-primary " role= " status " ></div>
<div class= " mt-2 " >正在发送...</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 " onclick= " sendItemEmail() " >
<i class= " bi bi-send " ></i> 发送
</button>
</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>
const API_BASE = ' /api ' ;
@@ -518,6 +885,9 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById( ' addTags ' ).addEventListener( ' input ' , showTagSuggestions);
document.getElementById( ' editTags ' ).addEventListener( ' input ' , showTagSuggestionsEdit);
// 标签搜索实时过滤
document.getElementById( ' tagSearch ' )?.addEventListener( ' input ' , debounce(loadTagManagerList, 300));
// 类型切换时显示/隐藏字段
document.getElementById( ' addType ' ).addEventListener( ' change ' , (e) => {
const type = e.target.value;
@@ -527,6 +897,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));
@@ -600,6 +975,7 @@ function renderItems(items) {
</div>
<div class= " d-flex align-items-center gap-1 flex-wrap ms-2 " onclick= " event.stopPropagation(); " >
$ { item.tags.slice(0, 2).map(t => `<span class= " badge bg-secondary " style= " font-size:10px; " >$ {t} </span>`).join( ' ' )}
<button class= " btn btn-sm btn-outline-info py-0 px-1 " onclick= " showSendEmailModal($ {item.id} ) " title= " 发送邮件 " ><i class= " bi bi-envelope " style= " font-size:11px; " ></i></button>
<button class= " btn btn-sm btn-outline-primary py-0 px-1 " onclick= " openEditModal($ {item.id} ) " title= " 编辑 " ><i class= " bi bi-pencil " style= " font-size:11px; " ></i></button>
$ { item.type === ' todo ' && item.status !== ' completed ' ? `<button class= " btn btn-sm btn-outline-success py-0 px-1 " onclick= " completeItem($ {item.id} ) " title= " 完成 " ><i class= " bi bi-check-lg " style= " font-size:11px; " ></i></button>` : ' ' }
<button class= " btn btn-sm btn-outline-danger py-0 px-1 " onclick= " deleteItem($ {item.id} ) " title= " 删除 " ><i class= " bi bi-trash " style= " font-size:11px; " ></i></button>
@@ -787,14 +1163,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 || ' ' ;
@@ -811,16 +1184,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,
@@ -964,20 +1342,42 @@ async function loadTagManagerList() {
if (!data.success) return;
const container = document.getElementById( ' tagListContainer ' );
if (!data.data.length) {
// 搜索过滤
const searchKeyword = document.getElementById( ' tagSearch ' ).value.trim().toLowerCase();
let tags = data.data;
if (searchKeyword) {
tags = tags.filter(t => t.name.toLowerCase().includes(searchKeyword));
}
if (!tags.length) {
container.innerHTML = ' <div class= " text-center text-muted py-3 " >暂无标签</div> ' ;
return;
}
container.innerHTML = data.da ta.map(tag => `
<div class= " d-flex justify-content-between align-items-center p-2 border-bottom " >
<div>
container.innerHTML = tags .map(tag => `
<div class= " d-flex justify-content-between align-items-center p-2 border-bottom " id= " tag-row-$ {tag.id} " >
<div id= " tag-display-$ {tag.id} " >
<span class= " badge bg-secondary " >$ {tag.name} </span>
<span class= " text-muted small ms-2 " >$ { tag.item_count || 0} 个条目</span>
</div>
<button class= " btn btn-sm btn-outline-danger " onclick= " deleteTagManager($ {tag.id} , ' $ {tag.name} ' ) " >
<i class= " bi bi-trash " ></i>
</button >
<div id= " tag-edit-$ {tag.id} " style= " display:none; " >
<input type= " text " class= " form-control form-control-sm " id= " edit-tag-name-$ {tag.id} " value= " $ {tag.name} " style= " width:150px; " >
</div >
<div class= " btn-group btn-group-sm " >
<button class= " btn btn-outline-primary " id= " tag-edit-btn-$ {tag.id} " onclick= " showEditTag($ {tag.id} ) " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
<button class= " btn btn-outline-success " id= " tag-save-btn-$ {tag.id} " style= " display:none; " onclick= " saveEditTag($ {tag.id} ) " title= " 保存 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-outline-secondary " id= " tag-cancel-btn-$ {tag.id} " style= " display:none; " onclick= " cancelEditTag($ {tag.id} , ' $ {tag.name} ' ) " title= " 取消 " >
<i class= " bi bi-x " ></i>
</button>
<button class= " btn btn-outline-danger " onclick= " deleteTagManager($ {tag.id} , ' $ {tag.name} ' ) " title= " 删除 " >
<i class= " bi bi-trash " ></i>
</button>
</div>
</div>
`).join( ' ' );
}
@@ -1008,6 +1408,45 @@ async function deleteTagManager(id, name) {
loadItems();
}
// 编辑标签
function showEditTag(id) {
document.getElementById(`tag-display-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-edit-$ {id} `).style.display = ' block ' ;
document.getElementById(`tag-edit-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-save-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`tag-cancel-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`edit-tag-name-$ {id} `).focus();
}
function cancelEditTag(id, oldName) {
document.getElementById(`edit-tag-name-$ {id} `).value = oldName;
document.getElementById(`tag-display-$ {id} `).style.display = ' block ' ;
document.getElementById(`tag-edit-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-edit-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`tag-save-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-cancel-btn-$ {id} `).style.display = ' none ' ;
}
async function saveEditTag(id) {
const newName = document.getElementById(`edit-tag-name-$ {id} `).value.trim();
if (!newName) return;
const res = await fetch(`$ {API_BASE} /tags/$ {id} `, {
method: ' PUT ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { name: newName })
});
if (res.ok) {
loadTagManagerList();
loadTags();
loadItems();
} else {
const data = await res.json();
alert(data.error || ' 更新失败 ' );
}
}
// 导出数据
async function exportData() {
const res = await fetch(`$ {API_BASE} /items?limit=1000`);
@@ -1031,6 +1470,289 @@ 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);
}
}
// ============ 邮箱管理 ============
let allEmails = [];
async function loadEmails() {
const res = await fetch(`$ {API_BASE} /emails`);
const data = await res.json();
if (data.success) {
allEmails = data.data;
}
}
async function showEmailManager() {
await loadEmailManagerList();
new bootstrap.Modal(document.getElementById( ' emailManagerModal ' )).show();
}
async function loadEmailManagerList() {
const res = await fetch(`$ {API_BASE} /emails`);
const data = await res.json();
if (!data.success) return;
allEmails = data.data;
const container = document.getElementById( ' emailListContainer ' );
if (!data.data.length) {
container.innerHTML = ' <div class= " text-center text-muted py-3 " >暂无邮箱</div> ' ;
return;
}
container.innerHTML = data.data.map(email => `
<div class= " d-flex justify-content-between align-items-center p-2 border-bottom " id= " email-row-$ {email.id} " >
<div id= " email-display-$ {email.id} " >
<span class= " fw-bold " >$ {email.email} </span>
$ { email.name ? `<span class= " text-muted ms-2 " >($ {email.name} )</span>` : ' ' }
</div>
<div id= " email-edit-$ {email.id} " style= " display:none; " >
<input type= " email " class= " form-control form-control-sm me-2 " id= " edit-email-addr-$ {email.id} " value= " $ {email.email} " style= " width:180px; " >
<input type= " text " class= " form-control form-control-sm me-2 " id= " edit-email-name-$ {email.id} " value= " $ { email.name || ' ' } " style= " width:120px; " >
</div>
<div class= " btn-group btn-group-sm " >
<button class= " btn btn-outline-primary " id= " email-edit-btn-$ {email.id} " onclick= " showEditEmail($ {email.id} ) " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
<button class= " btn btn-outline-success " id= " email-save-btn-$ {email.id} " style= " display:none; " onclick= " saveEditEmail($ {email.id} ) " title= " 保存 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-outline-secondary " id= " email-cancel-btn-$ {email.id} " style= " display:none; " onclick= " cancelEditEmail($ {email.id} ) " title= " 取消 " >
<i class= " bi bi-x " ></i>
</button>
<button class= " btn btn-outline-danger " onclick= " deleteEmailManager($ {email.id} , ' $ {email.email} ' ) " title= " 删除 " >
<i class= " bi bi-trash " ></i>
</button>
</div>
</div>
`).join( ' ' );
}
async function createEmail() {
const emailAddr = document.getElementById( ' newEmailAddr ' ).value.trim();
const emailName = document.getElementById( ' newEmailName ' ).value.trim();
if (!emailAddr) {
alert( ' 请输入邮箱地址 ' );
return;
}
const res = await fetch(`$ {API_BASE} /emails`, {
method: ' POST ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { email: emailAddr, name: emailName })
});
const data = await res.json();
if (data.success) {
document.getElementById( ' newEmailAddr ' ).value = ' ' ;
document.getElementById( ' newEmailName ' ).value = ' ' ;
loadEmailManagerList();
} else {
alert(data.error || ' 添加失败 ' );
}
}
async function deleteEmailManager(id, email) {
if (!confirm(`确认删除邮箱 " $ {email} " ? `)) return;
await fetch(`$ {API_BASE} /emails/$ {id} `, { method: ' DELETE ' });
loadEmailManagerList();
loadEmails();
}
function showEditEmail(id) {
document.getElementById(`email-display-$ {id} `).style.display = ' none ' ;
document.getElementById(`email-edit-$ {id} `).style.display = ' flex ' ;
document.getElementById(`email-edit-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`email-save-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`email-cancel-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`edit-email-addr-$ {id} `).focus();
}
function cancelEditEmail(id) {
const email = allEmails.find(e => e.id === id);
if (email) {
document.getElementById(`edit-email-addr-$ {id} `).value = email.email;
document.getElementById(`edit-email-name-$ {id} `).value = email.name || ' ' ;
}
document.getElementById(`email-display-$ {id} `).style.display = ' block ' ;
document.getElementById(`email-edit-$ {id} `).style.display = ' none ' ;
document.getElementById(`email-edit-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`email-save-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`email-cancel-btn-$ {id} `).style.display = ' none ' ;
}
async function saveEditEmail(id) {
const emailAddr = document.getElementById(`edit-email-addr-$ {id} `).value.trim();
const emailName = document.getElementById(`edit-email-name-$ {id} `).value.trim();
if (!emailAddr) {
alert( ' 邮箱地址不能为空 ' );
return;
}
const res = await fetch(`$ {API_BASE} /emails/$ {id} `, {
method: ' PUT ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { email: emailAddr, name: emailName })
});
const data = await res.json();
if (data.success) {
loadEmailManagerList();
loadEmails();
} else {
alert(data.error || ' 更新失败 ' );
}
}
// ============ 发送邮件 ============
async function showSendEmailModal(itemId) {
document.getElementById( ' sendEmailItemId ' ).value = itemId;
document.getElementById( ' sendEmailInput ' ).value = ' ' ;
document.getElementById( ' sendEmailLoading ' ).style.display = ' none ' ;
// 加载已有邮箱
await loadEmails();
const select = document.getElementById( ' sendEmailSelect ' );
select.innerHTML = ' <option value= " " >-- 选择邮箱 --</option> ' +
allEmails.map(e => `<option value= " $ {e.email} " >$ {e.email} $ { e.name ? ` ($ {e.name} )` : ' ' }</option>`).join( ' ' );
new bootstrap.Modal(document.getElementById( ' sendEmailModal ' )).show();
}
async function sendItemEmail() {
const itemId = document.getElementById( ' sendEmailItemId ' ).value;
let emailAddr = document.getElementById( ' sendEmailSelect ' ).value;
// 如果没选择,使用输入的邮箱
if (!emailAddr) {
emailAddr = document.getElementById( ' sendEmailInput ' ).value.trim();
}
if (!emailAddr) {
alert( ' 请选择或输入邮箱地址 ' );
return;
}
// 显示加载
document.getElementById( ' sendEmailLoading ' ).style.display = ' block ' ;
try {
const res = await fetch(`$ {API_BASE} /send-email`, {
method: ' POST ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { item_id: parseInt(itemId), email: emailAddr })
});
const data = await res.json();
document.getElementById( ' sendEmailLoading ' ).style.display = ' none ' ;
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById( ' sendEmailModal ' )).hide();
alert(data.message || ' 发送成功! ' );
// 刷新邮箱列表(新邮箱自动保存)
loadEmails();
} else {
alert(data.error || ' 发送失败 ' );
}
} catch (e) {
document.getElementById( ' sendEmailLoading ' ).style.display = ' none ' ;
alert( ' 请求失败: ' + e.message);
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {