开发角度https://www.devdeg.com代码里的小发现Fri, 13 Mar 2026 15:38:25 +0000zh-Hans hourly 1 https://wordpress.org/?v=6.9.4https://www.devdeg.com/wp-content/uploads/2026/01/cropped-200840f7YieZz1L2VrogQt-1-32x32.png开发角度https://www.devdeg.com3232Skill vs MCP:AI能力扩展的两种方式https://www.devdeg.com/423.htmlhttps://www.devdeg.com/423.html#respondFri, 13 Mar 2026 15:38:25 +0000https://www.devdeg.com/?p=423什么是Skill?

Skill(技能)是OpenClaw平台的能力扩展包,类似于”知识模块”或”操作手册”。每个Skill包含:

  • SKILL.md:核心指令文档,告诉AI如何执行特定任务
  • scripts/:可执行脚本(Python、Bash等)
  • references/:参考文档和知识库
  • assets/:模板、图片等资源文件

Skill结构示意图

Skill的特点

  • ✅ 自包含:所有逻辑和资源都打包在一起
  • ✅ 离线可用:不依赖外部服务
  • ✅ 轻量级:通过文档指导AI行为
  • ✅ 易分享:.skill文件可以直接分发

什么是MCP?

MCP(Model Context Protocol)是一个开放协议,让AI能够连接外部服务和工具。它就像是”API接口标准”。

MCP协议示意图

MCP的特点

  • 🔌 实时连接:直接调用外部服务API
  • 🔄 动态数据:获取最新的实时信息
  • 🛠 功能丰富:可以执行复杂的远程操作
  • 🌐 需要网络:依赖外部服务可用性

核心区别

Skill vs MCP对比

对比维度SkillMCP
本质知识包/指令集通信协议
部署本地安装远程连接
数据静态文档动态实时
网络离线可用需要联网
更新手动升级服务端更新

实际案例:WordPress管理

wordpress-mcp Skill:这是一个Skill,它教AI如何使用MCP协议。Skill本身是文档和脚本,而MCP是它用来连接WordPress的协议。

流程:

  1. 安装wordpress-mcp Skill(本地)
  2. Skill告诉AI如何构造MCP请求
  3. AI通过MCP协议连接WordPress(远程)
  4. WordPress返回实时数据

总结

Skill和MCP不是对立关系,而是互补的:

  • Skill:教AI”怎么做”
  • MCP:让AI”连接外部世界”

很多时候,一个Skill会使用MCP来实现功能,就像wordpress-mcp这样。

]]>
https://www.devdeg.com/423.html/feed0
WordPress极简文本主题 – Just-Texthttps://www.devdeg.com/368.htmlhttps://www.devdeg.com/368.html#respondSun, 04 Jan 2026 15:29:00 +0000https://www.devdeg.com/?p=368Just Text
一个极简优雅的 WordPress 单栏文本主题,专注于内容阅读体验。样式参考本站

项目地址:https://github.com/hamwong233/just-text

✨ 特点
🎨 极简设计 – 纯文本风格,专注内容阅读
🌓 主题切换 – 支持日间/夜间模式,可自动跟随时间切换
📱 完全响应式 – 完美适配桌面端和移动端
🔤 单栏布局 – 最佳阅读体验,无干扰
⚡ 零构建 – 使用 Tailwind CSS CDN,开箱即用
📝 优化排版 – 精心设计的文章样式
🖼 图片预览 – 点击图片可放大查看,支持缩放和拖动
📋 代码高亮 – 自动语法高亮,支持 190+ 种语言,一键复制
💬 评论系统 – 优雅的评论区设计,支持 2 级嵌套回复
🏷 分类标签 – 完整的分类和标签支持
📄 页面模板 – 内置分类概览、标签概览、月份归档模板
🖼 图片懒加载 – 自动懒加载,提升页面性能
🚀 Gravatar 加速 – 使用国内 CDN 加速头像加载

]]>
https://www.devdeg.com/368.html/feed0
Gin接入Go-Captcha实现人机校验验证码获取和校验https://www.devdeg.com/188.htmlhttps://www.devdeg.com/188.html#commentsFri, 21 Feb 2025 12:54:16 +0000https://www.devdeg.com/?p=188以前校验都用的图形验证码,这次用go体验了一下行为验证码

