docs: add personal WeChat WCF ingress config
This commit is contained in:
@@ -64,6 +64,45 @@ interface WechatArchiveConfig {
|
||||
tokenKey: string;
|
||||
timeoutMs: number;
|
||||
};
|
||||
personalWechatIngress: {
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
target: { id: string; windowsRoute: string; hostRoute: string; dockerRoute: string };
|
||||
pcWechat: {
|
||||
isolation: string;
|
||||
requiredVersion: string;
|
||||
installerAsset: string;
|
||||
installRoot: string;
|
||||
dataRoot: string;
|
||||
autoUpdatePolicy: string;
|
||||
};
|
||||
wcfHost: {
|
||||
releaseTag: string;
|
||||
requiredWechatVersion: string;
|
||||
releaseUrl: string;
|
||||
sdkRoot: string;
|
||||
stateRoot: string;
|
||||
commandPort: number;
|
||||
messagePort: number;
|
||||
bindHost: string;
|
||||
supervisor: string;
|
||||
rdpPolicy: string;
|
||||
firewall: { publicExposure: boolean; allowedClientRefs: string[] };
|
||||
};
|
||||
collector: {
|
||||
runtime: string;
|
||||
containerName: string;
|
||||
wcfHost: string;
|
||||
commandPort: number;
|
||||
messagePort: number;
|
||||
stateRoot: string;
|
||||
queueMode: string;
|
||||
outboxMode: string;
|
||||
archiveCallbackRef: string;
|
||||
readOnly: { enabled: boolean; sendCapability: boolean; allowedMethods: string[]; forbiddenMethods: string[] };
|
||||
};
|
||||
poc: { accountPolicy: string; observationWindowHours: number; requiredMessageTypes: string[] };
|
||||
};
|
||||
baiduNetdisk: {
|
||||
serviceId: string;
|
||||
proxyMode: string;
|
||||
@@ -112,6 +151,7 @@ export function wechatArchiveHelp(): Record<string, unknown> {
|
||||
boundary: {
|
||||
langbot: "Receives WeChat/OpenClaw events and forwards archive events to the YAML-declared n8n webhook.",
|
||||
n8n: "Stores a YAML-rendered workflow that normalizes WeChat payloads.",
|
||||
personalWechatIngress: "Optionally declares the D601 Windows PC WeChat + WeChatFerry host and Docker read-only collector boundary.",
|
||||
baiduNetdisk: "Uploads and downloads through the private backend-core microservice proxy; Baidu credentials remain in the baidu-netdisk service.",
|
||||
database: "LangBot and n8n continue to use the single PK01/Pika01 external PostgreSQL instance with separate databases/roles.",
|
||||
},
|
||||
@@ -325,6 +365,7 @@ function readConfig(): WechatArchiveConfig {
|
||||
const n8n = recordField(root, "n8n", configLabel);
|
||||
const n8nWorkflow = recordField(n8n, "workflow", `${configLabel}.n8n`);
|
||||
const archiveCallback = recordField(root, "archiveCallback", configLabel);
|
||||
const personalWechatIngress = parsePersonalWechatIngress(recordField(root, "personalWechatIngress", configLabel));
|
||||
const baiduNetdisk = recordField(root, "baiduNetdisk", configLabel);
|
||||
const staging = recordField(baiduNetdisk, "staging", `${configLabel}.baiduNetdisk`);
|
||||
const archive = recordField(baiduNetdisk, "archive", `${configLabel}.baiduNetdisk`);
|
||||
@@ -387,6 +428,7 @@ function readConfig(): WechatArchiveConfig {
|
||||
tokenKey: stringField(archiveCallback, "tokenKey", `${configLabel}.archiveCallback`),
|
||||
timeoutMs: numberField(archiveCallback, "timeoutMs", `${configLabel}.archiveCallback`),
|
||||
},
|
||||
personalWechatIngress,
|
||||
baiduNetdisk: {
|
||||
serviceId: stringField(baiduNetdisk, "serviceId", `${configLabel}.baiduNetdisk`),
|
||||
proxyMode: stringField(baiduNetdisk, "proxyMode", `${configLabel}.baiduNetdisk`),
|
||||
@@ -448,6 +490,7 @@ function validateConfig(config: WechatArchiveConfig): void {
|
||||
}
|
||||
if (!config.baiduNetdisk.archive.pathTemplate.includes("{{remoteRoot}}")) throw new Error(`${configLabel}.baiduNetdisk.archive.pathTemplate must include {{remoteRoot}}`);
|
||||
if (config.baiduNetdisk.proxyMode !== "backend-core-microservice-proxy") throw new Error(`${configLabel}.baiduNetdisk.proxyMode must be backend-core-microservice-proxy`);
|
||||
validatePersonalWechatIngress(config.personalWechatIngress);
|
||||
}
|
||||
|
||||
function assertTarget(config: WechatArchiveConfig, targetId: string): void {
|
||||
@@ -487,6 +530,7 @@ function configSummary(config: WechatArchiveConfig): Record<string, unknown> {
|
||||
timeoutMs: config.archiveCallback.timeoutMs,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
personalWechatIngress: personalWechatIngressSummary(config.personalWechatIngress),
|
||||
baiduNetdisk: {
|
||||
serviceId: config.baiduNetdisk.serviceId,
|
||||
proxyMode: config.baiduNetdisk.proxyMode,
|
||||
@@ -506,6 +550,12 @@ function policyChecks(config: WechatArchiveConfig): Array<Record<string, unknown
|
||||
{ name: "baidu-private-proxy", ok: config.baiduNetdisk.proxyMode === "backend-core-microservice-proxy", detail: "Baidu Netdisk access stays behind backend-core microservice proxy." },
|
||||
{ name: "callback-token-source-ref", ok: Boolean(config.archiveCallback.tokenSourceRef && config.archiveCallback.tokenKey), detail: "n8n archive callback token is read from a YAML-declared local sourceRef and never reverse-engineered from runtime." },
|
||||
{ name: "personal-wechat-read-only", ok: config.langbot.readOnlyRecord.enabled && config.langbot.readOnlyRecord.discardPipelineUuid === "__discard__", detail: "LangBot inbound webhook must return skip_pipeline=true and bot fallback must discard so personal WeChat receives no automated replies." },
|
||||
{ name: "personal-wechat-ingress-yaml", ok: config.personalWechatIngress.enabled === true, detail: "D601 PC WeChat + WeChatFerry host and Docker collector boundary is declared in YAML." },
|
||||
{ name: "wcf-version-pin", ok: config.personalWechatIngress.pcWechat.requiredVersion === config.personalWechatIngress.wcfHost.requiredWechatVersion, detail: "The isolated PC WeChat version and WCF host requirement must match in YAML." },
|
||||
{ name: "wcf-no-public-exposure", ok: config.personalWechatIngress.wcfHost.firewall.publicExposure === false, detail: "WeChatFerry raw RPC must not be publicly exposed." },
|
||||
{ name: "collector-read-only", ok: config.personalWechatIngress.collector.readOnly.enabled === true && config.personalWechatIngress.collector.readOnly.sendCapability === false, detail: "Docker collector is a read-only WCF RPC client and must not export send/friend/group/database surfaces." },
|
||||
{ name: "collector-port-alignment", ok: config.personalWechatIngress.collector.commandPort === config.personalWechatIngress.wcfHost.commandPort && config.personalWechatIngress.collector.messagePort === config.personalWechatIngress.wcfHost.messagePort, detail: "Collector WCF ports must match the Windows WCF host ports declared in YAML." },
|
||||
{ name: "test-account-first", ok: config.personalWechatIngress.poc.accountPolicy === "test-account-first", detail: "Initial PoC must use a test or low-risk WeChat account before any production account promotion." },
|
||||
{ name: "single-pk01-postgres", ok: true, detail: "LangBot and n8n use dedicated databases inside the single PK01/Pika01 external PostgreSQL instance; this workflow adds no PostgreSQL instance." },
|
||||
{ name: "text-enabled", ok: config.messageTypes.text?.enabled === true, detail: "Text messages are archive-enabled." },
|
||||
{ name: "image-enabled", ok: config.messageTypes.image?.enabled === true, detail: "Image messages are archive-enabled." },
|
||||
@@ -513,6 +563,150 @@ function policyChecks(config: WechatArchiveConfig): Array<Record<string, unknown
|
||||
];
|
||||
}
|
||||
|
||||
function parsePersonalWechatIngress(raw: Record<string, unknown>): WechatArchiveConfig["personalWechatIngress"] {
|
||||
const target = recordField(raw, "target", `${configLabel}.personalWechatIngress`);
|
||||
const pcWechat = recordField(raw, "pcWechat", `${configLabel}.personalWechatIngress`);
|
||||
const wcfHost = recordField(raw, "wcfHost", `${configLabel}.personalWechatIngress`);
|
||||
const firewall = recordField(wcfHost, "firewall", `${configLabel}.personalWechatIngress.wcfHost`);
|
||||
const collector = recordField(raw, "collector", `${configLabel}.personalWechatIngress`);
|
||||
const readOnly = recordField(collector, "readOnly", `${configLabel}.personalWechatIngress.collector`);
|
||||
const poc = recordField(raw, "poc", `${configLabel}.personalWechatIngress`);
|
||||
return {
|
||||
enabled: booleanField(raw, "enabled", `${configLabel}.personalWechatIngress`),
|
||||
mode: stringField(raw, "mode", `${configLabel}.personalWechatIngress`),
|
||||
target: {
|
||||
id: stringField(target, "id", `${configLabel}.personalWechatIngress.target`),
|
||||
windowsRoute: stringField(target, "windowsRoute", `${configLabel}.personalWechatIngress.target`),
|
||||
hostRoute: stringField(target, "hostRoute", `${configLabel}.personalWechatIngress.target`),
|
||||
dockerRoute: stringField(target, "dockerRoute", `${configLabel}.personalWechatIngress.target`),
|
||||
},
|
||||
pcWechat: {
|
||||
isolation: stringField(pcWechat, "isolation", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
requiredVersion: stringField(pcWechat, "requiredVersion", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
installerAsset: stringField(pcWechat, "installerAsset", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
installRoot: stringField(pcWechat, "installRoot", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
dataRoot: stringField(pcWechat, "dataRoot", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
autoUpdatePolicy: stringField(pcWechat, "autoUpdatePolicy", `${configLabel}.personalWechatIngress.pcWechat`),
|
||||
},
|
||||
wcfHost: {
|
||||
releaseTag: stringField(wcfHost, "releaseTag", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
requiredWechatVersion: stringField(wcfHost, "requiredWechatVersion", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
releaseUrl: stringField(wcfHost, "releaseUrl", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
sdkRoot: stringField(wcfHost, "sdkRoot", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
stateRoot: stringField(wcfHost, "stateRoot", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
commandPort: numberField(wcfHost, "commandPort", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
messagePort: numberField(wcfHost, "messagePort", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
bindHost: stringField(wcfHost, "bindHost", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
supervisor: stringField(wcfHost, "supervisor", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
rdpPolicy: stringField(wcfHost, "rdpPolicy", `${configLabel}.personalWechatIngress.wcfHost`),
|
||||
firewall: {
|
||||
publicExposure: booleanField(firewall, "publicExposure", `${configLabel}.personalWechatIngress.wcfHost.firewall`),
|
||||
allowedClientRefs: stringArrayField(firewall, "allowedClientRefs", `${configLabel}.personalWechatIngress.wcfHost.firewall`),
|
||||
},
|
||||
},
|
||||
collector: {
|
||||
runtime: stringField(collector, "runtime", `${configLabel}.personalWechatIngress.collector`),
|
||||
containerName: stringField(collector, "containerName", `${configLabel}.personalWechatIngress.collector`),
|
||||
wcfHost: stringField(collector, "wcfHost", `${configLabel}.personalWechatIngress.collector`),
|
||||
commandPort: numberField(collector, "commandPort", `${configLabel}.personalWechatIngress.collector`),
|
||||
messagePort: numberField(collector, "messagePort", `${configLabel}.personalWechatIngress.collector`),
|
||||
stateRoot: stringField(collector, "stateRoot", `${configLabel}.personalWechatIngress.collector`),
|
||||
queueMode: stringField(collector, "queueMode", `${configLabel}.personalWechatIngress.collector`),
|
||||
outboxMode: stringField(collector, "outboxMode", `${configLabel}.personalWechatIngress.collector`),
|
||||
archiveCallbackRef: stringField(collector, "archiveCallbackRef", `${configLabel}.personalWechatIngress.collector`),
|
||||
readOnly: {
|
||||
enabled: booleanField(readOnly, "enabled", `${configLabel}.personalWechatIngress.collector.readOnly`),
|
||||
sendCapability: booleanField(readOnly, "sendCapability", `${configLabel}.personalWechatIngress.collector.readOnly`),
|
||||
allowedMethods: stringArrayField(readOnly, "allowedMethods", `${configLabel}.personalWechatIngress.collector.readOnly`),
|
||||
forbiddenMethods: stringArrayField(readOnly, "forbiddenMethods", `${configLabel}.personalWechatIngress.collector.readOnly`),
|
||||
},
|
||||
},
|
||||
poc: {
|
||||
accountPolicy: stringField(poc, "accountPolicy", `${configLabel}.personalWechatIngress.poc`),
|
||||
observationWindowHours: numberField(poc, "observationWindowHours", `${configLabel}.personalWechatIngress.poc`),
|
||||
requiredMessageTypes: stringArrayField(poc, "requiredMessageTypes", `${configLabel}.personalWechatIngress.poc`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validatePersonalWechatIngress(config: WechatArchiveConfig["personalWechatIngress"]): void {
|
||||
if (!config.enabled) return;
|
||||
if (config.pcWechat.requiredVersion !== config.wcfHost.requiredWechatVersion) {
|
||||
throw new Error(`${configLabel}.personalWechatIngress pcWechat.requiredVersion and wcfHost.requiredWechatVersion must match`);
|
||||
}
|
||||
if (config.collector.commandPort !== config.wcfHost.commandPort || config.collector.messagePort !== config.wcfHost.messagePort) {
|
||||
throw new Error(`${configLabel}.personalWechatIngress collector ports must match wcfHost ports`);
|
||||
}
|
||||
if (config.wcfHost.firewall.publicExposure !== false) {
|
||||
throw new Error(`${configLabel}.personalWechatIngress.wcfHost.firewall.publicExposure must be false`);
|
||||
}
|
||||
if (config.collector.readOnly.enabled !== true || config.collector.readOnly.sendCapability !== false) {
|
||||
throw new Error(`${configLabel}.personalWechatIngress.collector.readOnly must enable read-only mode and set sendCapability=false`);
|
||||
}
|
||||
const allowed = new Set(config.collector.readOnly.allowedMethods);
|
||||
const overlap = config.collector.readOnly.forbiddenMethods.filter((method) => allowed.has(method));
|
||||
if (overlap.length > 0) throw new Error(`${configLabel}.personalWechatIngress.collector.readOnly method allowlist overlaps forbiddenMethods: ${overlap.join(",")}`);
|
||||
if (config.collector.archiveCallbackRef !== "archiveCallback") {
|
||||
throw new Error(`${configLabel}.personalWechatIngress.collector.archiveCallbackRef must point at archiveCallback`);
|
||||
}
|
||||
}
|
||||
|
||||
function personalWechatIngressSummary(config: WechatArchiveConfig["personalWechatIngress"]): Record<string, unknown> {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
mode: config.mode,
|
||||
target: config.target,
|
||||
pcWechat: {
|
||||
isolation: config.pcWechat.isolation,
|
||||
requiredVersion: config.pcWechat.requiredVersion,
|
||||
installerAsset: config.pcWechat.installerAsset,
|
||||
installRoot: config.pcWechat.installRoot,
|
||||
dataRoot: config.pcWechat.dataRoot,
|
||||
autoUpdatePolicy: config.pcWechat.autoUpdatePolicy,
|
||||
},
|
||||
wcfHost: {
|
||||
releaseTag: config.wcfHost.releaseTag,
|
||||
requiredWechatVersion: config.wcfHost.requiredWechatVersion,
|
||||
releaseUrl: config.wcfHost.releaseUrl,
|
||||
sdkRoot: config.wcfHost.sdkRoot,
|
||||
stateRoot: config.wcfHost.stateRoot,
|
||||
commandPort: config.wcfHost.commandPort,
|
||||
messagePort: config.wcfHost.messagePort,
|
||||
bindHost: config.wcfHost.bindHost,
|
||||
supervisor: config.wcfHost.supervisor,
|
||||
rdpPolicy: config.wcfHost.rdpPolicy,
|
||||
firewall: config.wcfHost.firewall,
|
||||
},
|
||||
collector: {
|
||||
runtime: config.collector.runtime,
|
||||
containerName: config.collector.containerName,
|
||||
wcfHost: config.collector.wcfHost,
|
||||
commandPort: config.collector.commandPort,
|
||||
messagePort: config.collector.messagePort,
|
||||
stateRoot: config.collector.stateRoot,
|
||||
queueMode: config.collector.queueMode,
|
||||
outboxMode: config.collector.outboxMode,
|
||||
archiveCallbackRef: config.collector.archiveCallbackRef,
|
||||
readOnly: {
|
||||
enabled: config.collector.readOnly.enabled,
|
||||
sendCapability: config.collector.readOnly.sendCapability,
|
||||
allowedMethods: config.collector.readOnly.allowedMethods,
|
||||
forbiddenMethods: config.collector.readOnly.forbiddenMethods,
|
||||
},
|
||||
},
|
||||
poc: config.poc,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function stringArrayField(record: Record<string, unknown>, key: string, label: string): string[] {
|
||||
const value = record[key];
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
||||
throw new Error(`${label}.${key} must be a string array`);
|
||||
}
|
||||
return value.map((item) => String(item));
|
||||
}
|
||||
|
||||
function parseReadOnlyRecord(raw: Record<string, unknown>): WechatArchiveConfig["langbot"]["readOnlyRecord"] {
|
||||
return {
|
||||
enabled: booleanField(raw, "enabled", `${configLabel}.langbot.readOnlyRecord`),
|
||||
|
||||
Reference in New Issue
Block a user