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

Quelle  channel.ts

  Sprache: JAVA
 

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

/**
 * Synology Chat Channel Plugin for OpenClaw.
 *
 * Implements the ChannelPlugin interface following the LINE pattern.
 */

import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution";
import {
  createHybridChannelConfigAdapter,
  createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
import {
  composeWarningCollectors,
  createConditionalWarningCollector,
  projectAccountConfigWarningCollector,
  projectAccountWarningCollector,
} from "openclaw/plugin-sdk/channel-policy";
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { listAccountIds, resolveAccount } from "./accounts.js";
import { synologyChatApprovalAuth } from "./approval-auth.js";
import { sendMessage, sendFileUrl } from "./client.js";
import { SynologyChatChannelConfigSchema } from "./config-schema.js";
import {
  collectSynologyGatewayRoutingWarnings,
  registerSynologyWebhookRoute,
  validateSynologyGatewayAccountStartup,
} from "./gateway-runtime.js";
import { collectSynologyChatSecurityAuditFindings } from "./security-audit.js";
import { synologyChatSetupAdapter, synologyChatSetupWizard } from "./setup-surface.js";
import type { ResolvedSynologyChatAccount } from "./types.js";

const CHANNEL_ID = "synology-chat";

function normalizeLowercaseStringOrEmpty(value: unknown): string {
  return typeof value === "string" ? value.trim().toLowerCase() : "";
}

const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver<ResolvedSynologyChatAccount>({
  channelKey: CHANNEL_ID,
  resolvePolicy: (account) => account.dmPolicy,
  resolveAllowFrom: (account) => account.allowedUserIds,
  policyPathSuffix: "dmPolicy",
  defaultPolicy: "allowlist",
  approveHint: "openclaw pairing approve synology-chat <code>",
  normalizeEntry: (raw) => normalizeLowercaseStringOrEmpty(raw),
});

type SynologyChannelGatewayContext = {
  cfg: OpenClawConfig;
  accountId: string;
  abortSignal: AbortSignal;
  log?: {
    info: (message: string) => void;
    warn: (message: string) => void;
    error: (message: string) => void;
  };
};
type SynologyChannelOutboundContext = {
  cfg: OpenClawConfig;
  to: string;
  text?: string;
  mediaUrl?: string;
  accountId?: string | null;
};
type SynologyChannelSendTextContext = SynologyChannelOutboundContext & { text: string };
type _SynologyChannelSendMediaContext = SynologyChannelOutboundContext & { mediaUrl: string };
type SynologySecurityWarningContext = {
  cfg: OpenClawConfig;
  account: ResolvedSynologyChatAccount;
};

const synologyChatConfigAdapter = createHybridChannelConfigAdapter<ResolvedSynologyChatAccount>({
  sectionKey: CHANNEL_ID,
  listAccountIds,
  resolveAccount,
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
  clearBaseFields: [
    "token",
    "incomingUrl",
    "nasHost",
    "webhookPath",
    "dangerouslyAllowNameMatching",
    "dangerouslyAllowInheritedWebhookPath",
    "dmPolicy",
    "allowedUserIds",
    "rateLimitPerMinute",
    "botName",
    "allowInsecureSsl",
  ],
  resolveAllowFrom: (account) => account.allowedUserIds,
  formatAllowFrom: (allowFrom) =>
    allowFrom.map((entry) => normalizeLowercaseStringOrEmpty(String(entry))).filter(Boolean),
});

const collectSynologyChatSecurityWarnings =
  createConditionalWarningCollector<ResolvedSynologyChatAccount>(
    (account) =>
      !account.token &&
      "- Synology Chat: token is not configured. The webhook will reject all requests.",
    (account) =>
      !account.incomingUrl &&
      "- Synology Chat: incomingUrl is not configured. The bot cannot send replies.",
    (account) =>
      account.allowInsecureSsl &&
      "- Synology Chat: SSL verification is disabled (allowInsecureSsl=true). Only use this for local NAS with self-signed certificates.",
    (account) =>
      account.dangerouslyAllowNameMatching &&
      "- Synology Chat: dangerouslyAllowNameMatching=true re-enables mutable username/nickname recipient matching for replies. Prefer stable numeric user IDs.",
    (account) =>
      account.dangerouslyAllowInheritedWebhookPath &&
      account.webhookPathSource === "inherited-base" &&
      "- Synology Chat: dangerouslyAllowInheritedWebhookPath=true opts a named account into a shared inherited webhook path. Prefer an explicit per-account webhookPath.",
    (account) =>
      account.dmPolicy === "open" &&
      '- Synology Chat: dmPolicy="open" allows any user to message the bot. Consider "allowlist" for production use.',
    (account) =>
      account.dmPolicy === "allowlist" &&
      account.allowedUserIds.length === 0 &&
      '- Synology Chat: dmPolicy="allowlist" with empty allowedUserIds blocks all senders. Add users or set dmPolicy="open".',
  );

type SynologyChatOutboundResult = {
  channel: typeof CHANNEL_ID;
  messageId: string;
  chatId: string;
};

type SynologyChatPlugin = Omit<
  ChannelPlugin<ResolvedSynologyChatAccount>,
  "pairing" | "security" | "messaging" | "directory" | "outbound" | "gateway" | "agentPrompt"
> & {
  pairing: {
    idLabel: string;
    normalizeAllowEntry?: (entry: string) => string;
    notifyApproval: (params: { cfg: OpenClawConfig; id: string }) => Promise<void>;
  };
  security: {
    resolveDmPolicy: (params: { cfg: OpenClawConfig; account: ResolvedSynologyChatAccount }) => {
      policy: string | null | undefined;
      allowFrom?: Array<string | number>;
      normalizeEntry?: (raw: string) => string;
    } | null;
    collectWarnings: (params: {
      cfg: OpenClawConfig;
      account: ResolvedSynologyChatAccount;
    }) => string[];
  };
  messaging: {
    normalizeTarget: (target: string) => string | undefined;
    targetResolver: {
      looksLikeId: (id: string) => boolean;
      hint: string;
    };
  };
  directory: {
    self?: NonNullable<ChannelPlugin<ResolvedSynologyChatAccount>["directory"]>["self"];
    listPeers?: NonNullable<ChannelPlugin<ResolvedSynologyChatAccount>["directory"]>["listPeers"];
    listGroups?: NonNullable<ChannelPlugin<ResolvedSynologyChatAccount>["directory"]>["listGroups"];
  };
  outbound: {
    deliveryMode: "gateway";
    textChunkLimit: number;
    sendText: (ctx: SynologyChannelSendTextContext) => Promise<SynologyChatOutboundResult>;
    sendMedia: (ctx: SynologyChannelOutboundContext) => Promise<SynologyChatOutboundResult>;
  };
  gateway: {
    startAccount: (ctx: SynologyChannelGatewayContext) => Promise<unknown>;
    stopAccount: (ctx: SynologyChannelGatewayContext) => Promise<void>;
  };
  agentPrompt: {
    messageToolHints: () => string[];
  };
};

const collectSynologyChatRoutingWarnings = projectAccountConfigWarningCollector<
  ResolvedSynologyChatAccount,
  OpenClawConfig,
  SynologySecurityWarningContext
>(
  (cfg) => cfg,
  ({ account, cfg }) => collectSynologyGatewayRoutingWarnings({ account, cfg }),
);

function resolveOutboundAccount(
  cfg: OpenClawConfig,
  accountId?: string | null,
): ResolvedSynologyChatAccount {
  return resolveAccount(cfg ?? {}, accountId);
}

function requireIncomingUrl(account: ResolvedSynologyChatAccount): string {
  if (!account.incomingUrl) {
    throw new Error("Synology Chat incoming URL not configured");
  }
  return account.incomingUrl;
}

export function createSynologyChatPlugin(): SynologyChatPlugin {
  return createChatChannelPlugin({
    base: {
      id: CHANNEL_ID,
      meta: {
        id: CHANNEL_ID,
        label: "Synology Chat",
        selectionLabel: "Synology Chat (Webhook)",
        detailLabel: "Synology Chat (Webhook)",
        docsPath: "/channels/synology-chat",
        blurb: "Connect your Synology NAS Chat to OpenClaw",
        order: 90,
      },
      capabilities: {
        chatTypes: ["direct" as const],
        media: true,
        threads: false,
        reactions: false,
        edit: false,
        unsend: false,
        reply: false,
        effects: false,
        blockStreaming: false,
      },
      reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
      configSchema: SynologyChatChannelConfigSchema,
      setup: synologyChatSetupAdapter,
      setupWizard: synologyChatSetupWizard,
      config: {
        ...synologyChatConfigAdapter,
      },
      approvalCapability: synologyChatApprovalAuth,
      messaging: {
        normalizeTarget: (target: string) => {
          const trimmed = target.trim();
          if (!trimmed) {
            return undefined;
          }
          // Strip common prefixes
          return trimmed.replace(/^synology[-_]?chat:/i, "").trim();
        },
        targetResolver: {
          looksLikeId: (id: string) => {
            const trimmed = id?.trim();
            if (!trimmed) {
              return false;
            }
            // Synology Chat user IDs are numeric
            return /^\d+$/.test(trimmed) || /^synology[-_]?chat:/i.test(trimmed);
          },
          hint: "<userId>",
        },
      },
      directory: createEmptyChannelDirectoryAdapter(),
      gateway: {
        startAccount: async (ctx: SynologyChannelGatewayContext) => {
          const { cfg, accountId, log, abortSignal } = ctx;
          const account = resolveAccount(cfg, accountId);
          if (!validateSynologyGatewayAccountStartup({ cfg, account, accountId, log }).ok) {
            return waitUntilAbort(abortSignal);
          }

          log?.info?.(
            `Starting Synology Chat channel (account: ${accountId}, path: ${account.webhookPath})`,
          );
          const unregister = registerSynologyWebhookRoute({ account, accountId, log });

          log?.info?.(`Registered HTTP route: ${account.webhookPath} for Synology Chat`);

          // Keep alive until abort signal fires.
          // The gateway expects a Promise that stays pending while the channel is running.
          // Resolving immediately triggers a restart loop.
          return waitUntilAbort(abortSignal, () => {
            log?.info?.(`Stopping Synology Chat channel (account: ${accountId})`);
            unregister();
          });
        },

        stopAccount: async (ctx: SynologyChannelGatewayContext) => {
          ctx.log?.info?.(`Synology Chat account ${ctx.accountId} stopped`);
        },
      },
      agentPrompt: {
        messageToolHints: () => [
          "",
          "### Synology Chat Formatting",
          "Synology Chat supports limited formatting. Use these patterns:",
          "",
          "**Links**: Use `<URL|display text>` to create clickable links.",
          "  Example: `<https://example.com|Click here>` renders as a clickable link.",
          "",
          "**File sharing**: Include a publicly accessible URL to share files or images.",
          "  The NAS will download and attach the file (max 32 MB).",
          "",
          "**Limitations**:",
          "- No markdown, bold, italic, or code blocks",
          "- No buttons, cards, or interactive elements",
          "- No message editing after send",
          "- Keep messages under 2000 characters for best readability",
          "",
          "**Best practices**:",
          "- Use short, clear responses (Synology Chat has a minimal UI)",
          "- Use line breaks to separate sections",
          "- Use numbered or bulleted lists for clarity",
          "- Wrap URLs with `<URL|label>` for user-friendly links",
        ],
      },
    },
    pairing: {
      text: {
        idLabel: "synologyChatUserId",
        message: "OpenClaw: your access has been approved.",
        normalizeAllowEntry: (entry: string) => normalizeLowercaseStringOrEmpty(entry),
        notify: async ({ cfg, id, message }) => {
          const account = resolveAccount(cfg);
          if (!account.incomingUrl) {
            return;
          }
          await sendMessage(account.incomingUrl, message, id, account.allowInsecureSsl);
        },
      },
    },
    security: {
      resolveDmPolicy: resolveSynologyChatDmPolicy,
      collectWarnings: composeWarningCollectors(
        projectAccountWarningCollector<ResolvedSynologyChatAccount, SynologySecurityWarningContext>(
          collectSynologyChatSecurityWarnings,
        ),
        collectSynologyChatRoutingWarnings,
      ),
      collectAuditFindings: collectSynologyChatSecurityAuditFindings,
    },
    outbound: {
      deliveryMode: "gateway" as const,
      textChunkLimit: 2000,

      sendText: async ({ to, text, accountId, cfg }: SynologyChannelSendTextContext) => {
        const account = resolveOutboundAccount(cfg ?? {}, accountId);
        const incomingUrl = requireIncomingUrl(account);
        const ok = await sendMessage(incomingUrl, text, to, account.allowInsecureSsl);
        if (!ok) {
          throw new Error("Failed to send message to Synology Chat");
        }
        return attachChannelToResult(CHANNEL_ID, { messageId: `sc-${Date.now()}`, chatId: to });
      },

      sendMedia: async ({ to, mediaUrl, accountId, cfg }: SynologyChannelOutboundContext) => {
        const account = resolveOutboundAccount(cfg ?? {}, accountId);
        const incomingUrl = requireIncomingUrl(account);
        if (!mediaUrl) {
          throw new Error("No media URL provided");
        }

        const ok = await sendFileUrl(incomingUrl, mediaUrl, to, account.allowInsecureSsl);
        if (!ok) {
          throw new Error("Failed to send media to Synology Chat");
        }
        return attachChannelToResult(CHANNEL_ID, { messageId: `sc-${Date.now()}`, chatId: to });
      },
    },
  }) as unknown as SynologyChatPlugin;
}

export const synologyChatPlugin = createSynologyChatPlugin();

¤ Dauer der Verarbeitung: 0.26 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






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.