Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

偶尔下载一些视频媒体的时候会想要对它进行压缩来缩小占用空间,
但是如果每次等下载完再去用软件压缩会比较麻烦
这里就找到了inotify扩展,可以监听文件变更
这里其实想吹一下小米的mimo2.5模型,因为要装php扩展,
我的是php7.4,网上搜了下有基于pecl,有的是基于源码
我就直接丢给配了小米模型的hermes agent来安装,直接就给我装好了
顺带再帮我写了个artisan命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class InotifyWatchCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'file:watch
{path : Directory to watch}
{--recursive : Watch subdirectories recursively}
{--event=* : Events to watch (create, modify, delete, move). Default: create,modify}
{--ext=* : Only watch files with these extensions (e.g., --ext=mp4 --ext=mkv)}
{--output= : Output file path for logging (optional)}
{--format=text : Output format: text or json}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Watch directory for file write events using inotify';

/**
* Event constants mapping.
*/
protected $eventMap = [
'create' => IN_CREATE,
'modify' => IN_MODIFY,
'delete' => IN_DELETE,
'move' => IN_MOVED_TO,
'close_write' => IN_CLOSE_WRITE,
'open' => IN_OPEN,
'access' => IN_ACCESS,
'attrib' => IN_ATTRIB,
];

/**
* @var resource
*/
protected $inotify;

/**
* @var array
*/
protected $watchDescriptors = [];

/**
* @var resource|null
*/
protected $outputHandle;

/**
* @var bool
*/
protected $running = true;

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = trim($this->argument('path'));
$recursive = $this->option('recursive');
$events = $this->option('event') ?: ['create', 'modify'];
$extensions = $this->option('ext');
$outputFile = $this->option('output');
$format = $this->option('format');

// Validate path
if (!is_dir($path)) {
$this->error("Directory does not exist: {$path}");
return 1;
}

// Setup output file if specified
if ($outputFile) {
$this->outputHandle = fopen($outputFile, 'a');
if (!$this->outputHandle) {
$this->error("Cannot open output file: {$outputFile}");
return 1;
}
}

// Calculate event mask
$eventMask = 0;
foreach ($events as $event) {
if (!isset($this->eventMap[$event])) {
$this->error("Unknown event: {$event}. Available: " . implode(', ', array_keys($this->eventMap)));
return 1;
}
$eventMask |= $this->eventMap[$event];
}

// Always add IN_CLOSE_WRITE for reliable write detection
$eventMask |= IN_CLOSE_WRITE;

// Initialize inotify
$this->inotify = inotify_init();

if (!$this->inotify) {
$this->error("Failed to initialize inotify");
return 1;
}

// Set non-blocking mode for graceful shutdown
stream_set_blocking($this->inotify, false);

// Setup signal handling for graceful shutdown
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
}

// Add watches
$this->addWatch($path, $recursive);

$this->info("Watching: {$path}");
$this->info("Events: " . implode(', ', $events));
$this->info("Recursive: " . ($recursive ? 'yes' : 'no'));
if ($extensions) {
$this->info("Extensions: " . implode(', ', $extensions));
}
$this->info("Press Ctrl+C to stop");
$this->line('');

// Main loop
$checkInterval = 500000; // 500ms in microseconds

while ($this->running) {
// Check for signals
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}

$events = inotify_read($this->inotify);

if ($events !== false && !empty($events)) {
foreach ($events as $event) {
$this->handleEvent($event, $extensions, $format);
}
}

usleep($checkInterval);
}

// Cleanup
$this->cleanup();

return 0;
}

/**
* Add inotify watch for a directory.
*/
protected function addWatch(string $path, bool $recursive): void
{
$mask = IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE | IN_OPEN | IN_ATTRIB;
$wd = inotify_add_watch($this->inotify, $path, $mask);
$this->watchDescriptors[$wd] = $path;

if ($recursive) {
$items = scandir($path);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$this->addWatch($fullPath, true);
}
}
}
}

