普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月28日首页

上海市人工智能行业协会:AI发展不能因噎废食 安全和风控要协同推进

2026年3月28日 15:17
全球开发者先锋大会3月27日至3月29日在沪举行。大会期间,上海市人工智能行业协会秘书长钟俊浩在接受证券时报记者采访时表示,“养龙虾”热潮,表明AI正实现从生产工具到生产力的进阶。当前AI智能体技术仍在快速迭代,行业处于“在前行中探索,在探索中前行”的阶段。对待AI安全和规范议题,既不能因噎废食,也不能忽视风险,需依托既有网络安全管理体系,结合大模型内生安全建设,逐步规范行业发展。(证券时报)

申通快递与宁德时代签署战略合作协议

2026年3月28日 14:46
据申通快递消息,3月26日,申通快递与宁德时代在福建宁德时代总部正式签署战略合作协议,双方将围绕党建共建、绿色物流、车辆电动化、充换电设施、电池全生命周期管理等领域展开深度协同。

科达液压与科上、汇川签约 开启废钢加工设备电液伺服系统领域合作

2026年3月28日 14:21
据科达制造消息,3月26日,安徽科达液压技术有限公司与广东科上液压科技有限责任公司、苏州汇川技术有限公司举行战略合作签约仪式,三方携手开启废钢加工设备电液伺服系统领域合作。 根据合作协议,三方将进行专业化分工协作,一方聚焦废钢加工设备配套应用,将合作电液伺服泵组作为首选配置;一方提供“柱塞泵+伺服电机/驱动器”一体化电液伺服泵组集成方案;一方则供应伺服电机、驱动器及相关控制系统,并保障产品品质与售后服务,形成从核心部件供应、系统集成到设备应用的完整合作链条。(人民财讯)

讯飞医疗2025年营收同比增长25%

2026年3月28日 14:15
3月27日,讯飞医疗(02506.HK)发布2025年度业绩报告。报告期内,公司实现营业收入9.15亿元,同比增长25%;年内亏损6577万元,同比亏损大幅收窄。在3月27日晚的业绩说明会上,董事长刘庆峰表示将致力于推动医院智能化转型。

Meta内容政策主管比克特离职,将赴哈佛大学任教

2026年3月28日 13:29
Meta 长期担任内容政策主管的莫妮卡・比克特即将离职,前往哈佛法学院任职。她曾负责 Facebook 内容政策的制定与执行,并参与公司用户安全相关事务的决策。据路透社周五看到的一封内部公告,比克特表示自己一直有意从事教学工作。她将在 Meta 工作至今年 8 月,并与负责全球政策团队的凯文・马丁共同推进交接工作。作为内容政策负责人,在 Meta 因政治内容处理、青少年心理健康等问题引发争议期间,比克特经常作为公司对外发言人出面回应。(新浪财经)

31省份中央育儿补贴补助资金揭晓:广东河南四川居前三

2026年3月28日 13:01
近日,财政部披露2026年中央财政预算,其中《部分转移支付项目分地区情况表》(下称《情况表》)中首次披露去年中央对31个省份育儿补贴补助资金规模,以及今年预算数。 根据上述《情况表》,2025年中央财政育儿补贴补助资金总规模为903.58亿元,其中广东获得90.58亿元,位居各省之首。河南拿到72.81亿元,紧随其后,四川分得68.4亿元,排在第三。山东获得55.69亿元,位列第四。 广西、贵州、河北、湖南、云南拿到的中央育儿补贴补助资金规模在40亿元~50亿元之间,安徽、江苏、江西、湖北获得的这一资金规模在30亿元至40亿元之间,其余地方获得这一资金规模低于30亿元。(第一财经)

vivo秦飞:手机厂商早就能做智能体 但安全比速度更重要

2026年3月28日 12:36
3月27日,在中关村平行论坛“6G技术与产业创新论坛”上,vivo通信研究院院长秦飞表示,当前推动6G发展的关键,不是单纯追求节奏更快,而是先把未来真实需求进一步厘清。此外,随着智能体加速进入终端,手机将成为AI能力落地的重要入口。(新浪财经)

Tauri 2 Linux 上 asset://localhost 访问返回 403 避坑指南

