@ozaiya/openclaw-channel 0.10.37 → 0.10.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,8 @@ import { summarizeWithDoubao } from "./doubao.js";
31
31
  import { fetchXueqiuPost, searchXueqiuPosts } from "./xueqiu.js";
32
32
  import { fetchSocialMediaPost, searchSocialMedia, extractSocialMediaContent } from "./socialMedia.js";
33
33
  import { requestConfirmation, parseActionCallback, resolveConfirmation } from "./actionConfirmation.js";
34
+ import { createBotEmailTool, emailAddonOf, newMailNotice, watchMailboxes } from "./botEmail.js";
35
+ import { createBotMcpTool, mcpAddonOf } from "./botMcp.js";
34
36
  const DEFAULT_API_BASE_URL = "https://api.ozaiya.com";
35
37
  const DEFAULT_WEBHOOK_PATH = "/ozaiya/webhook";
36
38
  const DEFAULT_ACCOUNT_ID = "default";
@@ -104,6 +106,15 @@ function categorize(toolName) {
104
106
  }
105
107
  // Active dispatches keyed by accountId (one dispatch at a time per account)
106
108
  const activeDispatches = new Map();
109
+ /** Bot id → the chat a new-mail turn is running in (the owner's chat with the bot). */
110
+ const mailTurns = new Map();
111
+ /** Bot id → its running mailbox watch, and the config it was started with. */
112
+ const mailWatches = new Map();
113
+ /** Bot id → new-mail turns run one after another. */
114
+ const mailTurnQueues = new Map();
115
+ /** Bot id → when its recent new-mail turns started (a flood of mail doesn't become a flood of turns). */
116
+ const mailTurnTimes = new Map();
117
+ const MAIL_TURNS_PER_HOUR = 20;
107
118
  function buildTaskProgressContent(dispatch, completed) {
108
119
  return {
109
120
  taskProgress: {
@@ -1509,6 +1520,17 @@ function buildChannelTools(account, cfg) {
1509
1520
  const summarizeTool = addonOn("web_research") ? createSummarizeUrlTool(cfg) : null;
1510
1521
  if (summarizeTool)
1511
1522
  tools.push(summarizeTool);
1523
+ // The bot's own mailbox (email add-on): there only once the owner has added one.
1524
+ const emailConfig = emailAddonOf(account);
1525
+ if (emailConfig)
1526
+ tools.push(createEmailToolFor(account, emailConfig));
1527
+ // The owner's business systems over MCP (mcp add-on): one tool, write operations behind a card.
1528
+ const mcpConfig = mcpAddonOf(account);
1529
+ if (mcpConfig) {
1530
+ tools.push(createBotMcpTool(account, mcpConfig, {
1531
+ askOwner: async (label, params) => !!mcpConfig.ownerGroupId && requestConfirmation(makeConfirmationDeps(account), mcpConfig.ownerGroupId, "mcp", params, label),
1532
+ }));
1533
+ }
1512
1534
  // Plan tool: lets the agent declare + maintain a high-level task plan that the
1513
1535
  // app renders as a live "task progress N/M" checklist (distinct from tool steps).
1514
1536
  tools.push({
@@ -2090,10 +2112,12 @@ export const ozaiyaPlugin = {
2090
2112
  recordState(botAccount.accountId, { running: false, lastStopAt: Date.now() });
2091
2113
  });
2092
2114
  ctx.log?.info(`[${botAccount.accountId}] registered webhook at ${botAccount.webhookPath}`);
2115
+ syncMailWatch(botAccount, ctx.cfg, stateDir, ctx.log);
2093
2116
  };
2094
2117
  const stopBotHandler = (botId) => {
2095
2118
  botUnregisters.get(botId)?.();
2096
2119
  botUnregisters.delete(botId);
2120
+ stopMailWatch(botId);
2097
2121
  gatewayBotAccounts.delete(botId);
2098
2122
  };
2099
2123
  await startGatewayMode({
@@ -2170,6 +2194,7 @@ export const ozaiyaPlugin = {
2170
2194
  groupToActivePhoneCall.clear();
2171
2195
  for (const id of gatewayBotAccounts.keys()) {
2172
2196
  recordState(id, { running: false, lastStopAt: Date.now() });
2197
+ stopMailWatch(id);
2173
2198
  }
2174
2199
  gatewayBotAccounts.clear();
2175
2200
  recordState(account.accountId, { running: false, lastStopAt: Date.now() });
@@ -3106,6 +3131,148 @@ function createRequestUserApprovalTool(account) {
3106
3131
  },
3107
3132
  };
3108
3133
  }
3134
+ // ─── The bot's own mailbox (email add-on) ───────────────────────────────────
3135
+ /**
3136
+ * The email tool for this bot. Its mailbox may only be looked into from the owner's own chat with
3137
+ * the bot — the chat a message or new-mail turn is running in, when that is the only one running —
3138
+ * and every approval card goes to that chat, whoever asked.
3139
+ */
3140
+ function createEmailToolFor(account, config) {
3141
+ const ownerGroupId = config.ownerGroupId;
3142
+ return createBotEmailTool(account, config, {
3143
+ inOwnerChat: () => {
3144
+ const running = [activeDispatches.get(account.accountId)?.groupId, mailTurns.get(account.accountId)].filter(Boolean);
3145
+ return !!ownerGroupId && running.length > 0 && running.every((groupId) => groupId === ownerGroupId);
3146
+ },
3147
+ askOwner: async (label, params) => {
3148
+ if (!ownerGroupId)
3149
+ return false;
3150
+ return requestConfirmation(makeConfirmationDeps(account), ownerGroupId, "email", params, label);
3151
+ },
3152
+ });
3153
+ }
3154
+ /**
3155
+ * New mail reached the bot's own mailbox: run a turn in the owner's chat with the bot, so it tells
3156
+ * them and, where it may, answers. The mail's headers come in marked as outside data; the agent
3157
+ * reads the rest with its email tool.
3158
+ */
3159
+ async function handleNewMail(account, cfg, log, mailbox, mails) {
3160
+ const config = emailAddonOf(account);
3161
+ const groupId = config?.ownerGroupId;
3162
+ if (!config || !groupId)
3163
+ return;
3164
+ const now = Date.now();
3165
+ const recent = (mailTurnTimes.get(account.accountId) ?? []).filter((t) => now - t < 3_600_000);
3166
+ if (recent.length >= MAIL_TURNS_PER_HOUR) {
3167
+ log?.warn?.(`ozaiya: [${account.accountId}] ${mails.length} new email(s) in ${mailbox.email} not announced: over ${MAIL_TURNS_PER_HOUR} new-mail turns this hour`);
3168
+ return;
3169
+ }
3170
+ mailTurnTimes.set(account.accountId, [...recent, now]);
3171
+ recordState(account.accountId, { lastInboundAt: now });
3172
+ await getGroupKeyOrThrow(account, groupId);
3173
+ const ch = getOzaiyaRuntime().channel;
3174
+ const route = ch.routing.resolveAgentRoute({
3175
+ cfg,
3176
+ channel: "ozaiya",
3177
+ accountId: account.accountId,
3178
+ peer: { kind: "group", id: groupId },
3179
+ });
3180
+ const fromAddress = `ozaiya:group:${groupId}`;
3181
+ const conversationLabel = `group:${groupId}`;
3182
+ const storePath = ch.session.resolveStorePath(undefined, { agentId: route.agentId });
3183
+ const notice = newMailNotice(mailbox, mails, config.replyWithoutAsking !== false);
3184
+ const body = ch.reply.formatAgentEnvelope({
3185
+ channel: "Ozaiya",
3186
+ from: `Email to ${mailbox.email} (${conversationLabel})`,
3187
+ timestamp: now,
3188
+ previousTimestamp: ch.session.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }),
3189
+ envelope: ch.reply.resolveEnvelopeFormatOptions(cfg),
3190
+ body: notice,
3191
+ });
3192
+ const msgCtx = ch.reply.finalizeInboundContext({
3193
+ GroupSystemPrompt: account.persona,
3194
+ Body: body,
3195
+ BodyForAgent: notice,
3196
+ RawBody: notice,
3197
+ // Mail text never runs as a command.
3198
+ CommandBody: "",
3199
+ From: fromAddress,
3200
+ To: fromAddress,
3201
+ SessionKey: route.sessionKey,
3202
+ AccountId: route.accountId,
3203
+ ChatType: "direct",
3204
+ ConversationLabel: conversationLabel,
3205
+ GroupSubject: groupId,
3206
+ SenderId: "ozaiya-email",
3207
+ SenderName: "Email",
3208
+ Provider: "ozaiya",
3209
+ Surface: "ozaiya",
3210
+ MessageSid: `email-${mailbox.id}-${mails[mails.length - 1]?.uid ?? 0}-${now}`,
3211
+ Timestamp: now,
3212
+ MessageText: notice,
3213
+ CommandAuthorized: false,
3214
+ OriginatingChannel: "ozaiya",
3215
+ OriginatingTo: fromAddress,
3216
+ });
3217
+ void ch.session.recordInboundSession({
3218
+ storePath,
3219
+ sessionKey: route.sessionKey,
3220
+ ctx: msgCtx,
3221
+ onRecordError: (err) => log?.warn?.(`ozaiya: failed recording new-mail session: ${String(err)}`),
3222
+ }).catch((err) => log?.warn?.(`ozaiya: failed recording new-mail session: ${String(err)}`));
3223
+ mailTurns.set(account.accountId, groupId);
3224
+ try {
3225
+ await ch.reply.dispatchReplyWithBufferedBlockDispatcher({
3226
+ ctx: msgCtx,
3227
+ cfg,
3228
+ dispatcherOptions: {
3229
+ deliver: async (replyPayload, _info) => {
3230
+ const replyText = replyPayload.text;
3231
+ if (!replyText?.trim())
3232
+ return;
3233
+ await sendEncryptedChatContent({ account, groupId, content: { text: replyText }, log });
3234
+ },
3235
+ onError: (err) => log?.warn?.(`ozaiya: new-mail turn error: ${String(err)}`),
3236
+ },
3237
+ replyOptions: account.model ? { isHeartbeat: true, heartbeatModelOverride: account.model } : undefined,
3238
+ });
3239
+ }
3240
+ finally {
3241
+ mailTurns.delete(account.accountId);
3242
+ }
3243
+ }
3244
+ /**
3245
+ * Start, restart or stop a bot's mailbox watch to match its email add-on (gateway mode). A reload
3246
+ * that leaves the mailboxes and the owner's chat as they were keeps the running watch.
3247
+ */
3248
+ function syncMailWatch(account, cfg, stateDir, log) {
3249
+ const config = emailAddonOf(account);
3250
+ const signature = config?.ownerGroupId ? JSON.stringify([config.accounts, config.ownerGroupId]) : "";
3251
+ const running = mailWatches.get(account.accountId);
3252
+ if (running?.signature === signature)
3253
+ return;
3254
+ stopMailWatch(account.accountId);
3255
+ if (!signature)
3256
+ return;
3257
+ const botId = account.accountId;
3258
+ const stop = watchMailboxes(account, {
3259
+ stateDir,
3260
+ log: { info: (msg) => log?.info?.(msg), warn: (msg) => log?.warn?.(msg) },
3261
+ onNewMail: (mailbox, mails) => {
3262
+ // One new-mail turn at a time per bot, with the bot's config as it is by then.
3263
+ const next = (mailTurnQueues.get(botId) ?? Promise.resolve())
3264
+ .then(() => handleNewMail(gatewayBotAccounts.get(botId) ?? account, cfg, log, mailbox, mails))
3265
+ .catch((err) => log?.warn?.(`ozaiya: [${botId}] new-mail turn failed: ${String(err)}`));
3266
+ mailTurnQueues.set(botId, next);
3267
+ return next;
3268
+ },
3269
+ });
3270
+ mailWatches.set(botId, { signature, stop });
3271
+ }
3272
+ function stopMailWatch(botId) {
3273
+ mailWatches.get(botId)?.stop();
3274
+ mailWatches.delete(botId);
3275
+ }
3109
3276
  function createMakePhoneCallTool(account, cfg) {
3110
3277
  return {
3111
3278
  label: "Make Phone Call",