Compare commits
2 Commits
7ea07c13ad
...
2ea7d04289
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ea7d04289 | ||
|
|
d1d4bd1514 |
23
dave/bot.mjs
23
dave/bot.mjs
@@ -161,9 +161,14 @@ if (process.argv.includes('--invite')) {
|
|||||||
if (!TOKEN) { console.error('no DISCORD_BOT_TOKEN in env or ../.env'); process.exit(2); }
|
if (!TOKEN) { console.error('no DISCORD_BOT_TOKEN in env or ../.env'); process.exit(2); }
|
||||||
|
|
||||||
// ---------- client ----------
|
// ---------- client ----------
|
||||||
const client = new Client({
|
// Listing ALL server members needs the privileged "Server Members Intent".
|
||||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
|
// Gate it behind an env flag: enabling the intent in code while the Developer
|
||||||
});
|
// Portal toggle is OFF makes login fail ("disallowed intents"). So this stays
|
||||||
|
// off until the user turns the portal intent on and sets WSAI_MEMBERS_INTENT=1.
|
||||||
|
const MEMBERS_INTENT = (process.env.WSAI_MEMBERS_INTENT || env.WSAI_MEMBERS_INTENT) === '1';
|
||||||
|
const intents = [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates];
|
||||||
|
if (MEMBERS_INTENT) intents.push(GatewayIntentBits.GuildMembers);
|
||||||
|
const client = new Client({ intents });
|
||||||
|
|
||||||
const perUser = new Map(); // userId -> { opusPackets, pcmFrames }
|
const perUser = new Map(); // userId -> { opusPackets, pcmFrames }
|
||||||
|
|
||||||
@@ -271,7 +276,7 @@ function buildState() {
|
|||||||
roles: [...g.roles.cache.values()]
|
roles: [...g.roles.cache.values()]
|
||||||
.filter((r) => r.id !== g.id && r.name !== '@everyone')
|
.filter((r) => r.id !== g.id && r.name !== '@everyone')
|
||||||
.map((r) => ({ id: r.id, name: r.name })),
|
.map((r) => ({ id: r.id, name: r.name })),
|
||||||
members: [...g.members.cache.values()].slice(0, 200).map((m) => ({
|
members: [...g.members.cache.values()].slice(0, 2000).map((m) => ({
|
||||||
id: m.id, name: m.displayName, bot: m.user?.bot === true,
|
id: m.id, name: m.displayName, bot: m.user?.bot === true,
|
||||||
roleIds: [...m.roles.cache.keys()],
|
roleIds: [...m.roles.cache.keys()],
|
||||||
})),
|
})),
|
||||||
@@ -342,6 +347,16 @@ client.once('clientReady', async () => {
|
|||||||
log('no default channel — waiting for the dashboard to select a server/voice channel…');
|
log('no default channel — waiting for the dashboard to select a server/voice channel…');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With the members intent on, pull the full member list of each guild so the
|
||||||
|
// whitelist/blacklist popup can search every user (not just cached speakers).
|
||||||
|
if (MEMBERS_INTENT) {
|
||||||
|
for (const g of client.guilds.cache.values()) {
|
||||||
|
g.members.fetch()
|
||||||
|
.then((m) => log(`fetched ${m.size} members of "${g.name}"`))
|
||||||
|
.catch((e) => log(`member fetch failed ("${g.name}"): ${e.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Report state + poll commands forever. This is what powers the dashboard's
|
// Report state + poll commands forever. This is what powers the dashboard's
|
||||||
// bot info, server/voice-channel pickers, participant list, and join/leave.
|
// bot info, server/voice-channel pickers, participant list, and join/leave.
|
||||||
reportLoop();
|
reportLoop();
|
||||||
|
|||||||
@@ -726,7 +726,8 @@ PAGE = r"""<!DOCTYPE html>
|
|||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat"><b id="s-turns">0</b><span>대화 수</span></div>
|
<div class="stat"><b id="s-turns">0</b><span>대화 수</span></div>
|
||||||
<div class="stat"><b id="s-errors">0</b><span>오류</span></div>
|
<div class="stat"><b id="s-errors">0</b><span>오류</span></div>
|
||||||
<div class="stat" title="이 봇이 서버 시작 후 쓴 클로드 토큰"><b id="s-claude">0</b><span>클로드 토큰</span></div>
|
<div class="stat" title="오늘 사용한 클로드 토큰(입력+출력)"><b id="s-claude-today">0</b><span>오늘 토큰</span></div>
|
||||||
|
<div class="stat" title="최근 7일 사용한 클로드 토큰(입력+출력)"><b id="s-claude-week">0</b><span>주간 토큰</span></div>
|
||||||
<div class="stat"><b id="s-up">0초</b><span>가동시간</span></div>
|
<div class="stat"><b id="s-up">0초</b><span>가동시간</span></div>
|
||||||
<div class="stat"><b id="s-conn">·</b><span>연결</span></div>
|
<div class="stat"><b id="s-conn">·</b><span>연결</span></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -822,10 +823,14 @@ function renderStatus(s){
|
|||||||
$('s-turns').textContent = s.turns_total ?? 0;
|
$('s-turns').textContent = s.turns_total ?? 0;
|
||||||
$('s-errors').textContent = s.errors_total ?? 0;
|
$('s-errors').textContent = s.errors_total ?? 0;
|
||||||
$('s-up').textContent = fmtUptime(s.uptime_s);
|
$('s-up').textContent = fmtUptime(s.uptime_s);
|
||||||
const ci=s.claude_input_tokens||0, co=s.claude_output_tokens||0, cr=s.claude_requests||0;
|
const cu = s.claude_usage || {today:{input:0,output:0,requests:0}, week:{input:0,output:0,requests:0}};
|
||||||
const cel=$('s-claude');
|
const td=cu.today||{}, wk=cu.week||{};
|
||||||
if(cel){ cel.textContent = fmtNum(ci+co);
|
const te=$('s-claude-today');
|
||||||
cel.parentElement.title = '클로드 사용량 (서버 시작 후) — 입력 '+ci.toLocaleString()+' · 출력 '+co.toLocaleString()+' 토큰 · 요청 '+cr+'회'; }
|
if(te){ te.textContent = fmtNum((td.input||0)+(td.output||0));
|
||||||
|
te.parentElement.title = '오늘 클로드 — 입력 '+(td.input||0).toLocaleString()+' · 출력 '+(td.output||0).toLocaleString()+' 토큰 · 요청 '+(td.requests||0)+'회'; }
|
||||||
|
const we=$('s-claude-week');
|
||||||
|
if(we){ we.textContent = fmtNum((wk.input||0)+(wk.output||0));
|
||||||
|
we.parentElement.title = '최근 7일 클로드 — 입력 '+(wk.input||0).toLocaleString()+' · 출력 '+(wk.output||0).toLocaleString()+' 토큰 · 요청 '+(wk.requests||0)+'회'; }
|
||||||
const listening = s.listening;
|
const listening = s.listening;
|
||||||
$('dot').className = 'dot ' + (listening ? 'live' : 'off');
|
$('dot').className = 'dot ' + (listening ? 'live' : 'off');
|
||||||
$('listen').textContent = listening ? '듣는 중' : (s.running ? '실행 중 (대기)' : '중지됨');
|
$('listen').textContent = listening ? '듣는 중' : (s.running ? '실행 중 (대기)' : '중지됨');
|
||||||
|
|||||||
@@ -194,10 +194,15 @@ class Monitor:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
s = dict(self._status)
|
s = dict(self._status)
|
||||||
s["uptime_s"] = round(_now_wall() - s["started_wall"], 1)
|
s["uptime_s"] = round(_now_wall() - s["started_wall"], 1)
|
||||||
|
from . import usage_store
|
||||||
|
s["claude_usage"] = usage_store.summary() # 오늘 / 최근 7일 토큰
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def add_claude_usage(self, input_tokens: int, output_tokens: int) -> None:
|
def add_claude_usage(self, input_tokens: int, output_tokens: int) -> None:
|
||||||
"""Accumulate one Claude call's token usage for the dashboard."""
|
"""Record one Claude call's token usage (session counters + persisted
|
||||||
|
per-day store for the dashboard's 하루/일주일 사용량)."""
|
||||||
|
from . import usage_store
|
||||||
|
usage_store.record(input_tokens, output_tokens)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._status["claude_requests"] += 1
|
self._status["claude_requests"] += 1
|
||||||
self._status["claude_input_tokens"] += int(input_tokens or 0)
|
self._status["claude_input_tokens"] += int(input_tokens or 0)
|
||||||
|
|||||||
79
wsai/usage_store.py
Normal file
79
wsai/usage_store.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""Persisted per-day Claude token usage, for the dashboard's 하루/일주일 사용량.
|
||||||
|
|
||||||
|
Each brain call records its token usage into a date bucket
|
||||||
|
(``{ "2026-08-22": {"req": n, "in": n, "out": n}, ... }``) persisted to disk so
|
||||||
|
the totals survive a restart. ``summary()`` returns today's and the last-7-days
|
||||||
|
aggregates. Kept in memory; the file is read once and rewritten on each record.
|
||||||
|
|
||||||
|
Path: ``$WSAI_USAGE_PATH`` or ``~/.config/wsai/usage.json`` (disk, never tmpfs).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_LOCK = threading.Lock()
|
||||||
|
_DATA: dict[str, dict[str, int]] | None = None
|
||||||
|
_KEEP_DAYS = 90
|
||||||
|
|
||||||
|
|
||||||
|
def _path() -> Path:
|
||||||
|
override = os.environ.get("WSAI_USAGE_PATH")
|
||||||
|
return Path(override) if override else (Path.home() / ".config" / "wsai" / "usage.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> dict[str, dict[str, int]]:
|
||||||
|
global _DATA
|
||||||
|
if _DATA is not None:
|
||||||
|
return _DATA
|
||||||
|
try:
|
||||||
|
_DATA = json.loads(_path().read_text(encoding="utf-8"))
|
||||||
|
assert isinstance(_DATA, dict)
|
||||||
|
except (OSError, ValueError, AssertionError):
|
||||||
|
_DATA = {}
|
||||||
|
return _DATA
|
||||||
|
|
||||||
|
|
||||||
|
def _write() -> None:
|
||||||
|
p = _path()
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(json.dumps(_DATA, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def record(input_tokens: int, output_tokens: int) -> None:
|
||||||
|
"""Add one call's usage to today's bucket and persist."""
|
||||||
|
today = date.today().isoformat()
|
||||||
|
with _LOCK:
|
||||||
|
data = _load()
|
||||||
|
b = data.setdefault(today, {"req": 0, "in": 0, "out": 0})
|
||||||
|
b["req"] += 1
|
||||||
|
b["in"] += int(input_tokens or 0)
|
||||||
|
b["out"] += int(output_tokens or 0)
|
||||||
|
# Prune buckets older than _KEEP_DAYS so the file can't grow forever.
|
||||||
|
cutoff = (date.today() - timedelta(days=_KEEP_DAYS)).isoformat()
|
||||||
|
for d in [d for d in data if d < cutoff]:
|
||||||
|
del data[d]
|
||||||
|
_write()
|
||||||
|
|
||||||
|
|
||||||
|
def summary() -> dict:
|
||||||
|
"""Today's and the last-7-days token totals."""
|
||||||
|
with _LOCK:
|
||||||
|
data = _load()
|
||||||
|
today = date.today().isoformat()
|
||||||
|
week_start = (date.today() - timedelta(days=6)).isoformat()
|
||||||
|
t = data.get(today, {"req": 0, "in": 0, "out": 0})
|
||||||
|
wk = {"req": 0, "in": 0, "out": 0}
|
||||||
|
for d, b in data.items():
|
||||||
|
if d >= week_start:
|
||||||
|
wk["req"] += b.get("req", 0)
|
||||||
|
wk["in"] += b.get("in", 0)
|
||||||
|
wk["out"] += b.get("out", 0)
|
||||||
|
return {
|
||||||
|
"today": {"requests": t.get("req", 0), "input": t.get("in", 0), "output": t.get("out", 0)},
|
||||||
|
"week": {"requests": wk["req"], "input": wk["in"], "output": wk["out"]},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user