2024年9月

在现代 Web 应用中,尤其是涉及视频播放、实时通信、地图导航等长时间运行的任务时,用户常常希望设备不要因为空闲而自动进入睡眠模式或屏幕变暗。为了解决这一问题,Web API 提供了一个名为 Wake Lock 的接口,允许开发者请求设备保持唤醒状态。

本文将详细介绍如何使用 Wake Lock API 来控制设备的唤醒状态,提供示例代码,并讨论一些常见的使用场景,尤其是如何确保网页隐藏或显示时自动管理唤醒锁。

什么是 Wake Lock API?

Wake Lock API 是一个用于防止设备进入睡眠或屏幕变暗的 Web API。通过 Wake Lock API,开发者可以请求设备保持活跃状态,防止因为电源管理机制导致任务中断。

目前,
Wake Lock API 只支持一种类型的唤醒锁:
screen

,它用于保持屏幕亮起,防止屏幕自动关闭或调暗。

使用 Wake Lock API 的前提

  • 浏览器支持
    :Wake Lock API 目前在大多数现代浏览器中都已经得到支持。
  • HTTPS 环境
    :该 API 需要通过 HTTPS 访问才能正常工作。

基本用例

以下是一个简单的示例,展示了如何使用 Wake Lock API 来保持屏幕唤醒:

// 创建一个全局变量来存储 WakeLock 实例
let wakeLock = null;

// 请求屏幕保持唤醒的函数
async function requestWakeLock() {
  try {
    // 请求屏幕唤醒锁
    wakeLock = await navigator.wakeLock.request('screen');
    console.log('屏幕唤醒锁已激活');
    
    // 监听唤醒锁的释放事件
    wakeLock.addEventListener('release', () => {
      console.log('屏幕唤醒锁已释放');
    });
  } catch (err) {
    console.error(`${err.name}, ${err.message}`);
  }
}

// 释放唤醒锁的函数
function releaseWakeLock() {
  if (wakeLock !== null) {
    wakeLock.release();
    wakeLock = null;
    console.log('屏幕唤醒锁手动释放');
  }
}

// 调用函数请求唤醒锁
requestWakeLock();

// 在页面关闭时释放唤醒锁
window.addEventListener('beforeunload', releaseWakeLock);

页面可见性处理:自动管理唤醒锁

由于当网页被隐藏或切换到后台时会自动释放唤醒锁,因此我们可以监听
visibilitychange
事件来确保网页重新可见时重新获取唤醒锁。当页面恢复显示时再次请求锁定,页面隐藏时则释放唤醒锁。

// 创建一个全局变量来存储 WakeLock 实例
let wakeLock = null;

// 请求屏幕保持唤醒的函数
async function requestWakeLock() {
  try {
    // 请求屏幕唤醒锁
    wakeLock = await navigator.wakeLock.request('screen');
    console.log('屏幕唤醒锁已激活');
    
    // 监听唤醒锁的释放事件
    wakeLock.addEventListener('release', () => {
      console.log('屏幕唤醒锁已释放');
    });
  } catch (err) {
    console.error(`${err.name}, ${err.message}`);
  }
}

// 释放唤醒锁的函数
function releaseWakeLock() {
  if (wakeLock !== null) {
    wakeLock.release();
    wakeLock = null;
    console.log('屏幕唤醒锁手动释放');
  }
}

// 处理页面可见性变化
function handleVisibilityChange() {
  if (document.visibilityState === 'visible') {
    // 页面重新可见时,重新请求唤醒锁
    requestWakeLock();
  } else {
    // 页面隐藏时,释放唤醒锁
    releaseWakeLock();
  }
}

// 监听页面可见性变化事件
document.addEventListener('visibilitychange', handleVisibilityChange);

// 页面加载时立即请求唤醒锁
requestWakeLock();

// 在页面关闭时释放唤醒锁
window.addEventListener('beforeunload', releaseWakeLock);

使用场景

Wake Lock API 在以下几种典型场景中非常有用:

1. 视频或音频播放

在播放视频或音频的应用中,用户希望屏幕保持亮起,以便可以随时调整播放进度或音量。通过 Wake Lock API,在媒体播放时保持屏幕唤醒,提供更好的用户体验。

videoElement.addEventListener('play', requestWakeLock);
videoElement.addEventListener('pause', releaseWakeLock);

2. 实时通信应用

对于视频通话、会议等实时通信应用,屏幕关闭会影响用户的互动体验。使用 Wake Lock API,可以确保设备在通话期间保持活跃,防止通话中断。

if (isInCall) {
  requestWakeLock();
} else {
  releaseWakeLock();
}

3. 导航和地图应用

在导航应用中,用户通常需要长时间查看屏幕来获取行进路线信息。使用 Wake Lock API,可以确保屏幕不会因为闲置而熄灭。

navigator.geolocation.watchPosition(() => {
  requestWakeLock();
}, () => {
  releaseWakeLock();
});

4. 游戏或全屏应用

