{
  "id": "SummarizeUrlContent2",
  "schema_version": "v1",
  "name_for_human": "網址內容摘要 (2)",
  "name_for_model": "SummarizeUrlContent2",
  "description_for_human": "使用者提供一個圖片、PDF 或 Word(docx) 的網址時，自動下載並由 AI 摘要重點內容",
  "description_for_model": "當使用者提供一個網址，且該網址指向圖片、PDF 或 Word(docx) 檔案，並想要了解、摘要或詢問該檔案內容時使用。此 plugin 會依檔案類型組出對應內容，直接交給 AI 摘要並回傳。",
  "auth": {
    "type": "none"
  },
  "api": {
    "type": "python",
    "python": {
      "direct_response": true,
      "source": "import base64\nimport io\nimport re\nimport zipfile\nfrom urllib.parse import urlparse\n\nimport requests\n\nIMAGE_EXTS = {\".jpg\", \".jpeg\", \".png\", \".gif\", \".webp\", \".bmp\"}\nDOCX_EXTS = {\".docx\"}\nPDF_EXTS = {\".pdf\"}\n\nSUMMARY_SYSTEM_PROMPT = (\n    \"你是一位擅長閱讀文件與圖片的助理，請用繁體中文摘要使用者提供的內容重點，\"\n    \"並條列出關鍵資訊。\"\n)\nSUMMARY_INSTRUCTION = \"請閱讀以下內容（可能是圖片、PDF 或 Word 文件），摘要其重點內容。\"\n\n\ndef _guess_kind(url: str) -> str:\n    path = urlparse(url).path.lower()\n    ext = \".\" + path.rsplit(\".\", 1)[-1] if \".\" in path else \"\"\n    if ext in IMAGE_EXTS:\n        return \"image\"\n    if ext in DOCX_EXTS:\n        return \"docx\"\n    if ext in PDF_EXTS:\n        return \"pdf\"\n    return \"unknown\"\n\n\ndef _extract_docx_text(file_bytes: bytes) -> str:\n    \"\"\"docx 是 zip 檔，沙箱沒有 python-docx，改用 zipfile + 正則取出純文字。\"\"\"\n    with zipfile.ZipFile(io.BytesIO(file_bytes)) as z:\n        xml_content = z.read(\"word/document.xml\").decode(\"utf-8\")\n    paragraphs = []\n    for p_match in re.findall(r\"<w:p[ >].*?</w:p>\", xml_content, flags=re.DOTALL):\n        texts = re.findall(r\"<w:t[^>]*>(.*?)</w:t>\", p_match, flags=re.DOTALL)\n        paragraph_text = \"\".join(texts)\n        if paragraph_text:\n            paragraphs.append(paragraph_text)\n    return \"\\n\".join(paragraphs)\n\n\ndef _extract_pdf_text(file_bytes: bytes) -> str:\n    \"\"\"\n    支援多種 PDF 擷取策略：\n    1. 優先嘗試第三方套件 (pypdf, pypdf2, fitz, pdfminer)\n    2. 若環境無第三方套件，使用純 Python (zlib + 正則) 解析文字串流與 CMap\n    \"\"\"\n    # 策略 1: pypdf / pypdf2\n    for mod_name in (\"pypdf\", \"pypdf2\"):\n        try:\n            mod = __import__(mod_name)\n            reader = mod.PdfReader(io.BytesIO(file_bytes))\n            pages_text = [p.extract_text() or \"\" for p in reader.pages]\n            full = \"\\n\".join(t for t in pages_text if t.strip())\n            if full.strip():\n                return full.strip()\n        except Exception:\n            pass\n\n    # 策略 2: fitz (PyMuPDF)\n    try:\n        import fitz\n        doc = fitz.open(stream=file_bytes, filetype=\"pdf\")\n        pages_text = [p.get_text() or \"\" for p in doc]\n        full = \"\\n\".join(t for t in pages_text if t.strip())\n        if full.strip():\n            return full.strip()\n    except Exception:\n        pass\n\n    # 策略 3: pdfminer\n    try:\n        from pdfminer.high_level import extract_text\n        full = extract_text(io.BytesIO(file_bytes))\n        if full and full.strip():\n            return full.strip()\n    except Exception:\n        pass\n\n    # 策略 4: 純標準函式庫 (zlib + CMap + Tj/TJ 串流解析)\n    try:\n        import zlib\n        streams = []\n        pos = 0\n        while True:\n            s_idx = file_bytes.find(b\"stream\", pos)\n            if s_idx == -1: break\n            e_idx = file_bytes.find(b\"endstream\", s_idx)\n            if e_idx == -1: break\n            raw = file_bytes[s_idx+6:e_idx].strip(b\"\\r\\n\")\n            try:\n                streams.append(zlib.decompress(raw))\n            except Exception:\n                streams.append(raw)\n            pos = e_idx + 9\n\n        char_map = {}\n        for st in streams:\n            if b\"beginbfchar\" in st:\n                for m in re.finditer(rb\"<([0-9a-fA-F]+)>\\s*<([0-9a-fA-F]+)>\", st):\n                    try:\n                        src = int(m.group(1), 16)\n                        dst = bytes.fromhex(m.group(2).decode(\"ascii\")).decode(\"utf-16be\", errors=\"ignore\")\n                        char_map[src] = dst\n                    except Exception:\n                        pass\n            if b\"beginbfrange\" in st:\n                for m in re.finditer(rb\"<([0-9a-fA-F]+)>\\s*<([0-9a-fA-F]+)>\\s*<([0-9a-fA-F]+)>\", st):\n                    try:\n                        s_c, e_c, d_c = int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)\n                        for c in range(s_c, e_c + 1):\n                            char_map[c] = chr(d_c + (c - s_c))\n                    except Exception:\n                        pass\n\n        text_pieces = []\n        for st in streams:\n            if b\"begincmap\" in st: continue\n            for m in re.finditer(rb\"\\((.*?)\\)\\s*Tj\", st):\n                text_pieces.append(m.group(1).decode(\"latin1\", errors=\"ignore\"))\n            for m in re.finditer(rb\"<([0-9a-fA-F\\s]+)>\\s*Tj\", st):\n                h = re.sub(rb\"\\s+\", b\"\", m.group(1))\n                chars = [char_map.get(int(h[i:i+4], 16), \"\") for i in range(0, len(h), 4) if len(h[i:i+4]) == 4]\n                text_pieces.append(\"\".join(chars))\n\n        extracted = \" \".join(\"\".join(text_pieces).split())\n        if len(extracted) > 20:\n            return extracted\n    except Exception:\n        pass\n\n    return \"\"\n\n\ndef _build_user_content(url: str):\n    \"\"\"\n    依網址類型組出要送給 chat() 的 user content。\n\n    - 圖片：直接把網址交給 AI（image_url 官方範例本身就是這樣用，已驗證可行）\n    - docx：本地下載並用 zipfile 解析出純文字，再以 text 內容交給 AI 摘要\n    - pdf：本地下載並以 _extract_pdf_text 擷取內文，再以 text 內容交給 AI 摘要\n    - 其他未知類型：比照圖片，直接把網址交給 AI 嘗試\n    \"\"\"\n    kind = _guess_kind(url)\n\n    if kind == \"docx\":\n        try:\n            resp = requests.get(url, timeout=30)\n            resp.raise_for_status()\n            doc_text = _extract_docx_text(resp.content)\n        except Exception as e:\n            return None, f\"下載或解析 docx 失敗: {e}\"\n        if not doc_text.strip():\n            return None, \"docx 內容為空，或無法解析出文字。\"\n        return [\n            {\"type\": \"text\", \"text\": SUMMARY_INSTRUCTION},\n            {\"type\": \"text\", \"text\": f\"文件內容如下：\\n\\n{doc_text[:30000]}\"},\n        ], None\n\n    if kind == \"pdf\":\n        try:\n            resp = requests.get(url, timeout=30)\n            resp.raise_for_status()\n            file_bytes = resp.content\n        except Exception as e:\n            return None, f\"下載 PDF 失敗: {e}\"\n\n        pdf_text = _extract_pdf_text(file_bytes)\n        if not pdf_text or not pdf_text.strip():\n            return None, \"PDF 內容為空，或為純圖片掃描檔而無法提取文字。\"\n\n        return [\n            {\"type\": \"text\", \"text\": SUMMARY_INSTRUCTION},\n            {\"type\": \"text\", \"text\": f\"PDF 文件內容如下：\\n\\n{pdf_text[:30000]}\"},\n        ], None\n\n    # image 或未知類型：直接把網址交給 AI\n    return [\n        {\"type\": \"text\", \"text\": SUMMARY_INSTRUCTION},\n        {\"type\": \"image_url\", \"image_url\": {\"url\": url}},\n    ], None\n\n\nasync def main():\n    # 1. 從對話中推論出使用者要處理的檔案網址（取代原本寫死的網址）\n    params = await infer_params(\n        conversation=CURRENT_CONVERSATION,\n        description=\"從使用者對話中取得欲摘要之檔案網址，該檔案可能是圖片、PDF 或 Word(docx) 文件\",\n        parameters={\n            \"type\": \"object\",\n            \"properties\": {\n                \"url\": {\n                    \"type\": \"string\",\n                    \"description\": \"使用者提供的檔案網址（圖片 / PDF / docx 皆可）\",\n                }\n            },\n            \"required\": [\"url\"],\n        },\n    )\n    url = (params or {}).get(\"url\", \"\").strip()\n    if not url:\n        print(\"沒有偵測到有效的檔案網址，請提供圖片、PDF 或 docx 的連結。\")\n        return\n\n    # 2. 依網址類型組出要送給 AI 的內容\n    content, err = _build_user_content(url)\n    if err:\n        print(err)\n        return\n\n    # 3. 直接呼叫內建的 chat() 做摘要，不需要任何外部 API key / Assistant ID\n    response = await chat(\n        conversation=[\n            {\"role\": \"system\", \"content\": SUMMARY_SYSTEM_PROMPT},\n            {\"role\": \"user\", \"content\": content},\n        ]\n    )\n\n    summary = response.get(\"content\", \"\")\n    if not summary:\n        print(\"AI 沒有回傳任何內容。\")\n        return\n\n    print(summary)\n\n\nawait main()\n"
    }
  }
}