Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
608
extensions/line/src/bot-message-context.ts
Normal file
608
extensions/line/src/bot-message-context.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
// Line plugin module implements bot message context behavior.
|
||||
import type { webhook } from "@line/bot-sdk";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
||||
import {
|
||||
formatInboundMediaUnavailableText,
|
||||
formatInboundEnvelope,
|
||||
formatLocationText,
|
||||
resolveInboundSessionEnvelopeContext,
|
||||
toLocationContext,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
ensureConfiguredBindingRouteReady,
|
||||
resolvePinnedMainDmOwnerFromAllowlist,
|
||||
resolveConfiguredBindingRoute,
|
||||
resolveRuntimeConversationBindingRoute,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { createChannelHistoryWindow, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeAllowFrom } from "./bot-access.js";
|
||||
import { resolveLineGroupConfigEntry } from "./group-keys.js";
|
||||
import type { ResolvedLineAccount } from "./types.js";
|
||||
|
||||
type EventSource = webhook.Source | undefined;
|
||||
type MessageEvent = webhook.MessageEvent;
|
||||
type PostbackEvent = webhook.PostbackEvent;
|
||||
type StickerEventMessage = webhook.StickerMessageContent;
|
||||
|
||||
interface MediaRef {
|
||||
path: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
interface BuildLineMessageContextParams {
|
||||
event: MessageEvent;
|
||||
allMedia: MediaRef[];
|
||||
mediaUnavailable?: boolean;
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedLineAccount;
|
||||
commandAuthorized: boolean;
|
||||
groupHistories?: Map<string, HistoryEntry[]>;
|
||||
historyLimit?: number;
|
||||
}
|
||||
|
||||
type LineSourceInfo = {
|
||||
userId?: string;
|
||||
groupId?: string;
|
||||
roomId?: string;
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
export function getLineSourceInfo(source: EventSource): LineSourceInfo {
|
||||
if (!source) {
|
||||
return { userId: undefined, groupId: undefined, roomId: undefined, isGroup: false };
|
||||
}
|
||||
const userId =
|
||||
source.type === "user"
|
||||
? source.userId
|
||||
: source.type === "group"
|
||||
? source.userId
|
||||
: source.type === "room"
|
||||
? source.userId
|
||||
: undefined;
|
||||
const groupId = source.type === "group" ? source.groupId : undefined;
|
||||
const roomId = source.type === "room" ? source.roomId : undefined;
|
||||
const isGroup = source.type === "group" || source.type === "room";
|
||||
|
||||
return { userId, groupId, roomId, isGroup };
|
||||
}
|
||||
|
||||
function buildPeerId(source: EventSource): string {
|
||||
if (!source) {
|
||||
return "unknown";
|
||||
}
|
||||
const groupKey =
|
||||
normalizeOptionalString(source.type === "group" ? source.groupId : undefined) ??
|
||||
normalizeOptionalString(source.type === "room" ? source.roomId : undefined);
|
||||
if (groupKey) {
|
||||
return groupKey;
|
||||
}
|
||||
if (source.type === "user" && source.userId) {
|
||||
return source.userId;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
async function resolveLineInboundRoute(params: {
|
||||
source: EventSource;
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedLineAccount;
|
||||
}): Promise<{
|
||||
userId?: string;
|
||||
groupId?: string;
|
||||
roomId?: string;
|
||||
isGroup: boolean;
|
||||
peerId: string;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
}> {
|
||||
recordChannelActivity({
|
||||
channel: "line",
|
||||
accountId: params.account.accountId,
|
||||
direction: "inbound",
|
||||
});
|
||||
|
||||
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(params.source);
|
||||
const peerId = buildPeerId(params.source);
|
||||
let route = resolveAgentRoute({
|
||||
cfg: params.cfg,
|
||||
channel: "line",
|
||||
accountId: params.account.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "direct",
|
||||
id: peerId,
|
||||
},
|
||||
});
|
||||
|
||||
const configuredRoute = resolveConfiguredBindingRoute({
|
||||
cfg: params.cfg,
|
||||
route,
|
||||
conversation: {
|
||||
channel: "line",
|
||||
accountId: params.account.accountId,
|
||||
conversationId: peerId,
|
||||
},
|
||||
});
|
||||
let configuredBinding = configuredRoute.bindingResolution;
|
||||
const configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
|
||||
route = configuredRoute.route;
|
||||
|
||||
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
||||
route,
|
||||
conversation: {
|
||||
channel: "line",
|
||||
accountId: params.account.accountId,
|
||||
conversationId: peerId,
|
||||
},
|
||||
});
|
||||
route = runtimeRoute.route;
|
||||
if (runtimeRoute.bindingRecord) {
|
||||
configuredBinding = null;
|
||||
logVerbose(
|
||||
runtimeRoute.boundSessionKey
|
||||
? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}`
|
||||
: `line: plugin-bound conversation ${peerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (configuredBinding) {
|
||||
const ensured = await ensureConfiguredBindingRouteReady({
|
||||
cfg: params.cfg,
|
||||
bindingResolution: configuredBinding,
|
||||
});
|
||||
if (!ensured.ok) {
|
||||
logVerbose(
|
||||
`line: configured ACP binding unavailable for ${peerId} -> ${configuredBindingSessionKey}: ${ensured.error}`,
|
||||
);
|
||||
throw new Error(`Configured ACP binding unavailable: ${ensured.error}`);
|
||||
}
|
||||
logVerbose(
|
||||
`line: using configured ACP binding for ${peerId} -> ${configuredBindingSessionKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { userId, groupId, roomId, isGroup, peerId, route };
|
||||
}
|
||||
|
||||
const STICKER_PACKAGES: Record<string, string> = {
|
||||
"1": "Moon & James",
|
||||
"2": "Cony & Brown",
|
||||
"3": "Brown & Friends",
|
||||
"4": "Moon Special",
|
||||
"789": "LINE Characters",
|
||||
"6136": "Cony's Happy Life",
|
||||
"6325": "Brown's Life",
|
||||
"6359": "Choco",
|
||||
"6362": "Sally",
|
||||
"6370": "Edward",
|
||||
"11537": "Cony",
|
||||
"11538": "Brown",
|
||||
"11539": "Moon",
|
||||
};
|
||||
|
||||
function describeStickerKeywords(sticker: StickerEventMessage): string {
|
||||
const keywords = (sticker as StickerEventMessage & { keywords?: string[] }).keywords;
|
||||
if (keywords && keywords.length > 0) {
|
||||
return keywords.slice(0, 3).join(", ");
|
||||
}
|
||||
|
||||
const stickerText = (sticker as StickerEventMessage & { text?: string }).text;
|
||||
if (stickerText) {
|
||||
return stickerText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractMessageText(message: MessageEvent["message"]): string {
|
||||
if (message.type === "text") {
|
||||
return message.text;
|
||||
}
|
||||
if (message.type === "location") {
|
||||
const loc = message;
|
||||
return (
|
||||
formatLocationText({
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
name: loc.title,
|
||||
address: loc.address,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
if (message.type === "sticker") {
|
||||
const sticker = message;
|
||||
const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
|
||||
const keywords = describeStickerKeywords(sticker);
|
||||
|
||||
if (keywords) {
|
||||
return `[Sent a ${packageName} sticker: ${keywords}]`;
|
||||
}
|
||||
return `[Sent a ${packageName} sticker]`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractMediaPlaceholder(message: MessageEvent["message"]): string {
|
||||
switch (message.type) {
|
||||
case "image":
|
||||
return "<media:image>";
|
||||
case "video":
|
||||
return "<media:video>";
|
||||
case "audio":
|
||||
return "<media:audio>";
|
||||
case "file":
|
||||
return "<media:document>";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
type LineRouteInfo = ReturnType<typeof resolveAgentRoute>;
|
||||
type LineSourceInfoWithPeerId = LineSourceInfo & { peerId: string };
|
||||
|
||||
function resolveLineConversationLabel(params: {
|
||||
isGroup: boolean;
|
||||
groupId?: string;
|
||||
roomId?: string;
|
||||
senderLabel: string;
|
||||
}): string {
|
||||
return params.isGroup
|
||||
? params.groupId
|
||||
? `group:${params.groupId}`
|
||||
: params.roomId
|
||||
? `room:${params.roomId}`
|
||||
: "unknown-group"
|
||||
: params.senderLabel;
|
||||
}
|
||||
|
||||
function resolveLineAddresses(params: {
|
||||
isGroup: boolean;
|
||||
groupId?: string;
|
||||
roomId?: string;
|
||||
userId?: string;
|
||||
peerId: string;
|
||||
}): { fromAddress: string; toAddress: string; originatingTo: string } {
|
||||
const fromAddress = params.isGroup
|
||||
? params.groupId
|
||||
? `line:group:${params.groupId}`
|
||||
: params.roomId
|
||||
? `line:room:${params.roomId}`
|
||||
: `line:${params.peerId}`
|
||||
: `line:${params.userId ?? params.peerId}`;
|
||||
const toAddress = params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`;
|
||||
const originatingTo = params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`;
|
||||
return { fromAddress, toAddress, originatingTo };
|
||||
}
|
||||
|
||||
async function finalizeLineInboundContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedLineAccount;
|
||||
event: MessageEvent | PostbackEvent;
|
||||
route: LineRouteInfo;
|
||||
source: LineSourceInfoWithPeerId;
|
||||
rawBody: string;
|
||||
agentBody?: string;
|
||||
timestamp: number;
|
||||
messageSid: string;
|
||||
commandAuthorized: boolean;
|
||||
media: {
|
||||
firstPath: string | undefined;
|
||||
firstContentType?: string;
|
||||
paths?: string[];
|
||||
types?: string[];
|
||||
};
|
||||
locationContext?: ReturnType<typeof toLocationContext>;
|
||||
verboseLog: { kind: "inbound" | "postback"; mediaCount?: number };
|
||||
inboundHistory?: Pick<HistoryEntry, "sender" | "body" | "timestamp">[];
|
||||
}) {
|
||||
const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
|
||||
isGroup: params.source.isGroup,
|
||||
groupId: params.source.groupId,
|
||||
roomId: params.source.roomId,
|
||||
userId: params.source.userId,
|
||||
peerId: params.source.peerId,
|
||||
});
|
||||
|
||||
const senderId = params.source.userId ?? "unknown";
|
||||
const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
|
||||
const conversationLabel = resolveLineConversationLabel({
|
||||
isGroup: params.source.isGroup,
|
||||
groupId: params.source.groupId,
|
||||
roomId: params.source.roomId,
|
||||
senderLabel,
|
||||
});
|
||||
|
||||
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
||||
cfg: params.cfg,
|
||||
agentId: params.route.agentId,
|
||||
sessionKey: params.route.sessionKey,
|
||||
});
|
||||
|
||||
const agentBody = params.agentBody ?? params.rawBody;
|
||||
const body = formatInboundEnvelope({
|
||||
channel: "LINE",
|
||||
from: conversationLabel,
|
||||
timestamp: params.timestamp,
|
||||
body: agentBody,
|
||||
chatType: params.source.isGroup ? "group" : "direct",
|
||||
sender: {
|
||||
id: senderId,
|
||||
},
|
||||
previousTimestamp,
|
||||
envelope: envelopeOptions,
|
||||
});
|
||||
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: body,
|
||||
BodyForAgent: agentBody,
|
||||
RawBody: params.rawBody,
|
||||
CommandBody: params.rawBody,
|
||||
From: fromAddress,
|
||||
To: toAddress,
|
||||
SessionKey: params.route.sessionKey,
|
||||
AccountId: params.route.accountId,
|
||||
ChatType: params.source.isGroup ? "group" : "direct",
|
||||
ConversationLabel: conversationLabel,
|
||||
GroupSubject: params.source.isGroup
|
||||
? (params.source.groupId ?? params.source.roomId)
|
||||
: undefined,
|
||||
SenderId: senderId,
|
||||
Provider: "line",
|
||||
Surface: "line",
|
||||
MessageSid: params.messageSid,
|
||||
Timestamp: params.timestamp,
|
||||
MediaPath: params.media.firstPath,
|
||||
MediaType: params.media.firstContentType,
|
||||
MediaUrl: params.media.firstPath,
|
||||
MediaPaths: params.media.paths,
|
||||
MediaUrls: params.media.paths,
|
||||
MediaTypes: params.media.types,
|
||||
...params.locationContext,
|
||||
CommandAuthorized: params.commandAuthorized,
|
||||
OriginatingChannel: "line" as const,
|
||||
OriginatingTo: originatingTo,
|
||||
GroupSystemPrompt: params.source.isGroup
|
||||
? normalizeOptionalString(
|
||||
resolveLineGroupConfigEntry(params.account.config.groups, {
|
||||
groupId: params.source.groupId,
|
||||
roomId: params.source.roomId,
|
||||
})?.systemPrompt,
|
||||
)
|
||||
: undefined,
|
||||
InboundHistory: params.inboundHistory,
|
||||
});
|
||||
|
||||
const pinnedMainDmOwner = !params.source.isGroup
|
||||
? resolvePinnedMainDmOwnerFromAllowlist({
|
||||
dmScope: params.cfg.session?.dmScope,
|
||||
allowFrom: params.account.config.allowFrom,
|
||||
normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0],
|
||||
})
|
||||
: null;
|
||||
const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
|
||||
route: params.route,
|
||||
sessionKey: params.route.sessionKey,
|
||||
});
|
||||
if (shouldLogVerbose()) {
|
||||
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
||||
const mediaInfo =
|
||||
params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1
|
||||
? ` mediaCount=${params.verboseLog.mediaCount}`
|
||||
: "";
|
||||
const label = params.verboseLog.kind === "inbound" ? "line inbound" : "line postback";
|
||||
logVerbose(
|
||||
`${label}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
ctxPayload,
|
||||
replyToken: (params.event as { replyToken: string }).replyToken,
|
||||
turn: {
|
||||
storePath,
|
||||
record: {
|
||||
updateLastRoute: !params.source.isGroup
|
||||
? {
|
||||
sessionKey: inboundLastRouteSessionKey,
|
||||
channel: "line",
|
||||
to: params.source.userId ?? params.source.peerId,
|
||||
accountId: params.route.accountId,
|
||||
mainDmOwnerPin:
|
||||
inboundLastRouteSessionKey === params.route.mainSessionKey &&
|
||||
pinnedMainDmOwner &&
|
||||
params.source.userId
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: params.source.userId,
|
||||
onSkip: ({
|
||||
ownerRecipient,
|
||||
senderRecipient,
|
||||
}: {
|
||||
ownerRecipient: string;
|
||||
senderRecipient: string;
|
||||
}) => {
|
||||
logVerbose(
|
||||
`line: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err: unknown) => {
|
||||
logVerbose(`line: failed updating session meta: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildLineMessageContext(params: BuildLineMessageContextParams) {
|
||||
const {
|
||||
event,
|
||||
allMedia,
|
||||
mediaUnavailable,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized,
|
||||
groupHistories,
|
||||
historyLimit,
|
||||
} = params;
|
||||
|
||||
const source = event.source;
|
||||
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
||||
source,
|
||||
cfg,
|
||||
account,
|
||||
});
|
||||
|
||||
const message = event.message;
|
||||
const messageId = message.id;
|
||||
const timestamp = event.timestamp;
|
||||
|
||||
const textContent = extractMessageText(message);
|
||||
const placeholder = extractMediaPlaceholder(message);
|
||||
|
||||
let rawBody = textContent || placeholder;
|
||||
if (!rawBody && allMedia.length > 0) {
|
||||
rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
|
||||
}
|
||||
const agentBody = mediaUnavailable
|
||||
? formatInboundMediaUnavailableText({
|
||||
body: rawBody,
|
||||
mediaPlaceholder: placeholder,
|
||||
notice: "[line attachment unavailable]",
|
||||
})
|
||||
: rawBody;
|
||||
|
||||
if (!agentBody && allMedia.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let locationContext: ReturnType<typeof toLocationContext> | undefined;
|
||||
if (message.type === "location") {
|
||||
const loc = message;
|
||||
locationContext = toLocationContext({
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
name: loc.title,
|
||||
address: loc.address,
|
||||
});
|
||||
}
|
||||
|
||||
const historyKey = isGroup ? peerId : undefined;
|
||||
const inboundHistory =
|
||||
historyKey && groupHistories && (historyLimit ?? 0) > 0
|
||||
? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
|
||||
historyKey,
|
||||
limit: historyLimit ?? 0,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const finalized = await finalizeLineInboundContext({
|
||||
cfg,
|
||||
account,
|
||||
event,
|
||||
route,
|
||||
source: { userId, groupId, roomId, isGroup, peerId },
|
||||
rawBody,
|
||||
agentBody,
|
||||
timestamp,
|
||||
messageSid: messageId,
|
||||
commandAuthorized,
|
||||
media: {
|
||||
firstPath: allMedia[0]?.path,
|
||||
firstContentType: allMedia[0]?.contentType,
|
||||
paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : undefined,
|
||||
types:
|
||||
allMedia.length > 0
|
||||
? (allMedia.map((m) => m.contentType).filter(Boolean) as string[])
|
||||
: undefined,
|
||||
},
|
||||
locationContext,
|
||||
verboseLog: { kind: "inbound", mediaCount: allMedia.length },
|
||||
inboundHistory,
|
||||
});
|
||||
|
||||
return {
|
||||
ctxPayload: finalized.ctxPayload,
|
||||
turn: finalized.turn,
|
||||
event,
|
||||
userId,
|
||||
groupId,
|
||||
roomId,
|
||||
isGroup,
|
||||
route,
|
||||
replyToken: event.replyToken,
|
||||
accountId: account.accountId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildLinePostbackContext(params: {
|
||||
event: PostbackEvent;
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedLineAccount;
|
||||
commandAuthorized: boolean;
|
||||
}) {
|
||||
const { event, cfg, account, commandAuthorized } = params;
|
||||
|
||||
const source = event.source;
|
||||
const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
|
||||
source,
|
||||
cfg,
|
||||
account,
|
||||
});
|
||||
|
||||
const timestamp = event.timestamp;
|
||||
const rawData = event.postback?.data?.trim() ?? "";
|
||||
if (!rawData) {
|
||||
return null;
|
||||
}
|
||||
let rawBody = rawData;
|
||||
if (rawData.includes("line.action=")) {
|
||||
const searchParams = new URLSearchParams(rawData);
|
||||
const action = searchParams.get("line.action") ?? "";
|
||||
const device = searchParams.get("line.device");
|
||||
rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
|
||||
}
|
||||
|
||||
const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
|
||||
const finalized = await finalizeLineInboundContext({
|
||||
cfg,
|
||||
account,
|
||||
event,
|
||||
route,
|
||||
source: { userId, groupId, roomId, isGroup, peerId },
|
||||
rawBody,
|
||||
timestamp,
|
||||
messageSid,
|
||||
commandAuthorized,
|
||||
media: {
|
||||
firstPath: "",
|
||||
firstContentType: undefined,
|
||||
paths: undefined,
|
||||
types: undefined,
|
||||
},
|
||||
verboseLog: { kind: "postback" },
|
||||
});
|
||||
|
||||
return {
|
||||
ctxPayload: finalized.ctxPayload,
|
||||
turn: finalized.turn,
|
||||
event,
|
||||
userId,
|
||||
groupId,
|
||||
roomId,
|
||||
isGroup,
|
||||
route,
|
||||
replyToken: event.replyToken,
|
||||
accountId: account.accountId,
|
||||
};
|
||||
}
|
||||
|
||||
type LineMessageContext = NonNullable<Awaited<ReturnType<typeof buildLineMessageContext>>>;
|
||||
type LinePostbackContext = NonNullable<Awaited<ReturnType<typeof buildLinePostbackContext>>>;
|
||||
export type LineInboundContext = LineMessageContext | LinePostbackContext;
|
||||
Reference in New Issue
Block a user