AI Compose Commit:用 AI 智能重构 Git 提交工作流

AI Compose Commit:用 AI 智能重构 Git 提交工作流

Introduction

在软件开发过程中,提交代码是程序员每天都要面对的日常工作。可是你有没有经历过这样的场景:一天工作结束后,打开 Git 看到几十个未暂存的修改文件,却不知道该如何将它们组织成合理的提交?

传统的方式是手动将文件分批暂存、逐个提交、撰写提交信息,这个过程既耗时又容易出错。咱们就常常在这上面浪费了不少时间,毕竟谁也不想在已经疲惫的晚上还要为这些琐事烦心。

我们在 HagiCode 项目中推出了一项新功能------AI Compose Commit,旨在彻底改变这个工作流程。它通过 AI 智能分析工作区中的所有未提交变更,自动将它们分组为多个逻辑提交,并执行符合规范的提交操作。本文将深入探讨这个功能的实现原理、技术架构以及我们在实践中遇到的挑战与解决方案。

About HagiCode

本文分享的方案来自我们在 HagiCode 项目中的实践经验。

Background

传统 Git 提交的痛点

Git 作为版本控制系统,为开发者提供了强大的代码管理能力。但在实际使用中,提交操作往往成为开发流程中的瓶颈:

  1. 手动分组耗时: 当有大量文件变更时,开发者需要逐个检查文件内容,判断哪些属于同一个功能,这需要耗费大量脑力
  2. 提交信息质量参差: 撰写符合 Conventional Commits 规范的提交信息需要经验和技巧,新手常常写出不规范的提交
  3. 多仓库管理复杂: 在 monorepo 环境中,需要在不同仓库间切换,增加了操作复杂度
  4. 工作流被打断: 提交代码会打断开发思路,影响编码效率

这些问题在大型项目和团队协作环境中尤为明显。一个优秀的开发工具应该让开发者专注于核心的编码工作,而不是被繁琐的提交流程所困扰。

AI 辅助开发的趋势

近年来,AI 技术在软件开发领域的应用日益广泛。从代码补全、错误检测到自动生成文档,AI 正在逐步渗透到开发的各个环节。在 Git 工作流方面,虽然已有一些工具提供提交信息生成的功能,但大多局限于单次提交的场景,缺乏对整个工作区变更的智能分析和分组能力。

其实 HagiCode 在开发过程中也遇到了这些痛点,我们曾尝试过多种工具,但都或多或少存在一些局限性。要么是功能不够完善,要么是用户体验不够好。这也是为什么我们最终决定自己实现 AI Compose Commit 功能的原因。

HagiCode 的 AI Compose Commit 功能正是为了填补这一空白而生,它不仅是生成提交信息,而是完整接管从文件分析到执行提交的整个流程。

Problem

技术挑战

在实现 AI Compose Commit 功能的过程中,我们面临了多个技术挑战:

  1. 文件语义理解: AI 需要理解文件变更的语义关系,判断哪些文件属于同一个功能模块。这需要深入分析文件内容、目录结构以及变更的上下文。

  2. 提交分组策略: 如何定义合理的分组标准?是按功能、按模块,还是按文件类型?不同的项目可能适用不同的策略。

  3. 实时反馈与异步处理: Git 操作可能需要较长时间,特别是处理大量文件时。如何在保证用户体验的同时完成复杂操作?

  4. 多仓库支持: 在 monorepo 架构下,需要在主仓库和子仓库之间正确路由操作。

  5. 错误处理与回滚: 如果某个提交失败,如何处理已执行的提交?是否需要回滚已暂存的文件?

  6. 提交信息一致性: 生成的提交信息需要符合项目现有的风格,保持历史提交的格式一致。

性能考量

AI 处理大量文件变更会消耗显著的时间和计算资源。我们需要在以下方面进行优化:

  • 减少不必要的 AI 调用
  • 优化文件上下文的构建方式
  • 实现高效的 Git 操作批处理

这些问题在 HagiCode 的实际使用中都真实出现过,我们通过不断的迭代和优化才找到了相对完美的解决方案。如果你也在开发类似的工具,希望我们的经验能给你一些启发。

Solution

整体架构设计

我们采用了分层架构来实现 AI Compose Commit 功能,确保系统具有良好的可扩展性和可维护性:

