Files
clawpanel/scripts/serve.js
晴天 02e1ef6b14 feat: 版本管理 + macOS提示优化 + 部署文档更新
- OpenClaw 版本管理: 安装/升级/降级/切换版本, 汉化版/原版选择
- 新增 list_openclaw_versions API (Rust + Web)
- upgrade_openclaw 支持指定版本号
- 版本选择器弹窗 (about.js)
- macOS Gatekeeper 提示优化: 强调拖入应用程序, No such file 备选
- 部署文档统一使用 npm run serve 替代 npx vite
- showUpgradeModal 支持自定义标题 + onClose 回调
- serve.js 路径分隔符跨平台修复
- 扩展工具页面优化 + AI助手危险工具确认
2026-03-08 01:46:27 +08:00

206 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* ClawPanel 独立 Web 服务器Headless 模式)
* 无需 Tauri / Rust / GUI纯 Node.js 运行
* 适用于 Linux 服务器、Docker 等无桌面环境
*
* 用法:
* npm run serve # 默认 0.0.0.0:1420
* npm run serve -- --port 8080
* npm run serve -- --host 127.0.0.1 --port 3000
* PORT=8080 npm run serve
*/
import http from 'http'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { homedir } from 'os'
import net from 'net'
import { _initApi, _apiMiddleware } from './dev-api.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const DIST_DIR = path.resolve(__dirname, '..', 'dist')
// === 解析命令行参数 ===
function parseArgs() {
const args = process.argv.slice(2)
let host = process.env.HOST || '0.0.0.0'
let port = parseInt(process.env.PORT, 10) || 1420
for (let i = 0; i < args.length; i++) {
if (args[i] === '--host' && args[i + 1]) host = args[++i]
if (args[i] === '--port' && args[i + 1]) port = parseInt(args[++i], 10)
if (args[i] === '-p' && args[i + 1]) port = parseInt(args[++i], 10)
if (args[i] === '--help' || args[i] === '-h') {
console.log(`
ClawPanel Web Server (Headless)
用法: node scripts/serve.js [选项]
选项:
--host <addr> 监听地址 (默认: 0.0.0.0)
--port, -p <n> 监听端口 (默认: 1420)
--help, -h 显示帮助
环境变量:
HOST 监听地址
PORT 监听端口
示例:
npm run serve # 0.0.0.0:1420
npm run serve -- --port 8080 # 0.0.0.0:8080
npm run serve -- --host 127.0.0.1 -p 3000
`)
process.exit(0)
}
}
return { host, port }
}
// === MIME 类型映射 ===
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.webp': 'image/webp',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.txt': 'text/plain; charset=utf-8',
'.map': 'application/json',
}
// === 静态文件服务 ===
function serveStatic(req, res) {
// URL 去掉 query string
const urlPath = req.url.split('?')[0]
let filePath = path.join(DIST_DIR, urlPath === '/' ? 'index.html' : urlPath)
// 安全检查:不允许目录遍历
if (!filePath.startsWith(DIST_DIR)) {
res.statusCode = 403
res.end('Forbidden')
return
}
// 尝试读取文件
fs.stat(filePath, (err, stats) => {
if (!err && stats.isFile()) {
sendFile(res, filePath)
return
}
// SPA fallback非 API、非静态资源 → index.html
const ext = path.extname(urlPath)
if (!ext || ext === '.html') {
sendFile(res, path.join(DIST_DIR, 'index.html'))
} else {
res.statusCode = 404
res.end('Not Found')
}
})
}
function sendFile(res, filePath) {
const ext = path.extname(filePath)
const contentType = MIME_TYPES[ext] || 'application/octet-stream'
// 缓存策略资源文件长缓存HTML 不缓存
if (ext === '.html') {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
} else if (filePath.includes(`${path.sep}assets${path.sep}`)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
res.setHeader('Content-Type', contentType)
fs.createReadStream(filePath).pipe(res)
}
// === 启动服务器 ===
async function main() {
// 检查 dist 目录
if (!fs.existsSync(path.join(DIST_DIR, 'index.html'))) {
console.error('❌ 未找到 dist/index.html请先运行: npm run build')
process.exit(1)
}
const { host, port } = parseArgs()
// 初始化 API
_initApi()
const server = http.createServer(async (req, res) => {
// CORS 头(方便开发调试)
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return }
// API 请求
await _apiMiddleware(req, res, () => {
// 非 API → 静态文件
serveStatic(req, res)
})
})
// WebSocket 代理
let gatewayPort = 18789
try {
const cfgPath = path.join(homedir(), '.openclaw', 'openclaw.json')
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
gatewayPort = cfg?.gateway?.port || 18789
} catch {}
server.on('upgrade', (req, socket, head) => {
if (!req.url?.startsWith('/ws')) {
socket.destroy()
return
}
const target = net.createConnection(gatewayPort, '127.0.0.1', () => {
const reqLine = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`
const headers = Object.entries(req.headers)
.map(([k, v]) => `${k}: ${v}`)
.join('\r\n')
target.write(reqLine + headers + '\r\n\r\n')
if (head.length) target.write(head)
socket.pipe(target)
target.pipe(socket)
})
target.on('error', () => socket.destroy())
socket.on('error', () => target.destroy())
})
server.listen(port, host, () => {
console.log('')
console.log(' ┌─────────────────────────────────────────┐')
console.log(' │ │')
console.log(' │ 🦀 ClawPanel Web Server (Headless) │')
console.log(' │ │')
console.log(` │ http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/`.padEnd(44) + '│')
if (host === '0.0.0.0') {
console.log(` │ http://0.0.0.0:${port}/`.padEnd(44) + '│')
}
console.log(' │ │')
console.log(' └─────────────────────────────────────────┘')
console.log('')
console.log(' 按 Ctrl+C 停止服务')
console.log('')
})
// 优雅退出
process.on('SIGINT', () => { console.log('\n 👋 服务已停止'); process.exit(0) })
process.on('SIGTERM', () => { console.log('\n 👋 服务已停止'); process.exit(0) })
}
main().catch(e => { console.error('启动失败:', e); process.exit(1) })