diff --git a/.gitignore b/.gitignore index aa0926a..7311663 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,12 @@ node_modules/ dist/ .env *.log + +# Internal development docs +docs/ + +# Playwright persistent profile (login session cache) +.chromium-profile/ + +# SQLite runtime database +data/ diff --git a/README.md b/README.md index 0e84503..8babccd 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,100 @@ # ๐Ÿ•Š๏ธ RedNoteDanmuBot -ๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅผนๅน•ๆœบๅ™จไบบ โ€” ้€š่ฟ‡ Web ๆŽงๅˆถ้ขๆฟ่‡ชๅŠจๅ‘้€ๅผนๅน•ใ€‚ +ๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅผนๅน•ๆœบๅ™จไบบ โ€” Web ๆŽงๅˆถ้ขๆฟ่‡ชๅŠจๅ‘้€ๅผนๅน•๏ผŒๆ”ฏๆŒ Chrome / Edge / Safariใ€‚ -## ๅŠŸ่ƒฝ +## ๅŠŸ่ƒฝไธ€่งˆ -- **Web ๆŽงๅˆถ้ขๆฟ**๏ผšๆต่งˆๅ™จๆ“ไฝœ็•Œ้ข๏ผŒๅฎžๆ—ถๆŽงๅˆถ -- **่‡ชๅŠจๅ‘ๅผนๅน•**๏ผš็”จๆˆทๆŒ‡ๅฎšๅ‰็ผ€ + ้šๆœบๅญ—็ฌฆไธฒ๏ผˆ้˜ฒๅžๅผนๅน•๏ผ‰ -- **้ข‘็އๆŽงๅˆถ**๏ผšๅฏ่ฐƒๅ‘้€้—ด้š” + ้šๆœบๆŠ–ๅŠจ๏ผˆ้˜ฒ้ฃŽๆŽง๏ผ‰ -- **้šๆœบๅญ—็ฌฆไธฒ**๏ผšๅญ—ๆฏใ€็ฌฆๅทใ€่กจๆƒ…ๆททๆญ๏ผŒไธๅซๆ•ฐๅญ—๏ผŒ็”จๆˆทๅฏ่‡ชๅฎšไน‰ๅญ—็ฌฆ้›† -- **ๅฎžๆ—ถๆ—ฅๅฟ—**๏ผšๅ‘้€็Šถๆ€ใ€่ฎกๆ•ฐใ€้”™่ฏฏไฟกๆฏไธ€็›ฎไบ†็„ถ +| ๅŠŸ่ƒฝ | ่ฏดๆ˜Ž | +|------|------| +| ๐ŸŽฎ **ไธ‰้ขๆฟๆŽงๅˆถ** | ๆŽงๅˆถ / ๅ…ณ้”ฎ่ฏ / ๆ•ฐๆฎ๏ผŒๆ ‡็ญพ้กตๅˆ‡ๆข | +| ๐Ÿง  **ไธ‰ๆจกๅผๆถˆๆฏ** | ็›ดๆŽฅ / ๅ…ณ้”ฎ่ฏ / ๅ…จ้šๆœบ ไธ‰็งๆถˆๆฏๆจกๅผ | +| ๐Ÿ“ **ๅ…ณ้”ฎ่ฏ็ฎก็†** | ๅขžๅˆ ๆ”นๅ…ณ้”ฎ่ฏ๏ผŒiOS ๅผ€ๅ…ณๅฏ็”จ/็ฆ็”จ | +| ๐Ÿ“Š **ๆ•ฐๆฎ็œ‹ๆฟ** | ๅฎžๆ—ถๆŠ˜็บฟๅ›พ + 7 ๆ—ฅๆŸฑ็Šถๅ›พ + 4 ๅผ ็ปŸ่ฎกๅก | +| ๐Ÿ’พ **่‡ชๅŠจๆŒไน…ๅŒ–** | SQLite ๅญ˜ๅ‚จๅ‘้€่ฎฐๅฝ•๏ผŒ้‡ๅฏไธไธข | +| ๐ŸŽฏ **ๆ™บ่ƒฝ้˜ฒๆฃ€ๆต‹** | ้šๆœบๆŠ–ๅŠจ ยฑ30%ใ€ๆฏๆฌกๆถˆๆฏไธๅŒใ€็œŸไบบๅŒ– | +| ๐Ÿ”„ **ๅ…้‡ๅคๆ‰ซ็ ** | ๆต่งˆๅ™จไผš่ฏๆŒไน…ๅŒ–๏ผŒไธ€ๆฌก็™ปๅฝ•้•ฟๆœŸไฝฟ็”จ | +| ๐ŸŒ **ๅคšๆต่งˆๅ™จ** | Chrome / Edge / Safari ไปปๆ„ๅˆ‡ๆข | ## ๅฟซ้€Ÿๅผ€ๅง‹ +### macOS + ```bash -# 1. ๅฎ‰่ฃ…ๅŽ็ซฏไพ่ต– +# 1. ๅ…‹้š†้กน็›ฎ +git clone https://github.com/sekiwhat/RedNoteDanmuBot.git +cd RedNoteDanmuBot + +# 2. ๅฎ‰่ฃ…ๅŽ็ซฏไพ่ต– npm install -# 2. ๅฎ‰่ฃ…ๅ‰็ซฏไพ่ต–ๅนถๆž„ๅปบ +# 3. ๅฎ‰่ฃ…ๅ‰็ซฏไพ่ต–ๅนถๆž„ๅปบ cd client && npm install && npx vite build && cd .. -# 3. ๅฏๅŠจๆœๅŠก +# 4. ๅฏๅŠจ npm run server ``` ๆ‰“ๅผ€ๆต่งˆๅ™จ่ฎฟ้—ฎ **http://localhost:3000** -## ไฝฟ็”จ่ฏดๆ˜Ž +### Windows + +```bash +# ๆญฅ้ชคๅŒไธŠ๏ผŒๅฎŒๅ…จไธ€่‡ด +npm install +cd client && npm install && npx vite build && cd .. +npm run server +``` + +### macOS ็‰นๅˆซ่ฏดๆ˜Ž + +| ๆต่งˆๅ™จ | ๆ˜ฏๅฆ้œ€่ฆ้ขๅค–ๆ“ไฝœ | +|--------|----------------| +| **Chrome** | โœ… ๅณๅผ€ๅณ็”จ๏ผˆ้œ€ๅทฒๅฎ‰่ฃ… Chrome๏ผ‰ | +| **Edge** | โœ… ๅณๅผ€ๅณ็”จ | +| **Safari / WebKit** | ่ฟ่กŒ `npx playwright install webkit`๏ผˆ้ฆ–ๆฌก๏ผ‰ | + +> ๐Ÿ’ก macOS ไธŠ Safari ๆจกๅผไฝฟ็”จ็š„ๆ˜ฏ Playwright ่‡ชๅธฆ็š„ WebKit ๅผ•ๆ“Ž๏ผˆ้ž็ณป็ปŸ Safari๏ผ‰๏ผŒ่กจ็ŽฐไธŽ Chrome ไธ€่‡ดใ€‚ + +## ไฝฟ็”จๆŒ‡ๅ— + +### ็ฌฌไธ€ๆญฅ๏ผš่ฟžๆŽฅ็›ดๆ’ญ้—ด + +``` +โ‘  ๅฏๅŠจๆœๅŠก โ†’ โ‘ก ๆ‰“ๅผ€ http://localhost:3000 +โ‘ข ้€‰ๆ‹ฉๆต่งˆๅ™จ โ†’ โ‘ฃ ่พ“ๅ…ฅ็›ดๆ’ญ้—ด URL โ†’ โ‘ค ็‚นๅ‡ปใ€Œ่ฟžๆŽฅใ€ +โ‘ฅ ๅœจ Playwright ๆ‰“ๅผ€็š„ๆต่งˆๅ™จ็ช—ๅฃไธญๆ‰ซ็ ็™ปๅฝ•ๅฐ็บขไนฆ +``` + +### ็ฌฌไบŒๆญฅ๏ผš้€‰ๆ‹ฉๆถˆๆฏๆจกๅผ + +**็›ดๆŽฅๆจกๅผ**๏ผšไป…ๅ‘้€ๅ‰็ผ€๏ผŒไธๅŠ ไปปไฝ•ๅŽ็ผ€ + +``` +"76" โ†’ "76" +"ๅคงๅฎถๅฅฝ" โ†’ "ๅคงๅฎถๅฅฝ" +``` -1. **ๅฏๅŠจๆœๅŠก**ๅŽ๏ผŒๆต่งˆๅ™จๆ‰“ๅผ€ๆŽงๅˆถ้ขๆฟ -2. ๅœจๅฐ็บขไนฆ็›ดๆ’ญ้—ด้กต้ข**ๆ‰ซ็ ็™ปๅฝ•**๏ผˆ้œ€่ฆๅ…ˆๅœจๆต่งˆๅ™จ็™ปๅฝ•ๅฐ็บขไนฆ๏ผ‰ -3. ๅคๅˆถ็›ดๆ’ญ้—ด URL ็ฒ˜่ดดๅˆฐๆŽงๅˆถ้ขๆฟ โ†’ ็‚นๅ‡ปใ€Œ่ฟžๆŽฅใ€ -4. Playwright ไผš่‡ชๅŠจๆ‰“ๅผ€ไธ€ไธชๆต่งˆๅ™จ็ช—ๅฃ่ฟ›ๅ…ฅ็›ดๆ’ญ้—ด๏ผŒๅœจ**่ฏฅ็ช—ๅฃ**ไธญๅฎŒๆˆๆ‰ซ็ ็™ปๅฝ•๏ผˆๅฆ‚ๆœ‰้œ€่ฆ๏ผ‰ -5. ่ฎพ็ฝฎๆถˆๆฏๅ‰็ผ€ใ€ๅ‘้€้—ด้š”ใ€้šๆœบไธฒ้…็ฝฎ -6. ็‚นๅ‡ปใ€Œๅผ€ๅง‹ๅ‘้€ใ€ -7. ็‚นๅ‡ปใ€Œๅœๆญขใ€็ป“ๆŸๅ‘้€ +**ๅ…จ้šๆœบๆจกๅผ**๏ผˆ้ป˜่ฎค๏ผ‰๏ผšๅ‰็ผ€ + ้šๆœบๅญ—ๆฏ/็ฌฆๅท/่กจๆƒ…๏ผˆๅฏ่ฐƒ้•ฟๅบฆๅ’Œๅญ—็ฌฆ้›†๏ผ‰ + +``` +"76" + "xK@!๐Ÿ˜Š" โ†’ "76xK@!๐Ÿ˜Š" +"76" + "Ab~#๐ŸŒน" โ†’ "76Ab~#๐ŸŒน" +``` + +**ๅ…ณ้”ฎ่ฏๆจกๅผ**๏ผšๆฏๆฌกไปŽไฝ ็š„่ฏๅบ“ไธญ้šๆœบ้€‰ไธ€ไธช๏ผŒ่ฟฝๅŠ ็ฌฆๅท + +``` +"76" + "ไธญ๏ผ" + "~!@๐Ÿ˜Š" โ†’ "76ไธญ๏ผ~!@๐Ÿ˜Š" +"76" + "ๅ†ฒๅ†ฒๅ†ฒ" + "๐Ÿ”ฅโœจ" โ†’ "76ๅ†ฒๅ†ฒๅ†ฒ๐Ÿ”ฅโœจ" +``` + +### ็ฌฌไธ‰ๆญฅ๏ผšๅผ€ๅง‹ๅ‘้€ + +``` +่ฎพ็ฝฎๅ‰็ผ€๏ผˆๅฆ‚ "76"๏ผ‰โ†’ ่ฐƒๆ•ด้—ด้š”๏ผˆๅปบ่ฎฎ 2000ms๏ผ‰ +โ†’ ็‚นๅ‡ปใ€Œๅผ€ๅง‹ๅ‘้€ใ€โ†’ ๅˆ‡ๅˆฐใ€Œๆ•ฐๆฎใ€็œ‹ๅฎžๆ—ถ็ปŸ่ฎก +โ†’ ็‚นๅ‡ปใ€Œๅœๆญขใ€็ป“ๆŸ +``` ## ้…็ฝฎ @@ -42,61 +103,66 @@ npm run server | ้…็ฝฎ้กน | ้ป˜่ฎคๅ€ผ | ่ฏดๆ˜Ž | |--------|--------|------| | `port` | 3000 | ๆœๅŠก็ซฏๅฃ | -| `defaultInterval` | 2500 | ้ป˜่ฎคๅ‘้€้—ด้š” (ms) | -| `minInterval` | 1500 | ๆœ€ๅฐๅ‘้€้—ด้š” (ms) | -| `maxInterval` | 8000 | ๆœ€ๅคงๅ‘้€้—ด้š” (ms) | -| `jitterRatio` | 0.3 | ้—ด้š”ๆŠ–ๅŠจๆฏ”ไพ‹ (ยฑ30%) | -| `headless` | false | ๆ˜ฏๅฆๆ— ๅคดๆต่งˆๅ™จๆจกๅผ | -| `randomLengthMin` | 3 | ้šๆœบไธฒๆœ€ๅฐ้•ฟๅบฆ | -| `randomLengthMax` | 6 | ้šๆœบไธฒๆœ€ๅคง้•ฟๅบฆ | +| `browser` | `chrome` | ้ป˜่ฎคๆต่งˆๅ™จ | +| `defaultInterval` | 2500 | ๅ‘้€้—ด้š” (ms) | +| `minInterval` | 1500 | ๆœ€ๅฐ้—ด้š” | +| `maxInterval` | 8000 | ๆœ€ๅคง้—ด้š” | +| `jitterRatio` | 0.3 | ๆŠ–ๅŠจๆฏ”ไพ‹ (ยฑ30%) | +| `headless` | false | ๆ— ๅคดๆจกๅผ | +| `userDataDir` | `.chromium-profile` | ไผš่ฏ็ผ“ๅญ˜็›ฎๅฝ• | + +## ๆŠ€ๆœฏๆ ˆ + +| ๅฑ‚ | ๆŠ€ๆœฏ | +|----|------| +| ๅŽ็ซฏ | Node.js + Express + WebSocket | +| ๆต่งˆๅ™จ | Playwright (Chrome / Edge / WebKit) | +| ๅ‰็ซฏ | Vue 3 + Chart.js + Vite | +| ๅญ˜ๅ‚จ | SQLite (better-sqlite3) | + +## ้กน็›ฎ็ป“ๆž„ + +``` +RedNoteDanmuBot/ +โ”œโ”€โ”€ server/ # ๅŽ็ซฏ +โ”‚ โ”œโ”€โ”€ index.js # Express + WebSocket +โ”‚ โ”œโ”€โ”€ bot.js # Playwright ๆ ธๅฟƒ +โ”‚ โ”œโ”€โ”€ messageEngine.js # ๆ™บ่ƒฝๆถˆๆฏๅผ•ๆ“Ž +โ”‚ โ”œโ”€โ”€ database.js # SQLite ๆŒไน…ๅŒ– +โ”‚ โ”œโ”€โ”€ randomizer.js # ้šๆœบๅญ—็ฌฆไธฒ +โ”‚ โ”œโ”€โ”€ config.js # ้…็ฝฎ +โ”‚ โ””โ”€โ”€ __tests__/ # 13 ไธชๆต‹่ฏ• +โ”œโ”€โ”€ client/ # ๅ‰็ซฏ +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ App.vue # ไธป้ขๆฟ +โ”‚ โ”œโ”€โ”€ components/ +โ”‚ โ”‚ โ”œโ”€โ”€ KeywordManager.vue # ๅ…ณ้”ฎ่ฏ็ฎก็† +โ”‚ โ”‚ โ””โ”€โ”€ AnalyticsPanel.vue # ๆ•ฐๆฎ็œ‹ๆฟ +โ”‚ โ””โ”€โ”€ style.css +โ”œโ”€โ”€ data/ # SQLite๏ผˆ่‡ชๅŠจๅˆ›ๅปบ๏ผ‰ +โ”œโ”€โ”€ .chromium-profile/ # ๆต่งˆๅ™จไผš่ฏ๏ผˆ่‡ชๅŠจๅˆ›ๅปบ๏ผ‰ +โ””โ”€โ”€ package.json +``` ## ๅผ€ๅ‘ ```bash -# ๅ‰็ซฏ็ƒญๆ›ดๆ–ฐๅผ€ๅ‘ +# ๅ‰็ซฏ็ƒญๆ›ดๆ–ฐ cd client && npx vite # ่ฟ่กŒๆต‹่ฏ• -npx jest server/__tests__/randomizer.test.js +npx --node-options="--experimental-vm-modules" jest # ๆž„ๅปบๅ‰็ซฏ cd client && npx vite build ``` -## ้กน็›ฎ็ป“ๆž„ - -``` -RedNoteDanmuBot/ -โ”œโ”€โ”€ server/ -โ”‚ โ”œโ”€โ”€ index.js # Express + WebSocket ๆœๅŠก -โ”‚ โ”œโ”€โ”€ bot.js # Playwright ๅผนๅน•ๅ‘้€ๆ ธๅฟƒ -โ”‚ โ”œโ”€โ”€ randomizer.js # ้šๆœบๅญ—็ฌฆไธฒ็”Ÿๆˆ -โ”‚ โ”œโ”€โ”€ config.js # ้ป˜่ฎค้…็ฝฎ -โ”‚ โ””โ”€โ”€ __tests__/ -โ”‚ โ””โ”€โ”€ randomizer.test.js -โ”œโ”€โ”€ client/ -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ”œโ”€โ”€ App.vue # ไธปๆŽงๅˆถ้ขๆฟ -โ”‚ โ”‚ โ”œโ”€โ”€ main.js -โ”‚ โ”‚ โ””โ”€โ”€ style.css -โ”‚ โ”œโ”€โ”€ index.html -โ”‚ โ””โ”€โ”€ vite.config.js -โ”œโ”€โ”€ docs/ -โ”‚ โ””โ”€โ”€ superpowers/ -โ”œโ”€โ”€ package.json -โ””โ”€โ”€ README.md -``` - -## ๆŠ€ๆœฏๆ ˆ - -| ๅฑ‚ | ๆŠ€ๆœฏ | -|----|------| -| ๅŽ็ซฏ | Node.js + Express + WebSocket | -| ๆต่งˆๅ™จ่‡ชๅŠจๅŒ– | Playwright | -| ๅ‰็ซฏ | Vue 3 + Vite | +ๆต‹่ฏ•่ฆ†็›–๏ผš`randomizer` / `database` / `messageEngine`๏ผŒๅ…ฑ 13 ไธชใ€‚ -## ๆณจๆ„ไบ‹้กน +## ๆณจๆ„ -- ่ฏทๅˆ็†ไฝฟ็”จ๏ผŒ้ฟๅ…่ฟๅๅฐ็บขไนฆ็คพๅŒบ่ง„ๅˆ™ -- ๅ‘้€้ข‘็އไธๅฎœ่ฟ‡ๅฟซ๏ผŒๅปบ่ฎฎ 2-3 ็ง’ไปฅไธŠ้—ด้š” -- ้ฆ–ๆฌกไฝฟ็”จ้œ€่ฆๅœจๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅฎŒๆˆๆ‰ซ็ ็™ปๅฝ• +- โš ๏ธ ่ฏทๅˆ็†ไฝฟ็”จ๏ผŒ้ตๅฎˆๅฐ็บขไนฆ็คพๅŒบ่ง„ๅˆ™ +- โฑ ๅ‘้€้—ด้š”ๅปบ่ฎฎ 2000ms ไปฅไธŠ๏ผŒๅคชๅฟซๅฎนๆ˜“่ขซ้ฃŽๆŽง +- ๐Ÿ”‘ ้ฆ–ๆฌกไฝฟ็”จ้œ€ๅœจ Playwright ๆ‰“ๅผ€็š„ๆต่งˆๅ™จไธญๆ‰ซ็ ็™ปๅฝ• +- ๐Ÿ’พ ็™ปๅฝ•ๆ€ไฟๅญ˜ๅœจ `.chromium-profile/`๏ผŒไธ‹ๆฌกๅ…ๆ‰ซ็  +- ๐ŸŽ macOS ็”จๆˆท้ฆ–ๆฌกไฝฟ็”จ Safari ๆจกๅผ้œ€ๅ…ˆ่ท‘ `npx playwright install webkit` diff --git a/client/index.html b/client/index.html index e8b934f..10efd93 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,7 @@ - RedNoteDanmuBot + ๐Ÿ•Š๏ธ RedNoteDanmuBot
diff --git a/client/package-lock.json b/client/package-lock.json index f6be27b..9fc5093 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "rednote-danmu-bot-client", "version": "1.0.0", "dependencies": { + "chart.js": "^4.5.1", "vue": "^3.5.0" }, "devDependencies": { @@ -509,6 +510,12 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", @@ -1019,6 +1026,18 @@ "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", diff --git a/client/package.json b/client/package.json index b6989a8..14935d2 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "chart.js": "^4.5.1", "vue": "^3.5.0" }, "devDependencies": { diff --git a/client/src/App.vue b/client/src/App.vue index 944da56..2bc3649 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,145 +1,115 @@ diff --git a/client/src/components/AnalyticsPanel.vue b/client/src/components/AnalyticsPanel.vue new file mode 100644 index 0000000..ac5153c --- /dev/null +++ b/client/src/components/AnalyticsPanel.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/client/src/components/KeywordManager.vue b/client/src/components/KeywordManager.vue new file mode 100644 index 0000000..6f84cf6 --- /dev/null +++ b/client/src/components/KeywordManager.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/client/src/style.css b/client/src/style.css index 2bd6c48..14b7e27 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1,338 +1,471 @@ -/* โ”€โ”€โ”€ Reset & base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} +/* โ”€โ”€โ”€ RedNoteDanmuBot โ€” Design System v2 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + * Style: Dashboard Grid Layout + Neutral Color Scheme + * โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -:root { - --primary: #ff2442; - --primary-hov:#d91e37; - --primary-bg: #fff5f6; - --bg: #f5f5f5; - --card-bg: #ffffff; - --text: #1a1a1a; - --text-dim: #888888; - --border: #e8e8e8; - --radius: 12px; - --shadow: 0 2px 12px rgba(0,0,0,0.06); - --green: #34c759; - --orange: #ff9500; - --red: var(--primary); - --gray: #c7c7cc; - --log-bg: #1e1e2e; - --log-text: #cdd6f4; - --log-info: #89b4fa; - --log-error: #f38ba8; -} - -html { - font-size: 15px; -} +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); +:root { + /* Accent โ€” ไป…็”จไบŽๆŒ‰้’ฎ๏ผŒไธ็”จไบŽๆ–‡ๅญ— */ + --accent: #007aff; + --accent-hover: #0056cc; + --green: #34c759; + --green-hover: #2db84e; + --red: #ff3b30; + --red-hover: #d63031; + + /* Neutrals */ + --bg: #f2f2f7; + --surface: #ffffff; + --surface-hover:#f8f8fa; + --text: #1d1d1f; + --text-secondary:#6e6e73; + --text-tertiary: #aeaeb2; + --separator: rgba(60, 60, 67, 0.08); + + /* Log */ + --log-bg: #1d1d1f; + --log-text: #f5f5f7; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + + /* Radii */ + --radius-sm: 8px; + --radius-md: 12px; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0,0,0,0.04); + --shadow-md: 0 4px 20px rgba(0,0,0,0.06); + + --font: 'Inter', -apple-system, 'SF Pro Text', 'Helvetica Neue', 'Noto Sans SC', sans-serif; + --font-mono: 'SF Mono', 'SF Pro Text', 'Fira Code', monospace; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 16px; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif; + font-family: var(--font); background: var(--bg); color: var(--text); + -webkit-font-smoothing: antialiased; min-height: 100vh; - padding: 24px; } #app { - max-width: 720px; + max-width: 960px; margin: 0 auto; + padding: var(--space-lg) var(--space-md) 60px; display: flex; flex-direction: column; - gap: 16px; + gap: var(--space-md); } /* โ”€โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.header { - text-align: center; - padding: 20px 0 8px; +.app-header { + padding: 16px 4px 4px; + display: flex; + align-items: baseline; + gap: 12px; + flex-wrap: wrap; } - -.header h1 { - font-size: 1.6rem; +.app-header h1 { + font-size: 22px; font-weight: 700; - color: var(--primary); - letter-spacing: 0.5px; + color: var(--text); + letter-spacing: -0.3px; +} +.app-header .subtitle { + font-size: 14px; + color: var(--text-tertiary); + font-weight: 400; } -.header p { - font-size: 0.85rem; - color: var(--text-dim); - margin-top: 4px; +/* โ”€โ”€โ”€ Tab Bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.tab-bar { + display: flex; + background: var(--surface); + border-radius: var(--radius-md); + padding: 3px; + box-shadow: var(--shadow-sm); + border: 0.5px solid var(--separator); + gap: 2px; +} +.tab-btn { + flex: 1; + padding: 10px 0; + border: none; + border-radius: var(--radius-sm); + background: transparent; + font-size: 14px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + font-family: var(--font); + transition: all 0.2s; + -webkit-tap-highlight-color: transparent; +} +.tab-btn:hover { color: var(--text); } +.tab-btn.active { + background: var(--accent); + color: white; + font-weight: 600; } -/* โ”€โ”€โ”€ Cards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +/* โ”€โ”€โ”€ Dashboard Grid โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.dash-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: var(--space-md); +} +.dash-col { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +@media (max-width: 860px) { + .dash-grid { grid-template-columns: 1fr 1fr; } +} +@media (max-width: 540px) { + .dash-grid { grid-template-columns: 1fr; } +} + +/* โ”€โ”€โ”€ Cards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .card { - background: var(--card-bg); - border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 20px 24px; - border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + border: 0.5px solid var(--separator); + overflow: hidden; } -.card-title { - font-size: 0.85rem; +.card-header { + padding: 14px 16px 8px; + font-size: 12px; font-weight: 600; - color: var(--text-dim); + color: var(--text-secondary); text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 14px; + letter-spacing: 0.4px; } -/* โ”€โ”€โ”€ Status indicator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.status-row { +.card-body { + padding: 0 16px 14px; +} + +.card-row { display: flex; align-items: center; + min-height: 40px; gap: 10px; - margin-bottom: 14px; + border-bottom: 0.5px solid var(--separator); + padding: 6px 0; +} +.card-row:last-child { + border-bottom: none; } +/* โ”€โ”€โ”€ Status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.status-line { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: 14px; + font-weight: 500; + padding: 4px 0; +} .status-dot { - width: 10px; - height: 10px; + width: 8px; + height: 8px; border-radius: 50%; flex-shrink: 0; - animation: pulse 2s infinite; -} - -.status-dot.disconnected { - background: var(--gray); - animation: none; } -.status-dot.connected { - background: var(--green); -} -.status-dot.running { - background: var(--orange); - animation: pulse 1s infinite; -} -.status-dot.idle { - background: var(--green); - animation: none; -} - -.status-label { - font-size: 0.9rem; - font-weight: 500; -} - -.status-label.disconnected { color: var(--gray); } -.status-label.connected { color: var(--green); } -.status-label.running { color: var(--orange); } -.status-label.idle { color: var(--green); } - +.status-dot.disconnected { background: #c7c7cc; } +.status-dot.connected { background: var(--green); } +.status-dot.running { background: #ff9500; animation: pulse 1s infinite; } +.status-dot.idle { background: var(--green); } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.5; transform: scale(1.3); } -} - -/* โ”€โ”€โ”€ Form elements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.input-group { - display: flex; - gap: 10px; - margin-bottom: 12px; + 50% { opacity: 0.5; transform: scale(1.4); } } -.input-group:last-child { - margin-bottom: 0; +.status-url { + font-size: 12px; + color: var(--text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 280px; } -.input-group label { - font-size: 0.85rem; - font-weight: 500; - color: var(--text-dim); - min-width: 70px; +/* โ”€โ”€โ”€ Inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.input-row { display: flex; align-items: center; + gap: var(--space-sm); +} +.input-label { + font-size: 13px; + color: var(--text-secondary); flex-shrink: 0; + min-width: 40px; } - -.input-group input[type="text"], -.input-group input[type="number"] { +.input-field { flex: 1; - padding: 8px 12px; - border: 1px solid var(--border); - border-radius: 8px; - font-size: 0.9rem; + border: none; + background: transparent; + font-size: 14px; + color: var(--text); + text-align: left; outline: none; - transition: border-color 0.2s; -} - -.input-group input[type="text"]:focus, -.input-group input[type="number"]:focus { - border-color: var(--primary); + padding: 4px 0; + font-family: var(--font); + min-width: 0; } - -.input-group input[type="number"] { - max-width: 100px; +.input-field.right { + text-align: right; } - -.input-group input[type="text"] { - min-width: 0; +.input-field:focus { color: var(--accent); } +.input-field::placeholder { color: var(--text-tertiary); } +.input-field.num { max-width: 70px; text-align: center; } +.input-field:disabled { opacity: 0.35; } +.input-hint { + font-size: 12px; + color: var(--text-tertiary); + flex-shrink: 0; } -/* โ”€โ”€โ”€ Checkbox row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.checkbox-row { +/* โ”€โ”€โ”€ Buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.btn-row { display: flex; - gap: 18px; - flex-wrap: wrap; + gap: var(--space-sm); + padding: 4px 0; } - -.checkbox-row label { - display: flex; +.btn { + display: inline-flex; align-items: center; + justify-content: center; gap: 6px; - font-size: 0.85rem; - cursor: pointer; - user-select: none; -} - -.checkbox-row input[type="checkbox"] { - accent-color: var(--primary); - width: 16px; - height: 16px; - cursor: pointer; -} - -/* โ”€โ”€โ”€ Buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.btn { - padding: 8px 20px; + height: 36px; + padding: 0 18px; border: none; - border-radius: 8px; - font-size: 0.9rem; + border-radius: var(--radius-sm); + font-size: 14px; font-weight: 600; + font-family: var(--font); cursor: pointer; - transition: background 0.2s, opacity 0.2s; + transition: opacity 0.15s, transform 0.1s; + -webkit-tap-highlight-color: transparent; + outline: none; white-space: nowrap; } - -.btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.btn-primary { - background: var(--primary); - color: #fff; -} -.btn-primary:hover:not(:disabled) { - background: var(--primary-hov); -} - -.btn-secondary { - background: #f0f0f0; - color: var(--text); -} -.btn-secondary:hover:not(:disabled) { - background: #e0e0e0; -} - -.btn-success { - background: var(--green); - color: #fff; -} -.btn-success:hover:not(:disabled) { - background: #2db84e; +.btn:active:not(:disabled) { transform: scale(0.97); } +.btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; } +.btn-primary { background: var(--accent); color: white; } +.btn-primary:hover:not(:disabled) { background: var(--accent-hover); } +.btn-secondary { background: #e5e5ea; color: var(--text); } +.btn-secondary:hover:not(:disabled) { background: #d9d9de; } +.btn-success { background: var(--green); color: white; } +.btn-success:hover:not(:disabled) { background: var(--green-hover); } +.btn-danger { background: var(--red); color: white; } +.btn-danger:hover:not(:disabled) { background: var(--red-hover); } + +.btn-lg { + height: 44px; + font-size: 15px; + padding: 0 28px; } -.btn-danger { - background: var(--orange); - color: #fff; -} -.btn-danger:hover:not(:disabled) { - background: #e08600; -} +.btn-block { width: 100%; } +.btn-half { flex: 1; } -.btn-group { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -/* โ”€โ”€โ”€ Count display โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.count-display { +/* โ”€โ”€โ”€ Count Display โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.count-box { text-align: center; - padding: 8px 0 16px; + padding: 8px 0; } - .count-number { - font-size: 3rem; + font-size: 48px; font-weight: 800; - color: var(--primary); + color: var(--text); line-height: 1; font-variant-numeric: tabular-nums; + letter-spacing: -1.5px; + transition: transform 0.15s; } - +.count-number.bump { transform: scale(1.08); } .count-label { - font-size: 0.8rem; - color: var(--text-dim); - margin-top: 4px; + font-size: 12px; + color: var(--text-tertiary); + margin-top: 2px; + font-weight: 500; } -/* โ”€โ”€โ”€ Log area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.log-area { - background: var(--log-bg); - border-radius: 10px; - padding: 14px 16px; - height: 260px; - overflow-y: auto; - font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 0.78rem; - line-height: 1.6; +/* โ”€โ”€โ”€ Error Banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.error-banner { + background: rgba(255, 59, 48, 0.08); + border-radius: var(--radius-sm); + padding: 8px 12px; + font-size: 13px; + color: var(--red); + display: flex; + align-items: center; + gap: var(--space-sm); } -.log-area::-webkit-scrollbar { - width: 6px; +/* โ”€โ”€โ”€ Segmented Control โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.segmented { + display: flex; + border: 0.5px solid var(--separator); + border-radius: 7px; + overflow: hidden; + margin-left: auto; } -.log-area::-webkit-scrollbar-track { +.seg-btn { + padding: 5px 14px; + border: none; background: transparent; + font-size: 13px; + font-weight: 500; + font-family: var(--font); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.15s; + -webkit-tap-highlight-color: transparent; + white-space: nowrap; } -.log-area::-webkit-scrollbar-thumb { - background: #45475a; - border-radius: 3px; +.seg-btn + .seg-btn { + border-left: 0.5px solid var(--separator); } - -.log-entry { - white-space: pre-wrap; - word-break: break-all; +.seg-btn.active { + background: var(--accent); + color: white; } - -.log-time { - color: #6c7086; - margin-right: 8px; +.seg-btn:hover:not(.active) { + background: var(--surface-hover); } -.log-level-info { - color: var(--log-info); +/* โ”€โ”€โ”€ Browser Select โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.browser-select { + appearance: none; + -webkit-appearance: none; + padding: 8px 28px 8px 10px; + border: 0.5px solid var(--separator); + border-radius: 6px; + background: var(--surface); + font-size: 13px; + font-family: var(--font); + color: var(--text); + cursor: pointer; + outline: none; + min-width: 80px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236e6e73'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; } - -.log-level-error { - color: var(--log-error); +.browser-select:disabled { + opacity: 0.35; + cursor: not-allowed; +} +.browser-select:focus { + border-color: var(--accent); } -.log-message { - color: var(--log-text); +/* โ”€โ”€โ”€ Log โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.log-area { + background: var(--log-bg); + border-radius: var(--radius-sm); + padding: 12px 14px; + height: 220px; + overflow-y: auto; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.7; } +.log-area::-webkit-scrollbar { width: 4px; } +.log-area::-webkit-scrollbar-track { background: transparent; } +.log-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; } +.log-entry { white-space: pre-wrap; word-break: break-all; } +.log-time { color: #636366; margin-right: 8px; user-select: none; } + +.log-level-info .log-tag, +.log-level-info .log-msg { color: #64d2ff; } +.log-level-error .log-tag, +.log-level-error .log-msg { color: #ff453a; } +.log-level-success .log-tag, +.log-level-success .log-msg { color: #30d158; } + +.log-msg { color: var(--log-text); } .log-empty { - color: #6c7086; + color: #636366; text-align: center; - padding-top: 100px; -} - -/* โ”€โ”€โ”€ Responsive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -@media (max-width: 500px) { - body { padding: 12px; } - .card { padding: 16px; } - .input-group { flex-direction: column; gap: 4px; } - .input-group label { min-width: auto; } - .input-group input[type="number"] { max-width: 100%; } - .btn-group { flex-direction: column; } - .btn { width: 100%; text-align: center; } + padding-top: 80px; + font-size: 13px; +} + +/* โ”€โ”€โ”€ Sent Flash โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@keyframes checkPop { + 0% { transform: scale(0); opacity: 0; } + 50% { transform: scale(1.2); opacity: 1; } + 100% { transform: scale(1); opacity: 0; } +} +.sent-flash { + position: fixed; top: 50%; left: 50%; + font-size: 40px; + pointer-events: none; + animation: checkPop 0.35s ease forwards; + z-index: 100; +} + +/* โ”€โ”€โ”€ Toggle Switch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.toggle { + position: relative; + display: inline-block; + width: 42px; + height: 26px; + cursor: pointer; + flex-shrink: 0; +} +.toggle input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} +.toggle-slider { + position: absolute; + inset: 0; + background: #e5e5ea; + border-radius: 13px; + transition: background 0.2s; +} +.toggle-slider::after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + border-radius: 10px; + background: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.15); + transition: transform 0.2s; +} +.toggle input:checked + .toggle-slider { + background: var(--green); +} +.toggle input:checked + .toggle-slider::after { + transform: translateX(16px); +} + +/* โ”€โ”€โ”€ Reduced Motion โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + .status-dot.running { animation: none; } } diff --git a/docs/superpowers/plans/2025-01-rednote-danmu-bot-plan.md b/docs/superpowers/plans/2025-01-rednote-danmu-bot-plan.md deleted file mode 100644 index 2c5b546..0000000 --- a/docs/superpowers/plans/2025-01-rednote-danmu-bot-plan.md +++ /dev/null @@ -1,953 +0,0 @@ -# RedNoteDanmuBot ๅฎžๆ–ฝ่ฎกๅˆ’ - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `sp-subagent-dev` (recommended) or execute tasks directly. - -**็›ฎๆ ‡๏ผš** ๅฎž็Žฐๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅผนๅน•ๆœบๅ™จไบบ MVP๏ผŒ้€š่ฟ‡ Web ๆŽงๅˆถ้ขๆฟๆŽงๅˆถ Playwright ่‡ชๅŠจๅ‘้€ๅผนๅน• - -**ๆžถๆž„๏ผš** Node.js + Express + WebSocket ๅŽ็ซฏ + Vue 3 ๅ‰็ซฏ๏ผŒPlaywright ๆŽงๅˆถๆต่งˆๅ™จ - -**ๆŠ€ๆœฏๆ ˆ๏ผš** Node.js, Express, ws, Playwright, Vue 3, Vite - ---- - -### ไปปๅŠก 1: ้กน็›ฎๅˆๅง‹ๅŒ– - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `package.json` -- ๅˆ›ๅปบ: `server/config.js` -- ๅˆ›ๅปบ: `.gitignore` - -- [ ] **ๆญฅ้ชค 1: ๅˆ›ๅปบ package.json** - ```json - { - "name": "rednote-danmu-bot", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "server": "node server/index.js", - "client:dev": "cd client && npx vite", - "client:build": "cd client && npx vite build", - "dev": "node server/index.js", - "test": "node --experimental-vm-modules node_modules/.bin/jest" - }, - "dependencies": { - "express": "^4.21.0", - "ws": "^8.18.0", - "playwright": "^1.49.0" - }, - "devDependencies": { - "jest": "^29.7.0" - } - } - ``` - -- [ ] **ๆญฅ้ชค 2: ๅˆ›ๅปบ .gitignore** - ``` - node_modules/ - dist/ - .env - *.log - ``` - -- [ ] **ๆญฅ้ชค 3: ๅˆ›ๅปบ server/config.js** - ```javascript - export default { - port: 3000, - defaultInterval: 2500, - minInterval: 1500, - maxInterval: 8000, - jitterRatio: 0.3, - randomLengthMin: 3, - randomLengthMax: 6, - headless: false, - retryCount: 1, - maxConsecutiveErrors: 3, - }; - ``` - -- [ ] **ๆญฅ้ชค 4: ๅฎ‰่ฃ…ไพ่ต–** - ่ฟ่กŒ: `npm install` - ้ข„ๆœŸ: ๆ— ๆŠฅ้”™๏ผŒnode_modules ็›ฎๅฝ•็”Ÿๆˆ - -- [ ] **ๆญฅ้ชค 5: ๆไบค** - ```bash - git init - git add -A - git commit -m "chore: init RedNoteDanmuBot project" - ``` - ---- - -### ไปปๅŠก 2: randomizer.js ๆจกๅ— + ๅ•ๅ…ƒๆต‹่ฏ• - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `server/randomizer.js` -- ๅˆ›ๅปบ: `server/__tests__/randomizer.test.js` - -- [ ] **ๆญฅ้ชค 1: ๅˆ›ๅปบ็›ฎๅฝ•** - ่ฟ่กŒ: `mkdir -p server/__tests__` - -- [ ] **ๆญฅ้ชค 2: ๅ†™ๅคฑ่ดฅ็š„ๆต‹่ฏ• โ€” ้ป˜่ฎค็”ŸๆˆไธๅŒ…ๅซๆ•ฐๅญ—** - ```javascript - // server/__tests__/randomizer.test.js - import { generate } from '../randomizer.js'; - - describe('randomizer', () => { - test('default generate should not contain digits', () => { - for (let i = 0; i < 100; i++) { - const result = generate(); - expect(result).not.toMatch(/[0-9]/); - } - }); - - test('generate should respect length range', () => { - for (let i = 0; i < 50; i++) { - const result = generate({ minLen: 3, maxLen: 6 }); - expect(result.length).toBeGreaterThanOrEqual(3); - expect(result.length).toBeLessThanOrEqual(6); - } - }); - - test('generate should only use allowed char sets', () => { - for (let i = 0; i < 50; i++) { - const result = generate({ useLetters: true, useSymbols: false, useEmojis: false }); - expect(result).toMatch(/^[a-zA-Z]+$/); - } - }); - - test('generate with only emojis should return emojis', () => { - for (let i = 0; i < 20; i++) { - const result = generate({ useLetters: false, useSymbols: false, useEmojis: true, minLen: 2, maxLen: 2 }); - expect(result.length).toBe(2); - } - }); - }); - ``` - -- [ ] **ๆญฅ้ชค 3: ่ฟ่กŒๆต‹่ฏ•็กฎ่ฎคๅคฑ่ดฅ** - ่ฟ่กŒ: `npx jest server/__tests__/randomizer.test.js 2>&1` - ้ข„ๆœŸ: FAIL โ€” "Cannot find module '../randomizer.js'" - -- [ ] **ๆญฅ้ชค 4: ๅ†™ randomizer.js ๅฎž็Žฐ** - ```javascript - // server/randomizer.js - const LETTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const SYMBOLS = '~!@#$%^&*_+-=:;,.?'; - const EMOJIS = ['๐Ÿ˜Š','๐Ÿ˜‚','โค๏ธ','๐ŸŒน','๐ŸŽ‰','๐Ÿ”ฅ','โœจ','๐Ÿ’ช','๐ŸŽŠ','๐ŸŽ','๐Ÿ˜˜','๐Ÿฅฐ','๐Ÿ‘','๐Ÿ’•','๐ŸŽˆ','๐ŸŒบ','๐ŸŒธ','โญ','๐ŸŒŸ','๐Ÿ’ซ']; - - function pickRandom(arr) { - return arr[Math.floor(Math.random() * arr.length)]; - } - - function buildPool(options) { - let pool = ''; - if (options.useLetters !== false) pool += LETTERS; - if (options.useSymbols !== false) pool += SYMBOLS; - if (options.useEmojis !== false) pool += EMOJIS.join(''); - return pool; - } - - export function generate(options = {}) { - const minLen = options.minLen ?? 3; - const maxLen = options.maxLen ?? 6; - const length = minLen + Math.floor(Math.random() * (maxLen - minLen + 1)); - const pool = buildPool(options); - - if (!pool) { - throw new Error('At least one character set must be enabled'); - } - - let result = ''; - for (let i = 0; i < length; i++) { - result += pickRandom(pool); - } - return result; - } - - export function applyTemplate(template, prefix, randomStr) { - return template - .replace('{{prefix}}', prefix) - .replace('{{random}}', randomStr); - } - ``` - -- [ ] **ๆญฅ้ชค 5: ๅ†้…็ฝฎไธ€ไธ‹ Jest๏ผˆๅ› ไธบ type: module๏ผ‰** - ๅœจ `package.json` ไธญ่กฅๅ……: - ```json - "jest": { - "transform": {} - } - ``` - -- [ ] **ๆญฅ้ชค 6: ่ฟ่กŒๆต‹่ฏ•็กฎ่ฎค้€š่ฟ‡** - ่ฟ่กŒ: `npx jest server/__tests__/randomizer.test.js 2>&1` - ้ข„ๆœŸ: PASS (4 tests) - -- [ ] **ๆญฅ้ชค 7: ๆไบค** - ```bash - git add server/randomizer.js server/__tests__/randomizer.test.js package.json - git commit -m "feat: add randomizer module with tests" - ``` - ---- - -### ไปปๅŠก 3: bot.js โ€” Playwright ๆ ธๅฟƒ้€ป่พ‘ - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `server/bot.js` - -**ๆณจๆ„๏ผš** ๆœฌๆจกๅ—ๆถ‰ๅŠ็œŸๅฎžๆต่งˆๅ™จๆ“ไฝœ๏ผŒๆ— ๆณ•ๅ•ๅ…ƒๆต‹่ฏ•๏ผŒ้€š่ฟ‡ๆ‰‹ๅŠจ่ฟ่กŒ้ชŒ่ฏใ€‚ - -- [ ] **ๆญฅ้ชค 1: ๅ†™ bot.js** - ```javascript - // server/bot.js - import { chromium } from 'playwright'; - import { generate, applyTemplate } from './randomizer.js'; - import config from './config.js'; - - export class DanmuBot { - constructor() { - this.browser = null; - this.page = null; - this.running = false; - this._stopRequested = false; - this.sentCount = 0; - this.onLog = null; // callback(logLine) - this.onError = null; // callback(errorMsg) - this.onCount = null; // callback(sentCount) - } - - async connect(url) { - this.browser = await chromium.launch({ headless: config.headless }); - const context = await this.browser.newContext({ - viewport: { width: 1280, height: 720 }, - }); - this.page = await context.newPage(); - await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); - - // ็ญ‰ๅพ…็›ดๆ’ญ้—ดๅŠ ่ฝฝๅฎŒๆˆ โ€” ้€š่ฟ‡ๆฃ€ๆต‹ๅธธ่ง DOM ๅ…ƒ็ด  - // ๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅธธ่ง็‰นๅพ๏ผš่พ“ๅ…ฅๆก† .input-area ๆˆ–็›ธไผผ็š„ class - try { - await this.page.waitForSelector('input, textarea, [contenteditable="true"]', { timeout: 15000 }); - } catch { - // ็”จๆˆทๅฏ่ƒฝ้œ€่ฆๆ‰ซ็ ็™ปๅฝ•๏ผŒ็ป™ๅ……่ถณๆ—ถ้—ด - } - - this._log('info', `โœ… ๅทฒ่ฟžๆŽฅๅˆฐ็›ดๆ’ญ้—ด: ${url}`); - return true; - } - - async disconnect() { - await this.stop(); - if (this.browser) { - await this.browser.close(); - this.browser = null; - this.page = null; - } - this._log('info', '๐Ÿ”Œ ๅทฒๆ–ญๅผ€่ฟžๆŽฅ'); - } - - async start(prefix, options = {}) { - if (!this.page) throw new Error('ๆœช่ฟžๆŽฅๅˆฐ็›ดๆ’ญ้—ด'); - this.running = true; - this._stopRequested = false; - this.sentCount = 0; - - const interval = options.interval ?? config.defaultInterval; - const template = options.template ?? '{{prefix}} {{random}}'; - const randomOpts = { - minLen: options.randomMinLen ?? config.randomLengthMin, - maxLen: options.randomMaxLen ?? config.randomLengthMax, - useLetters: options.useLetters, - useSymbols: options.useSymbols, - useEmojis: options.useEmojis, - }; - - while (!this._stopRequested) { - try { - const randomStr = generate(randomOpts); - const message = applyTemplate(template, prefix, randomStr); - await this._sendMessage(message); - this.sentCount++; - this._log('success', `โœ… [${this.sentCount}] ๅทฒๅ‘้€: ${message}`); - if (this.onCount) this.onCount(this.sentCount); - } catch (err) { - this._log('error', `โŒ ๅ‘้€ๅคฑ่ดฅ: ${err.message}`); - if (this.onError) this.onError(err.message); - } - - // ๅธฆๆŠ–ๅŠจ็š„็ญ‰ๅพ… - const jitter = (Math.random() * 2 - 1) * interval * config.jitterRatio; - const waitMs = Math.max(config.minInterval, interval + jitter); - await this._sleep(waitMs); - } - - this.running = false; - this._log('info', 'โน ๅทฒๅœๆญขๅ‘้€'); - } - - async stop() { - this._stopRequested = true; - this.running = false; - } - - async _sendMessage(text) { - // ๅฐ่ฏ•ๅคš็งๅฏ่ƒฝ็š„่พ“ๅ…ฅๆก†้€‰ๆ‹ฉๅ™จ๏ผˆๅฐ็บขไนฆๅฏ่ƒฝๆœ‰ๅคšไธช็‰ˆๆœฌ๏ผ‰ - const selectors = [ - 'input[placeholder*="่ฏด็‚นไป€ไนˆ"]', - 'input[placeholder*="ๅผนๅน•"]', - 'input[placeholder*="่ฏ„่ฎบ"]', - 'textarea[placeholder*="่ฏด็‚นไป€ไนˆ"]', - '[contenteditable="true"]', - '.input-area input', - '.chat-input input', - ]; - - let inputEl = null; - for (const sel of selectors) { - inputEl = await this.page.$(sel); - if (inputEl) break; - } - - if (!inputEl) { - // ๅฐ่ฏ•้€š็”จ็š„ input ๆˆ– textarea - inputEl = await this.page.$('input') || await this.page.$('textarea'); - } - - if (!inputEl) throw new Error('ๆ‰พไธๅˆฐ่พ“ๅ…ฅๆก†'); - - await inputEl.click(); - await inputEl.fill(''); - await inputEl.type(text, { delay: 50 }); - - // ๅฐ่ฏ•็‚นๅ‡ปๅ‘้€ๆŒ‰้’ฎ - const sendBtnSelectors = [ - 'button:has-text("ๅ‘้€")', - '[type="submit"]', - '.send-btn', - 'button:has-text("ๅ‘ๅธƒ")', - ]; - - let sent = false; - for (const sel of sendBtnSelectors) { - const btn = await this.page.$(sel); - if (btn) { - await btn.click(); - sent = true; - break; - } - } - - if (!sent) { - // ๆŒ‰ๅ›ž่ฝฆๅ‘้€ - await this.page.keyboard.press('Enter'); - } - } - - _sleep(ms) { - return new Promise(r => setTimeout(r, ms)); - } - - _log(level, message) { - if (this.onLog) this.onLog({ level, message, time: new Date().toISOString() }); - } - } - ``` - -- [ ] **ๆญฅ้ชค 2: ๆไบค** - ```bash - git add server/bot.js - git commit -m "feat: add Playwright bot module" - ``` - ---- - -### ไปปๅŠก 4: Server โ€” Express + WebSocket - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `server/index.js` - -- [ ] **ๆญฅ้ชค 1: ๅ†™ server/index.js** - ```javascript - // server/index.js - import express from 'express'; - import { createServer } from 'http'; - import { WebSocketServer } from 'ws'; - import path from 'path'; - import { fileURLToPath } from 'url'; - import { DanmuBot } from './bot.js'; - import config from './config.js'; - - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const app = express(); - const server = createServer(app); - - // ็”Ÿไบง็Žฏๅขƒๆ‰˜็ฎกๅ‰็ซฏ้™ๆ€ๆ–‡ไปถ - const clientDist = path.join(__dirname, '..', 'client', 'dist'); - app.use(express.static(clientDist)); - app.get('*', (req, res) => { - res.sendFile(path.join(clientDist, 'index.html')); - }); - - // WebSocket - const wss = new WebSocketServer({ server }); - const bot = new DanmuBot(); - - wss.on('connection', (ws) => { - console.log('[WS] ๅฎขๆˆท็ซฏๅทฒ่ฟžๆŽฅ'); - - // ๆŠŠ bot ็š„ๅ›ž่ฐƒ็ป‘ๅฎšๅˆฐ WebSocket ๆŽจ้€ - bot.onLog = (logEntry) => { - ws.send(JSON.stringify({ type: 'log', ...logEntry })); - }; - bot.onError = (msg) => { - ws.send(JSON.stringify({ type: 'error', message: msg })); - }; - bot.onCount = (count) => { - ws.send(JSON.stringify({ type: 'count', sent: count })); - }; - - ws.on('message', async (data) => { - try { - const msg = JSON.parse(data.toString()); - - switch (msg.type) { - case 'connect': - await bot.connect(msg.url); - ws.send(JSON.stringify({ type: 'status', state: 'connected', url: msg.url })); - break; - - case 'disconnect': - await bot.disconnect(); - ws.send(JSON.stringify({ type: 'status', state: 'disconnected' })); - break; - - case 'start': - await bot.start(msg.prefix, msg.options || {}); - ws.send(JSON.stringify({ type: 'status', state: 'idle' })); - break; - - case 'stop': - await bot.stop(); - ws.send(JSON.stringify({ type: 'status', state: 'idle' })); - break; - - case 'getStatus': - ws.send(JSON.stringify({ - type: 'status', - state: bot.running ? 'running' : (bot.page ? 'connected' : 'disconnected'), - sentCount: bot.sentCount, - })); - break; - - default: - ws.send(JSON.stringify({ type: 'error', message: `ๆœช็Ÿฅๆถˆๆฏ็ฑปๅž‹: ${msg.type}` })); - } - } catch (err) { - ws.send(JSON.stringify({ type: 'error', message: err.message })); - } - }); - - ws.on('close', () => { - console.log('[WS] ๅฎขๆˆท็ซฏๅทฒๆ–ญๅผ€'); - }); - - // ๅ‘้€ๅˆๅง‹็Šถๆ€ - ws.send(JSON.stringify({ type: 'status', state: 'disconnected', sentCount: 0 })); - }); - - server.listen(config.port, () => { - console.log(`๐Ÿ•Š๏ธ RedNoteDanmuBot ๆœๅŠกๅทฒๅฏๅŠจ: http://localhost:${config.port}`); - }); - ``` - -- [ ] **ๆญฅ้ชค 2: ๆไบค** - ```bash - git add server/index.js - git commit -m "feat: add Express + WebSocket server" - ``` - ---- - -### ไปปๅŠก 5: Vue 3 ๅ‰็ซฏ โ€” ๆŽงๅˆถ้ขๆฟ - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `client/package.json` -- ๅˆ›ๅปบ: `client/index.html` -- ๅˆ›ๅปบ: `client/vite.config.js` -- ๅˆ›ๅปบ: `client/src/main.js` -- ๅˆ›ๅปบ: `client/src/App.vue` -- ๅˆ›ๅปบ: `client/src/style.css` - -- [ ] **ๆญฅ้ชค 1: ๅˆ›ๅปบ client ็›ฎๅฝ•็ป“ๆž„** - ่ฟ่กŒ: `mkdir -p client/src/components` - -- [ ] **ๆญฅ้ชค 2: ๅ†™ client/package.json** - ```json - { - "name": "rednote-danmu-bot-client", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "vue": "^3.5.0" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.2.0", - "vite": "^6.0.0" - } - } - ``` - -- [ ] **ๆญฅ้ชค 3: ๅ†™ client/vite.config.js** - ```javascript - import { defineConfig } from 'vite'; - import vue from '@vitejs/plugin-vue'; - - export default defineConfig({ - plugins: [vue()], - server: { - proxy: { - '/ws': { - target: 'ws://localhost:3000', - ws: true, - }, - }, - }, - }); - ``` - -- [ ] **ๆญฅ้ชค 4: ๅ†™ client/index.html** - ```html - - - - - - RedNoteDanmuBot - - -
- - - - ``` - -- [ ] **ๆญฅ้ชค 5: ๅ†™ client/src/main.js** - ```javascript - import { createApp } from 'vue'; - import App from './App.vue'; - import './style.css'; - - createApp(App).mount('#app'); - ``` - -- [ ] **ๆญฅ้ชค 6: ๅ†™ client/src/style.css** - ```css - * { box-sizing: border-box; margin: 0; padding: 0; } - body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #f5f5f5; - color: #333; - min-height: 100vh; - } - #app { - max-width: 800px; - margin: 0 auto; - padding: 20px; - } - h1 { - font-size: 24px; - margin-bottom: 20px; - color: #ff2442; - display: flex; - align-items: center; - gap: 8px; - } - .card { - background: white; - border-radius: 12px; - padding: 16px 20px; - margin-bottom: 16px; - box-shadow: 0 2px 8px rgba(0,0,0,0.06); - } - .card h2 { - font-size: 16px; - margin-bottom: 12px; - color: #666; - } - input, select { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 8px; - font-size: 14px; - outline: none; - transition: border-color 0.2s; - } - input:focus { border-color: #ff2442; } - button { - padding: 8px 20px; - border: none; - border-radius: 8px; - font-size: 14px; - cursor: pointer; - transition: opacity 0.2s; - } - button:hover { opacity: 0.85; } - button:disabled { opacity: 0.5; cursor: not-allowed; } - .btn-primary { background: #ff2442; color: white; } - .btn-success { background: #07c160; color: white; } - .btn-danger { background: #fa5151; color: white; } - .btn-default { background: #f0f0f0; color: #333; } - .status-dot { - display: inline-block; - width: 10px; height: 10px; - border-radius: 50%; - margin-right: 6px; - } - .status-dot.connected { background: #07c160; } - .status-dot.disconnected { background: #ccc; } - .status-dot.running { background: #ff2442; animation: pulse 1s infinite; } - .status-dot.error { background: #fa5151; } - @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } - } - .log-area { - background: #1a1a2e; - color: #e0e0e0; - border-radius: 8px; - padding: 12px; - font-family: 'Courier New', monospace; - font-size: 13px; - max-height: 300px; - overflow-y: auto; - line-height: 1.6; - } - .log-area .success { color: #07c160; } - .log-area .error { color: #fa5151; } - .log-area .info { color: #70b8ff; } - .row { display: flex; gap: 12px; align-items: center; } - .row > * { flex: 1; } - .checkbox-group { display: flex; gap: 16px; flex-wrap: wrap; } - .checkbox-group label { display: flex; align-items: center; gap: 4px; font-size: 14px; cursor: pointer; } - .checkbox-group input[type="checkbox"] { width: auto; } - .sent-count { - font-size: 32px; - font-weight: bold; - color: #ff2442; - text-align: center; - } - ``` - -- [ ] **ๆญฅ้ชค 7: ๅ†™ App.vue โ€” ไธป้ขๆฟ๏ผˆๅฎŒๆ•ด็ป„ไปถ๏ผ‰** - ```vue - - - - - ``` - -- [ ] **ๆญฅ้ชค 8: ๅฎ‰่ฃ…ๅ‰็ซฏไพ่ต–ๅนถๆต‹่ฏ•ๆž„ๅปบ** - ่ฟ่กŒ: - ```bash - cd client && npm install && npx vite build - ``` - ้ข„ๆœŸ: client/dist/ ็›ฎๅฝ•็”Ÿๆˆ - -- [ ] **ๆญฅ้ชค 9: ๆไบค** - ```bash - git add client/ - git commit -m "feat: add Vue 3 web control panel" - ``` - ---- - -### ไปปๅŠก 6: ๅฎŒๅ–„ README + ๆœ€็ปˆ่”่ฐƒ - -**ๆ–‡ไปถ๏ผš** -- ๅˆ›ๅปบ: `README.md` - -- [ ] **ๆญฅ้ชค 1: ๅ†™ README.md** - ```markdown - # ๐Ÿ•Š๏ธ RedNoteDanmuBot - - ๅฐ็บขไนฆ็›ดๆ’ญ้—ดๅผนๅน•ๆœบๅ™จไบบ โ€” ้€š่ฟ‡ Web ๆŽงๅˆถ้ขๆฟ่‡ชๅŠจๅ‘้€ๅผนๅน•ใ€‚ - - ## ๅฟซ้€Ÿๅผ€ๅง‹ - - ```bash - # ๅฎ‰่ฃ…ไพ่ต– - npm install - cd client && npm install && cd .. - - # ๆž„ๅปบๅ‰็ซฏ - cd client && npx vite build && cd .. - - # ๅฏๅŠจๆœๅŠก - npm run server - ``` - - ๆ‰“ๅผ€ๆต่งˆๅ™จ่ฎฟ้—ฎ `http://localhost:3000` - - ## ไฝฟ็”จ่ฏดๆ˜Ž - - 1. ๅœจๆต่งˆๅ™จไธญๆ‰“ๅผ€ๅฐ็บขไนฆ็›ดๆ’ญ้—ด้กต้ข๏ผŒๆ‰ซ็ ็™ปๅฝ• - 2. ๅœจๆŽงๅˆถ้ขๆฟ่พ“ๅ…ฅ็›ดๆ’ญ้—ด URL โ†’ ็‚นๅ‡ปใ€Œ่ฟžๆŽฅใ€ - 3. ๅœจ Playwright ๆ‰“ๅผ€็š„ๆต่งˆๅ™จไธญๅฎŒๆˆๆ‰ซ็ ็™ปๅฝ• - 4. ่ฎพ็ฝฎๆถˆๆฏๅ‰็ผ€ใ€ๅ‘้€้—ด้š”ใ€้šๆœบๅญ—็ฌฆไธฒ้…็ฝฎ - 5. ็‚นๅ‡ปใ€Œๅผ€ๅง‹ๅ‘้€ใ€ - - ## ๅผ€ๅ‘ - - ```bash - # ๅ‰็ซฏๅผ€ๅ‘๏ผˆ็ƒญๆ›ดๆ–ฐ๏ผ‰ - cd client && npx vite - - # ่ฟ่กŒๆต‹่ฏ• - npx jest - ``` - - ## ้…็ฝฎ - - ่ง `server/config.js` - ``` - -- [ ] **ๆญฅ้ชค 2: ๆไบค** - ```bash - git add README.md - git commit -m "docs: add README" - ``` - -- [ ] **ๆญฅ้ชค 3: ๆœ€็ปˆ้ชŒ่ฏ** - ่ฟ่กŒ: `npm run server` - ้ข„ๆœŸ: ๆœๅŠกๅฏๅŠจๅœจ http://localhost:3000๏ผŒๆต่งˆๅ™จๆ‰“ๅผ€ๅฏ็œ‹ๅˆฐๆŽงๅˆถ้ขๆฟ - ---- - -## Spec ่ฆ†็›–ๆฃ€ๆŸฅ - -- [x] ๆŠ€ๆœฏๆžถๆž„๏ผˆNode + Express + WS + Playwright + Vue 3๏ผ‰โ†’ ไปปๅŠก 1, 4, 5 -- [x] randomizer ๆจกๅ— โ†’ ไปปๅŠก 2 -- [x] bot.js ๅผนๅน•ๅ‘้€้€ป่พ‘ โ†’ ไปปๅŠก 3 -- [x] WebSocket ้€šไฟกๅ่ฎฎ โ†’ ไปปๅŠก 4, 5 -- [x] Web ๆŽงๅˆถ้ขๆฟ UI โ†’ ไปปๅŠก 5 -- [x] ้ข‘็އๆŽงๅˆถ + ้šๆœบๆŠ–ๅŠจ โ†’ ไปปๅŠก 3 (bot.js start ๆ–นๆณ•) -- [x] ้šๆœบๅญ—็ฌฆไธฒ้…็ฝฎ โ†’ ไปปๅŠก 2, 5 -- [x] ็™ปๅฝ•ๆ€้€š่ฟ‡ Playwright ๆ‰‹ๅŠจๆ‰ซ็  โ†’ ไปปๅŠก 3 (connect ๆ–นๆณ•) diff --git a/docs/superpowers/specs/2025-01-xiaohongshu-live-bot-design.md b/docs/superpowers/specs/2025-01-xiaohongshu-live-bot-design.md deleted file mode 100644 index 7df15d6..0000000 --- a/docs/superpowers/specs/2025-01-xiaohongshu-live-bot-design.md +++ /dev/null @@ -1,202 +0,0 @@ -# ๅฐ็บขไนฆ็›ดๆ’ญๅผนๅน•ๆœบๅ™จไบบ โ€” ่ฎพ่ฎกๆ–‡ๆกฃ - -## ๆฆ‚่ฟฐ - -ไธ€ไธช้€š่ฟ‡ Web ๆŽงๅˆถ้ขๆฟๆŽงๅˆถๆต่งˆๅ™จ่‡ชๅŠจๅ‘้€็›ดๆ’ญ้—ดๅผนๅน•็š„ๅทฅๅ…ท๏ผŒ็”จไบŽๅœจๆŒ‡ๅฎšๅฐ็บขไนฆ็›ดๆ’ญ้—ดไธญไปฅๅฏๆŽง้ข‘็އๅ‘้€่‡ชๅฎšไน‰ๆถˆๆฏ๏ผŒๅธฎๅŠฉ็”จๆˆทๅœจ็›ดๆ’ญ้—ดไบ’ๅŠจ/ๆŠขไธœ่ฅฟใ€‚ - -## ๆŠ€ๆœฏๆžถๆž„ - -| ๅฑ‚็บง | ้€‰ๅž‹ | ็†็”ฑ | -|------|------|------| -| ๅŽ็ซฏ่ฟ่กŒๆ—ถ | Node.js | Playwright ๅฎ˜ๆ–น Node.js SDK๏ผŒ็”Ÿๆ€ๆˆ็†Ÿ | -| Web ๆก†ๆžถ | Express | ่ฝป้‡๏ผŒ้…ๅˆ ws ๅบ“ๅš WebSocket | -| ๅฎžๆ—ถ้€šไฟก | WebSocket (ws) | ๅŒๅ‘ๅฎžๆ—ถๆŽจ้€็Šถๆ€ + ๆŽงๅˆถๆŒ‡ไปค | -| ๆต่งˆๅ™จ่‡ชๅŠจๅŒ– | Playwright | ๆจกๆ‹Ÿ็œŸไบบๆต่งˆๅ™จ๏ผŒ็จณๅฎšๆ€งๅฅฝ | -| ๅ‰็ซฏๆก†ๆžถ | Vue 3 + Vite | ๅ›ฝๅ†…็”Ÿๆ€ๅ‹ๅฅฝ๏ผŒๆŽงๅˆถ้ขๆฟๅœบๆ™ฏ็ฎ€ๆด | -| ๅ‰็ซฏ HTTP ๅฎขๆˆท็ซฏ | fetch + WebSocket ๅŽŸ็”Ÿ API | ไธๅผ•ๅ…ฅ้ขๅค–ไพ่ต– | - -## ้กน็›ฎ็›ฎๅฝ•็ป“ๆž„ - -``` -xiaohongshu-live-bot/ -โ”œโ”€โ”€ server/ -โ”‚ โ”œโ”€โ”€ index.js # Express ๅ…ฅๅฃ + WebSocket ๆœๅŠก -โ”‚ โ”œโ”€โ”€ bot.js # Playwright ๆ ธๅฟƒ้€ป่พ‘๏ผˆๆต่งˆๅ™จ็ฎก็† + ๅผนๅน•ๅ‘้€๏ผ‰ -โ”‚ โ”œโ”€โ”€ randomizer.js # ้šๆœบๅญ—็ฌฆไธฒ็”Ÿๆˆๅ™จ -โ”‚ โ””โ”€โ”€ config.js # ้ป˜่ฎค้…็ฝฎ -โ”œโ”€โ”€ client/ -โ”‚ โ”œโ”€โ”€ index.html -โ”‚ โ”œโ”€โ”€ vite.config.js -โ”‚ โ””โ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ main.js -โ”‚ โ”œโ”€โ”€ App.vue # ไธป้ขๆฟ -โ”‚ โ”œโ”€โ”€ components/ -โ”‚ โ”‚ โ”œโ”€โ”€ ConnectionPanel.vue # ็›ดๆ’ญ้—ด่ฟžๆŽฅๅŒบ -โ”‚ โ”‚ โ”œโ”€โ”€ ControlPanel.vue # ๅผ€ๅง‹/ๅœๆญข/้ข‘็އๆŽงๅˆถ -โ”‚ โ”‚ โ”œโ”€โ”€ MessageInput.vue # ๆถˆๆฏๅ‰็ผ€ + ้šๆœบไธฒ้…็ฝฎ -โ”‚ โ”‚ โ””โ”€โ”€ StatusLog.vue # ๅฎžๆ—ถๆ—ฅๅฟ— -โ”‚ โ””โ”€โ”€ style.css -โ”œโ”€โ”€ docs/ -โ”‚ โ””โ”€โ”€ superpowers/ -โ”‚ โ””โ”€โ”€ specs/ -โ”‚ โ””โ”€โ”€ 2025-01-xiaohongshu-live-bot-design.md -โ”œโ”€โ”€ package.json -โ””โ”€โ”€ README.md -``` - -## ๆ ธๅฟƒๆจกๅ—่ฎพ่ฎก - -### 1. ่ฟžๆŽฅๆจกๅ— (bot.js) - -``` -็”จๆˆท่พ“ๅ…ฅ็›ดๆ’ญ้—ดURL - โ†’ Playwright ๅฏๅŠจๆต่งˆๅ™จ๏ผˆๆœ‰ๅคด/ๆ— ๅคดๅฏ้…๏ผ‰ - โ†’ ๆ‰“ๅผ€็›ดๆ’ญ้—ด้กต้ข - โ†’ ๆฃ€ๆต‹้กต้ขๅŠ ่ฝฝๅฎŒๆˆ๏ผˆ็ญ‰ๅพ…็‰นๅฎš DOM ๅ…ƒ็ด ๅ‡บ็Žฐ๏ผ‰ - โ†’ ๅ‡†ๅค‡ๅฐฑ็ปช โ†’ ้€š็Ÿฅๅ‰็ซฏ -``` - -- **ๆต่งˆๅ™จๅฎžไพ‹**๏ผšๅ…จๅฑ€ๅ•ไพ‹๏ผŒๅค็”จๅŒไธ€ไธชๆต่งˆๅ™จไธŠไธ‹ๆ–‡ -- **ๆœ‰ๅคด/ๆ— ๅคด**๏ผš้ป˜่ฎคๆœ‰ๅคด๏ผˆๅฏ่งๆต่งˆๅ™จไพฟไบŽ่ฐƒ่ฏ•๏ผ‰๏ผŒๆ”ฏๆŒๅˆ‡ๆขๆ— ๅคดๆจกๅผ -- **็™ปๅฝ•ๆ€**๏ผš็”จๆˆท้œ€่ฆๅ…ˆๅœจ Playwright ๆ‰“ๅผ€็š„ๆต่งˆๅ™จไธญๆ‰ซ็ ็™ปๅฝ•ๅฐ็บขไนฆ -- **่ฟžๆŽฅ็Šถๆ€**๏ผšdisconnected / connecting / connected / error - -### 2. ๅผนๅน•ๅ‘้€ๆจกๅ— (bot.js) - -``` -sendMessage(prefix, randomString) - โ†’ ๅฎšไฝ่พ“ๅ…ฅๆก†๏ผˆDOM ้€‰ๆ‹ฉๅ™จ๏ผ‰ - โ†’ ๆธ…็ฉบ่พ“ๅ…ฅๆก† - โ†’ ่พ“ๅ…ฅ: prefix + " " + randomString - โ†’ ็‚นๅ‡ปๅ‘้€ๆŒ‰้’ฎ๏ผˆๆˆ–ๅ›ž่ฝฆ๏ผ‰ - โ†’ ่ฎฐๅฝ•ๅ‘้€ๆ—ฅๅฟ— -``` - -**DOM ้€‰ๆ‹ฉๅ™จ็ญ–็•ฅ๏ผš** -- ่พ“ๅ…ฅๆก†๏ผš้€š่ฟ‡ๅฐ็บขไนฆ็›ดๆ’ญ้—ด้กต้ข็š„ DOM ็ป“ๆž„ๅฎšไฝ๏ผˆ้œ€่ฆๅฎžๆต‹๏ผ‰ -- ๅ‘้€ๆŒ‰้’ฎ๏ผšๅŒไธŠ -- ๆ”ฏๆŒ้€‰ๆ‹ฉๅ™จ้…็ฝฎๅŒ–๏ผŒไพฟไบŽ้กต้ขๆ”น็‰ˆๆ—ถๆ›ดๆ–ฐ - -### 3. ้šๆœบๅญ—็ฌฆไธฒ็”Ÿๆˆๅ™จ (randomizer.js) - -```javascript -// ้ป˜่ฎคๅญ—็ฌฆ้›† -const CHARS = { - letters: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', - symbols: '~!@#$%^&*_+-=:;,.?', - emojis: '๐Ÿ˜Š๐Ÿ˜‚โค๏ธ๐ŸŒน๐ŸŽ‰๐Ÿ”ฅโœจ๐Ÿ’ช๐ŸŽŠ๐ŸŽ๐Ÿ˜˜๐Ÿฅฐ๐Ÿ‘๐Ÿ’•๐ŸŽˆ๐ŸŒบ๐ŸŒธโญ๐ŸŒŸ๐Ÿ’ซ' -}; - -generate(options = {}) - // length: ้šๆœบไธฒ้•ฟๅบฆ๏ผˆ้ป˜่ฎค 3-6๏ผ‰ - // charSets: ไฝฟ็”จ็š„ๅญ—็ฌฆ้›†๏ผˆ้ป˜่ฎค letters + symbols + emojis๏ผ‰ - // excludeNumbers: true๏ผˆ็กฌ็ผ–็ ๏ผŒไธๅซๆ•ฐๅญ—๏ผ‰ - // customTemplate: ็”จๆˆท่‡ชๅฎšไน‰ๆ ผๅผ -``` - -็”จๆˆทๅฏ้…็ฝฎ้กน๏ผš -- ้šๆœบไธฒ้•ฟๅบฆ่Œƒๅ›ด๏ผˆ้ป˜่ฎค 3-6๏ผ‰ -- ๅฏ็”จ/็ฆ็”จๆŸ็ฑปๅญ—็ฌฆ๏ผˆๅญ—ๆฏ/็ฌฆๅท/่กจๆƒ…๏ผ‰ -- ่‡ชๅฎšไน‰ๆจกๆฟ๏ผˆๅฆ‚ `{{prefix}} {{random}} ~`๏ผ‰ - -### 4. ๆŽงๅˆถ้€ป่พ‘ - -**ๅ‘้€ๆจกๅผ๏ผšๅพช็Žฏๅ‘้€** -``` -็”จๆˆท็‚นๅ‡ปใ€Œๅผ€ๅง‹ใ€ - โ†’ ่ฏปๅ–้—ด้š”้…็ฝฎ๏ผˆ้ป˜่ฎค 2500ms๏ผŒๅธฆ ยฑ800ms ้šๆœบๆŠ–ๅŠจ๏ผ‰ - โ†’ ่ฟ›ๅ…ฅๅพช็Žฏ๏ผš - 1. ็”Ÿๆˆ้šๆœบๅญ—็ฌฆไธฒ - 2. ๅ‘้€ๆถˆๆฏ๏ผˆprefix + " " + randomString๏ผ‰ - 3. ็ญ‰ๅพ… interval + randomJitter - 4. ๆฃ€ๆŸฅๆ˜ฏๅฆๆ”ถๅˆฐใ€Œๅœๆญขใ€ไฟกๅท - โ†’ ็”จๆˆท็‚นๅ‡ปใ€Œๅœๆญขใ€โ†’ ้€€ๅ‡บๅพช็Žฏ -``` - -**้ข‘็އไฟๆŠค๏ผš** -- ๅŸบ็ก€้—ด้š”๏ผš็”จๆˆทๅฏ้…็ฝฎ๏ผˆๅปบ่ฎฎ่Œƒๅ›ด 1500-8000ms๏ผ‰ -- ้šๆœบๆŠ–ๅŠจ๏ผšยฑ30% ็š„ๅŸบ็ก€้—ด้š”๏ผˆ้˜ฒๆญขๅ›บๅฎš้—ด้š”่ขซ้ฃŽๆŽง๏ผ‰ -- ๅ‘้€่ฎกๆ•ฐ๏ผšๅฎžๆ—ถๆ˜พ็คบๅทฒๅ‘้€ๆกๆ•ฐ -- ้”™่ฏฏๅค„็†๏ผšๅ‘้€ๅคฑ่ดฅ่‡ชๅŠจ้‡่ฏ• 1 ๆฌก๏ผŒ่ฟž็ปญ 3 ๆฌกๅคฑ่ดฅๅˆ™ๆš‚ๅœ - -### 5. WebSocket ๆถˆๆฏๅ่ฎฎ - -``` -ๅฎขๆˆท็ซฏ โ†’ ๆœๅŠก็ซฏ๏ผš - { type: 'connect', url: 'https://...' } // ่ฟžๆŽฅ็›ดๆ’ญ้—ด - { type: 'disconnect' } // ๆ–ญๅผ€่ฟžๆŽฅ - { type: 'start', prefix: '76', interval: 2500 } // ๅผ€ๅง‹ๅ‘้€ - { type: 'stop' } // ๅœๆญขๅ‘้€ - { type: 'updateConfig', { ... } } // ๆ›ดๆ–ฐ้…็ฝฎ - -ๆœๅŠก็ซฏ โ†’ ๅฎขๆˆท็ซฏ๏ผš - { type: 'status', state: 'connected' } // ็Šถๆ€ๆ›ดๆ–ฐ - { type: 'log', message: '...', level: 'info' } // ๆ—ฅๅฟ— - { type: 'count', sent: 42 } // ๅ‘้€่ฎกๆ•ฐ - { type: 'error', message: '...' } // ้”™่ฏฏ -``` - -### 6. ๅ‰็ซฏ็•Œ้ขๅธƒๅฑ€ - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๅฐ็บขไนฆ็›ดๆ’ญๅผนๅน•ๆœบๅ™จไบบ โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ ็›ดๆ’ญ้—ด่ฟžๆŽฅ โ”‚ -โ”‚ [่พ“ๅ…ฅ็›ดๆ’ญ้—ด URL _________________] [่ฟžๆŽฅ] โ”‚ -โ”‚ ็Šถๆ€: โ— ๅทฒ่ฟžๆŽฅ / โ—‹ ๆœช่ฟžๆŽฅ โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ ๆถˆๆฏ่ฎพ็ฝฎ โ”‚ -โ”‚ ๆถˆๆฏๅ‰็ผ€: [76 ] โ”‚ -โ”‚ ้šๆœบไธฒ้•ฟๅบฆ: [3] ~ [6] โ”‚ -โ”‚ โ˜‘ ๅญ—ๆฏ โ˜‘ ็ฌฆๅท โ˜‘ ่กจๆƒ… โ”‚ -โ”‚ ่‡ชๅฎšไน‰ๆจกๆฟ: [{{prefix}} {{random}}] โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ ๅ‘้€ๆŽงๅˆถ [โ–ถ ๅผ€ๅง‹] [โ–  ๅœๆญข] โ”‚ -โ”‚ ้—ด้š”: [2500]ms ๅทฒๅ‘้€: 42 ๆก โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ ่ฟ่กŒๆ—ฅๅฟ— โ”‚ -โ”‚ [12:00:01] โœ… ๅทฒๅ‘้€: 76 Ab3@ ๐Ÿ˜Š โ”‚ -โ”‚ [12:00:03] โœ… ๅทฒๅ‘้€: 76 xK!9 ๐ŸŒน โ”‚ -โ”‚ [12:00:06] โŒ ๅ‘้€ๅคฑ่ดฅ๏ผŒ้‡่ฏ•ไธญ... โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## ๆต‹่ฏ•็ญ–็•ฅ - -- Playwright ๆต่งˆๅ™จๆ“ไฝœ้ƒจๅˆ†๏ผš้€š่ฟ‡ๆ‰‹ๅŠจๆต‹่ฏ•้ชŒ่ฏ๏ผˆ้œ€่ฆ็œŸๅฎžๅฐ็บขไนฆ็›ดๆ’ญ้—ด๏ผ‰ -- randomizer.js๏ผšๅ•ๅ…ƒๆต‹่ฏ•๏ผˆJest๏ผ‰๏ผŒ้ชŒ่ฏๅญ—็ฌฆ้›†ใ€้•ฟๅบฆใ€ไธๅŒ…ๅซๆ•ฐๅญ— -- WebSocket ๅ่ฎฎ๏ผš้›†ๆˆๆต‹่ฏ• -- ๅ‰็ซฏ๏ผšVue ็ป„ไปถ็š„ๆ‰‹ๅŠจๆต‹่ฏ•ไธบไธป - -## ้กน็›ฎๅˆๅง‹ๅŒ– - -```bash -npm init -y -npm install express ws playwright vue@next vite @vitejs/plugin-vue -npx playwright install chromium -``` - -## MVP ่Œƒๅ›ด - -็ฌฌไธ€ๆœŸๅฎž็Žฐ๏ผš -- [x] ๆ‰‹ๅŠจ่พ“ๅ…ฅ็›ดๆ’ญ้—ด URL ่ฟžๆŽฅ -- [x] Playwright ๆ‰“ๅผ€็›ดๆ’ญ้—ด้กต้ข -- [x] ๅ‘้€ๆถˆๆฏ๏ผˆๅ‰็ผ€ + ้šๆœบไธฒ๏ผ‰ -- [x] Web ๆŽงๅˆถ้ขๆฟ๏ผˆๅผ€ๅง‹/ๅœๆญข/็Šถๆ€/ๆ—ฅๅฟ—๏ผ‰ -- [x] ้ข‘็އๆŽงๅˆถ๏ผˆๅฏ้…็ฝฎ้—ด้š” + ้šๆœบๆŠ–ๅŠจ๏ผ‰ -- [x] ้šๆœบๅญ—็ฌฆไธฒ้…็ฝฎ๏ผˆๅญ—็ฌฆ้›†/้•ฟๅบฆ/ๆจกๆฟ๏ผ‰ -- [x] ๅ‘้€่ฎกๆ•ฐๆ˜พ็คบ - -ๅŽ็ปญ่ฟญไปฃ๏ผš -- [ ] ๅฐ็บขไนฆ่กจๆƒ…ๅบ“ๅ†…็ฝฎ -- [ ] ๆ‰ซ็ ็™ปๅฝ• + ็›ดๆ’ญๅˆ—่กจ -- [ ] ๅ่ฎฎ็›ดๅ‘๏ผˆๆ— ้œ€ๆต่งˆๅ™จ๏ผ‰ -- [ ] ๅคš็›ดๆ’ญ้—ดๅŒๆ—ถๆŽงๅˆถ -- [ ] ๅฎšๆ—ถไปปๅŠก/้ข„่ฎพๆ–นๆกˆ - -## ๅผ€ๆ”พ้—ฎ้ข˜ - -1. DOM ้€‰ๆ‹ฉๅ™จ้œ€่ฆๅœจๅฎž้™…ๅฐ็บขไนฆ็›ดๆ’ญ้—ด้กต้ข้ชŒ่ฏ -2. ็™ปๅฝ•ๆ€ไฟๆŒ็ญ–็•ฅ๏ผš้œ€่ฆ็”จๆˆทๆ‰‹ๅŠจๆ‰ซ็ ็™ปๅฝ•ไธ€ๆฌก๏ผŒๅŽ็ปญ้€š่ฟ‡ session/cookie ๆŒไน…ๅŒ–๏ผŸ -3. ๆ˜ฏๅฆ้œ€่ฆ Docker ๅฎนๅ™จๅŒ–ๆ–นไพฟ้ƒจ็ฝฒ๏ผŸ diff --git a/package-lock.json b/package-lock.json index dee317d..969eb4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "rednote-danmu-bot", "version": "1.0.0", "dependencies": { + "better-sqlite3": "^12.10.0", "express": "^4.21.0", "playwright": "^1.49.0", "ws": "^8.18.0" @@ -1282,6 +1283,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.30", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", @@ -1295,6 +1316,40 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -1387,6 +1442,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1500,6 +1579,12 @@ "node": ">=10" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1672,6 +1757,21 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -1687,6 +1787,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1716,6 +1825,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1792,6 +1910,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -1914,6 +2041,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -1994,6 +2130,12 @@ "bser": "2.1.1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2057,6 +2199,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2168,6 +2316,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2292,6 +2446,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2340,6 +2514,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3370,6 +3550,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3383,12 +3575,33 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3405,6 +3618,30 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3470,7 +3707,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3705,6 +3941,33 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -3760,6 +4023,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3816,6 +4089,30 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -3823,6 +4120,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4077,6 +4388,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -4144,6 +4500,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -4245,6 +4610,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4289,6 +4682,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4372,6 +4777,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4453,7 +4864,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 748d209..b4c4c87 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "test": "npx --node-options=\"--experimental-vm-modules\" jest" }, "dependencies": { + "better-sqlite3": "^12.10.0", "express": "^4.21.0", - "ws": "^8.18.0", - "playwright": "^1.49.0" + "playwright": "^1.49.0", + "ws": "^8.18.0" }, "devDependencies": { "jest": "^29.7.0" diff --git a/server/__tests__/database.test.js b/server/__tests__/database.test.js new file mode 100644 index 0000000..ce44bab --- /dev/null +++ b/server/__tests__/database.test.js @@ -0,0 +1,110 @@ +import { listKeywords, addKeyword, removeKeyword, toggleKeyword, logSend, getStats } from '../database.js'; + +// Helper: find a keyword by text +function findByText(list, text) { + return list.find(k => k.text === text); +} + +// Helper: find a keyword by id +function findById(list, id) { + return list.find(k => k.id === id); +} + +afterAll(() => { + // Clean up any test keywords left behind + const all = listKeywords(); + for (const kw of all) { + if (kw.text.startsWith('__test__')) { + removeKeyword(kw.id); + } + } +}); + +test('listKeywords returns default keywords (โ‰ฅ8) with id, text, enabled', () => { + const list = listKeywords(); + expect(list.length).toBeGreaterThanOrEqual(8); + for (const kw of list) { + expect(kw).toHaveProperty('id'); + expect(kw).toHaveProperty('text'); + expect(kw).toHaveProperty('enabled'); + } +}); + +test('addKeyword adds a new keyword and it appears in the list', () => { + const testText = '__test__add_' + Date.now(); + const updatedList = addKeyword(testText); + const found = findByText(updatedList, testText); + expect(found).toBeDefined(); + expect(found.text).toBe(testText); + expect(found.enabled).toBe(1); + + // Clean up + removeKeyword(found.id); +}); + +test('toggleKeyword toggles enabled status', () => { + // Add a fresh keyword to toggle + const testText = '__test__toggle_' + Date.now(); + const afterAdd = addKeyword(testText); + const kw = findByText(afterAdd, testText); + + // Initially enabled should be 1 + expect(kw.enabled).toBe(1); + + // Toggle โ†’ should become 0 + const afterToggle1 = toggleKeyword(kw.id); + const toggled1 = findById(afterToggle1, kw.id); + expect(toggled1.enabled).toBe(0); + + // Toggle again โ†’ should become 1 + const afterToggle2 = toggleKeyword(kw.id); + const toggled2 = findById(afterToggle2, kw.id); + expect(toggled2.enabled).toBe(1); + + // Clean up + removeKeyword(kw.id); +}); + +test('removeKeyword removes a keyword and it no longer appears', () => { + const testText = '__test__remove_' + Date.now(); + const afterAdd = addKeyword(testText); + const kw = findByText(afterAdd, testText); + expect(kw).toBeDefined(); + + const afterRemove = removeKeyword(kw.id); + const stillThere = findByText(afterRemove, testText); + expect(stillThere).toBeUndefined(); +}); + +test('getStats returns the correct structure', () => { + // Insert a test log entry so stats have at least some data + logSend('testPrefix', 'testKeyword', 'testPrefix testKeyword', 'success'); + + const stats = getStats(); + expect(stats).toHaveProperty('total'); + expect(typeof stats.total).toBe('number'); + expect(stats.total).toBeGreaterThanOrEqual(1); + + expect(stats).toHaveProperty('today'); + expect(typeof stats.today).toBe('number'); + + expect(stats).toHaveProperty('todaySuccess'); + expect(typeof stats.todaySuccess).toBe('number'); + + expect(stats).toHaveProperty('todayFail'); + expect(typeof stats.todayFail).toBe('number'); + + expect(stats).toHaveProperty('recentRate'); + expect(Array.isArray(stats.recentRate)).toBe(true); + if (stats.recentRate.length > 0) { + expect(stats.recentRate[0]).toHaveProperty('time'); + expect(stats.recentRate[0]).toHaveProperty('count'); + } + + expect(stats).toHaveProperty('dailyHistory'); + expect(Array.isArray(stats.dailyHistory)).toBe(true); + if (stats.dailyHistory.length > 0) { + expect(stats.dailyHistory[0]).toHaveProperty('date'); + expect(stats.dailyHistory[0]).toHaveProperty('count'); + } +}); diff --git a/server/__tests__/messageEngine.test.js b/server/__tests__/messageEngine.test.js new file mode 100644 index 0000000..4d68455 --- /dev/null +++ b/server/__tests__/messageEngine.test.js @@ -0,0 +1,27 @@ +import { buildMessage } from '../messageEngine.js'; + +const PREFIX = 'ใ€ๆต‹่ฏ•ใ€‘'; + +test('buildMessage returns message starting with prefix', () => { + const result = buildMessage(PREFIX); + expect(result.fullMessage.startsWith(PREFIX)).toBe(true); +}); + +test('buildMessage result contains the selected keyword', () => { + const result = buildMessage(PREFIX); + expect(result.fullMessage).toContain(result.keyword); +}); + +test('buildMessage appends symbols after keyword (length โ‰ฅ 2)', () => { + const result = buildMessage(PREFIX); + const symbolsPart = result.fullMessage.slice(PREFIX.length + result.keyword.length); + expect(symbolsPart.length).toBeGreaterThanOrEqual(2); +}); + +test('buildMessage random part contains no digits across 20 runs', () => { + for (let i = 0; i < 20; i++) { + const result = buildMessage(PREFIX); + const randomPart = result.fullMessage.slice(PREFIX.length); + expect(randomPart).not.toMatch(/[0-9]/); + } +}); diff --git a/server/bot.js b/server/bot.js index add7cde..5ffe57b 100644 --- a/server/bot.js +++ b/server/bot.js @@ -1,10 +1,12 @@ -import { chromium } from 'playwright'; +import { chromium, webkit } from 'playwright'; import { generate, applyTemplate } from './randomizer.js'; +import { buildMessage } from './messageEngine.js'; import config from './config.js'; class DanmuBot { constructor() { this.browser = null; + this.context = null; this.page = null; this.running = false; this._stopRequested = false; @@ -14,54 +16,45 @@ class DanmuBot { this.onCount = null; } - async connect(url) { - this.browser = await chromium.launch({ headless: config.headless }); - const context = await this.browser.newContext({ - viewport: { width: 1280, height: 720 }, - }); - this.page = await context.newPage(); - await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); + async connect(url, browserType) { + browserType = browserType || config.browser; - // Try multiple selectors to find the input box - const inputSelectors = [ - 'input[placeholder*="่ฏด็‚นไป€ไนˆ"]', - 'input[placeholder*="ๅผนๅน•"]', - 'input[placeholder*="่ฏ„่ฎบ"]', - 'textarea[placeholder*="่ฏด็‚นไป€ไนˆ"]', - '[contenteditable="true"]', - '.input-area input', - '.chat-input input', - ]; - - let found = false; - for (const selector of inputSelectors) { - try { - await this.page.waitForSelector(selector, { timeout: 5000 }); - this._log('info', `Connected โ€” input found with selector: ${selector}`); - found = true; - return; - } catch { - // Try next selector - } + const launchOptions = { + headless: config.headless, + viewport: { width: 1280, height: 720 }, + }; + + switch (browserType) { + case 'edge': + launchOptions.channel = 'msedge'; + this.context = await chromium.launchPersistentContext(config.userDataDir, launchOptions); + break; + case 'safari': + // ไฝฟ็”จ Playwright ่‡ชๅธฆ็š„ WebKit ๅผ•ๆ“Ž๏ผˆ้ž็ณป็ปŸ Safari๏ผ‰ + this.context = await webkit.launchPersistentContext(config.userDataDir, launchOptions); + break; + case 'chromium': + // Playwright ่‡ชๅธฆ็š„ Chromium๏ผˆไธ้œ€่ฆ็ณป็ปŸๅฎ‰่ฃ…๏ผ‰ + this.context = await chromium.launchPersistentContext(config.userDataDir, launchOptions); + break; + default: + // 'chrome' โ€” ไฝฟ็”จ็ณป็ปŸๅฎ‰่ฃ…็š„ Chrome + launchOptions.channel = 'chrome'; + this.context = await chromium.launchPersistentContext(config.userDataDir, launchOptions); + break; } - // Fallback to generic input or textarea - if (!found) { - try { - await this.page.waitForSelector('input', { timeout: 5000 }); - this._log('info', 'Connected โ€” input found with generic selector: input'); - } catch { - await this.page.waitForSelector('textarea', { timeout: 5000 }); - this._log('info', 'Connected โ€” input found with generic selector: textarea'); - } - } + const pages = this.context.pages(); + this.page = pages.length > 0 ? pages[0] : await this.context.newPage(); + await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); + this._log('info', `[${browserType}] ๆต่งˆๅ™จๅทฒๆ‰“ๅผ€๏ผŒ่ฏทๅœจๆต่งˆๅ™จ็ช—ๅฃไธญ็™ปๅฝ•ๅฐ็บขไนฆ`); } async disconnect() { await this.stop(); - if (this.browser) { - await this.browser.close(); - this.browser = null; + if (this.context) { + await this.context.close(); + this.context = null; this.page = null; } } @@ -72,26 +65,35 @@ class DanmuBot { this.sentCount = 0; const interval = options.interval || config.defaultInterval; - const template = options.template; + const mode = options.mode || 'random'; + const randomOptions = options.random || {}; while (!this._stopRequested) { try { - const randomStr = generate(options.random || {}); - let message; - if (template) { - message = applyTemplate(template, prefix, randomStr); + let fullMessage; + if (mode === 'direct') { + fullMessage = prefix; + } else if (mode === 'random') { + const randomStr = generate({ + minLen: randomOptions.minLen || config.randomLengthMin, + maxLen: randomOptions.maxLen || config.randomLengthMax, + useLetters: randomOptions.useLetters !== false, + useSymbols: randomOptions.useSymbols !== false, + useEmojis: randomOptions.useEmojis !== false, + }); + fullMessage = prefix + randomStr; } else { - message = prefix + ' ' + randomStr; + fullMessage = buildMessage(prefix).fullMessage; } - await this._sendMessage(message); + await this._sendMessage(fullMessage); this.sentCount++; if (this.onCount) { this.onCount(this.sentCount); } - this._log('info', `Message sent: "${message}"`); + this._log('info', `Message sent: "${fullMessage}"`); } catch (err) { if (this.onError) { this.onError(err); @@ -142,7 +144,7 @@ class DanmuBot { } if (!inputEl) { - throw new Error('Cannot find input element on the page'); + throw new Error('ๆ‰พไธๅˆฐ่พ“ๅ…ฅๆก† โ€” ่ฏท็กฎ่ฎคๅทฒๅœจๆต่งˆๅ™จไธญ็™ปๅฝ•ๅฐ็บขไนฆๅนถ่ฟ›ๅ…ฅ็›ดๆ’ญ้—ด'); } await inputEl.click(); diff --git a/server/config.js b/server/config.js index 140b616..d3fc094 100644 --- a/server/config.js +++ b/server/config.js @@ -1,6 +1,6 @@ export default { port: 3000, - defaultInterval: 2500, + defaultInterval: 3000, minInterval: 1500, maxInterval: 8000, jitterRatio: 0.3, @@ -9,4 +9,10 @@ export default { headless: false, retryCount: 1, maxConsecutiveErrors: 3, + + // ๆต่งˆๅ™จ็”จๆˆทๆ•ฐๆฎ็›ฎๅฝ•๏ผˆ็”จไบŽๆŒไน…ๅŒ–็™ปๅฝ•ๆ€๏ผŒ้ฟๅ…้‡ๅคๆ‰ซ็ ๏ผ‰ + userDataDir: '.chromium-profile', + + // ๆต่งˆๅ™จ็ฑปๅž‹: 'chrome' | 'edge' | 'chromium' | 'firefox' + browser: 'chrome', }; diff --git a/server/database.js b/server/database.js new file mode 100644 index 0000000..4905688 --- /dev/null +++ b/server/database.js @@ -0,0 +1,102 @@ +import Database from 'better-sqlite3'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_DIR = path.resolve(__dirname, '..', 'data'); +const DB_PATH = path.join(DB_DIR, 'bot.db'); + +// โ”€โ”€โ”€ Initialize โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +fs.mkdirSync(DB_DIR, { recursive: true }); + +const db = new Database(DB_PATH); +db.pragma('journal_mode = WAL'); + +db.exec(` + CREATE TABLE IF NOT EXISTS send_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prefix TEXT NOT NULL, + keyword TEXT, + full_message TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'success', + error TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) + ); + + CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL UNIQUE, + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now','localtime')) + ); +`); + +// โ”€โ”€โ”€ Default keywords โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const DEFAULT_KEYWORDS = ['ไธญ๏ผ', 'ๅ›žๅฎถ๏ผ', 'ๅ†ฒๅ†ฒๅ†ฒ', 'ๆฅไบ†ๆฅไบ†', 'ๅฏไปฅๅฏไปฅ', 'ๅคชๆฃ’ไบ†', 'ๅฏไปฅๅ•Š', 'ไธ้”™ไธ้”™']; + +const insertKeyword = db.prepare('INSERT OR IGNORE INTO keywords (text) VALUES (?)'); +for (const kw of DEFAULT_KEYWORDS) { + insertKeyword.run(kw); +} + +// โ”€โ”€โ”€ Prepared statements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const stmtListKeywords = db.prepare('SELECT id, text, enabled FROM keywords ORDER BY id'); +const stmtAddKeyword = db.prepare('INSERT INTO keywords (text) VALUES (?)'); +const stmtRemoveKeyword = db.prepare('DELETE FROM keywords WHERE id = ?'); +const stmtToggleKeyword = db.prepare('UPDATE keywords SET enabled = CASE WHEN enabled = 1 THEN 0 ELSE 1 END WHERE id = ?'); +const stmtLogSend = db.prepare('INSERT INTO send_logs (prefix, keyword, full_message, status, error) VALUES (?, ?, ?, ?, ?)'); +const stmtTotal = db.prepare('SELECT COUNT(*) as total FROM send_logs'); +const stmtToday = db.prepare("SELECT COUNT(*) as count FROM send_logs WHERE date(created_at) = date('now','localtime')"); +const stmtTodaySuccess = db.prepare("SELECT COUNT(*) as count FROM send_logs WHERE date(created_at) = date('now','localtime') AND status = 'success'"); +const stmtTodayFail = db.prepare("SELECT COUNT(*) as count FROM send_logs WHERE date(created_at) = date('now','localtime') AND status != 'success'"); +const stmtRecentRate = db.prepare(` + SELECT strftime('%H:%M', created_at) as time, COUNT(*) as count + FROM send_logs + WHERE created_at >= datetime('now','localtime','-30 minutes') + GROUP BY strftime('%H:%M', created_at) + ORDER BY time +`); +const stmtDailyHistory = db.prepare(` + SELECT date(created_at) as date, COUNT(*) as count + FROM send_logs + WHERE created_at >= datetime('now','localtime','-7 days') + GROUP BY date(created_at) + ORDER BY date +`); + +// โ”€โ”€โ”€ Exported functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export function listKeywords() { + return stmtListKeywords.all(); +} + +export function addKeyword(text) { + stmtAddKeyword.run(text); + return listKeywords(); +} + +export function removeKeyword(id) { + stmtRemoveKeyword.run(id); + return listKeywords(); +} + +export function toggleKeyword(id) { + stmtToggleKeyword.run(id); + return listKeywords(); +} + +export function logSend(prefix, keyword, fullMessage, status = 'success', error = null) { + stmtLogSend.run(prefix, keyword, fullMessage, status, error); +} + +export function getStats() { + const total = stmtTotal.get().total; + const today = stmtToday.get().count; + const todaySuccess = stmtTodaySuccess.get().count; + const todayFail = stmtTodayFail.get().count; + const recentRate = stmtRecentRate.all(); + const dailyHistory = stmtDailyHistory.all(); + + return { total, today, todaySuccess, todayFail, recentRate, dailyHistory }; +} diff --git a/server/index.js b/server/index.js index af4f2fa..7e9c869 100644 --- a/server/index.js +++ b/server/index.js @@ -75,7 +75,7 @@ wss.on('connection', (ws) => { ws.send(JSON.stringify({ type: 'status', state: 'disconnected', sentCount: 0 })); // โ”€โ”€ Handle incoming messages โ”€โ”€ - ws.on('message', (data) => { + ws.on('message', async (data) => { let parsed; try { parsed = JSON.parse(data.toString()); @@ -92,7 +92,7 @@ wss.on('connection', (ws) => { ws.send(JSON.stringify({ type: 'error', message: 'Missing url in connect message' })); return; } - bot.connect(url) + bot.connect(url, parsed.browser) .then(() => { ws.send(JSON.stringify({ type: 'status', state: 'connected', sentCount: bot.sentCount })); }) @@ -135,6 +135,40 @@ wss.on('connection', (ws) => { break; } + case 'listKeywords': { + const { listKeywords } = await import('./database.js'); + ws.send(JSON.stringify({ type: 'keywords', list: listKeywords() })); + break; + } + + case 'addKeyword': { + const { addKeyword } = await import('./database.js'); + if (!parsed.text || !parsed.text.trim()) { + ws.send(JSON.stringify({ type: 'error', message: 'ๅ…ณ้”ฎ่ฏไธ่ƒฝไธบ็ฉบ' })); + return; + } + ws.send(JSON.stringify({ type: 'keywords', list: addKeyword(parsed.text) })); + break; + } + + case 'removeKeyword': { + const { removeKeyword } = await import('./database.js'); + ws.send(JSON.stringify({ type: 'keywords', list: removeKeyword(parsed.id) })); + break; + } + + case 'toggleKeyword': { + const { toggleKeyword } = await import('./database.js'); + ws.send(JSON.stringify({ type: 'keywords', list: toggleKeyword(parsed.id) })); + break; + } + + case 'getStats': { + const { getStats } = await import('./database.js'); + ws.send(JSON.stringify({ type: 'stats', ...getStats() })); + break; + } + case 'getStatus': { let state = 'disconnected'; if (bot.page) state = 'connected'; diff --git a/server/messageEngine.js b/server/messageEngine.js new file mode 100644 index 0000000..59e2e27 --- /dev/null +++ b/server/messageEngine.js @@ -0,0 +1,32 @@ +import { listKeywords, logSend } from './database.js'; +import { generate } from './randomizer.js'; + +export function buildMessage(prefix) { + // 1. ่Žทๅ–ๆ‰€ๆœ‰ๅฏ็”จ็š„ๅ…ณ้”ฎ่ฏ + const keywords = listKeywords().filter(k => k.enabled); + + if (keywords.length === 0) { + // Fallback: ๆฒกๆœ‰ๅฏ็”จๅ…ณ้”ฎ่ฏๆ—ถ็”จ้šๆœบๅญ—็ฌฆไธฒ + const randomStr = generate({ minLen: 3, maxLen: 6 }); + const fullMessage = prefix + randomStr; + logSend(prefix, '(random)', fullMessage, 'success'); + return { fullMessage, keyword: '(random)' }; + } + + // 2. ้šๆœบ้€‰ไธ€ไธชๅ…ณ้”ฎ่ฏ + const keyword = keywords[Math.floor(Math.random() * keywords.length)].text; + + // 3. ็”Ÿๆˆ 2-5 ไธช้šๆœบ็ฌฆๅท๏ผˆsymbols + emojis๏ผŒไธๅซๆ•ฐๅญ—ๅ’Œๅญ—ๆฏ๏ผ‰ + const symbols = generate({ + useLetters: false, + useSymbols: true, + useEmojis: true, + minLen: 2, + maxLen: 5, + }); + + // 4. ็ป„่ฃ…ๆถˆๆฏ + const fullMessage = prefix + keyword + symbols; + logSend(prefix, keyword, fullMessage, 'success'); + return { fullMessage, keyword }; +}