跳到主要内容

· 阅读需 3 分钟

我工作的主力语言是 Kotlin, 那东西,写起来还挺舒服的,但是就是离了 IDEA 完全写不了。

那在我自己的电脑上,没有IDEA 啊,所以还是用别的语言吧,JAVA 是挣钱的根本啊,所以还是得想办法能够做到不用 IDEA 写 JAVA 就行了。

Config with Lazyvim

1. Enable Java in Lazyvim Extras

Lazyvim 的 Extra 中启用 JAVA,你就基本上能够用 Neovim 写 JAVA 了

在 Vim 中输入 LazyExtras, 弹出的popup window 会显示类似如下的内容,自己研究下怎么Enable Java 就行了

  LazyVim Extras

This is a list of all enabled/disabled LazyVim extras.
Each extra shows the required and optional plugins it may install.
Enable/disable extras with the <x> key

Enabled: (14)
● dap.core  mason-nvim-dap.nvim  nvim-dap  nvim-dap-ui  nvim-dap-virtual-text  which-key.nvim
// highlight-next-line
● lang.go  mason.nvim  neotest-go  nvim-dap-go  nvim-lspconfig  nvim-treesitter  conform.nvim  neotest  none-ls.nvim  nvim-dap
● lang.java  mason.nvim  nvim-jdtls  nvim-lspconfig  nvim-treesitter  which-key.nvim  nvim-dap

Disabled: (28)
○ coding.codeium  codeium.nvim  nvim-cmp  lualine.nvim

2. Debug 单元测试

https://github.com/LintaoAmons/CoolStuffes/blob/6cd43927a7ea804d9f2d496e24eb61d5dbe96df0/lazyvim/.config/nvim/lua/plugins/lang-java.lua#L106

这个需要去看看 nvim-jdtls 的 README. 如果有问题的话,可以尝试留言问我。

主要是你需要自己 compile 一下 vscode-java-test,

我已经试过了,是可以成功的,也蛮简单,compile 完了之后,你需要加上一行配置在已有的 JAVA Extras 上,你可以在我的配置里面找到这一行.

不要看着多,其实我就是去 Lazyvim Java Extras 里面把它的配置粘贴出来放在这个文件里了,然后加上了这一句而已,你需要把路径换成你的 jar 包路径

先随便写写,有问题再来更新

· 阅读需 4 分钟

代码可以在 https://github.com/LintaoAmons/print-config.nvim 找到

下面是这个示例插件的文件树

print-config.nvim on  main
❯ tree .
.
├── README.md
├── lua
│   └── print-config
│   ├── config.lua
│   └── init.lua
└── plugin
└── print_config_plugin.lua # ⭐️

4 directories, 4 files

当插件开始加载时,位于插件repo下的 plugin 文件下的脚本首先被执行,在本示例中,这个文件既 plugin/print_config_plugin.lua

vim.print("This is the start point(can be consider as MainFunction/EntryPoint) when loading a plugin to neovim")

-- you can do anything you want by writing lua scripts
require("print-config").setup({
called_by = "plugin/print_config_plugin.lua"
}) -- here I just call the setup method

vim.print("This is the end of Loading a plugin")

在代码中,我会执行一系列打印操作,这样,我们在加载插件的时候,就能够从中了解到代码执行的顺序

