Zautomatyzuj typowe zadania centrum pomocy za pomocą interfejsu API Notiondesk

Przykłady API Notiondesk: synchronizacja z GitHub Actions, tworzenie kopii zapasowych artykułów w formacie Markdown, publikowanie opinii w Slacku i zgłaszanie wyszukiwań, które nie dały żadnych wyników.

7 min czytania

W tym artykule zebrano gotowe do użycia skrypty API Notiondesk do zadań, które zespoły automatyzują najczęściej: publikowanie edycji Notion z potoku CI, tworzenie kopii zapasowych każdego artykułu w formacie Markdown, wysyłanie negatywnych opinii do Slacka, zgłaszanie wyszukiwań, na które centrum pomocy nie potrafi odpowiedzieć, wyszukiwanie nieudanych synchronizacji oraz stosowanie jednej marki do kilku centrów pomocy. Skopiuj przepis, ustaw dwie zmienne środowiskowe i uruchom go.

Zanim zaczniesz

  • Ustaw oba jako zmienne środowiskowe:
export NOTIONDESK_API_KEY="nd_wsk_..."
export NOTIONDESK_SITE_ID="c74781f6-1aea-4e17-ab23-bbb8b4d174b6"

Przepisy JavaScript wymagają Node.js 18 lub nowszego.

Mały, wspólny pomocnik

Zapisz ten plik jako notiondesk.mjs obok swoich skryptów. Wysyła on nagłówki, czeka na odpowiedź API 429 rate_limited i odczytuje każdą stronę listy.

Synchronizuj swoje centrum pomocy z GitHub Actions

Zakres: sync

Publikuj swoje zmiany w Notionie z poziomu przepływu pracy GitHub Actions, na przykład zaraz po wydaniu. Zapisz klucz jako sekret repozytorium NOTIONDESK_API_KEY, a identyfikator centrum pomocy jako zmienną repozytorium NOTIONDESK_SITE_ID.

export const SITE_ID = process.env.NOTIONDESK_SITE_ID;

export async function api(path, options = {}) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(`https://api.notiondesk.so${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${process.env.NOTIONDESK_API_KEY}`,
        "nd-version": "2026-10-16",
        ...options.headers,
      },
    });

    if (response.status === 429) {
      const seconds = Number(response.headers.get("Retry-After")) || 5;
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (!response.ok) {
      throw new Error(`${response.status} ${await response.text()}`);
    }
    return response.status === 204 ? null : response.json();
  }
  throw new Error(`Still rate limited on ${path}`);
}

export async function listAll(path) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(`https://api.notiondesk.so${path}`);
    url.searchParams.set("page_size", "100");
    if (cursor) {
      url.searchParams.set("cursor", cursor);
    }
    const page = await api(`${url.pathname}${url.search}`);
    items.push(...page.data);
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
  return items;
}
name: Sync help center
on:
  workflow_dispatch:
  release:
    types: [published]

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger a Notiondesk sync
        run: |
          status=$(curl -s -o response.json -w "%{http_code}" -X POST \
            "https://api.notiondesk.so/sites/${{ vars.NOTIONDESK_SITE_ID }}/sync" \
            -H "Authorization: Bearer ${{ secrets.NOTIONDESK_API_KEY }}" \
            -H "nd-version: 2026-10-16" \
            -H "Idempotency-Key: github-${{ github.run_id }}-${{ github.run_attempt }}")
          if [ "$status" = "409" ]; then echo "A sync is already running."; exit 0; fi
          if [ "$status" != "204" ]; then cat response.json; exit 1; fi
          echo "Sync queued."
  • 204 oznacza, że ​​synchronizacja jest w kolejce. Działa w tle.
  • 409 conflict_error oznacza, że ​​synchronizacja centrum pomocy jest już w kolejce lub jest w trakcie wykonywania, więc krok zostanie wykonany pomyślnie bez konieczności uruchamiania kolejnego.
  • Idempotency-Key sprawia, że ​​ponowna próba kolejki żądań staje się dwukrotnie nieważna.
Gdy automatyczne tłumaczenie jest włączone, każda synchronizacja tłumaczy zmienioną treść i rozlicza ją. Uruchom przepływ pracy po faktycznych edycjach, a nie według krótkiego harmonogramu. Zobacz: Wyzwalanie synchronizacji Notion z API Notiondesk .

