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*\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 = ''