235 lines
7.7 KiB
JavaScript
235 lines
7.7 KiB
JavaScript
|
|
/**
|
||
|
|
* server.js - AI 接单系统主服务
|
||
|
|
* 支持 Docker 部署、垃圾过滤、环境变量配置
|
||
|
|
*/
|
||
|
|
import http from 'http';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
import { fileURLToPath } from 'url';
|
||
|
|
import { CONFIG, validateConfig } from './config.js';
|
||
|
|
import {
|
||
|
|
createOrder, getActiveOrders, getArchivedOrders, getAllOrders, findOrder, getOrder,
|
||
|
|
updateStatus, addSubTask, updateSubStatus, updateProgress, setExpectedDays,
|
||
|
|
deliverOrder, archiveOrder, addComment, getRecentOrders, getPendingCount, deleteOrder, flagOrder
|
||
|
|
} from './order-manager.js';
|
||
|
|
import { analyzeRequirement, generateConfirmation } from './ai-intake.js';
|
||
|
|
import { filterOrder, getRateStatus } from './spam-filter.js';
|
||
|
|
|
||
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
|
||
|
|
// 启动检查
|
||
|
|
const configWarnings = validateConfig();
|
||
|
|
if (configWarnings.length) configWarnings.forEach(w => console.log(w));
|
||
|
|
|
||
|
|
// 路由表
|
||
|
|
const routes = {
|
||
|
|
'/' : serveStatic('submit.html', 'text/html'),
|
||
|
|
'/submit' : serveStatic('submit.html', 'text/html'),
|
||
|
|
'/admin' : serveStatic('admin.html', 'text/html'),
|
||
|
|
'/track' : serveStatic('track.html', 'text/html'),
|
||
|
|
'/style.css' : serveStatic('style.css', 'text/css'),
|
||
|
|
'/api/orders' : apiOrders,
|
||
|
|
'/api/orders/active': apiActiveOrders,
|
||
|
|
'/api/orders/archived': apiArchivedOrders,
|
||
|
|
'/api/orders/search': apiSearchOrders,
|
||
|
|
'/api/orders/pending': apiPendingCount,
|
||
|
|
'/api/orders/recent': apiRecentOrders,
|
||
|
|
'/api/orders/analyze': apiAnalyze,
|
||
|
|
'/api/health' : apiHealth,
|
||
|
|
};
|
||
|
|
|
||
|
|
function serveStatic(file, mime) {
|
||
|
|
return (req, res) => {
|
||
|
|
const p = path.join(__dirname, 'public', file);
|
||
|
|
try {
|
||
|
|
let data = fs.readFileSync(p, 'utf-8');
|
||
|
|
// 注入站点名称
|
||
|
|
data = data.replace(/__SITE_NAME__/g, CONFIG.siteName);
|
||
|
|
data = data.replace(/__ADMIN_TOKEN__/g, CONFIG.adminToken);
|
||
|
|
res.writeHead(200, { 'Content-Type': mime });
|
||
|
|
res.end(data);
|
||
|
|
} catch {
|
||
|
|
res.writeHead(404);
|
||
|
|
res.end('Not found');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function json(res, data, code = 200) {
|
||
|
|
res.writeHead(code, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||
|
|
res.end(JSON.stringify(data));
|
||
|
|
}
|
||
|
|
|
||
|
|
function readBody(req) {
|
||
|
|
return new Promise(resolve => {
|
||
|
|
let d = '';
|
||
|
|
req.on('data', c => d += c);
|
||
|
|
req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseUrl(req) {
|
||
|
|
return new URL(req.url, `http://localhost:${CONFIG.port}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getClientIp(req) {
|
||
|
|
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket?.remoteAddress || '0.0.0.0';
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- API Handlers ---
|
||
|
|
|
||
|
|
async function apiOrders(req, res) {
|
||
|
|
const url = parseUrl(req);
|
||
|
|
const parts = url.pathname.split('/');
|
||
|
|
const id = parts.length >= 4 ? parts[3] : null;
|
||
|
|
const ip = getClientIp(req);
|
||
|
|
// Special sub-paths - redirect to specific handlers
|
||
|
|
if (id === 'active') return apiActiveOrders(req, res);
|
||
|
|
if (id === 'archived') return apiArchivedOrders(req, res);
|
||
|
|
if (id === 'search') return apiSearchOrders(req, res);
|
||
|
|
if (id === 'pending') return apiPendingCount(req, res);
|
||
|
|
if (id === 'recent') return apiRecentOrders(req, res);
|
||
|
|
if (id === 'analyze') return apiAnalyze(req, res);
|
||
|
|
|
||
|
|
// GET
|
||
|
|
if (req.method === 'GET') {
|
||
|
|
if (id) {
|
||
|
|
const order = getOrder(id);
|
||
|
|
if (!order) return json(res, { error: '订单不存在' }, 404);
|
||
|
|
return json(res, order);
|
||
|
|
}
|
||
|
|
return json(res, getAllOrders());
|
||
|
|
}
|
||
|
|
|
||
|
|
// POST — 创建订单(含垃圾过滤)
|
||
|
|
if (req.method === 'POST') {
|
||
|
|
const body = await readBody(req);
|
||
|
|
if (!body.requirements || !body.contact) {
|
||
|
|
return json(res, { error: '需求和联系方式不能为空' }, 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 垃圾过滤三道关
|
||
|
|
const security = filterOrder(body, ip);
|
||
|
|
if (!security.allowed) {
|
||
|
|
return json(res, {
|
||
|
|
error: '提交被拒绝',
|
||
|
|
issues: security.issues,
|
||
|
|
flagged: true,
|
||
|
|
}, 429);
|
||
|
|
}
|
||
|
|
|
||
|
|
const order = createOrder({
|
||
|
|
name: body.name,
|
||
|
|
contact: body.contact,
|
||
|
|
contactType: body.contactType || '微信',
|
||
|
|
requirements: body.requirements,
|
||
|
|
budget: body.budget || '',
|
||
|
|
expectedDays: parseInt(body.expectedDays) || 0,
|
||
|
|
source: body.source || 'web',
|
||
|
|
});
|
||
|
|
|
||
|
|
// 如果被标记为可疑,自动标记
|
||
|
|
if (security.flagged) {
|
||
|
|
flagOrder(order.id, true, security.flagReasons?.join('; ') || '系统自动标记');
|
||
|
|
addComment(order.id, {
|
||
|
|
author: '系统', content: `⚠️ 此订单被自动标记为可疑: ${security.flagReasons?.join('、') || '内容异常'}。请审核。`, side: 'dev'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// AI 确认(异步)
|
||
|
|
try {
|
||
|
|
const msg = await generateConfirmation(order);
|
||
|
|
addComment(order.id, { author: '系统', content: msg, side: 'dev' });
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
return json(res, {
|
||
|
|
success: true,
|
||
|
|
order,
|
||
|
|
flagged: security.flagged,
|
||
|
|
flagReasons: security.flagged ? security.flagReasons : undefined,
|
||
|
|
}, 201);
|
||
|
|
}
|
||
|
|
|
||
|
|
// PUT
|
||
|
|
if (req.method === 'PUT') {
|
||
|
|
const body = await readBody(req);
|
||
|
|
let result;
|
||
|
|
if (body.action === 'status') result = updateStatus(id, body.status);
|
||
|
|
else if (body.action === 'subtask') result = addSubTask(id, body);
|
||
|
|
else if (body.action === 'sub-status') result = updateSubStatus(id, body.subId, body.status);
|
||
|
|
else if (body.action === 'comment') result = addComment(id, body);
|
||
|
|
else if (body.action === 'flag') result = flagOrder(id, body.flagged, body.reason);
|
||
|
|
else if (body.action === 'progress') result = updateProgress(id, body.progress, body.note);
|
||
|
|
else if (body.action === 'set-expected') result = setExpectedDays(id, body.days);
|
||
|
|
else if (body.action === 'deliver') result = deliverOrder(id);
|
||
|
|
else if (body.action === 'archive') result = archiveOrder(id);
|
||
|
|
if (!result) return json(res, { error: '操作失败' }, 400);
|
||
|
|
return json(res, { success: true, data: result });
|
||
|
|
}
|
||
|
|
|
||
|
|
// DELETE
|
||
|
|
if (req.method === 'DELETE') {
|
||
|
|
const ok = deleteOrder(id);
|
||
|
|
return json(res, { success: ok });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiSearchOrders(req, res) {
|
||
|
|
const q = parseUrl(req).searchParams.get('q') || '';
|
||
|
|
json(res, findOrder(q));
|
||
|
|
}
|
||
|
|
|
||
|
|
function apiPendingCount(req, res) {
|
||
|
|
json(res, { count: getPendingCount() });
|
||
|
|
}
|
||
|
|
|
||
|
|
function apiActiveOrders(req, res) {
|
||
|
|
json(res, getActiveOrders());
|
||
|
|
}
|
||
|
|
|
||
|
|
function apiArchivedOrders(req, res) {
|
||
|
|
json(res, getArchivedOrders());
|
||
|
|
}
|
||
|
|
|
||
|
|
function apiRecentOrders(req, res) {
|
||
|
|
json(res, getRecentOrders(120));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiAnalyze(req, res) {
|
||
|
|
const body = await readBody(req);
|
||
|
|
if (!body.text) return json(res, { error: '缺少需求文本' }, 400);
|
||
|
|
try {
|
||
|
|
const analysis = await analyzeRequirement(body.text);
|
||
|
|
json(res, analysis || { error: '分析失败' });
|
||
|
|
} catch (e) {
|
||
|
|
json(res, { error: e.message }, 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function apiHealth(req, res) {
|
||
|
|
json(res, {
|
||
|
|
status: 'ok',
|
||
|
|
site: CONFIG.siteName,
|
||
|
|
version: '1.0.0',
|
||
|
|
aiEnabled: !!CONFIG.deepseekKey && CONFIG.deepseekKey !== 'your-api-key-here',
|
||
|
|
docker: !!process.env.DOCKER,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Server
|
||
|
|
const server = http.createServer((req, res) => {
|
||
|
|
const pathname = parseUrl(req).pathname;
|
||
|
|
if (pathname.startsWith('/api/orders')) return apiOrders(req, res);
|
||
|
|
const handler = routes[pathname];
|
||
|
|
if (handler) handler(req, res);
|
||
|
|
else { res.writeHead(404); res.end('Not found'); }
|
||
|
|
});
|
||
|
|
|
||
|
|
server.listen(CONFIG.port, '0.0.0.0', () => {
|
||
|
|
console.log(`🧰 zhiqiu-order-system v1.0`);
|
||
|
|
console.log(` 📋 客户提交: http://0.0.0.0:${CONFIG.port}/submit`);
|
||
|
|
console.log(` 🔧 管理面板: http://0.0.0.0:${CONFIG.port}/admin`);
|
||
|
|
console.log(` 🔍 订单追踪: http://0.0.0.0:${CONFIG.port}/track`);
|
||
|
|
console.log(` 🤖 AI 辅助: ${CONFIG.deepseekKey && CONFIG.deepseekKey !== 'your-api-key-here' ? '已启用' : '未配置'}`);
|
||
|
|
});
|