OAuth 2.0 Scope 的使用与设计规划

一、Scope 是什么

Scope(权限范围)是 OAuth 2.0(Open Authorization 2.0)授权框架中用于限定访问令牌(Access Token)权限边界 的机制。它回答的核心问题不是"你是谁",而是"这个令牌被允许做什么"。

OAuth 2.0 的核心思想是「最小权限授权」:第三方应用不应该拿到用户的密码,也不应该拿到无限权限的令牌,而只应拿到完成特定任务所必需的那部分权限。Scope 正是这部分权限的形式化描述。

复制代码
传统模式:第三方拿到密码 → 无限权限,无法撤销,风险极高
OAuth 模式:第三方拿到受限令牌 → 权限由 scope 圈定,可独立撤销

需要厘清三个相邻概念的边界:

概念 回答的问题 归属阶段
Authentication(认证) 你是谁 由 OIDC(OpenID Connect)的 id_token 承载
Authorization(授权) 你被允许做什么 由 scope + 后端策略共同决定
Scope(范围) 令牌「请求/被授予」了哪些权限类别 贯穿授权流程

一个关键认知:Scope 不是完整的授权决策,它只是授权决策的输入之一。这一点是后文设计部分的基石。


二、Scope 在协议中的位置与流转

Scope 在 RFC 6749(OAuth 2.0 核心规范)中以一个空格分隔的字符串形式存在,区分大小写。

2.1 完整流转链路

以授权码模式(Authorization Code Flow)为例,scope 在四个节点出现:
资源服务器(Resource Server) 授权服务器(Authorization Server) 客户端(Client) 用户(Resource Owner) 资源服务器(Resource Server) 授权服务器(Authorization Server) 客户端(Client) 用户(Resource Owner) #mermaid-svg-8uvO6fEpQC3mUCU6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8uvO6fEpQC3mUCU6 .error-icon{fill:#552222;}#mermaid-svg-8uvO6fEpQC3mUCU6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8uvO6fEpQC3mUCU6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8uvO6fEpQC3mUCU6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8uvO6fEpQC3mUCU6 .marker.cross{stroke:#333333;}#mermaid-svg-8uvO6fEpQC3mUCU6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8uvO6fEpQC3mUCU6 p{margin:0;}#mermaid-svg-8uvO6fEpQC3mUCU6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8uvO6fEpQC3mUCU6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8uvO6fEpQC3mUCU6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8uvO6fEpQC3mUCU6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8uvO6fEpQC3mUCU6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8uvO6fEpQC3mUCU6 .sequenceNumber{fill:white;}#mermaid-svg-8uvO6fEpQC3mUCU6 #sequencenumber{fill:#333;}#mermaid-svg-8uvO6fEpQC3mUCU6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8uvO6fEpQC3mUCU6 .messageText{fill:#333;stroke:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8uvO6fEpQC3mUCU6 .labelText,#mermaid-svg-8uvO6fEpQC3mUCU6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .loopText,#mermaid-svg-8uvO6fEpQC3mUCU6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8uvO6fEpQC3mUCU6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8uvO6fEpQC3mUCU6 .noteText,#mermaid-svg-8uvO6fEpQC3mUCU6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8uvO6fEpQC3mUCU6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8uvO6fEpQC3mUCU6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8uvO6fEpQC3mUCU6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8uvO6fEpQC3mUCU6 .actorPopupMenu{position:absolute;}#mermaid-svg-8uvO6fEpQC3mUCU6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8uvO6fEpQC3mUCU6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8uvO6fEpQC3mUCU6 .actor-man circle,#mermaid-svg-8uvO6fEpQC3mUCU6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8uvO6fEpQC3mUCU6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 授权请求 scope=read:profile write:posts2. 展示同意页(基于请求的 scope)3. 用户授权(可能裁剪部分 scope)4. 返回授权码 code5. code 换取令牌6. 返回 token + 实际授予的 scope7. 携带 Access Token 访问 API8. 校验 token 中的 scope 是否覆盖该 API9. 返回数据 或 403 insufficient_scope

2.2 四个关键节点

节点 1:请求 scope(客户端发起)

复制代码
GET /authorize?
    response_type=code
    &client_id=s6BhdRkqt3
    &scope=read:profile%20write:posts
    &redirect_uri=https://app.example.com/cb
    &state=xyz

