Teams会议侧边栏应用开发-会议转写

news2024/12/24 9:51:12

Teams应用开发,主要是权限比较麻烦,大量阅读和实践,摸索了几周,才搞明白。现将经验总结如下:

一、目标:开发一个Teams会议的侧边栏应用,实现会议的实时转写。

二、前提:

1)Teams 365基础版本以上账号Developer Portal,主要是可以登录开发者门户,建议开放管理员权限,以便可以上传开发好的APP(实际上仅仅是个mainfest.json);

2)Teams 基础版本以上账号,可以登录Teams,添加App;

3)Azure 账号,建议开放管理员权限,以方便授予同意权限。

以上是基本要求,否则无法进行后续的工作。

三、需求分解:

1)侧边栏->关键配置(configurableTabs)

2)实时转写->转写->会议组织者ID->会议ID->用户ID

四、涉及的权限:

1)用户token=委托权限(Delegated),如:转录,需要有会议组织者ID的用户权限;

2)应用token=应用权限(Application),如:获取转录列表。

以上两类权限,可以通过在Azure注册应用获得,如:

五、辅助工具:

1)Graph Explorer | Try Microsoft Graph APIs - Microsoft Graph可以帮助你调试API,判断是否有权限,及需要什么权限,如:User.Read, OnlineMeetingTranscript.Read.All 等。

2)jwt.ms: Welcome!帮助你判断获得的Token是哪类Token, typ=user or app。

基本要求交代完成后,下面说说具体的,正式开始。

六、注册Azure 应用

1)先找到入口-应用注册

2)记录注册应用ID和租户ID(获取user token或 app token都要用到)

3)增加一个密钥并记录,后面就看不到了。

4)授予权限,非常重要,否则无法调用对应到接口。

5)需要设置一个回调地址(user token需要)

6)隐式获取设置(可以一步直接获取user token,存储在回调页面/auth的heders URL hash片段)

正常应该是先获取code,然后拿code换token。

以上关于注册应用的设置全部完毕。

七、应用开发

1)开发工具使用VS Code,下载Teams Tookit插件,创建一个Tab应用,使用JS语言,应用名称随意,如:MeetingRTT。

2)mainfest.json,这个很关键。注意3个地方:id(注册应用ID)、configurableTabs(侧边栏配置)和validDomains(合法域名)。

{
    "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
    "manifestVersion": "1.16",
    "version": "1.0.0",
    "id": "ba82be1b-xxx",
    "packageName": "com.helport.transcription",
    "developer": {
        "name": "Helport",
        "websiteUrl": "https://your-website.com",
        "privacyUrl": "https://your-website.com/privacy",
        "termsOfUseUrl": "https://your-website.com/terms"
    },
    "name": {
        "short": "Meeting Transcription",
        "full": "Real-time Meeting Transcription"
    },
    "description": {
        "short": "Real-time meeting transcription",
        "full": "This app provides real-time meeting transcription in Teams meetings."
    },
    "icons": {
        "outline": "outline.png",
        "color": "color.png"
    },
    "accentColor": "#FFFFFF",
    "configurableTabs": [
        {
            "configurationUrl": "https://xxx.ngrok-free.app/config",
            "canUpdateConfiguration": true,
            "scopes": [
                "team",
                "groupchat"
            ],
            "context": [
                "meetingSidePanel",
                "meetingStage"
            ]
        }
    ],
    "permissions": [
        "identity",
        "messageTeamMembers"
    ],
    "validDomains": [
        "xxx.ngrok-free.app"
    ]
}

3) 侧边栏安装配置:一个html页面config.html

<!DOCTYPE html>
<html>
<head>
    <title>Configure Transcription</title>
    <script src="https://statics.teams.cdn.office.net/sdk/v1.11.0/js/MicrosoftTeams.min.js"></script>
</head>
<body>
    <button id="save">Save</button>
    <script>
        microsoftTeams.initialize();
        document.getElementById('save').addEventListener('click', () => {
            microsoftTeams.settings.setSettings({
                entityId: "transcriptionPanel",
                contentUrl: "https://xxx.ngrok-free.app/meetingTab",
                suggestedDisplayName: "Helport"
            });
            microsoftTeams.settings.setValidityState(true);
            microsoftTeams.settings.registerOnSaveHandler((saveEvent) => {
                saveEvent.notifySuccess();
            });
        });
    </script>
