fix: 修复报告摘要生成并稳定默认端口

This commit is contained in:
meijiali
2026-07-03 12:20:59 +08:00
parent 5c34ce75b6
commit 0a3477cc44
10 changed files with 230 additions and 20 deletions
+17 -5
View File
@@ -88,11 +88,23 @@ class DouyinPlatform:
return self.map_items(payload, limit=limit)
def fetch_comments(self, source_item_id: str, *, limit: int) -> list[CommentData]:
payload = self.client.get(
"/api/v1/douyin/app/v3/fetch_video_comments",
params={"aweme_id": source_item_id, "cursor": 0, "count": 20},
)
return self.map_comments(payload, limit=limit)
comments: list[CommentData] = []
cursor = 0
page_size = min(20, limit)
while len(comments) < limit:
payload = self.client.get(
"/api/v1/douyin/app/v3/fetch_video_comments",
params={"aweme_id": source_item_id, "cursor": cursor, "count": page_size},
)
page_comments = self.map_comments(payload, limit=limit - len(comments))
comments.extend(page_comments)
data = payload.get("data", {})
next_cursor = data.get("cursor") or payload.get("cursor")
has_more = data.get("has_more", payload.get("has_more", 0))
if not page_comments or not has_more or next_cursor in (None, cursor):
break
cursor = next_cursor
return comments[:limit]
def _author_name(self, comment: dict[str, Any]) -> str | None:
user = comment.get("user") or {}
+42 -2
View File
@@ -42,6 +42,46 @@ def build_comment_prompt(comments: list[dict[str, str]]) -> str:
)
def build_report_summary_prompt(metrics: dict, typical: dict, *, word_limit: int) -> str:
sentiment = metrics.get("sentiment", {})
sentiment_lines = []
for name in ("positive", "neutral", "negative", "unknown"):
entry = sentiment.get(name, {})
sentiment_lines.append(f"- {name}: {entry.get('count', 0)} ({entry.get('pct', 0)}%)")
label_lines = [
f"- {label.get('name', '')}: {label.get('count', 0)}"
for label in metrics.get("top_labels", [])
if label.get("name")
]
if not label_lines:
label_lines = ["- 暂无"]
typical_lines = []
for sentiment_name, comments in typical.items():
for comment in comments:
typical_lines.append(f"- [{sentiment_name}] {str(comment.get('content', ''))[:150]}")
if not typical_lines:
typical_lines = ["- 暂无"]
item_count_line = ""
if "item_count" in metrics:
item_count_line = f"内容条目数量:{metrics.get('item_count', 0)}\n"
return (
f"只返回一段中文总结,不使用 Markdown,不超过 {word_limit} 字。\n"
"总结必须基于以下统计数据和典型评论,不要编造未提供的信息。\n"
f"{item_count_line}"
f"样本评论数量:{metrics.get('sample_count', 0)}\n"
"情绪分布:\n"
f"{chr(10).join(sentiment_lines)}\n"
"Top 标签:\n"
f"{chr(10).join(label_lines)}\n"
"典型评论:\n"
f"{chr(10).join(typical_lines)}"
)
def parse_ai_comment_response(response_text: str, *, expected_comment_ids: set[str]) -> list[AIAnalysisResult]:
try:
raw = json.loads(response_text)
@@ -118,7 +158,7 @@ class OpenAICompatibleAIClient:
self.model = model
self._http_client = http_client or httpx.Client(timeout=timeout_seconds)
def request(self, prompt: str) -> str:
def request(self, prompt: str, *, system_prompt: str = "请严格遵循用户指令输出。") -> str:
endpoint = f"{self.base_url}/chat/completions" if self.base_url.endswith("/v1") else f"{self.base_url}/v1/chat/completions"
response = self._http_client.post(
endpoint,
@@ -128,7 +168,7 @@ class OpenAICompatibleAIClient:
"messages": [
{
"role": "system",
"content": "只返回 JSON Array,不输出 Markdown 或解释性自然语言。",
"content": system_prompt,
},
{"role": "user", "content": prompt},
],
+6 -1
View File
@@ -128,7 +128,12 @@ def _safe_summary(summary_provider, metrics: dict, typical: dict, *, limit: int)
if summary_provider is None:
return DEFAULT_SUMMARY
try:
summary = summary_provider(metrics, typical)
summary = summary_provider(metrics, typical, word_limit=limit)
except TypeError:
try:
summary = summary_provider(metrics, typical)
except Exception:
return DEFAULT_SUMMARY
except Exception:
return DEFAULT_SUMMARY
return str(summary)[:limit]
+37 -7
View File
@@ -13,7 +13,7 @@ from app.platforms.base import PlatformAPIError, TikHubClient
from app.platforms.douyin import DouyinPlatform
from app.platforms.xiaohongshu import XiaohongshuPlatform
from app.schemas import CreateTaskRequest
from app.services.ai_service import OpenAICompatibleAIClient, analyze_comments_with_retry, calculate_analysis_status
from app.services.ai_service import OpenAICompatibleAIClient, analyze_comments_with_retry, build_report_summary_prompt, calculate_analysis_status
from app.services.report_service import generate_hotspot_report, generate_item_report
@@ -52,16 +52,25 @@ def build_platform(platform: str):
raise ValueError(f"Unsupported platform: {platform}")
def build_ai_requester():
def build_ai_client() -> OpenAICompatibleAIClient | None:
settings = get_settings()
if settings.ai_base_url and settings.ai_api_key and settings.ai_model:
client = OpenAICompatibleAIClient(
return OpenAICompatibleAIClient(
base_url=settings.ai_base_url,
api_key=settings.ai_api_key,
model=settings.ai_model,
timeout_seconds=settings.ai_timeout_seconds,
)
return client.request
return None
def build_ai_requester(client: OpenAICompatibleAIClient | None = None):
client = client if client is not None else build_ai_client()
if client is not None:
return lambda prompt: client.request(
prompt,
system_prompt="只返回 JSON Array,不输出 Markdown 或解释性自然语言。",
)
def fallback_requester(_prompt: str) -> str:
return "[]"
@@ -69,6 +78,25 @@ def build_ai_requester():
return fallback_requester
def build_report_summary_provider(client: OpenAICompatibleAIClient | None):
if client is None:
return None
def summarize_report(metrics: dict, typical: dict, *, word_limit: int = 200) -> str:
return client.request(
build_report_summary_prompt(metrics, typical, word_limit=word_limit),
system_prompt="只返回一段中文总结,不使用 Markdown。",
)
return summarize_report
def build_ai_dependencies():
client = build_ai_client()
requester = build_ai_requester(client)
return requester, build_report_summary_provider(client)
def create_task(session: Session, request: CreateTaskRequest, *, submit_background: bool = True) -> Task:
task = Task(
platform=request.platform,
@@ -108,6 +136,8 @@ def run_task(task_id: str, *, session_factory: Callable[[], Session] | sessionma
return
try:
platform = build_platform(task.platform)
ai_requester, report_summary_provider = build_ai_dependencies()
try:
hotspots = platform.fetch_hotspots(limit=task.hotspot_limit)
except PlatformAPIError as exc:
@@ -183,7 +213,7 @@ def run_task(task_id: str, *, session_factory: Callable[[], Session] | sessionma
item_comments = list(session.scalars(select(Comment).where(Comment.content_item_id == item.id)))
ai_results = analyze_comments_with_retry(
[{"comment_id": comment.id, "content": comment.content} for comment in item_comments],
requester=build_ai_requester(),
requester=ai_requester,
max_retries=get_settings().ai_max_retries,
)
results_by_comment_id = {result.comment_id: result for result in ai_results}
@@ -201,7 +231,7 @@ def run_task(task_id: str, *, session_factory: Callable[[], Session] | sessionma
comment.ai_analysis_status = result.ai_analysis_status
item.status = "success"
task.successful_items_count += 1
generate_item_report(session, item.id)
generate_item_report(session, item.id, summary_provider=report_summary_provider)
except Exception as exc:
item.status = "failed"
item.error_stage = "crawl_comments"
@@ -223,7 +253,7 @@ def run_task(task_id: str, *, session_factory: Callable[[], Session] | sessionma
total_count=total_comments,
)
for hotspot in task.hotspots:
generate_hotspot_report(session, hotspot.id)
generate_hotspot_report(session, hotspot.id, summary_provider=report_summary_provider)
task.status = "success"
else:
_mark_task_failed(task, task.error_stage or "crawl_items", task.error_type or "no_successful_items", task.error_message or "没有任何内容条目成功")