@aspalagin/openclaw-max 0.7.2
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/LICENSE +21 -0
- package/README.md +424 -0
- package/SKILL.md +108 -0
- package/dist/index.js +17 -0
- package/dist/setup-entry.js +8 -0
- package/dist/src/accounts.js +148 -0
- package/dist/src/actions.js +421 -0
- package/dist/src/api.js +553 -0
- package/dist/src/callbacks.js +111 -0
- package/dist/src/channel-agent-prompt.js +38 -0
- package/dist/src/channel-config.js +103 -0
- package/dist/src/channel-directory.js +84 -0
- package/dist/src/channel-lifecycle.js +219 -0
- package/dist/src/channel-outbound.js +133 -0
- package/dist/src/channel-policy.js +130 -0
- package/dist/src/channel.js +76 -0
- package/dist/src/config-schema.js +133 -0
- package/dist/src/deliver.js +126 -0
- package/dist/src/dispatch.js +186 -0
- package/dist/src/format.js +31 -0
- package/dist/src/inbound-attachments.js +125 -0
- package/dist/src/inbound.js +362 -0
- package/dist/src/media-temp.js +92 -0
- package/dist/src/model-buttons.js +96 -0
- package/dist/src/monitor-types.js +27 -0
- package/dist/src/monitor.js +40 -0
- package/dist/src/onboarding.js +180 -0
- package/dist/src/polling.js +98 -0
- package/dist/src/presentation.js +374 -0
- package/dist/src/runtime.js +36 -0
- package/dist/src/russian-trusted-ca.js +84 -0
- package/dist/src/send.js +612 -0
- package/dist/src/state.js +111 -0
- package/dist/src/sticker-cache.js +26 -0
- package/dist/src/stream-draft.js +102 -0
- package/dist/src/types.js +5 -0
- package/dist/src/webhook-queue.js +215 -0
- package/dist/src/webhook-runner.js +233 -0
- package/dist/src/webhook.js +196 -0
- package/openclaw.plugin.json +263 -0
- package/package.json +100 -0
- package/scripts/check-cycles.mjs +38 -0
- package/scripts/test-api.mjs +169 -0
- package/scripts/test-send.mjs +162 -0
- package/scripts/update-schema.mjs +48 -0
- package/src/accounts.ts +193 -0
- package/src/actions.ts +501 -0
- package/src/api.ts +758 -0
- package/src/callbacks.ts +140 -0
- package/src/channel-agent-prompt.ts +47 -0
- package/src/channel-config.ts +138 -0
- package/src/channel-directory.ts +90 -0
- package/src/channel-lifecycle.ts +256 -0
- package/src/channel-outbound.ts +159 -0
- package/src/channel-policy.ts +185 -0
- package/src/channel.ts +99 -0
- package/src/config-schema.ts +154 -0
- package/src/deliver.ts +168 -0
- package/src/dispatch.ts +224 -0
- package/src/format.ts +33 -0
- package/src/inbound-attachments.ts +152 -0
- package/src/inbound.ts +412 -0
- package/src/media-temp.ts +103 -0
- package/src/model-buttons.ts +150 -0
- package/src/monitor-types.ts +61 -0
- package/src/monitor.ts +49 -0
- package/src/onboarding.ts +215 -0
- package/src/polling.ts +125 -0
- package/src/presentation.ts +452 -0
- package/src/runtime.ts +56 -0
- package/src/russian-trusted-ca.ts +86 -0
- package/src/send.ts +774 -0
- package/src/state.ts +149 -0
- package/src/sticker-cache.ts +28 -0
- package/src/stream-draft.ts +125 -0
- package/src/types.ts +263 -0
- package/src/webhook-queue.ts +248 -0
- package/src/webhook-runner.ts +296 -0
- package/src/webhook.ts +268 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MAX Bot API — integration test script
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* MAX_BOT_TOKEN=xxx node scripts/test-api.mjs [chat_id]
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* 1. GET /me — bot is alive, prints name + user_id
|
|
11
|
+
* 2. GET /updates — fetches recent updates (short poll, 3s)
|
|
12
|
+
* 3. POST /messages — sends test message (if chat_id provided)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const API = "https://platform-api.max.ru";
|
|
16
|
+
const TOKEN = process.env.MAX_BOT_TOKEN;
|
|
17
|
+
const CHAT_ID = process.argv[2] ? Number(process.argv[2]) : null;
|
|
18
|
+
|
|
19
|
+
if (!TOKEN) {
|
|
20
|
+
console.error("❌ MAX_BOT_TOKEN is not set. Export it first:");
|
|
21
|
+
console.error(" export MAX_BOT_TOKEN=your_token_here");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const headers = {
|
|
26
|
+
Authorization: TOKEN,
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
async function apiGet(path) {
|
|
33
|
+
const url = `${API}${path}`;
|
|
34
|
+
console.log(`→ GET ${url}`);
|
|
35
|
+
const res = await fetch(url, { headers });
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body = await res.text().catch(() => "");
|
|
38
|
+
throw new Error(`HTTP ${res.status}: ${body.slice(0, 300)}`);
|
|
39
|
+
}
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function apiPost(path, body) {
|
|
44
|
+
const url = `${API}${path}`;
|
|
45
|
+
console.log(`→ POST ${url}`);
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const text = await res.text().catch(() => "");
|
|
53
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
54
|
+
}
|
|
55
|
+
return res.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ok(msg) {
|
|
59
|
+
console.log(`✅ ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(msg) {
|
|
63
|
+
console.error(`❌ ${msg}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Test 1: GET /me ────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async function testMe() {
|
|
69
|
+
console.log("\n── Test 1: GET /me ──");
|
|
70
|
+
try {
|
|
71
|
+
const me = await apiGet("/me");
|
|
72
|
+
ok(`Bot is alive!`);
|
|
73
|
+
console.log(` user_id: ${me.user_id}`);
|
|
74
|
+
console.log(` name: ${me.first_name ?? me.name ?? "–"}`);
|
|
75
|
+
console.log(` username: ${me.username ?? "–"}`);
|
|
76
|
+
console.log(` is_bot: ${me.is_bot}`);
|
|
77
|
+
return me;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
fail(`GET /me failed: ${err.message}`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Test 2: GET /updates ───────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function testUpdates() {
|
|
87
|
+
console.log("\n── Test 2: GET /updates (short poll, 3s) ──");
|
|
88
|
+
try {
|
|
89
|
+
const data = await apiGet("/updates?timeout=3&limit=5");
|
|
90
|
+
const count = data.updates?.length ?? 0;
|
|
91
|
+
ok(`Got ${count} update(s), marker=${data.marker ?? "null"}`);
|
|
92
|
+
|
|
93
|
+
if (count > 0) {
|
|
94
|
+
for (const u of data.updates) {
|
|
95
|
+
const type = u.update_type;
|
|
96
|
+
const ts = new Date(u.timestamp).toISOString();
|
|
97
|
+
let info = "";
|
|
98
|
+
|
|
99
|
+
if (type === "message_created" && u.message) {
|
|
100
|
+
const sender = u.message.sender;
|
|
101
|
+
const text = u.message.body?.text ?? "(no text)";
|
|
102
|
+
const sName = sender
|
|
103
|
+
? `${sender.first_name ?? ""} ${sender.last_name ?? ""}`.trim()
|
|
104
|
+
: "unknown";
|
|
105
|
+
info = `from=${sName} (${sender?.user_id}): "${text.slice(0, 60)}"`;
|
|
106
|
+
} else if (type === "bot_started") {
|
|
107
|
+
info = `user=${u.user?.first_name} (${u.user?.user_id})`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(` [${ts}] ${type} ${info}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
fail(`GET /updates failed: ${err.message}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Test 3: POST /messages ─────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async function testSendMessage(chatId) {
|
|
123
|
+
console.log(`\n── Test 3: POST /messages (chat_id=${chatId}) ──`);
|
|
124
|
+
try {
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
const body = {
|
|
127
|
+
text: `🤖 Тестовое сообщение от MAX-бота.\nВремя: ${now}`,
|
|
128
|
+
format: "markdown",
|
|
129
|
+
};
|
|
130
|
+
const data = await apiPost(`/messages?chat_id=${chatId}`, body);
|
|
131
|
+
ok(`Message sent!`);
|
|
132
|
+
console.log(` message_id: ${data.message?.body?.mid ?? JSON.stringify(data).slice(0, 100)}`);
|
|
133
|
+
return data;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
fail(`POST /messages failed: ${err.message}`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Main ───────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
console.log("═══ MAX Bot API Test ═══");
|
|
144
|
+
console.log(`Token: ${TOKEN.slice(0, 8)}...${TOKEN.slice(-4)}`);
|
|
145
|
+
|
|
146
|
+
const me = await testMe();
|
|
147
|
+
if (!me) {
|
|
148
|
+
console.error("\nBot unreachable. Check your token.");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await testUpdates();
|
|
153
|
+
|
|
154
|
+
if (CHAT_ID) {
|
|
155
|
+
await testSendMessage(CHAT_ID);
|
|
156
|
+
} else {
|
|
157
|
+
console.log("\n── Test 3: skipped (no chat_id) ──");
|
|
158
|
+
console.log(" To test sending, run:");
|
|
159
|
+
console.log(` MAX_BOT_TOKEN=... node scripts/test-api.mjs <chat_id>`);
|
|
160
|
+
console.log(" (Write to the bot first, then grab chat_id from updates)");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log("\n═══ Done ═══");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
console.error("Fatal:", err);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MAX Bot API — send + edit + delete integration test
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* MAX_BOT_TOKEN=xxx node scripts/test-send.mjs <chat_id> [текст]
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Send message to chat_id
|
|
11
|
+
* 2. Wait 3 seconds
|
|
12
|
+
* 3. Edit message (append " ✅ отредактировано")
|
|
13
|
+
* 4. Wait 3 seconds
|
|
14
|
+
* 5. Delete message
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const API = "https://platform-api.max.ru";
|
|
18
|
+
const TOKEN = process.env.MAX_BOT_TOKEN;
|
|
19
|
+
const CHAT_ID = process.argv[2];
|
|
20
|
+
const TEXT = process.argv[3] || "🤖 Тестовое сообщение от MAX-бота";
|
|
21
|
+
|
|
22
|
+
if (!TOKEN) {
|
|
23
|
+
console.error("❌ MAX_BOT_TOKEN is not set.");
|
|
24
|
+
console.error(" export MAX_BOT_TOKEN=your_token_here");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!CHAT_ID) {
|
|
29
|
+
console.error("❌ chat_id not provided.");
|
|
30
|
+
console.error(" Usage: MAX_BOT_TOKEN=xxx node scripts/test-send.mjs <chat_id> [текст]");
|
|
31
|
+
console.error(" Tip: get chat_id from `node scripts/test-api.mjs` updates");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const headers = {
|
|
36
|
+
Authorization: TOKEN,
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Accept: "application/json",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function sleep(ms) {
|
|
42
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Step 1: Send ───────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
async function sendMessage(chatId, text) {
|
|
48
|
+
console.log(`\n── Step 1: POST /messages (send) ──`);
|
|
49
|
+
console.log(` chat_id: ${chatId}`);
|
|
50
|
+
console.log(` text: "${text}"`);
|
|
51
|
+
|
|
52
|
+
const url = `${API}/messages?chat_id=${chatId}`;
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify({ text }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = await res.text().catch(() => "");
|
|
61
|
+
throw new Error(`Send failed: HTTP ${res.status} — ${body.slice(0, 300)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const mid = data.message?.body?.mid;
|
|
66
|
+
|
|
67
|
+
if (!mid) {
|
|
68
|
+
console.log(" Response:", JSON.stringify(data).slice(0, 200));
|
|
69
|
+
throw new Error("No message_id in response");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(` ✅ Sent! message_id: ${mid}`);
|
|
73
|
+
return mid;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Step 2: Edit ───────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
async function editMessage(messageId, newText) {
|
|
79
|
+
console.log(`\n── Step 3: PUT /messages (edit) ──`);
|
|
80
|
+
console.log(` message_id: ${messageId}`);
|
|
81
|
+
console.log(` new text: "${newText}"`);
|
|
82
|
+
|
|
83
|
+
const url = `${API}/messages?message_id=${messageId}`;
|
|
84
|
+
const res = await fetch(url, {
|
|
85
|
+
method: "PUT",
|
|
86
|
+
headers,
|
|
87
|
+
body: JSON.stringify({ text: newText }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const body = await res.text().catch(() => "");
|
|
92
|
+
throw new Error(`Edit failed: HTTP ${res.status} — ${body.slice(0, 300)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
if (data.success === false) {
|
|
97
|
+
throw new Error(`Edit failed: ${data.message || "unknown"}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(` ✅ Edited!`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Step 3: Delete ─────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
async function deleteMessage(messageId) {
|
|
106
|
+
console.log(`\n── Step 5: DELETE /messages (delete) ──`);
|
|
107
|
+
console.log(` message_id: ${messageId}`);
|
|
108
|
+
|
|
109
|
+
const url = `${API}/messages?message_id=${messageId}`;
|
|
110
|
+
const res = await fetch(url, {
|
|
111
|
+
method: "DELETE",
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: TOKEN,
|
|
114
|
+
Accept: "application/json",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const body = await res.text().catch(() => "");
|
|
120
|
+
throw new Error(`Delete failed: HTTP ${res.status} — ${body.slice(0, 300)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
if (data.success === false) {
|
|
125
|
+
throw new Error(`Delete failed: ${data.message || "unknown"}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(` ✅ Deleted!`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Main ───────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
console.log("═══ MAX Bot API: Send → Edit → Delete Test ═══");
|
|
135
|
+
console.log(`Token: ${TOKEN.slice(0, 8)}...${TOKEN.slice(-4)}`);
|
|
136
|
+
console.log(`Chat: ${CHAT_ID}`);
|
|
137
|
+
|
|
138
|
+
// Step 1: Send
|
|
139
|
+
const mid = await sendMessage(CHAT_ID, TEXT);
|
|
140
|
+
|
|
141
|
+
// Step 2: Wait
|
|
142
|
+
console.log(`\n── Step 2: Ожидание 3 сек... ──`);
|
|
143
|
+
await sleep(3000);
|
|
144
|
+
|
|
145
|
+
// Step 3: Edit
|
|
146
|
+
const editedText = `${TEXT} ✅ отредактировано`;
|
|
147
|
+
await editMessage(mid, editedText);
|
|
148
|
+
|
|
149
|
+
// Step 4: Wait
|
|
150
|
+
console.log(`\n── Step 4: Ожидание 3 сек... ──`);
|
|
151
|
+
await sleep(3000);
|
|
152
|
+
|
|
153
|
+
// Step 5: Delete
|
|
154
|
+
await deleteMessage(mid);
|
|
155
|
+
|
|
156
|
+
console.log("\n═══ All steps passed! ═══");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((err) => {
|
|
160
|
+
console.error(`\n❌ Fatal: ${err.message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Refresh the MAX Bot API schema snapshot used by schema-conformance.test.ts.
|
|
2
|
+
//
|
|
3
|
+
// npm run schema:update # latest commit of max-messenger/api-schema
|
|
4
|
+
// npm run schema:update -- <ref> # a branch, tag or commit
|
|
5
|
+
//
|
|
6
|
+
// Clones the repository shallowly into a temp dir, writes
|
|
7
|
+
// src/__fixtures__/max-schema-<info.version>.yaml with a source header and
|
|
8
|
+
// removes older snapshots. Run `npm test` afterwards: a failing conformance
|
|
9
|
+
// test means the plugin sends or expects something the new schema changed.
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { parse } from "yaml";
|
|
16
|
+
|
|
17
|
+
const REPO = "https://github.com/max-messenger/api-schema";
|
|
18
|
+
const ref = process.argv[2];
|
|
19
|
+
const fixturesDir = new URL("../src/__fixtures__/", import.meta.url).pathname;
|
|
20
|
+
const work = mkdtempSync(join(tmpdir(), "max-api-schema-"));
|
|
21
|
+
const git = (...args) => execFileSync("git", args, { cwd: work, encoding: "utf8" }).trim();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
git("init", "-q");
|
|
25
|
+
git("remote", "add", "origin", REPO);
|
|
26
|
+
git("fetch", "-q", "--depth", "1", "origin", ref ?? "HEAD");
|
|
27
|
+
git("checkout", "-q", "FETCH_HEAD");
|
|
28
|
+
const commit = git("rev-parse", "HEAD");
|
|
29
|
+
const date = git("show", "-s", "--format=%cd", "--date=format:%d.%m.%Y", "HEAD");
|
|
30
|
+
const yamlText = readFileSync(join(work, "schema.yaml"), "utf8");
|
|
31
|
+
const version = parse(yamlText)?.info?.version;
|
|
32
|
+
if (!version) throw new Error("schema.yaml has no info.version");
|
|
33
|
+
|
|
34
|
+
const target = `max-schema-${version}.yaml`;
|
|
35
|
+
const header = [
|
|
36
|
+
`# Snapshot of the MAX Bot API OpenAPI schema (info.version ${version}).`,
|
|
37
|
+
`# Source: ${REPO}/blob/${commit}/schema.yaml`,
|
|
38
|
+
`# Commit ${commit}, ${date}. Refresh with: npm run schema:update`,
|
|
39
|
+
"",
|
|
40
|
+
].join("\n");
|
|
41
|
+
writeFileSync(join(fixturesDir, target), `${header}\n${yamlText}`);
|
|
42
|
+
for (const name of readdirSync(fixturesDir)) {
|
|
43
|
+
if (/^max-schema-.+\.yaml$/.test(name) && name !== target) unlinkSync(join(fixturesDir, name));
|
|
44
|
+
}
|
|
45
|
+
console.log(`src/__fixtures__/${target} ← ${commit.slice(0, 7)} (${date}); now run npm test`);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(work, { recursive: true, force: true });
|
|
48
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX account resolution — reads config and produces a resolved account object.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
import type { OpenClawConfig } from 'openclaw/plugin-sdk/core';
|
|
8
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from 'openclaw/plugin-sdk/core';
|
|
9
|
+
|
|
10
|
+
export interface MaxAccountConfig {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
botToken?: string;
|
|
13
|
+
tokenFile?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
dmPolicy?: string;
|
|
16
|
+
allowFrom?: Array<string | number>;
|
|
17
|
+
groups?: Record<string, { requireMention?: boolean; [key: string]: unknown }>;
|
|
18
|
+
groupPolicy?: string;
|
|
19
|
+
groupAllowFrom?: Array<string | number>;
|
|
20
|
+
/** Update transport; default: "webhook" when webhookUrl is set, otherwise "polling" */
|
|
21
|
+
transport?: 'polling' | 'webhook';
|
|
22
|
+
webhookUrl?: string;
|
|
23
|
+
webhookSecret?: string;
|
|
24
|
+
/** File holding the webhook secret (like tokenFile) */
|
|
25
|
+
webhookSecretFile?: string;
|
|
26
|
+
webhookPath?: string;
|
|
27
|
+
mediaMaxMb?: number;
|
|
28
|
+
streamMode?: 'off' | 'partial' | 'block';
|
|
29
|
+
/** Send mark_seen read receipts on inbound messages (default true) */
|
|
30
|
+
markSeen?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedMaxAccount {
|
|
34
|
+
accountId: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
token: string;
|
|
38
|
+
tokenSource: 'config' | 'env' | 'file' | 'none';
|
|
39
|
+
config: MaxAccountConfig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the MAX channel section from config.
|
|
44
|
+
*/
|
|
45
|
+
function getMaxSection(cfg: OpenClawConfig): Record<string, unknown> | undefined {
|
|
46
|
+
return (cfg.channels as Record<string, unknown>)?.max as Record<string, unknown> | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read a secret from a regular (non-symlink) file; "" when missing or unreadable. */
|
|
50
|
+
export function readSecretFile(filePath?: string): string {
|
|
51
|
+
return readTokenFile(filePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readTokenFile(tokenFile?: string): string {
|
|
55
|
+
const filePath = tokenFile?.trim();
|
|
56
|
+
if (!filePath) return '';
|
|
57
|
+
try {
|
|
58
|
+
const stat = lstatSync(filePath);
|
|
59
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return '';
|
|
60
|
+
return readFileSync(filePath, 'utf8').trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List all MAX account IDs from config.
|
|
68
|
+
*/
|
|
69
|
+
export function listMaxAccountIds(cfg: OpenClawConfig): string[] {
|
|
70
|
+
const section = getMaxSection(cfg);
|
|
71
|
+
if (!section) return [];
|
|
72
|
+
|
|
73
|
+
const ids: string[] = [];
|
|
74
|
+
// Check for default account (top-level botToken)
|
|
75
|
+
const hasDefault = section.botToken || section.tokenFile || process.env.MAX_BOT_TOKEN;
|
|
76
|
+
if (hasDefault) ids.push(DEFAULT_ACCOUNT_ID);
|
|
77
|
+
|
|
78
|
+
// Check for named accounts
|
|
79
|
+
const accounts = section.accounts as Record<string, unknown> | undefined;
|
|
80
|
+
if (accounts) {
|
|
81
|
+
for (const key of Object.keys(accounts)) {
|
|
82
|
+
const normalized = normalizeAccountId(key);
|
|
83
|
+
if (normalized !== DEFAULT_ACCOUNT_ID && !ids.includes(normalized)) {
|
|
84
|
+
ids.push(normalized);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If section exists but no token sources found, still return default
|
|
90
|
+
if (ids.length === 0 && section.enabled !== false) {
|
|
91
|
+
ids.push(DEFAULT_ACCOUNT_ID);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return ids;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the default account ID.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveDefaultMaxAccountId(_cfg: OpenClawConfig): string {
|
|
101
|
+
return DEFAULT_ACCOUNT_ID;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a single MAX account from config.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveMaxAccount(params: {
|
|
108
|
+
cfg: OpenClawConfig;
|
|
109
|
+
accountId?: string | null;
|
|
110
|
+
}): ResolvedMaxAccount {
|
|
111
|
+
const { cfg, accountId: rawId } = params;
|
|
112
|
+
const accountId = rawId ? normalizeAccountId(rawId) : DEFAULT_ACCOUNT_ID;
|
|
113
|
+
const section = getMaxSection(cfg) ?? {};
|
|
114
|
+
const accounts = section.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
115
|
+
|
|
116
|
+
let accountConfig: MaxAccountConfig;
|
|
117
|
+
let token = '';
|
|
118
|
+
let tokenSource: ResolvedMaxAccount['tokenSource'] = 'none';
|
|
119
|
+
|
|
120
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
121
|
+
// Default account: top-level config
|
|
122
|
+
accountConfig = {
|
|
123
|
+
enabled: section.enabled !== false,
|
|
124
|
+
botToken: section.botToken as string | undefined,
|
|
125
|
+
tokenFile: section.tokenFile as string | undefined,
|
|
126
|
+
name: section.name as string | undefined,
|
|
127
|
+
dmPolicy: section.dmPolicy as string | undefined,
|
|
128
|
+
allowFrom: section.allowFrom as Array<string | number> | undefined,
|
|
129
|
+
groups: section.groups as MaxAccountConfig['groups'],
|
|
130
|
+
groupPolicy: section.groupPolicy as string | undefined,
|
|
131
|
+
groupAllowFrom: section.groupAllowFrom as Array<string | number> | undefined,
|
|
132
|
+
transport: section.transport as MaxAccountConfig['transport'],
|
|
133
|
+
webhookUrl: section.webhookUrl as string | undefined,
|
|
134
|
+
webhookSecret: section.webhookSecret as string | undefined,
|
|
135
|
+
webhookSecretFile: section.webhookSecretFile as string | undefined,
|
|
136
|
+
webhookPath: section.webhookPath as string | undefined,
|
|
137
|
+
mediaMaxMb: section.mediaMaxMb as number | undefined,
|
|
138
|
+
streamMode: section.streamMode as MaxAccountConfig['streamMode'],
|
|
139
|
+
markSeen: section.markSeen as boolean | undefined,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (accountConfig.botToken?.trim()) {
|
|
143
|
+
token = accountConfig.botToken.trim();
|
|
144
|
+
tokenSource = 'config';
|
|
145
|
+
} else if (accountConfig.tokenFile?.trim()) {
|
|
146
|
+
token = readTokenFile(accountConfig.tokenFile);
|
|
147
|
+
tokenSource = token ? 'file' : 'none';
|
|
148
|
+
}
|
|
149
|
+
if (!token && process.env.MAX_BOT_TOKEN?.trim()) {
|
|
150
|
+
token = process.env.MAX_BOT_TOKEN.trim();
|
|
151
|
+
tokenSource = 'env';
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// Named account
|
|
155
|
+
const raw = accounts?.[accountId] ?? {};
|
|
156
|
+
accountConfig = {
|
|
157
|
+
enabled: raw.enabled !== false,
|
|
158
|
+
botToken: raw.botToken as string | undefined,
|
|
159
|
+
tokenFile: raw.tokenFile as string | undefined,
|
|
160
|
+
name: raw.name as string | undefined,
|
|
161
|
+
dmPolicy: raw.dmPolicy as string | undefined,
|
|
162
|
+
allowFrom: raw.allowFrom as Array<string | number> | undefined,
|
|
163
|
+
groups: raw.groups as MaxAccountConfig['groups'],
|
|
164
|
+
groupPolicy: raw.groupPolicy as string | undefined,
|
|
165
|
+
groupAllowFrom: raw.groupAllowFrom as Array<string | number> | undefined,
|
|
166
|
+
transport: raw.transport as MaxAccountConfig['transport'],
|
|
167
|
+
webhookUrl: raw.webhookUrl as string | undefined,
|
|
168
|
+
webhookSecret: raw.webhookSecret as string | undefined,
|
|
169
|
+
webhookSecretFile: raw.webhookSecretFile as string | undefined,
|
|
170
|
+
webhookPath: raw.webhookPath as string | undefined,
|
|
171
|
+
mediaMaxMb: raw.mediaMaxMb as number | undefined,
|
|
172
|
+
streamMode: raw.streamMode as MaxAccountConfig['streamMode'],
|
|
173
|
+
markSeen: raw.markSeen as boolean | undefined,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (accountConfig.botToken?.trim()) {
|
|
177
|
+
token = accountConfig.botToken.trim();
|
|
178
|
+
tokenSource = 'config';
|
|
179
|
+
} else if (accountConfig.tokenFile?.trim()) {
|
|
180
|
+
token = readTokenFile(accountConfig.tokenFile);
|
|
181
|
+
tokenSource = token ? 'file' : 'none';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
accountId,
|
|
187
|
+
name: accountConfig.name,
|
|
188
|
+
enabled: accountConfig.enabled ?? true,
|
|
189
|
+
token,
|
|
190
|
+
tokenSource,
|
|
191
|
+
config: accountConfig,
|
|
192
|
+
};
|
|
193
|
+
}
|