feat(docker): 配置同步+性格注入+Gateway认证

This commit is contained in:
晴天
2026-03-09 06:24:21 +08:00
parent 727903f94b
commit a084e23671
6 changed files with 258 additions and 133 deletions

View File

@@ -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 = '添加'
}
}
}

View File

@@ -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"/>',

View File

@@ -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 }),

View File

@@ -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')

View File

@@ -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);