Vibecoding 一个主题切换动画库:13 种揭幕方式

简介: 这是一个基于 View Transitions API 的主题切换动画库,支持 React/Vue/Next/Nuxt,内置 13 种扩散式动画(如圆形、方形、四向擦除等),强调体验流畅与状态可靠:蒙版几何精准计算、半像素抖动修复、受控模式无缝集成、不支持时自动降级。动画是增强,状态永远正确。

几乎每个站点都有明暗主题切换。点击按钮,页面瞬间从白变黑——功能上是完成了,但体验上像被硬生生拽了一下。

有一天我在想:如果新主题不是「替换」旧主题,而是从你点击的位置「长」出来、扩散着把旧主题揭开,会不会好很多?

于是有了 theme-switch-animation:一个基于 View Transitions API 的主题切换动画库,支持 React 18+、Vue 3+、Next.js App Router 与 Nuxt 3+,内置 13 种动画类型,可受控也可非受控,浏览器不支持时自动降级。

这个项目是我用 vibecoding 的方式做出来的:需求、技术选型、行为契约和验收标准由我定,代码主要由 AI 写。但下面这几个坑——半像素抖动、受控模式的 DOM 同步、Nuxt 的自动导入——都是靠真机实测和逐像素对比才收敛的,AI 的第一版同样会翻车。

这篇文章把它拆开讲讲:为什么只能用 mask、蒙版要算多大、13 种类型怎么共用一套代码、以及两个真正花了时间的坑

13 种类型都是什么效果

先看结果。下面每一格都是同一次切换进行到约 60% 时的画面:亮色是已经揭开的新主题,暗色是尚未被覆盖的旧主题,紫色标记处就是触发按钮的中心。

02-animation-types-overview.png

按实现归类其实只有三族:

类型 蒙版形制
中心扩散形状族 CIRCLE SQUARE DIAMOND RECTANGLE HEXAGON TRIANGLE STAR SVG data-URI 形状,从触发点 0 尺寸长到覆盖视口
四向擦除 LTR RTL TTB BTT linear-gradient 实心细条,尺寸从 4px 长到 100%
特殊 CIRCLE_REVERT(方向感知的圆)、CIRCLE_BLUR(边缘高斯模糊的圆) 反向蒙版「洞」/ 模糊烘焙进 SVG
pnpm add theme-switch-animation

浏览器已经帮我们拍了三张图

这类效果看起来像「同一个页面在变形」,但实现上并不是真的在变形——View Transitions API 的做法朴素得多:把变化前后的页面各拍一张静态快照,然后在两层快照之间演动画

01-view-transition-lifecycle.png

对应到代码,一次切换的编排大约就是这样:

export function runThemeTransition(params: RunThemeTransitionParams): RunThemeTransitionResult {
   
  const doc = resolveDocument(params.doc)
  const resolved = resolveAnimationOptions(params.options)

  // 降级路径:不支持 View Transitions 或用户关了动效,直接改状态,没有动画
  if (!supportsViewTransition(doc) || prefersReducedMotion(doc.defaultView)) {
   
    return {
    animated: false, finished: Promise.resolve(domUpdate()).then(() => undefined) }
  }

  const viewport = getViewportSize(doc)
  const center = getTriggerCenter(trigger, viewport)
  const geometry = getMaskGeometry(resolved.animationType, center, viewport, resolved.blurAmount)
  const css = buildAnimationCSS({
    animationType: resolved.animationType, geometry, ...resolved })
  const node = injectAnimationStyle(doc, css)   // 向 <head> 注入一份临时样式

  const transition = doc.startViewTransition(wrapped)  // wrapped 内部调用 domUpdate()
  const finished = transition.finished.then(
    () => scheduleCleanup(doc, resolved.duration, node),
    (error) => {
    /* 只清自己注入的节点,AbortError 视为正常跳过 */ },
  )
  return {
    animated: true, finished }
}

三个细节值得单独说:

第一,降级路径也调用 domUpdate 不支持 View Transitions 的浏览器、prefers-reduced-motion: reduce、SSR 阶段——这三种情况都会跳过动画,但状态照常翻转。这是我给自己的硬约束:动画是增强,状态正确是底线,任何环境下 isDark<html> 的类名都不能不一致。

第二,样式要自己收。 注进去的 <style> 不会自己消失,转场结束后延迟 duration 再移除,并且按 document 记账、用节点引用判定身份——否则多文档(iframe)场景下,B 文档的转场会把 A 文档的清理定时器取消掉,A 的转场样式就永久残留了。

