讓 Claude Code 讀懂公司的 Issue 系統:一個讀 Redmine 的自製 Skill


你把公司 issue 系統的連結貼給 Claude Code:「幫我看一下這張單」。

它打不開——因為那是內部系統,要登入、API 要認證、而且規格常常畫在截圖或 PowerPoint 附件裡,光讀文字會漏掉一半。

我寫了一個 skill 解決這件事:read-redmine-issue,一個 286 行的 Python 腳本,讓 Claude Code 能讀公司 Redmine 的 issue——含內文、留言、自訂欄位,還能把附件下載下來、小截圖自動放大讓 Claude Code 看得清楚。這篇拆解它。


為什麼不是 curl 就好

「不就是打個 API 嗎,curl 一下不行?」——實際上有四個坑:

為什麼 curl 不夠
認證內部系統走 OIDC(SSO)登入,REST API 只認 API key,瀏覽器 cookie 會被導去登入頁
附件規格常畫在截圖/簡報裡,要連附件一起抓,且附件下載也要帶認證
小截圖Redmine 的截圖常很小(500px 寬),直接餵給 Read 工具文字糊到看不清
中文編碼Windows 主控台預設 Big5,中文 issue 內容會變亂碼

這四件事 curl 都做不好。包成一個 skill,Claude Code 一句「看一下 issue 1234」就全部搞定。


Skill 的兩層結構

SKILL.md:觸發 + 用法

---
name: read-redmine-issue
description: 讀取公司 Redmine(redmine.internal)的 issue 內容。當使用者貼出
  redmine.internal/issues/<編號> 連結,或要求讀取/查看某個 Redmine issue 編號
  (標題、描述、留言、附件、自訂欄位)時使用。透過 Redmine REST API 以 API key 讀取。
---

description 列出觸發情境:貼連結、或說「讀一下 issue 1234」。Claude Code 看到 issue 連結就自動派這個 skill。

read_issue.py:實際引擎

python read_issue.py 1234              # 讀單一 issue
python read_issue.py 1234 1235 1236    # 讀多個
python read_issue.py 1234 --json       # 原始 JSON(要欄位細節時)
python read_issue.py 1234 --download   # 連附件一起下載,印出本機路徑

SKILL.md 是「何時用、怎麼用」,Python 是「怎麼做」。Skill 教 Claude Code 在對的時機跑對的指令。


核心 1:三層認證 + OIDC 陷阱

內部系統的登入是整個 skill 最麻煩的部分。腳本用三層 fallback:

def resolve_auth(args):
    # 1) API key(最穩,不受 OIDC 影響)
    key = args.key or os.environ.get("REDMINE_API_KEY")
    if not key and KEY_FILE.exists():
        key = KEY_FILE.read_text(encoding="utf-8").strip()
    if key:
        return {"X-Redmine-API-Key": key}, None, "API key"

    # 2) 手動給的 cookie / 環境變數 / 檔案
    cookie = args.cookie or os.environ.get("REDMINE_COOKIE")
    # ...
    if cookie:
        return {"Cookie": cookie}, None, "cookie (手動/檔案)"

    # 3) browser_cookie3 自動從瀏覽器讀
    import browser_cookie3 as bc
    # ... 從 chrome/edge/firefox 找 session cookie

最關鍵的一條經驗

# 注意: 此站 REST API(.json)只認 API key;
# session cookie 對 .json 會被導向 OIDC 登入。

這是踩過坑才知道的:公司系統走 SSO(OIDC / AWS Cognito),瀏覽器 cookie 能讀網頁,但打 .json REST API 會被導去登入頁。所以三層裡,API key 才是真正可靠的——它在 /my/account 取得、不會過期、不受 SSO 影響。cookie 那兩層是備援,實際上幾乎都用 API key。

API key 放在 .redmine_key 檔(已 gitignore,不進版控):

KEY_FILE = SCRIPT_DIR / ".redmine_key"

偵測「被導去登入頁」

def looks_like_login(resp):
    ctype = resp.headers.get("Content-Type", "")
    if "json" not in ctype:        # 不是 JSON → 大概是登入頁 HTML
        return True
    if "/oic/login" in resp.url or "/login" in resp.url:
        return True
    return False

認證失敗時 Redmine 不會回 401,而是 302 導去登入頁。腳本主動偵測「回應不是 JSON / URL 含 login」,給出明確錯誤(「cookie 過期,請更新或改用 API key」),而不是讓使用者對著一坨 HTML 困惑。


核心 2:附件下載 + 小截圖自動放大

這是整個 skill 最實用的功能。需求規格經常畫在截圖或簡報裡,光讀 issue 文字會漏。

def download_attachments(issue, issue_id, headers, cookies, stream):
    atts = issue.get("attachments") or []
    out_dir = Path(tempfile.gettempdir()) / "redmine" / str(issue_id)
    out_dir.mkdir(parents=True, exist_ok=True)
    for a in atts:
        url = a.get("content_url")
        r = requests.get(url, headers=headers, cookies=cookies, timeout=60)
        dest.write_bytes(r.content)
        stream.write(f"  • {dest}\n")
        if dest.suffix.lower() in IMAGE_EXTS:
            up = _maybe_upscale(dest)
            if up:
                stream.write(f"      ↳ 放大: {up}  ← 小圖建議讀這張\n")

下載到 %TEMP%\redmine\<id>\印出每個檔案的本機絕對路徑——Claude Code 拿到路徑就能用 Read 工具看圖(Read 原生支援 PNG/JPG)。

小圖放大:用 PyMuPDF 反推像素

SMALL_WIDTH = 800     # 寬度小於此值視為「小圖」
TARGET_WIDTH = 1600   # 放大目標

