# -*- coding: utf-8 -*-
"""
百家号一键存草稿
==================
使用步骤：
  1. 运行本脚本前，用 browser 工具点击标题引用 e171 进入编辑状态
     （browser → act → ref=e171 → click）
  2. 修改下方 TITLE 和 PARAGRAPHS
  3. python bjh_publish.py

已验证成功的操作：
  ✓ 标题输入：先 browser 工具点击 e171，再用 dispatchKey 输入
  ✓ 正文插入：通过 iframe.contentWindow.UE_V2.instants[key=content].execCommand('inserthtml')
  ✓ 图片插入：通过 iframe paste 事件
  ✓ 存草稿：browser 工具点击 ref=e18
"""
import sys, socket, json, time, urllib.request, base64, os, glob, random
from base64 import b64encode
sys.stdout.reconfigure(encoding='utf-8')

HOST = "localhost"
PORT = 28800
PAGE_ID = "0C993BDF32DDC631BFC69F41B0755EF3"

# ════════════════════════════════════════════════════════════
#  ★ 每次发布前修改这里 ★
# ════════════════════════════════════════════════════════════
TITLE = '请在这里填写文章标题'

PARAGRAPHS = [
    ('normal',  '这里是第一段正文内容。'),
    ('normal',  '这里是第二段正文内容。'),
    ('divider', ''),
    ('heading', '小标题示例'),
    ('normal',  '小标题下的内容。'),
]
# ════════════════════════════════════════════════════════════

def rand_sleep(min_sec, max_sec=None):
    if max_sec is None:
        time.sleep(min_sec)
    else:
        time.sleep(random.uniform(min_sec, max_sec))

def get_latest_image():
    image_dir = r'D:\wxpic'
    exts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp', '*.webp']
    all_images = []
    for ext in exts:
        all_images.extend(glob.glob(os.path.join(image_dir, ext)))
    if not all_images:
        raise FileNotFoundError(f"在 {image_dir} 目录下没有找到图片")
    return max(all_images, key=os.path.getmtime)

IMAGE_PATH = get_latest_image()
print(f"[INFO] 图片: {IMAGE_PATH}")

def fmt_para(style, text):
    if style == 'normal':
        return '<p style="margin:16px 0">　　' + text + '</p>'
    elif style == 'heading':
        return '<p style="font-weight:bold;font-size:18px;margin:20px 0 10px">' + text + '</p>'
    elif style == 'divider':
        return '<p style="text-align:center;color:#ccc;margin:12px 0">————————————————————</p>'
    return ''

# ── CDP ──────────────────────────────────────────────────────────────────────
def ws():
    import urllib.parse
    ws_url = "ws://%s:%s/devtools/page/%s" % (HOST, PORT, PAGE_ID)
    parsed = urllib.parse.urlparse(ws_url)
    sock = socket.create_connection((parsed.hostname, parsed.port or 80), timeout=15)
    req = ("GET %s HTTP/1.1\r\nHost: %s:%s\r\n"
           "Upgrade: websocket\r\nConnection: Upgrade\r\n"
           "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n"
           % (parsed.path, parsed.hostname, parsed.port))
    sock.send(req.encode())
    resp = b''
    while b'\r\n\r\n' not in resp:
        resp += sock.recv(4096)
    return sock

def send(sock, d):
    d = d.encode('utf-8')
    n = len(d)
    m = b'\x01\x02\x03\x04'
    if n <= 125: h = bytes([0x81, n|0x80]) + m
    elif n <= 65535: h = bytes([0x81, 0xFE]) + n.to_bytes(2,'big') + m
    else: h = bytes([0x81, 0xFF]) + n.to_bytes(8,'big') + m
    sock.send(h + bytes(d[i]^m[i%4] for i in range(n)))

def recv(sock):
    def rx(n):
        b = b''
        while len(b) < n: b += sock.recv(n-len(b))
        return b
    h = rx(2); n = h[1]&0x7f
    if n == 126: n = int.from_bytes(rx(2),'big')
    elif n == 127: n = int.from_bytes(rx(8),'big')
    return rx(n).decode('utf-8')

_mid = 0
def cdp(sock, expr, timeout=20):
    global _mid
    _mid += 1
    send(sock, json.dumps({'id':_mid,'method':'Runtime.evaluate','params':{'expression':expr,'returnByValue':True}}))
    sock.settimeout(timeout)
    while True:
        r = json.loads(recv(sock))
        if r.get('id') == _mid:
            return r.get('result',{}).get('result',{}).get('value', r.get('result',{}).get('result',{}).get('description',''))

def mouse(sock, x, y, n=1):
    global _mid
    for t in ['mousePressed','mouseReleased']:
        _mid += 1
        send(sock, json.dumps({'id':_mid,'method':'Input.dispatchMouseEvent','params':{'type':t,'x':x,'y':y,'button':'left','clickCount':n}}))
        recv(sock)
        time.sleep(0.05)

