From a6852109290632df1c8f6523569a7eef7f909cc2 Mon Sep 17 00:00:00 2001 From: yumi Date: Sat, 17 Oct 2026 16:52:18 +0800 Subject: [PATCH] feat: add Pages demo, guided setup and contributor checks --- .github/pull_request_template.md | 18 + .github/workflows/checks.yml | 62 +++ .github/workflows/pages.yml | 50 +++ .gitignore | 1 + CONTRIBUTING.md | 71 +++ README.en.md | 122 +++-- README.md | 81 +++- backend/diagnostics.py | 71 +++ backend/main.py | 8 + backend/voice_client.py | 5 + docs/DEMO.md | 57 +++ proto/voice.proto | 2 + tests/test_diagnostics.py | 65 +++ voice-service/src/main.rs | 6 + web/.env.demo | 3 + web/index.html | 3 +- web/package-lock.json | 641 ++++++++++++++++++++++++++- web/package.json | 20 +- web/public/demo-cover.svg | 8 + web/src/App.vue | 45 +- web/src/api.test.ts | 27 ++ web/src/api.ts | 5 + web/src/appConfig.ts | 18 +- web/src/components/MusicPlayer.vue | 3 + web/src/demo/api.test.ts | 68 +++ web/src/demo/api.ts | 185 ++++++++ web/src/demoMode.ts | 2 + web/src/projectLinks.ts | 4 + web/src/router.ts | 6 +- web/src/style.css | 12 + web/src/types/settings.ts | 31 ++ web/src/utils/favorites.ts | 2 +- web/src/views/ChangePasswordView.vue | 3 +- web/src/views/HistoryView.vue | 4 +- web/src/views/LikesView.vue | 5 +- web/src/views/SearchView.vue | 8 + web/src/views/SettingsView.vue | 13 +- web/src/views/SetupView.vue | 128 ++++++ web/vite.config.ts | 4 + web/vitest.config.ts | 5 + 40 files changed, 1774 insertions(+), 98 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/checks.yml create mode 100644 .github/workflows/pages.yml create mode 100644 CONTRIBUTING.md create mode 100644 backend/diagnostics.py create mode 100644 docs/DEMO.md create mode 100644 tests/test_diagnostics.py create mode 100644 web/.env.demo create mode 100644 web/public/demo-cover.svg create mode 100644 web/src/api.test.ts create mode 100644 web/src/demo/api.test.ts create mode 100644 web/src/demo/api.ts create mode 100644 web/src/demoMode.ts create mode 100644 web/src/projectLinks.ts create mode 100644 web/src/types/settings.ts create mode 100644 web/src/views/SetupView.vue create mode 100644 web/vitest.config.ts diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..30fc953 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## 解决的问题 / Problem + + + +## 修改后的行为 / Resulting behavior + + + +## 验证 / Validation + + + +- [ ] 已阅读贡献指南 / Read [CONTRIBUTING.md](https://github.com/yichen11818/NeteaseTSBot/blob/main/CONTRIBUTING.md) +- [ ] 已检查改动中没有真实凭据或运行数据 / Checked for credentials and runtime data + +## 兼容性 / Compatibility + + diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..ed0c49d --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,62 @@ +name: Pull request checks + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend regression tests + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + DATABASE_URL: 'sqlite:///:memory:' + TSBOT_COOKIE_KEY: ci-test-key-not-for-deployment + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: backend/requirements.txt + - run: python -m pip install -r backend/requirements.txt + - run: python -m unittest discover -s tests -p 'test_*.py' + + web: + name: Web tests and builds + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci --prefix web + - run: npm --prefix web run typecheck + - run: npm --prefix web test + - run: npm --prefix web run build + - run: npm --prefix web run build:demo + + voice: + name: Voice protocol check + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: voice-service + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake + - run: cargo check --manifest-path voice-service/Cargo.toml diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..7196d5a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,50 @@ +name: Demo on GitHub Pages + +on: + push: + branches: [main] + paths: ['web/**', '.github/workflows/pages.yml'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci --prefix web + - run: npm --prefix web test + - run: npm --prefix web run typecheck + - run: npm --prefix web run build:demo + env: + VITE_BASE_PATH: /${{ github.event.repository.name }}/ + - uses: actions/upload-pages-artifact@v3 + with: + path: web/dist-demo + + deploy: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/configure-pages@v5 + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.gitignore b/.gitignore index d021540..a989026 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ yarn.lock voice-service/build/ voice-service/target/ web/dist/ +web/dist-demo/ backend/data/ data/uploads/ .playwright-cli/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1c0f3fe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# 参与贡献 / Contributing + +欢迎用中文或英文提交 Issue 和 PR。部署验证、复现问题、改进文档和翻译都属于贡献,无需同时掌握 Python、Vue 和 Rust。 + +Issues and pull requests in English or Chinese are welcome. Deployment reports, bug reproduction, documentation, and translations are useful contributions too. You can work on one component at a time. + +## 适合第一次贡献的任务 / Starter tasks + +下面是可讨论的任务建议,不代表已有认领或已发布的 Issue。先搜索 [现有 Issue](https://github.com/yichen11818/NeteaseTSBot/issues),避免重复;小型文档修正可以直接提交 PR。涉及行为或接口的改动,请先开 Issue 说明方案。 + +These are proposed starting points, not assigned issues. Search existing issues first. Small documentation fixes can go straight to a PR; discuss behavior or API changes in an issue first. + +| 任务 / Task | 范围 / Scope | 验收标准 / Acceptance criteria | +| --- | --- | --- | +| 补充首次部署排障 / First-install troubleshooting | `HOWTOSTART.md` | 在干净 Linux 环境走一次预构建镜像部署;记录系统、版本、失败现象、诊断命令和有效解决步骤,隐去凭据。 / Document one reproducible setup problem, environment, diagnostic command, and verified fix without credentials. | +| 翻译外部 API 入门示例 / Translate an external API example | 新增 `docs/API.en.md`,链接至 `docs/API.md` / Add an English guide linked to the existing API reference | 覆盖 `/external/status`、`/external/search`、`/external/queue`;对照现有 API 文档核实方法、字段和 Token 用法,给出占位符请求与响应示例。 / Verify methods, fields, and token usage against the current reference; include placeholder requests and example responses. | +| 补充聊天指令示例 / Add chat-command examples | `README.en.md` | 对照中文 README 与实际指令处理代码,补充 `help`、`playlist`、`select`、`clear`、`shuffle`、`order`、`play` 的英文表格;注明歌单仅支持网易云以及选择结果的有效期。 / Check against the implementation and Chinese README; document the Netease-only playlist scope and result expiry. | + +也欢迎提交 TS3/TS6 部署兼容性报告:版本、部署方式、是否成功进频道、是否成功播放、异常现象。请注明实际验证的环境,不要把未测试的平台写为已支持。 + +Compatibility reports are welcome: include server version, deployment method, channel join result, playback result, and any failure. Report only environments you actually tested. + +## 开始修改 / Getting started + +Fork 后克隆自己的仓库,从最新 `main` 创建分支。 / Fork the repository, clone your fork, and branch from an up-to-date `main`. + +```bash +git clone https://github.com//NeteaseTSBot.git +cd NeteaseTSBot +git switch -c docs/your-change +``` + +| 修改内容 / Change | 主要位置 / Location | 需要的检查 / Validation | +| --- | --- | --- | +| 文档、翻译 / Docs, translations | `README*.md`, `HOWTOSTART.md`, `docs/` | 核对命令、链接和对应功能;预览 Markdown。 / Check commands, links, and feature accuracy; preview Markdown. | +| 前端 / Web UI | `web/` | `npm --prefix web ci`,然后 / then `npm --prefix web run typecheck`、`npm --prefix web test`、`npm --prefix web run build`、`npm --prefix web run build:demo`;附界面截图和手工验证步骤。 / Include screenshots and manual verification steps. | +| 后端 / Backend | `backend/`, `tests/` | 运行下方回归测试;业务行为变更补充相关用例。 / Run the regression tests below; add coverage for behavior changes. | +| 语音 / Voice | `voice-service/`, `proto/`, `vendor/` | `cargo check --manifest-path voice-service/Cargo.toml`;连接或播放改动还需 TS 实机验证。 / Also verify connection or playback changes against a real TS server. | +| 部署 / Deployment | Compose files, `Dockerfile.*`, scripts | 对改动的部署方式执行配置检查和实际启动;记录环境。 / Validate configuration and start the affected deployment; record the environment. | + +### 后端开发 / Backend development + +以下命令在 Linux 项目根目录执行,推荐 Python 3.11(与后端 Docker 镜像一致)。 / Run from the project root on Linux; Python 3.11 matches the backend Docker image. + +```bash +python3.11 -m venv backend/.venv +backend/.venv/bin/python -m pip install -r backend/requirements.txt +backend/.venv/bin/python -m unittest discover -s tests -p 'test_*.py' +``` + +回归测试使用临时数据库和 mock;不需要真实 TeamSpeak 服务器或平台账号。测试通过不等于已验证真实音频播放。 / The regression tests use temporary databases and mocks; no real TeamSpeak server or platform account is needed. Passing them does not verify real audio playback. + +只在验证 B 站扫码或浏览器字幕链路时额外安装 Chromium: / Install Chromium when testing Bilibili QR login or browser-based subtitle retrieval: + +```bash +backend/.venv/bin/python -m playwright install chromium +``` + +完整服务运行方式见 [中文 README](README.md) 或 [English README](README.en.md)。 / See either README for running the full stack. + +只修改前端时可使用[离线演示构建](docs/DEMO.md),无需真实服务器或平台账号。PR 会自动运行后端回归、前端测试及类型检查、两种前端构建和 Voice 协议检查。 / The offline demo build lets you develop UI without a real server or platform account. Pull requests run regression tests, frontend tests and type checks, both web builds, and a Voice protocol check. + +## 提交 PR / Submitting a PR + +- 一次解决一个问题;解释修改前后的行为,并关联对应 Issue(如有)。 / Keep one problem per PR; describe the before/after behavior and link an issue if applicable. +- 写明实际执行的检查;未验证的部分直接说明,UI 改动附截图。 / List checks you actually ran and any untested behavior; include screenshots for UI changes. +- 不提交 `tsbot.env`、数据库、真实 Cookie、Token、密码、运行日志或构建产物;示例使用占位符。 / Keep credentials, databases, runtime logs, and build output out of commits; use placeholders in examples. +- 大范围重构、新依赖、多机器人管理或协议改动先讨论边界和兼容性。 / Discuss large refactors, new dependencies, multi-bot management, or protocol changes before implementation. +- 如使用 AI 辅助,请自行理解并验证改动;描述受影响的行为和验证结果。 / If you use AI assistance, understand and verify the changes yourself; explain the affected behavior and validation. + +提交代码即表示你同意按项目的 [MIT License](LICENSE) 授权该贡献。 / Contributions are provided under the project's MIT License. diff --git a/README.en.md b/README.en.md index 789d6f9..290c059 100644 --- a/README.en.md +++ b/README.en.md @@ -1,18 +1,55 @@ -# TSBot (NeteaseTSBot) +# TSBot · TeamSpeak 3 / 6 Music Bot [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![Platform](https://img.shields.io/badge/platform-Linux-informational) -![Python](https://img.shields.io/badge/python-3.8%2B-blue) -![Node](https://img.shields.io/badge/node-16%2B-brightgreen) -![Rust](https://img.shields.io/badge/rust-1.70%2B-orange) -![FastAPI](https://img.shields.io/badge/FastAPI-0.115.6-009688?logo=fastapi&logoColor=white) -![Vue](https://img.shields.io/badge/Vue-3-42b883?logo=vue.js&logoColor=white) -![Vite](https://img.shields.io/badge/Vite-5-646CFF?logo=vite&logoColor=white) -![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) +[![Release](https://img.shields.io/github/v/release/yichen11818/NeteaseTSBot)](https://github.com/yichen11818/NeteaseTSBot/releases/latest) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![中文 README](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-red)](README.md) -> 🎵 **TSBot (NeteaseTSBot)** is a high-performance multi-platform music bot built for **TeamSpeak (TS / TS3 / TS6)**. -> Supports audio parsing and playback from **Netease Cloud Music (网易云音乐)**, **QQ Music**, and **Bilibili (B站)** with an out-of-the-box Web Console. +Listen together in your TeamSpeak channel. Search and queue **Netease Cloud Music, QQ Music, and Bilibili audio** through chat commands or a Web console. + +**NeteaseTSBot** supports **TeamSpeak 3 / 6**, with playback queues, lyrics, and prebuilt Docker images for self-hosting. + +[Get started](#start-with-prebuilt-docker-images) · [API reference](docs/API.md) · [Contribute](CONTRIBUTING.md) · [Report an issue](https://github.com/yichen11818/NeteaseTSBot/issues/new/choose) · [中文](README.md) + +A [static interactive demo mode](docs/DEMO.md) is available for GitHub Pages, with simulated search, queues, and lyrics. It runs without a backend and does not play real audio. + +![TSBot Web console](docs/1.png) + +## Start with prebuilt Docker images + +You need a Linux host, Docker Compose, and a reachable TeamSpeak 3 / 6 server. Existing installations should keep their environment file, database, and encryption key when upgrading. + +```bash +git clone https://github.com/yichen11818/NeteaseTSBot.git +cd NeteaseTSBot +cp tsbot.env.example tsbot.env +``` + +Replace `TSBOT_COOKIE_KEY` in `tsbot.env` with your own random key (generate one with `openssl rand -hex 32`), then start: + +```bash +docker compose -f docker-compose.prebuilt.yml up -d +docker compose -f docker-compose.prebuilt.yml ps +``` + +1. Open `http://:8080`. +2. Read the initial password from `logs/initial-admin-password.txt` on the host, sign in as `admin`, and change the password. +3. Set your TeamSpeak server, channel, and music sources in the Web system settings. Apply the configuration and check that the bot joins your channel. + First-time administrators enter the setup wizard after changing their password; it saves and applies configuration, then checks Voice, TeamSpeak, and the optional Netease API separately. +4. Search and queue a track in the Web console, or send `help` to the bot in TeamSpeak. + +Netease features require your own `NeteaseCloudMusicApi` service. QQ Music and Bilibili adapters are built in. Some features require a platform login; playback depends on account permissions and platform restrictions. + +For troubleshooting, run `docker compose -f docker-compose.prebuilt.yml logs --tail=100 backend voice-service`. See [source installation](#quick-start-recommended) and [Docker options](#docker-deployment) for more detail. + +## Help build TSBot + +Deployment reports, reproducible bug reports, documentation, translations, and code are welcome. See the [contribution guide and starter tasks](CONTRIBUTING.md); you can work on one component at a time. Issues and pull requests in English or Chinese are welcome. + +If TSBot helps you, consider starring the repository. To receive release notifications, choose **Watch → Custom → Releases** on GitHub. + +## Features TSBot is a TeamSpeak based music bot; the `voice-service` primary client connection already supports TS6 (historical env var names remain `TSBOT_TS3_*`). It provides: @@ -20,13 +57,19 @@ TSBot is a TeamSpeak based music bot; the `voice-service` primary client connect - **Queue and playback control** (pause/resume/next/previous, volume, shuffle/repeat, etc.) - **Netease music search/playlists/likes/lyrics** (via external `NeteaseCloudMusicApi`) - **QQ Music search/playlists/lyrics/play URLs** (built into backend; login-required features can be configured from the web console) +- **Bilibili video search and audio playback** (built-in adapter and local audio cache; available subtitles can be displayed on the lyrics page) - **Web console** (Vue 3 frontend for search/queue/lyrics/settings) +- **External integration API** (`/external/*` for search, queue, status, and playback history; see [API documentation](docs/API.md)) + +
+More screenshots + +![TSBot screenshot 2](docs/2.png) +![TSBot screenshot 3](docs/3.png) +![TSBot screenshot 4](docs/4.png) +![TSBot screenshot 5](docs/5.png) -![Preview](docs/1.png) -![Preview](docs/2.png) -![Preview](docs/3.png) -![Preview](docs/4.png) -![Preview](docs/5.png) +
## Why TSBot (Pain Points and Goals) @@ -47,7 +90,7 @@ In practice, this stack often suffers from: TSBot redraws those boundaries: - **Voice and business logic are decoupled**: `voice-service` only handles "connect to TeamSpeak + play audio", exposing a stable gRPC control surface. -- **Netease is a replaceable dependency**: backend talks to external `NeteaseCloudMusicApi` through `TSBOT_NETEASE_API_BASE`; future replacements/upgrades are largely isolated in backend adapters. +- **Netease is a replaceable dependency**: backend talks to an external `NeteaseCloudMusicApi` service configured in Web system settings; future replacements/upgrades are largely isolated in backend adapters. - **Maintainable and evolvable architecture**: frontend/backend/voice-service can iterate independently, reducing single-point breakage. The project has 3 components: @@ -87,7 +130,7 @@ Music source dependency notes: ## TeamSpeak / TS6 Support - The `voice-service` primary client connection path already supports TeamSpeak login, channel join, text messaging, and audio playback against TS3/TS6 servers. -- For backward compatibility, env vars still use the `TSBOT_TS3_*` naming scheme, even when connecting to TS6. +- Configure TeamSpeak connections in Web system settings. Historical `TSBOT_TS3_*` environment values are imported once when the corresponding database settings do not yet exist. - The code still contains an optional legacy `ServerQuery` fallback used only for old-style `client_description` updates; it is not the TS6 HTTP(S) Query interface. ## Netease Support (Optional, via `NeteaseCloudMusicApi`) @@ -97,7 +140,7 @@ This project does **not** directly call Netease official APIs. Instead, it forwa - **NPM**: https://www.npmjs.com/package/NeteaseCloudMusicApi - **Docs**: https://neteasecloudmusicapi.js.org/#/ -After deployment, set `TSBOT_NETEASE_API_BASE` to the service URL (for example, `http://127.0.0.1:3000/`). +After deployment, set the Netease API URL in Web system settings (for example, `http://127.0.0.1:3000/` for a source installation on the same host). Common deployment options (choose one, exact args follow upstream docs): @@ -129,26 +172,17 @@ Copy the template and edit: cp tsbot.env.example tsbot.env ``` -At minimum, set: +For a new installation, set `TSBOT_COOKIE_KEY` to your own random key. Keep the same key when upgrading so stored credentials remain readable. -- `TSBOT_TS3_HOST` / `TSBOT_TS3_PORT` / `TSBOT_TS3_CHANNEL_ID` (TeamSpeak connection settings; historical `TSBOT_TS3_*` naming is retained) -- `TSBOT_COOKIE_KEY` (used to encrypt stored admin cookies; use your own random string) +The backend generates an initial password for `admin`, writes it to `logs/initial-admin-password.txt`, and requires a password change on first login. Configure TeamSpeak, music sources, platform logins, and external API tokens in the Web system settings, then apply the configuration. Saving settings alone does not apply them to running services. -Depending on music source: +If you lose the admin password, run this on the server from the project root (using the backend environment created below): -- For Netease: set `TSBOT_NETEASE_API_BASE` to your `NeteaseCloudMusicApi` URL, for example `http://127.0.0.1:3000/` -- For QQ Music login-required features: store the admin QQ Music cookie via the web console or `/admin/qqmusic/*` APIs - -Optional: +```bash +backend/.venv/bin/python -m backend.admin_cli reset-password +``` -- `TSBOT_TS3_SERVER_PASSWORD` / `TSBOT_TS3_CHANNEL_PASSWORD` / `TSBOT_TS3_CHANNEL_PATH` -- `TSBOT_TS3_IDENTITY` / `TSBOT_TS3_IDENTITY_FILE` / `TSBOT_TS3_AVATAR_DIR` -- `TSBOT_ADMIN_TOKEN`: enable backend admin endpoint protection (request header `x-admin-token`) -- `TSBOT_WEB_HOST` / `TSBOT_WEB_PORT`: frontend production preview bind host/port (used by `run-web.sh` / `nohup-start.sh`) -- `TSBOT_WEB_API_PROXY_TARGET`: proxy target for frontend dev / preview mode (defaults to `TSBOT_HOST` / `TSBOT_PORT`) -- `TSBOT_WEB_ALLOWED_HOSTS`: comma-separated host allowlist when you access Vite dev / preview through a domain -- `VITE_DEV_HOST` / `VITE_DEV_PORT`: frontend dev server bind host/port -- `VITE_API_BASE`: frontend backend base URL (recommended default `/api`, forwarded by dev / preview / Docker reverse proxy) +The environment file retains startup settings for the database, listening addresses, Web proxy, and development ports. Old `TSBOT_TS3_*` and music-source environment values are imported only when their database settings do not exist. `TSBOT_ADMIN_TOKEN` is a legacy one-time initialization input, not an ongoing admin API credential. ### 2) Install Dependencies @@ -159,6 +193,7 @@ cd backend python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt +python -m playwright install chromium cd .. ``` @@ -255,9 +290,7 @@ The repository now includes: cp tsbot.env.example tsbot.env ``` -If your `NeteaseCloudMusicApi` runs on the host machine, set: - -- `TSBOT_NETEASE_API_BASE=http://host.docker.internal:3000/` +If your `NeteaseCloudMusicApi` runs on the host machine, set its URL to `http://host.docker.internal:3000/` in Web system settings after startup. ### 2) Build and run @@ -328,7 +361,14 @@ Backend OpenAPI docs: - `http://127.0.0.1:8009/docs` -## Admin Login State (Netease / QQ Music) +## Platform Login State (Netease / QQ Music / Bilibili) + +Use **System settings → Music membership login** to manage platform logins. Cookies are encrypted in the database. The old `/cookie` page redirects to this settings section. + +### Bilibili + +The built-in adapter supports video search and audio playback, with local audio caching. Administrator login is available through QR code or manual cookie entry. For subtitle fallback on videos with AI subtitles, the backend may use Playwright with the stored Bilibili login. Source installations need `python -m playwright install chromium`; the provided backend Docker image installs Chromium and its system dependencies during the image build. + ### Netease Cookie @@ -337,7 +377,7 @@ Backend encrypts and stores the "admin Netease cookie" in database (`tsbot.db`) - getting more stable song URLs (avoid anonymous restrictions on some endpoints) - accessing features requiring login state (playlists, likes, etc.) -Setup APIs (if admin token is enabled, include header `x-admin-token: `): +The Web console handles administrator authentication through `/auth/login`. Direct API clients must retain the admin session and include the session CSRF token on state-changing requests. Admin APIs include: - `POST /admin/cookie`: store cookie - `GET /admin/status`: check if cookie exists @@ -352,7 +392,7 @@ Backend also encrypts and stores the "admin QQ Music cookie" in database (`tsbot - getting more stable QQ Music play URLs - accessing login-required features such as user playlists and account info -Setup APIs (if admin token is enabled, include header `x-admin-token: `): +The Web console handles administrator authentication through `/auth/login`. Direct API clients must retain the admin session and include the session CSRF token on state-changing requests. Admin APIs include: - `GET /admin/qqmusic/status`: check if cookie exists - `POST /admin/qqmusic/cookie`: store cookie manually @@ -404,7 +444,7 @@ See `LOGGING.md` for details (`scripts/log-viewer.sh` / `scripts/unified-logger. - Ensure `make voice-run` or `run-voicemake.sh` is running - **Is TS6 fully supported?** - - The primary client connection already supports TS6, while configuration still uses the historical `TSBOT_TS3_*` names. + - The primary client connection supports TS6. Configure it in Web system settings; historical `TSBOT_TS3_*` environment values are imported once for migration. - Legacy `TSBOT_TS3_SERVERQUERY_*` settings still map to the old ServerQuery fallback, not TS6 HTTP(S) Query. ## License diff --git a/README.md b/README.md index 78c731f..ceed4a6 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,55 @@ -# TSBot (NeteaseTSBot) +# TSBot · TeamSpeak 3 / 6 音乐机器人 [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![Platform](https://img.shields.io/badge/platform-Linux-informational) -![Python](https://img.shields.io/badge/python-3.8%2B-blue) -![Node](https://img.shields.io/badge/node-16%2B-brightgreen) -![Rust](https://img.shields.io/badge/rust-1.70%2B-orange) -![FastAPI](https://img.shields.io/badge/FastAPI-0.115.6-009688?logo=fastapi&logoColor=white) -![Vue](https://img.shields.io/badge/Vue-3-42b883?logo=vue.js&logoColor=white) -![Vite](https://img.shields.io/badge/Vite-5-646CFF?logo=vite&logoColor=white) -![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) +[![Release](https://img.shields.io/github/v/release/yichen11818/NeteaseTSBot)](https://github.com/yichen11818/NeteaseTSBot/releases/latest) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![English README](https://img.shields.io/badge/README-English-blue)](README.en.md) -

