[docs][refactor]: 将旧 01~06 中文教程合并整理为 modules/frameworks 文件夹结构

- 删除 01~05 旧中文教程目录
- 将 03 核心功能教程拆分为各 framework 模块的子文档
- 06 工具脚本移入 modules/frameworks/编辑器工具/UI工具/
- 非框架内容合并进 rules/20-开发规则.md 与 rules/40-工作流清单.md
- 更新所有交叉链接与文档关联表

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lms
2026-07-07 18:36:15 +08:00
parent 5e5b9f8c80
commit ee1ff7f444
77 changed files with 6263 additions and 1133 deletions

89
hooks/Hook共享层.md Normal file
View File

@@ -0,0 +1,89 @@
# Hook 共享层
> 本目录存放 UnityAI / StrayFog 框架知识库共享的 hook 脚本和规则。
>
> 各 AI 工具自己的私有目录只放适配器,不重复维护 hook 逻辑。
---
## 脚本清单
| 脚本 | 用途 |
|---|---|
| [archive-check.ps1](archive-check.ps1) | 读取 git diff 中变更的 `.md` 文件,解析 `## 文档关联` 表,输出需要同步检查的关联文档 |
| [sync-doc-associations.ps1](sync-doc-associations.ps1) | Python 脚本的 PowerShell 包装器 |
| [sync_doc_associations.py](sync_doc_associations.py) | 在关联文档中追加变更提醒(只追加、不覆盖,执行后需 review |
| [commit-check.ps1](commit-check.ps1) | 提交前检查:推断变更文件所属模块,校验提交信息格式 |
| [commit-module-mappings.json](commit-module-mappings.json) | `commit-check.ps1` 使用的目录-模块映射配置 |
---
## 归档检查
所有任务结束后都应执行归档检查,判断是否需要同步更新长期文档。
```powershell
# PowerShell
.\docs\hooks\archive-check.ps1
```
该脚本只输出建议,不会自动修改任何文档。最终是否更新关联文档,需要用户确认。
---
## 文档关联同步
如需在关联文档中自动留下变更提醒,可运行:
```powershell
.\docs\hooks\sync-doc-associations.ps1
```
该脚本只追加、不覆盖,执行后必须 review。
---
## 提交检查
提交前可运行:
```powershell
.\docs\hooks\commit-check.ps1 -Message "[entity][docs]: 补充实体池说明"
```
检查项包括:
- 提交信息是否符合 `[模块][类型]: 描述` 格式
- 变更文件是否能映射到已知模块
- 模块映射配置来自 [commit-module-mappings.json](commit-module-mappings.json)
---
## 触发方式
| 触发方式 | 命令/路径 | 说明 |
|---|---|---|
| Codex Stop hook | `.codex/hooks/archive-reminder.ps1` | 若已配置,每次任务结束自动调用 `hooks/archive-check.ps1` |
| Claude Code 手动 | 直接运行脚本 | 任务结束时手动调用 `hooks/archive-check.ps1` |
| 手动脚本 | `hooks/archive-check.ps1` | 直接运行 PowerShell 脚本 |
---
## 新增 Hook 时
1. 把脚本放入 `hooks/`
2. 在本文件中登记。
3. 在 [ai/AI接入入口.md](../ai/AI接入入口.md) 中说明触发方式。
4. 如果涉及提交信息格式,同步更新 [rules/40-工作流清单.md](../rules/40-工作流清单.md)。
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [ai/AI接入入口.md](../ai/AI接入入口.md) | AI 接入入口引用 hook 共享层 |
| [rules/项目规则入口.md](../rules/项目规则入口.md) | 规则入口索引归档触发机制 |
| [rules/40-工作流清单.md](../rules/40-工作流清单.md) | 提交前检查与工作流清单互相引用 |

181
hooks/archive-check.ps1 Normal file
View File

@@ -0,0 +1,181 @@
# hooks/archive-check.ps1
# Check changed .md files and suggest related docs to update.
# This script only outputs suggestions; it does NOT modify any files.
#
# Mechanism:
# For every changed .md file, parse its "## 文档关联" section and extract
# Markdown links. Those links are the documents that may need syncing.
$ErrorActionPreference = "SilentlyContinue"
$scriptPath = $MyInvocation.MyCommand.Path
if (-not [System.IO.Path]::IsPathRooted($scriptPath)) {
$scriptPath = Join-Path (Get-Location) $scriptPath
}
$scriptPath = [System.IO.Path]::GetFullPath($scriptPath)
$scriptDir = Split-Path -Parent $scriptPath
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ".."))
Set-Location $projectRoot
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Resolve-RelativePath {
param(
[string]$BaseFile,
[string]$LinkPath
)
# Skip anchors, query strings, and external URLs
if ($LinkPath -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') { return $null }
if ([string]::IsNullOrWhiteSpace($LinkPath)) { return $null }
# Drop fragment
$pathOnly = ($LinkPath -split '#')[0]
if ([string]::IsNullOrWhiteSpace($pathOnly)) { return $null }
$baseDir = Split-Path -Parent $BaseFile
$combined = Join-Path $baseDir $pathOnly
try {
$resolved = [System.IO.Path]::GetFullPath($combined)
} catch {
return $null
}
# Convert to relative path from project root, normalizing separators
$fullProjectRoot = [System.IO.Path]::GetFullPath($projectRoot)
if ($resolved.StartsWith($fullProjectRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
$relative = $resolved.Substring($fullProjectRoot.Length).TrimStart('\', '/')
return $relative -replace '\\', '/'
}
return $pathOnly -replace '\\', '/'
}
function Get-DocumentAssociations {
param(
[string]$MdFile
)
$fullPath = Join-Path $projectRoot $MdFile
if (-not (Test-Path $fullPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($fullPath, [System.Text.Encoding]::UTF8)
$associations = @()
$inSection = $false
$inCodeBlock = $false
foreach ($line in $lines) {
# Toggle fenced code block state (``` or ~~~)
if ($line -match '^\s*```' -or $line -match '^\s*~~~') {
$inCodeBlock = -not $inCodeBlock
continue
}
if ($inCodeBlock) { continue }
# Match exactly '## 文档关联' (not '## 文档关联机制' etc.).
if ($line -match "^##\s+文档关联\s*$") {
$inSection = $true
continue
}
if ($inSection -and $line -match '^##\s+') {
break
}
if ($inSection) {
# Extract markdown links: [text](path)
$regex = '\[(?<text>[^\]]*)\]\((?<path>[^\)]+)\)'
$matches = [regex]::Matches($line, $regex)
foreach ($match in $matches) {
$linkPath = $match.Groups['path'].Value.Trim()
$resolved = Resolve-RelativePath -BaseFile $MdFile -LinkPath $linkPath
if ($resolved) {
$associations += $resolved
}
}
}
}
return $associations | Select-Object -Unique
}
function Test-WildcardMatch {
param(
[string]$Pattern,
[string]$Path
)
$regex = "^" + ($Pattern -replace "\.", "\." -replace "\*", ".*" -replace "\?", ".") + "$"
return $Path -match $regex
}
# ---------------------------------------------------------------------------
# Collect changed .md files (tracked modifications + untracked new files)
# ---------------------------------------------------------------------------
$changedFiles = @(cmd /c "git diff --name-only 2>nul") | Where-Object { $_ }
if ($changedFiles.Count -eq 0) {
$changedFiles = @(cmd /c "git diff --name-only --cached 2>nul") | Where-Object { $_ }
}
# Also include untracked .md files so new documents are checked too
$untrackedFiles = @(cmd /c "git ls-files --others --exclude-standard 2>nul") | Where-Object { $_ }
$allChangedFiles = @($changedFiles) + @($untrackedFiles) | Where-Object { $_ } | Select-Object -Unique
if (-not $allChangedFiles) {
Write-Output "[archive-check] No documentation changes detected (git diff is empty and no untracked .md files)."
exit 0
}
$changedMdFiles = $allChangedFiles | Where-Object { $_ -like "*.md" } | Select-Object -Unique
if ($changedMdFiles.Count -eq 0) {
Write-Output "[archive-check] No .md documentation changes detected."
exit 0
}
# ---------------------------------------------------------------------------
# Build suggestions from each changed file's "## 文档关联" section
# ---------------------------------------------------------------------------
$suggestions = @{}
foreach ($changed in $changedMdFiles) {
$relatedDocs = Get-DocumentAssociations -MdFile $changed
foreach ($related in $relatedDocs) {
# Normalize related path
$relatedNormalized = $related -replace '\\', '/'
if (-not $suggestions.ContainsKey($relatedNormalized)) {
$suggestions[$relatedNormalized] = @()
}
if ($suggestions[$relatedNormalized] -notcontains $changed) {
$suggestions[$relatedNormalized] += $changed
}
}
}
# ---------------------------------------------------------------------------
# Output results
# ---------------------------------------------------------------------------
Write-Output "[archive-check] Changed documentation files:"
foreach ($changed in $changedMdFiles | Sort-Object) {
Write-Output " - $changed"
}
if ($suggestions.Count -eq 0) {
Write-Output ""
Write-Output "[archive-check] No associations found in changed files' `"## 文档关联`" sections."
exit 0
}
Write-Output ""
Write-Output "[archive-check] Related docs that may need updating:"
foreach ($related in $suggestions.Keys | Sort-Object) {
$sources = $suggestions[$related] | Sort-Object
$sourceList = $sources -join ", "
Write-Output " - $related (mentioned by: $sourceList)"
}
Write-Output ""
Write-Output "[archive-check] Please confirm whether to sync-update the above docs."
exit 0

111
hooks/commit-check.ps1 Normal file
View File

@@ -0,0 +1,111 @@
<#
.SYNOPSIS
提交前共享检查:推断变更文件所属模块,校验提交信息格式。
.DESCRIPTION
读取 git diff 中变更的文件,根据 hooks/commit-module-mappings.json
推断模块,然后校验提交信息是否符合 [模块][类型]: 描述 格式。
.PARAMETER Message
待校验的提交信息。
.EXAMPLE
.\docs\hooks\commit-check.ps1 -Message "[entity][docs]: 补充实体池说明"
#>
param(
[Parameter(Mandatory = $true)]
[string]$Message
)
$ErrorActionPreference = 'Stop'
$mappingFile = Join-Path $PSScriptRoot 'commit-module-mappings.json'
if (-not (Test-Path $mappingFile)) {
Write-Error "Module mapping file not found: $mappingFile"
exit 1
}
$mapping = Get-Content $mappingFile -Raw | ConvertFrom-Json
function Get-ChangedFiles {
$staged = (cmd /c "git diff --cached --name-only 2>nul") -split "`r?`n" | Where-Object { $_ }
if (-not $staged) { $staged = @() }
$unstaged = (cmd /c "git diff --name-only 2>nul") -split "`r?`n" | Where-Object { $_ }
if (-not $unstaged) { $unstaged = @() }
$all = @($staged) + @($unstaged) | Select-Object -Unique | Where-Object { $_ }
return $all
}
function Get-ModuleForFile($file) {
foreach ($entry in $mapping.mappings) {
if ($file -match $entry.pattern) {
return $entry.module
}
}
return $null
}
function Get-ModulesFromChangedFiles($files) {
$modules = @()
foreach ($file in $files) {
$module = Get-ModuleForFile $file
if ($module) {
$modules += $module
}
}
return $modules | Select-Object -Unique
}
function Test-CommitMessage($message, $modules) {
if ($message -notmatch '^\[([^\]]+)\]\[([^\]]+)\]:\s*(.+)$') {
return @{ IsValid = $false; Reason = '提交信息格式应为:[模块][类型]: 描述' }
}
$msgModule = $Matches[1]
$msgType = $Matches[2]
$msgDesc = $Matches[3]
if (-not $msgDesc) {
return @{ IsValid = $false; Reason = '提交信息缺少描述内容' }
}
if ($mapping.validTypes -notcontains $msgType) {
return @{ IsValid = $false; Reason = "无效的类型 '$msgType'。有效类型:$($mapping.validTypes -join ', ')" }
}
if ($modules -and $modules -notcontains $msgModule) {
Write-Warning "提交信息中的模块 [$msgModule] 与变更文件推断的模块 [$($modules -join ', ')] 不一致,请确认是否 intentional。"
}
return @{ IsValid = $true; Reason = '' }
}
$files = Get-ChangedFiles
if (-not $files) {
Write-Host "未检测到变更文件。" -ForegroundColor Yellow
exit 0
}
$modules = Get-ModulesFromChangedFiles $files
Write-Host "变更文件:"
$files | ForEach-Object { Write-Host " - $_" }
if ($modules) {
Write-Host "推断模块:$($modules -join ', ')"
}
else {
Write-Warning "未能从变更文件推断出模块,请检查提交信息是否手动指定了正确模块。"
}
$result = Test-CommitMessage $Message $modules
if (-not $result.IsValid) {
Write-Error $result.Reason
exit 1
}
Write-Host "提交信息格式正确。" -ForegroundColor Green
exit 0

View File

@@ -0,0 +1,88 @@
{
"mappings": [
{
"pattern": "rules/.*",
"module": "rules"
},
{
"pattern": "modules/frameworks/实体系统.*",
"module": "entity"
},
{
"pattern": "modules/frameworks/UI窗口系统.*",
"module": "ui"
},
{
"pattern": "modules/frameworks/事件系统.*",
"module": "event"
},
{
"pattern": "modules/frameworks/资源管理.*|modules/frameworks/通用框架.*",
"module": "resource"
},
{
"pattern": "modules/frameworks/内存对象管理.*",
"module": "pool"
},
{
"pattern": "modules/frameworks/相机系统.*",
"module": "camera"
},
{
"pattern": "modules/frameworks/场景系统.*",
"module": "scene"
},
{
"pattern": "modules/frameworks/网络系统.*",
"module": "network"
},
{
"pattern": "modules/frameworks/数据配置框架.*",
"module": "config"
},
{
"pattern": "modules/frameworks/技能系统.*",
"module": "skill"
},
{
"pattern": "modules/frameworks/Hotfix启动.*",
"module": "hotfix"
},
{
"pattern": "modules/frameworks/编辑器工具.*|modules/frameworks/第三方插件.*",
"module": "editor"
},
{
"pattern": "hooks/.*",
"module": "hooks"
},
{
"pattern": "ai/.*|AGENTS\\.md|CLAUDE\\.md|RULES\\.md",
"module": "ai"
},
{
"pattern": "specs/.*|plans/.*",
"module": "spec"
},
{
"pattern": "modules/frameworks/编辑器工具/UI工具/.*",
"module": "tools"
},
{
"pattern": "business-reference/.*",
"module": "business-reference"
},
{
"pattern": "README\\.md",
"module": "docs"
}
],
"validTypes": [
"feat",
"fix",
"docs",
"refactor",
"test",
"chore"
]
}

View File

@@ -0,0 +1,22 @@
# hooks/sync-doc-associations.ps1
# Wrapper for sync_doc_associations.py
# This script appends change reminders to associated .md files.
# It only appends to standard sections; it never overwrites existing content.
$ErrorActionPreference = "Stop"
$scriptPath = $MyInvocation.MyCommand.Path
if (-not [System.IO.Path]::IsPathRooted($scriptPath)) {
$scriptPath = Join-Path (Get-Location) $scriptPath
}
$scriptPath = [System.IO.Path]::GetFullPath($scriptPath)
$scriptDir = Split-Path -Parent $scriptPath
$pythonScript = Join-Path $scriptDir "sync_doc_associations.py"
if (-not (Test-Path $pythonScript)) {
Write-Error "Python script not found: $pythonScript"
exit 1
}
python "$pythonScript"
exit $LASTEXITCODE

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
# hooks/sync_doc_associations.py
#
# 当 md 文件变更时,自动在关联文档的标准位置追加变更提醒:
# - 有 ## 变更记录 表格的模块文档:追加一行变更记录
# - 有 ## 下一步 清单的 progress/tasks 文档:追加一个待检查项
# - 其他文档:追加 ## 最近关联变更 章节
#
# 本脚本只追加、不覆盖正文;执行后请通过 git diff 人工确认。
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
EXCLUDED_PREFIXES = (
'.claude/', '.codex/', '.git/',
'Library/', 'Temp/', 'Logs/', 'UserSettings/',
'obj/', 'bin/', 'HybridCLRData/',
'Assets/ThirdPlugins/',
'Assets/.WX-WASM-SDK-V2/',
'Packages/',
)
def run_git(args):
result = subprocess.run(
["git"] + args,
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
encoding="utf-8",
)
return result.stdout.strip(), result.returncode
def get_changed_md_files():
"""获取 tracked 改动 + untracked 新 md 文件。"""
changed, code = run_git(["diff", "--name-only"])
if code != 0 or not changed:
changed = ""
untracked, code = run_git(["ls-files", "--others", "--exclude-standard"])
if code == 0 and untracked:
changed = (changed + "\n" + untracked).strip()
if not changed:
return []
files = [line.strip() for line in changed.splitlines() if line.strip().endswith(".md")]
files = [f for f in files if not f.startswith(EXCLUDED_PREFIXES)]
return sorted(set(files))
def resolve_link(source_file: Path, link: str):
"""解析相对链接为项目相对路径。"""
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", link):
return None
if link.startswith("#"):
return None
link = link.split("#")[0]
if not link:
return None
target = (source_file.parent / link).resolve()
try:
target.relative_to(PROJECT_ROOT)
except ValueError:
return None
rel = target.relative_to(PROJECT_ROOT)
return str(rel).replace("\\", "/")
def extract_associations(md_path: Path):
"""读取 md 的 ## 文档关联 章节,返回关联文件相对路径列表。"""
content = md_path.read_text(encoding="utf-8")
in_section = False
targets = []
for line in content.splitlines():
if re.match(r"^##\s+文档关联\s*$", line):
in_section = True
continue
if in_section and re.match(r"^##\s+", line):
break
if in_section:
for _, link in re.findall(r"\[([^\]]*)\]\(([^\)]+)\)", line):
resolved = resolve_link(md_path, link)
if resolved:
targets.append(resolved)
return targets
def find_section_range(lines, title_re):
"""返回 [start, end) 行号end 为下一个 ## 标题或文件末尾。"""
start = None
for i, line in enumerate(lines):
if re.match(title_re, line):
start = i
continue
if start is not None and re.match(r"^##\s+", line):
return start, i
if start is not None:
return start, len(lines)
return None, None
def append_changelog(target_path: Path, source_rel: str, date_str: str) -> bool:
"""在 ## 变更记录 表格追加一行;成功返回 True。"""
content = target_path.read_text(encoding="utf-8")
lines = content.splitlines()
start, end = find_section_range(lines, r"^##\s+变更记录\s*$")
if start is None:
return False
# 找到表格最后一行(跳过标题和分隔线)
insert_pos = end
for i in range(start + 1, end):
if lines[i].strip().startswith("|") and not re.match(r"^\|[-:\s|]+\|$", lines[i].strip()):
insert_pos = i + 1
new_line = f"| {date_str} | [{source_rel}](../../{source_rel}) 发生变更 | 待补充影响范围 | 待确认 |"
lines.insert(insert_pos, new_line)
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
return True
def append_next_step(target_path: Path, source_rel: str) -> bool:
"""在 ## 下一步 清单追加一个待检查项;成功返回 True。"""
content = target_path.read_text(encoding="utf-8")
lines = content.splitlines()
start, end = find_section_range(lines, r"^##\s+下一步\s*$")
if start is None:
return False
new_line = f"- [ ] 检查 [{source_rel}](../../{source_rel}) 的变更是否影响本文档"
# 避免重复
for line in lines[start:end]:
if source_rel in line and "变更是否影响本文档" in line:
return True # 已存在,视为成功但不写入
lines.insert(end, new_line)
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
return True
def append_recent_changes_section(target_path: Path, source_rel: str, date_str: str) -> bool:
"""在没有 变更记录/下一步 的文档末尾追加 ## 最近关联变更。"""
content = target_path.read_text(encoding="utf-8")
# 生成从 target 到 source 的相对链接
try:
link = os.path.relpath(PROJECT_ROOT / source_rel, target_path.parent).replace("\\", "/")
except ValueError:
link = source_rel
if "## 最近关联变更" in content:
lines = content.splitlines()
start, end = find_section_range(lines, r"^##\s+最近关联变更\s*$")
if start is not None:
new_line = f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。"
lines.insert(end, new_line)
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
return True
section = (
"\n---\n\n"
"## 最近关联变更\n\n"
f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。\n"
)
target_path.write_text(content.rstrip() + section, encoding="utf-8")
return True
def sync_associations():
changed_files = get_changed_md_files()
if not changed_files:
print("[sync-doc-assoc] No .md changes detected.")
return 0
date_str = datetime.now().strftime("%Y-%m-%d")
modified = []
for source_rel in changed_files:
source_path = PROJECT_ROOT / source_rel
if not source_path.exists():
continue
targets = extract_associations(source_path)
for target_rel in targets:
target_path = PROJECT_ROOT / target_rel
if not target_path.is_file():
# Skip directories, globs, or missing targets
continue
if target_rel == source_rel:
continue
done = (
append_changelog(target_path, source_rel, date_str)
or append_next_step(target_path, source_rel)
or append_recent_changes_section(target_path, source_rel, date_str)
)
if done:
modified.append((source_rel, target_rel))
if not modified:
print("[sync-doc-assoc] No associated docs need updating.")
return 0
print("[sync-doc-assoc] Updated associated docs:")
for source, target in modified:
print(f" - {source} -> {target}")
print("\n[sync-doc-assoc] Please review the changes with git diff before committing.")
return 0
if __name__ == "__main__":
sys.exit(sync_associations())