Undici 跨端三方库仓颉适配全流程

本文记录 Undici 8.11.2 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端 HTTP 三方库,并在本地终端中通过真实 Node.js 会话和本地 TCP、TLS、HTTP/2、WebSocket、SSE 服务验证原库公开能力。由于 Undici 本身是 Node.js HTTP 客户端,本次适配采用仓颉启动 Node.js 子进程的桥接方案,保留当前仓库 index.js 的完整实现,同时让仓颉调用方获得稳定的类型化入口和可观察的运行结果。

项目地址: AtomGit/nutpi_cj/undici

上游仓库: nodejs/undici

开发工具: 华为云码道

一、背景

1.1 为什么适配 Undici

Undici 是 Node.js 生态中面向 HTTP/1.1 的底层客户端,当前版本同时提供 request、fetch、连接池、代理、HTTP/2、WebSocket、EventSource、Mock、缓存和拦截器等能力。JavaScript 版本依赖动态 options、Promise、ReadableStream、Buffer、Node.js socket 以及大量回调对象,而仓颉强调静态类型、显式异常和由语言标准库提供的进程与网络能力。直接逐行翻译 Undici 源码,无法保留这些运行时对象的身份和异步生命周期。

本项目没有把 Undici 的 Node.js 协议栈伪装成纯仓颉实现,而是把仓颉调用边界设计成 Undici、UndiciResult 和 UndiciError。仓颉负责配置会话、传输程序源码和参数、收集标准输出与标准错误;Node.js 会话加载当前仓库的 index.js,因此 request、fetch 和所有高级 API 都由正在适配的原库实际执行。

1.2 跨端三方库的适配边界

当前项目提供的是仓颉跨端包,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:

边界 仓颉实现
包管理 cjpm.toml、cjpm.lock、cjpm check 和静态库输出
调用入口 Undici、run、runSession、request、fetch、exports 和 demonstrate
进程传输 std.process 启动 Node.js,程序通过标准输入传输,参数通过参数数组传输,不经过 shell
原库加载 校验 package.json 为当前 Undici 仓库,并由本地 require 加载 index.js
HTTP 能力 在 Node.js 会话中真实执行 HTTP/1.1、HTTPS、HTTP/2、连接池、代理和请求 API
Web 能力 在 Node.js 会话中真实执行 WebSocket、WebSocketStream、EventSource、Headers、Response 和 FormData
高级能力 在 Node.js 会话中真实执行 8 个拦截器、两种缓存存储、Mock、快照和 Cookie API
错误模型 UndiciError 保留退出码、标准错误和失败前已产生的标准输出
交付形态 仓颉静态库和 .cjo 接口文件,构建目录位于仓库外的 .cjpm-target
验证方式 cjpm test、独立 test/main.cj 和 example/example.cj 实际输出

这里没有把 Node.js 对象转换成仓颉对象,也没有宣称已经完成纯仓颉网络栈或鸿蒙原生 API 适配。目标平台需要提供兼容版本的仓颉 SDK、std.process 和 Node.js >= 22.19.0。只要满足这些条件,就可以使用相同的仓颉源码和 cjpm 配置构建并调用当前 Undici 仓库。

1.3 适配后的能力

当前仓颉桥接包公开 7 个入口,覆盖以下能力:

  • 会话配置:Undici(repository:, node:, timeoutSeconds:),指定原库目录、Node.js 可执行文件和会话时限;
  • 通用执行:run(program, args:) 执行可信 JavaScript 并返回标准输出,runSession(program, args:) 同时返回 stdout 和 stderr;
  • 便捷请求:request(url, options:) 调用原库 undici.request,返回状态、headers、trailers、UTF-8 body 和 bodyBase64;
  • Fetch 调用:fetch(url, options:) 调用原库 undici.fetch,返回状态、headers、最终 URL、重定向状态和正文;
  • API 核对:exports() 读取当前仓库实际导出的顶层名称,避免文档与源码漂移;
  • 完整演示:demonstrate() 启动本地测试服务,展示并断言 58/58 顶层导出、8 个拦截器和两种缓存存储;
  • 错误传递:UndiciError 让仓颉调用方读取 Node.js 退出码、stderr 和部分 stdout。

