fix(router): route casual chat to DeepSeek and reserve Cursor for page tasks
Default unmatched conversation to direct_chat, stop blanket Agent session continuation and generic Agent→Cursor promotion; fix WeChat page retry userPublishDir scope and simplify ChatPanel controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+46
-2
@@ -59,7 +59,8 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
# MEMIND_AGENT_RUN_WORKER_POLL_MS=1000
|
||||
# MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_MS=30000
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=90000
|
||||
# 页面/代码任务建议 5 分钟,避免长任务被误判 stale
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=300000
|
||||
# MEMIND_AGENT_RUN_RECOVERY_INTERVAL_MS=30000
|
||||
# MEMIND_RUNTIME_BUILD_ID=<release-or-build-id>
|
||||
# MEMIND_AGENT_RUN_WORKER_ID=<optional-instance-id>
|
||||
@@ -147,7 +148,19 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
# MEMIND_CHAT_ROUTER_MIN_CONFIDENCE=0.65
|
||||
# MEMIND_CHAT_ROUTER_TIMEOUT_MS=1200
|
||||
# MEMIND_CHAT_ROUTER_MEMORY_ENABLED=1
|
||||
# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=agent_orchestration
|
||||
# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=direct_chat
|
||||
|
||||
# ----- Token 压缩(本地开发可选,降低 DeepSeek 上下文)-----
|
||||
# Agent 会话历史压缩:超过阈值后清空 Goose 会话并保留摘要(默认 180 条 / 12 万字符,偏松)
|
||||
# MEMIND_AGENT_SESSION_COMPACT_MESSAGE_COUNT=40
|
||||
# MEMIND_AGENT_SESSION_COMPACT_CHAR_COUNT=30000
|
||||
# 路由时不再附加 Memory 上下文(省 input token)
|
||||
# MEMIND_CHAT_ROUTER_MEMORY_ENABLED=0
|
||||
# Memory 召回预算(也可在 memind_adm Memory V2 页调整 retriever.tokenBudget / limit)
|
||||
# MEMORY_RETRIEVER_TOKEN_BUDGET=900
|
||||
# MEMORY_RETRIEVER_LIMIT=4
|
||||
# MEMORY_AGENT_RESOLVE_LIMIT=2
|
||||
# 对比脚本:node scripts/benchmark-token-optimization.mjs --scenario token-benchmark-chat
|
||||
|
||||
# ----- 本地 LLM Router(pnpm dev,全员 Active,无 Shadow)-----
|
||||
# Router 模型建议(分类 JSON,短 prompt,要快不要大):
|
||||
@@ -501,3 +514,34 @@ MEMIND_RUNTIME_PROFILE=local
|
||||
# MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:8082
|
||||
# MINDSPACE_REMOTE_AUTH_TOKEN=<与 cn.tkmind.mindspace-service 一致>
|
||||
# MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cursor 双轨(help 工程师 + 实验性 Goose 聊天桥)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Track B: 用户 help → Cursor agent → 自动 Goose 原会话补发链接
|
||||
# MEMIND_CURSOR_HELP_ENABLED=1
|
||||
# MEMIND_CURSOR_HELP_WORKER_ENABLED=1
|
||||
# MEMIND_CURSOR_HELP_WORKSPACE=/Users/john/Project/Memind
|
||||
# MEMIND_CURSOR_HELP_AGENT_BIN=/Users/john/.local/bin/agent
|
||||
# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1
|
||||
|
||||
# Track C: Goose 编排 + Cursor 作为 code executor(MindSpace 落盘,推荐 PoC)
|
||||
# MEMIND_CURSOR_EXECUTOR_ENABLED=1
|
||||
# MEMIND_AIDER_SKILL_USE_CURSOR=1
|
||||
# 默认将页面生成、问卷(Page Data)、Excel 分析路由到 Cursor 执行
|
||||
# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=1
|
||||
# 已废弃:不再把普通 Agent 编排全量切到 Cursor;简单对话走 DeepSeek direct_chat
|
||||
# MEMIND_CURSOR_AGENT_TASKS_DEFAULT=0
|
||||
# MEMIND_CURSOR_DEEPSEEK_FALLBACK=1
|
||||
# MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR=cursor
|
||||
# MEMIND_TOOL_GATEWAY_ENABLED=1
|
||||
# MEMIND_TOOL_GATEWAY_DRY_RUN=0
|
||||
# MEMIND_TOOL_GATEWAY_CURSOR_TASK_TYPES=h5_chat_code_task,mindspace_page,mindspace_html_page
|
||||
# MEMIND_CURSOR_EXECUTOR_AGENT_BIN=/Users/john/.local/bin/agent
|
||||
|
||||
# Track A: goosed 经 OpenAI 兼容桥接调 Cursor(PoC,不支持 tools;仅灰度)
|
||||
# MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1
|
||||
# MEMIND_CURSOR_CHAT_BRIDGE_PORT=18040
|
||||
# MEMIND_CURSOR_CHAT_BRIDGE_ENTRYPOINT=1
|
||||
# CURSOR_API_KEY=your-cursor-api-key
|
||||
# memindadm 选择「Cursor Chat Bridge (实验)」后 sync 到 goosed
|
||||
|
||||
+36
-39
@@ -399,11 +399,9 @@ export function coerceRealtimeWebSkill(classification, text, { grantedSkills = [
|
||||
}
|
||||
|
||||
function resolveRouterFallbackRoute(env, overrides = {}) {
|
||||
const raw = overrides.fallbackRoute
|
||||
return overrides.fallbackRoute
|
||||
?? normalizeRoute(env?.MEMIND_CHAT_ROUTER_FALLBACK_ROUTE)
|
||||
?? CHAT_INTENT_ROUTE.AGENT;
|
||||
// Timeout/parse failures must fail open to agent so realtime/tool tasks are not dropped.
|
||||
return raw === CHAT_INTENT_ROUTE.DIRECT_CHAT ? CHAT_INTENT_ROUTE.AGENT : raw;
|
||||
?? CHAT_INTENT_ROUTE.DIRECT_CHAT;
|
||||
}
|
||||
|
||||
const DEFAULT_ROUTER_TIMEOUT_MS = 1200;
|
||||
@@ -1071,12 +1069,7 @@ export function classifyWithRules({
|
||||
reason: '消息包含非文本内容',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
isMemoryRecallQuestion(normalized) &&
|
||||
!hasPriorAgentConversation(sessionId, sessionMessageCount)
|
||||
) {
|
||||
if (includeIntentPatterns && normalized && isMemoryRecallQuestion(normalized)) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.98,
|
||||
@@ -1091,16 +1084,6 @@ export function classifyWithRules({
|
||||
reason: faqMatch?.reason ?? '用户明确要求纯文字、不生成页面',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (hasPriorAgentConversation(sessionId, sessionMessageCount)) {
|
||||
const reason = isAgentSessionContinueText(normalized)
|
||||
? 'Agent 会话确认/续聊'
|
||||
: '延续已有 Agent 会话';
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
confidence: 1,
|
||||
reason,
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (includeIntentPatterns && normalized) {
|
||||
const faqMatch = matchDirectChatFaqRule(normalized);
|
||||
if (faqMatch) {
|
||||
@@ -1111,6 +1094,35 @@ export function classifyWithRules({
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized))
|
||||
) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.95,
|
||||
reason: '简单寒暄或连通性测试',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
OBVIOUS_DIRECT_CHAT_PATTERNS.some((pattern) => pattern.test(normalized))
|
||||
) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.95,
|
||||
reason: '纯文字创作或闲聊,无需执行任务',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (hasPriorAgentConversation(sessionId, sessionMessageCount) && isAgentSessionContinueText(normalized)) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
confidence: 1,
|
||||
reason: 'Agent 会话确认/续聊',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (includeIntentPatterns && normalized && isPageDataDevIntent(normalized)) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
@@ -1160,26 +1172,11 @@ export function classifyWithRules({
|
||||
if (includeIntentPatterns && normalized && isRealtimeInfoQuestion(normalized)) {
|
||||
return finalizeRouterClassification(buildRealtimeInfoClassification(), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized))
|
||||
) {
|
||||
if (includeIntentPatterns && normalized) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.95,
|
||||
reason: '简单寒暄或连通性测试',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
OBVIOUS_DIRECT_CHAT_PATTERNS.some((pattern) => pattern.test(normalized))
|
||||
) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
confidence: 0.95,
|
||||
reason: '纯文字创作或闲聊,无需执行任务',
|
||||
confidence: 0.72,
|
||||
reason: '普通对话默认走轻量聊天',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
return null;
|
||||
@@ -1220,7 +1217,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildFallbackClassification(reason = '规则未命中,走 skill 默认 Agent 通道') {
|
||||
function buildFallbackClassification(reason = '规则未命中,默认轻量聊天') {
|
||||
return normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0.5,
|
||||
|
||||
+62
-42
@@ -443,14 +443,22 @@ test('classifyWithRules keeps agent session continuation even when llm router fl
|
||||
assert.match(continued.reason, /Agent 会话确认|延续已有 Agent 会话/);
|
||||
});
|
||||
|
||||
test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => {
|
||||
test('classifyWithRules keeps agent session confirmation on agent path', () => {
|
||||
const continued = classifyWithRules({
|
||||
text: '继续聊聊',
|
||||
sessionId: '20260704_2',
|
||||
sessionMessageCount: 2,
|
||||
});
|
||||
assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(continued.reason, '延续已有 Agent 会话');
|
||||
assert.equal(continued.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.match(continued.reason, /轻量聊天/);
|
||||
|
||||
const confirmed = classifyWithRules({
|
||||
text: '可以',
|
||||
sessionId: '20260704_2',
|
||||
sessionMessageCount: 2,
|
||||
});
|
||||
assert.equal(confirmed.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.match(confirmed.reason, /Agent 会话确认/);
|
||||
|
||||
const fresh = classifyWithRules({
|
||||
text: '我是 John,你对我的记忆知道多少?',
|
||||
@@ -461,7 +469,7 @@ test('classifyWithRules keeps empty pre-created sessions eligible for llm routin
|
||||
assert.match(fresh.reason, /记忆/);
|
||||
});
|
||||
|
||||
test('classifyWithRules keeps memory recall on agent when session already active', () => {
|
||||
test('classifyWithRules keeps memory recall on direct chat even when session already active', () => {
|
||||
const result = classifyWithRules({
|
||||
text: '你记得我说想去哪儿吗',
|
||||
sessionId: '20260704_9',
|
||||
@@ -472,8 +480,23 @@ test('classifyWithRules keeps memory recall on agent when session already active
|
||||
metadata: { displayText: '你记得我说想去哪儿吗' },
|
||||
},
|
||||
});
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.match(result.reason, /记忆|历史对话/);
|
||||
});
|
||||
|
||||
test('classifyWithRules keeps casual FAQ chat on direct chat even when session already active', () => {
|
||||
const result = classifyWithRules({
|
||||
text: '你觉得人都是为了什么而活',
|
||||
sessionId: '20260826_39',
|
||||
sessionMessageCount: 4,
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '你觉得人都是为了什么而活' }],
|
||||
metadata: { displayText: '你觉得人都是为了什么而活' },
|
||||
},
|
||||
});
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.match(result.reason, /闲聊|FAQ/);
|
||||
});
|
||||
|
||||
test('classifyWithRules bypasses llm router when user selected summarize skill', async () => {
|
||||
@@ -559,7 +582,7 @@ test('classifyWithRules bypasses llm router for explicit web skill prompt', asyn
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter keeps continued sessions on agent without llm router', async () => {
|
||||
test('createChatIntentRouter keeps continued sessions on direct chat for general conversation', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
@@ -590,12 +613,12 @@ test('createChatIntentRouter keeps continued sessions on agent without llm route
|
||||
});
|
||||
|
||||
assert.equal(llmCalls.length, 0);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||||
assert.match(result.reason, /轻量聊天/);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter uses agent fallback for general conversational analysis when llm router disabled', async () => {
|
||||
test('createChatIntentRouter uses direct chat fallback for general conversational analysis when llm router disabled', async () => {
|
||||
const llmCalls = [];
|
||||
const resolveCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
@@ -630,8 +653,8 @@ test('createChatIntentRouter uses agent fallback for general conversational anal
|
||||
grantedSkills: ['web'],
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(resolveCalls.length, 0);
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
@@ -775,11 +798,11 @@ test('createManagedChatIntentRouter hot-loads admin config and activates llm rou
|
||||
},
|
||||
});
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'llm');
|
||||
assert.equal(llmCalls.length, 1);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter rejects low-confidence llm direct_chat and falls back to agent', async () => {
|
||||
test('createChatIntentRouter rejects low-confidence llm direct_chat and keeps rule default', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
@@ -809,12 +832,12 @@ test('createChatIntentRouter rejects low-confidence llm direct_chat and falls ba
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(llmCalls.length, 1);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter falls back to agent when llm router fails', async () => {
|
||||
test('createChatIntentRouter keeps direct chat when llm router fails', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
@@ -835,12 +858,12 @@ test('createChatIntentRouter falls back to agent when llm router fails', async (
|
||||
grantedSkills: ['web'],
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(llmCalls.length, 1);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter uses agent fallback when llm router disabled', async () => {
|
||||
test('createChatIntentRouter uses direct chat when llm router disabled', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: false,
|
||||
@@ -860,8 +883,8 @@ test('createChatIntentRouter uses agent fallback when llm router disabled', asyn
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
@@ -1176,13 +1199,13 @@ test('createChatIntentRouter fast-paths world cup without calling router LLM', a
|
||||
assert.equal(result.source, 'rule');
|
||||
});
|
||||
|
||||
test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => {
|
||||
test('resolveChatIntentRouterPolicy defaults fallback route to direct chat', () => {
|
||||
const policy = resolveChatIntentRouterPolicy({
|
||||
env: {
|
||||
MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat',
|
||||
},
|
||||
});
|
||||
assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
});
|
||||
|
||||
test('coerceRealtimeWebSkill replaces search with web for world cup query', () => {
|
||||
@@ -1259,7 +1282,7 @@ test('createChatIntentRouter coerces LLM search route to web for realtime query'
|
||||
assert.match(result.agentBrief, /web_search/);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => {
|
||||
test('createChatIntentRouter timeout fallback keeps direct chat default', async () => {
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
timeoutMs: 40,
|
||||
@@ -1280,8 +1303,8 @@ test('createChatIntentRouter timeout fallback prefers agent even when policy req
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
});
|
||||
|
||||
test('agent memory shadow resolve is independently gated and never marked for injection', async () => {
|
||||
@@ -1592,14 +1615,14 @@ test('createChatIntentRouter limits llm routing to canary users', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(control.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(control.source, 'fallback');
|
||||
assert.equal(control.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(control.source, 'rule');
|
||||
assert.equal(canary.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(canary.source, 'llm');
|
||||
assert.equal(llmCalls.length, 1);
|
||||
assert.equal(canary.source, 'rule');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter shadow mode logs llm suggestion without changing route', async () => {
|
||||
test('createChatIntentRouter shadow mode skips llm shadow when rule default matches', async () => {
|
||||
const lines = [];
|
||||
const logger = { warn(line) { lines.push(line); }, info(line) { lines.push(line); } };
|
||||
const router = createChatIntentRouter({
|
||||
@@ -1631,13 +1654,10 @@ test('createChatIntentRouter shadow mode logs llm suggestion without changing ro
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(result.llmShadow?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.llmShadow?.wouldChangeRoute, true);
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0], /\[chat-llm-router-shadow\]/);
|
||||
assert.match(lines[0], /wouldChangeRoute":true/);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(result.llmShadow, undefined);
|
||||
assert.equal(lines.length, 0);
|
||||
});
|
||||
|
||||
test('logChatLlmRouterShadow emits structured payload', () => {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
function envFlag(value, fallback = false) {
|
||||
const raw = String(value ?? '').trim().toLowerCase();
|
||||
if (!raw) return fallback;
|
||||
return ['1', 'true', 'yes', 'on'].includes(raw);
|
||||
}
|
||||
|
||||
export function expandHome(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return text;
|
||||
if (text.startsWith('~/')) return path.join(os.homedir(), text.slice(2));
|
||||
if (text === '~') return os.homedir();
|
||||
return text;
|
||||
}
|
||||
|
||||
export function cursorExecutorEnabled(env = process.env) {
|
||||
return envFlag(env.MEMIND_CURSOR_EXECUTOR_ENABLED, false);
|
||||
}
|
||||
|
||||
export function resolvePreferredCodeExecutor(env = process.env) {
|
||||
if (!cursorExecutorEnabled(env)) return 'aider';
|
||||
if (envFlag(env.MEMIND_AIDER_SKILL_USE_CURSOR, true)) return 'cursor';
|
||||
return 'aider';
|
||||
}
|
||||
|
||||
export function cursorPageTasksDefaultEnabled(env = process.env) {
|
||||
if (!cursorExecutorEnabled(env)) return false;
|
||||
return envFlag(env.MEMIND_CURSOR_PAGE_TASKS_DEFAULT, true);
|
||||
}
|
||||
|
||||
/** @deprecated 普通 Agent 编排不再默认走 Cursor;仅页面/问卷/Excel 等代码任务使用 enforcePageGenerationCursorRuntime */
|
||||
export function cursorAgentTasksDefaultEnabled(env = process.env) {
|
||||
if (!cursorExecutorEnabled(env)) return false;
|
||||
return envFlag(env.MEMIND_CURSOR_AGENT_TASKS_DEFAULT, false);
|
||||
}
|
||||
|
||||
/** 智趣执行失败时回退 DeepSeek / Goose 会话 */
|
||||
export function cursorDeepseekFallbackEnabled(env = process.env) {
|
||||
return envFlag(env.MEMIND_CURSOR_DEEPSEEK_FALLBACK, false);
|
||||
}
|
||||
|
||||
export function resolveCursorAgentBin(env = process.env) {
|
||||
const configured = expandHome(
|
||||
env.MEMIND_CURSOR_EXECUTOR_AGENT_BIN
|
||||
?? env.MEMIND_CURSOR_HELP_AGENT_BIN
|
||||
?? '~/.local/bin/agent',
|
||||
);
|
||||
if (configured && fs.existsSync(configured)) return configured;
|
||||
return 'agent';
|
||||
}
|
||||
|
||||
export function buildCursorExecutorInstruction(instruction) {
|
||||
const base = String(instruction ?? '').trim();
|
||||
if (!base) return base;
|
||||
return [
|
||||
base,
|
||||
'',
|
||||
'执行要求:',
|
||||
'- 在当前 MindSpace 工作区(cwd)内完成改动',
|
||||
'- 页面产物写入 public/ 目录下的 .html 文件',
|
||||
'- 直接完成可交付产物,不要等待用户确认',
|
||||
'- 不要执行 git commit、git push、git checkout、git reset、生产发布或远端同步命令',
|
||||
'- 不要修改当前工作区之外的文件',
|
||||
'- 完成后在最终输出中简述改动文件与验证结果',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildCursorExecutorLaunchPlan({
|
||||
cwd,
|
||||
instruction,
|
||||
env = process.env,
|
||||
agentBin = resolveCursorAgentBin(env),
|
||||
} = {}) {
|
||||
const workspace = path.resolve(String(cwd ?? env.MEMIND_CURSOR_EXECUTOR_WORKSPACE ?? process.cwd()));
|
||||
const prompt = buildCursorExecutorInstruction(instruction);
|
||||
if (!prompt) {
|
||||
return {
|
||||
ok: false,
|
||||
executor: 'cursor',
|
||||
message: 'TKMind 智趣执行器需要 instruction',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
executor: 'cursor',
|
||||
executorLabel: 'TKMind 智趣',
|
||||
cwd: workspace,
|
||||
command: agentBin,
|
||||
args: [
|
||||
'--print',
|
||||
'--trust',
|
||||
'--force',
|
||||
'--approve-mcps',
|
||||
'--output-format',
|
||||
'text',
|
||||
'--workspace',
|
||||
workspace,
|
||||
prompt,
|
||||
],
|
||||
env: {},
|
||||
notes: [
|
||||
'TKMind 智趣执行器在 MindSpace 工作区 headless 执行。',
|
||||
'Goose 仍负责对话编排,智趣只负责代码/页面落盘。',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildCursorExecutorLaunchPlan,
|
||||
cursorExecutorEnabled,
|
||||
resolvePreferredCodeExecutor,
|
||||
} from './cursor-agent-launch.mjs';
|
||||
|
||||
test('cursor executor is disabled unless explicitly enabled', () => {
|
||||
assert.equal(cursorExecutorEnabled({}), false);
|
||||
assert.equal(cursorExecutorEnabled({ MEMIND_CURSOR_EXECUTOR_ENABLED: '1' }), true);
|
||||
});
|
||||
|
||||
test('resolvePreferredCodeExecutor prefers cursor when enabled', () => {
|
||||
assert.equal(resolvePreferredCodeExecutor({}), 'aider');
|
||||
assert.equal(
|
||||
resolvePreferredCodeExecutor({ MEMIND_CURSOR_EXECUTOR_ENABLED: '1' }),
|
||||
'cursor',
|
||||
);
|
||||
assert.equal(
|
||||
resolvePreferredCodeExecutor({
|
||||
MEMIND_CURSOR_EXECUTOR_ENABLED: '1',
|
||||
MEMIND_AIDER_SKILL_USE_CURSOR: '0',
|
||||
}),
|
||||
'aider',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCursorExecutorLaunchPlan includes workspace and mindspace hints', async () => {
|
||||
const fs = await import('node:fs/promises');
|
||||
const os = await import('node:os');
|
||||
const path = await import('node:path');
|
||||
const agentStub = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'cursor-agent-')), 'agent');
|
||||
await fs.writeFile(agentStub, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
|
||||
const plan = buildCursorExecutorLaunchPlan({
|
||||
cwd: '/tmp/mindspace/user-1',
|
||||
instruction: '写一首春日诗并生成 public/spring-poem.html',
|
||||
env: { MEMIND_CURSOR_EXECUTOR_AGENT_BIN: agentStub },
|
||||
});
|
||||
assert.equal(plan.ok, true);
|
||||
assert.equal(plan.executor, 'cursor');
|
||||
assert.equal(plan.cwd, '/tmp/mindspace/user-1');
|
||||
assert.equal(plan.command, agentStub);
|
||||
assert.ok(plan.args.includes('--workspace'));
|
||||
assert.ok(plan.args.includes('/tmp/mindspace/user-1'));
|
||||
assert.match(plan.args.at(-1) ?? '', /public\/.*\.html/);
|
||||
assert.match(plan.args.at(-1) ?? '', /不要执行 git commit/);
|
||||
assert.match(plan.args.at(-1) ?? '', /不要修改当前工作区之外的文件/);
|
||||
});
|
||||
|
||||
test('buildCursorExecutorLaunchPlan rejects empty instruction', () => {
|
||||
const plan = buildCursorExecutorLaunchPlan({ cwd: '/tmp/work', instruction: ' ' });
|
||||
assert.equal(plan.ok, false);
|
||||
assert.match(plan.message ?? '', /instruction/i);
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
import {
|
||||
AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
EXCEL_ANALYST_SKILL_NAME,
|
||||
PAGE_DATA_COLLECT_SKILL_NAME,
|
||||
extractAiderDevelopmentTask,
|
||||
isExcelAnalysisIntent,
|
||||
isGenericPageGenerationRequest,
|
||||
isPageDataDevIntent,
|
||||
isPageDataIntent,
|
||||
isPageGenerationIntent,
|
||||
} from './chat-skills.mjs';
|
||||
import {
|
||||
cursorAgentTasksDefaultEnabled,
|
||||
cursorPageTasksDefaultEnabled,
|
||||
resolvePreferredCodeExecutor,
|
||||
} from './cursor-agent-launch.mjs';
|
||||
|
||||
const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||||
|
||||
const PAGE_CURSOR_TASK_TYPE = 'h5_chat_code_task';
|
||||
|
||||
const CURSOR_TASK_KIND = {
|
||||
PAGE_GENERATION: 'page_generation',
|
||||
PAGE_DATA: 'page_data',
|
||||
EXCEL_ANALYSIS: 'excel_analysis',
|
||||
};
|
||||
|
||||
function selectedChatSkill(userMessage) {
|
||||
const metadata = userMessage?.metadata;
|
||||
const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {};
|
||||
return String(runMetadata.selectedChatSkill ?? '').trim();
|
||||
}
|
||||
|
||||
export function resolveCursorTaskKind(taskText, skill = '') {
|
||||
const normalized = String(taskText ?? '').trim();
|
||||
if (!normalized || isPageDataDevIntent(normalized)) return null;
|
||||
|
||||
const selectedSkill = String(skill ?? '').trim();
|
||||
if (
|
||||
selectedSkill === PAGE_DATA_COLLECT_SKILL_NAME
|
||||
|| isPageDataIntent(normalized)
|
||||
) {
|
||||
return CURSOR_TASK_KIND.PAGE_DATA;
|
||||
}
|
||||
if (
|
||||
selectedSkill === EXCEL_ANALYST_SKILL_NAME
|
||||
|| isExcelAnalysisIntent(normalized)
|
||||
) {
|
||||
return CURSOR_TASK_KIND.EXCEL_ANALYSIS;
|
||||
}
|
||||
if (
|
||||
selectedSkill === PUBLISH_SKILL_NAME
|
||||
|| isPageGenerationIntent(normalized)
|
||||
) {
|
||||
if (isGenericPageGenerationRequest(normalized)) return null;
|
||||
return CURSOR_TASK_KIND.PAGE_GENERATION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isPageCursorDefaultCandidate(userMessage, {
|
||||
rawToolMode = 'chat',
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
if (!cursorPageTasksDefaultEnabled(env)) return false;
|
||||
if (String(rawToolMode ?? '').trim().toLowerCase() === 'code') return false;
|
||||
|
||||
const skill = selectedChatSkill(userMessage);
|
||||
if (skill === AIDER_DEVELOPMENT_SKILL_NAME) return false;
|
||||
|
||||
const runMetadata = userMessage?.metadata?.memindRun ?? userMessage?.metadata?.agentRun ?? {};
|
||||
if (runMetadata.pageTemplateSkill || runMetadata.pageTemplateStrict) return false;
|
||||
|
||||
const taskText = extractAiderDevelopmentTask(userMessage);
|
||||
if (!taskText) return false;
|
||||
|
||||
return resolveCursorTaskKind(taskText, skill) !== null;
|
||||
}
|
||||
|
||||
function buildCursorPageInstruction(taskText) {
|
||||
return [
|
||||
taskText,
|
||||
'',
|
||||
'[Memind page task via TKMind 智趣 executor]',
|
||||
'在当前用户 MindSpace 工作区生成或更新 public/*.html 页面。',
|
||||
'完成后确保页面可通过 /MindSpace/<userId>/public/<file>.html 访问。',
|
||||
'不要只回复文字,必须实际落盘 HTML 文件。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildCursorPageDataInstruction(taskText) {
|
||||
return [
|
||||
taskText,
|
||||
'',
|
||||
'[Memind Page Data survey task via TKMind 智趣 executor]',
|
||||
'在当前用户 MindSpace 工作区完成可提交、可持久化的问卷/数据收集与结果分析页面。',
|
||||
'必须先阅读 docs/page-data-api-usage.md 与 docs/examples/page-data-survey-test.md。',
|
||||
'需要:1) 创建/更新 public/*.html 问卷页(含 MindSpacePageData.createClient);',
|
||||
'2) 按需创建后台分析页;3) 注册 dataset 与 page policy(可参考 scripts/setup-page-data-survey-demo.mjs);',
|
||||
'4) 数据必须持久化到 PG(通过 Page Data API),不要旁路自建 API。',
|
||||
'完成后确保页面可通过 /MindSpace/<userId>/public/<file>.html 访问。',
|
||||
'不要只回复文字,必须实际落盘 HTML 与相关配置文件。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildCursorExcelInstruction(taskText) {
|
||||
return [
|
||||
taskText,
|
||||
'',
|
||||
'[Memind Excel analysis task via TKMind 智趣 executor]',
|
||||
'在当前用户 MindSpace 工作区分析用户上传的 Excel/CSV 等表格文件。',
|
||||
'先在工作区 attachments/、uploads/ 或用户消息提及的路径定位附件文件,读取并分析数据。',
|
||||
'输出分析结论,并生成可访问的结果页面 public/*.html 或分析报告文件。',
|
||||
'不要只回复文字,必须实际读取文件并产出可交付结果。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildCursorTaskInstruction(taskText, taskKind) {
|
||||
switch (taskKind) {
|
||||
case CURSOR_TASK_KIND.PAGE_DATA:
|
||||
return buildCursorPageDataInstruction(taskText);
|
||||
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
|
||||
return buildCursorExcelInstruction(taskText);
|
||||
case CURSOR_TASK_KIND.PAGE_GENERATION:
|
||||
default:
|
||||
return buildCursorPageInstruction(taskText);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCursorTaskSkill(taskKind, runMetadataSkill = '') {
|
||||
const existing = String(runMetadataSkill ?? '').trim();
|
||||
if (existing && existing !== AIDER_DEVELOPMENT_SKILL_NAME) return existing;
|
||||
switch (taskKind) {
|
||||
case CURSOR_TASK_KIND.PAGE_DATA:
|
||||
return PAGE_DATA_COLLECT_SKILL_NAME;
|
||||
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
|
||||
return EXCEL_ANALYST_SKILL_NAME;
|
||||
case CURSOR_TASK_KIND.PAGE_GENERATION:
|
||||
default:
|
||||
return PUBLISH_SKILL_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCursorSuggestedDelivery(taskKind) {
|
||||
switch (taskKind) {
|
||||
case CURSOR_TASK_KIND.PAGE_DATA:
|
||||
return 'mindspace_page_data';
|
||||
case CURSOR_TASK_KIND.EXCEL_ANALYSIS:
|
||||
return 'mindspace_analysis_html';
|
||||
case CURSOR_TASK_KIND.PAGE_GENERATION:
|
||||
default:
|
||||
return 'mindspace_public_html';
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteUserMessageForCursorTask(userMessage, taskText, taskKind) {
|
||||
const message = userMessage && typeof userMessage === 'object' && !Array.isArray(userMessage)
|
||||
? { ...userMessage }
|
||||
: { value: userMessage };
|
||||
const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
|
||||
? { ...message.metadata }
|
||||
: {};
|
||||
const instruction = buildCursorTaskInstruction(taskText, taskKind);
|
||||
metadata.displayText = taskText;
|
||||
metadata.userVisible = true;
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content.map((item) => (
|
||||
item?.type === 'text'
|
||||
? { ...item, text: instruction }
|
||||
: item
|
||||
))
|
||||
: [{ type: 'text', text: instruction }];
|
||||
return { ...message, metadata, content };
|
||||
}
|
||||
|
||||
export function enforcePageGenerationCursorRuntime(userMessage, {
|
||||
rawToolMode = 'chat',
|
||||
taskType = null,
|
||||
forceDeepReasoning = false,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env })) {
|
||||
return {
|
||||
userMessage,
|
||||
rawToolMode,
|
||||
taskType,
|
||||
forceDeepReasoning,
|
||||
requiredExecutor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const taskText = extractAiderDevelopmentTask(userMessage);
|
||||
const skill = selectedChatSkill(userMessage);
|
||||
const taskKind = resolveCursorTaskKind(taskText, skill);
|
||||
if (!taskKind) {
|
||||
return {
|
||||
userMessage,
|
||||
rawToolMode,
|
||||
taskType,
|
||||
forceDeepReasoning,
|
||||
requiredExecutor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const codeExecutor = resolvePreferredCodeExecutor(env);
|
||||
const message = rewriteUserMessageForCursorTask(userMessage, taskText, taskKind);
|
||||
const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
|
||||
? { ...message.metadata }
|
||||
: {};
|
||||
const runMetadata = metadata.memindRun && typeof metadata.memindRun === 'object' && !Array.isArray(metadata.memindRun)
|
||||
? { ...metadata.memindRun }
|
||||
: {};
|
||||
|
||||
metadata.memindRun = {
|
||||
...runMetadata,
|
||||
executor: codeExecutor,
|
||||
pageCursorDefault: true,
|
||||
cursorTaskKind: taskKind,
|
||||
suggestedDelivery: resolveCursorSuggestedDelivery(taskKind),
|
||||
selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill),
|
||||
};
|
||||
|
||||
return {
|
||||
userMessage: { ...message, metadata },
|
||||
rawToolMode: 'code',
|
||||
taskType: PAGE_CURSOR_TASK_TYPE,
|
||||
forceDeepReasoning: true,
|
||||
requiredExecutor: codeExecutor,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCursorFirstAgentExecution(userMessage, runOptions = {}, {
|
||||
routingDecision = null,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
// Cursor 仅服务于显式页面/代码任务(enforcePageGenerationCursorRuntime)。
|
||||
// 普通 Agent 编排走 Goose/DeepSeek,不因 MEMIND_CURSOR_AGENT_TASKS_DEFAULT 全量切 Cursor。
|
||||
void userMessage;
|
||||
void runOptions;
|
||||
void routingDecision;
|
||||
void env;
|
||||
return { userMessage, runOptions, cursorFirst: false };
|
||||
}
|
||||
|
||||
export function __cursorPageRoutingTestExports() {
|
||||
return {
|
||||
PAGE_CURSOR_TASK_TYPE,
|
||||
CURSOR_TASK_KIND,
|
||||
cursorPageTasksDefaultEnabled,
|
||||
cursorAgentTasksDefaultEnabled,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
enforcePageGenerationCursorRuntime,
|
||||
isPageCursorDefaultCandidate,
|
||||
resolveCursorTaskKind,
|
||||
applyCursorFirstAgentExecution,
|
||||
} from './cursor-page-routing.mjs';
|
||||
|
||||
const enabledEnv = {
|
||||
MEMIND_CURSOR_EXECUTOR_ENABLED: '1',
|
||||
MEMIND_CURSOR_PAGE_TASKS_DEFAULT: '1',
|
||||
};
|
||||
|
||||
test('page cursor default matches poem page requests', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
||||
metadata: { displayText: '帮我写首诗,并做成页面', userVisible: true },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
|
||||
assert.equal(resolveCursorTaskKind('帮我写首诗,并做成页面'), 'page_generation');
|
||||
});
|
||||
|
||||
test('page cursor default matches survey page-data requests', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
||||
metadata: { displayText: '帮我做一个调查问卷,结果存 PG' },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
|
||||
assert.equal(resolveCursorTaskKind('帮我做一个调查问卷,结果存 PG'), 'page_data');
|
||||
});
|
||||
|
||||
test('page cursor default matches excel analysis requests', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
|
||||
metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true);
|
||||
assert.equal(resolveCursorTaskKind('帮我分析这个 Excel 表格的数据趋势'), 'excel_analysis');
|
||||
});
|
||||
|
||||
test('page cursor default skips generic empty page requests', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我做一个页面吧' }],
|
||||
metadata: { displayText: '帮我做一个页面吧' },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false);
|
||||
});
|
||||
|
||||
test('page cursor default skips page-data dev repair requests', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '问卷页面提交失败,帮我排查 Page Data 绑定' }],
|
||||
metadata: { displayText: '问卷页面提交失败,帮我排查 Page Data 绑定' },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false);
|
||||
});
|
||||
|
||||
test('enforcePageGenerationCursorRuntime switches page generation to code cursor executor', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
||||
metadata: { displayText: '帮我写首诗,并做成页面' },
|
||||
};
|
||||
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
|
||||
assert.equal(enforced.rawToolMode, 'code');
|
||||
assert.equal(enforced.taskType, 'h5_chat_code_task');
|
||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.executor, 'cursor');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.pageCursorDefault, true);
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_generation');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'static-page-publish');
|
||||
assert.match(
|
||||
enforced.userMessage.content[0].text,
|
||||
/MindSpace 工作区生成或更新 public\/\*\.html/,
|
||||
);
|
||||
});
|
||||
|
||||
test('enforcePageGenerationCursorRuntime switches survey tasks to cursor page-data flow', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }],
|
||||
metadata: { displayText: '帮我做一个调查问卷,结果存 PG' },
|
||||
};
|
||||
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
|
||||
assert.equal(enforced.rawToolMode, 'code');
|
||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_data');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'page-data-collect');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.suggestedDelivery, 'mindspace_page_data');
|
||||
assert.match(enforced.userMessage.content[0].text, /Page Data survey task via TKMind 智趣 executor/);
|
||||
assert.match(enforced.userMessage.content[0].text, /page-data-api-usage\.md/);
|
||||
});
|
||||
|
||||
test('enforcePageGenerationCursorRuntime switches excel analysis to cursor flow', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }],
|
||||
metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' },
|
||||
};
|
||||
const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv });
|
||||
assert.equal(enforced.rawToolMode, 'code');
|
||||
assert.equal(enforced.requiredExecutor, 'cursor');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'excel_analysis');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'excel-analyst');
|
||||
assert.match(enforced.userMessage.content[0].text, /Excel analysis task via TKMind 智趣 executor/);
|
||||
});
|
||||
|
||||
test('page cursor default is disabled without env flag', () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我写首诗,并做成页面' }],
|
||||
metadata: { displayText: '帮我写首诗,并做成页面' },
|
||||
};
|
||||
assert.equal(isPageCursorDefaultCandidate(userMessage, { env: {} }), false);
|
||||
});
|
||||
|
||||
test('applyCursorFirstAgentExecution no longer forces cursor for generic agent tasks', () => {
|
||||
const enabledEnv = {
|
||||
MEMIND_CURSOR_EXECUTOR_ENABLED: '1',
|
||||
MEMIND_CURSOR_AGENT_TASKS_DEFAULT: '1',
|
||||
};
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我查一下今天的热点新闻并总结' }],
|
||||
metadata: { memindRun: {} },
|
||||
};
|
||||
const applied = applyCursorFirstAgentExecution(userMessage, { toolMode: 'chat' }, {
|
||||
routingDecision: 'agent_orchestration',
|
||||
env: enabledEnv,
|
||||
});
|
||||
assert.equal(applied.cursorFirst, false);
|
||||
assert.equal(applied.runOptions.toolMode, 'chat');
|
||||
});
|
||||
|
||||
test('applyCursorFirstAgentExecution skips direct chat routes', () => {
|
||||
const enabledEnv = {
|
||||
MEMIND_CURSOR_EXECUTOR_ENABLED: '1',
|
||||
MEMIND_CURSOR_AGENT_TASKS_DEFAULT: '1',
|
||||
};
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '你好' }],
|
||||
metadata: { memindRun: {} },
|
||||
};
|
||||
const applied = applyCursorFirstAgentExecution(userMessage, { toolMode: 'chat' }, {
|
||||
routingDecision: 'direct_chat',
|
||||
env: enabledEnv,
|
||||
});
|
||||
assert.equal(applied.cursorFirst, false);
|
||||
});
|
||||
+25
-103
@@ -1,5 +1,5 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useLayoutEffect, useRef, useState, type ClipboardEvent, type MutableRefObject } from 'react';
|
||||
import { BrainCircuit, Database, Image, ImageOff, ImagePlus } from 'lucide-react';
|
||||
import { BrainCircuit } from 'lucide-react';
|
||||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||
import { openAvatarPicker } from '../utils/userAvatar';
|
||||
import {
|
||||
@@ -11,11 +11,7 @@ import {
|
||||
} from '../utils/chatSkills';
|
||||
import { getMessageSaveActions } from '../utils/messageSave';
|
||||
import { getDisplayText } from '../utils/message';
|
||||
import {
|
||||
IMAGE_GENERATION_MODE_LABELS,
|
||||
nextImageGenerationMode,
|
||||
type ImageGenerationMode,
|
||||
} from '../utils/imageGeneration';
|
||||
import type { ImageGenerationMode } from '../utils/imageGeneration';
|
||||
import {
|
||||
downloadChatMessageDocx,
|
||||
} from '../api/client';
|
||||
@@ -39,6 +35,7 @@ import { ChatSkillPicker } from './ChatSkillPicker';
|
||||
import { ChatSkillIcon } from './ChatSkillIcons';
|
||||
import { PageTemplateShopModal } from './PageTemplateShopModal';
|
||||
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
|
||||
import { useTaskLoadingTip } from '../hooks/useTaskLoadingTip';
|
||||
import { ChatSharePreviewModal } from './ChatSharePreviewModal';
|
||||
import { ChatPlazaPublishModal } from './ChatPlazaPublishModal';
|
||||
import { MessageList } from './MessageList';
|
||||
@@ -183,6 +180,7 @@ export function ChatPanel({
|
||||
onBalanceUpdate,
|
||||
onGrantedSkillsUpdate,
|
||||
onOpenRecharge,
|
||||
taskLoadingTip = null,
|
||||
}: {
|
||||
variant: 'full' | 'compact';
|
||||
user?: PortalUser | null;
|
||||
@@ -231,6 +229,7 @@ export function ChatPanel({
|
||||
onBalanceUpdate?: (balanceCents: number) => void;
|
||||
onGrantedSkillsUpdate?: (skills: string[]) => void;
|
||||
onOpenRecharge?: () => void;
|
||||
taskLoadingTip?: string | null;
|
||||
}) {
|
||||
const online = useNetworkStatus();
|
||||
const [input, setInput] = useState('');
|
||||
@@ -240,12 +239,8 @@ export function ChatPanel({
|
||||
const [plazaPublishSource, setPlazaPublishSource] = useState<Message | null>(null);
|
||||
const [voiceNotice, setVoiceNotice] = useState<string | null>(null);
|
||||
const [forceDeepReasoning, setForceDeepReasoning] = useState(false);
|
||||
const [pgRequired, setPgRequired] = useState(false);
|
||||
const [imageGenerationMode, setImageGenerationMode] = useState<ImageGenerationMode>('auto');
|
||||
const [imageGenerationTipVisible, setImageGenerationTipVisible] = useState(false);
|
||||
const [pgTipVisible, setPgTipVisible] = useState(false);
|
||||
const [deepReasoningTipVisible, setDeepReasoningTipVisible] = useState(false);
|
||||
const [chatControlOnboardingStep, setChatControlOnboardingStep] = useState<0 | 1 | 2 | null>(null);
|
||||
const [chatControlOnboardingStep, setChatControlOnboardingStep] = useState<0 | 1 | null>(null);
|
||||
const [templateShopOpen, setTemplateShopOpen] = useState(false);
|
||||
const [activeTemplatePrefill, setActiveTemplatePrefill] = useState<{
|
||||
skillId: string;
|
||||
@@ -261,8 +256,6 @@ export function ChatPanel({
|
||||
const [voiceStopSignal, setVoiceStopSignal] = useState(0);
|
||||
const pendingSkillRef = useRef<string | null>(null);
|
||||
const chatInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const imageGenerationTipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pgTipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const deepReasoningTipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [randomPrompt] = useState(
|
||||
() => CHAT_PLACEHOLDER_PROMPTS[Math.floor(Math.random() * CHAT_PLACEHOLDER_PROMPTS.length)],
|
||||
@@ -282,6 +275,7 @@ export function ChatPanel({
|
||||
}>(null);
|
||||
const prevMessageCountRef = useRef(messages.length);
|
||||
const showAssistantTyping = chatState === 'waiting' || chatState === 'streaming';
|
||||
const displayLoadingTip = useTaskLoadingTip(showAssistantTyping, taskLoadingTip);
|
||||
|
||||
inputRef.current = input;
|
||||
pendingImagesRef.current = pendingImages;
|
||||
@@ -337,19 +331,12 @@ export function ChatPanel({
|
||||
setChatControlOnboardingStep(0);
|
||||
const timers = [
|
||||
window.setTimeout(() => setChatControlOnboardingStep(1), 2_200),
|
||||
window.setTimeout(() => setChatControlOnboardingStep(2), 4_400),
|
||||
window.setTimeout(() => setChatControlOnboardingStep(null), 6_600),
|
||||
window.setTimeout(() => setChatControlOnboardingStep(null), 4_400),
|
||||
];
|
||||
return () => timers.forEach((timer) => window.clearTimeout(timer));
|
||||
}, [session?.id]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (imageGenerationTipTimerRef.current) {
|
||||
clearTimeout(imageGenerationTipTimerRef.current);
|
||||
}
|
||||
if (pgTipTimerRef.current) {
|
||||
clearTimeout(pgTipTimerRef.current);
|
||||
}
|
||||
if (deepReasoningTipTimerRef.current) {
|
||||
clearTimeout(deepReasoningTipTimerRef.current);
|
||||
}
|
||||
@@ -372,26 +359,6 @@ export function ChatPanel({
|
||||
[],
|
||||
);
|
||||
|
||||
const showImageGenerationSlowTip = useCallback(() => {
|
||||
showTransientControlTip(setImageGenerationTipVisible, imageGenerationTipTimerRef);
|
||||
}, [showTransientControlTip]);
|
||||
|
||||
const handlePgToggle = useCallback(() => {
|
||||
setPgRequired((current) => {
|
||||
const next = !current;
|
||||
if (next) {
|
||||
showTransientControlTip(setPgTipVisible, pgTipTimerRef);
|
||||
} else {
|
||||
setPgTipVisible(false);
|
||||
if (pgTipTimerRef.current) {
|
||||
clearTimeout(pgTipTimerRef.current);
|
||||
pgTipTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [showTransientControlTip]);
|
||||
|
||||
const handleDeepReasoningToggle = useCallback(() => {
|
||||
setForceDeepReasoning((current) => {
|
||||
const next = !current;
|
||||
@@ -408,11 +375,6 @@ export function ChatPanel({
|
||||
});
|
||||
}, [showTransientControlTip]);
|
||||
|
||||
const handleImageGenerationClick = useCallback(() => {
|
||||
setImageGenerationMode((current) => nextImageGenerationMode(current));
|
||||
showImageGenerationSlowTip();
|
||||
}, [showImageGenerationSlowTip]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = mainRef.current;
|
||||
if (!container) return;
|
||||
@@ -876,8 +838,8 @@ export function ChatPanel({
|
||||
await onSubmit(trimmed, imagesToSend, previewImagesToSend, {
|
||||
messageId: outgoingMessageId,
|
||||
forceDeepReasoning,
|
||||
pgRequired,
|
||||
imageGenerationMode,
|
||||
pgRequired: false,
|
||||
imageGenerationMode: 'auto',
|
||||
selectedChatSkill,
|
||||
fileAttachments: fileAttachmentsToSend,
|
||||
});
|
||||
@@ -907,8 +869,6 @@ export function ChatPanel({
|
||||
}, [
|
||||
activeTemplatePrefill,
|
||||
forceDeepReasoning,
|
||||
pgRequired,
|
||||
imageGenerationMode,
|
||||
input,
|
||||
onSubmit,
|
||||
onUploadFile,
|
||||
@@ -1118,6 +1078,7 @@ export function ChatPanel({
|
||||
<MessageList
|
||||
messages={messages}
|
||||
streaming={showAssistantTyping}
|
||||
typingTip={displayLoadingTip}
|
||||
onAvatarClick={compact ? undefined : openAvatarPicker}
|
||||
onSaveAsPage={openSaveActions}
|
||||
onShareToPlaza={openPlazaPublish}
|
||||
@@ -1323,14 +1284,19 @@ export function ChatPanel({
|
||||
className={`input chat-input-field chat-input-task-status${uploadFieldClass}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={taskInputStatusText}
|
||||
aria-label={[taskInputStatusText, displayLoadingTip].filter(Boolean).join(' ')}
|
||||
>
|
||||
<span className="chat-input-task-status-text">{taskInputStatusText}</span>
|
||||
<span className="typing-dots chat-input-task-status-dots" aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<div className="chat-input-task-status-main">
|
||||
<span className="chat-input-task-status-text">{taskInputStatusText}</span>
|
||||
<span className="typing-dots chat-input-task-status-dots" aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
{displayLoadingTip ? (
|
||||
<span className="chat-input-task-status-tip">{displayLoadingTip}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
@@ -1396,51 +1362,7 @@ export function ChatPanel({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`chat-deep-reasoning-toggle chat-icon-toggle${pgRequired ? ' is-active' : ''}${pgTipVisible ? ' chat-icon-toggle-tip-active' : ''}${chatControlOnboardingStep === 1 ? ' chat-control-onboarding-active' : ''}`}
|
||||
disabled={voiceDisabled}
|
||||
role="checkbox"
|
||||
aria-checked={pgRequired}
|
||||
aria-label="使用 PostgreSQL 数据空间"
|
||||
title="点击开启后,本轮生成的可保存数据必须使用你的专属 PostgreSQL 数据空间"
|
||||
onClick={handlePgToggle}
|
||||
>
|
||||
<Database className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
<span className="chat-icon-toggle-label">db</span>
|
||||
{pgTipVisible && (
|
||||
<span className="chat-control-onboarding-tip chat-icon-toggle-tip" role="status">
|
||||
数据将写入你的 PostgreSQL 专属空间
|
||||
</span>
|
||||
)}
|
||||
{chatControlOnboardingStep === 1 && (
|
||||
<span className="chat-control-onboarding-tip" role="status">需要长期使用的数据,放进专属空间</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`chat-deep-reasoning-toggle chat-icon-toggle chat-image-generation-toggle is-${imageGenerationMode}${imageGenerationMode === 'required' ? ' is-active' : ''}${imageGenerationTipVisible ? ' chat-icon-toggle-tip-active' : ''}`}
|
||||
disabled={voiceDisabled}
|
||||
role="checkbox"
|
||||
aria-checked={imageGenerationMode === 'auto' ? 'mixed' : imageGenerationMode === 'required'}
|
||||
aria-label={`AI 配图:${IMAGE_GENERATION_MODE_LABELS[imageGenerationMode]}`}
|
||||
title={`AI 配图:${IMAGE_GENERATION_MODE_LABELS[imageGenerationMode]}。启用后会加载智能生图 skill,自动补全场景、构图、光线和负面约束。点击切换为${IMAGE_GENERATION_MODE_LABELS[nextImageGenerationMode(imageGenerationMode)]}`}
|
||||
onClick={handleImageGenerationClick}
|
||||
>
|
||||
{imageGenerationMode === 'required' ? (
|
||||
<ImagePlus className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
) : imageGenerationMode === 'disabled' ? (
|
||||
<ImageOff className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
) : (
|
||||
<Image className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
)}
|
||||
{imageGenerationTipVisible && (
|
||||
<span className="chat-control-onboarding-tip chat-icon-toggle-tip" role="status">
|
||||
图片生成较慢,请耐心等待
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`chat-deep-reasoning-toggle chat-icon-toggle${forceDeepReasoning ? ' is-active' : ''}${deepReasoningTipVisible ? ' chat-icon-toggle-tip-active' : ''}${chatControlOnboardingStep === 2 ? ' chat-control-onboarding-active' : ''}`}
|
||||
className={`chat-deep-reasoning-toggle chat-icon-toggle${forceDeepReasoning ? ' is-active' : ''}${deepReasoningTipVisible ? ' chat-icon-toggle-tip-active' : ''}${chatControlOnboardingStep === 1 ? ' chat-control-onboarding-active' : ''}`}
|
||||
disabled={voiceDisabled}
|
||||
role="checkbox"
|
||||
aria-checked={forceDeepReasoning}
|
||||
@@ -1455,7 +1377,7 @@ export function ChatPanel({
|
||||
深度推理耗时较长,请耐心等待
|
||||
</span>
|
||||
)}
|
||||
{chatControlOnboardingStep === 2 && (
|
||||
{chatControlOnboardingStep === 1 && (
|
||||
<span className="chat-control-onboarding-tip" role="status">复杂问题,开启深度推理</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
+112
-29
@@ -6,6 +6,12 @@ import { mergeMessageContent } from './message-stream.mjs';
|
||||
import { reconcileAgentSession } from './session-reconcile.mjs';
|
||||
import { resolveSessionAccess } from './session-broker.mjs';
|
||||
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
||||
import { executeWechatCursorAgentRun } from './wechat-cursor-agent-run.mjs';
|
||||
import { resolveWechatCursorExecutorEligible } from './wechat-cursor-executor-policy.mjs';
|
||||
import {
|
||||
isWechatCursorChannelReply,
|
||||
prepareWechatCursorPageDelivery,
|
||||
} from './wechat-cursor-page-delivery.mjs';
|
||||
import {
|
||||
downloadTemporaryMedia,
|
||||
persistWechatAttachment,
|
||||
@@ -532,6 +538,7 @@ function hasAnyToolRequest(messages = []) {
|
||||
}
|
||||
|
||||
function isSuspiciousBareCompletionReply(reply, intent) {
|
||||
if (isWechatCursorChannelReply(reply)) return false;
|
||||
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
|
||||
if (!isBareCompletionText(reply?.text)) return false;
|
||||
if (extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0) return false;
|
||||
@@ -575,6 +582,7 @@ async function isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists = d
|
||||
}
|
||||
|
||||
function isMissingRequiredPublishSkill(reply, intent) {
|
||||
if (isWechatCursorChannelReply(reply)) return false;
|
||||
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
|
||||
const wroteHtml = extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0;
|
||||
if (!wroteHtml) return false;
|
||||
@@ -1022,6 +1030,11 @@ export function shouldRetryHtmlGenerationReply({
|
||||
confirmedArtifacts = [],
|
||||
hasValidLinkInReply = false,
|
||||
}) {
|
||||
if (isWechatCursorChannelReply(reply)) {
|
||||
return nonStubHtmlArtifacts(confirmedArtifacts).length === 0
|
||||
&& !hasAnyPublicHtmlLinkInReply(reply);
|
||||
}
|
||||
|
||||
if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false;
|
||||
|
||||
const replyHasPublicLinks = hasAnyPublicHtmlLinkInReply(reply);
|
||||
@@ -1615,6 +1628,8 @@ export function createWechatMpService({
|
||||
htmlDeliveryAuthority = null,
|
||||
pageDataFinishGuard = null,
|
||||
pageDataDeliveryReviewer = null,
|
||||
wechatCursorExecutorPolicyService = null,
|
||||
agentRunGateway = null,
|
||||
wechatFetch = undiciFetch,
|
||||
linkExists = defaultPublicHtmlLinkExists,
|
||||
h5Root = '',
|
||||
@@ -2874,8 +2889,10 @@ export function createWechatMpService({
|
||||
logger,
|
||||
intentTransactionEnabled: isIntentTransactionEnabled(),
|
||||
});
|
||||
let userPublishDir = null;
|
||||
try {
|
||||
const requestStartedAt = Date.now();
|
||||
userPublishDir = await userAuth.resolveWorkingDir(user.userId);
|
||||
const agentPrompt =
|
||||
wechatIntent.kind === 'page.generate'
|
||||
? buildPageGenerateAgentPrompt(intent, {
|
||||
@@ -2900,36 +2917,97 @@ export function createWechatMpService({
|
||||
const activeAgentPrompt =
|
||||
pageAttempt === 1 ? agentPrompt : buildImmediateContextPageRepairPrompt(intent);
|
||||
const activeRequestId = pageAttempt === 1 ? requestId : crypto.randomUUID();
|
||||
reply = await executeSessionReply(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
activeRequestId,
|
||||
activeAgentPrompt,
|
||||
buildIntentMetadata(intent, {
|
||||
mediaAnalysisEnabled,
|
||||
imagePolicy,
|
||||
pgRequired: isPageDataRequest,
|
||||
}),
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
reply = null;
|
||||
let wechatCursorPolicy = null;
|
||||
if (
|
||||
pageAttempt === 1
|
||||
&& wechatCursorExecutorPolicyService?.getEffectivePolicy
|
||||
&& agentRunGateway
|
||||
) {
|
||||
try {
|
||||
wechatCursorPolicy = await wechatCursorExecutorPolicyService.getEffectivePolicy(
|
||||
user.userId,
|
||||
user,
|
||||
);
|
||||
} catch (policyErr) {
|
||||
logger.warn?.('WeChat MP cursor policy lookup failed:', policyErr);
|
||||
}
|
||||
}
|
||||
const useWechatCursorChannel = resolveWechatCursorExecutorEligible({
|
||||
user,
|
||||
intentKind: wechatIntent.kind,
|
||||
policy: wechatCursorPolicy,
|
||||
});
|
||||
if (useWechatCursorChannel) {
|
||||
try {
|
||||
reply = await executeWechatCursorAgentRun({
|
||||
agentRunGateway,
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
userMessage,
|
||||
preserveAgentPrompt: sessionPageContinuation,
|
||||
requestId: activeRequestId,
|
||||
displayText: String(intent?.displayText ?? intent?.agentText ?? '').trim(),
|
||||
agentPrompt: activeAgentPrompt,
|
||||
intentKind: wechatIntent.kind,
|
||||
timeoutMs: agentReplyTimeoutMs,
|
||||
logger,
|
||||
});
|
||||
const buildCanonicalUrl = (relativePath) => {
|
||||
const base = String(config.publicBaseUrl ?? '').replace(/\/+$/, '');
|
||||
const normalized = String(relativePath ?? '')
|
||||
.replace(/^\/+/, '')
|
||||
.split('/')
|
||||
.map(encodeURIComponent)
|
||||
.join('/');
|
||||
return `${base}/MindSpace/${encodeURIComponent(user.userId)}/${normalized}`;
|
||||
};
|
||||
reply = prepareWechatCursorPageDelivery({
|
||||
reply,
|
||||
intent,
|
||||
publishDir: userPublishDir,
|
||||
requestStartedAt,
|
||||
buildCanonicalUrl,
|
||||
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||
});
|
||||
} catch (cursorErr) {
|
||||
logger.warn?.('WeChat MP cursor channel failed:', cursorErr);
|
||||
if (wechatCursorPolicy?.fallbackToDeepseek === false) {
|
||||
throw cursorErr;
|
||||
}
|
||||
reply = null;
|
||||
}
|
||||
}
|
||||
if (!reply) {
|
||||
reply = await executeSessionReply(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
activeRequestId,
|
||||
activeAgentPrompt,
|
||||
buildIntentMetadata(intent, {
|
||||
mediaAnalysisEnabled,
|
||||
imagePolicy,
|
||||
pgRequired: isPageDataRequest,
|
||||
}),
|
||||
submitReply: submitSessionReply
|
||||
? ({ requestId: replyRequestId, userMessage }) =>
|
||||
submitSessionReply({
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
requestId: replyRequestId,
|
||||
userMessage,
|
||||
options: { requireHistoricalImageIsolation: true },
|
||||
})
|
||||
: null,
|
||||
timeoutMs: agentReplyTimeoutMs,
|
||||
},
|
||||
);
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
userMessage,
|
||||
preserveAgentPrompt: sessionPageContinuation,
|
||||
}),
|
||||
submitReply: submitSessionReply
|
||||
? ({ requestId: replyRequestId, userMessage }) =>
|
||||
submitSessionReply({
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
requestId: replyRequestId,
|
||||
userMessage,
|
||||
options: { requireHistoricalImageIsolation: true },
|
||||
})
|
||||
: null,
|
||||
timeoutMs: agentReplyTimeoutMs,
|
||||
},
|
||||
);
|
||||
}
|
||||
generatedImages = collectWechatGeneratedImages(replyRequestMessages(reply));
|
||||
if (imagePolicy.standaloneImageMode === 'required' && generatedImages.length === 0) {
|
||||
const error = new Error('图片生成没有获得本轮新的有效位图,请稍后重试');
|
||||
@@ -3003,8 +3081,9 @@ export function createWechatMpService({
|
||||
replyHasPublicLinks,
|
||||
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||
repairContext: {
|
||||
publishDir: resolveUserPublishDir(user.userId),
|
||||
publishDir: userPublishDir,
|
||||
},
|
||||
wechatCursorChannel: isWechatCursorChannelReply(reply),
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry') {
|
||||
if (sessionPageContinuation && pageAttempt < maxImmediateContextPageAttempts) {
|
||||
@@ -3293,6 +3372,9 @@ export function createWechatMpService({
|
||||
}
|
||||
const retryId = crypto.randomUUID();
|
||||
const retryStartedAt = Date.now();
|
||||
if (!userPublishDir) {
|
||||
userPublishDir = await userAuth.resolveWorkingDir(user.userId);
|
||||
}
|
||||
const retryPageContinuation = sessionPageContinuation && !historicalImageError;
|
||||
const retryPrompt =
|
||||
wechatIntent.kind === 'page.generate'
|
||||
@@ -3407,8 +3489,9 @@ export function createWechatMpService({
|
||||
replyHasPublicLinks,
|
||||
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||
repairContext: {
|
||||
publishDir: resolveUserPublishDir(user.userId),
|
||||
publishDir: userPublishDir,
|
||||
},
|
||||
wechatCursorChannel: isWechatCursorChannelReply(reply),
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
|
||||
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
||||
|
||||
Reference in New Issue
Block a user