Bỏ qua, tới nội dung
Tài liệu APIv1

O3O DocBuilder và chuyển đổi

Tổng quan O3O DocBuilder

O3O DocBuilder là API REST xử lý tài liệu phía máy chủ: chuyển đổi định dạng, dựng văn bản, bảng tính và trình chiếu từ kịch bản JSON, điền mẫu, trích văn bản, siêu dữ liệu và ảnh thu nhỏ, không cần mở trình soạn thảo.

Trong trang này

DocBuilder là một dịch vụ riêng trong bản Docker của O3O Office Online, phục vụ ở đường dẫn /v1/ phía sau proxy. Hệ thống của bạn gửi yêu cầu HTTP từ máy chủ; DocBuilder trả về tệp kết quả hoặc dữ liệu JSON. Không có giao diện, không có phiên soạn thảo, người dùng cuối không cần mở trình duyệt.

Dùng khi nào#

Bạn cầnEndpointXem
Đổi định dạng: docx sang PDF, xlsx sang CSV, một trang chiếu thành ảnhPOST /v1/convertChuyển đổi định dạng
Sinh báo giá, bảng lương, bộ trình chiếu từ dữ liệu có sẵn trong hệ thốngPOST /v1/buildDựng tài liệu, o3oscript
Điền dữ liệu vào mẫu Word do bộ phận nghiệp vụ tự soạnPOST /v1/template/renderĐiền mẫu (bản doanh nghiệp)
Lấy chữ để đánh chỉ mục tìm kiếm, đọc siêu dữ liệu, tạo ảnh thu nhỏPOST /v1/extract/text, POST /v1/extract/meta, POST /v1/extract/thumbnailTrích xuất
Chạy việc lâu hoặc nhiều tệp ra mà không giữ kết nối HTTPGET /v1/jobs/{id}, GET /v1/files/{id}Việc bất đồng bộ

Cách DocBuilder chạy trong v1#

  • Một container o3o-docbuilder: Debian bookworm, LibreOffice cài từ kho của Debian, python3-uno, poppler-utils và FastAPI, cổng nội bộ 8060.
  • Bên trong là một hồ chứa O3O_DOCBUILDER_WORKERS tiến trình LibreOffice chạy không giao diện (mặc định min(số nhân CPU, 4), đặt được từ 1 tới 32). Mỗi tiến trình có hồ sơ người dùng riêng và chỉ làm một việc tại một thời điểm.
  • Hàng đợi và trạng thái job nằm trong bộ nhớ. Khởi động lại dịch vụ thì job đang chờ và đang chạy bị mất; job đã xong vẫn tải được tệp cho tới khi hết thời gian giữ.
  • Worker được dựng lại sau O3O_DOCBUILDER_WORKER_MAX_JOBS việc (mặc định 200), sau O3O_DOCBUILDER_WORKER_MAX_AGE_MINUTES phút (mặc định 60) và sau mọi lần quá giờ hoặc gặp sự cố.
  • Macro không bao giờ chạy: mọi tệp nạp qua LibreOffice đều ở chế độ không chạy macro và không cập nhật liên kết ngoài. Riêng điền mẫu không nạp mẫu qua LibreOffice, nên mẫu có macro bị từ chối ngay với 422 macro_not_allowed, xem Điền mẫu.
  • Phiên bản LibreOffice thật sự đang chạy được báo ở GET /v1/status, trường core. DocBuilder v1 dùng LibreOffice của Debian, chưa dùng lõi O3O 26.8.

Giấy phép của phần mềm nguồn mở trong image#

  • LibreOffice là phần mềm mã nguồn mở do The Document Foundation quản lý, cùng cộng đồng người đóng góp. Giấy phép chính là MPL 2.0; một số tệp theo Apache 2.0, LGPL và các giấy phép khác của bên thứ ba. Image cài nguyên gói của Debian bookworm, không sửa mã LibreOffice.
  • Các gói khác trong image giữ giấy phép riêng của gói, ví dụ poppler-utils theo GPL. Văn bản giấy phép của từng gói nằm ngay trong image, ở /usr/share/doc/<tên-gói>/copyright.
  • Mã nguồn đúng phiên bản của các gói lấy tại kho mã nguồn của Debian bookworm (sources.debian.org). Khi phân phối lại image cho người khác, hãy giữ nguyên các tệp giấy phép và cho người nhận biết cách lấy mã nguồn. Tóm tắt nghĩa vụ MPL 2.0 ở trang Dựng từ nguồn; phần này không phải tư vấn pháp lý.
  • Mã riêng của O3O DocBuilder (dịch vụ API, hồ worker, điền mẫu, trình dựng o3oscript) do O3O sở hữu và không sửa mã LibreOffice. Image phân phối KÈM LibreOffice, nên khi phân phối image phải kèm thông báo giấy phép của LibreOffice và các gói Debian, và chỉ rõ nơi lấy mã nguồn của chúng.
  • Tên LibreOffice là nhãn hiệu của The Document Foundation; tài liệu này chỉ dùng tên đó để gọi đúng thành phần đang chạy bên trong DocBuilder.

