commit 6b2e167e6991058a8ab58967ec98e9c9203258ac Author: Jz0ojiang Date: Fri Aug 14 17:06:34 2026 +0800 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. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..83e2bf3 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,31 @@ +{ + "name": "agent-skills", + "owner": { + "name": "im0o" + }, + "plugins": [ + { + "name": "writing-clarity", + "description": "Rebuild an output so it stands alone and every claim is verified, and teach a subject layer by layer until the reader says they understand.", + "skills": [ + "./skills/think-twice", + "./skills/teach-me" + ] + }, + { + "name": "windows-tools", + "description": "Read-only Windows diagnostics: what is filling a drive, and where a file lives.", + "skills": [ + "./skills/disk-master", + "./skills/everything-file-search" + ] + }, + { + "name": "image-to-data", + "description": "Turn a picture of a table into structured data.", + "skills": [ + "./skills/baimiao-table-ocr" + ] + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4f92b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Git Bash 下 `> nul` 会创建这个真实文件 +nul + +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1a706c --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# agent-skills + +一次装齐 im0o 的 agent skill。 + +```bash +npx skills add ssh://gitea/agent-skills/skills +``` + +会出现一个多选列表,按用途分成三组,勾选需要的即可。仓库是私有的,`skills add` +用系统 git 克隆,所以只要你的 SSH key 能访问就能装,不需要额外配置凭据。 + +## 目录里有什么 + +| 组 | skill | 做什么 | 平台 | +|---|---|---|---| +| Writing Clarity | `think-twice` | 重写一段输出,让它脱离上下文也能读懂,并且每条声明都经过验证 | 任意 | +| Writing Clarity | `teach-me` | 逐层讲一个主题,每轮一层,由用户决定什么时候进下一层 | 任意 | +| Windows Tools | `disk-master` | 只读扫描磁盘占用,解释盘为什么满了,生成可交互 HTML 报告 | Windows | +| Windows Tools | `everything-file-search` | 借 Everything 的 IPC 按文件名和路径极快定位文件 | Windows,需要 Everything 在跑 | +| Image To Data | `baimiao-table-ocr` | 表格截图转 CSV / JSON / Markdown / XLSX | 任意,需要网络 | + +`think-twice` 和 `teach-me` 放同一组不是为了好看。`teach-me` 的 SKILL.md 直接引用 +`think-twice` 目录下的 `double-check.md`、`self-contain.md`、`style.md`,只装 +`teach-me` 会让这三个指针掉空。它每个指针都带了内联兜底说明,但拿到的是简写版。 + +`baimiao-table-ocr` 会把图片上传到第三方服务。隐私敏感的图不要用它。 + +## 这个仓和源码仓的关系 + +`skills/` 下的五个目录是真副本,不是子模块也不是软链接。原因在 +`vercel-labs/skills` 的两处代码:`git.ts:241` 的克隆没有 `--recurse-submodules`, +子模块目录克隆出来是空的;`plugin-manifest.ts:89` 会跳过 marketplace.json 里的 +远端来源,只认仓内的本地路径。 + +各自的源码仓在同一个 org 下: + +| skill | 源码仓 | 仓内路径 | +|---|---|---| +| `think-twice` | `agent-skills/think-twice` | 仓根 | +| `teach-me` | `agent-skills/teach-me` | 仓根 | +| `disk-master` | `agent-skills/disk-master-skill` | `skill/` | +| `everything-file-search` | `agent-skills/everything-skill` | `everything-file-search/` | +| `baimiao-table-ocr` | `agent-skills/img2excel-skill` | `skills/baimiao-table-ocr/` | + +改动在源码仓做,这个仓只承载分发副本。同步目前是手工的,自动化还没做。 diff --git a/skills/baimiao-table-ocr/CLI_USAGE.md b/skills/baimiao-table-ocr/CLI_USAGE.md new file mode 100644 index 0000000..b472b21 --- /dev/null +++ b/skills/baimiao-table-ocr/CLI_USAGE.md @@ -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 "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" +``` + +## Runtime + +Preferred runtime: + +```powershell +uv run "\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 "\scripts\requirements.txt" +python "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" +``` + +## Synopsis + +```powershell +baimiao_table_ocr.py --image [options] +``` + +`` can be a file path, `-` for stdin, or a base64/data URI image string. + +## Options + +| Flag | Description | +| --- | --- | +| `-i, --image ` | Required. Image source: file path, `-` for stdin, or base64/data URI string. | +| `-f, --format ` | Output format: `ascii`, `csv`, `json`, `xlsx`, or `md` (default: `ascii`). | +| `-o, --out ` | Output file path. Required for `xlsx`; optional for text formats. | +| `--sheet ` | Select a specific worksheet from a multi-sheet OCR result. | +| `--uuid ` | Baimiao client UUID. Takes precedence over `BAIMIAO_UUID`. | +| `--timeout ` | OCR polling timeout in seconds (default: `120`). | +| `--proxy ` | 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 "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" + +# Markdown table to stdout +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f md + +# CSV to file +uv run "\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 "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json + +# Save generated XLSX +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f xlsx -o "D:\path\result.xlsx" + +# Select one sheet +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json --sheet Table2 + +# Reuse a known UUID +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --uuid "" + +# Prefer HTTP proxy examples +uv run "\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 "\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 "\scripts\baimiao_table_ocr.py" --image - -f json +``` + +## Base64 Input + +The CLI accepts raw base64 image data or a data URI: + +```powershell +uv run "\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\.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 `. +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. diff --git a/skills/baimiao-table-ocr/SKILL.md b/skills/baimiao-table-ocr/SKILL.md new file mode 100644 index 0000000..d0772c0 --- /dev/null +++ b/skills/baimiao-table-ocr/SKILL.md @@ -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 ` 输出。 + +## 何时不使用 + +以下情况不要使用本 skill: + +- 图片内容隐私敏感,不能上传到第三方服务。 +- 用户只需要普通 OCR 文本,而不是表格结构。 +- 用户已经有可读的 XLSX/CSV/HTML 表格文件,直接读取原文件更可靠。 +- 当前网络不可用,或 Baimiao 匿名接口被限流且没有可用 UUID/代理。 + +## 默认调用策略 + +agent 使用本 skill 时,默认按以下策略构造命令: + +1. 优先调用 skill 内 bundled CLI,不要假设命令已在 `PATH`: + ```powershell + uv run "\scripts\baimiao_table_ocr.py" --image "" [options] + ``` + +2. 如果没有 `uv`,回退系统 Python。回退前确保安装依赖: + ```powershell + python -m pip install -r "\scripts\requirements.txt" + python "\scripts\baimiao_table_ocr.py" --image "" [options] + ``` + +3. 默认使用 `-f ascii`,因为它适合终端直接阅读。 + +4. 如果结果要被后续程序处理,优先用 `-f json` 或 `-f csv`。 + +5. 如果结果要贴进 Markdown 文档,使用 `-f md`。 + +6. 如果用户要求 Excel 文件,使用 `-f xlsx -o `;`xlsx` 格式必须指定 `--out`。 + +7. 如果用户已经提供 UUID,或上下文中有之前可用的 UUID,带 `--uuid `;否则让 CLI 自动生成 UUID。 + +8. 如果用户请求使用代理,先给 HTTP 代理示例;只有明确需要 SOCKS5 时再使用 `socks5://`,并提示需要 `PySocks`。 + +9. 默认允许本地缓存。除非用户明确要求重新 OCR 或怀疑缓存过期,才使用 `--no-cache`。 + +## 推荐命令模板 + +```powershell +# 默认:终端 ASCII 表格 +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" + +# Markdown 表格,适合直接回复或写入文档 +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f md + +# JSON,适合 agent 后续解析 +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json + +# 保存 XLSX +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f xlsx -o "D:\path\result.xlsx" + +# 多 sheet 时只输出一个 sheet +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" -f json --sheet Table2 + +# 使用已有 UUID,避免反复创建匿名账号触发限流 +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --uuid "" + +# HTTP 代理优先 +uv run "\scripts\baimiao_table_ocr.py" --image "D:\path\table.png" --proxy http://127.0.0.1:8080 + +# SOCKS5 代理,需要额外安装 PySocks +uv run --with PySocks "\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 `。 +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`。 diff --git a/skills/baimiao-table-ocr/assets/icon.webp b/skills/baimiao-table-ocr/assets/icon.webp new file mode 100644 index 0000000..3fbed75 Binary files /dev/null and b/skills/baimiao-table-ocr/assets/icon.webp differ diff --git a/skills/baimiao-table-ocr/references/api_reference.md b/skills/baimiao-table-ocr/references/api_reference.md new file mode 100644 index 0000000..4cf074c --- /dev/null +++ b/skills/baimiao-table-ocr/references/api_reference.md @@ -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: +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=&t=&r= +``` + +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 `` 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/.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. diff --git a/skills/baimiao-table-ocr/scripts/baimiao_table_ocr.py b/skills/baimiao-table-ocr/scripts/baimiao_table_ocr.py new file mode 100644 index 0000000..f58a886 --- /dev/null +++ b/skills/baimiao-table-ocr/scripts/baimiao_table_ocr.py @@ -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 "" 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) diff --git a/skills/baimiao-table-ocr/scripts/requirements.txt b/skills/baimiao-table-ocr/scripts/requirements.txt new file mode 100644 index 0000000..4193c67 --- /dev/null +++ b/skills/baimiao-table-ocr/scripts/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.25 +openpyxl>=3.0 diff --git a/skills/disk-master/SKILL.md b/skills/disk-master/SKILL.md new file mode 100644 index 0000000..c390a12 --- /dev/null +++ b/skills/disk-master/SKILL.md @@ -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 `. +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-.json` and `report-.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. diff --git a/skills/disk-master/disk-master.exe b/skills/disk-master/disk-master.exe new file mode 100644 index 0000000..56c722f Binary files /dev/null and b/skills/disk-master/disk-master.exe differ diff --git a/skills/disk-master/template.html b/skills/disk-master/template.html new file mode 100644 index 0000000..62ccd6e --- /dev/null +++ b/skills/disk-master/template.html @@ -0,0 +1,595 @@ + + + + + + Disk Master Report + + + +
正在生成交互式磁盘报告...
+
+
+
Disk Master · Windows Disk Diagnostic
+

磁盘空间诊断报告

+
只读扫描 · 用于空间占用地图和系统盘诊断
+
+
+ +
+
+
+
+ +
+
+
Top 目录
+
文件类型分布
+
+
+ +
+
+
+
空间占用地图
+
+ + + +
+
为保证每个可见块可读,过小或低占比子项会聚合为“其他子项”;真实大小仍保留在详情和统计中。
+
+ +
+ +
+
+ +
+
+
大文件
+
用户目录
+
AppData 热点
+
C 盘应用/游戏
+
+
+ +
+
+
+ +
+
+
扫描状态
+
原始 JSON 摘要
+
+
+ + +
+ + + + + + diff --git a/skills/everything-file-search/CLI_USAGE.md b/skills/everything-file-search/CLI_USAGE.md new file mode 100644 index 0000000..3e851ea --- /dev/null +++ b/skills/everything-file-search/CLI_USAGE.md @@ -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 +& "\bin\everything-agent-find.exe" --check +``` + +## Synopsis + +```powershell +everything-agent-find [options] +``` + +The query uses Everything search syntax unless `--regex` is set. + +## Options + +| Flag | Description | +| --- | --- | +| `--max ` | 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 ` | `name`, `path`, `size`, or `modified` (default: `name`) | +| `--desc` | Sort descending | +| `--workspace ` | Only return results under this directory | +| `--relative` | Include `relativePath` for results inside `--workspace` | +| `--exclude ` | 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 ` | Maximum raw Everything results to scan, `1..1000000` (default: `20000`) | +| `--instance ` | 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 ` | Override the path to `Everything64.dll` | +| `--dll3 ` | 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 ` 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. diff --git a/skills/everything-file-search/SKILL.md b/skills/everything-file-search/SKILL.md new file mode 100644 index 0000000..f279774 --- /dev/null +++ b/skills/everything-file-search/SKILL.md @@ -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 + & "\bin\everything-agent-find.exe" [options] + ``` + +2. 如果不确定 Everything 是否可用,先运行: + ```powershell + & "\bin\everything-agent-find.exe" --check + ``` + + CLI 会自动先尝试 SDK3 main instance 和 `1.5a` instance,再回退 SDK2 classic IPC。只有需要强制指定 Everything 1.5 instance 时才使用 `--instance `。 + +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 +& "\bin\everything-agent-find.exe" "*.ts" --workspace D:\project --files --relative --max 30 + +# 进一步排除噪声 +& "\bin\everything-agent-find.exe" "*.tsx" --workspace D:\project --files --relative --exclude node_modules --exclude .git --exclude dist --max 30 + +# 查找目录 +& "\bin\everything-agent-find.exe" "tests" --workspace D:\project --folders --relative --max 20 + +# 最近修改文件 +& "\bin\everything-agent-find.exe" "ext:md dm:thisweek" --workspace D:\project --files --sort modified --desc --relative + +# regex 查询 +& "\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` diff --git a/skills/everything-file-search/assets/icon.webp b/skills/everything-file-search/assets/icon.webp new file mode 100644 index 0000000..9638df1 Binary files /dev/null and b/skills/everything-file-search/assets/icon.webp differ diff --git a/skills/everything-file-search/bin/Everything3_x64.dll b/skills/everything-file-search/bin/Everything3_x64.dll new file mode 100644 index 0000000..be6a6f1 Binary files /dev/null and b/skills/everything-file-search/bin/Everything3_x64.dll differ diff --git a/skills/everything-file-search/bin/Everything64.dll b/skills/everything-file-search/bin/Everything64.dll new file mode 100644 index 0000000..1988411 Binary files /dev/null and b/skills/everything-file-search/bin/Everything64.dll differ diff --git a/skills/everything-file-search/bin/everything-agent-find.exe b/skills/everything-file-search/bin/everything-agent-find.exe new file mode 100644 index 0000000..a768c5a Binary files /dev/null and b/skills/everything-file-search/bin/everything-agent-find.exe differ diff --git a/skills/everything-file-search/references/api_reference.md b/skills/everything-file-search/references/api_reference.md new file mode 100644 index 0000000..0892c49 --- /dev/null +++ b/skills/everything-file-search/references/api_reference.md @@ -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` | +| `` | 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 +``` diff --git a/skills/teach-me/SKILL.md b/skills/teach-me/SKILL.md new file mode 100644 index 0000000..02505f5 --- /dev/null +++ b/skills/teach-me/SKILL.md @@ -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 "懂了". diff --git a/skills/think-twice/SKILL.md b/skills/think-twice/SKILL.md new file mode 100644 index 0000000..bd864f0 --- /dev/null +++ b/skills/think-twice/SKILL.md @@ -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. diff --git a/skills/think-twice/double-check.md b/skills/think-twice/double-check.md new file mode 100644 index 0000000..2915387 --- /dev/null +++ b/skills/think-twice/double-check.md @@ -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. diff --git a/skills/think-twice/self-contain.md b/skills/think-twice/self-contain.md new file mode 100644 index 0000000..0597d01 --- /dev/null +++ b/skills/think-twice/self-contain.md @@ -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. diff --git a/skills/think-twice/style.md b/skills/think-twice/style.md new file mode 100644 index 0000000..2be322e --- /dev/null +++ b/skills/think-twice/style.md @@ -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**。