Distribution repo skeleton

One `npx skills add ssh://gitea/agent-skills/skills` now offers all five
skills in a grouped multi-select list. Groups come from
.claude-plugin/marketplace.json: writing-clarity, windows-tools,
image-to-data.

skills/ holds real copies rather than submodules or pointers. The installer
clones without --recurse-submodules (git.ts:241), so a submodule directory
would arrive empty, and it skips remote sources declared in a marketplace
manifest (plugin-manifest.ts:89), so only in-repo paths are resolvable.

Grouping applies only to skills listed explicitly under a plugin's skills
array with a ./-prefixed path (plugin-manifest.ts:147-151); anything not
listed falls into "Other".

think-twice and teach-me share a group because teach-me's SKILL.md points at
double-check.md, self-contain.md and style.md inside the think-twice
directory. Installing teach-me alone leaves those pointers unresolved.

Syncing from the five upstream repos is manual for now.
This commit is contained in:
2026-08-14 17:06:34 +08:00
commit 6b2e167e69
24 changed files with 2589 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
# baimiao_table_ocr.py CLI Usage
`baimiao_table_ocr.py` is a Python CLI for converting table images into structured table data through Baimiao's anonymous image-to-excel web API. It uploads the image to Baimiao's OSS flow, downloads the generated XLSX, and renders it as ASCII, Markdown, CSV, JSON, or XLSX.
## Location
```text
scripts\baimiao_table_ocr.py
```
Run by absolute path when the skill directory is not the current directory:
```powershell
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png"
```
## Runtime
Preferred runtime:
```powershell
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png"
```
The script includes PEP 723 dependency metadata for `requests` and `openpyxl`, so `uv run` can prepare dependencies automatically.
Fallback runtime:
```powershell
python -m pip install -r "<skill-dir>\scripts\requirements.txt"
python "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png"
```
## Synopsis
```powershell
baimiao_table_ocr.py --image <source> [options]
```
`<source>` can be a file path, `-` for stdin, or a base64/data URI image string.
## Options
| Flag | Description |
| --- | --- |
| `-i, --image <source>` | Required. Image source: file path, `-` for stdin, or base64/data URI string. |
| `-f, --format <type>` | Output format: `ascii`, `csv`, `json`, `xlsx`, or `md` (default: `ascii`). |
| `-o, --out <path>` | Output file path. Required for `xlsx`; optional for text formats. |
| `--sheet <name>` | Select a specific worksheet from a multi-sheet OCR result. |
| `--uuid <uuid>` | Baimiao client UUID. Takes precedence over `BAIMIAO_UUID`. |
| `--timeout <seconds>` | OCR polling timeout in seconds (default: `120`). |
| `--proxy <url>` | Proxy URL, for example `http://127.0.0.1:8080` or `socks5://127.0.0.1:7890`. |
| `--no-cache` | Skip local XLSX cache and force a new OCR request. |
| `-q, --quiet` | Suppress progress logs on stderr. |
| `-h, --help` | Show help. |
## Examples
```powershell
# Default ASCII table to stdout
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png"
# Markdown table to stdout
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f md
# CSV to file
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f csv -o "D:\path\table.csv"
# JSON to stdout for agent parsing
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json
# Save generated XLSX
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f xlsx -o "D:\path\result.xlsx"
# Select one sheet
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json --sheet Table2
# Reuse a known UUID
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --uuid "<UUID>"
# Prefer HTTP proxy examples
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --proxy http://127.0.0.1:8080
# SOCKS5 proxy requires PySocks
uv run --with PySocks "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --proxy socks5://127.0.0.1:7890
```
## Stdin Input
Use `--image -` when the image bytes come from stdin:
```powershell
Get-Content -LiteralPath "D:\path\table.png" -AsByteStream | uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image - -f json
```
## Base64 Input
The CLI accepts raw base64 image data or a data URI:
```powershell
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "data:image/png;base64,iVBORw0KGgo..." -f md
```
## Output
### ASCII
Default output. Width calculation is CJK-aware, so Chinese table cells align better in terminals.
### Markdown
Use `-f md`. Multi-sheet results are separated with `### SheetName` headings.
### CSV
Use `-f csv`. Multi-sheet results are separated with comment-like section headers such as `# Table1`.
### JSON
Use `-f json`. The output is a JSON object where each key is a worksheet name and each value is a two-dimensional array of strings:
```json
{
"Table1": [
["Name", "Amount"],
["A", "100"]
]
}
```
### XLSX
Use `-f xlsx -o result.xlsx`. The downloaded OCR result file is copied to the requested output path.
## Cache
The CLI stores downloaded OCR XLSX files under:
```text
scripts\.baimiao_cache\<image_md5>.xlsx
```
Cache hits skip the network OCR flow and only re-render the requested output format. Use `--no-cache` to force recognition again.
On every run, the CLI automatically removes cached `.xlsx` files older than 7 days based on file modified time.
## UUID Behavior
UUID selection order:
1. `--uuid <uuid>`.
2. `BAIMIAO_UUID` environment variable.
3. Newly generated UUID.
The CLI does not write persistent auth files. Tokens are fetched per run through anonymous login.
Reusing a UUID avoids repeatedly creating anonymous accounts, but each UUID may still have OCR quota limits. When the server rejects new anonymous UUID creation with `code: 4`, reuse a previous UUID or switch to a different network/proxy exit.
## Error Messages
The CLI prints short one-line errors to stderr and exits non-zero. Common messages:
| Message | Meaning |
| --- | --- |
| `代理连接失败` | Proxy address, port, or protocol is wrong/unreachable. |
| `SSL 握手超时` | Proxy can connect but cannot complete TLS to the target. |
| `连接超时` | Target server is unreachable from the current network. |
| `网络超时` | Request took too long; retry or use another proxy. |
| `网络不通` | General connection failure. |
| `UUID 配额已用尽` | Current UUID cannot request more OCR tasks. |
## Requirements
- Python 3.9+.
- `requests>=2.25`.
- `openpyxl>=3.0`.
- Optional for SOCKS5 proxy: `PySocks`.
- Network access to Baimiao and Aliyun OSS endpoints.
+147
View File
@@ -0,0 +1,147 @@
---
name: baimiao-table-ocr
description: >
将表格截图、扫描图或图片中的表格转换为结构化数据。用于用户提供图片并希望提取表格、转 CSV/JSON/Markdown/XLSX、或让 agent 读取图片里的行列内容时。通过 bundled Python CLI 调用白描匿名 Web API,支持文件路径、stdin、base64/data URI 输入,默认输出终端友好的 ASCII 表格。需要网络;不适合隐私敏感或不能上传到第三方服务的图片。
---
# 白描表格 OCR
本 skill 用于把图片中的表格识别成结构化数据。它通过 bundled CLI 调用白描 Web 的匿名 image-to-excel API,上传图片并下载 OCR 后的 `.xlsx`,再按需要渲染为 ASCII、Markdown、CSV、JSON 或保留 XLSX。
CLI 的完整参数说明在 [CLI_USAGE.md](CLI_USAGE.md)。逆向 API 细节在 [references/api_reference.md](references/api_reference.md)。
它不负责:
- 本地离线 OCR。
- 保证敏感图片不离开本机。
- 识别非表格型自然文本。
- 修复 OCR 模型本身的识别错误。
## 何时优先使用
以下情况优先选择本 skill:
- 用户提供的是表格截图、账单表、价格表、清单、报表、课程表等图片。
- 用户要求“图片转 Excel”“提取图片里的表格”“OCR 表格”“转 CSV/JSON/Markdown”。
- agent 需要把图片里的表格内容转成可复制、可分析的结构化文本。
- 用户希望保留原始 Excel 文件,用 `-f xlsx -o <path>` 输出。
## 何时不使用
以下情况不要使用本 skill:
- 图片内容隐私敏感,不能上传到第三方服务。
- 用户只需要普通 OCR 文本,而不是表格结构。
- 用户已经有可读的 XLSX/CSV/HTML 表格文件,直接读取原文件更可靠。
- 当前网络不可用,或 Baimiao 匿名接口被限流且没有可用 UUID/代理。
## 默认调用策略
agent 使用本 skill 时,默认按以下策略构造命令:
1. 优先调用 skill 内 bundled CLI,不要假设命令已在 `PATH`:
```powershell
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "<image>" [options]
```
2. 如果没有 `uv`,回退系统 Python。回退前确保安装依赖:
```powershell
python -m pip install -r "<skill-dir>\scripts\requirements.txt"
python "<skill-dir>\scripts\baimiao_table_ocr.py" --image "<image>" [options]
```
3. 默认使用 `-f ascii`,因为它适合终端直接阅读。
4. 如果结果要被后续程序处理,优先用 `-f json` 或 `-f csv`。
5. 如果结果要贴进 Markdown 文档,使用 `-f md`。
6. 如果用户要求 Excel 文件,使用 `-f xlsx -o <output.xlsx>`;`xlsx` 格式必须指定 `--out`。
7. 如果用户已经提供 UUID,或上下文中有之前可用的 UUID,带 `--uuid <uuid>`;否则让 CLI 自动生成 UUID。
8. 如果用户请求使用代理,先给 HTTP 代理示例;只有明确需要 SOCKS5 时再使用 `socks5://`,并提示需要 `PySocks`。
9. 默认允许本地缓存。除非用户明确要求重新 OCR 或怀疑缓存过期,才使用 `--no-cache`。
## 推荐命令模板
```powershell
# 默认:终端 ASCII 表格
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png"
# Markdown 表格,适合直接回复或写入文档
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f md
# JSON,适合 agent 后续解析
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json
# 保存 XLSX
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f xlsx -o "D:\path\result.xlsx"
# 多 sheet 时只输出一个 sheet
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json --sheet Table2
# 使用已有 UUID,避免反复创建匿名账号触发限流
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --uuid "<UUID>"
# HTTP 代理优先
uv run "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --proxy http://127.0.0.1:8080
# SOCKS5 代理,需要额外安装 PySocks
uv run --with PySocks "<skill-dir>\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --proxy socks5://127.0.0.1:7890
```
## 输入说明
`--image` 支持三类输入:
- 文件路径,例如 `D:\path\table.png`。
- stdin,用 `--image -`,图片二进制从标准输入传入。
- base64 或 data URI,例如 `data:image/png;base64,...`。
## 输出说明
默认输出 ASCII 表格到 stdout。多 sheet 结果会按 sheet 分段输出。
可选格式:
- `ascii`:终端友好,默认格式。
- `md`:Markdown 表格。
- `csv`:CSV 文本;多 sheet 时用 `# SheetName` 分段。
- `json`:对象形式,键为 sheet 名,值为二维数组。
- `xlsx`:保存原始 OCR 结果文件,必须指定 `--out`。
## UUID 与限流
白描匿名流程使用客户端 UUID 登录获取 guest token。CLI 不持久化 token 或 UUID,只按以下顺序取 UUID:
1. `--uuid <uuid>`。
2. 环境变量 `BAIMIAO_UUID`。
3. 自动生成新 UUID。
同一个 UUID 可以反复登录获得新的 guest token,但 OCR 免费额度可能耗尽。创建新 UUID 可能被服务端按 IP 限流,表现为登录返回 `code: 4`。如果出现限流,优先复用已有 UUID;如果 UUID 额度耗尽,则需要更换 UUID 或切换可用出口 IP。
## 缓存策略
CLI 会按图片 MD5 缓存 OCR 下载得到的 `.xlsx`,缓存目录位于脚本目录下的 `.baimiao_cache`。每次运行会自动清理修改时间超过 7 天的 `.xlsx` 缓存文件。缓存命中时会跳过网络 OCR,只重新渲染请求的输出格式。
使用 `--no-cache` 可强制重新识别。
## 排查与回退
常见错误处理策略:
- `代理连接失败`:检查 `--proxy` 地址和端口。
- `SSL 握手超时`:代理可能无法访问目标服务器,换 HTTP 代理或出口。
- `连接超时` / `网络不通`:检查网络或代理。
- `UUID 配额已用尽`:更换 UUID,或等待额度恢复。
- `anonymous login failed: rate limited`:当前出口 IP 创建匿名账号过多,复用旧 UUID 或切换出口 IP。
如果用户不允许上传图片到第三方服务,停止使用本 skill,并说明需要本地 OCR 方案。
## 前置条件
- Python 3.9+。
- 推荐安装 `uv`,脚本包含 PEP 723 内联依赖元数据,可用 `uv run` 自动准备 `requests` 和 `openpyxl`。
- 网络能访问 `https://web.baimiaoapp.com` 和 `https://baimiaopdf.oss-cn-hangzhou.aliyuncs.com`。
- SOCKS5 代理需要额外安装 `PySocks`,例如 `uv run --with PySocks ...` 或 `python -m pip install PySocks`。
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,274 @@
# Baimiao Table OCR API Reference
This reference summarizes the anonymous web API flow used by `scripts/baimiao_table_ocr.py`. It is based on reverse engineering of `https://web.baimiaoapp.com/image-to-excel`.
## Overview
```text
client UUID
-> POST /api/user/login/anonymous
-> POST /api/perm/single
-> GET /api/oss/sign
-> POST https://baimiaopdf.oss-cn-hangzhou.aliyuncs.com/
-> POST /api/ocr/table/parser
-> GET result XLSX URL
```
Base URL:
```text
https://web.baimiaoapp.com
```
Referer used by the CLI:
```text
https://web.baimiaoapp.com/image-to-excel
```
## Authentication Headers
Most API calls use:
| Header | Meaning |
| --- | --- |
| `X-AUTH-UUID` | Client-side UUID v4. |
| `X-AUTH-TOKEN` | Guest token returned by anonymous login. |
| `Content-Type` | `application/json;charset=UTF-8` for JSON POST requests. |
| `Referer` | `https://web.baimiaoapp.com/image-to-excel`. |
The CLI does not persist tokens. It logs in once per run unless a cached XLSX result can satisfy the request.
## Step 1: Anonymous Login
```http
POST /api/user/login/anonymous
```
Headers:
```text
Content-Type: application/json;charset=UTF-8
X-AUTH-UUID: <uuid>
Referer: https://web.baimiaoapp.com/image-to-excel
```
Body:
```json
{}
```
Typical success response:
```json
{
"code": 1,
"data": {
"token": "guest-token",
"user": null
},
"msg": "success"
}
```
Important observed responses:
| Response | Meaning |
| --- | --- |
| `code: 1` | Login succeeded. |
| `code: 4` | New anonymous account creation is rate-limited for the current exit IP. |
## Step 2: OCR Permission Token
```http
POST /api/perm/single
```
Headers include `X-AUTH-UUID` and `X-AUTH-TOKEN`.
Body:
```json
{
"mode": "single",
"version": "v2"
}
```
Typical success response:
```json
{
"code": 1,
"data": {
"token": "task-token",
"engine": "plus",
"meta": {}
},
"msg": "success"
}
```
Observed quota/exhaustion response:
```json
{
"code": 0,
"data": {},
"msg": "error"
}
```
The CLI treats this as UUID/OCR quota exhaustion and raises a user-facing UUID quota error.
## Step 3: OSS Upload Signature
```http
GET /api/oss/sign?mime_type=<mime>&t=<timestamp_ms>&r=<random_6>
```
Supported image MIME types observed or inferred from the web client:
| MIME | Extension |
| --- | --- |
| `image/png` | `.png` |
| `image/jpeg` | `.jpg`, `.jpeg` |
| `image/gif` | `.gif` |
| `image/bmp` | `.bmp` |
| `image/webp` | `.webp` |
Typical success response shape:
```json
{
"code": 1,
"data": {
"result": {
"policy": "base64-policy",
"x_oss_signature_version": "OSS4-HMAC-SHA256",
"x_oss_credential": "STS.../aliyun_v4_request",
"x_oss_date": "20260514T180430Z",
"signature": "hex-signature",
"security_token": "security-token",
"host": "https://baimiaopdf.oss-cn-hangzhou.aliyuncs.com",
"file_key": "path/to/file.png",
"content_types": ["image/png"]
}
},
"msg": "success"
}
```
## Step 4: Upload Image To OSS
Upload endpoint is the `host` returned by Step 3, with a trailing `/`.
```http
POST https://baimiaopdf.oss-cn-hangzhou.aliyuncs.com/
```
Multipart form fields:
| Field | Value |
| --- | --- |
| `success_action_status` | `200` |
| `policy` | `result.policy` |
| `x-oss-signature` | `result.signature` |
| `x-oss-signature-version` | `result.x_oss_signature_version` |
| `x-oss-credential` | `result.x_oss_credential` |
| `x-oss-date` | `result.x_oss_date` |
| `key` | `result.file_key` |
| `x-oss-security-token` | `result.security_token` |
| `file` | Image bytes with the detected MIME type. |
Success is HTTP `200`. The CLI also checks for an OSS `<Error>` body.
The image MD5 is computed locally and used in the parser request.
## Step 5: Submit Table Parser Request
```http
POST /api/ocr/table/parser
```
Headers include `X-AUTH-UUID` and `X-AUTH-TOKEN`.
Body:
```json
{
"token": "task-token-from-perm-single",
"hash": "image-md5-hex",
"fileKey": "oss-file-key"
}
```
The CLI polls this endpoint until a result URL appears or `--timeout` is exceeded.
Observed success response shapes:
```json
{
"code": 1,
"data": {
"resultUrl": "https://.../result.xlsx"
},
"msg": "success"
}
```
Observed production response may nest the URL under `ydResp`:
```json
{
"code": 1,
"data": {
"ydResp": {
"result": {
"file_download_url": "https://.../result.xlsx",
"file_preview_url": "https://.../preview.xlsx"
}
}
},
"msg": "success"
}
```
The CLI checks result URLs in this order:
1. `data.resultUrl`.
2. `data.ydResp.result.file_download_url`.
3. `data.ydResp.result.file_preview_url`.
If the parser temporarily returns an upload propagation error, the CLI waits and retries until timeout.
## Step 6: Download XLSX Result
The final result URL is fetched directly with `GET` and saved as XLSX bytes. Text outputs are generated locally by parsing the XLSX with `openpyxl`.
## Rate Limits And Quotas
Observed behavior:
- Reusing an existing UUID can still log in and obtain a new guest token.
- `POST /api/perm/single` may return `code: 0, msg: "error"` when the UUID has no available OCR quota.
- Creating a fresh anonymous UUID may return `code: 4` when the current IP is rate-limited.
- Spoofing `X-Forwarded-For` did not bypass `code: 4` in testing.
Agent guidance:
- Prefer `--uuid` or `BAIMIAO_UUID` when available.
- Use HTTP proxy first when changing network exit is required.
- Use SOCKS5 only when PySocks is installed.
- Do not create or rely on persistent auth/token files.
## Local Cache
The CLI caches the downloaded XLSX by image MD5:
```text
scripts/.baimiao_cache/<image_md5>.xlsx
```
This saves OCR quota and allows re-rendering to another format without another API call. Cached `.xlsx` files older than 7 days are removed automatically on each CLI run. Use `--no-cache` to bypass cache lookup.
@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "requests>=2.25",
# "openpyxl>=3.0",
# ]
# ///
"""
白描图片转表格 CLI — 非登录态完整调用链
用法:
uv run baimiao_table_ocr.py --image table.png # 默认 ASCII 表格输出
uv run baimiao_table_ocr.py --image table.png -f csv # CSV 输出
uv run baimiao_table_ocr.py --image table.png -f xlsx -o out.xlsx # 保存 xlsx
uv run baimiao_table_ocr.py --image table.png --proxy http://127.0.0.1:8080 # 通过代理
UUID 管理:
优先使用 --uuid 参数,其次 $BAIMIAO_UUID 环境变量。
未提供时自动生成新 UUID 并提示设置环境变量以跳过限流。
同一个 UUID 可以反复登录拿新 token,不会触发 rate limiting。
"""
import argparse
import base64
import csv as _csv
import hashlib
import io
import mimetypes
import os
import sys
import tempfile
import time
import unicodedata
import uuid as _uuid
import requests
REQ_TIMEOUT = (15, 30) # (connect_timeout, read_timeout)
BASE = "https://web.baimiaoapp.com"
REFERER = "https://web.baimiaoapp.com/image-to-excel"
DEFAULT_TIMEOUT = 120
POLL_INTERVAL = 3
CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".baimiao_cache")
CACHE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
SESSION = requests.Session()
# SESSION.trust_env = False # uncomment to bypass system proxy
def _file_md5(file_path):
with open(file_path, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
def _cache_path(md5_hash):
return os.path.join(CACHE_DIR, f"{md5_hash}.xlsx")
def _cache_get(image_path):
md5 = _file_md5(image_path)
path = _cache_path(md5)
return (path, md5) if os.path.isfile(path) else (None, md5)
def _cache_put(md5_hash, src_path):
import shutil
os.makedirs(CACHE_DIR, exist_ok=True)
shutil.copy2(src_path, _cache_path(md5_hash))
def _cache_cleanup(max_age_seconds=CACHE_MAX_AGE_SECONDS):
if not os.path.isdir(CACHE_DIR):
return 0
cutoff = time.time() - max_age_seconds
removed = 0
for name in os.listdir(CACHE_DIR):
if not name.lower().endswith(".xlsx"):
continue
path = os.path.join(CACHE_DIR, name)
try:
if os.path.isfile(path) and os.path.getmtime(path) < cutoff:
os.unlink(path)
removed += 1
except OSError:
pass
return removed
def auth_headers(uuid_, token):
return {
"Content-Type": "application/json;charset=UTF-8",
"X-AUTH-UUID": uuid_,
"X-AUTH-TOKEN": token,
"Referer": REFERER,
}
# ─── UUID resolution ──────────────────────────────────────────────
def resolve_uuid(cli_uuid=None):
"""返回 (uuid, is_new)"""
if cli_uuid:
return cli_uuid, False
env_uuid = os.environ.get("BAIMIAO_UUID")
if env_uuid:
return env_uuid, False
new_uuid = str(_uuid.uuid4())
return new_uuid, True
# ─── API steps ────────────────────────────────────────────────────
def step_anonymous_login(uuid_):
for attempt in range(3):
r = SESSION.post(
f"{BASE}/api/user/login/anonymous",
json={},
headers={
"Content-Type": "application/json;charset=UTF-8",
"X-AUTH-UUID": uuid_,
"Referer": REFERER,
},
timeout=REQ_TIMEOUT,
)
data = r.json()
if data.get("code") == 1:
return data["data"]["token"]
if data.get("code") == 4:
wait = 2 * (attempt + 1)
print(f" ⚠ rate limited, retry in {wait}s (attempt {attempt+1}/3)", file=sys.stderr)
for _ in range(wait):
time.sleep(1)
continue
raise RuntimeError(f"anonymous login failed: {data}")
raise RuntimeError("anonymous login failed: rate limited after 3 attempts")
def step_perm(uuid_, token):
r = SESSION.post(
f"{BASE}/api/perm/single",
json={"mode": "single", "version": "v2"},
headers=auth_headers(uuid_, token),
timeout=REQ_TIMEOUT,
)
data = r.json()
if data.get("code") != 1:
if data.get("code") == 0 and data.get("msg") == "error":
raise TokenExhaustedError(f"token exhausted or expired: {data}")
raise RuntimeError(f"perm failed: {data}")
return data["data"]["token"]
def step_oss_sign(uuid_, token, mime_type):
r = SESSION.get(
f"{BASE}/api/oss/sign",
params={
"mime_type": mime_type,
"t": str(int(time.time() * 1000)),
"r": str(int.from_bytes(os.urandom(4), "big") % 1000000),
},
headers=auth_headers(uuid_, token),
timeout=REQ_TIMEOUT,
)
data = r.json()
if data.get("code") != 1:
if data.get("code") == 0 and data.get("msg") == "error":
raise TokenExhaustedError(f"token exhausted: {data}")
raise RuntimeError(f"oss sign failed: {data}")
return data["data"]["result"]
def step_upload(sign, file_path, mime_type):
host = sign["host"].rstrip("/")
key = sign["file_key"]
with open(file_path, "rb") as f:
file_bytes = f.read()
md5 = hashlib.md5(file_bytes).hexdigest()
ext = {"image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif",
"image/bmp": ".bmp", "image/webp": ".webp"}.get(mime_type, ".bin")
upload_fields = [
("success_action_status", (None, "200")),
("policy", (None, sign["policy"])),
("x-oss-signature", (None, sign["signature"])),
("x-oss-signature-version", (None, sign["x_oss_signature_version"])),
("x-oss-credential", (None, sign["x_oss_credential"])),
("x-oss-date", (None, sign["x_oss_date"])),
("key", (None, key)),
("x-oss-security-token", (None, sign["security_token"])),
("file", (f"file{ext}", file_bytes, mime_type)),
]
r = SESSION.post(f"{host}/", files=upload_fields, timeout=(30, 120))
if r.status_code != 200:
raise RuntimeError(f"OSS upload failed HTTP {r.status_code}: {r.text[:500]}")
if r.text.strip() and "<Error>" in r.text:
raise RuntimeError(f"OSS upload error: {r.text[:500]}")
return key, md5
def step_parser(uuid_, token, task_token, file_hash, file_key, timeout=DEFAULT_TIMEOUT):
body = {"token": task_token, "hash": file_hash, "fileKey": file_key}
headers = auth_headers(uuid_, token)
deadline = time.time() + timeout
attempt = 0
while True:
attempt += 1
r = SESSION.post(
f"{BASE}/api/ocr/table/parser",
json=body,
headers=headers,
timeout=REQ_TIMEOUT,
)
data = r.json()
code = data.get("code")
msg = data.get("msg", "")
if code == 1:
resp_data = data.get("data", {})
url = resp_data.get("resultUrl")
if not url:
yd = resp_data.get("ydResp", {})
url = yd.get("result", {}).get("file_download_url")
if not url:
url = yd.get("result", {}).get("file_preview_url")
if url:
return url
task_id = resp_data.get("taskId")
if attempt % 5 == 1 and task_id:
print(f" 识别中... (taskId={task_id})", file=sys.stderr)
elif code == 0 and "上传出错" in msg:
if attempt <= 2:
print(" OSS propagation delay, retrying...", file=sys.stderr)
else:
raise RuntimeError(f"parser failed (attempt #{attempt}): {data}")
if time.time() > deadline:
raise TimeoutError(f"recognition timed out after {timeout}s (last response: {data})")
# interruptible sleep (responds to Ctrl+C)
for _ in range(POLL_INTERVAL):
time.sleep(1)
def step_download(url, out_path):
r = SESSION.get(url, timeout=(30, 300))
r.raise_for_status()
with open(out_path, "wb") as f:
f.write(r.content)
# ─── output formatters ────────────────────────────────────────────
def parse_xlsx(file_path, sheet=None):
"""返回 {sheet_name: list[list[str]]} 或指定 sheet 的 list[list[str]]"""
from openpyxl import load_workbook
wb = load_workbook(file_path, read_only=True, data_only=True)
if sheet:
ws = wb[sheet]
rows = [[str(c.value).strip() if c.value is not None else "" for c in row] for row in ws.iter_rows()]
wb.close()
return rows
result = {}
for name in wb.sheetnames:
ws = wb[name]
result[name] = [[str(c.value).strip() if c.value is not None else "" for c in row] for row in ws.iter_rows()]
wb.close()
return result
def _display_width(s):
"""字符串终端显示宽度 (CJK 占 2, ASCII 占 1)"""
w = 0
for ch in s:
ea = unicodedata.east_asian_width(ch)
w += 2 if ea in ("F", "W") else 1
return w
def _pad_cell(s, target_width):
"""将字符串填充到目标显示宽度"""
current = _display_width(s)
return s + " " * (target_width - current)
def render_ascii(rows):
if not rows:
return "(empty)"
col_widths = [0] * max(len(r) for r in rows)
for r in rows:
for i, cell in enumerate(r):
col_widths[i] = max(col_widths[i], _display_width(cell))
def _sep(left, mid, right, fill):
parts = [fill * (w + 2) for w in col_widths]
return left + mid.join(parts) + right + "\n"
top = _sep("┌", "┬", "┐", "─")
bottom = _sep("└", "┴", "┘", "─")
def _row(cells):
parts = [f" {_pad_cell(c, col_widths[i])} " for i, c in enumerate(cells)]
return "│" + "│".join(parts) + "│\n"
out = top
if rows:
for r in rows:
out += _row(r)
out += bottom
return out
def render_csv(rows):
buf = io.StringIO()
w = _csv.writer(buf, lineterminator="\n")
for r in rows:
w.writerow(r)
return buf.getvalue()
def render_markdown(rows):
if not rows:
return ""
lines = []
lines.append("| " + " | ".join(rows[0]) + " |")
lines.append("| " + " | ".join("---" for _ in rows[0]) + " |")
for r in rows[1:]:
padded = r + [""] * (len(rows[0]) - len(r))
lines.append("| " + " | ".join(padded[:len(rows[0])]) + " |")
return "\n".join(lines) + "\n"
def detect_mime(file_path):
ext = os.path.splitext(file_path)[1].lower()
mime_map = {
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".gif": "image/gif", ".bmp": "image/bmp", ".webp": "image/webp",
}
mime = mime_map.get(ext)
if mime:
return mime
mime, _ = mimetypes.guess_type(file_path)
if mime and mime.startswith("image/"):
return mime
return "image/png"
def resolve_image(image_arg):
"""
返回 (file_path, is_temp) 。支持:
- 本地文件路径: table.png
- stdin: -
- base64 字符串: data:image/png;base64,xxxx 或纯 base64
"""
if not image_arg:
raise ValueError("--image 不能为空")
# stdin
if image_arg == "-":
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
tmp.write(sys.stdin.buffer.read())
tmp.close()
return tmp.name, True
# base64
if image_arg.startswith("data:") or _looks_like_base64(image_arg):
b64 = image_arg
if b64.startswith("data:"):
b64 = b64.split(",", 1)[1] if "," in b64 else b64.split(";base64,", 1)[-1]
data = base64.b64decode(b64)
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
tmp.write(data)
tmp.close()
return tmp.name, True
# local file
return image_arg, False
def _looks_like_base64(s):
return len(s) > 100 and not any(c in s for c in ("/", "\\", " ")) and not os.path.exists(s)
# ─── custom exception ─────────────────────────────────────────────
class TokenExhaustedError(RuntimeError):
pass
# ─── main ─────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="白描表格 OCR — 图片转表格 (非登录态)")
parser.add_argument("--image", "-i", required=True, help="图片来源: 文件路径 / - (stdin) / base64 或 data URI")
parser.add_argument("--out", "-o", default=None, help="输出文件路径 (xlsx 模式必填, 其他格式可选)")
parser.add_argument("--format", "-f", choices=["ascii", "csv", "xlsx", "json", "md"], default="ascii",
help="输出格式: ascii(默认) / csv / xlsx / json / md")
parser.add_argument("--sheet", default=None, help="指定工作表名称 (多 sheet 时可选)")
parser.add_argument("--uuid", default=None, help="白描客户端 UUID (优先于 $BAIMIAO_UUID)")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="识别超时秒数")
parser.add_argument("--proxy", default=None, help="代理地址 (如 http://127.0.0.1:8080 或 socks5://127.0.0.1:7890)")
parser.add_argument("--no-cache", action="store_true", help="跳过缓存,强制重新 OCR")
parser.add_argument("--quiet", "-q", action="store_true", help="抑制进度输出到 stderr")
args = parser.parse_args()
log = (lambda *a, **kw: None) if args.quiet else (lambda *a, **kw: print(*a, file=sys.stderr, **kw))
removed_cache_files = _cache_cleanup()
if removed_cache_files:
log(f"[cache] 已清理 {removed_cache_files} 个 7 天前的缓存文件")
if args.proxy:
SESSION.proxies = {"http": args.proxy, "https": args.proxy}
SESSION.trust_env = False
log(f"[proxy] 使用代理: {args.proxy}")
if not os.path.isfile(args.image) and args.image != "-" and not _looks_like_base64(args.image):
print(f"❌ 图片不存在: {args.image}", file=sys.stderr)
sys.exit(1)
if args.format == "xlsx" and not args.out:
print("❌ xlsx 格式必须指定 --out", file=sys.stderr)
sys.exit(1)
image_path, image_is_temp = resolve_image(args.image)
uuid_, is_new = resolve_uuid(args.uuid)
log(f"[0/5] UUID: {uuid_}")
if is_new and not args.quiet:
print(f" 💡 首次使用,建议设置环境变量避免限流:", file=sys.stderr)
print(f' export BAIMIAO_UUID="{uuid_}"', file=sys.stderr)
print(f" 或每次调用时传 --uuid {uuid_}", file=sys.stderr)
# ── cache check ──
is_cached = False
cached_path, image_md5 = _cache_get(image_path)
if cached_path and not args.no_cache:
log(f"[cache] 命中缓存 (md5={image_md5[:12]}...),跳过 OCR")
tmp_path = cached_path
is_cached = True
else:
log("[1/5] 匿名登录获取 guest token ...")
token = step_anonymous_login(uuid_)
mime = detect_mime(image_path)
# pipeline with auto-relogin on token exhaustion
for retry in range(3):
try:
log(f"[2/5] 获取 OSS 直传签名 & 上传图片 (mime={mime}) ...")
sign = step_oss_sign(uuid_, token, mime)
file_key, file_hash = step_upload(sign, image_path, mime)
log("[3/5] 获取 OCR 任务凭证 ...")
task_token = step_perm(uuid_, token)
log("[4/5] 提交表格识别 (轮询等待) ...")
result_url = step_parser(uuid_, token, task_token, file_hash, file_key, args.timeout)
log("[5/5] 下载 & 解析结果 ...")
break
except TokenExhaustedError:
if retry < 2:
uuid_ = str(_uuid.uuid4())
log(f" UUID 配额已用尽 → 新 UUID: {uuid_}")
if not args.quiet:
print(f" 💡 请更新环境变量:", file=sys.stderr)
print(f' export BAIMIAO_UUID="{uuid_}"', file=sys.stderr)
token = step_anonymous_login(uuid_)
continue
raise
tmp = tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False)
tmp.close()
try:
step_download(result_url, tmp.name)
_cache_put(image_md5, tmp.name)
tmp_path = tmp.name
except Exception:
os.unlink(tmp.name)
raise
# ── render ──
try:
if args.sheet:
rows = parse_xlsx(tmp_path, sheet=args.sheet)
sheets_data = {args.sheet: rows}
else:
result = parse_xlsx(tmp_path)
if isinstance(result, dict):
sheets_data = result
else:
sheets_data = {"Sheet1": result}
if args.format == "ascii":
out_parts = []
for sname, srows in sheets_data.items():
if len(sheets_data) > 1:
out_parts.append(f"\n── {sname} ──\n")
out_parts.append(render_ascii(srows))
out = "".join(out_parts)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(out)
else:
sys.stdout.write(out)
elif args.format == "md":
out_parts = []
for sname, srows in sheets_data.items():
if len(sheets_data) > 1:
out_parts.append(f"### {sname}\n\n")
out_parts.append(render_markdown(srows))
out_parts.append("\n")
out = "".join(out_parts)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(out)
else:
sys.stdout.write(out)
elif args.format == "csv":
out_parts = []
for sname, srows in sheets_data.items():
if len(sheets_data) > 1:
out_parts.append(f"# {sname}\n")
out_parts.append(render_csv(srows))
out_parts.append("\n")
out = "".join(out_parts)
if args.out:
with open(args.out, "w", encoding="utf-8", newline="") as f:
f.write(out)
else:
sys.stdout.write(out)
elif args.format == "json":
import json
out = json.dumps(sheets_data, ensure_ascii=False, indent=2)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(out)
else:
sys.stdout.write(out)
elif args.format == "xlsx":
import shutil
if args.out:
shutil.copy2(tmp_path, args.out)
finally:
if args.format != "xlsx" and not is_cached:
os.unlink(tmp_path)
log("✅ 完成")
if image_is_temp:
try:
os.unlink(image_path)
except OSError:
pass
if __name__ == "__main__":
try:
main()
except requests.exceptions.ProxyError:
print("❌ 代理连接失败 — 请检查 --proxy 地址是否正确", file=sys.stderr)
sys.exit(1)
except requests.exceptions.SSLError:
print("❌ SSL 握手超时 — 代理可能无法连接目标服务器", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectTimeout:
print("❌ 连接超时 — 目标服务器不可达", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ReadTimeout:
print("❌ 网络超时 — 请检查网络连接或换一个 --proxy", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectionError:
print("❌ 网络不通 — 请检查网络或代理是否可用", file=sys.stderr)
sys.exit(1)
except TokenExhaustedError:
print("❌ UUID 配额已用尽且无法自动刷新 — 请更换 UUID", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("", file=sys.stderr)
sys.exit(130)
except Exception as e:
print(f"❌ {e}", file=sys.stderr)
sys.exit(1)
@@ -0,0 +1,2 @@
requests>=2.25
openpyxl>=3.0
+63
View File
@@ -0,0 +1,63 @@
---
name: disk-master
description: Diagnose Windows disk usage with disk-master.exe, explain what is filling the drive, and generate an interactive SpaceSniffer-style HTML report.
---
# Disk Master
Use this skill when the user asks why a Windows drive is full, wants to find large files or folders, wants to inspect C: drive software/AppData usage, or wants an interactive disk report.
## Workflow
1. If the user does not specify a drive, first inspect available local drives, then ask which drive to analyze. Also ask whether they approve a UAC prompt for faster NTFS/MFT scanning. Explain that the scan is local and read-only.
2. Run `disk-master.exe scan --drive C: --scan quick --out result.json` for a fast estimate, or `disk-master.exe scan --drive C: --scan full --out result.json` when the user asks for a complete scan.
3. If the user explicitly agrees to a UAC prompt for privileged NTFS/MFT scanning, add `--elevate --mode mft` and keep `--out <path>`.
4. After scanning, use the CLI status output to report the JSON path and a short human-readable summary. Do not run an extra file-existence check unless the command fails or the path is missing.
5. Read `result.json` and explain the disk state: largest directories, largest files, file types, user folders, AppData hotspots, installed apps/games, and advice items.
6. Generate an interactive static HTML report from `template.html` by replacing `%%DATA%%` with the JSON text.
7. Present the summary first, then link the JSON path and HTML report path. Make clear that all suggestions are review-only and no cleanup was performed.
## Clarifying Questions
When the request is ambiguous, ask concise interactive questions before scanning:
- Drive: choose from detected local drives, for example `C:` or `D:`.
- Scan depth: quick estimate or full scan.
- UAC/MFT: whether to allow an administrator prompt for the faster NTFS engine.
- Output: whether to generate only JSON or also an HTML report.
Recommended wording: "我可以全程本地只读扫描。允许 UAC 的话,NTFS 盘会优先用更快的 MFT 引擎;不允许也可以用普通模式,只是更慢。你想分析哪个分区?"
## SOP
1. Stop and ask before scanning if any required detail is not confirmed. Use the available interactive question tool when possible instead of guessing.
2. Confirm the target drive. If the user only says "scan my disk" or similar, list local fixed drives first, then ask which one to analyze.
3. Confirm whether to use fast index mode. Explain it as: "快速索引需要管理员权限,会优先使用 NTFS/MFT 引擎,速度更快;不授权也可以用普通遍历模式,只是更慢。"
4. Confirm scan scope when unclear: quick scan for a fast estimate, full scan for the final report.
5. Confirm output expectation: JSON only, natural-language analysis, HTML report, or all of them. Default to JSON + HTML report when the user asks for a report.
6. If fast index mode is enabled, tell the user before running the command: "接下来会出现一个 Windows UAC/授权弹窗,请选择允许;扫描只读、仅在本机运行。"
7. Run the scan with an explicit output path. Prefer timestamped names such as `result-<timestamp>.json` and `report-<timestamp>.html` to avoid overwriting prior reports.
8. Expect the report JSON to be large. Be prepared to read it in chunks or use targeted extraction instead of assuming one read will return the whole file.
9. Generate the HTML report by injecting the JSON into `template.html`. If the environment can display or open the HTML directly for the user, do so; otherwise provide the absolute report path.
10. Present results in this order: short human summary, JSON path, HTML path, top findings, safety note. Never imply that cleanup was performed.
Minimum confirmations before execution:
- Target drive, for example `C:` or `D:`.
- Fast index / administrator permission: yes or no.
- Scan scope: quick or full.
- Output format: JSON, HTML, or both.
## Report Sections
- Summary cards: used/free space, largest directory, number of high-priority suggestions.
- SpaceSniffer section: interactive treemap with drill-down, breadcrumbs, search, and detail panel.
- Diagnostic details: large files, user paths, AppData, C: drive apps/games, file extensions.
- Advice tutorials: each recommendation expands into step-by-step safe handling guidance.
- Scan metadata: mode, permissions, MFT state, errors, and raw JSON summary.
## Safety
The tool is read-only for user data. Do not delete files, uninstall applications, clean the registry, or modify user data as part of this skill.
Never present a recommendation as an automatic cleanup action. Use language like "建议检查", "可以考虑", and "请先确认" for uncertain items.
Binary file not shown.
+595
View File
@@ -0,0 +1,595 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Disk Master Report</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--dm-blue: #3266ad;
--dm-green: #3B6D11;
--dm-amber: #BA7517;
--dm-red: #A32D2D;
--dm-gray: #73726c;
--dm-bg: var(--color-background-primary, #ffffff);
--dm-bg-2: var(--color-background-secondary, #f7f7f5);
--dm-text: var(--color-text-primary, #22221f);
--dm-text-2: var(--color-text-secondary, #5f5e58);
--dm-text-3: var(--color-text-tertiary, #888780);
--dm-border: var(--color-border-tertiary, rgba(120, 119, 111, .28));
--dm-radius: var(--border-radius-md, 10px);
--dm-radius-lg: var(--border-radius-lg, 14px);
}
body { font-family: var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); color: var(--dm-text); background: transparent; }
.report { max-width: 1180px; margin: 0 auto; padding: 1.5rem 0 2rem; }
.report-header { padding: 1.5rem 0 1.25rem; border-bottom: .5px solid var(--dm-border); margin-bottom: 1.25rem; }
.header-meta { font-size: 11px; letter-spacing: .08em; color: var(--dm-text-3); text-transform: uppercase; margin-bottom: .5rem; }
.header-title { font-size: 24px; line-height: 1.2; font-weight: 500; color: var(--dm-text); margin-bottom: .3rem; }
.header-sub { font-size: 14px; color: var(--dm-text-2); }
.badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 1rem; }
.badge { font-size: 11px; padding: 3px 10px; border-radius: var(--dm-radius); border: .5px solid var(--dm-border); color: var(--dm-text-2); background: transparent; }
.badge.blue { background: #E6F1FB; color: #185FA5; border-color: #B5D4F4; }
.badge.green { background: #EAF3DE; color: #3B6D11; border-color: #C0DD97; }
.badge.amber { background: #FAEEDA; color: #854F0B; border-color: #E1B76A; }
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 1.25rem; }
.metric { background: var(--dm-bg-2); border-radius: var(--dm-radius); padding: 1rem; min-width: 0; }
.metric-label { font-size: 12px; color: var(--dm-text-2); margin-bottom: 6px; }
.metric-value { font-size: 22px; font-weight: 500; color: var(--dm-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.metric-note { font-size: 12px; color: var(--dm-text-3); margin-top: 4px; }
.highlights { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1.25rem; }
.highlight { border-left: 3px solid; background: var(--dm-bg-2); border-radius: 0 var(--dm-radius) var(--dm-radius) 0; padding: .75rem 1rem; }
.hl-label { font-size: 11px; color: var(--dm-text-3); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 4px; }
.hl-value { font-size: 15px; font-weight: 500; color: var(--dm-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hl-note { font-size: 12px; color: var(--dm-text-2); margin-top: 2px; }
.panel { display: block; margin-top: 1.5rem; }
.info-card { background: var(--dm-bg-2); border: .5px solid var(--dm-border); border-radius: var(--dm-radius-lg); padding: 1rem 1.15rem; margin-bottom: 1.25rem; }
.info-title { font-size: 14px; font-weight: 500; color: var(--dm-text); margin-bottom: .4rem; }
.info-text { font-size: 13px; line-height: 1.65; color: var(--dm-text-2); }
.evaluation { display: grid; grid-template-columns: minmax(0, .75fr) minmax(0, 1.25fr); gap: 1rem; align-items: stretch; margin-bottom: 1.25rem; }
.score-card { border-left: 4px solid var(--dm-blue); }
.score-value { font-size: 34px; line-height: 1; font-weight: 500; color: var(--dm-text); margin: .5rem 0 .35rem; }
.score-label { font-size: 13px; color: var(--dm-text-2); }
.section-title { font-size: 13px; font-weight: 500; color: var(--dm-text-2); letter-spacing: .06em; text-transform: uppercase; padding-bottom: .5rem; border-bottom: .5px solid var(--dm-border); margin-bottom: 1rem; }
.card { background: var(--dm-bg); border: .5px solid var(--dm-border); border-radius: var(--dm-radius-lg); padding: 1rem 1.15rem; }
.two-col { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(280px, .65fr); gap: 1rem; }
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: .75rem; align-items: center; }
.input, .select { border: .5px solid var(--dm-border); border-radius: var(--dm-radius); background: var(--dm-bg); color: var(--dm-text); padding: .45rem .6rem; font: inherit; font-size: 12px; min-height: 32px; }
.input { flex: 1; min-width: 180px; }
.button { border: .5px solid var(--dm-border); border-radius: var(--dm-radius); background: var(--dm-bg-2); color: var(--dm-text-2); padding: .45rem .65rem; font: inherit; font-size: 12px; cursor: pointer; }
.button:hover { color: var(--dm-text); border-color: var(--dm-blue); }
#treemap { width: 100%; height: 520px; border-radius: var(--dm-radius); overflow: hidden; background: var(--dm-bg-2); position: relative; }
.crumbs { display: flex; flex-wrap: wrap; gap: 4px; margin: .75rem 0; font-size: 12px; color: var(--dm-text-2); }
.crumb { cursor: pointer; padding: 2px 6px; border-radius: 6px; background: var(--dm-bg-2); }
.node { position: absolute; border: 1px solid rgba(255,255,255,.75); overflow: hidden; cursor: pointer; border-radius: 4px; padding: 7px; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.55); z-index: 1; transition: outline-color .15s ease, opacity .15s ease; }
.node::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,.34), rgba(0,0,0,.05) 46%, rgba(0,0,0,.18)); pointer-events: none; }
.node:hover { outline: 2px solid rgba(50,102,173,.8); z-index: 3; }
.node.pruned { box-shadow: inset 0 0 0 2px rgba(255,255,255,.28); background-image: repeating-linear-gradient(135deg, rgba(255,255,255,.16) 0 8px, transparent 8px 16px); }
.node.search-hit { outline: 3px solid #F2C14E; z-index: 4; }
.node-name, .node-size, .node-hint { position: relative; z-index: 1; }
.node-name { font-size: 12px; line-height: 1.22; font-weight: 650; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; color: #fff; }
.node-size { font-size: 11px; line-height: 1.25; opacity: .96; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: rgba(255,255,255,.94); }
.node-hint { position: absolute; left: 7px; right: 7px; bottom: 6px; font-size: 10px; line-height: 1.2; color: rgba(255,255,255,.86); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.detail-list { display: grid; gap: 8px; }
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: .45rem 0; border-bottom: .5px solid var(--dm-border); }
.row:last-child { border-bottom: 0; }
.row-main { min-width: 0; }
.row-title { font-size: 13px; color: var(--dm-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-sub { font-size: 12px; color: var(--dm-text-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-wrap { height: 6px; background: var(--dm-bg-2); border-radius: 999px; overflow: hidden; margin-top: 6px; }
.bar { height: 100%; border-radius: 999px; background: var(--dm-blue); }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; color: var(--dm-text-3); font-weight: 500; border-bottom: .5px solid var(--dm-border); padding: .5rem .35rem; }
td { border-bottom: .5px solid var(--dm-border); padding: .5rem .35rem; color: var(--dm-text-2); vertical-align: top; }
td strong { color: var(--dm-text); font-weight: 500; }
.path { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; word-break: break-all; }
.advice { border: .5px solid var(--dm-border); border-radius: var(--dm-radius-lg); margin-bottom: .75rem; overflow: hidden; }
.advice-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start; padding: .85rem 1rem; cursor: pointer; background: var(--dm-bg); }
.priority { font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--dm-bg-2); color: var(--dm-text-2); }
.priority.p1 { background: #FAE0DC; color: var(--dm-red); }
.priority.p2 { background: #FAEEDA; color: var(--dm-amber); }
.advice-body { display: none; padding: 0 1rem 1rem 1rem; color: var(--dm-text-2); font-size: 13px; }
.advice.open .advice-body { display: block; }
.steps { margin-top: .75rem; display: grid; gap: .5rem; }
.step { background: var(--dm-bg-2); border-radius: var(--dm-radius); padding: .65rem .75rem; }
.step strong { color: var(--dm-text); font-weight: 500; }
pre { white-space: pre-wrap; word-break: break-word; background: var(--dm-bg-2); border-radius: var(--dm-radius); padding: 1rem; font-size: 11px; color: var(--dm-text-2); max-height: 520px; overflow: auto; }
.footer { padding-top: 1.25rem; border-top: .5px solid var(--dm-border); display: flex; justify-content: space-between; gap: 1rem; color: var(--dm-text-3); font-size: 11px; margin-top: 1.5rem; }
.loading { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(255,255,255,.72); backdrop-filter: blur(8px); z-index: 9999; color: var(--dm-text-2); }
.loading.hidden { display: none; }
.spinner { width: 28px; height: 28px; border-radius: 999px; border: 2px solid rgba(50,102,173,.18); border-top-color: var(--dm-blue); animation: spin .8s linear infinite; margin: 0 auto .75rem; }
.loading-text { font-size: 13px; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 820px) {
.metrics, .highlights, .grid-2, .grid-3, .two-col, .evaluation { grid-template-columns: 1fr; }
#treemap { height: 420px; }
}
</style>
</head>
<body>
<div class="loading" id="loading"><div><div class="spinner"></div><div class="loading-text">正在生成交互式磁盘报告...</div></div></div>
<main class="report">
<section class="report-header">
<div class="header-meta">Disk Master · Windows Disk Diagnostic</div>
<h1 class="header-title" id="report-title">磁盘空间诊断报告</h1>
<div class="header-sub" id="report-sub">只读扫描 · 用于空间占用地图和系统盘诊断</div>
<div class="badges" id="badges"></div>
</section>
<section class="metrics" id="metrics"></section>
<section class="highlights" id="highlights"></section>
<section class="evaluation" id="disk-evaluation"></section>
<section class="info-card" id="skill-about"></section>
<section class="panel active" id="tab-overview">
<div class="grid-2">
<div class="card"><div class="section-title">Top 目录</div><div id="overview-dirs" class="detail-list"></div></div>
<div class="card"><div class="section-title">文件类型分布</div><div id="overview-ext" class="detail-list"></div></div>
</div>
</section>
<section class="panel" id="tab-sniffer">
<div class="two-col">
<div class="card">
<div class="section-title">空间占用地图</div>
<div class="toolbar">
<input class="input" id="tree-search" placeholder="在当前层级搜索并高亮">
<select class="select" id="detail-level"><option value="350">简略视图</option><option value="800" selected>标准视图</option><option value="1600">更多细节</option></select>
<button class="button" id="tree-reset">回到根目录</button>
</div>
<div class="row-sub" style="margin-bottom:.65rem">为保证每个可见块可读,过小或低占比子项会聚合为“其他子项”;真实大小仍保留在详情和统计中。</div>
<div class="crumbs" id="crumbs"></div>
<div id="treemap" role="img" aria-label="磁盘空间 Treemap"></div>
</div>
<aside class="card">
<div class="section-title">当前选中</div>
<div id="node-detail" class="detail-list"></div>
</aside>
</div>
</section>
<section class="panel" id="tab-details">
<div class="grid-2">
<div class="card"><div class="section-title">大文件</div><div id="large-files"></div></div>
<div class="card"><div class="section-title">用户目录</div><div id="user-paths"></div></div>
<div class="card"><div class="section-title">AppData 热点</div><div id="appdata"></div></div>
<div class="card"><div class="section-title">C 盘应用/游戏</div><div id="apps"></div></div>
</div>
</section>
<section class="panel" id="tab-advice">
<div id="advice-list"></div>
</section>
<section class="panel" id="tab-raw">
<div class="grid-2">
<div class="card"><div class="section-title">扫描状态</div><div id="scan-meta" class="detail-list"></div></div>
<div class="card"><div class="section-title">原始 JSON 摘要</div><pre id="raw-json"></pre></div>
</div>
</section>
<div class="footer"><span>数据来源:disk-master.exe · 只读扫描</span><span>报告不会删除、移动或卸载任何内容</span></div>
</main>
<script>const DATA = %%DATA%%;</script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const fmtBytes = (bytes) => {
const n = Number(bytes || 0);
if (n >= 1024 ** 4) return (n / 1024 ** 4).toFixed(2) + ' TB';
if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(1) + ' GB';
if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB';
if (n >= 1024) return (n / 1024).toFixed(1) + ' KB';
return n + ' B';
};
const pct = (part, total) => total > 0 ? Math.round((part / total) * 1000) / 10 : 0;
const shortPath = (path) => String(path || '').split(/[\\/]/).filter(Boolean).pop() || path || '-';
const colorFor = (node) => {
const p = String(node.data.path || '').toLowerCase();
if (p.includes('temp') || p.includes('cache') || p.includes('log')) return '#BA7517';
if (p.includes('download') || p.includes('steam') || p.includes('game')) return '#3B6D11';
if (p.includes('windows') || p.includes('program files')) return '#73726c';
return '#3266ad';
};
const topItems = (items, n = 8) => [...(items || [])].sort((a,b) => (b.sizeBytes || 0) - (a.sizeBytes || 0)).slice(0, n);
function cloneNode(node) {
return JSON.parse(JSON.stringify(node || {path: DATA.drive || 'root', sizeBytes: 0, children: []}));
}
function normalizeDiskPath(path) {
return String(path || '').replace(/\//g, '\\').replace(/\\+$/g, '').toLowerCase();
}
function displayPathJoin(parent, name) {
const clean = String(parent || '').replace(/\\+$/g, '');
return clean + '\\' + name;
}
function splitDiskPath(path) {
const text = String(path || '').replace(/\//g, '\\').replace(/\\+$/g, '');
const driveMatch = text.match(/^[a-zA-Z]:/);
const drive = driveMatch ? driveMatch[0] + '\\' : '';
const rest = drive ? text.slice(3) : text;
return { drive, parts: rest.split('\\').filter(Boolean) };
}
function findChildByPath(node, path) {
const key = normalizeDiskPath(path);
return (node.children || []).find(c => normalizeDiskPath(c.path) === key);
}
function ensureSyntheticPath(root, targetPath, sizeBytes = 0) {
if (!root || !targetPath) return null;
const rootPath = String(root.path || DATA.drive || '').replace(/\\+$/g, '');
const target = String(targetPath).replace(/\//g, '\\').replace(/\\+$/g, '');
if (!normalizeDiskPath(target).startsWith(normalizeDiskPath(rootPath))) return null;
const relative = target.slice(rootPath.length).replace(/^\\+/, '');
const parts = relative.split('\\').filter(Boolean);
let current = root;
let currentPath = rootPath;
for (const part of parts) {
current.children = current.children || [];
currentPath = displayPathJoin(currentPath, part);
let child = findChildByPath(current, currentPath);
if (!child) {
child = { path: currentPath, sizeBytes: 0, depth: (current.depth || 0) + 1, children: [], synthetic: true };
current.children.push(child);
}
if (sizeBytes && child.sizeBytes < sizeBytes) child.sizeBytes = sizeBytes;
current = child;
}
return current;
}
function enrichFocusTree(root) {
if (!root) return root;
const focusPaths = [];
(DATA.userPaths || []).forEach(p => focusPaths.push({ path: p.path, sizeBytes: p.sizeBytes || 0 }));
(DATA.appData || []).forEach(a => focusPaths.push({ path: a.path, sizeBytes: a.sizeBytes || 0 }));
focusPaths.forEach(item => ensureSyntheticPath(root, item.path, item.sizeBytes));
normalizeSyntheticSizes(root);
return root;
}
function normalizeSyntheticSizes(node) {
const childSum = (node.children || []).reduce((sum, child) => sum + normalizeSyntheticSizes(child), 0);
if (childSum > 0 && (!node.sizeBytes || node.synthetic || node.sizeBytes < childSum)) node.sizeBytes = childSum;
return node.sizeBytes || 0;
}
const RAW_ROOT = enrichFocusTree(cloneNode(DATA.dirTree && DATA.dirTree[0]));
const PATH_INDEX = new Map();
const ALLOW_HINTS = ['users', 'appdata', 'local', 'roaming', 'downloads', 'desktop', 'documents', 'pictures', 'videos', 'program files', 'programdata', 'steam', 'docker', 'virtualbox', 'vmware', 'temp'];
const DENSE_HINTS = ['node_modules', '.git', 'winsxs', 'installer', 'package cache', 'cache', 'caches', 'overlay2', '.pnpm-store', '.npm', '.yarn'];
const DETAIL_PRESETS = {
350: { limit: 45, minRatio: 0.010, maxShare: 0.72 },
800: { limit: 80, minRatio: 0.005, maxShare: 0.66 },
1600: { limit: 140, minRatio: 0.0025, maxShare: 0.60 }
};
const MIN_TILE_WIDTH = 112;
const MIN_TILE_HEIGHT = 64;
const MIN_TILE_AREA = MIN_TILE_WIDTH * MIN_TILE_HEIGHT;
const VIRTUAL_INDEX = new Map();
function indexTree(node) {
if (!node || !node.path) return;
PATH_INDEX.set(String(node.path).toLowerCase(), node);
(node.children || []).forEach(indexTree);
}
indexTree(RAW_ROOT);
function hasHint(path, hints) {
const text = String(path || '').toLowerCase();
return hints.some(h => text.includes(h));
}
function visualLimit(node, budget) {
const children = node.children || [];
const parentSize = Math.max(1, node.sizeBytes || 1);
const ordered = topItems(children, Math.min(children.length, 8));
const top5 = ordered.slice(0, 5).reduce((s, c) => s + (c.sizeBytes || 0), 0);
const top5Ratio = pct(top5, parentSize);
const fragmented = children.length > 180 && top5Ratio < 45;
let limit = (DETAIL_PRESETS[budget] || DETAIL_PRESETS[800]).limit;
if (hasHint(node.path, ALLOW_HINTS)) limit += 40;
if (hasHint(node.path, DENSE_HINTS)) limit -= 25;
if (fragmented) limit -= 35;
return Math.max(12, Math.min(limit, 180));
}
function visibleTileCapacity(width, height) {
return Math.max(4, Math.floor((width * height) / (MIN_TILE_AREA * 1.35)));
}
function makeTreemapView(rootNode, budget, width, height) {
VIRTUAL_INDEX.clear();
if (rootNode && rootNode.pruned) VIRTUAL_INDEX.set(String(rootNode.path || '').toLowerCase(), rootNode);
const children = topItems(rootNode.children || [], Number.MAX_SAFE_INTEGER);
const preset = DETAIL_PRESETS[budget] || DETAIL_PRESETS[800];
const parentSize = Math.max(1, rootNode.sizeBytes || 1);
const capacity = visibleTileCapacity(width, height);
const limit = Math.min(visualLimit(rootNode, budget), capacity);
const visibleMap = new Map();
children.slice(0, limit).forEach(c => visibleMap.set(c.path, c));
// Allow-list paths are not hidden by the first visual cut; they stay visible
// for one more click level and their own children are judged again there.
children.filter(c => hasHint(c.path, ALLOW_HINTS)).forEach(c => visibleMap.set(c.path, c));
let visible = topItems([...visibleMap.values()], Math.max(limit, visibleMap.size));
if (visible.length > limit) {
const allowed = visible.filter(c => hasHint(c.path, ALLOW_HINTS));
const normal = visible.filter(c => !hasHint(c.path, ALLOW_HINTS));
visible = [...topItems(allowed, Math.min(allowed.length, Math.ceil(limit * 0.4))), ...topItems(normal, Math.max(0, limit - Math.min(allowed.length, Math.ceil(limit * 0.4))))];
}
const visiblePaths = new Set(visible.map(c => c.path));
let hidden = children.filter(c => !visiblePaths.has(c.path));
const areaRatioFloor = MIN_TILE_AREA / Math.max(1, width * height);
const minDisplaySize = parentSize * Math.max(preset.minRatio, areaRatioFloor * 1.2);
const maxDisplaySize = parentSize * preset.maxShare;
const smallVisible = visible.filter(c => (c.sizeBytes || 0) < minDisplaySize && !hasHint(c.path, ALLOW_HINTS));
if (smallVisible.length > 0) {
const smallPaths = new Set(smallVisible.map(c => c.path));
hidden = hidden.concat(smallVisible);
visible = visible.filter(c => !smallPaths.has(c.path));
}
const mapped = visible.map(c => {
const displaySizeBytes = Math.max(Math.min(c.sizeBytes || 0, maxDisplaySize), Math.min(minDisplaySize, c.sizeBytes || 0));
return { path: c.path, sizeBytes: c.sizeBytes, displaySizeBytes, childCount: (c.children || []).length, hasChildren: (c.children || []).length > 0, isDenseHint: hasHint(c.path, DENSE_HINTS), isAllowHint: hasHint(c.path, ALLOW_HINTS) };
});
if (hidden.length > 0) {
const hiddenBytes = hidden.reduce((s, c) => s + (c.sizeBytes || 0), 0);
if (hiddenBytes <= 0) return { path: rootNode.path, sizeBytes: rootNode.sizeBytes, displaySizeBytes: rootNode.sizeBytes, children: mapped };
const virtualPath = rootNode.path + ' :: 其他子项';
const virtualNode = {
path: virtualPath,
sizeBytes: hiddenBytes,
childCount: hidden.length,
pruned: true,
hasChildren: hidden.length > 0,
pruneReason: '子项数量较多,已聚合低占比项目',
children: hidden
};
VIRTUAL_INDEX.set(virtualPath.toLowerCase(), virtualNode);
mapped.push({
path: virtualPath,
sizeBytes: hiddenBytes,
displaySizeBytes: Math.max(Math.min(hiddenBytes, maxDisplaySize), minDisplaySize),
childCount: hidden.length,
pruned: true,
hasChildren: hidden.length > 0,
pruneReason: '子项数量较多,已聚合低占比项目'
});
}
return { path: rootNode.path, sizeBytes: rootNode.sizeBytes, displaySizeBytes: rootNode.sizeBytes, children: mapped };
}
function rawOrVirtual(path) {
const key = String(path || '').toLowerCase();
return VIRTUAL_INDEX.get(key) || PATH_INDEX.get(key);
}
function layoutValue(d) {
return d.children ? 0 : (d.displaySizeBytes || d.sizeBytes || 0);
}
function findPathToNode(root, targetPath, acc = []) {
if (!root) return null;
const next = [...acc, root];
if (root.path === targetPath) return next;
for (const child of root.children || []) {
const found = findPathToNode(child, targetPath, next);
if (found) return found;
}
return null;
}
function row(title, sub, value, ratio, color = '#3266ad') {
return `<div class="row"><div class="row-main"><div class="row-title" title="${title}">${title}</div><div class="row-sub" title="${sub || ''}">${sub || ''}</div>${ratio != null ? `<div class="bar-wrap"><div class="bar" style="width:${Math.max(1, Math.min(100, ratio))}%; background:${color}"></div></div>` : ''}</div><strong>${value || ''}</strong></div>`;
}
function renderHeader() {
document.getElementById('report-title').textContent = `${DATA.drive || '磁盘'} 空间诊断报告`;
document.getElementById('report-sub').textContent = `${DATA.scanTime || ''} · ${DATA.scanMode || 'unknown'} · ${DATA.scanScope || 'unknown'}`;
const badges = [
`<span class="badge blue">${DATA.drive || '-'}</span>`,
`<span class="badge ${DATA.isAdmin ? 'green' : 'amber'}">${DATA.isAdmin ? '管理员' : '普通权限'}</span>`,
`<span class="badge ${DATA.scanMode === 'mft' ? 'green' : 'blue'}">${DATA.scanMode || '-'}</span>`,
`<span class="badge">只读扫描</span>`
];
document.getElementById('badges').innerHTML = badges.join('');
}
function renderMetrics() {
const disk = DATA.disk || {};
const largestDir = topItems(DATA.topDirs, 1)[0];
const highAdvice = (DATA.advice || []).filter(a => a.priority === 1).length;
const cards = [
['已用空间', fmtBytes(disk.usedBytes), `${pct(disk.usedBytes, disk.totalBytes)}% used`],
['剩余空间', fmtBytes(disk.freeBytes), `${Number(disk.freePercent || 0).toFixed(1)}% free`],
['最大目录', largestDir ? shortPath(largestDir.path) : '-', largestDir ? fmtBytes(largestDir.sizeBytes) : '-'],
['建议数量', `${(DATA.advice || []).length} 条`, `${highAdvice} 条高优先级`]
];
document.getElementById('metrics').innerHTML = cards.map(c => `<div class="metric"><div class="metric-label">${c[0]}</div><div class="metric-value">${c[1]}</div><div class="metric-note">${c[2]}</div></div>`).join('');
}
function renderHighlights() {
const dirs = topItems(DATA.topDirs, 1)[0];
const movable = (DATA.advice || []).find(a => a.type === 'Movable');
const clean = (DATA.advice || []).find(a => a.type === 'Cleanable');
const cards = [
['最大占用来源', dirs ? shortPath(dirs.path) : '-', dirs ? fmtBytes(dirs.sizeBytes) : '暂无数据', '#3266ad'],
['可移动候选', movable ? movable.target : '暂无高置信候选', movable ? fmtBytes(movable.sizeMB * 1024 * 1024) : '保持观察', '#3B6D11'],
['可清理/需检查', clean ? clean.target : '暂无明显项', clean ? fmtBytes(clean.sizeMB * 1024 * 1024) : '建议复扫确认', '#BA7517']
];
document.getElementById('highlights').innerHTML = cards.map(c => `<div class="highlight" style="border-color:${c[3]}"><div class="hl-label">${c[0]}</div><div class="hl-value" title="${c[1]}">${c[1]}</div><div class="hl-note">${c[2]}</div></div>`).join('');
}
function evaluateDiskUsage() {
const disk = DATA.disk || {};
const isSystemDrive = String(DATA.drive || '').toLowerCase().startsWith('c:');
const advice = DATA.advice || [];
const high = advice.filter(a => a.priority === 1).length;
const medium = advice.filter(a => a.priority === 2).length;
const optimizableMb = advice.filter(a => ['Movable', 'Cleanable', 'Redirect'].includes(a.type)).reduce((s, a) => s + Number(a.sizeMB || 0), 0);
const usedMb = Number(disk.usedBytes || 0) / 1024 / 1024;
const optimizableRatio = usedMb > 0 ? optimizableMb / usedMb : 0;
let score = 100;
if ((disk.freePercent || 0) < 15) score -= 30;
else if ((disk.freePercent || 0) < 25) score -= 15;
score -= Math.min(30, high * 10 + medium * 4);
score -= Math.min(20, optimizableRatio * 100);
if (isSystemDrive) {
const redirected = (DATA.userPaths || []).filter(p => ['Downloads','Desktop','Documents'].includes(p.name) && !p.isOnCDrive).length;
score += redirected * 3;
}
score = Math.max(0, Math.min(100, Math.round(score)));
let label = '使用结构健康';
let color = '#3B6D11';
let note = isSystemDrive ? '系统盘可优化项较少,用户数据和缓存压力可控。' : '数据盘没有明显系统盘风险,重点关注大文件和长期未用内容即可。';
if (score < 55) { label = '需要优先优化'; color = '#A32D2D'; note = isSystemDrive ? '系统盘存在较多可移动或可清理项,建议先处理高优先级建议。' : '该盘存在明显大文件或可归档内容,建议检查 Top 目录和大文件。'; }
else if (score < 75) { label = '有优化空间'; color = '#BA7517'; note = isSystemDrive ? '系统盘仍有可迁移或可清理项,建议逐项确认。' : '数据盘整体可用,但有部分目录值得整理或归档。'; }
return {score, label, color, note, isSystemDrive, optimizableMb, high};
}
function renderEvaluation() {
const ev = evaluateDiskUsage();
document.getElementById('disk-evaluation').innerHTML = `
<div class="card score-card" style="border-left-color:${ev.color}">
<div class="section-title">磁盘使用合理性</div>
<div class="score-value">${ev.score}</div>
<div class="score-label">${ev.label}</div>
<div class="row-sub" style="margin-top:.5rem">${ev.note}</div>
</div>
<div class="card">
<div class="section-title">判断依据</div>
${row('磁盘类型', '', ev.isSystemDrive ? '系统盘' : '数据盘', null)}
${row('可优化项规模', 'Movable / Cleanable / Redirect 建议合计', fmtBytes(ev.optimizableMb * 1024 * 1024), null, '#BA7517')}
${row('高优先级建议', '越少说明当前使用越合理', `${ev.high} 条`, null, ev.high ? '#A32D2D' : '#3B6D11')}
</div>`;
}
function renderSkillAbout() {
document.getElementById('skill-about').innerHTML = `<div class="info-title">这个报告在做什么</div><div class="info-text">Disk Master 是一个只读磁盘诊断 Skill:CLI 负责快速采集磁盘、应用、AppData、用户目录和建议数据;报告负责把这些数据组织成空间地图、详细诊断和安全教程。它不会删除、移动、卸载或修改你的文件,所有“可清理/可移动”内容都需要你确认后手动处理。</div>`;
}
function renderOverview() {
const total = (DATA.dirTree && DATA.dirTree[0] && DATA.dirTree[0].sizeBytes) || (DATA.disk || {}).usedBytes || 1;
document.getElementById('overview-dirs').innerHTML = topItems(DATA.topDirs, 10).map(d => row(shortPath(d.path), d.path, fmtBytes(d.sizeBytes), pct(d.sizeBytes, total))).join('') || '暂无目录数据';
const ext = Object.entries(DATA.extStats || {}).map(([name, v]) => ({name, ...v})).sort((a,b) => b.totalBytes - a.totalBytes).slice(0, 10);
document.getElementById('overview-ext').innerHTML = ext.map(e => row(e.name, `${e.count} 个文件`, fmtBytes(e.totalBytes), pct(e.totalBytes, total), '#73726c')).join('') || '暂无扩展名数据';
}
let currentRoot;
function renderTreemap(rootNode) {
showLoading('正在绘制空间占用地图...');
currentRoot = rootNode;
const container = document.getElementById('treemap');
container.innerHTML = '';
const w = container.clientWidth, h = container.clientHeight;
const viewRoot = makeTreemapView(rootNode, Number(document.getElementById('detail-level').value), w, h);
const root = d3.hierarchy(viewRoot).sum(layoutValue).sort((a,b)=>b.value-a.value);
d3.treemap().tile(d3.treemapSquarify.ratio(1.25)).size([w,h]).paddingInner(3).paddingOuter(2).round(true)(root);
const nodes = (root.children || []).filter(d => d.x1 > d.x0 && d.y1 > d.y0);
for (const d of nodes) {
const left = Math.max(0, Math.min(w, d.x0));
const tileTop = Math.max(0, Math.min(h, d.y0));
const right = Math.max(left, Math.min(w, d.x1));
const bottom = Math.max(tileTop, Math.min(h, d.y1));
const tileWidth = Math.max(0, right - left);
const tileHeight = Math.max(0, bottom - tileTop);
if (tileWidth < 4 || tileHeight < 4) continue;
const el = document.createElement('div');
el.className = 'node' + (d.data.pruned || d.data.isDenseHint ? ' pruned' : '');
el.dataset.path = d.data.path || '';
el.dataset.name = shortPath(d.data.path || '');
el.style.left = left + 'px'; el.style.top = tileTop + 'px';
el.style.width = tileWidth + 'px'; el.style.height = tileHeight + 'px';
el.style.background = colorFor(d);
el.title = `${d.data.path}\n实际大小:${fmtBytes(d.data.sizeBytes)}${d.data.displaySizeBytes !== d.data.sizeBytes ? '\n显示面积已做可读性调整' : ''}${d.data.pruned ? '\n' + d.data.pruneReason + '\n点击展开这些折叠子项' : ''}`;
if (tileWidth >= 72 && tileHeight >= 38) {
const hint = d.data.pruned && tileHeight >= 86 ? '<div class="node-hint">点击展开折叠项</div>' : '';
el.innerHTML = `<div class="node-name">${shortPath(d.data.path)}</div><div class="node-size">${fmtBytes(d.data.sizeBytes)}${d.data.childCount ? ` · ${d.data.childCount}项` : ''}</div>${hint}`;
}
el.onclick = () => {
const raw = rawOrVirtual(d.data.path);
if (raw && (raw.children || []).length) renderTreemap(raw); else renderNodeDetail(d.data);
};
container.appendChild(el);
}
renderCrumbs(rootNode);
renderNodeDetail(rootNode);
requestAnimationFrame(() => hideLoading());
}
function renderCrumbs(node) {
let path = findPathToNode(RAW_ROOT, node.path);
if (!path && node.pruned) {
const parentPath = String(node.path || '').split(' :: ')[0];
path = findPathToNode(RAW_ROOT, parentPath);
if (path) path = [...path, node];
}
path = path || [RAW_ROOT].filter(Boolean);
document.getElementById('crumbs').innerHTML = path.map((item, index) => `<span class="crumb" data-path="${item.path}">${index === 0 ? (DATA.drive || 'root') : shortPath(item.path)}</span>`).join('<span>›</span>');
document.querySelectorAll('#crumbs .crumb').forEach(el => el.onclick = () => {
const raw = rawOrVirtual(el.dataset.path);
if (raw) renderTreemap(raw);
});
}
function renderNodeDetail(node) {
const children = topItems(node.children || [], 12);
const foldInfo = node.pruned ? row('已折叠为虚拟目录', `${node.childCount || 0} 个子项,点击 Treemap 中该块可单独展开查看`, '', null, '#BA7517') : '';
document.getElementById('node-detail').innerHTML = row(shortPath(node.path), node.path, fmtBytes(node.sizeBytes), null) + foldInfo + children.map(c => row(shortPath(c.path), c.path, fmtBytes(c.sizeBytes), pct(c.sizeBytes, node.sizeBytes || 1))).join('');
}
function renderDetails() {
document.getElementById('large-files').innerHTML = `<table><thead><tr><th>文件</th><th>大小</th><th>类型</th></tr></thead><tbody>${topItems(DATA.topFiles, 20).map(f => `<tr><td><strong>${shortPath(f.path)}</strong><div class="path">${f.path}</div></td><td>${fmtBytes(f.sizeBytes)}</td><td>${f.ext || '-'}</td></tr>`).join('')}</tbody></table>`;
document.getElementById('user-paths').innerHTML = (DATA.userPaths || []).map(p => row(p.name, p.path, fmtBytes(p.sizeBytes), null, p.isOnCDrive ? '#BA7517' : '#3B6D11')).join('') || '暂无用户目录数据';
document.getElementById('appdata').innerHTML = topItems(DATA.appData, 20).map(a => row(`${a.area} · ${a.name}`, a.path, fmtBytes(a.sizeBytes), null, '#BA7517')).join('') || '暂无 AppData 数据';
const apps = (DATA.apps || []).filter(a => a.isOnCDrive).slice(0, 30);
document.getElementById('apps').innerHTML = apps.map(a => row(a.name, `${a.source || '-'} · ${a.installLocation || ''}`, a.sizeMB ? `${a.sizeMB.toFixed(1)} MB` : '-', null, a.isGame ? '#3B6D11' : '#3266ad')).join('') || '暂无 C 盘应用数据';
}
function stepsFor(advice) {
if (advice.type === 'Cleanable') return ['确认路径和来源,避免删除业务数据。', '优先使用 Windows 存储感知、磁盘清理或应用内置清理功能。', '不要手动清空不认识的系统目录。', '清理后重新运行 Disk Master 对比空间变化。'];
if (advice.type === 'Movable') return ['确认目标内容不是系统组件。', '在应用设置中查找库目录、缓存目录或数据目录迁移选项。', '迁移到非系统盘后启动应用验证。', '确认无误后再清理旧位置。'];
if (advice.type === 'Redirect') return ['打开对应用户文件夹属性。', '进入“位置”选项卡。', '选择非系统盘目录并应用。', '让 Windows 自动移动现有内容。'];
return ['打开路径检查内容来源。', '确认是否仍在使用。', '必要时归档到非系统盘。', '不确定时不要删除。'];
}
function renderAdvice() {
const items = DATA.advice || [];
document.getElementById('advice-list').innerHTML = items.map((a, i) => `<div class="advice"><div class="advice-head"><span class="priority p${a.priority}">P${a.priority}</span><div><div class="row-title">${a.type} · ${a.target}</div><div class="row-sub">${a.reason}</div></div><strong>${fmtBytes((a.sizeMB || 0) * 1024 * 1024)}</strong></div><div class="advice-body"><div class="path">${a.target}</div><div class="steps">${stepsFor(a).map((s, idx) => `<div class="step"><strong>${idx + 1}. </strong>${s}</div>`).join('')}</div></div></div>`).join('') || '<div class="card">暂无建议。当前扫描未发现明显可移动或可清理候选。</div>';
document.querySelectorAll('.advice-head').forEach(h => h.onclick = () => h.parentElement.classList.toggle('open'));
}
function renderRaw() {
const meta = [
['扫描模式', DATA.scanMode], ['扫描范围', DATA.scanScope], ['完整扫描', DATA.isComplete ? '是' : '否'], ['管理员', DATA.isAdmin ? '是' : '否'], ['MFT 可用', DATA.mft?.available ? '是' : '否'], ['错误', (DATA.errors || []).join('; ') || '无']
];
document.getElementById('scan-meta').innerHTML = meta.map(m => row(m[0], '', m[1], null)).join('');
document.getElementById('raw-json').textContent = JSON.stringify({scanTime: DATA.scanTime, scanMode: DATA.scanMode, disk: DATA.disk, mft: DATA.mft, errors: DATA.errors}, null, 2);
}
function bind() {
document.getElementById('tree-reset').onclick = () => renderTreemap(RAW_ROOT);
document.getElementById('detail-level').onchange = () => renderTreemap(currentRoot || RAW_ROOT);
document.getElementById('tree-search').oninput = (e) => {
const q = e.target.value.trim().toLowerCase();
const nodes = [...document.querySelectorAll('#treemap .node')];
nodes.forEach(n => n.classList.remove('search-hit'));
if (!q) return;
const hits = nodes.filter(n => (n.dataset.path || '').toLowerCase().includes(q) || (n.dataset.name || '').toLowerCase().includes(q));
hits.forEach(n => n.classList.add('search-hit'));
if (hits[0]) {
const raw = rawOrVirtual(hits[0].dataset.path);
renderNodeDetail(raw || {path: hits[0].dataset.path, sizeBytes: 0, children: []});
}
};
}
function showLoading(text) {
const el = document.getElementById('loading');
if (!el) return;
const label = el.querySelector('.loading-text');
if (label && text) label.textContent = text;
el.classList.remove('hidden');
}
function hideLoading() {
const el = document.getElementById('loading');
if (el) el.classList.add('hidden');
}
showLoading('正在生成交互式磁盘报告...');
renderHeader(); renderMetrics(); renderHighlights(); renderEvaluation(); renderSkillAbout(); renderOverview(); renderDetails(); renderAdvice(); renderRaw(); bind();
if (RAW_ROOT) renderTreemap(RAW_ROOT); else hideLoading();
</script>
</body>
</html>
+153
View File
@@ -0,0 +1,153 @@
# everything-agent-find CLI Usage
`everything-agent-find.exe` is a Windows-only JSON CLI for fast file discovery through the Everything SDK IPC interface. It supports Everything 1.4 classic IPC and Everything 1.5 SDK3 named-pipe IPC, including the default `1.5a` alpha instance. It requires a running Everything desktop process.
## Location
```text
bin\everything-agent-find.exe
```
If the executable is not on `PATH`, run it by absolute path:
```powershell
& "<skill-dir>\bin\everything-agent-find.exe" --check
```
## Synopsis
```powershell
everything-agent-find <query> [options]
```
The query uses Everything search syntax unless `--regex` is set.
## Options
| Flag | Description |
| --- | --- |
| `--max <n>` | Maximum returned results, `1..500` (default: `50`) |
| `--files` | Return files only |
| `--folders` | Return folders only |
| `--regex` | Treat the entire query as an Everything SDK regex |
| `--sort <type>` | `name`, `path`, `size`, or `modified` (default: `name`) |
| `--desc` | Sort descending |
| `--workspace <dir>` | Only return results under this directory |
| `--relative` | Include `relativePath` for results inside `--workspace` |
| `--exclude <text>` | Exclude paths containing this text, case-insensitive; repeatable |
| `--include-hidden` | Include hidden and system paths |
| `--debug-query` | Include `query` and `effectiveQuery` in the output |
| `--limit-scan <n>` | Maximum raw Everything results to scan, `1..1000000` (default: `20000`) |
| `--instance <name>` | Force an Everything 1.5 SDK3 instance name, for example `1.5a`; default is auto-detect |
| `--check` | Test Everything IPC connectivity and exit |
| `--version` | Print human-readable CLI and SDK version information and exit |
| `--dll <path>` | Override the path to `Everything64.dll` |
| `--dll3 <path>` | Override the path to `Everything3_x64.dll` |
| `--json` | Accepted as a no-op; JSON is always used |
| `--help` | Show usage help |
`--files` and `--folders` are mutually exclusive.
## Examples
```powershell
# Check IPC availability
everything-agent-find --check
# Print version information
everything-agent-find --version
# Find TypeScript files inside a workspace
everything-agent-find "*.ts" --workspace D:\project --files --relative --max 30
# Exclude common noisy directories
everything-agent-find "*.tsx" --workspace D:\project --files --relative --exclude node_modules --exclude .git --exclude dist --max 30
# Find folders named tests
everything-agent-find "tests" --workspace D:\project --folders --relative --max 20
# Sort by recently modified files
everything-agent-find "ext:md dm:thisweek" --workspace D:\project --files --sort modified --desc --relative
# Regex mode
everything-agent-find "^test_.*\.ts$" --regex --workspace D:\project --files --relative --max 20
# Debug the final query sent to Everything
everything-agent-find "*.rs" --workspace D:\repo --files --debug-query
```
## Workspace Behavior
For non-regex searches, `--workspace` is pushed into the Everything query as a path constraint and is also verified locally against returned absolute paths.
For `--regex`, the CLI preserves the regex exactly and filters paged results locally. This avoids changing regex semantics while still preventing out-of-workspace paths from being returned.
## Output
Successful searches print JSON to stdout:
```json
{
"results": [
{
"path": "D:\\project\\src\\main.ts",
"type": "file",
"size": 1234,
"modified": 1715702400000,
"relativePath": "src\\main.ts"
}
],
"ok": true,
"backend": "sdk3",
"instance": "1.5a",
"count": 1,
"scanned": 12,
"truncated": false,
"elapsedMs": 12
}
```
Fields:
| Field | Meaning |
| --- | --- |
| `ok` | Whether the command succeeded |
| `backend` | `sdk3` for Everything 1.5 named-pipe IPC, or `sdk2` for Everything 1.4 classic IPC |
| `instance` | SDK3 instance name, usually `1.5a`; omitted for SDK2 |
| `results` | Result objects |
| `path` | Full absolute path |
| `relativePath` | Path relative to `--workspace`; only emitted with `--relative` |
| `type` | `file`, `folder`, or `volume` |
| `size` | File size in bytes, or `-1` if unavailable |
| `modified` | Unix epoch milliseconds for the modified timestamp |
| `count` | Number of returned results |
| `scanned` | Raw Everything results inspected before filters |
| `truncated` | `true` when a successful search stopped at `--max` or `--limit-scan` |
| `elapsedMs` | CLI runtime in milliseconds |
| `query` | Original query, only with `--debug-query` |
| `effectiveQuery` | Query sent to Everything, only with `--debug-query` |
Errors are JSON too:
```json
{"ok":false,"error":"ipc_unavailable","message":"Everything IPC not available. Is Everything running with the taskbar icon?","elapsedMs":3}
```
## Notes For Agents
Use `--workspace` whenever the user request is scoped to a project. Use `--relative` for workspace searches so summaries can refer to concise paths. Keep `--max` small, usually `20..50`. If `truncated` is `true`, ask for a narrower query or raise `--limit-scan` only when necessary.
Use `--check` to verify Everything IPC availability. The CLI tries SDK3 first with the main unnamed instance and then `1.5a`; if SDK3 is unavailable it falls back to SDK2 classic IPC. `--version` prints human-readable terminal output and only verifies that the CLI can load the SDK DLLs and report versions; it is not a connectivity check and is not JSON.
Use `--instance <name>` only when you need to force a specific Everything 1.5 instance. Without it, auto-detection handles the common Everything 1.4 and Everything 1.5a cases.
Use `--exclude` for noisy path fragments such as `node_modules`, `.git`, `dist`, `build`, or `.venv`. The exclusion is a case-insensitive substring match against the returned absolute path, not an Everything glob.
Hidden and system paths are excluded by default. Add `--include-hidden` when searching for dotfiles, system-managed files, or hidden project metadata.
## Requirements
- Windows.
- A running Everything desktop process with IPC available.
- For Everything 1.5/1.5a: `Everything3_x64.dll` beside the executable, or `EVERYTHING3_SDK_DLL` pointing to it.
- For Everything 1.4 classic IPC fallback: `Everything64.dll` beside the executable, or `EVERYTHING_SDK_DLL` pointing to it.
+129
View File
@@ -0,0 +1,129 @@
---
name: everything-file-search
description: >
Windows 上基于 Everything 的超快速文件发现工具。用于按文件名、扩展名或路径模式定位文件和目录,适合磁盘范围大、项目很大或搜索范围不明确的场景;支持 Everything 1.4 classic IPC 和 1.5/1.5a SDK3 named-pipe IPC。只负责文件发现,不负责全文搜索或代码语义搜索。
---
# Everything 文件搜索
本 skill 用于 Windows 上的快速文件发现。它通过 bundled CLI 调用 [Everything](https://www.voidtools.com/) 的官方 SDK IPC,按文件名、路径和 Everything 查询语法定位候选文件或目录。CLI 支持 Everything 1.5/1.5a 的 SDK3 named-pipe IPC,并保留 Everything 1.4 classic IPC fallback。
它不负责:
- 全文搜索
- 代码语义搜索
- 替代 `Grep` / `Read` 的内容验证工作
如果用户要“查内容”或“找某段代码在哪”,且搜索范围很大或候选路径未知,可先用本 skill 缩小候选路径,再用 `Grep` 或 `Read` 验证。若范围很小或目标是明确的函数、类、代码片段,优先直接用 `Grep`。
CLI 的完整参数说明在 [CLI_USAGE.md](CLI_USAGE.md)。
## 何时优先使用
以下情况优先选择本 skill,而不是直接用 `Glob` 或 `rg --files`:
- 用户请求本质是“找文件”“定位路径”“where is”“locate”“search for files”。
- 项目或磁盘范围很大,遍历成本高。
- 用户不确定文件在哪个目录,搜索范围可能跨盘或跨多个项目。
- 需要先产出候选路径,再交给 `Read`、`Grep` 或其他工具继续处理。
## 何时不使用
以下情况不要优先使用本 skill:
- 用户要搜索文件内容、函数名、类名或代码语义。
- 用户只在很小的目录树内找文件,且当前仓库的 `Glob` 已经足够快。
- 用户明确要求全文搜索、正则搜索代码体、或搜索函数实现位置。
## 默认调用策略
agent 使用本 skill 时,默认按以下策略构造命令:
1. 优先调用 skill 内 bundled CLI,而不是假设命令已在 `PATH`:
```powershell
& "<skill-dir>\bin\everything-agent-find.exe" <query> [options]
```
2. 如果不确定 Everything 是否可用,先运行:
```powershell
& "<skill-dir>\bin\everything-agent-find.exe" --check
```
CLI 会自动先尝试 SDK3 main instance 和 `1.5a` instance,再回退 SDK2 classic IPC。只有需要强制指定 Everything 1.5 instance 时才使用 `--instance <name>`。
3. 只要用户请求有明确项目或目录范围,就带 `--workspace`。
4. 默认带 `--relative`,让输出结果在 workspace 内时同时给出 `relativePath`,更适合后续总结和展示。
5. 默认使用 `--files` 或 `--folders`;除非用户明确要两类结果,才同时省略。
6. 保持 `--max` 较小,通常 `20` 到 `50`。
7. 对常见噪声目录,默认建议使用 `--exclude`,例如:
- `node_modules`
- `.git`
- `dist`
- `build`
- `.venv`
8. 如果成功输出中出现 `truncated: true`,优先建议用户缩小查询范围;只在确实需要时再调大 `--limit-scan`。当 `ok: false` 时,先处理错误,不要根据 `truncated` 做范围判断。
## 推荐命令模板
```powershell
# 首选:workspace + files + relative
& "<skill-dir>\bin\everything-agent-find.exe" "*.ts" --workspace D:\project --files --relative --max 30
# 进一步排除噪声
& "<skill-dir>\bin\everything-agent-find.exe" "*.tsx" --workspace D:\project --files --relative --exclude node_modules --exclude .git --exclude dist --max 30
# 查找目录
& "<skill-dir>\bin\everything-agent-find.exe" "tests" --workspace D:\project --folders --relative --max 20
# 最近修改文件
& "<skill-dir>\bin\everything-agent-find.exe" "ext:md dm:thisweek" --workspace D:\project --files --sort modified --desc --relative
# regex 查询
& "<skill-dir>\bin\everything-agent-find.exe" "^test_.*\.ts$" --regex --workspace D:\project --files --relative --max 20
```
## 输出说明
CLI 始终向 stdout 输出 JSON。成功时主要字段包括:
- `ok`
- `results`
- `count`
- `scanned`
- `truncated`
- `elapsedMs`
每条 result 通常包含:
- `path`:绝对路径
- `relativePath`:仅在 `--relative` 且结果位于 `--workspace` 内时出现
- `type`
- `size`
- `modified`
典型错误输出:
```json
{"ok":false,"error":"ipc_unavailable","message":"Everything IPC not available. Is Everything running with the taskbar icon?","elapsedMs":3}
```
## 排查与回退
当返回 `ok: false` 或 `error: "ipc_unavailable"` 时,agent 应:
1. 告诉用户安装 Everything,或打开 Everything 桌面应用。
2. 说明需要运行的是带任务栏图标的用户进程,而不只是 Windows service。
3. 退回 `Glob`、`rg --files` 或 `Get-ChildItem -Recurse`。
4. 如果问题出现在查询构造上,可使用 `--debug-query` 检查实际发送给 Everything 的查询。
5. `--version` 只验证 CLI 和 DLL 可加载;判断 Everything IPC 是否可用必须使用 `--check`。
## 前置条件
- Windows
- 正在运行的 Everything 桌面进程
- Everything 1.5/1.5a:`Everything3_x64.dll` 与 exe 在同一目录,或设置 `EVERYTHING3_SDK_DLL`
- Everything 1.4 classic fallback:`Everything64.dll` 与 exe 在同一目录,或设置 `EVERYTHING_SDK_DLL`
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.
@@ -0,0 +1,138 @@
# Everything Query Syntax Reference
Full Everything search syntax for constructing agent queries.
## Basic operators
| Syntax | Meaning | Example |
|------------------|-------------------------------------------|----------------------|
| `word1 word2` | AND (space-separated words must all match)| `readme .md` |
| `word1|word2` | OR | `*.ts|*.tsx` |
| `!word` | NOT | `!node_modules *.js` |
| `<word1 word2>` | Grouping | `<*.c|*.h> main` |
| `"exact phrase"` | Literal match with spaces | `"my file.txt"` |
## Wildcards
| Syntax | Meaning | Example |
|-----------|----------------------|------------------------|
| `*` | Zero or more chars | `*.png` |
| `?` | Exactly one char | `file?.txt` |
| `#` | Exactly one digit | `img####.jpg` |
| `[abc]` | Character class | `file[0-9].txt` |
## Search functions (prefix with function name and colon)
| Function | Description | Example |
|-----------------|--------------------------------|--------------------------------|
| `ext:` | File extension | `ext:rs` |
| `path:` | Path contains | `path:src\components` |
| `folder:` | Folder name | `folder:node_modules` |
| `parent:` | Parent folder name | `parent:src *.ts` |
| `root:` | Root / drive | `root:C:` |
| `size:` | File size (bytes / KB / MB) | `size:>1mb` |
| `dm:` | Date modified | `dm:today`, `dm:lastweek` |
| `dc:` | Date created | `dc:2024-01-01..2024-12-31` |
| `da:` | Date accessed | `da:thisweek` |
| `attrib:` | File attributes | `attrib:H` (hidden) |
| `dupe:` | Find duplicates | `dupe:size` |
| `regex:` | Regex (Everything flavor) | `regex:^test_.*\.spec\.ts$` |
| `case:` | Case-sensitive search | `case:ReadMe.md` |
| `wholeword:` | Match whole word | `wholeword:main` |
| `child:` | Files inside folder | `child:src *.ts` |
| `type:` | File type | `type:video` |
| `infolder:` | Exact folder path | `infolder:D:\project\src` |
| `depth:` | Folder depth from root | `depth:<=3 *.ts` |
| `count:` | File count in folder | `count:>100` |
| `empty:` | Empty folders | `empty:` |
## Date macros
| Macro | Meaning |
|---------------|--------------------------------------|
| `today` | Today |
| `yesterday` | Yesterday |
| `lastweek` | Previous 7 days |
| `thisweek` | Current week |
| `lastmonth` | Previous month |
| `thismonth` | Current month |
| `lastyear` | Previous year |
| `thisyear` | Current year |
Date ranges: `dm:2025-01-01..2025-06-30` or `dm:>2025-01-01`
## Size macros
| Macro | Value |
|-----------|-------------|
| `tiny` | 0-10 KB |
| `small` | 10-100 KB |
| `medium` | 100 KB-1 MB |
| `large` | 1-16 MB |
| `huge` | 16-128 MB |
| `gigantic`| >128 MB |
Size ranges: `size:>100kb size:<10mb`
## Attribute codes
| Code | Meaning |
|------|-------------|
| `A` | Archive |
| `H` | Hidden |
| `S` | System |
| `R` | Read-only |
| `D` | Directory |
| `N` | Not indexed |
Example: `attrib:!H !S` (not hidden, not system)
## Practical agent query patterns
`regex:` can be used inside normal Everything syntax. The CLI also supports
`--regex`, which treats the whole query string as a regex by calling the
Everything SDK regex mode directly.
The `Query` value is Everything syntax. The `CLI flags` value is passed as
separate command-line options to `everything-agent-find.exe`.
```
# All TypeScript source files modified today
Query: ext:ts dm:today
CLI flags: --files
# Config files named "tsconfig" anywhere
Query: tsconfig ext:json
CLI flags: --files
# React component files in a project
Query: *.tsx *.jsx
CLI flags: --workspace D:\project --files
# Recently changed documentation files
Query: ext:md dm:thisweek
CLI flags: --sort modified --desc
# Find node_modules folders
Query: node_modules
CLI flags: --folders --workspace D:\project
# Find large log files
Query: *.log size:>10mb
CLI flags: --files
# Files with both "test" and "spec" in name
Query: test spec ext:ts
CLI flags: --files
# Regex for TypeScript files whose names start with test_
Query: ^test_.*\.ts$
CLI flags: --regex --files
# Python files NOT in venv directories
Query: ext:py !path:venv
# Files modified between two dates
Query: dc:2026-01-01..2026-05-14 ext:cpp
CLI flags: --files
```
+73
View File
@@ -0,0 +1,73 @@
---
name: teach-me
description: >
Walk the user through something until they actually understand it — layer by
layer, one round at a time, every claim verified before it is taught. Use
when the user asks to be walked through a design, codebase, requirement, or
decision ("讲懂我", "给我讲讲", "teach me"), or wants to understand what
exists from what was built. Also fires on handover from the think-twice
skill after a second miss (a rewrite already failed).
---
# Teach Me
The user is present and can talk back: teaching is a loop, not a lecture. The
goal is their understanding, confirmed by them — never declared by you. An
explainer that re-explains everything, announces the matter settled, and
changes the subject has failed, even if every sentence was true.
The rule files referenced below — `double-check.md`, `self-contain.md`,
`style.md` — are parts of the **think-twice** skill; find them in that
skill's own directory. Where think-twice is not installed, the inline clause
at each pointer carries the rule's core.
## Loop
1. **Map and slice.** Build context from zero — the user starts as a **cold
reader**: assume no earlier names or interfaces remembered. Split the subject into layers, ordered so each
layer stands on the one before it. One layer per round.
2. **Check yourself before you teach.** Every claim in the layer follows
`double-check.md` from the think-twice skill: verify it
(read the code, run the command, open the source) or label it uncertain.
When the stakes warrant, spawn a context-free subagent to blind-check a
claim. Never teach a guess.
3. **Teach one layer.** Settled things are stated directly — not re-opened as
questions. Express under `self-contain.md` from the think-twice skill: every
term glossed at first use, examples drawn from material the conversation
already holds — real files, real numbers, the user's own work — before any
abstraction. Teach the subject, not the method: this skill's own vocabulary
(layers, gates, loop) never appears in what the user reads. Chinese prose
for a human reader also follows `style.md` from the think-twice skill:
plain sentences over bold-label bullets, bold at most twice per paragraph,
no filler connectives, no emoji.
4. **Route real decisions only.** Put to the user only what changes behavior,
scope, or a public interface; digest implementation detail yourself. When a
proper decision interview emerges, hand over to the grilling skill where
installed; otherwise ask the decisions yourself, one per message, each with
a recommended answer.
5. **The user gates the loop.** A round ends with one line — 这段讲了:plus a
one-clause recap — then stop. No question attached, no preview of what
comes next: the user's natural reply is the gate. Advance on "懂了"; stay
on anything else. Never ask "clear?" — it gets a polite yes, and politeness
is not information. On "没懂": first locate the stuck point — one question,
with candidate options so answering is cheap (options belong here and only
here) — then re-teach that same layer from a different angle: a concrete
scenario walked start to end, a contrast with the thing it is most often
confused with, the technical terms reattached last. Never change the subject
yourself — when the user changes it, follow them, and pick the layer back
up when they return.
The final round closes differently: a short summary of everything taught,
conclusion first — not a completion announcement.
## Done when
The user says they understand, or says it is enough. Nothing else ends the
loop — not your own judgement that the matter is settled.
## Out of scope
Multi-session curriculum learning (workspace, lessons, learning records) is
the `teach` skill, where installed. Rewriting one outgoing text is
`think-twice`. This skill is the live loop that gets a present user from
"没懂" to "懂了".
+77
View File
@@ -0,0 +1,77 @@
---
name: think-twice
description: >
Rebuild output so a reader with only this text can understand or act, with
every claim verified. Use when the user doesn't understand your last
output, asks to expand / explain / rewrite plainly / make it
self-contained; when shipping prose that must stand alone (handoff,
PR/issue body, doc, subagent brief); or when another skill needs a clarity
or evidence pass.
---
# Think Twice
Do not polish the last draft with more jargon. Rebuild from **only what will
appear in the output you send**, on two pillars:
- **Self-contained** — the words work alone for the **cold reader**: someone
with this text and only what is guaranteed in front of them. Rules in
[self-contain.md](self-contain.md).
- **Double-checked** — every claim in them is verified or labeled. Rules in
[double-check.md](double-check.md).
Skip trivial one-liners the user already follows.
## Second miss
A rewrite already shipped and they still don't get it, or ask the same
question in other words → invoke the **teach-me** skill (needs a reader who
can talk back); never a third rewrite. Where teach-me is not installed, do the
minimum it would: locate the stuck point with one question, then re-explain
that point through a concrete example.
## Process
1. **Diagnose** — one primary blocker: **concept** | **scope** | **steps**
| **token** (undefined word or dangling reference). Fix the highest one
first. For proactive prose where no one is stuck yet, diagnose the blocker
the reader is most likely to hit.
2. **Three questions**, answered silently: what does the reader have right now
(default: the cold reader)? Can they understand or act from that
alone? Which facts must the text pin — conclusion, scope, evidence, next
step?
3. **Double-check** — verify every claim the text will make, per
[double-check.md](double-check.md).
4. **Rebuild — do not patch.** Substance order: conclusion → what it is in
plain terms → in/out of scope when partial work matters → evidence when
trust matters → minimal path when action matters (success shape; on
failure, the next move, not only an error name). Every line obeys
[self-contain.md](self-contain.md).
5. **Deliver.** Match the shape to the reader and the scene. Only two rules are
fixed: conclusion first, and drop anything that does not change
understanding or action. For a chat reply to a person, short is the
default. This draft shape is scaffolding, not a mould; use it, reshape it,
or discard it:
1. **Takeaway** — 1–3 sentences
2. **Need-to-know** — only terms blocking this message (omit if none)
3. **Next step** — only if boundaries or action matter
The slots name the substance and its order, not literal headings — they can
be plain sentences. Chinese prose for a human reader additionally loads
[style.md](style.md), an optional part.
## Done when
- [ ] Passes the [self-contain.md](self-contain.md) final check
- [ ] Passes the [double-check.md](double-check.md) final check
- [ ] Conclusion first; everything left changes understanding or action
- [ ] Chinese human-facing output: every [style.md](style.md) rule swept, rule
by rule
- [ ] A second miss went to teach-me — or, where it is not installed, to its
minimum — not a third rewrite
## Out of scope
This skill rebuilds the words; tracker/PR mechanics (linking, closing,
labels) belong to whatever process owns them.
+36
View File
@@ -0,0 +1,36 @@
# Double-check — the claims hold
A claim that reaches the reader unverified is a guess wearing a conclusion's
clothes. The first mismatch the reader finds costs the whole text its credit.
## Rules
- **Verified or labeled.** Every claim either carries evidence you actually
checked — code read, command run, file opened, source quoted — or an
explicit uncertainty label ("unverified", "low confidence, no repro").
Never write a guess in a conclusion's voice.
- **Evidence is concrete.** The file and line, the command and its real
output, the exact quote. "I checked" is not evidence.
- **Quotes are evidence.** A decision, constraint, or priority is quoted
verbatim, in its original language, with who said it and when. A paraphrase
is your judgement of their words; presented as fact, it turns a reversible
decision into a law of physics for the reader.
- **Fact vs judgement.** A verifiable fact gets checked. Someone's decision
gets attributed. Keep the two visibly distinct in the text.
- **Pre-commit, then look.** When a claim can be tested (a command, a check),
write the expected result before looking at the actual one. Matching after
the fact is **back-fitting** — the output would have "confirmed" whatever
you wrote.
- **Already-run work is evidence.** Something executed earlier in this session
proved itself then — copy its real output; re-running can repeat side
effects. Pre-commit still applies: write the success condition from the
step's original goal before inspecting that old output, or you are
back-fitting.
- **Blocked legwork is stated, not papered over.** If verification is
impossible from here, write what is unknown, why, and what would resolve
it. That sentence is itself the label.
## Final check
Strip every sentence you could not defend if the reader asked "how do you
know?". What survives is the text.
+34
View File
@@ -0,0 +1,34 @@
# Self-contain — the words work alone
The **cold reader** has only this text, plus whatever is guaranteed to be in
front of them at the moment they read it. Every reference must resolve inside
that set. An LLM cold reader is worse off than a human: it does not error on
an undefined reference — it silently invents a plausible meaning for it.
## Rules
- **No dangling references.** Undefined unless the body supplies them: "as
discussed", "per the design", "as usual"; "see above" with no findable
target; a private path or id as the *only* source of a required fact.
Restate the fact, or give a resolvable locator **and** what to take from it.
- **Gloss on first use.** Acronyms, nicknames, codenames, team shorthand — one
clause, at the first appearance, nowhere else.
- **One term, one meaning.** A thing keeps its name for the whole text; a
synonym midway reads as a second thing.
- **Mechanism → observable consequence.** What the reader can see or do, not
internal names first. Plain words first; technical name second.
- **Define before you label.** Why it matters → what it does and does not do →
this text's scope. The label comes after the reader can already picture it.
- **Pin partial work.** What this can do, what it deliberately does not,
whether a larger goal is finished or only advanced.
- **Match detail to what the reader can use.** If they have only the text, put
boundaries and failure next-moves in the text. If they can open or run
things, say exactly how.
- **Examples fit the reader.** Output that ships beyond this conversation gets
generic, domain-neutral examples. Inside a conversation, the reader's own
material is shared context and the best example there is.
## Final check
Hand the text to the cold reader. If they must ask "what is X / how far does
this go / what's out / what next?", not done.
+18
View File
@@ -0,0 +1,18 @@
# Style — 中文表述 (optional part)
Load only when the output is Chinese prose for a human reader; even then,
skip code-heavy text. This file is a guardrail against model-flavoured
prose; when model writing no longer needs it, delete the file and every
pointer to it (grep both skills for style.md) — neither core depends on it.
Every rule is countable. Before delivery, sweep the list once: each rule
passes, or the text changes until it does.
1. **删空转连接词**:此外 / 然而 / 值得注意的是 / 需要指出的是。句子没了它们照样成立。
2. **删填充开场**:为了实现这一目标 → 为了这一点;在这个时间点 → 现在;系统具有处理的能力 → 系统可以处理。
3. **禁 `**小标题:** 一句话` 式 bullet 列表**。每项都以加粗标签加冒号开头的列表是模型腔;写成句子。(输出文档里操作步骤内的字段标签,如「操作:」「预期结果:」,是结构,保留。)
4. **禁强行三段式**。三个并列项逐个做删除测试:删掉后不改变动作、判断或风险的,删。两项或四项都正常。
5. **加粗每段最多两处**。加粗是"读者不能错过",多了等于没有。
6. **连续三句同长,破一句**。
7. **删中文破折号(——)**,改逗号或句号。
8. **无 emoji**。