diff --git a/server/app.mjs b/server/app.mjs index 907136a..c1e2d12 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -108,6 +108,8 @@ export function createAdminApp(services) { skillRuntimeConfigService, billingConfigService, wechatScheduleLlmConfigService, + wechatIntentRouterConfigService, + wechatCursorExecutorPolicyService, adminSystemTestService, systemTestAccountService, wordFilterService, @@ -395,6 +397,62 @@ export function createAdminApp(services) { res.json(result); }); + adminApi.get('/wechat/intent-router/config', requireAdmin, async (_req, res) => { + if (!wechatIntentRouterConfigService?.getConfig) { + return res.status(503).json({ message: '微信意图路由配置未启用' }); + } + const config = await wechatIntentRouterConfigService.getConfig(); + return res.json({ config }); + }); + + adminApi.get('/wechat/intent-router/runtime', requireAdmin, async (_req, res) => { + if (!wechatIntentRouterConfigService?.getRuntimeState) { + return res.status(503).json({ message: '微信意图路由配置未启用' }); + } + return res.json(await wechatIntentRouterConfigService.getRuntimeState()); + }); + + const updateWechatIntentRouterConfig = async (req, res) => { + if (!wechatIntentRouterConfigService?.updateConfig) { + return res.status(503).json({ message: '微信意图路由配置未启用' }); + } + const config = await wechatIntentRouterConfigService.updateConfig(req.body ?? {}, { + updatedBy: req.currentUser.id, + }); + return res.json({ config }); + }; + + adminApi.put('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig); + adminApi.patch('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig); + + adminApi.get('/wechat/cursor-executor/config', requireAdmin, async (_req, res) => { + if (!wechatCursorExecutorPolicyService?.getAdminConfig) { + return res.status(503).json({ message: '微信 Cursor 体验通道未启用' }); + } + return res.json(await wechatCursorExecutorPolicyService.getAdminConfig()); + }); + + adminApi.get('/wechat/cursor-executor/runtime', requireAdmin, async (_req, res) => { + if (!wechatCursorExecutorPolicyService?.getRuntimeState) { + return res.status(503).json({ message: '微信 Cursor 体验通道未启用' }); + } + return res.json(await wechatCursorExecutorPolicyService.getRuntimeState()); + }); + + const updateWechatCursorExecutorConfig = async (req, res) => { + if (!wechatCursorExecutorPolicyService?.updateAdminConfig) { + return res.status(503).json({ message: '微信 Cursor 体验通道未启用' }); + } + const result = await wechatCursorExecutorPolicyService.updateAdminConfig( + req.body?.config ?? req.body ?? {}, + { updatedBy: req.currentUser.id }, + ); + return res.json(result); + }; + + adminApi.put('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig); + adminApi.patch('/wechat/cursor-executor/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); diff --git a/server/bootstrap.mjs b/server/bootstrap.mjs index 5c8defd..d4021ac 100644 --- a/server/bootstrap.mjs +++ b/server/bootstrap.mjs @@ -43,6 +43,10 @@ export async function bootstrapAdminServices() { const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs'); const { ensureAssetGatewaySchema } = await importMemind('db.mjs'); const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs'); + const { createWechatIntentRouterConfigService } = await importMemind('wechat-intent-router-config.mjs'); + const { createWechatCursorExecutorAdminConfigService } = await importMemind( + 'wechat-cursor-executor-admin-config.mjs', + ); const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs'); const { createOpsApi } = await importMemind('admin-routes.mjs'); const { createWordFilterService, ensureWordFilterSchema } = await importMemind('word-filter.mjs'); @@ -126,6 +130,8 @@ export async function bootstrapAdminServices() { const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool, { env: process.env, }); + const wechatIntentRouterConfigService = createWechatIntentRouterConfigService(pool); + const wechatCursorExecutorPolicyService = createWechatCursorExecutorAdminConfigService(pool); const adminSystemTestService = createAdminSystemTestService({ pool, userAuth, @@ -198,6 +204,8 @@ export async function bootstrapAdminServices() { skillRuntimeConfigService, billingConfigService, wechatScheduleLlmConfigService, + wechatIntentRouterConfigService, + wechatCursorExecutorPolicyService, adminSystemTestService, systemTestAccountService, wordFilterService, diff --git a/server/index.mjs b/server/index.mjs index a0c7ff2..60e6391 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -112,6 +112,8 @@ ready skillRuntimeConfigService, billingConfigService, wechatScheduleLlmConfigService, + wechatIntentRouterConfigService, + wechatCursorExecutorPolicyService, adminSystemTestService, systemTestAccountService, wordFilterService, @@ -142,6 +144,8 @@ ready skillRuntimeConfigService, billingConfigService, wechatScheduleLlmConfigService, + wechatIntentRouterConfigService, + wechatCursorExecutorPolicyService, adminSystemTestService, systemTestAccountService, wordFilterService, diff --git a/src/admin/pages/WechatPage.tsx b/src/admin/pages/WechatPage.tsx index 95b4777..c922af1 100644 --- a/src/admin/pages/WechatPage.tsx +++ b/src/admin/pages/WechatPage.tsx @@ -6,6 +6,8 @@ import { getWechatAdminSummary, getWechatIntentRouterConfig, getWechatIntentRouterRuntime, + getWechatCursorExecutorConfig, + getWechatCursorExecutorRuntime, listAdminUsers, listLlmProviderKeys, listWechatBindings, @@ -14,6 +16,7 @@ import { listWechatMessages, listWechatWebNotifications, patchWechatIntentRouterConfig, + patchWechatCursorExecutorConfig, resumeWechatDigest, updateWechatScheduleLlmConfig, } from '../../api/client'; @@ -26,6 +29,8 @@ import type { WechatDigestSubscription, WechatIntentRouterAdminConfig, WechatIntentRouterRuntimeState, + WechatCursorExecutorAdminConfig, + WechatCursorExecutorRuntimeState, WechatMessage, WechatWebNotification, } from '../../types'; @@ -120,6 +125,268 @@ function intentRouterRuntimeMode(overrides: Record) { 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(); + + 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(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)); + + 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 ( +
{ + if (event.target === event.currentTarget) onClose(); + }} + > +
event.stopPropagation()} + > +
+
+

