Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/extensions/qqbot/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 10 kB image not shown  

Impressum channel.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
// Register the PlatformAdapter before any core/ module is used.
import "./bridge/bootstrap.js";
import { getQQBotApprovalCapability } from "./bridge/approval/capability.js";
import { qqbotConfigAdapter, qqbotMeta, qqbotSetupAdapterShared } from "./bridge/config-shared.js";
import {
  applyQQBotAccountConfig,
  DEFAULT_ACCOUNT_ID,
  resolveQQBotAccount,
} from "./bridge/config.js";
import { getQQBotRuntime } from "./bridge/runtime.js";
import { qqbotSetupWizard } from "./bridge/setup/surface.js";
import { qqbotChannelConfigSchema } from "./config-schema.js";
import { loadCredentialBackup, saveCredentialBackup } from "./engine/config/credential-backup.js";
import { clearAccountCredentials } from "./engine/config/credentials.js";
import {
  normalizeTarget as coreNormalizeTarget,
  looksLikeQQBotTarget,
} from "./engine/messaging/target-parser.js";
// Re-export text helpers from core/.
export { chunkText, TEXT_CHUNK_LIMIT } from "./engine/utils/text-chunk.js";
import type { ResolvedQQBotAccount } from "./types.js";

// Shared promise so concurrent multi-account startups serialize the dynamic
// import of the gateway module, avoiding an ESM circular-dependency race.
let _gatewayModulePromise: Promise<typeof import("./bridge/gateway.js")> | undefined;
function loadGatewayModule(): Promise<typeof import("./bridge/gateway.js")> {
  _gatewayModulePromise ??= import("./bridge/gateway.js");
  return _gatewayModulePromise;
}

const EXEC_APPROVAL_COMMAND_RE =
  /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;

function shouldSuppressLocalQQBotApprovalPrompt(params: {
  cfg: OpenClawConfig;
  accountId?: string | null;
  payload: { text?: string; channelData?: unknown };
  hint?: { kind: "approval-pending" | "approval-resolved"; approvalKind: "exec" | "plugin" };
}): boolean {
  if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") {
    return false;
  }
  const account = resolveQQBotAccount(params.cfg, params.accountId);
  if (!account.enabled || account.secretSource === "none") {
    return false;
  }
  if (getExecApprovalReplyMetadata(params.payload as never)) {
    return true;
  }
  const text = typeof params.payload.text === "string" ? params.payload.text : "";
  return EXEC_APPROVAL_COMMAND_RE.test(text);
}

