普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月17日掘金 前端

【swift开发基础】34丨访问和操作数组:查找操作

2026年1月16日 23:34

一、访问和操作数组

1. 在末尾添加

  • 可变性要求:数组必须是变量(用var定义),常量数组不可修改
  • append方法:
    • append(:):在数组末尾添加单个元素
    • append(contentsOf:):在末尾添加多个元素(接受序列)
  • 示例:
    • numbers.append(100):在[2,3,4,5,6,7]后添加100 → [2,3,4,5,6,7,100]
    • numbers.append(contentsOf: 100...105):添加100到105区间 → [2,3,4,5,6,7,100,101,102,103,104,105]

2. 在任意位置添加

  • insert方法:
    • insert(_:at:):在指定位置插入单个元素
    • insert(contentsOf:at:):在指定位置插入多个元素
  • 示例:
    • numbers.insert(-1, at: 0):在0位置插入-1 → [-1,2,3,4,5,6,7]
    • numbers.insert(contentsOf: -2...0, at: 0):在0位置插入-2到0 → [-2,-1,0,2,3,4,5,6,7]

3. 字符串也是Collection

  • 特性:字符串是Character类型的集合,可直接作为内容插入字符数组

  • 示例:

    • chars.insert(contentsOf: "hello", at: 0):将"hello"分解为字符插入 → ["h","e","l","l","o","a","b","c"]

4. 移除单个元素

  • remove(at:):移除并返回指定位置元素(非空数组必须)

  • removeFirst():移除并返回首元素(数组为空会崩溃)

  • popFirst():安全移除首元素(返回Optional,数组为空返回nil)

  • 示例:

    • chars.remove(at: 1):移除索引1的元素"b" → ["a","c","d"]
    • chars.removeFirst():移除"a" → ["b","c","d"]
    • chars.popFirst():安全移除首元素(推荐使用)

5. 移除多个元素

  • 带参数版本:

    • removeFirst(_:):移除前N个元素
    • removeLast(_:):移除后N个元素
  • 示例:

    • chars.removeFirst(2):移除前两个 → ["c","d"]
    • chars.removeLast(2):移除后两个 → ["a","b"]

6. 移除多个元素

  • removeSubrange(_:):移除指定范围内的元素

  • removeAll():清空数组(释放内存)

  • removeAll(keepingCapacity:):清空但保留容量

  • 容量优化:建议后续需要插入操作时使用keepingCapacity:true

  • 示例:

    • chars.removeSubrange(1...2):移除索引1-2 → ["a","d"]
    • chars.removeAll(keepingCapacity: true):清空但容量保持4

7. 应用案例

1)例题:数组的添加和删除操作
  • 实践要点:

    • 添加操作必须使用变量数组
    • popLast()比removeLast()更安全
    • 批量操作时注意索引范围有效性
    • 频繁增删时建议使用keepingCapacity保持性能

二、知识小结

知识点 核心内容 易混淆点 难度系数
Swift数组添加元素 使用append在末尾添加单个元素,append(contentsOf:)添加多个元素 必须使用var定义可变数组 ⭐⭐
Swift数组插入元素 使用insert(_:at:)在指定位置插入单个元素,insert(contentsOf:at:)插入多个元素 插入位置索引越界会引发运行时错误 ⭐⭐⭐
Swift数组移除元素 remove(at:)移除指定位置元素,removeFirst()/removeLast()移除首尾元素,popLast()安全移除尾元素 removeFirst/removeLast在空数组调用会崩溃,而popLast返回可选值 ⭐⭐⭐⭐
Swift数组批量移除 removeSubrange(_:)移除范围元素,removeAll(keepingCapacity:)保留容量清空数组 keepingCapacity参数对性能优化的作用 ⭐⭐⭐
OC与Swift数组对比 OC中NSArray不可变,只有NSMutableArray可修改;Swift用let/var区分 OC需要显式使用可变类,Swift通过变量声明控制 ⭐⭐
字符串作为集合处理 字符串可分解为字符插入字符数组,体现Swift的Collection协议统一性 字符串与字符数组的类型转换机制 ⭐⭐⭐
昨天 — 2026年1月16日掘金 前端

Logoly:几秒钟做出「P站 风格」Logo 的开源小工具

作者 修己xj
2026年1月16日 23:15

很多人做个人主页、公众号封面、技术分享 PPT,都会遇到一个共同问题:
想要一个眼前一亮的 Logo,但又不会设计,也懒得打开 PS / Figma。

Logoly 就是为这种场景而生的一个开源项目:
一个可以在线生成 Pornhub / OnlyFans 风格 Logo 的小工具,只需要改几个字、调调颜色,几秒钟就能导出一张“似曾相识”的趣味 Logo。

