| name | label-manager |
| description | 管理和查询 ZSpace NAS 标签——按文件名打标(label-manager scan)、按文件内容语义搜打标(RAG)、按标签找文件、新建/删除标签。 触发词:打标签、给 XX 打 XX 标签、按标签找、找所有带 XX 标签的文件、新建标签、删除标签、给 XX 内容的文件打标签、智能打标、语义打标、给一年级的文件打《一年级》标签、给课本打标签、按内容找文件打标、给所有 XX 内容的文件批量打标。 不适用:文件重命名(用 MCP file_rename)、笔记搜索(用 MCP notebook_search)。 |
Label Manager — NAS 标签全生命周期
概述
NAS 标签的全生命周期管理——两种发现方式,同一入口:
| 发现方式 | 用哪个命令 | 适合 |
|---|
| 按文件名/扩展名 | label_manager.py scan + find-by-label | 目录递归、批量、精确匹配 |
| 按文件内容(RAG 语义) | 纯 LLM 编排,semantic_search → save_file_label | 自然语言描述、不靠文件名 |
最终都走 save_file_label MCP tool 落盘。
前置:nas-setup skill(验证 NAS 登录)。RAG 场景额外需要 rag-manager(门控)。
MCP tool 依赖
| list_file_labels | 列出所有标签 |
| save_file_label(label_names, paths) | 打标签(覆盖式,会自动创建新标签名) |
| delete_label(label_names) | 删除标签(从所有文件上彻底移除) |
| notebook_updatelabel(id, label) | 笔记标签 |
| label_manager.py 命令(机械活) | 用途 |
|---|
list-labels | 同 MCP,但走脚本(批量友好) |
scan --root X --ext Y | BFS 扫目录找文件(LLM 决策前先用) |
find-by-label --label X | 反向查询带某标签的文件 |
5 个标准场景
场景 1:给单个/多个文件打标签
用户说:"给 /sata14/my/data/docker-compose.yml 打 docker 标签"
步骤:
- 调
save_file_label(label_names="docker", paths="/sata14/my/data/docker-compose.yml")
- 返回结果(返回 200 即可)
注意:
paths 多个用英文逗号分隔,最多 50 个/次(NAS 限速)
- 如果
label_names 里有不存在的标签名,NAS 会自动创建
场景 2:批量打标(目录递归 + LLM 决策)
用户说:"把 /sata14/my/data/ 下所有 .yml 文件打 docker 标签"
步骤:
- exec
python skills/label-manager/label_manager.py scan --root /sata14/my/data/ --ext yml --max-depth 5 --output /tmp/scan.json
- 读
/tmp/scan.json,得到 items 数组(含 path/name/labels)
- LLM 自己判断哪些真该打(README.yml 不该打),过滤后分批(每批 50 个)
- LLM 调
save_file_label(label_names="docker", paths="path1,path2,...") 分批执行
为什么 LLM 决策:不是所有 .yml 都是 docker 配置。LLM 看 path/name 决定。
场景 3:按标签找文件(反向查询)
用户说:"找所有带 docker 标签的文件"
步骤:
- exec
python skills/label-manager/label_manager.py find-by-label --label docker --root /sata14/my/data/ --max-depth 5 --output /tmp/docker.json
- 读
/tmp/docker.json,得到 matches 数组(含 path/name/is_dir/labels)
- 格式化返回给用户
已知 gap:
- 受
--max-depth 限制,深度外文件找不到
- 用户只能扫
/<pool>/my/<子目录>/,跨池越权 N001411
- 脚本内部走 BFS + 串行,sleep 0.1s/层,100 个目录约 10s
场景 4:新建标签
用户说:"新建一个 备份 标签"
步骤:
- 调
save_file_label(label_names="备份", paths="/sata14/my/data/")(自动建)
- 可选:调
list_file_labels() 验证已存在
注意:
- NAS 没有专门的"创建标签"端点 — 用
save_file_label 传不存在标签名会自动建
- 想纯创建不打任何文件:传
paths="/sata14/my/data/"(任意已有路径)
场景 5:删除标签(⚠️ 必须二次确认)
用户说:"删除 docker 标签"
步骤:
- 必须先调
list_file_labels() 确认标签名拼写正确(避免删错)
- LLM 必须显式二次确认:"即将删除 'docker' 标签,这会让所有文件上的 'docker' 标签消失,确认吗?"
- 用户确认后,调
delete_label(label_names="docker")
- 警告:该标签会从所有文件彻底移除,不可恢复
关键约束(必读)
- 串行不并发:N150 性能差,任何批量操作串行,每步 sleep 0.1s
- 判断交回 LLM:脚本只做机械活(扫文件、过滤),决策(打哪些、删哪些)由 LLM 判断
- 删除前必须确认:误删标签会让所有文件上的该标签消失
- 路径格式:
/<pool>/my/<子目录>/,目录必须以 / 结尾
- save_file_label 是覆盖式:会清掉文件已有的其他标签。打之前先
file_info(path) 看当前标签
- 打标签前先 list_file_labels:确认标签名,避免拼错
- 写操作走 MCP tool:不要在脚本里加 apply / delete 子命令(LLM 弹 UI 让用户批更好)
调用示例
例 1:用户说"找 docker-compose.yml,打 docker 标签"
- LLM:调
list_files("/sata14/my/data/") → 找到路径 /sata14/my/data/docker-compose.yml
- LLM:调
save_file_label("docker", "/sata14/my/data/docker-compose.yml")
- 回复:"已打标"
例 2:用户说"把 /sata14/my/data/ 下所有 .yml 打 docker"
- LLM:
exec label_manager.py scan --root /sata14/my/data/ --ext yml --max-depth 5
- LLM:读 candidates,过滤掉 README/CHANGELOG 等
- LLM:批量调
save_file_label(每批 50 个)
例 3:用户说"找所有 docker 标签的文件"
- LLM:
exec label_manager.py find-by-label --label docker
- LLM:读结果,格式化返回
例 4:用户说"新建一个 备份 标签"
- LLM:调
save_file_label("备份", "/sata14/my/data/")(自动建)
- LLM:调
list_file_labels() 验证
例 5:用户说"删除 docker 标签"
- LLM:先调
list_file_labels() 确认存在
- LLM:弹 UI 让用户确认:"即将删除 docker 标签..."
- 用户确认后,调
delete_label("docker")
故障排查
| 现象 | 原因 | 处理 |
|---|
RuntimeError: NAS_USER / NAS_PASSWORD env not set | .env 没加载或没填 | cp .env.example .env + 填密码 |
找不到 /sata14/my/data/ | 路径错(忘了 /) | 目录路径必须 / 结尾 |
code=N001411 无权限 | 路径不在 /<pool>/my/<子目录>/ | 用户只能扫自己 /池名/my/ 下 |
code=N001212 参数有误 | 字段名错或 JSON 而非 form | 走 MCP tool,不用脚本 |
| 扫目录很慢 | max-depth 太深或目录太多 | 改小 --max-depth 或换更窄的 --root |
| 找不到带标签的文件 | max-depth 不够 | 加大 --max-depth(但会更慢) |
| 标签删除后没生效 | NAS 缓存 | 几秒后刷新 |
| 标签名拼错 | delete 是按名字匹配 | 先 list_file_labels() 看准确名字 |
NAS 字段类型坑(必读)
/v2/file/list 返回的 item 字段:
is_dir 是字符串 "0" 或 "1",不是 bool
size / modify_time 是字符串(不是 int),脚本里要 int()
labels 是逗号分隔字符串(如 "docker" 或 "docker,重要"),要 split
- 文件列表字段叫
data.list(不是 data.items)
/v2/labels/alllabels 返回的 list 元素:
id / created_at / updated_at / weight 是 int
label_name 是 str
top_flag 是 int(0/1)
后续可以做(等需求)
- 多轮打标:用户连续说"再给它们打 XX 标签",Agent 维护上下文
- 冲突检测:打标前检查文件已有标签,提示"已有 Y 标签,要覆盖吗"
场景 6:用户说"给教材目录下所有一年级课本打《一年级》标签"(RAG 语义搜)
步骤:
- 拆解需求:内容关键词、限定范围、标签名
- RAG 门控:加载
rag-manager skill → index_status → chunks>0 通过,不通拒绝
semantic_search(query=关键词,scope="files",top_k=30) → 命中清单
- Agent 过滤:distance<1.0 可靠,>1.2 误报
list_file_labels() 确认标签名
save_file_label(label_names="标签", paths="...") — MCP 弹 UI,≤50 个/批
场景 7:文件太大没进 RAG 索引(降级)
文件 > RAG_MAX_FILE_SIZE_KB(默认 100KB)不进索引。降级:文件名匹配(场景 2 scan)。
场景 8:多标签 + 目录双确认
list_files(范围) + semantic_search(关键词) → Agent 取交集 → save_file_label
RAG 关键约束
- 写死走 MCP(save_file_label 弹 UI),不自动批量打
- save_file_label 是覆盖式,先
file_info 看现有标签
- scope=files=内容,不是文件名
- RAG 不通时拒绝(不降级),让用户先部署/重启 rag-server
- 一次 ≤ 50 个路径
跟其他 skill 的分工
| 场景 | 走哪 |
|---|
| 按文件名/扩展名打标 | label-manager scan(本 skill) |
| 按内容语义搜打标 | label-manager 场景 6-8(本 skill) |
| RAG 索引管理 | rag-manager |
| 按标签反向查文件 | label-manager find-by-label |
| 新建/删除标签 | label-manager |
| 找重复/孤儿文件 | file-organizer |