fix: migrate clawdbot CLI calls to gateway RPC and improve session labels (#407)
Replace all `runClawdbot(['-c', ...])` invocations with `callOpenClawGateway()` RPC calls in session control, spawn, and delete routes. This eliminates the deprecated CLI dependency and uses the structured gateway protocol instead. Also add `formatSessionLabel()` helper and `useAgentSessions()` hook to task-board-panel for better session dropdown display labels. Closes #401, closes #406
This commit is contained in:
parent
85b4184aa9
commit
17bf0761f5
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireRole } from '@/lib/auth'
|
||||
import { runClawdbot } from '@/lib/command'
|
||||
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
||||
import { db_helpers } from '@/lib/db'
|
||||
import { mutationLimiter } from '@/lib/rate-limit'
|
||||
import { logger } from '@/lib/logger'
|
||||
|
|
@ -36,20 +36,14 @@ export async function POST(
|
|||
)
|
||||
}
|
||||
|
||||
let result
|
||||
let result: unknown
|
||||
if (action === 'terminate') {
|
||||
result = await runClawdbot(
|
||||
['-c', `sessions_kill("${id}")`],
|
||||
{ timeoutMs: 10000 }
|
||||
)
|
||||
result = await callOpenClawGateway('sessions_kill', { sessionKey: id }, 10_000)
|
||||
} else {
|
||||
const message = action === 'monitor'
|
||||
? JSON.stringify({ type: 'control', action: 'monitor' })
|
||||
: JSON.stringify({ type: 'control', action: 'pause' })
|
||||
result = await runClawdbot(
|
||||
['-c', `sessions_send("${id}", ${JSON.stringify(message)})`],
|
||||
{ timeoutMs: 10000 }
|
||||
)
|
||||
? { type: 'control', action: 'monitor' }
|
||||
: { type: 'control', action: 'pause' }
|
||||
result = await callOpenClawGateway('sessions_send', { sessionKey: id, message }, 10_000)
|
||||
}
|
||||
|
||||
db_helpers.logActivity(
|
||||
|
|
@ -65,7 +59,7 @@ export async function POST(
|
|||
success: true,
|
||||
action,
|
||||
session: id,
|
||||
stdout: result.stdout.trim(),
|
||||
result,
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error({ err: error }, 'Session control error')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { scanCodexSessions } from '@/lib/codex-sessions'
|
|||
import { scanHermesSessions } from '@/lib/hermes-sessions'
|
||||
import { getDatabase, db_helpers } from '@/lib/db'
|
||||
import { requireRole } from '@/lib/auth'
|
||||
import { runClawdbot } from '@/lib/command'
|
||||
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
||||
import { mutationLimiter } from '@/lib/rate-limit'
|
||||
import { logger } from '@/lib/logger'
|
||||
|
||||
|
|
@ -60,7 +60,8 @@ export async function POST(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
||||
}
|
||||
|
||||
let rpcFn: string
|
||||
let rpcMethod: string
|
||||
let rpcParams: Record<string, unknown>
|
||||
let logDetail: string
|
||||
|
||||
switch (action) {
|
||||
|
|
@ -69,7 +70,8 @@ export async function POST(request: NextRequest) {
|
|||
if (!VALID_THINKING_LEVELS.includes(level)) {
|
||||
return NextResponse.json({ error: `Invalid thinking level. Must be: ${VALID_THINKING_LEVELS.join(', ')}` }, { status: 400 })
|
||||
}
|
||||
rpcFn = `session_setThinking("${sessionKey}", "${level}")`
|
||||
rpcMethod = 'session_setThinking'
|
||||
rpcParams = { sessionKey, level }
|
||||
logDetail = `Set thinking=${level} on ${sessionKey}`
|
||||
break
|
||||
}
|
||||
|
|
@ -78,7 +80,8 @@ export async function POST(request: NextRequest) {
|
|||
if (!VALID_VERBOSE_LEVELS.includes(level)) {
|
||||
return NextResponse.json({ error: `Invalid verbose level. Must be: ${VALID_VERBOSE_LEVELS.join(', ')}` }, { status: 400 })
|
||||
}
|
||||
rpcFn = `session_setVerbose("${sessionKey}", "${level}")`
|
||||
rpcMethod = 'session_setVerbose'
|
||||
rpcParams = { sessionKey, level }
|
||||
logDetail = `Set verbose=${level} on ${sessionKey}`
|
||||
break
|
||||
}
|
||||
|
|
@ -87,7 +90,8 @@ export async function POST(request: NextRequest) {
|
|||
if (!VALID_REASONING_LEVELS.includes(level)) {
|
||||
return NextResponse.json({ error: `Invalid reasoning level. Must be: ${VALID_REASONING_LEVELS.join(', ')}` }, { status: 400 })
|
||||
}
|
||||
rpcFn = `session_setReasoning("${sessionKey}", "${level}")`
|
||||
rpcMethod = 'session_setReasoning'
|
||||
rpcParams = { sessionKey, level }
|
||||
logDetail = `Set reasoning=${level} on ${sessionKey}`
|
||||
break
|
||||
}
|
||||
|
|
@ -96,7 +100,8 @@ export async function POST(request: NextRequest) {
|
|||
if (typeof label !== 'string' || label.length > 100) {
|
||||
return NextResponse.json({ error: 'Label must be a string up to 100 characters' }, { status: 400 })
|
||||
}
|
||||
rpcFn = `session_setLabel("${sessionKey}", ${JSON.stringify(label)})`
|
||||
rpcMethod = 'session_setLabel'
|
||||
rpcParams = { sessionKey, label }
|
||||
logDetail = `Set label="${label}" on ${sessionKey}`
|
||||
break
|
||||
}
|
||||
|
|
@ -104,7 +109,7 @@ export async function POST(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Invalid action. Must be: set-thinking, set-verbose, set-reasoning, set-label' }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await runClawdbot(['-c', rpcFn], { timeoutMs: 10000 })
|
||||
const result = await callOpenClawGateway(rpcMethod, rpcParams, 10_000)
|
||||
|
||||
db_helpers.logActivity(
|
||||
'session_control',
|
||||
|
|
@ -115,7 +120,7 @@ export async function POST(request: NextRequest) {
|
|||
{ session_key: sessionKey, action }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, action, sessionKey, stdout: result.stdout.trim() })
|
||||
return NextResponse.json({ success: true, action, sessionKey, result })
|
||||
} catch (error: any) {
|
||||
logger.error({ err: error }, 'Session POST error')
|
||||
return NextResponse.json({ error: error.message || 'Session action failed' }, { status: 500 })
|
||||
|
|
@ -137,10 +142,7 @@ export async function DELETE(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await runClawdbot(
|
||||
['-c', `session_delete("${sessionKey}")`],
|
||||
{ timeoutMs: 10000 }
|
||||
)
|
||||
const result = await callOpenClawGateway('session_delete', { sessionKey }, 10_000)
|
||||
|
||||
db_helpers.logActivity(
|
||||
'session_control',
|
||||
|
|
@ -151,7 +153,7 @@ export async function DELETE(request: NextRequest) {
|
|||
{ session_key: sessionKey, action: 'delete' }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, sessionKey, stdout: result.stdout.trim() })
|
||||
return NextResponse.json({ success: true, sessionKey, result })
|
||||
} catch (error: any) {
|
||||
logger.error({ err: error }, 'Session DELETE error')
|
||||
return NextResponse.json({ error: error.message || 'Session deletion failed' }, { status: 500 })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { runClawdbot } from '@/lib/command'
|
||||
import { requireRole } from '@/lib/auth'
|
||||
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
||||
import { config } from '@/lib/config'
|
||||
import { readdir, readFile, stat } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
|
@ -14,11 +14,6 @@ function getPreferredToolsProfile(): string {
|
|||
return String(process.env.OPENCLAW_TOOLS_PROFILE || 'coding').trim() || 'coding'
|
||||
}
|
||||
|
||||
async function runSpawnWithCompatibility(spawnPayload: Record<string, unknown>) {
|
||||
const commandArg = `sessions_spawn(${JSON.stringify(spawnPayload)})`
|
||||
return runClawdbot(['-c', commandArg], { timeoutMs: 10000 })
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = requireRole(request, 'operator')
|
||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
|
|
@ -68,19 +63,14 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
try {
|
||||
// Execute the spawn command (OpenClaw 2026.3.2+ defaults tools.profile to messaging).
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
// Call gateway sessions_spawn directly. Try with tools.profile first,
|
||||
// fall back without it for older gateways that don't support the field.
|
||||
let result: any
|
||||
let compatibilityFallbackUsed = false
|
||||
try {
|
||||
const result = await runSpawnWithCompatibility(spawnPayload)
|
||||
stdout = result.stdout
|
||||
stderr = result.stderr
|
||||
result = await callOpenClawGateway('sessions_spawn', spawnPayload, 15_000)
|
||||
} catch (firstError: any) {
|
||||
const rawErr = String(firstError?.stderr || firstError?.message || '').toLowerCase()
|
||||
// Only retry without tools.profile when the error specifically indicates the
|
||||
// gateway doesn't recognize the tools/profile fields. Other errors (auth,
|
||||
// network, model not found, etc.) should propagate immediately.
|
||||
const rawErr = String(firstError?.message || '').toLowerCase()
|
||||
const isToolsSchemaError =
|
||||
(rawErr.includes('unknown field') || rawErr.includes('unknown key') || rawErr.includes('invalid argument')) &&
|
||||
(rawErr.includes('tools') || rawErr.includes('profile'))
|
||||
|
|
@ -88,23 +78,11 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
const fallbackPayload = { ...spawnPayload }
|
||||
delete (fallbackPayload as any).tools
|
||||
const fallback = await runSpawnWithCompatibility(fallbackPayload)
|
||||
stdout = fallback.stdout
|
||||
stderr = fallback.stderr
|
||||
result = await callOpenClawGateway('sessions_spawn', fallbackPayload, 15_000)
|
||||
compatibilityFallbackUsed = true
|
||||
}
|
||||
|
||||
// Parse the response to extract session info
|
||||
let sessionInfo = null
|
||||
try {
|
||||
// Look for session information in stdout
|
||||
const sessionMatch = stdout.match(/Session created: (.+)/)
|
||||
if (sessionMatch) {
|
||||
sessionInfo = sessionMatch[1]
|
||||
}
|
||||
} catch (parseError) {
|
||||
logger.error({ err: parseError }, 'Failed to parse session info')
|
||||
}
|
||||
const sessionInfo = result?.sessionId || result?.session_id || null
|
||||
|
||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
||||
logAuditEvent({
|
||||
|
|
@ -131,8 +109,7 @@ export async function POST(request: NextRequest) {
|
|||
label,
|
||||
timeoutSeconds: timeout,
|
||||
createdAt: Date.now(),
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
result,
|
||||
compatibility: {
|
||||
toolsProfile: getPreferredToolsProfile(),
|
||||
fallbackUsed: compatibilityFallbackUsed,
|
||||
|
|
@ -141,7 +118,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
} catch (execError: any) {
|
||||
logger.error({ err: execError }, 'Spawn execution error')
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
spawnId,
|
||||
|
|
|
|||
|
|
@ -95,22 +95,43 @@ const STATUS_COLUMN_KEYS = [
|
|||
{ key: 'done', titleKey: 'colDone', color: 'bg-green-500/20 text-green-400' },
|
||||
]
|
||||
|
||||
/** Build a human-readable label for a session key like "agent:nefes:telegram-group-123" */
|
||||
function formatSessionLabel(s: { key: string; channel?: string; kind?: string; label?: string }): string {
|
||||
if (s.label) return s.label
|
||||
// Extract the identifier part after the last colon: "agent:name:main" → "main"
|
||||
const parts = (s.key || '').split(':')
|
||||
const identifier = parts.length > 2 ? parts.slice(2).join(':') : s.key
|
||||
const channel = s.channel || ''
|
||||
if (channel && identifier !== 'main') {
|
||||
return `${channel} (${identifier})`
|
||||
}
|
||||
if (channel) return `${channel} (${s.kind || 'default'})`
|
||||
return identifier || s.key
|
||||
}
|
||||
|
||||
/** Fetch active gateway sessions for a given agent name. */
|
||||
function useAgentSessions(agentName: string | undefined) {
|
||||
const [sessions, setSessions] = useState<Array<{ key: string; id: string; channel?: string; label?: string }>>([])
|
||||
const [sessions, setSessions] = useState<Array<{ key: string; id: string; channel?: string; kind?: string; label?: string; displayLabel: string }>>([])
|
||||
useEffect(() => {
|
||||
if (!agentName) { setSessions([]); return }
|
||||
let cancelled = false
|
||||
fetch('/api/sessions?include_local=1')
|
||||
fetch('/api/sessions')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (cancelled) return
|
||||
const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; label?: string; active?: boolean }>
|
||||
const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; kind?: string; label?: string; active?: boolean }>
|
||||
const filtered = all.filter(s =>
|
||||
s.agent?.toLowerCase() === agentName.toLowerCase() ||
|
||||
s.key?.toLowerCase().includes(agentName.toLowerCase())
|
||||
)
|
||||
setSessions(filtered.map(s => ({ key: s.key, id: s.id, channel: s.channel, label: s.label })))
|
||||
setSessions(filtered.map(s => ({
|
||||
key: s.key,
|
||||
id: s.id,
|
||||
channel: s.channel,
|
||||
kind: s.kind,
|
||||
label: s.label,
|
||||
displayLabel: formatSessionLabel(s),
|
||||
})))
|
||||
})
|
||||
.catch(() => { if (!cancelled) setSessions([]) })
|
||||
return () => { cancelled = true }
|
||||
|
|
@ -2052,7 +2073,7 @@ function CreateTaskModal({
|
|||
<option value="">New session (default)</option>
|
||||
{agentSessions.map(s => (
|
||||
<option key={s.key} value={s.key}>
|
||||
{s.label || s.channel || s.key}
|
||||
{s.displayLabel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -2305,7 +2326,7 @@ function EditTaskModal({
|
|||
<option value="">New session (default)</option>
|
||||
{agentSessions.map(s => (
|
||||
<option key={s.key} value={s.key}>
|
||||
{s.label || s.channel || s.key}
|
||||
{s.displayLabel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
Loading…
Reference in New Issue