</body>
</html>

4) 本地服务端点实现,主要是为了解决跨域访问问题,需要https://,可以使用ngrok弄个免费的。

本地服务端点,主要实现有:

/confg,上面的侧边栏安装配置页面;

/meetingTab,侧边栏主页面,完成转写;

/auth,完成user token的获取,返回一个页面获取access_token(user_token),存储在redis中;

/store_user_token,存储access_token(user_tokne)到redis中;

/get_user_token,从redis获取acces_token(user_token),页面获取会议信息需要;

/getTranscripts,获取转录列表,需要使用app token;

/getTranscriptContent,获取转写,需要使用user_token。

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';

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 storage
        async 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 token
        handleToken();
    </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 token
      await 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 token
    const 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 = '';

// 定义 /getTranscripts 端点
server.get('/getTranscripts', async (req, res) => {
  try {
      let token = "";
      if (app_token !=''){
        token = app_token
      }
      else{
        // 构建请求体
        const requestBody = new URLSearchParams({
          "grant_type": "client_credentials",
          "client_id": client_id, //注册应用ID
          "client_secret": client_secret, //主要应用密钥
          "scope": "https://graph.microsoft.com/.default", //默认范围,即注册应用配置的所有权限
        }).toString();

        // 获取app令牌
        const tokenUrl = `https://login.microsoftonline.com/${tenant_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();
          res.send(500, { error: errorData.error_description });
        }
        const tokenData = await tokenResponse.json();
        app_token = tokenData.access_token
        token = app_token
        console.log("app_token recevied!")
      }

      
      const organizerId = req.query.organizerId;

      if (!organizerId) {
          res.send(400, { error: 'Organizer ID is required' });
      }

      // 调用 Microsoft Graph API
      const graphUrl = `https://graph.microsoft.com/beta/users/${organizerId}/onlineMeetings/getAllTranscripts(meetingOrganizerUserId='${organizerId}')/delta`;
      const graphResponse = await fetch(graphUrl, {
          headers: {
              Authorization: `Bearer ${token}`,
          },
      });

      if (!graphResponse.ok) {
          const errorData = await graphResponse.json();
          res.send(500, { error: errorData.error.message });
      }

      const data = await graphResponse.json();

      // 返回转录文本
      res.send(200, data);
  } catch (error) {
      // 返回错误
      res.send(500, { error: error.message });
  }
});

// 定义 /getTranscriptContent 端点
server.get('/getTranscriptContent', async (req, res) => {
  try {

    const response = await fetch('https://xxx.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' });
      }
      const content_url = `${transcriptContentUrl}?$format=text/vtt`;
      console.log('content_url:', content_url)
      // 调用 Microsoft Graph API
      const 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.error.message });
      }

      const data = await graphResponse.text();

      console.log('data:', data)

      // 返回转录文本
      res.send(200, data);
  } catch (error) {
      // 返回错误
      res.send(500, { error: error.message });
  }
});

5)侧边栏页面

获取当前登录用户的信息,获取会议信息,获取会议组织者ID,获取转录列表,获取转写。前提是需要获取user token才可以获取用户信息,这里使用microsoftTeams.authentication.authenticate的url去打开一个授权登录页面,由于当前用户已经登录了,默认不需要去登录,会重定向到/auth,并将user token推送到/auth页面,该/auth服务端点接收到后,存储在redis中,然后关闭页面,之所以使用redis是因为其它存储都无效,从定向后,localStorage、sessionStorage都无法保存,该页面在浏览器中和在teams中完全是两个环境,这个两个环境的数据无法交换,所以只能使用redis在服务端存储。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Teams User Info</title>
    <script src="https://res.cdn.office.net/teams-js/2.0.0/js/MicrosoftTeams.min.js"></script>
