# @ai_wanjia/sdk

玩家AI 平台的创作者 SDK。

它用于运行在平台 iframe 内的独立页面，通过 `postMessage RPC` 与平台通信，读取角色信息、读写变量、发送消息、控制界面，并在 React 项目中提供现成的 Hooks 和组件。

---

## 适用场景

| 场景 | 是否适合 |
|------|----------|
| 独立 HTML 页面、原生 JS 页面 | ✅ |
| React 独立页面 | ✅ |
| 角色卡里需要完整自定义 UI 的游戏界面 | ✅ |
| 仅在 AI 消息里插一个小卡片 | 更推荐直接用平台注入的 `window.$charx` |
| Node.js 服务端、后端任务 | ❌ |

---

## 官方 Starter

仓库内已经提供两套官方快速开发模板：

| 模板 | 目录 | 适用场景 |
|------|------|----------|
| `charx-h5-starter` | `sdk/examples/charx-h5-starter` | 原生 `html + css + js`，零构建起步 |
| `charx-react-starter` | `sdk/examples/charx-react-starter` | `Vite + React + TypeScript`，适合组件化开发 |

这两套模板都支持：

- 本地开发态自动切到 mock 运行时
- 平台 iframe 内自动切到真实 SDK
- 首页自带 `character / variable / storage / chat / ui.toast` 示例

---

## 安装

```bash
npm install @ai_wanjia/sdk
```

---

## 五分钟上手

### 原生 JavaScript / TypeScript

```typescript
import { CharxSdk } from '@ai_wanjia/sdk'

const sdk = CharxSdk.getInstance()
await sdk.init()

const info = await sdk.character.getInfo()
console.log('角色名：', info.name)

const hp = await sdk.variable.get<number>('hp') ?? 100
console.log('当前 HP：', hp)

sdk.chat.send('总结一下我当前的状态', {
  onData: (chunk) => {
    console.log('流式片段：', chunk)
  },
  onComplete: (message) => {
    console.log('完整回复：', message.content)
  },
  onError: (error) => {
    console.error(error)
  },
})
```

### React

```tsx
import { CharxProvider, useChat, useVariable, useCharacter } from '@ai_wanjia/sdk/react'

export default function App() {
  return (
    <CharxProvider fallback={<div>连接平台中...</div>}>
      <GamePage />
    </CharxProvider>
  )
}

function GamePage() {
  const character = useCharacter()
  const { messages, send, status, stop } = useChat()
  const [hp, setHp] = useVariable<number>('hp', 100)

  return (
    <div>
      <h1>{character?.name}</h1>
      <p>HP: {hp}</p>

      {messages.map((message) => (
        <div key={message.id}>{message.content}</div>
      ))}

      {status === 'streaming' ? (
        <button onClick={stop}>停止</button>
      ) : (
        <button onClick={() => send('继续推进剧情')}>继续</button>
      )}

      <button onClick={() => setHp(hp + 5)}>恢复 5 点 HP</button>
    </div>
  )
}
```

---

## 运行时规则

### 1. 必须在平台 iframe 内运行

`sdk.init()` 会向父窗口发起握手。

```typescript
const sdk = CharxSdk.getInstance()
await sdk.init()
```

如果页面不是由玩家AI平台加载，而是直接在浏览器里打开，`init()` 会在超时后抛错。

### 2. `init()` 之前不要调用其他 API

推荐做法：

```typescript
const sdk = CharxSdk.getInstance()
await sdk.init()

// 从这里开始再调用其他模块
```

### 3. `variable` 和 `storage` 不是一回事

| 能力 | 作用域 | 是否随当前对话变化 | 适合存什么 |
|------|--------|--------------------|------------|
| `sdk.variable` | 当前对话 | 是 | HP、MP、好感度、剧情阶段、当前位置 |
| `sdk.storage` | `user + character` | 否 | 设置、成就、已解锁结局、长期背包 |

### 4. SDK 负责界面层，不负责替代平台 AI 运行时

SDK 能做的事情：

- 发送消息到平台聊天链路
- 读写变量和持久化存储
- 读取角色信息
- 控制当前独立页面 UI

SDK 不做的事情：

- 不直接调用模型
- 不绕开平台上下文组装
- 不替代角色卡、世界书、正则、系统提示词

---

## API 概览

`CharxSdk` 是单例对象，所有能力都挂在它的模块上：

```text
CharxSdk
├── chat
├── character
├── variable
├── storage
├── ui
├── media
├── game
├── worldbook
└── utils
```

### `sdk.character`

读取当前角色、人设和对话配置。

```typescript
const info = await sdk.character.getInfo()
const persona = await sdk.character.getPersona()
const settings = await sdk.character.getSettings()
```

`sdk.character.getPersona()` 在用户没有设置 persona 时返回 `null`。

### `sdk.chat`

负责发消息、停止生成、切开场白、读取历史消息。