👁️‍🗨️ Logoly 是什么?

Logoly 的作者给它的定义是:

A Simple Online Logo Generator for People Who Want to Design Logos Easily.
—— 让任何人都能轻松做 Logo 的在线生成器。

它最早因为可以生成类似 p站 风格的 Logo 在社区火了一把,后来又加入了 OnlyFans 风格的样式,逐渐变成一个 “恶搞 / 表情包 / 个人小品牌” 都能用的小工具。

特点概括一下:

  • 在线使用,无需安装,打开网页就能玩
  • 生成 P站 / OnlyFans 风格的 Logo
  • 支持自定义文字内容
  • 支持自定义颜色和字体大小
  • 一键导出 PNG / SVG 两种格式
  • 开源,基于 WTFPL 2 授权,几乎「想怎么玩就怎么玩」

github地址: github.com/bestony/log… 在线体验:www.logoly.pro/

该项目目前在github 上已有 7.9k ⭐️star

🎯 功能一览

1. 两种经典风格:P站风格 & OnlyFans

你可以一键做出这两种在互联网上辨识度极高的 Logo 风格,用来做:

  • 朋友圈/微博的整活配图
  • 技术分享 PPT 中的彩蛋
  • 团队内部的梗图或文化海报
  • 个人主页 / 博客上的趣味 Logo 等

风格本身非常强烈,但文字是你自定义的,可以天马行空。

注意:Logoly 本身不涉及任何成人内容,只是借鉴了这些品牌 Logo 的视觉风格。

2. 自定义文字内容

页面中有一个文本输入框,只需要:

  1. 把默认的文字改成你想要的(英文、数字、甚至中文都可以尝试)
  2. 实时预览 Logo 效果

很适合搞一些有梗、容易被记住的短词。

3. 自定义颜色和字体大小

你可以自由调整:

  • 左右两段文字的颜色
  • 中间色块的背景色(具体视当前主题而定)
  • 整体字号大小,用来适配不同场景(比如头像 vs 横幅)

简单几下滑动/点选,就能做出风格迥异的变体。

4. 导出 PNG / SVG

完成之后,点击 Export 按钮可以导出两种格式:

  • PNG:适合直接作为图片使用(社交头像、封面图、PPT、聊天表情等)
  • SVG:矢量格式,放大不会糊,适合用在网页、印刷、再加工设计中

对于前端开发者来说,SVG 的可编辑性也很友好,可以进一步嵌入到自己的项目里。

在线使用教程:4 步搞定一个 Logo

作者已经把使用流程总结成 4 步,实际体验下来也确实非常简单:

  1. 打开网站:
    访问 logoly.pro/

  2. 编辑文字:
    把默认的文本替换成你想要的单词/短句

  3. 调整样式:

    • 修改颜色
    • 调整字体大小
  4. 导出图片:
    点击 Export 按钮,选择 PNG 或 SVG 导出

整个过程基本不需要学习成本,几乎是「一看就会」。

以下是我生成的一些案例

✏️ 面向开发者:如何在本地运行 Logoly?

环境要求

项目对 Node / npm 版本有明确要求:

  • Node.js 18+
  • npm 10+
    (官方说明:只支持 npm 作为包管理器,请不要提交其他管理器生成的 lockfile)

也就是说:

  • 不建议使用 yarn / pnpm / Bun 来跑这个项目,以免锁文件不一致、CI 失败。
  • CI 流水线里也是基于 npm 的脚本执行。

本地跑起来的步骤

在命令行中依次执行:

# 1. 克隆仓库
git clone https://github.com/bestony/logoly.git
cd logoly

# 2. 安装依赖
npm install

# 3. 启动开发服务器
npm run dev

