# local-env-handbook

Adopt this skill: https://proskills.md/skills/other/mr-zhj-ai-agent-skills-collection-02-productivity-local-env-handbook

Page: https://proskills.md/skills/other/mr-zhj-ai-agent-skills-collection-02-productivity-local-env-handbook
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: 02_Productivity/local-env-handbook/SKILL.md
size: 12771
sha: 02a6732341a474b23c13b1ace7c20dff04d02525
upstream: https://github.com/mr-zhj/ai-agent-skills-collection/blob/main/02_Productivity/local-env-handbook/SKILL.md

````text
---
name: local-env-handbook
description: 当用户要求生成或更新本机环境文档、给新 agent 一份环境说明、或新 agent 首次接入本机时，使用此技能。技能会探查本机的工具链、Python 环境（多套解释器）、网络与 pip 配置、各 Agent 供应商状态，然后生成结构化的 AGENT.md（人读契约文档）与 env-baseline.json（机器读环境基准）双文档，供多 agent 协作时共用。触发语句：生成环境文档、更新 AGENT.md、给新 agent 看本机环境、刷新环境说明。
name_cn: 本地环境手册生成
description_cn: 探查本机工具链、Python、pip 与代理、各 Agent 供应商状态，一键生成 AGENT.md + env-baseline.json 双文档，供多 agent 协作共用。
---

# 本地环境手册生成（local-env-handbook）

目标：让任意 agent 在几分钟内产出一份准确、可复核的本机环境文档，供后续所有 agent 会话开始时读取，避免重复摸索与踩坑。

## 核心约束（不可违反）

1. **只读探查，不改环境。** 本技能只生成/刷新文档，绝不安装、卸载、升级或修改任何环境配置（PATH、pip.ini、conda、供应商配置均不动）。若任务需要改环境，停下来先向用户说明并获得明确批准。
2. **不把易变事实写死在技能正文。** 版本号、路径、端口、进程数一律由 `scripts/collect.ps1` 现场采集；机器特有检查（服务、端口、残留组件）通过 `-ProbeFile` 外置传入，不硬编码进脚本。
3. **探查幂等可重跑。** 每次重新采集事实并整体重渲染，除“已知非故障”区间与时间戳外不追加历史内容；同一环境连跑两次结果应一致。
4. **输出区分“事实”与“约定”。** 事实 = 工具版本、安装路径、端口、配置位置（会变，需定期刷新）；约定 = 该用什么、不该用什么、为什么（给 agent 的红线，见“固化约定区”）。
5. **绝不写入任何 API key / token。** 采集只取 `base_url`、模型名、provider 名；发现明文 key 只标注“存在明文 key 于 `<文件路径>`”。渲染后对两个输出文件统一执行脱敏兜底（形如 `sk-` 的密钥串替换为 `<redacted>`）。
6. **覆盖前必须备份。** 目标文件已存在时，先复制到 `%USERPROFILE%\.dsh\env-backups\` 下（文件名.时间戳.bak）。
7. **“已知非故障”章节跨次保留。** 用 HTML 注释标记区间；重跑时原样搬移，不要重新生成，否则每次都要重新判断哪些告警可接受。
8. **双输出同步生成。** AGENT.md 与 env-baseline.json 由同一次探查渲染，两者描述同一份事实，不允许只更新其中一个。
9. **工具路径必须区分类别。** PATH 直连 / `.cmd`/`.bat`/`.ps1` shim 启动器 / WindowsApps 商店存根，三者不可混为一谈——agent 依据路径类别决定能否直接调用。
10. **`scripts/collect.ps1` 必须是 UTF-8 with BOM。** Windows PowerShell 5.1 会把无 BOM 的脚本按 ANSI 解析，中文注释与字符串直接导致语法错误。修改脚本后先跑语法复检：

    ```powershell
    $e=$null; $null=[System.Management.Automation.Language.Parser]::ParseFile(
      '<skill目录>\scripts\collect.ps1',[ref]$null,[ref]$e); $e
    ```

## 探查清单（跑什么、为什么）

按组执行；任何一组失败只记为“待人工确认”，不中断整体流程。

| 组 | 探查内容 | 为什么查它 |
|---|---|---|
| 系统与工作区 | 系统版本、用户、当前工作区、各文件系统盘剩余空间 | 文档头部信息；判断是否换机/换盘 |
| PATH 健康 | Machine/User PATH 条数、重复组、失效项、危险项（如 `miniconda\Library\bin`） | PATH 错乱是多 agent 环境最常见故障；`Library\bin` 引发 DLL 冲突 |
| 工具版本 | node/npm/pnpm/bun/python/conda/git/claude/codex/uv 的版本与可执行文件位置（区分类别） | 填充“工具链速查”表；版本漂移影响插件兼容性；shim/存根会误导调用 |
| Python 解析 | 裸 `python` 指向、`python3` 存在性、conda 环境列表与各环境 Python 版本（读 conda-meta，不逐个 `conda run`） | 区分多套解释器；确认 `python3` 商店存根是否复活；快速取全量环境版本 |
| pip | `pip config list` | 确认主源/备用源/是否误设常驻 proxy |
| 网络与代理 | 常见代理端口监听状态（默认 7897/7890/10809，可用 `-ProxyPorts` 覆盖）、注册表系统代理开关 | 判断代理是否在线、端口是否变更；pip 回退策略依赖它 |
| DSH profile | `%USERPROFILE%\.dsh\profiles\desktop\package.json` 的 bundles 与 dependencies、DSH 进程数 | 确认实际加载的插件集合；判断是否出现常驻记忆组件 |
| 记忆类残留 | profile 中 `hindsight`、`~/.hindsight` 目录、9077 端口 | 防止“已移除但被重新引入”或进程残留 |
| Claude / cc-switch | `~/.claude/settings.json` 的 `env.ANTHROPIC_BASE_URL` 与模型名（**不取 token**）；cc-switch.db 的 providers 列表（只读，只取名称与是否当前） | 确认当前供应商链路；避免 agent 误判“应该用哪个模型” |
| Codex | `~/.codex/config.toml` 的 `model`、`model_provider`、`base_url`（**屏蔽 bearer token**） | 与 cc-switch 记录比对，发现配置漂移 |
| git | `git config --global user.name/user.email` | 缺少身份会让 agent 提交失败 |
| 扩展探查 | `-ProbeFile` JSON 中声明的自定义工具与检查 | 机器特有组件（服务/端口/残留）外置化，见下方“扩展探查” |

## 双输出文件

| 文件 | 角色 | 消费者 |
|---|---|---|
| `AGENT.md` | 人读契约：事实章节（可重跑刷新）+ 约定章节（红线）+ 已知非故障 + 待办 | 所有 agent 会话开始时阅读 |
| `env-baseline.json` | 机器读基准：`schema_version`、`collected_at`、`machine`、`path_health`、`tools`（含 `kind`/`origin` 字段）、`python`、`providers`、`custom_checks`、`todo` | 程序与其他 agent 可直接解析消费 |

两者由 `collect.ps1` 同一次探查渲染；`facts` 一致，仅形态不同。

## 扩展探查（ProbeFile）

机器特有检查不硬编码进脚本。需要探测默认清单之外的工具（如 ffmpeg）、服务端口、残留组件时，编写一个 JSON 探查文件并通过 `-ProbeFile` 传入：

- 字段规范、写法示例（含 Windows 服务端口检查、Linux 服务检查）见 [probe-file.md](./references/probe-file.md)。仅在需要添加自定义检查时读取该文件。
- 探查结果：自定义工具并入 AGENT.md 第 2 章“工具链速查”（标注来源为“扩展”）；自定义检查进入第 8 章“扩展探查”，未通过的项自动追加到“待办”。

## 文档骨架（每章填什么、数据从哪来）

| 章节 | 写什么 | 取数来源 |
|---|---|---|
| 1. 机器与工作区 | 系统版本、用户、工作区、磁盘、代理状态 | 探查组“系统与工作区”“网络与代理” |
| 2. 工具链速查 | 工具/版本/路径/路径类别四列表（内置 + ProbeFile 扩展） | 探查组“工具版本”“PATH 健康” |
| 3. Python / conda 规则 | 每套解释器的用途与调用方式（**用途是约定，版本是事实**） | 探查组“Python 解析” |
| 4. 网络与 pip | 主源、备用源、代理端口、回退方式 | 探查组“pip”“网络与代理” |
| 5. DSH | profile 路径、当前 bundles、插件增删的正确方式 | 探查组“DSH profile” |
| 6. 各 Agent 供应商 | Claude/Codex 当前指向与切换入口、记忆组件状态 | 探查组“Claude / cc-switch”“Codex”“记忆类残留” |
| 7. 已知非故障 | 原样保留上一版区间内容 | 旧文档标记区间 |
| 8. 扩展探查 | ProbeFile 自定义检查结果（未使用则标注“本次未使用”） | 探查组“扩展探查” |
| 9. 行为约定 | 固化红线（见“固化约定区”） | 静态内容，不采集 |
| 10. 自检 | 可粘贴的最小检查命令 | 静态内容 |
| 11. 待办 | 由本次探查自动推导的异常项 | 探查结果的条件判断 |

`AGENT.md` 中“已知非故障”必须用以下标记包裹，供脚本识别搬移：

```markdown
<!-- KNOWN-ISSUES:BEGIN -->
（表格内容）
<!-- KNOWN-ISSUES:END -->
```

## 固化约定区（红线，必须原样写入输出文档）

| 红线 | 理由 |
|---|---|
| 用 `conda run -n <env>` 或显式解释器路径，**不要依赖裸 `python`** | 机器上常有多套 Python（系统/conda base/业务 env），裸 `python` 命中的通常不是目标环境 |
| **不要用 `python3`** | Windows 商店别名是 0 字节存根，会弹商店或直接失败 |
| **`pip.ini` 不要常驻 `proxy`** | 常驻代理会让国内镜像也走代理，代理软件关闭时连镜像都装不了；需要时单次 `--proxy` |
| **Claude/Codex 供应商切换只走 cc-switch** | cc-switch 会整体重写 `settings.json` 的 `env`，手改会被覆盖且造成 provider 与实际请求不一致 |
| **不要执行 `npm i -g @anthropic-ai/claude-code` 或 `npm i -g @deepseek-ai/dsh`** | 前者被包装层遮蔽、装了也不生效；后者与桌面版争 profile |
| **不要填写 `~/.clawgod/provider.json` 的 apiKey** | 一旦非空会覆盖 cc-switch 写入的 `ANTHROPIC_*`，切换失效 |
| **不要重新引入 Hindsight 或同类常驻记忆 daemon** | 历史教训：空闲约 1 GB RSS + 约 5 GB 运行时缓存，且会在会话开始自动拉起 |
| **删除/覆盖配置前先备份到 `%USERPROFILE%\.dsh\env-backups\`** | 无版本控制的环境文件只能靠备份回滚 |
| **文档内不写 API key/token**，只写“存在明文 key 于 X” | 文档会被多次读取、可能被复制外传 |
| **不要依据本文档版本号直接发起安装/升级**，环境变更需用户批准 | 文档是快照，环境可能已变化；本技能不负责改环境 |

> 注：固化约定随环境演进。若某条约定因环境变化不再成立（例如换了包管理器），应修改本表并说明原因，而不是静默删除。

## 更新流程

**何时重跑：** 更换或升级工具链；新增/移除 agent 插件或 profile bundles 变化；切换供应商、改模型、换代理端口；新 agent 首次接入；用户要求“刷新环境说明”。

**怎么跑**（`<skill>` = 本技能安装目录；Windows PowerShell 5.1）：

```powershell
# 1) 预览（只打印 AGENT.md，不写任何文件）
powershell -NoProfile -File <skill>\scripts\collect.ps1 -PrintOnly