网页游戏或需要长时间用户交互的全屏应用,也可以利用 Wake Lock API,避免游戏过程中屏幕突然熄灭。

document.addEventListener('fullscreenchange', () => {
  if (document.fullscreenElement) {
    requestWakeLock();
  } else {
    releaseWakeLock();
  }
});

错误处理和兼容性

虽然 Wake Lock API 提供了有用的功能,但它在某些设备上可能受到电源管理策略的限制。因此,开发者在请求唤醒锁时应当加入错误处理,以确保程序的健壮性。

if ('wakeLock' in navigator) {
  requestWakeLock();
} else {
  console.error('当前浏览器不支持 Wake Lock API');
}

浏览器兼容性

- Chrome Edge Firefox Opera Safari Chrome Android Firefox Android Opera Android Safari iOS Samsung Internet WebView Android
WakeLock 84 84 126 70 16.4 84 126 60 16.4 14.0 84
request 84 84 126 70 16.4 84 126 60 16.4 14.0 84

iOS 版 Safari

  • 16.4 (Released 2023-03-27)
  • 部分支持
  • 在独立的主屏幕Web应用程序不生效。详情请看
    bug 254545
    .

总结

Wake Lock API 为 Web 开发者提供了控制设备唤醒状态的能力,尤其适合那些需要保持屏幕长时间活跃的应用,如视频播放、实时通信、导航等。通过监听
visibilitychange
事件,应用程序可以智能地管理唤醒锁的状态,在页面可见时重新获取锁定,隐藏时释放锁定。

随着更多浏览器对 Wake Lock API 的支持,它将会成为提升用户体验的重要工具。如果你的应用涉及到长时间的任务或需要保持屏幕亮起,建议集成这个 API 来优化用户体验。

前言

在工业生产中,定制化的软件对于每个环节都至关重要。对于仓库管理,推荐一款开源的仓库管理系统(WMS)解决方案。

这款基于.NET 框架开发的移动应用,提供了全面的仓库操作、订单处理、主数据管理、数据分析及个人信息设置等功能,是工业仓库管理的有利助手。

项目介绍

SmoWMS 是一款基于.NET 技术开发的移动仓库管理系统。

包含了仓库管理中基础的入库、出库、订单管理、调拨、盘点、报表等功能。

支持扫码条码扫描、RFID扫描等仓库中常见的场景。

它通过 Visual Studio 作为 IDE,结合 Smobiler 开发平台,使用 SmobilerDesigner 工具来创建 .NET 组件,从而在 Visual Studio 环境中高效开发移动应用。

SmobilerClient 作为框架的客户端,采用专有的 stml 协议来实现原生控件的渲染和事件处理。

另外,SmoWMS 的云平台支持生成适用于 Android 和 iOS 的安装包,方便用户部署和使用。

项目功能

1、仓库管理

仓库部分按区域管理,包含管理、出入库、调拨和盘点等核心功能。

其中,出入库和盘点等功能支持扫码作业,可调用手机摄像头、手持终端的扫描头和 RFID 模块进行扫描。

2、订单管理

订单部分分为采购订单和销售订单,支持创建和跟踪采购与销售的各个阶段,并可进行入库、退库等操作。

右下角的快捷菜单便于快速创建采购和销售单。

3、主数据管理

在主数据部分,可以维护资产、仓库、客户、供应商等信息。

每类主数据支持三级分类,例如:资产分类 -> 电脑整机 -> 台式机 -> 联想。

4、统计功能

统计功能提供资产和耗材的商品分析、采购分析和销售分析。

每项分析均配有直观图表展示,并可选择仓库和类型以查看更详细的数据资料。

5、设置

在设置部分,可以修改个人信息,包括拍照或从相册上传个人头像,以及修改密码、邮箱和电话等。

项目环境

1、客户端运行环境要求

Android版本:支持Android 4.4及以上版本

IOS版本:支持IOS 9.0及以上版本

2、源代码运行环境要求

.NET FrameWork版本: .支持NET FrameWork 4.0及以上版本

Visual Studio版本:支持Visual studio 2010及以上版本

SmobilerDesigner版本:4.5.0及以上(下载并安装SmobilerDesigner)

项目文档

1、Smobiler 示例

Smobiler也使开发人员,可以在VisualStudio上,像开发WinForm一样拖拉控件,让许多人在开发APP时,再次回到所见即所得的开发方式中去。

地址:https://www.smobiler.com/webdemo/baseControl/albumview.aspx

2、服务端部署教程

下载源代码后,可以部署服务器进行运行和调试。具体的使用方法和部署步骤,请参考相关文档。

地址:https://www.smobiler.com/onlineDoc.html?pdf=serviceDoc_SmoWMS

3、客户端使用手册

用户手册详细地介绍了如何登录系统、使用各个功能模块、维护基础数据、管理资产以及操作耗材等。如需更多详细的信息,请查阅提供的文档。

地址:https://www.smobiler.com/onlineDoc.html?pdf=clientDoc

项目地址

