@chrysb/alphaclaw 0.1.25 → 0.2.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/bin/alphaclaw.js +391 -54
- package/lib/public/js/app.js +38 -19
- package/lib/public/js/components/channels.js +14 -3
- package/lib/public/js/components/telegram-workspace.js +1377 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/helpers.js +32 -0
- package/lib/server/onboarding/github.js +1 -2
- package/lib/server/onboarding/index.js +9 -7
- package/lib/server/onboarding/openclaw.js +4 -11
- package/lib/server/onboarding/workspace.js +26 -3
- package/lib/server/routes/auth.js +24 -6
- package/lib/server/routes/proxy.js +4 -4
- package/lib/server/routes/telegram.js +407 -0
- package/lib/server/telegram-api.js +65 -0
- package/lib/server/telegram-workspace.js +82 -0
- package/lib/server/topic-registry.js +152 -0
- package/lib/server.js +7 -1
- package/lib/setup/core-prompts/TOOLS.md +1 -1
- package/lib/setup/env.template +3 -0
- package/lib/setup/hourly-git-sync.sh +9 -10
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const kTelegramApiBase = "https://api.telegram.org";
|
|
2
|
+
|
|
3
|
+
const createTelegramApi = (getToken) => {
|
|
4
|
+
const call = async (method, params = {}) => {
|
|
5
|
+
const token = typeof getToken === "function" ? getToken() : getToken;
|
|
6
|
+
if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set");
|
|
7
|
+
const url = `${kTelegramApiBase}/bot${token}/${method}`;
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify(params),
|
|
12
|
+
});
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
if (!data.ok) {
|
|
15
|
+
const err = new Error(data.description || `Telegram API error: ${method}`);
|
|
16
|
+
err.telegramErrorCode = data.error_code;
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
return data.result;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getMe = () => call("getMe");
|
|
23
|
+
|
|
24
|
+
const getChat = (chatId) => call("getChat", { chat_id: chatId });
|
|
25
|
+
|
|
26
|
+
const getChatMember = (chatId, userId) =>
|
|
27
|
+
call("getChatMember", { chat_id: chatId, user_id: userId });
|
|
28
|
+
|
|
29
|
+
const getChatAdministrators = (chatId) =>
|
|
30
|
+
call("getChatAdministrators", { chat_id: chatId });
|
|
31
|
+
|
|
32
|
+
const createForumTopic = (chatId, name, opts = {}) =>
|
|
33
|
+
call("createForumTopic", {
|
|
34
|
+
chat_id: chatId,
|
|
35
|
+
name,
|
|
36
|
+
...(opts.iconColor != null && { icon_color: opts.iconColor }),
|
|
37
|
+
...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const deleteForumTopic = (chatId, messageThreadId) =>
|
|
41
|
+
call("deleteForumTopic", {
|
|
42
|
+
chat_id: chatId,
|
|
43
|
+
message_thread_id: messageThreadId,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const editForumTopic = (chatId, messageThreadId, opts = {}) =>
|
|
47
|
+
call("editForumTopic", {
|
|
48
|
+
chat_id: chatId,
|
|
49
|
+
message_thread_id: messageThreadId,
|
|
50
|
+
...(opts.name && { name: opts.name }),
|
|
51
|
+
...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
getMe,
|
|
56
|
+
getChat,
|
|
57
|
+
getChatMember,
|
|
58
|
+
getChatAdministrators,
|
|
59
|
+
createForumTopic,
|
|
60
|
+
deleteForumTopic,
|
|
61
|
+
editForumTopic,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = { createTelegramApi };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const kTelegramTopicConcurrencyMultiplier = 3;
|
|
2
|
+
const kAgentConcurrencyFloor = 8;
|
|
3
|
+
const kSubagentConcurrencyFloor = 4;
|
|
4
|
+
|
|
5
|
+
const syncConfigForTelegram = ({
|
|
6
|
+
fs,
|
|
7
|
+
openclawDir,
|
|
8
|
+
topicRegistry,
|
|
9
|
+
groupId,
|
|
10
|
+
requireMention = false,
|
|
11
|
+
resolvedUserId = "",
|
|
12
|
+
}) => {
|
|
13
|
+
const configPath = `${openclawDir}/openclaw.json`;
|
|
14
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
15
|
+
|
|
16
|
+
// Remove legacy root keys from older setup flow.
|
|
17
|
+
delete cfg.sessions;
|
|
18
|
+
delete cfg.groups;
|
|
19
|
+
delete cfg.groupAllowFrom;
|
|
20
|
+
|
|
21
|
+
if (!cfg.channels) cfg.channels = {};
|
|
22
|
+
if (!cfg.channels.telegram) cfg.channels.telegram = {};
|
|
23
|
+
if (!cfg.channels.telegram.groups) cfg.channels.telegram.groups = {};
|
|
24
|
+
const existingGroupConfig = cfg.channels.telegram.groups[groupId] || {};
|
|
25
|
+
cfg.channels.telegram.groups[groupId] = {
|
|
26
|
+
...existingGroupConfig,
|
|
27
|
+
requireMention,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const registryTopics = topicRegistry.getGroup(groupId)?.topics || {};
|
|
31
|
+
const promptTopics = {};
|
|
32
|
+
for (const [threadId, topic] of Object.entries(registryTopics)) {
|
|
33
|
+
const systemPrompt = String(topic?.systemInstructions || "").trim();
|
|
34
|
+
if (!systemPrompt) continue;
|
|
35
|
+
promptTopics[threadId] = { systemPrompt };
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(promptTopics).length > 0) {
|
|
38
|
+
cfg.channels.telegram.groups[groupId].topics = promptTopics;
|
|
39
|
+
} else {
|
|
40
|
+
delete cfg.channels.telegram.groups[groupId].topics;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cfg.channels.telegram.groupPolicy = "allowlist";
|
|
44
|
+
if (!Array.isArray(cfg.channels.telegram.groupAllowFrom)) {
|
|
45
|
+
cfg.channels.telegram.groupAllowFrom = [];
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
resolvedUserId
|
|
49
|
+
&& !cfg.channels.telegram.groupAllowFrom.includes(String(resolvedUserId))
|
|
50
|
+
) {
|
|
51
|
+
cfg.channels.telegram.groupAllowFrom.push(String(resolvedUserId));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Persist thread sessions and keep concurrency in schema-valid agent defaults.
|
|
55
|
+
if (!cfg.session) cfg.session = {};
|
|
56
|
+
if (!cfg.session.resetByType) cfg.session.resetByType = {};
|
|
57
|
+
cfg.session.resetByType.thread = { mode: "idle", idleMinutes: 525600 };
|
|
58
|
+
|
|
59
|
+
const totalTopics = topicRegistry.getTotalTopicCount();
|
|
60
|
+
const maxConcurrent = Math.max(
|
|
61
|
+
totalTopics * kTelegramTopicConcurrencyMultiplier,
|
|
62
|
+
kAgentConcurrencyFloor,
|
|
63
|
+
);
|
|
64
|
+
if (!cfg.agents) cfg.agents = {};
|
|
65
|
+
if (!cfg.agents.defaults) cfg.agents.defaults = {};
|
|
66
|
+
cfg.agents.defaults.maxConcurrent = maxConcurrent;
|
|
67
|
+
if (!cfg.agents.defaults.subagents) cfg.agents.defaults.subagents = {};
|
|
68
|
+
cfg.agents.defaults.subagents.maxConcurrent = Math.max(
|
|
69
|
+
maxConcurrent - 2,
|
|
70
|
+
kSubagentConcurrencyFloor,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
totalTopics,
|
|
77
|
+
maxConcurrent: cfg.agents.defaults.maxConcurrent,
|
|
78
|
+
subagentMaxConcurrent: cfg.agents.defaults.subagents.maxConcurrent,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
module.exports = { syncConfigForTelegram };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { WORKSPACE_DIR } = require("./constants");
|
|
3
|
+
|
|
4
|
+
const kRegistryPath = `${WORKSPACE_DIR}/topic-registry.json`;
|
|
5
|
+
|
|
6
|
+
const readRegistry = () => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(kRegistryPath, "utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
return { groups: {} };
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const writeRegistry = (registry) => {
|
|
15
|
+
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
16
|
+
fs.writeFileSync(kRegistryPath, JSON.stringify(registry, null, 2));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getGroup = (groupId) => {
|
|
20
|
+
const registry = readRegistry();
|
|
21
|
+
return registry.groups[groupId] || null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const setGroup = (groupId, groupData) => {
|
|
25
|
+
const registry = readRegistry();
|
|
26
|
+
const existingGroup = registry.groups[groupId] || {
|
|
27
|
+
name: groupId,
|
|
28
|
+
topics: {},
|
|
29
|
+
};
|
|
30
|
+
registry.groups[groupId] = {
|
|
31
|
+
...existingGroup,
|
|
32
|
+
...groupData,
|
|
33
|
+
topics: existingGroup.topics || {},
|
|
34
|
+
};
|
|
35
|
+
writeRegistry(registry);
|
|
36
|
+
return registry;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const addTopic = (groupId, threadId, topicData) => {
|
|
40
|
+
const registry = readRegistry();
|
|
41
|
+
if (!registry.groups[groupId]) {
|
|
42
|
+
registry.groups[groupId] = { name: groupId, topics: {} };
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
!registry.groups[groupId].topics ||
|
|
46
|
+
typeof registry.groups[groupId].topics !== "object"
|
|
47
|
+
) {
|
|
48
|
+
registry.groups[groupId].topics = {};
|
|
49
|
+
}
|
|
50
|
+
registry.groups[groupId].topics[String(threadId)] = topicData;
|
|
51
|
+
writeRegistry(registry);
|
|
52
|
+
return registry;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const updateTopic = (groupId, threadId, topicData) => {
|
|
56
|
+
const registry = readRegistry();
|
|
57
|
+
if (!registry.groups[groupId]) {
|
|
58
|
+
registry.groups[groupId] = { name: groupId, topics: {} };
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
!registry.groups[groupId].topics ||
|
|
62
|
+
typeof registry.groups[groupId].topics !== "object"
|
|
63
|
+
) {
|
|
64
|
+
registry.groups[groupId].topics = {};
|
|
65
|
+
}
|
|
66
|
+
const existing = registry.groups[groupId].topics[String(threadId)] || {};
|
|
67
|
+
registry.groups[groupId].topics[String(threadId)] = {
|
|
68
|
+
...existing,
|
|
69
|
+
...topicData,
|
|
70
|
+
};
|
|
71
|
+
writeRegistry(registry);
|
|
72
|
+
return registry;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const removeTopic = (groupId, threadId) => {
|
|
76
|
+
const registry = readRegistry();
|
|
77
|
+
if (registry.groups[groupId]?.topics) {
|
|
78
|
+
delete registry.groups[groupId].topics[String(threadId)];
|
|
79
|
+
}
|
|
80
|
+
writeRegistry(registry);
|
|
81
|
+
return registry;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getTotalTopicCount = () => {
|
|
85
|
+
const registry = readRegistry();
|
|
86
|
+
let count = 0;
|
|
87
|
+
for (const group of Object.values(registry.groups)) {
|
|
88
|
+
count += Object.keys(group.topics || {}).length;
|
|
89
|
+
}
|
|
90
|
+
return count;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Render the topic registry as a markdown section for TOOLS.md
|
|
94
|
+
const renderTopicRegistryMarkdown = ({ includeSyncGuidance = false } = {}) => {
|
|
95
|
+
const registry = readRegistry();
|
|
96
|
+
const rows = [];
|
|
97
|
+
for (const [groupId, group] of Object.entries(registry.groups)) {
|
|
98
|
+
for (const [threadId, topic] of Object.entries(group.topics || {})) {
|
|
99
|
+
rows.push({
|
|
100
|
+
groupName: group.name || groupId,
|
|
101
|
+
groupId,
|
|
102
|
+
topicName: topic.name,
|
|
103
|
+
threadId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (rows.length === 0 && !includeSyncGuidance) return "";
|
|
108
|
+
|
|
109
|
+
const lines = [
|
|
110
|
+
"",
|
|
111
|
+
"## Topic Registry",
|
|
112
|
+
"",
|
|
113
|
+
"When sending messages to group topics, use these thread IDs:",
|
|
114
|
+
"",
|
|
115
|
+
"| Group | Topic | Thread ID |",
|
|
116
|
+
"| ----- | ----- | --------- |",
|
|
117
|
+
];
|
|
118
|
+
for (const r of rows) {
|
|
119
|
+
lines.push(
|
|
120
|
+
`| ${r.groupName} (${r.groupId}) | ${r.topicName} | ${r.threadId} |`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (includeSyncGuidance) {
|
|
124
|
+
lines.push(
|
|
125
|
+
"",
|
|
126
|
+
"### Sync Rules",
|
|
127
|
+
"",
|
|
128
|
+
"When Telegram workspace is enabled, keep topic mappings in sync with real Telegram activity:",
|
|
129
|
+
"",
|
|
130
|
+
"- If a message arrives in an unregistered Telegram topic, ask the user to name it for addition to the registry.",
|
|
131
|
+
'- When adding a topic (new or missing) run `alphaclaw telegram topic add --thread <threadId> --name "<topicName>"` immediately, no confirmation needed.',
|
|
132
|
+
"- Never edit `hooks/bootstrap/TOOLS.md` directly for topic changes",
|
|
133
|
+
"",
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
lines.push("");
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
kRegistryPath,
|
|
143
|
+
readRegistry,
|
|
144
|
+
writeRegistry,
|
|
145
|
+
getGroup,
|
|
146
|
+
setGroup,
|
|
147
|
+
addTopic,
|
|
148
|
+
updateTopic,
|
|
149
|
+
removeTopic,
|
|
150
|
+
getTotalTopicCount,
|
|
151
|
+
renderTopicRegistryMarkdown,
|
|
152
|
+
};
|
package/lib/server.js
CHANGED
|
@@ -36,6 +36,7 @@ const { createLoginThrottle } = require("./server/login-throttle");
|
|
|
36
36
|
const { createOpenclawVersionService } = require("./server/openclaw-version");
|
|
37
37
|
const { createAlphaclawVersionService } = require("./server/alphaclaw-version");
|
|
38
38
|
const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
|
|
39
|
+
const { createTelegramApi } = require("./server/telegram-api");
|
|
39
40
|
|
|
40
41
|
const { registerAuthRoutes } = require("./server/routes/auth");
|
|
41
42
|
const { registerPageRoutes } = require("./server/routes/pages");
|
|
@@ -46,6 +47,7 @@ const { registerPairingRoutes } = require("./server/routes/pairings");
|
|
|
46
47
|
const { registerCodexRoutes } = require("./server/routes/codex");
|
|
47
48
|
const { registerGoogleRoutes } = require("./server/routes/google");
|
|
48
49
|
const { registerProxyRoutes } = require("./server/routes/proxy");
|
|
50
|
+
const { registerTelegramRoutes } = require("./server/routes/telegram");
|
|
49
51
|
|
|
50
52
|
const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
|
|
51
53
|
|
|
@@ -146,6 +148,10 @@ registerGoogleRoutes({
|
|
|
146
148
|
getApiEnableUrl,
|
|
147
149
|
constants,
|
|
148
150
|
});
|
|
151
|
+
const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
|
|
152
|
+
const doSyncPromptFiles = () =>
|
|
153
|
+
syncBootstrapPromptFiles({ fs, workspaceDir: constants.WORKSPACE_DIR });
|
|
154
|
+
registerTelegramRoutes({ app, telegramApi, syncPromptFiles: doSyncPromptFiles });
|
|
149
155
|
registerProxyRoutes({ app, proxy, SETUP_API_PREFIXES, requireAuth });
|
|
150
156
|
|
|
151
157
|
const server = http.createServer(app);
|
|
@@ -170,7 +176,7 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
170
176
|
|
|
171
177
|
server.listen(PORT, "0.0.0.0", () => {
|
|
172
178
|
console.log(`[alphaclaw] Express listening on :${PORT}`);
|
|
173
|
-
|
|
179
|
+
doSyncPromptFiles();
|
|
174
180
|
if (isOnboarded()) {
|
|
175
181
|
reloadEnv();
|
|
176
182
|
syncChannelConfig(readEnvFile());
|
|
@@ -25,7 +25,7 @@ Google Workspace is connected via the **General** tab (`{{SETUP_UI_URL}}#general
|
|
|
25
25
|
**Commit and push after every set of changes.** Your entire .openclaw directory (config, cron, workspace) is version controlled. This is how your work survives container restarts.
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
28
|
+
alphaclaw git-sync --message "description"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Never force push. Always pull before pushing if there might be remote changes.
|
package/lib/setup/env.template
CHANGED
|
@@ -4,6 +4,14 @@ set -euo pipefail
|
|
|
4
4
|
REPO="$(cd "$(dirname "$0")" && pwd)"
|
|
5
5
|
cd "$REPO"
|
|
6
6
|
|
|
7
|
+
# Load persisted env vars when running under cron's minimal environment.
|
|
8
|
+
if [[ -f "$REPO/.env" ]]; then
|
|
9
|
+
set -a
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "$REPO/.env"
|
|
12
|
+
set +a
|
|
13
|
+
fi
|
|
14
|
+
|
|
7
15
|
# Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
|
|
8
16
|
maybe_restore_if_runtime_only() {
|
|
9
17
|
local file="$1"
|
|
@@ -73,14 +81,5 @@ NODE
|
|
|
73
81
|
maybe_restore_if_runtime_only "cron/jobs.json"
|
|
74
82
|
maybe_restore_if_runtime_only "crons.json"
|
|
75
83
|
|
|
76
|
-
# Stage everything else.
|
|
77
|
-
git add -A
|
|
78
|
-
|
|
79
|
-
# Nothing to commit? done.
|
|
80
|
-
if git diff --cached --quiet; then
|
|
81
|
-
exit 0
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
84
|
msg="Auto-commit hourly sync $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
85
|
-
git
|
|
86
|
-
git push
|
|
85
|
+
alphaclaw git-sync -m "$msg"
|