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.
178 lines
8.3 KiB
TypeScript
178 lines
8.3 KiB
TypeScript
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,
|
||
};
|