# -*- coding: utf-8 -*-
"""
百家号文章填写 + 存草稿（修复版）
"""
import sys, socket, json, time, urllib.request, base64, os, glob, random

sys.stdout.reconfigure(encoding='utf-8')

HOST = "localhost"
PORT = 9223

# ════════════════════════════════════════════════════════════
#  ★ 每次发布前修改这里 ★
# ════════════════════════════════════════════════════════════
TITLE = '一个抢到"第二医科大"，一个弯道超车拿下"山东医药大学"，同是更名，谁更赚？'

PARAGRAPHS = [
    ('normal', '在山东的高教版图上，最近最热闹的莫过于两所医学院的"改名记"。2023年，潍坊医学院更名为山东第二医科大学，校友们欢呼"圆梦"；2026年3月，滨州医学院更名为山东医药大学，舆论直接炸锅——"这名字比\'第二\'大气多了！"'),
    ('normal', '很多家长和考生在后台问我：这两所学校底子差不多，谁改名改得更"赚"？今天，我们就抛开情绪，从品牌稀缺性、录取红利、以及城市能级等多个维度，来一场实打实的"性价比"大比拼。'),
    ('divider', ''),
    ('heading', '01 名字格局：滨医赢麻了？'),
    ('normal', '先看结果。潍坊医学院→山东第二医科大学。名号里带个"第二"，在山东已有"第一医科大"的格局下，难免有种"千年老二"的既视感。'),
    ('normal', '滨州医学院→山东医药大学。直接去掉"滨州"的地域标签，换上"山东"省级头衔，还不带数字排序。"医药"二字医+药并举，学科覆盖面比单纯的"医科"更广。'),
    ('normal', '网友神评："第二医科大学赢了名次，却输了格局；医药大学看似没抢到\'第二\'，实则拿了个\'王炸\'。"这一局，滨医的名字确实更"唬人"。'),
    ('divider', ''),
    ('heading', '02 硬核实力：谁更有底气？'),
    ('normal', '但改名不能只看名字，还得看底子。山东第二医科大学（原潍医）：临床医学ESI全球前5‰，有博士培养项目，直属附属医院3所，国家级特色专业4个。'),
    ('normal', '山东医药大学（原滨医）：5个学科进入ESI全球前1%，临床医学进入前2.49‰，药理学与毒理学进入前5.2‰，国家级一流本科专业10个。更重要的是，它开创了我国残疾人高等医学教育的先河，康复医学特色全国独一份。'),
    ('normal', '从学科硬指标看，滨医的"厚度"甚至略胜一筹。'),
    ('divider', ''),
    ('heading', '03 网友热议：谁才是真正赢家？'),
    ('normal', '网上议论最热闹的，是滨医这一波"错位突围"的操作。2019年，滨医曾与潍医竞争"山东第二医科大学"，结果慢了半步。但滨医没有死磕"第三"，而是转攻"山东医药大学"——这波操作被网友称为"输掉了战斗，赢下了战争"。'),
    ('normal', '有人评论："潍医抢到了\'第二\'的名号，却让滨医拿走了\'省级\'的格局。以后考生报志愿，光看名字，山东医药大学听起来比第二医科大还牛。"'),
    ('normal', '但也有理性声音指出："名字只是一块敲门砖，最后还得看学科实力和就业质量。潍医有博士点，滨医有康复王牌，各有所长。"'),
    ('divider', ''),
    ('heading', '04 城市能级：潍坊的"独立" VS 烟台的"虹吸"'),
    ('normal', '这可能是决定未来就业最关键的一点。潍坊医学院→山东第二医科大学：潍坊是山东的人口大市、经济强市，但这所学校是潍坊唯一的"大学"（本科层次）。在潍坊，它是当之无愧的"高教长子"，市政府给钱给地毫不手软，本地就业资源会向它高度倾斜。'),
    ('normal', '滨州医学院→山东医药大学：虽然根在滨州，但学校的主阵地早已转移到了烟台。烟台本身就有烟台大学、鲁东大学，高教资源本就丰富。赚在哪？烟台的经济体量和生物医药产业（绿叶制药、荣昌生物等）远超潍坊。对于想进药企、搞研发的同学，山东医药大学背后的产业资源更"富贵"。亏在哪？滨州校区可能会面临边缘化，如果你是被调剂到滨州校区，体验感可能会打折扣。'),
    ('divider', ''),
    ('heading', '05 报考建议：2026届考生怎么选？'),
    ('normal', '如果非要分个高下——从"名字红利"看：滨医的"山东医药大学"显然更具想象空间，首年招生分数线大概率冲得更猛。'),
    ('normal', '从"特色优势"看：想学临床、有读博打算，潍医（山二医）底子更扎实；想做康复、残疾人医疗方向，滨医（山医药）全国独一份。'),
    ('normal', '从"捡漏机会"看：两校更名后热门专业都会涨分，但非热门专业可能存在"窗口期"。'),
    ('normal', '最后借用网友一句话收尾："名字决定起点，实力决定终点。谁更赚便宜，十年后再看。"'),
    ('divider', ''),
    ('normal', '互动问题：如果让你选，你更愿意报"山东第二医科大学"还是"山东医药大学"？评论区聊聊你的理由。'),
    ('divider', ''),
    ('normal', '说明：本文涉及山东第二医科大学（原潍坊医学院）的学科实力、办学层次、更名时间等信息，主要来源于山东第二医科大学官方网站（https://www.sdsmu.edu.cn）发布的官方数据，以及教育部官网公开发布的高校更名批复函件。山东医药大学（原滨州医学院）的相关信息来源于中新网山东新闻及山东省人民政府公开通知。'),
]
# ════════════════════════════════════════════════════════════

