| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- 'use strict';
- // 共享状态装配:config / session / 手动店铺 / workspace 路径守卫 / 日志。
- // 由 server.js 在启动时调用一次 createContext(),得到的 ctx 贯穿所有服务与路由。
- // 语义与拆分前一致:config 为同一个可变对象;getWorkspaceRoot() 每次重读 config.rootDir。
- const fs = require('fs');
- const path = require('path');
- const ROOT_DIR = path.resolve(__dirname, '..');
- const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
- const CONFIG_PATH = path.join(ROOT_DIR, 'config.json');
- const CONFIG_EXAMPLE_PATH = path.join(ROOT_DIR, 'config.example.json');
- const SESSION_PATH = path.join(ROOT_DIR, 'session.json');
- const MANUAL_SHOPS_PATH = path.join(ROOT_DIR, 'manual-shops.json');
- const CONFIG_DEFAULTS = {
- port: 5177,
- apiKey: '',
- apiBase: 'https://api.lk888.ai',
- model: 'gpt-image-2',
- maxConcurrentJobs: 4,
- pageSize: 50,
- delayMs: 300,
- requestTimeoutMs: 20000,
- chatModel: '',
- chatApiBase: '',
- chatApiKey: '',
- accessToken: '',
- // HTTP 访问日志:true 时在控制台打印每个请求(5xx 与超过 1s 的慢请求始终打印)
- httpLog: false,
- // 钉钉 AI 表格(在线主档案/共享层):同步完成后自动推送,其他机器可拉取
- // 非敏感项给组织级默认值(appKey/baseId/sheetName/operatorId),新机器只需填 AppSecret;
- // appSecret 按规矩不进源码(gitignored config.json 各机自存)。
- aitable: {
- enabled: false,
- appKey: 'ding5vlpq3yeu7h6c6kj',
- appSecret: '',
- baseId: '7QG4Yx2JpLkaBl7EIpXNx0aGJ9dEq3XD',
- sheetName: '数据表',
- operatorId: 'zkWXDRXbxDlUiPgRPI3XjpwiEiE',
- },
- // 经营数据每日自动同步(商家客户端在线时自动补昨日数据),默认关闭
- autoSync: { enabled: false, time: '06:00' },
- };
- function writeConfigFile(cfg) {
- const temp = CONFIG_PATH + '.tmp';
- fs.writeFileSync(temp, JSON.stringify(cfg, null, 2), 'utf8');
- fs.renameSync(temp, CONFIG_PATH);
- }
- // 缺失/损坏时自动用模板(或内置默认)在本机生成新的 config.json —— 分发包开箱即用
- function loadConfig() {
- try {
- return { ...CONFIG_DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) };
- } catch (e) {
- if (e.code !== 'ENOENT') {
- console.warn(`config.json 解析失败(${e.message || e}),已改用默认配置;保存设置后会重建该文件。`);
- }
- let seeded = {};
- try { seeded = JSON.parse(fs.readFileSync(CONFIG_EXAMPLE_PATH, 'utf8')); } catch { /* 无模板则用内置默认 */ }
- const cfg = { ...CONFIG_DEFAULTS, ...seeded };
- try {
- writeConfigFile(cfg);
- console.log('已生成本机默认 config.json —— 请在「系统设置」中填写你自己的 API Key。');
- } catch { /* 只读目录等场景忽略 */ }
- return cfg;
- }
- }
- const MODELS = [
- { id: 'gpt-image-2', label: 'GPT Image 2(默认)' },
- { id: 'gpt-image-2-guan', label: 'GPT Image 2 官转' },
- { id: 'doubao-seedream-5-0-pro-260628', label: '豆包 Seedream 5.0 Pro' },
- { id: 'doubao-seedream-5-0-260128', label: '即梦 5.0(Seedream 5.0)' },
- { id: 'doubao-seedream-4-5-251128', label: '即梦 4.5(Seedream 4.5)' },
- { id: 'wan2.6-image', label: '通义万相 wan2.6-image' },
- ];
- const MODEL_IDS = new Set(MODELS.map((m) => m.id));
- // These seedream variants don't support the '1K' tier the UI's size dropdown offers by
- // default (doubao-seedream-4-5-251128: 2K/4K only; doubao-seedream-5-0-260128: 2K/3K only) -
- // map it up to '2K' rather than sending an invalid enum value.
- const NO_1K_SUPPORT = new Set(['doubao-seedream-4-5-251128', 'doubao-seedream-5-0-260128']);
- // ---------- workspace 路径守卫 ----------
- function createWorkspaceService(config) {
- const WORKSPACE_ROOT = path.join(path.dirname(ROOT_DIR), 'workspace');
- function getWorkspaceRoot() {
- const configuredRoot = String(config.rootDir || WORKSPACE_ROOT).trim() || WORKSPACE_ROOT;
- return path.resolve(configuredRoot);
- }
- function isWorkspaceRoot(candidate) {
- try {
- return !!candidate && path.resolve(String(candidate)) === getWorkspaceRoot();
- } catch {
- return false;
- }
- }
- function isWorkspacePath(candidate) {
- try {
- const resolved = path.resolve(String(candidate));
- const root = getWorkspaceRoot();
- return resolved === root || resolved.startsWith(root + path.sep);
- } catch {
- return false;
- }
- }
- function safeShopDir(root, folderName) {
- if (!isWorkspaceRoot(root) || typeof folderName !== 'string' ||
- !folderName || folderName === '.' || folderName === '..' ||
- folderName.includes('/') || folderName.includes('\\')) return null;
- const rootDir = path.resolve(root);
- const shopDir = path.resolve(rootDir, folderName);
- return shopDir.startsWith(rootDir + path.sep) ? shopDir : null;
- }
- return { WORKSPACE_ROOT, getWorkspaceRoot, isWorkspaceRoot, isWorkspacePath, safeShopDir };
- }
- // ---------- 采集 Cookie 会话 ----------
- function extractCookieValue(cookie, name) {
- const match = String(cookie || '').match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
- if (!match) return '';
- try { return decodeURIComponent(match[1]); } catch { return match[1]; }
- }
- function createSessionService() {
- const session = { cookie: '' };
- function loadSession() {
- try {
- const saved = JSON.parse(fs.readFileSync(SESSION_PATH, 'utf8'));
- session.cookie = String(saved.cookie || '');
- } catch { /* no saved session */ }
- }
- function saveSession() {
- fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2), 'utf8');
- }
- function getKsid() { return extractCookieValue(session.cookie, 'ksid'); }
- loadSession();
- return {
- get cookie() { return session.cookie; },
- set cookie(v) { session.cookie = v; },
- saveSession,
- getKsid,
- };
- }
- // ---------- 手动店铺清单 ----------
- function createShopsService() {
- let shopList = [];
- function loadManualShops() {
- try {
- const data = JSON.parse(fs.readFileSync(MANUAL_SHOPS_PATH, 'utf8'));
- if (data.version !== 1 || !Array.isArray(data.shops)) throw new Error('清单格式无效');
- const shopsById = new Map();
- for (const shop of data.shops) {
- if (!shop || !Number.isInteger(shop.shopId) || shop.shopId <= 0) continue;
- shopsById.set(shop.shopId, { shopId: shop.shopId, shopName: String(shop.shopName || shop.shopId), source: 'manual' });
- }
- shopList = Array.from(shopsById.values());
- } catch (e) {
- if (e.code !== 'ENOENT') console.warn(`读取手动店铺清单失败: ${e.message || e}`);
- }
- }
- function saveManualShops(shops) {
- const temp = `${MANUAL_SHOPS_PATH}.${process.pid}.tmp`;
- fs.writeFileSync(temp, JSON.stringify({ version: 1, shops }, null, 2), 'utf8');
- fs.renameSync(temp, MANUAL_SHOPS_PATH);
- }
- function mergeShops(newOnes) {
- const shopsById = new Map(shopList.map((shop) => [shop.shopId, shop]));
- for (const shop of newOnes) shopsById.set(shop.shopId, shop);
- return Array.from(shopsById.values());
- }
- loadManualShops();
- return {
- getList: () => shopList,
- setList: (v) => { shopList = v; },
- mergeShops,
- saveManualShops,
- };
- }
- // ---------- 日志(系统日志 + 统一序号;采集任务日志由 crawl 服务持有,这里负责合并) ----------
- function createLogsService() {
- const systemLogs = [];
- let logSeq = 0;
- function detectLogLevel(message) {
- if (/失败|错误|异常|过期|error|鉴权/i.test(message)) return 'error';
- if (/停止|跳过|警告|warn/i.test(message)) return 'warn';
- if (/完成|成功|已保存|已启动|done/i.test(message)) return 'success';
- return 'info';
- }
- function createLogEntry(message, source = 'task', level = 'info') {
- const now = new Date();
- return {
- seq: ++logSeq,
- id: `${source}-${logSeq}`,
- source,
- level,
- time: now.toTimeString().slice(0, 8),
- timestamp: now.getTime(),
- message: String(message),
- };
- }
- function pushSystemLog(msg, level = 'info') {
- systemLogs.push(createLogEntry(msg, 'system', level));
- if (systemLogs.length > 500) systemLogs.shift();
- }
- function mergeLogs(crawlLog) {
- return [...crawlLog, ...systemLogs]
- .sort((a, b) => b.seq - a.seq)
- .slice(0, 500);
- }
- function clearAll(crawlLog) {
- if (crawlLog) crawlLog.length = 0;
- systemLogs.length = 0;
- }
- return { detectLogLevel, createLogEntry, pushSystemLog, mergeLogs, clearAll };
- }
- function createContext() {
- const config = loadConfig();
- // 局域网访问口令:环境变量可临时覆盖(不落盘),便于临时开启/测试
- if (process.env.ACCESS_TOKEN) config.accessToken = process.env.ACCESS_TOKEN;
- const workspace = createWorkspaceService(config);
- try {
- fs.mkdirSync(workspace.getWorkspaceRoot(), { recursive: true });
- } catch (e) {
- // 历史配置的 rootDir 在本机不可创建(权限/目录被移动)时不要崩,允许进设置页修改
- console.warn(`输出根目录不可用:${workspace.getWorkspaceRoot()}(${e.code || e.message})。请到「系统设置」修改输出根目录。`);
- }
- const session = createSessionService();
- const shops = createShopsService();
- const logs = createLogsService();
- function saveConfig() {
- writeConfigFile(config);
- }
- return {
- ROOT_DIR,
- PUBLIC_DIR,
- config,
- saveConfig,
- models: { MODELS, MODEL_IDS, NO_1K_SUPPORT },
- workspace,
- session,
- shops,
- logs,
- };
- }
- // 智能问数的对话模型:未在设置中选择时使用的兜底模型,以及「生图 Key 复用」推导。
- // 不放入 CONFIG_DEFAULTS —— 避免落盘到 config.json(保持拆分前的持久化内容)。
- const DEFAULT_CHAT_MODEL = 'deepseek-v4.1-flash';
- function makeChatCfg(ctx) {
- return {
- chatApiBase: ctx.config.chatApiBase || ctx.config.apiBase,
- chatApiKey: ctx.config.chatApiKey || ctx.config.apiKey,
- chatModel: ctx.config.chatModel || DEFAULT_CHAT_MODEL,
- };
- }
- module.exports = {
- createContext,
- CONFIG_DEFAULTS,
- MODELS,
- MODEL_IDS,
- NO_1K_SUPPORT,
- DEFAULT_CHAT_MODEL,
- makeChatCfg,
- };
|