feat(vision): 具名化 JS 导出并动态包裹错误(by AI)

This commit is contained in:
AI Engineer 2026-06-21 10:18:46 +08:00
parent 9f06dbedb4
commit 84e72eec65
4 changed files with 215 additions and 123 deletions

View File

@ -1,5 +1,9 @@
# CHANGELOG - apigo.cc/go/vision
## v1.5.3 (2026-06-21)
- **JS 对齐**: 重构 JS 导出为具名函数,并引入 `jsmod.MakeError` 动态包装错误以获取调用栈。
- **依赖更新**: 升级依赖 `jsmod``v1.5.3``cast``v1.5.3``rand``v1.5.3``encoding``v1.5.4``safe``v1.5.2``file``v1.5.5`
## v1.5.2 (2026-06-11)
- **版本对齐**: 基础设施全局对齐 v1.5.2。

10
TEST.md
View File

@ -37,9 +37,13 @@ go test -bench .
| 测试项 | 耗时 (ns/op) |
| :--- | :--- |
| **WarpPerspective** | 7,079,540 |
| **PHash** | 958,618 |
| **ExtractPalette** | 402,176 |
| **WarpPerspective** | 8,928,754 |
| **PHash** | 1,041,925 |
| **ExtractPalette** | 453,400 |
## 🛡️ 鲁棒性防御 (Robustness)
- **可选参数提示**`New``Save` 等方法接受指针作为可选参数,结合最新的 `go/js` 引擎智能过滤空参数。
- **JS 错误调用栈**JS 桥接层改用具名导出并使用 `jsmod.MakeError` 包裹错误,确保 JS 抛出异常时携带准确的 Go 运行时堆栈。
---
所有测试均遵循 `@go` 基础设施标准,无外部系统依赖(除 FFmpeg 视频测试外,该部分会自动跳过或提示引导)。

12
go.mod
View File