启动成功后,在浏览器里访问提示的本地地址(通常是 http://localhost:xxxx),就能看到 Logoly 的开发版界面。

修改源码后即可实时看到效果。
开发完成后,可以进行构建:

npm run build

docker部署

如果你想使用docker部署,则只需要启动一个nginx 容器,将构建后的dist目录下的文件及文件夹挂载到容器中即可,不会部署nginx的家人们可以搜索下博主的历史文章,有介绍docker 部署nginx 及配置的博文。

🦆 总结

如果你:

  • 需要一个有辨识度的 Logo,又不想折腾复杂设计工具
  • 想给项目、团队或朋友做一个带梗的图标
  • 想找一个小而美的前端开源项目学习 / 贡献

那么 Logoly 非常值得收藏一下。

你可以先用线上版做几个 Logo 玩玩。

解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!

作者 北辰alk
2026年1月16日 22:31

解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!

无论你是Vue新手还是有一定经验的开发者,相信在工作中都遇到过这样的场景:多个组件需要用到同一个工具函数,比如格式化日期、权限验证、HTTP请求等。如果每个组件都单独引入,不仅代码冗余,维护起来也让人头疼。

今天我就为大家分享5种定义全局方法的实用方案,让你轻松解决这个问题!

🤔 为什么需要全局方法?

先来看一个真实的例子。假设你的项目中有三个组件都需要格式化日期:

// UserProfile.vue
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

// OrderList.vue  
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

// Dashboard.vue
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

发现了问题吗?同样的代码写了三遍!  这就是我们需要全局方法的原因。

📝 方案一:Vue.prototype(最经典的方式)

这是Vue 2时代最常用的方法,直接扩展Vue的原型链:

// main.js 或 plugins/global.js
import Vue from 'vue'

// 定义全局方法
Vue.prototype.$formatDate = function(date) {
  const dayjs = require('dayjs')
  return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}

Vue.prototype.$checkPermission = function(permission) {
  const user = this.$store.state.user
  return user.permissions.includes(permission)
}

// 在组件中使用
export default {
  mounted() {
    console.log(this.$formatDate(new Date()))
    if (this.$checkPermission('admin')) {
      // 执行管理员操作
    }
  }
}

优点:

  • • 使用简单,直接通过 this 调用
  • • 广泛支持,兼容性好

缺点:

  • • 污染Vue原型链
  • • 方法多了难以管理
  • • TypeScript支持需要额外声明

🎯 方案二:全局混入(适合通用逻辑)

如果你有一组相关的全局方法,可以考虑使用混入:

// mixins/globalMethods.js
export default {
  methods: {
    $showSuccess(message) {
      this.$message.success(message)
    },
    $showError(error) {
      this.$message.error(error.message || '操作失败')
    },
    $confirmAction(title, content) {
      return this.$confirm(content, title, {
        type'warning'
      })
    }
  }
}

// main.js
import Vue from 'vue'
import GlobalMixin from './mixins/globalMethods'

Vue.mixin(GlobalMixin)

// 组件中使用
export default {
  methods: {
    async deleteItem() {
      try {
        await this.$confirmAction('确认删除''确定删除该记录吗?')
        await api.deleteItem(this.id)
        this.$showSuccess('删除成功')
      } catch (error) {
        this.$showError(error)
      }
    }
  }
}

适合场景:  UI反馈、确认对话框等通用交互逻辑。

🏗️ 方案三:独立模块 + Provide/Inject(Vue 3推荐)

Vue 3提供了更优雅的解决方案:

// utils/globalMethods.js
export const globalMethods = {
  // 防抖函数
  debounce(fn, delay = 300) {
    let timer = null
    return function(...args) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  },
  
  // 深度拷贝
  deepClone(obj) {
    return JSON.parse(JSON.stringify(obj))
  },
  
  // 生成唯一ID
  generateId() {
    return Math.random().toString(36).substr(29)
  }
}

// main.js
import { createApp } from 'vue'
import { globalMethods } from './utils/globalMethods'

const app = createApp(App)

// 通过provide提供给所有组件
app.provide('$global', globalMethods)

// 组件中使用
import { inject } from 'vue'

export default {
  setup() {
    const $global = inject('$global')
    
    const handleInput = $global.debounce((value) => {
      console.log('搜索:', value)
    }, 500)
    
    return { handleInput }
  }
}

这是Vue 3的推荐方式,保持了良好的类型推断和代码组织。

📦 方案四:插件化封装(企业级方案)

对于大型项目,建议采用插件化的方式:

// plugins/globalMethods.js
const GlobalMethodsPlugin = {
  install(app, options) {
    // 添加全局方法
    app.config.globalProperties.$http = async (url, config) => {
      try {
        const response = await fetch(url, config)
        return await response.json()
      } catch (error) {
        console.error('请求失败:', error)
        throw error
      }
    }
    
    app.config.globalProperties.$validate = {
      email(email) {
        return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)
      },
      phone(phone) {
        return /^1[3-9]\d{9}$/.test(phone)
      }
    }
    
    // 添加全局属性
    app.config.globalProperties.$appName = options?.appName || 'My App'
    
    // 添加自定义指令
    app.directive('focus', {
      mounted(el) {
        el.focus()
      }
    })
  }
}

// main.js
import { createApp } from 'vue'
import GlobalMethodsPlugin from './plugins/globalMethods'

const app = createApp(App)
app.use(GlobalMethodsPlugin, {
  appName'企业管理系统'
})

// 组件中使用
export default {
  mounted() {
    // 使用全局方法
    this.$http('/api/users')
    
    // 使用验证
    if (this.$validate.email(this.email)) {
      // 邮箱有效
    }
    
    // 访问全局属性
    console.log('应用名称:'this.$appName)
  }
}

