news 2026/8/7 3:23:08

cfapi 入门实战(三):为什么需要占位符文件(Placeholder)?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cfapi 入门实战(三):为什么需要占位符文件(Placeholder)?

云同步程序开发围绕Placeholder进行的!

这个微软官方定义占位符文件

生成支持占位符文件的云同步引擎 - Win32 apps | Microsoft Learn

  • 同步引擎可以创建只占用 1 KB 存储空间用于文件系统标头的占位符文件,并在正常使用条件下自动转变为完整文件。 占位符文件在 Windows Shell 中以典型文件的形式呈现给应用程序和最终用户。
  • 占位符文件从 Windows 内核垂直集成到 Windows Shell,并且与占位符文件的应用兼容性通常不是问题。 无论你是使用文件系统 API、命令提示符还是桌面或 UWP 应用来访问占位符文件,文件都会解除冻结,而无需进行其他代码更改,并且该应用可以正常使用该文件。
  • 文件可以存在于三种状态:
    • 占位符文件(Placeholder):文件的空表示形式,仅当同步服务可用时才可用。
    • 完整文件(Full):文件已隐式冻结,如果需要空间,系统可能会解除冻结。
    • 固定的完整文件(Full Pinned):该文件已由用户通过文件资源管理器下载,并保证可脱机使用。

下图演示了文件资源管理器中如何显示占位符、完整和固定的完整文件状态。

在真正开始写FetchData 回调、流式下载、同步状态更新之前,必须先理解一个核心问题:

CFAPI 为什么一定要用“占位符(Placeholder)”?

如果不理解这一点,后面几乎所有行为——
Explorer 显示、CreateFile 行为、OnFetchData 触发时机、同步状态图标——都会让人困惑。


一、CFAPI 的本质目标

CFAPI(Cloud Files API)的目标不是“做一个网盘下载器”,而是:

让远端数据,在本地看起来就像真实存在的文件系统对象

也就是说:

用户视角系统视角
文件已经在磁盘上实际数据可能完全不在本地
能看到文件名、大小、时间仅存在元数据
双击才下载按需拉取数据

占位符(Placeholder)正是实现这一点的基础设施。


二、什么是占位符(Placeholder)

一句话定义:

占位符是一个“没有真实文件数据,但具备完整文件系统元信息的文件 / 目录对象”

它存在于 NTFS 中,但内容是“空的”或“未实体化的”。

占位符具备什么?

✔ 文件名
✔ 路径
✔ 文件大小
✔ 创建 / 修改时间
✔ 文件属性
✔ 同步状态(云、已下载、部分下载)

占位符缺少什么?

✘ 实际文件数据(Data Stream)


三、没有占位符会发生什么?

假设你不用占位符,而是“真正下载后才创建文件”。

1️⃣ Explorer 无法显示远端文件

  • 文件不在磁盘上

  • Explorer 没有任何对象可枚举

  • 用户看到的是一个“空目录”

👉用户体验直接失败


2️⃣ CreateFile 无法被拦截

CFAPI 的核心是:

当用户或程序访问文件时,系统回调给 Provider

但前提是:

这个文件对象已经存在于 NTFS

没有占位符:

CreateFile("cloud.txt") → ERROR_FILE_NOT_FOUND

FetchData 根本不会触发


3️⃣ Windows 无法管理同步状态

这些状态图标:

  • ☁ 云端

  • ✔ 本地

  • ⬇ 正在下载

全部依赖于:

占位符 + CFAPI 状态机

没有占位符,Windows 不知道:

  • 这个文件是不是云文件

  • 是否允许按需下载

  • 是否可以释放本地数据


四、占位符在 CFAPI 中的地位

你可以把占位符理解为:

Cloud Files 的“合同文件”

CFAPI 的工作流程(简化)

远端元数据 ↓ CfCreatePlaceholders ↓ 占位符(NTFS 对象) ↓ 用户访问(CreateFile / Read) ↓ OnFetchData 回调 ↓ 下载真实数据 ↓ CfWriteFile / CfCompleteFetchData

👉没有占位符,就没有后续所有步骤

五、占位符 ≠ 空文件(非常关键)

这是新手最容易犯的错误。

项目占位符空文件
NTFS 对象
文件大小可为真实大小通常为 0
数据是否存在
触发 FetchData
支持按需下载

空文件一旦存在真实数据流,CFAPI 就认为“你已经有数据了”