/**
* Handle inotify event.
*/
protected function handleEvent(array $event, array $extensions, string $format): void
{
$wd = $event['wd'];
$mask = $event['mask'];
$name = $event['name'] ?? '';
$cookie = $event['cookie'] ?? 0;

// Get directory path from watch descriptor
$dir = $this->watchDescriptors[$wd] ?? 'unknown';

// Skip if no filename
if (empty($name)) {
return;
}

// Filter by extension if specified
if (!empty($extensions)) {
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!in_array($ext, $extensions)) {
return;
}
}

// Determine event type
$eventType = $this->getEventType($mask);

// Build file path
$filePath = $dir . DIRECTORY_SEPARATOR . $name;

// Build event data
$data = [
'timestamp' => date('Y-m-d H:i:s'),
'event' => $eventType,
'file' => $name,
'path' => $filePath,
'dir' => $dir,
'cookie' => $cookie,
];

// Output based on format
if ($format === 'json') {
$output = json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
} else {
$output = $this->formatTextOutput($data);
}

// Write to console
$this->writeToConsole($eventType, $output);

// Write to file if handle exists
if ($this->outputHandle) {
fwrite($this->outputHandle, $output);
}
}

/**
* Get human-readable event type from mask.
*/
protected function getEventType(int $mask): string
{
$types = [];

if ($mask & IN_CREATE) {
$types[] = 'CREATE';
}
if ($mask & IN_MODIFY) {
$types[] = 'MODIFY';
}
if ($mask & IN_DELETE) {
$types[] = 'DELETE';
}
if ($mask & IN_MOVED_TO) {
$types[] = 'MOVED_TO';
}
if ($mask & IN_CLOSE_WRITE) {
$types[] = 'CLOSE_WRITE';
}
if ($mask & IN_OPEN) {
$types[] = 'OPEN';
}
if ($mask & IN_ACCESS) {
$types[] = 'ACCESS';
}
if ($mask & IN_ATTRIB) {
$types[] = 'ATTRIB';
}

return implode('|', $types);
}

/**
* Format text output.
*/
protected function formatTextOutput(array $data): string
{
return sprintf(
"[%s] %-12s %s (%s)\n",
$data['timestamp'],
$data['event'],
$data['file'],
$data['dir']
);
}

/**
* Write output to console with color coding.
*/
protected function writeToConsole(string $eventType, string $output): void
{
if (strpos($eventType, 'CREATE') !== false) {
$this->info(rtrim($output));
} elseif (strpos($eventType, 'DELETE') !== false) {
$this->error(rtrim($output));
} elseif (strpos($eventType, 'MODIFY') !== false || strpos($eventType, 'CLOSE_WRITE') !== false) {
$this->comment(rtrim($output));
} elseif (strpos($eventType, 'MOVED_TO') !== false) {
$this->warn(rtrim($output));
} else {
$this->line(rtrim($output));
}
}

/**
* Handle signal for graceful shutdown.
*/
public function handleSignal(int $signal): void
{
$this->running = false;
$this->line('');
$this->info("Received signal {$signal}, shutting down...");
}

/**
* Cleanup resources.
*/
protected function cleanup(): void
{
if ($this->outputHandle) {
fclose($this->outputHandle);
}

// Remove all watches
foreach ($this->watchDescriptors as $wd => $path) {
inotify_rm_watch($this->inotify, $wd);
}

if (is_resource($this->inotify)) {
fclose($this->inotify);
}

$this->info("Watch stopped.");
}
}

只需要执行命令
php artisan file:watch /home/nick/listen --recursive
然后我新建一个文本文件

1
touch 1.txt

就能看到文件的变更

这个流程下来我都没有任何调整,都是一步到位了
这个因为没有准确的条件对比,可能是hermes也起了一定作用,还是说本身模型就很强
主要是监听了文件的创建,修改,关闭写入等,
首先是inotify_init函数 — 初始化 inotify 实例,类似于客户端
然后是inotify_add_watch 函数 — 添加监听到已初始化的 inotify 实例

1
2
3
4
5
6
7
8
inotify_add_watch(resource $inotify_instance, string $pathname, int $mask): int|false
参数 ¶
inotify_instance
inotify_init()返回的资源
pathname
要监听的文件或目录
mask
监听事件。详情见 预定义常量。

