Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6880980
Генератор, CLI и нода n8n: union без дискриминатора, ретраи, порядок …
lookinway Jul 25, 2026
9c0bc07
Примеры в доках: рукописные примеры, nullable и round-trip формы
lookinway Jul 25, 2026
904fded
Описания полей: когда значение бывает null, пунктуация и граница end_…
lookinway Jul 25, 2026
0dac1c9
Ссылки и контент доков: CardGroup, OG-превью, якоря, METHOD-ссылки
lookinway Jul 26, 2026
9783e5e
Changelog: 32 задокументированных nullable-поля вместо 38
lookinway Jul 26, 2026
f1e237e
Спека: тело ответов статуса, display_*, Reaction.name
lookinway Jul 26, 2026
b1063d5
Комментарий: зачем нужна нормализация ключей x-enum-descriptions
lookinway Jul 27, 2026
4ff710c
Записи релиза: дата и разделение по адресатам
lookinway Jul 27, 2026
e423055
Запись обновлений: описание поведения API вместо истории правок
lookinway Jul 27, 2026
331e184
Python SDK: импорт пакета и лимит длины сообщения
lookinway Jul 27, 2026
b11b0a6
Гайды: лимиты кнопок и поведение удалённого сообщения
lookinway Jul 27, 2026
e8bc444
CLI и SDK: пустой ответ загрузки, типизированный ApiError, гейт сборки
lookinway Jul 27, 2026
ef2a43e
Nullable first_name и value, примеры SDK и гейт на них
lookinway Jul 27, 2026
04c2735
TS-примеры типизируются скриптом пакета, а не npx
lookinway Jul 27, 2026
dbabac1
Swift-пример прокси собирается и на Linux
lookinway Jul 27, 2026
2846d05
Нода n8n: подстановка выбора сущности и список сотрудников
lookinway Jul 27, 2026
bbbc80b
Записи релиза: дата 28 июля и формулировки без причин
lookinway Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .github/workflows/n8n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ jobs:
- name: Generate n8n node
run: bun run integrations/n8n/scripts/generate-n8n.ts

# The node files are committed, but nothing else verifies they match the
# spec: check-generated-sync.mjs deliberately excludes integrations/**.
# Without this, a spec change that regenerates the node leaves the commit
# stale, and check-release (which diffs HEAD~1) then reports no code
# change and silently skips the publish.
- name: n8n node is in sync with the spec
run: git diff --exit-code -- integrations/n8n/nodes integrations/n8n/credentials

- name: Check n8n release (changelog-driven)
id: changes
run: |
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,63 @@ jobs:
run: bun run verify
working-directory: sdk/typescript

# Each SDK keeps its examples in a sibling directory that the SDK build
# never touches, so a spec change that renames a type or adds a union
# member breaks the shipped examples silently. They are linked from the
# READMEs, so users hit it first — compile them here too.
- name: Typecheck TypeScript examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.ts_changed == 'true'
run: bun run typecheck:examples
working-directory: sdk/typescript

- name: Compile Go SDK
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.go_changed == 'true'
working-directory: sdk/go/generated
run: go build ./...

- name: Compile Go examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.go_changed == 'true'
working-directory: sdk/go/examples
# Every example is its own `package main` in one directory, so
# `go build ./...` only reports "main redeclared". Build them one at a
# time, the way a reader runs them.
run: for f in *.go; do go build -o /dev/null "$f" || exit 1; done

- name: Compile Kotlin SDK
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.kotlin_changed == 'true'
working-directory: sdk/kotlin/generated
run: ./gradlew compileKotlin -Pversion=0.0.0

- name: Compile Kotlin examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.kotlin_changed == 'true'
working-directory: sdk/kotlin/generated
run: ./gradlew compileExamplesKotlin -Pversion=0.0.0

- name: Check Python SDK
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.python_changed == 'true'
working-directory: sdk/python/generated
run: python3 -m py_compile $(find pachca -name '*.py')

- name: Import Python SDK and examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.python_changed == 'true'
working-directory: sdk/python
# py_compile only parses: a module-level NameError (a registry naming a
# type that was never imported, an example importing a model the spec no
# longer generates) compiles fine and blows up on import. Import for
# real, with httpx stubbed so the check needs no dependencies.
run: |
python3 -c '
import sys, types, pathlib, importlib
stub = types.ModuleType("httpx")
stub.__getattr__ = lambda name: type(name, (), {})
sys.modules["httpx"] = stub
sys.path.insert(0, "generated")
import pachca.models, pachca.utils
sys.path.insert(0, "examples")
for path in sorted(pathlib.Path("examples").glob("*.py")):
importlib.import_module(path.stem)
'