接下来我们要用到的go库是

GoCaptcha | 行为验证码

它支持多种校验方式

安装Go-Captcha

安装模块

go get -u github.com/wenlng/go-captcha/v2@latest

安装预制静态资源

go get -u github.com/wenlng/go-captcha/v2@latest

初始化验证码

我参考作者文档和demo编写的以下方法

package common

import (
	"github.com/golang/freetype/truetype"
	"github.com/wenlng/go-captcha-assets/bindata/chars"
	"github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw"
	"github.com/wenlng/go-captcha-assets/resources/images"
	"github.com/wenlng/go-captcha-assets/resources/tiles"
	"github.com/wenlng/go-captcha/v2/base/option"
	"github.com/wenlng/go-captcha/v2/click"
	"github.com/wenlng/go-captcha/v2/rotate"
	"github.com/wenlng/go-captcha/v2/slide"
	"log"
)

// ClickCaptcha 全局点选验证码对象
var ClickCaptcha *click.Captcha

// SlideCaptcha 全局滑动验证码对象
var SlideCaptcha *slide.Captcha

// RotateCaptcha 全局旋转验证码对象
var RotateCaptcha *rotate.Captcha

// 验证码类型
const (
	CaptchaTypeClick  string = "click"
	CaptchaTypeSlide  string = "slide"
	CaptchaTypeRotate string = "rotate"
)

// InitClickCaptcha
// @Description 初始化点选验证码
// @Author Ham 2025-02-19 20:42:43
func InitClickCaptcha() {
	builder := click.NewBuilder()

	// fonts
	fonts, err := fzshengsksjw.GetFont()
	if err != nil {
		log.Fatalln(err)
	}

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	builder.SetResources(
		click.WithChars(chars.GetChineseChars()),
		click.WithFonts([]*truetype.Font{fonts}),
		click.WithBackgrounds(imgs),
	)

	textCapt := builder.Make()
	ClickCaptcha = &textCapt
}

// InitSlideCaptcha
// @Description 初始化滑动验证码
// @Author Ham 2025-02-19 20:45:06
func InitSlideCaptcha() {
	builder := slide.NewBuilder(
		//slide.WithGenGraphNumber(2),
		slide.WithEnableGraphVerticalRandom(true),
	)

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	graphs, err := tiles.GetTiles()
	if err != nil {
		log.Fatalln(err)
	}

	var newGraphs = make([]*slide.GraphImage, 0, len(graphs))
	for i := 0; i < len(graphs); i++ {
		graph := graphs[i]
		newGraphs = append(newGraphs, &slide.GraphImage{
			OverlayImage: graph.OverlayImage,
			MaskImage:    graph.MaskImage,
			ShadowImage:  graph.ShadowImage,
		})
	}

	// set resources
	builder.SetResources(
		slide.WithGraphImages(newGraphs),
		slide.WithBackgrounds(imgs),
	)

	slideCapt := builder.Make()
	SlideCaptcha = &slideCapt
}

// InitRotateCaptcha
// @Description 初始化旋转验证码
// @Author Ham 2025-02-19 20:50:14
func InitRotateCaptcha() {
	builder := rotate.NewBuilder(rotate.WithRangeAnglePos([]option.RangeVal{
		{Min: 20, Max: 330},
	}))

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	// set resources
	builder.SetResources(
		rotate.WithImages(imgs),
	)

	rotateCapt := builder.Make()
	RotateCaptcha = &rotateCapt
}

// InitCaptcha
// @Description 初始化验证码
// @Author Ham 2025-02-19 20:50:27
func InitCaptcha() {
	InitClickCaptcha()
	InitSlideCaptcha()
	InitRotateCaptcha()
}