具体的预定义常量有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
IN_ACCESS (int)
文件被访问(读)(*)

IN_MODIFY (int)
文件被修改(*)

IN_ATTRIB (int)
元数据变更(例如:权限、修改时间等)(*)

IN_CLOSE_WRITE (int)
打开写入后关闭的文件(*)

IN_CLOSE_NOWRITE (int)
未打开写入的文件被关闭(*)

IN_OPEN (int)
文件被打开(*)

IN_MOVED_TO (int)
文件被移动到监听目录(*)

IN_MOVED_FROM (int)
文件移出监听目录(*)

IN_CREATE (int)
在监听目录中创建文件或文件夹(*)

IN_DELETE (int)
监听目录中删除文件或文件夹(*)

IN_DELETE_SELF (int)
监听的文件或目录被删除

IN_MOVE_SELF (int)
IN_CLOSE (int)
与 IN_CLOSE_WRITE | IN_CLOSE_NOWRITE 相等

IN_MOVE (int)
与 IN_MOVED_FROM | IN_MOVED_TO 相等

IN_ALL_EVENTS (int)
以上所有常量的位掩码

IN_UNMOUNT (int)
卸载包含监听对象的文件系统

IN_Q_OVERFLOW (int)
事件队列溢出(此事件中监听描述符是 -1)

IN_IGNORED (int)
监听被移除(通过 inotify_rm_watch() 显示移除,或者是文件被移动,或者文件系统被卸载)

IN_ISDIR (int)
事件发生的主体是目录

IN_ONLYDIR (int)
如果是目录,仅监听路径名(从 Linux 2.6.15 开始)

IN_DONT_FOLLOW (int)
如果是符号链接,不引用路径名(从 Linux 2.6.15 开始)

IN_MASK_ADD (int)
如果路径名存在,追加监听此路径的事件掩码(代替更换掩码)

IN_ONESHOT (int)
路径触发一个监听事件后,从监听列表中移除。

接下去是

1
inotify_read(resource $inotify_instance): array

会返回以下内容

1
2
3
4
wd 是由 inotify_add_watch() 返回的监听描述符
mask 是 events 的位掩码
cookie 是连接相关事件(例如:IN_MOVE_FROM 和 IN_MOVE_TO)的唯一 id
name 是文件名(例如:监听目录中被修改的文件)

注意这个返回值

1
返回 inotify 事件数组。在没有待处理事件或 inotify_instance 非阻塞时返回 false。事件发生时都会返回包含以下键的数组:

如果要设置非阻塞的话

1
stream_set_blocking($fd, 0);

$fd就是init返回的inotify对象
如果是非阻塞就是自己循环去read有没有事件,
如果是阻塞的话就是read会一直等到有事件了才返回
还可以通过

1
inotify_queue_len(resource $inotify_instance): int

如果有数量代表read不是阻塞的,有待处理的事件

最后还有通过inotify_rm_watch来清除监听器

1
inotify_rm_watch(resource $inotify_instance, int $watch_descriptor): bool

这样针对文件系统有变更的话就会比较方便去做下一步处理,
比如压缩,移动归档等
之前最开始的处理方式是每次都遍历,感觉还是挺不科学的,
已有的文件会一遍遍被重复扫描
这个方法还有个局限性就是得在Linux系统里,
其他系统例如mac需要fs_usage这些工具才行
Windows里需要使用 ReadDirectoryChangesW 等api
总结下来这个方式对于Linux系统下的ext4系统是比较方便的,
其他的系统可能需要再研究下

一、先看总原则

  1. 创建对象麻烦

用创建型模式

  1. 类或对象的组合关系复杂

用结构型模式

  1. 对象之间职责划分、通信流程复杂

用行为型模式


二、不同设计模式适合的场景


(一)创建型模式:解决“对象怎么创建”

  1. 单例模式(Singleton)

适合场景:

  • 系统中某个类只能有一个实例
  • 如配置管理器、线程池、缓存管理器、日志对象