节点 3:用户裁剪(重要但常被忽略)

用户在同意页(Consent Screen)有权只勾选部分 scope。因此请求的 scope ≠ 授予的 scope

节点 6:返回实际授予的 scope(RFC 6749 §3.3)

如果授予的 scope 与请求的不一致,授权服务器必须 在令牌响应中返回 scope 字段告知客户端实际范围:

json 复制代码
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read:profile"
}

这意味着客户端代码不能假设 自己请求的 scope 全部到手,必须读取响应中的 scope 做降级处理。

节点 8:资源服务器校验

权限不足时按 RFC 6750(Bearer Token 规范)返回:

http 复制代码
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                  scope="write:posts",
                  error_description="The request requires write:posts"

三、Scope 的承载方式:JWT vs 内省

Scope 信息最终要被资源服务器读取,有两种主流方式:

3.1 JWT(JSON Web Token)自包含

scope 直接编码进 Access Token 的 payload:

json 复制代码
{
  "sub": "user-12345",
  "aud": "https://api.example.com",
  "iss": "https://auth.example.com",
  "exp": 1735689600,
  "scope": "read:profile write:posts"
}

资源服务器本地验签即可读取 scope,无需远程调用------性能好,但 scope 在令牌有效期内无法实时撤销。

注意字段命名差异:RFC 9068(JWT Profile for Access Tokens)规定用 scope(单数、空格分隔字符串);但部分实现(如旧版 Spring Authorization Server)使用 scp(数组)。设计时需统一约定。

3.2 Token Introspection(令牌内省,RFC 7662)

不透明令牌(Opaque Token)本身不含信息,资源服务器需回调授权服务器查询:

http 复制代码
POST /introspect
Authorization: Basic <client_credentials>

token=mF_9.B5f-4.1JqM
json 复制代码
{
  "active": true,
  "scope": "read:profile write:posts",
  "sub": "user-12345",
  "exp": 1735689600
}

可实时撤销,但每次都增加一跳延迟。

维度 JWT 自包含 内省
校验性能 本地,快 远程调用,慢
实时撤销 难(需配黑名单)
scope 一致性 令牌签发时冻结 实时反映授权服务器状态

四、Scope 的命名与粒度设计

这是整篇文章的核心。Scope 设计本质上是一次 API 权限模型的领域建模,设计不当会导致后期权限体系僵化、难以演进。

4.1 命名风格

业界主流是 资源:操作 的二段式风格(Google、GitHub 普遍采用):

复制代码
read:profile          # 读取用户资料
write:posts           # 创建/编辑文章
delete:comments       # 删除评论
admin:billing         # 账单管理

也有 服务.资源.操作 三段式(更适合大型多服务平台):

复制代码
crm.contacts.read
crm.contacts.write
billing.invoices.export

设计原则:

  1. 可读性:用户在同意页要能看懂,所以 scope 名应当能直接映射成人话描述。
  2. 稳定性:scope 一旦发布并被第三方集成,几乎不可删除(破坏向后兼容),因此命名要慎重、预留演进空间。
  3. 一致性:动词、单复数、分隔符在整个体系内统一。

4.2 粒度:粗 vs 细的权衡

这是最难的决策点。

复制代码
过粗(如只有 read / write):
  ✗ 违背最小权限,第三方拿到 write 就能改一切
  ✓ 同意页简单,集成方便

过细(如 read:profile:email、read:profile:phone......):
  ✓ 权限精确
  ✗ 同意页爆炸,用户疲劳(同意疲劳 consent fatigue)
  ✗ 第三方需请求一长串 scope,集成复杂

推荐的中庸策略------按「业务能力」而非「数据库表」划分:

反模式(按表/字段) 推荐(按业务能力)
read:user_table read:profile
read:email_column read:contact_info
write:posts_table write:post_tags_table write:posts

经验法则:一个 scope 应对应用户能理解的一种「能力」,而不是一张表或一个 endpoint

4.3 读写分离

强烈建议至少在读/写维度拆分。这是性价比最高的粒度划分:大量第三方(如数据分析、展示类应用)只需读权限,读写分离能显著降低用户授权的心理负担和泄露风险。

