接口加密做成框架级能力有多难?我扒了 3600 行源码:从 RSA 握手到落库密文迁移

配套源码:开源框架 ForgeAdmin(forge-starter-crypto 模块,3638 行 Java) 系列定位:企业级安全设施源码深扒。上一篇《数据权限拦截器:681 行 SQL 改写》讲了"谁能看",这篇讲"传输和落库都不怕被看"。

开头:三个场景,逼着你把加密做成框架能力

先说我踩过的三个真实场景:

场景一,抓包。 前两年做某政务系统的接口联调,甲方扔给我一个他们抓的 HTTP 包:登录请求里明文躺着账号密码,响应里明文躺着身份证号。对方只说了句"我们等保测评要过了"------剩下的话不用说了,全是我的活。

场景二,密评。 项目要求密码算法用国密(SM2/SM3/SM4),不能拿 AES 直接交差。改算法本身不难,难的是线上已经跑了两年的存量数据------几百万条旧 AES 密文躺在库里,你不能停机全表重写,得让新旧算法共存着平滑切过去。

场景三,字段泄漏。 全链路加密做完,接口是安全的了。结果一次日志排查发现,业务代码把整个实体 toString() 打进了日志------身份证、手机号、银行卡号,明文躺在日志文件里,比接口泄漏还狠。

三个场景拼起来,结论很明确:加密不该是某个接口的临时方案,它得是一套框架级能力------能按需开关、能加解密自动拦截、能落库自带版本管理、能顺手把脱敏也干了。

这篇文章把我设计的 3638 行加解密 starter(forge-starter-crypto)从外到内拆一遍,讲清楚一件事:传输加密、存储加密、脱敏,这三件事怎么在一个框架里统一掉

一、先给全貌:一条加密请求的一生

先看整体架构,避免后面贴代码时迷路。

css 复制代码
浏览器 / App / 第三方系统
        │ ① GET /crypto/public-key 拿 RSA 公钥(明文,仅一次)
        ▼
   ┌───────────────────────────┐
   │  KeyExchangeController    │  ② 前端随机生成会话密钥(SM4/AES)
   │  密钥协商(握手)          │     用 RSA 公钥加密后 POST 回来
   └───────────┬───────────────┘     后端 RSA 私钥解密 → 存 Redis
               ▼
   后续请求:POST /api/order {"data":"<对称密文>","algorithm":"SM4"}
               ▼
   ┌───────────────────────────────────────────────┐
   │  DecryptRequestBodyAdvice(RequestBodyAdvice)│  ③ 请求体自动解密
   │  读密文 → 定算法 → 找会话密钥 → 解密 →        │
   │  把明文流塞回 body,交给 Jackson 正常反序列化   │
   └───────────────────────────────────────────────┘
               ▼
   Controller 业务代码 ------ 全程无感知,拿到的是普通 DTO
               ▼
   ┌───────────────────────────────────────────────┐
   │  EncryptResponseBodyAdvice(ResponseBodyAdvice)│ ④ 响应体自动加密
   │  业务返回对象 → 序列化成 JSON → 加密 → 包壳返回   │
   └───────────────────────────────────────────────┘
               ▼
   落库前:字段级 @CryptoField 序列化器加密 / @Desensitize 脱敏
               ▼
   ┌───────────────────────────────────────────────┐
   │  VersionedPersistentCryptoService             │ ⑤ 落库密文带版本
   │  FPC1:{算法}:{keyId}:{密文} → 换钥/换算法可渐进迁移 │
   └───────────────────────────────────────────────┘

   横切:ReplayAttackFilter(防重放:X-Timestamp + X-Nonce)

从⑤往回看,你会发现一个有意思的设计:越靠近用户越"隐式",越靠近存储越"显式"

  • 传输层(③④):用 RequestBodyAdvice / ResponseBodyAdvice 做 AOP 拦截,业务代码零感知;
  • 字段层:用 Jackson 注解 + 自定义序列化器,一个 @CryptoField 解决问题;
  • 存储层(⑤):密文里显式带上算法和 keyId------因为落库的密文要活很多年,必须自己能解释自己

下面按这个顺序拆。

二、算法层:一个接口 + 一个工厂,塞进三种算法

加密框架最底层是算法抽象。接口只有四个方法 + 一个标识:

java 复制代码
public interface Encryptor {
    /** 使用默认密钥加密 */
    String encrypt(String plainText);
    /** 使用指定密钥加密(会话密钥场景) */
    String encrypt(String plainText, String key);
    /** 使用默认密钥解密 */
    String decrypt(String cipherText);
    /** 使用指定密钥解密 */
    String decrypt(String cipherText, String key);
    /** 支持的算法类型 */
    CryptoAlgorithm algorithm();
}

关键是所有密文都是 Base64 字符串------算法差异被抹平在接口里,上层永远在跟字符串打交道,不关心底层是块密码还是流密码。

算法枚举把三套都注册了:

java 复制代码
SM4("SM4", "国密SM4对称加密"),
AES("AES", "AES对称加密"),
AES_GCM("AES_GCM", "AES-GCM认证加密");

注意 fromCode(null) 的兜底是 SM4------默认算法走国密,这是为密评场景设计的(等保/密评要求密码算法合规,默认就给你合规的)。

工厂用 ConcurrentHashMap 做注册表:

java 复制代码
public class EncryptorFactory {
    private final Map<CryptoAlgorithm, Encryptor> encryptorMap = new ConcurrentHashMap<>();

    public void register(Encryptor encryptor) {
        encryptorMap.put(encryptor.algorithm(), encryptor);
    }

    public Encryptor getEncryptor(String algorithm) {
        CryptoAlgorithm algo = StrUtil.isNotBlank(algorithm)
                ? CryptoAlgorithm.fromCode(algorithm)          // 按传入算法
                : CryptoAlgorithm.fromCode(properties.getAlgorithm()); // 全局默认
        return getEncryptor(algo);
    }
    // getEncryptor(CryptoAlgorithm) 未命中直接抛 IllegalArgumentException
}

设计要点:算法是运行时可选的,不是编译期写死的 。请求体里可以带 algorithm 字段、注解上可以指定 algorithm,框架根据调用方决定用哪套------这就是后面"多算法平滑迁移"的地基。

三、握手层:RSA 保护密钥,对称加密跑业务

传输加密最大的坑是密钥从哪来。你不可能把对称密钥写死在前端 JS 里(一抓包就泄露),也不能每次请求都用 RSA 加解密(性能扛不住)。

标准解法是"混合加密",代码里分三步:

第一步,前端拿 RSA 公钥。 KeyExchangeController 暴露一个明文接口返回公钥(Base64)。

第二步,前端生成会话密钥,用 RSA 公钥加密回传:

java 复制代码
public class KeyExchangeService {
    // 前端 POST 上来的、被 RSA 公钥加密过的会话密钥
    public boolean exchangeKey(String sessionId, String encryptedKey) {
        // RSA 私钥解密出真正的会话密钥
        String sessionKey = rsaKeyPairHolder.decryptByPrivateKey(encryptedKey);
        // 存 Redis,2 小时过期
        sessionKeyStore.storeKey(sessionId, sessionKey);
        return true;
    }
}

第三步,会话密钥进 Redis 存储:

java 复制代码
public class SessionKeyStore {
    private static final String KEY_PREFIX = "crypto:session:";
    // key = crypto:session:{会话ID},value = 对称密钥,TTL 2h
    public void storeKey(String sessionId, String secretKey) {
        String cacheKey = KEY_PREFIX + sessionId;
        cacheService.set(cacheKey, secretKey, expireSeconds, TimeUnit.SECONDS);
    }
    public String getKey(String sessionId) {
        Object value = cacheService.get(KEY_PREFIX + sessionId);
        return value != null ? value.toString() : null;
    }
}

之后业务请求里,对称密文用它解密。RSA 只干一次活(握手),日常流量全走对称加密------这就是性能和安全的平衡点。

会话标识从哪取?框架给了三级回退,兼容各种前端:

java 复制代码
private String getSessionIdFromRequest(HttpServletRequest request) {
    // 1. Authorization: Bearer <token>(最常见)
    // 2. X-Session-Id 头
    // 3. HTTP Session ID
}

补充一个容易漏的点:会话密钥在 Redis 有过期时间,过期后前端需要重新握手。所以前端 SDK 里要监听解密失败(服务端返回"密钥失效"类错误)后自动重走一次握手,这个降级链路不做,线上就会莫名冒出"偶发解密失败"。

