import { useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { getWechatCursorExecutorConfig, getWechatCursorExecutorRuntime, listAdminUsers, patchWechatCursorExecutorConfig, } from '../../api/client'; import type { AdminUserRow, WechatCursorExecutorAdminConfig, WechatCursorExecutorRuntimeState, } from '../../types'; import { formatTime } from '../utils/format'; type CursorChannelForm = { enabled: boolean; h5Enabled: boolean; wechatEnabled: boolean; selectedUserIds: string[]; manualAllowlistEntries: string[]; intentAllowlist: string; fallbackToDeepseek: boolean; }; 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; function userMatchesAllowlistToken(user: AdminUserRow, token: string) { const normalized = token.trim().toLowerCase(); if (!normalized) return false; const identities = [user.id, user.username, user.slug, user.publishSlug, user.displayName] .map((value) => String(value ?? '').trim().toLowerCase()) .filter(Boolean); return identities.includes(normalized); } function resolveAllowlistTokens(tokens: string[], users: AdminUserRow[]) { const selectedUserIds: string[] = []; const manualAllowlistEntries: string[] = []; const seenIds = new Set(); for (const rawToken of tokens) { const token = String(rawToken ?? '').trim(); if (!token) continue; const matchedUser = users.find((user) => userMatchesAllowlistToken(user, token)); if (matchedUser) { if (!seenIds.has(matchedUser.id)) { seenIds.add(matchedUser.id); selectedUserIds.push(matchedUser.id); } continue; } if (USER_ID_PATTERN.test(token)) { if (!seenIds.has(token)) { seenIds.add(token); selectedUserIds.push(token); } continue; } manualAllowlistEntries.push(token); } return { selectedUserIds, manualAllowlistEntries }; } function configToForm( config: WechatCursorExecutorAdminConfig | undefined, users: AdminUserRow[] = [], ): CursorChannelForm { const tokens = Array.isArray(config?.userAllowlist) ? config.userAllowlist : []; const channels = Array.isArray(config?.channelAllowlist) ? config.channelAllowlist : ['h5', 'wechat_mp']; const { selectedUserIds, manualAllowlistEntries } = resolveAllowlistTokens(tokens, users); return { enabled: Boolean(config?.enabled), h5Enabled: channels.includes('h5'), wechatEnabled: channels.includes('wechat_mp'), selectedUserIds, manualAllowlistEntries, intentAllowlist: Array.isArray(config?.intentAllowlist) ? config.intentAllowlist.join('\n') : 'page.generate', fallbackToDeepseek: config?.fallbackToDeepseek !== false, }; } function formToAllowlist(form: CursorChannelForm) { const manual = form.manualAllowlistEntries.map((item) => item.trim()).filter(Boolean); return [...new Set([...form.selectedUserIds, ...manual])]; } function formToChannelAllowlist(form: CursorChannelForm) { const channels: string[] = []; if (form.h5Enabled) channels.push('h5'); if (form.wechatEnabled) channels.push('wechat_mp'); return channels; } function CursorAllowlistPickerModal({ open, users, selectedUserIds, busy, onClose, onConfirm, }: { open: boolean; users: AdminUserRow[]; selectedUserIds: string[]; busy?: boolean; onClose: () => void; onConfirm: (nextIds: string[]) => void; }) { const [search, setSearch] = useState(''); const [draftIds, setDraftIds] = useState(selectedUserIds); const [pickerUsers, setPickerUsers] = useState(users); const [loadingUsers, setLoadingUsers] = useState(false); useEffect(() => { if (!open) return; setDraftIds(selectedUserIds); setSearch(''); setPickerUsers(users); }, [open, selectedUserIds, users]); useEffect(() => { if (!open) return; const timer = window.setTimeout(() => { void (async () => { setLoadingUsers(true); try { const result = await listAdminUsers({ page: 1, pageSize: 500, status: 'active', search: search.trim() || undefined, }); setPickerUsers(result.items); } catch { setPickerUsers(users); } finally { setLoadingUsers(false); } })(); }, search.trim() ? 250 : 0); return () => window.clearTimeout(timer); }, [open, search, users]); if (!open) return null; const visibleIds = pickerUsers.map((user) => user.id); const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => draftIds.includes(id)); return (
{ if (event.target === event.currentTarget) onClose(); }} >
event.stopPropagation()} >

