chore: scripts Dokploy/BPS (cron secret, Ads MCC, sync y probes)
Incluye utilidades de ops para CRON_SECRET, login customer Google Ads, sync de seguimiento BPS y probes de conexiones. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
145
scripts/sync_bps_gdoc_days.ts
Normal file
145
scripts/sync_bps_gdoc_days.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
config({ path: resolve(process.cwd(), '.env.local') });
|
||||
|
||||
import {
|
||||
exportGoogleDocPlain,
|
||||
exportGoogleDocHtml,
|
||||
upsertSeguimientoRows,
|
||||
patchSuperhumanIntegration,
|
||||
} from '../src/lib/superhuman-docs';
|
||||
import { getValidAccessToken } from '../src/lib/google-drive';
|
||||
|
||||
const BPS = '8bec977a-6032-4759-aa7f-b5042081f450';
|
||||
const FILE = '1uDnRPWf1Dqx5b9L3TuhfvutyZpSaifz7YFNrRy_eBhw';
|
||||
const DOC = '7TGywaNiUY';
|
||||
|
||||
function parseDayLabel(raw: string): { dateIso: string; label: string } | null {
|
||||
const m = raw.trim().match(/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})$/);
|
||||
if (!m) return null;
|
||||
let y = Number(m[3]);
|
||||
const mo = Number(m[2]);
|
||||
const d = Number(m[1]);
|
||||
if (y < 100) y += 2000;
|
||||
if (y < 2000 || y > 2100 || mo < 1 || mo > 12 || d < 1 || d > 31) return null;
|
||||
return {
|
||||
dateIso: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`,
|
||||
label: `${d}/${mo}/${String(y).slice(2)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function splitNotesByDay(text: string) {
|
||||
const clean = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
||||
const dayRe =
|
||||
/^(?:#{1,4}\s*)?(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})\s*$/;
|
||||
const lines = clean.split('\n');
|
||||
const hits: { idx: number; label: string; dateIso: string }[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = lines[i].trim().match(dayRe);
|
||||
if (!m) continue;
|
||||
const parsed = parseDayLabel(`${m[1]}/${m[2]}/${m[3]}`);
|
||||
if (!parsed) continue;
|
||||
hits.push({ idx: i, ...parsed });
|
||||
}
|
||||
if (!hits.length) return [] as { dateIso: string; label: string; body: string }[];
|
||||
const sections: { dateIso: string; label: string; body: string }[] = [];
|
||||
for (let h = 0; h < hits.length; h++) {
|
||||
const start = hits[h].idx + 1;
|
||||
const end = h + 1 < hits.length ? hits[h + 1].idx : lines.length;
|
||||
const body = lines
|
||||
.slice(start, end)
|
||||
.join('\n')
|
||||
.replace(/^\n+/, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
if (!body) continue;
|
||||
sections.push({ dateIso: hits[h].dateIso, label: hits[h].label, body });
|
||||
}
|
||||
sections.sort((a, b) => b.dateIso.localeCompare(a.dateIso));
|
||||
return sections;
|
||||
}
|
||||
|
||||
async function exportViaDownload(fileId: string, mime: string) {
|
||||
const token = await getValidAccessToken();
|
||||
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(mime)}`;
|
||||
const r = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!r.ok) throw new Error(`${mime} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
||||
return await r.text();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let plain = '';
|
||||
try {
|
||||
plain = await exportGoogleDocPlain(FILE);
|
||||
console.log('plain via helper', plain.length);
|
||||
} catch (e) {
|
||||
console.log('plain helper fail', e instanceof Error ? e.message : e);
|
||||
}
|
||||
if (!plain) {
|
||||
try {
|
||||
plain = await exportViaDownload(FILE, 'text/plain');
|
||||
console.log('plain via download', plain.length);
|
||||
} catch (e) {
|
||||
console.log('plain download fail', e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const html = await exportGoogleDocHtml(FILE);
|
||||
console.log('html ok', html.length);
|
||||
} catch (e) {
|
||||
console.log('html fail', e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
if (!plain) throw new Error('no plain export');
|
||||
console.log('--- sample ---\n', plain.slice(0, 2000));
|
||||
const secs = splitNotesByDay(plain);
|
||||
console.log('sections', secs.length, secs.slice(0, 8).map((s) => s.label));
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const rows = secs.slice(0, 120).map((sec) => {
|
||||
const firstLine =
|
||||
sec.body
|
||||
.split('\n')
|
||||
.map((l) => l.replace(/^[\s*•\-]+/, '').trim())
|
||||
.find((l) => l.length > 8) || '';
|
||||
return {
|
||||
tenant_id: BPS,
|
||||
source: 'superhuman' as const,
|
||||
entry_type: 'meeting' as const,
|
||||
external_id: `gdoc:${FILE}:${sec.dateIso}`,
|
||||
title: `Seguimiento · ${sec.label}`,
|
||||
summary: firstLine.slice(0, 220) || null,
|
||||
body_md: `<pre>${sec.body
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')}</pre>`,
|
||||
browser_link: `https://docs.google.com/document/d/${FILE}/edit`,
|
||||
metrics: {},
|
||||
occurred_at: `${sec.dateIso}T12:00:00.000Z`,
|
||||
synced_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
});
|
||||
|
||||
const n = await upsertSeguimientoRows(rows);
|
||||
await patchSuperhumanIntegration(BPS, {
|
||||
status: 'connected',
|
||||
external_account_id: DOC,
|
||||
label: '01.BLUE PARROT SCHOOL',
|
||||
last_sync_at: now,
|
||||
error_message: null,
|
||||
meta: {
|
||||
doc_name: '01.BLUE PARROT SCHOOL',
|
||||
gdoc_id: FILE,
|
||||
gdoc_days: n,
|
||||
synced_at: now,
|
||||
},
|
||||
});
|
||||
console.log('upserted day notes', n);
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user