Địa chỉ gốc#

Mọi ví dụ dùng máy chủ mẫu http://localhost:8080, là proxy của bản Docker trên máy DEV; proxy chuyển mọi đường dẫn /v1/ tới DocBuilder. Khi chạy thật, thay bằng địa chỉ công khai của bạn (biến O3O_PUBLIC_URL). Proxy không đệm thân yêu cầu và chờ tối đa 660 giây; giới hạn cỡ tệp do DocBuilder tự áp theo gói.

Xác thực#

Mọi endpoint, trừ GET /v1/status, yêu cầu header Authorization: Bearer <giá trị>. Giá trị là một trong hai loại dưới đây.

LoạiKhai báo trên máy chủQuy tắc
Khoá APIO3O_DOCBUILDER_API_KEYS: danh sách phân tách bằng dấu phẩy, mỗi phần tử dạng ten:khoa hoặc chỉ khoa.Khớp ^[A-Za-z0-9_-]{24,128}$, nên bắt đầu bằng o3o_. Máy chủ so sánh bằng hàm thời gian hằng.
JWT HS256 (tuỳ chọn)O3O_DOCBUILDER_JWT_SECRET, tối thiểu 32 ký tự. Để trống thì đường JWT tắt.Giá trị có đúng hai dấu chấm được coi là JWT. Bắt buộc có exp, thời hạn còn lại không quá 24 giờ, cho phép lệch đồng hồ 60 giây. sub (tuỳ chọn) làm nhãn trong nhật ký. Thuật toán khác HS256, kể cả none, bị từ chối.

Cấp JWT ngắn hạn#

Dùng JWT khi muốn cấp cho từng hệ thống con một quyền gọi có thời hạn thay vì chia sẻ khoá API dài hạn. Máy chủ của bạn và DocBuilder dùng chung khoá bí mật O3O_DOCBUILDER_JWT_SECRET; ví dụ dưới ký một JWT sống một giờ rồi gọi GET /v1/limits.

Ký JWT HS256 rồi gọi API
import os
import time

import jwt  # gói PyJWT
import requests

secret = os.environ["O3O_DOCBUILDER_JWT_SECRET"]
token = jwt.encode({"sub": "crm-noi-bo", "exp": int(time.time()) + 3600}, secret, algorithm="HS256")

r = requests.get("http://localhost:8080/v1/limits", headers={"Authorization": f"Bearer {token}"}, timeout=30)
print(r.status_code, r.json())
import { createHmac } from "node:crypto";

const b64url = (data) => Buffer.from(data).toString("base64url");

function signJwt(secret, sub, ttlSeconds = 3600) {
  const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
  const payload = b64url(JSON.stringify({ sub, exp: Math.floor(Date.now() / 1000) + ttlSeconds }));
  const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
  return `${header}.${payload}.${signature}`;
}

const token = signJwt(process.env.O3O_DOCBUILDER_JWT_SECRET, "crm-noi-bo");
const res = await fetch("http://localhost:8080/v1/limits", { headers: { Authorization: `Bearer ${token}` } });
console.log(res.status, await res.json());
<?php
function b64url(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function sign_jwt(string $secret, string $sub, int $ttl = 3600): string
{
    $header = b64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
    $payload = b64url(json_encode(['sub' => $sub, 'exp' => time() + $ttl]));
    $signature = b64url(hash_hmac('sha256', "$header.$payload", $secret, true));
    return "$header.$payload.$signature";
}

$token = sign_jwt(getenv('O3O_DOCBUILDER_JWT_SECRET'), 'crm-noi-bo');
$ch = curl_init('http://localhost:8080/v1/limits');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
    CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

static string B64Url(byte[] data) =>
    Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

static string SignJwt(string secret, string sub, int ttlSeconds = 3600)
{
    var header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
    var exp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ttlSeconds;
    var payload = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { sub, exp }));
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var signature = B64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{header}.{payload}")));
    return $"{header}.{payload}.{signature}";
}

