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,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',
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user