This commit is contained in:
熊潇 2025-05-06 22:16:31 +08:00
commit 54f5caeeaa
33 changed files with 7191 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

24
js-demo/client/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

5
js-demo/client/README.md Normal file
View File

@ -0,0 +1,5 @@
### DEMO
此处为验证API的调试环境
By Vite

13
js-demo/client/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2694
js-demo/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "poros-demo-client",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --port 8888 --host 0.0.0.0 ",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@originjs/vite-plugin-commonjs": "1.0.3",
"@types/node": "17.0.25",
"@vitejs/plugin-vue": "2.3.0",
"axios": "0.26.1",
"typescript": "4.5.4",
"vite": "2.9.0",
"vue-tsc": "0.29.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

196
js-demo/client/src/App.vue Normal file
View File

@ -0,0 +1,196 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import { ref } from "vue"
import axios from "axios"
import { createSocket, destroySocket } from "./socket/index"
const api = axios.create({
baseURL: "http://localhost:3000",
})
//
const appKey = ref("")
const appSecret = ref("")
//
const codeId = ref("")
// app []
const appId = ref("")
// [ node server]
const gameId = ref("")
// v2server response websocket
const authBody = ref("")
const wssLinks = ref([])
// heartBeat Timer
const heartBeatTimer = ref<NodeJS.Timer>()
// be ready
clearInterval(heartBeatTimer.value!)
/**
* 测试请求鉴权接口
*/
const getAuth = () => {
api.post("/getAuth", {
appKey: appKey.value,
appSecret: appSecret.value,
})
.then(({ data }) => {
console.log("-----鉴权成功-----")
console.log("返回:", data)
})
.catch((err) => {
console.log("-----鉴权失败-----")
})
}
const heartBeatThis = (game_id) => {
//
api.post("/gameHeartBeat", {
game_id,
})
.then(({ data }) => {
console.log("-----心跳成功-----")
console.log("返回:", data)
})
.catch((err) => {
console.log("-----心跳失败-----")
})
}
/**
* @comment 注意所有的接口基于鉴权成功后才能正确返回
* 测试请求游戏开启接口
*/
const gameStart = () => {
api.post("/gameStart", {
code: codeId.value,
app_id: Number(appId.value),
})
.then(({ data }) => {
if (data.code === 0) {
const res = data.data
const { game_info, websocket_info } = res
const { auth_body, wss_link } = websocket_info
authBody.value = auth_body
wssLinks.value = wss_link
console.log("-----游戏开始成功-----")
console.log("返回GameId", game_info)
gameId.value = game_info.game_id
// v220s60s
heartBeatTimer.value = setInterval(() => {
heartBeatThis(game_info.game_id)
}, 20000)
} else {
console.log("-----游戏开始失败-----")
console.log("原因:", data)
}
})
.catch((err) => {
console.log("-----游戏开始失败-----")
console.log(err)
})
}
/**
* @comment 基于gameStart成功后才会关闭正常否则获取不到game_id
* 测试请求游戏关闭接口
*/
const gameEnd = () => {
api.post("/gameEnd", {
game_id: gameId.value,
app_id: Number(appId.value),
})
.then(({ data }) => {
if (data.code === 0) {
console.log("-----游戏关闭成功-----")
console.log("返回:", data)
//
authBody.value = ""
wssLinks.value = []
clearInterval(heartBeatTimer.value)
handleDestroySocket()
console.log("-----心跳关闭成功-----")
} else {
console.log("-----游戏关闭失败-----")
console.log("原因:", data)
}
})
.catch((err) => {
console.log("-----游戏关闭失败-----")
console.log(err)
})
}
/**
* 测试创建长长连接接口
*/
const handleCreateSocket = () => {
if (authBody.value && wssLinks.value) {
createSocket(authBody.value, wssLinks.value)
}
}
/**
* 测试销毁长长连接接口
*/
const handleDestroySocket = () => {
destroySocket()
console.log("-----长连接销毁成功-----")
}
</script>
<template>
<div>
<h3>-- !-- 所有输出信息都在控制台 -- --</h3>
<div>鉴权部分</div>
<div class="form">
<input
type="text"
placeholder="填写access_key_id"
v-model="appKey"
/>
<input
type="text"
placeholder="填写access_key_secret"
v-model="appSecret"
/>
<button @click="getAuth">鉴权</button>
</div>
<hr />
<div>程序开启部分 [需要先鉴权]</div>
<div class="form">
<input type="text" placeholder="填写主播身份码" v-model="codeId" />
<input type="text" placeholder="填写 app_id" v-model="appId" />
<button @click="gameStart">游戏开始</button>
<button @click="gameEnd">游戏结束</button>
</div>
<hr />
<div>长连接部分 [需要先游戏开启]</div>
<div class="form">
<button @click="handleCreateSocket">建立长连接</button>
<button @click="handleDestroySocket">销毁长连接</button>
</div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.form {
display: flex;
flex-direction: column;
}
.form input,
.form button {
width: 400px;
height: 50px;
margin: 10px 0;
}
</style>

