feat: implement gemini async nlp extraction, contact auto-creation, data enrichment & clean notes
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 9s
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 9s
This commit is contained in:
291
server.js
291
server.js
@@ -208,7 +208,13 @@ app.post("/api/analyze", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(analyzeText(text, mode, contactsList));
|
try {
|
||||||
|
const analysis = await analyzeTextAI(text, mode, contactsList);
|
||||||
|
res.json(analysis);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API] Analysis error:", error.message);
|
||||||
|
res.status(500).json({ error: "Analysis failed.", detail: error.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/save", async (req, res) => {
|
app.post("/api/save", async (req, res) => {
|
||||||
@@ -260,14 +266,128 @@ export async function writeToMonica(entry) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactId = Number(entry.contact?.id || 0);
|
let contactId = Number(entry.contact?.id || 0);
|
||||||
if (contactId && entry.note?.enabled && entry.note?.body) {
|
|
||||||
|
// Auto-creation of a new contact
|
||||||
|
if (entry.contact?.is_new && entry.contact?.first_name) {
|
||||||
|
try {
|
||||||
|
console.log(`[CRM] Auto-creating new contact: "${entry.contact.first_name} ${entry.contact.last_name || ''}"`);
|
||||||
|
const newContactPayload = await monicaFetch("/api/contacts", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: entry.contact.first_name,
|
||||||
|
last_name: entry.contact.last_name || "",
|
||||||
|
description: "Automatisch angelegter Kontakt via Friends",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const newContact = newContactPayload.data || {};
|
||||||
|
contactId = Number(newContact.id);
|
||||||
|
|
||||||
|
if (contactId) {
|
||||||
|
entry.contact.id = contactId;
|
||||||
|
entry.contact.name = newContact.complete_name || [entry.contact.first_name, entry.contact.last_name].filter(Boolean).join(" ");
|
||||||
|
results.push({ type: "contact_create", ok: true, id: contactId });
|
||||||
|
} else {
|
||||||
|
throw new Error("Could not retrieve new contact ID from Monica response.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[CRM] Failed to auto-create contact in Monica:", error.message);
|
||||||
|
results.push({ type: "contact_create", ok: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update/enrich contact fields if we have a contact ID
|
||||||
|
if (contactId) {
|
||||||
|
// 1. Birthdate enrichment (native birthday field on the contact)
|
||||||
|
if (entry.contact?.birthdate) {
|
||||||
|
try {
|
||||||
|
console.log(`[CRM] Enriching contact ${contactId} birthdate: ${entry.contact.birthdate}`);
|
||||||
|
// Monica PUT /api/contacts/:id requires first_name to be passed, so we fetch it first
|
||||||
|
const contactDetails = await monicaFetch(`/api/contacts/${contactId}`);
|
||||||
|
const existingData = contactDetails.data || {};
|
||||||
|
|
||||||
|
await monicaFetch(`/api/contacts/${contactId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: existingData.first_name || entry.contact.first_name || "Unbekannt",
|
||||||
|
last_name: existingData.last_name || entry.contact.last_name || "",
|
||||||
|
birthdate: entry.contact.birthdate,
|
||||||
|
is_birthdate_known: entry.contact.birthdate.startsWith("0000") ? "unknown" : "true",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
results.push({ type: "contact_birthdate", ok: true, id: contactId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CRM] Failed to update birthdate for contact ${contactId}:`, error.message);
|
||||||
|
results.push({ type: "contact_birthdate", ok: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Email enrichment
|
||||||
|
if (entry.contact?.email) {
|
||||||
|
try {
|
||||||
|
console.log(`[CRM] Adding email to contact ${contactId}: ${entry.contact.email}`);
|
||||||
|
const field = await monicaFetch("/api/contactfields", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
contact_id: contactId,
|
||||||
|
contact_field_type_id: 1, // Email
|
||||||
|
data: entry.contact.email,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
results.push({ type: "contact_email", ok: true, id: field.data?.id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CRM] Failed to add email for contact ${contactId}:`, error.message);
|
||||||
|
results.push({ type: "contact_email", ok: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Phone enrichment
|
||||||
|
if (entry.contact?.phone) {
|
||||||
|
try {
|
||||||
|
console.log(`[CRM] Adding phone to contact ${contactId}: ${entry.contact.phone}`);
|
||||||
|
const field = await monicaFetch("/api/contactfields", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
contact_id: contactId,
|
||||||
|
contact_field_type_id: 3, // Cell phone
|
||||||
|
data: entry.contact.phone,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
results.push({ type: "contact_phone", ok: true, id: field.data?.id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CRM] Failed to add phone for contact ${contactId}:`, error.message);
|
||||||
|
results.push({ type: "contact_phone", ok: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Address enrichment
|
||||||
|
if (entry.contact?.address) {
|
||||||
|
try {
|
||||||
|
console.log(`[CRM] Adding address to contact ${contactId}: ${entry.contact.address}`);
|
||||||
|
const addr = await monicaFetch("/api/addresses", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
contact_id: contactId,
|
||||||
|
name: "Hauptadresse",
|
||||||
|
street: entry.contact.address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
results.push({ type: "contact_address", ok: true, id: addr.data?.id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CRM] Failed to add address for contact ${contactId}:`, error.message);
|
||||||
|
results.push({ type: "contact_address", ok: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Contextual Clean Note
|
||||||
|
if (entry.note?.enabled && entry.note?.body) {
|
||||||
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: entry.note.isFavorited ? 1 : 0,
|
is_favorited: entry.note.isFavorited ? 1 : 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (contactId && Array.isArray(entry.reminders)) {
|
if (contactId && Array.isArray(entry.reminders)) {
|
||||||
for (const reminder of entry.reminders.filter((item) => item.enabled && item.initial_date && item.title)) {
|
for (const reminder of entry.reminders.filter((item) => item.enabled && item.initial_date && item.title)) {
|
||||||
@@ -298,6 +418,171 @@ async function monicaWrite(type, apiPath, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function analyzeTextWithGemini(text, mode = "auto", contactsList = []) {
|
||||||
|
const geminiKey = process.env.GEMINI_API_KEY;
|
||||||
|
if (!geminiKey) return null;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const todayStr = today.toISOString().slice(0, 10);
|
||||||
|
const weekdayStr = today.toLocaleDateString("de-DE", { weekday: "long" });
|
||||||
|
|
||||||
|
const systemInstruction = `
|
||||||
|
You are an advanced NLP analysis assistant for a personal CRM (Monica) and diary system.
|
||||||
|
Analyze the user's text message or transcribed voice input, and extract contact details, notes, journal entries, and reminders.
|
||||||
|
|
||||||
|
The user's local date/time is: ${todayStr} (today is ${weekdayStr}).
|
||||||
|
|
||||||
|
We have the following existing contacts in our CRM:
|
||||||
|
${JSON.stringify(contactsList.map(c => ({ id: c.id, name: c.name })))}
|
||||||
|
|
||||||
|
Perform the following tasks:
|
||||||
|
1. Contact Matching:
|
||||||
|
- Match the input text to one of the existing contacts if the user is talking about or to them.
|
||||||
|
- Look for full names, first names, nicknames, or close matches.
|
||||||
|
- If matched, set "contact" to:
|
||||||
|
{ "is_new": false, "id": <existing_id>, "name": "<existing_name>", "first_name": "<existing_first_name>", "last_name": "<existing_last_name>" }
|
||||||
|
(Note: extract the first_name and last_name of the contact if known, or leave them empty/null).
|
||||||
|
|
||||||
|
2. New Contact Auto-Creation:
|
||||||
|
- If NO existing contact matches, and the user mentions a new name with BOTH a First and a Last name (e.g., "Tobias Misch"), OR mentions a single name with explicit contact details (like phone, email, address, or birthdate), set "contact" to:
|
||||||
|
{ "is_new": true, "id": null, "name": "<full_name>", "first_name": "<parsed_first_name>", "last_name": "<parsed_last_name>", "birthdate": "<parsed_birthdate>", "phone": "<parsed_phone>", "email": "<parsed_email>", "address": "<parsed_address>" }
|
||||||
|
- IMPORTANT: Only create a new contact (is_new: true) if BOTH first and last names are detected, OR if there is a single name accompanied by explicit details (phone, email, address, or birthdate). Do not auto-create contacts for a single first name with no other details (to avoid clutter).
|
||||||
|
|
||||||
|
3. Contact Details Extraction & Enrichment (for BOTH new and existing contacts):
|
||||||
|
- Extract any of the following if mentioned:
|
||||||
|
- birthdate: Birthday formatted as "YYYY-MM-DD" or "0000-MM-DD" if the year is unknown/unstated (e.g. "Geburtstag am 14. September" -> "0000-09-14").
|
||||||
|
- phone: Cell phone or phone number.
|
||||||
|
- email: Email address.
|
||||||
|
- address: Address / place of living.
|
||||||
|
|
||||||
|
4. Note Extraction (Clean Contact Note):
|
||||||
|
- If the text is about a contact, extract/write a clean, contextual, grammatically correct German note summarizing the information or interaction.
|
||||||
|
- CRITICAL: Keep note fields clean! The note body MUST NOT contain raw commands or instructions (e.g., "Erinnere mich an...", "Trage Geburtstag ein"), nor should it contain raw contact details (phone, email, address, birthdate) that are already captured in the structured fields. The note should be a short, premium German summary of the actual interaction or context (e.g., "Heute getroffen und ein Bier getrunken." or "Ist umgezogen und hat eine neue Telefonnummer.").
|
||||||
|
- Set note.enabled = true, note.isFavorited = true if the input feels highly important.
|
||||||
|
|
||||||
|
5. Reminder Extraction:
|
||||||
|
- Extract reminders if the user wants to be reminded of something (e.g., "erinnere mich am Freitag nachzufragen", "erinnere mich morgen anzurufen").
|
||||||
|
- Calculate the absolute date based on relative expressions and today's date (${todayStr}).
|
||||||
|
- Format: { "enabled": true, "title": "<German short title>", "description": "<full original sentence>", "initial_date": "YYYY-MM-DD", "frequency_type": "one_time", "frequency_number": 1 }
|
||||||
|
- CRITICAL: Skip creating reminders for birthdays, since the native birthdate field in Monica CRM automatically triggers birthday reminders.
|
||||||
|
|
||||||
|
6. Journal / Tagebuch:
|
||||||
|
- If the text has a diary/journal character (e.g. "heute war...", "ich fühle...", or general reflection), set journal.enabled = true, write a suitable German journal title, and set the post as the text.
|
||||||
|
|
||||||
|
7. Mode override:
|
||||||
|
- If mode is "journal", force type to "journal", set journal.enabled = true, and contact/note to null/disabled.
|
||||||
|
|
||||||
|
Output a single valid JSON object following this EXACT schema. Do not output any markdown formatting, backticks, or explanation. Only the raw JSON.
|
||||||
|
JSON Schema:
|
||||||
|
{
|
||||||
|
"type": "contact" | "journal" | "mixed",
|
||||||
|
"summary": "Short German summary of the text (max 150 chars)",
|
||||||
|
"contact": {
|
||||||
|
"is_new": boolean,
|
||||||
|
"id": number | null,
|
||||||
|
"name": "string" | null,
|
||||||
|
"first_name": "string" | null,
|
||||||
|
"last_name": "string" | null,
|
||||||
|
"birthdate": "YYYY-MM-DD" | "0000-MM-DD" | null,
|
||||||
|
"phone": "string" | null,
|
||||||
|
"email": "string" | null,
|
||||||
|
"address": "string" | null
|
||||||
|
} | null,
|
||||||
|
"note": {
|
||||||
|
"enabled": boolean,
|
||||||
|
"body": "string" | null,
|
||||||
|
"isFavorited": boolean
|
||||||
|
},
|
||||||
|
"journal": {
|
||||||
|
"enabled": boolean,
|
||||||
|
"title": "string" | null,
|
||||||
|
"post": "string" | null
|
||||||
|
},
|
||||||
|
"reminders": [
|
||||||
|
{
|
||||||
|
"enabled": boolean,
|
||||||
|
"title": "string",
|
||||||
|
"description": "string",
|
||||||
|
"initial_date": "YYYY-MM-DD",
|
||||||
|
"frequency_type": "one_time",
|
||||||
|
"frequency_number": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confidence": "low" | "medium" | "high"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const modelsToTry = [
|
||||||
|
"gemini-2.5-flash",
|
||||||
|
"gemini-2.0-flash",
|
||||||
|
"gemini-1.5-flash-latest",
|
||||||
|
"gemini-1.5-flash"
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastError = null;
|
||||||
|
for (const model of modelsToTry) {
|
||||||
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${geminiKey}`;
|
||||||
|
try {
|
||||||
|
console.log(`[NLP] Trying text analysis with model: ${model}`);
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
text: `${systemInstruction}\n\nUser input text to analyze:\n"${text}"`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
generationConfig: {
|
||||||
|
responseMimeType: "application/json"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
console.warn(`[NLP] Model ${model} returned 404. Trying next model...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
throw new Error(`Gemini API failed (${res.status}): ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||||
|
const parsed = JSON.parse(responseText.trim());
|
||||||
|
|
||||||
|
// Ensure rawText is added
|
||||||
|
parsed.rawText = text;
|
||||||
|
return parsed;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[NLP] Error with model ${model}:`, err.message);
|
||||||
|
lastError = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`All Gemini models failed for NLP analysis. Last error: ${lastError ? lastError.message : "unknown"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function analyzeTextAI(text, mode = "auto", contactsList = []) {
|
||||||
|
if (process.env.GEMINI_API_KEY) {
|
||||||
|
try {
|
||||||
|
const result = await analyzeTextWithGemini(text, mode, contactsList);
|
||||||
|
if (result) return result;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[NLP] Gemini analysis failed. Falling back to regex:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return analyzeText(text, mode, contactsList);
|
||||||
|
}
|
||||||
|
|
||||||
export function analyzeText(text, mode = "auto", contactsList = []) {
|
export function analyzeText(text, mode = "auto", contactsList = []) {
|
||||||
const normalized = text.replace(/\s+/g, " ").trim();
|
const normalized = text.replace(/\s+/g, " ").trim();
|
||||||
const contacts = mode === "journal" ? [] : inferContacts(normalized, contactsList);
|
const contacts = mode === "journal" ? [] : inferContacts(normalized, contactsList);
|
||||||
|
|||||||
45
telegram.js
45
telegram.js
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
analyzeText,
|
analyzeTextAI,
|
||||||
writeToMonica,
|
writeToMonica,
|
||||||
appendLocalRecord,
|
appendLocalRecord,
|
||||||
monicaFetch,
|
monicaFetch,
|
||||||
@@ -270,7 +270,7 @@ async function handleUpdate(update) {
|
|||||||
// 3. Match contacts and run NLP engine
|
// 3. Match contacts and run NLP engine
|
||||||
await sendChatAction(chatId, "typing");
|
await sendChatAction(chatId, "typing");
|
||||||
const contacts = await fetchMonicaContacts();
|
const contacts = await fetchMonicaContacts();
|
||||||
const analysis = analyzeText(processedText, "auto", contacts);
|
const analysis = await analyzeTextAI(processedText, "auto", contacts);
|
||||||
|
|
||||||
// 4. Save entry locally and sync to Monica CRM
|
// 4. Save entry locally and sync to Monica CRM
|
||||||
const entry = {
|
const entry = {
|
||||||
@@ -305,9 +305,14 @@ async function handleUpdate(update) {
|
|||||||
responseText += `🎤 <b>Transkription (Gemini):</b>\n<i>"${escapeHtml(processedText)}"</i>\n\n`;
|
responseText += `🎤 <b>Transkription (Gemini):</b>\n<i>"${escapeHtml(processedText)}"</i>\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.contact) {
|
if (entry.contact) {
|
||||||
const contactUrl = `${getMonicaBaseUrl()}/people/${analysis.contact.id}`;
|
if (entry.contact.id) {
|
||||||
responseText += `👤 <b>Kontakt:</b> <a href="${contactUrl}">${escapeHtml(analysis.contact.name)}</a>\n`;
|
const contactUrl = `${getMonicaBaseUrl()}/people/${entry.contact.id}`;
|
||||||
|
const nameStr = entry.contact.is_new ? `${entry.contact.name} (neu erstellt)` : entry.contact.name;
|
||||||
|
responseText += `👤 <b>Kontakt:</b> <a href="${contactUrl}">${escapeHtml(nameStr)}</a>\n`;
|
||||||
|
} else {
|
||||||
|
responseText += `👤 <b>Kontakt:</b> <i>Fehler bei Erstellung (${escapeHtml(entry.contact.name)})</i>\n`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
responseText += `👤 <b>Kontakt:</b> <i>Keine Zuordnung</i>\n`;
|
responseText += `👤 <b>Kontakt:</b> <i>Keine Zuordnung</i>\n`;
|
||||||
}
|
}
|
||||||
@@ -329,9 +334,35 @@ async function handleUpdate(update) {
|
|||||||
responseText += `\n<b>Monica CRM Synchronisation:</b>\n`;
|
responseText += `\n<b>Monica CRM Synchronisation:</b>\n`;
|
||||||
if (localRecord.monicaResults && localRecord.monicaResults.length > 0) {
|
if (localRecord.monicaResults && localRecord.monicaResults.length > 0) {
|
||||||
for (const res of localRecord.monicaResults) {
|
for (const res of localRecord.monicaResults) {
|
||||||
const typeLabel = res.type === "journal" ? "📖 Tagebuch" : res.type === "note" ? "📝 Notiz" : "📅 Erinnerung";
|
let typeLabel = "⚙️ CRM";
|
||||||
|
switch (res.type) {
|
||||||
|
case "journal":
|
||||||
|
typeLabel = "📖 Tagebuch";
|
||||||
|
break;
|
||||||
|
case "note":
|
||||||
|
typeLabel = "📝 Notiz";
|
||||||
|
break;
|
||||||
|
case "reminder":
|
||||||
|
typeLabel = "📅 Erinnerung";
|
||||||
|
break;
|
||||||
|
case "contact_create":
|
||||||
|
typeLabel = "👤 Kontakt erstellt";
|
||||||
|
break;
|
||||||
|
case "contact_birthdate":
|
||||||
|
typeLabel = "📅 Geburtstag aktualisiert";
|
||||||
|
break;
|
||||||
|
case "contact_email":
|
||||||
|
typeLabel = "📧 E-Mail hinzugefügt";
|
||||||
|
break;
|
||||||
|
case "contact_phone":
|
||||||
|
typeLabel = "📞 Telefon hinzugefügt";
|
||||||
|
break;
|
||||||
|
case "contact_address":
|
||||||
|
typeLabel = "🏠 Adresse hinzugefügt";
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
responseText += `- ${typeLabel}: ✅ Erfolgreich (ID: ${res.id})\n`;
|
responseText += `- ${typeLabel}: ✅ Erfolgreich\n`;
|
||||||
} else {
|
} else {
|
||||||
responseText += `- ${typeLabel}: ❌ Fehler: <code>${escapeHtml(res.error)}</code>\n`;
|
responseText += `- ${typeLabel}: ❌ Fehler: <code>${escapeHtml(res.error)}</code>\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
23
test_contacts.js
Normal file
23
test_contacts.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
import { monicaFetch } from "./server.js";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Checking Monica API token...", process.env.MONICA_API_TOKEN ? "Present" : "Missing");
|
||||||
|
console.log("Monica Base URL:", process.env.MONICA_BASE_URL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await monicaFetch("/api/contacts?limit=100");
|
||||||
|
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
||||||
|
console.log(`Fetched ${contacts.length} contacts from Monica CRM:`);
|
||||||
|
for (const c of contacts) {
|
||||||
|
const name = c.complete_name || [c.first_name, c.last_name].filter(Boolean).join(" ");
|
||||||
|
console.log(`- ID: ${c.id}, Name: "${name}", First Name: "${c.first_name}", Last Name: "${c.last_name}"`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching contacts:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
Reference in New Issue
Block a user