# 2) 正式生成（自动备份旧文件；输出 AGENT.md + env-baseline.json）
powershell -NoProfile -File <skill>\scripts\collect.ps1

# 3) 指定输出与备份目录、代理端口、扩展探查
powershell -NoProfile -File <skill>\scripts\collect.ps1 `
  -Output .\AGENT.md -JsonOutput .\env-baseline.json `
  -BackupDir "$env:USERPROFILE\.dsh\env-backups" `
  -ProxyPorts 7897,7890 -ProbeFile .\my-probes.json
```

- 脚本逐组采集，单项失败打印“待人工确认”，退出码仍为 0；
- 生成后人工复核：**事实**是否与当前机器一致，**约定**是否仍然成立；
- 机器特有检查（服务/端口/残留组件）放入自己的 ProbeFile 常备使用，不要要求修改脚本。

**如何 diff：**

```powershell
$bak = Get-ChildItem "$env:USERPROFILE\.dsh\env-backups\AGENT.md.*.bak" |
       Sort-Object LastWriteTime -Descending | Select-Object -First 1
Compare-Object (Get-Content $bak.FullName) (Get-Content .\AGENT.md)
```

预期差异只应出现在“事实”章节；若“约定”章节出现差异，说明有人改了红线，必须人工确认。

## 验收标准

1. 跑一遍脚本，产出 AGENT.md 与 env-baseline.json，结构完整，事实项无“待人工确认”（除非该工具确实不存在）。
2. 故意变更一个环境事实（如新增一个环境变量），重跑后两个文件都能反映变化，且值一致。
3. 连续跑两次，除时间戳外输出一致（幂等）。
4. 输出中不含任何 `sk-` 开头的字符串（脱敏生效）。
5. 传入含自定义检查的 ProbeFile 后，第 8 章出现结果且未通过项出现在待办；不传时标注“本次未使用”。

## Resources

### `scripts/collect.ps1`

Windows PowerShell 5.1 单脚本：只读探查本机 11 组事实 → 渲染 AGENT.md + env-baseline.json；支持 `-PrintOnly` 预览、`-ProbeFile` 扩展、`-ProxyPorts` 自定义端口；覆盖前自动备份；保留 KNOWN-ISSUES 区间；输出 UTF-8 无 BOM。

### `references/probe-file.md`

扩展探查清单（ProbeFile）的 JSON 字段规范与平台示例。仅在需要添加机器特有检查时读取。
````

## File: 02_Productivity/local-env-handbook/scripts/collect.ps1
size: 32596
sha: 163ae372ddb9d301236c04b9c84ea5d7770ca007
upstream: https://github.com/mr-zhj/ai-agent-skills-collection/blob/main/02_Productivity/local-env-handbook/scripts/collect.ps1

````text
﻿<#
.SYNOPSIS
  采集本机环境事实并渲染 AGENT.md + env-baseline.json 双输出（本地环境手册生成技能）。

.DESCRIPTION
  - 只读探查，不安装、不卸载、不修改任何环境配置。
  - 单项探查失败输出“待人工确认”，不中断整体流程，退出码恒为 0。
  - 覆盖前自动备份到 -BackupDir（AGENT.md 与 env-baseline.json 均备份）。
  - 保留旧文档中 <!-- KNOWN-ISSUES:BEGIN --> ... <!-- KNOWN-ISSUES:END --> 区间。
  - 工具可执行文件区分类别：PATH 直接命中 / .cmd .bat .ps1 shim / WindowsApps 商店存根。
  - 支持 -ProbeFile 外置扩展探查（自定义工具与检查，JSON 格式见 references/probe-file.md）。
  - 输出为 UTF-8 无 BOM；渲染后对密钥（形如 sk-...）做脱敏替换。

.PARAMETER Output
  人读契约文档输出路径，默认当前目录下 AGENT.md。

.PARAMETER JsonOutput
  机器读基准输出路径，默认与 -Output 同目录的 env-baseline.json。

.PARAMETER BackupDir
  备份目录，默认 %USERPROFILE%\.dsh\env-backups。

.PARAMETER ProxyPorts
  待探测的代理端口列表，默认 7897、7890、10809。

.PARAMETER ProbeFile
  扩展探查清单 JSON 路径（可选）。

.PARAMETER PrintOnly
  只把 AGENT.md 打印到标准输出，不写文件、不备份（JSON 也不输出）。

