fix: pipeline content tracking + topic article preview
db_helper.py: save_article now calculates and persists word_count generator.py: run_creator_blocking sets word_count for HTML-imported articles writer.py: fix title regex stripping content-leading numbers (35岁后→岁后) trends.py: fix Baidu hot_score str/int type comparison crash database.py: add missing content_tasks.org_id ALTER TABLE migration schemas.py + topics.py: topic list API returns article_count + articles[] previews topics.html: table view and card view show article badges with word counts, clickable to open preview Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"sessionID": "ses_1dede8a7affeG6DxtfPUOviXrp",
|
"sessionID": "ses_1dede8a7affeG6DxtfPUOviXrp",
|
||||||
"updatedAt": "2026-06-17T08:18:52.213Z",
|
"updatedAt": "2026-06-23T00:31:03.722Z",
|
||||||
"sources": {
|
"sources": {
|
||||||
"background-task": {
|
"background-task": {
|
||||||
"state": "active",
|
"state": "idle",
|
||||||
"reason": "2 background task(s) active",
|
"updatedAt": "2026-06-23T00:31:03.722Z"
|
||||||
"updatedAt": "2026-06-17T08:18:52.213Z"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,63 @@ def list_topics(
|
|||||||
else:
|
else:
|
||||||
query = query.order_by(sort_col.asc())
|
query = query.order_by(sort_col.asc())
|
||||||
|
|
||||||
return query.offset(offset).limit(limit).all()
|
topics = query.offset(offset).limit(limit).all()
|
||||||
|
# Enrich with article count and previews
|
||||||
|
from ..models import Article
|
||||||
|
topic_ids = [t.id for t in topics]
|
||||||
|
if topic_ids:
|
||||||
|
article_rows = (
|
||||||
|
db.query(Article.topic_id, Article.platform, Article.title, Article.word_count, Article.status)
|
||||||
|
.filter(Article.topic_id.in_(topic_ids))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
articles_by_topic: Dict[str, list] = {}
|
||||||
|
for ar in article_rows:
|
||||||
|
articles_by_topic.setdefault(ar.topic_id, []).append({
|
||||||
|
"platform": ar.platform,
|
||||||
|
"title": ar.title,
|
||||||
|
"word_count": ar.word_count,
|
||||||
|
"status": ar.status,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
articles_by_topic = {}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for t in topics:
|
||||||
|
t_dict = {
|
||||||
|
"id": t.id,
|
||||||
|
"field_id": t.field_id,
|
||||||
|
"field_name": t.field_name,
|
||||||
|
"org_id": t.org_id,
|
||||||
|
"title": t.title,
|
||||||
|
"format": t.format,
|
||||||
|
"core_concept": t.core_concept,
|
||||||
|
"audience_pain": t.audience_pain,
|
||||||
|
"unique_angle": t.unique_angle,
|
||||||
|
"priority": t.priority,
|
||||||
|
"priority_score": t.priority_score,
|
||||||
|
"total_score": t.total_score,
|
||||||
|
"status": t.status,
|
||||||
|
"tags": t.tags or [],
|
||||||
|
"custom_data": t.custom_data or {},
|
||||||
|
"scoring_data": t.scoring_data or {},
|
||||||
|
"lock_by": t.lock_by,
|
||||||
|
"lock_at": t.lock_at,
|
||||||
|
"series": t.series,
|
||||||
|
"created_at": t.created_at,
|
||||||
|
"updated_at": t.updated_at,
|
||||||
|
"generated_at": t.generated_at,
|
||||||
|
"reviewed_at": t.reviewed_at,
|
||||||
|
"ready_at": t.ready_at,
|
||||||
|
"published_at": t.published_at,
|
||||||
|
"compliance_score": t.compliance_score,
|
||||||
|
"platform_urls": t.platform_urls or {},
|
||||||
|
"cases": [],
|
||||||
|
"article_count": len(articles_by_topic.get(t.id, [])),
|
||||||
|
"articles": articles_by_topic.get(t.id, []),
|
||||||
|
}
|
||||||
|
result.append(t_dict)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
@router.get("/stats")
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ def run_creator_blocking(topic_id: str = None, timeout: int = 1800):
|
|||||||
existing = db.query(Article).filter(Article.id == article_id).first()
|
existing = db.query(Article).filter(Article.id == article_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
existing.html_content = html
|
existing.html_content = html
|
||||||
|
existing.word_count = len(html)
|
||||||
else:
|
else:
|
||||||
db.add(Article(id=article_id, topic_id=topic_id, platform=platform_dir, file_path=f"db:{article_id}", html_content=html, status="draft"))
|
db.add(Article(id=article_id, topic_id=topic_id, platform=platform_dir, file_path=f"db:{article_id}", html_content=html, word_count=len(html), status="draft"))
|
||||||
hf.unlink()
|
hf.unlink()
|
||||||
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
|
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
|
||||||
pdir = dd / platform_dir
|
pdir = dd / platform_dir
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ def init_db():
|
|||||||
("search_rankings", "org_id", "VARCHAR DEFAULT 'default'"),
|
("search_rankings", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||||
("geo_readiness_scores", "org_id", "VARCHAR DEFAULT 'default'"),
|
("geo_readiness_scores", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||||
("articles", "org_id", "VARCHAR DEFAULT 'default'"),
|
("articles", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||||
|
("content_tasks", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||||
("topics", "series", "VARCHAR"),
|
("topics", "series", "VARCHAR"),
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ class TopicResponse(TopicBase):
|
|||||||
compliance_score: Optional[int] = None
|
compliance_score: Optional[int] = None
|
||||||
platform_urls: Dict[str, str] = {}
|
platform_urls: Dict[str, str] = {}
|
||||||
cases: List[Any] = []
|
cases: List[Any] = []
|
||||||
|
article_count: int = 0
|
||||||
|
articles: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,17 @@
|
|||||||
<el-table-column prop="status" label="状态" width="90">
|
<el-table-column prop="status" label="状态" width="90">
|
||||||
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
|
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="文章" width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
<div v-if="scope.row.article_count > 0" style="display:flex;gap:4px;flex-wrap:wrap;">
|
||||||
|
<el-tag v-for="a in (scope.row.articles || [])" :key="a.platform" :type="a.status === 'published' ? 'success' : 'warning'" size="small" style="cursor:pointer;" @click="openTopicArticlePreview(scope.row, a)">
|
||||||
|
{{ platformShort(a.platform) }}
|
||||||
|
<span v-if="a.word_count" style="margin-left:2px;opacity:0.7;">{{ fmtWordCount(a.word_count) }}</span>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else style="color:#c0c4cc;font-size:12px;">无</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="compliance_score" label="合规分" width="90">
|
<el-table-column prop="compliance_score" label="合规分" width="90">
|
||||||
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
|
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -145,6 +156,10 @@
|
|||||||
<div class="topic-card-tags">
|
<div class="topic-card-tags">
|
||||||
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
|
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
|
||||||
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
|
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
|
||||||
|
<el-tag v-for="a in (topic.articles || [])" :key="a.platform" size="small" :type="a.status === 'published' ? 'success' : 'warning'" style="cursor:pointer;" @click="openTopicArticlePreview(topic, a)">
|
||||||
|
{{ platformShort(a.platform) }}{{ a.word_count ? ' ' + fmtWordCount(a.word_count) : '' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="topic.article_count === 0" size="small" type="info" effect="plain">无文章</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="topic-card-meta">
|
<div class="topic-card-meta">
|
||||||
<div>创建: {{ formatDate(topic.created_at) }}</div>
|
<div>创建: {{ formatDate(topic.created_at) }}</div>
|
||||||
@@ -534,6 +549,20 @@ const TopicsApp = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
platformName(platform) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[platform] || platform; },
|
platformName(platform) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[platform] || platform; },
|
||||||
|
platformShort(platform) { return { zhihu: '知', wechat: '公', xiaohongshu: '红' }[platform] || platform; },
|
||||||
|
fmtWordCount(wc) { if (!wc) return ''; if (wc >= 1000) return (wc / 1000).toFixed(wc >= 10000 ? 0 : 1) + 'k'; return wc + ''; },
|
||||||
|
openTopicArticlePreview(topic, article) {
|
||||||
|
this.previewTopic = topic;
|
||||||
|
this.previewPlatform = article.platform;
|
||||||
|
this.previewVisible = true;
|
||||||
|
this.platformContents = {};
|
||||||
|
this.previewImages = {};
|
||||||
|
const token = this.getToken();
|
||||||
|
if (!token) return;
|
||||||
|
fetch(`/api/articles/${topic.id}/preview?platform=${article.platform}`, { headers: { 'Authorization': 'Bearer ' + token } })
|
||||||
|
.then(r => r.ok ? r.json() : null).then(d => { if (d && d.html) { this.platformContents[article.platform] = d.html; if (d.images) this.previewImages[article.platform] = d.images; } })
|
||||||
|
.catch(e => console.error(`加载${article.platform}预览失败:`, e));
|
||||||
|
},
|
||||||
copyContent(platform) {
|
copyContent(platform) {
|
||||||
const html = this.platformContents[platform];
|
const html = this.platformContents[platform];
|
||||||
if (!html) { this.$message.warning('暂无内容可复制'); return; }
|
if (!html) { this.$message.warning('暂无内容可复制'); return; }
|
||||||
|
|||||||
@@ -246,12 +246,14 @@ def save_article(topic_id: str, platform: str, html_content: str, *, title: str
|
|||||||
from app.models import Article
|
from app.models import Article
|
||||||
article_id = f"{platform}_{topic_id}"
|
article_id = f"{platform}_{topic_id}"
|
||||||
existing = db.query(Article).filter(Article.id == article_id).first()
|
existing = db.query(Article).filter(Article.id == article_id).first()
|
||||||
|
word_count = len(content) if content else (len(html_content) if html_content else 0)
|
||||||
if existing:
|
if existing:
|
||||||
existing.html_content = html_content
|
existing.html_content = html_content
|
||||||
if title:
|
if title:
|
||||||
existing.title = title
|
existing.title = title
|
||||||
if content:
|
if content:
|
||||||
existing.content = content
|
existing.content = content
|
||||||
|
existing.word_count = word_count
|
||||||
else:
|
else:
|
||||||
article = Article(
|
article = Article(
|
||||||
id=article_id,
|
id=article_id,
|
||||||
@@ -261,6 +263,7 @@ def save_article(topic_id: str, platform: str, html_content: str, *, title: str
|
|||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
html_content=html_content,
|
html_content=html_content,
|
||||||
|
word_count=word_count,
|
||||||
status="draft",
|
status="draft",
|
||||||
compliance_score=None
|
compliance_score=None
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-1
@@ -193,7 +193,8 @@ def fetch_baidu_hot() -> List[Dict]:
|
|||||||
word = item.get("query", "").strip() or item.get("word", "").strip()
|
word = item.get("query", "").strip() or item.get("word", "").strip()
|
||||||
if not word:
|
if not word:
|
||||||
continue
|
continue
|
||||||
hot_score = item.get("hotScore", 0) or item.get("heat", 0)
|
hot_score_raw = item.get("hotScore", 0) or item.get("heat", 0)
|
||||||
|
hot_score = int(hot_score_raw) if hot_score_raw else 0
|
||||||
desc = item.get("desc", "")
|
desc = item.get("desc", "")
|
||||||
results.append({
|
results.append({
|
||||||
"domain": _guess_domain(word, desc),
|
"domain": _guess_domain(word, desc),
|
||||||
|
|||||||
+1
-1
@@ -557,7 +557,7 @@ class Writer:
|
|||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
line = re.sub(r'^\d+[.、)\s]+', '', line)
|
line = re.sub(r'^\d+[.、))]\s*', '', line)
|
||||||
line = line.strip('*#- \t"\'"''"')
|
line = line.strip('*#- \t"\'"''"')
|
||||||
# 跳过思考/建议类输出(如"不如:"、"或者:"、"建议方案"等)
|
# 跳过思考/建议类输出(如"不如:"、"或者:"、"建议方案"等)
|
||||||
if re.match(r'^(不如|或者|建议|推荐|参考|方案[一二三]|第[一二三]种|以[下是]|标题[一二三]|选项)', line):
|
if re.match(r'^(不如|或者|建议|推荐|参考|方案[一二三]|第[一二三]种|以[下是]|标题[一二三]|选项)', line):
|
||||||
|
|||||||
Reference in New Issue
Block a user