@clipwright/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/__fixtures__/queue-promises.d.ts +87 -0
- package/dist/__fixtures__/queue-promises.js +99 -0
- package/dist/__fixtures__/queue-promises.js.map +1 -0
- package/dist/account.d.ts +49 -0
- package/dist/account.js +45 -0
- package/dist/account.js.map +1 -0
- package/dist/api-failure.d.ts +192 -0
- package/dist/api-failure.js +491 -0
- package/dist/api-failure.js.map +1 -0
- package/dist/contract-dispositions.d.ts +2 -19
- package/dist/contract-dispositions.js +2 -19
- package/dist/contract-dispositions.js.map +1 -1
- package/dist/errors.d.ts +148 -0
- package/dist/errors.js +192 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/public-endpoints.d.ts +29 -0
- package/dist/public-endpoints.js +49 -0
- package/dist/public-endpoints.js.map +1 -0
- package/dist/rate-limit.d.ts +160 -0
- package/dist/rate-limit.js +182 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/render-backend.d.ts +44 -6
- package/dist/render-backend.js +24 -0
- package/dist/render-backend.js.map +1 -1
- package/dist/run-static-warnings.d.ts +5 -0
- package/dist/run-static-warnings.js +30 -0
- package/dist/run-static-warnings.js.map +1 -0
- package/dist/runs.d.ts +3 -0
- package/dist/runs.js +18 -1
- package/dist/runs.js.map +1 -1
- package/dist/skills.d.ts +27 -2
- package/dist/skills.js +48 -8
- package/dist/skills.js.map +1 -1
- package/dist/tts-backend.d.ts +4 -0
- package/dist/tts-backend.js +0 -13
- package/dist/tts-backend.js.map +1 -1
- package/dist/vendor-response.d.ts +98 -0
- package/dist/vendor-response.js +152 -0
- package/dist/vendor-response.js.map +1 -0
- package/dist/voices.d.ts +42 -26
- package/dist/voices.js +11 -8
- package/dist/voices.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* RATE LIMIT — ФОРМА ОТВЕТА И ИМЕНА ЗАГОЛОВКОВ, ОДНИМ ДОМОМ (US-605).
|
|
4
|
+
*
|
|
5
|
+
* `spec.md` и `docs/03-api.md` объявляли «60 rpm, заголовки
|
|
6
|
+
* `X-RateLimit-Remaining`/`X-RateLimit-Reset`, код `rate_limited` 429» — и в
|
|
7
|
+
* коде не было ничего. Строить это тремя литералами в трёх пакетах значило бы
|
|
8
|
+
* воспроизвести ровно тот разрыв между объявлением и реализацией, ради
|
|
9
|
+
* закрытия которого стори и заведена: имя заголовка, разъехавшееся между
|
|
10
|
+
* сервером и клиентом, не ловится ни компилятором, ни тестом одной стороны.
|
|
11
|
+
*
|
|
12
|
+
* Поэтому здесь лежат И схема тела, И имена заголовков, И числа лимитов.
|
|
13
|
+
* REST, SDK и MCP берут их отсюда; тест `rate-limit.contract.test.ts` в
|
|
14
|
+
* `apps/api` проверяет, что литерала `rate_limited` нет ни в одной из трёх
|
|
15
|
+
* поверхностей (правило проекта №2, блокер 9 внешнего ревью плана).
|
|
16
|
+
*/
|
|
17
|
+
/** Имя заголовка с потолком ведра — сколько запросов в окне разрешено всего. */
|
|
18
|
+
export declare const RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit";
|
|
19
|
+
/** Сколько запросов в текущем окне ещё осталось. Ноль — законное значение. */
|
|
20
|
+
export declare const RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining";
|
|
21
|
+
/**
|
|
22
|
+
* Через сколько СЕКУНД остаток вырастет хотя бы на единицу.
|
|
23
|
+
*
|
|
24
|
+
* ЕДИНИЦА — ДЕЛЬТА, А НЕ UNIX-ВРЕМЯ, и это осознанное расхождение с GitHub.
|
|
25
|
+
* Дельта не зависит от часов клиента: агент на машине с уехавшим временем,
|
|
26
|
+
* вычитая epoch из своего `Date.now()`, ждал бы неверно и упирался бы в лимит
|
|
27
|
+
* повторно. Направление то же, что у черновика IETF (`RateLimit-Reset`), и то
|
|
28
|
+
* же, что у `Retry-After` ниже — две единицы в одном ответе путали бы сильнее,
|
|
29
|
+
* чем расхождение с чужой реализацией.
|
|
30
|
+
*
|
|
31
|
+
* СМЫСЛ ИМЕННО «ВЫРАСТЕТ НА ЕДИНИЦУ», а не «обнулится счётчик»: окно
|
|
32
|
+
* скользящее, и полного обнуления в нём не происходит — запросы выпадают из
|
|
33
|
+
* окна по одному. Для отказа это ровно момент, когда можно повторить.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RATE_LIMIT_RESET_HEADER = "X-RateLimit-Reset";
|
|
36
|
+
/** Стандартный `Retry-After` (RFC 9110), в секундах. Только на 429. */
|
|
37
|
+
export declare const RETRY_AFTER_HEADER = "Retry-After";
|
|
38
|
+
/** Длина окна. Одна для обоих вёдер: «в минуту» — то, что объявлено. */
|
|
39
|
+
export declare const RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
40
|
+
/**
|
|
41
|
+
* ПЛАТНОЕ ВЕДРО — 60 запросов в минуту. Число не выбрано здесь, а взято из
|
|
42
|
+
* объявления (`spec.md`, `docs/03-api.md`): стори закрывает разрыв «объявлено,
|
|
43
|
+
* но не построено», и менять заодно само объявление значило бы закрыть его
|
|
44
|
+
* подгонкой.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RATE_LIMIT_PAID_PER_MINUTE = 60;
|
|
47
|
+
/**
|
|
48
|
+
* БЕСПЛАТНОЕ ВЕДРО — 300 запросов в минуту, отдельным счётчиком.
|
|
49
|
+
*
|
|
50
|
+
* ПОЧЕМУ ОТДЕЛЬНО. Общее ведро с платным путём отравило бы ШТАТНЫЙ агентный
|
|
51
|
+
* цикл: по ADR-009 клиент поллит `GET /v1/runs/:id` каждые ~5 с, то есть 12
|
|
52
|
+
* запросов в минуту НА РАН. При потолке в три параллельных рана (та же стори)
|
|
53
|
+
* один только поллинг съедает 36 из 60, и агент упирался бы в лимит, ничего не
|
|
54
|
+
* заказывая — отказ там, где нет ни траты, ни нагрузки на вендора.
|
|
55
|
+
*
|
|
56
|
+
* ОТКУДА 300. Пол — те самые 36 плюс `quote` перед каждым запуском и редкий
|
|
57
|
+
* `voices`: около 40 в минуту на аккаунт, работающий на полном потолке
|
|
58
|
+
* параллельности. 300 даёт запас в 7.5 раза (место для ретраев, нескольких
|
|
59
|
+
* агентов на одном аккаунте и более частого поллинга), но остаётся ПОТОЛКОМ:
|
|
60
|
+
* зациклившийся клиент упирается в него на пятой секунде, а не выкачивает
|
|
61
|
+
* базу минутами. Число живёт здесь, а не в переменной окружения, потому что
|
|
62
|
+
* оно часть публичного контракта и объявлено в `docs/03-api.md`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const RATE_LIMIT_FREE_PER_MINUTE = 300;
|
|
65
|
+
/**
|
|
66
|
+
* ПОТОЛОК ОДНОВРЕМЕННЫХ РЕНДЕРОВ НА АККАУНТ — 3 (`spec.md:106`).
|
|
67
|
+
*
|
|
68
|
+
* Число живёт рядом с лимитом частоты, потому что они считают одно и то же
|
|
69
|
+
* сверху: сколько работы аккаунт может держать у нас в моменте. Из него же
|
|
70
|
+
* выведен пол бесплатного ведра (три рана × 12 опросов в минуту).
|
|
71
|
+
*
|
|
72
|
+
* ПРЕВЫШЕНИЕ СТАВИТ В ОЧЕРЕДЬ, А НЕ ОТВЕРГАЕТ. Четвёртый ран получает 202 и
|
|
73
|
+
* ждёт своей очереди; отказ здесь был бы хуже для агента, чем ожидание, —
|
|
74
|
+
* ему пришлось бы городить собственный планировщик поверх нашего.
|
|
75
|
+
*
|
|
76
|
+
* ЭТОТ ПОТОЛОК ЖИВЁТ ПОД ОБЩИМ ЛИМИТОМ ТАРИФА Trigger.dev, а не вместо него.
|
|
77
|
+
* Числа тарифа и следствие для волны N=50 — в `docs/02-architecture.md`.
|
|
78
|
+
*/
|
|
79
|
+
export declare const MAX_CONCURRENT_RENDERS_PER_ACCOUNT = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Ведро. Оно же — ось счётчика в БД и значение колонки `bucket`.
|
|
82
|
+
*
|
|
83
|
+
* Тип живёт здесь, а не в `service-core`, ровно потому, что ведро НАЗВАНО в
|
|
84
|
+
* ответе клиенту (сообщение ниже отличает платный лимит от бесплатного).
|
|
85
|
+
* Разъедься строка в счётчике со строкой в сообщении — клиент получал бы
|
|
86
|
+
* совет ждать не того окна.
|
|
87
|
+
*/
|
|
88
|
+
export type RateLimitScope = "paid" | "free";
|
|
89
|
+
/**
|
|
90
|
+
* Тело отказа 429. Числа лежат ВНУТРИ `error` — по тому же доводу, что и у
|
|
91
|
+
* денежных отказов в `errors.ts`: у ответа об ошибке один конверт, и всё, что
|
|
92
|
+
* объясняет отказ, лежит в нём.
|
|
93
|
+
*/
|
|
94
|
+
export declare const rateLimitedError: z.ZodObject<{
|
|
95
|
+
error: z.ZodObject<{
|
|
96
|
+
code: z.ZodLiteral<"rate_limited">;
|
|
97
|
+
message: z.ZodString;
|
|
98
|
+
limit: z.ZodNumber;
|
|
99
|
+
window_seconds: z.ZodNumber;
|
|
100
|
+
retry_after_seconds: z.ZodNumber;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
}, z.core.$strip>;
|
|
103
|
+
export type RateLimitedError = z.infer<typeof rateLimitedError>;
|
|
104
|
+
/**
|
|
105
|
+
* Сборка тела «слишком часто».
|
|
106
|
+
*
|
|
107
|
+
* Формулировка живёт ЗДЕСЬ, а не в мидлваре, по той же причине, что и форма:
|
|
108
|
+
* вёдер два, и объяснять отказ они обязаны одинаково. Сообщение НАЗЫВАЕТ
|
|
109
|
+
* ведро — иначе агент, упершийся в платный лимит, не отличит его от
|
|
110
|
+
* бесплатного и остановит поллинг уже принятых ранов.
|
|
111
|
+
*/
|
|
112
|
+
export declare function rateLimitedBody(args: {
|
|
113
|
+
limit: number;
|
|
114
|
+
windowSeconds: number;
|
|
115
|
+
retryAfterSeconds: number;
|
|
116
|
+
scope: RateLimitScope;
|
|
117
|
+
/**
|
|
118
|
+
* Был ли отбитый запрос АДРЕСОВАН СОЗДАНИЮ РАНА. Знает это только вызыватель:
|
|
119
|
+
* ведро само по себе не различает платный маршрут и любой неизвестный путь,
|
|
120
|
+
* который попадает туда же по безопасному дефолту.
|
|
121
|
+
*
|
|
122
|
+
* Нужен ради правдивости совета (внешнее ревью, круг 5): предупреждение
|
|
123
|
+
* «новый ключ может запустить ВТОРОЙ платный рендер» осмысленно только там,
|
|
124
|
+
* где рендер вообще бывает; на опечатке в пути или неподдержанном методе оно
|
|
125
|
+
* говорит о невозможном. По умолчанию `false` — молчим, пока не сказано
|
|
126
|
+
* обратное.
|
|
127
|
+
*
|
|
128
|
+
* ИМЯ ГОВОРИТ «АДРЕСОВАН», А НЕ «ЗАПУСТИТ», И ЭТО ТОЧНОЕ СЛОВО (круг 6).
|
|
129
|
+
* Знать, дошёл бы запрос до создания рана, мидлвара не может: битое тело,
|
|
130
|
+
* отклонённое поле, конфликт формата, опущенный килсвитч и нехватка кредитов
|
|
131
|
+
* отбиваются УЖЕ В ОБРАБОТЧИКЕ, то есть после лимита. Чтобы это выяснить,
|
|
132
|
+
* пришлось бы выполнить работу обработчика до решения о лимите — ровно ту
|
|
133
|
+
* работу, ради недопущения которой лимит и стоит впереди. Поэтому доступный
|
|
134
|
+
* факт один: «запрос шёл на платный маршрут», и обещание сформулировано в
|
|
135
|
+
* сослагательном («может»), а не в изъявительном наклонении.
|
|
136
|
+
*/
|
|
137
|
+
targetsRunCreation?: boolean;
|
|
138
|
+
}): RateLimitedError;
|
|
139
|
+
/**
|
|
140
|
+
* Сборка тела «слишком много НЕУДАЧНЫХ ПОПЫТОК АУТЕНТИФИКАЦИИ» (US-626, круг
|
|
141
|
+
* подтверждения).
|
|
142
|
+
*
|
|
143
|
+
* ЖИВЁТ ЗДЕСЬ, А НЕ В МИДЛВАРЕ, ПО ПРАВИЛУ ПРОЕКТА №2 — и это не формальность:
|
|
144
|
+
* литерал `rate_limited`, собранный на месте, поймал сторож
|
|
145
|
+
* `rate-limit.single-source.test.ts` в тот же прогон. Форма 429 у продукта одна,
|
|
146
|
+
* потому что разбирают её клиентские пакеты одной схемой (`rateLimitedError`):
|
|
147
|
+
* тело без любого из трёх чисел не пройдёт разбор вовсе, и агент получит
|
|
148
|
+
* «неизвестный отказ» вместо «подожди столько секунд».
|
|
149
|
+
*
|
|
150
|
+
* ОТЛИЧИЕ ОТ `rateLimitedBody` — ТОЛЬКО СООБЩЕНИЕ, и различать их обязательно.
|
|
151
|
+
* Тот отказ адресован ДЕЙСТВУЮЩЕМУ клиенту и говорит про ведро аккаунта,
|
|
152
|
+
* идемпотентность и второй платный рендер. Этот приходит тому, кто предъявил
|
|
153
|
+
* НЕработающий ключ: ни аккаунта, ни рана у него нет, и совет про
|
|
154
|
+
* `Idempotency-Key` был бы разговором о невозможном.
|
|
155
|
+
*/
|
|
156
|
+
export declare function authThrottledBody(args: {
|
|
157
|
+
limit: number;
|
|
158
|
+
windowSeconds: number;
|
|
159
|
+
retryAfterSeconds: number;
|
|
160
|
+
}): RateLimitedError;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* RATE LIMIT — ФОРМА ОТВЕТА И ИМЕНА ЗАГОЛОВКОВ, ОДНИМ ДОМОМ (US-605).
|
|
4
|
+
*
|
|
5
|
+
* `spec.md` и `docs/03-api.md` объявляли «60 rpm, заголовки
|
|
6
|
+
* `X-RateLimit-Remaining`/`X-RateLimit-Reset`, код `rate_limited` 429» — и в
|
|
7
|
+
* коде не было ничего. Строить это тремя литералами в трёх пакетах значило бы
|
|
8
|
+
* воспроизвести ровно тот разрыв между объявлением и реализацией, ради
|
|
9
|
+
* закрытия которого стори и заведена: имя заголовка, разъехавшееся между
|
|
10
|
+
* сервером и клиентом, не ловится ни компилятором, ни тестом одной стороны.
|
|
11
|
+
*
|
|
12
|
+
* Поэтому здесь лежат И схема тела, И имена заголовков, И числа лимитов.
|
|
13
|
+
* REST, SDK и MCP берут их отсюда; тест `rate-limit.contract.test.ts` в
|
|
14
|
+
* `apps/api` проверяет, что литерала `rate_limited` нет ни в одной из трёх
|
|
15
|
+
* поверхностей (правило проекта №2, блокер 9 внешнего ревью плана).
|
|
16
|
+
*/
|
|
17
|
+
/** Имя заголовка с потолком ведра — сколько запросов в окне разрешено всего. */
|
|
18
|
+
export const RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit";
|
|
19
|
+
/** Сколько запросов в текущем окне ещё осталось. Ноль — законное значение. */
|
|
20
|
+
export const RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining";
|
|
21
|
+
/**
|
|
22
|
+
* Через сколько СЕКУНД остаток вырастет хотя бы на единицу.
|
|
23
|
+
*
|
|
24
|
+
* ЕДИНИЦА — ДЕЛЬТА, А НЕ UNIX-ВРЕМЯ, и это осознанное расхождение с GitHub.
|
|
25
|
+
* Дельта не зависит от часов клиента: агент на машине с уехавшим временем,
|
|
26
|
+
* вычитая epoch из своего `Date.now()`, ждал бы неверно и упирался бы в лимит
|
|
27
|
+
* повторно. Направление то же, что у черновика IETF (`RateLimit-Reset`), и то
|
|
28
|
+
* же, что у `Retry-After` ниже — две единицы в одном ответе путали бы сильнее,
|
|
29
|
+
* чем расхождение с чужой реализацией.
|
|
30
|
+
*
|
|
31
|
+
* СМЫСЛ ИМЕННО «ВЫРАСТЕТ НА ЕДИНИЦУ», а не «обнулится счётчик»: окно
|
|
32
|
+
* скользящее, и полного обнуления в нём не происходит — запросы выпадают из
|
|
33
|
+
* окна по одному. Для отказа это ровно момент, когда можно повторить.
|
|
34
|
+
*/
|
|
35
|
+
export const RATE_LIMIT_RESET_HEADER = "X-RateLimit-Reset";
|
|
36
|
+
/** Стандартный `Retry-After` (RFC 9110), в секундах. Только на 429. */
|
|
37
|
+
export const RETRY_AFTER_HEADER = "Retry-After";
|
|
38
|
+
/** Длина окна. Одна для обоих вёдер: «в минуту» — то, что объявлено. */
|
|
39
|
+
export const RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
40
|
+
/**
|
|
41
|
+
* ПЛАТНОЕ ВЕДРО — 60 запросов в минуту. Число не выбрано здесь, а взято из
|
|
42
|
+
* объявления (`spec.md`, `docs/03-api.md`): стори закрывает разрыв «объявлено,
|
|
43
|
+
* но не построено», и менять заодно само объявление значило бы закрыть его
|
|
44
|
+
* подгонкой.
|
|
45
|
+
*/
|
|
46
|
+
export const RATE_LIMIT_PAID_PER_MINUTE = 60;
|
|
47
|
+
/**
|
|
48
|
+
* БЕСПЛАТНОЕ ВЕДРО — 300 запросов в минуту, отдельным счётчиком.
|
|
49
|
+
*
|
|
50
|
+
* ПОЧЕМУ ОТДЕЛЬНО. Общее ведро с платным путём отравило бы ШТАТНЫЙ агентный
|
|
51
|
+
* цикл: по ADR-009 клиент поллит `GET /v1/runs/:id` каждые ~5 с, то есть 12
|
|
52
|
+
* запросов в минуту НА РАН. При потолке в три параллельных рана (та же стори)
|
|
53
|
+
* один только поллинг съедает 36 из 60, и агент упирался бы в лимит, ничего не
|
|
54
|
+
* заказывая — отказ там, где нет ни траты, ни нагрузки на вендора.
|
|
55
|
+
*
|
|
56
|
+
* ОТКУДА 300. Пол — те самые 36 плюс `quote` перед каждым запуском и редкий
|
|
57
|
+
* `voices`: около 40 в минуту на аккаунт, работающий на полном потолке
|
|
58
|
+
* параллельности. 300 даёт запас в 7.5 раза (место для ретраев, нескольких
|
|
59
|
+
* агентов на одном аккаунте и более частого поллинга), но остаётся ПОТОЛКОМ:
|
|
60
|
+
* зациклившийся клиент упирается в него на пятой секунде, а не выкачивает
|
|
61
|
+
* базу минутами. Число живёт здесь, а не в переменной окружения, потому что
|
|
62
|
+
* оно часть публичного контракта и объявлено в `docs/03-api.md`.
|
|
63
|
+
*/
|
|
64
|
+
export const RATE_LIMIT_FREE_PER_MINUTE = 300;
|
|
65
|
+
/**
|
|
66
|
+
* ПОТОЛОК ОДНОВРЕМЕННЫХ РЕНДЕРОВ НА АККАУНТ — 3 (`spec.md:106`).
|
|
67
|
+
*
|
|
68
|
+
* Число живёт рядом с лимитом частоты, потому что они считают одно и то же
|
|
69
|
+
* сверху: сколько работы аккаунт может держать у нас в моменте. Из него же
|
|
70
|
+
* выведен пол бесплатного ведра (три рана × 12 опросов в минуту).
|
|
71
|
+
*
|
|
72
|
+
* ПРЕВЫШЕНИЕ СТАВИТ В ОЧЕРЕДЬ, А НЕ ОТВЕРГАЕТ. Четвёртый ран получает 202 и
|
|
73
|
+
* ждёт своей очереди; отказ здесь был бы хуже для агента, чем ожидание, —
|
|
74
|
+
* ему пришлось бы городить собственный планировщик поверх нашего.
|
|
75
|
+
*
|
|
76
|
+
* ЭТОТ ПОТОЛОК ЖИВЁТ ПОД ОБЩИМ ЛИМИТОМ ТАРИФА Trigger.dev, а не вместо него.
|
|
77
|
+
* Числа тарифа и следствие для волны N=50 — в `docs/02-architecture.md`.
|
|
78
|
+
*/
|
|
79
|
+
export const MAX_CONCURRENT_RENDERS_PER_ACCOUNT = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Тело отказа 429. Числа лежат ВНУТРИ `error` — по тому же доводу, что и у
|
|
82
|
+
* денежных отказов в `errors.ts`: у ответа об ошибке один конверт, и всё, что
|
|
83
|
+
* объясняет отказ, лежит в нём.
|
|
84
|
+
*/
|
|
85
|
+
export const rateLimitedError = z.object({
|
|
86
|
+
error: z.object({
|
|
87
|
+
code: z.literal("rate_limited"),
|
|
88
|
+
message: z.string().min(1),
|
|
89
|
+
/** Потолок ведра, в которое попал запрос. */
|
|
90
|
+
limit: z.number().int().positive(),
|
|
91
|
+
/** Длина окна в секундах — без неё `limit` не значит ничего. */
|
|
92
|
+
window_seconds: z.number().int().positive(),
|
|
93
|
+
/** Через сколько секунд повторять. Совпадает с `Retry-After`. */
|
|
94
|
+
retry_after_seconds: z.number().int().positive(),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Сборка тела «слишком часто».
|
|
99
|
+
*
|
|
100
|
+
* Формулировка живёт ЗДЕСЬ, а не в мидлваре, по той же причине, что и форма:
|
|
101
|
+
* вёдер два, и объяснять отказ они обязаны одинаково. Сообщение НАЗЫВАЕТ
|
|
102
|
+
* ведро — иначе агент, упершийся в платный лимит, не отличит его от
|
|
103
|
+
* бесплатного и остановит поллинг уже принятых ранов.
|
|
104
|
+
*/
|
|
105
|
+
export function rateLimitedBody(args) {
|
|
106
|
+
// ХВОСТ СООБЩЕНИЯ ЗАВИСИТ ОТ ВЕДРА, И ЭТО НЕ УКРАШЕНИЕ (внешнее ревью, круги 2 и 3).
|
|
107
|
+
//
|
|
108
|
+
// Первая редакция дописывала «no run was created» к ОБОИМ отказам, и это была
|
|
109
|
+
// ложь дважды.
|
|
110
|
+
//
|
|
111
|
+
// НА БЕСПЛАТНОМ ВЕДРЕ: агент, опрашивающий статус уже ЗАПУЩЕННОГО рана,
|
|
112
|
+
// получал бы 429 с уверением, что рана нет, — и мог бы бросить работу, за
|
|
113
|
+
// которую уже заплачено. Ту же работу защищает и решение пропускать
|
|
114
|
+
// бесплатные пути при отказе счётчика.
|
|
115
|
+
//
|
|
116
|
+
// НА ПЛАТНОМ: лимит стоит ПЕРЕД обработчиком, поэтому 429 получает и ПОВТОР
|
|
117
|
+
// с уже занятым `Idempotency-Key` — запрос, за которым ран существует. «No
|
|
118
|
+
// run was created» толкает агента взять новый ключ, а новый ключ есть второй
|
|
119
|
+
// платный рендер. Поэтому платный хвост больше ничего не утверждает о
|
|
120
|
+
// существовании рана и прямо велит повторить С ТЕМ ЖЕ ключом: это верно и
|
|
121
|
+
// для первого запроса (там повтор просто создаст ран), и для повтора.
|
|
122
|
+
// ОПИСАНИЕ ВЕДРА ТОЖЕ ОБЯЗАНО БЫТЬ ВЕРНЫМ (внешнее ревью, круг 4). В платное
|
|
123
|
+
// ведро по построению попадает не только создание рана, но и любой
|
|
124
|
+
// неизвестный путь под авторизованными префиксами — так выбран безопасный
|
|
125
|
+
// дефолт. Формулировка «starting runs» описывала бы клиенту не тот отказ.
|
|
126
|
+
const surface = args.scope === "paid"
|
|
127
|
+
? "paid requests (starting a run, and any unrecognised authenticated path)"
|
|
128
|
+
: "free requests (quotes, voices, run status, account)";
|
|
129
|
+
// СОВЕТ ПРО КЛЮЧ ДАЁТСЯ ТОЛЬКО ТАМ, ГДЕ ОН ОСМЫСЛЕН (круги 4 и 5).
|
|
130
|
+
//
|
|
131
|
+
// Круг 4 снял безусловность: 429 получает и запрос, который ключа не
|
|
132
|
+
// присылал. Круг 5 снял вторую неточность — предупреждение о «втором платном
|
|
133
|
+
// рендере» уходило и на опечатку в пути, и на неподдержанный метод, где
|
|
134
|
+
// никакой ключ ничего не отрендерит. Теперь его печатает только запуск рана,
|
|
135
|
+
// и `targetsRunCreation` приходит от мидлвары — она единственная знает маршрут.
|
|
136
|
+
const tail = args.scope === "free"
|
|
137
|
+
? "nothing was charged; runs already started keep going and stay retrievable."
|
|
138
|
+
: args.targetsRunCreation === true
|
|
139
|
+
? "nothing was charged. If this request carried an Idempotency-Key, retry with the SAME key: a new key can start a second paid render."
|
|
140
|
+
: "nothing was charged.";
|
|
141
|
+
return rateLimitedError.parse({
|
|
142
|
+
error: {
|
|
143
|
+
code: "rate_limited",
|
|
144
|
+
message: `Rate limit of ${args.limit} ${surface} per ${args.windowSeconds} seconds is exhausted ` +
|
|
145
|
+
`for this account. Retry in ${args.retryAfterSeconds} seconds; ${tail}`,
|
|
146
|
+
limit: args.limit,
|
|
147
|
+
window_seconds: args.windowSeconds,
|
|
148
|
+
retry_after_seconds: args.retryAfterSeconds,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Сборка тела «слишком много НЕУДАЧНЫХ ПОПЫТОК АУТЕНТИФИКАЦИИ» (US-626, круг
|
|
154
|
+
* подтверждения).
|
|
155
|
+
*
|
|
156
|
+
* ЖИВЁТ ЗДЕСЬ, А НЕ В МИДЛВАРЕ, ПО ПРАВИЛУ ПРОЕКТА №2 — и это не формальность:
|
|
157
|
+
* литерал `rate_limited`, собранный на месте, поймал сторож
|
|
158
|
+
* `rate-limit.single-source.test.ts` в тот же прогон. Форма 429 у продукта одна,
|
|
159
|
+
* потому что разбирают её клиентские пакеты одной схемой (`rateLimitedError`):
|
|
160
|
+
* тело без любого из трёх чисел не пройдёт разбор вовсе, и агент получит
|
|
161
|
+
* «неизвестный отказ» вместо «подожди столько секунд».
|
|
162
|
+
*
|
|
163
|
+
* ОТЛИЧИЕ ОТ `rateLimitedBody` — ТОЛЬКО СООБЩЕНИЕ, и различать их обязательно.
|
|
164
|
+
* Тот отказ адресован ДЕЙСТВУЮЩЕМУ клиенту и говорит про ведро аккаунта,
|
|
165
|
+
* идемпотентность и второй платный рендер. Этот приходит тому, кто предъявил
|
|
166
|
+
* НЕработающий ключ: ни аккаунта, ни рана у него нет, и совет про
|
|
167
|
+
* `Idempotency-Key` был бы разговором о невозможном.
|
|
168
|
+
*/
|
|
169
|
+
export function authThrottledBody(args) {
|
|
170
|
+
return rateLimitedError.parse({
|
|
171
|
+
error: {
|
|
172
|
+
code: "rate_limited",
|
|
173
|
+
message: `Too many failed authentication attempts: ${args.limit} per ${args.windowSeconds} seconds ` +
|
|
174
|
+
`from this address. Retry in ${args.retryAfterSeconds} seconds. ` +
|
|
175
|
+
`Nothing was charged, and no account was identified.`,
|
|
176
|
+
limit: args.limit,
|
|
177
|
+
window_seconds: args.windowSeconds,
|
|
178
|
+
retry_after_seconds: args.retryAfterSeconds,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
package/dist/render-backend.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AspectRatio, Resolution } from "./skills.js";
|
|
2
|
+
import type { VendorRefusalClass } from "./vendor-response.js";
|
|
2
3
|
/**
|
|
3
4
|
* Вендор-нейтральный контракт рендера аватара.
|
|
4
5
|
*
|
|
@@ -44,8 +45,9 @@ export type ImageSource = {
|
|
|
44
45
|
*/
|
|
45
46
|
export interface AvatarClipRequest {
|
|
46
47
|
/**
|
|
47
|
-
* Наш run_id. Адаптер обязан пробросить
|
|
48
|
-
* (`Idempotency-Key`,
|
|
48
|
+
* Наш run_id. Адаптер обязан пробросить в идемпотентность вендора
|
|
49
|
+
* (`Idempotency-Key`, если она есть) ОБЛАСТЬ — `idempotencyScope ?? runId`,
|
|
50
|
+
* а не сам `runId` безусловно: разбор — у поля `idempotencyScope` ниже.
|
|
49
51
|
*/
|
|
50
52
|
runId: string;
|
|
51
53
|
/**
|
|
@@ -122,10 +124,23 @@ export interface RenderBackend {
|
|
|
122
124
|
* Реализация обязана:
|
|
123
125
|
* - разрешать внешность персонажа ВНУТРИ (вендорский каталог), не принимая её
|
|
124
126
|
* из запроса — идентификаторы аватара и каталог наружу не торчат;
|
|
125
|
-
* - слать вендорский `Idempotency-Key`, детерминированный от
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* - слать вендорский `Idempotency-Key`, детерминированный от ОБЛАСТИ
|
|
128
|
+
* идемпотентности (`req.idempotencyScope ?? req.runId`, US-526), — вторая
|
|
129
|
+
* независимая линия против двойного списания (подтверждено, docs/13).
|
|
130
|
+
* Именно от области, а не от `runId`: у сегментного ролика на один ран
|
|
131
|
+
* приходится несколько платных create, и общий ключ заставил бы вендора
|
|
132
|
+
* вернуть первый ролик в ответ на каждый следующий — молча и с кодом 200;
|
|
133
|
+
* - помечать ошибку признаком `retryable`: 4xx кроме 429 — `false`, 5xx и
|
|
134
|
+
* 429 — `true`.
|
|
135
|
+
*
|
|
136
|
+
* ПРИЗНАК ОПИСЫВАЕТ ПРИРОДУ ОШИБКИ, А НЕ РАЗРЕШЕНИЕ ПОВТОРИТЬ ЭТОТ ВЫЗОВ
|
|
137
|
+
* (уточнение US-606b, круг 11 внешнего ревью; прежняя редакция говорила «их
|
|
138
|
+
* ретрай починит» и читалась как позволение). «Транзиентная» и «можно
|
|
139
|
+
* повторить» — разные утверждения, и для ПЛАТНОГО создания второе всегда
|
|
140
|
+
* ложно: 429 означает, что вендор отбил, а 5xx — что неизвестно, начал ли он
|
|
141
|
+
* работу, и повтор превратил бы одно возможное списание в два достоверных.
|
|
142
|
+
* Признак читается ТОЛЬКО на бесплатном поллинге (`utils/poll-retry.ts`);
|
|
143
|
+
* `createAvatarJob` не повторяется ни при каком статусе.
|
|
129
144
|
*/
|
|
130
145
|
createAvatarJob(req: AvatarClipRequest): Promise<AvatarJobHandle>;
|
|
131
146
|
/**
|
|
@@ -141,7 +156,30 @@ export interface RenderBackend {
|
|
|
141
156
|
* ожидания и бросать по его превышению, а не висеть на зависшей очереди.
|
|
142
157
|
*/
|
|
143
158
|
pollAvatarJob(handle: AvatarJobHandle): Promise<AvatarClipResult>;
|
|
159
|
+
/** Чья причина за отказом: наш счёт у вендора или его беда. ОБЯЗАТЕЛЕН — */
|
|
160
|
+
/** новый вендор без него не скомпилируется. Разбор — `docs/18` §11.6. */
|
|
161
|
+
classifyRefusal(error: unknown): VendorRefusalClass;
|
|
144
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* ЧИТАТЕЛЬ ПРИЗНАКА, КОТОРЫЙ ТРЕБУЕТ КОНТРАКТ ВЫШЕ (US-606b).
|
|
165
|
+
*
|
|
166
|
+
* ЧТО ОН ЧИТАЕТ: природу ошибки, а не разрешение повторить операцию. Кто и что
|
|
167
|
+
* повторяет — решает вызывающий, и на ПЛАТНОМ создании ответ всегда «ничего»
|
|
168
|
+
* (см. `createAvatarJob` выше). Сегодня единственный потребитель — ретрай
|
|
169
|
+
* бесплатного поллинга.
|
|
170
|
+
*
|
|
171
|
+
* ЗАЧЕМ ЗДЕСЬ, А НЕ У ВЫЗЫВАЮЩЕГО. Обязанность помечать ошибку записана строкой
|
|
172
|
+
* выше, и до этой стори её никто не читал: адаптер считал признак, а
|
|
173
|
+
* потребителя у него не было — то есть контракт держался тестами адаптера на
|
|
174
|
+
* самого себя. Читатель обязан жить там же, где написана обязанность, иначе он
|
|
175
|
+
* станет разбором формы КОНКРЕТНОГО адаптера по месту, и второй бэкенд (Argil в
|
|
176
|
+
* docs/05) выполнит контракт, а вызывающий его не заметит.
|
|
177
|
+
*
|
|
178
|
+
* СТРОГОЕ РАВЕНСТВО `true`, А НЕ ПРИВЕДЕНИЕ К ИСТИНЕ: решение о повторе рядом с
|
|
179
|
+
* деньгами — последнее место, где уместна догадка «похоже на да». Чего в
|
|
180
|
+
* признаке нет (ошибка без поля, чужой объект, строка) — то не повторяется.
|
|
181
|
+
*/
|
|
182
|
+
export declare function isRetryableVendorError(error: unknown): boolean;
|
|
145
183
|
/**
|
|
146
184
|
* Приведение запроса к возможностям бэкенда.
|
|
147
185
|
*
|
package/dist/render-backend.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ЧИТАТЕЛЬ ПРИЗНАКА, КОТОРЫЙ ТРЕБУЕТ КОНТРАКТ ВЫШЕ (US-606b).
|
|
3
|
+
*
|
|
4
|
+
* ЧТО ОН ЧИТАЕТ: природу ошибки, а не разрешение повторить операцию. Кто и что
|
|
5
|
+
* повторяет — решает вызывающий, и на ПЛАТНОМ создании ответ всегда «ничего»
|
|
6
|
+
* (см. `createAvatarJob` выше). Сегодня единственный потребитель — ретрай
|
|
7
|
+
* бесплатного поллинга.
|
|
8
|
+
*
|
|
9
|
+
* ЗАЧЕМ ЗДЕСЬ, А НЕ У ВЫЗЫВАЮЩЕГО. Обязанность помечать ошибку записана строкой
|
|
10
|
+
* выше, и до этой стори её никто не читал: адаптер считал признак, а
|
|
11
|
+
* потребителя у него не было — то есть контракт держался тестами адаптера на
|
|
12
|
+
* самого себя. Читатель обязан жить там же, где написана обязанность, иначе он
|
|
13
|
+
* станет разбором формы КОНКРЕТНОГО адаптера по месту, и второй бэкенд (Argil в
|
|
14
|
+
* docs/05) выполнит контракт, а вызывающий его не заметит.
|
|
15
|
+
*
|
|
16
|
+
* СТРОГОЕ РАВЕНСТВО `true`, А НЕ ПРИВЕДЕНИЕ К ИСТИНЕ: решение о повторе рядом с
|
|
17
|
+
* деньгами — последнее место, где уместна догадка «похоже на да». Чего в
|
|
18
|
+
* признаке нет (ошибка без поля, чужой объект, строка) — то не повторяется.
|
|
19
|
+
*/
|
|
20
|
+
export function isRetryableVendorError(error) {
|
|
21
|
+
return (typeof error === "object" &&
|
|
22
|
+
error !== null &&
|
|
23
|
+
error.retryable === true);
|
|
24
|
+
}
|
|
1
25
|
/**
|
|
2
26
|
* Приведение запроса к возможностям бэкенда.
|
|
3
27
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-backend.js","sourceRoot":"","sources":["../src/render-backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-backend.js","sourceRoot":"","sources":["../src/render-backend.ts"],"names":[],"mappings":"AAsKA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAiC,CAAC,SAAS,KAAK,IAAI,CACtD,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAA0D,EAC1D,IAAyB,EACzB,WAAmB;IAEnB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,IAAI,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;IAChC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,WAAW,WAAW,oCAAoC,CAAC,CAAC;QAC9E,CAAC;QACD,QAAQ,CAAC,IAAI,CACX,cAAc,UAAU,wBAAwB,WAAW,iBAAiB,QAAQ,EAAE,CACvF,CAAC;QACF,UAAU,GAAG,QAAQ,CAAC;IACxB,CAAC;IAED,IAAI,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;IAClC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,WAAW,WAAW,sCAAsC,CAAC,CAAC;QAChF,CAAC;QACD,QAAQ,CAAC,IAAI,CACX,gBAAgB,WAAW,wBAAwB,WAAW,iBAAiB,QAAQ,EAAE,CAC1F,CAAC;QACF,WAAW,GAAG,QAAQ,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Сохранённый вход нечитаем: перечислить несоблюдённые поля нечем. */
|
|
2
|
+
export declare const UNREADABLE_RUN_INPUT_WARNING = "run input could not be read: parameters that are accepted but not honored cannot be listed for this run";
|
|
3
|
+
export declare function derivedRunWarnings(storedInput: unknown): string[];
|
|
4
|
+
/** Полный `warnings[]` рана. Выводимое первым — тем же порядком, что у `quote`. */
|
|
5
|
+
export declare function composeRunWarnings(stored: unknown, storedInput: unknown): string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { makeUgcInput, resolvedScript } from "./skills.js";
|
|
2
|
+
import { dispositionWarnings } from "./contract-dispositions.js";
|
|
3
|
+
import { buildVoiceWarnings } from "./voices.js";
|
|
4
|
+
// Писатели хранят невыводимое, читатель выводит статику. Рационал — docs/18, Д41.
|
|
5
|
+
/** Сохранённый вход нечитаем: перечислить несоблюдённые поля нечем. */
|
|
6
|
+
export const UNREADABLE_RUN_INPUT_WARNING = "run input could not be read: parameters that are accepted but not honored cannot be listed for this run";
|
|
7
|
+
// Реестр диспозиций и языковой warning пресета. Почему без `voice_id` —
|
|
8
|
+
// docs/18, Д41.
|
|
9
|
+
export function derivedRunWarnings(storedInput) {
|
|
10
|
+
const parsed = makeUgcInput.safeParse(storedInput);
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
return [UNREADABLE_RUN_INPUT_WARNING];
|
|
13
|
+
}
|
|
14
|
+
const input = parsed.data;
|
|
15
|
+
return [
|
|
16
|
+
...dispositionWarnings(input),
|
|
17
|
+
...buildVoiceWarnings({ voice: input.voice, script: resolvedScript(input) }),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
/** Строки колонки `warnings`; нечитаемое значение даёт пустой список. */
|
|
21
|
+
function storedStrings(stored) {
|
|
22
|
+
if (!Array.isArray(stored))
|
|
23
|
+
return [];
|
|
24
|
+
return stored.filter((item) => typeof item === "string");
|
|
25
|
+
}
|
|
26
|
+
/** Полный `warnings[]` рана. Выводимое первым — тем же порядком, что у `quote`. */
|
|
27
|
+
export function composeRunWarnings(stored, storedInput) {
|
|
28
|
+
return [...new Set([...derivedRunWarnings(storedInput), ...storedStrings(stored)])];
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=run-static-warnings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-static-warnings.js","sourceRoot":"","sources":["../src/run-static-warnings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,kFAAkF;AAElF,uEAAuE;AACvE,MAAM,CAAC,MAAM,4BAA4B,GACvC,yGAAyG,CAAC;AAE5G,wEAAwE;AACxE,gBAAgB;AAChB,MAAM,UAAU,kBAAkB,CAAC,WAAoB;IACrD,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACxC,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAC1B,OAAO;QACL,GAAG,mBAAmB,CAAC,KAAK,CAAC;QAC7B,GAAG,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;KAC7E,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,SAAS,aAAa,CAAC,MAAe;IACpC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,kBAAkB,CAAC,MAAe,EAAE,WAAoB;IACtE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,kBAAkB,CAAC,WAAW,CAAC,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC"}
|
package/dist/runs.d.ts
CHANGED
|
@@ -53,6 +53,9 @@ export declare const AI_DISCLOSURE_TEXT = "This video was generated with AI: the
|
|
|
53
53
|
* всегда и детерминированно, без разбора боксов MP4.
|
|
54
54
|
*/
|
|
55
55
|
export declare const AI_DISCLOSURE_OBJECT_METADATA: Readonly<Record<string, string>>;
|
|
56
|
+
/** Форма идентификатора рана. Отдельной схемой, потому что её спрашивают и на */
|
|
57
|
+
/** ВХОДЕ — MCP-тул `get_run`, где голый `z.string()` пускал мусор до сети. */
|
|
58
|
+
export declare const runId: z.ZodString;
|
|
56
59
|
export declare const run: z.ZodObject<{
|
|
57
60
|
run_id: z.ZodString;
|
|
58
61
|
skill: z.ZodString;
|
package/dist/runs.js
CHANGED
|
@@ -49,13 +49,30 @@ export const AI_DISCLOSURE_OBJECT_METADATA = {
|
|
|
49
49
|
generator: "clipwright",
|
|
50
50
|
"ai-disclosure": AI_DISCLOSURE_TEXT,
|
|
51
51
|
};
|
|
52
|
+
/** Форма идентификатора рана. Отдельной схемой, потому что её спрашивают и на */
|
|
53
|
+
/** ВХОДЕ — MCP-тул `get_run`, где голый `z.string()` пускал мусор до сети. */
|
|
54
|
+
export const runId = z.string().regex(/^run_[a-zA-Z0-9]+$/);
|
|
52
55
|
export const run = z.object({
|
|
53
|
-
run_id:
|
|
56
|
+
run_id: runId,
|
|
54
57
|
skill: z.string(),
|
|
55
58
|
state: runState,
|
|
56
59
|
credits_reserved: z.number().int().nonnegative(),
|
|
57
60
|
credits_charged: z.number().int().nonnegative().nullable(),
|
|
58
61
|
warnings: z.array(z.string()).default([]),
|
|
62
|
+
/**
|
|
63
|
+
* Причина. НЕПУСТОЙ `error` НЕ ЗНАЧИТ `state: "failed"` — и это стало правдой
|
|
64
|
+
* с clipwright#48, поэтому сказано здесь, в контракте, а не только в коде.
|
|
65
|
+
*
|
|
66
|
+
* Второй случай: ран, не доживший до расчёта, чей счёт записал предыдущий
|
|
67
|
+
* проход сборщика. Его исход НЕИЗВЕСТЕН — успел он отдать видео или упал,
|
|
68
|
+
* по строке рана не различить, — поэтому состояние остаётся нетерминальным
|
|
69
|
+
* (врать о нём нельзя), а `error` несёт пометку «нужен разбор человеком».
|
|
70
|
+
* Пишет её `markRunForReview`.
|
|
71
|
+
*
|
|
72
|
+
* ЧТО ИЗ ЭТОГО СЛЕДУЕТ КЛИЕНТУ: опрос по-прежнему ждёт `succeeded` либо
|
|
73
|
+
* `failed`, а `error` при нетерминальном состоянии читается как объяснение
|
|
74
|
+
* задержки, а не как исход. Ран в этом состоянии сам не завершится.
|
|
75
|
+
*/
|
|
59
76
|
error: z.string().nullable(),
|
|
60
77
|
final_output: z
|
|
61
78
|
.object({
|
package/dist/runs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,qDAAqD;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,QAAQ;IACR,WAAW;IACX,KAAK;IACL,QAAQ;IACR,aAAa;IACb,WAAW;IACX,WAAW;IACX,QAAQ;CACA,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAG3C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC5D,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC7C,SAAS,EAAE,CAAC;SACT,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;SACrF,OAAO,CAAC,EAAE,CAAC;CACf,CAAC,CAAC;AAEH;;;;GAIG;AACH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC7B,wFAAwF,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAqC;IAC7E,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,YAAY;IACvB,eAAe,EAAE,kBAAkB;CACpC,CAAC;AAEF,MAAM,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,qDAAqD;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,QAAQ;IACR,WAAW;IACX,KAAK;IACL,QAAQ;IACR,aAAa;IACb,WAAW;IACX,WAAW;IACX,QAAQ;CACA,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAG3C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC5D,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC7C,SAAS,EAAE,CAAC;SACT,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;SACrF,OAAO,CAAC,EAAE,CAAC;CACf,CAAC,CAAC;AAEH;;;;GAIG;AACH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC7B,wFAAwF,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAqC;IAC7E,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,YAAY;IACvB,eAAe,EAAE,kBAAkB;CACpC,CAAC;AAEF,iFAAiF;AACjF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1B,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,KAAK,EAAE,QAAQ;IACf,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAChD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IAC1D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACzC;;;;;;;;;;;;;OAaG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,YAAY,EAAE,CAAC;SACZ,MAAM,CAAC;QACN,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QAC3B,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QAClD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QACzC,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QAChD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACnC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACvC;;;;;;;WAOG;QACH,YAAY,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QAC7B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAChC;;;;;;;;;;;;;WAaG;QACH,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7C,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC1C,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACzC,wDAAwD;QACxD,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC1C,CAAC;SACD,QAAQ,EAAE;IACb,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACnC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,WAAoB,CAAC;AACpD,MAAM,CAAC,MAAM,YAAY,GAAG,QAAiB,CAAC;AAE9C,8EAA8E;AAC9E,wEAAwE;AACxE,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,eAAe,EAAE,YAAY,CAAU,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAGzD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAQ,eAAqC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
|
package/dist/skills.d.ts
CHANGED
|
@@ -45,7 +45,23 @@ export declare const DEV_RESOLUTION: Resolution;
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function resolveResolution(requested: Resolution | undefined, isDev: boolean): Resolution;
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* ⚠️ ЭТО БАЗИС ВЫВОДА ПОТОЛКА СЛОВ, А НЕ ПОТОЛОК СЕКУНД, и имя переименовано
|
|
49
|
+
* ровно поэтому (находка внешней приёмки, круг 23). Прежнее — `MAX_SCRIPT_SECONDS`
|
|
50
|
+
* — читалось как «максимум секунд» и дважды подряд стало источником ДЕНЕЖНОГО
|
|
51
|
+
* дефекта на публичной странице цены: ею зажимали длительность, обещая клиенту
|
|
52
|
+
* меньше возможного счёта. Схема ограничивает только СЛОВА; сколько из них выйдет
|
|
53
|
+
* секунд, не ограничено ничем, потому что гейт пейсинга удалён.
|
|
54
|
+
*
|
|
55
|
+
* Число осталось прежним, роль названа честно: это множимое, из которого вместе
|
|
56
|
+
* с быстрым краем пейсинга получается `MAX_SCRIPT_WORDS`.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ ИСХОДНЫЙ ДОВОД НИЖЕ ОПИСЫВАЕТ ПОЛОЖЕНИЕ ДО УДАЛЕНИЯ ГЕЙТА ПЕЙСИНГА и
|
|
59
|
+
* оставлен как история, а не как действующее объяснение (находка внешней
|
|
60
|
+
* приёмки, круг 24): он объясняет число через «~45с озвучки» и пейсинг-гейт,
|
|
61
|
+
* которого с 2026-07-22 не существует. Действующее объяснение — абзац выше:
|
|
62
|
+
* это множимое, и ограничивает контракт только слова.
|
|
63
|
+
*
|
|
64
|
+
* Исходный довод (docs/12 §11) — временная замена
|
|
49
65
|
* мульти-тейка (docs/12 §3: длинные скрипты раньше резались на несколько
|
|
50
66
|
* тейков, эта логика вырезана на время прототипа). Пока мульти-тейк не
|
|
51
67
|
* вернули, скрипт длиннее одного тейка нужно отклонять на входе, а не
|
|
@@ -56,8 +72,17 @@ export declare function resolveResolution(requested: Resolution | undefined, isD
|
|
|
56
72
|
* потолок секунд без нарушения пейсинг-гейта. Числа пейсинга не дублируем,
|
|
57
73
|
* берём готовые константы из pacing.ts.
|
|
58
74
|
*/
|
|
59
|
-
export declare const
|
|
75
|
+
export declare const SCRIPT_SECONDS_BASIS = 45;
|
|
60
76
|
export declare const MAX_SCRIPT_WORDS: number;
|
|
77
|
+
/** Потолок длины ЛЮБОГО адреса из чужого тела: без него поле берёт весь потолок */
|
|
78
|
+
/** тела запроса. Число — верхний предел, который переваривает любой браузер и CDN. */
|
|
79
|
+
export declare const MAX_URL_LENGTH = 2048;
|
|
80
|
+
/** ЧТО ДЕЛАЕТ `make_ugc` — один текст на обе контрактные поверхности: публичный */
|
|
81
|
+
/** REST-каталог и MCP `tools/list`. Разойдясь, они описали бы РАЗНЫЙ скилл. */
|
|
82
|
+
export declare const MAKE_UGC_DESCRIPTION: string;
|
|
83
|
+
/** Как ЗВАТЬ скилл по MCP. Отдельно от описания выше: имена соседних тулов и */
|
|
84
|
+
/** поллинг — свойства протокола, и REST-каталогу они не адресованы. */
|
|
85
|
+
export declare const MAKE_UGC_AGENT_PROTOCOL: string;
|
|
61
86
|
/**
|
|
62
87
|
* Сырой zod-shape входа make_ugc — БЕЗ `.refine()`.
|
|
63
88
|
*
|