作者 ssshooter
2026年3月28日 12:27

很多人在 Tauri v2(尤其是 Linux 系统)中使用 convertFileSrc()asset://localhost 协议加载本地图片、视频、音频等资源时,经常遇到 403 Forbidden 错误。Windows/macOS 可能正常,Linux 却直接翻车。

本文把整个坑的来龙去脉、根本原因、glob 匹配规则彻底讲清楚,并给出最稳的配置方案,帮助大家一次性避坑。

一、问题现象

  • 使用 convertFileSrc(fullPath) 生成的 URL 在 <img><video><audio> 等标签中加载失败
  • 浏览器控制台报 403
  • 终端(Rust 侧)日志提示类似:
    asset protocol not configured to allow the path: /home/user/.local/share/xxx/xxx.png
    
  • 尤其容易出现在 隐藏目录(以 . 开头的目录)下:.local/share.cache.config

二、根本原因:Tauri 的 Glob Scope + Linux 隐藏目录规则

Tauri v2 的 assetProtocol.scope 使用的是 Rust globset 库实现的 glob 模式来做安全校验。只有路径匹配 scope 里的 glob,才允许浏览器通过 asset 协议访问。

最坑的一点在于,Linux(Unix-like 系统)下:

通配符 *?**默认不会匹配以 . 开头的路径(dotfiles / dotdirs),除非你在 glob 模式里字面写出 .

所以即使你写了最宽松的 "**/*",它也进不了 .local.cache 等隐藏目录,导致 403。

这不是 bug,而是 Tauri 为了安全故意设计的(和 Linux shell 的 ls * 默认不显示隐藏文件一样)。

三、Glob 模式最容易搞混的两个写法:**/ vs **/*

glob 写法 含义 能匹配什么 在 assetProtocol.scope 里的实际效果 推荐程度
**/* 递归匹配所有文件 文件(如 a.pngsub/b.mp4 ✅ 强烈推荐 ★★★★★
**/ 递归匹配所有目录 纯目录路径(如 images/sub/ ❌ 几乎没用(scope 要的是文件路径) ★☆☆☆☆

一句话总结

  • **/* = “递归所有文件”(你 99% 的情况都需要这个)
  • **/ = “递归所有目录”(基本不要单独写在 scope 里)

正确写法是 你的路径/**/* 或直接 **/*

四、正确配置(一步到位)

1. 主配置(推荐同时加 Linux 专属配置)

src-tauri/tauri.conf.json(全局):

{
  "app": {
    "security": {
      "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost; video-src 'self' asset: http://asset.localhost; audio-src 'self' asset: http://asset.localhost; style-src 'self' 'unsafe-inline';",
      "assetProtocol": {
        "enable": true,
        "scope": [
          "**/*",
          "**/.local/share/**/*",
          "**/.cache/**/*",
          "$CACHE/**",
          "$CONFIG/**",
          "$HOME/**"
        ]
      }
    }
  }
}

src-tauri/tauri.linux.conf.json(Linux 专属,强烈建议):

{
  "app": {
    "security": {
      "assetProtocol": {
        "enable": true,
        "scope": [
          "**/*",
          "**/.local/share/**/*",
          "**/.cache/**/*",
          "$CACHE/**",
          "$CONFIG/**"
        ]
      }
    }
  }
}

这样 Windows/macOS 不会被多余的 scope 影响。

2. 代码侧使用(不变)

import { convertFileSrc } from '@tauri-apps/api/core';

const assetUrl = convertFileSrc(absoluteFilePath);

五、操作流程

  1. 按上面修改配置文件
  2. (推荐)cargo clean
  3. pnpm tauri dev(或 npm run tauri dev)测试
  4. 还是 403?看终端日志,把报错里提示的路径对应的 glob 补进去

六、额外避坑小贴士

  • 用 Tauri 内置变量 $CACHE$CONFIG 最香,自动处理平台差异
  • 如果是用户通过 dialog.open() 选择的路径,Tauri 会自动扩展 scope,但持久化路径仍需写进配置
  • 打包进 bundle 的资源不需要 assetProtocol,走 frontendDist 即可
  • Rust 版本建议 ≥ 1.77,Tauri CLI 保持最新