这也是你之前遇到的:

  • CreateFile 直接触发下载

  • 或不触发 FetchData

  • 或 ERROR_CLOUD_FILE_INVALID_REQUEST

的根本原因之一。


六、为什么 CFAPI 要“先占位、后取数”

1️⃣ 性能

  • 百万级文件列表

  • 秒级展示目录结构

  • 不下载任何内容

2️⃣ 资源管理

  • 磁盘空间可控

  • CfDehydratePlaceholder 可释放本地数据

  • 系统自动回收

3️⃣ 系统一致性

  • Explorer

  • CMD / PowerShell

  • 第三方程序(CreateFile)

全部通过 NTFS 统一入口


七、结合你当前开发中的几个关键点

✔ 为什么只是CreateFile就触发OnFetchData

因为:

  • 打开的是占位符

  • 访问方式涉及数据访问

  • 系统认为需要实体化

✔ 为什么获取句柄要小心 Flags

FILE_FLAG_OPEN_REPARSE_POINT
  • 避免系统认为你要读数据

  • 只是“操作占位符元信息”


✔ 为什么 CfOpenFileWithOplock 很重要

  • 它明确告诉系统:
    这是 Provider 操作,不是用户访问

  • 避免错误触发 FetchData


八、一句话总结

占位符不是“可选项”,而是 CFAPI 的根基。

没有占位符:

  • Explorer 不可见

  • FetchData 不触发

  • 同步状态失效

  • CFAPI 退化成普通文件 IO

九、占位符(Placeholder)操作类