GitHub:
https://github.com/comsmobiler/SmoWMS

Gitee:
https://gitee.com/smobiler/SmoWMS

APP体验地址

https://apps.smobiler.com/App/AppDetails?AppID=110

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

Canvas简历编辑器-Monorepo+Rspack工程实践

在之前我们围绕
Canvas
聊了很多代码设计层面的东西,在这里我们聊一下工程实践。在之前的文中我也提到过,因为是本着学习的态度以及对技术的好奇心来做的,所以除了一些工具类的库例如
ArcoDesign

ResizeObserve

Jest
等包之外,关于 数据结构
packages/delta
、插件化
packages/plugin
、核心引擎
packages/core
等都是手动实现的,所以在这里除了学习了
Canvas
之外,实际上还做了一些项目工程化的实践。

关于
Canvas
简历编辑器项目的相关文章:

Pnpm+Monorepo

我们先来聊聊为什么要用
monorepo
,先举一个我之前踩过的坑作为例子,在之前我的富文本编辑器项目
DocEditor
就是完全写在了独立的单个
src
目录中,在项目本身的运行过程中是没什么问题的,但是当时我想将编辑器独立出来作为
NPM
包用,打包的过程是借助了
Rollup
也没什么问题,问题就出在了引用方上。当时我在简历编辑器中引入文档编辑器的
NPM
包时,发现有一个模块被错误的
TreeShaking
了,现在都还能在编辑器中看到这部分兼容。

module: {
  rules: [
    {
      // 对`doc-editor-light`的`TreeShaking`有点问题
      test: /doc-editor-light\/dist\/tslib.*\.js/,
      sideEffects: true,
     },
   ]
}

这个问题导致了我在
dev
模式下没有什么问题,但是在
build
之后这部分代码被错误地移除掉了,导致编辑器的
wrapper
节点出现了问题,列表等元素不能正确添加。当然实际上这不能说明独立包项目不好,只能说整个管理的时候可能并不是那么简单,尤其是打包为
NPM
包的时候需要注意各个入口问题。那么现在引用我的富文本编辑器包已经变成了
4
个独立的包分别引用,各司其职,就没再出现过这个问题。

说起来打包的问题,我还踩过一个坑,不知道大家是不是见到过
React

Invalid hook call
这个经典报错。之前我将其独立拆包的时候之后,发现会报这个错,但是我在
package.json
中是标注的
peerDependencies "react": ">=16"
,按理说这里会直接应用安装该包的
React
,不可能出现版本不一致的问题,至于
Rules of Hooks
肯定也不可能,因为我之前是好好的,拆完包才出的问题。最后发现是我在
rollup
中没把
peerDependencies
这部分解析,导致
jsx-runtime
被打进了包里,虽然
React
的版本都是
17.0.2
但是实际上是运行了两个独立词法作用域的
React Hooks
,这才导致了这个问题。

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

1.  You might have mismatching versions of React and the renderer (such as React DOM)
2.  You might be breaking the Rules of Hooks
3.  You might have more than one copy of React in the same app See for tips about how to debug and fix this problem.

接着回到项目本身,当前项目已经抽离出来独立的
RspackMonoTemplate
,平时开发也会基于这个模版创建仓库。当前简历编辑器项目的结构
tree -L 2 -I node_modules --dirsfirst
如下:

CanvasEditor
│── packages
│   ├── core
│   ├── delta
│   ├── plugin
│   ├── react
│   └── utils
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
  • packages/core
    : 编辑器核心引擎模块,对于 剪贴板操作、事件管理、状态管理、
    History
    模块、
    Canvas
    操作、选区操作 等等都封装在这里,相当于实现了基本的
    Canvas
    引擎能力。
  • packages/delta
    : 数据结构模块,设计了基准数据结构,实现了
    DeltaSet
    数据结构以及原子化的
    Op
    操作,主要用于描述整个编辑器的数据结构以及操作,实现了
    invert
    等能力,对于实现
    History
    模块有很大的意义。
  • packages/plugin
    : 插件模块,在
    packages/delta
    的基础上设计了插件化的能力,主要为了实现编辑器的功能模块化,例如
    Text

    Image

    Rect
    等插件都是在这里实现的。
  • packages/react
    :
    React
    模块,主要是为了通过实现编辑器的视图层,在这里有比较重要的一点,我们的核心模块是视图框架无关的,如果有必要的话同样可以使用
    Vue

    Angular
    等框架来实现视图层。
  • packages/utils
    : 工具模块,主要是一些工具函数的封装,例如
    FixedNumber

    Palette
    等等,这些工具函数在整个编辑器中都有使用,是作为基础包在整个
    workspace
    中引用的。
  • package.json
    : 整个
    workspace

    package.json
    ,在这里配置了一些项目的信息,
    EsLint

    StyleLint
    相关的配置也都在这里实现。
  • pnpm-lock.yaml
    :
    pnpm
    的锁文件,用于锁定整个
    workspace
    的依赖版本。
  • pnpm-workspace.yaml
    :
    pnpm

    workspace
    配置文件,用于配置
    monorepo
    的能力。
  • tsconfig.json
    : 整个
    workspace

    tsconfig
    配置文件,用于配置整个
    workspace

    TypeScript
    编译配置,在这里是作为基准配置以提供给项目中的模块引用。

