feat: allow canvas agent rollout settings

This commit is contained in:
134 2026-06-23 21:43:49 +08:00
parent 020a74a6a8
commit fae9d32ffb
5 changed files with 142 additions and 6 deletions

View File

@ -58,9 +58,9 @@ import {
} from "@/lib/canvas/agent-context";
import {
canApplyCanvasAgentMutations,
getCanvasAgentFeatureFlags,
shouldStreamCanvasAgentShadowOperations,
} from "@/lib/canvas/agent-feature-flags";
import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings";
import {
buildPendingCanvasOperation,
extractPendingCanvasOperationFromWorkingState,
@ -906,7 +906,7 @@ export async function POST(request: Request) {
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
contextPayload || undefined
);
const canvasAgentFeatures = getCanvasAgentFeatureFlags();
const canvasAgentFeatures = await getCanvasAgentFeatureFlagsFromSystemSettings();
const canApplyCanvasAgentMutation =
canApplyCanvasAgentMutations(canvasAgentFeatures);
const currentMessageAttachmentCount = message.parts.filter(

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { getCanvasAgentFeatureFlags } from "@/lib/canvas/agent-feature-flags";
import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings";
import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations";
import { handleCanvasAgentOperationRouteRequest } from "@/lib/canvas/agent-operation-route";
@ -14,7 +14,7 @@ export async function POST(
userId: session?.user?.id,
projectId: id,
readBody: () => request.json(),
features: getCanvasAgentFeatureFlags(),
features: await getCanvasAgentFeatureFlagsFromSystemSettings(),
applyOperation: applyCanvasOperationPlan,
logger: console,
});

View File

@ -11,7 +11,29 @@ export type CanvasAgentFeatureFlags = {
annotationEditEnabled: boolean;
};
export const CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY =
"canvas_agent_operation_protocol_enabled";
export const CANVAS_AGENT_WRITEBACK_MODE_KEY = "canvas_agent_writeback_mode";
export const CANVAS_AGENT_SLOT_ENABLED_KEY = "canvas_agent_slot_enabled";
export const CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY =
"canvas_agent_view_operation_enabled";
export const CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY =
"canvas_agent_annotation_edit_enabled";
export const CANVAS_AGENT_FEATURE_SETTING_KEYS = [
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY,
CANVAS_AGENT_WRITEBACK_MODE_KEY,
CANVAS_AGENT_SLOT_ENABLED_KEY,
CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY,
CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY,
] as const;
type EnvLike = Record<string, string | undefined>;
export type CanvasAgentFeatureSettingKey =
(typeof CANVAS_AGENT_FEATURE_SETTING_KEYS)[number];
export type CanvasAgentFeatureSettingsLike = Partial<
Record<CanvasAgentFeatureSettingKey, unknown>
>;
const WRITEBACK_MODES = new Set<CanvasAgentWritebackMode>([
"legacy_canvas_media",
@ -23,6 +45,8 @@ export function normalizeCanvasAgentBoolean(
value: unknown,
fallback: boolean
) {
if (typeof value === "boolean") return value;
if (typeof value === "number" && Number.isFinite(value)) return value !== 0;
if (typeof value !== "string") return fallback;
const normalized = value.trim().toLowerCase();
if (!normalized) return fallback;
@ -41,10 +65,41 @@ export function normalizeCanvasAgentWritebackMode(
: fallback;
}
export function getCanvasAgentFeatureFlags(
env: EnvLike = process.env
export function mergeCanvasAgentFeatureFlagsWithSettings(
flags: CanvasAgentFeatureFlags,
settings?: CanvasAgentFeatureSettingsLike | null
): CanvasAgentFeatureFlags {
if (!settings) return flags;
return {
operationProtocolEnabled: normalizeCanvasAgentBoolean(
settings[CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY],
flags.operationProtocolEnabled
),
writebackMode: normalizeCanvasAgentWritebackMode(
settings[CANVAS_AGENT_WRITEBACK_MODE_KEY],
flags.writebackMode
),
slotEnabled: normalizeCanvasAgentBoolean(
settings[CANVAS_AGENT_SLOT_ENABLED_KEY],
flags.slotEnabled
),
viewOperationEnabled: normalizeCanvasAgentBoolean(
settings[CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY],
flags.viewOperationEnabled
),
annotationEditEnabled: normalizeCanvasAgentBoolean(
settings[CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY],
flags.annotationEditEnabled
),
};
}
export function getCanvasAgentFeatureFlags(
env: EnvLike = process.env,
settings?: CanvasAgentFeatureSettingsLike | null
): CanvasAgentFeatureFlags {
const envFlags: CanvasAgentFeatureFlags = {
operationProtocolEnabled: normalizeCanvasAgentBoolean(
env.CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED,
true
@ -66,6 +121,7 @@ export function getCanvasAgentFeatureFlags(
true
),
};
return mergeCanvasAgentFeatureFlagsWithSettings(envFlags, settings);
}
export function canApplyCanvasAgentMutations(flags: CanvasAgentFeatureFlags) {

View File

@ -0,0 +1,44 @@
import { inArray } from "drizzle-orm";
import { db } from "@/lib/db";
import { systemSettings } from "@/lib/db/schema";
import {
CANVAS_AGENT_FEATURE_SETTING_KEYS,
type CanvasAgentFeatureFlags,
type CanvasAgentFeatureSettingsLike,
getCanvasAgentFeatureFlags,
} from "@/lib/canvas/agent-feature-flags";
type EnvLike = Record<string, string | undefined>;
export async function readCanvasAgentFeatureSettings(): Promise<CanvasAgentFeatureSettingsLike> {
const rows = await db
.select({ key: systemSettings.key, value: systemSettings.value })
.from(systemSettings)
.where(inArray(systemSettings.key, [...CANVAS_AGENT_FEATURE_SETTING_KEYS]));
const settings: CanvasAgentFeatureSettingsLike = {};
rows.forEach((row) => {
if (
CANVAS_AGENT_FEATURE_SETTING_KEYS.includes(
row.key as (typeof CANVAS_AGENT_FEATURE_SETTING_KEYS)[number]
)
) {
settings[row.key as keyof CanvasAgentFeatureSettingsLike] = row.value;
}
});
return settings;
}
export async function getCanvasAgentFeatureFlagsFromSystemSettings(
env: EnvLike = process.env
): Promise<CanvasAgentFeatureFlags> {
try {
return getCanvasAgentFeatureFlags(env, await readCanvasAgentFeatureSettings());
} catch (error) {
console.warn(
"[CanvasAgentFeatureFlags] Failed to read SystemSettings, using env fallback:",
error
);
return getCanvasAgentFeatureFlags(env);
}
}

View File

@ -129,6 +129,11 @@ import {
findCanvasAgentOperationAudit,
} from "@/lib/canvas/agent-operation-audit";
import {
CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY,
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY,
CANVAS_AGENT_SLOT_ENABLED_KEY,
CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY,
CANVAS_AGENT_WRITEBACK_MODE_KEY,
canApplyCanvasAgentMutations,
getCanvasAgentFeatureFlags,
normalizeCanvasAgentBoolean,
@ -1805,6 +1810,37 @@ test.describe("canvas chat pure helpers", () => {
});
expect(canApplyCanvasAgentMutations(disabledProtocol)).toBe(false);
expect(shouldStreamCanvasAgentShadowOperations(disabledProtocol)).toBe(false);
const settingsOverride = getCanvasAgentFeatureFlags(
{
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED: "disabled",
CANVAS_AGENT_WRITEBACK_MODE: "legacy_canvas_media",
CANVAS_AGENT_SLOT_ENABLED: "0",
},
{
[CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY]: true,
[CANVAS_AGENT_WRITEBACK_MODE_KEY]: "operation_authoritative",
[CANVAS_AGENT_SLOT_ENABLED_KEY]: true,
[CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY]: false,
[CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY]: "off",
}
);
expect(settingsOverride).toMatchObject({
operationProtocolEnabled: true,
writebackMode: "operation_authoritative",
slotEnabled: true,
viewOperationEnabled: false,
annotationEditEnabled: false,
});
expect(canApplyCanvasAgentMutations(settingsOverride)).toBe(true);
const invalidSettingsFallback = getCanvasAgentFeatureFlags(
{ CANVAS_AGENT_WRITEBACK_MODE: "operation_shadow" },
{
[CANVAS_AGENT_WRITEBACK_MODE_KEY]: "not-a-mode",
}
);
expect(invalidSettingsFallback.writebackMode).toBe("operation_shadow");
});
test("builds idempotent canvas operation audit results", () => {