Перейти к содержимому
Pre-MVP. Кодовая база 1.0 не является выпущенным продуктом — см. границы продукта 1.0.

Плагин планирования

Это плагин — удалённый сервис. Он планирует публикацию одной закреплённой версии или снятие с публикации одной закреплённой опубликованной версии и выполняет это изменение позже через протокол плагинов Dee Wan v1. У него свой Cloudflare Worker, своя база данных D1, свой cron-триггер и свой UI управления. Никакой его код не выполняется внутри Dee Wan.

Имя репозитория, remote и id манифеста — всё ещё открытое решение владельца проекта. com.example.scheduling — это заглушка. Не устанавливайте плагин с ним в продакшене.

Документация: Планирование от начала до конца запускает плагин против локального Dee Wan. Со стороны ядра см. Написание плагина, Протокол v1, Безопасность плагинов и Тестирование плагина.

Плагин Манифест Протокол Dee Wan
0.1.0 v1 v1
Маршрут Кто Что
GET /dee-wan/manifest.json Админка Dee Wan Манифест v1, origin берётся из PUBLIC_ORIGIN.
POST /admin/activation-codes Оператор, с Bearer ADMIN_SECRET Выдаёт одноразовый код, действительный 15 минут. Тело {} выдаёт код активации; {"installation_id":"..."} выдаёт код ротации только для этой инсталляции.
POST /dee-wan/activate Ядро Dee Wan activate создаёт инсталляцию и отказывает для уже существующей. rotate заменяет токен. Каждый код используется один раз.
GET /installations/:id?dee_wan_launch=dwl_... Браузер, из Dee Wan Обменивает код, устанавливает сессию и перенаправляет с 303 на страницу без кода.
GET /installations/:id Сессия Показывает контент, форму расписания и список расписаний.
POST /installations/:id/schedules Сессия и CSRF-токен Создаёт расписание.
POST /installations/:id/schedules/:scheduleId/cancel Сессия и CSRF-токен Отменяет ожидающее или повторяемое расписание.
POST /admin/installations/:id/delete-data Оператор Удаляет все данные плагина для инсталляции.

scheduled() выполняет один такт исполнителя.

  1. Разверните Worker (см. ниже) и задайте PUBLIC_ORIGIN.
  2. Выпустите код активации:
    Окно терминала
    curl -X POST -H "Authorization: Bearer $ADMIN_SECRET" https://scheduling.example/admin/activation-codes
  3. В Dee Wan перейдите в Settings → Plugins и установите плагин из https://scheduling.example/dee-wan/manifest.json с этим кодом. Выдайте возможности и модели.
  4. Чтобы выполнить ротацию токена, выпустите код с {"installation_id":"<id>"} и передайте его действию ротации в Dee Wan.

Скопируйте wrangler.example.jsonc в wrangler.jsonc и заполните id базы данных D1.

  • Привязка: PLUGIN_DB (D1).
  • Cron: * * * * *.
  • Переменные: PUBLIC_ORIGIN, ENVIRONMENT, DEE_WAN_DEV_ORIGINS, MAX_ATTEMPTS (5), MAX_LATENESS_MS (900000), LEASE_MS (60000), MAX_RETRY_AFTER_MS (300000).
  • Секреты: ADMIN_SECRET (не менее 16 символов), TOKEN_KEY (base64 от 32 случайных байт; openssl rand -base64 32).
Окно терминала
npx wrangler d1 create dee-wan-scheduling
npx wrangler d1 migrations apply dee-wan-scheduling --remote # --local for dev
npx wrangler secret put ADMIN_SECRET
npx wrangler secret put TOKEN_KEY
npx wrangler deploy

Потеря TOKEN_KEY делает сохранённые токены инсталляций нечитаемыми. Тогда расписания отклоняются с token_unreadable, и для каждой инсталляции нужно выполнить ротацию.

Локальная разработка против локального Dee Wan

Заголовок раздела «Локальная разработка против локального Dee Wan»

Cookie UI управления использует префикс __Host- и атрибут Secure, поэтому браузеру нужен HTTPS или localhost.

  • .dev.vars плагина: ENVIRONMENT=development, PUBLIC_ORIGIN=http://localhost:8788, DEE_WAN_DEV_ORIGINS=http://localhost:8787, ADMIN_SECRET=..., TOKEN_KEY=....
  • Ядро Dee Wan: ENVIRONMENT=development, PLUGIN_DEV_ORIGINS=http://localhost:8788, BACKEND_URL=http://localhost:8787.
  • Выполните npx wrangler d1 migrations apply dee-wan-scheduling --local, затем npx wrangler dev --port 8788 --test-scheduled.
  • Чтобы запустить такт, выполните curl "http://localhost:8788/__scheduled?cron=*+*+*+*+*".