之后在main方法里面,调用initCaptcha(),初始化验证码

验证码生成和校验工具

参考作者demo编写了以下代码,第一次看作者的demo可能有点一头雾水,没有注释,可以看看下面的

package util

import (
	"backend/internal/common"
)

// 生成验证码时调用下面的GenerateCaptcha方法,传入验证码类型,然后将captchaData返回给前端,用于生成验证码

// GenerateCaptcha
// @Description 生成验证码
// @Author Ham 2025-02-19 21:00:43
// @Param captchaType 验证码类型
// @Return captchaData 验证码数据
func GenerateCaptcha(captchaType string) (captchaData map[string]interface{}, err error) {
	switch captchaType {
	// 点选验证码
	case common.CaptchaTypeClick:
		{
			captcha := *common.ClickCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			thumbImageBase64, err := generatedCaptcha.GetThumbImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"thumbImage":  thumbImageBase64,
				"masterImage": masterImageBase64,
			}
	                // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	//	滑动验证码
	case common.CaptchaTypeSlide:
		{
			captcha := *common.SlideCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			tileImageBase64, err := generatedCaptcha.GetTileImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"tileImage":   tileImageBase64,
				"masterImage": masterImageBase64,
                                "thumbX":      generatedCaptcha.GetData().TileX,
		                "thumbY":      generatedCaptcha.GetData().TileY,
		                "thumbWidth":  generatedCaptcha.GetData().Width,
		                "thumbHeight": generatedCaptcha.GetData().Height,
			}
                        // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	// 旋转验证码
	case common.CaptchaTypeRotate:
		{
			captcha := *common.RotateCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			thumbImageBase64, err := generatedCaptcha.GetThumbImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"thumbImage":  thumbImageBase64,
				"masterImage": masterImageBase64,
			}
                        // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	default:
		{
			return
		}
	}
}

// 下面是校验的演示,这里是用了redis,业务中,请替换自己的缓存逻辑,校验时调用ValidateCaptcha即可

// ValidateClickCaptcha
// @Description 校验点选验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateClickCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	rightCaptchaData := map[int]*click.Dot{}
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	dotsInterface := captchaData["dots"].([]interface{})
	dots := make([]float64, len(dotsInterface))
	for i, v := range dotsInterface {
		dots[i], _ = v.(float64) // 将每个元素转换为float64
	}
	// 点数*2 为x,y坐标总数,数量应与请求传来的x,y坐标数相同
	if (len(rightCaptchaData) * 2) == len(dots) {
		for i := 0; i < len(rightCaptchaData); i++ {
			dot := rightCaptchaData[i]
			j := i * 2
			k := i*2 + 1
			sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", dots[j]), 64)
			sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", dots[k]), 64)

			validated = click.CheckPoint(int64(sx), int64(sy), int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0)
			if !validated {
				break
			}
		}
	}

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateSlideCaptcha
// @Description 校验滑动验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateSlideCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	var rightCaptchaData *slide.Block
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	pointsInterface := captchaData["points"].([]interface{})
	points := make([]float64, len(pointsInterface))
	for i, v := range pointsInterface {
		points[i], _ = v.(float64) // 将每个元素转换为float64
	}
	if 2 == len(points) {
		sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", points[0]), 64)
		sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", points[1]), 64)
		validated = slide.CheckPoint(int64(sx), int64(sy), int64(rightCaptchaData.X), int64(rightCaptchaData.Y), 4)
	}

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateRotateCaptcha
// @Description 校验旋转验证码
// @Author Ham 2025-02-20 14:14:07
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateRotateCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	var rightCaptchaData *rotate.Block
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	angle := captchaData["angle"].(float64)
	sAngle, _ := strconv.ParseFloat(fmt.Sprintf("%v", angle), 64)
	validated = rotate.CheckAngle(int64(sAngle), int64(rightCaptchaData.Angle), 2)

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateCaptcha
// @Description 校验验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaType 验证码类型
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateCaptcha(captchaType string, captchaData map[string]interface{}) (validated bool, err error) {
	switch captchaType {
	case CaptchaTypeClick:
		return ValidateClickCaptcha(captchaData)
	case CaptchaTypeSlide:
		return ValidateSlideCaptcha(captchaData)
	case CaptchaTypeRotate:
		return ValidateRotateCaptcha(captchaData)
	default:
		return false, common.NewError("不存在的验证码类型")
	}
}