File diff suppressed because one or more lines are too long

9
js-demo/client/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -0,0 +1,5 @@
import { createApp } from "vue"
import App from "./App.vue"
createApp(App).mount("#app")

View File

@ -0,0 +1,80 @@
import DanmakuWebSocket from "../assets/danmaku-websocket.min.js"
let ws: DanmakuWebSocket
/**
* socket长连接
* @param authBody
* @param wssLinks
*/
function createSocket(authBody: string, wssLinks: string[]) {
const opt = {
...getWebSocketConfig(authBody, wssLinks),
// 收到消息,
onReceivedMessage: (res) => {
console.log(res)
},
// 收到心跳处理回调
onHeartBeatReply: (data) => console.log("收到心跳处理回调:", data),
onError: (data) => console.log("error", data),
onListConnectError: () => {
console.log("list connect error")
destroySocket()
},
}
if (!ws) {
ws = new DanmakuWebSocket(opt)
}
return ws
}
/**
* websocket配置信息
* @param authBody
* @param wssLinks
*/
function getWebSocketConfig(authBody: string, wssLinks: string[]) {
const url = wssLinks[0]
const urlList = wssLinks
const auth_body = JSON.parse(authBody)
return {
url,
urlList,
customAuthParam: [
{
key: "key",
value: auth_body.key,
type: "string",
},
{
key: "group",
value: auth_body.group,
type: "string",
},
],
rid: auth_body.roomid,
protover: auth_body.protoover,
uid: auth_body.uid,
}
}
/**
* websocket
*/
function destroySocket() {
console.log("destroy1")
ws && ws.destroy()
ws = undefined
console.log("destroy2")
}
/**
* websocket实例
*/
function getWsClient() {
return ws
}
export { createSocket, destroySocket, getWebSocketConfig, getWsClient }

19
js-demo/client/src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
declare interface ISocketData {
// ip地址
ip: number[]
// host地址 可能是ip 也可能是域名。
host: string[]
// 长连使用的请求json体 第三方无需关注内容,建立长连时使用即可。
auth_body: string
// tcp 端口号
tcp_port: number[]
// ws 端口号
ws_port: number[]
// wss 端口号
wss_port: number[]
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"noImplicitAny": false,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,17 @@
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import { viteCommonjs } from "@originjs/vite-plugin-commonjs"
import * as path from "path"
// https://vitejs.dev/config/
export default defineConfig({
server: {
// https: true
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src")
}
},
plugins: [vue(), viteCommonjs()]
})

5
js-demo/server/README.md Normal file
View File

@ -0,0 +1,5 @@
### 说明
基于 npm 16.13.0 构建
koa server服务

View File

@ -0,0 +1,22 @@
import axios from "axios"
import { getEncodeHeader } from "./tool/index"
// axios 拦截器
const api = axios.create({
baseURL: "https://live-open.biliapi.com"
// baseURL: "http://test-live-open.biliapi.net" //test
})
// 鉴权加密处理headers下次请求自动带上
api.interceptors.request.use(config => {
const headers = getEncodeHeader(
config.data,
global.appKey,
global.appSecret
)
console.log('headers: ', headers)
config.headers = headers
return config
})
export default api

1777
js-demo/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "poros-demo-server",
"version": "1.0.0",
"description": "",
"main": "server.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"server": "node --loader ts-node/esm --experimental-specifier-resolution=node server.ts"
},
"keywords": [],
"author": "sometimes & re",
"type": "module",
"license": "ISC",
"devDependencies": {
"axios": "0.26.1",
"http": "0.0.1-security",
"https": "1.0.0",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"koa-logger": "3.2.1",
"koa-router": "10.1.1",
"koa2-cors": "2.0.6",
"ts-node": "10.8.0",
"tslib": "2.3.1",
"typescript": "4.6.3",
"@types/node": "17.0.25"
}
}

View File

@ -0,0 +1,17 @@
import api from "../interceptor"
/**
*
* @param ctx
*/
export default async function GameBatchHeartBeat(ctx) {
const params = ctx.request.body
await api
.post("/v2/app/batchHeartbeat", params)
.then(({ data }) => {
ctx.body = data
})
.catch(err => {
ctx.body = err
})
}

View File

