fix: resolve open GitHub issues (#142 #139 #136 #131 #127 #124 #132 #125)

- install.sh: clean invalid binding 'pattern' field (Closes #142)
- server.py: make dispatch channel configurable via agent_config.json (Closes #139)
- server.py: add POST /api/set-dispatch-channel endpoint
- sync_agent_config.py: preserve dispatchChannel across syncs
- ModelConfig.tsx/api.ts: add dispatch channel selector UI
- install.ps1: add Windows PowerShell install script (Closes #136)
- Verify gongbu/bingbu SOUL.md are correct and consistent (Closes #131)
- Already fixed in prior commit: Closes #127, Closes #124, Closes #132, Closes #125
This commit is contained in:
cft0808
2026-03-16 22:08:33 +08:00
parent 689b1eafbf
commit 02dfdcd8b0
8 changed files with 418 additions and 29 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>三省六部 · Edict Dashboard</title>
<script type="module" crossorigin src="/assets/index-J5u1Q_A5.js"></script>
<script type="module" crossorigin src="/assets/index-DQ-p_wPk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-NQIHw-yB.css">
</head>
<body>

View File

@@ -1952,8 +1952,11 @@ def dispatch_for_state(task_id, task, new_state, trigger='state-transition'):
'lastDispatchTrigger': trigger,
}))
return
# Fix #139: dispatch channel 可配置(默认 feishu支持 telegram/wecom/signal 等)
_agent_cfg = read_json(DATA / 'agent_config.json', {})
_channel = (_agent_cfg.get('dispatchChannel') or 'feishu').strip()
cmd = ['openclaw', 'agent', '--agent', agent_id, '-m', msg,
'--deliver', '--channel', 'feishu', '--timeout', '300']
'--deliver', '--channel', _channel, '--timeout', '300']
max_retries = 2
err = ''
for attempt in range(1, max_retries + 1):
@@ -2466,6 +2469,19 @@ class Handler(BaseHTTPRequestHandler):
threading.Thread(target=apply_async, daemon=True).start()
self.send_json({'ok': True, 'message': f'Queued: {agent_id}{model}'})
# Fix #139: 设置派发渠道feishu/telegram/wecom/signal/tui
elif p == '/api/set-dispatch-channel':
channel = body.get('channel', '').strip()
allowed = {'feishu', 'telegram', 'wecom', 'signal', 'tui', 'discord', 'slack'}
if not channel or channel not in allowed:
self.send_json({'ok': False, 'error': f'channel must be one of: {", ".join(sorted(allowed))}'}, 400)
return
def _set_channel(cfg):
cfg['dispatchChannel'] = channel
return cfg
atomic_json_update(DATA / 'agent_config.json', _set_channel, {})
self.send_json({'ok': True, 'message': f'派发渠道已切换为 {channel}'})
# ── 朝堂议政 POST ──
elif p == '/api/court-discuss/start':
topic = body.get('topic', '').strip()

View File

@@ -49,6 +49,8 @@ export const api = {
// 操作类
setModel: (agentId: string, model: string) =>
postJ<ActionResult>(`${API_BASE}/api/set-model`, { agentId, model }),
setDispatchChannel: (channel: string) =>
postJ<ActionResult>(`${API_BASE}/api/set-dispatch-channel`, { channel }),
agentWake: (agentId: string) =>
postJ<ActionResult>(`${API_BASE}/api/agent-wake`, { agentId }),
taskAction: (taskId: string, action: string, reason: string) =>
@@ -190,6 +192,7 @@ export interface KnownModel {
export interface AgentConfig {
agents: AgentInfo[];
knownModels?: KnownModel[];
dispatchChannel?: string;
}
export interface ChangeLogEntry {

View File

@@ -15,6 +15,16 @@ const FALLBACK_MODELS = [
{ id: 'copilot/gemini-2.5-pro', l: 'Gemini 2.5 Pro', p: 'Copilot' },
];
const CHANNELS = [
{ id: 'feishu', label: '飞书 Feishu' },
{ id: 'telegram', label: 'Telegram' },
{ id: 'wecom', label: '企业微信 WeCom' },
{ id: 'discord', label: 'Discord' },
{ id: 'slack', label: 'Slack' },
{ id: 'signal', label: 'Signal' },
{ id: 'tui', label: 'TUI (终端)' },
];
export default function ModelConfig() {
const agentConfig = useStore((s) => s.agentConfig);
const changeLog = useStore((s) => s.changeLog);
@@ -23,6 +33,8 @@ export default function ModelConfig() {
const [selMap, setSelMap] = useState<Record<string, string>>({});
const [statusMap, setStatusMap] = useState<Record<string, { cls: string; text: string }>>({});
const [channelSel, setChannelSel] = useState('feishu');
const [channelStatus, setChannelStatus] = useState('');
useEffect(() => {
loadAgentConfig();
@@ -36,6 +48,9 @@ export default function ModelConfig() {
});
setSelMap(m);
}
if (agentConfig?.dispatchChannel) {
setChannelSel(agentConfig.dispatchChannel);
}
}, [agentConfig]);
if (!agentConfig?.agents) {
@@ -116,6 +131,30 @@ export default function ModelConfig() {
})}
</div>
{/* Dispatch Channel 配置 */}
<div style={{ marginTop: 24, marginBottom: 8 }}>
<div className="sec-title"></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0' }}>
<select className="msel" value={channelSel} onChange={(e) => setChannelSel(e.target.value)}
style={{ maxWidth: 220 }}>
{CHANNELS.map((ch) => (
<option key={ch.id} value={ch.id}>{ch.label}</option>
))}
</select>
<button className="btn btn-p" disabled={channelSel === (agentConfig?.dispatchChannel || 'feishu')}
onClick={async () => {
try {
const r = await api.setDispatchChannel(channelSel);
if (r.ok) { setChannelStatus('✅ 已保存'); toast('派发渠道已切换', 'ok'); loadAgentConfig(); }
else setChannelStatus('❌ ' + (r.error || '失败'));
} catch { setChannelStatus('❌ 无法连接'); }
setTimeout(() => setChannelStatus(''), 3000);
}}></button>
{channelStatus && <span style={{ fontSize: 12, color: channelStatus.startsWith('✅') ? 'var(--success)' : 'var(--danger)' }}>{channelStatus}</span>}
</div>
<div style={{ fontSize: 11, color: 'var(--muted)' }}>使 OpenClaw openclaw.json channel</div>
</div>
{/* Change Log */}
<div style={{ marginTop: 24 }}>
<div className="sec-title"></div>

308
install.ps1 Normal file
View File

@@ -0,0 +1,308 @@
# ══════════════════════════════════════════════════════════════
# 三省六部 · OpenClaw Multi-Agent System 一键安装脚本 (Windows)
# PowerShell 版本 — 对应 install.sh
# ══════════════════════════════════════════════════════════════
#Requires -Version 5.1
$ErrorActionPreference = "Stop"
$REPO_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$OC_HOME = Join-Path $env:USERPROFILE ".openclaw"
$OC_CFG = Join-Path $OC_HOME "openclaw.json"
function Write-Banner {
Write-Host ""
Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Blue
Write-Host "║ 🏛️ 三省六部 · OpenClaw Multi-Agent ║" -ForegroundColor Blue
Write-Host "║ 安装向导 (Windows) ║" -ForegroundColor Blue
Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Blue
Write-Host ""
}
function Log { param($msg) Write-Host "$msg" -ForegroundColor Green }
function Warn { param($msg) Write-Host "⚠️ $msg" -ForegroundColor Yellow }
function Error { param($msg) Write-Host "$msg" -ForegroundColor Red }
function Info { param($msg) Write-Host " $msg" -ForegroundColor Blue }
# ── Step 0: 依赖检查 ──
function Check-Deps {
Info "检查依赖..."
$oc = Get-Command openclaw -ErrorAction SilentlyContinue
if (-not $oc) {
Error "未找到 openclaw CLI。请先安装 OpenClaw: https://openclaw.ai"
exit 1
}
Log "OpenClaw CLI: OK"
$py = Get-Command python3 -ErrorAction SilentlyContinue
if (-not $py) {
$py = Get-Command python -ErrorAction SilentlyContinue
}
if (-not $py) {
Error "未找到 python3 或 python"
exit 1
}
$global:PYTHON = $py.Source
Log "Python: $($global:PYTHON)"
if (-not (Test-Path $OC_CFG)) {
Error "未找到 openclaw.json。请先运行 openclaw 完成初始化。"
exit 1
}
Log "openclaw.json: $OC_CFG"
}
# ── Step 0.5: 备份已有 Agent 数据 ──
function Backup-Existing {
$hasExisting = Get-ChildItem -Path $OC_HOME -Directory -Filter "workspace-*" -ErrorAction SilentlyContinue
if ($hasExisting) {
Info "检测到已有 Agent Workspace自动备份中..."
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
$backupDir = Join-Path $OC_HOME "backups\pre-install-$ts"
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
Get-ChildItem -Path $OC_HOME -Directory -Filter "workspace-*" | ForEach-Object {
Copy-Item -Path $_.FullName -Destination (Join-Path $backupDir $_.Name) -Recurse
}
if (Test-Path $OC_CFG) {
Copy-Item $OC_CFG (Join-Path $backupDir "openclaw.json")
}
Log "已备份到: $backupDir"
}
}
# ── Step 1: 创建 Workspace ──
function Create-Workspaces {
Info "创建 Agent Workspace..."
$agents = @("taizi","zhongshu","menxia","shangshu","hubu","libu","bingbu","xingbu","gongbu","libu_hr","zaochao")
foreach ($agent in $agents) {
$ws = Join-Path $OC_HOME "workspace-$agent"
New-Item -ItemType Directory -Path (Join-Path $ws "skills") -Force | Out-Null
$soulSrc = Join-Path $REPO_DIR "agents\$agent\SOUL.md"
$soulDst = Join-Path $ws "SOUL.md"
if (Test-Path $soulSrc) {
if (Test-Path $soulDst) {
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
Copy-Item $soulDst "$soulDst.bak.$ts"
Warn "已备份旧 SOUL.md → $soulDst.bak.$ts"
}
$content = (Get-Content $soulSrc -Raw) -replace "__REPO_DIR__", $REPO_DIR
Set-Content -Path $soulDst -Value $content -Encoding UTF8
}
Log "Workspace 已创建: $ws"
# AGENTS.md
$agentsMd = @"
# AGENTS.md ·
1. ""
2. ID/
3.
4. /
"@
Set-Content -Path (Join-Path $ws "AGENTS.md") -Value $agentsMd -Encoding UTF8
}
}
# ── Step 2: 注册 Agents ──
function Register-Agents {
Info "注册三省六部 Agents..."
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
Copy-Item $OC_CFG "$OC_CFG.bak.sansheng-$ts"
Log "已备份配置: $OC_CFG.bak.*"
$pyScript = @"
import json, pathlib, sys, os
cfg_path = pathlib.Path(os.environ['USERPROFILE']) / '.openclaw' / 'openclaw.json'
cfg = json.loads(cfg_path.read_text(encoding='utf-8'))
AGENTS = [
{"id": "taizi", "subagents": {"allowAgents": ["zhongshu"]}},
{"id": "zhongshu", "subagents": {"allowAgents": ["menxia", "shangshu"]}},
{"id": "menxia", "subagents": {"allowAgents": ["shangshu", "zhongshu"]}},
{"id": "shangshu", "subagents": {"allowAgents": ["zhongshu", "menxia", "hubu", "libu", "bingbu", "xingbu", "gongbu", "libu_hr"]}},
{"id": "hubu", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "libu", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "bingbu", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "xingbu", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "gongbu", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "libu_hr", "subagents": {"allowAgents": ["shangshu"]}},
{"id": "zaochao", "subagents": {"allowAgents": []}},
]
agents_cfg = cfg.setdefault('agents', {})
agents_list = agents_cfg.get('list', [])
existing_ids = {a['id'] for a in agents_list}
added = 0
for ag in AGENTS:
ag_id = ag['id']
ws = str(pathlib.Path(os.environ['USERPROFILE']) / f'.openclaw/workspace-{ag_id}')
if ag_id not in existing_ids:
entry = {'id': ag_id, 'workspace': ws, **{k:v for k,v in ag.items() if k!='id'}}
agents_list.append(entry)
added += 1
print(f' + added: {ag_id}')
else:
print(f' ~ exists: {ag_id} (skipped)')
agents_cfg['list'] = agents_list
# Fix #142: clean invalid binding pattern
bindings = cfg.get('bindings', [])
for b in bindings:
match = b.get('match', {})
if isinstance(match, dict) and 'pattern' in match:
del match['pattern']
print(f' cleaned invalid pattern from binding: {b.get("agentId", "?")}')
cfg_path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding='utf-8')
print(f'Done: {added} agents added')
"@
& $global:PYTHON -c $pyScript
Log "Agents 注册完成"
}
# ── Step 3: 初始化 Data ──
function Init-Data {
Info "初始化数据目录..."
$dataDir = Join-Path $REPO_DIR "data"
New-Item -ItemType Directory -Path $dataDir -Force | Out-Null
foreach ($f in @("live_status.json","agent_config.json","model_change_log.json")) {
$fp = Join-Path $dataDir $f
if (-not (Test-Path $fp)) { Set-Content $fp "{}" -Encoding UTF8 }
}
Set-Content (Join-Path $dataDir "pending_model_changes.json") "[]" -Encoding UTF8
Log "数据目录初始化完成"
}
# ── Step 3.3: 创建 data/scripts 目录连接 (Junction) ──
function Link-Resources {
Info "创建 data/scripts 目录连接..."
$linked = 0
$agents = @("taizi","zhongshu","menxia","shangshu","hubu","libu","bingbu","xingbu","gongbu","libu_hr","zaochao")
foreach ($agent in $agents) {
$ws = Join-Path $OC_HOME "workspace-$agent"
New-Item -ItemType Directory -Path $ws -Force | Out-Null
# data 目录
$wsData = Join-Path $ws "data"
$srcData = Join-Path $REPO_DIR "data"
if (-not (Test-Path $wsData)) {
cmd /c mklink /J "$wsData" "$srcData" | Out-Null
$linked++
} elseif (-not ((Get-Item $wsData).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
Rename-Item $wsData "$wsData.bak.$ts"
cmd /c mklink /J "$wsData" "$srcData" | Out-Null
$linked++
}
# scripts 目录
$wsScripts = Join-Path $ws "scripts"
$srcScripts = Join-Path $REPO_DIR "scripts"
if (-not (Test-Path $wsScripts)) {
cmd /c mklink /J "$wsScripts" "$srcScripts" | Out-Null
$linked++
} elseif (-not ((Get-Item $wsScripts).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
Rename-Item $wsScripts "$wsScripts.bak.$ts"
cmd /c mklink /J "$wsScripts" "$srcScripts" | Out-Null
$linked++
}
}
Log "已创建 $linked 个目录连接 (data/scripts → 项目目录)"
}
# ── Step 3.5: 设置 Agent 间通信可见性 ──
function Setup-Visibility {
Info "配置 Agent 间消息可见性..."
try {
openclaw config set tools.sessions.visibility all 2>$null
Log "已设置 tools.sessions.visibility=all"
} catch {
Warn "设置 visibility 失败,请手动执行: openclaw config set tools.sessions.visibility all"
}
}
# ── Step 4: 构建前端 ──
function Build-Frontend {
Info "构建 React 前端..."
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) {
Warn "未找到 node跳过前端构建。"
Warn "请安装 Node.js 18+ 后运行: cd edict\frontend && npm install && npm run build"
return
}
$pkgJson = Join-Path $REPO_DIR "edict\frontend\package.json"
if (Test-Path $pkgJson) {
Push-Location (Join-Path $REPO_DIR "edict\frontend")
npm install --silent 2>$null
npm run build 2>$null
Pop-Location
$indexHtml = Join-Path $REPO_DIR "dashboard\dist\index.html"
if (Test-Path $indexHtml) {
Log "前端构建完成: dashboard\dist\"
} else {
Warn "前端构建可能失败,请手动检查"
}
}
}
# ── Step 5: 首次数据同步 ──
function First-Sync {
Info "执行首次数据同步..."
Push-Location $REPO_DIR
$env:REPO_DIR = $REPO_DIR
try { & $global:PYTHON scripts/sync_agent_config.py } catch { Warn "sync_agent_config 有警告" }
try { & $global:PYTHON scripts/sync_officials_stats.py } catch { Warn "sync_officials_stats 有警告" }
try { & $global:PYTHON scripts/refresh_live_data.py } catch { Warn "refresh_live_data 有警告" }
Pop-Location
Log "首次同步完成"
}
# ── Step 6: 重启 Gateway ──
function Restart-Gateway {
Info "重启 OpenClaw Gateway..."
try {
openclaw gateway restart 2>$null
Log "Gateway 重启成功"
} catch {
Warn "Gateway 重启失败,请手动重启: openclaw gateway restart"
}
}
# ── Main ──
Write-Banner
Check-Deps
Backup-Existing
Create-Workspaces
Register-Agents
Init-Data
Link-Resources
Setup-Visibility
Build-Frontend
First-Sync
Restart-Gateway
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "║ 🎉 三省六部安装完成! ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
Write-Host "下一步:"
Write-Host " 1. 配置 API Key如尚未配置:"
Write-Host " openclaw agents add taizi # 按提示输入 Anthropic API Key"
Write-Host " .\install.ps1 # 重新运行以同步到所有 Agent"
Write-Host " 2. 启动数据刷新循环: Start-Process python3 -ArgumentList 'scripts/run_loop.sh'"
Write-Host " 3. 启动看板服务器: python3 dashboard/server.py"
Write-Host " 4. 打开看板: http://127.0.0.1:7891"
Write-Host ""
Warn "首次安装必须配置 API Key否则 Agent 会报错"
Info "文档: docs/getting-started.md"

View File

@@ -165,6 +165,19 @@ for ag in AGENTS:
print(f' ~ exists: {ag_id} (skipped)')
agents_cfg['list'] = agents_list
# Fix #142: 清理 bindings 中的非法字段pattern 不被 gateway 支持)
bindings = cfg.get('bindings', [])
cleaned = 0
for b in bindings:
match = b.get('match', {})
if isinstance(match, dict) and 'pattern' in match:
del match['pattern']
cleaned += 1
print(f' 🧹 cleaned invalid "pattern" from binding: {b.get("agentId", "?")}')
if cleaned:
print(f'Cleaned {cleaned} invalid binding field(s)')
cfg_path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2))
print(f'Done: {added} agents added')
PYEOF

View File

@@ -165,10 +165,20 @@ def main():
'isDefaultModel': True,
})
# 保留已有的 dispatchChannel 配置 (Fix #139)
existing_cfg = {}
cfg_path = DATA / 'agent_config.json'
if cfg_path.exists():
try:
existing_cfg = json.loads(cfg_path.read_text())
except Exception:
pass
payload = {
'generatedAt': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'defaultModel': default_model,
'knownModels': merged_models,
'dispatchChannel': existing_cfg.get('dispatchChannel', 'feishu'),
'agents': result,
}
DATA.mkdir(exist_ok=True)