"""WeChat Viewer Flask 主应用"""
import subprocess
import json
import time
import os
from pathlib import Path
from flask import Flask, jsonify, request, render_template, send_file
import config
import wechat_db
import local_db
def _su_exists(path: str) -> bool:
try:
r = subprocess.run(
["su", "-c", f"test -f {path} && echo yes || echo no"],
capture_output=True, text=True, timeout=5
)
return "yes" in r.stdout
except Exception:
return False
def _safe_exists(path: str) -> bool:
try:
if Path(path).exists():
return True
except PermissionError:
return _su_exists(path)
except Exception:
return False
return False
import re
import html as html_module
app = Flask(__name__)
local_db.init_db()
def _extract_dedup_key(url: str) -> str:
if not url:
return ""
if "mp.weixin.qq.com" in url:
biz = re.search(r"__biz=([^&]+)", url)
mid = re.search(r"mid=(\d+)", url)
idx = re.search(r"idx=(\d+)", url)
sn = re.search(r"sn=([a-f0-9]+)", url)
if biz and mid and idx and sn:
return f"{biz.group(1)}|{mid.group(1)}|{idx.group(1)}|{sn.group(1)}"
return url
# ── Page: 文章列表 ──────────────────────────────────
@app.route("/articles")
def articles_page():
return render_template("articles.html")
@app.route("/api/articles")
def api_articles():
try:
sql = """SELECT m.msgId, m.talker, m.content, m.createTime
FROM message m WHERE m.type = 49 ORDER BY m.createTime DESC LIMIT 500"""
rows = wechat_db.query(sql)
url_groups = {}
for row in rows:
content = row.get("content", "") or ""
url_match = re.search(r"(.*?)", content)
if not url_match:
continue
url = url_match.group(1)
url = re.sub(r"", r"\1", url, flags=re.DOTALL)
if not url or url.startswith("https://support.weixin.qq.com"):
continue
dedup_key = url
if "mp.weixin.qq.com" in url:
biz = re.search(r"__biz=([^&]+)", url)
mid = re.search(r"mid=(\d+)", url)
idx = re.search(r"idx=(\d+)", url)
sn = re.search(r"sn=([a-f0-9]+)", url)
if biz and mid and idx and sn:
dedup_key = f"{biz.group(1)}|{mid.group(1)}|{idx.group(1)}|{sn.group(1)}"
if dedup_key not in url_groups:
title_match = re.search(r"
(.*?)", content)
desc_match = re.search(r"(.*?)", content)
title = title_match.group(1) if title_match else ""
title = html_module.unescape(re.sub(r"", r"\1", title, flags=re.DOTALL))
desc = desc_match.group(1) if desc_match else ""
desc = html_module.unescape(re.sub(r"", r"\1", desc, flags=re.DOTALL))
url_groups[dedup_key] = {"title": title or url, "description": desc[:200],
"createTime": row.get("createTime"), "url": url, "talkers": set()}
url_groups[dedup_key]["talkers"].add(row.get("talker", ""))
all_talkers = set()
for v in url_groups.values():
all_talkers.update(v["talkers"])
talker_names = {}
for t in all_talkers:
if t.endswith("@chatroom") or t.startswith("gh_"):
info = wechat_db.get_talker_info(t)
if info:
talker_names[t] = info.get("displayname", t) if isinstance(info, dict) else str(info)
for t in all_talkers:
if t not in talker_names:
talker_names[t] = t
articles = []
for dedup_key, v in url_groups.items():
groups = []
for t in sorted(v["talkers"]):
name = talker_names.get(t, t)
if isinstance(name, dict):
name = t
groups.append(str(name)[:30])
articles.append({"url": v.get("url", dedup_key), "title": v["title"],
"description": v["description"], "createTime": v["createTime"],
"shareCount": len(groups), "groups": groups})
articles.sort(key=lambda a: a["createTime"] or 0, reverse=True)
return jsonify({"ok": True, "data": articles})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/articles/summarize", methods=["POST"])
def api_article_summarize():
data = request.json or {}
url = data.get("url", "")
title = data.get("title", "")
if not url:
return jsonify({"ok": False, "error": "需要 url"}), 400
dedup_key = _extract_dedup_key(url)
if not dedup_key:
return jsonify({"ok": False, "error": "无法提取去重标识"}), 400
existing = local_db.find_article_summary(dedup_key, "summary")
if existing:
return jsonify({"ok": True, "data": {"status": "cached", "report_id": existing["id"],
"content": existing["content"], "summary": (existing.get("content", "") or "")[:200],
"created_at": existing.get("created_at", "")}})
import uuid, threading
task_id = uuid.uuid4().hex[:12]
result_file = f"/data/data/com.termux/files/home/tmp/article_summary_{task_id}.md"
prompt = (f"请抓取并阅读以下文章,生成一份核心观点摘要。\n\n"
f"文章标题: {title}\n文章链接: {url}\n\n"
f"要求:\n1. 用 terminal curl 或 web 抓取文章全文\n"
f"2. 提取 3-5 个核心观点,每个观点用一句话概括\n"
f"3. 列出文章中的关键数据或案例(如有)\n"
f"4. 格式为 Markdown,标题: # {title} 摘要\n"
f"5. 将结果写入文件: {result_file}\n6. 确认文件已写入\n\n"
f"重要: 只做抓取和摘要,完成后不要做其他操作。")
def _run():
try:
subprocess.run(["hermes", "chat", "-q", prompt, "--yolo", "-t", "terminal,web,file"],
capture_output=True, text=True, timeout=180, cwd=str(Path(__file__).parent))
if Path(result_file).exists():
md = Path(result_file).read_text(encoding="utf-8", errors="replace")
else:
md = f"# {title} 摘要\n\n生成失败,请稍后重试。"
local_db.report_add("article", title, "summary", md, 0, 0, 0, dedup_key=dedup_key, article_url=url)
try: Path(result_file).unlink()
except: pass
except Exception:
pass
threading.Thread(target=_run, daemon=True).start()
return jsonify({"ok": True, "data": {"status": "started", "task_id": task_id}})
@app.route("/api/articles/learn", methods=["POST"])
def api_article_learn():
data = request.json or {}
url = data.get("url", "")
title = data.get("title", "")
if not url:
return jsonify({"ok": False, "error": "需要 url"}), 400
dedup_key = _extract_dedup_key(url)
if not dedup_key:
return jsonify({"ok": False, "error": "无法提取去重标识"}), 400
existing = local_db.find_article_summary(dedup_key, "learn")
if existing:
return jsonify({"ok": True, "data": {"status": "cached", "report_id": existing["id"],
"content": existing["content"], "summary": (existing.get("content", "") or "")[:200],
"created_at": existing.get("created_at", "")}})
import uuid, threading
task_id = uuid.uuid4().hex[:12]
result_file = f"/data/data/com.termux/files/home/tmp/article_learn_{task_id}.md"
prompt = (f"请学习以下文章,按照 learn-and-build skill 的流程处理。\n\n"
f"文章标题: {title}\n文章链接: {url}\n\n"
f"要求:\n1. 抓取文章全文\n2. 判断文章类型(开源代码/论文/方法论)\n"
f"3. 按类型执行对应的学习+构建流程\n"
f"4. 将学习结果和构建内容写入文件: {result_file}\n5. 确认文件已写入\n\n"
f"完成后不要做其他操作。")
def _run():
try:
subprocess.run(["hermes", "chat", "-q", prompt, "-s", "learn-and-build", "--yolo", "-t", "terminal,web,file"],
capture_output=True, text=True, timeout=300, cwd=str(Path(__file__).parent))
if Path(result_file).exists():
md = Path(result_file).read_text(encoding="utf-8", errors="replace")
else:
md = f"# {title} 学习结果\n\n生成失败,请稍后重试。"
local_db.report_add("article", title, "learn", md, 0, 0, 0, dedup_key=dedup_key, article_url=url)
try: Path(result_file).unlink()
except: pass
except Exception:
pass
threading.Thread(target=_run, daemon=True).start()
return jsonify({"ok": True, "data": {"status": "started", "task_id": task_id}})
@app.route("/api/articles/check-summaries", methods=["POST"])
def api_article_check_summaries():
data = request.json or {}
urls = data.get("urls", [])
report_type = data.get("type", "summary")
if not urls:
return jsonify({"ok": False, "error": "需要 urls 数组"}), 400
url_map = {}
for item in urls:
url = item.get("url", "") if isinstance(item, dict) else str(item)
title = item.get("title", "") if isinstance(item, dict) else ""
dk = _extract_dedup_key(url)
if dk:
url_map[dk] = {"url": url, "title": title}
if not url_map:
return jsonify({"ok": True, "data": {"results": {}, "missing": urls}})
found = local_db.find_article_summaries_batch(list(url_map.keys()), report_type)
missing = []
for dk, info in url_map.items():
if dk not in found:
missing.append({"url": info["url"], "title": info["title"], "dedup_key": dk})
return jsonify({"ok": True, "data": {"results": found, "missing": missing}})
@app.route("/api/articles/generate-image", methods=["POST"])
def api_article_generate_image():
data = request.json or {}
title = data.get("title","")
url = data.get("url","")
if not title:
return jsonify({"ok":False,"error":"需要 title"}), 400
import uuid, threading
task_id = uuid.uuid4().hex[:12]
img_file = f"/data/data/com.termux/files/home/tmp/article_img_{task_id}.png"
prompt = f"为以下文章内容生成一张 19 世纪复古铜版画风格图片。\n\n文章标题: {title}\n\n要求:\n1. 生成中文 prompt: 复古老旧报纸版面,19世纪黑白铜版画蚀刻风格,泛黄做旧牛皮纸,折痕毛边污渍,顶部大号粗黑体报纸刊头={title[:20]},主标题加粗醒目,油墨印刷颗粒质感,黑白单色调,高细节,8K\n2. 用 GPT Image 2 API 生成图片(model=GPT Image 2, n=1, size=1024x1024)\n3. 将 base64 解码保存到 {img_file}\n4. 确认文件已写入\n\n重要: 完成后不要做其他操作。"
def _run():
try:
subprocess.run(["hermes","chat","-q",prompt,"-s","gpt-image-gen","--yolo","-t","terminal,web,file"],capture_output=True,text=True,timeout=300)
if Path(img_file).exists():
local_db.report_image_add(0, img_file, "wsj")
try: Path(img_file).unlink()
except: pass
except: pass
threading.Thread(target=_run, daemon=True).start()
return jsonify({"ok":True,"data":{"status":"started","task_id":task_id}})
# ── Page 1: 消息浏览器 ──────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/chat/")
def chat_page(talker):
return render_template("chat.html", talker=talker)
@app.route("/api/sessions")
def api_sessions():
try:
rows = wechat_db.get_session_list()
for r in rows:
r["summary"] = (r.get("last_message", "") or "")[:100]
return jsonify({"ok": True, "data": rows})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/talker/")
def api_talker_info(talker):
try:
info = wechat_db.get_talker_info(talker)
if info:
return jsonify({"ok": True, "data": info})
return jsonify({"ok": False, "error": "not found"}), 404
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/messages")
def api_messages():
talker = request.args.get("talker", "")
limit = int(request.args.get("limit", "40"))
offset = int(request.args.get("offset", "0"))
if not talker:
return jsonify({"ok": False, "error": "需要 talker"}), 400
try:
rows = wechat_db.get_messages(talker, limit=limit, offset=offset)
parsed = []
for row in rows:
p = _parse_message_content(row.get("content", "") or "", row.get("imgPath", "") or "", row.get("type", 0))
p["msgId"] = row.get("msgId"); p["type"] = row.get("type")
p["isSend"] = row.get("isSend"); p["createTime"] = row.get("createTime")
p["talker"] = row.get("talker")
parsed.append(p)
return jsonify({"ok": True, "data": parsed})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
# ── 消息内容解析 ────────────────────────────────────
def _parse_message_content(content: str, img_path: str, msg_type: int) -> dict:
result = {"text": "", "images": [], "voice": None, "articles": []}
clean_content = content or ""
clean_content = re.sub(r"", r"\1", clean_content, flags=re.DOTALL)
display_text = clean_content
if clean_content and "\n" in clean_content and ":" in clean_content.split("\n")[0]:
parts = clean_content.split("\n", 1)
if len(parts) == 2:
display_text = parts[1]
if msg_type == 1:
result["text"] = re.sub(r"<[^>]+>", "", display_text).strip() if "" in display_text else display_text.strip()
elif msg_type == 3:
if img_path:
result["images"].append({"src": img_path})
result["text"] = "[图片]"
elif msg_type == 34:
if img_path:
result["voice"] = {"path": img_path}
elif msg_type == 49:
url_match = re.search(r"(.*?)", clean_content)
title_match = re.search(r"(.*?)", clean_content)
desc_match = re.search(r"(.*?)", clean_content)
thumb_match = re.search(r"(.*?)", clean_content)
if url_match or title_match:
result["articles"].append({"url": url_match.group(1) if url_match else "",
"title": html_module.unescape(title_match.group(1)) if title_match else "",
"description": html_module.unescape(desc_match.group(1)) if desc_match else "",
"thumbnail": thumb_match.group(1) if thumb_match else ""})
txt = re.sub(r"<[^>]+>", "", display_text).strip()
if txt:
result["text"] = txt
elif msg_type == 47:
result["text"] = "[表情]"
elif msg_type == 43:
result["text"] = "[视频]"
elif msg_type == 754974769 or "" in clean_content:
cdata_clean = re.sub(r"", r"\1", clean_content)
fnick = re.search(r"(.*?)", cdata_clean)
favatar = re.search(r"(.*?)", cdata_clean)
fdesc = re.search(r"(.*?)", cdata_clean)
fcover = re.search(r"(.*?)", cdata_clean)
fthumb = re.search(r"(.*?)", cdata_clean)
fvideo = re.search(r".*?(.*?)", cdata_clean, re.DOTALL)
fdur = re.search(r"(\d+)", cdata_clean)
result["finder"] = {"nickname": html_module.unescape(fnick.group(1).strip()) if fnick else "",
"avatar": favatar.group(1).strip() if favatar else "",
"desc": html_module.unescape(fdesc.group(1).strip()) if fdesc else "",
"cover": (fcover.group(1) or fthumb.group(1)).strip() if (fcover or fthumb) else "",
"video_url": fvideo.group(1).strip() if fvideo else "", "duration": fdur.group(1) if fdur else "0"}
elif msg_type == 822083633:
display_name = re.search(r"(.*?)", clean_content)
refer_title = re.search(r"(.*?)", clean_content)
result["text"] = f"[引用 {display_name.group(1)}: {refer_title.group(1)}]" if display_name and refer_title else "[引用消息]"
elif msg_type == 10000:
result["text"] = "[系统消息]"
return result
# ── 媒体文件代理 ────────────────────────────────────
@app.route("/media/image")
def serve_image():
path = request.args.get("path", "")
if not path:
return "no path", 400
real_path = _resolve_image_path(path)
if not real_path:
return "image not found", 404
tmp_path = f"/tmp/img_{abs(hash(real_path))}"
if not _safe_exists(tmp_path):
subprocess.run(["su", "-c", f"cp {real_path} {tmp_path} && chmod 644 {tmp_path}"],
capture_output=True, text=True, timeout=10)
if _safe_exists(tmp_path):
return send_file(tmp_path)
return "image not found", 404
@app.route("/media/image/full")
def serve_image_full():
path = request.args.get("path", "")
if not path:
return "no path", 400
hash_part = path.replace("THUMBNAIL_DIRPATH://", "").replace("THUMBNAIL_DIR_PATH://", "")
clean = hash_part.replace("th_", "")
full_path = _resolve_image_path(f"THUMBNAIL_DIRPATH://{clean}")
if full_path:
tmp_path = f"/tmp/img_full_{abs(hash(full_path))}"
if not _safe_exists(tmp_path):
subprocess.run(["su", "-c", f"cp {full_path} {tmp_path} && chmod 644 {tmp_path}"],
capture_output=True, text=True, timeout=10)
if _safe_exists(tmp_path):
return send_file(tmp_path)
return serve_image()
@app.route("/media/voice")
def serve_voice():
path = request.args.get("path", "")
if not path:
return "no path", 400
real_path = _resolve_voice_path(path)
if not real_path:
return "voice not found", 404
tmp_amr = f"/tmp/voice_{abs(hash(real_path))}.amr"
if not _safe_exists(tmp_amr):
subprocess.run(["su", "-c", f"cp {real_path} {tmp_amr} && chmod 644 {tmp_amr}"],
capture_output=True, text=True, timeout=10)
if not _safe_exists(tmp_amr):
return "voice not found", 404
mp3_path = tmp_amr.replace(".amr", ".mp3")
if not _safe_exists(mp3_path):
subprocess.run(["ffmpeg", "-y", "-i", tmp_amr, "-acodec", "libmp3lame", "-ab", "32k", mp3_path],
capture_output=True, text=True, timeout=30)
if _safe_exists(mp3_path):
return send_file(mp3_path, mimetype="audio/mpeg")
return send_file(tmp_amr)
def _resolve_image_path(path: str) -> str | None:
"""THUMBNAIL_DIRPATH://th_ → image2/XX/YY/.jpg (原图优先) 或 th_ (缩略图)"""
if not path:
return None
if path.startswith("/") and _safe_exists(path):
return path
path2 = path.replace("THUMBNAIL_DIRPATH://", "").replace("THUMBNAIL_DIR_PATH://", "")
if not path2:
return None
hash_part = path2.replace("th_", "")
if len(hash_part) >= 4:
sub1, sub2 = hash_part[:2], hash_part[2:4]
base = f"{config.WECHAT_DATA_ROOT}/image2/{sub1}/{sub2}"
for c in [f"{base}/{hash_part}.jpg", f"{base}/{hash_part}.png", f"{base}/{hash_part}", f"{base}/{path2}"]:
if _safe_exists(c):
return c
for ext in [".jpg", ".png", ""]:
c = f"{config.WECHAT_DATA_ROOT}/image2/{hash_part}{ext}"
if _safe_exists(c):
return c
return None
def _resolve_voice_path(path: str) -> str | None:
if not path:
return None
if _safe_exists(path):
return path
clean = path.replace("THUMBNAIL_DIRPATH://", "").replace("THUMBNAIL_DIR_PATH://", "")
try:
r = subprocess.run(["su", "-c", f"find {config.WECHAT_DATA_ROOT}/voice2 -name '*{clean}*' 2>/dev/null | head -1"],
capture_output=True, text=True, timeout=10)
found = r.stdout.strip()
if found:
return found
except Exception:
pass
return None
# ── Page 2: 数据中心 ────────────────────────────────
@app.route("/circle")
def circle_page():
return render_template("circle.html")
@app.route("/api/circle/stats")
def api_circle_stats():
try:
rows = wechat_db.query("SELECT COUNT(*) as cnt FROM message")
messages_total = rows[0]["cnt"] if rows else 0
rows = wechat_db.query("SELECT COUNT(DISTINCT talker) as cnt FROM message WHERE talker LIKE '%@chatroom'")
groups_total = rows[0]["cnt"] if rows else 0
conn = local_db.get_conn()
images_total = conn.execute("SELECT COUNT(*) FROM image_analysis").fetchone()[0]
reports_total = conn.execute("SELECT COUNT(*) FROM daily_reports WHERE report_date NOT IN ('summary','learn')").fetchone()[0]
conn.close()
return jsonify({"ok": True, "data": {"messages_total": messages_total, "groups_total": groups_total,
"images_total": images_total, "reports_total": reports_total}})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
# ── Page 3: 日报系统 ─────────────────────────────────
@app.route("/reports")
def reports_page():
return render_template("reports.html")
@app.route("/report/")
def report_detail(report_id):
return render_template("report_detail.html", report_id=report_id)
@app.route("/api/reports")
def api_reports():
try:
rows = local_db.report_list()
for r in rows:
r["summary"] = (r.get("content", "") or "")[:200]
r.pop("content", None)
return jsonify({"ok": True, "data": rows})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/report/")
def api_report_detail(report_id):
try:
row = local_db.report_get(report_id)
if row:
return jsonify({"ok": True, "data": row})
return jsonify({"ok": False, "error": "not found"}), 404
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/reports/generate", methods=["POST"])
def api_report_generate():
data = request.json or {}
chatroom_id = data.get("chatroom_id", "")
date_str = data.get("date", "")
if not chatroom_id or not date_str:
return jsonify({"ok": False, "error": "需要 chatroom_id 和 date"}), 400
try:
import uuid, threading
talker_info = wechat_db.get_talker_info(chatroom_id)
chatroom_name = str(talker_info.get("displayname", chatroom_id)) if talker_info else chatroom_id
if isinstance(chatroom_name, dict):
chatroom_name = chatroom_id
task_id = uuid.uuid4().hex[:12]
task_file = f"/data/data/com.termux/files/home/tmp/report_task_{task_id}.json"
result_file = f"/data/data/com.termux/files/home/tmp/report_result_{task_id}.md"
with open(task_file, "w") as f:
json.dump({"chatroom_id": chatroom_id, "date": date_str, "result_file": result_file}, f)
prompt = (f"你的任务是生成一份微信群的日报。\n\n参数文件: {task_file}\n群聊 ID: {chatroom_id}\n日期: {date_str}\n\n"
f"请按以下步骤操作:\n1. 用 mcp_android_phone_query_wechat 查询群 {chatroom_id} 在 {date_str} 的所有消息"
f"(时间戳范围: 该日期 00:00 到 23:59 的毫秒时间戳)\n"
f"2. 提取所有文本(type=1)、图片(type=3)、文章(type=49)、视频号(type=754974769)、引用(type=822083633)消息\n"
f"3. 对文章链接的处理:\n a. 先收集所有文章 URL 和标题,用 terminal curl 调 http://127.0.0.1:5000/api/articles/check-summaries\n"
f" 批量查询哪些文章已有摘要(POST JSON: {{\"urls\": [{{\"url\": \"...\", \"title\": \"...\"}}, ...], \"type\": \"summary\"}})\n"
f" b. 已有摘要的文章直接引用现有摘要内容\n c. 没有摘要的文章才需要抓取正文生成摘要\n"
f" d. 新生成的摘要用 terminal curl 调 POST http://127.0.0.1:5000/api/articles/summarize 写入 daily_reports\n"
f"4. 生成完整 Markdown 日报写入: {result_file}\n"
f"重要: 只做查询和生成,不要修改任何微信数据。完成后不要做其他操作。")
def _run_hermes():
nonlocal chatroom_id, chatroom_name, date_str, task_file, result_file
try:
result = subprocess.run(["hermes", "chat", "-q", prompt, "-s", "wechat-group-digest", "--yolo",
"-t", "terminal,file,web", "-m", "deepseek-v4-pro", "--provider", "deepseek"],
capture_output=True, text=True, timeout=300, cwd=str(Path(__file__).parent))
if Path(result_file).exists():
md_content = Path(result_file).read_text(encoding="utf-8", errors="replace")
elif result.stdout.strip():
md_content = result.stdout.strip()
else:
md_content = f"# 生成失败\n\n{result.stderr[:500]}"
except subprocess.TimeoutExpired:
md_content = "# 生成超时\n\n日报生成超过 3 分钟,请稍后重试。"
except Exception as e:
md_content = f"# 生成失败\n\n{str(e)}"
local_db.report_add(chatroom_id, chatroom_name, date_str, md_content, 0, 0, 0)
try: Path(task_file).unlink()
except: pass
try: Path(result_file).unlink()
except: pass
threading.Thread(target=_run_hermes, daemon=True).start()
return jsonify({"ok": True, "data": {"status": "started", "task_id": task_id}})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/report//delete", methods=["POST"])
def api_report_delete(report_id):
try:
local_db.report_delete(report_id)
return jsonify({"ok": True})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/reports/generate/status")
def api_report_status():
try:
rows = local_db.report_list(1)
if rows:
return jsonify({"ok": True, "data": rows[0]})
return jsonify({"ok": True, "data": None})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
# ── Page 4: 日报图片 ─────────────────────────────────
@app.route("/report-images")
def report_images_page():
return render_template("report_images.html")
@app.route("/api/report-images")
def api_report_images():
try:
rows = local_db.report_image_list()
return jsonify({"ok": True, "data": rows})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/media/report-image/")
def serve_report_image(image_id):
conn = local_db.get_conn()
row = conn.execute("SELECT image_path FROM report_images WHERE id = ?", (image_id,)).fetchone()
conn.close()
if row and row["image_path"] and _safe_exists(row["image_path"]):
return send_file(row["image_path"])
return "image not found", 404
@app.route("/api/reports//generate-image", methods=["POST"])
def api_report_generate_image(report_id):
try:
report = local_db.report_get(report_id)
if not report:
return jsonify({"ok": False, "error": "日报不存在"}), 404
import uuid, threading
task_id = uuid.uuid4().hex[:12]
content_file = f"/data/data/com.termux/files/home/tmp/report_content_{task_id}.md"
img_file = f"/data/data/com.termux/files/home/tmp/report_img_{task_id}.png"
Path(content_file).write_text(report["content"] or "", encoding="utf-8")
chatroom_name = report.get("chatroom_name", "群聊")
report_date = report.get("report_date", "")
prompt = (f"将微信日报生成19世纪复古铜版画风格图片。\n日报文件: {content_file}\n输出: {img_file}\n"
f"群名称: {chatroom_name}\n日期: {report_date}\n"
f"步骤: 1.read_file读日报 2.生成中文prompt(黑白铜版画蚀刻风,泛黄牛皮纸折痕毛边,大号粗黑体刊头={chatroom_name},"
f"复古工业插画,底部分栏,油墨颗粒质感,8K,黑白单色) 3.GPT Image 2 API(model='GPT Image 2',n=1,size='1024x1024') "
f"4.base64解码保存到{img_file} 5.ls确认")
def _run():
try:
subprocess.run(["hermes", "chat", "-q", prompt, "-s", "gpt-image-gen", "--yolo", "-t", "terminal,web,file"],
capture_output=True, text=True, timeout=300, cwd=str(Path(__file__).parent))
if Path(img_file).exists():
local_db.report_image_add(report_id, img_file, "wsj")
try: Path(content_file).unlink()
except: pass
try: Path(img_file).unlink()
except: pass
except Exception:
pass
threading.Thread(target=_run, daemon=True).start()
return jsonify({"ok": True, "data": {"status": "started", "task_id": task_id}})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/report-image//delete", methods=["POST"])
def api_report_image_delete(image_id):
try:
conn = local_db.get_conn()
row = conn.execute("SELECT image_path FROM report_images WHERE id = ?", (image_id,)).fetchone()
if row and row["image_path"]:
try: Path(row["image_path"]).unlink()
except: pass
conn.close()
local_db.report_image_delete(image_id)
return jsonify({"ok": True})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/report-image//send", methods=["POST"])
def api_report_image_send(image_id):
data = request.json or {}
wxid = data.get("to", "")
if not wxid:
return jsonify({"ok": False, "error": "需要 to (wxid)"}), 400
try:
conn = local_db.get_conn()
row = conn.execute("SELECT image_path FROM report_images WHERE id = ?", (image_id,)).fetchone()
conn.close()
if not row or not row["image_path"]:
return jsonify({"ok": False, "error": "图片不存在"}), 404
import shutil
dest = f"/sdcard/Pictures/report_{image_id}.png"
shutil.copy2(row["image_path"], dest)
import sys
def _send():
try:
sys.path.insert(0, "/data/data/com.termux/files/home/.hermes/event_bus")
from wechat_hook_send import send_image_via_hook
send_image_via_hook(wxid, dest)
except Exception:
pass
threading.Thread(target=_send, daemon=True).start()
return jsonify({"ok": True, "data": {"status": "sent"}})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)