def key(sock, k, txt=''):
    global _mid
    for t in ['keyDown','keyUp']:
        _mid += 1
        p = {'type':t,'key':k}
        if txt: p['text'] = txt
        send(sock, json.dumps({'id':_mid,'method':'Input.dispatchKeyEvent','params':p}))
        recv(sock)
    time.sleep(0.06)

# ── 主流程 ────────────────────────────────────────────────────────────────────
sock = ws()
print("[OK] 已连接")

# ── 标题（先用 browser 工具点击了 e171，这里直接键盘输入）──────────────────
print("\n[Step 1] 填标题...")
# Ctrl+A 全选再输入
key(sock, 'Control', 'a')
rand_sleep(0.15)
key(sock, 'Delete')
rand_sleep(0.2)
for ch in TITLE:
    key(sock, ch, ch)
rand_sleep(0.5)
r = cdp(sock, "var e=document.querySelectorAll('p');for(var p of e){if((p.innerText||'').trim().length>5&&!(p.innerText||'').includes('输入'))return 'OK:'+p.innerText.trim().substring(0,40);}return 'empty';")
print(f"  标题: {r}")

# ── 图片 ────────────────────────────────────────────────────────────────────
print("\n[Step 2] 插图片...")
if os.path.exists(IMAGE_PATH):
    with open(IMAGE_PATH,'rb') as f: img=b64encode(f.read()).decode()
    ext=os.path.splitext(IMAGE_PATH)[1].lower()
    mime='image/jpeg' if ext in ['.jpg','.jpeg'] else 'image/png'
    r=cdp(sock, "var ifr=document.querySelectorAll('iframe');for(var i of ifr){try{var d=i.contentDocument||i.contentWindow.document;var b=d.querySelector('body');if(!b)continue;b.focus();var s=i.contentWindow.getSelection();var r=d.createRange();r.setStart(b,0);r.collapse(true);s.removeAllRanges();s.addRange(r);var bs=atob('"+img+"');var ab=new ArrayBuffer(bs.length);var ia=new Uint8Array(ab);for(var j=0;j<bs.length;j++)ia[j]=bs.charCodeAt(j);var bl=new Blob([ab],{type:'"+mime+"'});var f=new File([bl],'c.jpg',{type:'"+mime+"'});var dt=new DataTransfer();dt.items.add(f);var e=new ClipboardEvent('paste',{bubbles:true,cancelable:true,clipboardData:dt});Object.defineProperty(e,'dataTransfer',{value:dt});b.dispatchEvent(e);return 'ok';}catch(e){}}return 'fail';", timeout=30)
    print(f"  图片: {r}")
    rand_sleep(6, 10)
else:
    print("  跳过（图片不存在）")

# ── 正文 ────────────────────────────────────────────────────────────────────
print(f"\n[Step 3] 填正文（共 {len(PARAGRAPHS)} 段）...")
ok=0
for i,(style,text) in enumerate(PARAGRAPHS):
    html=fmt_para(style,text)
    if not html.strip(): continue
    esc=html.replace("'","\\'")
    r=cdp(sock, "var ifr=document.querySelectorAll('iframe');for(var i of ifr){try{if(i.contentWindow.UE_V2&&i.contentWindow.UE_V2.instants){var ks=Object.keys(i.contentWindow.UE_V2.instants);for(var k of ks){var inst=i.contentWindow.UE_V2.instants[k];if(inst&&inst.key&&inst.key.includes('content')){try{inst.execCommand('inserthtml','"+esc+"');}catch(e){}}}}}catch(e){}}return 'ok';")
    if 'ok' in str(r): ok+=1
    else: print(f"  [{i}] {r}")
    rand_sleep(0.3, 0.6)
print(f"  正文: {ok}/{len(PARAGRAPHS)} 段")

# ── 封面（自动选第一张图，即文首图）────────────────────────────────────────
print("\n[Step 4] 封面...")
r=cdp(sock, "var els=document.querySelectorAll('*');for(var el of els){var t=(el.innerText||'').trim();if(t==='选择封面'){var r=el.getBoundingClientRect();return JSON.stringify({x:Math.round(r.left+r.width/2),y:Math.round(r.top+r.height/2)});}}return 'not_found';")
print(f"  封面按钮: {r}")

# ── 验证 ────────────────────────────────────────────────────────────────────
rand_sleep(2, 3)
r=cdp(sock, "var els=document.querySelectorAll('*');var info=[];for(var el of els){var t=(el.innerText||'').trim();if(t.includes('字数')&&t.includes('已保存'))info.push(t);}return info.join(' | ');")
print(f"\n[状态] {r}")
print(f"\n✅ 完成！请在浏览器中确认内容，然后点击'发布'")
sock.close()
