news 2026/9/12 16:16:15

fhevm 实战:用 Heads or Tails 游戏理解公开解密(Public Decryption)与链上 KMS 签名验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fhevm 实战:用 Heads or Tails 游戏理解公开解密(Public Decryption)与链上 KMS 签名验证

fhevm 实战:用 Heads or Tails 游戏理解公开解密(Public Decryption)与链上 KMS 签名验证

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

导读

本文以 fhevm 仓库中的 HeadsOrTails 示例 为骨架,完整演示一个"掷硬币"链上游戏如何利用 fhevm 的**公开解密(public decryption)**机制,在无需任何用户私钥授权的前提下,让任何人都能请求解密、并在链上通过 KMS 签名证明验证结果的真实性。读完本文,你将掌握FHE.randEbool()FHE.makePubliclyDecryptable()FHE.checkSignatures()的完整调用链,以及如何用 Hardhat 测试套件验证"单个明文值确实是对应单个链上密文的可验证解密结果"这一核心断言。


一、示例要回答的核心问题

该示例的出发点非常聚焦:如何让"所有人都能看"的解密结果,在链上被"可验证地"信任

具体而言,示例要证明的核心断言是:

给定的一个明文(cleartext),是对应于链上一个原始密文(ciphertext)解密的、可被密码学验证的结果。

换句话说,公开解密并不是简单地"把密文解开给大家看",而是要在解密之外附带一份由KMS(密钥管理系统)签名生成、可在链上校验的解密证明(decryption proof)。任何人(包括恶意操作者)都可以提交解密结果,但如果结果被篡改、证明被伪造、或张冠李戴(把游戏 A 的结果安到游戏 B 上),链上验证环节必须使其交易失败。

与此相关的完整背景可参考仓库中的 Public Decryption SDK 指南:公开解密适用于"所有人共享结果"的场景(例如私人拍卖的成交价),由 Relayer 通过 HTTP 接口发起解密请求,同时返回明文值与可验证的密码学证明。


二、示例概览:一局"掷硬币"游戏的完整数据流

Heads or Tails(猜正反)是一局两人游戏:一个玩家选"正面(Heads)",另一个选"反面(Tails)",合约用 fhevm 的随机数生成一个加密的布尔值ebool作为游戏结果。整个流程分三个阶段:

  1. 开局(加密阶段):调用headsOrTails(),合约内部用FHE.randEbool()生成加密随机结果,存入Game结构体,并调用FHE.makePubliclyDecryptable()把该结果标记为"可公开解密"。
  2. 解密(链下阶段):任何人在链下调用 Zama Relayer 的publicDecrypt接口,提交密文句柄(handle),Relayer 返回明文值 + ABI 编码值 + KMS 解密证明。
  3. 结算(链上验证阶段):调用recordAndVerifyWinner(),合约用FHE.checkSignatures()校验"明文 ↔ 密文 ↔ 证明"三者绑定关系,验证通过后正式宣布胜者。

这个流程最大的特点是不再依赖传统 oracle 工作流HeadsOrTails.sol的注释明确说明,"Instead of calling the function FHE.requestDecryption, we make the result publicly decryptable directly"(不再调用requestDecryption,而是直接把结果设为可公开解密),最终由终端用户/任意调用者自己完成解密请求与结果提交。


三、运行前置:文件放置与测试环境

原文档对运行环境给出了两条硬性约束,必须严格遵循:

  1. 文件放置位置(确保 Hardhat 能正常编译与测试):
    • .sol合约文件 →<your-project-root-dir>/contracts/
    • .ts测试文件 →<your-project-root-dir>/test/
  2. 测试环境约束:测试代码开头有明确检查:
if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); }

即该测试套件只能运行在 fhevm mock 环境(本地 Hardhat/Anvil 网络,chainId = 31337),不能在 Sepolia 测试网直接运行。这是因为 mock 环境内置了 Relayer 的模拟实现,测试才能通过fhevm.publicDecrypt()一步到位地拿到明文与证明。

测试中涉及的核心类型Signers来自测试工具目录,测试所需的类型链(typechain-types)由 Hardhat 编译时自动生成。


四、合约实现深度解析:HeadsOrTails.sol

