init
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
release/
|
||||
frontweb-dist/
|
||||
backend-app/
|
||||
*.log
|
||||
@@ -0,0 +1,3 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
strict-ssl=false
|
||||
better_sqlite3_binary_host_mirror=https://npmmirror.com/mirrors/better-sqlite3
|
||||
@@ -0,0 +1,131 @@
|
||||
# LocalMiniDrama 桌面客户端
|
||||
|
||||
基于 Electron 的本地桌面应用,内嵌 `backend-node` 与 `frontweb`,打包为 Windows exe / macOS dmg 后可直接运行。当前版本:**v1.2.7**
|
||||
|
||||
---
|
||||
|
||||
## 主要功能(v1.2.7)
|
||||
|
||||
| 模块 | 功能 |
|
||||
|------|------|
|
||||
| 首页(项目列表) | 创建/打开剧集项目;素材库(角色/场景/道具全局复用);AI 配置;明暗主题切换 |
|
||||
| 剧集管理页 | 管理剧集信息(标题/风格/比例);分集列表(新增/删除/预览剧本);本剧资源库(角色/场景/道具按剧过滤);从素材库导入资源 |
|
||||
| 制作页(分集) | 剧本编辑、角色/场景/道具 AI 生成与图片管理;分镜脚本生成与逐镜编辑(图片提示词、视频提示词) |
|
||||
| 分镜全能模式 | 分镜可在**经典**与**全能模式**间切换;全能模式中间为**片段描述**(`@图片1`… 多图参考),配合 AI 配置中 **`volcengine_omni`(Seedance 2.0)** 或 **`kling_omni`(可灵 Omni)**;生视频前校验模型匹配;支持「根据分镜生成提示词」 |
|
||||
| 尾帧衔接 / 导出分镜表 | **尾帧衔接**:提取本镜视频末帧设为下一镜首帧;**导出分镜表**:HTML 表格导出当前集全部镜头字段 |
|
||||
| 生成任务进度 | 角色 / 场景 / 道具 / 分镜图 / 视频任务统一轮询与恢复(`generationTaskStore`) |
|
||||
| 分镜图生成 | **相机角度视角**:仰视/俯视/侧面/背面角度自动影响背景透视;**四宫格序列图**:一键生成 2×2 四帧序列参考图,自动拆分面板,随时切换主分镜图 |
|
||||
| 一键流水线 | **一键生成视频**:全流程自动执行;**补全并生成**:仅生成缺失内容,自动跳过已有 |
|
||||
| 图片/视频生成 | 支持 DashScope、Volcengine、Gemini 等多种 API;生成失败自动重试 3 次;错误信息持久显示 |
|
||||
| 合成视频 | 将所有分镜视频合成为完整剧集 |
|
||||
| 主题 | 支持暗色模式(默认)与浅色模式,偏好持久保存 |
|
||||
|
||||
---
|
||||
|
||||
## 开发运行
|
||||
|
||||
1. 确保已构建前端(否则窗口内会显示「请先构建前端」提示):
|
||||
```bash
|
||||
cd ../frontweb && npm install && npm run build
|
||||
```
|
||||
2. 安装依赖并启动 Electron:
|
||||
```bash
|
||||
cd desktop
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
开发时后端工作目录为 `backend-node/`,配置与数据使用仓库内路径。
|
||||
|
||||
---
|
||||
|
||||
## 打包为 exe
|
||||
|
||||
在 `desktop` 目录下执行:
|
||||
|
||||
```bash
|
||||
cd desktop
|
||||
npm install
|
||||
npm run dist
|
||||
```
|
||||
|
||||
**国内网络**:若从 GitHub 下载 Electron 或 winCodeSign 超时,使用国内镜像:
|
||||
|
||||
```bash
|
||||
npm run dist:cn
|
||||
```
|
||||
|
||||
本目录下的 `.npmrc` 已配置 `registry=https://registry.npmmirror.com`,`npm install` 会使用国内源;`dist:cn` 脚本会将 Electron 与 electron-builder 的二进制下载也切换到 npmmirror 镜像。
|
||||
|
||||
产物在 `desktop/release/` 下:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `LocalMiniDrama Setup x.x.x.exe` | NSIS 安装包(有安装引导,可选安装目录) |
|
||||
| `LocalMiniDrama x.x.x.exe` | 便携版(单文件,无需安装,双击即用) |
|
||||
|
||||
首次运行时,会在用户数据目录(如 `%APPDATA%/LocalMiniDrama`)下生成 `backend/`,包含 `configs/config.yaml`(从 example 复制)和 `data/`(数据库与文件存储),按需修改配置即可。
|
||||
|
||||
---
|
||||
|
||||
## 脚本说明
|
||||
|
||||
| 脚本 | 说明 |
|
||||
|------|------|
|
||||
| `npm start` | 启动 Electron(开发模式) |
|
||||
| `npm run build:front` | 仅构建前端(frontweb) |
|
||||
| `npm run copy-front` | 将 frontweb/dist 复制到 desktop/frontweb-dist(打包前置步骤) |
|
||||
| `npm run pack` | 构建前端 + 复制 + 打出未压缩目录(便于检查打包内容) |
|
||||
| `npm run dist` | 构建前端 + 复制 + 打出 Windows 安装包与便携 exe |
|
||||
| `npm run dist:cn` | 同上,使用国内镜像(Electron、electron-builder 二进制) |
|
||||
| `npm run prepare-backend` | 将 backend-node 复制到 backend-app(打包前置步骤) |
|
||||
| `bash dist-mac.sh` | macOS 一键打包(完整版 + 纯净版 DMG,含国内镜像加速) |
|
||||
|
||||
---
|
||||
|
||||
## 打包后如何看日志 / 调试
|
||||
|
||||
### 1. 查看后端日志文件(推荐)
|
||||
|
||||
双击运行 exe 时,后端日志会自动写入:
|
||||
|
||||
```
|
||||
%APPDATA%\LocalMiniDrama\backend\logs\app.log
|
||||
```
|
||||
|
||||
用记事本或 VS Code 打开后,点击「AI 生成角色」等按钮,查看是否有对应请求行、报错信息,便于判断是请求未发出、AI 超时还是配置有误。
|
||||
|
||||
### 2. 从命令行运行(实时日志)
|
||||
|
||||
```powershell
|
||||
& "D:\path\to\release\LocalMiniDrama 1.2.7.exe"
|
||||
```
|
||||
|
||||
日志会直接打印在终端,操作软件时可实时看到所有输出。
|
||||
|
||||
### 3. 打开前端开发者工具
|
||||
|
||||
```powershell
|
||||
$env:LOCALMINIDRAMA_DEVTOOLS=1
|
||||
& "D:\path\to\release\LocalMiniDrama 1.2.7.exe"
|
||||
```
|
||||
|
||||
在 Network 面板查看各 API 请求(如 `POST /api/v1/generation/characters`)是否正常发出和返回。
|
||||
|
||||
### 4. 确认配置与网络
|
||||
|
||||
配置文件位于:
|
||||
|
||||
```
|
||||
%APPDATA%\LocalMiniDrama\backend\configs\config.yaml
|
||||
```
|
||||
|
||||
AI 相关配置需在软件「AI 配置」弹窗中填写并保存(会写入上述 yaml 文件);本机网络需能访问对应 API(如 dashscope、volcengine 等)。
|
||||
|
||||
---
|
||||
|
||||
## 依赖
|
||||
|
||||
- Node.js >= 18
|
||||
- 本仓库中的 `backend-node`(打包时通过 `prepare-backend` 复制到 `backend-app`)
|
||||
- 前端需先在 `frontweb` 目录执行 `npm run build`,再打包或开发运行
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
npm run dist:cn
|
||||
pause
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# macOS 打包脚本(完整版 + 纯净版 DMG)
|
||||
# 用法:在 desktop/ 目录下执行 bash dist-mac.sh
|
||||
# 或先授权:chmod +x dist-mac.sh && ./dist-mac.sh
|
||||
|
||||
set -e
|
||||
|
||||
# 使用国内镜像加速 Electron 下载
|
||||
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
|
||||
export ELECTRON_BUILDER_BINARIES_MIRROR="https://cdn.npmmirror.com/binaries/electron-builder-binaries/"
|
||||
|
||||
# 禁用 macOS 代码签名(无证书时跳过签名流程)
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
|
||||
# 切换到 desktop 目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo ""
|
||||
echo "========== [1/2] 构建完整版(含示例资源)=========="
|
||||
echo ""
|
||||
|
||||
# 准备后端 + 编译前端 + 复制前端产物 + electron-builder 打包
|
||||
npm run prepare-backend
|
||||
npm run build:front
|
||||
npm run copy-front
|
||||
npx electron-builder --mac --config electron-builder-mac.json
|
||||
|
||||
echo ""
|
||||
echo "========== [2/2] 构建纯净版(不含示例资源)=========="
|
||||
echo ""
|
||||
|
||||
# 前端/后端已准备好,直接再打一次 lite 包
|
||||
npx electron-builder --mac --config electron-builder-mac-lite.json
|
||||
|
||||
echo ""
|
||||
echo "========== 全部构建完成 =========="
|
||||
echo "输出目录:release/"
|
||||
echo " 完整版(Intel):LocalMiniDrama-x.x.x-mac-x64.dmg"
|
||||
echo " 完整版(ARM) :LocalMiniDrama-x.x.x-mac-arm64.dmg"
|
||||
echo " 纯净版(Intel):LocalMiniDrama-Lite-x.x.x-mac-x64.dmg"
|
||||
echo " 纯净版(ARM) :LocalMiniDrama-Lite-x.x.x-mac-arm64.dmg"
|
||||
echo ""
|
||||
Vendored
+16
File diff suppressed because one or more lines are too long
+6
@@ -0,0 +1,6 @@
|
||||
directories:
|
||||
output: dist
|
||||
buildResources: build
|
||||
artifactName: LocalMiniDrama-${buildVersion}.${ext}
|
||||
files: []
|
||||
electronVersion: 28.3.3
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"appId": "com.localminidrama.desktop",
|
||||
"productName": "LocalMiniDrama",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"package.json",
|
||||
"backend-app/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/better-sqlite3/**",
|
||||
"node_modules/sharp/**",
|
||||
"backend-app/tools/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "frontweb-dist",
|
||||
"to": "frontweb/dist",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../backend-node/tools/ffmpeg",
|
||||
"to": "ffmpeg",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": ["nsis", "portable"],
|
||||
"icon": null,
|
||||
"signAndEditExecutable": false,
|
||||
"artifactName": "${productName}-Lite-${version}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"artifactName": "${productName}-Lite-Setup-${version}.${ext}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"appId": "com.localminidrama.desktop",
|
||||
"productName": "LocalMiniDrama",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"package.json",
|
||||
"backend-app/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/better-sqlite3/**",
|
||||
"backend-app/tools/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "frontweb-dist",
|
||||
"to": "frontweb/dist",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "ffmpeg-mac",
|
||||
"to": "ffmpeg",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] }
|
||||
],
|
||||
"category": "public.app-category.entertainment",
|
||||
"icon": null,
|
||||
"identity": null
|
||||
},
|
||||
"dmg": {
|
||||
"title": "${productName} Lite ${version}",
|
||||
"artifactName": "${productName}-Lite-${version}-mac-${arch}.dmg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"appId": "com.localminidrama.desktop",
|
||||
"productName": "LocalMiniDrama",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"package.json",
|
||||
"backend-app/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/better-sqlite3/**",
|
||||
"backend-app/tools/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "frontweb-dist",
|
||||
"to": "frontweb/dist",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../example_drama",
|
||||
"to": "example_drama",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "ffmpeg-mac",
|
||||
"to": "ffmpeg",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] }
|
||||
],
|
||||
"category": "public.app-category.entertainment",
|
||||
"icon": null,
|
||||
"identity": null
|
||||
},
|
||||
"dmg": {
|
||||
"title": "${productName} ${version}",
|
||||
"artifactName": "${productName}-${version}-mac-${arch}.dmg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
将 macOS 版 ffmpeg 可执行文件放在本目录:
|
||||
ffmpeg-mac/ffmpeg
|
||||
ffmpeg-mac/ffprobe (可选,建议一并放入)
|
||||
|
||||
推荐使用 evermeet.cx 的静态构建版本,解压后放入此目录,注意需要有可执行权限:
|
||||
chmod +x ffmpeg ffprobe
|
||||
|
||||
构建 dmg 后,这两个文件会随安装包分发;用户首次启动时自动复制到:
|
||||
~/Library/Application Support/localminidrama-desktop/backend/tools/ffmpeg/
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
const { app, BrowserWindow, Menu } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 显式固定 userData 目录,使开发模式与打包 exe 路径完全一致,防止 productName 变更导致路径漂移
|
||||
const USERDATA_DIR = path.join(app.getPath('appData'), 'localminidrama-desktop');
|
||||
app.setPath('userData', USERDATA_DIR);
|
||||
|
||||
const MAIN_STARTUP_LOG = path.join(USERDATA_DIR, 'main-startup.log');
|
||||
function writeMainLog(msg) {
|
||||
const line = `${new Date().toISOString()} ${msg}\n`;
|
||||
try {
|
||||
if (!fs.existsSync(USERDATA_DIR)) fs.mkdirSync(USERDATA_DIR, { recursive: true });
|
||||
fs.appendFileSync(MAIN_STARTUP_LOG, line);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
writeMainLog(`uncaughtException: ${err && err.stack ? err.stack : err}`);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
const text = reason instanceof Error ? reason.stack : String(reason);
|
||||
writeMainLog(`unhandledRejection: ${text}`);
|
||||
});
|
||||
|
||||
writeMainLog(`main.js loaded packaged=${app.isPackaged} exec=${process.execPath}`);
|
||||
|
||||
// 兼容迁移:若旧路径 LocalMiniDrama 有数据而新路径为空,自动迁移
|
||||
;(function migrateOldUserData() {
|
||||
const oldPath = path.join(app.getPath('appData'), 'LocalMiniDrama');
|
||||
if (fs.existsSync(oldPath) && !fs.existsSync(USERDATA_DIR)) {
|
||||
try {
|
||||
fs.renameSync(oldPath, USERDATA_DIR);
|
||||
} catch (e) {
|
||||
// rename 跨驱动器时会失败,此时静默忽略,用户数据仍可手动迁移
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const BACKEND_APP_PATH = path.join(__dirname, 'backend-app');
|
||||
const BACKEND_NODE_PATH = path.join(__dirname, '..', 'backend-node');
|
||||
const DEFAULT_PORT = 5679;
|
||||
|
||||
let serverInstance = null;
|
||||
|
||||
/** 开发模式用 backend-node(改代码即生效);打包后用 backend-app */
|
||||
function getBackendModulePath() {
|
||||
if (app.isPackaged) return BACKEND_APP_PATH;
|
||||
// Electron 开发模式必须用 backend-app:require 会向上解析到 desktop/node_modules,
|
||||
// 其中 better-sqlite3 已由 postinstall 的 electron-rebuild 对准当前 Electron ABI。
|
||||
// 若直接用 backend-node,则会加载 backend-node/node_modules(多为本机 Node 编的 ABI,必炸)。
|
||||
if (process.versions.electron && fs.existsSync(path.join(BACKEND_APP_PATH, 'src', 'app.js'))) {
|
||||
return BACKEND_APP_PATH;
|
||||
}
|
||||
return fs.existsSync(BACKEND_NODE_PATH) ? BACKEND_NODE_PATH : BACKEND_APP_PATH;
|
||||
}
|
||||
|
||||
function getBackendCwd() {
|
||||
if (app.isPackaged) {
|
||||
return path.join(app.getPath('userData'), 'backend');
|
||||
}
|
||||
return getBackendModulePath();
|
||||
}
|
||||
|
||||
function ensureBackendCwd(backendCwd) {
|
||||
if (!fs.existsSync(backendCwd)) {
|
||||
fs.mkdirSync(backendCwd, { recursive: true });
|
||||
}
|
||||
const configsDir = path.join(backendCwd, 'configs');
|
||||
const dataDir = path.join(backendCwd, 'data');
|
||||
const logsDir = path.join(backendCwd, 'logs');
|
||||
const configPath = path.join(configsDir, 'config.yaml');
|
||||
|
||||
if (!fs.existsSync(configsDir)) fs.mkdirSync(configsDir, { recursive: true });
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
||||
|
||||
// 首次安装时,从打包内置的 config.yaml 复制到用户数据目录
|
||||
const bundledConfig = path.join(getBackendModulePath(), 'configs', 'config.yaml');
|
||||
if (!fs.existsSync(configPath) && fs.existsSync(bundledConfig)) {
|
||||
fs.copyFileSync(bundledConfig, configPath);
|
||||
}
|
||||
|
||||
// 每次启动时,将内置 config.yaml 中的 vendor_lock 节强制同步到用户 config.yaml,
|
||||
// 确保打包时配置的锁定策略对所有用户生效,不受首次安装后遗留旧配置影响。
|
||||
if (fs.existsSync(bundledConfig) && fs.existsSync(configPath)) {
|
||||
try {
|
||||
const yaml = require('js-yaml');
|
||||
const userCfg = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
||||
const bundledCfg = yaml.load(fs.readFileSync(bundledConfig, 'utf8')) || {};
|
||||
if (bundledCfg.vendor_lock !== undefined) {
|
||||
userCfg.vendor_lock = bundledCfg.vendor_lock;
|
||||
fs.writeFileSync(configPath, yaml.dump(userCfg, { lineWidth: -1 }), 'utf8');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[config] Failed to sync vendor_lock from bundled config:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次启动时,将打包内置的 ffmpeg 自动复制到 userData/backend/tools/ffmpeg/。
|
||||
* 来源:process.resourcesPath/ffmpeg/(由 electron-builder extraResources 写入)。
|
||||
* 已存在则跳过,不会重复覆盖,也不影响用户手动替换版本。
|
||||
*/
|
||||
function ensureFfmpeg(backendCwd) {
|
||||
if (!app.isPackaged) return;
|
||||
const isWin = process.platform === 'win32';
|
||||
const ffmpegName = isWin ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
const ffprobeName = isWin ? 'ffprobe.exe' : 'ffprobe';
|
||||
|
||||
const destDir = path.join(backendCwd, 'tools', 'ffmpeg');
|
||||
const destFfmpeg = path.join(destDir, ffmpegName);
|
||||
|
||||
// 已存在则跳过(支持用户手动替换)
|
||||
if (fs.existsSync(destFfmpeg)) {
|
||||
console.log('[ffmpeg] Already exists at', destFfmpeg);
|
||||
return;
|
||||
}
|
||||
|
||||
const srcDir = path.join(process.resourcesPath, 'ffmpeg');
|
||||
const srcFfmpeg = path.join(srcDir, ffmpegName);
|
||||
if (!fs.existsSync(srcFfmpeg)) {
|
||||
console.warn(
|
||||
'[ffmpeg] Bundled ffmpeg not found, skipping auto-extract. Expected:',
|
||||
srcFfmpeg,
|
||||
'(打包前请将 ffmpeg.exe 放入 backend-node/tools/ffmpeg,并确保 package.json 的 extraResources 包含该目录)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
||||
fs.copyFileSync(srcFfmpeg, destFfmpeg);
|
||||
if (!isWin) fs.chmodSync(destFfmpeg, 0o755);
|
||||
|
||||
const srcFfprobe = path.join(srcDir, ffprobeName);
|
||||
if (fs.existsSync(srcFfprobe)) {
|
||||
const destFfprobe = path.join(destDir, ffprobeName);
|
||||
fs.copyFileSync(srcFfprobe, destFfprobe);
|
||||
if (!isWin) fs.chmodSync(destFfprobe, 0o755);
|
||||
}
|
||||
console.log('[ffmpeg] Auto-extracted to', destDir);
|
||||
} catch (e) {
|
||||
console.warn('[ffmpeg] Auto-extract failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getWebDistPath() {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, 'frontweb', 'dist');
|
||||
}
|
||||
return path.join(__dirname, '..', 'frontweb', 'dist');
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测端口是否空闲:优先使用 preferredPort,被占用时让 OS 分配一个随机空闲端口。
|
||||
* 返回最终可用的端口号。
|
||||
*/
|
||||
function findFreePort(preferredPort) {
|
||||
const net = require('net');
|
||||
return new Promise((resolve) => {
|
||||
const probe = net.createServer();
|
||||
probe.once('error', () => {
|
||||
// 首选端口被占,让 OS 随机分配
|
||||
const fallback = net.createServer();
|
||||
fallback.listen(0, '127.0.0.1', () => {
|
||||
const port = fallback.address().port;
|
||||
fallback.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
probe.listen(preferredPort, '127.0.0.1', () => {
|
||||
probe.close(() => resolve(preferredPort));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow(port) {
|
||||
Menu.setApplicationMenu(null);
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
webPreferences: { nodeIntegration: false, contextIsolation: true },
|
||||
show: false,
|
||||
});
|
||||
win.once('ready-to-show', () => {
|
||||
win.show();
|
||||
writeMainLog('window ready-to-show');
|
||||
});
|
||||
// 若页面长期不触发 ready-to-show,避免用户误以为“点了没反应”
|
||||
setTimeout(() => {
|
||||
if (!win.isDestroyed() && !win.isVisible()) {
|
||||
win.show();
|
||||
writeMainLog('window shown (fallback timeout, check page load)');
|
||||
}
|
||||
}, 8000);
|
||||
win.webContents.on('did-fail-load', (_e, code, desc, url) => {
|
||||
writeMainLog(`did-fail-load code=${code} desc=${desc} url=${url}`);
|
||||
});
|
||||
writeMainLog(`createWindow loadURL http://127.0.0.1:${port}`);
|
||||
win.loadURL(`http://127.0.0.1:${port}`);
|
||||
win.on('closed', () => app.quit());
|
||||
if (process.env.LOCALMINIDRAMA_DEVTOOLS === '1') {
|
||||
win.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端始终在主进程内运行(打包用子进程会重复启动 exe 导致大量进程,故取消) */
|
||||
async function startBackend() {
|
||||
const backendCwd = getBackendCwd();
|
||||
ensureBackendCwd(backendCwd);
|
||||
ensureFfmpeg(backendCwd);
|
||||
process.env.WEB_DIST_PATH = getWebDistPath();
|
||||
if (app.isPackaged) {
|
||||
process.env.LOG_FILE = path.join(backendCwd, 'logs', 'app.log');
|
||||
process.env.EXAMPLE_DRAMA_PATH = path.join(process.resourcesPath, 'example_drama');
|
||||
} else {
|
||||
process.env.EXAMPLE_DRAMA_PATH = path.join(__dirname, '..', 'example_drama');
|
||||
}
|
||||
process.chdir(backendCwd);
|
||||
|
||||
const backendModulePath = getBackendModulePath();
|
||||
try {
|
||||
require(path.join(backendModulePath, 'src', 'db', 'migrate.js'));
|
||||
} catch (err) {
|
||||
console.warn('Migration warning:', err.message);
|
||||
}
|
||||
|
||||
const { createApp } = require(path.join(backendModulePath, 'src', 'app.js'));
|
||||
const { createServer } = require('http');
|
||||
const { app: expressApp, config } = createApp();
|
||||
const preferredPort = config.server?.port || DEFAULT_PORT;
|
||||
|
||||
// 自动探测空闲端口:优先默认端口,被占时由 OS 分配,支持多实例同时运行
|
||||
const port = await findFreePort(preferredPort);
|
||||
if (port !== preferredPort) {
|
||||
console.log(`Port ${preferredPort} in use, using ${port}`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer(expressApp);
|
||||
serverInstance = server;
|
||||
server.on('error', reject);
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log('Backend listening on', port);
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
writeMainLog('app.whenReady');
|
||||
let port;
|
||||
try {
|
||||
port = await startBackend();
|
||||
writeMainLog(`startBackend ok port=${port}`);
|
||||
} catch (err) {
|
||||
const stack = err && err.stack ? err.stack : String(err);
|
||||
writeMainLog(`Failed to start backend\n${stack}`);
|
||||
console.error('Failed to start backend', err);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
// startBackend 的 Promise 在 listen 回调中 resolve,服务器此时已就绪,直接建窗口
|
||||
createWindow(port);
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (serverInstance) {
|
||||
serverInstance.close();
|
||||
serverInstance = null;
|
||||
}
|
||||
});
|
||||
Generated
+6732
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "localminidrama-desktop",
|
||||
"version": "1.2.7",
|
||||
"description": "LocalMiniDrama 本地桌面客户端",
|
||||
"main": "main.js",
|
||||
"author": "LocalMiniDrama",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prestart": "node scripts/copy-backend.js",
|
||||
"start": "electron .",
|
||||
"rebuild:backend-native": "electron-rebuild -f --only better-sqlite3 --project-dir ../backend-node",
|
||||
"build:front": "cd ../frontweb && npm run build",
|
||||
"copy-front": "node scripts/copy-front.js",
|
||||
"pack": "npm run prepare-backend && npm run build:front && npm run copy-front && electron-builder --dir",
|
||||
"dist": "npm run prepare-backend && npm run build:front && npm run copy-front && electron-builder --win",
|
||||
"dist:cn": "node scripts/dist-cn.js",
|
||||
"dist:mac": "bash dist-mac.sh",
|
||||
"postinstall": "node scripts/copy-backend.js && electron-rebuild && electron-rebuild -f --only better-sqlite3 --project-dir ../backend-node",
|
||||
"prepare-backend": "node scripts/copy-backend.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"better-sqlite3": "^11.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonrepair": "^3.13.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.34.5",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-rebuild": "^3.2.9"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.localminidrama.desktop",
|
||||
"productName": "本地短剧助手",
|
||||
"artifactName": "LocalMiniDrama ${version}.${ext}",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"package.json",
|
||||
"backend-app/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/better-sqlite3/**",
|
||||
"node_modules/sharp/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "frontweb-dist",
|
||||
"to": "frontweb/dist",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../example_drama",
|
||||
"to": "example_drama",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../backend-node/tools/ffmpeg",
|
||||
"to": "ffmpeg",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": ["nsis", "portable"],
|
||||
"icon": null,
|
||||
"signAndEditExecutable": false
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const src = path.join(repoRoot, 'backend-node');
|
||||
const dest = path.join(__dirname, '..', 'backend-app');
|
||||
|
||||
const dirsToCopy = ['src', 'configs', 'scripts', 'migrations'];
|
||||
|
||||
if (!fs.existsSync(src)) {
|
||||
console.error('backend-node not found at', src);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
for (const dir of dirsToCopy) {
|
||||
const from = path.join(src, dir);
|
||||
const to = path.join(dest, dir);
|
||||
if (fs.existsSync(from)) {
|
||||
fs.cpSync(from, to, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 合并 desktop 自带的初始迁移(保证 01_init、02_add_default_model 等存在)
|
||||
const migrationsDest = path.join(dest, 'migrations');
|
||||
const initialMigrations = path.join(__dirname, 'initial-migrations');
|
||||
if (!fs.existsSync(migrationsDest)) fs.mkdirSync(migrationsDest, { recursive: true });
|
||||
if (fs.existsSync(initialMigrations)) {
|
||||
for (const f of fs.readdirSync(initialMigrations)) {
|
||||
if (f.endsWith('.sql')) {
|
||||
fs.copyFileSync(path.join(initialMigrations, f), path.join(migrationsDest, f));
|
||||
}
|
||||
}
|
||||
console.log('Merged initial-migrations -> desktop/backend-app/migrations');
|
||||
}
|
||||
|
||||
console.log('Copied backend-node (src, configs, scripts, migrations) -> desktop/backend-app');
|
||||
@@ -0,0 +1,14 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const src = path.join(repoRoot, 'frontweb', 'dist');
|
||||
const dest = path.join(__dirname, '..', 'frontweb-dist');
|
||||
|
||||
if (!fs.existsSync(src)) {
|
||||
console.error('frontweb/dist not found. Run: cd frontweb && npm run build');
|
||||
process.exit(1);
|
||||
}
|
||||
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true });
|
||||
console.log('Copied frontweb/dist -> desktop/frontweb-dist');
|
||||
@@ -0,0 +1,43 @@
|
||||
process.env.ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/';
|
||||
process.env.ELECTRON_BUILDER_BINARIES_MIRROR = 'https://cdn.npmmirror.com/binaries/electron-builder-binaries/';
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const isWin = process.platform === 'win32';
|
||||
const cwd = path.join(__dirname, '..');
|
||||
|
||||
// 第一步:完整构建(含示例资源),前端/后端同时准备
|
||||
console.log('\n========== [1/2] 构建完整版(含示例资源)==========\n');
|
||||
const full = spawnSync(isWin ? 'npm.cmd' : 'npm', ['run', 'dist'], {
|
||||
stdio: 'inherit',
|
||||
shell: isWin,
|
||||
cwd,
|
||||
});
|
||||
if (full.status !== 0) {
|
||||
console.error('完整版构建失败,终止。');
|
||||
process.exit(full.status || 1);
|
||||
}
|
||||
|
||||
// 第二步:纯净版构建(不含示例资源),前端/后端已准备好,直接调 electron-builder
|
||||
console.log('\n========== [2/2] 构建纯净版(不含示例资源)==========\n');
|
||||
const lite = spawnSync(
|
||||
isWin ? 'npx.cmd' : 'npx',
|
||||
['electron-builder', '--win', '--config', 'electron-builder-lite.json'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: isWin,
|
||||
cwd,
|
||||
}
|
||||
);
|
||||
if (lite.status !== 0) {
|
||||
console.error('纯净版构建失败。');
|
||||
process.exit(lite.status || 1);
|
||||
}
|
||||
|
||||
console.log('\n========== 全部构建完成 ==========');
|
||||
console.log('输出目录:release/');
|
||||
console.log(' 完整版安装包:LocalMiniDrama Setup x.x.x.exe');
|
||||
console.log(' 完整版便携版:LocalMiniDrama x.x.x.exe');
|
||||
console.log(' 纯净版安装包:LocalMiniDrama-Lite-Setup-x.x.x.exe');
|
||||
console.log(' 纯净版便携版:LocalMiniDrama-Lite-x.x.x.exe\n');
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,294 @@
|
||||
-- 最小初始表结构,与 backend-node 业务代码对齐(若无 backend-node/migrations 则使用本文件)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dramas (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
style TEXT DEFAULT 'realistic',
|
||||
tags TEXT,
|
||||
thumbnail TEXT,
|
||||
total_episodes INTEGER DEFAULT 1,
|
||||
total_duration INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'draft',
|
||||
metadata TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS episodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
episode_number INTEGER DEFAULT 0,
|
||||
title TEXT DEFAULT '',
|
||||
script_content TEXT,
|
||||
description TEXT,
|
||||
duration INTEGER DEFAULT 0,
|
||||
video_url TEXT,
|
||||
thumbnail TEXT,
|
||||
status TEXT DEFAULT 'draft',
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS storyboards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
episode_id INTEGER NOT NULL,
|
||||
scene_id INTEGER,
|
||||
storyboard_number INTEGER DEFAULT 0,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
location TEXT,
|
||||
time TEXT,
|
||||
duration REAL,
|
||||
dialogue TEXT,
|
||||
action TEXT,
|
||||
atmosphere TEXT,
|
||||
image_prompt TEXT,
|
||||
video_prompt TEXT,
|
||||
characters TEXT,
|
||||
shot_type TEXT,
|
||||
angle TEXT,
|
||||
movement TEXT,
|
||||
video_url TEXT,
|
||||
status TEXT DEFAULT 'draft',
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS characters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
role TEXT,
|
||||
description TEXT,
|
||||
personality TEXT,
|
||||
appearance TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
voice_style TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS episode_characters (
|
||||
episode_id INTEGER NOT NULL,
|
||||
character_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (episode_id, character_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scenes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
episode_id INTEGER,
|
||||
location TEXT,
|
||||
time TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
storyboard_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'draft',
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS props (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
type TEXT,
|
||||
description TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS storyboard_props (
|
||||
storyboard_id INTEGER NOT NULL,
|
||||
prop_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (storyboard_id, prop_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS frame_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storyboard_id INTEGER NOT NULL,
|
||||
frame_type TEXT,
|
||||
prompt TEXT,
|
||||
description TEXT,
|
||||
layout TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_service_configs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_type TEXT NOT NULL,
|
||||
provider TEXT DEFAULT '',
|
||||
name TEXT DEFAULT '',
|
||||
base_url TEXT DEFAULT '',
|
||||
api_key TEXT,
|
||||
model TEXT,
|
||||
default_model TEXT,
|
||||
endpoint TEXT,
|
||||
query_endpoint TEXT,
|
||||
priority INTEGER DEFAULT 0,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
settings TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS async_tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
progress INTEGER DEFAULT 0,
|
||||
message TEXT,
|
||||
resource_id TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
completed_at TEXT,
|
||||
error TEXT,
|
||||
result TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_generations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storyboard_id INTEGER,
|
||||
drama_id INTEGER,
|
||||
scene_id INTEGER,
|
||||
character_id INTEGER,
|
||||
provider TEXT,
|
||||
prompt TEXT,
|
||||
negative_prompt TEXT,
|
||||
model TEXT,
|
||||
frame_type TEXT,
|
||||
reference_images TEXT,
|
||||
size TEXT,
|
||||
quality TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
status TEXT,
|
||||
task_id TEXT,
|
||||
completed_at TEXT,
|
||||
error_msg TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_generations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER,
|
||||
storyboard_id INTEGER,
|
||||
provider TEXT,
|
||||
prompt TEXT,
|
||||
model TEXT,
|
||||
duration REAL,
|
||||
aspect_ratio TEXT,
|
||||
image_url TEXT,
|
||||
first_frame_url TEXT,
|
||||
last_frame_url TEXT,
|
||||
reference_image_urls TEXT,
|
||||
video_url TEXT,
|
||||
local_path TEXT,
|
||||
status TEXT,
|
||||
task_id TEXT,
|
||||
scene_id INTEGER,
|
||||
completed_at TEXT,
|
||||
error_msg TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_merges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
episode_id INTEGER,
|
||||
drama_id INTEGER,
|
||||
title TEXT,
|
||||
provider TEXT,
|
||||
model TEXT,
|
||||
status TEXT,
|
||||
scenes TEXT,
|
||||
task_id TEXT,
|
||||
created_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS character_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
category TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
description TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scene_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location TEXT NOT NULL DEFAULT '',
|
||||
time TEXT,
|
||||
prompt TEXT,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
category TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prop_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
category TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER,
|
||||
name TEXT,
|
||||
type TEXT,
|
||||
category TEXT,
|
||||
url TEXT,
|
||||
local_path TEXT,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
duration REAL,
|
||||
image_gen_id INTEGER,
|
||||
video_gen_id INTEGER,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 为已有库增加 default_model 列(新建库 01 已包含则跳过)
|
||||
ALTER TABLE ai_service_configs ADD COLUMN default_model TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 道具归属集:从某集剧本提取的道具记入该集,本集资源列表会展示
|
||||
ALTER TABLE props ADD COLUMN episode_id INTEGER;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- async_tasks 缺少 completed_at、error、result 时补上(与 taskService 一致)
|
||||
ALTER TABLE async_tasks ADD COLUMN completed_at TEXT;
|
||||
ALTER TABLE async_tasks ADD COLUMN error TEXT;
|
||||
ALTER TABLE async_tasks ADD COLUMN result TEXT;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- image_generations 缺少 completed_at / error_msg 时补上
|
||||
ALTER TABLE image_generations ADD COLUMN completed_at TEXT;
|
||||
ALTER TABLE image_generations ADD COLUMN error_msg TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- characters 表缺少 local_path 时补上
|
||||
ALTER TABLE characters ADD COLUMN local_path TEXT;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- scenes 表缺少 image_url / local_path 时补上
|
||||
ALTER TABLE scenes ADD COLUMN image_url TEXT;
|
||||
ALTER TABLE scenes ADD COLUMN local_path TEXT;
|
||||
Reference in New Issue
Block a user