> **用于从 Nuxt 服务端发送 Kakao 通知消息、品牌消息与短信的官方 Vue SDK**

`@sendgo/vue` 把 [`@sendgo/node`](https://github.com/send-go/node) 核心封装为 **Vue 插件**，通过 Vue 的注入系统提供客户端。

> **⚠️ 仅限服务端使用。** 访问密钥与私密密钥**绝不可**进入浏览器。请把插件注册为 **server-only** 的 Nuxt 插件，或直接在 `server/api` 路由里使用客户端。**切勿**注册为通用（universal）插件。

要求 Vue 3.4+ 与 Nuxt 3+。

---

## 安装

```bash
npm install @sendgo/vue
```

核心 `@sendgo/node` 会作为依赖一并安装。

---

## 注册插件（Nuxt，仅服务端）

文件名的 `.server.ts` 后缀正是把密钥挡在客户端 bundle 之外的关键：

```typescript
// plugins/sendgo.server.ts
import { SendgoPlugin } from '@sendgo/vue';

export default defineNuxtPlugin((app) => {
  app.vueApp.use(SendgoPlugin, {
    accessKey: process.env.SENDGO_ACCESS_KEY!,
    secretKey: process.env.SENDGO_SECRET_KEY!,
    kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
    smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY,
    apiVersion: 'v2',
  });
});
```

---

## 使用注入的客户端

```vue
<script setup lang="ts">
import { inject } from 'vue';
import { SENDGO_KEY } from '@sendgo/vue';
import type Sendgo from '@sendgo/node';

// 只在 SSR 期间能解析到 —— 插件本身就是设计为仅服务端的。
const sendgo = inject<Sendgo>(SENDGO_KEY);

await sendgo?.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [{ contact: '01012345678', var1: 'ORD-001' }],
});
</script>
```

`SENDGO_KEY` 提供的是**完整的核心客户端**，因此各渠道都可用：

| 属性 | 渠道 |
|------|------|
| `sendgo.alimtalk` | Kakao 通知消息 |
| `sendgo.friendtalk` | Kakao 好友消息 |
| `sendgo.brandMessage` | Kakao 品牌消息（仅 v2） |
| `sendgo.sms` | SMS / LMS / MMS |

---

## Nuxt server route（推荐）

对于由用户操作触发的发送，server route 比 SSR 注入更清晰 —— 浏览器调用你的端点，密钥留在服务端：

```typescript
// server/api/notify.post.ts
import { Sendgo, SendgoError } from '@sendgo/vue';

const sendgo = new Sendgo({
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
  apiVersion: 'v2',
});

export default defineEventHandler(async (event) => {
  const { phone, orderNo } = await readBody(event);

  try {
    await sendgo.alimtalk.send({
      templateCode: 'ORDER_CONFIRM_001',
      contacts: [{ contact: phone, var1: orderNo }],
    });

    return { ok: true };
  } catch (error) {
    if (error instanceof SendgoError) {
      // 上游细节记录到日志，对外只返回通用状态。
      console.error(`Sendgo ${error.statusCode} [${error.errorCode}]: ${error.message}`);

      throw createError({
        statusCode: error.statusCode >= 500 ? 502 : 400,
        statusMessage: '消息发送失败',
      });
    }

    throw error;
  }
});
```

客户端在模块作用域构造一次，因此令牌缓存可以跨请求复用。

---

## 品牌消息

> **好友消息已于 2025-12-31 停止服务。** 自 2026-01-01 起，Kakao 会将好友消息请求
> 自动改以品牌消息（自由形式）发出。`friendtalk` 仍可调用，且自由文本类型
> `FT`/`FI`/`FW` 发往单个接收者的路径目前仍只有这一条 —— 基于模板的富文本类型
> （`FL`/`FC`/`FM`/`FP`/`FA`）、非好友定向（`N`/`I`）与群发（`F`）请使用品牌消息。
品牌消息是好友消息的后继渠道：可触达**非频道好友**的接收者（`targeting: 'N'`），也可向**已同意接收的全部频道好友群发**（`targeting: 'F'`）。消息类型与好友消息一一对应（`FT`→`BT`、`FI`→`BI`、`FW`→`BW`、`FL`→`BL`、`FC`→`BC`、`FM`→`BM`、`FP`→`BP`、`FA`→`BA`）—— 传入好友消息的代码，服务端会自动转换。

> 仅 v2 支持。请设置 `apiVersion: 'v2'`。

```typescript
// server/api/campaign.post.ts
export default defineEventHandler(async (event) => {
  const { mode, phone } = await readBody(event);

  if (mode === 'broadcast') {
    // 不传接收者列表 —— 由 Kakao 展开受众
    return sendgo.brandMessage.broadcast({
      messageType: 'FW',
      friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
    });
  }

  return sendgo.brandMessage.send({
    targeting: 'M',
    messageType: 'FL',
    friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
    contacts: [{ contact: phone, var1: '29,000元' }],
  });
});
```

```typescript
// server/api/campaigns.get.ts —— 群发为异步处理，需轮询
export default defineEventHandler(async (event) => {
  const { campaignId } = getQuery(event);

  return campaignId
    ? sendgo.brandMessage.campaign(String(campaignId))
    : sendgo.brandMessage.campaigns({ count: 10 });
});
```

群发可能触达整个好友列表，请在该 server route 内先做权限校验。

`targeting` 的取值含义：

| 值 | 含义 |
|----|------|
| `M` | 频道好友中的指定接收者 |
| `N` | 非频道好友的接收者 |
| `I` | 按标识符指定的接收者 |
| `F` | 已同意接收的全部频道好友（群发） |

---

## 客户端组合式函数

`useAlimtalk` 封装了对**你自己的** server route 的调用，并附带 pending / error 状态。它返回 `{ send, loading, error, data, reset }`：

```vue
<script setup lang="ts">
import { useAlimtalk } from '@sendgo/vue';

const { send, loading, error, data, reset } = useAlimtalk();

const notify = () => send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [{ contact: '01012345678', var1: 'ORD-001' }],
});
</script>

<template>
  <button :disabled="loading" @click="notify">
    {{ loading ? '发送中…' : '发送' }}
  </button>
  <p v-if="error" role="alert">
    {{ error.message }}
    <button @click="reset">关闭</button>
  </p>
  <p v-else-if="data">已发送。</p>
</template>
```

这个组合式函数只负责请求你的服务端端点 —— 它不会持有任何密钥。

---

## 短信 / 长短信 / 多媒体短信

```typescript
// server/api/sms.post.ts
await sendgo.sms.sendSms({
  content: '[Sendgo] 验证码：123456（请在 5 分钟内输入）',
  contacts: [{ contact: '01012345678' }],
});

await sendgo.sms.sendLms({
  subject: '[重要] 服务维护通知',
  content: '服务将于 2026-07-25 02:00 ~ 06:00 进行维护。',
  contacts: [{ contact: '01012345678' }],
});

await sendgo.sms.sendMms({
  subject: '[活动] 7 月特惠',
  content: '欢迎查看本月特价商品。',
  contacts: [{ contact: '01012345678' }],
});
```

---

## 纯 Vue 3（不使用 Nuxt）

没有 Nuxt 就没有服务端／客户端的划分，因此插件必须运行在 Node 进程中 —— 例如用 `@vue/server-renderer` 做 SSR 的 Express／Fastify 后端，或一个 worker。**不要**在纯浏览器应用中注册。

```typescript
import { createSSRApp } from 'vue';
import { SendgoPlugin } from '@sendgo/vue';

const app = createSSRApp(App);
app.use(SendgoPlugin, {
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  apiVersion: 'v2',
});
```

---

## 类型

所有请求结构都从核心包重新导出：

```typescript
import type {
  AlimtalkParams,
  FriendtalkParams,
  BrandMessageParams,
  BrandMessageListParams,
  BrandMessageTargeting,
  SmsParams,
  Contact,
  SendgoConfig,
  SendgoResponse,
} from '@sendgo/vue';
```

`Sendgo`（客户端类）与 `SendgoError` 以值的形式导出，而非仅类型。

---

## 配置项

| 键 | 是否必填 | 默认值 | 说明 |
|----|----------|--------|------|
| `accessKey` | **必填** | — | Sendgo 访问密钥 |
| `secretKey` | **必填** | — | Sendgo 私密密钥 |
| `kakaoSenderKey` | 选填 | `undefined` | Kakao 发送者资料密钥 |
| `smsSenderKey` | 选填 | `undefined` | 短信主叫号码密钥 |
| `apiVersion` | 选填 | `v1` | API 版本（`v1` \| `v2`） |
| `baseUrl` | 选填 | `https://sendgo.io` | API 基础地址 |

---

## 短链接

短链接会缩短消息正文中的链接，并统计该链接是否被真正点击。
短信按字节计费，因此链接更短就意味着正文可以写更多内容。

> 仅 v2 支持。

再次缩短同一个原始 URL 会**直接返回已有的链接**。若希望按活动分别统计反应，
请使用 `forceNew` 生成新的短码。

`deactivate` 不会删除链接，只停止重定向。当已发送消息中的链接必须失效时使用它；
累计统计会保留，访问已停止的链接会返回 `410 Gone`。

```typescript
// server/api/shorten.post.ts
export default defineEventHandler(async (event) => {
  const { targetUrl } = await readBody(event);

  const created = await sendgo.shortUrl.create({ targetUrl });

  return { shortUrl: created.data.shortUrl, code: created.data.code };
});
```

`stats` 返回按日趋势（`daily`）以及按设备（`byDevice`）、来源（`byReferer`）、国家（`byCountry`）的分解。按日趋势读取的是预聚合表，因此点击量再大响应时间也保持稳定。

---

## 包信息

- **包名**：`@sendgo/vue`（npm）
- **仓库**：[send-go/vue](https://github.com/send-go/vue)
- **注册表**：https://www.npmjs.com/package/@sendgo/vue
- **许可**：MIT

### 如何获取 API 密钥

登录 Sendgo 后，在 **API/SDK → API 对接** 菜单中签发访问密钥与私密密钥。
使用通知消息／好友消息需在 **Kakao 渠道** 登记发送者资料以获取 `kakaoSenderKey`；使用短信需在 **发送号码** 菜单登记主叫号码。