🌟 方案五:Composition API方式(最现代)

如果你使用Vue 3的Composition API,可以这样组织:

// composables/useGlobalMethods.js
import { readonly } from 'vue'

export function useGlobalMethods() {
  // 定义所有全局方法
  const methods = {
    // 金额格式化
    formatCurrency(amount) {
      return '¥' + Number(amount).toFixed(2)
    },
    
    // 文件大小格式化
    formatFileSize(bytes) {
      const units = ['B''KB''MB''GB']
      let size = bytes
      let unitIndex = 0
      
      while (size >= 1024 && unitIndex < units.length - 1) {
        size /= 1024
        unitIndex++
      }
      
      return `${size.toFixed(1)} ${units[unitIndex]}`
    },
    
    // 复制到剪贴板
    async copyToClipboard(text) {
      try {
        await navigator.clipboard.writeText(text)
        return true
      } catch {
        // 降级方案
        const textArea = document.createElement('textarea')
        textArea.value = text
        document.body.appendChild(textArea)
        textArea.select()
        document.execCommand('copy')
        document.body.removeChild(textArea)
        return true
      }
    }
  }
  
  return readonly(methods)
}

// main.js
import { createApp } from 'vue'
import { useGlobalMethods } from './composables/useGlobalMethods'

const app = createApp(App)

// 挂载到全局
app.config.globalProperties.$globalMethods = useGlobalMethods()

// 组件中使用
import { getCurrentInstance } from 'vue'

export default {
  setup() {
    const instance = getCurrentInstance()
    const $global = instance?.appContext.config.globalProperties.$globalMethods
    
    // 或者在setup中直接引入
    // const $global = useGlobalMethods()
    
    return { $global }
  },
  mounted() {
    console.log(this.$global.formatCurrency(1234.56))
  }
}

📊 5种方案对比总结

方案 适用版本 优点 缺点 推荐指数
Vue.prototype Vue 2 简单直接 污染原型链 ⭐⭐⭐
全局混入 Vue 2/3 逻辑分组 可能造成冲突 ⭐⭐⭐
Provide/Inject Vue 3 类型安全 使用稍复杂 ⭐⭐⭐⭐
插件封装 Vue 2/3 功能完整 配置复杂 ⭐⭐⭐⭐⭐
Composition API Vue 3 现代灵活 需要Vue 3 ⭐⭐⭐⭐⭐

💡 最佳实践建议

  1. 1. 按功能分类组织
// 不推荐:把所有方法堆在一个文件
// 推荐:按功能模块拆分
utils/
  ├── formatters/    # 格式化相关
  ├── validators/    # 验证相关  
  ├── http/         # 请求相关
  └── ui/           # UI交互相关
  1. 2. 添加TypeScript支持
// global.d.ts
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $formatDate(date: Date) => string
    $checkPermission(permission: string) => boolean
  }
}
  1. 3. 注意性能影响
  • • 避免在全局方法中执行重逻辑
  • • 考虑使用懒加载
  • • 及时清理不再使用的方法
  1. 4. 保持方法纯净
  • • 一个方法只做一件事
  • • 做好错误处理
  • • 添加详细的JSDoc注释

🎁 福利:一个实用的全局方法库

我整理了一些常用的全局方法,你可以直接使用:

// utils/essentials.js
export const essentials = {
  // 下载文件
  downloadFile(url, filename) {
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  },
  
  // 获取URL参数
  getUrlParam(name) {
    const params = new URLSearchParams(window.location.search)
    return params.get(name)
  },
  
  // 休眠函数
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  },
  
  // 对象转FormData
  objectToFormData(obj) {
    const formData = new FormData()
    Object.keys(obj).forEach(key => {
      formData.append(key, obj[key])
    })
    return formData
  }
}

✨ 结语

掌握全局方法的定义和使用,能够让你的Vue项目更加模块化、可维护、高效。不同的方案适用于不同的场景和需求,关键是要根据项目实际情况选择最合适的方式。

记住:好的代码不是写出来的,而是设计出来的。

希望今天的分享对你有帮助!如果你有更好的方案或实践经验,欢迎在评论区留言分享。

详细说一下nuxt generate是干啥的

作者 江湖文人
2026年1月16日 18:21

nuxt generate 是 Nuxt.js 框架的一个**静态站点生成(Static Site Generation, SSG)**命令。我来详细解释一下它的含义和作用:

是什么?

nuxt generate 命令会将你的 Nuxt 应用预渲染为静态 HTML 文件。它会:

  • 为每个路由生成对应的 HTML 文件
  • 将生成的静态文件保存在 dist/ 目录中
  • 包含必要的 CSS、JavaScript 和资源文件

主要作用

