This commit is contained in:
supernijia
2026-02-28 11:30:35 +08:00
parent a861c9a89e
commit dbb6f97f4e
4 changed files with 578 additions and 89 deletions

View File

@@ -1,42 +1,285 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
:root {
--background: #ffffff;
--foreground: #171717;
}
--bg-color: #0d1117;
--bg-gradient: radial-gradient(circle at top right, #1a2333, #0d1117);
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--accent-color: #58a6ff;
--accent-hover: #3182ce;
--panel-bg: rgba(22, 27, 34, 0.65);
--panel-border: rgba(48, 54, 61, 0.5);
--input-bg: rgba(13, 17, 23, 0.7);
--input-border: #30363d;
--input-focus: #58a6ff;
--success-color: #238636;
--error-color: #f85149;
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
--glass-blur: blur(12px);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--transition-speed: 0.2s;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
padding: 0;
}
a {
color: inherit;
text-decoration: none;
body {
font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif;
background-color: var(--bg-color);
background-image: var(--bg-gradient);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
main {
max-width: 1000px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #58a6ff 0%, #bc8cff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
}
.subtitle {
text-align: center;
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: 3rem;
font-weight: 300;
}
/* Glassmorphism Panel Container */
.glass-panel {
background: var(--panel-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--panel-border);
border-radius: 16px;
padding: 2.5rem;
box-shadow: var(--glass-shadow);
display: flex;
flex-direction: column;
gap: 2rem;
animation: fadeIn 0.6s ease-out;
}
@media (min-width: 768px) {
.glass-panel {
flex-direction: row;
align-items: stretch;
}
}
.form-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.preview-section {
flex: 1;
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
border: 1px solid var(--panel-border);
overflow: hidden;
}
/* Form Controls */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 500;
font-size: 0.95rem;
color: var(--text-primary);
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 0.75rem 1rem;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
font-family: inherit;
transition: all var(--transition-speed) ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--input-focus);
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
}
.form-group select option {
background-color: var(--bg-color);
color: var(--text-primary);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid var(--input-border);
border-radius: 4px;
background: var(--input-bg);
cursor: pointer;
position: relative;
transition: all var(--transition-speed);
}
.checkbox-group input[type="checkbox"]:checked {
background: var(--accent-color);
border-color: var(--accent-color);
}
.checkbox-group input[type="checkbox"]:checked::after {
content: "";
position: absolute;
top: 2px;
left: 6px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.form-text {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid var(--panel-border);
background: rgba(13, 17, 23, 0.5);
}
.tab-btn {
flex: 1;
background: none;
border: none;
padding: 1rem;
color: var(--text-secondary);
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-speed);
border-bottom: 2px solid transparent;
}
.tab-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.tab-btn.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
background: rgba(88, 166, 255, 0.1);
}
/* Code Preview */
.code-container {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}
.code-container pre {
margin: 0;
padding: 1.5rem;
overflow-x: auto;
font-family:
"ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace;
font-size: 0.9rem;
color: #c9d1d9;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
height: 100%;
}
.copy-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(48, 54, 61, 0.8);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-speed);
display: flex;
align-items: center;
gap: 0.5rem;
backdrop-filter: blur(4px);
}
.copy-btn:hover {
background: var(--input-border);
transform: translateY(-1px);
}
.copy-btn.copied {
background: var(--success-color);
border-color: var(--success-color);
color: white;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -1,66 +1,14 @@
import Image from "next/image";
import styles from "./page.module.css";
import ScriptGenerator from '@/components/ScriptGenerator';
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className={styles.intro}>
<h1>To get started, edit the page.tsx file.</h1>
<p>
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className={styles.secondary}
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<main>
<h1>OpenClaw </h1>
<p className="subtitle">
OpenClaw
</p>
<ScriptGenerator />
</main>
);
}

View File