export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
  id: "qqbot",
  setupWizard: qqbotSetupWizard,
  meta: {
    ...qqbotMeta,
  },
  capabilities: {
    chatTypes: ["direct", "group"],
    media: true,
    reactions: false,
    threads: false,
    blockStreaming: true,
  },
  reload: { configPrefixes: ["channels.qqbot"] },
  configSchema: qqbotChannelConfigSchema,
  config: {
    ...qqbotConfigAdapter,
    /**
     * Treat an account as configured when either the live config has
     * credentials OR a recoverable credential backup exists. This mirrors
     * the standalone plugin and lets the gateway survive a hot upgrade
     * that wiped openclaw.json mid-flight.
     */
    isConfigured: (account: ResolvedQQBotAccount | undefined) => {
      if (qqbotConfigAdapter.isConfigured(account)) {
        return true;
      }
      if (!account) {
        return false;
      }
      const backup = loadCredentialBackup(account.accountId);
      return Boolean(backup?.appId && backup?.clientSecret);
    },
  },
  setup: {
    ...qqbotSetupAdapterShared,
  },
  approvalCapability: getQQBotApprovalCapability(),
  messaging: {
    /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
    normalizeTarget: coreNormalizeTarget,
    targetResolver: {
      /** Return true when the id looks like a QQ Bot target. */
      looksLikeId: looksLikeQQBotTarget,
      hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)",
    },
  },
  outbound: {
    deliveryMode: "direct",
    chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
    chunkerMode: "markdown",
    textChunkLimit: 5000,
    shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) =>
      shouldSuppressLocalQQBotApprovalPrompt({
        cfg,
        accountId,
        payload,
        hint,
      }),
    sendText: async ({ to, text, accountId, replyToId, cfg }) => {
      // Ensure bridge/gateway.ts module-level registrations (audio adapter factory,
      // platform adapter, etc.) have executed before engine code runs.
      await loadGatewayModule();
      const account = resolveQQBotAccount(cfg, accountId);
      const { sendText } = await import("./engine/messaging/outbound.js");
      const result = await sendText({ to, text, accountId, replyToId, account: account as never });
      return {
        channel: "qqbot" as const,
        messageId: result.messageId ?? "",
        meta: result.error ? { error: result.error } : undefined,
      };
    },
    sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
      // Same guard as sendText — ensure adapters are registered.
      await loadGatewayModule();
      const account = resolveQQBotAccount(cfg, accountId);
      const { sendMedia } = await import("./engine/messaging/outbound.js");
      const result = await sendMedia({
        to,
        text: text ?? "",
        mediaUrl: mediaUrl ?? "",
        accountId,
        replyToId,
        account: account as never,
      });
      return {
        channel: "qqbot" as const,
        messageId: result.messageId ?? "",
        meta: result.error ? { error: result.error } : undefined,
      };
    },
  },
  gateway: {
    startAccount: async (ctx) => {
      let { account, cfg } = ctx;
      const { abortSignal, log } = ctx;

      // Recover credentials from the per-account backup if the live
      // config is missing appId/secret (e.g. a hot-upgrade wiped
      // openclaw.json). We only restore when both fields are empty so a
      // user's intentional clear isn't silently undone.
      if (!account.appId || !account.clientSecret) {
        const backup = loadCredentialBackup(account.accountId);
        if (backup?.appId && backup?.clientSecret) {
          try {
            const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
              appId: backup.appId,
              clientSecret: backup.clientSecret,
            });
            const runtime = getQQBotRuntime();
            const configApi = runtime.config as {
              writeConfigFile: (cfg: OpenClawConfig) => Promise<void>;
            };
            await configApi.writeConfigFile(nextCfg);
            cfg = nextCfg;
            account = resolveQQBotAccount(nextCfg, account.accountId);
            log?.info(
              `[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`,
            );
          } catch (err) {
            log?.error(
              `[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`,
            );
          }
        }
      }

      // Serialize the dynamic import so concurrent multi-account startups
      // do not hit an ESM circular-dependency race where the gateway chunk's
      // transitive imports have not finished evaluating yet.
      const { startGateway } = await loadGatewayModule();

      log?.info(
        `[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`,
      );

      await startGateway({
        account,
        abortSignal,
        cfg,
        log,
        channelRuntime: ctx.channelRuntime as never,
        onReady: () => {
          log?.info(`[qqbot:${account.accountId}] Gateway ready`);
          ctx.setStatus({
            ...ctx.getStatus(),
            running: true,
            connected: true,
            lastConnectedAt: Date.now(),
          });
          // Snapshot credentials so we can recover from the next hot
          // upgrade that might wipe openclaw.json mid-flight.
          if (account.appId && account.clientSecret) {
            saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
          }
        },
        onResumed: () => {
          log?.info(`[qqbot:${account.accountId}] Gateway resumed`);
          ctx.setStatus({
            ...ctx.getStatus(),
            running: true,
            connected: true,
            lastConnectedAt: Date.now(),
          });
          if (account.appId && account.clientSecret) {
            saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
          }
        },
        onError: (error) => {
          log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
          ctx.setStatus({
            ...ctx.getStatus(),
            lastError: error.message,
          });
        },
      });
    },
    logoutAccount: async ({ accountId, cfg }) => {
      const { nextCfg, cleared, changed } = clearAccountCredentials(
        cfg as unknown as Record<string, unknown>,
        accountId,
      );

      if (changed) {
        const runtime = getQQBotRuntime();
        const configApi = runtime.config as {
          writeConfigFile: (cfg: OpenClawConfig) => Promise<void>;
        };
        await configApi.writeConfigFile(nextCfg as OpenClawConfig);
      }

      const resolved = resolveQQBotAccount((changed ? nextCfg : cfg) as OpenClawConfig, accountId);
      const loggedOut = resolved.secretSource === "none";
      const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);

      return { ok: true, cleared, envToken, loggedOut };
    },
  },
  status: {
    defaultRuntime: {
      accountId: DEFAULT_ACCOUNT_ID,
      running: false,
      connected: false,
      lastConnectedAt: null,
      lastError: null,
      lastInboundAt: null,
      lastOutboundAt: null,
    },
    buildChannelSummary: ({ snapshot }) => ({
      configured: snapshot.configured ?? false,
      tokenSource: snapshot.tokenSource ?? "none",
      running: snapshot.running ?? false,
      connected: snapshot.connected ?? false,
      lastConnectedAt: snapshot.lastConnectedAt ?? null,
      lastError: snapshot.lastError ?? null,
    }),
    buildAccountSnapshot: ({ account, runtime }) => ({
      accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
      name: account?.name,
      enabled: account?.enabled ?? false,
      configured: Boolean(account?.appId && account?.clientSecret),
      tokenSource: account?.secretSource,
      running: runtime?.running ?? false,
      connected: runtime?.connected ?? false,
      lastConnectedAt: runtime?.lastConnectedAt ?? null,
      lastError: runtime?.lastError ?? null,
      lastInboundAt: runtime?.lastInboundAt ?? null,
      lastOutboundAt: runtime?.lastOutboundAt ?? null,
    }),
  },
};

¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.20Bemerkung:  (vorverarbeitet am  2026-04-27) ¤

*Bot Zugriff






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.