1. 性能优化

  • 预生成的 HTML 文件无需服务器端渲染,加载速度极快
  • CDN 友好,可以轻松缓存
  • 减少服务器压力和响应时间

2. SEO 优化

  • 搜索引擎可以直接抓取静态 HTML 内容
  • 更好的 SEO 表现(相比于纯客户端渲染)

3. 部署简单

  • 生成的文件可以部署到任何静态主机:
    • Netlify、Vercel、GitHub Pages
    • AWS S3、Firebase Hosting
    • Nginx、Apache 等传统服务器

4. 成本效益

  • 无需专门的 Node.js 服务器
  • 可以使用廉价的静态托管服务

使用场景

适合使用 nuxt generate

内容型网站:博客、文档、营销页面
数据不频繁变化:产品展示页、公司官网
需要优秀 SEO 的应用
高访问量 的只读页面

不适合使用(需要考虑 SSR 或 CSR):

用户个性化内容:每个用户看到的内容不同
实时数据:股票行情、聊天应用
频繁更新:社交媒体动态
需要身份验证 的页面(可通过混合模式解决)

基本使用

# 生成静态文件
nuxt generate

# 生成后预览
nuxt generate && nuxt start

# 构建并生成(常用)
npm run generate
# 在 package.json 中通常配置为:
# "scripts": {
#   "generate": "nuxt generate"
# }

配置示例

// nuxt.config.js
export default {
  target: 'static', // 明确指定为静态站点
  generate: {
    // 动态路由需要指定
    routes: [
      '/users/1',
      '/users/2',
      '/blog/post-1'
    ],
    // 或者异步获取路由
    async routes() {
      const posts = await $fetch('/api/posts')
      return posts.map(post => `/blog/${post.id}`)
    }
  }
}

工作流程

执行 nuxt generate
    ↓
Nuxt 启动构建过程
    ↓
为每个路由生成 HTML
    ↓
提取 CSS 和 JavaScript
    ↓
保存到 dist/ 目录
    ↓
完成!可以部署到任何静态主机

nuxt build 的区别

  • nuxt generate:生成静态 HTML 文件,用于静态托管
  • nuxt build:构建应用,用于服务器端渲染(SSR)部署

高级特性

1. 混合模式

// 部分页面静态生成,部分页面动态渲染
export default {
  generate: {
    exclude: [
      '/dashboard',  // 这个页面保持动态
      '/admin/**'    // 所有 admin 页面都动态
    ]
  }
}

2. 增量静态再生

可以通过定时任务重新生成部分页面。

实际示例

# 1. 创建 Nuxt 项目
npx nuxi@latest init my-static-site

# 2. 安装依赖
cd my-static-site
npm install

# 3. 生成静态文件
npm run generate

# 4. 查看生成的文件
ls -la dist/
# 会看到 index.html, about.html 等

# 5. 本地测试生成的文件
npx serve dist/

总之,nuxt generate 是 Nuxt.js 强大的静态站点生成功能,特别适合需要优秀性能、SEO 和低成本部署的场景。对于适合静态化的项目,它能提供极佳的用户体验。

小程序增加用户协议

作者 一涯
2026年1月16日 17:55
在小程序中增加一个用户协议

1.开发用户协议页面

在一个内部网站上开发一个用户协议的页面。

2.在小程序开发者后台添加业务域名

添加的时候,会给一个验证码文件。下载下来。

3. 将验证码文件放在用户协议所在网站的根目录

4. 在小程序中使用webview加载网页链接

<template>
  <view class="settings-page">
    <view class="list-card">
      <view
        class="list-item"
        @tap="openPage(userAgreementUrl)"
      >
        <text>用户协议</text>
        <view class="arrow" />
      </view>
    </view>
  </view>
  <view>
    <web-view
        v-if="userAgreementUrl"
        :src="userAgreementUrl"
    ></web-view>
</template>

<script>
import Taro from '@tarojs/taro'
import { USER_AGREEMENT_URL } from '@/config/legal'

export default {
  name: 'settings-page',
  data() {
    return {
      userAgreementUrl: USER_AGREEMENT_URL,
    }
  },
  methods: {
    openPage(url) {
      if (!url) {
        Taro.showToast({
          title: '链接未配置',
          icon: 'none'
        })
        return
      }
      const target = encodeURIComponent(url)
      Taro.navigateTo({
        url: `/pages/webview/webview?url=${target}`
      })
    }
  }
}
</script>

其中legal文件代码如下:

export const USER_AGREEMENT_URL = 'https:/XXXXXXX/privacy/user.html'

如何用一个 mcp 来教笨蛋 AI 好好干活

作者 voderl
2026年1月16日 17:47

我们在写伪代码吗?

