fix(wechat): add cursor channel modules required by wechat-mp imports
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:
john
2026-08-26 21:14:36 +08:00
parent 5bba9d3e40
commit 644f7fc632
7 changed files with 929 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import { buildCodeRunCompletionReply } from './agent-run-gateway.mjs';
import { enforcePageGenerationCursorRuntime } from './cursor-page-routing.mjs';
const DEFAULT_POLL_MS = 2000;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function buildWechatCursorUserMessage({
displayText,
intentKind,
}) {
const taskText = String(displayText ?? '').trim();
return {
role: 'user',
content: [{ type: 'text', text: taskText }],
metadata: {
displayText: taskText,
memindRun: {
channel: 'wechat_mp',
wechatCursorChannel: true,
taskType: 'wechat_page_generate',
toolMode: 'code',
requiredExecutor: 'cursor',
intentKind: String(intentKind ?? '').trim() || null,
},
},
};
}
async function readCursorCompletionText(agentRunGateway, userId, runId) {
const eventsResult = await agentRunGateway.listRunEventsForUser(userId, runId, { limit: 200 });
const events = Array.isArray(eventsResult?.events) ? eventsResult.events : [];
const resultEvent = [...events].reverse().find((item) => item.eventType === 'tool_gateway_result');
if (!resultEvent?.data) {
return buildCodeRunCompletionReply({ executor: 'cursor', stdout: '' });
}
return buildCodeRunCompletionReply({
executor: resultEvent.data.executor ?? 'cursor',
stdout: resultEvent.data.stdoutTail ?? '',
});
}
export async function executeWechatCursorAgentRun({
agentRunGateway,
userId,
sessionId = null,
requestId,
displayText,
agentPrompt,
intentKind = 'page.generate',
timeoutMs = 15 * 60 * 1000,
pollMs = DEFAULT_POLL_MS,
logger = console,
} = {}) {
if (!agentRunGateway?.createRun || !agentRunGateway?.dispatchRun || !agentRunGateway?.getRunForUser) {
throw Object.assign(new Error('WeChat Cursor 执行网关不可用'), {
code: 'WECHAT_CURSOR_GATEWAY_UNAVAILABLE',
});
}
let userMessage = buildWechatCursorUserMessage({ displayText, intentKind });
const cursorRuntime = enforcePageGenerationCursorRuntime(userMessage, {
rawToolMode: 'code',
taskType: 'wechat_page_generate',
env: process.env,
});
userMessage = cursorRuntime.userMessage;
const run = await agentRunGateway.createRun(userId, {
sessionId,
requestId,
userMessage,
toolMode: 'code',
taskType: cursorRuntime.taskType ?? 'h5_chat_code_task',
});
agentRunGateway.dispatchRun(run.id);
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (Date.now() <= deadline) {
const latest = await agentRunGateway.getRunForUser(userId, run.id);
if (!latest) {
throw Object.assign(new Error('WeChat Cursor 任务丢失'), {
code: 'WECHAT_CURSOR_RUN_MISSING',
});
}
if (latest.status === 'succeeded') {
const text = await readCursorCompletionText(agentRunGateway, userId, run.id);
logger.info?.('[wechat-cursor] run succeeded', {
userId,
runId: run.id,
requestId,
});
return {
text,
tokenState: null,
messages: [{
role: 'assistant',
content: [{ type: 'text', text }],
metadata: { userVisible: true, source: 'wechat-cursor-agent-run' },
}],
requestMessages: [],
};
}
if (latest.status === 'failed') {
const error = new Error(latest.error || 'WeChat Cursor 执行失败');
error.code = 'WECHAT_CURSOR_RUN_FAILED';
throw error;
}
await sleep(Math.max(500, Number(pollMs) || DEFAULT_POLL_MS));
}
throw Object.assign(new Error('WeChat Cursor 执行超时'), {
code: 'WECHAT_CURSOR_RUN_TIMEOUT',
});
}
+341
View File
@@ -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);
});
+220
View File
@@ -0,0 +1,220 @@
const CONFIG_TABLE = 'h5_wechat_cursor_executor_config';
const CONFIG_SCOPE = 'global';
const POLICY_SOURCE_DEFAULT = 'default';
const POLICY_SOURCE_ADMIN_DB = 'admin-db';
const DEFAULT_INTENT_ALLOWLIST = Object.freeze(['page.generate']);
function defaultConfigShape() {
return {
enabled: false,
userAllowlist: [],
intentAllowlist: [...DEFAULT_INTENT_ALLOWLIST],
fallbackToDeepseek: true,
meta: {
notes: '',
},
};
}
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
function normalizeStringList(value) {
if (!Array.isArray(value)) {
return String(value ?? '')
.split(/[\n,]+/)
.map((item) => item.trim())
.filter(Boolean);
}
return value.map((item) => String(item ?? '').trim()).filter(Boolean);
}
function cloneConfig(config = null) {
return structuredClone?.(config ?? defaultConfigShape())
?? JSON.parse(JSON.stringify(config ?? defaultConfigShape()));
}
function parseJsonLike(value, fallback) {
if (value == null || value === '') return fallback;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
if (typeof value === 'object') return value;
return fallback;
}
function mergePatch(currentConfig, patch = {}) {
const next = cloneConfig(currentConfig);
if ('enabled' in patch) next.enabled = normalizeBoolean(patch.enabled, false);
if ('userAllowlist' in patch) next.userAllowlist = normalizeStringList(patch.userAllowlist);
if ('intentAllowlist' in patch) {
const intents = normalizeStringList(patch.intentAllowlist);
next.intentAllowlist = intents.length ? intents : [...DEFAULT_INTENT_ALLOWLIST];
}
if ('fallbackToDeepseek' in patch) {
next.fallbackToDeepseek = normalizeBoolean(patch.fallbackToDeepseek, true);
}
if (patch?.meta && typeof patch.meta.notes === 'string') {
next.meta.notes = patch.meta.notes;
}
return next;
}
function flattenPolicy(config, source) {
const intentAllowlist = normalizeStringList(config.intentAllowlist);
return {
source,
enabled: Boolean(config.enabled),
userAllowlist: normalizeStringList(config.userAllowlist),
intentAllowlist: intentAllowlist.length ? intentAllowlist : [...DEFAULT_INTENT_ALLOWLIST],
fallbackToDeepseek: config.fallbackToDeepseek !== false,
};
}
export function isUserAllowedByWechatCursorPolicy(user, policy) {
if (!policy?.enabled) return false;
const allowlist = normalizeStringList(policy.userAllowlist).map((item) => item.toLowerCase());
if (allowlist.length === 0) return false;
if (allowlist.includes('*')) return true;
const identities = [
user?.userId,
user?.id,
user?.username,
user?.slug,
user?.displayName,
user?.nickname,
]
.map((value) => String(value ?? '').trim().toLowerCase())
.filter(Boolean);
return identities.some((identity) => allowlist.includes(identity));
}
export function isIntentAllowedByWechatCursorPolicy(intentKind, policy) {
const allowlist = (policy?.intentAllowlist ?? DEFAULT_INTENT_ALLOWLIST)
.map((item) => String(item ?? '').trim())
.filter(Boolean);
const normalized = String(intentKind ?? '').trim();
if (!normalized) return false;
return allowlist.includes(normalized);
}
async function ensureConfigTable(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
config_scope VARCHAR(32) PRIMARY KEY,
config_json JSON NOT NULL,
updated_by CHAR(36) NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
async function loadStoredState(pool) {
await ensureConfigTable(pool);
const [rows] = await pool.query(
`SELECT config_json, updated_by, updated_at
FROM ${CONFIG_TABLE}
WHERE config_scope = ?
LIMIT 1`,
[CONFIG_SCOPE],
);
const row = rows[0];
if (!row) return null;
const parsed = parseJsonLike(row.config_json, {});
return {
config: mergePatch(defaultConfigShape(), parsed),
updatedAt: Number(row.updated_at ?? 0) || null,
updatedBy: row.updated_by ?? null,
};
}
export function createWechatCursorExecutorAdminConfigService(pool) {
async function loadEffectiveConfig() {
const stored = await loadStoredState(pool);
if (stored) {
return {
config: cloneConfig(stored.config),
updatedAt: stored.updatedAt,
updatedBy: stored.updatedBy,
source: POLICY_SOURCE_ADMIN_DB,
};
}
return {
config: defaultConfigShape(),
updatedAt: null,
updatedBy: null,
source: POLICY_SOURCE_DEFAULT,
};
}
return {
async getAdminConfig() {
const state = await loadEffectiveConfig();
return {
config: state.config,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
source: state.source,
};
},
async updateAdminConfig(patch = {}, { updatedBy = null } = {}) {
const stored = await loadStoredState(pool);
const base = stored?.config ?? defaultConfigShape();
const nextConfig = mergePatch(base, patch.config ?? patch);
await ensureConfigTable(pool);
const now = Date.now();
await pool.query(
`INSERT INTO ${CONFIG_TABLE}
(config_scope, config_json, updated_by, updated_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[CONFIG_SCOPE, JSON.stringify(nextConfig), updatedBy, now],
);
return this.getAdminConfig();
},
async getRuntimeState() {
const state = await loadEffectiveConfig();
const policy = flattenPolicy(state.config, state.source);
return {
source: state.source,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
config: state.config,
policy,
};
},
async getEffectivePolicy(userId, user = null) {
const state = await loadEffectiveConfig();
const policy = flattenPolicy(state.config, state.source);
const subject = user ?? { userId };
return {
...policy,
userAllowed: isUserAllowedByWechatCursorPolicy(subject, policy),
};
},
};
}
export const wechatCursorExecutorAdminConfigInternals = {
CONFIG_TABLE,
defaultConfigShape,
mergePatch,
flattenPolicy,
};
+17
View File
@@ -0,0 +1,17 @@
import {
isIntentAllowedByWechatCursorPolicy,
isUserAllowedByWechatCursorPolicy,
} from './wechat-cursor-executor-admin-config.mjs';
export function resolveWechatCursorExecutorEligible({
user = null,
userId = null,
intentKind = '',
policy = null,
} = {}) {
if (!policy?.enabled) return false;
const subject = user ?? { userId };
if (!isUserAllowedByWechatCursorPolicy(subject, policy)) return false;
if (!isIntentAllowedByWechatCursorPolicy(intentKind, policy)) return false;
return true;
}
+91
View File
@@ -0,0 +1,91 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createWechatCursorExecutorAdminConfigService,
isUserAllowedByWechatCursorPolicy,
isIntentAllowedByWechatCursorPolicy,
} from './wechat-cursor-executor-admin-config.mjs';
import { resolveWechatCursorExecutorEligible } from './wechat-cursor-executor-policy.mjs';
function createMemoryPool() {
const rows = new Map();
return {
async query(sql, params = []) {
const normalized = String(sql).replace(/\s+/g, ' ').trim();
if (normalized.startsWith('CREATE TABLE')) return [[]];
if (normalized.startsWith('INSERT INTO h5_wechat_cursor_executor_config')) {
rows.set('global', {
config_json: params[1],
updated_by: params[2],
updated_at: params[3],
});
return [{ affectedRows: 1 }];
}
if (normalized.startsWith('SELECT config_json')) {
const row = rows.get('global');
return [row ? [row] : []];
}
throw new Error(`Unexpected SQL: ${normalized}`);
},
};
}
test('default policy disables cursor channel for everyone', async () => {
const service = createWechatCursorExecutorAdminConfigService(createMemoryPool());
const policy = await service.getEffectivePolicy('user-1', { userId: 'user-1' });
assert.equal(policy.enabled, false);
assert.equal(policy.userAllowed, false);
assert.deepEqual(policy.intentAllowlist, ['page.generate']);
});
test('allowlisted user can use cursor channel for page.generate only', async () => {
const pool = createMemoryPool();
const service = createWechatCursorExecutorAdminConfigService(pool);
await service.updateAdminConfig({
enabled: true,
userAllowlist: ['john-uuid'],
intentAllowlist: ['page.generate'],
}, { updatedBy: 'admin-1' });
const policy = await service.getEffectivePolicy('john-uuid', { userId: 'john-uuid' });
assert.equal(policy.userAllowed, true);
assert.equal(
resolveWechatCursorExecutorEligible({
user: { userId: 'john-uuid' },
intentKind: 'page.generate',
policy,
}),
true,
);
assert.equal(
resolveWechatCursorExecutorEligible({
user: { userId: 'john-uuid' },
intentKind: 'chat.general',
policy,
}),
false,
);
assert.equal(
resolveWechatCursorExecutorEligible({
user: { userId: 'other-user' },
intentKind: 'page.generate',
policy,
}),
false,
);
});
test('isUserAllowedByWechatCursorPolicy matches username aliases', () => {
const policy = { enabled: true, userAllowlist: ['john'] };
assert.equal(
isUserAllowedByWechatCursorPolicy({ userId: 'x', username: 'john' }, policy),
true,
);
assert.equal(isUserAllowedByWechatCursorPolicy({ userId: 'x' }, policy), false);
});
test('isIntentAllowedByWechatCursorPolicy respects allowlist', () => {
const policy = { intentAllowlist: ['page.generate'] };
assert.equal(isIntentAllowedByWechatCursorPolicy('page.generate', policy), true);
assert.equal(isIntentAllowedByWechatCursorPolicy('chat.general', policy), false);
});
+105
View File
@@ -0,0 +1,105 @@
import fs from 'node:fs';
import { prepareWechatHtmlDeliveryAtWorkspace } from './mindspace-wechat-html-delivery.mjs';
import { resolveArtifactLocalPath } from './wechat/verify/page-artifact.mjs';
import {
extractPageTitle,
repairArtifactSharePreview,
repairSharePreviewHtml,
} from './wechat/verify/share-preview-repair.mjs';
const MIN_CURSOR_PAGE_BYTES = 512;
export function isWechatCursorChannelReply(reply) {
return reply?.wechatCursorChannel === true;
}
function padHtmlToMinBytes(html, minBytes = MIN_CURSOR_PAGE_BYTES) {
let next = String(html ?? '');
if (Buffer.byteLength(next, 'utf8') >= minBytes) return next;
const deficit = minBytes - Buffer.byteLength(next, 'utf8') + 32;
const pad = `<!-- cursor-page-pad -->${' '.repeat(Math.max(0, deficit))}`;
if (/<\/body>/i.test(next)) {
return next.replace(/<\/body>/i, `${pad}</body>`);
}
if (/<\/html>/i.test(next)) {
return next.replace(/<\/html>/i, `${pad}</html>`);
}
return `${next}${pad}`;
}
function normalizeCursorArtifactForWechatDelivery(artifact, { publishDir, topic }) {
const localPath = resolveArtifactLocalPath(artifact, publishDir);
if (!localPath) return artifact;
try {
if (!fs.existsSync(localPath)) return artifact;
const title = extractPageTitle(fs.readFileSync(localPath, 'utf8'));
let content = fs.readFileSync(localPath, 'utf8');
content = padHtmlToMinBytes(content);
content = repairSharePreviewHtml(content, { title, topic });
fs.writeFileSync(localPath, content, 'utf8');
repairArtifactSharePreview(
{ ...artifact, localPath },
{ topic, publishDir },
);
} catch {
// Best-effort repair; delivery validation will fail closed if still unusable.
}
return artifact;
}
export function prepareWechatCursorPageDelivery({
reply,
intent,
publishDir,
requestStartedAt,
buildCanonicalUrl,
topic = '',
}) {
const baseReply = {
...reply,
wechatCursorChannel: true,
};
if (!String(publishDir ?? '').trim()) {
return baseReply;
}
const prepared = prepareWechatHtmlDeliveryAtWorkspace({
reply: baseReply,
intent,
publishDir,
requestStartedAt,
allowRecentArtifacts: true,
buildCanonicalUrl,
});
const candidateArtifacts = [
...(prepared.recentArtifacts ?? []),
...(prepared.confirmedArtifacts ?? []),
...(prepared.publishedArtifacts ?? []),
];
for (const artifact of candidateArtifacts) {
normalizeCursorArtifactForWechatDelivery(artifact, { publishDir, topic });
}
const reprepared = prepareWechatHtmlDeliveryAtWorkspace({
reply: baseReply,
intent,
publishDir,
requestStartedAt,
allowRecentArtifacts: true,
buildCanonicalUrl,
});
const confirmedArtifacts = reprepared.confirmedArtifacts ?? [];
const urls = confirmedArtifacts
.map((artifact) => String(artifact?.url ?? '').trim())
.filter(Boolean);
const uniqueUrls = [...new Set(urls)];
return {
...baseReply,
text: uniqueUrls.length > 0
? [String(baseReply.text ?? '').trim(), ...uniqueUrls].filter(Boolean).join('\n')
: String(baseReply.text ?? '').trim(),
cursorPreparedArtifacts: confirmedArtifacts,
};
}
+38
View File
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
isWechatCursorChannelReply,
prepareWechatCursorPageDelivery,
} from './wechat-cursor-page-delivery.mjs';
test('prepareWechatCursorPageDelivery repairs recent cursor html for wechat delivery', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-cursor-delivery-'));
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
const htmlPath = path.join(publishDir, 'public', 'cursor-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 reply = prepareWechatCursorPageDelivery({
reply: {
text: '已由 TKMind 智趣完成执行,并通过平台文件验收。',
requestMessages: [],
},
intent: { agentText: '帮我做一个简单测试页面', displayText: '帮我做一个简单测试页面' },
publishDir,
requestStartedAt: now - 1000,
topic: '简单测试页面',
buildCanonicalUrl: (relativePath) =>
`https://example.com/MindSpace/user-1/${String(relativePath).replace(/^\/+/, '')}`,
});
assert.equal(isWechatCursorChannelReply(reply), true);
assert.match(reply.text, /https:\/\/example.com\/MindSpace\/user-1\//);
const content = fs.readFileSync(htmlPath, 'utf8');
assert.match(content, /mindspace-cover/i);
assert.match(content, /platform-brand/i);
assert.ok(Buffer.byteLength(content, 'utf8') >= 512);
});