Add dedicated admin page for TKMind Cursor experience channel (H5 + WeChat).
Centralize whitelist and channel toggles away from the WeChat page so Cursor stays an opt-in path for selected users only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -453,6 +453,23 @@ export function createAdminApp(services) {
|
||||
adminApi.put('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
|
||||
adminApi.patch('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
|
||||
|
||||
adminApi.get('/cursor-executor-channel/config', requireAdmin, async (_req, res) => {
|
||||
if (!wechatCursorExecutorPolicyService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'TKMind 智趣体验通道未启用' });
|
||||
}
|
||||
return res.json(await wechatCursorExecutorPolicyService.getAdminConfig());
|
||||
});
|
||||
|
||||
adminApi.get('/cursor-executor-channel/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!wechatCursorExecutorPolicyService?.getRuntimeState) {
|
||||
return res.status(503).json({ message: 'TKMind 智趣体验通道未启用' });
|
||||
}
|
||||
return res.json(await wechatCursorExecutorPolicyService.getRuntimeState());
|
||||
});
|
||||
|
||||
adminApi.put('/cursor-executor-channel/config', requireAdmin, updateWechatCursorExecutorConfig);
|
||||
adminApi.patch('/cursor-executor-channel/config', requireAdmin, updateWechatCursorExecutorConfig);
|
||||
|
||||
adminApi.get('/mindspace/config', requireAdmin, async (_req, res) => {
|
||||
if (!loadMindSpaceConfig) return res.status(503).json({ message: 'MindSpace 配置未启用' });
|
||||
const config = await loadMindSpaceConfig(pool);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TemplateCatalogPage } from './admin/pages/TemplateCatalogPage';
|
||||
import { UserDetailPage } from './admin/pages/UserDetailPage';
|
||||
import { UsersPage } from './admin/pages/UsersPage';
|
||||
import { WechatPage } from './admin/pages/WechatPage';
|
||||
import { CursorChannelPage } from './admin/pages/CursorChannelPage';
|
||||
import { AssetGatewayPage } from './admin/pages/AssetGatewayPage';
|
||||
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
||||
import { AnalyticsConfigPage } from './admin/pages/AnalyticsConfigPage';
|
||||
@@ -137,6 +138,7 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }
|
||||
<Route path="orchestrator" element={<OrchestratorPage />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="cursor-channel" element={<CursorChannelPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
||||
@@ -30,6 +30,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
{
|
||||
label: '平台配置',
|
||||
items: [
|
||||
{ to: '/cursor-channel', label: '智趣体验通道' },
|
||||
{ to: '/wechat', label: '服务号' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置' },
|
||||
{ to: '/analytics', label: 'Analytics 配置' },
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
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<string>();
|
||||
|
||||
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<string[]>(selectedUserIds);
|
||||
const [pickerUsers, setPickerUsers] = useState<AdminUserRow[]>(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 (
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-box"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cursor-allowlist-picker-title"
|
||||
style={{ maxWidth: 720, width: 'min(720px, calc(100vw - 32px))' }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="modal-head">
|
||||
<div>
|
||||
<h3 id="cursor-allowlist-picker-title">选择体验通道用户</h3>
|
||||
<p className="muted" style={{ margin: '6px 0 0', fontSize: 13 }}>
|
||||
仅勾选用户会进入 TKMind 智趣独立通道;其余用户完全不受影响,仍走原有 DeepSeek/Goose 链路。
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="modal-close" onClick={onClose} aria-label="关闭">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="wechat-toolbar" style={{ marginBottom: 12 }}>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="搜索用户名、昵称或 userId"
|
||||
style={{ flex: 1, minWidth: 220 }}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={loadingUsers || pickerUsers.length === 0}
|
||||
onClick={() => {
|
||||
setDraftIds((current) => {
|
||||
if (allVisibleSelected) {
|
||||
return current.filter((id) => !visibleIds.includes(id));
|
||||
}
|
||||
return [...new Set([...current, ...visibleIds])];
|
||||
});
|
||||
}}
|
||||
>
|
||||
{allVisibleSelected ? '取消全选当前列表' : '全选当前列表'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="cursor-allowlist-picker-list">
|
||||
{loadingUsers ? <p className="muted">加载用户…</p> : null}
|
||||
{!loadingUsers && pickerUsers.length === 0 ? (
|
||||
<p className="muted">没有匹配的用户</p>
|
||||
) : null}
|
||||
{pickerUsers.map((user) => (
|
||||
<label key={user.id} className="cursor-allowlist-picker-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draftIds.includes(user.id)}
|
||||
onChange={() => {
|
||||
setDraftIds((current) =>
|
||||
current.includes(user.id)
|
||||
? current.filter((id) => id !== user.id)
|
||||
: [...current, user.id],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{user.displayName || user.username}
|
||||
<span className="muted"> @{user.username}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="ghost-btn" onClick={onClose} disabled={busy}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-btn"
|
||||
disabled={busy}
|
||||
onClick={() => onConfirm(draftIds)}
|
||||
>
|
||||
确认选择 ({draftIds.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CursorChannelPage() {
|
||||
const [users, setUsers] = useState<AdminUserRow[]>([]);
|
||||
const [form, setForm] = useState<CursorChannelForm | null>(null);
|
||||
const [savedForm, setSavedForm] = useState<CursorChannelForm | null>(null);
|
||||
const [runtime, setRuntime] = useState<WechatCursorExecutorRuntimeState | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(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 (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>TKMind 智趣体验通道</h2>
|
||||
<p className="muted">
|
||||
独立于现有 DeepSeek/Goose 链路的 Cursor 执行通道。仅对白名单用户生效,支持 H5 与服务号;其余用户完全不受影响。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{notice && <p className="banner banner-info">{notice}</p>}
|
||||
|
||||
<section className="admin-card">
|
||||
<h3>通道说明</h3>
|
||||
<ul className="muted" style={{ margin: '8px 0 0', paddingLeft: 20, lineHeight: 1.7 }}>
|
||||
<li>
|
||||
<strong>H5</strong>:白名单用户在页面生成、问卷(Page Data)、Excel 分析等任务时,走 Cursor 独立执行。
|
||||
</li>
|
||||
<li>
|
||||
<strong>服务号</strong>:白名单用户在指定意图(默认 page.generate)下走 Cursor;其余意图仍走原有链路。
|
||||
</li>
|
||||
<li>非白名单用户:H5 与服务号均保持原有 DeepSeek/Goose 行为,不会被 Cursor 路由影响。</li>
|
||||
</ul>
|
||||
<p className="muted" style={{ marginTop: 12 }}>
|
||||
服务端需开启 <code>MEMIND_CURSOR_EXECUTOR_ENABLED=1</code> 与 Tool Gateway;详见 Memind{' '}
|
||||
<code>.env.example</code>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{form ? (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>通道配置</h2>
|
||||
<p className="muted" style={{ marginTop: 6 }}>
|
||||
默认关闭。开启后仅白名单用户进入智趣通道,不影响其他用户。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void handleSave()}
|
||||
disabled={loading || saving || !dirty}
|
||||
>
|
||||
{saving ? '保存中…' : dirty ? '保存配置' : '已保存'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="admin-form" style={{ marginTop: 16 }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.enabled}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) =>
|
||||
setForm((current) => current && { ...current, enabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
启用智趣体验通道
|
||||
</label>
|
||||
<fieldset style={{ border: 'none', padding: 0, margin: '12px 0' }}>
|
||||
<legend className="muted" style={{ marginBottom: 8 }}>
|
||||
接入渠道
|
||||
</legend>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.h5Enabled}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) =>
|
||||
setForm((current) => current && { ...current, h5Enabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
H5 聊天
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.wechatEnabled}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) =>
|
||||
setForm((current) => current && { ...current, wechatEnabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
微信服务号
|
||||
</label>
|
||||
</fieldset>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.fallbackToDeepseek}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) =>
|
||||
setForm((current) =>
|
||||
current && { ...current, fallbackToDeepseek: event.target.checked },
|
||||
)
|
||||
}
|
||||
/>{' '}
|
||||
Cursor 失败时自动回退 DeepSeek
|
||||
</label>
|
||||
<label>
|
||||
体验用户白名单
|
||||
<div className="wechat-toolbar" style={{ marginTop: 8, marginBottom: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={loading || saving}
|
||||
onClick={() => setPickerOpen(true)}
|
||||
>
|
||||
从用户列表选择…
|
||||
</button>
|
||||
{form.selectedUserIds.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={loading || saving}
|
||||
onClick={() =>
|
||||
setForm((current) => current && { ...current, selectedUserIds: [] })
|
||||
}
|
||||
>
|
||||
清空已选
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedUsers.length > 0 ? (
|
||||
<div className="cursor-allowlist-chips">
|
||||
{selectedUsers.map(({ userId, user }) => (
|
||||
<span key={userId} className="cursor-allowlist-chip">
|
||||
<span>
|
||||
{user ? user.displayName || user.username : userId}
|
||||
{user ? <span className="muted"> @{user.username}</span> : null}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-allowlist-chip-remove"
|
||||
aria-label="移出白名单"
|
||||
disabled={loading || saving}
|
||||
onClick={() =>
|
||||
setForm((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
selectedUserIds: current.selectedUserIds.filter((id) => id !== userId),
|
||||
}
|
||||
: current,
|
||||
)
|
||||
}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
尚未选择用户。可在 <Link to="/users">用户管理</Link> 中查找账号后在此勾选。
|
||||
</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 }}>
|
||||
运行时:{runtime.policy.enabled ? '已开启' : '已关闭'}
|
||||
{' · '}
|
||||
白名单 {runtime.policy.userAllowlist.length} 人
|
||||
{' · '}
|
||||
渠道 {(runtime.policy.channelAllowlist ?? []).join('、') || '无'}
|
||||
{' · '}
|
||||
来源 {runtime.source}
|
||||
{runtime.updatedAt ? ` · 更新 ${formatTime(runtime.updatedAt)}` : ''}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{form ? (
|
||||
<CursorAllowlistPickerModal
|
||||
open={pickerOpen}
|
||||
users={users}
|
||||
selectedUserIds={form.selectedUserIds}
|
||||
busy={loading || saving}
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onConfirm={(nextIds) => {
|
||||
setForm((current) => current && { ...current, selectedUserIds: nextIds });
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+12
-473
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
cancelWechatDigest,
|
||||
clearWechatRoute,
|
||||
@@ -6,8 +7,6 @@ import {
|
||||
getWechatAdminSummary,
|
||||
getWechatIntentRouterConfig,
|
||||
getWechatIntentRouterRuntime,
|
||||
getWechatCursorExecutorConfig,
|
||||
getWechatCursorExecutorRuntime,
|
||||
listAdminUsers,
|
||||
listLlmProviderKeys,
|
||||
listWechatBindings,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
listWechatMessages,
|
||||
listWechatWebNotifications,
|
||||
patchWechatIntentRouterConfig,
|
||||
patchWechatCursorExecutorConfig,
|
||||
resumeWechatDigest,
|
||||
updateWechatScheduleLlmConfig,
|
||||
} from '../../api/client';
|
||||
@@ -29,8 +27,6 @@ import type {
|
||||
WechatDigestSubscription,
|
||||
WechatIntentRouterAdminConfig,
|
||||
WechatIntentRouterRuntimeState,
|
||||
WechatCursorExecutorAdminConfig,
|
||||
WechatCursorExecutorRuntimeState,
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
} from '../../types';
|
||||
@@ -125,268 +121,6 @@ function intentRouterRuntimeMode(overrides: Record<string, string>) {
|
||||
return '已激活(chat.general 可升级为 page.generate)';
|
||||
}
|
||||
|
||||
type CursorExecutorForm = {
|
||||
enabled: 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<string>();
|
||||
|
||||
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 cursorExecutorToForm(
|
||||
config: WechatCursorExecutorAdminConfig | undefined,
|
||||
users: AdminUserRow[] = [],
|
||||
): CursorExecutorForm {
|
||||
const tokens = Array.isArray(config?.userAllowlist) ? config.userAllowlist : [];
|
||||
const { selectedUserIds, manualAllowlistEntries } = resolveAllowlistTokens(tokens, users);
|
||||
return {
|
||||
enabled: Boolean(config?.enabled),
|
||||
selectedUserIds,
|
||||
manualAllowlistEntries,
|
||||
intentAllowlist: Array.isArray(config?.intentAllowlist)
|
||||
? config.intentAllowlist.join('\n')
|
||||
: 'page.generate',
|
||||
fallbackToDeepseek: config?.fallbackToDeepseek !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function cursorFormToAllowlist(form: CursorExecutorForm) {
|
||||
const manual = form.manualAllowlistEntries
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
return [...new Set([...form.selectedUserIds, ...manual])];
|
||||
}
|
||||
|
||||
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<string[]>(selectedUserIds);
|
||||
const [pickerUsers, setPickerUsers] = useState<AdminUserRow[]>(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));
|
||||
|
||||
const toggleUser = (userId: string) => {
|
||||
setDraftIds((current) =>
|
||||
current.includes(userId)
|
||||
? current.filter((id) => id !== userId)
|
||||
: [...current, userId],
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAllVisible = () => {
|
||||
setDraftIds((current) => {
|
||||
if (allVisibleSelected) {
|
||||
return current.filter((id) => !visibleIds.includes(id));
|
||||
}
|
||||
return [...new Set([...current, ...visibleIds])];
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-box"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cursor-allowlist-picker-title"
|
||||
style={{ maxWidth: 720, width: 'min(720px, calc(100vw - 32px))' }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="modal-head">
|
||||
<div>
|
||||
<h3 id="cursor-allowlist-picker-title">选择体验通道用户</h3>
|
||||
<p className="muted" style={{ margin: '6px 0 0', fontSize: 13 }}>
|
||||
勾选后将加入 TKMind 智趣体验白名单;未勾选用户仍走原有 DeepSeek 链路。
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="modal-close" onClick={onClose} aria-label="关闭">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="wechat-toolbar" style={{ marginBottom: 12 }}>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="搜索用户名、昵称或 userId"
|
||||
style={{ flex: 1, minWidth: 220 }}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="button" className="ghost-btn" onClick={toggleAllVisible} disabled={loadingUsers || pickerUsers.length === 0}>
|
||||
{allVisibleSelected ? '取消全选当前列表' : '全选当前列表'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="muted" style={{ margin: '0 0 8px', fontSize: 12 }}>
|
||||
已选 {draftIds.length} 人{loadingUsers ? ' · 加载中…' : ''}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="admin-table-wrap"
|
||||
style={{ maxHeight: 360, overflow: 'auto', border: '1px solid var(--color-border, #e7dfd1)', borderRadius: 8 }}
|
||||
>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 44 }} />
|
||||
<th>用户</th>
|
||||
<th>用户名</th>
|
||||
<th>userId</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pickerUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="muted">
|
||||
{loadingUsers ? '加载用户列表…' : '没有匹配的用户'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
pickerUsers.map((user) => {
|
||||
const checked = draftIds.includes(user.id);
|
||||
return (
|
||||
<tr
|
||||
key={user.id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => toggleUser(user.id)}
|
||||
>
|
||||
<td onClick={(event) => event.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleUser(user.id)}
|
||||
/>
|
||||
</td>
|
||||
<td>{user.displayName || user.username}</td>
|
||||
<td className="muted">@{user.username}</td>
|
||||
<td className="mono">{user.id}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions" style={{ marginTop: 16 }}>
|
||||
<button type="button" className="ghost-btn" onClick={onClose} disabled={busy}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy}
|
||||
onClick={() => onConfirm(draftIds)}
|
||||
>
|
||||
确认加入白名单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WechatPage() {
|
||||
const [summary, setSummary] = useState<WechatAdminSummary | null>(null);
|
||||
const [bindings, setBindings] = useState<WechatBinding[]>([]);
|
||||
@@ -413,11 +147,6 @@ export function WechatPage() {
|
||||
const [savedIntentForm, setSavedIntentForm] = useState<IntentRouterForm | null>(null);
|
||||
const [intentRuntime, setIntentRuntime] = useState<WechatIntentRouterRuntimeState | null>(null);
|
||||
const [intentSaving, setIntentSaving] = useState(false);
|
||||
const [cursorForm, setCursorForm] = useState<CursorExecutorForm | null>(null);
|
||||
const [savedCursorForm, setSavedCursorForm] = useState<CursorExecutorForm | null>(null);
|
||||
const [cursorRuntime, setCursorRuntime] = useState<WechatCursorExecutorRuntimeState | null>(null);
|
||||
const [cursorSaving, setCursorSaving] = useState(false);
|
||||
const [cursorAllowlistPickerOpen, setCursorAllowlistPickerOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -434,11 +163,6 @@ export function WechatPage() {
|
||||
[llmKeys, intentForm?.modelProviderKeyId],
|
||||
);
|
||||
|
||||
const cursorDirty = useMemo(() => {
|
||||
if (!cursorForm || !savedCursorForm) return false;
|
||||
return JSON.stringify(cursorForm) !== JSON.stringify(savedCursorForm);
|
||||
}, [cursorForm, savedCursorForm]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -454,8 +178,6 @@ export function WechatPage() {
|
||||
nextLlmKeys,
|
||||
nextIntentConfig,
|
||||
nextIntentRuntime,
|
||||
nextCursorConfig,
|
||||
nextCursorRuntime,
|
||||
] =
|
||||
await Promise.all([
|
||||
getWechatAdminSummary(),
|
||||
@@ -468,8 +190,6 @@ export function WechatPage() {
|
||||
listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]),
|
||||
getWechatIntentRouterConfig().catch(() => null),
|
||||
getWechatIntentRouterRuntime().catch(() => null),
|
||||
getWechatCursorExecutorConfig().catch(() => null),
|
||||
getWechatCursorExecutorRuntime().catch(() => null),
|
||||
]);
|
||||
setSummary(nextSummary);
|
||||
setScheduleLlmEnabled(nextSummary.config.scheduleLlmEnabled ?? false);
|
||||
@@ -487,12 +207,6 @@ export function WechatPage() {
|
||||
setSavedIntentForm(nextForm);
|
||||
}
|
||||
setIntentRuntime(nextIntentRuntime);
|
||||
if (nextCursorConfig?.config) {
|
||||
const nextCursorForm = cursorExecutorToForm(nextCursorConfig.config, nextUsers.items);
|
||||
setCursorForm(nextCursorForm);
|
||||
setSavedCursorForm(nextCursorForm);
|
||||
}
|
||||
setCursorRuntime(nextCursorRuntime);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载服务号管理失败');
|
||||
} finally {
|
||||
@@ -630,42 +344,6 @@ export function WechatPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCursorExecutor = async () => {
|
||||
if (!cursorForm) return;
|
||||
setCursorSaving(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await patchWechatCursorExecutorConfig({
|
||||
enabled: cursorForm.enabled,
|
||||
userAllowlist: cursorFormToAllowlist(cursorForm),
|
||||
intentAllowlist: cursorForm.intentAllowlist
|
||||
.split(/[\s,]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
fallbackToDeepseek: cursorForm.fallbackToDeepseek,
|
||||
});
|
||||
const nextForm = cursorExecutorToForm(result.config, users);
|
||||
setCursorForm(nextForm);
|
||||
setSavedCursorForm(nextForm);
|
||||
setCursorRuntime(await getWechatCursorExecutorRuntime().catch(() => null));
|
||||
setNotice('微信 TKMind 智趣体验通道已保存。未在白名单内的用户仍走原有 DeepSeek 链路。');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setCursorSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCursorUsers = useMemo(() => {
|
||||
if (!cursorForm) return [];
|
||||
const byId = new Map(users.map((user) => [user.id, user]));
|
||||
return cursorForm.selectedUserIds.map((userId) => ({
|
||||
userId,
|
||||
user: byId.get(userId) ?? null,
|
||||
}));
|
||||
}, [cursorForm, users]);
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
@@ -882,156 +560,17 @@ export function WechatPage() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{cursorForm ? (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>服务号 TKMind 智趣体验通道</h2>
|
||||
<p className="muted" style={{ marginTop: 6 }}>
|
||||
默认全员仍走原有 DeepSeek/Goose 链路。仅白名单用户在「做页面」等指定意图下,才会尝试 TKMind 智趣执行;失败可自动回退 DeepSeek。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void handleSaveCursorExecutor()}
|
||||
disabled={busy || cursorSaving || !cursorDirty}
|
||||
>
|
||||
{cursorSaving ? '保存中…' : cursorDirty ? '保存配置' : '已保存'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-form" style={{ marginTop: 16 }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cursorForm.enabled}
|
||||
disabled={busy || cursorSaving}
|
||||
onChange={(event) =>
|
||||
setCursorForm((current) => current && { ...current, enabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
启用体验通道(需选择白名单用户)
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cursorForm.fallbackToDeepseek}
|
||||
disabled={busy || cursorSaving}
|
||||
onChange={(event) =>
|
||||
setCursorForm((current) =>
|
||||
current && { ...current, fallbackToDeepseek: event.target.checked },
|
||||
)
|
||||
}
|
||||
/>{' '}
|
||||
Cursor 失败时自动回退 DeepSeek
|
||||
</label>
|
||||
<label>
|
||||
体验用户白名单
|
||||
<div className="wechat-toolbar" style={{ marginTop: 8, marginBottom: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || cursorSaving}
|
||||
onClick={() => setCursorAllowlistPickerOpen(true)}
|
||||
>
|
||||
从用户列表选择…
|
||||
</button>
|
||||
{cursorForm.selectedUserIds.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || cursorSaving}
|
||||
onClick={() =>
|
||||
setCursorForm((current) =>
|
||||
current && { ...current, selectedUserIds: [] },
|
||||
)
|
||||
}
|
||||
>
|
||||
清空已选
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedCursorUsers.length > 0 ? (
|
||||
<div className="cursor-allowlist-chips">
|
||||
{selectedCursorUsers.map(({ userId, user }) => (
|
||||
<span key={userId} className="cursor-allowlist-chip">
|
||||
<span>
|
||||
{user ? (user.displayName || user.username) : userId}
|
||||
{user ? <span className="muted"> @{user.username}</span> : null}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-allowlist-chip-remove"
|
||||
aria-label="移出白名单"
|
||||
disabled={busy || cursorSaving}
|
||||
onClick={() =>
|
||||
setCursorForm((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
selectedUserIds: current.selectedUserIds.filter((id) => id !== userId),
|
||||
}
|
||||
: current,
|
||||
)
|
||||
}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
尚未选择用户。点击「从用户列表选择」勾选要开通体验通道的账号。
|
||||
</p>
|
||||
)}
|
||||
{cursorForm.manualAllowlistEntries.length > 0 ? (
|
||||
<p className="muted" style={{ marginTop: 8, fontSize: 12 }}>
|
||||
另有 {cursorForm.manualAllowlistEntries.length} 条手动条目仍保留:
|
||||
{' '}
|
||||
{cursorForm.manualAllowlistEntries.join('、')}
|
||||
</p>
|
||||
) : null}
|
||||
</label>
|
||||
<label>
|
||||
启用意图(默认 page.generate)
|
||||
<textarea
|
||||
value={cursorForm.intentAllowlist}
|
||||
disabled={busy || cursorSaving}
|
||||
rows={2}
|
||||
placeholder="page.generate"
|
||||
onChange={(event) =>
|
||||
setCursorForm((current) => current && { ...current, intentAllowlist: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{cursorRuntime ? (
|
||||
<p className="muted" style={{ marginTop: 12 }}>
|
||||
运行时:{cursorRuntime.policy.enabled ? '已开启' : '已关闭'}
|
||||
{' · '}
|
||||
白名单 {cursorRuntime.policy.userAllowlist.length} 人
|
||||
{' · '}
|
||||
来源 {cursorRuntime.source}
|
||||
{cursorRuntime.updatedAt ? ` · 更新 ${dateLabel(cursorRuntime.updatedAt)}` : ''}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{cursorForm ? (
|
||||
<CursorAllowlistPickerModal
|
||||
open={cursorAllowlistPickerOpen}
|
||||
users={users}
|
||||
selectedUserIds={cursorForm.selectedUserIds}
|
||||
busy={busy || cursorSaving}
|
||||
onClose={() => setCursorAllowlistPickerOpen(false)}
|
||||
onConfirm={(nextIds) => {
|
||||
setCursorForm((current) => current && { ...current, selectedUserIds: nextIds });
|
||||
setCursorAllowlistPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<section className="admin-card">
|
||||
<h2>TKMind 智趣体验通道</h2>
|
||||
<p className="muted" style={{ marginTop: 6 }}>
|
||||
Cursor 独立执行通道已迁移至独立管理页,可同时配置 H5 与服务号白名单用户,不影响其他用户原有链路。
|
||||
</p>
|
||||
<p style={{ marginTop: 12 }}>
|
||||
<Link to="/cursor-channel" className="ghost-btn">
|
||||
前往智趣体验通道配置 →
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
|
||||
+3
-3
@@ -478,20 +478,20 @@ export async function getWechatIntentRouterRuntime(): Promise<WechatIntentRouter
|
||||
}
|
||||
|
||||
export async function getWechatCursorExecutorConfig(): Promise<WechatCursorExecutorAdminConfigState> {
|
||||
return portalFetch<WechatCursorExecutorAdminConfigState>('/admin-api/wechat/cursor-executor/config');
|
||||
return portalFetch<WechatCursorExecutorAdminConfigState>('/admin-api/cursor-executor-channel/config');
|
||||
}
|
||||
|
||||
export async function patchWechatCursorExecutorConfig(
|
||||
patch: Partial<WechatCursorExecutorAdminConfig>,
|
||||
): Promise<WechatCursorExecutorAdminConfigState> {
|
||||
return portalFetch<WechatCursorExecutorAdminConfigState>('/admin-api/wechat/cursor-executor/config', {
|
||||
return portalFetch<WechatCursorExecutorAdminConfigState>('/admin-api/cursor-executor-channel/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ config: patch }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWechatCursorExecutorRuntime(): Promise<WechatCursorExecutorRuntimeState> {
|
||||
return portalFetch<WechatCursorExecutorRuntimeState>('/admin-api/wechat/cursor-executor/runtime');
|
||||
return portalFetch<WechatCursorExecutorRuntimeState>('/admin-api/cursor-executor-channel/runtime');
|
||||
}
|
||||
|
||||
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
|
||||
|
||||
@@ -349,6 +349,7 @@ export type WechatIntentRouterRuntimeState = {
|
||||
export type WechatCursorExecutorAdminConfig = {
|
||||
enabled: boolean;
|
||||
userAllowlist: string[];
|
||||
channelAllowlist: string[];
|
||||
intentAllowlist: string[];
|
||||
fallbackToDeepseek: boolean;
|
||||
meta?: {
|
||||
@@ -371,6 +372,7 @@ export type WechatCursorExecutorRuntimeState = {
|
||||
policy: {
|
||||
enabled: boolean;
|
||||
userAllowlist: string[];
|
||||
channelAllowlist: string[];
|
||||
intentAllowlist: string[];
|
||||
fallbackToDeepseek: boolean;
|
||||
source?: string;
|
||||
|
||||
Reference in New Issue
Block a user