初始化红餐观察库项目

This commit is contained in:
freedakgmail
2026-07-19 18:11:08 +08:00
commit c663539548
22 changed files with 1261 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Extract cross-article restaurant industry signals from cached Qwen analyses."""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
import urllib.error
import urllib.request
from datetime import datetime, timedelta
from app import DB_PATH, ensure_schema, now_iso
def call_qwen(items: list[dict], model: str) -> list[dict]:
key = os.getenv("DASHSCOPE_API_KEY", "").strip()
if not key:
raise RuntimeError("尚未配置 DASHSCOPE_API_KEY")
prompt = f"""你是餐饮产业首席分析师。根据以下多篇文章的结构化分析,识别跨文章、可验证、有经营意义的行业信号。
只返回合法 JSON{{"signals":[...]}}。每个 signal 必须包含:title(短标题)、summary100-180字)、trendemerging/accelerating/stable/declining)、confidence0到1)、article_ids(至少2个证据文章ID;确实只有单篇强信号时可为1个)、implications2-4条经营启示)、tags3-6个标签)。
不要把单一品牌新闻简单改写成行业信号;合并重复主题;最多输出8条;没有充分证据就少输出。
输入:{json.dumps(items, ensure_ascii=False)}"""
body = json.dumps({
"model": model,
"messages": [
{"role": "system", "content": "你进行基于证据的餐饮行业趋势聚类,避免空泛结论。"},
{"role": "user", "content": prompt},
],
"temperature": 0.2,
"response_format": {"type": "json_object"},
}, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
data=body,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as response:
result = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
raise RuntimeError(f"千问接口返回 {exc.code}{exc.read().decode('utf-8', errors='replace')[:500]}") from exc
text = result["choices"][0]["message"]["content"].strip()
if text.startswith("```"):
text = text.strip("`").removeprefix("json").strip()
return json.loads(text).get("signals", [])
def run(days: int) -> int:
ensure_schema()
model = os.getenv("QWEN_MODEL", "qwen-plus")
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT a.id,a.title,a.published_at,x.result_json
FROM articles a JOIN ai_analyses x ON x.article_id=a.id
WHERE a.crawl_status='success' AND a.published_at>=?
ORDER BY a.published_at DESC
""",
(cutoff,),
).fetchall()
items = []
for row in rows:
analysis = json.loads(row["result_json"])
items.append({
"article_id": row["id"], "title": row["title"], "published_at": row["published_at"],
"summary": analysis.get("summary"), "key_points": analysis.get("key_points", []),
"industry_signal": analysis.get("industry_signal"), "tags": analysis.get("tags", []),
})
if not items:
print(json.dumps({"status": "skipped", "reason": "没有已分析文章"}, ensure_ascii=False))
return 0
signals = call_qwen(items, model)
signal_date = datetime.now().astimezone().date().isoformat()
timestamp = now_iso()
conn.execute("DELETE FROM industry_signals WHERE signal_date=?", (signal_date,))
for signal in signals:
conn.execute(
"""
INSERT INTO industry_signals
(signal_date,title,summary,trend,confidence,implications_json,article_ids_json,tags_json,model,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
""",
(
signal_date, signal["title"], signal["summary"], signal.get("trend", "emerging"),
float(signal.get("confidence", 0.5)), json.dumps(signal.get("implications", []), ensure_ascii=False),
json.dumps(signal.get("article_ids", [])), json.dumps(signal.get("tags", []), ensure_ascii=False),
model, timestamp, timestamp,
),
)
conn.commit()
conn.close()
print(json.dumps({"status": "success", "input_articles": len(items), "signals": len(signals), "date": signal_date}, ensure_ascii=False))
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="提炼红餐文章跨文行业信号")
parser.add_argument("--days", type=int, default=30, help="聚合最近多少天的文章")
args = parser.parse_args()
raise SystemExit(run(args.days))