def _maybe_upscale(path):
    import fitz  # PyMuPDF
    pix = fitz.Pixmap(str(path))
    if pix.width >= SMALL_WIDTH:
        return None  # 夠大,不用放大
    factor = max(2, min(6, math.ceil(TARGET_WIDTH / max(pix.width, 1))))
    # 把小圖塞進 PDF page 再以 factor 倍率 render 出來
    doc = fitz.open()
    page = doc.new_page(width=pix.width, height=pix.height)
    page.insert_image(fitz.Rect(0, 0, pix.width, pix.height), filename=str(path))
    big = page.get_pixmap(matrix=fitz.Matrix(factor, factor))
    out = path.with_name(f"{path.stem}_{factor}x.png")
    big.save(str(out))
    return out

為什麼要放大:Redmine 上使用者貼的截圖常是 500-700px 寬的小圖。直接餵給 Read 工具,上面的文字(欄位名、錯誤訊息)糊到讀不出來。放大到 1600px 後,Claude Code 才看得清截圖上寫什麼。

放大後另存 xxx_4x.png 並標「← 小圖建議讀這張」,引導 Claude Code 讀放大版而不是原圖。

SKILL.md 還補了一條教訓:別走 issues/<id>.pdf 那條路——PDF 內嵌的是低解析縮圖,比直接抓原始 PNG 還糊。要清晰就抓原檔 + 放大。


核心 3:Windows 中文不亂碼

if sys.platform == "win32":
    import ctypes
    ctypes.windll.kernel32.SetConsoleOutputCP(65001)  # 主控台改 UTF-8
    ctypes.windll.kernel32.SetConsoleCP(65001)
for _stream in (sys.stdout, sys.stderr):
    _stream.reconfigure(encoding="utf-8")

Windows 主控台預設 Big5(cp950),印中文 issue 內容會變亂碼。腳本啟動時強制把主控台和 stdout/stderr 都切成 UTF-8——這樣 Claude Code 收到的輸出是乾淨的中文,不是 ???

這是 Windows 上跑 Python 給 AI 用的必備防呆。沒做這個,整份 issue 內容都會是亂碼。


核心 4:給 AI 看的錯誤訊息

腳本對每種失敗都給可執行的指引,不是丟個 stack trace:

if resp.status_code == 404:
    sys.stderr.write(f"#{issue_id} 找不到(404)— 編號錯誤或無權限\n")
elif resp.status_code in (401, 403) or looks_like_login(resp):
    sys.stderr.write(
        f"#{issue_id} 驗證失敗或被導向登入頁(HTTP {resp.status_code})。\n"
        "  → cookie 可能過期,請更新;或改用 API key(REST 須在後台啟用)。\n"
    )

當 Claude Code 跑這個 skill 遇到錯誤,看到的是「404,編號錯了或沒權限」「驗證失敗,去更新 API key」——它知道下一步怎麼辦。錯誤訊息為 AI 設計,跟我安全護欄 Hook 的設計哲學一樣。

還有個細節:--json 模式時,附件下載清單印到 stderr 而不是 stdout:

download_attachments(..., stream=sys.stderr if args.as_json else sys.stdout)

保持 stdout 是合法 JSON——如果把下載訊息混進 stdout,Claude Code 要 parse JSON 就會失敗。


怎麼跟 vibe-research 搭配

這個 skill 是 vibe-research 調查工作流 的第一步(Step 0:讀單):

vibe-research (全棧調查)
   └─ Step 0 → read-redmine-issue --download
                ├─ 讀 issue 內文 + 留言 + 自訂欄位
                ├─ 下載附件(規格截圖/簡報)
                └─ 小圖放大 → Claude Code 用 Read 看圖
   └─ Step 1 → 從截圖+文字抓出需求,開始追呼叫鏈

讀單是獨立能力——不一定每次都要全棧調查,有時就只想看一張單的內容。所以拆成獨立 skill,vibe-research 在需要時呼叫它。這是 skill 組合的典型分工:小 skill 做一件事、大 skill 編排它們。


給其他人的借鑑:包一個讀你 issue 系統的 skill

如果你的團隊用內部 issue 系統(Redmine / Jira / GitLab Issues / 自架),這個 pattern 可以抄:

  1. 認證優先用 API token:SSO 環境下,瀏覽器 cookie 對 REST API 多半無效,token 才穩。token 放 gitignore 的檔案,別進版控
  2. 附件一定要能抓:規格常在截圖/附件裡,只讀文字會漏。下載到暫存資料夾、印出本機路徑給 Read 工具
  3. 小圖放大:issue 截圖常很小,放大後 AI 才讀得清文字
  4. 編碼防呆:Windows 要強制 UTF-8,否則中文亂碼
  5. 錯誤訊息給 AI 看:404/認證失敗都給「下一步怎麼辦」,不是 stack trace
  6. stdout 保持乾淨--json 模式時雜訊印到 stderr,讓 AI 能 parse

結語

「讓 Claude Code 讀懂公司的 issue 系統」聽起來簡單,實際上卡在一堆瑣碎的坑:SSO 認證、附件、小圖、編碼。這些坑單獨看都不難,但每次手動處理很煩。

包成 skill 後,這些瑣事一次解決——以後一句「看一下 issue 1234」,Claude Code 就把內文、留言、放大過的截圖全部讀進來,直接進入「理解需求」的狀態。

286 行 Python,把「AI 看不到的內部系統」變成「AI 的第一手資料來源」。加上 vibe-research兩個查詢型 subagent,組成從「讀單」到「定位」到「實作」的完整鏈路——這是把 Claude Code 接進真實開發流程的關鍵一塊。