fix(wechat): deliver failure notices and hot-swappable wechat-mp bundle
Notify users when HTML publish guards reject stub pages instead of silently failing, send real page links when non-stub files exist, and split wechat-mp into wechat-mp.bundle.mjs for fast production updates without full portal rebuilds. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -369,6 +369,7 @@ async function writeMetadata() {
|
||||
'',
|
||||
'Bundled alongside server.mjs (required for sandbox-fs MCP):',
|
||||
' mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)',
|
||||
' wechat-mp.bundle.mjs (esbuild bundle; hot-swappable WeChat MP module)',
|
||||
'',
|
||||
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
|
||||
' Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.',
|
||||
@@ -423,10 +424,16 @@ async function writeMetadata() {
|
||||
);
|
||||
}
|
||||
|
||||
async function bundleWechatMp() {
|
||||
console.log('==> 打包 WeChat MP runtime bundle');
|
||||
await run('node', ['scripts/build-wechat-mp-runtime.mjs']);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await buildFrontend();
|
||||
await copyRuntimeAssets();
|
||||
await bundleServer();
|
||||
await bundleWechatMp();
|
||||
await bundleSandboxMcp();
|
||||
await bundleAgentRunWorker();
|
||||
if (runtimeInstallMode === 'bundle-node-modules' && !skipNodeModules) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.join(__dirname, '..');
|
||||
const runtimeRoot = path.join(root, '.runtime', 'portal');
|
||||
const esbuildBin = path.join(root, 'node_modules', '.pnpm', 'node_modules', '.bin', 'esbuild');
|
||||
const runtimeNodeTarget = process.env.PORTAL_RUNTIME_NODE_TARGET || 'node24';
|
||||
const outputArg = process.argv.find((arg) => arg.startsWith('--outfile='));
|
||||
const outfile = outputArg
|
||||
? outputArg.slice('--outfile='.length)
|
||||
: path.join(runtimeRoot, 'wechat-mp.bundle.mjs');
|
||||
|
||||
const externalPackages = [
|
||||
'undici',
|
||||
'mysql2',
|
||||
'mysql2/promise',
|
||||
'sharp',
|
||||
'fsevents',
|
||||
];
|
||||
|
||||
function run(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { cwd: root, stdio: 'inherit', env: process.env });
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 1}`));
|
||||
});
|
||||
child.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(path.dirname(outfile), { recursive: true });
|
||||
const args = [
|
||||
'wechat-mp.mjs',
|
||||
'--bundle',
|
||||
'--platform=node',
|
||||
'--format=esm',
|
||||
`--target=${runtimeNodeTarget}`,
|
||||
`--outfile=${outfile}`,
|
||||
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
|
||||
];
|
||||
for (const pkg of externalPackages) {
|
||||
args.push(`--external:${pkg}`);
|
||||
}
|
||||
await run(esbuildBin, args);
|
||||
console.log(`WeChat MP runtime bundle written: ${outfile}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err instanceof Error ? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
HOST="${MEMIND_PROD_HOST:-john@58.38.22.103}"
|
||||
REMOTE_ROOT="${MEMIND_PROD_ROOT:-/Users/john/Project/Memind}"
|
||||
BUNDLE=".runtime/portal/wechat-mp.bundle.mjs"
|
||||
|
||||
echo "[deploy-wechat-mp] build bundle"
|
||||
node scripts/build-wechat-mp-runtime.mjs
|
||||
|
||||
if [[ ! -f "${BUNDLE}" ]]; then
|
||||
echo "missing bundle: ${BUNDLE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[deploy-wechat-mp] upload ${BUNDLE} -> ${HOST}:${REMOTE_ROOT}/wechat-mp.bundle.mjs"
|
||||
scp "${BUNDLE}" "${HOST}:${REMOTE_ROOT}/wechat-mp.bundle.mjs"
|
||||
|
||||
echo "[deploy-wechat-mp] restart portal"
|
||||
ssh "${HOST}" 'launchctl kickstart -k "gui/$(id -u)/cn.tkmind.memind-portal"'
|
||||
|
||||
echo "[deploy-wechat-mp] health check"
|
||||
ssh "${HOST}" 'curl -s -o /dev/null -w "portal=%{http_code}\n" http://127.0.0.1:8081/api/status'
|
||||
|
||||
echo "[deploy-wechat-mp] done"
|
||||
@@ -151,7 +151,7 @@ fi
|
||||
|
||||
verify_runtime_artifact() {
|
||||
local missing=0
|
||||
for required in server.mjs mindspace-sandbox-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
|
||||
for required in server.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
|
||||
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
|
||||
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
|
||||
missing=1
|
||||
|
||||
+4
-2
@@ -157,7 +157,8 @@ import {
|
||||
WECHAT_NOTIFY_SUCCESS_V2,
|
||||
} from './wechat-pay.mjs';
|
||||
import { createWechatOAuthService, isWechatUserAgent, loadWechatOAuthConfig } from './wechat-oauth.mjs';
|
||||
import { createWechatMpService, loadWechatMpConfig } from './wechat-mp.mjs';
|
||||
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
||||
import { loadWechatMpModule } from './wechat-mp-loader.mjs';
|
||||
import { validateWechatShareSignatureUrl } from './wechat-share.mjs';
|
||||
import { createScheduleService } from './schedule-service.mjs';
|
||||
import { createFeedbackService } from './user-feedback.mjs';
|
||||
@@ -559,7 +560,8 @@ async function bootstrapUserAuth() {
|
||||
maxConcurrentRuns: Number(process.env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY ?? 1),
|
||||
runTimeoutMs: Number(process.env.MEMIND_AGENT_RUN_TIMEOUT_MS ?? 15 * 60 * 1000),
|
||||
});
|
||||
wechatMpService = createWechatMpService({
|
||||
const wechatMp = await loadWechatMpModule(__dirname);
|
||||
wechatMpService = wechatMp.createWechatMpService({
|
||||
config: WECHAT_MP_CONFIG,
|
||||
userAuth,
|
||||
apiFetch: tkmindProxy.apiFetch,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
const DEFAULT_ACK_TEXT = '已收到,正在处理,完成后发到这里。';
|
||||
const DEFAULT_PROGRESS_TEXT = '还在处理,请稍等片刻。';
|
||||
const DEFAULT_STATUS_TEXT =
|
||||
'我在这边。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。';
|
||||
const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接发文字给我。';
|
||||
const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:';
|
||||
const DEFAULT_PROGRESS_DELAY_MS = 8000;
|
||||
const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000;
|
||||
const DEFAULT_SESSION_MESSAGE_ROTATE_COUNT = 200;
|
||||
const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
|
||||
const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
|
||||
'https://api.weixin.qq.com/cgi-bin/message/custom/send';
|
||||
const DEFAULT_WECHAT_JSAPI_TICKET_URL = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
|
||||
|
||||
function deriveWechatEndpointFromUrl(baseUrl, suffix) {
|
||||
const normalized = String(baseUrl ?? '').trim();
|
||||
if (!normalized) return '';
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
return `${url.origin}${suffix}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function loadWechatMpConfig(env = process.env) {
|
||||
const appId = env.H5_WECHAT_MP_APP_ID?.trim() ?? env.H5_WECHAT_APP_ID?.trim() ?? '';
|
||||
const appSecret =
|
||||
env.H5_WECHAT_MP_APP_SECRET?.trim() ?? env.H5_WECHAT_APP_SECRET?.trim() ?? '';
|
||||
const token = env.H5_WECHAT_MP_TOKEN?.trim() ?? '';
|
||||
const publicBaseUrl = env.H5_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') ?? '';
|
||||
const enabledFlag = env.H5_WECHAT_MP_ENABLED === '1';
|
||||
const bindPath = env.H5_WECHAT_MP_BIND_PATH?.trim() || '/auth/wechat/authorize?intent=login';
|
||||
return {
|
||||
enabled: enabledFlag && Boolean(appId && appSecret && token && publicBaseUrl),
|
||||
appId,
|
||||
appSecret,
|
||||
token,
|
||||
publicBaseUrl,
|
||||
bindPath,
|
||||
ackText: env.H5_WECHAT_MP_ACK_TEXT?.trim() || DEFAULT_ACK_TEXT,
|
||||
ackRandomEnabled: env.H5_WECHAT_MP_ACK_RANDOM !== '0',
|
||||
ackNicknameEnabled: env.H5_WECHAT_MP_ACK_NICKNAME !== '0',
|
||||
progressText: env.H5_WECHAT_MP_PROGRESS_TEXT?.trim() || DEFAULT_PROGRESS_TEXT,
|
||||
progressDelayMs: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_PROGRESS_DELAY_MS ?? DEFAULT_PROGRESS_DELAY_MS),
|
||||
),
|
||||
sessionIdleRotateMs: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
||||
),
|
||||
sessionMessageRotateCount: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_SESSION_MESSAGE_ROTATE_COUNT ?? DEFAULT_SESSION_MESSAGE_ROTATE_COUNT),
|
||||
),
|
||||
statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT,
|
||||
unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT,
|
||||
unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT,
|
||||
tokenUrl: env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_WECHAT_TOKEN_URL,
|
||||
customerServiceUrl:
|
||||
env.H5_WECHAT_MP_CUSTOMER_SERVICE_URL?.trim() || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL,
|
||||
jsapiTicketUrl:
|
||||
env.H5_WECHAT_MP_JSAPI_TICKET_URL?.trim() ||
|
||||
deriveWechatEndpointFromUrl(env.H5_WECHAT_MP_TOKEN_URL?.trim(), '/cgi-bin/ticket/getticket') ||
|
||||
DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
||||
mediaPublicBaseUrl:
|
||||
env.H5_WECHAT_MP_MEDIA_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') || publicBaseUrl,
|
||||
maxImageBytes: Math.max(1, Number(env.H5_WECHAT_MP_MAX_IMAGE_BYTES ?? 10 * 1024 * 1024)),
|
||||
acceptVoice: env.H5_WECHAT_MP_ACCEPT_VOICE !== '0',
|
||||
acceptImage: env.H5_WECHAT_MP_ACCEPT_IMAGE !== '0',
|
||||
acceptLocation: env.H5_WECHAT_MP_ACCEPT_LOCATION !== '0',
|
||||
acceptLink: env.H5_WECHAT_MP_ACCEPT_LINK !== '0',
|
||||
encodingAesKey: env.H5_WECHAT_MP_ENCODING_AES_KEY?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
||||
|
||||
export { loadWechatMpConfig };
|
||||
|
||||
let cachedModule = null;
|
||||
let cachedTarget = '';
|
||||
let cachedMtime = 0;
|
||||
|
||||
function resolveWechatMpEntry(root = process.cwd()) {
|
||||
const bundlePath = path.join(root, 'wechat-mp.bundle.mjs');
|
||||
if (fs.existsSync(bundlePath)) return bundlePath;
|
||||
return path.join(root, 'wechat-mp.mjs');
|
||||
}
|
||||
|
||||
export async function loadWechatMpModule(root = process.cwd()) {
|
||||
const target = resolveWechatMpEntry(root);
|
||||
const mtime = fs.statSync(target).mtimeMs;
|
||||
if (cachedModule && cachedTarget === target && cachedMtime === mtime) {
|
||||
return cachedModule;
|
||||
}
|
||||
cachedModule = await import(`${pathToFileURL(target).href}?v=${mtime}`);
|
||||
cachedTarget = target;
|
||||
cachedMtime = mtime;
|
||||
return cachedModule;
|
||||
}
|
||||
+122
-109
@@ -6,7 +6,8 @@ import { developerToolsFromPolicy } from './capabilities.mjs';
|
||||
import { mergeMessageContent } from './message-stream.mjs';
|
||||
import { reconcileAgentSession } from './session-reconcile.mjs';
|
||||
import { isScheduleIntent, parseScheduleIntent, shouldUseScheduleAssistant } from './schedule-intent.mjs';
|
||||
import { materializeMissingPublicHtmlWrites } from './mindspace-public-finish-sync.mjs';
|
||||
import { isStubPublicHtmlContent, materializeMissingPublicHtmlWrites } from './mindspace-public-finish-sync.mjs';
|
||||
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
||||
import { buildCurrentTimeAgentPrefix } from './user-memory-profile.mjs';
|
||||
import { buildPublicUrl, PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||
import { buildAutoChatSkillPrefix } from './chat-skills.mjs';
|
||||
@@ -18,15 +19,7 @@ const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
|
||||
'https://api.weixin.qq.com/cgi-bin/message/custom/send';
|
||||
const DEFAULT_WECHAT_JSAPI_TICKET_URL = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
|
||||
const DEFAULT_ASR_TARGET = process.env.H5_ASR_TARGET ?? 'https://asr.tkmind.cn';
|
||||
const DEFAULT_ACK_TEXT = '已收到,正在处理,完成后发到这里。';
|
||||
const DEFAULT_PROGRESS_TEXT = '还在处理,请稍等片刻。';
|
||||
const DEFAULT_STATUS_TEXT =
|
||||
'我在这边。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。';
|
||||
const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接发文字给我。';
|
||||
const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:';
|
||||
const DEFAULT_PROGRESS_DELAY_MS = 8000;
|
||||
const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000;
|
||||
const DEFAULT_SESSION_MESSAGE_ROTATE_COUNT = 200;
|
||||
export { loadWechatMpConfig };
|
||||
const PUBLIC_HTML_LINK_PATTERN =
|
||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
||||
const SESSION_WORKSPACE_TOOL_NAMES = new Set([
|
||||
@@ -806,13 +799,22 @@ function downgradePrematurePublishClaims(text) {
|
||||
.replace(/已发布成功/g, '生成未完成');
|
||||
}
|
||||
|
||||
function buildHtmlPublishFailureText() {
|
||||
export function buildHtmlPublishFailureText() {
|
||||
return [
|
||||
'这次页面没有按 H5 里的页面技能真正生成成功,所以我先不发不一致的简版页。',
|
||||
'请直接重发一次你的页面需求,我会按和 H5 相同的 `static-page-publish` 技能链路重做。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function isHtmlPublishFailureMessage(message) {
|
||||
const normalized = String(message ?? '').trim();
|
||||
return (
|
||||
normalized.includes('页面技能') ||
|
||||
normalized.includes('static-page-publish') ||
|
||||
normalized.includes('简版页')
|
||||
);
|
||||
}
|
||||
|
||||
export { maybeAttachPublishedHtmlLink };
|
||||
|
||||
function artifactFileExists(artifact) {
|
||||
@@ -903,6 +905,26 @@ function resolveHtmlPublishArtifacts({
|
||||
};
|
||||
}
|
||||
|
||||
function selectHtmlPublishArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
|
||||
const candidates = verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts;
|
||||
const sendable = nonStubHtmlArtifacts(candidates);
|
||||
return sendable.length > 0 ? sendable : candidates;
|
||||
}
|
||||
|
||||
function isStubPublicHtmlArtifact(artifact) {
|
||||
const localPath = String(artifact?.localPath ?? '').trim();
|
||||
if (!localPath) return false;
|
||||
try {
|
||||
return isStubPublicHtmlContent(fs.readFileSync(localPath, 'utf8'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function nonStubHtmlArtifacts(artifacts = []) {
|
||||
return artifacts.filter((artifact) => artifactFileExists(artifact) && !isStubPublicHtmlArtifact(artifact));
|
||||
}
|
||||
|
||||
export function shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
@@ -911,6 +933,8 @@ export function shouldRetryHtmlGenerationReply({
|
||||
}) {
|
||||
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
|
||||
|
||||
if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false;
|
||||
|
||||
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text);
|
||||
if (replyHasPublicLinks) {
|
||||
if (!hasValidLinkInReply) return true;
|
||||
@@ -1015,12 +1039,22 @@ export function sanitizeWechatAgentOutboundText(text) {
|
||||
|
||||
function formatWechatAgentFailureMessage(err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (isHtmlPublishFailureMessage(message)) return buildHtmlPublishFailureText();
|
||||
if (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message)) {
|
||||
return '刚才专属会话状态异常,我已切换到新会话。请再发一次你的需求。';
|
||||
}
|
||||
return `这次转发到专属 Agent 失败了:${message.slice(0, 200)}`;
|
||||
}
|
||||
|
||||
function markWechatUserNotified(err) {
|
||||
if (err && typeof err === 'object') err.wechatUserNotified = true;
|
||||
return err;
|
||||
}
|
||||
|
||||
function wasWechatUserNotified(err) {
|
||||
return Boolean(err && typeof err === 'object' && err.wechatUserNotified);
|
||||
}
|
||||
|
||||
function normalizeNumber(value) {
|
||||
if (value === '' || value === null || value === undefined) return null;
|
||||
const num = Number(value);
|
||||
@@ -1303,58 +1337,6 @@ function successResponse(task = null) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadWechatMpConfig(env = process.env) {
|
||||
const appId = env.H5_WECHAT_MP_APP_ID?.trim() ?? env.H5_WECHAT_APP_ID?.trim() ?? '';
|
||||
const appSecret =
|
||||
env.H5_WECHAT_MP_APP_SECRET?.trim() ?? env.H5_WECHAT_APP_SECRET?.trim() ?? '';
|
||||
const token = env.H5_WECHAT_MP_TOKEN?.trim() ?? '';
|
||||
const publicBaseUrl = env.H5_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') ?? '';
|
||||
const enabledFlag = env.H5_WECHAT_MP_ENABLED === '1';
|
||||
const bindPath = env.H5_WECHAT_MP_BIND_PATH?.trim() || '/auth/wechat/authorize?intent=login';
|
||||
return {
|
||||
enabled: enabledFlag && Boolean(appId && appSecret && token && publicBaseUrl),
|
||||
appId,
|
||||
appSecret,
|
||||
token,
|
||||
publicBaseUrl,
|
||||
bindPath,
|
||||
ackText: env.H5_WECHAT_MP_ACK_TEXT?.trim() || DEFAULT_ACK_TEXT,
|
||||
ackRandomEnabled: env.H5_WECHAT_MP_ACK_RANDOM !== '0',
|
||||
ackNicknameEnabled: env.H5_WECHAT_MP_ACK_NICKNAME !== '0',
|
||||
progressText: env.H5_WECHAT_MP_PROGRESS_TEXT?.trim() || DEFAULT_PROGRESS_TEXT,
|
||||
progressDelayMs: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_PROGRESS_DELAY_MS ?? DEFAULT_PROGRESS_DELAY_MS),
|
||||
),
|
||||
sessionIdleRotateMs: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
||||
),
|
||||
sessionMessageRotateCount: Math.max(
|
||||
0,
|
||||
Number(env.H5_WECHAT_MP_SESSION_MESSAGE_ROTATE_COUNT ?? DEFAULT_SESSION_MESSAGE_ROTATE_COUNT),
|
||||
),
|
||||
statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT,
|
||||
unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT,
|
||||
unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT,
|
||||
tokenUrl: env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_WECHAT_TOKEN_URL,
|
||||
customerServiceUrl:
|
||||
env.H5_WECHAT_MP_CUSTOMER_SERVICE_URL?.trim() || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL,
|
||||
jsapiTicketUrl:
|
||||
env.H5_WECHAT_MP_JSAPI_TICKET_URL?.trim() ||
|
||||
deriveWechatEndpointFromUrl(env.H5_WECHAT_MP_TOKEN_URL?.trim(), '/cgi-bin/ticket/getticket') ||
|
||||
DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
||||
mediaPublicBaseUrl:
|
||||
env.H5_WECHAT_MP_MEDIA_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') || publicBaseUrl,
|
||||
maxImageBytes: Math.max(1, Number(env.H5_WECHAT_MP_MAX_IMAGE_BYTES ?? 10 * 1024 * 1024)),
|
||||
acceptVoice: env.H5_WECHAT_MP_ACCEPT_VOICE !== '0',
|
||||
acceptImage: env.H5_WECHAT_MP_ACCEPT_IMAGE !== '0',
|
||||
acceptLocation: env.H5_WECHAT_MP_ACCEPT_LOCATION !== '0',
|
||||
acceptLink: env.H5_WECHAT_MP_ACCEPT_LINK !== '0',
|
||||
encodingAesKey: env.H5_WECHAT_MP_ENCODING_AES_KEY?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function sha1Hex(parts) {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
@@ -1483,21 +1465,11 @@ export function createWechatMpService({
|
||||
};
|
||||
}
|
||||
config = {
|
||||
...loadWechatMpConfig({}),
|
||||
...config,
|
||||
tokenUrl: config.tokenUrl || DEFAULT_WECHAT_TOKEN_URL,
|
||||
customerServiceUrl: config.customerServiceUrl || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL,
|
||||
jsapiTicketUrl: config.jsapiTicketUrl || DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
||||
progressText: config.progressText ?? DEFAULT_PROGRESS_TEXT,
|
||||
progressDelayMs: Math.max(0, Number(config.progressDelayMs ?? DEFAULT_PROGRESS_DELAY_MS)),
|
||||
sessionIdleRotateMs: Math.max(
|
||||
0,
|
||||
Number(config.sessionIdleRotateMs ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
||||
),
|
||||
sessionMessageRotateCount: Math.max(
|
||||
0,
|
||||
Number(config.sessionMessageRotateCount ?? DEFAULT_SESSION_MESSAGE_ROTATE_COUNT),
|
||||
),
|
||||
statusText: config.statusText || DEFAULT_STATUS_TEXT,
|
||||
mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl,
|
||||
maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)),
|
||||
acceptVoice: config.acceptVoice !== false,
|
||||
@@ -2046,32 +2018,50 @@ export function createWechatMpService({
|
||||
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
||||
confirmedArtifacts,
|
||||
});
|
||||
if (
|
||||
shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts,
|
||||
hasValidLinkInReply,
|
||||
}) ||
|
||||
(expectedArtifacts.length === 0 &&
|
||||
recentArtifacts.length === 0 &&
|
||||
(await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })))
|
||||
) {
|
||||
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text);
|
||||
const suspiciousPublishClaim =
|
||||
expectedArtifacts.length === 0 &&
|
||||
recentArtifacts.length === 0 &&
|
||||
(await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest }));
|
||||
const htmlGenerationNeedsRetry = shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts,
|
||||
hasValidLinkInReply,
|
||||
});
|
||||
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
|
||||
if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
if (
|
||||
suspiciousPublishClaim ||
|
||||
bareCompletionReply ||
|
||||
(htmlGenerationNeedsRetry && replyHasPublicLinks && confirmedArtifacts.length === 0)
|
||||
) {
|
||||
throw new Error('stale_session_poisoned_completion');
|
||||
}
|
||||
if (htmlGenerationNeedsRetry) {
|
||||
const text = buildHtmlPublishFailureText();
|
||||
try {
|
||||
await sendCustomerServiceText(inbound.fromUserName, text, user);
|
||||
} catch (sendErr) {
|
||||
logger.error?.('WeChat MP html publish failure notice failed:', sendErr);
|
||||
}
|
||||
throw markWechatUserNotified(new Error(text));
|
||||
}
|
||||
} else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) {
|
||||
throw new Error('stale_session_poisoned_completion');
|
||||
}
|
||||
if (reply.tokenState) {
|
||||
await userAuth.billSessionUsage(user.userId, sessionId, reply.tokenState, requestId);
|
||||
}
|
||||
const publishArtifacts = selectHtmlPublishArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
const finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
|
||||
workingDir,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
||||
artifacts: publishArtifacts,
|
||||
});
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
||||
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
||||
(artifact) => artifact.url,
|
||||
),
|
||||
verifiedHtmlUrls: publishArtifacts.map((artifact) => artifact.url),
|
||||
linkExistsForRequest,
|
||||
});
|
||||
return { sessionId };
|
||||
@@ -2115,32 +2105,50 @@ export function createWechatMpService({
|
||||
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
||||
confirmedArtifacts,
|
||||
});
|
||||
if (
|
||||
shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts,
|
||||
hasValidLinkInReply,
|
||||
}) ||
|
||||
(expectedArtifacts.length === 0 &&
|
||||
recentArtifacts.length === 0 &&
|
||||
(await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })))
|
||||
) {
|
||||
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text);
|
||||
const suspiciousPublishClaim =
|
||||
expectedArtifacts.length === 0 &&
|
||||
recentArtifacts.length === 0 &&
|
||||
(await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest }));
|
||||
const htmlGenerationNeedsRetry = shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts,
|
||||
hasValidLinkInReply,
|
||||
});
|
||||
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
|
||||
if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
if (
|
||||
suspiciousPublishClaim ||
|
||||
bareCompletionReply ||
|
||||
(htmlGenerationNeedsRetry && replyHasPublicLinks && confirmedArtifacts.length === 0)
|
||||
) {
|
||||
throw new Error(buildHtmlPublishFailureText());
|
||||
}
|
||||
if (htmlGenerationNeedsRetry) {
|
||||
const text = buildHtmlPublishFailureText();
|
||||
try {
|
||||
await sendCustomerServiceText(inbound.fromUserName, text, user);
|
||||
} catch (sendErr) {
|
||||
logger.error?.('WeChat MP html publish failure notice failed:', sendErr);
|
||||
}
|
||||
throw markWechatUserNotified(new Error(text));
|
||||
}
|
||||
} else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) {
|
||||
throw new Error(buildHtmlPublishFailureText());
|
||||
}
|
||||
if (reply.tokenState) {
|
||||
await userAuth.billSessionUsage(user.userId, sessionId, reply.tokenState, retryId);
|
||||
}
|
||||
const publishArtifacts = selectHtmlPublishArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
const finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
|
||||
workingDir,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
||||
artifacts: publishArtifacts,
|
||||
});
|
||||
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
||||
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
||||
(artifact) => artifact.url,
|
||||
),
|
||||
verifiedHtmlUrls: publishArtifacts.map((artifact) => artifact.url),
|
||||
linkExistsForRequest,
|
||||
});
|
||||
return { sessionId };
|
||||
@@ -2563,11 +2571,16 @@ export function createWechatMpService({
|
||||
}).catch(() => {});
|
||||
}
|
||||
logger.error?.('WeChat MP background reply failed:', err);
|
||||
return sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
formatWechatAgentFailureMessage(err),
|
||||
boundUser,
|
||||
).catch(() => {});
|
||||
if (wasWechatUserNotified(err)) return;
|
||||
try {
|
||||
await sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
formatWechatAgentFailureMessage(err),
|
||||
boundUser,
|
||||
);
|
||||
} catch (sendErr) {
|
||||
logger.error?.('WeChat MP failure notice failed:', sendErr);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+4
-4
@@ -2084,7 +2084,7 @@ test('wechat mp service recreates poisoned dedicated session after bare completi
|
||||
assert.match(payload.text.content, /页面生成未完成|hello\.html/);
|
||||
});
|
||||
|
||||
test('wechat mp service recreates dedicated session when html was written before loading static-page-publish skill', async () => {
|
||||
test('wechat mp service forwards html link when write_file created a real page without static-page-publish skill', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
@@ -2249,11 +2249,11 @@ test('wechat mp service recreates dedicated session when html was written before
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
|
||||
assert.equal(routeCleared, true);
|
||||
assert.equal(started, true);
|
||||
assert.equal(routeCleared, false);
|
||||
assert.equal(started, false);
|
||||
const sendCall = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
const payload = JSON.parse(sendCall[2]);
|
||||
assert.match(payload.text.content, /Hello World/);
|
||||
assert.match(payload.text.content, /hello-world\.html/);
|
||||
assert.match(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/hello-world\.html/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user