📄 command-auth.ts

import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";

import type { WecomAccountConfig } from "../types.js";

function normalizeWecomAllowFromEntry(raw: string): string {
  return raw
    .trim()
    .toLowerCase()
    .replace(/^wecom:/, "")
    .replace(/^user:/, "")
    .replace(/^userid:/, "");
}

function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolean {
  const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
  if (list.includes("*")) return true;
  const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
  if (!normalizedSender) return false;
  return list.includes(normalizedSender);
}

export async function resolveWecomCommandAuthorization(params: {
  core: PluginRuntime;
  cfg: OpenClawConfig;
  accountConfig: WecomAccountConfig;
  rawBody: string;
  senderUserId: string;
}): Promise<{
  shouldComputeAuth: boolean;
  dmPolicy: "pairing" | "allowlist" | "open" | "disabled";
  senderAllowed: boolean;
  authorizerConfigured: boolean;
  commandAuthorized: boolean | undefined;
  effectiveAllowFrom: string[];
}> {
  const { core, cfg, accountConfig, rawBody, senderUserId } = params;

  const dmPolicy = (accountConfig.dm?.policy ?? "pairing") as "pairing" | "allowlist" | "open" | "disabled";
  const configAllowFrom = (accountConfig.dm?.allowFrom ?? []).map((v) => String(v));

  const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, cfg);
  // WeCom channel currently does NOT support the `openclaw pairing` CLI workflow
  // ("Channel wecom does not support pairing"). So we must not rely on pairing
  // store approvals for command authorization here.
  //
  // Policy semantics:
  // - open: commands are allowed for everyone by default (unless higher-level access-groups deny).
  // - allowlist: commands require allowFrom entries.
  // - pairing: treated the same as allowlist for WeCom (since pairing CLI is unsupported).
  const effectiveAllowFrom = dmPolicy === "open" ? ["*"] : configAllowFrom;

  const senderAllowed = isWecomSenderAllowed(senderUserId, effectiveAllowFrom);
  const allowAllConfigured = effectiveAllowFrom.some((entry) => normalizeWecomAllowFromEntry(entry) === "*");
  const authorizerConfigured = allowAllConfigured || effectiveAllowFrom.length > 0;
  const useAccessGroups = cfg.commands?.useAccessGroups !== false;

  const commandAuthorized = shouldComputeAuth
    ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
      useAccessGroups,
      authorizers: [{ configured: authorizerConfigured, allowed: senderAllowed }],
    })
    : undefined;

  return {
    shouldComputeAuth,
    dmPolicy,
    senderAllowed,
    authorizerConfigured,
    commandAuthorized,
    effectiveAllowFrom,
  };
}

export function buildWecomUnauthorizedCommandPrompt(params: {
  senderUserId: string;
  dmPolicy: "pairing" | "allowlist" | "open" | "disabled";
  scope: "bot" | "agent";
}): string {
  const user = params.senderUserId || "unknown";
  const policy = params.dmPolicy;
  const scopeLabel = params.scope === "bot" ? "Bot(智能机器人)" : "Agent(自建应用)";
  const dmPrefix = params.scope === "bot" ? "channels.wecom.bot.dm" : "channels.wecom.agent.dm";
  const allowCmd = (value: string) => `openclaw config set ${dmPrefix}.allowFrom '${value}'`;
  const policyCmd = (value: string) => `openclaw config set ${dmPrefix}.policy "${value}"`;

  if (policy === "disabled") {
    return [
      `无权限执行命令(${scopeLabel} 已禁用:dm.policy=disabled)`,
      `触发者:${user}`,
      `管理员:${policyCmd("open")}(全放开)或 ${policyCmd("allowlist")}(白名单)`,
    ].join("\n");
  }
  // WeCom 不支持 pairing CLI,因此这里统一给出“open / allowlist”两种明确的配置指令
  return [
    `无权限执行命令(入口:${scopeLabel},userid:${user})`,
    `管理员全放开:${policyCmd("open")}`,
    `管理员放行该用户:${policyCmd("allowlist")}`,
    `然后设置白名单:${allowCmd(JSON.stringify([user]))}`,
    `如果仍被拦截:检查 commands.useAccessGroups/访问组`,
  ].join("\n");
}