新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android 11 USB权限默认授予:系统级管家与AOSP框架改造实践

发布时间:2026/9/29 9:33:47来源:尧图网络
Android 11 USB权限默认授予:系统级管家与AOSP框架改造实践
上周帮朋友收拾一批自助终端设备机器上跑着一个自研的第三方应用负责和读卡器通信。麻烦的地方在于每次断电重启或者读卡器拔下来再插回去屏幕上就会弹出一个是否允许该应用访问此 USB 设备的对话框。现场没人值守对话框杵在那儿没人点业务就卡死了。类似的问题在工控一体机、医疗终端、车载设备、门禁闸机上都出现过本质上是一个问题——Android 11 默认授予第三方应用 USB 权限这件事能不能做到、怎么做才稳、做完之后会不会有副作用。这篇东西就是把这套流程从头到尾说清楚的。我会先讲清楚 USB 权限在系统里到底是怎么存的、谁在管然后给出两条落地路线一条是不动固件的系统级授权管家方案一条是直接改框架代码的定制固件方案。两条路我都实际趟过各有各的适用边界。内容偏工程实操需要你会用 adb、能看懂一点 AOSP 的 Java 代码最好是已经在做设备定制或者行业应用交付的同行。如果你只是想给自己的应用申请一下 USB 权限然后弹窗确认那用不上这些老老实实调requestPermission就行。1. 从每次插拔都要点一次允许说起需求与整体思路1.1 这个需求到底在解决什么问题先把场景拆干净。Android 的 USB Host 模式下一个应用想要跟外接的 USB 设备读卡器、打印机、串口模块、HID 采集器通信得先拿到一个东西官方叫USB 设备访问权限。注意它和运行时权限比如相机、存储不是一回事不走ActivityCompat.requestPermissions那套流程而是由UsbManager单独管理的一套机制授权结果存在系统服务里跟着设备和包名走。默认行为是这样的应用检测到设备插入调用UsbManager.requestPermission(device, pendingIntent)系统弹一个对话框问用户允许吗用户点了允许权限才会落到UsbService的权限表里之后应用调用openDevice()才能拿到UsbDeviceConnection。问题是这个授权不是永久有效的——它绑定在具体的UsbDevice对象上而设备重新插拔之后/dev/bus/usb/001/002这种节点路径会变系统拿新的设备对象去查权限表查不到于是又弹一次。所以默认授予要解决的其实是两个动作第一次授予以及后续每次设备拓扑变化后的重新授予。只做前者不做后者现场照样出问题这一点很多人第一次做的时候会漏。1.2 三条技术路线的取舍对比我见过的做法大致三类各有各的代价。第一类是改应用让应用自己处理弹窗。听着最简单但根本解决不了无人值守的问题弹窗还是得有人点。而且如果设备上有多个应用要用同一个 USB 设备每个都得弹一次体验一样差。第二类是做一个系统级的授权管家应用持有MANAGE_USB权限驻留在后台监听设备插拔主动给白名单里的包授予权限。这条路不改框架升级系统只要重新验证一下就行适合已经量产、不方便刷机的设备。代价是应用必须进/system/priv-app得有固件的签名或者写入权限。第三类是直接改 AOSP 框架代码在UsbService或者权限管理类里动手脚让某些包直接跳过对话框。这条路最彻底但每换一个 Android 大版本就要重新适配一次类名和方法签名都可能在版本之间漂移。1.3 动手之前必须想清楚的三件事第一件是授权范围。不要图省事写成所有第三方应用默认全部授权。USB 设备能做的事情不少随随便便放开等于把外设的控制权交给任意一个装进来的应用。我一般建议维护一份包名白名单写死在配置里或者放在一个只读的系统属性里另外可以按 vendorId / productId 做二次过滤只给特定厂商的设备开绿灯。第二件是多用户。Android 的 USB 权限是按userId维度存的你给 user 0 授权切到访客用户或者另一个工作资料里照样要重新来一遍。行业设备很多是单用户但车机、教育一体机这类场景要注意得遍历一下当前活跃用户。第三件是可观测性。授权这事儿如果失败了现场人员是看不出原因的只会跟你反馈读卡器没反应。所以从第一天起就要有日志把发现设备—尝试授权—授权结果—目标应用是否成功打开这条链路打出来后面排查能省掉一大半时间。2. USB 权限机制的底层逻辑拆解2.1 从 UsbManager 到 UsbService 的调用链理解调用链是后面所有改动的根基。应用侧拿到的UsbManager其实是个壳所有操作都通过 Binder 转发到系统进程里的UsbService。权限相关的几个入口大致是这样// 应用侧 UsbManager manager (UsbManager) getSystemService(Context.USB_SERVICE); boolean has manager.hasPermission(device); // 查询 manager.requestPermission(device, pendingIntent); // 申请可能弹窗 manager.grantPermission(device, packageName); // 系统 API直接授予requestPermission会走到UsbService的requestDevicePermission再由权限管理模块决定是弹对话框还是直接授。grantPermission就更直接绕过了整个申请流程直接把权限写进权限表——这也是为什么它被标记成SystemApi并且需要MANAGE_USB保护权限普通应用拿不到。这里有个容易踩坑的点grantPermission成功后目标应用不会收到任何通知。它如果正在等ACTION_USB_PERMISSION那个广播那它会一直等下去。所以管家应用在授权之后最好再给目标应用发一个自定义广播或者目标应用干脆改成轮询hasPermission的方式来判断。2.2 权限是怎么被记住的持久化存储的位置与结构Android 11 里权限表在frameworks/base/services/usb/java/com/android/server/usb/下面的权限管理类里维护核心结构是一个以UsbDevice为 key、以SparseArrayPackagePermissions为 value 的 HashMap内层的SparseArray用userId做索引PackagePermissions里存这一组包名。注意UsbDevice作为 key 这件事。它的hashCode和equals是基于设备名、vendorId、productId、设备类等信息算出来的设备名里带着总线号和设备号拔插一次就变了。所以前面说的重新插拔后权限失效根因就在这儿——不是系统把权限删了是新的设备对象在表里根本找不到对应的条目。这个设计决定了默认授权必须做成事件驱动的监听ACTION_USB_DEVICE_ATTACHED每次设备出现就重新授一遍。想靠一次性写死来一劳永逸在 USB 这个场景下是行不通的。顺带提一句查询工具验证权限表最直接的办法就是adb shell dumpsys usb这个命令会把当前设备列表、每个设备允许的包名、当前 USB 功能状态全打出来。改完代码之后拿它跟预期对一下比翻日志快得多。2.3 Android 11 带来的变化与兼容影响Android 11 在这个模块上做了拆分把权限的存储和申请流程分到了不同的类里代码结构比 Android 10 清楚不少。这也意味着如果你手里有 Android 10 时代的补丁直接往 11 上搬大概率编译不过得先看清楚类和方法去了哪里。到了 Android 12 及以后这套代码又做了一轮重构权限管理的类名进一步调整方法签名也跟着变。我自己的经验是不要记类名记关键词。在源码根目录下敲grep -rn hasPermission frameworks/base/services/usb/ grep -rn grantDevicePermission frameworks/base/services/usb/按方法名定位比按类名定位靠谱得多跨版本都适用。另一个变化是隐式广播的限制。Android 8.0 之后大部分隐式广播不能在 Manifest 里静态注册USB 设备插拔广播属于少数豁免的但豁免不等于稳——某些定制固件会把它一起收掉。所以我的做法是动态注册为主、静态注册兜底两个都留着。3. 通过系统级应用实现默认授予推荐方案3.1 让应用持有 MANAGE_USB 权限这条路的前提是你的授权管家应用得能被系统认可。android.permission.MANAGE_USB的保护级别是signature|privileged意味着要么用平台签名签要么作为特权应用装进/system/priv-app并且进白名单。行业设备一般走第二条因为平台签名通常不好拿。应用得作为系统应用编译进固件最简单的做法是在设备源码里建一个目录把它加进PRODUCT_PACKAGES# device/vendor/product/device.mk PRODUCT_PACKAGES UsbGrantor \ privapp-permissions-usbgrantor.xml白名单文件是关键Android 9 之后特权应用申请白名单外的权限会直接拒绝启动日志里报Privileged permission ... not in privapp-permissions whitelist!-- /system/etc/permissions/privapp-permissions-usbgrantor.xml -- permissions privapp-permissions packagecom.example.usbgrantor permission nameandroid.permission.MANAGE_USB / permission nameandroid.permission.QUERY_ALL_PACKAGES / /privapp-permissions /permissionsQUERY_ALL_PACKAGES不是必须的但如果你要根据包名查应用信息做校验加上会省事。这个文件必须和 APK 一起进 system 分区临时用adb push的话记得改权限并且重启生效adb shell stop adb shell start也行不用整机重启。3.2 监听设备插拔并主动授权应用的 Manifest 里声明 USB Host 特性然后动态注册插拔广播manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.usbgrantor uses-permission android:nameandroid.permission.MANAGE_USB / uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / application android:allowBackupfalse android:persistenttrue android:labelUsbGrantor receiver android:name.BootReceiver android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / /intent-filter /receiver /application /manifestandroid:persistenttrue只有系统应用能用作用是让进程常驻被杀了系统会自动拉起来。对于无人值守设备这个属性很有价值比你自己写守护逻辑稳。服务里的核心逻辑分两块启动时全量扫一遍之后靠广播增量处理。public class UsbGrantService extends Service { private static final String TAG UsbGrantor; private static final String[] WHITELIST { com.example.reader, com.example.printer }; private static final int VENDOR_ID 0x1A86; // 只给指定厂商的设备放行 private UsbManager mUsbManager; private final BroadcastReceiver mReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device null) return; if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { Log.i(TAG, attached: device.getDeviceName()); grantFor(device); } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { Log.i(TAG, detached: device.getDeviceName()); } } }; Override public void onCreate() { super.onCreate(); mUsbManager (UsbManager) getSystemService(Context.USB_SERVICE); IntentFilter filter new IntentFilter(); filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); } else { registerReceiver(mReceiver, filter); } } Override public int onStartCommand(Intent intent, int flags, int startId) { // 冷启动或者重启后兜底扫一遍 for (UsbDevice device : mUsbManager.getDeviceList().values()) { grantFor(device); } return START_STICKY; } private void grantFor(UsbDevice device) { if (device.getVendorId() ! VENDOR_ID) { Log.i(TAG, skip vendor device.getVendorId()); return; } for (String pkg : WHITELIST) { try { mUsbManager.grantPermission(device, pkg); Log.i(TAG, grant ok: device.getDeviceName() - pkg); } catch (SecurityException e) { Log.e(TAG, grant failed for pkg, e); } } } }Android 14 之后动态注册广播强制要求指定导出标志代码里那段Build.VERSION.SDK_INT判断就是为了兼容这个变化别省省了在新系统上直接崩。3.3 完整代码实现中的几个细节上面这段是骨架实际跑起来还有几个地方要补。第一个是授权失败的返回值。grantPermission是个 void 方法它不告诉你成功还是失败。想知道结果得在调用之后用mUsbManager.hasPermission(device)复查一遍。我在实际项目里会给每个包做一次复查失败的重试两次还失败就打到日志里并且上报。别小看这一步某些设备在插入瞬间的枚举还没完成第一次授权是有可能落空的。第二个是通知目标应用。前面说过被授权的应用不会收到系统广播。如果目标应用是你自己写的最简单的做法是在onResume或者onStartCommand里主动查一次hasPermission而不是死等广播。如果是第三方应用改不了那就只能靠它自己的重试逻辑或者你在管家应用里给它发一个自定义的Intent前提是双方约定好。第三个是应用列表缓存。如果你的白名单不是硬编码而是从服务端下发的注意PackageManager查询在启动阶段有开销别在主线程里反复查。3.4 编译与部署验证把应用放进packages/apps/UsbGrantor/写一个 Android.mk 或者 Android.bp# Android.mk LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : UsbGrantor LOCAL_SRC_FILES : $(LOCAL_MODULE).apk LOCAL_MODULE_CLASS : APPS LOCAL_MODULE_TAGS : optional LOCAL_PRIVILEGED_MODULE : true LOCAL_CERTIFICATE : platform LOCAL_DEX_PREOPT : false include $(BUILD_PREBUILT)编译刷机之后用这几条命令验证# 看应用有没有起来 adb shell ps -A | grep usbgrantor # 看权限有没有拿到 adb shell dumpsys package com.example.usbgrantor | grep -A2 MANAGE_USB # 看 USB 权限表 adb shell dumpsys usb # 看日志 adb logcat -s UsbGrantor:V UsbService:Vdumpsys usb的输出里如果能看到目标包名挂在设备下面基本就成了。如果dumpsys package里压根没有MANAGE_USB这一项那就是白名单文件没生效回头检查文件路径和权限。注意LOCAL_CERTIFICATE : platform要求你的构建环境里有平台密钥。没有的话就改成PRESIGNED但那样拿不到signature级别的权限只能靠privileged这条路白名单文件就更不能出错。4. 修改系统框架层的做法定制固件场景4.1 定位关键类与方法如果你手里就是固件源码改框架其实比做管家应用更省事——不用多维护一个 APK也不用考虑进程存活。缺点前面说过版本升级要重新对齐。第一步永远是在源码根目录 grep别凭记忆找类名cd frameworks/base/services/usb grep -rn class UsbUserSettingsManager\|class UsbPermissionManager\|class UsbUserPermissionManager . grep -rn boolean hasPermission . grep -rn grantDevicePermission .Android 11 上你大概率会找到权限存储的实现类和权限申请流程的实现类两者分开。Android 12 之后可能合并成一个。找到之后先别急着改通读一遍hasPermission和requestPermission的调用方搞清楚谁在什么地方用到它们。4.2 两种改法与差异改法一是存储层放行也就是在hasPermission里加白名单判断命中就直接返回truepublic boolean hasPermission(UsbDevice device, String packageName, int userId) { if (packageName ! null isInWhitelist(packageName)) { return true; } // 原始逻辑不动 synchronized (mLock) { SparseArrayPackagePermissions permissions mPermissions.get(device); if (permissions null) return false; PackagePermissions packagePermissions permissions.get(userId); if (packagePermissions null) return false; return packagePermissions.packageNames.contains(packageName); } } private boolean isInWhitelist(String pkg) { return com.example.reader.equals(pkg) || com.example.printer.equals(pkg); }这种改法的好处是彻底任何路径过来的查询都放行应用重启、设备重插都不受影响。坏处是权限表本身还是空的某些依赖getPermissionGrantedDevices()做展示的地方会出现明明能用但列表里没有的割裂现象测试的时候容易被误判成 bug。改法二是申请层直接授在requestPermission的处理流程里遇到白名单包就不走弹窗直接调授予逻辑。这种改法保留了权限表的真实性dumpsys usb的输出和实际状态一致代价是要找到正确的位置而且得保证直接授这一步不会因为设备还没枚举完而失败。我两个都用过最后倾向于第二种理由是状态一致对后期排查太重要了。为了省几行代码导致的看起来没授权其实能用在交付现场会带来很多无谓的沟通成本。4.3 权限对话框禁用开关AOSP 里其实留过一个全局开关的思路——让权限申请流程判断某个设置项如果被打开就跳过对话框直接授。不同分支上这个设置项的命名和读取方式可能不一样可以这样确认grep -rn PERMISSION_DIALOG frameworks/base/core/java/android/provider/Settings.java grep -rn PERMISSION_DIALOG frameworks/base/services/usb/如果确认存在对应项那么临时验证的时候可以直接用 adb 打开adb shell settings put secure 对应的key 1 adb shell settings get secure 对应的key这个办法的好处是不用重新编译刷完机调一下就能验证整条链路通不通特别适合前期做可行性验证。正式发布的时候再把它固化成默认值或者写进overlay里。要提醒的是这个开关一旦打开是全局生效的等于把所有 USB 权限对话框都关了所以它只适合这台设备上装了哪些应用我完全可控的场景。提示改框架之后一定要跑一遍adb shell dumpsys usb和完整插拔循环。我曾经遇到过一次改动看起来生效了实际上是因为前一次测试的权限残留清掉重来才发现新设备根本没授上。5. 常见问题与排查技巧实录5.1 权限授予成功但应用仍报无权限这是问得最多的一个。日志上grantPermission没抛异常dumpsys usb里也能看到包名但目标应用就是拿到null的UsbDeviceConnection。按我的经验八成是下面几个原因之一。第一个原因是设备对象不匹配。目标应用手里持有的UsbDevice是很久之前从getDeviceList()拿的旧对象而设备中间拔插过deviceName已经变了。系统用新的设备查权限表当然查得到但应用拿旧的设备去openDevice路径都对不上。解决办法是让目标应用每次打开之前重新getDeviceList()按 vendorId / productId 反查一遍。第二个原因是 vendorId 或者接口配置不对。有些复合设备有多个接口应用声明要打开的那个接口在UsbInterface列表里位置变了claimInterface就失败。打印一下device.getInterfaceCount()和每个接口的getId()对比看看。第三个原因是设备节点权限。这种情况日志里通常会有avc: denied相关的记录adb shell dmesg | grep avc或者adb logcat | grep avc能看到。碰上了就得往 SELinux 策略上想改file_contexts或者加allow规则。5.2 高频问题速查表把上面这些和实际踩过的坑整理成表排查的时候按现象查现象可能原因排查命令处理方式grantPermission 抛 SecurityException应用没有 MANAGE_USBdumpsys package pkg | grep MANAGE_USB检查 privapp-permissions 白名单文件路径与内容授权成功但 openDevice 返回 null设备对象过期或接口不匹配打印device.getDeviceName()对比重新获取设备列表核对接口重启后权限丢失设备 deviceName 变化dumpsys usb前后对比监听 ATTACHED 广播重新授予应用启动即崩溃日志报特权权限priv-app 白名单缺失logcat | grep privapp补全白名单 XML 并重新刷入切用户后失效权限按 userId 存储切换用户后dumpsys usb对每个活跃 userId 分别授予插拔后目标应用长时间无响应应用在死等权限广播观察应用日志应用侧改为主动轮询 hasPermission设备根本不出现在列表内核驱动或 OTG 供电问题ls /dev/bus/usb/检查 dts 配置与供电电流5.3 版本升级与定制终端的兼容坑跨版本升级是另一个高频翻车点。Android 12 之后这套代码做过重构方法名字、参数个数都可能变。我的建议是每次升级大版本先把定位类和方法的 grep 命令跑一遍确认调用关系再决定改哪里。别指望一个 patch 能用三个大版本。另外一类坑来自定制终端。一些运营商定制的机顶盒类固件会额外收紧第三方应用的入口比如把安装渠道关掉、限制非系统签名的应用启动等等。这里要强调的是这类限制和 USB 权限完全是两码事分别在不同的模块里千万别混为一谈。如果你的应用根本装不上去或者起不来那先解决启动的问题再谈 USB 授权。反过来如果应用能跑但拿不到 USB 权限那就老老实实回到UsbService这条链路上查。还有一个容易忽略的是 Android 12 之后对包可见性的收紧。如果你的白名单逻辑里要用PackageManager.getPackageInfo去校验目标应用是否存在没有声明queries或者QUERY_ALL_PACKAGES的话会查不到误判成应用未安装。这个坑我踩过一次排查了小半天。6. 工程化落地的一些补充经验6.1 白名单怎么管才不容易出事硬编码在代码里最省事但每次调整都要重新编译刷机量产的机器更麻烦。折中方案是放在一个只读的系统属性里比如ro.usb.grant.whitelist编译期写进build.prop运行时读出来解析。属性是只读的应用改不了安全性够用改起来也只需要刷一个 prop 文件。再复杂一点就是服务端下发应用定期拉取配置。这条路灵活但引入了网络依赖离线设备就废了。我一般只在设备本身有联网能力、且有明确的远程运维需求时才用。不管用哪种建议同时支持 vendorId / productId 过滤。光靠包名白名单等于告诉系统这个应用可以用任何 USB 设备范围还是太大。加上设备指纹之后攻击面能小不少。6.2 日志和可观测性怎么做前面反复提到日志具体打什么我的习惯是四个点设备插入时的完整描述deviceName、vendorId、productId、接口数、授权前后的hasPermission结果、授权调用的耗时、目标应用是否在预期时间内成功打开设备。把这四个点串起来现场一出问题就能定位到是哪一环断的。日志级别用Log.i别用Log.d很多量产固件默认把 debug 级别关掉了。同时做好日志轮转USB 插拔频繁的设备一天能刷出几百条不做限制会把存储写满。如果设备支持远程日志上报把授权失败单独打一个 tag方便做告警。授权失败的次数居高不下往往意味着硬件层面有问题接触不良、供电不足这时候从软件侧是查不出东西来的。6.3 回滚与灰度这套改动一旦上线影响面是设备上所有依赖 USB 外设的业务。所以我的做法是第一阶段只开日志不实际授权观察一到两周确认白名单里的包名和实际用到的设备都对得上第二阶段开授权但保留对话框兜底逻辑第三阶段才完全关掉弹窗。中间任何一步发现异常把系统属性或者设置项改回去就行不用回刷固件这一点在方案设计的时候就要考虑到。我见过一个项目把白名单硬编码进 smali 里出问题的时候只能整批回刷代价很大。另外提醒一句如果你的设备上有 OTA 能力一定要注意 OTA 之后priv-app目录下的应用会不会被覆盖掉。有些 OTA 包的差分包策略比较激进会把非标准路径下的应用清理掉导致授权功能在某个版本之后突然失效。上线前在自己的 OTA 流程里跑一遍完整回归。我个人在几个项目里用下来系统级管家应用这条路更适合已经量产的设备改动小、回滚快框架层改造适合还在研发阶段、后续要出定制固件的场景省一个常驻进程资源占用更干净。选哪条路先看你能不能拿到固件写入权限和平台签名这个前提决定了后面所有事。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

