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;
};