def rand_sleep(min_sec, max_sec):
    t = random.uniform(min_sec, max_sec)
    time.sleep(t)

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 format_paragraph(style, text):
    if style == 'normal':
        return '　　' + text
    elif style == 'heading':
        return text
    elif style == 'divider':
        return '————————————————————'
    return text

# ── CDP helpers ──────────────────────────────────────────────────────────────
def get_bjh_page_id():
    pages = json.loads(urllib.request.urlopen(f'http://{HOST}:{PORT}/json').read())
    for p in pages:
        url = p.get('url', '')
        if 'baijiahao.baidu.com' in url and ('edit' in url or 'builder' in url):
            return p['id']
    return None

def ws_connect(page_id):
    import urllib.parse
    ws_url = f"ws://{HOST}:{PORT}/devtools/page/{page_id}"
    parsed = urllib.parse.urlparse(ws_url)
    sock = socket.create_connection((parsed.hostname, parsed.port or 80), timeout=15)
    key = 'dGhlIHNhbXBsZSBub25jZQ=='
    handshake = (
        f'GET {parsed.path} HTTP/1.1\r\n'
        f'Host: {parsed.hostname}:{parsed.port}\r\n'
        'Upgrade: websocket\r\nConnection: Upgrade\r\n'
        f'Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'
    )
    sock.send(handshake.encode())
    resp = b''
    while b'\r\n\r\n' not in resp:
        resp += sock.recv(4096)
    return sock

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

def recv_frame(sock):
    def rx(n):
        buf = b''
        while len(buf) < n:
            buf += sock.recv(n - len(buf))
        return buf
    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')

_msg_id = 0
def call(sock, expr, timeout=15):
    global _msg_id
    _msg_id += 1
    mid = _msg_id
    send_frame(sock, json.dumps({
        'id': mid,
        'method': 'Runtime.evaluate',
        'params': {'expression': expr, 'returnByValue': True}
    }))
    sock.settimeout(timeout)
    while True:
        raw = recv_frame(sock)
        obj = json.loads(raw)
        if obj.get('id') == mid:
            res = obj.get('result', {}).get('result', {})
            return res.get('value', res.get('description', ''))

def screenshot(sock, filename):
    global _msg_id
    _msg_id += 1
    mid = _msg_id
    send_frame(sock, json.dumps({
        'id': mid,
        'method': 'Page.captureScreenshot',
        'params': {'format': 'png'}
    }))
    sock.settimeout(15)
    while True:
        raw = recv_frame(sock)
        obj = json.loads(raw)
        if obj.get('id') == mid:
            data = obj.get('result', {}).get('data', '')
            if data:
                script_dir = os.path.dirname(os.path.abspath(__file__))
                path = os.path.join(script_dir, filename)
                with open(path, 'wb') as f:
                    f.write(base64.b64decode(data))
                print(f"[截图] 已保存: {path}")
            return

# ── 主流程 ────────────────────────────────────────────────────────────────────
page_id = get_bjh_page_id()
if not page_id:
    print("[ERROR] 未找到百家号编辑页面（端口 9223）")
    sys.exit(1)
print(f"[OK] Page ID: {page_id}")

sock = ws_connect(page_id)
print("[OK] WebSocket 已连接")
time.sleep(1)

