Isolate Cursor executor to admin-configured whitelist channel for H5 and WeChat.
Memind CI / Test, build, and release guards (push) Failing after 4m22s

Non-allowlisted users stay on the existing Goose/DeepSeek path; only memindadm whitelist users enter the TKMind Cursor channel on selected intents and channels.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-28 15:27:10 +08:00
parent 02fdcdedc5
commit 416bf7a29a
13 changed files with 339 additions and 42 deletions
+4 -4
View File
@@ -525,11 +525,11 @@ MEMIND_RUNTIME_PROFILE=local
# MEMIND_CURSOR_HELP_AGENT_BIN=/Users/john/.local/bin/agent
# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1
# Track C: Goose 编排 + Cursor 作为 code executorMindSpace 落盘,推荐 PoC
# MEMIND_CURSOR_EXECUTOR_ENABLED=1
# Track C: TKMind 智趣(Cursor作为 code executor — 仅对白名单用户生效(memindadm「智趣体验通道」
# MEMIND_CURSOR_EXECUTOR_ENABLED=1 # 服务端 Cursor CLI 基础设施开关
# MEMIND_AIDER_SKILL_USE_CURSOR=1
# 默认将页面生成、问卷(Page Data)、Excel 分析路由到 Cursor 执行
# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=1
# 已废弃全员路由:页面/问卷/Excel 是否走 Cursor 由 memindadm 白名单控制,不再靠 MEMIND_CURSOR_PAGE_TASKS_DEFAULT
# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=0
# 已废弃:不再把普通 Agent 编排全量切到 Cursor;简单对话走 DeepSeek direct_chat
# MEMIND_CURSOR_AGENT_TASKS_DEFAULT=0
# MEMIND_CURSOR_DEEPSEEK_FALLBACK=1
+17
View File
@@ -955,6 +955,23 @@ export function createAdminApi({
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('/llm-providers/catalog', requireAdmin, (_req, res) => {
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
res.json({ catalog: llmProviderService.catalog });
+51 -21
View File
@@ -25,6 +25,10 @@ import { wrapRunStreamPayload, writeSseErrorAndEnd } from './sse-event-taxonomy.
import { resolveGoalBindingForAgentRun } from './goal-run-resolve.mjs';
import { enforcePageGenerationCursorRuntime } from './cursor-page-routing.mjs';
import { resolvePreferredCodeExecutor } from './cursor-agent-launch.mjs';
import {
CURSOR_EXECUTOR_CHANNEL,
resolveCursorChannelEligible,
} from './wechat-cursor-executor-policy.mjs';
function envFlag(value) {
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
@@ -188,6 +192,7 @@ export function createPostAgentRunsHandler({
agentRunGateway,
mindSpaceAssetAgent = null,
codeRunPolicyService = null,
cursorExecutorPolicyService = null,
goalRunService = null,
chatIntentRouter = null,
templateCatalogService = null,
@@ -259,12 +264,30 @@ export function createPostAgentRunsHandler({
let requiredExecutor = selectedSkillRuntime.requiredExecutor ?? null;
let requiredReviewExecutor = selectedSkillRuntime.requiredReviewExecutor ?? null;
let cursorChannelEligible = false;
if (cursorExecutorPolicyService?.getEffectivePolicy) {
try {
const cursorPolicy = await cursorExecutorPolicyService.getEffectivePolicy(
request.currentUser.id,
request.currentUser,
);
cursorChannelEligible = resolveCursorChannelEligible({
user: request.currentUser,
channel: CURSOR_EXECUTOR_CHANNEL.H5,
policy: cursorPolicy,
});
} catch {
cursorChannelEligible = false;
}
}
if (!requiredExecutor && !requiredReviewExecutor) {
const pageCursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
rawToolMode,
taskType,
forceDeepReasoning,
env: process.env,
channelEligible: cursorChannelEligible,
});
userMessage = pageCursorRuntime.userMessage;
rawToolMode = pageCursorRuntime.rawToolMode;
@@ -307,29 +330,36 @@ export function createPostAgentRunsHandler({
const policyTaskType = requiredReviewExecutor
? 'h5_chat_code_task'
: taskType;
if (!codeRunPolicy.enabled) {
response.status(403).json({ message: '代码任务灰度未开启' });
return;
}
if (!codeRunPolicy.userAllowed) {
response.status(403).json({ message: '当前用户未开启代码任务灰度' });
return;
}
const taskTypeAllowlist = codeRunPolicy.taskTypeAllowlist ?? [];
if (
taskTypeAllowlist.length > 0 &&
(
!policyTaskType ||
!taskTypeAllowlist
.map((item) => String(item).toLowerCase())
.includes(policyTaskType.toLowerCase())
)
) {
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
return;
const isCursorChannelRun = cursorChannelEligible && (
requiredExecutor === 'cursor'
|| userMessage?.metadata?.memindRun?.pageCursorDefault === true
);
if (!isCursorChannelRun) {
if (!codeRunPolicy.enabled) {
response.status(403).json({ message: '代码任务灰度未开启' });
return;
}
if (!codeRunPolicy.userAllowed) {
response.status(403).json({ message: '当前用户未开启代码任务灰度' });
return;
}
const taskTypeAllowlist = codeRunPolicy.taskTypeAllowlist ?? [];
if (
taskTypeAllowlist.length > 0 &&
(
!policyTaskType ||
!taskTypeAllowlist
.map((item) => String(item).toLowerCase())
.includes(policyTaskType.toLowerCase())
)
) {
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
return;
}
}
if (
codeRunPolicy.requireValidation
!isCursorChannelRun
&& codeRunPolicy.requireValidation
&& !hasExpectedFileValidation(userMessage)
&& !userMessage?.metadata?.memindRun?.pageCursorDefault
&& !userMessage?.metadata?.memindRun?.cursorAgentDefault
+125
View File
@@ -1091,3 +1091,128 @@ test('POST /agent/runs materializes selected assets before creating run', async
assetIds: ['asset-9'],
}]);
});
test('POST /agent/runs keeps non-allowlisted H5 users on existing chat flow for page tasks', async () => {
const created = [];
const prevCursorEnabled = process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
const prevUseCursor = process.env.MEMIND_AIDER_SKILL_USE_CURSOR;
process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = '1';
process.env.MEMIND_AIDER_SKILL_USE_CURSOR = '1';
try {
const handler = createPostAgentRunsHandler({
userAuth: {
async ownsSession() {
return true;
},
},
cursorExecutorPolicyService: {
async getEffectivePolicy(userId) {
return {
enabled: true,
userAllowlist: ['other-user'],
channelAllowlist: ['h5', 'wechat_mp'],
intentAllowlist: ['page.generate'],
userAllowed: userId === 'other-user',
};
},
},
agentRunGateway: {
async createRun(userId, payload) {
created.push({ userId, payload });
return { id: 'run-chat-1', status: 'queued' };
},
},
});
const res = createResponseRecorder();
await handler(
{
currentUser: { id: 'user-1', username: 'john' },
body: {
session_id: 'session-1',
request_id: 'req-page-1',
user_message: {
role: 'user',
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
metadata: { displayText: '帮我写首诗,并做成页面' },
},
},
},
res,
);
assert.equal(res.statusCode, 202);
assert.equal(created[0].payload.toolMode, 'chat');
assert.equal(created[0].payload.taskType, null);
assert.notEqual(created[0].payload.userMessage?.metadata?.memindRun?.executor, 'cursor');
assert.notEqual(created[0].payload.userMessage?.metadata?.memindRun?.pageCursorDefault, true);
} finally {
if (prevCursorEnabled == null) delete process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
else process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = prevCursorEnabled;
if (prevUseCursor == null) delete process.env.MEMIND_AIDER_SKILL_USE_CURSOR;
else process.env.MEMIND_AIDER_SKILL_USE_CURSOR = prevUseCursor;
}
});
test('POST /agent/runs routes allowlisted H5 users into cursor channel for page tasks', async () => {
const created = [];
const prevCursorEnabled = process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
const prevUseCursor = process.env.MEMIND_AIDER_SKILL_USE_CURSOR;
process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = '1';
process.env.MEMIND_AIDER_SKILL_USE_CURSOR = '1';
try {
const handler = createPostAgentRunsHandler({
userAuth: {
async ownsSession() {
return true;
},
},
cursorExecutorPolicyService: {
async getEffectivePolicy(userId) {
return {
enabled: true,
userAllowlist: ['user-1'],
channelAllowlist: ['h5'],
intentAllowlist: ['page.generate'],
userAllowed: userId === 'user-1',
};
},
},
agentRunGateway: {
async createRun(userId, payload) {
created.push({ userId, payload });
return { id: 'run-cursor-1', status: 'queued' };
},
},
});
const res = createResponseRecorder();
await handler(
{
currentUser: { id: 'user-1', username: 'john' },
body: {
session_id: 'session-1',
request_id: 'req-page-2',
user_message: {
role: 'user',
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
metadata: { displayText: '帮我写首诗,并做成页面' },
},
},
},
res,
);
assert.equal(res.statusCode, 202);
assert.equal(created[0].payload.toolMode, 'code');
assert.equal(created[0].payload.taskType, 'h5_chat_code_task');
assert.equal(created[0].payload.userMessage?.metadata?.memindRun?.executor, 'cursor');
assert.equal(created[0].payload.userMessage?.metadata?.memindRun?.pageCursorDefault, true);
assert.equal(created[0].payload.userMessage?.metadata?.memindRun?.cursorChannel, true);
} finally {
if (prevCursorEnabled == null) delete process.env.MEMIND_CURSOR_EXECUTOR_ENABLED;
else process.env.MEMIND_CURSOR_EXECUTOR_ENABLED = prevCursorEnabled;
if (prevUseCursor == null) delete process.env.MEMIND_AIDER_SKILL_USE_CURSOR;
else process.env.MEMIND_AIDER_SKILL_USE_CURSOR = prevUseCursor;
}
});
+2 -1
View File
@@ -27,8 +27,9 @@ export function resolvePreferredCodeExecutor(env = process.env) {
}
export function cursorPageTasksDefaultEnabled(env = process.env) {
// 已废弃:页面任务是否走 Cursor 由 memindadm「智趣体验通道」白名单控制,不再靠此 env 全员开启。
if (!cursorExecutorEnabled(env)) return false;
return envFlag(env.MEMIND_CURSOR_PAGE_TASKS_DEFAULT, true);
return envFlag(env.MEMIND_CURSOR_PAGE_TASKS_DEFAULT, false);
}
/** @deprecated 普通 Agent 编排不再默认走 Cursor;仅页面/问卷/Excel 等代码任务使用 enforcePageGenerationCursorRuntime */
+8 -2
View File
@@ -11,6 +11,7 @@ import {
} from './chat-skills.mjs';
import {
cursorAgentTasksDefaultEnabled,
cursorExecutorEnabled,
cursorPageTasksDefaultEnabled,
resolvePreferredCodeExecutor,
} from './cursor-agent-launch.mjs';
@@ -61,8 +62,10 @@ export function resolveCursorTaskKind(taskText, skill = '') {
export function isPageCursorDefaultCandidate(userMessage, {
rawToolMode = 'chat',
env = process.env,
channelEligible = false,
} = {}) {
if (!cursorPageTasksDefaultEnabled(env)) return false;
if (!cursorExecutorEnabled(env)) return false;
if (!channelEligible) return false;
if (String(rawToolMode ?? '').trim().toLowerCase() === 'code') return false;
const skill = selectedChatSkill(userMessage);
@@ -178,8 +181,9 @@ export function enforcePageGenerationCursorRuntime(userMessage, {
taskType = null,
forceDeepReasoning = false,
env = process.env,
channelEligible = false,
} = {}) {
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env })) {
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env, channelEligible })) {
return {
userMessage,
rawToolMode,
@@ -216,6 +220,8 @@ export function enforcePageGenerationCursorRuntime(userMessage, {
executor: codeExecutor,
pageCursorDefault: true,
cursorTaskKind: taskKind,
cursorChannel: true,
channel: 'h5',
suggestedDelivery: resolveCursorSuggestedDelivery(taskKind),
selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill),
};
+17 -8
View File
@@ -18,7 +18,7 @@ test('page cursor default matches poem page requests', () => {
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
metadata: { displayText: '帮我写首诗,并做成页面', userVisible: true },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: true }), true);
assert.equal(resolveCursorTaskKind('帮我写首诗,并做成页面'), 'page_generation');
});
@@ -28,7 +28,7 @@ test('page cursor default matches survey page-data requests', () => {
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
metadata: { displayText: '帮我做一个调查问卷,结果存 PG' },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: true }), true);
assert.equal(resolveCursorTaskKind('帮我做一个调查问卷,结果存 PG'), 'page_data');
});
@@ -38,7 +38,7 @@ test('page cursor default matches excel analysis requests', () => {
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: true }), true);
assert.equal(resolveCursorTaskKind('帮我分析这个 Excel 表格的数据趋势'), 'excel_analysis');
});
@@ -48,7 +48,7 @@ test('page cursor default skips generic empty page requests', () => {
content: [{ type: 'text', text: '帮我做一个页面吧' }],
metadata: { displayText: '帮我做一个页面吧' },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false);
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: true }), false);
});
test('page cursor default skips page-data dev repair requests', () => {
@@ -57,7 +57,7 @@ test('page cursor default skips page-data dev repair requests', () => {
content: [{ type: 'text', text: '问卷页面提交失败,帮我排查 Page Data 绑定' }],
metadata: { displayText: '问卷页面提交失败,帮我排查 Page Data 绑定' },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false);
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: true }), false);
});
test('enforcePageGenerationCursorRuntime switches page generation to code cursor executor', () => {
@@ -66,7 +66,7 @@ test('enforcePageGenerationCursorRuntime switches page generation to code cursor
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
metadata: { displayText: '帮我写首诗,并做成页面' },
};
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv, channelEligible: true });
assert.equal(enforced.rawToolMode, 'code');
assert.equal(enforced.taskType, 'h5_chat_code_task');
assert.equal(enforced.requiredExecutor, 'cursor');
@@ -86,7 +86,7 @@ test('enforcePageGenerationCursorRuntime switches survey tasks to cursor page-da
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
metadata: { displayText: '帮我做一个调查问卷,结果存 PG' },
};
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv, channelEligible: true });
assert.equal(enforced.rawToolMode, 'code');
assert.equal(enforced.requiredExecutor, 'cursor');
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_data');
@@ -102,7 +102,7 @@ test('enforcePageGenerationCursorRuntime switches excel analysis to cursor flow'
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' },
};
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv, channelEligible: true });
assert.equal(enforced.rawToolMode, 'code');
assert.equal(enforced.requiredExecutor, 'cursor');
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'excel_analysis');
@@ -110,6 +110,15 @@ test('enforcePageGenerationCursorRuntime switches excel analysis to cursor flow'
assert.match(enforced.userMessage.content[0].text, /Excel analysis task via TKMind 智趣 executor/);
});
test('page cursor default is disabled without channelEligible even when executor env is on', () => {
const userMessage = {
role: 'user',
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
metadata: { displayText: '帮我写首诗,并做成页面' },
};
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv, channelEligible: false }), false);
});
test('page cursor default is disabled without env flag', () => {
const userMessage = {
role: 'user',
+1
View File
@@ -1407,6 +1407,7 @@ attachPortalAgentRuntimeRoutes(api, {
getSessionAccess: () => sessionAccess,
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
getCodeRunPolicyService: () => agentCodeRunPolicyService,
getCursorExecutorPolicyService: () => wechatCursorExecutorPolicyService,
getUserToken: userToken,
getDeepSearchInternalSecret: () =>
DEEP_SEARCH_INTERNAL_SECRET,
+2
View File
@@ -42,6 +42,7 @@ export function attachPortalAgentRuntimeRoutes(
getSessionAccess = () => null,
getMindSpaceAssetAgent = () => null,
getCodeRunPolicyService = () => null,
getCursorExecutorPolicyService = () => null,
getUserToken = () => null,
getDeepSearchInternalSecret = () => null,
bearerToken = () => null,
@@ -133,6 +134,7 @@ export function attachPortalAgentRuntimeRoutes(
agentRunGateway,
mindSpaceAssetAgent: getMindSpaceAssetAgent(),
codeRunPolicyService: getCodeRunPolicyService(),
cursorExecutorPolicyService: getCursorExecutorPolicyService(),
goalRunService: getGoalRunService(),
chatIntentRouter: getChatIntentRouter(),
templateCatalogService: getTemplateCatalog(),
+1
View File
@@ -65,6 +65,7 @@ export async function executeWechatCursorAgentRun({
rawToolMode: 'code',
taskType: 'wechat_page_generate',
env: process.env,
channelEligible: true,
});
userMessage = cursorRuntime.userMessage;
+22
View File
@@ -4,11 +4,18 @@ const POLICY_SOURCE_DEFAULT = 'default';
const POLICY_SOURCE_ADMIN_DB = 'admin-db';
const DEFAULT_INTENT_ALLOWLIST = Object.freeze(['page.generate']);
const DEFAULT_CHANNEL_ALLOWLIST = Object.freeze(['h5', 'wechat_mp']);
export const CURSOR_EXECUTOR_CHANNEL = Object.freeze({
H5: 'h5',
WECHAT_MP: 'wechat_mp',
});
function defaultConfigShape() {
return {
enabled: false,
userAllowlist: [],
channelAllowlist: [...DEFAULT_CHANNEL_ALLOWLIST],
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
fallbackToDeepseek: true,
meta: {
@@ -58,6 +65,10 @@ function mergePatch(currentConfig, patch = {}) {
const next = cloneConfig(currentConfig);
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
if ('userAllowlist' in patch) next.userAllowlist = normalizeStringList(patch.userAllowlist);
if ('channelAllowlist' in patch) {
const channels = normalizeStringList(patch.channelAllowlist);
next.channelAllowlist = channels.length ? channels : [...DEFAULT_CHANNEL_ALLOWLIST];
}
if ('intentAllowlist' in patch) {
const intents = normalizeStringList(patch.intentAllowlist);
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
@@ -73,15 +84,26 @@ function mergePatch(currentConfig, patch = {}) {
function flattenPolicy(config, source) {
const intentAllowlist = normalizeStringList(config.intentAllowlist);
const channelAllowlist = normalizeStringList(config.channelAllowlist);
return {
source,
enabled: Boolean(config.enabled),
userAllowlist: normalizeStringList(config.userAllowlist),
channelAllowlist: channelAllowlist.length ? channelAllowlist : [...DEFAULT_CHANNEL_ALLOWLIST],
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
fallbackToDeepseek: config.fallbackToDeepseek !== false,
};
}
export function isChannelAllowedByCursorPolicy(channel, policy) {
const allowlist = normalizeStringList(policy?.channelAllowlist ?? DEFAULT_CHANNEL_ALLOWLIST)
.map((item) => item.toLowerCase());
if (allowlist.length === 0) return false;
const normalized = String(channel ?? '').trim().toLowerCase();
if (!normalized) return false;
return allowlist.includes(normalized);
}
export function isUserAllowedByWechatCursorPolicy(user, policy) {
if (!policy?.enabled) return false;
const allowlist = normalizeStringList(policy.userAllowlist).map((item) => item.toLowerCase());
+29 -5
View File
@@ -1,17 +1,41 @@
import {
CURSOR_EXECUTOR_CHANNEL,
isChannelAllowedByCursorPolicy,
isIntentAllowedByWechatCursorPolicy,
isUserAllowedByWechatCursorPolicy,
} from './wechat-cursor-executor-admin-config.mjs';
export { CURSOR_EXECUTOR_CHANNEL };
export function resolveCursorChannelEligible({
user = null,
userId = null,
channel = '',
intentKind = '',
policy = null,
} = {}) {
if (!policy?.enabled) return false;
const subject = user ?? { userId };
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
if (!isChannelAllowedByCursorPolicy(channel, policy)) return false;
const normalizedChannel = String(channel ?? '').trim().toLowerCase();
if (normalizedChannel === CURSOR_EXECUTOR_CHANNEL.WECHAT_MP) {
if (!isIntentAllowedByWechatCursorPolicy(intentKind, policy)) return false;
}
return true;
}
export function resolveWechatCursorExecutorEligible({
user = null,
userId = null,
intentKind = '',
policy = null,
} = {}) {
if (!policy?.enabled) return false;
const subject = user ?? { userId };
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
if (!isIntentAllowedByWechatCursorPolicy(intentKind, policy)) return false;
return true;
return resolveCursorChannelEligible({
user,
userId,
channel: CURSOR_EXECUTOR_CHANNEL.WECHAT_MP,
intentKind,
policy,
});
}
+60 -1
View File
@@ -2,10 +2,15 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {
createWechatCursorExecutorAdminConfigService,
isChannelAllowedByCursorPolicy,
isUserAllowedByWechatCursorPolicy,
isIntentAllowedByWechatCursorPolicy,
} from './wechat-cursor-executor-admin-config.mjs';
import { resolveWechatCursorExecutorEligible } from './wechat-cursor-executor-policy.mjs';
import {
CURSOR_EXECUTOR_CHANNEL,
resolveCursorChannelEligible,
resolveWechatCursorExecutorEligible,
} from './wechat-cursor-executor-policy.mjs';
function createMemoryPool() {
const rows = new Map();
@@ -89,3 +94,57 @@ test('isIntentAllowedByWechatCursorPolicy respects allowlist', () => {
assert.equal(isIntentAllowedByWechatCursorPolicy('page.generate', policy), true);
assert.equal(isIntentAllowedByWechatCursorPolicy('chat.general', policy), false);
});
test('H5 channel does not require intent allowlist', async () => {
const pool = createMemoryPool();
const service = createWechatCursorExecutorAdminConfigService(pool);
await service.updateAdminConfig({
enabled: true,
userAllowlist: ['john-uuid'],
channelAllowlist: ['h5', 'wechat_mp'],
}, { updatedBy: 'admin-1' });
const policy = await service.getEffectivePolicy('john-uuid', { userId: 'john-uuid' });
assert.equal(
resolveCursorChannelEligible({
user: { userId: 'john-uuid' },
channel: CURSOR_EXECUTOR_CHANNEL.H5,
policy,
}),
true,
);
});
test('H5 channel respects channelAllowlist', async () => {
const pool = createMemoryPool();
const service = createWechatCursorExecutorAdminConfigService(pool);
await service.updateAdminConfig({
enabled: true,
userAllowlist: ['john-uuid'],
channelAllowlist: ['wechat_mp'],
}, { updatedBy: 'admin-1' });
const policy = await service.getEffectivePolicy('john-uuid', { userId: 'john-uuid' });
assert.equal(
resolveCursorChannelEligible({
user: { userId: 'john-uuid' },
channel: CURSOR_EXECUTOR_CHANNEL.H5,
policy,
}),
false,
);
assert.equal(
resolveCursorChannelEligible({
user: { userId: 'john-uuid' },
channel: CURSOR_EXECUTOR_CHANNEL.WECHAT_MP,
intentKind: 'page.generate',
policy,
}),
true,
);
});
test('isChannelAllowedByCursorPolicy defaults to both channels', () => {
const policy = { channelAllowlist: ['h5', 'wechat_mp'] };
assert.equal(isChannelAllowedByCursorPolicy('h5', policy), true);
assert.equal(isChannelAllowedByCursorPolicy('wechat_mp', policy), true);
assert.equal(isChannelAllowedByCursorPolicy('other', policy), false);
});