datai/datai-scenes/datai-scene-salesforce/docs/retros/20260118-metadata-api-client-retro.md
Kris 2e6f087732 docs: 完成REQ-010-17和REQ-010-2的文档创建
- 完成REQ-010-17(性能优化和限流处理)的所有6个阶段
  - 创建ADR文档:0026-performance-optimization.md
  - 创建Prompt文档:027-performance-optimization.md
  - 创建会话记录:20260119-performance-optimization.md
  - 创建变更记录:20260119-performance-optimization.md
  - 创建复盘报告:20260119-performance-optimization-retro.md
  - 更新index.md和CHANGELOG.md

- 完成REQ-010-2(基础实体类和Mapper创建)的前3个阶段
  - 更新ADR文档:0011-entity-mapper-create.md
  - 创建Prompt文档:002-entity-mapper-create.md
  - 更新index.md

所有文档均按照SSOT方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录和闭环复盘。
2026-01-19 10:06:09 +08:00

25 KiB
Raw Blame History

闭环复盘 - Metadata API客户端封装

复盘信息

  • 复盘编号: Retro-20260118-002
  • 复盘日期: 2026-01-18
  • 相关需求: REQ-010-5 - Metadata API客户端封装
  • 相关决策: ADR-0014 - Metadata API客户端封装架构决策
  • 相关提示词: Prompt-015 - Metadata API客户端封装实现提示词
  • 相关会话: Session-20260118-002 - Metadata API客户端封装执行会话
  • 相关变更: Change-015 - Metadata API客户端封装变更记录

目标与实际产出对比

目标

  1. 封装 Salesforce Metadata API 客户端,提供 retrieve() 和 deploy() 方法的调用接口
  2. 支持异步执行
  3. 支持状态轮询
  4. 支持 Job ID 获取
  5. 支持 Zip 文件下载和解压处理
  6. 支持部署结果解析
  7. 实现错误处理机制

实际产出

  1. 完成了 MetadataApiClient 客户端类的创建,包括:

    • getMetadataConnection: 获取 Metadata 连接
    • retrieve: 检索元数据
    • deploy: 部署元数据
    • checkRetrieveStatus: 检查检索状态
    • checkDeployStatus: 检查部署状态
  2. 完成了 MetadataApiService 服务层的创建,包括:

    • retrieveAsync: 异步检索元数据
    • deployAsync: 异步部署元数据
    • cancelJob: 取消任务
    • getJobStatus: 获取任务状态
  3. 完成了 AsyncConfig 配置类的创建,包括:

    • 配置线程池(核心线程数 5最大线程数 10队列容量 100
    • 配置线程名称前缀
    • 配置拒绝策略
  4. 完成了状态轮询机制的创建,包括:

    • 定时轮询任务状态
    • 支持超时处理30 分钟)
    • 支持取消轮询
    • 更新任务状态
  5. 完成了 Zip 文件处理工具类的创建,包括:

    • unzip: 解压 Zip 文件
    • readZipFile: 读取 Zip 文件内容
    • 使用流式处理避免内存溢出
  6. 完成了 MetadataApiUtil 工具类的创建,包括:

    • parsePackageXml: 解析 package.xml
    • parseDeployResult: 解析部署结果
    • extractErrorMessages: 提取错误信息
    • extractCodeCoverage: 提取代码覆盖率
  7. 完成了自定义异常类的创建,包括:

    • MetadataApiException: 统一的异常处理机制
  8. 完成了结果类的创建,包括:

    • RetrieveResult: 检索结果类
    • DeployResult: 部署结果类
    • JobStatus: 任务状态类
  9. 完成了 MetadataApiController 控制器的创建,包括:

    • POST /metadata/retrieve - 检索元数据
    • POST /metadata/deploy - 部署元数据
    • GET /metadata/job/{jobId}/status - 获取任务状态
    • DELETE /metadata/job/{jobId} - 取消任务
  10. 完成了 MetadataManager 前端组件的创建,包括:

    • 元数据检索界面
    • 元数据部署界面
    • 任务状态查询界面
    • 任务取消界面

对比分析

