@jerryliang122/openclaw-qqbot 1.0.1 → 1.0.3
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/index.cjs +7343 -7210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/adapter/pairing.ts +14 -1
- package/src/adapter/webhook.ts +16 -3
- package/src/commands/bot-approve.ts +2 -2
- package/src/commands/bot-group-always.ts +1 -1
- package/src/commands/bot-streaming.ts +1 -1
- package/src/commands/config-util.ts +8 -0
- package/src/commands/index.ts +49 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/features/approval-capability.ts +19 -6
- package/src/features/credential-backup.ts +8 -3
- package/src/features/history-store.ts +8 -1
- package/src/features/quota-manager.ts +5 -2
- package/src/features/typing-refresh.ts +9 -1
- package/src/gateway/event-handlers.ts +36 -12
- package/src/gateway/lifecycle.ts +8 -0
- package/src/gateway/qqbot-gateway.ts +24 -1
- package/src/middleware/access-control.ts +3 -1
- package/src/middleware/secret-capture.ts +5 -3
- package/src/outbound/debounce.ts +19 -2
- package/src/outbound-adapter.ts +10 -1
- package/src/setup/finalize.ts +3 -2
- package/src/tools/platform.ts +8 -0
- package/src/tools/secret-input.ts +8 -0
- package/src/types-plugin.ts +1 -0
- package/src/typing-lifecycle.ts +9 -4
- package/skills/qqbot-remind/SKILL.md +0 -159
|
@@ -49,14 +49,16 @@ async function replyToUser(
|
|
|
49
49
|
try {
|
|
50
50
|
await ctx.bot.sendText(ctx.replyTarget, text);
|
|
51
51
|
return;
|
|
52
|
-
} catch {
|
|
52
|
+
} catch (err) {
|
|
53
53
|
rollback();
|
|
54
|
-
|
|
54
|
+
ctx.log?.warn?.(`[secret] passive reply failed, falling back to proactive: ${err instanceof Error ? err.message : String(err)}`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const proactiveTarget = { scope: 'c2c' as const, targetId: ctx.message.senderId as string };
|
|
59
|
-
await ctx.bot.sendText(proactiveTarget, text).catch(() => {
|
|
59
|
+
await ctx.bot.sendText(proactiveTarget, text).catch((err: unknown) => {
|
|
60
|
+
ctx.log?.warn?.(`[secret] proactive reply failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function formatSetFailure(name: string, result: SecretsStoreSetResult): string {
|
package/src/outbound/debounce.ts
CHANGED
|
@@ -13,6 +13,9 @@ const DEFAULT_WINDOW_MS = 1500;
|
|
|
13
13
|
const DEFAULT_MAX_WAIT_MS = 8000;
|
|
14
14
|
const DEFAULT_SEPARATOR = '\n\n---\n\n';
|
|
15
15
|
|
|
16
|
+
// 最小结构类型:兼容 PluginLogger 与 SDK Logger(与 config-util 同款)
|
|
17
|
+
type WarnSink = { warn?: (msg: string, meta?: Record<string, unknown>) => void };
|
|
18
|
+
|
|
16
19
|
interface PendingDeliver {
|
|
17
20
|
texts: string[];
|
|
18
21
|
firstAt: number;
|
|
@@ -26,15 +29,18 @@ export class DeliverDebouncer {
|
|
|
26
29
|
private readonly maxWaitMs: number;
|
|
27
30
|
private readonly separator: string;
|
|
28
31
|
private readonly flush: (targetId: string, mergedText: string) => Promise<void>;
|
|
32
|
+
private readonly log?: WarnSink;
|
|
29
33
|
|
|
30
34
|
constructor(
|
|
31
35
|
config: DeliverDebounceConfig | undefined,
|
|
32
36
|
flush: (targetId: string, mergedText: string) => Promise<void>,
|
|
37
|
+
log?: WarnSink,
|
|
33
38
|
) {
|
|
34
39
|
this.windowMs = config?.windowMs ?? DEFAULT_WINDOW_MS;
|
|
35
40
|
this.maxWaitMs = config?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
36
41
|
this.separator = config?.separator ?? DEFAULT_SEPARATOR;
|
|
37
42
|
this.flush = flush;
|
|
43
|
+
this.log = log;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
get enabled(): boolean {
|
|
@@ -60,7 +66,7 @@ export class DeliverDebouncer {
|
|
|
60
66
|
if (Date.now() - existing.firstAt >= this.maxWaitMs) {
|
|
61
67
|
await this.doFlush(targetId);
|
|
62
68
|
} else {
|
|
63
|
-
existing.timer = setTimeout(() => this.
|
|
69
|
+
existing.timer = setTimeout(() => this.scheduledFlush(targetId), this.windowMs);
|
|
64
70
|
}
|
|
65
71
|
return;
|
|
66
72
|
}
|
|
@@ -70,13 +76,24 @@ export class DeliverDebouncer {
|
|
|
70
76
|
const pending: PendingDeliver = {
|
|
71
77
|
texts: [text],
|
|
72
78
|
firstAt: Date.now(),
|
|
73
|
-
timer: setTimeout(() => this.
|
|
79
|
+
timer: setTimeout(() => this.scheduledFlush(targetId), this.windowMs),
|
|
74
80
|
resolve,
|
|
75
81
|
};
|
|
76
82
|
this.pending.set(targetId, pending);
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 定时器触发的 flush:promise 被 setTimeout 回调丢弃,失败若不就地
|
|
88
|
+
* 捕获会成为 unhandledRejection——记 WARN 封口(pending.resolve 在
|
|
89
|
+
* doFlush 的 finally 中仍会执行,入队方不会被悬挂)。
|
|
90
|
+
*/
|
|
91
|
+
private scheduledFlush(targetId: string): void {
|
|
92
|
+
void this.doFlush(targetId).catch((err: unknown) => {
|
|
93
|
+
this.log?.warn?.(`[debounce] timer flush failed target=${targetId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
private async doFlush(targetId: string): Promise<void> {
|
|
81
98
|
const pending = this.pending.get(targetId);
|
|
82
99
|
if (!pending) return;
|
package/src/outbound-adapter.ts
CHANGED
|
@@ -28,6 +28,11 @@ import { isApprovalPayload } from './features/approval-utils.js';
|
|
|
28
28
|
import { TEXT_CHUNK_LIMIT } from './constants.js';
|
|
29
29
|
import { tryGetQQBotRuntime } from './runtime.js';
|
|
30
30
|
import { getAdapters } from './adapter/resolve.js';
|
|
31
|
+
import { createPluginLogger } from './utils/plugin-logger.js';
|
|
32
|
+
|
|
33
|
+
// 契约入口(框架直发 / message 工具 / cron announce)无调用方 logger 可传,
|
|
34
|
+
// 用模块级 logger 兜底——frameworkSink 每次调用时 resolve,register 早期自动降级 console
|
|
35
|
+
const alog = createPluginLogger({ prefix: '[outbound]' });
|
|
31
36
|
|
|
32
37
|
export type SendTextFn = (params: {
|
|
33
38
|
to: string;
|
|
@@ -178,6 +183,8 @@ export function createQQBotOutboundAdapter(params: QQBotOutboundAdapterParams):
|
|
|
178
183
|
if (canReply && replyToId && result.error) {
|
|
179
184
|
rollback();
|
|
180
185
|
log?.debug?.(`[${resolvedAccountId}] rollback quota: send failed`);
|
|
186
|
+
} else if (!canReply) {
|
|
187
|
+
log?.info?.(`[${resolvedAccountId}] [quota] fallback to proactive media send: quota exhausted or no msgId`);
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
return result;
|
|
@@ -287,6 +294,7 @@ export function createQQBotChannelOutbound(params: QQBotOutboundAdapterParams =
|
|
|
287
294
|
accountId: ctx.accountId ?? undefined,
|
|
288
295
|
replyToId: resolvePassiveFirstReplyToId(ctx.to, ctx.replyToId),
|
|
289
296
|
account,
|
|
297
|
+
log: alog,
|
|
290
298
|
});
|
|
291
299
|
if (result.error) throw new Error(result.error);
|
|
292
300
|
return { channel: 'qqbot', messageId: result.messageId ?? '' };
|
|
@@ -302,7 +310,8 @@ export function createQQBotChannelOutbound(params: QQBotOutboundAdapterParams =
|
|
|
302
310
|
accountId: resolvedAccountId,
|
|
303
311
|
replyToId: resolvePassiveFirstReplyToId(ctx.to, ctx.replyToId),
|
|
304
312
|
account,
|
|
305
|
-
agentId: resolveMCPAgentId(ctx.to, resolvedAccountId, ctx.cfg),
|
|
313
|
+
agentId: resolveMCPAgentId(ctx.to, resolvedAccountId, ctx.cfg, alog),
|
|
314
|
+
log: alog,
|
|
306
315
|
});
|
|
307
316
|
if (result.error) throw new Error(result.error);
|
|
308
317
|
return { channel: 'qqbot', messageId: result.messageId ?? '' };
|
package/src/setup/finalize.ts
CHANGED
|
@@ -49,13 +49,14 @@ async function linkViaQrCode(cfg: OpenClawConfig, _accountId: string, prompter:
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function linkViaManual(cfg: OpenClawConfig, _accountId: string, prompter: Prompter): Promise<OpenClawConfig> {
|
|
52
|
+
async function linkViaManual(cfg: OpenClawConfig, _accountId: string, prompter: Prompter, rt: Runtime): Promise<OpenClawConfig> {
|
|
53
53
|
const appIdInput = await prompter.text({ message: '请输入 QQ Bot AppID', validate: (v) => v.trim() ? undefined : 'AppID 不能为空' });
|
|
54
54
|
const secret = await prompter.text({ message: '请输入 QQ Bot AppSecret', validate: (v) => v.trim() ? undefined : 'AppSecret 不能为空' });
|
|
55
55
|
const appId = appIdInput.trim();
|
|
56
56
|
const key = resolveAccountKey(cfg, appId);
|
|
57
57
|
let next = applyQQBotAccountConfig(cfg, key, { appId, clientSecret: secret.trim() });
|
|
58
58
|
next = applyAccountDefaults(next, key);
|
|
59
|
+
rt.log(`手动绑定完成!账户: ${key} (AppID: ${appId})`);
|
|
59
60
|
await prompter.note('✔ QQ Bot 配置完成!', 'QQ Bot');
|
|
60
61
|
return next;
|
|
61
62
|
}
|
|
@@ -82,7 +83,7 @@ export async function finalizeQQBotSetup(params: {
|
|
|
82
83
|
if (mode === 'qr') {
|
|
83
84
|
next = await linkViaQrCode(next, accountId, params.prompter, params.runtime);
|
|
84
85
|
} else if (mode === 'manual') {
|
|
85
|
-
next = await linkViaManual(next, accountId, params.prompter);
|
|
86
|
+
next = await linkViaManual(next, accountId, params.prompter, params.runtime);
|
|
86
87
|
} else if (!configured) {
|
|
87
88
|
await params.prompter.note('您可以稍后运行以下命令重新配置:\n openclaw channels add', 'QQ Bot');
|
|
88
89
|
}
|
package/src/tools/platform.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
|
2
2
|
import { listQQBotAccountIds } from '../config.js';
|
|
3
3
|
import { getBotForAccount } from '../bot-instance.js';
|
|
4
4
|
import { getRequestAccountId } from '../request-context.js';
|
|
5
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
6
|
+
|
|
7
|
+
// 工具 execute 无 logger 参数;request-context 活跃期间 enrichMeta
|
|
8
|
+
// 自动带上 accountId/openId/messageId
|
|
9
|
+
const toolLog = createPluginLogger({ prefix: '[tool:platform]' });
|
|
5
10
|
|
|
6
11
|
// ========== JSON Schema ==========
|
|
7
12
|
|
|
@@ -111,6 +116,7 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
|
111
116
|
try {
|
|
112
117
|
const bot = getBotForAccount(accountId);
|
|
113
118
|
const apiGateway = bot.api;
|
|
119
|
+
const startedAt = Date.now();
|
|
114
120
|
|
|
115
121
|
let data: unknown;
|
|
116
122
|
switch (method) {
|
|
@@ -131,10 +137,12 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
|
131
137
|
break;
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
toolLog.info(`${method} ${p.path} ok (${Date.now() - startedAt}ms)`, { accountId });
|
|
134
141
|
return json(data);
|
|
135
142
|
} catch (err: unknown) {
|
|
136
143
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
137
144
|
const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
|
|
145
|
+
toolLog.warn(`${method} ${p.path} failed status=${apiErr.httpStatus ?? '?'} code=${apiErr.bizCode ?? '?'}: ${errMsg}`, { accountId });
|
|
138
146
|
return json({
|
|
139
147
|
error: errMsg,
|
|
140
148
|
status: apiErr.httpStatus,
|
|
@@ -22,8 +22,13 @@ import {
|
|
|
22
22
|
} from '../features/secret-input-store.js';
|
|
23
23
|
import { getRequestAccountId, getRequestTarget } from '../request-context.js';
|
|
24
24
|
import { parseTarget } from '../outbound/target.js';
|
|
25
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
25
26
|
import type { InlineKeyboard, KeyboardButton } from '../types.js';
|
|
26
27
|
|
|
28
|
+
// 工具 execute 无 logger 参数可传;request-context 在工具执行期间处于活跃,
|
|
29
|
+
// 模块级 logger 的 enrichMeta 会自动带上 accountId/openId/messageId
|
|
30
|
+
const toolLog = createPluginLogger({ prefix: '[tool:secret-input]' });
|
|
31
|
+
|
|
27
32
|
// ========== JSON Schema ==========
|
|
28
33
|
|
|
29
34
|
const SecretInputSchema = {
|
|
@@ -162,11 +167,14 @@ export function registerSecretInputTool(api: OpenClawPluginApi): void {
|
|
|
162
167
|
);
|
|
163
168
|
} catch (err) {
|
|
164
169
|
cancelPendingSecretInput(accountId, parsed.targetId);
|
|
170
|
+
toolLog.error(`card send failed: ${err instanceof Error ? err.message : String(err)}`, { name, accountId });
|
|
165
171
|
return json({
|
|
166
172
|
error: `密钥输入卡片发送失败:${err instanceof Error ? err.message : String(err)}`,
|
|
167
173
|
});
|
|
168
174
|
}
|
|
169
175
|
|
|
176
|
+
toolLog.info(`card sent, pending registered name=${name} ttlMinutes=${DEFAULT_SECRET_INPUT_TTL_MS / 60_000}`);
|
|
177
|
+
|
|
170
178
|
return json({
|
|
171
179
|
ok: true,
|
|
172
180
|
name,
|
package/src/types-plugin.ts
CHANGED
package/src/typing-lifecycle.ts
CHANGED
|
@@ -59,14 +59,17 @@ export async function startTypingWithRenewal(
|
|
|
59
59
|
|
|
60
60
|
log?.debug?.(`[${accountId}] typing started: ${sessionKey}`);
|
|
61
61
|
|
|
62
|
-
const timer = setTimeout(
|
|
63
|
-
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
// promise 被 setTimeout 回调丢弃,失败不就地捕获会成为 unhandledRejection
|
|
64
|
+
void handleTypingRenewal({
|
|
64
65
|
sessionKey,
|
|
65
66
|
accountId,
|
|
66
67
|
to,
|
|
67
68
|
replyToId,
|
|
68
69
|
log,
|
|
69
70
|
sendTyping,
|
|
71
|
+
}).catch((err: unknown) => {
|
|
72
|
+
log?.warn?.(`[${accountId}] typing renewal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
73
|
});
|
|
71
74
|
}, TYPING_RENEWAL_MS);
|
|
72
75
|
|
|
@@ -120,8 +123,10 @@ async function handleTypingRenewal(
|
|
|
120
123
|
state.renewalCount += 1;
|
|
121
124
|
log?.debug?.(`[${accountId}] typing renewed #${state.renewalCount}: ${sessionKey}`);
|
|
122
125
|
|
|
123
|
-
state.timer = setTimeout(
|
|
124
|
-
|
|
126
|
+
state.timer = setTimeout(() => {
|
|
127
|
+
void handleTypingRenewal(params).catch((err: unknown) => {
|
|
128
|
+
log?.warn?.(`[${accountId}] typing renewal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
});
|
|
125
130
|
}, TYPING_RENEWAL_MS);
|
|
126
131
|
|
|
127
132
|
activeTypingSessions.set(sessionKey, state);
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: qqbot-remind
|
|
3
|
-
description: QQBot 定时提醒。支持一次性和周期性提醒的创建、查询、取消。当通过 QQ 通道通信且涉及提醒/定时任务时使用。
|
|
4
|
-
metadata: {"openclaw":{"emoji":"⏰","requires":{"config":["channels.qqbot"]}}}
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# QQ Bot 定时提醒
|
|
8
|
-
|
|
9
|
-
## ⚠️ 强制规则
|
|
10
|
-
|
|
11
|
-
**当用户提到「提醒」「闹钟」「定时」「X分钟/小时后」「每天X点」「叫我」等任何涉及延时或定时的请求时,你必须调用工具,绝对不能只用自然语言回复说"好的,我会提醒你"!**
|
|
12
|
-
|
|
13
|
-
你没有内存或后台线程,口头承诺"到时候提醒"是无效的——只有调用工具才能真正注册定时任务。
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 推荐流程(优先使用 `qqbot_remind` 工具)
|
|
18
|
-
|
|
19
|
-
**第一步**:调用 `qqbot_remind` 工具,传入简单参数:
|
|
20
|
-
|
|
21
|
-
| 参数 | 说明 | 示例 |
|
|
22
|
-
|------|------|------|
|
|
23
|
-
| `action` | 操作类型 | `"add"` / `"list"` / `"remove"` |
|
|
24
|
-
| `content` | 提醒内容 | `"喝水"` |
|
|
25
|
-
| `to` | 目标地址(可选,系统自动获取,通常无需填写) | — |
|
|
26
|
-
| `time` | 时间(相对时间或 cron 表达式) | `"5m"` / `"1h30m"` / `"0 8 * * *"` |
|
|
27
|
-
| `jobId` | 任务 ID(仅 remove) | `"xxx"` |
|
|
28
|
-
|
|
29
|
-
**第二步**:`qqbot_remind` 会返回 `cronParams`,你必须**立即**将 `cronParams` 的值**原样**作为参数调用 `cron` 工具。**禁止修改、省略或重组 `cronParams` 中的任何字段**(尤其是 `delivery` 对象中的 `accountId`)。
|
|
30
|
-
|
|
31
|
-
**第三步**:根据 `cron` 工具的返回结果,回复用户。
|
|
32
|
-
|
|
33
|
-
### 示例
|
|
34
|
-
|
|
35
|
-
用户说:"5分钟后提醒我喝水"
|
|
36
|
-
|
|
37
|
-
1. 调用 `qqbot_remind`:`{ "action": "add", "content": "喝水", "time": "5m" }`
|
|
38
|
-
2. 收到返回的 `cronParams` → 立即调用 `cron` 工具,参数为该 `cronParams`
|
|
39
|
-
3. 回复用户:`⏰ 好的,5分钟后提醒你喝水~`
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 备用方案(直接使用 `cron` 工具)
|
|
44
|
-
|
|
45
|
-
> 仅当 `qqbot_remind` 工具不可用时使用以下方式。
|
|
46
|
-
|
|
47
|
-
### 核心规则
|
|
48
|
-
|
|
49
|
-
> **payload.kind 必须是 `"agentTurn"`,绝对不能用 `"systemEvent"`!**
|
|
50
|
-
> `systemEvent` 只在 AI 会话内部注入文本,用户收不到 QQ 消息。
|
|
51
|
-
|
|
52
|
-
**5 个不可更改字段**:
|
|
53
|
-
|
|
54
|
-
| 字段 | 固定值 | 原因 |
|
|
55
|
-
|------|--------|------|
|
|
56
|
-
| `payload.kind` | `"agentTurn"` | `systemEvent` 不会发 QQ 消息 |
|
|
57
|
-
| `delivery.mode` | `"announce"` | 投递模式 |
|
|
58
|
-
| `delivery.channel` | `"qqbot"` | QQ 通道标识 |
|
|
59
|
-
| `delivery.to` | 用户 openid | 从 `To` 字段获取 |
|
|
60
|
-
| `sessionTarget` | `"isolated"` | 隔离会话避免污染 |
|
|
61
|
-
|
|
62
|
-
> `delivery.accountId` 必须填写当前会话的账户 ID(如果已知),以确保多账户场景下消息通过正确的机器人账户发送。
|
|
63
|
-
|
|
64
|
-
> `schedule.atMs` 必须是**绝对毫秒时间戳**(如 `1770733800000`),不支持 `"5m"` 等相对字符串。
|
|
65
|
-
> 计算方式:`当前时间戳ms + 延迟毫秒`。
|
|
66
|
-
|
|
67
|
-
### 一次性提醒(schedule.kind = "at")
|
|
68
|
-
|
|
69
|
-
```json
|
|
70
|
-
{
|
|
71
|
-
"action": "add",
|
|
72
|
-
"job": {
|
|
73
|
-
"name": "{任务名}",
|
|
74
|
-
"schedule": { "kind": "at", "atMs": "{当前时间戳ms + N*60000}" },
|
|
75
|
-
"sessionTarget": "isolated",
|
|
76
|
-
"wakeMode": "now",
|
|
77
|
-
"deleteAfterRun": true,
|
|
78
|
-
"payload": {
|
|
79
|
-
"kind": "agentTurn",
|
|
80
|
-
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"
|
|
81
|
-
},
|
|
82
|
-
"delivery": {
|
|
83
|
-
"mode": "announce",
|
|
84
|
-
"channel": "qqbot",
|
|
85
|
-
"to": "{openid}",
|
|
86
|
-
"accountId": "{accountId}"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### 周期提醒(schedule.kind = "cron")
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"action": "add",
|
|
97
|
-
"job": {
|
|
98
|
-
"name": "{任务名}",
|
|
99
|
-
"schedule": { "kind": "cron", "expr": "0 8 * * *", "tz": "Asia/Shanghai" },
|
|
100
|
-
"sessionTarget": "isolated",
|
|
101
|
-
"wakeMode": "now",
|
|
102
|
-
"payload": {
|
|
103
|
-
"kind": "agentTurn",
|
|
104
|
-
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"
|
|
105
|
-
},
|
|
106
|
-
"delivery": {
|
|
107
|
-
"mode": "announce",
|
|
108
|
-
"channel": "qqbot",
|
|
109
|
-
"to": "{openid}",
|
|
110
|
-
"accountId": "{accountId}"
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
> 周期任务**不加** `deleteAfterRun`。群聊 `to` 格式为 `"qqbot:group:{group_openid}"`。
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## cron 表达式速查
|
|
121
|
-
|
|
122
|
-
| 场景 | expr |
|
|
123
|
-
|------|------|
|
|
124
|
-
| 每天早上8点 | `"0 8 * * *"` |
|
|
125
|
-
| 每天晚上10点 | `"0 22 * * *"` |
|
|
126
|
-
| 工作日早上9点 | `"0 9 * * 1-5"` |
|
|
127
|
-
| 每周一早上9点 | `"0 9 * * 1"` |
|
|
128
|
-
| 每周末上午10点 | `"0 10 * * 0,6"` |
|
|
129
|
-
| 每小时整点 | `"0 * * * *"` |
|
|
130
|
-
|
|
131
|
-
> 周期提醒必须加 `"tz": "Asia/Shanghai"`。
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## AI 决策指南
|
|
136
|
-
|
|
137
|
-
| 用户说法 | action | time 格式 |
|
|
138
|
-
|----------|--------|-----------|
|
|
139
|
-
| "5分钟后提醒我喝水" | `add` | `"5m"` |
|
|
140
|
-
| "1小时后提醒开会" | `add` | `"1h"` |
|
|
141
|
-
| "每天8点提醒我打卡" | `add` | `"0 8 * * *"` |
|
|
142
|
-
| "工作日早上9点提醒" | `add` | `"0 9 * * 1-5"` |
|
|
143
|
-
| "我有哪些提醒" | `list` | — |
|
|
144
|
-
| "取消喝水提醒" | `remove` | — |
|
|
145
|
-
| "修改提醒时间" | `remove` → `add` | — |
|
|
146
|
-
| "提醒我"(无时间) | **需追问** | — |
|
|
147
|
-
|
|
148
|
-
纯相对时间("5分钟后"、"1小时后")可直接计算,无需确认。时间模糊或缺失时需追问。
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## 回复模板
|
|
153
|
-
|
|
154
|
-
- 一次性:`⏰ 好的,{时间}后提醒你{内容}~`
|
|
155
|
-
- 周期:`⏰ 收到,{周期}提醒你{内容}~`
|
|
156
|
-
- 查询无结果:`📋 目前没有提醒哦~ 说"5分钟后提醒我xxx"试试?`
|
|
157
|
-
- 删除成功:`✅ 已取消"{名称}"`
|
|
158
|
-
|
|
159
|
-
openclaw cron add \ --name "下班提醒" \ --at "2026-03-26T21:42:31+08:00" \ --message "test" \ --to …``
|