以上三种校验方法的形参captchaData分别为

// 点选验证码:
{
    uuid (string),
    dots ([]int)
}

// 滑动验证码
{
    uuid (string),
    points ([]int)
}

// 旋转验证码
{
    uuid (string),
    angle (int)
}

校验时前端像上面这样传数据来校验就可以了

uuid在前端生成验证码时请求后端生成验证码接口获取,

dots,points,angle的计算方法参考下面每一个demo的confirmEvent方法

go-captcha-example/web/vue/src/hooks at master · wenlng/go-captcha-example

]]>
https://www.devdeg.com/188.html/feed1
Nuxt3使用keepalive缓存页面后, 切换不同布局, watch可能重复调用的问题https://www.devdeg.com/170.htmlhttps://www.devdeg.com/170.html#respondSat, 04 Jan 2025 07:30:13 +0000https://www.devdeg.com/?p=170Nuxt3对于页面缓存的实现很简单, 在app.vue的NuxtPage组件上添加一个keepalive就可以实现了

但是如果你的两个页面是不同的布局, 这个缓存可能会失效, 同时也会带来一个小问题

以下是一个简单的页面

通过点击上面的模块按钮, 切换模块, 这里会监听模块的切换, 输出”我被请求了”

目前是没有问题的, 点击切换, 方法只会执行一次

现在跳转到另一个布局不同的页面, 比如我的登录页面

再回到首页, 现在点击一次切换, 会出现方法被调用两次的情况

问题推测

之前我就切换布局, KeepAlive失效问题问过Nuxt开发者, 他的回复大概意思就是:

KeepAlive如果自身被重新创建(通过渲染新的/不同的布局),则无法保留组件实例

这是 Vue 的限制

所以我推测问题大概就是切换布局后 , 页面缓存失效了, 但是watch没有失效, 还在继续监听

当然, 在组件失效后不手动移除监听是我书写的不规范, 如果你有记得移除, 就不会遇到我这种问题

问题解决

利用onActivated, onDeactivated钩子, 在页面缓存失活后, 将监听移除, 页面缓存存活时开启监听,

并直接调用startWatch确保刚加载进页面时, 监听也可以正常工作

// 监听当前模块
let watching
const startWatch = () => {
    watching = watch(currentModule, () => {
        getModuleContent()
    })
}
const stopWatch = () => {
    if (watching) {
        watching()
    }
}
startWatch()
onActivated(() => {
    startWatch()
})
onDeactivated(() => {
    stopWatch()
})

经过这样修改后, 经过测试, 刚进入页面或者跳转到其他布局任意的页面再回来, watch都可以正常监听, 不会重复工作

写完这个文章后, 又经过我的后来多次测试, 我感觉Nuxt3的keepalive + 多布局会是一个大坑, 如果你有多个布局, 并且多处使用监听器, 我建议不要用keepalive, 可能会有意想不到的问题, 如果你有更好的解决方法, 欢迎在评论区回复一下, 互相学习😉

]]>
https://www.devdeg.com/170.html/feed0
Nuxt3 使用Arco Design组件库简单地实现动态切换主色https://www.devdeg.com/161.htmlhttps://www.devdeg.com/161.html#respondTue, 31 Dec 2024 14:00:38 +0000https://www.devdeg.com/?p=161Arco Design的主颜色默认使用css变量指向了他们的Arco Blue 蓝色

最近刚好需要在前端打包完以后也能根据后台的颜色配置替换主色, 如果你也需要, 这个文章获取可以帮到你

