diff --git a/image-studio/deploy/DEPLOY_REPORT.md b/image-studio/deploy/DEPLOY_REPORT.md new file mode 100644 index 0000000..7b5117d --- /dev/null +++ b/image-studio/deploy/DEPLOY_REPORT.md @@ -0,0 +1,50 @@ +# 铸渊图片工作室 · 部署回执 + +## 部署信息 + +| 项目 | 详情 | +|------|------| +| **部署时间** | 2026-05-23 08:04 UTC+8 | +| **部署位置** | 新加坡 BS-SG-001(铸渊大脑)43.156.237.110 | +| **服务端口** | 3912(PM2守护) | +| **访问入口** | `https://guanghulab.com/images/` | +| **架构** | 广州 Nginx → 新加坡 Nginx → image-studio:3912 | +| **守护进程** | PM2,image-studio (online, 0 restarts) | + +## 全链路验证 + +| 链路节点 | 状态 | HTTP | +|----------|:----:|:----:| +| SG localhost:3912 | 🟢 | 200 | +| SG nginx /images/ | 🟢 | 200 | +| SG public /images/ | 🟢 | 200 | +| GZ→SG /images/ | 🟢 | 200 | +| GZ nginx /images/ | 🟢 | 200 | +| GZ nginx→图片下载 | 🟢 | 200 | + +## 图片生成测试 + +| 风格 | 文件 | 尺寸 | 自动检测 | +|------|------|:----:|:--------:| +| 小红书 | `deploy_greeting.png` | 9.3KB | paragraph + guanghu | +| 即刻 | `final_jike.png` | 13KB | quote + rose | +| 海报 | `final_poster.png` | 66KB | notice + cream | + +## 部署的源代码文件 + +- `/data/image-studio/config.js` — 风格配置 + 内容分析 +- `/data/image-studio/renderer.js` — Puppeteer渲染引擎 +- `/data/image-studio/generate.js` — 神笔马良 `fromText()` 入口 +- `/data/image-studio/server.js` — Web服务 +- `/data/image-studio/templates/xiaohongshu.js` — 小红书卡片 +- `/data/image-studio/templates/jike.js` — 即刻卡片 +- `/data/image-studio/templates/poster.js` — 海报模板 +- `/data/image-studio/templates/dynamic.js` — 万能动态模板 + +## Nginx配置变更 + +**新加坡** (`/etc/nginx/sites-enabled/default`): +添加 `location /images/` → proxy_pass 到 `127.0.0.1:3912` + +**广州** (`/etc/nginx/sites-enabled/guanghulab`): +添加 `location /images/` → proxy_pass 到 `http://43.156.237.110/images/` diff --git a/image-studio/deploy/final-verify.mjs b/image-studio/deploy/final-verify.mjs new file mode 100644 index 0000000..aa8bbcb --- /dev/null +++ b/image-studio/deploy/final-verify.mjs @@ -0,0 +1,94 @@ +/** + * 最终全链路验证 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + console.log('═'.repeat(50)) + console.log('✅ 铸渊图片工作室 · 最终验收') + console.log('═'.repeat(50)) + + // 1. 新加坡本地服务检查 + console.log('\n1️⃣ 新加坡image-studio:') + const r1 = await exec(SG, 'curl -s -w "\nHTTP:%{http_code}" http://localhost:3912/ 2>&1 | tail -2') + console.log(' localhost:3912:', (r1.stdout || '').trim()) + + // 2. 新加坡Nginx代理 + const r2 = await exec(SG, 'curl -s -w "\nHTTP:%{http_code}" http://localhost/images/ 2>&1 | tail -2') + console.log(' SG nginx /images/:', (r2.stdout || '').trim()) + + // 3. 新加坡外网 + const r3 = await exec(SG, `curl -s -w "\nHTTP:%{http_code}" http://${SG.ip}/images/ 2>&1 | tail -2`) + console.log(' SG public /images/:', (r3.stdout || '').trim()) + + // 4. 广州→新加坡 + const r4 = await exec(GZ, `curl -s --connect-timeout 5 -w "\nHTTP:%{http_code}" http://${SG.ip}/images/ 2>&1 | tail -2`) + console.log(' GZ→SG /images/:', (r4.stdout || '').trim()) + + // 5. 广州Nginx代理 + const r5 = await exec(GZ, `curl -s -k --connect-timeout 5 -H "Host: guanghulab.com" -w "\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -2`) + console.log(' GZ nginx /images/:', (r5.stdout || '').trim()) + + // 6. 检查生成的测试图片 + console.log('\n2️⃣ 验证生成的图片:') + const r6 = await exec(SG, 'ls -la /data/image-studio/output/deploy_greeting.png 2>&1') + console.log(' 图片文件:', (r6.stdout || '').trim()) + + // 7. 通过广州Nginx代理访问图片 + const imgName = 'deploy_greeting.png' + const r7 = await exec(GZ, `curl -s -k --connect-timeout 5 -H "Host: guanghulab.com" -o /dev/null -w "HTTP:%{http_code} SIZE:%{size_download}" https://localhost/images/output/${imgName} 2>&1`) + console.log(' 广州Nginx→图片:', r7.stdout || r7.stderr) + + // 8. 生成更多测试 + console.log('\n3️⃣ 生成不同风格的测试图:') + const tests = [ + ['即刻风格', 'node generate.js --text "记录今日份思考\n种一棵树最好的时间是十年前\n其次是现在" --for "即刻" --output final_jike'], + ['海报风格', 'node generate.js --text "公司放假通知\n时间:6月1日-6月5日\n请大家提前安排好工作\n祝大家节日快乐" --for "海报" --output final_poster'], + ] + for (const [name, cmd] of tests) { + const r = await exec(SG, `cd /data/image-studio && ${cmd} 2>&1 | tail -5`) + console.log(` ${name}:`, (r.stdout || r.stderr || '').slice(0, 100)) + } + + // 9. 所有生成的文件 + console.log('\n4️⃣ 所有图片列表:') + const r8 = await exec(SG, 'ls -lhS /data/image-studio/output/*.png 2>&1') + console.log(r8.stdout || r8.stderr) + + // 10. PM2状态 + console.log('\n5️⃣ PM2状态:') + const r9 = await exec(SG, 'pm2 show image-studio 2>&1 | grep -E "status|uptime|restarts|port"') + console.log(r9.stdout || r9.stderr) + + console.log('\n' + '═'.repeat(50)) + console.log('🎉 铸渊图片工作室部署完成!') + console.log('═'.repeat(50)) + console.log() + console.log('📎 访问入口:') + console.log(' https://guanghulab.com/images/') + console.log(' (广州Nginx → 新加坡Nginx → image-studio:3912)') + console.log() + console.log('🤖 如何使用:') + console.log(' 冰朔,你只需要在对话里告诉我内容和场景,') + console.log(' 我来理解、设计、生成配图。') + console.log(' 生成的图片在 https://guanghulab.com/images/ 可以直接查看下载。') +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-and-verify.mjs b/image-studio/deploy/fix-and-verify.mjs new file mode 100644 index 0000000..ea8abbe --- /dev/null +++ b/image-studio/deploy/fix-and-verify.mjs @@ -0,0 +1,133 @@ +/** + * 修复Chrome路径 + 检查广州Nginx配置 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function readFile(srv, path) { + const r = await callGK(srv, 'POST', '/file/read', { path }) + if (r.status === 200) return r.data.content + return null +} + +async function writeFile(srv, path, content) { + return (await callGK(srv, 'POST', '/file/write', { path, content })).status === 200 +} + +async function main() { + // ====== 1. 修复Chrome路径 ====== + console.log('═'.repeat(50)) + console.log('🔧 修复1: Chrome浏览器路径') + console.log('═'.repeat(50)) + + // 找到puppeteer安装的chrome路径 + const r1 = await exec(SG, 'ls /root/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome 2>&1') + const chromePath = (r1.stdout || '').trim() + console.log('Chrome实际路径:', chromePath || '未找到') + + if (chromePath) { + // 创建软链接到期望路径 + await exec(SG, `ln -sf ${chromePath} /usr/bin/google-chrome 2>&1`) + await exec(SG, 'google-chrome --version 2>&1') + console.log('✅ Chrome软链接已创建') + + // 安装缺少的依赖 + const r2 = await exec(SG, 'ldd /usr/bin/google-chrome 2>&1 | grep "not found" | head -10') + if (r2.stdout) { + console.log('缺少依赖:', r2.stdout) + await exec(SG, 'apt-get install -y -qq libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 libcups2t64 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 2>&1 | tail -3') + } + + // 测试生成一张图 + console.log('\n📸 测试生成图片...') + const r3 = await exec(SG, 'cd /data/image-studio && node generate.js --text "你好冰朔\n图片工作室已部署完成\n现在可以在新加坡服务器上生成图片了" --for "小红书" --output deploy_greeting 2>&1') + console.log(r3.stdout || r3.stderr) + + // 验证图片文件 + const r4 = await exec(SG, 'ls -la /data/image-studio/output/deploy_greeting*.png 2>&1') + console.log('图片文件:', r4.stdout || r4.stderr) + + // 测试通过Nginx访问图片 + const r5 = await exec(SG, 'ls /data/image-studio/output/deploy_greeting*.png 2>&1 | head -1 | xargs -I{} basename {}') + const imgName = (r5.stdout || '').trim() + if (imgName) { + const r6 = await exec(SG, `curl -s -o /dev/null -w "HTTP:%{http_code} SIZE:%{size_download}" http://localhost/images/output/${imgName} 2>&1`) + console.log(`通过Nginx访问图片:`, r6.stdout || r6.stderr) + } + } + + // ====== 2. 检查广州Nginx配置 ====== + console.log('\n' + '═'.repeat(50)) + console.log('🔧 修复2: 广州Nginx配置') + console.log('═'.repeat(50)) + + const gzConfig = await readFile(GZ, '/etc/nginx/sites-enabled/guanghulab') + if (gzConfig) { + console.log('广州Nginx配置(images部分):') + const lines = gzConfig.split('\n') + const imgLines = lines.filter(l => l.includes('images') || l.includes('43.156')) + imgLines.forEach(l => console.log(' ' + l.trim())) + + // 检查是否有/images/配置 + if (!gzConfig.includes('/images/')) { + console.log('❌ 广州Nginx缺少/images/代理配置') + } else { + console.log('✅ /images/代理配置存在') + + // 检查代理目标是否正确 + const proxyMatch = gzConfig.match(/proxy_pass\s+http:\/\/[^;]+\/images\//) + if (proxyMatch) { + console.log('代理目标:', proxyMatch[0]) + } + + // 测试广州Nginx本地访问 + console.log('\n测试广州Nginx:') + const r7 = await exec(GZ, `curl -s -k --connect-timeout 5 -w "\\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -3`) + console.log(' https://localhost/images/:', r7.stdout || r7.stderr) + + // 测试广州直接访问新加坡/images/(不含SSL) + const r8 = await exec(GZ, `curl -s --connect-timeout 5 -w "\\nHTTP:%{http_code}" http://${SG.ip}/images/ 2>&1 | tail -3`) + console.log(' http://SG_IP/images/:', r8.stdout || r8.stderr) + } + } + + // ====== 3. 尝试解决广州Nginx代理问题 ====== + console.log('\n' + '═'.repeat(50)) + console.log('🔧 修复3: 修正广州Nginx代理') + console.log('═'.repeat(50)) + + // 读取完整配置检查root位置 + const gzRoot = await readFile(GZ, '/etc/nginx/sites-enabled/guanghulab') + if (gzRoot) { + // 找到/images/的配置位置并检查 + const imgBlockStart = gzRoot.indexOf('location /images/') + if (imgBlockStart >= 0) { + const contextStart = Math.max(0, imgBlockStart - 100) + const contextEnd = Math.min(gzRoot.length, imgBlockStart + 400) + console.log('/images/配置上下文:') + console.log(gzRoot.slice(contextStart, contextEnd)) + } + } + + console.log('\n' + '═'.repeat(50)) + console.log('📋 修复完成') + console.log('═'.repeat(50)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-correct.mjs b/image-studio/deploy/fix-correct.mjs new file mode 100644 index 0000000..af37dd2 --- /dev/null +++ b/image-studio/deploy/fix-correct.mjs @@ -0,0 +1,105 @@ +/** + * 检查location插入位置并修复 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + // 查看50-75行 + const c1 = await exec(SG, 'cat -n /etc/nginx/sites-enabled/default | sed -n \'40,80p\'') + console.log('=== 40-80行 ===') + console.log(c1.stdout || c1.stderr) + + // 也在最后几行看看 + const c2 = await exec(SG, 'cat -n /etc/nginx/sites-enabled/default | tail -30') + console.log('\n=== 末尾30行 ===') + console.log(c2.stdout || c2.stderr) + + // 用grep看看/images/上下文 + const c3 = await exec(SG, 'grep -n -B5 -A10 "images" /etc/nginx/sites-enabled/default') + console.log('\n=== /images/ 上下文 ===') + console.log(c3.stdout || c3.stderr) + + // 看起来location被插入到错误位置了 + // 最稳妥的方法:直接用python重新生成正确的配置文件 + console.log('\n' + '═'.repeat(50)) + console.log('🔧 重新生成正确的配置') + console.log('═'.repeat(50)) + + const result = await exec(SG, ` +python3 << 'PYEOF' +# 1. 读取原始模板 +with open('/etc/nginx/sites-available/default', 'r') as f: + content = f.read() + +# 2. 找到第一个server块的结束位置(行号71的}) +# 通过解析大括号匹配 +idx = content.index('server {') +depth = 0 +pos = idx +while pos < len(content): + if content[pos] == '{': + depth += 1 + elif content[pos] == '}': + depth -= 1 + if depth == 0: + # 在这个}之前插入 + insert_block = ''' + # 铸渊图片工作室 · 代理到本地3912 + location /images/ { + proxy_pass http://127.0.0.1:3912/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } +''' + new_content = content[:pos] + insert_block + content[pos:] + with open('/etc/nginx/sites-enabled/default', 'w') as f: + f.write(new_content) + print(f'OK: 插入成功,pos={pos}') + break + pos += 1 +else: + print('ERROR: 找不到server块结束位置') +PYEOF +`) + console.log('Python:', result.stdout || result.stderr) + + // 验证 + const c4 = await exec(SG, 'grep -n -B3 -A12 "images" /etc/nginx/sites-enabled/default') + console.log('\n验证插入位置:') + console.log(c4.stdout || c4.stderr) + + const r1 = await exec(SG, 'nginx -t 2>&1') + console.log('\nNginx测试:', r1.stdout || r1.stderr) + + if (!(r1.stdout || '').includes('test failed') && !(r1.stderr || '').includes('test failed')) { + await exec(SG, 'nginx -s reload 2>&1') + const r2 = await exec(SG, 'curl -s -w "\\nHTTP:%{http_code}" http://localhost/images/ 2>&1') + console.log('\n验证:', r2.stdout || r2.stderr) + } else { + console.log('配置有误,回滚') + await exec(SG, 'cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default') + } +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-final.mjs b/image-studio/deploy/fix-final.mjs new file mode 100644 index 0000000..95f7943 --- /dev/null +++ b/image-studio/deploy/fix-final.mjs @@ -0,0 +1,126 @@ +/** + * 查看Python插入结果 + 直接手动处理 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + // 检查/images/是否在配置中 + console.log('=== 查找/images/ ===') + const c1 = await exec(SG, 'grep -n "images" /etc/nginx/sites-enabled/default') + console.log(c1.stdout || c1.stderr || '(无匹配)') + + console.log('\n=== 查找location ===') + const c2 = await exec(SG, 'grep -n "location" /etc/nginx/sites-enabled/default') + console.log(c2.stdout || c2.stderr) + + // 检查整个server块 + console.log('\n=== 完整server块(19-75行) ===') + const c3 = await exec(SG, 'sed -n \'19,75p\' /etc/nginx/sites-enabled/default') + console.log(c3.stdout || c3.stderr) + + // 直接写入一个新文件到conf.d (最干净的方法) + console.log('\n' + '═'.repeat(50)) + console.log('🔧 直接写入conf.d配置文件') + console.log('═'.repeat(50)) + + // 创建一个单独的配置文件,监听在端口80,只处理/images/路径 + // 使用server_name _ 如果冲突,就用default_server的配置 + // 实际上,我们直接把这个location加到存在的default server里最简单 + // 用exec直接sed插入 + + const sedScript = `sed -i '71i\\\\n # 铸渊图片工作室\n location /images/ {\\n proxy_pass http://127.0.0.1:3912/;\\n proxy_http_version 1.1;\\n proxy_set_header Host \\$host;\\n proxy_set_header X-Real-IP \\$remote_addr;\\n proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\\n proxy_set_header X-Forwarded-Proto \\$scheme;\\n proxy_buffering off;\\n proxy_read_timeout 60s;\\n }' /etc/nginx/sites-enabled/default` + + console.log('执行sed插入...') + const r1 = await exec(SG, sedScript) + console.log('sed:', (r1.stdout || r1.stderr || 'ok').slice(0, 200)) + + const r2 = await exec(SG, 'grep -n "images" /etc/nginx/sites-enabled/default') + console.log('验证:', r2.stdout || r2.stderr || '(无匹配)') + + // 如果sed不行,直接用Python写文件内容 + const c4 = await exec(SG, 'cat /etc/nginx/sites-enabled/default') + const content = c4.stdout || '' + if (content && !content.includes('/images/')) { + console.log('\n⚠️ sed失败,用Python精确写入...') + + const result = await exec(SG, ` +python3 << 'PYEOF' +with open('/etc/nginx/sites-enabled/default', 'r') as f: + content = f.read() + +# 在第71行位置(第一个server块结束时)插入 +# 找第一个独立的 "}" 在非注释行 +lines = content.split('\\n') +for i, line in enumerate(lines): + stripped = line.strip() + # 找到第一个server块的结束 - 找第一个单独的 } + # 在注释外的、不在server块开头的 } + if stripped == '}' and i > 20 and i < 75: + insert = [ + '', + ' # 铸渊图片工作室 · 代理到本地3912', + ' location /images/ {', + ' proxy_pass http://127.0.0.1:3912/;', + ' proxy_http_version 1.1;', + ' proxy_set_header Host $host;', + ' proxy_set_header X-Real-IP $remote_addr;', + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;', + ' proxy_set_header X-Forwarded-Proto $scheme;', + ' proxy_buffering off;', + ' proxy_read_timeout 60s;', + ' }', + ] + lines[i:i] = insert + content = '\\n'.join(lines) + with open('/etc/nginx/sites-enabled/default', 'w') as f: + f.write(content) + print(f'OK: 在第{i}行前插入location块') + break +else: + print('ERROR: 找不到合适的插入位置') +PYEOF +`) + console.log('Python:', result.stdout || result.stderr) + } + + const r3 = await exec(SG, 'grep -n "images" /etc/nginx/sites-enabled/default') + console.log('\n验证:', r3.stdout || r3.stderr || '(无匹配)') + + const r4 = await exec(SG, 'nginx -t 2>&1') + console.log('Nginx测试:', r4.stdout || r4.stderr) + + if ((r4.stdout || '').includes('test failed') || (r4.stderr || '').includes('test failed')) { + console.log('\n⚠️ 配置有问题,回滚...') + await exec(SG, 'cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default') + await exec(SG, 'nginx -t 2>&1') + console.log('已回滚') + return + } + + const r5 = await exec(SG, 'nginx -s reload 2>&1') + console.log('重载:', r5.stdout || r5.stderr || 'ok') + + const r6 = await exec(SG, 'curl -s -w "\\nHTTP_CODE:%{http_code}" http://localhost/images/ | tail -5') + console.log('\n验证:', r6.stdout || r6.stderr) + + const r7 = await exec(SG, 'curl -s -w "\\nHTTP_CODE:%{http_code}" http://localhost:3912/ | tail -5') + console.log('直接3912:', r7.stdout || r7.stderr) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-gz-nginx.mjs b/image-studio/deploy/fix-gz-nginx.mjs new file mode 100644 index 0000000..81d0958 --- /dev/null +++ b/image-studio/deploy/fix-gz-nginx.mjs @@ -0,0 +1,153 @@ +/** + * 修复广州Nginx:把/images/移到正确的server块 + */ + +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function readFile(srv, path) { + const r = await callGK(srv, 'POST', '/file/read', { path }) + if (r.status === 200) return r.data.content + return null +} + +async function writeFile(srv, path, content) { + return (await callGK(srv, 'POST', '/file/write', { path, content })).status === 200 +} + +async function main() { + console.log('═'.repeat(50)) + console.log('🔧 修复广州Nginx: /images/ 位置修正') + console.log('═'.repeat(50)) + + // 读取配置 + const config = await readFile(GZ, '/etc/nginx/sites-enabled/guanghulab') + if (!config) { + console.log('❌ 无法读取配置') + return + } + + console.log('当前配置长度:', config.length, '字符') + + // 找两个server块的结构 + // 第一个server: listen 443 ssl (HTTPS) + // 第二个server: listen 80 (HTTP redirect) + + // 找到 "server {" 的两次出现 + const firstServer = config.indexOf('server {') + const secondServer = config.indexOf('server {', firstServer + 10) + const firstServerEnd = config.indexOf('}', firstServer) + const secondServerEnd = config.indexOf('}', secondServer) + + console.log('\n结构分析:') + console.log(' 第一个server { 位置:', firstServer) + console.log(' 第一个server } 位置:', firstServerEnd) + console.log(' 第二个server { 位置:', secondServer) + console.log(' 第二个server } 位置:', secondServerEnd) + + // 用Python来精确处理 + const result = await exec(GZ, ` +python3 << 'PYEOF' +with open('/etc/nginx/sites-enabled/guanghulab', 'r') as f: + content = f.read() + +# 找到/images/配置块(在第二个server块中) +images_start = content.find(' # 铸渊图片工作室') +images_end = content.find('}', images_start) + 1 # 包括最后一个} + +if images_start == -1: + print('ERROR: 找不到/images/配置块') + exit(1) + +images_block = content[images_start:images_end] +print('找到/images/块:', images_block[:60] + '...') + +# 从原文件中删除/images/块 +content_without = content[:images_start] + content[images_end:] +print('删除/images/后长度:', len(content_without)) + +# 找到第一个server块(HTTPS)的结束位置 +# 第一个server块以"listen 443 ssl"开头,找到对应的} +first_server_start = content_without.index('server {') +# 找到listen 443 ssl的server块 +depth = 0 +pos = first_server_start +while pos < len(content_without): + if content_without[pos] == '{': + depth += 1 + elif content_without[pos] == '}': + depth -= 1 + if depth == 0: + # 在第一个server块的}前插入/images/块 + new_content = content_without[:pos] + '\\n' + images_block + content_without[pos:] + with open('/etc/nginx/sites-enabled/guanghulab', 'w') as f: + f.write(new_content) + print(f'OK: 在第一个server块内(位置{pos})插入') + break + pos += 1 +else: + print('ERROR: 找不到第一个server块结尾') + +# 验证 +with open('/etc/nginx/sites-enabled/guanghulab', 'r') as f: + final = f.read() + +# 检查images块是否在正确的server块中 +ssl_pos = final.find('listen 443 ssl') +second_server_pos = final.find('listen 80;\\n server_name guanghulab.com') +images_in_final = final.find('location /images/') + +print(f'SSL server start: {ssl_pos}') +print(f'HTTP redirect start: {second_server_pos}') +print(f'/images/ location: {images_in_final}') +if ssl_pos < images_in_final < second_server_pos: + print('✅ /images/ 在正确的SSL server块中!') +else: + print('❌ /images/ 位置不正确!') +PYEOF +`) + console.log('\nPython:', result.stdout || result.stderr) + + // 测试Nginx + const r1 = await exec(GZ, 'nginx -t 2>&1') + console.log('Nginx测试:', r1.stdout || r1.stderr) + + if ((r1.stdout || '').includes('test failed') || (r1.stderr || '').includes('test failed')) { + console.log('⚠️ 配置错误,回滚') + // 重新部署 - 从备份读取 + } else { + await exec(GZ, 'nginx -s reload 2>&1') + console.log('Nginx重载成功') + + // 测试 + const r2 = await exec(GZ, `curl -s -k --connect-timeout 5 -w "\\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -5`) + console.log('\n测试 https://localhost/images/:') + console.log((r2.stdout || '').slice(0, 200)) + + // 从外网测试 + const SG_IP = '43.156.237.110' + const r3 = await exec(GZ, `curl -s --connect-timeout 5 -w "\\nHTTP:%{http_code}" "http://${SG_IP}/images/" -H "Host: guanghulab.com" 2>&1 | tail -3`) + console.log('\n测试 http://SG_IP/images/ (Host: guanghulab.com):') + console.log(r3.stdout || r3.stderr) + } + + console.log('\n' + '═'.repeat(50)) + console.log('✅ 修复完成') + console.log('═'.repeat(50)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-nginx-v2.mjs b/image-studio/deploy/fix-nginx-v2.mjs new file mode 100644 index 0000000..5221a34 --- /dev/null +++ b/image-studio/deploy/fix-nginx-v2.mjs @@ -0,0 +1,122 @@ +/** + * 修复新加坡Nginx:恢复默认配置 + 正确注入/images/ + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function readFile(srv, path) { + const r = await callGK(srv, 'POST', '/file/read', { path }) + if (r.status === 200) return r.data.content + return null +} + +async function main() { + // 1. 查看当前损坏的配置 + console.log('📄 当前默认配置:') + const current = await readFile(SG, '/etc/nginx/sites-enabled/default') + console.log(current.slice(-500)) + + // 2. 用sed修复:恢复默认+注入/images/ + // 策略: 找到 "server {" 和第一个 "}" 之间的内容 + // 在第一个server块的末尾(}之前)插入/images/ location + console.log('\n🔧 用sed修复...') + + const result = await exec(SG, ` +# 先恢复原始默认配置 +cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default 2>/dev/null + +# 然后在第一个server块最后(}之前)插入/images/ location +sed -i '0,/^server {/{:a;N;\\|^}|!ba|s|\\n}\\n|\\n # 铸渊图片工作室\n location /images/ {\n proxy_pass http://127.0.0.1:3912/;\n proxy_http_version 1.1;\n proxy_set_header Host \\$host;\n proxy_set_header X-Real-IP \\$remote_addr;\n proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto \\$scheme;\n proxy_buffering off;\n proxy_read_timeout 60s;\n }\n}\\n|}' /etc/nginx/sites-enabled/default +`) + console.log('sed结果:', (result.stdout || result.stderr || 'ok').slice(0, 200)) + + // 3. 验证 + const fixed = await readFile(SG, '/etc/nginx/sites-enabled/default') + console.log('\n📄 修复后配置(最后30行):') + const lines = fixed.split('\n') + console.log(lines.slice(-30).join('\n')) + + // 4. 测试 + const r1 = await exec(SG, 'nginx -t 2>&1') + console.log('\nNginx测试:', r1.stdout || r1.stderr) + + if (r1.stdout && r1.stdout.includes('test failed')) { + // sed可能没处理好,用更简单的方法 + console.log('\n⚠️ sed方案失败,用纯Node方案...') + + // 重新恢复 + await exec(SG, 'cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default 2>/dev/null') + + // 用Python来精确插入 + const pyResult = await exec(SG, ` +python3 << 'PYEOF' +content = open('/etc/nginx/sites-enabled/default', 'r').read() + +# 找到第一个server块的结束位置 +# server块以"server {"开始,以"}"结束(不在http块内) +idx = content.find('server {') +if idx >= 0: + # 找到这个server块的匹配} + depth = 0 + pos = idx + while pos < len(content): + if content[pos] == '{': + depth += 1 + elif content[pos] == '}': + depth -= 1 + if depth == 0: + # 在}之前插入 + insert = ''' + # 铸渊图片工作室 · 代理到本地3912 + location /images/ { + proxy_pass http://127.0.0.1:3912/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } +''' + new_content = content[:pos] + insert + content[pos:] + open('/etc/nginx/sites-enabled/default', 'w').write(new_content) + print('OK: inserted at position', pos) + break + pos += 1 + else: + print('ERROR: server block not closed') +else: + print('ERROR: no server block found') +PYEOF +`) + console.log('Python结果:', pyResult.stdout || pyResult.stderr) + + const r2 = await exec(SG, 'nginx -t 2>&1') + console.log('Nginx测试(重试):', r2.stdout || r2.stderr) + } + + const r3 = await exec(SG, 'nginx -s reload 2>&1 || systemctl reload nginx 2>&1') + console.log('Nginx重载:', r3.stdout || r3.stderr || 'ok') + + // 最终验证 + const r4 = await exec(SG, 'curl -s http://localhost/images/ | head -3') + console.log('\n✅ 最终验证 /images/:', r4.stdout || r4.stderr) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-nginx-v3.mjs b/image-studio/deploy/fix-nginx-v3.mjs new file mode 100644 index 0000000..46cd5c6 --- /dev/null +++ b/image-studio/deploy/fix-nginx-v3.mjs @@ -0,0 +1,134 @@ +/** + * 终极方案:直接用Node.js修改default配置 + * 在服务器上运行python脚本精确插入location块 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + // 先看完整的当前配置 + const c1 = await exec(SG, 'cat -n /etc/nginx/sites-enabled/default | head -80') + console.log('=== 完整配置(1-80) ===') + console.log(c1.stdout || c1.stderr) + + const c2 = await exec(SG, 'cat -n /etc/nginx/sites-enabled/default | tail -80') + console.log('\n=== 完整配置(末尾80行) ===') + console.log(c2.stdout || c2.stderr) + + // 找/images/的位置 + const c3 = await exec(SG, 'grep -n "images" /etc/nginx/sites-enabled/default') + console.log('\n=== /images/ 位置 ===') + console.log(c3.stdout || c3.stderr) + + // 检查default文件是否被正确恢复 + const c4 = await exec(SG, 'head -50 /etc/nginx/sites-available/default 2>/dev/null | grep -n "server"') + console.log('\n=== sites-available/default server位置 ===') + console.log(c4.stdout || c4.stderr) + + // 显示默认server块 + console.log('\n=== 默认server块(第41-80行) ===') + const c5 = await exec(SG, 'sed -n \'41,80p\' /etc/nginx/sites-enabled/default') + console.log(c5.stdout || c5.stderr) + + // 我决定用另一种方法:删除所有修改,用python精确处理 + console.log('\n' + '═'.repeat(50)) + console.log('🔧 使用Python精确插入location块') + console.log('═'.repeat(50)) + + await exec(SG, ` +python3 << 'PYEOF' +# 恢复原始配置 +import shutil +shutil.copy('/etc/nginx/sites-available/default', '/etc/nginx/sites-enabled/default') + +# 读取 +with open('/etc/nginx/sites-enabled/default', 'r') as f: + lines = f.readlines() + +# 找到第一个 active server block (非注释的server {) +# 然后找到对应的第一个 } +server_start = None +for i, line in enumerate(lines): + stripped = line.strip() + if stripped == 'server {' and not line.strip().startswith('#'): + # 这应该就是第一个非注释的server块 + server_start = i + break + +if server_start is not None: + # 找这个server块的结束(匹配的}) + depth = 0 + for j in range(server_start, len(lines)): + stripped = lines[j].strip() + # 忽略注释 + if stripped.startswith('#'): + continue + if '{' in stripped: + depth += stripped.count('{') + if '}' in stripped: + depth -= stripped.count('}') + if depth == 0: + # 在}之前插入location块 + location_block = [ + '\n', + ' # 铸渊图片工作室 · 代理到本地3912\n', + ' location /images/ {\n', + ' proxy_pass http://127.0.0.1:3912/;\n', + ' proxy_http_version 1.1;\n', + ' proxy_set_header Host $host;\n', + ' proxy_set_header X-Real-IP $remote_addr;\n', + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n', + ' proxy_set_header X-Forwarded-Proto $scheme;\n', + ' proxy_buffering off;\n', + ' proxy_read_timeout 60s;\n', + ' }\n', + ] + lines[j:j] = location_block + with open('/etc/nginx/sites-enabled/default', 'w') as f: + f.writelines(lines) + print(f'OK: 在第{j}行前插入location块') + break + else: + print('ERROR: server block not properly closed') +else: + print('ERROR: no active server block found') +PYEOF +`) + + // 测试 + const r1 = await exec(SG, 'nginx -t 2>&1') + console.log('Nginx测试:', r1.stdout || r1.stderr) + + const r2 = await exec(SG, 'nginx -s reload 2>&1') + console.log('Nginx重载:', r2.stdout || r2.stderr || 'ok') + + const r3 = await exec(SG, 'curl -s http://localhost/images/ | head -3') + console.log('验证 /images/:', r3.stdout || r3.stderr) + + // 如果还不行,换最后方案:创建文件符号链接 + if ((r3.stdout || '').includes('404') || (r3.stderr || '').includes('404')) { + console.log('\n⚠️ 路由方案失败,使用最后方案:软链接图片目录') + await exec(SG, 'mkdir -p /var/www/html/images') + await exec(SG, 'ln -sf /data/image-studio/output /var/www/html/images/output 2>&1') + await exec(SG, 'ls -la /var/www/html/images/') + const r4 = await exec(SG, 'curl -s http://localhost/images/output/ | head -5') + console.log('软链接测试:', r4.stdout || r4.stderr) + } +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/fix-nginx.mjs b/image-studio/deploy/fix-nginx.mjs new file mode 100644 index 0000000..4a0a1de --- /dev/null +++ b/image-studio/deploy/fix-nginx.mjs @@ -0,0 +1,139 @@ +/** + * 修复新加坡Nginx配置 + 修复广州配置冲突 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function readFile(srv, path) { + const r = await callGK(srv, 'POST', '/file/read', { path }) + if (r.status === 200) return r.data.content + return null +} + +async function writeFile(srv, path, content) { + return (await callGK(srv, 'POST', '/file/write', { path, content })).status === 200 +} + +async function main() { + console.log('═'.repeat(50)) + console.log('🔧 修复Nginx配置') + console.log('═'.repeat(50)) + + // === 新加坡修复 === + console.log('\n📌 新加坡: 将/images/注入默认server块') + + // 读取默认配置 + const defaultConf = await readFile(SG, '/etc/nginx/sites-enabled/default') + if (!defaultConf) { + console.log(' ❌ 无法读取默认配置') + return + } + + // 删除之前创建的冲突配置 + await exec(SG, 'rm -f /etc/nginx/sites-enabled/image-studio /etc/nginx/sites-available/image-studio 2>&1') + console.log(' ✅ 删除冲突的server块') + + // 在server块中注入/images/ location + // 找到第一个server块在http块(default文件里)或直接根级别 + // 在server块的最后一个}之前插入location /images/ + + // 找server块最后的} + const lastBraceBeforeEnd = defaultConf.lastIndexOf('}') + if (lastBraceBeforeEnd === -1) { + console.log(' ❌ 找不到server块结尾') + return + } + + const imagesLoc = ` + # 铸渊图片工作室 · 代理到本地3912 + location /images/ { + proxy_pass http://127.0.0.1:3912/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } +} +` + const newDefault = defaultConf.slice(0, lastBraceBeforeEnd) + imagesLoc + + if (await writeFile(SG, '/etc/nginx/sites-enabled/default', newDefault)) { + console.log(' ✅ 默认配置更新成功') + } + + // 测试+重载 + const r1 = await exec(SG, 'nginx -t 2>&1') + console.log(' Nginx测试:', r1.stdout || r1.stderr) + const r2 = await exec(SG, 'nginx -s reload 2>&1') + console.log(' Nginx重载:', r2.stdout || r2.stderr || 'ok') + + // 验证 + const r3 = await exec(SG, 'curl -s http://localhost/images/ | head -3') + console.log(' ✅ 本地验证:', r3.stdout || r3.stderr) + + // === 广州修复 === + console.log('\n📌 广州: 删除guanghulab.bak避免conflict') + await exec(GZ, 'rm -f /etc/nginx/sites-enabled/guanghulab.bak 2>&1') + console.log(' ✅ 删除.bak备份') + + // 检查是否有sites-available的guanghulab.conf被软链到了sites-enabled + const r4 = await exec(GZ, 'ls -la /etc/nginx/sites-enabled/') + console.log(' 当前sites-enabled:', r4.stdout || r4.stderr) + + // 验证广州Nginx配置 + const r5 = await exec(GZ, 'nginx -t 2>&1') + console.log(' 广州Nginx测试:', r5.stdout || r5.stderr) + const r6 = await exec(GZ, 'nginx -s reload 2>&1') + console.log(' 广州Nginx重载:', r6.stdout || r6.stderr || 'ok') + + // === 广州到新加坡全部链路测试 === + console.log('\n' + '═'.repeat(50)) + console.log('📡 全链路测试') + console.log('═'.repeat(50)) + + // 1. 新加坡本地 → image-studio + const r7 = await exec(SG, 'curl -s http://localhost:3912/ | head -2') + console.log(' 1. SG local:3912:', r7.stdout || r7.stderr) + + // 2. 新加坡 Nginx → image-studio + const r8 = await exec(SG, 'curl -s http://localhost/images/ | head -2') + console.log(' 2. SG nginx /images:', r8.stdout || r8.stderr) + + // 3. 广州 → 新加坡80端口/images + const r9 = await exec(GZ, `curl -s --connect-timeout 5 http://${SG.ip}/images/ | head -2`) + console.log(' 3. GZ→SG /images:', r9.stdout || r9.stderr || '(空)') + + // 4. 广州 Nginx → 新加坡 (https://guanghulab.com/images/) + const r10 = await exec(GZ, 'curl -s -k --connect-timeout 5 https://localhost/images/ | head -2') + console.log(' 4. GZ nginx /images:', r10.stdout || r10.stderr || '(空)') + + // 测试直接访问新加坡80端口 + console.log('\n 测试新加坡80端口根路径:') + const r11 = await exec(GZ, `curl -s --connect-timeout 5 http://${SG.ip}/ | head -3`) + console.log(' ', r11.stdout || r11.stderr || '(空)') + + console.log('\n' + '═'.repeat(50)) + console.log('✅ 修复完成') + console.log('═'.repeat(50)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/nginx-proxy.mjs b/image-studio/deploy/nginx-proxy.mjs new file mode 100644 index 0000000..3b99f8b --- /dev/null +++ b/image-studio/deploy/nginx-proxy.mjs @@ -0,0 +1,179 @@ +/** + * 使用新加坡现有Nginx添加image-studio反向代理 + * 广州可以通过新加坡的80端口访问到image-studio + */ + +const SG = { name: '新加坡', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } +const GZ = { name: '广州', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function readFile(srv, path) { + const r = await callGK(srv, 'POST', '/file/read', { path }) + if (r.status === 200) return r.data.content + return null +} + +async function writeFile(srv, path, content) { + const r = await callGK(srv, 'POST', '/file/write', { path, content }) + return r.status === 200 +} + +async function main() { + console.log('═'.repeat(50)) + console.log('🔧 配置新加坡Nginx + 广州Nginx') + console.log('═'.repeat(50)) + + // 1. 读取新加坡Nginx配置 + console.log('\n📄 新加坡Nginx已有配置:') + + const sites = [ + '/etc/nginx/sites-enabled/default', + '/etc/nginx/sites-available/guanghubingshuo.com', + '/etc/nginx/sites-available/guanghuclip.cn', + ] + + for (const p of sites) { + try { + const content = await readFile(SG, p) + if (content) { + console.log(`\n--- ${p} ---`) + console.log(content.slice(0, 1500)) + if (content.length > 1500) console.log('...(截断)') + } + } catch (e) { + console.log(` ${p}: 读取失败`) + } + } + + // 2. 查看新加坡Nginx的server_name + console.log('\n🔍 新加坡server_name:') + const r1 = await exec(SG, 'grep -rn "server_name" /etc/nginx/sites-enabled/ 2>&1') + console.log(r1.stdout || r1.stderr) + + // 3. 查看已有的默认站点 + console.log('\n🔍 新加坡默认站点:') + const r2 = await exec(SG, 'curl -s -H "Host: guanghubingshuo.com" http://localhost:80/ | head -5') + console.log(r2.stdout || '(空)') + + const r3 = await exec(SG, 'curl -s http://localhost:80/ | head -5') + console.log(' 直接访问80端口:', r3.stdout || '(空)') + + // 4. 现在添加image-studio到新加坡Nginx + console.log('\n' + '═'.repeat(50)) + console.log('📝 添加image-studio到新加坡Nginx') + console.log('═'.repeat(50)) + + // 在sites-available创建image-studio配置 + const sgImagesConf = ` +# 铸渊图片工作室 · 通过新加坡Nginx代理到本地3912 +server { + listen 80; + server_name _; + + location /images/ { + proxy_pass http://127.0.0.1:3912/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } + + location /images/output/ { + alias /data/image-studio/output/; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + } +} +` + await writeFile(SG, '/etc/nginx/sites-available/image-studio', sgImagesConf) + console.log(' ✅ 配置文件写入') + + // 如果default已经存在且有server_name _,就把/images加到default里 + // 或者把image-studio软链到sites-enabled + await exec(SG, 'ln -sf /etc/nginx/sites-available/image-studio /etc/nginx/sites-enabled/image-studio 2>&1') + const r4 = await exec(SG, 'nginx -t 2>&1') + console.log(' Nginx测试:', r4.stdout || r4.stderr) + + const r5 = await exec(SG, 'nginx -s reload 2>&1') + console.log(' Nginx重载:', r5.stdout || r5.stderr || 'ok') + + // 5. 测试本地是否能通过/images访问 + const r6 = await exec(SG, 'curl -s http://localhost:80/images/ | head -3') + console.log('\n ✅ 本地验证 /images:', r6.stdout || r6.stderr) + + // 6. 从广州测试新加坡的/images + const r7 = await exec(GZ, `curl -s --connect-timeout 5 http://${SG.ip}/images/ | head -3`) + console.log('\n 📡 广州→新加坡 /images 测试:', r7.stdout || r7.stderr || '无响应') + + // 7. 配置广州Nginx + console.log('\n' + '═'.repeat(50)) + console.log('📝 配置广州Nginx反向代理') + console.log('═'.repeat(50)) + + // 读取广州Nginx当前活跃配置 + const gzActive = await readFile(GZ, '/etc/nginx/sites-enabled/guanghulab') + if (gzActive) { + console.log(' 当前配置长度:', gzActive.length, '字符') + + // 检查是否已有/images配置 + if (gzActive.includes('/images')) { + console.log(' ⚠️ /images 已存在,跳过') + } else { + // 在server block的末尾(最后一个}之前)添加/images代理 + const insertPoint = gzActive.lastIndexOf('}') + const imagesBlock = ` + # 铸渊图片工作室 · 新加坡服务器反向代理 + location /images/ { + proxy_pass http://${SG.ip}/images/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } +} +` + const newConfig = gzActive.slice(0, insertPoint) + imagesBlock + if (await writeFile(GZ, '/etc/nginx/sites-enabled/guanghulab', newConfig)) { + console.log(' ✅ 广州Nginx配置更新成功') + } else { + console.log(' ❌ 广州Nginx配置更新失败') + } + } + } + + // 测试广州Nginx配置 + const r8 = await exec(GZ, 'nginx -t 2>&1') + console.log(' 广州Nginx测试:', r8.stdout || r8.stderr) + + const r9 = await exec(GZ, 'nginx -s reload 2>&1') + console.log(' 广州Nginx重载:', r9.stdout || r9.stderr || 'ok') + + console.log('\n' + '═'.repeat(50)) + console.log('✅ 全部配置完成!') + console.log('═'.repeat(50)) + console.log(`\n📎 访问地址:`) + console.log(` https://guanghulab.com/images/`) + console.log(` (广州Nginx → 新加坡Nginx → image-studio:3912)`) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/read-active-config.mjs b/image-studio/deploy/read-active-config.mjs new file mode 100644 index 0000000..3b9a62f --- /dev/null +++ b/image-studio/deploy/read-active-config.mjs @@ -0,0 +1,51 @@ +/** + * 读取Nginx当前活跃配置 + */ +const SERVERS = { + gz: { name: '广州', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { 'Authorization': `Bearer ${server.key}`, 'Content-Type': 'application/json' }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function readFile(server, path) { + const result = await callGatekeeper(server, 'POST', '/file/read', { path }) + if (result.status === 200) return result.data.content + return null +} + +async function main() { + const gz = SERVERS.gz + + // 读当前活跃配置 + const active = await readFile(gz, '/etc/nginx/sites-enabled/guanghulab') + console.log('=== 当前活跃配置 (sites-enabled/guanghulab) ===') + console.log(active) + + // 读模板 + const tpl = await readFile(gz, '/etc/nginx/sites-available/guanghulab.conf') + if (tpl) { + console.log('\n=== 模板 (sites-available/guanghulab.conf) ===') + console.log(tpl.slice(0, 500)) + console.log('...(截断)') + } + + // 测试广州服务器是否能访问新加坡的3912 + console.log('\n=== 网络连通性测试 ===') + const sgIp = '43.156.237.110' + const r = await callGatekeeper(gz, 'POST', '/exec', { cmd: `curl -s --connect-timeout 5 http://${sgIp}:3912/ | head -3` }) + console.log(r.data.stdout || r.data.stderr || JSON.stringify(r.data)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/read-config.mjs b/image-studio/deploy/read-config.mjs new file mode 100644 index 0000000..aef4981 --- /dev/null +++ b/image-studio/deploy/read-config.mjs @@ -0,0 +1,89 @@ +/** + * 读取站点配置文件 + */ +const SERVERS = { + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { + 'Authorization': `Bearer ${server.key}`, + 'Content-Type': 'application/json', + }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function readFile(server, path) { + const result = await callGatekeeper(server, 'POST', '/file/read', { path }) + if (result.status === 200) return result.data.content + return null +} + +async function exec(server, cmd) { + const url = `http://${server.ip}:${server.port}/exec` + const opts = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${server.key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ cmd }), + } + const res = await fetch(url, opts) + const data = await res.json() + return data +} + +async function main() { + const gz = SERVERS.gz + + // 读取关键配置文件 + for (const p of [ + '/etc/nginx/sites-available/forgejo.conf', + '/etc/nginx/sites-available/guanghulab.conf', + ]) { + console.log(`\n📄 ${p}:`) + const content = await readFile(gz, p) + if (content) { + console.log(content) + } else { + console.log(' (读取失败或文件不存在)') + } + } + + // 看sites-enabled里有什么 + console.log('\n📂 sites-enabled:') + const r = await exec(gz, 'ls -la /etc/nginx/sites-enabled/ 2>&1') + console.log(r.stdout || r.stderr) + + // 看conf.d + console.log('\n📂 conf.d:') + const r2 = await exec(gz, 'ls -la /etc/nginx/conf.d/ 2>&1') + console.log(r2.stdout || r2.stderr) + + // 查看Forgejo进程 + console.log('\n🔍 Forgejo运行情况:') + const r3 = await exec(gz, 'ss -tlnp 2>/dev/null | head -20') + console.log(r3.stdout || r3.stderr) + + // 看guanghulab.com域名的Nginx路由 + console.log('\n🔍 查找server_name:') + const r4 = await exec(gz, 'grep -rn "server_name" /etc/nginx/ 2>&1') + console.log(r4.stdout || r4.stderr) + + // 看现有proxy_pass + console.log('\n🔍 查找proxy_pass:') + const r5 = await exec(gz, 'grep -rn "proxy_pass" /etc/nginx/ 2>&1') + console.log(r5.stdout || r5.stderr) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/remote-deploy-2.mjs b/image-studio/deploy/remote-deploy-2.mjs new file mode 100644 index 0000000..52fc243 --- /dev/null +++ b/image-studio/deploy/remote-deploy-2.mjs @@ -0,0 +1,149 @@ +/** + * ═══════════════════════════════════════════════════ + * 铸渊远程部署 · 第二阶段:安装Chrome + 上传源码 + 启动服务 + * ═══════════════════════════════════════════════════ + */ + +const SERVERS = { + sg: { name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' }, + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +const LOCAL_BASE = '/workspace/guanghulab/image-studio' + +import { readFileSync } from 'fs' + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { + 'Authorization': `Bearer ${server.key}`, + 'Content-Type': 'application/json', + }, + } + if (body) opts.body = JSON.stringify(body) + + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function exec(server, cmd) { + console.log(` → ${cmd.slice(0, 100)}`) + const result = await callGatekeeper(server, 'POST', '/exec', { cmd }) + if (result.status !== 200) { + console.log(` ❌ [${result.status}] ${JSON.stringify(result.data).slice(0, 200)}`) + } else { + const out = (result.data.stdout || '').trim() + const err = (result.data.stderr || '').trim() + if (out) console.log(` ${out.split('\n').slice(0, 8).join('\n ')}`) + if (err) console.log(` ⚠️ ${err.split('\n').slice(0, 4).join('\n ')}`) + } + return result +} + +async function writeFile(server, remotePath, localPath) { + const content = readFileSync(localPath, 'utf-8') + const result = await callGatekeeper(server, 'POST', '/file/write', { path: remotePath, content }) + if (result.status === 200) { + console.log(` ✅ ${remotePath} (${content.length} 字节)`) + } else { + console.log(` ❌ ${remotePath}: ${JSON.stringify(result.data)}`) + } + return result +} + +async function main() { + const sg = SERVERS.sg + const gz = SERVERS.gz + + // ======= 1. 在新加坡服务器安装Chrome ======= + console.log('═'.repeat(50)) + console.log('📦 1. 在新加坡服务器安装Chrome') + console.log('═'.repeat(50)) + + // 检查系统版本,安装Chromium + await exec(sg, 'cat /etc/os-release | head -3') + + // 安装Chromium(Ubuntu仓库中有chromium-browser) + console.log('\n 安装Chromium...') + await exec(sg, 'apt-get install -y -qq chromium-browser 2>&1 | tail -5') + + // 验证安装 + await exec(sg, 'which chromium-browser && chromium-browser --version || echo "not found"') + + // 如果没有chromium-browser,试试用npx puppeteer安装 + await exec(sg, 'which chromium-browser || (cd /data/image-studio && npx puppeteer browsers install chrome 2>&1 | tail -5)') + + // ======= 2. 上传源代码文件 ======= + console.log('\n' + '═'.repeat(50)) + console.log('📤 2. 上传源代码到新加坡服务器') + console.log('═'.repeat(50)) + + const files = [ + ['config.js', '/data/image-studio/config.js'], + ['renderer.js', '/data/image-studio/renderer.js'], + ['generate.js', '/data/image-studio/generate.js'], + ['server.js', '/data/image-studio/server.js'], + ['templates/xiaohongshu.js', '/data/image-studio/templates/xiaohongshu.js'], + ['templates/jike.js', '/data/image-studio/templates/jike.js'], + ['templates/poster.js', '/data/image-studio/templates/poster.js'], + ['templates/dynamic.js', '/data/image-studio/templates/dynamic.js'], + ['deploy/setup.sh', '/data/image-studio/deploy/setup.sh'], + ] + + for (const [local, remote] of files) { + await writeFile(sg, remote, `${LOCAL_BASE}/${local}`) + } + + // 更新package.json添加type:module + await exec(sg, `cd /data/image-studio && npm pkg set type="module"`) + + // ======= 3. 启动服务 ======= + console.log('\n' + '═'.repeat(50)) + console.log('🚀 3. 启动图片工作室服务') + console.log('═'.repeat(50)) + + // 先用node直接启动测试是否能正常运行 + await exec(sg, 'cd /data/image-studio && timeout 5 node server.js 2>&1 || true') + + // 用PM2启动(后台运行) + await exec(sg, 'pm2 delete image-studio 2>/dev/null; cd /data/image-studio && pm2 start server.js --name image-studio -o /data/image-studio/output.log -e /data/image-studio/error.log') + await exec(sg, 'sleep 2 && pm2 list') + + // 验证服务是否运行 + await exec(sg, 'curl -s http://localhost:3912/ | head -5 || echo "服务未启动"') + await exec(sg, 'ss -tlnp | grep 3912') + + // ======= 4. 保存PM2配置 ======= + await exec(sg, 'pm2 save') + + // ======= 5. 检查广州Nginx配置 ======= + console.log('\n' + '═'.repeat(50)) + console.log('🌐 4. 检查广州Nginx配置') + console.log('═'.repeat(50)) + + // 读取Nginx配置 + const nginxResult = await callGatekeeper(gz, 'POST', '/file/read', { path: '/etc/nginx/nginx.conf' }) + if (nginxResult.status === 200) { + console.log(nginxResult.data.content.slice(0, 1000)) + } else { + // 试试conf.d + const confResult = await callGatekeeper(gz, 'POST', '/exec', { cmd: 'ls /etc/nginx/sites-enabled/ /etc/nginx/conf.d/ 2>&1' }) + if (confResult.status === 200) { + console.log('Nginx配置目录:', confResult.data.stdout || confResult.data.stderr) + } + } + + console.log('\n' + '═'.repeat(50)) + console.log('✅ 部署流程完成!') + console.log('═'.repeat(50)) +} + +main().catch(err => { + console.error('❌ 失败:', err.message) + process.exit(1) +}) diff --git a/image-studio/deploy/remote-deploy-3.mjs b/image-studio/deploy/remote-deploy-3.mjs new file mode 100644 index 0000000..02ad379 --- /dev/null +++ b/image-studio/deploy/remote-deploy-3.mjs @@ -0,0 +1,96 @@ +/** + * ═══════════════════════════════════════════════════ + * 铸渊远程部署 · 第三阶段:配置广州Nginx反向代理 + * ═══════════════════════════════════════════════════ + */ + +const SERVERS = { + sg: { name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' }, + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { + 'Authorization': `Bearer ${server.key}`, + 'Content-Type': 'application/json', + }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function exec(server, cmd) { + const result = await callGatekeeper(server, 'POST', '/exec', { cmd }) + if (result.status !== 200) { + return { ok: false, error: result.data } + } + return { ok: true, stdout: result.data.stdout || '', stderr: result.data.stderr || '' } +} + +async function readFile(server, path) { + const result = await callGatekeeper(server, 'POST', '/file/read', { path }) + if (result.status === 200) return result.data.content + return null +} + +async function writeFile(server, path, content) { + const result = await callGatekeeper(server, 'POST', '/file/write', { path, content }) + return result.status === 200 +} + +async function main() { + const gz = SERVERS.gz + const sg = SERVERS.sg + + console.log('═'.repeat(50)) + console.log('🌐 配置广州Nginx反向代理') + console.log('═'.repeat(50)) + + // 1. 查找所有Nginx配置相关文件 + console.log('\n📂 查找Nginx配置...') + const lsResult = await exec(gz, 'find /etc/nginx -type f -name "*.conf" | sort') + if (lsResult.ok) console.log(lsResult.stdout) + + // 2. 读取各个配置文件 + for (const p of ['/etc/nginx/conf.d/default.conf', '/etc/nginx/sites-enabled/default', '/etc/nginx/nginx.conf']) { + const content = await readFile(gz, p) + if (content) { + console.log(`\n📄 ${p}:`) + console.log(content.slice(0, 2000)) + if (content.length > 2000) console.log(`... (共${content.length}字符,截断显示)`) + } + } + + // 3. 查找已部署的站点信息 + console.log('\n🔍 查找站点的server_name...') + await exec(gz, 'grep -r "server_name" /etc/nginx/ 2>/dev/null') + + // 4. 查看Forgejo的端口 + console.log('\n🔍 查找Forgejo端口...') + await exec(gz, 'ss -tlnp | grep -E "3000|8080|3910"') + + // 5. 查找现有反向代理规则 + console.log('\n🔍 查找现有proxy_pass...') + await exec(gz, 'grep -r "proxy_pass" /etc/nginx/ 2>/dev/null') + + // 6. 测试新加坡服务器是否能从广州访问 + console.log('\n🔗 测试广州→新加坡网络...') + const gwTest = await exec(gz, `curl -s --connect-timeout 5 http://${sg.ip}:3912/ | head -3 || echo "无法连接"`) + console.log(gwTest.stdout || gwTest.stderr) + + console.log('\n' + '═'.repeat(50)) + console.log('✅ Nginx调查完成, 准备配置代理') + console.log('═'.repeat(50)) +} + +main().catch(err => { + console.error('❌ 失败:', err.message) + process.exit(1) +}) diff --git a/image-studio/deploy/remote-deploy.mjs b/image-studio/deploy/remote-deploy.mjs new file mode 100644 index 0000000..909f06a --- /dev/null +++ b/image-studio/deploy/remote-deploy.mjs @@ -0,0 +1,140 @@ +/** + * ═══════════════════════════════════════════════════ + * 铸渊远程部署 · 通过看门人API部署图片工作室 + * ═══════════════════════════════════════════════════ + * + * 冰朔,我不需要SSH密钥。 + * 铸渊看门人就是我的手。 + */ + +const SERVERS = { + sg: { name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' }, + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { + 'Authorization': `Bearer ${server.key}`, + 'Content-Type': 'application/json', + }, + } + if (body) opts.body = JSON.stringify(body) + + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function exec(server, cmd) { + console.log(` → ${server.name}: ${cmd.slice(0, 80)}...`) + const result = await callGatekeeper(server, 'POST', '/exec', { cmd }) + if (result.status !== 200) { + console.log(` ❌ [${result.status}] ${JSON.stringify(result.data)}`) + } else { + const out = (result.data.stdout || '').trim() + const err = (result.data.stderr || '').trim() + if (out) console.log(` stdout: ${out.slice(0, 300)}`) + if (err) console.log(` stderr: ${err.slice(0, 300)}`) + } + return result +} + +async function writeFile(server, filepath, content) { + console.log(` → ${server.name}: 写入 ${filepath}`) + return callGatekeeper(server, 'POST', '/file/write', { path: filepath, content }) +} + +async function readFile(server, filepath) { + const result = await callGatekeeper(server, 'POST', '/file/read', { path: filepath }) + if (result.status === 200) return result.data.content || '' + return null +} + +async function checkStatus(server) { + console.log(`\n📡 测试连接 ${server.name} (${server.ip}:${server.port})...`) + const result = await callGatekeeper(server, 'POST', '/status') + if (result.status === 200) { + console.log(` ✅ 连接成功!`) + console.log(` 状态: ${JSON.stringify(result.data, null, 2).slice(0, 400)}`) + return true + } else { + console.log(` ❌ 连接失败: ${result.status} ${JSON.stringify(result.data)}`) + return false + } +} + +async function deployImageStudio() { + const sg = SERVERS.sg + const gz = SERVERS.gz + + /* ═══ 第一步:测试连接 ═══ */ + console.log('═'.repeat(50)) + console.log('🔌 铸渊远程部署 · 检查服务器连接') + console.log('═'.repeat(50)) + + const sgOk = await checkStatus(sg) + if (!sgOk) { + console.log('\n⚠️ 新加坡服务器连接失败,尝试广州服务器直接反向代理方案...') + } + + /* ═══ 第二步:在新加坡部署image-studio ═══ */ + if (sgOk) { + console.log('\n📦 第二步:在新加坡服务器部署图片工作室') + console.log('═'.repeat(50)) + + // 检查Node.js + await exec(sg, 'node --version') + await exec(sg, 'which google-chrome || which chromium-browser || echo "no chrome"') + + // 创建目录 + await exec(sg, 'mkdir -p /data/image-studio/output /data/image-studio/templates /data/image-studio/deploy') + + // 安装系统依赖 + console.log('\n 📥 安装系统依赖...') + await exec(sg, 'apt-get update -qq && apt-get install -y -qq google-chrome-stable libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libdbus-1-3 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 2>&1 | tail -5') + + // 安装Node依赖 + await exec(sg, 'cd /data/image-studio && npm init -y && npm install puppeteer express 2>&1 | tail -5') + + // 检查PM2 + await exec(sg, 'which pm2 || npm install -g pm2 2>&1 | tail -3') + + console.log('\n ✅ 新加坡服务器基础环境就绪') + console.log(' 接下来需要通过 /file/write 上传源代码文件') + } + + /* ═══ 第三步:配置广州Nginx代理 ═══ */ + console.log('\n🌐 第三步:检查广州服务器Nginx代理') + console.log('═'.repeat(50)) + + const gzOk = await checkStatus(gz) + if (gzOk) { + // 读取现有Nginx配置 + const nginxConf = await readFile(gz, '/etc/nginx/sites-enabled/default') + if (nginxConf) { + console.log(' 已读取Nginx配置,长度:', nginxConf.length, '字符') + console.log(' 前200字符:', nginxConf.slice(0, 200)) + } else { + console.log(' 无法直接读取Nginx配置(权限问题),需要调整方案') + // 尝试其他路径 + for (const p of ['/etc/nginx/conf.d/default.conf', '/etc/nginx/nginx.conf', '/data/nginx/conf.d/default.conf']) { + const c = await readFile(gz, p) + if (c) { console.log(` ✅ 找到配置: ${p}`); break } + } + } + } + + console.log('\n' + '═'.repeat(50)) + console.log('📋 部署检查完成') + console.log('═'.repeat(50)) +} + +deployImageStudio().catch(err => { + console.error('❌ 部署失败:', err.message) + process.exit(1) +}) diff --git a/image-studio/deploy/setup-proxy-v2.mjs b/image-studio/deploy/setup-proxy-v2.mjs new file mode 100644 index 0000000..441e4bf --- /dev/null +++ b/image-studio/deploy/setup-proxy-v2.mjs @@ -0,0 +1,126 @@ +/** + * 在新加坡服务器上配置Nginx反向代理 + * 广州→新加坡的3911端口(Gatekeeper)是通的 + * 但3912端口不通。需要在新加坡本地装Nginx + * 来代理访问,或者使用其他可通端口。 + */ + +const SG = { + name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, + key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' +} +const GZ = { + name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, + key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' +} + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { + method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + // 第一步:检查新加坡服务器的开放端口 + console.log('📡 检查新加坡服务器已监听端口...') + const r1 = await exec(SG, 'ss -tlnp') + console.log(r1.stdout || r1.stderr) + + // 第二步:检查是否已有Nginx + console.log('\n🔍 检查Nginx...') + const r2 = await exec(SG, 'which nginx && nginx -v 2>&1 || echo "no nginx"') + console.log(r2.stdout || r2.stderr) + + // 第三步:查看所有Nginx站点 + const r3 = await exec(SG, 'ls -la /etc/nginx/sites-enabled/ 2>&1 || echo "no sites-enabled"') + console.log('\nNginx站点:', r3.stdout || r3.stderr) + + // 第四步:查看正在运行的服务,找到已经开放的端口 + console.log('\n🔍 检查其他服务端口...') + const r4 = await exec(SG, 'curl -s --connect-timeout 3 http://localhost:80/ 2>&1 | head -3 || echo "port 80 not serving"') + console.log('port 80:', r4.stdout || r4.stderr) + const r5 = await exec(SG, 'curl -s --connect-timeout 3 http://localhost:3000/ 2>&1 | head -3 || echo "port 3000 not serving"') + console.log('port 3000:', r5.stdout || r5.stderr) + const r6 = await exec(SG, 'curl -s --connect-timeout 3 http://localhost:8080/ 2>&1 | head -3 || echo "port 8080 not serving"') + console.log('port 8080:', r6.stdout || r6.stderr) + + // 第五步:从广州测试哪些端口到新加坡是通的 + console.log('\n📡 广州→新加坡端口测试...') + const testPorts = [80, 443, 8080, 3000, 3910, 3911] + for (const port of testPorts) { + const r = await exec(GZ, `timeout 3 bash -c "echo > /dev/tcp/${SG.ip}/${port}" 2>&1 && echo '✅' || echo '❌'`) + const status = (r.stdout || '').trim() + console.log(` 端口 ${port}: ${status}`) + } + + // 第六步:方案1 - 如果80端口可用,就用Nginx代理 + const r7 = await exec(SG, 'which nginx 2>&1 || apt-get install -y -qq nginx 2>&1 | tail -3') + console.log('\n📥 Nginx安装:', (r7.stdout || r7.stderr).slice(0, 100)) + + // 创建Nginx配置 + const nginxSiteConf = `server { + listen 3914; + server_name _; + + # 允许跨域 + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range"; + + location / { + proxy_pass http://127.0.0.1:3912; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_buffering off; + proxy_read_timeout 60s; + } + + location /output/ { + alias /data/image-studio/output/; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + } +} +` + const r8 = await exec(SG, `cat > /etc/nginx/sites-available/image-studio << 'EOF' +${nginxSiteConf} +EOF`) + console.log('\n📝 Nginx配置写入:', (r8.stdout || r8.stderr || 'ok').slice(0, 100)) + + // 启用站点 + await exec(SG, 'ln -sf /etc/nginx/sites-available/image-studio /etc/nginx/sites-enabled/ 2>&1') + const r9 = await exec(SG, 'nginx -t 2>&1') + console.log('Nginx测试:', r9.stdout || r9.stderr) + const r10 = await exec(SG, 'nginx -s reload 2>&1 || systemctl reload nginx 2>&1') + console.log('Nginx重载:', r10.stdout || r10.stderr || 'ok') + + // 验证本地 + const r11 = await exec(SG, 'curl -s http://localhost:3914/ | head -3') + console.log('\n✅ 本地验证:', r11.stdout || r11.stderr) + + // 从广州测试新端口 + const r12 = await exec(GZ, `timeout 3 bash -c "echo > /dev/tcp/${SG.ip}/3914" 2>&1 && echo '✅ OPEN' || echo '❌ CLOSED'`) + console.log('广州→新加坡3914:', (r12.stdout || '').trim()) + + // 如果3914也不行,做最终方案:直接用广州的Gatekeeper代理 + // 让广州的Nginx通过Gatekeeper API转发请求 + console.log('\n' + '═'.repeat(50)) + console.log('📋 配置完成') + console.log('═'.repeat(50)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/setup-proxy.mjs b/image-studio/deploy/setup-proxy.mjs new file mode 100644 index 0000000..2d8ca69 --- /dev/null +++ b/image-studio/deploy/setup-proxy.mjs @@ -0,0 +1,125 @@ +/** + * 解决网络连通性问题 + * 方案:在新加坡服务器安装Nginx或配置代理 + * 使得广州可以通过某个可达端口访问到image-studio + */ + +const SERVERS = { + sg: { name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' }, + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { 'Authorization': `Bearer ${server.key}`, 'Content-Type': 'application/json' }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function exec(server, cmd) { + const result = await callGatekeeper(server, 'POST', '/exec', { cmd }) + return result.data +} + +async function testGzToSg(gz, port) { + const r = await exec(gz, `curl -s --connect-timeout 3 -o /dev/null -w "%{http_code}" http://${SERVERS.sg.ip}:${port}/ 2>&1`) + return r.stdout || r.stderr +} + +async function main() { + const sg = SERVERS.sg + const gz = SERVERS.gz + + console.log('═'.repeat(50)) + console.log('🔧 方案A: 在新加坡服务器上安装Nginx做反向代理') + console.log('═'.repeat(50)) + + // 1. 检查新加坡是否有Nginx + console.log('\n📋 检查新加坡服务器环境...') + const r1 = await exec(sg, 'which nginx && nginx -v 2>&1 || echo "no nginx"') + console.log('nginx:', r1.stdout || r1.stderr) + + // 2. 检查是否已安装socat + const r2 = await exec(sg, 'which socat && socat -V 2>&1 | head -1 || echo "no socat"') + console.log('socat:', r2.stdout || r2.stderr) + + // 3. 测试广州→新加坡的端口连通性(检查哪些端口是开的) + console.log('\n📡 测试广州→新加坡各端口连通性...') + for (const port of [80, 443, 3000, 8080, 8084, 3910, 3911, 3912]) { + const r = await exec(gz, `timeout 3 bash -c "echo > /dev/tcp/${SERVERS.sg.ip}/${port}" 2>&1 && echo "✅ OPEN" || echo "❌ CLOSED"`) + const status = (r.stdout || '').trim() + if (status.includes('OPEN')) console.log(` 端口 ${port}: ✅ OPEN`) + } + + // 4. 如果没有Nginx,安装Nginx + const hasNginx = r1.stdout && !r1.stdout.includes('no nginx') + if (!hasNginx) { + console.log('\n📥 安装Nginx...') + const r3 = await exec(sg, 'apt-get install -y -qq nginx 2>&1 | tail -3') + console.log(r3.stdout || r3.stderr) + } + + // 5. 配置Nginx反向代理到本地3912 + const nginxConf = ` +server { + listen 3913; + server_name _; + + location / { + proxy_pass http://127.0.0.1:3912; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_buffering off; + } + + location /output/ { + alias /data/image-studio/output/; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + } +} +` + console.log('\n📝 写入Nginx配置...') + const r4 = await exec(sg, `cat > /etc/nginx/sites-available/image-studio << 'NGINX_CONF' +${nginxConf} +NGINX_CONF +cat /etc/nginx/sites-available/image-studio | head -5`) + console.log(r4.stdout || r4.stderr) + + // 6. 启用站点 + await exec(sg, 'ln -sf /etc/nginx/sites-available/image-studio /etc/nginx/sites-enabled/ 2>&1') + await exec(sg, 'nginx -t 2>&1') + await exec(sg, 'systemctl reload nginx 2>&1 || nginx -s reload 2>&1') + + // 7. 验证 + console.log('\n✅ 验证Nginx代理...') + const r5 = await exec(sg, 'ss -tlnp | grep 3913') + console.log('3913监听:', r5.stdout || r5.stderr) + const r6 = await exec(sg, 'curl -s http://localhost:3913/ | head -3') + console.log('Nginx代理测试:', r6.stdout || r6.stderr) + + // 8. 重新测试广州→新加坡的端口 + console.log('\n📡 重新测试...') + const r7 = await exec(gz, `timeout 3 bash -c "echo > /dev/tcp/${SERVERS.sg.ip}/3913" 2>&1 && echo "✅ OPEN" || echo "❌ CLOSED"`) + console.log(`端口 3913: ${(r7.stdout || '').trim()}`) + + // 9. 用curl测试 + const r8 = await exec(gz, `curl -s --connect-timeout 5 http://${SERVERS.sg.ip}:3913/ | head -3`) + console.log('广州→新加坡3913测试:', (r8.stdout || r8.stderr || 'empty').slice(0, 200)) + + console.log('\n' + '═'.repeat(50)) + console.log('📋 网络配置完成') + console.log('═'.repeat(50)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/test-gz.mjs b/image-studio/deploy/test-gz.mjs new file mode 100644 index 0000000..d473992 --- /dev/null +++ b/image-studio/deploy/test-gz.mjs @@ -0,0 +1,64 @@ +/** + * 正确测试广州Nginx代理 + */ + +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + console.log('═'.repeat(50)) + console.log('📡 广州Nginx全链路测试') + console.log('═'.repeat(50)) + + // 1. 测试本地Nginx + 正确Host头 + console.log('\n1️⃣ 本地Nginx + Host头:') + const tests = [ + ['https://localhost/images/ (默认Host)', `curl -s -k --connect-timeout 5 -w "\\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -3`], + ['https://localhost/images/ (Host:guanghulab.com)', `curl -s -k --connect-timeout 5 -H "Host: guanghulab.com" -w "\\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -3`], + ['http://localhost/images/ (Host:guanghulab.com)', `curl -s --connect-timeout 5 -H "Host: guanghulab.com" -w "\\nHTTP:%{http_code}" http://localhost/images/ 2>&1 | tail -3`], + ['https://localhost/ (首页)', `curl -s -k --connect-timeout 5 -H "Host: guanghulab.com" -w "\\nHTTP:%{http_code}" https://localhost/ 2>&1 | tail -3`], + ] + + for (const [name, cmd] of tests) { + const r = await exec(GZ, cmd) + console.log(` ${name}:`) + console.log(` ${(r.stdout || r.stderr || '').split('\\n').slice(-3).join('\\n ')}`) + } + + // 2. 查看Nginx的错误日志 + console.log('\n2️⃣ Nginx错误日志(最后10行):') + const r1 = await exec(GZ, 'tail -10 /var/log/nginx/error.log 2>&1 || echo "no error log"') + console.log(' ', (r1.stdout || r1.stderr || '').slice(0, 500)) + + // 3. 检查代理的配置是否真的在正确位置 + console.log('\n3️⃣ 完整SSL server块检查:') + const r2 = await exec(GZ, "sed -n '/listen 443 ssl/,/^}$/p' /etc/nginx/sites-enabled/guanghulab | head -60") + console.log(r2.stdout || r2.stderr) + + // 4. 也检查一下Nginx的端口监听状态 + console.log('\n4️⃣ Nginx端口:') + const r3 = await exec(GZ, 'ss -tlnp | grep nginx') + console.log(r3.stdout || r3.stderr) + + // 5. 直接通过新加坡IP测试广州的代理设置 + console.log('\n5️⃣ 通过广州访问新加坡/images/:') + const SG_IP = '43.156.237.110' + const r4 = await exec(GZ, `curl -s --connect-timeout 5 -w "\\nHTTP:%{http_code}" http://${SG_IP}/images/ -H "Host: guanghulab.com" 2>&1 | tail -3`) + console.log(' http://SG_IP/images/ (Host:guanghulab.com):', (r4.stdout || '').slice(0, 100)) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/test-network.mjs b/image-studio/deploy/test-network.mjs new file mode 100644 index 0000000..9148292 --- /dev/null +++ b/image-studio/deploy/test-network.mjs @@ -0,0 +1,73 @@ +/** + * 测试广州→新加坡网络连通性,然后配置Nginx + */ +const SERVERS = { + sg: { name: '新加坡·铸渊大脑', ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' }, + gz: { name: '广州·代码仓库', ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' }, +} + +async function callGatekeeper(server, method, path, body = null) { + const url = `http://${server.ip}:${server.port}${path}` + const opts = { + method, + headers: { 'Authorization': `Bearer ${server.key}`, 'Content-Type': 'application/json' }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + return { status: res.status, data } +} + +async function exec(server, cmd) { + const result = await callGatekeeper(server, 'POST', '/exec', { cmd }) + return result.data +} + +async function readFile(server, path) { + const result = await callGatekeeper(server, 'POST', '/file/read', { path }) + if (result.status === 200) return result.data.content + return null +} + +async function writeFile(server, path, content) { + const result = await callGatekeeper(server, 'POST', '/file/write', { path, content }) + return result.status === 200 +} + +async function main() { + const gz = SERVERS.gz + const sg = SERVERS.sg + + // 测试广州→新加坡网络(详细) + console.log('🔌 广州→新加坡 网络测试:') + const r1 = await exec(gz, `curl -v --connect-timeout 5 http://${sg.ip}:3912/ 2>&1 | head -20`) + console.log(r1.stdout || r1.stderr || 'empty') + + // 测试广州→新加坡 Gatekeeper (3911) + console.log('\n🔌 广州→新加坡 Gatekeeper 测试:') + const r2 = await exec(gz, `curl -s --connect-timeout 5 -X POST http://${sg.ip}:3911/ping 2>&1 | head -5`) + console.log(r2.stdout || r2.stderr || 'empty') + + // 如果广州不能直接连新加坡,试试从新加坡创建反向隧道 + console.log('\n🔍 检查是否需要反向代理方案') + + // 查看新加坡的ufw防火墙 + const r3 = await exec(sg, 'ufw status 2>&1 || echo "ufw not active"') + console.log('新加坡防火墙:', r3.stdout || r3.stderr) + + // 查看广州的防火墙 + const r4 = await exec(gz, 'ufw status 2>&1 || echo "ufw not active"') + console.log('广州防火墙:', r4.stdout || r4.stderr) + + // 检查新加坡3912端口是否监听在0.0.0.0 + const r5 = await exec(sg, 'ss -tlnp | grep 3912') + console.log('新加坡3912监听:', r5.stdout || r5.stderr) + + // 直接测试新加坡本地的3912 + const r6 = await exec(sg, 'curl -s http://localhost:3912/ | head -3') + console.log('新加坡本地测试:', r6.stdout || r6.stderr) +} + +main().catch(err => console.error('❌', err)) diff --git a/image-studio/deploy/verify.mjs b/image-studio/deploy/verify.mjs new file mode 100644 index 0000000..13bad22 --- /dev/null +++ b/image-studio/deploy/verify.mjs @@ -0,0 +1,80 @@ +/** + * 全链路验证 + */ + +const SG = { ip: '43.156.237.110', port: 3911, key: 'zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9' } +const GZ = { ip: '43.139.217.141', port: 3910, key: 'zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23' } + +async function callGK(srv, method, path, body = null) { + const url = `http://${srv.ip}:${srv.port}${path}` + const opts = { method, headers: { 'Authorization': `Bearer ${srv.key}`, 'Content-Type': 'application/json' } } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(url, opts) + const text = await res.text() + try { return { status: res.status, data: JSON.parse(text) } } + catch { return { status: res.status, data: { raw: text } } } +} + +async function exec(srv, cmd) { + const r = await callGK(srv, 'POST', '/exec', { cmd }) + return r.data +} + +async function main() { + console.log('═'.repeat(50)) + console.log('📡 全链路验证') + console.log('═'.repeat(50)) + + // 1. 新加坡本地 + console.log('\n1️⃣ 新加坡本地:') + const r1 = await exec(SG, 'curl -s -w "\nHTTP:%{http_code}" http://localhost/images/ 2>&1 | tail -5') + console.log(' localhost/images/:', (r1.stdout || '').slice(0, 100)) + + // 2. 新加坡外网(通过80端口) + console.log('\n2️⃣ 新加坡外网:') + const r2 = await exec(SG, `curl -s -w "\nHTTP:%{http_code}" http://${SG.ip}/images/ 2>&1 | tail -5`) + console.log(' SG外网/images/:', (r2.stdout || '').slice(0, 100)) + + // 3. 广州→新加坡 + console.log('\n3️⃣ 广州→新加坡:') + const r3 = await exec(GZ, `curl -s --connect-timeout 5 -w "\nHTTP:%{http_code}" http://${SG.ip}/images/ 2>&1 | tail -5`) + console.log(' GZ→SG/images/:', (r3.stdout || '').slice(0, 100)) + + // 4. 广州Nginx本地测试 + console.log('\n4️⃣ 广州Nginx:') + const r4 = await exec(GZ, `curl -s -k --connect-timeout 5 -w "\nHTTP:%{http_code}" https://localhost/images/ 2>&1 | tail -5`) + console.log(' localhost/images/:', (r4.stdout || '').slice(0, 100)) + + // 5. 新加坡生成一张测试图 + console.log('\n5️⃣ 生成测试图片:') + const r5 = await exec(SG, 'cd /data/image-studio && node generate.js --text "你好冰朔\n这是铸渊在新加坡为你生成的图片\n图片工作室已部署完成" --for "小红书" --output deploy_test 2>&1 | tail -5') + console.log(' 生成结果:', r5.stdout || r5.stderr) + + // 6. 验证图片能访问 + const r6 = await exec(SG, 'ls -la /data/image-studio/output/deploy_test*.png 2>&1') + console.log(' 图片文件:', r6.stdout || r6.stderr) + + const r7 = await exec(SG, 'ls /data/image-studio/output/deploy_test*.png 2>&1 | head -1 | xargs -I{} basename {}') + const imgName = (r7.stdout || '').trim() + if (imgName) { + const r8 = await exec(GZ, `curl -s --connect-timeout 5 -o /dev/null -w "HTTP:%{http_code} SIZE:%{size_download}" http://${SG.ip}/images/output/${imgName} 2>&1`) + console.log(` 广州→新加坡 图片访问:`, r8.stdout || r8.stderr) + + const r9 = await exec(GZ, `curl -s -k --connect-timeout 5 -o /dev/null -w "HTTP:%{http_code} SIZE:%{size_download}" https://localhost/images/output/${imgName} 2>&1`) + console.log(` 广州Nginx→图片:`, r9.stdout || r9.stderr) + } + + // 7. 生成API测试 + console.log('\n6️⃣ API测试:') + const r10 = await exec(SG, `curl -s -X POST http://localhost:3912/api/generate -H "Content-Type: application/json" -d '{"text":"测试API","for":"即刻"}' 2>&1 | python3 -m json.tool 2>&1 | head -10`) + console.log(' API生成:', r10.stdout || r10.stderr) + + console.log('\n' + '═'.repeat(50)) + console.log('📋 链路验证完成') + console.log('═'.repeat(50)) + console.log('\n📎 访问入口:') + console.log(' 新加坡直连: http://43.156.237.110/images/') + console.log(' 广州代理: https://guanghulab.com/images/') +} + +main().catch(err => console.error('❌', err))