diff --git a/commands.yaml b/commands.yaml index 4627a68..32bc7fe 100644 --- a/commands.yaml +++ b/commands.yaml @@ -553,6 +553,134 @@ stopwatch_stop: - выключи секундомер action: {type: stopwatch, op: stop} +news_headlines: + phrases: + - что нового + - новости + - что в мире + - последние новости + - расскажи новости + - топ новостей + action: {type: news} + +currency_rate: + phrases: + - курс доллара + - курс евро + - сколько стоит доллар + - сколько стоит евро + - курс юаня + - курс валют + - что с долларом + - что с евро + action: {type: currency_rate} + +crypto_price: + phrases: + - сколько биткоин + - цена биткоина + - курс биткоина + - цена эфира + - сколько эфир + - цена крипты + - цена соланы + action: {type: crypto_price} + +joke: + phrases: + - расскажи шутку + - пошути + - анекдот + - рассмеши меня + action: {type: fun_ask, kind: joke} + +fact: + phrases: + - расскажи факт + - интересный факт + - поделись фактом + action: {type: fun_ask, kind: fact} + +quote: + phrases: + - вдохнови меня + - цитата дня + - мотивируй + - процитируй кого-нибудь + action: {type: fun_ask, kind: quote} + +compliment: + phrases: + - сделай комплимент + - похвали меня + action: {type: fun_ask, kind: compliment} + +wiki_lookup: + phrases: + - что такое + - кто такой + - кто такая + - расскажи про + - википедия про + - найди в википедии про + action: {type: wiki_lookup} + +shutdown_pc: + phrases: + - выключи компьютер + - выключи пк + - выключи комп + action: {type: power, op: shutdown} + +restart_pc: + phrases: + - перезагрузи компьютер + - перезагрузи пк + - ребутни + - ребут + action: {type: power, op: restart} + +sleep_pc: + phrases: + - усыпи компьютер + - усыпи пк + - режим сна + - перейди в сон + action: {type: power, op: sleep} + +hibernate_pc: + phrases: + - гибернация + - переведи в гибернацию + - режим гибернации + action: {type: power, op: hibernate} + +logoff_user: + phrases: + - выйди из системы + - разлогинь + - выйди из учётки + - сеанс закончи + action: {type: power, op: logoff} + +cancel_shutdown: + phrases: + - отмени выключение + - отмени ребут + - отмени перезагрузку + - стой не выключай + action: {type: power, op: cancel} + +help: + phrases: + - что ты умеешь + - какие команды + - список команд + - помощь + - помоги + - что можешь + action: {type: help_commands} + thanks: phrases: - спасибо diff --git a/main.py b/main.py index 007bcf1..0779cc6 100644 --- a/main.py +++ b/main.py @@ -856,6 +856,224 @@ def _stopwatch_state_path() -> str: return p +import urllib.request as _urlreq +import urllib.parse as _urlparse + + +def _http_get(url: str, timeout: float = 15.0) -> str: + req = _urlreq.Request(url, headers={'User-Agent': 'Jarvis/1.0'}) + with _urlreq.urlopen(req, timeout=timeout) as r: + return r.read().decode('utf-8', errors='replace') + + +def do_news(action, voice: str): + feed = action.get('feed', 'https://lenta.ru/rss/news') + try: + body = _http_get(feed, timeout=15) + except Exception as e: + _speak("Не получил RSS.") + print(f"news fetch failed: {e}") + return + titles = _re_reminders.findall(r'\s*<!\[CDATA\[(.+?)\]\]>\s*', body) + if not titles: + titles = _re_reminders.findall(r'\s*(.+?)\s*', body) + titles = [t.strip() for t in titles if t.strip()] + if len(titles) <= 1: + _speak("Не получилось распарсить новости.") + return + top = titles[1:6] + joined = ' | '.join(top) + _set_clipboard(joined) + say = None + if config.GROQ_TOKEN: + try: + resp = client.chat.completions.create( + model=config.GROQ_MODEL, + messages=[ + {'role': 'system', 'content': 'Ты диктор. Сжато перескажи 3 главные темы из списка заголовков, 2-3 предложения.'}, + {'role': 'user', 'content': joined}, + ], + max_tokens=220, temperature=0.4, + ) + say = resp.choices[0].message.content.strip() + except Exception as e: + print(f"news LLM summary failed: {e}") + if not say: + say = "Главные новости: " + ". ".join(top[:3]) + _speak(say) + + +_CURRENCY_LABELS = {'USD': 'Доллар', 'EUR': 'Евро', 'CNY': 'Юань', + 'GBP': 'Фунт', 'JPY': 'Йена', 'KZT': 'Тенге'} + + +def do_currency_rate(action, voice: str): + lower = voice.lower() + code = 'USD' + if 'евро' in lower or 'eur' in lower: code = 'EUR' + elif 'юан' in lower or 'cny' in lower: code = 'CNY' + elif 'фунт' in lower: code = 'GBP' + elif 'йен' in lower: code = 'JPY' + elif 'тенге' in lower: code = 'KZT' + try: + data = json.loads(_http_get('https://www.cbr-xml-daily.ru/daily_json.js', timeout=10)) + except Exception as e: + _speak("Не получил данные ЦБ.") + print(f"cbr fetch failed: {e}") + return + v = data.get('Valute', {}).get(code) + if not v: + _speak(f"Нет данных по {code}.") + return + val = v['Value'] / max(1, v.get('Nominal', 1)) + prev = v.get('Previous', v['Value']) / max(1, v.get('Nominal', 1)) + diff = val - prev + label = _CURRENCY_LABELS.get(code, code) + if diff > 0.01: + _speak(f"{label} стоит {val:.2f} рубля, поднялся на {diff:.2f}.") + elif diff < -0.01: + _speak(f"{label} стоит {val:.2f} рубля, опустился на {abs(diff):.2f}.") + else: + _speak(f"{label} стоит {val:.2f} рубля, без изменений.") + + +def do_crypto_price(action, voice: str): + lower = voice.lower() + if 'эфир' in lower or 'eth' in lower or 'ethereum' in lower: + coin, label = 'ethereum', 'Эфир' + elif 'солан' in lower: coin, label = 'solana', 'Солана' + elif 'doge' in lower or 'дог' in lower: coin, label = 'dogecoin', 'Догикоин' + elif 'ton' in lower or 'тон' in lower: coin, label = 'the-open-network', 'TON' + else: coin, label = 'bitcoin', 'Биткоин' + try: + url = (f"https://api.coingecko.com/api/v3/simple/price?ids={coin}" + "&vs_currencies=usd,rub&include_24hr_change=true") + data = json.loads(_http_get(url, timeout=10)).get(coin, {}) + except Exception as e: + _speak("CoinGecko не ответил.") + print(f"coingecko fetch failed: {e}") + return + if not data: + _speak(f"Нет данных по {label}.") + return + usd = data.get('usd', 0) + ch = data.get('usd_24h_change', 0) + if ch > 1: + _speak(f"{label} стоит {int(usd)} долларов, за сутки вырос на {ch:.1f} процентов.") + elif ch < -1: + _speak(f"{label} стоит {int(usd)} долларов, за сутки упал на {abs(ch):.1f} процентов.") + else: + _speak(f"{label} стоит {int(usd)} долларов, без существенных изменений.") + + +_FUN_PROMPTS = { + 'joke': "Расскажи одну короткую шутку на русском, 1-3 предложения. Без вступления.", + 'fact': "Расскажи один интересный неочевидный факт. 1-3 предложения. Без 'вот факт'.", + 'quote': "Дай вдохновляющую цитату известного автора. Формат: <цитата> — <автор>.", + 'compliment': "Сделай искренний короткий комплимент пользователю, как Джарвис-дворецкий. 1-2 предложения, обращение «сэр».", +} + + +def do_fun_ask(action, voice: str): + kind = action.get('kind', 'joke') + sys_prompt = _FUN_PROMPTS.get(kind, _FUN_PROMPTS['joke']) + if not config.GROQ_TOKEN: + _speak("Groq токен не задан.") + return + try: + resp = client.chat.completions.create( + model=config.GROQ_MODEL, + messages=[ + {'role': 'system', 'content': sys_prompt}, + {'role': 'user', 'content': f'seed {random.randint(1, 9999)}'}, + ], + max_tokens=220, temperature=1.0, + ) + text = resp.choices[0].message.content.strip() + except Exception as e: + print(f"fun api failed: {e}") + _speak("Ошибка.") + return + _speak(text) + + +_WIKI_TRIGGERS = ('найди в википедии про', 'википедия про', 'расскажи про', + 'кто такая', 'кто такой', 'что такое', + 'tell me about', 'wikipedia about', 'who is', 'what is', + 'розкажи про', 'хто такий', 'що таке') + + +def do_wiki_lookup(action, voice: str): + query = _strip_trigger(voice, _WIKI_TRIGGERS).strip() + if not query: + _speak("О чём рассказать?") + return + host = 'ru.wikipedia.org' + try: + search_url = (f"https://{host}/w/api.php?action=opensearch" + f"&search={_urlparse.quote(query)}&limit=1&format=json") + search_data = json.loads(_http_get(search_url, timeout=10)) + if len(search_data) < 2 or not search_data[1]: + _speak(f"Не нашёл {query}.") + return + title = search_data[1][0] + summary_url = f"https://{host}/api/rest_v1/page/summary/{_urlparse.quote(title)}" + summary = json.loads(_http_get(summary_url, timeout=10)) + except Exception as e: + _speak("Википедия не ответила.") + print(f"wiki fetch failed: {e}") + return + extract = summary.get('extract', '') + if not extract: + _speak("Не получил конспект.") + return + say = extract[:400] + if config.GROQ_TOKEN and len(extract) > 350: + try: + resp = client.chat.completions.create( + model=config.GROQ_MODEL, + messages=[ + {'role': 'system', 'content': "Перескажи текст из Википедии простым языком в 2-3 предложения. Без 'википедия гласит'."}, + {'role': 'user', 'content': extract[:2500]}, + ], + max_tokens=220, temperature=0.3, + ) + say = resp.choices[0].message.content.strip() + except Exception as e: + print(f"wiki LLM failed: {e}") + _set_clipboard(f"{summary.get('title', title)}\n\n{extract}") + _speak(say) + + +_POWER_ACTIONS = { + 'shutdown': (['shutdown.exe', '/s', '/t', '30', '/c', 'J.A.R.V.I.S.: shutdown in 30s'], "Выключение через 30 секунд."), + 'restart': (['shutdown.exe', '/r', '/t', '30', '/c', 'J.A.R.V.I.S.: restart in 30s'], "Перезагрузка через 30 секунд."), + 'logoff': (['shutdown.exe', '/l'], "Выход из системы."), + 'sleep': (['rundll32.exe', 'powrprof.dll,SetSuspendState', '0,1,0'], "Перехожу в сон."), + 'hibernate': (['shutdown.exe', '/h'], "Гибернация."), + 'cancel': (['shutdown.exe', '/a'], "Выключение отменено."), +} + + +def do_power(action, voice: str): + op = action.get('op', 'cancel') + cmd, label = _POWER_ACTIONS.get(op, _POWER_ACTIONS['cancel']) + res = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if res.returncode == 0 or op == 'cancel': + tail = " Скажи отмени если передумал." if op in ('shutdown', 'restart') else "" + _speak(label + tail) + else: + _speak(f"Не получилось: {op}.") + print(f"power {op} failed: {res.stderr[:200]}") + + +def do_help_commands(action, voice: str): + names = sorted(VA_CMD_LIST.keys()) + preview = ', '.join(names[:12]) + _set_clipboard('\n'.join(names)) + _speak(f"Загружено {len(names)} команд. Например: {preview}. Полный список в буфере.") + + def do_stopwatch(action, voice: str): op = action.get('op', 'check') state = _stopwatch_state_path() @@ -1006,6 +1224,20 @@ def run_action(action): do_dice(action, _current_voice or '') elif t == 'stopwatch': do_stopwatch(action, _current_voice or '') + elif t == 'news': + do_news(action, _current_voice or '') + elif t == 'currency_rate': + do_currency_rate(action, _current_voice or '') + elif t == 'crypto_price': + do_crypto_price(action, _current_voice or '') + elif t == 'fun_ask': + do_fun_ask(action, _current_voice or '') + elif t == 'wiki_lookup': + do_wiki_lookup(action, _current_voice or '') + elif t == 'power': + do_power(action, _current_voice or '') + elif t == 'help_commands': + do_help_commands(action, _current_voice or '') _current_voice: str = ''