Twórz kopie zapasowe każdego artykułu w formacie Markdown

Zakres: read

Zapisz każdy artykuł, w każdym języku obsługiwanym przez Twoje centrum pomocy, jako plik Markdown:

Kopia zapasowa obejmuje wersje robocze i zarchiwizowane artykuły. Odczyty są ograniczone do 120 żądań na minutę, więc centrum pomocy z 500 artykułami na język potrzebuje około pięciu minut; pomocnik czeka automatycznie po osiągnięciu limitu.

Wysyłaj negatywne opinie do Slacka każdego dnia

Zakres: tylko read

Opublikuj negatywne opinie z ostatnich 24 godzin na kanale Slack za pomocą przychodzącego webhooka . Ustaw SLACK_WEBHOOK_URL, a następnie uruchamiaj skrypt raz dziennie.

Najnowsza pozycja na liście opinii jest wyświetlana jako pierwsza, więc skrypt przestaje wyświetlać stronę przy pierwszym wpisie starszym niż 24 godziny.

import { mkdir, writeFile } from "node:fs/promises";
import { api, listAll, SITE_ID } from "./notiondesk.mjs";

const site = await api(`/sites/${SITE_ID}`);

for (const locale of site.supported_locales) {
  await mkdir(`backup/${locale}`, { recursive: true });
  const articles = await listAll(`/sites/${SITE_ID}/articles?locale=${locale}`);

  for (const article of articles) {
    const full = await api(`/sites/${SITE_ID}/articles/${encodeURIComponent(article.id)}?locale=${locale}`);
    const frontMatter = `---\ntitle: ${JSON.stringify(full.title ?? "")}\nstatus: ${full.status}\nurl: ${full.url ?? ""}\n---\n\n`;
    await writeFile(`backup/${locale}/${encodeURIComponent(article.id)}.md`, frontMatter + full.text);
    if (full.truncated) {
      console.warn(`${locale}/${article.id} is longer than 200,000 bytes and was truncated`);
    }
  }
  console.log(`${locale}: ${articles.length} articles saved`);
}
import { api, SITE_ID } from "./notiondesk.mjs";

const since = Date.now() - 24 * 60 * 60 * 1000;
const escapeSlack = (text) => (text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

async function feedbackSince(timestamp) {
  const entries = [];
  let cursor = null;
  do {
    const query = new URLSearchParams({ page_size: "100" });
    if (cursor) {
      query.set("cursor", cursor);
    }
    const page = await api(`/sites/${SITE_ID}/feedback?${query}`);
    const recent = page.data.filter((entry) => Date.parse(entry.submitted_at) >= timestamp);
    entries.push(...recent);
    cursor = page.has_more && recent.length === page.data.length ? page.next_cursor : null;
  } while (cursor);
  return entries;
}

const negative = (await feedbackSince(since)).filter(
  (entry) => entry.reaction_group === "negative" || entry.reaction_group === "not_helpful",
);

if (negative.length > 0) {
  const lines = negative.map(
    (entry) => `• *${escapeSlack(entry.article_title)}* (${entry.article_locale}): ${escapeSlack(entry.message) || "_no message_"}`,
  );
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: `${negative.length} negative feedback in the last 24 hours\n${lines.join("\n")}` }),
  });
}
Opinie są pisane przez czytelników. Uruchom ten skrypt z kluczem zawierającym tylko , użyj znaku ucieczki przed wysłaniem tekstu i nigdy nie przekazuj go do LLM, który przechowuje klucz z możliwością zapisu. Zobacz Eksportowanie opinii czytelników za pomocą API Notiondesk .read

Zgłaszaj wyszukiwania bez rezultatów co tydzień

Zakres: read

Wypisz, czego czytelnicy szukali w zeszłym tygodniu, ale nie znaleźli artykułu. Każde zapytanie jest kandydatem na nowy artykuł lub lepszy tytuł.

Każda lista zapytań może pomieścić do 50 wierszy. Raporty analityczne mają również wspólny budżet 30 żądań na minutę dla każdego centrum pomocy. Zobacz Uzyskaj dostęp do analityki w centrum pomocy za pomocą interfejsu API Notiondesk .

