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 typing import List, Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
import pymysql import pymysql
@@ -38,6 +39,9 @@ def get_db_connection():
class ParseRequest(BaseModel): class ParseRequest(BaseModel):
text: str text: str
class SummaryRequest(BaseModel):
ids: List[str] = []
def update_status(id, status, error_msg=None): def update_status(id, status, error_msg=None):
try: try:
conn = get_db_connection() conn = get_db_connection()
@@ -258,6 +262,64 @@ def delete_record(id: str):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(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(): async def recover_pending_tasks():
""" """
Check for tasks stuck in PENDING or PROCESSING state (due to server restart) Check for tasks stuck in PENDING or PROCESSING state (due to server restart)

View File

@@ -99,6 +99,48 @@ body {
min-width: 0; 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 { .right-panel {
flex: 2; flex: 2;
display: flex; display: flex;

View File

@@ -7,7 +7,6 @@
<link rel="stylesheet" href="css/element-plus.index.css"> <link rel="stylesheet" href="css/element-plus.index.css">
<link rel="stylesheet" href="css/app.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"> <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/katex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
</head> </head>
@@ -154,12 +153,34 @@
<!-- Douyin Tab --> <!-- Douyin Tab -->
<div class="douyin-container" v-show="activeTab==='douyin'" style="padding: 24px; max-width: 95%; margin: 0 auto;"> <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;"> <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;"> <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 class="btn-primary" @click="fetchDouyinRecords" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle;">
🔄 刷新列表 🔄 刷新列表
</button> </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> </h1>
<p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p> <p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p>
</div> </div>

View File

@@ -21,6 +21,11 @@ createApp({
const douyinRecords = ref([]); const douyinRecords = ref([]);
let douyinTimer = null; let douyinTimer = null;
// Douyin Summary State
const showSummaryDialog = ref(false);
const summaryLoading = ref(false);
const summaryText = ref('');
// Handle window resize // Handle window resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768; isMobile.value = window.innerWidth <= 768;
@@ -432,11 +437,10 @@ createApp({
// Computed Properties (Markdown) // Computed Properties (Markdown)
// ========================================== // ==========================================
/*
marked.use({ marked.use({
gfm: true, gfm: true,
breaks: true, breaks: true,
mangle: false,
headerIds: false,
renderer: { renderer: {
code(code, language) { code(code, language) {
if (language === 'echarts') { if (language === 'echarts') {
@@ -447,6 +451,7 @@ createApp({
} }
} }
}); });
*/
const renderedAiText = computed(() => { const renderedAiText = computed(() => {
if (!aiText.value) return ""; 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 // 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) => { const statusColor = (status) => {
switch(status) { switch(status) {
case 'COMPLETED': return '#10b981'; case 'COMPLETED': return '#10b981';
@@ -658,6 +761,15 @@ createApp({
// Douyin // Douyin
shareText, douyinLoading, douyinRecords, shareText, douyinLoading, douyinRecords,
startParsing, fetchDouyinRecords, deleteRecord, statusColor, statusType, formatDate, startParsing, fetchDouyinRecords, deleteRecord, statusColor, statusType, formatDate,
// Douyin Summary
showSummaryDialog,
summaryLoading,
summaryText,
renderedSummary,
openSummaryDialog,
handleSummaryClose,
fetchDouyinSummary,
// Ad // Ad
showAd, adCountdown, closeAd 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>