﻿#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""IMA OpenAPI 本地桥梁
用途：与《课程AI助手（v4.4）》配套，将浏览器中的 ima 请求从 127.0.0.1 转发到 ima.qq.com，解决浏览器 CORS 限制。
安全边界：只白名单转发两个检索端点；不把凭据写入磁盘，不记录请求内容。
启动：python ima-local-proxy.py   （默认 http://127.0.0.1:8766）
"""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

HOST = "127.0.0.1"
PORT = int(os.environ.get("IMA_PROXY_PORT", "8766"))
IMA_BASE = "https://ima.qq.com/openapi/wiki/v1"
ALLOWED_ENDPOINTS = {"search_knowledge_base", "search_knowledge"}


def cors_headers(handler, content_type="application/json; charset=utf-8"):
    handler.send_header("Access-Control-Allow-Origin", "*")
    handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
    handler.send_header("Access-Control-Allow-Headers", "Content-Type, ima-openapi-clientid, ima-openapi-apikey")
    handler.send_header("Access-Control-Max-Age", "86400")
    handler.send_header("Cache-Control", "no-store")
    handler.send_header("Content-Type", content_type)


class IMAProxyHandler(BaseHTTPRequestHandler):
    server_version = "LingxiIMAProxy/1.0"

    def log_message(self, fmt, *args):
        # 只记录方法和状态，不记录 Authorization / ima 凭据 / 查询正文
        sys.stderr.write("[ima-proxy] %s\n" % (fmt % args))

    def do_OPTIONS(self):
        self.send_response(204)
        cors_headers(self, "text/plain; charset=utf-8")
        self.end_headers()

    def do_GET(self):
        path = urllib.parse.urlparse(self.path).path
        if path == "/health":
            self._json(200, {"ok": True, "service": "ima-local-proxy", "allowed": sorted(ALLOWED_ENDPOINTS)})
            return
        if path == "/":
            text = (
                "<!doctype html><meta charset='utf-8'><title>IMA 本地桥梁</title>"
                "<style>body{font-family:Microsoft YaHei,sans-serif;max-width:680px;margin:48px auto;line-height:1.8;padding:0 20px}"
                "code{background:#f2f2f2;padding:2px 6px;border-radius:4px}</style>"
                "<h1>IMA 本地桥梁已启动</h1>"
                "<p>请回到《课程AI助手 v4.4》的「📒 知识库问答」页签，连接模式保持 <code>本地代理</code>。</p>"
                "<p>代理地址：<code>http://127.0.0.1:8766/api/ima</code></p>"
                "<p>本服务只做浏览器与 ima.qq.com 之间的白名单转发，不保存凭据和查询内容。</p>"
            ).encode("utf-8")
            self.send_response(200)
            cors_headers(self, "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(text)
            return
        self._json(404, {"code": 404, "msg": "not found", "hint": "GET /health 或 POST /api/ima/search_knowledge"})

    def do_POST(self):
        path = urllib.parse.urlparse(self.path).path.strip("/")
        prefix = "api/ima/"
        if not path.startswith(prefix) or path[len(prefix):] not in ALLOWED_ENDPOINTS:
            self._json(404, {"code": 404, "msg": "endpoint not allowed", "allowed": sorted(ALLOWED_ENDPOINTS)})
            return
        endpoint = path[len(prefix):]
        client_id = (self.headers.get("ima-openapi-clientid") or "").strip()
        api_key = (self.headers.get("ima-openapi-apikey") or "").strip()
        if not client_id or not api_key:
            self._json(401, {"code": 401, "msg": "缺少 ima Client ID 或 API Key；请在知识库模块中输入或启用教师个人版内置凭据"})
            return
        try:
            length = int(self.headers.get("Content-Length") or "0")
            payload = self.rfile.read(length)
            if length > 1024 * 1024:
                self._json(413, {"code": 413, "msg": "请求体过大"})
                return
            # 校验 JSON，避免灰色请求透传
            parsed = json.loads(payload.decode("utf-8") or b"{}".decode())
            body = json.dumps(parsed, ensure_ascii=False).encode("utf-8")
        except Exception as exc:
            self._json(400, {"code": 400, "msg": "JSON 请求体无效", "detail": str(exc)})
            return
        req = urllib.request.Request(
            IMA_BASE + "/" + endpoint,
            data=body,
            method="POST",
            headers={
                "Content-Type": "application/json; charset=utf-8",
                "ima-openapi-clientid": client_id,
                "ima-openapi-apikey": api_key,
                "User-Agent": "LingxiIMAProxy/1.0",
            },
        )
        try:
            with urllib.request.urlopen(req, timeout=60) as res:
                out = res.read()
                status = res.status
        except urllib.error.HTTPError as exc:
            out = exc.read()
            status = exc.code
        except Exception as exc:
            self._json(502, {"code": 502, "msg": "无法连接 ima.qq.com", "detail": str(exc)})
            return
        self.send_response(status)
        cors_headers(self, "application/json; charset=utf-8")
        self.end_headers()
        self.wfile.write(out)

    def _json(self, status, obj):
        data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        cors_headers(self)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


if __name__ == "__main__":
    server = ThreadingHTTPServer((HOST, PORT), IMAProxyHandler)
    print(f"IMA 本地桥梁已启动：http://{HOST}:{PORT}/api/ima")
    print("保持本窗口打开即可；关闭窗口即停止服务。Ctrl+C 退出。")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nIMA 本地桥梁已停止")