mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-13 12:26:05 +00:00
docs: absorb documentation PR sweep
This commit is contained in:
@@ -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) ||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
24
extensions/signal/src/daemon.test.ts
Normal file
24
extensions/signal/src/daemon.test.ts
Normal 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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user