pnpm
自身是非常优秀的包管理器,通过硬链接和符号链接来节省磁盘空间,每个版本的包只需要存储一次,最重要的是
pnpm
创建了一个非扁平化的
node_modules
结构,从而确保依赖与声明严格匹配,严格控制了依赖提升,能够避免依赖升级的意外问题,这提高了项目的一致性和可预测性。

而说回到
monorepo

pnpm
不光是非常优秀的包管理器,其还提供了一个开箱即用的
monorepo
能力。在
pnpm
中存在一个
pnpm-workspace.yaml
文件,这个文件是用来配置
workspack
的,而
pnpm

workspace
就可以作为
monorepo
的能力,而我们的配置也非常简单,我们认为在
packages
目录下的所有目录都作为子项目。

packages:
  - 'packages/*'

通过
monorepo
我们可以很方便的管理所有子项目,特别是对于需要发
Npm
包的项目,将子模块拆分是个不错的选择,特别如果能够做到视图层框架无关的话就显得更加有意义。此外,
monorepo
对于整个项目的管理也有很多益处,例如在打包整个应用的时候,我们不需要对每个子项目发新的包之后才能打包,而是可以直接将编译过程放在
workspace
层面,这样就可以保证整个项目的一致性,简化了构建过程和持续集成流程,让所有项目可以共享构建脚本和工具配置。此外所有项目和模块共用同一个版本控制系统,便于进行统一的版本管理和变更跟踪,而且还有助于同步更新这些项目间的依赖关系。

TS+Rspack最佳实践

说了这么多使用
pnpm + monorepo
管理项目带来的好处,我们再来聊聊我对
TS

Rspack
应用于
Monorepo
的最佳实践,不知道大家是不是遇到过这样的两个问题:

  • 子项目的
    TS
    声明更改后不能实时生效,必须要编译一次子项目才可以,而子项目编译的过程中如果将
    dist
    等产物包删除,那么在
    vsc
    或者其他编辑器中就会报
    TS
    找不到引用声明的错误,这个时候就必须要用命令重新
    Reload TypeScript Project
    来去掉报错。而如果不将产物包删除的话,就会出现一些隐性的问题,例如原来某个文件命名为
    a.tsx
    ,此时因为一些原因需要将其移动到同名的
    a
    目录并且重新命名为
    index.tsx
    ,那么执行了这一顿操作之后,发现如果更改此时的
    index.tsx
    代码不会更新,必须要重启应用的
    webpack
    等编译器才行,因为其还是引用了原来的文件,产生类似的问题虽然不复杂但是排查起来还是需要时间的。
  • 更改子项目的
    TS
    代码必须要重新编译子项目,因为项目是
    monorepo
    管理的,在
    package.json
    中会有
    workspace
    引用,而
    workspace
    实际上是在
    node_modules
    被引用的,所以虽然是子项目但是仍然需要遵循
    node_modules
    的规则才可以,那么其通常需要被编译为
    js
    才可以被执行,所以每次修改代码都必须要全量执行一遍很是麻烦,当然通常我们可以通过
    -w
    命令来观察变动,但是毕竟多了一道步骤,且如果是存在
    alias
    的项目可能仅仅使用
    tsc
    来编译还不够。此外在
    monorepo
    中我们通常会有很多子项目,如果每个子项目都需要这样的话,特别在这种编译时全量编译而不是增量编译的情况下,那么整个项目的编译时间就会变得非常长。

那么在这里我们先来看第一个问题,子项目的
TS
声明更改后不能实时生效,因为我们也提到了
monorepo
子项目实际上是通过
node_modules
来管理和引用的,所以其在默认情况下依然需要遵循
node_modules
的规则,即
packages.json

types
字段指向的
TS
声明文件,那么我们有没有什么办法可以修改这个行为呢,当然是有的,我们在整个项目的根
tsconfig.json
配置
path
就可以完美解决这个问题。当我们配置好如下的内容之后,通过按住
Ctrl
加鼠标左键点击的时候,就可以跳转到子项目的根目录声明了。此外这里有个要关注的点是,在项目中不建议配置
"baseUrl": "."
,在这里会有一些奇奇怪怪的路径引用问题,所以在简历编辑器项目中除了要打包
Npm

tsconfig.build.json
之外,都是直接使用相对路径配置的。

{
  "compilerOptions": {
    "...": "...",
    "paths": {
      "sketching-core": ["./packages/core/src"],
      "sketching-delta": ["./packages/delta/src"],
      "sketching-plugin": ["./packages/plugin/src"],
      "sketching-utils": ["./packages/utils/src"],
    },
  },
  "include": [
    "packages/*/src"
  ]
}

那么解决了项目的
TS
声明问题之后,我们再来看编译的问题,这里的问题看起来会复杂一些,因为
TS
声明就单纯只是类型声明而已,不会影响到项目本身代码的编译,编译类型检查除外。那么在
Rspack
中应该配置才能让我们的代码直接指向子项目,而不是必须要走
node_modules
这套规则,实际上这里也很简单,只需要配置
resolve.alias
就可以了,这样当我们直接修改
TS
代码时,也能让编辑器立即响应增量编译。

{
// ....
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "sketching-core": path.resolve(__dirname, "../core/src"),
      "sketching-delta": path.resolve(__dirname, "../delta/src"),
      "sketching-plugin": path.resolve(__dirname, "../plugin/src"),
      "sketching-utils": path.resolve(__dirname, "../utils/src"),
    },
  },
// ....
}

实际上对于
Rspack
而言其帮我们做了很多事,比如即使是
node_modules

TS
文件也会编译,而对于一些通过
CRA
创建的
webpack
项目来说,这个配置就麻烦一些,当然我们同样也可以借助
customize-cra
来完成这件事,此外我们还要关闭一些类似于
ModuleScopePlugin
的插件才可以,下面是富文本编辑器项目
DocEditor
的配置。

const src = path.resolve(__dirname, "src");
const index = path.resolve(__dirname, "src/index.tsx");
const core = path.resolve(__dirname, "../core/src");
const delta = path.resolve(__dirname, "../delta/src");
const plugin = path.resolve(__dirname, "../plugin/src");
const utils = path.resolve(__dirname, "../utils/src");

module.exports = {
  paths: function (paths) {
    paths.appSrc = src;
    paths.appIndexJs = index;
    return paths;
  },
  webpack: override(
    ...[
      // ...
      addWebpackResolve({
        alias: {
          "doc-editor-core": core,
          "doc-editor-delta": delta,
          "doc-editor-plugin": plugin,
          "doc-editor-utils": utils,
        },
      }),
      babelInclude([src, core, delta, plugin, utils]),
      // ...
      configWebpackPlugins(),
    ].filter(Boolean)
  ),
};

此外,简历编辑器是纯前端的项目,这样的项目有个很大的优势是可以直接使用静态资源就可以运行,而如果我们借助
GitHub Action
就可以通过
Git Pages
在仓库中直接部署,并且可以直接通过
GitHub Pages
访问,这样在仓库中就能呈现一个完整的
DEMO

// .github/workflows/deploy.yml
name: deploy gh-pages

on:
  push:
    branches:
      - master

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          persist-credentials: false
          
      - name: install node-v16
        uses: actions/setup-node@v3
        with:
          node-version: '16.16.0'

      - name: install dependencies
        run: |
          node -v
          npm install -g pnpm
          pnpm config set registry https://registry.npmjs.org/
          pnpm install --registry=https://registry.npmjs.org/

      - name: build project
        run: |
          npm run build:react

      - name: deploy project
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: gh-pages
          FOLDER: packages/react/build

最后

在这里我们聊了为什么要用
Monorepo
以及简单聊了一下
pnpm workspace
的优势,然后解决了在子项目开发中会遇到的
TS
编译、项目编译的两个实际问题,分别在
Monorepo

Rspack

Webpack
项目中相关的部分实践了一下,最后还简单聊了一下利用
GitHub Action
直接在
Git Pages
部署在线
DEMO
。那么再往后边的文章中,我们就需要聊一聊如何实现 层级渲染与事件管理 的能力设计。

说明

该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

结合上一篇文章使用,味道更佳:
.net core8 使用Swagger(附当前源码)

有兴趣的朋友,请关注我吧(*^▽^*)。

第一步:安装最新Jwt包

包名:Microsoft.AspNetCore.Authentication.JwtBearer

第二步:appsettings.json中配置jwt

 /*jwt鉴权*/
 "JwtSetting": {"Issuer": "微信公众号:不只是码农", //发行人
   "Audience": "微信公众号:不只是码农", //订阅人
   "ExpireSeconds": 120, //过期时间,默认分钟
   "ENAlgorithm": "HS256", //秘钥算法
   "SecurityKey": "bzsmn=Start20240913EndOverallAuth-WebApi" //秘钥构成
 }

第三步:创建jwt解析模型

在OverallAuth-WebApi项目的目录下创建文件夹【model】,并创建一个类文件JwtSettingModel.cs

OverallAuth-WebApi结构,见上一篇文章:
.net core8 使用Swagger(附当前源码)

 /// <summary>
 ///jwt配置模型/// </summary>
 public classJwtSettingModel
{
/// <summary> ///密钥/// </summary> public string SecurityKey { get; set; }/// <summary> ///加密算法/// </summary> public string ENAlgorithm { get; set; }/// <summary> ///颁发者/// </summary> public string Issuer { get; set; }/// <summary> ///接收者/// </summary> public string Audience { get; set; }/// <summary> ///过期时间 单位:秒/// </summary> public int ExpireSeconds { get; set; }
}