四、请求/响应层:两个 Advice 撑起全自动加解密

这是框架的核心,也是最"隐式"的部分------业务代码完全无感。

4.1 请求解密:DecryptRequestBodyAdvice

java 复制代码
@RestControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // ① 总开关没开 → 不处理
        if (!enabled || !enableApiCrypto) return false;
        // ② 明文控制端点(密钥配置页)→ 不处理
        if (isPlaintextControlEndpoint(request)) return false;
        // ③ 可信内部调用(如流程客户端)→ 明文直达,跳过解密
        if (internalCallRequestVerifier.isTrustedInternalCall(request)) return false;
        // ④ 动态 API 配置优先;否则看类/方法上的 @ApiDecrypt
        ApiConfigInfo apiConfig = apiConfigManager.getApiConfig(uri, method);
        if (apiConfig != null) return apiConfig.getNeedEncrypt();
        return AnnotatedElementUtils.hasAnnotation(cls, ApiDecrypt.class)
                || methodParameter.hasMethodAnnotation(ApiDecrypt.class);
    }
}

这个 supports() 是整个框架的"路由中枢",四层判断值得细品:

  • 总开关(enabled + enableApiCrypto)保证功能可整体下掉,灰度失败一键回滚;
  • 明文端点白名单防止控制面(比如密钥配置接口本身)被加密逻辑套住形成死锁;
  • 内部调用识别 是最容易被忽略的坑------服务间调用(比如流程引擎回调)走的是明文 JSON,如果被当成外部请求强行解密,直接 500。框架用一个 InternalCallRequestVerifier 统一放行,避免每个内部接口都手动 @IgnoreCrypto
  • 动态配置优先于注解意味着运营可以在不改代码的情况下,把某个接口临时切到加密/明文。

真正的解密在 beforeBodyRead,注意算法三级优先级:

java 复制代码
@Override
public HttpInputMessage beforeBodyRead(...) throws IOException {
    // 读取请求体 → 解析成 { data, algorithm } 壳
    EncryptedRequest request = objectMapper.readValue(encryptedBody, EncryptedRequest.class);

    // 算法优先级:注解指定 > 请求体自带 > 全局默认
    String algorithm = 注解有值 ? annotation.algorithm()
            : (request.getAlgorithm() 非空 ? request.getAlgorithm()
            : properties.getAlgorithm());

    Encryptor encryptor = encryptorFactory.getEncryptor(algorithm);

    // 会话密钥优先,拿不到就退回默认密钥
    String sessionKey = getSessionKey();
    String decryptedData = (sessionKey != null)
            ? encryptor.decrypt(request.getData(), sessionKey)
            : encryptor.decrypt(request.getData());

    // 关键:把解密后的明文包成新的 HttpInputMessage 还回去
    return new DecryptedHttpInputMessage(
            inputMessage.getHeaders(),
            new ByteArrayInputStream(decryptedData.getBytes(StandardCharsets.UTF_8)));
}

DecryptedHttpInputMessage 是这个设计的精髓:RequestBodyAdvice 允许你在进入 JSON 反序列化之前偷换 body 流。解密后塞一个明文流进去,后面 Spring 的 Jackson 转换器完全不知道发生过什么------Controller 拿到的就是一个干干净净的 DTO。这就是"隐式"的实现方式。

4.2 响应加密:EncryptResponseBodyAdvice

响应侧对称,beforeBodyWrite 里把返回对象序列化成 JSON → 加密 → 包壳返回:

java 复制代码
@Override
public Object beforeBodyWrite(Object body, ...) {
    // 二进制响应(图片/文件下载 ResponseEntity<byte[]>)直接跳过
    if (isBinaryResponseType(returnType)) return body;

    String jsonBody = objectMapper.writeValueAsString(body);
    String sessionKey = getSessionKey(request);
    String encryptedData = (sessionKey != null)
            ? encryptor.encrypt(jsonBody, sessionKey)
            : encryptor.encrypt(jsonBody);

    return new EncryptedResponse(encryptedData, algorithm); // {data, algorithm}
}

值得说的是 isBinaryResponseType:文件下载接口返回 ResponseEntity<byte[]>,你要是把二进制当 JSON 序列化再加密,前端拿到的就是一堆乱码。所以 supports 阶段就要用泛型反射识别出 byte[] 类型参数,直接放行。

