Files
OJ2/apps/api/src/config.ts
yuetsh 06ad6745b7
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(AI 提示): 两段式提示——先诊断出错误标签,再生成提示
AI 时代 OJ 设计的 2b。标准答案能让提示准得多,但不能进生成提示的 prompt
(学生代码里一段注释就能把它套走),所以拆成两段:
- 诊断:看得到标准答案、第一个没过的测试点、带行号的学生代码;出参只允许
  { tag, lines, confidence },safeParse 过闸、多余字段剥掉,没有自由文本通道
- 生成提示:看不到标准答案和测试点原文,只多一句「问题定位:X,大约在第 a 行」
- 诊断失败(20 秒超时、不是 JSON、校验不过)退回单段式;同一条提交复用诊断;
  编译失败不诊断
- 契约新增 HINT_ERROR_TAGS(13 个,落库值,只增不改)与 hintDiagnosisSchema
- 迁移 0018:ai_hint 加 diagnosis / diagnosis_error 两列
- 单段式 prompt 原样搬进 services/hint-diagnosis.ts,记版本 1;两段式记版本 2
- completeChat 支持 JSON 模式和自定义超时,现有调用不受影响
- 开关 AI_HINT_DIAGNOSE 默认关(2a 的基线还在攒),两套生产 compose 透传

实跑(一次性库 + 本地假 LLM):开关关时请求与 2a 逐字节相同;开时正常 /
复用 / 坏 JSON / 非法标签 / 行号越界 / 超时 / 编译失败七种场景符合预期;
12 次模型请求里诊断全都带标准答案和测试点,生成全都不带。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:56:38 -06:00

112 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomBytes } from "node:crypto"
import { readFileSync } from "node:fs"
import { isAbsolute, resolve } from "node:path"
import { isCompiled, pathBase } from "./runtime"
/**
* Bun 只自动加载「当前工作目录」下的 .env。而本应用的启动方式`bun run --filter '@oj2/api' dev`
* 会把 cwd 切到 apps/api/,于是仓库根的 .env 读不到 —— 而 .env.example 恰恰教人写在根目录。
* 这里显式补读仓库根的 .env让文档指引真正生效且不管从哪个目录启动都一致。
*
* 只填充尚未设置的键:真实环境变量与 cwd 下的 .env 优先级更高,不被覆盖。
*
* 编译成单二进制后不做这件事:生产靠 compose 注入环境变量,而 `import.meta.dir` 在
* 二进制里是 `/$bunfs/root`,往上三级会去读 `/.env` —— 读到什么都是意外。
*/
function loadRepoRootEnv() {
if (isCompiled) return
try {
const text = readFileSync(resolve(pathBase, ".env"), "utf8")
for (const line of text.split("\n")) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith("#")) continue
const eq = trimmed.indexOf("=")
if (eq <= 0) continue
const key = trimmed.slice(0, eq).trim()
if (process.env[key] !== undefined) continue
process.env[key] = trimmed
.slice(eq + 1)
.trim()
.replace(/^["']|["']$/g, "")
}
} catch {
// 根目录没有 .env 是正常情况(例如生产用真实环境变量注入),静默跳过
}
}
loadRepoRootEnv()
/**
* 判题机 token。对齐旧后端 `options/options.py:93`
* token = os.environ.get("JUDGE_SERVER_TOKEN"); return token if token else rand_str()
* env 缺失时生成随机值 fail-safe —— 宁可判题机连不上(启动时有明显告警),
* 也不要在仓库里写死一个人人都知道的弱默认值。
*/
/**
* 相对路径按 `pathBase` 解析(开发时是仓库根,编译后是 cwd基准的取舍见 runtime.ts。
*
* 生产环境**应当**用绝对路径的环境变量把这些目录显式指定掉,相对路径只是开发便利。
*/
function repoPath(value: string) {
return isAbsolute(value) ? value : resolve(pathBase, value)
}
function judgeServerToken() {
const fromEnv = process.env.JUDGE_SERVER_TOKEN
if (fromEnv) return fromEnv
console.warn(
"[config] JUDGE_SERVER_TOKEN 未设置,已生成一次性随机 token。" +
"判题机将无法通过鉴权,本地开发请在 .env 里设置 JUDGE_SERVER_TOKEN" +
"并让 docker/compose.dev.yml 的 OJ2_JUDGE_TOKEN 取同一个值。",
)
return randomBytes(32).toString("hex")
}
export const config = {
port: Number(process.env.PORT ?? 3000),
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6380",
sessionCookie: "oj2_session",
sessionTtlSeconds: Number(
process.env.SESSION_TTL_SECONDS ?? 7 * 24 * 60 * 60,
),
secureCookies: process.env.COOKIE_SECURE === "true",
judgeServerUrl: process.env.JUDGE_SERVER_URL ?? "http://localhost:8081",
judgeServerToken: judgeServerToken(),
judgeConcurrency: Number(process.env.JUDGE_CONCURRENCY ?? 2),
avatarDirectory: repoPath(process.env.AVATAR_DIRECTORY ?? "data/avatar"),
// 判题沙箱把这个目录挂成只读的 /test_case两边必须指同一处
testCaseDirectory: repoPath(
process.env.TEST_CASE_DIRECTORY ?? "data/test_case",
),
uploadDirectory: repoPath(process.env.UPLOAD_DIRECTORY ?? "data/upload"),
// 一言数据集hitokoto.cn 官方导出),和旧后端读同一份:容器里是 /data/hitokoto。
// 本机 dev 默认路径下没有这份数据,读不到就回落到内置的几条,不影响启动。
hitokotoDirectory: repoPath(
process.env.HITOKOTO_DIRECTORY ?? "data/hitokoto",
),
/**
* WebSocket 升级时额外放行的来源(逗号分隔的完整 origin如 https://oj.example.com
* 同源本来就放行,只有前后端分处不同域名时才需要配。
*/
allowedWebSocketOrigins: (process.env.ALLOWED_WS_ORIGINS ?? "")
.split(",")
.map((value) => value.trim())
.filter(Boolean),
uploadUriPrefix: process.env.UPLOAD_URI_PREFIX ?? "/public/upload",
avatarUriPrefix: process.env.AVATAR_URI_PREFIX ?? "/public/avatar",
aiBaseUrl: process.env.AI_BASE_URL ?? "https://api.deepseek.com",
/** 只用来写 ai_analysis.provider 这一列,换 provider 时和 AI_BASE_URL 一起改 */
aiProvider: process.env.AI_PROVIDER ?? "deepseek",
aiKey: process.env.AI_KEY ?? "",
aiModel: process.env.AI_MODEL ?? "deepseek-flash",
/**
* AI 提示走两段式(先诊断、再生成),见 services/hint-diagnosis.ts。**默认关**
* 2026-09-19 起 ai_hint 在攒单段式的基线数据,攒够之前别打开,否则两批数据混在一起没法比。
* 设成 "1" 打开。
*/
aiHintDiagnose: process.env.AI_HINT_DIAGNOSE === "1",
ruffPath: process.env.RUFF_PATH ?? "ruff",
clangFormatPath: process.env.CLANG_FORMAT_PATH ?? "clang-format",
}