所以只要在body上面替换这些变量就可以轻松的实现替换Arco Design主色

我的思路就是我们输入一个主色, 然后利用js生成10个色阶, 将10个颜色替换进去就可以了

刚好Arco团队还有一个颜色库, 利用算法生成包含 10 种颜色的渐变色板, 还适用于浅色和深色模式

实现替换Arco Design主色

1. 安装以下依赖

npm i @arco-design/color

简单使用:

let colors // 颜色数组, 用于存放10个颜色
colors = generate("red", { // 根据主颜色生成10个色阶
        list: true, // 生成列表
        dark: true // 是否为深色模式
    })

以上方法就会生成10个基于红色的色阶, 打印出来是这样的

而且dark如果为false的话, 生成出来的10个颜色也是不一样的, 所以这个库生成的颜色对浅色和深色模式都可以用

然后我们就可以将这10个颜色放在body上面, 用于覆盖Arco Design的默认主色

2. Nuxt3参考以下方法 (ssr友好)

export const initPrimaryColor = (color) => {
    // 初始化主颜色css变量
    let primaryColors = []

    // 当前主题 (这里写死了,实际根据你的当前主题)
    const theme = 'dark'

    primaryColors = generate(color, { // 根据主颜色生成10个色阶
        list: true, // 生成列表
        dark: theme == 'dark ' // 是否为深色模式
    })
    primaryColors = primaryColors.map(color => getRgbStr(color)) // 提取字符串中的 r、g、b 值

    useHead({
        bodyAttrs: {
            style: `
                --primary-1: ${primaryColors[0]};
                --primary-2: ${primaryColors[1]};
                --primary-3: ${primaryColors[2]};
                --primary-4: ${primaryColors[3]};
                --primary-5: ${primaryColors[4]};
                --primary-6: ${primaryColors[5]};
                --primary-7: ${primaryColors[6]};
                --primary-8: ${primaryColors[7]};
                --primary-9: ${primaryColors[8]};
                --primary-10: ${primaryColors[9]};
              `
        }
    })
}

如果你没有用ssr框架, 可以直接用那10个颜色遍历, 往body上面添加变量, 例如下面这样

document.body.style.setProperty(`--primary-序号`, `值`)

3. 效果

经过以上操作, 可以轻而易举地替换主色, 在程序打包完以后也可以利用接口获取主色来替换前端的主色

参考上面的思路, Arco的其他css变量也可以这样替换😎, 虽然这样实现很粗暴, 但是确实很简单的

]]>
https://www.devdeg.com/161.html/feed0
Nuxt3按需引入Arco Designhttps://www.devdeg.com/158.htmlhttps://www.devdeg.com/158.html#respondSun, 29 Dec 2024 09:19:51 +0000https://www.devdeg.com/?p=158前段时间打算用NaiveUI的, 但是今天写着写着发现他们的响应式栅格在SSR框架会出现抖动和水合的问题, 应该有解决办法, 但是我搞不来, 只好换一下框架😂

引入步骤

1. 安装依赖

# npm
npm install --save-dev @arco-design/web-vue
# yarn
yarn add --dev @arco-design/web-vue

2. 安装Arco的插件

Arco的按需引入有两种:

推荐第二种,第一种据官方描述,如果手动导入的组件,需要自己再手动导入对应样式

执行以下安装按需引入插件

# npm
npm install @arco-plugins/vite-vue
# yarn
yarn add @arco-plugins/vite-vue

3. 配置nuxt.config.ts

import { vitePluginForArco } from '@arco-plugins/vite-vue'

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    plugins: [
      vitePluginForArco({
        style: 'css'
      })
    ]
  },
})

4. 这样按需引入就完成了😊

]]>
https://www.devdeg.com/158.html/feed0
Golang使用Garble混淆打包及遇到的问题https://www.devdeg.com/155.htmlhttps://www.devdeg.com/155.html#commentsFri, 27 Dec 2024 05:01:08 +0000https://www.devdeg.com/?p=155最近在用Gin框架+Gorm来写接口查数据库