@@ -0,0 +1,201 @@
'use client';
import { useState } from 'react';
import { generateScript, OpenClawConfig } from '../utils/generateScripts';
export default function ScriptGenerator() {
const [config, setConfig] = useState<OpenClawConfig>({
workspace: '',
mode: 'local',
flow: 'quickstart',
nodeManager: 'npm',
installDaemon: true,
gatewayPort: '',
gatewayBind: '',
gatewayToken: '',
aiProvider: 'skip',
apiKey: '',
defaultModel: ''
});
const [activeTab, setActiveTab] = useState<'unix' | 'windows'>('unix');
const [copied, setCopied] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
setConfig(prev => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
}));
};
const currentScript = generateScript(config, activeTab);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(currentScript);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
}
};
return (
<div className="glass-panel">
{/* Configuration Form */}
<div className="form-section">
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}></h2>
<div className="form-group">
<label htmlFor="workspace"> (Workspace Path)</label>
<input
type="text"
id="workspace"
name="workspace"
value={config.workspace}
onChange={handleChange}
placeholder="例如: /opt/openclaw 或 C:\openclaw"
/>
<span className="form-text">OpenClaw </span>
</div>
<div style={{ display: 'flex', gap: '1rem' }}>
<div className="form-group" style={{ flex: 1 }}>
<label htmlFor="mode"> (Mode)</label>
<select name="mode" id="mode" value={config.mode} onChange={handleChange}>
<option value="local"> (Local)</option>
<option value="remote"> (Remote)</option>
</select>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label htmlFor="flow"> (Flow)</label>
<select name="flow" id="flow" value={config.flow} onChange={handleChange}>
<option value="quickstart"> (Quickstart)</option>
<option value="advanced"> (Advanced)</option>
<option value="manual"> (Manual)</option>
</select>
</div>
</div>
<div className="form-group">
<label htmlFor="nodeManager">Node </label>
<select name="nodeManager" id="nodeManager" value={config.nodeManager} onChange={handleChange}>
<option value="npm">NPM</option>
<option value="pnpm">PNPM</option>
<option value="bun">BUN</option>
</select>
</div>
<div className="form-group" style={{ marginTop: '0.5rem' }}>
<label className="checkbox-group">
<input
type="checkbox"
name="installDaemon"
checked={config.installDaemon}
onChange={handleChange}
/>
(Daemon)
</label>
</div>
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', borderBottom: '1px solid var(--panel-border)', paddingBottom: '0.5rem' }}>AI </h3>
<div className="form-group">
<label htmlFor="aiProvider">AI (AI Provider)</label>
<select name="aiProvider" id="aiProvider" value={config.aiProvider} onChange={handleChange}>
<option value="skip"> ()</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="custom"> (Custom)</option>
</select>
</div>
{config.aiProvider !== 'skip' && (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div className="form-group" style={{ flex: '1 1 100%' }}>
<label htmlFor="apiKey">API (API Key)</label>
<input
type="text"
name="apiKey"
id="apiKey"
placeholder="在此输入您的 API Key"
value={config.apiKey}
onChange={handleChange}
/>
</div>
<div className="form-group" style={{ flex: '1 1 100%' }}>
<label htmlFor="defaultModel"> (Default Model)</label>
<input
type="text"
name="defaultModel"
id="defaultModel"
placeholder="例如: gpt-4o, claude-3-5-sonnet-latest"
value={config.defaultModel}
onChange={handleChange}
/>
</div>
</div>
)}
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', borderBottom: '1px solid var(--panel-border)', paddingBottom: '0.5rem' }}> ()</h3>
<div style={{ display: 'flex', gap: '1rem' }}>
<div className="form-group" style={{ flex: 1 }}>
<label htmlFor="gatewayPort"> (Port)</label>
<input type="number" name="gatewayPort" id="gatewayPort" placeholder="8080" value={config.gatewayPort} onChange={handleChange} />
</div>
<div className="form-group" style={{ flex: 1 }}>
<label htmlFor="gatewayBind"> (Bind)</label>
<input type="text" name="gatewayBind" id="gatewayBind" placeholder="0.0.0.0" value={config.gatewayBind} onChange={handleChange} />
</div>
</div>
<div className="form-group">
<label htmlFor="gatewayToken"> (Token)</label>
<input type="text" name="gatewayToken" id="gatewayToken" placeholder="留空则由系统自动生成" value={config.gatewayToken} onChange={handleChange} />
</div>
</div>
{/* Script Preview */}
<div className="preview-section">
<div className="tabs">
<button
className={`tab-btn ${activeTab === 'unix' ? 'active' : ''}`}
onClick={() => setActiveTab('unix')}
>
Linux / Mac (.sh)
</button>
<button
className={`tab-btn ${activeTab === 'windows' ? 'active' : ''}`}
onClick={() => setActiveTab('windows')}
>
Windows (.ps1)
</button>
</div>
<div className="code-container">
<button className={`copy-btn ${copied ? 'copied' : ''}`} onClick={handleCopy}>
{copied ? (
<>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
!
</>
) : (
<>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</>
)}
</button>
<pre>
<code>{currentScript}</code>
</pre>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
export interface OpenClawConfig {
workspace: string;
mode: 'local' | 'remote';
flow: 'quickstart' | 'advanced' | 'manual';
nodeManager: 'npm' | 'pnpm' | 'bun';
installDaemon: boolean;
gatewayPort: string;
gatewayBind: string;
gatewayToken: string;
aiProvider: 'openai' | 'anthropic' | 'custom' | 'skip';
apiKey: string;
defaultModel: string;
}
export const generateScript = (config: OpenClawConfig, platform: 'unix' | 'windows') => {
const args: string[] = ['--non-interactive'];
if (config.workspace) args.push(`--workspace "${config.workspace}"`);
if (config.mode) args.push(`--mode ${config.mode}`);
if (config.flow) args.push(`--flow ${config.flow}`);
if (config.nodeManager) args.push(`--node-manager ${config.nodeManager}`);
if (config.installDaemon) {
args.push('--install-daemon');
} else {
args.push('--skip-daemon');
}
if (config.gatewayPort) args.push(`--gateway-port ${config.gatewayPort}`);
if (config.gatewayBind) args.push(`--gateway-bind ${config.gatewayBind}`);
if (config.gatewayToken) args.push(`--gateway-token "${config.gatewayToken}"`);
if (config.aiProvider === 'skip') {
args.push('--auth-choice skip');
} else {
args.push(`--auth-choice ${config.aiProvider}`);
if (config.apiKey) {
if (config.aiProvider === 'openai') args.push(`--openai-api-key "${config.apiKey}"`);
else if (config.aiProvider === 'anthropic') args.push(`--anthropic-api-key "${config.apiKey}"`);
else if (config.aiProvider === 'custom') args.push(`--custom-api-key "${config.apiKey}"`);
}
if (config.defaultModel) {
args.push(`--default-model "${config.defaultModel}"`);
}
}
const joinedArgs = args.join(' ');
if (platform === 'unix') {
return `#!/bin/bash
# 将代码包裹在函数中,避免直接复制粘贴运行失败时导致终端退出
install_openclaw() {
# 检查当前系统环境是否安装了所选的包管理器
if ! command -v ${config.nodeManager} &> /dev/null
then
echo "错误: 未在您的系统中检测到 \`${config.nodeManager}\` 环境。"
echo "请先安装 Node.js (https://nodejs.org) 或相应的包管理工具后再运行此脚本。"
return 1
fi
echo "正在使用 ${config.nodeManager} 全局安装 OpenClaw 最新版..."
${config.nodeManager === 'npm' ? 'npm install -g' : config.nodeManager === 'pnpm' ? 'pnpm add -g' : 'bun install -g'} openclaw@latest
echo "正在运行 OpenClaw 自动化配置向导..."
openclaw onboard ${joinedArgs}
echo "安装与配置完成!"
}
install_openclaw
`;
} else {
return `# Windows PowerShell 自动化脚本 (建议以管理员身份运行)
# 将代码包裹在函数中,避免直接执行失败时导致终端异常
function Install-OpenClaw {
# 检查当前系统环境是否安装了所选的包管理器
if (!(Get-Command ${config.nodeManager} -ErrorAction SilentlyContinue)) {
Write-Host "错误: 未在您的系统中检测到 \`${config.nodeManager}\` 环境。" -ForegroundColor Red
Write-Host "请先下载并安装 Node.js (https://nodejs.org) 或相应的包管理工具后再运行此脚本。" -ForegroundColor Yellow
return
}
Write-Host "正在使用 ${config.nodeManager} 全局安装 OpenClaw 最新版..."
${config.nodeManager === 'npm' ? 'npm install -g' : config.nodeManager === 'pnpm' ? 'pnpm add -g' : 'bun install -g'} openclaw@latest
Write-Host "正在运行 OpenClaw 自动化配置向导..."
openclaw onboard ${joinedArgs}
Write-Host "安装与配置完成!"
}
Install-OpenClaw
`;
}
};