Files
memind/shenmei-opinion-form-routes.mjs
T
john 32fb2cdeaf feat: chat uploads, vision turn isolation, and MindSpace agent improvements
Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 00:23:01 +08:00

176 lines
5.4 KiB
JavaScript

import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
const FORM_SLUG = 'shenmei-opinion-unified';
const ADMIN_PASSWORD = 'sm6666';
const MAX_RECORDS_PER_REQUEST = 50;
const MAX_FIELD_LENGTH = 5000;
const ALLOWED_FIELDS = [
'id',
'name',
'goodPeople',
'goodBehaviors',
'badPeople',
'badSuggestions',
'time',
'timestamp',
];
function normalizeString(value) {
return String(value ?? '').trim().slice(0, MAX_FIELD_LENGTH);
}
function normalizeRecord(raw) {
const source = raw && typeof raw === 'object' ? raw : {};
const record = {};
for (const field of ALLOWED_FIELDS) {
if (source[field] == null) continue;
record[field] = field === 'timestamp' ? Number(source[field]) || Date.now() : normalizeString(source[field]);
}
if (!record.id) {
record.id = `shenmei_${Date.now()}_${Math.random().toString(36).slice(2)}`;
}
if (!record.time) {
record.time = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
}
if (!record.timestamp) {
record.timestamp = Date.now();
}
return record;
}
function recordFingerprint(record) {
const parts = ['name', 'goodPeople', 'goodBehaviors', 'badPeople', 'badSuggestions', 'time', 'timestamp'].map((key) =>
String(record?.[key] ?? '').trim().replace(/\s+/g, ' '),
);
return parts.join('\u0000');
}
function isValidRecord(record) {
return Boolean(record.name && record.goodPeople && record.badPeople);
}
export async function readShenmeiOpinionEntries(filePath) {
let text = '';
try {
text = await fsPromises.readFile(filePath, 'utf8');
} catch (error) {
if (error?.code === 'ENOENT') return [];
throw error;
}
return text
.split(/\n/)
.filter(Boolean)
.map((line) => JSON.parse(line));
}
function publicRecordFromEntry(entry) {
return entry?.record && typeof entry.record === 'object' ? entry.record : entry;
}
async function appendJsonLine(filePath, value) {
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
await fsPromises.appendFile(filePath, `${JSON.stringify(value)}\n`, 'utf8');
}
function makeEnvelope(record, req) {
return {
form: FORM_SLUG,
receivedAt: new Date().toISOString(),
requestId: req.id ?? null,
ipHash: req.ip
? String(req.ip)
.split('')
.reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0)
: null,
userAgentHash: req.get('user-agent')
? String(req.get('user-agent'))
.split('')
.reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0)
: null,
record,
};
}
export function createShenmeiOpinionFormService({ dataFile }) {
if (!dataFile) throw new Error('dataFile is required');
async function listRecords() {
const entries = await readShenmeiOpinionEntries(dataFile);
return entries.map(publicRecordFromEntry).filter((record) => record && typeof record === 'object');
}
async function submit(rawRecords, req) {
const sourceRecords = Array.isArray(rawRecords) ? rawRecords : [rawRecords];
const normalized = sourceRecords.slice(0, MAX_RECORDS_PER_REQUEST).map(normalizeRecord).filter(isValidRecord);
if (normalized.length === 0) return { inserted: 0, skipped: sourceRecords.length, total: (await listRecords()).length };
const existingEntries = await readShenmeiOpinionEntries(dataFile);
const seen = new Set(existingEntries.map(publicRecordFromEntry).map(recordFingerprint));
let inserted = 0;
let skipped = 0;
for (const record of normalized) {
const fingerprint = recordFingerprint(record);
if (seen.has(fingerprint)) {
skipped += 1;
continue;
}
seen.add(fingerprint);
await appendJsonLine(dataFile, makeEnvelope(record, req));
inserted += 1;
}
return {
inserted,
skipped,
total: existingEntries.length + inserted,
};
}
return {
dataFile,
listRecords,
submit,
};
}
export function attachShenmeiOpinionFormRoutes(api, options = {}) {
const dataFile =
options.dataFile ?? path.join(options.rootDir ?? process.cwd(), 'data', 'form-responses', `${FORM_SLUG}.ndjson`);
const service = options.service ?? createShenmeiOpinionFormService({ dataFile });
api.post(`/public/forms/${FORM_SLUG}/submit`, async (req, res) => {
try {
const records = Array.isArray(req.body?.records) ? req.body.records : req.body?.record ?? req.body;
const result = await service.submit(records, req);
return res.json({ ok: true, ...result });
} catch (error) {
console.error('[ShenmeiOpinionForm] submit failed:', error instanceof Error ? error.message : error);
return res.status(500).json({ ok: false, message: '保存失败,请稍后重试' });
}
});
api.get(`/public/forms/${FORM_SLUG}/responses`, async (req, res) => {
if (req.get('x-shenmei-admin') !== ADMIN_PASSWORD) {
return res.status(403).json({ ok: false, message: '后台密码错误' });
}
try {
const data = await service.listRecords();
return res.json({ ok: true, data });
} catch (error) {
console.error('[ShenmeiOpinionForm] read failed:', error instanceof Error ? error.message : error);
return res.status(500).json({ ok: false, message: '读取失败,请稍后重试' });
}
});
return service;
}
export const shenmeiOpinionFormInternals = {
FORM_SLUG,
normalizeRecord,
recordFingerprint,
};