@ -0,0 +1,17 @@
import api from "../interceptor"
/**
*
* @param ctx
*/
export default async function GameEnd(ctx) {
const params = ctx.request.body
await api
.post("/v2/app/end", params)
.then(({ data }) => {
ctx.body = data
})
.catch(err => {
ctx.body = err
})
}

View File

@ -0,0 +1,17 @@
import api from "../interceptor"
/**
*
* @param ctx
*/
export default async function GameHeartBeat(ctx) {
const params = ctx.request.body
await api
.post("/v2/app/heartbeat", params)
.then(({ data }) => {
ctx.body = data
})
.catch(err => {
ctx.body = err
})
}

View File

@ -0,0 +1,17 @@
import api from "../interceptor"
/**
*
* @param ctx
*/
export default async function GameStart(ctx) {
const params = ctx.request.body
await api
.post("/v2/app/start", params)
.then(({ data }) => {
ctx.body = data
})
.catch(err => {
ctx.body = err
})
}

View File

@ -0,0 +1,30 @@
import api from "../interceptor"
/**
*
* @param ctx
*/
export default async function GetAuth(ctx) {
const params = ctx.request.body
// 为了方便存在全局
global.appKey = params.appKey
global.appSecret = params.appSecret
// [只为验证鉴权,可删除]
await api
.post("/v2/app/start", {})
.then(({ data }) => {
// 非文档描述性codesign success
if (data.code === -400) {
ctx.body = {
msg: "auth success",
type: "success",
state: 200
}
} else {
ctx.body = data
}
})
.catch(err => {
ctx.body = err
})
}

View File

@ -0,0 +1,17 @@
import Router from "koa-router"
import GetAuth from "./getAuth"
import GameStart from "./gameStart"
import GameEnd from "./gameEnd"
import GameHeartBeat from "./gameHeartBeat"
import GameBatchHeartBeat from "./gameBatchHeartBeat"
const router = new Router()
// 开启路由
router.post("/getAuth", GetAuth)
router.post("/gameStart", GameStart)
router.post("/gameEnd", GameEnd)
router.post("/gameHeartBeat", GameHeartBeat)
router.post("/gameBatchHeartBeat", GameBatchHeartBeat)
export default router

37
js-demo/server/server.ts Normal file
View File

@ -0,0 +1,37 @@
import Koa from "koa"
import logger from "koa-logger"
import cors from "koa2-cors"
import bodyParser from "koa-bodyparser"
import router from "./routes/index"
// axios 拦截
import "./interceptor"
// init
const app = new Koa()
// 开启logger
app.use(logger())
// 开启bodyParser
app.use(bodyParser())
// 开启跨域
app.use(cors())
// 开启router
app.use(router.routes())
app.use(router.allowedMethods())
app.use(async ctx => {
ctx.body = "bilibili创作者服务中心"
})
const protocol = "http"
const host = "127.0.0.1"
const port = 3000
app.listen(port, () => {
console.log(`Listening on ${protocol}://${host}:${port}`)
})

View File

@ -0,0 +1,50 @@
import crypto from "crypto"
/**
*
* @param {*} params
* @returns
*/
export function getEncodeHeader(
params = {},
appKey: string,
appSecret: string
) {
const timestamp = parseInt(Date.now() / 1000 + "")
const nonce = parseInt(Math.random() * 100000 + "") + timestamp
const header = {
"x-bili-accesskeyid": appKey,
"x-bili-content-md5": getMd5Content(JSON.stringify(params)),
"x-bili-signature-method": "HMAC-SHA256",
"x-bili-signature-nonce": nonce + "",
"x-bili-signature-version": "1.0",
"x-bili-timestamp": timestamp
}
const data: string[] = []
for (const key in header) {
data.push(`${key}:${header[key]}`)
}
const signature = crypto
.createHmac("sha256", appSecret)
.update(data.join("\n"))
.digest("hex")
return {
Accept: "application/json",
"Content-Type": "application/json",
...header,
Authorization: signature
}
}
/**
* MD5加密
* @param {*} str
* @returns
*/
export function getMd5Content(str) {
return crypto
.createHash("md5")
.update(str)
.digest("hex")
}

View File

@ -0,0 +1,48 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Language and Environment */
"target": "es2016",
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "ESNext",
/* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node",
/* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": ["./types"],
/* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["node"],
/* Specify type package names to be included without being referenced in a source file. */
/* JavaScript Support */
// "allowJs": true,
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowSyntheticDefaultImports": true,
"noImplicitAny": false,
/* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true,
/* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true,
/* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true
/* Enable all strict type-checking options. */
/* Completeness */
// "declaration": true // *.d.ts
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"ts-node": {
"esm": true
}
}

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "bilibili-open-demo",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module"
}

1968
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- 'packages/*'
- 'js-demo/*'