@ozaiya/openclaw-channel 0.10.51 → 0.10.53

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.
@@ -30,8 +30,9 @@ import { summarizeWithYuanbao } from "./yuanbao.js";
30
30
  import { summarizeWithDoubao } from "./doubao.js";
31
31
  import { fetchXueqiuPost, searchXueqiuPosts } from "./xueqiu.js";
32
32
  import { fetchSocialMediaPost, searchSocialMedia, extractSocialMediaContent } from "./socialMedia.js";
33
- import { requestConfirmation, parseActionCallback, resolveConfirmation } from "./actionConfirmation.js";
33
+ import { requestConfirmation, requestLastingConfirmation, parseActionCallback, resolveConfirmation, restoreParkedActions, takeParkedAction } from "./actionConfirmation.js";
34
34
  import { STAGED_NAME_SUFFIX, createBotEmailTool, emailAddonOf, newMailNotice, watchMailboxes } from "./botEmail.js";
35
+ import { cancelScheduledMail, restoreScheduledMails, scheduleMail, scheduledMails } from "./mailSchedule.js";
35
36
  import { createBotMcpTool, mcpAddonOf } from "./botMcp.js";
36
37
  import { lastChatFromSessionStore } from "./lastChat.js";
37
38
  import { writeBotLocalSettings } from "./botLocalSettings.js";
@@ -109,6 +110,8 @@ function categorize(toolName) {
109
110
  }
110
111
  // Active dispatches keyed by accountId (one dispatch at a time per account)
111
112
  const activeDispatches = new Map();
113
+ /** Bots whose parked approval cards were restored after this gateway started. */
114
+ const parkedRestored = new Set();
112
115
  /** Bot id → the chat a new-mail turn is running in (the owner's chat with the bot). */
113
116
  const mailTurns = new Map();
114
117
  /**
@@ -1121,6 +1124,43 @@ async function sendEncryptedChatContent(params) {
1121
1124
  recordState(account.accountId, { lastOutboundAt: Date.now() });
1122
1125
  return result;
1123
1126
  }
1127
+ /** Deps for a lasting card (mail, business-system writes): parked actions live in the gateway's state dir. */
1128
+ function makeLastingDeps(account) {
1129
+ return { ...makeConfirmationDeps(account), accountId: account.accountId, stateDir: getOzaiyaRuntime().state.resolveStateDir() };
1130
+ }
1131
+ /** A lasting card's answer as the tools take it: true / false now, or "later" (runs on a later Approve). */
1132
+ async function askOwnerLasting(account, groupId, kind, label, params, args) {
1133
+ if (!groupId)
1134
+ return false;
1135
+ const answer = await requestLastingConfirmation(makeLastingDeps(account), groupId, kind, params, label, { tool: kind, args });
1136
+ return answer === "later" ? "later" : answer === "approved";
1137
+ }
1138
+ /** The mcp tool for this bot; its writes ask on a lasting card in the owner's chat. */
1139
+ function createMcpToolFor(account, config) {
1140
+ return createBotMcpTool(account, config, {
1141
+ askOwner: (label, params, args) => askOwnerLasting(account, config.ownerGroupId, "mcp", label, params, args),
1142
+ });
1143
+ }
1144
+ /**
1145
+ * The owner approved a parked card after its first wait: do it now with the tool it came from,
1146
+ * approval already given, and post what came of it in that chat.
1147
+ */
1148
+ async function runParkedAction(account, cfg, action) {
1149
+ const zh = /[\u3400-\u9fff]/.test(`${action.label} ${JSON.stringify(action.card.params)}`);
1150
+ let result;
1151
+ if (action.tool === "email") {
1152
+ const config = emailAddonOf(account);
1153
+ result = config ? await createEmailToolFor(account, config, cfg).runApproved(action.args) : zh ? "邮箱 add-on 已关闭,没有执行。" : "The email add-on is off; nothing was done.";
1154
+ }
1155
+ else {
1156
+ const config = mcpAddonOf(account);
1157
+ result = config ? await createMcpToolFor(account, config).runApproved(action.args) : zh ? "业务系统 add-on 已关闭,没有执行。" : "The business-system add-on is off; nothing was done.";
1158
+ }
1159
+ // What the tool tells the model, minus the data markers meant for it.
1160
+ const shown = result.replace(/^<<(end )?external [^>]*>>\n?/gm, "").trim();
1161
+ const text = `${zh ? "✅ 已按你的同意执行" : "✅ Done on your approval"}:${action.label}\n${shown.length > 1500 ? `${shown.slice(0, 1500)}…` : shown}`;
1162
+ await sendEncryptedChatContent({ account, groupId: action.groupId, content: { text } }).catch(() => { });
1163
+ }
1124
1164
  /** Build ConfirmationDeps for the action confirmation module. */
