mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
refactor: align pairing replies, daemon hints, and feishu mention policy
This commit is contained in:
@@ -233,10 +233,23 @@ describe("runServiceRestart token drift", () => {
|
||||
opts: { json: true },
|
||||
});
|
||||
|
||||
const payload = readJsonLog<{ ok?: boolean; result?: string; hints?: string[] }>();
|
||||
const payload = readJsonLog<{
|
||||
ok?: boolean;
|
||||
result?: string;
|
||||
hints?: string[];
|
||||
hintItems?: Array<{ kind: string; text: string }>;
|
||||
}>();
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.result).toBe("not-loaded");
|
||||
expect(payload.hints).toEqual(expect.arrayContaining(["openclaw gateway install"]));
|
||||
expect(payload.hintItems).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "install",
|
||||
text: "openclaw gateway install",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(service.restart).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,8 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveGatewayTokenForDriftCheck } from "./gateway-token-drift.js";
|
||||
import {
|
||||
buildDaemonServiceSnapshot,
|
||||
createNullWriter,
|
||||
type DaemonAction,
|
||||
createDaemonActionContext,
|
||||
type DaemonActionResponse,
|
||||
emitDaemonActionJson,
|
||||
} from "./response.js";
|
||||
import { filterContainerGenericHints } from "./shared.js";
|
||||
|
||||
@@ -58,28 +56,9 @@ async function maybeAugmentSystemdHints(hints: string[]): Promise<string[]> {
|
||||
];
|
||||
}
|
||||
|
||||
function createActionIO(params: { action: DaemonAction; json: boolean }) {
|
||||
const stdout = params.json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: Omit<DaemonActionResponse, "action">) => {
|
||||
if (!params.json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: params.action, ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (params.json) {
|
||||
emit({ ok: false, error: message, hints });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
return { stdout, emit, fail };
|
||||
}
|
||||
|
||||
function emitActionMessage(params: {
|
||||
json: boolean;
|
||||
emit: ReturnType<typeof createActionIO>["emit"];
|
||||
emit: ReturnType<typeof createDaemonActionContext>["emit"];
|
||||
payload: Omit<DaemonActionResponse, "action">;
|
||||
}) {
|
||||
params.emit(params.payload);
|
||||
@@ -94,7 +73,7 @@ async function handleServiceNotLoaded(params: {
|
||||
loaded: boolean;
|
||||
renderStartHints: () => string[];
|
||||
json: boolean;
|
||||
emit: ReturnType<typeof createActionIO>["emit"];
|
||||
emit: ReturnType<typeof createDaemonActionContext>["emit"];
|
||||
}) {
|
||||
const hints = filterContainerGenericHints(
|
||||
await maybeAugmentSystemdHints(params.renderStartHints()),
|
||||
@@ -117,7 +96,7 @@ async function handleServiceNotLoaded(params: {
|
||||
async function resolveServiceLoadedOrFail(params: {
|
||||
serviceNoun: string;
|
||||
service: GatewayService;
|
||||
fail: ReturnType<typeof createActionIO>["fail"];
|
||||
fail: ReturnType<typeof createDaemonActionContext>["fail"];
|
||||
}): Promise<boolean | null> {
|
||||
try {
|
||||
return await params.service.isLoaded({ env: process.env });
|
||||
@@ -158,7 +137,7 @@ export async function runServiceUninstall(params: {
|
||||
assertNotLoadedAfterUninstall: boolean;
|
||||
}) {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "uninstall", json });
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "uninstall", json });
|
||||
|
||||
if (resolveIsNixMode(process.env)) {
|
||||
fail("Nix mode detected; service uninstall is disabled.");
|
||||
@@ -209,7 +188,7 @@ export async function runServiceStart(params: {
|
||||
opts?: DaemonLifecycleOptions;
|
||||
}) {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "start", json });
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "start", json });
|
||||
|
||||
if (
|
||||
(await resolveServiceLoadedOrFail({
|
||||
@@ -279,7 +258,7 @@ export async function runServiceStop(params: {
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
}) {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "stop", json });
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "stop", json });
|
||||
|
||||
const loaded = await resolveServiceLoadedOrFail({
|
||||
serviceNoun: params.serviceNoun,
|
||||
@@ -350,7 +329,7 @@ export async function runServiceRestart(params: {
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
}): Promise<boolean> {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "restart", json });
|
||||
const { stdout, emit, fail } = createDaemonActionContext({ action: "restart", json });
|
||||
const warnings: string[] = [];
|
||||
let handledNotLoaded: NotLoadedActionResult | null = null;
|
||||
const emitScheduledRestart = (
|
||||
|
||||
39
src/cli/daemon-cli/response.test.ts
Normal file
39
src/cli/daemon-cli/response.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDaemonHintItems } from "./response.js";
|
||||
|
||||
describe("buildDaemonHintItems", () => {
|
||||
it("classifies common daemon hint kinds", () => {
|
||||
expect(
|
||||
buildDaemonHintItems([
|
||||
"openclaw gateway install",
|
||||
"Restart the container or the service that manages it for openclaw-demo-container.",
|
||||
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
|
||||
"On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
|
||||
"If you're in a container, run the gateway in the foreground instead of `openclaw gateway`.",
|
||||
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
|
||||
]),
|
||||
).toEqual([
|
||||
{ kind: "install", text: "openclaw gateway install" },
|
||||
{
|
||||
kind: "container-restart",
|
||||
text: "Restart the container or the service that manages it for openclaw-demo-container.",
|
||||
},
|
||||
{
|
||||
kind: "systemd-unavailable",
|
||||
text: "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
|
||||
},
|
||||
{
|
||||
kind: "systemd-headless",
|
||||
text: "On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
|
||||
},
|
||||
{
|
||||
kind: "container-foreground",
|
||||
text: "If you're in a container, run the gateway in the foreground instead of `openclaw gateway`.",
|
||||
},
|
||||
{
|
||||
kind: "wsl-systemd",
|
||||
text: "WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,20 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
|
||||
export type DaemonAction = "install" | "uninstall" | "start" | "stop" | "restart";
|
||||
|
||||
export type DaemonHintKind =
|
||||
| "install"
|
||||
| "container-restart"
|
||||
| "container-foreground"
|
||||
| "systemd-unavailable"
|
||||
| "systemd-headless"
|
||||
| "wsl-systemd"
|
||||
| "generic";
|
||||
|
||||
export type DaemonHintItem = {
|
||||
kind: DaemonHintKind;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type DaemonActionResponse = {
|
||||
ok: boolean;
|
||||
action: DaemonAction;
|
||||
@@ -11,6 +25,7 @@ export type DaemonActionResponse = {
|
||||
message?: string;
|
||||
error?: string;
|
||||
hints?: string[];
|
||||
hintItems?: DaemonHintItem[];
|
||||
warnings?: string[];
|
||||
service?: {
|
||||
label: string;
|
||||
@@ -24,6 +39,42 @@ export function emitDaemonActionJson(payload: DaemonActionResponse) {
|
||||
defaultRuntime.writeJson(payload);
|
||||
}
|
||||
|
||||
function classifyDaemonHintText(text: string): DaemonHintKind {
|
||||
if (text.includes("openclaw gateway install") || text.startsWith("Service not installed. Run:")) {
|
||||
return "install";
|
||||
}
|
||||
if (text.startsWith("Restart the container or the service that manages it for ")) {
|
||||
return "container-restart";
|
||||
}
|
||||
if (text.startsWith("systemd user services are unavailable;")) {
|
||||
return "systemd-unavailable";
|
||||
}
|
||||
if (
|
||||
text.startsWith("On a headless server (SSH/no desktop session):") ||
|
||||
text.startsWith("Also ensure XDG_RUNTIME_DIR is set:")
|
||||
) {
|
||||
return "systemd-headless";
|
||||
}
|
||||
if (text.startsWith("If you're in a container, run the gateway in the foreground instead of")) {
|
||||
return "container-foreground";
|
||||
}
|
||||
if (
|
||||
text.startsWith("WSL2 needs systemd enabled:") ||
|
||||
text.startsWith("Then run: wsl --shutdown") ||
|
||||
text.startsWith("Verify: systemctl --user status")
|
||||
) {
|
||||
return "wsl-systemd";
|
||||
}
|
||||
return "generic";
|
||||
}
|
||||
|
||||
export function buildDaemonHintItems(hints: string[] | undefined): DaemonHintItem[] | undefined {
|
||||
if (!hints?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return hints.map((text) => ({ kind: classifyDaemonHintText(text), text }));
|
||||
}
|
||||
|
||||
export function buildDaemonServiceSnapshot(service: GatewayService, loaded: boolean) {
|
||||
return {
|
||||
label: service.label,
|
||||
@@ -56,6 +107,7 @@ export function createDaemonActionContext(params: { action: DaemonAction; json:
|
||||
emitDaemonActionJson({
|
||||
action: params.action,
|
||||
...payload,
|
||||
hintItems: payload.hintItems ?? buildDaemonHintItems(payload.hints),
|
||||
warnings: payload.warnings ?? (warnings.length ? warnings : undefined),
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user