openclaw-plugin-yuanbao 2.13.2 → 2.13.4
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/access/ws/runtime.d.ts +4 -0
- package/dist/src/access/ws/runtime.js +3 -1
- package/dist/src/business/pipeline/middlewares/dispatch-reply.js +1 -1
- package/dist/src/business/tools/group.js +9 -3
- package/dist/src/business/tools/member.js +11 -4
- package/dist/src/business/tools/remind.d.ts +6 -5
- package/dist/src/business/tools/remind.js +522 -225
- 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
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Uses Map<accountId, WsClient> to manage concurrent connections.
|
|
5
5
|
* Each account's WsClient reference is stored when the ws-gateway starts
|
|
6
6
|
* and consumed by the outbound sendText path.
|
|
7
|
+
*
|
|
8
|
+
* Uses globalThis + Symbol.for() to guarantee a process-wide singleton,
|
|
9
|
+
* because the bundled channel entry may load this module in separate scopes
|
|
10
|
+
* (plugin scope vs tool-registration scope).
|
|
7
11
|
*/
|
|
8
12
|
import type { YuanbaoWsClient } from "./client.js";
|
|
9
13
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const WS_CLIENTS_KEY = Symbol.for("yuanbao:ws:activeClients");
|
|
2
|
+
const activeClients = globalThis[WS_CLIENTS_KEY]
|
|
3
|
+
?? (globalThis[WS_CLIENTS_KEY] = new Map());
|
|
2
4
|
/**
|
|
3
5
|
* Store a WebSocket client reference for the given account.
|
|
4
6
|
*/
|
|
@@ -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
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Fetches group info via the queryGroupInfo interface (WS protocol).
|
|
8
8
|
*/
|
|
9
9
|
import { getMember } from "../../infra/cache/member.js";
|
|
10
|
+
import { createLog } from "../../logger.js";
|
|
10
11
|
import { extractGroupCode, json } from "../utils/utils.js";
|
|
11
12
|
/**
|
|
12
13
|
* Create the query_group_info tool definition.
|
|
@@ -14,6 +15,9 @@ import { extractGroupCode, json } from "../utils/utils.js";
|
|
|
14
15
|
* Queries basic group info including name, owner (userId + nickname), and member count.
|
|
15
16
|
*/
|
|
16
17
|
function createQueryGroupInfoTool(ctx) {
|
|
18
|
+
const log = createLog("tools.group");
|
|
19
|
+
if (!ctx.messageChannel?.includes('yuanbao'))
|
|
20
|
+
return null;
|
|
17
21
|
const sessionKey = ctx.sessionKey ?? "";
|
|
18
22
|
const accountId = ctx.agentAccountId ?? "";
|
|
19
23
|
return {
|
|
@@ -32,8 +36,8 @@ function createQueryGroupInfoTool(ctx) {
|
|
|
32
36
|
* 1. No groupCode -> inform model no group context
|
|
33
37
|
* 2. Call queryGroupInfo to get basic group info
|
|
34
38
|
*/
|
|
35
|
-
async execute(
|
|
36
|
-
|
|
39
|
+
async execute(toolCallId, _params) {
|
|
40
|
+
log.debug("execute", { toolCallId });
|
|
37
41
|
const groupCode = extractGroupCode(sessionKey);
|
|
38
42
|
// 1. No groupCode -> cannot locate group
|
|
39
43
|
if (!groupCode) {
|
|
@@ -75,5 +79,7 @@ function createQueryGroupInfoTool(ctx) {
|
|
|
75
79
|
* - query_group_info: Query basic group info (always available)
|
|
76
80
|
*/
|
|
77
81
|
export function registerGroupTools(api) {
|
|
78
|
-
|
|
82
|
+
const log = createLog("tools.group");
|
|
83
|
+
log.info("register tool", { name: "query_group_info", optional: false });
|
|
84
|
+
api.registerTool(createQueryGroupInfoTool, { name: "query_group_info", optional: false });
|
|
79
85
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* For group owner info, use the query_group_info tool.
|
|
9
9
|
*/
|
|
10
10
|
import { getMember } from "../../infra/cache/member.js";
|
|
11
|
+
import { createLog } from "../../logger.js";
|
|
11
12
|
import { extractGroupCode, json } from "../utils/utils.js";
|
|
12
13
|
/** @mention hint text (used as JSON field value) */
|
|
13
14
|
const MENTION_HINT_TEXT = 'To @mention a user, you MUST use the format: space + @ + nickname + space (e.g. " @Alice ").';
|
|
@@ -92,7 +93,11 @@ function handleListAll(allMembers, mention) {
|
|
|
92
93
|
*
|
|
93
94
|
* Merges the original lookup_session_members and query_group_members into one tool.
|
|
94
95
|
* Prefers API-fetched full member list; session cache as fallback.
|
|
95
|
-
*/
|
|
96
|
+
*/
|
|
97
|
+
function createQuerySessionMembersTool(ctx) {
|
|
98
|
+
const log = createLog("tools.member");
|
|
99
|
+
if (!ctx.messageChannel?.includes('yuanbao'))
|
|
100
|
+
return null;
|
|
96
101
|
const sessionKey = ctx.sessionKey ?? "";
|
|
97
102
|
const accountId = ctx.agentAccountId ?? "";
|
|
98
103
|
return {
|
|
@@ -130,11 +135,11 @@ function handleListAll(allMembers, mention) {
|
|
|
130
135
|
* 2. Query via Member facade: prefer GroupMember (WS API) -> fallback SessionMember (cache)
|
|
131
136
|
* 3. Dispatch to action handlers
|
|
132
137
|
*/
|
|
133
|
-
async execute(
|
|
138
|
+
async execute(toolCallId, params) {
|
|
139
|
+
log.debug("execute", { toolCallId });
|
|
134
140
|
const action = typeof params.action === "string" ? params.action : "list_all";
|
|
135
141
|
const nameFilter = typeof params.name === "string" ? params.name.trim() : "";
|
|
136
142
|
const mention = params.mention === true || params.mention === "true";
|
|
137
|
-
// Extract groupCode from sessionKey
|
|
138
143
|
const groupCode = extractGroupCode(sessionKey);
|
|
139
144
|
if (!groupCode) {
|
|
140
145
|
return json({
|
|
@@ -169,5 +174,7 @@ function handleListAll(allMembers, mention) {
|
|
|
169
174
|
* - query_session_members: Query session members (always available)
|
|
170
175
|
*/
|
|
171
176
|
export function registerMemberTools(api) {
|
|
172
|
-
|
|
177
|
+
const log = createLog("tools.member");
|
|
178
|
+
log.info("register tool", { name: "query_session_members", optional: false });
|
|
179
|
+
api.registerTool(createQuerySessionMembersTool, { name: "query_session_members", optional: false });
|
|
173
180
|
}
|
|
@@ -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,112 @@
|
|
|
1
|
-
/** Scheduled reminder
|
|
1
|
+
/** Scheduled reminder tool (yuanbao_remind). */
|
|
2
|
+
import { createLog } from "../../logger.js";
|
|
2
3
|
import { json } from "../utils/utils.js";
|
|
3
|
-
|
|
4
|
+
let _callGatewayTool;
|
|
5
|
+
let _runPluginCmd;
|
|
6
|
+
async function resolveCallGatewayTool() {
|
|
7
|
+
if (_callGatewayTool !== undefined)
|
|
8
|
+
return _callGatewayTool;
|
|
9
|
+
try {
|
|
10
|
+
const sdkPath = 'openclaw/plugin-sdk/agent-harness-runtime';
|
|
11
|
+
const mod = await import(sdkPath);
|
|
12
|
+
_callGatewayTool = mod.callGatewayTool;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
_callGatewayTool = null;
|
|
16
|
+
}
|
|
17
|
+
return _callGatewayTool;
|
|
18
|
+
}
|
|
19
|
+
async function resolveRunPluginCommand() {
|
|
20
|
+
if (_runPluginCmd !== undefined)
|
|
21
|
+
return _runPluginCmd;
|
|
22
|
+
try {
|
|
23
|
+
const sdkPath = 'openclaw/plugin-sdk/matrix';
|
|
24
|
+
const mod = await import(sdkPath);
|
|
25
|
+
_runPluginCmd = mod.runPluginCommandWithTimeout;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
_runPluginCmd = null;
|
|
29
|
+
}
|
|
30
|
+
return _runPluginCmd;
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Constants
|
|
34
|
+
// ============================================================================
|
|
35
|
+
const DEFAULT_GATEWAY_TIMEOUT_MS = 60_000;
|
|
36
|
+
const DEFAULT_CLI_TIMEOUT_MS = 60_000;
|
|
4
37
|
const RemindSchema = {
|
|
5
|
-
type:
|
|
38
|
+
type: 'object',
|
|
6
39
|
properties: {
|
|
7
40
|
action: {
|
|
8
|
-
type:
|
|
9
|
-
enum: [
|
|
10
|
-
description:
|
|
11
|
-
+
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: ['add', 'list', 'remove'],
|
|
43
|
+
description: '操作类型: add=创建定时任务, list=查询已有任务, remove=删除任务。'
|
|
44
|
+
+ '删除前请先通过 list 获取 jobId。',
|
|
12
45
|
},
|
|
13
46
|
intent: {
|
|
14
|
-
type:
|
|
15
|
-
enum: [
|
|
16
|
-
description:
|
|
17
|
-
+ "
|
|
47
|
+
type: 'string',
|
|
48
|
+
enum: ['remind', 'task'],
|
|
49
|
+
description: '语义类型,决定触发后的行为模式。'
|
|
50
|
+
+ 'remind: 只需要提醒用户去做某事(如"休息一下""开会"); '
|
|
51
|
+
+ 'task: 需要 AI 实际执行并输出结果(如"查询今日新闻""检查服务状态")。'
|
|
52
|
+
+ '判断标准: 用户期望 AI 动手做事选 task,只需要一句提醒选 remind。',
|
|
18
53
|
},
|
|
19
54
|
content: {
|
|
20
|
-
type:
|
|
21
|
-
description:
|
|
22
|
-
+ '例如: "
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: '任务需求的详细内容。action=add 时必填。'
|
|
57
|
+
+ '例如: "提醒用户休息一下"、"查询今日新闻"、"检查服务状态"。',
|
|
23
58
|
},
|
|
24
59
|
time: {
|
|
25
|
-
type:
|
|
26
|
-
description:
|
|
27
|
-
+ "
|
|
28
|
-
+ '
|
|
29
|
-
+ "
|
|
30
|
-
},
|
|
31
|
-
timezone: {
|
|
32
|
-
type: "string",
|
|
33
|
-
description: "时区, 仅循环任务(cron)生效。默认 Asia/Shanghai。",
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: '时间描述, action=add 时必填。'
|
|
62
|
+
+ '绝对时间: ISO 8601 UTC 格式, 如 "2026-04-29T14:00:00.000Z"(一次性任务); '
|
|
63
|
+
+ '相对时间: 5m、1h、1h30m、2d(一次性任务); '
|
|
64
|
+
+ 'cron 表达式: "0 8 * * *"、"0 9 * * 1-5"(循环任务)。',
|
|
34
65
|
},
|
|
35
66
|
name: {
|
|
36
|
-
type:
|
|
37
|
-
description:
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: '任务名称(20字以内),action=add 时可基于 content 内容生成,便于后续 list/remove 管理。',
|
|
38
69
|
},
|
|
39
70
|
jobId: {
|
|
40
|
-
type:
|
|
41
|
-
description:
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: '任务 ID。仅 action=remove 时必填, 需先通过 action=list 获取。',
|
|
42
73
|
},
|
|
43
74
|
},
|
|
44
|
-
required: [
|
|
75
|
+
required: ['action'],
|
|
45
76
|
};
|
|
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
|
|
77
|
+
/** task mode: execute scheduled task, no reply length limit */
|
|
78
|
+
const TASK_AGENT_PROMPT_TEMPLATE = (content) => `你是一个任务执行助手。请在当前时刻完成以下任务:${content}。\n\n`
|
|
79
|
+
+ '## 要求\n'
|
|
80
|
+
+ '- 不要回复 HEARTBEAT_OK\n'
|
|
81
|
+
+ '- 不要解释你是谁\n'
|
|
82
|
+
+ '- 直接执行任务并输出可直接给用户的结果\n'
|
|
83
|
+
+ '- **禁止**调用 yuanbao_remind 或 cron 工具,**禁止**新建任何定时任务\n';
|
|
84
|
+
/** remind mode: warm reminder, keep reply brief */
|
|
85
|
+
const REMIND_AGENT_PROMPT_TEMPLATE = (content) => `你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:${content}。\n\n`
|
|
86
|
+
+ '## 要求\n'
|
|
87
|
+
+ '- 不要回复 HEARTBEAT_OK\n'
|
|
88
|
+
+ '- 不要解释你是谁\n'
|
|
89
|
+
+ '- 直接输出一条暖心的提醒消息,不要携带其他和该提醒无关的内容\n'
|
|
90
|
+
+ '- 可以加一句简短的关怀话语\n'
|
|
91
|
+
+ '- 控制在 2-3 句话以内\n'
|
|
92
|
+
+ '- **禁止**调用 yuanbao_remind 或 cron 工具,**禁止**新建任何定时任务\n';
|
|
93
|
+
const TOOL_DESCRIPTION = [
|
|
94
|
+
'创建/查询/删除元宝定时任务。当用户提出提醒、闹钟、定时、X分钟/小时后、每天/每周/每月/每年X点等需求时,必须调用本工具。',
|
|
95
|
+
'重要:调用后必须检查返回结果中的 status 字段判断操作是否完成。',
|
|
96
|
+
'status="ok" 表示已完成,可以告知用户;status="PENDING_CRON_CALL" 表示还需要调用 cron 工具才能生效。',
|
|
97
|
+
'禁止在 status 不是 "ok" 时告知用户任务已设置成功。',
|
|
98
|
+
].join('\n');
|
|
99
|
+
// Legacy fallback: imperative instructions guiding the model to call cron (concise, non-ignorable)
|
|
100
|
+
const LEGACY_ADD_NEXT_STEP = '你必须立即调用 cron 工具,将 cronToolParams 中的参数传入。只有 cron 返回成功后才能告知用户。未调用 cron 之前,禁止回复"已设置"或"已创建"。';
|
|
101
|
+
const LEGACY_LIST_NEXT_STEP = '你必须立即调用 cron 工具执行 list,获取全量任务后按 filter.to 过滤,只返回当前会话的任务。禁止在调用 cron 之前回复任务列表。';
|
|
102
|
+
const LEGACY_REMOVE_NEXT_STEP = '你必须立即调用 cron 工具执行 remove,传入 cronToolParams.jobId。只有 cron 返回成功后才能告知用户已删除。';
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Time parsing & formatting
|
|
105
|
+
// ============================================================================
|
|
99
106
|
function parseRelativeTime(raw) {
|
|
100
107
|
const s = raw.trim().toLowerCase();
|
|
101
|
-
if (!s)
|
|
108
|
+
if (!s)
|
|
102
109
|
return null;
|
|
103
|
-
}
|
|
104
110
|
if (/^\d+$/.test(s)) {
|
|
105
111
|
return parseInt(s, 10) * 60_000;
|
|
106
112
|
}
|
|
@@ -113,214 +119,505 @@ function parseRelativeTime(raw) {
|
|
|
113
119
|
const value = parseFloat(match[1]);
|
|
114
120
|
const unit = match[2];
|
|
115
121
|
switch (unit) {
|
|
116
|
-
case
|
|
122
|
+
case 'd':
|
|
117
123
|
totalMs += value * 86_400_000;
|
|
118
124
|
break;
|
|
119
|
-
case
|
|
125
|
+
case 'h':
|
|
120
126
|
totalMs += value * 3_600_000;
|
|
121
127
|
break;
|
|
122
|
-
case
|
|
128
|
+
case 'm':
|
|
123
129
|
totalMs += value * 60_000;
|
|
124
130
|
break;
|
|
125
|
-
case
|
|
131
|
+
case 's':
|
|
126
132
|
totalMs += value * 1_000;
|
|
127
133
|
break;
|
|
128
|
-
default:
|
|
129
|
-
break;
|
|
134
|
+
default: break;
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
return matched ? Math.round(totalMs) : null;
|
|
133
138
|
}
|
|
134
|
-
|
|
139
|
+
/** Parses an ISO 8601 absolute time string; returns epoch ms, or null if invalid/expired. */
|
|
140
|
+
function parseAbsoluteTime(time) {
|
|
141
|
+
if (!/\d{4}-\d{2}/.test(time))
|
|
142
|
+
return null;
|
|
143
|
+
const ms = Date.parse(time);
|
|
144
|
+
if (Number.isNaN(ms))
|
|
145
|
+
return null;
|
|
146
|
+
return ms > Date.now() ? ms : null;
|
|
147
|
+
}
|
|
135
148
|
function isCronExpression(timeText) {
|
|
136
149
|
const parts = timeText.trim().split(/\s+/);
|
|
137
150
|
return parts.length >= 3 && parts.length <= 6;
|
|
138
151
|
}
|
|
139
|
-
// Format delay duration
|
|
140
152
|
function formatDelay(ms) {
|
|
141
153
|
const seconds = Math.round(ms / 1000);
|
|
142
|
-
if (seconds < 60)
|
|
154
|
+
if (seconds < 60)
|
|
143
155
|
return `${seconds}秒`;
|
|
144
|
-
}
|
|
145
156
|
const minutes = Math.round(ms / 60_000);
|
|
146
|
-
if (minutes < 60)
|
|
157
|
+
if (minutes < 60)
|
|
147
158
|
return `${minutes}分钟`;
|
|
148
|
-
}
|
|
149
159
|
const hours = Math.floor(minutes / 60);
|
|
150
160
|
const remains = minutes % 60;
|
|
151
|
-
if (remains === 0)
|
|
161
|
+
if (remains === 0)
|
|
152
162
|
return `${hours}小时`;
|
|
153
|
-
}
|
|
154
163
|
return `${hours}小时${remains}分钟`;
|
|
155
164
|
}
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Session resolution
|
|
167
|
+
// ============================================================================
|
|
159
168
|
function resolveToFromSession(ctx) {
|
|
160
|
-
const sessionKey = ctx.sessionKey ??
|
|
161
|
-
const groupPrefix =
|
|
162
|
-
const directPrefix =
|
|
169
|
+
const sessionKey = ctx.sessionKey ?? '';
|
|
170
|
+
const groupPrefix = 'yuanbao:group:';
|
|
171
|
+
const directPrefix = 'yuanbao:direct:';
|
|
163
172
|
const groupIdx = sessionKey.indexOf(groupPrefix);
|
|
164
173
|
if (groupIdx !== -1) {
|
|
165
174
|
const groupCode = sessionKey.slice(groupIdx + groupPrefix.length).trim();
|
|
166
|
-
if (groupCode)
|
|
175
|
+
if (groupCode)
|
|
167
176
|
return `group:${groupCode}`;
|
|
168
|
-
}
|
|
169
177
|
}
|
|
170
178
|
const directIdx = sessionKey.indexOf(directPrefix);
|
|
171
179
|
if (directIdx !== -1) {
|
|
172
|
-
//
|
|
180
|
+
// sessionKey lowercases userId, so use requesterSenderId instead
|
|
173
181
|
const userId = ctx.requesterSenderId;
|
|
174
|
-
if (userId)
|
|
182
|
+
if (userId)
|
|
175
183
|
return `direct:${userId}`;
|
|
176
|
-
}
|
|
177
184
|
}
|
|
178
185
|
return null;
|
|
179
186
|
}
|
|
180
|
-
//
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Prompt & name generation
|
|
189
|
+
// ============================================================================
|
|
181
190
|
function buildReminderPrompt(content, intent) {
|
|
182
|
-
if (intent ===
|
|
191
|
+
if (intent === 'task') {
|
|
183
192
|
return TASK_AGENT_PROMPT_TEMPLATE(content);
|
|
184
193
|
}
|
|
185
194
|
return REMIND_AGENT_PROMPT_TEMPLATE(content);
|
|
186
195
|
}
|
|
187
|
-
|
|
188
|
-
function generateJobName(content, intent) {
|
|
196
|
+
function generateJobName(content) {
|
|
189
197
|
const text = content.trim();
|
|
190
|
-
|
|
191
|
-
return `${intent === "task" ? "任务" : "提醒"}: ${short}`;
|
|
198
|
+
return text.length > 20 ? `${text.slice(0, 20)}...` : text;
|
|
192
199
|
}
|
|
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) {
|
|
200
|
+
/** Resolves shared fields for a one-time job. */
|
|
201
|
+
function resolveOnceBase(params, time, intent) {
|
|
200
202
|
const content = params.content;
|
|
201
|
-
const at = `${Math.max(1, Math.round(delayMs / 1000))}s`;
|
|
202
203
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
session: "isolated",
|
|
207
|
-
deleteAfterRun: true,
|
|
204
|
+
name: params.name || generateJobName(content),
|
|
205
|
+
atMs: time.type === 'absolute' ? time.atMs : Date.now() + time.delayMs,
|
|
206
|
+
atStr: time.type === 'absolute' ? time.iso : `${Math.max(1, Math.round(time.delayMs / 1000))}s`,
|
|
208
207
|
message: buildReminderPrompt(content, intent),
|
|
209
|
-
channel: "yuanbao",
|
|
210
|
-
to,
|
|
211
208
|
};
|
|
212
209
|
}
|
|
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) {
|
|
210
|
+
/** Resolves shared fields for a recurring (cron) job. */
|
|
211
|
+
function resolveCronBase(params, intent) {
|
|
220
212
|
const content = params.content;
|
|
221
213
|
return {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
tz: params.timezone || "Asia/Shanghai",
|
|
226
|
-
session: "isolated",
|
|
227
|
-
deleteAfterRun: false,
|
|
214
|
+
name: params.name || generateJobName(content),
|
|
215
|
+
expr: params.time.trim(),
|
|
216
|
+
tz: params.timezone || 'Asia/Shanghai',
|
|
228
217
|
message: buildReminderPrompt(content, intent),
|
|
229
|
-
channel: "yuanbao",
|
|
230
|
-
to,
|
|
231
218
|
};
|
|
232
219
|
}
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Gateway mode: job builders
|
|
222
|
+
// ============================================================================
|
|
223
|
+
/** Builds a Gateway job config for a one-time job. */
|
|
224
|
+
function buildOnceJob(params, time, to, accountId, intent) {
|
|
225
|
+
const { name, atMs, message } = resolveOnceBase(params, time, intent);
|
|
226
|
+
return {
|
|
227
|
+
name,
|
|
228
|
+
schedule: { kind: 'at', atMs },
|
|
229
|
+
sessionTarget: 'isolated',
|
|
230
|
+
wakeMode: 'now',
|
|
231
|
+
deleteAfterRun: true,
|
|
232
|
+
payload: { kind: 'agentTurn', message },
|
|
233
|
+
delivery: { mode: 'announce', channel: 'yuanbao', to, accountId },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Builds a Gateway job config for a recurring (cron) job. */
|
|
237
|
+
function buildCronJob(params, to, accountId, intent) {
|
|
238
|
+
const { name, expr, tz, message } = resolveCronBase(params, intent);
|
|
239
|
+
return {
|
|
240
|
+
name,
|
|
241
|
+
schedule: { kind: 'cron', expr, tz },
|
|
242
|
+
sessionTarget: 'isolated',
|
|
243
|
+
wakeMode: 'now',
|
|
244
|
+
payload: { kind: 'agentTurn', message },
|
|
245
|
+
delivery: { mode: 'announce', channel: 'yuanbao', to, accountId },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// CLI mode: argv builders
|
|
250
|
+
// ============================================================================
|
|
251
|
+
/** Builds CLI argv for `openclaw cron add --at ...` (one-time job). */
|
|
252
|
+
function buildOnceCliArgs(params, time, to, accountId, intent) {
|
|
253
|
+
const { name, atStr, message } = resolveOnceBase(params, time, intent);
|
|
254
|
+
const argv = [
|
|
255
|
+
'openclaw', 'cron', 'add',
|
|
256
|
+
'--name', name,
|
|
257
|
+
'--at', atStr,
|
|
258
|
+
'--message', message,
|
|
259
|
+
'--session', 'isolated',
|
|
260
|
+
'--delete-after-run',
|
|
261
|
+
'--announce',
|
|
262
|
+
'--channel', 'yuanbao',
|
|
263
|
+
'--to', to,
|
|
264
|
+
'--json',
|
|
265
|
+
];
|
|
266
|
+
if (accountId)
|
|
267
|
+
argv.push('--account', accountId);
|
|
268
|
+
return argv;
|
|
269
|
+
}
|
|
270
|
+
/** Builds CLI argv for `openclaw cron add --cron ...` (recurring job). */
|
|
271
|
+
function buildCronCliArgs(params, to, accountId, intent) {
|
|
272
|
+
const { name, expr, tz, message } = resolveCronBase(params, intent);
|
|
273
|
+
const argv = [
|
|
274
|
+
'openclaw', 'cron', 'add',
|
|
275
|
+
'--name', name,
|
|
276
|
+
'--cron', expr,
|
|
277
|
+
'--tz', tz,
|
|
278
|
+
'--message', message,
|
|
279
|
+
'--session', 'isolated',
|
|
280
|
+
'--announce',
|
|
281
|
+
'--channel', 'yuanbao',
|
|
282
|
+
'--to', to,
|
|
283
|
+
'--json',
|
|
284
|
+
];
|
|
285
|
+
if (accountId)
|
|
286
|
+
argv.push('--account', accountId);
|
|
287
|
+
return argv;
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Legacy mode: cron params builders (used by executeLegacy)
|
|
291
|
+
// ============================================================================
|
|
292
|
+
/** Builds Legacy cronToolParams for a one-time job. */
|
|
293
|
+
function buildOnceLegacyParams(params, time, to, intent) {
|
|
294
|
+
const { name, atStr, message } = resolveOnceBase(params, time, intent);
|
|
295
|
+
return { action: 'add', name, at: atStr, session: 'isolated', deleteAfterRun: true, message, channel: 'yuanbao', to };
|
|
296
|
+
}
|
|
297
|
+
/** Builds Legacy cronToolParams for a recurring (cron) job. */
|
|
298
|
+
function buildCronLegacyParams(params, to, intent) {
|
|
299
|
+
const { name, expr, tz, message } = resolveCronBase(params, intent);
|
|
300
|
+
return { action: 'add', name, cron: expr, tz, session: 'isolated', deleteAfterRun: false, message, channel: 'yuanbao', to };
|
|
301
|
+
}
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Result filtering & error formatting
|
|
304
|
+
// ============================================================================
|
|
305
|
+
function filterJobsByTarget(cronResult, to) {
|
|
306
|
+
let jobs;
|
|
307
|
+
if (Array.isArray(cronResult)) {
|
|
308
|
+
jobs = cronResult;
|
|
309
|
+
}
|
|
310
|
+
else if (cronResult && typeof cronResult === 'object') {
|
|
311
|
+
const r = cronResult;
|
|
312
|
+
jobs = Array.isArray(r.jobs) ? r.jobs : [];
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
jobs = [];
|
|
316
|
+
}
|
|
317
|
+
return jobs.filter((job) => {
|
|
318
|
+
const j = job;
|
|
319
|
+
const delivery = (j.delivery ?? j.job?.delivery);
|
|
320
|
+
if (delivery?.channel !== 'yuanbao')
|
|
321
|
+
return false;
|
|
322
|
+
return !to || delivery?.to === to;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function formatTimeLabel(time, intent) {
|
|
326
|
+
if (time.type === 'absolute') {
|
|
327
|
+
const d = new Date(time.atMs);
|
|
328
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
329
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
330
|
+
const label = intent === 'task' ? '执行任务' : '提醒';
|
|
331
|
+
return `${d.getMonth() + 1}/${d.getDate()} ${hh}:${mm} ${label}`;
|
|
332
|
+
}
|
|
333
|
+
const suffix = intent === 'task' ? '后执行任务' : '后提醒';
|
|
334
|
+
return `${formatDelay(time.delayMs)}${suffix}`;
|
|
335
|
+
}
|
|
336
|
+
function formatSchedulerError(error) {
|
|
337
|
+
return error instanceof Error ? error.message : String(error);
|
|
338
|
+
}
|
|
339
|
+
function tryParseJson(text) {
|
|
340
|
+
try {
|
|
341
|
+
return JSON.parse(text);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Shared parameter validation
|
|
349
|
+
// ============================================================================
|
|
350
|
+
function validateAddParams(p, resolvedTo) {
|
|
351
|
+
if (!p.content?.trim())
|
|
352
|
+
return { error: 'action=add 时 content 为必填。' };
|
|
353
|
+
if (!p.time?.trim())
|
|
354
|
+
return { error: 'action=add 时 time 为必填。示例:5m / 1h30m / 0 8 * * *' };
|
|
355
|
+
if (!resolvedTo)
|
|
356
|
+
return { error: '无法确定投递目标。请确保在元宝会话中发起请求。' };
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
function validateRemoveParams(p) {
|
|
360
|
+
if (!p.jobId?.trim())
|
|
361
|
+
return { error: 'action=remove 时 jobId 为必填,请先调用 action=list 获取任务 ID。' };
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
function parseAndValidateTime(time) {
|
|
365
|
+
const atMs = parseAbsoluteTime(time);
|
|
366
|
+
if (atMs !== null) {
|
|
367
|
+
if (atMs - Date.now() < 30_000)
|
|
368
|
+
return { error: '提醒时间不能少于 30 秒。' };
|
|
369
|
+
return { timeSpec: { type: 'absolute', atMs, iso: new Date(atMs).toISOString() } };
|
|
370
|
+
}
|
|
371
|
+
const delayMs = parseRelativeTime(time);
|
|
372
|
+
if (!delayMs || delayMs <= 0) {
|
|
373
|
+
return { error: `无法解析时间 "${time}"。支持 ISO 时间(如 2026-04-29T14:00:00Z)、相对时间(5m/1h/1h30m/2d)或 cron 表达式(如 0 8 * * *)。` };
|
|
374
|
+
}
|
|
375
|
+
if (delayMs < 30_000)
|
|
376
|
+
return { error: '提醒时间不能少于 30 秒。' };
|
|
377
|
+
return { timeSpec: { type: 'relative', delayMs } };
|
|
378
|
+
}
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// Gateway execute implementation
|
|
381
|
+
// ============================================================================
|
|
382
|
+
async function executeGateway(gatewayTool, p, resolvedTo, accountId) {
|
|
383
|
+
switch (p.action) {
|
|
384
|
+
case 'list': {
|
|
385
|
+
try {
|
|
386
|
+
const cronResult = await gatewayTool('cron.list', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, {});
|
|
387
|
+
const filtered = filterJobsByTarget(cronResult, resolvedTo);
|
|
388
|
+
return json({ status: 'ok', action: 'list', _via: 'gateway', jobs: filtered });
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return json({ error: `查询定时任务失败: ${formatSchedulerError(error)}` });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
case 'remove': {
|
|
395
|
+
const err = validateRemoveParams(p);
|
|
396
|
+
if (err)
|
|
397
|
+
return json(err);
|
|
398
|
+
try {
|
|
399
|
+
const cronResult = await gatewayTool('cron.remove', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { jobId: p.jobId.trim() });
|
|
400
|
+
return json({ status: 'ok', action: 'remove', _via: 'gateway', cronResult });
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return json({ error: `删除定时任务失败: ${formatSchedulerError(error)}` });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
case 'add': {
|
|
407
|
+
const addErr = validateAddParams(p, resolvedTo);
|
|
408
|
+
if (addErr)
|
|
409
|
+
return json(addErr);
|
|
410
|
+
const intent = p.intent ?? 'remind';
|
|
411
|
+
if (isCronExpression(p.time)) {
|
|
412
|
+
const job = buildCronJob({ ...p, content: p.content.trim() }, resolvedTo, accountId, intent);
|
|
413
|
+
try {
|
|
414
|
+
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { job });
|
|
415
|
+
const typeLabel = intent === 'task' ? '循环任务' : '周期提醒';
|
|
416
|
+
return json({
|
|
417
|
+
status: 'ok',
|
|
418
|
+
action: 'add',
|
|
419
|
+
_via: 'gateway',
|
|
420
|
+
summary: `${typeLabel}: "${p.content.trim()}" (${p.time.trim()}, tz=${p.timezone || 'Asia/Shanghai'})`,
|
|
421
|
+
cronResult,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
return json({ error: `创建周期任务失败: ${formatSchedulerError(error)}` });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const timeResult = parseAndValidateTime(p.time);
|
|
429
|
+
if ('error' in timeResult)
|
|
430
|
+
return json(timeResult);
|
|
431
|
+
const job = buildOnceJob({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, accountId, intent);
|
|
432
|
+
try {
|
|
433
|
+
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { job });
|
|
434
|
+
return json({
|
|
435
|
+
status: 'ok',
|
|
436
|
+
action: 'add',
|
|
437
|
+
_via: 'gateway',
|
|
438
|
+
summary: `${formatTimeLabel(timeResult.timeSpec, intent)}: "${p.content.trim()}"`,
|
|
439
|
+
cronResult,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
return json({ error: `创建定时任务失败: ${formatSchedulerError(error)}` });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
default:
|
|
447
|
+
return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// CLI execute implementation
|
|
452
|
+
// Returns null on command failure; validation errors return json directly (no fallback).
|
|
453
|
+
// ============================================================================
|
|
233
454
|
/**
|
|
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.
|
|
455
|
+
* CLI execution tier. Returns null on command failure to trigger Legacy fallback.
|
|
240
456
|
*/
|
|
457
|
+
async function executeCli(runner, p, resolvedTo, accountId) {
|
|
458
|
+
switch (p.action) {
|
|
459
|
+
case 'list': {
|
|
460
|
+
try {
|
|
461
|
+
const result = await runner({ argv: ['openclaw', 'cron', 'list', '--json'], timeoutMs: DEFAULT_CLI_TIMEOUT_MS });
|
|
462
|
+
if (result.code !== 0)
|
|
463
|
+
return null;
|
|
464
|
+
const parsed = tryParseJson(result.stdout);
|
|
465
|
+
if (parsed) {
|
|
466
|
+
const filtered = filterJobsByTarget(parsed, resolvedTo);
|
|
467
|
+
return json({ status: 'ok', action: 'list', _via: 'cli', jobs: filtered });
|
|
468
|
+
}
|
|
469
|
+
return json({ status: 'ok', action: 'list', _via: 'cli', raw: result.stdout.trim() });
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
case 'remove': {
|
|
476
|
+
const err = validateRemoveParams(p);
|
|
477
|
+
if (err)
|
|
478
|
+
return json(err);
|
|
479
|
+
try {
|
|
480
|
+
const result = await runner({
|
|
481
|
+
argv: ['openclaw', 'cron', 'rm', p.jobId.trim(), '--json'],
|
|
482
|
+
timeoutMs: DEFAULT_CLI_TIMEOUT_MS,
|
|
483
|
+
});
|
|
484
|
+
if (result.code !== 0)
|
|
485
|
+
return null;
|
|
486
|
+
const parsed = tryParseJson(result.stdout);
|
|
487
|
+
return json({ status: 'ok', action: 'remove', _via: 'cli', cronResult: parsed ?? result.stdout.trim() });
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
case 'add': {
|
|
494
|
+
const addErr = validateAddParams(p, resolvedTo);
|
|
495
|
+
if (addErr)
|
|
496
|
+
return json(addErr);
|
|
497
|
+
const intent = p.intent ?? 'remind';
|
|
498
|
+
let argv;
|
|
499
|
+
let summary;
|
|
500
|
+
if (isCronExpression(p.time)) {
|
|
501
|
+
argv = buildCronCliArgs({ ...p, content: p.content.trim() }, resolvedTo, accountId, intent);
|
|
502
|
+
const typeLabel = intent === 'task' ? '循环任务' : '周期提醒';
|
|
503
|
+
summary = `${typeLabel}: "${p.content.trim()}" (${p.time.trim()}, tz=${p.timezone || 'Asia/Shanghai'})`;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const timeResult = parseAndValidateTime(p.time);
|
|
507
|
+
if ('error' in timeResult)
|
|
508
|
+
return json(timeResult);
|
|
509
|
+
argv = buildOnceCliArgs({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, accountId, intent);
|
|
510
|
+
summary = `${formatTimeLabel(timeResult.timeSpec, intent)}: "${p.content.trim()}"`;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const result = await runner({ argv, timeoutMs: DEFAULT_CLI_TIMEOUT_MS });
|
|
514
|
+
if (result.code !== 0)
|
|
515
|
+
return null;
|
|
516
|
+
const parsed = tryParseJson(result.stdout);
|
|
517
|
+
return json({ status: 'ok', action: 'add', _via: 'cli', summary, cronResult: parsed ?? result.stdout.trim() });
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
default:
|
|
524
|
+
return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// Legacy execute implementation
|
|
529
|
+
// ============================================================================
|
|
530
|
+
/** Returns PENDING_CRON_CALL to guide the model to invoke the cron tool. */
|
|
531
|
+
function executeLegacy(p, resolvedTo) {
|
|
532
|
+
switch (p.action) {
|
|
533
|
+
case 'list':
|
|
534
|
+
return json({
|
|
535
|
+
status: 'PENDING_CRON_CALL',
|
|
536
|
+
_via: 'legacy',
|
|
537
|
+
completed: false,
|
|
538
|
+
next_step: LEGACY_LIST_NEXT_STEP,
|
|
539
|
+
cronToolParams: { action: 'list' },
|
|
540
|
+
filter: { to: resolvedTo },
|
|
541
|
+
});
|
|
542
|
+
case 'remove': {
|
|
543
|
+
const err = validateRemoveParams(p);
|
|
544
|
+
if (err)
|
|
545
|
+
return json(err);
|
|
546
|
+
return json({
|
|
547
|
+
status: 'PENDING_CRON_CALL',
|
|
548
|
+
_via: 'legacy',
|
|
549
|
+
completed: false,
|
|
550
|
+
next_step: LEGACY_REMOVE_NEXT_STEP,
|
|
551
|
+
cronToolParams: { action: 'remove', jobId: p.jobId.trim() },
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
case 'add': {
|
|
555
|
+
const addErr = validateAddParams(p, resolvedTo);
|
|
556
|
+
if (addErr)
|
|
557
|
+
return json(addErr);
|
|
558
|
+
const intent = p.intent ?? 'remind';
|
|
559
|
+
if (isCronExpression(p.time)) {
|
|
560
|
+
const cronToolParams = buildCronLegacyParams({ ...p, content: p.content.trim() }, resolvedTo, intent);
|
|
561
|
+
return json({
|
|
562
|
+
status: 'PENDING_CRON_CALL',
|
|
563
|
+
_via: 'legacy',
|
|
564
|
+
completed: false,
|
|
565
|
+
next_step: LEGACY_ADD_NEXT_STEP,
|
|
566
|
+
cronToolParams,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const timeResult = parseAndValidateTime(p.time);
|
|
570
|
+
if ('error' in timeResult)
|
|
571
|
+
return json(timeResult);
|
|
572
|
+
const cronToolParams = buildOnceLegacyParams({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, intent);
|
|
573
|
+
return json({
|
|
574
|
+
status: 'PENDING_CRON_CALL',
|
|
575
|
+
_via: 'legacy',
|
|
576
|
+
completed: false,
|
|
577
|
+
next_step: LEGACY_ADD_NEXT_STEP,
|
|
578
|
+
cronToolParams,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
default:
|
|
582
|
+
return json({ error: `不支持的 action: ${String(p.action)}。可选值: add/list/remove。` });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// Tool definition & registration
|
|
587
|
+
// ============================================================================
|
|
588
|
+
/** Returns null for non-yuanbao channels. */
|
|
241
589
|
function createYuanbaoRemindTool(ctx) {
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
590
|
+
const log = createLog("tools.remind");
|
|
591
|
+
if (!ctx.messageChannel?.includes('yuanbao'))
|
|
244
592
|
return null;
|
|
245
|
-
|
|
246
|
-
|
|
593
|
+
const resolvedTo = resolveToFromSession(ctx);
|
|
594
|
+
const accountId = ctx.deliveryContext?.accountId ?? ctx.agentAccountId ?? '';
|
|
247
595
|
return {
|
|
248
|
-
name:
|
|
249
|
-
label:
|
|
250
|
-
description:
|
|
596
|
+
name: 'yuanbao_remind',
|
|
597
|
+
label: '元宝定时任务',
|
|
598
|
+
description: TOOL_DESCRIPTION,
|
|
251
599
|
parameters: RemindSchema,
|
|
252
|
-
|
|
600
|
+
async execute(toolCallId, params) {
|
|
601
|
+
log.debug("execute", { toolCallId });
|
|
253
602
|
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。` });
|
|
603
|
+
const gatewayTool = await resolveCallGatewayTool();
|
|
604
|
+
if (gatewayTool) {
|
|
605
|
+
return executeGateway(gatewayTool, p, resolvedTo, accountId);
|
|
315
606
|
}
|
|
607
|
+
const runner = await resolveRunPluginCommand();
|
|
608
|
+
if (runner) {
|
|
609
|
+
const cliResult = await executeCli(runner, p, resolvedTo, accountId);
|
|
610
|
+
if (cliResult !== null)
|
|
611
|
+
return cliResult;
|
|
612
|
+
// CLI command failed (non-zero exit or exception), fall through to Legacy
|
|
613
|
+
}
|
|
614
|
+
// Final fallback: guide the model to call cron via PENDING response
|
|
615
|
+
return executeLegacy(p, resolvedTo);
|
|
316
616
|
},
|
|
317
617
|
};
|
|
318
618
|
}
|
|
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
619
|
export function registerRemindTools(api) {
|
|
325
|
-
|
|
620
|
+
const log = createLog("tools.remind");
|
|
621
|
+
log.info("register tool", { name: "yuanbao_remind", optional: false });
|
|
622
|
+
api.registerTool(createYuanbaoRemindTool, { name: "yuanbao_remind", optional: false });
|
|
326
623
|
}
|
|
@@ -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