184 lines
7.2 KiB
JavaScript
184 lines
7.2 KiB
JavaScript
const Database = require('better-sqlite3');
|
||
const path = require('path');
|
||
|
||
const DB_PATH = path.join(__dirname, '..', 'data.db');
|
||
|
||
function initDatabase() {
|
||
const db = new Database(DB_PATH);
|
||
db.pragma('journal_mode = WAL');
|
||
db.pragma('foreign_keys = ON');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS shops (
|
||
id TEXT PRIMARY KEY,
|
||
shop_id TEXT UNIQUE NOT NULL,
|
||
shop_name TEXT NOT NULL,
|
||
remark TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS products (
|
||
id TEXT PRIMARY KEY,
|
||
shop_id TEXT NOT NULL,
|
||
title TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated')),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS notes (
|
||
id TEXT PRIMARY KEY,
|
||
product_id TEXT NOT NULL,
|
||
shop_id TEXT NOT NULL,
|
||
title TEXT NOT NULL,
|
||
content TEXT DEFAULT '',
|
||
topics TEXT DEFAULT '[]',
|
||
image_paths TEXT DEFAULT '[]',
|
||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated', 'draft', 'approved', 'publishing', 'published', 'failed', 'rejected')),
|
||
use_status TEXT DEFAULT 'pending',
|
||
error_message TEXT DEFAULT '',
|
||
publish_result TEXT DEFAULT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
published_at DATETIME DEFAULT NULL,
|
||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS configs (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL,
|
||
category TEXT DEFAULT 'general',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS publish_tasks (
|
||
id TEXT PRIMARY KEY,
|
||
note_id TEXT NOT NULL,
|
||
shop_id TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'claimed', 'executing', 'completed', 'failed')),
|
||
claimed_by TEXT DEFAULT NULL,
|
||
claimed_at DATETIME DEFAULT NULL,
|
||
error_message TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS generation_jobs (
|
||
id TEXT PRIMARY KEY,
|
||
shop_id TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
|
||
total_products INTEGER DEFAULT 0,
|
||
total_notes INTEGER DEFAULT 0,
|
||
completed_notes INTEGER DEFAULT 0,
|
||
failed_notes INTEGER DEFAULT 0,
|
||
current_product_id TEXT DEFAULT NULL,
|
||
current_product_title TEXT DEFAULT '',
|
||
message TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_products_shop_id ON products(shop_id);
|
||
CREATE INDEX IF NOT EXISTS idx_notes_product_id ON notes(product_id);
|
||
CREATE INDEX IF NOT EXISTS idx_notes_shop_id ON notes(shop_id);
|
||
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
|
||
CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status);
|
||
CREATE INDEX IF NOT EXISTS idx_publish_tasks_shop_id ON publish_tasks(shop_id);
|
||
CREATE INDEX IF NOT EXISTS idx_generation_jobs_status ON generation_jobs(status);
|
||
CREATE INDEX IF NOT EXISTS idx_generation_jobs_shop_id ON generation_jobs(shop_id);
|
||
|
||
CREATE TABLE IF NOT EXISTS image_hashes (
|
||
id TEXT PRIMARY KEY,
|
||
note_id TEXT NOT NULL,
|
||
image_path TEXT NOT NULL,
|
||
phash TEXT NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_image_hashes_phash ON image_hashes(phash);
|
||
`);
|
||
|
||
// Migration: add missing fields if not exist
|
||
const migrate = [
|
||
"ALTER TABLE notes ADD COLUMN image_prompt TEXT DEFAULT ''",
|
||
"ALTER TABLE notes ADD COLUMN worker_id TEXT DEFAULT NULL",
|
||
"ALTER TABLE notes ADD COLUMN claimed_at DATETIME DEFAULT NULL",
|
||
"ALTER TABLE notes ADD COLUMN use_status TEXT DEFAULT 'pending'",
|
||
"ALTER TABLE products ADD COLUMN reference_image TEXT DEFAULT ''",
|
||
];
|
||
for (const sql of migrate) {
|
||
try { db.exec(sql); } catch (e) { /* column already exists */ }
|
||
}
|
||
|
||
db.exec(`
|
||
UPDATE notes
|
||
SET use_status = 'used', status = 'approved'
|
||
WHERE status = 'published';
|
||
|
||
UPDATE notes
|
||
SET use_status = 'failed', status = 'approved'
|
||
WHERE status = 'failed'
|
||
AND EXISTS (
|
||
SELECT 1 FROM publish_tasks
|
||
WHERE publish_tasks.note_id = notes.id
|
||
AND publish_tasks.status = 'failed'
|
||
);
|
||
|
||
UPDATE notes
|
||
SET use_status = 'pending'
|
||
WHERE status IN ('approved', 'publishing')
|
||
AND (use_status IS NULL OR use_status = '');
|
||
`);
|
||
|
||
|
||
// Insert default configs
|
||
const insertConfig = db.prepare(`
|
||
INSERT OR IGNORE INTO configs (key, value, category) VALUES (?, ?, ?)
|
||
`);
|
||
|
||
const defaults = [
|
||
['ai_provider', 'deepseek', 'ai'],
|
||
['ai_model', 'deepseek-chat', 'ai'],
|
||
['ai_api_key', '', 'ai'],
|
||
['ai_base_url', 'https://api.deepseek.com', 'ai'],
|
||
['image_provider', 'agnes-image-2.1-flash', 'ai'],
|
||
['image_api_key', '', 'ai'],
|
||
['image_base_url', 'https://apihub.agnes-ai.com', 'ai'],
|
||
['publish_interval_min', '30', 'publish'],
|
||
['publish_interval_max', '90', 'publish'],
|
||
['daily_publish_limit', '20', 'publish'],
|
||
['max_consecutive_failures', '3', 'publish'],
|
||
['poll_interval', '20', 'extension'],
|
||
['title_prompt_template', '为该商品写一个小红书标题,要求:简洁有力、带emoji、包含关键词,能吸引用户点击。商品信息:{{title}}', 'prompt'],
|
||
['content_prompt_template', '为该商品写一篇小红书正文,要求:1.正文800-1200字,分段落 2.话题3-5个相关标签 3.语气自然亲切,像朋友分享 4.包含emoji。商品信息:{{title}}', 'prompt'],
|
||
['prompt_template', '为该商品写一篇小红书笔记,要求:\n1. 标题吸引人,带emoji\n2. 正文800-1200字,分段落\n3. 话题3-5个相关标签\n4. 语气自然亲切,像朋友分享\n\n商品信息:{{title}}', 'prompt'],
|
||
['image_prompt_template', '{{title}},商品照片,自然光线,清晰细节,商业摄影,{{style}}风格', 'prompt'],
|
||
['image_style_options', '["清新","极简","ins风","高级感","小红书","日系"]', 'prompt'],
|
||
['image_count', '3', 'image_config'],
|
||
['image_size', '1K', 'image_config'],
|
||
['image_save_path', '', 'image_config'],
|
||
['ai_temperature', '0.8', 'ai'],
|
||
];
|
||
|
||
const insertMany = db.transaction(() => {
|
||
for (const [key, value, category] of defaults) {
|
||
insertConfig.run(key, value, category);
|
||
}
|
||
});
|
||
insertMany();
|
||
|
||
return db;
|
||
}
|
||
|
||
function getDatabase() {
|
||
return new Database(DB_PATH);
|
||
}
|
||
|
||
module.exports = { initDatabase, getDatabase, DB_PATH };
|