var token = SignJwt(Environment.GetEnvironmentVariable("O3O_DOCBUILDER_JWT_SECRET")!, "crm-noi-bo");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync("http://localhost:8080/v1/limits"));
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }

SECRET="$O3O_DOCBUILDER_JWT_SECRET"
HEADER=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url)
PAYLOAD=$(printf '{"sub":"crm-noi-bo","exp":%d}' "$(( $(date +%s) + 3600 ))" | b64url)
SIGNATURE=$(printf '%s.%s' "$HEADER" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
TOKEN="$HEADER.$PAYLOAD.$SIGNATURE"

curl -sS http://localhost:8080/v1/limits -H "Authorization: Bearer $TOKEN"

Gói và hạn mức#

Gói của DocBuilder được xác định từ token bản quyền đã nạp vào máy chủ: là bản doanh nghiệp khi token ở trạng thái valid hoặc grace và có tính năng api_full; mọi trường hợp khác là bản cộng đồng. Hạn mức tính chung cho cả instance, không theo từng khoá API. Hai biến O3O_DOCBUILDER_MAX_FILE_MBO3O_DOCBUILDER_JOB_TIMEOUT_SECONDS chỉ có thể hạ giới hạn của gói, không nâng được.

Hạng mụcBản cộng đồngBản doanh nghiệp
Yêu cầu tính hạn mức mỗi phút (rate_per_minute)10clamp(60 * ceil(conns / 50), 60, 3000), trong đó conns là số kết nối trong token
Yêu cầu tính hạn mức mỗi ngày (rate_per_day)200không giới hạn
Job chạy song song (parallel_jobs)1bằng số worker (O3O_DOCBUILDER_WORKERS)
Job chờ tối đa trong hàng đợi (max_queued_jobs)101.000
Cỡ tệp vào hoặc ra tối đa (max_file_mb)10 MB300 MB
Thời gian giữ job và tệp kết quả (result_ttl_minutes)15 phút1.440 phút (24 giờ)
Thời gian chờ tối đa của chế độ đồng bộ (sync_timeout_seconds)60 giây60 giây
Thời gian chạy tối đa của một job (job_timeout_seconds)120 giây600 giây
Chế độ bất đồng bộ (async)
Callback khi job xong (callback)khôngcó, khi token có tính năng callback
Đơn vị kịch bản tối đa (max_script_units)50020.000
Số tệp ra tối đa mỗi lần dựng (max_outputs_per_build)15

Bản cộng đồng dùng được mọi endpoint trừ POST /v1/template/render. Hạn mức mỗi phút của bản doanh nghiệp theo số kết nối trong token, ví dụ: 50 → 60, 120 → 180, 200 → 240, 201 → 300, 500 → 600, 800 → 960, 2.500 → 3.000, 5.000 → 3.000.

  • Chỉ các yêu cầu làm việc bị tính: POST /v1/convert, POST /v1/build, POST /v1/template/render, POST /v1/extract/text, POST /v1/extract/meta, POST /v1/extract/thumbnail.
  • Mọi yêu cầu GET, kể cả thăm dò job và tải tệp, không bị tính. Yêu cầu bị từ chối vì 401 hoặc 403 cũng không bị tính.
  • Cửa sổ phút là phút đồng hồ theo UTC. Cửa sổ ngày đặt lại lúc 00:00 giờ Việt Nam (UTC+7).
  • Endpoint hoặc tuỳ chọn không thuộc gói trả 403 forbidden_feature, ví dụ gửi callback_url khi đang ở bản cộng đồng.

Header giới hạn tần suất#

HeaderNội dung
X-RateLimit-LimitSố yêu cầu tính hạn mức được phép mỗi phút.
X-RateLimit-RemainingSố còn lại trong phút hiện tại.
X-RateLimit-ResetGiây Unix khi cửa sổ phút đặt lại.
X-RateLimit-Limit-DaySố yêu cầu mỗi ngày; vắng mặt khi gói không giới hạn theo ngày.
X-RateLimit-Remaining-DaySố còn lại trong ngày; vắng mặt khi gói không giới hạn theo ngày.
Retry-AfterChỉ có ở 429503: số giây nên chờ trước khi gửi lại.

Xem hạn mức đang áp dụng#

GET/v1/limits

Hạn mức của gói hiện hành (đã tính ra số, đã áp các biến môi trường chỉ-hạ) và mức đã dùng.

Xác thực: BearerCộng đồngDoanh nghiệp

Tham số

  • Authorizationheaderbắt buộc
    Bearer <khoá API hoặc JWT>.
Đọc hạn mức
curl -sS http://localhost:8080/v1/limits \
  -H "Authorization: Bearer O3O_DEMO_KEY"
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };

const res = await fetch(`${BASE_URL}/v1/limits`, { headers: HEADERS });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());
import requests

BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}

r = requests.get(f"{BASE_URL}/v1/limits", headers=HEADERS, timeout=30)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())
<?php
$ch = curl_init("http://localhost:8080/v1/limits");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
print_r(json_decode($body, true));
using System.Net.Http.Headers;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");

using var res = await http.GetAsync("http://localhost:8080/v1/limits");
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());
200Bản cộng đồng. Khoá của limits trùng tên với mục docbuilder trong plans.json; usage cho biết mức đã dùng trong phút và trong ngày.
{
  "edition": "community",
  "limits": {
    "rate_per_minute": 10,
    "rate_per_day": 200,
    "parallel_jobs": 1,
    "max_queued_jobs": 10,
    "max_file_mb": 10,
    "result_ttl_minutes": 15,
    "sync_timeout_seconds": 60,
    "job_timeout_seconds": 120,
    "async": true,
    "callback": false,
    "max_script_units": 500,
    "max_outputs_per_build": 1
  },
  "usage": {
    "minute": {
      "limit": 10,
      "used": 3,
      "remaining": 7,
      "reset_at": "2026-09-21T10:01:00Z"
    },
    "day": {
      "limit": 200,
      "used": 41,
      "remaining": 159,
      "reset_at": "2026-09-21T17:00:00Z"
    },
    "running_jobs": 0,
    "queued_jobs": 0
  },
  "allowed_endpoints": [
    "GET /v1/status",
    "GET /v1/formats",
    "GET /v1/limits",
    "POST /v1/convert",
    "GET /v1/jobs/{id}",
    "GET /v1/files/{id}",
    "POST /v1/build",
    "POST /v1/extract/text",
    "POST /v1/extract/meta",
    "POST /v1/extract/thumbnail"
  ],
  "features": []
}

Lỗi thường gặp: 401 unauthorized khi thiếu hoặc sai khoá, 401 token_expired khi JWT quá hạn. Endpoint GET không bị tính vào hạn mức.

Kiểm tra sức khoẻ#

GET/v1/status

Trạng thái dịch vụ. Không cần xác thực, không chứa bí mật.

Xác thực: không cầnCộng đồngDoanh nghiệp

Tham số

  • Authorizationheadertuỳ chọn
    Không cần. Endpoint này không yêu cầu xác thực.
Hỏi trạng thái
curl -sS http://localhost:8080/v1/status
const BASE_URL = "http://localhost:8080";

const res = await fetch(`${BASE_URL}/v1/status`);
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());
import requests

BASE_URL = "http://localhost:8080"

r = requests.get(f"{BASE_URL}/v1/status", timeout=30)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())
<?php
$ch = curl_init("http://localhost:8080/v1/status");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
print_r(json_decode($body, true));
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };

using var res = await http.GetAsync("http://localhost:8080/v1/status");
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());
200Dịch vụ đang chạy. core là lõi LibreOffice thật sự đang chạy (giá trị trong ví dụ chỉ minh hoạ); workers.restarts đếm số lần worker bị dựng lại từ khi khởi động.
{
  "service": "o3o-docbuilder",
  "version": "1.0.0",
  "api": "v1",
  "time": "2026-09-21T10:00:00Z",
  "edition": "community",
  "dev_mode": false,
  "core": {
    "name": "LibreOffice",
    "version": "7.4.7.2",
    "source": "debian-bookworm"
  },
  "workers": {
    "total": 2,
    "idle": 2,
    "busy": 0,
    "restarts": 0
  },
  "queue": {
    "queued": 0,
    "running": 0
  },
  "uptime_seconds": 3600
}

