Плагин планирования
Это плагин — удалённый сервис. Он планирует публикацию одной закреплённой версии или снятие с публикации одной закреплённой опубликованной версии и выполняет это изменение позже через протокол плагинов 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() выполняет один такт исполнителя.
Установка
Заголовок раздела «Установка»- Разверните Worker (см. ниже) и задайте
PUBLIC_ORIGIN. - Выпустите код активации:
Окно терминала curl -X POST -H "Authorization: Bearer $ADMIN_SECRET" https://scheduling.example/admin/activation-codes - В Dee Wan перейдите в Settings → Plugins и установите плагин из
https://scheduling.example/dee-wan/manifest.jsonс этим кодом. Выдайте возможности и модели. - Чтобы выполнить ротацию токена, выпустите код с
{"installation_id":"<id>"}и передайте его действию ротации в Dee Wan.
Wrangler
Заголовок раздела «Wrangler»Скопируйте 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-schedulingnpx wrangler d1 migrations apply dee-wan-scheduling --remote # --local for devnpx wrangler secret put ADMIN_SECRETnpx wrangler secret put TOKEN_KEYnpx 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. Вызов отклоняется с 409execution_in_progress, пока выполняется какое-либо расписание. Автоматическая очистка по сроку хранения не реализована.
npm test # vitest, from this directorynpm run typechecktest/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 ядра сверяет копию таблицы исходов, возможностей и форм секретов этого плагина с собственными данными ядра.