Add per-feature Cursor channel toggles for admin and runtime.
Memind CI / Test, build, and release guards (push) Has been cancelled
Memind CI / Test, build, and release guards (push) Has been cancelled
Expose page/data/scheduled-task/chat-bridge switches in the智趣体验通道 config so Tang can roll out Cursor paths independently with DeepSeek fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -388,6 +388,22 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
|
||||
# VITE_TKMIND_MODEL=gpt-4o
|
||||
|
||||
# 可选:harness recall 的默认查询
|
||||
|
||||
# Harness 控制面(只读 shadow 投影 / 失败分类 / 对账;默认全部关闭)
|
||||
# MEMIND_HARNESS_SCHEMA_REQUIRED=0
|
||||
# MEMIND_HARNESS_PROJECTOR_ENABLED=0
|
||||
# MEMIND_HARNESS_RECONCILER_ENABLED=0
|
||||
# MEMIND_HARNESS_ADMIN_API_ENABLED=0
|
||||
# MEMIND_HARNESS_SOURCE_EVENTS_ENABLED=0
|
||||
# MEMIND_HARNESS_CASES_ENABLED=0
|
||||
# MEMIND_HARNESS_REPLAY_ENABLED=0
|
||||
# MEMIND_HARNESS_REPLAY_ENV_GATE=0
|
||||
# HARNESS_LATENESS_BUDGET_MS=900000
|
||||
# HARNESS_PROJECTOR_BATCH_SIZE=200
|
||||
# HARNESS_PROJECTOR_LEASE_MS=60000
|
||||
# HARNESS_MIGRATION_MAX_MS=120000
|
||||
# 本地 shadow 验收:node scripts/verify-harness-shadow.mjs [--project-once] [--reconcile-once]
|
||||
|
||||
# Memory V2 Phase A canary (local dev)
|
||||
# Full template: docs/memory-v2/phase-a-canary.env.example
|
||||
# Readiness check: npm run check:memory-v2-phase-a
|
||||
@@ -535,6 +551,7 @@ MEMIND_RUNTIME_PROFILE=local
|
||||
# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1
|
||||
|
||||
# Track C: TKMind 智趣(Cursor)作为 code executor — 仅对白名单用户生效(memindadm「智趣体验通道」)
|
||||
# 能力开关(pageGenerate/pageData/excelAnalysis/chatBridge/scheduledTasks)在管理后台配置,存 h5_wechat_cursor_executor_config
|
||||
# MEMIND_CURSOR_EXECUTOR_ENABLED=1 # 服务端 Cursor CLI 基础设施开关
|
||||
# MEMIND_AIDER_SKILL_USE_CURSOR=1
|
||||
# 已废弃全员路由:页面/问卷/Excel 是否走 Cursor 由 memindadm 白名单控制,不再靠 MEMIND_CURSOR_PAGE_TASKS_DEFAULT
|
||||
|
||||
@@ -265,9 +265,10 @@ export function createPostAgentRunsHandler({
|
||||
let requiredReviewExecutor = selectedSkillRuntime.requiredReviewExecutor ?? null;
|
||||
|
||||
let cursorChannelEligible = false;
|
||||
let cursorPolicy = null;
|
||||
if (cursorExecutorPolicyService?.getEffectivePolicy) {
|
||||
try {
|
||||
const cursorPolicy = await cursorExecutorPolicyService.getEffectivePolicy(
|
||||
cursorPolicy = await cursorExecutorPolicyService.getEffectivePolicy(
|
||||
request.currentUser.id,
|
||||
request.currentUser,
|
||||
);
|
||||
@@ -278,6 +279,7 @@ export function createPostAgentRunsHandler({
|
||||
});
|
||||
} catch {
|
||||
cursorChannelEligible = false;
|
||||
cursorPolicy = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +290,7 @@ export function createPostAgentRunsHandler({
|
||||
forceDeepReasoning,
|
||||
env: process.env,
|
||||
channelEligible: cursorChannelEligible,
|
||||
policy: cursorPolicy,
|
||||
});
|
||||
userMessage = pageCursorRuntime.userMessage;
|
||||
rawToolMode = pageCursorRuntime.rawToolMode;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
export const CURSOR_CHANNEL_FEATURES = Object.freeze({
|
||||
PAGE_GENERATE: 'pageGenerate',
|
||||
PAGE_DATA: 'pageData',
|
||||
EXCEL_ANALYSIS: 'excelAnalysis',
|
||||
CHAT_BRIDGE: 'chatBridge',
|
||||
SCHEDULED_TASKS: 'scheduledTasks',
|
||||
});
|
||||
|
||||
export const CURSOR_TASK_KIND = Object.freeze({
|
||||
PAGE_GENERATION: 'page_generation',
|
||||
PAGE_DATA: 'page_data',
|
||||
EXCEL_ANALYSIS: 'excel_analysis',
|
||||
});
|
||||
|
||||
const TASK_KIND_TO_FEATURE = Object.freeze({
|
||||
[CURSOR_TASK_KIND.PAGE_GENERATION]: CURSOR_CHANNEL_FEATURES.PAGE_GENERATE,
|
||||
[CURSOR_TASK_KIND.PAGE_DATA]: CURSOR_CHANNEL_FEATURES.PAGE_DATA,
|
||||
[CURSOR_TASK_KIND.EXCEL_ANALYSIS]: CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS,
|
||||
});
|
||||
|
||||
export function defaultCursorChannelFeatures() {
|
||||
return {
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS]: { enabled: false },
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFeatureFlag(value, fallback = false) {
|
||||
if (value == null) return fallback;
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'object' && value !== null && 'enabled' in value) {
|
||||
return normalizeFeatureFlag(value.enabled, fallback);
|
||||
}
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function normalizeCursorChannelFeatures(rawFeatures, {
|
||||
intentAllowlist = ['page.generate'],
|
||||
} = {}) {
|
||||
const defaults = defaultCursorChannelFeatures();
|
||||
const source = rawFeatures && typeof rawFeatures === 'object' ? rawFeatures : {};
|
||||
const legacyIntents = Array.isArray(intentAllowlist)
|
||||
? intentAllowlist.map((item) => String(item ?? '').trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
const hasExplicitFeatures = Object.keys(source).length > 0;
|
||||
return {
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: {
|
||||
enabled: hasExplicitFeatures
|
||||
? normalizeFeatureFlag(
|
||||
source[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE],
|
||||
legacyIntents.includes('page.generate'),
|
||||
)
|
||||
: legacyIntents.includes('page.generate'),
|
||||
},
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA]: {
|
||||
enabled: normalizeFeatureFlag(source[CURSOR_CHANNEL_FEATURES.PAGE_DATA], false),
|
||||
},
|
||||
[CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS]: {
|
||||
enabled: normalizeFeatureFlag(source[CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS], false),
|
||||
},
|
||||
[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE]: {
|
||||
enabled: normalizeFeatureFlag(source[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE], false),
|
||||
},
|
||||
[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS]: {
|
||||
enabled: normalizeFeatureFlag(source[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS], false),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function intentAllowlistFromFeatures(features) {
|
||||
const normalized = normalizeCursorChannelFeatures(features);
|
||||
const intents = [];
|
||||
if (normalized[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]?.enabled) {
|
||||
intents.push('page.generate');
|
||||
}
|
||||
return intents.length > 0 ? intents : ['page.generate'];
|
||||
}
|
||||
|
||||
export function isCursorFeatureEnabled(policy, featureKey) {
|
||||
if (!policy?.enabled) return false;
|
||||
const features = policy.features
|
||||
?? normalizeCursorChannelFeatures(null, { intentAllowlist: policy.intentAllowlist });
|
||||
return normalizeFeatureFlag(features?.[featureKey], false);
|
||||
}
|
||||
|
||||
export function isCursorTaskKindEnabled(policy, taskKind) {
|
||||
const featureKey = TASK_KIND_TO_FEATURE[String(taskKind ?? '').trim()];
|
||||
if (!featureKey) return false;
|
||||
return isCursorFeatureEnabled(policy, featureKey);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
CURSOR_CHANNEL_FEATURES,
|
||||
CURSOR_TASK_KIND,
|
||||
defaultCursorChannelFeatures,
|
||||
intentAllowlistFromFeatures,
|
||||
isCursorFeatureEnabled,
|
||||
isCursorTaskKindEnabled,
|
||||
normalizeCursorChannelFeatures,
|
||||
} from './cursor-channel-features.mjs';
|
||||
|
||||
test('normalizeCursorChannelFeatures derives page.generate from legacy intent allowlist', () => {
|
||||
const features = normalizeCursorChannelFeatures(null, {
|
||||
intentAllowlist: ['page.generate'],
|
||||
});
|
||||
assert.equal(features.pageGenerate.enabled, true);
|
||||
assert.equal(features.pageData.enabled, false);
|
||||
});
|
||||
|
||||
test('isCursorTaskKindEnabled respects per-feature toggles', () => {
|
||||
const policy = {
|
||||
enabled: true,
|
||||
features: {
|
||||
...defaultCursorChannelFeatures(),
|
||||
pageGenerate: { enabled: true },
|
||||
pageData: { enabled: false },
|
||||
},
|
||||
};
|
||||
assert.equal(isCursorTaskKindEnabled(policy, CURSOR_TASK_KIND.PAGE_GENERATION), true);
|
||||
assert.equal(isCursorTaskKindEnabled(policy, CURSOR_TASK_KIND.PAGE_DATA), false);
|
||||
});
|
||||
|
||||
test('intentAllowlistFromFeatures keeps wechat compatibility', () => {
|
||||
const intents = intentAllowlistFromFeatures({
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA]: { enabled: true },
|
||||
});
|
||||
assert.deepEqual(intents, ['page.generate']);
|
||||
});
|
||||
|
||||
test('isCursorFeatureEnabled requires master enabled flag', () => {
|
||||
const policy = {
|
||||
enabled: false,
|
||||
features: defaultCursorChannelFeatures(),
|
||||
};
|
||||
policy.features.pageGenerate.enabled = true;
|
||||
assert.equal(isCursorFeatureEnabled(policy, CURSOR_CHANNEL_FEATURES.PAGE_GENERATE), false);
|
||||
});
|
||||
+10
-2
@@ -15,6 +15,7 @@ import {
|
||||
cursorPageTasksDefaultEnabled,
|
||||
resolvePreferredCodeExecutor,
|
||||
} from './cursor-agent-launch.mjs';
|
||||
import { isCursorTaskKindEnabled } from './cursor-channel-features.mjs';
|
||||
|
||||
const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||||
|
||||
@@ -63,6 +64,7 @@ export function isPageCursorDefaultCandidate(userMessage, {
|
||||
rawToolMode = 'chat',
|
||||
env = process.env,
|
||||
channelEligible = false,
|
||||
policy = null,
|
||||
} = {}) {
|
||||
if (!cursorExecutorEnabled(env)) return false;
|
||||
if (!channelEligible) return false;
|
||||
@@ -77,7 +79,12 @@ export function isPageCursorDefaultCandidate(userMessage, {
|
||||
const taskText = extractAiderDevelopmentTask(userMessage);
|
||||
if (!taskText) return false;
|
||||
|
||||
return resolveCursorTaskKind(taskText, skill) !== null;
|
||||
const taskKind = resolveCursorTaskKind(taskText, skill);
|
||||
if (taskKind === null) return false;
|
||||
if (policy?.enabled) {
|
||||
return isCursorTaskKindEnabled(policy, taskKind);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildCursorPageInstruction(taskText) {
|
||||
@@ -182,8 +189,9 @@ export function enforcePageGenerationCursorRuntime(userMessage, {
|
||||
forceDeepReasoning = false,
|
||||
env = process.env,
|
||||
channelEligible = false,
|
||||
policy = null,
|
||||
} = {}) {
|
||||
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env, channelEligible })) {
|
||||
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env, channelEligible, policy })) {
|
||||
return {
|
||||
userMessage,
|
||||
rawToolMode,
|
||||
|
||||
@@ -128,6 +128,30 @@ test('page cursor default is disabled without env flag', () => {
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: {} }), false);
|
||||
});
|
||||
|
||||
test('page cursor default respects policy feature toggles', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我做一个调查问卷页面' }],
|
||||
metadata: { displayText: '帮我做一个调查问卷页面' },
|
||||
};
|
||||
const policy = {
|
||||
enabled: true,
|
||||
features: {
|
||||
pageGenerate: { enabled: false },
|
||||
pageData: { enabled: false },
|
||||
excelAnalysis: { enabled: false },
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
isPageCursorDefaultCandidate(userMessage, {
|
||||
env: enabledEnv,
|
||||
channelEligible: true,
|
||||
policy,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('applyCursorFirstAgentExecution no longer forces cursor for generic agent tasks', () => {
|
||||
const enabledEnv = {
|
||||
MEMIND_CURSOR_EXECUTOR_ENABLED: '1',
|
||||
|
||||
@@ -2258,6 +2258,46 @@ export function createLlmProviderService(
|
||||
return { ok: true, providerId: goosedProviderId, model: row.default_model, source: 'vision' };
|
||||
},
|
||||
|
||||
async applyCursorChatBridgeForSession(sessionId, fetchImpl = apiFetchImpl) {
|
||||
if (!cursorChatBridgeEnabled()) {
|
||||
return { ok: false, message: 'TKMind Chat Bridge 未启用(MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1)' };
|
||||
}
|
||||
const catalogItem = catalogById.custom_cursor;
|
||||
const profile = {
|
||||
providerId: 'custom_cursor',
|
||||
defaultModel: catalogItem?.defaultModel ?? 'cursor-chat-bridge',
|
||||
models: catalogItem?.models ?? ['cursor-chat-bridge'],
|
||||
apiKey: 'cursor-bridge-local',
|
||||
};
|
||||
const sessionGoosedApi = (pathname, init) =>
|
||||
goosedApiFetch(apiTarget, apiSecret, pathname, init, fetchImpl);
|
||||
try {
|
||||
const goosedProviderId = await syncCursorChatBridgeProfileToGoosed(
|
||||
apiTarget,
|
||||
apiSecret,
|
||||
profile,
|
||||
fetchImpl,
|
||||
);
|
||||
await updateSessionProvider(
|
||||
sessionGoosedApi,
|
||||
sessionId,
|
||||
goosedProviderId,
|
||||
profile.defaultModel,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
providerId: goosedProviderId,
|
||||
model: profile.defaultModel,
|
||||
source: 'cursor_chat_bridge',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
message: err instanceof Error ? err.message : 'Cursor Chat Bridge 同步失败',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Calls the vision provider directly (not through Goose) to analyze images.
|
||||
// Returns the model's text description, or null on failure.
|
||||
async analyzeImagesWithVision(imageItems, userText) {
|
||||
|
||||
+92
-35
@@ -14,6 +14,8 @@ import {
|
||||
materializeMissingPublicHtmlWrites,
|
||||
} from './mindspace-public-finish-sync.mjs';
|
||||
import { localDateLabel } from './schedule-time.mjs';
|
||||
import { executeCursorChannelCodeRun } from './wechat-cursor-agent-run.mjs';
|
||||
import { resolveCursorScheduledTaskEligible } from './wechat-cursor-executor-policy.mjs';
|
||||
|
||||
function messageText(message) {
|
||||
if (typeof message?.content === 'string') return message.content.trim();
|
||||
@@ -529,6 +531,8 @@ export async function resendScheduledTaskWechatForReadyPage({
|
||||
export async function executeScheduledTask(task, {
|
||||
userAuth,
|
||||
tkmindProxy,
|
||||
agentRunGateway = null,
|
||||
cursorExecutorPolicyService = null,
|
||||
sessionSnapshotService = null,
|
||||
pool = null,
|
||||
h5Root = null,
|
||||
@@ -539,13 +543,6 @@ export async function executeScheduledTask(task, {
|
||||
if (!userAuth || typeof userAuth.canUseChat !== 'function') {
|
||||
throw new Error('缺少 userAuth.canUseChat');
|
||||
}
|
||||
if (
|
||||
!tkmindProxy
|
||||
|| typeof tkmindProxy.startSessionForUser !== 'function'
|
||||
|| typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser !== 'function'
|
||||
) {
|
||||
throw new Error('缺少 tkmindProxy 会话执行能力');
|
||||
}
|
||||
|
||||
const gate = await userAuth.canUseChat(task.userId);
|
||||
if (!gate?.ok) {
|
||||
@@ -555,38 +552,97 @@ export async function executeScheduledTask(task, {
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
const started = await tkmindProxy.startSessionForUser(task.userId, {
|
||||
origin: 'h5',
|
||||
});
|
||||
const sessionId = started?.id ?? started?.sessionId;
|
||||
if (!sessionId) throw new Error('创建定时任务会话失败');
|
||||
|
||||
const userMessage = buildScheduledTaskExecutionPrompt(task);
|
||||
logger.info?.('[ScheduledTask] executing', {
|
||||
taskId: task.id,
|
||||
userId: task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
|
||||
task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
{ timeoutMs },
|
||||
);
|
||||
|
||||
let messages = await refreshScheduledTaskMessages({
|
||||
userId: task.userId,
|
||||
sessionId,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService,
|
||||
});
|
||||
|
||||
const publishDir = h5Root && task.userId
|
||||
? path.join(h5Root, 'MindSpace', task.userId)
|
||||
: null;
|
||||
|
||||
let cursorPolicy = null;
|
||||
let useCursorPath = false;
|
||||
if (agentRunGateway && cursorExecutorPolicyService?.getEffectivePolicy) {
|
||||
try {
|
||||
cursorPolicy = await cursorExecutorPolicyService.getEffectivePolicy(task.userId, {
|
||||
userId: task.userId,
|
||||
});
|
||||
useCursorPath = resolveCursorScheduledTaskEligible({
|
||||
user: { userId: task.userId },
|
||||
policy: cursorPolicy,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn?.('[ScheduledTask] cursor policy lookup failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
let sessionId = null;
|
||||
let messages = [];
|
||||
|
||||
if (useCursorPath) {
|
||||
logger.info?.('[ScheduledTask] executing via cursor channel', {
|
||||
taskId: task.id,
|
||||
userId: task.userId,
|
||||
requestId,
|
||||
});
|
||||
try {
|
||||
const cursorResult = await executeCursorChannelCodeRun({
|
||||
agentRunGateway,
|
||||
userId: task.userId,
|
||||
requestId,
|
||||
displayText: `定时任务:${task.title}`,
|
||||
agentPrompt: userMessage.content[0]?.text ?? userMessage.content,
|
||||
intentKind: 'page.generate',
|
||||
channel: 'scheduled_task',
|
||||
taskType: 'h5_chat_code_task',
|
||||
policy: cursorPolicy,
|
||||
forceCursorExecutor: true,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
messages = cursorResult.messages ?? [];
|
||||
sessionId = cursorResult.runId ?? null;
|
||||
} catch (cursorErr) {
|
||||
if (cursorPolicy?.fallbackToDeepseek === false) throw cursorErr;
|
||||
logger.warn?.('[ScheduledTask] cursor execution failed, falling back to goose:', cursorErr);
|
||||
useCursorPath = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!useCursorPath) {
|
||||
if (
|
||||
!tkmindProxy
|
||||
|| typeof tkmindProxy.startSessionForUser !== 'function'
|
||||
|| typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser !== 'function'
|
||||
) {
|
||||
throw new Error('缺少 tkmindProxy 会话执行能力');
|
||||
}
|
||||
const started = await tkmindProxy.startSessionForUser(task.userId, {
|
||||
origin: 'h5',
|
||||
});
|
||||
sessionId = started?.id ?? started?.sessionId;
|
||||
if (!sessionId) throw new Error('创建定时任务会话失败');
|
||||
|
||||
logger.info?.('[ScheduledTask] executing via goose session', {
|
||||
taskId: task.id,
|
||||
userId: task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
|
||||
task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
{ timeoutMs },
|
||||
);
|
||||
|
||||
messages = await refreshScheduledTaskMessages({
|
||||
userId: task.userId,
|
||||
sessionId,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService,
|
||||
});
|
||||
}
|
||||
|
||||
let deliveryText = extractScheduledTaskDeliveryText(messages, task);
|
||||
const deliveryResult = publishDir
|
||||
? await awaitScheduledTaskPageDelivery({
|
||||
@@ -624,5 +680,6 @@ export async function executeScheduledTask(task, {
|
||||
deliveryText,
|
||||
messages,
|
||||
readyPaths,
|
||||
executor: useCursorPath ? 'cursor' : 'goose',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export function startScheduledTaskWorker({
|
||||
executeTask = executeScheduledTask,
|
||||
userAuth = null,
|
||||
tkmindProxy = null,
|
||||
agentRunGateway = null,
|
||||
cursorExecutorPolicyService = null,
|
||||
sessionSnapshotService = null,
|
||||
notificationDispatcher = null,
|
||||
pool = null,
|
||||
@@ -124,6 +126,8 @@ export function startScheduledTaskWorker({
|
||||
const result = await executeTask(task, {
|
||||
userAuth,
|
||||
tkmindProxy,
|
||||
agentRunGateway,
|
||||
cursorExecutorPolicyService,
|
||||
sessionSnapshotService,
|
||||
pool,
|
||||
h5Root,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 将唐用户智趣体验通道更新为推荐配置(103 生产库)
|
||||
* 默认 dry-run;--apply 才写入 h5_wechat_cursor_executor_config
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/apply-tang-cursor-config-103.mjs
|
||||
* node scripts/apply-tang-cursor-config-103.mjs --apply
|
||||
*/
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import mysql from 'mysql2/promise';
|
||||
import {
|
||||
CURSOR_CHANNEL_FEATURES,
|
||||
defaultCursorChannelFeatures,
|
||||
intentAllowlistFromFeatures,
|
||||
} from '../cursor-channel-features.mjs';
|
||||
|
||||
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
||||
|
||||
const RECOMMENDED = {
|
||||
enabled: true,
|
||||
userAllowlist: [TANG],
|
||||
channelAllowlist: ['h5', 'wechat_mp'],
|
||||
features: {
|
||||
...defaultCursorChannelFeatures(),
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS]: { enabled: true },
|
||||
},
|
||||
fallbackToDeepseek: true,
|
||||
meta: { notes: '唐用户 Cursor 灰度 — 页面+问卷+定时任务' },
|
||||
};
|
||||
|
||||
RECOMMENDED.intentAllowlist = intentAllowlistFromFeatures(RECOMMENDED.features);
|
||||
|
||||
async function main() {
|
||||
const apply = process.argv.includes('--apply');
|
||||
const databaseUrl = execSync(
|
||||
"ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 \"grep '^DATABASE_URL=' /Users/john/Project/Memind/.env | cut -d= -f2-\"",
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
const pool = mysql.createPool({ uri: databaseUrl, connectionLimit: 2 });
|
||||
const now = Date.now();
|
||||
|
||||
const [before] = await pool.query(
|
||||
'SELECT config_json FROM h5_wechat_cursor_executor_config WHERE config_scope = ? LIMIT 1',
|
||||
['global'],
|
||||
);
|
||||
console.log('--- before ---');
|
||||
console.log(JSON.stringify(before[0]?.config_json ?? null, null, 2));
|
||||
console.log('--- target ---');
|
||||
console.log(JSON.stringify(RECOMMENDED, null, 2));
|
||||
|
||||
if (!apply) {
|
||||
console.log('\n(dry-run) 追加 --apply 写入生产库');
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO h5_wechat_cursor_executor_config
|
||||
(config_scope, config_json, updated_by, updated_at)
|
||||
VALUES ('global', ?, NULL, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_json = VALUES(config_json),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[JSON.stringify(RECOMMENDED), now],
|
||||
);
|
||||
|
||||
const [after] = await pool.query(
|
||||
'SELECT config_json, updated_at FROM h5_wechat_cursor_executor_config WHERE config_scope = ? LIMIT 1',
|
||||
['global'],
|
||||
);
|
||||
console.log('\n--- applied ---');
|
||||
console.log(JSON.stringify(after[0] ?? null, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -12,8 +12,8 @@ loadH5Environment(import.meta.dirname);
|
||||
|
||||
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
||||
const DAILY_TASK_IDS = [
|
||||
'21799936-0532-420c-b144-65ea3846cde1', // 5:30 news
|
||||
'23456db0-918e-4055-9ca3-e17e83b2dc24', // 8:00 weather
|
||||
'816dc9a2-5304-4e45-bb29-667143997362', // 5:30 每日新闻早报页面
|
||||
'23456db0-918e-4055-9ca3-e17e83b2dc24', // 8:00 每日天气预报(上海+武穴)
|
||||
];
|
||||
|
||||
const fmt = (ms) => new Date(Number(ms)).toLocaleString('zh-CN', {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 唐用户智趣通道 103 只读巡检 + 推荐配置输出
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-tang-cursor-channel-103.mjs
|
||||
* node scripts/verify-tang-cursor-channel-103.mjs --print-recommended-patch
|
||||
*/
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import mysql from 'mysql2/promise';
|
||||
import {
|
||||
CURSOR_CHANNEL_FEATURES,
|
||||
defaultCursorChannelFeatures,
|
||||
normalizeCursorChannelFeatures,
|
||||
} from '../cursor-channel-features.mjs';
|
||||
|
||||
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
||||
const TANG_USERNAME = 'wx_ul610et8';
|
||||
|
||||
const RECOMMENDED_FEATURES = {
|
||||
...defaultCursorChannelFeatures(),
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA]: { enabled: true },
|
||||
[CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE]: { enabled: false },
|
||||
[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS]: { enabled: true },
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const printPatch = args.includes('--print-recommended-patch');
|
||||
|
||||
function pass(label, detail = '') {
|
||||
console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
function warn(label, detail = '') {
|
||||
console.warn(`⚠ ${label}${detail ? `: ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
function fail(label, detail = '') {
|
||||
console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
async function loadProdDatabaseUrl() {
|
||||
return execSync(
|
||||
"ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 \"grep '^DATABASE_URL=' /Users/john/Project/Memind/.env | cut -d= -f2-\"",
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function read103Env(pattern) {
|
||||
try {
|
||||
const out = execSync(
|
||||
`ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 'grep -E "${pattern}" /Users/john/Project/Memind/.env || true'`,
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
return out.split('\n').filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let failed = 0;
|
||||
const databaseUrl = await loadProdDatabaseUrl();
|
||||
const pool = mysql.createPool({ uri: databaseUrl, connectionLimit: 2 });
|
||||
|
||||
const [users] = await pool.query(
|
||||
'SELECT id, username, display_name FROM h5_users WHERE id = ? LIMIT 1',
|
||||
[TANG],
|
||||
);
|
||||
const user = users[0];
|
||||
if (user?.username === TANG_USERNAME) pass('唐用户身份', `${user.display_name} / ${user.username}`);
|
||||
else { fail('唐用户身份'); failed += 1; }
|
||||
|
||||
const envLines = await read103Env('^(MEMIND_CURSOR|MEMIND_CURSOR_CHAT_BRIDGE|H5_SCHEDULED_TASK)');
|
||||
const envMap = Object.fromEntries(
|
||||
envLines.map((line) => {
|
||||
const idx = line.indexOf('=');
|
||||
return [line.slice(0, idx), line.slice(idx + 1)];
|
||||
}),
|
||||
);
|
||||
|
||||
if (envMap.MEMIND_CURSOR_EXECUTOR_ENABLED === '1') pass('Cursor 基础设施', 'MEMIND_CURSOR_EXECUTOR_ENABLED=1');
|
||||
else { fail('Cursor 基础设施', '未开启 MEMIND_CURSOR_EXECUTOR_ENABLED'); failed += 1; }
|
||||
|
||||
if (envMap.MEMIND_CURSOR_CHAT_BRIDGE_ENABLED === '1') {
|
||||
pass('Chat Bridge 基础设施', '已配置');
|
||||
} else {
|
||||
warn('Chat Bridge 基础设施', '未开 MEMIND_CURSOR_CHAT_BRIDGE_ENABLED;后台 chatBridge 开关暂不会生效');
|
||||
}
|
||||
|
||||
if (envMap.H5_SCHEDULED_TASK_WORKER_ENABLED === '1') pass('定时任务 Worker', '已开启');
|
||||
else { warn('定时任务 Worker', 'H5_SCHEDULED_TASK_WORKER_ENABLED 未开'); }
|
||||
|
||||
const [cursorRows] = await pool.query(
|
||||
'SELECT config_json FROM h5_wechat_cursor_executor_config WHERE config_scope = ? LIMIT 1',
|
||||
['global'],
|
||||
);
|
||||
const stored = cursorRows[0]?.config_json ?? null;
|
||||
const parsed = typeof stored === 'string' ? JSON.parse(stored) : (stored ?? {});
|
||||
const features = normalizeCursorChannelFeatures(parsed.features, {
|
||||
intentAllowlist: parsed.intentAllowlist,
|
||||
});
|
||||
|
||||
if (parsed.enabled) pass('智趣总开关', 'enabled');
|
||||
else { warn('智趣总开关', '当前关闭'); }
|
||||
|
||||
const allowlist = Array.isArray(parsed.userAllowlist) ? parsed.userAllowlist : [];
|
||||
if (allowlist.map((v) => v.toLowerCase()).includes(TANG.toLowerCase())) {
|
||||
pass('唐在白名单', `${allowlist.length} 人`);
|
||||
} else {
|
||||
fail('唐在白名单', `当前: ${allowlist.join(', ') || '(空)'}`);
|
||||
failed += 1;
|
||||
}
|
||||
|
||||
for (const [key, label] of [
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_GENERATE, '页面生成'],
|
||||
[CURSOR_CHANNEL_FEATURES.PAGE_DATA, '问卷 Page Data'],
|
||||
[CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS, '定时任务 Cursor'],
|
||||
[CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE, '普通聊天 Bridge'],
|
||||
]) {
|
||||
const on = features[key]?.enabled === true;
|
||||
const want = RECOMMENDED_FEATURES[key]?.enabled === true;
|
||||
if (on === want) pass(`能力 ${label}`, on ? '已开' : '已关(符合推荐)');
|
||||
else warn(`能力 ${label}`, `当前=${on ? '开' : '关'},推荐=${want ? '开' : '关'}`);
|
||||
}
|
||||
|
||||
const [activeTasks] = await pool.query(
|
||||
`SELECT COUNT(*) AS c FROM h5_scheduled_tasks WHERE user_id = ? AND status IN ('active','locked')`,
|
||||
[TANG],
|
||||
);
|
||||
const activeCount = Number(activeTasks[0]?.c ?? 0);
|
||||
if (activeCount > 0) pass('active 定时任务', String(activeCount));
|
||||
else warn('active 定时任务', '0 — 需重建新闻/天气任务后才能验证 scheduledTasks');
|
||||
|
||||
const [failedTasks] = await pool.query(
|
||||
`SELECT title, last_error FROM h5_scheduled_tasks
|
||||
WHERE user_id = ? AND status = 'failed'
|
||||
ORDER BY updated_at DESC LIMIT 5`,
|
||||
[TANG],
|
||||
);
|
||||
if (failedTasks.length) {
|
||||
console.log('\n--- 最近 failed 定时任务 ---');
|
||||
for (const row of failedTasks) {
|
||||
console.log(`- ${row.title}: ${String(row.last_error ?? '').slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const recommendedPatch = {
|
||||
enabled: true,
|
||||
userAllowlist: [TANG],
|
||||
channelAllowlist: ['h5', 'wechat_mp'],
|
||||
features: RECOMMENDED_FEATURES,
|
||||
fallbackToDeepseek: true,
|
||||
};
|
||||
|
||||
if (printPatch) {
|
||||
console.log('\n--- 推荐 memind_adm 保存 payload ---');
|
||||
console.log(JSON.stringify(recommendedPatch, null, 2));
|
||||
} else {
|
||||
console.log('\n提示: node scripts/verify-tang-cursor-channel-103.mjs --print-recommended-patch');
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
resolveRequestOrigin,
|
||||
} from './server/portal-publication-shell.mjs';
|
||||
import { attachPortalRuntimeRoutes } from './server/portal-runtime-routes.mjs';
|
||||
import { createHarnessRuntimeService } from './services/harness/runtime.mjs';
|
||||
import { attachPortalStaticDeliveryRoutes } from './server/portal-static-delivery-routes.mjs';
|
||||
import { createPortalWorkspacePublicationDelivery } from './server/portal-workspace-publication-delivery.mjs';
|
||||
import { attachPortalSeoDiscoveryRoutes } from './server/portal-seo-discovery-routes.mjs';
|
||||
@@ -283,6 +284,7 @@ let memoryV2ConfigService = null;
|
||||
let skillRuntimeConfigService = null;
|
||||
let systemDisclosurePolicyService = null;
|
||||
let agentCodeRunPolicyService = null;
|
||||
let harnessRuntimeService = null;
|
||||
let wechatCursorExecutorPolicyService = null;
|
||||
let wechatScheduleLlmConfigService = null;
|
||||
let wechatScheduledTaskManageLlmConfigService = null;
|
||||
@@ -338,6 +340,7 @@ async function bootstrapUserAuth() {
|
||||
try {
|
||||
if (!isDatabaseConfigured()) return false;
|
||||
const pool = createDbPool();
|
||||
harnessRuntimeService = createHarnessRuntimeService({ pool, env: process.env });
|
||||
const domainServices =
|
||||
await bootstrapPortalDomainServices({
|
||||
pool,
|
||||
@@ -517,6 +520,7 @@ async function bootstrapUserAuth() {
|
||||
conversationMemoryService,
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService,
|
||||
wechatCursorExecutorPolicyService,
|
||||
mindSpaceAssets,
|
||||
getWorkspacePublicationDelivery: () =>
|
||||
mindSpaceWorkspacePublicationDelivery,
|
||||
@@ -789,6 +793,7 @@ attachPortalRuntimeRoutes(api, {
|
||||
getEpisodicMemoryService: () => episodicMemoryService,
|
||||
getAgentRunGateway: () => agentRunGateway,
|
||||
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
||||
getHarnessRuntimeService: () => harnessRuntimeService,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function bootstrapPortalGatewayServices({
|
||||
conversationMemoryService,
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService,
|
||||
wechatCursorExecutorPolicyService = null,
|
||||
mindSpaceAssets,
|
||||
getWorkspacePublicationDelivery = () =>
|
||||
null,
|
||||
@@ -182,6 +183,7 @@ export function bootstrapPortalGatewayServices({
|
||||
conversationMemoryService,
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService,
|
||||
cursorExecutorPolicyService: wechatCursorExecutorPolicyService,
|
||||
localFetchAsset: mindSpaceAssets
|
||||
? async (userId, assetId) => {
|
||||
const { asset, bodyBase64 } =
|
||||
|
||||
@@ -226,8 +226,8 @@ export async function bootstrapPortalIntegrationServices({
|
||||
});
|
||||
}
|
||||
},
|
||||
applySessionLlmProvider: (sessionId) =>
|
||||
tkmindProxy.applySessionLlmProvider(sessionId),
|
||||
applySessionLlmProvider: (sessionId, options = {}) =>
|
||||
tkmindProxy.applySessionLlmProvider(sessionId, options),
|
||||
refreshSessionSnapshot:
|
||||
sessionSnapshotService?.isEnabled()
|
||||
? (sessionId, userId) =>
|
||||
@@ -300,6 +300,8 @@ export async function bootstrapPortalIntegrationServices({
|
||||
scheduleService,
|
||||
userAuth,
|
||||
tkmindProxy,
|
||||
agentRunGateway,
|
||||
cursorExecutorPolicyService: wechatCursorExecutorPolicyService,
|
||||
sessionSnapshotService,
|
||||
notificationDispatcher,
|
||||
pool,
|
||||
|
||||
+33
-9
@@ -58,6 +58,7 @@ import {
|
||||
shouldPersistSessionStreamEvent,
|
||||
shouldSkipUpstreamAfterSessionReplay,
|
||||
} from './session-stream.mjs';
|
||||
import { resolveCursorChatBridgeEligible } from './wechat-cursor-executor-policy.mjs';
|
||||
|
||||
const insecureDispatcher = new Agent({
|
||||
connect: { rejectUnauthorized: false },
|
||||
@@ -1172,6 +1173,7 @@ export function createTkmindProxy({
|
||||
sessionAccess = null,
|
||||
sessionStreamStore = null,
|
||||
llmProviderService,
|
||||
cursorExecutorPolicyService = null,
|
||||
localFetchAsset,
|
||||
subscriptionService,
|
||||
billingConfigService = null,
|
||||
@@ -1570,7 +1572,7 @@ export function createTkmindProxy({
|
||||
: null,
|
||||
},
|
||||
);
|
||||
await applySessionLlmProvider(session.id);
|
||||
await applySessionLlmProvider(session.id, { userId });
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1600,14 +1602,36 @@ export function createTkmindProxy({
|
||||
}
|
||||
}
|
||||
|
||||
async function applySessionLlmProvider(sessionId) {
|
||||
async function applySessionLlmProvider(sessionId, { userId = null } = {}) {
|
||||
if (!llmProviderService || !sessionId) return null;
|
||||
try {
|
||||
const target = await resolveTarget(sessionId);
|
||||
return await llmProviderService.applyBestProviderForSession(
|
||||
sessionId,
|
||||
(url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init),
|
||||
);
|
||||
const fetchImpl = (url, init) =>
|
||||
apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init);
|
||||
|
||||
if (userId && cursorExecutorPolicyService?.getEffectivePolicy) {
|
||||
try {
|
||||
const cursorPolicy = await cursorExecutorPolicyService.getEffectivePolicy(userId, {
|
||||
userId,
|
||||
});
|
||||
if (
|
||||
resolveCursorChatBridgeEligible({
|
||||
user: { userId },
|
||||
policy: cursorPolicy,
|
||||
})
|
||||
&& typeof llmProviderService.applyCursorChatBridgeForSession === 'function'
|
||||
) {
|
||||
return await llmProviderService.applyCursorChatBridgeForSession(sessionId, fetchImpl);
|
||||
}
|
||||
} catch (policyErr) {
|
||||
console.warn(
|
||||
'Cursor chat bridge policy lookup skipped:',
|
||||
policyErr instanceof Error ? policyErr.message : policyErr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await llmProviderService.applyBestProviderForSession(sessionId, fetchImpl);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'LLM provider apply skipped:',
|
||||
@@ -2027,7 +2051,7 @@ export function createTkmindProxy({
|
||||
forceDeepReasoning,
|
||||
disableImageReading: disableImageReading || visionHandlesThisTurn,
|
||||
});
|
||||
await applySessionLlmProvider(sessionId);
|
||||
await applySessionLlmProvider(sessionId, { userId });
|
||||
await repairSessionToolHistory(sessionId);
|
||||
|
||||
const user = await userAuth.getUserById(userId);
|
||||
@@ -2406,7 +2430,7 @@ export function createTkmindProxy({
|
||||
});
|
||||
return;
|
||||
}
|
||||
await applySessionLlmProvider(session.id);
|
||||
await applySessionLlmProvider(session.id, { userId: req.currentUser.id });
|
||||
}
|
||||
res.status(upstream.status).json(session);
|
||||
} catch (err) {
|
||||
@@ -2491,7 +2515,7 @@ export function createTkmindProxy({
|
||||
}
|
||||
}
|
||||
|
||||
await applySessionLlmProvider(sessionId);
|
||||
await applySessionLlmProvider(sessionId, { userId: req.currentUser.id });
|
||||
|
||||
res.status(upstream.status).json(payload);
|
||||
} catch (err) {
|
||||
|
||||
+134
-61
@@ -10,6 +10,7 @@ function sleep(ms) {
|
||||
function buildWechatCursorUserMessage({
|
||||
displayText,
|
||||
intentKind,
|
||||
channel = 'wechat_mp',
|
||||
}) {
|
||||
const taskText = String(displayText ?? '').trim();
|
||||
return {
|
||||
@@ -18,9 +19,9 @@ function buildWechatCursorUserMessage({
|
||||
metadata: {
|
||||
displayText: taskText,
|
||||
memindRun: {
|
||||
channel: 'wechat_mp',
|
||||
wechatCursorChannel: true,
|
||||
taskType: 'wechat_page_generate',
|
||||
channel,
|
||||
wechatCursorChannel: channel === 'wechat_mp',
|
||||
taskType: channel === 'wechat_mp' ? 'wechat_page_generate' : 'h5_chat_code_task',
|
||||
toolMode: 'code',
|
||||
requiredExecutor: 'cursor',
|
||||
intentKind: String(intentKind ?? '').trim() || null,
|
||||
@@ -42,6 +43,123 @@ async function readCursorCompletionText(agentRunGateway, userId, runId) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeCursorChannelCodeRun({
|
||||
agentRunGateway,
|
||||
userId,
|
||||
sessionId = null,
|
||||
requestId,
|
||||
displayText,
|
||||
agentPrompt = null,
|
||||
intentKind = 'page.generate',
|
||||
channel = 'wechat_mp',
|
||||
taskType = null,
|
||||
policy = null,
|
||||
forceCursorExecutor = false,
|
||||
timeoutMs = 15 * 60 * 1000,
|
||||
pollMs = DEFAULT_POLL_MS,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (!agentRunGateway?.createRun || !agentRunGateway?.dispatchRun || !agentRunGateway?.getRunForUser) {
|
||||
throw Object.assign(new Error('Cursor 执行网关不可用'), {
|
||||
code: 'CURSOR_CHANNEL_GATEWAY_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
|
||||
const promptText = String(agentPrompt ?? displayText ?? '').trim();
|
||||
let userMessage = buildWechatCursorUserMessage({
|
||||
displayText: String(displayText ?? promptText).trim(),
|
||||
intentKind,
|
||||
channel,
|
||||
});
|
||||
if (promptText) {
|
||||
userMessage = {
|
||||
...userMessage,
|
||||
content: [{ type: 'text', text: promptText }],
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedTaskType = taskType
|
||||
?? (channel === 'wechat_mp' ? 'wechat_page_generate' : 'h5_chat_code_task');
|
||||
let cursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
|
||||
rawToolMode: 'code',
|
||||
taskType: resolvedTaskType,
|
||||
env: process.env,
|
||||
channelEligible: true,
|
||||
policy,
|
||||
});
|
||||
if (forceCursorExecutor && !cursorRuntime.requiredExecutor) {
|
||||
cursorRuntime = {
|
||||
...cursorRuntime,
|
||||
rawToolMode: 'code',
|
||||
taskType: resolvedTaskType,
|
||||
requiredExecutor: 'cursor',
|
||||
};
|
||||
userMessage = {
|
||||
...userMessage,
|
||||
metadata: {
|
||||
...(userMessage.metadata ?? {}),
|
||||
memindRun: {
|
||||
...(userMessage.metadata?.memindRun ?? {}),
|
||||
requiredExecutor: 'cursor',
|
||||
executor: 'cursor',
|
||||
toolMode: 'code',
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
userMessage = cursorRuntime.userMessage;
|
||||
}
|
||||
|
||||
const run = await agentRunGateway.createRun(userId, {
|
||||
sessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
toolMode: 'code',
|
||||
taskType: cursorRuntime.taskType ?? resolvedTaskType,
|
||||
});
|
||||
agentRunGateway.dispatchRun(run.id);
|
||||
|
||||
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
||||
while (Date.now() <= deadline) {
|
||||
const latest = await agentRunGateway.getRunForUser(userId, run.id);
|
||||
if (!latest) {
|
||||
throw Object.assign(new Error('Cursor 任务丢失'), {
|
||||
code: 'CURSOR_CHANNEL_RUN_MISSING',
|
||||
});
|
||||
}
|
||||
if (latest.status === 'succeeded') {
|
||||
const text = await readCursorCompletionText(agentRunGateway, userId, run.id);
|
||||
logger.info?.('[cursor-channel] run succeeded', {
|
||||
userId,
|
||||
runId: run.id,
|
||||
requestId,
|
||||
channel,
|
||||
});
|
||||
return {
|
||||
text,
|
||||
runId: run.id,
|
||||
tokenState: null,
|
||||
messages: [{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text }],
|
||||
metadata: { userVisible: true, source: 'cursor-channel-agent-run', channel },
|
||||
}],
|
||||
requestMessages: [],
|
||||
};
|
||||
}
|
||||
if (latest.status === 'failed') {
|
||||
const error = new Error(latest.error || 'Cursor 执行失败');
|
||||
error.code = 'CURSOR_CHANNEL_RUN_FAILED';
|
||||
throw error;
|
||||
}
|
||||
await sleep(Math.max(500, Number(pollMs) || DEFAULT_POLL_MS));
|
||||
}
|
||||
|
||||
throw Object.assign(new Error('Cursor 执行超时'), {
|
||||
code: 'CURSOR_CHANNEL_RUN_TIMEOUT',
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeWechatCursorAgentRun({
|
||||
agentRunGateway,
|
||||
userId,
|
||||
@@ -50,69 +168,24 @@ export async function executeWechatCursorAgentRun({
|
||||
displayText,
|
||||
agentPrompt,
|
||||
intentKind = 'page.generate',
|
||||
policy = null,
|
||||
timeoutMs = 15 * 60 * 1000,
|
||||
pollMs = DEFAULT_POLL_MS,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (!agentRunGateway?.createRun || !agentRunGateway?.dispatchRun || !agentRunGateway?.getRunForUser) {
|
||||
throw Object.assign(new Error('WeChat Cursor 执行网关不可用'), {
|
||||
code: 'WECHAT_CURSOR_GATEWAY_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
|
||||
let userMessage = buildWechatCursorUserMessage({ displayText, intentKind });
|
||||
const cursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
|
||||
rawToolMode: 'code',
|
||||
taskType: 'wechat_page_generate',
|
||||
env: process.env,
|
||||
channelEligible: true,
|
||||
});
|
||||
userMessage = cursorRuntime.userMessage;
|
||||
|
||||
const run = await agentRunGateway.createRun(userId, {
|
||||
return executeCursorChannelCodeRun({
|
||||
agentRunGateway,
|
||||
userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
toolMode: 'code',
|
||||
taskType: cursorRuntime.taskType ?? 'h5_chat_code_task',
|
||||
});
|
||||
agentRunGateway.dispatchRun(run.id);
|
||||
|
||||
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
||||
while (Date.now() <= deadline) {
|
||||
const latest = await agentRunGateway.getRunForUser(userId, run.id);
|
||||
if (!latest) {
|
||||
throw Object.assign(new Error('WeChat Cursor 任务丢失'), {
|
||||
code: 'WECHAT_CURSOR_RUN_MISSING',
|
||||
});
|
||||
}
|
||||
if (latest.status === 'succeeded') {
|
||||
const text = await readCursorCompletionText(agentRunGateway, userId, run.id);
|
||||
logger.info?.('[wechat-cursor] run succeeded', {
|
||||
userId,
|
||||
runId: run.id,
|
||||
requestId,
|
||||
});
|
||||
return {
|
||||
text,
|
||||
tokenState: null,
|
||||
messages: [{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text }],
|
||||
metadata: { userVisible: true, source: 'wechat-cursor-agent-run' },
|
||||
}],
|
||||
requestMessages: [],
|
||||
};
|
||||
}
|
||||
if (latest.status === 'failed') {
|
||||
const error = new Error(latest.error || 'WeChat Cursor 执行失败');
|
||||
error.code = 'WECHAT_CURSOR_RUN_FAILED';
|
||||
throw error;
|
||||
}
|
||||
await sleep(Math.max(500, Number(pollMs) || DEFAULT_POLL_MS));
|
||||
}
|
||||
|
||||
throw Object.assign(new Error('WeChat Cursor 执行超时'), {
|
||||
code: 'WECHAT_CURSOR_RUN_TIMEOUT',
|
||||
displayText,
|
||||
agentPrompt,
|
||||
intentKind,
|
||||
channel: 'wechat_mp',
|
||||
taskType: 'wechat_page_generate',
|
||||
policy,
|
||||
timeoutMs,
|
||||
pollMs,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
CURSOR_CHANNEL_FEATURES,
|
||||
defaultCursorChannelFeatures,
|
||||
intentAllowlistFromFeatures,
|
||||
isCursorFeatureEnabled,
|
||||
isCursorTaskKindEnabled,
|
||||
normalizeCursorChannelFeatures,
|
||||
} from './cursor-channel-features.mjs';
|
||||
|
||||
const CONFIG_TABLE = 'h5_wechat_cursor_executor_config';
|
||||
const CONFIG_SCOPE = 'global';
|
||||
const POLICY_SOURCE_DEFAULT = 'default';
|
||||
@@ -6,17 +15,21 @@ 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 { CURSOR_CHANNEL_FEATURES, isCursorFeatureEnabled, isCursorTaskKindEnabled };
|
||||
|
||||
export const CURSOR_EXECUTOR_CHANNEL = Object.freeze({
|
||||
H5: 'h5',
|
||||
WECHAT_MP: 'wechat_mp',
|
||||
});
|
||||
|
||||
function defaultConfigShape() {
|
||||
const features = defaultCursorChannelFeatures();
|
||||
return {
|
||||
enabled: false,
|
||||
userAllowlist: [],
|
||||
channelAllowlist: [...DEFAULT_CHANNEL_ALLOWLIST],
|
||||
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
|
||||
intentAllowlist: intentAllowlistFromFeatures(features),
|
||||
features,
|
||||
fallbackToDeepseek: true,
|
||||
meta: {
|
||||
notes: '',
|
||||
@@ -61,6 +74,25 @@ function parseJsonLike(value, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function mergeFeaturePatch(currentFeatures, patchFeatures) {
|
||||
const base = normalizeCursorChannelFeatures(currentFeatures, {
|
||||
intentAllowlist: DEFAULT_INTENT_ALLOWLIST,
|
||||
});
|
||||
if (!patchFeatures || typeof patchFeatures !== 'object') return base;
|
||||
const next = { ...base };
|
||||
for (const featureKey of Object.values(CURSOR_CHANNEL_FEATURES)) {
|
||||
if (!(featureKey in patchFeatures)) continue;
|
||||
const raw = patchFeatures[featureKey];
|
||||
next[featureKey] = {
|
||||
enabled: normalizeBoolean(
|
||||
typeof raw === 'object' && raw !== null ? raw.enabled : raw,
|
||||
base[featureKey]?.enabled ?? false,
|
||||
),
|
||||
};
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function mergePatch(currentConfig, patch = {}) {
|
||||
const next = cloneConfig(currentConfig);
|
||||
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
|
||||
@@ -69,9 +101,17 @@ function mergePatch(currentConfig, patch = {}) {
|
||||
const channels = normalizeStringList(patch.channelAllowlist);
|
||||
next.channelAllowlist = channels.length ? channels : [...DEFAULT_CHANNEL_ALLOWLIST];
|
||||
}
|
||||
if ('features' in patch) {
|
||||
next.features = mergeFeaturePatch(next.features, patch.features);
|
||||
}
|
||||
if ('intentAllowlist' in patch) {
|
||||
const intents = normalizeStringList(patch.intentAllowlist);
|
||||
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
|
||||
next.features = normalizeCursorChannelFeatures(next.features, {
|
||||
intentAllowlist: next.intentAllowlist,
|
||||
});
|
||||
} else if ('features' in patch) {
|
||||
next.intentAllowlist = intentAllowlistFromFeatures(next.features);
|
||||
}
|
||||
if ('fallbackToDeepseek' in patch) {
|
||||
next.fallbackToDeepseek = normalizeBoolean(patch.fallbackToDeepseek, true);
|
||||
@@ -79,18 +119,27 @@ function mergePatch(currentConfig, patch = {}) {
|
||||
if (patch?.meta && typeof patch.meta.notes === 'string') {
|
||||
next.meta.notes = patch.meta.notes;
|
||||
}
|
||||
if (!next.features) {
|
||||
next.features = normalizeCursorChannelFeatures(null, {
|
||||
intentAllowlist: next.intentAllowlist,
|
||||
});
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function flattenPolicy(config, source) {
|
||||
const intentAllowlist = normalizeStringList(config.intentAllowlist);
|
||||
const channelAllowlist = normalizeStringList(config.channelAllowlist);
|
||||
const features = normalizeCursorChannelFeatures(config.features, {
|
||||
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
|
||||
});
|
||||
return {
|
||||
source,
|
||||
enabled: Boolean(config.enabled),
|
||||
userAllowlist: normalizeStringList(config.userAllowlist),
|
||||
channelAllowlist: channelAllowlist.length ? channelAllowlist : [...DEFAULT_CHANNEL_ALLOWLIST],
|
||||
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
|
||||
intentAllowlist: intentAllowlistFromFeatures(features),
|
||||
features,
|
||||
fallbackToDeepseek: config.fallbackToDeepseek !== false,
|
||||
};
|
||||
}
|
||||
@@ -123,11 +172,14 @@ export function isUserAllowedByWechatCursorPolicy(user, policy) {
|
||||
}
|
||||
|
||||
export function isIntentAllowedByWechatCursorPolicy(intentKind, policy) {
|
||||
const normalized = String(intentKind ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
if (normalized === 'page.generate') {
|
||||
return isCursorFeatureEnabled(policy, CURSOR_CHANNEL_FEATURES.PAGE_GENERATE);
|
||||
}
|
||||
const allowlist = (policy?.intentAllowlist ?? DEFAULT_INTENT_ALLOWLIST)
|
||||
.map((item) => String(item ?? '').trim())
|
||||
.filter(Boolean);
|
||||
const normalized = String(intentKind ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
return allowlist.includes(normalized);
|
||||
}
|
||||
|
||||
@@ -154,8 +206,9 @@ async function loadStoredState(pool) {
|
||||
const row = rows[0];
|
||||
if (!row) return null;
|
||||
const parsed = parseJsonLike(row.config_json, {});
|
||||
const merged = mergePatch(defaultConfigShape(), parsed);
|
||||
return {
|
||||
config: mergePatch(defaultConfigShape(), parsed),
|
||||
config: merged,
|
||||
updatedAt: Number(row.updated_at ?? 0) || null,
|
||||
updatedBy: row.updated_by ?? null,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
CURSOR_EXECUTOR_CHANNEL,
|
||||
isChannelAllowedByCursorPolicy,
|
||||
isCursorFeatureEnabled,
|
||||
isIntentAllowedByWechatCursorPolicy,
|
||||
isUserAllowedByWechatCursorPolicy,
|
||||
} from './wechat-cursor-executor-admin-config.mjs';
|
||||
import { CURSOR_CHANNEL_FEATURES } from './cursor-channel-features.mjs';
|
||||
|
||||
export { CURSOR_EXECUTOR_CHANNEL };
|
||||
export { CURSOR_EXECUTOR_CHANNEL, CURSOR_CHANNEL_FEATURES };
|
||||
|
||||
export function resolveCursorChannelEligible({
|
||||
user = null,
|
||||
@@ -25,6 +27,32 @@ export function resolveCursorChannelEligible({
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveCursorChatBridgeEligible({
|
||||
user = null,
|
||||
userId = null,
|
||||
policy = null,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
if (!policy?.enabled) return false;
|
||||
const subject = user ?? { userId };
|
||||
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
|
||||
if (!isCursorFeatureEnabled(policy, CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE)) return false;
|
||||
const bridgeEnvEnabled = String(env?.MEMIND_CURSOR_CHAT_BRIDGE_ENABLED ?? '').trim();
|
||||
if (!['1', 'true', 'yes', 'on'].includes(bridgeEnvEnabled.toLowerCase())) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveCursorScheduledTaskEligible({
|
||||
user = null,
|
||||
userId = null,
|
||||
policy = null,
|
||||
} = {}) {
|
||||
if (!policy?.enabled) return false;
|
||||
const subject = user ?? { userId };
|
||||
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
|
||||
return isCursorFeatureEnabled(policy, CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS);
|
||||
}
|
||||
|
||||
export function resolveWechatCursorExecutorEligible({
|
||||
user = null,
|
||||
userId = null,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
CURSOR_EXECUTOR_CHANNEL,
|
||||
resolveCursorChannelEligible,
|
||||
resolveCursorScheduledTaskEligible,
|
||||
resolveWechatCursorExecutorEligible,
|
||||
} from './wechat-cursor-executor-policy.mjs';
|
||||
|
||||
@@ -49,11 +50,18 @@ test('allowlisted user can use cursor channel for page.generate only', async ()
|
||||
await service.updateAdminConfig({
|
||||
enabled: true,
|
||||
userAllowlist: ['john-uuid'],
|
||||
intentAllowlist: ['page.generate'],
|
||||
features: {
|
||||
pageGenerate: { enabled: true },
|
||||
pageData: { enabled: false },
|
||||
excelAnalysis: { enabled: false },
|
||||
chatBridge: { enabled: false },
|
||||
scheduledTasks: { enabled: false },
|
||||
},
|
||||
}, { updatedBy: 'admin-1' });
|
||||
|
||||
const policy = await service.getEffectivePolicy('john-uuid', { userId: 'john-uuid' });
|
||||
assert.equal(policy.userAllowed, true);
|
||||
assert.equal(policy.features.pageGenerate.enabled, true);
|
||||
assert.equal(
|
||||
resolveWechatCursorExecutorEligible({
|
||||
user: { userId: 'john-uuid' },
|
||||
@@ -80,6 +88,24 @@ test('allowlisted user can use cursor channel for page.generate only', async ()
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveCursorScheduledTaskEligible follows scheduledTasks feature toggle', async () => {
|
||||
const pool = createMemoryPool();
|
||||
const service = createWechatCursorExecutorAdminConfigService(pool);
|
||||
await service.updateAdminConfig({
|
||||
enabled: true,
|
||||
userAllowlist: ['john-uuid'],
|
||||
features: {
|
||||
pageGenerate: { enabled: true },
|
||||
scheduledTasks: { enabled: true },
|
||||
},
|
||||
}, { updatedBy: 'admin-1' });
|
||||
const policy = await service.getEffectivePolicy('john-uuid', { userId: 'john-uuid' });
|
||||
assert.equal(
|
||||
resolveCursorScheduledTaskEligible({ user: { userId: 'john-uuid' }, policy }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('isUserAllowedByWechatCursorPolicy matches username aliases', () => {
|
||||
const policy = { enabled: true, userAllowlist: ['john'] };
|
||||
assert.equal(
|
||||
@@ -90,7 +116,17 @@ test('isUserAllowedByWechatCursorPolicy matches username aliases', () => {
|
||||
});
|
||||
|
||||
test('isIntentAllowedByWechatCursorPolicy respects allowlist', () => {
|
||||
const policy = { intentAllowlist: ['page.generate'] };
|
||||
const policy = {
|
||||
enabled: true,
|
||||
intentAllowlist: ['page.generate'],
|
||||
features: {
|
||||
pageGenerate: { enabled: true },
|
||||
pageData: { enabled: false },
|
||||
excelAnalysis: { enabled: false },
|
||||
chatBridge: { enabled: false },
|
||||
scheduledTasks: { enabled: false },
|
||||
},
|
||||
};
|
||||
assert.equal(isIntentAllowedByWechatCursorPolicy('page.generate', policy), true);
|
||||
assert.equal(isIntentAllowedByWechatCursorPolicy('chat.general', policy), false);
|
||||
});
|
||||
|
||||
+15
-7
@@ -2179,9 +2179,9 @@ export function createWechatMpService({
|
||||
throw notifyFailure ? markWechatUserNotified(error) : error;
|
||||
};
|
||||
|
||||
const ensureSessionProvider = async (sessionId) => {
|
||||
const ensureSessionProvider = async (sessionId, userId = null) => {
|
||||
if (!applySessionLlmProvider || !sessionId) return;
|
||||
const applied = await applySessionLlmProvider(sessionId);
|
||||
const applied = await applySessionLlmProvider(sessionId, { userId });
|
||||
if (applied && applied.ok === false) {
|
||||
throw new Error(applied.message || '专属 Agent Provider 未配置');
|
||||
}
|
||||
@@ -2539,6 +2539,9 @@ export function createWechatMpService({
|
||||
void refreshWechatSessionSnapshot(sessionId, userId);
|
||||
};
|
||||
|
||||
/** Cursor channel writes the assistant turn via respondDeterministically; goose stays empty. */
|
||||
const shouldRefreshWechatSessionSnapshot = (reply) => !isWechatCursorChannelReply(reply);
|
||||
|
||||
const rememberWechatUserContext = async (sessionId, user, { forceBootstrap = false } = {}) => {
|
||||
const addressName = resolveWechatAddressName(user);
|
||||
if (!addressName) return;
|
||||
@@ -2820,7 +2823,7 @@ export function createWechatMpService({
|
||||
});
|
||||
let sessionId = route.sessionId;
|
||||
let carriedSessionContent = String(route.carriedSessionContent ?? '').trim();
|
||||
await ensureSessionProvider(sessionId);
|
||||
await ensureSessionProvider(sessionId, user.userId);
|
||||
if (wechatIntent.kind === 'session.reset') {
|
||||
await sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
@@ -2847,7 +2850,7 @@ export function createWechatMpService({
|
||||
if (pollutionRotation.carriedSessionContent) {
|
||||
carriedSessionContent = pollutionRotation.carriedSessionContent;
|
||||
}
|
||||
await ensureSessionProvider(sessionId);
|
||||
await ensureSessionProvider(sessionId, user.userId);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: true });
|
||||
}
|
||||
if (
|
||||
@@ -2964,6 +2967,7 @@ export function createWechatMpService({
|
||||
displayText: String(intent?.displayText ?? intent?.agentText ?? '').trim(),
|
||||
agentPrompt: activeAgentPrompt,
|
||||
intentKind: wechatIntent.kind,
|
||||
policy: wechatCursorPolicy,
|
||||
timeoutMs: agentReplyTimeoutMs,
|
||||
logger,
|
||||
});
|
||||
@@ -3279,7 +3283,9 @@ export function createWechatMpService({
|
||||
finalizedReply = appendGeneratedImageFallbackLink(finalizedReply, generatedImages[0]);
|
||||
}
|
||||
}
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
if (shouldRefreshWechatSessionSnapshot(finalizedReply)) {
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
}
|
||||
const delivery = await sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
await guardScheduleReply(finalizedReply),
|
||||
@@ -3386,7 +3392,7 @@ export function createWechatMpService({
|
||||
userContext: user,
|
||||
});
|
||||
sessionId = route.sessionId;
|
||||
await ensureSessionProvider(sessionId);
|
||||
await ensureSessionProvider(sessionId, user.userId);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
if (historicalImageError) {
|
||||
prepareWechatIntentForHistoricalImageRetry(intent, {
|
||||
@@ -3668,7 +3674,9 @@ export function createWechatMpService({
|
||||
finalizedReply = appendGeneratedImageFallbackLink(finalizedReply, generatedImages[0]);
|
||||
}
|
||||
}
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
if (shouldRefreshWechatSessionSnapshot(finalizedReply)) {
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
}
|
||||
const retryDelivery = await sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
await guardScheduleReply(finalizedReply),
|
||||
|
||||
Reference in New Issue
Block a user