选择体验通道用户

仅勾选用户会进入 TKMind 智趣独立通道;其余用户完全不受影响,仍走原有 DeepSeek/Goose 链路。

setSearch(event.target.value)} placeholder="搜索用户名、昵称或 userId" style={{ flex: 1, minWidth: 220 }} autoFocus />
{loadingUsers ?

加载用户…

: null} {!loadingUsers && pickerUsers.length === 0 ? (

没有匹配的用户

) : null} {pickerUsers.map((user) => ( ))}
); } export function CursorChannelPage() { const [users, setUsers] = useState([]); const [form, setForm] = useState(null); const [savedForm, setSavedForm] = useState(null); const [runtime, setRuntime] = useState(null); const [pickerOpen, setPickerOpen] = useState(false); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const dirty = useMemo(() => { if (!form || !savedForm) return false; return JSON.stringify(form) !== JSON.stringify(savedForm); }, [form, savedForm]); const selectedUsers = useMemo(() => { if (!form) return []; const byId = new Map(users.map((user) => [user.id, user])); return form.selectedUserIds.map((userId) => ({ userId, user: byId.get(userId) ?? null, })); }, [form, users]); const load = useCallback(async () => { setLoading(true); setError(null); try { const [nextUsers, nextConfig, nextRuntime] = await Promise.all([ listAdminUsers({ page: 1, pageSize: 200, status: 'active' }), getWechatCursorExecutorConfig().catch(() => null), getWechatCursorExecutorRuntime().catch(() => null), ]); setUsers(nextUsers.items); const nextForm = configToForm(nextConfig?.config, nextUsers.items); setForm(nextForm); setSavedForm(nextForm); setRuntime(nextRuntime); } catch (err) { setError(err instanceof Error ? err.message : '加载失败'); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const handleSave = async () => { if (!form) return; setSaving(true); setError(null); setNotice(null); try { const channelAllowlist = formToChannelAllowlist(form); if (form.enabled && channelAllowlist.length === 0) { throw new Error('启用体验通道时,至少选择一个接入渠道(H5 或服务号)'); } if (form.enabled && formToAllowlist(form).length === 0) { throw new Error('启用体验通道时,至少选择一名白名单用户'); } const result = await patchWechatCursorExecutorConfig({ enabled: form.enabled, userAllowlist: formToAllowlist(form), channelAllowlist, intentAllowlist: form.intentAllowlist .split(/[\s,]+/u) .map((item) => item.trim()) .filter(Boolean), fallbackToDeepseek: form.fallbackToDeepseek, }); const nextForm = configToForm(result.config, users); setForm(nextForm); setSavedForm(nextForm); setRuntime(await getWechatCursorExecutorRuntime().catch(() => null)); setNotice('TKMind 智趣体验通道已保存。未在白名单内的用户不受影响。'); } catch (err) { setError(err instanceof Error ? err.message : '保存失败'); } finally { setSaving(false); } }; return (

TKMind 智趣体验通道

独立于现有 DeepSeek/Goose 链路的 Cursor 执行通道。仅对白名单用户生效,支持 H5 与服务号;其余用户完全不受影响。

{error &&

{error}

} {notice &&

{notice}

}

通道说明

  • H5:白名单用户在页面生成、问卷(Page Data)、Excel 分析等任务时,走 Cursor 独立执行。
  • 服务号:白名单用户在指定意图(默认 page.generate)下走 Cursor;其余意图仍走原有链路。
  • 非白名单用户:H5 与服务号均保持原有 DeepSeek/Goose 行为,不会被 Cursor 路由影响。

服务端需开启 MEMIND_CURSOR_EXECUTOR_ENABLED=1 与 Tool Gateway;详见 Memind{' '} .env.example

{form ? (

通道配置

默认关闭。开启后仅白名单用户进入智趣通道,不影响其他用户。

接入渠道