相信很多人都有这种感觉:写 Prompt 越来越像在写伪代码

  • “先 xxx,再 xxx” → 对应代码的执行顺序
  • “如果 xxx,就 xxx” → if 逻辑
  • “一直执行直到 xxx” → while 循环

既然如此,我们能不能直接把一段“伪代码”丢给 AI?

kimi-k2 执行效果: image.png

看起来还可以!

给它加一点限制,让它从 1+...+ 到 n,同时排除所有位数之和加起来为 5 的倍数的数字,不能调用脚本:

let count = Tool.AskUserQuestion(`please input a number`);
let sum = 0;
for (let i = 1; i <= count; i++) {
  if (Prompt(`${i} 的每个位数加起来之和为 5 的倍数`)) {

  } else {
    sum += i;
  }
}
Prompt(`最终计算结果为 ${sum}`)

image.png

好吧,这下原型毕露了,比如 86 这个完全不应该排除的数字,被它直接排除掉了。

而且它是先计算的所有值的和,再减去需要排除的值,其实没有严格按照我们的逻辑来执行。

其实好好想一想上面的过程,我们把一个伪代码丢给大模型来执行,期望于就像把代码丢给编译器来执行一样,但是 AI 有着很多的幻觉,这个“编译器”很不稳定。

在一些复杂任务的实测里,它会跳过逻辑胡乱执行,比如没按照预期调用 tool,或者直接在半路上认为自己已经成功了,特别是一些笨蛋 AI,每次执行过程和结果可能都不一样。

可以用代码驱动 AI 吗?

为了解决这种不稳定性,我们需要一种能强约束执行流程的工具。

在 Claude Code 或类似的 Agent 框架中,AI 可以根据 Tool 的返回决定下一步。那么,我们能不能反过来?由一段真实的代码来驱动 AI,AI 只负责完成其中的“自然语言函数”部分?

这正是 agent-workflow-mcp-tool 的核心思路:利用 MCP (Model Context Protocol) 协议,通过 TypeScript 的 Generator 函数,将 AI 变成流程中的一个执行单元。

下面的代码是可以真实执行的代码而非伪代码:

async function* Workflow() {
  const count = yield* ClaudeCodeTools.AskUserQuestion(
    `please input a number`,
    z.number()
  );

  let sum = 0;
  for (let i = 1; i <= count; i++) {
    sum = yield* Prompt(`calculate ${sum} + ${i}`, z.number());
  }
  return sum;
}

github 地址:github.com/voderl/agen…

它有哪些优势呢:

  • 用代码控制流程。 Agent lies, code not
  • 使用 zod 强校验,避免模型幻觉
  • 完善的 typescript 支持
  • 支持 async await throw catch 等语法
  • 对比其他工具特别轻量,完全基于 mcp 协议
  • 配合 claude code 和 kimi-k2 & deepseek 工作良好

比如用我们再举上面的例子,如果用该工具去处理上面的问题,让 AI 从 1+...+n,同时排除所有位数之和加起来为 5 的倍数的数字,那么完整的写法如下:

import { ClaudeCodeTools, registerWorkflowTool, Prompt, z } from "agent-workflow-mcp-tool";

const server = new McpServer({
  name: "agent-workflow",
  version: "0.0.1",
});

registerWorkflowTool(
  server,
  "workflow",
  {
    title: "workflow",
    description: `workflow control`,
  },
  async function* Workflow() {
    const count = yield* ClaudeCodeTools.AskUserQuestion(
      `please input a number`,
      z.number()
    );

    let sum = 0;
    for (let i = 1; i <= count; i++) {
      if (
        yield* Prompt(
          `计算 "${i}" 的所有位数加起来之和是否为 5 的倍数`,
          z.boolean()
        )
      ) {
      } else {
        sum += i;
      }
    }
    return sum;
  }
);

这时你让 Claude Code 直接执行该 mcp

Ask: use mcp "agent-workflow" tool "workflow" directly

执行结果见下图:

image.png

kimi-k2 真的严格按照代码给定的流程,从 1 + 2 + 直到加到 87,对每一个数字判断其所有数字加起来之和是否为 5 的倍数,调用了 87 次 mcp tool,最终得出了正确的结果。

可以在图中看出,在大模型每次调用 mcp 时,mcp 会给出当前的任务,如果大模型执行成功,大模型需要在下次调用时带上上次任务的执行结果。

每一步都有完善的 zod 类型校验,如果传参不对会给大模型提示,避免大模型的传参幻觉。

基于这样的 workflow,我们也可以把“对每一个数字判断其所有数字加起来之和是否为 5 的倍数”这一步可能会有大模型幻觉产生的步骤,改为使用代码来执行保证。

