feat(channels): add dingtalk support
This commit is contained in:
parent
fcc53bcf1b
commit
80c54ba057
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@ -3,6 +3,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/bluebubbles/**"
|
||||
- "docs/channels/bluebubbles.md"
|
||||
"channel: dingtalk":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/dingtalk/**"
|
||||
- "docs/channels/dingtalk.md"
|
||||
"channel: discord":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
61
docs/channels/dingtalk.md
Normal file
61
docs/channels/dingtalk.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
summary: "DingTalk bot support status, capabilities, and configuration"
|
||||
read_when:
|
||||
- Working on DingTalk channel features
|
||||
---
|
||||
# DingTalk
|
||||
|
||||
Status: beta; inbound via WebSocket using `dingtalk-stream` SDK.
|
||||
|
||||
## Quick setup
|
||||
1) Create an enterprise internal robot in DingTalk Open Platform.
|
||||
2) Enable "Stream Mode" in robot settings.
|
||||
3) Configure `channels.dingtalk.clientId` and `channels.dingtalk.clientSecret`.
|
||||
4) Start the gateway.
|
||||
|
||||
Minimal config:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
dingtalk: {
|
||||
enabled: true,
|
||||
clientId: "YOUR_APP_KEY",
|
||||
clientSecret: "YOUR_APP_SECRET"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account example:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
dingtalk: {
|
||||
accounts: {
|
||||
work: { clientId: "APP_KEY", clientSecret: "APP_SECRET" },
|
||||
personal: { clientId: "APP_KEY", clientSecret: "APP_SECRET" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
- Messages are received over the DingTalk WebSocket stream (no public URL needed).
|
||||
- Replies are sent back via the session webhook provided in each message.
|
||||
- DMs use pairing by default (`channels.dingtalk.dm.policy`).
|
||||
- Group chats can be restricted with `channels.dingtalk.groupPolicy` and `channels.dingtalk.groups`.
|
||||
|
||||
## Target formats
|
||||
- `dingtalk:<conversation_id>` for group chats.
|
||||
- `dingtalk:<user_id>` for direct messages.
|
||||
|
||||
## Configuration reference (DingTalk)
|
||||
- `channels.dingtalk.clientId`: DingTalk AppKey (Client ID).
|
||||
- `channels.dingtalk.clientSecret`: DingTalk AppSecret (Client Secret).
|
||||
- `channels.dingtalk.dm.policy`: DM policy (`pairing`, `allowlist`, `open`, `disabled`).
|
||||
- `channels.dingtalk.dm.allowFrom`: allowlist for DMs when policy is `allowlist` or `open`.
|
||||
- `channels.dingtalk.groupPolicy`: `open`, `allowlist`, or `disabled`.
|
||||
- `channels.dingtalk.groups`: per-chat overrides keyed by `conversation_id` (supports `requireMention`, `tools`, `users`).
|
||||
- `channels.dingtalk.requireMention`: whether the bot must be @mentioned in groups (default: `true`).
|
||||
- `channels.dingtalk.textChunkLimit`: max characters per message chunk (default: `4000`).
|
||||
18
extensions/dingtalk/index.ts
Normal file
18
extensions/dingtalk/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { MoltbotPluginApi } from "clawdbot/plugin-sdk";
|
||||
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { dingtalkDock, dingtalkPlugin } from "./src/channel.js";
|
||||
import { setDingTalkRuntime } from "./src/runtime.js";
|
||||
|
||||
const plugin = {
|
||||
id: "dingtalk",
|
||||
name: "DingTalk",
|
||||
description: "Moltbot DingTalk channel plugin (Stream mode)",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: MoltbotPluginApi) {
|
||||
setDingTalkRuntime(api.runtime);
|
||||
api.registerChannel({ plugin: dingtalkPlugin, dock: dingtalkDock });
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
11
extensions/dingtalk/moltbot.plugin.json
Normal file
11
extensions/dingtalk/moltbot.plugin.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "dingtalk",
|
||||
"channels": [
|
||||
"dingtalk"
|
||||
],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
39
extensions/dingtalk/package.json
Normal file
39
extensions/dingtalk/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@moltbot/dingtalk",
|
||||
"version": "2026.1.27-beta.1",
|
||||
"type": "module",
|
||||
"description": "Moltbot DingTalk channel plugin",
|
||||
"moltbot": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"channel": {
|
||||
"id": "dingtalk",
|
||||
"label": "DingTalk",
|
||||
"selectionLabel": "DingTalk (钉钉)",
|
||||
"detailLabel": "DingTalk",
|
||||
"docsPath": "/channels/dingtalk",
|
||||
"docsLabel": "dingtalk",
|
||||
"blurb": "DingTalk enterprise robot via Stream mode.",
|
||||
"aliases": [
|
||||
"dingding",
|
||||
"ding"
|
||||
],
|
||||
"order": 56
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@moltbot/dingtalk",
|
||||
"localPath": "extensions/dingtalk",
|
||||
"defaultChoice": "npm"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"dingtalk-stream": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"moltbot": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"moltbot": ">=2026.1.26"
|
||||
}
|
||||
}
|
||||
125
extensions/dingtalk/src/accounts.ts
Normal file
125
extensions/dingtalk/src/accounts.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import type { DingTalkAccountConfig, MoltbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
||||
|
||||
import type { DingTalkConfig } from "./types.js";
|
||||
|
||||
export type DingTalkCredentialSource = "config" | "env" | "none";
|
||||
|
||||
export type ResolvedDingTalkAccount = {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
enabled: boolean;
|
||||
config: DingTalkAccountConfig;
|
||||
credentialSource: DingTalkCredentialSource;
|
||||
/** Stream mode: Client ID (AppKey) */
|
||||
clientId?: string;
|
||||
/** Stream mode: Client Secret (AppSecret) */
|
||||
clientSecret?: string;
|
||||
};
|
||||
|
||||
// Stream mode environment variables
|
||||
const ENV_CLIENT_ID = "DINGTALK_CLIENT_ID";
|
||||
const ENV_CLIENT_SECRET = "DINGTALK_CLIENT_SECRET";
|
||||
|
||||
function listConfiguredAccountIds(cfg: MoltbotConfig): string[] {
|
||||
const accounts = (cfg.channels?.dingtalk as DingTalkConfig | undefined)?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return [];
|
||||
return Object.keys(accounts).filter(Boolean);
|
||||
}
|
||||
|
||||
export function listDingTalkAccountIds(cfg: MoltbotConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||
return ids.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveDefaultDingTalkAccountId(cfg: MoltbotConfig): string {
|
||||
const channel = cfg.channels?.dingtalk as DingTalkConfig | undefined;
|
||||
if (channel?.defaultAccount?.trim()) return channel.defaultAccount.trim();
|
||||
const ids = listDingTalkAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function resolveAccountConfig(
|
||||
cfg: MoltbotConfig,
|
||||
accountId: string,
|
||||
): DingTalkAccountConfig | undefined {
|
||||
const accounts = (cfg.channels?.dingtalk as DingTalkConfig | undefined)?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return undefined;
|
||||
return accounts[accountId] as DingTalkAccountConfig | undefined;
|
||||
}
|
||||
|
||||
function mergeDingTalkAccountConfig(
|
||||
cfg: MoltbotConfig,
|
||||
accountId: string,
|
||||
): DingTalkAccountConfig {
|
||||
const raw = (cfg.channels?.dingtalk ?? {}) as DingTalkConfig;
|
||||
const { accounts: _ignored, ...base } = raw;
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account } as DingTalkAccountConfig;
|
||||
}
|
||||
|
||||
function resolveCredentialsFromConfig(params: {
|
||||
accountId: string;
|
||||
account: DingTalkAccountConfig;
|
||||
}): {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
source: DingTalkCredentialSource;
|
||||
} {
|
||||
const { account, accountId } = params;
|
||||
|
||||
// Check Stream mode credentials from config
|
||||
if (account.clientId?.trim() && account.clientSecret?.trim()) {
|
||||
return {
|
||||
clientId: account.clientId.trim(),
|
||||
clientSecret: account.clientSecret.trim(),
|
||||
source: "config",
|
||||
};
|
||||
}
|
||||
|
||||
// Check environment variables for default account
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
const envClientId = process.env[ENV_CLIENT_ID]?.trim();
|
||||
const envClientSecret = process.env[ENV_CLIENT_SECRET]?.trim();
|
||||
if (envClientId && envClientSecret) {
|
||||
return {
|
||||
clientId: envClientId,
|
||||
clientSecret: envClientSecret,
|
||||
source: "env",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { source: "none" };
|
||||
}
|
||||
|
||||
export function resolveDingTalkAccount(params: {
|
||||
cfg: MoltbotConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedDingTalkAccount {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const baseEnabled =
|
||||
(params.cfg.channels?.dingtalk as DingTalkConfig | undefined)?.enabled !== false;
|
||||
const merged = mergeDingTalkAccountConfig(params.cfg, accountId);
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
const credentials = resolveCredentialsFromConfig({ accountId, account: merged });
|
||||
|
||||
return {
|
||||
accountId,
|
||||
name: merged.name?.trim() || undefined,
|
||||
enabled,
|
||||
config: merged,
|
||||
credentialSource: credentials.source,
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: credentials.clientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
export function listEnabledDingTalkAccounts(cfg: MoltbotConfig): ResolvedDingTalkAccount[] {
|
||||
return listDingTalkAccountIds(cfg)
|
||||
.map((accountId) => resolveDingTalkAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
}
|
||||
250
extensions/dingtalk/src/api.ts
Normal file
250
extensions/dingtalk/src/api.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import type { ResolvedDingTalkAccount } from "./accounts.js";
|
||||
import type {
|
||||
DingTalkApiResponse,
|
||||
DingTalkOutboundMessage,
|
||||
DingTalkTextMessage,
|
||||
DingTalkMarkdownMessage,
|
||||
} from "./types.js";
|
||||
|
||||
const DINGTALK_ROBOT_SEND_URL = "https://oapi.dingtalk.com/robot/send";
|
||||
|
||||
/**
|
||||
* Generate HMAC-SHA256 signature for DingTalk webhook authentication.
|
||||
* Format: timestamp + "\n" + secret, then HMAC-SHA256, base64, URL encode
|
||||
* @deprecated Legacy webhook mode - use Stream mode instead
|
||||
*/
|
||||
export function generateSignature(timestamp: number, secret: string): string {
|
||||
const stringToSign = `${timestamp}\n${secret}`;
|
||||
const hmac = crypto.createHmac("sha256", secret);
|
||||
hmac.update(stringToSign, "utf8");
|
||||
const digest = hmac.digest("base64");
|
||||
return encodeURIComponent(digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify incoming webhook signature from DingTalk.
|
||||
* The signature is in request headers: timestamp and sign.
|
||||
* @deprecated Legacy webhook mode - use Stream mode instead
|
||||
*/
|
||||
export function verifySignature(params: {
|
||||
timestamp: string;
|
||||
sign: string;
|
||||
secret: string;
|
||||
}): { ok: boolean; error?: string } {
|
||||
const { timestamp, sign, secret } = params;
|
||||
|
||||
// Check timestamp is within 1 hour
|
||||
const ts = Number.parseInt(timestamp, 10);
|
||||
if (!Number.isFinite(ts)) {
|
||||
return { ok: false, error: "invalid timestamp" };
|
||||
}
|
||||
const now = Date.now();
|
||||
if (Math.abs(now - ts) > 60 * 60 * 1000) {
|
||||
return { ok: false, error: "timestamp expired" };
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
const expected = generateSignature(ts, secret);
|
||||
// URL decode the incoming sign for comparison
|
||||
const decodedSign = decodeURIComponent(sign);
|
||||
const decodedExpected = decodeURIComponent(expected);
|
||||
|
||||
if (decodedSign !== decodedExpected) {
|
||||
return { ok: false, error: "signature mismatch" };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full webhook URL with authentication parameters.
|
||||
* @deprecated Legacy webhook mode - use Stream mode instead
|
||||
*/
|
||||
export function buildWebhookUrl(params: {
|
||||
accessToken: string;
|
||||
secret?: string;
|
||||
}): string {
|
||||
const { accessToken, secret } = params;
|
||||
const url = new URL(DINGTALK_ROBOT_SEND_URL);
|
||||
url.searchParams.set("access_token", accessToken);
|
||||
|
||||
if (secret) {
|
||||
const timestamp = Date.now();
|
||||
const sign = generateSignature(timestamp, secret);
|
||||
url.searchParams.set("timestamp", String(timestamp));
|
||||
url.searchParams.set("sign", sign);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to DingTalk robot webhook.
|
||||
*/
|
||||
export async function sendDingTalkMessage(params: {
|
||||
accessToken: string;
|
||||
secret?: string;
|
||||
message: DingTalkOutboundMessage;
|
||||
}): Promise<{ ok: boolean; response?: DingTalkApiResponse; error?: string }> {
|
||||
const { accessToken, secret, message } = params;
|
||||
|
||||
const url = buildWebhookUrl({ accessToken, secret });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as DingTalkApiResponse;
|
||||
|
||||
if (data.errcode !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
response: data,
|
||||
error: `DingTalk API error: ${data.errmsg} (code: ${data.errcode})`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, response: data };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text message to DingTalk.
|
||||
*/
|
||||
export async function sendTextMessage(params: {
|
||||
accessToken: string;
|
||||
secret?: string;
|
||||
content: string;
|
||||
atUserIds?: string[];
|
||||
atMobiles?: string[];
|
||||
isAtAll?: boolean;
|
||||
}): Promise<{ ok: boolean; error?: string }> {
|
||||
const { accessToken, secret, content, atUserIds, atMobiles, isAtAll } = params;
|
||||
|
||||
const message: DingTalkTextMessage = {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content,
|
||||
},
|
||||
};
|
||||
|
||||
if (atUserIds?.length || atMobiles?.length || isAtAll) {
|
||||
message.at = {
|
||||
atUserIds,
|
||||
atMobiles,
|
||||
isAtAll,
|
||||
};
|
||||
}
|
||||
|
||||
return sendDingTalkMessage({ accessToken, secret, message });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a markdown message to DingTalk.
|
||||
*/
|
||||
export async function sendMarkdownMessage(params: {
|
||||
accessToken: string;
|
||||
secret?: string;
|
||||
title: string;
|
||||
text: string;
|
||||
atUserIds?: string[];
|
||||
atMobiles?: string[];
|
||||
isAtAll?: boolean;
|
||||
}): Promise<{ ok: boolean; error?: string }> {
|
||||
const { accessToken, secret, title, text, atUserIds, atMobiles, isAtAll } = params;
|
||||
|
||||
const message: DingTalkMarkdownMessage = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title,
|
||||
text,
|
||||
},
|
||||
};
|
||||
|
||||
if (atUserIds?.length || atMobiles?.length || isAtAll) {
|
||||
message.at = {
|
||||
atUserIds,
|
||||
atMobiles,
|
||||
isAtAll,
|
||||
};
|
||||
}
|
||||
|
||||
return sendDingTalkMessage({ accessToken, secret, message });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to a message using the session webhook.
|
||||
* This is the preferred method when replying to incoming messages.
|
||||
*/
|
||||
export async function replyWithSessionWebhook(params: {
|
||||
sessionWebhook: string;
|
||||
message: DingTalkOutboundMessage;
|
||||
}): Promise<{ ok: boolean; response?: DingTalkApiResponse; error?: string }> {
|
||||
const { sessionWebhook, message } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(sessionWebhook, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as DingTalkApiResponse;
|
||||
|
||||
if (data.errcode !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
response: data,
|
||||
error: `DingTalk API error: ${data.errmsg} (code: ${data.errcode})`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, response: data };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe DingTalk connection.
|
||||
* For Stream mode, we validate clientId and clientSecret are configured.
|
||||
*/
|
||||
export async function probeDingTalk(account: ResolvedDingTalkAccount): Promise<{
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
configured: boolean;
|
||||
mode: "stream" | "none";
|
||||
}> {
|
||||
// Check Stream mode credentials
|
||||
if (account.clientId?.trim() && account.clientSecret?.trim()) {
|
||||
return {
|
||||
ok: true,
|
||||
configured: true,
|
||||
mode: "stream",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
configured: false,
|
||||
mode: "none",
|
||||
error: "DingTalk credentials not configured. Set clientId and clientSecret for Stream mode.",
|
||||
};
|
||||
}
|
||||
430
extensions/dingtalk/src/channel.ts
Normal file
430
extensions/dingtalk/src/channel.ts
Normal file
@ -0,0 +1,430 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
emptyPluginConfigSchema,
|
||||
formatPairingApproveHint,
|
||||
getChatChannelMeta,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelDock,
|
||||
type ChannelPlugin,
|
||||
type MoltbotConfig,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
import {
|
||||
listDingTalkAccountIds,
|
||||
resolveDefaultDingTalkAccountId,
|
||||
resolveDingTalkAccount,
|
||||
type ResolvedDingTalkAccount,
|
||||
} from "./accounts.js";
|
||||
import { probeDingTalk } from "./api.js";
|
||||
import { dingtalkOnboardingAdapter } from "./onboarding.js";
|
||||
import { getDingTalkRuntime } from "./runtime.js";
|
||||
import { startDingTalkMonitor } from "./monitor.js";
|
||||
|
||||
const meta = getChatChannelMeta("dingtalk");
|
||||
|
||||
const formatAllowFromEntry = (entry: string) =>
|
||||
entry
|
||||
.trim()
|
||||
.replace(/^(dingtalk|dingding|ding):/i, "")
|
||||
.toLowerCase();
|
||||
|
||||
export const dingtalkDock: ChannelDock = {
|
||||
id: "dingtalk",
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
reactions: false,
|
||||
media: false, // DingTalk robot API has limited media support
|
||||
threads: false,
|
||||
blockStreaming: true,
|
||||
},
|
||||
outbound: { textChunkLimit: 4000 },
|
||||
config: {
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
(
|
||||
resolveDingTalkAccount({ cfg: cfg as MoltbotConfig, accountId }).config.dm?.allowFrom ?? []
|
||||
).map((entry) => String(entry)),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry))
|
||||
.filter(Boolean)
|
||||
.map(formatAllowFromEntry),
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: ({ cfg, accountId }) => {
|
||||
const account = resolveDingTalkAccount({ cfg: cfg as MoltbotConfig, accountId });
|
||||
return account.config.requireMention ?? true;
|
||||
},
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.["dingtalk"]?.replyToMode ?? "off",
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
hasRepliedRef,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const dingtalkPlugin: ChannelPlugin<ResolvedDingTalkAccount> = {
|
||||
id: "dingtalk",
|
||||
meta: { ...meta },
|
||||
onboarding: dingtalkOnboardingAdapter,
|
||||
pairing: {
|
||||
idLabel: "dingtalkUserId",
|
||||
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
|
||||
notifyApproval: async ({ cfg, id }) => {
|
||||
const account = resolveDingTalkAccount({ cfg: cfg as MoltbotConfig });
|
||||
if (account.credentialSource === "none" || !account.clientId) return;
|
||||
// Note: DingTalk doesn't support sending DMs directly via robot API
|
||||
// The user will need to @mention the robot first to establish a session
|
||||
// This is a limitation of the DingTalk platform
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
reactions: false,
|
||||
threads: false,
|
||||
media: false,
|
||||
nativeCommands: false,
|
||||
blockStreaming: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.dingtalk"] },
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listDingTalkAccountIds(cfg as MoltbotConfig),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
resolveDingTalkAccount({ cfg: cfg as MoltbotConfig, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultDingTalkAccountId(cfg as MoltbotConfig),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
sectionKey: "dingtalk",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
sectionKey: "dingtalk",
|
||||
accountId,
|
||||
clearBaseFields: ["accessToken", "accessTokenFile", "secret", "secretFile", "webhookPath", "webhookUrl", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.credentialSource !== "none",
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
credentialSource: account.credentialSource,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
(
|
||||
resolveDingTalkAccount({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
accountId,
|
||||
}).config.dm?.allowFrom ?? []
|
||||
).map((entry) => String(entry)),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry))
|
||||
.filter(Boolean)
|
||||
.map(formatAllowFromEntry),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const useAccountPath = Boolean(
|
||||
(cfg as MoltbotConfig).channels?.["dingtalk"]?.accounts?.[resolvedAccountId],
|
||||
);
|
||||
const allowFromPath = useAccountPath
|
||||
? `channels.dingtalk.accounts.${resolvedAccountId}.dm.`
|
||||
: "channels.dingtalk.dm.";
|
||||
return {
|
||||
policy: account.config.dm?.policy ?? "pairing",
|
||||
allowFrom: account.config.dm?.allowFrom ?? [],
|
||||
allowFromPath,
|
||||
approveHint: formatPairingApproveHint("dingtalk"),
|
||||
normalizeEntry: (raw) => formatAllowFromEntry(raw),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const warnings: string[] = [];
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
if (groupPolicy === "open") {
|
||||
warnings.push(
|
||||
`- DingTalk groups: groupPolicy="open" allows any group to trigger (mention-gated). Set channels.dingtalk.groupPolicy="allowlist" and configure channels.dingtalk.groups.`,
|
||||
);
|
||||
}
|
||||
if (account.config.dm?.policy === "open") {
|
||||
warnings.push(
|
||||
`- DingTalk DMs are open to anyone. Set channels.dingtalk.dm.policy="pairing" or "allowlist".`,
|
||||
);
|
||||
}
|
||||
return warnings;
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: ({ cfg, accountId }) => {
|
||||
const account = resolveDingTalkAccount({ cfg: cfg as MoltbotConfig, accountId });
|
||||
return account.config.requireMention ?? true;
|
||||
},
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.["dingtalk"]?.replyToMode ?? "off",
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: (raw) => {
|
||||
const trimmed = raw?.trim();
|
||||
if (!trimmed) return null;
|
||||
return trimmed.replace(/^(dingtalk|dingding|ding):/i, "");
|
||||
},
|
||||
targetResolver: {
|
||||
looksLikeId: (raw, normalized) => {
|
||||
const value = normalized ?? raw.trim();
|
||||
return Boolean(value);
|
||||
},
|
||||
hint: "<conversationId>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveDingTalkAccount({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
accountId,
|
||||
});
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const allowFrom = account.config.dm?.allowFrom ?? [];
|
||||
const peers = Array.from(
|
||||
new Set(
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*"),
|
||||
),
|
||||
)
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
return peers;
|
||||
},
|
||||
listGroups: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveDingTalkAccount({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
accountId,
|
||||
});
|
||||
const groups = account.config.groups ?? {};
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const entries = Object.keys(groups)
|
||||
.filter((key) => key && key !== "*")
|
||||
.filter((key) => (q ? key.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
return entries;
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
channelKey: "dingtalk",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ accountId, input }) => {
|
||||
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return "DINGTALK_ACCESS_TOKEN env var can only be used for the default account.";
|
||||
}
|
||||
if (!input.useEnv && !input.token && !input.tokenFile) {
|
||||
return "DingTalk requires access token or --token-file (or --use-env).";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
channelKey: "dingtalk",
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig as MoltbotConfig,
|
||||
channelKey: "dingtalk",
|
||||
})
|
||||
: namedConfig;
|
||||
const patch = input.useEnv
|
||||
? {}
|
||||
: input.tokenFile
|
||||
? { accessTokenFile: input.tokenFile }
|
||||
: input.token
|
||||
? { accessToken: input.token }
|
||||
: {};
|
||||
const webhookPath = input.webhookPath?.trim();
|
||||
const webhookUrl = input.webhookUrl?.trim();
|
||||
const configPatch = {
|
||||
...patch,
|
||||
...(webhookPath ? { webhookPath } : {}),
|
||||
...(webhookUrl ? { webhookUrl } : {}),
|
||||
};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
dingtalk: {
|
||||
...(next.channels?.["dingtalk"] ?? {}),
|
||||
enabled: true,
|
||||
...configPatch,
|
||||
},
|
||||
},
|
||||
} as MoltbotConfig;
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
dingtalk: {
|
||||
...(next.channels?.["dingtalk"] ?? {}),
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...(next.channels?.["dingtalk"]?.accounts ?? {}),
|
||||
[accountId]: {
|
||||
...(next.channels?.["dingtalk"]?.accounts?.[accountId] ?? {}),
|
||||
enabled: true,
|
||||
...configPatch,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as MoltbotConfig;
|
||||
},
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text: string, limit: number) =>
|
||||
getDingTalkRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, accountId }) => {
|
||||
const account = resolveDingTalkAccount({
|
||||
cfg: cfg as MoltbotConfig,
|
||||
accountId,
|
||||
});
|
||||
// Stream mode doesn't support outbound messages via accessToken
|
||||
// Messages are sent via sessionWebhook in response to inbound messages
|
||||
if (!account.clientId) {
|
||||
throw new Error("DingTalk clientId not configured");
|
||||
}
|
||||
// For outbound messages, we need to use a different API
|
||||
// This is a limitation of DingTalk's robot API
|
||||
throw new Error(
|
||||
"DingTalk Stream mode does not support proactive outbound messages. Messages can only be sent in response to user messages.",
|
||||
);
|
||||
},
|
||||
sendMedia: async () => {
|
||||
// DingTalk robot API has very limited media support
|
||||
// Media messages typically require different API endpoints
|
||||
throw new Error("DingTalk robot API does not support media attachments");
|
||||
},
|
||||
},
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
collectStatusIssues: (accounts: Array<{ accountId?: string; enabled?: boolean; configured?: boolean; clientId?: string }>) =>
|
||||
accounts.flatMap((entry) => {
|
||||
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const enabled = entry.enabled !== false;
|
||||
const configured = entry.configured === true;
|
||||
if (!enabled || !configured) return [];
|
||||
const issues: Array<{ channel: string; accountId: string; kind: string; message: string; fix: string }> = [];
|
||||
if (!entry.clientId) {
|
||||
issues.push({
|
||||
channel: "dingtalk",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "DingTalk clientId (AppKey) is missing.",
|
||||
fix: "Set channels.dingtalk.clientId.",
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}),
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
configured: snapshot.configured ?? false,
|
||||
credentialSource: snapshot.credentialSource ?? "none",
|
||||
mode: "stream",
|
||||
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 }) => probeDingTalk(account),
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.credentialSource !== "none",
|
||||
credentialSource: account.credentialSource,
|
||||
mode: "stream",
|
||||
clientId: account.clientId,
|
||||
running: runtime?.running ?? false,
|
||||
lastStartAt: runtime?.lastStartAt ?? null,
|
||||
lastStopAt: runtime?.lastStopAt ?? null,
|
||||
lastError: runtime?.lastError ?? null,
|
||||
lastInboundAt: runtime?.lastInboundAt ?? null,
|
||||
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
||||
dmPolicy: account.config.dm?.policy ?? "pairing",
|
||||
probe,
|
||||
}),
|
||||
},
|
||||
gateway: {
|
||||
startAccount: async (ctx: {
|
||||
account: ResolvedDingTalkAccount;
|
||||
cfg: unknown;
|
||||
runtime: { log?: (msg: string) => void; error?: (msg: string) => void };
|
||||
abortSignal: AbortSignal;
|
||||
log?: { info: (msg: string) => void };
|
||||
setStatus: (patch: Record<string, unknown>) => void;
|
||||
}) => {
|
||||
const account = ctx.account;
|
||||
ctx.log?.info(`[${account.accountId}] starting DingTalk Stream mode`);
|
||||
ctx.setStatus({
|
||||
accountId: account.accountId,
|
||||
running: true,
|
||||
lastStartAt: Date.now(),
|
||||
mode: "stream",
|
||||
});
|
||||
const unregister = await startDingTalkMonitor({
|
||||
account,
|
||||
config: ctx.cfg as MoltbotConfig,
|
||||
runtime: ctx.runtime,
|
||||
abortSignal: ctx.abortSignal,
|
||||
statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
|
||||
});
|
||||
return () => {
|
||||
unregister?.();
|
||||
ctx.setStatus({
|
||||
accountId: account.accountId,
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
40
extensions/dingtalk/src/dingtalk-stream.d.ts
vendored
Normal file
40
extensions/dingtalk/src/dingtalk-stream.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Type declarations for dingtalk-stream SDK
|
||||
*/
|
||||
|
||||
declare module "dingtalk-stream" {
|
||||
export interface DWClientOptions {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
export interface StreamResponse {
|
||||
headers: {
|
||||
messageId: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
data: string;
|
||||
}
|
||||
|
||||
export class DWClient {
|
||||
constructor(options: DWClientOptions);
|
||||
registerCallbackListener(
|
||||
path: string,
|
||||
callback: (res: StreamResponse) => Promise<void>,
|
||||
): DWClient;
|
||||
connect(): Promise<void>;
|
||||
disconnect?(): void;
|
||||
socketCallBackResponse(messageId: string, response: string): void;
|
||||
}
|
||||
|
||||
export const EventAck: {
|
||||
SUCCESS: string;
|
||||
LATER: string;
|
||||
};
|
||||
|
||||
export const GATEWAY_URL: string;
|
||||
export const GET_TOKEN_URL: string;
|
||||
export const TOPIC_AI_GRAPH_API: string;
|
||||
export const TOPIC_CARD: string;
|
||||
export const TOPIC_ROBOT: string;
|
||||
}
|
||||
443
extensions/dingtalk/src/monitor.ts
Normal file
443
extensions/dingtalk/src/monitor.ts
Normal file
@ -0,0 +1,443 @@
|
||||
import type { DingTalkAccountConfig, MoltbotConfig } from "clawdbot/plugin-sdk";
|
||||
|
||||
import type { ResolvedDingTalkAccount } from "./accounts.js";
|
||||
import { replyWithSessionWebhook } from "./api.js";
|
||||
import { getDingTalkRuntime } from "./runtime.js";
|
||||
import type {
|
||||
DingTalkStreamMessage,
|
||||
DingTalkMarkdownMessage,
|
||||
DingTalkGroupConfig,
|
||||
} from "./types.js";
|
||||
|
||||
export type DingTalkRuntimeEnv = {
|
||||
log?: (message: string) => void;
|
||||
error?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type DingTalkMonitorOptions = {
|
||||
account: ResolvedDingTalkAccount;
|
||||
config: MoltbotConfig;
|
||||
runtime: DingTalkRuntimeEnv;
|
||||
abortSignal: AbortSignal;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
};
|
||||
|
||||
type DingTalkCoreRuntime = ReturnType<typeof getDingTalkRuntime>;
|
||||
|
||||
function logVerbose(core: DingTalkCoreRuntime, runtime: DingTalkRuntimeEnv, message: string) {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
runtime.log?.(`[dingtalk] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve group entry from config for allowlist checking
|
||||
*/
|
||||
function resolveGroupEntry(
|
||||
config: DingTalkAccountConfig,
|
||||
conversationId: string,
|
||||
): {
|
||||
entry: DingTalkGroupConfig | undefined;
|
||||
wildcard: DingTalkGroupConfig | undefined;
|
||||
allowlistConfigured: boolean;
|
||||
} {
|
||||
const groups = config.groups ?? {};
|
||||
const entry = groups[conversationId];
|
||||
const wildcard = groups["*"];
|
||||
const allowlistConfigured = Object.keys(groups).length > 0;
|
||||
return { entry, wildcard, allowlistConfigured };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start DingTalk Stream mode monitor using dingtalk-stream SDK.
|
||||
* Stream mode uses WebSocket connection, no public URL needed.
|
||||
*/
|
||||
export async function startDingTalkMonitor(params: DingTalkMonitorOptions): Promise<() => void> {
|
||||
const { account, config, runtime, abortSignal, statusSink } = params;
|
||||
const core = getDingTalkRuntime();
|
||||
|
||||
// Validate Stream mode credentials
|
||||
if (!account.clientId || !account.clientSecret) {
|
||||
runtime.error?.(
|
||||
`[${account.accountId}] DingTalk Stream mode requires clientId and clientSecret`,
|
||||
);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
runtime.log?.(`[${account.accountId}] Starting DingTalk Stream mode connection...`);
|
||||
|
||||
let client: DingTalkStreamClient | null = null;
|
||||
let isConnected = false;
|
||||
|
||||
try {
|
||||
// Dynamic import of dingtalk-stream SDK
|
||||
const { DWClient, EventAck } = await import("dingtalk-stream");
|
||||
|
||||
// Create DingTalk Stream client
|
||||
client = new DWClient({
|
||||
clientId: account.clientId,
|
||||
clientSecret: account.clientSecret,
|
||||
}) as DingTalkStreamClient;
|
||||
|
||||
// Register robot message callback listener
|
||||
// The callback path for robot messages is '/v1.0/im/bot/messages/get'
|
||||
client.registerCallbackListener(
|
||||
"/v1.0/im/bot/messages/get",
|
||||
async (res: DingTalkStreamResponse) => {
|
||||
try {
|
||||
statusSink?.({ lastInboundAt: Date.now() });
|
||||
|
||||
const message = JSON.parse(res.data) as DingTalkStreamMessage;
|
||||
logVerbose(core, runtime, `received stream message: ${JSON.stringify(message)}`);
|
||||
|
||||
await processStreamMessage({
|
||||
message,
|
||||
account,
|
||||
config,
|
||||
runtime,
|
||||
core,
|
||||
statusSink,
|
||||
});
|
||||
|
||||
// Acknowledge the message was processed
|
||||
client?.socketCallBackResponse(res.headers.messageId, EventAck.SUCCESS);
|
||||
} catch (err) {
|
||||
runtime.error?.(`[${account.accountId}] Stream message processing error: ${String(err)}`);
|
||||
// Still acknowledge to prevent redelivery
|
||||
client?.socketCallBackResponse(res.headers.messageId, EventAck.SUCCESS);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Connect to DingTalk Stream
|
||||
await client.connect();
|
||||
isConnected = true;
|
||||
runtime.log?.(`[${account.accountId}] DingTalk Stream mode connected successfully`);
|
||||
|
||||
// Handle abort signal
|
||||
const handleAbort = () => {
|
||||
if (client && isConnected) {
|
||||
runtime.log?.(`[${account.accountId}] Disconnecting DingTalk Stream...`);
|
||||
try {
|
||||
client.disconnect?.();
|
||||
} catch {
|
||||
// Ignore disconnect errors
|
||||
}
|
||||
isConnected = false;
|
||||
}
|
||||
};
|
||||
|
||||
abortSignal.addEventListener("abort", handleAbort);
|
||||
|
||||
return () => {
|
||||
abortSignal.removeEventListener("abort", handleAbort);
|
||||
handleAbort();
|
||||
};
|
||||
} catch (err) {
|
||||
runtime.error?.(`[${account.accountId}] DingTalk Stream connection failed: ${String(err)}`);
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
// Type definitions for dingtalk-stream SDK
|
||||
interface DingTalkStreamClient {
|
||||
registerCallbackListener: (
|
||||
path: string,
|
||||
callback: (res: DingTalkStreamResponse) => Promise<void>,
|
||||
) => DingTalkStreamClient;
|
||||
connect: () => Promise<void>;
|
||||
disconnect?: () => void;
|
||||
socketCallBackResponse: (messageId: string, response: string) => void;
|
||||
}
|
||||
|
||||
interface DingTalkStreamResponse {
|
||||
headers: {
|
||||
messageId: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
data: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming DingTalk Stream message
|
||||
*/
|
||||
async function processStreamMessage(params: {
|
||||
message: DingTalkStreamMessage;
|
||||
account: ResolvedDingTalkAccount;
|
||||
config: MoltbotConfig;
|
||||
runtime: DingTalkRuntimeEnv;
|
||||
core: DingTalkCoreRuntime;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
}): Promise<void> {
|
||||
const { message, account, config, runtime, core, statusSink } = params;
|
||||
|
||||
// Only handle text messages for now
|
||||
if (message.msgtype !== "text") {
|
||||
logVerbose(core, runtime, `skip non-text message (type=${message.msgtype})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageText = message.text?.content?.trim() ?? "";
|
||||
if (!messageText) {
|
||||
logVerbose(core, runtime, "skip empty message");
|
||||
return;
|
||||
}
|
||||
|
||||
const isGroup = message.conversationType === "2";
|
||||
const senderId = message.senderStaffId;
|
||||
const senderName = message.senderNick ?? "";
|
||||
const conversationId = message.conversationId;
|
||||
const conversationTitle = message.conversationTitle ?? "";
|
||||
|
||||
// Check if sender is allowed (simplified DM policy check)
|
||||
const dmPolicy = account.config.dm?.policy ?? "pairing";
|
||||
const configAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v));
|
||||
|
||||
if (!isGroup && dmPolicy !== "open") {
|
||||
const allowed = isSenderAllowed(senderId, configAllowFrom);
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "dingtalk",
|
||||
id: senderId,
|
||||
meta: { name: senderName || undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(core, runtime, `dingtalk pairing request sender=${senderId}`);
|
||||
try {
|
||||
await replyWithSessionWebhook({
|
||||
sessionWebhook: message.sessionWebhook,
|
||||
message: {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: core.channel.pairing.buildPairingReply({
|
||||
channel: "dingtalk",
|
||||
idLine: `Your DingTalk user ID: ${senderId}`,
|
||||
code,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
logVerbose(core, runtime, `pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logVerbose(core, runtime, `Blocked unauthorized DingTalk sender ${senderId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Group message handling
|
||||
if (isGroup) {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
if (groupPolicy === "disabled") {
|
||||
logVerbose(core, runtime, `drop group message (groupPolicy=disabled)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const groupInfo = resolveGroupEntry(account.config, conversationId);
|
||||
const groupEntry = groupInfo.entry;
|
||||
|
||||
// Allowlist check
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (!groupInfo.allowlistConfigured) {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`drop group message (allowlist empty, chat=${conversationId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const groupAllowed = Boolean(groupEntry) || Boolean(groupInfo.wildcard);
|
||||
if (!groupAllowed) {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`drop group message (not allowlisted, chat=${conversationId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if group is disabled
|
||||
if (groupEntry?.enabled === false || groupEntry?.allow === false) {
|
||||
logVerbose(core, runtime, `drop group message (disabled, chat=${conversationId})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check group user allowlist
|
||||
if (groupEntry?.users && groupEntry.users.length > 0) {
|
||||
const ok = isSenderAllowed(
|
||||
senderId,
|
||||
groupEntry.users.map((v) => String(v)),
|
||||
);
|
||||
if (!ok) {
|
||||
logVerbose(core, runtime, `drop group message (sender not allowed, ${senderId})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// @mention check
|
||||
const requireMention =
|
||||
groupEntry?.requireMention ?? account.config.requireMention ?? true;
|
||||
const wasMentioned = message.isInAtList === true;
|
||||
|
||||
if (requireMention && !wasMentioned) {
|
||||
logVerbose(core, runtime, `drop group message (mention required)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build route and session
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
cfg: config,
|
||||
channel: "dingtalk",
|
||||
accountId: account.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: conversationId,
|
||||
},
|
||||
});
|
||||
|
||||
const fromLabel = isGroup
|
||||
? conversationTitle || `group:${conversationId}`
|
||||
: senderName || `user:${senderId}`;
|
||||
|
||||
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
|
||||
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
||||
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
||||
storePath,
|
||||
sessionKey: route.sessionKey,
|
||||
});
|
||||
|
||||
const body = core.channel.reply.formatAgentEnvelope({
|
||||
channel: "DingTalk",
|
||||
from: fromLabel,
|
||||
timestamp: message.createAt ?? Date.now(),
|
||||
previousTimestamp,
|
||||
envelope: envelopeOptions,
|
||||
body: messageText,
|
||||
});
|
||||
|
||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||
Body: body,
|
||||
RawBody: messageText,
|
||||
CommandBody: messageText,
|
||||
From: `dingtalk:${senderId}`,
|
||||
To: `dingtalk:${conversationId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: isGroup ? "channel" : "direct",
|
||||
ConversationLabel: fromLabel,
|
||||
SenderName: senderName || undefined,
|
||||
SenderId: senderId,
|
||||
WasMentioned: isGroup ? message.isInAtList : undefined,
|
||||
Provider: "dingtalk",
|
||||
Surface: "dingtalk",
|
||||
MessageSid: message.msgId,
|
||||
MessageSidFull: message.msgId,
|
||||
GroupSpace: isGroup ? conversationTitle ?? undefined : undefined,
|
||||
OriginatingChannel: "dingtalk",
|
||||
OriginatingTo: `dingtalk:${conversationId}`,
|
||||
});
|
||||
|
||||
void core.channel.session
|
||||
.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
runtime.error?.(`dingtalk: failed updating session meta: ${String(err)}`);
|
||||
});
|
||||
|
||||
// Dispatch reply
|
||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: config,
|
||||
dispatcherOptions: {
|
||||
deliver: async (payload) => {
|
||||
await deliverDingTalkReply({
|
||||
payload,
|
||||
sessionWebhook: message.sessionWebhook,
|
||||
runtime,
|
||||
core,
|
||||
config,
|
||||
account,
|
||||
statusSink,
|
||||
});
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
`[${account.accountId}] DingTalk ${info.kind} reply failed: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
||||
if (allowFrom.includes("*")) return true;
|
||||
const normalizedSenderId = senderId.toLowerCase();
|
||||
return allowFrom.some((entry) => {
|
||||
const normalized = String(entry).trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
if (normalized === normalizedSenderId) return true;
|
||||
if (normalized.replace(/^dingtalk:/i, "") === normalizedSenderId) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async function deliverDingTalkReply(params: {
|
||||
payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string };
|
||||
sessionWebhook: string;
|
||||
runtime: DingTalkRuntimeEnv;
|
||||
core: DingTalkCoreRuntime;
|
||||
config: MoltbotConfig;
|
||||
account: ResolvedDingTalkAccount;
|
||||
statusSink?: (patch: { lastOutboundAt?: number }) => void;
|
||||
}): Promise<void> {
|
||||
const { payload, sessionWebhook, runtime, core, config, account, statusSink } = params;
|
||||
|
||||
// DingTalk doesn't support media attachments via session webhook in the same way
|
||||
// We'll send text messages and note media limitations
|
||||
if (payload.text) {
|
||||
const chunkLimit = account.config.textChunkLimit ?? 4000;
|
||||
const chunkMode = core.channel.text.resolveChunkMode(config, "dingtalk", account.accountId);
|
||||
const chunks = core.channel.text.chunkMarkdownTextWithMode(payload.text, chunkLimit, chunkMode);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
// Use markdown format for better formatting
|
||||
const message: DingTalkMarkdownMessage = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: "Reply",
|
||||
text: chunk,
|
||||
},
|
||||
};
|
||||
await replyWithSessionWebhook({ sessionWebhook, message });
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
runtime.error?.(`DingTalk message send failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Media handling would require additional API endpoints
|
||||
// DingTalk's robot API has limited media support compared to other platforms
|
||||
}
|
||||
|
||||
// Legacy exports for backward compatibility
|
||||
export function resolveDingTalkWebhookPath(_params: {
|
||||
account: ResolvedDingTalkAccount;
|
||||
}): string {
|
||||
// Stream mode doesn't use webhooks, but return a placeholder for compatibility
|
||||
return "/dingtalk-stream";
|
||||
}
|
||||
230
extensions/dingtalk/src/onboarding.ts
Normal file
230
extensions/dingtalk/src/onboarding.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import type { MoltbotConfig, DmPolicy } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
formatDocsLink,
|
||||
promptAccountId,
|
||||
type ChannelOnboardingAdapter,
|
||||
type ChannelOnboardingDmPolicy,
|
||||
type WizardPrompter,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
import {
|
||||
listDingTalkAccountIds,
|
||||
resolveDefaultDingTalkAccountId,
|
||||
resolveDingTalkAccount,
|
||||
} from "./accounts.js";
|
||||
|
||||
const channel = "dingtalk" as const;
|
||||
|
||||
// Stream mode environment variables
|
||||
const ENV_CLIENT_ID = "DINGTALK_CLIENT_ID";
|
||||
const ENV_CLIENT_SECRET = "DINGTALK_CLIENT_SECRET";
|
||||
|
||||
function setDingTalkDmPolicy(cfg: MoltbotConfig, policy: DmPolicy) {
|
||||
const allowFrom =
|
||||
policy === "open"
|
||||
? addWildcardAllowFrom(cfg.channels?.dingtalk?.dm?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
dingtalk: {
|
||||
...(cfg.channels?.dingtalk ?? {}),
|
||||
dm: {
|
||||
...(cfg.channels?.dingtalk?.dm ?? {}),
|
||||
policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseAllowFromInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function promptAllowFrom(params: {
|
||||
cfg: MoltbotConfig;
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<MoltbotConfig> {
|
||||
const current = params.cfg.channels?.dingtalk?.dm?.allowFrom ?? [];
|
||||
const entry = await params.prompter.text({
|
||||
message: "DingTalk allowFrom (user ID)",
|
||||
placeholder: "user123, user456",
|
||||
initialValue: current[0] ? String(current[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = parseAllowFromInput(String(entry));
|
||||
const unique = [...new Set(parts)];
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
dingtalk: {
|
||||
...(params.cfg.channels?.dingtalk ?? {}),
|
||||
enabled: true,
|
||||
dm: {
|
||||
...(params.cfg.channels?.dingtalk?.dm ?? {}),
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "DingTalk",
|
||||
channel,
|
||||
policyKey: "channels.dingtalk.dm.policy",
|
||||
allowFromKey: "channels.dingtalk.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.dingtalk?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setDingTalkDmPolicy(cfg, policy),
|
||||
promptAllowFrom,
|
||||
};
|
||||
|
||||
function applyAccountConfig(params: {
|
||||
cfg: MoltbotConfig;
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): MoltbotConfig {
|
||||
const { cfg, accountId, patch } = params;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
dingtalk: {
|
||||
...(cfg.channels?.dingtalk ?? {}),
|
||||
enabled: true,
|
||||
...patch,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
dingtalk: {
|
||||
...(cfg.channels?.dingtalk ?? {}),
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...(cfg.channels?.dingtalk?.accounts ?? {}),
|
||||
[accountId]: {
|
||||
...(cfg.channels?.dingtalk?.accounts?.[accountId] ?? {}),
|
||||
enabled: true,
|
||||
...patch,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function promptCredentials(params: {
|
||||
cfg: MoltbotConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<MoltbotConfig> {
|
||||
const { cfg, prompter, accountId } = params;
|
||||
const envReady =
|
||||
accountId === DEFAULT_ACCOUNT_ID &&
|
||||
Boolean(process.env[ENV_CLIENT_ID]) &&
|
||||
Boolean(process.env[ENV_CLIENT_SECRET]);
|
||||
|
||||
if (envReady) {
|
||||
const useEnv = await prompter.confirm({
|
||||
message: "Use DINGTALK_CLIENT_ID/DINGTALK_CLIENT_SECRET env vars?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
return applyAccountConfig({ cfg, accountId, patch: {} });
|
||||
}
|
||||
}
|
||||
|
||||
const clientId = await prompter.text({
|
||||
message: "DingTalk AppKey (Client ID)",
|
||||
placeholder: "dingxxxxxxxx",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
|
||||
const clientSecret = await prompter.text({
|
||||
message: "DingTalk AppSecret (Client Secret)",
|
||||
placeholder: "xxxxxxxx",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
|
||||
return applyAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: {
|
||||
clientId: String(clientId).trim(),
|
||||
clientSecret: String(clientSecret).trim(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function noteDingTalkSetup(prompter: WizardPrompter) {
|
||||
await prompter.note(
|
||||
[
|
||||
"DingTalk Stream mode uses WebSocket connection (no public URL needed).",
|
||||
"1. Create an enterprise internal robot in DingTalk Open Platform",
|
||||
"2. Enable 'Stream Mode' in robot settings",
|
||||
"3. Copy the AppKey (Client ID) and AppSecret (Client Secret)",
|
||||
`Docs: ${formatDocsLink("/channels/dingtalk", "channels/dingtalk")}`,
|
||||
].join("\n"),
|
||||
"DingTalk setup",
|
||||
);
|
||||
}
|
||||
|
||||
export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
dmPolicy,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listDingTalkAccountIds(cfg).some(
|
||||
(accountId) => resolveDingTalkAccount({ cfg, accountId }).credentialSource !== "none",
|
||||
);
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`DingTalk: ${configured ? "configured" : "needs credentials"}`],
|
||||
selectionHint: configured ? "configured" : "needs auth",
|
||||
};
|
||||
},
|
||||
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
||||
const override = accountOverrides["dingtalk"]?.trim();
|
||||
const defaultAccountId = resolveDefaultDingTalkAccountId(cfg);
|
||||
let accountId = override ? normalizeAccountId(override) : defaultAccountId;
|
||||
|
||||
if (shouldPromptAccountIds && !override) {
|
||||
accountId = await promptAccountId({
|
||||
cfg,
|
||||
prompter,
|
||||
label: "DingTalk",
|
||||
currentId: accountId,
|
||||
listAccountIds: listDingTalkAccountIds,
|
||||
defaultAccountId,
|
||||
});
|
||||
}
|
||||
|
||||
let next = cfg;
|
||||
await noteDingTalkSetup(prompter);
|
||||
next = await promptCredentials({ cfg: next, prompter, accountId });
|
||||
|
||||
const namedConfig = migrateBaseNameToDefaultAccount({
|
||||
cfg: next,
|
||||
channelKey: "dingtalk",
|
||||
});
|
||||
|
||||
return { cfg: namedConfig, accountId };
|
||||
},
|
||||
};
|
||||
14
extensions/dingtalk/src/runtime.ts
Normal file
14
extensions/dingtalk/src/runtime.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
|
||||
let runtime: PluginRuntime | null = null;
|
||||
|
||||
export function setDingTalkRuntime(next: PluginRuntime) {
|
||||
runtime = next;
|
||||
}
|
||||
|
||||
export function getDingTalkRuntime(): PluginRuntime {
|
||||
if (!runtime) {
|
||||
throw new Error("DingTalk runtime not initialized");
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
209
extensions/dingtalk/src/types.ts
Normal file
209
extensions/dingtalk/src/types.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* DingTalk Bot API Types
|
||||
* Based on: https://open.dingtalk.com/document/orgapp/robot-message-types-and-data-format
|
||||
*/
|
||||
|
||||
// Re-export core types for convenience
|
||||
export type {
|
||||
DingTalkAccountConfig,
|
||||
DingTalkConfig,
|
||||
DingTalkDmConfig,
|
||||
DingTalkGroupConfig,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
// Inbound message types (received from DingTalk webhook callback)
|
||||
|
||||
export type DingTalkUser = {
|
||||
/** User ID in DingTalk */
|
||||
dingtalkId: string;
|
||||
/** Staff ID in enterprise */
|
||||
staffId?: string;
|
||||
/** User nickname */
|
||||
nick?: string;
|
||||
};
|
||||
|
||||
export type DingTalkAtUser = {
|
||||
/** DingTalk ID of the mentioned user */
|
||||
dingtalkId: string;
|
||||
/** Staff ID of the mentioned user */
|
||||
staffId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Legacy webhook callback event from DingTalk
|
||||
* Use Stream mode instead (DingTalkStreamMessage)
|
||||
*/
|
||||
export type DingTalkCallbackEvent = {
|
||||
/** Message ID */
|
||||
msgId: string;
|
||||
/** Message type: text, richText, picture, audio, video, file */
|
||||
msgtype: string;
|
||||
/** Conversation ID */
|
||||
conversationId: string;
|
||||
/** Conversation type: "1" = single chat, "2" = group chat */
|
||||
conversationType: "1" | "2";
|
||||
/** Conversation title (group name for group chat) */
|
||||
conversationTitle?: string;
|
||||
/** Chat bot corp ID */
|
||||
chatbotCorpId: string;
|
||||
/** Chat bot user ID */
|
||||
chatbotUserId: string;
|
||||
/** Sender ID */
|
||||
senderId: string;
|
||||
/** Sender corp ID */
|
||||
senderCorpId?: string;
|
||||
/** Sender nickname */
|
||||
senderNick: string;
|
||||
/** Sender staff ID */
|
||||
senderStaffId?: string;
|
||||
/** Whether sender is admin */
|
||||
isAdmin?: boolean;
|
||||
/** Session webhook URL for replying */
|
||||
sessionWebhook: string;
|
||||
/** Session webhook expiration timestamp */
|
||||
sessionWebhookExpiredTime: number;
|
||||
/** Message creation timestamp */
|
||||
createAt: number;
|
||||
/** Robot code */
|
||||
robotCode?: string;
|
||||
/** Text content for text messages */
|
||||
text?: {
|
||||
content: string;
|
||||
};
|
||||
/** Content for rich text messages */
|
||||
content?: string;
|
||||
/** At users list */
|
||||
atUsers?: DingTalkAtUser[];
|
||||
/** Whether the robot was @mentioned */
|
||||
isInAtList?: boolean;
|
||||
};
|
||||
|
||||
// Outbound message types (sent to DingTalk)
|
||||
|
||||
export type DingTalkTextMessage = {
|
||||
msgtype: "text";
|
||||
text: {
|
||||
content: string;
|
||||
};
|
||||
at?: {
|
||||
atMobiles?: string[];
|
||||
atUserIds?: string[];
|
||||
isAtAll?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type DingTalkLinkMessage = {
|
||||
msgtype: "link";
|
||||
link: {
|
||||
title: string;
|
||||
text: string;
|
||||
messageUrl: string;
|
||||
picUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DingTalkMarkdownMessage = {
|
||||
msgtype: "markdown";
|
||||
markdown: {
|
||||
title: string;
|
||||
text: string;
|
||||
};
|
||||
at?: {
|
||||
atMobiles?: string[];
|
||||
atUserIds?: string[];
|
||||
isAtAll?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type DingTalkActionCardMessage = {
|
||||
msgtype: "actionCard";
|
||||
actionCard: {
|
||||
title: string;
|
||||
text: string;
|
||||
singleTitle?: string;
|
||||
singleURL?: string;
|
||||
btnOrientation?: "0" | "1";
|
||||
btns?: Array<{
|
||||
title: string;
|
||||
actionURL: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type DingTalkFeedCardMessage = {
|
||||
msgtype: "feedCard";
|
||||
feedCard: {
|
||||
links: Array<{
|
||||
title: string;
|
||||
messageURL: string;
|
||||
picURL: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type DingTalkOutboundMessage =
|
||||
| DingTalkTextMessage
|
||||
| DingTalkLinkMessage
|
||||
| DingTalkMarkdownMessage
|
||||
| DingTalkActionCardMessage
|
||||
| DingTalkFeedCardMessage;
|
||||
|
||||
// API response types
|
||||
|
||||
export type DingTalkApiResponse = {
|
||||
errcode: number;
|
||||
errmsg: string;
|
||||
};
|
||||
|
||||
// Stream mode message types (from dingtalk-stream SDK)
|
||||
|
||||
export type DingTalkStreamMessage = {
|
||||
/** Platform of sender */
|
||||
senderPlatform?: string;
|
||||
/** Conversation ID */
|
||||
conversationId: string;
|
||||
/** At users list */
|
||||
atUsers?: Array<{
|
||||
dingtalkId: string;
|
||||
staffId?: string;
|
||||
}>;
|
||||
/** Chat bot corp ID */
|
||||
chatbotCorpId?: string;
|
||||
/** Message ID */
|
||||
msgId: string;
|
||||
/** Sender nickname */
|
||||
senderNick?: string;
|
||||
/** Whether sender is admin */
|
||||
isAdmin?: boolean;
|
||||
/** Sender staff ID (user ID) */
|
||||
senderStaffId: string;
|
||||
/** Session webhook URL for replying */
|
||||
sessionWebhook: string;
|
||||
/** Session webhook expiration timestamp */
|
||||
sessionWebhookExpiredTime?: number;
|
||||
/** Conversation type: "1" = single chat, "2" = group chat */
|
||||
conversationType?: "1" | "2";
|
||||
/** Conversation title (group name for group chat) */
|
||||
conversationTitle?: string;
|
||||
/** Text content */
|
||||
text?: {
|
||||
content: string;
|
||||
};
|
||||
/** Robot code */
|
||||
robotCode?: string;
|
||||
/** Message type: text, image, voice, file, link, markdown, etc. */
|
||||
msgtype: string;
|
||||
/** Whether robot was @mentioned (in the atUsers list) */
|
||||
isInAtList?: boolean;
|
||||
/** Sender corp ID */
|
||||
senderCorpId?: string;
|
||||
/** Message creation timestamp */
|
||||
createAt?: number;
|
||||
};
|
||||
|
||||
// Local config type with additional legacy fields (for backward compatibility)
|
||||
export type DingTalkLocalConfig = {
|
||||
accounts?: Record<string, unknown>;
|
||||
defaultAccount?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
};
|
||||
295
pnpm-lock.yaml
generated
295
pnpm-lock.yaml
generated
@ -302,6 +302,16 @@ importers:
|
||||
specifier: ^1.39.0
|
||||
version: 1.39.0
|
||||
|
||||
extensions/dingtalk:
|
||||
dependencies:
|
||||
dingtalk-stream:
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4
|
||||
devDependencies:
|
||||
moltbot:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
|
||||
extensions/discord: {}
|
||||
|
||||
extensions/google-antigravity-auth: {}
|
||||
@ -383,12 +393,12 @@ importers:
|
||||
'@microsoft/agents-hosting-extensions-teams':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
moltbot:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
moltbot:
|
||||
specifier: workspace:*
|
||||
version: link:../..
|
||||
proper-lockfile:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
@ -1126,89 +1136,105 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@ -1277,48 +1303,6 @@ packages:
|
||||
'@kwsites/promise-deferred@1.1.1':
|
||||
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
|
||||
|
||||
'@lancedb/lancedb-darwin-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-8w0sMCNMwBv2kv5+fczGeSVlNOL+BOKChSsO4usM0hMw3PmxasONPctQBsESDuPS8lQ6/AKAQc2HT/ddd5Mg5w==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@lancedb/lancedb-linux-arm64-gnu@0.23.0':
|
||||
resolution: {integrity: sha512-+xse2IspO7hbuHT4H62q8Ct00fTojnuBxXp1X1I3/27dDvW8E+/itFiJuTZ0YMaJc7nNr9qh9YFXZ9hZdEmReg==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@lancedb/lancedb-linux-arm64-musl@0.23.0':
|
||||
resolution: {integrity: sha512-c2UCtGoYjA3oDdw5y3RLK7J2th3rSjYBng+1I03vU9g092y8KATAJO/lV2AtyxSC+esSuyY1dMEaj8ADcXjZAA==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@lancedb/lancedb-linux-x64-gnu@0.23.0':
|
||||
resolution: {integrity: sha512-OPL7tK3JCTx43ZxvbVs+CljfCer0KrojANQbcJ2V4VAp6XBhKx1sBAlIVGuCrd93pA8UOUP3iHsM7aglPo6rCg==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@lancedb/lancedb-linux-x64-musl@0.23.0':
|
||||
resolution: {integrity: sha512-1ZEoQDwOrKvwPyAG+95/r1NYqX8Ca5bRek8Vr62CzWCEmHd/pFeEGWZ5STrkh+Bt3GLdi2JOivFtRbmuBAJypQ==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@lancedb/lancedb-win32-arm64-msvc@0.23.0':
|
||||
resolution: {integrity: sha512-OuD1mkrgXvijRlXdbx3LvfuorO04FD5qHegnTOWGXh1sIwwrvvhcJAvXUGBNLY4n/lsWvA+xTjtMwRjUitvPKg==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@lancedb/lancedb-win32-x64-msvc@0.23.0':
|
||||
resolution: {integrity: sha512-5ve1hvVtp8zWxSE9A+MOQaicXl2Rn0ZG/NUaMTjTD3/CQHPKFmtrqDnM5khoPICTj2O2b10F6mn4cUzl5PASgA==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@lancedb/lancedb@0.23.0':
|
||||
resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@ -1398,24 +1382,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@mariozechner/clipboard-linux-riscv64-gnu@0.3.0':
|
||||
resolution: {integrity: sha512-4BC08CIaOXSSAGRZLEjqJmQfioED8ohAzwt0k2amZPEbH96YKoBNorq5EdwPf5VT+odS0DeyCwhwtxokRLZIvQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@mariozechner/clipboard-linux-x64-gnu@0.3.0':
|
||||
resolution: {integrity: sha512-GpNY5Y9nOzr0Vt0Qi5U88qwe6piiIHk44kSMexl8ns90LluN5UTNYmyfi7Xq3/lmPZCpnB2xvBTYbsXCxnopIA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@mariozechner/clipboard-linux-x64-musl@0.3.0':
|
||||
resolution: {integrity: sha512-+PnR48/x9GMY5Kh8BLjzHMx6trOegMtxAuqTM9X/bhV3QuW6sLLd7nojDHSGj/ZueK6i0tcQxvOrgNLozVtNDA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@mariozechner/clipboard-win32-arm64-msvc@0.3.0':
|
||||
resolution: {integrity: sha512-+dy2vZ1Ph4EYj0cotB+bVUVk/uKl2bh9LOp/zlnFqoCCYDN6sm+L0VyIOPPo3hjoEVdGpHe1MUxp3qG/OLwXgg==}
|
||||
@ -1516,30 +1504,35 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
|
||||
resolution: {integrity: sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
|
||||
resolution: {integrity: sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
|
||||
resolution: {integrity: sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.88':
|
||||
resolution: {integrity: sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
|
||||
resolution: {integrity: sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==}
|
||||
@ -1585,36 +1578,28 @@ packages:
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64, x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@node-llama-cpp/linux-armv7l@3.15.0':
|
||||
resolution: {integrity: sha512-ZuZ3q6mejQnEP4o22la7zBv7jNR+IZfgItDm3KjAl04HUXTKJ43HpNwjnf9GyYYd+dEgtoX0MESvWz4RnGH8Jw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm, x64]
|
||||
os: [linux]
|
||||
|
||||
'@node-llama-cpp/linux-x64-cuda-ext@3.15.0':
|
||||
resolution: {integrity: sha512-wQwgSl7Qm8vH56oBt7IuWWDNNsDECkVMS000C92wl3PkbzjwZFiWzehwa+kF8Lr2BBMiCJNkI5nEabhYH3RN2Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@node-llama-cpp/linux-x64-cuda@3.15.0':
|
||||
resolution: {integrity: sha512-mDjyVulCTRYilm9Emm3lDMx7dbI1vzGqk28Pj28shartjERTUu8aUNDYOmVKNMLpUKS1akw7vy0lMF8t4qswxQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@node-llama-cpp/linux-x64-vulkan@3.15.0':
|
||||
resolution: {integrity: sha512-htVIthQKq/rr8v5e7NiVtcHsstqTBAAC50kUymmDMbrzAu6d/EHacCJpNbU57b1UUa1nKN5cBqr6Jr+QqEalMA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@node-llama-cpp/linux-x64@3.15.0':
|
||||
resolution: {integrity: sha512-etUuTqSyNefRObqc5+JZviNTkuef2XEtHcQLaamEIWwjI1dj7nTD2YMZPBP7H3M3E55HSIY82vqCQ1bp6ZILiA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@node-llama-cpp/mac-arm64-metal@3.15.0':
|
||||
resolution: {integrity: sha512-3Vkq6bpyQZaIzoaLLP7H2Tt8ty5BS0zxUY2pX0ox2S9P4fp8Au0CCJuUJF4V+EKi+/PTn70A6R1QCkRMfMQJig==}
|
||||
@ -1634,24 +1619,6 @@ packages:
|
||||
cpu: [arm64, x64]
|
||||
os: [win32]
|
||||
|
||||
'@node-llama-cpp/win-x64-cuda-ext@3.15.0':
|
||||
resolution: {integrity: sha512-KQoNH9KsVtqGVXaRdPrnHPrg5w3KOM7CfynPmG1m16gmjmDSIspdPg/Dbg6DgHBfkdAzt+duRZEBk8Bg8KbDHw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@node-llama-cpp/win-x64-cuda@3.15.0':
|
||||
resolution: {integrity: sha512-2Kyu1roDwXwFLaJgGZQISIXP9lCDZtJCx/DRcmrYRHcSUFCzo5ikOuAECyliSSQmRUAvvlRCuD+GrTcegbhojA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@node-llama-cpp/win-x64-vulkan@3.15.0':
|
||||
resolution: {integrity: sha512-sH+K7lO49WrUvCCC3RPddCBrn2ZQwKCXKL90P/NZicMRgxTPFZEVSU2jXR/bu1K8B+4lNN+z5OEbjSYs7cKEcA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@node-llama-cpp/win-x64@3.15.0':
|
||||
resolution: {integrity: sha512-gWhtc8l3HOry5guO46YfFohLQnF0NfL4On0GAO8E27JiYYxHO9nHSCfFif4+U03+FfHquZXL0znJ1qPVOiwOPw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@ -1962,21 +1929,25 @@ packages:
|
||||
resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/linux-arm64-musl@0.26.0':
|
||||
resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/linux-x64-gnu@0.26.0':
|
||||
resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/linux-x64-musl@0.26.0':
|
||||
resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/win32-arm64@0.26.0':
|
||||
resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==}
|
||||
@ -2032,21 +2003,25 @@ packages:
|
||||
resolution: {integrity: sha512-Fow7H84Bs8XxuaK1yfSEWBC8HI7rfEQB9eR2A0J61un1WgCas7jNrt1HbT6+p6KmUH2bhR+r/RDu/6JFAvvj4g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.41.0':
|
||||
resolution: {integrity: sha512-WoRRDNwgP5W3rjRh42Zdx8ferYnqpKoYCv2QQLenmdrLjRGYwAd52uywfkcS45mKEWHeY1RPwPkYCSROXiGb2w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.41.0':
|
||||
resolution: {integrity: sha512-75k3CKj3fOc/a/2aSgO81s3HsTZOFROthPJ+UI2Oatic1LhvH6eKjKfx3jDDyVpzeDS2qekPlc/y3N33iZz5Og==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/linux-x64-musl@1.41.0':
|
||||
resolution: {integrity: sha512-8r82eBwGPoAPn67ZvdxTlX/Z3gVb+ZtN6nbkyFzwwHWAh8yGutX+VBcVkyrePSl6XgBP4QAaddPnHmkvJjqY0g==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/win32-arm64@1.41.0':
|
||||
resolution: {integrity: sha512-aK+DAcckQsNCOXKruatyYuY/ROjNiRejQB1PeJtkZwM21+8rV9ODYbvKNvt0pW+YCws7svftBSFMCpl3ke2unw==}
|
||||
@ -2118,24 +2093,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@reflink/reflink-linux-arm64-musl@0.1.19':
|
||||
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@reflink/reflink-linux-x64-gnu@0.1.19':
|
||||
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@reflink/reflink-linux-x64-musl@0.1.19':
|
||||
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@reflink/reflink-win32-arm64-msvc@0.1.19':
|
||||
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
|
||||
@ -2188,24 +2167,28 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.1':
|
||||
resolution: {integrity: sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.1':
|
||||
resolution: {integrity: sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.1':
|
||||
resolution: {integrity: sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.1':
|
||||
resolution: {integrity: sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==}
|
||||
@ -2267,66 +2250,79 @@ packages:
|
||||
resolution: {integrity: sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.55.3':
|
||||
resolution: {integrity: sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.55.3':
|
||||
resolution: {integrity: sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.55.3':
|
||||
resolution: {integrity: sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.55.3':
|
||||
resolution: {integrity: sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.55.3':
|
||||
resolution: {integrity: sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.55.3':
|
||||
resolution: {integrity: sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.55.3':
|
||||
resolution: {integrity: sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.55.3':
|
||||
resolution: {integrity: sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==}
|
||||
@ -3214,11 +3210,6 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
clawdbot@2026.1.24-3:
|
||||
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3407,6 +3398,9 @@ packages:
|
||||
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dingtalk-stream@2.1.4:
|
||||
resolution: {integrity: sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==}
|
||||
|
||||
discord-api-types@0.38.37:
|
||||
resolution: {integrity: sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==}
|
||||
|
||||
@ -4097,24 +4091,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@ -6848,39 +6846,10 @@ snapshots:
|
||||
'@kwsites/promise-deferred@1.1.1':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-darwin-arm64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-linux-arm64-gnu@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-linux-arm64-musl@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-linux-x64-gnu@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-linux-x64-musl@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-win32-arm64-msvc@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb-win32-x64-msvc@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@lancedb/lancedb@0.23.0(apache-arrow@18.1.0)':
|
||||
dependencies:
|
||||
apache-arrow: 18.1.0
|
||||
reflect-metadata: 0.2.2
|
||||
optionalDependencies:
|
||||
'@lancedb/lancedb-darwin-arm64': 0.23.0
|
||||
'@lancedb/lancedb-linux-arm64-gnu': 0.23.0
|
||||
'@lancedb/lancedb-linux-arm64-musl': 0.23.0
|
||||
'@lancedb/lancedb-linux-x64-gnu': 0.23.0
|
||||
'@lancedb/lancedb-linux-x64-musl': 0.23.0
|
||||
'@lancedb/lancedb-win32-arm64-msvc': 0.23.0
|
||||
'@lancedb/lancedb-win32-x64-msvc': 0.23.0
|
||||
|
||||
'@line/bot-sdk@10.6.0':
|
||||
dependencies:
|
||||
@ -7189,12 +7158,6 @@ snapshots:
|
||||
'@node-llama-cpp/linux-armv7l@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/linux-x64-cuda-ext@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/linux-x64-cuda@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/linux-x64-vulkan@3.15.0':
|
||||
optional: true
|
||||
|
||||
@ -7210,15 +7173,6 @@ snapshots:
|
||||
'@node-llama-cpp/win-arm64@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/win-x64-cuda-ext@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/win-x64-cuda@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/win-x64-vulkan@3.15.0':
|
||||
optional: true
|
||||
|
||||
'@node-llama-cpp/win-x64@3.15.0':
|
||||
optional: true
|
||||
|
||||
@ -9098,84 +9052,6 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock': 3.975.0
|
||||
'@buape/carbon': 0.14.0(hono@4.11.4)
|
||||
'@clack/prompts': 0.11.0
|
||||
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
|
||||
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
|
||||
'@homebridge/ciao': 1.3.4
|
||||
'@line/bot-sdk': 10.6.0
|
||||
'@lydell/node-pty': 1.2.0-beta.3
|
||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.49.3
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@sinclair/typebox': 0.34.47
|
||||
'@slack/bolt': 4.6.0(@types/express@5.0.6)
|
||||
'@slack/web-api': 7.13.0
|
||||
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
|
||||
ajv: 8.17.1
|
||||
body-parser: 2.2.2
|
||||
chalk: 5.6.2
|
||||
chokidar: 5.0.0
|
||||
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
|
||||
cli-highlight: 2.1.11
|
||||
commander: 14.0.2
|
||||
croner: 9.1.0
|
||||
detect-libc: 2.1.2
|
||||
discord-api-types: 0.38.37
|
||||
dotenv: 17.2.3
|
||||
express: 5.2.1
|
||||
file-type: 21.3.0
|
||||
grammy: 1.39.3
|
||||
hono: 4.11.4
|
||||
jiti: 2.6.1
|
||||
json5: 2.2.3
|
||||
jszip: 3.10.1
|
||||
linkedom: 0.18.12
|
||||
long: 5.3.2
|
||||
markdown-it: 14.1.0
|
||||
node-edge-tts: 1.2.9
|
||||
osc-progress: 0.3.0
|
||||
pdfjs-dist: 5.4.530
|
||||
playwright-core: 1.58.0
|
||||
proper-lockfile: 4.1.2
|
||||
qrcode-terminal: 0.12.0
|
||||
sharp: 0.34.5
|
||||
sqlite-vec: 0.1.7-alpha.2
|
||||
tar: 7.5.4
|
||||
tslog: 4.10.2
|
||||
undici: 7.19.0
|
||||
ws: 8.19.0
|
||||
yaml: 2.8.2
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.88
|
||||
node-llama-cpp: 3.15.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@discordjs/opus'
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- '@types/express'
|
||||
- audio-decode
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- canvas
|
||||
- debug
|
||||
- devtools-protocol
|
||||
- encoding
|
||||
- ffmpeg-static
|
||||
- jimp
|
||||
- link-preview-js
|
||||
- node-opus
|
||||
- opusscript
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
@ -9349,6 +9225,16 @@ snapshots:
|
||||
|
||||
diff@8.0.3: {}
|
||||
|
||||
dingtalk-stream@2.1.4:
|
||||
dependencies:
|
||||
axios: 1.13.2(debug@4.4.3)
|
||||
debug: 4.4.3
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
discord-api-types@0.38.37: {}
|
||||
|
||||
docx-preview@0.3.7:
|
||||
@ -10526,16 +10412,11 @@ snapshots:
|
||||
'@node-llama-cpp/linux-arm64': 3.15.0
|
||||
'@node-llama-cpp/linux-armv7l': 3.15.0
|
||||
'@node-llama-cpp/linux-x64': 3.15.0
|
||||
'@node-llama-cpp/linux-x64-cuda': 3.15.0
|
||||
'@node-llama-cpp/linux-x64-cuda-ext': 3.15.0
|
||||
'@node-llama-cpp/linux-x64-vulkan': 3.15.0
|
||||
'@node-llama-cpp/mac-arm64-metal': 3.15.0
|
||||
'@node-llama-cpp/mac-x64': 3.15.0
|
||||
'@node-llama-cpp/win-arm64': 3.15.0
|
||||
'@node-llama-cpp/win-x64': 3.15.0
|
||||
'@node-llama-cpp/win-x64-cuda': 3.15.0
|
||||
'@node-llama-cpp/win-x64-cuda-ext': 3.15.0
|
||||
'@node-llama-cpp/win-x64-vulkan': 3.15.0
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { DingTalkConfig } from "./types.dingtalk.js";
|
||||
import type { DiscordConfig } from "./types.discord.js";
|
||||
import type { GoogleChatConfig } from "./types.googlechat.js";
|
||||
import type { IMessageConfig } from "./types.imessage.js";
|
||||
@ -25,6 +26,7 @@ export type ChannelDefaultsConfig = {
|
||||
|
||||
export type ChannelsConfig = {
|
||||
defaults?: ChannelDefaultsConfig;
|
||||
dingtalk?: DingTalkConfig;
|
||||
whatsapp?: WhatsAppConfig;
|
||||
telegram?: TelegramConfig;
|
||||
discord?: DiscordConfig;
|
||||
|
||||
90
src/config/types.dingtalk.ts
Normal file
90
src/config/types.dingtalk.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type {
|
||||
BlockStreamingCoalesceConfig,
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
MarkdownConfig,
|
||||
ReplyToMode,
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type DingTalkDmConfig = {
|
||||
/** If false, ignore all incoming DingTalk DMs. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Direct message access policy (default: pairing). */
|
||||
policy?: DmPolicy;
|
||||
/** Allowlist for DM senders (user IDs). */
|
||||
allowFrom?: Array<string | number>;
|
||||
};
|
||||
|
||||
export type DingTalkGroupConfig = {
|
||||
/** If false, disable the bot in this group. (Alias for allow: false.) */
|
||||
enabled?: boolean;
|
||||
/** Legacy group allow toggle; prefer enabled. */
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this group. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Allowlist of users that can invoke the bot in this group. */
|
||||
users?: Array<string | number>;
|
||||
/** Optional system prompt for this group. */
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type DingTalkAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Optional provider capability tags used for agent/runtime guidance. */
|
||||
capabilities?: string[];
|
||||
/** Markdown formatting overrides (tables). */
|
||||
markdown?: MarkdownConfig;
|
||||
/** Allow channel-initiated config writes (default: true). */
|
||||
configWrites?: boolean;
|
||||
/** If false, do not start this DingTalk account. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Stream mode: Client ID (AppKey). */
|
||||
clientId?: string;
|
||||
/** Stream mode: Client Secret (AppSecret). */
|
||||
clientSecret?: string;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||
allowBots?: boolean;
|
||||
/** Default mention requirement for group chats (default: true). */
|
||||
requireMention?: boolean;
|
||||
/**
|
||||
* Controls how group chats are handled:
|
||||
* - "open": groups bypass allowlists; mention-gating applies
|
||||
* - "disabled": block all group chats
|
||||
* - "allowlist": only allow groups present in channels.dingtalk.groups
|
||||
*/
|
||||
groupPolicy?: GroupPolicy;
|
||||
/** Max group messages to keep as history context (0 disables). */
|
||||
historyLimit?: number;
|
||||
/** Max DM turns to keep as history context. */
|
||||
dmHistoryLimit?: number;
|
||||
/** Per-DM config overrides keyed by user ID. */
|
||||
dms?: Record<string, DmConfig>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreaming?: boolean;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
/** Max media size in MB. */
|
||||
mediaMaxMb?: number;
|
||||
/** Control reply threading when reply tags are present (off|first|all). */
|
||||
replyToMode?: ReplyToMode;
|
||||
dm?: DingTalkDmConfig;
|
||||
groups?: Record<string, DingTalkGroupConfig>;
|
||||
/** Heartbeat visibility settings for this channel. */
|
||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||
};
|
||||
|
||||
export type DingTalkConfig = {
|
||||
/** Optional per-account DingTalk configuration (multi-account). */
|
||||
accounts?: Record<string, DingTalkAccountConfig>;
|
||||
} & DingTalkAccountConfig;
|
||||
@ -9,6 +9,7 @@ export * from "./types.browser.js";
|
||||
export * from "./types.channels.js";
|
||||
export * from "./types.clawdbot.js";
|
||||
export * from "./types.cron.js";
|
||||
export * from "./types.dingtalk.js";
|
||||
export * from "./types.discord.js";
|
||||
export * from "./types.googlechat.js";
|
||||
export * from "./types.gateway.js";
|
||||
|
||||
@ -770,3 +770,65 @@ export const MSTeamsConfigSchema = z
|
||||
'channels.msteams.dmPolicy="open" requires channels.msteams.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
// DingTalk schemas
|
||||
export const DingTalkDmSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
policy: DmPolicySchema.optional().default("pairing"),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.policy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.dingtalk.dm.policy="open" requires channels.dingtalk.dm.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
export const DingTalkGroupSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const DingTalkAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
allowBots: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
replyToMode: ReplyToModeSchema.optional(),
|
||||
dm: DingTalkDmSchema.optional(),
|
||||
groups: z.record(z.string(), DingTalkGroupSchema.optional()).optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const DingTalkConfigSchema = DingTalkAccountSchema.extend({
|
||||
accounts: z.record(z.string(), DingTalkAccountSchema.optional()).optional(),
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import {
|
||||
BlueBubblesConfigSchema,
|
||||
DingTalkConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
@ -27,6 +28,7 @@ export const ChannelsSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
dingtalk: DingTalkConfigSchema.optional(),
|
||||
whatsapp: WhatsAppConfigSchema.optional(),
|
||||
telegram: TelegramConfigSchema.optional(),
|
||||
discord: DiscordConfigSchema.optional(),
|
||||
|
||||
@ -84,6 +84,10 @@ export type {
|
||||
GroupToolPolicyBySenderConfig,
|
||||
MarkdownConfig,
|
||||
MarkdownTableMode,
|
||||
DingTalkAccountConfig,
|
||||
DingTalkConfig,
|
||||
DingTalkDmConfig,
|
||||
DingTalkGroupConfig,
|
||||
GoogleChatAccountConfig,
|
||||
GoogleChatConfig,
|
||||
GoogleChatDmConfig,
|
||||
@ -95,6 +99,8 @@ export type {
|
||||
MSTeamsTeamConfig,
|
||||
} from "../config/types.js";
|
||||
export {
|
||||
DingTalkConfigSchema,
|
||||
DingTalkAccountSchema,
|
||||
DiscordConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user