传输层的加密协议壳统一是 {data, algorithm},请求和响应共用同一套结构------前端 SDK 只写一次解密逻辑,所有接口通用。

五、字段层:@CryptoField 与 fail-closed 序列化器

传输加密解决"路上",但落库的敏感字段、打进日志的实体对象,光靠 Advice 管不到。字段级加密用 Jackson 注解解决:

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = CryptoFieldSerializer.class)
@JsonDeserialize(using = CryptoFieldDeserializer.class)
public @interface CryptoField {
    /** 加密算法,默认用全局配置 */
    String algorithm() default "";
    /** 序列化时是否加密 */
    boolean encrypt() default true;
    /** 反序列化时是否解密 */
    boolean decrypt() default true;
}

实体上标一下,出参自动加密、入参自动解密:

java 复制代码
public class CustomerVO {
    @CryptoField
    private String phone;      // 出参变密文,入参自动解密
    @CryptoField(algorithm = "SM4")
    private String idCard;     // 指定国密
}

实现上有个细节很关键------失败必须拒绝输出,而不是降级成明文

java 复制代码
public class CryptoFieldSerializer extends JsonSerializer<String>
        implements ContextualSerializer {

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        // ... 各种开关判断,不满足就走明文
        try {
            Encryptor encryptor = encryptorFactory.getEncryptor(annotation.algorithm());
            gen.writeString(encryptor.encrypt(value));
        } catch (Exception e) {
            log.error("字段加密失败: algorithm={}", annotation.algorithm(), e);
            throw new IOException("字段加密失败,拒绝输出原文", e);  // fail-closed
        }
    }
}

ContextualSerializer 让你能在序列化时拿到字段上的注解(property.getAnnotation(CryptoField.class)),从而决定用哪个算法------同一个 ObjectMapper,不同字段走不同加密器,这是 Jackson 注解驱动序列化器的标准玩法。

fail-closed(加密失败宁可报错也不放明文) 这条必须刻在脑子里:安全组件最怕"静默降级",一旦密钥配错,线上悄悄开始吐明文,等保审计一抓一个准。

字段加密和文章开头说的"日志泄漏"是同一件事的两面------字段密文即使被 toString() 打进日志,也是不可读的密文。这就是为什么我在开头第三个场景里说,字段级加密是日志泄漏的最后一道闸。

六、防重放:时间窗 + nonce 原子登记

加密只保证"看不懂",不保证"没被复制"。攻击者把抓到的合法密文原样重放一遍,服务端照样能解密成功------所以防重放必须跟加密配套。

ReplayAttackFilter 的思路是标准的 timestamp + nonce 双因子:

java 复制代码
// 请求头带 X-Timestamp 和 X-Nonce
String timestamp = httpRequest.getHeader("X-Timestamp");
String nonce = httpRequest.getHeader("X-Nonce");

// 1. 时间戳必须在窗口内(默认 60s,可配)
long requestTime = Long.parseLong(timestamp);
if (Math.abs(currentTime - requestTime) > timeWindow) {
    sendError(httpResponse, "请求已过期");   // 老请求直接拒
    return;
}

// 2. nonce 必须没被用过 ------ 原子登记
//    TTL = 2×窗口:覆盖时间戳允许的完整正负窗口,避免边界重放
long nonceTtlSeconds = replayWindowSeconds * 2L;
if (!tokenCache.markIfAbsent(nonce, nonceTtlSeconds)) {
    log.warn("检测到重复请求, nonce: {}", nonce);
    sendError(httpResponse, "重复的请求");    // 同一请求第二次来直接拒
    return;
}

两个细节是实践里踩出来的:

  • 时间戳窗口要允许正负偏差 ------Math.abs(currentTime - requestTime) > timeWindow,不然客户端时钟慢 30 秒,合法请求全被误杀;
  • nonce 的 TTL 要是窗口的 2 倍------注释里写得很清楚:"缓存覆盖时间戳允许的完整正负窗口,避免边界重放"。窗口 60s,那 timestamp 合法区间是 -60s, +60s,nonce 只存 60s 的话,一个在 -59s 发出的请求,重放到 +59s 时 nonce 已过期、但时间戳还在窗口内------就漏了。存 120s 才能把整个合法区间盖住。