async function* Workflow() {
  const count = yield* ClaudeCodeTools.AskUserQuestion(
    `please input a number`,
    z.number()
  );

  let sum = 0;
  for (let i = 1; i <= count; i++) {
    if (sumDigits(i) % 5 === 0) {
    } else {
      sum = yield* Prompt(`calculate ${sum} + ${i}`, z.number())
    }
  }
  return sum;
}

更多复杂场景

基于该工具,我们可以实现更复杂的逻辑,比如自动生成 commit 信息并在用户确认后提交代码:

registerWorkflowTool(
    server,
    "auto-commit",
    {
      title: "auto commit",
      description: `auto commit`,
    },
    async function* Workflow() {
      const filesChangeList = yield* Prompt(
        `获取当前变更文件列表`,
        z.array(z.string())
      );

      if (filesChangeList.length === 0) {
        return `没有任何代码更改`;
      }

      const commitMessage = yield* Prompt(
        `根据当前变更内容生成对应的 commit message,格式需满足:
(fix|feat|chore): 单行简洁的提示

多行详细变更内容`,
        z.string()
      );

      const { is_confirm } = yield* ClaudeCodeTools.AskUserQuestion(
        `commit message 为 ${commitMessage},是否确认继续`,
        z.object({
          is_confirm: z.boolean(),
        })
      );

      if (!is_confirm) return `已取消`;

      yield* Prompt(`将当前变更代码提交,commit message 为 ${commitMessage}`);
    }
  );

自动提交确认效果: image.png

还可以基于上面的流程,在 commit 前获取所有变更文件,挨个给每一个文件都使用 AI review 一遍,可以试试看~

使用

npm install agent-workflow-mcp-tool
import { registerWorkflowTool, Prompt, ClaudeCodeTools, z } from 'agent-workflow-mcp-tool';

欢迎使用和反馈~

你知道项目需要什么 node 版本吗?哪个包管理工具的什么版本?

作者 Carry345
2026年1月16日 17:11

近期接手了一个新项目,clone下来发现

  1. readme 无迹可循,node 版本等信息,只能口口相传,强依赖于上一个开发者
  2. 项目中有 npm, yan, pnpm 相关的配置,却无法知道明确应该使用那个包管理工具

于是开始致力于寻找解决之法

1. 指定 node 版本

1.1. 约束

问题:如果开发者任意使用了某个版本的 node,显然是不符合预期的,所以我们需要添加约束,来尽可能早的暴露错误

目的:开发者可以从项目配置获取到 node 版本信息,以及使用了不符合的 node 版本时warn 或 error

步骤一:最基础约束:package.json → engines

配置方式:

{
  "engines": {
    "node": ">=18 <21"
  }
}

实际效果:

  • npm / pnpm / yarn 会检查

不一样的包管理工具,或同一包管理工具的不同版本,对应行为的说法多种多样,最好自己试一下,下面贴我试下来的结果:

  • npm 10.9.2
  • pnpm 9.5.0
  • yarn 1.22.22

npm: warn

pnpm: warn

yarn: error

步骤二:最基础约束基础上添加engine-strict=true

engine-strict=true

npm: error

pnpm: error

yarn: error (原本也是)

步骤三:脚本约束(最终防线 可选)

const [major] = process.versions.node.split('.').map(Number)

if (major < 18 || major >= 21) {
  console.error(
    `❌ Node.js version ${process.versions.node} is not supported.\n` +
    `Required: >=18 <21`
  )
  process.exit(1)
}
{
  "scripts":{
    "preinstall": "node scripts/check-node.js"
  }
}

1.2. node 版本切换辅助

目的:进入项目实现 node 版本自动切换,或简化手动版本切换

方案一:nvm + .nvmrc (手动)

1️⃣ 在项目根目录新建 .nvmrc

22.16.0

2️⃣ 进入项目执行 nvm use

开发者需要提前安装

  • nvm(macOS / Linux)
  • Windows 需要 nvm-windows

方案二:Volta (自动)

1️⃣ 在项目中 pin node

volta pin node@22.16.0

# 包管理工具一起固定
# volta pin pnpm@9.12.2     # 或 yarn@1.22.19 / npm@10.8.3

生成如下内容,会添加在 package.josn文件中

{
  "volta": {
    "node": "18.19.0"
  }
}

2️⃣ 自动切换 node

不需要手动切换,进入项目后,volta 会在第一次用到 node 时自动切换为目标 node 版本(如果没有目标版本,会自动下载),进入项目 node -v可验证

开发者需要提前安装

  • Volta

注意⚠️:现在 volta 不支持 uninstall node

原因:Volta 把 Node 当成“基础设施”,官方不支持、也不推荐卸载 node

