JS/Jint SmartScripts — справочник паттернов и граблей¶
Практический справочник для написания JS SmartScripts в 1Форме. Каждый паттерн проверен на production (HD). Цель — не повторять ошибки, найденные в SS 492 (Анфиса), SS 493 (AI Search; sync-часть с 2026-04-11 в Module:ai-search-engine SS 592, async — searchDb/cascadeSearch и др. — остаются в SS 493), Telegram-синхронизации и GitLab-интеграции.
Все доступные Smart Actions с сигнатурами — MCP-серверы
/mcp-actions-*(21 группа, ~205 действий). Каталог:docs/reference/api/mcp-catalog.md.
API reference: docs/domains/smart-actions/js-scripting-jint.md
0. Версионирование — обязательно¶
Каждый SmartScript и JS-вставка обязаны содержать комментарий с версией и датой в начале скрипта:
// v1 | 2026-03-08 15:30 | Начальная версия
// v2 | 2026-03-08 16:45 | Добавлена проверка прав
// v3 | 2026-03-10 09:00 | Фикс: TTL кэша 24ч → 1ч
- Версия инкрементируется при каждом изменении
- Дата/время — момент правки (local time)
- Краткое описание — что изменилось
- Если скрипт без версии — добавить
v1при первом касании
0.1. Cross-platform SQL — MSSQL + PG¶
DB_TYPE — глобальная переменная, доступная в JS SmartScripts: "MSSQL" или "PG".
Проблема: Key, Value — reserved words. MSSQL требует [Key], PG — "key" (lowercase). TOP N — только MSSQL, PG — LIMIT N.
Паттерн: функция qi() (quote identifier):
var IS_PG = DB_TYPE === "PG";
function qi(name) { return IS_PG ? '"' + name.toLowerCase() + '"' : '[' + name + ']'; }
Использование:
// SettingsCustom — самый частый случай
var val = SQL.scalar(
"SELECT " + qi('Value') + " FROM SettingsCustom WHERE " + qi('Key') + " = 'my_setting'");
// SELECT с TOP/LIMIT
var rows = SQL.query(
"SELECT " + (IS_PG ? "" : "TOP 1 ") + qi('Value') +
" FROM SettingsCustom WHERE " + qi('Key') + " = '" + escSql(key) + "'" +
(IS_PG ? " LIMIT 1" : ""));
// INSERT/UPDATE
SQL.query("INSERT INTO SettingsCustom (" + qi('Key') + ", " + qi('Value') + ") VALUES (@k, @v)",
{ k: key, v: value });
Полный набор хелперов (для скриптов с тяжёлыми SQL):
var IS_PG = DB_TYPE === "PG";
function qi(name) { return IS_PG ? '"' + name.toLowerCase() + '"' : '[' + name + ']'; }
var NL = IS_PG ? '' : ' WITH(NOLOCK)';
function topN(n) { return IS_PG ? '' : 'TOP ' + n + ' '; }
function limitN(n) { return IS_PG ? ' LIMIT ' + n : ''; }
var NOW_FN = IS_PG ? 'NOW()' : 'GETDATE()';
var RV_COL = IS_PG ? "xmin::text::bigint" : "CAST(row_version AS BIGINT)";
var RV_ORDER = IS_PG ? "xmin" : "row_version";
function slen(col) { return IS_PG ? 'LENGTH(' + col + ')' : 'LEN(' + col + ')'; }
function ifnull(expr, def) { return IS_PG ? 'COALESCE(' + expr + ', ' + def + ')' : 'ISNULL(' + expr + ', ' + def + ')'; }
Gotchas:
- PG хранит identifiers в lowercase → qi('Value') → "value" (не "Value")
Backend-слой при DataTable -> Dictionary использует StringComparer.OrdinalIgnoreCase, поэтому доступ к колонкам результата через объект/словарь в runtime обычно не зависит от регистра имени поля. Это не отменяет необходимость правильно квотировать identifiers в самом SQL для PG.
- MSSQL: sc.key без скобок → Incorrect syntax near the keyword 'key'
- PG: [Key] → синтаксическая ошибка (квадратные скобки не поддерживаются)
- WITH(NOLOCK) — только MSSQL, на PG не существует. var NL — динамический хинт
- LEN() → LENGTH() (PG). ISNULL() → COALESCE() (PG)
- row_version (MSSQL) → xmin (PG) для change tracking
- String concat: MSSQL +, PG ||. Пример: IS_PG ? "a || b" : "a + b"
- N'...' (unicode prefix) — только MSSQL, на PG убирать
Lua-аналог: get_mspg_query() с тегами {MS}...{/MS} и {PG}...{/PG} + $NOW.
1. CACHE API — больше чем key-value¶
Базовое использование¶
CACHE.set("my_key", "value", 3600); // lifetime в секундах
var val = CACHE.get("my_key"); // → string или null
CACHE.delete("my_key");
TTL gotcha: default = MaxInt. Без явного lifetime значение живёт вечно (до рестарта процесса). Всегда указывай TTL.
// Плохо — кэш никогда не протухнет
CACHE.set("nav_index", data);
// Хорошо — явный TTL
CACHE.set("nav_index", UTILS.json_encode(data), 86400); // 24ч
Значение — строка. Объекты → UTILS.json_encode() перед set, UTILS.json_decode() после get.
CACHE = общее пространство имён с бэкендом¶
CACHE в JS/Lua скриптах — тот же ScriptDataCache (LuaCache), что использует серверный код 1Формы. Это discovery: из скрипта можно читать и инвалидировать серверные кэши.
js-scripting-jint.md:189 — «Использует тот же ScriptDataCache (LuaCache), что и Lua-скрипты — кэш общий.»
Следствия:
- Ключи из разных скриптов (JS и Lua) конфликтуют. Используй уникальные префиксы: ss492_, ai_search_, gitlab_.
- При обновлении скрипта через SQL (не через admin API) CACHE не сбрасывается. Нужно менять имя ключа или ждать TTL. Через admin API — кэш скрипта инвалидируется автоматически (SQLRefactor/CLAUDE.md).
Rate limiting через CACHE¶
Паттерн из SS 492 (Анфиса): ограничение вызовов на пользователя.
function checkRateLimit(userId, maxPerHour) {
var key = "ss492_rate_" + userId;
var raw = CACHE.get(key);
var count = raw ? parseInt(raw) : 0;
if (count >= maxPerHour) return false;
CACHE.set(key, String(count + 1), 3600); // TTL = 1 час
return true;
}
if (!checkRateLimit(authorId, 500)) {
postComment(taskId, "Превышен лимит запросов. Попробуйте позже.");
return;
}
docs/domains/ai/README.md — rate limit 500 req/hr/user, daily token budget 5M.
State machine через CACHE¶
Бюджет токенов, история диалогов, промежуточные состояния — всё хранится в CACHE с TTL.
// Трекинг бюджета за день
var budgetKey = "ss492_budget_" + today();
var raw = CACHE.get(budgetKey);
var spent = raw ? parseInt(raw) : 0;
if (spent + estimatedTokens > DAILY_BUDGET) {
// budget hit — отказ или fallback на дешёвую модель
return;
}
CACHE.set(budgetKey, String(spent + usedTokens), 86400);
docs/domains/ai/README.md — budget management: daily cap 5M tokens.
CACHE.get — проверка на null и "null"¶
var raw = CACHE.get("my_key");
// raw === null → ключа нет
// raw === "null" → кто-то закэшировал json_encode(null)!
if (!raw || raw === "null") {
// rebuild
}
SQLRefactor/CLAUDE.md — «CACHE:get проверять на ~= nil и ~= "null". Нет API для ручного сброса».
2. HTTP — внешние вызовы¶
Базовый паттерн¶
var resp = HTTP.send_http_request(
"POST",
"https://api.example.com/endpoint",
null, // query params
{
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
UTILS.json_encode(body), // rawBody
null // options
);
HTTP response — обёртка, не raw JSON¶
Проблема: HTTP.send_http_request возвращает не десериализованный JSON, а обёрточный объект.
Решение:
var resp = HTTP.send_http_request("POST", url, null, headers, rawBody);
// НЕПРАВИЛЬНО — так не работает
// var data = resp.data[0].embedding;
// ПРАВИЛЬНО
if (resp.InnerError) throw new Error(resp.InnerError);
var content = resp.HttpResponse.ResponseContent; // строка!
var parsed = UTILS.json_decode(content);
var data = parsed.data[0].embedding;
smartscript-migration-spec.md:434-447 — подтверждённая gotcha.
DefaultHttpClientV2 timeout 15 секунд¶
Проблема: внутренний HttpClientType.DefaultHttpClientV2 имеет TimeoutSeconds = 15. Для тяжёлых запросов (Claude API, большие payload) 15 секунд мало → TaskCanceledException.
Решение: прямой вызов CLR через UTILS.resolve_instance:
var httpSvc = UTILS.resolve_instance(
"Valhalla.Services",
"Valhalla.Services.HttpRequestService"
);
// Создаём options с увеличенным таймаутом
var options = UTILS.create_instance(
"Valhalla.Services",
"Valhalla.Services.Models.HttpRequestOptions"
);
options.RequestTimeoutMs = 120000; // 2 минуты
var result = httpSvc.SendBodyContainingHttpRequest(
url, "POST", rawBody, headersDict, options
);
docs/domains/ai/README.md — gotcha «DefaultHttpClientV2 timeout 15с».
ToJSON баг — Dictionary не оборачивается в {}¶
Проблема: при сериализации Dictionary через ToJSON (внутренний сериализатор HTTP API) пары key-value не оборачиваются в {}. JSON невалиден.
Решение: создавать словари через UTILS.create_instance:
var headers = UTILS.create_instance(
"System.Private.CoreLib",
"System.Collections.Generic.Dictionary`2[[System.String],[System.String]]"
);
headers.Add("Content-Type", "application/json");
headers.Add("X-Api-Key", apiKey);
docs/domains/ai/README.md — «ToJSON баг → обход через create_instance».
Retry с backoff¶
Паттерн для внешних API (Claude, OpenAI, Cohere):
function httpWithRetry(method, url, headers, body, maxRetries) {
maxRetries = maxRetries || 3;
for (var attempt = 0; attempt < maxRetries; attempt++) {
var resp = HTTP.send_http_request(method, url, null, headers, body);
var status = resp.HttpResponse ? resp.HttpResponse.StatusCode : 0;
if (status === 200) return resp;
if (status === 429 || status === 500 || status === 529) {
// backoff: ~1s, ~2s, ~4s
var waitMs = Math.pow(2, attempt) * 1000;
var start = Date.now();
while (Date.now() - start < waitMs) { /* busy wait — Jint не имеет sleep */ }
continue;
}
break; // 4xx (кроме 429) — не ретраить
}
return resp;
}
Важно: в Jint нет setTimeout/sleep. Busy wait допустим при коротких паузах (до 5 секунд), но считается в MaxStatements.
SSL для внутренних стендов¶
// *.{host} — self-signed сертификаты
// HTTP.send_http_request по умолчанию НЕ проверяет сертификаты
// Дополнительных действий не нужно
var resp = HTTP.send_http_request("GET", "https://{host}/api/...");
3. UTILS.resolve_instance — CLR interop¶
Зачем¶
Прямой доступ к .NET-сервисам 1Формы из SmartScript — без SQL, без HTTP hop. Сервис резолвится из DI-контейнера ASP.NET Core.
Когда использовать: когда SQL = обход сервисного слоя. Пример: AI Search (sync — Module:ai-search-engine SS 592 после Etap 5; async — SS 493) — 0 строк SQL для поиска, всё через .NET-сервисы in-process.
resolve_instance — DI resolve¶
var searchSvc = UTILS.resolve_instance(
"Valhalla.Services", // assembly name
"Valhalla.Services.SearchService" // full type name
);
var result = searchSvc.SimpleSearch(query, userId);
smartscript-migration-spec.md:117-119 — паттерн resolve_instance для ISearchService.
create_instance — Activator.CreateInstance¶
Создание объектов напрямую (когда нет DI-регистрации):
// Словарь (для HTTP headers и т.п.)
var dict = UTILS.create_instance(
"System.Private.CoreLib",
"System.Collections.Generic.Dictionary`2[[System.String],[System.String]]"
);
// List<int>
var list = UTILS.create_instance(
"System.Private.CoreLib",
"System.Collections.Generic.List`1[[System.Int32]]"
);
// Npgsql connection (для PG fallback)
var conn = UTILS.create_instance(
"Npgsql",
"Npgsql.NpgsqlConnection",
[connStr] // constructor args — массив
);
CLR static-классы — sanitization через TextUtils¶
TextUtils — не в DI (все методы static), поэтому resolve_instance не сработает. Но create_instance (Activator) создаёт инстанс, а Jint через ObjectWrapper видит static-методы. Проверено на dev 2026-04-11.
var tu = UTILS.create_instance("Valhalla.Utils", "Valhalla.Utils.Text.TextUtils");
// FTS (CONTAINS/CONTAINSTABLE) — tokenize, escape, FORMSOF, обрезка длины
var ftsQuery = tu.FormSearchParameterForFullTextContainsSearch(userInput, false);
// "тест поиск" → FORMSOF(INFLECTIONAL, "тест") and FORMSOF(INFLECTIONAL, "поиск")
// второй аргумент true = искать в альтернативной раскладке (EN↔RU)
// LIKE — bracket escaping [%], [_], [[]
var likeQuery = tu.FormSearchParametersForStandartSearch(userInput);
// "test%_[special" → "test[%]_[[]special"
// Обрезка по лимиту (Constants.SearchTextLengthLimit)
var trimmed = tu.CutSearchStringIfTooLong(userInput);
Зачем: production-tested sanitization (используется в Grids, Reports, CommentsFeed, TaskTextFilter), не нужно дублировать логику в JS. Не нужна доработка бэка — create_instance уже есть.
Ключевое: create_instance, не resolve_instance. Static-классы не регистрируются в Autofac → resolve_instance бросит исключение.
Gotcha: не все типы резолвятся¶
| Тип | create_instance | Workaround |
|---|---|---|
List<int> |
✅ | — |
CancellationToken |
✅ value type | — |
GridSettings |
✅ (но namespace Dto.AgGrid.GridSettings, НЕ Dto.NewDataSources.GridSettings) |
— |
GetDataSourceRequest |
❌ возвращает null | Duck-typed JS object |
smartscript-migration-spec.md:370-378
Duck typing — обход create_instance¶
Jint маппит JS-объект на C#-параметр по совпадению имён свойств:
// GetDataSourceRequest не резолвится через create_instance
// Но C#-метод примет JS-объект с нужными полями:
var dsRequest = {
Request: rowRequest,
GridSettings: gridSettings,
UserId: 28,
TaskIdsOnly: true
};
service.GetTasksDataAsync(dsRequest, ct); // работает!
smartscript-migration-spec.md:399-405
Правильные пути сервисов¶
| Сервис | Assembly | Type |
|---|---|---|
| RequestHandlerService | Valhalla.Services |
Valhalla.Services.RequestHandlerService |
| ISearchService | Valhalla.Services |
Valhalla.Services.SearchService |
| CommentService | Valhalla.Services |
Valhalla.Services.CommentService |
| HttpRequestService | Valhalla.Services |
Valhalla.Services.HttpRequestService |
НЕ Valhalla.Services.AgGridServices.RequestHandlerService — неверный путь, не найдёт.
smartscript-migration-spec.md:382-387
Риски CLR interop¶
- Привязка к internal API — нет гарантий стабильности между версиями.
- Namespace и assembly могут меняться при рефакторинге.
- Если тип не в whitelist CLR-типов SmartScript → ошибка.
4. SMART — действия в контексте задачи¶
PostComment¶
SMART.execute_action_in_task_context("PostComment", taskId, {
Task: taskId, // ОБЯЗАТЕЛЬНО — иначе NullRef
CommentText: "Текст",
Recipients: [authorId] // кому адресован ответ
});
Gotcha: PostComment NullRef без параметра Task. SMART.execute_action_in_task_context("PostComment", ...) без Task в params → binder добавляет pars[2]=null → (int)null = NullReferenceException.
docs/domains/ai/README.md — gotcha подтверждена.
Смена состояния¶
SMART.execute_action_in_task_context("ChangeTaskState", taskId, {
StepId: targetStateId,
Comment: "[auto:gitlab-sync] Создана ветка feature/123-fix"
});
Обновление ДП¶
SMART.execute_action_in_task_context("UpdateExtParam", taskId, {
ExtParamId: 12408,
Value: branchName
});
Async-выполнение¶
// async = true — выполнение в фоне (не блокирует скрипт)
SMART.execute_action_in_task_context("PostComment", taskId, {
Task: taskId,
CommentText: "Фоновая операция"
}, true);
5. include() / import() — модуляризация¶
Два механизма загружают library-скрипты (IsLibrary=true). Отличаются временем выполнения и областью видимости.
| Механизм | Вызов | Когда применим | Как резолвит |
|---|---|---|---|
include(X) |
include(496) / include("agent-tools-local") |
Синхронный. Можно на top level. | ID или Description |
import(X) |
await import("Module:agent-utils") |
Async. Только внутри async-функции. |
Description (рекомендуется) — не ломается при dev ↔ HD restore |
Глубина include-рекурсии — не более 5 уровней.
js-scripting-jint.md:210-215
Паттерн: модульная архитектура (SS 492 Анфисы)¶
С Wave 1-5 (2026-04) Анфиса полностью на ES-модулях через import():
// SS 492 — оркестратор (фрагмент)
AgentUtils = await import('Module:agent-utils');
AgentConfig = await import('Module:agent-config');
// ... 40+ импортов
// Остались 2 include (отдельный трек миграции / не в плане):
include("ai-search");
include("agent-tools-1c");
Полный инвентарь активных SS — ss-includes.md.
Рекомендация¶
Выносить в отдельные library-SS: - Конфигурацию (константы, ID, SettingsCustom-ключи) - Утилиты (парсинг, форматирование, retry-логику) - API-обёртки (Claude, OpenAI, GitLab, Telegram)
Новый код — через import() с префиксом Module: в Description. Один параметризованный скрипт + библиотеки лучше трёх копий одного кода (антипаттерн P5 из Telegram-аудита).
Грабля: var в async IIFE не виден include-скриптам¶
JsScriptEngine.ExecuteScriptAsync оборачивает код в (async () => { ... })(). Любой var внутри хойстится на scope этой IIFE-функции, не на engine globals. include() выполняется через engine.Execute() — видит только engine globals.
Симптом: include-скрипт получает ReferenceError на переменную, которая «точно объявлена» в orchestrator.
Реальный пример (agent-loop v223):
// Line 264 — implicit global (видно в include):
_currentTaskId = null;
// Line 1263 — var-дубликат, shadow'ил глобал (НЕ видно в include):
var _currentTaskId = 0; // ❌
_currentTaskId. Fix — убрать var.
Правило: Переменные, которые должны быть видны include-скриптам, объявлять через implicit global (_foo = value), не var _foo = value. Перед добавлением переменной — grep на совпадения.
Связанный случай: внутри самого library-скрипта var _FOO = true тоже не создаёт engine-global — родительский SS не увидит его через implicit global. Пример: SS 612 dispatcher, починено 2026-04-04 (убрали var из _ANFISA_JOB_MODE = true).
Грабля: include() и import() пишут в одни globals — последний побеждает¶
Оба механизма декларируют функции/переменные в engine globals. Если функция с одним именем определена в обоих — выигрывает тот, что выполнился позже. В оркестраторе типично import()-ы идут после include()-ов → import перезаписывает include.
Реальный пример: toolFindDocsFiles определена в agent-tools-docs.js (старый include SS 498) и ai-search-docs.js (import Module:ai-search-docs). Правка в SS 498 не имела эффекта — runtime использовал версию из Module-скрипта.
Правило при правке функции:
1. grep funcName по всем library-скриптам.
2. Если больше одного определения — править все. Или (надёжнее) убедиться что правишь ту, что в активной import-цепочке.
3. При наличии одновременно include-версии и import-версии — import = источник правды.
6. Безопасность и секреты¶
Никогда не хардкодить токены в SmartScript¶
P1 из Telegram-аудита: три бот-токена Telegram вшиты прямо в Lua-код SmartScript. Любой администратор с доступом к SmartScripts видит токены.
docs/domains/integrations/audit-2026-03-07.md:172-174
SettingsCustom — правильное хранение¶
// Чтение секрета в рантайме
var token = SQL.scalar(
"SELECT Value FROM SettingsCustom WHERE [Key] =@name",
{ name: "gitlab_private_token" }
);
token = decrypt(token); // если XOR-encoded
Для XOR-encoding: ключ хранится в конфигурации (appsettings.json), значение — hex-строка в SettingsCustom.
gitlab-1forma-integration-spec.md:415-424 — конфигурация секретов.
smartscript-migration-spec.md:204-219 — SettingsCustom для API keys (AI Search).
encrypt/decrypt¶
var encrypted = encrypt("secret_value"); // → зашифрованная строка
var original = decrypt(encrypted); // → исходное значение
Встроенные функции используют серверный ключ шифрования. Подходят для хранения в БД.
Publications [AllowAnonymous] — валидация обязательна¶
Публикации с анонимным доступом открыты всему интернету. Входящие данные нельзя доверять.
// Верификация webhook secret (GitLab)
var secret = decrypt(SQL.scalar(
"SELECT Value FROM SettingsCustom WHERE [Key] ='gitlab_webhook_secret'"
));
if (params["secret"] !== secret) {
RESULT = { error: "unauthorized" };
return;
}
gitlab-1forma-integration-spec.md:196-203
7. Publications как webhook endpoint¶
Архитектура¶
Внешний сервис (GitLab, Telegram, ...)
→ POST /app/v1.2/api/publications/action/{alias}
→ PublishedObjectsController [AllowAnonymous]
→ SmartPublicationsService
→ ActionsPack:
1. HTTP-ответ {"ok": 200} (мгновенный ответ, Action=88)
2. JS SmartScript (основная логика, async)
Payload доступен через EVENTPARAMS["PublishedObjectParameters"].
ВАЖНО: EVENTPARAMS доступен ТОЛЬКО на верхнем уровне скрипта. Внутри try-catch блоков не виден (Jint scope quirk). Сохраняй в переменную ДО try-catch.
../notifications/admin.md — настройка публикаций.
docs/domains/integrations/audit-2026-03-07.md:22-29 — пример data flow.
Парсинг входящего payload¶
GOTCHA (2026-03-08): PublishedObjectParameters — НЕ плоский объект. Структура:
- params.requestBody — JSON-тело запроса (объект)
- params.headers — HTTP-заголовки (ключи в mixed-case: x-Gitlab-Token, x-Gitlab-Event, content-Type)
var raw = EVENTPARAMS["PublishedObjectParameters"];
var params = typeof raw === "string" ? UTILS.json_decode(raw) : raw;
var headers = params.headers || {};
var body = params.requestBody || {};
// Headers приходят в mixed-case — нужен case-insensitive поиск
function getHeader(name) {
var lower = name.toLowerCase();
var keys = Object.keys(headers);
for (var i = 0; i < keys.length; i++) {
if (keys[i].toLowerCase() === lower) return headers[keys[i]];
}
return "";
}
var event = getHeader("X-Gitlab-Event") || body.object_kind;
PostComment в контексте публикаций¶
GOTCHA (2026-03-08): CommentAuthor обязателен при вызове из анонимной публикации. Без него SessionUserId=-1 → NullRef → HTTP 500.
var ROBOT_USER_ID = 3; // Робот 1Ф
SMART.execute_action_in_task_context("PostComment", taskId, {
CommentAuthor: ROBOT_USER_ID, // ОБЯЗАТЕЛЬНО в публикациях!
Task: taskId,
CommentText: "[auto:gitlab] MR !42 принят",
SilentComment: false,
ForcedEmail: false,
CommentSMS: false,
NoSubscription: true
});
ChangeExtParamValue — обновление ДП из скрипта¶
Действие называется ChangeExtParamValue (не UpdateExtParam).
SMART.execute_action_in_task_context("ChangeExtParamValue", taskId, {
Task: taskId,
User: ROBOT_USER_ID,
ExtParam: epId,
Value: String(value),
WriteCommentOnChange: false
}, false);
Table ДП — формат Value¶
ChangeExtParamValue работает для Table ДП. Value — строка с префиксом-операцией.
Добавление строки (+):
// columnId без кавычек, значения в "First"
Value: '+[{111:{"First":"текст"},222:{"First":"42"}}]'
Изменение строки (=):
// "First" = rowId строки, "Second" = объект с колонками
Value: '={"First":"555","Second":{111:{"First":"новый текст"}}}'
Массовое изменение (=[...]):
Value: '=[{"First":"1","Second":{100:{"First":"a"}}},{"First":"2","Second":{100:{"First":"b"}}}]'
Удаление строки (-):
Value: '-555' // 555 = rowId
Полная перезапись (# smart merge, | wipe+create):
// # — удаляет отсутствующие, добавляет новые, обновляет совпадающие
Value: '#{"1":{11:{"First":"текст1"}},"2":{11:{"First":"текст2"}}}'
// | — удаляет все строки, создаёт заново (если rowId неизвестны)
Value: '|{"1":{11:{"First":"текст1"}},"2":{11:{"First":"текст2"}}}'
Экранирование в Value: " → \", \ → \\, ' → ''. Переносы \n\r — без экранирования.
Колонка SelectUsers:
Value: '+[{9:{"First":"{\\"Users\\":{\\"Added\\":[2122]}}"}}]'
Колонка Выпадающий список: "First" = текст, "Second" = значение (обязателен для выпадающих).
Источник: архивное руководство администратора по smart expressions, секция «Работа с ДП Таблица».
Anti-loop маркеры¶
При двусторонней синхронизации (1Ф ↔ GitLab, 1Ф ↔ Telegram) скрипт может триггерить сам себя.
Решение: текстовый маркер в комментариях.
var MARKER = "[auto:gitlab]";
// Входящий скрипт — маркирует свои комментарии
SMART.execute_action_in_task_context("PostComment", taskId, {
CommentAuthor: ROBOT_USER_ID,
Task: taskId,
CommentText: MARKER + " MR !42 принят",
SilentComment: false,
ForcedEmail: false,
CommentSMS: false,
NoSubscription: true
});
// Исходящий скрипт — проверяет маркер и пропускает свои
if (commentText.indexOf("[auto:gitlab]") >= 0) return;
Два маркера: [auto:gitlab-sync] (вход) и [auto:1forma-sync] (выход) — чтобы различать направление.
gitlab-1forma-integration-spec.md:427-434
Идемпотентность — delivery ID tracking¶
Telegram и GitLab гарантируют at-least-once delivery. Повторный webhook → дубль комментария.
Решение через CACHE:
function isProcessed(deliveryId) {
if (!deliveryId) return false;
var key = "gitlab_delivery_" + deliveryId;
if (CACHE.get(key)) return true;
CACHE.set(key, "1", 86400); // 24ч TTL
return false;
}
var deliveryId = params["X-Gitlab-Delivery"] || "";
if (isProcessed(deliveryId)) {
RESULT = { status: "duplicate" };
return;
}
gitlab-1forma-integration-spec.md:437-454 — паттерн идемпотентности.
docs/domains/integrations/audit-2026-03-07.md:209 — P9: нет идемпотентности в Telegram.
8. SQL из SmartScript¶
SQL.query — 0-indexed List, не Dict¶
var rows = SQL.query("SELECT TaskID, Description FROM Tasks WHERE SubcatID = @sub", { sub: 5641 });
// НЕПРАВИЛЬНО:
// Object.keys(rows) → ["Capacity", "Count"] — НЕ индексы строк
// ПРАВИЛЬНО:
for (var i = 0; i < rows.Count; i++) {
var row = rows[i];
var taskId = row.TaskID; // PascalCase, как в SQL
var desc = row.Description;
}
smartscript-migration-spec.md:356-363
SQL.query_one — ⚠️ НЕ РАБОТАЕТ в публикациях¶
GOTCHA (2026-03-08): SQL.query_one() возвращает null даже для существующих строк в контексте публикаций (OnCallPublishedObject, eventId=106). Причина — Jint interop: метод существует, но маршаллинг результата ломается.
Рабочая замена — SQL.query() + rows[0]:
// НЕ работает: var task = SQL.query_one("SELECT ...", { id: taskId });
// Работает:
var rows = SQL.query("SELECT TOP 1 TaskID, SubcatID, IsClosed FROM Tasks WHERE TaskID = " + parseInt(taskId));
var task = rows.Count > 0 ? rows[0] : null;
if (!task) return;
var subcatId = task.SubcatID;
SQL.query() возвращает .NET List — используй .Count и [0], не .length.
SQL.scalar — скалярное значение¶
Работает корректно, включая публикации.
var count = SQL.scalar("SELECT COUNT(*) FROM Tasks WHERE SubcatID = @sub", { sub: 5641 });
var name = SQL.scalar("SELECT Value FROM SettingsCustom WHERE [Key] = @n", { n: "my_key" });
GOTCHA: колонка в SettingsCustom — [Key], не Name. Квадратные скобки обязательны (зарезервированное слово).
VECTORDB API (PG) — 1-indexed Dict — [deprecated 2026-05-08]¶
[deprecated 2026-05-08] Csharp 1f-agent больше не использует
VECTORDBAPI; раздел оставлен для legacy JS SmartScript'ов.VECTORDBвсё ещё регистрируется backend'ом при наличииVectorDbConnectionString, но удаление API — отдельная инициатива вcore/.
VECTORDB.query() возвращает Dict<int, Dict<string, object>> с ключами от 1, не от 0.
var rows = VECTORDB.query("SELECT task_id, score FROM vector_search.chunks LIMIT 10");
var rowNum = 1;
while (rows[rowNum]) {
var row = rows[rowNum];
var taskId = row.task_id; // lowercase — PG convention
rowNum++;
}
VECTORDB регистрируется условно — только когда VectorDbConnectionString есть в appsettings.json. Проверка:
var hasVectordb = typeof VECTORDB !== "undefined";
smartscript-migration-spec.md:449-464
Npgsql параметры — @pN, не $N¶
GOTCHA (2026-03-10, SS 492/498). AddWithValue("$1", value) в Npgsql НЕ биндит позиционные $N. Ошибка: 42P02 — parameter $1 does not exist или silent mismatch.
Причина: AddWithValue ожидает имя параметра, Npgsql автоматически добавляет @ при связывании. SQL должен использовать @pN, AddWithValue — "pN" (без @).
// НЕПРАВИЛЬНО — $N через AddWithValue
pgExec(
"INSERT INTO table (col1, col2) VALUES ($1, $2)",
{ "$1": val1, "$2": val2 }
);
// → 42P02: parameter $1 does not exist
// ПРАВИЛЬНО — @pN в SQL, pN в AddWithValue
pgExec(
"INSERT INTO table (col1, col2) VALUES (@p1, @p2)",
{ "p1": val1, "p2": val2 }
);
Cast-синтаксис: @p1::text[], @p1::halfvec(1024) — работает корректно с @pN.
Null-значения: AddWithValue("pN", null) в Npgsql интерпретируется как «нет параметра», не как DBNull. CLR interop из SmartScript не поддерживает System.DBNull.Value. Вместо null передавать safe defaults: "{}" (пустой PG array), 0, "".
Применимо к: pgQueryNpgsql() и pgExec() в agent-tools-docs.js (SS 498).
docs/domains/ai/anfisa-qa-memory-spec.md — секция 10.2, полный разбор.
Ловушки имён колонок — ВСЕГДА проверяй DDL¶
Имена колонок в 1Форме контринтуитивны. Не угадывай — проверь DDL (DB_MSSQL/dbo.{Table}.Table.sql).
Типичные ошибки:
| Таблица | Ожидаешь | На самом деле | Примечание |
|---|---|---|---|
Subcategories |
Name |
Description (varchar 140) |
Нет колонки Name вообще |
ExtParams |
Name |
ExtParamName (varchar 900) |
Нет колонки Name |
SettingsCustom |
Key, Value |
[Key], [Value] |
Зарезервированные слова — квотировать через qi() |
SmartExpressions |
Public |
[Public] (bit) |
Зарезервированное слово — qi("Public") |
Categories |
— | Это Разделы, не Категории | Категории = Subcategories |
Зарезервированные слова (требуют qi() или квадратных скобок): Name, Order, Key, Value, Public, Type, Description, Default. Если колонка — зарезервированное слово, используй qi("ColumnName").
Правило при написании SS с SQL: перед деплоем — для каждого SQL-запроса проверить все колонки по DDL в DB_MSSQL/. Не полагаться на «очевидные» имена.
Правило при создании нового SS через API:
- language — enum-строка: "Lua", "JavaScript", "Python" (не "JS", не "Js")
- contextType — строка "None" (не null, не 0)
- isLibrary — boolean (true/false)
9. Sandbox и ограничения¶
| Параметр | Значение |
|---|---|
| Timeout | 5 минут |
| MaxStatements | 1 000 000 |
| CLR-типы | Заблокированы по умолчанию — только whitelist |
| CLR exceptions | Перехватываются как JS Error (ExceptionHandler = _ => true) |
js-scripting-jint.md:63-82
print() бесполезен для дебага¶
print() пишет в консоль SmartScript (output движка), но не в комментарии, не в логи. В production — не видно никому.
docs/domains/ai/README.md — gotcha «print() в SmartScript — бесполезно».
Дебаг: _perfLog + служебный тред¶
Паттерн из SS 492:
var _perfLog = [];
function logPerf(label, startMs) {
_perfLog.push(label + ": " + (Date.now() - startMs) + "ms");
}
// В конце скрипта — постим лог в служебный тред
if (_perfLog.length > 0) {
SMART.execute_action_in_task_context("PostComment", taskId, {
Task: taskId,
CommentText: "[perf] " + _perfLog.join(", "),
ThreadCommentId: serviceThreadId,
Recipients: [CONFIG.ANFISA_USER_ID]
});
}
docs/domains/ai/README.md — «_perfLog массив + постинг в служебный тред».
logToAutomation — структурированные логи¶
Для постоянного логирования — AutomationScriptsLog (таблица БД):
// SS 492 v4: замена print → logToAutomation
// Доступен через SMART или SQL в зависимости от версии
SQL.query(
"INSERT INTO AutomationScriptsLog (ScriptId, Message, TaskId, CreatedAt) VALUES (@sid, @msg, @tid, GETDATE())",
{ sid: 492, msg: "Tool: qmd_search, 230ms", tid: taskId }
);
SESSION_USER — всегда доступен¶
SESSION_USER.Id (не .UserId). Работает даже при тестовом запуске (id=0, contextType=5).
CONTEXT и EVENTPARAMS при тестовом запуске = undefined.
smartscript-migration-spec.md:367, 477
Async → sync безопасен¶
task.Result для CLR Task-ов безопасен в ASP.NET Core (нет SynchronizationContext). Deadlock невозможен.
ВАЖНО: await НЕ работает в рекуренсах (синхронный запуск). editor/execute использует ExecuteAsync и поддерживает await, но рекуренс запускает скрипт синхронно — top-level await вызовет ошибку парсинга: "await is only valid in async functions". Всегда использовать .Result.
smartscript-migration-spec.md:430-431
SmartScript Editor API — формат запроса¶
{
"script": {
"id": 0,
"scriptCode": "...",
"language": 1
}
}
НЕ {"id":0, "languageId":1, "code":"..."} — вызовет NullReferenceException (editor.Script = null).
Поля SmartScriptDto: language (ScriptLanguage enum: 0=Lua, 1=JavaScript, 2=Python), eventId (106 для публикаций), isLibrary, contextType, description. Поле languageId не существует — будет проигнорировано, язык останется 0 (Lua). При обновлении без eventId — значение обнулится → EVENTPARAMS undefined.
Save: {"script": scriptDto, "context": {"subcatId": ..., "eventId": ..., "contextType": ...}}.
Multipart upload из SmartScript — CLR обход¶
HTTP.send_http_request не поддерживает Content-Type: multipart/form-data; boundary=... — StringContent в .NET 8+ отклоняет boundary в MediaTypeHeaderValue. HTTP.post_multipart с Value (без FileId) создаёт StringContent без filename в Content-Disposition.
Решение: создать MultipartFormDataContent через CLR:
var multipart = UTILS.create_instance(
"System.Net.Http", "System.Net.Http.MultipartFormDataContent", []);
var modelContent = UTILS.create_instance(
"System.Net.Http", "System.Net.Http.StringContent", [modelJson]);
multipart.Add(modelContent, "modelData");
var fileContent = UTILS.create_instance(
"System.Net.Http", "System.Net.Http.StringContent", [fileText]);
multipart.Add(fileContent, "file", fileName); // 3-й аргумент = filename
var httpClient = UTILS.create_instance(
"System.Net.Http", "System.Net.Http.HttpClient", []);
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("1F-Pat", pat);
var response = httpClient.PostAsync(url, multipart).Result;
var body = response.Content.ReadAsStringAsync().Result;
multipart.Dispose();
httpClient.Dispose();
docs/domains/integrations/docs-disk-sync-gitlab.js — рабочий пример (SS 510).
smartscript-migration-spec.md:407-426
10. Table EP — запись строк из SmartScript¶
Табличные ДП (Table ExtParam) — запись/обновление/удаление строк из JS SS.
API endpoints¶
| Версия | Endpoint | Описание |
|---|---|---|
| v1.0 | POST /app/v1.0/api/mobile/tasks/{taskId}/extparamtable |
Legacy, body: {id: epId, rows: string[]} |
| v1.2 | POST /app/v1.2/api/task/{taskId}/ep/table/{epId}/update |
Актуальный, DTO ниже |
DTO: EpTableRowModifyDto¶
// Добавить строку в Table EP
var body = {
rows: [{
id: 0, // 0 = новая строка
modifyType: "create", // "create" | "update" | "delete"
cols: [
{ id: 35911, value: "Валидация" }, // Фаза (Text column)
{ id: 35921, value: "OK" }, // Статус (Text column)
{ id: 35931, value: "YAML parsed" }, // Детали (Text column)
{ id: 35941, value: "2026-03-13T12:00:00" } // Timestamp (DateTime column)
]
}]
};
// Вызов из SS через HTTP.fetch (v1.2 endpoint)
var resp = HTTP.fetch(
BASE_URL + "/app/v1.2/api/task/" + taskId + "/ep/table/" + epId + "/update",
{ method: "POST", body: JSON.stringify(body), headers: {"Content-Type": "application/json"} }
);
Формат значений в cols¶
| Тип колонки | Формат value |
|---|---|
| Text | строка: "some text" |
| DateTime | ISO: "2026-03-13T12:00:00" |
| Number | строка числа: "42.5" |
| Select/Combobox | ID значения: "12755" |
| Lookup | taskId: "98765" |
Чтение данных Table EP¶
POST /app/v1.2/api/task/{taskId}/ep/table/{epId}
Body: { take: 100, skip: 0 }
Ответ: data.data[] — массив строк. Значения в полях c{columnId}, например:
- c35911.stringValue — текст
- c35941.dateTimeValue — дата
Обновление/удаление¶
// Обновить существующую строку
{ id: 12345, modifyType: "update", cols: [{ id: 35921, value: "Failed" }] }
// Удалить строку
{ id: 12345, modifyType: "delete", cols: [] }
Из SmartScript: альтернатива через MCP action¶
MCP-тул mobile_tasks_ext_params_modify (сервер /mcp-ext-params-table-mobile) — обёртка над v1.0 endpoint. Подходит если SS вызывает через SMART actions, а не HTTP.
Граблеe¶
- id=0 для новой строки — обязательно,
nullили отсутствие → ошибка - modifyType — строка, не enum-число.
"create", не0 - При
"delete"массивcolsможно передать пустым, но поле должно присутствовать - v1.0 endpoint принимает
rowsкакstring[](JSON-строки), v1.2 — как объекты
11. Антипаттерны — чеклист «не делай так»¶
Хардкод ID категорий/пользователей (P6)¶
Проблема: if (chatId == -123456789) { ... } else if (chatId == -987654321) { ... } — каждый новый клиент/чат требует правки кода.
Решение: конфигурационная таблица или SettingsCustom с JSON.
// Плохо
if (subcatId == 5641) { ... }
// Хорошо
var config = UTILS.json_decode(
SQL.scalar("SELECT Value FROM SettingsCustom WHERE [Key] ='gitlab_state_mapping'")
);
var mapping = config[String(subcatId)];
docs/domains/integrations/audit-2026-03-07.md:194-196 — P6.
Дублирование кода между скриптами (P5)¶
Проблема: три копии одного скрипта (SS 7/274/133 в Telegram). Рассинхронизация гарантирована.
Решение: один параметризованный скрипт + include() библиотек.
docs/domains/integrations/audit-2026-03-07.md:190-192 — P5.
SQL вместо сервисного слоя¶
Проблема: прямой UPDATE Comments SET Content = '...' — обходит валидацию, ACL, логирование.
Решение: SMART.execute_action или UTILS.resolve_instance для вызова сервисов.
docs/domains/integrations/audit-2026-03-07.md:180-183 — P3.
Отсутствие rate limit на внешние API¶
Проблема: без ограничений скрипт может забить внешний API (Anthropic: 2K RPM, OpenAI: лимиты на org). Блокировка ключа необратима.
Решение: rate limit через CACHE (см. секцию 1).
Игнорирование идемпотентности webhook-ов¶
Проблема: повторный webhook → дубль комментария, дубль задачи, дубль перехода.
Решение: delivery ID tracking через CACHE (см. секцию 7).
docs/domains/integrations/audit-2026-03-07.md:209 — P9.
print() для дебага в production¶
Проблема: print() пишет в никуда. 30+ вызовов var_dump в SS 7 (Telegram) засоряют output.
Решение: _perfLog + служебный тред, или logToAutomation.
docs/domains/integrations/audit-2026-03-07.md:216-218 — P11.
Предсказуемые пароли при автосоздании пользователей (P2)¶
Проблема: {ФамилияИмя}Temp123456 — зная алгоритм, можно войти под любым автосозданным юзером.
Решение: случайный пароль или invite-flow без пароля.
docs/domains/integrations/audit-2026-03-07.md:177-179 — P2.
DELETE в SELECT-запросе (P4)¶
Проблема: побочный эффект при каждом incoming webhook. Мутация в read-path.
Решение: отдельный scheduled cleanup.
docs/domains/integrations/audit-2026-03-07.md:184-186 — P4.
Связанные документы¶
| Документ | Путь |
|---|---|
| Jint API reference | docs/domains/smart-actions/js-scripting-jint.md |
| Анфиса (SS 492) — архитектура + gotchas | docs/domains/ai/README.md |
| AI Search миграция — Jint Runtime Gotchas | docs/domains/search/smartscript-migration-spec.md |
| Telegram-синхронизация аудит | docs/domains/integrations/audit-2026-03-07.md |
| GitLab-интеграция спека | docs/domains/integrations/gitlab-1forma-integration-spec.md |
| Публикации (admin manual) | ../notifications/admin.md |
| Python scripting | docs/domains/smart-actions/python-scripting.md |
| NLua проблемы на .NET 9 | docs/domains/smart-actions/faq-lua-pcall-error-handling.md |
| Перегрузки CLR-методов в Jint (overload resolution) | docs/domains/smart-actions/faq-jint-method-overload-resolution.md |
| Обзор смарт-действий | docs/domains/smart-actions/backend.md |