fix: isolate WeChat runtime delivery path
This commit is contained in:
@@ -268,7 +268,13 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
const order = [];
|
||||
const reviewCalls = [];
|
||||
const wechatCalls = [];
|
||||
const submittedMessages = [];
|
||||
const finishedMessages = [];
|
||||
let failReview = false;
|
||||
const userAuth = createWechatUserAuth(workspaceRoot);
|
||||
userAuth.finishWechatMpMessage = async (input) => {
|
||||
finishedMessages.push(input);
|
||||
};
|
||||
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
@@ -286,7 +292,7 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
pageDataAiderReviewEnabled: true,
|
||||
pageDataAiderReviewUsers: ['唐'],
|
||||
},
|
||||
userAuth: createWechatUserAuth(workspaceRoot),
|
||||
userAuth,
|
||||
htmlDeliveryAuthority: {
|
||||
async prepareWechatHtmlDelivery() {
|
||||
const artifact = {
|
||||
@@ -339,7 +345,7 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
};
|
||||
},
|
||||
},
|
||||
sessionApiFetch: async (sessionId, pathname) => {
|
||||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||||
if (pathname === `/sessions/${sessionId}/events`) {
|
||||
return new Response(
|
||||
[
|
||||
@@ -351,6 +357,7 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
);
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||||
submittedMessages.push(JSON.parse(init.body ?? '{}').user_message);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
@@ -377,6 +384,10 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
assert.equal(reviewCalls[0].sourceChannel, 'wechat_mp');
|
||||
assert.equal(reviewCalls[0].sourceMessageId, '10001');
|
||||
assert.equal(reviewCalls[0].forcePageData, true);
|
||||
assert.equal(
|
||||
submittedMessages[0]?.metadata?.memindRun?.pgRequired,
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(
|
||||
reviewCalls[0].relativePaths,
|
||||
['public/children-diet-survey.html'],
|
||||
@@ -398,6 +409,11 @@ test('integration: WeChat Page Data runs Aider review before the delivery contra
|
||||
assert.ok(failedSend);
|
||||
const failedPayload = JSON.parse(failedSend[2]);
|
||||
assert.match(failedPayload.text.content, /Page Data|数据收集|绑定/);
|
||||
assert.equal(finishedMessages.at(-1)?.status, 'failed');
|
||||
assert.equal(
|
||||
finishedMessages.at(-1)?.agentSessionId,
|
||||
'session-page-data-1',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
failedPayload.text.content,
|
||||
/https:\/\/m\.tkmind\.cn\/MindSpace\/user-page-data\/public\/children-diet-survey\.html/,
|
||||
|
||||
@@ -512,6 +512,7 @@ async function bootstrapUserAuth() {
|
||||
await bootstrapPortalIntegrationServices({
|
||||
pool,
|
||||
h5Root: H5_ROOT,
|
||||
codeRoot: __dirname,
|
||||
env: process.env,
|
||||
usersRoot: USERS_ROOT,
|
||||
wechatMpConfig: WECHAT_MP_CONFIG,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
export async function bootstrapPortalIntegrationServices({
|
||||
pool,
|
||||
h5Root,
|
||||
codeRoot = h5Root,
|
||||
env = process.env,
|
||||
usersRoot,
|
||||
wechatMpConfig,
|
||||
@@ -76,7 +77,9 @@ export async function bootstrapPortalIntegrationServices({
|
||||
);
|
||||
}
|
||||
|
||||
const wechatMp = await loadWechatMpModuleFn(h5Root);
|
||||
const wechatMpRoot = codeRoot || h5Root;
|
||||
logger.log?.(`[WeChat MP] Loading runtime module from code root: ${wechatMpRoot}`);
|
||||
const wechatMp = await loadWechatMpModuleFn(wechatMpRoot);
|
||||
const pageDataDeliveryReviewer =
|
||||
wechatMpConfig?.pageDataAiderReviewEnabled === true
|
||||
? createPageDataDeliveryCodeReviewServiceFn({
|
||||
@@ -122,6 +125,7 @@ export async function bootstrapPortalIntegrationServices({
|
||||
tkmindProxy.startSessionForUser(userId, {
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
origin: 'wechat',
|
||||
}),
|
||||
sessionApiFetch: async (
|
||||
sessionId,
|
||||
|
||||
@@ -97,6 +97,7 @@ function createSetup(overrides = {}) {
|
||||
const options = {
|
||||
pool,
|
||||
h5Root: '/app',
|
||||
codeRoot: '/runtime',
|
||||
env: {
|
||||
H5_SCHEDULE_ENABLED: '1',
|
||||
H5_REMINDER_WORKER_ENABLED: '1',
|
||||
@@ -236,6 +237,10 @@ test('preserves WeChat service configuration and proxy callbacks', async () => {
|
||||
);
|
||||
const { wechatOptions } = setup.getCaptured();
|
||||
|
||||
assert.deepEqual(
|
||||
setup.calls.find(([kind]) => kind === 'load-wechat'),
|
||||
['load-wechat', '/runtime'],
|
||||
);
|
||||
assert.equal(
|
||||
wechatOptions.pageDataFinishGuard,
|
||||
setup.options.mindSpacePublicFinish,
|
||||
@@ -268,6 +273,18 @@ test('preserves WeChat service configuration and proxy callbacks', async () => {
|
||||
}),
|
||||
{ sessionId: 'session-1' },
|
||||
);
|
||||
assert.deepEqual(
|
||||
setup.calls.find(([kind]) => kind === 'start-session'),
|
||||
[
|
||||
'start-session',
|
||||
'user-1',
|
||||
{
|
||||
workingDir: '/workspace',
|
||||
sessionPolicy: { sandboxed: true },
|
||||
origin: 'wechat',
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
await wechatOptions.sessionApiFetch(
|
||||
'session-1',
|
||||
|
||||
+7
-1
@@ -1430,6 +1430,7 @@ export function createTkmindProxy({
|
||||
sessionPolicy = null,
|
||||
recipe = null,
|
||||
disableImageReading = false,
|
||||
origin = 'h5',
|
||||
} = {},
|
||||
) {
|
||||
const resolvedWorkingDir = workingDir ?? await userAuth.resolveWorkingDir(userId);
|
||||
@@ -1458,7 +1459,12 @@ export function createTkmindProxy({
|
||||
throw new Error('创建会话失败:缺少 session id');
|
||||
}
|
||||
await rememberSessionTarget(session.id, startTarget);
|
||||
await sessionStore.registerAgentSession(userId, session.id, startTarget);
|
||||
await sessionStore.registerSession({
|
||||
userId,
|
||||
sessionId: session.id,
|
||||
target: startTarget,
|
||||
origin: origin === 'wechat' ? 'wechat' : 'h5',
|
||||
});
|
||||
const baseSessionScopedPolicy =
|
||||
typeof userAuth.getAgentSessionPolicy ===
|
||||
'function'
|
||||
|
||||
@@ -1006,6 +1006,34 @@ test('startSessionForUser resolves memories through Memory V2 facade', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('startSessionForUser records WeChat origin without changing the selected target', async () => {
|
||||
const registrations = [];
|
||||
let selectedTarget = '';
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
|
||||
selectedTarget = apiTarget;
|
||||
const proxy = createTkmindProxy({
|
||||
apiTarget,
|
||||
apiSecret: 'test-secret',
|
||||
userAuth: createMemoryTestUserAuth(workingDir),
|
||||
sessionAccess: {
|
||||
enabled: true,
|
||||
async registerSession(input) {
|
||||
registrations.push(input);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await proxy.startSessionForUser('user-1', { origin: 'wechat' });
|
||||
});
|
||||
|
||||
assert.deepEqual(registrations, [{
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
target: selectedTarget,
|
||||
origin: 'wechat',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('startSessionForUser does not inject memories when Memory V2 is disabled', async () => {
|
||||
let legacyTouched = false;
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir, harnessEntries }) => {
|
||||
|
||||
+62
-5
@@ -65,6 +65,7 @@ const DEFAULT_ASR_TARGET = process.env.H5_ASR_TARGET ?? 'https://asr.tkmind.cn';
|
||||
const WECHAT_RECENT_MEDIA_TTL_MS = 15 * 60 * 1000;
|
||||
const WECHAT_RECENT_IMAGE_MAX_COUNT = 10;
|
||||
const DEFAULT_WECHAT_AGENT_REPLY_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const WECHAT_SESSION_RESET_ACK_TEXT = '已切换到新会话,请发送你的新需求。';
|
||||
export { loadWechatMpConfig };
|
||||
const PUBLIC_HTML_LINK_PATTERN =
|
||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
||||
@@ -1517,6 +1518,7 @@ export function createWechatMpService({
|
||||
expiresAt: 0,
|
||||
};
|
||||
const rememberedWechatContexts = new Map();
|
||||
const confirmedWechatSessionOrigins = new Set();
|
||||
const messageTasksByOpenid = new Map();
|
||||
const recentMediaByOpenid = new Map();
|
||||
let jsapiTicketCache = {
|
||||
@@ -1527,6 +1529,19 @@ export function createWechatMpService({
|
||||
const fetchForSession = (sessionId, pathname, init) =>
|
||||
sessionApiFetch ? sessionApiFetch(sessionId, pathname, init) : apiFetch(pathname, init);
|
||||
|
||||
const confirmWechatSessionOrigin = async (sessionId) => {
|
||||
if (!sessionId || confirmedWechatSessionOrigins.has(sessionId)) return;
|
||||
if (typeof userAuth.setSessionOrigin === 'function') {
|
||||
try {
|
||||
await userAuth.setSessionOrigin(sessionId, 'wechat');
|
||||
} catch (err) {
|
||||
logger.warn?.('WeChat MP session origin update failed:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
confirmedWechatSessionOrigins.add(sessionId);
|
||||
};
|
||||
|
||||
const enqueueMessageTask = (openid, taskFactory) => {
|
||||
const key = String(openid ?? '').trim();
|
||||
const previous = messageTasksByOpenid.get(key) ?? Promise.resolve();
|
||||
@@ -2038,6 +2053,7 @@ export function createWechatMpService({
|
||||
sessionPolicy,
|
||||
).catch(() => false);
|
||||
if (routeHasTools) {
|
||||
await confirmWechatSessionOrigin(existingRoute.agentSessionId);
|
||||
if (typeof userAuth.touchWechatAgentRoute === 'function') {
|
||||
await userAuth.touchWechatAgentRoute(config.appId, openid, now).catch((err) => {
|
||||
logger.warn?.('WeChat MP route touch failed:', err);
|
||||
@@ -2057,7 +2073,12 @@ export function createWechatMpService({
|
||||
throw new Error(gate.message || '当前用户无法使用聊天能力');
|
||||
}
|
||||
const started = startAgentSession
|
||||
? await startAgentSession({ userId, workingDir, sessionPolicy })
|
||||
? await startAgentSession({
|
||||
userId,
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
origin: 'wechat',
|
||||
})
|
||||
: await readJsonResponse(
|
||||
await apiFetch('/agent/start', {
|
||||
method: 'POST',
|
||||
@@ -2094,6 +2115,7 @@ export function createWechatMpService({
|
||||
origin: 'wechat',
|
||||
});
|
||||
}
|
||||
await confirmWechatSessionOrigin(sessionId);
|
||||
await reconcileAgentSession(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
@@ -2167,7 +2189,14 @@ export function createWechatMpService({
|
||||
}
|
||||
};
|
||||
|
||||
const buildIntentMetadata = (intent, { mediaAnalysisEnabled = false, imagePolicy = null } = {}) => {
|
||||
const buildIntentMetadata = (
|
||||
intent,
|
||||
{
|
||||
mediaAnalysisEnabled = false,
|
||||
imagePolicy = null,
|
||||
pgRequired = false,
|
||||
} = {},
|
||||
) => {
|
||||
const mediaPublicUrl = intent.media?.publicUrl || null;
|
||||
const mediaItems = Array.isArray(intent.recentMediaItems) && intent.recentMediaItems.length > 0
|
||||
? intent.recentMediaItems
|
||||
@@ -2199,7 +2228,10 @@ export function createWechatMpService({
|
||||
recognition: intent.msgType === 'voice' ? intent.agentText || null : null,
|
||||
location: intent.location || null,
|
||||
link: intent.link || null,
|
||||
memindRun: buildWechatImageRunMetadata(imagePolicy),
|
||||
memindRun: {
|
||||
...buildWechatImageRunMetadata(imagePolicy),
|
||||
...(pgRequired ? { pgRequired: true } : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2348,6 +2380,14 @@ export function createWechatMpService({
|
||||
});
|
||||
let sessionId = route.sessionId;
|
||||
await ensureSessionProvider(sessionId);
|
||||
if (wechatIntent.kind === 'session.reset') {
|
||||
await sendCustomerServiceText(
|
||||
inbound.fromUserName,
|
||||
WECHAT_SESSION_RESET_ACK_TEXT,
|
||||
user,
|
||||
);
|
||||
return { sessionId };
|
||||
}
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
let finished = false;
|
||||
let progressTimer = null;
|
||||
@@ -2384,7 +2424,11 @@ export function createWechatMpService({
|
||||
sessionId,
|
||||
requestId,
|
||||
agentPrompt,
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
|
||||
buildIntentMetadata(intent, {
|
||||
mediaAnalysisEnabled,
|
||||
imagePolicy,
|
||||
pgRequired: isPageDataRequest,
|
||||
}),
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
userId: user.userId,
|
||||
@@ -2574,6 +2618,14 @@ export function createWechatMpService({
|
||||
});
|
||||
return { sessionId };
|
||||
} catch (err) {
|
||||
if (
|
||||
err &&
|
||||
typeof err === 'object' &&
|
||||
sessionId &&
|
||||
!err.wechatAgentSessionId
|
||||
) {
|
||||
err.wechatAgentSessionId = sessionId;
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (err?.code === 'WECHAT_AGENT_REPLY_TIMEOUT') {
|
||||
await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => {
|
||||
@@ -2628,7 +2680,11 @@ export function createWechatMpService({
|
||||
sessionId,
|
||||
retryId,
|
||||
retryPrompt,
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
|
||||
buildIntentMetadata(intent, {
|
||||
mediaAnalysisEnabled,
|
||||
imagePolicy,
|
||||
pgRequired: isPageDataRequest,
|
||||
}),
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
userId: user.userId,
|
||||
@@ -3284,6 +3340,7 @@ export function createWechatMpService({
|
||||
openid: inbound.fromUserName,
|
||||
msgId: inbound.msgId,
|
||||
status: 'failed',
|
||||
agentSessionId: err?.wechatAgentSessionId ?? null,
|
||||
}).catch(() => {});
|
||||
}
|
||||
logger.error?.('WeChat MP background reply failed:', err);
|
||||
|
||||
@@ -333,6 +333,94 @@ test('Page Data requests always rotate away from an existing WeChat route', () =
|
||||
assert.equal(shouldForceNewWechatAgentSession({ kind: 'chat.general' }, '换新会话'), true);
|
||||
});
|
||||
|
||||
test('WeChat session reset acknowledges without sending the control text to Agent', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const sentPayloads = [];
|
||||
const finishedMessages = [];
|
||||
let activeRoute = { agentSessionId: 'session-old' };
|
||||
let startedSessions = 0;
|
||||
let agentReplies = 0;
|
||||
const sessionOrigins = [];
|
||||
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
startAgentSession: async (input) => {
|
||||
startedSessions += 1;
|
||||
assert.equal(input.origin, 'wechat');
|
||||
return { id: 'session-new' };
|
||||
},
|
||||
userAuth: {
|
||||
async getWechatAgentRoute() {
|
||||
return activeRoute;
|
||||
},
|
||||
async clearWechatAgentRoute() {
|
||||
activeRoute = null;
|
||||
},
|
||||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||||
activeRoute = { agentSessionId };
|
||||
},
|
||||
async setSessionOrigin(sessionId, origin) {
|
||||
sessionOrigins.push({ sessionId, origin });
|
||||
},
|
||||
async finishWechatMpMessage(input) {
|
||||
finishedMessages.push(input);
|
||||
},
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname.includes('/reply')) {
|
||||
agentReplies += 1;
|
||||
throw new Error('session reset must not reach Agent');
|
||||
}
|
||||
return new Response('{}', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({
|
||||
access_token: 'access-1',
|
||||
expires_in: 7200,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
sentPayloads.push(JSON.parse(init.body));
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '新会话' }),
|
||||
{
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
},
|
||||
);
|
||||
await result.task;
|
||||
|
||||
assert.equal(startedSessions, 1);
|
||||
assert.equal(agentReplies, 0);
|
||||
assert.equal(activeRoute?.agentSessionId, 'session-new');
|
||||
assert.deepEqual(sessionOrigins, [{
|
||||
sessionId: 'session-new',
|
||||
origin: 'wechat',
|
||||
}]);
|
||||
assert.match(sentPayloads[0]?.text?.content ?? '', /已切换到新会话/);
|
||||
assert.equal(finishedMessages.at(-1)?.status, 'done');
|
||||
assert.equal(finishedMessages.at(-1)?.agentSessionId, 'session-new');
|
||||
});
|
||||
|
||||
test('WeChat Page Data repair intent receives the guarded repair workflow', () => {
|
||||
assert.equal(isWechatPageDataTask('提交失败,数据没有保存成功'), true);
|
||||
assert.equal(isWechatPageDataTask('修复问卷的 Page Data 绑定'), true);
|
||||
|
||||
Reference in New Issue
Block a user