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_AGENT_BIN=/Users/john/.local/bin/agent
|
||||||
# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1
|
# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1
|
||||||
|
|
||||||
# Track C: Goose 编排 + Cursor 作为 code executor(MindSpace 落盘,推荐 PoC)
|
# Track C: TKMind 智趣(Cursor)作为 code executor — 仅对白名单用户生效(memindadm「智趣体验通道」)
|
||||||
# MEMIND_CURSOR_EXECUTOR_ENABLED=1
|
# MEMIND_CURSOR_EXECUTOR_ENABLED=1 # 服务端 Cursor CLI 基础设施开关
|
||||||
# MEMIND_AIDER_SKILL_USE_CURSOR=1
|
# MEMIND_AIDER_SKILL_USE_CURSOR=1
|
||||||
# 默认将页面生成、问卷(Page Data)、Excel 分析路由到 Cursor 执行
|
# 已废弃全员路由:页面/问卷/Excel 是否走 Cursor 由 memindadm 白名单控制,不再靠 MEMIND_CURSOR_PAGE_TASKS_DEFAULT
|
||||||
# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=1
|
# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=0
|
||||||
# 已废弃:不再把普通 Agent 编排全量切到 Cursor;简单对话走 DeepSeek direct_chat
|
# 已废弃:不再把普通 Agent 编排全量切到 Cursor;简单对话走 DeepSeek direct_chat
|
||||||
# MEMIND_CURSOR_AGENT_TASKS_DEFAULT=0
|
# MEMIND_CURSOR_AGENT_TASKS_DEFAULT=0
|
||||||
# MEMIND_CURSOR_DEEPSEEK_FALLBACK=1
|
# MEMIND_CURSOR_DEEPSEEK_FALLBACK=1
|
||||||
|
|||||||
@@ -955,6 +955,23 @@ export function createAdminApi({
|
|||||||
adminApi.put('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
|
adminApi.put('/wechat/cursor-executor/config', requireAdmin, updateWechatCursorExecutorConfig);
|
||||||
adminApi.patch('/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) => {
|
adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => {
|
||||||
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
||||||
res.json({ catalog: llmProviderService.catalog });
|
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 { resolveGoalBindingForAgentRun } from './goal-run-resolve.mjs';
|
||||||
import { enforcePageGenerationCursorRuntime } from './cursor-page-routing.mjs';
|
import { enforcePageGenerationCursorRuntime } from './cursor-page-routing.mjs';
|
||||||
import { resolvePreferredCodeExecutor } from './cursor-agent-launch.mjs';
|
import { resolvePreferredCodeExecutor } from './cursor-agent-launch.mjs';
|
||||||
|
import {
|
||||||
|
CURSOR_EXECUTOR_CHANNEL,
|
||||||
|
resolveCursorChannelEligible,
|
||||||
|
} from './wechat-cursor-executor-policy.mjs';
|
||||||
|
|
||||||
function envFlag(value) {
|
function envFlag(value) {
|
||||||
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
||||||
@@ -188,6 +192,7 @@ export function createPostAgentRunsHandler({
|
|||||||
agentRunGateway,
|
agentRunGateway,
|
||||||
mindSpaceAssetAgent = null,
|
mindSpaceAssetAgent = null,
|
||||||
codeRunPolicyService = null,
|
codeRunPolicyService = null,
|
||||||
|
cursorExecutorPolicyService = null,
|
||||||
goalRunService = null,
|
goalRunService = null,
|
||||||
chatIntentRouter = null,
|
chatIntentRouter = null,
|
||||||
templateCatalogService = null,
|
templateCatalogService = null,
|
||||||
@@ -259,12 +264,30 @@ export function createPostAgentRunsHandler({
|
|||||||
let requiredExecutor = selectedSkillRuntime.requiredExecutor ?? null;
|
let requiredExecutor = selectedSkillRuntime.requiredExecutor ?? null;
|
||||||
let requiredReviewExecutor = selectedSkillRuntime.requiredReviewExecutor ?? 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) {
|
if (!requiredExecutor && !requiredReviewExecutor) {
|
||||||
const pageCursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
|
const pageCursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
|
||||||
rawToolMode,
|
rawToolMode,
|
||||||
taskType,
|
taskType,
|
||||||
forceDeepReasoning,
|
forceDeepReasoning,
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
channelEligible: cursorChannelEligible,
|
||||||
});
|
});
|
||||||
userMessage = pageCursorRuntime.userMessage;
|
userMessage = pageCursorRuntime.userMessage;
|
||||||
rawToolMode = pageCursorRuntime.rawToolMode;
|
rawToolMode = pageCursorRuntime.rawToolMode;
|
||||||
@@ -307,29 +330,36 @@ export function createPostAgentRunsHandler({
|
|||||||
const policyTaskType = requiredReviewExecutor
|
const policyTaskType = requiredReviewExecutor
|
||||||
? 'h5_chat_code_task'
|
? 'h5_chat_code_task'
|
||||||
: taskType;
|
: taskType;
|
||||||
if (!codeRunPolicy.enabled) {
|
const isCursorChannelRun = cursorChannelEligible && (
|
||||||
response.status(403).json({ message: '代码任务灰度未开启' });
|
requiredExecutor === 'cursor'
|
||||||
return;
|
|| userMessage?.metadata?.memindRun?.pageCursorDefault === true
|
||||||
}
|
);
|
||||||
if (!codeRunPolicy.userAllowed) {
|
if (!isCursorChannelRun) {
|
||||||
response.status(403).json({ message: '当前用户未开启代码任务灰度' });
|
if (!codeRunPolicy.enabled) {
|
||||||
return;
|
response.status(403).json({ message: '代码任务灰度未开启' });
|
||||||
}
|
return;
|
||||||
const taskTypeAllowlist = codeRunPolicy.taskTypeAllowlist ?? [];
|
}
|
||||||
if (
|
if (!codeRunPolicy.userAllowed) {
|
||||||
taskTypeAllowlist.length > 0 &&
|
response.status(403).json({ message: '当前用户未开启代码任务灰度' });
|
||||||
(
|
return;
|
||||||
!policyTaskType ||
|
}
|
||||||
!taskTypeAllowlist
|
const taskTypeAllowlist = codeRunPolicy.taskTypeAllowlist ?? [];
|
||||||
.map((item) => String(item).toLowerCase())
|
if (
|
||||||
.includes(policyTaskType.toLowerCase())
|
taskTypeAllowlist.length > 0 &&
|
||||||
)
|
(
|
||||||
) {
|
!policyTaskType ||
|
||||||
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
|
!taskTypeAllowlist
|
||||||
return;
|
.map((item) => String(item).toLowerCase())
|
||||||
|
.includes(policyTaskType.toLowerCase())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
codeRunPolicy.requireValidation
|
!isCursorChannelRun
|
||||||
|
&& codeRunPolicy.requireValidation
|
||||||
&& !hasExpectedFileValidation(userMessage)
|
&& !hasExpectedFileValidation(userMessage)
|
||||||
&& !userMessage?.metadata?.memindRun?.pageCursorDefault
|
&& !userMessage?.metadata?.memindRun?.pageCursorDefault
|
||||||
&& !userMessage?.metadata?.memindRun?.cursorAgentDefault
|
&& !userMessage?.metadata?.memindRun?.cursorAgentDefault
|
||||||
|
|||||||
@@ -1091,3 +1091,128 @@ test('POST /agent/runs materializes selected assets before creating run', async
|
|||||||
assetIds: ['asset-9'],
|
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) {
|
export function cursorPageTasksDefaultEnabled(env = process.env) {
|
||||||
|
// 已废弃:页面任务是否走 Cursor 由 memindadm「智趣体验通道」白名单控制,不再靠此 env 全员开启。
|
||||||
if (!cursorExecutorEnabled(env)) return false;
|
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 */
|
/** @deprecated 普通 Agent 编排不再默认走 Cursor;仅页面/问卷/Excel 等代码任务使用 enforcePageGenerationCursorRuntime */
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from './chat-skills.mjs';
|
} from './chat-skills.mjs';
|
||||||
import {
|
import {
|
||||||
cursorAgentTasksDefaultEnabled,
|
cursorAgentTasksDefaultEnabled,
|
||||||
|
cursorExecutorEnabled,
|
||||||
cursorPageTasksDefaultEnabled,
|
cursorPageTasksDefaultEnabled,
|
||||||
resolvePreferredCodeExecutor,
|
resolvePreferredCodeExecutor,
|
||||||
} from './cursor-agent-launch.mjs';
|
} from './cursor-agent-launch.mjs';
|
||||||
@@ -61,8 +62,10 @@ export function resolveCursorTaskKind(taskText, skill = '') {
|
|||||||
export function isPageCursorDefaultCandidate(userMessage, {
|
export function isPageCursorDefaultCandidate(userMessage, {
|
||||||
rawToolMode = 'chat',
|
rawToolMode = 'chat',
|
||||||
env = process.env,
|
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;
|
if (String(rawToolMode ?? '').trim().toLowerCase() === 'code') return false;
|
||||||
|
|
||||||
const skill = selectedChatSkill(userMessage);
|
const skill = selectedChatSkill(userMessage);
|
||||||
@@ -178,8 +181,9 @@ export function enforcePageGenerationCursorRuntime(userMessage, {
|
|||||||
taskType = null,
|
taskType = null,
|
||||||
forceDeepReasoning = false,
|
forceDeepReasoning = false,
|
||||||
env = process.env,
|
env = process.env,
|
||||||
|
channelEligible = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env })) {
|
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env, channelEligible })) {
|
||||||
return {
|
return {
|
||||||
userMessage,
|
userMessage,
|
||||||
rawToolMode,
|
rawToolMode,
|
||||||
@@ -216,6 +220,8 @@ export function enforcePageGenerationCursorRuntime(userMessage, {
|
|||||||
executor: codeExecutor,
|
executor: codeExecutor,
|
||||||
pageCursorDefault: true,
|
pageCursorDefault: true,
|
||||||
cursorTaskKind: taskKind,
|
cursorTaskKind: taskKind,
|
||||||
|
cursorChannel: true,
|
||||||
|
channel: 'h5',
|
||||||
suggestedDelivery: resolveCursorSuggestedDelivery(taskKind),
|
suggestedDelivery: resolveCursorSuggestedDelivery(taskKind),
|
||||||
selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill),
|
selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test('page cursor default matches poem page requests', () => {
|
|||||||
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
||||||
metadata: { displayText: '帮我写首诗,并做成页面', userVisible: true },
|
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');
|
assert.equal(resolveCursorTaskKind('帮我写首诗,并做成页面'), 'page_generation');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ test('page cursor default matches survey page-data requests', () => {
|
|||||||
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
||||||
metadata: { displayText: '帮我做一个调查问卷,结果存 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');
|
assert.equal(resolveCursorTaskKind('帮我做一个调查问卷,结果存 PG'), 'page_data');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ test('page cursor default matches excel analysis requests', () => {
|
|||||||
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
|
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
|
||||||
metadata: { displayText: '帮我分析这个 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');
|
assert.equal(resolveCursorTaskKind('帮我分析这个 Excel 表格的数据趋势'), 'excel_analysis');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ test('page cursor default skips generic empty page requests', () => {
|
|||||||
content: [{ type: 'text', text: '帮我做一个页面吧' }],
|
content: [{ type: 'text', text: '帮我做一个页面吧' }],
|
||||||
metadata: { displayText: '帮我做一个页面吧' },
|
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', () => {
|
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 绑定' }],
|
content: [{ type: 'text', text: '问卷页面提交失败,帮我排查 Page Data 绑定' }],
|
||||||
metadata: { displayText: '问卷页面提交失败,帮我排查 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', () => {
|
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: '帮我写首诗,并做成页面' }],
|
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
||||||
metadata: { displayText: '帮我写首诗,并做成页面' },
|
metadata: { displayText: '帮我写首诗,并做成页面' },
|
||||||
};
|
};
|
||||||
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
|
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv, channelEligible: true });
|
||||||
assert.equal(enforced.rawToolMode, 'code');
|
assert.equal(enforced.rawToolMode, 'code');
|
||||||
assert.equal(enforced.taskType, 'h5_chat_code_task');
|
assert.equal(enforced.taskType, 'h5_chat_code_task');
|
||||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||||
@@ -86,7 +86,7 @@ test('enforcePageGenerationCursorRuntime switches survey tasks to cursor page-da
|
|||||||
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
||||||
metadata: { displayText: '帮我做一个调查问卷,结果存 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.rawToolMode, 'code');
|
||||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||||
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_data');
|
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 表格的数据趋势' }],
|
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
|
||||||
metadata: { displayText: '帮我分析这个 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.rawToolMode, 'code');
|
||||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||||
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'excel_analysis');
|
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/);
|
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', () => {
|
test('page cursor default is disabled without env flag', () => {
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
|||||||
@@ -1407,6 +1407,7 @@ attachPortalAgentRuntimeRoutes(api, {
|
|||||||
getSessionAccess: () => sessionAccess,
|
getSessionAccess: () => sessionAccess,
|
||||||
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
|
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
|
||||||
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
||||||
|
getCursorExecutorPolicyService: () => wechatCursorExecutorPolicyService,
|
||||||
getUserToken: userToken,
|
getUserToken: userToken,
|
||||||
getDeepSearchInternalSecret: () =>
|
getDeepSearchInternalSecret: () =>
|
||||||
DEEP_SEARCH_INTERNAL_SECRET,
|
DEEP_SEARCH_INTERNAL_SECRET,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function attachPortalAgentRuntimeRoutes(
|
|||||||
getSessionAccess = () => null,
|
getSessionAccess = () => null,
|
||||||
getMindSpaceAssetAgent = () => null,
|
getMindSpaceAssetAgent = () => null,
|
||||||
getCodeRunPolicyService = () => null,
|
getCodeRunPolicyService = () => null,
|
||||||
|
getCursorExecutorPolicyService = () => null,
|
||||||
getUserToken = () => null,
|
getUserToken = () => null,
|
||||||
getDeepSearchInternalSecret = () => null,
|
getDeepSearchInternalSecret = () => null,
|
||||||
bearerToken = () => null,
|
bearerToken = () => null,
|
||||||
@@ -133,6 +134,7 @@ export function attachPortalAgentRuntimeRoutes(
|
|||||||
agentRunGateway,
|
agentRunGateway,
|
||||||
mindSpaceAssetAgent: getMindSpaceAssetAgent(),
|
mindSpaceAssetAgent: getMindSpaceAssetAgent(),
|
||||||
codeRunPolicyService: getCodeRunPolicyService(),
|
codeRunPolicyService: getCodeRunPolicyService(),
|
||||||
|
cursorExecutorPolicyService: getCursorExecutorPolicyService(),
|
||||||
goalRunService: getGoalRunService(),
|
goalRunService: getGoalRunService(),
|
||||||
chatIntentRouter: getChatIntentRouter(),
|
chatIntentRouter: getChatIntentRouter(),
|
||||||
templateCatalogService: getTemplateCatalog(),
|
templateCatalogService: getTemplateCatalog(),
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export async function executeWechatCursorAgentRun({
|
|||||||
rawToolMode: 'code',
|
rawToolMode: 'code',
|
||||||
taskType: 'wechat_page_generate',
|
taskType: 'wechat_page_generate',
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
channelEligible: true,
|
||||||
});
|
});
|
||||||
userMessage = cursorRuntime.userMessage;
|
userMessage = cursorRuntime.userMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ const POLICY_SOURCE_DEFAULT = 'default';
|
|||||||
const POLICY_SOURCE_ADMIN_DB = 'admin-db';
|
const POLICY_SOURCE_ADMIN_DB = 'admin-db';
|
||||||
|
|
||||||
const DEFAULT_INTENT_ALLOWLIST = Object.freeze(['page.generate']);
|
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() {
|
function defaultConfigShape() {
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
userAllowlist: [],
|
userAllowlist: [],
|
||||||
|
channelAllowlist: [...DEFAULT_CHANNEL_ALLOWLIST],
|
||||||
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
|
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
|
||||||
fallbackToDeepseek: true,
|
fallbackToDeepseek: true,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -58,6 +65,10 @@ function mergePatch(currentConfig, patch = {}) {
|
|||||||
const next = cloneConfig(currentConfig);
|
const next = cloneConfig(currentConfig);
|
||||||
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
|
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
|
||||||
if ('userAllowlist' in patch) next.userAllowlist = normalizeStringList(patch.userAllowlist);
|
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) {
|
if ('intentAllowlist' in patch) {
|
||||||
const intents = normalizeStringList(patch.intentAllowlist);
|
const intents = normalizeStringList(patch.intentAllowlist);
|
||||||
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
|
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
|
||||||
@@ -73,15 +84,26 @@ function mergePatch(currentConfig, patch = {}) {
|
|||||||
|
|
||||||
function flattenPolicy(config, source) {
|
function flattenPolicy(config, source) {
|
||||||
const intentAllowlist = normalizeStringList(config.intentAllowlist);
|
const intentAllowlist = normalizeStringList(config.intentAllowlist);
|
||||||
|
const channelAllowlist = normalizeStringList(config.channelAllowlist);
|
||||||
return {
|
return {
|
||||||
source,
|
source,
|
||||||
enabled: Boolean(config.enabled),
|
enabled: Boolean(config.enabled),
|
||||||
userAllowlist: normalizeStringList(config.userAllowlist),
|
userAllowlist: normalizeStringList(config.userAllowlist),
|
||||||
|
channelAllowlist: channelAllowlist.length ? channelAllowlist : [...DEFAULT_CHANNEL_ALLOWLIST],
|
||||||
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
|
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
|
||||||
fallbackToDeepseek: config.fallbackToDeepseek !== false,
|
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) {
|
export function isUserAllowedByWechatCursorPolicy(user, policy) {
|
||||||
if (!policy?.enabled) return false;
|
if (!policy?.enabled) return false;
|
||||||
const allowlist = normalizeStringList(policy.userAllowlist).map((item) => item.toLowerCase());
|
const allowlist = normalizeStringList(policy.userAllowlist).map((item) => item.toLowerCase());
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
import {
|
import {
|
||||||
|
CURSOR_EXECUTOR_CHANNEL,
|
||||||
|
isChannelAllowedByCursorPolicy,
|
||||||
isIntentAllowedByWechatCursorPolicy,
|
isIntentAllowedByWechatCursorPolicy,
|
||||||
isUserAllowedByWechatCursorPolicy,
|
isUserAllowedByWechatCursorPolicy,
|
||||||
} from './wechat-cursor-executor-admin-config.mjs';
|
} 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({
|
export function resolveWechatCursorExecutorEligible({
|
||||||
user = null,
|
user = null,
|
||||||
userId = null,
|
userId = null,
|
||||||
intentKind = '',
|
intentKind = '',
|
||||||
policy = null,
|
policy = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!policy?.enabled) return false;
|
return resolveCursorChannelEligible({
|
||||||
const subject = user ?? { userId };
|
user,
|
||||||
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
|
userId,
|
||||||
if (!isIntentAllowedByWechatCursorPolicy(intentKind, policy)) return false;
|
channel: CURSOR_EXECUTOR_CHANNEL.WECHAT_MP,
|
||||||
return true;
|
intentKind,
|
||||||
|
policy,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
createWechatCursorExecutorAdminConfigService,
|
createWechatCursorExecutorAdminConfigService,
|
||||||
|
isChannelAllowedByCursorPolicy,
|
||||||
isUserAllowedByWechatCursorPolicy,
|
isUserAllowedByWechatCursorPolicy,
|
||||||
isIntentAllowedByWechatCursorPolicy,
|
isIntentAllowedByWechatCursorPolicy,
|
||||||
} from './wechat-cursor-executor-admin-config.mjs';
|
} 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() {
|
function createMemoryPool() {
|
||||||
const rows = new Map();
|
const rows = new Map();
|
||||||
@@ -89,3 +94,57 @@ test('isIntentAllowedByWechatCursorPolicy respects allowlist', () => {
|
|||||||
assert.equal(isIntentAllowedByWechatCursorPolicy('page.generate', policy), true);
|
assert.equal(isIntentAllowedByWechatCursorPolicy('page.generate', policy), true);
|
||||||
assert.equal(isIntentAllowedByWechatCursorPolicy('chat.general', policy), false);
|
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