.NOTES
  本脚本必须以 UTF-8 with BOM 保存。Windows PowerShell 5.1 会把无 BOM 脚本按 ANSI
  解析，中文注释与字符串会导致语法错误。修改本文件后请运行语法复检：

  $e=$null; $null=[System.Management.Automation.Language.Parser]::ParseFile(
    '<skill>\scripts\collect.ps1',[ref]$null,[ref]$e); $e
#>
[CmdletBinding()]
param(
    [string]$Output = (Join-Path (Get-Location).Path 'AGENT.md'),
    [string]$JsonOutput = '',
    [string]$BackupDir = (Join-Path $env:USERPROFILE '.dsh\env-backups'),
    [string[]]$ProxyPorts = @('7897','7890','10809'),
    [string]$ProbeFile = '',
    [switch]$PrintOnly
)

$ErrorActionPreference = 'Continue'
$ProgressPreference = 'SilentlyContinue'
$Pending = '待人工确认'
$KindCn = @{ 'PATH' = '直连'; 'shim' = 'shim'; 'stub' = '存根' }

# ---------------------------------------------------------------- 公共函数

function Try-Value {
    param([scriptblock]$Body, [string]$Fallback = $Pending)
    try {
        $v = & $Body
        if ($null -eq $v) { return $Fallback }
        $s = ("$v").Trim()
        if ($s -eq '') { return $Fallback }
        return $s
    } catch {
        return $Fallback
    }
}