使用 C# .NET8

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0-windows10.0.17763.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageReference Include="Serilog" Version="4.3.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageReference Include="Vanara.PInvoke.CldApi" Version="4.2.1" /> <PackageReference Include="Vanara.PInvoke.SearchApi" Version="4.2.1" /> </ItemGroup> </Project>
using Serilog; using System.ComponentModel; using System.Runtime.InteropServices; using Vanara.PInvoke; using static Vanara.PInvoke.CldApi; using static Vanara.PInvoke.Kernel32; namespace CfapiSync { public class Placeholder { public static bool Create(string fileId , string baseDirectoryPath, string relativeFileName, CF_FS_METADATA fsMetadata) { bool isDirectory = fsMetadata.BasicInfo.FileAttributes.HasFlag(FileFlagsAndAttributes.FILE_ATTRIBUTE_DIRECTORY); var cloudEntry = new CF_PLACEHOLDER_CREATE_INFO { FileIdentity = Marshal.StringToCoTaskMemUni(fileId), FileIdentityLength = (uint)(fileId.Length * Marshal.SizeOf(typeof(char))), RelativeFileName = relativeFileName, FsMetadata = fsMetadata, Flags = CF_PLACEHOLDER_CREATE_FLAGS.CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC, }; if (isDirectory) { cloudEntry.Flags |= CF_PLACEHOLDER_CREATE_FLAGS.CF_PLACEHOLDER_CREATE_FLAG_DISABLE_ON_DEMAND_POPULATION; cloudEntry.FsMetadata.FileSize = 0; } var entitys = new CF_PLACEHOLDER_CREATE_INFO[] { cloudEntry }; var res = CfCreatePlaceholders(baseDirectoryPath, entitys, 1, CF_CREATE_FLAGS.CF_CREATE_FLAG_NONE, out uint entresProcessed); if (res.Succeeded) { Log.Information($"Placeholder-> Create : {relativeFileName} "); return true; } if(res.Code == 183) { Log.Warning($"Placeholder-> Create -> 已经存在 {relativeFileName} "); Convert(fileId, System.IO.Path.Combine(baseDirectoryPath, relativeFileName)); return false; } if (!res.Succeeded) { Log.Error($"Placeholder-> Create : {relativeFileName} 创建失败! {res}"); return false; } return true; } public static bool Convert(string fileId, string filePath) { var hFile = CreateFile(filePath, Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (hFile.IsInvalid) { Log.Warning("Placeholder-> Convert: 文件无效!"); return false; } var fileIdentity = Marshal.StringToCoTaskMemUni(fileId); var fileIdentityLength = (uint)(fileId.Length * Marshal.SizeOf(typeof(char))); // CF_CONVERT_FLAG_NONE 什么都不显示 // CF_CONVERT_FLAG_MARK_IN_SYNC 与云同步 绿色对号, // CF_CONVERT_FLAG_DEHYDRATE 文件操作后 变为云文件 var res = CfConvertToPlaceholder(hFile, fileIdentity, fileIdentityLength, CF_CONVERT_FLAGS.CF_CONVERT_FLAG_MARK_IN_SYNC, out long ConvertUsn, 0); hFile.Dispose(); if (res.Succeeded) { Log.Information($"Placeholder-> Convert : {filePath} "); return true; } else { Log.Error($"Placeholder-> Convert : {filePath} 转换失败! {res}"); return false; } } // 将占位符恢复为常规文件,去除所有特殊特征,例如重分析标记、文件标识等。 public static bool Revert( string filePath) { var hFile = CreateFile(filePath, Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); var res = CfRevertPlaceholder(hFile, CF_REVERT_FLAGS.CF_REVERT_FLAG_NONE,0); if (res.Succeeded) { Log.Information($"Placeholder-> Revert : {filePath} "); } else { Log.Error($"Placeholder-> Revert : {filePath} 转换失败! {res}"); } return true; } public static bool Update(string filePath, CF_FS_METADATA fsMetadata) { var hFile = CreateFile(filePath, Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); // HRESULT hr = CfOpenFileWithOplock(filePath,CF_OPEN_FILE_FLAGS.CF_OPEN_FILE_FLAG_DELETE_ACCESS, out SafeHCFFILE hFile); //if (hr.Failed) //{ // Log.Error($"Placeholder-> Update : {filePath} 更新失败!"); // return false; //} IntPtr infoBuffer = IntPtr.Zero; try { CF_PLACEHOLDER_INFO_CLASS infoClass = CF_PLACEHOLDER_INFO_CLASS.CF_PLACEHOLDER_INFO_STANDARD; uint bufferLength = (uint)Marshal.SizeOf<CF_PLACEHOLDER_STANDARD_INFO>() + 4096; infoBuffer = Marshal.AllocHGlobal((int)bufferLength); var fileHandle = hFile.DangerousGetHandle(); var hr = CfGetPlaceholderInfo( fileHandle, infoClass, infoBuffer, bufferLength, out uint returnedLength); if (hr.Failed) { Log.Error($"Placeholder-> Update : {filePath} 更新失败!"); return false; } var standardInfo = Marshal.PtrToStructure<CF_PLACEHOLDER_STANDARD_INFO>(infoBuffer); // 2. FileIdentity 必须有效 if (standardInfo.FileIdentity == null || standardInfo.FileIdentity.Length == 0 || standardInfo.FileIdentityLength == 0) { throw new InvalidOperationException("Invalid FileIdentity."); } GCHandle handle = GCHandle.Alloc(standardInfo.FileIdentity, GCHandleType.Pinned); try { IntPtr fileIdentityPtr = handle.AddrOfPinnedObject(); System.Int64 usn =0; hr = CfUpdatePlaceholder( fileHandle, // 句柄无效 fsMetadata, fileIdentityPtr, standardInfo.FileIdentityLength, null, 0, CF_UPDATE_FLAGS.CF_UPDATE_FLAG_VERIFY_IN_SYNC, ref usn ); if (hr.Failed) { Log.Error($"Placeholder-> Update : {filePath} 更新失败!{hr}"); return false; } } finally { handle.Free(); } Log.Information($"Placeholder-> Update : {filePath}"); return true; } finally { if (infoBuffer != IntPtr.Zero) Marshal.FreeHGlobal(infoBuffer); hFile.Dispose(); } } /// <summary> /// 判断路径否是 Cloud Placeholder,以及它的脱水与同步状态 /// </summary> public static CF_PLACEHOLDER_STATE GetStatus(string filePath, out bool isPlaceholder, out bool isDehydrated ,out bool isSynced ) { isPlaceholder = false; isDehydrated = false; isSynced = false; var hFile = CreateFile( filePath, 0, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (hFile.IsInvalid) { Log.Error($"Placeholder-> GetStatus : {filePath},文件无效!"); return CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_INVALID; } try { int size = Marshal.SizeOf<FILE_ATTRIBUTE_TAG_INFO>(); IntPtr infoBuffer = Marshal.AllocHGlobal(size); try { bool ok = GetFileInformationByHandleEx(hFile, FILE_INFO_BY_HANDLE_CLASS.FileAttributeTagInfo, infoBuffer, (uint)size); if (!ok) { Log.Error($"Placeholder-> GetStatus -> GetFileInformationByHandleEx -> {filePath} !"); return CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_INVALID; } CF_PLACEHOLDER_STATE state = CfGetPlaceholderStateFromFileInfo(infoBuffer, FILE_INFO_BY_HANDLE_CLASS.FileAttributeTagInfo); if (state == CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_INVALID) throw new InvalidOperationException("Invalid placeholder state"); isPlaceholder = (state & CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_PLACEHOLDER) != 0; isDehydrated = (state & CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_PLACEHOLDER) != 0 && (state & CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_PARTIALLY_ON_DISK) != 0; isSynced = (state & CF_PLACEHOLDER_STATE.CF_PLACEHOLDER_STATE_IN_SYNC) != 0; Log.Information($"Placeholder-> GetStatus : {filePath},state->{state}!"); return state; } finally { Marshal.FreeHGlobal(infoBuffer); } } finally { hFile.Dispose(); } } /// <summary> /// 设置占位符文件或文件夹的同步状态。 /// </summary> /// <param name="filePath">文件地址</param> /// <param name="syncOngoing">同步进行中</param> /// <returns></returns> public static bool SetStatus(string filePath, bool syncOngoing) { var hFile = CreateFile( filePath, 0, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (hFile.IsInvalid) { Log.Error($"Placeholder-> GetStatus : {filePath},文件无效!"); return false; } CF_IN_SYNC_STATE inSyncState = syncOngoing ? CF_IN_SYNC_STATE.CF_IN_SYNC_STATE_IN_SYNC : CF_IN_SYNC_STATE.CF_IN_SYNC_STATE_NOT_IN_SYNC; CF_SET_IN_SYNC_FLAGS inSyncFlags = CF_SET_IN_SYNC_FLAGS.CF_SET_IN_SYNC_FLAG_NONE; long inSyncUsn = 0; var res = CfSetInSyncState(hFile, inSyncState, inSyncFlags, ref inSyncUsn); hFile.Dispose(); if (res.Succeeded) { Log.Information($"Placeholder-> SetStatus : {filePath},state->{syncOngoing}!"); return true; } else { Log.Error($"Placeholder-> SetStatus : {filePath},state->{syncOngoing}!"); return false; } } /// <summary> /// 获取占位符信息 /// </summary> public static CF_PLACEHOLDER_STANDARD_INFO GetInfo( string filePath) { var hFile = CreateFile( filePath, 0, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT | FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (hFile.IsInvalid) { Log.Error($"Placeholder-> GetInfo : {filePath},文件无效!"); throw new Win32Exception(Marshal.GetLastWin32Error()); } try { var infoClass = CF_PLACEHOLDER_INFO_CLASS.CF_PLACEHOLDER_INFO_STANDARD; var length = 4096; var buffer = Marshal.AllocHGlobal( length); // 正式获取信息 HRESULT hr = CfGetPlaceholderInfo( hFile, infoClass, buffer, (uint)length, out uint returnedLength); if (hr.Failed) { Log.Error($"Placeholder-> GetInfo : 获取失败 {hr}"); Marshal.ThrowExceptionForHR(hr.Code); } var info = Marshal.PtrToStructure<CF_PLACEHOLDER_STANDARD_INFO>(buffer); Log.Information($"Placeholder-> GetInfo : 获取成功{info}"); Marshal.FreeHGlobal(buffer); return info; } finally { hFile.Dispose(); } } //CF_PIN_STATE_PINNED 固定文件,用户本地一定有完整文件内容 //CF_PIN_STATE_UNPINNED 取消固定,文件不需要始终保留本地内容,可以节省磁盘空间 //CF_PIN_STATE_EXCLUDED 排除同步,占位符永远不被同步到云端 //CF_PIN_STATE_INHERIT 继承, 状态继承自父目录 //CF_PIN_STATE_UNSPECIFIED 未指定, 系统自由管理占位符内容 public static bool SetPinState(string path, CF_PIN_STATE state) { var hFile = CreateFile(path, Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE, FileShare.ReadWrite | FileShare.Delete, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_OPEN_REPARSE_POINT |FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (hFile.IsInvalid) { Console.WriteLine("Failed to open file."); return false; } var res = CfSetPinState(hFile, state, CF_SET_PIN_FLAGS.CF_SET_PIN_FLAG_NONE, IntPtr.Zero); hFile.Dispose(); if (res.Succeeded) { Log.Information($"Placeholder-> SetPinState({state}) : {path} "); return true; } else { Log.Warning($"Placeholder-> SetPinState({state}) : {path} , 错误 {res} "); return false; } } } }

Create —— 创建占位符

根据远端文件或目录的元数据,在本地 NTFS 中创建一个云文件占位符。
创建后文件立即可见但不包含实际数据,不占用磁盘空间。


Convert —— 转换为占位符

将一个已存在的普通文件或目录转换为云同步占位符。
常用于接管本地已有文件,使其纳入云同步体系。


Revert —— 恢复为普通文件

移除文件的云占位符特性,将其还原为标准 NTFS 文件或目录。
用于解除云同步绑定或回滚云文件状态。


Update —— 更新占位符元数据

更新占位符的文件大小、时间戳、属性等元数据信息。
用于同步远端变更,不涉及文件数据读写。


GetStatus —— 获取占位符状态

判断指定路径是否为云占位符,并获取其实体化与同步状态。
用于决定是否需要下载、脱水或更新同步标识。


SetStatus —— 设置同步状态

显式设置占位符的“已同步 / 未同步”状态。
用于控制 Explorer 中的同步完成图标显示。


GetInfo —— 获取占位符详细信息

读取占位符的标准 CFAPI 信息结构,包括标识、状态和标志位。
主要用于调试、审计或高级同步逻辑判断。


SetPinState —— 固定 / 取消固定文件

设置占位符文件的固定策略,控制是否必须常驻本地数据。
用于支持“始终保留本地副本”或“按需下载”的用户行为。

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

14、Linux与Windows环境下NFS和NIS的使用指南

Linux与Windows环境下NFS和NIS的使用指南 1. NFS协议概述 NFS(Network File System)是原生的UNIX协议,允许UNIX机器通过网络共享驱动器,它与微软的SMB协议有部分功能相似,但更为简单,不包含认证和打印功能。认证由UNIX(或Linux)主机处理,打印功能由lpr和lpd处理。 …

作者头像 李华
网站建设 2026/8/5 4:36:55

15、Linux与Windows系统集成:NIS、FTP及Telnet配置指南

Linux与Windows系统集成:NIS、FTP及Telnet配置指南 在当今的网络环境中,Linux和Windows系统的集成是一个常见且重要的需求。本文将详细介绍NIS(网络信息服务)、FTP(文件传输协议)和Telnet在Linux和Windows系统中的配置与使用,帮助你更好地实现系统间的协同工作。 NIS相…

作者头像 李华
网站建设 2026/8/5 4:30:13

提升团队协作效率:用LobeChat搭建统一AI助手平台

提升团队协作效率&#xff1a;用LobeChat搭建统一AI助手平台 在企业加速智能化转型的今天&#xff0c;AI已经不再是实验室里的“黑科技”&#xff0c;而是真正走进了日常办公场景。越来越多的团队开始尝试使用大语言模型&#xff08;LLM&#xff09;辅助写作、编程、客户服务和…

作者头像 李华
网站建设 2026/8/5 7:13:08

应用层|低空应用安全的 “精工锻造者”,中科数测以多工具矩阵赋能应用从开发到运维的全周期安全

从无人系统管理平台的精准调度&#xff0c;到空中交通管制系统的高效指挥&#xff0c;再到低空飞行监控系统的实时预警&#xff0c;应用层是低空经济价值交付的“终端窗口”&#xff0c;其安全直接决定了用户体验的优劣与业务价值的最终实现。中科数测整合固件检测工具、协议模…

作者头像 李华
网站建设 2026/8/6 15:23:01

横观水力压裂模型:从 PDE 建模到 Comsol 模拟

横观水力压裂模型 pde建模 横观各向同性介质水力压裂裂纹扩展模型 使用comsol软件实现相场法模拟裂纹扩展 均基于断裂力学理论 模拟单边拉裂纹受拉伸荷载作用和受剪切荷载作用 考虑初始地应力场作用下裂纹扩展模拟 瞬态水力压裂裂隙扩展 包括文章和模型在地质工程领域&#xff…

作者头像 李华
网站建设 2026/8/4 9:50:44

值得关注的人形机器人公司盘点,智元AGIBOT以卓越实力登顶

随着AI大模型与柔性驱动技术的深度融合&#xff0c;人形机器人正逐渐走向规模商业化&#xff0c;在服务、工业、文娱等场景实现阵阵落地。当前行业呈现“技术智能化、场景多元化、生态一体化”三大趋势&#xff0c;一批具备核心技术与落地能力的企业脱颖而出&#xff0c;以下5 …

作者头像 李华