feat(openclaw): improve dispatcher and integration
🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
@@ -14,6 +14,7 @@ import { createModelCacheState } from "./plugin-state"
|
|||||||
import { createFirstMessageVariantGate } from "./shared/first-message-variant"
|
import { createFirstMessageVariantGate } from "./shared/first-message-variant"
|
||||||
import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared"
|
import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared"
|
||||||
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
|
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
|
||||||
|
import { lspManager } from "./tools/lsp/client"
|
||||||
import { startTmuxCheck } from "./tools"
|
import { startTmuxCheck } from "./tools"
|
||||||
|
|
||||||
let activePluginDispose: PluginDispose | null = null
|
let activePluginDispose: PluginDispose | null = null
|
||||||
@@ -83,6 +84,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const dispose = createPluginDispose({
|
const dispose = createPluginDispose({
|
||||||
backgroundManager: managers.backgroundManager,
|
backgroundManager: managers.backgroundManager,
|
||||||
skillMcpManager: managers.skillMcpManager,
|
skillMcpManager: managers.skillMcpManager,
|
||||||
|
lspManager,
|
||||||
disposeHooks: hooks.disposeHooks,
|
disposeHooks: hooks.disposeHooks,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
interpolateInstruction,
|
interpolateInstruction,
|
||||||
resolveCommandTimeoutMs,
|
resolveCommandTimeoutMs,
|
||||||
shellEscapeArg,
|
shellEscapeArg,
|
||||||
|
terminateCommandProcess,
|
||||||
wakeGateway,
|
wakeGateway,
|
||||||
wakeCommandGateway,
|
wakeCommandGateway,
|
||||||
} from "../dispatcher"
|
} from "../dispatcher"
|
||||||
@@ -41,6 +42,10 @@ describe("OpenClaw Dispatcher", () => {
|
|||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(fetchSpy).toHaveBeenCalled()
|
expect(fetchSpy).toHaveBeenCalled()
|
||||||
const call = fetchSpy.mock.calls.find(c => c[0] === "https://example.com")
|
const call = fetchSpy.mock.calls.find(c => c[0] === "https://example.com")
|
||||||
|
expect(call).toBeDefined()
|
||||||
|
if (!call) {
|
||||||
|
throw new Error("Expected fetch call for https://example.com")
|
||||||
|
}
|
||||||
expect(call[0]).toBe("https://example.com")
|
expect(call[0]).toBe("https://example.com")
|
||||||
expect(call[1]?.method).toBe("POST")
|
expect(call[1]?.method).toBe("POST")
|
||||||
expect(call[1]?.body).toBe('{"foo":"bar"}')
|
expect(call[1]?.body).toBe('{"foo":"bar"}')
|
||||||
@@ -67,4 +72,40 @@ describe("OpenClaw Dispatcher", () => {
|
|||||||
else process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS = original
|
else process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS = original
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("terminateCommandProcess kills process group on unix when pid exists", () => {
|
||||||
|
const killSpy = spyOn(process, "kill").mockImplementation(() => true)
|
||||||
|
const proc = {
|
||||||
|
pid: 4321,
|
||||||
|
kill: mock(() => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
terminateCommandProcess(proc, "SIGKILL")
|
||||||
|
|
||||||
|
expect(killSpy).toHaveBeenCalledWith(-4321, "SIGKILL")
|
||||||
|
expect(proc.kill).not.toHaveBeenCalled()
|
||||||
|
} finally {
|
||||||
|
killSpy.mockRestore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("terminateCommandProcess falls back to direct kill when process group kill fails", () => {
|
||||||
|
const killSpy = spyOn(process, "kill").mockImplementation(() => {
|
||||||
|
throw new Error("group kill failed")
|
||||||
|
})
|
||||||
|
const proc = {
|
||||||
|
pid: 9876,
|
||||||
|
kill: mock(() => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
terminateCommandProcess(proc, "SIGKILL")
|
||||||
|
|
||||||
|
expect(killSpy).toHaveBeenCalledWith(-9876, "SIGKILL")
|
||||||
|
expect(proc.kill).toHaveBeenCalledWith("SIGKILL")
|
||||||
|
} finally {
|
||||||
|
killSpy.mockRestore()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -141,18 +141,17 @@ export async function wakeCommandGateway(
|
|||||||
return shellEscapeArg(value)
|
return shellEscapeArg(value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Always use sh -c to handle the shell command string correctly
|
|
||||||
const proc = spawn(["sh", "-c", interpolated], {
|
const proc = spawn(["sh", "-c", interpolated], {
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
|
detached: process.platform !== "win32",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle timeout manually
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
proc.kill()
|
terminateCommandProcess(proc, "SIGKILL")
|
||||||
reject(new Error("Command timed out"))
|
reject(new Error("Command timed out"))
|
||||||
}, timeout)
|
}, timeout)
|
||||||
})
|
})
|
||||||
@@ -178,3 +177,24 @@ export async function wakeCommandGateway(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KillableProcess = {
|
||||||
|
pid?: number
|
||||||
|
kill: (signal?: NodeJS.Signals) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function terminateCommandProcess(proc: KillableProcess, signal: NodeJS.Signals): void {
|
||||||
|
try {
|
||||||
|
if (process.platform !== "win32" && proc.pid) {
|
||||||
|
try {
|
||||||
|
process.kill(-proc.pid, signal)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
proc.kill(signal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.kill(signal)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user