- name: Setup .NET
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.csharp_changed == 'true'
uses: actions/setup-dotnet@v4
Expand All @@ -124,6 +166,11 @@ jobs:
working-directory: sdk/csharp/generated
run: dotnet build

- name: Compile C# examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.csharp_changed == 'true'
working-directory: sdk/csharp/examples
run: dotnet build

- name: Setup Swift
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.swift_changed == 'true'
uses: swift-actions/setup-swift@v2
Expand All @@ -132,6 +179,11 @@ jobs:
working-directory: sdk/swift
run: swift build

- name: Compile Swift examples
if: github.event_name != 'pull_request' || steps.sdk_changes.outputs.swift_changed == 'true'
working-directory: sdk/swift/examples
run: swift build

- name: Copy Swift Package.swift to root
if: github.event_name == 'push' && steps.sdk_check.outputs.should_publish == 'true'
run: cp sdk/swift/Package.swift Package.swift
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ bin/
obj/
*.user
*.suo
sdk/csharp/examples/Examples.csproj

# Рабочие планы/черновики — не в репозитории
*_PLAN.md
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/.well-known/api-catalog/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function GET(): Promise<Response> {
{
href: `${SITE_URL}/openapi.yaml`,
type: 'application/yaml',
title: 'Pachca API — OpenAPI 3.1 specification',
title: 'Pachca API — OpenAPI 3.0 specification',
},
{
href: `${SITE_URL}/pachca.postman_collection.json`,
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/app/robots.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const rules: Rule[] = [
// Allowed: enables Gemini / Google AI grounding & citation of these
// docs; does NOT affect Google Search ranking.
{ userAgent: 'Google-Extended', allow: ['/'] },
{ userAgent: '*', allow: ['/'], disallow: ['/internal/search', '/internal/og'] },
// `/internal/og` must stay crawlable: it is the og:image / twitter:image for
// every page, and Twitterbot, Slackbot, LinkedInBot and facebookexternalhit
// all honour robots.txt — disallowing it stripped the preview from every
// shared dev.pachca.com link. Only the search endpoint is worth hiding.
{ userAgent: '*', allow: ['/'], disallow: ['/internal/search'] },
];

// Cloudflare Content Signals Policy (2026): a machine-readable statement of
Expand Down
17 changes: 10 additions & 7 deletions apps/docs/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from 'node:path';
import { parseOpenAPI } from '@/lib/openapi/parser';
import { generateUrlFromOperation } from '@/lib/openapi/mapper';
import { getOrderedPages } from '@/lib/ordered-pages';
import { loadUpdates, loadTimeline, groupTimelineByDate } from '@/lib/updates-parser';
import { loadTimeline, groupTimelineByDate } from '@/lib/updates-parser';
import { groupBySeason } from '@/lib/seasons';

const BASE_URL = 'https://dev.pachca.com';
Expand Down Expand Up @@ -56,17 +56,20 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
});
}

