import { isRecord } from "../../model/config/schema.js";
import type {
PilotAdaptersConfig,
PilotConfigDiagnostic,
PilotGatewayConfig,
PilotPlatformAdapterConfig,
} from "./types.js";
export function parseGatewayConfig(rawGateway: unknown, diagnostics: PilotConfigDiagnostic[]): PilotGatewayConfig | undefined {
if (rawGateway === undefined) {
return undefined;
}
if (!isRecord(rawGateway)) {
diagnostics.push({
code: "GATEWAY_CONFIG_INVALID",
severity: "fatal",
message: "gateway config must be an object.",
path: "gateway",
recoverable: false,
});
return undefined;
}
const bindAddress = stringField(rawGateway, "bindAddress", "127.0.0.1");
if (bindAddress !== "127.0.0.1") {
diagnostics.push({
code: "GATEWAY_BIND_ADDRESS_UNSUPPORTED",
severity: "fatal",
message: "gateway.bindAddress must be 127.0.0.1 in the first phase.",
path: "gateway.bindAddress",
recoverable: false,
});
}
if (rawGateway.tokenPath !== undefined) {
diagnostics.push({
code: "GATEWAY_TOKEN_PATH_REMOVED",
severity: "warning",
message: "gateway.tokenPath is no longer configurable; the gateway token is stored under PilotHome.",
path: "gateway.tokenPath",
recoverable: true,
});
}
const maxMcp = numberField(rawGateway, "maxPerSessionMcpInstances", 5);
return {
port: numberField(rawGateway, "port", 18789),
bindAddress: "127.0.0.1",
idleSessionTimeoutMinutes: numberField(rawGateway, "idleSessionTimeoutMinutes", 30),
staticAssetsPath: stringField(rawGateway, "staticAssetsPath"),
maxPerSessionMcpInstances: Math.max(1, maxMcp),
};
}
export function parseAdaptersConfig(rawAdapters: unknown, diagnostics: PilotConfigDiagnostic[]): PilotAdaptersConfig | undefined {
if (rawAdapters === undefined) {
return undefined;
}
if (!isRecord(rawAdapters)) {
diagnostics.push({
code: "ADAPTERS_CONFIG_INVALID",
severity: "fatal",
message: "adapters config must be an object.",
path: "adapters",
recoverable: false,
});
return undefined;
}
const PLATFORM_KEYS = [
"telegram", "discord", "slack", "matrix", "mattermost",
"signal", "whatsapp", "bluebubbles",
"dingtalk", "wecom", "wecomCallback",
"email", "sms", "homeassistant",
"apiServer", "webhook",
] as const;
const result: PilotAdaptersConfig = {
cli: parseAutoConnect(rawAdapters.cli),
tui: parseAutoConnect(rawAdapters.tui),
feishu: parseFeishu(rawAdapters.feishu),
weixin: parseEnabledOnly(rawAdapters.weixin),
};
for (const key of PLATFORM_KEYS) {
const parsed = parsePlatformAdapter(rawAdapters[key]);
if (parsed) {
(result as Record<string, unknown>)[key] = parsed;
}
}
return result;
}
function parseEnabledOnly(raw: unknown): { enabled: boolean } | undefined {
if (!isRecord(raw)) return undefined;
return { enabled: booleanField(raw, "enabled", false) };
}
function parsePlatformAdapter(raw: unknown): PilotPlatformAdapterConfig | undefined {
if (!isRecord(raw)) return undefined;
const extra = isRecord(raw.extra) ? (raw.extra as Record<string, unknown>) : undefined;
return {
enabled: booleanField(raw, "enabled", false),
token: stringField(raw, "token"),
apiKey: stringField(raw, "apiKey"),
webhookUrl: stringField(raw, "webhookUrl"),
extra,
};
}
function parseAutoConnect(raw: unknown): { autoConnectServer: boolean } | undefined {
if (!isRecord(raw)) {
return undefined;
}
return { autoConnectServer: booleanField(raw, "autoConnectServer", true) };
}
function parseFeishu(raw: unknown): PilotAdaptersConfig["feishu"] {
if (!isRecord(raw)) {
return undefined;
}
const mode = stringField(raw, "connectionMode");
const domain = stringField(raw, "domainName");
return {
enabled: booleanField(raw, "enabled", false),
appId: stringField(raw, "appId"),
appSecret: stringField(raw, "appSecret"),
encryptKey: stringField(raw, "encryptKey"),
verifyToken: stringField(raw, "verifyToken"),
defaultSessionLabel: stringField(raw, "defaultSessionLabel", "general") ?? "general",
connectionMode: mode === "stream" || mode === "webhook" ? mode : undefined,
domainName: domain === "feishu" || domain === "lark" ? domain : undefined,
};
}
function stringField(record: Record<string, unknown>, key: string, fallback?: string): string | undefined {
return typeof record[key] === "string" ? record[key] : fallback;
}
function numberField(record: Record<string, unknown>, key: string, fallback: number): number {
return typeof record[key] === "number" && Number.isFinite(record[key]) ? record[key] : fallback;
}
function booleanField(record: Record<string, unknown>, key: string, fallback: boolean): boolean {
return typeof record[key] === "boolean" ? record[key] : fallback;
}