docs: absorb documentation PR sweep

This commit is contained in:
Peter Steinberger
2026-05-23 10:23:22 +01:00
parent 6b04170167
commit 2c536a8626
39 changed files with 455 additions and 71 deletions

View File

@@ -50,6 +50,7 @@ export function resolveSignalAccount(params: {
const baseUrl = normalizeOptionalString(merged.httpUrl) ?? `http://${host}:${port}`;
const configured = Boolean(
normalizeOptionalString(merged.account) ||
normalizeOptionalString(merged.configPath) ||
normalizeOptionalString(merged.httpUrl) ||
normalizeOptionalString(merged.cliPath) ||
normalizeOptionalString(merged.httpHost) ||

View File

@@ -17,4 +17,8 @@ export const signalChannelConfigUiHints = {
label: "Signal Account",
help: "Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state.",
},
configPath: {
label: "Signal CLI Config Path",
help: "Optional directory passed to signal-cli via --config when the service needs a non-default signal-cli data path.",
},
} satisfies Record<string, ChannelConfigUiHint>;

View File

@@ -0,0 +1,24 @@
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { __testing } from "./daemon.js";
describe("signal daemon args", () => {
it("expands home-relative configPath before passing it to signal-cli", () => {
expect(
__testing.buildDaemonArgs({
cliPath: "signal-cli",
configPath: "~/.openclaw/signal-cli",
httpHost: "127.0.0.1",
httpPort: 8080,
}),
).toEqual([
"--config",
path.join(os.homedir(), ".openclaw/signal-cli"),
"daemon",
"--http",
"127.0.0.1:8080",
"--no-receive-stdout",
]);
});
});

View File

@@ -1,8 +1,11 @@
import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
type SignalDaemonOpts = {
cliPath: string;
configPath?: string;
account?: string;
httpHost: string;
httpPort: number;
@@ -63,8 +66,22 @@ function bindSignalCliOutput(params: {
});
}
function resolveSignalCliConfigPath(raw: string): string {
const value = raw.trim();
if (value === "~") {
return os.homedir();
}
if (value.startsWith("~/") || value.startsWith("~\\")) {
return path.join(os.homedir(), value.slice(2));
}
return value;
}
function buildDaemonArgs(opts: SignalDaemonOpts): string[] {
const args: string[] = [];
if (opts.configPath?.trim()) {
args.push("--config", resolveSignalCliConfigPath(opts.configPath));
}
if (opts.account) {
args.push("-a", opts.account);
}
@@ -145,3 +162,8 @@ export function spawnSignalDaemon(opts: SignalDaemonOpts): SignalDaemonHandle {
},
};
}
export const __testing = {
buildDaemonArgs,
resolveSignalCliConfigPath,
} as const;

View File

@@ -114,6 +114,42 @@ describe("monitorSignalProvider autostart", () => {
expectWaitForTransportReadyTimeout(90_000);
});
it("passes channels.signal.configPath to signal-cli daemon startup", async () => {
const runtime = createMonitorRuntime();
setSignalAutoStartConfig({ configPath: "~/.openclaw/signal-cli" });
const abortController = createAutoAbortController();
await runMonitorWithMocks({
autoStart: true,
baseUrl: SIGNAL_BASE_URL,
abortSignal: abortController.signal,
runtime,
});
expect(spawnSignalDaemonMock).toHaveBeenCalledWith(
expect.objectContaining({
configPath: "~/.openclaw/signal-cli",
}),
);
});
it("omits configPath when channels.signal.configPath is blank", async () => {
const runtime = createMonitorRuntime();
setSignalAutoStartConfig({ configPath: " " });
const abortController = createAutoAbortController();
await runMonitorWithMocks({
autoStart: true,
baseUrl: SIGNAL_BASE_URL,
abortSignal: abortController.signal,
runtime,
});
const [daemonOpts] = spawnSignalDaemonMock.mock.calls[0] ?? [];
expect(daemonOpts).toBeDefined();
expect(daemonOpts).not.toHaveProperty("configPath");
});
it("caps startupTimeoutMs at 2 minutes", async () => {
const runtime = createMonitorRuntime();
setSignalAutoStartConfig({ startupTimeoutMs: 180_000 });

View File

@@ -56,6 +56,7 @@ export type MonitorSignalOpts = {
autoStart?: boolean;
startupTimeoutMs?: number;
cliPath?: string;
configPath?: string;
httpHost?: string;
httpPort?: number;
receiveMode?: "on-start" | "manual";
@@ -460,10 +461,14 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
if (autoStart) {
const cliPath = opts.cliPath ?? accountInfo.config.cliPath ?? "signal-cli";
const configPath =
normalizeOptionalString(opts.configPath) ??
normalizeOptionalString(accountInfo.config.configPath);
const httpHost = opts.httpHost ?? accountInfo.config.httpHost ?? "127.0.0.1";
const httpPort = opts.httpPort ?? accountInfo.config.httpPort ?? 8080;
daemonHandle = spawnSignalDaemon({
cliPath,
...(configPath ? { configPath } : {}),
account,
httpHost,
httpPort,

View File

@@ -32,7 +32,7 @@ export const signalConfigAdapter = createScopedChannelConfigAdapter<ResolvedSign
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: adaptScopedAccountAccessor((params) => resolveSignalAccount(params)),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
clearBaseFields: ["account", "configPath", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom

View File

@@ -55,6 +55,19 @@ export function resolveTelegramScopedGroupConfig(
chatId: string | number,
messageThreadId?: number,
) {
const resolveTopicConfig = <T extends object>(
scopedConfig: { topics?: Record<string, T | undefined> } | undefined,
): T | undefined => {
if (!scopedConfig || messageThreadId == null) {
return undefined;
}
const defaultConfig = scopedConfig.topics?.["*"];
const exactConfig = scopedConfig.topics?.[String(messageThreadId)];
if (defaultConfig && exactConfig) {
return { ...defaultConfig, ...exactConfig };
}
return exactConfig ?? defaultConfig;
};
const groups = telegramCfg.groups;
const direct = telegramCfg.direct;
const chatIdStr = String(chatId);
@@ -62,18 +75,12 @@ export function resolveTelegramScopedGroupConfig(
if (isDm) {
const groupConfig = direct?.[chatIdStr] ?? direct?.["*"];
const topicConfig =
groupConfig && messageThreadId != null
? groupConfig.topics?.[String(messageThreadId)]
: undefined;
const topicConfig = resolveTopicConfig(groupConfig);
return { groupConfig, topicConfig };
}
const groupConfig = groups?.[chatIdStr] ?? groups?.["*"];
const topicConfig =
groupConfig && messageThreadId != null
? groupConfig.topics?.[String(messageThreadId)]
: undefined;
const topicConfig = resolveTopicConfig(groupConfig);
return { groupConfig, topicConfig };
}

View File

@@ -2622,6 +2622,72 @@ describe("createTelegramBot", () => {
expect(topicConfig).toEqual({});
});
it("uses topics.* as the default config for unmatched forum topics", () => {
const { groupConfig, topicConfig } = resolveTelegramScopedGroupConfig(
{
groupPolicy: "allowlist",
groups: {
"-1001234567890": {
allowFrom: ["999999999"],
topics: {
"*": { allowFrom: ["123456789"], agentId: "zu" },
},
},
},
},
-1001234567890,
77,
);
const group = groupConfig as TelegramGroupConfig | undefined;
expect(group?.allowFrom).toEqual(["999999999"]);
expect(topicConfig).toEqual({ allowFrom: ["123456789"], agentId: "zu" });
});
it("prefers exact topic config over topics.* fallback", () => {
const { topicConfig } = resolveTelegramScopedGroupConfig(
{
groupPolicy: "allowlist",
groups: {
"-1001234567890": {
topics: {
"*": { allowFrom: ["123456789"], agentId: "zu" },
"77": { allowFrom: ["555555555"], agentId: "main" },
},
},
},
},
-1001234567890,
77,
);
expect(topicConfig).toEqual({ allowFrom: ["555555555"], agentId: "main" });
});
it("inherits topics.* fields that exact topic config does not override", () => {
const { topicConfig } = resolveTelegramScopedGroupConfig(
{
groupPolicy: "allowlist",
groups: {
"-1001234567890": {
topics: {
"*": { allowFrom: ["123456789"], requireMention: false },
"77": { agentId: "main" },
},
},
},
},
-1001234567890,
77,
);
expect(topicConfig).toEqual({
allowFrom: ["123456789"],
requireMention: false,
agentId: "main",
});
});
it.each([
{
label: "parent binding",

View File

@@ -33,6 +33,36 @@ describe("resolveTelegramGroupRequireMention", () => {
}),
).toBe(false);
});
it("lets exact topic configs inherit wildcard topic requireMention", () => {
const cfg = {
channels: {
telegram: {
botToken: "telegram-test",
groups: {
"-1001": {
requireMention: true,
topics: {
"*": {
requireMention: false,
},
"77": {
agentId: "main",
},
},
},
},
},
},
} as OpenClawConfig;
expect(
resolveTelegramGroupRequireMention({
cfg,
groupId: "-1001:topic:77",
}),
).toBe(false);
});
});
describe("resolveTelegramGroupToolPolicy", () => {

View File

@@ -40,9 +40,14 @@ function resolveTelegramRequireMention(params: {
cfg.channels?.telegram?.groups;
const groupConfig = scopedGroups?.[chatId];
const groupDefault = scopedGroups?.["*"];
const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const topicConfig =
topicId && groupConfig?.topics
? { ...groupConfig.topics["*"], ...groupConfig.topics[topicId] }
: undefined;
const defaultTopicConfig =
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
topicId && groupDefault?.topics
? { ...groupDefault.topics["*"], ...groupDefault.topics[topicId] }
: undefined;
if (typeof topicConfig?.requireMention === "boolean") {
return topicConfig.requireMention;
}