想实现混淆打包的二进制文件, 避免被轻易反编译, 就发现了Garble

安装Garble教程

确保Go环境已经装好

运行下面的命令

go install mvdan.cc/garble@latest

如果你配了GoPath的话, Garble的可执行文件会在你的$GOPATH/bin目录下, 之后把这个文件剪切到你的Go环境安装目录下的bin目录下, 此时如果你已经给Go配置了环境变量, 打开cmd输入

garble version

可以看到以下, 代表安装并配置全局可用

Garble的使用

常用命令

garble build

用于混淆和构建 Go 程序

示例

garble build main.go
garble test

混淆并运行测试文件

示例

garble test main.go
garble version

检查当前安装的 garble 版本

示例

garble version

常用参数

-literals

混淆文字常量(如字符串、数字等)

示例

garble -literals build main.go
-seed=

指定混淆使用的种子

默认值:固定种子,生成的混淆结果可重复

示例

使用随机种子:

garble -seed=random build main.go

指定固定种子:

garble -seed=12345 build main.go
-debugdir=

将混淆前的源码或文件输出到指定路径,用于调试

示例

garble -debugdir=./debug build main.go
-tiny

进一步缩短混淆后的名称

示例

garble -tiny build main.go
-debug

打印调试信息,用于查看混淆的过程

示例

garble -debug build main.go
-ldflags

传递给 Go 构建器的 ldflags 参数,用于优化二进制文件大小

-s: 去除符号表 -w: 去除 DWARF 调试信息

示例

garble build -ldflags "-s -w" main.go

遇到的问题

打包后, 运行二进制文件, 发现Gorm的AutoMigrate不好用了, 我要用这个方法检查数据库结构和代码里面的结构是否相同并修正

通过查询Github的文章, 发现是要给那个结构体加上一行代码, 避免Garble将它混淆

在你的实体类里面加上这一行( Category要改成你的结构体 )

// 忽略garble混淆, 确保 GORM 的反射机制能够正常工作
var _ = reflect.TypeOf(Category{})

加上了之后, Gorm的反射就可以正常工作了

这段代码通过引用类型信息,告诉 Garble 需要保留特定结构体的字段和元信息,以免在运行时出现问题(例如,GORM 中动态解析表结构时的错误), 如果其他的库用到了反射, 涉及到某个结构体, 只需要加上上面这一行代码就可以了

参考文章:

using GORM results in obfuscated table names due to its indirect use of reflection · Issue #554 · burrowers/garble

]]>
https://www.devdeg.com/155.html/feed2
Nuxt3使用Naive UI解决刷新页面样式延迟加载问题https://www.devdeg.com/148.htmlhttps://www.devdeg.com/148.html#respondWed, 25 Dec 2024 12:14:44 +0000https://www.devdeg.com/?p=148最近开始用这个组件库, 在按照官网文档搞完按需自动引入后, 发现首页出现刚进入时会没有样式

像上图这样

错误原因

可能是因为nuxtjs-naive-ui这个库太久没更新来适配Nuxt, 上次更新是7个月前, 我看到有人提交修复, 但是还没有合并到分支

临时解决方法

然后在github找了很久解决办法, 最终找到一个办法

在plugins目录下建naive-ui.ts

添加以下代码

import { setup } from '@css-render/vue3-ssr'
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin(nuxtApp => {
  if (import.meta.server) {
    const { collect } = setup(nuxtApp.vueApp)
    nuxtApp.ssrContext!.head.push({
      style: () =>
        collect()
          .split('</style>')
          .map(block => {
            const id = block.match(/cssr-id="(.+?)"/)?.[1]
            const style = (block.match(/>(.*)/s)?.[1] || '').trim()
            return {
              ['cssr-id']: id,
              innerHTML: style
            }
          })
    })
  }
})

然后刷新页面, 发现组件样式可以正常加载

但是又有新的问题, 控制台又报错了

