feat: 资源部署

This commit is contained in:
R524809
2026-02-02 13:19:46 +08:00
parent 27f7404816
commit b13ab6f9fb
7 changed files with 359 additions and 16 deletions
+6
View File
@@ -7,5 +7,11 @@ node_modules/
# voice
data/voice/*
# 本地部署拷贝目录(由 deploy-assets 生成)
pipi-assets/
# 生成的 nginx 配置(含机器相关路径,每次运行脚本会重新生成)
scripts/nginx-pipi-assets.conf
# 输出文件(可选,如果不想提交生成的数据)
# data/char/char_introduce.json
+25
View File
@@ -0,0 +1,25 @@
# 生成资源
pnpm generate-introduce 生成简介。混元生成的简介质量不高,自己使用Cursor的tab自动补全,反而质量更高一些
pnpm generate-voice 根据 generate-introduce 生成的简介,来生成音频。
# 本地开发(拷贝到 pipi-assets,并生成 nginx 配置)
pnpm run deploy-assets
# 或
pnpm run deploy-assets:local
# 服务器部署前(拷贝到 /var/www/pipi-assets,并生成对应 nginx 配置)
pnpm run deploy-assets:server
# 自定义目标目录
node scripts/deploy-assets.js --target /path/to/dir
# 指定 nginx 配置输出路径
node scripts/deploy-assets.js --server --nginx /etc/nginx/sites-available/pipi-assets
+4 -1
View File
@@ -6,7 +6,10 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"generate-introduce": "cross-env node scripts/generate_char_introduce.js",
"generate-voice": "node scripts/generate_char_voice.js"
"generate-voice": "node scripts/generate_char_voice.js",
"deploy-assets": "node scripts/deploy-assets.js",
"deploy-assets:local": "node scripts/deploy-assets.js --local",
"deploy-assets:server": "node scripts/deploy-assets.js --server"
},
"keywords": [],
"author": "",
@@ -0,0 +1,129 @@
# 用本机 Nginx 测试 pipi-assets 配置
按顺序做下面几步即可。
---
## 1. 检查是否已安装 Nginx
在终端执行:
```bash
nginx -v
```
- **有版本号**(如 `nginx version: nginx/1.27.5`)→ 已安装,跳到第 2 步。
- **报错 command not found** → 需要先安装。Mac 推荐用 Homebrew
```bash
brew install nginx
```
---
## 2. 确认资源目录存在
本配置会使用项目下的 `pipi-assets` 目录。若还没有,先执行一次:
```bash
cd platform-pipi/packages/voice-production
pnpm run deploy-assets:local
```
这样会生成 `pipi-assets/`(内含 `char/`、`voice/` 等)。
---
## 3. 让主 Nginx 加载本项目的配置
你的 Nginx 主配置在 **Homebrew** 安装时一般是:
- Apple Silicon`/opt/homebrew/etc/nginx/nginx.conf`
- Intel`/usr/local/etc/nginx/nginx.conf`
任选一种方式,让主配置把「本项目里的 pipi-assets 配置」包含进去。
### 方式 A:在主配置里加一行 include(推荐)
1. 用编辑器打开主配置,例如:
```bash
open -e /opt/homebrew/etc/nginx/nginx.conf
```
(Intel 机器把路径改成 `/usr/local/etc/nginx/nginx.conf`
2. 在 `http { ... }` 块里找合适位置(例如在 `include servers/*;` 那一行**上面**),加一行:
```nginx
include /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/scripts/nginx-pipi-assets-local.conf;
```
注意把路径改成你本机实际的项目路径。
3. 保存退出。
### 方式 B:放到 Nginx 的 servers 目录
1. 若没有 `servers` 目录,先建一个:
```bash
sudo mkdir -p /opt/homebrew/etc/nginx/servers
```
Intel 用 `/usr/local/etc/nginx/servers`
2. 做一份软链,指向项目里的本地配置:
```bash
sudo ln -sf /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/scripts/nginx-pipi-assets-local.conf /opt/homebrew/etc/nginx/servers/pipi-assets.conf
```
主配置里已有 `include servers/*;` 时会自动加载。
---
## 4. 检查配置并重载 Nginx
```bash
# 检查语法是否正确
nginx -t
# 若无报错,重载 Nginx(不中断已有连接)
nginx -s reload
```
若你是用 `brew services start nginx` 启用的,可以改为:
```bash
brew services restart nginx
```
---
## 5. 在浏览器或终端里测试
本地站点监听 **8081 端口**,用 `localhost` 即可访问(无需改 hosts)。
- 健康检查:
**http://localhost:8081/health**
应看到页面内容:`ok`
- 角色 JSON
**http://localhost:8081/char/char_introduce.json**
应返回 JSON 内容
- 语音文件(示例):
**http://localhost:8081/voice/一\_4e00.mp3**
应能播放或下载
终端快速验证:
```bash
curl -s http://localhost:8081/health
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/char/char_introduce.json
```
---
## 说明
- **端口**:使用 8081 是为了不和 80 端口上已有的其他站点(如 simulation.data-platform.local 等)冲突。
- **配置文件**`nginx-pipi-assets.conf` 是给服务器部署用的(80 端口 + 二级域名);`nginx-pipi-assets-local.conf` 是给本机测试用的(8081 + localhost),路径已指向项目下的 `pipi-assets`。
@@ -0,0 +1,140 @@
#!/usr/bin/env node
/**
* 将 data 目录内容拷贝到指定目录(本地开发或服务器),并生成对应 nginx 配置。
*
* 用法:
* node scripts/deploy-assets.js # 默认:拷贝到本地 pipi-assets
* node scripts/deploy-assets.js --local # 同上
* node scripts/deploy-assets.js --server # 拷贝到 /var/www/pipi-assets/
* node scripts/deploy-assets.js --target /path/to/dir # 自定义目标目录
*
* 每次运行都会生成 nginx 配置(root 为当前目标目录),默认输出到 scripts/nginx-pipi-assets.conf。
* 使用 --nginx /path/to/file 可指定 nginx 配置输出路径。
*/
const fs = require('fs');
const path = require('path');
const PACKAGE_ROOT = path.resolve(__dirname, '..');
const DATA_DIR = path.join(PACKAGE_ROOT, 'data');
const LOCAL_TARGET = path.join(PACKAGE_ROOT, 'pipi-assets');
const SERVER_TARGET = '/var/www/pipi-assets';
const DEFAULT_NGINX_OUTPUT = path.join(__dirname, 'nginx-pipi-assets.conf');
function parseArgs() {
const args = process.argv.slice(2);
let target = null;
let nginxOutput = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--local' || args[i] === '--server') {
if (target) {
console.warn('警告: 多次指定目标,以最后一次为准');
}
target = args[i] === '--local' ? LOCAL_TARGET : SERVER_TARGET;
} else if (args[i] === '--target' && args[i + 1]) {
target = path.resolve(args[++i]);
} else if (args[i] === '--nginx' && args[i + 1] && !args[i + 1].startsWith('--')) {
nginxOutput = path.resolve(args[++i]);
}
}
if (target === null) {
target = LOCAL_TARGET;
}
return { target, nginxOutput };
}
function copyDirRecursive(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
function deployDataToTarget(targetDir) {
if (!fs.existsSync(DATA_DIR)) {
console.error('错误: data 目录不存在:', DATA_DIR);
process.exit(1);
}
const entries = fs.readdirSync(DATA_DIR, { withFileTypes: true });
if (entries.length === 0) {
console.log('data 目录为空,跳过拷贝');
return;
}
fs.mkdirSync(targetDir, { recursive: true });
for (const entry of entries) {
const srcPath = path.join(DATA_DIR, entry.name);
const destPath = path.join(targetDir, entry.name);
if (entry.isDirectory()) {
if (fs.existsSync(destPath)) {
fs.rmSync(destPath, { recursive: true });
}
copyDirRecursive(srcPath, destPath);
console.log(' 已拷贝:', entry.name + '/');
} else {
fs.copyFileSync(srcPath, destPath);
console.log(' 已拷贝:', entry.name);
}
}
console.log('资源已拷贝到:', targetDir);
}
function generateNginxConfig(rootPath, serverName = 'assets.yourdomain.com') {
return `# pipi-assets 静态资源站点
# 生成后请将 server_name 改为你的二级域名,并视需放到 /etc/nginx/sites-available/ 后启用
server {
listen 80;
server_name ${serverName};
root ${rootPath};
autoindex off;
location /voice/ {
alias ${rootPath}/voice/;
add_header Cache-Control "public, max-age=86400";
}
location /char/ {
alias ${rootPath}/char/;
add_header Cache-Control "public, max-age=3600";
add_header Access-Control-Allow-Origin "*";
}
location /image/ {
alias ${rootPath}/image/;
add_header Cache-Control "public, max-age=86400";
}
location /health {
return 200 "ok";
add_header Content-Type text/plain;
}
}
`;
}
function main() {
const { target, nginxOutput } = parseArgs();
console.log('目标目录:', target);
deployDataToTarget(target);
const nginxPath = nginxOutput ?? DEFAULT_NGINX_OUTPUT;
const config = generateNginxConfig(target);
fs.writeFileSync(nginxPath, config, 'utf8');
console.log('Nginx 配置已写入:', nginxPath);
}
main();
@@ -206,32 +206,41 @@ async function main() {
const charString = JSON.parse(fs.readFileSync(charStringPath, 'utf-8'));
// 获取所有汉字
const chars = charString['3500'].split('');
console.log(`共有 ${chars.length} 个汉字需要处理`);
const allChars = charString['3500'].split('');
console.log(`共有 ${allChars.length} 个汉字需要处理`);
// 检查是否有已存在的进度文件
const outputPath = path.join(__dirname, '../data/char/char_introduce.json');
let results = [];
let startIndex = 0;
let existingResults = [];
const generatedSet = new Set();
if (fs.existsSync(outputPath)) {
try {
results = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
startIndex = results.length;
console.log(`发现已有进度,从第 ${startIndex + 1} 个汉字继续`);
existingResults = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
existingResults.forEach((item) => generatedSet.add(item.char));
console.log(`发现已有 ${existingResults.length} 个汉字介绍`);
} catch (e) {
console.log('已有文件解析失败,从头开始');
}
}
// 过滤出未生成的汉字
const charsToGenerate = allChars.filter((char) => !generatedSet.has(char));
console.log(`其中 ${generatedSet.size} 个已生成,${charsToGenerate.length} 个待生成`);
if (charsToGenerate.length === 0) {
console.log('所有汉字介绍已生成完毕,无需处理');
return;
}
// 批量处理配置
const BATCH_SIZE = 20; // 每次 API 调用处理 20 个汉字
const DELAY_BETWEEN_BATCHES = 1000; // 批次间延迟1秒
for (let i = startIndex; i < chars.length; i += BATCH_SIZE) {
const batch = chars.slice(i, Math.min(i + BATCH_SIZE, chars.length));
for (let i = 0; i < charsToGenerate.length; i += BATCH_SIZE) {
const batch = charsToGenerate.slice(i, Math.min(i + BATCH_SIZE, charsToGenerate.length));
console.log(
`\n处理第 ${i + 1} - ${Math.min(i + BATCH_SIZE, chars.length)} 个汉字(共 ${batch.length} 个)...`
`\n处理第 ${i + 1} - ${Math.min(i + BATCH_SIZE, charsToGenerate.length)} 个汉字(共 ${batch.length} 个)...`
);
console.log(`汉字列表: ${batch.join('')}`);
@@ -243,19 +252,21 @@ async function main() {
console.log(` ${item.char}: ${item.introduce}`);
}
results.push(...batchResults);
// 合并到已有结果
existingResults.push(...batchResults);
batchResults.forEach((item) => generatedSet.add(item.char));
// 每批处理完后保存进度
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf-8');
console.log(`已保存进度:${results.length}/${chars.length}`);
fs.writeFileSync(outputPath, JSON.stringify(existingResults, null, 2), 'utf-8');
console.log(`已保存进度:${existingResults.length}/${allChars.length}`);
// 批次间延迟,避免API限流
if (i + BATCH_SIZE < chars.length) {
if (i + BATCH_SIZE < charsToGenerate.length) {
await delay(DELAY_BETWEEN_BATCHES);
}
}
console.log(`\n完成!共生成 ${results.length} 个汉字介绍`);
console.log(`\n完成!共生成 ${existingResults.length} 个汉字介绍`);
console.log(`结果已保存到: ${outputPath}`);
}
@@ -0,0 +1,29 @@
# pipi-assets 本地测试(端口 8081,避免与 80 端口其他站点冲突)
# 使用方式见同目录下的 NGINX-本地测试说明.md
server {
listen 8081;
server_name localhost;
root /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets;
autoindex off;
location /voice/ {
alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/voice/;
add_header Cache-Control "public, max-age=86400";
}
location /char/ {
alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/char/;
add_header Cache-Control "public, max-age=3600";
add_header Access-Control-Allow-Origin "*";
}
location /image/ {
alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/image/;
add_header Cache-Control "public, max-age=86400";
}
location /health {
return 200 "ok";
add_header Content-Type text/plain;
}
}