Я работаю над проектом, где задачи ведутся в Битрикс24. Условие простое: не меньше 20 часов в месяц. Больше можно, если есть задачи, меньше нельзя. Поэтому каждую задачу я засекаю штатным таймером, а в конце месяца сверяю, сколько набежало. Сам Битрикс24 по месяцам это не считает: в списке задач колонка «Затрачено» показывает время за всю жизнь задачи. Сумму выдавало приложение из Маркета.
25 октября приложение сломалось. Окно открывалось, настройки сохранялись, а сервер разработчика на каждый запрос отчёта отвечал кодом 500.
Починить это со своей стороны нельзя никак. Ночью я понял, что могу попросить ИИ-агента посчитать часы без чужих приложений, и к утру у меня было своё расширение для браузера: одна кнопка, таблица по задачам и итог за месяц. Ниже о том, как мы к нему пришли через два тупика и почему главная часть решения — один запрос fetch.
Тупик №1: своё приложение для портала
Первая идея, которую предложил агент, была очевидной: написать локальное приложение для Битрикс24. Это страница, которая встраивается в портал, получает доступ к REST API и может жить хоть в левом меню. Агент даже собрал её: один index.html в zip-архиве, который портал хранит сам, без всякого хостинга.
Поставить её я не смог. В разделе «Разработчикам» → «Другое» у меня было только два пункта, исходящий и входящий вебхук. Локальные приложения добавляет администратор портала, а я на этом портале обычный сотрудник.
Тупик №2: входящий вебхук
Вебхук — это адрес с секретным ключом, через который скрипт работает с порталом от вашего имени. Создать его может и сотрудник, так что агент переписал подсчёт на Python: скрипт берёт адрес вебхука из файла и печатает часы в консоль.
Я открыл «Входящий вебхук» и увидел рекламу и предложение купить подписку. На портале нет подписки на Маркет, а без неё вебхуки закрыты. Тупик.
Находка: портал сам ходит в REST от вашего имени
Тот, кто вошёл в Битрикс24 через браузер, уже авторизован. Страницы портала держат идентификатор сессии BX.bitrix_sessid(), и с ним REST API принимает запросы без всяких вебхуков. Проверить это можно за минуту: открыть портал, нажать F12, вкладка Console, и вставить строку:
fetch('/rest/task.elapseditem.getlist.json?sessid=' + BX.bitrix_sessid(), {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ORDER: {ID: 'desc'}, FILTER: {}, SELECT: ['ID', 'TASK_ID', 'SECONDS', 'CREATED_DATE'], PARAMS: {NAV_PARAMS: {nPageSize: 5}}})}).then(r => r.json()).then(console.log)
Ответ пришёл: пять последних записей затраченного времени с номерами задач, секундами и датами. Значит, всё, что нужно, доступно с правами обычного сотрудника. Запрос только читает данные и ничего не меняет.
allow pasting. Я набрал эти слова и всякий раз получал Uncaught SyntaxError: Unexpected identifier 'pasting': консоль выполнила фразу как код на JavaScript. А вот когда я вставил код, который выше, консоль потребовала ввести allow pasting, что я и сделал. После этого снова вставил код, и тогда он сработал. Это не ошибка браузера или портала, это такая защита."total": 6683. Столько записей у меня быть не могло: метод отдаёт время всех людей по всем задачам, которые вам видны. Поэтому фильтр по пользователю обязателен, а свой ID расширение берёт из user.current.Как считается месяц
Записи о затраченном времени отдаёт метод task.elapseditem.getlist. Фильтр — мой ID и даты месяца, страницы по 50 записей:
for (let page = 1; ; page++) {
const items = await call('task.elapseditem.getlist', {
ORDER: { ID: 'asc' },
FILTER: { USER_ID: me, '>=CREATED_DATE': ym + '-01T00:00:00', '<CREATED_DATE': next + '-01T00:00:00' },
SELECT: ['ID', 'TASK_ID', 'SECONDS', 'CREATED_DATE'],
PARAMS: { NAV_PARAMS: { nPageSize: 50, iNumPage: page } },
});
for (const it of items) if (it.CREATED_DATE.startsWith(ym)) perTask[it.TASK_ID] = (perTask[it.TASK_ID] || 0) + Number(it.SECONDS);
if (items.length < 50) break;
}
Проверка startsWith(ym) дублирует фильтр по дате. Старые методы задач понимают формат дат по-разному, и если портал проигнорирует условие, в сумму всё равно не попадёт чужой месяц. Названия задач добираются потом одним запросом tasks.task.list на каждые 50 задач.
Сама функция call — это тот же fetch, что и в консоли, только с проверкой ошибки:
const call = async (method, params) => {
const r = await fetch('/rest/' + method + '.json?sessid=' + BX.bitrix_sessid(), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
});
const d = await r.json();
if (d.error) throw d.error + ' ' + (d.error_description || '');
return d.result;
};
Портал не торопится: в моём тесте один запрос шёл 2,15 секунды. Месяц с десятками записей считается за несколько секунд.
Из закладки в расширение
Сначала это была закладка: javascript:-ссылка на панели браузера, которая рисует окно поверх портала. Но мне хотелось нормальную кнопку в отдельном профиле браузера, где у меня открыт рабочий портал, поэтому я попросил сделать расширение. Оно получилось совсем маленьким: манифест, фоновый скрипт на 16 строк, страница настроек и тот же код подсчёта на 76 строк.
Главная тонкость — где выполнять код. Обычный скрипт расширения работает в изолированном мире и не видит BX со страницы. Поэтому он внедряется в мир самой страницы, world: 'MAIN':
chrome.action.onClicked.addListener(async tab => {
const cfg = await chrome.storage.sync.get({ portal: '', norm: 0 });
if (cfg.portal && !(tab.url || '').startsWith(cfg.portal)) return chrome.tabs.create({ url: cfg.portal });
const target = { tabId: tab.id };
try {
await chrome.scripting.executeScript({ target, world: 'MAIN', func: c => { window.myTimeConfig = c; }, args: [cfg] });
await chrome.scripting.executeScript({ target, world: 'MAIN', files: ['mytime.js'] });
} catch {
chrome.runtime.openOptionsPage();
}
});
Права расширению нужны минимальные: activeTab, то есть только та вкладка, на которой вы нажали кнопку, scripting и storage для настроек. Доступа к другим сайтам у него нет.
В настройках два поля: адрес портала и норма часов в месяц. Если адрес задан, а вы нажали кнопку на другой вкладке, расширение откроет портал. Если задана норма, под итогом появляется строка «До нормы 20 ч осталось 9:06». Ради этой строки всё и затевалось.
Как поставить у себя
- Скачайте репозиторий и распакуйте.
- Откройте
chrome://extensions/(в Яндекс Браузереbrowser://extensions/) и включите «Режим разработчика». - Нажмите «Загрузить распакованное расширение» и выберите папку
extension. - Откроются настройки: впишите адрес своего портала и норму часов, если она у вас есть.
- Откройте портал и нажмите значок расширения. Повторное нажатие закрывает окно.
Удаляется расширение кнопкой «Удалить» на той же странице. Если правите код под себя, после правки нажмите ↻ на карточке расширения. Самопроверка подсчёта без портала: node test/test_mytime.mjs должен напечатать ok.
Итог
Было: отчёт о моих часах зависел от чужого сервера, и в ночь, когда этот сервер упал, я остался считать на пальцах. Стало: кнопка в браузере, которая берёт данные прямо из портала. Без администратора, без вебхуков, без подписки и без посредников, полторы сотни строк вместе с настройками.
Про работу с агентом скажу честно: два первых его решения были правильными, но не для моей ситуации. Локальное приложение и вебхук — это как раз то, что советует документация. Сработало только третье, когда мы перестали искать официальный вход и проверили одной строкой в консоли, что портал и так нас пускает. Урок шире Битрикса: прежде чем строить интеграцию, посмотрите, как с сервером разговаривает его собственная страница. Часто нужный ключ у вас уже в кармане.
И второй урок, из дневника: почему я не сделал этого раньше? Потому что приложение работало и меня устраивало. Пока не отвалилось.
Код: git.08h.ru/alex/bitrix24-my-time. Напрашивается продолжение: показывать прогресс к норме прямо на значке расширения и напоминать, если таймер забыт включённым. Если у вас не заводится или портал отвечает ошибкой, пишите в комментариях, разберёмся.
Как это было той ночью, коротко записано в личном дневнике.