Add independent Cursor channel feature toggles in admin UI.

Replace free-text intent allowlist with per-capability switches aligned with Memind runtime policy schema.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-31 09:40:53 +08:00
parent 86c7c5fb08
commit c1aa1ba459
2 changed files with 123 additions and 25 deletions
+109 -25
View File
@@ -8,6 +8,8 @@ import {
} from '../../api/client';
import type {
AdminUserRow,
CursorChannelFeatureKey,
CursorChannelFeatures,
WechatCursorExecutorAdminConfig,
WechatCursorExecutorRuntimeState,
} from '../../types';
@@ -19,10 +21,70 @@ type CursorChannelForm = {
wechatEnabled: boolean;
selectedUserIds: string[];
manualAllowlistEntries: string[];
intentAllowlist: string;
features: CursorChannelFeatures;
fallbackToDeepseek: boolean;
};
const FEATURE_OPTIONS: Array<{
key: CursorChannelFeatureKey;
label: string;
description: string;
}> = [
{
key: 'pageGenerate',
label: '页面生成',
description: 'H5 / 服务号 page.generate 与页面落盘任务走 Cursor。',
},
{
key: 'pageData',
label: '问卷 Page Data',
description: 'H5 问卷收集、PG 持久化与相关分析页走 Cursor。',
},
{
key: 'excelAnalysis',
label: 'Excel 分析',
description: 'H5 表格分析与结果页走 Cursor。',
},
{
key: 'chatBridge',
label: '普通聊天(Chat Bridge)',
description: '白名单用户的 Goose 会话 LLM 走 Cursor Chat Bridge;需服务端 MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1。',
},
{
key: 'scheduledTasks',
label: '定时任务执行',
description: '到点自动任务(新闻/天气等)走 Cursor 而非 Goose Session。',
},
];
function defaultFeatures(): CursorChannelFeatures {
return {
pageGenerate: { enabled: true },
pageData: { enabled: false },
excelAnalysis: { enabled: false },
chatBridge: { enabled: false },
scheduledTasks: { enabled: false },
};
}
function normalizeFeatures(
config: WechatCursorExecutorAdminConfig | undefined,
): CursorChannelFeatures {
const base = defaultFeatures();
const raw = config?.features;
if (raw) {
for (const option of FEATURE_OPTIONS) {
base[option.key] = {
enabled: Boolean(raw[option.key]?.enabled),
};
}
return base;
}
const intents = Array.isArray(config?.intentAllowlist) ? config.intentAllowlist : ['page.generate'];
base.pageGenerate.enabled = intents.includes('page.generate');
return base;
}
const USER_ID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -77,9 +139,7 @@ function configToForm(
wechatEnabled: channels.includes('wechat_mp'),
selectedUserIds,
manualAllowlistEntries,
intentAllowlist: Array.isArray(config?.intentAllowlist)
? config.intentAllowlist.join('\n')
: 'page.generate',
features: normalizeFeatures(config),
fallbackToDeepseek: config?.fallbackToDeepseek !== false,
};
}
@@ -316,10 +376,7 @@ export function CursorChannelPage() {
enabled: form.enabled,
userAllowlist: formToAllowlist(form),
channelAllowlist,
intentAllowlist: form.intentAllowlist
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean),
features: form.features,
fallbackToDeepseek: form.fallbackToDeepseek,
});
const nextForm = configToForm(result.config, users);
@@ -350,12 +407,15 @@ export function CursorChannelPage() {
<h3>通道说明</h3>
<ul className="muted" style={{ margin: '8px 0 0', paddingLeft: 20, lineHeight: 1.7 }}>
<li>
<strong>H5</strong>:白名单用户在页面生成、问卷(Page Data)、Excel 分析等任务时,走 Cursor 独立执行。
<strong>总开关</strong>:开启后,仅白名单用户可进入下方已勾选的能力通道;其余用户完全不受影响。
</li>
<li>
<strong>服务号</strong>:白名单用户在指定意图(默认 page.generate)下走 Cursor;其余意图仍走原有链路。
<strong>H5 / 服务号渠道</strong>:可分别控制接入端;页面类能力主要作用于 H5,服务号目前以 page.generate 为主。
</li>
<li>非白名单用户:H5 与服务号均保持原有 DeepSeek/Goose 行为,不会被 Cursor 路由影响。</li>
<li>
<strong>独立能力开关</strong>:页面生成、问卷、Excel、普通聊天 Bridge、定时任务可分别启用;未勾选的能力仍走 DeepSeek/Goose。
</li>
<li>非白名单用户:所有通道保持原有 DeepSeek/Goose 行为。</li>
</ul>
<p className="muted" style={{ marginTop: 12 }}>
服务端需开启 <code>MEMIND_CURSOR_EXECUTOR_ENABLED=1</code> 与 Tool Gateway;详见 Memind{' '}
@@ -432,8 +492,39 @@ export function CursorChannelPage() {
)
}
/>{' '}
Cursor 失败时自动回退 DeepSeek
Cursor 失败时自动回退 DeepSeek / Goose
</label>
<fieldset style={{ border: 'none', padding: 0, margin: '12px 0' }}>
<legend className="muted" style={{ marginBottom: 8 }}>
能力通道(需总开关与白名单同时满足)
</legend>
{FEATURE_OPTIONS.map((option) => (
<label key={option.key} style={{ display: 'block', marginBottom: 10 }}>
<input
type="checkbox"
checked={form.features[option.key]?.enabled ?? false}
disabled={loading || saving || !form.enabled}
onChange={(event) =>
setForm((current) =>
current
? {
...current,
features: {
...current.features,
[option.key]: { enabled: event.target.checked },
},
}
: current,
)
}
/>{' '}
<strong>{option.label}</strong>
<span className="muted" style={{ display: 'block', marginLeft: 22, fontSize: 13 }}>
{option.description}
</span>
</label>
))}
</fieldset>
<label>
体验用户白名单
<div className="wechat-toolbar" style={{ marginTop: 8, marginBottom: 8 }}>
@@ -493,19 +584,6 @@ export function CursorChannelPage() {
</p>
)}
</label>
<label>
服务号启用意图(H5 不受此限制,默认 page.generate)
<textarea
value={form.intentAllowlist}
disabled={loading || saving}
rows={2}
placeholder="page.generate"
onChange={(event) =>
setForm((current) => current && { ...current, intentAllowlist: event.target.value })
}
/>
</label>
</div>
{runtime ? (
<p className="muted" style={{ marginTop: 12 }}>
@@ -515,10 +593,16 @@ export function CursorChannelPage() {
{' · '}
渠道 {(runtime.policy.channelAllowlist ?? []).join('、') || '无'}
{' · '}
能力{' '}
{FEATURE_OPTIONS.filter((item) => runtime.policy.features?.[item.key]?.enabled)
.map((item) => item.label)
.join('、') || '无'}
{' · '}
来源 {runtime.source}
{runtime.updatedAt ? ` · 更新 ${formatTime(runtime.updatedAt)}` : ''}
</p>
) : null}
</div>
</section>
) : null}
+14
View File
@@ -346,11 +346,24 @@ export type WechatIntentRouterRuntimeState = {
config: WechatIntentRouterAdminConfig;
};
export type CursorChannelFeatureKey =
| 'pageGenerate'
| 'pageData'
| 'excelAnalysis'
| 'chatBridge'
| 'scheduledTasks';
export type CursorChannelFeatures = Record<
CursorChannelFeatureKey,
{ enabled: boolean }
>;
export type WechatCursorExecutorAdminConfig = {
enabled: boolean;
userAllowlist: string[];
channelAllowlist: string[];
intentAllowlist: string[];
features?: CursorChannelFeatures;
fallbackToDeepseek: boolean;
meta?: {
notes?: string;
@@ -374,6 +387,7 @@ export type WechatCursorExecutorRuntimeState = {
userAllowlist: string[];
channelAllowlist: string[];
intentAllowlist: string[];
features?: CursorChannelFeatures;
fallbackToDeepseek: boolean;
source?: string;
};