fix(wechat): add cursor channel modules required by wechat-mp imports
Memind CI / Test, build, and release guards (push) Has been cancelled
Memind CI / Test, build, and release guards (push) Has been cancelled
Ship the WeChat Cursor executor helpers referenced by the page delivery path so tests and runtime imports resolve consistently. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createWechatMpService as createProductionWechatMpService } from './wechat-mp.mjs';
|
||||
import { prepareWechatHtmlDeliveryAtWorkspace } from './mindspace-wechat-html-delivery.mjs';
|
||||
import {
|
||||
isWechatCursorChannelReply,
|
||||
prepareWechatCursorPageDelivery,
|
||||
} from './wechat-cursor-page-delivery.mjs';
|
||||
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||
|
||||
function sha1(parts) {
|
||||
return crypto.createHash('sha1').update([...parts].sort().join('')).digest('hex');
|
||||
}
|
||||
|
||||
function signatureFor(token, timestamp, nonce) {
|
||||
return sha1([token, timestamp, nonce]);
|
||||
}
|
||||
|
||||
function inboundXml({ fromUser = 'openid-test-1', content = '帮我做个测试页面' } = {}) {
|
||||
return [
|
||||
'<xml>',
|
||||
'<ToUserName><![CDATA[gh_test]]></ToUserName>',
|
||||
`<FromUserName><![CDATA[${fromUser}]]></FromUserName>`,
|
||||
'<CreateTime>1710000000</CreateTime>',
|
||||
'<MsgType><![CDATA[text]]></MsgType>',
|
||||
`<Content><![CDATA[${content}]]></Content>`,
|
||||
'<MsgId>msg-cursor-test-1</MsgId>',
|
||||
'</xml>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
function createWechatMpService(options) {
|
||||
const resolvePublishDir = async (userId) =>
|
||||
options.userAuth?.resolveWorkingDir?.(userId) ?? `/tmp/${userId}`;
|
||||
const buildCanonicalUrl = (userId, relativePath) => {
|
||||
const base = String(options.config?.publicBaseUrl ?? 'https://example.com').replace(/\/+$/, '');
|
||||
const normalized = String(relativePath).replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
|
||||
return `${base}/MindSpace/${encodeURIComponent(userId)}/${normalized}`;
|
||||
};
|
||||
const htmlDeliveryAuthority = options.htmlDeliveryAuthority ?? {
|
||||
async prepareWechatHtmlDelivery(input) {
|
||||
const publishDir = await resolvePublishDir(input.userId);
|
||||
return prepareWechatHtmlDeliveryAtWorkspace({
|
||||
reply: input.reply,
|
||||
intent: input.intent,
|
||||
publishDir,
|
||||
requestStartedAt: input.requestStartedAt,
|
||||
allowRecentArtifacts: input.allowRecentArtifacts,
|
||||
buildCanonicalUrl: (relativePath) => buildCanonicalUrl(input.userId, relativePath),
|
||||
});
|
||||
},
|
||||
async ensureWechatFreshPageThumbnails() {
|
||||
return { artifacts: [], images: [] };
|
||||
},
|
||||
};
|
||||
return createProductionWechatMpService({
|
||||
...options,
|
||||
htmlDeliveryAuthority,
|
||||
});
|
||||
}
|
||||
|
||||
function createBoundWechatService(overrides = {}) {
|
||||
const apiCalls = [];
|
||||
const wechatCalls = [];
|
||||
const cursorCalls = [];
|
||||
const token = overrides.token ?? 'test-token';
|
||||
const userId = overrides.userId ?? 'user-cursor-test';
|
||||
|
||||
const agentRunGateway = overrides.agentRunGateway ?? {
|
||||
async createRun(runUserId, payload) {
|
||||
cursorCalls.push(['createRun', runUserId, payload]);
|
||||
const touchNow = Date.now();
|
||||
fs.utimesSync(htmlPath, touchNow / 1000, touchNow / 1000);
|
||||
return { id: 'run-cursor-1', status: 'queued' };
|
||||
},
|
||||
dispatchRun(runId) {
|
||||
cursorCalls.push(['dispatchRun', runId]);
|
||||
},
|
||||
async getRunForUser(runUserId, runId) {
|
||||
cursorCalls.push(['getRunForUser', runUserId, runId]);
|
||||
return { id: runId, status: 'succeeded', error: null };
|
||||
},
|
||||
async listRunEventsForUser(runUserId, runId) {
|
||||
cursorCalls.push(['listRunEventsForUser', runUserId, runId]);
|
||||
return {
|
||||
events: [{
|
||||
id: 'evt-1',
|
||||
eventType: 'tool_gateway_result',
|
||||
data: {
|
||||
executor: 'cursor',
|
||||
stdoutTail: '已生成 public/cursor-wechat-test-page.html',
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
}],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const wechatCursorExecutorPolicyService = overrides.wechatCursorExecutorPolicyService ?? {
|
||||
async getEffectivePolicy() {
|
||||
return {
|
||||
enabled: false,
|
||||
userAllowed: false,
|
||||
userAllowlist: [],
|
||||
intentAllowlist: ['page.generate'],
|
||||
fallbackToDeepseek: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const h5Root = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-cursor-h5-'));
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
||||
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
|
||||
const htmlPath = path.join(publishDir, 'public', 'cursor-wechat-test-page.html');
|
||||
fs.writeFileSync(htmlPath, '<html><head><title>Cursor Page</title></head><body><main>hi</main></body></html>');
|
||||
const now = Date.now();
|
||||
fs.utimesSync(htmlPath, now / 1000, now / 1000);
|
||||
|
||||
const service = createWechatMpService({
|
||||
h5Root,
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx_local_test_only',
|
||||
appSecret: 'local-secret',
|
||||
token,
|
||||
publicBaseUrl: 'https://example.com',
|
||||
mediaPublicBaseUrl: 'https://example.com',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
unsupportedText: 'unsupported',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
progressDelayMs: 0,
|
||||
requireFreshPageThumbnail: false,
|
||||
...(overrides.config ?? {}),
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid(_appId, openid) {
|
||||
return {
|
||||
userId,
|
||||
status: 'active',
|
||||
nickname: '测试用户',
|
||||
username: 'test-user',
|
||||
openid,
|
||||
};
|
||||
},
|
||||
async getWechatAgentRoute() {
|
||||
return { agentSessionId: 'session-cursor-1', isNewSession: false };
|
||||
},
|
||||
async clearWechatAgentRoute() {},
|
||||
async canUseChat() {
|
||||
return { ok: true };
|
||||
},
|
||||
async resolveWorkingDir() {
|
||||
return publishDir;
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { displayName: '测试用户', username: 'test-user', slug: 'test-user', constraints: null };
|
||||
},
|
||||
async registerAgentSession() {},
|
||||
async upsertWechatAgentRoute() {},
|
||||
async billSessionUsage() {},
|
||||
async recordWechatMpMessage() {
|
||||
return { inserted: true };
|
||||
},
|
||||
async finishWechatMpMessage() {},
|
||||
async insertWechatMpMessageDetail() {},
|
||||
...(overrides.userAuth ?? {}),
|
||||
},
|
||||
agentRunGateway,
|
||||
wechatCursorExecutorPolicyService,
|
||||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||||
apiCalls.push([pathname, init.method ?? 'GET']);
|
||||
if (pathname.endsWith('/events')) {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-fallback-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"DeepSeek 回退页面已完成\\nhttps://example.com/MindSpace/user-cursor-test/public/fallback-page.html"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-fallback-1","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname.endsWith('/reply')) {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname.includes('/agent/harness_remember') || pathname.includes('/agent/session/reconcile') || pathname.includes('/agent/harness_bootstrap')) {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname} session=${sessionId}`);
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
wechatCalls.push([String(url), init.method ?? 'GET', init]);
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'local-access-token', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
linkExists: async (urlText) => String(urlText).includes('cursor-wechat-test-page.html')
|
||||
|| String(urlText).includes('fallback-page.html'),
|
||||
...(overrides.extra ?? {}),
|
||||
});
|
||||
|
||||
return {
|
||||
service,
|
||||
apiCalls,
|
||||
wechatCalls,
|
||||
cursorCalls,
|
||||
publishDir,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
test('non-allowlisted wechat user keeps deepseek session reply path', async () => {
|
||||
const { service, apiCalls, cursorCalls, token } = createBoundWechatService();
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce-no-cursor';
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '帮我做一个简单测试页面' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), false);
|
||||
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), true);
|
||||
});
|
||||
|
||||
test('allowlisted wechat user uses cursor agent-run path without session reply', async () => {
|
||||
const { service, apiCalls, cursorCalls, wechatCalls, token } = createBoundWechatService({
|
||||
wechatCursorExecutorPolicyService: {
|
||||
async getEffectivePolicy(_userId, user) {
|
||||
return {
|
||||
enabled: true,
|
||||
userAllowed: true,
|
||||
userAllowlist: [user?.userId ?? 'user-cursor-test'],
|
||||
intentAllowlist: ['page.generate'],
|
||||
fallbackToDeepseek: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const timestamp = '1710000001';
|
||||
const nonce = 'nonce-cursor';
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '帮我做一个简单测试页面' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), true);
|
||||
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), false);
|
||||
const customSend = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
assert.ok(customSend, 'expected wechat custom send');
|
||||
const sendBody = JSON.parse(String(customSend[2]?.body ?? '{}'));
|
||||
const outboundText = String(sendBody?.text?.content ?? '');
|
||||
assert.doesNotMatch(outboundText, /没有按服务号页面技能真正生成成功/);
|
||||
assert.match(outboundText, /cursor-wechat-test-page\.html/);
|
||||
});
|
||||
|
||||
test('allowlisted user falls back to deepseek when cursor run fails', async () => {
|
||||
const { service, apiCalls, cursorCalls, token } = createBoundWechatService({
|
||||
agentRunGateway: {
|
||||
async createRun(runUserId, payload) {
|
||||
cursorCalls.push(['createRun', runUserId, payload]);
|
||||
return { id: 'run-fail-1', status: 'queued' };
|
||||
},
|
||||
dispatchRun(runId) {
|
||||
cursorCalls.push(['dispatchRun', runId]);
|
||||
},
|
||||
async getRunForUser(runUserId, runId) {
|
||||
cursorCalls.push(['getRunForUser', runUserId, runId]);
|
||||
return { id: runId, status: 'failed', error: 'simulated cursor failure' };
|
||||
},
|
||||
async listRunEventsForUser() {
|
||||
return { events: [] };
|
||||
},
|
||||
},
|
||||
wechatCursorExecutorPolicyService: {
|
||||
async getEffectivePolicy() {
|
||||
return {
|
||||
enabled: true,
|
||||
userAllowed: true,
|
||||
userAllowlist: ['user-cursor-test'],
|
||||
intentAllowlist: ['page.generate'],
|
||||
fallbackToDeepseek: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const timestamp = '1710000002';
|
||||
const nonce = 'nonce-fallback';
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '帮我做一个简单测试页面' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), true);
|
||||
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), true);
|
||||
});
|
||||
|
||||
test('production-like openid in script default is not used when policy disabled', async () => {
|
||||
const prodLikeOpenid = 'ooil-0VFj68QK1tkHl39uL610et8';
|
||||
const { service, cursorCalls, token } = createBoundWechatService({
|
||||
userId: '1c99b83b-0454-474f-a5d2-129d34506a32',
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid(_appId, openid) {
|
||||
assert.equal(openid, prodLikeOpenid);
|
||||
return {
|
||||
userId: '1c99b83b-0454-474f-a5d2-129d34506a32',
|
||||
status: 'active',
|
||||
nickname: 'John',
|
||||
username: 'john',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const timestamp = '1710000003';
|
||||
const nonce = 'nonce-prod-openid';
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ fromUser: prodLikeOpenid, content: '你好' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
assert.equal(cursorCalls.length, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user