@ozaiya/openclaw-channel 0.10.35 → 0.10.37

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.
@@ -117,6 +117,7 @@ function buildTaskProgressContent(dispatch, completed) {
117
117
  completedAt: s.completedAt,
118
118
  })),
119
119
  completed,
120
+ stopped: dispatch.stopped,
120
121
  plan: dispatch.plan,
121
122
  deliverables: dispatch.deliverables,
122
123
  live: !completed,
@@ -1486,21 +1487,26 @@ function buildChannelTools(account, cfg) {
1486
1487
  createMakePhoneCallTool(account, cfg),
1487
1488
  createHangUpCallTool(account),
1488
1489
  createStartInAppCallTool(account, cfg),
1490
+ createRequestUserApprovalTool(account),
1489
1491
  ];
1490
- const imageTool = createGenerateImageTool(account, cfg, resolveForGroup);
1492
+ // A bot hosted on Ozaiya pays per addon, so its addon-backed tools appear only once the
1493
+ // addon is on; a self-hosted gateway keeps every tool its own config enables.
1494
+ const addonOn = (key) => !account.hosted || !!account.addons?.[key];
1495
+ const imageTool = addonOn("image_gen") ? createGenerateImageTool(account, cfg, resolveForGroup) : null;
1491
1496
  if (imageTool)
1492
1497
  tools.push(imageTool);
1493
- tools.push(createDesktopTool());
1494
- const xueqiuTool = createFetchXueqiuTool(cfg);
1498
+ if (addonOn("wechat"))
1499
+ tools.push(createDesktopTool());
1500
+ const xueqiuTool = addonOn("xueqiu") ? createFetchXueqiuTool(cfg) : null;
1495
1501
  if (xueqiuTool)
1496
1502
  tools.push(xueqiuTool);
1497
- const xueqiuSearchTool = createSearchXueqiuTool(cfg);
1503
+ const xueqiuSearchTool = addonOn("xueqiu") ? createSearchXueqiuTool(cfg) : null;
1498
1504
  if (xueqiuSearchTool)
1499
1505
  tools.push(xueqiuSearchTool);
1500
- const scrapeTool = createScrapeSocialMediaTool(cfg);
1506
+ const scrapeTool = addonOn("social_media") ? createScrapeSocialMediaTool(cfg) : null;
1501
1507
  if (scrapeTool)
1502
1508
  tools.push(scrapeTool);
1503
- const summarizeTool = createSummarizeUrlTool(cfg);
1509
+ const summarizeTool = addonOn("web_research") ? createSummarizeUrlTool(cfg) : null;
1504
1510
  if (summarizeTool)
1505
1511
  tools.push(summarizeTool);
1506
1512
  // Plan tool: lets the agent declare + maintain a high-level task plan that the
