Tự động hóa các tác vụ thường gặp trong trung tâm trợ giúp bằng API Notiondesk

Ví dụ về API: đồng bộ từ GitHub Actions, sao lưu bài viết dưới dạng markdown, đăng phản hồi lên Slack và báo cáo các tìm kiếm không có kết quả.

11 phút đọc

Bài viết này tập hợp các tập lệnh API Notiondesk sẵn sàng sử dụng cho các tác vụ mà các nhóm tự động hóa nhiều nhất: xuất bản các chỉnh sửa Notion từ quy trình CI, sao lưu mọi bài viết dưới dạng markdown, gửi phản hồi tiêu cực đến Slack, báo cáo các tìm kiếm mà trung tâm trợ giúp của bạn không thể trả lời, tìm các lỗi đồng bộ hóa và áp dụng một thương hiệu cho nhiều trung tâm trợ giúp. Sao chép một công thức, thiết lập hai biến môi trường và chạy nó.

Trước khi bạn bắt đầu

  • Đặt cả hai thành biến môi trường:
export NOTIONDESK_API_KEY="nd_wsk_..."
export NOTIONDESK_SITE_ID="c74781f6-1aea-4e17-ab23-bbb8b4d174b6"

Các công thức JavaScript yêu cầu Node.js phiên bản 18 trở lên.

Một công cụ hỗ trợ nhỏ dùng chung

Lưu tệp này dưới dạng notiondesk.mjs bên cạnh các tập lệnh của bạn. Nó gửi các tiêu đề, chờ khi API trả lời 429 rate_limited và đọc từng trang của danh sách.

Đồng bộ hóa trung tâm trợ giúp của bạn từ GitHub Actions

Phạm vi: __ĐƯỢC BẢO QUẢN_30__

Xuất bản các chỉnh sửa Notion của bạn từ quy trình làm việc GitHub Actions, ví dụ như ngay sau khi phát hành. Lưu khóa dưới dạng bí mật kho lưu trữ NOTIONDESK_API_KEY và ID trung tâm trợ giúp dưới dạng biến kho lưu trữ 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 nghĩa là quá trình đồng bộ đang được xếp vào hàng đợi. Nó chạy ngầm trong nền.
  • 409 conflict_error có nghĩa là quá trình đồng bộ hóa trung tâm trợ giúp đã được xếp vào hàng đợi hoặc đang chạy, vì vậy bước này thành công mà không cần bắt đầu một bước khác.
  • Idempotency-Key khiến yêu cầu được thử lại không có gì hai lần.
Khi tính năng dịch tự động được bật, mỗi lần đồng bộ sẽ dịch nội dung đã thay đổi và tính phí. Kích hoạt quy trình làm việc sau khi chỉnh sửa thực sự, chứ không phải theo lịch trình ngắn hạn. Xem Kích hoạt đồng bộ Notion bằng API Notiondesk .

Sao lưu mọi bài viết dưới dạng Markdown.

Phạm vi: __ĐƯỢC BẢO QUẢN_24__

Hãy lưu lại mọi bài viết, bằng mọi ngôn ngữ mà trung tâm trợ giúp của bạn hỗ trợ, dưới dạng tệp Markdown:

Bản sao lưu bao gồm cả bản nháp và bài viết đã được lưu trữ. Số lượng yêu cầu đọc được giới hạn ở mức 120 yêu cầu mỗi phút, vì vậy một trung tâm trợ giúp với 500 bài viết sẽ mất khoảng năm phút cho mỗi ngôn ngữ; hệ thống sẽ tự động chờ khi đạt đến giới hạn này.

Mỗi ngày, hãy gửi phản hồi tiêu cực lên Slack.

Phạm vi: read chỉ

Đăng tải phản hồi tiêu cực trong 24 giờ qua lên kênh Slack thông qua webhook đến . Đặt SLACK_WEBHOOK_URL, sau đó chạy tập lệnh một lần mỗi ngày.