- 🌟 推荐项目 / Related Project

- - WebSpeak-client-for-TeamSpeak - -

+在 TeamSpeak 频道里和朋友一起听歌:通过聊天指令或 Web 控制台点播 **网易云音乐、QQ 音乐和 Bilibili 音频**。 -> 🎵 **TSBot (NeteaseTSBot)** 是一个专为 **TeamSpeak (TS / TS3 / TS6)** 打造的高性能多平台音乐点播机器人 (Music Bot)。 -> 支持 **网易云音乐 (Netease Cloud Music)**、**QQ 音乐** 与 **Bilibili (B站)** 音频解析与点播播放,并配备开箱即用的 Web 控制台与一键式部署方案。 +**NeteaseTSBot** 支持 **TeamSpeak 3 / 6**,提供播放队列、歌词和 Docker 预构建镜像,可自行部署。 + +[快速部署](#先把机器人跑起来) · [聊天指令](#teamspeak-聊天指令) · [API 文档](docs/API.md) · [参与贡献](CONTRIBUTING.md) · [反馈问题](https://github.com/yichen11818/NeteaseTSBot/issues/new/choose) + +想先体验界面?前端现已提供可部署到 GitHub Pages 的[交互演示模式](docs/DEMO.md),无需后端即可体验模拟搜索、队列和歌词。 + +![TSBot Web 控制台预览](docs/1.png) + +## 先把机器人跑起来 + +需要一台 Linux 主机、Docker Compose 和可连接的 TeamSpeak 3 / 6 服务器。已有部署升级请保留原来的 `tsbot.env`、数据库和加密密钥。 + +```bash +git clone https://github.com/yichen11818/NeteaseTSBot.git +cd NeteaseTSBot +cp tsbot.env.example tsbot.env +``` + +在 `tsbot.env` 中把 `TSBOT_COOKIE_KEY` 改为自己的随机密钥(可用 `openssl rand -hex 32` 生成),然后使用预构建镜像启动: + +```bash +docker compose -f docker-compose.prebuilt.yml up -d +docker compose -f docker-compose.prebuilt.yml ps +``` + +1. 打开 `http://<服务器地址>:8080`。 +2. 在服务器的 `logs/initial-admin-password.txt` 读取初始密码,以 `admin` 登录并修改密码。 +3. 在“系统配置”填写 TeamSpeak 地址、频道和音乐源配置,点击“应用配置”,确认机器人进入频道。 + 首次登录改密后也可跟随“部署向导”完成配置,使用“应用并检查”分别检查 Voice、TeamSpeak 和音乐接口。 +4. 在 Web 搜索并加入一首歌曲,或在 TeamSpeak 发送 `帮助` 查看聊天指令。 + +网易云功能需要自行部署 `NeteaseCloudMusicApi`;QQ 音乐和 B 站适配已内建。部分播放和账号功能需要对应平台登录态,实际可播放内容受账号权限及平台限制影响。 + +遇到问题先查看 `docker compose -f docker-compose.prebuilt.yml logs --tail=100 backend voice-service`。更多选项见 [源码安装](#快速开始推荐)、[Docker 部署](#docker-部署) 和 [运行指南](HOWTOSTART.md)。 + +## 一起完善 TSBot + +欢迎贡献部署经验、问题复现、文档、翻译和代码。第一次参与可以从 [贡献指南中的小任务](CONTRIBUTING.md#适合第一次贡献的任务--starter-tasks) 开始,无需先弄懂 Python、Vue 和 Rust 全部模块。 + +如果 TSBot 帮到了你,欢迎 Star;想收到新版本通知,可使用 GitHub **Watch → Custom → Releases**。 + +## 功能一览 TSBot 是一个基于 TeamSpeak 的音乐机器人;`voice-service` 主客户端连接已支持 TS6(历史环境变量名仍保留 `TSBOT_TS3_*`),提供: @@ -30,11 +60,16 @@ TSBot 是一个基于 TeamSpeak 的音乐机器人;`voice-service` 主客户 - **B 站视频搜索与音频播放**(支持搜索视频、展示简介/点赞/收藏/投币,并在播放时缓存音频;有字幕的视频可在歌词页显示字幕时间轴,支持通过管理员登录态补抓 AI 字幕) - **Web 控制台**(Vue3 前端,用于搜索/队列/最近播放/本地收藏/歌词,以及受管理员密码保护的完整运行配置) - **外部集成 API**(`/external/*` 提供统一搜索、入队、状态读取、历史读取与历史重播) -![预览图](docs/1.png) -![预览图](docs/2.png) -![预览图](docs/3.png) -![预览图](docs/4.png) -![预览图](docs/5.png) + +
+查看更多界面截图 + +![TSBot 界面预览 2](docs/2.png) +![TSBot 界面预览 3](docs/3.png) +![TSBot 界面预览 4](docs/4.png) +![TSBot 界面预览 5](docs/5.png) + +
## Why TSBot(痛点与目标) @@ -490,6 +525,10 @@ B 站后台授权支持扫码登录或手动 Cookie,用于登录态接口和 A - 当前主客户端连接已支持 TS6,连接参数仍沿用 `TSBOT_TS3_*` 命名。 - 但 legacy `TSBOT_TS3_SERVERQUERY_*` 仍是旧式 ServerQuery fallback,不是 TS6 的 HTTP(S) Query。 +## 相关项目 + +- [WebSpeak-client-for-TeamSpeak](https://github.com/EchoSixHIYA/WebSpeak-client-for-TeamSpeak) + ## License See `LICENSE`. diff --git a/backend/diagnostics.py b/backend/diagnostics.py new file mode 100644 index 0000000..3e764c1 --- /dev/null +++ b/backend/diagnostics.py @@ -0,0 +1,71 @@ +"""Bounded, administrator-only deployment checks. Never return upstream errors or credentials.""" +from __future__ import annotations + +import asyncio +from typing import Any + +import httpx +from sqlalchemy import text +from sqlalchemy.orm import Session + +from .config import settings +from .runtime_config import DEFINITION_BY_KEY, get_value, settings_payload, voice_config_revision +from .voice_client import VoiceClient + + +def _check(id_: str, label: str, status: str, detail: str, action: str = "", *, group: str = "backend", required: bool = True) -> dict[str, Any]: + return {"id": id_, "label": label, "status": status, "detail": detail, "action": action, "group": group, "required": required} + + +async def _voice_checks(voice: VoiceClient, expected_revision: str) -> list[dict[str, Any]]: + try: + status = await asyncio.wait_for(voice.get_status(), timeout=3) + except Exception: + return [ + _check("voice", "Voice 服务", "error", "后端无法在 3 秒内读取语音服务状态。", "检查 Voice 服务是否运行、gRPC 地址和容器网络;Docker 部署使用 voice-service:50051。"), + _check("teamspeak", "TeamSpeak 连接", "warning", "语音服务离线,暂时无法检查 TeamSpeak 连接。", "先恢复 Voice 服务,再重新检查。", group="teamspeak"), + ] + checks = [_check("voice", "Voice 服务", "success", "后端已成功读取语音服务状态。")] + if expected_revision and getattr(status, "config_revision", "") != expected_revision: + checks.append(_check("voice_config", "配置应用", "warning", "Voice 尚未确认当前配置版本。", "稍候重试;若一直未更新,检查共享配置文件挂载和 Voice 重启日志。")) + connected = getattr(status, "teamspeak_connected", None) + if connected is True: + checks.append(_check("teamspeak", "TeamSpeak 连接", "success", "语音服务已报告 TeamSpeak 会话连接成功。连接成功不代表已验证目标频道或实际音频播放。", group="teamspeak")) + elif connected is False: + checks.append(_check("teamspeak", "TeamSpeak 连接", "error", "Voice 正在运行,但尚未连接 TeamSpeak。", "检查服务器地址、语音 UDP 端口、服务器及频道密码、身份权限与防火墙,再查看 Voice 日志。", group="teamspeak")) + else: + checks.append(_check("teamspeak", "TeamSpeak 连接", "warning", "当前 Voice 版本不提供 TeamSpeak 连接状态。", "更新 Voice 服务,或在 TeamSpeak 客户端确认机器人是否已进入目标频道。", group="teamspeak")) + return checks + + +async def _netease_check(base_url: str) -> dict[str, Any]: + try: + async with httpx.AsyncClient(timeout=4, follow_redirects=False, trust_env=False) as client: + response = await client.get(f"{base_url.rstrip('/')}/search/default") + response.raise_for_status() + data = response.json() + # A reachable HTML page or an arbitrary JSON service is not a music API. + if not isinstance(data, dict) or data.get("code") != 200 or not isinstance(data.get("data"), dict): + raise ValueError("unexpected API response") + return _check("netease", "网易云 API(可选)", "success", "音乐 API 返回了有效响应;此检查不验证账号权限或歌曲可播放性。", group="music", required=False) + except Exception: + return _check("netease", "网易云 API(可选)", "warning", "音乐 API 不可访问或返回格式不符合预期。QQ 音乐和 B 站不依赖它。", "使用网易云时请检查服务地址和部署状态;容器内的 127.0.0.1 指向该容器自身。", group="music", required=False) + + +async def run_diagnostics(session: Session, voice: VoiceClient) -> dict[str, Any]: + checks = [] + try: + session.execute(text("SELECT 1")) + checks.append(_check("backend", "后端与数据库", "success", "管理员会话和数据库读取正常。")) + pending = settings_payload(session)["apply_pending"] + netease_url = str(get_value(session, DEFINITION_BY_KEY["backend.netease_api_base"])) + except Exception: + return {"ready": False, "checks": [_check("backend", "后端配置与数据库", "error", "无法读取数据库或运行配置。", "检查数据目录权限、数据库状态,以及升级后是否保留原加密密钥。") ]} + if pending: + checks.append(_check("pending", "配置应用", "warning", "有已保存但尚未应用的配置。", "点击“应用并检查”后重新检查连接。")) + if not settings.cookie_key or settings.cookie_key in {"dev-cookie-key", "replace_with_a_long_random_key"}: + checks.append(_check("encryption", "启动加密密钥", "warning", "仍在使用默认加密密钥。", "新安装请在 tsbot.env 设置自己的 TSBOT_COOKIE_KEY;已有加密数据不要直接更换密钥。", required=False)) + voice_checks, music_check = await asyncio.gather(_voice_checks(voice, voice_config_revision()), _netease_check(netease_url)) + checks.extend(voice_checks) + checks.append(music_check) + return {"ready": all(item["status"] == "success" for item in checks if item["required"]), "checks": checks} diff --git a/backend/main.py b/backend/main.py index 6fceab2..f3ea996 100644 --- a/backend/main.py +++ b/backend/main.py @@ -22,6 +22,7 @@ from sqlalchemy.orm import Session from .bilibili_cache import prune_audio_cache +from .diagnostics import run_diagnostics from .bilibili_auth import ( close_all_bilibili_qr_sessions, cookie_string_to_dict, @@ -2386,6 +2387,7 @@ async def voice_status() -> dict: "repeat_mode": _repeat_mode, "voice_connected": voice_connected, "voice_config_revision": st.config_revision, + "teamspeak_connected": getattr(st, "teamspeak_connected", None), } @@ -4279,6 +4281,12 @@ def admin_settings(request: Request, session: Session = Depends(get_session)) -> return settings_payload(session) +@app.get("/admin/diagnostics") +async def admin_diagnostics(request: Request, session: Session = Depends(get_session)) -> dict: + require_admin(request, session) + return await run_diagnostics(session, voice) + + @app.put("/admin/settings") async def admin_update_settings( req: SettingsUpdateRequest, diff --git a/backend/voice_client.py b/backend/voice_client.py index a58ac77..9210f05 100644 --- a/backend/voice_client.py +++ b/backend/voice_client.py @@ -19,6 +19,7 @@ class VoiceStatus: now_playing_source_url: str volume_percent: int config_revision: str = "" + teamspeak_connected: bool | None = None @dataclass @@ -127,6 +128,10 @@ async def get_status(self) -> VoiceStatus: now_playing_source_url=resp.now_playing_source_url, volume_percent=resp.volume_percent, config_revision=str(getattr(resp, "config_revision", "") or ""), + teamspeak_connected=( + bool(resp.teamspeak_connected) + if resp.HasField("teamspeak_connected") else None + ), ) async def set_audio_fx( diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 0000000..74d1d6d --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,57 @@ +# GitHub Pages 在线体验 + +前端可以静态部署到 GitHub Pages。Pages 不运行 Python 后端、Rust Voice 或 TeamSpeak 连接;这里发布的是同一套 Vue 界面的交互演示。 + +## 本地运行 + +在项目根目录执行: + +```bash +npm --prefix web ci +npm --prefix web run build:demo +npm --prefix web run preview:demo +``` + +访问终端显示的地址,并保留 `/NeteaseTSBot/` 子目录。演示默认使用哈希路由,例如 `/NeteaseTSBot/#/queue`,直接打开或刷新子页面都不需要服务器路由重写。 + +演示支持三平台模拟搜索、添加/移除/重排队列、模拟播放暂停和进度、歌词、示例歌单、历史记录、本地收藏和部署向导。队列与配置只保存在当前页面内存中,刷新即重置;收藏、搜索历史和主题使用浏览器本地存储。 + +界面明确标注模拟数据,不播放真实音频,不连接音乐平台或 TeamSpeak。演示构建中的 API 只调用本地适配器;未实现的接口会明确报错,不会回退到真实请求。平台登录界面不展示,也不接收 Cookie、密码或上传文件。 + +## 发布到 GitHub Pages + +1. 合入 `.github/workflows/pages.yml` 与前端改动。 +2. 在 GitHub 仓库 **Settings → Pages → Build and deployment → Source** 选择 **GitHub Actions**。 +3. 在 **Actions → Demo on GitHub Pages → Run workflow** 运行,或在 `main` 上推送前端变更。 +4. 工作流通过测试、类型检查和演示构建后发布 `web/dist-demo`。部署成功时,以 Actions 返回的 `page_url` 为准,再将地址加入 README 和仓库 About。 + +该仓库的标准项目站点预期地址为 `https://yichen11818.github.io/NeteaseTSBot/`;写入本文档不代表站点已经发布。Fork 的工作流会根据仓库名设置子目录;如果使用用户根站点或自定义域名,需要将工作流中的 `VITE_BASE_PATH` 改为 `/`。 + +## 真实部署保持独立 + +`npm --prefix web run build` 仍输出 `web/dist`,使用真实 `/api` 和 history 路由;现有 Docker/Nginx 路径不变。`build:demo` 单独输出 `web/dist-demo`,避免将演示产物混入生产容器。 + +`VITE_DEMO_MODE` 是构建时开关,不能用 URL 参数打开或关闭。不要给演示构建配置真实 API Token、Cookie 或平台凭据;公开前端构建不应包含这些信息。 + +## 部署向导与连接诊断 + +真实部署首次登录改密后进入 `/setup`,已有管理员可以从导航或系统配置进入。前两步只保存 TeamSpeak 与网易云 API 配置,最后一步应用所有已保存的配置,等待 Voice 确认配置版本后检查连接。 + +`GET /admin/diagnostics` 仅管理员会话可访问,分别检查数据库、待应用配置、Voice gRPC、TeamSpeak 会话和可选的网易云 API。Voice 检查最多等待 3 秒;音乐 API 使用 4 秒 HTTP 超时,两者并发执行。错误响应不会包含上游异常文本、地址中的凭据或 Cookie。 + +TeamSpeak 状态由 Voice 通过新增的可选 protobuf 字段报告:旧版 Voice 会显示“无法确认”,不会把 gRPC 可达当作 TeamSpeak 已连接。连接成功也不保证已进入预期频道或歌曲可播放,最后仍需点播一首具备账号权限的曲目验证。 + +QQ 音乐和 B 站不依赖网易云 API;暂不使用网易云时,该项警告不会阻止必要连接显示就绪。 + +## 验证命令 + +```bash +npm --prefix web run typecheck +npm --prefix web test +npm --prefix web run build +npm --prefix web run build:demo +python -m unittest discover -s tests -p 'test_*.py' +cargo check --manifest-path voice-service/Cargo.toml +``` + +后端命令使用已安装 `backend/requirements.txt` 的 Python 环境。PR 的 `Pull request checks` 工作流自动执行后端回归测试、前端检查与两种构建,以及 Voice 协议构建检查;无需访问真实平台账号。 diff --git a/proto/voice.proto b/proto/voice.proto index cbe55c3..6ad7a48 100644 --- a/proto/voice.proto +++ b/proto/voice.proto @@ -75,6 +75,8 @@ message StatusResponse { string now_playing_source_url = 3; int32 volume_percent = 4; string config_revision = 5; + // Optional so backends can distinguish older services from an offline connection. + optional bool teamspeak_connected = 6; } message SetAudioFxRequest { diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 0000000..6fbef8c --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,65 @@ +import json +from types import SimpleNamespace +import unittest +from unittest.mock import AsyncMock, Mock, patch + +from fastapi import HTTPException +import httpx + +from backend import diagnostics, main + + +class DeploymentDiagnosticsTests(unittest.IsolatedAsyncioTestCase): + async def test_offline_voice_is_not_reported_as_a_teamspeak_connection(self): + voice = SimpleNamespace(get_status=AsyncMock(side_effect=RuntimeError("password=private"))) + checks = await diagnostics._voice_checks(voice, "") + self.assertEqual("error", checks[0]["status"]) + self.assertEqual("warning", checks[1]["status"]) + self.assertNotIn("private", json.dumps(checks)) + + async def test_older_voice_and_unapplied_revisions_do_not_report_ready(self): + voice = SimpleNamespace(get_status=AsyncMock(return_value=SimpleNamespace(config_revision="old"))) + checks = await diagnostics._voice_checks(voice, "new") + self.assertEqual("warning", next(item for item in checks if item["id"] == "teamspeak")["status"]) + self.assertEqual("warning", next(item for item in checks if item["id"] == "voice_config")["status"]) + + async def test_report_requires_real_teamspeak_state_but_not_optional_netease(self): + voice = SimpleNamespace(get_status=AsyncMock(return_value=SimpleNamespace(config_revision="current", teamspeak_connected=True))) + with ( + patch.object(diagnostics, "settings_payload", return_value={"apply_pending": False}), + patch.object(diagnostics, "get_value", return_value="http://music.invalid"), + patch.object(diagnostics, "voice_config_revision", return_value="current"), + patch.object(diagnostics, "_netease_check", AsyncMock(return_value=diagnostics._check("netease", "Music", "warning", "Unavailable", required=False))), + ): + result = await diagnostics.run_diagnostics(Mock(), voice) + self.assertTrue(result["ready"]) + voice.get_status.return_value.teamspeak_connected = False + result = await diagnostics.run_diagnostics(Mock(), voice) + self.assertFalse(result["ready"]) + + async def test_music_check_rejects_html_arbitrary_json_and_redirects(self): + for status_code, data in [(200, {}), (200, {"code": 401, "data": {}}), (302, {"code": 200, "data": {}})]: + client = httpx.AsyncClient(transport=httpx.MockTransport(lambda request: httpx.Response(status_code, json=data))) + with patch.object(diagnostics.httpx, "AsyncClient", return_value=client): + result = await diagnostics._netease_check("http://user:private@music.invalid") + self.assertEqual("warning", result["status"]) + self.assertNotIn("private", json.dumps(result)) + + async def test_music_check_accepts_expected_api_response(self): + client = httpx.AsyncClient(transport=httpx.MockTransport(lambda request: httpx.Response(200, json={"code": 200, "data": {"showKeyword": "music"}}))) + with patch.object(diagnostics.httpx, "AsyncClient", return_value=client): + result = await diagnostics._netease_check("http://music.invalid") + self.assertEqual("success", result["status"]) + + async def test_diagnostics_requires_admin_before_probing_services(self): + with ( + patch.object(main, "require_admin", side_effect=HTTPException(status_code=401)), + patch.object(main, "run_diagnostics", AsyncMock()) as probe, + ): + with self.assertRaises(HTTPException): + await main.admin_diagnostics(Mock(), Mock()) + probe.assert_not_awaited() + + +if __name__ == "__main__": + unittest.main() diff --git a/voice-service/src/main.rs b/voice-service/src/main.rs index 7451538..0ad0efa 100644 --- a/voice-service/src/main.rs +++ b/voice-service/src/main.rs @@ -40,6 +40,7 @@ use voicev1::voice_service_server::{VoiceService, VoiceServiceServer}; static DIRECT_DESCRIPTION_UPDATE_WARNED: AtomicBool = AtomicBool::new(false); static CONFIG_RESTART_REQUESTED: AtomicBool = AtomicBool::new(false); +static TEAMSPEAK_CONNECTED: AtomicBool = AtomicBool::new(false); #[derive(Default)] struct SharedStatus { @@ -548,6 +549,7 @@ impl VoiceService for VoiceServiceImpl { now_playing_source_url: st.now_playing_source_url.clone(), volume_percent: st.volume_percent, config_revision: get_env("TSBOT_VOICE_CONFIG_REVISION", ""), + teamspeak_connected: Some(TEAMSPEAK_CONNECTED.load(Ordering::SeqCst)), })) } @@ -1093,6 +1095,7 @@ async fn ts3_actor( let max_backoff = Duration::from_secs(60); 'outer: loop { + TEAMSPEAK_CONNECTED.store(false, Ordering::SeqCst); if shutdown_token.is_cancelled() { break; } @@ -1178,6 +1181,7 @@ async fn ts3_actor( Some(Some(Ok(StreamItem::BookEvents(evts)))) => { if !logged_connected { logged_connected = true; + TEAMSPEAK_CONNECTED.store(true, Ordering::SeqCst); emit_log(&events_tx, 2, "ts3 connected"); if !avatar_set_done { @@ -1455,6 +1459,7 @@ async fn ts3_actor( ); } + TEAMSPEAK_CONNECTED.store(false, Ordering::SeqCst); if let Err(e) = con.disconnect(DisconnectOptions::new()) { emit_log(&events_tx, 3, format!("ts3 disconnect failed: {e}")); } @@ -1961,6 +1966,7 @@ async fn main() -> Result<()> { if let Err(e) = ts3_actor(ts3_audio_rx, ts3_notice_rx, ts3_cmd_rx, events_tx_clone, shutdown_token_clone).await { error!(%e, "ts3 actor exited"); } + TEAMSPEAK_CONNECTED.store(false, Ordering::SeqCst); }) }; diff --git a/web/.env.demo b/web/.env.demo new file mode 100644 index 0000000..b8ba3bc --- /dev/null +++ b/web/.env.demo @@ -0,0 +1,3 @@ +VITE_DEMO_MODE=true +VITE_BASE_PATH=/NeteaseTSBot/ +VITE_WEB_APP_NAME=TSBot 在线体验 diff --git a/web/index.html b/web/index.html index 5087da6..cd9d986 100644 --- a/web/index.html +++ b/web/index.html @@ -1,9 +1,10 @@ - + + TSBot Music diff --git a/web/package-lock.json b/web/package-lock.json index 21d005e..b224e82 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,7 +20,9 @@ "postcss": "^8.4.32", "tailwindcss": "^3.3.6", "typescript": "^5.3.0", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vitest": "^3.2.7", + "vue-tsc": "^2.2.12" } }, "node_modules/@alloc/quick-lru": { @@ -877,6 +879,24 @@ "vue": "^2.7.0 || ^3.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -902,6 +922,160 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.26", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz", @@ -948,11 +1122,47 @@ "@vue/shared": "3.5.26" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.6.4", "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@vue/reactivity": { "version": "3.5.26", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.26.tgz", @@ -998,6 +1208,13 @@ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz", "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==" }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", @@ -1023,6 +1240,16 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/autoprefixer": { "version": "10.4.23", "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.23.tgz", @@ -1059,6 +1286,13 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.14", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", @@ -1080,6 +1314,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", @@ -1126,6 +1370,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1155,6 +1409,33 @@ } ] }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", @@ -1217,6 +1498,41 @@ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1246,6 +1562,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", @@ -1298,6 +1621,16 @@ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1407,6 +1740,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1474,6 +1817,13 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1492,6 +1842,13 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lucide-vue-next": { "version": "0.294.0", "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.294.0.tgz", @@ -1530,6 +1887,36 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", @@ -1591,12 +1978,36 @@ "node": ">= 6" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -1926,6 +2337,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1934,6 +2352,33 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", @@ -2026,6 +2471,20 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2072,6 +2531,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2200,6 +2689,122 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.26", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz", @@ -2247,6 +2852,40 @@ "peerDependencies": { "vue": "^3.5.0" } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/web/package.json b/web/package.json index 6022dd9..994f048 100644 --- a/web/package.json +++ b/web/package.json @@ -6,21 +6,27 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "build:demo": "vite build --mode demo", + "preview": "vite preview", + "preview:demo": "vite preview --mode demo", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" }, "dependencies": { - "vue": "^3.4.0", - "vue-router": "^4.3.0", "@headlessui/vue": "^1.7.16", "lucide-vue-next": "^0.294.0", - "vue-draggable-plus": "^0.3.5" + "vue": "^3.4.0", + "vue-draggable-plus": "^0.3.5", + "vue-router": "^4.3.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", "typescript": "^5.3.0", "vite": "^5.0.0", - "tailwindcss": "^3.3.6", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.32" + "vitest": "^3.2.7", + "vue-tsc": "^2.2.12" } } diff --git a/web/public/demo-cover.svg b/web/public/demo-cover.svg new file mode 100644 index 0000000..b9e67e8 --- /dev/null +++ b/web/public/demo-cover.svg @@ -0,0 +1,8 @@ + + + + + + TSBot Sessions + INTERACTIVE DEMO + diff --git a/web/src/App.vue b/web/src/App.vue index 2fefe61..07f6062 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,9 +1,11 @@