目录结构如下:

第四步:创建Jwt、AppSettings插件

目录结构如下:

上图可以看到,我们创建了JwtPlugInUnit和AppSettingsPlugInUnit2个插件,它分别对应jwt和AppSettings配件文件的解析。

那么我们看下,这2个类里面的具体内容。

JwtPlugInUnit如下:

/// <summary>
///jwt插件/// </summary>
public static classJwtPlugInUnit
{
/// <summary> ///初始化JWT/// </summary> /// <param name="services"></param> public static void InitJWT(thisIServiceCollection services)
{
var jwtsetting = AppSettingsPlugInUnit.GetNode<JwtSettingModel>("JwtSetting");
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o
=>{
o.TokenValidationParameters
= newTokenValidationParameters()
{
ValidateIssuerSigningKey
= true,
ValidIssuer
=jwtsetting.Issuer,
ValidAudience
=jwtsetting.Audience,
IssuerSigningKey
= newSymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtsetting.SecurityKey)),
ValidateIssuer
= true,
ValidateAudience
= true,
ValidateLifetime
= true,
ClockSkew
=TimeSpan.Zero
};
});
}
}

AppSettingsPlugInUnit如下:

 /// <summary>
 ///AppSettings配置文件插件/// </summary>
 public classAppSettingsPlugInUnit
{
/// <summary> ///声明配置属性/// </summary> public static IConfiguration Configuration { get; set; }/// <summary> ///构造函数/// </summary> staticAppSettingsPlugInUnit()
{
Configuration
= newConfigurationBuilder()
.Add(
new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true})
.Build();
}
/// <summary> ///获得配置文件的对象值/// </summary> /// <param name="jsonPath">文件路径</param> /// <param name="key"></param> /// <returns></returns> public static string GetJson(string jsonPath, stringkey)
{
if (string.IsNullOrEmpty(jsonPath) || string.IsNullOrEmpty(key)) return null;
IConfiguration config
= new ConfigurationBuilder().AddJsonFile(jsonPath).Build();//json文件地址 return config.GetSection(key).Value;//json某个对象 }/// <summary> ///获取数据库连接字符串/// </summary> /// <returns></returns> public static stringGetMysqlConnection()
{
return Configuration.GetConnectionString("MySql").Trim();
}
/// <summary> ///根据节点名称获取配置模型/// </summary> /// <typeparam name="T"></typeparam> /// <param name="Node"></param> /// <returns></returns> public static T GetNode<T>(string Node) where T : new()
{
T model
= Configuration.GetSection(Node).Get<T>();returnmodel;

}
}

第五步:让jwt遵守Swagger协议

因为我们系统使用到了Swagger,所以要让jwt遵守Swagger协议,因此我们要在Swagger中添加如下代码。

/// <summary>
///初始化Swagger/// </summary>
/// <param name="services"></param>
public static void InitSwagger(thisIServiceCollection services)
{
//添加swagger services.AddSwaggerGen(optinos =>{typeof(ModeuleGroupEnum).GetEnumNames().ToList().ForEach(version =>{
optinos.SwaggerDoc(version,
newOpenApiInfo()
{
Title
= "权限管理系统",
Version
= "V2.0",
Description
= "求关注,求一键三连",
Contact
= new OpenApiContact { Name = "微信公众号作者:不只是码农 b站作者:我不是码农呢", Url = new Uri("http://www.baidu.com") }
});

});
//反射获取接口及方法描述 var xmlFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
optinos.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFileName),
true);

//使用jwt
optinos.AddSecurityDefinition(
"Bearer", newOpenApiSecurityScheme
{
Description
= "请在下方输入框子输入Bearer Token 开启JWT鉴权",
Name
= "Authorization", //默认名称,不能修改 In =ParameterLocation.Header,
Type
=SecuritySchemeType.ApiKey,
Scheme
= "Bearer"});//让swagger遵守jwt协议 optinos.AddSecurityRequirement(newOpenApiSecurityRequirement
{
{
newOpenApiSecurityScheme
{
Reference
= newOpenApiReference
{
Type
=ReferenceType.SecurityScheme,
Id
= "Bearer"}
},
new List<string>()
}
});

});
}

说明:InitSwagger方法是初始化Swagger的方法,在上一篇文章:
.net core8 使用Swagger(附当前源码)
中有讲到。

第六步:初始化Jwt

在Program中添加一下代码,初始化Jwt

第七步:验证Jwt

做好以上步骤,jwt就可以正常使用。

当你看到图中标识时,就表示jwt初始化成功,就可以在系统中使用jwt鉴权等操作。

使用【[Authorize]】、【 [AllowAnonymous]】特性测试鉴权。

以下2个接口一个需要验证、一个不需要验证,我们来测试下。

[Authorize]开启验证测试

CheckJwt接口:开启验证,不传token

可以看到,开启jwt验证的,接口在没有传入token的情况下,访问失败。

UnCheckJwt接口:不开启验证。

