mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-01 04:11:03 +00:00
refactor: finish decoupling plugin sdk seams
This commit is contained in:
@@ -1004,8 +1004,10 @@ Compatibility note:
|
||||
helper is only needed by a bundled extension, keep it behind the extension's
|
||||
local `api.js` or `runtime-api.js` seam instead of promoting it into
|
||||
`openclaw/plugin-sdk/<extension>`.
|
||||
- Channel-branded bundled bars stay private unless they are explicitly added
|
||||
back to the public contract.
|
||||
- New shared helper seams should be generic, not channel-branded. Shared target
|
||||
parsing belongs on `openclaw/plugin-sdk/channel-targets`; channel-specific
|
||||
internals stay behind the owning plugin's local `api.js` or `runtime-api.js`
|
||||
seam.
|
||||
- Capability-specific subpaths such as `image-generation`,
|
||||
`media-understanding`, and `speech` exist because bundled/native plugins use
|
||||
them today. Their presence does not by itself mean every exported helper is a
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import {
|
||||
__testing as sessionBindingTesting,
|
||||
@@ -17,7 +16,6 @@ const baseCfg = {
|
||||
describe("resolveBlueBubblesConversationRoute", () => {
|
||||
beforeEach(() => {
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
});
|
||||
|
||||
it("lets runtime BlueBubbles conversation bindings override default routing", () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type ParsedChatTarget,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
} from "openclaw/plugin-sdk/imessage-core";
|
||||
} from "openclaw/plugin-sdk/channel-targets";
|
||||
|
||||
export type BlueBubblesService = "imessage" | "sms" | "auto";
|
||||
|
||||
|
||||
50
extensions/imessage/src/conversation-id-core.ts
Normal file
50
extensions/imessage/src/conversation-id-core.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";
|
||||
|
||||
export function normalizeIMessageAcpConversationId(
|
||||
conversationId: string,
|
||||
): { conversationId: string } | null {
|
||||
const trimmed = conversationId.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parseIMessageTarget(trimmed);
|
||||
if (parsed.kind === "handle") {
|
||||
const handle = normalizeIMessageHandle(parsed.to);
|
||||
return handle ? { conversationId: handle } : null;
|
||||
}
|
||||
if (parsed.kind === "chat_id") {
|
||||
return { conversationId: String(parsed.chatId) };
|
||||
}
|
||||
if (parsed.kind === "chat_guid") {
|
||||
return { conversationId: parsed.chatGuid };
|
||||
}
|
||||
return { conversationId: parsed.chatIdentifier };
|
||||
} catch {
|
||||
const handle = normalizeIMessageHandle(trimmed);
|
||||
return handle ? { conversationId: handle } : null;
|
||||
}
|
||||
}
|
||||
|
||||
export function matchIMessageAcpConversation(params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
}): { conversationId: string; matchPriority: number } | null {
|
||||
const binding = normalizeIMessageAcpConversationId(params.bindingConversationId);
|
||||
const conversation = normalizeIMessageAcpConversationId(params.conversationId);
|
||||
if (!binding || !conversation) {
|
||||
return null;
|
||||
}
|
||||
if (binding.conversationId !== conversation.conversationId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: conversation.conversationId,
|
||||
matchPriority: 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
|
||||
return normalizeIMessageAcpConversationId(target)?.conversationId;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
matchIMessageAcpConversation,
|
||||
normalizeIMessageAcpConversationId,
|
||||
resolveIMessageConversationIdFromTarget,
|
||||
} from "openclaw/plugin-sdk/imessage-core";
|
||||
} from "./conversation-id-core.js";
|
||||
import { normalizeIMessageHandle } from "./targets.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import {
|
||||
__testing as sessionBindingTesting,
|
||||
@@ -17,7 +16,6 @@ const baseCfg = {
|
||||
describe("resolveIMessageConversationRoute", () => {
|
||||
beforeEach(() => {
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
});
|
||||
|
||||
it("lets runtime iMessage conversation bindings override default routing", () => {
|
||||
|
||||
@@ -1,223 +1,14 @@
|
||||
import { isAllowedParsedChatSender } from "openclaw/plugin-sdk/allow-from";
|
||||
|
||||
export type ServicePrefix<TService extends string> = { prefix: string; service: TService };
|
||||
|
||||
export type ChatTargetPrefixesParams = {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
};
|
||||
|
||||
export type ParsedChatTarget =
|
||||
| { kind: "chat_id"; chatId: number }
|
||||
| { kind: "chat_guid"; chatGuid: string }
|
||||
| { kind: "chat_identifier"; chatIdentifier: string };
|
||||
|
||||
export type ParsedChatAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string };
|
||||
|
||||
export type ChatSenderAllowParams = {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
chatId?: number | null;
|
||||
chatGuid?: string | null;
|
||||
chatIdentifier?: string | null;
|
||||
};
|
||||
|
||||
function stripPrefix(value: string, prefix: string): string {
|
||||
return value.slice(prefix.length).trim();
|
||||
}
|
||||
|
||||
function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean {
|
||||
return prefixes.some((prefix) => value.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedTarget<TService extends string, TTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<ServicePrefix<TService>>;
|
||||
isChatTarget: (remainderLower: string) => boolean;
|
||||
parseTarget: (remainder: string) => TTarget;
|
||||
}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null {
|
||||
for (const { prefix, service } of params.servicePrefixes) {
|
||||
if (!params.lower.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const remainder = stripPrefix(params.trimmed, prefix);
|
||||
if (!remainder) {
|
||||
throw new Error(`${prefix} target is required`);
|
||||
}
|
||||
const remainderLower = remainder.toLowerCase();
|
||||
if (params.isChatTarget(remainderLower)) {
|
||||
return params.parseTarget(remainder);
|
||||
}
|
||||
return { kind: "handle", to: remainder, service };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedChatTarget<TService extends string, TTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<ServicePrefix<TService>>;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
extraChatPrefixes?: string[];
|
||||
parseTarget: (remainder: string) => TTarget;
|
||||
}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null {
|
||||
const chatPrefixes = [
|
||||
...params.chatIdPrefixes,
|
||||
...params.chatGuidPrefixes,
|
||||
...params.chatIdentifierPrefixes,
|
||||
...(params.extraChatPrefixes ?? []),
|
||||
];
|
||||
return resolveServicePrefixedTarget({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
servicePrefixes: params.servicePrefixes,
|
||||
isChatTarget: (remainderLower) => startsWithAnyPrefix(remainderLower, chatPrefixes),
|
||||
parseTarget: params.parseTarget,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseChatTargetPrefixesOrThrow(
|
||||
params: ChatTargetPrefixesParams,
|
||||
): ParsedChatTarget | null {
|
||||
for (const prefix of params.chatIdPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(chatId)) {
|
||||
throw new Error(`Invalid chat_id: ${value}`);
|
||||
}
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatGuidPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (!value) {
|
||||
throw new Error("chat_guid is required");
|
||||
}
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatIdentifierPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (!value) {
|
||||
throw new Error("chat_identifier is required");
|
||||
}
|
||||
return { kind: "chat_identifier", chatIdentifier: value };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedAllowTarget<TAllowTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<{ prefix: string }>;
|
||||
parseAllowTarget: (remainder: string) => TAllowTarget;
|
||||
}): (TAllowTarget | { kind: "handle"; handle: string }) | null {
|
||||
for (const { prefix } of params.servicePrefixes) {
|
||||
if (!params.lower.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const remainder = stripPrefix(params.trimmed, prefix);
|
||||
if (!remainder) {
|
||||
return { kind: "handle", handle: "" };
|
||||
}
|
||||
return params.parseAllowTarget(remainder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedOrChatAllowTarget<
|
||||
TAllowTarget extends ParsedChatAllowTarget,
|
||||
>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<{ prefix: string }>;
|
||||
parseAllowTarget: (remainder: string) => TAllowTarget;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
}): TAllowTarget | null {
|
||||
const servicePrefixed = resolveServicePrefixedAllowTarget({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
servicePrefixes: params.servicePrefixes,
|
||||
parseAllowTarget: params.parseAllowTarget,
|
||||
});
|
||||
if (servicePrefixed) {
|
||||
return servicePrefixed as TAllowTarget;
|
||||
}
|
||||
|
||||
const chatTarget = parseChatAllowTargetPrefixes({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
chatIdPrefixes: params.chatIdPrefixes,
|
||||
chatGuidPrefixes: params.chatGuidPrefixes,
|
||||
chatIdentifierPrefixes: params.chatIdentifierPrefixes,
|
||||
});
|
||||
if (chatTarget) {
|
||||
return chatTarget as TAllowTarget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createAllowedChatSenderMatcher<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
normalizeSender: (sender: string) => string;
|
||||
parseAllowTarget: (entry: string) => TParsed;
|
||||
}): (input: ChatSenderAllowParams) => boolean {
|
||||
return (input) =>
|
||||
isAllowedParsedChatSender({
|
||||
allowFrom: input.allowFrom,
|
||||
sender: input.sender,
|
||||
chatId: input.chatId,
|
||||
chatGuid: input.chatGuid,
|
||||
chatIdentifier: input.chatIdentifier,
|
||||
normalizeSender: params.normalizeSender,
|
||||
parseAllowTarget: params.parseAllowTarget,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseChatAllowTargetPrefixes(
|
||||
params: ChatTargetPrefixesParams,
|
||||
): ParsedChatTarget | null {
|
||||
for (const prefix of params.chatIdPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(chatId)) {
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatGuidPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (value) {
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatIdentifierPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (value) {
|
||||
return { kind: "chat_identifier", chatIdentifier: value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
export {
|
||||
createAllowedChatSenderMatcher,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedChatTarget,
|
||||
resolveServicePrefixedOrChatAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ChatSenderAllowParams,
|
||||
type ChatTargetPrefixesParams,
|
||||
type ParsedChatAllowTarget,
|
||||
type ParsedChatTarget,
|
||||
type ServicePrefix,
|
||||
} from "openclaw/plugin-sdk/channel-targets";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
import {
|
||||
createAllowedChatSenderMatcher,
|
||||
type ChatSenderAllowParams,
|
||||
|
||||
@@ -494,10 +494,6 @@
|
||||
"types": "./dist/plugin-sdk/imessage.d.ts",
|
||||
"default": "./dist/plugin-sdk/imessage.js"
|
||||
},
|
||||
"./plugin-sdk/imessage-core": {
|
||||
"types": "./dist/plugin-sdk/imessage-core.d.ts",
|
||||
"default": "./dist/plugin-sdk/imessage-core.js"
|
||||
},
|
||||
"./plugin-sdk/imessage-policy": {
|
||||
"types": "./dist/plugin-sdk/imessage-policy.d.ts",
|
||||
"default": "./dist/plugin-sdk/imessage-policy.js"
|
||||
@@ -506,10 +502,6 @@
|
||||
"types": "./dist/plugin-sdk/imessage-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/imessage-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/imessage-targets": {
|
||||
"types": "./dist/plugin-sdk/imessage-targets.d.ts",
|
||||
"default": "./dist/plugin-sdk/imessage-targets.js"
|
||||
},
|
||||
"./plugin-sdk/irc": {
|
||||
"types": "./dist/plugin-sdk/irc.d.ts",
|
||||
"default": "./dist/plugin-sdk/irc.js"
|
||||
|
||||
@@ -113,10 +113,8 @@
|
||||
"image-generation",
|
||||
"image-generation-core",
|
||||
"imessage",
|
||||
"imessage-core",
|
||||
"imessage-policy",
|
||||
"imessage-runtime",
|
||||
"imessage-targets",
|
||||
"irc",
|
||||
"irc-surface",
|
||||
"kimi-coding",
|
||||
|
||||
@@ -427,19 +427,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
||||
"isHuggingfacePolicyLocked",
|
||||
],
|
||||
},
|
||||
{
|
||||
subpath: "imessage-targets",
|
||||
source: pluginSource("imessage", "api.js"),
|
||||
exports: [
|
||||
"normalizeIMessageHandle",
|
||||
"parseChatAllowTargetPrefixes",
|
||||
"parseChatTargetPrefixesOrThrow",
|
||||
"resolveServicePrefixedAllowTarget",
|
||||
"resolveServicePrefixedTarget",
|
||||
"ParsedChatTarget",
|
||||
],
|
||||
typeExports: ["ParsedChatTarget"],
|
||||
},
|
||||
{
|
||||
subpath: "image-generation-runtime",
|
||||
source: pluginSource("image-generation-core", "runtime-api.js"),
|
||||
|
||||
@@ -64,7 +64,10 @@ function toChatChannelMeta(params: {
|
||||
function buildChatChannelMetaById(): Record<ChatChannelId, ChatChannelMeta> {
|
||||
const entries = new Map<ChatChannelId, ChatChannelMeta>();
|
||||
|
||||
for (const entry of listBundledPluginMetadata()) {
|
||||
for (const entry of listBundledPluginMetadata({
|
||||
includeChannelConfigs: true,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
})) {
|
||||
const channel =
|
||||
entry.packageManifest && "channel" in entry.packageManifest
|
||||
? entry.packageManifest.channel
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function ensureConfiguredBindingTargetReady(params: {
|
||||
cfg: OpenClawConfig;
|
||||
bindingResolution: ConfiguredBindingResolution | null;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
ensureStatefulTargetBuiltinsRegistered();
|
||||
await ensureStatefulTargetBuiltinsRegistered();
|
||||
if (!params.bindingResolution) {
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export async function resetConfiguredBindingTargetInPlace(params: {
|
||||
sessionKey: string;
|
||||
reason: "new" | "reset";
|
||||
}): Promise<{ ok: true } | { ok: false; skipped?: boolean; error?: string }> {
|
||||
ensureStatefulTargetBuiltinsRegistered();
|
||||
await ensureStatefulTargetBuiltinsRegistered();
|
||||
const resolved = resolveStatefulBindingTargetBySessionKey({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -53,7 +53,7 @@ export async function ensureConfiguredBindingTargetSession(params: {
|
||||
cfg: OpenClawConfig;
|
||||
bindingResolution: ConfiguredBindingResolution;
|
||||
}): Promise<{ ok: true; sessionKey: string } | { ok: false; sessionKey: string; error: string }> {
|
||||
ensureStatefulTargetBuiltinsRegistered();
|
||||
await ensureStatefulTargetBuiltinsRegistered();
|
||||
const driver = getStatefulBindingTargetDriver(params.bindingResolution.statefulTarget.driverId);
|
||||
if (!driver) {
|
||||
return {
|
||||
|
||||
269
src/channels/plugins/chat-target-prefixes.ts
Normal file
269
src/channels/plugins/chat-target-prefixes.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
export type ServicePrefix<TService extends string> = { prefix: string; service: TService };
|
||||
|
||||
export type ChatTargetPrefixesParams = {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
};
|
||||
|
||||
export type ParsedChatTarget =
|
||||
| { kind: "chat_id"; chatId: number }
|
||||
| { kind: "chat_guid"; chatGuid: string }
|
||||
| { kind: "chat_identifier"; chatIdentifier: string };
|
||||
|
||||
export type ParsedChatAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string };
|
||||
|
||||
export type ChatSenderAllowParams = {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
chatId?: number | null;
|
||||
chatGuid?: string | null;
|
||||
chatIdentifier?: string | null;
|
||||
};
|
||||
|
||||
function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
chatId?: number | null;
|
||||
chatGuid?: string | null;
|
||||
chatIdentifier?: string | null;
|
||||
normalizeSender: (sender: string) => string;
|
||||
parseAllowTarget: (entry: string) => TParsed;
|
||||
}): boolean {
|
||||
const allowFrom = params.allowFrom.map((entry) => String(entry).trim());
|
||||
if (allowFrom.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (allowFrom.includes("*")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const senderNormalized = params.normalizeSender(params.sender);
|
||||
const chatId = params.chatId ?? undefined;
|
||||
const chatGuid = params.chatGuid?.trim();
|
||||
const chatIdentifier = params.chatIdentifier?.trim();
|
||||
|
||||
for (const entry of allowFrom) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
const parsed = params.parseAllowTarget(entry);
|
||||
if (parsed.kind === "chat_id" && chatId !== undefined) {
|
||||
if (parsed.chatId === chatId) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "chat_guid" && chatGuid) {
|
||||
if (parsed.chatGuid === chatGuid) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "chat_identifier" && chatIdentifier) {
|
||||
if (parsed.chatIdentifier === chatIdentifier) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "handle" && senderNormalized) {
|
||||
if (parsed.handle === senderNormalized) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function stripPrefix(value: string, prefix: string): string {
|
||||
return value.slice(prefix.length).trim();
|
||||
}
|
||||
|
||||
function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean {
|
||||
return prefixes.some((prefix) => value.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedTarget<TService extends string, TTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<ServicePrefix<TService>>;
|
||||
isChatTarget: (remainderLower: string) => boolean;
|
||||
parseTarget: (remainder: string) => TTarget;
|
||||
}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null {
|
||||
for (const { prefix, service } of params.servicePrefixes) {
|
||||
if (!params.lower.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const remainder = stripPrefix(params.trimmed, prefix);
|
||||
if (!remainder) {
|
||||
throw new Error(`${prefix} target is required`);
|
||||
}
|
||||
const remainderLower = remainder.toLowerCase();
|
||||
if (params.isChatTarget(remainderLower)) {
|
||||
return params.parseTarget(remainder);
|
||||
}
|
||||
return { kind: "handle", to: remainder, service };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedChatTarget<TService extends string, TTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<ServicePrefix<TService>>;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
extraChatPrefixes?: string[];
|
||||
parseTarget: (remainder: string) => TTarget;
|
||||
}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null {
|
||||
const chatPrefixes = [
|
||||
...params.chatIdPrefixes,
|
||||
...params.chatGuidPrefixes,
|
||||
...params.chatIdentifierPrefixes,
|
||||
...(params.extraChatPrefixes ?? []),
|
||||
];
|
||||
return resolveServicePrefixedTarget({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
servicePrefixes: params.servicePrefixes,
|
||||
isChatTarget: (remainderLower) => startsWithAnyPrefix(remainderLower, chatPrefixes),
|
||||
parseTarget: params.parseTarget,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseChatTargetPrefixesOrThrow(
|
||||
params: ChatTargetPrefixesParams,
|
||||
): ParsedChatTarget | null {
|
||||
for (const prefix of params.chatIdPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(chatId)) {
|
||||
throw new Error(`Invalid chat_id: ${value}`);
|
||||
}
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatGuidPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (!value) {
|
||||
throw new Error("chat_guid is required");
|
||||
}
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatIdentifierPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (!value) {
|
||||
throw new Error("chat_identifier is required");
|
||||
}
|
||||
return { kind: "chat_identifier", chatIdentifier: value };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedAllowTarget<TAllowTarget>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<{ prefix: string }>;
|
||||
parseAllowTarget: (remainder: string) => TAllowTarget;
|
||||
}): (TAllowTarget | { kind: "handle"; handle: string }) | null {
|
||||
for (const { prefix } of params.servicePrefixes) {
|
||||
if (!params.lower.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const remainder = stripPrefix(params.trimmed, prefix);
|
||||
if (!remainder) {
|
||||
return { kind: "handle", handle: "" };
|
||||
}
|
||||
return params.parseAllowTarget(remainder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveServicePrefixedOrChatAllowTarget<
|
||||
TAllowTarget extends ParsedChatAllowTarget,
|
||||
>(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
servicePrefixes: Array<{ prefix: string }>;
|
||||
parseAllowTarget: (remainder: string) => TAllowTarget;
|
||||
chatIdPrefixes: string[];
|
||||
chatGuidPrefixes: string[];
|
||||
chatIdentifierPrefixes: string[];
|
||||
}): TAllowTarget | null {
|
||||
const servicePrefixed = resolveServicePrefixedAllowTarget({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
servicePrefixes: params.servicePrefixes,
|
||||
parseAllowTarget: params.parseAllowTarget,
|
||||
});
|
||||
if (servicePrefixed) {
|
||||
return servicePrefixed as TAllowTarget;
|
||||
}
|
||||
|
||||
const chatTarget = parseChatAllowTargetPrefixes({
|
||||
trimmed: params.trimmed,
|
||||
lower: params.lower,
|
||||
chatIdPrefixes: params.chatIdPrefixes,
|
||||
chatGuidPrefixes: params.chatGuidPrefixes,
|
||||
chatIdentifierPrefixes: params.chatIdentifierPrefixes,
|
||||
});
|
||||
if (chatTarget) {
|
||||
return chatTarget as TAllowTarget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createAllowedChatSenderMatcher<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
normalizeSender: (sender: string) => string;
|
||||
parseAllowTarget: (entry: string) => TParsed;
|
||||
}): (input: ChatSenderAllowParams) => boolean {
|
||||
return (input) =>
|
||||
isAllowedParsedChatSender({
|
||||
allowFrom: input.allowFrom,
|
||||
sender: input.sender,
|
||||
chatId: input.chatId,
|
||||
chatGuid: input.chatGuid,
|
||||
chatIdentifier: input.chatIdentifier,
|
||||
normalizeSender: params.normalizeSender,
|
||||
parseAllowTarget: params.parseAllowTarget,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseChatAllowTargetPrefixes(
|
||||
params: ChatTargetPrefixesParams,
|
||||
): ParsedChatTarget | null {
|
||||
for (const prefix of params.chatIdPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(chatId)) {
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatGuidPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (value) {
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of params.chatIdentifierPrefixes) {
|
||||
if (params.lower.startsWith(prefix)) {
|
||||
const value = stripPrefix(params.trimmed, prefix);
|
||||
if (value) {
|
||||
return { kind: "chat_identifier", chatIdentifier: value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeIMessageHandle } from "../../../plugin-sdk/imessage-targets.js";
|
||||
import { normalizeIMessageHandle } from "../../../plugin-sdk/imessage-policy.js";
|
||||
import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js";
|
||||
|
||||
// Service prefixes that indicate explicit delivery method; must be preserved during normalization
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { acpStatefulBindingTargetDriver } from "./acp-stateful-target-driver.js";
|
||||
import {
|
||||
registerStatefulBindingTargetDriver,
|
||||
unregisterStatefulBindingTargetDriver,
|
||||
} from "./stateful-target-drivers.js";
|
||||
|
||||
export function ensureStatefulTargetBuiltinsRegistered(): void {
|
||||
registerStatefulBindingTargetDriver(acpStatefulBindingTargetDriver);
|
||||
let builtinsRegisteredPromise: Promise<void> | null = null;
|
||||
|
||||
export async function ensureStatefulTargetBuiltinsRegistered(): Promise<void> {
|
||||
if (builtinsRegisteredPromise) {
|
||||
await builtinsRegisteredPromise;
|
||||
return;
|
||||
}
|
||||
builtinsRegisteredPromise = (async () => {
|
||||
const { acpStatefulBindingTargetDriver } = await import("./acp-stateful-target-driver.js");
|
||||
registerStatefulBindingTargetDriver(acpStatefulBindingTargetDriver);
|
||||
})();
|
||||
try {
|
||||
await builtinsRegisteredPromise;
|
||||
} catch (error) {
|
||||
builtinsRegisteredPromise = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetStatefulTargetBuiltinsForTesting(): void {
|
||||
export async function resetStatefulTargetBuiltinsForTesting(): Promise<void> {
|
||||
builtinsRegisteredPromise = null;
|
||||
const { acpStatefulBindingTargetDriver } = await import("./acp-stateful-target-driver.js");
|
||||
unregisterStatefulBindingTargetDriver(acpStatefulBindingTargetDriver.id);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { recordSessionMetaFromInbound, resolveStorePath } from "../config/sessions.js";
|
||||
|
||||
let inboundSessionRuntimePromise: Promise<
|
||||
typeof import("../config/sessions/inbound.runtime.js")
|
||||
> | null = null;
|
||||
|
||||
function loadInboundSessionRuntime() {
|
||||
inboundSessionRuntimePromise ??= import("../config/sessions/inbound.runtime.js");
|
||||
return inboundSessionRuntimePromise;
|
||||
}
|
||||
|
||||
export async function recordInboundSessionMetaSafe(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -9,11 +17,12 @@ export async function recordInboundSessionMetaSafe(params: {
|
||||
ctx: MsgContext;
|
||||
onError?: (error: unknown) => void;
|
||||
}): Promise<void> {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
const runtime = await loadInboundSessionRuntime();
|
||||
const storePath = runtime.resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
try {
|
||||
await recordSessionMetaFromInbound({
|
||||
await runtime.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
ctx: params.ctx,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { MsgContext } from "../auto-reply/templating.js";
|
||||
const recordSessionMetaFromInboundMock = vi.fn((_args?: unknown) => Promise.resolve(undefined));
|
||||
const updateLastRouteMock = vi.fn((_args?: unknown) => Promise.resolve(undefined));
|
||||
|
||||
vi.mock("../config/sessions.js", () => ({
|
||||
vi.mock("../config/sessions/inbound.runtime.js", () => ({
|
||||
recordSessionMetaFromInbound: (args: unknown) => recordSessionMetaFromInboundMock(args),
|
||||
updateLastRoute: (args: unknown) => updateLastRouteMock(args),
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import {
|
||||
recordSessionMetaFromInbound,
|
||||
type GroupKeyResolution,
|
||||
type SessionEntry,
|
||||
updateLastRoute,
|
||||
} from "../config/sessions.js";
|
||||
import type { GroupKeyResolution, SessionEntry } from "../config/sessions/types.js";
|
||||
|
||||
let inboundSessionRuntimePromise: Promise<
|
||||
typeof import("../config/sessions/inbound.runtime.js")
|
||||
> | null = null;
|
||||
|
||||
function loadInboundSessionRuntime() {
|
||||
inboundSessionRuntimePromise ??= import("../config/sessions/inbound.runtime.js");
|
||||
return inboundSessionRuntimePromise;
|
||||
}
|
||||
|
||||
function normalizeSessionStoreKey(sessionKey: string): string {
|
||||
return sessionKey.trim().toLowerCase();
|
||||
@@ -49,13 +53,16 @@ export async function recordInboundSession(params: {
|
||||
}): Promise<void> {
|
||||
const { storePath, sessionKey, ctx, groupResolution, createIfMissing } = params;
|
||||
const canonicalSessionKey = normalizeSessionStoreKey(sessionKey);
|
||||
void recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: canonicalSessionKey,
|
||||
ctx,
|
||||
groupResolution,
|
||||
createIfMissing,
|
||||
}).catch(params.onRecordError);
|
||||
const runtime = await loadInboundSessionRuntime();
|
||||
void runtime
|
||||
.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: canonicalSessionKey,
|
||||
ctx,
|
||||
groupResolution,
|
||||
createIfMissing,
|
||||
})
|
||||
.catch(params.onRecordError);
|
||||
|
||||
const update = params.updateLastRoute;
|
||||
if (!update) {
|
||||
@@ -65,7 +72,7 @@ export async function recordInboundSession(params: {
|
||||
return;
|
||||
}
|
||||
const targetSessionKey = normalizeSessionStoreKey(update.sessionKey);
|
||||
await updateLastRoute({
|
||||
await runtime.updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: targetSessionKey,
|
||||
deliveryContext: {
|
||||
|
||||
2
src/config/sessions/inbound.runtime.ts
Normal file
2
src/config/sessions/inbound.runtime.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { resolveStorePath } from "./paths.js";
|
||||
export { recordSessionMetaFromInbound, updateLastRoute } from "./store.js";
|
||||
@@ -223,17 +223,6 @@ export interface PluginSdkFacadeTypeMap {
|
||||
};
|
||||
types: {};
|
||||
};
|
||||
"imessage-targets": {
|
||||
module: typeof import("@openclaw/imessage/api.js");
|
||||
sourceModules: {
|
||||
source1: {
|
||||
module: typeof import("@openclaw/imessage/api.js");
|
||||
};
|
||||
};
|
||||
types: {
|
||||
ParsedChatTarget: import("@openclaw/imessage/api.js").ParsedChatTarget;
|
||||
};
|
||||
};
|
||||
"image-generation-runtime": {
|
||||
module: typeof import("@openclaw/image-generation-core/runtime-api.js");
|
||||
sourceModules: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeConversationText } from "../../acp/conversation-id.js";
|
||||
import { listBundledChannelPlugins } from "../../channels/plugins/bundled.js";
|
||||
import { normalizeAnyChannelId } from "../../channels/registry.js";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import { loadJsonFile } from "../../infra/json-file.js";
|
||||
@@ -128,9 +127,11 @@ function resolveChannelSupportsCurrentConversationBinding(channel: string): bool
|
||||
const matchesPluginId = (plugin: { id: string; meta?: { aliases?: readonly string[] } }) =>
|
||||
plugin.id === normalized ||
|
||||
(plugin.meta?.aliases ?? []).some((alias) => alias.trim().toLowerCase() === normalized);
|
||||
const plugin =
|
||||
getActivePluginChannelRegistry()?.channels.find((entry) => matchesPluginId(entry.plugin))
|
||||
?.plugin ?? listBundledChannelPlugins().find((entry) => matchesPluginId(entry));
|
||||
// Keep this resolver on the active runtime registry only. Importing bundled
|
||||
// channel loaders here creates a module cycle through plugin-sdk surfaces.
|
||||
const plugin = getActivePluginChannelRegistry()?.channels.find((entry) =>
|
||||
matchesPluginId(entry.plugin),
|
||||
)?.plugin;
|
||||
return plugin?.conversationBindings?.supportsCurrentConversationBinding === true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import {
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedTarget,
|
||||
type ParsedChatTarget,
|
||||
} from "./imessage-targets.js";
|
||||
} from "./channel-targets.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
|
||||
// Keep this list additive and scoped to the conversation-binding seam only.
|
||||
@@ -324,7 +324,7 @@ export {
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ParsedChatTarget,
|
||||
} from "./imessage-targets.js";
|
||||
} from "./channel-targets.js";
|
||||
export { stripMarkdown } from "./text-runtime.js";
|
||||
export { parseFiniteNumber } from "../infra/parse-finite-number.js";
|
||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
|
||||
@@ -13,11 +13,10 @@ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const REPO_ROOT = resolve(ROOT_DIR, "..");
|
||||
const ALLOWED_EXTENSION_PUBLIC_SURFACES = new Set(GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES);
|
||||
ALLOWED_EXTENSION_PUBLIC_SURFACES.add("test-api.js");
|
||||
const BUNDLED_EXTENSION_IDS = new Set(
|
||||
readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name !== "shared")
|
||||
.map((entry) => entry.name),
|
||||
);
|
||||
const BUNDLED_EXTENSION_IDS = readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name !== "shared")
|
||||
.map((entry) => entry.name)
|
||||
.toSorted((left, right) => right.length - left.length);
|
||||
const GUARDED_CHANNEL_EXTENSIONS = new Set([
|
||||
"bluebubbles",
|
||||
"discord",
|
||||
@@ -190,6 +189,7 @@ const LOCAL_EXTENSION_API_BARREL_GUARDS = [
|
||||
"diffs",
|
||||
"feishu",
|
||||
"google",
|
||||
"imessage",
|
||||
"irc",
|
||||
"llm-task",
|
||||
"line",
|
||||
@@ -472,7 +472,12 @@ function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): v
|
||||
continue;
|
||||
}
|
||||
const targetSubpath = specifier.slice("openclaw/plugin-sdk/".length);
|
||||
if (!BUNDLED_EXTENSION_IDS.has(targetSubpath) || targetSubpath === currentExtensionId) {
|
||||
const targetExtensionId =
|
||||
BUNDLED_EXTENSION_IDS.find(
|
||||
(extensionId) =>
|
||||
targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`),
|
||||
) ?? null;
|
||||
if (!targetExtensionId || targetExtensionId === currentExtensionId) {
|
||||
continue;
|
||||
}
|
||||
expect.fail(
|
||||
@@ -585,7 +590,7 @@ describe("channel import guardrails", () => {
|
||||
expect(
|
||||
text,
|
||||
`${normalized} should import ${extensionId} helpers via the local api barrel`,
|
||||
).not.toMatch(new RegExp(`["']openclaw/plugin-sdk/${extensionId}["']`, "u"));
|
||||
).not.toMatch(new RegExp(`["']openclaw/plugin-sdk/${extensionId}(?:["'/])`, "u"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,20 @@ export {
|
||||
type MessagingTargetKind,
|
||||
type MessagingTargetParseOptions,
|
||||
} from "../channels/targets.js";
|
||||
export {
|
||||
createAllowedChatSenderMatcher,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedChatTarget,
|
||||
resolveServicePrefixedOrChatAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ChatSenderAllowParams,
|
||||
type ChatTargetPrefixesParams,
|
||||
type ParsedChatAllowTarget,
|
||||
type ParsedChatTarget,
|
||||
type ServicePrefix,
|
||||
} from "../channels/plugins/chat-target-prefixes.js";
|
||||
export {
|
||||
buildUnresolvedTargetResults,
|
||||
resolveTargetsWithOptionalToken,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import {
|
||||
normalizeIMessageHandle,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ParsedChatTarget,
|
||||
} from "./imessage-targets.js";
|
||||
|
||||
export type { ChannelPlugin } from "./channel-plugin-common.js";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildChannelConfigSchema,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "./channel-plugin-common.js";
|
||||
export {
|
||||
formatTrimmedAllowFromEntries,
|
||||
resolveIMessageConfigAllowFrom,
|
||||
resolveIMessageConfigDefaultTo,
|
||||
} from "./channel-config-helpers.js";
|
||||
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||
export {
|
||||
normalizeIMessageHandle,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ParsedChatTarget,
|
||||
} from "./imessage-targets.js";
|
||||
|
||||
type IMessageService = "imessage" | "sms" | "auto";
|
||||
|
||||
type IMessageTarget = ParsedChatTarget | { kind: "handle"; to: string; service: IMessageService };
|
||||
|
||||
const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
|
||||
const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
|
||||
const CHAT_IDENTIFIER_PREFIXES = ["chat_identifier:", "chatidentifier:", "chatident:"];
|
||||
const SERVICE_PREFIXES: Array<{ prefix: string; service: IMessageService }> = [
|
||||
{ prefix: "imessage:", service: "imessage" },
|
||||
{ prefix: "sms:", service: "sms" },
|
||||
{ prefix: "auto:", service: "auto" },
|
||||
];
|
||||
|
||||
function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean {
|
||||
return prefixes.some((prefix) => value.startsWith(prefix));
|
||||
}
|
||||
|
||||
function parseIMessageTarget(raw: string): IMessageTarget {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("iMessage target is required");
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
const servicePrefixed = resolveServicePrefixedTarget({
|
||||
trimmed,
|
||||
lower,
|
||||
servicePrefixes: SERVICE_PREFIXES,
|
||||
isChatTarget: (remainderLower) =>
|
||||
startsWithAnyPrefix(remainderLower, [
|
||||
...CHAT_ID_PREFIXES,
|
||||
...CHAT_GUID_PREFIXES,
|
||||
...CHAT_IDENTIFIER_PREFIXES,
|
||||
]),
|
||||
parseTarget: parseIMessageTarget,
|
||||
});
|
||||
if (servicePrefixed) {
|
||||
return servicePrefixed;
|
||||
}
|
||||
|
||||
const chatTarget = parseChatTargetPrefixesOrThrow({
|
||||
trimmed,
|
||||
lower,
|
||||
chatIdPrefixes: CHAT_ID_PREFIXES,
|
||||
chatGuidPrefixes: CHAT_GUID_PREFIXES,
|
||||
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
|
||||
});
|
||||
if (chatTarget) {
|
||||
return chatTarget;
|
||||
}
|
||||
|
||||
return { kind: "handle", to: trimmed, service: "auto" };
|
||||
}
|
||||
|
||||
export function normalizeIMessageAcpConversationId(
|
||||
conversationId: string,
|
||||
): { conversationId: string } | null {
|
||||
const trimmed = conversationId.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parseIMessageTarget(trimmed);
|
||||
if (parsed.kind === "handle") {
|
||||
const handle = normalizeIMessageHandle(parsed.to);
|
||||
return handle ? { conversationId: handle } : null;
|
||||
}
|
||||
if (parsed.kind === "chat_id") {
|
||||
return { conversationId: String(parsed.chatId) };
|
||||
}
|
||||
if (parsed.kind === "chat_guid") {
|
||||
return { conversationId: parsed.chatGuid };
|
||||
}
|
||||
return { conversationId: parsed.chatIdentifier };
|
||||
} catch {
|
||||
const handle = normalizeIMessageHandle(trimmed);
|
||||
return handle ? { conversationId: handle } : null;
|
||||
}
|
||||
}
|
||||
|
||||
export function matchIMessageAcpConversation(params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
}): { conversationId: string; matchPriority: number } | null {
|
||||
const binding = normalizeIMessageAcpConversationId(params.bindingConversationId);
|
||||
const conversation = normalizeIMessageAcpConversationId(params.conversationId);
|
||||
if (!binding || !conversation) {
|
||||
return null;
|
||||
}
|
||||
if (binding.conversationId !== conversation.conversationId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: conversation.conversationId,
|
||||
matchPriority: 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
|
||||
return normalizeIMessageAcpConversationId(target)?.conversationId;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Generated by scripts/generate-plugin-sdk-facades.mjs. Do not edit manually.
|
||||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["imessage-targets"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "imessage",
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
}
|
||||
export const normalizeIMessageHandle: FacadeModule["normalizeIMessageHandle"] = ((...args) =>
|
||||
loadFacadeModule()["normalizeIMessageHandle"](
|
||||
...args,
|
||||
)) as FacadeModule["normalizeIMessageHandle"];
|
||||
export const parseChatAllowTargetPrefixes: FacadeModule["parseChatAllowTargetPrefixes"] = ((
|
||||
...args
|
||||
) =>
|
||||
loadFacadeModule()["parseChatAllowTargetPrefixes"](
|
||||
...args,
|
||||
)) as FacadeModule["parseChatAllowTargetPrefixes"];
|
||||
export const parseChatTargetPrefixesOrThrow: FacadeModule["parseChatTargetPrefixesOrThrow"] = ((
|
||||
...args
|
||||
) =>
|
||||
loadFacadeModule()["parseChatTargetPrefixesOrThrow"](
|
||||
...args,
|
||||
)) as FacadeModule["parseChatTargetPrefixesOrThrow"];
|
||||
export const resolveServicePrefixedAllowTarget: FacadeModule["resolveServicePrefixedAllowTarget"] =
|
||||
((...args) =>
|
||||
loadFacadeModule()["resolveServicePrefixedAllowTarget"](
|
||||
...args,
|
||||
)) as FacadeModule["resolveServicePrefixedAllowTarget"];
|
||||
export const resolveServicePrefixedTarget: FacadeModule["resolveServicePrefixedTarget"] = ((
|
||||
...args
|
||||
) =>
|
||||
loadFacadeModule()["resolveServicePrefixedTarget"](
|
||||
...args,
|
||||
)) as FacadeModule["resolveServicePrefixedTarget"];
|
||||
export type ParsedChatTarget = FacadeEntry["types"]["ParsedChatTarget"];
|
||||
@@ -35,12 +35,16 @@ export {
|
||||
normalizeIMessageMessagingTarget,
|
||||
} from "../channels/plugins/normalize/imessage.js";
|
||||
export {
|
||||
createAllowedChatSenderMatcher,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedChatTarget,
|
||||
resolveServicePrefixedOrChatAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
type ChatSenderAllowParams,
|
||||
type ParsedChatTarget,
|
||||
} from "./imessage-targets.js";
|
||||
} from "./channel-targets.js";
|
||||
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
@@ -70,6 +74,12 @@ type IMessageFacadeModule = {
|
||||
accountId?: string;
|
||||
cfg: OpenClawConfig;
|
||||
}) => IMessageConversationBindingManager;
|
||||
matchIMessageAcpConversation: (params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
}) => { conversationId: string; matchPriority: number } | null;
|
||||
normalizeIMessageAcpConversationId: (conversationId: string) => { conversationId: string } | null;
|
||||
resolveIMessageConversationIdFromTarget: (target: string) => string | undefined;
|
||||
};
|
||||
|
||||
function loadIMessageFacadeModule(): IMessageFacadeModule {
|
||||
@@ -85,3 +95,20 @@ export function createIMessageConversationBindingManager(params: {
|
||||
}): IMessageConversationBindingManager {
|
||||
return loadIMessageFacadeModule().createIMessageConversationBindingManager(params);
|
||||
}
|
||||
|
||||
export function normalizeIMessageAcpConversationId(
|
||||
conversationId: string,
|
||||
): { conversationId: string } | null {
|
||||
return loadIMessageFacadeModule().normalizeIMessageAcpConversationId(conversationId);
|
||||
}
|
||||
|
||||
export function matchIMessageAcpConversation(params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
}): { conversationId: string; matchPriority: number } | null {
|
||||
return loadIMessageFacadeModule().matchIMessageAcpConversation(params);
|
||||
}
|
||||
|
||||
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
|
||||
return loadIMessageFacadeModule().resolveIMessageConversationIdFromTarget(target);
|
||||
}
|
||||
|
||||
@@ -199,22 +199,16 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expectSourceContains("telegram", 'export * from "./telegram-core.js";');
|
||||
expectSourceContains("telegram", 'export * from "./telegram-runtime.js";');
|
||||
expectSourceMentions("imessage", [
|
||||
"normalizeIMessageAcpConversationId",
|
||||
"matchIMessageAcpConversation",
|
||||
"normalizeIMessageHandle",
|
||||
"parseChatAllowTargetPrefixes",
|
||||
"parseChatTargetPrefixesOrThrow",
|
||||
"resolveIMessageConversationIdFromTarget",
|
||||
"resolveServicePrefixedAllowTarget",
|
||||
"resolveServicePrefixedTarget",
|
||||
"chunkTextForOutbound",
|
||||
]);
|
||||
expectSourceMentions("imessage-core", [
|
||||
"normalizeIMessageAcpConversationId",
|
||||
"matchIMessageAcpConversation",
|
||||
"resolveIMessageConversationIdFromTarget",
|
||||
"parseChatAllowTargetPrefixes",
|
||||
"parseChatTargetPrefixesOrThrow",
|
||||
"resolveServicePrefixedAllowTarget",
|
||||
"resolveServicePrefixedTarget",
|
||||
]);
|
||||
expectSourceMentions("bluebubbles", [
|
||||
"normalizeBlueBubblesAcpConversationId",
|
||||
"matchBlueBubblesAcpConversation",
|
||||
@@ -503,11 +497,18 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"applyChannelMatchMeta",
|
||||
"buildChannelKeyCandidates",
|
||||
"buildMessagingTarget",
|
||||
"createAllowedChatSenderMatcher",
|
||||
"ensureTargetId",
|
||||
"parseChatAllowTargetPrefixes",
|
||||
"parseMentionPrefixOrAtUserTarget",
|
||||
"parseChatTargetPrefixesOrThrow",
|
||||
"requireTargetKind",
|
||||
"resolveChannelEntryMatchWithFallback",
|
||||
"resolveChannelMatchConfig",
|
||||
"resolveServicePrefixedAllowTarget",
|
||||
"resolveServicePrefixedChatTarget",
|
||||
"resolveServicePrefixedOrChatAllowTarget",
|
||||
"resolveServicePrefixedTarget",
|
||||
"resolveTargetsWithOptionalToken",
|
||||
]);
|
||||
expectSourceMentions("channel-config-writes", [
|
||||
|
||||
@@ -360,6 +360,7 @@ function collectBundledChannelConfigs(params: {
|
||||
function collectBundledPluginMetadataForPackageRoot(
|
||||
packageRoot: string,
|
||||
includeChannelConfigs: boolean,
|
||||
includeSyntheticChannelConfigs: boolean,
|
||||
): readonly BundledPluginMetadata[] {
|
||||
const scanDir = resolveBundledPluginScanDir(packageRoot);
|
||||
if (!scanDir || !fs.existsSync(scanDir)) {
|
||||
@@ -404,13 +405,14 @@ function collectBundledPluginMetadataForPackageRoot(
|
||||
...(setupSourcePath ? { setupEntry: setupSourcePath } : {}),
|
||||
});
|
||||
const runtimeSidecarArtifacts = collectRuntimeSidecarArtifacts(publicSurfaceArtifacts);
|
||||
const channelConfigs = includeChannelConfigs
|
||||
? collectBundledChannelConfigs({
|
||||
pluginDir,
|
||||
manifest: manifestResult.manifest,
|
||||
packageManifest,
|
||||
})
|
||||
: manifestResult.manifest.channelConfigs;
|
||||
const channelConfigs =
|
||||
includeChannelConfigs && includeSyntheticChannelConfigs
|
||||
? collectBundledChannelConfigs({
|
||||
pluginDir,
|
||||
manifest: manifestResult.manifest,
|
||||
packageManifest,
|
||||
})
|
||||
: manifestResult.manifest.channelConfigs;
|
||||
|
||||
entries.push({
|
||||
dirName,
|
||||
@@ -448,16 +450,27 @@ function collectBundledPluginMetadataForPackageRoot(
|
||||
export function listBundledPluginMetadata(params?: {
|
||||
rootDir?: string;
|
||||
includeChannelConfigs?: boolean;
|
||||
includeSyntheticChannelConfigs?: boolean;
|
||||
}): readonly BundledPluginMetadata[] {
|
||||
const rootDir = path.resolve(params?.rootDir ?? OPENCLAW_PACKAGE_ROOT);
|
||||
const includeChannelConfigs = params?.includeChannelConfigs ?? !RUNNING_FROM_BUILT_ARTIFACT;
|
||||
const cacheKey = JSON.stringify({ rootDir, includeChannelConfigs });
|
||||
const includeSyntheticChannelConfigs =
|
||||
params?.includeSyntheticChannelConfigs ?? includeChannelConfigs;
|
||||
const cacheKey = JSON.stringify({
|
||||
rootDir,
|
||||
includeChannelConfigs,
|
||||
includeSyntheticChannelConfigs,
|
||||
});
|
||||
const cached = bundledPluginMetadataCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const entries = Object.freeze(
|
||||
collectBundledPluginMetadataForPackageRoot(rootDir, includeChannelConfigs),
|
||||
collectBundledPluginMetadataForPackageRoot(
|
||||
rootDir,
|
||||
includeChannelConfigs,
|
||||
includeSyntheticChannelConfigs,
|
||||
),
|
||||
);
|
||||
bundledPluginMetadataCache.set(cacheKey, entries);
|
||||
return entries;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { imessageOutbound } from "../../test/channel-outbounds.js";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { normalizeIMessageHandle } from "../plugin-sdk/imessage-targets.js";
|
||||
import { normalizeIMessageHandle } from "../plugin-sdk/imessage-policy.js";
|
||||
import { collectStatusIssuesFromLastError } from "../plugin-sdk/status-helpers.js";
|
||||
|
||||
export const createIMessageTestPlugin = (params?: {
|
||||
|
||||
Reference in New Issue
Block a user