第三,快速连点不制造噪音。 连点时浏览器会中止未完成的转场,finishedAbortError 结算。这类 rejection 在库内被消化掉,消费方拿到的 finished 不会变成全局 unhandledrejection

为什么只能用 mask

这是整套技术路线的起点,也是从参考实现里踩出来的结论:WebKit 会忽略 view-transition 伪元素上的 clip-path 和 WAAPI

也就是说,那些「用 clip-path 画个圆做动画」的写法在 Safari 上会直接失效。能稳定跨浏览器工作的只剩 mask——而且因为 view-transition 伪元素是浏览器生成的,我们还必须先把它的默认行为关掉:

/* UA 默认让两层交叉淡入淡出,混合模式还是 plus-lighter */
::view-transition-old(root),
::view-transition-new(root) {
   
  animation: none;
  mix-blend-mode: normal;
}

plus-lighter 是为「两层同时淡出淡入」设计的加色混合。旧层静止不动时,两张图会被相加,屏幕直接发白。所以这两行不是优化,是必须。

另外还有一条同样重要的原则:蒙版只挂新层,旧层完整垫底。蒙版之外露出的必须是旧主题;如果两层挂同一份蒙版,透明区露出的就是已经翻转的实时页面,主题会在动画开始的瞬间全变。

蒙版要长多大:一次几何推导

圆心好办,getBoundingClientRect() 取触发元素中心即可。麻烦的是终点尺寸:蒙版要长到多大,才能保证动画结束时刚好盖满整个视口?

答案是「触发点到视口四个角中最远的那个距离」,再乘一个余量系数。

03-circle-mask-geometry.png

/** 触发点到视口四角的最大距离 */
export function getMaxRadiusToCorners(center: Point, viewport: Size): number {
   
  const {
    x, y } = center
  const {
    width, height } = viewport
  return Math.max(
    Math.hypot(x, y),
    Math.hypot(width - x, y),
    Math.hypot(x, height - y),
    Math.hypot(width - x, height - y),
  )
}

export function getCircleMaskGeometry(center: Point, viewport: Size): MaskGeometry {
   
  const endSize = getMaxRadiusToCorners(center, viewport) * CIRCLE_SIZE_FACTOR  // 2.1
  return {
   
    maskImage: CIRCLE_MASK_IMAGE,
    startSize: '0px 0px',
    startPosition: `${
     px(center.x)} ${
     px(center.y)}`,
    endSize: `${
     px(endSize)} ${
     px(endSize)}`,
    // 尺寸在长,位置同步往左上偏移一半边长 —— 圆心始终钉在触发点
    endPosition: `${
     px(center.x - endSize / 2)} ${
     px(center.y - endSize / 2)}`,
  }
}

这里有两个容易被忽略的点:

  • mask-position 必须跟着动。 CSS 的 mask 尺寸变化是以左上角为原点的,只改 mask-size 会让圆从左上角「长大」,而不是从按钮长出来。终态的 mask-position 取「圆心坐标减去一半边长」,圆心就钉住了。
  • 所有几何值一律取整到整数 px。 Math.hypot 的结果是无理数,而快照层逐帧动画 mask 时,分数像素偏移会在 GPU 栅格化(尤其在 Windows 分数缩放下)暴露 1px 级接缝,表现为边缘偶发的「线条抖动」。取整损失 ≤0.5px,而 2.1 倍的余量远大于它,安全性无虞。

13 种类型怎么共用一套实现

搞懂圆形之后,剩下 12 种基本都是同一个套路:换蒙版图形,几何计算按形状的内切半径重新推。中心扩散的形状族全部走同一个函数:

function pinCenterGeometry(maskImage: string, center: Point, endSize: Size): MaskGeometry {
   
  return {
   
    maskImage,
    startSize: '0px 0px',
    startPosition: `${
     px(center.x)} ${
     px(center.y)}`,
    endSize: `${
     px(endSize.width)} ${
     px(endSize.height)}`,
    endPosition: `${
     px(center.x - endSize.width / 2)} ${
     px(center.y - endSize.height / 2)}`,
  }
}

区别只在 endSize 怎么算。判断依据是「蒙版的内切半径必须盖住视口最远角」,各形状的内切半径与外接半径比例不同,于是系数也不同:

类型 终点边长 依据
CIRCLE / SQUARE maxRadius × 2.1 / max(halfW, halfH) × 2 × 1.05 5% 余量防末帧缝隙
RECTANGLE halfW × 2 × 1.05halfH × 2 × 1.05 贴合视口宽高比
DIAMOND / HEXAGON maxRadius × √2 × 1.05 × 2 内切半径 = 外接半径 × √2/2(六边形另有 cos30° 富余)
TRIANGLE maxRadius × 2.2 × 2 内切半径 = 外接半径 / 2,自带 10% 余量
STAR maxRadius × 2.5 × 2 最差方向是内凹谷,半径比 0.42,故 1/0.42 × 1.05 ≈ 2.5

这里有一个故意偏离参考实现的决定:五角星的外接圆我取了 2.5 × maxRadius,而常见实现只取约 1.45 ×。原因是它们的蒙版走 clip-path,会随转场组一起销毁,末帧角落透出旧主题无所谓;而 mask 在样式移除前是持续生效的(animation-fill-mode: both),一旦盖不满,用户就会看到四个角残留旧主题。形状好不好看是次要的,盖得住是硬要求。

四向擦除则完全不用改几何,靠 CSS 的百分比 mask-position「钉边」:

const DIRECTIONAL_START = {
   
  [ThemeAnimationType.LTR]: {
    size: `${
     BAR_START_PX}px 100%`, position: '0% 0%' },   // 钉左
  [ThemeAnimationType.RTL]: {
    size: `${
     BAR_START_PX}px 100%`, position: '100% 0%' },  // 钉右
  [ThemeAnimationType.TTB]: {
    size: `100% ${
     BAR_START_PX}px`, position: '0% 0%' },    // 钉上
  [ThemeAnimationType.BTT]: {
    size: `100% ${
     BAR_START_PX}px`, position: '0% 100%' },  // 钉下
}

起始是一条 4px 的细条,被钉住的那条边不动,尺寸长到 100% 100%,就得到了从该边向对侧擦除的效果——mask-image 甚至是零成本的一句话:linear-gradient(#fff, #fff)

至于 CIRCLE_BLUR,模糊是用 feGaussianBlur 烘焙进 SVG 蒙版本身的(不是 CSS filter,那样 Safari 会有兼容问题),并且只挂新层:旧层完整垫底,蒙版外是旧主题,直到模糊圆扫过。如果两层同蒙版,模糊区外露出的会是已翻转的实时页面——主题瞬间全变,糊的只是边缘。

CIRCLE_REVERT:方向感知,与一次「静止盒子」重写

CIRCLE_REVERT 想表达的语义是:切到暗色时,暗色圆从点击处扩散;切回亮色时,暗色圆收回点击处。同一个按钮,来回切换自然产生一次扩散、一次收起,没有额外的隐藏状态。

方向怎么判断?在非受控模式下,转场前 <html> 的类名就是「当前旧主题」,toggle 之后必为取反;受控模式下外部系统可能写的是 data-theme 而不是 class,从类名反推会永远判成「扩散」,所以 runThemeTransition 提供了一个显式参数:

const toDark = nextIsDark ?? !hasThemeClass(doc, resolved.darkClassName)

但在「收起」这个方向上,第一版实现翻了车。

04-circle-revert-hole-mask.png

第一版是把蒙版挂到旧截图层上并 z-index: 1 置顶,然后逐帧动画 mask-size / mask-position,让暗色圆收缩。功能上能跑,但合成器会按设备像素对齐蒙版盒子,逐帧推着圆心走——实测圆心位移的标准差 σ = 0.329px,视觉上就是边缘抖动、偶尔闪过一条横带。

修法是把「动」的部分从盒子上挪走:蒙版盒子完全静止,只动画一个半径

/* 半径注册成 <length> 后才能被动画插值 */
@property --theme-switch-radius {
   
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

/* 圆内透明、圆外不透明 —— 视觉上等价于「旧层的暗色圆」,但层序回到 UA 默认 */
::view-transition-new(root) {
   
  mask-image: radial-gradient(circle at 402px 104px,
    transparent calc(var(--theme-switch-radius) - 0.5px),
    #000      calc(var(--theme-switch-radius) + 0.5px));
  mask-size: 100% 100%;
  mask-position: 0 0;
  animation: theme-switch-circle-revert var(--theme-switch-duration, 750ms) both;
}
@keyframes theme-switch-circle-revert {
   
  from {
    --theme-switch-radius: 490px; }
  to   {
    --theme-switch-radius: 0px; }
}

改动之后:圆心位移降到 σ = 0.011px(噪声级别),而新旧两种实现的渲染结果逐像素比对,最大差值 ≤ 0.068/255,只出现在圆弧那 1px 抗锯齿边上——视觉等价,但不再抖

顺带一个冷知识:@property 注册的自定义属性是全局的、无法注销,所以变量名加了 --theme-switch- 前缀避免撞名。

受控模式:不抢状态管理,只是「等」

如果用户已经在用 next-themes@nuxtjs/color-mode,再塞一个库去管主题状态就是灾难。所以库提供了受控模式:同时传 isDark + onChange 就进入受控,此后库不碰 localStorage、不自己改 class,只做两件事——注入动画样式,以及等外部系统把 DOM 真正改写后再让浏览器截图

难点在于外部系统是异步写 DOM 的(React 用 passive effect,Nuxt 用插件 watch)。onChange 调用后立刻返回,截到的还是旧主题。

05-controlled-mode-sync.png

export function waitForThemeSync({
    doc, darkClassName, nextIsDark, timeoutMs = 300 }): Promise<boolean> {
   
  const synced = () => hasThemeClass(doc, darkClassName) === nextIsDark
  if (synced()) return Promise.resolve(true)              // 同步写入的系统,直接放行
  if (typeof MutationObserver === 'undefined') return Promise.resolve(false)

  return new Promise<boolean>((resolve) => {
   
    let settled = false
    const observer = new MutationObserver((mutations) => {
   
      for (const m of mutations) {
   
        if (m.attributeName === 'class' && synced()) return settle(true)
        if (m.attributeName?.startsWith('data-')) return settle(true)  // 兼容 data-theme 型系统
      }
    })
    const timer = setTimeout(() => settle(synced()), timeoutMs)  // 超时前复查一次
    ...
    observer.observe(doc.documentElement, {
    attributes: true })
  })
}

超时之后不播放动画,而是直接跳过转场:

domUpdate: () => {
   
  onChange(next)
  return waitForThemeSync({
    doc: document, darkClassName, nextIsDark: next })
    .then((synced) => (synced ? undefined : SKIP_TRANSITION))
}

SKIP_TRANSITION 是个哨兵值,runThemeTransition 收到它会立刻调用 transition.skipTransition()。原因很直白:此刻新截图必然还是旧主题,继续演动画只会得到一段「旧 → 旧」的空转,不如直接切换。状态已经由 onChange 落地了,跳过的只是视觉效果。

顺带一提,超时值从最早的 150ms 调到 300ms,是因为 MutationObserver 虽快但并非绝对,低性能设备上要留余量。

跨框架:一份 core,两层薄适配

整个仓库是 pnpm workspace,但只发布一个包,四个入口由 exports 分出。

06-single-package-architecture.png

core 是完全框架无关的(约 95% 代码),框架差异被压缩到两处渲染时机:

  • React 的转场回调里需要用 flushSync 强制同步提交。React 的 setState 是异步批处理的,不强制同步的话,浏览器截图时 DOM 还没更新,会截到旧主题。
domUpdate: () => {
  const next = !hasThemeClass(document, resolved.darkClassName)
  applyThemeClass(document, next, resolved.darkClassName)
  writeStoredTheme(document, next)
  flushSync(() => setUncontrolledIsDark(next))   // 截图前必须已提交
}
  • Vue 更省事:startViewTransition 的回调允许返回 Promise,所以写成 async () => { …; await nextTick() } 就行,浏览器会等 DOM 更新完再截图。
domUpdate: async () => {
   
  const next = !hasThemeClass(document, resolved.darkClassName)
  applyThemeClass(document, next, resolved.darkClassName)
  uncontrolledIsDark.value = next
  await nextTick()   // 等组件树渲染完成
}

Nuxt 模块本身很短,但有个不太显眼的坑:addImportsDir 扫描的是目录下文件的命名导出,而 ThemeAnimationType 既是值(ThemeAnimationType.LTR)又是同名类型(animationType: ThemeAnimationType)。自动导入只会给出值含义,用 addImports() 补类型又会变成「仅类型含义」,值用法直接报 TS1362。最后的解法是利用 TS 的值/类型命名空间相互独立——用 addTypeTemplate 额外补一个全局类型别名,与自动导入的全局 const 自然合并,两种用法同时可用。

打包:单包多入口的几个坑

构建用 tsup,配置不长但每一条都对应一个真实问题:

export default defineConfig({
   
  entry,                                   // index / react / vue / nuxt 四个入口
  format: ['esm'],
  outExtension: () => ({
    js: '.mjs' }),
  // 私有 workspace 包不会随根包发布,类型必须内联进各入口的 d.ts
  dts: {
    resolve: [/^@theme-switch-animation\//, /^\.\.?\//] },
  splitting: false,
  // 关掉 rollup 的二次摇树:它会剥掉入口顶部的 'use client' 指令(React / Next 客户端边界必需)
  treeshake: false,
  external: ['react', 'react-dom', 'vue', '@nuxt/kit', '@nuxt/schema'],
  noExternal: [/^@theme-switch-animation\//],  // core 被打进各入口
})
  • dts.resolve 里要同时放包名和相对路径的正则。它本质是 resolveOnly 过滤器,如果只写包名,core/index.ts 内部的相对导入会被留成 from './types' 这种指向不存在文件的引用。
  • treeshake: false 是刻意的:rollup 的二次摇树会把入口顶部的 'use client' 当成无用代码剥掉,而这是 Next.js 客户端组件的边界声明。esbuild 自身的摇树仍然生效,配合 sideEffects: false,最终由消费方的打包器做最后一步。
  • peerDependencies 里 React、Vue、@nuxt/kit 全部标成 optional——用一个包覆盖四个框架,不能逼着 Vue 用户装 React。react-dom 也要显式声明,因为 /react 入口用了它的 flushSync;在 pnpm 的严格隔离布局(hoist=false)下漏声明会导致子路径解析失败。

状态永远正确:降级策略

最后把降级行为汇总一下,这也是我在 README 里最想强调的部分:

场景 行为
浏览器不支持 View Transitions(Safari < 18、Firefox < 144 等) 直接切换,无动画,状态正确
prefers-reduced-motion: reduce 直接切换,无动画
SSR 渲染阶段 不触碰 window / document / localStorage,无 hydration 报错
受控模式外部系统 300ms 未同步 跳过转场直切,不播放「旧 → 旧」空转
快速连点 浏览器中止前一轮转场,状态不受影响,无 unhandledrejection
localStorage 不可用(隐私模式 / 跨域 iframe) 静默跳过持久化,状态仍以 <html> 类名为准

非受控模式下还有一个我比较满意的设计:<html> 上的暗色类名作为事实源。同页多个实例的 isDark 都镜像它,其它标签页的切换经 storage 事件同步。这样「多个按钮状态不一致」这类问题从结构上就不存在。

用法速览

React / Next.js(App Router 的组件记得加 'use client'):

import { useState } from 'react'
import { useThemeAnimation, ThemeAnimationType } from 'theme-switch-animation/react'

function ThemeButton() {
  const { ref, toggleTheme, isDark, finished } = useThemeAnimation({
    animationType: ThemeAnimationType.CIRCLE,
    duration: 750,
    easing: 'ease-in-out',
  })
  const [animating, setAnimating] = useState(false)
  return (
    <button
      ref={ref}
      disabled={animating}
      onClick={async () => {
        setAnimating(true)
        toggleTheme()
        await finished     // 动画期间禁用,降级时立即结算
        setAnimating(false)
      }}
    >
      {isDark ? '🌙' : '☀️'}
    </button>
  )
}

Vue 3:

<script setup lang="ts">
const { triggerRef, toggleTheme, isDark } = useThemeAnimation<HTMLButtonElement>({
  animationType: ThemeAnimationType.CIRCLE,
})
</script>

<template>
  <button ref="triggerRef" @click="toggleTheme">{
  { isDark ? '🌙' : '☀️' }}</button>
</template>

Nuxt 3:

export default defineNuxtConfig({
   
  modules: ['theme-switch-animation/nuxt'],
})

useThemeAnimation / ThemeAnimationType 等全部自动导入,无需 import。

接入 next-themes 时切到受控模式即可——库不碰存储、不改 class,只负责把动画演出来:

const { resolvedTheme, setTheme } = useNextThemes()
const { ref, toggleTheme } = useThemeAnimation({
  isDark: resolvedTheme === 'dark',
  onChange: (next) => setTheme(next ? 'dark' : 'light'),
  animationType: ThemeAnimationType.CIRCLE_REVERT,
})

小结

回头看,这个库真正的难点其实不在动画本身,而在几件容易被当成「边角料」的事:

  • 蒙版几何:从触发点算到视口最远角,圆心靠 mask-position 钉住,所有值取整到整数 px;
  • 层序:蒙版只挂新层、旧层完整垫底;收起方向用「新层上的洞」替代「旧层置顶」,顺手消掉了半像素抖动;
  • 等,而不是替:受控模式下让外部主题系统自己写 DOM,库只观察、只兜底;
  • 状态优先于动画:任何环境、任何超时、任何连点,isDark 与类名都必须是对的。

包已经发布在 npm(theme-switch-animation),源码与每种动画的在线 demo 都在文档站上:

如果你也在做主题切换,希望它能帮你省掉推导蒙版尺寸的那几个晚上。

相关文章
|
8天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1827 14
|
7天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
|
13天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
12天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1655 3
|
7天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
9天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
799 2
|
7天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
815 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
|
14天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1647 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
|
21天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3999 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
12天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1158 0