复制代码
read:*    → 只读类应用申请,风险低,同意页可弱化
write:*   → 写入类需单独申请,同意页应强提示
delete:*  → 删除类建议进一步独立,因其不可逆

五、Scope 与 RBAC/ABAC 的协作

一个常见的严重误区:把 scope 当成完整的权限系统

核心原则:Scope 圈定的是「这个令牌最多能做什么」,而不是「这个用户实际能做什么」。最终授权 = Scope ∩ 用户实际权限(RBAC/ABAC)。

举例说明这个交集关系:

复制代码
某令牌 scope = write:posts        (客户端被授予了写文章的能力)
但该用户角色 = 普通读者            (RBAC 角色不允许写)

最终结果:拒绝。

因为:scope 只是「客户端被允许代表用户做的事的上界」,
      它永远不能突破用户本身的权限。

正确的分层校验:
#mermaid-svg-pUcBcLKdtBCFnMu5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pUcBcLKdtBCFnMu5 .error-icon{fill:#552222;}#mermaid-svg-pUcBcLKdtBCFnMu5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pUcBcLKdtBCFnMu5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .marker.cross{stroke:#333333;}#mermaid-svg-pUcBcLKdtBCFnMu5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pUcBcLKdtBCFnMu5 p{margin:0;}#mermaid-svg-pUcBcLKdtBCFnMu5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster-label text{fill:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster-label span{color:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster-label span p{background-color:transparent;}#mermaid-svg-pUcBcLKdtBCFnMu5 .label text,#mermaid-svg-pUcBcLKdtBCFnMu5 span{fill:#333;color:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .node rect,#mermaid-svg-pUcBcLKdtBCFnMu5 .node circle,#mermaid-svg-pUcBcLKdtBCFnMu5 .node ellipse,#mermaid-svg-pUcBcLKdtBCFnMu5 .node polygon,#mermaid-svg-pUcBcLKdtBCFnMu5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .rough-node .label text,#mermaid-svg-pUcBcLKdtBCFnMu5 .node .label text,#mermaid-svg-pUcBcLKdtBCFnMu5 .image-shape .label,#mermaid-svg-pUcBcLKdtBCFnMu5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pUcBcLKdtBCFnMu5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .rough-node .label,#mermaid-svg-pUcBcLKdtBCFnMu5 .node .label,#mermaid-svg-pUcBcLKdtBCFnMu5 .image-shape .label,#mermaid-svg-pUcBcLKdtBCFnMu5 .icon-shape .label{text-align:center;}#mermaid-svg-pUcBcLKdtBCFnMu5 .node.clickable{cursor:pointer;}#mermaid-svg-pUcBcLKdtBCFnMu5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .arrowheadPath{fill:#333333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pUcBcLKdtBCFnMu5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pUcBcLKdtBCFnMu5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pUcBcLKdtBCFnMu5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster text{fill:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 .cluster span{color:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pUcBcLKdtBCFnMu5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pUcBcLKdtBCFnMu5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pUcBcLKdtBCFnMu5 .icon-shape,#mermaid-svg-pUcBcLKdtBCFnMu5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pUcBcLKdtBCFnMu5 .icon-shape p,#mermaid-svg-pUcBcLKdtBCFnMu5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pUcBcLKdtBCFnMu5 .icon-shape .label rect,#mermaid-svg-pUcBcLKdtBCFnMu5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pUcBcLKdtBCFnMu5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pUcBcLKdtBCFnMu5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pUcBcLKdtBCFnMu5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否





请求到达 API
Token 有效?
401 Unauthorized
Scope 覆盖该操作?
403 insufficient_scope
用户 RBAC/ABAC 允许?
403 Forbidden
放行

两层是**与(AND)**关系,缺一不可。scope 通过不代表业务授权通过。


六、特殊场景下的 Scope

6.1 OIDC 标准 scope

OIDC 在 OAuth 之上定义了一组保留 scope,用于身份认证:

Scope 含义
openid 必需 ,声明这是一次 OIDC 请求,触发返回 id_token
profile 请求姓名、头像等基本资料 claim
email 请求邮箱及验证状态
offline_access 请求返回 Refresh Token(刷新令牌)

