chore: 初始化 TFS 2013 工作项批量导入工具集

- TfsExcelImporter: WinForms 桌面版,走 TFS Client OM(本环境 wit REST 路由 404,OM 是唯一可行通道)
- tfs_import.py: REST 版命令行导入器,供 wit 接口可用的 TFS 实例使用
- templates: Excel 导入模板及其 OOXML 源部件
- README: 两套工具定位、配置、环境坑点与验证方式
- 真实台账与本机工作笔记不入库,仅提交 work_items.example.csv 说明列结构
This commit is contained in:
chenshoulin
2026-11-01 11:21:20 +08:00
commit 8d1af0d11c
23 changed files with 2423 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
# Build TFS import xlsx from XML parts as a stored (uncompressed) ZIP.
# Pure in-process .NET byte APIs: no external processes, no assembly loading.
# Output file name uses ASCII+char trick to stay encoding-safe.
$ErrorActionPreference = 'Stop'
$parts = Join-Path $PSScriptRoot '_parts'
$outName = 'TFS' + [char]0x5BFC + [char]0x5165 + [char]0x6A21 + [char]0x677F + '.xlsx'
$out = Join-Path $PSScriptRoot $outName
# ---------- CRC32 (IEEE 802.3) ----------
$tbl = New-Object 'System.Int64[]' 256
for ($i = 0; $i -lt 256; $i++) {
$c = [int64]$i
for ($k = 0; $k -lt 8; $k++) {
if (($c -band 1) -eq 1) { $c = (3988292384 -bxor ($c -shr 1)) } else { $c = ($c -shr 1) }
$c = $c -band 4294967295
}
$tbl[$i] = $c
}
function Get-Crc32([byte[]]$data) {
$crc = [int64]4294967295
foreach ($b in $data) {
$idx = [int](($crc -bxor [int64]$b) -band 255)
$crc = ($tbl[$idx] -bxor ($crc -shr 8)) -band 4294967295
}
return ($crc -bxor 4294967295)
}
# ---------- entry list (source relative path, zip entry name) ----------
$entries = @(
@{ src = '[Content_Types].xml'; zip = '[Content_Types].xml' },
@{ src = '_rels\.rels'; zip = '_rels/.rels' },
@{ src = 'xl\workbook.xml'; zip = 'xl/workbook.xml' },
@{ src = 'xl\_rels\workbook.xml.rels'; zip = 'xl/_rels/workbook.xml.rels' },
@{ src = 'xl\styles.xml'; zip = 'xl/styles.xml' },
@{ src = 'xl\worksheets\sheet1.xml'; zip = 'xl/worksheets/sheet1.xml' }
)
$ms = New-Object System.IO.MemoryStream
$central = New-Object System.Collections.ArrayList
foreach ($e in $entries) {
$srcPath = Join-Path $parts $e['src']
$bytes = [System.IO.File]::ReadAllBytes($srcPath)
$nameBytes = [System.Text.Encoding]::ASCII.GetBytes([string]$e['zip'])
$crc = Get-Crc32 $bytes
$offset = $ms.Position
$ms.Write([BitConverter]::GetBytes([uint32]0x04034b50), 0, 4) # local header signature
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method: stored
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # mod time
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # mod date 1980-01-01
$ms.Write([BitConverter]::GetBytes([uint32]$crc), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]$nameBytes.Length), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
$ms.Write($nameBytes, 0, $nameBytes.Length)
$ms.Write($bytes, 0, $bytes.Length)
[void]$central.Add(@{ name = $nameBytes; crc = $crc; size = $bytes.Length; offset = $offset })
}
# ---------- central directory ----------
$cdStart = $ms.Position
foreach ($c in $central) {
$ms.Write([BitConverter]::GetBytes([uint32]0x02014b50), 0, 4) # central header signature
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # made by
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # time
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # date
$ms.Write([BitConverter]::GetBytes([uint32]$c['crc']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]$c['name'].Length), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk start
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # internal attrs
$ms.Write([BitConverter]::GetBytes([uint32]0), 0, 4) # external attrs
$ms.Write([BitConverter]::GetBytes([uint32]$c['offset']), 0, 4)
$ms.Write($c['name'], 0, $c['name'].Length)
}
$cdSize = $ms.Position - $cdStart
# ---------- end of central directory ----------
$ms.Write([BitConverter]::GetBytes([uint32]0x06054b50), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk number
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # cd disk
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint32]$cdSize), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$cdStart), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
[System.IO.File]::WriteAllBytes($out, $ms.ToArray())
$ms.Dispose()
Write-Output ('CREATED ' + $out + ' size=' + (New-Object System.IO.FileInfo $out).Length)