Add smart ACK provider for WeChat MP replies

Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
john
2026-06-26 15:19:03 +08:00
parent 9ed4fd48d7
commit 9b4a25799f
162 changed files with 17276 additions and 2054 deletions
+70 -1
View File
@@ -4,6 +4,7 @@ import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createAssetService, validateUploadRequest } from './mindspace-assets.mjs';
import { DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
function createMockPool(state) {
return {
@@ -87,7 +88,7 @@ function createMockPool(state) {
state.versions.push({
id: params[0],
asset_id: params[1],
storage_key: params[3],
storage_key: params[2],
scan_status: params[8],
});
return [[]];
@@ -166,6 +167,23 @@ test('validateUploadRequest rejects traversal filenames', () => {
);
});
test('validateUploadRequest allows 5MB uploads by default', () => {
assert.doesNotThrow(() =>
validateUploadRequest({
filename: 'handbook.pdf',
sizeBytes: DEFAULT_MAX_FILE_BYTES,
}),
);
assert.throws(
() =>
validateUploadRequest({
filename: 'handbook.pdf',
sizeBytes: DEFAULT_MAX_FILE_BYTES + 1,
}),
(error) => error.code === 'file_too_large',
);
});
test('completeUpload marks safe files ready and blocks script payloads', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
const state = {
@@ -206,6 +224,57 @@ test('completeUpload marks safe files ready and blocks script payloads', async (
assert.equal(asset.scanStatus, 'blocked');
});
test('completeUpload publishes public images to the public temp image library', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-h5-'));
const state = {
categories: [
{ id: 'cat-public', user_id: 'user-1', space_id: 'space-1', category_code: 'public' },
],
spaces: [
{
id: 'space-1',
user_id: 'user-1',
quota_bytes: 5 * 1024 * 1024,
used_bytes: 0,
reserved_bytes: 0,
status: 'active',
},
],
uploads: [],
assets: [],
versions: [],
};
let nextId = 0;
const service = createAssetService(createMockPool(state), {
storageRoot,
h5Root,
idFactory: () => `id-${++nextId}`,
});
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
'base64',
);
const upload = await service.createUpload('user-1', {
categoryId: 'cat-public',
filename: 'look.png',
sizeBytes: png.length,
});
await service.writeUploadContent('user-1', upload.id, png);
const asset = await service.completeUpload('user-1', upload.id);
assert.equal(asset.status, 'ready');
assert.match(asset.publicUrl, /^https:\/\/g2\.tkmind\.cn\/MindSpace\/user-1\/public\/\.tmp-images\/id-2\.png$/);
assert.equal(state.versions[0].storage_key, 'users/user-1/public/.tmp-images/id-2.png');
await assert.doesNotReject(() =>
fs.stat(path.join(storageRoot, 'users/user-1/public/.tmp-images/id-2.png')),
);
await assert.doesNotReject(() =>
fs.stat(path.join(h5Root, 'MindSpace/user-1/public/.tmp-images/id-2.png')),
);
});
test('createUpload rejects another users category id', async () => {
const state = {
categories: [