commit 95eda06dbcc8056fa49a3b06341ad7399bbc2017 Author: old易 <156663459@qq.com> Date: Sat Mar 29 15:55:29 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d865bdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules/ +yarn.lock +package-lock.json + +# Build outputs +dist/ +build/ +*.tgz + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Testing +coverage/ + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..31dfb02 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry = 'https://registry.npmjs.org/' \ No newline at end of file diff --git a/AccessToken.js b/AccessToken.js new file mode 100644 index 0000000..3088ecf --- /dev/null +++ b/AccessToken.js @@ -0,0 +1,240 @@ +var crypto = require('crypto'); + +var randomInt = Math.floor(Math.random() * 0xFFFFFFFF); + +const VERSION = "001"; +const VERSION_LENGTH = 3; + +const APP_ID_LENGTH = 24; + +privileges = { + PrivPublishStream: 0, + + // not exported, do not use directly + privPublishAudioStream: 1, + privPublishVideoStream: 2, + privPublishDataStream: 3, + + PrivSubscribeStream: 4, +}; + + +module.exports.privileges = privileges; + +// Initializes token struct by required parameters. +var AccessToken = function (appID, appKey, roomID, userID) { + let token = this; + this.appID = appID; + this.appKey = appKey; + this.roomID = roomID; + this.userID = userID; + this.issuedAt = Math.floor(new Date() / 1000); + this.nonce = randomInt; + this.expireAt = 0; + this.privileges = {}; + + // AddPrivilege adds permission for token with an expiration. + this.addPrivilege = function (privilege, expireTimestamp) { + if (token.privileges === undefined) { + token.privileges = {} + } + token.privileges[privilege] = expireTimestamp; + + if (privilege === privileges.PrivPublishStream) { + token.privileges[privileges.privPublishVideoStream] = expireTimestamp; + token.privileges[privileges.privPublishAudioStream] = expireTimestamp; + token.privileges[privileges.privPublishDataStream] = expireTimestamp; + } + }; + + // ExpireTime sets token expire time, won't expire by default. + // The token will be invalid after expireTime no matter what privilege's expireTime is. + this.expireTime = function (expireTimestamp) { + token.expireAt = expireTimestamp; + }; + + this.packMsg = function () { + var bufM = new ByteBuf(); + bufM.putUint32(token.nonce); + bufM.putUint32(token.issuedAt); + bufM.putUint32(token.expireAt); + bufM.putString(token.roomID); + bufM.putString(token.userID); + bufM.putTreeMapUInt32(token.privileges); + return bufM.pack() + }; + + // Serialize generates the token string + this.serialize = function () { + var bytesM = this.packMsg(); + + var signature = encodeHMac(token.appKey, bytesM); + var content = new ByteBuf().putBytes(bytesM).putBytes(signature).pack(); + + return (VERSION + token.appID + content.toString('base64')); + }; + + // Verify checks if this token valid, called by server side. + this.verify = function (key) { + if (token.expireAt > 0 && Math.floor(new Date() / 1000) > token.expireAt) { + return false + } + + token.appKey = key; + return encodeHMac(token.appKey, this.packMsg()).toString() === token.signature; + } + +}; + +// Parse retrieves token information from raw string +var Parse = function (raw) { + try { + if (raw.length <= VERSION_LENGTH + APP_ID_LENGTH) { + return + } + if (raw.substr(0, VERSION_LENGTH) !== VERSION) { + return + } + var token = new AccessToken("", "", "", ""); + token.appID = raw.substr(VERSION_LENGTH, APP_ID_LENGTH); + + var contentBuf = Buffer.from(raw.substr(VERSION_LENGTH + APP_ID_LENGTH), 'base64'); + var readbuf = new ReadByteBuf(contentBuf); + + var msg = readbuf.getString(); + token.signature = readbuf.getString().toString(); + + // parse msg + var msgBuf = new ReadByteBuf(msg); + token.nonce = msgBuf.getUint32(); + token.issuedAt = msgBuf.getUint32(); + token.expireAt = msgBuf.getUint32(); + token.roomID = msgBuf.getString().toString(); + token.userID = msgBuf.getString().toString(); + token.privileges = msgBuf.getTreeMapUInt32(); + + return token + } catch (err) { + console.log(err); + } +}; + + +module.exports.version = VERSION; +module.exports.AccessToken = AccessToken; +module.exports.Parse = Parse; + +var encodeHMac = function (key, message) { + return crypto.createHmac('sha256', key).update(message).digest(); +}; + +var ByteBuf = function () { + var that = { + buffer: Buffer.alloc(1024) + , position: 0 + }; + +// that.buffer.fill(0); + + that.pack = function () { + var out = Buffer.alloc(that.position); + that.buffer.copy(out, 0, 0, out.length); + return out; + }; + + that.putUint16 = function (v) { + that.buffer.writeUInt16LE(v, that.position); + that.position += 2; + return that; + }; + + that.putUint32 = function (v) { + that.buffer.writeUInt32LE(v, that.position); + that.position += 4; + return that; + }; + + that.putBytes = function (bytes) { + that.putUint16(bytes.length); + bytes.copy(that.buffer, that.position); + that.position += bytes.length; + return that; + }; + + that.putString = function (str) { + return that.putBytes(Buffer.from(str)); + }; + + that.putTreeMap = function (map) { + if (!map) { + that.putUint16(0); + return that; + } + + that.putUint16(Object.keys(map).length); + for (var key in map) { + that.putUint16(key); + that.putString(map[key]); + } + + return that; + }; + + that.putTreeMapUInt32 = function (map) { + if (!map) { + that.putUint16(0); + return that; + } + + that.putUint16(Object.keys(map).length); + for (var key in map) { + that.putUint16(key); + that.putUint32(map[key]); + } + + return that; + }; + + return that; +}; + +var ReadByteBuf = function (bytes) { + var that = { + buffer: bytes + , position: 0 + }; + + that.getUint16 = function () { + var ret = that.buffer.readUInt16LE(that.position); + that.position += 2; + return ret; + }; + + that.getUint32 = function () { + var ret = that.buffer.readUInt32LE(that.position); + that.position += 4; + return ret; + }; + + that.getString = function () { + var len = that.getUint16(); + + var out = Buffer.alloc(len); + that.buffer.copy(out, 0, that.position, (that.position + len)); + that.position += len; + return out; + }; + + that.getTreeMapUInt32 = function () { + var map = {}; + var len = that.getUint16(); + for (var i = 0; i < len; i++) { + var key = that.getUint16(); + var value = that.getUint32(); + map[key] = value; + } + return map; + }; + + return that; +}; diff --git a/app.js b/app.js new file mode 100644 index 0000000..0005042 --- /dev/null +++ b/app.js @@ -0,0 +1,153 @@ +/** + * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. + * SPDX-license-identifier: BSD-3-Clause + */ + +const Koa = require('koa'); +const bodyParser = require('koa-bodyparser'); +const cors = require('koa2-cors'); +const { Signer } = require('@volcengine/openapi'); +const fetch = require('node-fetch'); +const AccessToken = require('./AccessToken'); +const Privileges = require('./AccessToken').privileges; + +const app = new Koa(); + +app.use(cors({ + origin: '*' +})); + +/** + * @notes 在 https://console.volcengine.com/iam/keymanage/ 获取 AK/SK + */ +const ACCOUNT_INFO = { + /** + * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取 + */ + accessKeyId: 'AKLTNGJhYTRiMWE5Yzc2NDhlODk3NzE0YTI4NzEyZTM3NDI', + /** + * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取 + */ + secretKey: 'T1RrellUVXpaRFU0WXpVME5EZzRNV0UxTXpObFl6QmhPVFl4TUdFME0yTQ==', + /** + * @notes 非必填, 主账号无须传入, 子账号须传, 获取方式可参考 + * https://www.volcengine.com/docs/6348/1315561 中的 步骤 4-使用子账号调用智能体接口 一节 + */ + // sessionToken: 'Your SessionToken', +} + +// AccessToken 应用配置 +const appID = "67e11a296ff39301ed7429aa"; +const appKey = "51151bf3ab2545bbb1b1494ffaae0521"; + +app.use(bodyParser()); + + +app.use(async ctx => { + /** + * @brief 代理 AIGC 的 OpenAPI 请求 + */ + if (ctx.url.startsWith('/api/proxyaigc') && ctx.method.toLowerCase() === 'post') { + const { Action, Version = '2023-11-01' } = ctx.query || {}; + const body = { + AppId: appID, // 自动添加 AppId + ...ctx.request.body + }; + + const openApiRequestData = { + region: 'cn-north-1', + method: 'POST', + params: { + Action, + Version, + }, + headers: { + Host: 'rtc.volcengineapi.com', + 'Content-type': 'application/json', + Authorization: '', + }, + body, + }; + const signer = new Signer(openApiRequestData, "rtc"); + signer.addAuthorization(ACCOUNT_INFO); + + console.log('请求参数:', { Action, Version, body }); // 添加日志 + + const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, { + method: 'POST', + headers: { + ...openApiRequestData.headers, + Authorization: openApiRequestData.headers.Authorization, + }, + body: JSON.stringify(body), + }); + + const volcResponse = await result.json(); + console.log('响应结果:', volcResponse); // 添加日志 + + if (!volcResponse.ResponseMetadata?.Error) { + ctx.body = volcResponse; + } else { + ctx.status = 500; + ctx.body = { + code: 500, + message: volcResponse.ResponseMetadata.Error.Message || '请求失败', + data: null + }; + } +} + /** + * @brief 生成 token 的 API 接口 + */ + else if (ctx.url === '/api/token' && ctx.method.toLowerCase() === 'post') { + try { + const { roomId, userId } = ctx.request.body; + + // 参数验证 + if (!roomId || !userId) { + ctx.status = 400; + ctx.body = { + code: 400, + message: '缺少必要参数 roomId 或 userId', + data: null + }; + return; + } + + // 创建 AccessToken + const key = new AccessToken.AccessToken(appID, appKey, roomId, userId); + + // 添加权限 + key.addPrivilege(Privileges.PrivSubscribeStream, 0); + key.addPrivilege(Privileges.PrivPublishStream, Math.floor(new Date() / 1000) + (24 * 3600)); + key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600)); + + // 序列化 token + const token = key.serialize(); + + // 返回成功响应 + ctx.status = 200; + ctx.body = { + code: 200, + message: '获取 token 成功', + data: { token } + }; + } catch (error) { + console.error('生成 token 出错:', error); + ctx.status = 500; + ctx.body = { + code: 500, + message: '服务器内部错误', + data: null + }; + } + } else { + ctx.body = '

404 Not Found

'; + } +}); + +app.listen(3001, () => { + console.log('AIGC Server is running at http://localhost:3001'); + console.log('Token API is available at http://localhost:3001/api/token'); +}); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..002b1a7 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "AIGCServer", + "version": "1.0.0", + "description": "Server for demo to call open api", + "main": "app.js", + "license": "BSD-3-Clause", + "private": true, + "dependencies": { + "@volcengine/openapi": "^1.22.0", + "AIGCServer": "file:", + "express": "^4.21.2", + "koa": "^2.15.3", + "koa-bodyparser": "^4.4.1", + "koa2-cors": "^2.0.6", + "node-fetch": "^2.3.2" + }, + "scripts": { + "dev": "node app.js" + } +}