@ -3,10 +3,10 @@ module apigo.cc/go/vision
go 1.25.0
require (
apigo.cc/go/cast v1.5.2
apigo.cc/go/file v1.5.4
apigo.cc/go/jsmod v1.5.2
apigo.cc/go/rand v1.5.2
apigo.cc/go/cast v1.5.3
apigo.cc/go/file v1.5.5
apigo.cc/go/jsmod v1.5.3
apigo.cc/go/rand v1.5.3
github.com/boombuler/barcode v1.1.0
github.com/disintegration/imaging v1.6.2
github.com/flopp/go-findfont v0.1.0
@ -17,8 +17,8 @@ require (
)
require (
apigo.cc/go/encoding v1.5.3
apigo.cc/go/safe v1.5.1
apigo.cc/go/encoding v1.5.4
apigo.cc/go/safe v1.5.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/kr/text v0.2.0 // indirect
golang.org/x/crypto v0.52.0 // indirect

View File

@ -10,113 +10,167 @@ import (
func init() {
jsmod.Register("vision", map[string]any{
// 基础操作
"Load": func(ctx context.Context, path string) (*jsCanvas, error) {
"Load": jsLoad,
"New": jsNew,
"Save": jsSave,
// 生成类
"GenerateQRCode": jsGenerateQRCode,
"GenerateBarcode": jsGenerateBarcode,
"GenerateCaptcha": jsGenerateCaptcha,
// 预览类 (直接写文件)
"GenerateImagePreview": jsGenerateImagePreview,
"GenerateVideoPreview": jsGenerateVideoPreview,
// 转换类
"Convert": jsConvert,
"Optimize": jsOptimize,
// 辅助工具
"LoadFonts": jsLoadFonts,
"Recognize": jsRecognize,
})
}
func jsLoad(ctx context.Context, path string) (*jsCanvas, error) {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return nil, err
return nil, jsmod.MakeError(err)
}
c, err := Load(p)
if err != nil {
return nil, err
return nil, jsmod.MakeError(err)
}
return &jsCanvas{ctx: ctx, c: c}, nil
},
"New": func(ctx context.Context, w, h int, bg *string) *jsCanvas {
}
func jsNew(ctx context.Context, w, h int, bg *string) *jsCanvas {
if bg != nil {
return &jsCanvas{ctx: ctx, c: New(w, h, *bg)}
}
return &jsCanvas{ctx: ctx, c: New(w, h)}
},
"Save": func(ctx context.Context, j *jsCanvas, path string, quality *int) error {
}
func jsSave(ctx context.Context, j *jsCanvas, path string, quality *int) error {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
return jsmod.MakeError(err)
}
if quality != nil {
return Save(j.c, p, *quality)
if err := Save(j.c, p, *quality); err != nil {
return jsmod.MakeError(err)
}
return Save(j.c, p)
},
return nil
}
if err := Save(j.c, p); err != nil {
return jsmod.MakeError(err)
}
return nil
}
// 生成类
"GenerateQRCode": func(ctx context.Context, content string, size int) (*jsCanvas, error) {
func jsGenerateQRCode(ctx context.Context, content string, size int) (*jsCanvas, error) {
c, err := GenerateQRCode(content, size)
if err != nil {
return nil, err
return nil, jsmod.MakeError(err)
}
return &jsCanvas{ctx: ctx, c: c}, nil
},
"GenerateBarcode": func(ctx context.Context, content string, w, h int) (*jsCanvas, error) {
}
func jsGenerateBarcode(ctx context.Context, content string, w, h int) (*jsCanvas, error) {
c, err := GenerateBarcode(content, w, h)
if err != nil {
return nil, err
return nil, jsmod.MakeError(err)
}
return &jsCanvas{ctx: ctx, c: c}, nil
},
"GenerateCaptcha": func(ctx context.Context, opt *CaptchaOption) *jsCanvas {
return &jsCanvas{ctx: ctx, c: GenerateCaptcha(opt)}
},
}
// 预览类 (直接写文件)
"GenerateImagePreview": func(ctx context.Context, src, out string, w, h int) error {
func jsGenerateCaptcha(ctx context.Context, opt *CaptchaOption) *jsCanvas {
return &jsCanvas{ctx: ctx, c: GenerateCaptcha(opt)}
}
func jsGenerateImagePreview(ctx context.Context, src, out string, w, h int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil {
return err
return jsmod.MakeError(err)
}
pOut, err := file.VerifyPathForSafeMode(ctx, out)
if err != nil {
return err
return jsmod.MakeError(err)
}
return GenerateImagePreview(pSrc, pOut, w, h)
},
"GenerateVideoPreview": func(ctx context.Context, src, out string, w, h int, interval *int) error {
if err := GenerateImagePreview(pSrc, pOut, w, h); err != nil {
return jsmod.MakeError(err)
}
return nil
}
func jsGenerateVideoPreview(ctx context.Context, src, out string, w, h int, interval *int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil {
return err
return jsmod.MakeError(err)
}
pOut, err := file.VerifyPathForSafeMode(ctx, out)
if err != nil {
return err
return jsmod.MakeError(err)
}
if interval != nil {
return GenerateVideoPreview(pSrc, pOut, w, h, *interval)
if err := GenerateVideoPreview(pSrc, pOut, w, h, *interval); err != nil {
return jsmod.MakeError(err)
}
return GenerateVideoPreview(pSrc, pOut, w, h)
},
return nil
}
if err := GenerateVideoPreview(pSrc, pOut, w, h); err != nil {
return jsmod.MakeError(err)
}
return nil
}
// 转换类
"Convert": func(ctx context.Context, src, dst string, quality *int) error {
func jsConvert(ctx context.Context, src, dst string, quality *int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil {
return err
return jsmod.MakeError(err)
}
pDst, err := file.VerifyPathForSafeMode(ctx, dst)
if err != nil {
return err
return jsmod.MakeError(err)
}
if quality != nil {
return Convert(pSrc, pDst, *quality)
if err := Convert(pSrc, pDst, *quality); err != nil {
return jsmod.MakeError(err)
}
return Convert(pSrc, pDst)
},
"Optimize": func(ctx context.Context, path string, maxWidth int, quality int) error {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return err
return nil
}
return Optimize(p, maxWidth, quality)
},
if err := Convert(pSrc, pDst); err != nil {
return jsmod.MakeError(err)
}
return nil
}
// 辅助工具
"LoadFonts": LoadFonts,
"Recognize": func(ctx context.Context, path string) (string, error) {
func jsOptimize(ctx context.Context, path string, maxWidth int, quality int) error {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return "", err
return jsmod.MakeError(err)
}
return Recognize(p)
},
})
if err := Optimize(p, maxWidth, quality); err != nil {
return jsmod.MakeError(err)
}
return nil
}
func jsLoadFonts(paths ...string) {
LoadFonts(paths...)
}
func jsRecognize(ctx context.Context, path string) (string, error) {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return "", jsmod.MakeError(err)
}
res, err := Recognize(p)
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}
// jsCanvas 包装器,支持链式调用
@ -131,12 +185,18 @@ func (j *jsCanvas) Height() int { return j.c.Height() }
func (j *jsCanvas) Save(path string, quality *int) error {
p, err := file.VerifyPathForSafeMode(j.ctx, path)
if err != nil {
return err
return jsmod.MakeError(err)
}
if quality != nil {
return Save(j.c, p, *quality)
if err := Save(j.c, p, *quality); err != nil {
return jsmod.MakeError(err)
}
return Save(j.c, p)
return nil
}
if err := Save(j.c, p); err != nil {
return jsmod.MakeError(err)
}
return nil
}
// 效果与绘图 (返回自身以支持链式)
@ -150,8 +210,14 @@ func (j *jsCanvas) FlipV() *jsCanvas { j.c.FlipV(); return j }
func (j *jsCanvas) Sharpen(sigma float64) *jsCanvas { j.c.Sharpen(sigma); return j }
func (j *jsCanvas) AdjustBrightness(p float64) *jsCanvas { j.c.AdjustBrightness(p); return j }
func (j *jsCanvas) Rect(x, y, w, h float64, opt *DrawStyle) *jsCanvas { j.c.Rect(x, y, w, h, opt); return j }
func (j *jsCanvas) Circle(x, y, r float64, opt *DrawStyle) *jsCanvas { j.c.Circle(x, y, r, opt); return j }
func (j *jsCanvas) Rect(x, y, w, h float64, opt *DrawStyle) *jsCanvas {
j.c.Rect(x, y, w, h, opt)
return j
}
func (j *jsCanvas) Circle(x, y, r float64, opt *DrawStyle) *jsCanvas {
j.c.Circle(x, y, r, opt)
return j
}
func (j *jsCanvas) Line(x1, y1, x2, y2 float64, opt *DrawStyle) *jsCanvas {
j.c.Line(x1, y1, x2, y2, opt)
return j
@ -162,6 +228,24 @@ func (j *jsCanvas) DrawText(x, y float64, text string, opt *TextOption) *jsCanva
}
// 识别类
func (j *jsCanvas) DecodeAll() (string, error) { return j.c.DecodeAll() }
func (j *jsCanvas) Recognize() (string, error) { return j.c.Recognize() }
func (j *jsCanvas) DecodeQRCode() (string, error) { return j.c.DecodeQRCode() }
func (j *jsCanvas) DecodeAll() (string, error) {
res, err := j.c.DecodeAll()
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}
func (j *jsCanvas) Recognize() (string, error) {
res, err := j.c.Recognize()
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}
func (j *jsCanvas) DecodeQRCode() (string, error) {
res, err := j.c.DecodeQRCode()
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}