522 lines
15 KiB
JavaScript
522 lines
15 KiB
JavaScript
|
|
import crypto from 'node:crypto';
|
|||
|
|
import fs from 'node:fs';
|
|||
|
|
import os from 'node:os';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
import { spawn } from 'node:child_process';
|
|||
|
|
import { normalizeDeliveryRelativePath } from './mindspace-delivery-contract.mjs';
|
|||
|
|
import {
|
|||
|
|
expandHome,
|
|||
|
|
resolveCursorAgentBin as resolveSharedCursorAgentBin,
|
|||
|
|
} from './cursor-agent-launch.mjs';
|
|||
|
|
|
|||
|
|
export const HELP_ESCALATION_ACK_TEXT =
|
|||
|
|
'已转工程师处理,请稍候。处理完成后会通知你结果。';
|
|||
|
|
|
|||
|
|
export const HELP_ESCALATION_DETAILS_PROMPT = [
|
|||
|
|
'请补充具体问题后再发送,例如:',
|
|||
|
|
'help 我的页面 tang-dynasty 打不开',
|
|||
|
|
'help 聊天一直卡住',
|
|||
|
|
'help 定时任务没有推送',
|
|||
|
|
'',
|
|||
|
|
'工程师需要知道:出了什么问题、相关页面或链接、你期望的结果。',
|
|||
|
|
].join('\n');
|
|||
|
|
|
|||
|
|
export const HELP_ESCALATION_STATUS = {
|
|||
|
|
QUEUED: 'queued',
|
|||
|
|
RUNNING: 'running',
|
|||
|
|
SUCCEEDED: 'succeeded',
|
|||
|
|
FAILED: 'failed',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const HELP_INTENT_PATTERNS = [
|
|||
|
|
/^help(?:\s+|$)/i,
|
|||
|
|
/^\/help(?:\s+|$)/i,
|
|||
|
|
/^帮助(?:\s+|$)/u,
|
|||
|
|
/^人工帮助(?:\s+|$)/u,
|
|||
|
|
/^找工程师(?:\s+|$)/u,
|
|||
|
|
/^升级处理(?:\s+|$)/u,
|
|||
|
|
/^工程师介入(?:\s+|$)/u,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const HELP_COMMAND_PREFIX_PATTERNS = [
|
|||
|
|
/^help[,,]?\s*/i,
|
|||
|
|
/^\/help\s*/i,
|
|||
|
|
/^帮助\s*/u,
|
|||
|
|
/^人工帮助\s*/u,
|
|||
|
|
/^找工程师\s*/u,
|
|||
|
|
/^升级处理\s*/u,
|
|||
|
|
/^工程师介入\s*/u,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function envFlag(value, fallback = false) {
|
|||
|
|
const raw = String(value ?? '').trim().toLowerCase();
|
|||
|
|
if (!raw) return fallback;
|
|||
|
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nowMs() {
|
|||
|
|
return Date.now();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function extractHelpEscalationText(input) {
|
|||
|
|
if (typeof input === 'string') return input.trim();
|
|||
|
|
if (!input || typeof input !== 'object') return '';
|
|||
|
|
if (typeof input.text === 'string') return input.text.trim();
|
|||
|
|
if (Array.isArray(input.content)) {
|
|||
|
|
return input.content
|
|||
|
|
.map((item) => {
|
|||
|
|
if (!item || typeof item !== 'object') return '';
|
|||
|
|
if (item.type === 'text') return String(item.text ?? '').trim();
|
|||
|
|
return '';
|
|||
|
|
})
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.join('\n')
|
|||
|
|
.trim();
|
|||
|
|
}
|
|||
|
|
return String(input.value ?? '').trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function normalizeHelpEscalationCommandText(text) {
|
|||
|
|
const normalized = String(text ?? '').trim();
|
|||
|
|
if (!normalized) return '';
|
|||
|
|
const blocks = normalized.split(/\n\n+/);
|
|||
|
|
const lastBlock = (blocks[blocks.length - 1] ?? normalized).trim();
|
|||
|
|
const candidate = lastBlock || normalized;
|
|||
|
|
return candidate.replace(/^help[,,]\s*/i, 'help ');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function isHelpEscalationIntent(text) {
|
|||
|
|
const normalized = String(text ?? '').trim();
|
|||
|
|
if (!normalized) return false;
|
|||
|
|
const commandText = normalizeHelpEscalationCommandText(normalized);
|
|||
|
|
for (const candidate of [normalized, commandText]) {
|
|||
|
|
if (!candidate) continue;
|
|||
|
|
if (HELP_INTENT_PATTERNS.some((pattern) => pattern.test(candidate))) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function extractHelpEscalationProblemText(text) {
|
|||
|
|
const commandText = normalizeHelpEscalationCommandText(text);
|
|||
|
|
if (!commandText) return '';
|
|||
|
|
for (const pattern of HELP_COMMAND_PREFIX_PATTERNS) {
|
|||
|
|
if (!pattern.test(commandText)) continue;
|
|||
|
|
return commandText.replace(pattern, '').trim();
|
|||
|
|
}
|
|||
|
|
return commandText.trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function hasHelpEscalationProblemDescription(text) {
|
|||
|
|
return extractHelpEscalationProblemText(text).length >= 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const HELP_PAGE_PATH_PATTERN = /\b(public\/[^\s"'<>]+\.html)\b/i;
|
|||
|
|
|
|||
|
|
export function extractHelpEscalationPagePath(userText, context = {}) {
|
|||
|
|
const rawContextPath = context.relativePath ?? context.pagePath ?? context.workspaceRelativePath ?? '';
|
|||
|
|
const fromContext = normalizeDeliveryRelativePath(rawContextPath);
|
|||
|
|
if (fromContext) return fromContext;
|
|||
|
|
const text = String(userText ?? '');
|
|||
|
|
const match = text.match(HELP_PAGE_PATH_PATTERN);
|
|||
|
|
return match?.[1] ? match[1].replace(/\\/g, '/').trim() : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function extractHelpEscalationSessionId(escalation) {
|
|||
|
|
const context = escalation?.context ?? {};
|
|||
|
|
return String(
|
|||
|
|
context.sessionId
|
|||
|
|
?? context.agentSessionId
|
|||
|
|
?? escalation?.agentSessionId
|
|||
|
|
?? '',
|
|||
|
|
).trim() || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseJsonSafe(value, fallback = null) {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(value);
|
|||
|
|
} catch {
|
|||
|
|
return fallback;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function rowToEscalation(row) {
|
|||
|
|
if (!row) return null;
|
|||
|
|
return {
|
|||
|
|
id: row.id,
|
|||
|
|
userId: row.user_id,
|
|||
|
|
channel: row.channel,
|
|||
|
|
status: row.status,
|
|||
|
|
userText: row.user_text,
|
|||
|
|
context: parseJsonSafe(row.context_json, {}),
|
|||
|
|
agentSessionId: row.agent_session_id,
|
|||
|
|
resultText: row.result_text,
|
|||
|
|
agentOutput: row.agent_output,
|
|||
|
|
errorMessage: row.error_message,
|
|||
|
|
createdAt: Number(row.created_at),
|
|||
|
|
updatedAt: Number(row.updated_at),
|
|||
|
|
startedAt: row.started_at == null ? null : Number(row.started_at),
|
|||
|
|
completedAt: row.completed_at == null ? null : Number(row.completed_at),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function buildHelpEscalationPrompt(escalation, { memindRoot = process.cwd() } = {}) {
|
|||
|
|
const context = escalation?.context ?? {};
|
|||
|
|
const sessionId = context.sessionId ?? context.agentSessionId ?? escalation.agentSessionId ?? null;
|
|||
|
|
return [
|
|||
|
|
'你是 Memind 平台值班工程师。用户通过 help 指令请求人工介入,请在本机直接排查并修复问题。',
|
|||
|
|
'',
|
|||
|
|
'要求:',
|
|||
|
|
'1. 工作目录是 Memind 仓库,可以直接修改文件、运行脚本、重启相关服务。',
|
|||
|
|
'2. 优先修复用户问题,不要只做分析。',
|
|||
|
|
'3. 页面/链接类问题:必须先修复 delivery contract,再通过 Goose 原会话把链接补发给用户(禁止只改库不通知)。',
|
|||
|
|
'4. 页面链接补救优先运行(把参数换成工单上下文中的真实值):',
|
|||
|
|
' node scripts/help-remediate-page-delivery.mjs \\',
|
|||
|
|
' --user-id=<用户ID> \\',
|
|||
|
|
' --session-id=<会话ID> \\',
|
|||
|
|
' --page=public/<页面文件名>.html \\',
|
|||
|
|
' --via-goose',
|
|||
|
|
'5. 若用户描述不清,可在 userMessage 里继续引导其补充;问题已解决则明确告知链接与下一步。',
|
|||
|
|
'6. 完成后用中文给出简短结果,说明做了什么、用户接下来怎么用。',
|
|||
|
|
'7. 最后一行单独输出 JSON:{"status":"succeeded|failed","userMessage":"给用户的中文回复"}',
|
|||
|
|
'',
|
|||
|
|
`Memind 根目录: ${memindRoot}`,
|
|||
|
|
`工单 ID: ${escalation.id}`,
|
|||
|
|
`用户 ID: ${escalation.userId}`,
|
|||
|
|
`渠道: ${escalation.channel}`,
|
|||
|
|
`用户原文: ${escalation.userText}`,
|
|||
|
|
`会话 ID: ${sessionId ?? '无'}`,
|
|||
|
|
`OpenID: ${context.openid ?? '无'}`,
|
|||
|
|
`Request ID: ${context.requestId ?? '无'}`,
|
|||
|
|
'',
|
|||
|
|
'相关上下文 JSON:',
|
|||
|
|
JSON.stringify(context, null, 2),
|
|||
|
|
].join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function parseHelpEscalationAgentResult(stdout) {
|
|||
|
|
const text = String(stdout ?? '').trim();
|
|||
|
|
if (!text) {
|
|||
|
|
return {
|
|||
|
|
status: HELP_ESCALATION_STATUS.FAILED,
|
|||
|
|
userMessage: '工程师处理失败:Cursor 未返回结果。',
|
|||
|
|
rawOutput: text,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|||
|
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|||
|
|
const candidate = parseJsonSafe(lines[index], null);
|
|||
|
|
if (
|
|||
|
|
candidate
|
|||
|
|
&& typeof candidate === 'object'
|
|||
|
|
&& typeof candidate.userMessage === 'string'
|
|||
|
|
&& candidate.userMessage.trim()
|
|||
|
|
) {
|
|||
|
|
const status = String(candidate.status ?? '').trim().toLowerCase() === 'failed'
|
|||
|
|
? HELP_ESCALATION_STATUS.FAILED
|
|||
|
|
: HELP_ESCALATION_STATUS.SUCCEEDED;
|
|||
|
|
return {
|
|||
|
|
status,
|
|||
|
|
userMessage: candidate.userMessage.trim(),
|
|||
|
|
rawOutput: text,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
status: HELP_ESCALATION_STATUS.SUCCEEDED,
|
|||
|
|
userMessage: text.slice(-4000),
|
|||
|
|
rawOutput: text,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function resolveCursorHelpAgentBin(env = process.env) {
|
|||
|
|
return resolveSharedCursorAgentBin(env);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function resolveCursorHelpWorkspace(env = process.env, fallbackRoot = process.cwd()) {
|
|||
|
|
return expandHome(env.MEMIND_CURSOR_HELP_WORKSPACE ?? fallbackRoot);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function runCursorHelpAgent({
|
|||
|
|
prompt,
|
|||
|
|
workspace,
|
|||
|
|
agentBin = resolveCursorHelpAgentBin(),
|
|||
|
|
env = process.env,
|
|||
|
|
logger = console,
|
|||
|
|
timeoutMs = Number(env.MEMIND_CURSOR_HELP_TIMEOUT_MS ?? 30 * 60 * 1000),
|
|||
|
|
}) {
|
|||
|
|
const args = [
|
|||
|
|
'--print',
|
|||
|
|
'--trust',
|
|||
|
|
'--force',
|
|||
|
|
'--approve-mcps',
|
|||
|
|
'--output-format',
|
|||
|
|
'text',
|
|||
|
|
'--workspace',
|
|||
|
|
workspace,
|
|||
|
|
prompt,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
logger.info?.('[cursor-help] spawning agent', {
|
|||
|
|
agentBin,
|
|||
|
|
workspace,
|
|||
|
|
timeoutMs,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const child = spawn(agentBin, args, {
|
|||
|
|
cwd: workspace,
|
|||
|
|
env: { ...env },
|
|||
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let stdout = '';
|
|||
|
|
let stderr = '';
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
child.kill('SIGTERM');
|
|||
|
|
reject(new Error(`Cursor help agent timed out after ${timeoutMs}ms`));
|
|||
|
|
}, timeoutMs);
|
|||
|
|
timer.unref?.();
|
|||
|
|
|
|||
|
|
child.stdout.on('data', (chunk) => {
|
|||
|
|
stdout += String(chunk);
|
|||
|
|
});
|
|||
|
|
child.stderr.on('data', (chunk) => {
|
|||
|
|
stderr += String(chunk);
|
|||
|
|
});
|
|||
|
|
child.on('error', (error) => {
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
reject(error);
|
|||
|
|
});
|
|||
|
|
child.on('close', (code) => {
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
if (code !== 0) {
|
|||
|
|
const error = new Error(
|
|||
|
|
stderr.trim() || stdout.trim() || `Cursor help agent exited with code ${code}`,
|
|||
|
|
);
|
|||
|
|
error.code = code;
|
|||
|
|
error.stdout = stdout;
|
|||
|
|
error.stderr = stderr;
|
|||
|
|
reject(error);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
resolve({ stdout, stderr });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function createHelpEscalationService({
|
|||
|
|
pool,
|
|||
|
|
logger = console,
|
|||
|
|
env = process.env,
|
|||
|
|
} = {}) {
|
|||
|
|
const enabled = envFlag(env.MEMIND_CURSOR_HELP_ENABLED, false);
|
|||
|
|
const memindRoot = resolveCursorHelpWorkspace(env);
|
|||
|
|
|
|||
|
|
async function enqueue({
|
|||
|
|
userId,
|
|||
|
|
channel,
|
|||
|
|
userText,
|
|||
|
|
context = {},
|
|||
|
|
agentSessionId = null,
|
|||
|
|
}) {
|
|||
|
|
if (!enabled) {
|
|||
|
|
throw new Error('Cursor help escalation is disabled');
|
|||
|
|
}
|
|||
|
|
const id = crypto.randomUUID();
|
|||
|
|
const now = nowMs();
|
|||
|
|
await pool.query(
|
|||
|
|
`INSERT INTO h5_help_escalations (
|
|||
|
|
id, user_id, channel, status, user_text, context_json, agent_session_id,
|
|||
|
|
created_at, updated_at
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|||
|
|
[
|
|||
|
|
id,
|
|||
|
|
userId,
|
|||
|
|
channel,
|
|||
|
|
HELP_ESCALATION_STATUS.QUEUED,
|
|||
|
|
userText,
|
|||
|
|
JSON.stringify(context ?? {}),
|
|||
|
|
agentSessionId,
|
|||
|
|
now,
|
|||
|
|
now,
|
|||
|
|
],
|
|||
|
|
);
|
|||
|
|
return getById(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function getById(id) {
|
|||
|
|
const [rows] = await pool.query(
|
|||
|
|
'SELECT * FROM h5_help_escalations WHERE id = ? LIMIT 1',
|
|||
|
|
[id],
|
|||
|
|
);
|
|||
|
|
return rowToEscalation(rows[0]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function claimNext(workerId = 'cursor-help-worker') {
|
|||
|
|
const connection = await pool.getConnection();
|
|||
|
|
try {
|
|||
|
|
await connection.beginTransaction();
|
|||
|
|
const [rows] = await connection.query(
|
|||
|
|
`SELECT *
|
|||
|
|
FROM h5_help_escalations
|
|||
|
|
WHERE status = ?
|
|||
|
|
ORDER BY created_at ASC
|
|||
|
|
LIMIT 1
|
|||
|
|
FOR UPDATE`,
|
|||
|
|
[HELP_ESCALATION_STATUS.QUEUED],
|
|||
|
|
);
|
|||
|
|
const row = rows[0];
|
|||
|
|
if (!row) {
|
|||
|
|
await connection.commit();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
const now = nowMs();
|
|||
|
|
await connection.query(
|
|||
|
|
`UPDATE h5_help_escalations
|
|||
|
|
SET status = ?, started_at = ?, updated_at = ?, error_message = NULL
|
|||
|
|
WHERE id = ?`,
|
|||
|
|
[HELP_ESCALATION_STATUS.RUNNING, now, now, row.id],
|
|||
|
|
);
|
|||
|
|
await connection.commit();
|
|||
|
|
logger.info?.('[cursor-help] claimed escalation', { id: row.id, workerId });
|
|||
|
|
return rowToEscalation({ ...row, status: HELP_ESCALATION_STATUS.RUNNING, started_at: now, updated_at: now });
|
|||
|
|
} catch (error) {
|
|||
|
|
await connection.rollback();
|
|||
|
|
throw error;
|
|||
|
|
} finally {
|
|||
|
|
connection.release();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function complete(id, {
|
|||
|
|
status,
|
|||
|
|
resultText = null,
|
|||
|
|
agentOutput = null,
|
|||
|
|
errorMessage = null,
|
|||
|
|
}) {
|
|||
|
|
const now = nowMs();
|
|||
|
|
await pool.query(
|
|||
|
|
`UPDATE h5_help_escalations
|
|||
|
|
SET status = ?, result_text = ?, agent_output = ?, error_message = ?,
|
|||
|
|
completed_at = ?, updated_at = ?
|
|||
|
|
WHERE id = ?`,
|
|||
|
|
[status, resultText, agentOutput, errorMessage, now, now, id],
|
|||
|
|
);
|
|||
|
|
return getById(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function getQueueStats() {
|
|||
|
|
const [rows] = await pool.query(
|
|||
|
|
`SELECT status, COUNT(*) AS count
|
|||
|
|
FROM h5_help_escalations
|
|||
|
|
GROUP BY status`,
|
|||
|
|
);
|
|||
|
|
const stats = {
|
|||
|
|
queued: 0,
|
|||
|
|
running: 0,
|
|||
|
|
succeeded: 0,
|
|||
|
|
failed: 0,
|
|||
|
|
};
|
|||
|
|
for (const row of rows) {
|
|||
|
|
stats[row.status] = Number(row.count);
|
|||
|
|
}
|
|||
|
|
return stats;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
enabled,
|
|||
|
|
memindRoot,
|
|||
|
|
enqueue,
|
|||
|
|
getById,
|
|||
|
|
claimNext,
|
|||
|
|
complete,
|
|||
|
|
getQueueStats,
|
|||
|
|
buildPrompt: (escalation) => buildHelpEscalationPrompt(escalation, { memindRoot }),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function tryHandleHelpEscalation({
|
|||
|
|
helpEscalationService,
|
|||
|
|
userId,
|
|||
|
|
channel,
|
|||
|
|
userText,
|
|||
|
|
context = {},
|
|||
|
|
agentSessionId = null,
|
|||
|
|
onAck = null,
|
|||
|
|
}) {
|
|||
|
|
if (!helpEscalationService?.enabled) return null;
|
|||
|
|
const rawText = String(userText ?? '').trim();
|
|||
|
|
const commandText = normalizeHelpEscalationCommandText(rawText);
|
|||
|
|
if (!isHelpEscalationIntent(commandText || rawText)) return null;
|
|||
|
|
|
|||
|
|
if (!hasHelpEscalationProblemDescription(commandText || rawText)) {
|
|||
|
|
const message = HELP_ESCALATION_DETAILS_PROMPT;
|
|||
|
|
if (typeof onAck === 'function') {
|
|||
|
|
await onAck(message, null);
|
|||
|
|
}
|
|||
|
|
return { needsDetails: true, message };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const escalation = await helpEscalationService.enqueue({
|
|||
|
|
userId,
|
|||
|
|
channel,
|
|||
|
|
userText: commandText || rawText,
|
|||
|
|
context,
|
|||
|
|
agentSessionId,
|
|||
|
|
});
|
|||
|
|
if (typeof onAck === 'function') {
|
|||
|
|
await onAck(HELP_ESCALATION_ACK_TEXT, escalation);
|
|||
|
|
}
|
|||
|
|
return escalation;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function deliverHelpEscalationResult({
|
|||
|
|
escalation,
|
|||
|
|
scheduleService = null,
|
|||
|
|
sendWechatTextToUser = null,
|
|||
|
|
logger = console,
|
|||
|
|
}) {
|
|||
|
|
const userMessage = String(
|
|||
|
|
escalation?.resultText
|
|||
|
|
?? escalation?.errorMessage
|
|||
|
|
?? '工程师已处理完成,但未返回详细说明。',
|
|||
|
|
).trim();
|
|||
|
|
const title = escalation?.status === HELP_ESCALATION_STATUS.SUCCEEDED
|
|||
|
|
? '工程师已处理完成'
|
|||
|
|
: '工程师处理失败';
|
|||
|
|
|
|||
|
|
if (scheduleService?.createUserNotification) {
|
|||
|
|
await scheduleService.createUserNotification({
|
|||
|
|
userId: escalation.userId,
|
|||
|
|
channel: 'web',
|
|||
|
|
notificationType: 'help_escalation_result',
|
|||
|
|
title,
|
|||
|
|
body: userMessage,
|
|||
|
|
data: {
|
|||
|
|
escalationId: escalation.id,
|
|||
|
|
status: escalation.status,
|
|||
|
|
channel: escalation.channel,
|
|||
|
|
},
|
|||
|
|
}).catch((error) => {
|
|||
|
|
logger.warn?.('[cursor-help] web notification failed:', error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let wechatSent = false;
|
|||
|
|
if (typeof sendWechatTextToUser === 'function') {
|
|||
|
|
wechatSent = await sendWechatTextToUser(
|
|||
|
|
escalation.userId,
|
|||
|
|
`${title}\n${userMessage}`.trim(),
|
|||
|
|
).catch((error) => {
|
|||
|
|
logger.warn?.('[cursor-help] wechat notification failed:', error);
|
|||
|
|
return false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { userMessage, wechatSent };
|
|||
|
|
}
|