目标项 完成度 说明
MetadataApiClient 客户端类 100% 完全实现,使用 Salesforce WSC 库
MetadataApiService 服务层 100% 完全实现,支持异步执行
AsyncConfig 配置类 100% 完全实现,配置线程池
状态轮询机制 100% 完全实现,支持超时处理和取消轮询
Zip 文件处理工具类 100% 完全实现,使用流式处理
MetadataApiUtil 工具类 100% 完全实现,提供多种工具方法
自定义异常类 100% 完全实现,统一的异常处理机制
结果类 100% 完全实现,封装任务执行结果
MetadataApiController 控制器 100% 完全实现,提供 4 个 RESTful API 接口
MetadataManager 前端组件 100% 完全实现,基于 Vue 3 和 Element Plus

总体完成度: 100%

结论: 所有目标均已实现,功能完整,符合需求。

有效的 Prompt 技巧3条

技巧1明确角色设定和技术栈

描述: 在提示词开头明确设定角色和技术栈,让 AI 清楚自己的定位和要使用的技术。

示例:

### 角色设定

你是一个经验丰富的 Spring Boot 全栈开发工程师,专注于 Salesforce 元数据管理系统的开发。你熟悉以下技术栈:

- **后端**: Spring Boot 3, MyBatis Plus, MySQL
- **前端**: Vue 3, Element Plus
- **Salesforce API**: Metadata API, Partner API
- **Salesforce WSC**: Web Service Connector 库
- **工具**: Maven, Git, Postman

### 技术栈限制

- **后端**: Spring Boot 3, MyBatis Plus, MySQL
- **Salesforce API**: Metadata API, Partner API
- **Salesforce WSC**: 必须使用 Salesforce WSC (Web Service Connector) 库
- **异步执行**: 必须使用 Spring 的 @Async 注解和线程池
- **状态轮询**: 必须使用定时任务实现状态轮询

效果: AI 能够快速理解角色定位和技术栈,生成符合要求的代码。

技巧2提供详细的代码示例和实现方案

描述: 在提示词中提供详细的代码示例和实现方案,包括类结构、方法签名、参数类型等。

示例:

#### 2.1 MetadataApiClient 类