关键词:

  • 唯一实例
  • 全局访问点

记忆点:

  • “一个类只有一个对象”基本就是单例

  1. 工厂方法模式(Factory Method)

适合场景:

  • 需要创建对象,但不想直接 new
  • 不同条件下创建不同具体产品
  • 如不同数据库连接、不同导出器、不同支付方式对象

关键词:

  • 延迟实例化
  • 子类决定创建哪种对象

记忆点:

  • 关注“创建一个等级结构中的某一个产品”

  1. 抽象工厂模式(Abstract Factory)

适合场景:

  • 要创建一整组相关或相互依赖的对象
  • 如不同风格 UI 组件:Windows 风格按钮、文本框;Mac 风格按钮、文本框
  • 不同数据库族:连接、命令、结果集统一切换

关键词:

  • 产品族
  • 成套创建

记忆点:

  • 工厂方法是“一个产品”
  • 抽象工厂是“一族产品”

  1. 建造者模式(Builder)

适合场景:

  • 对象构造过程复杂,步骤多,但步骤顺序稳定
  • 如组装电脑、构建 SQL、创建复杂报表、创建复杂请求对象

关键词:

  • 分步构建
  • 构建过程与表示分离

记忆点:

  • “复杂对象一步步组装”就是建造者

  1. 原型模式(Prototype)

适合场景:

  • 创建对象成本高,复制比重新创建更高效
  • 如图形对象复制、配置模板复制、带大量初始化数据的对象

关键词:

  • 克隆
  • 复制现有对象

记忆点:

  • 区分浅拷贝、深拷贝是常见延伸点

(二)结构型模式:解决“类和对象怎么组织”

  1. 适配器模式(Adapter)

适合场景:

  • 已有类能用,但接口不兼容
  • 老系统对接新系统
  • 第三方接口封装

关键词:

  • 接口转换
  • 兼容旧接口

记忆点:

  • 核心是“让本来不能一起工作的类可以一起工作”

  1. 桥接模式(Bridge)

适合场景:

  • 存在两个独立变化维度,希望分别扩展
  • 如消息类型(普通/紧急)和发送方式(短信/邮件)都在变化
  • 图形形状和绘制平台都要扩展

关键词:

  • 抽象与实现分离
  • 多维度变化

记忆点:

  • 如果题目说“两个维度都要扩展,避免类爆炸”,多半是桥接

  1. 组合模式(Composite)

适合场景:

  • 树形结构
  • 如文件夹/文件、菜单/子菜单、组织架构

关键词:

  • 部分—整体
  • 树形结构统一处理

记忆点:

  • 叶子节点和容器节点统一对待

  1. 装饰器模式(Decorator)

适合场景:

  • 动态给对象增加功能,不想修改原类
  • 如 I/O 流加缓冲、加加密、加日志
  • 给组件附加权限校验、统计功能

关键词:

  • 动态增强
  • 比继承更灵活

记忆点:

  • 与代理模式容易混淆
  • 装饰器强调“增强功能”

  1. 外观模式(Facade)

适合场景:

  • 子系统复杂,希望对外提供统一简单入口
  • 如一键启动流程、统一服务入口、SDK 封装

关键词:

  • 简化调用
  • 统一入口

记忆点:

  • 核心是“隐藏子系统复杂性”

  1. 享元模式(Flyweight)

适合场景:

  • 系统中有大量细粒度对象,内存占用大
  • 如字符对象、棋子对象、图标对象

关键词:

  • 共享对象
  • 节省内存

记忆点:

  • 区分内部状态与外部状态

  1. 代理模式(Proxy)

适合场景:

  • 需要控制对对象的访问
  • 如远程代理、虚代理、权限代理、缓存代理

关键词:

  • 控制访问
  • 间接访问真实对象

记忆点:

  • 与装饰器区分:
    • 代理:控制访问
    • 装饰器:增强功能

(三)行为型模式:解决“对象之间怎么协作”

  1. 策略模式(Strategy)

适合场景:

  • 有多种算法或业务规则可互换
  • 如支付策略、折扣策略、排序策略、路由策略

