Isolate Cursor executor to admin-configured whitelist channel for H5 and WeChat.
Memind CI / Test, build, and release guards (push) Failing after 4m22s
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:
+4
-4
@@ -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 executor(MindSpace 落盘,推荐 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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1407,6 +1407,7 @@ attachPortalAgentRuntimeRoutes(api, {
|
||||
getSessionAccess: () => sessionAccess,
|
||||
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
|
||||
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
||||
getCursorExecutorPolicyService: () => wechatCursorExecutorPolicyService,
|
||||
getUserToken: userToken,
|
||||
getDeepSearchInternalSecret: () =>
|
||||
DEEP_SEARCH_INTERNAL_SECRET,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function executeWechatCursorAgentRun({
|
||||
rawToolMode: 'code',
|
||||
taskType: 'wechat_page_generate',
|
||||
env: process.env,
|
||||
channelEligible: true,
|
||||
});
|
||||
userMessage = cursorRuntime.userMessage;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user