Znajdź artykuły, których ostatnia synchronizacja się nie powiodła

Zakres: read, plus sync w celu ponowienia próby

Najpierw popraw każdy artykuł w Notion, a następnie zsynchronizuj go ponownie z POST /sites/{site_id}/articles/{article_id}/sync.

Zastosuj jedną markę do każdego centrum pomocy

Zakres: read i admin

Skopiuj kolory i czcionki z jednego centrum pomocy do wszystkich pozostałych centrów pomocy w obszarze roboczym. Ustaw SOURCE_SITE_ID jako centrum pomocy, z którego chcesz kopiować.

Każda aktualizacja brandingu odświeża całe docelowe centrum pomocy, dlatego skrypt wysyła jedno żądanie do każdego centrum pomocy. Kolor zapisany w skróconej formie, takiej jak #fff, jest odrzucany: należy go najpierw poprawić w panelu źródłowego centrum pomocy. Zobacz „Dostosowywanie brandingu i motywów centrum pomocy za pomocą interfejsu API Notiondesk” .

Przekierowania importu po migracji

Zakres: read i write

Przenosisz się z innego narzędzia Centrum pomocy? Skrypt importu przekierowań odczytuje plik CSV ze starymi ścieżkami i tworzy jedno przekierowanie na wiersz. Zobacz Zarządzanie przekierowaniami stron za pomocą interfejsu API Notiondesk .

import { api, SITE_ID } from "./notiondesk.mjs";

const end = new Date();
end.setUTCHours(0, 0, 0, 0);
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
const toApiDate = (date) => `${date.toISOString().slice(0, 19)}Z`;

const query = new URLSearchParams({
  start_date: toApiDate(start),
  end_date: toApiDate(end),
  date_scale: "day",
});
const report = await api(`/sites/${SITE_ID}/analytics/search?${query}`);

console.log(`Searches: ${report.searches.total} (${report.searches.percentage_difference}% vs previous week)`);
console.log(`Without results: ${report.searches_without_results.total}`);
console.log(`Click-through rate: ${report.click_through_rate.current.toFixed(1)}%`);

for (const { query: text, searches } of report.searches_without_results.queries) {
  console.log(`${searches}\t${text}`);
}
import { api, listAll, SITE_ID } from "./notiondesk.mjs";

const articles = await listAll(`/sites/${SITE_ID}/articles`);
const failed = articles.filter((article) => article.sync_failed || article.sync_timeout);

for (const article of failed) {
  const reason = article.sync_timeout ? "timed out" : article.sync_error_code ?? "failed";
  console.log(`${article.title}: ${reason} ${article.notion_article_page_url ?? ""}`);
}
import { createHash } from "node:crypto";
import { api, listAll } from "./notiondesk.mjs";

const sourceId = process.env.SOURCE_SITE_ID;
const { colors, primary_font, secondary_font } = await api(`/sites/${sourceId}/settings/customize`);
const body = JSON.stringify({ colors, primary_font, secondary_font });
const bodyHash = createHash("sha256").update(body).digest("hex");
const sites = await listAll("/sites");

for (const site of sites.filter((candidate) => candidate.id !== sourceId)) {
  await api(`/sites/${site.id}/settings/customize`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", "Idempotency-Key": `brand-${site.id}-${bodyHash}` },
    body,
  });
  console.log(`Updated ${site.full_domain}`);
}

Często zadawane pytania

Jakiego języka mogę używać z API Notiondesk?

Dowolny język, który może wysyłać żądania HTTPS. API to zwykły REST z JSON, więc curl, JavaScript, Python, PHP, Go lub Ruby działają.

Gdzie powinienem uruchomić te skrypty?

Na serwerze, w zaplanowanym zadaniu lub w procesie CI, z kluczem przechowywanym jako sekret. Nigdy nie uruchamiaj ich w przeglądarce.

Czy mogę używać tych skryptów z protokołem OAuth zamiast klucza API?

Tak. Zastąp klucz API tokenem dostępu OAuth w nagłówku Authorization i odśwież go przed wygaśnięciem. Zobacz Łączenie aplikacji z Notiondesk za pomocą OAuth .

Co dalej?

Powiązane artykuły

Czy ta strona była pomocna?

Still need help? Ask us