1 Commits
3 changed files with 41 additions and 6 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "telegram-codex-bot",
"version": "0.1.13",
"version": "0.1.14",
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
"license": "MIT",
"type": "commonjs",
+40 -5
View File
@@ -215,6 +215,32 @@ function startBot(config) {
return message.quote?.text || message.reply_to_message?.text || message.reply_to_message?.caption || '';
}
// Telegram 限制:bot 永遠看不到其他 bot 的訊息——使用者「回覆另一隻 bot + tag 我」時,
// reply_to_message 會被整個剝掉。繞法:每隻 bot 把自己在各聊天室的最後一則回覆
// 寫進共用資料夾,收到「只有 tag 沒內容」時就拿同聊天室最近一則兄弟 bot 的發言當輸入。
const CHATLAST_DIR = path.join(os.homedir(), '.tgcodex', 'chatlast');
const SIBLING_MAX_AGE_MS = 6 * 60 * 60 * 1000; // 超過 6 小時的舊訊息就不當引用
function recordChatLast(chatId, text) {
try {
fs.mkdirSync(CHATLAST_DIR, { recursive: true });
fs.writeFileSync(
path.join(CHATLAST_DIR, `${chatId}.json`),
JSON.stringify({ botUsername, text, ts: Date.now() }) + '\n'
);
} catch { /* 純輔助功能,失敗不影響回覆 */ }
}
function siblingLastOf(chatId) {
try {
const j = JSON.parse(fs.readFileSync(path.join(CHATLAST_DIR, `${chatId}.json`), 'utf-8'));
if (!j.text || Date.now() - j.ts > SIBLING_MAX_AGE_MS) return null;
return j;
} catch {
return null;
}
}
function buildPrompt(message, content, { includeOwnReply = false } = {}) {
const from = message.from || {};
const name = [from.first_name, from.last_name].filter(Boolean).join(' ') || '未知使用者';
@@ -297,8 +323,11 @@ function startBot(config) {
await t.sendMessage(chatId, statusText(chatId), { replyTo: message.message_id });
return;
}
// 只有「引用/回覆 + tag 我」沒打字:直接把引用內容當作這次的輸入
const quotedOnly = !content && !photo && !!quotedTextOf(message);
// 只有「引用/回覆 + tag 我」沒打字:直接把引用內容當作這次的輸入
// 引用的是其他 bot 的訊息時 Telegram 不給內容,改拿共用記錄裡同聊天室最近一則 bot 發言。
const quoted = quotedTextOf(message);
const sibling = (!content && !photo && !quoted) ? siblingLastOf(chatId) : null;
const quotedOnly = !content && !photo && !!(quoted || sibling);
if (!content && !photo && !quotedOnly) {
await t.sendMessage(chatId, HELP_TEXT, { replyTo: message.message_id });
return;
@@ -332,9 +361,14 @@ function startBot(config) {
images.push(dest);
}
const fallback = quotedOnly
? '(使用者沒有輸入文字,只引用了上面的內容並 tag 我:請直接把引用內容當作這次的輸入來處理)'
: '(使用者只傳了圖片,請描述並依上下文處理)';
let fallback = '(使用者只傳了圖片,請描述並依上下文處理)';
if (quotedOnly && (quoted || !sibling)) {
fallback = '(使用者沒有輸入文字,只引用了上面的內容並 tag 我:請直接把引用內容當作這次的輸入來處理)';
} else if (quotedOnly && sibling) {
fallback =
`【引用的 bot 訊息(@${sibling.botUsername} 在本聊天室的最近發言)】\n${sibling.text}\n\n` +
'(使用者沒有輸入文字,只引用了訊息並 tag 我:請直接把上面引用的內容當作這次的輸入來處理)';
}
const prompt = buildPrompt(message, content || fallback, { includeOwnReply: quotedOnly });
const existing = sessionKey ? sessions.get(sessionKey) : null;
const turnStart = Date.now();
@@ -400,6 +434,7 @@ function startBot(config) {
const footer = buildFooter(config, result.usage, modelMeta, codexDefaults);
const html = t.mdToTgHtml(sessionResetNote + (result.text || 'Codex 沒有回覆文字)') + ctxNote + footer);
await t.editOrSplit(chatId, statusMsg.message_id, html, { html: true });
recordChatLast(chatId, result.text || '');
await flushOutputs(chatId, message.message_id, result.threadId, turnStart);
await t.react(chatId, message.message_id, '👍');
} catch (error) {