This commit is contained in:
Victor Jiao 2026-03-15 22:49:50 +00:00 committed by GitHub
commit 792c85d0e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 28737 additions and 33322 deletions

File diff suppressed because it is too large Load Diff

View File

@ -99,6 +99,7 @@ Text + native (when enabled):
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
- `/dock-discord` (alias: `/dock_discord`) (switch replies to Discord)
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
- `/set_topic_name <name>` (Telegram topics only)
- `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only)
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)

View File

@ -1,10 +1,12 @@
import { resolveConfiguredAcpRoute } from "../../../src/acp/persistent-bindings.route.js";
import { resolveDefaultAgentId } from "../../../src/agents/agent-scope.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { logVerbose } from "../../../src/globals.js";
import { getSessionBindingService } from "../../../src/infra/outbound/session-binding-service.js";
import {
buildAgentSessionKey,
deriveLastRoutePolicy,
pickFirstExistingAgentId,
resolveAgentRoute,
} from "../../../src/routing/resolve-route.js";
import {
@ -58,7 +60,12 @@ export function resolveTelegramConversationRoute(params: {
if (rawTopicAgentId) {
// Preserve the configured topic agent ID so topic-bound sessions stay stable
// even when that agent is not present in the current config snapshot.
const topicAgentId = sanitizeAgentId(rawTopicAgentId);
const normalizedRawTopicAgentId = sanitizeAgentId(rawTopicAgentId);
const defaultAgentId = sanitizeAgentId(resolveDefaultAgentId(params.cfg));
let topicAgentId = pickFirstExistingAgentId(params.cfg, rawTopicAgentId);
if (topicAgentId === defaultAgentId && normalizedRawTopicAgentId !== defaultAgentId) {
topicAgentId = normalizedRawTopicAgentId;
}
route = {
...route,
agentId: topicAgentId,

View File

@ -382,6 +382,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
textAlias: "/unfocus",
category: "management",
}),
defineChatCommand({
key: "set_topic_name",
nativeName: "set_topic_name",
description: "Set the display label for the current Telegram topic.",
textAlias: "/set_topic_name",
category: "management",
args: [
{
name: "name",
description: "Topic label",
type: "string",
captureRemaining: true,
},
],
}),
defineChatCommand({
key: "agents",
nativeName: "agents",

View File

@ -35,6 +35,7 @@ import {
handleUsageCommand,
} from "./commands-session.js";
import { handleSubagentsCommand } from "./commands-subagents.js";
import { handleSetTopicNameCommand } from "./commands-topic-name.js";
import { handleTtsCommands } from "./commands-tts.js";
import type {
CommandHandler,
@ -184,6 +185,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
handleSessionCommand,
handleRestartCommand,
handleTtsCommands,
handleSetTopicNameCommand,
handleHelpCommand,
handleCommandsListCommand,
handleStatusCommand,

View File

@ -0,0 +1,128 @@
import { callGateway } from "../../gateway/call.js";
import { logVerbose } from "../../globals.js";
import { isTelegramSurface } from "./channel-context.js";
import type { CommandHandler } from "./commands-types.js";
const COMMAND_REGEX = /^\/set_topic_name(?:\s|$)/i;
const MAX_NAME_LEN = 64;
const MAX_LABEL_LEN = 64;
function parseTopicNameCommand(
raw: string,
): { ok: true; name: string } | { ok: false; error: string } | null {
const trimmed = raw.trim();
const match = trimmed.match(COMMAND_REGEX);
if (!match) {
return null;
}
const rest = trimmed.slice(match[0].length).trim();
const name = rest.replace(/·/g, " ").slice(0, MAX_NAME_LEN).trim();
if (!name) {
return { ok: false, error: "Usage: /set_topic_name <name>" };
}
return { ok: true, name };
}
function resolveConversationLabel(params: Parameters<CommandHandler>[0]): string {
return (
(typeof params.ctx.ConversationLabel === "string" ? params.ctx.ConversationLabel.trim() : "") ||
(typeof params.ctx.GroupSubject === "string" ? params.ctx.GroupSubject.trim() : "")
);
}
function normalizeBaseLabel(raw: string): string {
return raw.replace(/^telegram\s*·\s*/i, "").trim();
}
function buildTopicLabel(baseLabel: string, name: string): string {
const prefix = "telegram";
const separator = " · ";
let normalizedBase = baseLabel ? normalizeBaseLabel(baseLabel) : "";
const maxNameWithoutBase = Math.max(1, MAX_LABEL_LEN - (prefix + separator).length);
let nextName = name.slice(0, maxNameWithoutBase).trim();
if (normalizedBase) {
const maxBaseLen = MAX_LABEL_LEN - (prefix + separator + separator + nextName).length;
if (maxBaseLen > 0) {
normalizedBase = normalizedBase.slice(0, maxBaseLen).trim();
} else {
normalizedBase = "";
}
}
const maxNameLen = Math.max(
1,
MAX_LABEL_LEN -
(prefix + separator + (normalizedBase ? normalizedBase + separator : "")).length,
);
nextName = name.slice(0, maxNameLen).trim();
return normalizedBase
? `${prefix}${separator}${normalizedBase}${separator}${nextName}`
: `${prefix}${separator}${nextName}`;
}
export const handleSetTopicNameCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) {
return null;
}
const parsed = parseTopicNameCommand(params.command.commandBodyNormalized);
if (!parsed) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /set_topic_name from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
);
return { shouldContinue: false };
}
if (!isTelegramSurface(params)) {
return {
shouldContinue: false,
reply: { text: "⚙️ /set_topic_name only works for Telegram topics." },
};
}
if (!parsed.ok) {
return { shouldContinue: false, reply: { text: parsed.error } };
}
const threadId = params.ctx.MessageThreadId;
if (threadId == null || `${threadId}`.trim() === "") {
return {
shouldContinue: false,
reply: { text: "⚙️ /set_topic_name only works inside a Telegram topic." },
};
}
// Telegram topics are thread-scoped; sessionKey already includes the thread context.
// threadId is only used to enforce topic usage, not to disambiguate sessions.patch.
if (!params.sessionKey) {
return {
shouldContinue: false,
reply: { text: "⚙️ Could not resolve the current session key." },
};
}
const baseLabel = resolveConversationLabel(params);
const label = buildTopicLabel(baseLabel, parsed.name);
try {
await callGateway({
method: "sessions.patch",
params: {
key: params.sessionKey,
label,
},
timeoutMs: 10_000,
});
} catch (err) {
logVerbose(`/set_topic_name gateway error: ${String(err)}`);
return {
shouldContinue: false,
reply: { text: "❌ Failed to set topic name. Please try again later." },
};
}
return {
shouldContinue: false,
reply: { text: `✅ Topic label set to ${label}.` },
};
};