这些入口由仓颉源码真实调用,不把 Node.js 的高级能力改写成只打印断言通过的假示例。示例会打印请求体、响应体、连接 ID、缓存命中次数、重试次数、WebSocket 消息和 SSE 事件等结果。

二、实现路线图

text 复制代码
第 1 阶段:盘点 API       ── 对照 Undici 顶层导出,确定仓颉桥接边界和 Node.js 版本要求
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和仓库外构建输出目录
第 3 阶段:类型映射       ── 用 Undici、UndiciResult 和 UndiciError 表达进程调用边界
第 4 阶段:实现桥接       ── 完成 stdin 程序传输、参数数组、输出收集、超时和原库加载
第 5 阶段:测试与核对     ── 验证参数安全、错误传递、双管道读取和顶层导出清单
第 6 阶段:终端演示       ── 启动本地协议服务,输出所有已验证功能和效果图

每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,桥接阶段得到 undici_cangjie 静态库,测试阶段得到 6 项仓颉测试全部通过,最终示例则输出来自真实 Undici 会话的 49 组结果。

三、逐步实现过程

第 1 阶段:盘点原库 API 和仓颉工程边界

1.1 识别动态 API

Undici 的 JavaScript API 接受动态 options,并通过 dispatcher、handler、stream、Promise 和 Web API 对象组合请求生命周期。index.js 当前导出 58 个顶层名称,包含 Client、Pool、Agent、fetch、WebSocket、MockAgent、SnapshotAgent 和 EventSource 等不同类型。仓颉没有与这些 JavaScript 对象完全等价的跨进程对象模型,因此本适配把"执行原库"和"展示结果"作为桥接协议的核心。

仓颉侧使用 String 传递 JavaScript 程序,使用 Array<String> 传递参数;Node.js 侧通过 AsyncFunction 接收 undici、本地 require 和 args。这样复杂的回调、dispatcher、流和 signal 仍然可以在一个 Node.js 会话内保持原始对象身份,而仓颉侧只需要处理确定的文本输出和异常。

1.2 固定仓颉项目结构

以下目录树按照当前 Undici 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 JavaScript、TypeScript、lib/、test/ 和原有构建文件保持原位置:

text 复制代码
undici/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│   ├── undici.cj
│   ├── demonstration.cj
│   └── undici_test.cj
├── test/
│   └── main.cj
└── example/
    ├── cjpm.toml
    ├── cjpm.lock
    └── example.cj

源码集中在 src/undici.cj,演示脚本位于 src/demonstration.cj,测试位于 src/undici_test.cj。test/main.cj 额外启动仓颉 TCP 服务,独立验证 request 和 fetch 两个便捷入口。示例工程位于 example/,其 src-dir 是当前目录,但 target-dir 放在仓库外的 ../../.cjpm-target/undici_example,不会被源码扫描。

第 2 阶段:创建 cjpm 工程

根目录 cjpm.toml 使用静态库输出类型,并将源码目录和构建目录固定下来:

toml 复制代码
[package]
  cjc-version = "1.1.3"
  name = "undici_cangjie"
  version = "8.11.2"
  description = "Cangjie subprocess bridge to the original Undici implementation"
  output-type = "static"
  src-dir = "src"
  target-dir = "../.cjpm-target/undici_cangjie"

示例工程使用本地路径依赖,并把构建目录放在 example/ 外部:

toml 复制代码
[package]
  cjc-version = "1.1.3"
  name = "undici_example"
  version = "1.0.0"
  output-type = "executable"
  src-dir = "."
  target-dir = "../../.cjpm-target/undici_example"

[dependencies]
  undici_cangjie = { path = ".." }

根项目从仓库目录计算 ../.cjpm-target/undici_cangjie,示例项目从 example/ 目录计算 ../../.cjpm-target/undici_example。两个目录都不在相应 src-dir 扫描范围内,运行 cjpm run 时不会出现把构建输出目录当作源码目录的警告,也不会把构建产物提交到仓库。

第 3 阶段:把 JavaScript 动态对象映射为仓颉类型

3.1 会话配置和结果

