# 一键创建应用（Go）

Go SDK 提供了 `RegisterApp` 方法，基于 OAuth 2.0 Device Authorization Grant（RFC 8628）协议实现一键创建应用。

调用该方法会返回一个验证链接，用户在飞书或 Lark 中打开该链接（或扫码）完成授权后，即可自动注册应用并获取凭据（App ID 和 App Secret），无需手动前往开发者后台创建。

查看源码：[oapi-sdk-go](https://github.com/larksuite/oapi-sdk-go#%E4%B8%80%E9%94%AE%E5%88%9B%E5%BB%BA%E5%BA%94%E7%94%A8--one-click-app-registration)

## 前提条件

- 安装 `oapi-sdk-go`，且 SDK 版本为最新 v3 版本。

```shell
  go get github.com/larksuite/oapi-sdk-go/v3@latest
  ```

## 快速开始示例
```go
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

lark "github.com/larksuite/oapi-sdk-go/v3"
	"github.com/larksuite/oapi-sdk-go/v3/scene/registration"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

result, err := registration.RegisterApp(ctx, &registration.Options{
		OnQRCode: func(info *registration.QRCodeInfo) {
			fmt.Printf("open or scan this url: %s\n", info.URL)
			fmt.Printf("the link expires in %d seconds\n", info.ExpireIn)
		},
		OnStatusChange: func(info *registration.StatusChangeInfo) {
			// status: polling | slow_down | domain_switched
			fmt.Printf("registration status: %s", info.Status)
			if info.Interval > 0 {
				fmt.Printf(", next poll after %d seconds", info.Interval)
			}
			fmt.Println()
		},
	})
	if err != nil {
		var regErr *registration.RegisterAppError
		if errors.As(err, &regErr) {
			fmt.Printf("register app failed: code=%s, description=%s\n", regErr.Code, regErr.Description)
			return
		}
		panic(err)
	}

fmt.Println("App ID:", result.ClientID)
	fmt.Println("App Secret:", result.ClientSecret)

client := lark.NewClient(result.ClientID, result.ClientSecret)
	_ = client
}
```

## 自定义权限、事件、回调与更新已有应用

创建应用时，可通过 `Options.Addons` 在平台基础模板上增量申请权限、事件订阅和回调。这些配置会预填到用户扫码后的确认页中，用户确认后生效。`Options.CreateOnly=true` 时，页面只允许创建新应用；传入 `Options.AppID` 时，页面会进入更新已有应用配置的确认流程。

```go
_, err := registration.RegisterApp(ctx, &registration.Options{
	Addons: &registration.AppAddons{
		Scopes: registration.AppAddonsScopes{
			Tenant: []string{"im:message:send_as_bot"},
			User:   []string{"calendar:calendar:read"},
		},
		Events: registration.AppAddonsEvents{
			Items: registration.AppAddonsEventItems{
				Tenant: []string{"im.message.receive_v1"},
			},
		},
		Callbacks: registration.AppAddonsCallbacks{
			Items: []string{"card.action.trigger"},
		},
	},
	CreateOnly: true,
	OnQRCode: func(info *registration.QRCodeInfo) {
		fmt.Println(info.URL)
	},
})
if err != nil {
	panic(err)
}

_, err = registration.RegisterApp(ctx, &registration.Options{
	AppID: "cli_xxx",
	Addons: &registration.AppAddons{
		Scopes: registration.AppAddonsScopes{
			Tenant: []string{"drive:drive.metadata:readonly"},
		},
	},
	OnQRCode: func(info *registration.QRCodeInfo) {
		fmt.Println(info.URL)
	},
})
if err != nil {
	panic(err)
}
```

注意：`Addons` 仅支持增量叠加，不支持删除基础模板中的配置；SDK 只校验数据形状和非空字符串，不校验权限点、事件或回调名称是否存在。

### 通过 `Preset` 选择模板底座

`AppAddons.Preset` 用于选择应用创建的模板底座（与 `Options.AppPreset` 无关，后者只预填应用名称、描述和头像）：

| 取值 | 模板底座 | 行为 |
| ---- | ---- | ---- |
| 不设置（`nil`） | 平台默认模板 | 与该字段出现之前完全一致；编码后的 payload 不含 `preset` 键。 |
| `false` | 最小基础模板 | 确认页只展示 `Addons` 中显式声明的权限、事件和回调。 |
| `true` | 平台默认模板 | 显式声明使用默认底座，效果与不设置相同。 |

`Preset` 为 `false` 时，允许 `Addons` 不携带任何权限、事件或回调——页面仍会进入确认流程，创建仅含最小底座能力的应用：

```go
preset := false
_, err := registration.RegisterApp(ctx, &registration.Options{
	Addons: &registration.AppAddons{
		Preset: &preset,
	},
	OnQRCode: func(info *registration.QRCodeInfo) {
		fmt.Println(info.URL)
	},
})
if err != nil {
	panic(err)
}
```

## registration.RegisterApp 参数

| 参数 | 描述 | 类型 | 必填 | 默认值 |
| ---- | ---- | ---- | ---- | ---- |
| `ctx` | 控制注册流程的超时与取消；取消 `context` 会终止轮询。 | `context.Context` | 是 | - |
| `Options.Source` | 来源标识，会拼入二维码 URL 的 `source` 参数，格式为 `go-sdk/{source}`。 | `string` | 否 | `go-sdk` |
| `Options.Domain` | 自定义飞书认证域名，支持传完整前缀，如 `https://accounts.feishu.cn`。 | `string` | 否 | `https://accounts.feishu.cn` |
| `Options.LarkDomain` | 自定义 Lark 认证域名；检测到 `tenant_brand=lark` 时自动切换。 | `string` | 否 | `https://accounts.larksuite.com` |
| `Options.AppPreset` | 预设应用信息，仅用于初始化创建页；用户仍可在页面修改，最终以页面提交为准。 | `*registration.AppPreset` | 否 | - |
| `Options.AppPreset.Avatar` | 应用头像 URL，支持 1-6 个；第一个默认选中。传原始 URL，SDK 会编码。头像展示、图片可访问性、GIF 取帧等由创建页处理。 | `[]string` | 否 | - |
| `Options.AppPreset.Name` | 应用名称，支持 `{user}` 占位符；传原始值，SDK 会编码。 | `string` | 否 | - |
| `Options.AppPreset.Desc` | 应用描述，支持 `{user}` 占位符；传原始值，SDK 会编码。 | `string` | 否 | - |
| `Options.Addons` | 增量权限、事件、回调配置，预填到扫码后的确认页，用户确认后生效。 | `*registration.AppAddons` | 否 | - |
| `Options.Addons.Preset` | 模板底座选择：不设置为平台默认模板；`false` 为最小基础模板（此时允许增量为空）；`true` 为显式声明默认底座。 | `*bool` | 否 | - |
| `Options.Addons.Scopes.Tenant` | 应用身份权限列表，如 `im:message:send_as_bot`。 | `[]string` | 否 | - |
| `Options.Addons.Scopes.User` | 用户身份权限列表，如 `calendar:calendar:read`。 | `[]string` | 否 | - |
| `Options.Addons.Events.Items.Tenant` | 应用身份事件列表，如 `im.message.receive_v1`。 | `[]string` | 否 | - |
| `Options.Addons.Events.Items.User` | 用户身份事件列表，如 `calendar.calendar.event.changed_v4`。 | `[]string` | 否 | - |
| `Options.Addons.Callbacks.Items` | 回调列表，如 `card.action.trigger`。 | `[]string` | 否 | - |
| `Options.CreateOnly` | 为 `true` 时落地页仅允许创建新应用；与 `Options.AppID` 同时传入时，页面侧优先走新建流程。 | `bool` | 否 | `false` |
| `Options.AppID` | 已有应用的 App ID；传入后会作为二维码 URL 的 `clientID` 参数，用于更新已有应用配置。 | `string` | 否 | - |
| `Options.OnQRCode` | 验证链接就绪时的回调，参数为 `{ URL, ExpireIn }`。可直接展示链接，或将其渲染为二维码供用户扫码。 | `func(info *registration.QRCodeInfo)` | 是 | - |
| `Options.OnStatusChange` | 轮询状态变化回调，参数为 `{ Status, Interval }`。`Status` 可能为 `polling`、`slow_down`、`domain_switched`。 | `func(info *registration.StatusChangeInfo)` | 否 | - |

## 返回值

| 字段 | 类型 | 描述 |
| ---- | ---- | ---- |
| `ClientID` | `string` | 应用的 `App ID` |
| `ClientSecret` | `string` | 应用的 `App Secret` |
| `UserInfo` | `*registration.UserInfo` | 扫码用户信息 |
| `UserInfo.OpenID` | `string` | 扫码用户的 `open_id` |
| `UserInfo.TenantBrand` | `string` | `"feishu"` 或 `"lark"` |

## 错误处理

返回的错误通常可以通过 `errors.As(err, &registration.RegisterAppError)` 获取 `Code` 与 `Description` 字段。更具体的错误类型还包括 `registration.AccessDeniedError` 和 `registration.ExpiredError`。

| `Code` | 描述 |
| ---- | ---- |
| `access_denied` | 用户拒绝授权 |
| `expired_token` | 二维码过期或轮询超时 |
| `invalid_response` | 接口返回缺少必要字段或响应为空 |
