Teams集成-订阅事件处理

在Teams会议侧边栏应用开发-会议转写-CSDN博客的基础上,使用/delta接口尝试获取实时转写,发现只能更新了一次,然后就不再更新了,想尝试使用订阅事件去获取转写,发现也不是实时的,当会议结束时,订阅事件才会产生,且有一定的延迟。本文主要针对订阅事件如何创建及事件接收后如何处理进行描述。

转写免费配额(很容易使用完,可以重新注册一个新的应用再次获取免费配额):

转写计费成本:

订阅事件的创建:

// 定义 /subscribe 端点
server.post('/subscribe', async (req, res) => {try {const onlineMeetingId = req.query.meetingId;console.log('/subscribe meetingId:',onlineMeetingId);const response = await fetch("https://graph.microsoft.com/beta/subscriptions", {method: "GET",headers: {"Content-Type": "application/json",Authorization: `Bearer ${app_token}`,}});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error.message);}const data = await response.json();let noSub = true;data.value.forEach(sub=>{if (sub.resource === `communications/onlineMeetings/${onlineMeetingId}/transcripts`){noSub = false;}})if (noSub){const subscription = {changeType: "created",notificationUrl: "https://shortly-adapted-akita.ngrok-free.app/webhook",resource: `communications/onlineMeetings/${onlineMeetingId}/transcripts`,expirationDateTime: new Date(Date.now() +  60 * 60 * 1000).toISOString(), //1小时clientState: "secretClientValue",};const response2 = await fetch("https://graph.microsoft.com/beta/subscriptions", {method: "POST",headers: {"Content-Type": "application/json",Authorization: `Bearer ${app_token}`,},body: JSON.stringify(subscription),});if (!response2.ok) {const errorData = await response2.json();throw new Error(errorData.error.message);}const data2 = await response2.json();console.log("Subscription created:", data2.id);res.send(200, data2);}} catch (error) {console.error("Error creating subscription:", error);res.send(500, { error: error.message });}
});

订阅事件的接收(订阅事件没有使用加密方式,需要调用2次API获取转写):

// 定义 /webhook 端点
server.post('/webhook', async (req, res) => {const validationToken = req.query.validationToken;console.log('/webhook validationToken:', validationToken);// 如果是验证请求,返回 validationTokenif (validationToken) {res.setHeader('Content-Type', 'text/plain');res.send(200, validationToken);return;}const notification = req.body.value[0];const odataId = notification.resourceData['@odata.id'];const url = `https://graph.microsoft.com/beta/${odataId}`.replace('communications','me');//console.log('url:', url);const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/get_user_token');const token_data = await response.json();// 获取具体的转写数据try {const response = await fetch(url, {headers: {Authorization: `Bearer ${token_data.user_token}`,},});if (!response.ok) {const errorData = await response.json();//console.log(errorData);throw new Error(errorData.error.message);}const transcriptData = await response.json();//console.log("Transcript Data:", transcriptData);// 获取转写内容const transcriptContentUrl = transcriptData.transcriptContentUrl;const contentResponse = await fetch(`${transcriptContentUrl}?$format=text/vtt`, {headers: {Authorization: `Bearer ${token_data.user_token}`,},});if (!contentResponse.ok) {const errorData = await contentResponse.text();//console.log(errorData);throw new Error(errorData);}const transcriptContent = await contentResponse.text();console.log("Transcript Content:", transcriptContent);// 将转写数据发送到所有连接的 WebSocket 客户端wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(JSON.stringify({ type: 'newTranscript', data: transcriptContent }));}});} catch (error) {console.error("Error fetching transcript data:", error);}res.send(200, "OK");
});

完整的服务端代码如下:

import restify from "restify";
import send from "send";
import fs from "fs";
import fetch from "node-fetch";
import path from 'path';
import { fileURLToPath } from 'url';
import { storeToken, getToken } from './redisClient.js';
import { WebSocketServer, WebSocket } from 'ws';const __filename = fileURLToPath(import.meta.url);
console.log('__filename: ', __filename);const __dirname = path.dirname(__filename);
console.log('__dirname: ', __dirname);// Create HTTP server.
const server = restify.createServer({key: process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined,certificate: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined,formatters: {"text/html": function (req, res, body) {return body;},},
});server.use(restify.plugins.bodyParser());
server.use(restify.plugins.queryParser());server.get("/static/*",restify.plugins.serveStatic({directory: __dirname,})
);server.listen(process.env.port || process.env.PORT || 3000, function () {console.log(`\n${server.name} listening to ${server.url}`);
});// Adding tabs to our app. This will setup routes to various views
// Setup home page
server.get("/config", (req, res, next) => {send(req, __dirname + "/config/config.html").pipe(res);
});// Setup the static tab
server.get("/meetingTab", (req, res, next) => {send(req, __dirname + "/panel/panel.html").pipe(res);
});//获得用户token
server.get('/auth', (req, res, next) => {res.status(200);res.send(`
<!DOCTYPE html>
<html>
<head><script>// Function to handle the token storageasync function handleToken() {const hash = window.location.hash.substring(1);const hashParams = new URLSearchParams(hash);const access_token = hashParams.get('access_token');console.log('Received hash parameters:', hashParams);if (access_token) {console.log('Access token found:', access_token);localStorage.setItem("access_token", access_token);console.log('Access token stored in localStorage');try {const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/store_user_token', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ "user_token" : access_token })});if (response.ok) {console.log('Token stored successfully');} else {console.error('Failed to store token:', response.statusText);}} catch (error) {console.error('Error storing token:', error);}} else {console.log('No access token found');}window.close();}// Call the function to handle the tokenhandleToken();</script>
</head>
<body></body>
</html>`);next();
});// 存储 user_token
server.post('/store_user_token', async (req, res) => {const user_token = req.body.user_token;if (!user_token) {res.status(400);res.send('user_token are required');}try {// Store user tokenawait storeToken('user_token', user_token);console.log('user_token stored in Redis');} catch (err) {console.error('user_token store Error:', err);}res.status(200);   res.send('Token stored successfully');
});// 获取 user_token
server.get('/get_user_token', async (req, res) => {try {// Store user tokenconst user_token = await getToken('user_token');console.log('user_token get in Redis');res.send({"user_token": user_token});} catch (err) {console.error('user_token get Error:', err);}
});//应用token
let app_token = '';
let app_token_expires_at = 0;
const app_token_refresh_interval = 3000 * 1000; // 3000秒const getAppToken = async () => {try {// 构建请求体const requestBody = new URLSearchParams({"grant_type": "client_credentials","client_id": "Azure注册应用ID","client_secret": "Azure注册应用密码","scope": "https://graph.microsoft.com/.default",}).toString();// 获取app令牌const tokenUrl = `https://login.microsoftonline.com/注册应用租户ID/oauth2/v2.0/token`;const tokenResponse = await fetch(tokenUrl, {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: requestBody,});if (!tokenResponse.ok) {const errorData = await tokenResponse.json();throw new Error(errorData.error_description);}const tokenData = await tokenResponse.json();app_token = tokenData.access_token;app_token_expires_at = Date.now() + app_token_refresh_interval;console.log("app_token received!");} catch (error) {console.error('Error getting app token:', error);}
};// 定期刷新 app_token
setInterval(getAppToken, app_token_refresh_interval);// 定义 /getTranscripts 端点
server.get('/getTranscripts', async (req, res) => {try {const url = req.query.url;if (!url) {res.send(400, { error: 'URL is required' });return;}// 调用 Microsoft Graph APIconst graphResponse = await fetch(url, {headers: {Authorization: `Bearer ${app_token}`,},});if (!graphResponse.ok) {const errorData = await graphResponse.json();res.send(500, { error: errorData.error.message });return;}const data = await graphResponse.json();const currentTime = new Date().toISOString(); // 获取当前时间console.log(currentTime, ', getTranscripts length:',data.value.length);// 返回转录文本res.send(200, data);} catch (error) {// 返回错误res.send(500, { error: error.message });}
});// 定义 /getTranscriptContent 端点
let callCount = 0; // 调用计数器server.get('/getTranscriptContent', async (req, res) => {try {const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/get_user_token');const token_data = await response.json();const transcriptContentUrl = req.query.transcriptContentUrl;if (!transcriptContentUrl) {res.send(400, { error: 'transcriptContentUrl is required' });return;}const content_url = `${transcriptContentUrl}?$format=text/vtt`;// 调用 Microsoft Graph APIconst graphResponse = await fetch(content_url, {headers: {Authorization: `Bearer ${token_data.user_token}`,},});if (!graphResponse.ok) {const errorData = await graphResponse.text();res.send(500, { error: errorData });return;}const data = await graphResponse.text();callCount++; // 增加正确调用计数const currentTime = new Date().toISOString(); // 获取当前时间console.log(`getTranscriptContent called at ${currentTime}, call count: ${callCount}`); // 输出日志console.log('content:', data)// 返回转录文本res.send(200, data);} catch (error) {// 返回错误res.send(500, { error: error.message });}
});// 确保在服务器启动时获取 app_token
getAppToken();// 初始化 WebSocket 服务器
const wss = new WebSocketServer({ server: server.server });wss.on('connection', (ws) => {console.log('A client connected');ws.on('message', (message) => {console.log(`Received message: ${message}`);});ws.on('close', () => {console.log('A client disconnected');});
});// 定义 /subscribe 端点
server.post('/subscribe', async (req, res) => {try {const onlineMeetingId = req.query.meetingId;console.log('/subscribe meetingId:',onlineMeetingId);const response = await fetch("https://graph.microsoft.com/beta/subscriptions", {method: "GET",headers: {"Content-Type": "application/json",Authorization: `Bearer ${app_token}`,}});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error.message);}const data = await response.json();let noSub = true;data.value.forEach(sub=>{if (sub.resource === `communications/onlineMeetings/${onlineMeetingId}/transcripts`){noSub = false;}})if (noSub){const subscription = {changeType: "created",notificationUrl: "https://shortly-adapted-akita.ngrok-free.app/webhook",resource: `communications/onlineMeetings/${onlineMeetingId}/transcripts`,expirationDateTime: new Date(Date.now() +  60 * 60 * 1000).toISOString(), //1小时clientState: "secretClientValue",};const response2 = await fetch("https://graph.microsoft.com/beta/subscriptions", {method: "POST",headers: {"Content-Type": "application/json",Authorization: `Bearer ${app_token}`,},body: JSON.stringify(subscription),});if (!response2.ok) {const errorData = await response2.json();throw new Error(errorData.error.message);}const data2 = await response2.json();console.log("Subscription created:", data2.id);res.send(200, data2);}} catch (error) {console.error("Error creating subscription:", error);res.send(500, { error: error.message });}
});// 定义 /webhook 端点
server.post('/webhook', async (req, res) => {const validationToken = req.query.validationToken;console.log('/webhook validationToken:', validationToken);// 如果是验证请求,返回 validationTokenif (validationToken) {res.setHeader('Content-Type', 'text/plain');res.send(200, validationToken);return;}const notification = req.body.value[0];const odataId = notification.resourceData['@odata.id'];const url = `https://graph.microsoft.com/beta/${odataId}`.replace('communications','me');//console.log('url:', url);const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/get_user_token');const token_data = await response.json();// 获取具体的转写数据try {const response = await fetch(url, {headers: {Authorization: `Bearer ${token_data.user_token}`,},});if (!response.ok) {const errorData = await response.json();//console.log(errorData);throw new Error(errorData.error.message);}const transcriptData = await response.json();//console.log("Transcript Data:", transcriptData);// 获取转写内容const transcriptContentUrl = transcriptData.transcriptContentUrl;const contentResponse = await fetch(`${transcriptContentUrl}?$format=text/vtt`, {headers: {Authorization: `Bearer ${token_data.user_token}`,},});if (!contentResponse.ok) {const errorData = await contentResponse.text();//console.log(errorData);throw new Error(errorData);}const transcriptContent = await contentResponse.text();console.log("Transcript Content:", transcriptContent);// 将转写数据发送到所有连接的 WebSocket 客户端wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(JSON.stringify({ type: 'newTranscript', data: transcriptContent }));}});} catch (error) {console.error("Error fetching transcript data:", error);}res.send(200, "OK");
});

完整的页面代码如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Meeting Transcripts</title><script src="https://res.cdn.office.net/teams-js/2.0.0/js/MicrosoftTeams.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script><style>.subtitle {display: flex;align-items: center;margin-bottom: 10px;}.speaker-photo {width: 20px;height: 20px;border-radius: 50%;margin-right: 10px;}</style>
</head>
<body><h2>Meeting Transcripts</h2><div id="transcripts"></div><script>const clientId = 'Azure注册应用ID';const tenantId = 'Azure注册应用租户ID';const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;const redirectUri = '你的https域名/auth'; // 确保与服务器端一致const scope = 'user.read';let user_token = null;let meetingOrganizerUserId = null;let participants = {}; // 用于存储参会者的信息let nextLink = null;let deltaLink = null;let userPhotoCache = {}; // 用于缓存用户头像let tokenFetched = false; // 标志变量,用于跟踪是否已经获取了 user_tokenconst getUserInfo = async (userId, accessToken) => {const graphUrl = `https://graph.microsoft.com/v1.0/users/${userId}`;const response = await fetch(graphUrl, {headers: {'Authorization': `Bearer ${accessToken}`}});if (response.status === 401) {// 如果 token 超期,重新触发 initAuthenticationinitAuthentication();return null;}const userInfo = await response.json();return userInfo;};const getUserPhoto = async (userId, accessToken) => {if (userPhotoCache[userId]) {return userPhotoCache[userId];}const graphUrl = `https://graph.microsoft.com/v1.0/users/${userId}/photo/$value`;const response = await fetch(graphUrl, {headers: {'Authorization': `Bearer ${accessToken}`}});if (!response.ok) {const errorData = await response.json();console.error('Error fetching user photo:', errorData);return null;}const photoBlob = await response.blob();const photoUrl = URL.createObjectURL(photoBlob);userPhotoCache[userId] = photoUrl; // 缓存头像 URLreturn photoUrl;};const getMeetingDetails = async (user_token, joinMeetingId) => {const apiUrl = `https://graph.microsoft.com/v1.0/me/onlineMeetings?$filter=joinMeetingIdSettings/joinMeetingId eq '${joinMeetingId}'`;const response = await fetch(apiUrl, {method: 'GET',headers: {'Authorization': `Bearer ${user_token}`,'Content-Type': 'application/json'}});if (!response.ok) {const errorData = await response.json();throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error}`);}const data = await response.json();return data.value[0];};const getTranscriptContent = async (content) => {const lines = content.trim().split('\n');const subtitles = [];let currentSpeaker = null;for (let i = 0; i < lines.length; i++) {const line = lines[i].trim();if (line.includes('-->')) {const [startTime, endTime] = line.split(' --> ');const text = lines[i + 1].trim();const speakerMatch = text.match(/<v\s*([^>]+)>/);const speaker = speakerMatch ? speakerMatch[1] : null;const content = text.replace(/<v\s*[^>]*>/, '').replace(/<\/v>/, '');if (speaker && speaker !== currentSpeaker) {currentSpeaker = speaker;}subtitles.push({ startTime, endTime, speaker: currentSpeaker, content });i++; // Skip the next line as it's the text content}}return subtitles;};const displaySubtitle = async (subtitle, transcriptElement, accessToken) => {const subtitleElement = document.createElement('div');subtitleElement.classList.add('subtitle');// 获取说话者的头像const speakerUserId = participants[subtitle.speaker];const speakerPhotoUrl = speakerUserId ? await getUserPhoto(speakerUserId, accessToken) : 'default-avatar.png';// 创建头像元素const speakerPhotoElement = document.createElement('img');speakerPhotoElement.src = speakerPhotoUrl;speakerPhotoElement.alt = subtitle.speaker;speakerPhotoElement.classList.add('speaker-photo');// 创建输出字符串const output = `${subtitle.startTime}\n${subtitle.content}`;subtitleElement.appendChild(speakerPhotoElement);subtitleElement.appendChild(document.createTextNode(output));transcriptElement.appendChild(subtitleElement);};const subscribeToWebhook = async (meetingId) => {try {const subscribeResponse = await fetch(`https://shortly-adapted-akita.ngrok-free.app/subscribe?meetingId=${meetingId}`, {method: 'POST'});if (!subscribeResponse.ok) {const errorData = await subscribeResponse.json();console.error('Webhook subscription failed:', errorData);} else {console.log('Webhook subscription successful');}} catch (error) {console.error('Error subscribing to webhook:', error);}};const init = async () => {try {if (!tokenFetched) {const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/get_user_token');const data = await response.json();if (response.ok) {user_token = data.user_token;console.log('user token retrieved:', user_token);tokenFetched = true;} else {console.error('Failed to get token:', response.statusText);return;}}const joinMeetingId = '45756456529'; // 替换为你要查询的 joinMeetingIdtry {const meetingDetails = await getMeetingDetails(user_token, joinMeetingId);console.log('Meeting Details:', meetingDetails);meetingOrganizerUserId = meetingDetails.participants.organizer.identity.user.id;const meetingId = meetingDetails.id; // 获取会议 IDconsole.log('Organizer User ID:', meetingOrganizerUserId);console.log('Meeting ID:', meetingId);// 获取主持人信息const organizerInfo = await getUserInfo(meetingOrganizerUserId, user_token);const organizerDisplayName = organizerInfo.displayName;participants[organizerDisplayName] = meetingOrganizerUserId;// 获取参会者信息const attendeesPromises = meetingDetails.participants.attendees.map(async attendee => {const userId = attendee.identity.user.id;const userInfo = await getUserInfo(userId, user_token);const displayName = userInfo.displayName;participants[displayName] = userId;});await Promise.all(attendeesPromises);// 初始化历史转写// await fetchTranscripts();// 订阅 Webhookawait subscribeToWebhook(meetingId);// 设置每小时调用一次订阅 WebhooksetInterval(() => subscribeToWebhook(meetingId), 3600000); // 每3600秒(1小时)调用一次} catch (error) {console.error('Error fetching meeting details:', error);}console.log('User Token:', user_token);} catch (error) {console.error('Error getting token:', error);}};const initAuthentication = () => {microsoftTeams.app.initialize();microsoftTeams.authentication.authenticate({url: `${authUrl}?client_id=${clientId}&response_type=token&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`,width: 600,height: 535,successCallback: async (result) => {console.log('Authentication success:', result);},failureCallback: (error) => {console.error('Authentication failed:', error);}});};// 设置较长的轮询时间来防止 user_token 的超期setInterval(initAuthentication, 3000000); // 每3000秒(50分钟)轮询一次initAuthentication();init();// 初始化 WebSocket 客户端const socket = new WebSocket('wss://shortly-adapted-akita.ngrok-free.app');socket.onopen = () => {console.log('WebSocket connection established');};socket.onmessage = (event) => {const message = JSON.parse(event.data);console.log(message);if (message.type === 'newTranscript') {const transcriptData = message.data;console.log('New Transcript Data:', transcriptData);const transcriptsContainer = document.getElementById('transcripts');const transcriptElement = document.createDocumentFragment(); // 使用 DocumentFragment 优化 DOM 操作getTranscriptContent(transcriptData).then(subtitles => {subtitles.forEach(subtitle => {displaySubtitle(subtitle, transcriptElement, user_token);});}).catch(error => {const errorElement = document.createElement('div');errorElement.innerHTML = `<strong>${error}</strong>`;transcriptElement.appendChild(errorElement);}).finally(() => {transcriptsContainer.appendChild(transcriptElement); // 一次性插入 DOM});}};socket.onclose = () => {console.log('WebSocket connection closed');};socket.onerror = (error) => {console.error('WebSocket error:', error);};</script>
</body>
</html>

输出效果:

​​​​​​​

摸索不易,欢迎点赞👍加关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1547033.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

排序题目:对角线遍历 II

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;对角线遍历 II 出处&#xff1a;1424. 对角线遍历 II 难度 6 级 题目描述 要求 给定一个二维整数数组 nums \texttt{nums} nums&#xff0c;将 …

阅读记录:iCaRL: Incremental Classifier and Representation Learning

1. Contribution 提出了一种新的训练策略&#xff0c;iCaRL&#xff1a;允许以增量方式学习&#xff1a;只需要同时存在一小部分类别的训练数据&#xff0c;新类别可以逐步添加。同时学习分类器和数据表示&#xff1a;iCaRL能够同时学习强大的分类器和数据表示&#xff0c;这与…

vscode【实用插件】Markdown Preview Enhanced 预览 .md 文件

安装 在 vscode 插件市场的搜索 Markdown Preview Enhanced点安装 使用 用 vscode 打开任意 .md 文件右键快捷菜单 最终效果 可打开导航目录

有哪些小众但高逼格的蓝牙耳机推荐?百元开放式耳机推荐大赏

如今的耳机市场中&#xff0c;主流品牌的影响力不容小觑。然而&#xff0c;还有一些小众的耳机品牌&#xff0c;犹如未被发掘的珍宝&#xff0c;静候着人们去探索。这些小众品牌或许没有进行大规模的广告推广&#xff0c;但它们凭借独特的设计、出色的音质以及对品质的不懈坚持…

需求: 通过后台生成的树形结构,返回给前台用于动态生成表格标题,并将对应标题下面的信息对应起来

1. 如图所以&#xff0c;完成以下内容对应 2. 代码示例如下&#xff0c; 动态生成树形结构列名称&#xff0c;并将表格中存在的值与其对应起来 /*** 查询资源计划列表** param resourcePlan 资源计划* return 资源计划*/Overridepublic Map<String, Object> selectResour…

【通俗易懂】FFT求解全过程,各参数详细解释

在进行FFT全过程讲解之前&#xff0c;小编先给大家解释一下&#xff0c;在FFT中出现的一些参数名词解释。 &#xff08;1&#xff09;采样频率 Fs Fs 1 / 采样间隔 根据奈奎斯特定理&#xff1a;Fs ≥ 最高频率分量的两倍&#xff0c;这样才能避免混叠 &#xff08;2&…

CAT1 RTU软硬件设计开源资料分析(TCP协议+Modbus协议+GNSS定位版本 )

01 CAT1 RTU方案简介&#xff1a; 远程终端单元( Remote Terminal Unit&#xff0c;RTU)&#xff0c;一种针对通信距离较长和工业现场环境恶劣而设计的具有模块化结构的、特殊的计算机测控单元&#xff0c;它将末端检测仪表和执行机构与远程控制中心相连接。 奇迹TCP RTUGNS…

Alertmanager 路由匹配

Alertmanager主要负责对Prometheus产生的告警进行统一处理&#xff0c;因此在Alertmanager配置中一般会包含以下几个主要部分&#xff1a; 全局配置&#xff08;global&#xff09;&#xff1a;用于定义一些全局的公共参数&#xff0c;如全局的SMTP配置&#xff0c;Slack配置等…

uni-app canvas文本自动换行

封装 // 填充自动换行的文本function fillFeedText({ctx, text, x, y, maxWidth, lineHeight, color, size}) {// 文本配置ctx.setFontSize(size);ctx.setFillStyle(color);// 计算文本换行宽高&#xff0c;换行逻辑const words text.split();let line ;const lines [];for …

en造数据结构与算法C# 二叉树的顺序存储和前中后序遍历

二叉树的序号和索引区别 二叉树的顺序存储代码 我用的是List表&#xff0c;只要是线性表就都能实现二叉树 public class Tree<T> : MonoBehaviour {private List<T> bitTree new List<T>();//添加顺序存储方法public void AddTree(T[] values){for(int i…

2024最新盘点:推荐几款主流的采购管理系统

大家都明白采购对制造型企业的重要性&#xff0c;但是在面对市场上琳琅满目的采购管理系统企业却不知道该如何选择&#xff0c;不要担心。 本篇文章将对市面上知名的采购管理系统进行综合测评&#xff0c;深入剖析这些平台的特点与优势。看完这篇内容&#xff0c;你将对不同采…

这本书简直就是自然语言处理学习者的福音!

自然语言处理被誉为“人工智能皇冠上的明珠”! 深度学习等技术的引入为自然语言处理技术带来了一场革命&#xff0c;近年来也出现了自然语言处理的新范式。 早期的静态词向量预训练模型&#xff0c;以及后来的动态词向量预训练模型&#xff0c;特别是2018 年以来&#xff0c;…

书生大模型实战营学习[9] OpenCompass 评测 InternLM-1.8B 实践

准备工作 打开开发机&#xff0c;选择cuda11.7环境&#xff0c;A100选择10%&#xff0c;点击创建&#xff0c;然后进入开发机即可&#xff0c;和之前的操作一样。接下来创建环境&#xff0c;下载必要的依赖包 conda create -n opencompass python3.10 conda install pytorch2…

盘点几款财务人必备的财务管理系统,建议收藏!

相信很多财务人在工作中会遇到各种各样的难题&#xff0c;数据杂乱、对账不清晰、财务流程复杂等&#xff0c;一个好用的财务管理系统绝对是雪中送炭。那么财务人知道有哪些财务管理系统吗&#xff1f; 财务管理系统从多方面为财务人的工作保驾护航&#xff0c;优化流程系统、…

数据结构:实现链式结构二叉树(Tree) 手把手带你入门数据结构~

文章目录 前言一、链式结构二叉树的概念1. 定义2. 节点结构3. 操作4. 优势与劣势 二、链式结构二叉树的实现1. 树结构的定义2. 树的遍历&#xff08;1&#xff09;前序遍历&#xff08;2&#xff09;中序遍历&#xff08;3&#xff09;后序遍历 3. 二叉树结点个数4. 二叉树叶子…

828华为云征文 | 基于华为云Flexus云服务器X搭建部署——AI知识库问答系统(使用1panel面板安装)

&#x1f680;对于企业来讲为什么需要华为云Flexus X来搭建自己的知识库问答系统&#xff1f;&#xff1f;&#xff1f; 【重塑知识边界&#xff0c;华为云Flexus云服务器X引领开源问答新纪元&#xff01;】 &#x1f31f; 解锁知识新动力&#xff0c;华为云Flexus云服务器X携…

力扣 简单 876.链表的中间结点

文章目录 题目介绍题解 题目介绍 题解 法一&#xff1a; class Solution {public ListNode middleNode(ListNode head) {ListNode cur head;int n 0;while (cur ! null) {n;cur cur.next;}ListNode curr head;for (int i 0; i < n / 2; i) {curr curr.next;}return …

C++ 红黑树封装map和set

目录 前言 1.红黑树的改造 1.1主题框架 1.2迭代器 operator &#xff08;&#xff09; begin&#xff08;&#xff09;和end&#xff08;&#xff09; 1.3红黑树相关接口的改造 Find函数的改造 Insert 函数的改造 2.红黑树改造的完整代码 3.Set的封装 4.Map的封装 前…

freeRDP OPenssl

libusb需要下载 我使用的是VS2019编译 所以需要include 与vs2019 在cmake里面修改路径 C:/Users/JPM/source/repos/freeRDP/FreeRDP-stable-2.0/libusb-1.0.24/include/libusb-1.0 C:/Users/JPM/source/repos/freeRDP/FreeRDP-stable-2.0/libusb-1.0.24/VS2019/MS64/static/l…

pycharm24.2运行框中无法输入中文但是可以粘贴中文、输入英文、数字

文章目录 一、问题描述二、解决办法解决办法1解决办法2解决办法3 一、问题描述 pycharm24.2版本中运行框中无法输入中文&#xff0c;敲击空格键发现中文并没有输入进去: 但是可以粘贴中文: 输入英文、数字没有问题。 二、解决办法 该问题为pycharm24.2版本问题。 解决办法…