Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f.
This commit is contained in:
@@ -1,17 +0,0 @@
|
|||||||
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
||||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
||||||
import { linqPlugin } from "./src/channel.js";
|
|
||||||
import { setLinqRuntime } from "./src/runtime.js";
|
|
||||||
|
|
||||||
const plugin = {
|
|
||||||
id: "linq",
|
|
||||||
name: "Linq",
|
|
||||||
description: "Linq iMessage channel plugin — real iMessage over API, no Mac required",
|
|
||||||
configSchema: emptyPluginConfigSchema(),
|
|
||||||
register(api: OpenClawPluginApi) {
|
|
||||||
setLinqRuntime(api.runtime);
|
|
||||||
api.registerChannel({ plugin: linqPlugin as ChannelPlugin });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "linq",
|
|
||||||
"channels": ["linq"],
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@openclaw/linq",
|
|
||||||
"version": "2026.2.16",
|
|
||||||
"private": true,
|
|
||||||
"description": "OpenClaw Linq iMessage channel plugin",
|
|
||||||
"type": "module",
|
|
||||||
"devDependencies": {
|
|
||||||
"openclaw": "workspace:*"
|
|
||||||
},
|
|
||||||
"openclaw": {
|
|
||||||
"extensions": [
|
|
||||||
"./index.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
import {
|
|
||||||
applyAccountNameToChannelSection,
|
|
||||||
buildChannelConfigSchema,
|
|
||||||
DEFAULT_ACCOUNT_ID,
|
|
||||||
deleteAccountFromConfigSection,
|
|
||||||
formatPairingApproveHint,
|
|
||||||
getChatChannelMeta,
|
|
||||||
listLinqAccountIds,
|
|
||||||
migrateBaseNameToDefaultAccount,
|
|
||||||
normalizeAccountId,
|
|
||||||
resolveDefaultLinqAccountId,
|
|
||||||
resolveLinqAccount,
|
|
||||||
setAccountEnabledInConfigSection,
|
|
||||||
type ChannelPlugin,
|
|
||||||
type OpenClawConfig,
|
|
||||||
type ResolvedLinqAccount,
|
|
||||||
type LinqProbe,
|
|
||||||
LinqConfigSchema,
|
|
||||||
} from "openclaw/plugin-sdk";
|
|
||||||
import { getLinqRuntime } from "./runtime.js";
|
|
||||||
|
|
||||||
const meta = getChatChannelMeta("linq");
|
|
||||||
|
|
||||||
export const linqPlugin: ChannelPlugin<ResolvedLinqAccount, LinqProbe> = {
|
|
||||||
id: "linq",
|
|
||||||
meta: {
|
|
||||||
...meta,
|
|
||||||
aliases: ["linq-imessage"],
|
|
||||||
},
|
|
||||||
pairing: {
|
|
||||||
idLabel: "phoneNumber",
|
|
||||||
notifyApproval: async ({ id }) => {
|
|
||||||
// Approval notification would need a chat_id, not just a phone number.
|
|
||||||
// For now this is a no-op; pairing replies are sent in the monitor.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
chatTypes: ["direct", "group"],
|
|
||||||
reactions: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
reload: { configPrefixes: ["channels.linq"] },
|
|
||||||
configSchema: buildChannelConfigSchema(LinqConfigSchema),
|
|
||||||
config: {
|
|
||||||
listAccountIds: (cfg) => listLinqAccountIds(cfg),
|
|
||||||
resolveAccount: (cfg, accountId) => resolveLinqAccount({ cfg, accountId }),
|
|
||||||
defaultAccountId: (cfg) => resolveDefaultLinqAccountId(cfg),
|
|
||||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
||||||
setAccountEnabledInConfigSection({
|
|
||||||
cfg,
|
|
||||||
sectionKey: "linq",
|
|
||||||
accountId,
|
|
||||||
enabled,
|
|
||||||
allowTopLevel: true,
|
|
||||||
}),
|
|
||||||
deleteAccount: ({ cfg, accountId }) =>
|
|
||||||
deleteAccountFromConfigSection({
|
|
||||||
cfg,
|
|
||||||
sectionKey: "linq",
|
|
||||||
accountId,
|
|
||||||
clearBaseFields: ["apiToken", "tokenFile", "fromPhone", "name"],
|
|
||||||
}),
|
|
||||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
|
||||||
describeAccount: (account) => ({
|
|
||||||
accountId: account.accountId,
|
|
||||||
name: account.name,
|
|
||||||
enabled: account.enabled,
|
|
||||||
configured: Boolean(account.token?.trim()),
|
|
||||||
tokenSource: account.tokenSource,
|
|
||||||
fromPhone: account.fromPhone,
|
|
||||||
}),
|
|
||||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
||||||
(resolveLinqAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)),
|
|
||||||
formatAllowFrom: ({ allowFrom }) =>
|
|
||||||
allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
||||||
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
||||||
const linqSection = (cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
const useAccountPath = Boolean(
|
|
||||||
(linqSection?.accounts as Record<string, unknown> | undefined)?.[resolvedAccountId],
|
|
||||||
);
|
|
||||||
const basePath = useAccountPath
|
|
||||||
? `channels.linq.accounts.${resolvedAccountId}.`
|
|
||||||
: "channels.linq.";
|
|
||||||
return {
|
|
||||||
policy: account.config.dmPolicy ?? "pairing",
|
|
||||||
allowFrom: account.config.allowFrom ?? [],
|
|
||||||
policyPath: `${basePath}dmPolicy`,
|
|
||||||
allowFromPath: basePath,
|
|
||||||
approveHint: formatPairingApproveHint("linq"),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
collectWarnings: ({ account }) => {
|
|
||||||
const groupPolicy = account.config.groupPolicy ?? "open";
|
|
||||||
if (groupPolicy !== "open") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
`- Linq groups: groupPolicy="open" allows any group member to trigger. Set channels.linq.groupPolicy="allowlist" + channels.linq.groupAllowFrom to restrict senders.`,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groups: {
|
|
||||||
resolveRequireMention: (params) => undefined,
|
|
||||||
resolveToolPolicy: (params) => undefined,
|
|
||||||
},
|
|
||||||
messaging: {
|
|
||||||
normalizeTarget: (raw) => raw ?? "",
|
|
||||||
targetResolver: {
|
|
||||||
looksLikeId: (id) => /^[A-Za-z0-9_-]+$/.test(id ?? ""),
|
|
||||||
hint: "<chatId>",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup: {
|
|
||||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
||||||
applyAccountName: ({ cfg, accountId, name }) =>
|
|
||||||
applyAccountNameToChannelSection({
|
|
||||||
cfg,
|
|
||||||
channelKey: "linq",
|
|
||||||
accountId,
|
|
||||||
name,
|
|
||||||
}),
|
|
||||||
validateInput: ({ accountId, input }) => {
|
|
||||||
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
||||||
return "LINQ_API_TOKEN can only be used for the default account.";
|
|
||||||
}
|
|
||||||
if (!input.useEnv && !input.token && !input.tokenFile) {
|
|
||||||
return "Linq requires an API token or --token-file (or --use-env).";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
||||||
const namedConfig = applyAccountNameToChannelSection({
|
|
||||||
cfg,
|
|
||||||
channelKey: "linq",
|
|
||||||
accountId,
|
|
||||||
name: input.name,
|
|
||||||
});
|
|
||||||
const next =
|
|
||||||
accountId !== DEFAULT_ACCOUNT_ID
|
|
||||||
? migrateBaseNameToDefaultAccount({ cfg: namedConfig, channelKey: "linq" })
|
|
||||||
: namedConfig;
|
|
||||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
channels: {
|
|
||||||
...next.channels,
|
|
||||||
linq: {
|
|
||||||
...((next.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined),
|
|
||||||
enabled: true,
|
|
||||||
...(input.useEnv
|
|
||||||
? {}
|
|
||||||
: input.tokenFile
|
|
||||||
? { tokenFile: input.tokenFile }
|
|
||||||
: input.token
|
|
||||||
? { apiToken: input.token }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const linqSection = (next.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
channels: {
|
|
||||||
...next.channels,
|
|
||||||
linq: {
|
|
||||||
...linqSection,
|
|
||||||
enabled: true,
|
|
||||||
accounts: {
|
|
||||||
...(linqSection?.accounts as Record<string, unknown> | undefined),
|
|
||||||
[accountId]: {
|
|
||||||
...((linqSection?.accounts as Record<string, unknown> | undefined)?.[accountId] as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined),
|
|
||||||
enabled: true,
|
|
||||||
...(input.tokenFile
|
|
||||||
? { tokenFile: input.tokenFile }
|
|
||||||
: input.token
|
|
||||||
? { apiToken: input.token }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outbound: {
|
|
||||||
deliveryMode: "direct",
|
|
||||||
chunker: (text, limit) => getLinqRuntime().channel.text.chunkText(text, limit),
|
|
||||||
chunkerMode: "text",
|
|
||||||
textChunkLimit: 4000,
|
|
||||||
sendText: async ({ to, text, accountId }) => {
|
|
||||||
const send = getLinqRuntime().channel.linq.sendMessageLinq;
|
|
||||||
const result = await send(to, text, { accountId: accountId ?? undefined });
|
|
||||||
return { channel: "linq", ...result };
|
|
||||||
},
|
|
||||||
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
||||||
const send = getLinqRuntime().channel.linq.sendMessageLinq;
|
|
||||||
const result = await send(to, text, {
|
|
||||||
mediaUrl,
|
|
||||||
accountId: accountId ?? undefined,
|
|
||||||
});
|
|
||||||
return { channel: "linq", ...result };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
defaultRuntime: {
|
|
||||||
accountId: DEFAULT_ACCOUNT_ID,
|
|
||||||
running: false,
|
|
||||||
lastStartAt: null,
|
|
||||||
lastStopAt: null,
|
|
||||||
lastError: null,
|
|
||||||
},
|
|
||||||
collectStatusIssues: (accounts) =>
|
|
||||||
accounts.flatMap((account) => {
|
|
||||||
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
|
|
||||||
if (!lastError) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
channel: "linq",
|
|
||||||
accountId: account.accountId,
|
|
||||||
kind: "runtime",
|
|
||||||
message: `Channel error: ${lastError}`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
buildChannelSummary: ({ snapshot }) => ({
|
|
||||||
configured: snapshot.configured ?? false,
|
|
||||||
tokenSource: snapshot.tokenSource ?? "none",
|
|
||||||
running: snapshot.running ?? false,
|
|
||||||
lastStartAt: snapshot.lastStartAt ?? null,
|
|
||||||
lastStopAt: snapshot.lastStopAt ?? null,
|
|
||||||
lastError: snapshot.lastError ?? null,
|
|
||||||
probe: snapshot.probe,
|
|
||||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
||||||
}),
|
|
||||||
probeAccount: async ({ account, timeoutMs }) =>
|
|
||||||
getLinqRuntime().channel.linq.probeLinq(account.token, timeoutMs, account.accountId),
|
|
||||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
||||||
accountId: account.accountId,
|
|
||||||
name: account.name,
|
|
||||||
enabled: account.enabled,
|
|
||||||
configured: Boolean(account.token?.trim()),
|
|
||||||
tokenSource: account.tokenSource,
|
|
||||||
fromPhone: account.fromPhone,
|
|
||||||
running: runtime?.running ?? false,
|
|
||||||
lastStartAt: runtime?.lastStartAt ?? null,
|
|
||||||
lastStopAt: runtime?.lastStopAt ?? null,
|
|
||||||
lastError: runtime?.lastError ?? null,
|
|
||||||
probe,
|
|
||||||
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
||||||
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
gateway: {
|
|
||||||
startAccount: async (ctx) => {
|
|
||||||
const account = ctx.account;
|
|
||||||
const token = account.token.trim();
|
|
||||||
let phoneLabel = "";
|
|
||||||
try {
|
|
||||||
const probe = await getLinqRuntime().channel.linq.probeLinq(token, 2500);
|
|
||||||
if (probe.ok && probe.phoneNumbers?.length) {
|
|
||||||
phoneLabel = ` (${probe.phoneNumbers.join(", ")})`;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Probe failure is non-fatal for startup.
|
|
||||||
}
|
|
||||||
ctx.log?.info(`[${account.accountId}] starting Linq provider${phoneLabel}`);
|
|
||||||
return getLinqRuntime().channel.linq.monitorLinqProvider({
|
|
||||||
accountId: account.accountId,
|
|
||||||
config: ctx.cfg,
|
|
||||||
runtime: ctx.runtime,
|
|
||||||
abortSignal: ctx.abortSignal,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
logoutAccount: async ({ accountId, cfg }) => {
|
|
||||||
const nextCfg = { ...cfg };
|
|
||||||
const linqSection = (cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
let cleared = false;
|
|
||||||
let changed = false;
|
|
||||||
if (linqSection) {
|
|
||||||
const nextLinq = { ...linqSection };
|
|
||||||
if (accountId === DEFAULT_ACCOUNT_ID && nextLinq.apiToken) {
|
|
||||||
delete nextLinq.apiToken;
|
|
||||||
cleared = true;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
const accounts =
|
|
||||||
nextLinq.accounts && typeof nextLinq.accounts === "object"
|
|
||||||
? { ...(nextLinq.accounts as Record<string, unknown>) }
|
|
||||||
: undefined;
|
|
||||||
if (accounts && accountId in accounts) {
|
|
||||||
const entry = accounts[accountId];
|
|
||||||
if (entry && typeof entry === "object") {
|
|
||||||
const nextEntry = { ...(entry as Record<string, unknown>) };
|
|
||||||
if ("apiToken" in nextEntry) {
|
|
||||||
cleared = true;
|
|
||||||
delete nextEntry.apiToken;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (Object.keys(nextEntry).length === 0) {
|
|
||||||
delete accounts[accountId];
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
accounts[accountId] = nextEntry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (accounts) {
|
|
||||||
if (Object.keys(accounts).length === 0) {
|
|
||||||
delete nextLinq.accounts;
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
nextLinq.accounts = accounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
if (Object.keys(nextLinq).length > 0) {
|
|
||||||
nextCfg.channels = { ...nextCfg.channels, linq: nextLinq } as typeof nextCfg.channels;
|
|
||||||
} else {
|
|
||||||
const nextChannels = { ...nextCfg.channels } as Record<string, unknown>;
|
|
||||||
delete nextChannels.linq;
|
|
||||||
nextCfg.channels = nextChannels as typeof nextCfg.channels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
await getLinqRuntime().config.writeConfigFile(nextCfg);
|
|
||||||
}
|
|
||||||
const resolved = resolveLinqAccount({ cfg: changed ? nextCfg : cfg, accountId });
|
|
||||||
return { cleared, loggedOut: resolved.tokenSource === "none" };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
||||||
|
|
||||||
let runtime: PluginRuntime | null = null;
|
|
||||||
|
|
||||||
export function setLinqRuntime(next: PluginRuntime) {
|
|
||||||
runtime = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLinqRuntime(): PluginRuntime {
|
|
||||||
if (!runtime) {
|
|
||||||
throw new Error("Linq runtime not initialized");
|
|
||||||
}
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -357,12 +357,6 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
extensions/linq:
|
|
||||||
devDependencies:
|
|
||||||
openclaw:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
|
|
||||||
extensions/llm-task:
|
extensions/llm-task:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type {
|
||||||
|
ChannelCapabilities,
|
||||||
|
ChannelCommandAdapter,
|
||||||
|
ChannelElevatedAdapter,
|
||||||
|
ChannelGroupAdapter,
|
||||||
|
ChannelId,
|
||||||
|
ChannelAgentPromptAdapter,
|
||||||
|
ChannelMentionAdapter,
|
||||||
|
ChannelPlugin,
|
||||||
|
ChannelThreadingContext,
|
||||||
|
ChannelThreadingAdapter,
|
||||||
|
ChannelThreadingToolContext,
|
||||||
|
} from "./plugins/types.js";
|
||||||
import {
|
import {
|
||||||
resolveChannelGroupRequireMention,
|
resolveChannelGroupRequireMention,
|
||||||
resolveChannelGroupToolsPolicy,
|
resolveChannelGroupToolsPolicy,
|
||||||
} from "../config/group-policy.js";
|
} from "../config/group-policy.js";
|
||||||
import { resolveDiscordAccount } from "../discord/accounts.js";
|
import { resolveDiscordAccount } from "../discord/accounts.js";
|
||||||
import { resolveIMessageAccount } from "../imessage/accounts.js";
|
import { resolveIMessageAccount } from "../imessage/accounts.js";
|
||||||
import { resolveLinqAccount } from "../linq/accounts.js";
|
|
||||||
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import { normalizeAccountId } from "../routing/session-key.js";
|
import { normalizeAccountId } from "../routing/session-key.js";
|
||||||
import { resolveSignalAccount } from "../signal/accounts.js";
|
import { resolveSignalAccount } from "../signal/accounts.js";
|
||||||
@@ -29,19 +41,6 @@ import {
|
|||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
resolveWhatsAppGroupToolPolicy,
|
resolveWhatsAppGroupToolPolicy,
|
||||||
} from "./plugins/group-mentions.js";
|
} from "./plugins/group-mentions.js";
|
||||||
import type {
|
|
||||||
ChannelCapabilities,
|
|
||||||
ChannelCommandAdapter,
|
|
||||||
ChannelElevatedAdapter,
|
|
||||||
ChannelGroupAdapter,
|
|
||||||
ChannelId,
|
|
||||||
ChannelAgentPromptAdapter,
|
|
||||||
ChannelMentionAdapter,
|
|
||||||
ChannelPlugin,
|
|
||||||
ChannelThreadingContext,
|
|
||||||
ChannelThreadingAdapter,
|
|
||||||
ChannelThreadingToolContext,
|
|
||||||
} from "./plugins/types.js";
|
|
||||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId, getChatChannelMeta } from "./registry.js";
|
import { CHAT_CHANNEL_ORDER, type ChatChannelId, getChatChannelMeta } from "./registry.js";
|
||||||
|
|
||||||
export type ChannelDock = {
|
export type ChannelDock = {
|
||||||
@@ -461,23 +460,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
buildDirectOrGroupThreadToolContext({ context, hasRepliedRef }),
|
buildDirectOrGroupThreadToolContext({ context, hasRepliedRef }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
linq: {
|
|
||||||
id: "linq",
|
|
||||||
capabilities: {
|
|
||||||
chatTypes: ["direct", "group"],
|
|
||||||
reactions: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
outbound: { textChunkLimit: 4000 },
|
|
||||||
config: {
|
|
||||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
||||||
(resolveLinqAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
|
|
||||||
String(entry),
|
|
||||||
),
|
|
||||||
formatAllowFrom: ({ allowFrom }) =>
|
|
||||||
allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildDockFromPlugin(plugin: ChannelPlugin): ChannelDock {
|
function buildDockFromPlugin(plugin: ChannelPlugin): ChannelDock {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
|
||||||
import type { ChannelMeta } from "./plugins/types.js";
|
import type { ChannelMeta } from "./plugins/types.js";
|
||||||
import type { ChannelId } from "./plugins/types.js";
|
import type { ChannelId } from "./plugins/types.js";
|
||||||
|
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
|
|
||||||
// Channel docking: add new core channels here (order + meta + aliases), then
|
// Channel docking: add new core channels here (order + meta + aliases), then
|
||||||
// register the plugin in its extension entrypoint and keep protocol IDs in sync.
|
// register the plugin in its extension entrypoint and keep protocol IDs in sync.
|
||||||
@@ -13,7 +13,6 @@ export const CHAT_CHANNEL_ORDER = [
|
|||||||
"slack",
|
"slack",
|
||||||
"signal",
|
"signal",
|
||||||
"imessage",
|
"imessage",
|
||||||
"linq",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number];
|
export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number];
|
||||||
@@ -110,16 +109,6 @@ const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
|
|||||||
blurb: "this is still a work in progress.",
|
blurb: "this is still a work in progress.",
|
||||||
systemImage: "message.fill",
|
systemImage: "message.fill",
|
||||||
},
|
},
|
||||||
linq: {
|
|
||||||
id: "linq",
|
|
||||||
label: "Linq",
|
|
||||||
selectionLabel: "Linq (iMessage API)",
|
|
||||||
detailLabel: "Linq iMessage",
|
|
||||||
docsPath: "/channels/linq",
|
|
||||||
docsLabel: "linq",
|
|
||||||
blurb: "real iMessage blue bubbles via API — no Mac required. Get a token at linqapp.com.",
|
|
||||||
systemImage: "bubble.left.and.text.bubble.right",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
||||||
@@ -127,7 +116,6 @@ export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
|||||||
"internet-relay-chat": "irc",
|
"internet-relay-chat": "irc",
|
||||||
"google-chat": "googlechat",
|
"google-chat": "googlechat",
|
||||||
gchat: "googlechat",
|
gchat: "googlechat",
|
||||||
"linq-imessage": "linq",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeChannelKey = (raw?: string | null): string | undefined => {
|
const normalizeChannelKey = (raw?: string | null): string | undefined => {
|
||||||
|
|||||||
@@ -1010,65 +1010,3 @@ export const MSTeamsConfigSchema = z
|
|||||||
'channels.msteams.dmPolicy="open" requires channels.msteams.allowFrom to include "*"',
|
'channels.msteams.dmPolicy="open" requires channels.msteams.allowFrom to include "*"',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Linq ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const LinqAllowFromEntry = z.union([z.string(), z.number()]);
|
|
||||||
|
|
||||||
const LinqAccountSchemaBase = z
|
|
||||||
.object({
|
|
||||||
name: z.string().optional(),
|
|
||||||
enabled: z.boolean().optional(),
|
|
||||||
apiToken: z.string().optional().register(sensitive),
|
|
||||||
tokenFile: z.string().optional(),
|
|
||||||
fromPhone: z.string().optional(),
|
|
||||||
dmPolicy: DmPolicySchema.optional(),
|
|
||||||
allowFrom: z.array(LinqAllowFromEntry).optional(),
|
|
||||||
groupPolicy: GroupPolicySchema.optional(),
|
|
||||||
groupAllowFrom: z.array(LinqAllowFromEntry).optional(),
|
|
||||||
mediaMaxMb: z.number().positive().optional(),
|
|
||||||
textChunkLimit: z.number().positive().optional(),
|
|
||||||
webhookUrl: z.string().optional(),
|
|
||||||
webhookSecret: z.string().optional().register(sensitive),
|
|
||||||
webhookPath: z.string().optional(),
|
|
||||||
webhookHost: z.string().optional(),
|
|
||||||
historyLimit: z.number().nonnegative().optional(),
|
|
||||||
blockStreaming: z.boolean().optional(),
|
|
||||||
groups: z
|
|
||||||
.record(
|
|
||||||
z.string(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
requireMention: z.boolean().optional(),
|
|
||||||
tools: ToolPolicySchema,
|
|
||||||
toolsBySender: ToolPolicyBySenderSchema,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
responsePrefix: z.string().optional(),
|
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
const LinqAccountSchema = LinqAccountSchemaBase.superRefine((value, ctx) => {
|
|
||||||
requireOpenAllowFrom({
|
|
||||||
policy: value.dmPolicy,
|
|
||||||
allowFrom: value.allowFrom,
|
|
||||||
ctx,
|
|
||||||
path: ["allowFrom"],
|
|
||||||
message: 'channels.linq.dmPolicy="open" requires channels.linq.allowFrom to include "*"',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LinqConfigSchema = LinqAccountSchemaBase.extend({
|
|
||||||
accounts: z.record(z.string(), LinqAccountSchema.optional()).optional(),
|
|
||||||
}).superRefine((value, ctx) => {
|
|
||||||
requireOpenAllowFrom({
|
|
||||||
policy: value.dmPolicy,
|
|
||||||
allowFrom: value.allowFrom,
|
|
||||||
ctx,
|
|
||||||
path: ["allowFrom"],
|
|
||||||
message: 'channels.linq.dmPolicy="open" requires channels.linq.allowFrom to include "*"',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import { readFileSync } from "node:fs";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
|
||||||
import type { LinqAccountConfig } from "./types.js";
|
|
||||||
|
|
||||||
export type ResolvedLinqAccount = {
|
|
||||||
accountId: string;
|
|
||||||
enabled: boolean;
|
|
||||||
name?: string;
|
|
||||||
token: string;
|
|
||||||
tokenSource: "config" | "env" | "file" | "none";
|
|
||||||
fromPhone?: string;
|
|
||||||
config: LinqAccountConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
|
||||||
const accounts = (cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| LinqAccountConfig
|
|
||||||
| undefined;
|
|
||||||
if (!accounts?.accounts || typeof accounts.accounts !== "object") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return Object.keys(accounts.accounts).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listLinqAccountIds(cfg: OpenClawConfig): string[] {
|
|
||||||
const ids = listConfiguredAccountIds(cfg);
|
|
||||||
if (ids.length === 0) {
|
|
||||||
return [DEFAULT_ACCOUNT_ID];
|
|
||||||
}
|
|
||||||
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveDefaultLinqAccountId(cfg: OpenClawConfig): string {
|
|
||||||
const ids = listLinqAccountIds(cfg);
|
|
||||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
||||||
return DEFAULT_ACCOUNT_ID;
|
|
||||||
}
|
|
||||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAccountConfig(
|
|
||||||
cfg: OpenClawConfig,
|
|
||||||
accountId: string,
|
|
||||||
): LinqAccountConfig | undefined {
|
|
||||||
const linqSection = (cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| LinqAccountConfig
|
|
||||||
| undefined;
|
|
||||||
if (!linqSection?.accounts || typeof linqSection.accounts !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return linqSection.accounts[accountId];
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeLinqAccountConfig(cfg: OpenClawConfig, accountId: string): LinqAccountConfig {
|
|
||||||
const linqSection = (cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| (LinqAccountConfig & { accounts?: unknown })
|
|
||||||
| undefined;
|
|
||||||
const { accounts: _ignored, ...base } = linqSection ?? {};
|
|
||||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
||||||
return { ...base, ...account };
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveToken(
|
|
||||||
merged: LinqAccountConfig,
|
|
||||||
accountId: string,
|
|
||||||
): { token: string; source: "config" | "env" | "file" } | { token: ""; source: "none" } {
|
|
||||||
// Environment variable takes priority for the default account.
|
|
||||||
const envToken = process.env.LINQ_API_TOKEN?.trim() ?? "";
|
|
||||||
if (envToken && accountId === DEFAULT_ACCOUNT_ID) {
|
|
||||||
return { token: envToken, source: "env" };
|
|
||||||
}
|
|
||||||
// Config token.
|
|
||||||
if (merged.apiToken?.trim()) {
|
|
||||||
return { token: merged.apiToken.trim(), source: "config" };
|
|
||||||
}
|
|
||||||
// Token file (read synchronously to keep resolver sync-friendly).
|
|
||||||
if (merged.tokenFile?.trim()) {
|
|
||||||
try {
|
|
||||||
const content = readFileSync(merged.tokenFile.trim(), "utf8").trim();
|
|
||||||
if (content) {
|
|
||||||
return { token: content, source: "file" };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { token: "", source: "none" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveLinqAccount(params: {
|
|
||||||
cfg: OpenClawConfig;
|
|
||||||
accountId?: string | null;
|
|
||||||
}): ResolvedLinqAccount {
|
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
|
||||||
const linqSection = (params.cfg.channels as Record<string, unknown> | undefined)?.linq as
|
|
||||||
| LinqAccountConfig
|
|
||||||
| undefined;
|
|
||||||
const baseEnabled = linqSection?.enabled !== false;
|
|
||||||
const merged = mergeLinqAccountConfig(params.cfg, accountId);
|
|
||||||
const accountEnabled = merged.enabled !== false;
|
|
||||||
const { token, source } = resolveToken(merged, accountId);
|
|
||||||
return {
|
|
||||||
accountId,
|
|
||||||
enabled: baseEnabled && accountEnabled,
|
|
||||||
name: merged.name?.trim() || undefined,
|
|
||||||
token,
|
|
||||||
tokenSource: source,
|
|
||||||
fromPhone: merged.fromPhone?.trim() || undefined,
|
|
||||||
config: merged,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
||||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
||||||
import { resolveHumanDelayConfig } from "../agents/identity.js";
|
|
||||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
|
||||||
import { dispatchInboundMessage } from "../auto-reply/dispatch.js";
|
|
||||||
import {
|
|
||||||
formatInboundEnvelope,
|
|
||||||
formatInboundFromLabel,
|
|
||||||
resolveEnvelopeFormatOptions,
|
|
||||||
} from "../auto-reply/envelope.js";
|
|
||||||
import {
|
|
||||||
createInboundDebouncer,
|
|
||||||
resolveInboundDebounceMs,
|
|
||||||
} from "../auto-reply/inbound-debounce.js";
|
|
||||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
|
||||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
|
||||||
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
|
|
||||||
import { recordInboundSession } from "../channels/session.js";
|
|
||||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
|
||||||
import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
|
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
|
||||||
import { buildPairingReply } from "../pairing/pairing-messages.js";
|
|
||||||
import {
|
|
||||||
readChannelAllowFromStore,
|
|
||||||
upsertChannelPairingRequest,
|
|
||||||
} from "../pairing/pairing-store.js";
|
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import { truncateUtf16Safe } from "../utils.js";
|
|
||||||
import { resolveLinqAccount } from "./accounts.js";
|
|
||||||
import { markAsReadLinq, sendMessageLinq, startTypingLinq } from "./send.js";
|
|
||||||
import type {
|
|
||||||
LinqMediaPart,
|
|
||||||
LinqMessageReceivedData,
|
|
||||||
LinqTextPart,
|
|
||||||
LinqWebhookEvent,
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
export type MonitorLinqOpts = {
|
|
||||||
accountId?: string;
|
|
||||||
config?: OpenClawConfig;
|
|
||||||
runtime?: RuntimeEnv;
|
|
||||||
abortSignal?: AbortSignal;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MonitorRuntime = {
|
|
||||||
info: (msg: string) => void;
|
|
||||||
error?: (msg: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveRuntime(opts: MonitorLinqOpts): MonitorRuntime {
|
|
||||||
return {
|
|
||||||
info: (msg) => logVerbose(msg),
|
|
||||||
error: (msg) => logVerbose(msg),
|
|
||||||
...opts.runtime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAllowList(raw?: Array<string | number>): string[] {
|
|
||||||
if (!raw || !Array.isArray(raw)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return raw.map((v) => String(v).trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractTextContent(parts: Array<{ type: string; value?: string }>): string {
|
|
||||||
return parts
|
|
||||||
.filter((p): p is LinqTextPart => p.type === "text")
|
|
||||||
.map((p) => p.value)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractMediaUrls(
|
|
||||||
parts: Array<{ type: string; url?: string; mime_type?: string }>,
|
|
||||||
): Array<{ url: string; mimeType: string }> {
|
|
||||||
return parts
|
|
||||||
.filter(
|
|
||||||
(p): p is LinqMediaPart & { url: string; mime_type: string } =>
|
|
||||||
p.type === "media" && Boolean(p.url) && Boolean(p.mime_type),
|
|
||||||
)
|
|
||||||
.map((p) => ({ url: p.url, mimeType: p.mime_type }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyWebhookSignature(
|
|
||||||
secret: string,
|
|
||||||
payload: string,
|
|
||||||
timestamp: string,
|
|
||||||
signature: string,
|
|
||||||
): boolean {
|
|
||||||
const message = `${timestamp}.${payload}`;
|
|
||||||
const expected = createHmac("sha256", secret).update(message).digest("hex");
|
|
||||||
try {
|
|
||||||
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAllowedLinqSender(allowFrom: string[], sender: string): boolean {
|
|
||||||
if (allowFrom.includes("*")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const normalized = sender.replace(/[\s()-]/g, "").toLowerCase();
|
|
||||||
return allowFrom.some((entry) => {
|
|
||||||
const norm = entry.replace(/[\s()-]/g, "").toLowerCase();
|
|
||||||
return norm === normalized;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function monitorLinqProvider(opts: MonitorLinqOpts = {}): Promise<void> {
|
|
||||||
const runtime = resolveRuntime(opts);
|
|
||||||
const cfg = opts.config ?? loadConfig();
|
|
||||||
const accountInfo = resolveLinqAccount({ cfg, accountId: opts.accountId });
|
|
||||||
const linqCfg = accountInfo.config;
|
|
||||||
const token = accountInfo.token;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("Linq API token not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowFrom = normalizeAllowList(linqCfg.allowFrom);
|
|
||||||
const dmPolicy = linqCfg.dmPolicy ?? "pairing";
|
|
||||||
const webhookSecret = linqCfg.webhookSecret?.trim() ?? "";
|
|
||||||
const webhookPath = linqCfg.webhookPath?.trim() || "/linq-webhook";
|
|
||||||
const webhookHost = linqCfg.webhookHost?.trim() || "0.0.0.0";
|
|
||||||
const fromPhone = accountInfo.fromPhone;
|
|
||||||
|
|
||||||
const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "linq" });
|
|
||||||
const inboundDebouncer = createInboundDebouncer<{ event: LinqMessageReceivedData }>({
|
|
||||||
debounceMs: inboundDebounceMs,
|
|
||||||
buildKey: (entry) => {
|
|
||||||
const sender = entry.event.from?.trim();
|
|
||||||
if (!sender) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `linq:${accountInfo.accountId}:${entry.event.chat_id}:${sender}`;
|
|
||||||
},
|
|
||||||
shouldDebounce: (entry) => {
|
|
||||||
const text = extractTextContent(
|
|
||||||
entry.event.message.parts as Array<{ type: string; value?: string }>,
|
|
||||||
);
|
|
||||||
if (!text.trim()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !hasControlCommand(text, cfg);
|
|
||||||
},
|
|
||||||
onFlush: async (entries) => {
|
|
||||||
const last = entries.at(-1);
|
|
||||||
if (!last) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (entries.length === 1) {
|
|
||||||
await handleMessage(last.event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const combinedText = entries
|
|
||||||
.map((e) =>
|
|
||||||
extractTextContent(e.event.message.parts as Array<{ type: string; value?: string }>),
|
|
||||||
)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n");
|
|
||||||
const syntheticEvent: LinqMessageReceivedData = {
|
|
||||||
...last.event,
|
|
||||||
message: {
|
|
||||||
...last.event.message,
|
|
||||||
parts: [{ type: "text" as const, value: combinedText }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await handleMessage(syntheticEvent);
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
runtime.error?.(`linq debounce flush failed: ${String(err)}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleMessage(data: LinqMessageReceivedData) {
|
|
||||||
const sender = data.from?.trim();
|
|
||||||
if (!sender) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.is_from_me) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter: only process messages sent to this account's phone number.
|
|
||||||
if (fromPhone && data.recipient_phone !== fromPhone) {
|
|
||||||
logVerbose(`linq: skipping message to ${data.recipient_phone} (not ${fromPhone})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = data.chat_id;
|
|
||||||
const text = extractTextContent(data.message.parts as Array<{ type: string; value?: string }>);
|
|
||||||
const media = extractMediaUrls(
|
|
||||||
data.message.parts as Array<{ type: string; url?: string; mime_type?: string }>,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!text.trim() && media.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send read receipt and typing indicator immediately (fire-and-forget).
|
|
||||||
markAsReadLinq(chatId, token).catch(() => {});
|
|
||||||
startTypingLinq(chatId, token).catch(() => {});
|
|
||||||
|
|
||||||
const storeAllowFrom = await readChannelAllowFromStore("linq").catch(() => []);
|
|
||||||
const effectiveDmAllowFrom = Array.from(new Set([...allowFrom, ...storeAllowFrom]))
|
|
||||||
.map((v) => String(v).trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const dmHasWildcard = effectiveDmAllowFrom.includes("*");
|
|
||||||
const dmAuthorized =
|
|
||||||
dmPolicy === "open"
|
|
||||||
? true
|
|
||||||
: dmHasWildcard ||
|
|
||||||
(effectiveDmAllowFrom.length > 0 && isAllowedLinqSender(effectiveDmAllowFrom, sender));
|
|
||||||
|
|
||||||
if (dmPolicy === "disabled") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dmAuthorized) {
|
|
||||||
if (dmPolicy === "pairing") {
|
|
||||||
const { code, created } = await upsertChannelPairingRequest({
|
|
||||||
channel: "linq",
|
|
||||||
id: sender,
|
|
||||||
meta: { sender, chatId },
|
|
||||||
});
|
|
||||||
if (created) {
|
|
||||||
logVerbose(`linq pairing request sender=${sender}`);
|
|
||||||
try {
|
|
||||||
await sendMessageLinq(
|
|
||||||
chatId,
|
|
||||||
buildPairingReply({
|
|
||||||
channel: "linq",
|
|
||||||
idLine: `Your phone number: ${sender}`,
|
|
||||||
code,
|
|
||||||
}),
|
|
||||||
{ token, accountId: accountInfo.accountId },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`linq pairing reply failed for ${sender}: ${String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logVerbose(`Blocked linq sender ${sender} (dmPolicy=${dmPolicy})`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = resolveAgentRoute({
|
|
||||||
cfg,
|
|
||||||
channel: "linq",
|
|
||||||
accountId: accountInfo.accountId,
|
|
||||||
peer: { kind: "direct", id: sender },
|
|
||||||
});
|
|
||||||
const bodyText = text.trim() || (media.length > 0 ? "<media:image>" : "");
|
|
||||||
if (!bodyText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const replyContext = data.message.reply_to ? { id: data.message.reply_to.message_id } : null;
|
|
||||||
const createdAt = data.received_at ? Date.parse(data.received_at) : undefined;
|
|
||||||
|
|
||||||
const fromLabel = formatInboundFromLabel({
|
|
||||||
isGroup: false,
|
|
||||||
directLabel: sender,
|
|
||||||
directId: sender,
|
|
||||||
});
|
|
||||||
const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
|
|
||||||
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
|
||||||
const previousTimestamp = readSessionUpdatedAt({
|
|
||||||
storePath,
|
|
||||||
sessionKey: route.sessionKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const replySuffix = replyContext?.id ? `\n\n[Replying to message ${replyContext.id}]` : "";
|
|
||||||
const body = formatInboundEnvelope({
|
|
||||||
channel: "Linq iMessage",
|
|
||||||
from: fromLabel,
|
|
||||||
timestamp: createdAt,
|
|
||||||
body: `${bodyText}${replySuffix}`,
|
|
||||||
chatType: "direct",
|
|
||||||
sender: { name: sender, id: sender },
|
|
||||||
previousTimestamp,
|
|
||||||
envelope: envelopeOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const linqTo = chatId;
|
|
||||||
const ctxPayload = finalizeInboundContext({
|
|
||||||
Body: body,
|
|
||||||
BodyForAgent: bodyText,
|
|
||||||
RawBody: bodyText,
|
|
||||||
CommandBody: bodyText,
|
|
||||||
From: `linq:${sender}`,
|
|
||||||
To: linqTo,
|
|
||||||
SessionKey: route.sessionKey,
|
|
||||||
AccountId: route.accountId,
|
|
||||||
ChatType: "direct",
|
|
||||||
ConversationLabel: fromLabel,
|
|
||||||
SenderName: sender,
|
|
||||||
SenderId: sender,
|
|
||||||
Provider: "linq",
|
|
||||||
Surface: "linq",
|
|
||||||
MessageSid: data.message.id,
|
|
||||||
ReplyToId: replyContext?.id,
|
|
||||||
Timestamp: createdAt,
|
|
||||||
MediaUrl: media[0]?.url,
|
|
||||||
MediaType: media[0]?.mimeType,
|
|
||||||
MediaUrls: media.length > 0 ? media.map((m) => m.url) : undefined,
|
|
||||||
MediaTypes: media.length > 0 ? media.map((m) => m.mimeType) : undefined,
|
|
||||||
WasMentioned: true,
|
|
||||||
CommandAuthorized: dmAuthorized,
|
|
||||||
OriginatingChannel: "linq" as const,
|
|
||||||
OriginatingTo: linqTo,
|
|
||||||
});
|
|
||||||
|
|
||||||
await recordInboundSession({
|
|
||||||
storePath,
|
|
||||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
||||||
ctx: ctxPayload,
|
|
||||||
updateLastRoute: {
|
|
||||||
sessionKey: route.mainSessionKey,
|
|
||||||
channel: "linq",
|
|
||||||
to: linqTo,
|
|
||||||
accountId: route.accountId,
|
|
||||||
},
|
|
||||||
onRecordError: (err) => {
|
|
||||||
logVerbose(`linq: failed updating session meta: ${String(err)}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldLogVerbose()) {
|
|
||||||
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
|
|
||||||
logVerbose(
|
|
||||||
`linq inbound: chatId=${chatId} from=${sender} len=${body.length} preview="${preview}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
||||||
cfg,
|
|
||||||
agentId: route.agentId,
|
|
||||||
channel: "linq",
|
|
||||||
accountId: route.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dispatcher = createReplyDispatcher({
|
|
||||||
...prefixOptions,
|
|
||||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
|
||||||
deliver: async (payload) => {
|
|
||||||
const replyText = typeof payload === "string" ? payload : (payload.text ?? "");
|
|
||||||
if (replyText) {
|
|
||||||
await sendMessageLinq(chatId, replyText, {
|
|
||||||
token,
|
|
||||||
accountId: accountInfo.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (err, info) => {
|
|
||||||
runtime.error?.(danger(`linq ${info.kind} reply failed: ${String(err)}`));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await dispatchInboundMessage({
|
|
||||||
ctx: ctxPayload,
|
|
||||||
cfg,
|
|
||||||
dispatcher,
|
|
||||||
replyOptions: {
|
|
||||||
disableBlockStreaming:
|
|
||||||
typeof linqCfg.blockStreaming === "boolean" ? !linqCfg.blockStreaming : undefined,
|
|
||||||
onModelSelected,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- HTTP webhook server ---
|
|
||||||
const port = linqCfg.webhookUrl ? new URL(linqCfg.webhookUrl).port || "0" : "0";
|
|
||||||
|
|
||||||
const server: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
||||||
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
||||||
if (req.method !== "POST" || !url.pathname.startsWith(webhookPath)) {
|
|
||||||
res.writeHead(404);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
let size = 0;
|
|
||||||
const maxPayloadBytes = 1024 * 1024; // 1MB limit
|
|
||||||
for await (const chunk of req) {
|
|
||||||
size += (chunk as Buffer).length;
|
|
||||||
if (size > maxPayloadBytes) {
|
|
||||||
res.writeHead(413);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chunks.push(chunk as Buffer);
|
|
||||||
}
|
|
||||||
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
||||||
|
|
||||||
// Verify webhook signature if a secret is configured.
|
|
||||||
if (webhookSecret) {
|
|
||||||
const timestamp = req.headers["x-webhook-timestamp"] as string | undefined;
|
|
||||||
const signature = req.headers["x-webhook-signature"] as string | undefined;
|
|
||||||
if (
|
|
||||||
!timestamp ||
|
|
||||||
!signature ||
|
|
||||||
!verifyWebhookSignature(webhookSecret, rawBody, timestamp, signature)
|
|
||||||
) {
|
|
||||||
res.writeHead(401);
|
|
||||||
res.end("invalid signature");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Reject stale webhooks (>5 minutes).
|
|
||||||
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
|
|
||||||
if (age > 300) {
|
|
||||||
res.writeHead(401);
|
|
||||||
res.end("stale timestamp");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acknowledge immediately.
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ received: true }));
|
|
||||||
|
|
||||||
// Parse and dispatch.
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(rawBody) as LinqWebhookEvent;
|
|
||||||
if (event.event_type === "message.received") {
|
|
||||||
const data = event.data as LinqMessageReceivedData;
|
|
||||||
await inboundDebouncer.enqueue({ event: data });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
runtime.error?.(`linq webhook parse error: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const listenPort = Number(port) || 0;
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
server.listen(listenPort, webhookHost, () => {
|
|
||||||
const addr = server.address();
|
|
||||||
const boundPort = typeof addr === "object" ? addr?.port : listenPort;
|
|
||||||
runtime.info(`linq: webhook listener started on ${webhookHost}:${boundPort}${webhookPath}`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
server.on("error", reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle shutdown.
|
|
||||||
const abort = opts.abortSignal;
|
|
||||||
if (abort) {
|
|
||||||
const onAbort = () => {
|
|
||||||
server.close();
|
|
||||||
};
|
|
||||||
abort.addEventListener("abort", onAbort, { once: true });
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.on("close", resolve);
|
|
||||||
if (abort.aborted) {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
abort.removeEventListener("abort", onAbort);
|
|
||||||
} else {
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.on("close", resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { loadConfig } from "../config/config.js";
|
|
||||||
import { resolveLinqAccount } from "./accounts.js";
|
|
||||||
import type { LinqProbe } from "./types.js";
|
|
||||||
|
|
||||||
const LINQ_API_BASE = "https://api.linqapp.com/api/partner/v3";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Probe Linq API availability by listing phone numbers.
|
|
||||||
*
|
|
||||||
* @param token - Linq API token (if not provided, resolved from config).
|
|
||||||
* @param timeoutMs - Request timeout in milliseconds.
|
|
||||||
*/
|
|
||||||
export async function probeLinq(
|
|
||||||
token?: string,
|
|
||||||
timeoutMs?: number,
|
|
||||||
accountId?: string,
|
|
||||||
): Promise<LinqProbe> {
|
|
||||||
let resolvedToken = token?.trim() ?? "";
|
|
||||||
if (!resolvedToken) {
|
|
||||||
const cfg = loadConfig();
|
|
||||||
const account = resolveLinqAccount({ cfg, accountId });
|
|
||||||
resolvedToken = account.token;
|
|
||||||
}
|
|
||||||
if (!resolvedToken) {
|
|
||||||
return { ok: false, error: "Linq API token not configured" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${LINQ_API_BASE}/phonenumbers`;
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = timeoutMs && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bearer ${resolvedToken}`, "User-Agent": "OpenClaw/1.0" },
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => "");
|
|
||||||
return { ok: false, error: `Linq API ${response.status}: ${text.slice(0, 200)}` };
|
|
||||||
}
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
phone_numbers?: Array<{ phone_number?: string }>;
|
|
||||||
};
|
|
||||||
const phoneNumbers = (data.phone_numbers ?? [])
|
|
||||||
.map((p) => p.phone_number)
|
|
||||||
.filter(Boolean) as string[];
|
|
||||||
return { ok: true, phoneNumbers };
|
|
||||||
} catch (err) {
|
|
||||||
if (controller.signal.aborted) {
|
|
||||||
return { ok: false, error: `Linq probe timed out (${timeoutMs}ms)` };
|
|
||||||
}
|
|
||||||
return { ok: false, error: String(err) };
|
|
||||||
} finally {
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
123
src/linq/send.ts
123
src/linq/send.ts
@@ -1,123 +0,0 @@
|
|||||||
import { loadConfig } from "../config/config.js";
|
|
||||||
import { resolveLinqAccount, type ResolvedLinqAccount } from "./accounts.js";
|
|
||||||
import type { LinqSendResult } from "./types.js";
|
|
||||||
|
|
||||||
const LINQ_API_BASE = "https://api.linqapp.com/api/partner/v3";
|
|
||||||
const UA = "OpenClaw/1.0";
|
|
||||||
|
|
||||||
export type LinqSendOpts = {
|
|
||||||
accountId?: string;
|
|
||||||
mediaUrl?: string;
|
|
||||||
replyToMessageId?: string;
|
|
||||||
verbose?: boolean;
|
|
||||||
token?: string;
|
|
||||||
config?: ReturnType<typeof loadConfig>;
|
|
||||||
account?: ResolvedLinqAccount;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message via Linq Blue V3 API.
|
|
||||||
*
|
|
||||||
* @param to - Chat ID (Linq chat_id) to send to.
|
|
||||||
* @param text - Message text.
|
|
||||||
* @param opts - Optional send options.
|
|
||||||
*/
|
|
||||||
export async function sendMessageLinq(
|
|
||||||
to: string,
|
|
||||||
text: string,
|
|
||||||
opts: LinqSendOpts = {},
|
|
||||||
): Promise<LinqSendResult> {
|
|
||||||
const cfg = opts.config ?? loadConfig();
|
|
||||||
const account = opts.account ?? resolveLinqAccount({ cfg, accountId: opts.accountId });
|
|
||||||
const token = opts.token?.trim() || account.token;
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("Linq API token not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: Array<Record<string, unknown>> = [];
|
|
||||||
if (text) {
|
|
||||||
parts.push({ type: "text", value: text });
|
|
||||||
}
|
|
||||||
if (opts.mediaUrl?.trim()) {
|
|
||||||
parts.push({ type: "media", url: opts.mediaUrl.trim() });
|
|
||||||
}
|
|
||||||
if (parts.length === 0) {
|
|
||||||
throw new Error("Linq send requires text or media");
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: Record<string, unknown> = { parts };
|
|
||||||
if (opts.replyToMessageId?.trim()) {
|
|
||||||
message.reply_to = { message_id: opts.replyToMessageId.trim() };
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(to)}/messages`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": UA,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text().catch(() => "");
|
|
||||||
throw new Error(`Linq API error: ${response.status} ${errorText.slice(0, 200)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
chat_id?: string;
|
|
||||||
message?: { id?: string };
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
messageId: data.message?.id ?? "unknown",
|
|
||||||
chatId: data.chat_id ?? to,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send a typing indicator. */
|
|
||||||
export async function startTypingLinq(chatId: string, token: string): Promise<void> {
|
|
||||||
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/typing`;
|
|
||||||
await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Clear a typing indicator. */
|
|
||||||
export async function stopTypingLinq(chatId: string, token: string): Promise<void> {
|
|
||||||
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/typing`;
|
|
||||||
await fetch(url, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mark a chat as read. */
|
|
||||||
export async function markAsReadLinq(chatId: string, token: string): Promise<void> {
|
|
||||||
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/read`;
|
|
||||||
await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send a reaction to a message. */
|
|
||||||
export async function sendReactionLinq(
|
|
||||||
messageId: string,
|
|
||||||
type: "love" | "like" | "dislike" | "laugh" | "emphasize" | "question",
|
|
||||||
token: string,
|
|
||||||
operation: "add" | "remove" = "add",
|
|
||||||
): Promise<void> {
|
|
||||||
const url = `${LINQ_API_BASE}/messages/${encodeURIComponent(messageId)}/reactions`;
|
|
||||||
await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": UA,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ operation, type }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/** Linq Blue V3 webhook event envelope. */
|
|
||||||
export type LinqWebhookEvent = {
|
|
||||||
api_version: "v3";
|
|
||||||
event_id: string;
|
|
||||||
created_at: string;
|
|
||||||
trace_id: string;
|
|
||||||
partner_id: string;
|
|
||||||
event_type: string;
|
|
||||||
data: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinqMessageReceivedData = {
|
|
||||||
chat_id: string;
|
|
||||||
from: string;
|
|
||||||
recipient_phone: string;
|
|
||||||
received_at: string;
|
|
||||||
is_from_me: boolean;
|
|
||||||
service: "iMessage" | "SMS" | "RCS";
|
|
||||||
message: LinqIncomingMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinqIncomingMessage = {
|
|
||||||
id: string;
|
|
||||||
parts: LinqMessagePart[];
|
|
||||||
effect?: { type: "screen" | "bubble"; name: string };
|
|
||||||
reply_to?: { message_id: string; part_index?: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinqTextPart = { type: "text"; value: string };
|
|
||||||
export type LinqMediaPart = {
|
|
||||||
type: "media";
|
|
||||||
url?: string;
|
|
||||||
attachment_id?: string;
|
|
||||||
filename?: string;
|
|
||||||
mime_type?: string;
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
export type LinqMessagePart = LinqTextPart | LinqMediaPart;
|
|
||||||
|
|
||||||
export type LinqSendResult = {
|
|
||||||
messageId: string;
|
|
||||||
chatId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinqProbe = {
|
|
||||||
ok: boolean;
|
|
||||||
error?: string | null;
|
|
||||||
phoneNumbers?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Per-account config for the Linq channel (mirrors the Zod schema shape). */
|
|
||||||
export type LinqAccountConfig = {
|
|
||||||
name?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
/** Linq API bearer token. */
|
|
||||||
apiToken?: string;
|
|
||||||
/** Read token from file instead of config (mutual exclusive with apiToken). */
|
|
||||||
tokenFile?: string;
|
|
||||||
/** Phone number this account sends from (E.164). */
|
|
||||||
fromPhone?: string;
|
|
||||||
/** DM security policy. */
|
|
||||||
dmPolicy?: "pairing" | "open" | "disabled";
|
|
||||||
/** Allowed sender IDs (phone numbers or "*"). */
|
|
||||||
allowFrom?: Array<string | number>;
|
|
||||||
/** Group chat security policy. */
|
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
||||||
/** Allowed group sender IDs. */
|
|
||||||
groupAllowFrom?: Array<string | number>;
|
|
||||||
/** Max media size in MB (default: 10). */
|
|
||||||
mediaMaxMb?: number;
|
|
||||||
/** Max text chunk length (default: 4000). */
|
|
||||||
textChunkLimit?: number;
|
|
||||||
/** Webhook URL for inbound messages from Linq. */
|
|
||||||
webhookUrl?: string;
|
|
||||||
/** Webhook HMAC signing secret. */
|
|
||||||
webhookSecret?: string;
|
|
||||||
/** Local HTTP path prefix for the webhook listener (default: /linq-webhook). */
|
|
||||||
webhookPath?: string;
|
|
||||||
/** Local HTTP host to bind the webhook listener on. */
|
|
||||||
webhookHost?: string;
|
|
||||||
/** History limit for group chats. */
|
|
||||||
historyLimit?: number;
|
|
||||||
/** Block streaming responses. */
|
|
||||||
blockStreaming?: boolean;
|
|
||||||
/** Group configs keyed by chat_id. */
|
|
||||||
groups?: Record<string, unknown>;
|
|
||||||
/** Per-account sub-accounts. */
|
|
||||||
accounts?: Record<string, LinqAccountConfig>;
|
|
||||||
};
|
|
||||||
@@ -123,7 +123,6 @@ export {
|
|||||||
DiscordConfigSchema,
|
DiscordConfigSchema,
|
||||||
GoogleChatConfigSchema,
|
GoogleChatConfigSchema,
|
||||||
IMessageConfigSchema,
|
IMessageConfigSchema,
|
||||||
LinqConfigSchema,
|
|
||||||
MSTeamsConfigSchema,
|
MSTeamsConfigSchema,
|
||||||
SignalConfigSchema,
|
SignalConfigSchema,
|
||||||
SlackConfigSchema,
|
SlackConfigSchema,
|
||||||
@@ -456,14 +455,5 @@ export {
|
|||||||
} from "../line/markdown-to-line.js";
|
} from "../line/markdown-to-line.js";
|
||||||
export type { ProcessedLineMessage } from "../line/markdown-to-line.js";
|
export type { ProcessedLineMessage } from "../line/markdown-to-line.js";
|
||||||
|
|
||||||
// Channel: Linq
|
|
||||||
export {
|
|
||||||
listLinqAccountIds,
|
|
||||||
resolveDefaultLinqAccountId,
|
|
||||||
resolveLinqAccount,
|
|
||||||
type ResolvedLinqAccount,
|
|
||||||
} from "../linq/accounts.js";
|
|
||||||
export type { LinqProbe } from "../linq/types.js";
|
|
||||||
|
|
||||||
// Media utilities
|
// Media utilities
|
||||||
export { loadWebMedia, type WebMediaResult } from "../web/media.js";
|
export { loadWebMedia, type WebMediaResult } from "../web/media.js";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
import type { PluginRuntime } from "./types.js";
|
||||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
||||||
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
|
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
|
||||||
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
||||||
@@ -91,14 +92,6 @@ import {
|
|||||||
sendMessageLine,
|
sendMessageLine,
|
||||||
} from "../../line/send.js";
|
} from "../../line/send.js";
|
||||||
import { buildTemplateMessageFromPayload } from "../../line/template-messages.js";
|
import { buildTemplateMessageFromPayload } from "../../line/template-messages.js";
|
||||||
import {
|
|
||||||
listLinqAccountIds,
|
|
||||||
resolveDefaultLinqAccountId,
|
|
||||||
resolveLinqAccount,
|
|
||||||
} from "../../linq/accounts.js";
|
|
||||||
import { monitorLinqProvider } from "../../linq/monitor.js";
|
|
||||||
import { probeLinq } from "../../linq/probe.js";
|
|
||||||
import { sendMessageLinq } from "../../linq/send.js";
|
|
||||||
import { getChildLogger } from "../../logging.js";
|
import { getChildLogger } from "../../logging.js";
|
||||||
import { normalizeLogLevel } from "../../logging/levels.js";
|
import { normalizeLogLevel } from "../../logging/levels.js";
|
||||||
import { convertMarkdownTables } from "../../markdown/tables.js";
|
import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||||
@@ -146,7 +139,6 @@ import {
|
|||||||
} from "../../web/auth-store.js";
|
} from "../../web/auth-store.js";
|
||||||
import { loadWebMedia } from "../../web/media.js";
|
import { loadWebMedia } from "../../web/media.js";
|
||||||
import { formatNativeDependencyHint } from "./native-deps.js";
|
import { formatNativeDependencyHint } from "./native-deps.js";
|
||||||
import type { PluginRuntime } from "./types.js";
|
|
||||||
|
|
||||||
let cachedVersion: string | null = null;
|
let cachedVersion: string | null = null;
|
||||||
|
|
||||||
@@ -386,14 +378,6 @@ export function createPluginRuntime(): PluginRuntime {
|
|||||||
probeIMessage,
|
probeIMessage,
|
||||||
sendMessageIMessage,
|
sendMessageIMessage,
|
||||||
},
|
},
|
||||||
linq: {
|
|
||||||
sendMessageLinq,
|
|
||||||
probeLinq,
|
|
||||||
monitorLinqProvider,
|
|
||||||
listLinqAccountIds,
|
|
||||||
resolveDefaultLinqAccountId,
|
|
||||||
resolveLinqAccount,
|
|
||||||
},
|
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
getActiveWebListener,
|
getActiveWebListener,
|
||||||
getWebAuthAgeMs,
|
getWebAuthAgeMs,
|
||||||
|
|||||||
@@ -132,15 +132,6 @@ type SignalMessageActions =
|
|||||||
type MonitorIMessageProvider = typeof import("../../imessage/monitor.js").monitorIMessageProvider;
|
type MonitorIMessageProvider = typeof import("../../imessage/monitor.js").monitorIMessageProvider;
|
||||||
type ProbeIMessage = typeof import("../../imessage/probe.js").probeIMessage;
|
type ProbeIMessage = typeof import("../../imessage/probe.js").probeIMessage;
|
||||||
type SendMessageIMessage = typeof import("../../imessage/send.js").sendMessageIMessage;
|
type SendMessageIMessage = typeof import("../../imessage/send.js").sendMessageIMessage;
|
||||||
|
|
||||||
// Linq channel types
|
|
||||||
type SendMessageLinq = typeof import("../../linq/send.js").sendMessageLinq;
|
|
||||||
type ProbeLinq = typeof import("../../linq/probe.js").probeLinq;
|
|
||||||
type MonitorLinqProvider = typeof import("../../linq/monitor.js").monitorLinqProvider;
|
|
||||||
type ListLinqAccountIds = typeof import("../../linq/accounts.js").listLinqAccountIds;
|
|
||||||
type ResolveDefaultLinqAccountId =
|
|
||||||
typeof import("../../linq/accounts.js").resolveDefaultLinqAccountId;
|
|
||||||
type ResolveLinqAccount = typeof import("../../linq/accounts.js").resolveLinqAccount;
|
|
||||||
type GetActiveWebListener = typeof import("../../web/active-listener.js").getActiveWebListener;
|
type GetActiveWebListener = typeof import("../../web/active-listener.js").getActiveWebListener;
|
||||||
type GetWebAuthAgeMs = typeof import("../../web/auth-store.js").getWebAuthAgeMs;
|
type GetWebAuthAgeMs = typeof import("../../web/auth-store.js").getWebAuthAgeMs;
|
||||||
type LogoutWeb = typeof import("../../web/auth-store.js").logoutWeb;
|
type LogoutWeb = typeof import("../../web/auth-store.js").logoutWeb;
|
||||||
@@ -326,14 +317,6 @@ export type PluginRuntime = {
|
|||||||
probeIMessage: ProbeIMessage;
|
probeIMessage: ProbeIMessage;
|
||||||
sendMessageIMessage: SendMessageIMessage;
|
sendMessageIMessage: SendMessageIMessage;
|
||||||
};
|
};
|
||||||
linq: {
|
|
||||||
sendMessageLinq: SendMessageLinq;
|
|
||||||
probeLinq: ProbeLinq;
|
|
||||||
monitorLinqProvider: MonitorLinqProvider;
|
|
||||||
listLinqAccountIds: ListLinqAccountIds;
|
|
||||||
resolveDefaultLinqAccountId: ResolveDefaultLinqAccountId;
|
|
||||||
resolveLinqAccount: ResolveLinqAccount;
|
|
||||||
};
|
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
getActiveWebListener: GetActiveWebListener;
|
getActiveWebListener: GetActiveWebListener;
|
||||||
getWebAuthAgeMs: GetWebAuthAgeMs;
|
getWebAuthAgeMs: GetWebAuthAgeMs;
|
||||||
|
|||||||
Reference in New Issue
Block a user