</head>
<body>
    <button id="fetchTranscripts">Fetch Transcripts</button>
    <h2>Meeting Transcripts</h2>
    <div id="transcripts"></div>

    <script>
        const clientId ='your_client_id';
        const tenantId = 'your_tentant_id';
        const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
        const redirectUri = 'https://xxx.ngrok-free.app/auth'; // 确保与服务器端一致
        const scope = 'user.read'; //这个随便,获取到user token后会返回注册应用配置的所有权限

        

        const getUserInfo = async (accessToken) => {
            const graphUrl = 'https://graph.microsoft.com/v1.0/me';
            const response = await fetch(graphUrl, {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });

            const userInfo = await response.json();
            return userInfo;
        };

        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) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
        
            const data = await response.json();
            return data.value[0];
        };

        const getTranscripts = async (organizerId) => {
            const response = await fetch(`https://xxx.ngrok-free.app/getTranscripts?organizerId=${organizerId}`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const transcripts = await response.json();
            return transcripts;
        };

        const getTranscriptContent = async (transcriptContentUrl) => {
            const response = await fetch(`https://xxx.ngrok-free.app/getTranscriptContent?transcriptContentUrl=${transcriptContentUrl}`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
        
            const content = await response.text();
            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 = (subtitle, transcriptElement) => {
            const subtitleElement = document.createElement('div');
            subtitleElement.textContent = `${subtitle.speaker}: ${subtitle.content}`;
            transcriptElement.appendChild(subtitleElement);
        };

        const displayTranscripts = (transcripts) => {
            const transcriptsContainer = document.getElementById('transcripts');
            transcriptsContainer.innerHTML = ''; // 清空之前的转录信息
        
            if (transcripts && transcripts.value && transcripts.value.length > 0) {
                transcripts.value.forEach(transcript => {
                    const transcriptElement = document.createElement('div');
                    getTranscriptContent(transcript.transcriptContentUrl)
                        .then(subtitles => {
                            subtitles.forEach(subtitle => {
                                displaySubtitle(subtitle, transcriptElement);
                            });
                        })
                        .catch(error => {
                            transcriptElement.innerHTML = `
                                <p><strong>${error}</strong></p>
                            `;
                        });
        
                    transcriptsContainer.appendChild(transcriptElement);
                });
            } else {
                transcriptsContainer.innerText = 'No transcripts found.';
            }
        };
        const init = async () => {
                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);
                    },
                });
                try {
                    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);
                        // Fetch user info using the access token
                        const userInfo = await getUserInfo(user_token);
                        if (userInfo)
                        {
                            console.log('User Info:', userInfo);
                        }
                        const joinMeetingId = '45756456529'; // 替换为你要查询的 joinMeetingId
                        try {
                            const meetingDetails = await getMeetingDetails(user_token, joinMeetingId);
                            console.log('Meeting Details:', meetingDetails);
                            try {
                                meetingOrganizerUserId = meetingDetails.participants.organizer.identity.user.id;
                                document.getElementById('fetchTranscripts').addEventListener('click', async () => {
                                    const organizerId = meetingOrganizerUserId
                        
                                    if (!organizerId) {
                                        console.log('Organizer ID is required');
                                        return;
                                    }
                        
                                    try {
                                        const transcripts = await getTranscripts(organizerId);
                                        console.log(transcripts)
                                        displayTranscripts(transcripts);
                                    } catch (error) {
                                        console.log(`Error: ${error.message}`);
                                    }
                                });
                            } catch (error) {
                                console.error('Error fetching transcripts:', error);
                                document.getElementById('transcripts').innerText = 'Error fetching transcripts.';
                            }
                        } catch (error) {
                            console.error('Error fetching meeting details:', error);

                        }
                            console.log('User Token:', data.user_token);
                    } else {
                        console.error('Failed to get token:', response.statusText);
                    }
                } catch (error) {
                    console.error('Error getting token:', error);
                }
        };

        init();
    </script>
</body>
</html>

到处代码就开发完毕了。

八、部署调试

1) 点击Teams的打包工具,选择mainfest.json,选择dev即可。

2) dev的配置如下:即注册应用ID,TAB_ENDOINT侧边栏服务端点,TAB_DOMAIN域名

3)登录Developer Portal开发门户,上传appPackage.dev.zip即可。

4)调试,点击即可用将应用安装到chat的某个自己作为主持人权限的会议(如:Teams App Test)中去。

5) 会议中的app

总结:当前的转写只是一次性全显示出来,实际上需要同步实时更新,/deta接口的几种调用方式可以解决问题。

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

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

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

相关文章

株洲芦淞大桥事故的深刻反思

