import dotenv from "dotenv"; import { z } from "zod"; // 本地开发可在项目根目录放 .local.env;已存在的系统环境变量优先级更高。 dotenv.config({ path: ".local.env", override: false }); const optionalString = () => z.preprocess( (value) => { if (typeof value !== "string") { return value; } const normalized = value.trim(); return normalized.length > 0 ? normalized : undefined; }, z.string().optional(), ); // 统一在启动时解析环境变量,避免业务代码里散落字符串默认值。 const envSchema = z .object({ // 运行环境标识,如 development / production。 NODE_ENV: z.string().default("development"), // HTTP 服务监听端口。 PORT: z.coerce.number().int().positive().default(8787), // HTTP 服务监听地址。 HOST: z.string().default("0.0.0.0"), // Pino 日志级别。 LOG_LEVEL: z.string().default("info"), // LLM 工具/技能调用审计日志路径。 LLM_REQUEST_AUDIT_LOG_PATH: z .string() .default("./logs/llm-request-audit.log"), // 内部工具桥调用本服务时使用的鉴权 token;未显式配置时启动阶段会自动生成。 AGENT_INTERNAL_TOKEN: optionalString(), // opencode 运行模式:embedded 会启动本地 CLI 子进程;client 只连接现有 server。 OPENCODE_MODE: z.enum(["embedded", "client"]).default("embedded"), // embedded opencode server 的监听地址。 OPENCODE_HOSTNAME: z.string().default("127.0.0.1"), // embedded opencode server 的监听端口。 OPENCODE_PORT: z.coerce.number().int().positive().default(4096), // opencode SDK 启动或连接运行时时的超时时间(毫秒)。 OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000), // 默认使用的 opencode 模型标识。 OPENCODE_MODEL: z.string().default("deepseek/deepseek-v4-pro"), // client 模式下,目标 opencode server 的基础地址。 OPENCODE_CLIENT_BASE_URL: z.string().url().optional(), // chat session 在本地注册表中的保活时长(秒)。 SESSION_TTL_SECONDS: z.coerce.number().int().positive().default(1800), // 提供给本地 opencode tools 读取的会话上下文目录。 SESSION_CONTEXT_STORAGE_DIR: z.string().default("./data/session-contexts"), // TJWater 后端 API 的基础地址。 TJWATER_API_BASE_URL: z.string().default("http://127.0.0.1:8000"), // 代理调用 TJWater 后端 API 的超时时间(毫秒)。 TJWATER_API_TIMEOUT_MS: z.coerce.number().int().positive().default(30000), // 后端结果在直接内联返回给模型前允许的最大字节数。 MAX_INLINE_RESULT_BYTES: z.coerce.number().int().positive().default(12000), // 生成结果 preview 时最多抽样的条目数。 MAX_PREVIEW_SAMPLE_ITEMS: z.coerce.number().int().positive().default(3), // memory 持久化存储目录。 MEMORY_STORAGE_DIR: z.string().default("./data/memory"), // 持久化文件写入前保留历史版本的目录。 PERSISTENCE_HISTORY_DIR: z.string().default("./data/history"), // 注入到 prompt 的 memory 快照最大字符数,避免上下文过大。 MEMORY_MAX_PROMPT_CHARS: z.coerce.number().int().positive().default(1800), // session transcript 持久化目录。 SESSION_HISTORY_STORAGE_DIR: z.string().default("./data/session-history"), // 每个会话最多保留多少轮 transcript,超过后裁剪旧记录。 SESSION_HISTORY_MAX_TURNS_PER_SESSION: z.coerce .number() .int() .positive() .default(120), // session_search 工具默认返回的最大命中数。 SESSION_SEARCH_MAX_RESULTS: z.coerce.number().int().positive().default(8), // session_search 查询文本最大长度。 SESSION_SEARCH_MAX_QUERY_CHARS: z.coerce.number().int().positive().default(240), // learning review 会话状态目录。 LEARNING_STATE_STORAGE_DIR: z.string().default("./data/learning-state"), // learning audit 日志路径。 LEARNING_AUDIT_LOG_PATH: z .string() .default("./logs/learning-audit.log"), // learning gate 的最小 turn 冷却间隔;这是运行时节流,不参与内容判断。 LEARNING_GATE_TURN_COOLDOWN: z.coerce.number().int().positive().default(2), // gate 结果被提升为 review 前的最低置信度。 LEARNING_GATE_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.65), // review prompt 最多携带多少轮最近 transcript。 LEARNING_REVIEW_MAX_RECENT_TURNS: z.coerce.number().int().positive().default(8), // review proposal 的最低置信度阈值。 LEARNING_MIN_PROPOSAL_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.8), // result_ref 持久化存储目录。 RESULT_REF_STORAGE_DIR: z.string().default("./data/result-refs"), // result_ref 保留时长(小时)。 RESULT_REF_TTL_HOURS: z.coerce.number().int().positive().default(168), // 定时清理过期 result_ref 的扫描周期(毫秒)。 RESULT_REF_CLEANUP_INTERVAL_MS: z.coerce .number() .int() .positive() .default(3600000), // fetch_result_ref 默认最多返回的顶层项/字段数量。 RESULT_REF_MAX_RETRIEVAL_ITEMS: z.coerce .number() .int() .positive() .default(50), }) .superRefine((env, ctx) => { if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["OPENCODE_CLIENT_BASE_URL"], message: "OPENCODE_CLIENT_BASE_URL is required when OPENCODE_MODE=client", }); } }); export type AppConfig = z.infer; const normalizedEnv = { ...process.env, OPENCODE_MODE: process.env.OPENCODE_MODE ?? (process.env.OPENCODE_CLIENT_BASE_URL || process.env.OPENCODE_BASE_URL ? "client" : "embedded"), OPENCODE_CLIENT_BASE_URL: process.env.OPENCODE_CLIENT_BASE_URL ?? process.env.OPENCODE_BASE_URL, }; export const config: AppConfig = envSchema.parse(normalizedEnv);