1125
1165
  function makeConfirmationDeps(account) {
1126
1166
  return {
@@ -1571,11 +1611,8 @@ function buildChannelTools(account, cfg) {
1571
1611
  tools.push(createEmailToolFor(account, emailConfig, cfg));
1572
1612
  // The owner's business systems over MCP (mcp add-on): one tool, write operations behind a card.
1573
1613
  const mcpConfig = mcpAddonOf(account);
1574
- if (mcpConfig) {
1575
- tools.push(createBotMcpTool(account, mcpConfig, {
1576
- askOwner: async (label, params) => !!mcpConfig.ownerGroupId && requestConfirmation(makeConfirmationDeps(account), mcpConfig.ownerGroupId, "mcp", params, label),
1577
- }));
1578
- }
1614
+ if (mcpConfig)
1615
+ tools.push(createMcpToolFor(account, mcpConfig));
1579
1616
  // Plan tool: lets the agent declare + maintain a high-level task plan that the
1580
1617
  // app renders as a live "task progress N/M" checklist (distinct from tool steps).
1581
1618
  tools.push({
@@ -2204,6 +2241,11 @@ export const ozaiyaPlugin = {
2204
2241
  });
2205
2242
  ctx.log?.info(`[${botAccount.accountId}] registered webhook at ${botAccount.webhookPath}`);
2206
2243
  syncMailWatch(botAccount, ctx.cfg, stateDir, ctx.log);
2244
+ if (!parkedRestored.has(botAccount.accountId)) {
2245
+ parkedRestored.add(botAccount.accountId);
2246
+ void restoreParkedActions(makeLastingDeps(botAccount)).catch(() => { });
2247
+ void restoreScheduledMails(mailScheduleDeps(botAccount, ctx.cfg)).catch(() => { });
2248
+ }
2207
2249
  // The app's model + add-on settings, for helpers beside the gateway (e.g. a 闲鱼 inbox watcher).
2208
2250
  writeBotLocalSettings(stateDir, botAccount).catch((err) => {
2209
2251
  ctx.log?.warn?.(`[${botAccount.accountId}] could not write local settings: ${String(err)}`);
@@ -3248,11 +3290,7 @@ function createEmailToolFor(account, config, cfg) {
3248
3290
  const running = [activeDispatches.get(account.accountId)?.groupId, mailTurns.get(account.accountId)].filter(Boolean);
3249
3291
  return running.length > 0 && running.every((groupId) => groupId === ownerGroupId);
3250
3292
  },
3251
- askOwner: async (label, params) => {
3252
- if (!ownerGroupId)
3253
- return false;
3254
- return requestConfirmation(makeConfirmationDeps(account), ownerGroupId, "email", params, label);
3255
- },
3293
+ askOwner: (label, params, args) => askOwnerLasting(account, ownerGroupId, "email", label, params, args),
3256
3294
  // Files the owner sends in that chat are staged here (stageInboundAttachmentsForAgent).
3257
3295
  ownerFiles: () => ownerGroupId
3258
3296
  ? path.join(getOzaiyaRuntime().state.resolveStateDir(), "media", "ozaiya", sanitizePathSegment(ownerGroupId, "group"))
@@ -3263,8 +3301,33 @@ function createEmailToolFor(account, config, cfg) {
3263
3301
  const uploaded = await uploadFile(account.apiBaseUrl, account.botToken, ownerGroupId, file.name, resolveAttachmentMime(file.name, file.mime), file.data);
3264
3302
  await sendEncryptedChatContent({ account, groupId: ownerGroupId, content: { files: [uploaded], ...(file.caption ? { text: file.caption } : {}) } });
3265
3303
  },
3304
+ schedule: async (entry) => {
3305
+ if (!ownerGroupId)
3306
+ throw new Error("no chat with the owner");
3307
+ return (await scheduleMail(mailScheduleDeps(account, cfg), { ...entry, groupId: ownerGroupId })).id;
3308
+ },
3309
+ scheduledList: () => scheduledMails(account.accountId),
3310
+ cancelScheduled: (id) => cancelScheduledMail(mailScheduleDeps(account, cfg), id),
3266
3311
  });
3267
3312
  }
3313
+ /**
3314
+ * Scheduled sending for a bot: at the time, the email tool — built then, from the bot's config as it
3315
+ * is by then — sends with approval already given, and the outcome goes to the owner's chat.
3316
+ */
3317
+ function mailScheduleDeps(account, cfg) {
3318
+ return {
3319
+ accountId: account.accountId,
3320
+ stateDir: getOzaiyaRuntime().state.resolveStateDir(),
3321
+ send: async (args) => {
3322
+ const current = gatewayBotAccounts.get(account.accountId) ?? account;
3323
+ const config = emailAddonOf(current);
3324
+ return config ? createEmailToolFor(current, config, cfg).runApproved(args) : "Failed: the email add-on is off.";
3325
+ },
3326
+ notify: async (groupId, text) => {
3327
+ await sendEncryptedChatContent({ account: gatewayBotAccounts.get(account.accountId) ?? account, groupId, content: { text } });
3328
+ },
3329
+ };
3330
+ }
3268
3331
  /**
3269
3332
  * New mail reached the bot's own mailbox: run a turn in the owner's chat with the bot, so it tells
3270
3333
  * them and, where it may, answers. The mail's headers come in marked as outside data; the agent
@@ -3372,6 +3435,14 @@ function syncMailWatch(account, cfg, stateDir, log) {
3372
3435
  const stop = watchMailboxes(account, {
3373
3436
  stateDir,
3374
3437
  log: { info: (msg) => log?.info?.(msg), warn: (msg) => log?.warn?.(msg) },
3438
+ onLoginRejected: async (mailbox, detail) => {
3439
+ const groupId = config?.ownerGroupId;
3440
+ if (!groupId)
3441
+ return;
3442
+ const text = `⚠️ 邮箱 ${mailbox.email} 登录被邮件服务器拒绝(${detail}),暂时收不到新邮件。\n`
3443
+ + "如果改过这个邮箱的密码,请在 app 里把邮箱重新添加一次;如果没改过,可能是服务商暂时限制了登录,我会每 30 分钟重试一次,恢复后自动继续。";
3444
+ await sendEncryptedChatContent({ account: gatewayBotAccounts.get(botId) ?? account, groupId, content: { text } });
3445
+ },
3375
3446
  onNewMail: (mailbox, mails) => {
3376
3447
  // One new-mail turn at a time per bot, with the bot's config as it is by then.
3377
3448
  const next = (mailTurnQueues.get(botId) ?? Promise.resolve())
@@ -4529,6 +4600,13 @@ ctx) {
4529
4600
  const resolved = await resolveConfirmation(deps, actionCallback.actionId, actionCallback.approved);
4530
4601
  if (resolved)
4531
4602
  return; // intercepted — don't dispatch to agent
4603
+ // A lasting card answered after its first wait: run the parked action now.
4604
+ const late = await takeParkedAction(makeLastingDeps(account), actionCallback.actionId, actionCallback.approved);
4605
+ if (late.found) {
4606
+ if (late.run)
4607
+ await runParkedAction(account, ctx.cfg, late.run);
4608
+ return;
4609
+ }
4532
4610
  }
4533
4611
  // Intercept task-control callbacks from the chat task card.
4534
4612
  // oz_stop:<taskId> → abort the running dispatch mid-flight (interrupts the LLM turn)