2026-08-27 12:33:57 +08:00
// MCN工作台 - 本地 Web 服务(零依赖 Node 实现)
// 启动: node server.js → http://localhost:8899
// 功能: 技能导航 + 产出内容浏览(默认读桌面 MCNSkill项目)+ dsh 数据功能页(账号/视频/脚本/复盘/周榜,只读)
'use strict' ;
const http = require ( 'http' );
const fs = require ( 'fs' );
const path = require ( 'path' );
const os = require ( 'os' );
const { URL } = require ( 'url' );
const dsh = require ( './dsh-data' );
const PORT_BASE = 8899 ;
const ROOT = __dirname ;
const PUBLIC_DIR = path . join ( ROOT , 'public' );
const CONFIG_FILE = path . join ( ROOT , 'config.json' );
// ---- 默认根目录:桌面/MCNSkill项目 ----
const DEFAULT_ROOT = path . join ( os . homedir (), 'Desktop' , 'MCNSkill项目' );
// ---- 配置读写(自定义根目录列表)----
function loadConfig () {
try {
if ( fs . existsSync ( CONFIG_FILE )) {
const c = JSON . parse ( fs . readFileSync ( CONFIG_FILE , 'utf8' ));
if ( Array . isArray ( c . extraRoots )) return { extraRoots : c . extraRoots };
}
} catch ( e ) { /* 忽略损坏配置 */ }
return { extraRoots : [] };
}
function saveConfig ( cfg ) {
fs . writeFileSync ( CONFIG_FILE , JSON . stringify ( cfg , null , 2 ), 'utf8' );
}
// ---- 允许读取的根目录白名单 ----
function allowedRoots () {
const cfg = loadConfig ();
const list = [ DEFAULT_ROOT ]. concat ( cfg . extraRoots || []);
// 去重、只保留存在的目录
return list . filter (( p , i ) => p && fs . existsSync ( p ) && fs . statSync ( p ). isDirectory () && list . indexOf ( p ) === i );
}
// ---- 路径安全:必须落在某个允许根目录内 ----
function resolveSafe ( rawPath ) {
if ( ! rawPath ) return null ;
let p ;
try { p = path . resolve ( rawPath ); } catch ( e ) { return null ; }
for ( const root of allowedRoots ()) {
const r = path . resolve ( root );
if ( p === r || p . startsWith ( r + path . sep )) {
return { abs : p , root : r };
}
}
return null ;
}
2026-08-31 15:27:54 +08:00
// ---- 查找账号设定卡 SVG ----
// 产出规范:{产出根}/{达人昵称}/{达人昵称}账号设定卡.svg(功能六产出,与账号设定.md 同目录)
// 查找顺序:规范路径 → 根下直挂 → 账号目录内扫描(防目录名/账号名不一致)
function findAccountCard ( name ) {
const safeName = String ( name || '' ). replace ( /[\\/:*?"<>|]/g , '_' ). trim ();
if ( ! safeName ) return null ;
for ( const root of allowedRoots ()) {
const p1 = path . join ( root , safeName , safeName + '账号设定卡.svg' );
const p2 = path . join ( root , safeName + '账号设定卡.svg' );
for ( const c of [ p1 , p2 ]) {
try { if ( fs . existsSync ( c ) && fs . statSync ( c ). isFile ()) return c ; } catch ( e ) { /* 忽略 */ }
}
}
for ( const root of allowedRoots ()) {
const dir = path . join ( root , safeName );
try {
if ( fs . statSync ( dir ). isDirectory ()) {
const hit = fs . readdirSync ( dir ). find (( f ) => f . toLowerCase (). endsWith ( '.svg' ) && f . includes ( '账号设定卡' ));
if ( hit ) return path . join ( dir , hit );
}
} catch ( e ) { /* 忽略 */ }
}
return null ;
}
2026-08-27 12:33:57 +08:00
// ---- 目录树(递归,跳过隐藏项)----
function buildTree ( absPath , depth ) {
if ( depth > 4 ) return null ;
let entries ;
try { entries = fs . readdirSync ( absPath , { withFileTypes : true }); } catch ( e ) { return null ; }
const dirs = [];
const files = [];
for ( const ent of entries ) {
if ( ent . name . startsWith ( '.' )) continue ; // 隐藏文件
if ( ent . name === 'node_modules' || ent . name === '.git' ) continue ;
const full = path . join ( absPath , ent . name );
try {
if ( ent . isDirectory ()) {
const child = buildTree ( full , depth + 1 );
if ( child ) dirs . push ({ name : ent . name , path : full , type : 'dir' , children : child });
} else if ( ent . isFile ()) {
const st = fs . statSync ( full );
files . push ({ name : ent . name , path : full , type : 'file' , ext : path . extname ( ent . name ). toLowerCase (), size : st . size });
}
} catch ( e ) { /* 权限/占用跳过 */ }
}
dirs . sort (( a , b ) => a . name . localeCompare ( b . name , 'zh' ));
files . sort (( a , b ) => a . name . localeCompare ( b . name , 'zh' ));
return dirs . concat ( files );
}
// ---- MIME ----
const MIME = {
'.html' : 'text/html; charset=utf-8' ,
'.js' : 'text/javascript; charset=utf-8' ,
'.css' : 'text/css; charset=utf-8' ,
'.json' : 'application/json; charset=utf-8' ,
'.svg' : 'image/svg+xml' ,
'.png' : 'image/png' ,
'.jpg' : 'image/jpeg' ,
'.jpeg' : 'image/jpeg' ,
'.gif' : 'image/gif' ,
'.ico' : 'image/x-icon' ,
'.woff2' : 'font/woff2' ,
};
function mimeOf ( p ) {
return MIME [ path . extname ( p ). toLowerCase ()] || 'application/octet-stream' ;
}
// ---- 读取文本文件(按扩展名判定预览能力)----
const PREVIEW_EXTS = [ '.md' , '.markdown' , '.txt' , '.json' , '.csv' , '.log' ];
function readText ( absPath ) {
const ext = path . extname ( absPath ). toLowerCase ();
if ( ! PREVIEW_EXTS . includes ( ext )) return null ;
const buf = fs . readFileSync ( absPath );
// BOM 去除
let text = buf . toString ( 'utf8' );
if ( text . charCodeAt ( 0 ) === 0xFEFF ) text = text . slice ( 1 );
return text ;
}
// ---- 简易日志 ----
function log ( msg ) {
console . log ( '[' + new Date (). toLocaleTimeString ( 'zh-CN' , { hour12 : false }) + '] ' + msg );
}
// ---- 静态文件 / API ----
const server = http . createServer (( req , res ) => {
const u = new URL ( req . url , 'http://localhost' );
const p = decodeURIComponent ( u . pathname );
const q = u . searchParams ;
// 统一 JSON 响应
const sendJSON = ( code , obj ) => {
const body = JSON . stringify ( obj );
res . writeHead ( code , { 'Content-Type' : 'application/json; charset=utf-8' , 'Cache-Control' : 'no-store' });
res . end ( body );
};
const sendErr = ( code , msg ) => sendJSON ( code , { error : msg });
// ---------- API ----------
if ( p === '/api/roots' ) {
return sendJSON ( 200 , {
defaultRoot : DEFAULT_ROOT ,
extraRoots : loadConfig (). extraRoots ,
defaultExists : fs . existsSync ( DEFAULT_ROOT ),
});
}
if ( p === '/api/roots' && req . method === 'POST' ) {
let body = '' ;
req . on ( 'data' , ( c ) => { body += c ; if ( body . length > 1e6 ) req . destroy (); });
req . on ( 'end' , () => {
try {
const { extraRoots } = JSON . parse ( body || '{}' );
if ( ! Array . isArray ( extraRoots )) return sendErr ( 400 , 'extraRoots 必须为数组' );
const cleaned = extraRoots
. map (( x ) => ( typeof x === 'string' ? x . trim () : '' ))
. filter (( x ) => x && fs . existsSync ( x ) && fs . statSync ( x ). isDirectory ());
saveConfig ({ extraRoots : cleaned });
sendJSON ( 200 , { extraRoots : cleaned });
} catch ( e ) {
sendErr ( 500 , '配置保存失败: ' + e . message );
}
});
return ;
}
if ( p === '/api/tree' ) {
const rootParam = q . get ( 'root' ) || DEFAULT_ROOT ;
const safe = resolveSafe ( rootParam );
if ( ! safe ) return sendErr ( 403 , '目录不在允许范围,请先在设置中添加' );
const children = buildTree ( safe . abs , 0 ) || [];
return sendJSON ( 200 , { root : safe . abs , tree : children });
}
if ( p === '/api/read' ) {
const safe = resolveSafe ( q . get ( 'path' ));
if ( ! safe ) return sendErr ( 403 , '路径不在允许范围' );
try {
const st = fs . statSync ( safe . abs );
if ( st . size > 5 * 1024 * 1024 ) return sendErr ( 413 , '文件超过 5MB,不支持在线预览,请下载查看' );
const text = readText ( safe . abs );
if ( text === null ) return sendErr ( 415 , '该类型不支持在线预览,请下载后用本地软件打开' );
return sendJSON ( 200 , {
name : path . basename ( safe . abs ),
path : safe . abs ,
ext : path . extname ( safe . abs ). toLowerCase (),
size : st . size ,
content : text ,
});
} catch ( e ) {
return sendErr ( 500 , '读取失败: ' + e . message );
}
}
if ( p === '/api/download' ) {
const safe = resolveSafe ( q . get ( 'path' ));
if ( ! safe ) return sendErr ( 403 , '路径不在允许范围' );
try {
const st = fs . statSync ( safe . abs );
const fname = encodeURIComponent ( path . basename ( safe . abs ));
res . writeHead ( 200 , {
'Content-Type' : mimeOf ( safe . abs ),
'Content-Length' : st . size ,
'Content-Disposition' : "attachment; filename*=UTF-8''" + fname ,
'Cache-Control' : 'no-store' ,
});
fs . createReadStream ( safe . abs ). pipe ( res );
} catch ( e ) {
return sendErr ( 500 , '下载失败: ' + e . message );
}
return ;
}
2026-08-31 15:27:54 +08:00
if ( p === '/api/file' ) {
// 内联输出(无 attachment 头):供 <img>/<object> 直接渲染(如账号设定卡 SVG)
const safe = resolveSafe ( q . get ( 'path' ));
if ( ! safe ) return sendErr ( 403 , '路径不在允许范围' );
try {
const st = fs . statSync ( safe . abs );
if ( ! st . isFile ()) return sendErr ( 404 , '文件不存在' );
res . writeHead ( 200 , {
'Content-Type' : mimeOf ( safe . abs ),
'Content-Length' : st . size ,
'Cache-Control' : 'no-store' ,
});
fs . createReadStream ( safe . abs ). pipe ( res );
} catch ( e ) {
return sendErr ( 500 , '读取失败: ' + e . message );
}
return ;
}
2026-08-27 12:33:57 +08:00
// ---------- 数据功能(只读工作台自有数据库副本) ----------
const dshOk = ( code , obj ) => sendJSON ( code , { ok : true , ... obj });
const dshGuard = ( res2 ) => { if ( ! dsh . dbAvailable ()) { sendJSON ( 200 , { ok : false , error : '未检测到工作台数据库(mcn-plugin.db),本功能不可用' }); return true ; } return false ; };
const qInt = ( k , def ) => { const v = Number ( q . get ( k )); return Number . isFinite ( v ) ? v : def ; };
if ( p === '/api/dsh/meta' ) {
return sendJSON ( 200 , { ok : true , db : dsh . dbAvailable (), dbPath : dsh . DSH_DB , rankingDir : dsh . RANKING_DIR , ranking : dsh . rankingMeta () });
}
if ( p === '/api/dsh/stats' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , { stats : dsh . getStats () });
}
if ( p === '/api/dsh/recent-scripts' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , { items : dsh . listRecentScripts ( qInt ( 'limit' , 4 )) });
}
if ( p === '/api/dsh/tracks' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , { tracks : dsh . listTracks () });
}
if ( p === '/api/dsh/accounts' ) {
if ( dshGuard ()) return ;
try {
return dshOk ( 200 , dsh . listAccounts ({ page : qInt ( 'page' , 1 ), pageSize : qInt ( 'pageSize' , 20 ), search : String ( q . get ( 'search' ) || '' ), track : String ( q . get ( 'track' ) || '' ), type : String ( q . get ( 'type' ) || 'all' ) }));
} catch ( e ) { return sendErr ( 500 , e . message ); }
}
2026-08-28 18:54:01 +08:00
if ( p === '/api/dsh/account-favorite' && req . method === 'POST' ) {
if ( dshGuard ()) return ;
let body = '' ;
req . on ( 'data' , ( c ) => { body += c ; if ( body . length > 1e6 ) req . destroy (); });
req . on ( 'end' , () => {
try {
const { id , fav } = JSON . parse ( body || '{}' );
if ( ! id ) return sendErr ( 400 , '缺少 id' );
dsh . setFavorite ( id , !! fav );
return dshOk ( 200 , { id , favorite : !! fav });
} catch ( e ) { return sendErr ( 500 , e . message ); }
});
return ;
}
2026-08-27 12:33:57 +08:00
if ( p === '/api/dsh/account-videos' ) {
if ( dshGuard ()) return ;
try {
return dshOk ( 200 , dsh . listAccountVideos ( qInt ( 'accountId' , 0 ), qInt ( 'page' , 1 ), qInt ( 'pageSize' , 20 )));
} catch ( e ) { return sendErr ( 500 , e . message ); }
}
if ( p === '/api/dsh/account-persona' ) {
if ( dshGuard ()) return ;
const r = dsh . getPersona ( qInt ( 'accountId' , 0 ));
return dshOk ( 200 , { content : r ? r . content : null , analysisTime : r ? r . analysis_time : null });
}
2026-08-31 15:27:54 +08:00
if ( p === '/api/dsh/account-card' ) {
// 查询账号设定卡 SVG 文件(桌面产出,允许根目录下查找):{账号名}账号设定卡.svg
if ( dshGuard ()) return ;
const hit = findAccountCard ( q . get ( 'name' ));
return dshOk ( 200 , { exists : !! hit , path : hit || null });
}
2026-08-27 12:33:57 +08:00
if ( p === '/api/dsh/account-analysis' ) {
if ( dshGuard ()) return ;
const r = dsh . getAccountAnalysis ( qInt ( 'accountId' , 0 ));
return dshOk ( 200 , { content : r ? r . content : null , analysisTime : r ? r . analysis_time : null });
}
if ( p === '/api/dsh/videos' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , dsh . listVideos ({ page : qInt ( 'page' , 1 ), pageSize : qInt ( 'pageSize' , 20 ), search : String ( q . get ( 'search' ) || '' ), parsed : String ( q . get ( 'parsed' ) || '' ) }));
}
if ( p === '/api/dsh/video-detail' ) {
if ( dshGuard ()) return ;
try {
return dshOk ( 200 , dsh . getVideoDetail ({ videoId : qInt ( 'videoId' , 0 ), awemeId : q . get ( 'awemeId' ) }));
} catch ( e ) { return sendErr ( 500 , e . message ); }
}
if ( p === '/api/dsh/video-script' ) {
if ( dshGuard ()) return ;
try {
return dshOk ( 200 , dsh . getVideoScript ( qInt ( 'videoId' , 0 )));
} catch ( e ) { return sendErr ( 500 , e . message ); }
}
if ( p === '/api/dsh/rewrites' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , dsh . listRewrites ( qInt ( 'videoId' , 0 )));
}
if ( p === '/api/dsh/storyboards' ) {
if ( dshGuard ()) return ;
return dshOk ( 200 , { items : dsh . listStoryboards ( qInt ( 'videoId' , 0 )) });
}
if ( p === '/api/dsh/review-detail' ) {
if ( dshGuard ()) return ;
const r = dsh . getReviewDetail ( qInt ( 'id' , 0 ));
return r ? dshOk ( 200 , { review : r }) : sendErr ( 404 , '复盘记录不存在' );
}
if ( p === '/api/dsh/review-full' ) {
if ( dshGuard ()) return ;
const r = dsh . getReviewFull ( qInt ( 'rewriteId' , 0 ));
return r ? dshOk ( 200 , { reviews : r }) : sendErr ( 404 , '改写记录不存在' );
}
if ( p === '/api/dsh/ranking' ) {
try {
const r = dsh . getRanking ({ date : String ( q . get ( 'date' ) || '' ), category : String ( q . get ( 'category' ) || '' ), sortBy : String ( q . get ( 'sortBy' ) || 'rank' ), order : String ( q . get ( 'order' ) || '' ), page : qInt ( 'page' , 1 ), pageSize : qInt ( 'pageSize' , 20 ) });
return dshOk ( 200 , r );
} catch ( e ) { return sendErr ( 500 , e . message ); }
}
2026-08-27 20:43:39 +08:00
// ---------- 提交 AI 任务(Automation 一次性任务,WorkBuddy 自动执行,左侧会话栏可见)----------
if ( p === '/api/run' && req . method === 'POST' ) {
let body = '' ;
req . on ( 'data' , ( c ) => { body += c ; if ( body . length > 1e6 ) req . destroy (); });
req . on ( 'end' , () => {
try {
const { prompt , name } = JSON . parse ( body || '{}' );
if ( ! prompt || typeof prompt !== 'string' || ! prompt . trim ()) return sendErr ( 400 , 'prompt 不能为空' );
const { DatabaseSync } = require ( 'node:sqlite' );
const db = new DatabaseSync ( process . env . WORKBUDDY_DB || path . join ( os . homedir (), '.workbuddy' , 'workbuddy.db' ));
const now = Date . now ();
const id = 'automation-' + now ;
const cwd = 'D:\\AgentSkill\\mcn-workshop' ; // 工作台触发的会话归入 mcn-workshop 空间分组
const d = new Date ();
const pad = ( n ) => String ( n ). padStart ( 2 , '0' );
const scheduledAt = ` ${ d . getFullYear () } - ${ pad ( d . getMonth () + 1 ) } - ${ pad ( d . getDate ()) } T ${ pad ( d . getHours ()) } : ${ pad ( d . getMinutes ()) } ` ;
const uidRow = db . prepare ( "SELECT user_id FROM sessions WHERE user_id IS NOT NULL AND user_id <> '' ORDER BY created_at DESC LIMIT 1" ). get ();
const ownerUserId = uidRow ? uidRow . user_id : '' ;
db . prepare ( `INSERT INTO automations (id,name,prompt,status,schedule_type,next_run_at,scheduled_at,rrule,cwds,created_at,updated_at,skills_json,connector_ids_json,model_id,permission_mode,owner_user_id,owner_status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)` )
. run ( id , name || '工作台任务' , prompt . trim (), 'ACTIVE' , 'once' , now , scheduledAt , '' , JSON . stringify ([ cwd ]), now , now , '[]' , '[]' , 'deepseek-v4-flash' , 'fullAccess' , ownerUserId , 'confirmed' );
db . close ();
log ( '已提交 AI 任务: ' + id + ( name ? ' (' + name + ')' : '' ));
return sendJSON ( 200 , { ok : true , id , message : '任务已提交,请在左侧会话栏查看执行' });
} catch ( e ) {
return sendErr ( 500 , '任务提交失败: ' + e . message );
}
});
return ;
}
2026-08-28 18:54:01 +08:00
// 查询 AI 任务执行状态(供前端轮询恢复按钮):queued / pending / running / done / error / notfound
if ( p === '/api/run/status' && req . method === 'GET' ) {
const id = q . get ( 'id' );
if ( ! id ) return sendErr ( 400 , '缺少 id' );
try {
const { DatabaseSync } = require ( 'node:sqlite' );
const db = new DatabaseSync ( process . env . WORKBUDDY_DB || path . join ( os . homedir (), '.workbuddy' , 'workbuddy.db' ), { readOnly : true });
const auto = db . prepare ( 'SELECT status, deleted_at, last_run_at FROM automations WHERE id=?' ). get ( id );
if ( ! auto || auto . deleted_at ) { db . close (); return sendJSON ( 200 , { ok : true , id , state : 'notfound' }); }
const st = db . prepare ( 'SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?' ). get ( id );
const runs = db . prepare ( "SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1" ). get ( id );
db . close ();
let state = 'queued' ;
if ( st && Number ( st . running ) === 1 ) state = 'running' ;
else if ( runs ) {
const s = String ( runs . status || '' ). toUpperCase ();
if ( s === 'IN_PROGRESS' ) state = 'running' ;
else if ( s === 'PENDING_REVIEW' ) state = 'pending' ;
else if ([ 'DONE' , 'COMPLETED' , 'SUCCESS' , 'FINISHED' ]. includes ( s )) state = 'done' ;
else if ([ 'ERROR' , 'CANCELLED' , 'FAILED' , 'INTERRUPTED' ]. includes ( s )) state = 'error' ;
else state = 'pending' ;
}
return sendJSON ( 200 , { ok : true , id , state , error : ( st && st . last_error ) || null });
} catch ( e ) {
return sendErr ( 500 , '状态查询失败: ' + e . message );
}
}
// ---------- AI 生成选题(Dify chat-messages 即时调用,参考技能 S5 选题方法论) ----------
if ( p === '/api/ai/topics' && req . method === 'POST' ) {
let body = '' ;
req . on ( 'data' , ( c ) => { body += c ; if ( body . length > 1e6 ) req . destroy (); });
req . on ( 'end' , async () => {
try {
const { accountName } = JSON . parse ( body || '{}' );
if ( ! accountName || typeof accountName !== 'string' ) return sendErr ( 400 , '缺少账号名' );
// 1) 查账号定位 + 人设摘要(工作台库)
let contentText = '' , personaText = '' ;
try {
const { DatabaseSync } = require ( 'node:sqlite' );
const db = new DatabaseSync ( path . join ( ROOT , 'mcn-plugin.db' ), { readOnly : true });
const acc = db . prepare ( 'SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1' ). get ( accountName );
if ( acc ) {
contentText = String ( acc . content || '' ). slice ( 0 , 500 );
const p = db . prepare ( 'SELECT content_json, summary FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1' ). get ( acc . id );
if ( p ) personaText = String ( p . summary || p . content_json || '' ). slice ( 0 , 500 );
}
db . close ();
} catch ( e ) { /* 库不可用不阻塞,仍可生成 */ }
// 2) 读 Dify key:环境变量优先,其次技能脚本内置默认
let apiKey = process . env . DIFY_MCN_CYLG_KEY || '' ;
if ( ! apiKey ) {
const scriptPath = path . join ( ROOT , '..' , 'scripts' , 'MCN_CYLG_API.py' );
try {
const src = fs . readFileSync ( scriptPath , 'utf8' );
const m = src . match ( /DEFAULT_CYLG_KEY\s*=\s*"([^"]+)"/ );
if ( m ) apiKey = m [ 1 ];
} catch ( e ) { /* 脚本不存在则走 env */ }
}
if ( ! apiKey ) return sendErr ( 500 , '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)' );
// 3) 构造选题 query(参考 references/创作流程/5_生成短视频选题.md 方法论)
const query = '你是短视频选题策划师,请为达人「' + accountName + '」结合账号设定生成 3 个爆款选题方案。\n\n'
+ '【达人信息】\n' + ( contentText || '(无账号定位信息)' ) + '\n'
+ ( personaText ? '【人设摘要】\n' + personaText + '\n' : '' )
+ '\n要求:\n'
+ '1. 选题要跳出账号核心行为框架,但保留账号结构性符号(固定机制/口头禅等)\n'
+ '2. 每个选题一句话说清主题+切入角度,标注目标体感(如治愈感/爽感/获得感)\n'
+ '3. 只输出 3 个选题,每行一个,格式:1. 主题(切入角度|目标体感)\n'
+ '4. 不要输出其他解释' ;
// 4) 调 Dify chat-messages( blocking 即时返回)
const difyUrl = String ( process . env . DIFY_URL || 'https://mydify.youmanvideo.com/v1' ). replace ( /\/+$/ , '' );
const ctrl = new AbortController ();
const timer = setTimeout (() => ctrl . abort (), 100000 );
let resp ;
try {
resp = await fetch ( difyUrl + '/chat-messages' , {
method : 'POST' ,
headers : { 'Authorization' : 'Bearer ' + apiKey , 'Content-Type' : 'application/json' },
body : JSON . stringify ({ inputs : { is_think : '0' , is_online : '0' , model : process . env . DIFY_MCN_MODEL || 'gemini' }, query , response_mode : 'blocking' , user : 'mcn-work-shop' }),
signal : ctrl . signal ,
});
} finally { clearTimeout ( timer ); }
const data = await resp . json (). catch (() => ({}));
if ( ! resp . ok ) return sendErr ( 502 , 'AI 接口错误: ' + ( data . message || resp . status ));
log ( 'AI 选题生成: ' + accountName );
return dshOk ( 200 , { topics : data . answer || '' });
} catch ( e ) {
return sendErr ( 500 , '选题生成失败: ' + e . message );
}
});
return ;
}
2026-08-27 12:33:57 +08:00
// ---------- 静态文件 ----------
let filePath = path . join ( PUBLIC_DIR , p === '/' ? 'index.html' : p );
// 防穿越
if ( ! filePath . startsWith ( PUBLIC_DIR )) return sendErr ( 403 , '禁止访问' );
fs . stat ( filePath , ( err , st ) => {
if ( err || ! st . isFile ()) return sendErr ( 404 , 'Not Found' );
res . writeHead ( 200 , { 'Content-Type' : mimeOf ( filePath ), 'Content-Length' : st . size , 'Cache-Control' : 'no-store' });
fs . createReadStream ( filePath ). pipe ( res );
});
});
// ---- 端口自动避让 ----
function listen ( port ) {
server . once ( 'error' , ( e ) => {
if ( e . code === 'EADDRINUSE' ) {
log ( '端口 ' + port + ' 被占用,尝试 ' + ( port + 1 ));
listen ( port + 1 );
} else {
console . error ( '启动失败:' , e . message );
process . exit ( 1 );
}
});
server . listen ( port , '127.0.0.1' , () => {
log ( 'MCN工作台已启动: http://localhost:' + port );
log ( '默认项目地址: ' + DEFAULT_ROOT + ( fs . existsSync ( DEFAULT_ROOT ) ? '' : '(不存在)' ));
log ( '按 Ctrl+C 停止服务' );
});
}
listen ( PORT_BASE );