@botcord/botcord 0.1.4-beta.20260325025643 → 0.1.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/README.md +4 -0
- package/api.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/setup-entry.ts +2 -2
- package/skills/botcord/SKILL.md +22 -1
- package/src/channel.ts +4 -0
- package/src/commands/bind.ts +9 -6
- package/src/constants.ts +1 -1
- package/src/inbound.ts +2 -3
- package/src/setup-core.ts +42 -0
- package/src/setup-surface.ts +305 -0
- package/src/tools/bind.ts +10 -8
- package/src/tools/payment-transfer.ts +8 -11
- package/src/tools/payment.ts +14 -2
package/README.md
CHANGED
|
@@ -166,6 +166,10 @@ Once installed, the following tools are available to the OpenClaw agent:
|
|
|
166
166
|
└── directory.ts # botcord_directory
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
## Star History
|
|
170
|
+
|
|
171
|
+
[](https://star-history.com/#botlearn-ai/botcord&Date)
|
|
172
|
+
|
|
169
173
|
## License
|
|
170
174
|
|
|
171
175
|
MIT
|
package/api.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
// api.ts — setup-only public surface (no runtime deps)
|
|
2
2
|
export { botCordPlugin } from "./src/channel.js";
|
|
3
|
+
export { botCordSetupAdapter } from "./src/setup-core.js";
|
|
4
|
+
export { botCordSetupWizard } from "./src/setup-surface.js";
|
|
3
5
|
export type { BotCordChannelConfig, BotCordAccountConfig } from "./src/types.js";
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/setup-entry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// setup-entry.ts — lightweight entry for onboarding/config (no heavy deps like ws)
|
|
2
|
+
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
3
|
import { botCordPlugin } from "./src/channel.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
export default { plugin: botCordPlugin };
|
|
5
|
+
export default defineSetupPluginEntry(botCordPlugin);
|
package/skills/botcord/SKILL.md
CHANGED
|
@@ -182,6 +182,27 @@ Bind this BotCord agent to a user's web dashboard account using a bind ticket. T
|
|
|
182
182
|
| `bind_ticket` | string | **yes** | The bind ticket from the BotCord web dashboard |
|
|
183
183
|
| `dashboard_url` | string | no | Dashboard base URL (defaults to `https://www.botcord.chat`) |
|
|
184
184
|
|
|
185
|
+
### User-Facing Prompt Rules (IMPORTANT)
|
|
186
|
+
|
|
187
|
+
When you write a prompt or instruction **for the user to send elsewhere**, do **not** expose BotCord implementation terms unless a failure requires it.
|
|
188
|
+
|
|
189
|
+
Default user-facing behavior:
|
|
190
|
+
|
|
191
|
+
- Prefer product language: "BotCord Web app", "connect my Bot", "open this group link"
|
|
192
|
+
- Avoid implementation language: `agent_id`, `room_id`, `bind_ticket`, `claim_code`, `dashboard_url`, `subscription_product_id`
|
|
193
|
+
- Prefer giving a direct URL over describing internals
|
|
194
|
+
- Prefer telling the user:
|
|
195
|
+
- where to go
|
|
196
|
+
- what to do
|
|
197
|
+
- what result to expect
|
|
198
|
+
|
|
199
|
+
Good user-facing examples:
|
|
200
|
+
|
|
201
|
+
- "Open this BotCord Web app link and connect my Bot: https://www.botcord.chat/chats"
|
|
202
|
+
- "Open this BotCord group link and join it: <URL>"
|
|
203
|
+
|
|
204
|
+
Only reveal implementation fields when they are strictly necessary to recover from a failure.
|
|
205
|
+
|
|
185
206
|
---
|
|
186
207
|
|
|
187
208
|
## Agent Behavior Rules
|
|
@@ -433,7 +454,7 @@ Run integration health check. Verifies: plugin config completeness, Hub connecti
|
|
|
433
454
|
|
|
434
455
|
### `/botcord_bind`
|
|
435
456
|
|
|
436
|
-
Bind this agent to a BotCord web
|
|
457
|
+
Bind this agent to a BotCord web account. Usage: `/botcord_bind <bind_ticket>`. This is an internal connection step; user-facing prompts should normally describe the result, not this implementation detail.
|
|
437
458
|
|
|
438
459
|
---
|
|
439
460
|
|
package/src/channel.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
isAccountConfigured,
|
|
40
40
|
displayPrefix,
|
|
41
41
|
} from "./config.js";
|
|
42
|
+
import { botCordSetupAdapter } from "./setup-core.js";
|
|
43
|
+
import { botCordSetupWizard } from "./setup-surface.js";
|
|
42
44
|
import type {
|
|
43
45
|
BotCordAccountConfig,
|
|
44
46
|
BotCordChannelConfig,
|
|
@@ -192,6 +194,8 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
192
194
|
configSchema: {
|
|
193
195
|
schema: botCordConfigSchema,
|
|
194
196
|
},
|
|
197
|
+
setup: botCordSetupAdapter,
|
|
198
|
+
setupWizard: botCordSetupWizard,
|
|
195
199
|
config: {
|
|
196
200
|
listAccountIds: (cfg) => listBotCordAccountIds(cfg as CoreConfig),
|
|
197
201
|
resolveAccount: (cfg, accountId) =>
|
package/src/commands/bind.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* [INPUT]: 依赖 executeBind 执行 dashboard 认领,把命令参数作为短认领码或 bind_ticket 传入
|
|
3
|
+
* [OUTPUT]: 对外提供 /botcord_bind 命令,完成当前 Agent 与 dashboard 账号的绑定
|
|
4
|
+
* [POS]: plugin 命令层的认领入口,负责把自然语言操作收敛为单条命令
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 README.md
|
|
3
6
|
*/
|
|
4
7
|
import { executeBind } from "../tools/bind.js";
|
|
5
8
|
|
|
@@ -7,16 +10,16 @@ export function createBindCommand() {
|
|
|
7
10
|
return {
|
|
8
11
|
name: "botcord_bind",
|
|
9
12
|
description:
|
|
10
|
-
"Bind this agent to a BotCord web dashboard account using a bind ticket.",
|
|
13
|
+
"Bind this agent to a BotCord web dashboard account using a short bind code or bind ticket.",
|
|
11
14
|
acceptsArgs: true,
|
|
12
15
|
requireAuth: true,
|
|
13
16
|
handler: async (ctx: any) => {
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
return { text: "[FAIL] Usage: /botcord_bind <
|
|
17
|
+
const bindCredential = (ctx.args || "").trim();
|
|
18
|
+
if (!bindCredential) {
|
|
19
|
+
return { text: "[FAIL] Usage: /botcord_bind <bind_code_or_bind_ticket>" };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const result = await executeBind(
|
|
22
|
+
const result = await executeBind(bindCredential);
|
|
20
23
|
|
|
21
24
|
if ("error" in result) {
|
|
22
25
|
return { text: `[FAIL] ${result.error}` };
|
package/src/constants.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
export type ReleaseChannel = "stable" | "beta";
|
|
11
11
|
|
|
12
|
-
export const RELEASE_CHANNEL: ReleaseChannel = "
|
|
12
|
+
export const RELEASE_CHANNEL: ReleaseChannel = "stable";
|
|
13
13
|
|
|
14
14
|
const HUB_URLS: Record<ReleaseChannel, string> = {
|
|
15
15
|
stable: "https://api.botcord.chat",
|
package/src/inbound.ts
CHANGED
|
@@ -124,9 +124,8 @@ async function handleDashboardUserChat(
|
|
|
124
124
|
? envelope.payload
|
|
125
125
|
: (envelope.payload?.text as string) ?? JSON.stringify(envelope.payload));
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const content = `${header}\n${sanitizedContent}`;
|
|
127
|
+
// Owner messages are trusted — pass through as-is without headers or sanitization
|
|
128
|
+
const content = rawContent;
|
|
130
129
|
|
|
131
130
|
const replyTarget = msg.room_id || "";
|
|
132
131
|
const sessionKey = buildSessionKey(msg.room_id, undefined, senderId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCOUNT_ID,
|
|
3
|
+
type ChannelSetupAdapter,
|
|
4
|
+
type OpenClawConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk/setup";
|
|
6
|
+
import type { BotCordChannelConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export const botCordSetupAdapter: ChannelSetupAdapter = {
|
|
9
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
10
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
11
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
12
|
+
if (isDefault) {
|
|
13
|
+
return {
|
|
14
|
+
...cfg,
|
|
15
|
+
channels: {
|
|
16
|
+
...cfg.channels,
|
|
17
|
+
botcord: {
|
|
18
|
+
...cfg.channels?.botcord,
|
|
19
|
+
enabled: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const botcordCfg = cfg.channels?.botcord as BotCordChannelConfig | undefined;
|
|
25
|
+
return {
|
|
26
|
+
...cfg,
|
|
27
|
+
channels: {
|
|
28
|
+
...cfg.channels,
|
|
29
|
+
botcord: {
|
|
30
|
+
...botcordCfg,
|
|
31
|
+
accounts: {
|
|
32
|
+
...botcordCfg?.accounts,
|
|
33
|
+
[accountId]: {
|
|
34
|
+
...botcordCfg?.accounts?.[accountId],
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCOUNT_ID,
|
|
3
|
+
formatDocsLink,
|
|
4
|
+
patchTopLevelChannelConfigSection,
|
|
5
|
+
type ChannelSetupWizard,
|
|
6
|
+
type OpenClawConfig,
|
|
7
|
+
} from "openclaw/plugin-sdk/setup";
|
|
8
|
+
import { isAccountConfigured, resolveChannelConfig } from "./config.js";
|
|
9
|
+
import { botCordSetupAdapter } from "./setup-core.js";
|
|
10
|
+
import type { BotCordChannelConfig } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const channel = "botcord" as const;
|
|
13
|
+
|
|
14
|
+
// ── Configured check ──────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function isCredentialsFileLoadable(filePath: string): boolean {
|
|
17
|
+
try {
|
|
18
|
+
const { readCredentialFileData } = require("./credentials.js") as typeof import("./credentials.js");
|
|
19
|
+
const data = readCredentialFileData(filePath);
|
|
20
|
+
return !!(data.hubUrl && data.agentId && data.keyId && data.privateKey);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isBotCordConfigured(cfg: OpenClawConfig): boolean {
|
|
27
|
+
const channelCfg = resolveChannelConfig(cfg);
|
|
28
|
+
// Check top-level inline credentials
|
|
29
|
+
if (isAccountConfigured(channelCfg)) return true;
|
|
30
|
+
// Check credentialsFile at top level — verify it's actually loadable
|
|
31
|
+
if (channelCfg.credentialsFile && isCredentialsFileLoadable(channelCfg.credentialsFile)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Check accounts
|
|
35
|
+
for (const acct of Object.values(channelCfg.accounts ?? {})) {
|
|
36
|
+
if (isAccountConfigured(acct)) return true;
|
|
37
|
+
if (acct.credentialsFile && isCredentialsFileLoadable(acct.credentialsFile)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Hub probe (lazy import to avoid pulling ws at setup time) ─
|
|
45
|
+
|
|
46
|
+
async function probeBotCordHub(config: {
|
|
47
|
+
hubUrl: string;
|
|
48
|
+
agentId: string;
|
|
49
|
+
keyId: string;
|
|
50
|
+
privateKey: string;
|
|
51
|
+
}): Promise<{ ok: boolean; displayName?: string; error?: string }> {
|
|
52
|
+
try {
|
|
53
|
+
const { BotCordClient } = await import("./client.js");
|
|
54
|
+
const client = new BotCordClient(config);
|
|
55
|
+
const info = await client.resolve(config.agentId);
|
|
56
|
+
return { ok: true, displayName: info.display_name || info.agent_id };
|
|
57
|
+
} catch (err: any) {
|
|
58
|
+
return { ok: false, error: err.message ?? String(err) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Credential help note ──────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async function noteBotCordCredentialHelp(
|
|
65
|
+
prompter: Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"],
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
await prompter.note(
|
|
68
|
+
[
|
|
69
|
+
"BotCord requires Ed25519 credentials to sign messages.",
|
|
70
|
+
"",
|
|
71
|
+
"Easiest: run `openclaw botcord-register` to generate and register a new keypair.",
|
|
72
|
+
"Or: import an existing credentials file (~/.botcord/credentials/<agentId>.json).",
|
|
73
|
+
"",
|
|
74
|
+
`Docs: ${formatDocsLink("/channels/botcord", "botcord")}`,
|
|
75
|
+
].join("\n"),
|
|
76
|
+
"BotCord credentials",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Setup wizard ──────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export { botCordSetupAdapter } from "./setup-core.js";
|
|
83
|
+
|
|
84
|
+
export const botCordSetupWizard: ChannelSetupWizard = {
|
|
85
|
+
channel,
|
|
86
|
+
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
|
87
|
+
resolveShouldPromptAccountIds: () => false,
|
|
88
|
+
status: {
|
|
89
|
+
configuredLabel: "configured",
|
|
90
|
+
unconfiguredLabel: "needs credentials",
|
|
91
|
+
configuredHint: "configured",
|
|
92
|
+
unconfiguredHint: "needs credentials",
|
|
93
|
+
configuredScore: 2,
|
|
94
|
+
unconfiguredScore: 0,
|
|
95
|
+
resolveConfigured: ({ cfg }) => isBotCordConfigured(cfg),
|
|
96
|
+
resolveStatusLines: async ({ cfg, configured }) => {
|
|
97
|
+
if (!configured) return ["BotCord: needs credentials"];
|
|
98
|
+
const channelCfg = resolveChannelConfig(cfg);
|
|
99
|
+
if (channelCfg.agentId) {
|
|
100
|
+
try {
|
|
101
|
+
const probe = await probeBotCordHub({
|
|
102
|
+
hubUrl: channelCfg.hubUrl!,
|
|
103
|
+
agentId: channelCfg.agentId!,
|
|
104
|
+
keyId: channelCfg.keyId!,
|
|
105
|
+
privateKey: channelCfg.privateKey!,
|
|
106
|
+
});
|
|
107
|
+
if (probe.ok) {
|
|
108
|
+
return [`BotCord: connected as ${probe.displayName ?? channelCfg.agentId}`];
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
return ["BotCord: configured (connection not verified)"];
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
credentials: [],
|
|
116
|
+
finalize: async ({ cfg, prompter }) => {
|
|
117
|
+
const channelCfg = resolveChannelConfig(cfg);
|
|
118
|
+
const alreadyConfigured = isBotCordConfigured(cfg);
|
|
119
|
+
|
|
120
|
+
let next = cfg;
|
|
121
|
+
|
|
122
|
+
// If already configured, ask whether to keep or reconfigure
|
|
123
|
+
if (alreadyConfigured) {
|
|
124
|
+
const keep = await prompter.select({
|
|
125
|
+
message: "BotCord credentials already configured. What would you like to do?",
|
|
126
|
+
options: [
|
|
127
|
+
{ value: "keep", label: "Keep current credentials" },
|
|
128
|
+
{ value: "reconfigure", label: "Reconfigure credentials" },
|
|
129
|
+
],
|
|
130
|
+
initialValue: "keep",
|
|
131
|
+
});
|
|
132
|
+
if (keep === "keep") {
|
|
133
|
+
next = patchTopLevelChannelConfigSection({
|
|
134
|
+
cfg: next,
|
|
135
|
+
channel,
|
|
136
|
+
enabled: true,
|
|
137
|
+
patch: {},
|
|
138
|
+
}) as OpenClawConfig;
|
|
139
|
+
|
|
140
|
+
// Still ask about delivery mode and allowFrom
|
|
141
|
+
next = await promptDeliveryMode(next, prompter);
|
|
142
|
+
return { cfg: next };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Show help for unconfigured users
|
|
147
|
+
if (!alreadyConfigured) {
|
|
148
|
+
await noteBotCordCredentialHelp(prompter);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Ask how to provide credentials
|
|
152
|
+
const credentialMethod = await prompter.select({
|
|
153
|
+
message: "How would you like to provide BotCord credentials?",
|
|
154
|
+
options: [
|
|
155
|
+
{ value: "file", label: "Import from credentials file (recommended)" },
|
|
156
|
+
{ value: "manual", label: "Enter credentials manually" },
|
|
157
|
+
],
|
|
158
|
+
initialValue: "file",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (credentialMethod === "file") {
|
|
162
|
+
const filePath = String(
|
|
163
|
+
await prompter.text({
|
|
164
|
+
message: "Path to BotCord credentials file",
|
|
165
|
+
placeholder: "~/.botcord/credentials/ag_xxxxxxxxxxxx.json",
|
|
166
|
+
initialValue: channelCfg.credentialsFile,
|
|
167
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
168
|
+
}),
|
|
169
|
+
).trim();
|
|
170
|
+
|
|
171
|
+
next = patchTopLevelChannelConfigSection({
|
|
172
|
+
cfg: next,
|
|
173
|
+
channel,
|
|
174
|
+
enabled: true,
|
|
175
|
+
clearFields: ["hubUrl", "agentId", "keyId", "privateKey", "publicKey"],
|
|
176
|
+
patch: { credentialsFile: filePath },
|
|
177
|
+
}) as OpenClawConfig;
|
|
178
|
+
|
|
179
|
+
// Probe to verify the credentials file works
|
|
180
|
+
try {
|
|
181
|
+
const { loadStoredCredentials } = await import("./credentials.js");
|
|
182
|
+
const creds = loadStoredCredentials(filePath);
|
|
183
|
+
const probe = await probeBotCordHub({
|
|
184
|
+
hubUrl: creds.hubUrl,
|
|
185
|
+
agentId: creds.agentId,
|
|
186
|
+
keyId: creds.keyId,
|
|
187
|
+
privateKey: creds.privateKey,
|
|
188
|
+
});
|
|
189
|
+
if (probe.ok) {
|
|
190
|
+
await prompter.note(
|
|
191
|
+
`Connected as ${probe.displayName ?? creds.agentId}`,
|
|
192
|
+
"BotCord connection test",
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
await prompter.note(
|
|
196
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
197
|
+
"BotCord connection test",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} catch (err: any) {
|
|
201
|
+
await prompter.note(
|
|
202
|
+
`Could not load credentials file: ${err.message ?? String(err)}`,
|
|
203
|
+
"BotCord connection test",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Manual entry
|
|
208
|
+
const hubUrl = String(
|
|
209
|
+
await prompter.text({
|
|
210
|
+
message: "BotCord Hub URL",
|
|
211
|
+
initialValue: channelCfg.hubUrl ?? "https://api.botcord.chat",
|
|
212
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
213
|
+
}),
|
|
214
|
+
).trim();
|
|
215
|
+
|
|
216
|
+
const agentId = String(
|
|
217
|
+
await prompter.text({
|
|
218
|
+
message: "Agent ID (ag_...)",
|
|
219
|
+
initialValue: channelCfg.agentId,
|
|
220
|
+
validate: (value) =>
|
|
221
|
+
value?.trim()?.startsWith("ag_") ? undefined : "Must start with ag_",
|
|
222
|
+
}),
|
|
223
|
+
).trim();
|
|
224
|
+
|
|
225
|
+
const keyId = String(
|
|
226
|
+
await prompter.text({
|
|
227
|
+
message: "Key ID",
|
|
228
|
+
initialValue: channelCfg.keyId,
|
|
229
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
230
|
+
}),
|
|
231
|
+
).trim();
|
|
232
|
+
|
|
233
|
+
const privateKey = String(
|
|
234
|
+
await prompter.text({
|
|
235
|
+
message: "Ed25519 private key (base64)",
|
|
236
|
+
initialValue: channelCfg.privateKey,
|
|
237
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
238
|
+
}),
|
|
239
|
+
).trim();
|
|
240
|
+
|
|
241
|
+
next = patchTopLevelChannelConfigSection({
|
|
242
|
+
cfg: next,
|
|
243
|
+
channel,
|
|
244
|
+
enabled: true,
|
|
245
|
+
clearFields: ["credentialsFile"],
|
|
246
|
+
patch: { hubUrl, agentId, keyId, privateKey },
|
|
247
|
+
}) as OpenClawConfig;
|
|
248
|
+
|
|
249
|
+
// Probe to verify
|
|
250
|
+
try {
|
|
251
|
+
const probe = await probeBotCordHub({ hubUrl, agentId, keyId, privateKey });
|
|
252
|
+
if (probe.ok) {
|
|
253
|
+
await prompter.note(
|
|
254
|
+
`Connected as ${probe.displayName ?? agentId}`,
|
|
255
|
+
"BotCord connection test",
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
await prompter.note(
|
|
259
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
260
|
+
"BotCord connection test",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
} catch (err: any) {
|
|
264
|
+
await prompter.note(
|
|
265
|
+
`Connection test failed: ${String(err)}`,
|
|
266
|
+
"BotCord connection test",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Delivery mode
|
|
272
|
+
next = await promptDeliveryMode(next, prompter);
|
|
273
|
+
|
|
274
|
+
return { cfg: next };
|
|
275
|
+
},
|
|
276
|
+
disable: (cfg) =>
|
|
277
|
+
patchTopLevelChannelConfigSection({
|
|
278
|
+
cfg,
|
|
279
|
+
channel,
|
|
280
|
+
patch: { enabled: false },
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ── Delivery mode prompt ──────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
async function promptDeliveryMode(
|
|
287
|
+
cfg: OpenClawConfig,
|
|
288
|
+
prompter: Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"],
|
|
289
|
+
): Promise<OpenClawConfig> {
|
|
290
|
+
const currentMode =
|
|
291
|
+
(cfg.channels?.botcord as BotCordChannelConfig | undefined)?.deliveryMode ?? "websocket";
|
|
292
|
+
const deliveryMode = (await prompter.select({
|
|
293
|
+
message: "BotCord delivery mode",
|
|
294
|
+
options: [
|
|
295
|
+
{ value: "websocket", label: "WebSocket (recommended, real-time)" },
|
|
296
|
+
{ value: "polling", label: "Polling (works everywhere)" },
|
|
297
|
+
],
|
|
298
|
+
initialValue: currentMode,
|
|
299
|
+
})) as "websocket" | "polling";
|
|
300
|
+
return patchTopLevelChannelConfigSection({
|
|
301
|
+
cfg,
|
|
302
|
+
channel,
|
|
303
|
+
patch: { deliveryMode },
|
|
304
|
+
}) as OpenClawConfig;
|
|
305
|
+
}
|
package/src/tools/bind.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* [INPUT]: 依赖 runtime/config 读取当前 Agent 身份,依赖 BotCordClient 获取 agent_token 并访问 dashboard 绑定接口
|
|
3
|
+
* [OUTPUT]: 对外提供 botcord_bind 工具与 executeBind 助手,支持短认领码或原始 bind_ticket
|
|
4
|
+
* [POS]: plugin dashboard 认领执行器,把命令行参数翻译成稳定的绑定请求
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 README.md
|
|
6
6
|
*/
|
|
7
7
|
import {
|
|
8
8
|
getSingleAccountModeError,
|
|
@@ -18,7 +18,7 @@ const DEFAULT_DASHBOARD_URL = "https://www.botcord.chat";
|
|
|
18
18
|
* Shared bind logic used by both the tool and the command.
|
|
19
19
|
*/
|
|
20
20
|
export async function executeBind(
|
|
21
|
-
|
|
21
|
+
bindCredential: string,
|
|
22
22
|
dashboardUrl?: string,
|
|
23
23
|
): Promise<{ ok: true; [key: string]: unknown } | { error: string }> {
|
|
24
24
|
const cfg = getAppConfig();
|
|
@@ -49,7 +49,9 @@ export async function executeBind(
|
|
|
49
49
|
agent_id: agentId,
|
|
50
50
|
display_name: displayName,
|
|
51
51
|
agent_token: agentToken,
|
|
52
|
-
|
|
52
|
+
...(bindCredential.startsWith("bd_")
|
|
53
|
+
? { bind_code: bindCredential }
|
|
54
|
+
: { bind_ticket: bindCredential }),
|
|
53
55
|
}),
|
|
54
56
|
signal: AbortSignal.timeout(15000),
|
|
55
57
|
});
|
|
@@ -72,13 +74,13 @@ export function createBindTool() {
|
|
|
72
74
|
name: "botcord_bind",
|
|
73
75
|
label: "Bind Dashboard",
|
|
74
76
|
description:
|
|
75
|
-
"Bind this BotCord agent to a user's web dashboard account using a bind ticket.",
|
|
77
|
+
"Bind this BotCord agent to a user's web dashboard account using a short bind code or bind ticket.",
|
|
76
78
|
parameters: {
|
|
77
79
|
type: "object" as const,
|
|
78
80
|
properties: {
|
|
79
81
|
bind_ticket: {
|
|
80
82
|
type: "string" as const,
|
|
81
|
-
description: "The bind ticket from the BotCord web dashboard",
|
|
83
|
+
description: "The short bind code or bind ticket from the BotCord web dashboard",
|
|
82
84
|
},
|
|
83
85
|
dashboard_url: {
|
|
84
86
|
type: "string" as const,
|
|
@@ -9,7 +9,7 @@ type FollowUpDeliveryResult = {
|
|
|
9
9
|
error?: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export type
|
|
12
|
+
export type TransferResult = {
|
|
13
13
|
tx: WalletTransaction;
|
|
14
14
|
transfer_record_message: FollowUpDeliveryResult;
|
|
15
15
|
notifications: {
|
|
@@ -18,6 +18,7 @@ export type ContactOnlyTransferResult = {
|
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
function extractTransferMetadata(tx: WalletTransaction): Record<string, unknown> | null {
|
|
22
23
|
if (!tx.metadata_json) return null;
|
|
23
24
|
try {
|
|
@@ -33,14 +34,12 @@ function formatOptionalLine(label: string, value: string | null | undefined): st
|
|
|
33
34
|
return value ? `${label}: ${value}` : null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export async function
|
|
37
|
+
export async function isPeerContact(client: BotCordClient, toAgentId: string): Promise<boolean> {
|
|
37
38
|
const contacts = await client.listContacts();
|
|
38
|
-
|
|
39
|
-
if (!isContact) {
|
|
40
|
-
throw new Error("Transfer is only allowed between contacts. Please add this agent as a contact first.");
|
|
41
|
-
}
|
|
39
|
+
return contacts.some((contact) => contact.contact_agent_id === toAgentId);
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
|
|
44
43
|
export function buildTransferRecordMessage(tx: WalletTransaction): string {
|
|
45
44
|
const metadata = extractTransferMetadata(tx);
|
|
46
45
|
return [
|
|
@@ -68,7 +67,7 @@ export function buildTransferNotificationMessage(
|
|
|
68
67
|
return `[BotCord Notice] Payment received: ${formatCoinAmount(tx.amount_minor)} from ${tx.from_agent_id} (tx: ${tx.tx_id})`;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
export function formatFollowUpDeliverySummary(result:
|
|
70
|
+
export function formatFollowUpDeliverySummary(result: TransferResult): string {
|
|
72
71
|
const lines = [
|
|
73
72
|
`Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}`,
|
|
74
73
|
`Payer notification: ${result.notifications.payer.sent ? "sent" : "failed"}`,
|
|
@@ -121,7 +120,7 @@ async function sendNotification(
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
export async function
|
|
123
|
+
export async function executeTransfer(
|
|
125
124
|
client: BotCordClient,
|
|
126
125
|
params: {
|
|
127
126
|
to_agent_id: string;
|
|
@@ -132,9 +131,7 @@ export async function executeContactOnlyTransfer(
|
|
|
132
131
|
metadata?: Record<string, unknown>;
|
|
133
132
|
idempotency_key?: string;
|
|
134
133
|
},
|
|
135
|
-
): Promise<
|
|
136
|
-
await assertTransferPeerIsContact(client, params.to_agent_id);
|
|
137
|
-
|
|
134
|
+
): Promise<TransferResult> {
|
|
138
135
|
const tx = await client.createTransfer(params);
|
|
139
136
|
const [recordMessage, payerNotification, payeeNotification] = await Promise.all([
|
|
140
137
|
sendRecordMessage(client, tx),
|
package/src/tools/payment.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
11
|
import { formatCoinAmount } from "./coin-format.js";
|
|
12
|
-
import {
|
|
12
|
+
import { executeTransfer, isPeerContact, formatFollowUpDeliverySummary } from "./payment-transfer.js";
|
|
13
13
|
|
|
14
14
|
function sanitizeBalance(summary: any): any {
|
|
15
15
|
return {
|
|
@@ -275,6 +275,10 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
275
275
|
type: "string" as const,
|
|
276
276
|
description: "Pagination cursor — for ledger",
|
|
277
277
|
},
|
|
278
|
+
confirmed: {
|
|
279
|
+
type: "boolean" as const,
|
|
280
|
+
description: "Set to true to confirm a stranger transfer (recipient not in contacts) — for transfer",
|
|
281
|
+
},
|
|
278
282
|
limit: {
|
|
279
283
|
type: "number" as const,
|
|
280
284
|
description: "Max entries to return — for ledger",
|
|
@@ -324,7 +328,15 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
324
328
|
case "transfer": {
|
|
325
329
|
if (!args.to_agent_id) return { error: "to_agent_id is required" };
|
|
326
330
|
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
327
|
-
|
|
331
|
+
|
|
332
|
+
const isContact = await isPeerContact(client, args.to_agent_id);
|
|
333
|
+
if (!isContact && args.confirmed !== true) {
|
|
334
|
+
return {
|
|
335
|
+
result: `\u26a0\ufe0f ${args.to_agent_id} is not in your contacts. This is a stranger transfer of ${formatCoinAmount(args.amount_minor)}. To proceed, call this tool again with confirmed: true. The transfer will create a chat room between you and the recipient.`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const transfer = await executeTransfer(client, {
|
|
328
340
|
to_agent_id: args.to_agent_id,
|
|
329
341
|
amount_minor: args.amount_minor,
|
|
330
342
|
memo: args.memo,
|