合约的完整代码(本文档主版本)如下,随后逐段拆解:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, ebool } from "@fhevm/solidity/lib/FHE.sol"; import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * @title HeadsOrTails * @notice Implements a simple Heads or Tails game demonstrating public, permissionless decryption * using the FHE.makePubliclyDecryptable feature. * @dev Inherits from ZamaEthereumConfig to access FHE functions like FHE.randEbool() and FHE.verifySignatures(). */ contract HeadsOrTails is ZamaEthereumConfig { constructor() {} /** * @notice Simple counter to assign a unique ID to each new game. */ uint256 private counter = 0; /** * @notice Defines the entire state for a single Heads or Tails game instance. */ struct Game { /// @notice The address of the player who chose Heads. address headsPlayer; /// @notice The address of the player who chose Tails. address tailsPlayer; /// @notice The core encrypted result. This is a publicly decryptable ebool handle. // true means Heads won; false means Tails won. ebool encryptedHasHeadsWon; /// @notice The clear address of the final winner, set after decryption and verification. address winner; } /** * @notice Mapping to store all game states, accessible by a unique game ID. */ mapping(uint256 gameId => Game game) public games; /** * @notice Emitted when a new game is started, providing the encrypted handle required for decryption. * @param gameId The unique identifier for the game. * @param headsPlayer The address choosing Heads. * @param tailsPlayer The address choosing Tails. * @param encryptedHasHeadsWon The encrypted handle (ciphertext) storing the result. */ event GameCreated( uint256 indexed gameId, address indexed headsPlayer, address indexed tailsPlayer, ebool encryptedHasHeadsWon ); /** * @notice Initiates a new Heads or Tails game, generates the result using FHE, * and makes the result publicly available for decryption. * @param headsPlayer The player address choosing Heads. * @param tailsPlayer The player address choosing Tails. */ function headsOrTails(address headsPlayer, address tailsPlayer) external { require(headsPlayer != address(0), "Heads player is address zero"); require(tailsPlayer != address(0), "Tails player is address zero"); require(headsPlayer != tailsPlayer, "Heads player and Tails player should be different"); // true: Heads // false: Tails ebool headsOrTailsResult = FHE.randEbool(); counter++; // gameId > 0 uint256 gameId = counter; games[gameId] = Game({ headsPlayer: headsPlayer, tailsPlayer: tailsPlayer, encryptedHasHeadsWon: headsOrTailsResult, winner: address(0) }); // We make the result publicly decryptable. FHE.makePubliclyDecryptable(headsOrTailsResult); // You can catch the event to get the gameId and the encryptedHasHeadsWon handle // for further decryption requests, or create a view function. emit GameCreated(gameId, headsPlayer, tailsPlayer, games[gameId].encryptedHasHeadsWon); } /** * @notice Returns the number of games created so far. * @return The number of games created. */ function getGamesCount() public view returns (uint256) { return counter; } /** * @notice Returns the encrypted ebool handle that stores the game result. * @param gameId The ID of the game. * @return The encrypted result (ebool handle). */ function hasHeadsWon(uint256 gameId) public view returns (ebool) { return games[gameId].encryptedHasHeadsWon; } /** * @notice Returns the address of the game winner. * @param gameId The ID of the game. * @return The winner's address (address(0) if not yet revealed). */ function getWinner(uint256 gameId) public view returns (address) { require(games[gameId].winner != address(0), "Game winner not yet revealed"); return games[gameId].winner; } /** * @notice Verifies the provided (decryption proof, ABI-encoded clear value) pair against the stored ciphertext, * and then stores the winner of the game. * @param gameId The ID of the game to settle. * @param abiEncodedClearGameResult The ABI-encoded clear value (bool) associated to the `decryptionProof`. * @param decryptionProof The proof that validates the decryption. */ function recordAndVerifyWinner( uint256 gameId, bytes memory abiEncodedClearGameResult, bytes memory decryptionProof ) public { require(games[gameId].winner == address(0), "Game winner already revealed"); // 1. FHE Verification: Build the list of ciphertexts (handles) and verify the proof. // The verification checks that 'abiEncodedClearGameResult' is the true decryption // of the 'encryptedHasHeadsWon' handle using the provided 'decryptionProof'. // Creating the list of handles in the right order! In this case the order does not matter since the proof // only involves 1 single handle. bytes32[] memory cts = new bytes32[](1); cts[0] = FHE.toBytes32(games[gameId].encryptedHasHeadsWon); // This FHE call reverts the transaction if the decryption proof is invalid. FHE.checkSignatures(cts, abiEncodedClearGameResult, decryptionProof); // 2. Decode the clear result and determine the winner's address. // In this very specific case, the function argument `abiEncodedClearGameResult` could have been a simple // `bool` instead of an abi-encoded bool. In this case, we should have compute abi.encode on-chain bool decodedClearGameResult = abi.decode(abiEncodedClearGameResult, (bool)); address winner = decodedClearGameResult ? games[gameId].headsPlayer : games[gameId].tailsPlayer; // 3. Store the winner games[gameId].winner = winner; } }

注:仓库中的 library-solidity/examples/HeadsOrTails.sol 是同一思路的另一版本实现(checkWinner函数、命名略有差异),与本文档主版本可对照阅读。

4.1 继承ZamaEthereumConfig:绑定网络协处理器

合约继承自ZamaEthereumConfig(定义于 library-solidity/config/ZamaConfig.sol):

abstract contract ZamaEthereumConfig { constructor() { FHE.setCoprocessor(ZamaConfig.getEthereumCoprocessorConfig()); } ... }

它在构造阶段根据block.chainid注入 fhevm 运行所必需的三个系统合约地址(ACL、Coprocessor、KMSVerifier)。ZamaConfig内部按链路由:Ethereum 主网(chainId = 1)、Sepolia(chainId = 11155111)以及本地网络(chainId = 31337)都返回各自已部署的配置;其他链会 revertZamaProtocolUnsupported。如果要在 Polygon / Amoy 上部署,则应改用ZamaPolygonConfigZamaMultiChainConfig。这正是FHE.randEbool()FHE.checkSignatures()等函数能够正常工作的前置条件。

4.2 开局:FHE.randEbool()生成加密随机结果

randEbool()在 library-solidity/lib/FHE.sol 中的实现非常简单:

function randEbool() internal returns (ebool) { return ebool.wrap(Impl.rand(FheType.Bool)); }

真正的工作发生在Impl.rand(见 library-solidity/lib/Impl.sol):它通过协处理器合约发起IFHEVMExecutor(...).fheRandBounded(upperBound, randType)调用。也就是说,"掷硬币"的随机性由 fhevm 协处理器在密文域内生成,合约本身始终只接触加密句柄,游戏结果从诞生那一刻起就是保密的。

ebool是 fhevm 的加密布尔类型(true= 正面胜,false= 反面胜),其底层是一个bytes32密文句柄。

4.3 关键一步:FHE.makePubliclyDecryptable()开放公开解密

FHE.makePubliclyDecryptable(ebool value)同样是薄封装(FHE.sol 中每个加密类型各有一个重载,从ebooleuint8euint256eaddress全覆盖),最终落到:

function makePubliclyDecryptable(bytes32 handle) internal { CoprocessorConfig storage $ = getCoprocessorConfig(); bytes32[] memory handleArray = new bytes32[](1); handleArray[0] = handle; IACL($.ACLAddress).allowForDecryption(handleArray); }

它的本质是调用ACL 合约allowForDecryption,把该句柄标记为"允许任何人解密"。这与 fhevm 的权限模型直接相关(详见 host-contracts/contracts/ACL.sol 及配套的 ACL 指南):默认情况下,密文句柄只能由拥有权限的账户使用,而makePubliclyDecryptable相当于把"解密权"放开给全网。调用者必须是对该句柄有权限的账户,否则 ACL 会拒绝(Impl.sol的注释明确警告:"The caller must be allowed to use handle for makePubliclyDecryptable() to succeed. If not, makePubliclyDecryptable() reverts.")。

也可以配合Impl.isPubliclyDecryptable(handle)(内部调用IACL.isAllowedForDecryption)来链上查询某个句柄是否已是公开可解密状态。

4.4 结算:FHE.checkSignatures()完成链上验证

结算函数的关键步骤是把存储的ebool句柄转成bytes32数组(注意句柄列表顺序必须与明文的 ABI 编码顺序一致,此处只有一个句柄,顺序天然无歧义),再交给FHE.checkSignatures()

bytes32[] memory cts = new bytes32[](1); cts[0] = FHE.toBytes32(games[gameId].encryptedHasHeadsWon); FHE.checkSignatures(cts, abiEncodedClearGameResult, decryptionProof);

checkSignatures在 FHE.sol 中的定义(约第 9831 行)明确了它的行为与回滚条件:

  • 入参handlesList(句柄数组)、abiEncodedCleartexts(各句柄对应明文的 ABI 编码,顺序必须一致)、decryptionProof(KMS 公开解密证明,内含 KMS 签名、关联元数据与验证上下文)。
  • 验证内容decryptionProof非空且长度合法;有效签名数量达到 KMS 签名者阈值;每个签名都来自已注册的 KMS 签名者;签名验证本身通过。
  • 失败行为:任一条件不满足即revert InvalidKMSSignatures()
  • 成功行为:发出PublicDecryptionVerified(handlesList, abiEncodedCleartexts)事件,该事件对前端监听"链上已验证某次解密结果"非常关键。

checkSignatures非 view函数,且利用 transient storage 缓存验证结果以降低 gas;文档明确建议优先使用它而非 view 变体isPublicDecryptionResultValid,因为后者返回布尔值,调用方一旦忘记require,伪造结果就会静默通过——这是公开解密集成中最常见的安全陷阱。

验证通过后,合约解码abiEncodedClearGameResultbool,据此把胜者确定为headsPlayertailsPlayer并写入存储。文档还提醒了一个 Solidity 细节:如果函数参数直接传bool而非 ABI 编码的bytes,就需要在链上先abi.encode再交给checkSignatures,因此这里统一采用"外部传入 ABI 编码值"的形态,与 Relayer SDK 的返回格式天然对齐。


五、测试与 Relayer 集成:HeadsOrTails.ts

5.1 测试环境初始化与部署

测试在before钩子中校验 mock 环境并获取三个签名者:owneralicebob,其中playerA = aliceplayerB = bobbeforeEach中每次重新部署合约,保证用例彼此隔离:

async function deployFixture() { const factory = (await ethers.getContractFactory("HeadsOrTails")) as HeadsOrTails__factory; const headsOrTails = (await factory.deploy()) as HeadsOrTails; const headsOrTails_address = await headsOrTails.getAddress(); return { headsOrTails, headsOrTails_address }; }

5.2 核心正向用例:解密成功并公布胜者

正向用例的完整链路如下(这是全文最核心的实战代码):

// Starts a new Heads or Tails game. This will emit a `GameCreated` event const tx = await contract.connect(signers.owner).headsOrTails(playerA, playerB); // Parse the `GameCreated` event const gameCreatedEvent = parseGameCreatedEvent(await tx.wait()); // GameId is 1 since we are playing the first game expect(gameCreatedEvent.gameId).to.eq(1); expect(gameCreatedEvent.headsPlayer).to.eq(playerA.address); expect(gameCreatedEvent.tailsPlayer).to.eq(playerB.address); expect(await contract.getGamesCount()).to.eq(1); const gameId = gameCreatedEvent.gameId; const encryptedBool: string = gameCreatedEvent.encryptedHasHeadsWon; // Call the Zama Relayer to compute the decryption const publicDecryptResults = await fhevm.publicDecrypt([encryptedBool]); // The Relayer returns a `PublicDecryptResults` object containing: // - the ORDERED clear values (here we have only one single value) // - the ORDERED clear values in ABI-encoded form // - the KMS decryption proof associated with the ORDERED clear values in ABI-encoded form const abiEncodedClearGameResult = publicDecryptResults.abiEncodedClearValues; const decryptionProof = publicDecryptResults.decryptionProof; // Let's forward the `PublicDecryptResults` content to the on-chain contract whose job // will simply be to verify the proof and declare the final winner of the game await contract.recordAndVerifyWinner(gameId, abiEncodedClearGameResult, decryptionProof); const winner = await contract.getWinner(gameId); expect(winner === playerA.address || winner === playerB.address).to.eq(true);

GameCreated事件解析:测试提供了解析辅助函数parseGameCreatedEvent(基于contract.interface.parseLog遍历交易收据中的日志)。文档明确标注该函数"仅作演示、非生产级",因为它没有处理同一交易内出现多个事件的情况。从事件中取出encryptedHasHeadsWon句柄,是发起解密请求的前提;事件索引字段gameIdheadsPlayertailsPlayer便于链下检索。

fhevm.publicDecrypt()的返回值结构PublicDecryptResults):

字段含义
clearValues有序明文值(此处仅一个布尔值)
abiEncodedClearValues有序明文值的 ABI 编码形式
decryptionProof与 ABI 编码明文对应的 KMS 解密证明

关于公开解密的 HTTP 语义,可对照 docs/sdk-guides/public-decryption.md 中的instance.publicDecrypt(handles)示例理解:SDK 把句柄列表发送到 Relayer,Relayer 校验该句柄确实"允许公开解密"(对应合约里的allowForDecryption标记),执行解密并返回明文与证明。SDK 侧的底层实现在 sdk/js-sdk/src/core/kms/publicDecrypt.ts 及 sdk/js-sdk/src/core/modules/relayer/cleartext/fetchPublicDecrypt.ts:后者通过publicDecryptABI 调用 Relayer 合约,并完成"checkAllowedForDecryption"(第 4 步)与"签名摘要与验证者匹配"(第 6 步)等关键检查。

测试要点:正向用例最后断言winner必然是两位玩家之一——因为在密文域随机、解密证明经 KMS 签名且链上验证的前提下,结果只可能是正面或反面,不存在第三种可能。

5.3 三个负向用例:验证安全边界

三个负向用例共同的目标是证明:任何人提交的"结果"都无法逃过链上验证

用例一:证明无效必须失败

await expect( contract.recordAndVerifyWinner( gameCreatedEvent.gameId, publicDecryptResults.abiEncodedClearValues, publicDecryptResults.decryptionProof + "dead", // 在证明尾部拼接垃圾字节 ), ).to.be.revertedWithCustomError( { interface: new EthersT.Interface(["error KMSInvalidSigner(address invalidSigner)"]) }, "KMSInvalidSigner", );

篡改证明的任一字节,KMS 签名校验随即失效。这里观察到一个实现细节:文档版本测试断言的是KMSInvalidSigner自定义错误(来自 KMSVerifier 合约),而checkSignatures本身兜底抛出InvalidKMSSignatures;不同 mock/网络版本下错误路径可能以不同自定义错误呈现,测试断言以当前环境为准。

用例二:伪造游戏结果必须失败

const clearHeadsHasWon = publicDecryptResults.clearValues[gameCreatedEvent.encryptedHasHeadsWon]; const decodedHeadsHasWon = EthersT.AbiCoder.defaultAbiCoder().decode( ["bool"], publicDecryptResults.abiEncodedClearValues, )[0]; expect(decodedHeadsHasWon).to.eq(clearHeadsHasWon); // Let's try to forge the game result const forgedABIEncodedClearValues = EthersT.AbiCoder.defaultAbiCoder().encode(["bool"], [!clearHeadsHasWon]); await expect( contract.recordAndVerifyWinner( gameCreatedEvent.gameId, forgedABIEncodedClearValues, publicDecryptResults.decryptionProof, ), ).to.be.revertedWithCustomError(...);

恶意操作者拿到了真实证明,但把明文从true翻转为false(或反之)再提交。由于 KMS 签名绑定的是"句柄 + ABI 编码明文"这一完整三元组,明文一变,签名即失效,交易必然回滚。这从测试侧印证了核心断言:明文与密文的绑定关系是密码学强制保证的,而非合约逻辑约定

用例三:跨游戏张冠李戴必须失败

// Game 1 const tx1 = await contract.connect(signers.owner).headsOrTails(playerA, playerB); const gameCreatedEvent1 = parseGameCreatedEvent(await tx1.wait()); // Game 2 const tx2 = await contract.connect(signers.owner).headsOrTails(playerA, playerB); const gameCreatedEvent2 = parseGameCreatedEvent(await tx2.wait()); // Let's try to forge the Game1's winner using the result of Game2 const publicDecryptResults2 = await fhevm.publicDecrypt([gameCreatedEvent2.encryptedHasHeadsWon]); await expect( contract.recordAndVerifyWinner( gameCreatedEvent1.gameId, publicDecryptResults2.abiEncodedClearValues, publicDecryptResults2.decryptionProof, ), ).to.be.revertedWithCustomError(...);

这是最隐蔽的攻击:Game2 的解密结果本身完全合法(证明有效、明文正确),只是被错用于 Game1。验证会失败,是因为 Game1 存储的句柄encryptedHasHeadsWon与证明所锚定的 Game2 句柄不是同一个密文——checkSignatures校验的正是"这组明文确实是这组句柄的解密结果"。句柄不同,绑定关系即被破坏。

5.4 测试工具与本地验证命令

测试中使用的Signers类型、fhevm全局对象(含isMockpublicDecrypt)来自 Hardhat fhevm 插件与仓库测试基础设施,可参考 library-solidity/test/signers.ts 与 library-solidity/test/instance.ts 中的实现。在本仓库对应的测试目录(如library-solidity/testtest-suite/e2e/test等)下按 Hardhat 惯例执行npx hardhat test即可跑通完整用例。


六、公开解密的安全模型与适用边界

通过这个示例,可以提炼出 fhevm 公开解密机制的完整安全模型:

  1. 密文侧:游戏结果由randEbool()在协处理器内以密文形式生成,合约层看不到明文,杜绝了合约开发者/验证者作弊的可能。
  2. 权限侧makePubliclyDecryptable通过 ACL 的allowForDecryption放行解密权;未标记的句柄无法被 Relayer 解密。想要保持隐私的场景(如私人转账、机密拍卖出价)则不应调用此函数。
  3. 证明侧:解密由持有 FHE 密钥的 KMS 完成,KMS 为每个句柄+明文组合签发多签证明;链上checkSignatures负责校验签名者注册状态、签名阈值与签名有效性。
  4. 结算侧:证明与句柄、明文三元组强绑定,任何"改明文、改证明、换句柄"的操作都在链上验证环节被拦截,三个负向用例分别覆盖了这三类攻击路径。

需要留意的是:公开解密意味着结果对全网可见,因此它天然适用于"结果需要公开共识"的业务(游戏开奖、拍卖成交、投票计票等),而非隐私保护场景。若需要"仅特定用户可见"的解密,应转向 fhevm 的**用户解密(user decryption)**机制(参见 docs/sdk-guides/user-decryption.md 与 用户解密示例)。


七、总结与延伸阅读

Heads or Tails示例用不到百行合约 + 一组精心设计的正反向测试,完整覆盖了 fhevm 公开解密的三个核心环节:密文域随机、全网公开解密授权、链上 KMS 签名验证。它同时演示了生产级集成中必须警惕的三类攻击(证明伪造、结果伪造、跨游戏错配),是对"单个明文 = 单个密文的可验证解密结果"这一核心断言最直观的工程化诠释。

继续深入可参阅以下仓库材料:

  • 合约源码层面:library-solidity/lib/FHE.sol(randEbool/makePubliclyDecryptable/checkSignatures定义)、library-solidity/lib/Impl.sol(底层协处理器与 ACL 调用)、library-solidity/config/ZamaConfig.sol(网络配置路由);
  • 权限模型:docs/solidity-guides/acl/README.md 与 host-contracts/contracts/ACL.sol;
  • 解密指南:docs/sdk-guides/public-decryption.md、docs/solidity-guides/decryption/;
  • 更多同主题示例:公开解密示例 MakePubliclyDecryptable.sol、docs/examples/fhe-encrypt-single-value.md。

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 16:14:28

WT2003Hx B1指令实现毫秒级语音插播

1. 项目概述&#xff1a;为什么“插播”在语音播报场景里不是锦上添花&#xff0c;而是生死线 我做嵌入式语音模块开发快八年了&#xff0c;从WT2003S、WT2003M一路用到现在的WT2003Hx&#xff0c;踩过的坑比走过的路还多。去年给一个地铁站台广播系统做升级时&#xff0c;客户…

作者头像 李华
网站建设 2026/9/12 16:12:53

Claude Code UI 完整指南:10 分钟远程管好你的 AI 编码会话

Claude Code UI 完整指南&#xff1a;10 分钟远程管好你的 AI 编码会话 【免费下载链接】claudecodeui Use Claude Code, OpenCode, Cursor CLI, and Codex on mobile and web with CloudCLI (aka Claude Code UI). CloudCLI is a free open source webui/GUI that helps you m…

作者头像 李华
网站建设 2026/9/12 16:10:55

专科生论文写作神器:9款AI工具实测推荐

1. 论文写作痛点与AI工具崛起作为经历过毕业论文折磨的老学长&#xff0c;我深知专科生在论文写作中的三大痛点&#xff1a;文献综述无从下手、开题报告逻辑混乱、重复率居高不下。去年帮表弟改论文时&#xff0c;发现现在AI写作工具已经能解决80%的基础性问题。但市面上近百款…

作者头像 李华