// Per-update pages
const updates = loadUpdates();
for (const update of updates) {
// Per-update pages. Must match generateStaticParams in
// app/updates/[date]/page.tsx, which pre-renders updates ∪ releases — using
// loadUpdates() alone left every release-only date (a dozen of them) out of
// the sitemap even though the page and its .md twin both exist.
const timelineByDate = groupTimelineByDate(loadTimeline());
for (const group of timelineByDate) {
entries.push({
url: `${BASE_URL}/updates/${update.date}`,
lastModified: new Date(update.date),
url: `${BASE_URL}/updates/${group.date}`,
lastModified: new Date(group.date),
});
}

// Per-season pages (newest date in the season drives lastModified)
const seasons = groupBySeason(groupTimelineByDate(loadTimeline()));
const seasons = groupBySeason(timelineByDate);
for (const sg of seasons) {
entries.push({
url: `${BASE_URL}/updates/season/${sg.season.slug}`,
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/mdx/cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
Gauge,
ListOrdered,
Upload,
ExternalLink,
type LucideIcon,
} from 'lucide-react';

Expand Down Expand Up @@ -119,6 +120,7 @@ const iconMap: Record<string, LucideIcon> = {
Gauge,
ListOrdered,
Upload,
ExternalLink,
};

/** Icon mapping for guide pages by path */
Expand Down
6 changes: 4 additions & 2 deletions apps/docs/content/guides/buttons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ Data-кнопка отправляет данные на сервер через

## Ограничения

- Максимальное количество кнопок в строке — 8
- Максимальное число кнопок у сообщения — 100
- Максимальное количество строк с кнопками — 32
- Максимальная длина `text` на кнопке — 255 символов
- Максимальная длина `data` у кнопки — 255 символов

Количество кнопок внутри строки API не ограничивает, но в интерфейсе строка не
переносится: больше пяти-шести кнопок в ряд читаются плохо на узких экранах.
4 changes: 2 additions & 2 deletions apps/docs/content/guides/forms/handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ related:
{
"type": "view",
"event": "submit",
"private_metadata": "{'timeoff_id':4378}",
"callback_id": "timeoff_reguest_form",
"private_metadata": "{\"timeoff_id\":4378}",
"callback_id": "timeoff_request_form",
"user_id": 1235523,
"data": {
"date_start": "2025-07-01",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/incoming-webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Liquid также поддерживает:
используйте API-метод [Новое сообщение](POST /messages).
</Warning>

- **Максимальная длина сообщения** — 40 000 символов. Если результат шаблона превышает этот лимит, сообщение не будет отправлено.
- **Максимальная длина сообщения** — 40 000 байт. Для кириллицы это около 20 000 символов, поскольку каждый символ занимает два байта. Если результат шаблона превышает лимит, сообщение не будет отправлено.
- **Пустое сообщение** — если шаблон возвращает пустую строку, сообщение не будет отправлено.
- **Ошибка в шаблоне** — если шаблон содержит синтаксическую ошибку, в чат будет отправлено сообщение вида `Ошибка в шаблоне: описание ошибки`.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/n8n/resources.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ related:
|----------|-----|
| Create | [Загрузка файла](POST /uploads) |

Подробнее — в разделе [Продвинутые функции](/guides/n8n/advanced#zagruzka-fajlov).
Подробнее — в разделе [Продвинутые функции](/guides/n8n/advanced#zagruzka-faylov).

---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/n8n/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ related:
<CardGroup>
<Card compact icon="ExternalLink" title="Pachca" href="https://n8n.io/integrations/pachca/">витрина · verified</Card>
<Card compact icon="ExternalLink" title="Pachca Trigger" href="https://n8n.io/integrations/pachca-trigger/">витрина · verified</Card>
<Card compact icon="Package" title="n8n-nodes-pachca" href="https://www.npmjs.com/package/n8n-nodes-pachca">npm · v2.0.6</Card>
<Card compact icon="Package" title="n8n-nodes-pachca" href="https://www.npmjs.com/package/n8n-nodes-pachca">npm</Card>
</CardGroup>

## Где запустить n8n
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/content/guides/n8n/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ related:

<ImageCard src="/images/n8n/credentials-test.avif" alt="Успешная проверка Credentials" caption="Connection tested successfully" maxWidth={960} />

При ошибке 401 — токен неверный или просрочен. Симптомы и решения — в разделе [401 Unauthorized](/guides/n8n/troubleshooting#nevernyi-token-401-unauthorized).
При ошибке 401 — токен неверный или просрочен. Симптомы и решения — в разделе [401 Unauthorized](/guides/n8n/troubleshooting#nevernyy-token-401-unauthorized).

## Тестирование триггера

Expand Down Expand Up @@ -133,7 +133,7 @@ related:
После этого запустите **Listen for Test Event**, проведите тест, снова активируйте workflow. На время теста продакшен будет выключен — учитывайте это, если на workflow приходит критичный трафик.
</Step>
<Step title="Подмена URL в ручном режиме">
Оставьте **Webhook Setup** = **Manual** (значение по умолчанию) — в [ручном режиме](/guides/n8n/trigger#ruchnoi-rezhim) узел не управляет `outgoing_url`, и вы сами решаете, какой URL прописан в настройках бота в Пачке. Можно временно вручную заменить Production URL на Test URL в настройках бота, провести тест, затем вернуть Production URL обратно.
Оставьте **Webhook Setup** = **Manual** (значение по умолчанию) — в [ручном режиме](/guides/n8n/trigger#ruchnoy-rezhim) узел не управляет `outgoing_url`, и вы сами решаете, какой URL прописан в настройках бота в Пачке. Можно временно вручную заменить Production URL на Test URL в настройках бота, провести тест, затем вернуть Production URL обратно.

Этот способ даёт максимальный контроль, но требует аккуратности — легко забыть вернуть Production URL.
</Step>
Expand Down Expand Up @@ -226,9 +226,9 @@ related:

## Если что-то пошло не так

- **Вебхук не приходит** — проверьте, что бот в чате и workflow активен. Симптомы и решения: [Вебхук не приходит](/guides/n8n/troubleshooting#vebkhuk-ne-prikhodit)
- **Вебхук не приходит** — проверьте, что бот в чате и workflow активен. Симптомы и решения: [Вебхук не приходит](/guides/n8n/troubleshooting#vebhuk-ne-prihodit)
- **403 при активации Pachca Trigger** — токену не хватает прав: для токена бота `bot_self:webhook:write`, для персонального `bots:write` плюс доступ редактора к боту. Решение: [403 Forbidden при активации Pachca Trigger](/guides/n8n/troubleshooting#403-forbidden-pri-aktivatsii-pachca-trigger)
- **Signature Mismatch** — Signing Secret в Credentials не совпадает с секретом бота. Решение: [Ошибка подписи](/guides/n8n/troubleshooting#oshibka-podpisi-signature-mismatch)
- **401 Unauthorized** — неверный или просроченный токен. Решение: [401 Unauthorized](/guides/n8n/troubleshooting#nevernyi-token-401-unauthorized)
- **401 Unauthorized** — неверный или просроченный токен. Решение: [401 Unauthorized](/guides/n8n/troubleshooting#nevernyy-token-401-unauthorized)

Полный список типовых ошибок — в разделе [Устранение ошибок](/guides/n8n/troubleshooting).
6 changes: 3 additions & 3 deletions apps/docs/content/guides/n8n/trigger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Pachca Trigger поддерживает два режима настройки:
<ImageCard src="/images/n8n/trigger-webhook-urls.avif" alt="Test URL и Production URL в панели узла n8n" caption="Test URL и Production URL в верхней части панели узла" maxWidth={960} />
</Step>
<Step title="Вставьте URL в настройки бота в Пачке">
Откройте настройки вашего бота в Пачке, перейдите на вкладку **Исходящий Webhook** и вставьте скопированный Production URL в поле **Webhook URL**. Подробнее — в разделе [Настройка и типы событий](/guides/webhook/events#obschie-nastroiki).
Откройте настройки вашего бота в Пачке, перейдите на вкладку **Исходящий Webhook** и вставьте скопированный Production URL в поле **Webhook URL**. Подробнее — в разделе [Настройка и типы событий](/guides/webhook/events#obschie-nastroyki).
</Step>
<Step title="Вернитесь в n8n и активируйте">
Нажмите **Activate** в n8n. С этого момента бот отправляет события в ваш workflow.
Expand Down Expand Up @@ -184,15 +184,15 @@ Pachca Trigger поддерживает два режима настройки:

### Проверка подписи

Для защиты от поддельных запросов добавьте **Signing Secret** бота в [Credentials](/guides/n8n/setup#sozdanie-credentials). Trigger автоматически проверяет HMAC-SHA256 подпись каждого входящего запроса через заголовок `pachca-signature` и отклоняет невалидные.
Для защиты от поддельных запросов добавьте **Signing Secret** бота в [Credentials](/guides/n8n/setup#nastroyka-credentials). Trigger автоматически проверяет HMAC-SHA256 подпись каждого входящего запроса через заголовок `pachca-signature` и отклоняет невалидные.

Подробнее о механизме подписи — в разделе [Безопасность и обработчик](/guides/webhook/handler#bezopasnost).

<Info>Рекомендуется всегда использовать Signing Secret в продакшене для защиты от несанкционированных запросов.</Info>

### Ограничение по IP

Укажите **Webhook Allowed IPs** в [Credentials](/guides/n8n/setup#sozdanie-credentials) — через запятую список IP-адресов, с которых принимаются вебхуки. Пачка отправляет вебхуки с IP `37.200.70.177`.
Укажите **Webhook Allowed IPs** в [Credentials](/guides/n8n/setup#nastroyka-credentials) — через запятую список IP-адресов, с которых принимаются вебхуки. Пачка отправляет вебхуки с IP `37.200.70.177`.

Если поле пустое — проверка IP отключена и запросы принимаются с любого адреса.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/n8n/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ related:

1. **Токен бота:** включите для бота скоуп `bot_self:webhook:write` — настройки бота → вкладка **API**
2. **Персональный токен:** убедитесь, что у пользователя токена стоит роль **Редактор** в списке доступов бота, и что у токена есть скоуп `bots:write` (в настройках токена в **Автоматизации** → **Интеграции** → **API**)
3. Либо переключите **Webhook Setup** = **Manual** и пропишите Production URL в настройках бота самостоятельно — см. [Ручной режим](/guides/n8n/trigger#ruchnoi-rezhim)
3. Либо переключите **Webhook Setup** = **Manual** и пропишите Production URL в настройках бота самостоятельно — см. [Ручной режим](/guides/n8n/trigger#ruchnoy-rezhim)

---

Expand Down
Loading
Loading