关键词:

  • 算法封装
  • 可互换

记忆点:

  • “同一问题,多种算法可切换”就是策略

  1. 模板方法模式(Template Method)

适合场景:

  • 流程固定,但某些步骤实现不同
  • 如报表生成流程、数据导入流程、考试流程

关键词:

  • 定义算法骨架
  • 子类重写具体步骤

记忆点:

  • “整体流程不变,细节步骤可变”就是模板方法

  1. 观察者模式(Observer)

适合场景:

  • 一个对象状态变化,要通知多个对象
  • 如事件监听、消息订阅、UI 联动、发布订阅

关键词:

  • 一对多通知
  • 发布—订阅

记忆点:

  • 题干出现“通知多个依赖对象”基本就是观察者

  1. 责任链模式(Chain of Responsibility)

适合场景:

  • 多个对象都可能处理同一个请求,谁处理运行时决定
  • 如审批流程、过滤器链、异常处理链

关键词:

  • 请求沿链传递
  • 解耦发送者和接收者

记忆点:

  • 常见于工作流、过滤器、权限校验链

  1. 命令模式(Command)

适合场景:

  • 需要把请求封装成对象
  • 支持撤销/重做、请求队列、日志记录
  • 如菜单操作、遥控器、事务操作

关键词:

  • 请求对象化
  • 支持撤销、排队

记忆点:

  • “请求发送者与执行者解耦”是命令模式核心

  1. 状态模式(State)

适合场景:

  • 对象在不同状态下行为不同
  • 如订单状态、工作流状态、播放器状态、TCP 连接状态

关键词:

  • 状态变化引起行为变化
  • 消除大量 if-else

记忆点:

  • 与策略模式易混淆
  • 策略:主动切换算法
  • 状态:状态变化驱动行为变化

  1. 迭代器模式(Iterator)

适合场景:

  • 顺序访问聚合对象中的元素,而不暴露内部结构
  • 如遍历集合、列表、树结构

关键词:

  • 统一遍历接口
  • 隐藏内部结构

记忆点:

  • 主要用于集合遍历

  1. 中介者模式(Mediator)

适合场景:

  • 多个对象之间交互复杂,网状依赖严重
  • 如聊天系统、界面控件协作、航班调度

关键词:

  • 集中协调
  • 降低对象间耦合

记忆点:

  • “对象间不直接通信,都通过中间者协调”

  1. 备忘录模式(Memento)

适合场景:

  • 需要保存对象历史状态,以便恢复
  • 如撤销、回滚、存档点

关键词:

  • 状态快照
  • 恢复历史状态

记忆点:

  • 常和命令模式一起出现

  1. 访问者模式(Visitor)

适合场景:

  • 数据结构相对稳定,但对其进行的操作经常变化
  • 如编译器语法树处理、报表统计、对象结构分析

关键词:

  • 操作与数据结构分离
  • 易于新增操作

记忆点:

  • “结构稳定、操作多变”是判断关键

  1. 解释器模式(Interpreter)

适合场景:

  • 需要定义语法规则并解释执行
  • 如表达式计算、规则引擎、简单脚本解析

关键词:

  • 语法解释
  • 表达式求值

记忆点:

  • 适合简单语言或规则系统,不适合特别复杂语法

三、软考最常考的几个区分

  1. 工厂方法 vs 抽象工厂
  • 工厂方法:创建一种产品
  • 抽象工厂:创建一族产品

  1. 装饰器 vs 代理
  • 装饰器:增强功能
  • 代理:控制访问

  1. 策略 vs 状态
  • 策略:多种算法任选其一
  • 状态:对象状态改变导致行为变化

  1. 模板方法 vs 策略
  • 模板方法:流程骨架固定,细节延迟到子类
  • 策略:算法整体可替换

  1. 适配器 vs 桥接
  • 适配器:解决现有接口不兼容
  • 桥接:解决两个维度独立变化

