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:
parent
5790ca18af
commit
40a46567b8
2 changed files with 360 additions and 0 deletions
128
commands.yaml
128
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:
|
||||
- спасибо
|
||||
|
|
|
|||
232
main.py
232
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'<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 = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue