本文基于 apps-schedulercat-led 两个实际项目,介绍懒猫微服 Go-SDK 的使用方法。

SDK 简介

懒猫微服 Go-SDK(gitee.com/linakesi/lzc-sdk)是懒猫微服平台提供的 Go 语言 SDK,允许应用通过 gRPC 与懒猫微服系统交互。SDK 封装了以下核心能力:

  • 应用管理(PkgManager):查询、启动、暂停应用
  • 用户管理(Users):查询用户信息
  • 设备管理(Box):查询设备信息、控制 LED、关机/重启

项目结构

使用 Go-SDK 开发的懒猫应用推荐采用如下项目结构:

your-app/
├── cmd/
│   └── your-app/
│       └── main.go               # 应用入口
├── internal/
│   ├── web/
│   │   └── server.go             # Web 服务器配置与路由
│   ├── handlers/
│   │   ├── app.go                # SDK 调用相关 handler
│   │   └── userinfo.go           # 用户信息 handler
│   ├── biz/
│   │   └── usecase.go            # 业务逻辑与数据库操作
│   ├── auth/
│   │   └── oidc.go               # OIDC 认证
│   └── ent/
│       └── schema/               # ent ORM schema 定义
├── go.mod
├── manifest.yml                   # 懒猫应用清单
├── lzc-deploy-params.yml          # 部署参数配置(可选)
├── lzc-build.yml                  # 构建配置
└── icon.png                       # 应用图标

依赖配置

go.mod 中添加 SDK 依赖:

module your-app

go 1.24.0

require (
    gitee.com/linakesi/lzc-sdk v0.0.0-20250307093731-41fc0a4beab9
    google.golang.org/grpc v1.63.2
)

SDK 通过 gRPC 与系统通信,因此也需要 google.golang.org/grpc 依赖。

核心概念:APIGateway

SDK 的所有功能都通过 APIGateway 对象来访问。使用模式固定为三步:创建 → 调用 → 关闭。

import (
    gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
    "google.golang.org/grpc/metadata"
)

// 1. 准备 Context,附加用户标识
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)

// 2. 创建 APIGateway
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
    return err
}
defer gw.Close()  // 3. 确保关闭

// 通过 gw 调用各种服务
// gw.PkgManager  - 应用管理
// gw.Users       - 用户管理
// gw.Box         - 设备管理

每次调用 SDK 都需要创建新的 APIGateway 实例并在使用完毕后关闭。务必使用 defer gw.Close() 避免资源泄漏。

Context Metadata

SDK 使用 gRPC metadata 传递用户身份信息。最关键的是 x-hc-user-id

import "google.golang.org/grpc/metadata"

ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)

如果不传递 x-hc-user-id,某些需要用户上下文的 API 可能无法正常工作。

应用管理 API

查询应用列表

import "gitee.com/linakesi/lzc-sdk/lang/go/sys"

resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{})
if err != nil {
    return err
}

for _, info := range resp.InfoList {
    fmt.Println("应用ID:", info.Appid)
    fmt.Println("状态:", info.Status.String())           // Installed, NotInstalled 等
    fmt.Println("实例状态:", info.InstanceStatus.String()) // Running, Stopped, Starting 等

    // 注意:Title, Icon, Version, Builtin 是指针类型,可能为 nil
    if info.Title != nil {
        fmt.Println("标题:", *info.Title)
    }
    if info.Icon != nil {
        fmt.Println("图标:", *info.Icon)
    }
    if info.Version != nil {
        fmt.Println("版本:", *info.Version)
    }
    fmt.Println("是否多实例:", info.MultiInstance)
    if info.Builtin != nil {
        fmt.Println("是否内置:", *info.Builtin)
    }
}

也可以指定查询特定应用:

resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{
    AppidList: []string{"com.example.app1", "com.example.app2"},
})

启动应用

_, err = gw.PkgManager.Resume(ctx, &sys.AppInstance{
    Appid: appID,
    Uid:   userID,
})

暂停应用

_, err = gw.PkgManager.Pause(ctx, &sys.AppInstance{
    Appid: appID,
    Uid:   userID,
})

启动前检查状态(推荐)

直接调用 Resume/Pause 前,先查询应用状态可以避免不必要的错误:

func resumeApp(ctx context.Context, appID, userID string) error {
    ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)

    gw, err := gohelper.NewAPIGateway(ctx)
    if err != nil {
        return err
    }
    defer gw.Close()

    // 查询当前状态
    resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{
        AppidList: []string{appID},
    })
    if err != nil {
        return err
    }
    if len(resp.InfoList) == 0 {
        return fmt.Errorf("application %s not found", appID)
    }

    appInfo := resp.InfoList[0]

    // 检查是否已安装
    if appInfo.Status != sys.AppStatus_Installed {
        return fmt.Errorf("application %s is not installed (status: %s)", appID, appInfo.Status.String())
    }

    // 已运行则直接返回(幂等)
    if appInfo.InstanceStatus == sys.InstanceStatus_Status_Running {
        return nil
    }
    // 启动中则等待
    if appInfo.InstanceStatus == sys.InstanceStatus_Status_Starting {
        return nil
    }

    // 调用 Resume
    _, err = gw.PkgManager.Resume(ctx, &sys.AppInstance{
        Appid: appID,
        Uid:   userID,
    })
    return err
}

用户管理 API

查询用户信息

import "gitee.com/linakesi/lzc-sdk/lang/go/common"

userInfo, err := gw.Users.QueryUserInfo(ctx, &common.UserID{Uid: userID})
if err == nil && userInfo != nil {
    fmt.Println("昵称:", userInfo.Nickname)
    fmt.Println("头像:", userInfo.Avatar)
}

设备管理 API

查询设备信息

boxInfo, err := gw.Box.QueryInfo(ctx, nil)
if err != nil {
    return err
}
fmt.Println("LED 状态:", boxInfo.PowerLed)

控制 LED

import users "gitee.com/linakesi/lzc-sdk/lang/go/common"

// 开启 LED
_, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
    PowerLed: true,
})

// 关闭 LED
_, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
    PowerLed: false,
})

设备关机/重启

// 关机
_, err = gw.Box.Shutdown(ctx, &users.ShutdownRequest{
    Action: users.ShutdownRequest_Poweroff,
})

// 重启
_, err = gw.Box.Shutdown(ctx, &users.ShutdownRequest{
    Action: users.ShutdownRequest_Reboot,
})

OIDC 认证

懒猫微服为每个应用注入 OIDC 认证信息。应用需要实现 OIDC 认证流程来获取用户身份。

环境变量

懒猫微服会自动注入以下环境变量:

环境变量 说明
LAZYCAT_AUTH_OIDC_CLIENT_ID OIDC Client ID
LAZYCAT_AUTH_OIDC_CLIENT_SECRET OIDC Client Secret
LAZYCAT_AUTH_OIDC_AUTH_URI OIDC 授权端点
LAZYCAT_AUTH_OIDC_TOKEN_URI OIDC Token 端点
LAZYCAT_AUTH_OIDC_USERINFO_URI OIDC 用户信息端点
LAZYCAT_APP_DOMAIN 应用域名

获取用户身份

有两种方式获取用户身份:

方式一:通过 HTTP Header(系统注入)

懒猫微服网关会自动在请求头中注入 x-hc-user-idx-hc-user-role。这是最简单的方式:

// Echo 框架
func GetUserID(c echo.Context) string {
    if userID := c.Request().Header.Get("x-hc-user-id"); userID != "" {
        return userID
    }
    // 回退到 session
    if userID := c.Get("user_id"); userID != nil {
        return userID.(string)
    }
    return ""
}

// Gin 框架
func GetUserID(c *gin.Context) string {
    if userID := c.GetHeader("x-hc-user-id"); userID != "" {
        return userID
    }
    if sessionUserID, exists := c.Get("user_id"); exists && sessionUserID != "" {
        return sessionUserID.(string)
    }
    return ""
}

方式二:通过 OIDC 认证流程

当 Header 中没有用户信息时(如用户直接通过浏览器访问),需要走 OIDC 认证:

import (
    "github.com/coreos/go-oidc/v3/oidc"
    "golang.org/x/oauth2"
)

type OIDCProvider struct {
    config      *oauth2.Config
    userInfoURL string
}

func NewOIDCProvider() (*OIDCProvider, error) {
    clientID := os.Getenv("LAZYCAT_AUTH_OIDC_CLIENT_ID")
    clientSecret := os.Getenv("LAZYCAT_AUTH_OIDC_CLIENT_SECRET")
    authURL := os.Getenv("LAZYCAT_AUTH_OIDC_AUTH_URI")
    tokenURL := os.Getenv("LAZYCAT_AUTH_OIDC_TOKEN_URI")
    userInfoURL := os.Getenv("LAZYCAT_AUTH_OIDC_USERINFO_URI")

    if clientID == "" || clientSecret == "" || authURL == "" || tokenURL == "" {
        return nil, errors.New("missing required OIDC configuration")
    }

    domain := os.Getenv("LAZYCAT_APP_DOMAIN")
    redirectURL := fmt.Sprintf("https://%s/auth/oidc/callback", domain)

    oauth2Config := &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        Scopes:       []string{oidc.ScopeOpenID, "profile", "email", "groups"},
        Endpoint: oauth2.Endpoint{
            AuthURL:  authURL,
            TokenURL: tokenURL,
        },
    }

    return &OIDCProvider{
        config:      oauth2Config,
        userInfoURL: userInfoURL,
    }, nil
}

认证中间件

中间件的核心逻辑是:优先读 Header → 回退到 Session → 未认证则重定向登录页。

// Echo 版本
func AuthMiddleware(oidcProvider *OIDCProvider) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 优先检查系统注入的 Header
            if userID := c.Request().Header.Get("x-hc-user-id"); userID != "" {
                return next(c)
            }
            // 检查 Session
            if userID := c.Get("user_id"); userID != nil && userID.(string) != "" {
                c.Response().Header().Set("x-hc-user-id", userID.(string))
                return next(c)
            }
            // 放行公共路径
            if isPublicPath(c.Request().URL.Path) {
                return next(c)
            }
            // 重定向到登录
            return c.Redirect(http.StatusFound, "/login")
        }
    }
}

// Gin 版本
func AuthMiddleware(oidcProvider *OIDCProvider) gin.HandlerFunc {
    return func(c *gin.Context) {
        if userID := c.GetHeader("x-hc-user-id"); userID != "" {
            c.Next()
            return
        }
        if sessionUserID, exists := c.Get("user_id"); exists && sessionUserID != "" {
            c.Header("x-hc-user-id", sessionUserID.(string))
            c.Next()
            return
        }
        if isPublicPath(c.Request.URL.Path) {
            c.Next()
            return
        }
        c.Redirect(http.StatusFound, "/login")
        c.Abort()
    }
}

manifest.yml 配置

使用 SDK 开发的应用通过 manifest.yml 声明应用信息和运行方式。与 Docker 应用不同,SDK 应用使用 backend_launch_command 直接运行二进制文件:

name: 应用定时管家
package: community.lazycat.app.czyt.apps-scheduler
version: 1.0.0
min_os_version: 1.3.8
description: "定时启动和停止懒猫微服上的应用"
license: mit/
author:

application:
  subdomain: apps-scheduler
  oidc_redirect_path: /auth/oidc/callback
  public_path:
    - /

  upstreams:
    - location: /
      backend: http://127.0.0.1:8080/
      backend_launch_command: /lzcapp/pkg/content/apps-scheduler

  environment:
    - TZ={{.U.timezone}}
    - LAZYCAT_AUTH_OIDC_CLIENT_ID=${LAZYCAT_AUTH_OIDC_CLIENT_ID}
    - LAZYCAT_AUTH_OIDC_CLIENT_SECRET=${LAZYCAT_AUTH_OIDC_CLIENT_SECRET}
    - LAZYCAT_AUTH_OIDC_AUTH_URI=${LAZYCAT_AUTH_OIDC_AUTH_URI}
    - LAZYCAT_AUTH_OIDC_TOKEN_URI=${LAZYCAT_AUTH_OIDC_TOKEN_URI}
    - LAZYCAT_AUTH_OIDC_USERINFO_URI=${LAZYCAT_AUTH_OIDC_USERINFO_URI}
    - LAZYCAT_APP_DOMAIN=${LAZYCAT_APP_DOMAIN}
    - DB_PATH=/lzcapp/var/data/apps-scheduler.db
    - LOG_DIR=/lzcapp/var/data/logs

locales:
  zh:
    name: "应用定时管家"
    description: "定时启动和停止懒猫微服上的应用"
  en:
    name: "App Scheduler"
    description: "Schedule start and stop operations for apps on LazyCat"

关键配置说明:

  • backend_launch_command:指定可执行文件路径,懒猫会直接运行该二进制。打包的二进制文件位于 /lzcapp/pkg/content/ 目录下。
  • oidc_redirect_path:OIDC 回调路径,需与代码中的路由一致。
  • environment${...} 是系统运行时注入的变量,{{.U.xxx}} 是用户部署时配置的参数。

lzc-build.yml 配置

SDK 应用的构建配置与 Docker 应用不同,需要指定构建脚本和内容目录:

# 构建脚本路径
buildscript: ./build.sh

# manifest 文件路径
manifest: ./manifest.yml

# 打包到 lpk 的内容目录(编译产物放这里)
contentdir: ./dist

# lpk 包输出路径
pkgout: ./

# 应用图标
icon: ./icon.png

# 开发环境配置
devshell:
  routes:
    - /=http://127.0.0.1:5173
  dependencies:
    - nodejs
    - npm
    - go
  setupscript: |
    export npm_config_registry=https://registry.npmmirror.com
    export GOPROXY=https://goproxy.cn,direct    

devshell 部分用于 lzc-cli project devshell 开发调试,可以指定开发依赖和路由规则。

数据库配置

两个项目都使用 ent ORM 配合 SQLite。SQLite 数据库文件存储在 /lzcapp/var/data/ 目录下,确保数据持久化。

import (
    "your-app/internal/ent"
    _ "github.com/lib-x/entsqlite"
)

func NewUseCase(dbPath string) (*UseCase, error) {
    dataSourceName := fmt.Sprintf(
        "file:%s?cache=shared&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(10000)",
        dbPath,
    )

    client, err := ent.Open("sqlite3", dataSourceName)
    if err != nil {
        return nil, fmt.Errorf("failed to connect to database: %w", err)
    }

    // 自动迁移
    if err := client.Schema.Create(context.Background()); err != nil {
        client.Close()
        return nil, fmt.Errorf("failed to create schema: %w", err)
    }

    return &UseCase{client: client}, nil
}

推荐的 SQLite 配置:

  • cache=shared:共享缓存模式
  • journal_mode(WAL):WAL 模式提升并发性能
  • synchronous(NORMAL):平衡安全性和性能
  • busy_timeout(10000):10 秒忙等待,避免 database is locked 错误

完整示例:应用入口

package main

import (
    "os"
    "os/signal"
    "path/filepath"
    "syscall"

    "your-app/internal/biz"
    "your-app/internal/web"
)

const (
    defaultDBPath = "/lzcapp/var/data/your-app.db"
    defaultLogDir = "/lzcapp/var/data/logs"
)

