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:
@@ -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}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user