@botcord/botcord 0.3.6 → 0.3.8-beta.20260415044559

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.
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_subscription — Create and manage coin-priced subscription products.
3
3
  */
4
- import {
5
- getSingleAccountModeError,
6
- resolveAccountConfig,
7
- isAccountConfigured,
8
- } from "../config.js";
9
- import { BotCordClient } from "../client.js";
10
- import { attachTokenPersistence } from "../credentials.js";
11
- import { getConfig as getAppConfig } from "../runtime.js";
4
+ import { withClient } from "./with-client.js";
5
+ import { validationError, dryRunResult } from "./tool-result.js";
12
6
  import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
13
7
 
14
8
  function formatProduct(product: any): string {
@@ -127,31 +121,23 @@ export function createSubscriptionTool() {
127
121
  type: "number" as const,
128
122
  description: "Slow mode interval in seconds — for create_subscription_room or bind_room_to_product",
129
123
  },
124
+ dry_run: {
125
+ type: "boolean" as const,
126
+ description: "Preview the request without executing. Returns the API call that would be made.",
127
+ },
130
128
  },
131
129
  required: ["action"],
132
130
  },
133
131
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
134
- const cfg = getAppConfig();
135
- if (!cfg) return { error: "No configuration available" };
136
- const singleAccountError = getSingleAccountModeError(cfg);
137
- if (singleAccountError) return { error: singleAccountError };
138
-
139
- const acct = resolveAccountConfig(cfg);
140
- if (!isAccountConfigured(acct)) {
141
- return { error: "BotCord is not configured." };
142
- }
143
-
144
- const client = new BotCordClient(acct);
145
- attachTokenPersistence(client, acct);
146
-
147
- try {
132
+ return withClient(async (client) => {
148
133
  switch (args.action) {
149
134
  case "create_product": {
150
- if (!args.name) return { error: "name is required" };
151
- if (!args.amount) return { error: "amount is required" };
152
- if (!args.billing_interval) return { error: "billing_interval is required" };
135
+ if (!args.name) return validationError("name is required");
136
+ if (!args.amount) return validationError("amount is required");
137
+ if (!args.billing_interval) return validationError("billing_interval is required");
153
138
  const amountMinor = parseCoinToMinor(args.amount);
154
- if (amountMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
139
+ if (amountMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
140
+ if (args.dry_run) return dryRunResult("POST", "/subscriptions/products", { name: args.name, amount_minor: amountMinor, billing_interval: args.billing_interval });
155
141
  const product = await client.createSubscriptionProduct({
156
142
  name: args.name,
157
143
  description: args.description,
@@ -173,14 +159,16 @@ export function createSubscriptionTool() {
173
159
  }
174
160
 
175
161
  case "archive_product": {
176
- if (!args.product_id) return { error: "product_id is required" };
162
+ if (!args.product_id) return validationError("product_id is required");
163
+ if (args.dry_run) return dryRunResult("POST", `/subscriptions/products/${args.product_id}/archive`);
177
164
  const product = await client.archiveSubscriptionProduct(args.product_id);
178
165
  return { result: formatProduct(product), data: product };
179
166
  }
180
167
 
181
168
  case "create_subscription_room": {
182
- if (!args.product_id) return { error: "product_id is required" };
183
- if (!args.name) return { error: "name is required" };
169
+ if (!args.product_id) return validationError("product_id is required");
170
+ if (!args.name) return validationError("name is required");
171
+ if (args.dry_run) return dryRunResult("POST", "/hub/rooms", { name: args.name, description: args.description, visibility: "public", join_policy: "open", required_subscription_product_id: args.product_id });
184
172
  const room = await client.createRoom({
185
173
  name: args.name,
186
174
  description: args.description,
@@ -200,8 +188,9 @@ export function createSubscriptionTool() {
200
188
  }
201
189
 
202
190
  case "bind_room_to_product": {
203
- if (!args.room_id) return { error: "room_id is required" };
204
- if (!args.product_id) return { error: "product_id is required" };
191
+ if (!args.room_id) return validationError("room_id is required");
192
+ if (!args.product_id) return validationError("product_id is required");
193
+ if (args.dry_run) return dryRunResult("PATCH", `/hub/rooms/${args.room_id}`, { visibility: "public", join_policy: "open", required_subscription_product_id: args.product_id });
205
194
  const room = await client.updateRoom(args.room_id, {
206
195
  name: args.name,
207
196
  description: args.description,
@@ -221,7 +210,8 @@ export function createSubscriptionTool() {
221
210
  }
222
211
 
223
212
  case "subscribe": {
224
- if (!args.product_id) return { error: "product_id is required" };
213
+ if (!args.product_id) return validationError("product_id is required");
214
+ if (args.dry_run) return dryRunResult("POST", `/subscriptions/products/${args.product_id}/subscribe`);
225
215
  const subscription = await client.subscribeToProduct(args.product_id, args.idempotency_key);
226
216
  return { result: formatSubscription(subscription), data: subscription };
227
217
  }
@@ -232,23 +222,22 @@ export function createSubscriptionTool() {
232
222
  }
233
223
 
234
224
  case "list_subscribers": {
235
- if (!args.product_id) return { error: "product_id is required" };
225
+ if (!args.product_id) return validationError("product_id is required");
236
226
  const subscriptions = await client.listProductSubscribers(args.product_id);
237
227
  return { result: formatSubscriptionList(subscriptions), data: subscriptions };
238
228
  }
239
229
 
240
230
  case "cancel": {
241
- if (!args.subscription_id) return { error: "subscription_id is required" };
231
+ if (!args.subscription_id) return validationError("subscription_id is required");
232
+ if (args.dry_run) return dryRunResult("POST", `/subscriptions/${args.subscription_id}/cancel`);
242
233
  const subscription = await client.cancelSubscription(args.subscription_id);
243
234
  return { result: formatSubscription(subscription), data: subscription };
244
235
  }
245
236
 
246
237
  default:
247
- return { error: `Unknown action: ${args.action}` };
238
+ return validationError(`Unknown action: ${args.action}`);
248
239
  }
249
- } catch (err: any) {
250
- return { error: `Subscription action failed: ${err.message}` };
251
- }
240
+ });
252
241
  },
253
242
  };
254
243
  }
@@ -28,7 +28,8 @@ export interface DryRunRequest {
28
28
  method: string;
29
29
  path: string;
30
30
  body?: unknown;
31
- query?: Record<string, string>;
31
+ query?: Record<string, string | string[]>;
32
+ note?: string;
32
33
  }
33
34
 
34
35
  export type DryRunResult = { ok: true; dry_run: true; request: DryRunRequest };
@@ -60,11 +61,17 @@ export function apiError(code: string, message: string, hint?: string): ToolFail
60
61
  return fail("api", code, message, hint);
61
62
  }
62
63
 
63
- export function dryRunResult(method: string, path: string, body?: unknown, query?: Record<string, string>): DryRunResult {
64
+ export function dryRunResult(method: string, path: string, body?: unknown, options?: { query?: Record<string, string | string[]>; note?: string }): DryRunResult {
64
65
  return {
65
66
  ok: true,
66
67
  dry_run: true,
67
- request: { method, path, ...(body !== undefined ? { body } : {}), ...(query ? { query } : {}) },
68
+ request: {
69
+ method,
70
+ path,
71
+ ...(body !== undefined ? { body } : {}),
72
+ ...(options?.query ? { query: options.query } : {}),
73
+ ...(options?.note ? { note: options.note } : {}),
74
+ },
68
75
  };
69
76
  }
70
77
 
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_topics — Topic lifecycle management within rooms.
3
3
  */
4
- import {
5
- getSingleAccountModeError,
6
- resolveAccountConfig,
7
- isAccountConfigured,
8
- } from "../config.js";
9
- import { BotCordClient } from "../client.js";
10
- import { attachTokenPersistence } from "../credentials.js";
11
- import { getConfig as getAppConfig } from "../runtime.js";
4
+ import { withClient } from "./with-client.js";
5
+ import { validationError, dryRunResult } from "./tool-result.js";
12
6
 
13
7
  export function createTopicsTool() {
14
8
  return {
@@ -50,27 +44,19 @@ export function createTopicsTool() {
50
44
  enum: ["open", "completed", "failed", "expired"],
51
45
  description: "Topic status — for list (filter) or update (transition)",
52
46
  },
47
+ dry_run: {
48
+ type: "boolean" as const,
49
+ description: "Preview the request without executing. Returns the API call that would be made.",
50
+ },
53
51
  },
54
52
  required: ["action", "room_id"],
55
53
  },
56
54
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
57
- const cfg = getAppConfig();
58
- if (!cfg) return { error: "No configuration available" };
59
- const singleAccountError = getSingleAccountModeError(cfg);
60
- if (singleAccountError) return { error: singleAccountError };
61
-
62
- const acct = resolveAccountConfig(cfg);
63
- if (!isAccountConfigured(acct)) {
64
- return { error: "BotCord is not configured." };
65
- }
66
-
67
- const client = new BotCordClient(acct);
68
- attachTokenPersistence(client, acct);
69
-
70
- try {
55
+ return withClient(async (client) => {
71
56
  switch (args.action) {
72
57
  case "create":
73
- if (!args.title) return { error: "title is required" };
58
+ if (!args.title) return validationError("title is required");
59
+ if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/topics`, { title: args.title, description: args.description, goal: args.goal });
74
60
  return await client.createTopic(args.room_id, {
75
61
  title: args.title,
76
62
  description: args.description,
@@ -78,14 +64,15 @@ export function createTopicsTool() {
78
64
  });
79
65
 
80
66
  case "list":
81
- return await client.listTopics(args.room_id, args.status);
67
+ return { topics: await client.listTopics(args.room_id, args.status) };
82
68
 
83
69
  case "get":
84
- if (!args.topic_id) return { error: "topic_id is required" };
70
+ if (!args.topic_id) return validationError("topic_id is required");
85
71
  return await client.getTopic(args.room_id, args.topic_id);
86
72
 
87
73
  case "update":
88
- if (!args.topic_id) return { error: "topic_id is required" };
74
+ if (!args.topic_id) return validationError("topic_id is required");
75
+ if (args.dry_run) return dryRunResult("PATCH", `/hub/rooms/${args.room_id}/topics/${args.topic_id}`, { title: args.title, status: args.status, goal: args.goal });
89
76
  return await client.updateTopic(args.room_id, args.topic_id, {
90
77
  title: args.title,
91
78
  description: args.description,
@@ -94,16 +81,15 @@ export function createTopicsTool() {
94
81
  });
95
82
 
96
83
  case "delete":
97
- if (!args.topic_id) return { error: "topic_id is required" };
84
+ if (!args.topic_id) return validationError("topic_id is required");
85
+ if (args.dry_run) return dryRunResult("DELETE", `/hub/rooms/${args.room_id}/topics/${args.topic_id}`);
98
86
  await client.deleteTopic(args.room_id, args.topic_id);
99
87
  return { ok: true, deleted: args.topic_id, room: args.room_id };
100
88
 
101
89
  default:
102
- return { error: `Unknown action: ${args.action}` };
90
+ return validationError(`Unknown action: ${args.action}`);
103
91
  }
104
- } catch (err: any) {
105
- return { error: `Topic action failed: ${err.message}` };
106
- }
92
+ });
107
93
  },
108
94
  };
109
95
  }
package/src/types.ts CHANGED
@@ -1,38 +1,7 @@
1
- // BotCord protocol types (mirrors hub/schemas.py)
2
-
3
- export type BotCordSignature = {
4
- alg: "ed25519";
5
- key_id: string;
6
- value: string; // base64
7
- };
8
-
9
- export type MessageType =
10
- | "message"
11
- | "ack"
12
- | "result"
13
- | "error"
14
- | "contact_request"
15
- | "contact_request_response"
16
- | "contact_removed"
17
- | "system";
18
-
19
- export type BotCordMessageEnvelope = {
20
- v: string;
21
- msg_id: string;
22
- ts: number;
23
- from: string;
24
- to: string;
25
- type: MessageType;
26
- reply_to: string | null;
27
- ttl_sec: number;
28
- topic?: string | null;
29
- goal?: string | null;
30
- payload: Record<string, unknown>;
31
- payload_hash: string;
32
- sig: BotCordSignature;
33
- mentions?: string[] | null;
34
- };
1
+ // Re-export all protocol types from shared package
2
+ export * from "@botcord/protocol-core";
35
3
 
4
+ // Plugin-specific types (not shared)
36
5
  // Account config in openclaw.json channels.botcord
37
6
  export type BotCordAccountConfig = {
38
7
  enabled?: boolean;
@@ -52,252 +21,3 @@ export type BotCordAccountConfig = {
52
21
  };
53
22
 
54
23
  export type BotCordChannelConfig = BotCordAccountConfig;
55
-
56
- // Inbox poll response
57
- export type SourceType = "agent" | "dashboard_user_chat";
58
-
59
- export type InboxMessage = {
60
- hub_msg_id: string;
61
- envelope: BotCordMessageEnvelope;
62
- text?: string;
63
- room_id?: string;
64
- room_name?: string;
65
- room_rule?: string | null;
66
- room_member_count?: number;
67
- room_member_names?: string[];
68
- my_role?: string;
69
- my_can_send?: boolean;
70
- topic?: string;
71
- topic_id?: string;
72
- goal?: string;
73
- mentioned?: boolean;
74
- source_type?: SourceType;
75
- source_user_id?: string | null;
76
- source_user_name?: string | null;
77
- source_session_kind?: string | null;
78
- };
79
-
80
- export type InboxPollResponse = {
81
- messages: InboxMessage[];
82
- count: number;
83
- has_more: boolean;
84
- };
85
-
86
- // Hub API response types
87
- export type SendResponse = {
88
- queued: boolean;
89
- hub_msg_id: string;
90
- status: string;
91
- topic_id?: string;
92
- };
93
-
94
- export type RoomInfo = {
95
- room_id: string;
96
- name: string;
97
- description?: string;
98
- rule?: string | null;
99
- visibility: "private" | "public";
100
- join_policy: "invite_only" | "open";
101
- required_subscription_product_id?: string | null;
102
- max_members?: number | null;
103
- default_send: boolean;
104
- default_invite?: boolean;
105
- slow_mode_seconds?: number | null;
106
- member_count: number;
107
- created_at: string;
108
- };
109
-
110
- export type PublicSubscriptionProduct = {
111
- product_id: string;
112
- name: string;
113
- description: string;
114
- amount_minor: string;
115
- billing_interval: string;
116
- };
117
-
118
- export type PublicRoom = {
119
- room_id: string;
120
- name: string;
121
- description: string;
122
- owner_id: string;
123
- visibility: string;
124
- member_count: number;
125
- required_subscription_product_id?: string | null;
126
- subscription_product?: PublicSubscriptionProduct | null;
127
- last_message_preview?: string | null;
128
- last_message_at?: string | null;
129
- last_sender_name?: string | null;
130
- };
131
-
132
- export type PublicRoomsResponse = {
133
- rooms: PublicRoom[];
134
- total: number;
135
- };
136
-
137
- export type AgentInfo = {
138
- agent_id: string;
139
- display_name?: string;
140
- bio?: string;
141
- message_policy: string;
142
- endpoints: Array<{ url: string; state: string }>;
143
- };
144
-
145
- export type ContactInfo = {
146
- contact_agent_id: string;
147
- display_name?: string;
148
- created_at: string;
149
- };
150
-
151
- export type ContactRequestInfo = {
152
- request_id: string;
153
- from_agent_id: string;
154
- to_agent_id: string;
155
- state: "pending" | "accepted" | "rejected";
156
- created_at: string;
157
- };
158
-
159
- // File upload response (mirrors hub/schemas.py FileUploadResponse)
160
- export type FileUploadResponse = {
161
- file_id: string;
162
- url: string;
163
- original_filename: string;
164
- content_type: string;
165
- size_bytes: number;
166
- expires_at: string; // ISO 8601
167
- };
168
-
169
- // Attachment metadata included in message payloads
170
- export type MessageAttachment = {
171
- filename: string;
172
- url: string;
173
- content_type?: string;
174
- size_bytes?: number;
175
- };
176
-
177
- // Wallet types (mirrors hub wallet schemas)
178
-
179
- export type WalletSummary = {
180
- agent_id: string;
181
- asset_code: string;
182
- available_balance_minor: string;
183
- locked_balance_minor: string;
184
- total_balance_minor: string;
185
- updated_at: string;
186
- };
187
-
188
- export type WalletTransaction = {
189
- tx_id: string;
190
- type: "topup" | "withdrawal" | "transfer";
191
- status: "pending" | "processing" | "completed" | "failed" | "cancelled";
192
- asset_code: string;
193
- amount_minor: string;
194
- fee_minor: string;
195
- from_agent_id: string | null;
196
- to_agent_id: string | null;
197
- reference_type: string | null;
198
- reference_id: string | null;
199
- idempotency_key: string | null;
200
- metadata_json: string | null;
201
- created_at: string;
202
- updated_at: string;
203
- completed_at: string | null;
204
- };
205
-
206
- export type WalletLedgerEntry = {
207
- entry_id: string;
208
- tx_id: string;
209
- agent_id: string;
210
- asset_code: string;
211
- direction: "debit" | "credit";
212
- amount_minor: string;
213
- balance_after_minor: string;
214
- created_at: string;
215
- };
216
-
217
- export type WalletLedgerResponse = {
218
- entries: WalletLedgerEntry[];
219
- next_cursor: string | null;
220
- has_more: boolean;
221
- };
222
-
223
- export type TopupResponse = {
224
- topup_id: string;
225
- tx_id: string | null;
226
- agent_id: string;
227
- asset_code: string;
228
- amount_minor: string;
229
- status: string;
230
- channel: string;
231
- created_at: string;
232
- completed_at: string | null;
233
- };
234
-
235
- export type WithdrawalResponse = {
236
- withdrawal_id: string;
237
- tx_id: string | null;
238
- agent_id: string;
239
- asset_code: string;
240
- amount_minor: string;
241
- fee_minor: string;
242
- status: string;
243
- destination_type: string | null;
244
- review_note: string | null;
245
- created_at: string;
246
- reviewed_at: string | null;
247
- completed_at: string | null;
248
- };
249
-
250
- export type BillingInterval = "week" | "month" | "once";
251
-
252
- export type SubscriptionProductStatus = "active" | "archived";
253
-
254
- export type SubscriptionStatus = "active" | "past_due" | "cancelled";
255
-
256
- export type SubscriptionChargeAttemptStatus = "pending" | "succeeded" | "failed";
257
-
258
- export type SubscriptionProduct = {
259
- product_id: string;
260
- owner_agent_id: string;
261
- name: string;
262
- description: string;
263
- asset_code: string;
264
- amount_minor: string;
265
- billing_interval: BillingInterval;
266
- status: SubscriptionProductStatus;
267
- created_at: string;
268
- updated_at: string;
269
- archived_at: string | null;
270
- };
271
-
272
- export type Subscription = {
273
- subscription_id: string;
274
- product_id: string;
275
- subscriber_agent_id: string;
276
- provider_agent_id: string;
277
- asset_code: string;
278
- amount_minor: string;
279
- billing_interval: BillingInterval;
280
- status: SubscriptionStatus;
281
- current_period_start: string;
282
- current_period_end: string;
283
- next_charge_at: string;
284
- cancel_at_period_end: boolean;
285
- cancelled_at: string | null;
286
- last_charged_at: string | null;
287
- last_charge_tx_id: string | null;
288
- consecutive_failed_attempts: number;
289
- created_at: string;
290
- updated_at: string;
291
- };
292
-
293
- export type SubscriptionChargeAttempt = {
294
- attempt_id: string;
295
- subscription_id: string;
296
- billing_cycle_key: string;
297
- status: SubscriptionChargeAttemptStatus;
298
- scheduled_at: string;
299
- attempted_at: string | null;
300
- tx_id: string | null;
301
- failure_reason: string | null;
302
- created_at: string;
303
- };
@@ -1,139 +0,0 @@
1
- /**
2
- * Onboarding hook — injects step-by-step onboarding guidance for first-time users.
3
- *
4
- * Checks the credential file for `onboardedAt`. If absent, injects a
5
- * prependContext that walks the user through features, use cases, goal
6
- * setting, cron setup, checklist, and healthcheck — one step at a time.
7
- */
8
- import { isOnboarded } from "./credentials.js";
9
- import { resolveAccountConfig, isAccountConfigured } from "./config.js";
10
- import { getConfig } from "./runtime.js";
11
-
12
- function buildOnboardingPrompt(baseUrl: string): string {
13
- return `[BotCord Onboarding — Step-by-Step]
14
-
15
- This is a newly installed BotCord plugin. Guide the user through onboarding ONE STEP AT A TIME.
16
- 这是新安装的 BotCord 插件。引导用户完成新手流程,每次只做一步。
17
-
18
- CRITICAL RULES:
19
- - Complete each step before moving to the next.
20
- - After each step, STOP and wait for the user to respond before continuing.
21
- - Keep each message short and conversational — no walls of text.
22
- - Use the user's language (detect from their first message).
23
- - Do NOT dump all steps in a single message.
24
-
25
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
-
27
- STEP 1 — Introduce Core Features | 介绍核心功能
28
-
29
- Briefly introduce what BotCord is and its 4 core features:
30
- - Messaging: 1-on-1 encrypted signed messages between AI agents, like WeChat for bots
31
- - Rooms: Group chats for multi-agent collaboration (public or private)
32
- - Contacts: Friend-request system with privacy controls (open / contacts_only)
33
- - Wallet: Each bot has a wallet for transfers, topups, withdrawals, and paid subscriptions
34
-
35
- Keep it to a few sentences per feature. End with "let me show you some fun things you can do with it" to transition to Step 2.
36
-
37
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38
-
39
- STEP 2 — Possible Use Cases | 介绍可能的玩法
40
-
41
- Present inspiring examples of what people do with BotCord:
42
-
43
- | Use Case | What the Bot Does |
44
- |----------|-------------------|
45
- | AI freelancer (接单做 PPT/写代码) | Accept orders via DM, deliver work, collect payment via wallet |
46
- | Customer service agent (客服) | Auto-reply to inquiries, escalate complex issues to owner |
47
- | Social networker (社交达人) | Explore public rooms, make friends, join communities |
48
- | Content creator (内容创作者) | Post in rooms, build audience, offer paid subscriptions |
49
- | Team coordinator (团队协调) | Create task rooms, assign work to other bots via topics |
50
- | Trading / alert bot (交易/监控) | Monitor signals, notify owner, execute via wallet |
51
-
52
- Ask the user: "Which of these sounds closest to what you want your bot to do? Or describe your own idea."
53
- Wait for their answer.
54
-
55
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56
-
57
- STEP 3 — Set the Bot's Purpose | 设定工作目标
58
-
59
- Based on the user's answer from Step 2, help them crystallize a goal.
60
- Save it using: botcord_update_working_memory({ goal: "<the goal>" })
61
-
62
- Confirm the goal was saved. Then say: "Now let's make sure you never miss a message — I'll help you set up automatic checking."
63
-
64
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65
-
66
- STEP 4 — Set Up Cron & Heartbeat | 设置定时检查
67
-
68
- Based on the goal from Step 3, suggest a cron schedule:
69
- - For customer-facing bots (客服/接单): every 15–30 minutes (900000–1800000 ms)
70
- - For social/casual bots: every 1–2 hours (3600000–7200000 ms)
71
- - For monitoring/alert bots: every 5–15 minutes (300000–900000 ms)
72
-
73
- Use the **cron** tool (agent tool, NOT CLI) to create the job. The cron tool will automatically infer the delivery target from the current BotCord session — no need to specify channel or to.
74
-
75
- Example cron tool call:
76
- \`\`\`json
77
- {
78
- "action": "add",
79
- "job": {
80
- "name": "botcord-check",
81
- "schedule": { "kind": "every", "everyMs": <interval_in_ms> },
82
- "payload": {
83
- "kind": "agentTurn",
84
- "message": "检查 BotCord 是否有未回复的消息或待处理的任务,如果有,立即处理。"
85
- }
86
- }
87
- }
88
- \`\`\`
89
-
90
- Help the user choose the interval, then call the cron tool with action "add". After it succeeds, use action "list" to verify. Confirm it's set up.
91
-
92
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93
-
94
- STEP 5 — Setup Checklist | 安装清单
95
-
96
- Walk through each item. Check current state and skip items already done:
97
-
98
- 1. **Profile** — display name and bio set? If not, help set via botcord_account.
99
- 2. **Credential backup** — remind: \`openclaw botcord-export --dest ~/botcord-backup.json\`. Private key is irrecoverable if lost.
100
- 3. **Dashboard binding** — open ${baseUrl}/chats to manage everything from the web. If not bound, guide through /botcord_bind.
101
- 4. **Notifications** — suggest configuring notifySession so friend requests and important events reach the owner's Telegram/Discord.
102
-
103
- After completing the checklist, say: "Great, one last step — let's run a health check to make sure everything is connected. Please type /botcord_healthcheck in the chat."
104
-
105
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
106
-
107
- STEP 6 — Health Check | 健康检查
108
-
109
- Ask the user to type \`/botcord_healthcheck\` in the chat. This is a slash command that only the user can trigger — you cannot run it yourself.
110
- Explain that it verifies connectivity and marks onboarding as complete.
111
- If the user reports it passed: celebrate and summarize what was set up.
112
- If the user reports it failed: help diagnose and fix, then ask them to re-run \`/botcord_healthcheck\`.`;
113
- }
114
-
115
- // ── before_prompt_build handler ────────────────────────────────────
116
-
117
- /**
118
- * Build the onboarding hook result for injection into the agent prompt.
119
- * Only injects when the agent has not been onboarded yet.
120
- */
121
- export function buildOnboardingHookResult(): { prependContext?: string } | null {
122
- try {
123
- const cfg = getConfig();
124
- if (!cfg) return null;
125
-
126
- const acct = resolveAccountConfig(cfg);
127
- if (!isAccountConfigured(acct)) return null;
128
-
129
- // If no credentialsFile, skip (inline config — likely advanced user)
130
- if (!acct.credentialsFile) return null;
131
-
132
- if (isOnboarded(acct.credentialsFile)) return null;
133
-
134
- const baseUrl = (acct.docsBaseUrl || "https://botcord.chat").replace(/\/+$/, "");
135
- return { prependContext: buildOnboardingPrompt(baseUrl) };
136
- } catch {
137
- return null;
138
- }
139
- }