配套脚本 · 本地开发

本地开发配置说明

本页面收录课程AI助手 v6.4 / v6.6 / v6.7 / v6.8 公开演示版所引用的两个本地脚本: IMA 本地代理3D 摄像头启动器。前者用于绕过浏览器跨域限制访问 ima 知识库; 后者是作者本机的 v6.8 启动辅助,仅在 work/avatar-camera/ 目录完整时可用。

为什么需要本地脚本?

所有 v6.x 公开演示版都运行在 zgdgt.cn 公开域名下。出于安全合规,公网页面里不内置任何 API 凭据, 也不直接连接第三方服务的服务器。两个例外场景需要本地脚本:

两个脚本均不收集、不上传访客数据,凭据仅留存在浏览器和/或访客本地 daemon 内存中。

IMA 本地代理(ima-local-proxy.py

一个 100% Python 标准库实现的 HTTP 代理,监听 127.0.0.1:8766, 只把白名单端点 search_knowledge_base / search_knowledge 转发到 https://ima.qq.com/openapi/wiki/v1。请求体里含 CORS 敏感的 Authorization: IMA 用 ima-openapi-clientid + ima-openapi-apikey 两个自定义头,代理直接转发,不解析、不缓存。

默认监听http://127.0.0.1:8766(可由环境变量 IMA_PROXY_PORT 覆盖)
运行环境Python 3.7+(仅用 http.server / urllib,零依赖)
白名单端点search_knowledge_basesearch_knowledge
请求体上限1 MiB(超过返回 413)
凭据存储不留盘;只在本次请求转发里携带
健康检查访问 http://127.0.0.1:8766/health,返回 JSON {"ok":true,"service":"ima-local-proxy",...}

启动步骤(Windows / macOS / Linux 通用)

# 进入下载目录 cd ~/Downloads/ # 或把文件放到任何目录 # 直接启动 python ima-local-proxy.py # 自定义端口 IMA_PROXY_PORT=9000 python ima-local-proxy.py # macOS/Linux set IMA_PROXY_PORT=9000 && python ima-local-proxy.py # Windows cmd

看到 IMA 本地桥梁已启动:http://127.0.0.1:8766/api/ima 即成功;关闭窗口即停止服务。

安全提示

① 代理只监听 127.0.0.1,外网无法访问(除非你显式改成 0.0.0.0,但不建议)。

② 需要填的 ima Client ID / API Key 只在该次请求里用到, 代理不缓存、不记日志、不写到磁盘;但你仍应保管好来源凭据。

③ 启动后浏览器会自动允许跨域(Access-Control-Allow-Origin: *), 这意味着代理启动期间任何访问 zgdgt.cn 的浏览器请求都能直接命中本地服务,用于课程用途无问题。

在课程AI助手里配合使用

① 进入 v6.4 / v6.6 / v6.7 / v6.8 任一公开演示版 → ② 切到「📒 知识库问答」页签 → ③ 连接模式保持「本地代理」 → ④ 输入你的 ima 凭据 → ⑤ 提问即可。

下载 ima-local-proxy.py(5934 B)

源代码预览

  1class=class="s">"c">#!/usr/bin/env python3
  2class=class="s">"c"># -*- coding: utf-class="n">8 -*-
  3"""IMA OpenAPI 本地桥梁
  4用途:与《课程AI助手(v4.class="n">4)》配套,将浏览器中的 ima 请求从 class="n">127.0.class="n">0.1 转发到 ima.qq.com,解决浏览器 CORS 限制。
  5安全边界:只白名单转发两个检索端点;不把凭据写入磁盘,不记录请求内容。
  6启动:python ima-local-proxy.py   (默认 http://class="n">127.0.class="n">0.1:class="n">8766)
  7"""
  8from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  9import json
 10import os
 11import sys
 12import urllib.error
 13import urllib.parse
 14import urllib.request
 15 
 16HOST = "class="n">127.0.class="n">0.1"
 17PORT = int(os.environ.get("IMA_PROXY_PORT", "class="n">8766"))
 18IMA_BASE = "https://ima.qq.com/openapi/wiki/v1"
 19ALLOWED_ENDPOINTS = {"search_knowledge_base", "search_knowledge"}
 20 
 21 
 22def cors_headers(handler, content_type="application/json; charset=utf-class="n">8"):
 23    handler.send_header("Access-Control-Allow-Origin", "*")
 24    handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
 25    handler.send_header("Access-Control-Allow-Headers", "Content-Type, ima-openapi-clientid, ima-openapi-apikey")
 26    handler.send_header("Access-Control-Max-Age", "class="n">86400")
 27    handler.send_header("Cache-Control", "no-store")
 28    handler.send_header("Content-Type", content_type)
 29 
 30 
 31class IMAProxyHandler(BaseHTTPRequestHandler):
 32    server_version = "LingxiIMAProxy/class="n">1.0"
 33 
 34    def log_message(self, fmt, *args):
 35        class=class="s">"c"># 只记录方法和状态,不记录 Authorization / ima 凭据 / 查询正文
 36        sys.stderr.write("[ima-proxy] %s\n" % (fmt % args))
 37 
 38    def do_OPTIONS(self):
 39        self.send_response(class="n">204)
 40        cors_headers(self, "text/plain; charset=utf-class="n">8")
 41        self.end_headers()
 42 
 43    def do_GET(self):
 44        path = urllib.parse.urlparse(self.path).path
 45        if path == "/health":
 46            self._json(class="n">200, {"ok": True, "service": "ima-local-proxy", "allowed": sorted(ALLOWED_ENDPOINTS)})
 47            return
 48        if path == "/":
 49            text = (
 50                "<!doctype html><meta charset=&class=class="s">"c">#x27;utf-class="n">8'><title>IMA 本地桥梁</title>"
 51                "<style>body{font-family:Microsoft YaHei,sans-serif;max-width:680px;margin:48px auto;line-height:class="n">1.8;padding:class="n">0 20px}"
 52                "code{background:class=class="s">"c">#f2f2f2;padding:2px 6px;border-radius:4px}</style>"
 53                "<h1>IMA 本地桥梁已启动</h1>"
 54                "<p>请回到《课程AI助手 v4.class="n">4》的「📒 知识库问答」页签,连接模式保持 <code>本地代理</code>。</p>"
 55                "<p>代理地址:<code>http://class="n">127.0.class="n">0.1:class="n">8766/api/ima</code></p>"
 56                "<p>本服务只做浏览器与 ima.qq.com 之间的白名单转发,不保存凭据和查询内容。</p>"
 57            ).encode("utf-class="n">8")
 58            self.send_response(class="n">200)
 59            cors_headers(self, "text/html; charset=utf-class="n">8")
 60            self.end_headers()
 61            self.wfile.write(text)
 62            return
 63        self._json(class="n">404, {"code": class="n">404, "msg": "not found", "hint": "GET /health 或 POST /api/ima/search_knowledge"})
 64 
 65    def do_POST(self):
 66        path = urllib.parse.urlparse(self.path).path.strip("/")
 67        prefix = "api/ima/"
 68        if not path.startswith(prefix) or path[len(prefix):] not in ALLOWED_ENDPOINTS:
 69            self._json(class="n">404, {"code": class="n">404, "msg": "endpoint not allowed", "allowed": sorted(ALLOWED_ENDPOINTS)})
 70            return
 71        endpoint = path[len(prefix):]
 72        client_id = (self.headers.get("ima-openapi-clientid") or "").strip()
 73        api_key = (self.headers.get("ima-openapi-apikey") or "").strip()
 74        if not client_id or not api_key:
 75            self._json(class="n">401, {"code": class="n">401, "msg": "缺少 ima Client ID 或 API Key;请在知识库模块中输入或启用教师个人版内置凭据"})
 76            return
 77        try:
 78            length = int(self.headers.get("Content-Length") or "class="n">0")
 79            payload = self.rfile.read(length)
 80            if length > class="n">1024 * class="n">1024:
 81                self._json(class="n">413, {"code": class="n">413, "msg": "请求体过大"})
 82                return
 83            class=class="s">"c"># 校验 JSON,避免灰色请求透传
 84            parsed = json.loads(payload.decode("utf-class="n">8") or b"{}".decode())
 85            body = json.dumps(parsed, ensure_ascii=False).encode("utf-class="n">8")
 86        except Exception as exc:
 87            self._json(class="n">400, {"code": class="n">400, "msg": "JSON 请求体无效", "detail": str(exc)})
 88            return
 89        req = urllib.request.Request(
 90            IMA_BASE + "/" + endpoint,
 91            data=body,
 92            method="POST",
 93            headers={
 94                "Content-Type": "application/json; charset=utf-class="n">8",
 95                "ima-openapi-clientid": client_id,
 96                "ima-openapi-apikey": api_key,
 97                "User-Agent": "LingxiIMAProxy/class="n">1.0",
 98            },
 99        )
100        try:
101            with urllib.request.urlopen(req, timeout=class="n">60) as res:
102                out = res.read()
103                status = res.status
104        except urllib.error.HTTPError as exc:
105            out = exc.read()
106            status = exc.code
107        except Exception as exc:
108            self._json(class="n">502, {"code": class="n">502, "msg": "无法连接 ima.qq.com", "detail": str(exc)})
109            return
110        self.send_response(status)
111        cors_headers(self, "application/json; charset=utf-class="n">8")
112        self.end_headers()
113        self.wfile.write(out)
114 
115    def _json(self, status, obj):
116        data = json.dumps(obj, ensure_ascii=False).encode("utf-class="n">8")
117        self.send_response(status)
118        cors_headers(self)
119        self.send_header("Content-Length", str(len(data)))
120        self.end_headers()
121        self.wfile.write(data)
122 
123 
124if __name__ == "__main__":
125    server = ThreadingHTTPServer((HOST, PORT), IMAProxyHandler)
126    print(f"IMA 本地桥梁已启动:http://{HOST}:{PORT}/api/ima")
127    print("保持本窗口打开即可;关闭窗口即停止服务。Ctrl+C 退出。")
128    try:
129        server.serve_forever()
130    except KeyboardInterrupt:
131        print("\nIMA 本地桥梁已停止")

3D 摄像头启动器(启动3D摄像头课堂.cmd

适用对象:作者本人(或拥有完整工作目录的二次开发者)。

本文件是作者本机 v6.8 配套启动器。它会调用同一目录下 work\avatar-camera\local-server.cjs 起一个允许浏览器 getUserMedia 的本地服务。

公开访客直接通过 https://zgdgt.cn/course-ai-assistant-3d.html 体验即可——无需本地启动任何东西。

作者机使用步骤

  1. 课程AI助手(双课教学版-v6.8-3D摄像头交互版).html 与同目录全部 work/avatar-camera/ 保留在同一文件夹。
  2. 安装 Node.js(v18+)。
  3. 双击 启动3D摄像头课堂.cmd。窗口里出现的本机 URL 在 Edge/Chrome 中打开即可获得摄像头权限。
  4. 关闭 cmd 窗口即停止服务。

代码

@echo off
setlocal
cd /d "%~dp0"
where node >nul 2>nul
if errorlevel 1 (
  echo Node.js is required. You can also open the v6.8 HTML directly in Edge.
  pause
  exit /b 1
)
node "work\avatar-camera\local-server.cjs"
pause

下载 启动3D摄像头课堂.cmd(222 B)

相关公开演示版(含本地脚本)