Автоматизируйте стандартные задачи справочного центра с помощью API Notiondesk

Примеры использования API Notiondesk: синхронизация из GitHub Actions, резервное копирование статей в формате Markdown, отправка отзывов в Slack и сообщение о результатах поиска.

4 мин чтения

В этой статье собраны готовые к использованию API-скрипты Notiondesk для задач, которые команды автоматизируют чаще всего: публикация правок Notion из конвейера CI, резервное копирование каждой статьи в формате Markdown, отправка негативных отзывов в Slack, сообщение о запросах, на которые ваш справочный центр не может ответить, поиск неудачных синхронизаций и применение одного бренда к нескольким справочным центрам. Скопируйте рецепт, установите две переменные среды и запустите его.

Прежде чем начать

  • Установите оба параметра в качестве переменных среды:
export NOTIONDESK_API_KEY="nd_wsk_..."
export NOTIONDESK_SITE_ID="c74781f6-1aea-4e17-ab23-bbb8b4d174b6"

Для работы JavaScript-рецептов требуется Node.js версии 18 или выше.

Небольшой помощник, работающий на себя.

Сохраните этот файл как notiondesk.mjs рядом со своими скриптами. Он отправляет заголовки, ожидает ответа API 429 rate_limited и считывает каждую страницу списка.

Синхронизируйте свой справочный центр из GitHub Actions.

Область применения: __СОХРАНЕНО_30__

Публикуйте свои изменения в Notion из рабочего процесса GitHub Actions, например, сразу после релиза. Сохраните ключ в качестве секрета репозитория NOTIONDESK_API_KEY, а идентификатор центра поддержки — в качестве переменной репозитория 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 означает, что синхронизация поставлена ​​в очередь. Она выполняется в фоновом режиме.
  • 409 conflict_error означает, что синхронизация справочного центра уже находится в очереди или выполняется, поэтому этот шаг завершается успешно без запуска следующего.
  • Параметр Idempotency-Key приводит к тому, что при повторной попытке запроса очередь запросов дважды остается пустой.
Пока включен автоматический перевод, каждая синхронизация переводит измененный контент и выставляет счет. Запускайте рабочий процесс после реальных правок, а не по срочному расписанию. См. раздел «Запуск синхронизации Notion с помощью API Notiondesk» .

Сохраняйте резервные копии каждой статьи в формате Markdown.

Область применения: __СОХРАНЕНО_24__

Сохраняйте все статьи на всех языках, на которых работает ваш справочный центр, в формате Markdown:

Резервная копия включает черновики и архивные статьи. Количество запросов на чтение ограничено 120 в минуту, поэтому для справочного центра с 500 статьями требуется около пяти минут на каждый язык; помощник автоматически ожидает, когда будет достигнут лимит.

Отправляйте негативные отзывы в Slack каждый день.

Область применения: только read

Опубликуйте негативные отзывы за последние 24 часа в канале Slack через входящий веб-хук . Установите значение SLACK_WEBHOOK_URL, затем запускайте скрипт один раз в день.

Список отзывов отсортирован по дате добавления (сначала самые новые записи), поэтому скрипт прекращает постраничную навигацию с первой записи старше 24 часов.

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")}` }),
  });
}
Отзывы пишут ваши читатели. Запускайте этот скрипт с ключом, содержащим только запятую, экранируйте текст перед отправкой и никогда не передавайте его в LLM, содержащий ключ, позволяющий записывать. См. Экспорт отзывов читателей с помощью API Notiondesk .read

Еженедельно появляются сообщения о безрезультатных поисковых запросах.

Область применения: __СОХРАНЕНО_20__

Перечислите поисковые запросы, которые читатели использовали на прошлой неделе, но не нашли подходящей статьи. Каждый запрос может стать поводом для написания новой статьи или для более удачного заголовка.

Каждый список запросов содержит до 50 строк. Аналитические отчеты также используют общий бюджет в 30 запросов в минуту на каждый центр поддержки. См. раздел «Получение аналитики центра поддержки с помощью API Notiondesk» .

Найдите статьи, последняя синхронизация которых не удалась.

Область действия: read, плюс sync для повторной попытки

Сначала исправьте каждую статью в Notion, а затем снова синхронизируйте ее с POST /sites/{site_id}/articles/{article_id}/sync.

Применяйте один и тот же бренд ко всем центрам поддержки.

Область применения: read и admin

Скопируйте цвета и шрифты одного центра справки во все остальные центры справки в вашем рабочем пространстве. Установите значение SOURCE_SITE_ID на центр справки, из которого нужно скопировать.

Каждое обновление фирменной символики обновляет весь целевой справочный центр, поэтому скрипт отправляет один запрос на каждый справочный центр. Цвет, сохраненный в сокращенном виде, например, #fff, отклоняется: сначала исправьте его на панели управления исходного справочного центра. См. раздел «Настройка фирменной символики и тем справочного центра с помощью API Notiondesk» .

Импорт перенаправлений после миграции

Область применения: read и write

Переходите с другого инструмента для создания справочных центров? Скрипт импорта перенаправлений считывает CSV-файл со старыми путями и создает одно перенаправление для каждой строки. См. раздел «Управление перенаправлениями страниц с помощью 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}`);
}

Часто задаваемые вопросы

Какой язык можно использовать с API Notiondesk?

Любой язык, способный отправлять HTTPS-запросы. API представляет собой обычный REST с использованием JSON, поэтому curl, JavaScript, Python, PHP, Go или Ruby — всё работает.

Где мне следует запускать эти скрипты?

Запускайте их на сервере, в запланированном задании или в конвейере CI, храня ключ в качестве секрета. Никогда не запускайте их в браузере.

Можно ли использовать эти скрипты с OAuth вместо ключа API?

Да. Замените ключ API токеном доступа OAuth в заголовке Authorization и обновите его до истечения срока действия. См. раздел «Подключение вашего приложения к Notiondesk с помощью OAuth» .

Что дальше?

Связанные статьи

Эта страница была полезной?

Still need help? Ask us