Posted in

为什么你的Go打字动画在VS Code终端里乱码?深入分析TERM环境变量协商失败的5种场景

第一章:Go打字动画在VS Code终端乱码的现象与本质

当使用 Go 编写的命令行工具(如 cobra CLI 应用)实现打字动画(typewriter effect)时,在 VS Code 集成终端中常出现字符重叠、光标错位、残留乱码或换行异常等问题。典型表现为:本应逐字打印的文本显示为“Hello Word”,或动画结束后末尾残留不可见控制符,甚至触发终端渲染崩溃。

乱码的根源在于终端能力与 ANSI 控制序列的不匹配

VS Code 终端(基于 xterm.js)对部分 ANSI 转义序列的支持存在边界行为。Go 程序若直接使用 \r 回车而不配合 \033[K(清行)或 \033[2K(清除整行),在快速刷新时旧内容未被彻底擦除,导致新旧字符叠加。此外,Windows 系统下 VS Code 默认启用“Windows Console Host”兼容模式,可能截断或误解析 UTF-8 编码的宽字符(如 emoji 或中文),加剧乱码。

验证与复现步骤

  1. 创建最小复现实例:
    package main
    import (
    "fmt"
    "time"
    )
    func main() {
    msg := "Hello, 世界!"
    for i := 0; i <= len(msg); i++ {
        fmt.Print("\r" + msg[:i]) // 仅用 \r,无清行
        time.Sleep(100 * time.Millisecond)
    }
    fmt.Println() // 换行收尾
    }
  2. 在 VS Code 终端运行 go run main.go,观察末尾是否残留“界!”或显示为“Hello, 世!”

推荐修复方案

  • 强制清行:将 fmt.Print("\r" + msg[:i]) 替换为 fmt.Print("\r\033[2K" + msg[:i])
  • 显式设置终端编码:启动前执行 chcp 65001(Windows)或确保 export LANG=en_US.UTF-8(Linux/macOS);
  • 禁用 VS Code 终端硬件加速:在设置中搜索 terminal.integrated.gpuAcceleration 并设为 off,可缓解 xterm.js 渲染竞态。
问题现象 根本原因 修复动作
字符重叠 \r 未清除原行剩余字符 添加 \033[2K 清行控制序列
中文显示为 终端未启用 UTF-8 模式 执行 chcp 65001 或配置 locale
动画卡顿/跳帧 time.Sleep 精度受 OS 调度影响 改用 time.AfterFunc + channel 控制节奏

第二章:TERM环境变量协商失败的底层机制剖析

2.1 终端类型协商流程:从pty创建到TERM值传递的全链路追踪

sshdocker exec -it 启动交互式会话时,内核通过 ioctl(TIOCSCTTY) 分配伪终端(pty)主从设备对,随后父进程(如 sshd)将从设备文件描述符传递给子进程(如 bash)。

pty 初始化关键调用链

  • open("/dev/pts/N") 获取从设备句柄
  • setsid() 创建新会话并成为会话首进程
  • ioctl(slave_fd, TIOCSCTTY, 0) 将从设备设为控制终端

TERM 环境变量注入时机

// 子进程启动前,shell 父进程设置环境变量
putenv("TERM=xterm-256color");  // 实际值常由客户端协商决定(如 SSH_CLIENT)
execve("/bin/bash", argv, environ);

此处 TERM 并非内核传递,而是用户态进程依据协议上下文(如 SSH 的 terminal-type channel request)主动注入。bash启动后读取该值以初始化terminfo` 数据库匹配。

终端能力协商流程(简化)

graph TD
    A[SSH Client] -->|SSH_MSG_CHANNEL_REQUEST<br>terminal-type=xterm-kitty| B(SSH Server)
    B --> C[spawn shell with TERM=xterm-kitty]
    C --> D[bash reads TERM → loads terminfo entry]
组件 是否参与 TERM 传递 说明
Linux 内核 仅提供 tty 设备抽象
sshd 解析 SSH 协议并注入环境变量
bash/zsh 读取并用于 terminal 初始化

2.2 VS Code内置终端的伪TTY实现与TERM默认策略实测分析

VS Code 内置终端并非原生 TTY,而是基于 xterm.js 实现的伪 TTY(Pseudo-TTY),由 Electron 主进程通过 pty 模块(如 node-pty)桥接。

TERM 环境变量默认行为

启动时,VS Code 自动设置:

$ echo $TERM
xterm-256color

该值由 VS Code 内部硬编码策略决定,不继承系统 shell 的 TERM,且无法通过 settings.json 直接覆盖(需 terminal.integrated.env.* 注入)。

伪TTY能力验证

运行以下命令检测终端能力:

# 检查是否支持 true color 和 cursor positioning
$ tput colors && tput setaf 208 && echo "✅ TrueColor OK"
$ tput civis && tput cnorm && echo "✅ Cursor control OK"

逻辑分析tput 依赖 TERM 查找 terminfo 数据库。xterm-256color 提供 256 色及基础 CSI 序列支持,但缺失 xterm-direct 的 16M 色能力——这解释了为何 echo -e "\e[38;2;255;105;180mPink" 在部分主题下渲染异常。

默认 TERM 策略对比表

场景 TERM 值 支持 true color 支持鼠标事件 备注
默认集成终端 xterm-256color ❌(仅 256 色) 兼容性优先
手动覆盖为 xterm-direct xterm-direct 需启用 "terminal.integrated.enableColorfulText": true
graph TD
    A[VS Code 启动终端] --> B{调用 node-pty.spawn}
    B --> C[分配伪TTY主/从设备对]
    C --> D[设置环境变量 TERM=xterm-256color]
    D --> E[加载 xterm.js 渲染器]
    E --> F[映射 CSI 序列到 DOM 样式]

2.3 Go runtime对os.Stdin/os.Stdout的终端能力探测逻辑逆向解读

Go runtime 在 os 包初始化时通过 syscall.Syscallioctl 系统调用探测文件描述符是否关联终端(tty),核心路径位于 src/os/file_unix.goinit()file.go 中的 isTerminal 判断。

终端检测入口逻辑

// src/os/file_unix.go(简化)
func (f *File) isTerminal() bool {
    var termios syscall.Termios
    _, _, err := syscall.Syscall6(
        syscall.SYS_IOCTL,
        f.fd, uintptr(syscall.TCGETS),
        uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
    return err == 0
}

该调用尝试执行 TCGETS ioctl —— 仅对真实终端设备返回成功;对管道、重定向文件或 /dev/null 均返回 ENOTTY 错误。

能力缓存策略

  • 每个 *os.File 实例首次调用 isTerminal() 后,结果被缓存在 f.isTerminal 字段(sync.Once + atomic.Bool);
  • os.Stdin/Stdout/Stderros.init() 中即完成探测并固化能力标记。
文件描述符 ioctl 成功 isTerminal() 返回 典型场景
/dev/tty true 交互式终端
pipe ❌ (ENOTTY) false cmd1 | cmd2
/dev/null false 重定向丢弃输出
graph TD
    A[os.Stdin.isTerminal()] --> B{ioctl(fd, TCGETS, &termios)}
    B -->|success| C[cache = true]
    B -->|ENOTTY| D[cache = false]
    C & D --> E[后续调用直接返回缓存值]

2.4 ANSI转义序列兼容性断层:xterm-256color vs xterm vs vscode-1.0的实证对比

不同终端声明对ANSI控制序列的支持存在显著差异,尤其在256色模式、光标定位与清除操作上。

色彩支持边界测试

# 测试256色索引#196(标准红色)在各终端中的渲染一致性
echo -e "\033[38;5;196mRED\033[0m"

该序列在 xterm-256color 中精确映射至sRGB(205,0,0),xterm(未声明256色)降级为16色调色板第1色,而 vscode-1.0 对>231的高亮色索引存在1:1映射但缺失gamma校准。

兼容性对照表

特性 xterm-256color xterm vscode-1.0
\033[2J(清屏) ✅ 完整清空 ⚠️ 仅清可见区
\033[38;5;N ✅ 支持0–255 ❌ 仅0–15 ✅ 0–255(无抖动)
\033[?1049h(备用缓冲区) ❌ 忽略

渲染行为差异流程

graph TD
    A[发送\033[38;5;196m] --> B{xterm-256color?}
    B -->|是| C[查256色LUT→sRGB]
    B -->|否| D{vscode-1.0?}
    D -->|是| E[直通索引→WebGL纹理]
    D -->|否| F[降级为16色集]

2.5 Go动画库(如gocui、bubbletea)中TermEnv检测绕过陷阱的调试复现

TermEnv检测的常见逻辑链

bubbleteagocui 均依赖 os.Getenv("TERM")os.Getenv("COLORTERM") 判断终端能力。但部分 CI 环境或容器化部署会伪造环境变量,导致误判为支持 ANSI 的交互式终端。

绕过触发条件复现

以下代码可稳定触发 bubbleteaIsTerminal() 误判:

package main

import (
    "os"
    "runtime"
    "github.com/charmbracelet/bubbletea"
)

func main() {
    // 模拟CI环境:伪造TERM但禁用实际TTY
    os.Setenv("TERM", "xterm-256color")
    os.Setenv("CI", "true") // bubbletea v0.24+ 会检查此变量
    os.Stdout = nil          // 强制破坏fd

    p := tea.NewProgram(nil)
    p.Start() // panic: invalid file descriptor
}

逻辑分析tea.NewProgram 初始化时调用 isTerminal(os.Stdout),该函数在 Linux/macOS 下通过 syscall.Ioctl 检查 os.Stdout.Fd() 是否为 TTY;但 os.Stdout = nil 导致 Fd() 返回 -1Ioctl 失败后 fallback 到 os.Getenv("TERM") —— 此时因 CI=truebubbletea 主动忽略,最终误判为“非终端”,却未提前校验 os.Stdout 可用性,引发 runtime panic。

关键环境变量影响对比

环境变量 值示例 bubbletea v0.24+ 行为 gocui v0.6.0 行为
TERM + CI=true xterm-256color 跳过 TTY 检查,强制降级为非交互模式 忽略 CI,仍尝试 ioctl → panic
COLORTERM=24bit + NO_COLOR=1 尊重 NO_COLOR,禁用颜色 不识别 NO_COLOR,颜色照常输出

修复路径示意

graph TD
    A[启动程序] --> B{os.Stdout.Fd() >= 0?}
    B -->|否| C[立即返回 ErrInvalidStdout]
    B -->|是| D[执行 syscall.Ioctl TIOCGWINSZ]
    D --> E{成功?}
    E -->|否| F[检查 TERM/COLORTERM + CI]
    E -->|是| G[正常初始化 TUI]

第三章:五类典型协商失败场景的精准归因

3.1 VS Code远程开发(SSH/Dev Container)中TERM被服务端覆盖的链路拦截实验

在 VS Code 远程开发中,TERM 环境变量常被服务端(如 sshd 或容器入口脚本)强制重写为 xterm-256color,导致本地终端能力丢失。

关键拦截点定位

  • SSH 连接阶段:/etc/ssh/sshd_configAcceptEnv TERM 控制是否接收客户端 TERM
  • Dev Container 启动阶段:devcontainer.jsonremoteEnvpostCreateCommand 可能覆盖环境
  • Shell 初始化链:/etc/profile~/.bashrc~/.vscode-server/.../bin/code-shell 逐层覆写

TERM 覆盖链路示意

graph TD
    A[VS Code 客户端] -->|SSH env TERM=screen-256color| B[sshd]
    B --> C{AcceptEnv TERM?}
    C -->|否| D[sshd 强制设为 xterm-256color]
    C -->|是| E[保留 client TERM]
    E --> F[Shell 启动脚本二次覆盖]

拦截验证命令

# 在远程终端执行,观察实际生效值
echo $TERM && ps -o args= -p $$
# 输出示例:xterm-256color /bin/sh -c 'exec "$@"' -- /bin/bash

该命令揭示:$TERM 已被 sshd 或 shell wrapper 覆盖;ps 显示真实启动链,确认覆盖发生在 exec "$@" 封装层。

拦截环节 配置位置 是否可禁用
SSH 层覆盖 /etc/ssh/sshd_config ✅ 修改 AcceptEnv
容器入口覆盖 Dockerfile ENTRYPOINT ✅ 替换或跳过
VS Code Server ~/.vscode-server/.../bin/code-shell ❌ 只读二进制

3.2 Windows Subsystem for Linux(WSL2)下Windows Terminal与VS Code终端的TERM继承冲突验证

当 WSL2 启动时,TERM 环境变量由宿主终端决定:

  • Windows Terminal 默认设为 xterm-256color
  • VS Code 集成终端默认设为 vscode(或 xterm,取决于版本)

冲突现象复现

# 在 VS Code 终端中执行
echo $TERM  # 输出:xterm
stty -a | grep columns  # 可能显示错误列数(因 TERM 不匹配导致 terminfo 解析异常)

该命令依赖 TERM 查找 /usr/share/terminfo/x/xterm 描述;若 VS Code 实际渲染能力与 xterm 不一致(如缺少 setaf 支持),tput setaf 2 将静默失败。

关键差异对比

终端环境 默认 TERM 支持 truecolor `infocmp -1 xterm grep setaf`
Windows Terminal xterm-256color 包含 setaf=\E[38;5;%p1%dm
VS Code Terminal xterm ❌(v1.85前) 仅含 setaf=\E[3%p1%dm

根本原因流程

graph TD
    A[WSL2 启动] --> B{终端注入 TERM}
    B --> C[Windows Terminal → xterm-256color]
    B --> D[VS Code → xterm]
    C --> E[正确加载 256 色 terminfo]
    D --> F[降级使用基础 xterm 描述]
    F --> G[颜色/光标等控制序列失效]

3.3 Go构建产物跨终端分发时硬编码TERM导致的运行时错配诊断

当Go程序在构建时静态嵌入 TERM=xterm-256color(如通过 -ldflags "-X main.term=xterm-256color"),在无终端或TERM受限环境(如Docker Alpine、Windows Git Bash、CI runner)中运行会触发tput失败、颜色库panic或os/exec子进程异常。

常见错配表现

  • tput: No value for $TERM and no -T specified
  • failed to get terminal size: ioctl: inappropriate ioctl for device
  • color.NoColor = true 被意外绕过

根本原因分析

// build-time injection —— 危险!
var term = "xterm-256color" // ← 硬编码,无法随运行时环境动态适配

func init() {
    os.Setenv("TERM", term) // 强制覆盖,破坏runtime环境感知
}

该代码在构建期固化TERM值,绕过os.Getenv("TERM")的运行时协商机制;os.Setenv对已启动进程的os.Stdin.Fd()无感知,导致golang.org/x/term.IsTerminal()返回错误结果。

推荐修复策略

方案 安全性 兼容性 备注
运行时动态探测 ✅ 高 ✅ 全平台 使用 os.Getenv("TERM") + fallback
构建时禁用TERM注入 ✅ 高 移除 -X main.term=...
显式传参控制 ⚠️ 中 启动时 --term=screen
graph TD
    A[Go构建产物] --> B{运行时TERM环境}
    B -->|存在且合法| C[正常启用ANSI]
    B -->|为空/invalid| D[降级为纯文本]
    B -->|硬编码xterm-*| E[子进程ioctl失败]

第四章:可落地的协同修复方案与工程化防御体系

4.1 在main入口注入TERM协商兜底逻辑:os.Setenv + terminal.IsTerminal组合实践

当程序在非交互式环境(如CI管道、容器init进程)中运行时,os.Stdin 可能非终端,导致 term.ReadPassword 等调用阻塞或 panic。需在 main() 最早阶段主动协商终端能力。

兜底策略设计原则

  • 优先信任环境变量 TERM 原值
  • 仅当 !terminal.IsTerminal(int(os.Stdin.Fd()))TERM 未设置时,才安全注入 TERM=dumb
  • 避免覆盖用户显式配置(如 TERM=xterm-256color

实现代码

func initTerminalFallback() {
    if !terminal.IsTerminal(int(os.Stdin.Fd())) {
        if os.Getenv("TERM") == "" {
            os.Setenv("TERM", "dumb") // 强制设为哑终端,禁用ANSI转义
        }
    }
}

逻辑分析terminal.IsTerminal 底层调用 ioctl(TIOCGETA) 检测文件描述符是否关联tty;os.Setenv 在进程启动早期生效,确保后续所有依赖 TERM 的库(如 golang.org/x/term)读取到一致值。

典型环境行为对比

环境类型 IsTerminal() TERM初始值 注入后TERM
本地bash终端 true xterm-256color 不变
GitHub Actions false “” dumb
Docker exec -it true linux 不变
graph TD
    A[main] --> B[initTerminalFallback]
    B --> C{IsTerminal stdin?}
    C -->|false| D{TERM empty?}
    C -->|true| E[跳过]
    D -->|yes| F[os.Setenv TERM=dumb]
    D -->|no| E

4.2 VS Code launch.json与tasks.json中env配置的优先级陷阱与正确写法

VS Code 中环境变量注入存在明确的覆盖链:系统环境 tasks.json env launch.json env launch.json environment(调试器内)。若未注意层级,极易导致路径、SDK 或配置未生效。

环境变量生效优先级(自低到高)

来源 示例位置 是否可覆盖上层
系统环境变量 终端启动时继承 ❌ 不可覆盖
tasks.jsonenv tasksenv 字段 ✅ 可被 launch.json 覆盖
launch.jsonenv configurationsenv ✅ 可被 environment 覆盖
launch.jsonenvironment configurationsenvironment(数组) ✅ 最高优先级
{
  "version": "2.0.0",
  "configurations": [{
    "type": "pwa-node",
    "request": "launch",
    "name": "Run with custom PATH",
    "env": { "PATH": "/opt/mybin:${env:PATH}" }, // ← 覆盖 tasks.json 的 PATH
    "environment": [ { "name": "NODE_ENV", "value": "development" } ] // ← 最终生效
  }]
}

env 是对象字面量,用于简单键值覆盖;environment 是数组,支持动态插值(如 ${workspaceFolder}),且在调试器进程启动时最后注入,优先级最高。${env:PATH} 表示继承当前环境的 PATH 值,避免完全替换。

graph TD
  A[系统环境] --> B[tasks.json env]
  B --> C[launch.json env]
  C --> D[launch.json environment]
  D --> E[调试器最终环境]

4.3 基于github.com/muesli/termenv的动态能力探测+降级渲染适配器开发

终端能力千差万别:从支持24位真彩色的 iTerm2,到仅支持8色的老旧 SSH 终端,甚至无色环境(如 CI 日志流)。硬编码 ANSI 序列必然导致乱码或功能失效。

动态能力探测机制

termenv 提供 termenv.ColorProfile() 自动识别当前终端支持的色彩模型(NoColor / ANSI / TrueColor),并可结合 os.Getenv("TERM_PROGRAM") 等环境变量增强判断精度。

降级策略设计

func NewRenderer() *Renderer {
    profile := termenv.EnvColorProfile()
    r := termenv.NewOutput(os.Stdout).WithProfile(profile)

    // 降级映射表:TrueColor → ANSI → NoColor
    fallbackMap := map[termenv.Profile]termenv.Profile{
        termenv.TrueColor: termenv.ANSI,
        termenv.ANSI:      termenv.NoColor,
    }
    return &Renderer{r: r, fallback: fallbackMap}
}

该构造函数基于运行时探测结果初始化渲染器,并预置降级路径。termenv.Output.WithProfile() 决定后续 Foreground() 等调用是否生成颜色序列;若为 NoColor,所有样式调用自动静默。

能力等级 支持特性 典型环境
TrueColor 16777216 色、RGB 指定 kitty, VS Code 终端
ANSI 16 色 + 基础样式(粗体/下划线) macOS Terminal, GNOME Terminal
NoColor 纯文本,忽略所有样式 Jenkins, Docker logs
graph TD
    A[启动渲染器] --> B{探测 ColorProfile}
    B -->|TrueColor| C[启用 RGB 色彩]
    B -->|ANSI| D[映射至 16 色调色板]
    B -->|NoColor| E[跳过所有 ANSI 序列]
    C --> F[渲染完成]
    D --> F
    E --> F

4.4 CI/CD流水线中终端仿真环境(如act、gha)的TERM标准化注入脚本模板

act 或 GitHub Actions runner 中,多数容器默认未设置 TERM 环境变量,导致 tputcoloramarich 等依赖终端能力的工具降级或报错。

核心注入策略

统一注入 TERM=xterm-256color,兼顾兼容性与色彩支持:

# .github/scripts/ensure-term.sh
#!/bin/sh
export TERM="${TERM:-xterm-256color}"
echo "✅ TERM set to: $TERM"

逻辑说明:"${TERM:-xterm-256color}" 使用 Bash 参数扩展,仅当 TERM 为空或未定义时提供默认值;避免覆盖用户显式配置,同时确保下游 CLI 工具可安全调用 tput colors 等命令。

推荐集成方式

  • jobdefaults.run.pre 中全局注入
  • 或通过 act--env TERM=xterm-256color 启动参数强制设定
工具 支持方式 是否需 root
act --env TERM=....actrc
GitHub Actions env: block 或 pre: script
graph TD
    A[CI Job Start] --> B{TERM set?}
    B -->|No| C[Inject xterm-256color]
    B -->|Yes| D[Preserve existing TERM]
    C & D --> E[Run CLI tools safely]

第五章:超越TERM——面向终端抽象层的Go CLI演进思考

现代CLI工具已远非简单输出文本的脚本集合。当 kubectl 支持动态表格渲染、gh 实现交互式 PR 选择器、terraform 在计划阶段提供结构化差异高亮时,底层终端交互逻辑正从“适配TERM环境变量”跃迁为“构建可组合、可测试、可扩展的终端抽象层”。

终端能力不再是布尔开关而是连续谱系

传统 isatty(os.Stdout) 判断已失效。真实场景中需区分:是否支持24位真彩色(\x1b[38;2;R;G;Bm)、是否启用鼠标事件(\x1b[?1006h)、是否支持光标定位与区域滚动(如 tcellScreen 接口)。以下为某生产级CLI在启动时探测结果的结构化快照:

能力项 检测值 依赖协议 失效降级策略
真彩色支持 true CSI 38/48 退至256色调色板
鼠标点击事件 false DECSET 1006 启用键盘导航模式
行内覆盖刷新 true \r + ANSI ERASE 回退逐行重绘

抽象层设计必须隔离三类状态

终端抽象层需显式分离:设备状态(如当前光标位置、窗口尺寸)、呈现状态(待渲染的富文本节点树)、交互状态(焦点控件、按键队列)。以 gumchoose 命令为例,其核心结构体定义如下:

type TerminalRenderer struct {
    screen tcell.Screen // 底层设备句柄
    viewport *Viewport  // 当前可视区域坐标
    nodes    []RenderNode // 声明式UI节点(非ANSI字符串)
    inputQueue chan KeyEvent // 异步输入事件流
}

构建可测试的终端交互流水线

关键突破在于将ANSI序列生成与终端IO解耦。通过 io.Pipe() 注入假终端流,单元测试可断言具体控制序列:

func TestTableRenderer_Render(t *testing.T) {
    r, w := io.Pipe()
    renderer := NewTableRenderer(w)
    renderer.Render(data)
    w.Close()
    output, _ := io.ReadAll(r)
    assert.Contains(t, string(output), "\x1b[1mNAME\x1b[0m") // 验证表头加粗
}

流程图:终端抽象层生命周期

flowchart LR
    A[CLI启动] --> B[探测终端能力]
    B --> C{支持真彩色?}
    C -->|是| D[初始化24位色渲染器]
    C -->|否| E[加载256色映射表]
    D --> F[构建声明式UI树]
    E --> F
    F --> G[事件循环:读取输入→更新状态→生成ANSI→写入stdout]

生产环境中的渐进式迁移路径

某云平台CLI团队将旧版 fmt.Printf 混合ANSI代码重构为抽象层:第一阶段保留原输出函数但注入 TerminalWriter 接口;第二阶段将所有颜色/样式逻辑移入 Style 结构体;第三阶段实现 ScreenBuffer 双缓冲机制,解决高频刷新下的闪烁问题。迁移后,交互式资源筛选器响应延迟从320ms降至47ms。

抽象层必须承载语义而非仅语法

fmt.Sprintf("\x1b[32m%s\x1b[0m", name) 是语法表达,而 Text(name).Foreground(ColorGreen).Bold() 是语义表达。后者允许在无色终端中自动降级为 *name*,在无障碍终端中转译为 name, highlighted 的语音提示,甚至导出为HTML文档时生成 <strong class="green">name</strong>

终端抽象层的本质,是将字符终端这一古老接口,转化为具备现代UI框架特征的可编程表面。当 cobra 命令树与 bubbletea 渲染引擎通过统一 Terminal 接口桥接时,CLI开发者获得的不再是“打印工具”,而是真正的终端应用开发平台。

守护数据安全,深耕加密算法与零信任架构。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注