openclaw-plugin-yuanbao 2.13.2 → 2.13.3-beta.09201bf4

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.
@@ -53,7 +53,7 @@ export const dispatchReply = {
53
53
  },
54
54
  account,
55
55
  toAccount: fromAccount,
56
- groupCode,
56
+ groupCode: isGroup ? groupCode : undefined,
57
57
  };
58
58
  const heartbeat = createReplyHeartbeatController({ meta: heartbeatMeta });
59
59
  // Track deliver kind transitions, detect tool-call boundaries
@@ -14,6 +14,8 @@ import { extractGroupCode, json } from "../utils/utils.js";
14
14
  * Queries basic group info including name, owner (userId + nickname), and member count.
15
15
  */
16
16
  function createQueryGroupInfoTool(ctx) {
17
+ if (!ctx.messageChannel?.includes('yuanbao'))
18
+ return null;
17
19
  const sessionKey = ctx.sessionKey ?? "";
18
20
  const accountId = ctx.agentAccountId ?? "";
19
21
  return {
@@ -92,7 +92,10 @@ function handleListAll(allMembers, mention) {
92
92
  *
93
93
  * Merges the original lookup_session_members and query_group_members into one tool.
94
94
  * Prefers API-fetched full member list; session cache as fallback.
95
- */ function createQuerySessionMembersTool(ctx) {
95
+ */
96
+ function createQuerySessionMembersTool(ctx) {
97
+ if (!ctx.messageChannel?.includes('yuanbao'))
98
+ return null;
96
99
  const sessionKey = ctx.sessionKey ?? "";
97
100
  const accountId = ctx.agentAccountId ?? "";
98
101
  return {
@@ -1,8 +1,9 @@
1
- /** Scheduled reminder guidance tool (yuanbao_remind). */
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
1
+ /** Scheduled reminder tool (yuanbao_remind). */
3
2
  /**
4
- * Register reminder-related tools to the OpenClaw plugin API.
5
- *
6
- * Centralized registration ensures the tool is reliably enabled during the plugin lifecycle.
3
+ * Three-tier execution chain, attempted top-down:
4
+ * 1. Gateway API (agent-harness-runtime available): calls cron API directly, status="ok"
5
+ * 2. CLI (plugin-sdk/matrix available): runs `openclaw cron` command, status="ok"; falls back to tier 3 on failure
6
+ * 3. Legacy fallback: returns status="PENDING_CRON_CALL" with cronToolParams to guide the model
7
7
  */
8
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
8
9
  export declare function registerRemindTools(api: OpenClawPluginApi): void;
@@ -1,106 +1,111 @@
1
- /** Scheduled reminder guidance tool (yuanbao_remind). */
1
+ /** Scheduled reminder tool (yuanbao_remind). */
2
2
  import { json } from "../utils/utils.js";
3
- // Prompt constants
3
+ let _callGatewayTool;
4
+ let _runPluginCmd;
5
+ async function resolveCallGatewayTool() {
6
+ if (_callGatewayTool !== undefined)
7
+ return _callGatewayTool;
8
+ try {
9
+ const sdkPath = 'openclaw/plugin-sdk/agent-harness-runtime';
10
+ const mod = await import(sdkPath);
11
+ _callGatewayTool = mod.callGatewayTool;
12
+ }
13
+ catch {
14
+ _callGatewayTool = null;
15
+ }
16
+ return _callGatewayTool;
17
+ }
18
+ async function resolveRunPluginCommand() {
19
+ if (_runPluginCmd !== undefined)
20
+ return _runPluginCmd;
21
+ try {
22
+ const sdkPath = 'openclaw/plugin-sdk/matrix';
23
+ const mod = await import(sdkPath);
24
+ _runPluginCmd = mod.runPluginCommandWithTimeout;
25
+ }
26
+ catch {
27
+ _runPluginCmd = null;
28
+ }
29
+ return _runPluginCmd;
30
+ }
31
+ // ============================================================================
32
+ // Constants
33
+ // ============================================================================
34
+ const DEFAULT_GATEWAY_TIMEOUT_MS = 60_000;
35
+ const DEFAULT_CLI_TIMEOUT_MS = 60_000;
4
36
  const RemindSchema = {
5
- type: "object",
37
+ type: 'object',
6
38
  properties: {
7
39
  action: {
8
- type: "string",
9
- enum: ["add", "list", "remove"],
10
- description: "操作类型: add=创建定时任务, list=查询已有任务, remove=删除任务。"
11
- + "删除前请先通过 list 获取 jobId。",
40
+ type: 'string',
41
+ enum: ['add', 'list', 'remove'],
42
+ description: '操作类型: add=创建定时任务, list=查询已有任务, remove=删除任务。'
43
+ + '删除前请先通过 list 获取 jobId。',
12
44
  },
13
45
  intent: {
14
- type: "string",
15
- enum: ["remind", "task"],
16
- description: "内容语义类型: 明确是提醒类文案时使用 remind; "
17
- + "需要定时执行某项任务时使用 task。默认 remind。",
46
+ type: 'string',
47
+ enum: ['remind', 'task'],
48
+ description: '语义类型,决定触发后的行为模式。'
49
+ + 'remind: 只需要提醒用户去做某事(如"休息一下""开会"); '
50
+ + 'task: 需要 AI 实际执行并输出结果(如"查询今日新闻""检查服务状态")。'
51
+ + '判断标准: 用户期望 AI 动手做事选 task,只需要一句提醒选 remind。',
18
52
  },
19
53
  content: {
20
- type: "string",
21
- description: "任务内容。action=add 时必填。"
22
- + '例如: "喝水"、"检查服务状态"、"整理今天会议纪要并发送总结"。',
54
+ type: 'string',
55
+ description: '任务需求的详细内容。action=add 时必填。'
56
+ + '例如: "提醒用户休息一下"、"查询今日新闻"、"检查服务状态"。',
23
57
  },
24
58
  time: {
25
- type: "string",
26
- description: "时间描述, action=add 时必填。"
27
- + "相对时间: 5m、1h、1h30m、2d(一次性任务); "
28
- + 'cron 表达式: "0 8 * * *""0 9 * * 1-5"(循环任务)'
29
- + "包含空格识别为 cron, 否则按相对时间处理。",
30
- },
31
- timezone: {
32
- type: "string",
33
- description: "时区, 仅循环任务(cron)生效。默认 Asia/Shanghai。",
59
+ type: 'string',
60
+ description: '时间描述, action=add 时必填。'
61
+ + '绝对时间: ISO 8601 UTC 格式, 如 "2026-04-29T14:00:00.000Z"(一次性任务); '
62
+ + '相对时间: 5m1h、1h30m、2d(一次性任务); '
63
+ + 'cron 表达式: "0 8 * * *"、"0 9 * * 1-5"(循环任务)。',
34
64
  },
35
65
  name: {
36
- type: "string",
37
- description: "任务名称(可选)。默认根据 content 自动生成, 便于后续 list/remove 管理。",
66
+ type: 'string',
67
+ description: '任务名称(20字以内),action=add 时可基于 content 内容生成,便于后续 list/remove 管理。',
38
68
  },
39
69
  jobId: {
40
- type: "string",
41
- description: "任务 ID。仅 action=remove 时必填, 需先通过 action=list 获取。",
70
+ type: 'string',
71
+ description: '任务 ID。仅 action=remove 时必填, 需先通过 action=list 获取。',
42
72
  },
43
73
  },
44
- required: ["action"],
74
+ required: ['action'],
45
75
  };
46
- /** Task scenario: execute scheduled task, no reply length limit */
47
- const TASK_AGENT_PROMPT_TEMPLATE = (content) => `你是一个任务执行助手。请在当前时刻完成以下任务:${content}。`
48
- + "要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 "
49
- + "(3) 直接执行任务并输出可直接给用户的结果 (4) 此时是在执行任务,不要再新建定时任务";
50
- /** Remind scenario: warm reminder, limited to short reply */
51
- const REMIND_AGENT_PROMPT_TEMPLATE = (content) => `你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:${content}。`
52
- + "要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 "
53
- + "(3) 直接输出一条暖心的提醒消息,不要携带其他和该提醒无关的内容 (4) 可以加一句简短的关怀话语 "
54
- + "(5) 控制在2-3句话以内 (6) 此时是在执行任务,不要再新建定时任务";
55
- /** Tool-level description: guide model to prioritize this tool and use cron result as source of truth */
56
- const YUANBAO_REMIND_TOOL_DESCRIPTION = [
57
- "用于规范化创建/查询/删除元宝定时任务,并生成可直接提交给 cron 工具的参数。",
58
- "当用户提出提醒、闹钟、定时、X分钟/小时后、每天/每周/每月/每年X点等需求时,或需要查询/删除定时任务/提醒/闹钟时,必须优先调用本工具,",
59
- "并在 cron 执行后核对返回结果,只能在确认成功后再告知用户已设置。",
60
- ].join("\n");
61
- /** Common execution constraint for cron calls */
62
- const CRON_COMMON_REQUIREMENT = [
63
- "请立即调用 cron 工具执行以下参数,并严格以 cron 返回结果为准。",
64
- "未执行或执行失败时,不得回复“已设置成功”。",
65
- "若执行失败,请明确告知用户执行失败并说明失败原因。",
66
- ].join("\n");
67
- /** add/create scenario instruction */
68
- const CRON_ADD_REQUIREMENT = [
69
- CRON_COMMON_REQUIREMENT,
70
- "仅在确认成功后,才向用户确认定时任务已创建。",
71
- "必要时请再调用 cron list 确认任务是否真实创建。",
72
- "用法: openclaw cron add|create [options], Options:",
73
- " --at <when>: Run once at time (ISO) or +duration (e.g. 20m)",
74
- ' --channel <channel>: Delivery channel (last) (default: "last")',
75
- " --cron <expr>: Cron expression (5-field or 6-field with seconds)",
76
- " --delete-after-run: Delete one-shot job after it succeeds (default: false)",
77
- " --message <text>: Agent message payload",
78
- " --name <name>: Job name",
79
- " --session <target>: Session target (main|isolated)",
80
- " --to <dest>: Delivery destination. For this tool, use direct:userId, group:groupCode",
81
- ' --tz <iana>: Timezone for cron expressions (IANA) (default: "")',
82
- ].join("\n");
83
- /** list scenario instruction */
84
- const CRON_LIST_REQUIREMENT = [
85
- CRON_COMMON_REQUIREMENT,
86
- "这是查询定时任务列表场景: 调用 openclaw cron list --json",
87
- "获取全量任务后,必须基于当前会话目标进行过滤,只返回当前会话的任务。",
88
- "过滤规则: 优先使用本次参数中的 to;若未显式提供 to,则使用当前会话自动解析出的目标(例如 direct:<userId> 或 group:<groupCode>)进行匹配。",
89
- "如过滤后结果为空,请明确告知“当前会话没有定时任务”。",
90
- ].join("\n");
91
- /** remove scenario instruction */
92
- const CRON_REMOVE_REQUIREMENT = [
93
- CRON_COMMON_REQUIREMENT,
94
- "这是删除任务场景: 调用 cron remove 后仅在确认成功时回复已删除;失败时必须返回失败原因。",
95
- "删除后建议再调用 cron list 做二次确认。",
96
- "用法: openclaw cron rm|remove <jobId>",
97
- ].join("\n");
98
- // Parse relative time
76
+ /** task mode: execute scheduled task, no reply length limit */
77
+ const TASK_AGENT_PROMPT_TEMPLATE = (content) => `你是一个任务执行助手。请在当前时刻完成以下任务:${content}。\n\n`
78
+ + '## 要求\n'
79
+ + '- 不要回复 HEARTBEAT_OK\n'
80
+ + '- 不要解释你是谁\n'
81
+ + '- 直接执行任务并输出可直接给用户的结果\n'
82
+ + '- **禁止**调用 yuanbao_remind cron 工具,**禁止**新建任何定时任务\n';
83
+ /** remind mode: warm reminder, keep reply brief */
84
+ const REMIND_AGENT_PROMPT_TEMPLATE = (content) => `你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:${content}。\n\n`
85
+ + '## 要求\n'
86
+ + '- 不要回复 HEARTBEAT_OK\n'
87
+ + '- 不要解释你是谁\n'
88
+ + '- 直接输出一条暖心的提醒消息,不要携带其他和该提醒无关的内容\n'
89
+ + '- 可以加一句简短的关怀话语\n'
90
+ + '- 控制在 2-3 句话以内\n'
91
+ + '- **禁止**调用 yuanbao_remind cron 工具,**禁止**新建任何定时任务\n';
92
+ const TOOL_DESCRIPTION = [
93
+ '创建/查询/删除元宝定时任务。当用户提出提醒、闹钟、定时、X分钟/小时后、每天/每周/每月/每年X点等需求时,必须调用本工具。',
94
+ '重要:调用后必须检查返回结果中的 status 字段判断操作是否完成。',
95
+ 'status="ok" 表示已完成,可以告知用户;status="PENDING_CRON_CALL" 表示还需要调用 cron 工具才能生效。',
96
+ '禁止在 status 不是 "ok" 时告知用户任务已设置成功。',
97
+ ].join('\n');
98
+ // Legacy fallback: imperative instructions guiding the model to call cron (concise, non-ignorable)
99
+ const LEGACY_ADD_NEXT_STEP = '你必须立即调用 cron 工具,将 cronToolParams 中的参数传入。只有 cron 返回成功后才能告知用户。未调用 cron 之前,禁止回复"已设置"或"已创建"。';
100
+ const LEGACY_LIST_NEXT_STEP = '你必须立即调用 cron 工具执行 list,获取全量任务后按 filter.to 过滤,只返回当前会话的任务。禁止在调用 cron 之前回复任务列表。';
101
+ const LEGACY_REMOVE_NEXT_STEP = '你必须立即调用 cron 工具执行 remove,传入 cronToolParams.jobId。只有 cron 返回成功后才能告知用户已删除。';
102
+ // ============================================================================
103
+ // Time parsing & formatting
104
+ // ============================================================================
99
105
  function parseRelativeTime(raw) {
100
106
  const s = raw.trim().toLowerCase();
101
- if (!s) {
107
+ if (!s)
102
108
  return null;
103
- }
104
109
  if (/^\d+$/.test(s)) {
105
110
  return parseInt(s, 10) * 60_000;
106
111
  }
@@ -113,214 +118,501 @@ function parseRelativeTime(raw) {
113
118
  const value = parseFloat(match[1]);
114
119
  const unit = match[2];
115
120
  switch (unit) {
116
- case "d":
121
+ case 'd':
117
122
  totalMs += value * 86_400_000;
118
123
  break;
119
- case "h":
124
+ case 'h':
120
125
  totalMs += value * 3_600_000;
121
126
  break;
122
- case "m":
127
+ case 'm':
123
128
  totalMs += value * 60_000;
124
129
  break;
125
- case "s":
130
+ case 's':
126
131
  totalMs += value * 1_000;
127
132
  break;
128
- default:
129
- break;
133
+ default: break;
130
134
  }
131
135
  }
132
136
  return matched ? Math.round(totalMs) : null;
133
137
  }
134
- // Check if string is a cron expression
138
+ /** Parses an ISO 8601 absolute time string; returns epoch ms, or null if invalid/expired. */
139
+ function parseAbsoluteTime(time) {
140
+ if (!/\d{4}-\d{2}/.test(time))
141
+ return null;
142
+ const ms = Date.parse(time);
143
+ if (Number.isNaN(ms))
144
+ return null;
145
+ return ms > Date.now() ? ms : null;
146
+ }
135
147
  function isCronExpression(timeText) {
136
148
  const parts = timeText.trim().split(/\s+/);
137
149
  return parts.length >= 3 && parts.length <= 6;
138
150
  }
139
- // Format delay duration
140
151
  function formatDelay(ms) {
141
152
  const seconds = Math.round(ms / 1000);
142
- if (seconds < 60) {
153
+ if (seconds < 60)
143
154
  return `${seconds}秒`;
144
- }
145
155
  const minutes = Math.round(ms / 60_000);
146
- if (minutes < 60) {
156
+ if (minutes < 60)
147
157
  return `${minutes}分钟`;
148
- }
149
158
  const hours = Math.floor(minutes / 60);
150
159
  const remains = minutes % 60;
151
- if (remains === 0) {
160
+ if (remains === 0)
152
161
  return `${hours}小时`;
153
- }
154
162
  return `${hours}小时${remains}分钟`;
155
163
  }
156
- // Resolve delivery target from session
157
- // If session contains group:groupCode or direct:userId, return the corresponding target
158
- // Otherwise return null
164
+ // ============================================================================
165
+ // Session resolution
166
+ // ============================================================================
159
167
  function resolveToFromSession(ctx) {
160
- const sessionKey = ctx.sessionKey ?? "";
161
- const groupPrefix = "yuanbao:group:";
162
- const directPrefix = "yuanbao:direct:";
168
+ const sessionKey = ctx.sessionKey ?? '';
169
+ const groupPrefix = 'yuanbao:group:';
170
+ const directPrefix = 'yuanbao:direct:';
163
171
  const groupIdx = sessionKey.indexOf(groupPrefix);
164
172
  if (groupIdx !== -1) {
165
173
  const groupCode = sessionKey.slice(groupIdx + groupPrefix.length).trim();
166
- if (groupCode) {
174
+ if (groupCode)
167
175
  return `group:${groupCode}`;
168
- }
169
176
  }
170
177
  const directIdx = sessionKey.indexOf(directPrefix);
171
178
  if (directIdx !== -1) {
172
- // Cannot parse userID from sessionKey because sessionKey lowercases it, while userID is mixed-case
179
+ // sessionKey lowercases userId, so use requesterSenderId instead
173
180
  const userId = ctx.requesterSenderId;
174
- if (userId) {
181
+ if (userId)
175
182
  return `direct:${userId}`;
176
- }
177
183
  }
178
184
  return null;
179
185
  }
180
- // Build SubAgent execution prompt
186
+ // ============================================================================
187
+ // Prompt & name generation
188
+ // ============================================================================
181
189
  function buildReminderPrompt(content, intent) {
182
- if (intent === "task") {
190
+ if (intent === 'task') {
183
191
  return TASK_AGENT_PROMPT_TEMPLATE(content);
184
192
  }
185
193
  return REMIND_AGENT_PROMPT_TEMPLATE(content);
186
194
  }
187
- // Generate job name
188
- function generateJobName(content, intent) {
195
+ function generateJobName(content) {
189
196
  const text = content.trim();
190
- const short = text.length > 20 ? `${text.slice(0, 20)}...` : text;
191
- return `${intent === "task" ? "任务" : "提醒"}: ${short}`;
197
+ return text.length > 20 ? `${text.slice(0, 20)}...` : text;
192
198
  }
193
- /**
194
- * Build cron params for a one-time (relative time) reminder task.
195
- *
196
- * Encapsulates one-time task scheduling strategy (kind=at, delete after run)
197
- * to prevent callers from missing critical fields.
198
- */
199
- function buildOnceCronParams(params, delayMs, to, intent) {
199
+ /** Resolves shared fields for a one-time job. */
200
+ function resolveOnceBase(params, time, intent) {
200
201
  const content = params.content;
201
- const at = `${Math.max(1, Math.round(delayMs / 1000))}s`;
202
202
  return {
203
- action: "add",
204
- name: params.name || generateJobName(content, intent),
205
- at,
206
- session: "isolated",
207
- deleteAfterRun: true,
203
+ name: params.name || generateJobName(content),
204
+ atMs: time.type === 'absolute' ? time.atMs : Date.now() + time.delayMs,
205
+ atStr: time.type === 'absolute' ? time.iso : `${Math.max(1, Math.round(time.delayMs / 1000))}s`,
208
206
  message: buildReminderPrompt(content, intent),
209
- channel: "yuanbao",
210
- to,
211
207
  };
212
208
  }
213
- /**
214
- * Build cron params for a recurring (cron expression) reminder task.
215
- *
216
- * Separated from one-time tasks to ensure recurring-specific fields (timezone, expression)
217
- * are always complete, reducing LLM parameter assembly ambiguity.
218
- */
219
- function buildCronParams(params, to, intent) {
209
+ /** Resolves shared fields for a recurring (cron) job. */
210
+ function resolveCronBase(params, intent) {
220
211
  const content = params.content;
221
212
  return {
222
- action: "add",
223
- name: params.name || generateJobName(content, intent),
224
- cron: params.time.trim(),
225
- tz: params.timezone || "Asia/Shanghai",
226
- session: "isolated",
227
- deleteAfterRun: false,
213
+ name: params.name || generateJobName(content),
214
+ expr: params.time.trim(),
215
+ tz: params.timezone || 'Asia/Shanghai',
228
216
  message: buildReminderPrompt(content, intent),
229
- channel: "yuanbao",
230
- to,
231
217
  };
232
218
  }
219
+ // ============================================================================
220
+ // Gateway mode: job builders
221
+ // ============================================================================
222
+ /** Builds a Gateway job config for a one-time job. */
223
+ function buildOnceJob(params, time, to, accountId, intent) {
224
+ const { name, atMs, message } = resolveOnceBase(params, time, intent);
225
+ return {
226
+ name,
227
+ schedule: { kind: 'at', atMs },
228
+ sessionTarget: 'isolated',
229
+ wakeMode: 'now',
230
+ deleteAfterRun: true,
231
+ payload: { kind: 'agentTurn', message },
232
+ delivery: { mode: 'announce', channel: 'yuanbao', to, accountId },
233
+ };
234
+ }
235
+ /** Builds a Gateway job config for a recurring (cron) job. */
236
+ function buildCronJob(params, to, accountId, intent) {
237
+ const { name, expr, tz, message } = resolveCronBase(params, intent);
238
+ return {
239
+ name,
240
+ schedule: { kind: 'cron', expr, tz },
241
+ sessionTarget: 'isolated',
242
+ wakeMode: 'now',
243
+ payload: { kind: 'agentTurn', message },
244
+ delivery: { mode: 'announce', channel: 'yuanbao', to, accountId },
245
+ };
246
+ }
247
+ // ============================================================================
248
+ // CLI mode: argv builders
249
+ // ============================================================================
250
+ /** Builds CLI argv for `openclaw cron add --at ...` (one-time job). */
251
+ function buildOnceCliArgs(params, time, to, accountId, intent) {
252
+ const { name, atStr, message } = resolveOnceBase(params, time, intent);
253
+ const argv = [
254
+ 'openclaw', 'cron', 'add',
255
+ '--name', name,
256
+ '--at', atStr,
257
+ '--message', message,
258
+ '--session', 'isolated',
259
+ '--delete-after-run',
260
+ '--announce',
261
+ '--channel', 'yuanbao',
262
+ '--to', to,
263
+ '--json',
264
+ ];
265
+ if (accountId)
266
+ argv.push('--account', accountId);
267
+ return argv;
268
+ }
269
+ /** Builds CLI argv for `openclaw cron add --cron ...` (recurring job). */
270
+ function buildCronCliArgs(params, to, accountId, intent) {
271
+ const { name, expr, tz, message } = resolveCronBase(params, intent);
272
+ const argv = [
273
+ 'openclaw', 'cron', 'add',
274
+ '--name', name,
275
+ '--cron', expr,
276
+ '--tz', tz,
277
+ '--message', message,
278
+ '--session', 'isolated',
279
+ '--announce',
280
+ '--channel', 'yuanbao',
281
+ '--to', to,
282
+ '--json',
283
+ ];
284
+ if (accountId)
285
+ argv.push('--account', accountId);
286
+ return argv;
287
+ }
288
+ // ============================================================================
289
+ // Legacy mode: cron params builders (used by executeLegacy)
290
+ // ============================================================================
291
+ /** Builds Legacy cronToolParams for a one-time job. */
292
+ function buildOnceLegacyParams(params, time, to, intent) {
293
+ const { name, atStr, message } = resolveOnceBase(params, time, intent);
294
+ return { action: 'add', name, at: atStr, session: 'isolated', deleteAfterRun: true, message, channel: 'yuanbao', to };
295
+ }
296
+ /** Builds Legacy cronToolParams for a recurring (cron) job. */
297
+ function buildCronLegacyParams(params, to, intent) {
298
+ const { name, expr, tz, message } = resolveCronBase(params, intent);
299
+ return { action: 'add', name, cron: expr, tz, session: 'isolated', deleteAfterRun: false, message, channel: 'yuanbao', to };
300
+ }
301
+ // ============================================================================
302
+ // Result filtering & error formatting
303
+ // ============================================================================
304
+ function filterJobsByTarget(cronResult, to) {
305
+ let jobs;
306
+ if (Array.isArray(cronResult)) {
307
+ jobs = cronResult;
308
+ }
309
+ else if (cronResult && typeof cronResult === 'object') {
310
+ const r = cronResult;
311
+ jobs = Array.isArray(r.jobs) ? r.jobs : [];
312
+ }
313
+ else {
314
+ jobs = [];
315
+ }
316
+ return jobs.filter((job) => {
317
+ const j = job;
318
+ const delivery = (j.delivery ?? j.job?.delivery);
319
+ if (delivery?.channel !== 'yuanbao')
320
+ return false;
321
+ return !to || delivery?.to === to;
322
+ });
323
+ }
324
+ function formatTimeLabel(time, intent) {
325
+ if (time.type === 'absolute') {
326
+ const d = new Date(time.atMs);
327
+ const hh = String(d.getHours()).padStart(2, '0');
328
+ const mm = String(d.getMinutes()).padStart(2, '0');
329
+ const label = intent === 'task' ? '执行任务' : '提醒';
330
+ return `${d.getMonth() + 1}/${d.getDate()} ${hh}:${mm} ${label}`;
331
+ }
332
+ const suffix = intent === 'task' ? '后执行任务' : '后提醒';
333
+ return `${formatDelay(time.delayMs)}${suffix}`;
334
+ }
335
+ function formatSchedulerError(error) {
336
+ return error instanceof Error ? error.message : String(error);
337
+ }
338
+ function tryParseJson(text) {
339
+ try {
340
+ return JSON.parse(text);
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ // ============================================================================
347
+ // Shared parameter validation
348
+ // ============================================================================
349
+ function validateAddParams(p, resolvedTo) {
350
+ if (!p.content?.trim())
351
+ return { error: 'action=add 时 content 为必填。' };
352
+ if (!p.time?.trim())
353
+ return { error: 'action=add 时 time 为必填。示例:5m / 1h30m / 0 8 * * *' };
354
+ if (!resolvedTo)
355
+ return { error: '无法确定投递目标。请确保在元宝会话中发起请求。' };
356
+ return null;
357
+ }
358
+ function validateRemoveParams(p) {
359
+ if (!p.jobId?.trim())
360
+ return { error: 'action=remove 时 jobId 为必填,请先调用 action=list 获取任务 ID。' };
361
+ return null;
362
+ }
363
+ function parseAndValidateTime(time) {
364
+ const atMs = parseAbsoluteTime(time);
365
+ if (atMs !== null) {
366
+ if (atMs - Date.now() < 30_000)
367
+ return { error: '提醒时间不能少于 30 秒。' };
368
+ return { timeSpec: { type: 'absolute', atMs, iso: new Date(atMs).toISOString() } };
369
+ }
370
+ const delayMs = parseRelativeTime(time);
371
+ if (!delayMs || delayMs <= 0) {
372
+ return { error: `无法解析时间 "${time}"。支持 ISO 时间(如 2026-04-29T14:00:00Z)、相对时间(5m/1h/1h30m/2d)或 cron 表达式(如 0 8 * * *)。` };
373
+ }
374
+ if (delayMs < 30_000)
375
+ return { error: '提醒时间不能少于 30 秒。' };
376
+ return { timeSpec: { type: 'relative', delayMs } };
377
+ }
378
+ // ============================================================================
379
+ // Gateway execute implementation
380
+ // ============================================================================
381
+ async function executeGateway(gatewayTool, p, resolvedTo, accountId) {
382
+ switch (p.action) {
383
+ case 'list': {
384
+ try {
385
+ const cronResult = await gatewayTool('cron.list', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, {});
386
+ const filtered = filterJobsByTarget(cronResult, resolvedTo);
387
+ return json({ status: 'ok', action: 'list', _via: 'gateway', jobs: filtered });
388
+ }
389
+ catch (error) {
390
+ return json({ error: `查询定时任务失败: ${formatSchedulerError(error)}` });
391
+ }
392
+ }
393
+ case 'remove': {
394
+ const err = validateRemoveParams(p);
395
+ if (err)
396
+ return json(err);
397
+ try {
398
+ const cronResult = await gatewayTool('cron.remove', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { jobId: p.jobId.trim() });
399
+ return json({ status: 'ok', action: 'remove', _via: 'gateway', cronResult });
400
+ }
401
+ catch (error) {
402
+ return json({ error: `删除定时任务失败: ${formatSchedulerError(error)}` });
403
+ }
404
+ }
405
+ case 'add': {
406
+ const addErr = validateAddParams(p, resolvedTo);
407
+ if (addErr)
408
+ return json(addErr);
409
+ const intent = p.intent ?? 'remind';
410
+ if (isCronExpression(p.time)) {
411
+ const job = buildCronJob({ ...p, content: p.content.trim() }, resolvedTo, accountId, intent);
412
+ try {
413
+ const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { job });
414
+ const typeLabel = intent === 'task' ? '循环任务' : '周期提醒';
415
+ return json({
416
+ status: 'ok',
417
+ action: 'add',
418
+ _via: 'gateway',
419
+ summary: `${typeLabel}: "${p.content.trim()}" (${p.time.trim()}, tz=${p.timezone || 'Asia/Shanghai'})`,
420
+ cronResult,
421
+ });
422
+ }
423
+ catch (error) {
424
+ return json({ error: `创建周期任务失败: ${formatSchedulerError(error)}` });
425
+ }
426
+ }
427
+ const timeResult = parseAndValidateTime(p.time);
428
+ if ('error' in timeResult)
429
+ return json(timeResult);
430
+ const job = buildOnceJob({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, accountId, intent);
431
+ try {
432
+ const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { job });
433
+ return json({
434
+ status: 'ok',
435
+ action: 'add',
436
+ _via: 'gateway',
437
+ summary: `${formatTimeLabel(timeResult.timeSpec, intent)}: "${p.content.trim()}"`,
438
+ cronResult,
439
+ });
440
+ }
441
+ catch (error) {
442
+ return json({ error: `创建定时任务失败: ${formatSchedulerError(error)}` });
443
+ }
444
+ }
445
+ default:
446
+ return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
447
+ }
448
+ }
449
+ // ============================================================================
450
+ // CLI execute implementation
451
+ // Returns null on command failure; validation errors return json directly (no fallback).
452
+ // ============================================================================
233
453
  /**
234
- * Create the `yuanbao_remind` tool definition.
235
- *
236
- * Standardizes natural language "reminder/task" requests into cron-executable params,
237
- * guiding the model to complete structured scheduling before replying to the user.
238
- * The execute callback handles add/list/remove branches with centralized validation,
239
- * target resolution, param building, and error messaging.
454
+ * CLI execution tier. Returns null on command failure to trigger Legacy fallback.
240
455
  */
456
+ async function executeCli(runner, p, resolvedTo, accountId) {
457
+ switch (p.action) {
458
+ case 'list': {
459
+ try {
460
+ const result = await runner({ argv: ['openclaw', 'cron', 'list', '--json'], timeoutMs: DEFAULT_CLI_TIMEOUT_MS });
461
+ if (result.code !== 0)
462
+ return null;
463
+ const parsed = tryParseJson(result.stdout);
464
+ if (parsed) {
465
+ const filtered = filterJobsByTarget(parsed, resolvedTo);
466
+ return json({ status: 'ok', action: 'list', _via: 'cli', jobs: filtered });
467
+ }
468
+ return json({ status: 'ok', action: 'list', _via: 'cli', raw: result.stdout.trim() });
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ }
474
+ case 'remove': {
475
+ const err = validateRemoveParams(p);
476
+ if (err)
477
+ return json(err);
478
+ try {
479
+ const result = await runner({
480
+ argv: ['openclaw', 'cron', 'rm', p.jobId.trim(), '--json'],
481
+ timeoutMs: DEFAULT_CLI_TIMEOUT_MS,
482
+ });
483
+ if (result.code !== 0)
484
+ return null;
485
+ const parsed = tryParseJson(result.stdout);
486
+ return json({ status: 'ok', action: 'remove', _via: 'cli', cronResult: parsed ?? result.stdout.trim() });
487
+ }
488
+ catch {
489
+ return null;
490
+ }
491
+ }
492
+ case 'add': {
493
+ const addErr = validateAddParams(p, resolvedTo);
494
+ if (addErr)
495
+ return json(addErr);
496
+ const intent = p.intent ?? 'remind';
497
+ let argv;
498
+ let summary;
499
+ if (isCronExpression(p.time)) {
500
+ argv = buildCronCliArgs({ ...p, content: p.content.trim() }, resolvedTo, accountId, intent);
501
+ const typeLabel = intent === 'task' ? '循环任务' : '周期提醒';
502
+ summary = `${typeLabel}: "${p.content.trim()}" (${p.time.trim()}, tz=${p.timezone || 'Asia/Shanghai'})`;
503
+ }
504
+ else {
505
+ const timeResult = parseAndValidateTime(p.time);
506
+ if ('error' in timeResult)
507
+ return json(timeResult);
508
+ argv = buildOnceCliArgs({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, accountId, intent);
509
+ summary = `${formatTimeLabel(timeResult.timeSpec, intent)}: "${p.content.trim()}"`;
510
+ }
511
+ try {
512
+ const result = await runner({ argv, timeoutMs: DEFAULT_CLI_TIMEOUT_MS });
513
+ if (result.code !== 0)
514
+ return null;
515
+ const parsed = tryParseJson(result.stdout);
516
+ return json({ status: 'ok', action: 'add', _via: 'cli', summary, cronResult: parsed ?? result.stdout.trim() });
517
+ }
518
+ catch {
519
+ return null;
520
+ }
521
+ }
522
+ default:
523
+ return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
524
+ }
525
+ }
526
+ // ============================================================================
527
+ // Legacy execute implementation
528
+ // ============================================================================
529
+ /** Returns PENDING_CRON_CALL to guide the model to invoke the cron tool. */
530
+ function executeLegacy(p, resolvedTo) {
531
+ switch (p.action) {
532
+ case 'list':
533
+ return json({
534
+ status: 'PENDING_CRON_CALL',
535
+ _via: 'legacy',
536
+ completed: false,
537
+ next_step: LEGACY_LIST_NEXT_STEP,
538
+ cronToolParams: { action: 'list' },
539
+ filter: { to: resolvedTo },
540
+ });
541
+ case 'remove': {
542
+ const err = validateRemoveParams(p);
543
+ if (err)
544
+ return json(err);
545
+ return json({
546
+ status: 'PENDING_CRON_CALL',
547
+ _via: 'legacy',
548
+ completed: false,
549
+ next_step: LEGACY_REMOVE_NEXT_STEP,
550
+ cronToolParams: { action: 'remove', jobId: p.jobId.trim() },
551
+ });
552
+ }
553
+ case 'add': {
554
+ const addErr = validateAddParams(p, resolvedTo);
555
+ if (addErr)
556
+ return json(addErr);
557
+ const intent = p.intent ?? 'remind';
558
+ if (isCronExpression(p.time)) {
559
+ const cronToolParams = buildCronLegacyParams({ ...p, content: p.content.trim() }, resolvedTo, intent);
560
+ return json({
561
+ status: 'PENDING_CRON_CALL',
562
+ _via: 'legacy',
563
+ completed: false,
564
+ next_step: LEGACY_ADD_NEXT_STEP,
565
+ cronToolParams,
566
+ });
567
+ }
568
+ const timeResult = parseAndValidateTime(p.time);
569
+ if ('error' in timeResult)
570
+ return json(timeResult);
571
+ const cronToolParams = buildOnceLegacyParams({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, intent);
572
+ return json({
573
+ status: 'PENDING_CRON_CALL',
574
+ _via: 'legacy',
575
+ completed: false,
576
+ next_step: LEGACY_ADD_NEXT_STEP,
577
+ cronToolParams,
578
+ });
579
+ }
580
+ default:
581
+ return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
582
+ }
583
+ }
584
+ // ============================================================================
585
+ // Tool definition & registration
586
+ // ============================================================================
587
+ /** Returns null for non-yuanbao channels. */
241
588
  function createYuanbaoRemindTool(ctx) {
242
- const isYuanbaoChannel = ctx.messageChannel === "yuanbao";
243
- if (!isYuanbaoChannel) {
589
+ if (!ctx.messageChannel?.includes('yuanbao'))
244
590
  return null;
245
- }
246
- // Only enable in yuanbao channel
591
+ const resolvedTo = resolveToFromSession(ctx);
592
+ const accountId = ctx.deliveryContext?.accountId ?? ctx.agentAccountId ?? '';
247
593
  return {
248
- name: "yuanbao_remind",
249
- label: "元宝定时任务",
250
- description: YUANBAO_REMIND_TOOL_DESCRIPTION,
594
+ name: 'yuanbao_remind',
595
+ label: '元宝定时任务',
596
+ description: TOOL_DESCRIPTION,
251
597
  parameters: RemindSchema,
252
- execute: async (_toolCallId, params) => {
598
+ async execute(_toolCallId, params) {
253
599
  const p = params;
254
- const resolvedTo = resolveToFromSession(ctx);
255
- switch (p.action) {
256
- case "list":
257
- return json({
258
- instruction: CRON_LIST_REQUIREMENT,
259
- cronParams: { action: "list" },
260
- filter: { to: resolvedTo },
261
- });
262
- case "remove":
263
- if (!p.jobId?.trim()) {
264
- return json({
265
- error: "action=remove 时 jobId 为必填,请先调用 action=list 获取任务 ID。",
266
- });
267
- }
268
- return json({
269
- instruction: CRON_REMOVE_REQUIREMENT,
270
- cronParams: { action: "remove", jobId: p.jobId.trim() },
271
- });
272
- case "add": {
273
- if (!p.content?.trim()) {
274
- return json({ error: "action=add 时 content 为必填。" });
275
- }
276
- if (!p.time?.trim()) {
277
- return json({ error: "action=add 时 time 为必填。示例:5m / 1h30m / 0 8 * * *" });
278
- }
279
- const intent = p.intent ?? "remind";
280
- const isCron = isCronExpression(p.time);
281
- if (!resolvedTo) {
282
- return json({
283
- error: "无法确定投递目标 to。请显式传入 to,例如 direct:<userId> 或 group:<groupCode>。",
284
- });
285
- }
286
- if (isCron) {
287
- const cronParams = buildCronParams({ ...p, content: p.content.trim() }, resolvedTo, intent);
288
- const typeLabel = intent === "task" ? "循环任务" : "周期提醒";
289
- return json({
290
- instruction: CRON_ADD_REQUIREMENT,
291
- cronParams,
292
- summary: `⏰ ${typeLabel}: "${p.content.trim()}" (${p.time.trim()}, tz=${p.timezone || "Asia/Shanghai"})`,
293
- });
294
- }
295
- const delayMs = parseRelativeTime(p.time);
296
- if (!delayMs || delayMs <= 0) {
297
- return json({
298
- error: `无法解析时间 "${p.time}"。支持相对时间(5m/1h/1h30m/2d)`
299
- + "或 cron 表达式(如 0 8 * * *)。",
300
- });
301
- }
302
- if (delayMs < 30_000) {
303
- return json({ error: "提醒时间不能少于 30 秒。" });
304
- }
305
- const cronParams = buildOnceCronParams({ ...p, content: p.content.trim() }, delayMs, resolvedTo, intent);
306
- const typeLabel = intent === "task" ? "后执行任务" : "后提醒";
307
- return json({
308
- instruction: CRON_ADD_REQUIREMENT,
309
- cronParams,
310
- summary: `⏰ ${formatDelay(delayMs)}${typeLabel}: "${p.content.trim()}"`,
311
- });
312
- }
313
- default:
314
- return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
600
+ const gatewayTool = await resolveCallGatewayTool();
601
+ if (gatewayTool) {
602
+ return executeGateway(gatewayTool, p, resolvedTo, accountId);
315
603
  }
604
+ const runner = await resolveRunPluginCommand();
605
+ if (runner) {
606
+ const cliResult = await executeCli(runner, p, resolvedTo, accountId);
607
+ if (cliResult !== null)
608
+ return cliResult;
609
+ // CLI command failed (non-zero exit or exception), fall through to Legacy
610
+ }
611
+ // Final fallback: guide the model to call cron via PENDING response
612
+ return executeLegacy(p, resolvedTo);
316
613
  },
317
614
  };
318
615
  }
319
- /**
320
- * Register reminder-related tools to the OpenClaw plugin API.
321
- *
322
- * Centralized registration ensures the tool is reliably enabled during the plugin lifecycle.
323
- */
324
616
  export function registerRemindTools(api) {
325
617
  api.registerTool(createYuanbaoRemindTool, { optional: false });
326
618
  }
@@ -22,7 +22,7 @@ export declare const yuanbaoMeta: {
22
22
  readonly docsPath: "/plugins/community#yuanbao";
23
23
  readonly docsLabel: "yuanbao";
24
24
  readonly blurb: "Tencent Yuanbao AI assistant conversation channel";
25
- readonly aliases: readonly ["yb", "tencent-yuanbao", "元宝"];
25
+ readonly aliases: readonly ["yb", "yuanbao", "tencent-yuanbao", "元宝"];
26
26
  readonly order: 85;
27
27
  readonly quickstartAllowFrom: true;
28
28
  };
@@ -22,7 +22,7 @@ export const yuanbaoMeta = {
22
22
  docsPath: "/plugins/community#yuanbao",
23
23
  docsLabel: "yuanbao",
24
24
  blurb: "Tencent Yuanbao AI assistant conversation channel",
25
- aliases: ["yb", "tencent-yuanbao", "元宝"],
25
+ aliases: ["yb", "yuanbao", "tencent-yuanbao", "元宝"],
26
26
  order: 85,
27
27
  quickstartAllowFrom: true,
28
28
  };
@@ -42,7 +42,7 @@ const assertHostVersionCompatible = (hostVersion) => {
42
42
  const constraint = getMinHostVersion();
43
43
  if (!constraint)
44
44
  return;
45
- if (!semver.satisfies(hostVersion, constraint)) {
45
+ if (!semver.satisfies(hostVersion, constraint, { includePrerelease: true })) {
46
46
  throw new Error(`openclaw-plugin-yuanbao requires openclaw ${constraint}, but current version is ${hostVersion}. Please upgrade openclaw first.`);
47
47
  }
48
48
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-plugin-yuanbao",
3
- "version": "2.13.2",
3
+ "version": "2.13.3-beta.09201bf4",
4
4
  "name": "YuanBao",
5
5
  "description": "YuanBao channel plugin",
6
6
  "channels": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-yuanbao",
3
- "version": "2.13.2",
3
+ "version": "2.13.3-beta.09201bf4",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Tencent YuanBao intelligent bot channel plugin",