Danh sách phản hồi được sắp xếp theo thứ tự mới nhất trước, vì vậy tập lệnh sẽ dừng việc phân trang ở mục đầu tiên có thời gian hiển thị hơn 24 giờ.

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")}` }),
  });
}
Phản hồi được viết bởi độc giả của bạn. Hãy chạy tập lệnh này với một khóa chỉ chứa ký tự đặc biệt, mã hóa văn bản trước khi đăng và tuyệt đối không chuyển nó cho LLM nào giữ khóa có quyền ghi. Xem phần Xuất phản hồi của độc giả bằng API Notiondesk .read

Báo cáo các tìm kiếm không có kết quả mỗi tuần

Phạm vi: __ĐÃ BẢO TỒN_20__

Hãy liệt kê những từ khóa mà độc giả đã tìm kiếm tuần trước mà không tìm thấy bài viết nào. Mỗi truy vấn đều là một ứng cử viên cho một bài viết mới hoặc một tiêu đề hay hơn.

Mỗi danh sách truy vấn chứa tối đa 50 hàng. Báo cáo phân tích cũng chia sẻ ngân sách 30 yêu cầu mỗi phút cho mỗi trung tâm trợ giúp. Xem Nhận phân tích trung tâm trợ giúp với API Notiondesk .

Tìm các bài viết có lần đồng bộ cuối cùng không thành công

Phạm vi: read, cộng thêm sync để thử lại

Trước tiên hãy sửa từng bài viết trong Notion, sau đó đồng bộ lại với POST /sites/{site_id}/articles/{article_id}/sync.

Áp dụng một thương hiệu duy nhất cho tất cả các trung tâm trợ giúp.

Phạm vi: read và admin

Sao chép màu sắc và phông chữ của một trung tâm trợ giúp sang tất cả các trung tâm trợ giúp khác trong không gian làm việc của bạn. Đặt SOURCE_SITE_ID thành trung tâm trợ giúp cần sao chép.

Mỗi lần cập nhật thương hiệu sẽ làm mới toàn bộ trung tâm trợ giúp mục tiêu, vì vậy tập lệnh sẽ gửi một yêu cầu cho mỗi trung tâm trợ giúp. Màu được lưu trữ ở dạng viết tắt như #fff sẽ bị từ chối: trước tiên hãy sửa lỗi này trong bảng điều khiển của trung tâm trợ giúp nguồn. Xem phần Tùy chỉnh thương hiệu và chủ đề trung tâm trợ giúp bằng API Notiondesk .

Chuyển hướng nhập sau khi di chuyển

Phạm vi: read và write

Bạn đang chuyển từ một công cụ trung tâm trợ giúp khác? Tập lệnh nhập chuyển hướng sẽ đọc một tệp CSV chứa các đường dẫn cũ và tạo một chuyển hướng cho mỗi hàng. Xem Quản lý chuyển hướng trang bằng 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}`);
}

Câu hỏi thường gặp

Tôi có thể sử dụng ngôn ngữ nào với API Notiondesk?

Bất kỳ ngôn ngữ nào có thể gửi yêu cầu HTTPS đều được. API là REST thuần túy với JSON, vì vậy JavaScript, Python, PHP, Go hoặc Ruby đều hoạt động.

Tôi nên chạy các tập lệnh này ở đâu?

Trên máy chủ, trong một tác vụ theo lịch trình hoặc trong quy trình CI, khóa được lưu trữ dưới dạng bí mật. Tuyệt đối không được chạy chúng trong trình duyệt.

Tôi có thể sử dụng các tập lệnh này với OAuth thay vì khóa API được không?

Vâng. Hãy thay thế khóa API bằng mã thông báo truy cập OAuth trong tiêu đề Authorization và làm mới trước khi nó hết hạn. Xem Kết nối ứng dụng của bạn với Notiondesk bằng OAuth .

Tiếp theo là gì?

Bài viết liên quan

Trang này có hữu ích không?

Still need help? Ask us