@ozaiya/openclaw-channel 0.10.35 → 0.10.36

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,
@@ -1487,20 +1488,24 @@ function buildChannelTools(account, cfg) {
1487
1488
  createHangUpCallTool(account),
1488
1489
  createStartInAppCallTool(account, cfg),
1489
1490
  ];
1490
- const imageTool = createGenerateImageTool(account, cfg, resolveForGroup);
1491
+ // A bot hosted on Ozaiya pays per addon, so its addon-backed tools appear only once the
1492
+ // addon is on; a self-hosted gateway keeps every tool its own config enables.
1493
+ const addonOn = (key) => !account.hosted || !!account.addons?.[key];
1494
+ const imageTool = addonOn("image_gen") ? createGenerateImageTool(account, cfg, resolveForGroup) : null;
1491
1495
  if (imageTool)
1492
1496
  tools.push(imageTool);
1493
- tools.push(createDesktopTool());
1494
- const xueqiuTool = createFetchXueqiuTool(cfg);
1497
+ if (addonOn("wechat"))
1498
+ tools.push(createDesktopTool());
1499
+ const xueqiuTool = addonOn("xueqiu") ? createFetchXueqiuTool(cfg) : null;
1495
1500
  if (xueqiuTool)
1496
1501
  tools.push(xueqiuTool);
1497
- const xueqiuSearchTool = createSearchXueqiuTool(cfg);
1502
+ const xueqiuSearchTool = addonOn("xueqiu") ? createSearchXueqiuTool(cfg) : null;
1498
1503
  if (xueqiuSearchTool)
1499
1504
  tools.push(xueqiuSearchTool);
1500
- const scrapeTool = createScrapeSocialMediaTool(cfg);
1505
+ const scrapeTool = addonOn("social_media") ? createScrapeSocialMediaTool(cfg) : null;
1501
1506
  if (scrapeTool)
1502
1507
  tools.push(scrapeTool);
1503
- const summarizeTool = createSummarizeUrlTool(cfg);
1508
+ const summarizeTool = addonOn("web_research") ? createSummarizeUrlTool(cfg) : null;
1504
1509
  if (summarizeTool)
1505
1510
  tools.push(summarizeTool);
1506
1511
  // Plan tool: lets the agent declare + maintain a high-level task plan that the
@@ -3406,6 +3411,7 @@ async function handlePhoneAutoTranscript(text, session, account, cfg, groupId) {
3406
3411
  const bodyForAgent = `${voicePrompt}\n\n${text}`;
3407
3412
  const body = `[ozaiya] from: phone caller | at: ${new Date().toISOString()}\n---\n${text}`;
3408
3413
  const msgCtx = ch.reply.finalizeInboundContext({
3414
+ GroupSystemPrompt: account.persona,
3409
3415
  Body: body,
3410
3416
  BodyForAgent: bodyForAgent,
3411
3417
  RawBody: text,
@@ -3819,6 +3825,7 @@ ctx) {
3819
3825
  });
3820
3826
  // Build finalized context for dispatch
3821
3827
  const msgCtx = ch.reply.finalizeInboundContext({
3828
+ GroupSystemPrompt: account.persona,
3822
3829
  Body: body,
3823
3830
  BodyForAgent: effectiveAgentInput,
3824
3831
  RawBody: agentInput,
@@ -3880,6 +3887,7 @@ ctx) {
3880
3887
  agentId: route.agentId,
3881
3888
  title: deriveTaskTitle(effectiveMessageText ?? agentInput),
3882
3889
  replyTexts: [],
3890
+ abortController: new AbortController(),
3883
3891
  };
3884
3892
  activeDispatches.set(account.accountId, dispatch);
3885
3893
  // Start screen recording for replay (best-effort; null if nothing recordable).
@@ -4033,9 +4041,23 @@ ctx) {
4033
4041
  ctx.log?.warn?.(`ozaiya: reply dispatch error: ${String(err)}`);
4034
4042
  },
4035
4043
  },
4036
- replyOptions: account.model ? { isHeartbeat: true, heartbeatModelOverride: account.model } : undefined,
4044
+ // The abortSignal lets a "stop" callback interrupt the running turn mid-flight.
4045
+ replyOptions: {
4046
+ abortSignal: dispatch.abortController.signal,
4047
+ ...(account.model ? { isHeartbeat: true, heartbeatModelOverride: account.model } : {}),
4048
+ },
4037
4049
  });
4038
4050
  }
