Compare commits

...

2 Commits

Author SHA1 Message Date
HuangHai
9efe1d3872 'commit' 2026-01-21 07:29:31 +08:00
HuangHai
21fb8b39d8 'commit' 2026-01-20 21:50:32 +08:00
9 changed files with 272 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import pymysql
@@ -38,6 +39,9 @@ def get_db_connection():
class ParseRequest(BaseModel):
text: str
class SummaryRequest(BaseModel):
ids: List[str] = []
def update_status(id, status, error_msg=None):
try:
conn = get_db_connection()
@@ -258,6 +262,64 @@ def delete_record(id: str):
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/douyin/summary")
async def generate_summary(request: SummaryRequest):
try:
# Fetch transcripts
conn = get_db_connection()
cursor = conn.cursor()
if request.ids:
# Secure way to handle list in SQL
format_strings = ','.join(['%s'] * len(request.ids))
sql = f"SELECT video_name, transcript FROM t_douyin_record WHERE id IN ({format_strings}) AND status='COMPLETED'"
cursor.execute(sql, tuple(request.ids))
else:
# Default to latest 20
cursor.execute("SELECT video_name, transcript FROM t_douyin_record WHERE status='COMPLETED' ORDER BY create_time DESC LIMIT 20")
records = cursor.fetchall()
conn.close()
if not records:
# If no records, just return a simple message stream
async def empty_stream():
yield "未找到可总结的已完成记录,请先解析视频。"
return StreamingResponse(empty_stream(), media_type="text/event-stream")
# Prepare text
full_text = ""
for r in records:
if r['transcript']:
full_text += f"【标题:{r['video_name']}】\n内容:{r['transcript']}\n\n"
if not full_text:
async def empty_text_stream():
yield "记录中没有有效的文案内容。"
return StreamingResponse(empty_text_stream(), media_type="text/event-stream")
# Prompt
prompt = f"""
请对以下充电行业相关的视频内容进行知识精华提取。
要求:
1. 忽略无关闲聊和口语化表达;
2. 按条目列出核心知识点,不要长篇大论;
3. 保持简洁专业,只保留干货;
4. 返回格式为Markdown列表。
内容如下:
{full_text[:15000]}
"""
# Limit context to avoid errors, 15000 chars is roughly safe for most models,
# but if using a small model, might need less. Assuming robust model.
return StreamingResponse(get_llm_response(prompt), media_type="text/event-stream")
except Exception as e:
logger.error(f"Summary generation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def recover_pending_tasks():
"""
Check for tasks stuck in PENDING or PROCESSING state (due to server restart)

View File

@@ -99,6 +99,48 @@ body {
min-width: 0;
}
/* Markdown Styles */
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
color: #334155; /* Slate 700 */
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: #0f172a; /* Slate 900 */
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body li {
margin: 0.25em 0;
}
.markdown-body strong {
font-weight: 600;
color: #0f172a;
}
.markdown-body blockquote {
padding: 0 1em;
color: #64748b;
border-left: 0.25em solid #cbd5e1;
margin: 0 0 16px 0;
}
.right-panel {
flex: 2;
display: flex;

View File

@@ -7,7 +7,6 @@
<link rel="stylesheet" href="css/element-plus.index.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
</head>
@@ -154,12 +153,34 @@
<!-- Douyin Tab -->
<div class="douyin-container" v-show="activeTab==='douyin'" style="padding: 24px; max-width: 95%; margin: 0 auto;">
<!-- Summary Dialog -->
<el-dialog v-model="showSummaryDialog" title="💡 知识精华总结" width="60%" :before-close="handleSummaryClose">
<div class="dialog-content" style="max-height: 60vh; overflow-y: auto; padding: 10px;">
<div v-if="summaryLoading && !summaryText" style="text-align: center; padding: 20px; color: #6b7280;">
<p>⚡ AI正在阅读您的知识库并提炼精华,请稍候...</p>
</div>
<div class="markdown-body" v-html="renderedSummary" style="font-size: 15px; line-height: 1.8; color: #1f2937;"></div>
<span v-if="summaryLoading && summaryText" class="cursor-blink">|</span>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleSummaryClose">关闭</el-button>
<el-button type="primary" @click="fetchDouyinSummary" :loading="summaryLoading">
{{ summaryText ? '重新生成' : '开始总结' }}
</el-button>
</span>
</template>
</el-dialog>
<div class="douyin-header" style="text-align: center; margin-bottom: 40px;">
<h1 style="font-size: 2.5rem; font-weight: bold; color: #f1f5f9; margin-bottom: 0.5rem;">
抖音知识库
<button class="btn-primary" @click="fetchDouyinRecords" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle;">
🔄 刷新列表
</button>
<button class="btn-primary" @click="openSummaryDialog" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); border: none;">
✨ 提取知识精华
</button>
</h1>
<p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p>
</div>

View File

@@ -20,7 +20,12 @@ createApp({
const douyinLoading = ref(false);
const douyinRecords = ref([]);
let douyinTimer = null;
// Douyin Summary State
const showSummaryDialog = ref(false);
const summaryLoading = ref(false);
const summaryText = ref('');
// Handle window resize
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768;
@@ -432,11 +437,10 @@ createApp({
// Computed Properties (Markdown)
// ==========================================
/*
marked.use({
gfm: true,
breaks: true,
mangle: false,
headerIds: false,
renderer: {
code(code, language) {
if (language === 'echarts') {
@@ -447,6 +451,7 @@ createApp({
}
}
});
*/
const renderedAiText = computed(() => {
if (!aiText.value) return "";
@@ -468,6 +473,61 @@ createApp({
}
});
// 内置简易 Markdown 解析器,确保不依赖外部库也能正常显示
const simpleMarkdown = (text) => {
if (!text) return '';
let lines = text.split('\n');
let html = '';
let inList = false;
// 辅助函数:解析行内样式(粗体、代码等)
const parseInline = (str) => {
return str
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
.replace(/`(.*?)`/g, '<code style="background:#f1f5f9; padding:2px 4px; border-radius:4px;">$1</code>'); // Code
};
for (let line of lines) {
let trimmed = line.trim();
// 忽略空行
if (!trimmed) {
continue;
}
// 标题处理 (###)
if (trimmed.startsWith('### ')) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h3 style="margin-top:16px; margin-bottom:8px; font-weight:bold; font-size:1.1em;">${parseInline(trimmed.substring(4))}</h3>`;
}
// 列表项处理 (- 或 1.)
else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
if (!inList) { html += '<ul style="padding-left:20px; margin:0;">'; inList = true; }
let content = trimmed.replace(/^(- |\d+\. )/, '');
html += `<li style="margin-bottom:4px;">${parseInline(content)}</li>`;
}
// 普通段落
else {
if (inList) { html += '</ul>'; inList = false; }
html += `<p style="margin-bottom:8px;">${parseInline(trimmed)}</p>`;
}
}
if (inList) html += '</ul>';
return html;
};
const renderedSummary = computed(() => {
if (!summaryText.value) return '';
// 优先使用内置解析器,确保绝对可用
try {
return simpleMarkdown(summaryText.value);
} catch (e) {
console.error("Simple markdown error:", e);
return summaryText.value;
}
});
// ==========================================
// Watchers
// ==========================================
@@ -572,6 +632,49 @@ createApp({
}
};
const openSummaryDialog = () => {
showSummaryDialog.value = true;
// Optionally auto-start if empty
if (!summaryText.value) {
fetchDouyinSummary();
}
};
const handleSummaryClose = () => {
showSummaryDialog.value = false;
};
const fetchDouyinSummary = async () => {
if (summaryLoading.value) return;
summaryText.value = "";
summaryLoading.value = true;
try {
// Pass empty IDs to summarize all/latest
const response = await fetch(apiBase.value + "/api/douyin/summary", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ids: []})
});
if (!response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {stream: true});
summaryText.value += chunk;
// Auto scroll? The user can scroll the dialog.
}
} catch (e) {
console.error(e);
summaryText.value += "\n(总结过程出错: " + e.message + ")";
} finally {
summaryLoading.value = false;
}
};
const statusColor = (status) => {
switch(status) {
case 'COMPLETED': return '#10b981';
@@ -658,6 +761,15 @@ createApp({
// Douyin
shareText, douyinLoading, douyinRecords,
startParsing, fetchDouyinRecords, deleteRecord, statusColor, statusType, formatDate,
// Douyin Summary
showSummaryDialog,
summaryLoading,
summaryText,
renderedSummary,
openSummaryDialog,
handleSummaryClose,
fetchDouyinSummary,
// Ad
showAd, adCountdown, closeAd
};

31
test_marked.html Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
</head>
<body>
<div id="output"></div>
<div id="error" style="color: red;"></div>
<script>
try {
marked.use({
gfm: true,
breaks: true,
renderer: {
code(code, language) {
if (language === 'echarts') {
return 'ECHARTS';
}
return false;
}
}
});
const text = "- **Test Bold**";
const html = marked.parse(text);
document.getElementById('output').innerHTML = html;
} catch (e) {
document.getElementById('error').innerText = e.toString();
}
</script>
</body>
</html>