2026-03-14 15:54:30 +08:00
|
|
|
/**
|
|
|
|
|
* 配置处理模块
|
|
|
|
|
* @module config
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
PITBotAccountConfig,
|
|
|
|
|
ResolvedPITBotAccount
|
|
|
|
|
} from "./types.js";
|
|
|
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
|
|
|
import { createLogger } from "./utils/logger.js";
|
|
|
|
|
|
|
|
|
|
const MODULE = "config";
|
|
|
|
|
|
2026-03-15 12:42:38 +08:00
|
|
|
interface PITChannelConfig extends PITBotAccountConfig {
|
|
|
|
|
accounts?: Record<string, PITBotAccountConfig>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 15:54:30 +08:00
|
|
|
/**
|
|
|
|
|
* 默认账户 ID
|
|
|
|
|
*/
|
|
|
|
|
export const DEFAULT_ACCOUNT_ID = "default";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取 PIT Bot 账户 ID 列表
|
|
|
|
|
* @returns 账户 ID 列表
|
|
|
|
|
*/
|
2026-03-15 12:42:38 +08:00
|
|
|
export function listPITBotAccountIds(): string[] {
|
|
|
|
|
return [DEFAULT_ACCOUNT_ID];
|
2026-03-14 15:54:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析 PIT Bot 账户
|
|
|
|
|
* @param cfg OpenClaw 配置
|
|
|
|
|
* @param accountId 账户 ID
|
|
|
|
|
* @returns 解析后的账户配置
|
|
|
|
|
* @throws 如果配置无效
|
|
|
|
|
*/
|
|
|
|
|
export function resolvePITBotAccount(
|
|
|
|
|
cfg: OpenClawConfig,
|
|
|
|
|
accountId: string
|
|
|
|
|
): ResolvedPITBotAccount {
|
|
|
|
|
const log = createLogger(MODULE);
|
|
|
|
|
|
2026-03-15 12:42:38 +08:00
|
|
|
const pit = cfg.channels?.["zhidui-channel"] as PITChannelConfig | undefined;
|
|
|
|
|
|
|
|
|
|
// 如果没有配置,返回默认值
|
|
|
|
|
if (!pit) {
|
|
|
|
|
log.warn(`No zhidui-channel configuration found, using defaults`);
|
|
|
|
|
return {
|
|
|
|
|
accountId,
|
|
|
|
|
name: undefined,
|
|
|
|
|
enabled: false,
|
|
|
|
|
routerUrl: "",
|
|
|
|
|
authToken: "",
|
|
|
|
|
secretSource: "none",
|
|
|
|
|
config: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
reconnectInterval: 5000,
|
|
|
|
|
heartbeatInterval: 30000,
|
|
|
|
|
heartbeatTimeout: 10000,
|
|
|
|
|
ackTimeout: 30000,
|
|
|
|
|
maxQueueSize: 100,
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-03-14 15:54:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取全局默认配置
|
|
|
|
|
const defaults: PITBotAccountConfig = {
|
2026-03-15 12:42:38 +08:00
|
|
|
enabled: pit.enabled ?? true,
|
|
|
|
|
routerUrl: pit.routerUrl,
|
|
|
|
|
authToken: pit.authToken,
|
|
|
|
|
name: pit.name,
|
|
|
|
|
reconnectInterval: pit.reconnectInterval ?? 5000,
|
|
|
|
|
heartbeatInterval: pit.heartbeatInterval ?? 30000,
|
|
|
|
|
heartbeatTimeout: pit.heartbeatTimeout ?? 10000,
|
|
|
|
|
ackTimeout: pit.ackTimeout ?? 30000,
|
|
|
|
|
maxQueueSize: pit.maxQueueSize ?? 100,
|
2026-03-14 15:54:30 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取特定账户配置
|
|
|
|
|
let accountConfig: PITBotAccountConfig = {};
|
|
|
|
|
|
2026-03-15 12:42:38 +08:00
|
|
|
if (pit.accounts && typeof pit.accounts === "object") {
|
|
|
|
|
const specificConfig = pit.accounts[accountId];
|
2026-03-14 15:54:30 +08:00
|
|
|
if (specificConfig && typeof specificConfig === "object") {
|
|
|
|
|
accountConfig = specificConfig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 合并配置(账户配置覆盖默认配置)
|
|
|
|
|
const mergedConfig: PITBotAccountConfig = {
|
|
|
|
|
...defaults,
|
|
|
|
|
...accountConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 解析认证 Token
|
|
|
|
|
const { authToken, secretSource } = resolveAuthToken(mergedConfig.authToken, accountId, log);
|
|
|
|
|
|
|
|
|
|
// 验证必需字段
|
|
|
|
|
const routerUrl = mergedConfig.routerUrl;
|
|
|
|
|
if (!routerUrl || typeof routerUrl !== "string") {
|
2026-03-15 12:42:38 +08:00
|
|
|
log.warn(`No routerUrl configured for account ${accountId}`);
|
2026-03-14 15:54:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
accountId,
|
|
|
|
|
name: mergedConfig.name,
|
|
|
|
|
enabled: mergedConfig.enabled ?? true,
|
2026-03-15 12:42:38 +08:00
|
|
|
routerUrl: routerUrl ?? "",
|
2026-03-14 15:54:30 +08:00
|
|
|
authToken: authToken ?? "",
|
|
|
|
|
secretSource,
|
|
|
|
|
config: mergedConfig,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 应用 PIT Bot 账户配置
|
|
|
|
|
* @param cfg OpenClaw 配置
|
|
|
|
|
* @param accountId 账户 ID
|
|
|
|
|
* @param config 新配置
|
|
|
|
|
*/
|
|
|
|
|
export function applyPITBotAccountConfig(
|
|
|
|
|
cfg: OpenClawConfig,
|
|
|
|
|
accountId: string,
|
2026-03-15 12:42:38 +08:00
|
|
|
config: Partial<PITBotAccountConfig>
|
2026-03-14 15:54:30 +08:00
|
|
|
): void {
|
2026-03-15 12:42:38 +08:00
|
|
|
// No-op for now - configuration is managed by OpenClaw
|
|
|
|
|
console.log(`[zhidui-channel] applyAccountConfig called for ${accountId}:`, config);
|
2026-03-14 15:54:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析认证 Token
|
|
|
|
|
* 支持环境变量引用和环境变量名
|
|
|
|
|
*/
|
|
|
|
|
function resolveAuthToken(
|
|
|
|
|
configToken: string | undefined,
|
|
|
|
|
accountId: string,
|
|
|
|
|
log: { warn: (msg: string) => void }
|
|
|
|
|
): { authToken: string | null; secretSource: "config" | "env" | "none" } {
|
|
|
|
|
// 1. 如果配置中直接设置了 Token
|
|
|
|
|
if (configToken && typeof configToken === "string") {
|
|
|
|
|
// 检查是否是环境变量引用 ${VAR_NAME}
|
|
|
|
|
const envMatch = configToken.match(/^\$\{(.+)\}$/);
|
|
|
|
|
if (envMatch) {
|
|
|
|
|
const envVar = envMatch[1];
|
|
|
|
|
const token = process.env[envVar];
|
|
|
|
|
if (!token) {
|
|
|
|
|
log.warn(`Environment variable ${envVar} is not set for account ${accountId}`);
|
|
|
|
|
return { authToken: null, secretSource: "env" };
|
|
|
|
|
}
|
|
|
|
|
return { authToken: token, secretSource: "env" };
|
|
|
|
|
}
|
|
|
|
|
return { authToken: configToken, secretSource: "config" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 尝试从标准环境变量名获取
|
|
|
|
|
const envVarName = accountId === DEFAULT_ACCOUNT_ID
|
|
|
|
|
? "PIT_ROUTER_TOKEN"
|
|
|
|
|
: `PIT_ROUTER_TOKEN_${accountId.toUpperCase()}`;
|
|
|
|
|
|
|
|
|
|
const envToken = process.env[envVarName];
|
|
|
|
|
if (envToken) {
|
|
|
|
|
return { authToken: envToken, secretSource: "env" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 无 Token
|
|
|
|
|
return { authToken: null, secretSource: "none" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 验证配置
|
|
|
|
|
*/
|
2026-03-15 12:42:38 +08:00
|
|
|
export function validateConfig(config: PITBotAccountConfig | undefined): { valid: boolean; errors: string[] } {
|
2026-03-14 15:54:30 +08:00
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
2026-03-15 12:42:38 +08:00
|
|
|
if (!config) {
|
|
|
|
|
errors.push("Configuration is required");
|
|
|
|
|
return { valid: false, errors };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 15:54:30 +08:00
|
|
|
if (!config.routerUrl) {
|
|
|
|
|
errors.push("routerUrl is required");
|
|
|
|
|
} else if (!isValidUrl(config.routerUrl)) {
|
|
|
|
|
errors.push(`routerUrl is invalid: ${config.routerUrl}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.reconnectInterval !== undefined && config.reconnectInterval < 1000) {
|
|
|
|
|
errors.push("reconnectInterval must be at least 1000ms");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.heartbeatInterval !== undefined && config.heartbeatInterval < 1000) {
|
|
|
|
|
errors.push("heartbeatInterval must be at least 1000ms");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
valid: errors.length === 0,
|
|
|
|
|
errors,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 验证 URL
|
|
|
|
|
*/
|
|
|
|
|
function isValidUrl(url: string): boolean {
|
|
|
|
|
try {
|
|
|
|
|
new URL(url);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|