func main() {
    // 初始化日志
    logDir := os.Getenv("LOG_DIR")
    if logDir == "" {
        logDir = defaultLogDir
    }
    os.MkdirAll(logDir, 0755)

    // 数据库路径
    dbPath := os.Getenv("DB_PATH")
    if dbPath == "" {
        dbPath = defaultDBPath
    }
    os.MkdirAll(filepath.Dir(dbPath), 0755)

    // 初始化数据库
    useCase, err := biz.NewUseCase(dbPath)
    if err != nil {
        log.Fatal().Err(err).Msg("Failed to initialize database")
    }
    defer useCase.Close()

    // 启动 Web 服务
    addr := os.Getenv("LISTEN_ADDR")
    if addr == "" {
        addr = ":8080"
    }
    server := web.NewServer(useCase)

    // 优雅关闭
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        server.Shutdown()
    }()

    server.Start(addr)
}

完整示例:Web 服务配置(Echo)

package web

import (
    "embed"
    "io/fs"
    "net/http"

    "your-app/internal/auth"
    "your-app/internal/biz"
    "your-app/internal/handlers"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

//go:embed public/*
var publicFS embed.FS

type Server struct {
    echo    *echo.Echo
    useCase *biz.UseCase
}

func NewServer(useCase *biz.UseCase) *Server {
    e := echo.New()
    e.HideBanner = true

    oidcProvider, _ := auth.NewOIDCProvider()

    publicContent, _ := fs.Sub(publicFS, "public")

    server := &Server{echo: e, useCase: useCase}

    // 中间件
    e.Use(middleware.Recover())
    e.Use(auth.SessionMiddleware())

    // 静态文件
    staticHandler := http.FileServer(http.FS(publicContent))
    e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))

    // OIDC 路由
    if oidcProvider != nil {
        e.GET("/auth/oidc/login", oidcProvider.HandleLogin)
        e.GET("/auth/oidc/callback", oidcProvider.HandleCallback)
    }

    // 需认证的路由
    protected := e.Group("")
    protected.Use(auth.AuthMiddleware(oidcProvider))

    // API
    api := protected.Group("/api")
    appHandler := handlers.NewAppHandler()
    api.GET("/apps", appHandler.ListApps)
    api.POST("/apps/:appId/resume", appHandler.ResumeApp)
    api.POST("/apps/:appId/pause", appHandler.PauseApp)

    return server
}

完整示例:在 Handler 中调用 SDK

查询应用列表(Echo)

package handlers

import (
    "context"
    "net/http"

    "your-app/internal/auth"

    gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
    "gitee.com/linakesi/lzc-sdk/lang/go/sys"
    "github.com/labstack/echo/v4"
    "google.golang.org/grpc/metadata"
)

type AppInfo struct {
    AppID          string `json:"appId"`
    Title          string `json:"title"`
    Icon           string `json:"icon"`
    Version        string `json:"version"`
    Status         string `json:"status"`
    InstanceStatus string `json:"instanceStatus"`
}

func (h *AppHandler) ListApps(c echo.Context) error {
    ctx := context.Background()
    userID := auth.GetUserID(c)
    ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)

    gw, err := gohelper.NewAPIGateway(ctx)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to connect to gateway",
        })
    }
    defer gw.Close()

    resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{})
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to query applications",
        })
    }

    apps := make([]AppInfo, 0, len(resp.InfoList))
    for _, info := range resp.InfoList {
        if info.Status != sys.AppStatus_Installed {
            continue
        }
        // 跳过内置应用和多实例应用
        if info.Builtin != nil && *info.Builtin {
            continue
        }
        if info.MultiInstance {
            continue
        }

        app := AppInfo{
            AppID:          info.Appid,
            Status:         info.Status.String(),
            InstanceStatus: info.InstanceStatus.String(),
        }
        if info.Title != nil {
            app.Title = *info.Title
        } else {
            app.Title = info.Appid
        }
        if info.Icon != nil {
            app.Icon = *info.Icon
        }
        if info.Version != nil {
            app.Version = *info.Version
        }
        apps = append(apps, app)
    }

    return c.JSON(http.StatusOK, apps)
}

控制 LED(Gin)

package handlers

import (
    "net/http"
    "sync"

    gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
    users "gitee.com/linakesi/lzc-sdk/lang/go/common"
    "github.com/gin-gonic/gin"
)

