Add per-feature Cursor channel toggles for admin and runtime.
Memind CI / Test, build, and release guards (push) Has been cancelled

Expose page/data/scheduled-task/chat-bridge switches in the智趣体验通道 config so Tang can roll out Cursor paths independently with DeepSeek fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-31 09:40:52 +08:00
parent ac2c87be43
commit 617ff0d1dd
21 changed files with 915 additions and 127 deletions
+58 -5
View File
@@ -1,3 +1,12 @@
import {
CURSOR_CHANNEL_FEATURES,
defaultCursorChannelFeatures,
intentAllowlistFromFeatures,
isCursorFeatureEnabled,
isCursorTaskKindEnabled,
normalizeCursorChannelFeatures,
} from './cursor-channel-features.mjs';
const CONFIG_TABLE = 'h5_wechat_cursor_executor_config';
const CONFIG_SCOPE = 'global';
const POLICY_SOURCE_DEFAULT = 'default';
@@ -6,17 +15,21 @@ const POLICY_SOURCE_ADMIN_DB = 'admin-db';
const DEFAULT_INTENT_ALLOWLIST = Object.freeze(['page.generate']);
const DEFAULT_CHANNEL_ALLOWLIST = Object.freeze(['h5', 'wechat_mp']);
export { CURSOR_CHANNEL_FEATURES, isCursorFeatureEnabled, isCursorTaskKindEnabled };
export const CURSOR_EXECUTOR_CHANNEL = Object.freeze({
H5: 'h5',
WECHAT_MP: 'wechat_mp',
});
function defaultConfigShape() {
const features = defaultCursorChannelFeatures();
return {
enabled: false,
userAllowlist: [],
channelAllowlist: [...DEFAULT_CHANNEL_ALLOWLIST],
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
intentAllowlist: intentAllowlistFromFeatures(features),
features,
fallbackToDeepseek: true,
meta: {
notes: '',
@@ -61,6 +74,25 @@ function parseJsonLike(value, fallback) {
return fallback;
}
function mergeFeaturePatch(currentFeatures, patchFeatures) {
const base = normalizeCursorChannelFeatures(currentFeatures, {
intentAllowlist: DEFAULT_INTENT_ALLOWLIST,
});
if (!patchFeatures || typeof patchFeatures !== 'object') return base;
const next = { ...base };
for (const featureKey of Object.values(CURSOR_CHANNEL_FEATURES)) {
if (!(featureKey in patchFeatures)) continue;
const raw = patchFeatures[featureKey];
next[featureKey] = {
enabled: normalizeBoolean(
typeof raw === 'object' && raw !== null ? raw.enabled : raw,
base[featureKey]?.enabled ?? false,
),
};
}
return next;
}
function mergePatch(currentConfig, patch = {}) {
const next = cloneConfig(currentConfig);
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
@@ -69,9 +101,17 @@ function mergePatch(currentConfig, patch = {}) {
const channels = normalizeStringList(patch.channelAllowlist);
next.channelAllowlist = channels.length ? channels : [...DEFAULT_CHANNEL_ALLOWLIST];
}
if ('features' in patch) {
next.features = mergeFeaturePatch(next.features, patch.features);
}
if ('intentAllowlist' in patch) {
const intents = normalizeStringList(patch.intentAllowlist);
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
next.features = normalizeCursorChannelFeatures(next.features, {
intentAllowlist: next.intentAllowlist,
});
} else if ('features' in patch) {
next.intentAllowlist = intentAllowlistFromFeatures(next.features);
}
if ('fallbackToDeepseek' in patch) {
next.fallbackToDeepseek = normalizeBoolean(patch.fallbackToDeepseek, true);
@@ -79,18 +119,27 @@ function mergePatch(currentConfig, patch = {}) {
if (patch?.meta && typeof patch.meta.notes === 'string') {
next.meta.notes = patch.meta.notes;
}
if (!next.features) {
next.features = normalizeCursorChannelFeatures(null, {
intentAllowlist: next.intentAllowlist,
});
}
return next;
}
function flattenPolicy(config, source) {
const intentAllowlist = normalizeStringList(config.intentAllowlist);
const channelAllowlist = normalizeStringList(config.channelAllowlist);
const features = normalizeCursorChannelFeatures(config.features, {
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
});
return {
source,
enabled: Boolean(config.enabled),
userAllowlist: normalizeStringList(config.userAllowlist),
channelAllowlist: channelAllowlist.length ? channelAllowlist : [...DEFAULT_CHANNEL_ALLOWLIST],
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
intentAllowlist: intentAllowlistFromFeatures(features),
features,
fallbackToDeepseek: config.fallbackToDeepseek !== false,
};
}
@@ -123,11 +172,14 @@ export function isUserAllowedByWechatCursorPolicy(user, policy) {
}
export function isIntentAllowedByWechatCursorPolicy(intentKind, policy) {
const normalized = String(intentKind ?? '').trim();
if (!normalized) return false;
if (normalized === 'page.generate') {
return isCursorFeatureEnabled(policy, CURSOR_CHANNEL_FEATURES.PAGE_GENERATE);
}
const allowlist = (policy?.intentAllowlist ?? DEFAULT_INTENT_ALLOWLIST)
.map((item) => String(item ?? '').trim())
.filter(Boolean);
const normalized = String(intentKind ?? '').trim();
if (!normalized) return false;
return allowlist.includes(normalized);
}
@@ -154,8 +206,9 @@ async function loadStoredState(pool) {
const row = rows[0];
if (!row) return null;
const parsed = parseJsonLike(row.config_json, {});
const merged = mergePatch(defaultConfigShape(), parsed);
return {
config: mergePatch(defaultConfigShape(), parsed),
config: merged,
updatedAt: Number(row.updated_at ?? 0) || null,
updatedBy: row.updated_by ?? null,
};