mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
feat(docker): 配置同步+性格注入+Gateway认证
This commit is contained in:
@@ -43,7 +43,7 @@ const NAV_ITEMS_FULL = [
|
||||
{
|
||||
section: '龙虾军团',
|
||||
items: [
|
||||
{ route: '/docker', label: '🦞 龙虾军团', icon: 'docker' },
|
||||
{ route: '/docker', label: '龙虾军团', icon: 'docker' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -64,10 +64,9 @@ const NAV_ITEMS_SETUP = [
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '扩展',
|
||||
section: '',
|
||||
items: [
|
||||
{ route: '/extensions', label: '扩展工具', icon: 'extensions' },
|
||||
{ route: '/skills', label: 'Skills', icon: 'skills' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -275,7 +274,7 @@ async function _toggleInstanceDropdown(sidebarEl) {
|
||||
if (!dd) return
|
||||
if (dd.classList.contains('open')) { dd.classList.remove('open'); return }
|
||||
|
||||
dd.innerHTML = '<div style="padding:8px;color:var(--text-tertiary);font-size:12px">loading...</div>'
|
||||
dd.innerHTML = '<div style="padding:8px;color:var(--text-tertiary);font-size:12px">加载中...</div>'
|
||||
dd.classList.add('open')
|
||||
|
||||
try {
|
||||
@@ -287,7 +286,7 @@ async function _toggleInstanceDropdown(sidebarEl) {
|
||||
const h = healthMap[inst.id] || {}
|
||||
const active = inst.id === activeId ? ' active' : ''
|
||||
const dot = h.online !== false ? 'online' : 'offline'
|
||||
const badge = inst.type === 'docker' ? '<span class="instance-badge">Docker</span>' : inst.type === 'remote' ? '<span class="instance-badge">Remote</span>' : ''
|
||||
const badge = inst.type === 'docker' ? '<span class="instance-badge docker">🦞 龙虾</span>' : inst.type === 'remote' ? '<span class="instance-badge remote">远程</span>' : ''
|
||||
html += `<div class="instance-option${active}" data-id="${inst.id}">
|
||||
<span class="instance-dot ${dot}"></span>
|
||||
<span class="instance-opt-name">${_escSidebar(inst.name)}</span>
|
||||
@@ -295,7 +294,7 @@ async function _toggleInstanceDropdown(sidebarEl) {
|
||||
</div>`
|
||||
}
|
||||
html += '<div class="instance-divider"></div>'
|
||||
html += '<div class="instance-option instance-add" id="btn-instance-add">+ Add Instance</div>'
|
||||
html += '<div class="instance-option instance-add" id="btn-instance-add">+ 添加实例</div>'
|
||||
dd.innerHTML = html
|
||||
} catch (e) {
|
||||
dd.innerHTML = `<div style="padding:8px;color:var(--error);font-size:12px">${_escSidebar(e.message)}</div>`
|
||||
@@ -307,27 +306,27 @@ async function _showAddInstanceDialog(sidebarEl) {
|
||||
overlay.className = 'docker-dialog-overlay'
|
||||
overlay.innerHTML = `
|
||||
<div class="docker-dialog">
|
||||
<div class="docker-dialog-title">Add Instance</div>
|
||||
<div class="docker-dialog-title">添加远程实例</div>
|
||||
<div class="form-group" style="margin-bottom:var(--space-md)">
|
||||
<label class="form-label">Name</label>
|
||||
<input class="form-input" id="inst-name" placeholder="My Server" />
|
||||
<label class="form-label">名称</label>
|
||||
<input class="form-input" id="inst-name" placeholder="远程服务器" />
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:var(--space-md)">
|
||||
<label class="form-label">Panel Endpoint</label>
|
||||
<label class="form-label">面板地址</label>
|
||||
<input class="form-input" id="inst-endpoint" placeholder="http://192.168.1.100:1420" />
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:var(--space-md)">
|
||||
<label class="form-label">Gateway Port (optional)</label>
|
||||
<label class="form-label">Gateway 端口(可选)</label>
|
||||
<input class="form-input" id="inst-gw-port" type="number" value="18789" />
|
||||
</div>
|
||||
<div class="docker-dialog-hint">
|
||||
The remote server must be running ClawPanel (serve.js).<br/>
|
||||
Example: <code>http://192.168.1.100:1420</code>
|
||||
远程服务器需要运行 ClawPanel (serve.js)。<br/>
|
||||
示例: <code>http://192.168.1.100:1420</code>
|
||||
</div>
|
||||
<div id="inst-add-error" style="color:var(--error);font-size:12px;margin-top:var(--space-sm)"></div>
|
||||
<div class="docker-dialog-actions">
|
||||
<button class="btn btn-secondary btn-sm" id="inst-cancel">Cancel</button>
|
||||
<button class="btn btn-primary btn-sm" id="inst-confirm">Add</button>
|
||||
<button class="btn btn-secondary btn-sm" id="inst-cancel">取消</button>
|
||||
<button class="btn btn-primary btn-sm" id="inst-confirm">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -339,16 +338,16 @@ async function _showAddInstanceDialog(sidebarEl) {
|
||||
const endpoint = overlay.querySelector('#inst-endpoint').value.trim()
|
||||
const gwPort = parseInt(overlay.querySelector('#inst-gw-port').value) || 18789
|
||||
const errEl = overlay.querySelector('#inst-add-error')
|
||||
if (!name || !endpoint) { errEl.textContent = 'Name and endpoint are required'; return }
|
||||
if (!name || !endpoint) { errEl.textContent = '请填写名称和面板地址'; return }
|
||||
const btn = overlay.querySelector('#inst-confirm')
|
||||
btn.disabled = true; btn.textContent = 'Adding...'
|
||||
btn.disabled = true; btn.textContent = '添加中...'
|
||||
try {
|
||||
await api.instanceAdd({ name, type: 'remote', endpoint, gatewayPort: gwPort })
|
||||
overlay.remove()
|
||||
renderSidebar(sidebarEl)
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message || String(e)
|
||||
btn.disabled = false; btn.textContent = 'Add'
|
||||
btn.disabled = false; btn.textContent = '添加'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ const PATHS = {
|
||||
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
||||
'send': '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||||
'download': '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
||||
'upload': '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>',
|
||||
'inbox': '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/>',
|
||||
'radio': '<circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 010 8.49m-8.48-.01a6 6 0 010-8.49m11.31-2.82a10 10 0 010 14.14m-14.14 0a10 10 0 010-14.14"/>',
|
||||
'lightbulb': '<line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0018 8 6 6 0 006 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 018.91 14"/>',
|
||||
|
||||
@@ -26,7 +26,7 @@ const WEB_ONLY_CMDS = new Set([
|
||||
'docker_test_endpoint',
|
||||
'docker_info', 'docker_list_containers', 'docker_create_container',
|
||||
'docker_start_container', 'docker_stop_container', 'docker_restart_container',
|
||||
'docker_remove_container', 'docker_container_logs', 'docker_container_exec', 'docker_gateway_chat', 'docker_pull_image', 'docker_pull_status',
|
||||
'docker_remove_container', 'docker_container_logs', 'docker_container_exec', 'docker_init_worker', 'docker_gateway_chat', 'docker_pull_image', 'docker_pull_status',
|
||||
'docker_list_images', 'docker_list_nodes', 'docker_add_node', 'docker_remove_node',
|
||||
'docker_cluster_overview',
|
||||
'instance_list', 'instance_add', 'instance_remove', 'instance_set_active',
|
||||
@@ -422,6 +422,7 @@ export const api = {
|
||||
dockerRemoveContainer: (nodeId, containerId, force = false) => { invalidate('docker_cluster_overview', 'docker_list_containers'); return invoke('docker_remove_container', { nodeId, containerId, force }) },
|
||||
dockerContainerLogs: (nodeId, containerId, tail = 200) => invoke('docker_container_logs', { nodeId, containerId, tail }),
|
||||
dockerContainerExec: (nodeId, containerId, cmd) => invoke('docker_container_exec', { nodeId, containerId, cmd }),
|
||||
dockerInitWorker: (nodeId, containerId, role) => invoke('docker_init_worker', { nodeId, containerId, role }),
|
||||
dockerGatewayChat: (nodeId, containerId, message) => invoke('docker_gateway_chat', { nodeId, containerId, message }),
|
||||
dockerPullImage: (nodeId, image, tag, requestId) => invoke('docker_pull_image', { nodeId, image, tag, requestId }),
|
||||
dockerPullStatus: (requestId) => invoke('docker_pull_status', { requestId }),
|
||||
|
||||
@@ -41,101 +41,6 @@ function isManagedContainer(c) {
|
||||
return isOpenClawContainer(c) || getAdoptedIds().has(c.id)
|
||||
}
|
||||
|
||||
// 兵种性格模板 — 部署后写入容器 workspace 的 SOUL.md 和 IDENTITY.md
|
||||
const ROLE_SOULS = {
|
||||
general: {
|
||||
identity: '龙虾步兵 · 通用作战单位',
|
||||
soul: `你是一名通用龙虾步兵,隶属于统帅的龙虾军团。
|
||||
## 核心能力
|
||||
- 能处理各类任务:写作、编程、翻译、分析
|
||||
- 灵活应变,根据任务类型自动调整工作方式
|
||||
## 性格
|
||||
- 忠诚可靠,执行力强
|
||||
- 回复简洁专业,不废话
|
||||
- 主动报告任务进展和结果`,
|
||||
},
|
||||
coder: {
|
||||
identity: '龙虾突击兵 · 编程作战专家',
|
||||
soul: `你是一名编程突击兵,隶属于统帅的龙虾军团,专精代码作战。
|
||||
## 核心能力
|
||||
- 精通多种编程语言和框架
|
||||
- 擅长代码编写、调试、重构和 Code Review
|
||||
- 能快速定位 Bug 并提供修复方案
|
||||
## 性格
|
||||
- 严谨精确,代码质量第一
|
||||
- 回复包含可运行的代码示例
|
||||
- 主动提示潜在问题和最佳实践`,
|
||||
},
|
||||
translator: {
|
||||
identity: '龙虾翻译官 · 多语言作战专家',
|
||||
soul: `你是一名翻译官,隶属于统帅的龙虾军团,精通多国语言。
|
||||
## 核心能力
|
||||
- 精通中英日韩法德西等主流语言互译
|
||||
- 理解文化差异,翻译自然流畅
|
||||
- 支持技术文档、文学作品、商务邮件等多种文体
|
||||
## 性格
|
||||
- 追求信达雅,翻译精准
|
||||
- 保留原文语境和风格
|
||||
- 对专业术语严格把关`,
|
||||
},
|
||||
writer: {
|
||||
identity: '龙虾文书官 · 写作任务专家',
|
||||
soul: `你是一名文书官,隶属于统帅的龙虾军团,笔下生花。
|
||||
## 核心能力
|
||||
- 擅长文案撰写、文章创作、创意写作
|
||||
- 能调整语气风格适应不同场景
|
||||
- 精通各种文体:博客、新闻稿、技术文档、营销文案
|
||||
## 性格
|
||||
- 文思敏捷,创意丰富
|
||||
- 注重可读性和表达力
|
||||
- 善于讲故事,引人入胜`,
|
||||
},
|
||||
analyst: {
|
||||
identity: '龙虾参谋 · 数据分析专家',
|
||||
soul: `你是一名参谋,隶属于统帅的龙虾军团,运筹帷幄。
|
||||
## 核心能力
|
||||
- 擅长数据分析、趋势判断和战略规划
|
||||
- 能从复杂信息中提炼关键洞察
|
||||
- 精通统计分析、商业分析、竞品分析
|
||||
## 性格
|
||||
- 逻辑清晰,善用数据说话
|
||||
- 结论有理有据,给出可行建议
|
||||
- 善于用图表和结构化格式呈现分析结果`,
|
||||
},
|
||||
custom: {
|
||||
identity: '龙虾特种兵 · 特殊任务执行者',
|
||||
soul: `你是一名特种兵,隶属于统帅的龙虾军团,执行特殊任务。
|
||||
## 核心能力
|
||||
- 按统帅需求灵活配置技能
|
||||
- 适应各种非标准任务场景
|
||||
- 快速学习和适应新领域
|
||||
## 性格
|
||||
- 灵活多变,适应力强
|
||||
- 完成任务不拘泥于形式
|
||||
- 主动寻找最优解决方案`,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署后注入角色性格到容器 workspace
|
||||
* @param {string} nodeId
|
||||
* @param {string} containerId
|
||||
* @param {string} role
|
||||
*/
|
||||
async function _injectRolePersonality(nodeId, containerId, role) {
|
||||
const soul = ROLE_SOULS[role] || ROLE_SOULS.general
|
||||
try {
|
||||
// 创建 workspace 目录并写入 IDENTITY.md 和 SOUL.md
|
||||
await api.dockerContainerExec(nodeId, containerId, [
|
||||
'sh', '-c',
|
||||
`mkdir -p /root/.openclaw/workspace && echo '${soul.identity}' > /root/.openclaw/workspace/IDENTITY.md && cat > /root/.openclaw/workspace/SOUL.md << 'CLAWEOF'\n${soul.soul}\nCLAWEOF`
|
||||
])
|
||||
console.log(`[cluster] 角色性格已注入: ${role} → ${containerId}`)
|
||||
} catch (e) {
|
||||
console.warn(`[cluster] 角色性格注入失败: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 军事化术语 & 兵种系统
|
||||
const MILITARY = {
|
||||
roles: {
|
||||
@@ -406,7 +311,8 @@ function _renderUnitCard(c, showAdopt) {
|
||||
<div class="unit-actions">
|
||||
${isRunning
|
||||
? `<button class="btn-icon" data-action="stop" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" title="休整">${icon('stop', 14)}</button>
|
||||
<button class="btn-icon" data-action="restart" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" title="整编">${icon('refresh-cw', 14)}</button>`
|
||||
<button class="btn-icon" data-action="restart" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" title="整编">${icon('refresh-cw', 14)}</button>
|
||||
<button class="btn-icon" data-action="sync-config" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" data-name="${esc(c.name)}" data-role="${esc(role)}" title="同步配置(API Key + 性格 + 记忆)">${icon('upload', 14)}</button>`
|
||||
: `<button class="btn-icon" data-action="start" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" title="出征">${icon('play', 14)}</button>`
|
||||
}
|
||||
<button class="btn-icon" data-action="inspect" data-ct="${esc(c.id)}" data-node="${esc(c.nodeId)}" title="军情">${icon('search', 14)}</button>
|
||||
@@ -779,6 +685,24 @@ function bindEvents(page) {
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'sync-config') {
|
||||
const cid = btn.dataset.ct
|
||||
const nid = btn.dataset.node || null
|
||||
const name = btn.dataset.name || cid
|
||||
const role = btn.dataset.role || 'general'
|
||||
toast(`正在同步配置到 ${name}...`, 'info')
|
||||
try {
|
||||
const result = await api.dockerInitWorker(nid, cid, role)
|
||||
const count = result?.files?.length || 0
|
||||
// docker_init_worker 内部已重启 Gateway,不需要重启容器(重启会触发 entrypoint 覆盖配置)
|
||||
toast(`${name}: 已同步 ${count} 个文件,Gateway 已重启`, 'success')
|
||||
setTimeout(() => refreshCluster(page), 3000)
|
||||
} catch (e) {
|
||||
toast(`${name} 同步失败: ${e.message}`, 'error')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'quick-chat') {
|
||||
const cid = btn.dataset.containerId
|
||||
const nid = btn.dataset.nodeId || null
|
||||
@@ -1240,18 +1164,25 @@ async function showDeployDialog(page, nodeId) {
|
||||
stepCreate.classList.add('done')
|
||||
createDetail.textContent = '完成'
|
||||
|
||||
// Step 3: 启动完成
|
||||
// Step 3: 启动 + 初始化
|
||||
stepStart.classList.add('active')
|
||||
startDetail.textContent = '启动中...'
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
|
||||
// 注入角色性格(SOUL.md + IDENTITY.md)
|
||||
// 全套初始化:配置同步 + 性格注入 + 记忆同步 + MCP
|
||||
const selectedRoleForInject = overlay.querySelector('#dd-role')?.value || 'general'
|
||||
if (selectedRoleForInject !== 'custom') {
|
||||
startDetail.textContent = '注入角色性格...'
|
||||
const cid = result.id || result.containerId || name
|
||||
await _injectRolePersonality(nodeId, cid, selectedRoleForInject)
|
||||
const cid = result.id || result.containerId || name
|
||||
try {
|
||||
startDetail.textContent = '同步配置 & 注入性格...'
|
||||
const initResult = await api.dockerInitWorker(nodeId, cid, selectedRoleForInject)
|
||||
const synced = initResult?.files?.length || 0
|
||||
startDetail.textContent = `已同步 ${synced} 个文件`
|
||||
console.log('[deploy] 初始化结果:', initResult)
|
||||
} catch (e) {
|
||||
console.warn('[deploy] 初始化警告:', e.message)
|
||||
startDetail.textContent = '初始化部分失败(不影响运行)'
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
stepStart.classList.remove('active')
|
||||
stepStart.classList.add('done')
|
||||
|
||||
@@ -143,6 +143,14 @@
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.instance-badge.docker {
|
||||
background: rgba(231,76,60,.1);
|
||||
color: #e74c3c;
|
||||
}
|
||||
.instance-badge.remote {
|
||||
background: rgba(6,182,212,.1);
|
||||
color: #06b6d4;
|
||||
}
|
||||
.instance-divider {
|
||||
height: 1px;
|
||||
background: var(--border-secondary);
|
||||
|
||||
Reference in New Issue
Block a user