株洲芦淞大桥事故的深刻反思 2024年9月23日清晨&#xff0c;株洲芦淞大桥上发生了一起令人痛心的交通事故&#xff0c;一辆白色小汽车被出租车追尾后失控&#xff0c;冲向对向车道&#xff0c;最终酿成6人死亡、多人受伤的惨剧。 这起事故不仅给受害者家庭带来了无法弥补的伤…

【Python机器学习系列】开发Streamlit应用程序并部署机器学习模型(案例+源码)

这是我的第357篇原创文章。 一、引言 近年来&#xff0c;随着机器学习和人工智能技术的迅猛发展&#xff0c;越来越多的研究者选择将他们的模型以应用程序&#xff08;App&#xff09;的形式进行部署&#xff0c;从而使审稿人和其他研究者可以通过简单的界面&#xff0c;输入相…

9月23日

头文件 // My_string.h #ifndef MY_STRING_H #define MY_STRING_H#include <cstring> #include <algorithm>class My_string { private:char* data;size_t length;void resize(size_t new_length) {size_t new_capacity std::max(new_length 1, length);char* n…

一种求解城市场景下无人机三维路径规划的高维多目标优化算法,MATLAB代码

在城市环境下进行无人机三维路径规划时&#xff0c;需要考虑的因素包括高楼、障碍物、飞行安全和效率等。为了解决这些问题&#xff0c;研究者们提出了多种算法&#xff0c;包括基于智能优化算法的方法。 首先&#xff0c;无人机航迹规划问题的数学模型需要考虑无人机的基本约…

用Flowise+OneAPI+Ollama做一个在线翻译工作流

用FlowiseOneAPIOllama做一个在线翻译工作流&#xff0c;输入一种语言&#xff0c;马上翻译成另外一种语言&#xff0c;使用到的结点主要有&#xff0c;ChatLLM、提示词模板还有LLM Chain。 一、设置OneAPI和Ollama 1、Ollama的安装及配置&#xff0c;请参考&#xff1a;在ub…

三种委派 非约束委派 约束委派 基于资源的约束委派 概念

前言 简单记录下委派攻击的概念。具体的攻击演示/复现这里没有。 强烈建议反复通读《域渗透攻防指南》P242开始的4.5&#xff01;&#xff01;&#xff01; 以前看gitbook那个学的&#xff0c;yysy&#xff0c;真的不怎么适合零基础的看。 趁课上认真看了看4.5章&#xff0c…