1. API 层(Web 层)

GitController 提供了 POST /api/git/auto-compose-commit 端点,作为功能入口。为了优化用户体验,我们采用了 Fire-and-Forget 异步模式:

  • 客户端发起请求后,服务器立即返回 HTTP 202 Accepted
  • 实际的 AI 处理在后台异步执行
  • 处理完成后通过 SignalR 通知客户端

这种设计确保了即使 AI 处理需要几分钟,用户也能立即得到响应,不会感觉系统卡顿。

2. 应用服务层(Application 层)

GitAppService 负责核心业务逻辑:

  • 仓库检测:支持 monorepo 中的多仓库管理
  • 锁管理:防止并发操作导致的冲突
  • 文件暂存协调:与 AI 处理流程的交互
  • 错误回滚:处理失败场景下的状态恢复

3. 分布式计算层(Orleans Grains)

AIGrain 作为 AI 操作的核心执行单元,实现了 IAIGrain 接口中的 AutoComposeCommitAsync 方法:

csharp 复制代码
// 定义 AI 自动组合提交的接口方法
// 参数说明:
// - projectId: 项目唯一标识符
// - unstagedFiles: 未暂存文件列表,包含文件路径和状态信息
// - projectPath: 项目根目录路径(可选),用于访问项目上下文
// 返回值: 包含执行结果的响应对象,包括成功/失败状态和详细信息
[Alias("AutoComposeCommitAsync")]
[ResponseTimeout("00:20:00")] // 20 分钟超时,适用于处理大型变更集
Task<AutoComposeCommitResponseDto> AutoComposeCommitAsync(
    string projectId,
    GitFileStatusDto[] unstagedFiles,
    string? projectPath = null);

这个方法设置了 20 分钟的超时时间,以处理大型变更集。HagiCode 在实际使用中发现,有些项目的单次变更可能涉及上百个文件,需要更长的处理时间。

4. AI 服务层

通过抽象的 IAIService 接口,我们实现了 AI 服务的可插拔架构。目前使用 Claude Helper 服务,但可以轻松切换到其他 AI 提供商。

核心实现逻辑

文件上下文构建

AI 需要了解每个文件的状态才能做出智能决策。我们通过 BuildFileChangesXml 方法构建文件上下文:

csharp 复制代码
/// <summary>
/// 构建文件变更的 XML 表示形式,用于为 AI 提供完整的文件上下文信息
/// </summary>
/// <param name="stagedFiles">已暂存的文件列表,包含文件路径、状态和旧路径(针对重命名操作)</param>
/// <returns>格式化的 XML 字符串,包含所有文件的元数据信息</returns>
private static string BuildFileChangesXml(GitFileStatusDto[] stagedFiles)
{
    var sb = new StringBuilder();
    sb.AppendLine("<files>");

    foreach (var file in stagedFiles)
    {
        sb.AppendLine("  <file>");
        // 使用 XML 转义确保特殊字符不会破坏 XML 结构
        sb.AppendLine($"    <path>{System.Security.SecurityElement.Escape(file.Path)}</path>");
        sb.AppendLine($"    <status>{System.Security.SecurityElement.Escape(file.Status)}</status>");

        // 处理文件重命名场景,记录旧路径以便 AI 理解变更关系
        if (!string.IsNullOrEmpty(file.OldPath))
        {
            sb.AppendLine($"    <oldPath>{System.Security.SecurityElement.Escape(file.OldPath)}</oldPath>");
        }

        sb.AppendLine("  </file>");
    }

    sb.AppendLine("</files>");
    return sb.ToString();
}

这个 XML 格式的上下文包含文件路径、状态和旧路径(针对重命名操作),为 AI 提供了完整的元数据。通过结构化的 XML 格式,我们确保了 AI 能够准确理解每个文件的状态和变更类型。

AI 权限管理

为了让 AI 能够直接执行 Git 操作,我们配置了全面的工具权限:

csharp 复制代码
// 定义 AI 可以使用的工具集合,包括文件操作和 Git 命令执行权限
// Read/Write/Edit: 文件读写和编辑能力
// Bash(git:*): 执行所有 Git 命令的权限
// 其他 Bash 命令: 用于查看文件内容和目录结构,辅助 AI 理解上下文
var allowedTools = new[]
{
    "Read", "Write", "Edit",
    "Bash(git:*)", "Bash(cat:*)", "Bash(ls:*)", "Bash(find:*)",
    "Bash(grep:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(wc:*)"
};