# ── Step 1: 填写标题 ─────────────────────────────────────────────────────────
print("\n[Step 1] 填写标题...")
r = call(sock, f"""
(function(){{
    // 方法1: 找到标题输入框 - cheetah-input 组件中的第一个 text 类型 input
    var inputs = document.querySelectorAll('input[type="text"]');
    var titleInput = null;

    for(var inp of inputs) {{
        // 找到 value 是数字的（字符数），它的前一个或相邻的可能就是标题
        if(inp.value && !isNaN(parseInt(inp.value)) && inp.value.length < 10) {{
            // 这个是字符计数显示，真正的标题可能在附近
            // 尝试找它的前一个兄弟元素或父元素
            var parent = inp.parentElement;
            if(parent) {{
                var prev = parent.previousElementSibling;
                if(prev) {{
                    var inputInPrev = prev.querySelector('input[type="text"]');
                    if(inputInPrev) {{ titleInput = inputInPrev; break; }}
                }}
                // 或者父元素本身就是 wrapper
                var wrapperInput = parent.querySelector('input');
                if(wrapperInput && wrapperInput !== inp) {{ titleInput = wrapperInput; break; }}
            }}
        }}
    }}

    // 方法2: 直接查找 cheetah-input 类名包含 css-ipx762 的 input
    if(!titleInput) {{
        var cheetahInputs = document.querySelectorAll('input.cheetah-input');
        for(var inp of cheetahInputs) {{
            if(inp.type === 'text' && inp.value.length < 10) {{
                titleInput = inp; break;
            }}
        }}
    }}

    // 方法3: 查找所有 input，按位置排序，找最上面的
    if(!titleInput) {{
        var allInputs = document.querySelectorAll('input');
        var minTop = 99999;
        for(var inp of allInputs) {{
            if(inp.type === 'text' && inp.getBoundingClientRect) {{
                var rect = inp.getBoundingClientRect();
                if(rect.top < minTop && rect.top > 0) {{
                    minTop = rect.top;
                    titleInput = inp;
                }}
            }}
        }}
    }}

    if(!titleInput) return 'title_not_found';

    // 设置值
    titleInput.focus();
    var nativeSetter = Object.getOwnPropertyDescriptor(
        Object.getPrototypeOf(titleInput), 'value'
    ).set;
    nativeSetter.call(titleInput, {json.dumps(TITLE)});
    titleInput.dispatchEvent(new Event('input', {{bubbles:true}}));
    titleInput.dispatchEvent(new Event('change', {{bubbles:true}}));

    return 'title_ok: ' + titleInput.value.substring(0,30);
}})()
""")
print(f"[title] {r}")
rand_sleep(2, 3)

# ── Step 2: 定位编辑器并清空 ─────────────────────────────────────────────────
print("\n[Step 2] 定位编辑器...")
r = call(sock, """
(function(){
    var selectors = [
        '[contenteditable="true"]',
        '.ProseMirror',
        '.ql-editor',
    ];
    for(var sel of selectors){
        var el = document.querySelector(sel);
        if(el && el.isContentEditable){
            // 不清空，直接追加
            return 'editor_found: ' + sel;
        }
    }
    return 'editor_not_found';
})()
""")
print(f"[editor] {r}")

if 'not_found' in str(r):
    print("[WARN] 未找到编辑器")
    screenshot(sock, "bjh_error.png")
    sock.close()
    sys.exit(1)

rand_sleep(1, 2)

# ── Step 3: 插入文首图片 ─────────────────────────────────────────────────────
print(f"\n[Step 3] 插入文首图片: {IMAGE_PATH}")
if os.path.exists(IMAGE_PATH):
    with open(IMAGE_PATH, 'rb') as f:
        img_data = f.read()
    img_b64 = base64.b64encode(img_data).decode('utf-8')
    ext = os.path.splitext(IMAGE_PATH)[1].lower()
    img_mime = 'image/jpeg' if ext in ['.jpg', '.jpeg'] else 'image/png'

    r = call(sock, f"""
(function(){{
    var selectors = ['[contenteditable="true"]', '.ProseMirror', '.ql-editor'];
    var editor = null;
    for(var sel of selectors){{
        var el = document.querySelector(sel);
        if(el && el.isContentEditable){{ editor = el; break; }}
    }}
    if(!editor) return 'editor_not_found';

    editor.focus();
    // 移动光标到最前面
    var sel = window.getSelection();
    var range = document.createRange();
    range.setStart(editor, 0);
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);

    // 通过 paste 事件插入图片
    var base64Data = '{img_b64}';
    var byteString = atob(base64Data);
    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for(var i=0; i<byteString.length; i++) ia[i] = byteString.charCodeAt(i);
    var blob = new Blob([ab], {{type: '{img_mime}'}});
    var file = new File([blob], 'cover_image.jpg', {{type: '{img_mime}'}});
    var dt = new DataTransfer();
    dt.items.add(file);
    var pasteEvent = new ClipboardEvent('paste', {{
        bubbles: true,
        cancelable: true,
        clipboardData: dt
    }});
    editor.dispatchEvent(pasteEvent);
    return 'image_paste_dispatched';
}})()
""", timeout=30)
    print(f"[image] {r}")
    rand_sleep(10, 15)  # 等待图片上传