英飞凌TC3xx SOTA升级:SWAP机制与UCB配置详解 2026/9/29 20:20:59

英飞凌TC3xx SOTA升级:SWAP机制与UCB配置详解

做汽车嵌入式开发的兄弟,几乎没有人没听过SOTA这个名字——整车OTA、固件远程升级,这几年已经是智能汽车的基本功。但真正在英飞凌TC3xx上把SOTA落地,你会发现难点根本不在网络传输、也不在文件解析,而在芯片本身的启动和存储机制…

阅读更多 →
Argent 视觉回归测试指南:OCR 截图对比如何揪出每一个意外 UI 变化 2026/9/29 20:20:53

Argent 视觉回归测试指南:OCR 截图对比如何揪出每一个意外 UI 变化

Argent 视觉回归测试指南:OCR 截图对比如何揪出每一个意外 UI 变化 【免费下载链接】argent An agentic toolkit to control, debug, and profile iOS and Android apps. Made by Software Mansion. 项目地址: https://gitcode.com/gh_mirrors/arg/argent Ar…

阅读更多 →
《微服务架构设计模式》 第八章读书笔记:外部API模式 2026/9/29 20:20:53

《微服务架构设计模式》 第八章读书笔记:外部API模式

标签:微服务 API网关 API Gateway BFF Spring Cloud Gateway GraphQL 一句话:微服务拆分后不能直接对外暴露细粒度服务接口;API Gateway/BFF 作为统一入口,解决多客户端、网络差异、协议转换、多服务数据聚合问题。前言单体应用对…

阅读更多 →
菜鸟教程:2026年OpenClaw(Clawdbot)搭建及指导——TaoToken统一Key接入与config.toml配置骨架 2026/9/29 20:20:53

菜鸟教程:2026年OpenClaw(Clawdbot)搭建及指导——TaoToken统一Key接入与config.toml配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
ChatGPT 又降智了?这次你可能都察觉不到:用 TaoToken 统一 Key 给 GPT-4o/o3 做一次可复现的模型路由体检 2026/9/29 20:20:53

ChatGPT 又降智了?这次你可能都察觉不到:用 TaoToken 统一 Key 给 GPT-4o/o3 做一次可复现的模型路由体检

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
VSCode 里 Rust 路径显示异常?用 TaoToken 统一 Key 排查配置链路 2026/9/29 20:20:52

VSCode 里 Rust 路径显示异常?用 TaoToken 统一 Key 排查配置链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