// 构建完整的 AI 请求对象
var request = new AIRequest
{
    Prompt = prompt,                          // 完整的 Prompt 模板,包含任务指令和约束条件
    WorkingDirectory = projectPath ?? GetTempDirectory(), // 工作目录,确保 AI 在正确的项目上下文中执行
    AllowedTools = allowedTools,               // 允许使用的工具集合
    PermissionMode = PermissionMode.bypassPermissions, // 绕过权限检查,允许直接执行 Git 操作
    LanguagePreference = languagePreference         // 语言偏好设置,确保生成符合用户期望的提交信息
};

这里使用了 PermissionMode.bypassPermissions 模式,允许 AI 直接执行 Git 命令而无需用户确认。这是功能设计的核心,但同时也需要严格的输入验证来防止滥用。HagiCode 在实际部署中,通过后端的参数验证和日志监控,确保了这个机制的安全性。

提交结果解析

AI 执行完成后,会返回结构化的结果。我们实现了双重解析策略以确保兼容性:

csharp 复制代码
/// <summary>
/// 解析 AI 返回的提交执行结果,支持分隔符格式和正则表达式格式
/// </summary>
/// <param name="aiResponse">AI 返回的原始响应内容</param>
/// <returns>解析后的提交结果列表,每个结果包含提交哈希和执行状态</returns>
private List<CommitResultDto> ParseCommitExecutionResults(string aiResponse)
{
    var results = new List<CommitResultDto>();

    // 优先使用分隔符解析(新格式),这种格式更加明确和可靠
    if (aiResponse.Contains("---"))
    {
        logger.LogDebug("Using delimiter-based parsing for AI response");
        results = ParseDelimitedFormat(aiResponse);

        if (results.Count > 0)
        {
            return results; // 成功解析,直接返回结果
        }

        logger.LogWarning("Delimiter-based parsing produced no results, falling back to regex");
    }
    else
    {
        logger.LogDebug("No delimiter found, using legacy regex-based parsing");
    }

    // 回退到正则表达式解析(旧格式),确保向后兼容性
    return ParseLegacyFormat(aiResponse);
}

分隔符格式使用 --- 作为提交之间的分隔,格式清晰且易于解析:

复制代码
---
Commit 1: abc123def456
feat(auth): add user login functionality

Implement JWT-based authentication with login form and API endpoints.

Co-Authored-By: Hagicode <noreply@hagicode.com>
---
Commit 2: 789ghi012jkl
docs(readme): update installation instructions

Add new setup steps for Docker environment.

Co-Authored-By: Hagicode <noreply@hagicode.com>
---

这种格式设计让解析变得简单可靠,同时人类阅读也很清晰。

锁管理机制

为了防止并发操作导致的状态冲突,我们实现了仓库锁机制:

csharp 复制代码
// 获取仓库锁,防止并发操作
// 参数说明:
// - fullPath: 仓库的完整路径,用于标识不同的仓库实例
// - requestedBy: 请求者标识,用于追踪和日志记录
await _autoComposeLockService.AcquireLockAsync(fullPath, requestedBy);

try
{
    // 执行 AI Compose Commit 操作
    // 这部分代码会调用 Orleans Grain 的方法,执行实际的 AI 处理和 Git 操作
    await aiGrain.AutoComposeCommitAsync(projectId, unstagedFiles, projectPath);
}
finally
{
    // 确保锁被释放,无论操作成功或失败
    // 使用 finally 块可以保证异常情况下也能释放锁,避免死锁
    await _autoComposeLockService.ReleaseLockAsync(fullPath);
}

锁具有 20 分钟的超时时间,与 AI 操作的超时设置保持一致。如果操作失败或超时,系统会自动释放锁,避免永久阻塞。HagiCode 在实际使用中发现,这个锁机制非常重要,特别是在团队协作环境中,多个开发者可能同时触发 AI Compose Commit 操作。

SignalR 实时通知

处理完成后,系统通过 SignalR 向前端发送通知:

csharp 复制代码
/// <summary>
/// 发送自动组合提交完成的通知
/// </summary>
/// <param name="projectId">项目标识符,用于路由通知到正确的客户端</param>
/// <param name="totalCount">总提交数量,包括成功和失败</param>
/// <param name="successCount">成功提交的数量</param>
/// <param name="failureCount">失败提交的数量</param>
/// <param name="success">整体操作是否成功标志</param>
/// <param name="error">错误信息(如果操作失败)</param>
private async Task SendAutoComposeCommitNotificationAsync(
    string projectId,
    int totalCount,
    int successCount,
    int failureCount,
    bool success,
    string? error)
{
    try
    {
        // 构建通知数据传输对象,包含详细的执行结果
        var notification = new AutoComposeCommitCompletedDto
        {
            ProjectId = projectId,
            TotalCount = totalCount,
            SuccessCount = successCount,
            FailureCount = failureCount,
            Success = success,
            Error = error
        };

        // 通过 SignalR Hub 广播通知到所有连接的客户端
        await messageService.SendAutoComposeCommitCompletedAsync(notification);

        logger.LogInformation(
            "Auto compose commit notification sent for project {ProjectId}: {SuccessCount}/{TotalCount} succeeded",
            projectId, successCount, totalCount);
    }
    catch (Exception ex)
    {
        // 记录通知错误但不影响主操作流程
        // 通知失败不应该导致整个操作失败
        logger.LogError(ex, "Failed to send auto compose commit notification for project {ProjectId}", projectId);
    }
}

前端收到通知后可以更新 UI,显示提交成功或失败的状态,提升用户体验。这种实时反馈机制在 HagiCode 的使用中获得了很好的用户反馈,用户可以清楚地知道操作何时完成以及结果如何。

Implementation

Prompt 工程设计

AI 的行为完全由 Prompt 决定,我们精心设计了 Auto Compose Commit 的 Prompt 模板。以中文版本为例(auto-compose-commit.zh-CN.hbs):

非交互式模式支持

Prompt 开头明确声明支持非交互式运行模式,这是 CI/CD 和自动化脚本的关键需求:

handlebars 复制代码
**重要提示**:此提示词可能在非交互式环境中运行(如 CI/CD、自动化脚本)。

**非交互式模式**:
- 禁止使用 AskUserQuestion 或任何交互式工具
- 当需要用户输入时:
  - 使用合理的默认值(如提交类型使用 feat)
  - 跳过可选的确认步骤
  - 记录所做的假设

这个设计确保了 AI Compose Commit 功能不仅能在交互式 IDE 环境中使用,也能集成到 CI/CD 流程中,实现完全自动化的提交流程。

分支保护机制

为了防止 AI 执行危险操作,我们在 Prompt 中添加了严格的分支保护规则:

handlebars 复制代码
**分支保护**:
- 禁止执行任何分支切换操作(git checkout、git switch)
- 所有 git commit 命令必须在当前分支上执行
- 不得创建、删除或重命名分支
- 不得修改未跟踪文件或未暂存变更
- 如果需要分支切换才能完成操作,应返回错误而非执行

这些规则通过约束 AI 的工具使用范围,确保操作的安全性。HagiCode 在实际测试中验证了这些约束的有效性,AI 在遇到需要分支切换的场景时会安全地返回错误,而不是执行危险操作。

智能分组决策树

Prompt 中详细定义了文件分组的决策逻辑:

handlebars 复制代码
**文件分组决策树**:
├── 是否为配置文件(package.json、tsconfig.json、.env 等)?
│   ├── 是 → 独立提交(类型:chore 或 build)
│   └── 否 → 继续
├── 是否为文档文件(README.md、*.md、docs/**)?
│   ├── 是 → 独立提交(类型:docs)
│   └── 否 → 继续
├── 是否与同一功能相关?
│   ├── 是 → 合并到同一提交
│   └── 否 → 分别提交
└── 是否为跨模块变更?
    ├── 是 → 按模块分组
    └── 否 → 按功能分组

这个决策树为 AI 提供了清晰的分组逻辑,确保生成的提交符合语义合理性。HagiCode 在实际使用中发现,这个决策树能够处理绝大多数常见场景,生成的分组结果符合开发者预期。

历史格式一致性分析

为了让提交信息与项目历史保持一致,Prompt 要求 AI 在生成前分析最近的提交历史:

handlebars 复制代码
**历史格式一致性**:在生成提交信息之前,你**必须**分析当前仓库的提交历史以匹配现有风格

1. 使用 git log -n 15 --pretty=format:"%H|%s|%b%n---%n" 获取最近的提交历史
2. 分析提交以识别:
   - 结构模式:项目是否使用多段落?是否有 "Changes:" 或 "Capabilities:" 部分?
   - 语言模式:提交信息是英文、中文还是混合?
   - 常用类型:最常使用哪些提交类型(feat、fix、docs 等)?
   - 特殊格式:是否有 Co-Authored-By 行?其他项目特定的约定?
3. 生成遵循检测到的模式的提交信息

这个分析确保了 AI 生成的提交信息不会显得突兀,而是与项目的提交历史保持风格一致。在 HagiCode 的多语言项目中,这个功能特别重要,它能够根据项目的提交历史自动选择合适的语言和格式。

Co-Authored-By 要求

每个提交必须包含 Co-Authored-By 信息:

handlebars 复制代码
**重要**:每个提交必须添加 Co-Authored-By 信息
- 使用以下格式:git commit -m "type(scope): subject" -m "" -m "Co-Authored-By: Hagicode <noreply@hagicode.com>"
- 或者直接在提交信息中包含 Co-Authored-By 行

这不仅是为了贡献规范,也是为了追踪 AI 辅助的提交历史。HagiCode 将这个要求作为强制规则,确保所有 AI 生成的提交都带有明确的来源标识。

工作流程详解

完整的 AI Compose Commit 工作流程如下:

  1. 用户触发: 用户在 Git Status 面板或 Quick Actions Zone 点击"AI Auto Compose Commit"按钮
  2. API 请求 : 前端发送 POST 请求到 /api/git/auto-compose-commit 端点
  3. 立即响应: 服务器返回 HTTP 202 Accepted,不等待处理完成
  4. 后台处理 :
    • GitAppService 获取仓库锁
    • 调用 AIGrain 的 AutoComposeCommitAsync 方法
    • 构建文件上下文 XML
    • 执行 AI Prompt,让 AI 分析并执行提交
  5. AI 执行 :
    • 使用 Git 命令获取所有未暂存变更
    • 读取文件内容理解变更性质
    • 按语义关系对文件分组
    • 对每组执行 git addgit commit 操作
  6. 结果解析: 解析 AI 返回的执行结果
  7. 通知发送: 通过 SignalR 通知前端
  8. 锁释放: 无论成功或失败,都释放仓库锁

这个流程的设计确保了用户可以在发起操作后立即继续其他工作,而不需要等待 AI 处理完成。HagiCode 的用户反馈表明,这种异步处理方式大大提升了工作流体验。

错误处理机制

我们实现了多层级的错误处理:

1. 输入验证

csharp 复制代码
// 验证请求参数的有效性,防止无效请求到达后端处理逻辑
if (request.UnstagedFiles == null || request.UnstagedFiles.Count == 0)
{
    return BadRequest(new
    {
        message = "No unstaged files provided. Please make changes in the working directory first.",
        status = "validation_failed"
    });
}

2. 错误回滚

如果 AI 处理过程中出现错误,系统会执行回滚操作,将已暂存的文件取消暂存,避免留下不一致的状态。这个机制在 HagiCode 的实际使用中挽救了多次意外中断,确保了仓库状态的完整性。

3. 超时处理

20 分钟的超时设置确保了长时间运行的操作不会无限期阻塞资源。超时后,系统会释放锁并通知用户操作失败。HagiCode 在实际使用中发现,大部分操作能够在 2-5 分钟内完成,只有处理超大型变更集时才会接近超时限制。

Best Practices

使用 AI Compose Commit 的最佳实践

1. 合理使用时机

AI Compose Commit 最适合以下场景:

  • 一天工作结束后,批量处理多个文件的变更
  • 重构操作后,多个相关文件需要分别提交
  • 功能开发完成,需要将相关变更分组提交

不适合以下场景:

  • 单个文件的快速提交(直接使用普通提交更快)
  • 需要精确控制提交内容的场景
  • 包含敏感信息的提交(需要人工审核)

2. 审查 AI 生成的提交

虽然 AI 智能分组很强大,但开发者仍应审查生成的提交:

  • 检查提交的分组是否符合预期
  • 验证提交信息的准确性
  • 确认没有遗漏或错误包含文件

如果发现不合理的分组,可以使用 git reset --soft HEAD~N 撤销后重新分组。HagiCode 的经验表明,即使 AI 分组很智能,人工审查仍然是有价值的,特别是在重要的功能提交时。

3. 配合项目规范

确保项目的 Git 配置支持 Conventional Commits:

bash 复制代码
# 安装 commitlint
npm install -g @commitlint/cli @commitlint/config-conventional

# 配置 commitlint
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

这样可以在 CI/CD 流程中验证提交信息格式,与 AI Compose Commit 生成的格式保持一致。

实现类似功能的建议

如果你想在项目中实现类似的 AI 辅助提交功能,以下是我们的建议:

1. 从小规模开始

先实现单次提交信息生成,再逐步扩展到多提交分组功能。这样更容易验证和迭代。HagiCode 也是按照这个路径逐步完善功能的,早期版本只支持单次提交,后来才扩展到多提交智能分组。

2. 使用成熟的 AI SDK

不要自己实现 AI 调用逻辑,使用现有的 SDK 可以减少开发时间和潜在 bug。我们使用了 Claude Helper 服务,它提供了稳定的接口和完善的错误处理。

3. 重视 Prompt 设计

Prompt 的质量直接决定了 AI 输出的质量。投入时间设计详细的 Prompt,包括:

  • 明确的任务描述
  • 具体的输出格式要求
  • 边界情况的处理规则
  • 示例说明

HagiCode 在 Prompt 设计上投入了大量时间,这是功能成功的关键因素之一。

4. 实现全面的错误处理

AI 操作可能因为各种原因失败(网络问题、API 限流、内容审查等)。确保你的系统能够优雅地处理这些错误,并提供有意义的错误信息。

5. 提供手动干预机制

不要完全自动化,给用户保留控制权。提供查看分组结果、调整分组、手动编辑提交信息等选项,平衡自动化与灵活性。HagiCode 虽然实现了自动执行,但仍然保留了预览和调整的能力。

性能优化技巧

1. 文件过滤

在构建文件上下文时,过滤掉不需要 AI 分析的文件:

csharp 复制代码
// 过滤掉自动生成的文件和过大的文件,减少 AI 处理负担
var relevantFiles = stagedFiles
    .Where(f => !IsGeneratedFile(f.Path))
    .Where(f => !IsLargeFile(f.Path))
    .ToArray();

2. 并行处理

如果支持多个独立仓库,可以并行处理不同仓库的提交,提高整体效率。

3. 缓存优化

缓存项目提交历史分析结果,避免每次都重新分析。可以在配置文件中存储历史格式偏好,减少 AI 调用次数。

Conclusion

AI Compose Commit 功能代表了 AI 技术在软件开发工具中的深度应用。通过智能分析文件变更、自动分组提交、生成规范的提交信息,它显著提升了 Git 工作流的效率,让开发者能够更专注于核心的编码工作。

在实现过程中,我们学到了几个重要的经验:

  1. 用户反馈是关键: 早期版本采用同步等待方式,用户反馈体验不佳,改为 Fire-and-Forget 模式后满意度大幅提升
  2. Prompt 设计决定质量: 一个精心设计的 Prompt 比复杂的算法更能保证 AI 输出的质量
  3. 安全永远是第一位的: 虽然赋予 AI 直接执行 Git 命令的权限带来了效率提升,但必须配合严格的约束和验证
  4. 渐进式改进: 从简单场景开始,逐步增加复杂度,比一次性实现所有功能更容易成功

未来,我们计划进一步优化 AI Compose Commit 功能,包括:

  • 支持更多提交分组策略(按时间、按开发者等)
  • 集成代码审查流程,在提交前自动触发审查
  • 支持自定义提交信息模板,满足不同项目的个性化需求

如果你觉得本文分享的方案有价值,不妨也试试 HagiCode,体验一下这个功能在实际开发中的效果。毕竟实践是检验真理的唯一标准嘛。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。