以上就是.net core8 使用jwt系统鉴权的配置过程。

感谢你的耐心观看。

如果对你有帮助,请关注我微信公众号吧(*^▽^*)。

源代码地址:https://gitee.com/yangguangchenjie/overall-auth2.0-web-api

帮我Star,谢谢。

有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

常见的多智能体框架有几类,有智能体相互沟通配合一起完成任务的例如ChatDev,CAMEL等协作模式, 还有就是一个智能体负责一类任务,通过选择最合适的智能体来完成任务的路由模式,当然还有一些多智能体共享记忆层的复杂交互模式,这一章我们针对智能体路由,也就是选择最合适的智能体来完成任务这个角度看看有哪些方案。

上一章我们讨论的何时使用RAG的决策问题,把范围放大,把RAG作为一个智能体,基座LLM作为另一个智能体,其实RAG决策问题也是多智能体路由问题的一个缩影。那实际应用场景中还有哪些类型的智能体路由呢?

  • 不同角色的智能体,例如看到最搞笑的是不同流派的算命机器人
  • 不同工具挂载的智能体,例如接入不同知识库,拥有不同领域工具
  • 不同思考方式的智能体,例如COT思考,有Step-back思考,有outline思考
  • 不同工作流的智能体,例如例如不使用RAG,使用单步RAG,多步RAG的智能体路由
  • 把以上融合,也就是不同角色,工具,思考方式,工作流的综合智能体路由

而这里我们看两种外挂策略,也就是可以直接在当前已有多智能体外层进行路由的方案。

基于能力和领域的智能体路由

MARS其实是一篇大模型出现前的文章,但是却可以作为多Agent路由的基础文章之一,它主要针对当
不同领域(能力)的智能体选择
。思路非常清晰。论文先定义了多智能体选择问题,该问题的组成元素包括

  • query: 用户提问
  • agent skill:对于智能体能力的描述,也可以是sample queries
  • agent response:智能体对用户提问的回答

那自然就有两种智能体选择的方案,
一个是直接基于query进行选择(Query-Pairing),一个是基于智能体response进行选择(Response-pairing)
,当前的多智能体决策也就是这两个大方向,前者更快但精度有限,后者更慢但效果更好。下面说下方案中的细节,因为实际操作时你会发现两个方案都有难点。

image

Question pairing

基于query进行判断的问题在于
如何描述agent能干啥
,论文指出智能体的能力边界不好界定,更难描述。

论文给出的一个方案是使用
query sample
,虽然不知道模型的全局能力,但是基于用户历史的使用情况,可以知道模型能回答哪些query,例如"locate me some good places in Kentucky that serve sushi"这个问题,"Alexa", "Google"可以回答这个问题。那就可以基于历史收集的query样本训练一个
多标签分类模型,预测每个query哪些智能体可以回答
。其实这种方案也是使用了response,只不过使用的是历史agent回答。

除了query分类,论文还用了相似度。论文收集了agent在公开网站上的能力描述,例如"Our productivity bot helps you stay productive and organized. From sleep timers and alarms to reminders, calendar management, and email ....".然后使用agent描述和query的文本相似度排序作为agent能否回答该问题的判断。这里论文尝试了bm25,USE,还有微调Roberta等方式进行向量编码。之前我们也考虑过类似KNN的方案,但这种方案有个问题在于文本相似可以衡量领域差异,例如数学Agent,金融Agent,但是无法区分任务复杂程度,所以不适用于领域之外的其他agent路由场景。

Response Pairing

使用在线模型回答来进行路由的核心难点其实就是如何判断response质量,论文指出的是前文多通过response和query的相似度来判断,这是不够的,还要判断准确性,因此论文采用了cross-encoder训练了query-response ranking模型。不过在大模型出来后的这两年,对于response回答质量有了更全面的评价标准,例如OpenAI的3H(Helful, Harmless,Honesty),DeepMind更关注的2H(helpful, harmless),也有了更多的Reward和Judement模型的训练方案,感兴趣的同学可以去看
好对齐RLHF-OpenAI·DeepMind·Anthropic对比分析

这里就不细说论文的方案了,直接来看下效果吧。论文在22年当时的四大Agent(Aleax,Google,houndify,Adasa)上评估,基于Response排序的方案最好,不过使用Query Sample分类的方案效果也不差。

image

基于问题复杂程度的智能体路由

  • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity

前面的MARS更多是从领域层面对智能体进行划分,例如bank agent,weather agent,transport agent,但是RAG问题上,领域差异更多只影响数据库路由,也就是使用哪些召回,查什么数据。还有一个更重要的差异,来自问题的复杂度。类似的方案有SELF-RAG,不过它是把路由融合在了模型推理的过程中,整体复杂度太高,可用性就有些低了。所以我们看下Adaptive-RAG的外挂路由的方案。

Adaptive-RAG提出了通过分类器,对query复杂程度进行分类,并基于分类结果分别选择LLM直接回答,简单单步RAG,或者复杂多步RAG(论文选择了Interleaving-COT),如下图
image