还需要注意的是这个 setup 方法,它是插件本身的一个方法,我这里强调的插件本身,是指你可以直接在 require("插件名") 之后拿到的方法,而不是 require("插件名.其他包)" 才能拿到的方法

因为这是一个 neovim 插件配置的 convention,这也是 lazy.nvim 配置插件的主要依据,下面就是我们如何在用 lazy.nvim 加载本插件的代码

  {
dir = "/Volumes/t7ex/Documents/oatnil/beta/print-config.nvim", -- 1. dir 是因为我这个插件是本地的插件
-- "LintaoAmons/print-config.nvim" -- 1. 你也可以直接声明 `github` 上的插件
opts = {
called_by = "lazy.nvim opts" -- 2. 注意这里的值,我们在最后加载插件的时候会通过这个值来辨别插件的代码执行顺序
},
},

好了,有了这一系列的代码,我们启动 neovim 的时候,就可以看到输出的信息

1: This is the start point(can be consider as MainFunction/EntryPoint) when loading a plugin to neovim
2: Setup method called by: plugin/print_config_plugin.lua
3: This is the end of Loading a plugin
4: Setup method called by: lazy.nvim opts
  • 1,2,3 行都是来自于加载插件运行 plugin/print_config_plugin.lua 中的代码打印出来的
  • 4 行是在 lazy.nvim 中声明插件的 opts 调用的

我们可以看看 lazy.nvim 插件的 README 中是怎描述 otps 这个字段的: opts should be a table (will be merged with parent specs), return a table (replaces parent specs) or should change a table. The table will be passed to the Plugin.config() function. Setting this value will imply Plugin.config()

那这里又多一个 Plugin.config() function, 我们再来看看这个字段的描述: config is executed when the plugin loads. The default implementation will automatically run require(MAIN).setup(opts). Lazy uses several heuristics to determine the plugin's MAIN module automatically based on the plugin's name. See also opts. To use the default implementation without opts set config to true.

  • 其实我理解,这里就是说,其实最终默认就是调用声明的插件的 setup 方法
    • 如果你想要修改这个调用过程,你就需要去重写这里的 config 字段
    • 如果你只是想要用一个自己的配置,只需要修改 opts 字段就好了

· 阅读需 5 分钟

cat

一个简单而直观的例子是 cat 命令,通常用于显示文件内容。在这个例子中,我们将使用 cat filename.txt

  • 输入(Input) 对于 cat filename.txt 命令,输入是 filename.txt 这个文件。这个文件包含了要被 cat 命令处理的数据。

  • 过程(Process) 过程是命令内部执行的操作。对于 cat filename.txt,这个过程是读取 filename.txt 文件中的内容。

  • 输出(Output) 输出是命令执行后的结果。在这里,cat filename.txt 的输出是文件 filename.txt 中的文本内容,通常显示在终端或命令行界面上。

理解管道命令:ls | grep txt | wc -l

理解了单个带输入命令之后,让我们看看如何将多个命令组合起来。我们使用的例子是 ls | grep txt | wc -l

  • ls 命令: 列出当前目录下的所有文件和文件夹。
  • grep txt 命令: 筛选出包含 "txt" 的行,通常是以 .txt 结尾的文件。
  • wc -l 命令: 计算输入中的行数,即 .txt 文件的数量。

在这个组合命令中,管道 | 的作用是将一个命令的输出转换成另一个命令的输入。这种方式使得每个命令都成为了一个处理单元,具有明确的输入和输出,可以灵活组合以完成复杂的任务。

函数签名

“输入、过程、输出”这一概念在计算机科学的各个方面都是普遍适用的。

理解这一点对于学习新的编程语言或技术是非常有帮助的。

它强调了无论使用哪种语言,核心的编程思维——理解和实现输入、过程和输出——始终是相同的。

函数签名这个概念一定要熟记在心

字符串长度计算

我们将展示如何在 JavaScript、Java 和 Python 中实现一个简单的函数:计算字符串的长度。这个函数将接受一个字符串作为输入,并输出该字符串的长度。

  • JavaScript 实现
function getStringLength(str) {
return str.length;
}
  • Python 实现
def get_string_length(str):
return len(str)
  • Java 实现
public class Main {
public static int getStringLength(String str) {
return str.length();
}
}

在这三个例子中,每个函数都有相同的“输入”(一个字符串)、“过程”(计算字符串长度)和“输出”(长度的数值)。尽管它们使用了不同的语言,但它们的功能和结构是一致的。

他们的函数签名分别是

# JavaScript
getStringLength(str)

# Python
get_string_length(str)

# JAVA
int getStringLength(String str)

可以看出,JAVA作为强类型语言,可以只从签名就看出他的输入为 String,输出为 int, js和py也能看出他的输入,但是输出是暗含在 return 的类型上的

函数签名定义了函数的输入和输出。在上述例子中,函数签名明确了每个函数接受一个字符串作为输入,并返回一个整数作为输出。这显示了即使在不同的编程语言中,函数签名的一致性和清晰性是至关重要的。

此外,函数的命名(如 getStringLength)也非常关键。一个好的函数名可以清晰地表达函数的作用,即它的“过程”。在我们的例子中,getStringLength 这个名字直观地说明了这个函数是用来获取(Get)一个字符串(String)的长度(Length)。

相信我,这个概念一定要熟记在心,编程语言知识工具,要他们变成一个个小零件,最后按照你的想法,互相关联,运转出你想要的结果,成就感是无与伦比的。

· 阅读需 10 分钟

想象一下,你有一个闹钟。

刨根问底就像是拆开这个闹钟,查看每一个齿轮、电路和部件,理解它们是如何协同工作来让闹钟响起的。在编程中,这就意味着你不仅使用代码来解决问题,还要深入理解代码的每一个部分是如何工作的,以及它们是如何相互作用的。

黑盒则像是只用闹钟来设定闹铃时间,而不关心里面的工作原理。你知道当你设定了一个时间,闹钟就会在那个时刻响起。在编程里,这就是关注输入(你告诉程序要做什么)和输出(程序完成任务后的结果),而不去深究它是如何内部实现的。

总结起来,刨根问底是深入到最基本的部分,理解事物的内在工作原理;而黑盒则是关注结果,而不深入探究背后的复杂过程。这两种方法根据不同的情况和需求,在学习和实践中都有其适用之处。

初学者的策略

对于初学者来说,在学习编程时找到“刨根问底”和“黑盒”方法之间的平衡至关重要。这个平衡的关键是识别何时深入细节以及何时保持对整体流程的关注,同时考虑到时间和精力的有效利用。以下是具体的应对方式和原则:

1. 有侧重点的刨根问底

  • 关键概念深入:选择核心和基础概念进行深入学习。例如,对于刚开始学习编程的人来说,理解变量、循环、条件语句等基本概念是非常重要的。
  • 分阶段深入:随着学习的进展,逐步深入更复杂的主题。一开始不需要深入了解所有高级主题,而是随着基础知识的稳固,逐渐扩展到更高级的概念。

2. 大量的黑盒使用

  • 利用现有工具:利用已有的库和框架来实现功能,而不是一开始就试图理解它们的内部工作原理。
  • 专注于实现功能:在初学阶段,重点应该放在如何使用编程语言和工具来解决问题,而不是深入到它们的每一个细节。

3. 支撑原理

  • 帕累托原则(28原则):遵循帕累托原则,专注于那20%的关键知识和技能,这样可以处理大约80%的常见问题或任务。
  • 性价比原则:考虑学习每个新概念的“性价比”。如果某个概念的学习成本远高于其带来的实际应用价值,那么它可能不是初学者现阶段的最佳学习对象。
  • 精力与时间分配:合理安排学习时间,确保在关键概念上投入足够的时间,同时避免在当前阶段不必要的细节上耗费太多精力。

其实我觉得,这些理念不仅是初学者,像我这样的入门老菜鸟,也要多多反思下自己的学习策略。

容易犯的错误

确实,对于刚开始学习编程的人来说,一个常见的误区是“用黑盒羞耻”,这是指感觉仅仅理解代码的输入和输出而不深入其内部原理是不够的。这种心态可能会导致以下几个问题:

1. 过度重视细节

  • 初学者可能认为为了成为一名“真正的程序员”,必须彻底理解每一行代码和每一个算法的内部工作原理。
  • 这种心态会使他们在早期阶段就过度深入那些复杂且对初学者来说并不直接重要的细节。

2. 效率低下的学习过程

  • 投入大量时间研究编程的边边角角,比如一个特定函数的所有可能用法或者某个库的内部实现,可能会导致学习进程缓慢。
  • 这样的学习方法可能会导致重要的核心概念被忽视,从而影响建立坚实的编程基础。

3. 挫败感和动力丧失

  • 当初学者发现自己无法完全理解每个细节时,可能会感到挫败。
  • 这种挫败感可能导致对编程的兴趣降低,甚至放弃学习。

4. 忽视实际应用

  • 过分关注理论和细节可能会忽视编程的实际应用,如解决实际问题和项目开发。
  • 编程的真正价值在于能够应用所学知识来创建有用的程序和解决问题,而不仅仅是理解理论。

应对策略

  • 接受逐步学习过程:理解编程是一个逐步学习和积累经验的过程。不需要一开始就了解所有的细节。
  • 平衡理论与实践:在理论学习和实践项目之间找到平衡,这可以帮助更好地理解和应用知识。
  • 重视核心概念:专注于学习那些对于大多数项目和问题至关重要的核心概念和技能。
  • 接受黑盒方法的有效性:认识到在某些情况下,理解输入和输出就足够了,不必深入每个细节。

通过这些策略,初学者可以更有效地利用他们的时间和精力,同时保持对编程的热情和兴趣。重要的是要认识到,学习编程是一个循序渐进的过程,不需要一开始就全面了解一切。

过早优化 与 MVP

对于那些容易陷入“刨根问底”的学习者来说,一个常见的陷阱是“过早优化”。过早优化是指在项目的早期阶段就过分关注性能和代码的完美性,而不是首先关注于功能的实现。这种做法通常会导致以下几个问题:

  • 时间和资源的浪费:在项目初期,很难准确判断哪些优化是真正必要的。因此,过早地进行优化可能会浪费宝贵的时间和资源在那些最终可能并不重要的地方。
  • 进度延迟:由于过分关注细节和性能优化,项目的整体进展可能会被延迟,这在紧迫的截止日期面前尤其成问题。
  • 忽视大局:过早优化可能会让人忽视更重要的问题,如功能的实现和用户体验。

在这种情况下,理解和应用“最小可行产品”(MVP)的概念非常重要。MVP 是指具有足够的特性来吸引早期用户和最终客户的产品的最简化版本,但同时又足够简单,不包含额外的功能。应用MVP概念的关键点包括:

  • 先实现核心功能:集中精力首先实现项目的核心功能,而不是一开始就关注于完善和优化。
  • 快速迭代:在MVP发布之后,根据用户的反馈和产品的实际表现来进行迭代和改进。
  • 避免过度设计:保持产品设计的简单性,避免过度设计和不必要的复杂性。

通过采用MVP方法,学习者可以避免“过早优化”的陷阱,更快地推出产品,同时保留时间和资源以便在后期根据实际需求进行优化。

· 阅读需 1 分钟
protocol//[username:password@]host1[:port1][,...hostN[:portN]][/[default-db][?options]]

Important: Any reserved characters for URLs (for example, /, :, @, (, ), [, ], &, #, =, ?, and space) that appear in any part of the connection URL must be percent encoded.

下面举两个例子:

postgresql://dbuser:dbpassword@localhost:5432/mydatabase?sslmode=require&connect_timeout=10
  • postgresql:// 被标记为“协议(Protocol)”。
  • dbuser:dbpassword 被标记为“用户名:密码(Username:Password)”。
  • @localhost:5432 被标记为“主机:端口(Host:Port)”。
  • /mydatabase 被标记为“数据库名称(Database Name)”。
  • ?sslmode=require 被标记为“SSL 模式选项(SSL Mode Option)”。
  • &connect_timeout=10 被标记为“连接超时选项(Connection Timeout Option)”。

这个图解清晰地展示了 PostgreSQL 数据库连接 URL 的结构及其各个部分的意义。

mongodb://mongoadmin:secret@192.168.50.230:27017

· 阅读需 1 分钟

你可以点击这里查看最新的配置

涉及到的插件:

  • kristijanhusak/vim-dadbod-ui
  • tpope/vim-dadbod
  • hrsh7th/nvim-cmp
  • kristijanhusak/vim-dadbod-completion

Usecase

我用了这个 Docker Image 起了这个一个 DB https://github.com/ghusta/docker-postgres-world-db

下面是一些连接参数

DB_HOST = "localhost"
DB_PORT = "5432"
DB_NAME = "world-db"
DB_USER = "world"
DB_PASS = "world123"

db-1

完整配置

-- this is a lazy.nvim config
return {
{
"kristijanhusak/vim-dadbod-ui",
dependencies = "tpope/vim-dadbod",
event = "VeryLazy",
},
{
"hrsh7th/nvim-cmp",
optional = true,
dependencies = {
{
"kristijanhusak/vim-dadbod-completion",
event = "VeryLazy",
init = function()
vim.api.nvim_create_autocmd("FileType", {
desc = "dadbod completion",
group = vim.api.nvim_create_augroup("dadbod_cmp", { clear = true }),
pattern = { "sql", "mysql", "plsql" },
callback = function()
require("cmp").setup.buffer({ sources = { { name = "vim-dadbod-completion" } } })
end,
})
end,
},
},
},
}

· 阅读需 4 分钟

遇到优秀的教程时,我会跟着做一遍;有新想法或者知识点时,我会立即动手尝试。但问题在于,我通常会忘记昨天的代码是如何编写的。因此,记录和回顾变得极其重要。

在学习这些知识点时,保持简单和专注至关重要,这样在复习时就能迅速抓住核心内容,避免干扰。

我的demo库需要独立的提交历史,同时我希望能将这些demo以语言分类,放入统一的仓库中,例如我的java仓库。这里面会有hello-world, jooq-setup等项目,我可以在一个远程仓库中查看它们。

我考虑过以下方法:

  • git worktree
  • git submodule
  • 使用脚本同时拉取/推送多个git仓库

传统的分支策略需要频繁切换上下文,编辑器重载会浪费大量时间。那么,是否有一种方式可以同时访问这些子项目的文件系统,并随时修改和提交?Git worktree 提供了这样的解决方案。

Git Worktree 简介

Git worktree 允许你在同一仓库的不同分支上同时工作,无需切换等待。每个worktree都有自己的工作目录,让你可以把分支当作完全独立的项目处理。

工作场景

比如,你有一个Java仓库,它包含了多个子项目。每个子项目在独立的分支上开发,你希望能够无缝切换这些分支,并与一个远程仓库同步。

传统分支 vs. Worktree

没有worktree的情况下,你需要在多个分支间切换,等待编辑器重载和索引。但有了worktree,每个子项目都保留在自己的工作目录,独立且随时可用。

多远程仓库的维护挑战

如果每个子项目对应一个远程仓库,会带来维护的复杂性,比如权限管理、钩子脚本和持续集成配置。

Worktree vs. Submodule

Submodule 允许将一个Git仓库嵌入另一个仓库中。相比之下,worktree提供了更简洁的工作流,无需处理子模块的同步和更新问题。

多敲,多积累,我的学习秘诀

release/lang-bases/java-worktrees为例,你可以为hello-worldjooq-setup等子项目创建独立的worktree,而主仓库main作为主要的开发线。

release/lang-bases/java-worktrees 
❯ tree . -L 2 -a
.
├── hello-world
│ ├── .git
├── jooq-setup
│ ├── .git
└── main
└── .git

每个目录都连接到主仓库,但可以独立提交到远程仓库,这样减少了上下文切换的成本。

结语

如果你在日常工作中遇到类似挑战,尝试将Git worktree集成到你的工作流中,它可能会成为提高效率的秘诀。

这两个命令就够了

# Add a new dir for branch
$ git worktree add [-b <branch>] <path> <remote>/<branch>

# Push changes of all branch to remote
git push --all origin

· 阅读需 2 分钟

想象你的 Terminal 已经在你的工作目录了,然后你从浏览器下载了一个东西,它自动放在了 ~/Downloads 目录下

好吧,我太笨了,根本不知道刚刚下载的文件叫什么乱七八糟的名字,也不想 cd 过去找它

这时候我会用下面这个命令把它直接挪过来,不管它叫什么名字(当然,你可以把它变成一个 alias, 方便使用)

ls -t ~/Downloads | head -n 1 | xargs -I {} cp ~/Downloads/{} .

下面我来逐一解释命令的每一部分:

  • ls -t ~/Downloads: 这个命令会列出 ~/Downloads 目录中的所有文件,并根据修改时间排序,最新修改的文件排在最前面。

  • |: 这个符号被称为管道符,它用来将一个命令的输出作为另一个命令的输入。

  • head -n 1: 这个命令会取前一个文件名(因为它们是最新修改的文件)。

  • |: 再次使用管道符,将 head 命令的输出传递给 xargs

  • xargs -I {} cp ~/Downloads/{} .: 这里 xargs 接受 head 命令的输出(即文件名)作为输入,并对每个输入执行 cp 命令。 -I {} 选项告诉 xargs{} 作为替换字符串,命令中的每次出现 {} 都会被替换为输入行。所以 cp ~/Downloads/{} . 会被替换为 cp ~/Downloads/file1 .,其中 file1 是从 ~/Downloads 目录中选出的最新修改的文件。

· 阅读需 3 分钟

其实就是概念上的 进程(命令)参数

Dockerfile 中的 CMD 和 ENTRYPOINT

  • CMD: 定义了容器启动时默认执行的命令和参数。如果运行容器时提供了其他命令和参数,CMD 中的内容会被覆盖。
  • ENTRYPOINT: 定义了容器启动时必须执行的命令,即容器的主进程。CMD 中定义的参数可以作为 ENTRYPOINT 的默认参数,但如果运行容器时提供了其他参数,CMD 中的参数会被这些参数覆盖。
  • 如果同时使用 ENTRYPOINTCMD,则 ENTRYPOINT 指定的命令会执行,而 CMD 中的内容会作为参数传给 ENTRYPOINT

Kubernetes 中的 command 和 args

  • command: 指定进程(命令), 表现上覆盖了容器启动时的默认入口命令(即 Dockerfile 中的 ENTRYPOINT)。
  • args: 指定参数, 表现上覆盖了容器启动时的默认参数(即 Dockerfile 中的 CMD),如果没有 command 被指定,args 的第一个就是主进程命令。
  • 如果你在 Kubernetes 的 Pod 配置中使用了 command,Dockerfile 中的 CMDENTRYPOINT 会被覆盖。如果你在 Pod 配置中使用了 args,但没有使用 command,那么 Dockerfile 中的 ENTRYPOINT 将作为入口命令,而 args 将覆盖 Dockerfile 中的 CMD

例子1: Dockerfile 中只有 CMD

Dockerfile

FROM ubuntu
CMD ["echo", "Hello, Docker!"]

Kubernetes Pod yaml

containers:
- name: mycontainer
image: myimage

Final Command

echo Hello, Docker!

例子2: Dockerfile 中只有 ENTRYPOINT

Dockerfile

FROM ubuntu
ENTRYPOINT ["echo"]

Kubernetes Pod yaml

containers:
- name: mycontainer
image: myimage
args: ["Hello, Kubernetes!"]

Final Command

echo Hello, Kubernetes!

例子3: Dockerfile 中同时有 CMD 和 ENTRYPOINT

Dockerfile

FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello, Docker!"]

Kubernetes Pod yaml

containers:
- name: mycontainer
image: myimage

Final Command

echo Hello, Docker!

例子4: Dockerfile 中有 CMD 和 ENTRYPOINT,Kubernetes Pod yaml 中有 command 和 args

Dockerfile

FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello, Docker!"]

Kubernetes Pod yaml

containers:
- name: mycontainer
image: myimage
command: ["cat"]
args: ["/etc/os-release"]

Final Command

cat /etc/os-release

例子5: Dockerfile 中没有 CMD 和 ENTRYPOINT,Kubernetes Pod yaml 中有 command 和 args

Dockerfile

FROM ubuntu

Kubernetes Pod yaml

containers:
- name: mycontainer
image: myimage
command: ["cat"]
args: ["/etc/os-release"]

Final Command

cat /etc/os-release

· 阅读需 3 分钟

说来惭愧,我的学习一直不怎么好,英语高考也还差 2 分到 100 (满分150)

上了大学,还是其实对英语感兴趣的,也有在继续学习

直到后来英语六级差 2 分上 600, 托福考试阅读满分,到现在在新加坡工作

我可以说对于自学英语还是有点心得的,因为一路走来就没有报过补习班

现在找到一个超级厉害的精听 workflow,分享给正在自学的你,啥都准备好了,帮你节约大把时间(性价比低的,砸大把时间换一点进步的,都不是我的风格,主打一个高效,有用)

其实逻辑很简单,精听,一定要把每一个单词听出,对比原文找差距

OK,听起来很简单吧,但是没有合适的工具(工作流),这个想法的落地可能工作量极大

那下面就是我整理的真实可执行的方法

工作流

  • 精听材料(免费的哈): https://ielts.kmf.com/practice/listening/cambridge/intensive
  • 一个语音输入的工具: 我用的是 Gboard(Google 输入法)
  • 一个AI: 遇到不懂的语法,交给他去解释分析。或者把你的输入和他的原文发给AI,让他帮你分析出错的根因(你错误的英语思维惯性)

首先这个听力材料就非常棒,他将素材切割成句子,并且可以(显示/关闭)原文,译文。切换前一句,后一句,重复播放本句都非常简单,交互做得非常棒

语音输入工具:Gboard 输入到我手机上的 obsidian 上,电脑听原句,手机复述,再一对比,完美

效率非常高,素材不用管,复述不耗时,有问题问AI