feat: add task workflow and asset downloads
This commit is contained in:
42
scripts/deploy.sh
Executable file
42
scripts/deploy.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "[deploy] Docker is required but was not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE=(docker-compose)
|
||||
else
|
||||
echo "[deploy] Docker Compose is required but was not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f .env.local ]; then
|
||||
cp .env.example .env.local
|
||||
echo "[deploy] Created .env.local from .env.example"
|
||||
echo "[deploy] Real generation requires API keys in .env.local. Empty keys keep mock/local flows available."
|
||||
fi
|
||||
|
||||
mkdir -p .runtime/data .runtime/uploads .runtime/generated-results
|
||||
|
||||
if [ -z "${APP_PORT:-}" ] && [ -f .env.local ]; then
|
||||
APP_PORT_FROM_FILE="$(sed -n 's/^APP_PORT=//p' .env.local | tail -n 1)"
|
||||
APP_PORT_FROM_FILE="${APP_PORT_FROM_FILE//\"/}"
|
||||
if [ -n "$APP_PORT_FROM_FILE" ]; then
|
||||
export APP_PORT="$APP_PORT_FROM_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
"${COMPOSE[@]}" up -d --build
|
||||
"${COMPOSE[@]}" ps
|
||||
|
||||
echo "[deploy] 智念AIGC平台 is starting at http://127.0.0.1:${APP_PORT:-3000}"
|
||||
echo "[deploy] If this is a public server, set NEXT_PUBLIC_APP_URL in .env.local to your domain."
|
||||
echo "[deploy] Web service and zhinian-worker are both managed by Docker Compose."
|
||||
@@ -1,6 +1,6 @@
|
||||
const port = process.env.PORT || process.env.NIANXX_PLAY_PORT || '3000';
|
||||
const port = process.env.PORT || process.env.APP_PORT || '3000';
|
||||
const hostname = process.env.HOSTNAME || '127.0.0.1';
|
||||
const baseUrl = process.env.NIANXXPLAY_PUBLIC_BASE_URL || `http://${hostname}:${port}`;
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.ZHINIAN_PUBLIC_BASE_URL || `http://${hostname}:${port}`;
|
||||
const healthUrl = new URL('/api/health', baseUrl).toString();
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -35,6 +35,5 @@ console.log(JSON.stringify({
|
||||
'@图片/@视频/@音频 references',
|
||||
'material thumbnails in chips and @ suggestions',
|
||||
'shared prompt assembly for image and video'
|
||||
],
|
||||
legacyRuntime: 'runtime/nianxx-play is kept as a reference artifact only'
|
||||
]
|
||||
}, null, 2));
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const runtimeDir = join(rootDir, 'runtime', 'nianxx-play');
|
||||
|
||||
function readJson(relativePath) {
|
||||
return JSON.parse(readFileSync(join(runtimeDir, relativePath), 'utf8'));
|
||||
}
|
||||
|
||||
const manifest = readJson('bundle-manifest.json');
|
||||
const pkg = readJson('package.json');
|
||||
const paths = readJson('.next/server/app-paths-manifest.json');
|
||||
const modes = readJson('content/seedance-starter/creation-modes.json');
|
||||
const catalog = readJson('content/seedance-starter/catalog.json');
|
||||
const planningCases = readJson('content/planning-cases.json');
|
||||
|
||||
const modeCounts = {};
|
||||
for (const item of catalog.cases || []) {
|
||||
modeCounts[item.mode] = (modeCounts[item.mode] || 0) + 1;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
project: 'Zhinian Creation Assistant',
|
||||
packageName: 'zhinian-creation-assistant',
|
||||
runtime: {
|
||||
appId: manifest.appId,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
bundledAt: manifest.bundledAt,
|
||||
entry: manifest.entry,
|
||||
sizeBytes: manifest.sizeBytes,
|
||||
},
|
||||
package: {
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
dependencies: pkg.dependencies,
|
||||
},
|
||||
routes: Object.keys(paths).sort(),
|
||||
creationModes: modes.map((mode) => ({
|
||||
id: mode.id,
|
||||
name: mode.name,
|
||||
editorType: mode.editorType,
|
||||
assetSlots: mode.assetSlots,
|
||||
})),
|
||||
catalog: {
|
||||
totalCases: (catalog.cases || []).length,
|
||||
modeCounts,
|
||||
},
|
||||
planningCases: planningCases.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
orientation: item.orientation,
|
||||
})),
|
||||
}, null, 2));
|
||||
25
scripts/setup.sh
Executable file
25
scripts/setup.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ ! -f .env.local ]; then
|
||||
cp .env.example .env.local
|
||||
echo "[setup] Created .env.local from .env.example"
|
||||
else
|
||||
echo "[setup] .env.local already exists"
|
||||
fi
|
||||
|
||||
mkdir -p .runtime/data .runtime/uploads .runtime/generated-results
|
||||
echo "[setup] Runtime directories are ready"
|
||||
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm install
|
||||
echo "[setup] Dependencies installed"
|
||||
else
|
||||
echo "[setup] npm was not found. Skip dependency install."
|
||||
fi
|
||||
|
||||
echo "[setup] Done. Start locally with:"
|
||||
echo " npm run dev -- --hostname 127.0.0.1 --port 3000"
|
||||
@@ -1,107 +0,0 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const runtimeDir = join(rootDir, 'runtime', 'nianxx-play');
|
||||
const serverPath = join(runtimeDir, 'server.js');
|
||||
|
||||
function parseEnvValue(raw) {
|
||||
const value = raw.trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseEnvFile(filePath) {
|
||||
if (!existsSync(filePath)) return {};
|
||||
const env = {};
|
||||
const raw = readFileSync(filePath, 'utf8');
|
||||
for (const line of raw.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||
if (!match) continue;
|
||||
env[match[1]] = parseEnvValue(match[2]);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function loadEnv() {
|
||||
const envFiles = [
|
||||
join(rootDir, '.env'),
|
||||
join(rootDir, '.env.local'),
|
||||
];
|
||||
if (process.env.NIANXXPLAY_LOAD_BUNDLED_ENV === '1') {
|
||||
envFiles.push(join(runtimeDir, '.env.runtime'));
|
||||
}
|
||||
|
||||
const fileEnv = {};
|
||||
for (const file of envFiles) {
|
||||
Object.assign(fileEnv, parseEnvFile(file));
|
||||
}
|
||||
return { ...fileEnv, ...process.env };
|
||||
}
|
||||
|
||||
if (!existsSync(serverPath)) {
|
||||
console.error(`Zhinian Creation Assistant runtime entry not found: ${serverPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseEnv = loadEnv();
|
||||
const port = baseEnv.PORT || baseEnv.NIANXX_PLAY_PORT || '3000';
|
||||
const hostname = baseEnv.HOSTNAME || '127.0.0.1';
|
||||
const runtimeRoot = baseEnv.NIANXXPLAY_RUNTIME_DIR || join(rootDir, '.runtime');
|
||||
const dataDir = baseEnv.NIANXXPLAY_DATA_DIR || join(runtimeRoot, 'data');
|
||||
const uploadDir = baseEnv.NIANXXPLAY_UPLOAD_DIR || join(runtimeRoot, 'uploads');
|
||||
const resultDir = baseEnv.NIANXXPLAY_RESULT_DIR || join(runtimeRoot, 'generated-results');
|
||||
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
mkdirSync(uploadDir, { recursive: true });
|
||||
mkdirSync(resultDir, { recursive: true });
|
||||
|
||||
const env = {
|
||||
...baseEnv,
|
||||
PORT: String(port),
|
||||
HOSTNAME: hostname,
|
||||
NODE_ENV: 'production',
|
||||
NEXT_TELEMETRY_DISABLED: '1',
|
||||
NIANXXPLAY_RUNTIME_DIR: runtimeRoot,
|
||||
NIANXXPLAY_DATA_DIR: dataDir,
|
||||
NIANXXPLAY_UPLOAD_DIR: uploadDir,
|
||||
NIANXXPLAY_RESULT_DIR: resultDir,
|
||||
NIANXXPLAY_PUBLIC_BASE_URL: baseEnv.NIANXXPLAY_PUBLIC_BASE_URL || `http://${hostname}:${port}`,
|
||||
NIANXXPLAY_DESKTOP_MANAGED: '1',
|
||||
};
|
||||
|
||||
console.log(`[Zhinian Creation Assistant] Starting on http://${hostname}:${port}`);
|
||||
console.log(`[Zhinian Creation Assistant] Runtime data: ${runtimeRoot}`);
|
||||
|
||||
const child = spawn(process.execPath, [serverPath], {
|
||||
cwd: runtimeDir,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
function stop(signal) {
|
||||
if (!child.killed) child.kill(signal);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => stop('SIGINT'));
|
||||
process.on('SIGTERM', () => stop('SIGTERM'));
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
46
scripts/worker.mjs
Executable file
46
scripts/worker.mjs
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const baseUrl = (process.env.ZHINIAN_WORKER_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || "http://127.0.0.1:3000").replace(/\/$/, "");
|
||||
const token = process.env.ZHINIAN_INTERNAL_WORKER_TOKEN || "";
|
||||
const intervalMs = positiveInt(process.env.ZHINIAN_WORKER_INTERVAL_MS, 5000);
|
||||
const limit = positiveInt(process.env.ZHINIAN_WORKER_BATCH_SIZE, 3);
|
||||
const once = process.argv.includes("--once");
|
||||
const workerId = process.env.ZHINIAN_WORKER_ID || `worker-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
async function tick() {
|
||||
const response = await fetch(`${baseUrl}/api/internal/worker/tick`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { "X-Zhinian-Worker-Token": token } : {})
|
||||
},
|
||||
body: JSON.stringify({ workerId, limit })
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`Worker tick failed: ${response.status} ${text}`);
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function run() {
|
||||
do {
|
||||
try {
|
||||
const result = await tick();
|
||||
console.log(`[zhinian-worker] claimed=${result.claimed || 0} worker=${result.workerId || workerId}`);
|
||||
} catch (error) {
|
||||
console.error(`[zhinian-worker] ${error instanceof Error ? error.message : String(error)}`);
|
||||
if (once) process.exitCode = 1;
|
||||
}
|
||||
if (!once) await sleep(intervalMs);
|
||||
} while (!once);
|
||||
}
|
||||
|
||||
function positiveInt(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user