前阵子在openclaw热度之后,又出来个hermes agent,主打个更高效智能的agent,我们就来体验试下
其实挺久之前就开始运行了,但是因为大部分的coding plan都涨价了,普通的api付费又可能被这类agent给刷爆
正好最近小米在做一个模型token推广计划
申请入口在这里
可以将自己的使用场景提交上去,我也申请到了一些token
就来配置到hermes agent试用下
一键安装脚本

1
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash

如果有一些科学上网可能会快一点
安装好之后,主要是配置模型和gateway
因为现在最新版的已经支持了很多模型的官方配置

1
hermes model

通过这个命令,就可以进行模型设置,小米的mimo也天然支持
我一开始想用lm studio的本地模型,但是因为笔记本上只有6g显存,内存也有一些日常程序在跑
正常最低的上下文长度比如4096就可以跑起来,但是像hermes agent需要最低64k的上下文长度
如果用64k上下文运行的话显存+内存就不够了,而且本地除非配置非常牛
不然本地的token速度不够,也会很慢
如果有了申请好的mimo订阅,那就把token和端点api的地址输进去

1
https://token-plan-cn.xiaomimimo.com/v1

另外hermes agent的gateway已经支持微信通道了
可以通过

1
hermes gateway setup

选择微信,
然后扫码进行登录链接,记得比如像我Qclaw,就是替换掉不能同时连接多个龙虾类agent
扫码完一路确定就好了
然后在微信回复消息,就会有个approve的口令

1
hermes pairing approve weixin xxxxx

然后就可以跟hermes发消息了

这样就能有一个手机上可以监控和交互的agent了
这一点感觉是龙虾类的一大重要特点
还可以让hermes来对比下它和openclaw

这块我觉得主要还是要有自己的工作流和技能,能让我自己的工作或者作业流程能更高效
比如做项目开发,类似于openspec这种做sdd开发
claude有一堆的前端和设计的技能
另外还有比较重要的就是模型也得比较厉害
当然国内的模型现在也已经都提升了很多
近期用下来比如glm 5.1已经是非常不错的模型了
在有比较明确的上下文和约定下,还是能完成很多开发任务的

1
2
3
4
5
6
7
8
9
10
11
12
开发者体验

能力 OpenClaw Hermes

MCP 协议 ✓ ✓
自定义工具 ✓ ✓ (Python 插件)
技能市场 ClawHub 内置 + Hub
配置方式 CLI + 文件 CLI + YAML + .env
Profile 多实例 ? ✓
Credential Pool ? ✓ (多 API Key 轮换)
ACP (IDE 集成) ? ✓
Git worktree ? ✓ (并行 Agent)

对比适用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14

选 OpenClaw 如果你:
- 主要需要生活/工作助手(邮件、日历、航班值机)
- 使用 Google Chat、IRC、LINE、Twitch 等小众平台
- 喜欢活跃的社区和赞助商支持
- 偏好开箱即用的体验

选 Hermes Agent 如果你:
- 主要是开发者/研究者/系统管理员
- 需要灵活切换 20+ LLM 提供商(含本地模型)
- 需要子代理委派、并行任务、Git worktree
- 需要丰富的内置技能(97+)
- 需要 Email/SMS/Home Assistant 集成
- 喜欢深度自定义和 Python 生态

总结下
两者都是优秀的开源个人 AI Agent,核心思路相似:
自托管 Gateway + 多平台消息 + 工具调用 + 持久记忆。
有兴趣的可以体验下看看

上次是开始使用了Obsidian,之前也听说了Obsidian也推出了cli命令行工具,这次就来体验下,主要是为了能在例如claude code跟codex中直接操作Obsidian,比如在一些知识的学习之后希望整理成笔记
首先安装比较简单,就是在软件内开启命令行就行,会加到系统的PATH路径里
开启命令行有版本要求

1
Using the CLI requires the Obsidian 1.12 installer. See the installer version update guide.


在关于的最后高级菜单中就可以开启了
我们可以先看看这个cli能用来干啥
当然可以用help命令来查看

1
obsidian help

日常使用可以参考这些命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Open today's daily note
obsidian daily

# Add a task to your daily note
obsidian daily:append content="- [ ] Buy groceries"

# Search your vault
obsidian search query="meeting notes"

# Read the active file
obsidian read

# List all tasks from your daily note
obsidian tasks daily

# Create a new note from a template
obsidian create name="Trip to Paris" template=Travel

# List all tags in your vault with counts
obsidian tags counts

# Compare two versions of a file
obsidian diff file=README from=1 to=3

包括记录今日的日常笔记,搜索,查看活动日志等等
在结合Claude Code使用的时候
我们可以装下大佬的skill
https://github.com/kepano/obsidian-skills
我们可以通过这个安装

1
2
/plugin marketplace add kepano/obsidian-skills
/plugin install obsidian@obsidian-skills

然后我们就能用它里面的技能来处理知识笔记处理
比如

1
使用defuddle技能帮我摘录这篇文章 https://obsidian.md/help/cli

这样就能用defuddle来处理这篇文章,摘录到本地
然后再用这个包里的obsidian技能把摘录下来的文章保存到obsidian
可以看到初步的效果

这样就初步打通了这个cli的一体化体验了
当然更细致的使用还有待研究和学习
可能这块的生态来说,的确有了cli工具,整体使用起来也会变得更丝滑
从文章摘录到整理到笔记
刚才还有一步重要的就是把知识进行总结整理
通过仔细查看cli的文档也能看到这个工具不是只做出来有个样子而已
已经是比较深入到整体的系统使用
甚至包括插件的管理都有
查看安装的所有插件
plugins

List installed plugins.

1
2
3
4
filter=core|community  # filter by plugin type

versions # include version numbers
format=json|tsv|csv # output format (default: tsv)

查看启动的插件
plugins:enabled

1
2
3
4
filter=core|community  # filter by plugin type

versions # include version numbers
format=json|tsv|csv # output format (default: tsv)

启用插件
plugin:enable

1
2
id=<id>                # (required) plugin ID
filter=core|community # plugin type

这些也只是一小部分,可以具体在cli进行学习参考

之前看到Karpathy这个wiki知识库提到了使用Obsidian作为笔记软件,因为Obsidian存储笔记的格式就是markdown
Obsidian的使用方式和别的笔记系统还是有比较大差别的,我们只需要指定一个笔记目录,就可以往里存放笔记了
它的语法整体就是markdown的语法,之前比如像我用hexo写博客的话大概也知道基本的语法,
只是有个比较特殊的内部链接
通过

1
[[标题]]

来做内部链接,就能链接到笔记库内部的另一篇笔记,同时会在被链接笔记的右侧侧边栏显示链接当前笔记的笔记,有点绕
这样呢,我个人理解还是通过文章之间的联系,把孤立的一篇篇文章连接成知识体系,因为像我之前就也出现了碰到一个问题想解决它,结果解决了想记录下,一搜发现以前已经写过了
没有成体系化的笔记系统,就需要定期去整理回顾之前的笔记和文章,当然呢,工具永远只是一部分,主动地去整理记忆才是最重要的。
哪怕最好的工具,不去好好使用也只是工具
当然这个Obsidian在使用还有它很好用的网页摘录插件,因为这个甚至是把它作为笔记软件的一个重要原因
我之前使用的是为知笔记,它有个特别好用的就是在微信公众号里的文章可以直接转发他们的企微助理号,就会直接同步到笔记库里,但是前阵子可能是被微信封了
目前这个也没有完全恢复,所以也这个最大的优点没有了之后,也在考虑可以换个软件
它的网页摘录插件就是
https://chromewebstore.google.com/detail/obsidian-web-clipper/cnjifjpddelmedmihgijeibhnjfabmlf

安装这个插件后就可以把网页直接转换成markdown进行保存,包括像网页内的图片也会摘取链接转换成markdown的图片格式
还有就是笔记多端同步了,这边因为我比较熟悉syncthing,所以可以使用syncthing来做同步,我现在主力使用的安卓机,
也可以安装syncthing和Obsidian的安卓客户端,通过syncthing同步后,手机上同样指定同步的笔记库目录,就可以跨端查看笔记了

0%