Image

HalChatLocalAI: как я встроил офлайн-ИИ прямо в мессенджер

Большинство ИИ-ассистентов работают в облаке. А я сделал локальный — прямо внутри мессенджера HalChat.

Большинство современных ИИ-ассистентов работают в облаке, требуют подключения к серверам и не дают контроля над данными. Я решил исследовать, возможно ли встроить искусственный интеллект прямо в мессенджер, чтобы он работал локально прямо в браузере, офлайн и под управлением самого пользователя.

Цель HalChatLocalAI — упростить взаимодействие человека с ИИ и встроить его в повседневную жизнь через общение в мессенджере. Пользователь может общаться с локальным ассистентом, подключать свои модели, а в будущем — приглашать ИИ-ботов в групповые чаты и голосовые комнаты.

Система реализована на JavaScript и моём собственном языке HalSM, через плагинную архитектуру.

Ключевые принципы:

  • Локальность — всё выполняется на устройстве, без отправки данных в облако.

  • Приватность — полное отсутствие внешней зависимости.

  • Децентрализация — любой разработчик может публиковать и подключать собственные модели под нужные функции.

  • Расширяемость — взаимодействие реализовано через систему плагинов HalSM.

Почему не просто «ещё один интерфейс к Ollama/WLLama»

WLLama используется как низкоуровневый исполнитель моделей, но вся архитектура взаимодействия построена с нуля:

  • Плагины HalSM управляют логикой запросов и контекстом.

  • JS-слой отвечает за интеграцию с HalChat и UI.

  • Сами модели не зависят от конкретной реализации — можно подключить любую, даже свою собственную.

Таким образом, HalChatLocalAI — это не «обёртка», а мост между плагином, пользователем и моделью.

Архитектура

Пользователь → HalChat → HalChatPlugin
→ HalSM → LocalAIHalSM → LocalAI
→ HalSM → HalChatPlugin → HalChat → Ответ

Это базовый пример как проходит от сообщения пользователя до конечного сгенерированного результат.

Практическое применение

  • Общение с локальным ассистентом в HalChat.

  • Создание личных ботов, которые работают без сервера.

  • Подключение ИИ в групповые чаты.

  • В будущем — интеграция в HalVoice (ИИ-участник голосового чата).

Преимущества локальных ИИ

  • Приватность — ваши данные не обрабатывает ИИ на сервере, и тем самым не создаёт возможные утечки, и использование их для маркетинга или иных целей.

  • Экономия и экологичность — сейчас новые ИИ всё больше и больше требуют огромных масштабов серверов, что приводит к подорожанию компонентов и к ухудшению экологии.

  • Модульность — возможность создавать целые сети из несколько локальных ИИ для взаимодействия между ними. Также есть возможность динамично её изменять исходя из потребностей и запроса пользователя.

  • Работа без интернета (*если заранее установлены модели и плагины).

Недостатки

  • Безопасность — злоумышленники могут менять ИИ модели или внедрять в плагины вредоносный код, позволяя тем самым доступ к вашим данным. (Это решается модерацией и разрешениями от пользователей на определённый доступ к данным и действиям — но это не 100% защита).

  • Скорость — очень низкая скорость по сравнению с вычислительными кластерами (*повышается за счёт многопоточности или ещё лучше — работа на видеокарте).

  • Ограничения в размере ИИ — браузеры ограничивают размеры памяти для страниц и кода.

  • Мало знаний — не могут быть использованы модели ИИ выше 7 миллиардов параметров для обычных ПК и смартфонов. Но есть преимущество в возможности узконаправленности моделей, на каждую группу задач своя ИИ модель, а найти подходящую можно будет на HalNetMarket.

Реализация

Я не стал переписывать весь движок WLLama, а лишь добавил модуль взаимодействия на JS и плагин на HalSM.

Код для взаимодействия с WLLama:

/* * LocalLLM.js (ESM) * * Как подключить: * <script type=»module» src=»https://halchat.halwarsing.net/resources/js/ai/LocalLLM.js»></script> * * Требования для многопоточного WASM: * На HTML-страницу отдай заголовки: COOP: same-origin, COEP: require-corp. * Для wasm отдай CORP: cross-origin и правильный MIME: application/wasm. */ export class HalChatLocalLLM { /** * @param {Object} opts * @param {string} [opts.wllamaModuleUrl] URL до /esm/wllama.js (локально на твоём сервере) * @param {{single:string,multi:string}} [opts.wasmPaths] Пути к wasm (single/multi) * @param {any} [opts.wllama] Уже импортированный класс Wllama (если не хочешь динамический import) * @param {number} [opts.parallelDownloads] */ constructor(opts = {}) { this.opts = opts; this.core = null; // экземпляр Wllama this.loaded = false; } async #ensureCore() { if (this.core) return; let WllamaCtor = this?.opts?.wllama; if (!WllamaCtor) { const moduleUrl = this?.opts?.wllamaModuleUrl; if (!moduleUrl) { throw new Error(‘[HalchatLocalLLM] Укажи opts.wllamaModuleUrl ИЛИ передай opts.wllama (класс Wllama).’); } const mod = await import(moduleUrl); WllamaCtor = mod.Wllama; if (!WllamaCtor) throw new Error(‘[HalchatLocalLLM] В модуле нет экспорта Wllama: ‘ + moduleUrl); } const single = this?.opts?.wasmPaths?.single; const multi = this?.opts?.wasmPaths?.multi; if (!single || !multi) { throw new Error(‘[HalchatLocalLLM] Укажи wasmPaths.single и wasmPaths.multi (локальные пути к wasm).’); } this.core = new WllamaCtor( { ‘single-thread/wllama.wasm’: single, ‘multi-thread/wllama.wasm’: multi, }, { parallelDownloads: this.opts.parallelDownloads ?? 4 } ); } /** * Загрузка модели из разных источников * @param {{kind:’hf’,repo:string,file:string}|{kind:’url’,url:string}|{kind:’urls’,urls:string[]}|{kind:’files’,files:FileList|Blob[]}} src * @param {{useCache?:boolean,n_threads?:number,n_ctx?:number,n_batch?:number,seed?:number,progress?:(p:{loaded:number,total?:number,pct:number})=>void}} [opt] */ async load(src, opt = {}) { await this.#ensureCore(); const w = this.core; const cfg = { useCache: opt.useCache ?? true, n_threads: opt.n_threads, n_ctx: opt.n_ctx, n_batch: opt.n_batch, seed: opt.seed, progressCallback: (st) => { const pct = st && st.total ? Math.round((st.loaded / st.total) * 100) : 0; opt.progress && opt.progress({ loaded: st.loaded, total: st.total, pct }); }, }; if (src.kind === ‘hf’) { await w.loadModelFromHF(src.repo, src.file, cfg); } else if (src.kind === ‘url’) { if (typeof w.loadModelFromUrl === ‘function’) { var url=new URL(src.url); url.searchParams.set(«isJson»,»1″); url=url.toString(); const json=await (await fetch(url,{method:’GET’,mode:’cors’,credentials:’include’})).json(); console.log(json); if(json[‘errorCode’]===0) { await w.loadModelFromUrl(json[‘url’], cfg); } //await w.loadModelFromUrl(src.url, cfg); } else { const blob = await (await fetch(src.url,{method:’GET’,mode:’no-cors’,credentials:’include’})).blob(); await w.loadModel([blob], cfg); } } else if (src.kind === ‘urls’) { if (typeof w.loadModelFromUrl === ‘function’) { for (const u of src.urls) await w.loadModelFromUrl(u, cfg); } else { const blobs = []; for (const u of src.urls) blobs.push(await (await fetch(u)).blob()); await w.loadModel(blobs, cfg); } } else if (src.kind === ‘files’) { const list = Array.isArray(src.files) ? src.files : Array.from(src.files); await w.loadModel(list, cfg); // Blob[]/File[] } else { throw new Error(‘[HalchatLocalLLM] Unknown load source’); } this.loaded = true; } async unload() { if (this.core && typeof this.core.unload === ‘function’) { try { this.core.unload(); } catch {} } this.loaded = false; } /** * Генерация чата — поток * @param {{role:’system’|’user’|’assistant’,content:string}[]} messages * @param {{template?:’qwen-chat’|’raw’,temperature?:number,top_k?:number,top_p?:number,maxNewTokens?:number,stop?:string[]}} [opt] */ async *generateChatStream(messages, opt = {}) { this.#assertLoaded(); const w = this.core; const prompt = this.#renderChatPrompt(messages, opt.template || ‘qwen-chat’); const t0 = performance.now(); let emitted = 0; if (typeof w.createChatCompletion === ‘function’) { const it = await w.createChatCompletion( [ { role: ‘user’, content: prompt } ], { stream: true, nPredict: opt.maxNewTokens ?? 192, sampling: { temp: opt.temperature ?? 0.25, top_k: opt.top_k ?? 40, top_p: opt.top_p ?? 0.9 }, stopPrompts: opt.stop, } ); for await (const chunk of it) { const text = chunk.currentText ?? chunk.text ?? »; const delta = chunk.delta ?? »; emitted++; const dt = (performance.now() — t0) / 1000; yield { text, delta, tokensPerSec: emitted / Math.max(dt, 0.001) }; } return; } // Фолбэк: без стрима — одним куском const text = await w.createCompletion(prompt, { nPredict: opt.maxNewTokens ?? 192, sampling: { temp: opt.temperature ?? 0.25, top_k: opt.top_k ?? 40, top_p: opt.top_p ?? 0.9 }, stopPrompts: opt.stop, }); yield { text }; } /** * Генерация по одному промпту — поток */ async *generatePromptStream(prompt, opt = {}) { this.#assertLoaded(); const w = this.core; const t0 = performance.now(); let emitted = 0; if (typeof w.createCompletionStream === ‘function’) { const it = await w.createCompletionStream(prompt, { nPredict: opt.maxNewTokens ?? 192, sampling: { temp: opt.temperature ?? 0.25, top_k: opt.top_k ?? 40, top_p: opt.top_p ?? 0.9 }, stopPrompts: opt.stop, }); for await (const chunk of it) { const text = chunk.currentText ?? chunk.text ?? »; const delta = chunk.delta ?? »; emitted++; const dt = (performance.now() — t0) / 1000; yield { text, delta, tokensPerSec: emitted / Math.max(dt, 0.001) }; } return; } const text = await w.createCompletion(prompt, { nPredict: opt.maxNewTokens ?? 192, sampling: { temp: opt.temperature ?? 0.25, top_k: opt.top_k ?? 40, top_p: opt.top_p ?? 0.9 }, stopPrompts: opt.stop, }); yield { text }; } /** Синхронизаторы: вернуть целиком */ async generateChat(messages, opt = {}) { let out = »; for await (const c of this.generateChatStream(messages, opt)) out = c.text; return out; } async generatePrompt(prompt, opt = {}) { let out = »; for await (const c of this.generatePromptStream(prompt, opt)) out = c.text; return out; } // ——— helpers ——— #renderChatPrompt(msgs, template) { if (template === ‘raw’) { return msgs.map(m => `${m.role.toUpperCase()} ${m.content}n`).join(‘n’); } const parts = []; for (const m of msgs) parts.push(`<|im_start|>${m.role} ${m.content}<|im_end|>`); parts.push(‘<|im_start|>assistantn’); return parts.join(‘n’); } #assertLoaded() { if (!this.loaded) throw new Error(‘Model is not loaded. Call load(…) first.’); } }

Дальше код модуля HalSM с LocalLLM.js:

import { HalChatLocalLLM } from «/resources/js/ai/LocalLLM.js»; let llm=null; export class LocalLLMHalSM { static name = ‘LocalLLM’; static version = ‘0.0.1’; static funcs = { «load»: LocalLLMHalSM.load, «run»: LocalLLMHalSM.run, «addEvent»: LocalLLMHalSM.addEvent } static clsses={}; static events={ «generate»:[], «generate_stream»:[], «load»:[], «progressload»:[], }; static localLLM=new HalChatLocalLLM({wllamaModuleUrl: ‘/ai/wllama/esm/index.js’, wasmPaths: { single: ‘/ai/wllama/esm/single-thread/wllama.wasm’, multi: ‘/ai/wllama/esm/multi-thread/wllama.wasm’ }, parallelDownloads: 4}); static initializeVars() { return { «test»:MainHalChatPlugins.jsValueToHalSMVar(«1455») }; } static async load(hsmc, args, vrs) { var lArgs=Module._getSizeHalSMArray(args); if (lArgs!=2) {return Module.HalSM.null;} const urlVar=Module._getVariableFromHalSMArray(args,1); if(Module._getTypeVariable(Module._getVariableFromHalSMArray(args,0))===Module.HalSM.HalSMVariableType.HalSMCModule&&Module._getTypeVariable(urlVar)===Module.HalSM.HalSMVariableType.str) { const url=CharArrayToString(Module._getStringFromValue(Module._getValueVariable(urlVar))); llm=LocalLLMHalSM.localLLM.load({ kind: ‘url’, url: url }, { n_threads: 6, n_ctx: 1024, n_batch: 64, useCache: true, progress: (p)=>LocalLLMHalSM.runEvent(«progressload»,[p.pct]) }); llm.then(()=>{ LocalLLMHalSM.runEvent(«load», []); }); } return Module.HalSM.null; } static async run(hsmc, args, vrs) { var lArgs=Module._getSizeHalSMArray(args); if (lArgs!=7) {return Module.HalSM.null;} const promptSystemVar=Module._getVariableFromHalSMArray(args,1); const promptVar=Module._getVariableFromHalSMArray(args,2); const temperatureVar=Module._getVariableFromHalSMArray(args,3); const top_kVar=Module._getVariableFromHalSMArray(args,4); const top_pVar=Module._getVariableFromHalSMArray(args,5); const max_new_tokensVar=Module._getVariableFromHalSMArray(args,6); if(Module._getTypeVariable(Module._getVariableFromHalSMArray(args,0))===Module.HalSM.HalSMVariableType.HalSMCModule&&Module._getTypeVariable(promptSystemVar)===Module.HalSM.HalSMVariableType.str &&Module._getTypeVariable(promptVar)===Module.HalSM.HalSMVariableType.str&&Module._getTypeVariable(temperatureVar)===Module.HalSM.HalSMVariableType.double&&Module._getTypeVariable(top_kVar)===Module.HalSM.HalSMVariableType.int &&Module._getTypeVariable(top_pVar)===Module.HalSM.HalSMVariableType.double&&Module._getTypeVariable(max_new_tokensVar)===Module.HalSM.HalSMVariableType.int) { const prompt=CharArrayToString(Module._getStringFromValue(Module._getValueVariable(promptVar))); const promptSystem=CharArrayToString(Module._getStringFromValue(Module._getValueVariable(promptSystemVar))); const temperature=Module._getDoubleFromValue(Module._getValueVariable(temperatureVar)); const top_k=Module._getIntFromValue(Module._getValueVariable(top_kVar)); const top_p=Module._getDoubleFromValue(Module._getValueVariable(top_pVar)); const max_new_tokens=Module._getIntFromValue(Module._getValueVariable(max_new_tokensVar)); await llm; const msgs=[ { role: ‘system’, content: promptSystem }, { role: ‘user’, content: prompt } ]; var lastCh=»»; for await (const ch of LocalLLMHalSM.localLLM.generateChatStream(msgs, { maxNewTokens: max_new_tokens, stop: [«<|im_end|>», «</s>», «<|endoftext|>»], temperature: temperature, top_k: top_k, top_p: top_p })) { LocalLLMHalSM.runEvent(«generate_stream», [ch.text]); lastCh=ch.text; } LocalLLMHalSM.runEvent(«generate», [lastCh]); return MainHalChatPlugins.jsValueToHalSMVar(lastCh); } return Module.HalSM.null; } static addEvent(hsmc, args, vrs) { var lArgs=Module._getSizeHalSMArray(args); if (lArgs!=3) {return Module.HalSM.null;} const nameVar=Module._getVariableFromHalSMArray(args,1); const funcVar=Module._getVariableFromHalSMArray(args,2); if(Module._getTypeVariable(Module._getVariableFromHalSMArray(args,0))===Module.HalSM.HalSMVariableType.HalSMCModule&&Module._getTypeVariable(nameVar)===Module.HalSM.HalSMVariableType.str&&Module._getTypeVariable(funcVar)===Module.HalSM.HalSMVariableType.HalSMLocalFunction) { const name=CharArrayToString(Module._getStringFromValue(Module._getValueVariable(nameVar))); const funcVal=Module._getValueVariable(funcVar); if(Object.keys(LocalLLMHalSM.events).indexOf(name)!==-1) { LocalLLMHalSM.events[name].push(funcVal); } } } static runEvent(name, args) { if(Object.keys(LocalLLMHalSM.events).indexOf(name)!==-1) { const hsmargs=MainHalChatPlugins.getHalSMArguments(args); for(const funcVal of LocalLLMHalSM.events[name]) { console.log(«runEvent: «+name); Module._runLocalFunction(funcVal, hsmargs, Module.HalSM.nulldict); } } } }

И сам код плагина на HalSM:

import LocalLLM import HalChat models=[«https://haldrive.halwarsing.net/file/n0RZLj1AQDKUUmcgZ8anqKXhSqcaN5z0VbvI2mJstdOjBPdRnosm0VvqPSOmeJDqb1v8lOGyt1BcbqZ6WfQArx7o6ayzLAvQLIpT.gguf»,»https://haldrive.halwarsing.net/file/xx5BkX8ZnJZlkj1olJal55JK36I4Hg5ic9PClt3oPW2UFpNyjE28yWFfsucLkbRD4ivPaQymxqCE3kTUWouhbRl66k9nSIcuYfHM.gguf»,»https://haldrive.halwarsing.net/file/rXwzvkC6oNNiosBm3LZEnH3znjhVEQtvFKGpFDLJmjfPPAtQarR1T4Z1dD7pRvIKwubeq8ocZusfgJGLIk0i9vleaYYVzHB6lHWO.gguf»] select_model=-1 global_msg_id=-1 system_prompt=»Роль: локальный ИИ-помощник. Отвечай точно, кратко и логично. Если вопрос очевиден математика, код, факты — просто дай результат без пояснений. Если информации нет — скажи: Я не располагаю достоверной информацией. Разделяй факты и предположения только если ответ неоднозначный. Не выдумывай, не фантазируй. Все вычисления и логика происходят локально. Не используй интернет и не храни данные.» #generation def on_generate(text) { HalChat.editMessage(global_msg_id, text) } def on_generate_stream(text) { HalChat.editLocalMessage(global_msg_id, text) } #download model def on_load() { if(global_msg_id==-1) { return false } HalChat.editMessage(global_msg_id, «Модель успешно загружена»); } def on_progress_load(pr) { if(global_msg_id==-1) { return false } HalChat.editLocalMessage(global_msg_id, «Загрузка: «+pr+»%»); } #get config model #on send message user def on_send_message(msgId,type,time,text,fromNickname,fromId,fromIcon,chatUid,attachments, pluginData) { if(select_model==-1) { if((text==»1″)||(text==»2″)||(text==»3″)) { select_model=int(text)-1 HalChat.sendMessage(«Модель загружается, подождите…», [], «», «», -1, -1, ‘{«LocalBotMsg»:{ «nickname»:»SUPER AI», «icon»:»7CvasBij84cPuQbyj7pMycUfPHXp7SNLRa6MfrwWGpmrhP7hp1xstSjK39kDBeRSriGFarbxSrZFEPsEcgHrmXEHjlQpqtQINuMx» },»LocalLLMTest»:»ignore»}’) LocalLLM.load(models[select_model]) } return false } HalChat.sendMessage(«Генерация…», [], «», «», -1, -1, ‘{«LocalBotMsg»:{ «nickname»:»SUPER AI», «icon»:»7CvasBij84cPuQbyj7pMycUfPHXp7SNLRa6MfrwWGpmrhP7hp1xstSjK39kDBeRSriGFarbxSrZFEPsEcgHrmXEHjlQpqtQINuMx» }}’) print(«Gen») LocalLLM.run(system_prompt, text, 0.5, 40, 0.9, 1000) } #get last msgId def on_local_sended_message(msgId, pluginData) { if(select_model==-1) { return false } global_msg_id=msgId } HalChat.addEvent(«onUserSendMessage», on_send_message) HalChat.addEvent(«onLocalBotSendedMessage»,on_local_sended_message) LocalLLM.addEvent(«generate», on_generate) LocalLLM.addEvent(«generate_stream», on_generate_stream) LocalLLM.addEvent(«load», on_load) LocalLLM.addEvent(«progressload»,on_progress_load) HalChat.sendMessage(«Выберите ИИ (напишите цифру): 1. QWEN-2.5-coder 0.5B 2. Llama3.2 1B 3. Gemma-3 1B», [], «», «», -1, -1, ‘{«LocalBotMsg»:{ «nickname»:»SUPER AI», «icon»:»7CvasBij84cPuQbyj7pMycUfPHXp7SNLRa6MfrwWGpmrhP7hp1xstSjK39kDBeRSriGFarbxSrZFEPsEcgHrmXEHjlQpqtQINuMx» },»LocalLLMTest»:»ignore»}’)

Демо

Создаём чат и добавляем тестовый ИИ плагин. При загрузке чата, он автоматически предложит выбрать модель из списка. После выбора он загружает модель с HalDrive (оттуда загружается и плагин). После загрузки пишем ему запрос.

Генерация идёт в реальном времени и выводит результат динамично, но только локально, он сохранит итог (изменит сообщение в HalChat) только после завершения генерации. Так что к переписке можем иметь доступ в любое время.

Выбираем модель
Выбираем модель
Пишем запросы
Пишем запросы

Итог

Сейчас HalChatLocalAI — базовая версия системы локальных ИИ. Несмотря на ограничения, подход показывает, что децентрализованные ИИ-агенты могут работать прямо в мессенджере (в браузере) без серверов. У локальных ИИ сейчас достаточно минусов, на мой взгляд, их потенциал перевешивает текущие ограничения.

Жду ваших вопросов связанной с этой статьёй, так и про мою экосистему и язык программирования HalSM.

Соц. сети:
https://halch.at/c/tZgWWT
https://t.me/halwarsingchat
https://www.youtube.com/@halwarsing
https://vk.com/halwarsingnet

Источник: habr.com

✅ Найденные теги: HalChatLocalAI:, новости

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых