@andrey4emk/npm-app-back-b24 3.6.1 → 3.7.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/README.md +51 -2
- package/bitrix24/b24.ts +424 -46
- package/bitrix24/eventB24.ts +5 -4
- package/package.json +1 -1
- package/sendMessage/smsgold.ts +5 -4
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
reinitializeB24,
|
|
55
55
|
stopProactiveRefresh,
|
|
56
56
|
getResultData,
|
|
57
|
+
callProtected,
|
|
57
58
|
errorB24,
|
|
58
59
|
event,
|
|
59
60
|
Event,
|
|
@@ -122,7 +123,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
122
123
|
|
|
123
124
|
Модуль работает с токенами через файл `authB24.json` (используется библиотека `conf`). Модель БД не требуется.
|
|
124
125
|
|
|
125
|
-
- **`$b24`** — готовый экземпляр `B24OAuth
|
|
126
|
+
- **`$b24`** — готовый экземпляр `B24Client` (`B24OAuth` плюс методы фасада, см. ниже), создаётся автоматически при импорте на основе сохранённых токенов. Если данные авторизации отсутствуют — `null`.
|
|
126
127
|
|
|
127
128
|
```js
|
|
128
129
|
import { $b24 } from "@andrey4emk/npm-app-back-b24";
|
|
@@ -137,9 +138,19 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
137
138
|
- Окружение (`DEV`/`PROD`) определяется переменной `APP_ENV` — используется как ключ секции в конфиге авторизации.
|
|
138
139
|
- Признак того, что проект использует OAuth, — заданные `APP_B24_CLIENT_ID` и `APP_B24_CLIENT_SECRET`. Если их нет, `$b24` молча становится `null` (уровень `debug`). Если они заданы, но токенов в `authB24.json` не хватает, пишется `error` — это уже настоящая проблема конфигурации.
|
|
139
140
|
|
|
141
|
+
**Методы `callMethod`, `callListMethod`, `fetchListMethod`, `callBatch`, `callBatchByChunk` — фасад пакета.**
|
|
142
|
+
|
|
143
|
+
SDK 2.x помечает эти пять методов к удалению в следующем major и на каждый вызов пишет предупреждение об устаревании. Пакет реализует их сам поверх `actions.v2.*`, поэтому вызовы у потребителей не меняются, а зависимости от удаляемого API у пакета больше нет. Сигнатуры и типы возврата прежние — правок в проектах не требуется.
|
|
144
|
+
|
|
145
|
+
Поведение прежнее у четырёх методов из пяти. **`callListMethod` отличается в двух местах:** он бросает внятную ошибку с именем метода там, где SDK падал `TypeError` из своих недр (портал вернул «мягкую» ошибку, ответ не список, конверт нечитаем, `next` не растёт), и у него есть потолок в 1000 страниц — при упоре в него вызов тоже бросает, а не отдаёт молча обрезанный список. Если у вас есть `try`/`catch` вокруг постраничных выборок, текст ошибки изменится; если его нет — падение станет заметнее, но не появится там, где раньше всё работало.
|
|
146
|
+
|
|
147
|
+
Прямой доступ к `$b24.actions.*` остаётся: это сырой SDK **без** retry и гейта идемпотентности. Если защита нужна, оборачивайте вызов в `callProtected()`.
|
|
148
|
+
|
|
140
149
|
**Retry при сетевых ошибках:**
|
|
141
150
|
|
|
142
|
-
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch`, а также к обновлению токена (`refreshAuth`). Метод `fetchListMethod` из ретраев исключён — это async-генератор, оборачивать его в async-функцию
|
|
151
|
+
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch`, а также к обновлению токена (`refreshAuth`). Метод `fetchListMethod` из ретраев исключён — это async-генератор, оборачивать его в async-функцию нельзя. `callBatchByChunk` пакет реализует, но не ретраит.
|
|
152
|
+
|
|
153
|
+
`callListMethod` повторяется целиком: при сетевом сбое пагинация начинается с нулевой страницы заново. Постраничный retry был бы сменой семантики, поэтому поведение оставлено прежним.
|
|
143
154
|
|
|
144
155
|
| Параметр | Значение |
|
|
145
156
|
| -------------- | ----------------------------------------- |
|
|
@@ -160,6 +171,10 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
160
171
|
|
|
161
172
|
У самого SDK есть собственный слой ретраев (`maxRetries: 3`), но он работает только при rate limit (HTTP 429, 503) и при распознанных `NETWORK_ERROR` / `REQUEST_TIMEOUT`. Замаскированные транспортные сбои приходят с кодом `JSSDK_UNKNOWN_ERROR`, который входит в `BUILT_IN_HARD_ERROR_CODES` — на таких ошибках SDK делает одну попытку, и повторяет только Proxy.
|
|
162
173
|
|
|
174
|
+
**Диагностика SDK.** Начиная с 3.7.0 собственные сообщения SDK от уровня `WARNING` и выше попадают в лог пакета с префиксом `SDK B24:` вместе с контекстом (`requestId`, `method`, `status`, код ошибки) — например предупреждение `fetchList` про игнорируемый `order` или про остановку пагинации, когда `idKey` не совпал с полем ответа. Порог нужен: на уровне `info` SDK пишет две строки на каждый запрос. Ошибки SDK приходят как `warn`, а не `error` — это диагностика отдельной неуспешной попытки, а окончательный провал вызова один раз пишет retry-слой.
|
|
175
|
+
|
|
176
|
+
Ответы со статусом 4xx уходят на `debug`: лимитер SDK пишет через свой `error()` любой 4xx кроме 408 и 429, а Bitrix24 отдаёт 400 на штатные «мягкие» ошибки вроде несуществующей сущности. Без этого проверка существования в цикле давала бы строку на каждой итерации. Разбирайте такие ответы через `getResultData()` — это нормальный путь, а не сбой.
|
|
177
|
+
|
|
163
178
|
**«Мягкие» ошибки Bitrix24:**
|
|
164
179
|
|
|
165
180
|
Часть ошибок (`ERROR_ENTITY_NOT_FOUND`, `BITRIX_REST_V3_EXCEPTION_*`) SDK не бросает исключением, а возвращает как `AjaxResult` с ошибкой: `isSuccess === false`, и тогда `response.getData()` вернёт `undefined`. Отдельный случай — успешный ответ, у которого сам `result` пустой (`null`/`undefined`). Чтобы не падать на `undefined`, разбирайте ответы через `getResultData()` — он покрывает обе ситуации и бросает понятную ошибку с именем метода.
|
|
@@ -244,6 +259,40 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
244
259
|
const deal = getResultData(response, "crm.deal.get");
|
|
245
260
|
```
|
|
246
261
|
|
|
262
|
+
- **`callProtected(run, methodName)`** — прогоняет произвольный вызов `$b24.actions.*` через тот же retry и гейт идемпотентности, что и методы фасада. Нужен, когда требуется API, которого у фасада нет: `FilterV3`, keyset-пагинация (`callTail`/`fetchTail`), `aggregate`.
|
|
263
|
+
|
|
264
|
+
- **Параметры:**
|
|
265
|
+
- `run` (`() => Promise<T>`) — сам вызов.
|
|
266
|
+
- `methodName` (`string | string[]`) — имя REST-метода B24 (**не** метода SDK) либо список имён, если внутри batch: по ним гейт решает, создаёт ли вызов сущность.
|
|
267
|
+
- **Возвращает:** промис с результатом `run`.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
import { $b24, callProtected } from "@andrey4emk/npm-app-back-b24";
|
|
271
|
+
|
|
272
|
+
const response = await callProtected(
|
|
273
|
+
() => $b24.actions.v3.call.make({ method: "crm.item.list", params }),
|
|
274
|
+
"crm.item.list"
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Для batch перечисляйте методы команд, а не "batch"
|
|
278
|
+
const batchResponse = await callProtected(
|
|
279
|
+
() => $b24.actions.v3.batch.make({ calls }),
|
|
280
|
+
["crm.item.get", "crm.item.add"]
|
|
281
|
+
);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Гейт работает fail-closed.** Если список имён пуст или хоть одно имя не похоже на REST-метод (легальные всегда с точкой — `crm.deal.add`, `disk.folder.uploadfile`), вызов считается создающим и не повторяется. Поэтому `callProtected(() => batch.make({ calls }), "batch")` защиту не обойдёт: имя `batch` не разбирается, и батч с `crm.deal.add` внутри не будет повторён пять раз при `status: 0`. Цена ошибки в эту сторону — лишняя необработанная сетевая ошибка; в обратную — дубликаты сущностей.
|
|
285
|
+
|
|
286
|
+
Без обёртки вызов `$b24.actions.*` идёт мимо защиты: ни повторов при сетевых сбоях, ни блокировки повтора для создающих методов.
|
|
287
|
+
|
|
288
|
+
- **`B24Client`** — экспортируемый тип: `B24OAuth` плюс пять методов, которые пакет реализует сам. Объявлен как `interface B24Client extends B24OAuth`, поэтому `$b24` по-прежнему принимается везде, где ожидается `B24OAuth` — например в `new Smsgold(auth, $b24)`.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import type { B24Client } from "@andrey4emk/npm-app-back-b24";
|
|
292
|
+
|
|
293
|
+
function sync(client: B24Client) { /* ... */ }
|
|
294
|
+
```
|
|
295
|
+
|
|
247
296
|
### Задачи ошибок
|
|
248
297
|
|
|
249
298
|
- **`errorB24(dataTask)`** — создаёт служебную задачу в Bitrix24 при ошибках/событиях. Использует глобальный `$b24`.
|
package/bitrix24/b24.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import { B24OAuth, AjaxError, SdkError, Logger } from "@bitrix24/b24jssdk";
|
|
2
|
-
import type {
|
|
1
|
+
import { B24OAuth, AjaxError, SdkError, Logger, LogLevel, Result } from "@bitrix24/b24jssdk";
|
|
2
|
+
import type {
|
|
3
|
+
B24OAuthParams,
|
|
4
|
+
B24OAuthSecret,
|
|
5
|
+
AuthData,
|
|
6
|
+
AjaxResult,
|
|
7
|
+
TypeCallParams,
|
|
8
|
+
BatchCommandsArrayUniversal,
|
|
9
|
+
BatchCommandsObjectUniversal,
|
|
10
|
+
BatchNamedCommandsUniversal,
|
|
11
|
+
Handler,
|
|
12
|
+
LogRecord,
|
|
13
|
+
Formatter,
|
|
14
|
+
} from "@bitrix24/b24jssdk";
|
|
3
15
|
import { logs } from "../logs/logs.ts";
|
|
4
16
|
import { isNetworkError, isPreConnectionError } from "../utils/fetchRetry.ts";
|
|
5
17
|
import Conf from "conf";
|
|
@@ -17,6 +29,49 @@ interface SaveResult {
|
|
|
17
29
|
message: string;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/**
|
|
33
|
+
* `B24OAuth` плюс пять методов, которые пакет реализует сам поверх `actions.v2.*`.
|
|
34
|
+
*
|
|
35
|
+
* SDK 2.x помечает `callMethod`/`callListMethod`/`fetchListMethod`/`callBatch`/`callBatchByChunk`
|
|
36
|
+
* к удалению в следующем major. Пакет предоставляет их сам, поэтому сигнатуры для потребителя
|
|
37
|
+
* не меняются, а зависимости от удаляемого API у нас больше нет.
|
|
38
|
+
*
|
|
39
|
+
* Тип расширяет `B24OAuth`, поэтому `$b24` по-прежнему передаётся туда, где ждут `B24OAuth`
|
|
40
|
+
* (`new Smsgold(auth, $b24)`, `new Event($b24)`).
|
|
41
|
+
*/
|
|
42
|
+
export interface B24Client extends B24OAuth {
|
|
43
|
+
callMethod<T = any>(method: string, params?: object, start?: number): Promise<AjaxResult<T>>;
|
|
44
|
+
callListMethod(method: string, params?: object, progress?: null | ((progress: number) => void), customKeyForResult?: string | null): Promise<Result>;
|
|
45
|
+
fetchListMethod(method: string, params?: any, idKey?: string, customKeyForResult?: string | null): AsyncGenerator<any[]>;
|
|
46
|
+
callBatch(calls: Array<any> | object, isHaltOnError?: boolean, returnAjaxResult?: boolean): Promise<Result>;
|
|
47
|
+
callBatchByChunk(calls: Array<any>, isHaltOnError: boolean): Promise<Result>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Минимальный контракт «умеет вызывать REST-методы B24».
|
|
52
|
+
*
|
|
53
|
+
* Нужен внутренним классам (`Event`, `Smsgold`), которые принимают экземпляр извне.
|
|
54
|
+
* Объявляет `callMethod` сам, поэтому переживёт удаление метода из `B24OAuth`.
|
|
55
|
+
*
|
|
56
|
+
* Объединение `B24OAuth | B24Client` эту задачу не решает: `B24Client` расширяет
|
|
57
|
+
* `B24OAuth`, union схлопывается в супертип, а вызов метода на union требует его
|
|
58
|
+
* наличия в обеих ветках — то есть после удаления из SDK тайпчек сломается всё равно.
|
|
59
|
+
*
|
|
60
|
+
* Структурный тип шире `B24OAuth`, поэтому публичные сигнатуры конструкторов
|
|
61
|
+
* не сужаются: и `B24OAuth`, и `$b24`, и любой свой объект с `callMethod` подходят.
|
|
62
|
+
*/
|
|
63
|
+
export interface B24MethodCaller {
|
|
64
|
+
// Дженерика здесь быть не должно: у SDK метод не generic, и `B24OAuth`
|
|
65
|
+
// потребителя перестал бы подходить под этот тип. Проверено тайпчеком
|
|
66
|
+
callMethod(method: string, params?: object, start?: number): Promise<AjaxResult>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Набор методов фасада — то, что Proxy отдаёт вместо реализаций SDK */
|
|
70
|
+
type FacadeMethods = Pick<B24Client, "callMethod" | "callListMethod" | "fetchListMethod" | "callBatch" | "callBatchByChunk">;
|
|
71
|
+
|
|
72
|
+
/** Формы списка команд batch, которые принимает actions.v2 */
|
|
73
|
+
type BatchCalls = BatchCommandsArrayUniversal | BatchCommandsObjectUniversal | BatchNamedCommandsUniversal;
|
|
74
|
+
|
|
20
75
|
// ==================== Константы ====================
|
|
21
76
|
|
|
22
77
|
const CONFIG_DIR = process.env.CONFIG_DIR || "../config";
|
|
@@ -34,9 +89,21 @@ const RETRY_COUNT = 5;
|
|
|
34
89
|
/** Задержка между попытками (мс) */
|
|
35
90
|
const RETRY_DELAY_MS = 500;
|
|
36
91
|
|
|
37
|
-
/** Методы
|
|
92
|
+
/** Методы фасада, оборачиваемые retry-логикой (fetchListMethod — async-генератор, его оборачивать нельзя) */
|
|
38
93
|
const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch"]);
|
|
39
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Имена, которые Proxy резолвит в собственную реализацию пакета поверх `actions.v2.*`.
|
|
97
|
+
*
|
|
98
|
+
* Шире, чем RETRYABLE_METHODS: `fetchListMethod` и `callBatchByChunk` мы реализуем,
|
|
99
|
+
* но не ретраим. `callBatchByChunk` в наборе обязателен — пока хоть один deprecated-метод
|
|
100
|
+
* SDK достижим через `$b24`, он будет писать предупреждение об устаревании.
|
|
101
|
+
*/
|
|
102
|
+
const FACADE_METHODS = new Set(["callMethod", "callListMethod", "fetchListMethod", "callBatch", "callBatchByChunk"]);
|
|
103
|
+
|
|
104
|
+
/** Потолок числа страниц в callListMethod — страховка от бесконечной пагинации */
|
|
105
|
+
const LIST_MAX_PAGES = 1000;
|
|
106
|
+
|
|
40
107
|
/**
|
|
41
108
|
* Коды транспортного сбоя. Один набор и для SdkError (AjaxError, RefreshTokenError),
|
|
42
109
|
* и для вложенного AxiosError — списки совпадали, держать их раздельно смысла нет.
|
|
@@ -95,6 +162,102 @@ export function getResultData<T = any>(response: AjaxResult, methodName: string)
|
|
|
95
162
|
return data.result as T;
|
|
96
163
|
}
|
|
97
164
|
|
|
165
|
+
/** Конверт ответа restApi:v2 — то, что прислал портал, до того как AjaxResult его урезал */
|
|
166
|
+
interface V2Envelope {
|
|
167
|
+
result?: unknown;
|
|
168
|
+
next?: unknown;
|
|
169
|
+
total?: unknown;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Читает сырой конверт ответа из AjaxResult.
|
|
174
|
+
*
|
|
175
|
+
* `getData()` отдаёт замороженную пару `{ result, time }` — полей `next` и `total`
|
|
176
|
+
* в ней нет намеренно (в restApi:v3 их не существует), а `isMore()`/`getTotal()`/`getNext()`
|
|
177
|
+
* помечены `@removed 2.0.0` и читают тот же приватный конверт. Единственный способ узнать
|
|
178
|
+
* смещение следующей страницы, не опираясь на удаляемое API, — прочитать конверт напрямую:
|
|
179
|
+
* `_data` объявлен `protected`, а не приватным полем класса, поэтому в рантайме доступен.
|
|
180
|
+
*
|
|
181
|
+
* Это единственная точка связи с внутренностями SDK. Если она перестанет работать,
|
|
182
|
+
* она обязана упасть громко: тихо оборванная на первой странице выборка — потеря данных.
|
|
183
|
+
*/
|
|
184
|
+
function readV2Envelope(response: AjaxResult, method: string): V2Envelope {
|
|
185
|
+
const envelope = (response as unknown as { _data?: unknown })._data;
|
|
186
|
+
|
|
187
|
+
if (!envelope || typeof envelope !== "object") {
|
|
188
|
+
throw new Error(`${method}: не удалось прочитать конверт ответа Bitrix24 (AjaxResult._data недоступен) — изменился внутренний формат SDK`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return envelope as V2Envelope;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ==================== Логгер SDK ====================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Мост из логгера SDK в `logs` пакета.
|
|
198
|
+
*
|
|
199
|
+
* Порог WARNING обязателен: SDK пишет `post/send` и `post/response` на уровне `info`
|
|
200
|
+
* на каждый запрос и `http batch request starting/completed` на `debug` — без фильтра
|
|
201
|
+
* это залило бы лог.
|
|
202
|
+
*
|
|
203
|
+
* `AbstractHandler` объявлен в типах SDK, но в рантайме не экспортируется,
|
|
204
|
+
* поэтому реализуем интерфейс `Handler` обычным классом.
|
|
205
|
+
*/
|
|
206
|
+
class B24SdkLogHandler implements Handler {
|
|
207
|
+
private formatter: Formatter | null = null;
|
|
208
|
+
|
|
209
|
+
isHandling(level: LogLevel): boolean {
|
|
210
|
+
return level >= LogLevel.WARNING;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
shouldBubble(): boolean {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setFormatter(formatter: Formatter): void {
|
|
218
|
+
this.formatter = formatter;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getFormatter(): Formatter | null {
|
|
222
|
+
return this.formatter;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async handle(record: LogRecord): Promise<boolean> {
|
|
226
|
+
// Бросать отсюда нельзя ни при каких обстоятельствах: Logger.log() делает
|
|
227
|
+
// await handle(), но лимитер зовёт логгер без await — исключение стало бы
|
|
228
|
+
// unhandled rejection и на дефолтных настройках Node убило бы процесс.
|
|
229
|
+
// Путь к броску реален: logs.add() читает conf, а тот перечитывает файл
|
|
230
|
+
// на каждом обращении и падает на битом log.json. В catch пишем через
|
|
231
|
+
// console.error, а не через logs — иначе рискуем зациклиться на той же ошибке.
|
|
232
|
+
try {
|
|
233
|
+
const status = Number(record.context?.status);
|
|
234
|
+
|
|
235
|
+
// Лимитер SDK через error() пишет любой 4xx кроме 408/429 как
|
|
236
|
+
// «non-retryable client error». Портал отдаёт 400 на штатные «мягкие»
|
|
237
|
+
// ошибки (несуществующая сущность), которые README описывает как
|
|
238
|
+
// нормальный путь через getResultData(): проверка существования в цикле
|
|
239
|
+
// дала бы строку на итерацию. Такие сообщения — уровень debug.
|
|
240
|
+
//
|
|
241
|
+
// Остальное: уровень ERROR у SDK — это диагностика отдельной неуспешной
|
|
242
|
+
// попытки, а таких попыток на один вызов до пятнадцати (3 внутри SDK × 5 наших).
|
|
243
|
+
// Уровень error пакета уходит в чат B24, поэтому маппим SDK-ERROR в warn:
|
|
244
|
+
// окончательный провал вызова логирует retry-слой, и он туда попадёт ровно один раз.
|
|
245
|
+
const level = status >= 400 && status < 500 ? "debug" : record.level >= LogLevel.CRITICAL ? "error" : "warn";
|
|
246
|
+
|
|
247
|
+
// Контекст SDK (requestId, method, code, wait, status) уже прогнан
|
|
248
|
+
// через redactSensitiveParams — токены в него не попадают
|
|
249
|
+
const context = record.context && Object.keys(record.context).length > 0 ? record.context : undefined;
|
|
250
|
+
|
|
251
|
+
logs.add(`SDK B24: ${record.message}`, level, context);
|
|
252
|
+
} catch (error: unknown) {
|
|
253
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
254
|
+
console.error(`B24SdkLogHandler: не удалось записать сообщение SDK — ${msg}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
98
261
|
// ==================== Создание экземпляра B24OAuth ====================
|
|
99
262
|
|
|
100
263
|
function createB24Instance(): B24OAuth | null {
|
|
@@ -142,10 +305,11 @@ function createB24Instance(): B24OAuth | null {
|
|
|
142
305
|
|
|
143
306
|
const b24 = new B24OAuth(authParams, secret);
|
|
144
307
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
308
|
+
// Диагностика SDK от WARNING и выше уходит в logs пакета: предупреждения
|
|
309
|
+
// callList/fetchList про игнорируемый order и остановку пагинации, сообщения лимитера.
|
|
310
|
+
// Дефолтный NullLogger не годится — LoggerFactory.forcedLog пишет мимо логгера
|
|
311
|
+
// прямо в console.warn, но после перехода на фасад этот путь у нас недостижим.
|
|
312
|
+
b24.setLogger(Logger.create("npm-app-back-b24").pushHandler(new B24SdkLogHandler()));
|
|
149
313
|
|
|
150
314
|
return b24;
|
|
151
315
|
}
|
|
@@ -415,71 +579,277 @@ function getRetryBlockReason(sdkMethod: string, args: any[]): string | null {
|
|
|
415
579
|
return blocking.length > 0 ? blocking.join(", ") : null;
|
|
416
580
|
}
|
|
417
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Общий цикл повторов при сетевых ошибках.
|
|
584
|
+
*
|
|
585
|
+
* @param fn — вызов без аргументов, уже замкнутый на нужные параметры
|
|
586
|
+
* @param label — префикс лог-строк, по нему в логе видно источник повтора
|
|
587
|
+
* @param blockReason — причина, по которой повтор запрещён (создающий вызов), либо null
|
|
588
|
+
*/
|
|
589
|
+
async function runWithRetry<T>(fn: () => Promise<T>, label: string, blockReason: string | null): Promise<T> {
|
|
590
|
+
let lastError: unknown;
|
|
591
|
+
|
|
592
|
+
for (let attempt = 1; attempt <= RETRY_COUNT; attempt++) {
|
|
593
|
+
try {
|
|
594
|
+
return await fn();
|
|
595
|
+
} catch (error: unknown) {
|
|
596
|
+
lastError = error;
|
|
597
|
+
|
|
598
|
+
// Ошибка не сетевая (бизнес-логика B24, неверные параметры) — отдаём
|
|
599
|
+
// вызывающему коду как есть, он решает, что с ней делать
|
|
600
|
+
if (!isB24NetworkError(error)) throw error;
|
|
601
|
+
|
|
602
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
603
|
+
const code = error instanceof SdkError ? ` [${error.code}]` : "";
|
|
604
|
+
|
|
605
|
+
// Окончательные отказы логируем на error: собственные модули пакета
|
|
606
|
+
// (errorB24, Event, Smsgold) ошибку только возвращают вызывающему коду,
|
|
607
|
+
// но не пишут в лог — без этих строк сбой $b24 не виден нигде
|
|
608
|
+
|
|
609
|
+
if (attempt === RETRY_COUNT) {
|
|
610
|
+
logs.add(`${label}${code}: исчерпаны все ${RETRY_COUNT} попыток — ${msg}`, "error");
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Ошибка со status 0 может означать и «запрос не ушёл», и «запрос выполнен,
|
|
615
|
+
// а ответ не разобрался»: различить их нельзя, поэтому создающие вызовы
|
|
616
|
+
// не повторяем — лучше вернуть ошибку, чем создать дубликат
|
|
617
|
+
if (blockReason) {
|
|
618
|
+
logs.add(`${label}${code}: повтор отменён, вызов создаёт сущности (${blockReason}) — ${msg}`, "error");
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Промежуточные попытки — warn: в чат B24 уходит только уровень error,
|
|
623
|
+
// иначе один упавший вызов дал бы пять сообщений
|
|
624
|
+
logs.add(`${label}${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "warn");
|
|
625
|
+
|
|
626
|
+
await delay(RETRY_DELAY_MS);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Недостижимо при RETRY_COUNT >= 1, нужно для TypeScript
|
|
631
|
+
throw lastError;
|
|
632
|
+
}
|
|
633
|
+
|
|
418
634
|
/** Оборачивает async-функцию retry-логикой при сетевых ошибках */
|
|
419
635
|
function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: any, methodName: string): T {
|
|
420
636
|
return (async (...args: any[]) => {
|
|
421
637
|
// Состав вызова между попытками не меняется — разбираем один раз
|
|
422
638
|
const blockReason = getRetryBlockReason(methodName, args);
|
|
423
|
-
|
|
639
|
+
return runWithRetry(() => fn.apply(context, args), `$b24.${methodName}`, blockReason);
|
|
640
|
+
}) as T;
|
|
641
|
+
}
|
|
424
642
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Прогоняет произвольный вызов `actions.*` через тот же retry и гейт идемпотентности,
|
|
645
|
+
* что и методы фасада. Прямой `$b24.actions.v3.call.make()` идёт мимо защиты — если она
|
|
646
|
+
* нужна (например, ради `FilterV3` или keyset-пагинации), вызов оборачивают этой функцией.
|
|
647
|
+
*
|
|
648
|
+
* @param run — сам вызов, например `() => $b24.actions.v3.call.make({ method, params })`
|
|
649
|
+
* @param methodName — имя REST-метода B24 (не метода SDK) либо список имён, если внутри
|
|
650
|
+
* batch: по ним гейт решает, создаёт ли вызов сущность
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* const response = await callProtected(
|
|
654
|
+
* () => $b24!.actions.v3.call.make({ method: "crm.item.list", params }),
|
|
655
|
+
* "crm.item.list"
|
|
656
|
+
* );
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* const response = await callProtected(
|
|
660
|
+
* () => $b24!.actions.v3.batch.make({ calls }),
|
|
661
|
+
* ["crm.item.get", "crm.item.add"]
|
|
662
|
+
* );
|
|
663
|
+
*/
|
|
664
|
+
export async function callProtected<T>(run: () => Promise<T>, methodName: string | string[]): Promise<T> {
|
|
665
|
+
const names = (Array.isArray(methodName) ? methodName : [methodName]).map((name) => String(name).trim()).filter((name) => name.length > 0);
|
|
430
666
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
667
|
+
// Fail-closed, как в гейте callBatch: список пуст или имя не похоже на REST-метод
|
|
668
|
+
// (легальные имена всегда с точкой — crm.deal.add, disk.folder.uploadfile) — считаем
|
|
669
|
+
// вызов создающим. Иначе callProtected(() => batch.make({ calls }), "batch") прошёл бы
|
|
670
|
+
// мимо гейта, и батч с crm.deal.add внутри повторился бы до пяти раз при status 0.
|
|
671
|
+
const isUnparsed = names.length === 0 || names.some((name) => !name.includes("."));
|
|
672
|
+
const blocking = names.filter((name) => NON_IDEMPOTENT_METHOD_RE.test(name));
|
|
434
673
|
|
|
435
|
-
|
|
436
|
-
const code = error instanceof SdkError ? ` [${error.code}]` : "";
|
|
674
|
+
const blockReason = isUnparsed ? "имя метода не разобрано" : blocking.length > 0 ? blocking.join(", ") : null;
|
|
437
675
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// но не пишут в лог — без этих строк сбой $b24 не виден нигде
|
|
676
|
+
return runWithRetry(run, `actions:${names.join(", ") || "имя не указано"}`, blockReason);
|
|
677
|
+
}
|
|
441
678
|
|
|
442
|
-
|
|
443
|
-
logs.add(`$b24.${methodName}${code}: исчерпаны все ${RETRY_COUNT} попыток — ${msg}`, "error");
|
|
444
|
-
throw error;
|
|
445
|
-
}
|
|
679
|
+
// ==================== Фасад поверх actions.v2.* ====================
|
|
446
680
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Собственные реализации пяти методов, которые SDK 2.x помечает к удалению.
|
|
683
|
+
* Повторяют `AbstractB24` дословно, включая дефолты и порядок аргументов.
|
|
684
|
+
*
|
|
685
|
+
* Позиционные сигнатуры сохранены один в один: на этом держится то, что гейт
|
|
686
|
+
* идемпотентности (`getRetryBlockReason`) видит те же аргументы, что и раньше,
|
|
687
|
+
* — retry оборачивает фасад снаружи, а преобразование в объект опций
|
|
688
|
+
* происходит уже внутри.
|
|
689
|
+
*/
|
|
690
|
+
function createFacade(target: B24OAuth): Record<string, (...args: any[]) => any> {
|
|
691
|
+
// Геттер actions при каждом обращении проверяет инициализацию экземпляра —
|
|
692
|
+
// читаем его в момент вызова, а не один раз при создании фасада
|
|
693
|
+
const v2 = () => target.actions.v2;
|
|
694
|
+
|
|
695
|
+
const callMethod = async <T = any>(method: string, params?: object, start?: number): Promise<AjaxResult<T>> => {
|
|
696
|
+
const merged: TypeCallParams = { ...params };
|
|
697
|
+
|
|
698
|
+
// Явный params.start приоритетнее аргумента start — как в AbstractB24.callMethod
|
|
699
|
+
if (!("start" in merged && Number.isInteger(merged.start)) && Number.isInteger(start)) {
|
|
700
|
+
merged.start = start;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return v2().call.make<T>({ method, params: merged });
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Собственный офсетный цикл, а не `actions.v2.callList.make`.
|
|
708
|
+
*
|
|
709
|
+
* У `callList.make` keyset-пагинация: он шлёт `start: -1`, навязывает
|
|
710
|
+
* `order: { ID: 'ASC' }` и добавляет в фильтр `>ID`. Пользовательский `order`
|
|
711
|
+
* при этом молча игнорируется, а колбэка `progress` там нет вовсе. Делегирование
|
|
712
|
+
* туда изменило бы поведение постраничных выборок у потребителей.
|
|
713
|
+
*/
|
|
714
|
+
const callListMethod = async (
|
|
715
|
+
method: string,
|
|
716
|
+
params?: object,
|
|
717
|
+
progress?: null | ((progress: number) => void),
|
|
718
|
+
customKeyForResult?: string | null
|
|
719
|
+
): Promise<Result> => {
|
|
720
|
+
const result = new Result();
|
|
721
|
+
const onProgress = typeof progress === "function" ? progress : null;
|
|
722
|
+
|
|
723
|
+
onProgress?.(0);
|
|
724
|
+
|
|
725
|
+
const list: unknown[] = [];
|
|
726
|
+
let start = 0;
|
|
727
|
+
let completed = false;
|
|
728
|
+
|
|
729
|
+
for (let page = 1; page <= LIST_MAX_PAGES; page++) {
|
|
730
|
+
const response = await v2().call.make({ method, params: { ...params, start } });
|
|
731
|
+
|
|
732
|
+
// SDK в своём callListMethod этой проверки не делает и падает TypeError
|
|
733
|
+
// на getData().result, когда портал вернул «мягкую» ошибку
|
|
734
|
+
if (!response.isSuccess) {
|
|
735
|
+
throw new Error(`${method}: Bitrix24 вернул ошибку — ${response.getErrorMessages().join("; ")}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Читаем конверт целиком: getData() режет ответ до { result, time },
|
|
739
|
+
// а пагинация держится на next, которого там нет
|
|
740
|
+
const envelope = readV2Envelope(response, method);
|
|
741
|
+
const payload = envelope.result;
|
|
742
|
+
const chunk = customKeyForResult ? (payload as Record<string, unknown> | undefined)?.[customKeyForResult] : payload;
|
|
743
|
+
|
|
744
|
+
if (!Array.isArray(chunk)) {
|
|
745
|
+
throw new Error(`${method}: ответ не является списком`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const item of chunk) {
|
|
749
|
+
list.push(item);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Единственное правило остановки — отсутствие next в конверте. Считать
|
|
753
|
+
// смещение самим (start += chunk.length) нельзя: портал ставит next = start + 50
|
|
754
|
+
// независимо от того, сколько строк реально отдал, и на странице, укороченной
|
|
755
|
+
// правами доступа или фильтром, собственный счётчик отстаёт — перекрытие
|
|
756
|
+
// читается второй раз и записи дублируются. Метод, который игнорирует start
|
|
757
|
+
// и отдаёт весь список одной страницей, next не присылает и останавливается здесь же.
|
|
758
|
+
if (!Number.isInteger(envelope.next)) {
|
|
759
|
+
completed = true;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
454
762
|
|
|
455
|
-
|
|
456
|
-
// иначе один упавший вызов дал бы пять сообщений
|
|
457
|
-
logs.add(`$b24.${methodName}${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "warn");
|
|
763
|
+
const nextStart = Number(envelope.next);
|
|
458
764
|
|
|
459
|
-
|
|
765
|
+
// next обязан расти. Портал, вернувший прежнее или меньшее смещение,
|
|
766
|
+
// иначе гонял бы одну и ту же страницу до потолка, раздувая список
|
|
767
|
+
if (nextStart <= start) {
|
|
768
|
+
throw new Error(`${method}: портал вернул непродвигающийся next (${nextStart}) при start ${start} — пагинация зациклилась`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
start = nextStart;
|
|
772
|
+
|
|
773
|
+
if (onProgress) {
|
|
774
|
+
const total = Number(envelope.total) || 0;
|
|
775
|
+
onProgress(total > 0 ? Math.round((100 * list.length) / total) : 100);
|
|
460
776
|
}
|
|
461
777
|
}
|
|
462
778
|
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
779
|
+
// Обрезанный список, отданный как успешный, — худший исход: потребитель
|
|
780
|
+
// примет неполную выборку за полную. Поэтому бросаем, а не логируем:
|
|
781
|
+
// addError() на базовом Result бесполезен — getData() отдаёт данные
|
|
782
|
+
// независимо от наличия ошибок, и существующие потребители её не увидят
|
|
783
|
+
if (!completed) {
|
|
784
|
+
throw new Error(`${method}: достигнут потолок в ${LIST_MAX_PAGES} страниц, портал продолжает отдавать next — выборка прервана как неполная`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
onProgress?.(100);
|
|
788
|
+
result.setData(list);
|
|
789
|
+
return result;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
async function* fetchListMethod(method: string, params?: any, idKey?: string, customKeyForResult?: string | null): AsyncGenerator<any[]> {
|
|
793
|
+
yield* v2().fetchList.make<any>({
|
|
794
|
+
method,
|
|
795
|
+
params,
|
|
796
|
+
idKey,
|
|
797
|
+
customKeyForResult: customKeyForResult === null ? undefined : customKeyForResult,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const callBatch = async (calls: Array<any> | object, isHaltOnError?: boolean, returnAjaxResult?: boolean): Promise<Result> => {
|
|
802
|
+
// Дефолты подставляем мы: batch.make своих не имеет, он просто
|
|
803
|
+
// расширяет переданный объект опций версией API
|
|
804
|
+
return v2().batch.make({
|
|
805
|
+
calls: calls as BatchCalls,
|
|
806
|
+
options: {
|
|
807
|
+
isHaltOnError: isHaltOnError ?? true,
|
|
808
|
+
returnAjaxResult: returnAjaxResult ?? false,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const callBatchByChunk = async (calls: Array<any>, isHaltOnError: boolean): Promise<Result> => {
|
|
814
|
+
// isHaltOnError передаём как есть, без ?? true — дословно по AbstractB24.
|
|
815
|
+
// returnAjaxResult не передаём: batchByChunk.make жёстко ставит false сам,
|
|
816
|
+
// а его тип опций этот ключ не принимает
|
|
817
|
+
return v2().batchByChunk.make({ calls, options: { isHaltOnError } });
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
return { callMethod, callListMethod, fetchListMethod, callBatch, callBatchByChunk } satisfies FacadeMethods;
|
|
466
821
|
}
|
|
467
822
|
|
|
468
823
|
/**
|
|
469
824
|
* Proxy-обёртка вокруг B24OAuth.
|
|
470
825
|
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
826
|
+
* Имена из FACADE_METHODS резолвятся в собственные реализации пакета поверх
|
|
827
|
+
* `actions.v2.*`; из них методы RETRYABLE_METHODS дополнительно оборачиваются
|
|
828
|
+
* retry-логикой. Все остальные методы привязываются к оригинальному объекту
|
|
829
|
+
* через bind: класс B24OAuth использует приватные поля (#authOAuthManager),
|
|
830
|
+
* и при вызове метода с this === Proxy движок бросает "Cannot read private member".
|
|
475
831
|
*
|
|
476
832
|
* Обёртки кешируются — по одной на метод, чтобы не ломать сравнение по ссылке.
|
|
477
833
|
*/
|
|
478
|
-
function wrapB24WithRetry(b24: B24OAuth):
|
|
834
|
+
function wrapB24WithRetry(b24: B24OAuth): B24Client {
|
|
479
835
|
const methodCache = new Map<string, Function>();
|
|
836
|
+
// Фасад создаётся один раз на экземпляр: он замкнут на конкретный target,
|
|
837
|
+
// а пересоздание на каждом обращении ломало бы кеш и сравнение по ссылке
|
|
838
|
+
const facade = createFacade(b24);
|
|
480
839
|
|
|
481
840
|
return new Proxy(b24, {
|
|
482
841
|
get(target, prop) {
|
|
842
|
+
// Имена фасада проверяем до Reflect.get: пока SDK ещё объявляет свои
|
|
843
|
+
// deprecated-методы, иначе мы отдавали бы их, а не свои
|
|
844
|
+
if (typeof prop === "string" && FACADE_METHODS.has(prop)) {
|
|
845
|
+
if (!methodCache.has(prop)) {
|
|
846
|
+
const impl = facade[prop]!;
|
|
847
|
+
methodCache.set(prop, RETRYABLE_METHODS.has(prop) ? withRetry(impl, target, prop) : impl);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return methodCache.get(prop);
|
|
851
|
+
}
|
|
852
|
+
|
|
483
853
|
// Передаём target третьим аргументом: геттеры (например, auth)
|
|
484
854
|
// тоже должны исполняться с this === target
|
|
485
855
|
const value = Reflect.get(target, prop, target);
|
|
@@ -497,13 +867,21 @@ function wrapB24WithRetry(b24: B24OAuth): B24OAuth {
|
|
|
497
867
|
|
|
498
868
|
return methodCache.get(prop);
|
|
499
869
|
},
|
|
500
|
-
|
|
870
|
+
|
|
871
|
+
// Страховка на будущее: когда SDK уберёт методы из прототипа,
|
|
872
|
+
// "callMethod" in $b24 обязано остаться истинным (eventB24.ts проверяет
|
|
873
|
+
// наличие метода перед работой). Цель расширяема, лишние true законны
|
|
874
|
+
has(target, prop) {
|
|
875
|
+
if (typeof prop === "string" && FACADE_METHODS.has(prop)) return true;
|
|
876
|
+
return Reflect.has(target, prop);
|
|
877
|
+
},
|
|
878
|
+
}) as B24Client;
|
|
501
879
|
}
|
|
502
880
|
|
|
503
881
|
// ==================== Инициализация ====================
|
|
504
882
|
|
|
505
883
|
let _b24Raw = createB24Instance();
|
|
506
|
-
export let $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
|
|
884
|
+
export let $b24: B24Client | null = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
|
|
507
885
|
|
|
508
886
|
/** Настраивает колбэк автосохранения токенов на экземпляре B24OAuth */
|
|
509
887
|
function setupRefreshCallback(raw: B24OAuth): void {
|
package/bitrix24/eventB24.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { $b24, getResultData } from "./b24.ts";
|
|
1
|
+
import { $b24, getResultData, type B24MethodCaller } from "./b24.ts";
|
|
3
2
|
|
|
4
3
|
// ==================== Типы ====================
|
|
5
4
|
|
|
@@ -83,14 +82,16 @@ function normalizeMessages(messages: unknown): any[] {
|
|
|
83
82
|
* Класс для работы с офлайн-событиями Bitrix24
|
|
84
83
|
*/
|
|
85
84
|
export class Event {
|
|
86
|
-
|
|
85
|
+
// Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
|
|
86
|
+
// удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
|
|
87
|
+
private b24: B24MethodCaller;
|
|
87
88
|
|
|
88
89
|
/** Расширения видео-файлов */
|
|
89
90
|
private static readonly VIDEO_EXTENSIONS = ["mp4", "mov", "avi", "mkv", "webm", "3gp", "m4v"];
|
|
90
91
|
/** Расширения изображений */
|
|
91
92
|
private static readonly IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "bmp"];
|
|
92
93
|
|
|
93
|
-
constructor(b24:
|
|
94
|
+
constructor(b24: B24MethodCaller) {
|
|
94
95
|
if (!b24 || typeof b24.callMethod !== "function") {
|
|
95
96
|
throw new Error("Event: передали некорректный b24 или не передали вообще");
|
|
96
97
|
}
|
package/package.json
CHANGED
package/sendMessage/smsgold.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { getResultData } from "../bitrix24/b24.ts";
|
|
1
|
+
import { getResultData, type B24MethodCaller } from "../bitrix24/b24.ts";
|
|
3
2
|
import { fetchRetry } from "../utils/fetchRetry.ts";
|
|
4
3
|
|
|
5
4
|
// ==================== Типы ====================
|
|
@@ -35,9 +34,11 @@ interface SmsResult {
|
|
|
35
34
|
export class Smsgold {
|
|
36
35
|
private user: string;
|
|
37
36
|
private pass: string;
|
|
38
|
-
|
|
37
|
+
// Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
|
|
38
|
+
// удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
|
|
39
|
+
private b24: B24MethodCaller | null;
|
|
39
40
|
|
|
40
|
-
constructor(authParam: SmsgoldAuthParam, b24:
|
|
41
|
+
constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null) {
|
|
41
42
|
this.user = authParam.user;
|
|
42
43
|
this.pass = authParam.pass;
|
|
43
44
|
this.b24 = b24;
|