else:
    print(f"[WARN] 图片不存在: {IMAGE_PATH}")

# ── Step 4: 逐段插入正文 ─────────────────────────────────────────────────────
print(f"\n[Step 4] 插入正文（共 {len(PARAGRAPHS)} 段）...")
for i, (style, text) in enumerate(PARAGRAPHS):
    formatted = format_paragraph(style, text)
    escaped = formatted.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
    r = call(sock, f"""
(function(){{
    var selectors = ['[contenteditable="true"]', '.ProseMirror', '.ql-editor'];
    var editor = null;
    for(var sel of selectors){{
        var el = document.querySelector(sel);
        if(el && el.isContentEditable){{ editor = el; break; }}
    }}
    if(!editor) return 'editor_not_found';

    editor.focus();
    // 使用 Range API 插入文本
    var sel = window.getSelection();
    var range = sel.rangeCount > 0 ? sel.getRangeAt(0) : document.createRange();
    range.selectNodeContents(editor);
    range.collapse(false);
    sel.removeAllRanges();
    sel.addRange(range);

    // 插入段落标记和文本
    document.execCommand('insertParagraph');
    document.execCommand('insertText', false, "{escaped}");

    return 'ok';
}})()
""")
    if r != 'ok':
        print(f"  [para {i}] {r}")
    rand_sleep(0.5, 1)  # 模拟打字间隔

print(f"[OK] 正文 {len(PARAGRAPHS)} 段插入完成")

# ── Step 5: 截图确认 ─────────────────────────────────────────────────────────
screenshot(sock, "bjh_before_save.png")
rand_sleep(2, 3)

# ── Step 6: 点击"存草稿"按钮 ────────────────────────────────────────────────
print("\n[Step 6] 点击存草稿...")
r = call(sock, """
(function(){
    var keywords = ['存草稿', '保存草稿', '草稿', '保存'];
    var all = document.querySelectorAll('button, a, span, div');
    // 优先精确匹配
    for(var kw of keywords){
        for(var el of all){
            var t = (el.innerText || '').trim();
            if(t === kw){
                var s = window.getComputedStyle(el);
                if(s.display !== 'none' && s.visibility !== 'hidden'){
                    el.click();
                    return 'clicked_exact: ' + kw + ' | ' + el.tagName + '.' + el.className.substring(0,30);
                }
            }
        }
    }
    // 模糊匹配
    for(var kw of keywords){
        for(var el of all){
            var t = (el.innerText || '').trim();
            if(t.includes(kw) && el.children.length === 0){
                var s = window.getComputedStyle(el);
                if(s.display !== 'none'){
                    el.click();
                    return 'clicked_fuzzy: ' + t + ' | ' + el.tagName;
                }
            }
        }
    }
    return '未找到存草稿按钮';
})()
""")
print(f"[存草稿] {r}")

if '未找到' in str(r):
    print("\n⚠️  未找到存草稿按钮！")
    screenshot(sock, "bjh_save_error.png")
    sock.close()
    sys.exit(1)

# 等待保存完成
print("[INFO] 等待保存完成（最多 30 秒）...")
saved = False
for i in range(30):
    time.sleep(1)
    r = call(sock, """
(function(){
    var all = document.querySelectorAll('*');
    for(var el of all){
        var t = (el.innerText || '').trim();
        if(t.includes('保存成功') || t.includes('已保存') || t.includes('草稿保存')){
            var s = window.getComputedStyle(el);
            if(s.display !== 'none') return 'SAVED:' + t;
        }
    }
    return 'waiting...';
})()
""")
    if 'SAVED' in str(r):
        print(f"  ✅ 第{i+1}秒: {r}")
        saved = True
        break
    if i % 5 == 0:
        print(f"  等待中... ({i+1}s)")

screenshot(sock, "bjh_after_save.png")

if saved:
    print("\n✅ 草稿保存成功！")
else:
    print("\n⚠️  未检测到保存成功信号，请查看截图确认状态")

sock.close()
print("\n[DONE] 脚本执行完毕")
