feat: oo_bridge_send — cliente registry del puente OnlyOffice (grupo onlyoffice)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from .caldav_put_event import caldav_put_event
|
|||||||
from .dav_list_resources import dav_list_resources
|
from .dav_list_resources import dav_list_resources
|
||||||
from .dav_get_resource import dav_get_resource
|
from .dav_get_resource import dav_get_resource
|
||||||
from .dav_delete_resource import dav_delete_resource
|
from .dav_delete_resource import dav_delete_resource
|
||||||
|
from .oo_bridge_send import oo_bridge_send
|
||||||
from .pg_insert_rows import pg_insert_rows
|
from .pg_insert_rows import pg_insert_rows
|
||||||
from .pg_apply_sql import pg_apply_sql
|
from .pg_apply_sql import pg_apply_sql
|
||||||
from .pg_query import pg_query
|
from .pg_query import pg_query
|
||||||
@@ -85,4 +86,5 @@ __all__ = [
|
|||||||
"dav_list_resources",
|
"dav_list_resources",
|
||||||
"dav_get_resource",
|
"dav_get_resource",
|
||||||
"dav_delete_resource",
|
"dav_delete_resource",
|
||||||
|
"oo_bridge_send",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: oo_bridge_send
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def oo_bridge_send(cmd: str, ref: str = '', text: str = '', opts: dict = None, target: str = 'cell', port: int = 8791, wait_s: float = 6.0) -> dict"
|
||||||
|
description: "Cliente programatico del puente OnlyOffice (app onlyoffice_bridge): encola un comando de la Automation API contra el documento OnlyOffice Desktop abierto y en foco, hace polling del resultado y lo devuelve como dict no-throw ({status: ok, result} o {status: error, error}). Lee y edita en vivo hojas (Cell), documentos (Word) y presentaciones sin cerrar/reabrir. Solo stdlib."
|
||||||
|
tags: [onlyoffice, bridge, live-edit, http, automation, spreadsheet, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [json, time, urllib.request, urllib.error]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/infra/oo_bridge_send.py"
|
||||||
|
params:
|
||||||
|
- name: cmd
|
||||||
|
desc: "Nombre del comando de la Automation API. Cell lectura: get_cell, get_range, get_formula, get_used_range, get_sheets. Cell escritura: set_cell, set_range, set_formula. Estructura: insert_rows, delete_rows, insert_cols, delete_cols, merge, add_sheet, rename_sheet, delete_sheet, activate_sheet. Formato: set_format, set_borders, set_colwidth, set_rowheight. Datos: add_named_range, sort, autofilter. Graficos/tablas: add_chart, format_as_table, table_chart. Word: insert_text, get_text."
|
||||||
|
- name: ref
|
||||||
|
desc: "Referencia A1 o rango sobre la que actua el comando: celda ('B2'), rango ('A1:C10'), filas ('4:6' para insert_rows/delete_rows), columnas ('B:C' para insert_cols/delete_cols). Vacio para comandos sin ref (get_sheets, add_sheet, get_used_range, insert_text)."
|
||||||
|
- name: text
|
||||||
|
desc: "Valor textual para comandos de escritura: valor de celda (set_cell), formula '=SUM(A1:A9)' (set_formula), nombre de hoja (add_sheet/rename_sheet/activate_sheet), texto a insertar (insert_text). Vacio para lecturas."
|
||||||
|
- name: opts
|
||||||
|
desc: "Dict con parametros extra segun el comando. values (matriz 2D para set_range), sheet (hoja destino por nombre en cualquier comando cell, sin cambiar la activa), bold/italic/underline/fill=[r,g,b]/fontColor=[r,g,b]/numFormat/fontName/fontSize/wrap/alignH/alignV (set_format), style (set_borders), width (set_colwidth), height (set_rowheight), by/desc (sort), type/dataRange/pos (add_chart), dataRange/type (table_chart). None se trata como {}."
|
||||||
|
- name: target
|
||||||
|
desc: "Editor destino: 'cell' (Spreadsheet, por defecto), 'word' (Document), 'slide' (Presentation). El server enruta el comando al editor con foco que coincida."
|
||||||
|
- name: port
|
||||||
|
desc: "Puerto del server puente loopback (server.py). Default 8791."
|
||||||
|
- name: wait_s
|
||||||
|
desc: "Segundos de polling a /pull antes de devolver un error de timeout. Default 6.0. Subir si el editor tarda en responder comandos pesados (add_chart, set_range grande)."
|
||||||
|
output: "Dict no-throw. Exito: {'status': 'ok', 'result': <valor>} donde <valor> es lo que devuelve el plugin (string de celda, matriz 2D para get_range, lista de nombres de hoja para get_sheets, o 'ok' para escrituras). Error: {'status': 'error', 'error': <msg>} si el plugin devolvio {error:...}, si vencio el timeout (editor sin foco o plugin no arrancado), o si hubo fallo de red (server no corriendo). NUNCA lanza excepcion."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join("python", "functions"))
|
||||||
|
from infra import oo_bridge_send
|
||||||
|
|
||||||
|
# Leer una celda de la hoja con foco
|
||||||
|
r = oo_bridge_send("get_cell", ref="B2")
|
||||||
|
print(r) # {'status': 'ok', 'result': '42'}
|
||||||
|
|
||||||
|
# Escribir una celda
|
||||||
|
oo_bridge_send("set_cell", ref="B2", text="123")
|
||||||
|
|
||||||
|
# Escritura batch 2D en un rango
|
||||||
|
oo_bridge_send("set_range", ref="D1:E2",
|
||||||
|
opts={"values": [["a", 1], ["b", 2]]})
|
||||||
|
|
||||||
|
# Formula
|
||||||
|
oo_bridge_send("set_formula", ref="F1", text="=SUM(A1:A9)")
|
||||||
|
|
||||||
|
# Leer un rango entero (matriz 2D)
|
||||||
|
r = oo_bridge_send("get_range", ref="A1:C10")
|
||||||
|
for row in r["result"]:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# Listar hojas y crear una nueva
|
||||||
|
print(oo_bridge_send("get_sheets")["result"]) # ['Hoja1', 'Datos']
|
||||||
|
oo_bridge_send("add_sheet", text="Resumen")
|
||||||
|
|
||||||
|
# Formato: negrita + relleno + formato numerico, sobre otra hoja por nombre
|
||||||
|
oo_bridge_send("set_format", ref="A1:C1",
|
||||||
|
opts={"bold": True, "fill": [230, 230, 250],
|
||||||
|
"numFormat": "0.00%", "sheet": "Datos"})
|
||||||
|
|
||||||
|
# Insertar 3 filas en la fila 4 / borrar columnas B y C
|
||||||
|
oo_bridge_send("insert_rows", ref="4:6")
|
||||||
|
oo_bridge_send("delete_cols", ref="B:C")
|
||||||
|
|
||||||
|
# Ordenar un rango por una columna, descendente
|
||||||
|
oo_bridge_send("sort", ref="A1:D20", opts={"by": "B1", "desc": True})
|
||||||
|
|
||||||
|
# Grafico de barras a partir de un rango
|
||||||
|
oo_bridge_send("add_chart",
|
||||||
|
opts={"type": "bar", "dataRange": "A1:B10", "pos": [5, 1]})
|
||||||
|
|
||||||
|
# Word: insertar y leer texto
|
||||||
|
oo_bridge_send("insert_text", text="Nuevo parrafo", target="word")
|
||||||
|
print(oo_bridge_send("get_text", target="word")["result"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites leer o editar EN VIVO un documento OnlyOffice Desktop ya abierto
|
||||||
|
(hoja de calculo, documento o presentacion) sin cerrarlo ni reabrirlo — por ejemplo
|
||||||
|
para rellenar celdas, aplicar formato, crear graficos o volcar una matriz calculada
|
||||||
|
directamente sobre la ventana que el usuario tiene delante. Es el bloque de
|
||||||
|
construccion de bajo nivel: un comando por llamada. Componla en pipelines o heredocs
|
||||||
|
para secuencias (crear hoja -> volcar datos -> formatear cabecera -> anadir grafico).
|
||||||
|
Prefierela sobre editar el `.xlsx`/`.docx` en disco (`write_xlsx_sheets_py_infra`,
|
||||||
|
etc.) cuando el archivo esta abierto en OnlyOffice, porque el editor Desktop NO
|
||||||
|
recarga cambios externos del disco (GitHub issue #2313).
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **El editor debe estar ABIERTO y con la ventana en FOCO** para que el plugin de
|
||||||
|
sistema arranque y procese comandos. Si no hay foco en el editor destino, el
|
||||||
|
polling vence y devuelve `{"status": "error", "error": "timeout: ..."}`. Abrir con
|
||||||
|
`open_onlyoffice_file_bash_shell` y traer la ventana al frente antes de enviar.
|
||||||
|
- **El server puente (`apps/onlyoffice_bridge/server.py`) debe estar corriendo** en
|
||||||
|
el puerto indicado (`python3 apps/onlyoffice_bridge/server.py 8791 &`, idealmente
|
||||||
|
como service systemd --user). Si no corre, la funcion devuelve
|
||||||
|
`{"status": "error", "error": "push failed: ... (is server.py running ...)"}`.
|
||||||
|
- **Persistencia a disco = Ctrl+S del usuario.** Estas ediciones ocurren sobre el
|
||||||
|
documento vivo en memoria; el guardado automatizado es intermitente. Para dejar el
|
||||||
|
cambio en el archivo, el usuario guarda con Ctrl+S (o usar
|
||||||
|
`save_onlyoffice_file_bash_shell`, que no siempre confirma).
|
||||||
|
- **Tablas nativas (ListObjects): solo se pueden CREAR** con `format_as_table`. Este
|
||||||
|
build de la Automation API NO soporta listar ni re-estilizar tablas nativas
|
||||||
|
existentes — `get_tables`/`set_table_style` no devuelven datos utiles. Para
|
||||||
|
visualizar datos tabulados usa rangos + `set_format` o crea la tabla de cero.
|
||||||
|
- La funcion NUNCA lanza: SIEMPRE inspecciona `result["status"]` antes de usar
|
||||||
|
`result["result"]`. Un `{"status": "ok", "result": None}` significa que el plugin
|
||||||
|
respondio vacio (comando aplicado sin valor de retorno).
|
||||||
|
- `opts.sheet` apunta a una hoja por nombre en cualquier comando cell sin cambiar la
|
||||||
|
hoja activa; util para editar en segundo plano.
|
||||||
|
- Comandos pesados (`set_range` de miles de celdas, `add_chart`) pueden tardar mas de
|
||||||
|
6s; subir `wait_s` para esos casos.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""Programmatic client for the OnlyOffice live bridge (apps/onlyoffice_bridge).
|
||||||
|
|
||||||
|
Speaks to the loopback server (server.py, 127.0.0.1:8791) that queues commands
|
||||||
|
for a system plugin running inside OnlyOffice Desktop Editors. The plugin runs
|
||||||
|
each command with the Automation API (Api.*) over the focused document and
|
||||||
|
answers. This function pushes one command, polls for its result and returns a
|
||||||
|
non-throwing dict — it never raises, so callers (pipelines, heredocs, other
|
||||||
|
functions) can compose it without try/except.
|
||||||
|
|
||||||
|
Only stdlib is used (urllib.request, json, time) so the function is importable
|
||||||
|
from any registry consumer without extra dependencies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
# Monotonic per-process counter combined with a millisecond timestamp keeps the
|
||||||
|
# id unique across quick successive calls without pulling in uuid/random.
|
||||||
|
_counter = [0]
|
||||||
|
|
||||||
|
|
||||||
|
def _next_id() -> str:
|
||||||
|
_counter[0] += 1
|
||||||
|
return "oo%d_%d" % (int(time.time() * 1000), _counter[0])
|
||||||
|
|
||||||
|
|
||||||
|
def oo_bridge_send(
|
||||||
|
cmd: str,
|
||||||
|
ref: str = "",
|
||||||
|
text: str = "",
|
||||||
|
opts: dict = None,
|
||||||
|
target: str = "cell",
|
||||||
|
port: int = 8791,
|
||||||
|
wait_s: float = 6.0,
|
||||||
|
) -> dict:
|
||||||
|
"""Send one command to the OnlyOffice live bridge and return its result.
|
||||||
|
|
||||||
|
Pushes ``{cmd, ref, text, opts, id, target}`` to ``POST /push`` on the
|
||||||
|
loopback bridge server, then long-polls ``GET /pull?id=<id>`` until the
|
||||||
|
plugin answers or ``wait_s`` elapses. The plugin's answer is a JSON string
|
||||||
|
such as ``{"result": ...}`` or ``{"error": "..."}``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Automation-API command name. Cell reads: get_cell, get_range,
|
||||||
|
get_formula, get_used_range, get_sheets. Cell writes: set_cell,
|
||||||
|
set_range, set_formula. Structure: insert_rows, delete_rows,
|
||||||
|
insert_cols, delete_cols, merge, add_sheet, rename_sheet,
|
||||||
|
delete_sheet, activate_sheet. Format: set_format, set_borders,
|
||||||
|
set_colwidth, set_rowheight. Data: add_named_range, sort,
|
||||||
|
autofilter. Charts/tables: add_chart, format_as_table,
|
||||||
|
table_chart. Word: insert_text, get_text.
|
||||||
|
ref: A1 reference or range the command acts on ("B2", "A1:C10",
|
||||||
|
"4:6" for rows, "B:C" for cols). Empty for commands with no ref.
|
||||||
|
text: Text value for write commands (cell value, formula "=SUM(...)",
|
||||||
|
sheet name, inserted paragraph). Empty for reads.
|
||||||
|
opts: Extra parameters as a dict, per command. Common keys: values
|
||||||
|
(2D matrix for set_range), sheet (target sheet by name for any
|
||||||
|
cell command), bold/italic/underline/fill=[r,g,b]/
|
||||||
|
fontColor=[r,g,b]/numFormat/fontName/fontSize/wrap/alignH/alignV
|
||||||
|
(set_format), style (set_borders), width (set_colwidth), height
|
||||||
|
(set_rowheight), by/desc (sort), type/dataRange/pos (add_chart),
|
||||||
|
dataRange/type (table_chart). None is treated as {}.
|
||||||
|
target: Destination editor: "cell" (Spreadsheet), "word" (Document),
|
||||||
|
"slide" (Presentation). Default "cell".
|
||||||
|
port: Loopback bridge server port. Default 8791.
|
||||||
|
wait_s: Seconds to poll /pull before giving up with a timeout error.
|
||||||
|
Default 6.0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A non-throwing dict. On success: ``{"status": "ok", "result": <value>}``
|
||||||
|
where ``<value>`` is whatever the plugin returned (a cell string, a 2D
|
||||||
|
matrix for get_range, a list of sheet names, or "ok" for writes). On
|
||||||
|
failure: ``{"status": "error", "error": <message>}`` when the plugin
|
||||||
|
replied with an error, when the poll timed out (editor not focused or
|
||||||
|
plugin not started), or on any network failure (server not running).
|
||||||
|
Never raises.
|
||||||
|
"""
|
||||||
|
base = "http://127.0.0.1:%d" % port
|
||||||
|
rid = _next_id()
|
||||||
|
payload = {
|
||||||
|
"cmd": cmd,
|
||||||
|
"ref": ref,
|
||||||
|
"text": text,
|
||||||
|
"opts": opts if opts is not None else {},
|
||||||
|
"id": rid,
|
||||||
|
"target": target,
|
||||||
|
}
|
||||||
|
net_timeout = max(wait_s, 2.0)
|
||||||
|
|
||||||
|
# 1) Enqueue the command on the bridge server.
|
||||||
|
try:
|
||||||
|
push_body = json.dumps(payload).encode("utf-8")
|
||||||
|
push_req = urllib.request.Request(
|
||||||
|
base + "/push",
|
||||||
|
data=push_body,
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(push_req, timeout=net_timeout) as resp:
|
||||||
|
resp.read()
|
||||||
|
except Exception as exc: # noqa: BLE001 — no-throw contract
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "push failed: %s (is server.py running on port %d?)"
|
||||||
|
% (exc, port),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2) Poll for the plugin's answer until it is ready or wait_s elapses.
|
||||||
|
deadline = time.time() + wait_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
pull_req = urllib.request.Request(
|
||||||
|
base + "/pull?id=" + rid, method="GET"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(pull_req, timeout=net_timeout) as resp:
|
||||||
|
res = json.loads(resp.read().decode("utf-8"))
|
||||||
|
except Exception as exc: # noqa: BLE001 — no-throw contract
|
||||||
|
return {"status": "error", "error": "pull failed: %s" % exc}
|
||||||
|
|
||||||
|
if res.get("ready"):
|
||||||
|
raw = res.get("text", "")
|
||||||
|
if not raw:
|
||||||
|
return {"status": "ok", "result": None}
|
||||||
|
try:
|
||||||
|
parsed = json.loads(raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Plugin returned non-JSON text; hand it back raw.
|
||||||
|
return {"status": "ok", "result": raw}
|
||||||
|
if isinstance(parsed, dict) and "error" in parsed:
|
||||||
|
return {"status": "error", "error": parsed["error"]}
|
||||||
|
if isinstance(parsed, dict) and "result" in parsed:
|
||||||
|
return {"status": "ok", "result": parsed["result"]}
|
||||||
|
return {"status": "ok", "result": parsed}
|
||||||
|
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "timeout: no reply in %.1fs "
|
||||||
|
"(editor not focused or plugin not started)" % wait_s,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user