"""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)