chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
apps/*/
|
||||
analysis/*/
|
||||
vaults/*
|
||||
!vaults/.gitkeep
|
||||
!vaults/vault.yaml
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# Convenciones de archivos — vault osint
|
||||
|
||||
Estándar para organizar la información de personas (y, a futuro, dominios y casos) en el vault
|
||||
`osint` (`/home/enmanuel/Obsidian/osint`). Toda extracción desde otros vaults o desde sesiones
|
||||
de navegación OSINT debe dejar los archivos siguiendo este estándar.
|
||||
|
||||
## 1. Estructura física (estándar: plano + subcarpeta de documentos)
|
||||
|
||||
```
|
||||
osint/
|
||||
README.md
|
||||
_self.md # ficha del titular (Enmanuel), referencia, NO es objetivo
|
||||
personas/
|
||||
<slug>.md # ficha de la persona (su "index")
|
||||
<slug>/ # carpeta con las notas-documento de esa persona
|
||||
<doc-slug>.md
|
||||
dominios/
|
||||
organizaciones/ # empresas/entidades (mismo patrón <slug>.md + <slug>/)
|
||||
lugares/ # direcciones/lugares de interés
|
||||
casos/
|
||||
inbox/
|
||||
attachments/
|
||||
personas/
|
||||
<slug>/
|
||||
<doc-slug>-NN.<ext> # imágenes/PDFs de esa persona
|
||||
```
|
||||
|
||||
Regla: la ficha vive como `.md` suelto en `personas/` (todas las fichas listables en un nivel);
|
||||
sus documentos en una subcarpeta homónima; los binarios en `attachments/personas/<slug>/`.
|
||||
|
||||
## 2. Slug
|
||||
|
||||
El `slug` es la clave estable de una persona. Se deriva del nombre completo:
|
||||
|
||||
- minúsculas, sin acentos ni `ñ` (transliteración: `ñ`→`n`, `á`→`a`, …)
|
||||
- espacios y separadores → `-`
|
||||
- solo `[a-z0-9-]`, sin guiones dobles ni al inicio/fin
|
||||
|
||||
Ejemplo: `Enmanuel Gutiérrez Pérez` → `enmanuel-gutierrez-perez`.
|
||||
|
||||
## 3. Ficha de persona — `personas/<slug>.md`
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
tipo: persona
|
||||
nombre: "Enmanuel Gutierrez Perez" # nombre legible original
|
||||
aliases: ["Enmanuel Gutierrez Perez"] # nombres viejos para que wikilinks previos resuelvan
|
||||
slug: enmanuel-gutierrez-perez
|
||||
sexo: hombre # hombre|mujer|null
|
||||
fecha_nacimiento: 1997-03-05 # ISO o null
|
||||
dni: 54370345R # o null
|
||||
direccion: "Calle Eugenia Rios 14, 29718 Almachar, Malaga" # texto plano, no wikilink
|
||||
pais: españa
|
||||
tags: [persona, osint]
|
||||
fuente: "NotasDeObsidian/personas/Enmanuel Gutierrez Perez.md" # origen de la extracción
|
||||
```
|
||||
|
||||
Body, secciones en este orden (omitir las vacías):
|
||||
|
||||
```markdown
|
||||
## Documentos
|
||||
- [[enmanuel-gutierrez-perez/dni|DNI]]
|
||||
- [[enmanuel-gutierrez-perez/certificado-digital|Certificado digital]]
|
||||
|
||||
## Relaciones
|
||||
- [[manuel-gutierrez-gamez|Manuel Gutierrez Gamez]] — hermano
|
||||
|
||||
## Notas
|
||||
Texto libre de investigación.
|
||||
```
|
||||
|
||||
## 4. Nota-documento — `personas/<slug>/<doc-slug>.md`
|
||||
|
||||
Una nota-documento agrupa los attachments de un tipo de documento de la persona.
|
||||
|
||||
`<doc-slug>` canónico (deriva del título original quitando el nombre de la persona y
|
||||
normalizando):
|
||||
|
||||
| Título original (ejemplos) | doc-slug | doc_tipo |
|
||||
|---|---|---|
|
||||
| `Dni X`, `DNI de X` | `dni` | dni |
|
||||
| `Certificado Digital X` | `certificado-digital` | certificado |
|
||||
| `Carnet de conducir de X`, `Documentos Carnet Conducir X` | `carnet-conducir` | carnet |
|
||||
| `Fotos X`, `Fotos de X` | `fotos` | fotos |
|
||||
| `Vida Laboral X` | `vida-laboral` | laboral |
|
||||
| `Nominas <empresa> X` | `nominas-<empresa>` | laboral |
|
||||
| `Documentos <banco> X` (Abanca, BBVA, Cajamar…) | `documentos-<banco>` | banco |
|
||||
| `Modelo 100 X`, `Alta autonomo X` | `modelo-100`, `alta-autonomo` | fiscal |
|
||||
| `Datos empadronamiento X` | `empadronamiento` | otro |
|
||||
| `Contrato <x> X` | `contrato-<x>` | contrato |
|
||||
| (sin patrón claro) | slug del título sin el nombre | otro |
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
tipo: documento
|
||||
doc_tipo: dni # dni|certificado|carnet|fotos|banco|fiscal|laboral|contrato|otro
|
||||
persona: "[[enmanuel-gutierrez-perez]]"
|
||||
fuente: "NotasDeObsidian/Dni Enmanuel Gutierrez.md"
|
||||
```
|
||||
|
||||
Body: los embeds reescritos a la ruta de attachments de la persona:
|
||||
|
||||
```markdown
|
||||
![[attachments/personas/enmanuel-gutierrez-perez/dni-1.jpg]]
|
||||
![[attachments/personas/enmanuel-gutierrez-perez/dni-2.jpeg]]
|
||||
```
|
||||
|
||||
## 5. Attachments — `attachments/personas/<slug>/`
|
||||
|
||||
- Cada binario referenciado por una nota-documento se mueve a `attachments/personas/<slug>/`.
|
||||
- Se renombra a `<doc-slug>-NN.<ext>` (1-indexed, en el orden en que aparece en la nota),
|
||||
preservando la extensión original en minúsculas. Ej: `dni-1.jpg`, `dni-2.jpeg`,
|
||||
`documentos-abanca-1.pdf`.
|
||||
- Si el nombre original es relevante, se anota en el body de la nota-documento como comentario.
|
||||
|
||||
## 6. Entidades que NO son personas
|
||||
|
||||
Notas como `FenixFood SL`, `Documento Endesa`, `Documentos Aduanas …` van a `organizaciones/`;
|
||||
las direcciones/lugares (`Calle Eugenia Rios …`) van a `lugares/`. Ambas carpetas siguen el
|
||||
mismo patrón que `personas/`: `<slug>.md` como ficha + subcarpeta `<slug>/` para sus
|
||||
documentos + `attachments/<organizaciones|lugares>/<slug>/` para sus binarios. El frontmatter
|
||||
usa `tipo: organizacion` / `tipo: lugar` en vez de `tipo: persona`.
|
||||
|
||||
## 7. Relaciones y direcciones
|
||||
|
||||
- **Relaciones** entre personas (`Hermano de [[X]]`) se normalizan a la sección `## Relaciones`
|
||||
de la ficha, enlazando al `slug` de la otra persona con alias legible.
|
||||
- **Direcciones**: en el frontmatter `direccion` como texto plano (no wikilink). Si una dirección
|
||||
es entidad de investigación propia, tiene además su ficha en `lugares/<slug>.md`, y la persona
|
||||
la enlaza con `direccion: "[[lugares/<slug>|texto legible]]"` si se quiere navegable.
|
||||
|
||||
## 8. Idempotencia de la extracción
|
||||
|
||||
El migrador debe ser re-ejecutable: si una persona ya existe en osint con su slug, se
|
||||
actualiza (no se duplica). Los attachments ya movidos no se vuelven a mover.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: osint
|
||||
description: "Investigaciones OSINT realizadas navegando con los perfiles de Chromium dedicados. Agrupa el vault de notas de investigacion y, a futuro, apps/analyses de recon y agregacion de entidades."
|
||||
tags: [osint, recon, web_scraping, investigation]
|
||||
repo_url: ""
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
Project dedicado a la investigacion OSINT. El nucleo hoy es el vault `osint` (un vault de
|
||||
Obsidian en `/home/enmanuel/Obsidian/osint`, enlazado en `vaults/`), donde se guardan las
|
||||
investigaciones hechas navegando con los perfiles de Chromium para OSINT.
|
||||
|
||||
### Vault osint
|
||||
|
||||
Notas Markdown con frontmatter YAML y wikilinks `[[...]]`. Estructura:
|
||||
|
||||
- `personas/` — fichas de personas investigadas (alias, cuentas, conexiones).
|
||||
- `dominios/` — dominios/sitios y su recon.
|
||||
- `casos/` — investigaciones completas (un caso enlaza personas y dominios).
|
||||
- `inbox/` — capturas rapidas sin clasificar.
|
||||
|
||||
El CRUD del vault se hace con el grupo de funciones del registry `obsidian`
|
||||
(`docs/capabilities/obsidian.md`) — headless, sin abrir la app GUI. Para la captura web que
|
||||
alimenta las investigaciones, ver el grupo `web-proxy` y el tooling de browser del project
|
||||
`web_scraping`.
|
||||
|
||||
### Relacion con web_scraping
|
||||
|
||||
`web_scraping` aporta la captura/automatizacion (perfiles Chromium, CDP, proxy, flow replay).
|
||||
`osint` aporta el destino: el conocimiento destilado de esas sesiones, conectado como grafo
|
||||
de notas.
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Migrador de organizaciones (empresas) desde NotasDeObsidian al vault osint.
|
||||
|
||||
A diferencia de las personas (que parten de una ficha con wikilinks), las organizaciones se
|
||||
detectan recogiendo todas las notas cuyo titulo menciona el nombre de la empresa. La nota cuyo
|
||||
titulo ES el nombre de la empresa actua como ficha; el resto son documentos.
|
||||
|
||||
Estructura resultante (estandar projects/osint/CONVENTIONS.md):
|
||||
organizaciones/<slug>.md
|
||||
organizaciones/<slug>/<doc-slug>.md
|
||||
attachments/organizaciones/<slug>/<doc-slug>-N.ext
|
||||
|
||||
Modo MOVER (borra originales). Idempotente (overwrite por slug).
|
||||
"""
|
||||
import sys, os, re, shutil
|
||||
|
||||
sys.path.insert(0, "/home/enmanuel/fn_registry/python/functions")
|
||||
from obsidian import (read_obsidian_note, create_obsidian_note,
|
||||
slugify_obsidian_name, extract_obsidian_embeds, resolve_obsidian_embed,
|
||||
list_obsidian_notes)
|
||||
|
||||
NOTAS = "/home/enmanuel/Obsidian/NotasDeObsidian"
|
||||
OSINT = "/home/enmanuel/Obsidian/osint"
|
||||
STOPWORDS = {"de", "del", "la", "las", "el", "los", "y", "a", "en"}
|
||||
|
||||
ORGS = [
|
||||
{"display": "FenixFood SL", "slug": "fenixfood-sl",
|
||||
"match": re.compile(r'fenix\s*food', re.I),
|
||||
"nametok": {"fenixfood", "fenix", "food", "sl"}},
|
||||
{"display": "BiorganicFood SL", "slug": "biorganicfood-sl",
|
||||
"match": re.compile(r'biorganic', re.I),
|
||||
"nametok": {"biorganicfood", "biorganic", "food", "sl"}},
|
||||
]
|
||||
|
||||
|
||||
def doc_slug(title, ntok):
|
||||
parts = [p for p in slugify_obsidian_name(title).split("-")
|
||||
if p and p not in ntok and p not in STOPWORDS]
|
||||
return "-".join(parts) or "documento"
|
||||
|
||||
|
||||
def migrate_org(org, all_notes, att_del, docs_del):
|
||||
matched = [n for n in all_notes if org["match"].search(os.path.basename(n)[:-3])]
|
||||
ficha_path = None
|
||||
for n in matched:
|
||||
t = os.path.basename(n)[:-3]
|
||||
if slugify_obsidian_name(t) == org["slug"] or t.lower() == org["display"].lower():
|
||||
ficha_path = n
|
||||
break
|
||||
|
||||
slug, ntok = org["slug"], org["nametok"]
|
||||
att_rel = f"attachments/organizaciones/{slug}"
|
||||
att_abs = f"{OSINT}/{att_rel}"
|
||||
os.makedirs(att_abs, exist_ok=True)
|
||||
|
||||
docs_done, ficha_extra = [], ""
|
||||
for n in matched:
|
||||
t = os.path.basename(n)[:-3]
|
||||
if n == ficha_path:
|
||||
ficha_extra = read_obsidian_note(n)["body"]
|
||||
continue
|
||||
dn = read_obsidian_note(n)
|
||||
ds = doc_slug(t, ntok)
|
||||
# evitar colision de doc-slug dentro de la misma org
|
||||
base_ds = ds
|
||||
k = 2
|
||||
while os.path.exists(f"{OSINT}/organizaciones/{slug}/{ds}.md"):
|
||||
ds = f"{base_ds}-{k}"; k += 1
|
||||
embeds = extract_obsidian_embeds(dn["body"])
|
||||
new = []
|
||||
for i, emb in enumerate(embeds, 1):
|
||||
ap = resolve_obsidian_embed(NOTAS, emb)
|
||||
if not ap:
|
||||
new.append(f"<!-- attachment no encontrado: {emb} -->"); continue
|
||||
ext = os.path.splitext(ap)[1].lower()
|
||||
nn = f"{ds}-{i}{ext}"
|
||||
shutil.copy2(ap, f"{att_abs}/{nn}"); att_del.add(ap)
|
||||
new.append(f"![[{att_rel}/{nn}]]")
|
||||
# conservar texto del doc si lo tiene, ademas de los embeds reescritos
|
||||
text = dn["body"]
|
||||
text_wo_embeds = re.sub(r'!\[\[[^\]]+\]\]', '', text).strip()
|
||||
parts = []
|
||||
if text_wo_embeds:
|
||||
parts.append(text_wo_embeds)
|
||||
if new:
|
||||
parts.append("\n".join(new))
|
||||
create_obsidian_note(OSINT, f"organizaciones/{slug}/{ds}",
|
||||
body="\n\n".join(parts) if parts else "(sin contenido)",
|
||||
frontmatter={"tipo": "documento", "entidad": f"[[{slug}]]",
|
||||
"fuente": "NotasDeObsidian/" + os.path.relpath(n, NOTAS)},
|
||||
overwrite=True)
|
||||
docs_done.append((t, ds)); docs_del.add(n)
|
||||
|
||||
bl = [f"Ficha de la organizacion {org['display']}.", ""]
|
||||
if ficha_extra.strip():
|
||||
bl += [ficha_extra.strip(), ""]
|
||||
bl += ["## Documentos", ""] + [f"- [[organizaciones/{slug}/{ds}|{t}]]" for t, ds in docs_done]
|
||||
bl += ["", "## Notas", ""]
|
||||
create_obsidian_note(OSINT, f"organizaciones/{slug}", body="\n".join(bl),
|
||||
frontmatter={"tipo": "organizacion", "nombre": org["display"], "slug": slug,
|
||||
"tags": ["organizacion", "osint"],
|
||||
"fuente": "NotasDeObsidian/" + (os.path.relpath(ficha_path, NOTAS) if ficha_path else "")},
|
||||
overwrite=True)
|
||||
if ficha_path:
|
||||
docs_del.add(ficha_path)
|
||||
return slug, len(docs_done), ficha_path is not None
|
||||
|
||||
|
||||
def main():
|
||||
all_notes = [n for n in list_obsidian_notes(NOTAS) if "/.git/" not in n and "/dist/" not in n]
|
||||
att_del, docs_del = set(), set()
|
||||
results = []
|
||||
for org in ORGS:
|
||||
results.append((org["display"],) + migrate_org(org, all_notes, att_del, docs_del))
|
||||
for p in docs_del:
|
||||
try: os.remove(p)
|
||||
except FileNotFoundError: pass
|
||||
moved_att = 0
|
||||
for ap in att_del:
|
||||
try:
|
||||
os.remove(ap); moved_att += 1
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
print(f"docs .md movidos: {len(docs_del)} | attachments movidos: {moved_att}\n")
|
||||
for disp, slug, ndocs, had_ficha in results:
|
||||
print(f" {disp} -> organizaciones/{slug} | docs={ndocs} | ficha_origen={'si' if had_ficha else 'no'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Migrador de fichas de persona desde NotasDeObsidian al vault osint.
|
||||
|
||||
Aplica el estandar de projects/osint/CONVENTIONS.md:
|
||||
personas/<slug>.md ficha normalizada
|
||||
personas/<slug>/<doc-slug>.md notas-documento
|
||||
attachments/personas/<slug>/<doc-slug>-N.ext binarios
|
||||
lugares/<slug>.md direcciones extraidas
|
||||
|
||||
Compone funciones del registry (grupo obsidian): slugify_obsidian_name,
|
||||
extract_obsidian_embeds, resolve_obsidian_embed, read/create/delete_obsidian_note.
|
||||
|
||||
Modo por defecto: MOVER (borra docs .md y attachments originales de NotasDeObsidian).
|
||||
Idempotente: re-ejecutar no duplica (overwrite por slug).
|
||||
"""
|
||||
import sys, os, re, shutil, glob
|
||||
|
||||
sys.path.insert(0, "/home/enmanuel/fn_registry/python/functions")
|
||||
from obsidian import (read_obsidian_note, create_obsidian_note, delete_obsidian_note,
|
||||
slugify_obsidian_name, extract_obsidian_embeds, resolve_obsidian_embed,
|
||||
list_obsidian_notes)
|
||||
|
||||
NOTAS = "/home/enmanuel/Obsidian/NotasDeObsidian"
|
||||
OSINT = "/home/enmanuel/Obsidian/osint"
|
||||
|
||||
|
||||
def doc_tipo(ds: str) -> str:
|
||||
if "dni" in ds: return "dni"
|
||||
if "certificado" in ds or "firma" in ds: return "certificado"
|
||||
if "carnet" in ds: return "carnet"
|
||||
if "foto" in ds: return "fotos"
|
||||
if "contrato" in ds: return "contrato"
|
||||
if "nomina" in ds or "laboral" in ds: return "laboral"
|
||||
if any(b in ds for b in ["abanca", "bbva", "cajamar", "unicaja", "ebury", "ovh", "legalitas", "banco"]):
|
||||
return "banco"
|
||||
if any(x in ds for x in ["modelo", "autonomo", "tributaria", "empadron"]): return "fiscal"
|
||||
return "otro"
|
||||
|
||||
|
||||
# Preposiciones/artículos que sobran en un doc-slug tras quitar el nombre de la persona
|
||||
# (p.ej. "DNI de Maria" -> "dni-de" -> "dni").
|
||||
STOPWORDS = {"de", "del", "la", "las", "el", "los", "y", "a", "en"}
|
||||
|
||||
|
||||
def doc_slug(title: str, ptok: set) -> str:
|
||||
parts = [p for p in slugify_obsidian_name(title).split("-")
|
||||
if p and p not in ptok and p not in STOPWORDS]
|
||||
return "-".join(parts) or "documento"
|
||||
|
||||
|
||||
def known_person_slugs() -> set:
|
||||
"""Slugs de todas las personas conocidas (fichas ya en osint/personas)."""
|
||||
slugs = set()
|
||||
for p in list_obsidian_notes(OSINT, subfolder="personas"):
|
||||
base = os.path.splitext(os.path.basename(p))[0]
|
||||
if base.startswith("_"):
|
||||
continue
|
||||
slugs.add(slugify_obsidian_name(base))
|
||||
return slugs
|
||||
|
||||
|
||||
def migrate_person(ficha_path, known_slugs, att_to_delete, docs_to_delete, move=True):
|
||||
f = read_obsidian_note(ficha_path)
|
||||
nombre = os.path.splitext(os.path.basename(ficha_path))[0]
|
||||
slug = slugify_obsidian_name(nombre)
|
||||
ptok = set(slug.split("-"))
|
||||
att_rel = f"attachments/personas/{slug}"
|
||||
att_abs = f"{OSINT}/{att_rel}"
|
||||
os.makedirs(att_abs, exist_ok=True)
|
||||
|
||||
# direccion -> lugar
|
||||
diru = f["frontmatter"].get("direccion") or ""
|
||||
dirtxt = re.sub(r'^\[\[|\]\]$', '', str(diru)).strip()
|
||||
lugar_link = ""
|
||||
if dirtxt:
|
||||
lslug = slugify_obsidian_name(dirtxt)
|
||||
create_obsidian_note(OSINT, f"lugares/{lslug}",
|
||||
body=f"Direccion vinculada a [[{slug}|{nombre}]].",
|
||||
frontmatter={"tipo": "lugar", "nombre": dirtxt, "slug": lslug,
|
||||
"tags": ["lugar", "osint"]}, overwrite=True)
|
||||
lugar_link = f"[[lugares/{lslug}|{dirtxt}]]"
|
||||
|
||||
docs_done, rels, missing = [], [], []
|
||||
for w in f["wikilinks"]:
|
||||
wslug = slugify_obsidian_name(w)
|
||||
if wslug in known_slugs and wslug != slug:
|
||||
rels.append((wslug, w)); continue
|
||||
p = resolve_obsidian_embed(NOTAS, w if w.lower().endswith(".md") else w + ".md")
|
||||
if not p:
|
||||
missing.append(w); continue
|
||||
dn = read_obsidian_note(p)
|
||||
if str(dn["frontmatter"].get("tipo", "")).lower() == "persona":
|
||||
rels.append((wslug, w)); continue
|
||||
ds = doc_slug(w, ptok)
|
||||
embeds = extract_obsidian_embeds(dn["body"])
|
||||
new, n_ok = [], 0
|
||||
for i, emb in enumerate(embeds, 1):
|
||||
ap = resolve_obsidian_embed(NOTAS, emb)
|
||||
if not ap:
|
||||
new.append(f"<!-- attachment no encontrado: {emb} -->"); continue
|
||||
ext = os.path.splitext(ap)[1].lower()
|
||||
nn = f"{ds}-{i}{ext}"
|
||||
shutil.copy2(ap, f"{att_abs}/{nn}")
|
||||
if move:
|
||||
att_to_delete.add(ap)
|
||||
new.append(f"![[{att_rel}/{nn}]]")
|
||||
n_ok += 1
|
||||
create_obsidian_note(OSINT, f"personas/{slug}/{ds}",
|
||||
body="\n".join(new) if new else "(sin attachments)",
|
||||
frontmatter={"tipo": "documento", "doc_tipo": doc_tipo(ds),
|
||||
"persona": f"[[{slug}]]",
|
||||
"fuente": "NotasDeObsidian/" + os.path.relpath(p, NOTAS)},
|
||||
overwrite=True)
|
||||
docs_done.append((w, ds, n_ok))
|
||||
if move:
|
||||
docs_to_delete.add(p)
|
||||
|
||||
# ficha normalizada
|
||||
fm = dict(f["frontmatter"])
|
||||
fm["nombre"], fm["slug"], fm["aliases"] = nombre, slug, [nombre]
|
||||
fm["tags"] = ["persona", "osint"]
|
||||
if dirtxt:
|
||||
fm["direccion"] = dirtxt
|
||||
bl = ["## Documentos", ""] + [f"- [[personas/{slug}/{ds}|{w}]]" for w, ds, _ in docs_done]
|
||||
bl += ["", "## Relaciones", ""] + [f"- [[{rs}|{rn}]]" for rs, rn in rels]
|
||||
if lugar_link:
|
||||
bl += ["", "## Lugares", "", f"- {lugar_link}"]
|
||||
bl += ["", "## Notas", ""]
|
||||
create_obsidian_note(OSINT, f"personas/{slug}", body="\n".join(bl), frontmatter=fm, overwrite=True)
|
||||
|
||||
# borrar ficha plana vieja en osint (nombre con espacios) si el slug difiere
|
||||
if os.path.abspath(ficha_path) != os.path.abspath(f"{OSINT}/personas/{slug}.md") \
|
||||
and "/backups/" not in ficha_path:
|
||||
delete_obsidian_note(ficha_path)
|
||||
|
||||
return dict(slug=slug, docs=len(docs_done), rels=len(rels), missing=missing,
|
||||
att=sum(n for *_, n in docs_done))
|
||||
|
||||
|
||||
def cleanup_enmanuel_originals(att_to_delete):
|
||||
"""Enmanuel se migro en el piloto SIN mover originales (solo copia). Aqui se borran
|
||||
sus docs originales (via campo `fuente` de cada doc en osint) y se marcan sus attachments."""
|
||||
enm = "enmanuel-gutierrez-perez"
|
||||
docs = list_obsidian_notes(OSINT, subfolder=f"personas/{enm}")
|
||||
removed = 0
|
||||
for docp in docs:
|
||||
src = read_obsidian_note(docp)["frontmatter"].get("fuente", "")
|
||||
if not src.startswith("NotasDeObsidian/"):
|
||||
continue
|
||||
orig = NOTAS + "/" + src[len("NotasDeObsidian/"):]
|
||||
if os.path.exists(orig):
|
||||
for emb in extract_obsidian_embeds(read_obsidian_note(orig)["body"]):
|
||||
ap = resolve_obsidian_embed(NOTAS, emb)
|
||||
if ap:
|
||||
att_to_delete.add(ap)
|
||||
os.remove(orig)
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
|
||||
def main():
|
||||
known = known_person_slugs()
|
||||
att_del, docs_del = set(), set()
|
||||
|
||||
# 1. Enmanuel: limpiar originales que el piloto dejo en NotasDeObsidian
|
||||
enm_removed = cleanup_enmanuel_originals(att_del)
|
||||
|
||||
# 2. Fichas pendientes: las que siguen planas en osint/personas (nombre != slug)
|
||||
pend = []
|
||||
for p in list_obsidian_notes(OSINT, subfolder="personas"):
|
||||
base = os.path.splitext(os.path.basename(p))[0]
|
||||
if base.startswith("_"):
|
||||
continue
|
||||
if slugify_obsidian_name(base) != base: # plana sin migrar
|
||||
pend.append(p)
|
||||
|
||||
results = []
|
||||
for fp in pend:
|
||||
results.append(migrate_person(fp, known, att_del, docs_del, move=True))
|
||||
|
||||
# 3. Aplicar borrados (mover): docs .md y attachments originales
|
||||
for p in docs_del:
|
||||
try: os.remove(p)
|
||||
except FileNotFoundError: pass
|
||||
moved_att = 0
|
||||
for ap in att_del:
|
||||
try:
|
||||
os.remove(ap); moved_att += 1
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# 4. Reporte
|
||||
print(f"enmanuel: {enm_removed} docs originales borrados")
|
||||
print(f"personas migradas en este batch: {len(results)}")
|
||||
print(f"docs .md movidos: {len(docs_del)} | attachments movidos: {moved_att}")
|
||||
total_missing = []
|
||||
for r in results:
|
||||
tag = f"{r['slug']}: docs={r['docs']} rels={r['rels']} att={r['att']}"
|
||||
if r["missing"]:
|
||||
tag += f" | missing={r['missing']}"
|
||||
total_missing += [(r['slug'], m) for m in r['missing']]
|
||||
print(" " + tag)
|
||||
if total_missing:
|
||||
print(f"\nlinks sin archivo (placeholders), total {len(total_missing)}:")
|
||||
for s, m in total_missing:
|
||||
print(f" {s} -> {m!r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
vaults:
|
||||
- name: osint
|
||||
description: "Vault Obsidian de investigaciones OSINT hechas con los perfiles de Chromium del navegador (personas, dominios, casos)."
|
||||
path: /home/enmanuel/Obsidian/osint
|
||||
tags: [osint, obsidian, recon, investigation]
|
||||
Reference in New Issue
Block a user