```typescript
sdk.chat.send('你好', {
  onData: (chunk) => console.log(chunk),
  onComplete: (message) => console.log(message.content),
})

const task = sdk.chat.sendStream('继续推进剧情', {
  timeoutMs: 60_000,
  temperature: 0.8,
  injectedSystemPrompts: ['本次回复优先输出行动结果。'],
  onStart: ({ requestId, assistantMessageId }) => {
    console.log('开始生成：', requestId, assistantMessageId)
  },
  onData: (chunk) => console.log(chunk),
  onComplete: (message) => console.log('完成：', message.content),
  onError: (error) => console.error(error),
})

// `timeoutMs` 表示流式事件空闲超时。持续输出超过 60 秒不会被中断。
// 不传 `model` 时，平台会使用当前会话配置的模型。
await task.done

await sdk.chat.stop()
await sdk.chat.regenerate()
await sdk.chat.continue()

sdk.chat.setInput('帮我润色这句话')
const input = await sdk.chat.getInput()

await sdk.chat.setGreetingIndex(1)
const greetingIndex = await sdk.chat.getGreetingIndex()

const allMessages = await sdk.chat.messages.getAll()
const page = await sdk.chat.messages.list({ limit: 20, offset: 0 })
const secondPage = await sdk.chat.messages.list({ page: 2, pageSize: 20 })
```

### `sdk.variable`

负责当前对话变量。

```typescript
const hp = await sdk.variable.get<number>('hp') ?? 100
const vars = await sdk.variable.getAll()

await sdk.variable.set('hp', 80)
await sdk.variable.setMany({ hp: 80, mp: 50 })

await sdk.variable.increment('hp', 5)
await sdk.variable.decrement('mp', 2)

const unwatch = sdk.variable.watch<number>('hp', (value) => {
  console.log('新的 HP：', value)
})
```

`sdk.variable.get<T>(key)` 在变量不存在时返回 `null`。需要默认值时直接使用 `??`。

### `sdk.storage`

负责跨对话持久化数据。
正式聊天宿主会写入平台后端；创作者编辑器和测试页只提供本地预览态。

```typescript
await sdk.storage.set('preferredTheme', 'deep-sea')

const theme = await sdk.storage.get<string>('preferredTheme') ?? 'deep-sea'
const settings = await sdk.storage.getOrDefault('settings', { bgm: true })
const keys = await sdk.storage.keys()
```

`sdk.storage.get<T>(key)` 在 key 不存在时返回 `null`。需要默认值时可以用 `??`，或直接使用 `getOrDefault()`。

### `sdk.ui`

负责 Toast、弹窗、主题、滚动和面板控制。

```typescript
sdk.ui.toast({ text: '获得道具', type: 'success' })

await sdk.ui.alert('任务失败')
await sdk.ui.alert({ title: '任务失败', message: '线索不足', confirmText: '知道了' })
const confirmed = await sdk.ui.confirm('确认继续？')
const shouldContinue = await sdk.ui.confirm({ title: '继续剧情', message: '是否消耗 1 点行动力？', confirmText: '继续', cancelText: '返回' })
const input = await sdk.ui.prompt('请输入角色名', '默认值')
const named = await sdk.ui.prompt({ title: '角色命名', message: '请输入角色名', placeholder: '例如：林见鹿', defaultValue: '林见鹿' })

sdk.ui.scrollToBottom()
sdk.ui.setTheme('dark')

await sdk.ui.openPanel({
  url: 'https://your-panel-url',
  title: '角色状态',
  mode: 'sidebar',
  width: '420px',
})
await sdk.ui.closePanel()
```

### `sdk.media`

负责 TTS、背景音乐和音效。

```typescript
await sdk.media.speak('欢迎来到这里', { voice: 'character' })
sdk.media.stopSpeak()

await sdk.media.bgm.play('/assets/bgm.mp3', { loop: true, volume: 0.7 })
await sdk.media.bgm.stop({ fadeOut: 500 })
sdk.media.bgm.pause()
sdk.media.bgm.resume()
sdk.media.bgm.setVolume(0.5)

await sdk.media.sfx.play('/assets/hit.mp3', { volume: 0.8 })
sdk.media.sfx.preload(['/assets/hit.mp3'])
```

### `sdk.game`

负责存档、成就和排行榜。
正式聊天宿主会走平台后端；创作者编辑器和测试页仍使用本地预览态 runtime。

```typescript
await sdk.game.save({ slot: 1, description: '第三章开头' })
const saveData = await sdk.game.load({ slot: 1 })
const saves = await sdk.game.listSaves()
await sdk.game.deleteSave(1)

await sdk.game.achievements.unlock('first_victory')
const achievements = await sdk.game.achievements.list()

await sdk.game.ranking.submit(9800, { category: 'story_mode' })
const top10 = await sdk.game.ranking.getTop({ limit: 10, category: 'story_mode' })
const myRank = await sdk.game.ranking.getSelf({ category: 'story_mode' })
```

### `sdk.worldbook`

负责当前对话内的世界书动态控制。

所有操作只影响当前对话，不会改角色卡模板。