@@ -1781,9 +1787,10 @@ export const ozaiyaPlugin = {
1781
1787
  resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
1782
1788
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
1783
1789
  isConfigured: (account) => {
1784
- // Gateway sentinel is configured if userToken is present
1790
+ // The gateway sentinel is only listed when a userToken (user gateway) or a provisionSecret
1791
+ // (fleet node, whose account has no botToken) is set, so being listed means configured.
1785
1792
  if (account.accountId === GATEWAY_ACCOUNT_ID) {
1786
- return Boolean(account.botToken?.trim());
1793
+ return true;
1787
1794
  }
1788
1795
  // Gateway-loaded bot accounts are always configured
1789
1796
  if (gatewayBotAccounts.has(account.accountId)) {
@@ -1795,7 +1802,7 @@ export const ozaiyaPlugin = {
1795
1802
  accountId: account.accountId,
1796
1803
  enabled: account.enabled,
1797
1804
  configured: account.accountId === GATEWAY_ACCOUNT_ID
1798
- ? Boolean(account.botToken?.trim())
1805
+ ? true
1799
1806
  : Boolean(account.botToken?.trim() && account.botPrivateKey?.trim()),
1800
1807
  webhookPath: account.webhookPath,
1801
1808
  baseUrl: account.apiBaseUrl,
@@ -3060,6 +3067,45 @@ function createStartInAppCallTool(account, cfg) {
3060
3067
  },
3061
3068
  };
3062
3069
  }
3070
+ /**
3071
+ * Ask the user before an action that can't be undone — paying, placing an order, submitting a
3072
+ * form with their details, posting or sending something in their name. Shows an Approve/Reject
3073
+ * card in the chat and waits for the answer (or the confirmation timeout).
3074
+ */
3075
+ function createRequestUserApprovalTool(account) {
3076
+ return {
3077
+ label: "Request User Approval",
3078
+ name: "request_user_approval",
3079
+ ownerOnly: false,
3080
+ description: "Ask the user to approve an action BEFORE doing it. Required before paying or placing an order, " +
3081
+ "submitting a form with their personal details, or sending a message, email or post on their behalf. " +
3082
+ "Shows Approve/Reject buttons in the chat and waits up to 5 minutes. Returns APPROVED or REJECTED; " +
3083
+ "only go ahead on APPROVED.",
3084
+ parameters: {
3085
+ type: "object",
3086
+ properties: {
3087
+ groupId: { type: "string", description: "The chat to ask in (the current conversation)." },
3088
+ action: { type: "string", description: "Short name of the action, e.g. \"Pay ¥128 for 2 movie tickets\"." },
3089
+ details: {
3090
+ type: "object",
3091
+ description: "Key facts the user should check, as short label → value pairs (amount, recipient, item, date…).",
3092
+ additionalProperties: { type: "string" },
3093
+ },
3094
+ },
3095
+ required: ["groupId", "action"],
3096
+ },
3097
+ execute: async (_toolCallId, rawArgs) => {
3098
+ const args = rawArgs;
3099
+ const params = {};
3100
+ for (const [key, value] of Object.entries(args.details ?? {})) {
3101
+ if (value !== undefined && value !== null && value !== "")
3102
+ params[key] = String(value).slice(0, 200);
3103
+ }
3104
+ const approved = await requestConfirmation(makeConfirmationDeps(account), args.groupId, "request_user_approval", params, String(args.action ?? "").slice(0, 120) || undefined);
3105
+ return { content: [{ type: "text", text: approved ? "APPROVED" : "REJECTED (or no answer in time) — do not do it." }] };
3106
+ },
3107
+ };
3108
+ }
3063
3109
  function createMakePhoneCallTool(account, cfg) {
3064
3110
  return {
3065
3111
  label: "Make Phone Call",
@@ -3406,6 +3452,7 @@ async function handlePhoneAutoTranscript(text, session, account, cfg, groupId) {
3406
3452
  const bodyForAgent = `${voicePrompt}\n\n${text}`;
3407
3453
  const body = `[ozaiya] from: phone caller | at: ${new Date().toISOString()}\n---\n${text}`;
3408
3454
  const msgCtx = ch.reply.finalizeInboundContext({
3455
+ GroupSystemPrompt: account.persona,
3409
3456
  Body: body,
3410
3457
  BodyForAgent: bodyForAgent,
3411
3458
  RawBody: text,
@@ -3819,6 +3866,7 @@ ctx) {
3819
3866
  });
3820
3867
  // Build finalized context for dispatch
3821
3868
  const msgCtx = ch.reply.finalizeInboundContext({
3869
+ GroupSystemPrompt: account.persona,
3822
3870
  Body: body,
3823
3871
  BodyForAgent: effectiveAgentInput,
3824
3872
  RawBody: agentInput,
@@ -3880,6 +3928,7 @@ ctx) {
3880
3928
  agentId: route.agentId,
3881
3929
  title: deriveTaskTitle(effectiveMessageText ?? agentInput),
3882
3930
  replyTexts: [],
3931
+ abortController: new AbortController(),
3883
3932
  };
3884
3933
  activeDispatches.set(account.accountId, dispatch);
3885
3934
  // Start screen recording for replay (best-effort; null if nothing recordable).
@@ -4033,9 +4082,23 @@ ctx) {
4033
4082
  ctx.log?.warn?.(`ozaiya: reply dispatch error: ${String(err)}`);
4034
4083
  },
4035
4084
  },
4036
- replyOptions: account.model ? { isHeartbeat: true, heartbeatModelOverride: account.model } : undefined,
4085
+ // The abortSignal lets a "stop" callback interrupt the running turn mid-flight.
4086
+ replyOptions: {
4087
+ abortSignal: dispatch.abortController.signal,
4088
+ ...(account.model ? { isHeartbeat: true, heartbeatModelOverride: account.model } : {}),
4089
+ },
4037
4090
  });
4038
4091
  }
4092
+ catch (err) {
4093
+ // A user-initiated stop aborts the running turn — that rejection is expected,
4094
+ // so don't surface it as a dispatch error (the card is finalized as "stopped" below).
4095
+ if (dispatch.stopped || dispatch.abortController.signal.aborted) {
4096
+ ctx.log?.info?.(`ozaiya: task ${dispatch.taskId} stopped by user`);
4097
+ }
4098
+ else {
4099
+ ctx.log?.warn?.(`ozaiya: reply dispatch threw: ${String(err)}`);
4100
+ }
4101
+ }
4039
4102
  finally {
4040
4103
  // Recover an inline table the model forgot to publish_artifact, BEFORE finalizing
4041
4104
  // so the final progress card includes the deliverable.
@@ -4183,6 +4246,52 @@ ctx) {
4183
4246
  if (resolved)
4184
4247
  return; // intercepted — don't dispatch to agent
4185
4248
  }
4249
+ // Intercept task-control callbacks from the chat task card.
4250
+ // oz_stop:<taskId> → abort the running dispatch mid-flight (interrupts the LLM turn)
4251
+ // oz_continue:<taskId> → resume the task with a fresh, fully-tracked dispatch
4252
+ if (callbackData.startsWith("oz_stop:")) {
4253
+ const taskId = callbackData.slice("oz_stop:".length);
4254
+ const dispatch = activeDispatches.get(account.accountId);
4255
+ if (dispatch && dispatch.taskId === taskId && !dispatch.abortController.signal.aborted) {
4256
+ dispatch.stopped = true; // makes finalizeTaskProgress emit a "stopped" card
4257
+ ctx.log?.info?.(`ozaiya: stop requested for task ${taskId} — aborting dispatch`);
4258
+ dispatch.abortController.abort();
4259
+ }
4260
+ else {
4261
+ ctx.log?.info?.(`ozaiya: stop requested for task ${taskId} but no matching live dispatch`);
4262
+ }
4263
+ return; // intercepted — don't dispatch to agent
4264
+ }
4265
+ if (callbackData.startsWith("oz_continue:")) {
4266
+ // Resume by feeding a synthetic "continue" message through the normal inbound
4267
+ // path, so it gets full task-progress tracking + a fresh live card. The agent's
4268
+ // session persists across dispatches, so it picks up with the task's full context.
4269
+ const continueText = "继续之前的任务,从你上次停下的地方接着做。\nContinue the previous task from where you stopped.";
4270
+ try {
4271
+ const groupKey = await getGroupKeyOrThrow(account, groupId);
4272
+ const synthetic = {
4273
+ event: "message.new",
4274
+ groupId,
4275
+ groupType: "group",
4276
+ replyAllowed: true,
4277
+ message: {
4278
+ id: `continue-${Date.now()}`,
4279
+ senderId: from,
4280
+ senderName: from,
4281
+ seq: 0,
4282
+ content: encryptMessage({ text: continueText }, groupKey),
4283
+ replyToId: null,
4284
+ createdAt: Date.now(),
4285
+ },
4286
+ };
4287
+ ctx.log?.info?.(`ozaiya: continue requested for task ${callbackData.slice("oz_continue:".length)} — re-dispatching`);
4288
+ await handleInboundMessage(synthetic, ctx);
4289
+ }
4290
+ catch (err) {
4291
+ ctx.log?.warn?.(`ozaiya: continue dispatch failed: ${String(err)}`);
4292
+ }
4293
+ return; // intercepted — don't run the lightweight callback dispatch
4294
+ }
4186
4295
  const runtime = getOzaiyaRuntime();
4187
4296
  const ch = runtime.channel;
4188
4297
  const { callbackBody, callbackCommand, callbackLabel, callbackOptionIndex } = normalizeCallbackQueryPayload({
@@ -4225,6 +4334,7 @@ ctx) {
4225
4334
  body: callbackBody,
4226
4335
  });
4227
4336
  const msgCtx = ch.reply.finalizeInboundContext({
4337
+ GroupSystemPrompt: account.persona,
4228
4338
  Body: body,
4229
4339
  BodyForAgent: callbackBody,
4230
4340
  RawBody: callbackBody,
@@ -4401,6 +4511,7 @@ ctx) {
4401
4511
  body: bodyForAgent,
4402
4512
  });
4403
4513
  const msgCtx = ch.reply.finalizeInboundContext({
4514
+ GroupSystemPrompt: account.persona,
4404
4515
  Body: body,
4405
4516
  BodyForAgent: bodyForAgent,
4406
4517
  RawBody: text,
@@ -4485,6 +4596,7 @@ ctx) {
4485
4596
  body: bodyForAgent,
4486
4597
  });
4487
4598
  const msgCtx = ch.reply.finalizeInboundContext({
4599
+ GroupSystemPrompt: account.persona,
4488
4600
  Body: body,
4489
4601
  BodyForAgent: bodyForAgent,
4490
4602
  RawBody: lastUserMessage,