Files
next-tjwater-agent/src/config.ts
T
jiang 6a49d2209e feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
2026-10-19 18:46:58 +08:00

178 lines
8.3 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 dotenv from "dotenv";
import { z } from "zod";
import { loadAgentModelConfig, type AgentModelConfig } from "./chat/modelConfig.js";
import { DEFAULT_SESSION_WORKSPACE_ROOT } from "./runtime/conversationWorkspace.js";
// 本地开发可在项目根目录放 .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 optionalBoolean = (defaultValue: boolean) =>
z.preprocess((value) => {
if (typeof value !== "string") {
return value;
}
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
return value;
}, z.boolean().default(defaultValue));
// 统一在启动时解析环境变量,避免业务代码里散落字符串默认值。
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("127.0.0.1"),
// Product Server base URL used to verify browser identity and project scope.
TJWATER_SERVER_BASE_URL: z.string().url().default("http://127.0.0.1:8000"),
// 工况任务调用 Server 的最长等待时间,必须短于两次租约心跳之间的间隔。
TJWATER_SERVER_TIMEOUT_MS: z.coerce.number().int().positive().max(30_000).default(10_000),
TJWATER_MCP_ENABLED: optionalBoolean(false),
TJWATER_MCP_ISSUER_URI: optionalString(),
TJWATER_MCP_DEV_HTTP_ISSUER_URI: optionalString(),
TJWATER_MCP_RESOURCE_URI: optionalString(),
TJWATER_MCP_AUDIENCE: z.string().default("tjwater-mcp"),
TJWATER_MCP_CLIENT_ID: z.string().default("tjwater-agent-mcp"),
TJWATER_MCP_CLIENT_SECRET: optionalString(),
// Pino 日志级别。
LOG_LEVEL: z.string().default("info"),
// LLM 工具/技能调用审计日志路径。
LLM_REQUEST_AUDIT_LOG_PATH: z
.string()
.default("./logs/llm-request-audit.log"),
// 内部工具桥鉴权 token;embedded 可自动生成,client 模式必须与外部 OpenCode 显式共享。
AGENT_INTERNAL_TOKEN: optionalString(),
// 每个 OpenCode 会话独占的文件工作目录;其他 data/logs 仍受保护。
CONVERSATION_WORKSPACE_ROOT: z.string().default(DEFAULT_SESSION_WORKSPACE_ROOT),
SANDBOX_MAX_OUTPUT_BYTES: z.coerce.number().int().positive().max(16 * 1024 * 1024).default(1024 * 1024),
// OpenCode 2 CLI 命令或绝对路径。
OPENCODE_COMMAND: z.string().default("opencode2"),
// 外部 OpenCode 2 Server 的密码;embedded 模式自动管理。
OPENCODE_CLIENT_PASSWORD: optionalString(),
// embedded 启动私有 CLI 子进程;client 连接现有 V2 server。
OPENCODE_MODE: z.enum(["embedded", "client"]).default("embedded"),
// embedded opencode server 的监听地址。
OPENCODE_HOSTNAME: z.string().default("127.0.0.1"),
// embedded opencode server 的监听端口;0 表示由系统动态分配。
OPENCODE_PORT: z.coerce.number().int().nonnegative().default(0),
// OpenCode 2 启动超时时间(毫秒)。
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
// 可选的统一模型配置文件;未设置时读取仓库中的 agent-models.json。
AGENT_MODEL_CONFIG_PATH: optionalString(),
// 平滑 token 输出,避免上游模型大块/抖动 token 直接传到前端。
AGENT_TOKEN_SMOOTHING_ENABLED: optionalBoolean(true),
// 平滑输出每段之间的间隔(毫秒)。
AGENT_TOKEN_SMOOTHING_DELAY_MS: z.coerce
.number()
.int()
.min(0)
.default(20),
// 平滑输出分词 locale,中文默认使用 zh。
AGENT_TOKEN_SMOOTHING_LOCALE: z.string().default("zh"),
// opencode skills 树目录;会在运行时解析为绝对路径,避免工具 cwd 偏移。
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
// client 模式下,目标 opencode server 的基础地址。
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
// 旧版 client 模式环境变量名,保留兼容,解析时会映射到 OPENCODE_CLIENT_BASE_URL。
OPENCODE_BASE_URL: z.string().url().optional(),
// memory 持久化存储目录。
MEMORY_STORAGE_DIR: z.string().default("./data/memory"),
// 持久化文件写入前保留备份版本的目录。
PERSISTENCE_BACKUP_DIR: z.string().default("./data/backup"),
// 注入到 prompt 的 memory 快照最大字符数,避免上下文过大。
MEMORY_MAX_PROMPT_CHARS: z.coerce.number().int().positive().default(1800),
// 旧 transcript 迁移来源;新记录位于用户会话工作空间的 chat/。
SESSION_TRANSCRIPT_STORAGE_DIR: z.string().default("./data/session-transcripts"),
// session metadata 持久化目录。
SESSION_METADATA_STORAGE_DIR: z.string().default("./data/session-metadata"),
// 旧 UI state 迁移来源;新完整聊天状态位于会话工作空间的 chat/。
SESSION_UI_STATE_STORAGE_DIR: z.string().default("./data/session-ui-states"),
// 每个会话最多保留多少轮 transcript,超过后裁剪旧记录。
SESSION_TRANSCRIPT_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 进度迁移来源;新进度位于会话 chat/learning.json。
SESSION_LEARNING_STATE_STORAGE_DIR: z.string().default("./data/session-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),
})
.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",
});
}
if (env.OPENCODE_MODE === "client" && !env.AGENT_INTERNAL_TOKEN) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["AGENT_INTERNAL_TOKEN"],
message: "AGENT_INTERNAL_TOKEN is required in client mode and must also be configured on the external OpenCode process",
});
}
});
export type AppConfig = z.infer<typeof envSchema> & {
MODEL_CONFIG: AgentModelConfig;
OPENCODE_MODEL: string;
OPERATIONAL_TASK_MODEL: string;
LEARNING_GATE_MODEL: string;
};
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,
};
const environment = envSchema.parse(normalizedEnv);
const modelConfiguration = loadAgentModelConfig(environment.AGENT_MODEL_CONFIG_PATH);
export const config: AppConfig = {
...environment, MODEL_CONFIG: modelConfiguration,
OPENCODE_MODEL: modelConfiguration.defaults.chat,
OPERATIONAL_TASK_MODEL: modelConfiguration.defaults.task,
LEARNING_GATE_MODEL: modelConfiguration.defaults.learning_gate,
};