mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 08:17:09 +00:00
fix(channels): honor paired DM policy before publish routing (#1314)
* fix(channels): honor paired DM policy before publish * fix(channels): clear advanced allowlist updates --------- Co-authored-by: ntduc <ntduc@cpp.ai.vn>
This commit is contained in:
1 parent
c02fb660bc
commit
d9a15be86c
3 files changed
+108
-21
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import { flattenConfig, unflattenConfig } from "@/lib/config-flatten";
|
||||
import { normalizeReasoningDeliveryConfig } from "../reasoning-delivery-config";
|
||||
|
||||
export const ESSENTIAL_CONFIG_KEYS = new Set(["dm_policy", "group_policy", "require_mention", "mention_mode"]);
|
||||
|
||||
function isAdvancedConfigKey(key: string): boolean {
|
||||
return !ESSENTIAL_CONFIG_KEYS.has(key) && key !== "groups" && !key.startsWith("groups.");
|
||||
}
|
||||
|
||||
export function deriveAdvancedInitialValues(config: Record<string, unknown> | null | undefined): Record<string, unknown> {
|
||||
const normalized = normalizeReasoningDeliveryConfig({ ...((config ?? {}) as Record<string, unknown>) });
|
||||
const flat = flattenConfig(normalized);
|
||||
// Only keep advanced keys (exclude essential + group overrides).
|
||||
return Object.fromEntries(
|
||||
Object.entries(flat).filter(([k]) => isAdvancedConfigKey(k)),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildAdvancedConfigUpdate(
|
||||
existingConfig: Record<string, unknown> | null | undefined,
|
||||
values: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const normalized = normalizeReasoningDeliveryConfig({ ...((existingConfig ?? {}) as Record<string, unknown>) });
|
||||
const flat = flattenConfig(normalized);
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (!isAdvancedConfigKey(key)) continue;
|
||||
if (value === undefined || value === "" || value === null) {
|
||||
delete flat[key];
|
||||
continue;
|
||||
}
|
||||
flat[key] = value;
|
||||
}
|
||||
|
||||
return normalizeReasoningDeliveryConfig(unflattenConfig(flat));
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAdvancedConfigUpdate, deriveAdvancedInitialValues } from "./channel-advanced-config";
|
||||
|
||||
describe("channel advanced config payload", () => {
|
||||
it("removes a stale allow_from entry when the allowed users picker is cleared", () => {
|
||||
const existing = {
|
||||
dm_policy: "pairing",
|
||||
group_policy: "open",
|
||||
allow_from: ["195835936795841454"],
|
||||
groups: {
|
||||
"-100123": { allow_from: ["group-user"] },
|
||||
},
|
||||
};
|
||||
|
||||
const values = deriveAdvancedInitialValues(existing);
|
||||
const next = buildAdvancedConfigUpdate(existing, { ...values, allow_from: undefined });
|
||||
|
||||
expect(next).not.toHaveProperty("allow_from");
|
||||
expect(next.dm_policy).toBe("pairing");
|
||||
expect(next.group_policy).toBe("open");
|
||||
expect(next.groups).toEqual(existing.groups);
|
||||
});
|
||||
|
||||
it("persists an explicit empty allow_from array when a tags field emits one", () => {
|
||||
const existing = {
|
||||
allow_from: ["195835936795841454"],
|
||||
proxy: "http://localhost:8080",
|
||||
};
|
||||
|
||||
const next = buildAdvancedConfigUpdate(existing, {
|
||||
allow_from: [],
|
||||
proxy: "",
|
||||
});
|
||||
|
||||
expect(next.allow_from).toEqual([]);
|
||||
expect(next).not.toHaveProperty("proxy");
|
||||
});
|
||||
|
||||
it("flattens nested delivery config for editing and writes it back nested", () => {
|
||||
const existing = {
|
||||
chat_behavior: {
|
||||
final_split: {
|
||||
enabled: "inherit",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const values = deriveAdvancedInitialValues(existing);
|
||||
expect(values["chat_behavior.final_split.enabled"]).toBe("inherit");
|
||||
|
||||
const next = buildAdvancedConfigUpdate(existing, {
|
||||
...values,
|
||||
"chat_behavior.final_split.enabled": "off",
|
||||
});
|
||||
|
||||
expect(next).toEqual({
|
||||
chat_behavior: {
|
||||
final_split: {
|
||||
enabled: "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
import { ConfigGroupHeader } from "@/components/shared/config-group-header";
|
||||
import { ChannelFields } from "../channel-fields";
|
||||
import { configSchema } from "../channel-schemas";
|
||||
import { normalizeReasoningDeliveryConfig } from "../reasoning-delivery-config";
|
||||
import {
|
||||
buildAdvancedConfigUpdate,
|
||||
deriveAdvancedInitialValues,
|
||||
ESSENTIAL_CONFIG_KEYS,
|
||||
} from "./channel-advanced-config";
|
||||
import type { ChannelInstanceData } from "@/types/channel";
|
||||
|
||||
interface ChannelAdvancedDialogProps {
|
||||
@@ -21,8 +25,6 @@ interface ChannelAdvancedDialogProps {
|
||||
onUpdate: (updates: Record<string, unknown>) => Promise<void>;
|
||||
}
|
||||
|
||||
const ESSENTIAL_CONFIG_KEYS = new Set(["dm_policy", "group_policy", "require_mention", "mention_mode"]);
|
||||
|
||||
const NETWORK_KEYS = new Set(["api_server", "proxy", "domain", "connection_mode", "webhook_port", "webhook_path", "webhook_url"]);
|
||||
const LIMITS_KEYS = new Set(["history_limit", "media_max_mb", "text_chunk_limit"]);
|
||||
const STREAMING_KEYS = new Set(["dm_stream", "group_stream", "draft_transport", "reasoning_delivery", "native_stream", "debounce_delay", "thread_ttl"]);
|
||||
@@ -41,14 +43,6 @@ function getAdvancedFields(channelType: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function deriveInitialValues(instance: ChannelInstanceData): Record<string, unknown> {
|
||||
const config = normalizeReasoningDeliveryConfig((instance.config ?? {}) as Record<string, unknown>);
|
||||
// Only keep advanced keys (exclude essential + groups)
|
||||
return Object.fromEntries(
|
||||
Object.entries(config).filter(([k]) => !ESSENTIAL_CONFIG_KEYS.has(k) && k !== "groups"),
|
||||
);
|
||||
}
|
||||
|
||||
export function ChannelAdvancedDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -58,14 +52,13 @@ export function ChannelAdvancedDialog({
|
||||
const { t } = useTranslation("channels");
|
||||
const groups = getAdvancedFields(instance.channel_type);
|
||||
|
||||
const [values, setValues] = useState<Record<string, unknown>>(() => deriveInitialValues(instance));
|
||||
const [values, setValues] = useState<Record<string, unknown>>(() => deriveAdvancedInitialValues(instance.config));
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Re-sync local state when dialog opens
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setValues(deriveInitialValues(instance));
|
||||
|
||||
setValues(deriveAdvancedInitialValues(instance.config));
|
||||
}, [open, instance]);
|
||||
|
||||
const handleChange = useCallback((key: string, value: unknown) => {
|
||||
@@ -75,13 +68,7 @@ export function ChannelAdvancedDialog({
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const existingConfig = (instance.config ?? {}) as Record<string, unknown>;
|
||||
const cleanAdvanced = Object.fromEntries(
|
||||
Object.entries(values).filter(([, v]) => v !== undefined && v !== "" && v !== null),
|
||||
);
|
||||
// Merge: preserve essential keys and groups from existing, overwrite advanced keys
|
||||
const merged = normalizeReasoningDeliveryConfig({ ...existingConfig, ...cleanAdvanced });
|
||||
await onUpdate({ config: merged });
|
||||
await onUpdate({ config: buildAdvancedConfigUpdate(instance.config, values) });
|
||||
onOpenChange(false);
|
||||
} catch { // toast shown by hook
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user