Undici 保存原库路径、Node.js 命令和超时时间:

cangjie 复制代码
let library = Undici(
    repository: "/workspace/undici",
    node: "node",
    timeoutSeconds: 60)
let output = library.request("http://127.0.0.1:8080/hello")
println(output)

request 和 fetch 适合可以用 JSON 描述的基础选项。返回值是 JSON 文本,便于仓颉调用方查看状态、headers、trailers、响应体和二进制 Base64。需要 callback、dispatcher、stream、AbortSignal 或 WebSocket 对象时,使用 runSession,让这些对象留在同一个 Node.js 会话内。

3.2 用 stdin 和参数数组表达动态输入

如果把 JavaScript 直接拼接到 shell 命令中,URL、header 或用户输入中的引号和 $() 都可能改变命令语义。本项目让仓颉把程序写入子进程 stdin,把参数放到 process.argv,Node.js 再按预期长度读取程序:

cangjie 复制代码
let result = library.runSession(#"
const response = await undici.request(args[0], JSON.parse(args[1]));
console.log(JSON.stringify({
  statusCode: response.statusCode,
  body: await response.body.text()
}));
"#, args: ["http://127.0.0.1:8080/hello", "{}"])
println(result.stdout)

这里的参数不会经过 shell 展开。runSession 同时启动 stdout 和 stderr 读取协程,避免某一路输出过多时阻塞 Node.js。仓颉测试还使用 200000 字节的双管道输出验证这一点。

3.3 用 UndiciError 表示失败

Node.js 会话出现语法错误、版本错误、Undici 请求错误或超时时,仓颉侧统一收到 UndiciError:

cangjie 复制代码
try {
    library.run("throw new Error('request failed')")
} catch (error: UndiciError) {
    println(error.exitCode)
    println(error.output)
    println(error.message)
}

exitCode 保存子进程退出码,output 保存失败前已经产生的 stdout,message 保存 stderr 或桥接失败原因。HTTP 的 4xx/5xx 是否抛出异常仍由原 Undici API 和调用方脚本决定,不会被仓颉层偷偷改写。

第 4 阶段:实现源码

4.1 文件 src/undici.cj

源码按桥接生命周期组织:首先规范化仓库路径,随后构造 Node.js 参数数组,启动 std.process.launch,并行读取 stdout 与 stderr;程序正文写入 stdInPipe,Node.js bootstrap 校验 package.json 的包名为 undici,使用 createRequire 加载当前仓库 index.js,最后执行仓颉传入的异步 JavaScript。会话结束后关闭全局 dispatcher,避免池连接阻止进程退出。

request 和 fetch 是两个小型便捷包装:它们在 Node.js 会话内读取完整响应正文,并把二进制内容同时保留为 UTF-8 和 bodyBase64。exports 用 Object.keys(undici).sort() 生成当前真实导出清单;demonstrate 则调用完整本地协议演示。

4.2 本地协议服务和完整展示

为了让示例可重复运行,src/demonstration.cj 中的 JavaScript 会在动态端口启动 HTTP、代理、SOCKS5、h2c、TLS、WebSocket 和 SSE 测试服务。每个调用都会输出带编号的 JSON 结果,并用 assert 检查真实返回值:

text 复制代码
[01] request       ── POST body、请求头、响应 JSON
[05] stream        ── 收到 chunk-one|chunk-two
[20] RetryAgent    ── attempts: 2,服务端第二次返回成功
[26] MemoryCacheStore ── serverRequests: 1
[31] Socks5ProxyAgent ── 通过 SOCKS5 返回真实 HTTP 正文
[33] MockAgent     ── 201 响应、调用记录和 GET 筛选
[41] WebSocket     ── 消息回显、Pong、1000 正常关闭
[46] EventSource   ── data、lastEventId 和 origin

演示覆盖 HTTP/1.1、证书校验的 HTTPS、h2c、TLS HTTP/2、连接池和代理,还验证 FormData、Response、Cookie、MIME、CacheStorage、快照回放、超时和取消。输出不是预先写死的文本;正文、headers、重试次数、缓存请求次数和事件字段都来自实际库调用。

4.3 Node.js 版本和平台边界

Node.js bootstrap 会读取当前仓库的 package.json,拒绝加载错误目录,并检查 Node.js >= 22.19.0。Undici 的 SQLite cache store 使用 Node.js node:sqlite,示例运行建议使用 Node 24 或更新版本。仓颉部分依赖 std.process,所以本方案适合拥有 Node.js 和进程能力的主机环境。

这条边界必须写在适配说明中:仓颉库负责进程桥接和结果传输,HTTP/2、TLS、WebSocket、流和代理仍由 Node.js 原库执行。对于没有 Node.js 或不允许启动子进程的纯仓颉/鸿蒙环境,需要另行实现原生网络 adapter,不能直接把本包当成原生 Undici 替代品。

第 5 阶段:建立自动化测试

src/undici_test.cj 通过仓颉单元测试验证桥接本身,test/main.cj 则直接链接静态库并启动仓颉 TCP 服务,验证便捷请求接口。测试覆盖以下行为:

测试组 示例
参数传输 包含引号和 $() 文本的参数不经过 shell 解释
原库加载 exports() 包含 Socks5ProxyAgent 和 WebSocketStream
输出读取 stdout 和 stderr 各 200000 字节时仍能完整收集
错误传递 JavaScript 异常保留退出码、部分 stdout 和错误消息
输入校验 无效 JSON 会从 Node.js 返回语法错误
会话时限 未结束的 Promise 被 deadline 终止并返回 124
仓颉 TCP request 和 fetch 都读取到本地仓颉服务器正文
完整示例 58/58 导出、8 个拦截器和两种 cache store 均实际验证

示例除了检查结果,还打印这些结果。运行成功时输出固定标记:

text 复制代码
Coverage: 58/58 top-level exports, all 8 interceptors, both cache stores; 49 result displays.

第 6 阶段:核对 Undici 适配 API

示例在最后用当前加载到的对象反向核对公开导出和功能集合:

text 复制代码
Top-level exports: 58/58
Interceptors: 8/8
Cache stores: 2/2
Result displays: 49
Missing demonstrations: 0

58 个顶层名称包括 dispatcher、pool、agent、fetch、Web API、连接 API、Mock、Snapshot、EventSource 和安装函数。8 个拦截器是 redirect、responseError、retry、dump、dns、cache、decompress 和 deduplicate;两种 cache store 是 MemoryCacheStore 和 SqliteCacheStore。这个核对说明文章中的能力清单与当前 index.js 一致,不代表已经穷尽 Undici 所有参数组合和上游测试。

四、完整验证步骤

以下命令均以仓库根目录 undici/ 为当前目录。先确认本机已经安装仓颉 SDK 和 Node.js:

bash 复制代码
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
node --version

然后检查并构建仓颉静态库:

bash 复制代码
cjpm check
cjpm build

执行仓颉桥接单元测试:

bash 复制代码
cjpm test --no-progress --no-color

编译并运行独立集成入口。--set-runtime-rpath 用于让 macOS 独立程序找到仓颉运行库:

bash 复制代码
cjc test/main.cj --set-runtime-rpath \
  --import-path ../.cjpm-target/undici_cangjie/release/undici_cangjie \
  -L ../.cjpm-target/undici_cangjie/release/undici_cangjie \
  -lundici_cangjie \
  -o ../.cjpm-target/undici_cangjie/integration
../.cjpm-target/undici_cangjie/integration

再构建并运行独立示例工程:

bash 复制代码
cd example
cjpm check
cjpm run

示例运行末尾会显示:

text 复制代码
Coverage: 58/58 top-level exports, all 8 interceptors, both cache stores; 49 result displays.
cjpm run finished

每一组编号结果都来自 Undici 实际返回值。验证结束后可以执行 cjpm clean,并确认 .cjpm-target 等构建输出位于仓库外、没有加入 Git。运行 cjpm run 不应出现 there is no '.cj' file ... target 警告。

五、运行效果

下面的效果图来自 example/example.cj 的实际终端运行,包含 Undici Cangjie 桥接完成后的真实响应、错误码和最终覆盖标记。

截图中可以看到 EventSource 的 data、lastEventId 和 origin,Headers timeout 的 UND_ERR_HEADERS_TIMEOUT,fetch 取消产生的 AbortError,以及 install 后的 fetch 结果。终端底部的 Coverage: 58/58... 说明示例不仅断言通过,还完成了公开导出、拦截器、缓存存储和 49 组结果展示的核对。服务端、客户端和 Node.js 原库都由示例程序在本地控制,因此效果图是可重复的真实运行输出,不是模拟页面。

六、关键决策说明

6.1 为什么使用明确的桥接结果结构

仓颉适配的价值在于让调用方知道进程调用会返回什么。UndiciResult 明确区分 stdout 和 stderr,UndiciError 明确暴露退出码、失败前输出和错误消息。若把所有输出拼成一个字符串,调用方无法区分业务结果、Node.js warning 和错误诊断,也无法可靠地对失败进行处理。

6.2 为什么保留 Undici 原始 API 能力

Undici 的价值在于大量经过验证的 Node.js HTTP 细节和周边 API。桥接层保留原始 undici 对象,允许仓颉调用方在 runSession 内使用 Client、Pool、MockAgent、WebSocket、EventSource 和拦截器,而不是重新发明一套名称相似但行为不兼容的仓颉 API。对纯仓颉环境无法满足的 Node.js 依赖,文档明确写出前提,不把限制隐藏起来。

6.3 为什么把测试和示例分开

单元测试负责桥接边界和失败处理,适合自动化;独立集成入口负责仓颉 TCP 服务与便捷 API;示例工程负责启动多种本地协议服务并打印完整结果,适合终端观察和截图。三者使用同一个 undici_cangjie 包,因此效果图来自库的真实调用,测试又不会因为展示文本而失去失败定位能力。

七、兼容边界和遗留问题

JavaScript Undici 与仓颉的进程、对象和异步模型不同,以下限制需要在接入时提前了解:

  1. 当前版本是 Node.js 子进程桥接,不是纯仓颉 HTTP/HTTPS 实现;需要 Node.js >= 22.19.0 和允许启动子进程的运行环境。
  2. request 和 fetch 便捷入口通过 JSON 表达基础选项并缓冲响应;callback、dispatcher、stream、AbortSignal、WebSocket 等对象需要在 runSession 内使用。
  3. JavaScript 对象身份、活动连接、流和回调不会跨进程变成仓颉对象;每个 run 调用默认创建一个新的 Node.js 会话。
  4. Node.js 会话使用仓颉标准输入传输程序,参数通过参数数组传递,但 run 的程序内容必须来自可信代码,桥接层不是 JavaScript 沙箱。
  5. HTTPS、HTTP/2、WebSocket、SSE、SQLite cache store、代理和压缩等能力依赖 Node.js 版本与本仓库已有实现,目标平台没有对应 Node.js 能力时不能使用这些示例。
  6. 示例中的 HTTP、TLS、SOCKS5、WebSocket 和 SSE 服务是有限的本地测试夹具,只验证文章列出的路径,不替代生产级协议服务器和安全审计。
  7. stdout、stderr 和便捷 API 的响应会暂存在内存中;大响应或长时间流式业务应在 Node.js 会话内部消费,并自行设计背压和生命周期管理。
  8. 适配覆盖公开导出和功能类别,不代表覆盖 Undici 的每一种 option、错误组合、RFC 边界或完整上游测试矩阵。

这些是适配边界,不是构建错误。只要 cjpm check、cjpm build、cjpm test 成功,独立集成程序读取到仓颉 TCP 服务正文,示例输出实际结果并出现 Coverage: 58/58...,就说明当前版本完成了本地编译和行为验收。

八、接入方式

在另一个仓颉项目中,可以将本仓库作为本地依赖:

toml 复制代码
[dependencies]
  undici_cangjie = { path = "../undici" }

仓颉调用方可以先使用便捷入口:

cangjie 复制代码
import undici_cangjie.*

main(): Int64 {
    let client = Undici(repository: "../undici", timeoutSeconds: 60)
    let response = client.request(
        "http://127.0.0.1:8080/hello",
        options: "{\"headers\":{\"accept\":\"application/json\"}}")
    println(response)
    return 0
}

需要原库完整 API 时,在 runSession 内保持相关 JavaScript 对象:

cangjie 复制代码
let result = client.runSession(#"
const agent = new undici.Agent({connections: 2, pipelining: 2});
try {
  const response = await undici.request(args[0], {dispatcher: agent});
  console.log(JSON.stringify({
    status: response.statusCode,
    body: await response.body.text()
  }));
} finally {
  await agent.close();
}
"#, args: ["http://127.0.0.1:8080/hello"])
println(result.stdout)

业务工程统一由 cjpm 构建,Node.js 路径可以通过 Undici(node:) 指定。跨端项目可以复用同一套仓颉调用和结果处理;具体的纯仓颉网络、鸿蒙系统能力、权限、线程和 UI 适配,需要由宿主平台另行实现。

九、总结

本次适配完成了 Undici 到仓颉调用边界的迁移:仓颉侧使用 Undici 管理 Node.js 会话,使用 UndiciResult 区分 stdout/stderr,使用 UndiciError 传递退出码和错误诊断;Node.js 会话加载当前 AtomGit 仓库的 index.js,实际执行 Undici 8.11.2 的完整公开 API。

验证链路包括 cjpm check、cjpm build、6 项仓颉桥接测试、仓颉 TCP 服务集成测试和本地多协议终端演示。最终效果图来自真实的 example/example.cj,能够直观看到 EventSource、超时、取消、install 和最终覆盖结果,而不是只看到断言通过。示例共验证 58/58 顶层导出、8 个拦截器、两种缓存存储并展示 49 组实际结果。

对于暂时无法脱离 Node.js 的对象生命周期和协议实现,项目通过明确的桥接接口、版本检查、超时、错误输出和文档边界保持行为可解释。后续如果目标平台需要纯仓颉实现,可以沿着同一套测试结果逐项替换 Node.js 能力,但应继续以真实请求和可观察输出作为验收标准。

适配过程 FAQ

以下问题来自仓颉工程创建、cjpm 依赖、与 HarmonyOS 设备联调的实际流程。

Q1:cjpm 依赖解析失败或拉取超时?

  • 检查 cjpm.toml 中的依赖坐标与版本号;
  • 配置可用的仓颉包仓库镜像或代理;
  • 删除 target、cjpm.lock 后重新执行 cjpm update / cjpm build。

Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?

  • 设置 CANGJIE_HOME 指向仓颉 SDK 根目录;
  • 确认 cjc --version、cjpm --version 与工程要求的版本一致;
  • 清理构建缓存后重新生成。

Q3:与 HarmonyOS 设备联调时安装失败?

  • 用 hdc list targets 确认设备在线;
  • 确认 HAP 已签名,bundleName 与签名 profile 一致;
  • 设备需加入调试 profile 的 deviceIds。

Q4:动态库或 FFI 符号找不到?

  • 检查导出符号名称、ABI 与打包路径;
  • 确认 .so 已随 HAP 一起打包到 libs/<abi>/;
  • 用 llvm-readelf -d 检查依赖库是否齐全。

Q5:测试用例在设备上失败,但本机通过?

  • 对比设备与主机的架构、SDK 版本和环境变量;
  • 用 hdc hilog 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
染指11101 小时前
158.Agent-LangGraph-子图(共享状态和不共享状态模式)
开发语言·人工智能·python·ui·agent·agents
wangchunyu1141 小时前
JAVA JVM内存结构与运行时数据区
java·开发语言·jvm
slongzhang_1 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
光锥智能1 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
行思理2 小时前
Nginx 跨站请求(CORS)配置完全指南:从原理到生产实践
java·前端·nginx
小溪学编程2 小时前
Spring @Bean 与 @Component 注解的区别详解
java·后端·spring
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 圈子互动章总结与自检清单
java·bootstrap·交友
云和数据.ChenGuang2 小时前
Python 常见特殊方法详解:__new__、__init__、__del__、__str__、__repr__ 与 __getattribute__
开发语言·人工智能·python·1024程序员节
xyouyuweizhu3 小时前
字节流与字符流:Java IO 的两条主线该怎么选
java·开发语言·python