Базовый URL Dee Wan по HTTP принимается, только когда ENVIRONMENT=development и его точный origin указан в DEE_WAN_DEV_ORIGINS. В остальных случаях базовый URL должен использовать HTTPS и не должен содержать учётных данных, query и fragment.

  • Расписание можно создать только из запуска для контента (действие Schedule). Плагин читает контент с делегированием, и выбранная пара (kind, to) должна входить в делегированные workflow.transitions.
  • Закрепления: публикация закрепляет current_version_id, снятие с публикации закрепляет published_version_id, и оба также закрепляют workflow.revision. Расписание хранится относительно основного экземпляра; ядро публикует всю группу переводов.
  • На каждую комбинацию инсталляции, элемента контента и вида существует не более одного живого (pending|retrying|running) расписания. Новое расписание вытесняет живое ожидающее или повторяемое расписание в одном пакете. Если живое расписание выполняется, создание завершается ошибкой execution_in_progress.
  • Перепланирование создаёт новую строку с новой ревизией и новым ключом идемпотентности.
  • Ключ идемпотентности — sched:<installation_id>:<schedule_id>:r<revision>. Он остаётся тем же при каждой повторной попытке.
  • Отменить можно только ожидающие или повторяемые расписания. Отмена выполняющегося расписания возвращает execution_in_progress. Завершённое расписание отменить нельзя. Отмена никогда не обращается к Dee Wan и никогда не отменяет переход.
  • В момент выполнения ядро заново проверяет полномочия человека, закрепления и ревизию.
  • Запуск для управления показывает и отменяет все расписания инсталляции. Создавать расписания он не может.

Статусы: pending, retrying, running, completed, refused, cancelled, superseded.

Каждое расписание хранит due_at в UTC, часовой пояс IANA, локальное время по часам и fold. Изменение правил часового пояса никогда не переписывает due_at. Вычисление использует только Intl.DateTimeFormat.

Ввод Результат
2026-07-01T09:00 America/New_York 2026-07-01 13:00 UTC
2026-03-29T02:30 Europe/Berlin отказ nonexistent_local_time (часы переводятся 02:00→03:00)
2026-10-25T02:30 Europe/Berlin, без fold отказ ambiguous_local_time
то же, fold earlier 2026-10-25 00:30 UTC (CEST)
то же, fold later 2026-10-25 01:30 UTC (CET)
2026-07-01T09:00 Mars/Base отказ unknown_timezone
2025-12-31T23:59 UTC, уже в прошлом отказ past_time

Подтверждение и список показывают локальное время с его поясом, а также время в UTC.

Каждый такт cron берёт до 20 строк, срок которых наступил, по кругу между инсталляциями. Захват — это compare-and-set по наблюдаемому статусу и токену аренды (lease). Он устанавливает running, новый токен аренды, lease_until = now + LEASE_MS и attempt_count + 1. Каждый захват отправляет одну команду. Строка фиксируется, только если собственный токен аренды исполнителя всё ещё действителен. Упавшая строка running захватывается заново после истечения её аренды и отправляется повторно с тем же ключом.

Ответ Dee Wan Результат плагина
200 transition_applied / idempotent_replay completed
400 invalid_request refused, без повтора
401 plugin_unauthorized refused, без повтора
403 capability_refused / requester_unauthorized refused, без повтора
404 content_not_found refused, без повтора
409 scheduled_target_stale / workflow_conflict / dependent_content / idempotency_key_reused refused, без повтора
422 transition_unavailable / transition_refused refused, без повтора
несоответствие статуса и кода, некорректный JSON, 3xx, другой статус refused untrusted_response
429 retrying в момент Retry-After (секунды или HTTP-дата)
500/502/503/504, сетевой сбой, тайм-аут (10 с) retrying, задержка 15 с·2^(n-1) с ограничением 5 мин, джиттер 50–100 %
сверх MAX_ATTEMPTS, позже due_at + MAX_LATENESS_MS или Retry-After > MAX_RETRY_AFTER_MS refused retry_exhausted, больше ничего не отправляется

retry_exhausted после сетевого сбоя означает, что исход неизвестен, поэтому проверьте контент в Dee Wan. Расписание никогда не отправляется позже предела опоздания, в том числе когда заново захватывается аренда упавшего исполнения.

Попытки записываются в schedule_attempt (попытка, начало, окончание, код, HTTP-статус). Это операционная запись для пользователя, а не телеметрия.

  • Хранимые данные: инсталляция (id, id сайта, базовый URL Dee Wan и токен, зашифрованный AES-GCM), хеши кодов активации, хеши сессий (30 минут) с их CSRF-токенами, расписания и попытки. Снимки контента и пользовательские сессии Dee Wan не хранятся.
  • Коды запуска и исходные токены никогда не пишутся в журнал и не хранятся в исходном виде.
  • Удаление плагина в Dee Wan отзывает токен. Данные плагина остаются. Оставшиеся расписания при наступлении срока отклоняются с plugin_unauthorized.
  • Чтобы удалить данные плагина, оператор вызывает POST /admin/installations/:id/delete-data с Bearer ADMIN_SECRET. Вызов отклоняется с 409 execution_in_progress, пока выполняется какое-либо расписание. Автоматическая очистка по сроку хранения не реализована.
Окно терминала
npm test # vitest, from this directory
npm run typecheck

test/helpers/d1.ts — это прослойка D1 на better-sqlite3. test/helpers/fake-core.ts имитирует протокол Dee Wan v1: идемпотентный повтор по ключу, проверки закреплений и ревизии, делегированные полномочия и внедрение сбоев.

test/docs.test.ts прогоняет приведённые выше таблицы «Исполнитель» и «Время» через classify и resolveLocalTime, поэтому строка, которую никто не обновил, приводит к падению. backend/test/plugin-contract.test.ts ядра сверяет копию таблицы исходов, возможностей и форм секретов этого плагина с собственными данными ядра.