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.
- package/dist/src/business/pipeline/middlewares/dispatch-reply.js +1 -1
- package/dist/src/business/tools/group.js +2 -0
- package/dist/src/business/tools/member.js +4 -1
- package/dist/src/business/tools/remind.d.ts +6 -5
- package/dist/src/business/tools/remind.js +516 -224
- package/dist/src/channel-shared.d.ts +1 -1
- package/dist/src/channel-shared.js +1 -1
- package/dist/src/infra/env.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
*/
|
|
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
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
1
|
+
/** Scheduled reminder tool (yuanbao_remind). */
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
1
|
+
/** Scheduled reminder tool (yuanbao_remind). */
|
|
2
2
|
import { json } from "../utils/utils.js";
|
|
3
|
-
|
|
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:
|
|
37
|
+
type: 'object',
|
|
6
38
|
properties: {
|
|
7
39
|
action: {
|
|
8
|
-
type:
|
|
9
|
-
enum: [
|
|
10
|
-
description:
|
|
11
|
-
+
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['add', 'list', 'remove'],
|
|
42
|
+
description: '操作类型: add=创建定时任务, list=查询已有任务, remove=删除任务。'
|
|
43
|
+
+ '删除前请先通过 list 获取 jobId。',
|
|
12
44
|
},
|
|
13
45
|
intent: {
|
|
14
|
-
type:
|
|
15
|
-
enum: [
|
|
16
|
-
description:
|
|
17
|
-
+ "
|
|
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:
|
|
21
|
-
description:
|
|
22
|
-
+ '例如: "
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: '任务需求的详细内容。action=add 时必填。'
|
|
56
|
+
+ '例如: "提醒用户休息一下"、"查询今日新闻"、"检查服务状态"。',
|
|
23
57
|
},
|
|
24
58
|
time: {
|
|
25
|
-
type:
|
|
26
|
-
description:
|
|
27
|
-
+ "
|
|
28
|
-
+ '
|
|
29
|
-
+ "
|
|
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
|
+
+ '相对时间: 5m、1h、1h30m、2d(一次性任务); '
|
|
63
|
+
+ 'cron 表达式: "0 8 * * *"、"0 9 * * 1-5"(循环任务)。',
|
|
34
64
|
},
|
|
35
65
|
name: {
|
|
36
|
-
type:
|
|
37
|
-
description:
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: '任务名称(20字以内),action=add 时可基于 content 内容生成,便于后续 list/remove 管理。',
|
|
38
68
|
},
|
|
39
69
|
jobId: {
|
|
40
|
-
type:
|
|
41
|
-
description:
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: '任务 ID。仅 action=remove 时必填, 需先通过 action=list 获取。',
|
|
42
72
|
},
|
|
43
73
|
},
|
|
44
|
-
required: [
|
|
74
|
+
required: ['action'],
|
|
45
75
|
};
|
|
46
|
-
/**
|
|
47
|
-
const TASK_AGENT_PROMPT_TEMPLATE = (content) => `你是一个任务执行助手。请在当前时刻完成以下任务:${content}
|
|
48
|
-
+
|
|
49
|
-
+
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
+
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
121
|
+
case 'd':
|
|
117
122
|
totalMs += value * 86_400_000;
|
|
118
123
|
break;
|
|
119
|
-
case
|
|
124
|
+
case 'h':
|
|
120
125
|
totalMs += value * 3_600_000;
|
|
121
126
|
break;
|
|
122
|
-
case
|
|
127
|
+
case 'm':
|
|
123
128
|
totalMs += value * 60_000;
|
|
124
129
|
break;
|
|
125
|
-
case
|
|
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
|
-
|
|
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
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Session resolution
|
|
166
|
+
// ============================================================================
|
|
159
167
|
function resolveToFromSession(ctx) {
|
|
160
|
-
const sessionKey = ctx.sessionKey ??
|
|
161
|
-
const groupPrefix =
|
|
162
|
-
const directPrefix =
|
|
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
|
-
//
|
|
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
|
-
//
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Prompt & name generation
|
|
188
|
+
// ============================================================================
|
|
181
189
|
function buildReminderPrompt(content, intent) {
|
|
182
|
-
if (intent ===
|
|
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
|
-
|
|
188
|
-
function generateJobName(content, intent) {
|
|
195
|
+
function generateJobName(content) {
|
|
189
196
|
const text = content.trim();
|
|
190
|
-
|
|
191
|
-
return `${intent === "task" ? "任务" : "提醒"}: ${short}`;
|
|
197
|
+
return text.length > 20 ? `${text.slice(0, 20)}...` : text;
|
|
192
198
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
243
|
-
if (!isYuanbaoChannel) {
|
|
589
|
+
if (!ctx.messageChannel?.includes('yuanbao'))
|
|
244
590
|
return null;
|
|
245
|
-
|
|
246
|
-
|
|
591
|
+
const resolvedTo = resolveToFromSession(ctx);
|
|
592
|
+
const accountId = ctx.deliveryContext?.accountId ?? ctx.agentAccountId ?? '';
|
|
247
593
|
return {
|
|
248
|
-
name:
|
|
249
|
-
label:
|
|
250
|
-
description:
|
|
594
|
+
name: 'yuanbao_remind',
|
|
595
|
+
label: '元宝定时任务',
|
|
596
|
+
description: TOOL_DESCRIPTION,
|
|
251
597
|
parameters: RemindSchema,
|
|
252
|
-
|
|
598
|
+
async execute(_toolCallId, params) {
|
|
253
599
|
const p = params;
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
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
|
};
|
package/dist/src/infra/env.js
CHANGED
|
@@ -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
|
};
|
package/openclaw.plugin.json
CHANGED