mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix(slack): print setup manifest as plain JSON
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/setup: print the generated app manifest as plain JSON instead of embedding it inside the framed setup note, so it can be copied into Slack without deleting border characters. Fixes #65751. Thanks @theDanielJLewis.
|
||||
- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash.
|
||||
- Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme.
|
||||
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
buildSlackManifest,
|
||||
buildSlackSetupLines,
|
||||
isSlackSetupAccountConfigured,
|
||||
SLACK_CHANNEL as channel,
|
||||
@@ -177,6 +178,17 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
shouldShow: ({ cfg, accountId }) =>
|
||||
!isSlackSetupAccountConfigured(resolveSlackAccount({ cfg, accountId })),
|
||||
},
|
||||
prepare: async ({ cfg, accountId, prompter }) => {
|
||||
if (isSlackSetupAccountConfigured(resolveSlackAccount({ cfg, accountId }))) {
|
||||
return;
|
||||
}
|
||||
const manifest = buildSlackManifest();
|
||||
if (prompter.plain) {
|
||||
await prompter.plain(manifest);
|
||||
} else {
|
||||
await prompter.note(manifest, "Slack manifest JSON");
|
||||
}
|
||||
},
|
||||
envShortcut: {
|
||||
prompt: "SLACK_BOT_TOKEN + SLACK_APP_TOKEN detected. Use env vars?",
|
||||
preferredEnvVar: "SLACK_BOT_TOKEN",
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { OpenClawConfig } from "./channel-api.js";
|
||||
|
||||
export const SLACK_CHANNEL = "slack" as const;
|
||||
|
||||
function buildSlackManifest(botName: string) {
|
||||
export function buildSlackManifest(botName = "OpenClaw") {
|
||||
const safeName = botName.trim() || "OpenClaw";
|
||||
const manifest = {
|
||||
display_information: {
|
||||
@@ -84,18 +84,16 @@ function buildSlackManifest(botName: string) {
|
||||
return JSON.stringify(manifest, null, 2);
|
||||
}
|
||||
|
||||
export function buildSlackSetupLines(botName = "OpenClaw"): string[] {
|
||||
export function buildSlackSetupLines(): string[] {
|
||||
return [
|
||||
"1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)",
|
||||
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
|
||||
"3) Install App to workspace to get the xoxb- bot token",
|
||||
"4) Enable Event Subscriptions (socket) for message and App Home events",
|
||||
"5) App Home -> enable the Home tab and Messages tab for DMs",
|
||||
"Manifest JSON follows as plain text for copy/paste.",
|
||||
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
|
||||
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||
"",
|
||||
"Manifest (JSON):",
|
||||
buildSlackManifest(botName),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import {
|
||||
createTestWizardPrompter,
|
||||
runSetupWizardPrepare,
|
||||
runSetupWizardFinalize,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createSlackSetupWizardBase } from "./setup-core.js";
|
||||
import { buildSlackSetupLines } from "./setup-shared.js";
|
||||
|
||||
const slackSetupWizard = createSlackSetupWizardBase({
|
||||
promptAllowFrom: async ({ cfg }) => cfg,
|
||||
@@ -18,16 +20,16 @@ const slackSetupWizard = createSlackSetupWizardBase({
|
||||
resolveGroupAllowlist: async ({ entries }) => entries,
|
||||
});
|
||||
|
||||
describe("slackSetupWizard.finalize", () => {
|
||||
const baseCfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
},
|
||||
const baseCfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
describe("slackSetupWizard.finalize", () => {
|
||||
it("prompts to enable interactive replies for newly configured Slack accounts", async () => {
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
@@ -75,6 +77,53 @@ describe("slackSetupWizard.finalize", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("slackSetupWizard.prepare", () => {
|
||||
it("keeps the manifest out of framed intro note lines", () => {
|
||||
const lines = buildSlackSetupLines();
|
||||
|
||||
expect(lines.join("\n")).not.toContain("Manifest (JSON):");
|
||||
expect(lines.join("\n")).not.toContain('"display_information"');
|
||||
expect(lines).toContain("Manifest JSON follows as plain text for copy/paste.");
|
||||
});
|
||||
|
||||
it("prints the manifest as plain JSON when Slack is not configured", async () => {
|
||||
const plain = vi.fn<NonNullable<WizardPrompter["plain"]>>(async () => {});
|
||||
const note = vi.fn(async () => {});
|
||||
|
||||
await runSetupWizardPrepare({
|
||||
prepare: slackSetupWizard.prepare,
|
||||
cfg: { channels: { slack: {} } } as OpenClawConfig,
|
||||
prompter: createTestWizardPrompter({
|
||||
plain,
|
||||
note: note as WizardPrompter["note"],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(plain).toHaveBeenCalledTimes(1);
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
const manifest = plain.mock.calls[0]?.[0];
|
||||
expect(typeof manifest).toBe("string");
|
||||
expect(JSON.parse(manifest)).toMatchObject({
|
||||
display_information: { name: "OpenClaw" },
|
||||
settings: { socket_mode_enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not print the manifest after Slack credentials are configured", async () => {
|
||||
const plain = vi.fn<NonNullable<WizardPrompter["plain"]>>(async () => {});
|
||||
|
||||
await runSetupWizardPrepare({
|
||||
prepare: slackSetupWizard.prepare,
|
||||
cfg: baseCfg,
|
||||
prompter: createTestWizardPrompter({
|
||||
plain,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(plain).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("slackSetupWizard.dmPolicy", () => {
|
||||
it("reads the named-account DM policy instead of the channel root", () => {
|
||||
expect(
|
||||
|
||||
@@ -66,6 +66,7 @@ export const WizardStepSchema = Type.Object(
|
||||
]),
|
||||
title: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
format: Type.Optional(Type.Union([Type.Literal("plain")])),
|
||||
options: Type.Optional(Type.Array(WizardStepOptionSchema)),
|
||||
initialValue: Type.Optional(Type.Unknown()),
|
||||
placeholder: Type.Optional(Type.String()),
|
||||
|
||||
@@ -11,6 +11,7 @@ type QueuedWizardPrompter = {
|
||||
intro: AsyncUnknownMock;
|
||||
outro: AsyncUnknownMock;
|
||||
note: AsyncUnknownMock;
|
||||
plain: AsyncUnknownMock;
|
||||
select: AsyncUnknownMock;
|
||||
multiselect: AsyncUnknownMock;
|
||||
text: AsyncUnknownMock;
|
||||
@@ -34,6 +35,7 @@ export function createTestWizardPrompter(overrides: Partial<WizardPrompter> = {}
|
||||
intro: vi.fn(async () => {}),
|
||||
outro: vi.fn(async () => {}),
|
||||
note: vi.fn(async () => {}),
|
||||
plain: vi.fn(async () => {}),
|
||||
select: selectFirstWizardOption as WizardPrompter["select"],
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => "") as WizardPrompter["text"],
|
||||
@@ -55,6 +57,7 @@ export function createQueuedWizardPrompter(params?: {
|
||||
const intro = vi.fn(async () => undefined);
|
||||
const outro = vi.fn(async () => undefined);
|
||||
const note = vi.fn(async () => undefined);
|
||||
const plain = vi.fn(async () => undefined);
|
||||
const select = vi.fn(async () => selectValues.shift() ?? "");
|
||||
const multiselect = vi.fn(async () => [] as string[]);
|
||||
const text = vi.fn(async () => textValues.shift() ?? "");
|
||||
@@ -68,6 +71,7 @@ export function createQueuedWizardPrompter(params?: {
|
||||
intro,
|
||||
outro,
|
||||
note,
|
||||
plain,
|
||||
select,
|
||||
multiselect,
|
||||
text,
|
||||
@@ -77,6 +81,7 @@ export function createQueuedWizardPrompter(params?: {
|
||||
intro,
|
||||
outro,
|
||||
note,
|
||||
plain,
|
||||
select: select as WizardPrompter["select"],
|
||||
multiselect: multiselect as WizardPrompter["multiselect"],
|
||||
text: text as WizardPrompter["text"],
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tokenizedOptionFilter } from "./clack-prompter.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createClackPrompter, tokenizedOptionFilter } from "./clack-prompter.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("tokenizedOptionFilter", () => {
|
||||
it("matches tokens regardless of order", () => {
|
||||
@@ -33,3 +37,14 @@ describe("tokenizedOptionFilter", () => {
|
||||
expect(tokenizedOptionFilter("openai gpt-5.4", option)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createClackPrompter", () => {
|
||||
it("prints plain output without note framing", async () => {
|
||||
const write = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
||||
const prompter = createClackPrompter();
|
||||
|
||||
await prompter.plain?.('{"ok":true}');
|
||||
|
||||
expect(write).toHaveBeenCalledWith('{"ok":true}\n');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,9 @@ export function createClackPrompter(): WizardPrompter {
|
||||
note: async (message, title) => {
|
||||
emitNote(message, title);
|
||||
},
|
||||
plain: async (message) => {
|
||||
process.stdout.write(message.endsWith("\n") ? message : `${message}\n`);
|
||||
},
|
||||
select: async (params) => {
|
||||
const options = params.options.map((opt) => {
|
||||
const base = { value: opt.value, label: opt.label };
|
||||
|
||||
@@ -39,6 +39,7 @@ export type WizardPrompter = {
|
||||
intro: (title: string) => Promise<void>;
|
||||
outro: (message: string) => Promise<void>;
|
||||
note: (message: string, title?: string) => Promise<void>;
|
||||
plain?: (message: string) => Promise<void>;
|
||||
select: <T>(params: WizardSelectParams<T>) => Promise<T>;
|
||||
multiselect: <T>(params: WizardMultiSelectParams<T>) => Promise<T[]>;
|
||||
text: (params: WizardTextParams) => Promise<string>;
|
||||
|
||||
@@ -47,6 +47,25 @@ describe("WizardSession", () => {
|
||||
expect(done.status).toBe("done");
|
||||
});
|
||||
|
||||
test("plain output is a client note with plain format", async () => {
|
||||
const session = new WizardSession(async (prompter) => {
|
||||
await prompter.plain?.('{"ok":true}');
|
||||
});
|
||||
|
||||
const first = await session.next();
|
||||
expect(first.step).toMatchObject({
|
||||
type: "note",
|
||||
message: '{"ok":true}',
|
||||
format: "plain",
|
||||
});
|
||||
if (!first.step) {
|
||||
throw new Error("expected plain note");
|
||||
}
|
||||
await session.answer(first.step.id, null);
|
||||
const done = await session.next();
|
||||
expect(done.done).toBe(true);
|
||||
});
|
||||
|
||||
test("invalid answers throw", async () => {
|
||||
const session = noteRunner();
|
||||
const first = await session.next();
|
||||
|
||||
@@ -12,6 +12,7 @@ export type WizardStep = {
|
||||
type: "note" | "select" | "text" | "confirm" | "multiselect" | "progress" | "action";
|
||||
title?: string;
|
||||
message?: string;
|
||||
format?: "plain";
|
||||
options?: WizardStepOption[];
|
||||
initialValue?: unknown;
|
||||
placeholder?: string;
|
||||
@@ -69,6 +70,10 @@ class WizardSessionPrompter implements WizardPrompter {
|
||||
await this.prompt({ type: "note", title, message, executor: "client" });
|
||||
}
|
||||
|
||||
async plain(message: string): Promise<void> {
|
||||
await this.prompt({ type: "note", message, format: "plain", executor: "client" });
|
||||
}
|
||||
|
||||
async select<T>(params: {
|
||||
message: string;
|
||||
options: Array<{ value: T; label: string; hint?: string }>;
|
||||
|
||||
Reference in New Issue
Block a user