@ocero/ujn-deepseek-webapi-node
v1.1.1
Published
UJN (济南大学) DeepSeek WebAPI —— OpenAI 兼容直通代理 + Anthropic Messages 兼容端点(Node.js,发布包为编译后 JS,可 npx/全局使用)
Maintainers
Readme
注意
本工具仅供技术研究,使用者应于24小时内删除从济南大学ChatUJN平台获取的非公开数据。开发者不对任何学术诚信审查问题负责,请优先在智慧济大中的ChatUJN开通API权限。 本程序不保证您的信息安全,使用者应自行承担风险。请勿将本程序用于非法用途,否则后果自负。 开发者不对使用本程序导致的任何问题负责。 请勿滥用!!!
鸣谢
感谢济南大学信息管理处教育技术与网络信息中心为济大学子提供的免费大模型调用服务。
也感谢 @zeroHYH @futz12 @szw0407 以及开源项目 SDU_DeepSeek——本作正是在它的基础上定制完善的。
UJN DeepSeek WebAPI
要求
- 本地开发(跑 TS 源码):Node.js ≥ 23.6(原生运行 TypeScript,无需编译步骤)
- 使用已发布包(
npx/ 全局安装,跑编译后 JS):Node.js ≥ 18
安装与运行
本地开发:
npm installnpx :
#server
npx -p @ocero/ujn-deepseek-webapi-node@latest ujn-serve -u 2021001234 -p 你的密码
#cli
npx -p @ocero/ujn-deepseek-webapi-node@latest ujn-chat启动 API 服务
# 账号密码为必需;host/port 可选
# 方式一:-u / -p 标记
node src/server.ts -u 2021001234 -p 你的密码 --port 8000
# 方式二:位置参数(经 npm start 透传时 -u/-p 会被 npm 吞掉,务必用这种)
npm start -- 2021001234 你的密码
# 直接运行也支持位置参数
node src/server.ts 2021001234 你的密码 --port 8000服务起来后(默认 http://127.0.0.1:8000):
- OpenAI 客户端连
/v1,例如http://127.0.0.1:8000/v1 - Anthropic / Claude Code 连根地址
http://127.0.0.1:8000,并设ANTHROPIC_BASE_URL(Anthropic ⇄ OpenAI 自动双向转换) - 模型列表见
GET /v1/models或 CLI 选择
交互式 CLI(自动拉起本地服务)
npm run cli开发
npm run typecheck # 类型检查
npm run build # 编译 src → dist(发布包用)超时与校外(公网)链路行为
校外经 WebVPN 访问时,故障大多不是「连不上」,而是网关/上游把请求挂住:既不回响应头也不断开。 因此超时是分层的(全部可用环境变量覆盖):
| 环境变量 | 默认 | 含义 |
| --- | --- | --- |
| UJN_TTFB_TIMEOUT_MS | 600000 | 响应头(TTFB)超时,用于等「预填充+排队」;收到响应头立刻解除,不影响长回答吐字 |
| UJN_STREAM_TOTAL_TIMEOUT_MS | 900000 | 流式对话整体上限(含吐字过程) |
| UJN_NONSTREAM_TOTAL_TIMEOUT_MS | 900000 | 非流式对话整体上限(此时响应头=整段生成结束) |
| UJN_SESSION_PROBE_TIMEOUT_MS | 10000 | 「会话是否还活着」旁证请求(GET /api/models)的超时 |
| UJN_LIST_TIMEOUT_MS | 60000 | 模型列表等轻量请求 |
| UJN_REQUEST_MAX_ATTEMPTS | 2 | 响应头阶段的最大尝试次数(含首次) |
| UJN_RELOGIN_COOLDOWN_MS | 60000 | 「响应头超时→强制重登」的最小间隔(防登录风暴) |
| UJN_VPN_BASE | https://webvpn.ujn.edu.cn | 仅本地联调用:把上游指向 mock |
为什么首字节要给到 10 分钟
2026-09 校外实测(经 WebVPN、GLM-5.3、无并发压力),首字节耗时基本随输入线性增长:
| 输入规模 | 首字节(TTFB) | | --- | --- | | 20k tokens | 23.6s | | 60k tokens | 58.0s | | 同前缀再次请求(命中后端前缀缓存) | 0.8~1.2s |
⇒ 几万~十几万 tokens 的上下文,首字节天然 1~3 分钟,后端排队时更久。 所以「响应头迟迟不来」在本后端上多数是正常的预填充,不是链路故障。
工具调用(function calling):UJN_TOOL_MODE
实测(2026-09,校外经 WebVPN,GLM-5.3,后端健康时):
| 请求 | 结果 |
| --- | --- |
| tools + tool_choice:"auto" | 200 @1.8s,finish_reason="tool_calls",tool_calls 正确 |
| tools(不传 tool_choice) | 200 @1.5s,同样正确 |
| 流式 auto | 200,流内出现 tool_calls 帧 + [DONE] |
所以 auto 是好的,默认原样透传。真正的坑是:某个模型的队列会被堵住(堵住期间该模型连
纯文本请求也一起被拖住,同时刻换别的模型 0.6s 就回;等它缓过来同样的请求又恢复正常)。
代理已针对这点做了三件事:
基线假设:学校后端默认不拥堵。 首字节慢基本都是长上下文预填充(实测 ≈0.8ms/token), 所以日志只做进度播报,不下「被堵」结论。
- 客户端断开就取消上游(不再积累僵尸请求);
- 本地并发上限
UJN_MAX_INFLIGHT_PER_MODEL(默认 2):同一模型同时只放这么多请求到上游, 超出的在本地排队(排队超过 1s 会提示);名额在「正文读完 / 出错 / 被取消」时释放, 并带兜底强释放(即使某条消费路径异常也不会把本地队列卡死)。拥堵往往是客户端一次并发好几个长请求造成的, 这一层是「少给自己制造拥堵」的主要手段; - 进度播报:播报时刻 =
max(UJN_SLOW_TTFB_WARN_MS, 预计预填充 × UJN_PREFILL_SLOW_FACTOR), 文案带prompt≈N tokens与预计预填充,等到响应头后补一条「共等待 Xs」;
调用调度器(并发 / 排队 / 会话粘性)
参照 koishibot/ernie-vilg 老插件的池化调度思路改写(那张图见 src/scheduler.ts 顶部注释):
| 参考实现 | 本调度器 |
| --- | --- |
| ApiPool(容器实例池) | 按"键"(模型名)的容量槽位(容量式,不真的建对象) |
| VilgApi.requestsQueue(每实例单飞) | 每键 FIFO 队列 + UJN_MAX_INFLIGHT_PER_MODEL 并发 |
| hasBeenOccupied / BindingsPool(会话绑定) | 会话粘性:请求体带 user 时,同会话调用串行且按提交顺序 |
| SearchFreeContainerInfos / GC() | prune() 只剪"空队列且无在飞"的键(trace: key_pruned) |
| EventEmitter 回结果 | Promise + AbortSignal + trace 事件 |
| 缺:超时/取消/总量/公平 | 补齐:排队超时(503)、排队期取消、全局上限、键间轮转(避免某模型长队饿死别的模型)、名额 hold/release + 兜底释放 |
开关:UJN_MAX_INFLIGHT_PER_MODEL(2) / UJN_MAX_INFLIGHT_GLOBAL(4) / UJN_MAX_QUEUE_WAIT_MS(300000) / UJN_KEY_COOLDOWN_MS(0=关)。
状态:GET /v1/scheduler 返回在飞/排队/最久等待/冷却,trace 里也有 job_enqueued/job_started/job_finished/key_pruned。
模型自动回退(fallback):零配置
候选从 GET /api/models 的模型列表里自动挑,不硬编码、不需要你传任何参数:
- 同家族优先:名字首段相同(
GLM-5.3-Flash→GLM-5.3); - 再按代理自己实测的最快最稳排序(滚动窗口,见
src/modelstats.ts); - 上下文超限(上游 400 context length)时,改成按"上下文窗口从大到小"挑(读列表里的
max_model_len), 并只考虑窗口装得下的模型。
触发门槛也自动调节:max(该模型近期中位耗时 × 4, 预计预填充 × 2.5),夹在 30s~180s。
所以"平时 1s 就回的模型卡了 30s"会立刻换,"本来就慢的模型"不会被误换;长上下文(预计预填充大)也不会被误换。
总预算 300s、最多再试 3 个,绝不会无限换。回复开头会插一句
<原模型>模型返回时间过长或遇到错误,已自动切换成<新模型>模型回复
(流式=首个文本帧;非流式=choices[0].message.content 开头)。
两个可选开关(不用就不必管):UJN_FALLBACK=0 关闭回退;UJN_FALLBACK_OVERRIDE=模型A,模型B 强制顺序。
每次回退都会写 trace(model_fallback:from/to/原因/耗时)。
卡住时怎么诊断:trace
代理会把每个上游请求的生命周期写进 JSONL trace(默认 <包目录>/ujn_trace.jsonl,
与 ujn_cookies.json 同目录;UJN_TRACE_FILE=0 关闭,写别的路径就设绝对路径;超 8MB 自动轮转 .1)。
记录内容:upstream_start(模型/prompt 规模/各阶段预算/路由)、upstream_headers(TTFB)、
upstream_error(失败类型与原因)、slot_wait/slot_acquired/slot_released(本地排队与在飞数)、
slot_leak_guard(本地名额兜底释放)。不含对话内容。
下次再卡住,把这份 trace(或它的尾部)发出来即可定位。
| UJN_TOOL_MODE 取值 | 行为 |
| --- | --- |
| passthrough(默认) | 原样透传,auto 正常工作 |
| strip | 应急:摘除 tools(并把历史里的 tool_calls/tool 折算成文本),模型只回文本,用于某模型队列已被堵死时先恢复可用性 |
客户端断开就取消上游
客户端取消/超时断开时,代理会立刻 abort 上游请求(原来的实现不会), 避免被放弃的长生成继续占着(很可能只有单卡的)校园后端,越积越多导致「连续不起来」。
失败后的动作
只有下面两种情况才会自动重试一次:连接层错误(ECONNRESET / UND_ERR_SOCKET 等)、
或旁证(10s 的 GET /api/models)显示会话确实已失效(返回登录页)→ 重登(换新 ticket)+ 换新连接。
若旁证显示会话正常,则判定为「上游慢」,不重试(避免再烧一遍 GPU 预填充),直接按类型报错:
超时/无响应头 → HTTP 504(并写明原因与可调开关),连接失败 → HTTP 502,会话失效 → HTTP 401。
undici 的 headersTimeout/bodyTimeout 在 server.ts 里被显式设为「整体上限 + 60s」,
确保不会再出现「undici 默认 300s 抢先触发、报 fetch failed <- UND_ERR_HEADERS_TIMEOUT 且被当成连接错误重试 3 次」的老问题。