```typescript
const worldbook = await sdk.worldbook.getAll()
const enabledEntries = worldbook.entries.filter(entry => entry.enabled)

await sdk.worldbook.enable('combat-mode')
await sdk.worldbook.disable('peace-mode')

await sdk.worldbook.batchToggle([
  { entryKey: 'combat-mode', enabled: true },
  { entryKey: 'peace-mode', enabled: false },
])

await sdk.worldbook.setContent('combat-mode', '新的世界书内容')
await sdk.worldbook.clearContent('combat-mode')
await sdk.worldbook.reset()
```

### `sdk.utils`

负责剪贴板和轻量格式处理。

```typescript
await sdk.utils.clipboard.writeText('复制这段文本')
const text = await sdk.utils.clipboard.readText()

const html = sdk.utils.format.markdown('**加粗** _斜体_ `代码`')
const safeHtml = sdk.utils.format.sanitize('<script>alert(1)</script>')
```

---

## React 集成

### 导出入口

```typescript
import { CharxSdk } from '@ai_wanjia/sdk'
import { CharxProvider, useChat, useVariable, useCharacter, usePersona, useStorage } from '@ai_wanjia/sdk/react'
```

### `CharxProvider`

`CharxProvider` 负责初始化 SDK 并通过 React Context 向下提供实例。

```tsx
import { CharxProvider } from '@ai_wanjia/sdk/react'

export default function App() {
  return (
    <CharxProvider
      fallback={<div>连接中...</div>}
      errorFallback={(error) => <div>初始化失败：{error.message}</div>}
    >
      <YourApp />
    </CharxProvider>
  )
}
```

### Hooks

| Hook | 作用 |
|------|------|
| `useChat()` | 管理消息列表、发送、流式任务、停止、继续、重试 |
| `useVariable(key, defaultValue)` | 订阅单个变量，变量不存在时显示默认值，`setValue` 写入成功后会立即同步本地状态 |
| `useVariables()` | 订阅全部变量 |
| `useCharacter()` | 读取当前角色信息 |
| `usePersona()` | 读取当前用户人设 |
| `useStorage(key, defaultValue)` | 读取并同步一个持久化存储值，返回 `[value, setValue, removeValue]` |

示例：

```tsx
import { useChat, useVariable, useCharacter, useStorage } from '@ai_wanjia/sdk/react'

function GamePanel() {
  const character = useCharacter()
  const { messages, send, status } = useChat()
  const [hp, setHp] = useVariable<number>('hp', 100)
  const [theme, setTheme, removeTheme] = useStorage('preferredTheme', 'deep-sea')

  return (
    <div>
      <h2>{character?.name}</h2>
      <p>HP: {hp}</p>
      <p>Theme: {theme}</p>

      <button onClick={() => setHp(hp + 5)}>恢复 5 点 HP</button>
      <button onClick={() => setTheme('mist')}>切换主题</button>
      <button disabled={status !== 'idle'} onClick={() => send('继续')}>
        继续
      </button>

      {messages.map((message) => (
        <div key={message.id}>{message.content}</div>
      ))}
    </div>
  )
}
```

### 组件

#### `Thread.MessageList`

`Thread` 是一个命名空间对象，当前导出的是 `Thread.MessageList` 和 `Thread.MessageItem`。

```tsx
import { Thread, useChat } from '@ai_wanjia/sdk/react'

function ChatView() {
  const { messages, status, loadMore } = useChat()

  return (
    <Thread.MessageList
      messages={messages}
      isStreaming={status === 'streaming'}
      onLoadMore={loadMore}
    />
  )
}
```

#### `MdRenderer`

```tsx
import { MdRenderer } from '@ai_wanjia/sdk/react'

function MessageBody({ content }: { content: string }) {
  return <MdRenderer content={content} />
}
```

---

## 包导出

| 导出路径 | 内容 |
|----------|------|
| `@ai_wanjia/sdk` | 核心 SDK、事件常量、类型 |
| `@ai_wanjia/sdk/react` | React Provider、Hooks、组件 |
| `@ai_wanjia/sdk/types` | 独立类型导出 |

---

## 通信协议

SDK 通过 `postMessage` 与平台通信。

| 方向 | 消息类型 | 说明 |
|------|----------|------|
| iframe → 平台 | `__charx_rpc__` | 方法调用 |
| 平台 → iframe | `__charx_ready__` | 初始化握手成功 |
| 平台 → iframe | `__charx_rpc_response__` | RPC 响应 |
| 平台 → iframe | `__charx_push__` | 平台主动推送事件 |

如果要直接订阅底层推送事件，可以使用：

```typescript
import { CharxSdk, PUSH } from '@ai_wanjia/sdk'

const sdk = CharxSdk.getInstance()
await sdk.init()

const unwatch = sdk.getRpc().push(PUSH.CHAT_STREAM_CHUNK, (data) => {
  console.log(data)
})
```

---

## 构建与开发

```bash
npm run build
npm run dev
npm run typecheck
```

输出目录为 `dist/`。

---

## 相关文档

- 仓库内创作者教程：`docs/sdk-creator-guide.md`
- 原生模板：`sdk/examples/charx-h5-starter`
- React 模板：`sdk/examples/charx-react-starter`

---

## 许可

内部使用，版权归玩家AI平台所有。
