TinyRobot Sender:打造强大的 AI 聊天输入体验
js
为什么输入组件如此重要
在 AI 聊天应用中,输入框是用户与 AI 之间最重要的交互界面。一个优秀的输入组件,不仅是文本的"收容所",更是连接用户意图与 AI 能力的桥梁。无论模型能力多么强大,如果用户无法精准、高效地表达自己的想法,整个 AI 体验都会大打折扣。
传统的 <input> 或 <textarea> 已经无法满足现代 AI 应用的复杂需求。用户需要:
- 结构化输入:通过模板快速搭建复杂 Prompt
- 智能联想:输入时自动提示相关话题或产品
- @提及:快速引用预设角色或上下文
- 语音输入:解放双手,自然表达
- 文件上传:将图片、文档等作为对话上下文
TinyRobot Sender 正是为此而生 ------ 它是一个高度可组合、可扩展的 AI 聊天输入组件,基于 Tiptap 富文本编辑器构建,提供企业级的输入体验。
快速上手
1. 基础用法:模式切换
Sender 支持单行 (single) 和多行 (multiple) 两种输入模式。单行模式下内容超出宽度时会自动切换为多行,无需用户手动调整。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const content = ref('')
const mode = ref<'single' | 'multiple'>('single')
const handleSubmit = (text: string) => {
console.log('提交内容:', text)
content.value = ''
}
</script>
<template>
<div>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<button
:style="{ background: mode === 'single' ? '#1476ff' : '#f0f0f0', color: mode === 'single' ? '#fff' : '#333' }"
@click="mode = 'single'"
>
单行模式
</button>
<button
:style="{ background: mode === 'multiple' ? '#1476ff' : '#f0f0f0', color: mode === 'multiple' ? '#fff' : '#333' }"
@click="mode = 'multiple'"
>
多行模式
</button>
</div>
<TrSender
v-model="content"
:mode="mode"
placeholder="请输入消息..."
:max-length="200"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
核心 Props 说明:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
modelValue / v-model |
输入框内容绑定 | string |
'' |
mode |
输入模式 | `'single' | 'multiple'` |
disabled |
是否禁用 | boolean |
false |
loading |
加载状态(显示停止按钮) | boolean |
false |
maxLength |
最大输入长度 | number |
Infinity |
showWordLimit |
显示字数统计 | boolean |
false |
size |
组件尺寸 | `'normal' | 'small'` |
扩展架构:可插拔的功能增强
Sender 的核心设计理念是可插拔的扩展架构 。通过 extensions prop,你可以按需组合 Template、Mention、Suggestion 等功能扩展,每个扩展互相独立、自由组合。
typescript
// 扩展就是普通的数组元素
const extensions = [
TrSender.template(templateData),
TrSender.mention(mentionItems),
TrSender.suggestion(suggestionItems, { filterFn: customFilter })
]
// 通过 props 传入
<TrSender :extensions="extensions" />
提供两种集成方式:
- 便捷函数(推荐) :
TrSender.template()、TrSender.mention()、TrSender.suggestion(),用于简单场景 - 标准配置 :
TrSender.Mention.configure({...}),用于复杂配置(如自定义 onSelect 回调、filterFn 等)
2. Template 扩展:结构化 Prompt 模板
Template 扩展让用户通过预设模板快速填写结构化 Prompt。支持 text(固定文本)、block(可编辑字段)、select(下拉选择器)三种节点类型。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@opentiny/vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { TemplateItem, StructuredData } from '@opentiny/tiny-robot'
const content = ref('')
const templateData = ref<TemplateItem[]>([])
// 便捷函数:传入响应式 ref,数据变化时自动同步编辑器
const extensions = [TrSender.template(templateData)]
const setTemplate = () => {
templateData.value = [
{ type: 'text', content: '请帮我写一份关于' },
{ type: 'block', content: '人工智能' },
{ type: 'text', content: '的' },
{ type: 'block', content: '技术报告' },
{ type: 'text', content: ',字数要求' },
{ type: 'block', content: '3000字' },
{ type: 'text', content: ',风格要求' },
{
type: 'select',
placeholder: '选择风格',
content: '',
options: [
{ label: '正式学术', value: 'formal_academic' },
{ label: '通俗易懂', value: 'easy_reading' },
{ label: '技术深度', value: 'technical_deep' },
]
},
{ type: 'text', content: '。' },
]
}
const handleSubmit = (text: string, data?: StructuredData) => {
console.log('纯文本:', text)
// data 结构:
// [
// { type: 'text', content: '请帮我写一份关于' },
// { type: 'block', content: '人工智能' },
// { type: 'text', content: '的技术报告...' }
// ]
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<Button size="small" @click="setTemplate">插入 Prompt 模板</Button>
<TrSender
v-model="content"
:extensions="extensions"
mode="multiple"
placeholder="点击按钮插入模板..."
:max-length="500"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
Template 通过响应式 ref 机制,当 templateData.value 发生变化时,编辑器内容会自动更新,光标也会自动聚焦到第一个可编辑字段,无需手动调用任何方法。
3. Mention 扩展:@提及功能
Mention 扩展实现了类似社交平台的 @提及功能。输入触发字符(默认 @)会弹出选择列表,支持键盘导航(↑↓)和搜索过滤。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { MentionItem, StructuredData } from '@opentiny/tiny-robot'
const content = ref('')
const assistants: MentionItem[] = [
{ label: '代码助手', value: 'you_are_a_coding_expert' },
{ label: '文案大师', value: 'you_are_a_copywriter' },
{ label: '数据分析师', value: 'you_are_a_data_analyst' },
{ label: '翻译专家', value: 'you_are_a_translator' },
]
// 便捷函数:默认触发字符为 @
const extensions = [TrSender.mention(assistants, '@')]
// 也可以自定义触发字符,如用 # 替代 @
// const extensions = [TrSender.mention(hashtags, '#')]
const handleSubmit = (text: string, data?: StructuredData) => {
// text: "帮我分析 @代码助手 返回的代码"
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'mention', content: '代码助手', value: 'you_are_a_coding_expert' },
// { type: 'text', content: ' 返回的代码' }
// ]
// 提取所有提及的助手
const mentions = data?.filter(item => item.type === 'mention') || []
const assistantIds = mentions.map(m => m.value)
console.log('提及的助手:', assistantIds)
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<p style="color: #666; font-size: 14px; margin: 0;">
💡 输入 <code>@</code> 触发提及选择,支持键盘导航和搜索过滤
</p>
<TrSender
v-model="content"
:extensions="extensions"
placeholder="输入 @ 选择助手..."
mode="multiple"
:max-length="500"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
Mention 扩展支持三个配置项:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items |
`MentionItem[] | Ref<MentionItem[]>` | [] |
char |
string |
'@' |
触发字符 |
allowSpaces |
boolean |
false |
是否允许触发字符后输入空格 |
4. Suggestion 扩展:智能联想与自定义过滤
Suggestion 扩展提供智能输入联想功能。用户输入时自动弹出建议列表,支持键盘导航和 Tab 自动补全。
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { SenderSuggestionItem } from '@opentiny/tiny-robot'
const input = ref('')
const suggestions: SenderSuggestionItem[] = [
{ content: 'ECS-云服务器卡顿排查' },
{ content: 'ECS-备份弹性云服务器' },
{ content: 'ECS-实例无法启动处理' },
{ content: 'CDN-权限管理配置指南' },
{ content: 'CDN-缓存刷新问题排查' },
{ content: 'OSS-存储桶访问控制' },
]
// 标准配置:使用自定义 filterFn 实现按分类前缀匹配
const extensions = computed(() => [
TrSender.Suggestion.configure({
items: suggestions,
// 自定义过滤逻辑
filterFn: (items: SenderSuggestionItem[], query: string) => {
if (!query) return []
const lowerQuery = query.toLowerCase()
// 按 - 前面的分类标签匹配
return items.filter((item) => {
const category = item.content.split('-')[0].toLowerCase()
return category.startsWith(lowerQuery)
})
},
showAutoComplete: true,
popupWidth: 500,
onSelect: (item) => {
console.log('选中建议:', item.content)
},
}),
])
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<p style="color: #666; font-size: 14px; margin: 0;">
💡 输入 ECS、CDN 或 OSS 查看分类联想,Tab 键快速补全
</p>
<TrSender
v-model="input"
:extensions="extensions"
placeholder="输入 ECS 或 CDN 查看建议..."
@submit="(text) => console.log('提交:', text)"
/>
</div>
</template>
Suggestion 配置项详解:
| 配置项 | 说明 | 类型 | 默认值 |
|---|---|---|---|
items |
建议项列表 | SenderSuggestionItem[] |
[] |
filterFn |
自定义过滤函数,不传则不过滤 | (items, query) => items |
undefined |
showAutoComplete |
显示灰色补全提示 | boolean |
true |
activeSuggestionKeys |
激活建议的按键 | string[] |
['Enter'] |
popupWidth |
弹窗宽度(支持数字/百分比) | `number | string` |
onSelect |
选中回调,返回 false 阻止默认回填 | `(item) => void | false` |
5. VoiceButton:语音输入
VoiceButton 是一个独立的语音输入按钮组件,通过 footer 或 footer-right 插槽集成到 Sender 中。它同时支持浏览器内置的 Web Speech API 和自定义第三方语音识别服务。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, VoiceButton } from '@opentiny/tiny-robot'
const content = ref('')
const voiceMode = ref<'mixed' | 'continuous'>('mixed')
const handleSubmit = (text: string) => {
console.log('提交内容:', text)
content.value = ''
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 12px; align-items: center;">
<span>语音模式:</span>
<label><input type="radio" v-model="voiceMode" value="mixed" /> 混合输入</label>
<label><input type="radio" v-model="voiceMode" value="continuous" /> 连续识别</label>
</div>
<p style="color: #666; font-size: 13px; margin: 0;">
{{ voiceMode === 'mixed'
? '识别结果追加到输入框,可继续编辑修改'
: '持续识别并自动替换内容,适合长段语音输入' }}
</p>
<!-- VoiceButton 通过 key 在模式切换时重新实例化 -->
<TrSender
:key="voiceMode"
v-model="content"
mode="multiple"
@submit="handleSubmit"
>
<template #footer-right>
<VoiceButton
:speech-config="voiceMode === 'mixed'
? { autoReplace: false, interimResults: true }
: { autoReplace: true, continuous: true }"
tooltip="语音输入"
@speech-final="(transcript) => { content += transcript }"
/>
</template>
</TrSender>
</div>
</template>
VoiceButton 核心配置:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
speechConfig.autoReplace |
是否自动替换内容(false 则追加) | boolean |
true |
speechConfig.continuous |
是否持续识别 | boolean |
false |
speechConfig.interimResults |
是否返回中间结果 | boolean |
false |
speechConfig.lang |
识别语言 | string |
浏览器默认语言 |
autoInsert |
是否自动插入识别结果到编辑器 | boolean |
true |
如需集成阿里云、百度、Azure 等第三方语音服务,通过 speechConfig.customHandler 传入自定义处理器即可。
6. UploadButton:文件上传
UploadButton 允许用户上传文件(图片、文档等)作为对话的上下文输入,支持文件类型过滤、大小限制和数量限制。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton } from '@opentiny/tiny-robot'
const content = ref('')
const uploadedFiles = ref<File[]>([])
const handleSubmit = (text: string) => {
console.log('提交文本:', text)
console.log('附带文件:', uploadedFiles.value)
content.value = ''
uploadedFiles.value = []
}
const handleFilesSelected = (files: File[]) => {
uploadedFiles.value = files
// 可以在这里做上传逻辑或直接作为附件信息
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<TrSender
v-model="content"
mode="multiple"
placeholder="输入问题,或上传相关文件..."
clearable
@submit="handleSubmit"
>
<template #footer-right>
<UploadButton
accept="image/*,.pdf,.doc,.docx"
:multiple="true"
:max-size="10"
:max-count="5"
tooltip="上传文件(最多5个,每个≤10MB)"
tooltip-placement="top"
@select="handleFilesSelected"
/>
</template>
</TrSender>
<div v-if="uploadedFiles.length" style="font-size: 13px; color: #666;">
已选择:{{ uploadedFiles.map(f => f.name).join(', ') }}
</div>
</div>
</template>
UploadButton 核心配置:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
accept |
接受的 MIME 类型 | string |
'*' |
multiple |
是否支持多选 | boolean |
false |
maxSize |
文件大小限制(MB) | number |
- |
maxCount |
最大文件数量 | number |
- |
tooltip |
悬停提示文本 | string |
- |
7. 自定义插槽:灵活布局
Sender 提供多个插槽位置以支持灵活的布局扩展:
| 插槽 | 位置 | 作用域 | 典型用途 |
|---|---|---|---|
header |
输入框上方 | - | 标题、提示信息 |
prefix |
输入框内部左侧 | - | AI 图标、模型选择 |
content |
编辑器内容区域 | { editor } |
完全自定义编辑器内容 |
actions-inline |
单行模式操作区 | - | 单行模式下的操作按钮 |
footer |
底部左侧 | { editor, hasContent, disabled, loading } |
增强功能按钮 |
footer-right |
底部右侧 | - | 上传、语音按钮 |
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton, VoiceButton } from '@opentiny/tiny-robot'
const content = ref('')
const deepThinking = ref(false)
const handleSubmit = (text: string) => {
console.log('提交:', text, deepThinking.value ? '(深度思考模式)' : '')
content.value = ''
}
</script>
<template>
<TrSender
v-model="content"
placeholder="输入你的问题..."
mode="multiple"
clearable
@submit="handleSubmit"
>
<!-- 顶部标题 -->
<template #header>
<div style="text-align: center; font-weight: 700; padding: 8px 0;">
🤖 AI 智能助手
</div>
</template>
<!-- 输入框前缀图标 -->
<template #prefix>
<span style="font-size: 22px; margin-right: 8px;">🧠</span>
</template>
<!-- 底部左侧功能按钮 -->
<template #footer="{ editor, hasContent }">
<button
:style="{
padding: '4px 12px', borderRadius: '16px', border: '1px solid #e0e0e0',
background: deepThinking ? '#1476ff' : 'transparent',
color: deepThinking ? '#fff' : '#666', cursor: 'pointer', fontSize: '13px'
}"
@click="deepThinking = !deepThinking"
>
{{ deepThinking ? '✓ 深度思考' : '🌐 深度思考' }}
</button>
<span v-if="hasContent" style="font-size: 12px; color: #999; margin-left: 8px;">
已输入 {{ content.length }} 字
</span>
</template>
<!-- 底部右侧功能按钮 -->
<template #footer-right>
<UploadButton
accept="image/*"
tooltip="上传图片"
tooltip-placement="top"
@select="(files) => console.log('选择文件:', files)"
/>
<VoiceButton
:speech-config="{ autoReplace: false, interimResults: true }"
tooltip="语音输入"
tooltip-placement="top"
/>
</template>
</TrSender>
</template>
8. submitType:灵活控制提交流程
不同的应用场景需要不同的提交方式。Sender 通过 submitType 属性提供三种方式:
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const content = ref('')
const submitType = ref<'enter' | 'ctrlEnter' | 'shiftEnter'>('ctrlEnter')
const handleSubmit = (text: string) => {
console.log('提交:', text)
content.value = ''
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 16px; align-items: center;">
<span>提交方式:</span>
<label><input type="radio" v-model="submitType" value="enter" /> Enter 提交</label>
<label><input type="radio" v-model="submitType" value="ctrlEnter" /> Ctrl+Enter 提交</label>
<label><input type="radio" v-model="submitType" value="shiftEnter" /> Shift+Enter 提交</label>
</div>
<TrSender
v-model="content"
:submit-type="submitType"
placeholder="输入内容后使用对应快捷键提交..."
@submit="handleSubmit"
/>
</div>
</template>
快捷键行为对照表:
| submitType | 提交快捷键 | 换行快捷键 |
|---|---|---|
enter |
Enter | Ctrl+Enter / Shift+Enter |
ctrlEnter |
Ctrl+Enter | Enter |
shiftEnter |
Shift+Enter | Enter |
提示:单行模式下使用换行快捷键时,会自动切换为多行模式。
结构化数据:读懂用户的意图
Sender 的 submit 事件不仅返回纯文本,当使用了 Template 或 Mention 扩展时,还会返回结构化的 data 参数,让你能够精确提取用户的意图。
typescript
// submit 事件签名
@submit="(text: string, data?: StructuredData) => { ... }"
Mention 结构化数据
typescript
const handleSubmit = (text: string, data?: StructuredData) => {
// 用户输入: "帮我分析 @代码助手 的代码质量"
// text: "帮我分析 @代码助手 的代码质量"
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'mention', content: '代码助手', value: 'you_are_a_coding_expert' },
// { type: 'text', content: ' 的代码质量' }
// ]
// 提取提及的助手 ID 列表
const mentions = data?.filter(item => item.type === 'mention') || []
const assistantIds = mentions.map(item => item.value)
// 自定义 Slack 风格格式
const customText = data?.map(item =>
item.type === 'mention' ? `<@${item.value}>` : item.content
).join('')
}
Template 结构化数据
typescript
const handleSubmit = (text: string, data?: StructuredData) => {
// 用户通过模板输入后提交
// text: "请帮我写一份关于人工智能的技术报告,字数要求3000字"
// data: [
// { type: 'text', content: '请帮我写一份关于' },
// { type: 'block', content: '人工智能' },
// { type: 'text', content: '的技术报告...' }
// ]
// 提取所有可编辑块的值
const blocks = data?.filter(item => item.type === 'block') || []
const blockValues = blocks.map(b => b.content)
}
这种结构化设计让你可以轻松实现:
- 将 @提及转换为自定义协议格式
- 提取模板中的关键填充字段
- 构建更丰富的 AI 请求上下文
v0.4 升级与 SenderCompat 迁移
Sender 在 v0.4 版本进行了重大重构,底层从传统 textarea 升级为基于 Tiptap 的富文本编辑器,带来了更强大的扩展能力和更灵活的插槽系统。
如果你的项目正在使用 v0.3.x
TinyRobot 提供了 SenderCompat 兼容组件作为过渡方案,只需修改一行导入:
typescript
// 旧代码 (v0.3.x)
import { TrSender } from '@opentiny/tiny-robot'
// 使用 SenderCompat 快速迁移
import { TrSenderCompat as TrSender } from '@opentiny/tiny-robot'
推荐的迁移路径:
v0.3.x Sender → SenderCompat(快速迁移,改导入即可)→ v0.4 Sender(按需完全升级)
v0.4 主要变更一览
| 变更类型 | v0.3.x | v0.4 | 说明 |
|---|---|---|---|
| 语音输入 | allow-speech prop |
VoiceButton 独立组件 |
组件化设计,通过插槽集成 |
| 文件上传 | allow-files + button-group |
UploadButton 独立组件 |
扁平化配置,更灵活 |
| 智能联想 | suggestions prop |
Suggestion 扩展 |
通过 extensions 接入 |
| @提及 | - | Mention 扩展 |
v0.4 新增功能 |
| 模板填充 | v-model:templateData |
Template 扩展 |
响应式 ref 驱动 |
| 主题 | theme prop |
ThemeProvider 包裹 |
全局继承,所有子组件自动生效 |
| 模板类型名 | type: 'template' |
type: 'block' |
类型名称变更,需手动适配 |
完全升级到 v0.4
如果你准备完全升级到 v0.4,下面是一个典型的迁移对照:
vue
<!-- v0.3.x 写法 -->
<template>
<TrSender
v-model="content"
:allow-speech="true"
:speech="{ lang: 'zh-CN' }"
:allow-files="true"
:button-group="{ file: { accept: 'image/*' } }"
:suggestions="filteredSuggestions"
@speech-end="onSpeechEnd"
@files-selected="onFilesSelected"
/>
</template>
<!-- v0.4 写法 -->
<template>
<TrSender
v-model="content"
:extensions="extensions"
>
<template #footer-right>
<UploadButton accept="image/*" @select="onFilesSelected" />
<VoiceButton :speech-config="{ lang: 'zh-CN' }" @speech-final="onSpeechEnd" />
</template>
</TrSender>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton, VoiceButton } from '@opentiny/tiny-robot'
const content = ref('')
const extensions = [
TrSender.Suggestion.configure({ items: [], filterFn: myFilter }),
]
const onFilesSelected = (files: File[]) => { /* ... */ }
const onSpeechEnd = (transcript: string) => { /* ... */ }
</script>
事件与方法速查
事件
| 事件名 | 说明 | 回调参数 |
|---|---|---|
update:modelValue |
内容更新 | (value: string) |
submit |
提交内容 | (text: string, data?: StructuredData) |
cancel |
取消操作(loading 状态下点击停止) | () |
clear |
清空内容 | () |
focus |
获得焦点 | (event: FocusEvent) |
blur |
失去焦点 | (event: FocusEvent) |
input |
输入变化 | (value: string) |
方法(通过 ref 调用)
| 方法 | 说明 | 参数 |
|---|---|---|
focus() |
使输入框获取焦点 | - |
blur() |
使输入框失去焦点 | - |
clear() |
清空输入内容 | - |
submit() |
手动触发提交 | - |
setContent(content) |
设置编辑器内容 | (content: string) |
getContent() |
获取编辑器内容 | - 返回 string |
cancel() |
手动触发取消 | - |
总结
TinyRobot Sender 通过以下设计原则,为 AI 聊天应用提供了企业级的输入体验:
- 可组合:各功能通过独立的扩展或组件实现,按需引入
- 响应式:扩展配置支持 Vue ref,数据变化自动同步 UI
- 结构化:submit 事件返回文本 + 结构化数据,精准传达用户意图
- 可插拔:Template、Mention、Suggestion 三大扩展独立运作,互不干扰
- 渐进迁移:通过 SenderCompat 兼容组件,平滑从 v0.3.x 升级
无论是构建一个简单的 AI 问答输入框,还是打造复杂的企业级 Copilot 交互界面,Sender 都能胜任。
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot智能助手、GenUI等新产品,加速企业应用的智能化改造,实现AI理解用户意图自主完成任务。
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design/ TinyRobot 代码仓库:github.com/opentiny/ti... (欢迎star ⭐) TinyRobot skill源码:github.com/opentiny/ag... (欢迎 Star ⭐)