那如何判断一个query的复杂程度呢,这里其实和前面MARS提出的query pairing中的query多标签分类模型的思路是相似的。也是使用同一个query,3种模式的回答结果的优劣作为标签来训练分类模型,当然也可以是listwise排序模型。论文使用的是有标准答案的QA数据集,因此多模型回答的结果判断起来会比较简单,这里3种回答方式也有优先级,那就是更简单的链路能回答正确的话,默认标签是最简单的方案。这里的query分类器,论文训练了T5-Large,样本只有400条query,以及每个问题对应在3种链路上的回答结果。

而在现实场景中RAG样本的反馈收集要复杂的多,需要先基于标注样本训练Reward模型,得到对回答质量的评分,再使用Reward模型对多个链路的回答进行打分从而得到分类标签。

如果你的RAG链路选择更多,优先级排序更加复杂的话,不妨使用多标签模型,得到多个候选agent,再基于多个agent之间的优先级选择复杂程度最低,或者在该任务上优先级最高的Agent进行回答。

效果论文分别在single-step和multi-hopQA数据集上进行验证,Adaptive都能在保证更优效果的同时,使用更少的时间和步骤完成任务(Oracle是当分类器完全正确时的效果比较天花板)

image

基于用户偏好的智能体路由

  • Zooter:Routing to the Expert: Efficient Reward-guided Ensemble of Large
    Language Models

第三篇论文是从用户回答偏好出发,选择最合适的agent,其实也是最优的基座模型。基座模型Ensemble和Routing也算是智能体路由中的一个独立的方向,包括的大模型小模型路由以求用更少的成本更快的速度来平衡效果,也有多个同等能能力的模型路由来互相取长补短。个人认为基座模型的路由比不同领域的Agent,或者rag要复杂一些,因为基座模型间的差异在文本表征上更加分散,抽象难以进行归类和划分。这差异可能来自预训练的数据分布差异,指令数据集的风格差异,或者rlhf的标注规则差异等等~

正是因为难以区分,所以基座模型路由要是想使用query-pairing达到可以和response-pairing相近的效果和泛化性,需要更多,更丰富的训练数据。Zooter给出的就是蒸馏方案,也就是训练reward模型对多模型的回答进行评分,然后把模型评分作为标签来训练query路由模型。如下

image

蒸馏部分,论文借鉴了蒸馏损失函数,为了从reward模型中保留更多的信息,这里没有把多模型的reward打分最后转化成top-answer的多分类问题,而是把reward打分进行了归一化,直接使用KL-divergence让模型去拟合多个模型回答之间的相对优劣。同时考虑到reward-model本身的噪声问题,论文在蒸馏时也使用了label-smoothing的方案来降低噪声,提高模型回答置信度。其实也可以使用多模型reward打分的熵值来进行样本筛选。

奖励函数,论文使用QwenRM作为reward模型,混合多数据集构建了47,986条query样本,对mdeberta-v3-base进行了蒸馏训练。

效果上,论文对比了6个单基座模型,使用蒸馏后的模型进行query路由(ours),以及使用不同Reward模型对response进行路由,还有SOTA GPT3.5和GPT4

  • 不同Reward模型的效果差异较大,在当前评估的4个任务集上,Qwen和Ultra的效果要显著更好
  • 论文蒸馏的方式训练的Zooter模型在query路由的效果上可以基本比肩使用RM进行response路由,使用1/6的推理成本就能做到相似的效果有相似的推理效果

image

更多智能体路由相关方案

更多RAG路由,智能体路由,基座模型路由Ensemble的论文,大家感兴趣的可以自己去看

  • 智能体路由
    • One Agent To Rule Them All: Towards Multi-agent Conversational AI
    • A Multi-Agent Conversational Recommender System
  • 基座模型路由&Ensemble
    • Large Language Model Routing with Benchmark Datasets
    • LLM-BL E N D E R: Ensembling Large Language Models with Pairwise Ranking and Generative Fusion
    • RouteLLM: Learning to Route LLMs with Preference Data
    • More Agents Is All You Need
    • Routing to the Expert: Efficient Reward-guided Ensemble of Large Language Models
  • 动态RAG(When to Search & Search Plan)
    • SELF-RAG: LEARNING TO RETRIEVE, GENERATE, AND CRITIQUE THROUGH SELF-REFLECTION ⭐
    • Self-Knowledge Guided Retrieval Augmentation for Large Language Models
    • Self-DC: When to retrieve and When to generate Self Divide-and-Conquer for Compositional Unknown Questions
    • Small Models, Big Insights: Leveraging Slim Proxy Models To Decide When and What to Retrieve for LLMs
    • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity
    • REAPER: Reasoning based Retrieval Planning for Complex RAG Systems
    • When to Retrieve: Teaching LLMs to Utilize Information Retrieval Effectively
    • PlanRAG: A Plan-then-Retrieval Augmented Generation for Generative Large Language Models as Decision Makers

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >>
DecryPrompt