偏方:自己找到文件夹的位置~/.volta/tools/image/node/删掉

2. 指定包管理工具

包管理工具 packageManager (PM)

当看见项目中关于npm,pnpm,yarn包管理工具的配置都存在时,我一脸蒙,不知道应该用哪一个包管理工具,此时明确指定包管理工具才是预期,那么如何指定呢?

2.1. 约束

目的:开发者可以从项目配置获取到可以使用哪个包管理工具,以及使用了不符合的 node 版本时 error

步骤一:package.json -> packageManager(声明 软提醒)

比如指定 pnpm

{
  "packageManager": "pnpm@10.18.3"
}

步骤二: only-allow(强制)

npx only-allow pnpm,npx only-allow npm,npx only-allow yarn

{
  "preinstall": "npx only-allow pnpm"
}

如果有其他脚本,建议把这个放在前面 npx only-allow pnpm && node scripts/check-node.js,此时再用pnpm外的包管理工具可就不行了:

步骤三:脚本约束(最终防线 可选)

const userAgent = process.env.npm_config_user_agent || '';
if (!userAgent.includes('pnpm')) {
  console.error('❌ 请使用 pnpm 安装依赖');
  console.error('💡 运行: corepack enable && pnpm install');
  process.exit(1);
}
{
  "scripts":{
    "preinstall": "npx only-allow pnpm && node scripts/check-node.js"
  }
}

暂时用"preinstall": "node scripts/check-node.js"查看报错:

2.2. 包管理工具切换辅助 ❌

我们没法辅助开发者切换npm/ pnpm /yarn,因为他们本来就不是项目级工具,而是系统级工具,开发者想用哪个用哪个(他尽管用,我们在约束环节已经拦截)

3. 指定包管理工具版本

3.1. 约束

3.1.1. npm 专有约束

1️⃣ package.json#npm

{
  "engines": {
    "node": ">=18 <21",
    "npm": "11.7.0"
  },
}

2️⃣ .npmrc

engine-strict=true

3.1.2. pnpm 专有约束

1️⃣ package.json (声明)

{
  "engines": {
    "node": ">=18 <21",
    "pnpm": "9.1.1"
  },
}

3.1.3. 共享约束:脚本约束

三个PM都可以用的约束,以 pnpm@10.28.2 为例

import * as semver from 'semver';
import { execSync } from 'child_process';

const REQUIRED = '10.28.2';

const current = execSync('pnpm -v').toString().trim();

if (!semver.eq(current, REQUIRED)) {
  console.error(`
❌ pnpm 版本不符合要求

当前版本: ${current}
要求版本: ${REQUIRED}
`);
  process.exit(1);
}

3.2. 包管理工具版本辅助切换

3.2.1. pnpm 自身的版本管理

 {
    "packageManager": "pnpm@10.28.0",
 }

pnpm 触发时,检查 packageManager字段,如果发现不一致会尝试下载并切换到packageManager指定的版本

3.2.2. yarn 依赖 packageManager+corepack

1️⃣ corepack enable

Node.js 版本 ≥ 16.9 <25 自带corepack,没有则先安装corepack

2️⃣ 提供PM信息

{
  "packageManager": "yarn@1.22.20",
}

3️⃣ 自动切换

使用PM时,corepack会读packageManager如果发现版本不一致,触发自动下载

关于corepack可以解决用什么PM(packageManager 包管理工具)?什么PM 版本?

我持怀疑态度,理由如下

corepack 出现的初衷本来就是为了统一PM的版本,而不是统一用户哪一个包版本工具,那都有人说它可以,那尝试一下硬着头皮用。

发现用它来指定PM需要:

  1. 开发者本地存在corepack或Node.js 版本 ≥ 16.9 <25(自带,也不完全自带,如果是 volta下载的,就不会带),且需要corepack enable
  2. 无论 packageManager 配置了什么,都不限制 npm install
  3. 只能限制 corepack下载的包版本工具,但哪个前端开发笔记本不安装几个包管理工具?

发现用它来指PM版本也存在问题:

Corepack 不管理 npm, 配置了npm@10.25.0但是任何版本的npm都会直接执行,不会下载指定版本

结论:❌ 多少有些不可靠,现在能想到的应用场景就只有辅助 yarn 版本切换了

绕死我了!🙂‍↔️🙂‍↔️🙂‍↔️

你的手势冲突解决了吗?鸿蒙事件拦截机制全解析

2026年1月16日 16:00
哈喽,兄弟们,我是 V 哥! 在鸿蒙开发中,尤其是做复杂的交互页面(比如列表里套按钮、横滑菜单、地图缩放)时,手势事件就像是一群调皮的孩子,谁都想抢着接盘。如果你不管好他们,App的体验会差强人意。
❌
❌