配套的白名单机制:WebSocket 长连接(path.startsWith("/ws"))、内部调用、控制端点都不做防重放------WebSocket 本来就是长连,每帧都带 nonce 会直接把服务端 Redis 打爆。

七、存储层:密文版本化------换算法不用全表重写(全文最值钱的一段)

这是整个 starter 里我最想讲的设计,也是回答开头"密评改造怎么处理存量数据"的答案。

问题:假设线上跑着 200 万条 AES 密文,现在密评要求换成 SM4。怎么办?

  • 停机全表重写?业务不允许。
  • 双写 + 定时任务迁移?你得先能区分"哪条是 AES、哪条是 SM4",否则解密时不知道该用哪个密钥哪个算法。

解法是密文自描述(self-describing)------密文里带上算法和 keyId,让每一段密文都能解释自己:

java 复制代码
public record PersistentCiphertext(
        Format format,     // LEGACY / ACTIVE / HISTORICAL / UNKNOWN_KEY / UNKNOWN
        String algorithm,  // SM4 / AES / AES_GCM
        String keyId,      // 用哪把 key 加密的
        String payload,    // 密文本体
        String failureReason
) {
    public static final String VERSION = "FPC1";
}

版本化密文的存储格式是:

css 复制代码
FPC1:{算法}:{keyId}:{密文Base64}

inspect() 是核心分类器,每次读库都先跑一遍:

java 复制代码
public static PersistentCiphertext inspect(String value,
                                           String legacyAlgorithm,
                                           String activeKeyId,
                                           Set<String> readableKeyIds) {
    // 没有 FPC1: 前缀 → 旧格式密文,用 legacyAlgorithm(默认 SM4)解
    if (!value.startsWith(VERSION + ":")) {
        return new PersistentCiphertext(Format.LEGACY, legacyAlgorithm, null, value, null);
    }
    // 解析四段
    String[] parts = value.split(":", 4);
    String algorithm = parts[1];   // 这段密文用啥算法
    String keyId = parts[2];       // 这段密文用哪把 key
    String payload = parts[3];

    if (keyId.equals(activeKeyId)) {
        return ...(Format.ACTIVE, ...);        // 当前激活 key → 正常解
    }
    if (readableKeyIds.contains(keyId)) {
        return ...(Format.HISTORICAL, ...);    // 历史 key 还能读 → 可解,提示重加密
    }
    return ...(Format.UNKNOWN_KEY, ...);       // key 已销毁 → 明确报"读不了"
}

解密服务按分类走不同路径:

java 复制代码
public String decrypt(String ciphertext, String legacyAlgorithm) {
    PersistentCiphertext parsed = inspect(ciphertext, legacyAlgorithm);
    return switch (parsed.format()) {
        case ACTIVE, HISTORICAL -> decryptVersioned(parsed);  // 用密文自带的算法+keyId 解
        case LEGACY -> decryptLegacy(parsed);                 // 走旧 key 解
        default -> throw new IllegalArgumentException("无法解密的密文: " + parsed.failureReason());
    };
}

轮换(rotation)和迁移(migration)被彻底解耦了

  • 换算法/换 key,老数据不用动------因为它带着自己的算法和 keyId,新代码照样能解;
  • 想要渐进迁移,写个定时任务读一批 → decrypt 老密文 → 用新算法 encrypt → 覆盖写回,随时可停可续,不用停机窗口;
  • 甚至能精确盘点:CryptoMigrationReport 统计出库里还有多少条 LEGACY、多少条已迁移------迁移进度是可视化的,不是黑盒。

encrypt 侧还支持"读时重加密"模式(writeVersioned=true 时每次写都用当前激活 key),保证新数据永远用最新配置。

这个思路对任何"线上有存量密文"的系统都成立:密文必须自描述,算法和 keyId 跟着密文走,而不是跟着代码走。代码可以随时升级,密文要活很多年。

八、脱敏:一套注解,八种策略

最后补一块跟加密配套但不属于加密的:脱敏。加密是"能还原",脱敏是"不可逆",场景不同------手机号展示给客服看,你不需要给她真实号码,脱敏就行。

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface Desensitize {
    DesensitizeType type() default DesensitizeType.CUSTOM;  // PHONE/ID_CARD/EMAIL/...
    int prefixKeep() default 0;   // 自定义脱敏:前缀保留长度
    int suffixKeep() default 0;   // 后缀保留长度
    char replaceChar() default '*';
}