总结
Tauri 2 的 asset 403 坑,99% 是因为 Linux 下 glob 默认不匹配 . 开头的隐藏目录。只要把 **/* + **/.local/share/**/* + $CACHE/** 写全,问题基本秒解。

把这篇配置直接复制到你的项目里,基本不会再踩这个坑了。

希望这篇文章能帮到更多 Tauri 开发者少走弯路!
如果你还有其他 Tauri v2 的奇葩问题,欢迎继续留言~

三大运营商算力收入提升 Token服务逐渐成为经营主线

2026年3月28日 12:13
近日,三大电信运营商2025年年报陆续出炉。总的来看,运营商主要业绩指标高位运行,但就总量而言,增长乏力的状况并没有显著改善,算力服务、创新业务、国际业务等结构性收入增长仍是主要亮点。2025年,三大运营商基础资源投入重点继续向算力方向倾斜。记者注意到,根据公布的资本开支计划,算力相关投入仍是运营商今年的重点方向。当然,智算基础设施的落脚点还要围绕如何变现。从三家运营商的表态来看,Token(词元)经营将逐渐成为主线。(人民财讯)

React 性能优化(下):useCallback 与 useTransition 实战

作者 Csvn
2026年3月28日 12:00

引言

在 React 应用性能优化中,useCallbackuseTransition 是两个强大但常被误解的 Hook。本文将深入探讨它们的正确使用场景、常见陷阱以及实际代码示例,帮助你写出更高效的 React 应用。

useCallback:避免不必要的函数重建

核心原理

useCallback 返回一个记忆化的回调函数,只有在依赖项变化时才会重新创建。这对于避免子组件不必要的重新渲染至关重要。

基础用法

import React, { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ 错误:每次渲染都会创建新函数
  const handleClick = () => {
    console.log('Count:', count);
  };

  // ✅ 正确:使用 useCallback 记忆化
  const handleMemoizedClick = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <div>
      <button onClick={handleMemoizedClick}>点击</button>
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

配合 React.memo 使用

import React, { useState, useCallback, memo } from 'react';

const ChildComponent = memo(({ onIncrement, value }) => {
  console.log('Child rendered');
  return (
    <button onClick={onIncrement}>
      Count: {value}
    </button>
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ✅ 只有 count 变化时,onIncrement 才会变化
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <ChildComponent onIncrement={handleIncrement} value={count} />
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

常见陷阱

// ❌ 陷阱 1:依赖项过多导致失去优化效果
const handler = useCallback(() => {
  doSomething(a, b, c, d, e);
}, [a, b, c, d, e]); // 几乎每次都会重新创建

// ✅ 解决:只依赖真正需要的变量
const handler = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// ❌ 陷阱 2:在 useCallback 内部使用非稳定引用
const handler = useCallback(() => {
  config.doSomething(); // config 每次都是新对象
}, [config]);

// ✅ 解决:依赖稳定的值
const handler = useCallback(() => {
  configRef.current.doSomething();
}, []);

useTransition:优化耗时更新

核心概念

useTransition 允许你将某些状态更新标记为"过渡"更新,让 UI 保持响应式,避免阻塞用户交互。

基础用法

import React, { useState, useTransition } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value) => {
    setQuery(value);
    
    // ✅ 将耗时的过滤操作标记为过渡更新
    startTransition(() => {
      const filtered = heavyFilter(value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input 
        value={query}
        onChange={e => handleSearch(e.target.value)}
        placeholder="搜索..."
      />
      {isPending && <span>加载中...</span>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function heavyFilter(query) {
  // 模拟耗时操作
  const data = largeDataset.filter(item => 
    item.name.includes(query)
  );
  return data;
}

实际场景:标签切换

import React, { useState, useTransition } from 'react';

function TabComponent() {
  const [activeTab, setActiveTab] = useState('posts');
  const [isPending, startTransition] = useTransition();

  const tabs = {
    posts: <PostsList />,
    comments: <CommentsList />,
    analytics: <AnalyticsPanel />
  };

  const handleTabChange = (tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        {Object.keys(tabs).map(tab => (
          <button
            key={tab}
            onClick={() => handleTabChange(tab)}
            disabled={isPending}
          >
            {tab}
          </button>
        ))}
      </nav>
      {isPending && <div className="spinner">切换中...</div>}
      <main>
        {tabs[activeTab]}
      </main>
    </div>
  );
}

与 Suspense 配合

import React, { useState, useTransition, Suspense } from 'react';

function Dashboard() {
  const [activeView, setActiveView] = useState('overview');
  const [isPending, startTransition] = useTransition();

  const handleViewChange = (view) => {
    startTransition(() => {
      setActiveView(view);
    });
  };

  return (
    <div>
      <TabNav onChange={handleViewChange} active={activeView} />
      <Suspense fallback={<LoadingSkeleton />}>
        <ViewContent view={activeView} />
      </Suspense>
    </div>
  );
}

性能对比实测

// 未优化的版本
function UnoptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  
  // 每次输入都会重新渲染整个列表
  const filtered = items.filter(item => 
    item.name.includes(filter)
  );

  return (
    <div>
      <input onChange={e => setFilter(e.target.value)} />
      <List data={filtered} />
    </div>
  );
}

// 优化后的版本
function OptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  const [displayItems, setDisplayItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setFilter(value);
    
    startTransition(() => {
      const filtered = items.filter(item => 
        item.name.includes(value)
      );
      setDisplayItems(filtered);
    });
  };

  return (
    <div>
      <input value={filter} onChange={handleChange} />
      {isPending && <LoadingIndicator />}
      <List data={displayItems} />
    </div>
  );
}

最佳实践总结

useCallback 使用指南

  1. 优先优化子组件:只有当函数作为 prop 传递给 React.memo 组件时才需要
  2. 避免过早优化:不是所有函数都需要 useCallback
  3. 注意依赖项:确保依赖项稳定且必要
  4. 配合 useMemo:复杂计算场景结合使用

useTransition 使用指南

  1. 识别耗时更新:列表过滤、大数据渲染、复杂计算
  2. 提供加载反馈:使用 isPending 显示加载状态
  3. 区分优先级:用户输入立即响应,数据更新可延迟
  4. 避免滥用:简单更新不需要过渡

总结

useCallbackuseTransition 是 React 性能优化的利器,但需要正确使用:

  • useCallback 解决的是函数引用稳定性问题
  • useTransition 解决的是更新优先级问题

记住:性能优化应该基于实际测量,而非猜测。使用 React DevTools Profiler 找出真正的瓶颈,再针对性地应用这些 Hook。

这 5 个 Elements 小技巧,真的能提高调试效率

作者 isArray
2026年3月28日 11:56

大家好,我是小贺。

如果你是前端,浏览器开发者工具大概已经是每天都要打交道的老朋友了。

但问题也恰恰在这里。我相信很多人会打开 F12,却只会最基础的查看元素、改两行样式、看看报错。真正能让调试变轻松的功能,反而常常被忽略。

这篇不讲太多原理,直接分享 5 个我觉得非常实用的 Elements 面板小技巧。它们不复杂,但一旦用顺手,确实能帮你少走不少弯路。


1. 选中即消失?“强制状态”拯救你

你一定遇到过这种情况:想调试一个鼠标悬浮(hover)后才出现的二级菜单,结果鼠标刚一移过去,菜单就没了,根本来不及查看。

这时候不用和手速较劲。

Elements 面板里,右键点击触发菜单的元素,选择 Force state -> :hover

这样元素就会被强制锁定在悬浮状态。除了 :hover,常见的 :active:focus 这些状态也都能手动模拟。

很多“选中就消失”的交互问题,本质上都可以靠这个功能解决。

强制状态.gif


2. 只有高手才知道的“H 键大法”

有时候你只是想临时看一眼页面效果,比如:

  • 这个 Banner 去掉之后页面会不会更干净?
  • 这个弹窗先隐藏,下面的布局会不会乱?
  • 这个模块不显示时,留白是不是太大?

这时其实不用手动改 display: none

直接在 Elements 面板里选中元素,按一下键盘上的 H

元素会立刻隐藏,再按一次又能恢复。

如果你想进一步测试,可以直接按 Delete 把节点删掉;删错了也不用慌,Ctrl + Z 在开发者工具里同样可以撤回。

这个功能特别适合快速试布局,不用来回改代码、刷新页面。

H 键大法.gif


3. $0:Elements 与控制台的“传送门”

假设你已经在 Elements 面板里选中了一个很深层的 div,这时候想在 Console 里看一下它的 offsetHeight,或者直接调用它的方法。

如果还要自己手写一遍 document.querySelector(...),就太费劲了。

Esc 呼出底部控制台,直接输入 $0

$0 就是你当前在 Elements 面板里选中的那个元素。

比如你可以直接这样用:

$0.offsetHeight;
$0.classList;
$0.getBoundingClientRect();

更方便的是,浏览器还会记住你的选择历史:$1 是上一次选中的元素,$2 是上上次。

调试复杂页面时,这个功能真的非常顺手。

$0.gif


4. 截图不用描边,自带“像素级”捕获

有时候你想把某个按钮、卡片、弹窗单独截出来发给设计师,或者临时拿去做文档、做演示。

这时候最省事的方法,不是手动截图框选,而是直接让浏览器截这个节点。

操作步骤很简单:

  1. Elements 面板里选中目标元素
  2. Ctrl + Shift + P 打开命令面板
  3. 输入 screenshot截图
  4. 选择 Capture node screenshot

这样导出的通常是一个干净的 PNG,元素边缘也会比手动框选更完整。对前端、设计协作、写文档的人来说,这个功能都很好用。

截图.gif


彩蛋:一键开启“整页可编辑”模式

有时候你只是想临时改改页面上的文字,看看排版效果,又不想在 Elements 里逐个节点去双击编辑。

在 Console 里输入:

document.designMode = 'on';

执行之后,整个页面会进入一种近似“可编辑文档”的状态。很多文字内容都可以直接点进去修改、删除、试排版。

它当然不能代替真正改代码,但在演示想法、快速验证文案展示效果时,非常方便。

编辑模式.gif


5. 元素找不到了?试试“滚动到视图”

页面一长,DOM 结构一复杂,经常会出现一种情况:

你在 Elements 面板里明明看到了某个节点,但一时找不到它到底对应页面上的哪一块。

这时可以直接在对应的 HTML 标签上右键,选择 Scroll into view滚动到视图)。

浏览器会自动把页面滚动到这个元素所在的位置。

这个小功能看起来不显眼,但在排查长页面、复杂列表、深层组件时,能省下很多来回找位置的时间。

滚动到视图.gif


写在最后

开发者工具里真正有用的,很多都不是“高级功能”,而是这些平时不太起眼、但一旦掌握就会频繁用到的小能力。

它们不会让你一夜之间变成高手,但会让你少做很多重复劳动。

如果你现在就在电脑前,不妨打开浏览器按一次 F12,先试试 $0H 键,基本马上就能体会到差别。

这里是《你真的会使用浏览器么?》系列的第一篇。如果大家喜欢我们后续继续更新。

ps:文章演示网站为-(小贺的博客)

国内多家品牌金饰克价逼近1400元

2026年3月28日 11:31
3月28日,国内黄金饰品价格对比显示,多家黄金珠宝品牌公布的境内足金首饰价格较昨日有所上调,老凤祥报价1396元/克,较前一日1368元/克上涨28元;老庙黄金报价1397元/克,较前一日1365元/克上涨32元;周生生报价1391元/克,较前一日1364元/克上涨27元。

做了一个AI聊天应用后,我决定试试这个状态管理库

作者 张一凡93
2026年3月28日 11:29

AI应用前端最大的坑,不是LLM调用,而是状态管理

背景

最近做了个AI聊天应用,类似ChatGPT的那种。

本来想用Redux,毕竟老牌方案,结果被毒打了一遍。

Redux的痛

痛点1:状态类型爆炸

// AI聊天应用需要管理的状态
interface ChatState {
  // 对话
  messages: Message[];

  // 流式响应
  streamingText: string;
  isStreaming: boolean;

  // 工具调用
  toolCalls: ToolCall[];
  currentToolCall: ToolCall | null;
  toolResults: Record<string, any>;

  // 上下文
  contextWindow: Message[];
  longTermMemory: MemoryItem[];

  // 用户意图
  currentIntent: Intent | null;
  intentHistory: Intent[];

  // 执行状态
  currentStep: number;
  stepResults: Record<number, any>;

  // 错误处理
  errors: Error[];
  retryQueue: RetryItem[];

  // ...
}

这还只是一个聊天模块的状态。

痛点2:action type能写死你

// 光是状态更新action就能写30个
const ADD_MESSAGE = "chat/ADD_MESSAGE";
const UPDATE_STREAMING = "chat/UPDATE_STREAMING";
const APPEND_STREAMING = "chat/APPEND_STREAMING";
const START_TOOL_CALL = "chat/START_TOOL_CALL";
const COMPLETE_TOOL_CALL = "chat/COMPLETE_TOOL_CALL";
const UPDATE_CONTEXT = "chat/UPDATE_CONTEXT";
const ADD_INTENT = "chat/ADD_INTENT";
// ... 还有几十个

痛点3:跨组件同步难

// Chat组件
const messages = useSelector((s) => s.chat.messages);

// Status组件
const toolCalls = useSelector((s) => s.chat.toolCalls);

// 怎么保证两个组件状态一致?靠redux-thunk?middleware?

然后我用了easy-model

// 一个类搞定AI聊天全状态
class AIChatModel {
  // 对话
  messages: Message[] = [];

  // 流式响应
  streamingText = "";
  isStreaming = false;

  // 工具调用
  toolCalls: ToolCall[] = [];
  currentToolCall: ToolCall | null = null;
  toolResults: Map<string, any> = new Map();

  // 上下文
  contextWindow: Message[] = [];
  longTermMemory: MemoryItem[] = [];

  // 用户意图
  currentIntent: Intent | null = null;
  intentHistory: Intent[] = [];

  // 执行
  currentStep = 0;
  stepResults: Map<number, any> = new Map();

  // 错误
  errors: Error[] = [];

  // 方法
  @loader.load()
  async sendMessage(content: string) {
    this.messages.push({ role: "user", content });
    this.isStreaming = true;

    const response = await llm.streamChat(this.messages);

    for await (const chunk of response) {
      this.streamingText += chunk;
    }

    this.messages.push({ role: "assistant", content: this.streamingText });
    this.streamingText = "";
    this.isStreaming = false;
  }

  async executeToolCall(tool: Tool, params: any) {
    this.currentToolCall = { tool, params, status: "running" };
    this.toolCalls.push(this.currentToolCall);

    const result = await tool.execute(params);

    this.currentToolCall.status = "completed";
    this.currentToolCall.result = result;
    this.toolResults.set(tool.name, result);
    this.currentToolCall = null;
  }
}

一个类,200行代码搞定Redux 500行都搞不定的事。

还能解决什么问题?

1. 撤销重做,调试AI回复

const chat = useModel(AIChatModel, []);
const history = useModelHistory(chat);

// 用户想撤回AI的上一次回复?
history.back();

// 想重做?
history.forward();

2. 跨组件状态共享

// 聊天区域
function ChatArea() {
  const chat = useModel(AIChatModel, ["main"]);
  return <MessageList messages={chat.messages} />;
}

// 状态显示
function StatusPanel() {
  const chat = useModel(AIChatModel, ["main"]);
  return <StatusBadge isStreaming={chat.isStreaming} />;
}

// 工具调用面板
function ToolPanel() {
  const chat = useModel(AIChatModel, ["main"]);
  return <ToolList calls={chat.toolCalls} />;
}

// 三个组件,状态自动同步

3. 深度监听

// 监听任意状态变化
watch(chat, (keys, prev, next) => {
  console.log(`状态变化: ${keys.join(".")}`, prev, "→", next);

  // "messages.5.content" - 第6条消息内容变了
  // "toolCalls.0.status" - 第一个工具调用状态变了
  // "streamingText" - 流式文本更新了
});

对比其他方案

特性 Redux Zustand MobX easy-model
类模型
无装饰器
依赖注入
撤销重做
深度监听 ⚠️
TypeScript友好 ⚠️ ⚠️

结论

AI应用前端,状态管理选easy-model就对了。


GitHub: github.com/ZYF93/easy-…

npm: pnpm add @e7w/easy-model

做AI应用前端,状态管理别再踩坑了,点个⭐️!

详解github workflows流

作者 怪兽同学
2026年3月28日 11:26

Workflows允许你在 GitHub(文章以此为例) 仓库中自动化构建、测试、部署等软件开发流程

1. 什么是Workflows

  • GitHub Actions 是 GitHub 提供的 CI/CD(持续集成/持续部署)平台,可以自动化执行软件开发工作流。基本上代码管理都支持actions,百用不赖
  • Workflow 是 Actions 中的一个可配置的自动化过程,由一个或多个 job 组成,由特定事件触发。每个 workflow 是一个独立的 YAML 文件,存放在仓库的 .github/workflows 目录下。

2. 基本概念

概念 说明
Workflow 一个自动化流程,由一个 YAML 文件定义。
Event 触发 workflow 运行的特定活动,如 pushpull_requestschedule 等。
Job workflow 中的一个任务,由多个 step 组成。一个 workflow 可以包含多个 job,它们可以并行或串行运行。
Step job 中的单个任务,可以是 shell 命令或一个 action
Action 可复用的最小单元,可以封装常用操作(如检出代码、设置 Node.js 环境等)。
Runner 运行 workflow 的服务器。GitHub 提供托管运行器(如 ubuntu-latestwindows-latest),也可自托管。

3. 简单示例

在仓库根目录下创建 .github/workflows 目录,然后放入一个或多个 .yml 或 .yaml 文件 实例如下

name: CI  # 名称

on:  # 触发条件
  push:
    branches: [ main ]

jobs: # 定义作业
  build:
    runs-on: ubuntu-latest  # 运行环境
    steps:
      - name: Checkout code
        uses: actions/checkout@v4  # 使用官方 checkout action
      - name: Run a one-line script
        run: echo Hello, world!

4. 触发事件 (on)

on 字段定义 workflow 何时运行。可以是单个事件、事件列表,或带有条件的事件。

4.1 基本事件

  • push 代码推送时触发
  • pull_request PR 相关事件
  • workflow_dispatch 手动触发(需要在 GitHub 界面点击按钮)
  • schedule 定时触发(cron)
  • 其他事件:如 releaseissueswatchfork见 👉🏻 详细文档地址

下面是详细的事件类型,贯穿仓库管理中的各个过程和节点。


image.png

4.2 多事件与条件过滤

可以组合多个事件,并利用 branchespathstags 等过滤器。

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
    paths-ignore:
      - 'docs/**'

5. Jobs 与 Steps

Jobs: 定义一个任务

Steps: 每个 step 可以是一个 run(执行 shell 命令)或 uses(引用一个 action)。

jobs:
  job1: 
    runs-on: ubuntu-latest # 指定运行器镜像。常用值:`ubuntu-latest`, `windows-latest`, `macos-latest`,也可以指定版本如 `ubuntu-22.04`。
    steps:
      - name: Step 1 # 可选,显示在日志中,推荐使用。
        if: github.ref == 'refs/heads/main'  # 使用 `if` 条件,支持表达式。
        run: echo "Step 1"
        with:
            node-version: '22'
  job2:
    runs-on: windows-latest
    needs: job1   # job2 在 job1 完成后运行;(定义依赖关系,默认所有 job 并行运行,加 `needs` 后变为串行)
    steps:
      - name: Step 2
        run: echo "Step 2"

6. Actions 的使用

Actions 是可复用的代码单元,可以从 GitHub Marketplace 获取或自定义。

6.1 使用官方或第三方 Action

格式:{owner}/{repo}@{ref} 或 {owner}/{repo}/{path}@{ref}

- name: Upload artifact
  uses: actions/upload-artifact@v4
  with:
    name: my-artifact
    path: dist/

6.2 自定义 Action

可以创建 Docker 容器 Action 或 JavaScript Action,放在仓库中本地引用:

- name: My custom action
  uses: ./.github/actions/my-action

7. 环境变量与 Secrets

7.1 环境变量

可以在 workflow、job 或 step 级别设置环境变量。

env:
  NODE_ENV: production

jobs:
  build:
    env:
      API_URL: https://api.example.com
    steps:
      - name: Use env
        run: echo ${{ env.API_URL }}

7.2 Secrets

敏感信息如密码、token 应存储在 GitHub 仓库的 Settings → Secrets and variables → Actions 中,然后在 workflow 中通过 ${{ secrets.SECRET_NAME }} 引用。

- name: Deploy
  run: npm run deploy -- --token ${{ secrets.DEPLOY_TOKEN }}

注意:Secrets 不会出现在日志中,但需小心避免将 secret 作为命令行参数输出(如 echo ${{ secrets.TOKEN }} 会暴露)。

8. 矩阵策略 (Matrix)

矩阵允许你使用变量组合并行运行多个 job,常用于测试多个操作系统、Node.js 版本等。

yaml

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16, 18, 20]
      fail-fast: false  # 是否在某个组合失败时取消所有
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
  • matrix:定义变量组合。
  • include/exclude:可以添加或排除特定组合。

9. 缓存依赖

通过 actions/cache 可以缓存依赖,加速构建。

- name: Cache npm
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

更常见的是使用针对特定生态的缓存 action,如 actions/setup-node 已内置缓存:

- uses: actions/setup-node@v4
  with:
    node-version: '18'
    cache: 'npm'

10. 工件 (Artifacts) 与部署

10.1 上传/下载工件

可以在 job 间传递文件。

- name: Upload build
  uses: actions/upload-artifact@v4
  with:
    name: build
    path: dist/

- name: Download build
  uses: actions/download-artifact@v4
  with:
    name: build
    path: dist/

10.2 部署到 GitHub Pages

- name: Deploy to GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./dist

GITHUB_TOKEN 是 GitHub 自动提供的临时 token,具有仓库写入权限。

11. 条件判断与上下文

11.1 条件执行

使用 if 条件,支持表达式。

- name: Only on main branch
  if: github.ref == 'refs/heads/main'
  run: echo "Deploying..."

11.2 上下文

GitHub Actions 提供丰富的上下文变量,如 githubenvsecretsmatrix 等。

- name: Print event name
  run: echo ${{ github.event_name }}

常用上下文:

  • github:包含仓库信息、触发者等。
  • runner:运行器信息。
  • env:环境变量。
  • secrets:仓库机密。
  • steps:上一步的输出。

12. 工作流嵌套与复用

12.1 调用其他 workflow

使用 workflow_call 事件,可以在一个 workflow 中调用另一个 workflow。

被调用的 workflow 需定义 on: workflow_call,并可定义输入和输出。

# .github/workflows/build.yml
name: Build
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      npm_token:
        required: true
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm run build

调用方:

name: Main Workflow
on: push
jobs:
  call-build:
    uses: ./.github/workflows/build.yml
    with:
      node-version: '18'
    secrets:
      npm_token: ${{ secrets.NPM_TOKEN }}

13. 运行器组与自托管运行器

如果不想使用 GitHub 托管运行器,可以添加自托管运行器(Self-hosted runner)。

jobs:
  build:
    runs-on: self-hosted

可以为自托管运行器指定标签,如 runs-on: [self-hosted, linux]

14. 调试与日志

  • 启用 step 调试:在 workflow 中设置 ACTIONS_STEP_DEBUG secret 为 true,会输出更详细的日志。
  • 启用 runner 调试:设置 ACTIONS_RUNNER_DEBUG secret 为 true,会输出 runner 级别的调试信息。
  • 手动重试:在 GitHub Actions 页面,可以重新运行失败的 job。

15. 安全最佳实践

  • 最小权限原则:使用 permissions 字段限制 workflow 的 token 权限。

    permissions:
      contents: read
      issues: write
    
  • 避免在日志中打印 secrets

  • 使用环境 (Environment) :对部署等敏感操作,可配置环境并设置保护规则。

    environment: production
    
  • 审查第三方 Action:优先使用官方或经过验证的 Action,固定版本 tag 或 commit SHA。

参考资料:

❌
❌