Dùng endpoint này làm kiểm tra sức khoẻ của container. Nếu proxy trả 502 thì container DocBuilder chưa chạy hoặc chưa sẵn sàng.

Định dạng lỗi#

Mọi lỗi ở mọi endpoint có cùng một dạng thân JSON. code là chuỗi ổn định để chương trình xử lý; message là câu tiếng Việt cho người đọc; detail là đối tượng (có thể rỗng) và không bao giờ chứa nội dung tài liệu; request_id trùng header X-O3O-Request-Id.

415Ví dụ lỗi chuyển khác họ tài liệu.
{
  "error": {
    "code": "unsupported_format",
    "message": "Không chuyển được từ docx sang xlsx.",
    "detail": {
      "from": "docx",
      "to": "xlsx"
    },
    "request_id": "req_0123456789abcdef"
  }
}
HTTPKhi nào
400bad_requestThiếu tham số, JSON hỏng, options sai kiểu, thiếu cả file lẫn url. detail.errors = [{path, message}].
401unauthorizedThiếu hoặc sai khoá. detail.reasonmissing, invalid hoặc no_keys_configured.
401token_expiredJWT đã quá hạn.
403forbidden_featureEndpoint hoặc tuỳ chọn không thuộc gói hiện hành. detail.feature, detail.edition.
404not_foundKhông có job hoặc tệp với mã đó.
410goneJob hoặc tệp đã hết thời gian giữ.
413file_too_largeTệp vào hoặc tệp ra vượt max_file_mb. detail.limit_mb.
415unsupported_formatKhông nhận ra định dạng nguồn, hoặc cặp nguồn và đích không được hỗ trợ.
422script_invalidKịch bản sai JSON Schema. detail.errors = [{path, message}], path là JSON Pointer.
422script_too_largeSố đơn vị kịch bản vượt max_script_units. detail.units, detail.limit.
422script_errorKịch bản hợp lệ nhưng thực thi lỗi. detail.path là JSON Pointer tới phần tử gây lỗi.
422template_errorThẻ lặp không cân, hoặc thiếu trường khi missing = "error". detail.fields.
422macro_not_allowedMẫu gửi tới POST /v1/template/render có macro: tệp OOXML có phần vbaProject.bin hoặc kiểu nội dung macroEnabled, tệp ODF có thư mục Basic/ hay Scripts/. detail.reason, detail.part.
422corrupt_sourceLibreOffice không mở được tệp nguồn.
422password_requiredTệp có mật khẩu mà không truyền mật khẩu, hoặc mật khẩu sai.
422url_not_allowedURL vi phạm quy tắc chống SSRF.
422download_failedKhông tải được URL nguồn.
429rate_limitedVượt hạn mức. detail.windowminute hoặc day; kèm Retry-After.
500internalLỗi không lường trước.
503queue_fullHàng đợi đã đầy (max_queued_jobs); kèm Retry-After.
503pool_unavailableKhông còn worker nào sống.
504timeoutChế độ đồng bộ quá sync_timeout_seconds, hoặc job quá job_timeout_seconds. Worker bị dừng và dựng lại.

Mã định danh#

Đối tượngDạngVí dụ
Jobjob_ + 24 ký tự hexjob_5f0c2a9e41b7d3c8a6e1f024
Tệp kết quảfile_ + 24 ký tự hexfile_9b3e7d21c4a8f0e65d1b2c37
Yêu cầureq_ + 16 ký tự hexreq_0123456789abcdef

Sắp có#

Sắp có

Chưa có trong v1: điền mẫu hàng loạt (/v1/template/render-batch), soi trường của mẫu (/v1/template/inspect), so sánh tài liệu (/v1/compare), trích cấu trúc và ảnh nhúng (/v1/extract/structure, /v1/extract/media), tải tệp lên trước (POST /v1/files), huỷ job, PDF có mật khẩu, bộ lọc ngày và tiền trong mẫu, điều kiện và ảnh trong mẫu, biểu đồ trong o3oscript, SDK chính thức cho các ngôn ngữ, lớp tương thích /compat/*, hàng đợi bền vững qua khởi động lại, dùng lõi O3O 26.8 thay LibreOffice của Debian.

Đọc tiếp#