策略工厂在构造时一次性注册 8 种内置策略:手机号、身份证、邮箱、银行卡、姓名、地址、密码、车牌,外加 CUSTOM 自由组合 prefixKeep/suffixKeep/replaceChar。同一套 Jackson 注解体系,字段上标 @Desensitize(type = PHONE) 出参自动变 138****1234

九、踩坑清单(都是线上踩出来的)

  1. 会话密钥过期没做自动重协商:Redis 里的会话密钥 2 小时过期,前端不知道,还在拿旧密钥加密 → 偶发解密失败。前端 SDK 必须监听失败自动重走握手。
  2. 二进制响应被当 JSON 加密 :文件下载 ResponseEntity<byte[]> 没做泛型识别就进加密链路,前端收到乱码。supports 阶段用 ParameterizedType 反射跳过 byte\[\]。
  3. 防重放 nonce TTL 只设了窗口长度:60s 窗口配 60s TTL,边界重放漏网(-59s 发出的请求 +59s 重放)。TTL 必须 2×窗口。
  4. 内部服务调用被强行解密 :服务间回调是明文 JSON,被全局 Advice 拦下后 500。必须有 InternalCallRequestVerifier 白名单机制。
  5. 加密失败静默降级成明文:密钥配错时安全组件悄悄吐原文,等保审计一抓一个准。序列化器必须 fail-closed(失败抛异常拒绝输出)。
  6. 控制面接口被加密逻辑套死:密钥配置接口本身走了加密 → 死锁(没法改配置了)。控制端点必须进明文白名单。

十、能带走的三条设计诀窍

诀窍
传输层 Advice 拦截 + {data, algorithm} 统一壳协议,业务零感知、前端 SDK 只写一遍
算法层 Encryptor 接口 + 工厂注册表 + 运行时可选算法,多算法共存是平滑迁移的前提
存储层 密文自描述:算法和 keyId 跟密文走,不跟代码走------换钥/换算法不用全表重写

总结

回头看开头三个场景:抓包问题被 Advice 全链路加密解决;密评改造的存量数据被密文版本化解决(新数据默认 SM4、旧 AES 密文自描述可解、渐进重加密);日志泄漏被字段级加密 + 脱敏双重兜底。

安全能力的正确形态是"框架级 + 可开关":注解一标、配置一开,全站生效;出问题时总开关一关,立刻回到明文模式------而不是散落在每个业务接口里的临时方案。

下一篇想拆 forge-starter-idempotent下单接口被连点 10 次会发生什么------SpEL 动态 key + 三种幂等策略(严格模式/Token 预申请/结果缓存)+ Redisson 分布式锁,支付场景必踩的坑,源码 1279 行。想看的评论区扣个 1。


文章涉及的源码模块(全部开源):

模块 作用
forge-starter-crypto 本文主角:加解密/防重放/密文迁移/脱敏
forge-starter-core 注解定义:@ApiEncrypt / @ApiDecrypt / CryptoProperties

项目地址:github.com/yaomindong1... 在线演示:www.dlforgelab.com:8084/forge/login... / 123456) 标签:#Spring Boot #加密 #安全 #国密 #架构设计 #Java

相关推荐
掘金挖土1 小时前
前端手摸手跑路之 AI 应用开发(二)
前端·后端
老周聊架构1 小时前
Ontology:Palantir 架构真正的核心,不是数据库也不是知识图谱
数据库·架构·知识图谱
天衍四九-1 小时前
第一章:从 LLM 到 Agent —— DeepSeek Harness 入门
网络·数据库·人工智能·python
RAOY的AI笔记1 小时前
ChatGPT怎么用得更专业?Prompt工程、API调用与AI Agent工作流实战
人工智能·chatgpt·prompt
ocean21032 小时前
2025-2026年AI提效与实践大厂面试高频问题
人工智能·面试·职场和发展·提示词工程·ai提效
xlq223222 小时前
Ai大模型接入day 6
人工智能
视频技术分享2 小时前
视频会议系统技术全解:底层架构、选型标准与实战优化
架构·音视频
沧沧凉凉2 小时前
让我退掉 Claude 的不是代码能力,是一张图
人工智能·游戏·ai编程
新知图书2 小时前
第 6 章DeepSeek的Function Calling与MCP应用实战
人工智能·智能体