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 创作 · 仅供参考
+
+
+
+
+
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 创作 · 仅供参考
+
+
+
+
+
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__ · 管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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__
+
+
+
+
+
+
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)