选择体验通道用户

+

+ 勾选后将加入 TKMind 智趣体验白名单;未勾选用户仍走原有 DeepSeek 链路。 +

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

+ 已选 {draftIds.length} 人{loadingUsers ? ' · 加载中…' : ''} +

+ +
+ + + + + + + + + + {pickerUsers.length === 0 ? ( + + + + ) : ( + pickerUsers.map((user) => { + const checked = draftIds.includes(user.id); + return ( + toggleUser(user.id)} + > + + + + + + ); + }) + )} + +
+ 用户用户名userId
+ {loadingUsers ? '加载用户列表…' : '没有匹配的用户'} +
event.stopPropagation()}> + toggleUser(user.id)} + /> + {user.displayName || user.username}@{user.username}{user.id}
+
+ +
+ + +
+
+
+ ); +} + export function WechatPage() { const [summary, setSummary] = useState(null); const [bindings, setBindings] = useState([]); @@ -146,6 +413,11 @@ export function WechatPage() { const [savedIntentForm, setSavedIntentForm] = useState(null); const [intentRuntime, setIntentRuntime] = useState(null); const [intentSaving, setIntentSaving] = useState(false); + const [cursorForm, setCursorForm] = useState(null); + const [savedCursorForm, setSavedCursorForm] = useState(null); + const [cursorRuntime, setCursorRuntime] = useState(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(null); @@ -162,6 +434,11 @@ 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); @@ -177,6 +454,8 @@ export function WechatPage() { nextLlmKeys, nextIntentConfig, nextIntentRuntime, + nextCursorConfig, + nextCursorRuntime, ] = await Promise.all([ getWechatAdminSummary(), @@ -189,6 +468,8 @@ 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); @@ -206,6 +487,12 @@ 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 { @@ -343,6 +630,42 @@ 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 (
@@ -559,6 +882,157 @@ export function WechatPage() { ) : null} + {cursorForm ? ( +
+
+
+

服务号 TKMind 智趣体验通道

+

+ 默认全员仍走原有 DeepSeek/Goose 链路。仅白名单用户在「做页面」等指定意图下,才会尝试 TKMind 智趣执行;失败可自动回退 DeepSeek。 +

+
+ +
+
+ + + +