var (
    ledStatus bool
    ledMutex  sync.Mutex
)

func GetLedStatus(c *gin.Context) {
    ctx := c.Request.Context()

    gw, err := gohelper.NewAPIGateway(ctx)
    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }
    defer gw.Close()

    boxInfo, err := gw.Box.QueryInfo(ctx, nil)
    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    ledMutex.Lock()
    ledStatus = boxInfo.PowerLed
    ledMutex.Unlock()

    c.JSON(http.StatusOK, gin.H{"status": ledStatus})
}

func LedControl(c *gin.Context) {
    ctx := c.Request.Context()

    gw, err := gohelper.NewAPIGateway(ctx)
    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }
    defer gw.Close()

    boxInfo, err := gw.Box.QueryInfo(ctx, nil)
    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    newStatus := !boxInfo.PowerLed

    ledMutex.Lock()
    defer ledMutex.Unlock()

    _, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
        PowerLed: newStatus,
    })
    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    ledStatus = newStatus
    c.JSON(http.StatusOK, gin.H{"status": ledStatus})
}

后台定时任务

对于需要后台定时执行 SDK 操作的场景(如定时启停应用、定时控制 LED),可以使用 goroutine + ticker 的模式:

type Scheduler struct {
    useCase *UseCase
    stopCh  chan struct{}
    wg      sync.WaitGroup
}

func NewScheduler(useCase *UseCase) *Scheduler {
    return &Scheduler{
        useCase: useCase,
        stopCh:  make(chan struct{}),
    }
}

func (s *Scheduler) Start() {
    s.wg.Add(1)
    go s.run()
}

func (s *Scheduler) Stop() {
    close(s.stopCh)
    s.wg.Wait()
}

func (s *Scheduler) run() {
    defer s.wg.Done()

    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()

    s.checkAndExecute() // 启动时立即检查一次

    for {
        select {
        case <-ticker.C:
            s.checkAndExecute()
        case <-s.stopCh:
            return
        }
    }
}

func (s *Scheduler) checkAndExecute() {
    ctx := context.Background()
    now := time.Now()
    weekday := int(now.Weekday())

    schedules, err := s.useCase.GetEnabledSchedules(ctx)
    if err != nil {
        return
    }

    for _, sch := range schedules {
        if sch.Hour == now.Hour() && sch.Minute == now.Minute() && containsDay(sch.WeekDays, weekday) {
            go s.executeSchedule(ctx, sch)
        }
    }
}

后台定时任务需要在 manifest.yml 中声明 background_task: true,防止系统因为不活跃而自动停止应用。

SDK 导入路径参考

包路径 别名 用途
gitee.com/linakesi/lzc-sdk/lang/go gohelper 创建 APIGateway
gitee.com/linakesi/lzc-sdk/lang/go/sys sys 应用管理(PkgManager)相关类型
gitee.com/linakesi/lzc-sdk/lang/go/common commonusers 用户管理和设备管理相关类型
google.golang.org/grpc/metadata metadata 传递 gRPC 上下文信息

常见问题

Gateway 创建失败

确保应用运行在懒猫微服环境中。SDK 通过特定的 gRPC 端点与系统通信,本地开发环境中无法直接使用,需要通过 lzc-cli project devshell 进入开发环境调试。

指针字段的处理

SDK 返回的很多字段(如 TitleIconVersionBuiltin)是指针类型。使用前必须判空:

// 正确
if info.Title != nil {
    title = *info.Title
}

// 错误 - 可能 panic
title = *info.Title

用户身份传递

调用 SDK API 前,确保 Context 中包含 x-hc-user-id metadata。部分 API(如 Resume/Pause)需要通过 userID 确定操作权限。

数据存储路径

  • 持久数据:/lzcapp/var/data/
  • 缓存数据:/lzcapp/cache/
  • 包内容(只读):/lzcapp/pkg/content/
  • 日志文件:/lzcapp/var/data/logs/

参考项目