refactor(channels): make built-in channels self-contained (#4908)

* refactor(channels): own setup and instance contracts

* refactor(channels): isolate management contracts

* refactor(channels): normalize activation contracts

* fix(channels): enforce management contracts

* refactor(channels): finish setup ownership migration

* fix(channels): harden management contracts

* fix(channels): enforce lazy loading and runtime ownership

* fix(feishu): make multi-instance startup idempotent

* fix(webui): render channel setup contracts cleanly

* fix(feishu): stop websocket clients cleanly

* fix(channels): enforce persistence and activation gates

* fix(channels): preserve global feature action scope

* fix(channels): apply defaults for single plugins

* fix(channels): enforce management contract boundaries

* refactor(feishu): remove identity helper indirection

* fix(channels): preserve management setup contracts

* refactor(channels): generalize instance settings UI

* refactor(channels): package channel plugins with web UI metadata

* refactor(channels): make built-ins self-contained packages

* test(channels): colocate tests with channel packages

* fix(dingtalk): use official brand icon

* feat(channels): colocate webui translations

* docs(channels): clarify plugin ownership

* test(exec): remove output wait race

* refactor(channels): unify plugin descriptors

* fix(channels): enforce descriptor-owned contracts

* refactor(channels): finish package-owned plugin setup

* refactor(channels): use repository-owned packages only

* fix(channels): self-describe dependencies and runtime state

* fix(channels): warn about legacy entry points
This commit is contained in:
chengyongru
2026-07-19 23:30:49 +08:00
committed by GitHub
parent 7aaac37bca
commit 462a0dfb0f
388 changed files with 17093 additions and 5110 deletions
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState, type ComponentType } from "react";
import {
Check,
ChevronDown,
@@ -9,6 +9,8 @@ import {
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { channelUiContribution } from "@/channel-plugins/registry";
import type { ChannelPluginConnectFlowProps } from "@/channel-plugins/types";
import { ToggleButton } from "@/components/settings/ToggleButton";
import {
type ChannelProviderPreset,
@@ -21,17 +23,15 @@ import {
} from "@/components/settings/channels/CredentialForm";
import {
ChannelLogo,
ChannelRuntimeError,
ChannelStatusBadge,
channelDescription,
channelDisplayName,
channelRequirements,
channelSetup,
channelStatusLabel,
channelToggleChecked,
localizedChannelDisplayName,
} from "@/components/settings/channels/ChannelIdentity";
import {
FeishuConnectFlow,
WeixinConnectFlow,
} from "@/components/settings/channels/ChannelQrConnectFlow";
import {
ChannelProviderPresets,
ChannelSetupActions,
@@ -41,7 +41,7 @@ import {
ChannelValidationChecks,
ChannelValidationDetails,
} from "@/components/settings/channels/ChannelSetupParts";
import { FeishuAssistantsPanel } from "@/components/settings/channels/FeishuAssistantsPanel";
import { ChannelInstancesPanel } from "@/components/settings/channels/ChannelInstancesPanel";
import { Button } from "@/components/ui/button";
import {
configureChannel,
@@ -68,12 +68,13 @@ export function ChannelCatalogRow({
}) {
const { t } = useTranslation();
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const displayName = localizedChannelDisplayName(feature, t);
return (
<button
type="button"
aria-label={t("settings.channels.selectChannel", {
name: channelDisplayName(feature),
name: displayName,
defaultValue: "View {{name}} settings",
})}
aria-pressed={selected}
@@ -88,14 +89,16 @@ export function ChannelCatalogRow({
<ChannelLogo feature={feature} showBrandLogos={showBrandLogos} />
<div className="min-w-0 flex-1">
<h3 className="truncate text-[14px] font-semibold leading-5 text-foreground">
{channelDisplayName(feature)}
{displayName}
</h3>
<p className="mt-0.5 truncate text-[12.5px] leading-5 text-muted-foreground">
{channelDescription(feature, t)}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<ChannelStatusBadge>{channelStatusLabel(feature, tx)}</ChannelStatusBadge>
<ChannelStatusBadge status={feature.runtime_status}>
{channelStatusLabel(feature, tx)}
</ChannelStatusBadge>
<ChevronRight
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
@@ -125,12 +128,28 @@ export function ChannelSetupPanel({
onAction: (action: "enable" | "disable", name: string) => void;
onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void;
}) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const displayName = localizedChannelDisplayName(feature, t);
const [connectRequestId, setConnectRequestId] = useState(0);
if (feature.name === "feishu") {
const uiContribution = channelUiContribution(feature.name, feature.webui);
const PluginPanel = uiContribution?.Panel;
if (PluginPanel) {
return (
<FeishuAssistantsPanel
<PluginPanel
token={token}
feature={feature}
actionKey={actionKey}
showBrandLogos={showBrandLogos}
chatAppsDocsUrl={chatAppsDocsUrl}
onAction={onAction}
onFeaturesUpdate={onFeaturesUpdate}
/>
);
}
if (feature.instances !== undefined) {
return (
<ChannelInstancesPanel
token={token}
feature={feature}
showBrandLogos={showBrandLogos}
@@ -142,22 +161,22 @@ export function ChannelSetupPanel({
const enableBusy = actionKey === `enable:${feature.name}`;
const disableBusy = actionKey === `disable:${feature.name}`;
const missingSupport = feature.enabled && !feature.installed;
const requiredWebui = feature.name === "websocket";
const channelChecked = requiredWebui || feature.enabled;
const alwaysEnabled = feature.capabilities?.includes("always_enabled") ?? false;
const channelChecked = alwaysEnabled || channelToggleChecked(feature);
const channelBusy = enableBusy || disableBusy;
const setup = channelSetup(feature);
const setup = channelSetup(feature, i18n.resolvedLanguage ?? i18n.language);
const needsSetupBeforeEnable =
!channelChecked
&& feature.configured === false
&& !(feature.name === "weixin" && setup.mode === "connect");
&& !(uiContribution?.canConnectBeforeConfigured && setup.mode === "connect");
const channelToggleDisabled =
requiredWebui
alwaysEnabled
|| channelBusy
|| needsSetupBeforeEnable
|| (!feature.install_supported && !feature.installed && !feature.enabled);
const installSupportLabel = tx("settings.nanobotFeatures.installSupport", "Install support");
const toggleAriaLabel = t("settings.channels.toggleChannel", {
name: channelDisplayName(feature),
name: displayName,
defaultValue: "{{name}} channel",
});
@@ -168,7 +187,7 @@ export function ChannelSetupPanel({
<ChannelLogo feature={feature} showBrandLogos={showBrandLogos} />
<div className="min-w-0 flex-1">
<h3 className="truncate text-[18px] font-semibold leading-6 text-foreground">
{channelDisplayName(feature)}
{displayName}
</h3>
<p className="mt-1 text-[13px] leading-5 text-muted-foreground">
{channelDescription(feature, t)}
@@ -193,7 +212,9 @@ export function ChannelSetupPanel({
</div>
</div>
<div className="flex shrink-0 items-center gap-2 pt-1">
<ChannelStatusBadge>{channelStatusLabel(feature, tx)}</ChannelStatusBadge>
<ChannelStatusBadge status={feature.runtime_status}>
{channelStatusLabel(feature, tx)}
</ChannelStatusBadge>
{channelBusy ? (
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" aria-hidden />
) : null}
@@ -204,7 +225,7 @@ export function ChannelSetupPanel({
label={channelChecked ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")}
onChange={(checked) => {
if (
feature.name === "weixin"
uiContribution?.canConnectBeforeConfigured
&& checked
&& !channelChecked
&& feature.configured === false
@@ -218,12 +239,15 @@ export function ChannelSetupPanel({
</div>
</div>
<ChannelRuntimeError message={feature.runtime_error} className="mt-4" />
<ChannelSetupSurface
token={token}
feature={feature}
setup={setup}
chatAppsDocsUrl={chatAppsDocsUrl}
connectRequestId={connectRequestId}
ConnectFlow={uiContribution?.ConnectFlow}
onFeaturesUpdate={onFeaturesUpdate}
/>
</aside>
@@ -236,6 +260,7 @@ function ChannelSetupSurface({
setup,
chatAppsDocsUrl,
connectRequestId,
ConnectFlow,
onFeaturesUpdate,
}: {
token: string;
@@ -243,6 +268,7 @@ function ChannelSetupSurface({
setup: ChannelSetupPresentation;
chatAppsDocsUrl?: string;
connectRequestId: number;
ConnectFlow?: ComponentType<ChannelPluginConnectFlowProps>;
onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void;
}) {
const { t } = useTranslation();
@@ -268,14 +294,10 @@ function ChannelSetupSurface({
const editableFields = mode === "credentials" ? fields : mode === "connect" ? manualFields : [];
const hasAdvanced = advancedFields.length > 0;
const requirements = channelRequirements(feature, t);
const summary = t(`settings.channels.items.${feature.name}.setup.summary`, {
defaultValue:
setup.summary ??
tx(
"settings.channels.setupSummary",
"Enable only turns on nanobot support. Add the platform credentials, then restart nanobot.",
),
});
const summary = setup.summary ?? tx(
"settings.channels.setupSummary",
"Enable only turns on nanobot support. Add the platform credentials, then restart nanobot.",
);
const [fieldValues, setFieldValues] = useState<Record<string, string>>(() =>
defaultChannelFieldValues(editableFields, feature.config_values),
);
@@ -370,12 +392,18 @@ function ChannelSetupSurface({
}
};
const primaryActionLabel = feature.enabled
const primaryActionLabel = channelToggleChecked(feature)
? tx("settings.channels.checkConnection", "Check connection")
: tx("settings.channels.checkAndEnable", "Check and enable");
return (
<div className="mt-5 overflow-hidden rounded-[16px] border border-border/70 bg-background shadow-none">
<form
className="mt-5 overflow-hidden rounded-[16px] border border-border/70 bg-background shadow-none"
onSubmit={(event) => {
event.preventDefault();
if (mode === "credentials") void saveCredentialSettings();
}}
>
<section className="px-4 py-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-[13px] font-semibold text-foreground">
@@ -404,18 +432,11 @@ function ChannelSetupSurface({
<ChannelSetupLinks feature={feature} setup={setup} chatAppsDocsUrl={chatAppsDocsUrl} />
<ChannelSetupActions feature={feature} setup={setup} onNotice={setNotice} />
{mode === "connect" && feature.name === "feishu" ? (
<FeishuConnectFlow
{mode === "connect" && ConnectFlow ? (
<ConnectFlow
token={token}
connectRequestId={connectRequestId}
onFeaturesUpdate={onFeaturesUpdate}
/>
) : mode === "connect" && feature.name === "weixin" ? (
<WeixinConnectFlow
token={token}
idleLabel={t(`settings.channels.items.${feature.name}.setup.primaryAction`, {
defaultValue: setup.primaryActionLabel ?? tx("settings.channels.connect", "Connect"),
})}
feature={feature}
idleLabel={setup.primaryActionLabel ?? tx("settings.channels.connect", "Connect")}
connectRequestId={connectRequestId}
onFeaturesUpdate={onFeaturesUpdate}
/>
@@ -436,9 +457,7 @@ function ChannelSetupSurface({
)
}
>
{t(`settings.channels.items.${feature.name}.setup.primaryAction`, {
defaultValue: setup.primaryActionLabel ?? tx("settings.channels.connect", "Connect"),
})}
{setup.primaryActionLabel ?? tx("settings.channels.connect", "Connect")}
</Button>
{setup.command ? (
<Button
@@ -463,7 +482,6 @@ function ChannelSetupSurface({
<>
{setup.presets?.length ? (
<ChannelProviderPresets
featureName={feature.name}
presets={setup.presets}
onApply={applyPreset}
/>
@@ -480,11 +498,10 @@ function ChannelSetupSurface({
) : null}
<div className="mt-3 flex flex-wrap justify-end gap-2">
<Button
type="button"
type="submit"
size="sm"
variant="outline"
className="h-8 rounded-full border-border/65 bg-background/80 px-3 text-[12px] font-semibold hover:bg-muted/70"
onClick={() => void saveCredentialSettings()}
disabled={saving}
>
{saving || validating ? (
@@ -519,7 +536,7 @@ function ChannelSetupSurface({
) : null}
{setup.steps.length ? (
<ChannelSetupSteps featureName={feature.name} steps={setup.steps} tryIt={setup.tryIt} />
<ChannelSetupSteps steps={setup.steps} tryIt={setup.tryIt} />
) : null}
{validation?.checks.length ? <ChannelValidationChecks validation={validation} /> : null}
@@ -547,6 +564,6 @@ function ChannelSetupSurface({
) : null}
</details>
) : null}
</div>
</form>
);
}