offline_access 尤其关键:它是「是否颁发 Refresh Token」的开关,决定了客户端能否在用户离线时持续访问。

6.2 客户端凭证模式(Client Credentials)

机器对机器(M2M)场景无用户参与,没有同意页。此时 scope 由授权服务器根据客户端注册时预配置的权限直接裁定,客户端请求的 scope 不能超出注册时的上限。

6.3 增量授权(Incremental Authorization)

不要在首次登录就索要全部 scope。最佳实践是用到时再要

复制代码
首次登录:scope=openid profile          (仅登录所需)
用户点击"导出到云盘"时:再发起 scope=write:drive 的授权

这能显著提升首屏授权转化率,也更符合最小权限原则。Google 的 API 即采用此模式。

6.4 Resource Indicators(RFC 8707)

当一个授权服务器服务多个资源服务器(API)时,相同名字的 scope 可能在不同 API 下含义不同。RFC 8707 引入 resource 参数,让客户端声明令牌的目标受众(audience),授权服务器据此签发受众绑定的窄令牌,避免一个令牌被滥用于多个 API:

复制代码
&scope=read:data
&resource=https://api-a.example.com

七、设计 Checklist(落地清单)

把前述原则浓缩为一份可执行清单:

  1. 命名 :确定统一风格(资源:操作),动词/单复数/分隔符全局一致。
  2. 粒度:按「业务能力」而非数据表划分;至少做读写分离;删除类独立。
  3. 最小化:客户端按需申请;首登只要登录 scope,敏感能力走增量授权。
  4. 同意页:每个 scope 提供人话描述;写/删类强提示。
  5. 不要信任请求 scope :客户端必须读取令牌响应中的实际 scope 并降级处理。
  6. 双层校验:资源服务器先查 scope(403 insufficient_scope),再查 RBAC/ABAC,二者为 AND。
  7. 承载方式 :JWT 自包含 vs 内省,按「性能 vs 实时撤销」需求选择;统一 scope 字段格式。
  8. 演进:scope 几乎只增不减,发布即承诺,命名预留空间。
  9. M2M:客户端凭证模式下 scope 受注册上限约束。
  10. 多 API:用 Resource Indicators 做受众绑定,防止令牌跨 API 滥用。

八、总结

Scope 的设计看似只是定义几个字符串,实则是在为整个平台的授权语义第三方生态奠定基础。三个最值得记住的结论:

  1. Scope 是授权的上界,不是授权本身------它必须与 RBAC/ABAC 取交集才构成完整决策。
  2. 粒度按业务能力划分 ------既非粗到 read/write,也非细到字段级,而是用户能理解的「能力」单位。
  3. 请求 ≠ 授予------用户和授权服务器都可能裁剪 scope,客户端必须以令牌响应为准。

设计良好的 scope 体系,应当让用户在同意页一眼看懂自己授予了什么,让第三方能精确申请所需的最小权限,让资源服务器能高效校验,并能随业务平滑演进。

相关推荐
2501_916008891 小时前
全面解析常用Web前端开发工具:编辑器、调试工具、性能分析器与框架
android·前端·ios·小程序·uni-app·编辑器·iphone
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 08 | Agent 的自我进化:nudge、后台审查与轨迹数据
java·前端·人工智能
IT_陈寒1 小时前
Redis集群节点迁移把我坑惨了,这个坑你得提前绕开
前端·人工智能·后端
新酱爱学习2 小时前
手搓 10 个 Skill 踩出来的坑,我做成了一套工程化工具链
前端·人工智能·agent
怕浪猫2 小时前
Electron 开发实战(八):多媒体处理全解|音视频播放、录屏、FFmpeg 实战
前端·javascript·electron
恋猫de小郭2 小时前
一个 Linux 调度器优化,让 Android 多耗 20% 的电,传音工程师如何发现问题?
android·前端·ios
kyriewen112 小时前
开源|Image Harvest v1.0.5:AI 智能标签 + Eagle 导出,设计师和开发者的图片工作流神器
前端·javascript·人工智能
步十人2 小时前
【Vue】认识单文件组件与模板语法
前端·javascript·vue.js
AIFQuant2 小时前
贵金属投资 APP 开发:实时报价、图表、提醒与交易数据全链路
开发语言·前端·websocket·金融·web app