發佈 0.1.9:imagegen 產圖保險網,漏複製也會傳到 Telegram
根本原因: codex 的 imagegen 技能預設把圖存在 ~/.codex/generated_images/<會話id>/, 是否複製進 .tgcodex-outbox/ 全靠模型遵守 prompt 規則,偶爾會漏, 導致 bot 回「畫好了」但沒附圖。 修法: 每輪結束除了交件匣,另掃描該會話的 generated_images 目錄,把本輪 (mtime 在輪次時間窗內)新生成的圖一併傳出;以檔案內容 MD5 去重, 交件匣有複製過的不會重複傳。generated_images 原檔保留(後續編輯可能要用)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram-codex-bot",
|
"name": "telegram-codex-bot",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
|
"description": "Telegram bot powered by the local Codex CLI — zero-dependency, pm2-managed, with a built-in PM2 web viewer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
+68
-19
@@ -55,8 +55,40 @@ function buildFooter(config, usage, modelMeta, codexDefaults) {
|
|||||||
return `\n\n${parts.join(' | ')}`;
|
return `\n\n${parts.join(' | ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
// Codex 要傳檔案給使用者的交件匣(在工作目錄底下,沙盒內可寫)
|
// Codex 要傳檔案給使用者的交件匣(在工作目錄底下,沙盒內可寫)
|
||||||
const OUTBOX_DIRNAME = '.tgcodex-outbox';
|
const OUTBOX_DIRNAME = '.tgcodex-outbox';
|
||||||
|
// codex 內建 imagegen 技能的預設輸出位置(依會話 id 分資料夾)
|
||||||
|
const GENERATED_IMAGES_DIR = path.join(require('os').homedir(), '.codex', 'generated_images');
|
||||||
|
|
||||||
|
// 這一輪 codex 用 imagegen 生成、但沒複製進交件匣的圖(模型偶爾會忘)
|
||||||
|
function collectGeneratedImages(threadId, sinceMs) {
|
||||||
|
if (!threadId) return [];
|
||||||
|
const dir = path.join(GENERATED_IMAGES_DIR, String(threadId));
|
||||||
|
try {
|
||||||
|
return fs.readdirSync(dir)
|
||||||
|
.filter((f) => /\.(png|jpe?g|gif|webp)$/i.test(f))
|
||||||
|
.map((f) => path.join(dir, f))
|
||||||
|
.filter((p) => {
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(p);
|
||||||
|
return st.isFile() && st.mtimeMs >= sinceMs;
|
||||||
|
} catch { return false; }
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5File(p) {
|
||||||
|
try {
|
||||||
|
return crypto.createHash('md5').update(fs.readFileSync(p)).digest('hex');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const OUTBOX_RULE =
|
const OUTBOX_RULE =
|
||||||
`【系統規則】若要把圖片或檔案傳給使用者,請將檔案寫入工作目錄下的 ${OUTBOX_DIRNAME}/ 資料夾,` +
|
`【系統規則】若要把圖片或檔案傳給使用者,請將檔案寫入工作目錄下的 ${OUTBOX_DIRNAME}/ 資料夾,` +
|
||||||
'bot 會在回覆後自動傳送到 Telegram 並清空該資料夾。';
|
'bot 會在回覆後自動傳送到 Telegram 並清空該資料夾。';
|
||||||
@@ -100,30 +132,46 @@ function startBot(config) {
|
|||||||
return commands.includes(head.toLowerCase());
|
return commands.includes(head.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 把 codex 放進交件匣的檔案傳到 Telegram,傳完清掉
|
// 把 codex 放進交件匣的檔案傳到 Telegram(傳完清掉),
|
||||||
async function flushOutbox(chatId, replyTo) {
|
// 再補傳這一輪 imagegen 生成但沒進交件匣的圖(用內容 hash 去重,避免重複傳)。
|
||||||
const dir = path.join(config.workDir, OUTBOX_DIRNAME);
|
async function flushOutputs(chatId, replyTo, threadId, sinceMs) {
|
||||||
let files = [];
|
const sentHashes = new Set();
|
||||||
|
let sentCount = 0;
|
||||||
|
|
||||||
|
const send = async (file, { deleteAfter }) => {
|
||||||
|
const hash = md5File(file);
|
||||||
|
if (!hash || sentHashes.has(hash)) return;
|
||||||
try {
|
try {
|
||||||
files = fs.readdirSync(dir)
|
await t.sendFile(chatId, file, { replyTo });
|
||||||
|
sentHashes.add(hash);
|
||||||
|
sentCount++;
|
||||||
|
if (deleteAfter) fs.unlinkSync(file);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('傳送檔案失敗:', file, err.message);
|
||||||
|
await t.sendMessage(chatId, `⚠️ 檔案傳送失敗:${path.basename(file)}(${err.message})`).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1) 交件匣(明確交付:任何檔案類型)
|
||||||
|
const dir = path.join(config.workDir, OUTBOX_DIRNAME);
|
||||||
|
let outbox = [];
|
||||||
|
try {
|
||||||
|
outbox = fs.readdirSync(dir)
|
||||||
.map((f) => path.join(dir, f))
|
.map((f) => path.join(dir, f))
|
||||||
.filter((p) => { try { return fs.statSync(p).isFile(); } catch { return false; } })
|
.filter((p) => { try { return fs.statSync(p).isFile(); } catch { return false; } })
|
||||||
.sort();
|
.sort();
|
||||||
} catch {
|
} catch { /* 沒有交件匣 */ }
|
||||||
return; // 沒有交件匣就沒事
|
for (const file of outbox.slice(0, MAX_OUTBOX_FILES)) {
|
||||||
|
await send(file, { deleteAfter: true });
|
||||||
}
|
}
|
||||||
const skipped = files.length - MAX_OUTBOX_FILES;
|
if (outbox.length > MAX_OUTBOX_FILES) {
|
||||||
for (const file of files.slice(0, MAX_OUTBOX_FILES)) {
|
await t.sendMessage(chatId, `⚠️ 交件匣一次最多傳 ${MAX_OUTBOX_FILES} 個檔案,還有 ${outbox.length - MAX_OUTBOX_FILES} 個留在 ${OUTBOX_DIRNAME}/`).catch(() => {});
|
||||||
try {
|
|
||||||
await t.sendFile(chatId, file, { replyTo });
|
|
||||||
fs.unlinkSync(file);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('傳送交件匣檔案失敗:', file, err.message);
|
|
||||||
await t.sendMessage(chatId, `⚠️ 檔案傳送失敗:${path.basename(file)}(${err.message})`).catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (skipped > 0) {
|
// 2) 保險網:imagegen 這一輪的產圖(不刪原檔,codex 之後編輯圖片可能還要用)
|
||||||
await t.sendMessage(chatId, `⚠️ 交件匣一次最多傳 ${MAX_OUTBOX_FILES} 個檔案,還有 ${skipped} 個留在 ${OUTBOX_DIRNAME}/`).catch(() => {});
|
for (const file of collectGeneratedImages(threadId, sinceMs).slice(0, MAX_OUTBOX_FILES)) {
|
||||||
|
if (sentCount >= MAX_OUTBOX_FILES) break;
|
||||||
|
await send(file, { deleteAfter: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +311,7 @@ function startBot(config) {
|
|||||||
|
|
||||||
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
|
const prompt = buildPrompt(message, content || '(使用者只傳了圖片,請描述並依上下文處理)');
|
||||||
const existing = sessions.get(sessionKey);
|
const existing = sessions.get(sessionKey);
|
||||||
|
const turnStart = Date.now();
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
let sessionResetNote = '';
|
let sessionResetNote = '';
|
||||||
@@ -290,7 +339,7 @@ function startBot(config) {
|
|||||||
const footer = buildFooter(config, result.usage, modelMeta, codexDefaults);
|
const footer = buildFooter(config, result.usage, modelMeta, codexDefaults);
|
||||||
const html = t.mdToTgHtml(sessionResetNote + (result.text || '(Codex 沒有回覆文字)') + footer);
|
const html = t.mdToTgHtml(sessionResetNote + (result.text || '(Codex 沒有回覆文字)') + footer);
|
||||||
await t.editOrSplit(chatId, statusMsg.message_id, html, { html: true });
|
await t.editOrSplit(chatId, statusMsg.message_id, html, { html: true });
|
||||||
await flushOutbox(chatId, message.message_id);
|
await flushOutputs(chatId, message.message_id, result.threadId, turnStart);
|
||||||
await t.react(chatId, message.message_id, '👍');
|
await t.react(chatId, message.message_id, '👍');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('處理訊息失敗:', error);
|
console.error('處理訊息失敗:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user