├── .eslintrc.cjs ├── .github └── workflows │ └── version.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── apps ├── QRLogin.js ├── SplitFiction.js ├── achievement.js ├── bind.js ├── cart.js ├── charts.js ├── client.js ├── dev.js ├── discounts.js ├── expend.js ├── help.js ├── index.js ├── info.js ├── inventory.js ├── online.js ├── push.js ├── review.js ├── rollGame.js ├── search.js ├── setting.js ├── stats.js ├── wishlist.js └── yearReview.js ├── components ├── App.js ├── Config.js ├── Render.js ├── Version.js ├── YamlReader.js └── index.js ├── config └── default_config │ ├── gif.yaml │ ├── other.yaml │ ├── push.yaml │ ├── steam.yaml │ └── tips.yaml ├── guoba.support.js ├── index.js ├── jsconfig.json ├── lib ├── Bot.js ├── common.js ├── index.js ├── logger.js ├── plugin.js ├── puppeteer.js ├── redis.js └── segment.js ├── models ├── api │ ├── IAccountCartService.js │ ├── IAccountPrivateAppsService.js │ ├── IAuthenticationService.js │ ├── ICheckoutService.js │ ├── IClientCommService.js │ ├── ICommunityService.js │ ├── IFamilyGroupsService.js │ ├── IFriendsListService.js │ ├── IMobileAppService.js │ ├── IPlayerService.js │ ├── ISaleFeatureService.js │ ├── ISteamChartsService.js │ ├── ISteamUser.js │ ├── ISteamUserOAuth.js │ ├── ISteamUserStats.js │ ├── ISteamWebAPIUtil.js │ ├── IStoreBrowseService.js │ ├── IStoreQueryService.js │ ├── IStoreService.js │ ├── IStoreTopSellersService.js │ ├── IUserAccountService.js │ ├── IUserReviewsService.js │ ├── IWishlistService.js │ ├── community.js │ ├── index.js │ └── store.js ├── bind │ └── index.js ├── canvas │ ├── canvas.js │ ├── game.js │ ├── index.js │ ├── info.js │ └── inventory.js ├── db │ ├── base.js │ ├── familyInventoryPush.js │ ├── game.js │ ├── index.js │ ├── kv.js │ ├── priceChangePush.js │ ├── push.js │ ├── stats.js │ ├── token.js │ ├── user.js │ └── userInventory.js ├── help │ ├── config.js │ ├── help.js │ ├── index.js │ └── theme.js ├── index.js ├── info │ ├── gif.js │ └── index.js ├── setting │ └── index.js ├── task │ ├── familyInventory.js │ ├── index.js │ ├── play.js │ ├── priceChange.js │ ├── userInventory.js │ └── userWishlist.js └── utils │ ├── bot.js │ ├── index.js │ ├── request.js │ └── steam.js ├── package.json └── resources ├── SplitFiction ├── 三人探戈.png ├── 不详的迎接.png ├── 与神抗争.png ├── 世界分隔.png ├── 九头蛇.png ├── 你好,锤子先生.png ├── 停车场.png ├── 全新视角.png ├── 农场生活.png ├── 冰封之王.png ├── 冰封殿堂.png ├── 分头行动.png ├── 列车劫案.png ├── 勇武骑士.png ├── 囚犯.png ├── 地下世界.png ├── 坠入奇境.png ├── 塌缩之星.png ├── 处决场.png ├── 大地之母.png ├── 大逃亡.png ├── 大都市生活.png ├── 太空逃生.png ├── 宝藏之庙.png ├── 宝藏叛徒.png ├── 实用无人机.png ├── 实验室.png ├── 屠龙者.png ├── 工厂入口.png ├── 工厂外围.png ├── 工艺之庙.png ├── 巨石之怒.png ├── 废物处理.png ├── 弹珠锁.png ├── 战争坡道.png ├── 打破常规.png ├── 攀登摩天楼.png ├── 暗夜之光.png ├── 最高安全级别.png ├── 月亮市集.png ├── 机动战术.png ├── 枪械升级.png ├── 森林之心.png ├── 横截面.png ├── 毁灭性的移动树干.png ├── 毒液滚筒.png ├── 水之庙.png ├── 沙鱼传奇.png ├── 深入风暴.png ├── 温暖的问候.png ├── 潜入行动.png ├── 灵魂向导.png ├── 牢房片区.png ├── 犯罪集团首领.png ├── 生日蛋糕.png ├── 电音律动.png ├── 登山远足.png ├── 皇宫.png ├── 监狱大院.png ├── 监狱飞船.png ├── 监督者.png ├── 神威之龙.png ├── 空中亡命.png ├── 笔记本.png ├── 系统安全模式.png ├── 终极决战.png ├── 翻转都市.png ├── 自由斗士.png ├── 节目游戏.png ├── 蜿蜒小路.png ├── 蠢猴子.png ├── 补水设施.png ├── 记忆碎片.png ├── 运输船.png ├── 重力摩托.png ├── 长青领主.png ├── 霓虹街道.png ├── 面对面.png ├── 风筝.png ├── 驾车逃离.png ├── 高峰时间.png ├── 鬼镇.png ├── 龙骑士大团结.png └── 龙魂.png ├── common ├── common.css ├── font │ ├── MiSans-Bold.ttf │ ├── MiSans-Light.ttf │ └── MiSans-Normal.ttf └── layout │ └── default.html ├── game ├── friend_add.png ├── friend_bg.png ├── game.css ├── game.html └── game.png ├── help ├── help.jpg ├── icon.png ├── index.css ├── index.html ├── theme │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png ├── version-info.css └── version-info.html ├── info ├── index.css ├── index.html ├── shared_global.css └── steam.html ├── inventory ├── index.css └── index.html ├── review ├── index.html └── recommended.css ├── setting ├── imgs │ ├── bg.png │ ├── cfg-right.jpg │ └── cfg-right.png ├── index.css └── index.html └── user ├── check.webp ├── index.css └── index.html /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: 'eslint:recommended', 7 | overrides: [ 8 | { 9 | env: { 10 | node: true 11 | }, 12 | files: [ 13 | '.eslintrc.{js,cjs}' 14 | ], 15 | parserOptions: { 16 | sourceType: 'script' 17 | } 18 | } 19 | ], 20 | parserOptions: { 21 | ecmaVersion: 'latest', 22 | sourceType: 'module' 23 | }, 24 | ignorePatterns: [ 25 | 'test/**' 26 | ], 27 | rules: { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: 更新版本号 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: 更新版本号 14 | uses: googleapis/release-please-action@v4 15 | id: release_please 16 | with: 17 | release-type: node 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | bump-minor-pre-major: true 20 | version-file: package.json 21 | fork: false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | **/test.js 3 | **/test.ts 4 | node_modules 5 | config/config 6 | data 7 | data.db 8 | temp 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 小叶 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam Plugin 2 | 3 |
4 | 5 | **提供 steam 相关功能** 6 | 7 |
8 | 9 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/XasYer/steam-plugin) 10 | ![GitHub stars](https://img.shields.io/github/stars/XasYer/steam-plugin?style=social) 11 | ![GitHub forks](https://img.shields.io/github/forks/XasYer/steam-plugin?style=social) 12 | ![GitHub license](https://img.shields.io/github/license/XasYer/steam-plugin) 13 | ![GitHub issues](https://img.shields.io/github/issues/XasYer/steam-plugin) 14 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/XasYer/steam-plugin) 15 | ![GitHub repo size](https://img.shields.io/github/repo-size/XasYer/steam-plugin) 16 |
17 | 18 | 19 | 20 |
21 | 22 | ![Star History Chart](https://api.star-history.com/svg?repos=XasYer/steam-plugin&type=Date) 23 | 24 | ## **注意** 25 | 26 | 1. 一定要填**Steam Web API Key**,否则无法使用绝大部分功能,通常会返回 401 或 403 错误,请前往[Steam API](https://steamcommunity.com/dev/apikey)申请API Key, 域名随意填写 27 | 28 | 相关链接: 29 | 30 | - [Steam Web API 说明](https://partner.steamgames.com/doc/webapi_overview/auth) 31 | - [申请API Key](https://steamcommunity.com/dev/apikey) 32 | - [Steam API 条款](https://steamcommunity.com/dev/apiterms) 33 | 34 | 2. Steam 是国外网站, 所以通常需要配置代理或反代链接, 否则可能会出现连接超时, 通常会返回: `timeout of 5000ms exceeded` 35 | 36 | ## 介绍 37 | 38 | 这是一个基于 [Miao-Yunzai](https://github.com/yoimiya-kokomi/Miao-Yunzai)&[Trss-Yunzai](https://github.com/TimeRainStarSky/Yunzai)&[Karin](https://github.com/KarinJS/Karin)的扩展插件, 提供 steam 群友状态播报, steam 库存, steam 愿望单 等功能 39 | 40 | ## 安装 41 | 42 | ### Yunzai使用 43 | 44 | #### 使用github 45 | 46 | ```bash 47 | git clone --depth=1 https://github.com/XasYer/steam-plugin.git ./plugins/steam-plugin 48 | ``` 49 | 50 | #### 使用gitee 51 | 52 | ```bash 53 | git clone --depth=1 https://gitee.com/xiaoye12123/steam-plugin.git ./plugins/steam-plugin 54 | ``` 55 | 56 | ### Karin使用 57 | 58 | #### 使用github 59 | 60 | ```bash 61 | git clone --depth=1 https://github.com/XasYer/steam-plugin.git ./plugins/karin-plugin-steam 62 | ``` 63 | 64 | #### 使用gitee 65 | 66 | ```bash 67 | git clone --depth=1 https://gitee.com/xiaoye12123/steam-plugin.git ./plugins/karin-plugin-steam 68 | ``` 69 | 70 | ### 安装依赖 71 | 72 | ```bash 73 | pnpm install --filter=steam-plugin 74 | ``` 75 | 76 | ## 功能 77 | 78 | ![帮助图](./resources/help/help.jpg) 79 | 80 | ## 联系方式 81 | 82 | - QQ 群: [741577559](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=IvPaOVo_p-6n--FaLm1v39ML9EZaBRCm&authKey=YPs0p%2FRh8MGPQrWZgn99fk4kGB5PtRAoOYIUqK71FBsBYCDdekxCEHFFHnznpYA1&noverify=0&group_code=741577559) 83 | 84 | ## 使用cloudflare搭建反代 (连接不上steam情况下的备选) 85 | 86 | 1. 需要`cloudflare账号`, 以及在`cf托管的域名`, 自行查看对应教程 87 | 2. 打开cf主页左侧的`Workers 和 Pages`, 点击`创建`, 然后点击`创建 Worker` 88 | 3. 名字随意, 可参考`steam` 然后点击`部署` 再点击`编辑代码` 89 | 4. 复制以下代码到编辑器, `覆盖`原内容, 然后点击`部署`, 出现`版本已保存`即可 90 | ```js 91 | export default { 92 | async fetch(request) { 93 | const url = new URL(request.url); 94 | const path = decodeURIComponent(url.pathname.replace("/", "")); 95 | if (!path || !path.startsWith("http")) { 96 | return new Response("Ciallo~(∠・ω< )⌒☆"); 97 | } 98 | const target = new URL(path); 99 | url.hostname = path.replace(/https?:\/\//, ""); 100 | url.protocol = target.protocol; 101 | url.pathname = target.pathname; 102 | return await fetch(new Request(url, request)); 103 | }, 104 | }; 105 | ``` 106 | 5. 依次点击`左上角第3步填写的名字`, `设置`, `域和路由`右边的`添加`, `自定义域`, 然后填入你想设置的二级或多级域名, 比如`steam.example.com`, 然后点`添加域`, 要改成自己的在cf托管的域名 107 | 6. 测试(可选): 浏览器访问`https://steam.example.com/https://api.steampowered.com/ISteamWebAPIUtil/GetServerInfo/v1/`, `steam.example.com`替换成第5步设置的域名, 如果能看到`servertime`字段, 说明配置成功 108 | 7. 对你的Bot发送`#steam设置通用反代https://steam.example.com/{{url}}`, 域名替换成第5步设置的域名 109 | 110 | ### 注意事项 111 | 112 | 1. cloudflare的workers免费账户的每天请求数量限制10w次(一个账号所有的workers请求总量) 113 | 2. 2024年12月03日 cloudflare 更新[服务条款 2.2.1](https://www.cloudflare.com/zh-cn/terms/) **禁止使用服务提供虚拟专用网络或其他类似的代理服务** 若任使用请知晓可能出现的风险, 包括但不限于: **暂停或终止您对cloudflare服务的使用或访问** 等等。 114 | 115 | ## 贡献者 116 | 117 | > 🌟 星光闪烁,你们的智慧如同璀璨的夜空。感谢所有为 **steam-plugin** 做出贡献的人! 118 | 119 | 120 | 121 | 122 | 123 | ![Alt](https://repobeats.axiom.co/api/embed/aafe6a6a7a72df285ae3965974546314c467db8d.svg "Repobeats analytics image") 124 | 125 | ## 其他 126 | 127 | 如果觉得此插件对你有帮助的话,可以点一个 star,你的支持就是不断更新的动力~ 128 | -------------------------------------------------------------------------------- /apps/QRLogin.js: -------------------------------------------------------------------------------- 1 | import { App } from '#components' 2 | import { segment } from '#lib' 3 | import { api, db, utils } from '#models' 4 | import moment from 'moment' 5 | import QRCode from 'qrcode' 6 | 7 | const appInfo = { 8 | id: 'QRLogin', 9 | name: '扫码登录' 10 | } 11 | 12 | const baseReg = '扫码(?:登[录陆]|绑定)' 13 | 14 | const rule = { 15 | QRLoginTips: { 16 | reg: App.getReg(`${baseReg}(帮助)?`), 17 | fnc: async (e) => { 18 | const msg = [ 19 | '将使用steamApp扫码二维码进行登录, 登录完成后机器人可获得对应账号的access_token并保存, 拥有access_token后可执行各种隐私操作, 请在**特别信任**的机器人上进行扫码登录, 如果确认需要扫码登录, 请先打开steamApp进入扫码界面并使用以下方法(不支持扫描相册二维码)', 20 | '使用方法:', 21 | '1. 发送#steam确认扫码登录', 22 | '可能会出现地区不一致导致Steam阻止登录的情况, 可以先尝试切换到和Bot反代地区一致后再次扫码登录', 23 | '---------------', 24 | '2. 本地请求二维码', 25 | '2.1: 打开Chrome浏览器访问 https://store.steampowered.com/', 26 | '2.2: 按f12点击控制台/Console,输入以下内容并回车', 27 | 'copy("#steam辅助扫码登录"+await fetch(\'https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaQR/v1?input_json={"device_details":{"device_friendly_name":"Xiaomi 15 Pro","platform_type":3,"os_type":-500,"gaming_device_type":528},"website_id":"Mobile"}\',{method: \'POST\'}).then(res=>res.json()).then(res=>res.response).then(JSON.stringify).then(res=>{console.log(res);return res}));alert("复制成功")', 28 | '2.3 弹窗复制成功后发送复制后的内容到机器人', 29 | 'ps: 需自行处理代理保证请求和扫码的地址一致' 30 | ] 31 | await utils.bot.makeForwardMsg(e, msg) 32 | return true 33 | } 34 | }, 35 | QRLogin: { 36 | reg: App.getReg(`(?:确认|辅助)${baseReg}(.*)`), 37 | fnc: async (e) => { 38 | const input = rule.QRLogin.reg.exec(e.msg)[1].trim() 39 | let session = input ? JSON.parse(input) : await api.IAuthenticationService.BeginAuthSessionViaQR() 40 | if (session.response) { 41 | session = session.response 42 | } 43 | const qrcode = (await QRCode.toDataURL(session.challenge_url)).replace('data:image/png;base64,', 'base64://') 44 | await App.reply(e, ['请在30秒内使用steamApp扫描二维码进行登录', segment.image(qrcode)], { 45 | recallMsg: 30, 46 | quote: true 47 | }) 48 | for (let i = 0; i < 6; i++) { 49 | await new Promise(resolve => setTimeout(resolve, 1000 * 5)) 50 | const qrcodeRes = await api.IAuthenticationService.PollAuthSessionStatus(session.client_id, session.request_id).catch(() => ({})) 51 | if (qrcodeRes.access_token) { 52 | const jwt = utils.steam.decodeAccessTokenJwt(qrcodeRes.access_token) 53 | const cookie = utils.steam.getCookie(jwt.sub, qrcodeRes.access_token) 54 | const dbRes = await db.token.set(e.user_id, qrcodeRes.access_token, cookie, qrcodeRes.refresh_token) 55 | const user = await db.user.getBySteamId(dbRes.steamId) 56 | if (user?.userId) { 57 | if (user.userId != e.user_id) { 58 | await db.user.del(user.userId, dbRes.steamId) 59 | await db.user.add(e.user_id, dbRes.steamId) 60 | } else { 61 | await db.user.set(e.user_id, dbRes.steamId, true) 62 | } 63 | } else { 64 | await db.user.add(e.user_id, dbRes.steamId) 65 | } 66 | return `登录成功\nsteamId: ${dbRes.steamId}\n登录名: ${qrcodeRes.account_name.replace(/^(.)(.*)(.)$/, '$1***$3')}\n需要切换到对应的steamId才会生效` 67 | } 68 | } 69 | return '登录超时~请重新触发指令' 70 | } 71 | }, 72 | refreshToken: { 73 | reg: App.getReg('刷新(access_token|token|ak|ck|accesstoken|cookie)'), 74 | cfg: { 75 | accessToken: true 76 | }, 77 | fnc: async (e) => { 78 | await utils.steam.refreshAccessToken(e.user_id, true) 79 | return 'access_token已刷新~' 80 | } 81 | }, 82 | showToken: { 83 | reg: App.getReg('我的(access_token|token|ak|ck|accesstoken|cookie)'), 84 | cfg: { 85 | accessToken: true, 86 | private: true 87 | }, 88 | fnc: async (e, { accessToken, cookie }) => { 89 | const jwt = utils.steam.decodeAccessTokenJwt(accessToken) 90 | await App.reply(e, accessToken, { at: false }) 91 | await App.reply(e, cookie, { at: false }) 92 | return [ 93 | `${accessToken.slice(0, 5)}...: access_token`, 94 | `${cookie.slice(0, 5)}...: cookie`, 95 | `过期时间: ${moment.unix(jwt.exp).format('YYYY-MM-DD HH:mm:ss')}` 96 | ].join('\n') 97 | } 98 | }, 99 | deleteToken: { 100 | reg: App.getReg('删除(access_token|token|ak|ck|accesstoken|cookie)'), 101 | cfg: { 102 | accessToken: true 103 | }, 104 | fnc: async (e, { steamId }) => { 105 | await db.token.del(e.user_id, steamId) 106 | return 'access_token已删除~' 107 | } 108 | } 109 | } 110 | 111 | export const app = new App(appInfo, rule).create() 112 | -------------------------------------------------------------------------------- /apps/achievement.js: -------------------------------------------------------------------------------- 1 | import { App, Render } from '#components' 2 | import { utils, api } from '#models' 3 | import _ from 'lodash' 4 | 5 | const appInfo = { 6 | id: 'achievement', 7 | name: '成就统计' 8 | } 9 | 10 | const rule = { 11 | achievements: { 12 | reg: App.getReg('(成就|统计)\\s*(\\d+|\\d+[-:\\s]\\d+)?'), 13 | cfg: { 14 | tips: true, 15 | steamId: true, 16 | appid: true 17 | }, 18 | fnc: async (e, { appid, steamId, uid }) => { 19 | const type = e.msg.includes('成就') ? '成就' : '统计' 20 | // 先获取游戏的成就列表 21 | const achievementsByGame = await api.ISteamUserStats.GetSchemaForGame(appid).catch(() => {}) 22 | if (!achievementsByGame || !achievementsByGame.availableGameStats) { 23 | return `没有找到${appid}的成就信息` 24 | } 25 | const achievementsByUser = await api.ISteamUserStats.GetUserStatsForGame(appid, steamId).catch(() => {}) 26 | if (!achievementsByUser || !achievementsByUser.achievements) { 27 | return `没有找到${steamId}在${appid}的成就信息` 28 | } 29 | const nickname = await utils.bot.getUserName(e.self_id, uid, e.group_id) 30 | const data = [ 31 | { 32 | title: `${nickname}的${achievementsByGame.gameName} ${type}统计`, 33 | games: [{ 34 | name: achievementsByGame.gameName, 35 | appid 36 | }] 37 | } 38 | ] 39 | if (type === '成就') { 40 | if (!achievementsByGame.availableGameStats.achievements?.length) { 41 | return `${achievementsByGame.gameName}好像没有成就呢` 42 | } 43 | const completeAchievements = [] 44 | const unCompleteAchievements = [] 45 | achievementsByGame.availableGameStats.achievements.forEach(all => { 46 | const user = achievementsByUser.achievements.find(i => i.name === all.name) 47 | const info = { 48 | name: all.displayName, 49 | desc: all.hidden ? '已隐藏' : all.description, 50 | image: user ? all.icon : all.icongray, 51 | isAvatar: true 52 | } 53 | if (user) { 54 | completeAchievements.push(info) 55 | } else { 56 | unCompleteAchievements.push(info) 57 | } 58 | }) 59 | data.push( 60 | { 61 | title: '已完成成就', 62 | desc: `共${completeAchievements.length}个`, 63 | games: completeAchievements 64 | }, 65 | { 66 | title: '未完成成就', 67 | desc: `共${unCompleteAchievements.length}个`, 68 | games: unCompleteAchievements 69 | } 70 | ) 71 | } else { 72 | if (!achievementsByGame.availableGameStats.stats?.length) { 73 | return `${achievementsByGame.gameName}好像没有统计呢` 74 | } 75 | const completeStats = achievementsByUser.stats.map(i => { 76 | const item = achievementsByGame.availableGameStats.stats.find(j => j.name === i.name) 77 | if (item) { 78 | return { 79 | name: item.name, 80 | detail: i.value, 81 | desc: item.displayName || '', 82 | noImg: true 83 | } 84 | } else { 85 | return false 86 | } 87 | }).filter(Boolean) 88 | data.push( 89 | { 90 | title: '已完成统计', 91 | desc: `共${completeStats.length}个`, 92 | games: completeStats 93 | } 94 | ) 95 | } 96 | return await Render.render('inventory/index', { data }) 97 | } 98 | }, 99 | achievementStats: { 100 | reg: App.getReg('成就统计\\s*(\\d*)'), 101 | cfg: { 102 | tips: true, 103 | appid: true 104 | }, 105 | fnc: async (e, { appid }) => { 106 | // 先获取游戏的成就列表 107 | const achievementsByGame = await api.ISteamUserStats.GetSchemaForGame(appid) 108 | if (!achievementsByGame || !achievementsByGame.availableGameStats) { 109 | return `没有找到${appid}的成就信息` 110 | } 111 | // 全球统计 112 | const achievements = await api.ISteamUserStats.GetGlobalAchievementPercentagesForApp(appid) 113 | const data = [ 114 | { 115 | title: `${achievementsByGame.gameName} 全球成就统计`, 116 | games: [{ 117 | name: achievementsByGame.gameName, 118 | appid 119 | }] 120 | } 121 | ] 122 | const games = [] 123 | achievementsByGame.availableGameStats.achievements.forEach(all => { 124 | const i = achievements.find(i => i.name === all.name) 125 | if (!i) return 126 | const percent = parseInt(i.percent) 127 | const info = { 128 | name: all.displayName, 129 | desc: all.hidden ? '已隐藏' : all.description, 130 | image: i ? all.icon : all.icongray, 131 | isAvatar: true, 132 | detail: `${percent}%`, 133 | detailPercent: percent 134 | } 135 | games.push(info) 136 | }) 137 | data.push({ 138 | title: '全球成就统计', 139 | desc: `共${games.length}个`, 140 | games: _.orderBy(games, 'detailPercent', 'desc') 141 | }) 142 | return await Render.render('inventory/index', { data }) 143 | } 144 | } 145 | } 146 | 147 | export const app = new App(appInfo, rule).create() 148 | -------------------------------------------------------------------------------- /apps/bind.js: -------------------------------------------------------------------------------- 1 | import { utils, db, bind } from '#models' 2 | import { App, Config } from '#components' 3 | import { logger } from '#lib' 4 | 5 | const appInfo = { 6 | id: 'bind', 7 | name: '绑定Steam' 8 | } 9 | 10 | const rule = { 11 | getBindImg: { 12 | // 这个指令必须# 不然可能会误触发 13 | reg: /^#steam$/i, 14 | fnc: async e => await bind.getBindSteamIdsImg(e.self_id, utils.bot.getAtUid(e.at, e.user_id), e.group_id) 15 | }, 16 | bind: { 17 | reg: App.getReg('(?:[切更]换)?(?:绑定|bind)\\s*(\\d+)?'), 18 | fnc: async e => { 19 | // 如果是主人可以at其他用户进行绑定 20 | const uid = utils.bot.getAtUid(e.isMaster ? e.at : '', e.user_id) 21 | const textId = rule.bind.reg.exec(e.msg)[1] 22 | const userBindAll = await db.user.getAllByUserId(uid) 23 | if (!textId) { 24 | return await bind.getBindSteamIdsImg(e.self_id, uid, e.group_id, userBindAll) 25 | } 26 | const index = Number(textId) <= userBindAll.length ? Number(textId) - 1 : -1 27 | const steamId = index >= 0 ? userBindAll[index].steamId : utils.steam.getSteamId(textId) 28 | // 检查steamId是否被绑定 29 | const bindInfo = await db.user.getBySteamId(steamId) 30 | if (bindInfo) { 31 | if (bindInfo.userId == uid) { 32 | await db.user.set(uid, steamId) 33 | } else { 34 | return Config.tips.repeatBindTips 35 | } 36 | } else { 37 | await db.user.add(uid, steamId) 38 | // 群聊绑定才添加 39 | if (e.group_id) { 40 | await db.push.setNA(uid, steamId) 41 | await db.push.set(uid, steamId, e.self_id, e.group_id, { 42 | play: Config.push.defaultPush, 43 | state: Config.push.defaultPush, 44 | inventory: false, 45 | wishlist: false 46 | }) 47 | } 48 | } 49 | return await bind.getBindSteamIdsImg(e.self_id, uid, e.group_id) 50 | } 51 | }, 52 | unbind: { 53 | reg: App.getReg('(?:强制)?(?:解除?绑定?|unbind|取消绑定)\\s*(\\d+)?'), 54 | fnc: async e => { 55 | const textId = rule.unbind.reg.exec(e.msg)[1] 56 | if (!textId) { 57 | return '要和SteamID或好友码一起发送哦' 58 | } 59 | const isForce = e.msg.includes('强制') && e.isMaster 60 | // 如果是主人可以at其他用户进行绑定 61 | const uid = utils.bot.getAtUid(e.isMaster ? e.at : '', e.user_id) 62 | const userBindAll = await db.user.getAllByUserId(uid) 63 | const index = Number(textId) <= userBindAll.length ? Number(textId) - 1 : -1 64 | const steamId = index >= 0 ? userBindAll[index].steamId : utils.steam.getSteamId(textId) 65 | // 检查steamId是否被绑定 66 | const bindInfo = await db.user.getBySteamId(steamId) 67 | if (bindInfo) { 68 | if (bindInfo.userId == uid || isForce) { 69 | const id = isForce ? bindInfo.userId : uid 70 | try { 71 | await db.user.del(id, steamId) 72 | return `已解除绑定${steamId}` 73 | } catch (error) { 74 | logger.error(error) 75 | return `解绑失败了, 请稍后再试\n${error.message}` 76 | } 77 | } else { 78 | return '只能解绑自己绑定的steamId哦' 79 | } 80 | } 81 | return '还没有人绑定这个steamId呢' 82 | } 83 | } 84 | } 85 | 86 | export const app = new App(appInfo, rule).create() 87 | -------------------------------------------------------------------------------- /apps/cart.js: -------------------------------------------------------------------------------- 1 | import { App, Render } from '#components' 2 | import { api, utils } from '#models' 3 | 4 | const appInfo = { 5 | id: 'cart', 6 | name: '购物车操作' 7 | } 8 | 9 | const rule = { 10 | modify: { 11 | reg: App.getReg('([添增]加|[删移][除出])购物车\\s*(\\d*)'), 12 | cfg: { 13 | tips: true, 14 | accessToken: true, 15 | appid: true 16 | }, 17 | fnc: async (e, { accessToken, steamId, appid }) => { 18 | // 获取用户的地区代码 19 | const country = await api.IUserAccountService.GetUserCountry(accessToken, steamId) 20 | if (!country) { 21 | return '获取地区代码失败...' 22 | } 23 | // 获取packageid 24 | const infos = await api.IStoreBrowseService.GetItems([{ bundleid: appid }, { appid }]) 25 | const info = infos[appid] 26 | if (!info) { 27 | return `没有获取到${appid}的信息` 28 | } 29 | const modifyType = e.msg.includes('加') ? 'add' : 'del' 30 | if (info.is_free && modifyType === 'add') { 31 | return `${info.name}是免费游戏哦, 直接入库吧` 32 | } 33 | const appType = info.best_purchase_option.packageid ? 'packageid' : 'bundleid' 34 | const packageid = appType === 'packageid' ? info.best_purchase_option.packageid : info.best_purchase_option.bundleid 35 | // 先检查有没有在购物车中 36 | const cart = await api.IAccountCartService.GetCart(accessToken, country) 37 | const item = cart.line_items?.find(i => i[appType] === packageid) 38 | if (modifyType === 'del') { 39 | if (item) { 40 | const res = await api.IAccountCartService.RemoveItemFromCart(accessToken, item.line_item_id, country) 41 | if (!res.line_items?.some(i => i[appType] === packageid)) { 42 | return `删除${info.name}成功~` 43 | } else { 44 | return `删除${info.name}失败...` 45 | } 46 | } else { 47 | return `${info.name}没有在购物车中~` 48 | } 49 | } else { 50 | if (!item) { 51 | // 加入购物车 52 | const res = await api.IAccountCartService.AddItemsToCart(accessToken, { [appType]: packageid }, country) 53 | if (res.cart.line_items?.some(i => i.packageid === packageid)) { 54 | return `已添加${info.name}到购物车~` 55 | } else { 56 | return `添加${info.name}到购物车失败...` 57 | } 58 | } else { 59 | return `${info.name}已经在购物车中~` 60 | } 61 | } 62 | } 63 | }, 64 | look: { 65 | reg: App.getReg('(查看|清空)购物车'), 66 | cfg: { 67 | accessToken: true 68 | }, 69 | fnc: async (e, { accessToken, steamId }) => { 70 | const country = await api.IUserAccountService.GetUserCountry(accessToken, steamId) 71 | if (!country) { 72 | return '获取地区代码失败...' 73 | } 74 | if (e.msg.includes('清空')) { 75 | await api.IAccountCartService.DeleteCart(accessToken) 76 | return '已清空购物车~' 77 | } else { 78 | const cart = await api.IAccountCartService.GetCart(accessToken, country) 79 | if (!cart.line_items) { 80 | return '购物车为空~' 81 | } 82 | const ids = cart.line_items.map(i => { 83 | if (i.packageid) { 84 | return { packageid: i.packageid } 85 | } else if (i.bundleid) { 86 | return { bundleid: i.bundleid } 87 | } else { 88 | return {} 89 | } 90 | }) 91 | const infos = await api.IStoreBrowseService.GetItems(ids, { include_assets: true }) 92 | const games = cart.line_items.map(i => { 93 | const info = infos[i.packageid || i.bundleid] 94 | if (!info) { 95 | return { 96 | name: '未知项目', 97 | appid: i.packageid || i.bundleid, 98 | image: '', 99 | price: { 100 | original: i.price_when_added?.formatted_amount 101 | } 102 | } 103 | } 104 | const image = info.assets 105 | // eslint-disable-next-line no-template-curly-in-string 106 | ? utils.steam.getStaticUrl(info.assets.asset_url_format.replace('${FILENAME}', info.assets.header)) 107 | : utils.steam.getHeaderImgUrlByAppid(info.appid) 108 | return { 109 | name: info.name, 110 | appid: info.appid || info.id, 111 | image, 112 | desc: i.packageid ? '游戏或DLC' : '捆绑包', 113 | price: { 114 | original: i.price_when_added.formatted_amount 115 | } 116 | } 117 | }) 118 | const data = [{ 119 | title: `${await utils.bot.getUserName(e.self_id, e.user_id, e.group_id)}购物车一共有${games.length}件商品`, 120 | desc: `一共 ${cart.subtotal.formatted_amount}`, 121 | games 122 | }] 123 | return await Render.render('inventory/index', { data }) 124 | } 125 | } 126 | } 127 | } 128 | 129 | export const app = new App(appInfo, rule).create() 130 | -------------------------------------------------------------------------------- /apps/client.js: -------------------------------------------------------------------------------- 1 | import { App, Render } from '#components' 2 | import { api } from '#models' 3 | 4 | const appInfo = { 5 | id: 'client', 6 | name: '客户端操作' 7 | } 8 | 9 | const baseReg = '(?:客户端|clinet|c)' 10 | 11 | const rule = { 12 | info: { 13 | reg: App.getReg(`${baseReg}信息`), 14 | cfg: { 15 | tips: true, 16 | accessToken: true 17 | }, 18 | fnc: async (e, { accessToken }) => { 19 | const clients = await api.IClientCommService.GetAllClientLogonInfo(accessToken) 20 | if (!clients.sessions) { 21 | return '没有登录Steam客户端' 22 | } 23 | const text = clients.sessions.map(i => [ 24 | `instanceid: ${i.client_instanceid}`, 25 | `os: ${i.os_name}`, 26 | `name: ${i.machine_name}` 27 | ].join('\n')).join('\n----------\n') 28 | return `可用instanceid指定客户端默认为第一个\n${text}` 29 | } 30 | }, 31 | appList: { 32 | reg: App.getReg(`${baseReg}游戏列表(\\d*)`), 33 | cfg: { 34 | tips: true, 35 | accessToken: true 36 | }, 37 | fnc: async (e, { accessToken }) => { 38 | const instanceid = rule.appList.reg.exec(e.msg)[1] 39 | const res = await api.IClientCommService.GetClientAppList(accessToken, instanceid) 40 | if (!res) { 41 | return `没有获取到客户端游戏列表${instanceid ? ', 请检查instanceid是否正确' : ''}` 42 | } 43 | // 需要更新的游戏 44 | const updateList = [] 45 | // 已安装的游戏 46 | const installList = [] 47 | // 未安装的游戏 48 | const uninstallList = [] 49 | res.apps.forEach(i => { 50 | if (i.app_type !== 'game') { 51 | return 52 | } 53 | const info = { 54 | appid: i.appid, 55 | name: i.app 56 | } 57 | if (i.installed) { 58 | if (i.target_buildid && i.target_buildid !== i.source_buildid) { 59 | // 还可以细分 60 | // queue_position -1: 未安排下载 0: 已安排 61 | // rt_time_scheduled: 计划下载时间 62 | // download_paused true: 暂停下载 false: 正在下载 63 | updateList.push({ 64 | ...info, 65 | desc: `${formatBytes(i.bytes_downloaded)} / ${formatBytes(i.bytes_to_download)}` 66 | }) 67 | } else { 68 | installList.push(info) 69 | } 70 | } else { 71 | uninstallList.push(info) 72 | } 73 | }) 74 | const data = [] 75 | if (updateList.length) { 76 | data.push({ 77 | title: '有更新的游戏', 78 | games: updateList 79 | }) 80 | } 81 | if (installList.length) { 82 | data.push({ 83 | title: '已安装的游戏', 84 | games: installList 85 | }) 86 | } 87 | if (uninstallList.length) { 88 | data.push({ 89 | title: '未安装的游戏', 90 | games: uninstallList 91 | }) 92 | } 93 | if (!data.length) { 94 | return '游戏列表为空' 95 | } 96 | return await Render.render('inventory/index', { 97 | data 98 | }) 99 | } 100 | }, 101 | install: { 102 | reg: App.getReg(`${baseReg}(?:安装|下载)(?:游戏)?\\s*(\\d*)\\s*(\\d*)`), 103 | cfg: { 104 | accessToken: true, 105 | appid: true 106 | }, 107 | fnc: async (e, { accessToken, appid }) => { 108 | const instanceid = rule.install.reg.exec(e.msg)[2] 109 | await api.IClientCommService.InstallClientApp(accessToken, appid, instanceid) 110 | return '已发送安装请求' 111 | } 112 | }, 113 | launch: { 114 | reg: App.getReg(`${baseReg}(?:启动|打开)(?:游戏)?\\s*(\\d*)\\s*(\\d*)`), 115 | cfg: { 116 | accessToken: true, 117 | appid: true 118 | }, 119 | fnc: async (e, { accessToken, appid }) => { 120 | const instanceid = rule.launch.reg.exec(e.msg)[2] 121 | await api.IClientCommService.LaunchClientApp(accessToken, appid, instanceid) 122 | return '已发送启动请求' 123 | } 124 | }, 125 | uninstall: { 126 | reg: App.getReg(`${baseReg}(?:卸载|删除)(?:游戏)?\\s*(\\d*)\\s*(\\d*)`), 127 | cfg: { 128 | accessToken: true, 129 | appid: true 130 | }, 131 | fnc: async (e, { accessToken, appid }) => { 132 | const instanceid = rule.uninstall.reg.exec(e.msg)[2] 133 | await api.IClientCommService.UninstallClientApp(accessToken, appid, instanceid) 134 | return '已发送卸载请求' 135 | } 136 | }, 137 | download: { 138 | reg: App.getReg(`${baseReg}(?:恢复|暂停|停止|继续)(?:下载|更新)?(?:游戏)?\\s*(\\d*)\\s*(\\d*)`), 139 | cfg: { 140 | accessToken: true, 141 | appid: true 142 | }, 143 | fnc: async (e, { accessToken, appid }) => { 144 | const instanceid = rule.download.reg.exec(e.msg)[2] 145 | const download = /(恢复|继续)/.test(e.msg) 146 | await api.IClientCommService.SetClientAppUpdateState(accessToken, appid, download, instanceid) 147 | return `已发送${download ? '继续' : '暂停'}下载请求` 148 | } 149 | } 150 | } 151 | 152 | function formatBytes (bytes, fractionDigits = 2) { 153 | if (!bytes) return '0 B' 154 | 155 | const k = 1024 156 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] 157 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 158 | 159 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(fractionDigits))} ${sizes[i]}` 160 | } 161 | 162 | export const app = new App(appInfo, rule).create() 163 | -------------------------------------------------------------------------------- /apps/dev.js: -------------------------------------------------------------------------------- 1 | import { App, Config } from '#components' 2 | import { api, db, utils } from '#models' 3 | import { segment } from '#lib' 4 | 5 | const appInfo = { 6 | id: 'dev', 7 | name: '接口测试' 8 | } 9 | 10 | const rule = { 11 | dev: { 12 | reg: App.getReg('dev\\s*(.*)'), 13 | cfg: { 14 | tips: true 15 | }, 16 | fnc: async e => { 17 | const keys = Object.keys(api) 18 | const text = rule.dev.reg.exec(e.msg)[1] 19 | if (!text) { 20 | const methods = keys.map((interfaceName, interfaceIndex) => { 21 | return Object.keys(api[interfaceName]).map((methodName, methodIndex) => { 22 | return `${interfaceIndex}.${methodIndex} ${interfaceName}.${methodName}(${getParams(api[interfaceName][methodName]).join(', ')})` 23 | }).join('\n\n') 24 | }) 25 | const msg = [ 26 | '使用方法: ', 27 | '#steamdev 接口名.方法名 参数1 参数2...', 28 | '带s的参数为数组', 29 | '参数可使用{steamid}和{accesstoken}占位符,表示当前绑定的SteamID和AccessToken', 30 | '接口名和方法名可使用数字索引,例如: 0.0 1.1 2.2', 31 | '可使用的接口名和方法名如下:', 32 | '部分api未经测试,可能存在bug', 33 | ...methods 34 | ] 35 | await utils.bot.makeForwardMsg(e, msg) 36 | return true 37 | } 38 | const [cmd, ...args] = split(text) 39 | const [interfaceKey, methodKey] = cmd.split('.') 40 | const interfaceName = keys[interfaceKey] || interfaceKey 41 | const methods = Object.keys(api[interfaceName]) 42 | const methodName = methods[methodKey] || methodKey 43 | const method = api[interfaceName][methodName] 44 | const methodParams = getParams(method) 45 | const uid = utils.bot.getAtUid(e.at, e.user_id) 46 | const steamId = await db.user.getBind(uid) 47 | if (!steamId) { 48 | await e.reply([segment.at(uid), '\n', Config.tips.noSteamIdTips]) 49 | return true 50 | } 51 | const hasAccessToken = /{access_?token}/i.test(e.msg) 52 | if (hasAccessToken && uid != e.user_id) { 53 | return '只能操作自己的accessToken' 54 | } 55 | const token = hasAccessToken && await utils.steam.getAccessToken(uid, steamId) 56 | if (hasAccessToken && !token.success) { 57 | return '没有绑定accessToken' 58 | } 59 | const replaceParams = (text) => { 60 | if (Array.isArray(text)) { 61 | return text.map(replaceParams) 62 | } else { 63 | return text.replace(/{steamid}/ig, steamId).replace(/{access_?token}/ig, token.accessToken) 64 | } 65 | } 66 | const params = args.map(replaceParams) 67 | const start = Date.now() 68 | const result = await method(...params) 69 | const end = Date.now() 70 | const time = end - start 71 | const msg = [ 72 | `接口: ${interfaceName}.${methodName}(${methodParams.join(', ')})`, 73 | `参数: ${params.map(i => i.length > 17 ? i.replace(/^(.{5})(.*)(.{5})$/, '$1...$3') : i).join(' ')}`, 74 | `耗时: ${time}ms`, 75 | '结果: ', 76 | JSON.stringify(result, null, 2) ?? 'undefined' 77 | ] 78 | await utils.bot.makeForwardMsg(e, msg) 79 | return true 80 | } 81 | } 82 | } 83 | 84 | function getParams (fn) { 85 | const fnStr = fn.toString().split('\n')[0] 86 | const params = fnStr.match(/\((.*)\)/)[1] 87 | return params.split(',').map(param => param.trim()).filter(Boolean) 88 | } 89 | 90 | function split (text) { 91 | const reg = /\[.*?\]|\S+/g 92 | const matches = text.match(reg) 93 | 94 | return matches.map(match => { 95 | if (match.startsWith('[') && match.endsWith(']')) { 96 | return match.slice(1, -1).split(' ') 97 | } 98 | return match 99 | }) 100 | } 101 | 102 | export const app = new App(appInfo, rule).create() 103 | -------------------------------------------------------------------------------- /apps/discounts.js: -------------------------------------------------------------------------------- 1 | import { App, Render } from '#components' 2 | import { api, utils } from '#models' 3 | import moment from 'moment' 4 | 5 | const appInfo = { 6 | id: 'discounts', 7 | name: '优惠' 8 | } 9 | 10 | const rule = { 11 | discounts: { 12 | reg: App.getReg('(优惠|特惠|热销|新品|即将推出)'), 13 | cfg: { 14 | tips: true 15 | }, 16 | fnc: async e => { 17 | const res = await api.store.featuredcategories() 18 | const items = [ 19 | { 20 | title: '优惠', 21 | key: 'specials' 22 | }, 23 | { 24 | title: '即将推出', 25 | key: 'coming_soon' 26 | }, 27 | { 28 | title: '热销', 29 | key: 'top_sellers' 30 | }, 31 | { 32 | title: '新品', 33 | key: 'new_releases' 34 | } 35 | ] 36 | const data = [] 37 | for (const item of items) { 38 | const key = { 39 | title: item.title, 40 | games: [] 41 | } 42 | for (const i of res[item.key]?.items || []) { 43 | key.games.push({ 44 | appid: i.id, 45 | name: i.name, 46 | desc: i.discount_expiration ? moment.unix(i.discount_expiration).format('YYYY-MM-DD HH:mm:ss') : '', 47 | image: i.image, 48 | price: i.discounted 49 | ? { 50 | original: `¥ ${i.original_price / 100}`, 51 | discount: i.discount_percent, 52 | current: `¥ ${i.final_price / 100}` 53 | } 54 | : { 55 | original: i.original_price ? `¥ ${i.original_price / 100}` : '' 56 | } 57 | }) 58 | } 59 | data.push(key) 60 | } 61 | return await Render.render('inventory/index', { data }) 62 | } 63 | }, 64 | queue: { 65 | reg: App.getReg('探索队列'), 66 | cfg: { 67 | tips: true, 68 | accessToken: true 69 | }, 70 | fnc: async (e, { accessToken, steamId }) => { 71 | const country = await api.IUserAccountService.GetUserCountry(accessToken, steamId) 72 | if (!country) { 73 | return '获取地区代码失败...' 74 | } 75 | const { appids, skipped } = await api.IStoreService.GetDiscoveryQueue(accessToken, country) 76 | const infoList = await api.IStoreBrowseService.GetItems(appids, { include_assets: true }) 77 | const games = appids.map(appid => { 78 | const info = infoList[appid] 79 | if (!info) { 80 | return { 81 | appid 82 | } 83 | } 84 | return { 85 | appid, 86 | name: info.name, 87 | image: utils.steam.getHeaderImgUrlByAppid(appid, 'apps', info.assets.header), 88 | price: utils.steam.generatePrice(info.best_purchase_option, info.is_free) 89 | } 90 | }) 91 | const data = [{ 92 | title: `${await utils.bot.getUserName(e.self_id, e.user_id, e.group_id)}的探索队列`, 93 | desc: [`已跳过${skipped}个游戏`, '#steam探索队列跳过+appid', '#steam探索队列全部跳过'], 94 | games 95 | }] 96 | return await Render.render('inventory/index', { data }) 97 | } 98 | }, 99 | queueSkip: { 100 | reg: App.getReg('探索队列跳过\\s*(\\d*)'), 101 | cfg: { 102 | accessToken: true, 103 | appid: true 104 | }, 105 | fnc: async (e, { accessToken, appid }) => { 106 | await api.IStoreService.SkipDiscoveryQueueItem(accessToken, appid) 107 | return `已跳过游戏${appid}~` 108 | } 109 | }, 110 | queueSkipAll: { 111 | reg: App.getReg('探索队列全部跳过'), 112 | cfg: { 113 | accessToken: true 114 | }, 115 | fnc: async (e, { accessToken, steamId }) => { 116 | const country = await api.IUserAccountService.GetUserCountry(accessToken, steamId) 117 | if (!country) { 118 | return '获取地区代码失败...' 119 | } 120 | const appids = (await api.IStoreService.GetDiscoveryQueue(accessToken, country)).appids 121 | await Promise.all(appids.map(async appid => await api.IStoreService.SkipDiscoveryQueueItem(accessToken, appid))) 122 | return '已跳过所有游戏~' 123 | } 124 | } 125 | } 126 | 127 | export const app = new App(appInfo, rule).create() 128 | -------------------------------------------------------------------------------- /apps/expend.js: -------------------------------------------------------------------------------- 1 | import { App } from '#components' 2 | import { api } from '#models' 3 | 4 | const appInfo = { 5 | id: 'expend', 6 | name: '支出' 7 | } 8 | 9 | const rule = { 10 | total: { 11 | reg: App.getReg('总?(支出|消费|花费)'), 12 | cfg: { 13 | accessToken: true, 14 | tips: true 15 | }, 16 | fnc: async (e, { cookie }) => { 17 | const html = await api.store.AjaxLoadMoreHistory(cookie) 18 | if (!html) { 19 | return '获取失败或消费为空' 20 | } 21 | const use = {} 22 | html.replace(/[\n\t]/g, '').split('').filter(i => !/(退款|wht_refunded|钱包<)/.test(i)).forEach(i => { 23 | const reg = /([\s\S]+?)<\/td>/ 24 | const regRet = reg.exec(i) 25 | if (regRet) { 26 | const [currency, value] = regRet[1].split(' ') 27 | use[currency] = (use[currency] || 0) + parseFloat(value) 28 | } 29 | }) 30 | const text = Object.keys(use).map(currency => `${currency} ${use[currency].toFixed(2)}`).join(' + ') 31 | return `在steam消费了${text}\n数据来源: 由 客服 -> 购买消费 -> 查看完整的购买记录 计算而来 仅供参考\n也可以前往 客服 -> 我的账户 -> 您 Steam 帐户的相关数据 -> 外部资金消费记录 查看 TotalSpend 的值` 32 | } 33 | } 34 | } 35 | 36 | export const app = new App(appInfo, rule).create() 37 | -------------------------------------------------------------------------------- /apps/help.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { help as helpUtil, utils } from '#models' 3 | import { App, Render, Version } from '#components' 4 | 5 | const appInfo = { 6 | id: 'help', 7 | name: '帮助' 8 | } 9 | 10 | const rule = { 11 | help: { 12 | reg: App.getReg('(插件|plugin)?(帮助|菜单|help)'), 13 | fnc: async e => { 14 | const helpGroup = [] 15 | 16 | const token = await utils.steam.getAccessToken(e.user_id) 17 | _.forEach(helpUtil.helpList, (group) => { 18 | switch (group.auth) { 19 | case 'master': 20 | if (!e.isMaster) { 21 | return true 22 | } 23 | break 24 | case 'accessToken': 25 | if (!token.success) { 26 | return true 27 | } 28 | } 29 | 30 | _.forEach(group.list, (help) => { 31 | const icon = _.random(1, 350) 32 | const x = (icon - 1) % 10 33 | const y = (icon - x - 1) / 10 34 | help.css = `background-position:-${x * 50}px -${y * 50}px` 35 | }) 36 | 37 | helpGroup.push(group) 38 | }) 39 | const themeData = await helpUtil.helpTheme.getThemeData({ 40 | colCount: token.success ? 4 : 3, 41 | colWidth: 275 42 | }) 43 | return await Render.render('help/index', { 44 | helpGroup, 45 | ...themeData, 46 | scale: 1.4 47 | }) 48 | } 49 | } 50 | // version: { 51 | // reg: /^#?steam(插件|plugin)?(版本|version)$/i, 52 | // fnc: version 53 | // } 54 | } 55 | 56 | // eslint-disable-next-line no-unused-vars 57 | async function version (e) { 58 | const img = await Render.render('help/version-info', { 59 | currentVersion: Version.version, 60 | changelogs: Version.changelogs, 61 | scale: 1.2 62 | }) 63 | return await e.reply(img) 64 | } 65 | 66 | export const app = new App(appInfo, rule).create() 67 | -------------------------------------------------------------------------------- /apps/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { logger } from '#lib' 3 | import { join } from 'node:path' 4 | import { Version } from '#components' 5 | 6 | const startTime = Date.now() 7 | 8 | const path = join(Version.pluginPath, 'apps') 9 | 10 | const apps = {} 11 | 12 | fs.readdirSync(path).forEach(file => { 13 | if (file.endsWith('.js') && file !== 'index.js') { 14 | apps[file.replace('.js', '')] = import(`file://${join(path, file)}`) 15 | } 16 | }) 17 | 18 | await Promise.all(Object.keys(apps).map(async id => { 19 | try { 20 | const startTime = Date.now() 21 | const exp = await apps[id] 22 | apps[id] = exp.app 23 | logger.debug(`加载js: apps/${id}.js成功 耗时: ${Date.now() - startTime}ms`) 24 | } catch (error) { 25 | delete apps[id] 26 | logger.log('error', `加载js: apps/${id}.js错误\n`, error) 27 | } 28 | })) 29 | 30 | export { apps } 31 | 32 | logger.log('info', '-----------------') 33 | logger.log('info', `${Version.pluginName} v${Version.pluginVersion} 加载成功~ 耗时: ${Date.now() - startTime}ms`) 34 | logger.log('info', '-------^_^-------') 35 | -------------------------------------------------------------------------------- /apps/online.js: -------------------------------------------------------------------------------- 1 | import { App } from '#components' 2 | import { segment } from '#lib' 3 | import { api, utils } from '#models' 4 | 5 | const appInfo = { 6 | id: 'online', 7 | name: 'Online' 8 | } 9 | 10 | const rule = { 11 | online: { 12 | reg: App.getReg('在线(?:统计|数据|人数)?\\s*(\\d*)'), 13 | cfg: { 14 | appid: true 15 | }, 16 | fnc: async (e, { appid }) => { 17 | const players = await api.ISteamUserStats.GetNumberOfCurrentPlayers(appid) 18 | if (players === false) { 19 | return '查询失败,可能没有这个游戏?' 20 | } 21 | const icon = utils.steam.getHeaderImgUrlByAppid(appid) 22 | const iconBuffer = await utils.getImgUrlBuffer(icon) 23 | const msg = [] 24 | if (iconBuffer) { 25 | msg.push(segment.image(iconBuffer)) 26 | } 27 | msg.push(`当前在线人数: ${players}`) 28 | return msg 29 | } 30 | } 31 | } 32 | 33 | export const app = new App(appInfo, rule).create() 34 | -------------------------------------------------------------------------------- /apps/review.js: -------------------------------------------------------------------------------- 1 | import { App, Render } from '#components' 2 | import { api } from '#models' 3 | 4 | const appInfo = { 5 | id: 'review', 6 | name: '评论' 7 | } 8 | 9 | const rule = { 10 | review: { 11 | reg: App.getReg('(?:最新|热门)?评论\\s*(\\d*)'), 12 | cfg: { 13 | tips: true, 14 | appid: true 15 | }, 16 | fnc: async (e, { appid }) => { 17 | const data = await api.store.appreviews(appid, 20, e.msg.includes('最新')) 18 | const [, state, honor] = /data-tooltip-html="(.+?)">(.+?)<\//.exec(data.review_score) || [] 19 | return await Render.render('review/index', { 20 | review: data.html, 21 | state, 22 | honor, 23 | appid 24 | }) 25 | } 26 | } 27 | } 28 | 29 | export const app = new App(appInfo, rule).create() 30 | -------------------------------------------------------------------------------- /apps/rollGame.js: -------------------------------------------------------------------------------- 1 | import { utils, api } from '#models' 2 | import { Render, App, Config } from '#components' 3 | import _ from 'lodash' 4 | 5 | const appInfo = { 6 | id: 'rollGame', 7 | name: 'roll游戏' 8 | } 9 | 10 | const rule = { 11 | rollGame: { 12 | reg: App.getReg('(玩什么|玩啥|roll)(游戏)?\\s*(\\d*)'), 13 | cfg: { 14 | tips: true, 15 | steamId: true 16 | }, 17 | fnc: async (e, { steamId, uid }) => { 18 | const nickname = await utils.bot.getUserName(e.self_id, uid, e.group_id) || steamId 19 | const screenshotOptions = { 20 | title: '', 21 | games: [], 22 | desc: '' 23 | } 24 | 25 | const games = await api.IPlayerService.GetOwnedGames(steamId) 26 | if (!games.length) { 27 | return Config.tips.inventoryEmptyTips 28 | } 29 | 30 | const configCount = Config.other.rollGameCount 31 | const recomendCount = games.length >= configCount ? configCount : games.length 32 | screenshotOptions.games = _.sampleSize(games, recomendCount) 33 | screenshotOptions.title = `${nickname} 随机给您推荐了 ${recomendCount} 个游戏` 34 | 35 | screenshotOptions.games.map(i => { 36 | i.desc = `${getTime(i.playtime_forever)} ${i.playtime_2weeks ? `/ ${getTime(i.playtime_2weeks)}` : ''}` 37 | return i 38 | }) 39 | screenshotOptions.desc = '以下游戏从您的游戏库中通过完全随机的方式选出,不代表任何个人或团体的观点' 40 | return await Render.render('inventory/index', { 41 | data: [screenshotOptions] 42 | }) 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * 将游戏时长(单位:分)转换小时 49 | * @param {number} time 50 | * @returns {string} 51 | */ 52 | function getTime (time) { 53 | return (time / 60).toFixed(1) + 'h' 54 | } 55 | 56 | export const app = new App(appInfo, rule).create() 57 | -------------------------------------------------------------------------------- /apps/search.js: -------------------------------------------------------------------------------- 1 | import { api } from '#models' 2 | import { App, Render } from '#components' 3 | 4 | const appInfo = { 5 | id: 'search', 6 | name: '搜索' 7 | } 8 | 9 | const rule = { 10 | search: { 11 | reg: App.getReg('(?:搜索|search|查找|find)\\s*(.*)'), 12 | cfg: { 13 | tips: true 14 | }, 15 | fnc: async e => { 16 | const name = rule.search.reg.exec(e.msg)[1].trim() 17 | if (!name) { 18 | return '要搜什么?' 19 | } 20 | const result = await api.store.search(name) 21 | const games = result.split('').map(i => { 22 | if (!i.includes('appid')) { 23 | return null 24 | } 25 | const appid = i.match(/data-ds-appid="(\d+)"/)?.[1] 26 | const name = i.match(/class="match_name">(.*?)<\/div>/)?.[1] 27 | const price = i.match(/class="match_price">(.*?)<\/div>/)?.[1] 28 | const image = i.match(//)?.[1] 29 | return { 30 | appid, 31 | name, 32 | image, 33 | price: price 34 | ? { 35 | discount: 0, 36 | original: price 37 | } 38 | : null 39 | } 40 | }).filter(Boolean) 41 | if (!games.length) { 42 | return '没有搜索到相关的游戏, 换个关键词试试?' 43 | } 44 | const screenshotOptions = { 45 | title: `${name} 搜索结果`, 46 | games 47 | 48 | } 49 | return await Render.render('inventory/index', { data: [screenshotOptions] }) 50 | } 51 | } 52 | } 53 | 54 | export const app = new App(appInfo, rule).create() 55 | -------------------------------------------------------------------------------- /apps/wishlist.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | import { utils, api } from '#models' 4 | import { App, Config, Render } from '#components' 5 | 6 | const appInfo = { 7 | id: 'wishlist', 8 | name: '愿望单' 9 | } 10 | 11 | const rule = { 12 | list: { 13 | reg: App.getReg('愿望单\\s*(\\d*)'), 14 | cfg: { 15 | tips: true, 16 | steamId: true 17 | }, 18 | fnc: async (e, { steamId, uid }) => { 19 | const nickname = await utils.bot.getUserName(e.self_id, uid, e.group_id) || steamId 20 | const wishlist = await api.IWishlistService.GetWishlist(steamId) 21 | if (!wishlist.length) { 22 | return Config.tips.wishListEmptyTips 23 | } 24 | if (wishlist.length > Config.other.hiddenLength) { 25 | wishlist.length = Config.other.hiddenLength 26 | } 27 | // 愿望单没有给name, 尝试获取一下, 顺便也可以获取一下价格 获取失败超过3次就不再获取了 28 | // 2024年11月27日 已更新 有个api可以获取多个appid 仅url长度限制 29 | const appidsInfo = await api.IStoreBrowseService.GetItems(wishlist.map(i => i.appid), { 30 | include_assets: true 31 | }) 32 | const total = { 33 | price: 0, 34 | currency: '' 35 | } 36 | const games = wishlist.map(i => { 37 | const info = appidsInfo[i.appid] || { 38 | best_purchase_option: { 39 | formatted_original_price: '获取失败' 40 | } 41 | } 42 | const price = info.best_purchase_option?.formatted_final_price ? /[\d.]+/.exec(info.best_purchase_option?.formatted_final_price)?.[0] : '' 43 | if (price) { 44 | total.price += parseFloat(price) 45 | total.currency = info.best_purchase_option.formatted_final_price.replace(price, '') 46 | } 47 | return { 48 | ...i, 49 | name: info.name || i.appid, 50 | image: utils.steam.getHeaderImgUrlByAppid(i.appid, 'apps', info.assets?.header), 51 | desc: moment.unix(i.date_added).format('YYYY-MM-DD HH:mm:ss'), 52 | price: info.is_free 53 | ? { 54 | discount: 0, 55 | original: '免费' 56 | } 57 | : { 58 | discount: info.best_purchase_option?.discount_pct || 0, 59 | original: info.best_purchase_option?.formatted_original_price || info.best_purchase_option?.formatted_final_price || '即将推出', 60 | current: info.best_purchase_option?.formatted_final_price || '' 61 | } 62 | } 63 | }) 64 | const data = [{ 65 | title: `${nickname} 愿望单共有 ${wishlist.length} 个游戏`, 66 | desc: `清空愿望单需要: ${total.currency}${total.price.toFixed(2)}`, 67 | games: _.orderBy(games, 'date_added', 'desc') 68 | }] 69 | return await Render.render('inventory/index', { 70 | data 71 | }) 72 | } 73 | }, 74 | add: { 75 | reg: App.getReg('[添增]加愿望单\\s*(\\d*)'), 76 | cfg: { 77 | accessToken: true, 78 | appid: true 79 | }, 80 | fnc: async (e, { appid, accessToken }) => { 81 | await api.IWishlistService.AddToWishlist(accessToken, appid) 82 | return '已添加愿望单~' 83 | } 84 | }, 85 | remove: { 86 | reg: App.getReg('[删移][除出]愿望单\\s*(\\d*)'), 87 | cfg: { 88 | accessToken: true, 89 | appid: true 90 | }, 91 | fnc: async (e, { appid, accessToken }) => { 92 | await api.IWishlistService.RemoveFromWishlist(accessToken, appid) 93 | return '已移出愿望单~' 94 | } 95 | } 96 | } 97 | 98 | export const app = new App(appInfo, rule).create() 99 | -------------------------------------------------------------------------------- /apps/yearReview.js: -------------------------------------------------------------------------------- 1 | import { App } from '#components' 2 | import { segment } from '#lib' 3 | import { api, utils } from '#models' 4 | import _ from 'lodash' 5 | import moment from 'moment' 6 | 7 | const appInfo = { 8 | id: 'yearReview', 9 | name: '年度回顾' 10 | } 11 | 12 | const rule = { 13 | shareImage: { 14 | reg: App.getReg('年度回顾分享图片\\s*(\\d+|\\d+[-:\\s]\\d+)?'), 15 | cfg: { 16 | steamId: true 17 | }, 18 | fnc: async (e, { steamId }) => { 19 | const year = e.msg.match(/\d+/g)?.shift() || getYear() 20 | const images = await api.ISaleFeatureService.GetUserYearInReviewShareImage(steamId, year) 21 | const i = _.sample(images) 22 | if (!i) { 23 | return `年度回顾可见性未公开, 获取失败, 可前往\nhttps://store.steampowered.com/replay/${steamId}/${year}\n进行查看` 24 | } 25 | const path = utils.steam.getStaticUrl(i.url_path) 26 | const buffer = await utils.getImgUrlBuffer(path) 27 | if (buffer) { 28 | return segment.image(buffer) 29 | } else { 30 | return '图片获取失败,请稍后再试' 31 | } 32 | } 33 | } 34 | } 35 | 36 | function getYear () { 37 | const m = moment().month() 38 | const y = moment().year() 39 | return m < 11 ? y - 1 : y 40 | } 41 | 42 | export const app = new App(appInfo, rule).create() 43 | -------------------------------------------------------------------------------- /components/Render.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import _ from 'lodash' 3 | import { join } from 'path' 4 | import { logger, puppeteer } from '#lib' 5 | import template from 'art-template' 6 | import { canvas, info, utils } from '#models' 7 | import { Version, Config } from '#components' 8 | 9 | function scale (pct = 1) { 10 | const scale = Math.min(2, Math.max(0.5, Config.other.renderScale / 100)) 11 | pct = pct * scale 12 | return `style=transform:scale(${pct})` 13 | } 14 | 15 | const Render = { 16 | async render (path, params) { 17 | path = path.replace(/.html$/, '') 18 | const layoutPath = join(Version.pluginPath, 'resources', 'common', 'layout') 19 | const data = { 20 | tplFile: `${Version.pluginPath}/resources/${path}.html`, 21 | pluResPath: `${Version.pluginPath}/resources/`, 22 | saveId: path.split('/').pop(), 23 | imgType: 'jpeg', 24 | defaultLayout: join(layoutPath, 'default.html'), 25 | sys: { 26 | scale: scale(params.scale || 1), 27 | copyright: params.copyright || `Created By ${Version.BotName} v${Version.BotVersion} & ${Version.pluginName} v${Version.pluginVersion} ` 28 | }, 29 | pageGotoParams: { 30 | // waitUntil: 'networkidle0' // +0.5s 31 | waitUntil: 'load' 32 | }, 33 | ...params 34 | } 35 | if (path === 'inventory/index') { 36 | const hiddenLength = Config.other.hiddenLength 37 | const minLength = Math.min( 38 | Math.max(...params.data.map(i => i.games?.length || 0)), 39 | Math.max(1, Number(Config.other.itemLength) || 1) 40 | ) 41 | params.data = await Promise.all(params.data.map(async i => { 42 | if (!Array.isArray(i.desc)) i.desc = [i.desc].filter(Boolean) 43 | if (!i.games) i.games = [] 44 | if (i.games.length > hiddenLength) { 45 | const length = i.games.length - hiddenLength 46 | i.desc.push(`太多辣 ! 已隐藏${length}个项目`) 47 | i.games.length = hiddenLength 48 | } 49 | const infos = params.schinese ? await utils.steam.getGameSchineseInfo(i.games.map(g => g.appid)) : {} 50 | i.games = i.games.map(g => { 51 | const info = infos[g.appid] || {} 52 | if (!g.image && !g.noImg) { 53 | g.image = utils.steam.getHeaderImgUrlByAppid(g.appid) 54 | } 55 | if (info.name) { 56 | g.name = info.name 57 | if (g.image) { 58 | g.image = utils.steam.getHeaderImgUrlByAppid(info.appid, 'apps', info.header) 59 | } 60 | } 61 | return g 62 | }) 63 | return i 64 | })) 65 | const len = minLength === 1 ? 1.4 : minLength 66 | data.style = `` 67 | // 暂时只支持inventory/index 68 | if (Config.other.renderType == 2) { 69 | return canvas.inventory.render(params.data, minLength) 70 | } 71 | } else if (path === 'game/game') { 72 | params.data = params.data.map(i => _.sortBy(i.games, 'name')).flat() 73 | if (Config.other.renderType == 2) { 74 | return canvas.game.render(params.data) 75 | } else { 76 | return this.simpleRender(path, params) 77 | } 78 | } else if (path === 'info/index') { 79 | if (data.toGif) { 80 | data.tempPath = join(Version.pluginPath, 'temp', String(data.tempName || Date.now())).replace(/\\/g, '/') 81 | try { 82 | return await info.gif.render(data) 83 | } catch (error) { 84 | if (fs.existsSync(data.tempPath)) { 85 | fs.rmdirSync(data.tempPath, { recursive: true }) 86 | } 87 | data.toGif = false 88 | logger.error(error) 89 | // throw error 90 | } 91 | } 92 | if (Config.other.renderType == 2) { 93 | return await canvas.info.render(data) 94 | } 95 | } 96 | const img = await puppeteer.screenshot(`${Version.pluginName}/${path}`, data) 97 | if (img) { 98 | return img 99 | } else { 100 | return Config.tips.makeImageFailedTips 101 | } 102 | }, 103 | async simpleRender (path, params) { 104 | path = path.replace(/.html$/, '') 105 | const data = { 106 | tplFile: `${Version.pluginPath}/resources/${path}.html`, 107 | pluResPath: `${Version.pluginPath}/resources/`, 108 | saveId: path.split('/').pop(), 109 | imgType: 'jpeg', 110 | pageGotoParams: { 111 | waitUntil: 'load' 112 | }, 113 | ...params 114 | } 115 | const img = await puppeteer.screenshot(`${Version.pluginName}/${path}`, data) 116 | if (img) { 117 | return img 118 | } else { 119 | return '制作图片出错辣!再试一次吧' 120 | } 121 | }, 122 | tplFile (path, params, tempPath) { 123 | const name = path.split('/').pop() 124 | const tplPath = join(tempPath, name + '.html') 125 | const tmp = template.render(fs.readFileSync(params.tplFile, 'utf-8'), params) 126 | fs.writeFileSync(tplPath, tmp) 127 | return tplPath.replace(/\\/g, '/') 128 | } 129 | } 130 | 131 | export default Render 132 | -------------------------------------------------------------------------------- /components/Version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { fileURLToPath } from 'url' 3 | import { join, dirname, basename } from 'path' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | 7 | const __dirname = dirname(__filename) 8 | 9 | const BotPackage = JSON.parse(fs.readFileSync('package.json', 'utf8')) 10 | 11 | const pluginPath = join(__dirname, '..').replace(/\\/g, '/') 12 | 13 | const pluginName = basename(pluginPath) 14 | 15 | const pluginPackage = JSON.parse(fs.readFileSync(join(pluginPath, 'package.json'), 'utf8')) 16 | 17 | const pluginVersion = pluginPackage.version 18 | 19 | /** 20 | * @type {'Karin'|'Miao-Yunzai'|'Trss-Yunzai'|'Yunzai-Next'} 21 | */ 22 | const BotName = (() => { 23 | if (/^karin/i.test(pluginName)) { 24 | return 'Karin' 25 | } else if (BotPackage.dependencies.react) { 26 | fs.rmSync(pluginPath, { recursive: true }) 27 | return 'Yunzai-Next' 28 | } else if (Array.isArray(global.Bot?.uin)) { 29 | return 'Trss-Yunzai' 30 | } else if (BotPackage.dependencies.sequelize) { 31 | return 'Miao-Yunzai' 32 | } else { 33 | throw new Error('还有人玩Yunzai-Bot??') 34 | } 35 | })() 36 | 37 | const BotVersion = BotPackage.version 38 | 39 | const BotPath = join(pluginPath, '../..') 40 | 41 | export default { 42 | BotName, 43 | BotPath, 44 | BotVersion, 45 | pluginName, 46 | pluginPath, 47 | pluginVersion 48 | } 49 | -------------------------------------------------------------------------------- /components/YamlReader.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import YAML from 'yaml' 3 | 4 | /** 5 | * YamlReader类提供了对YAML文件的动态读写功能 6 | */ 7 | export default class YamlReader { 8 | /** 9 | * 创建一个YamlReader实例。 10 | * @param {string} filePath - 文件路径 11 | */ 12 | constructor (filePath) { 13 | this.filePath = filePath 14 | this.document = this.parseDocument() 15 | } 16 | 17 | /** 18 | * 解析YAML文件并返回Document对象,保留注释。 19 | * @returns {Document} 包含YAML数据和注释的Document对象 20 | */ 21 | parseDocument () { 22 | const fileContent = fs.readFileSync(this.filePath, 'utf8') 23 | return YAML.parseDocument(fileContent) 24 | } 25 | 26 | /** 27 | * 修改指定参数的值。 28 | * @param {string} key - 参数键名 29 | * @param {any} value - 新的参数值 30 | */ 31 | set (key, value) { 32 | const keys = key.split('.') 33 | const lastKey = keys.pop() 34 | let current = this.document 35 | // 遍历嵌套键名,直到找到最后一个键 36 | for (const key of keys) { 37 | if (!current.has(key)) { 38 | current.set(key, new YAML.YAMLMap()) 39 | } 40 | current = current.get(key) 41 | } 42 | // 设置最后一个键的值 43 | current.set(lastKey, value) 44 | this.write() 45 | } 46 | 47 | /** 48 | * 从YAML文件中删除指定参数。 49 | * @param {string} key - 要删除的参数键名 50 | */ 51 | rm (key) { 52 | const keys = key.split('.') 53 | const lastKey = keys.pop() 54 | let current = this.document 55 | // 遍历嵌套键名,直到找到最后一个键 56 | for (const key of keys) { 57 | if (current.has(key)) { 58 | current = current.get(key) 59 | } else { 60 | return // 如果键不存在,直接返回 61 | } 62 | } 63 | // 删除最后一个键 64 | current.delete(lastKey) 65 | this.write() 66 | } 67 | 68 | /** 69 | * 将更新后的Document对象写入YAML文件中。 70 | */ 71 | write () { 72 | fs.writeFileSync(this.filePath, this.document.toString(), 'utf8') 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | import App from './App.js' 2 | import Render from './Render.js' 3 | import Config from './Config.js' 4 | import Version from './Version.js' 5 | import YamlReader from './YamlReader.js' 6 | 7 | export { 8 | Version, 9 | YamlReader, 10 | Render, 11 | Config, 12 | App 13 | } 14 | -------------------------------------------------------------------------------- /config/default_config/gif.yaml: -------------------------------------------------------------------------------- 1 | # 渲染gif图片 !谨慎开启! 会短时间内截图多次, 可能导致服务器压力过大 2 | # 由puppeteer截图多张图片通过ffmpeg生成gif 3 | # 需要有全局安装ffmpeg 4 | 5 | # 转换成gif的模式 6 | # 1: 使用puppeteer多次截图 7 | # 2: 使用puppeteer-screen-recorder录制视频 8 | # 3: 使用canvas多次生成图片 不会使用下面的配置项 9 | gifMode: 1 10 | 11 | # 帧率 12 | frameRate: 20 13 | 14 | # 截图的数量 由多少张图片合成一张gif 15 | frameCount: 30 16 | # 每张截图之间的延迟 单位毫秒 17 | frameSleep: 50 18 | 19 | # 视频长度 单位秒 20 | videoLimit: 3 21 | 22 | # steam状态 迷你背景图 头像框 头像 23 | infoGif: false 24 | 25 | # 仅用于更新注释捏 26 | gifDoc: 1 27 | -------------------------------------------------------------------------------- /config/default_config/other.yaml: -------------------------------------------------------------------------------- 1 | # 图片渲染精度 2 | renderScale: 130 3 | 4 | # 生成图片方式 5 | # 1: puppeteer 6 | # 2: canvas (仅支持部分功能) 7 | renderType: 1 8 | 9 | # 截图时数量达到多少时隐藏剩余项目 10 | # 比如库存有几千个游戏,截图时只截取游玩最多的前99个,那么后面的游戏就隐藏了 11 | hiddenLength: 99 12 | 13 | # 截图时每行显示的最大数量 14 | itemLength: 3 15 | 16 | # 是否显示Steam头像 可能会有18+的头像 17 | steamAvatar: true 18 | 19 | # steam状态 发送消息模式 20 | # 1: 文字 21 | # 2: 仿steam风格的迷你卡片 22 | # 3: 原汁原味的steam风格的迷你卡片(需要社区反代或通用反代) 23 | infoMode: 2 24 | 25 | # 是否开启日志 26 | log: true 27 | 28 | # 游戏推荐数量 29 | rollGameCount: 3 30 | 31 | # 统计数据的显示数量 比如群统计 32 | statsCount: 10 33 | 34 | # 插件优先级 数值越小优先级越高 35 | priority: 5 36 | 37 | # 是否必须携带#号开头才会触发命令 重启后生效 38 | requireHashTag: false 39 | 40 | # 是否监听文件变化 如果不监听则每次更改文件需要重启 41 | # 改动此配置需重启后生效 42 | watchFile: true 43 | 44 | # 默认地区代码 45 | # 部分游戏有锁区中国地区会不可见 46 | # 排行榜,搜索等会使用这个地区的数据 47 | # 以及游戏金额等 48 | # 可通过下面这个接口查看部分地区代码 49 | # https://api.steampowered.com/IStoreTopSellersService/GetCountryList/v1/?language=schinese 50 | countryCode: CN 51 | -------------------------------------------------------------------------------- /config/default_config/push.yaml: -------------------------------------------------------------------------------- 1 | # 游玩推送总开关 2 | enable: true 3 | 4 | # 开始游戏后是否推送 5 | playStart: true 6 | 7 | # 游玩结束后是否推送 8 | playEnd: true 9 | 10 | # 状态推送总开关 11 | stateChange: true 12 | 13 | # 上线是否推送 14 | stateOnline: true 15 | 16 | # 下线是否推送 17 | stateOffline: true 18 | 19 | # 设置每次检查时请求的api 20 | 21 | # 1: ISteamUserOAuth/GetUserSummaries/v2 22 | # 此接口需要access_token 429情况未知 23 | # 和2接口参数返回值一样, 但是使用access_token鉴权 24 | # 需要有人扫码登录获取access_token后才可以调用 25 | 26 | # 2: ISteamUser/GetPlayerSummaries/v2 27 | # 此接口会有429限制, 经测试 40+steamid 3min 会出现 steamid越多越容易出现 28 | # 429 是根据apiKey进行限制, 可配置多个apiKey 29 | 30 | # 3: IPlayerService/GetPlayerLinkDetails/v1 31 | # 429情况暂时未知, 但是这个接口只会返回正在玩的appid不会返回name, 所以需要再请求一个接口获得游戏名 32 | 33 | # 4: 随机 34 | 35 | # tips: 依次进行获取 如果选择1出现429则尝试使用2 2出现429则尝试使用3 3出现429则停止尝试 36 | # 1如果没有access_token则跳过 37 | 38 | # more api please wait or issue/pr... 39 | pushApi: 2 40 | 41 | # 推送模式 42 | # 1: 文字推送 一条消息就是一个群友 xxx正在玩xxx 43 | # 2: 图片推送 一张图片展示所有群友 会展示游戏的header图片 44 | # 3: 仿steam风格的播报图片 只会展示头像和游戏名 不会展示游戏的header图片和时间 45 | pushMode: 1 46 | 47 | # Steam Web API 使用条款 48 | # https://steamcommunity.com/dev/apiterms 49 | # 其中说明: 每天对 Steam Web API 的调用次数限制为十万 (100,000) 次 50 | # 可以是cron表达式 也可以是数字 单位: 分钟 51 | time: 5 52 | 53 | # 是否开启家庭库存增加推送 54 | # 请注意: 55 | # 1. 不能批量查询 56 | # 2. 需要绑定先扫码登录获取access_token 57 | familyInventotyAdd: false 58 | 59 | # 家庭库存检查间隔 60 | # 可以是cron表达式 也可以是数字 单位: 分钟 61 | familyInventotyTime: 0 0 12 * * ? 62 | 63 | # 是否开启降价推送 64 | # 可选 0: 关闭降价推送 1: 扫码绑定后可开启降价推送 2: 所有用户都可开启降价推送 65 | priceChange: 0 66 | 67 | # 降价推送方式 68 | # 1: 仅降价期间第一次查询推送 2: 每次检查都推送 69 | priceChangeType: 1 70 | 71 | # 降价检查间隔 72 | # 可以是cron表达式 也可以是数字 单位: 分钟 73 | priceChangeTime: 0 5 12 * * ? 74 | 75 | # 是否开启用户库存增加推送 76 | # 可选 0: 关闭用户库存推送 1: 扫码绑定后可开启库存推送 2: 所有用户都可开启库存推送 77 | # 请注意: 78 | # 1. 不能批量查询 79 | # 2. 因为接口没有返回入库时间, 所以会缓存用户的库存 80 | userInventoryChange: 0 81 | 82 | # 用户库存检查间隔 83 | # 可以是cron表达式 也可以是数字 单位: 分钟 84 | userInventoryTime: 0 0 12 * * ? 85 | 86 | # 是否开启用户愿望单增加推送 87 | # 可选 0: 关闭用户愿望单推送 1: 扫码绑定后可开启愿望单推送 2: 所有用户都可开启愿望单推送 88 | # 请注意: 89 | # 1. 不能批量查询 90 | userWishlistChange: 0 91 | 92 | # 用户愿望单检查间隔 93 | # 可以是cron表达式 也可以是数字 单位: 分钟 94 | userWishlistTime: 0 0 12 * * ? 95 | 96 | # 是否默认开启游玩推送和状态推送 即绑定steamId之后不需要发 #steam开启游玩推送 和 #steam开启状态推送 指令 97 | defaultPush: true 98 | 99 | # 是否随机Bot进行推送, 有多个Bot在同一群群时随机选择一个在线的Bot推送状态 (仅限TRSS) 100 | randomBot: false 101 | 102 | # 是否缓存游戏的中文名 103 | # 需要单独请求一个接口获取游戏的中文名并缓存在数据库中 104 | cacheName: true 105 | 106 | # 群统计是否过滤掉黑名单群和白名单群 107 | # 如果关闭则每次会获取忽略黑白名单的所有群 但是不会推送 仅统计 108 | statusFilterGroup: true 109 | 110 | # 推送的Bot黑名单, 不开启推送的Bot将不会被推送, 比如腾讯QQBot限制主动消息 111 | blackBotList: 112 | - 3889000138 113 | 114 | # 推送的Bot白名单, 只推送白名单中的Bot 115 | whiteBotList: [] 116 | 117 | # 推送黑名单群 118 | blackGroupList: 119 | - 741577559 120 | 121 | # 推送白名单群 122 | whiteGroupList: [] 123 | -------------------------------------------------------------------------------- /config/default_config/steam.yaml: -------------------------------------------------------------------------------- 1 | # Steam Web API的apiKey 大部分功能都需要 2 | # https://partner.steamgames.com/doc/webapi_overview/auth 3 | apiKey: [] 4 | 5 | # proxy代理 6 | proxy: "" 7 | 8 | # api请求超时时间 单位: 秒 9 | timeout: 5 10 | 11 | # 优先使用通用, 再使用指定, 如果填了proxy, 则都会带上proxy请求 12 | # 通用反代 比如填写: https://example.com/{{url}} 则会替换 {{url}} 为实际请求的url 13 | commonProxy: "" 14 | 15 | # 主要用的接口 16 | # https://api.steampowered.com 的反代 会替换成对应地址 17 | apiProxy: "" 18 | 19 | # steam商城相关的接口 20 | # https://store.steampowered.com 的反代 会替换成对应地址 21 | storeProxy: "" 22 | 23 | # steam社区相关的接口 24 | # https://steamcommunity.com/ 的反代 会替换成对应地址 25 | communityProxy: "" 26 | -------------------------------------------------------------------------------- /config/default_config/tips.yaml: -------------------------------------------------------------------------------- 1 | # 若未配置则使用默认配置 2 | 3 | # 重复触发指令时的提示语 4 | repeatTips: "太快辣! 要受不了了🥵" 5 | 6 | # 正在查询的提示语 7 | loadingTips: "在查了...在查了..." 8 | 9 | # 未绑定steamId时的提示语 10 | noSteamIdTips: "还没有绑定steamId哦, 先绑定steamId吧" 11 | 12 | # 未绑定accessToken时的提示语 13 | noAccessTokenTips: "没有绑定accessToken哦, 先#steam扫码登录吧" 14 | 15 | # 重复绑定steamId时的提示语 16 | repeatBindTips: "这个steamId已经被绑定了, 要不要换一个?" 17 | 18 | # 未输入游戏appid时的提示语 19 | noAppidTips: "需要带上游戏的appid哦~" 20 | 21 | # 库存为空或未公开时的提示语 22 | inventoryEmptyTips: "获得成就: 没有给G胖一分钱" 23 | 24 | # 最近游玩为空或未公开时的提示语 25 | recentPlayEmptyTips: "最近电子阳痿了" 26 | 27 | # 愿望单为空或未公开时的提示语 28 | wishListEmptyTips: "愿望当场就实现了, 羡慕" 29 | 30 | # 在私聊中使用群聊限定的指令时的提示语 31 | privateUseTips: "请在群聊中使用此功能~" 32 | 33 | # 在群聊中使用私聊限定的指令时的提示语 34 | groupUseTips: "请在私聊中使用此功能~" 35 | 36 | # 没有开启对应推送功能时的提示语 {{type}} 对应的推送类型 37 | pushDisableTips: "主人没有开启{{type}}推送功能哦" 38 | 39 | # 用户尝试开关其他人的推送时提示语 {{type}} 对应的推送类型 40 | pushPermissionTips: "只能开启或关闭自己的{{type}}推送哦" 41 | 42 | # 开启或关闭推送的提示语 43 | # 可用的模版字符串 44 | # {{target}} 开启/关闭 45 | # {{type}} 推送类型 46 | # {{groupId}} 群号 47 | # {{userId}} 用户id 48 | # {{steamId}} 用户绑定的steamId 49 | pushChangeTips: "已{{target}}{{type}}推送{{steamId}}到{{groupId}}~" 50 | 51 | # 在推送黑名单中的提示语 52 | blackGroupTips: "本群在推送黑名单中, 请联系主人解除~" 53 | 54 | # 不在推送白名单中的提示语 55 | noWhiteGroupTips: "本群没有在推送白名单中, 请联系主人添加~" 56 | 57 | # 制作图片失败时的提示语 58 | makeImageFailedTips: "制作图片出错辣!再试一次吧" 59 | 60 | # 没有开启家庭库存推送时的提示语 61 | familyInventoryDisabledTips: "主人没有开启家庭库存推送功能哦" 62 | -------------------------------------------------------------------------------- /guoba.support.js: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash' 2 | import { Config } from '#components' 3 | import { setting } from '#models' 4 | 5 | export function supportGuoba () { 6 | return { 7 | pluginInfo: { 8 | name: 'steam-plugin', 9 | title: 'steam-plugin', 10 | author: '@小叶', 11 | authorLink: 'https://github.com/XasYer', 12 | link: 'https://github.com/XasYer/steam-plugin', 13 | isV3: true, 14 | isV2: false, 15 | description: '提供 steam 相关功能', 16 | icon: 'mdi:steam' 17 | }, 18 | configInfo: { 19 | schemas: setting.getGuobasChemas(), 20 | getConfigData () { 21 | const data = {} 22 | for (const file of Config.files) { 23 | const name = file.replace('.yaml', '') 24 | data[name] = Config.getDefOrConfig(name) 25 | } 26 | return data 27 | }, 28 | setConfigData (data, { Result }) { 29 | const config = Config.getCfg() 30 | 31 | for (const key in data) { 32 | const split = key.split('.') 33 | if (lodash.isEqual(config[split[1]], data[key])) continue 34 | Config.modify(split[0], split[1], data[key]) 35 | } 36 | return Result.ok({}, '𝑪𝒊𝒂𝒍𝒍𝒐~(∠・ω< )⌒★') 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * from './apps/index.js' 2 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "#components": [ 6 | "components/index.js" 7 | ], 8 | "#models": [ 9 | "models/index.js" 10 | ], 11 | "#lib": [ 12 | "lib/index.js" 13 | ] 14 | } 15 | }, 16 | "exclude": [ 17 | "**/node_modules/*", 18 | "**/resources/*", 19 | "**/temp/*", 20 | "**/data/*" 21 | ], 22 | "include": [ 23 | "./**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /lib/Bot.js: -------------------------------------------------------------------------------- 1 | import { Version } from '#components' 2 | 3 | const Bot = await (async () => { 4 | switch (Version.BotName) { 5 | case 'Karin': 6 | return (await import('node-karin')).default 7 | default: 8 | return global.Bot 9 | } 10 | })() 11 | 12 | export default Bot 13 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | import { Version } from '#components' 2 | 3 | const common = await (async () => { 4 | switch (Version.BotName) { 5 | case 'Karin': 6 | return (await import('node-karin')).common 7 | default: 8 | return (await import('../../../lib/common/common.js')).default 9 | } 10 | })() 11 | 12 | export default common 13 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import Bot from './Bot.js' 2 | import redis from './redis.js' 3 | import logger from './logger.js' 4 | import plugin from './plugin.js' 5 | import common from './common.js' 6 | import segment from './segment.js' 7 | import puppeteer from './puppeteer.js' 8 | 9 | export { 10 | Bot, 11 | redis, 12 | logger, 13 | plugin, 14 | common, 15 | segment, 16 | puppeteer 17 | } 18 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | import { Config, Version } from '#components' 2 | import chalk from 'chalk' 3 | 4 | const logger = await (async () => { 5 | switch (Version.BotName) { 6 | case 'Karin': 7 | return (await import('node-karin')).logger 8 | default: 9 | return global.logger 10 | } 11 | })() 12 | 13 | const getRandomHexColor = () => { 14 | const randomColor = Math.floor(Math.random() * 16777215).toString(16) 15 | return `#${randomColor.padStart(6, '0')}` 16 | } 17 | 18 | export default { 19 | ...logger, 20 | log: (level, ...logs) => logger[level](chalk.hex(getRandomHexColor())(`[${Version.pluginName}]`, ...logs)), 21 | info: (...logs) => logger[Config.other.log ? 'info' : 'debug'](chalk.hex(getRandomHexColor())(`[${Version.pluginName}]`, ...logs)), 22 | error: (...logs) => Config.other.log && logger.error(`[${Version.pluginName}]`, ...logs), 23 | debug: (...logs) => logger.debug(`[${Version.pluginName}]`, ...logs), 24 | warn: (...logs) => logger.warn(`[${Version.pluginName}]`, ...logs) 25 | } 26 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | import { Version } from '#components' 2 | 3 | const plugin = await (async () => { 4 | switch (Version.BotName) { 5 | case 'Karin': 6 | return (await import('node-karin')).Plugin 7 | default: 8 | return global.plugin 9 | } 10 | })() 11 | 12 | export default plugin 13 | -------------------------------------------------------------------------------- /lib/puppeteer.js: -------------------------------------------------------------------------------- 1 | import { segment } from '#lib' 2 | import { Version } from '#components' 3 | 4 | const puppeteer = await (async () => { 5 | switch (Version.BotName) { 6 | case 'Karin': { 7 | const Renderer = (await import('node-karin')).Renderer 8 | return { 9 | screenshot: async (path, options) => { 10 | options.data = { ...options } 11 | options.name = Version.pluginName + path 12 | options.file = options.tplFile 13 | options.type = options.imgType || 'jpeg' 14 | options.fileID = options.saveId 15 | options.screensEval = '#container' 16 | const img = await Renderer.render(options) 17 | return segment.image(img) 18 | } 19 | } 20 | } 21 | default: 22 | return (await import('../../../lib/puppeteer/puppeteer.js')).default 23 | } 24 | })() 25 | 26 | export default puppeteer 27 | -------------------------------------------------------------------------------- /lib/redis.js: -------------------------------------------------------------------------------- 1 | import { Version } from '#components' 2 | 3 | const redis = await (async () => { 4 | switch (Version.BotName) { 5 | case 'Karin': 6 | return (await import('node-karin')).redis 7 | default: 8 | return global.redis 9 | } 10 | })() 11 | 12 | export default redis 13 | -------------------------------------------------------------------------------- /lib/segment.js: -------------------------------------------------------------------------------- 1 | import { Version } from '#components' 2 | 3 | const segment = await (async () => { 4 | switch (Version.BotName) { 5 | case 'Karin': 6 | return (await import('node-karin')).segment 7 | default: 8 | return global.segment 9 | } 10 | })() 11 | 12 | export default segment 13 | -------------------------------------------------------------------------------- /models/api/IAccountCartService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 添加游戏到购物车 5 | * @param {string} accessToken 6 | * @param {number|{packageid?: number,bundleid?: number}} packageid 7 | * @param {string} country 用户地区代码 8 | * @returns {Promise<{ 9 | * line_item_ids: string[] 10 | * cart: { 11 | * line_items: { 12 | * line_item_id: string 13 | * type: number 14 | * packageid: number 15 | * is_valid: boolean 16 | * time_added: number 17 | * price_when_added: { 18 | * amount_in_cents: string 19 | * currency_code: number 20 | * formatted_amount: string 21 | * } 22 | * flags: { 23 | * is_gift: boolean 24 | * is_private: boolean 25 | * } 26 | * }[] 27 | * subtotal: { 28 | * amount_in_cents: string 29 | * currency_code: number 30 | * formatted_amount: string 31 | * } 32 | * is_valid: boolean 33 | * } 34 | * }>} 35 | */ 36 | export async function AddItemsToCart (accessToken, packageid, country = 'CN') { 37 | const input = { 38 | items: [typeof packageid !== 'object' ? { packageid } : packageid], 39 | user_country: country, 40 | navdata: { 41 | domain: 'store.steampowered.com', 42 | controller: 'search', 43 | method: 'search', 44 | submethod: 'query', 45 | feature: '', 46 | depth: 0, 47 | countrycode: country, 48 | is_client: false, 49 | curator_data: {}, 50 | is_likely_bot: false, 51 | is_utm: false 52 | } 53 | } 54 | return utils.request.post('IAccountCartService/AddItemsToCart/v1', { 55 | params: { 56 | access_token: accessToken, 57 | input_json: JSON.stringify(input) 58 | } 59 | }).then(res => res.response) 60 | } 61 | 62 | /** 63 | * 清空购物车 64 | * @param {string} accessToken 65 | * @returns {Promise} 66 | */ 67 | export async function DeleteCart (accessToken) { 68 | return utils.request.post('IAccountCartService/DeleteCart/v1', { 69 | params: { 70 | access_token: accessToken 71 | } 72 | }).then(res => undefined) 73 | } 74 | 75 | /** 76 | * 查看购物车 77 | * @param {string} accessToken 78 | * @param {string} country 用户地区代码 79 | * @returns {Promise<{ 80 | * line_items?: { 81 | * line_item_id: string 82 | * type: number 83 | * packageid?: number 84 | * bundleid?: number 85 | * is_valid: boolean 86 | * time_added: number 87 | * price_when_added: { 88 | * amount_in_cents: string 89 | * currency_code: number 90 | * formatted_amount: string 91 | * } 92 | * flags: { 93 | * is_gift: boolean 94 | * is_private: boolean 95 | * } 96 | * }[] 97 | * subtotal: { 98 | * amount_in_cents: string 99 | * currency_code: number 100 | * formatted_amount: string 101 | * } 102 | * is_valid: boolean 103 | * }>} 104 | */ 105 | export async function GetCart (accessToken, country = 'CN') { 106 | return utils.request.get('IAccountCartService/GetCart/v1', { 107 | params: { 108 | access_token: accessToken, 109 | user_country: country 110 | } 111 | }).then(res => res.response.cart) 112 | } 113 | 114 | /** 115 | * 删除购物车某一项 116 | * @param {string} accessToken 117 | * @param {string} lineItemId 118 | * @param {string} country 用户地区代码 119 | * @returns {Promise<{ 120 | * line_items: { 121 | * line_item_id: string 122 | * type: number 123 | * packageid?: number 124 | * bundleid?: number 125 | * is_valid: boolean 126 | * time_added: number 127 | * price_when_added: { 128 | * amount_in_cents: string 129 | * currency_code: number 130 | * formatted_amount: string 131 | * } 132 | * flags: { 133 | * is_gift: boolean 134 | * is_private: boolean 135 | * } 136 | * }[] 137 | * subtotal: { 138 | * amount_in_cents: string 139 | * currency_code: number 140 | * formatted_amount: string 141 | * } 142 | * is_valid: boolean 143 | * }>} 144 | */ 145 | export async function RemoveItemFromCart (accessToken, lineItemId, country = 'CN') { 146 | return utils.request.post('IAccountCartService/RemoveItemFromCart/v1', { 147 | params: { 148 | access_token: accessToken, 149 | line_item_id: lineItemId, 150 | user_country: country 151 | } 152 | }).then(res => res.response.cart) 153 | } 154 | -------------------------------------------------------------------------------- /models/api/IAccountPrivateAppsService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 获得私密应用列表 5 | * @param {string} accessToken 6 | * @returns {Promise} 7 | */ 8 | export async function GetPrivateAppList (accessToken) { 9 | return utils.request.get('IAccountPrivateAppsService/GetPrivateAppList/v1', { 10 | params: { 11 | access_token: accessToken 12 | } 13 | }).then(res => res.response.private_apps?.appids || []) 14 | } 15 | 16 | /** 17 | * 添加或删除私密应用 18 | * @param {string} accessToken 19 | * @param {number[]} appids 20 | * @param {boolean} flag 21 | * @returns {Promise} 22 | */ 23 | export async function ToggleAppPrivacy (accessToken, appids, flag = true) { 24 | !Array.isArray(appids) && (appids = [appids]) 25 | const input = { 26 | private: flag, 27 | appids 28 | } 29 | await utils.request.post('IAccountPrivateAppsService/ToggleAppPrivacy/v1', { 30 | params: { 31 | access_token: accessToken, 32 | input_json: JSON.stringify(input) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /models/api/IAuthenticationService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 开始扫码登录 5 | * @returns {Promise<{ 6 | * client_id: string, 7 | * challenge_url: string, 8 | * request_id: string, 9 | * interval: number, 10 | * allowed_confirmations: { 11 | * confirmation_type: number 12 | * }[], 13 | * version: number 14 | * }>} 15 | */ 16 | export async function BeginAuthSessionViaQR () { 17 | const input = { 18 | device_details: { 19 | device_friendly_name: 'Xiaomi 15 Pro', 20 | platform_type: 3, 21 | os_type: -500, 22 | gaming_device_type: 528 23 | }, 24 | website_id: 'Mobile' 25 | } 26 | return utils.request.post('IAuthenticationService/BeginAuthSessionViaQR/v1', { 27 | params: { 28 | input_json: JSON.stringify(input), 29 | key: undefined 30 | } 31 | }).then(res => res.response) 32 | } 33 | 34 | /** 35 | * 列举已登录的账号 36 | * @param {string} accessToken 37 | * @returns {Promise<{ 38 | * refresh_tokens: { 39 | * token_id: string, 40 | * token_description: string, 41 | * time_updated: number, 42 | * platform_type: number, 43 | * logged_in: boolean, 44 | * os_platform: number, 45 | * auth_type: number, 46 | * gaming_device_type: number, 47 | * first_seen: { 48 | * time: number, 49 | * }, 50 | * last_seen: { 51 | * time: number, 52 | * ip: { 53 | * v4: number 54 | * } 55 | * country: string, 56 | * state: string, 57 | * city: string, 58 | * }, 59 | * os_type: number, 60 | * authentication_type: number, 61 | * }[] 62 | * requesting_token: string 63 | * }>} 64 | */ 65 | export async function EnumerateTokens (accessToken) { 66 | return utils.request.post('IAuthenticationService/EnumerateTokens/v1', { 67 | params: { 68 | access_token: accessToken, 69 | key: undefined 70 | } 71 | }).then(res => res.response) 72 | } 73 | 74 | /** 75 | * 刷新access_token 76 | * @param {string} refreshToken 77 | * @param {string} steamId 78 | * @returns {Promise<{ 79 | * access_token?: string, 80 | * }>} 81 | */ 82 | export async function GenerateAccessTokenForApp (refreshToken, steamId) { 83 | return utils.request.post('IAuthenticationService/GenerateAccessTokenForApp/v1', { 84 | params: { 85 | refresh_token: refreshToken, 86 | steamid: steamId, 87 | renewal_type: 0, 88 | key: undefined 89 | } 90 | }).then(res => res.response) 91 | } 92 | 93 | /** 94 | * 查询扫码登录结果 95 | * @param {string} clientId 96 | * @param {string} requestId 97 | * @returns {Promise<{ 98 | * had_remote_interaction: boolean, 99 | * refresh_token?: string, 100 | * access_token?: string, 101 | * account_name?: string, 102 | * }>} 103 | */ 104 | export async function PollAuthSessionStatus (clientId, requestId) { 105 | return utils.request.post('IAuthenticationService/PollAuthSessionStatus/v1', { 106 | params: { 107 | client_id: clientId, 108 | request_id: requestId, 109 | token_to_revoke: 0, 110 | key: undefined 111 | } 112 | }).then(res => res.response) 113 | } 114 | -------------------------------------------------------------------------------- /models/api/ICheckoutService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 添加免费游戏入库 5 | * @param {string} accessToken 6 | * @param {number} appid 7 | * @returns {Promise<{ 8 | * appids_added?: number[], 9 | * packageids_added?: number[], 10 | * purchase_result_detail?: number 11 | * }>} 12 | */ 13 | export async function AddFreeLicense (accessToken, appid) { 14 | const input = { 15 | item_id: { 16 | appid 17 | } 18 | } 19 | return utils.request.post('ICheckoutService/AddFreeLicense/v1', { 20 | params: { 21 | access_token: accessToken, 22 | input_json: JSON.stringify(input) 23 | } 24 | }).then(res => res.response) 25 | } 26 | 27 | /** 28 | * GetFriendOwnershipForGifting 29 | * @param {string} accessToken 30 | * @param {number[]} appids 31 | * @returns {Promise<{ 32 | * item_id: { 33 | * appid: number 34 | * }[], 35 | * friend_ownership: { 36 | * accountid: number, 37 | * already_owns: boolean, 38 | * }[] 39 | * }[]>} 40 | */ 41 | export async function GetFriendOwnershipForGifting (accessToken, appids) { 42 | !Array.isArray(appids) && (appids = [appids]) 43 | const input = { 44 | item_ids: appids.map(appid => ({ appid })) 45 | } 46 | return utils.request.get('ICheckoutService/GetFriendOwnershipForGifting/v1', { 47 | params: { 48 | access_token: accessToken, 49 | input_json: JSON.stringify(input) 50 | } 51 | }).then(res => res.response?.ownership_info || []) 52 | } 53 | 54 | /** 55 | * 验证购物车 56 | * @param {string} accessToken 57 | * @param {string} country 用户地区代码 58 | * @returns {Promise} 59 | */ 60 | export async function ValidateCart (accessToken, country = 'CN') { 61 | const input = { 62 | gidshoppingcart: '0', 63 | context: { 64 | language: 'schinese', 65 | elanguage: 0, 66 | country_code: country, 67 | steam_realm: 1 68 | }, 69 | data_request: { 70 | include_assets: true, 71 | include_release: true, 72 | include_platforms: true, 73 | include_all_purchase_options: false, 74 | include_screenshots: false, 75 | include_trailers: false, 76 | include_ratings: false, 77 | include_tag_count: 0, 78 | include_reviews: false, 79 | include_basic_info: true, 80 | include_supported_languages: false, 81 | include_full_description: false, 82 | include_included_items: true, 83 | included_item_data_request: { 84 | include_assets: true, 85 | include_release: true, 86 | include_platforms: true, 87 | include_all_purchase_options: false, 88 | include_screenshots: false, 89 | include_trailers: false, 90 | include_ratings: false, 91 | include_tag_count: 0, 92 | include_reviews: false, 93 | include_basic_info: true, 94 | include_supported_languages: false, 95 | include_full_description: false, 96 | include_included_items: false, 97 | included_item_data_request: null, 98 | include_assets_without_overrides: false, 99 | apply_user_filters: false, 100 | include_links: false 101 | }, 102 | include_assets_without_overrides: false, 103 | apply_user_filters: false, 104 | include_links: false 105 | }, 106 | gift_info: null, 107 | gidreplayoftransid: '0', 108 | for_init_purchase: false 109 | } 110 | return utils.request.get('ICheckoutService/ValidateCart/v1', { 111 | params: { 112 | access_token: accessToken, 113 | input_json: JSON.stringify(input) 114 | } 115 | }).then(res => res.response) 116 | } 117 | -------------------------------------------------------------------------------- /models/api/ICommunityService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 获取社区信息 5 | * @param {number} appid 6 | * @returns {Promise<{ 7 | * appid: number 8 | * name: string 9 | * icon: string 10 | * community_visible_stats: boolean 11 | * propagation: string 12 | * app_type: number, 13 | * content_descriptorids?: number[] 14 | * }[]>} 15 | */ 16 | export async function GetApps (appids) { 17 | !Array.isArray(appids) && (appids = [appids]) 18 | const input = { 19 | language: 6, 20 | appids 21 | } 22 | return await utils.request.get('ICommunityService/GetApps/v1', { 23 | params: { 24 | input_json: JSON.stringify(input) 25 | } 26 | }).then(res => res.response.apps || []) 27 | } 28 | 29 | /** 30 | * 获取历史头像 31 | * @param {string} accessToken 32 | * @param {string} steamid 33 | * @returns {Promise} 34 | */ 35 | export async function GetAvatarHistory (accessToken, steamid) { 36 | return await utils.request.post('ICommunityService/GetAvatarHistory/v1', { 37 | params: { 38 | access_token: accessToken, 39 | steamid 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /models/api/IFriendsListService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * GetFavorites 5 | * @param {string} accessToken 6 | * @returns {Promise} 7 | */ 8 | export async function GetFavorites (accessToken) { 9 | return utils.request.get('IFriendsListService/GetFavorites/v1', { 10 | params: { 11 | access_token: accessToken 12 | } 13 | }) 14 | } 15 | 16 | /** 17 | * 获得好友列表 18 | * @param {string} accessToken 19 | * @returns {Promise<{ 20 | * bincremental: boolean, 21 | * friends: { 22 | * ulfriendid: string, 23 | * efriendrelationship: number, 24 | * } 25 | * }>} 26 | */ 27 | export async function GetFriendsList (accessToken) { 28 | return utils.request.get('IFriendsListService/GetFriendsList/v1', { 29 | params: { 30 | access_token: accessToken 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /models/api/IMobileAppService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * GetMobileSummary 5 | * @param {string} accessToken 6 | * @param {number} authenticatorGid 7 | * @returns {Promise} 8 | */ 9 | export async function GetMobileSummary (accessToken, authenticatorGid) { 10 | return utils.request.post('IMobileAppService/GetMobileSummary/v1', { 11 | params: { 12 | access_token: accessToken, 13 | authenticator_gid: authenticatorGid 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /models/api/ISteamChartsService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | import { Config } from '#components' 3 | 4 | /** 5 | * GetBestOfYearPages 6 | * @returns {Promise<{ 7 | * name: string, 8 | * url_path: string, 9 | * banner_url: string[] 10 | * }[]>} 11 | */ 12 | export async function GetBestOfYearPages () { 13 | return utils.request.get('ISteamChartsService/GetBestOfYearPages/v1') 14 | .then(res => res.response.pages) 15 | } 16 | 17 | /** 18 | * 获取steam当前热玩排行榜 19 | * @returns {Promise<{ 20 | * last_update: number, 21 | * ranks: { 22 | * rank: number, 23 | * appid: number, 24 | * concurrent_in_game: number, 25 | * peak_in_game: number, 26 | * item: { 27 | * name: string, 28 | * is_free: boolean, 29 | * best_purchase_option?: { 30 | * formatted_final_price: string, 31 | * formatted_original_price: string, 32 | * discount_pct?: number, 33 | * } 34 | * } 35 | * }[] 36 | * }>} 37 | */ 38 | export async function GetGamesByConcurrentPlayers () { 39 | const input = { 40 | country_code: Config.other.countryCode, 41 | context: { 42 | language: 'schinese', 43 | country_code: Config.other.countryCode 44 | }, 45 | data_request: { 46 | include_basic_info: true 47 | } 48 | } 49 | return utils.request.get('ISteamChartsService/GetGamesByConcurrentPlayers/v1', { 50 | params: { 51 | input_json: JSON.stringify(input) 52 | } 53 | }).then(res => res.response) 54 | } 55 | 56 | /** 57 | * 获取steam每日热玩排行榜 58 | * @returns {Promise<{ 59 | * rollup_date: number, 60 | * ranks: { 61 | * rank: number, 62 | * appid: number, 63 | * last_week_rank: number, 64 | * peak_in_game: number, 65 | * item: { 66 | * name: string, 67 | * is_free: boolean, 68 | * best_purchase_option?: { 69 | * formatted_final_price: string, 70 | * formatted_original_price: string, 71 | * discount_pct?: number, 72 | * } 73 | * } 74 | * }[] 75 | * }>} 76 | */ 77 | export async function GetMostPlayedGames () { 78 | const input = { 79 | context: { 80 | language: 'schinese', 81 | country_code: Config.other.countryCode 82 | }, 83 | data_request: { 84 | include_basic_info: true 85 | } 86 | } 87 | return utils.request.get('ISteamChartsService/GetMostPlayedGames/v1', { 88 | params: { 89 | input_json: JSON.stringify(input) 90 | } 91 | }).then(res => res.response) 92 | } 93 | 94 | /** 95 | * 获取steam最热新品 (按月统计) 96 | * @returns {Promise<{ 97 | * name: string, 98 | * start_of_month: number, 99 | * url_path: string, 100 | * item_ids: { 101 | * appid: number, 102 | * }[] 103 | * }[]>} 104 | */ 105 | export async function GetTopReleasesPages () { 106 | return utils.request.get('ISteamChartsService/GetTopReleasesPages/v1') 107 | .then(res => res.response.pages) 108 | } 109 | -------------------------------------------------------------------------------- /models/api/ISteamUser.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { utils } from '#models' 3 | 4 | /** 5 | * 获取用户相关信息 6 | * @param {string|string[]} steamIds 7 | * @returns {Promise<{ 8 | * steamid: string, 9 | * communityvisibilitystate: number, 10 | * profilestate: number, 11 | * personaname: string, 12 | * profileurl: string, 13 | * avatar: string, 14 | * avatarmedium: string, 15 | * avatarfull: string, 16 | * lastlogoff?: number, 17 | * personastate: number, 18 | * timecreated: string, 19 | * gameid?: string, 20 | * gameextrainfo?: string, 21 | * }[]>} 22 | */ 23 | export async function GetPlayerSummaries (steamIds) { 24 | !Array.isArray(steamIds) && (steamIds = [steamIds]) 25 | const result = [] 26 | // 一次只能获取100个用户信息 27 | for (const items of _.chunk(steamIds, 100)) { 28 | const res = await utils.request.get('ISteamUser/GetPlayerSummaries/v2', { 29 | params: { 30 | steamIds: items.join(',') 31 | } 32 | }) 33 | if (res.response?.players?.length) { 34 | result.push(...res.response.players) 35 | } 36 | } 37 | return result 38 | } 39 | 40 | /** 41 | * 获取好友列表 42 | * @param {string} steamid 43 | * @returns {Promise<{ 44 | * steamid: string, 45 | * relationship: string, 46 | * friend_since: number 47 | * }[]>} 48 | */ 49 | export async function GetFriendList (steamid) { 50 | return await utils.request.get('ISteamUser/GetFriendList/v1', { 51 | params: { 52 | steamid 53 | } 54 | }).then(res => res.friendslist.friends) 55 | } 56 | 57 | /** 58 | * 获取群组列表 59 | * @param {string} steamid 60 | * @returns {Promise<{ 61 | * gid: string, 62 | * }[]>} 63 | */ 64 | export async function GetUserGroupList (steamid) { 65 | return await utils.request.get('ISteamUser/GetUserGroupList/v1', { 66 | params: { 67 | steamid 68 | } 69 | }).then(res => res.response?.groups || []) 70 | } 71 | 72 | /** 73 | * 获取用户封禁信息 74 | * @param {string|string[]} steamIds 75 | * @returns {Promise<{ 76 | * SteamId: string, 77 | * CommunityBanned: boolean, 78 | * VACBanned: boolean, 79 | * NumberOfVACBans: number, 80 | * DaysSinceLastBan: number, 81 | * NumberOfGameBans: number, 82 | * EconomyBan: string, 83 | * }[]>} 84 | */ 85 | export async function GetPlayerBans (steamIds) { 86 | !Array.isArray(steamIds) && (steamIds = [steamIds]) 87 | const result = [] 88 | for (const items of _.chunk(steamIds, 100)) { 89 | const res = await utils.request.get('ISteamUser/GetPlayerBans/v1', { 90 | params: { 91 | steamIds: items.join(',') 92 | } 93 | }) 94 | result.push(...res.players) 95 | } 96 | return result 97 | } 98 | -------------------------------------------------------------------------------- /models/api/ISteamUserOAuth.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { utils } from '#models' 3 | 4 | /** 5 | * 获取用户相关信息 6 | * @param {string} accessToken 7 | * @param {string|string[]} steamIds 8 | * @returns {Promise<{ 9 | * steamid: string, 10 | * communityvisibilitystate: number, 11 | * profilestate: number, 12 | * personaname: string, 13 | * profileurl: string, 14 | * avatar: string, 15 | * avatarmedium: string, 16 | * avatarfull: string, 17 | * lastlogoff?: number, 18 | * personastate: number, 19 | * timecreated: string, 20 | * gameid?: string, 21 | * gameextrainfo?: string, 22 | * }[]>} 23 | */ 24 | export async function GetUserSummaries (accessToken, steamIds) { 25 | !Array.isArray(steamIds) && (steamIds = [steamIds]) 26 | const result = [] 27 | // 一次只能获取100个用户信息 28 | for (const items of _.chunk(steamIds, 100)) { 29 | const res = await utils.request.get('ISteamUserOAuth/GetUserSummaries/v2', { 30 | params: { 31 | access_token: accessToken, 32 | steamIds: items.join(',') 33 | } 34 | }) 35 | if (res.players?.length) { 36 | result.push(...res.players) 37 | } 38 | } 39 | return result 40 | } 41 | -------------------------------------------------------------------------------- /models/api/ISteamUserStats.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 获取指定游戏的全局成就百分比 5 | * @param {string} appid 6 | * @returns {Promise<{ 7 | * name: string, 8 | * percent: number 9 | * }[]>} 10 | */ 11 | export async function GetGlobalAchievementPercentagesForApp (appid) { 12 | return utils.request.get('ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2', { 13 | params: { 14 | gameid: appid 15 | } 16 | }).then(res => res.achievementpercentages?.achievements || []) 17 | } 18 | 19 | /** 20 | * 获取指定游戏当前在 Steam 上的活跃玩家的总数量 21 | * @param {string} appid 22 | * @returns {Promise} 23 | */ 24 | export async function GetNumberOfCurrentPlayers (appid) { 25 | return utils.request.get('ISteamUserStats/GetNumberOfCurrentPlayers/v1', { 26 | params: { 27 | appid 28 | } 29 | }).then(res => res.response.player_count ?? false) 30 | } 31 | 32 | /** 33 | * 获取游戏成就总览 34 | * @param {string} appid 35 | * @returns {Promise<{ 36 | * gameName: string, 37 | * gameVersion: string, 38 | * availableGameStats?: { 39 | * achievements?: { 40 | * name: string, 41 | * defaultvalue: number, 42 | * displayName: string, 43 | * hidden: number, 44 | * description: string, 45 | * icon: string, 46 | * icongray: string 47 | * }[], 48 | * stats?: { 49 | * name: string, 50 | * defaultvalue: number, 51 | * displayName: string, 52 | * }[] 53 | * } 54 | * }>} 55 | */ 56 | export async function GetSchemaForGame (appid) { 57 | return utils.request.get('ISteamUserStats/GetSchemaForGame/v2', { 58 | params: { 59 | appid 60 | } 61 | }).then(res => res.game || {}) 62 | } 63 | 64 | /** 65 | * 获取用户游戏成就数据 66 | * @param {string} appid 67 | * @param {string} steamid 68 | * @returns {Promise<{ 69 | * steamID: string, 70 | * gameName: string, 71 | * achievements?: { 72 | * name: string, 73 | * achieved: number, 74 | * }[], 75 | * stats?: { 76 | * name: string, 77 | * value: number, 78 | * }[] 79 | * }>} 80 | */ 81 | export async function GetUserStatsForGame (appid, steamid) { 82 | return utils.request.get('ISteamUserStats/GetUserStatsForGame/v2', { 83 | params: { 84 | appid, 85 | steamid 86 | } 87 | }).then(res => res.playerstats || {}) 88 | } 89 | -------------------------------------------------------------------------------- /models/api/ISteamWebAPIUtil.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * GetServerInfo 5 | * @returns {Promise<{ 6 | * servertime: number, 7 | * servertimestring: string, 8 | * }>} 9 | */ 10 | export async function GetServerInfo () { 11 | return utils.request.get('ISteamWebAPIUtil/GetServerInfo/v1') 12 | } 13 | -------------------------------------------------------------------------------- /models/api/IStoreQueryService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | import { Config } from '#components' 3 | 4 | /** 5 | * 查询热销排行 6 | * @returns {Promise<{ 7 | * appid: number, 8 | * name: string, 9 | * is_free: boolean, 10 | * best_purchase_option?: { 11 | * formatted_final_price: string, 12 | * formatted_original_price: string, 13 | * discount_pct?: number, 14 | * } 15 | * }[]>} 16 | */ 17 | export async function Query () { 18 | const input = { 19 | query_name: 'SteamCharts Live Top Sellers', 20 | context: { 21 | language: 'schinese', 22 | country_code: Config.other.countryCode 23 | }, 24 | query: { 25 | start: 0, 26 | count: 100, 27 | sort: 10, 28 | filters: { 29 | type_filters: { 30 | include_apps: true 31 | } 32 | } 33 | }, 34 | data_request: { 35 | include_basic_info: true 36 | }, 37 | overrideCountryCode: Config.other.countryCode 38 | } 39 | return utils.request.get('IStoreQueryService/Query/v1', { 40 | params: { 41 | input_json: JSON.stringify(input) 42 | } 43 | }).then(res => res.response.store_items || []) 44 | } 45 | -------------------------------------------------------------------------------- /models/api/IStoreService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 获得探索队列 5 | * @param {string} accessToken 6 | * @param {string} country 用户地区代码 7 | * @returns {Promise<{ 8 | * appids: number[], 9 | * country_code: string, 10 | * settings: any, 11 | * skipped: number, 12 | * exhausted: boolean 13 | * experimental_cohort: number, 14 | * }>} 15 | */ 16 | export async function GetDiscoveryQueue (accessToken, country = 'CN') { 17 | const input = { 18 | queue_type: 0, 19 | country_code: country, 20 | rebuild_queue: true, 21 | rebuild_queue_if_stale: true 22 | } 23 | return utils.request.get('IStoreService/GetDiscoveryQueue/v1', { 24 | params: { 25 | access_token: accessToken, 26 | input_json: JSON.stringify(input) 27 | } 28 | }).then(res => res.response) 29 | } 30 | 31 | /** 32 | * 获得探索队列的设置 33 | * @param {string} accessToken 34 | * @returns {Promise} 35 | */ 36 | export async function GetDiscoveryQueueSettings (accessToken) { 37 | const input = { 38 | queue_type: 0 39 | } 40 | return utils.request.get('IStoreService/GetDiscoveryQueueSettings/v1', { 41 | params: { 42 | access_token: accessToken, 43 | input_json: JSON.stringify(input) 44 | } 45 | }).then(res => res.response) 46 | } 47 | 48 | /** 49 | * 获得探索队列中已跳过的游戏 50 | * @param {string} accessToken 51 | * @param {number} steamid 52 | * @returns {Promise} 53 | */ 54 | export async function GetDiscoveryQueueSkippedApps (accessToken, steamid) { 55 | const input = { 56 | queue_type: 0, 57 | steamid 58 | } 59 | return utils.request.get('IStoreService/GetDiscoveryQueueSkippedApps/v1', { 60 | params: { 61 | access_token: accessToken, 62 | input_json: JSON.stringify(input) 63 | } 64 | }).then(res => res.response.appids || []) 65 | } 66 | 67 | /** 68 | * 获取不同语言的tag名称 69 | * @param {number[]} tagids 70 | * @returns {Promise<{ 71 | * tagid: number, 72 | * english_name: string, 73 | * name: string, 74 | * normalized_name: string, 75 | * }[]>} 76 | */ 77 | export async function GetLocalizedNameForTags (tagids) { 78 | !Array.isArray(tagids) && (tagids = [tagids]) 79 | const input = { 80 | tagids, 81 | language: 'schinese' 82 | } 83 | return utils.request.get('IStoreService/GetLocalizedNameForTags/v1', { 84 | params: { 85 | input_json: JSON.stringify(input) 86 | } 87 | }).then(res => res.response.tags || []) 88 | } 89 | 90 | /** 91 | * 获取所有带有本地化名称的白名单标签。 92 | * @returns {Promise<{ 93 | * tagid: number, 94 | * name: string, 95 | * }[]>} 96 | */ 97 | export async function GetMostPopularTags () { 98 | return utils.request.get('IStoreService/GetMostPopularTags/v1').then(res => res.response.tags || []) 99 | } 100 | 101 | /** 102 | * 获得tag列表 103 | * @returns {Promise<{ 104 | * version_hash: string, 105 | * tags: { 106 | * tagid: number, 107 | * name: string, 108 | * }[] 109 | * }>} 110 | */ 111 | export async function GetTagList () { 112 | return utils.request.get('IStoreService/GetTagList/v1') 113 | .then(res => res.response) 114 | } 115 | 116 | /** 117 | * 跳过探索队列中的某一项 118 | * @param {string} accessToken 119 | * @param {number} appid 120 | * @returns {Promise} 121 | */ 122 | export async function SkipDiscoveryQueueItem (accessToken, appid) { 123 | const input = { 124 | queue_type: 0, 125 | appid 126 | } 127 | return utils.request.post('IStoreService/SkipDiscoveryQueueItem/v1', { 128 | params: { 129 | access_token: accessToken, 130 | input_json: JSON.stringify(input) 131 | } 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /models/api/IStoreTopSellersService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | import { Config } from '#components' 3 | 4 | /** 5 | * GetCountryList 6 | * @returns {Promise<{ 7 | * country_code: string, 8 | * name: string, 9 | * }[]>} 10 | */ 11 | export async function GetCountryList () { 12 | return utils.request.get('IStoreTopSellersService/GetCountryList/v1') 13 | .then(res => res.response.countries) 14 | } 15 | 16 | /** 17 | * 获取每周热销榜 18 | * @param {number?} startDate 开始日期 19 | * @returns {Promise<{ 20 | * start_date: number, 21 | * ranks: { 22 | * rank: number, 23 | * appid: number, 24 | * last_week_rank: number, 25 | * consecutive_weeks: number, 26 | * item: { 27 | * name: string, 28 | * appid: number, 29 | * is_free: boolean, 30 | * best_purchase_option?: { 31 | * formatted_final_price: string, 32 | * formatted_original_price: string, 33 | * discount_pct?: number, 34 | * } 35 | * }, 36 | * first_top100?: boolean, 37 | * consecutive_weeks: number, 38 | * last_week_rank?: number, 39 | * }[] 40 | * }>} 41 | */ 42 | export async function GetWeeklyTopSellers (startDate) { 43 | const input = { 44 | country_code: Config.other.countryCode, 45 | page_count: 100, 46 | context: { 47 | language: 'schinese', 48 | country_code: Config.other.countryCode 49 | }, 50 | data_request: { 51 | include_basic_info: true 52 | } 53 | } 54 | return utils.request.get('IStoreTopSellersService/GetWeeklyTopSellers/v1', { 55 | params: { 56 | input_json: JSON.stringify(input), 57 | start_date: startDate 58 | } 59 | }).then(res => res.response) 60 | } 61 | -------------------------------------------------------------------------------- /models/api/IUserAccountService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 获取用户的地区代码 5 | * @param {string} accessToken 6 | * @param {string} steamId 7 | * @returns {Promise} 8 | */ 9 | export async function GetUserCountry (accessToken, steamId) { 10 | return utils.request.post('IUserAccountService/GetUserCountry/v1', { 11 | params: { 12 | access_token: accessToken, 13 | steamid: steamId 14 | } 15 | }).then(res => res.response.country) 16 | } 17 | -------------------------------------------------------------------------------- /models/api/IUserReviewsService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * GetFriendsRecommendedApp 5 | * @returns {Promise} 6 | */ 7 | export async function GetFriendsRecommendedApp () { 8 | return utils.request.get('IUserReviewsService/GetFriendsRecommendedApp/v1') 9 | } 10 | -------------------------------------------------------------------------------- /models/api/IWishlistService.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | 3 | /** 4 | * 添加到愿望单 5 | * @param {string} accessToken 6 | * @param {string} appid 7 | * @returns {Promise} 8 | */ 9 | export async function AddToWishlist (accessToken, appid) { 10 | return utils.request.post('IWishlistService/AddToWishlist/v1', { 11 | params: { 12 | access_token: accessToken, 13 | appid 14 | } 15 | }).then(res => res.response) 16 | } 17 | 18 | /** 19 | * 获取用户的愿望单 (居然不给name) 20 | * @param {string} steamid 21 | * @returns {Promise<{ 22 | * appid: number, 23 | * priority: number, 24 | * date_added: number 25 | * }[]>} 26 | */ 27 | export async function GetWishlist (steamid) { 28 | return utils.request.get('IWishlistService/GetWishlist/v1', { 29 | params: { 30 | steamid 31 | } 32 | }).then(res => res.response.items || []) 33 | } 34 | 35 | /** 36 | * 获取用户的愿望单数量 37 | * @param {string} steamid 38 | * @returns {Promise} 39 | */ 40 | export async function GetWishlistItemCount (steamid) { 41 | return utils.request.get('IWishlistService/GetWishlistItemCount/v1', { 42 | params: { 43 | steamid 44 | } 45 | }).then(res => res.response.count) 46 | } 47 | 48 | /** 49 | * 移除愿望单 50 | * @param {string} accessToken 51 | * @param {string} appid 52 | * @returns {Promise} 愿望单数量 53 | */ 54 | export async function RemoveFromWishlist (accessToken, appid) { 55 | return utils.request.post('IWishlistService/RemoveFromWishlist/v1', { 56 | params: { 57 | access_token: accessToken, 58 | appid 59 | } 60 | }).then(res => res.response.wishlist_count) 61 | } 62 | -------------------------------------------------------------------------------- /models/api/community.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | import { Config } from '#components' 3 | 4 | export function getBaseURL () { 5 | const url = 'https://steamcommunity.com/' 6 | if (Config.steam.commonProxy) { 7 | return Config.steam.commonProxy.replace('{{url}}', url) 8 | } else if (Config.steam.communityProxy) { 9 | return Config.steam.communityProxy.replace(/\/$/, '') 10 | } else { 11 | return url 12 | } 13 | } 14 | 15 | /** 16 | * 获取迷你信息 17 | * @param {number} friendCode 18 | * @param {boolean} json 19 | * @returns {Promise<{ 20 | * level: number, 21 | * level_class: string, 22 | * avatar_url: string, 23 | * persona_name: string, 24 | * favorite_badge?: { 25 | * name: string, 26 | * xp: string 27 | * level: number 28 | * description: string 29 | * icon: string 30 | * }, 31 | * in_game?: { 32 | * name: string, 33 | * is_non_steam: boolean, 34 | * logo: string, 35 | * rich_presence: string 36 | * }, 37 | * profile_background?: { 38 | * 'video/webm': string, 39 | * 'video/mp4': string, 40 | * }, 41 | * avatar_frame?: string, 42 | * }|string>} 43 | */ 44 | export async function miniprofile (friendCode, json = false) { 45 | return utils.request.get(`miniprofile/${friendCode}${json ? '/json' : ''}`, { 46 | baseURL: getBaseURL(), 47 | params: { 48 | t: Date.now() 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /models/api/index.js: -------------------------------------------------------------------------------- 1 | export * as store from './store.js' 2 | export * as community from './community.js' 3 | export * as ISteamUser from './ISteamUser.js' 4 | export * as IStoreService from './IStoreService.js' 5 | export * as IPlayerService from './IPlayerService.js' 6 | export * as ISteamUserOAuth from './ISteamUserOAuth.js' 7 | export * as ISteamUserStats from './ISteamUserStats.js' 8 | export * as IWishlistService from './IWishlistService.js' 9 | export * as ICheckoutService from './ICheckoutService.js' 10 | export * as ISteamWebAPIUtil from './ISteamWebAPIUtil.js' 11 | export * as ICommunityService from './ICommunityService.js' 12 | export * as IMobileAppService from './IMobileAppService.js' 13 | export * as IClientCommService from './IClientCommService.js' 14 | export * as IStoreQueryService from './IStoreQueryService.js' 15 | export * as IStoreBrowseService from './IStoreBrowseService.js' 16 | export * as ISaleFeatureService from './ISaleFeatureService.js' 17 | export * as ISteamChartsService from './ISteamChartsService.js' 18 | export * as IFriendsListService from './IFriendsListService.js' 19 | export * as IUserReviewsService from './IUserReviewsService.js' 20 | export * as IAccountCartService from './IAccountCartService.js' 21 | export * as IUserAccountService from './IUserAccountService.js' 22 | export * as IFamilyGroupsService from './IFamilyGroupsService.js' 23 | export * as IAuthenticationService from './IAuthenticationService.js' 24 | export * as IStoreTopSellersService from './IStoreTopSellersService.js' 25 | export * as IAccountPrivateAppsService from './IAccountPrivateAppsService.js' 26 | -------------------------------------------------------------------------------- /models/bind/index.js: -------------------------------------------------------------------------------- 1 | import { db, utils, api } from '#models' 2 | import { Config, Render } from '#components' 3 | 4 | /** 5 | * 获得已绑定的steamId的图片 6 | * @param {string} bid 7 | * @param {string} uid 8 | * @param {string} gid 9 | * @param {import('models/db').UserColumns[]?} userBindSteamIdList 10 | * @returns {Promise} 11 | */ 12 | export async function getBindSteamIdsImg (bid, uid, gid, userBindSteamIdList = []) { 13 | if (!userBindSteamIdList?.length) { 14 | userBindSteamIdList = await db.user.getAllByUserId(uid) 15 | } 16 | if (!userBindSteamIdList.length) { 17 | return Config.tips.noSteamIdTips 18 | } 19 | const accessTokenList = await db.token.getAllByUserId(uid) 20 | const groupPermission = (() => { 21 | if (!Config.push.enable) { 22 | return false 23 | } 24 | if (Config.push.whiteBotList.length && !Config.push.whiteBotList.some(i => i == bid)) { 25 | return false 26 | } 27 | if (Config.push.blackBotList.length && Config.push.blackBotList.some(i => i == bid)) { 28 | return false 29 | } 30 | if (Config.push.whiteGroupList.length && !Config.push.whiteGroupList.some(i => i == gid)) { 31 | return false 32 | } 33 | if (Config.push.blackGroupList.length && Config.push.blackGroupList.some(i => i == gid)) { 34 | return false 35 | } 36 | return true 37 | })() 38 | const data = [] 39 | const enablePushSteamIdList = groupPermission ? await db.push.getAllByUserIdAndGroupId(uid, gid) : [] 40 | const userInfo = {} 41 | try { 42 | (await api.IPlayerService.GetPlayerLinkDetails(userBindSteamIdList.map(i => i.steamId))) 43 | .forEach(i => { 44 | const avatarhash = Buffer.from(i.public_data.sha_digest_avatar, 'base64').toString('hex') 45 | userInfo[i.public_data.steamid] = { 46 | name: i.public_data.persona_name, 47 | avatar: Config.other.steamAvatar ? `https://avatars.steamstatic.com/${avatarhash}_full.jpg` : '' 48 | } 49 | }) 50 | } catch { } 51 | let index = 1 52 | for (const item of userBindSteamIdList) { 53 | const accessToken = accessTokenList.find(i => i.steamId == item.steamId) 54 | const i = userInfo[item.steamId] || {} 55 | const avatar = Config.other.steamAvatar ? i.avatar : await utils.bot.getUserAvatar(bid, uid, gid) 56 | const pushInfo = enablePushSteamIdList.find(i => i.steamId == item.steamId) 57 | const info = { 58 | ...pushInfo, 59 | steamId: item.steamId, 60 | isBind: item.isBind, 61 | name: i.name || await utils.bot.getUserName(bid, uid, gid), 62 | avatar: avatar || await utils.bot.getUserAvatar(bid, uid, gid), 63 | index, 64 | type: accessToken ? 'ck' : 'reg' 65 | } 66 | data.push(info) 67 | index++ 68 | } 69 | return await Render.render('user/index', { 70 | data: [{ 71 | title: '已绑定的steamid', 72 | list: data 73 | }], 74 | groupPermission, 75 | play: Config.push.enable, 76 | state: Config.push.stateChange, 77 | inventory: Config.push.userInventoryChange, 78 | wishlist: Config.push.userWishlistChange, 79 | random: Math.floor(Math.random() * 5) + 1 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /models/canvas/canvas.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { join } from 'path' 3 | import { Version } from '#components' 4 | 5 | export const canvasPKG = await (async () => { 6 | try { 7 | const pkg = await import('@napi-rs/canvas') 8 | const { GlobalFonts } = pkg 9 | const basePath = join(Version.pluginPath, 'resources', 'common', 'font') 10 | const normalFontPath = join(basePath, 'MiSans-Normal.ttf') 11 | const boldFontPath = join(basePath, 'MiSans-Bold.ttf') 12 | GlobalFonts.registerFromPath(normalFontPath, 'MiSans') 13 | GlobalFonts.registerFromPath(boldFontPath, 'Bold') 14 | return pkg 15 | } catch (error) { 16 | return null 17 | } 18 | })() 19 | 20 | export const hasCanvas = !!canvasPKG 21 | 22 | const startTimeMap = new Map() 23 | 24 | /** 25 | * 加载图片 26 | * @param {any} source 27 | * @param {import('@napi-rs/canvas').LoadImageOptions} options 28 | * @returns {Promise} 29 | */ 30 | export const loadImage = async (source, options = {}) => { 31 | try { 32 | if (source?.startsWith?.('http')) { 33 | const buffer = (await axios.get(source, { responseType: 'arraybuffer', timeout: 5000 })).data 34 | return await canvasPKG.loadImage(buffer, options) 35 | } else { 36 | return await canvasPKG.loadImage(source, options) 37 | } 38 | } catch { 39 | return null 40 | } 41 | } 42 | 43 | export function createCanvas (width, height) { 44 | if (!hasCanvas) throw new Error('请先pnpm i 安装依赖') 45 | const canvas = canvasPKG.createCanvas(width, height) 46 | const ctx = canvas.getContext('2d') 47 | const start = Date.now() 48 | canvas.canvasId = Symbol('canvasId') 49 | startTimeMap.set(canvas.canvasId, start) 50 | setTimeout(() => { 51 | if (startTimeMap.has(canvas.canvasId)) { 52 | logger.error('[图片生成][canvas] 超时 30000ms') 53 | startTimeMap.delete(canvas.canvasId) 54 | } 55 | }, 1000 * 30) 56 | return { ctx, canvas } 57 | } 58 | 59 | /** 60 | * 转换成可发送的图片 61 | * @param {import('@napi-rs/canvas').Canvas} canvas 62 | * @returns 63 | */ 64 | export function toImage (canvas) { 65 | const buffer = canvas.toBuffer('image/jpeg') 66 | const end = Date.now() 67 | const start = startTimeMap.get(canvas.canvasId) || end - 1000 * 30 68 | startTimeMap.delete(canvas.canvasId) 69 | logger.info(`[图片生成][canvas] ${(buffer.length / 1024).toFixed(2)}KB ${end - start}ms`) 70 | if (Version.BotName === 'Karin') { 71 | return segment.image(`base64://${buffer.toString('base64')}`) 72 | } else { 73 | return segment.image(buffer) 74 | } 75 | } 76 | 77 | /** 78 | * 缩短文本 79 | * @param {import('@napi-rs/canvas').SKRSContext2D} ctx 80 | * @param {string} text 81 | * @param {number} maxWidth 82 | * @param {string} replace 83 | */ 84 | export function shortenText (ctx, text, maxWidth, replace = '...') { 85 | if (!text) return '' 86 | if (ctx.measureText(text).width < maxWidth) return text 87 | maxWidth -= ctx.measureText(replace).width 88 | while (ctx.measureText(text).width > maxWidth) { 89 | text = text.slice(0, -1) 90 | } 91 | return text + replace 92 | } 93 | 94 | export function drawBackgroundColor (ctx, color, x, y, width, height, radius) { 95 | ctx.beginPath() 96 | const backgroundX = x - 5 97 | const backgroundY = y - 20 98 | const backgroundWidth = width + 2 99 | const backgroundHeight = height + 5 100 | ctx.fillStyle = color 101 | ctx.moveTo(backgroundX + radius, backgroundY) 102 | ctx.arcTo(backgroundX + backgroundWidth, backgroundY, backgroundX + backgroundWidth, backgroundY + backgroundHeight, radius) 103 | ctx.arcTo(backgroundX + backgroundWidth, backgroundY + backgroundHeight, backgroundX, backgroundY + backgroundHeight, radius) 104 | ctx.arcTo(backgroundX, backgroundY + backgroundHeight, backgroundX, backgroundY, radius) 105 | ctx.arcTo(backgroundX, backgroundY, backgroundX + backgroundWidth, backgroundY, radius) 106 | ctx.closePath() 107 | ctx.fill() 108 | } 109 | -------------------------------------------------------------------------------- /models/canvas/game.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Version } from '#components' 3 | import { loadImage, createCanvas, toImage, shortenText } from './canvas.js' 4 | 5 | export async function render (data) { 6 | const bg = await loadImage(join(Version.pluginPath, 'resources', 'game', 'game.png')) 7 | 8 | const { ctx, canvas } = createCanvas(bg.width, bg.height * data.length) 9 | 10 | let x = 0 11 | let y = 0 12 | 13 | const images = await Promise.all(data.map(async (i) => ({ ...i, _avatar: await loadImage(i.avatar || i.image) }))).then(i => i.reduce((acc, cur) => { 14 | if (cur?._avatar) { 15 | acc[`${cur.name}${cur.appid}${cur.desc}`] = cur._avatar 16 | } 17 | return acc 18 | }, {})) 19 | 20 | for (const i of data) { 21 | ctx.drawImage(bg, x, y, bg.width, bg.height) 22 | 23 | x += 15 24 | y += 20 25 | const avatar = images[`${i.name}${i.appid}${i.desc}`] 26 | if (avatar) { 27 | ctx.drawImage(avatar, x, y, 66, 66) 28 | } 29 | 30 | ctx.font = '19px MiSans' 31 | ctx.fillStyle = '#e3ffc2' 32 | 33 | const nickname = shortenText(ctx, i.isAvatar ? i.name : i.detail, 300) 34 | 35 | x += 85 36 | y += 15 37 | ctx.fillText(nickname, x, y) 38 | 39 | if (!i.isAvatar) { 40 | y += 25 41 | ctx.font = '17px MiSans' 42 | ctx.fillStyle = '#969696' 43 | ctx.fillText(i.type === 'end' ? '结束玩' : '正在玩', x, y) 44 | 45 | const name = shortenText(ctx, i.name, 357) 46 | 47 | y += 25 48 | ctx.font = '14px Bold' 49 | ctx.fillStyle = '#91c257' 50 | ctx.fillText(name, x, y) 51 | } else { 52 | y += 50 53 | ctx.font = '14px Bold' 54 | ctx.fillStyle = i.desc.includes('在线') ? '#beee11' : '#999999' 55 | ctx.fillText(i.desc, x, y) 56 | } 57 | 58 | x = 0 59 | y += 20 60 | } 61 | 62 | return toImage(canvas) 63 | } 64 | -------------------------------------------------------------------------------- /models/canvas/index.js: -------------------------------------------------------------------------------- 1 | export * as game from './game.js' 2 | export * as info from './info.js' 3 | export * as inventory from './inventory.js' 4 | -------------------------------------------------------------------------------- /models/db/base.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { join } from 'path' 3 | import { Version } from '#components' 4 | import { Sequelize, DataTypes, Op, fn, col } from 'sequelize' 5 | 6 | const dataPath = join(Version.pluginPath, 'data') 7 | if (!fs.existsSync(dataPath)) { 8 | fs.mkdirSync(dataPath) 9 | } 10 | 11 | const sequelize = new Sequelize({ 12 | dialect: 'sqlite', 13 | storage: join(dataPath, 'data.db'), 14 | logging: false 15 | }) 16 | 17 | await sequelize.authenticate() 18 | 19 | export { 20 | Op, 21 | fn, 22 | col, 23 | DataTypes, 24 | sequelize 25 | } 26 | -------------------------------------------------------------------------------- /models/db/familyInventoryPush.js: -------------------------------------------------------------------------------- 1 | import { Config } from '#components' 2 | import { sequelize, DataTypes, Op } from './base.js' 3 | 4 | /** 5 | * @typedef {Object} familyInventoryPushColumns 6 | * @property {number} id 表id 7 | * @property {string} userId 用户id 8 | * @property {string} steamId steamId 9 | * @property {string} botId 机器人id 10 | * @property {string} groupId 群组id 11 | */ 12 | 13 | export const table = sequelize.define('familyInventoryPush', { 14 | id: { 15 | type: DataTypes.BIGINT, 16 | primaryKey: true, 17 | autoIncrement: true 18 | }, 19 | userId: { 20 | type: DataTypes.STRING, 21 | allowNull: false 22 | }, 23 | steamId: { 24 | type: DataTypes.STRING, 25 | allowNull: false 26 | }, 27 | botId: { 28 | type: DataTypes.STRING, 29 | allowNull: false 30 | }, 31 | groupId: { 32 | type: DataTypes.STRING, 33 | allowNull: false 34 | } 35 | }, { 36 | freezeTableName: true 37 | }) 38 | 39 | await table.sync() 40 | 41 | /** 42 | * 添加一个推送群 43 | * @param {string} userId 44 | * @param {string} steamId 45 | * @param {string} botId 46 | * @param {string} groupId 47 | * @param {import('sequelize').Transaction?} transaction 48 | * @returns {Promise} 49 | */ 50 | export async function add (userId, steamId, botId, groupId, transaction) { 51 | userId = String(userId) 52 | botId = String(botId) 53 | groupId = String(groupId) 54 | // 判断是否存在 55 | const data = await table.findOne({ 56 | where: { 57 | userId, 58 | steamId, 59 | botId, 60 | groupId 61 | } 62 | }).then(i => i?.dataValues) 63 | if (data) { 64 | return data 65 | } 66 | return await table.create({ 67 | userId, 68 | steamId, 69 | botId, 70 | groupId 71 | }, { transaction }).then(result => result?.dataValues) 72 | } 73 | 74 | /** 75 | * 删除推送群 76 | * @param {string} userId 77 | * @param {string} steamId 78 | * @param {string} botId 79 | * @param {string} groupId 80 | * @param {import('sequelize').Transaction} transaction 81 | * @returns {Promise} 82 | */ 83 | export async function del (userId, steamId, botId, groupId, transaction) { 84 | const where = {} 85 | if (userId) { 86 | where.userId = String(userId) 87 | } 88 | if (steamId) { 89 | where.steamId = steamId 90 | } 91 | if (botId) { 92 | where.botId = String(botId) 93 | } 94 | if (groupId) { 95 | where.groupId = String(groupId) 96 | } 97 | return await table.destroy({ 98 | where, 99 | transaction 100 | }).then(result => result?.[0]) 101 | } 102 | 103 | /** 104 | * 获取所有推送群组 105 | * @param {boolean} [filter=true] 是否使用黑白名单查找 默认开启 106 | * @returns {Promise} 107 | */ 108 | export async function getAll (filter = true) { 109 | const where = {} 110 | if (filter) { 111 | if (Config.push.whiteGroupList.length) { 112 | where.groupId = { 113 | [Op.in]: Config.push.whiteGroupList.map(String) 114 | } 115 | } else if (Config.push.blackGroupList.length) { 116 | where.groupId = { 117 | [Op.notIn]: Config.push.blackGroupList.map(String) 118 | } 119 | } 120 | if (Config.push.whiteBotList.length) { 121 | where.botId = { 122 | [Op.in]: Config.push.whiteBotList.map(String) 123 | } 124 | } else if (Config.push.blackBotList.length) { 125 | where.botId = { 126 | [Op.notIn]: Config.push.blackBotList.map(String) 127 | } 128 | } 129 | } 130 | return await table.findAll({ 131 | where 132 | }).then(result => result?.map(item => item?.dataValues)) 133 | } 134 | -------------------------------------------------------------------------------- /models/db/game.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { sequelize, DataTypes, Op } from './base.js' 3 | 4 | /** 5 | * @typedef {Object} GameColumns 6 | * @property {string} appid appid 7 | * @property {string} name 游戏名称 8 | * @property {string} community 社区icon 9 | * @property {string} header header图片 10 | */ 11 | 12 | export const table = sequelize.define('game', { 13 | id: { 14 | type: DataTypes.BIGINT, 15 | primaryKey: true, 16 | autoIncrement: true 17 | }, 18 | appid: { 19 | type: DataTypes.STRING, 20 | allowNull: false 21 | }, 22 | name: { 23 | type: DataTypes.STRING, 24 | allowNull: false 25 | }, 26 | community: { 27 | type: DataTypes.STRING, 28 | defaultValue: false 29 | }, 30 | header: { 31 | type: DataTypes.STRING, 32 | defaultValue: false 33 | } 34 | }, { 35 | freezeTableName: true 36 | }) 37 | 38 | await table.sync() 39 | 40 | /** 41 | * 添加游戏信息 42 | * @param {GameColumns[]} games 43 | */ 44 | export async function add (games) { 45 | if (!Array.isArray(games)) { 46 | if (typeof games === 'object') { 47 | games = Object.values(games) 48 | } else { 49 | games = [games] 50 | } 51 | } 52 | return (await table.bulkCreate(games.map(i => ({ ...i, appid: String(i.appid) })))).map(i => i.dataValues) 53 | } 54 | 55 | /** 56 | * 查询游戏信息 57 | * @param {string[]} appids 58 | * @returns {Promise<{[appid: string]: GameColumns}>} 59 | */ 60 | export async function get (appids) { 61 | if (!Array.isArray(appids)) appids = [appids] 62 | return await table.findAll({ 63 | where: { 64 | appid: { 65 | [Op.in]: appids.map(String) 66 | } 67 | } 68 | }).then(res => res.map(i => i.dataValues)).then(i => _.keyBy(i, 'appid')) 69 | } 70 | -------------------------------------------------------------------------------- /models/db/index.js: -------------------------------------------------------------------------------- 1 | export * as kv from './kv.js' 2 | export * as user from './user.js' 3 | export * as push from './push.js' 4 | export * as game from './game.js' 5 | export * as stats from './stats.js' 6 | export * as token from './token.js' 7 | export * as userInventory from './userInventory.js' 8 | export * as priceChangePush from './priceChangePush.js' 9 | export * as familyInventoryPush from './familyInventoryPush.js' 10 | -------------------------------------------------------------------------------- /models/db/kv.js: -------------------------------------------------------------------------------- 1 | import { sequelize, DataTypes } from './base.js' 2 | 3 | /** 4 | * @typedef {Object} KVColumns 5 | * @property {string} key 键 6 | * @property {string} value 值 7 | */ 8 | 9 | export const table = sequelize.define('kv', { 10 | key: { 11 | type: DataTypes.STRING, 12 | primaryKey: true, 13 | allowNull: false 14 | }, 15 | value: { 16 | type: DataTypes.STRING, 17 | allowNull: false 18 | } 19 | }, { 20 | freezeTableName: true 21 | }) 22 | 23 | /** 24 | * 查找 25 | * @param {string} key 26 | * @returns {Promise} value 27 | */ 28 | export async function get (key) { 29 | return await table.findOne({ 30 | where: { 31 | key 32 | } 33 | }).then(res => res?.dataValues) 34 | } 35 | 36 | /** 37 | * 插入或更新 38 | * @param {string} key 39 | * @param {string} value 40 | * @returns {Promise} 41 | */ 42 | export async function set (key, value) { 43 | if (!key || !value) { 44 | throw new Error('更新数据时,key和value不能为空') 45 | } 46 | return await table.upsert({ 47 | key, 48 | value 49 | }).then(res => res[0].dataValues) 50 | } 51 | 52 | /** 53 | * 删除 54 | * @param {string} key 55 | * @returns {Promise} 56 | */ 57 | export async function del (key) { 58 | return await table.destroy({ 59 | where: { 60 | key 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /models/db/priceChangePush.js: -------------------------------------------------------------------------------- 1 | import { Config } from '#components' 2 | import { sequelize, DataTypes, Op } from './base.js' 3 | 4 | /** 5 | * @typedef {Object} priceChangePushColumns 6 | * @property {number} id 表id 7 | * @property {string} appid appid 8 | * @property {string} botId 机器人id 9 | * @property {string} groupId 群组id 10 | * @property {number} lastTime 最后推送时间 unix时间戳 11 | */ 12 | 13 | export const table = sequelize.define('priceChangePush', { 14 | id: { 15 | type: DataTypes.BIGINT, 16 | primaryKey: true, 17 | autoIncrement: true 18 | }, 19 | appid: { 20 | type: DataTypes.STRING, 21 | allowNull: false 22 | }, 23 | botId: { 24 | type: DataTypes.STRING, 25 | allowNull: false 26 | }, 27 | groupId: { 28 | type: DataTypes.STRING, 29 | allowNull: false 30 | }, 31 | lastTime: { 32 | type: DataTypes.BIGINT, 33 | defaultValue: 0 34 | } 35 | }, { 36 | freezeTableName: true 37 | }) 38 | 39 | await table.sync() 40 | 41 | /** 42 | * 添加一个推送群 43 | * @param {string} appid 44 | * @param {string} botId 45 | * @param {string} groupId 46 | * @param {import('sequelize').Transaction?} transaction 47 | * @returns {Promise} 48 | */ 49 | export async function add (appid, botId, groupId, transaction) { 50 | appid = String(appid) 51 | botId = String(botId) 52 | groupId = String(groupId) 53 | // 判断是否存在 54 | const data = await table.findOne({ 55 | where: { 56 | appid, 57 | botId, 58 | groupId 59 | } 60 | }).then(i => i?.dataValues) 61 | if (data) { 62 | return data 63 | } 64 | return await table.create({ 65 | appid, 66 | botId, 67 | groupId 68 | }, { transaction }).then(result => result?.dataValues) 69 | } 70 | 71 | /** 72 | * 删除推送群 73 | * @param {string} appid 74 | * @param {string} botId 75 | * @param {string} groupId 76 | * @param {import('sequelize').Transaction?} [transaction] 77 | * @returns {Promise} 78 | */ 79 | export async function del (appid, botId, groupId, transaction) { 80 | appid = String(appid) 81 | botId = String(botId) 82 | groupId = String(groupId) 83 | return await table.destroy({ 84 | where: { 85 | appid, 86 | botId, 87 | groupId 88 | }, 89 | transaction 90 | }).then(result => result?.[0]) 91 | } 92 | 93 | /** 94 | * 更新最后推送时间 95 | * @param {string[]} appids 96 | * @param {number} lastTime 97 | * @returns {Promise} 98 | */ 99 | export async function updateLastTime (appids, lastTime) { 100 | if (!Array.isArray(appids)) appids = [appids] 101 | if (!appids.length) { 102 | return 0 103 | } 104 | return await table.update({ 105 | lastTime 106 | }, { 107 | where: { 108 | appid: { 109 | [Op.in]: appids.map(String) 110 | } 111 | } 112 | }).then(result => result?.[0]) 113 | } 114 | 115 | /** 116 | * 获得一个群所有的开启推送列 117 | * @param {string} groupId 118 | * @returns {Promise} 119 | */ 120 | export async function getOneGroup (groupId) { 121 | groupId = String(groupId) 122 | return await table.findAll({ 123 | where: { 124 | groupId 125 | } 126 | }).then(result => result?.map(item => item?.dataValues)) 127 | } 128 | 129 | /** 130 | * 获取所有推送群组 131 | * @param {boolean} [filter=true] 是否使用黑白名单查找 默认开启 132 | * @returns {Promise} 133 | */ 134 | export async function getAll (filter = true) { 135 | const where = {} 136 | if (filter) { 137 | if (Config.push.whiteGroupList.length) { 138 | where.groupId = { 139 | [Op.in]: Config.push.whiteGroupList.map(String) 140 | } 141 | } else if (Config.push.blackGroupList.length) { 142 | where.groupId = { 143 | [Op.notIn]: Config.push.blackGroupList.map(String) 144 | } 145 | } 146 | if (Config.push.whiteBotList.length) { 147 | where.botId = { 148 | [Op.in]: Config.push.whiteBotList.map(String) 149 | } 150 | } else if (Config.push.blackBotList.length) { 151 | where.botId = { 152 | [Op.notIn]: Config.push.blackBotList.map(String) 153 | } 154 | } 155 | } 156 | return await table.findAll({ 157 | where 158 | }).then(result => result?.map(item => item?.dataValues)) 159 | } 160 | -------------------------------------------------------------------------------- /models/db/token.js: -------------------------------------------------------------------------------- 1 | import { utils } from '#models' 2 | import { sequelize, DataTypes } from './base.js' 3 | 4 | /** 5 | * @typedef {Object} TokenColumns 6 | * @property {number} id 表id 7 | * @property {string} userId 用户id 8 | * @property {string} steamId steamId 9 | * @property {string} accessToken accessToken 10 | * @property {string} refreshToken refreshToken 11 | * @property {string} cookie cookie 12 | * @property {number} accessTokenExpires accessToken过期时间 unix时间戳 13 | * @property {number} refreshTokenExpires refreshToken过期时间 unix时间戳 14 | */ 15 | 16 | const table = sequelize.define('token', { 17 | id: { 18 | type: DataTypes.BIGINT, 19 | primaryKey: true, 20 | autoIncrement: true 21 | }, 22 | userId: { 23 | type: DataTypes.STRING, 24 | allowNull: false 25 | }, 26 | steamId: { 27 | type: DataTypes.STRING, 28 | allowNull: false 29 | }, 30 | accessToken: { 31 | type: DataTypes.STRING 32 | }, 33 | refreshToken: { 34 | type: DataTypes.STRING 35 | }, 36 | cookie: { 37 | type: DataTypes.STRING 38 | }, 39 | accessTokenExpires: { 40 | type: DataTypes.BIGINT 41 | }, 42 | refreshTokenExpires: { 43 | type: DataTypes.BIGINT 44 | } 45 | }, { 46 | freezeTableName: true 47 | }) 48 | 49 | await table.sync() 50 | 51 | /** 52 | * 添加accessToken到userId 53 | * @param {string} userId 54 | * @param {string} accessToken 55 | * @param {string?} refreshToken 56 | * @param {string?} cookie 57 | * @returns {Promise} 58 | */ 59 | export async function set (userId, accessToken, cookie = '', refreshToken = '') { 60 | userId = String(userId) 61 | 62 | const jwt = utils.steam.decodeAccessTokenJwt(accessToken) 63 | 64 | if (!jwt) { 65 | throw new Error('accessToken 解码失败') 66 | } 67 | 68 | const baseData = { userId, steamId: jwt.sub } 69 | 70 | const extraData = { 71 | accessToken, 72 | accessTokenExpires: jwt.exp || 0, 73 | refreshTokenExpires: jwt.rt_exp || 0 74 | } 75 | if (refreshToken) { 76 | extraData.refreshToken = refreshToken 77 | } 78 | if (cookie) { 79 | extraData.cookie = cookie 80 | } 81 | 82 | const existingRecord = await table.findOne({ 83 | where: baseData 84 | }) 85 | 86 | if (existingRecord) { 87 | return await existingRecord.update(extraData).then(res => res.dataValues) 88 | } else { 89 | const newRecord = { 90 | ...baseData, 91 | ...extraData 92 | } 93 | 94 | return await table.create(newRecord).then(res => res.dataValues) 95 | } 96 | } 97 | 98 | /** 99 | * 查询accessToken 100 | * @param {string} userId 101 | * @param {string} steamId 102 | * @returns {Promise} 103 | */ 104 | export async function getByUserIdAndSteamId (userId, steamId) { 105 | userId = String(userId) 106 | steamId = String(steamId) 107 | return await table.findOne({ 108 | where: { 109 | userId, 110 | steamId 111 | } 112 | }).then(res => res?.dataValues) 113 | } 114 | 115 | /** 116 | * 根据userId查询所有信息 117 | * @param {string} userId 118 | * @returns {Promise} 119 | */ 120 | export async function getAllByUserId (userId) { 121 | userId = String(userId) 122 | return await table.findAll({ 123 | where: { 124 | userId 125 | } 126 | }).then(res => res.map(item => item.dataValues)) 127 | } 128 | 129 | /** 130 | * 删除accessToken 131 | * @param {string} userId 132 | * @param {string} steamId 133 | * @returns 134 | */ 135 | export async function del (userId, steamId) { 136 | userId = String(userId) 137 | steamId = String(steamId) 138 | return await table.destroy({ 139 | where: { 140 | userId, 141 | steamId 142 | } 143 | }) 144 | } 145 | 146 | /** 147 | * 查询所有accessToken 148 | * @returns {Promise} 149 | */ 150 | export async function getAll () { 151 | return await table.findAll().then(res => res.map(item => item.dataValues)) 152 | } 153 | 154 | /** 155 | * 获得绑定的accessToken数量 156 | * @returns {Promise} 157 | */ 158 | export async function count () { 159 | return await table.count() 160 | } 161 | -------------------------------------------------------------------------------- /models/db/userInventory.js: -------------------------------------------------------------------------------- 1 | import { sequelize, DataTypes } from './base.js' 2 | 3 | export const table = sequelize.define('userInventory', { 4 | steamId: { 5 | type: DataTypes.STRING, 6 | primaryKey: true, 7 | allowNull: false 8 | }, 9 | appids: { 10 | type: DataTypes.JSON, 11 | defaultValue: [] 12 | } 13 | }, { 14 | freezeTableName: true 15 | }) 16 | 17 | await table.sync() 18 | 19 | /** 20 | * 创建或更新用户库存 21 | * @param {string} steamId 22 | * @param {number[]} appids 23 | * @returns {Promise<{steamId: string, appids: number[]}>} 24 | */ 25 | export async function set (steamId, appids) { 26 | const [res] = await table.upsert({ steamId, appids }) 27 | return res.dataValues 28 | } 29 | 30 | /** 31 | * 获取用户库存 32 | * @param {string} steamId 33 | * @returns {Promise} 34 | */ 35 | export async function get (steamId) { 36 | return await table.findOne({ where: { steamId } }).then(res => res?.dataValues.appids) 37 | } 38 | -------------------------------------------------------------------------------- /models/help/config.js: -------------------------------------------------------------------------------- 1 | export const style = { 2 | // 主文字颜色 3 | fontColor: '#ceb78b', 4 | // 主文字阴影: 横向距离 垂直距离 阴影大小 阴影颜色 5 | // fontShadow: '0px 0px 1px rgba(6, 21, 31, .9)', 6 | fontShadow: 'none', 7 | // 描述文字颜色 8 | descColor: '#eee', 9 | 10 | /* 面板整体底色,会叠加在标题栏及帮助行之下,方便整体帮助有一个基础底色 11 | * 若无需此项可将rgba最后一位置为0即为完全透明 12 | * 注意若综合透明度较低,或颜色与主文字颜色过近或太透明可能导致阅读困难 */ 13 | contBgColor: 'rgba(6, 21, 31, .5)', 14 | 15 | // 面板底图毛玻璃效果,数字越大越模糊,0-10 ,可为小数 16 | contBgBlur: 3, 17 | 18 | // 板块标题栏底色 19 | headerBgColor: 'rgba(6, 21, 31, .4)', 20 | // 帮助奇数行底色 21 | rowBgColor1: 'rgba(6, 21, 31, .2)', 22 | // 帮助偶数行底色 23 | rowBgColor2: 'rgba(6, 21, 31, .35)' 24 | } 25 | -------------------------------------------------------------------------------- /models/help/index.js: -------------------------------------------------------------------------------- 1 | import helpTheme from './theme.js' 2 | export { helpList } from './help.js' 3 | 4 | export { 5 | helpTheme 6 | } 7 | -------------------------------------------------------------------------------- /models/help/theme.js: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash' 2 | import { style } from './config.js' 3 | 4 | const helpTheme = { 5 | getThemeCfg () { 6 | const resPath = `{{pluResPath}}help/theme/${lodash.random(1, 5)}.png` 7 | return { 8 | bg: resPath, 9 | style 10 | } 11 | }, 12 | async getThemeData (diyStyle) { 13 | const helpConfig = lodash.extend({}, diyStyle) 14 | const colCount = Math.min(5, Math.max(parseInt(helpConfig?.colCount) || 3, 2)) 15 | const colWidth = Math.min(500, Math.max(100, parseInt(helpConfig?.colWidth) || 265)) 16 | const width = Math.min(2500, Math.max(800, colCount * colWidth + 30)) 17 | const theme = helpTheme.getThemeCfg() 18 | const themeStyle = theme.style || {} 19 | const ret = [` 20 | .container{background-image:url(${theme.bg});width:${width}px;} 21 | .help-table .td,.help-table .th{width:${100 / colCount}%} 22 | `] 23 | const defFnc = (...args) => { 24 | for (const idx in args) { 25 | if (!lodash.isUndefined(args[idx])) { 26 | return args[idx] 27 | } 28 | } 29 | } 30 | const css = function (sel, css, key, def, fn) { 31 | let val = defFnc(themeStyle[key], diyStyle[key], def) 32 | if (fn) { 33 | val = fn(val) 34 | } 35 | ret.push(`${sel}{${css}:${val}}`) 36 | } 37 | css('.help-title,.help-group', 'color', 'fontColor', '#ceb78b') 38 | css('.help-title,.help-group', 'text-shadow', 'fontShadow', 'none') 39 | css('.help-desc', 'color', 'descColor', '#eee') 40 | css('.cont-box', 'background', 'contBgColor', 'rgba(43, 52, 61, 0.8)') 41 | css('.cont-box', 'backdrop-filter', 'contBgBlur', 3, (n) => diyStyle.bgBlur === false ? 'none' : `blur(${n}px)`) 42 | css('.help-group', 'background', 'headerBgColor', 'rgba(34, 41, 51, .4)') 43 | css('.help-table .tr:nth-child(odd)', 'background', 'rowBgColor1', 'rgba(34, 41, 51, .2)') 44 | css('.help-table .tr:nth-child(even)', 'background', 'rowBgColor2', 'rgba(34, 41, 51, .4)') 45 | return { 46 | style: ``, 47 | colCount 48 | } 49 | } 50 | } 51 | export default helpTheme 52 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | export * as db from './db/index.js' 2 | export * as api from './api/index.js' 3 | export * as bind from './bind/index.js' 4 | export * as task from './task/index.js' 5 | export * as help from './help/index.js' 6 | export * as info from './info/index.js' 7 | export * as utils from './utils/index.js' 8 | export * as canvas from './canvas/index.js' 9 | export * as setting from './setting/index.js' 10 | -------------------------------------------------------------------------------- /models/info/gif.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { canvas } from '#models' 3 | import { execSync } from 'child_process' 4 | import { logger, puppeteer, segment } from '#lib' 5 | import { Config, Render, Version } from '#components' 6 | 7 | /** 8 | * 渲染gif 需要传入tempName参数 用于存放临时文件 建议使用唯一id 比如steamId 9 | * @param {{ 10 | * tempPath: string, 11 | * [key: string]: any 12 | * }} data 13 | */ 14 | export async function render (data) { 15 | const tempPath = data.tempPath 16 | if (fs.existsSync(tempPath)) { 17 | fs.rmSync(tempPath, { force: true, recursive: true }) 18 | } 19 | fs.mkdirSync(tempPath, { recursive: true }) 20 | if (Config.gif.gifMode == 3) { 21 | return await canvas.info.render(data) 22 | } else { 23 | if (Version.BotName === 'Karin') { 24 | throw new Error('暂不支持karin使用puppeteer渲染gif') 25 | } 26 | 27 | const tplPath = Render.tplFile('info/index', data, tempPath) 28 | if (!puppeteer.browser) { 29 | await puppeteer.browserInit() 30 | } 31 | const page = await puppeteer.browser.newPage() 32 | const output = `${tempPath}/output.gif` 33 | const fps = Math.abs(Config.gif.frameRate || 20) 34 | try { 35 | await page.goto(`file://${tplPath}`) 36 | 37 | const body = await page.$('#container') || await page.$('body') 38 | 39 | if (Config.gif.gifMode == 2) { 40 | const { PuppeteerScreenRecorder } = await import('puppeteer-screen-recorder') 41 | const boundingBox = await body.boundingBox() 42 | 43 | page.setViewport({ 44 | width: Math.round(boundingBox.width), 45 | height: Math.round(boundingBox.height) 46 | }) 47 | 48 | const recorder = new PuppeteerScreenRecorder(page, { 49 | fps, 50 | followNewTab: false 51 | }) 52 | 53 | const input = `${tempPath}/output.mp4` 54 | 55 | await recorder.start(input) 56 | const sleep = Math.abs(Config.gif.videoLimit || 3) 57 | await new Promise(resolve => setTimeout(resolve, sleep * 1000)) 58 | 59 | await recorder.stop() 60 | execSync(`ffmpeg -i ${input} "${output}" -loglevel quiet`) 61 | } else { 62 | const sleep = Math.abs(Config.gif.frameSleep || 50) 63 | const count = Math.abs(Config.gif.frameCount || 30) 64 | 65 | const task = [] 66 | for (let i = 1; i < count; i++) { 67 | await new Promise(resolve => setTimeout(resolve, sleep)) 68 | task.push( 69 | body.screenshot({ 70 | path: `${tempPath}/${i}.jpeg`, 71 | type: 'jpeg' 72 | }) 73 | ) 74 | } 75 | await Promise.all(task) 76 | 77 | execSync(`ffmpeg -framerate ${fps} -i "${tempPath}/%d.jpeg" "${output}" -loglevel quiet`) 78 | } 79 | page.close().catch((err) => logger.error(err)) 80 | } catch (error) { 81 | page.close().catch((err) => logger.error(err)) 82 | // 仅用于关闭页面 83 | throw error 84 | } 85 | setTimeout(() => { 86 | fs.rmSync(tempPath, { force: true, recursive: true }) 87 | }, 1000 * 60 * 5) // 5分钟后删除 88 | const base64 = fs.readFileSync(output, { encoding: 'base64' }) 89 | return segment.image(`base64://${base64}`) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /models/info/index.js: -------------------------------------------------------------------------------- 1 | export * as gif from './gif.js' 2 | -------------------------------------------------------------------------------- /models/task/familyInventory.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | import schedule from 'node-schedule' 4 | import { api, db, utils } from '#models' 5 | import { Config, Render } from '#components' 6 | import { logger, redis, segment } from '#lib' 7 | 8 | let timer = null 9 | 10 | const redisKey = 'steam-plugin:family-inventory-time:' 11 | 12 | export function startTimer () { 13 | if (!Config.push.familyInventotyAdd || !Config.push.familyInventotyTime) { 14 | return 15 | } 16 | clearInterval(timer) 17 | timer?.cancel?.() 18 | if (Number(Config.push.familyInventotyTime)) { 19 | timer = setInterval(callback, 1000 * 60 * Config.push.familyInventotyTime) 20 | } else { 21 | timer = schedule.scheduleJob(Config.push.familyInventotyTime, callback) 22 | } 23 | } 24 | 25 | export async function callback () { 26 | logger.info('开始检查Steam家庭库存信息') 27 | const pushList = await db.familyInventoryPush.getAll() 28 | for (const i of _.uniqBy(pushList, 'steamId')) { 29 | try { 30 | const token = await utils.steam.getAccessToken(i.userId, i.steamId) 31 | if (!token.success) { 32 | continue 33 | } 34 | const familyInfo = await api.IFamilyGroupsService.GetFamilyGroupForUser(token.accessToken, token.steamId) 35 | if (!familyInfo.family_groupid) { 36 | continue 37 | } 38 | const familyInventory = await api.IFamilyGroupsService.GetSharedLibraryApps(token.accessToken, familyInfo.family_groupid, token.steamId) 39 | if (!familyInventory.apps.length) { 40 | continue 41 | } 42 | const lastTime = await redis.get(redisKey + i.steamId) 43 | const nowTime = moment().unix() 44 | redis.set(redisKey + i.steamId, nowTime) 45 | if (!lastTime) { 46 | continue 47 | } 48 | const addItems = familyInventory.apps.filter(i => i.rt_time_acquired > Number(lastTime)) 49 | if (!addItems.length) { 50 | continue 51 | } 52 | // pop会修改原数组 53 | const steamIds = _.uniq(addItems.map(i => i.owner_steamids[i.owner_steamids.length - 1])) 54 | const infoMap = await api.IPlayerService.GetPlayerLinkDetails(steamIds) 55 | .catch(() => steamIds.map(i => ({ public_data: { persona_name: i, steamid: i } }))) 56 | const games = [] 57 | for (const app of addItems) { 58 | const steamId = app.owner_steamids.pop() 59 | const info = infoMap.find(i => i.public_data.steamid === steamId) 60 | games.push({ 61 | name: app.name, 62 | image: utils.steam.getHeaderImgUrlByAppid(app.appid), 63 | appid: app.appid, 64 | detail: moment.unix(app.rt_time_acquired).format('YYYY-MM-DD HH:mm:ss'), 65 | desc: `来自: ${info?.public_data?.persona_name || steamId}` 66 | }) 67 | } 68 | for (const g of pushList.filter(p => p.steamId === i.steamId)) { 69 | const username = await utils.bot.getUserName(g.botId, g.userId, g.groupId) 70 | if (Config.push.pushMode == 1) { 71 | for (const i of games) { 72 | const msg = [ 73 | segment.image(i.image), 74 | `[Steam] ${username}的家庭库存新增:\n${i.name}\n时间: ${i.appid}\n${i.desc}` 75 | ] 76 | await utils.bot.sendGroupMsg(g.botId, g.groupId, msg) 77 | } 78 | } else { 79 | const img = await Render.render('inventory/index', { 80 | data: [{ 81 | title: `${username}的家庭库存新增`, 82 | games 83 | }] 84 | }) 85 | await utils.bot.sendGroupMsg(g.botId, g.groupId, img) 86 | } 87 | } 88 | } catch { } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /models/task/index.js: -------------------------------------------------------------------------------- 1 | import * as play from './play.js' 2 | import * as priceChange from './priceChange.js' 3 | import * as familyInventory from './familyInventory.js' 4 | import * as userInventory from './userInventory.js' 5 | import * as userWishlist from './userWishlist.js' 6 | 7 | export function startTask () { 8 | play.startTimer() 9 | familyInventory.startTimer() 10 | userInventory.startTimer() 11 | userWishlist.startTimer() 12 | priceChange.startTimer() 13 | } 14 | 15 | export { 16 | play, 17 | familyInventory, 18 | userInventory, 19 | userWishlist, 20 | priceChange 21 | } 22 | -------------------------------------------------------------------------------- /models/task/priceChange.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | import schedule from 'node-schedule' 4 | import { api, db, utils } from '#models' 5 | import { Config, Render } from '#components' 6 | import { logger } from '#lib' 7 | 8 | let timer = null 9 | 10 | export function startTimer () { 11 | if (Config.push.priceChange == 0 || !Config.push.priceChangeTime) { 12 | return 13 | } 14 | clearInterval(timer) 15 | timer?.cancel?.() 16 | if (Number(Config.push.priceChangeTime)) { 17 | timer = setInterval(callback, 1000 * 60 * Config.push.priceChangeTime) 18 | } else { 19 | timer = schedule.scheduleJob(Config.push.priceChangeTime, callback) 20 | } 21 | } 22 | 23 | export async function callback () { 24 | logger.info('开始检查Steam游戏价格变化') 25 | const list = await db.priceChangePush.getAll() 26 | const appids = _.uniq(_.map(list, 'appid')) 27 | if (!appids.length) { 28 | return 29 | } 30 | const now = moment().unix() 31 | const updateLastTimeAppids = [] 32 | const deleteLastTimeAppids = [] 33 | const group = {} 34 | const infoList = await api.IStoreBrowseService.GetItems(appids, { include_assets: true }) 35 | for (const appid in infoList) { 36 | const info = infoList[appid] 37 | const price = utils.steam.generatePrice(info.best_purchase_option, info.is_free) 38 | list.filter(i => i.appid == appid).forEach(({ groupId, lastTime, botId }) => { 39 | // 有最后一次推送的时间但是没有打折那就是打折时间过了 40 | if (lastTime && !price.discount) { 41 | deleteLastTimeAppids.push(appid) 42 | return 43 | } 44 | // 有最后一次推送时间并且还在打折 看看需不需要每次都推送 45 | if (lastTime && price.discount && Config.push.priceChangeType == 1) { 46 | return 47 | } 48 | // 没有最后一次推送时间并且也没有打折 49 | if (!lastTime && !price.discount) { 50 | return 51 | } 52 | // 只剩没有最后一次推送时间并且正在打折 直接推送 53 | if (!group[botId]) { 54 | group[botId] = {} 55 | } 56 | if (!group[botId][groupId]) { 57 | group[botId][groupId] = [] 58 | } 59 | const unix = info.best_purchase_option.active_discounts.shift().discount_end_date 60 | group[botId][groupId].push({ 61 | appid, 62 | name: info.name, 63 | price, 64 | desc: `结束时间: ${moment.unix(unix).format('YYYY-MM-DD')}`, 65 | image: utils.steam.getHeaderImgUrlByAppid(appid, 'apps', info.assets?.header) 66 | }) 67 | if (updateLastTimeAppids.indexOf(appid) == -1) { 68 | updateLastTimeAppids.push(appid) 69 | } 70 | }) 71 | } 72 | db.priceChangePush.updateLastTime(updateLastTimeAppids, now).catch(e => {}) 73 | db.priceChangePush.updateLastTime(deleteLastTimeAppids, 0).catch(e => {}) 74 | for (const botId in group) { 75 | const botList = group[botId] 76 | for (const groupId in botList) { 77 | const games = botList[groupId] 78 | const data = [{ 79 | title: 'Steam降价推送', 80 | games 81 | }] 82 | const img = await Render.render('inventory/index', { 83 | data 84 | }) 85 | await utils.bot.sendGroupMsg(botId, groupId, img) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /models/task/userInventory.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import schedule from 'node-schedule' 3 | import { api, db, utils } from '#models' 4 | import { Config, Render } from '#components' 5 | import { logger, segment } from '#lib' 6 | 7 | let timer = null 8 | 9 | export function startTimer () { 10 | if (Config.push.userInventoryChange == 0 || !Config.push.userInventoryTime) { 11 | return 12 | } 13 | clearInterval(timer) 14 | timer?.cancel?.() 15 | if (Number(Config.push.userInventoryTime)) { 16 | timer = setInterval(callback, 1000 * 60 * Config.push.userInventoryTime) 17 | } else { 18 | timer = schedule.scheduleJob(Config.push.userInventoryTime, callback) 19 | } 20 | } 21 | 22 | export async function callback () { 23 | logger.info('开始检查Steam个人库存信息') 24 | const pushList = await db.push.getAll({ inventory: true }, true) 25 | for (const i of _.uniqBy(pushList, 'steamId')) { 26 | try { 27 | const newGames = await api.IPlayerService.GetOwnedGames(i.steamId) 28 | const newGamesAppids = newGames.map(i => i.appid) 29 | const oldGamesAppids = await db.userInventory.get(i.steamId) 30 | // 缓存新库存信息 31 | await db.userInventory.set(i.steamId, newGamesAppids) 32 | if (!oldGamesAppids.length) { 33 | continue 34 | } 35 | const diff = _.difference(newGamesAppids, oldGamesAppids) 36 | if (!diff.length) { 37 | continue 38 | } 39 | const games = newGames.filter(i => diff.includes(i.appid)) 40 | for (const g of pushList.filter(p => p.steamId === i.steamId)) { 41 | const username = await utils.bot.getUserName(g.botId, g.userId, g.groupId) 42 | if (Config.push.pushMode == 1) { 43 | for (const i of games) { 44 | const msg = [ 45 | segment.image(i.image), 46 | `[Steam] ${username}的库存新增: ${i.name}` 47 | ] 48 | await utils.bot.sendGroupMsg(g.botId, g.groupId, msg) 49 | } 50 | } else { 51 | const img = await Render.render('inventory/index', { 52 | data: [{ 53 | title: `${username}的库存新增`, 54 | games 55 | }] 56 | }) 57 | await utils.bot.sendGroupMsg(g.botId, g.groupId, img) 58 | } 59 | } 60 | } catch { } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /models/task/userWishlist.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | import schedule from 'node-schedule' 4 | import { api, db, utils } from '#models' 5 | import { Config, Render } from '#components' 6 | import { logger, redis, segment } from '#lib' 7 | 8 | let timer = null 9 | 10 | const redisKey = 'steam-plugin:user-wishlist-time:' 11 | 12 | export function startTimer () { 13 | if (Config.push.userWishlistChange == 0 || !Config.push.userWishlistTime) { 14 | return 15 | } 16 | clearInterval(timer) 17 | timer?.cancel?.() 18 | if (Number(Config.push.userWishlistTime)) { 19 | timer = setInterval(callback, 1000 * 60 * Config.push.userWishlistTime) 20 | } else { 21 | timer = schedule.scheduleJob(Config.push.userWishlistTime, callback) 22 | } 23 | } 24 | 25 | export async function callback () { 26 | logger.info('开始检查Steam用户愿望单信息') 27 | const pushList = await db.push.getAll({ wishlist: true }, true) 28 | for (const i of _.uniqBy(pushList, 'steamId')) { 29 | try { 30 | const wishlist = await api.IWishlistService.GetWishlist(i.steamId) 31 | const lastTime = await redis.get(redisKey + i.steamId) 32 | const nowTime = moment().unix() 33 | redis.set(redisKey + i.steamId, nowTime) 34 | if (!lastTime) { 35 | continue 36 | } 37 | const addItems = wishlist.filter(i => i.date_added > Number(lastTime)) 38 | if (!addItems.length) { 39 | continue 40 | } 41 | const infoMap = await api.IStoreBrowseService.GetItems(addItems.map(i => i.appid)).catch(() => ({})) 42 | const games = addItems.map(i => { 43 | const info = infoMap[i.appid] 44 | return { 45 | ...i, 46 | name: info.name, 47 | desc: moment.unix(i.date_added).format('YYYY-MM-DD HH:mm:ss') 48 | } 49 | }) 50 | for (const g of pushList.filter(p => p.steamId === i.steamId)) { 51 | const username = await utils.bot.getUserName(g.botId, g.userId, g.groupId) 52 | if (Config.push.pushMode == 1) { 53 | for (const i of games) { 54 | const msg = [ 55 | segment.image(i.image), 56 | `[Steam] ${username}的愿望单新增: ${i.name}` 57 | ] 58 | await utils.bot.sendGroupMsg(g.botId, g.groupId, msg) 59 | } 60 | } else { 61 | const img = await Render.render('inventory/index', { 62 | data: [{ 63 | title: `${username}的愿望单新增`, 64 | games 65 | }] 66 | }) 67 | await utils.bot.sendGroupMsg(g.botId, g.groupId, img) 68 | } 69 | } 70 | } catch { } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /models/utils/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import axios from 'axios' 3 | import moment from 'moment' 4 | import { join } from 'path' 5 | import { logger } from '#lib' 6 | import { Version } from '#components' 7 | 8 | export * as bot from './bot.js' 9 | export * as steam from './steam.js' 10 | export * as request from './request.js' 11 | 12 | const tempDir = join(Version.pluginPath, 'temp') 13 | try { 14 | if (fs.existsSync(tempDir)) { 15 | fs.rmSync(tempDir, { recursive: true, force: true }) 16 | } 17 | fs.mkdirSync(tempDir) 18 | } catch { } 19 | 20 | /** 21 | * 将对应时间转换成时长字符串 22 | * @param {number} inp 23 | * @param {import('moment').DurationInputArg2} unit 24 | * @returns {string} 25 | */ 26 | export function formatDuration (inp, unit = 'seconds') { 27 | const duration = moment.duration(inp, unit) 28 | 29 | const days = duration.days() 30 | const hours = duration.hours() 31 | const minutes = duration.minutes() 32 | const secs = duration.seconds() 33 | 34 | let formatted = '' 35 | if (days > 0)formatted += `${days}天` 36 | if (hours > 0) formatted += `${hours}小时` 37 | if (minutes > 0) formatted += `${minutes}分钟` 38 | if (formatted === '' && secs > 0) formatted += `${secs}秒` 39 | 40 | return formatted.trim() 41 | } 42 | 43 | /** 44 | * 获取图片buffer 45 | * @param {string} url 46 | * @param {number} retry 重试次数 默认3 47 | * @returns {Promise} 48 | */ 49 | export async function getImgUrlBuffer (url, retry = 3) { 50 | if (!url) return null 51 | retry = Number(retry) || 3 52 | for (let i = 0; i < retry; i++) { 53 | try { 54 | const buffer = await axios.get(url, { 55 | responseType: 'arraybuffer' 56 | }).then(res => res.data) 57 | if (Version.BotName === 'Karin') { 58 | return `base64://${buffer.toString('base64')}` 59 | } else { 60 | return buffer 61 | } 62 | } catch (error) { 63 | logger.error(`获取图片${url}失败, 第${i + 1}次重试\n`, error.message) 64 | } 65 | } 66 | return null 67 | } 68 | 69 | /** 70 | * 将图片保存到临时文件夹 71 | * @param {*} url 72 | * @param {number} retry 重试次数 默认3 73 | * @returns {Promise} 图片绝对路径 74 | */ 75 | export async function saveImg (url, retry = 3) { 76 | if (!url) return '' 77 | retry = Number(retry) || 3 78 | for (let i = 0; i < retry; i++) { 79 | try { 80 | let ext = '' 81 | const buffer = await axios.get(url, { 82 | responseType: 'arraybuffer' 83 | }).then(res => { 84 | ext = res.headers['content-type']?.split('/')?.pop() || 'png' 85 | return res.data 86 | }) 87 | const filename = `${Date.now()}.${ext}` 88 | const filepath = join(tempDir, filename) 89 | fs.writeFileSync(filepath, buffer) 90 | setTimeout(() => { 91 | fs.unlinkSync(filepath) 92 | }, 1000 * 60 * 10) // 10分钟后删除 93 | return filepath.replace(/\\/g, '/') 94 | } catch (error) { 95 | logger.error(`保存图片${url}失败, 第${i + 1}次重试\n${error.message}`) 96 | } 97 | } 98 | return '' 99 | } 100 | -------------------------------------------------------------------------------- /models/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Config } from '#components' 3 | import { HttpProxyAgent } from 'http-proxy-agent' 4 | import { HttpsProxyAgent } from 'https-proxy-agent' 5 | import { logger, redis } from '#lib' 6 | import moment from 'moment' 7 | 8 | const redisKey = 'steam-plugin' 9 | // api请求次数前缀 10 | const redisApiKey = `${redisKey}:api:` 11 | // 429的key前缀 12 | const redis429Key = `${redisKey}:429key:` 13 | // key使用次数前缀 14 | const redisUseKey = `${redisKey}:useKey:` 15 | 16 | /** 17 | * 通用请求方法 18 | * @param {string} url 19 | * @param {import('axios').AxiosRequestConfig} options 20 | * @returns {Promise>} 21 | */ 22 | export default async function request (url, options = {}, retry = { count: 0, keys: Config.steam.apiKey }) { 23 | const steamApi = (() => { 24 | const url = 'https://api.steampowered.com' 25 | if (Config.steam.commonProxy) { 26 | return Config.steam.commonProxy.replace('{{url}}', url) 27 | } else if (Config.steam.apiProxy) { 28 | return Config.steam.apiProxy.replace(/\/$/, '') 29 | } else { 30 | return url 31 | } 32 | })() 33 | // 最大重试次数 34 | const maxRetry = Math.max(Math.ceil(Config.steam.apiKey.length * 1.5), 3) 35 | 36 | const baseURL = options.baseURL ?? steamApi 37 | logger.info(`开始请求api: ${url}`) 38 | 39 | const now = moment().format('YYYY-MM-DD') 40 | incr(`${redisApiKey}${now}:${url}`) 41 | 42 | const start = Date.now() 43 | let key = '' 44 | let keys = [] 45 | const needKeyFlag = baseURL === steamApi && !options.params?.access_token 46 | if (needKeyFlag) { 47 | let { retKeys, retKey } = await getKey(retry.keys) 48 | key = retKey 49 | keys = retKeys 50 | if (retKeys.length > 1) { 51 | logger.info(`获取请求的key: ${key.slice(0, 5)}...${key.slice(-5)}`) 52 | } 53 | } 54 | return await axios.request({ 55 | url, 56 | baseURL, 57 | httpAgent: Config.steam.proxy ? new HttpProxyAgent(Config.steam.proxy) : undefined, 58 | httpsAgent: Config.steam.proxy ? new HttpsProxyAgent(Config.steam.proxy) : undefined, 59 | ...options, 60 | params: { 61 | key: key || undefined, 62 | l: 'schinese', 63 | cc: Config.other.countryCode, 64 | language: 'schinese', 65 | ...options.params 66 | }, 67 | timeout: Config.steam.timeout * 1000 68 | }).then(res => { 69 | logger.info(`请求api成功: ${url}, 耗时: ${Date.now() - start}ms`) 70 | // 缓存使用的key 71 | if (key) { incr(`${redisUseKey}${now}:${key}`, 7) } 72 | return res.data 73 | }).catch(err => { 74 | if (err.status === 429 && keys.length > 1 && key && retry.count < maxRetry) { 75 | // 十分钟内不使用相同的key 76 | redis.set(`${redis429Key}${key}`, 1, { EX: 60 * 10 }) 77 | retry.count++ 78 | retry.keys = keys.filter(k => k !== key) 79 | logger.error(`请求api失败: ${url}, 状态码: ${err.status}, 更换apiKey开始重试第${retry.count}次`) 80 | return request(url, options, retry) 81 | } 82 | throw err 83 | }) 84 | } 85 | 86 | /** 87 | * get 请求方法 88 | * @param {string} url 89 | * @param {import('axios').AxiosRequestConfig} options 90 | * @returns {Promise>} 91 | */ 92 | export async function get (url, options = {}) { 93 | return await request(url, { 94 | ...options, 95 | method: 'GET' 96 | }) 97 | } 98 | 99 | /** 100 | * post 请求方法 101 | * @param {string} url 102 | * @param {import('axios').AxiosRequestConfig} options 103 | * @returns {Promise>} 104 | */ 105 | export async function post (url, options = {}) { 106 | return await request(url, { 107 | ...options, 108 | method: 'POST' 109 | }) 110 | } 111 | 112 | async function getKey (keys = Config.steam.apiKey) { 113 | const retKeys = [] 114 | const now = moment().format('YYYY-MM-DD') 115 | if (keys.length > 1) { 116 | for (const key of keys) { 117 | if (!await redis.exists(`${redis429Key}${key}`)) { 118 | retKeys.push(key) 119 | } 120 | } 121 | if (retKeys.length === 0) { 122 | retKeys.push(keys[0]) 123 | } 124 | } else { 125 | retKeys.push(...keys) 126 | } 127 | 128 | if (retKeys.length <= 1) { 129 | return { retKeys, retKey: retKeys[0] } 130 | } 131 | 132 | const keyNowUses = retKeys.map(k => `${redisUseKey}${now}:${k}`) 133 | const keyUses = await redis.mGet(keyNowUses) 134 | const count = Math.min(...keyUses.map(i => i || 0)) 135 | const keyIndex = keyUses.findIndex(k => Number(k) === count) || 0 136 | const key = retKeys[keyIndex] 137 | return { retKeys, retKey: key } 138 | } 139 | 140 | function incr (key, day = 3) { 141 | redis.incr(key).then((i) => { 142 | if (i == 1 && day > 0) { 143 | redis.expire(key, 60 * 60 * 24 * day).catch(() => {}) 144 | } 145 | }).catch(() => {}) 146 | } 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steam-plugin", 3 | "version": "1.12.0", 4 | "main": "index.js", 5 | "author": "XasYer", 6 | "type": "module", 7 | "scripts": {}, 8 | "dependencies": { 9 | "@napi-rs/canvas": "^0.1.65", 10 | "art-template": "4.13.2", 11 | "axios": "^1.7.8", 12 | "chalk": "^5.3.0", 13 | "http-proxy-agent": "^7.0.2", 14 | "https-proxy-agent": "^7.0.5", 15 | "lodash": "^4.17.21", 16 | "moment": "^2.30.1", 17 | "node-schedule": "^2.1.1", 18 | "puppeteer-screen-recorder": "^3.0.6", 19 | "qrcode": "^1.5.4", 20 | "sequelize": "^6.37.5", 21 | "sqlite3": "5.1.6" 22 | }, 23 | "imports": { 24 | "#components": "./components/index.js", 25 | "#models": "./models/index.js", 26 | "#lib": "./lib/index.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/SplitFiction/三人探戈.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/三人探戈.png -------------------------------------------------------------------------------- /resources/SplitFiction/不详的迎接.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/不详的迎接.png -------------------------------------------------------------------------------- /resources/SplitFiction/与神抗争.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/与神抗争.png -------------------------------------------------------------------------------- /resources/SplitFiction/世界分隔.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/世界分隔.png -------------------------------------------------------------------------------- /resources/SplitFiction/九头蛇.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/九头蛇.png -------------------------------------------------------------------------------- /resources/SplitFiction/你好,锤子先生.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/你好,锤子先生.png -------------------------------------------------------------------------------- /resources/SplitFiction/停车场.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/停车场.png -------------------------------------------------------------------------------- /resources/SplitFiction/全新视角.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/全新视角.png -------------------------------------------------------------------------------- /resources/SplitFiction/农场生活.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/农场生活.png -------------------------------------------------------------------------------- /resources/SplitFiction/冰封之王.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/冰封之王.png -------------------------------------------------------------------------------- /resources/SplitFiction/冰封殿堂.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/冰封殿堂.png -------------------------------------------------------------------------------- /resources/SplitFiction/分头行动.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/分头行动.png -------------------------------------------------------------------------------- /resources/SplitFiction/列车劫案.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/列车劫案.png -------------------------------------------------------------------------------- /resources/SplitFiction/勇武骑士.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/勇武骑士.png -------------------------------------------------------------------------------- /resources/SplitFiction/囚犯.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/囚犯.png -------------------------------------------------------------------------------- /resources/SplitFiction/地下世界.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/地下世界.png -------------------------------------------------------------------------------- /resources/SplitFiction/坠入奇境.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/坠入奇境.png -------------------------------------------------------------------------------- /resources/SplitFiction/塌缩之星.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/塌缩之星.png -------------------------------------------------------------------------------- /resources/SplitFiction/处决场.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/处决场.png -------------------------------------------------------------------------------- /resources/SplitFiction/大地之母.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/大地之母.png -------------------------------------------------------------------------------- /resources/SplitFiction/大逃亡.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/大逃亡.png -------------------------------------------------------------------------------- /resources/SplitFiction/大都市生活.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/大都市生活.png -------------------------------------------------------------------------------- /resources/SplitFiction/太空逃生.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/太空逃生.png -------------------------------------------------------------------------------- /resources/SplitFiction/宝藏之庙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/宝藏之庙.png -------------------------------------------------------------------------------- /resources/SplitFiction/宝藏叛徒.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/宝藏叛徒.png -------------------------------------------------------------------------------- /resources/SplitFiction/实用无人机.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/实用无人机.png -------------------------------------------------------------------------------- /resources/SplitFiction/实验室.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/实验室.png -------------------------------------------------------------------------------- /resources/SplitFiction/屠龙者.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/屠龙者.png -------------------------------------------------------------------------------- /resources/SplitFiction/工厂入口.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/工厂入口.png -------------------------------------------------------------------------------- /resources/SplitFiction/工厂外围.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/工厂外围.png -------------------------------------------------------------------------------- /resources/SplitFiction/工艺之庙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/工艺之庙.png -------------------------------------------------------------------------------- /resources/SplitFiction/巨石之怒.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/巨石之怒.png -------------------------------------------------------------------------------- /resources/SplitFiction/废物处理.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/废物处理.png -------------------------------------------------------------------------------- /resources/SplitFiction/弹珠锁.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/弹珠锁.png -------------------------------------------------------------------------------- /resources/SplitFiction/战争坡道.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/战争坡道.png -------------------------------------------------------------------------------- /resources/SplitFiction/打破常规.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/打破常规.png -------------------------------------------------------------------------------- /resources/SplitFiction/攀登摩天楼.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/攀登摩天楼.png -------------------------------------------------------------------------------- /resources/SplitFiction/暗夜之光.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/暗夜之光.png -------------------------------------------------------------------------------- /resources/SplitFiction/最高安全级别.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/最高安全级别.png -------------------------------------------------------------------------------- /resources/SplitFiction/月亮市集.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/月亮市集.png -------------------------------------------------------------------------------- /resources/SplitFiction/机动战术.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/机动战术.png -------------------------------------------------------------------------------- /resources/SplitFiction/枪械升级.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/枪械升级.png -------------------------------------------------------------------------------- /resources/SplitFiction/森林之心.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/森林之心.png -------------------------------------------------------------------------------- /resources/SplitFiction/横截面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/横截面.png -------------------------------------------------------------------------------- /resources/SplitFiction/毁灭性的移动树干.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/毁灭性的移动树干.png -------------------------------------------------------------------------------- /resources/SplitFiction/毒液滚筒.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/毒液滚筒.png -------------------------------------------------------------------------------- /resources/SplitFiction/水之庙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/水之庙.png -------------------------------------------------------------------------------- /resources/SplitFiction/沙鱼传奇.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/沙鱼传奇.png -------------------------------------------------------------------------------- /resources/SplitFiction/深入风暴.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/深入风暴.png -------------------------------------------------------------------------------- /resources/SplitFiction/温暖的问候.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/温暖的问候.png -------------------------------------------------------------------------------- /resources/SplitFiction/潜入行动.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/潜入行动.png -------------------------------------------------------------------------------- /resources/SplitFiction/灵魂向导.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/灵魂向导.png -------------------------------------------------------------------------------- /resources/SplitFiction/牢房片区.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/牢房片区.png -------------------------------------------------------------------------------- /resources/SplitFiction/犯罪集团首领.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/犯罪集团首领.png -------------------------------------------------------------------------------- /resources/SplitFiction/生日蛋糕.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/生日蛋糕.png -------------------------------------------------------------------------------- /resources/SplitFiction/电音律动.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/电音律动.png -------------------------------------------------------------------------------- /resources/SplitFiction/登山远足.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/登山远足.png -------------------------------------------------------------------------------- /resources/SplitFiction/皇宫.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/皇宫.png -------------------------------------------------------------------------------- /resources/SplitFiction/监狱大院.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/监狱大院.png -------------------------------------------------------------------------------- /resources/SplitFiction/监狱飞船.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/监狱飞船.png -------------------------------------------------------------------------------- /resources/SplitFiction/监督者.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/监督者.png -------------------------------------------------------------------------------- /resources/SplitFiction/神威之龙.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/神威之龙.png -------------------------------------------------------------------------------- /resources/SplitFiction/空中亡命.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/空中亡命.png -------------------------------------------------------------------------------- /resources/SplitFiction/笔记本.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/笔记本.png -------------------------------------------------------------------------------- /resources/SplitFiction/系统安全模式.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/系统安全模式.png -------------------------------------------------------------------------------- /resources/SplitFiction/终极决战.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/终极决战.png -------------------------------------------------------------------------------- /resources/SplitFiction/翻转都市.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/翻转都市.png -------------------------------------------------------------------------------- /resources/SplitFiction/自由斗士.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/自由斗士.png -------------------------------------------------------------------------------- /resources/SplitFiction/节目游戏.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/节目游戏.png -------------------------------------------------------------------------------- /resources/SplitFiction/蜿蜒小路.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/蜿蜒小路.png -------------------------------------------------------------------------------- /resources/SplitFiction/蠢猴子.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/蠢猴子.png -------------------------------------------------------------------------------- /resources/SplitFiction/补水设施.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/补水设施.png -------------------------------------------------------------------------------- /resources/SplitFiction/记忆碎片.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/记忆碎片.png -------------------------------------------------------------------------------- /resources/SplitFiction/运输船.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/运输船.png -------------------------------------------------------------------------------- /resources/SplitFiction/重力摩托.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/重力摩托.png -------------------------------------------------------------------------------- /resources/SplitFiction/长青领主.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/长青领主.png -------------------------------------------------------------------------------- /resources/SplitFiction/霓虹街道.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/霓虹街道.png -------------------------------------------------------------------------------- /resources/SplitFiction/面对面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/面对面.png -------------------------------------------------------------------------------- /resources/SplitFiction/风筝.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/风筝.png -------------------------------------------------------------------------------- /resources/SplitFiction/驾车逃离.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/驾车逃离.png -------------------------------------------------------------------------------- /resources/SplitFiction/高峰时间.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/高峰时间.png -------------------------------------------------------------------------------- /resources/SplitFiction/鬼镇.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/鬼镇.png -------------------------------------------------------------------------------- /resources/SplitFiction/龙骑士大团结.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/龙骑士大团结.png -------------------------------------------------------------------------------- /resources/SplitFiction/龙魂.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/SplitFiction/龙魂.png -------------------------------------------------------------------------------- /resources/common/font/MiSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/common/font/MiSans-Bold.ttf -------------------------------------------------------------------------------- /resources/common/font/MiSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/common/font/MiSans-Light.ttf -------------------------------------------------------------------------------- /resources/common/font/MiSans-Normal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/common/font/MiSans-Normal.ttf -------------------------------------------------------------------------------- /resources/common/layout/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | steam-plugin 12 | {{block 'css'}} 13 | {{/block}} 14 | 15 | 16 |
17 | {{block 'main'}}{{/block}} 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/game/friend_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/game/friend_add.png -------------------------------------------------------------------------------- /resources/game/friend_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/game/friend_bg.png -------------------------------------------------------------------------------- /resources/game/game.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | #container { 8 | width: 402px; 9 | } 10 | 11 | .box { 12 | position: relative; 13 | height: 105px; 14 | background-image: url("./game.png"); 15 | } 16 | 17 | .avatar { 18 | position: absolute; 19 | top: 20px; 20 | left: 15px; 21 | } 22 | 23 | .info { 24 | position: absolute; 25 | top: 16px; 26 | left: 100px; 27 | height: 70px; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-between; 31 | width: 290px; 32 | } 33 | 34 | .info>div { 35 | white-space: nowrap; 36 | text-overflow: ellipsis; 37 | overflow: hidden; 38 | } 39 | 40 | .name { 41 | font-size: 19px; 42 | color: #e3ffc2; 43 | } 44 | 45 | .desc { 46 | font-size: 15px; 47 | color: #969696; 48 | } 49 | 50 | .game { 51 | font-size: 14px; 52 | color: #91c257; 53 | font-weight: bold; 54 | } 55 | 56 | .online { 57 | font-size: 14px; 58 | color: #beee11; 59 | font-weight: bold; 60 | } 61 | 62 | .offline { 63 | font-size: 14px; 64 | color: #999999; 65 | font-weight: bold; 66 | } -------------------------------------------------------------------------------- /resources/game/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | steam-plugin 8 | 9 | 10 | 11 | 12 |
13 | {{each data i}} 14 |
15 |
16 | 头像 17 |
18 |
19 | {{if i.isAvatar}} 20 |
{{i.name}}
21 |
{{i.desc}}
22 | {{else}} 23 |
{{i.detail}}
24 |
{{i.type === 'start' ? '正在玩' : '结束玩'}}
25 |
{{i.name}}
26 | {{/if}} 27 |
28 |
29 | {{/each}} 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /resources/game/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/game/game.png -------------------------------------------------------------------------------- /resources/help/help.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/help.jpg -------------------------------------------------------------------------------- /resources/help/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/icon.png -------------------------------------------------------------------------------- /resources/help/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 830px; 4 | } 5 | .container { 6 | background-size: 100% auto; 7 | width: 830px; 8 | } 9 | .head-box { 10 | margin: 60px 0 0 0; 11 | padding-bottom: 0; 12 | } 13 | .head-box .title { 14 | font-size: 50px; 15 | } 16 | .cont-box { 17 | border-radius: 15px; 18 | margin-top: 20px; 19 | margin-bottom: 20px; 20 | overflow: hidden; 21 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15); 22 | position: relative; 23 | } 24 | .help-group { 25 | font-size: 18px; 26 | font-weight: bold; 27 | padding: 15px 15px 10px 20px; 28 | } 29 | .help-table { 30 | text-align: center; 31 | border-collapse: collapse; 32 | margin: 0; 33 | border-radius: 0 0 10px 10px; 34 | display: table; 35 | overflow: hidden; 36 | width: 100%; 37 | color: #fff; 38 | } 39 | .help-table .tr { 40 | display: table-row; 41 | } 42 | .help-table .td, 43 | .help-table .th { 44 | font-size: 14px; 45 | display: table-cell; 46 | box-shadow: 0 0 1px 0 #888 inset; 47 | padding: 12px 0 12px 50px; 48 | line-height: 24px; 49 | position: relative; 50 | text-align: left; 51 | } 52 | .help-table .tr:last-child .td { 53 | padding-bottom: 12px; 54 | } 55 | .help-table .th { 56 | background: rgba(34, 41, 51, 0.5); 57 | } 58 | .help-icon { 59 | width: 40px; 60 | height: 40px; 61 | display: block; 62 | position: absolute; 63 | background: url("icon.png") 0 0 no-repeat; 64 | background-size: 500px auto; 65 | border-radius: 5px; 66 | left: 6px; 67 | top: 12px; 68 | transform: scale(0.85); 69 | } 70 | .help-title { 71 | display: block; 72 | color: #d3bc8e; 73 | font-size: 16px; 74 | line-height: 24px; 75 | } 76 | .help-desc { 77 | display: block; 78 | font-size: 13px; 79 | line-height: 18px; 80 | } 81 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /resources/help/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | <% style = style.replace(/{{pluResPath}}/g, pluResPath) %> 6 | {{@style}} 7 | {{/block}} 8 | 9 | {{block 'main'}} 10 | 11 |
12 |
13 |
steam帮助
14 |
15 |
16 | 17 | {{each helpGroup group}} 18 | {{set len = group?.list?.length || 0 }} 19 |
20 |
{{group.group}}
21 | {{if len > 0}} 22 |
23 |
24 | {{each group.list help idx}} 25 |
26 | 27 | {{help.title}} 28 | {{help.desc}} 29 |
30 | {{if idx%colCount === colCount-1 && idx>0 && idx< len-1}} 31 |
32 |
33 | {{/if}} 34 | {{/each}} 35 | <% for(let i=(len-1)%colCount; i< colCount-1 ; i++){ %> 36 |
37 | <% } %> 38 |
39 |
40 | {{/if}} 41 |
42 | {{/each}} 43 | {{/block}} 44 | -------------------------------------------------------------------------------- /resources/help/theme/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/theme/1.png -------------------------------------------------------------------------------- /resources/help/theme/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/theme/2.png -------------------------------------------------------------------------------- /resources/help/theme/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/theme/3.png -------------------------------------------------------------------------------- /resources/help/theme/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/theme/4.png -------------------------------------------------------------------------------- /resources/help/theme/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/help/theme/5.png -------------------------------------------------------------------------------- /resources/help/version-info.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | user-select: none; 6 | } 7 | body { 8 | font-size: 18px; 9 | color: #1e1f20; 10 | transform: scale(1.3); 11 | transform-origin: 0 0; 12 | width: 600px; 13 | } 14 | .container { 15 | background: url("./theme/5.png") top left repeat; 16 | background-size: 100% auto; 17 | width: 600px; 18 | padding: 10px 0 10px 0; 19 | } 20 | .log-cont { 21 | background-size: cover; 22 | margin: 5px 15px 5px 10px; 23 | border-radius: 10px; 24 | } 25 | .log-cont .cont { 26 | margin: 0; 27 | } 28 | .log-cont .cont-title { 29 | font-size: 16px; 30 | padding: 10px 20px 6px; 31 | } 32 | .log-cont .cont-title.current-version { 33 | font-size: 20px; 34 | } 35 | .log-cont ul { 36 | font-size: 14px; 37 | padding-left: 20px; 38 | } 39 | .log-cont ul li { 40 | margin: 3px 0; 41 | } 42 | .log-cont ul.sub-log-ul li { 43 | margin: 1px 0; 44 | } 45 | .log-cont .cmd { 46 | color: #d3bc8e; 47 | display: inline-block; 48 | border-radius: 3px; 49 | background: rgba(0, 0, 0, 0.5); 50 | padding: 0 3px; 51 | margin: 1px 2px; 52 | } 53 | .log-cont .strong { 54 | color: #24d5cd; 55 | } 56 | .log-cont .new { 57 | display: inline-block; 58 | width: 18px; 59 | margin: 0 -3px 0 1px; 60 | } 61 | .log-cont .new:before { 62 | content: "NEW"; 63 | display: inline-block; 64 | transform: scale(0.6); 65 | transform-origin: 0 0; 66 | color: #d3bc8e; 67 | white-space: nowrap; 68 | } 69 | .dev-cont { 70 | background: none; 71 | } 72 | .dev-cont .cont-title { 73 | background: rgba(0, 0, 0, 0.7); 74 | } 75 | .dev-cont .cont-body { 76 | background: rgba(0, 0, 0, 0.5); 77 | } 78 | .dev-cont .cont-body.dev-info { 79 | background: rgba(0, 0, 0, 0.2); 80 | } 81 | .dev-cont .strong { 82 | font-size: 15px; 83 | } 84 | /*# sourceMappingURL=version-info.css.map */ 85 | -------------------------------------------------------------------------------- /resources/help/version-info.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{/block}} 6 | 7 | {{block 'main'}} 8 | {{each changelogs ds idx}} 9 |
10 | {{set v = ds.version }} 11 |
12 | {{if idx === 0 }} 13 |
当前版本 {{v}}
14 | {{else}} 15 |
{{v}}
16 | {{/if}} 17 |
18 |
    19 | {{each ds.logs log}} 20 |
  • 21 |

    {{@log.title}}

    22 | {{if log.logs.length > 0}} 23 |
      24 | {{each log.logs ls}} 25 |
    • {{@ls}}
    • 26 | {{/each}} 27 |
    28 | {{/if}} 29 |
  • 30 | {{/each}} 31 |
32 |
33 |
34 |
35 | {{/each}} 36 | {{/block}} 37 | -------------------------------------------------------------------------------- /resources/info/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | #container { 8 | width: 593px; 9 | height: 561px; 10 | font-size: 20px; 11 | text-shadow: 12 | -1px -1px 0 black, 13 | 1px -1px 0 black, 14 | -1px 1px 0 black, 15 | 1px 1px 0 black; 16 | } 17 | 18 | .bg-webm { 19 | position: absolute; 20 | z-index: -1; 21 | top: 0; 22 | left: 0; 23 | } 24 | 25 | .bg-webm video { 26 | width: 593px; 27 | height: 561px; 28 | object-fit: cover; 29 | } 30 | 31 | #profile { 32 | width: 563px; 33 | height: 502px; 34 | padding: 20px; 35 | } 36 | 37 | #avatar-box { 38 | display: flex; 39 | margin-bottom: 10px; 40 | } 41 | 42 | #avatar-frame { 43 | position: absolute; 44 | width: 164px; 45 | height: 164px; 46 | } 47 | 48 | #avatar-frame>img { 49 | width: 200px; 50 | height: 200px; 51 | margin: -18px; 52 | } 53 | 54 | #name-box { 55 | margin-left: 20px; 56 | display: flex; 57 | flex-direction: column; 58 | justify-content: center; 59 | } 60 | 61 | #profile-name { 62 | font-size: 30px; 63 | white-space: nowrap; 64 | } 65 | 66 | #profile-status { 67 | margin-top: 10px; 68 | } 69 | 70 | #playing-box { 71 | margin-top: 20px; 72 | display: flex; 73 | } 74 | 75 | #playing-info { 76 | margin-left: 20px; 77 | display: flex; 78 | flex-direction: column; 79 | justify-content: center; 80 | } 81 | 82 | #profile-info { 83 | line-height: 2; 84 | } 85 | -------------------------------------------------------------------------------- /resources/info/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | {{block 'css'}} 3 | 4 | {{if !toGif}} 5 | 11 | {{/if}} 12 | {{/block}} 13 | 14 | {{block 'main'}} 15 | 16 | {{if toGif}} 17 |
18 | 21 |
22 | {{/if}} 23 | 24 |
25 |
26 | {{if frame}} 27 |
28 | 29 |
30 | {{/if}} 31 | 32 |
33 |
{{name}}
34 |
{{status}}
35 |
36 |
37 | {{if gameId}} 38 |
39 |
40 | 41 |
42 |
43 |
游戏中
44 |
{{gameName}}
45 |
46 |
47 | {{/if}} 48 |
49 |
50 | 好友代码: {{friendCode}} 51 |
52 |
53 | 注册时间: {{createTime}} 54 |
55 | {{if lastTime}} 56 |
57 | 最后在线: {{lastTime}} 58 |
59 | {{/if}} 60 | {{if country}} 61 |
62 | 账号地区: {{country}} 63 |
64 | {{/if}} 65 |
66 |
67 | {{/block}} 68 | -------------------------------------------------------------------------------- /resources/info/steam.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | steam-plugin 8 | 9 | 10 | 24 | 25 | 26 | 27 |
28 | {{@ html}} 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/inventory/index.css: -------------------------------------------------------------------------------- 1 | #container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .header { 8 | text-align: center; 9 | margin-bottom: 10px; 10 | } 11 | 12 | .games { 13 | display: flex; 14 | justify-content: space-around; 15 | flex-wrap: wrap; 16 | } 17 | 18 | .games>div { 19 | position: relative; 20 | box-sizing: border-box; 21 | display: flex; 22 | padding: 5px; 23 | border: 1px solid #ccc; 24 | border-radius: 5px; 25 | margin-bottom: 10px; 26 | } 27 | 28 | .index{ 29 | position: absolute; 30 | line-height: 0.8; 31 | top: -4px; 32 | left: 12px; 33 | font-size: 12px; 34 | background-color: white; 35 | } 36 | 37 | .appid{ 38 | position: absolute; 39 | line-height: 0.8; 40 | top: -4px; 41 | right: 12px; 42 | font-size: 12px; 43 | background-color: white; 44 | } 45 | 46 | .game-content { 47 | align-items: center; 48 | width: 360px; 49 | } 50 | 51 | .header-img>img { 52 | height: 56px; 53 | border-radius: 5px; 54 | vertical-align: middle; 55 | } 56 | 57 | .square { 58 | width: 56px; 59 | } 60 | 61 | .rectangle { 62 | width: 120px; 63 | } 64 | 65 | .game-info { 66 | margin-left: 5px; 67 | display: flex; 68 | flex-direction: column; 69 | justify-content: space-between; 70 | /* height: 60px; */ 71 | line-height: 20px; 72 | } 73 | 74 | .game-info-size-large { 75 | width: 340px; 76 | } 77 | 78 | .game-info-size-small { 79 | width: 170px; 80 | } 81 | 82 | .overflow { 83 | overflow: hidden; 84 | text-overflow: ellipsis; 85 | white-space: nowrap; 86 | } 87 | 88 | .game-title { 89 | font-size: 16px; 90 | font-weight: bold; 91 | color: #333; 92 | } 93 | 94 | .game-detail { 95 | font-size: 16px; 96 | color: #666; 97 | } 98 | 99 | .game-desc { 100 | font-size: 14px; 101 | color: #999; 102 | } 103 | 104 | .discount-percent { 105 | background-color: #beee11; 106 | border-radius: 5px; 107 | padding: 2px; 108 | font-size: 10px; 109 | color: #333; 110 | } 111 | 112 | .none { 113 | display: none; 114 | } 115 | 116 | .through { 117 | text-decoration: line-through; 118 | color: #999; 119 | } -------------------------------------------------------------------------------- /resources/inventory/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{@ style}} 6 | {{/block}} 7 | 8 | {{block 'main'}} 9 | {{each data item}} 10 | 11 |
12 |

{{item.title}}

13 | {{each item.desc desc}} 14 |

15 | {{desc}} 16 |

17 | {{/each}} 18 |
19 |
20 | {{each item.games i idx}} 21 |
22 |
No. {{idx+1}}
23 | {{if i.appid}} 24 |
Appid: {{i.appid}}
25 | {{/if}} 26 |
27 | {{if i.image}} 28 | 29 | {{/if}} 30 |
31 |
32 |
33 | {{i.name || ''}} 34 |
35 |
38 | {{i.detail || ''}} 39 |
40 |
43 | {{i.desc || ''}} 44 |
45 |
46 | {{if i.price}} 47 |
48 |
49 | {{i.price.original || ''}} 50 |
51 | {{if i.price.discount}} 52 |
53 | - {{i.price.discount}}% 54 |
55 |
56 | {{i.price.current}} 57 |
58 | {{/if}} 59 |
60 | {{/if}} 61 |
62 | {{/each}} 63 |
64 | {{/each}} 65 | {{/block}} -------------------------------------------------------------------------------- /resources/review/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{/block}} 6 | 7 | {{block 'main'}} 8 | 9 |
10 | {{name}}没有找到图片 12 |
13 | {{state}} {{honor ? `(${honor})` : ''}} 14 |
15 |
16 | {{@ review}} 17 | {{/block}} -------------------------------------------------------------------------------- /resources/setting/imgs/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/setting/imgs/bg.png -------------------------------------------------------------------------------- /resources/setting/imgs/cfg-right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/setting/imgs/cfg-right.jpg -------------------------------------------------------------------------------- /resources/setting/imgs/cfg-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/setting/imgs/cfg-right.png -------------------------------------------------------------------------------- /resources/setting/index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background: url("./imgs/bg.png") #000144 left top repeat-y; 3 | background-size: 1320px auto; 4 | width: 1320px; 5 | } 6 | 7 | .box { 8 | column-count: 2; 9 | column-gap: 10px; 10 | } 11 | 12 | .cfg-box { 13 | border-radius: 15px; 14 | margin-bottom: 20px; 15 | padding: 5px 15px; 16 | overflow: hidden; 17 | background: #f5f5f5; 18 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15); 19 | position: relative; 20 | background: rgba(35, 38, 57, 0.8); 21 | } 22 | .cfg-group { 23 | color: #ceb78b; 24 | font-size: 18px; 25 | font-weight: bold; 26 | padding: 10px 20px; 27 | } 28 | .cfg-li { 29 | border-radius: 18px; 30 | min-height: 36px; 31 | position: relative; 32 | overflow: hidden; 33 | margin-bottom: 10px; 34 | background: rgba(203, 196, 190, 0); 35 | } 36 | .cfg-line { 37 | color: #4e5769; 38 | line-height: 36px; 39 | padding-left: 20px; 40 | font-weight: bold; 41 | border-radius: 16px; 42 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 43 | background: url("./imgs/cfg-right.jpg") right top #cbc4be no-repeat; 44 | background-size: auto 36px; 45 | } 46 | .cfg-hint { 47 | font-size: 12px; 48 | font-weight: normal; 49 | margin-top: 3px; 50 | margin-bottom: -3px; 51 | } 52 | .cfg-status { 53 | position: absolute; 54 | top: 0; 55 | right: 0; 56 | height: 36px; 57 | width: 160px; 58 | text-align: center; 59 | line-height: 36px; 60 | font-size: 16px; 61 | color: #495366; 62 | font-weight: bold; 63 | border-radius: 0 16px 16px 0; 64 | } 65 | .cfg-status.status-off { 66 | color: #a95151; 67 | } 68 | .cfg-desc { 69 | font-size: 12px; 70 | color: #cbc4be; 71 | margin: 5px 0 5px 20px; 72 | } 73 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /resources/setting/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | {{block 'css'}} 3 | 4 | {{/block}} 5 | {{block 'main'}} 6 | 7 |
8 |
9 |
steam管理面板
10 |
#steam设置
11 |
12 |
13 | 14 |
15 | {{each schema cfgGroup cfgGroupKey}} 16 |
17 |
{{cfgGroup.title}}
18 |
    19 | {{each cfgGroup.cfg cfgItem cfgKey}} 20 |
  • 21 |
    22 | {{cfgItem.title}} 23 | #steam{{cfgItem.type === 'array' ? '添加/删除' : '设置'}}{{cfgItem.key}} 24 | {{if cfgItem.type==='number'}} 25 | {{cfgItem.def}} 26 | {{else if cfgItem.type==='string'}} 27 | xxx 28 | {{else if cfgItem.type==='boolean'}} 29 | 开启/关闭 30 | {{else if cfgItem.type==='array'}} 31 | xxx 32 | {{/if}} 33 | 34 | {{if cfgItem.type === 'number'}} 35 |
    {{cfg[cfgKey]}}
    36 | {{else if cfgItem.type ==='boolean'}} 37 | {{if cfg[cfgKey]}} 38 |
    已开启
    39 | {{else}} 40 |
    已关闭
    41 | {{/if}} 42 | {{else if cfgItem.type ==='string'}} 43 | {{if cfg[cfgKey]}} 44 |
    {{cfgGroupKey === 'steam' ? '已设置' : cfg[cfgKey]}}
    45 | {{else}} 46 |
    未设置
    47 | {{/if}} 48 | {{else if cfgItem.type === 'array'}} 49 |
    已配置{{cfg[cfgKey]?.length || 0}}项
    50 | {{/if}} 51 |
    52 | {{if cfgItem.desc && cfgItem.showDesc!== false}} 53 |
    {{cfgItem.desc}}
    54 | {{/if}} 55 |
  • 56 | {{/each}} 57 |
