diff --git a/modules/drama/index.html b/modules/drama/index.html new file mode 100644 index 0000000..ac6d03c --- /dev/null +++ b/modules/drama/index.html @@ -0,0 +1,141 @@ + + + + + +之秋 · AI 真人短剧工坊 + + + + +
+
+

🎬 之秋 · 真人短剧工坊

+

AI 帮你写剧本 · 三种创作模式

+
+ +
+
+ 🤖 + AI 全自动 +
给主题,出完整剧本
+
+
+ 💡 + 想法辅助 +
你提想法,AI 展开
+
+
+ + AI 润色 +
已有草稿,优化提升
+
+
+ +
+ +
+ + +
+
+ +
+ AI 正在创作中,请稍候… +
+ +
+
✍️ 生成完毕
+
+
+ +
生成内容由 AI 创作 · 仅供参考
+
+ + + + diff --git a/modules/drama/public/index.html b/modules/drama/public/index.html new file mode 100644 index 0000000..ac6d03c --- /dev/null +++ b/modules/drama/public/index.html @@ -0,0 +1,141 @@ + + + + + +之秋 · AI 真人短剧工坊 + + + + +
+
+

🎬 之秋 · 真人短剧工坊

+

AI 帮你写剧本 · 三种创作模式

+
+ +
+
+ 🤖 + AI 全自动 +
给主题,出完整剧本
+
+
+ 💡 + 想法辅助 +
你提想法,AI 展开
+
+
+ + AI 润色 +
已有草稿,优化提升
+
+
+ +
+ +
+ + +
+
+ +
+ AI 正在创作中,请稍候… +
+ +
+
✍️ 生成完毕
+
+
+ +
生成内容由 AI 创作 · 仅供参考
+
+ + + + diff --git a/modules/drama/server.js b/modules/drama/server.js new file mode 100644 index 0000000..53bc8fb --- /dev/null +++ b/modules/drama/server.js @@ -0,0 +1,121 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 3930; +const DEEPSEEK_KEY = 'sk-a9b69e9cd2dc4ca68d6aceaa84f22afb'; + +// Prompt 模板:三种模式 +function buildPrompt(mode, input) { + const base = `你是一位专业的真人短剧编剧。请用以下专业格式输出剧本: + +【人物表】 +列出主要角色及简介 + +第X幕 + +场景 X · 地点 · 时间 · 内外景 +[镜头提示] 动作/场景描述 +角色名:(语气)"对白" +角色名:动作描述 +[镜头切至] 下一个镜头 + +注意: +- 每场标注镜头语言(中景、特写、远景、跟拍等) +- 对白自然,符合人物性格 +- 动作描写简洁有力 +- 节奏紧凑,3-5分钟短剧体量`; + + switch (mode) { + case 'auto': + return `${base}\n\n用户提供的主题:「${input}」\n请根据这个主题创作一个完整的真人短剧剧本。`; + case 'assist': + return `${base}\n\n用户提供的想法:「${input}」\n请根据这个想法展开,创作一个完整的真人短剧剧本。`; + case 'polish': + return `${base}\n\n以下是用户提供的剧本草稿。请在不改变核心情节的前提下,优化对白、增强画面感、调整节奏:\n\n${input}`; + default: + return `${base}\n\n用户输入:「${input}」`; + } +} + +// 简易 HTML 注入 +function serveStatic(res, filePath) { + const fullPath = path.join(__dirname, 'public', filePath); + try { + const data = fs.readFileSync(fullPath, 'utf-8'); + const ext = path.extname(filePath); + const mime = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript' }[ext] || 'text/plain'; + res.writeHead(200, { 'Content-Type': mime }); + res.end(data); + } catch { + res.writeHead(404); + res.end('Not found'); + } +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`); + const pathname = url.pathname; + + // CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } + + // 静态文件 + if (pathname === '/' || pathname === '/index.html') return serveStatic(res, 'index.html'); + + // 生成剧本 API + if (pathname === '/api/generate' && req.method === 'POST') { + let body = ''; + req.on('data', c => body += c); + req.on('end', async () => { + try { + const { mode, input } = JSON.parse(body); + if (!input || input.trim().length < 2) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: '输入内容至少 2 个字符' })); + return; + } + + const prompt = buildPrompt(mode || 'auto', input); + + // 调 DeepSeek API + const apiRes = await fetch('https://api.deepseek.com/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${DEEPSEEK_KEY}` + }, + body: JSON.stringify({ + model: 'deepseek-chat', + messages: [ + { role: 'system', content: '你是一位专业的真人短剧编剧。输出格式严格遵循用户要求的剧本格式。' }, + { role: 'user', content: prompt } + ], + temperature: 0.8, + max_tokens: 4096 + }) + }); + + const data = await apiRes.json(); + const content = data.choices?.[0]?.message?.content || '生成失败,请重试'; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, content })); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: e.message })); + } + }); + return; + } + + res.writeHead(404); + res.end('Not found'); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`[drama] 启动成功 http://0.0.0.0:${PORT}`); +}); diff --git a/modules/orders/.dockerignore b/modules/orders/.dockerignore new file mode 100644 index 0000000..1ae4d68 --- /dev/null +++ b/modules/orders/.dockerignore @@ -0,0 +1,6 @@ +node_modules/ +data/ +server.log +.env +*.log +.DS_Store diff --git a/modules/orders/.gitignore b/modules/orders/.gitignore new file mode 100644 index 0000000..8e0f3a7 --- /dev/null +++ b/modules/orders/.gitignore @@ -0,0 +1,2 @@ +data/ +server.log diff --git a/modules/orders/Dockerfile b/modules/orders/Dockerfile new file mode 100644 index 0000000..c439ead --- /dev/null +++ b/modules/orders/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY . . + +RUN mkdir -p data && chmod 777 data + +ENV ZQ_PORT=3920 +ENV DEEPSEEK_API_KEY=your-api-key-here + +EXPOSE 3920 + +CMD ["node", "server.js"] diff --git a/modules/orders/README.md b/modules/orders/README.md new file mode 100644 index 0000000..5ac267b --- /dev/null +++ b/modules/orders/README.md @@ -0,0 +1,95 @@ +# zhiqiu-tools · 之秋工具箱 + +> 人格系统相关的小工具和小项目合集。 + +## AI 接单系统 + +智能技术开发接单管理工具,支持 AI 辅助需求录入、订单编号追踪、双向沟通同步。 + +**在线试用:** [guanghuice.com/orders/submit](https://guanghuice.com/orders/submit) + +### 功能 + +| 功能 | 说明 | +|------|------| +| 🤖 **AI 辅助录入** | 提交需求时 AI 自动分析并结构化,推荐分类和预估工时 | +| 🛡️ **垃圾过滤** | 蜜罐检测 + IP 频率限制 + 内容模式识别,自动标记可疑订单 | +| 📋 **编号系统** | `ZT-YYYYMMDD-NNN` 格式,子任务分层 `-XX`,精准追踪 | +| 🔧 **管理面板** | 订单列表、状态流转、子任务拆分、可疑审核、客户沟通 | +| 🔍 **客户追踪** | 根据订单号查询进度、查看沟通记录、留言 | +| 💬 **双向同步** | 客户和开发者通过评论双向沟通,时间线记录所有操作 | +| 🐳 **Docker 支持** | 一键部署,环境变量配置 | + +### 快速部署 + +#### 方式一:Docker(推荐) + +```bash +# 1. 克隆 +git clone https://gitea.com/zhizhideqiuqiu/zhiqiu-tools.git +cd zhiqiu-tools + +# 2. 配置环境变量 +export DEEPSEEK_API_KEY=sk-your-key-here +export ZQ_SITE_NAME="我的接单系统" + +# 3. 启动 +docker compose up -d + +# 4. 访问 http://localhost:3920 +``` + +#### 方式二:直接运行 + +```bash +# 需要 Node.js 18+ +git clone https://gitea.com/zhizhideqiuqiu/zhiqiu-tools.git +cd zhiqiu-tools + +# 配置 API Key(AI 功能需要) +export DEEPSEEK_API_KEY=sk-your-key-here + +# 启动 +node server.js +``` + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `DEEPSEEK_API_KEY` | - | DeepSeek API Key(AI 功能必需) | +| `ZQ_PORT` | `3920` | 服务端口 | +| `ZQ_SITE_NAME` | `之秋 · 技术开发接单` | 站点名称 | +| `ZQ_ADMIN_TOKEN` | `` | 管理面板简单鉴权(可选) | +| `ZQ_RATE_LIMIT` | `3` | 每 IP 每小时最多提交次数 | + +### 垃圾过滤机制 + +系统使用三重防护对抗垃圾提交: + +1. **蜜罐检测** — 表单中隐藏字段,正常用户不可见,机器人会自动填写 +2. **频率限制** — 同一 IP 每小时最多提交 3 次 +3. **内容分析** — 检测无意义字符、重复内容等异常模式 + +被标记的订单会在管理面板显示 ⚠️ 标识,管理员可以审核后解除标记或删除。 + +### 访问路径 + +| 页面 | 路径 | 说明 | +|------|------|------| +| 📝 客户提交 | `/submit` | 公开,任何人可提交需求 | +| 🔧 管理面板 | `/admin` | 管理员管理订单 | +| 🔍 订单追踪 | `/track` | 客户凭编号查询进度 | +| 💊 健康检查 | `/api/health` | 服务状态 | + +### 技术栈 + +- 后端:Node.js(原生 http,无框架依赖) +- 前端:原生 HTML/CSS/JS +- 存储:JSON 文件(无需数据库,开箱即用) +- AI:DeepSeek API +- 部署:Docker / 直接运行 + +### 许可证 + +MIT diff --git a/modules/orders/ai-intake.js b/modules/orders/ai-intake.js new file mode 100644 index 0000000..c51af27 --- /dev/null +++ b/modules/orders/ai-intake.js @@ -0,0 +1,79 @@ +/** + * ai-intake.js - AI 辅助需求录入 + * 使用 DeepSeek 分析用户提交的原始需求,结构化为标准表单 + */ +const DEEPSEEK_KEY = 'sk-a9b69e9cd2dc4ca68d6aceaa84f22afb'; + +async function callDeepSeek(messages, options = {}) { + const body = { + model: options.model || 'deepseek-chat', + messages, + temperature: options.temperature ?? 0.3, + max_tokens: options.max_tokens ?? 2000, + stream: false, + }; + const resp = await fetch('https://api.deepseek.com/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${DEEPSEEK_KEY}`, + }, + body: JSON.stringify(body), + }); + if (!resp.ok) throw new Error(`DeepSeek error ${resp.status}`); + const data = await resp.json(); + return data.choices[0].message.content; +} + +/** 用 AI 分析原始需求,提取结构化信息 */ +export async function analyzeRequirement(rawText) { + const prompt = `你是一个技术项目经理助理。请分析以下客户提交的原始需求,提取关键信息并以 JSON 格式输出。 + +原始需求: +${rawText} + +请提取: +1. projectType: 项目类型(网站/小程序/API/工具/其他) +2. summary: 一句话需求摘要(20字以内) +3. techStack: 可能需要的技术栈(数组,如 ["Node.js", "React"]) +4. complexity: 复杂度评估(简单/中等/复杂) +5. estimatedDays: 预估工时(天数,数字) +6. clarity: 需求清晰度(清晰/模糊/非常模糊) +7. suggestions: 建议用户补充的信息(数组,如 ["需要明确前端框架", "需要数据库类型"]) + +只输出 JSON,不要其他文字。格式: +{ + "projectType": "", + "summary": "", + "techStack": [], + "complexity": "", + "estimatedDays": 0, + "clarity": "", + "suggestions": [] +}`; + + try { + const result = await callDeepSeek([{ role: 'user', content: prompt }], { temperature: 0.2 }); + return JSON.parse(result.replace(/```json\s*|\s*```/g, '')); + } catch (e) { + console.error('AI analyze failed:', e.message); + return null; + } +} + +/** AI 自动回复确认(提交后给客户看到的信息) */ +export async function generateConfirmation(order) { + const prompt = `你是一个温和友好的客服,请根据以下订单信息生成一段确认回复,告诉客户订单已收到并给出预期。 + +客户: ${order.name} +需求: ${order.requirements} +订单编号: ${order.id} + +回复要求:简短、温暖、专业。告知订单编号已生成,开发者会尽快联系。`; + + try { + return await callDeepSeek([{ role: 'user', content: prompt }], { temperature: 0.6, max_tokens: 500 }); + } catch { + return `感谢您的提交!您的订单编号为 ${order.id},我们会尽快与您联系。`; + } +} diff --git a/modules/orders/config.js b/modules/orders/config.js new file mode 100644 index 0000000..0763c99 --- /dev/null +++ b/modules/orders/config.js @@ -0,0 +1,22 @@ +/** + * config.js - 配置中心 + * 所有可通过环境变量覆盖的配置 + */ +export const CONFIG = { + port: parseInt(process.env.ZQ_PORT || '3920'), + deepseekKey: process.env.DEEPSEEK_API_KEY || 'sk-a9b69e9cd2dc4ca68d6aceaa84f22afb', + adminToken: process.env.ZQ_ADMIN_TOKEN || '', // 可选,管理面板简单鉴权 + rateLimit: { + maxPerIp: parseInt(process.env.ZQ_RATE_LIMIT || '3'), + windowMinutes: 60, + }, + siteName: process.env.ZQ_SITE_NAME || '之秋 · 技术开发接单', +}; + +export function validateConfig() { + const warnings = []; + if (!CONFIG.deepseekKey || CONFIG.deepseekKey === 'your-api-key-here') { + warnings.push('⚠️ DEEPSEEK_API_KEY 未设置,AI 辅助录入功能将不可用'); + } + return warnings; +} diff --git a/modules/orders/docker-compose.yml b/modules/orders/docker-compose.yml new file mode 100644 index 0000000..9384a65 --- /dev/null +++ b/modules/orders/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' +services: + zhiqiu-orders: + build: . + container_name: zhiqiu-orders + ports: + - "3920:3920" + environment: + - ZQ_PORT=3920 + - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-your-api-key-here} + - ZQ_SITE_NAME=${ZQ_SITE_NAME:-之秋 · 技术开发接单} + - ZQ_ADMIN_TOKEN=${ZQ_ADMIN_TOKEN:-} + volumes: + - ./data:/app/data + restart: unless-stopped diff --git a/modules/orders/order-manager.js b/modules/orders/order-manager.js new file mode 100644 index 0000000..2aef4d7 --- /dev/null +++ b/modules/orders/order-manager.js @@ -0,0 +1,269 @@ +/** + * order-manager.js - 订单核心管理模块 + * 编号: ZT-YYYYMMDD-NNN, 子任务: -XX + * 状态: submitted → reviewing → accepted(开发中) → testing → completed → delivered + * 归档: 已完成/已交付订单可移入历史 + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DATA_FILE = path.join(__dirname, 'data', 'orders.json'); +const COUNTER_FILE = path.join(__dirname, 'data', 'counter.json'); +const HISTORY_FILE = path.join(__dirname, 'data', 'history.json'); + +fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true }); + +function loadOrders() { + try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8')); } + catch { return []; } +} +function saveOrders(orders) { + fs.writeFileSync(DATA_FILE, JSON.stringify(orders, null, 2)); +} +function loadCounter() { + try { return JSON.parse(fs.readFileSync(COUNTER_FILE, 'utf-8')); } + catch { return {}; } +} +function saveCounter(c) { + fs.writeFileSync(COUNTER_FILE, JSON.stringify(c, null, 2)); +} +function loadHistory() { + try { return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8')); } + catch { return []; } +} +function saveHistory(h) { + fs.writeFileSync(HISTORY_FILE, JSON.stringify(h, null, 2)); +} + +/** 生成编号 */ +export function nextOrderNumber() { + const counter = loadCounter(); + const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + counter[today] = (counter[today] || 0) + 1; + saveCounter(counter); + return `ZT-${today}-${String(counter[today]).padStart(3, '0')}`; +} + +/** 生成子编号 */ +export function nextSubNumber(parentId) { + const orders = loadOrders(); + const o = orders.find(x => x.id === parentId); + if (!o) return null; + return `${parentId}-${String((o.subs || []).length + 1).padStart(2, '0')}`; +} + +/** 创建订单 */ +export function createOrder({ name, contact, contactType, requirements, budget, expectedDays, source }) { + const now = new Date().toISOString(); + const id = nextOrderNumber(); + const order = { + id, name: name || '匿名客户', contact, contactType: contactType || '微信', + requirements: requirements || '', budget: budget || '', + expectedDays: expectedDays || 0, + expectedDelivery: '', + progress: 0, progressNote: '等待接单', + source: source || 'web', + status: 'submitted', subs: [], comments: [], + timeline: [{ at: now, action: 'created', detail: '订单已提交' }], + flagged: false, flagReason: '', + archived: false, + createdAt: now, updatedAt: now, + }; + const orders = loadOrders(); + orders.push(order); + saveOrders(orders); + return order; +} + +/** 获取未归档订单 */ +export function getActiveOrders() { + return loadOrders().filter(o => !o.archived).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); +} + +/** 获取已归档订单(历史) */ +export function getArchivedOrders() { + return loadHistory().sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); +} + +/** 获取全部(含归档) */ +export function getAllOrders() { + return [...loadOrders(), ...loadHistory()].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); +} + +/** 搜索 */ +export function findOrder(query) { + const all = getAllOrders(); + return all.filter(o => + o.id.toLowerCase().includes(query.toLowerCase()) || + (o.contact || '').includes(query) || + (o.name || '').includes(query) + ); +} + +/** 按 ID 获取(查活跃+历史) */ +export function getOrder(id) { + return loadOrders().find(o => o.id === id) || loadHistory().find(o => o.id === id) || null; +} + +/** 更新状态 */ +export function updateStatus(id, status) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const valid = ['submitted', 'reviewing', 'accepted', 'in-progress', 'testing', 'completed', 'delivered']; + if (!valid.includes(status)) return null; + const now = new Date().toISOString(); + order.status = status; + order.updatedAt = now; + const labels = { submitted:'已提交', reviewing:'审核中', accepted:'已接单', 'in-progress':'制作中', testing:'测试中', completed:'已完成', delivered:'已交付' }; + order.timeline.push({ at: now, action: 'status', detail: `状态 → ${labels[status] || status}` }); + // 接单时算预计交付日 + if (status === 'accepted' && order.expectedDays > 0) { + const d = new Date(); d.setDate(d.getDate() + order.expectedDays); + order.expectedDelivery = d.toISOString().slice(0, 10); + order.progressNote = `预计 ${order.expectedDays} 天内交付`; + } + saveOrders(orders); + return order; +} + +/** 更新进度百分比 + 说明 */ +export function updateProgress(id, progress, note = '') { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const now = new Date().toISOString(); + order.progress = Math.max(0, Math.min(100, progress)); + order.progressNote = note || order.progressNote; + order.updatedAt = now; + order.timeline.push({ at: now, action: 'progress', detail: `进度更新: ${order.progress}% - ${order.progressNote}` }); + saveOrders(orders); + return order; +} + +/** 设定预期天数(接单时) */ +export function setExpectedDays(id, days) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + order.expectedDays = days; + const d = new Date(); d.setDate(d.getDate() + days); + order.expectedDelivery = d.toISOString().slice(0, 10); + order.progressNote = `预计 ${days} 天内交付`; + order.updatedAt = new Date().toISOString(); + order.timeline.push({ at: order.updatedAt, action: 'estimate', detail: `设定预期 ${days} 天交付` }); + saveOrders(orders); + return order; +} + +/** 交付(标记为已交付) */ +export function deliverOrder(id) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const now = new Date().toISOString(); + order.status = 'delivered'; + order.progress = 100; + order.progressNote = '已完成并交付'; + order.updatedAt = now; + order.timeline.push({ at: now, action: 'deliver', detail: '订单已交付给客户' }); + saveOrders(orders); + return order; +} + +/** 归档订单(移入历史) */ +export function archiveOrder(id) { + const orders = loadOrders(); + const idx = orders.findIndex(o => o.id === id); + if (idx === -1) return null; + const order = orders[idx]; + order.archived = true; + order.updatedAt = new Date().toISOString(); + order.timeline.push({ at: order.updatedAt, action: 'archive', detail: '订单已归档' }); + const history = loadHistory(); + history.push(order); + saveHistory(history); + orders.splice(idx, 1); + saveOrders(orders); + return order; +} + +/** 子任务管理 */ +export function addSubTask(id, { title, description }) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const subId = nextSubNumber(id); + const now = new Date().toISOString(); + const sub = { subId, title: title || '', description: description || '', status: 'pending', createdAt: now }; + order.subs = order.subs || []; + order.subs.push(sub); + order.updatedAt = now; + order.timeline.push({ at: now, action: 'subtask', detail: `添加子任务: ${subId} - ${title}` }); + saveOrders(orders); + return sub; +} +export function updateSubStatus(id, subId, status) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const sub = order.subs?.find(s => s.subId === subId); + if (!sub) return null; + sub.status = status; + sub.updatedAt = new Date().toISOString(); + order.updatedAt = sub.updatedAt; + order.timeline.push({ at: sub.updatedAt, action: 'subtask', detail: `子任务 ${subId} → ${status}` }); + saveOrders(orders); + return sub; +} + +/** 评论 */ +export function addComment(id, { author, content, side }) { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + const now = new Date().toISOString(); + const comment = { id: `c-${Date.now()}`, author: author || '客户', content: content || '', side: side || 'client', createdAt: now }; + order.comments = order.comments || []; + order.comments.push(comment); + order.updatedAt = now; + order.timeline.push({ at: now, action: 'comment', detail: `${side === 'dev' ? '开发者' : '客户'}留言` }); + saveOrders(orders); + return comment; +} + +/** 标记可疑 */ +export function flagOrder(id, flagged, reason = '') { + const orders = loadOrders(); + const order = orders.find(o => o.id === id); + if (!order) return null; + order.flagged = flagged; + order.flagReason = reason; + order.updatedAt = new Date().toISOString(); + order.timeline.push({ at: order.updatedAt, action: 'flag', detail: flagged ? `标记可疑: ${reason}` : '解除可疑标记' }); + saveOrders(orders); + return order; +} + +/** 统计 */ +export function getFlaggedOrders() { return loadOrders().filter(o => o.flagged); } +export function getRecentOrders(min = 60) { + const cutoff = Date.now() - min * 60000; + return loadOrders().filter(o => new Date(o.createdAt).getTime() > cutoff); +} +export function getPendingCount() { + return loadOrders().filter(o => !o.archived && ['submitted', 'reviewing'].includes(o.status)).length; +} + +/** 删除订单 */ +export function deleteOrder(id) { + let orders = loadOrders(); + const idx = orders.findIndex(o => o.id === id); + if (idx !== -1) { orders.splice(idx, 1); saveOrders(orders); return true; } + let history = loadHistory(); + const hidx = history.findIndex(o => o.id === id); + if (hidx !== -1) { history.splice(hidx, 1); saveHistory(history); return true; } + return false; +} diff --git a/modules/orders/package.json b/modules/orders/package.json new file mode 100644 index 0000000..154a624 --- /dev/null +++ b/modules/orders/package.json @@ -0,0 +1,12 @@ +{ + "name": "zhiqiu-order-system", + "version": "1.0.0", + "description": "AI 接单系统 - 客户提交需求、管理员管理订单、编号追踪双向同步", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js" + }, + "author": "zhizhideqiuqiu", + "license": "MIT" +} diff --git a/modules/orders/public/admin.html b/modules/orders/public/admin.html new file mode 100644 index 0000000..688c166 --- /dev/null +++ b/modules/orders/public/admin.html @@ -0,0 +1,296 @@ + + +__SITE_NAME__ · 管理 + + +
+ + +
+
0
当前订单
+
0
待处理
+
0
制作中
+
0
历史
+
+ +
+ + +
+ +
+ + + +
+ +
+
+ +
+
+
+ + + diff --git a/modules/orders/public/style.css b/modules/orders/public/style.css new file mode 100644 index 0000000..19d3d9f --- /dev/null +++ b/modules/orders/public/style.css @@ -0,0 +1,94 @@ +/** + * zhiqiu-order-system 公共样式 + */ +body { margin:0;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;background:#f5f3f0;color:#2c2c2a; } +.container { max-width:960px;margin:0 auto;padding:20px; } + +/* 导航 */ +.nav { display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap; } +.nav a { text-decoration:none;padding:8px 18px;border-radius:8px;font-size:13px;color:#5f5e5a;background:#fff;border:0.5px solid #d3d1c7; } +.nav a:hover { border-color:#534ab7;color:#3c3489; } +.nav a.active { background:#534ab7;color:#fff;border-color:#534ab7; } + +/* 卡片 */ +.card { background:#fff;border-radius:12px;padding:20px;border:0.5px solid #d3d1c7;margin-bottom:16px; } +.card h2 { margin:0 0 12px;font-size:16px;font-weight:500; } +.card h3 { margin:0 0 8px;font-size:14px;font-weight:500;color:#3c3489; } + +/* 表单 */ +label { display:block;font-size:13px;font-weight:500;margin-top:12px;margin-bottom:4px;color:#3c3489; } +input,select,textarea { width:100%;padding:10px;border:1px solid #d3d1c7;border-radius:8px;font-size:13px;font-family:inherit; } +input:focus,textarea:focus { outline:none;border-color:#534ab7; } +textarea { resize:vertical;min-height:80px; } +button { margin-top:12px;padding:10px 20px;background:#534ab7;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer; } +button:hover { background:#3c3489; } +button.secondary { background:#888780; } +button.secondary:hover { background:#5f5e5a; } +button.small { padding:6px 12px;font-size:12px;margin-top:4px; } +button.danger { background:#e24b4a; } +button.danger:hover { background:#a32d2d; } + +/* 表格 / 订单卡片 */ +.order-card { border:0.5px solid #d3d1c7;border-radius:8px;padding:14px;margin-bottom:10px;cursor:pointer;transition:box-shadow .15s; } +.order-card:hover { box-shadow:0 2px 8px rgba(0,0,0,.06); } +.order-card .oh { display:flex;justify-content:space-between;align-items:center; } +.order-card .id { font-weight:500;color:#534ab7;font-size:13px;font-family:monospace; } +.order-card .status { display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px; } +.status-submitted { background:#faeeda;color:#854f0b; } +.status-reviewing { background:#e6f1fb;color:#185fa5; } +.status-accepted { background:#eaf3de;color:#3b6d11; } +.status-in-progress { background:#e1f5ee;color:#0f6e56; } +.status-testing { background:#fbeaf0;color:#993556; } +.status-completed { background:#d3d1c7;color:#2c2c2a; } +.status-delivered { background:#cecbf6;color:#3c3489; } + +/* 标签 */ +.tag { display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;background:#f1efe8;color:#5f5e5a;margin:2px; } + +/* 评论 */ +.comment { padding:10px;margin:6px 0;border-radius:8px;font-size:13px; } +.comment.dev { background:#e6f1fb;margin-left:20px; } +.comment.client { background:#f1efe8;margin-right:20px; } +.comment .author { font-size:11px;color:#888780; } +.comment .time { font-size:11px;color:#b4b2a9;float:right; } + +/* 状态条 */ +.status-bar { display:flex;gap:4px;margin:12px 0;flex-wrap:wrap; } +.status-dot { width:14px;height:14px;border-radius:50%; } +.status-dot.done { background:#534ab7; } +.status-dot.active { background:#1d9e75;box-shadow:0 0 0 2px #9fe1cb; } +.status-dot.pending { background:#d3d1c7; } + +/* 加载 */ +.loading { text-align:center;padding:40px;color:#888780; } + +/* 消息 */ +.msg { padding:10px;border-radius:8px;font-size:13px;margin:8px 0; } +.msg.success { background:#eaf3de;color:#3b6d11; } +.msg.error { background:#fcebeb;color:#a32d2d; } +.msg.info { background:#e6f1fb;color:#185fa5; } + +/* 子任务列表 */ +.sub-list { margin:4px 0;padding-left:16px; } +.sub-item { font-size:13px;padding:4px 0;display:flex;gap:8px;align-items:center; } +.sub-item .sid { font-family:monospace;font-size:11px;color:#888780; } +.sub-status { font-size:11px;padding:1px 6px;border-radius:4px; } +.sub-pending { background:#f1efe8;color:#5f5e5a; } +.sub-done { background:#eaf3de;color:#3b6d11; } + +/* 表单辅助 */ +.hint { font-size:12px;color:#888780;margin-top:4px; } +.flex { display:flex;gap:8px; } +.flex-grow { flex:1; } + +/* 仪表盘 */ +.stats { display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px; } +.stat-card { background:#fff;border-radius:8px;padding:16px;text-align:center;border:0.5px solid #d3d1c7; } +.stat-card .num { font-size:28px;font-weight:500;color:#534ab7; } +.stat-card .label { font-size:12px;color:#888780;margin-top:4px; } + +@media(max-width:640px) { + .container { padding:12px; } + .order-card .oh { flex-direction:column;align-items:flex-start;gap:4px; } + .stats { grid-template-columns:repeat(2,1fr); } +} diff --git a/modules/orders/public/submit.html b/modules/orders/public/submit.html new file mode 100644 index 0000000..91dfbcf --- /dev/null +++ b/modules/orders/public/submit.html @@ -0,0 +1,150 @@ + + +__SITE_NAME__ + + +
+ + +
+
+

之秋 · 技术开发接单

+

提交你的需求,AI 辅助梳理后推送给开发者

+ 🤖 由 AI 辅助录入 +
+ +
+

你的需求

+ + + +
+ + + + + +
+ + +
+ + + + + + + + +
+ + +
+ + +
+
+
+ +
+
+ + + diff --git a/modules/orders/public/track.html b/modules/orders/public/track.html new file mode 100644 index 0000000..6e80161 --- /dev/null +++ b/modules/orders/public/track.html @@ -0,0 +1,172 @@ + + +__SITE_NAME__ · 我的订单 + + +
+ + + +
+
+

🔍 我的订单

+

输入订单编号查看进度

+
+
+

查询订单

+
+ + +
+

提交需求后会自动获得订单编号

+
+
+ + + +
+ + + diff --git a/modules/orders/server.js b/modules/orders/server.js new file mode 100644 index 0000000..a891d83 --- /dev/null +++ b/modules/orders/server.js @@ -0,0 +1,234 @@ +/** + * 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' ? '已启用' : '未配置'}`); +}); diff --git a/modules/orders/spam-filter.js b/modules/orders/spam-filter.js new file mode 100644 index 0000000..dddd041 --- /dev/null +++ b/modules/orders/spam-filter.js @@ -0,0 +1,131 @@ +/** + * spam-filter.js - 垃圾订单过滤模块 + * 三道防线:频率限制 → 蜜罐检测 → 内容分析 + */ +const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1小时 +const MAX_ORDERS_PER_IP = 3; // 每 IP 每小时最多 3 单 +const MAX_ORDERS_PER_CONTACT = 2; // 同一联系方式最多 2 单未完成 + +// 内存中的速率限制表 +const ipCounter = new Map(); +const suspiciousPatterns = [ + /[a-z]{20,}/i, // 超长无意义字母 + /(.)\1{10,}/, // 重复字符超过10次 + /[\u4e00-\u9fff]{50,}/, // 超长无标点中文 + /http[s]?:\/\/(?!.*订单|.*需求|.*开发)/i, // 非常规链接 +]; + +/** 清理过期 IP 记录 */ +function cleanExpired() { + const now = Date.now(); + for (const [ip, data] of ipCounter) { + if (now - data.windowStart > RATE_LIMIT_WINDOW) ipCounter.delete(ip); + } +} + +/** 检查 IP 频率限制 */ +export function checkRateLimit(ip) { + cleanExpired(); + const now = Date.now(); + let data = ipCounter.get(ip); + if (!data || now - data.windowStart > RATE_LIMIT_WINDOW) { + data = { windowStart: now, count: 0 }; + ipCounter.set(ip, data); + } + data.count++; + return { + allowed: data.count <= MAX_ORDERS_PER_IP, + remaining: Math.max(0, MAX_ORDERS_PER_IP - data.count), + total: data.count, + }; +} + +/** 检查蜜罐字段(隐藏字段,机器人会填写) */ +export function checkHoneypot(body) { + // website / callback_url 是蜜罐字段,正常用户看不到 + if (body.website || body.callback_url) { + return { flagged: true, reason: '蜜罐字段被触发(疑似机器人)' }; + } + // 如果要求必填的联系方式为空但蜜罐填了 + if (!body.contact && (body.website || body.callback_url)) { + return { flagged: true, reason: '机器人提交' }; + } + return { flagged: false }; +} + +/** 检查内容是否可疑 */ +export function checkContent(body) { + const text = `${body.requirements || ''} ${body.name || ''} ${body.contact || ''}`; + const reasons = []; + + // 模式匹配 + for (const pattern of suspiciousPatterns) { + if (pattern.test(text)) { + reasons.push('内容模式异常'); + break; + } + } + + // 联系方式和需求内容完全没有关联 + const contact = (body.contact || '').trim(); + const req = (body.requirements || '').trim(); + if (contact && req && req.length < 5) { + reasons.push('需求描述过短'); + } + + // 同一联系方式重复提交 + if (body.contact) { + // 这个在 order-manager 里检查 + } + + return { + flagged: reasons.length > 0, + reasons, + }; +} + +/** 三重检查:完整过滤入口 */ +export function filterOrder(body, ip) { + const issues = []; + + // 1. 蜜罐检测 + const hp = checkHoneypot(body); + if (hp.flagged) issues.push(hp.reason); + + // 2. 频率限制 + const rl = checkRateLimit(ip); + if (!rl.allowed) issues.push(`提交过于频繁(本小时第 ${rl.total} 次,上限 ${MAX_ORDERS_PER_IP})`); + if (rl.total >= MAX_ORDERS_PER_IP) { + return { + allowed: false, + issues: [`提交次数已达上限(${MAX_ORDERS_PER_IP}次/小时),请稍后再试`], + flagged: true, + rateInfo: rl, + }; + } + + // 3. 内容检查 + const cc = checkContent(body); + if (cc.flagged) issues.push(...cc.reasons); + + return { + allowed: true, + issues, + flagged: issues.length > 0, + flagReasons: issues, + }; +} + +/** 获取速率限制状态 */ +export function getRateStatus(ip) { + cleanExpired(); + const data = ipCounter.get(ip); + if (!data) return { remaining: MAX_ORDERS_PER_IP, total: 0 }; + const elapsed = Date.now() - data.windowStart; + const remaining = Math.max(0, MAX_ORDERS_PER_IP - data.count); + return { + remaining, + total: data.count, + resetIn: Math.max(0, Math.ceil((RATE_LIMIT_WINDOW - elapsed) / 1000)), + }; +} diff --git a/tools/bili_drama_collector.py b/tools/bili_drama_collector.py new file mode 100644 index 0000000..51c6955 --- /dev/null +++ b/tools/bili_drama_collector.py @@ -0,0 +1,102 @@ +""" +B站短剧采集器 · 公开API版 +不需要登录,直接搜短剧数据 +""" + +import requests, json, time, os +from datetime import datetime + +DATA_DIR = '/opt/zhiqiu-tools/data/drama' +os.makedirs(DATA_DIR, exist_ok=True) + +HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + 'Referer': 'https://www.bilibili.com/' +} + +def search_bilibili(keyword, page=1): + """搜索B站视频""" + url = 'https://api.bilibili.com/x/web-interface/search/type' + params = {'search_type': 'video', 'keyword': keyword, 'page': page} + r = requests.get(url, params=params, headers=HEADERS, timeout=15) + data = r.json() + if data.get('code') != 0: + return [] + return data.get('data', {}).get('result', []) + +def format_video(v): + """提取关键字段""" + title = v.get('title', '').replace('', '').replace('', '') + return { + 'title': title, + 'play': v.get('play', 0), + 'author': v.get('author', ''), + 'duration': v.get('duration', ''), + 'bvid': v.get('bvid', ''), + 'aid': v.get('aid', ''), + 'tag': v.get('tag', ''), + 'description': v.get('description', '')[:200], + 'pic': v.get('pic', ''), + 'source': 'bilibili', + 'crawl_time': datetime.now().isoformat() + } + +def collect(keywords, max_pages=3): + """采集主函数""" + all_results = [] + seen = set() + + for kw in keywords: + print(f'\n🔍 搜索: {kw}') + for page in range(1, max_pages + 1): + try: + results = search_bilibili(kw, page) + except: + print(f' 第{page}页请求失败,跳过') + break + if not results: + print(f' 第{page}页无数据') + break + count = 0 + for v in results: + vid = v.get('bvid') or v.get('aid') + if vid and vid not in seen: + seen.add(vid) + item = format_video(v) + all_results.append(item) + count += 1 + print(f' 第{page}页: +{count}条 (累计{len(all_results)})') + time.sleep(0.5) + + # 定期保存中间结果(每轮关键词后) + def save_interim(data, tag): + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + path = os.path.join(DATA_DIR, f'bili_drama_{tag}_{ts}.json') + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return path + + save_interim(all_results, 'interim') + + # 按播放量排序 + + # 存文件 + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + path = os.path.join(DATA_DIR, f'bili_drama_{ts}.json') + with open(path, 'w', encoding='utf-8') as f: + json.dump(all_results, f, ensure_ascii=False, indent=2) + + print(f'\n✅ 采集完成: {len(all_results)}条') + print(f'📁 已保存: {path}') + + # 打印TOP5 + print('\n🏆 播放量TOP5:') + for i, item in enumerate(all_results[:5], 1): + print(f' {i}. [{item["play"]}播放] {item["title"][:50]}') + print(f' UP: {item["author"]} | {item["duration"]}') + + return all_results + +if __name__ == '__main__': + keywords = ['短剧', '短剧推荐', 'ai短剧', '真人短剧', '爆款短剧'] + collect(keywords, max_pages=2)