如何快速免费搭建自己的Docker私有镜像源来解决Docker无法拉取镜像的问题(搭建私有镜像源解决群晖Docker获取注册表失败的问题)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 Docker无法拉取镜像 📒📒 解决方案 📒🔖 方法一:免费快速搭建自己的Docker镜像源🎈 部署🎈 使用🔖 备用方案⚓️ 相关链接 🚓️📖 介绍 📖 在当前的网络环境下,Docker镜像的拉取问题屡见不鲜(各类Nas查询…

【编程基础知识】MySQL中什么叫做聚簇索引、非聚簇索引、回表、覆盖索引

一、引言 在数据库的奇妙世界里&#xff0c;索引是提升查询速度的超级英雄。就像图书馔的目录帮助我们快速找到书籍一样&#xff0c;MySQL中的索引加速了数据检索的过程。本文将带你深入了解MySQL中的聚簇索引、非聚簇索引、回表操作以及覆盖索引&#xff0c;探索它们如何影响…

机器人顶刊IEEE T-RO发布无人机动态环境高效表征成果:基于粒子的动态环境连续占有地图

摘要&#xff1a;本研究有效提高了动态环境中障碍物建模的精度和效率。NOKOV度量动作捕捉系统助力评估动态占用地图在速度估计方面的性能。 近日&#xff0c;上海交通大学、荷兰代尔夫特理工研究团队在机器人顶刊IEEE T-RO上发表题为Continuous Occupancy Mapping in Dynamic …

Keysight 下载信源 Visa 指令

用于传输原始的IQ数据 file.wiq 或者 file.bin wave_bin:bytes with open("./WaveForm.wfm","rb") as f:wave_bin f.read()log.info("File:WaveForm.wfm Size:%d Bytes"%len(wave_bin)) IMPL.sendCommand(":MEM:DATA \"WFM1:FILE1\&q…

每日OJ题_牛客_杨辉三角(动态规划)

目录 牛客_杨辉三角&#xff08;动态规划&#xff09; 解析代码 牛客_杨辉三角&#xff08;动态规划&#xff09; 杨辉三角_牛客题霸_牛客网 解析代码 最基础的 dp 模型&#xff0c;按照规律模拟出来杨辉三角即可。 #include <iostream> using namespace std;int dp…

企业上云不迷茫,香港电讯助力企业上云全攻略

在全球政策和市场双重驱动下&#xff0c;云计算产业正迎来前所未有的增长浪潮。据中国信通院《云计算白皮书&#xff08;2023年&#xff09;》1显示&#xff0c;2022年全球云计算市场规模已达到4,910亿美元&#xff0c;同比增长率高达百分之十九。而在中国市场&#xff0c;这一…

带线无人机现身俄罗斯抗干扰技术详解

带线无人机在俄罗斯的出现&#xff0c;特别是其光纤制导技术的应用&#xff0c;标志着无人机抗干扰技术的一大进步。以下是对俄罗斯带线无人机抗干扰技术的详细解析&#xff1a; 一、带线无人机抗干扰技术背景 技术突破&#xff1a;俄军成功研发了光纤制导无人机&#xff0c;…

数据链路层协议 —— 以太网协议

目录 1.数据链路层解决的问题 2.局域网通信方式 以太网 令牌环网 无线局域网 3.以太网协议 以太网帧格式 对比理解Mac地址和IP地址 认识MTU MTU对IP协议的影响 MTU对UDP的影响 MTU对TCP的影响 基于以太网协议的报文转发流程 交换机的工作原理 4.ARP协议 ARP协议…

springboot+vue高校两校区通勤校车预约系统的设计与实现

目录 用户功能管理员功能系统实现截图技术介绍核心代码部分展示使用说明详细视频演示源码获取 用户功能 登录注册&#xff1a;允许用户创建账户并登录系统。 首页&#xff1a;展示系统主要功能和通勤车相关的重要信息。 个人中心&#xff1a;用户可以查看和编辑自己的个人信息…

ios swift5 UITextView占位字符,记录限制字数

文章目录 截图代码&#xff1a;具体使用代码&#xff1a;CustomTextView 截图 代码&#xff1a;具体使用 scrollView.addSubview(contentTextView)contentTextView.placeholderLabel.text LocalizableManager.localValue("write_comment")contentTextView.maxCharac…

分享两个虚拟试衣工具,一个在线,一个离线,还有ComfyUI插件

SAM &#xff0c;对不住了&#xff01; 我没记错的话&#xff0c;OpenAI CEO&#xff0c;性别男&#xff0c;取向男&#xff0c;配偶男。 这又让我联想到了苹果CEO库克... 所以OpenAI和Apple可以一啪即合。 钢铁直男老马就和他们都不对付~~ 开个玩笑&#xff0c;聊…

以数赋能实景三维创新“科技+文旅”

在数字化时代&#xff0c;科技与文化的融合为我们带来了无限可能。今天&#xff0c;我们将探讨如何利用实景三维技术&#xff0c;推动“科技文旅”的创新发展。 1. 实景三维技术概述 实景三维技术&#xff0c;是一种集成了遥感、地理信息系统&#xff08;GIS&#xff09;、三…

量子计算如何引发第四次工业革命——解读加来道雄的量子物理观

在科技的历史长河中&#xff0c;人类经历了多次重大的技术变革&#xff1a;从第一次工业革命的蒸汽机到第三次计算机革命的互联网与半导体技术&#xff0c;每次技术革命都彻底改变了我们的生活。而如今&#xff0c;我们正处在第四次工业革命的前夕&#xff0c;其核心驱动力是量…

mybatis 配置文件完成增删改查(一):直接查询所有信息

文章目录 编写三步走查询所有编写接口方法编写sql语句执行方法&#xff0c;测试结果数据库字段名和实体类变量名不一致&#xff1a;ResultMap数据库字段名和实体类变量名不一致&#xff1a;方法二 编写三步走 编写接口方法&#xff1a;Mapper接口 参数有无 结果类型编写sql语句…