32fb2cdeaf
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>
176 lines
5.4 KiB
JavaScript
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,
|
|
};
|