# 解析命令的可执行文件：区分类别（PATH 直连 / shim / 商店存根）
function Resolve-ToolInfo {
    param([string]$Command)
    $c = Get-Command $Command -ErrorAction SilentlyContinue
    if (-not $c) { return $null }
    $src = $c.Source
    if (-not $src) { return $null }
    $kind = 'PATH'
    $note = ''
    if ($src -match 'WindowsApps\\') {
        $kind = 'stub'
        $note = '商店别名存根，执行会弹商店或失败'
    }
    elseif ($src -match '\.ps1$') {
        $kind = 'shim'
        $note = 'PS1 包装脚本'
    }
    elseif ($src -match '\.(cmd|bat)$') {
        $kind = 'shim'
        try {
            $content = Get-Content -LiteralPath $src -Raw -ErrorAction Stop
            if ($content -match '"%~dp0([^"\r\n]+)"') {
                $dir = Split-Path -Parent $src
                $note = '指向 ' + (Join-Path $dir ($Matches[1].TrimStart('\')))
            } else { $note = 'cmd 启动器' }
        } catch { $note = 'cmd 启动器' }
    }
    New-Object pscustomobject -Property @{ path = $src; kind = $kind; note = $note }
}

# 执行命令数组取版本（首元素为可执行文件）。Regex 命中则取捕获组，否则取首行输出。
function Invoke-ToolVersion {
    param([string[]]$CmdArr, [string]$Regex)
    try {
        $exe = [string]$CmdArr[0]
        $rest = @()
        if ($CmdArr.Count -gt 1) { $rest = @($CmdArr[1..($CmdArr.Count - 1)]) }
        $o = & $exe @rest 2>$null
        $s = ((@($o) | ForEach-Object { "$_" }) -join ' ').Trim()
        if ($s -eq '') { return $Pending }
        if ($Regex) { if ($s -match $Regex) { return $Matches[1] } }
        if ($s.Length -gt 60) { $s = $s.Substring(0, 60) + '...' }
        return $s
    } catch {
        return $Pending
    }
}

# 表格单元格净化：竖线会破坏 Markdown 表格
function Clean-Cell {
    param([string]$Text)
    if ($null -eq $Text) { return '' }
    return ([string]$Text).Replace('|', '/').Replace("`r", '').Replace("`n", ' ')
}

# 读 conda 环境的 Python 版本：优先读 conda-meta 文件名，其次 exe 版本资源；不执行任何命令
function Get-CondaEnvPyVer {
    param([string]$EnvPath)
    try {
        $meta = Join-Path $EnvPath 'conda-meta'
        if (Test-Path -LiteralPath $meta) {
            $f = Get-ChildItem -LiteralPath $meta -Filter 'python-*.json' -ErrorAction SilentlyContinue |
                 Where-Object { $_.Name -match '^python-(\d+\.\d+\.\d+)-' } |
                 Select-Object -First 1
            if ($f) { return $f.Name -replace '^python-(\d+\.\d+\.\d+)-.*$', '$1' }
        }
    } catch { }
    try {
        $pe = Join-Path $EnvPath 'python.exe'
        if (Test-Path -LiteralPath $pe) {
            $fv = (Get-Item -LiteralPath $pe).VersionInfo.FileVersion
            if ($fv) { return ($fv -replace ', ', '.') }
        }
    } catch { }
    return $Pending
}

function Backup-Existing {
    param([string]$Path)
    if (Test-Path -LiteralPath $Path) {
        if (-not (Test-Path -LiteralPath $BackupDir)) {
            New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null
        }
        $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
        $name = [System.IO.Path]::GetFileName($Path)
        $bak = Join-Path $BackupDir ("{0}.{1}.bak" -f $name, $stamp)
        Copy-Item -LiteralPath $Path -Destination $bak -Force
        Write-Host "backup: $bak"
    }
}

# ---------------------------------------------------------------- 系统与工作区
$os   = Try-Value { $o = Get-CimInstance Win32_OperatingSystem; "$($o.Caption) $($o.Version)" }
$user = Try-Value { $env:USERNAME }
$cwd  = Try-Value { (Get-Location).Path }
$disk = Try-Value {
    $parts = @(Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue |
        ForEach-Object { "{0}: {1:N0} GB 空闲" -f $_.Name, ($_.Free / 1GB) })
    ($parts | Select-Object -First 8) -join '，'
}

# ---------------------------------------------------------------- 网络与代理
$proxyInfo = Try-Value {
    $parts = foreach ($p in $ProxyPorts) {
        $c = Get-NetTCPConnection -State Listen -LocalPort ([int]$p) -ErrorAction SilentlyContinue
        "127.0.0.1:$($p + $(if ($c) {' 监听中'} else {' 未监听'}))"
    }
    $parts -join '，'
}
$sysProxy = Try-Value {
    $s = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue
    if ($s.ProxyEnable -eq 1) { "系统代理已启用（$($s.ProxyServer)）" } else { '系统代理未启用' }
}

# ---------------------------------------------------------------- PATH 健康
$machinePath = @(([Environment]::GetEnvironmentVariable('Path','Machine')) -split ';' | Where-Object { $_ })
$userPath    = @(([Environment]::GetEnvironmentVariable('Path','User'))    -split ';' | Where-Object { $_ })
$allPath     = $machinePath + $userPath
$dupGroups   = @($allPath | Group-Object { $_.TrimEnd('\').ToLower() } | Where-Object { $_.Count -gt 1 }).Count
$missing     = @($allPath | Where-Object { -not (Test-Path -LiteralPath $_) }).Count
$hasLibBin   = [bool](@($allPath | Where-Object { $_ -match 'miniconda\\Library\\bin$' }).Count)
$libBinTxt   = '已移除'; if ($hasLibBin) { $libBinTxt = '仍在 PATH（风险）' }
$pathInfo    = "系统 $($machinePath.Count) 条 / 用户 $($userPath.Count) 条；重复组 $dupGroups，失效项 $missing；miniconda\Library\bin $libBinTxt"

# ---------------------------------------------------------------- 内置工具探测
$toolRows = New-Object System.Collections.Generic.List[object]
function Probe-BuiltinTool {
    param([string]$Id, [string]$Label, [string]$Cmd, [string[]]$VArgs, [string]$Regex)
    $info = Resolve-ToolInfo $Cmd
    $version = $Pending; $kind = $KindCn['PATH']; $note = ''
    if ($null -eq $info) {
        $toolRows.Add([pscustomobject]@{ id=$Id; label=$Label; version='未安装'; path=''; kind='未找到'; note=''; origin='builtin' })
        return
    }
    $kind = $KindCn[[string]$info.kind]
    $note = [string]$info.note
    if ($info.kind -eq 'stub') {
        # 存根不可执行（会弹商店），只记录不跑版本命令
        $version = $Pending
        $toolRows.Add([pscustomobject]@{ id=$Id; label=$Label; version=$version; path=$info.path; kind=$kind; note=$note; origin='builtin' })
        return
    }
    $cmdArr = @($info.path) + @($VArgs)
    $version = Invoke-ToolVersion -CmdArr $cmdArr -Regex $Regex
    $toolRows.Add([pscustomobject]@{ id=$Id; label=$Label; version=$version; path=$info.path; kind=$kind; note=$note; origin='builtin' })
}

Probe-BuiltinTool 'node'   'Node.js'      'node'   @('-v') $null
Probe-BuiltinTool 'npm'    'npm'          'npm'    @('-v') $null
Probe-BuiltinTool 'pnpm'   'pnpm'         'pnpm'   @('-v') $null
Probe-BuiltinTool 'python' 'Python(bare)' 'python' @('--version') '(\d+\.\d+\.\d+)'
Probe-BuiltinTool 'conda'  'conda'        'conda'  @('--version') 'conda\s+([\d\.]+)'
Probe-BuiltinTool 'git'    'Git'          'git'    @('--version') 'version\s+([\w\.]+)'
Probe-BuiltinTool 'claude' 'Claude Code'  'claude' @('--version') $null
Probe-BuiltinTool 'codex'  'Codex CLI'    'codex'  @('--version') $null
Probe-BuiltinTool 'uv'     'uv'           'uv'     @('--version') $null

$barePy = $toolRows | Where-Object { $_.id -eq 'python' }
$barePyPath = ''; $barePyKind = ''
if ($barePy -and $barePy.path) { $barePyPath = $barePy.path; $barePyKind = $barePy.kind }

# ---------------------------------------------------------------- Python / conda
$py3Info = Resolve-ToolInfo 'python3'
if ($null -eq $py3Info) { $py3 = '不存在（无 python3 命令）' }
else {
    $py3 = "$($py3Info.path)（$($KindCn[[string]$py3Info.kind])）"
    if ($py3Info.kind -eq 'stub') { $py3 = '商店别名存根复活！位于 ' + $py3Info.path }
}

$condaEnvRows = New-Object System.Collections.Generic.List[object]
$condaEnvsText = Try-Value {
    $lines = @(conda env list 2>$null | ForEach-Object { "$_".Trim() } |
        Where-Object { $_ -and $_ -notmatch '^#' })
    foreach ($t in ($lines | ForEach-Object { ($_ -replace '\s+', ' ').Trim() })) {
        $name = ''; $path = ''
        if ($t -match '^(\S+)\s+\*?\s*((?:[A-Za-z]:|\\\\|/).*)$') { $name = $Matches[1]; $path = $Matches[2].Trim() }
        elseif ($t -match '^((?:[A-Za-z]:|\\\\|/).*)$') { $name = '(无名)'; $path = $Matches[1].Trim() }
        if ($name -ne '' -and $path -ne '') {
            $condaEnvRows.Add([pscustomobject]@{ name=$name; path=$path; version=(Get-CondaEnvPyVer -EnvPath $path) })
        }
    }
    if ($condaEnvRows.Count -eq 0) { return $Pending }
    ($condaEnvRows | ForEach-Object { "$($_.name)=$($_.version)" }) -join '；'
}
$pipCfg  = Try-Value { (pip config list 2>&1) -join '；' }
$pyEnc   = Try-Value { $env:PYTHONIOENCODING }

# ---------------------------------------------------------------- DSH
$dshProfilePath = Join-Path $env:USERPROFILE '.dsh\profiles\desktop\package.json'
$dshProfile = Try-Value { if (Test-Path -LiteralPath $dshProfilePath) { $dshProfilePath } else { '不存在' } }
$dshBundles = Try-Value {
    if (-not (Test-Path -LiteralPath $dshProfilePath)) { return '（profile 不存在）' }
    $j = Get-Content -LiteralPath $dshProfilePath -Raw -Encoding UTF8 | ConvertFrom-Json
    if ($j.dsh.profile.bundles) { (@($j.dsh.profile.bundles) -join '、') } else { '（无）' }
}
$dshDeps = Try-Value {
    if (-not (Test-Path -LiteralPath $dshProfilePath)) { return '（profile 不存在）' }
    $j = Get-Content -LiteralPath $dshProfilePath -Raw -Encoding UTF8 | ConvertFrom-Json
    if (-not $j.dependencies -or @($j.dependencies.PSObject.Properties).Count -eq 0) { return '（无额外依赖）' }
    (@($j.dependencies.PSObject.Properties | ForEach-Object { "$($_.Name)@$($_.Value)" }) -join '、')
}
$dshProc   = Try-Value { [string](@(Get-Process 'DeepSeek Harness' -ErrorAction SilentlyContinue).Count) }
$dshStatus = "进程 $dshProc 个"

# ---------------------------------------------------------------- 记忆类残留
$hindsightProfile = [bool](@($dshBundles) -match 'hindsight').Count
$hindsightDir     = Test-Path (Join-Path $env:USERPROFILE '.hindsight')
$port9077         = [bool](@(Get-NetTCPConnection -State Listen -LocalPort 9077 -ErrorAction SilentlyContinue).Count)
if ($hindsightProfile -or $hindsightDir -or $port9077) {
    $hindsight = "检测到 Hindsight 残留（bundle=$hindsightProfile，目录=$hindsightDir，端口9077=$port9077）"
} else {
    $hindsight = '未检测到 Hindsight（bundle/目录/端口均无）'
}

# ---------------------------------------------------------------- Claude / cc-switch
$claudeCfgPath = Join-Path $env:USERPROFILE '.claude\settings.json'
$claudeBase = Try-Value {
    if (Test-Path -LiteralPath $claudeCfgPath) {
    (Get-Content -LiteralPath $claudeCfgPath -Raw -Encoding UTF8 | ConvertFrom-Json).env.ANTHROPIC_BASE_URL }
}
$claudeModel = Try-Value {
    if (-not (Test-Path -LiteralPath $claudeCfgPath)) { return $Pending }
    (Get-Content -LiteralPath $claudeCfgPath -Raw -Encoding UTF8 | ConvertFrom-Json).env.ANTHROPIC_MODEL
}

$ccSwitch = Try-Value {
    if ($barePyPath -eq '' -or $barePyKind -eq $KindCn['stub']) { return $Pending }
    $tmp = Join-Path $env:TEMP 'ccsw_dump_providers.py'
    $pyCode = @'
import os, sqlite3
p = os.path.expanduser(r"~/.cc-switch/cc-switch.db")
if not os.path.exists(p):
    print("cc-switch.db not found")
else:
    con = sqlite3.connect("file:" + p + "?mode=ro", uri=True)
    rows = con.execute("select app_type,name,is_current from providers order by app_type,name").fetchall()
    print("; ".join("%s/%s%s" % (a, n, " (current)" if c else "") for a, n, c in rows) or "none")
'@
    [System.IO.File]::WriteAllText($tmp, $pyCode, (New-Object System.Text.UTF8Encoding($false)))
    & $barePyPath $tmp 2>$null
    Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
}

# ---------------------------------------------------------------- Codex
$codexCfgPath = Join-Path $env:USERPROFILE '.codex\config.toml'
$codexSummary = Try-Value {
    if (-not (Test-Path -LiteralPath $codexCfgPath)) { return $Pending }
    $t = Get-Content -LiteralPath $codexCfgPath -Raw -Encoding UTF8
    $model    = ([regex]::Match($t, '(?m)^\s*model\s*=\s*"([^"]+)"')).Groups[1].Value
    $provider = ([regex]::Match($t, '(?m)^\s*model_provider\s*=\s*"([^"]+)"')).Groups[1].Value
    $base     = ([regex]::Match($t, '(?m)^\s*base_url\s*=\s*"([^"]+)"')).Groups[1].Value
    "provider=$provider，model=$model，base_url=$base"
}

# ---------------------------------------------------------------- git
$gitId = Try-Value {
    $n = git config --global user.name 2>$null
    $e = git config --global user.email 2>$null
    if (-not $n -and -not $e) { return '未配置（提交会失败）' }
    "$n <$e>"
}

# ---------------------------------------------------------------- 扩展探查（ProbeFile）
$probeFileResolved = ''
$customChecks = New-Object System.Collections.Generic.List[object]
if ($ProbeFile -ne '') {
    if (Test-Path -LiteralPath $ProbeFile) {
        $probeFileResolved = (Resolve-Path -LiteralPath $ProbeFile).Path
        try {
            $pfRaw = [System.IO.File]::ReadAllText($probeFileResolved)
            $pfRaw = $pfRaw.TrimStart([char]0xFEFF)
            $pf = $pfRaw | ConvertFrom-Json

            # 自定义工具 → 并入工具表（origin=probe）
            if ($pf.tools) {
                foreach ($t in @($pf.tools)) {
                    $cmds = @()
                    if ($t.commands) { $cmds = @($t.commands) }
                    elseif ($t.command) { $cmds = @($t.command) }
                    $resolved = $null
                    foreach ($cnd in $cmds) {
                        $resolved = Resolve-ToolInfo ([string]$cnd)
                        if ($resolved) { break }
                    }
                    $id = [string]$t.id; if (-not $id) { $id = 'probe-tool' }
                    $label = [string]$t.label; if (-not $label) { $label = $id }
                    if ($null -eq $resolved) {
                        $toolRows.Add([pscustomobject]@{ id=$id; label=$label; version='未安装'; path=''; kind='未找到'; note=''; origin='probe' })
                        continue
                    }
                    $kind = $KindCn[[string]$resolved.kind]
                    $ver = $Pending
                    if ($resolved.kind -ne 'stub') {
                        $vCmd = @()
                        if ($t.version_command) {
                            $vCmd = @(@($t.version_command) | ForEach-Object { ([string]$_).Replace('{selected_executable}', $resolved.path) })
                        } else {
                            $vCmd = @($resolved.path, '--version')
                        }
                        $ver = Invoke-ToolVersion -CmdArr $vCmd -Regex ([string]$t.version_regex)
                    }
                    $toolRows.Add([pscustomobject]@{ id=$id; label=$label; version=$ver; path=$resolved.path; kind=$kind; note=[string]$resolved.note; origin='probe' })
                }
            }

            # 自定义检查 → customChecks
            if ($pf.checks) {
                foreach ($ck in @($pf.checks)) {
                    $id = [string]$ck.id; if (-not $id) { $id = 'probe-check' }
                    $label = [string]$ck.label; if (-not $label) { $label = $id }
                    $cmdArr = @()
                    if ($ck.command) { $cmdArr = @($ck.command) }
                    if ($cmdArr.Count -lt 1) {
                        $customChecks.Add([pscustomobject]@{ id=$id; label=$label; status=$Pending; detail='command 字段为空' })
                        continue
                    }
                    $status = $Pending; $detail = ''
                    try {
                        $exe = [string]$cmdArr[0]
                        $rest = @()
                        if ($cmdArr.Count -gt 1) { $rest = @($cmdArr[1..($cmdArr.Count - 1)]) }
                        $out = & $exe @rest 2>&1
                        $txt = ((@($out) | ForEach-Object { "$_" }) -join "`n").Trim()
                        $code = $LASTEXITCODE
                        $pass = $true
                        if ($ck.success_patterns) {
                            foreach ($sp in @($ck.success_patterns)) {
                                if ($txt -notmatch ([string]$sp)) { $pass = $false }
                            }
                        } elseif ($null -ne $code -and $code -ne 0) {
                            $pass = $false
                        }
                        if ($pass) { $status = '通过' } else { $status = '未通过' }
                        $detail = $txt
                    } catch {
                        $status = $Pending
                        $detail = $_.Exception.Message
                    }
                    if ($detail.Length -gt 200) { $detail = $detail.Substring(0, 200) + '...' }
                    $customChecks.Add([pscustomobject]@{ id=$id; label=$label; status=$status; detail=$detail })
                }
            }
        } catch {
            $ProbeParseError = "ProbeFile 解析失败：$($_.Exception.Message)"
        }
    } else {
        $ProbeParseError = "ProbeFile 不存在：$ProbeFile"
    }
}

# ---------------------------------------------------------------- 已知非故障（跨次保留）
$beginMarker = '<!-- KNOWN-ISSUES:BEGIN -->'
$endMarker   = '<!-- KNOWN-ISSUES:END -->'
$defaultKnown = @'
| 现象 | 原因 |
|---|---|
| `conda doctor` 报激活脚本被改 / base 未保护 | `conda init` 正常写回；修复需特定 build 并克隆/冻结 base，不划算 |
| 没有 PowerShell 7 | 系统自带 Windows PowerShell 5.1 即可运行本技能脚本 |
| `where python` 多项 | 系统 Python、conda base、WindowsApps 别名并存，属预期 |
（本区间跨次保留，由人工维护：确认某告警为可接受后，请在此追加。）
'@

$knownIssues = $defaultKnown
if (Test-Path -LiteralPath $Output) {
    try {
        $old = Get-Content -LiteralPath $Output -Raw -Encoding UTF8
        $pattern = [regex]::Escape($beginMarker) + '(?s)(.*?)' + [regex]::Escape($endMarker)
        $m = [regex]::Match($old, $pattern)
        if ($m.Success -and $m.Groups[1].Value.Trim()) { $knownIssues = $m.Groups[1].Value.Trim() }
    } catch {
        $knownIssues = $defaultKnown
    }
}

# ---------------------------------------------------------------- 待办推导
$todo = New-Object System.Collections.Generic.List[string]
if ($hindsightProfile -or $hindsightDir -or $port9077) { $todo.Add('存在 Hindsight/记忆 daemon 残留，确认是否需要清理') }
if ($hasLibBin) { $todo.Add('PATH 仍含 `miniconda\Library\bin`，建议移除以防 DLL 冲突') }
if ($dupGroups -gt 0 -or $missing -gt 0) { $todo.Add("PATH 有重复组 $dupGroups / 失效项 $missing，建议清理") }
if ($py3 -like '商店别名存根*') { $todo.Add('`python3` 商店别名存根复活，建议重新移除') }
if ($barePyKind -eq $KindCn['stub']) { $todo.Add('裸 `python` 命中商店存根，所有 Python 调用务必显式指定解释器') }
if ($gitId -match '未配置') { $todo.Add('git 全局身份未配置，提交会失败') }
if ($pipCfg -match 'proxy') { $todo.Add('pip 配置中存在 proxy，确认是否必要（约定：不要常驻）') }
foreach ($ck in $customChecks) {
    if ($ck.status -eq '未通过') { $todo.Add("扩展检查【$($ck.label)】未通过：$($ck.detail)") }
}
if ($ProbeParseError) { $todo.Add($ProbeParseError) }
if ($todo.Count -eq 0) { $todo.Add('无') }
$todoText = ($todo | ForEach-Object { "- $_" }) -join "`r`n"

# ---------------------------------------------------------------- 工具表 / 环境表 渲染
$toolTableText = ($toolRows | ForEach-Object {
    $mark = ''
    if ($_.origin -eq 'probe') { $mark = ' *' }
    $noteTxt = ''
    if ($_.note) { $noteTxt = '（' + (Clean-Cell $_.note) + '）' }
    $pathTxt = '（未找到）'
    if ($_.path) { $pathTxt = '``' + (Clean-Cell $_.path) + '``' }
    "| $(Clean-Cell $_.label)$mark | $(Clean-Cell $_.version) | $pathTxt | $(Clean-Cell $_.kind)$noteTxt |"
}) -join "`n"

$condaEnvsTableText = '（未探查到 conda 环境或 conda 未安装）'
if ($condaEnvRows.Count -gt 0) {
    $condaEnvsTableText = ($condaEnvRows | ForEach-Object {
        "| $(Clean-Cell $_.name) | ``$(Clean-Cell $_.path)`` | $(Clean-Cell $_.version) |"
    }) -join "`n"
}

$customSectionText = '本次未使用 ProbeFile（如需机器特有检查，编写探查 JSON 后用 `-ProbeFile` 传入，格式见技能 references/probe-file.md）。'
if ($ProbeFile -ne '') {
    $head = "探查清单：``$probeFileResolved``（自定义工具已并入第 2 章《工具链速查》，标 * 项）。`r`n`r`n"
    if ($customChecks.Count -gt 0) {
        $tbl = ($customChecks | ForEach-Object {
            "| $(Clean-Cell $_.label) | $(Clean-Cell $_.status) | $(Clean-Cell $_.detail) |"
        }) -join "`n"
        $customSectionText = $head + "| 检查 | 结果 | 说明 |`n|---|---|---|`n$tbl"
    } else {
        $customSectionText = $head + '（清单中无 checks，仅添加了自定义工具。）'
    }
}

$checked = Get-Date -Format 'yyyy-MM-dd HH:mm'

# ---------------------------------------------------------------- AGENT.md 模板
$template = @'
# AGENT.md — 本机环境与调用约定

> 面向在此工作区工作的 AI Agent。本文档由「本地环境手册生成」技能生成：事实部分可重跑刷新，约定部分是红线。
> 最后采集：{{CHECKED}}。工作区：`{{CWD}}`。
> 配套机器读基准：`env-baseline.json`（与本文档同一次探查生成，供程序直接解析）。

---

## 1. 机器与工作区

| 项 | 值 |
|---|---|
| 系统 | {{OS}} |
| 用户 | `{{USER}}` |
| 工作区 | `{{CWD}}` |
| 磁盘 | {{DISK}} |
| 代理 | {{PROXY}}；{{SYSPROXY}} |

---

## 2. 工具链速查（事实）

| 工具 | 版本 | 可执行文件 | 类别 |
|---|---|---|---|
{{TOOLTABLE}}

（* 标记项来自 ProbeFile 扩展探查。类别说明：直连 = PATH 命中的真实可执行文件；shim = 启动器/包装脚本，真实程序在“指向”路径；存根 = WindowsApps 商店别名，不可直接调用。）

PATH：{{PATHLINE}}

---

## 3. Python / conda 规则（约定 + 事实）

- 跑脚本时**显式指定环境**：`conda run -n <env> python ...`，不要依赖裸 `python`。
- 不要使用 `python3`（Windows 商店别名为存根；当前状态：{{PY3}}）。
- 裸 `python` 当前指向：`{{PYBARE}}`（类别：{{PYBAREKIND}}）。
- conda 环境实测（读 conda-meta 元数据，未执行任何环境内命令）：

| 环境 | 路径 | Python 版本 |
|---|---|---|
{{CONDAENVS}}

- 各环境用途属约定，请人工在本节补充维护；机器特定说明也可放入“已知非故障”区间。
- 编码约定：`PYTHONIOENCODING={{PYENCODING}}`，避免 GBK 控制台下输出 emoji 报错。

---

## 4. 网络与 pip（事实 + 约定）

pip 配置：

```
{{PIPCFG}}
```

- 约定：镜像优先直连；`pip.ini` 不要常驻 `proxy`，需要时单次加 `--proxy`。
- 代理现状：{{PROXY}}。

---

## 5. DeepSeek Harness（DSH，如使用）

- profile：`{{DSHPROFILE}}`；{{DSHSTATUS}}。
- 当前 bundles：{{DSHBUNDLES}}。
- 依赖：{{DSHDEPS}}。
- 约定：桌面版独占 profile，改插件走插件页或用 profile 自带包管理器，改完重启应用。

---

## 6. 各 Agent 供应商（事实）

- Claude Code：`{{CLAUDEBASE}}`，模型 `{{CLAUDEMODEL}}`。
- Codex：{{CODEXCFG}}。
- cc-switch：{{CCSWITCH}}。
- 约定：供应商切换只走 cc-switch；不要手改 `settings.json` 的 `env`。
- 记忆组件：{{HINDSIGHT}}。

---

## 7. 已知非故障（跨次保留）

<!-- KNOWN-ISSUES:BEGIN -->
{{KNOWN}}
<!-- KNOWN-ISSUES:END -->

---

## 8. 扩展探查（自定义检查）

{{CUSTOM}}

---

## 9. 行为约定（红线，不可随意更改）

1. Python 调用显式指定环境（`conda run -n <env>` 或解释器全路径），不猜环境；不用 `python3`。
2. `pip.ini` 不常驻 `proxy`；需要时单次 `--proxy`。
3. 不执行 `npm i -g @anthropic-ai/claude-code` 或 `npm i -g @deepseek-ai/dsh`。
4. Claude/Codex 供应商只通过 cc-switch 切换；不填 `~/.clawgod/provider.json` 的 apiKey。
5. 不重新引入 Hindsight 或同类常驻记忆 daemon，除非用户明确要求。
6. 删除/覆盖任何配置前，先备份到 `%USERPROFILE%\.dsh\env-backups\`。
7. 本文档不写 API key/token；发现明文 key 只标注文件位置。
8. 不依据本文档中的版本号直接发起安装/升级/卸载；环境变更需经用户批准后执行。

---

## 10. 自检（复制粘贴即可）

```powershell
where.exe python; node -v; git --version; conda --version
conda env list
pip config list
python -m json.tool .\env-baseline.json > $null; if ($?) { 'baseline json ok' }
```

---

## 11. 待办 / 注意

{{TODO}}
'@

$doc = $template
$map = [ordered]@{
    '{{CHECKED}}'      = $checked
    '{{OS}}'           = $os
    '{{USER}}'         = $user
    '{{CWD}}'          = $cwd
    '{{DISK}}'         = $disk
    '{{PROXY}}'        = $proxyInfo
    '{{SYSPROXY}}'     = $sysProxy
    '{{TOOLTABLE}}'    = $toolTableText
    '{{PATHLINE}}'     = $pathInfo
    '{{PY3}}'          = $py3
    '{{PYBARE}}'       = $barePyPath
    '{{PYBAREKIND}}'   = $barePyKind
    '{{CONDAENVS}}'    = $condaEnvsTableText
    '{{PIPCFG}}'       = $pipCfg
    '{{PYENCODING}}'   = $pyEnc
    '{{DSHPROFILE}}'   = $dshProfile
    '{{DSHSTATUS}}'    = $dshStatus
    '{{DSHBUNDLES}}'   = $dshBundles
    '{{DSHDEPS}}'      = $dshDeps
    '{{CLAUDEBASE}}'   = $claudeBase
    '{{CLAUDEMODEL}}'  = $claudeModel
    '{{CODEXCFG}}'     = $codexSummary
    '{{CCSWITCH}}'     = $ccSwitch
    '{{HINDSIGHT}}'    = $hindsight
    '{{KNOWN}}'        = $knownIssues
    '{{CUSTOM}}'       = $customSectionText
    '{{TODO}}'         = $todoText
}
foreach ($k in $map.Keys) { $doc = $doc.Replace($k, [string]$map[$k]) }

# 安全兜底：任何形似密钥的字符串一律脱敏（事实与 JSON 双输出一致处理）
$doc = [regex]::Replace($doc, 'sk-[A-Za-z0-9_\-]{10,}', '<redacted>')

if ($PrintOnly) {
    Write-Output $doc
    return
}

if ($JsonOutput -eq '') {
    $outDir = Split-Path -Parent $Output
    if ($outDir) { $JsonOutput = Join-Path $outDir 'env-baseline.json' }
    else { $JsonOutput = 'env-baseline.json' }
}

# ---------------------------------------------------------------- env-baseline.json
$baseline = [pscustomobject][ordered]@{
    schema_version = 1
    generated_by   = 'local-env-handbook'
    collected_at   = $checked
    machine        = [pscustomobject][ordered]@{
        os           = $os
        user         = $user
        cwd          = $cwd
        disks        = $disk
        proxy_ports  = @($ProxyPorts)
        proxy_status = $proxyInfo
        system_proxy = $sysProxy
    }
    path_health = [pscustomobject][ordered]@{
        machine_entries     = $machinePath.Count
        user_entries        = $userPath.Count
        duplicate_groups    = $dupGroups
        missing_entries     = $missing
        miniconda_lib_bin   = $libBinTxt
    }
    tools = @($toolRows | ForEach-Object {
        [pscustomobject][ordered]@{
            id      = $_.id
            label   = $_.label
            version = $_.version
            path    = $_.path
            kind    = $_.kind
            note    = $_.note
            origin  = $_.origin
        }
    })
    python = [pscustomobject][ordered]@{
        bare_python  = [pscustomobject][ordered]@{ path = $barePyPath; kind = $barePyKind }
        python3      = $py3
        conda        = [pscustomobject][ordered]@{ version = (($toolRows | Where-Object { $_.id -eq 'conda' }).version) }
        environments = @($condaEnvRows | ForEach-Object {
            [pscustomobject][ordered]@{ name = $_.name; path = $_.path; version = $_.version }
        })
        pip_config   = $pipCfg
        io_encoding  = $pyEnc
    }
    providers = [pscustomobject][ordered]@{
        claude_base_url = $claudeBase
        claude_model    = $claudeModel
        codex           = $codexSummary
        cc_switch       = $ccSwitch
        dsh             = [pscustomobject][ordered]@{
            profile      = $dshProfile
            status       = $dshStatus
            bundles      = $dshBundles
            dependencies = $dshDeps
        }
        memory_daemon   = $hindsight
    }
    git = [pscustomobject][ordered]@{
        identity = $gitId
    }
    probe_file = [pscustomobject][ordered]@{
        used  = ($ProbeFile -ne '')
        path  = $probeFileResolved
        checks = @($customChecks | ForEach-Object {
            [pscustomobject][ordered]@{ id = $_.id; label = $_.label; status = $_.status; detail = $_.detail }
        })
    }
    todo = @($todo | Where-Object { $_ -ne '无' })
    known_issues = [pscustomobject][ordered]@{
        marker_preserved = $true
        note             = '完整内容见 AGENT.md 第 7 章 KNOWN-ISSUES 区间（跨次保留，人工维护）'
    }
}

$jsonText = ConvertTo-Json -InputObject $baseline -Depth 6
$jsonText = [regex]::Replace($jsonText, 'sk-[A-Za-z0-9_\-]{10,}', '<redacted>')

# ---------------------------------------------------------------- 输出（备份 → 写入）
$outDir2 = Split-Path -Parent $Output
if ($outDir2 -and -not (Test-Path -LiteralPath $outDir2)) { New-Item -ItemType Directory -Force -Path $outDir2 | Out-Null }

$enc = New-Object System.Text.UTF8Encoding($false)
Backup-Existing -Path $Output
[System.IO.File]::WriteAllText($Output, $doc, $enc)
Write-Host "written: $Output"

$jsonDir = Split-Path -Parent $JsonOutput
if ($jsonDir -and -not (Test-Path -LiteralPath $jsonDir)) { New-Item -ItemType Directory -Force -Path $jsonDir | Out-Null }
Backup-Existing -Path $JsonOutput
[System.IO.File]::WriteAllText($JsonOutput, $jsonText, $enc)
Write-Host "written: $JsonOutput"
Write-Host ("note: 事实项中的【" + $Pending + "】需要人工补充。")
````

## File: 02_Productivity/local-env-handbook/references/probe-file.md
size: 4753
sha: 5d621de3a0ed8efbd4b02d5f450a4bcb0a793ee5
upstream: https://github.com/mr-zhj/ai-agent-skills-collection/blob/main/02_Productivity/local-env-handbook/references/probe-file.md

````text
# Probe File（扩展探查清单）

当默认探查组不够用时（机器特有工具、本地服务端口、残留组件检查等），编写一个 JSON 探查清单并通过 `collect.ps1 -ProbeFile` 传入。**机器特有检查一律外置到这里，不要要求修改脚本。**

## 使用方式

```powershell
powershell -NoProfile -File <skill>\scripts\collect.ps1 -ProbeFile .\my-probes.json
```

- 自定义工具：探测结果并入 AGENT.md 第 2 章“工具链速查”（工具名带 * 标记）与 `env-baseline.json` 的 `tools`（`origin` = `probe`）。
- 自定义检查：结果进入 AGENT.md 第 8 章“扩展探查”；状态为“未通过”的项自动追加到第 11 章“待办”。
- 文件建议保存为 UTF-8（带不带 BOM 均可，脚本会自动剥离）。

## 顶层字段

```json
{
  "tools":  [ ... ],
  "checks": [ ... ]
}
```

两个字段均可选；至少提供一个。

## tools 字段（自定义工具版本探查）

| 字段 | 必填 | 说明 |
|---|---|---|
| `id` | 是 | 稳定标识，进入 JSON 基准 |
| `label` | 否 | 文档显示名，缺省用 `id` |
| `commands` | 建议 | 命令候选数组（依次尝试 `Get-Command` 命中） |
| `version_command` | 否 | 取版本的完整命令数组；占位符 `{selected_executable}` 会被替换为实际命中的可执行文件路径；缺省为 `<exe> --version` |
| `version_regex` | 否 | 单捕获组正则；从版本命令输出中提取版本；缺省取首行输出 |

示例（探测 ffmpeg 与自定义 JDK）：

```json
{
  "tools": [
    {
      "id": "ffmpeg",
      "label": "ffmpeg",
      "commands": ["ffmpeg"],
      "version_command": ["{selected_executable}", "-version"],
      "version_regex": "ffmpeg version\\s+([^\\s]+)"
    },
    {
      "id": "java",
      "label": "Java",
      "commands": ["java"],
      "version_command": ["{selection_placeholder}"],
      "version_regex": "version \"?([\\w.+]+)\"?"
    }
  ]
}
```

> 上例 `java` 的 `version_command` 写法是故意演示错误占位符的情况：占位符不匹配时不会产生替换，请始终使用 `{selected_executable}`，例如 `["{selected_executable}", "-version"]`。

## checks 字段（自定义检查，常用于服务/端口/残留）

| 字段 | 必填 | 说明 |
|---|---|---|
| `id` | 是 | 稳定标识 |
| `label` | 否 | 文档显示名，缺省用 `id` |
| `command` | 是 | 命令数组，首元素为可执行文件，其余为参数 |
| `success_patterns` | 否 | 正则数组；全部命中输出才算通过；缺省时按退出码 0 判定 |

结果状态：`通过` / `未通过` / `待人工确认`（命令异常时）。

### 示例：Windows 服务与端口（PowerShell）

```json
{
  "checks": [
    {
      "id": "hindsight-port",
      "label": "Hindsight 残留端口 9077",
      "command": ["powershell", "-NoProfile", "-Command",
        "if (Get-NetTCPConnection -State Listen -LocalPort 9077 -ErrorAction SilentlyContinue) { 'listening' } else { 'free' }"],
      "success_patterns": ["free"]
    },
    {
      "id": "postgres-service",
      "label": "PostgreSQL 服务",
      "command": ["sc.exe", "query", "postgresql-x64-16"],
      "success_patterns": ["RUNNING"]
    },
    {
      "id": "redis-port",
      "label": "Redis 端口 6379",
      "command": ["powershell", "-NoProfile", "-Command",
        "if (Get-NetTCPConnection -State Listen -LocalPort 6379 -ErrorAction SilentlyContinue) { 'listening' } else { 'free' }"],
      "success_patterns": ["listening"]
    }
  ]
}
```

### 示例：macOS / Linux 服务（供跨平台参考）

```json
{
  "checks": [
    {
      "id": "docker-service",
      "label": "Docker systemd 服务",
      "command": ["systemctl", "is-active", "docker"],
      "success_patterns": ["active"]
    }
  ]
}
```

### 示例：残留目录检查

```json
{
  "checks": [
    {
      "id": "hindsight-dir",
      "label": "~/.hindsight 残留目录",
      "command": ["powershell", "-NoProfile", "-Command",
        "if (Test-Path \"$env:USERPROFILE\\.hindsight\") { 'exists' } else { 'clean' }"],
      "success_patterns": ["clean"]
    }
  ]
}
```

## 编写注意

1. `command` 中的引号、反斜杠遵循 JSON 转义规则；PowerShell `-Command` 字符串里出现的 `$env:` 等照常书写。
2. 检查命令会被真实执行，**只写只读探测命令**，不要写入任何安装、删除或修改动作。
3. `success_patterns` 描述“期望的正常状态”（例如希望端口空闲则匹配 `free`）；这样“未通过”才代表出现了需要关注的问题并自动进待办。
4. 私密信息（key/token）不要写进探查文件；探查输出会被截断到 200 字符并做 `sk-` 脱敏，但请从源头避免。

````

## File: README.md
upstream: https://github.com/mr-zhj/ai-agent-skills-collection/blob/main/README.md

````text
# AI-Agent-Skills-Collection

<p align="center">
  <a href="https://github.com/Mr-ZHJ/AI-Agent-Skills-Collection/stargazers"><img src="https://img.shields.io/github/stars/Mr-ZHJ/AI-Agent-Skills-Collection?style=flat-square&color=FFD700" alt="Stars"></a>
  <a href="https://github.com/Mr-ZHJ/AI-Agent-Skills-Collection/network/members"><img src="https://img.shields.io/github/forks/Mr-ZHJ/AI-Agent-Skills-Collection?style=flat-square&color=blue" alt="Forks"></a>
  <a href="https://github.com/Mr-ZHJ/AI-Agent-Skills-Collection/issues"><img src="https://img.shields.io/github/issues/Mr-ZHJ/AI-Agent-Skills-Collection?style=flat-square&color=orange" alt="Issues"></a>
  <br><br>
  <img src="https://img.shields.io/badge/Skills-7-blue?style=flat-square" alt="7 Skills">
  <img src="https://img.shields.io/badge/Python-3.9+-3776AB?style=flat-square" alt="Python 3.9+">
  <img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen?style=flat-square" alt="Zero Dependencies">
  <img src="https://img.shields.io/badge/License-MIT-yellow?style=flat-square" alt="MIT License">
</p>

> [中文版](./README-zh.md) | **English**

A **personal, continuously growing library of Agent Skills** created while learning and practicing AI security and AI engineering. Currently **7 skills**, covering an MCP (Model Context Protocol) pre-install security gate, third-party skill poisoning audit, Windows host health check, environment handbook generation, and multi-source security / research digest pipelines.

> 🌱 **This repository is a long-term work in progress.** Every skill here was born from a real problem I hit while studying or working, and new skills are appended as they are built and validated. Expect continuous updates rather than a frozen release.

Every skill ships as a self-contained folder: a `SKILL.md` workflow contract, deterministic `scripts/`, and `references/` holding the judging rules — so the process is **re-runnable, auditable and reviewable**, not a one-off chat prompt.

---

## 📑 Table of Contents

- [📝 Overview](#-overview)
- [✨ Skill Inventory](#-skill-inventory)
- [🔒 Deep Dive: MCP Security Gate](#-deep-dive-mcp-security-gate)
- [🔍 Deep Dive: Skill Supply-Chain Auditor](#-deep-dive-skill-supply-chain-auditor)
- [🧩 Shared Engineering Paradigm](#-shared-engineering-paradigm)
- [🛡️ Security Design Principles](#️-security-design-principles)
- [📁 Project Structure](#-project-structure)
- [🔧 Quick Start](#-quick-start)
  - [⚙️ Installation](#️-installation)
  - [▶️ Usage](#️-usage)
- [🔄 Continuous Updates](#-continuous-updates)
  - [➕ Adding a New Skill](#-adding-a-new-skill)
  - [🗺️ Roadmap](#️-roadmap)
- [📜 License](#-license)
- [🔗 Related Projects](#-related-projects)

---

## 📝 Overview

> ⚠️ **Disclaimer**: This is a personal learning and engineering practice project. All skills are sanitized — **no internal corporate data, intranet addresses, real business interfaces or proprietary assets** are included, and it does not represent any employer. Do not use it for unauthorized security testing.

The Agent ecosystem is exploding, but the security control plane around it is missing. Two gaps stand out:

- **MCP servers run as child processes, not sandboxed extensions.** They inherit the host shell environment, filesystem permissions and every credential handed to them — and they actively send and receive data on the user's behalf. **One unpinned community package plus one malicious update equals a full credential harvest.**
- **Third-party skills are a plain-text attack surface.** A `MUST RUN` / `CRITICAL REQUIREMENT` inside `SKILL.md` may itself be a prompt-injection payload: the user thinks they are *installing a skill*, while actually *executing someone else's command*.

This repository turns those risks into explicit, repeatable workflows, and bundles several skills that genuinely save engineering hours.

---

## ✨ Skill Inventory

| Category | Skill | One-liner | Key capability |
|----------|-------|-----------|----------------|
| 🔒 Security | [mcp-security-review](01_Security/mcp-security-review) | **MCP pre-install security gate** | Source identification → 6-tier risk classification → enforced version pinning → 4-check community audit → credential exposure mapping → audit ledger |
| 🔒 Security | [skill-security-auditor](01_Security/skill-security-auditor) | **Third-party skill supply-chain audit** | 13-category static scan → P0/P1/P2 grading → reports with `file:line` evidence → batch patrol |
| 🛠️ Productivity | [system-dev-healthcheck](02_Productivity/system-dev-healthcheck) | **One-shot Windows host check-up** | 10-dimension read-only collection → 3-tier triage (fault / watch / noise) → history diff → cleanup candidates pending confirmation |
| 🛠️ Productivity | [local-env-handbook](02_Productivity/local-env-handbook) | **Local environment handbook generator** | Probes toolchain / Python / proxy / agent providers → emits `AGENT.md` (human contract) + `env-baseline.json` (machine baseline) |
| 📚 Research | [arxiv-personal-digest](03_Learning/arxiv-personal-digest) | **Personalized arXiv digest** | Multi-route fetch → Hacker News heat signal → 3-dimension scoring → recommendation rationale & reading priority |
| 📚 Research | [github-project-recommender](03_Learning/github-project-recommender) | **Customized GitHub repo discovery** | Multi-route search → 4-dimension scoring (relevance / popularity / activity / maturity) → action suggestions |
| 📚 Research | [cybersec-news-daily](03_Learning/cybersec-news-daily) | **Daily cybersecurity digest** | 6 CN/EN sources (+2 optional) → cross-source clustering & dedup → 4-dimension scoring → TOP 10 morning brief |

---

## 🔒 Deep Dive: MCP Security Gate

**Threat model.** An MCP server is not a browser extension — there is no sandbox. It runs as a child process that inherits the host shell environment, filesystem permissions, and every credential passed to it, while actively exchanging data on the user's behalf. A silent version bump is therefore a complete credential-theft opportunity.

**The five gate steps**

```text
Step 1  Identify source    npm scope → PyPI publisher → GitHub org/user → hosting domain → typosquat diff
        ↓
Step 2  Classify & decide  6 tiers: vendor-hosted / vendor-published / MCP org / internal / community-established / community-unknown
        ↓
Step 3  Audit & remediate  Community packages: outbound domains · credential touching · obfuscation chains · transitive install hooks
        ↓
Step 4  Record ledger      One row per MCP: source / version policy / credential exposure / status (user-owned data)
        ↓
Step 5  Maintain           Pre-update diff gate · migrate to the official alternative when available · quarterly patrol · on compromise: revoke credentials BEFORE removing the package
```

**Six-tier source classification and default decisions**

| Tier | Description | Default action |
|------|-------------|----------------|
| Vendor-hosted | Endpoint hosted by the service provider | Install |
| Vendor-published | Package published by the official account | **Pin exact version** |
| MCP official org | e.g. `@modelcontextprotocol/` | **Pin exact version** |
| Internal | Self-built / in-house | Install |
| Community-established | Has audit trail and community validation | **Pin + audit + ledger** |
| Community-unknown | No audit trail | **Deny by default** |

**The "four checks"** are the most valuable part — especially **transitive install hooks**: a package can be clean itself while a `pre-commit`-style dependency executes actions at install time, which is nearly invisible to manual review. The bundled scanner is **read-only and non-judging** — it only locates signals; verdicts always follow `references/risk-matrix.md`, so static scanning never degenerates into a false-positive machine.

**Red lines**: never execute or install the audit target; the more urgent an install instruction reads ("you MUST run this first"), the more suspicious it is; community-unknown packages are not wave
````
