feat: parity — news, currency, crypto, fun, wiki, power, help (82 commands)

Mirrors the rust analog-inspired tier-2 packs. yaml + ast smoke pass.
56 → 82 commands (+26 yaml entries; 14 of them are power/fun/wiki/
news/etc. permutations).

main.py — 7 new action handlers, all built on stdlib urllib.request
(no new dep). Helper _http_get() with User-Agent header so RSS feeds
that 403 default UAs go through.

+ do_news: pulls RSS (default Lenta.ru), extracts <title> via regex,
  skips channel title (idx 0), takes 5, sends to Groq for a 2-3
  sentence summary at temp 0.4. Falls back to reading first 3
  headlines verbatim if no token.
+ do_currency_rate: cbr-xml-daily.ru daily_json.js. Recognises
  USD/EUR/CNY/GBP/JPY/KZT in the phrase, computes value/nominal,
  diffs against Previous, speaks Russian with direction.
+ do_crypto_price: CoinGecko simple/price for BTC/ETH/SOL/DOGE/TON
  with 24h change, picked by phrase keywords.
+ do_fun_ask: joke / fact / quote / compliment via Groq at temp 1.0
  with per-kind system prompts, random seed in user message.
+ do_wiki_lookup: ru.wikipedia opensearch → page summary, optional
  Groq retell when extract > 350 chars.
+ do_power: dispatch table for shutdown / restart / sleep /
  hibernate / logoff / cancel — runs shutdown.exe /s|/r|/h|/l|/a
  or rundll32 powrprof,SetSuspendState. 30-second grace on shutdown
  and restart with /c message so "отмени выключение" works.
+ do_help_commands: sorts VA_CMD_LIST.keys(), speaks first 12 + total,
  full list to clipboard.

commands.yaml +26 entries:
news_headlines, currency_rate, crypto_price, joke / fact / quote /
compliment (action:fun_ask, kind: ...), wiki_lookup, shutdown_pc /
restart_pc / sleep_pc / hibernate_pc / logoff_user / cancel_shutdown
(action:power, op: ...), help.

Inspired by what mainstream voice assistants (Siri/Alexa/Алиса/
Cortana) ship by default + power-user utilities (shutdown,
window/process control).
This commit is contained in:
Bossiara13 2026-05-15 12:43:26 +03:00
parent 5790ca18af
commit 40a46567b8
2 changed files with 360 additions and 0 deletions

View file

@ -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:
- спасибо

232
main.py
View file

@ -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'<title>\s*<!\[CDATA\[(.+?)\]\]>\s*</title>', body)
if not titles:
titles = _re_reminders.findall(r'<title>\s*(.+?)\s*</title>', 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 = ''