```java
package com.datai.metadata.client;

import com.datai.auth.service.ISessionManager;
import com.sforce.soap.metadata.*;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Metadata API 客户端类
 * 封装 Salesforce Metadata API 调用
 */
@Slf4j
@Component
public class MetadataApiClient {

    @Autowired
    private ISessionManager sessionManager;

    /**
     * 获取 Metadata 连接
     * 
     * @param orgConfigId 组织配置ID
     * @return Metadata 连接
     * @throws ConnectionException 连接异常
     */
    private MetadataConnection getMetadataConnection(Long orgConfigId) throws ConnectionException {
        // 使用 SessionManager 获取会话信息
        SessionInfo sessionInfo = sessionManager.getSession(orgConfigId);
        
        // 创建连接配置
        ConnectorConfig config = new ConnectorConfig();
        config.setSessionId(sessionInfo.getSessionId());
        config.setServiceEndpoint(sessionInfo.getMetadataEndpoint());
        
        // 创建 Metadata 连接
        return new MetadataConnection(config);
    }

    /**
     * 检索元数据
     * 
     * @param orgConfigId 组织配置ID
     * @param packageXml package.xml 内容
     * @param apiVersion API 版本
     * @return 检索结果
     * @throws MetadataApiException Metadata API 异常
     */
    public RetrieveResult retrieve(Long orgConfigId, String packageXml, String apiVersion) 
            throws MetadataApiException {
        try {
            // 获取 Metadata 连接
            MetadataConnection connection = getMetadataConnection(orgConfigId);
            
            // 创建 Retrieve 请求
            com.sforce.soap.metadata.RetrieveRequest request = new com.sforce.soap.metadata.RetrieveRequest();
            request.setApiVersion(Double.parseDouble(apiVersion));
            request.setUnpackaged(readPackageXml(packageXml));
            
            // 执行检索
            AsyncResult asyncResult = connection.retrieve(request);
            
            // 返回检索结果
            return new RetrieveResult(asyncResult.getId(), asyncResult.getState().name());
        } catch (ConnectionException e) {
            log.error("检索元数据失败", e);
            throw new MetadataApiException("检索元数据失败: " + e.getMessage(), e);
        }
    }
}

**效果**: AI 能够根据提供的代码示例生成符合项目规范的代码,减少修改和调整的工作量。

### 技巧3明确输出格式和验收标准

**描述**: 在提示词中明确输出格式和验收标准包括代码结构、单元测试、集成测试、API 文档等。

**示例**:

输出格式

1. 代码结构

datai-salesforce-metadata/
├── src/main/java/com/datai/metadata/
│   ├── client/
│   │   ├── MetadataApiClient.java - Metadata API 客户端类
│   │   ├── MetadataApiException.java - Metadata API 异常类
│   │   ├── RetrieveResult.java - 检索结果类
│   │   ├── DeployResult.java - 部署结果类
│   │   └── JobStatus.java - 任务状态类
│   ├── config/
│   │   └── AsyncConfig.java - 异步配置类
│   ├── service/
│   │   ├── IMetadataApiService.java - Metadata API 服务接口
│   │   └── impl/
│   │       └── MetadataApiServiceImpl.java - Metadata API 服务实现
│   ├── task/
│   │   └── MetadataPollingTask.java - Metadata 轮询任务
│   └── util/
│       ├── ZipUtil.java - Zip 文件处理工具类
│       └── MetadataApiUtil.java - Metadata API 工具类

3. 单元测试

必须为以下类编写单元测试:

  • MetadataApiClient
  • MetadataApiServiceImpl
  • ZipUtil
  • MetadataApiUtil

4. 集成测试

必须编写以下集成测试:

  • Metadata API 客户端调用测试
  • 异步执行测试
  • 状态轮询测试
  • Zip 文件处理测试
  • 错误处理测试

5. API 文档

为以下接口编写 API 文档:

  • POST /metadata/retrieve - 检索元数据
  • POST /metadata/deploy - 部署元数据
  • GET /metadata/job/{jobId}/status - 获取任务状态
  • DELETE /metadata/job/{jobId} - 取消任务

**效果**: AI 能够按照要求的格式输出代码,并确保代码质量和完整性。

## 避免的坑3个

### 坑1异步执行方案选择不当

**问题描述**: 在架构决策阶段对于异步执行方案最初考虑了多种方案Spring @Async、消息队列、CompletableFuture 等),如果选择不当可能导致实现复杂或功能受限。

**避免方法**: 在 ADR-0014 中,我们详细分析了三种方案的优缺点,最终选择了使用 Spring 的 @Async 注解和线程池的方案,理由是:
1. Spring 的 @Async 注解简单易用,无需额外配置
2. Spring 提供了线程池管理,可以控制并发数和资源使用
3. 异步执行不影响系统响应,提高用户体验
4. 支持异步任务的状态跟踪和结果获取
5. 对于简单的异步任务,使用消息队列是过度设计

**经验教训**: 在做技术选型时,要充分考虑业务需求、技术约束、实现复杂度等因素,选择最合适的方案。

### 坑2状态轮询方案选择不当

**问题描述**: 对于状态轮询方案最初考虑了多种方案定时任务、回调机制、WebSocket 等),如果选择不当可能导致实现复杂或功能受限。

**避免方法**: 我们选择了使用定时任务实现状态轮询,理由是:
1. Salesforce Metadata API 不支持回调机制
2. 定时任务简单易用,无需额外引入其他库
3. 可以控制轮询频率,避免 API 限流
4. 支持超时处理,防止无限轮询
5. 支持取消轮询,提高灵活性

**经验教训**: 在选择状态轮询方案时,要考虑 API 的限制、实现复杂度、资源消耗等因素,选择最合适的方案。

### 坑3Zip 文件处理方案选择不当

**问题描述**: 对于 Zip 文件处理方案最初考虑了多种方案ZipInputStream、Apache Commons Compress、其他 Zip 库等),如果选择不当可能导致内存溢出或性能问题。

**避免方法**: 我们选择了使用 Java 的 ZipInputStream 处理 Zip 文件,理由是:
1. Java 的 ZipInputStream 是标准库,无需额外引入其他库
2. 流式处理可以避免内存溢出,支持大文件处理
3. 可以逐条读取 Zip 文件中的条目,提高处理效率
4. 支持解压到指定目录,便于后续处理

**经验教训**: 在选择 Zip 文件处理方案时,要考虑内存使用、性能、依赖复杂度等因素,选择最合适的方案。

## 模板迭代

### 模板适用性评估

本次使用的 Prompt-015 模板Metadata API客户端封装实现提示词整体适用性良好能够指导 AI 完成开发任务。但仍有以下改进空间:

### 改进建议1增加异步执行示例

**问题描述**: 当前模板没有提供详细的异步执行示例,可能导致 AI 生成的代码异步执行不够完善。

**改进建议**: 在模板中增加异步执行示例,包括:
- @Async 注解的使用
- CompletableFuture 的使用
- 异步任务的状态跟踪
- 异步任务的取消和超时处理

**示例**:

2.3 MetadataApiServiceImpl 实现类

package com.datai.metadata.service.impl;

import com.datai.metadata.client.*;
import com.datai.metadata.service.IMetadataApiService;
import com.sforce.soap.metadata.DeployOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Metadata API 服务实现类
 */
@Slf4j
@Service
public class MetadataApiServiceImpl implements IMetadataApiService {

    @Autowired
    private MetadataApiClient metadataApiClient;

    private final ConcurrentHashMap<String, CompletableFuture<?>> jobMap = new ConcurrentHashMap<>();

    @Override
    @Async("metadataTaskExecutor")
    public CompletableFuture<RetrieveResult> retrieveAsync(Long orgConfigId, String packageXml, String apiVersion) {
        String jobId = generateJobId();
        CompletableFuture<RetrieveResult> future = new CompletableFuture<>();
        jobMap.put(jobId, future);

        try {
            // 执行检索
            RetrieveResult result = metadataApiClient.retrieve(orgConfigId, packageXml, apiVersion);
            
            // 轮询状态
            pollRetrieveStatus(orgConfigId, result.getJobId(), future);
            
            return future;
        } catch (Exception e) {
            log.error("异步检索元数据失败", e);
            future.completeExceptionally(e);
            jobMap.remove(jobId);
            return future;
        }
    }

    @Override
    @Async("metadataTaskExecutor")
    public CompletableFuture<DeployResult> deployAsync(Long orgConfigId, byte[] zipFile, DeployOptions options) {
        String jobId = generateJobId();
        CompletableFuture<DeployResult> future = new CompletableFuture<>();
        jobMap.put(jobId, future);

        try {
            // 执行部署
            DeployResult result = metadataApiClient.deploy(orgConfigId, zipFile, options);
            
            // 轮询状态
            pollDeployStatus(orgConfigId, result.getJobId(), future);
            
            return future;
        } catch (Exception e) {
            log.error("异步部署元数据失败", e);
            future.completeExceptionally(e);
            jobMap.remove(jobId);
            return future;
        }
    }

    @Override
    public void cancelJob(String jobId) {
        CompletableFuture<?> future = jobMap.get(jobId);
        if (future != null && !future.isDone()) {
            future.cancel(true);
            jobMap.remove(jobId);
            log.info("任务已取消: {}", jobId);
        }
    }

    @Override
    public JobStatus getJobStatus(String jobId) {
        CompletableFuture<?> future = jobMap.get(jobId);
        if (future == null) {
            return JobStatus.NOT_FOUND;
        }

        if (future.isDone()) {
            if (future.isCompletedExceptionally()) {
                return JobStatus.FAILED;
            } else {
                return JobStatus.COMPLETED;
            }
        } else {
            return JobStatus.IN_PROGRESS;
        }
    }

    /**
     * 轮询检索状态
     * 
     * @param orgConfigId 组织配置ID
     * @param jobId 任务ID
     * @param future CompletableFuture
     */
    private void pollRetrieveStatus(Long orgConfigId, String jobId, CompletableFuture<RetrieveResult> future) {
        // 使用定时任务轮询状态
        // 实现略
    }

    /**
     * 轮询部署状态
     * 
     * @param orgConfigId 组织配置ID
     * @param jobId 任务ID
     * @param future CompletableFuture
     */
    private void pollDeployStatus(Long orgConfigId, String jobId, CompletableFuture<DeployResult> future) {
        // 使用定时任务轮询状态
        // 实现略
    }

    /**
     * 生成任务ID
     * 
     * @return 任务ID
     */
    private String generateJobId() {
        return System.currentTimeMillis() + "-" + (int)(Math.random() * 1000);
    }
}

### 改进建议2增加状态轮询示例

**问题描述**: 当前模板没有提供详细的状态轮询示例,可能导致 AI 生成的代码状态轮询不够完善。

**改进建议**: 在模板中增加状态轮询示例,包括:
- @Scheduled 注解的使用
- 轮询频率的配置
- 超时处理的实现
- 取消轮询的实现

**示例**:

2.4 MetadataPollingTask 轮询任务

package com.datai.metadata.task;

import com.datai.metadata.client.*;
import com.datai.metadata.service.IMetadataApiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Metadata 轮询任务
 */
@Slf4j
@Component
public class MetadataPollingTask {

    @Autowired
    private IMetadataApiService metadataApiService;

    private final ConcurrentHashMap<String, PollingJob> pollingJobs = new ConcurrentHashMap<>();

    /**
     * 轮询检索状态
     * 
     * @param orgConfigId 组织配置ID
     * @param jobId 任务ID
     * @param future CompletableFuture
     */
    public void pollRetrieveStatus(Long orgConfigId, String jobId, CompletableFuture<RetrieveResult> future) {
        PollingJob pollingJob = new PollingJob(orgConfigId, jobId, future, System.currentTimeMillis());
        pollingJobs.put(jobId, pollingJob);
    }

    /**
     * 轮询部署状态
     * 
     * @param orgConfigId 组织配置ID
     * @param jobId 任务ID
     * @param future CompletableFuture
     */
    public void pollDeployStatus(Long orgConfigId, String jobId, CompletableFuture<DeployResult> future) {
        PollingJob pollingJob = new PollingJob(orgConfigId, jobId, future, System.currentTimeMillis());
        pollingJobs.put(jobId, pollingJob);
    }

    /**
     * 定时轮询任务状态
     * 每 5 秒轮询一次
     */
    @Scheduled(fixedRate = 5000)
    public void pollJobStatus() {
        for (Map.Entry<String, PollingJob> entry : pollingJobs.entrySet()) {
            String jobId = entry.getKey();
            PollingJob pollingJob = entry.getValue();

            // 检查是否超时
            long elapsedTime = System.currentTimeMillis() - pollingJob.getStartTime();
            if (elapsedTime > TimeUnit.MINUTES.toMillis(30)) {
                log.warn("任务超时: {}", jobId);
                pollingJob.getFuture().completeExceptionally(new TimeoutException("任务超时"));
                pollingJobs.remove(jobId);
                continue;
            }

            // 检查是否已取消
            if (pollingJob.getFuture().isCancelled()) {
                log.info("任务已取消: {}", jobId);
                pollingJobs.remove(jobId);
                continue;
            }

            // 轮询状态
            try {
                if (pollingJob.getFuture() instanceof CompletableFuture<?>) {
                    // 根据任务类型轮询状态
                    // 实现略
                }
            } catch (Exception e) {
                log.error("轮询任务状态失败: {}", jobId, e);
                pollingJob.getFuture().completeExceptionally(e);
                pollingJobs.remove(jobId);
            }
        }
    }

    /**
     * 轮询任务
     */
    private static class PollingJob {
        private final Long orgConfigId;
        private final String jobId;
        private final CompletableFuture<?> future;
        private final long startTime;

        public PollingJob(Long orgConfigId, String jobId, CompletableFuture<?> future, long startTime) {
            this.orgConfigId = orgConfigId;
            this.jobId = jobId;
            this.future = future;
            this.startTime = startTime;
        }

        public Long getOrgConfigId() {
            return orgConfigId;
        }

        public String getJobId() {
            return jobId;
        }

        public CompletableFuture<?> getFuture() {
            return future;
        }

        public long getStartTime() {
            return startTime;
        }
    }
}

### 改进建议3增加错误处理示例

**问题描述**: 当前模板没有提供详细的错误处理示例,可能导致 AI 生成的代码错误处理不够完善。

**改进建议**: 在模板中增加错误处理示例,包括:
- 自定义异常类的使用
- 统一的异常处理机制
- 错误信息的返回
- 错误重试机制

**示例**:

2.5 MetadataApiException 异常类

package com.datai.metadata.client;

import lombok.Getter;

/**
 * Metadata API 异常类
 */
@Getter
public class MetadataApiException extends RuntimeException {

    private final String errorCode;
    private final String errorMessage;

    public MetadataApiException(String errorMessage) {
        super(errorMessage);
        this.errorCode = "METADATA_API_ERROR";
        this.errorMessage = errorMessage;
    }

    public MetadataApiException(String errorMessage, Throwable cause) {
        super(errorMessage, cause);
        this.errorCode = "METADATA_API_ERROR";
        this.errorMessage = errorMessage;
    }

    public MetadataApiException(String errorCode, String errorMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    public MetadataApiException(String errorCode, String errorMessage, Throwable cause) {
        super(errorMessage, cause);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
}

2.6 统一异常处理

package com.datai.metadata.exception;

import com.datai.common.core.domain.AjaxResult;
import com.datai.metadata.client.MetadataApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 统一异常处理
 */
@Slf4j
@RestControllerAdvice
public class MetadataExceptionHandler {

    /**
     * 处理 Metadata API 异常
     * 
     * @param e Metadata API 异常
     * @return 错误结果
     */
    @ExceptionHandler(MetadataApiException.class)
    public AjaxResult handleMetadataApiException(MetadataApiException e) {
        log.error("Metadata API 异常: {}", e.getErrorMessage(), e);
        return AjaxResult.error(e.getErrorCode(), e.getErrorMessage());
    }

    /**
     * 处理其他异常
     * 
     * @param e 异常
     * @return 错误结果
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e) {
        log.error("系统异常", e);
        return AjaxResult.error("系统异常: " + e.getMessage());
    }
}

## 总结

### 成功经验

1. **遵循项目规则**: 严格按照项目规则的 6 个阶段执行,确保文档的完整性和可追溯性。
2. **架构决策合理**: 在架构决策阶段详细分析了多种方案,选择了最合适的方案。
3. **提示词设计完善**: 提示词设计清晰、详细,能够指导 AI 完成开发任务。
4. **文档更新及时**: 及时更新 CHANGELOG.md 和 index.md保持文档的同步。

### 改进方向

1. **增加异步执行示例**: 在提示词模板中增加详细的异步执行示例。
2. **增加状态轮询示例**: 在提示词模板中增加详细的状态轮询示例。
3. **增加错误处理示例**: 在提示词模板中增加详细的错误处理示例。
4. **增加性能优化建议**: 在提示词模板中增加性能优化建议。

### 下一步行动

1. **更新提示词模板**: 根据复盘结果,更新 Prompt-015 模板,增加异步执行、状态轮询、错误处理示例。
2. **应用到其他需求**: 将改进后的模板应用到其他 REQ-010 子需求的实现中。
3. **持续优化**: 在后续的开发中,持续优化提示词模板,提高 AI 生成代码的质量和效率。

## 相关文档

- [REQ-010-5.md](../requirements/REQ-010-5.md) - Metadata API客户端封装需求
- [ADR-0014.md](../decisions/adr/0014-metadata-api-client.md) - Metadata API客户端封装架构决策
- [Prompt-015.md](../prompts/015-metadata-api-client.md) - Metadata API客户端封装实现提示词
- [Session-20260118-002.md](../sessions/20260118-metadata-api-client.md) - Metadata API客户端封装执行会话
- [Change-015.md](../changelog/20260118-metadata-api-client.md) - Metadata API客户端封装变更记录

## 审核记录

| 日期 | 审核人 | 审核结果 | 审核意见 |
|------|--------|----------|----------|
| 2026-01-18 | Datai Team | 已通过 | 复盘完整,改进建议合理 |

## 变更历史

| 日期 | 版本 | 变更内容 | 变更人 |
|------|------|---------|--------|
| 2026-01-18 | v1.0.0 | 初始版本 | Datai Team |