4051
+ catch (err) {
4052
+ // A user-initiated stop aborts the running turn — that rejection is expected,
4053
+ // so don't surface it as a dispatch error (the card is finalized as "stopped" below).
4054
+ if (dispatch.stopped || dispatch.abortController.signal.aborted) {
4055
+ ctx.log?.info?.(`ozaiya: task ${dispatch.taskId} stopped by user`);
4056
+ }
4057
+ else {
4058
+ ctx.log?.warn?.(`ozaiya: reply dispatch threw: ${String(err)}`);
4059
+ }
4060
+ }
4039
4061
  finally {
4040
4062
  // Recover an inline table the model forgot to publish_artifact, BEFORE finalizing
4041
4063
  // so the final progress card includes the deliverable.
@@ -4183,6 +4205,52 @@ ctx) {
4183
4205
  if (resolved)
4184
4206
  return; // intercepted — don't dispatch to agent
4185
4207
  }
4208
+ // Intercept task-control callbacks from the chat task card.
4209
+ // oz_stop:<taskId> → abort the running dispatch mid-flight (interrupts the LLM turn)
4210
+ // oz_continue:<taskId> → resume the task with a fresh, fully-tracked dispatch
4211
+ if (callbackData.startsWith("oz_stop:")) {
4212
+ const taskId = callbackData.slice("oz_stop:".length);
4213
+ const dispatch = activeDispatches.get(account.accountId);
4214
+ if (dispatch && dispatch.taskId === taskId && !dispatch.abortController.signal.aborted) {
4215
+ dispatch.stopped = true; // makes finalizeTaskProgress emit a "stopped" card
4216
+ ctx.log?.info?.(`ozaiya: stop requested for task ${taskId} — aborting dispatch`);
4217
+ dispatch.abortController.abort();
4218
+ }
4219
+ else {
4220
+ ctx.log?.info?.(`ozaiya: stop requested for task ${taskId} but no matching live dispatch`);
4221
+ }
4222
+ return; // intercepted — don't dispatch to agent
4223
+ }
4224
+ if (callbackData.startsWith("oz_continue:")) {
4225
+ // Resume by feeding a synthetic "continue" message through the normal inbound
4226
+ // path, so it gets full task-progress tracking + a fresh live card. The agent's
4227
+ // session persists across dispatches, so it picks up with the task's full context.
4228
+ const continueText = "继续之前的任务,从你上次停下的地方接着做。\nContinue the previous task from where you stopped.";
4229
+ try {
4230
+ const groupKey = await getGroupKeyOrThrow(account, groupId);
4231
+ const synthetic = {
4232
+ event: "message.new",
4233
+ groupId,
4234
+ groupType: "group",
4235
+ replyAllowed: true,
4236
+ message: {
4237
+ id: `continue-${Date.now()}`,
4238
+ senderId: from,
4239
+ senderName: from,
4240
+ seq: 0,
4241
+ content: encryptMessage({ text: continueText }, groupKey),
4242
+ replyToId: null,
4243
+ createdAt: Date.now(),
4244
+ },
4245
+ };
4246
+ ctx.log?.info?.(`ozaiya: continue requested for task ${callbackData.slice("oz_continue:".length)} — re-dispatching`);
4247
+ await handleInboundMessage(synthetic, ctx);
4248
+ }
4249
+ catch (err) {
4250
+ ctx.log?.warn?.(`ozaiya: continue dispatch failed: ${String(err)}`);
4251
+ }
4252
+ return; // intercepted — don't run the lightweight callback dispatch
4253
+ }
4186
4254
  const runtime = getOzaiyaRuntime();
4187
4255
  const ch = runtime.channel;
4188
4256
  const { callbackBody, callbackCommand, callbackLabel, callbackOptionIndex } = normalizeCallbackQueryPayload({
@@ -4225,6 +4293,7 @@ ctx) {
4225
4293
  body: callbackBody,
4226
4294
  });
4227
4295
  const msgCtx = ch.reply.finalizeInboundContext({
4296
+ GroupSystemPrompt: account.persona,
4228
4297
  Body: body,
4229
4298
  BodyForAgent: callbackBody,
4230
4299
  RawBody: callbackBody,
@@ -4401,6 +4470,7 @@ ctx) {
4401
4470
  body: bodyForAgent,
4402
4471
  });
4403
4472
  const msgCtx = ch.reply.finalizeInboundContext({
4473
+ GroupSystemPrompt: account.persona,
4404
4474
  Body: body,
4405
4475
  BodyForAgent: bodyForAgent,
4406
4476
  RawBody: text,
@@ -4485,6 +4555,7 @@ ctx) {
4485
4555
  body: bodyForAgent,
4486
4556
  });
4487
4557
  const msgCtx = ch.reply.finalizeInboundContext({
4558
+ GroupSystemPrompt: account.persona,
4488
4559
  Body: body,
4489
4560
  BodyForAgent: bodyForAgent,
4490
4561
  RawBody: lastUserMessage,