ssr:warn [Vue warn]: App already provides property with key "@css-render/vue3-ssr". It will be overwritten with the new value. 
  at <anonymous> (E:\Users\Administrator\Documents\VSCode Projects\shortcut-frontend\node_modules\nuxt\dist\core\runtime\nitro\dev-server-logs.js)
  at Object.log (E:\Users\Administrator\Documents\VSCode Projects\shortcut-frontend\node_modules\nuxt\dist\core\runtime\nitro\dev-server-logs.js)
  at warn$1 (E:\Users\Administrator\Documents\VSCode Projects\shortcut-frontend\node_modules\@vue\runtime-core\dist\runtime-core.cjs.js)
  at Object.provide (E:\Users\Administrator\Documents\VSCode Projects\shortcut-frontend\node_modules\@vue\runtime-core\dist\runtime-core.cjs.js)
  at setup (E:\Users\Administrator\Documents\VSCode Projects\shortcut-frontend\node_modules\@css-render\vue3-ssr\lib\index.js)

参考了另外一个帖子后, 直接把nuxtjs-naive-ui删了就解决了

图中高亮位置可以删了, 官网的Nuxtjs自动按需引入配置可以保留, 依然生效

参考链接:

Nuxt3.7 组件样式没有在服务端生成的html内 · Issue #5220 · tusen-ai/naive-ui

<style> tags are not generated in SSR since Nuxt 3.12 · Issue #4 · 07akioni/nuxtjs-naive-ui

]]>
https://www.devdeg.com/148.html/feed0
Golang对象拷贝库推荐 – Copierhttps://www.devdeg.com/146.htmlhttps://www.devdeg.com/146.html#respondMon, 23 Dec 2024 08:49:25 +0000https://www.devdeg.com/?p=146最近在学习Golang, 之前在SpringBoot写接口时, service层常常需要entity数据库类转换为dto, 之前在Java我会使用Mapstruct库, 确实很方便, 写几个接口就完成了

然后在go写接口的时候, 就找了一下有没有类似Mapstruct这样的库, 没找到这么方便的, 不过找到一个Copier库, 用起来也还不错

安装

go get github.com/jinzhu/copier

使用

这是我在logic层的一个方法, 我认为是类似于springboot 中service实现类的一个方法, 在这里我将数据库模型entity批量转成了业务模型dto

copier.Copy支持单个/批量转换

单个对象复制:

使用 copier.Copy(&target, &source) 来复制一个对象

批量复制:

使用 copier.Copy(&target, &source) 来批量复制对象数组(或切片)

]]>
https://www.devdeg.com/146.html/feed0
IDEA类注释、方法注释模板https://www.devdeg.com/134.htmlhttps://www.devdeg.com/134.html#commentsMon, 16 Dec 2024 03:26:08 +0000https://www.devdeg.com/?p=134类注释模板

在创建类时,会添加一些默认的信息

如何添加

点击File,来到Settings

然后按照下图点击操作

/**
 * @description:
 * @author: Ham
 * @date: ${DATE} ${TIME}
 */   

复制点击Apply 关闭就可以了

方法注释模板

使用快捷键 /** + 回车,注释会添加一些默认信息

如何添加

还是来到设置

找到上面这个设置,点击 + ,点击Template Group添加一个模板组,随便命名即可

然后点击新添加的模板组,点击 + ,添加一个模板

按照下面填写即可

⚠注意下面的可用(Applicable)于截图时忘记标了,记得选一下Java

复制以下进Template Text

*
 $param$
 * @return: $return$
 * @description:
 * @author: Ham 
 * @date: $date$ $time$
 **/

最后点击 Edit Variables,定义变量

第一个:

groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) { result+='* @param: ' + params[i] + ((i < params.size() - 1) ? '\\n ' : '')};return result", methodParameters())  

其他三个按照截图选择即可

第一个是方法参数,填写这个脚本可以实现多个参数多行显示

都添加完点击Apply 应用就可以了

]]>
https://www.devdeg.com/134.html/feed1