@gakr-gakr/qqbot 0.1.0
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/api.ts +56 -0
- package/autobot.plugin.json +167 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +33 -0
- package/package.json +64 -0
- package/runtime-api.ts +9 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/skills/qqbot-channel/SKILL.md +262 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +37 -0
- package/skills/qqbot-remind/SKILL.md +153 -0
- package/src/bridge/approval/capability.ts +225 -0
- package/src/bridge/approval/handler-runtime.ts +204 -0
- package/src/bridge/bootstrap.ts +135 -0
- package/src/bridge/channel-entry.ts +18 -0
- package/src/bridge/commands/framework-context-adapter.ts +60 -0
- package/src/bridge/commands/framework-registration.ts +66 -0
- package/src/bridge/commands/from-parser.ts +60 -0
- package/src/bridge/commands/result-dispatcher.ts +76 -0
- package/src/bridge/config-shared.ts +132 -0
- package/src/bridge/config.ts +176 -0
- package/src/bridge/gateway.ts +178 -0
- package/src/bridge/logger.ts +31 -0
- package/src/bridge/narrowing.ts +31 -0
- package/src/bridge/plugin-version.ts +102 -0
- package/src/bridge/runtime.ts +25 -0
- package/src/bridge/sdk-adapter.ts +164 -0
- package/src/bridge/setup/finalize.ts +144 -0
- package/src/bridge/setup/surface.ts +34 -0
- package/src/bridge/tools/channel.ts +58 -0
- package/src/bridge/tools/index.ts +15 -0
- package/src/bridge/tools/remind.ts +91 -0
- package/src/channel.setup.ts +33 -0
- package/src/channel.ts +399 -0
- package/src/config-schema.ts +84 -0
- package/src/engine/access/index.ts +2 -0
- package/src/engine/access/resolve-policy.ts +30 -0
- package/src/engine/access/sender-match.ts +55 -0
- package/src/engine/access/types.ts +2 -0
- package/src/engine/adapter/audio.port.ts +27 -0
- package/src/engine/adapter/commands.port.ts +22 -0
- package/src/engine/adapter/history.port.ts +52 -0
- package/src/engine/adapter/index.ts +76 -0
- package/src/engine/adapter/mention-gate.port.ts +50 -0
- package/src/engine/adapter/types.ts +38 -0
- package/src/engine/api/api-client.ts +212 -0
- package/src/engine/api/media-chunked.ts +644 -0
- package/src/engine/api/media.ts +218 -0
- package/src/engine/api/messages.ts +293 -0
- package/src/engine/api/retry.ts +217 -0
- package/src/engine/api/routes.ts +95 -0
- package/src/engine/api/token.ts +277 -0
- package/src/engine/approval/index.ts +224 -0
- package/src/engine/commands/builtin/log-helpers.ts +341 -0
- package/src/engine/commands/builtin/register-all.ts +17 -0
- package/src/engine/commands/builtin/register-approve.ts +201 -0
- package/src/engine/commands/builtin/register-basic.ts +95 -0
- package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
- package/src/engine/commands/builtin/register-logs.ts +20 -0
- package/src/engine/commands/builtin/register-streaming.ts +138 -0
- package/src/engine/commands/builtin/state.ts +31 -0
- package/src/engine/commands/slash-command-auth.ts +88 -0
- package/src/engine/commands/slash-command-handler.ts +168 -0
- package/src/engine/commands/slash-command-test-support.ts +39 -0
- package/src/engine/commands/slash-commands-impl.ts +61 -0
- package/src/engine/commands/slash-commands.ts +202 -0
- package/src/engine/config/credential-backup.ts +108 -0
- package/src/engine/config/credentials.ts +76 -0
- package/src/engine/config/group.ts +227 -0
- package/src/engine/config/resolve.ts +283 -0
- package/src/engine/config/setup-logic.ts +84 -0
- package/src/engine/gateway/active-cfg.ts +52 -0
- package/src/engine/gateway/codec.ts +47 -0
- package/src/engine/gateway/constants.ts +117 -0
- package/src/engine/gateway/event-dispatcher.ts +177 -0
- package/src/engine/gateway/gateway-connection.ts +356 -0
- package/src/engine/gateway/gateway.ts +267 -0
- package/src/engine/gateway/inbound-attachments.ts +360 -0
- package/src/engine/gateway/inbound-context.ts +82 -0
- package/src/engine/gateway/inbound-pipeline.ts +171 -0
- package/src/engine/gateway/interaction-handler.ts +345 -0
- package/src/engine/gateway/message-queue.ts +404 -0
- package/src/engine/gateway/outbound-dispatch.ts +590 -0
- package/src/engine/gateway/reconnect.ts +199 -0
- package/src/engine/gateway/stages/access-stage.ts +99 -0
- package/src/engine/gateway/stages/assembly-stage.ts +156 -0
- package/src/engine/gateway/stages/content-stage.ts +77 -0
- package/src/engine/gateway/stages/envelope-stage.ts +144 -0
- package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
- package/src/engine/gateway/stages/index.ts +18 -0
- package/src/engine/gateway/stages/quote-stage.ts +113 -0
- package/src/engine/gateway/stages/refidx-stage.ts +62 -0
- package/src/engine/gateway/stages/stub-contexts.ts +77 -0
- package/src/engine/gateway/types.ts +230 -0
- package/src/engine/gateway/typing-keepalive.ts +102 -0
- package/src/engine/gateway/ws-client.ts +16 -0
- package/src/engine/group/activation.ts +88 -0
- package/src/engine/group/history.ts +321 -0
- package/src/engine/group/mention.ts +114 -0
- package/src/engine/group/message-gating.ts +108 -0
- package/src/engine/messaging/decode-media-path.ts +82 -0
- package/src/engine/messaging/media-source.ts +210 -0
- package/src/engine/messaging/media-type-detect.ts +27 -0
- package/src/engine/messaging/outbound-audio-port.ts +38 -0
- package/src/engine/messaging/outbound-deliver.ts +810 -0
- package/src/engine/messaging/outbound-media-send.ts +658 -0
- package/src/engine/messaging/outbound-reply.ts +27 -0
- package/src/engine/messaging/outbound-result-helpers.ts +54 -0
- package/src/engine/messaging/outbound-types.ts +47 -0
- package/src/engine/messaging/outbound.ts +485 -0
- package/src/engine/messaging/reply-dispatcher.ts +597 -0
- package/src/engine/messaging/reply-limiter.ts +164 -0
- package/src/engine/messaging/sender.ts +741 -0
- package/src/engine/messaging/streaming-c2c.ts +1192 -0
- package/src/engine/messaging/streaming-media-send.ts +544 -0
- package/src/engine/messaging/target-parser.ts +104 -0
- package/src/engine/ref/format-message-ref.ts +142 -0
- package/src/engine/ref/format-ref-entry.ts +27 -0
- package/src/engine/ref/store.ts +211 -0
- package/src/engine/ref/types.ts +27 -0
- package/src/engine/session/known-users.ts +138 -0
- package/src/engine/session/session-store.ts +207 -0
- package/src/engine/tools/channel-api.ts +244 -0
- package/src/engine/tools/remind-logic.ts +377 -0
- package/src/engine/types.ts +313 -0
- package/src/engine/utils/attachment-tags.ts +174 -0
- package/src/engine/utils/audio.ts +525 -0
- package/src/engine/utils/data-paths.ts +38 -0
- package/src/engine/utils/diagnostics.ts +93 -0
- package/src/engine/utils/file-utils.ts +215 -0
- package/src/engine/utils/format.ts +70 -0
- package/src/engine/utils/image-size.ts +249 -0
- package/src/engine/utils/log.ts +77 -0
- package/src/engine/utils/media-tags.ts +177 -0
- package/src/engine/utils/payload.ts +157 -0
- package/src/engine/utils/platform.ts +265 -0
- package/src/engine/utils/request-context.ts +60 -0
- package/src/engine/utils/string-normalize.ts +91 -0
- package/src/engine/utils/stt.ts +103 -0
- package/src/engine/utils/text-parsing.ts +155 -0
- package/src/engine/utils/upload-cache.ts +96 -0
- package/src/engine/utils/voice-text.ts +15 -0
- package/src/exec-approvals.ts +237 -0
- package/src/qqbot-test-support.ts +29 -0
- package/src/secret-contract.ts +82 -0
- package/src/types.ts +210 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot reminder tool core logic.
|
|
3
|
+
* QQBot 提醒工具核心逻辑。
|
|
4
|
+
*
|
|
5
|
+
* Pure functions for time parsing, cron detection, job building,
|
|
6
|
+
* and remind execution. The framework registration shell
|
|
7
|
+
* (bridge/tools/remind.ts) delegates all business logic here and
|
|
8
|
+
* supplies request-level context fallbacks (`to`, `accountId`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reminder tool input parameters.
|
|
13
|
+
* 提醒工具的输入参数。
|
|
14
|
+
*/
|
|
15
|
+
export interface RemindParams {
|
|
16
|
+
action: "add" | "list" | "remove";
|
|
17
|
+
content?: string;
|
|
18
|
+
to?: string;
|
|
19
|
+
time?: string;
|
|
20
|
+
timezone?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
jobId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context supplied by the bridge layer so the engine can remain free of
|
|
27
|
+
* framework / AsyncLocalStorage dependencies. `fallbackTo` and
|
|
28
|
+
* `fallbackAccountId` are consulted only when the corresponding AI-supplied
|
|
29
|
+
* parameter is missing.
|
|
30
|
+
*/
|
|
31
|
+
interface RemindExecuteContext {
|
|
32
|
+
fallbackTo?: string;
|
|
33
|
+
fallbackAccountId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type RemindCronAction =
|
|
37
|
+
| { action: "list" }
|
|
38
|
+
| { action: "remove"; jobId: string }
|
|
39
|
+
| {
|
|
40
|
+
action: "add";
|
|
41
|
+
job: ReturnType<typeof buildOnceJob>["job"] | ReturnType<typeof buildCronJob>["job"];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type RemindCronScheduler = (params: RemindCronAction) => Promise<unknown>;
|
|
45
|
+
|
|
46
|
+
type RemindCronPlan =
|
|
47
|
+
| {
|
|
48
|
+
ok: true;
|
|
49
|
+
action: RemindParams["action"];
|
|
50
|
+
cronAction: RemindCronAction;
|
|
51
|
+
summary?: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
ok: false;
|
|
55
|
+
error: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const PREPARED_CRON_PARAMS_INSTRUCTION =
|
|
59
|
+
"Gateway cron action prepared for internal QQ reminder scheduling.";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* JSON Schema for AI tool parameters (used by framework registration).
|
|
63
|
+
* AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
|
|
64
|
+
*/
|
|
65
|
+
export const RemindSchema = {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
action: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description:
|
|
71
|
+
"Action type. add=create a reminder, list=show reminders, remove=delete a reminder.",
|
|
72
|
+
enum: ["add", "list", "remove"],
|
|
73
|
+
},
|
|
74
|
+
content: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description:
|
|
77
|
+
'Reminder content, for example "drink water" or "join the meeting". Required when action=add.',
|
|
78
|
+
},
|
|
79
|
+
to: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description:
|
|
82
|
+
"Optional delivery target. The runtime automatically resolves the current " +
|
|
83
|
+
"conversation target, so you usually do not need to supply this. " +
|
|
84
|
+
"Direct-message format: qqbot:c2c:user_openid. Group format: qqbot:group:group_openid.",
|
|
85
|
+
},
|
|
86
|
+
time: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description:
|
|
89
|
+
"Time description. Supported formats:\n" +
|
|
90
|
+
'1. Relative time, for example "5m", "1h", "1h30m", or "2d"\n' +
|
|
91
|
+
'2. Cron expression, for example "0 8 * * *" or "0 9 * * 1-5"\n' +
|
|
92
|
+
"Values containing spaces are treated as cron expressions; everything else is treated as a one-shot relative delay.\n" +
|
|
93
|
+
"Required when action=add.",
|
|
94
|
+
},
|
|
95
|
+
timezone: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: 'Timezone used for cron reminders. Defaults to "Asia/Shanghai".',
|
|
98
|
+
},
|
|
99
|
+
name: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Optional reminder job name. Defaults to the first 20 characters of content.",
|
|
102
|
+
},
|
|
103
|
+
jobId: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Job ID to remove. Required when action=remove; fetch it with list first.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["action"],
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse a relative time string into milliseconds.
|
|
113
|
+
* 解析相对时间字符串为毫秒数。
|
|
114
|
+
*
|
|
115
|
+
* Supports: "5m", "1h", "1h30m", "2d", "45s", plain number (as minutes).
|
|
116
|
+
*
|
|
117
|
+
* @returns Milliseconds or null if unparseable.
|
|
118
|
+
*/
|
|
119
|
+
export function parseRelativeTime(timeStr: string): number | null {
|
|
120
|
+
const s = timeStr.toLowerCase();
|
|
121
|
+
if (/^\d+$/.test(s)) {
|
|
122
|
+
return Number.parseInt(s, 10) * 60_000;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let totalMs = 0;
|
|
126
|
+
let matched = false;
|
|
127
|
+
const regex = /(\d+(?:\.\d+)?)\s*(d|h|m|s)/g;
|
|
128
|
+
let match: RegExpExecArray | null;
|
|
129
|
+
while ((match = regex.exec(s)) !== null) {
|
|
130
|
+
matched = true;
|
|
131
|
+
const value = Number.parseFloat(match[1]);
|
|
132
|
+
const unit = match[2];
|
|
133
|
+
switch (unit) {
|
|
134
|
+
case "d":
|
|
135
|
+
totalMs += value * 86_400_000;
|
|
136
|
+
break;
|
|
137
|
+
case "h":
|
|
138
|
+
totalMs += value * 3_600_000;
|
|
139
|
+
break;
|
|
140
|
+
case "m":
|
|
141
|
+
totalMs += value * 60_000;
|
|
142
|
+
break;
|
|
143
|
+
case "s":
|
|
144
|
+
totalMs += value * 1_000;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return matched ? Math.round(totalMs) : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check whether a time string is a cron expression (3–6 space-separated fields).
|
|
153
|
+
* 判断时间字符串是否为 cron 表达式。
|
|
154
|
+
*/
|
|
155
|
+
export function isCronExpression(timeStr: string): boolean {
|
|
156
|
+
const parts = timeStr.trim().split(/\s+/);
|
|
157
|
+
if (parts.length < 3 || parts.length > 6) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return parts.every((p) => /^[0-9*?/,LW#-]/.test(p));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate a cron job name from reminder content (first 20 chars).
|
|
165
|
+
* 根据提醒内容生成 cron job 名称。
|
|
166
|
+
*/
|
|
167
|
+
export function generateJobName(content: string): string {
|
|
168
|
+
const trimmed = content.trim();
|
|
169
|
+
const short = trimmed.length > 20 ? `${trimmed.slice(0, 20)}…` : trimmed;
|
|
170
|
+
return `Reminder: ${short}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Build the reminder system prompt sent to the AI. */
|
|
174
|
+
export function buildReminderPrompt(content: string): string {
|
|
175
|
+
return (
|
|
176
|
+
`You are a warm reminder assistant. Please remind the user about: ${content}. ` +
|
|
177
|
+
`Requirements: (1) do not reply with HEARTBEAT_OK (2) do not explain who you are ` +
|
|
178
|
+
`(3) output a direct and caring reminder message (4) you may add a short encouraging line ` +
|
|
179
|
+
`(5) keep it within 2-3 sentences (6) use a small amount of emoji.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Build cron job params for a one-shot delayed reminder. */
|
|
184
|
+
function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
|
|
185
|
+
const atMs = Date.now() + delayMs;
|
|
186
|
+
const content = params.content!;
|
|
187
|
+
const name = params.name || generateJobName(content);
|
|
188
|
+
return {
|
|
189
|
+
action: "add" as const,
|
|
190
|
+
job: {
|
|
191
|
+
name,
|
|
192
|
+
schedule: { kind: "at" as const, atMs },
|
|
193
|
+
sessionTarget: "isolated" as const,
|
|
194
|
+
wakeMode: "now" as const,
|
|
195
|
+
deleteAfterRun: true,
|
|
196
|
+
payload: {
|
|
197
|
+
kind: "agentTurn" as const,
|
|
198
|
+
message: buildReminderPrompt(content),
|
|
199
|
+
},
|
|
200
|
+
delivery: {
|
|
201
|
+
mode: "announce" as const,
|
|
202
|
+
channel: "qqbot" as const,
|
|
203
|
+
to,
|
|
204
|
+
accountId,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Build cron job params for a recurring cron reminder. */
|
|
211
|
+
function buildCronJob(params: RemindParams, to: string, accountId: string) {
|
|
212
|
+
const content = params.content!;
|
|
213
|
+
const name = params.name || generateJobName(content);
|
|
214
|
+
const tz = params.timezone || "Asia/Shanghai";
|
|
215
|
+
return {
|
|
216
|
+
action: "add" as const,
|
|
217
|
+
job: {
|
|
218
|
+
name,
|
|
219
|
+
schedule: { kind: "cron" as const, expr: params.time!.trim(), tz },
|
|
220
|
+
sessionTarget: "isolated" as const,
|
|
221
|
+
wakeMode: "now" as const,
|
|
222
|
+
payload: {
|
|
223
|
+
kind: "agentTurn" as const,
|
|
224
|
+
message: buildReminderPrompt(content),
|
|
225
|
+
},
|
|
226
|
+
delivery: {
|
|
227
|
+
mode: "announce" as const,
|
|
228
|
+
channel: "qqbot" as const,
|
|
229
|
+
to,
|
|
230
|
+
accountId,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Format a delay in milliseconds as a short string (e.g. "5m", "1h30m"). */
|
|
237
|
+
export function formatDelay(ms: number): string {
|
|
238
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
239
|
+
if (totalSeconds < 60) {
|
|
240
|
+
return `${totalSeconds}s`;
|
|
241
|
+
}
|
|
242
|
+
const totalMinutes = Math.round(ms / 60_000);
|
|
243
|
+
if (totalMinutes < 60) {
|
|
244
|
+
return `${totalMinutes}m`;
|
|
245
|
+
}
|
|
246
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
247
|
+
const minutes = totalMinutes % 60;
|
|
248
|
+
if (minutes === 0) {
|
|
249
|
+
return `${hours}h`;
|
|
250
|
+
}
|
|
251
|
+
return `${hours}h${minutes}m`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function json(data: unknown) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
257
|
+
details: data,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatSchedulerError(error: unknown): string {
|
|
262
|
+
return error instanceof Error ? error.message : String(error);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function prepareRemindCronAction(
|
|
266
|
+
params: RemindParams,
|
|
267
|
+
ctx: RemindExecuteContext = {},
|
|
268
|
+
): RemindCronPlan {
|
|
269
|
+
if (params.action === "list") {
|
|
270
|
+
return { ok: true, action: "list", cronAction: { action: "list" } };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (params.action === "remove") {
|
|
274
|
+
if (!params.jobId) {
|
|
275
|
+
return { ok: false, error: "jobId is required when action=remove. Use action=list first." };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
action: "remove",
|
|
280
|
+
cronAction: { action: "remove", jobId: params.jobId },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!params.content) {
|
|
285
|
+
return { ok: false, error: "content is required when action=add" };
|
|
286
|
+
}
|
|
287
|
+
const resolvedTo = params.to || ctx.fallbackTo;
|
|
288
|
+
if (!resolvedTo) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error:
|
|
292
|
+
"Unable to determine delivery target for action=add. " +
|
|
293
|
+
"The reminder can only be scheduled from within an active conversation.",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (!params.time) {
|
|
297
|
+
return { ok: false, error: "time is required when action=add" };
|
|
298
|
+
}
|
|
299
|
+
const resolvedAccountId = ctx.fallbackAccountId || "default";
|
|
300
|
+
|
|
301
|
+
if (isCronExpression(params.time)) {
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
action: "add",
|
|
305
|
+
cronAction: buildCronJob(params, resolvedTo, resolvedAccountId),
|
|
306
|
+
summary: `⏰ Recurring reminder: "${params.content}" (${params.time}, tz=${params.timezone || "Asia/Shanghai"})`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const delayMs = parseRelativeTime(params.time);
|
|
311
|
+
if (delayMs == null) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: `Could not parse time format: ${params.time}. Use values like 5m, 1h, 1h30m, or a cron expression.`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (delayMs < 30_000) {
|
|
318
|
+
return { ok: false, error: "Reminder delay must be at least 30 seconds" };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
action: "add",
|
|
324
|
+
cronAction: buildOnceJob(params, delayMs, resolvedTo, resolvedAccountId),
|
|
325
|
+
summary: `⏰ Reminder in ${formatDelay(delayMs)}: "${params.content}"`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Execute the reminder tool logic.
|
|
331
|
+
* 执行提醒工具逻辑。
|
|
332
|
+
*
|
|
333
|
+
* Validates params, parses time, and returns a structured result
|
|
334
|
+
* containing cron job params that the framework shell passes back
|
|
335
|
+
* as the tool output.
|
|
336
|
+
*
|
|
337
|
+
* When the AI omits `to` / `accountId`, the bridge layer can supply
|
|
338
|
+
* `ctx.fallbackTo` / `ctx.fallbackAccountId` (typically resolved from
|
|
339
|
+
* the request-scoped AsyncLocalStorage) to fill them in.
|
|
340
|
+
*/
|
|
341
|
+
export function executeRemind(params: RemindParams, ctx: RemindExecuteContext = {}) {
|
|
342
|
+
const plan = prepareRemindCronAction(params, ctx);
|
|
343
|
+
if (!plan.ok) {
|
|
344
|
+
return json({ error: plan.error });
|
|
345
|
+
}
|
|
346
|
+
return json({
|
|
347
|
+
_instruction: PREPARED_CRON_PARAMS_INSTRUCTION,
|
|
348
|
+
action: plan.action,
|
|
349
|
+
summary: plan.summary,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function executeScheduledRemind(
|
|
354
|
+
params: RemindParams,
|
|
355
|
+
ctx: RemindExecuteContext,
|
|
356
|
+
scheduler: RemindCronScheduler,
|
|
357
|
+
) {
|
|
358
|
+
const plan = prepareRemindCronAction(params, ctx);
|
|
359
|
+
if (!plan.ok) {
|
|
360
|
+
return json({ error: plan.error });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const cronResult = await scheduler(plan.cronAction);
|
|
365
|
+
return json({
|
|
366
|
+
ok: true,
|
|
367
|
+
action: plan.action,
|
|
368
|
+
summary: plan.summary,
|
|
369
|
+
cronResult,
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return json({
|
|
373
|
+
error: `Failed to run Gateway cron action: ${formatSchedulerError(error)}`,
|
|
374
|
+
action: plan.action,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core API layer public types.
|
|
3
|
+
*
|
|
4
|
+
* These types are independent of the root `src/types.ts` and only define
|
|
5
|
+
* what the `core/api/` modules need. The old `src/types.ts` remains
|
|
6
|
+
* untouched for backward compatibility.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============ Structured API Error ============
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structured API error with HTTP status, path, and optional business error code.
|
|
13
|
+
*
|
|
14
|
+
* Compared to the old `api.ts` which throws plain `Error`, this carries
|
|
15
|
+
* machine-readable fields for downstream retry/fallback decisions.
|
|
16
|
+
*/
|
|
17
|
+
export class ApiError extends Error {
|
|
18
|
+
override readonly name = "ApiError";
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
message: string,
|
|
22
|
+
/** HTTP status code returned by the QQ Open Platform. */
|
|
23
|
+
public readonly httpStatus: number,
|
|
24
|
+
/** API path that produced the error (e.g. `/v2/users/{id}/messages`). */
|
|
25
|
+
public readonly path: string,
|
|
26
|
+
/** Business error code from the response body (`code` or `err_code`). */
|
|
27
|
+
public readonly bizCode?: number,
|
|
28
|
+
/** Original error message from the response body. */
|
|
29
|
+
public readonly bizMessage?: string,
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============ Logger ============
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Unified logger interface used across all engine/ modules.
|
|
39
|
+
*
|
|
40
|
+
* Replaces the previously fragmented ApiLogger, GatewayLogger, ReconnectLogger,
|
|
41
|
+
* MessageRefLogger, PathLogger, and SenderLogger interfaces.
|
|
42
|
+
*
|
|
43
|
+
* `info` and `error` are required; `warn` and `debug` are optional because
|
|
44
|
+
* some callers (e.g. the framework-injected `ctx.log`) may not provide them.
|
|
45
|
+
*/
|
|
46
|
+
export interface EngineLogger {
|
|
47
|
+
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
48
|
+
error: (msg: string, meta?: Record<string, unknown>) => void;
|
|
49
|
+
warn?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
50
|
+
debug?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============ Chat Scope ============
|
|
54
|
+
|
|
55
|
+
/** Chat scope used to unify C2C/Group path construction. */
|
|
56
|
+
export type ChatScope = "c2c" | "group";
|
|
57
|
+
|
|
58
|
+
// ============ Message Response ============
|
|
59
|
+
|
|
60
|
+
/** Standard message send response from the QQ Open Platform. */
|
|
61
|
+
export interface MessageResponse {
|
|
62
|
+
id: string;
|
|
63
|
+
timestamp: number | string;
|
|
64
|
+
/** Reference index for future quoting. */
|
|
65
|
+
ext_info?: {
|
|
66
|
+
ref_idx?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============ Media Types ============
|
|
71
|
+
|
|
72
|
+
/** QQ Open Platform media file type codes. */
|
|
73
|
+
export enum MediaFileType {
|
|
74
|
+
IMAGE = 1,
|
|
75
|
+
VIDEO = 2,
|
|
76
|
+
VOICE = 3,
|
|
77
|
+
FILE = 4,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Media upload response from the QQ Open Platform. */
|
|
81
|
+
export interface UploadMediaResponse {
|
|
82
|
+
file_uuid: string;
|
|
83
|
+
file_info: string;
|
|
84
|
+
ttl: number;
|
|
85
|
+
id?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Structured metadata recorded for outbound messages. */
|
|
89
|
+
export interface OutboundMeta {
|
|
90
|
+
/** Message text content. */
|
|
91
|
+
text?: string;
|
|
92
|
+
/** Media type tag. */
|
|
93
|
+
mediaType?: "image" | "voice" | "video" | "file";
|
|
94
|
+
/** Remote URL of the media source. */
|
|
95
|
+
mediaUrl?: string;
|
|
96
|
+
/** Local file path of the media source. */
|
|
97
|
+
mediaLocalPath?: string;
|
|
98
|
+
/** Original TTS text (voice messages only). */
|
|
99
|
+
ttsText?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============ API Client Config ============
|
|
103
|
+
|
|
104
|
+
/** Configuration for the core HTTP client. */
|
|
105
|
+
export interface ApiClientConfig {
|
|
106
|
+
/** Base URL for the QQ Open Platform REST API. */
|
|
107
|
+
baseUrl?: string;
|
|
108
|
+
/** Default request timeout in milliseconds. */
|
|
109
|
+
defaultTimeoutMs?: number;
|
|
110
|
+
/** File upload request timeout in milliseconds. */
|
|
111
|
+
fileUploadTimeoutMs?: number;
|
|
112
|
+
/** Logger instance. */
|
|
113
|
+
logger?: EngineLogger;
|
|
114
|
+
/** User-Agent header value, or a getter function for dynamic resolution. */
|
|
115
|
+
userAgent?: string | (() => string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============ Chunked Upload Types ============
|
|
119
|
+
|
|
120
|
+
/** Individual upload part metadata. */
|
|
121
|
+
export interface UploadPart {
|
|
122
|
+
/** Part index (1-based). */
|
|
123
|
+
index: number;
|
|
124
|
+
/** Pre-signed upload URL. */
|
|
125
|
+
presigned_url: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Response from the upload_prepare endpoint. */
|
|
129
|
+
export interface UploadPrepareResponse {
|
|
130
|
+
/** Upload task identifier. */
|
|
131
|
+
upload_id: string;
|
|
132
|
+
/** Block size in bytes. */
|
|
133
|
+
block_size: number;
|
|
134
|
+
/** Pre-signed upload parts. */
|
|
135
|
+
parts: UploadPart[];
|
|
136
|
+
/** Server-suggested upload concurrency. */
|
|
137
|
+
concurrency?: number;
|
|
138
|
+
/** Server-suggested retry timeout for upload_part_finish (seconds). */
|
|
139
|
+
retry_timeout?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** File hash information for upload_prepare. */
|
|
143
|
+
export interface UploadPrepareHashes {
|
|
144
|
+
/** Whole-file MD5 (hex). */
|
|
145
|
+
md5: string;
|
|
146
|
+
/** Whole-file SHA1 (hex). */
|
|
147
|
+
sha1: string;
|
|
148
|
+
/** MD5 of the first 10,002,432 bytes (hex). */
|
|
149
|
+
md5_10m: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============ Stream Message Types ============
|
|
153
|
+
|
|
154
|
+
/** Stream message input mode (C2C stream_messages API). */
|
|
155
|
+
export const StreamInputMode = {
|
|
156
|
+
/** Each chunk replaces full message content. */
|
|
157
|
+
REPLACE: "replace",
|
|
158
|
+
} as const;
|
|
159
|
+
export type StreamInputMode = (typeof StreamInputMode)[keyof typeof StreamInputMode];
|
|
160
|
+
|
|
161
|
+
/** Stream message input state (numeric per QQ Open Platform). */
|
|
162
|
+
export const StreamInputState = {
|
|
163
|
+
GENERATING: 1,
|
|
164
|
+
DONE: 10,
|
|
165
|
+
} as const;
|
|
166
|
+
export type StreamInputState = (typeof StreamInputState)[keyof typeof StreamInputState];
|
|
167
|
+
|
|
168
|
+
/** Stream message content type. */
|
|
169
|
+
export const StreamContentType = {
|
|
170
|
+
MARKDOWN: "markdown",
|
|
171
|
+
} as const;
|
|
172
|
+
export type StreamContentType = (typeof StreamContentType)[keyof typeof StreamContentType];
|
|
173
|
+
|
|
174
|
+
/** Stream message request body for `/v2/users/{openid}/stream_messages`. */
|
|
175
|
+
export interface StreamMessageRequest {
|
|
176
|
+
input_mode: StreamInputMode;
|
|
177
|
+
input_state: StreamInputState;
|
|
178
|
+
content_type: StreamContentType;
|
|
179
|
+
content_raw: string;
|
|
180
|
+
event_id: string;
|
|
181
|
+
msg_id: string;
|
|
182
|
+
stream_msg_id?: string;
|
|
183
|
+
msg_seq: number;
|
|
184
|
+
index: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============ Inline Keyboard Types ============
|
|
188
|
+
|
|
189
|
+
/** Inline keyboard button for approval/interaction flows. */
|
|
190
|
+
export interface KeyboardButton {
|
|
191
|
+
id: string;
|
|
192
|
+
render_data: {
|
|
193
|
+
label: string;
|
|
194
|
+
visited_label: string;
|
|
195
|
+
style: number;
|
|
196
|
+
};
|
|
197
|
+
action: {
|
|
198
|
+
type: number;
|
|
199
|
+
permission: { type: number };
|
|
200
|
+
data: string;
|
|
201
|
+
click_limit?: number;
|
|
202
|
+
};
|
|
203
|
+
group_id?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Inline keyboard structure attached to messages.
|
|
208
|
+
* Sent as the `keyboard` field in the message body:
|
|
209
|
+
* `{ "keyboard": { "content": { "rows": [...] } } }`
|
|
210
|
+
*/
|
|
211
|
+
export interface InlineKeyboard {
|
|
212
|
+
content: {
|
|
213
|
+
rows: Array<{ buttons: KeyboardButton[] }>;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============ Interaction Event Types ============
|
|
218
|
+
|
|
219
|
+
/** Button interaction event (INTERACTION_CREATE). */
|
|
220
|
+
export interface InteractionEvent {
|
|
221
|
+
/** Event ID — used to acknowledge the interaction (PUT /interactions/{id}). */
|
|
222
|
+
id: string;
|
|
223
|
+
/** Event sub-type: 11=message button, 12=c2c quick menu. */
|
|
224
|
+
type: number;
|
|
225
|
+
/** Scene identifier: c2c / group / guild. */
|
|
226
|
+
scene?: string;
|
|
227
|
+
/** Chat type: 0=guild, 1=group, 2=c2c. */
|
|
228
|
+
chat_type?: number;
|
|
229
|
+
timestamp?: string;
|
|
230
|
+
guild_id?: string;
|
|
231
|
+
channel_id?: string;
|
|
232
|
+
/** C2C user openid (c2c scene only). */
|
|
233
|
+
user_openid?: string;
|
|
234
|
+
/** Group openid (group scene only). */
|
|
235
|
+
group_openid?: string;
|
|
236
|
+
/** Group member openid (group scene only). */
|
|
237
|
+
group_member_openid?: string;
|
|
238
|
+
version: number;
|
|
239
|
+
data: {
|
|
240
|
+
type: number;
|
|
241
|
+
resolved: {
|
|
242
|
+
button_data?: string;
|
|
243
|
+
button_id?: string;
|
|
244
|
+
user_id?: string;
|
|
245
|
+
feature_id?: string;
|
|
246
|
+
message_id?: string;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============ Account Config View ============
|
|
252
|
+
|
|
253
|
+
import type { QQBotDmPolicy, QQBotGroupPolicy } from "./access/types.js";
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Typed view of known per-account configuration fields.
|
|
257
|
+
*
|
|
258
|
+
* Used for `as QQBotAccountConfigView` casts when reading fields from
|
|
259
|
+
* the raw `Record<string, unknown>` config. The actual config type
|
|
260
|
+
* stays `Record<string, unknown>` to avoid schema incompatibility.
|
|
261
|
+
*/
|
|
262
|
+
export interface QQBotAccountConfigView {
|
|
263
|
+
allowFrom?: Array<string | number>;
|
|
264
|
+
groupAllowFrom?: Array<string | number>;
|
|
265
|
+
dmPolicy?: QQBotDmPolicy;
|
|
266
|
+
groupPolicy?: QQBotGroupPolicy;
|
|
267
|
+
groups?: Record<string, Record<string, unknown>>;
|
|
268
|
+
streaming?:
|
|
269
|
+
| boolean
|
|
270
|
+
| {
|
|
271
|
+
mode?: string;
|
|
272
|
+
c2cStreamApi?: boolean;
|
|
273
|
+
};
|
|
274
|
+
audioFormatPolicy?: {
|
|
275
|
+
uploadDirectFormats?: string[];
|
|
276
|
+
transcodeEnabled?: boolean;
|
|
277
|
+
};
|
|
278
|
+
voiceDirectUploadFormats?: string[];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============ Gateway Account ============
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolved account configuration — shared across gateway/ and messaging/ layers.
|
|
285
|
+
*
|
|
286
|
+
* Lifted here from gateway/types.ts to eliminate the circular type dependency
|
|
287
|
+
* where messaging/ had to import from gateway/.
|
|
288
|
+
*/
|
|
289
|
+
export interface GatewayAccount {
|
|
290
|
+
accountId: string;
|
|
291
|
+
appId: string;
|
|
292
|
+
clientSecret: string;
|
|
293
|
+
markdownSupport: boolean;
|
|
294
|
+
systemPrompt?: string;
|
|
295
|
+
config: Record<string, unknown> & {
|
|
296
|
+
allowFrom?: Array<string | number>;
|
|
297
|
+
groupAllowFrom?: Array<string | number>;
|
|
298
|
+
dmPolicy?: "open" | "allowlist" | "disabled";
|
|
299
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
300
|
+
streaming?:
|
|
301
|
+
| boolean
|
|
302
|
+
| {
|
|
303
|
+
mode?: string;
|
|
304
|
+
/** When true, use QQ C2C `stream_messages` for DMs. Boolean `true` is equivalent. */
|
|
305
|
+
c2cStreamApi?: boolean;
|
|
306
|
+
};
|
|
307
|
+
audioFormatPolicy?: {
|
|
308
|
+
uploadDirectFormats?: string[];
|
|
309
|
+
transcodeEnabled?: boolean;
|
|
310
|
+
};
|
|
311
|
+
voiceDirectUploadFormats?: string[];
|
|
312
|
+
};
|
|
313
|
+
}
|