grok-telegram-bot 2.3.1 → 2.5.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/.env.example +64 -2
- package/CHANGELOG.md +156 -1
- package/README.md +58 -15
- package/docs/GROUP.md +225 -0
- package/docs/INSTALL.md +3 -0
- package/package.json +1 -1
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/lifetime-flag.ts +20 -0
- package/src/app/settings-store.ts +47 -8
- package/src/app/types.ts +30 -2
- package/src/app/updater.ts +38 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +10 -0
- package/src/bot/auth.ts +96 -15
- package/src/bot/bot.ts +154 -11
- package/src/bot/chat-controller.ts +82 -13
- package/src/bot/commands.ts +69 -27
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +22 -0
- package/src/bot/group-memory.ts +159 -0
- package/src/bot/handlers/accounts.ts +58 -1
- package/src/bot/handlers/control.ts +85 -32
- package/src/bot/handlers/document.ts +31 -4
- package/src/bot/handlers/forum.ts +207 -0
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +102 -61
- package/src/bot/handlers/message.ts +102 -21
- package/src/bot/handlers/photo.ts +123 -16
- package/src/bot/handlers/running.ts +172 -16
- package/src/bot/handlers/session-card.ts +20 -0
- package/src/bot/handlers/sessions.ts +76 -15
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/handlers/voice.ts +52 -7
- package/src/bot/image-return.ts +8 -5
- package/src/bot/menu/ephemeral.ts +13 -3
- package/src/bot/menu/keyboard.ts +54 -14
- package/src/bot/menu/refresh.ts +3 -1
- package/src/bot/menu/status-panel.ts +25 -6
- package/src/bot/permission-service.ts +19 -0
- package/src/bot/prompt-anchor.ts +300 -0
- package/src/bot/prompt-content.ts +7 -0
- package/src/bot/registry.ts +94 -1
- package/src/bot/scope.ts +94 -0
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +1254 -83
- package/src/bot/suggestions.ts +489 -0
- package/src/bot/telegram-actions.ts +440 -0
- package/src/bot/telegram-bots.ts +495 -0
- package/src/bot/telegram-io.ts +94 -10
- package/src/cli.ts +2 -0
- package/src/config.ts +242 -2
- package/src/forum/bind-path.ts +146 -0
- package/src/forum/manager.ts +651 -0
- package/src/forum/project-icon.ts +142 -0
- package/src/forum/thread.ts +16 -0
- package/src/forum/topic-store.ts +114 -0
- package/src/forum/types.ts +29 -0
- package/src/grok/client.ts +214 -37
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +315 -30
- package/src/projects/manager.ts +16 -3
- package/src/render/chunk.ts +17 -10
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/hashtags.ts +5 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +318 -0
- package/src/render/telegram-bridge.ts +360 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +444 -162
- package/src/render/truncate.ts +85 -0
- package/src/service/platform.ts +44 -7
- package/src/service/windows.ts +30 -6
- package/src/sessions/history.ts +98 -0
- package/src/sessions/process.ts +7 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +90 -15
- package/src/tasks/runner.ts +4 -3
|
@@ -1,28 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Control commands: /start /help /status /new /cancel /btw /flush.
|
|
2
|
+
* Control commands: /start /help /status /new /cancel /stop /btw /flush /menu.
|
|
3
|
+
*
|
|
4
|
+
* Slash messages are deleted instantly by bot.ts middleware; handlers post
|
|
5
|
+
* bot status messages so the user always sees the bot is alive (CLI can be slow).
|
|
3
6
|
*/
|
|
4
|
-
import type { Bot } from "grammy";
|
|
7
|
+
import type { Bot, Context } from "grammy";
|
|
5
8
|
import { basename } from "node:path";
|
|
6
9
|
import { textPrompt } from "../../app/types.js";
|
|
7
10
|
import type { BotDeps } from "../deps.js";
|
|
8
11
|
import { HELP_TEXT } from "../commands.js";
|
|
9
12
|
import { compactKeyboard } from "../menu/keyboard.js";
|
|
10
13
|
import { refreshMenu } from "../menu/refresh.js";
|
|
14
|
+
import { adoptUserPrompt } from "../prompt-anchor.js";
|
|
11
15
|
import { extractReplyContext } from "../reply-context.js";
|
|
16
|
+
import { resolveScope } from "../scope.js";
|
|
12
17
|
import { openMainMenu } from "./menu.js";
|
|
13
18
|
|
|
14
19
|
export function registerControl(bot: Bot, deps: BotDeps): void {
|
|
15
20
|
bot.command("start", async (ctx) => {
|
|
16
|
-
const
|
|
21
|
+
const scope = resolveScope(ctx, deps);
|
|
17
22
|
const agent = deps.acp.agentInfo;
|
|
23
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
18
24
|
const lines = [
|
|
19
25
|
"\u{1F44B} Welcome! I bridge Telegram to Grok Build over ACP.",
|
|
20
26
|
agent?.name ? `Connected to ${agent.name} ${agent.version ?? ""}`.trim() : "",
|
|
21
27
|
"",
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
isGroup || scope.isForum
|
|
29
|
+
? "In groups / topics: /menu for controls, /cancel or /stop to halt a turn."
|
|
30
|
+
: "Bar: \u2630 Menu \u00B7 \u{1F195} New session \u00B7 \u{1F9ED} Running \u00B7 \u23F9 Stop. Live status panel while I work",
|
|
31
|
+
isGroup || scope.isForum
|
|
32
|
+
? "Just send a message in this topic to start."
|
|
33
|
+
: "(\u2630 Menu \u2192 Status shows it anytime). Just send a message to start.",
|
|
24
34
|
].filter(Boolean);
|
|
25
|
-
await ctx.reply(lines.join("\n"), {
|
|
35
|
+
await ctx.reply(lines.join("\n"), {
|
|
36
|
+
reply_markup: compactKeyboard(),
|
|
37
|
+
...scope.threadExtra,
|
|
38
|
+
});
|
|
26
39
|
await deps.statusPanel.refresh(ctx.chat.id);
|
|
27
40
|
});
|
|
28
41
|
|
|
@@ -32,71 +45,111 @@ export function registerControl(bot: Bot, deps: BotDeps): void {
|
|
|
32
45
|
});
|
|
33
46
|
|
|
34
47
|
bot.command("help", async (ctx) => {
|
|
35
|
-
|
|
48
|
+
const scope = resolveScope(ctx, deps);
|
|
49
|
+
await ctx.reply(HELP_TEXT, scope.threadExtra);
|
|
36
50
|
});
|
|
37
51
|
|
|
38
52
|
bot.command("status", async (ctx) => {
|
|
39
|
-
const
|
|
53
|
+
const scope = resolveScope(ctx, deps);
|
|
54
|
+
const rt = scope.rt;
|
|
40
55
|
const lines = [
|
|
41
56
|
"\u{1F4CA} Status",
|
|
57
|
+
scope.isForum ? `Topic: ${scope.projectName ?? "topic"}` : "",
|
|
42
58
|
`Project: ${rt.projectName ?? (basename(rt.cwd) || rt.cwd)}`,
|
|
43
59
|
`Folder: ${rt.cwd}`,
|
|
44
60
|
`Session: ${rt.sessionId ?? "(none yet)"}`,
|
|
61
|
+
`Model: ${rt.model || "default"}`,
|
|
62
|
+
`Reasoning: ${rt.reasoning}`,
|
|
45
63
|
`State: ${rt.isBusy ? "\u23F3 working" : "\u2705 idle"}`,
|
|
46
64
|
`Queued follow-ups: ${rt.queueLength}`,
|
|
47
|
-
];
|
|
65
|
+
].filter(Boolean);
|
|
48
66
|
const subagents = deps.registry.subagentSummaryForChat(ctx.chat.id);
|
|
49
67
|
if (subagents) lines.push(`Subagents: ${subagents}`);
|
|
50
|
-
await ctx.reply(lines.join("\n"));
|
|
68
|
+
await ctx.reply(lines.join("\n"), scope.threadExtra);
|
|
51
69
|
});
|
|
52
70
|
|
|
53
71
|
bot.command("new", async (ctx) => {
|
|
54
|
-
const
|
|
72
|
+
const scope = resolveScope(ctx, deps);
|
|
73
|
+
// Instant feedback before ACP session/new (can be slow on cold CLI).
|
|
74
|
+
// Same copy as bar 🆕 New and inline m:new.
|
|
75
|
+
await ctx.reply("\u2728 Creating new session\u2026", scope.threadExtra).catch(() => {});
|
|
55
76
|
try {
|
|
56
|
-
await
|
|
57
|
-
await refreshMenu(ctx, deps, `\u2728 New session
|
|
77
|
+
await scope.controller.addNew(scope.rt.cwd, scope.rt.projectName);
|
|
78
|
+
await refreshMenu(ctx, deps, `\u2728 New session in ${scope.rt.projectName ?? scope.rt.cwd}`);
|
|
58
79
|
} catch (err) {
|
|
59
|
-
await ctx.reply(`\u274C
|
|
80
|
+
await ctx.reply(`\u274C ${(err as Error).message}`, scope.threadExtra);
|
|
60
81
|
}
|
|
61
82
|
});
|
|
62
83
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const cancelled = await rt.cancel();
|
|
66
|
-
await ctx.reply(
|
|
67
|
-
|
|
84
|
+
const cancelTurn = async (ctx: Context): Promise<void> => {
|
|
85
|
+
const scope = resolveScope(ctx, deps);
|
|
86
|
+
const cancelled = await scope.rt.cancel();
|
|
87
|
+
await ctx.reply(
|
|
88
|
+
cancelled ? "\u23F9 Cancelling current turn\u2026" : "Nothing is running.",
|
|
89
|
+
scope.threadExtra,
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
bot.command("cancel", cancelTurn);
|
|
94
|
+
bot.command("stop", cancelTurn);
|
|
68
95
|
|
|
69
96
|
bot.command("btw", async (ctx) => {
|
|
70
97
|
const text = (ctx.match || "").toString().trim();
|
|
71
98
|
if (!text) {
|
|
72
|
-
|
|
99
|
+
const scope = resolveScope(ctx, deps);
|
|
100
|
+
await ctx.reply(
|
|
101
|
+
"Usage: /btw <something for the agent to do — now if idle, otherwise next>",
|
|
102
|
+
scope.threadExtra,
|
|
103
|
+
);
|
|
73
104
|
return;
|
|
74
105
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
const scope = resolveScope(ctx, deps);
|
|
107
|
+
const userMsgId = ctx.message?.message_id;
|
|
108
|
+
const anchor = await adoptUserPrompt(deps.api, {
|
|
109
|
+
chatId: ctx.chat.id,
|
|
110
|
+
text,
|
|
111
|
+
// Command middleware already deleted the slash message; re-delete is a no-op.
|
|
112
|
+
userMessageIds: userMsgId !== undefined ? [userMsgId] : [],
|
|
113
|
+
messageThreadId: scope.threadExtra.message_thread_id,
|
|
114
|
+
projectName: scope.rt.projectName,
|
|
115
|
+
prefix: "\u{1F4DD} /btw",
|
|
116
|
+
});
|
|
117
|
+
// Fall back to userMsgId if anchor send failed (message may already be gone).
|
|
118
|
+
const outcome = await scope.rt.submit(
|
|
119
|
+
textPrompt(text, anchor?.replyTo ?? userMsgId, extractReplyContext(ctx), {
|
|
120
|
+
promptId: anchor?.promptId,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
79
123
|
if (outcome === "queued") {
|
|
124
|
+
const extra: Record<string, unknown> = { ...scope.threadExtra };
|
|
125
|
+
if (anchor?.replyTo !== undefined) {
|
|
126
|
+
extra.reply_parameters = {
|
|
127
|
+
message_id: anchor.replyTo,
|
|
128
|
+
allow_sending_without_reply: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
80
131
|
await ctx.reply(
|
|
81
|
-
`\u{1F4E5} Queued (position ${rt.queueLength}) \u2014 it'll run automatically as soon as the current task finishes.`,
|
|
132
|
+
`\u{1F4E5} Queued (position ${scope.rt.queueLength}) \u2014 it'll run automatically as soon as the current task finishes.`,
|
|
133
|
+
extra,
|
|
82
134
|
);
|
|
83
|
-
} else {
|
|
84
|
-
await ctx.reply("\u25B6\uFE0F On it\u2026");
|
|
85
135
|
}
|
|
86
136
|
});
|
|
87
137
|
|
|
88
138
|
bot.command("flush", async (ctx) => {
|
|
89
|
-
const
|
|
139
|
+
const scope = resolveScope(ctx, deps);
|
|
140
|
+
const rt = scope.rt;
|
|
90
141
|
if (rt.queueLength === 0) {
|
|
91
|
-
await ctx.reply("Queue is empty.");
|
|
142
|
+
await ctx.reply("Queue is empty.", scope.threadExtra);
|
|
92
143
|
return;
|
|
93
144
|
}
|
|
94
145
|
if (rt.isBusy) {
|
|
95
|
-
await ctx.reply(
|
|
146
|
+
await ctx.reply(
|
|
147
|
+
`\u23F3 ${rt.queueLength} queued \u2014 they'll run automatically when the current turn ends.`,
|
|
148
|
+
scope.threadExtra,
|
|
149
|
+
);
|
|
96
150
|
return;
|
|
97
151
|
}
|
|
98
|
-
|
|
99
|
-
await ctx.reply("\u25B6\uFE0F Running queued follow-ups\u2026");
|
|
152
|
+
await ctx.reply("\u25B6\uFE0F Running queued follow-ups\u2026", scope.threadExtra);
|
|
100
153
|
const drained = rt.drainQueueToPrompt();
|
|
101
154
|
if (drained) await rt.submit(drained);
|
|
102
155
|
});
|
|
@@ -57,7 +57,7 @@ export function registerDocuments(bot: Bot, deps: BotDeps): void {
|
|
|
57
57
|
|
|
58
58
|
const caption = ctx.message.caption ?? "";
|
|
59
59
|
const quoted = extractReplyContext(ctx);
|
|
60
|
-
const
|
|
60
|
+
const userMsgId = ctx.message.message_id;
|
|
61
61
|
|
|
62
62
|
let promptText: string;
|
|
63
63
|
if (looksLikeText(buf, doc.mime_type, name)) {
|
|
@@ -75,10 +75,37 @@ export function registerDocuments(bot: Bot, deps: BotDeps): void {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
78
|
+
const { resolveScope } = await import("../scope.js");
|
|
79
|
+
const { adoptUserPrompt } = await import("../prompt-anchor.js");
|
|
80
|
+
const scope = resolveScope(ctx, deps);
|
|
81
|
+
const anchorPreview = caption.trim() || `File: ${name}`;
|
|
82
|
+
const anchor = await adoptUserPrompt(deps.api, {
|
|
83
|
+
chatId,
|
|
84
|
+
text: anchorPreview,
|
|
85
|
+
userMessageIds: [userMsgId],
|
|
86
|
+
messageThreadId: scope.threadExtra.message_thread_id,
|
|
87
|
+
projectName: scope.rt.projectName,
|
|
88
|
+
prefix: `\u{1F4CE} ${name}`,
|
|
89
|
+
// Re-post the file so it stays in chat after the user message is deleted.
|
|
90
|
+
media: [{ type: "document", fileId: doc.file_id, fileName: name }],
|
|
91
|
+
});
|
|
92
|
+
const outcome = await scope.rt.submit(
|
|
93
|
+
textPrompt(promptText, anchor?.replyTo ?? userMsgId, quoted, {
|
|
94
|
+
promptId: anchor?.promptId,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
80
97
|
if (outcome === "queued") {
|
|
81
|
-
|
|
98
|
+
const extra: Record<string, unknown> = { ...scope.threadExtra };
|
|
99
|
+
if (anchor?.replyTo !== undefined) {
|
|
100
|
+
extra.reply_parameters = {
|
|
101
|
+
message_id: anchor.replyTo,
|
|
102
|
+
allow_sending_without_reply: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
await ctx.reply(
|
|
106
|
+
`\u{1F4E5} Queued "${name}" \u2014 will run after the current task.`,
|
|
107
|
+
extra,
|
|
108
|
+
);
|
|
82
109
|
}
|
|
83
110
|
} catch (e) {
|
|
84
111
|
log.warn(`submit failed for "${name}":`, (e as Error).message);
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forum topic group: auto-setup, user-created topic binding, and path prompts.
|
|
3
|
+
*/
|
|
4
|
+
import type { Bot } from "grammy";
|
|
5
|
+
import { createLogger } from "../../logger.js";
|
|
6
|
+
import type { ForumManager } from "../../forum/manager.js";
|
|
7
|
+
import type { BotDeps } from "../deps.js";
|
|
8
|
+
import { FORUM_GENERAL_THREAD_ID, forumThreadId } from "../../forum/thread.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger("forum-handler");
|
|
11
|
+
|
|
12
|
+
const BIND_HINT =
|
|
13
|
+
`Send the **absolute project path** or an **exact** catalog project name to bind this topic.\n` +
|
|
14
|
+
`Example: \`H:\\\\Lucru\\\\Domains\\\\MyApp\``;
|
|
15
|
+
|
|
16
|
+
export function registerForum(bot: Bot, deps: BotDeps, forum: ForumManager): void {
|
|
17
|
+
const groupId = forum.groupId;
|
|
18
|
+
|
|
19
|
+
// Service message: user (or another admin) created a topic.
|
|
20
|
+
bot.on("message:forum_topic_created", async (ctx) => {
|
|
21
|
+
if (ctx.chat?.id !== groupId) return;
|
|
22
|
+
// Group ignored (not admin / no Topics / probe failed).
|
|
23
|
+
if (!forum.isReady) {
|
|
24
|
+
log.debug(`ignore forum_topic_created — forum not ready (${forum.getStatusText()})`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const created = ctx.message.forum_topic_created;
|
|
28
|
+
const threadId = ctx.message.message_thread_id;
|
|
29
|
+
if (!created || threadId === undefined) return;
|
|
30
|
+
// Skip General — always workspace-bound.
|
|
31
|
+
if (threadId === FORUM_GENERAL_THREAD_ID) return;
|
|
32
|
+
|
|
33
|
+
forum.noteUserTopic(threadId, created.name);
|
|
34
|
+
log.info(`user topic created: ${created.name} (#${threadId})`);
|
|
35
|
+
|
|
36
|
+
// Exact catalog name → bind immediately (no message required).
|
|
37
|
+
const auto = await forum.tryAutoBindByTopicName(threadId, created.name);
|
|
38
|
+
if (auto.status === "bound") {
|
|
39
|
+
await ctx
|
|
40
|
+
.reply(
|
|
41
|
+
`\u2705 Topic **${created.name}** auto-bound to project:\n\`${auto.binding.projectPath}\`\n\nYou can chat here now.`,
|
|
42
|
+
{ parse_mode: "Markdown", message_thread_id: threadId },
|
|
43
|
+
)
|
|
44
|
+
.catch(() => {});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (auto.status === "already_bound") {
|
|
49
|
+
await ctx
|
|
50
|
+
.reply(
|
|
51
|
+
`\u{1F4CC} New topic **${created.name}**.\n\n` +
|
|
52
|
+
`Exact catalog match \`${auto.projectPath}\` is already bound to topic #${auto.otherThreadId}.\n` +
|
|
53
|
+
BIND_HINT,
|
|
54
|
+
{ parse_mode: "Markdown", message_thread_id: threadId },
|
|
55
|
+
)
|
|
56
|
+
.catch(() => {});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await ctx
|
|
61
|
+
.reply(
|
|
62
|
+
`\u{1F4CC} New topic **${created.name}**.\n\n` +
|
|
63
|
+
`No exact catalog project matched this name.\n` +
|
|
64
|
+
BIND_HINT,
|
|
65
|
+
{ parse_mode: "Markdown", message_thread_id: threadId },
|
|
66
|
+
)
|
|
67
|
+
.catch(() => {});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Re-probe when the bot is promoted/demoted in the configured group.
|
|
71
|
+
bot.on("my_chat_member", async (ctx) => {
|
|
72
|
+
if (ctx.chat?.id !== groupId) return;
|
|
73
|
+
const status = ctx.myChatMember.new_chat_member.status;
|
|
74
|
+
if (status === "administrator" || status === "creator") {
|
|
75
|
+
log.info(`bot became ${status} in topic group — re-running forum setup`);
|
|
76
|
+
void forum.ensureSetup().catch((e) => {
|
|
77
|
+
log.warn(`forum re-setup after promotion failed: ${(e as Error).message}`);
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (status === "member" || status === "restricted" || status === "left" || status === "kicked") {
|
|
82
|
+
forum.markDisabled(
|
|
83
|
+
"not_admin",
|
|
84
|
+
`bot status is now "${status}" — forum topic management ignored until the bot is admin again`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Optional: re-run setup command for admins in the group.
|
|
90
|
+
bot.command("forum_setup", async (ctx) => {
|
|
91
|
+
const threadId = ctx.message?.message_thread_id;
|
|
92
|
+
const replyOpts =
|
|
93
|
+
threadId !== undefined ? { message_thread_id: threadId } : {};
|
|
94
|
+
if (ctx.chat?.id !== groupId) {
|
|
95
|
+
await ctx.reply("Use this command inside the configured forum group.", replyOpts).catch(() => {});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await ctx
|
|
99
|
+
.reply(
|
|
100
|
+
"Setting up forum topics (this can take a while for large catalogs)…",
|
|
101
|
+
replyOpts,
|
|
102
|
+
)
|
|
103
|
+
.catch(() => {});
|
|
104
|
+
try {
|
|
105
|
+
await forum.ensureSetup();
|
|
106
|
+
if (!forum.isReady) {
|
|
107
|
+
await ctx
|
|
108
|
+
.reply(
|
|
109
|
+
`\u26A0\uFE0F Forum setup skipped / group ignored.\n${forum.getStatusText()}`,
|
|
110
|
+
replyOpts,
|
|
111
|
+
)
|
|
112
|
+
.catch(() => {});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const n = forum.store.all().length;
|
|
116
|
+
await ctx
|
|
117
|
+
.reply(`\u2705 Forum setup done. ${n} topic(s) mapped.\n${forum.getStatusText()}`, replyOpts)
|
|
118
|
+
.catch(() => {});
|
|
119
|
+
} catch (e) {
|
|
120
|
+
await ctx.reply(`Setup failed: ${(e as Error).message}`, replyOpts).catch(() => {});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
void deps;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a forum-group text message to a SessionRuntime, or handle bind flow.
|
|
129
|
+
* Returns "handled" when the message was consumed (bind ask / bind result).
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveForumRuntime(
|
|
132
|
+
deps: BotDeps,
|
|
133
|
+
forum: ForumManager,
|
|
134
|
+
chatId: number,
|
|
135
|
+
threadId: number | undefined,
|
|
136
|
+
text: string,
|
|
137
|
+
messageId: number,
|
|
138
|
+
): Promise<{ rt: import("../session-runtime.js").SessionRuntime } | "handled" | "ignore"> {
|
|
139
|
+
if (chatId !== forum.groupId) return "ignore";
|
|
140
|
+
// Not admin / Topics off / probe failed — do not steal group messages.
|
|
141
|
+
if (!forum.isReady) return "ignore";
|
|
142
|
+
const tid = forumThreadId(threadId);
|
|
143
|
+
|
|
144
|
+
// General topic → always workspace (no path prompt).
|
|
145
|
+
if (tid === FORUM_GENERAL_THREAD_ID) {
|
|
146
|
+
const cwd = deps.cfg.workspace;
|
|
147
|
+
if (!forum.store.get(tid)?.projectPath) {
|
|
148
|
+
forum.store.bindProject(tid, cwd, "General", "general");
|
|
149
|
+
}
|
|
150
|
+
const rt = deps.registry.getForumTopic(chatId, tid, cwd, "General");
|
|
151
|
+
return { rt };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ensure we know about this thread.
|
|
155
|
+
let binding = forum.store.get(tid);
|
|
156
|
+
if (!binding) {
|
|
157
|
+
binding = forum.noteUserTopic(tid, `Topic ${tid}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Unbound only: try to interpret text as path / exact catalog name (do not
|
|
161
|
+
// steal normal prompts when already bound — even if pending flag is stale).
|
|
162
|
+
if (!binding.projectPath) {
|
|
163
|
+
const result = forum.tryBindPath(tid, text);
|
|
164
|
+
if (result.ok) {
|
|
165
|
+
const iconNote = result.binding.iconPath ? `\nIcon: ${result.binding.iconPath}` : "";
|
|
166
|
+
await deps.api
|
|
167
|
+
.sendMessage(
|
|
168
|
+
chatId,
|
|
169
|
+
`\u2705 Bound to project:\n\`${result.binding.projectPath}\`${iconNote}\n\nYou can chat here now.`,
|
|
170
|
+
{ message_thread_id: tid, parse_mode: "Markdown", reply_parameters: { message_id: messageId } },
|
|
171
|
+
)
|
|
172
|
+
.catch(() => {});
|
|
173
|
+
// Warm runtime; path-only message is not submitted as an agent prompt.
|
|
174
|
+
const resolved = forum.resolveCwd(tid);
|
|
175
|
+
if (resolved) {
|
|
176
|
+
deps.registry.getForumTopic(chatId, tid, resolved.cwd, resolved.projectName);
|
|
177
|
+
}
|
|
178
|
+
return "handled";
|
|
179
|
+
}
|
|
180
|
+
await deps.api
|
|
181
|
+
.sendMessage(
|
|
182
|
+
chatId,
|
|
183
|
+
`\u2753 ${result.error}\n\n${BIND_HINT.replace(/\*\*/g, "")}`,
|
|
184
|
+
{ message_thread_id: tid, reply_parameters: { message_id: messageId } },
|
|
185
|
+
)
|
|
186
|
+
.catch(() => {});
|
|
187
|
+
return "handled";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resolved = forum.resolveCwd(tid);
|
|
191
|
+
if (!resolved) {
|
|
192
|
+
forum.store.markPending(tid);
|
|
193
|
+
await deps.api
|
|
194
|
+
.sendMessage(
|
|
195
|
+
chatId,
|
|
196
|
+
`\u2753 This topic is not linked to a project yet.\n${BIND_HINT.replace(/\*\*/g, "")}`,
|
|
197
|
+
{ message_thread_id: tid },
|
|
198
|
+
)
|
|
199
|
+
.catch(() => {});
|
|
200
|
+
return "handled";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Multi-session controller for this topic (own model/reasoning/running list).
|
|
204
|
+
// First message with no sessionId creates one via ensureSession on submit.
|
|
205
|
+
const controller = deps.registry.forumController(chatId, tid, resolved.cwd, resolved.projectName);
|
|
206
|
+
return { rt: controller.foreground() };
|
|
207
|
+
}
|