fix: monica API limits, robust contact name matching, gemini model fallbacks
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 10s
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 10s
This commit is contained in:
53
server.js
53
server.js
@@ -38,7 +38,7 @@ app.get("/api/contacts", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const limit = 150;
|
const limit = 100;
|
||||||
const suffix = query ? `?query=${encodeURIComponent(query)}&limit=${limit}` : `?limit=${limit}`;
|
const suffix = query ? `?query=${encodeURIComponent(query)}&limit=${limit}` : `?limit=${limit}`;
|
||||||
const payload = await monicaFetch(`/api/contacts${suffix}`);
|
const payload = await monicaFetch(`/api/contacts${suffix}`);
|
||||||
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
||||||
@@ -265,7 +265,7 @@ export async function writeToMonica(entry) {
|
|||||||
results.push(await monicaWrite("note", "/api/notes", {
|
results.push(await monicaWrite("note", "/api/notes", {
|
||||||
contact_id: contactId,
|
contact_id: contactId,
|
||||||
body: entry.note.body,
|
body: entry.note.body,
|
||||||
is_favorited: Boolean(entry.note.isFavorited),
|
is_favorited: entry.note.isFavorited ? 1 : 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,17 +347,37 @@ function inferContacts(text, contactsList = []) {
|
|||||||
if (Array.isArray(contactsList) && contactsList.length > 0) {
|
if (Array.isArray(contactsList) && contactsList.length > 0) {
|
||||||
for (const contact of contactsList) {
|
for (const contact of contactsList) {
|
||||||
if (!contact.name) continue;
|
if (!contact.name) continue;
|
||||||
const nameParts = contact.name.trim().split(/\s+/);
|
|
||||||
const firstName = nameParts[0];
|
|
||||||
const fullName = contact.name.trim();
|
|
||||||
|
|
||||||
const escFullName = fullName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
// Clean up parentheses from the name (e.g. "Aaron Lukas Lingel (Fludo Schlingel)" -> "Aaron Lukas Lingel")
|
||||||
|
const cleanContactName = contact.name.replace(/\(.*?\)/g, "").replace(/\s+/g, " ").trim();
|
||||||
|
if (!cleanContactName) continue;
|
||||||
|
|
||||||
|
const nameParts = cleanContactName.split(/\s+/).filter(p => p.length > 1);
|
||||||
|
if (nameParts.length === 0) continue;
|
||||||
|
|
||||||
|
const firstName = nameParts[0];
|
||||||
|
const lastName = nameParts[nameParts.length - 1];
|
||||||
|
|
||||||
|
const escFullName = cleanContactName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
const escFirstName = firstName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
const escFirstName = firstName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
|
||||||
const regexFull = new RegExp(`\\b${escFullName}\\b`, 'i');
|
const regexFull = new RegExp(`\\b${escFullName}\\b`, 'i');
|
||||||
const regexFirst = new RegExp(`\\b${escFirstName}\\b`, 'i');
|
const regexFirst = new RegExp(`\\b${escFirstName}\\b`, 'i');
|
||||||
|
|
||||||
if (regexFull.test(text) || (firstName.length > 2 && regexFirst.test(text))) {
|
let matches = false;
|
||||||
|
if (regexFull.test(text)) {
|
||||||
|
matches = true;
|
||||||
|
} else if (nameParts.length >= 2) {
|
||||||
|
const escLastName = lastName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regexLast = new RegExp(`\\b${escLastName}\\b`, 'i');
|
||||||
|
if (regexFirst.test(text) && regexLast.test(text)) {
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
} else if (firstName.length > 2 && regexFirst.test(text)) {
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
if (!candidates.some((item) => item.id === contact.id)) {
|
if (!candidates.some((item) => item.id === contact.id)) {
|
||||||
candidates.push({
|
candidates.push({
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
@@ -539,11 +559,14 @@ export function cryptoRandomId() {
|
|||||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(port, () => {
|
const isMain = process.argv[1] && (path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)));
|
||||||
console.log(`Friends listening on http://localhost:${port}`);
|
if (isMain) {
|
||||||
if (process.env.TELEGRAM_BOT_TOKEN) {
|
app.listen(port, () => {
|
||||||
startTelegramBot();
|
console.log(`Friends listening on http://localhost:${port}`);
|
||||||
} else {
|
if (process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
console.log("Telegram Bot not started: TELEGRAM_BOT_TOKEN is not configured in .env");
|
startTelegramBot();
|
||||||
}
|
} else {
|
||||||
});
|
console.log("Telegram Bot not started: TELEGRAM_BOT_TOKEN is not configured in .env");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
86
telegram.js
86
telegram.js
@@ -6,7 +6,9 @@ import {
|
|||||||
cryptoRandomId,
|
cryptoRandomId,
|
||||||
} from "./server.js";
|
} from "./server.js";
|
||||||
|
|
||||||
const monicaBaseUrl = (process.env.MONICA_BASE_URL || "https://crm.mischlabs.de").replace(/\/$/, "");
|
function getMonicaBaseUrl() {
|
||||||
|
return (process.env.MONICA_BASE_URL || "https://crm.mischlabs.de").replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to escape HTML characters for Telegram HTML parse_mode
|
// Helper to escape HTML characters for Telegram HTML parse_mode
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@@ -85,40 +87,64 @@ async function transcribeVoice(audioBuffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base64Audio = Buffer.from(audioBuffer).toString("base64");
|
const base64Audio = Buffer.from(audioBuffer).toString("base64");
|
||||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiKey}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
// List of models to try in order of preference
|
||||||
method: "POST",
|
const modelsToTry = [
|
||||||
headers: {
|
"gemini-2.5-flash",
|
||||||
"Content-Type": "application/json",
|
"gemini-2.0-flash",
|
||||||
},
|
"gemini-1.5-flash-latest",
|
||||||
body: JSON.stringify({
|
"gemini-1.5-flash"
|
||||||
contents: [
|
];
|
||||||
{
|
|
||||||
parts: [
|
let lastError = null;
|
||||||
|
for (const model of modelsToTry) {
|
||||||
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${geminiKey}`;
|
||||||
|
try {
|
||||||
|
console.log(`[Telegram] Trying voice transcription with model: ${model}`);
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [
|
||||||
{
|
{
|
||||||
inlineData: {
|
parts: [
|
||||||
mimeType: "audio/ogg",
|
{
|
||||||
data: base64Audio,
|
inlineData: {
|
||||||
},
|
mimeType: "audio/ogg",
|
||||||
},
|
data: base64Audio,
|
||||||
{
|
},
|
||||||
text: "Transkribiere diese Sprachnachricht wortgetreu in deutschen Text. Gib NUR die Transkription zurück, ohne Einleitung, Kommentare oder sonstige Zusätze. Falls nichts verständlich gesprochen wurde, antworte mit einem leeren Text.",
|
},
|
||||||
|
{
|
||||||
|
text: "Transkribiere diese Sprachnachricht wortgetreu in deutschen Text. Gib NUR die Transkription zurück, ohne Einleitung, Kommentare oder sonstige Zusätze. Falls nichts verständlich gesprochen wurde, antworte mit einem leeren Text.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
],
|
});
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (res.status === 404) {
|
||||||
const errorText = await res.text();
|
console.warn(`[Telegram] Model ${model} returned 404. Trying next model...`);
|
||||||
throw new Error(`Gemini API failed (${res.status}): ${errorText}`);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
throw new Error(`Gemini API failed (${res.status}): ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||||
|
return text.trim();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Telegram] Error with model ${model}:`, err.message);
|
||||||
|
lastError = err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
throw new Error(`All Gemini models failed. Last error: ${lastError ? lastError.message : "unknown"}`);
|
||||||
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
||||||
return text.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch available contacts from Monica to perform NLP name matching
|
// Fetch available contacts from Monica to perform NLP name matching
|
||||||
@@ -133,7 +159,7 @@ async function fetchMonicaContacts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await monicaFetch("/api/contacts?limit=150");
|
const payload = await monicaFetch("/api/contacts?limit=100");
|
||||||
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
||||||
return contacts.map((c) => ({
|
return contacts.map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
@@ -280,7 +306,7 @@ async function handleUpdate(update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.contact) {
|
if (analysis.contact) {
|
||||||
const contactUrl = `${monicaBaseUrl}/people/${analysis.contact.id}`;
|
const contactUrl = `${getMonicaBaseUrl()}/people/${analysis.contact.id}`;
|
||||||
responseText += `👤 <b>Kontakt:</b> <a href="${contactUrl}">${escapeHtml(analysis.contact.name)}</a>\n`;
|
responseText += `👤 <b>Kontakt:</b> <a href="${contactUrl}">${escapeHtml(analysis.contact.name)}</a>\n`;
|
||||||
} else {
|
} else {
|
||||||
responseText += `👤 <b>Kontakt:</b> <i>Keine Zuordnung</i>\n`;
|
responseText += `👤 <b>Kontakt:</b> <i>Keine Zuordnung</i>\n`;
|
||||||
|
|||||||
Reference in New Issue
Block a user