58 |
59 | {{/each}} 60 |
61 | {{/block}} -------------------------------------------------------------------------------- /resources/user/check.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XasYer/steam-plugin/cab6d1b96384d7ddf264407c8a4694553582ab0f/resources/user/check.webp -------------------------------------------------------------------------------- /resources/user/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | 10 | {{/block}} 11 | 12 | {{block 'main'}} 13 | 14 |
15 | {{each data i}} 16 |
17 |
18 | {{i.title}} 19 | 使用#steam更换绑定1切换steamId 20 |
21 |
22 | {{each i.list uid}} 23 |
24 |
25 | {{uid.index}} 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
{{uid.steamId}}{{uid.steamId}}
35 |
{{uid.type==='ck'?'扫码':'绑定'}}
36 |
37 |
38 |
{{uid.name}}
39 |
40 | {{if groupPermission}} 41 |
42 | {{if play}} 43 |
游玩推送
44 | {{/if}} 45 | {{if state}} 46 |
状态推送
47 | {{/if}} 48 | {{if inventory}} 49 |
库存推送
50 | {{/if}} 51 | {{if wishlist}} 52 |
愿望单推送
53 | {{/if}} 54 |
55 | {{/if}} 56 |
57 |
58 |
59 |
60 | {{/each}} 61 |
62 | 63 |
64 | {{/each}} 65 |
66 | 67 |
    68 |
  • 69 | #steam解除绑定1 70 | 删除指定序号steamId 71 |
  • 72 |
  • 73 | #steam绑定123456789 74 | 绑定对应steamId或好友码 75 |
  • 76 | {{if groupPermission}} 77 |
  • 78 | #steam(开启/关闭)游玩推送1 79 | 可以开启或关闭对应序号steamId的对应推送功能 80 |
  • 81 | {{/if}} 82 |
83 | {{/block}} --------------------------------------------------------------------------------