<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>技术 &#8211; 张三太爷</title>
	<atom:link href="https://www.somedoc.net/category/%e6%8a%80%e6%9c%af/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.somedoc.net</link>
	<description>看前面，黑洞洞</description>
	<lastBuildDate>Tue, 13 Oct 2026 13:43:29 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://www.somedoc.net/wp-content/uploads/2016/12/cropped-dandycheung-1-32x32.jpg</url>
	<title>技术 &#8211; 张三太爷</title>
	<link>https://www.somedoc.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>GitHub Desktop 团队，也是草台班子</title>
		<link>https://www.somedoc.net/2026/10/13/github-desktop-%e5%9b%a2%e9%98%9f%ef%bc%8c%e4%b9%9f%e6%98%af%e8%8d%89%e5%8f%b0%e7%8f%ad%e5%ad%90/</link>
					<comments>https://www.somedoc.net/2026/10/13/github-desktop-%e5%9b%a2%e9%98%9f%ef%bc%8c%e4%b9%9f%e6%98%af%e8%8d%89%e5%8f%b0%e7%8f%ad%e5%ad%90/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 13:43:18 +0000</pubDate>
				<category><![CDATA[Windows]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6720</guid>

					<description><![CDATA[在电脑里看到的最讨厌的事情之一就是有的程序/组件的重复。Ch <a href="https://www.somedoc.net/2026/10/13/github-desktop-%e5%9b%a2%e9%98%9f%ef%bc%8c%e4%b9%9f%e6%98%af%e8%8d%89%e5%8f%b0%e7%8f%ad%e5%ad%90/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">在电脑里看到的最讨厌的事情之一就是有的程序/组件的重复。Chromium 的 CEF 最垃圾，但是惹不起啊，只能忍受，但是别的东东，有时候还是要抗争一下。</p>



<p class="wp-block-paragraph">例如 git。好些个工具都自己带这个玩意儿，但又有好些个总是要检查系统里有没有现成的。既然这样，给你们合二为一不好吗？于是，俺就把 GitHub Desktop 自带的 git 放到了环境变量里。</p>



<p class="wp-block-paragraph">好日子没过几天，某次突然发现在命令行里找不到 git 了。再一看，GitHub Desktop 升级了，它的安装路径下，有一个入口可执行程序，与之平级会有不止一个 <code>app-x.y.x</code> 的目录（很显然，x.y.z 是版本号），每个目录下都是一份完整的对应版本。一般来说，会总是保留最近的两个版本，大概是为了出了问题好回退。这样交替更迭几回，那么死守着旧版本的环境变量总归会失效，需要手动去系统设置里修改一下路径，把版本号改到最新的一版。烦。</p>



<p class="wp-block-paragraph">于是请 WorkBuddy 来帮忙，让它写个批处理（本人一般不会动用 PowerShell 这种重器）。使用它这两天优惠着的 Space-Bunny 模型，好家伙，批处理里面的坑把它折腾的死去活来，整整迭代了九轮，才算出来个能用的，看了下大小，接近 20KB，是我这辈子产出的个头最大的批处理文件了。</p>



<p class="wp-block-paragraph">思路特简单，就是在 app-x.y.z 平级创建一个名为 app-latest 的 Junction（这个单词把老夫搞住了，不知道怎么翻译，之前以为是软链接，但 AI 说不是），如果在命令行里找不到 git 了，就把批处理执行一下，没有这个 Junction 则创建之，有则更新之，而系统环境变量里永远是 latest 不需要改。</p>



<p class="wp-block-paragraph">事情就是这么巧。昨天刚刚把批处理搞定，app-latest 创建好，也指向了最新的 app-3.6.6 目录，GitHub Desktop 工作正常。今天，就在刚刚，把窗口切换到 GitHub Desktop 时，突然弹框报错，说 git 找不到了。拿脚趾头想都知道跟俺昨天的移花接木小手术有关。那个弹框是源源不断的，鼠标点击永远点不完。这个时候要上手速，Alt-F4 连续按，症状消失。到安装目录里查看，果然 app-3.6.7 赫然在目。但这个时候，你如果运行安装目录里的入口程序，则没有任何效果，光标转一两秒后没有任何情况发生。但是，如果双进入到任一版本的目录中，再执行其中的可执行文件，GitHub Desktop 就会正常运行起来。</p>



<p class="wp-block-paragraph">其实昨天写批处理的时候，跟 AI 已经就这个 Junction 的名字讨论过一番了，当时它害怕的是，GitHub Desktop 的升级程序会根据 app- 前缀来过滤出所有匹配的子目录，除去它自己标明为最新版的那个的话，其余的都会一概删除，而这里面一定会包括有 app-latest。老夫当时的逻辑是，app-latest 指向的不管是哪个版本，对于升级器来说一定是指向了一个旧版本，它删就删了。出于洁癖，命名依然定为 app-latest。但今天的铁定事实证明，入口程序也会被 app-latest 影响：因为当俺把 app-latest 的首字母改掉以后，入口程序就也可以加载到正确的最新版本而成功运行起来了。</p>



<p class="wp-block-paragraph">看来还得退避一舍，把批处理里的 app-latest 改成别的名字，就叫 ghd-latest 吧。</p>



<p class="wp-block-paragraph">尽管问题因咱而起，但正如题目所说，开发团队实在是也相当粗疏，app- 的前缀之后，好歹也该检查一下版本格式的。为了表彰 WorkBuddy 虽然资质一般但毕竟勤勤恳恳，把它生成的批处理附录于此。</p>



<pre class="crayon-plain-tag">@echo off
rem ===========================================================================
rem  GitHub Desktop - ghd-latest directory-link maintainer (pure batch)
rem
rem  Usage:
rem    ghd-app-latest.bat                create/update the link as needed
rem    ghd-app-latest.bat -CheckOnly     report only, change nothing
rem    ghd-app-latest.bat latest         use a different link name
rem    ghd-app-latest.bat -Debug         extra tracing while diagnosing
rem
rem  Exit code: 0 ok | 2 install not found | 3 refused | 4 link failed | 1 error
rem
rem  NOTE: keep this file pure ASCII with NO byte order mark.
rem        A BOM-less UTF-8 batch is read by cmd.exe byte by byte, and CJK
rem        text gets chopped into fragments that are then parsed as commands
rem        (it produced errors like "'URTGT' is not recognized"). All prompts
rem        here are ASCII on purpose, so the file works under any code page.
rem
rem  ---------------------------------------------------------------------------
rem  Implementation notes (all native cmd, no PowerShell):
rem    find install : reg query on uninstall keys, else %LOCALAPPDATA%
rem    match names  : pure string substitution, no findstr and no regex. A
rem                   caret-anchored /c:"^app-&#91;0-9]$" never matches inside cmd,
rem                   whatever findstr really receives is not what it looks like.
rem    pick version : split on dots, compare segment by segment numerically.
rem                   Never sort / compare as string - that puts 3.6.10 before
rem                   3.6.9. Also never take the first entry of dir /o-n:
rem                   that is time-ordered and our own ghd-latest sorts first.
rem    read target  : fsutil reparsepoint query, parse the "Substitute Name"
rem                   line. Do NOT use "Print Name" - for a junction its
rem                   length is always 0.
rem    create link  : mklink /J (directory junction). No mklink /D fallback:
rem                   /D needs admin or developer mode, so it fails anyway.
rem    delete link  : rmdir, never with the recursive switch.
rem  ---------------------------------------------------------------------------
setlocal EnableExtensions EnableDelayedExpansion

rem ===========================================================================
rem  Parse arguments
rem ===========================================================================
rem The default link name is "ghd-latest", NOT "app-latest". Squirrel removes
rem stale version directories with a loose test for names CONTAINING "app-",
rem and its recursive delete does not recognise reparse points. A link named
rem app-latest therefore gets destroyed on the next GitHub Desktop update,
rem and the delete follows the junction into the real version directory and
rem removes files from it. That was observed happening on this machine.
rem "ghd-latest" contains no "app-" and stays outside that sweep entirely.
set "LINKNAME=ghd-latest"
set "CHECKONLY=0"
set "DEBUG=0"

:ParseArgs
if "%~1"=="" goto AfterArgs
if /i "%~1"=="-CheckOnly" (set "CHECKONLY=1" &amp; shift &amp; goto ParseArgs)
if /i "%~1"=="/CheckOnly" (set "CHECKONLY=1" &amp; shift &amp; goto ParseArgs)
if /i "%~1"=="-Debug" (set "DEBUG=1" &amp; shift &amp; goto ParseArgs)
if /i "%~1"=="/Debug" (set "DEBUG=1" &amp; shift &amp; goto ParseArgs)
set "LINKNAME=%~1"
shift
goto ParseArgs
:AfterArgs
if "%DEBUG%"=="1" echo (debug mode on)

set "RC=0"
set "E_NOFIND=2"
set "E_REFUSE=3"
set "E_LINKFAIL=4"

echo.
echo GitHub Desktop - ghd-latest link maintainer
echo --------------------------------------------
echo   Link name: %LINKNAME%

rem ===========================================================================
rem  Guard: the link name must not look like a version directory
rem
rem  Done with substitution, not findstr. A caret-anchored regex on a cmd
rem  command line is unreliable (see the note at :ConsiderDir), and a guard
rem  that silently never fires is worse than no guard at all.
rem  Rule: reject if the name starts with "app-" followed by a digit.
rem
rem  Written as a subroutine that returns E_GUARD=1 when it must refuse, so
rem  that no goto ever appears inside a nested if/for block.
rem ===========================================================================
set "E_GUARD=0"
call :CheckGuard
if "%E_GUARD%"=="1" (
    echo    x Link name "%LINKNAME%" clashes with the app-VERSION naming rule.
    echo      Refusing to run. Pick another name, e.g. ghd-app-latest.bat latest
    set "RC=%E_REFUSE%"
    goto Finish
)

rem ===========================================================================
rem  1. Locate the install root
rem ===========================================================================
echo.
echo &#91; Locate install ]

set "ROOT="
for /f "tokens=2,*" %%A in ('reg query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s /v InstallLocation 2^>nul ^| findstr /i /c:"REG_SZ"') do (
    if exist "%%~B\GitHubDesktop.exe" set "ROOT=%%~B"
)
if not defined ROOT if exist "%LOCALAPPDATA%\GitHubDesktop\GitHubDesktop.exe" set "ROOT=%LOCALAPPDATA%\GitHubDesktop"

if not defined ROOT (
    echo    x GitHub Desktop install directory not found.
    echo      Checked: HKCU uninstall InstallLocation and %%LOCALAPPDATA%%\GitHubDesktop
    set "RC=%E_NOFIND%"
    goto Finish
)
echo    + Install root: %ROOT%

rem ===========================================================================
rem  2. Enumerate version directories
rem ===========================================================================
echo.
echo &#91; Version directories ]

set "CAND="
for /f "delims=" %%D in ('dir /b /ad "%ROOT%" 2^>nul') do call :ConsiderDir "%%D"

if not defined CAND (
    echo    x No complete and usable app-VERSION directory found.
    set "RC=%E_NOFIND%"
    goto Finish
)

rem ===========================================================================
rem  3. Pick the highest version (numeric, segment by segment)
rem ===========================================================================
echo.
set "BEST="
for %%D in (%CAND%) do call :PickBest "%%D"

if not defined BEST (
    echo    x Cannot determine the effective version.
    set "RC=%E_NOFIND%"
    goto Finish
)
echo    + Effective version: !BEST!

rem ===========================================================================
rem  4. Inspect the existing link
rem ===========================================================================
echo.
echo &#91; Link state ]
set "LINK=%ROOT%\!LINKNAME!"

if exist "%LINK%\" (
    call :ReadTarget
    if defined CURTGT (
        echo    !LINKNAME! points to: !CURTGT!
        if /i "!CURTGT!"=="%ROOT%\!BEST!" (
            echo.
            echo &#91; Result ]
            echo    Already up to date. Nothing to do.
            set "RC=0"
            goto Finish
        )
        echo    * !LINKNAME! points to an older directory, repointing it to !BEST! ...
    ) else (
        rem Target unreadable: if it is a link, it is broken (rebuild it);
        rem if dir cannot even list it, it is a real directory (refuse).
        dir /a:l "%LINK%" >nul 2>&amp;1
        if errorlevel 1 (
            echo    x "%LINKNAME%" exists but is NOT a link, it is a real directory.
            echo      Refusing to delete it. Please inspect it manually.
            set "RC=%E_REFUSE%"
            goto Finish
        )
        echo    * Found a broken link, no readable target, rebuilding.
    )
) else (
    echo    !LINKNAME! does not exist yet.
    echo    Creating it to point to !BEST! ...
)

if "%CHECKONLY%"=="1" (
    echo.
    echo &#91; Result ]
    echo    CheckOnly mode, nothing was changed.
    echo    It should point to: %ROOT%\!BEST!
    set "RC=0"
    goto Finish
)

rem ===========================================================================
rem  5. Risk warning
rem ===========================================================================
rem WARN=1 when the chosen link name contains app-, which puts the link
rem inside Squirrel's stale-version sweep. That is not theoretical: a link
rem named app-latest was destroyed by a real GitHub Desktop update, and the
rem recursive delete followed the junction and emptied the old version
rem directory it pointed at. The default ghd-latest avoids it entirely.
rem
rem When the chosen name IS out of range, also check whether a leftover link
rem from the old default is still sitting there, and say so.
set "WARN=0"
call :ContainsAppDash
if "%WARN%"=="1" (
    echo.
    echo &#91; Risk warning ]
    echo    * Squirrel removes old versions with a loose "name contains app-"
    echo    * test, not an exact app-DIGIT match, so "%LINKNAME%" is in range.
    echo    * A GitHub Desktop update can delete this link, and the recursive
    echo    * delete does not recognise reparse points, so it follows the link
    echo    * into the real version directory and removes files from it.
    echo      Observed happening with the previous name "app-latest".
    echo      Workaround: ghd-app-latest.bat latest
)

rem leftover check: the old default name is dangerous, flag it if still present
if not "%LINKNAME%"=="app-latest" (
    if exist "%ROOT%\app-latest\" (
        echo.
        echo &#91; Leftover warning ]
        echo    * %ROOT%\app-latest still exists.
        echo    * It is inside the Squirrel sweep and its target directory may
        echo    * already have been emptied. Delete it, you will not need it.
    )
)

rem ===========================================================================
rem  6. Rebuild the link
rem ===========================================================================
echo.
echo &#91; Update link ]

if exist "%LINK%\" (
    rem rmdir removes the link entry itself.
    rem Never add the recursive switch: it would empty the real target.
    rmdir "%LINK%" >nul 2>&amp;1
    if exist "%LINK%\" (
        echo    x Failed to remove the old link, permission denied?
        set "RC=%E_LINKFAIL%"
        goto Finish
    )
    echo    Old link removed, target directory untouched.
    if not exist "%ROOT%\!BEST!\GitHubDesktop.exe" (
        echo    x Target vanished during the operation, aborting.
        set "RC=%E_NOFIND%"
        goto Finish
    )
)

echo    Creating directory junction with mklink /J ...
rem No mklink /D fallback: it needs admin or developer mode, so on a normal
rem user session it always fails. A junction is enough for a local NTFS dir.
mklink /J "%LINK%" "%ROOT%\!BEST!" >nul 2>&amp;1
if not errorlevel 1 goto Verify

echo    x mklink /J failed, check free disk space and permissions.
set "RC=%E_LINKFAIL%"
goto Finish

:Verify
call :ReadTarget
if not defined CURTGT (
    echo    x Cannot read the target after creating the link.
    set "RC=%E_LINKFAIL%"
    goto Finish
)
if /i not "%CURTGT%"=="%ROOT%\!BEST!" (
    echo    x Verification failed, actual target: %CURTGT%
    set "RC=%E_LINKFAIL%"
    goto Finish
)
if not exist "%LINK%\GitHubDesktop.exe" (
    echo    x GitHubDesktop.exe is not reachable through the link.
    set "RC=%E_LINKFAIL%"
    goto Finish
)

echo.
echo &#91; Result ]
echo    + Path : %LINK%
echo    + Target: !CURTGT!
echo    + Verified: GitHubDesktop.exe is reachable through the link
set "RC=0"
goto Finish

rem ===========================================================================
rem  Finish: single exit point, pause only when double-clicked
rem ===========================================================================
:Finish
echo.
if "%RC%"=="0"   echo &#91;OK] Done, the link points at the effective version.
if "%RC%"=="1"   echo &#91;ERR] Script error, see the details above.
if "%RC%"=="2"   echo &#91;ERR] GitHub Desktop install not found, nothing changed.
if "%RC%"=="3"   echo &#91;ERR] Refused on safety grounds, see the reason above.
if "%RC%"=="4"   echo &#91;ERR] Link creation failed, see the reason above.
rem CMDCMDLINE holds this file name when double-clicked, and does not when
rem invoked from an existing prompt - that is how we tell the two apart.
echo %CMDCMDLINE% | find /i "%~nx0" >nul &amp;&amp; pause
endlocal &amp; exit /b %RC%

rem ===========================================================================
rem  :ContainsAppDash -- set WARN=1 when LINKNAME contains "app-"
rem ===========================================================================
:ContainsAppDash
set "WARN=0"
set "WCOPY=!LINKNAME!"
set "WFOUND="
:ContainsLoop
if "!WCOPY!"=="" goto ContainsDone
if not defined WFOUND (
    if "!WCOPY:~0,4!"=="app-" set "WFOUND=1"
)
set "WCOPY=!WCOPY:~1!"
goto ContainsLoop
:ContainsDone
if defined WFOUND set "WARN=1"
exit /b 0

rem ===========================================================================
rem  :CheckGuard -- set E_GUARD=1 when LINKNAME looks like app-&lt;digit>...
rem ===========================================================================
:CheckGuard
set "E_GUARD=0"
set "GHEAD=!LINKNAME:~0,4!"
if not "!GHEAD!"=="app-" exit /b 0
set "GTAIL=!LINKNAME:~4!"
if "!GTAIL!"=="" exit /b 0
rem the character right after app- must be a digit
set "GONE=!GTAIL:~0,1!"
if "!GONE!"=="0" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="1" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="2" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="3" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="4" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="5" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="6" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="7" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="8" set "E_GUARD=1" &amp; exit /b 0
if "!GONE!"=="9" set "E_GUARD=1" &amp; exit /b 0
exit /b 0

rem ===========================================================================
rem  :ConsiderDir "&lt;dirname>" -- keep it in CAND if it is a usable version dir
rem
rem  This is a subroutine on purpose. It used to be a label INSIDE the
rem  "for /f ... do ( ... )" block, with "goto SkipD" as the skip path. That
rem  is the classic batch trap: goto into a block label leaves the parser in
rem  the wrong context, so the block's closing ")" is reported as
rem  ") was unexpected at this time.". A goto that jumps OUT of a block is
rem  fine; jumping INTO one is not. Using "call" keeps every goto outside.
rem
rem  Same reason it does not use setlocal: it must append to the caller's
rem  CAND list.
rem
rem  NOTE: the argument is held in DNAME, never in CD. "%CD%" is a cmd dynamic
rem  variable that expands to the current working directory.
rem
rem  The name test uses string substitution, NOT findstr and NOT a regex.
rem  Debug output proved that inside cmd a double quoted "/c:^app-&#91;0-9]&#91;0-9.]*$"
rem  never matches, whatever findstr receives is not what we think it gets
rem  (caret and dollar are cmd metacharacters on that command line).
rem  Substitution has no such ambiguity:
rem      1. must start with app-
rem      2. strip app-, the remainder must not be empty
rem      3. the remainder must consist only of digits and dots
rem  Verified to accept exactly the same set as the old regex on real data,
rem  plus it additionally rejects mixed case like APP-1.2.
rem ===========================================================================
:ConsiderDir
set "DNAME=%~1"

rem --- name test by substitution, see the note above ---
rem must begin with app-
if "!DNAME:~0,4!" neq "app-" exit /b 0
rem strip the app- prefix, the remainder must not be empty
set "TAIL=!DNAME:~4!"
if "!TAIL!"=="" exit /b 0
rem delete every digit and dot; whatever survives must be empty.
rem Written out longhand instead of a "for %%d in (...)" loop: this routine is
rem reached through CALL, and loop-variable expansion inside a called
rem subroutine is one of the places where it is easy to get the wrong value.
set "REST=!TAIL!"
set "REST=!REST:0=!"
set "REST=!REST:1=!"
set "REST=!REST:2=!"
set "REST=!REST:3=!"
set "REST=!REST:4=!"
set "REST=!REST:5=!"
set "REST=!REST:6=!"
set "REST=!REST:7=!"
set "REST=!REST:8=!"
set "REST=!REST:9=!"
set "REST=!REST:.=!"
if not "!REST!"=="" exit /b 0

if not exist "!ROOT!\!DNAME!\GitHubDesktop.exe" exit /b 0
if exist "!ROOT!\!DNAME!\.dead" exit /b 0
if exist "!ROOT!\!DNAME!\.not-finished" exit /b 0
set "CAND=!CAND! !DNAME!"
echo     !DNAME!
exit /b 0

rem ===========================================================================
rem  :ReadTarget -- read the reparse point target of %LINK% into CURTGT
rem
rem  fsutil prints:
rem      Substitute Name:       \??\C:\path\to\app-3.6.6
rem  tokens=2,* skips the two words "Substitute" and "Name:", so %%B is the
rem  path. The "Print Name" line is unusable, its length is 0 for junctions.
rem  Do NOT use setlocal here, CURTGT could not reach the caller.
rem ===========================================================================
:ReadTarget
set "CURTGT="
set "SUB="
for /f "tokens=2,*" %%A in ('fsutil reparsepoint query "%LINK%" 2^>nul ^| findstr /i /c:"Substitute Name:"') do set "SUB=%%B"
if not defined SUB exit /b 0
rem strip the \??\ reparse prefix to get a normal drive path.
rem Delayed expansion is required here: with "%SUB:...=%" the percent form is
rem expanded while the line is read, so it would pick up the value from the
rem PREVIOUS call rather than the one just assigned on the line above.
set "CURTGT=!SUB:\??\=!"
rem %%~fP makes it absolute and drops the trailing backslash, so that the
rem caller can compare it with plain string equality
if defined CURTGT for %%P in ("!CURTGT!") do set "CURTGT=%%~fP"
exit /b 0

rem ===========================================================================
rem  :PickBest "&lt;dirname>" -- compare version numbers, replace BEST if larger
rem
rem  CALL is used to reuse code, so every routine works on global variables and
rem  must NOT use setlocal (values would not travel back to the caller).
rem  Comparison uses set /a and deliberately no zero padding, because set /a
rem  reads a leading zero as octal and fails on 08.
rem
rem  NOTE: the candidate is held in DNAME, never in CD - "%CD%" is a cmd
rem  dynamic variable holding the current working directory.
rem ===========================================================================
:PickBest
set "DNAME=%~1"
rem String substitution in the delayed-expansion family uses "!" at BOTH ends:
rem   !VAR:find=replace!
rem Writing "%" at the end (as this used to: !DNAME:app-=%) makes cmd treat
rem the trailing "%" as the start of a percent expansion, the substitution
rem never happens, and CV ends up holding the literal text "DNAME:app-=".
rem The debug output showed exactly that, which is why every version compared
rem equal and the first candidate always won.
set "CV=!DNAME:app-=!"

if not defined BEST (
    set "BEST=!DNAME!"
    goto PBEnd
)

rem split the current BEST version into B1..B4
rem Each variable is assigned from its own explicit token index. An earlier
rem version used "if not defined B1 set "B1=%%x"" and relied on the token
rem variables rotating as the for loop iterated; that mixes parse-time
rem (%%x) expansion with run-time ("if not defined") state and is fragile
rem inside a CALLed routine.
rem NOTE: every option of "for /f" must live inside ONE quoted string. Writing
rem for /f "tokens=1" delims=. %%x splits it into two parts and cmd reports
rem "delims was unexpected at this time."
set "BV=!BEST:app-=!"
set "B1=0" &amp; set "B2=0" &amp; set "B3=0" &amp; set "B4=0"
for /f "tokens=1 delims=." %%x in ("!BV!") do set "B1=%%x"
for /f "tokens=2 delims=." %%x in ("!BV!") do set "B2=%%x"
for /f "tokens=3 delims=." %%x in ("!BV!") do set "B3=%%x"
for /f "tokens=4 delims=." %%x in ("!BV!") do set "B4=%%x"

rem split the candidate version into C1..C4
set "C1=0" &amp; set "C2=0" &amp; set "C3=0" &amp; set "C4=0"
for /f "tokens=1 delims=." %%x in ("!CV!") do set "C1=%%x"
for /f "tokens=2 delims=." %%x in ("!CV!") do set "C2=%%x"
for /f "tokens=3 delims=." %%x in ("!CV!") do set "C3=%%x"
for /f "tokens=4 delims=." %%x in ("!CV!") do set "C4=%%x"

rem Segment by segment compare. Written flat on purpose: a chain of nested
rem if-blocks with goto in between is easy to break, and jumping into or
rem across block labels is exactly what caused ") was unexpected at this
rem time." earlier. Here every goto stays at the top level of the routine.
call :Seg !C1! !B1!
if "!CMP!"=="LT" goto PBEnd
if "!CMP!"=="GT" goto PBSet

call :Seg !C2! !B2!
if "!CMP!"=="LT" goto PBEnd
if "!CMP!"=="GT" goto PBSet

call :Seg !C3! !B3!
if "!CMP!"=="LT" goto PBEnd
if "!CMP!"=="GT" goto PBSet

call :Seg !C4! !B4!
if "!CMP!"=="GT" goto PBSet

rem All four segments are equal: keep the earlier one. Two directories with
rem the same version would be abnormal, and not overwriting is the safer
rem choice.
goto PBEnd

:PBSet
set "BEST=!DNAME!"
:PBEnd
exit /b 0

rem ===========================================================================
rem  :Seg &lt;A> &lt;B> -- numeric compare, result in CMP: GT / LT / EQ
rem
rem  The operands arrive as command line arguments. "%~1" is the correct way
rem  to read them: the tilde modifier belongs to the percent family only, so
rem  "!~1!" expands to nothing at all. That silently made both operands 0,
rem  every comparison returned EQ, and BEST stayed on the first candidate
rem  (it picked app-3.6.5 over app-3.6.6).
rem ===========================================================================
:Seg
set "SBA=%~1"
set "SBB=%~2"
if "!SBA!"=="" set "SBA=0"
if "!SBB!"=="" set "SBB=0"
rem "set /a X=X+0" normalises any stray leading zero, which set /a would
rem otherwise read as octal.
set /a "SBA=SBA+0" 2>nul
set /a "SBB=SBB+0" 2>nul
if !SBA! GTR !SBB! (set "CMP=GT") else if !SBA! LSS !SBB! (set "CMP=LT") else (set "CMP=EQ")
exit /b 0</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/10/13/github-desktop-%e5%9b%a2%e9%98%9f%ef%bc%8c%e4%b9%9f%e6%98%af%e8%8d%89%e5%8f%b0%e7%8f%ad%e5%ad%90/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>得偿所望之记事本</title>
		<link>https://www.somedoc.net/2026/10/09/%e5%be%97%e5%81%bf%e6%89%80%e6%9c%9b%e4%b9%8b%e8%ae%b0%e4%ba%8b%e6%9c%ac/</link>
					<comments>https://www.somedoc.net/2026/10/09/%e5%be%97%e5%81%bf%e6%89%80%e6%9c%9b%e4%b9%8b%e8%ae%b0%e4%ba%8b%e6%9c%ac/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Fri, 09 Oct 2026 05:00:20 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[生活]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6715</guid>

					<description><![CDATA[使用 Windows 工作多年，每天输入文本，使用过的文本编 <a href="https://www.somedoc.net/2026/10/09/%e5%be%97%e5%81%bf%e6%89%80%e6%9c%9b%e4%b9%8b%e8%ae%b0%e4%ba%8b%e6%9c%ac/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">使用 Windows 工作多年，每天输入文本，使用过的文本编辑的工具真的可以说是五花八门不计其数，但你要问起来，在感觉上最顺手的是哪个，可能诸位会意想不到：记事本。</p>



<p class="wp-block-paragraph">DOS 年代实在太过久远，单行编辑的窘境只残留在某些上古传说里，亲身体会的场景，也就命令提示符下差可比拟。你就是把 DOS 自带的 Edit 从头数落到脚，人家也是个全须全尾的全屏编辑器。以 Borland 公司倡导的 Turbo Vision 字符界面框架没有走几步，Windows 时代（尤其是 Windows 95）就呼啸而至了。</p>



<p class="wp-block-paragraph">记事本成了最便捷的录入工具——但是，也最是恨铁不成钢。见得越频繁，怨得越频繁。三大痛点：</p>



<ul class="wp-block-list">
<li>没有语法高亮；</li>



<li>打开大文件时容易白屏挂死在那儿；</li>



<li>字体不是默认等宽。</li>
</ul>



<p class="wp-block-paragraph">为了免遭这些缺点的困扰，又不知尝试过多少编辑器。有 EditPlus、UltraEdit，还有什么 Notepad++、Notepad2/3/4（这么些个版本后缀，但并不是同一个软件），再有什么 Sublime Text 等等。它们往往在上述痛点的某一个或者某几个方面都触达到了优秀的地步，但又往往会带来其它难以忍受的局面：</p>



<ul class="wp-block-list">
<li>收费，需要破解才能用，或者即使不用破解也凑合能用，但是一打开就有一个烦人提示框要先关闭，标题上故意恶心添加上“未注册版本”的字样；</li>



<li>太丑；这方面 Notepad++ 是典型，我一看它那个标签页，隔夜饭就快要哕出来，后面竟然还有什么民族主义的怪异东西裹入；</li>



<li>太大、笨重；例如 UltraEdit，启动的时候拖泥带水，看见界面显示全乎感觉就要好久；</li>



<li>对中文在内的各种非英语语言支持不好，有的甚至会出现按下回退键只能删除半个汉字的情况（近年来此状况已经有很大好转）；</li>
</ul>



<p class="wp-block-paragraph">十多年前，俺就曾想自己写一个文本编辑的小工具，对标就是系统的记事本，在保留它几乎所有我喜欢的优点的前提下把开头列出的几个问题顺便满足那么一下子。当时起了个头，项目名称为 danedit，意即“Dandy Editor”，然后，就没有然后了。谁让移动时代到来了呢？:)</p>



<p class="wp-block-paragraph">前几天把 TWL（三太爷的一个小型 Windows 开发类库）与时俱进之后，这个念头又涌了上来。这些年，文本编辑器是用 AkelPad 凑合着，这已经是能找到最折中趁手的了，但我觉得仍然不够轻盈。不得不说，AI，给我智慧给我胆，越是艰险越向前。</p>



<p class="wp-block-paragraph">就这样，一时冲动开启了新的项目，名字朴实无华：TextEdit。做了一天之后，突然回想到了 danedit，于是让 AI 检查一下那里面还有没有值得提取的内容，AI 说：</p>



<figure class="wp-block-image size-full"><a href="https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103.png" class="highslide-image" onclick="return hs.expand(this);"><img fetchpriority="high" decoding="async" width="972" height="865" src="https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103.png" alt="" class="wp-image-6716" srcset="https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103.png 972w, https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103-300x267.png 300w, https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103-768x683.png 768w, https://www.somedoc.net/wp-content/uploads/2026/10/DanEdit-之与往事干杯-屏幕截图-2026-10-07-133103-562x500.png 562w" sizes="(max-width: 972px) 100vw, 972px" /></a></figure>



<p class="wp-block-paragraph">老夫从善如流，Shift+Delete 一按，与往事干杯。</p>



<p class="wp-block-paragraph">昨天一天，在搞 TextEdit 的暗黑模式。别看系统发展到今天，看上去已经很完善的样子了似的，但对于 TextEdit 这种专门选择了从系统基础的 SDK API 开发的程序，在 Win32 这个层面上，微软并没有开放稳定的接口用于切换主题模式。但即使从三太爷本人的使用体验来说，暗黑模式也是个无法忽视的基本需求了。AI 一开始无计可施，说难度很大，老夫只好亲自出马，翻找前人遗珍，终有所获。完成之后迫不及待找三五好友帮着试用，除了某些安全怪咖凛然拒绝运行这一可执行文件外，其他选手还算配合。</p>



<p class="wp-block-paragraph">截至半夜，已经收来两个 bug：1. 有些对话框界面里的文字显示会被截断；2. 有时候编辑区会花屏。前者经过俺追问验证，果然是显示器 DPI 惹的祸，而后者虽然概率低，但却严重性高。最终排查的结果是，AI 写代码时忽略了 TWL 的一个原则性的 C++ 对象生命周期的管理方案，导致构造时采用了错误的方式。于是连忙于今早进行 bug 修复。</p>



<p class="wp-block-paragraph">到现在，TextEdit 的大概特点如下：</p>



<ul class="wp-block-list">
<li>单文档界面，支持常见编辑操作；</li>



<li>内置了若干种常见编程语言的高亮规则；</li>



<li>默认强制等宽字体，且一个中文严格等宽两个 ASCII 字符（字体暂不支持自定义）；</li>



<li>支持比较快速地打开体积比较大的文件（100 MB 左右的 JSON 文件大概 1 秒多的样子）；</li>



<li>支持暗黑模式（可强制指定，或者跟随系统）；</li>



<li>理论上能后向兼容至 Windows XP 64 位（32 位的后向兼容可达 Windows 95/NT4，但当前未构建测试）；</li>



<li>单可执行文件，体积 714 KB（1.0.0.93）。</li>
</ul>



<p class="wp-block-paragraph">图标是 WorkBuddy 为它制作的。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://www.somedoc.net/wp-content/uploads/2026/10/得偿所望.jpg" class="highslide-image" onclick="return hs.expand(this);"><img decoding="async" width="600" height="329" src="https://www.somedoc.net/wp-content/uploads/2026/10/得偿所望.jpg" alt="" class="wp-image-6717" srcset="https://www.somedoc.net/wp-content/uploads/2026/10/得偿所望.jpg 600w, https://www.somedoc.net/wp-content/uploads/2026/10/得偿所望-300x165.jpg 300w" sizes="(max-width: 600px) 100vw, 600px" /></a></figure>
</div>]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/10/09/%e5%be%97%e5%81%bf%e6%89%80%e6%9c%9b%e4%b9%8b%e8%ae%b0%e4%ba%8b%e6%9c%ac/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>TWL 整葺二三事</title>
		<link>https://www.somedoc.net/2026/10/01/twl-%e6%95%b4%e8%91%ba%e4%ba%8c%e4%b8%89%e4%ba%8b/</link>
					<comments>https://www.somedoc.net/2026/10/01/twl-%e6%95%b4%e8%91%ba%e4%ba%8c%e4%b8%89%e4%ba%8b/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Thu, 01 Oct 2026 12:25:28 +0000</pubDate>
				<category><![CDATA[Windows]]></category>
		<category><![CDATA[技术]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6708</guid>

					<description><![CDATA[花有重开日，人无再少年。可任谁也没想到，二十年后，TWL 还 <a href="https://www.somedoc.net/2026/10/01/twl-%e6%95%b4%e8%91%ba%e4%ba%8c%e4%b8%89%e4%ba%8b/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">花有重开日，人无再少年。可任谁也没想到，二十年后，TWL 还能借助最新的技术进展，在 AI 的帮助下完善、增强。</p>



<p class="wp-block-paragraph">先是把 64 位支持的缺陷（当时还不太急迫，所以只是个缺点；而现在显然就是缺陷了）补上，又把之前一些零星散落在各个测试工程里的有用的控件（如 TreeList）、组件（如 Caption Button、Menu Pin）、功能函数（如 Docking 能力、msimg32 软实现等）、代码片段做了一些整合，融入到 TWL 的整体中来。</p>



<p class="wp-block-paragraph">当基本上告一段落时，三太爷又想尝试一下看能否把一个现成的 SDK 或者 MFC 程序迁移到 TWL 上来，以便观察整体的难度，以及考察 TWL 本身的完整性和适应性。思来想去，选了一个奇怪的目标：Windows 系统中期里的画笔程序。</p>



<p class="wp-block-paragraph">先是到 GitHub 上找到了之前微软泄漏出来的源代码树：</p>



<ul class="wp-block-list">
<li>Windows NT 4：<a href="https://github.com/ZoloZiak/WinNT4">https://github.com/ZoloZiak/WinNT4</a>（貌似仅内核部分）；</li>



<li>Windows 2000：<a href="https://github.com/gameprive/win2k">https://github.com/gameprive/win2k</a>；</li>



<li>Windows XP：<a href="https://github.com/ufwt/windows-XP-SP1">https://github.com/ufwt/windows-XP-SP1</a>；</li>



<li>Windows Server 2003：<a href="https://github.com/selfrender/Windows-Server-2003">https://github.com/selfrender/Windows-Server-2003</a>。</li>
</ul>



<p class="wp-block-paragraph">Windows NT 4 的代码里没有画笔的部分，显然是上传的人没有完整上传（因为根据之前的印象，应用层的很多代码在那次的泄露内容中是有的）。既然如此，那就到 Windows 2000 里去找，GitHub 的项目内查找做得快如闪电，一击命中，位置在 <code>private/windows/shell/accesory/mspaint</code> 下，同时为了证实，也去 Windows XP 的那个仓库里看了一眼，也有，<code>XPSP1/NT/shell/osshell/accesory/mspaint</code>。</p>



<p class="wp-block-paragraph">都取下来，出于好奇还比较了一下，发现相差无几。让反重力出个迁移至 TWL 的方案，它的结论有模有样，建议为了不至于对整个项目的代码大肆翻腾，除了把缺的一些边角自动补齐外，建议做一个 MFC 到 TWL 的“垫层”来对接画笔现有代码和 TWL。甚有工程思维，准了。</p>



<p class="wp-block-paragraph">原以为它语气笃定、轻描淡写就出来的方案会很容易，实在没想到它吭哧憋肚出来的第一个可以运行的版本用时竟然在小时这个量级上。跑起来的时候我觉得这一定就是 TWL 的版本了，更没成想反重力说的是：现在咱们把第一阶段要补的都补齐了，下一个阶段计划把 OLE 服务器的特性去除掉，然后再对接 TWL 怎么样？还能怎么样，当然是你说怎样就怎样了。剥离 OLE 特性好像用时不多，接下来就是老夫在体验 AI 这么长的时间里遇到的第一个比较漫长的单一任务了：它来来回回调整一个叫 <code>twlmfc.h</code> 的头文件，直至最终构建出 TWL 版的画笔程序来，耗时六个半小时。其后又花了大概略逊但接近的时间，把我体验过程中发现的共计十个 bug 修复了。TWL 版的画笔初步落成。</p>



<p class="wp-block-paragraph">整个过程中没有提出 TWL 需要调整的建议，而耗时较长的主要原因在于，TWL 没有对 MFC 里非常重头的文档-视图结构进行支持。总的来说，结果还不错。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">刚刚在 GitHub 的搜索结果里多翻了几页，还有几个仓库：</p>



<ul class="wp-block-list">
<li><a href="https://github.com/Hengle/windows_nt_3_5_source_code">https://github.com/Hengle/windows_nt_3_5_source_code</a>；</li>



<li><a href="https://github.com/lianthony/NT4.0">https://github.com/lianthony/NT4.0</a>（较前文另一链接的内容更多）；</li>



<li><a href="https://github.com/tongzx/nt5src">https://github.com/tongzx/nt5src</a>；</li>



<li><a href="https://github.com/Hengle/windows_2000_source_code">https://github.com/Hengle/windows_2000_source_code</a>。</li>
</ul>



<p class="wp-block-paragraph">也许跟更上面的一样，聊做备份。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/10/01/twl-%e6%95%b4%e8%91%ba%e4%ba%8c%e4%b8%89%e4%ba%8b/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bambook 桌面版工具筹备中</title>
		<link>https://www.somedoc.net/2026/09/13/bambook-%e6%a1%8c%e9%9d%a2%e7%89%88%e5%b7%a5%e5%85%b7%e7%ad%b9%e5%a4%87%e4%b8%ad/</link>
					<comments>https://www.somedoc.net/2026/09/13/bambook-%e6%a1%8c%e9%9d%a2%e7%89%88%e5%b7%a5%e5%85%b7%e7%ad%b9%e5%a4%87%e4%b8%ad/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 14:44:46 +0000</pubDate>
				<category><![CDATA[备忘录]]></category>
		<category><![CDATA[技术]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6703</guid>

					<description><![CDATA[老规矩，肯定是先收集资料。 找到一个现成的项目，很老很老了（ <a href="https://www.somedoc.net/2026/09/13/bambook-%e6%a1%8c%e9%9d%a2%e7%89%88%e5%b7%a5%e5%85%b7%e7%ad%b9%e5%a4%87%e4%b8%ad/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">老规矩，肯定是先收集资料。</p>



<p class="wp-block-paragraph">找到一个现成的项目，很老很老了（当然 Bambook 本来就很老很老了）：<a href="https://github.com/zhanzushun/Bambook-Factory">https://github.com/zhanzushun/Bambook-Factory</a>。</p>



<p class="wp-block-paragraph">另一个看着也有关，先列到这儿：<a href="https://github.com/quake/BambookPlugin">https://github.com/quake/BambookPlugin</a>。这里的 bambook-sdk 看上去是官方的，就是不知道版本是不是最后更新的。</p>



<p class="wp-block-paragraph">还查出来当年盛大的一个技术人员（似乎就是 SDK 团队的？）的一篇文章：《<a href="https://zbw25.wordpress.com/2010/11/11/%e5%bc%80%e5%8f%91%e5%ba%94%e7%94%a8%e7%a8%8b%e5%ba%8f-%e7%99%bd%e6%8b%bfbambook/" data-type="link" data-id="https://zbw25.wordpress.com/2010/11/11/%e5%bc%80%e5%8f%91%e5%ba%94%e7%94%a8%e7%a8%8b%e5%ba%8f-%e7%99%bd%e6%8b%bfbambook/">开发应用程序 白拿 Bambook</a>》。</p>



<p class="wp-block-paragraph">SDK 的下载就得到互联网历史博物馆里翻腾了<a href="https://web.archive.org/web/20140904082059/http://bbsdk.sdo.com/download_sdk.do">https://web.archive.org/web/20140904082059/http://bbsdk.sdo.com/download_sdk.do</a>。只可惜，有链接，没有文件了，例如 C++ 的最终版本真实链接是 <a href="http://static.bbsdk.sdo.com/sdk_src/BBSDK_0.9.7.0_CPP.zip">http://static.bbsdk.sdo.com/sdk_src/BBSDK_0.9.7.0_CPP.zip</a>，这顶多知道个文件名。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/09/13/bambook-%e6%a1%8c%e9%9d%a2%e7%89%88%e5%b7%a5%e5%85%b7%e7%ad%b9%e5%a4%87%e4%b8%ad/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>黑苹果的转战</title>
		<link>https://www.somedoc.net/2026/04/05/%e9%bb%91%e8%8b%b9%e6%9e%9c%e7%9a%84%e8%bd%ac%e6%88%98/</link>
					<comments>https://www.somedoc.net/2026/04/05/%e9%bb%91%e8%8b%b9%e6%9e%9c%e7%9a%84%e8%bd%ac%e6%88%98/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 05 Apr 2026 14:59:59 +0000</pubDate>
				<category><![CDATA[macOS]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[软件]]></category>
		<category><![CDATA[问题解决]]></category>
		<category><![CDATA[黑苹果]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6673</guid>

					<description><![CDATA[家里用的主力机，原来也曾有过更换的经历，例如有一段时间就是  <a href="https://www.somedoc.net/2026/04/05/%e9%bb%91%e8%8b%b9%e6%9e%9c%e7%9a%84%e8%bd%ac%e6%88%98/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">家里用的主力机，原来也曾有过更换的经历，例如有一段时间就是 Latitude 5400，不过用过一阵之后，还是用回了 ThinkPad T450s。原因说起来特别简单，尽管 T450s 的 CPU 比较羸弱，但是在把内存升级到板载的 4GB 再加上 16GB 条子后，已经能代偿一部分。最重要的是，随着时间推移，机子里的 SSD 都升级到了 1TB，三块盘各自为政，Windows、Ubuntu、macOS 相安无事，巴适得很。</p>



<p class="wp-block-paragraph">可是逐步地，CPU 的短板还是被逐渐感知到了。更何况近些时来，大家热热闹闹地玩 AI，这个家伙不管是对 CPU 还是内存/GPU，都不会特别迁就。所以故技重施，先把办公室的干活儿主力机升了升格，从 Latitude 5401 升级到 Latitude 5431，再把退下来的 Latitude 5401 搬回家里来用（因为它也可以支持三硬盘）。不过比较讨厌的是，它已经不支持 BIOS/MBR 引导了，而且虽然没有实测，可直觉上其 m.2 插槽也不会支持 SATA 协议的 SSD 了。这就是说，现有的 T450s 上的三个系统，每一个都得换盘，不仅如此，还要视情况进行针对性的外科手术式迁移适配。</p>



<p class="wp-block-paragraph">最先，也最顺利的是 Windows 10，毕竟咱有积累的<a href="https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/" data-type="link" data-id="https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/">经验</a>打底。Ubuntu 的使用频度目前在家里不算高，因此暂时搁置。比较不好搞的是那份黑苹果。从以往的历篇博文也可以看出，在 T450s 上表现完美的Monterey 时至今日也已略显过时，而升级到 Sequoia 之后，本身在 T450s 上还算不得完美。历尽艰难，才把升级后遇到的无法登录的问题解决掉，而进入桌面后，陆续又发现一些使用上的毛刺。一是触控板驱动的不太对头，想触发点击操作，需要在触控板上连点两次；二是显示屏的亮度无法调节，这个情况应该是和显卡有关，理由是当我把这款盘从双显卡的 T450s 上平移到另一台只有核显的 T450s 上之后，调节亮度的控件就会正常出现；第三个问题也比较古怪，那就是如果把外置电池接上的话，系统引导的时候会卡死。基于这些，心想正好把黑苹果也干脆迁到 Latitude 5401 上就好了。</p>



<p class="wp-block-paragraph">找了一块闲置的 m.2 的 SSD，用 DiskGenius 的磁盘克隆功能成功把原来 NGFF SSD 上的内容克隆了过来。需要说明的是，这份黑苹果目前面临着两个问题：1. 如何在 Latitude 5401 上才能引导起来；2. 它自身的状态与 Latitude 5401 是否能兼容。第一个问题的解决方法很标准，那就是在网络上找是否有现成的 EFI 被人分享了出来，幸运的很，确实有，在<a href="https://github.com/TeksuSiK/hackintosh-5401">这里</a>。不过在看作者的文档时，发现了一个之前少有的要求：需要使用技术手段微调这台本子的固件界面里不曾体现的选项。途径则是在 OpenCore 引导时，按空格键调出额外的引导项，进入到 OpenShell 控制台，按需执行 setup_var.efi 命令。要调整项的偏移和值文档中有现成的，但作者又提醒说，各人应该自行确认一下，以免失误导致设备变砖。于是三太爷不得不阅读 <a href="https://github.com/datasone/setup_var.efi">setup_var.efi 的文档</a>，按步骤操作。首先去下载了 Intel 的 <a href="https://github.com/CE1CECL/IntelCSTools" data-type="link" data-id="https://github.com/CE1CECL/IntelCSTools">CSME System Tools 工具集</a>中的 Flash Programming Tool（FPTW64.exe），执行 <code>FPTW64.exe -bios -d bios-dump.bin</code> 把设备当前的 BIOS 转储，然后使用 <a href="https://github.com/LongSoft/UEFITool">UEFITool</a> 把所需的 <code>Setup/*</code> 节抽取出来，再用 <a href="https://github.com/LongSoft/IFRExtractor-RS">IFRExtractor RS</a> 把配置信息转为文本格式。最后打开转换后的文本文件，一一核对提及的需要修改的配置项的偏移，顺便把当前值记录下来，以便将来可能需要恢复回去。还好，所有的偏移均与 EFI 的作者列出的一致。</p>



<p class="wp-block-paragraph">作为一个好奇宝宝，显然在修改之前必须尝试一次引导，以观察不修改的话会有什么后果。后果其实很直接：不能引导成功，会重启。老老实实修改后，引导进入到了 macOS 的安装器界面。令人痛苦的是，界面的文字非中非英，一眼看去像是俄文。为了后续的步骤可以看明白，总得把这个界面显示问题解决掉。问了 AI 若干次，也按照它的回复对 OpenCore 的 config.plist 调整了若干次，统统无效（例如在 boot-args 里加上 -apple-language en_US，并指定 SystemLanguage 等）。最后没辙儿了，拿出手机打开 Google 翻译，照着屏幕一看，翻译 app 判定显示语言为匈牙利语。靠着它，才发现第二菜单下的唯一下拉菜单项其实就是语言切换的入口，这样一来，引导问题算是基本搞定。</p>



<p class="wp-block-paragraph">接下来就要尝试用这份 EFI 引导一下前面克隆完成的 SSD 上的 macOS Sequoia 了。把 SSD 插入插槽，用带着定制了的 EFI 的 macOS 安装引导 U 盘启动，结果是令人意想不到的失望，一而再再而三地重启，根本不能像之前那样进入到安装界面。后来不禁想，不会是 U 盘自己输了什么问题了吧，决定验证下。把 SSD 取下，用最简单的环境来测试，结果这次好了，又能进入安装界面（也是 macOS 的恢复界面）了。这下恍然大悟，看来是 SSD 在作祟。赶忙到搜索引擎里查询，果然<a href="https://zhuanlan.zhihu.com/p/698148497">有人早就警示</a>：若干多的 SSD 与黑苹果相克，而老夫所用的海力士 BC711 赫然在列。好像说西部数据的好一些。</p>



<p class="wp-block-paragraph">于是到闲鱼上查访，发现 SSD 这半年涨价的实在离谱，西数的 SN520，512GB 的竟然要 350 元上下（这还不算更离谱的标价）。要知道去年十月份，1TB 的 SSD 也就这个价。唉，黑苹果的迁移大业，先停停吧。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/04/05/%e9%bb%91%e8%8b%b9%e6%9e%9c%e7%9a%84%e8%bd%ac%e6%88%98/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>BIOS/MBR 引导的 Windows 10 就地转换为 UEFI/GPT 引导实操</title>
		<link>https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/</link>
					<comments>https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Thu, 12 Mar 2026 05:11:08 +0000</pubDate>
				<category><![CDATA[Windows]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[电脑]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6657</guid>

					<description><![CDATA[有一块祖传的 SATA SSD 上，有一份 Windows  <a href="https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">有一块祖传的 SATA SSD 上，有一份 Windows 10，引导模式还停留在 BIOS/MBR 状态。想把它接续着用起来，但是新机器已经放不下 SATA 盘了，而且也不支持传统引导方式了。正好有一块同容量的 NVMe SSD，于是产生把系统克隆过来，改造后再使用的想法。</p>



<p class="wp-block-paragraph">在 Windows 10 下，把源盘（SATA 盘）和目标盘（NVMe 盘）都用 USB 方式连接好。</p>



<p class="wp-block-paragraph">第一步是先用 DiskGenius 磁盘克隆，此后源盘抛开，不再参与后续步骤。成功克隆目标盘后的第二步是用 DiskGenius 的分区表类型转换功能把其 MBR 分区表格式转换为 GPT 分区表格式。</p>



<p class="wp-block-paragraph">第三步有少许精微，其目的是要把 GPT 引导所需的 ESP 分区和 Windows 系统所需的 MSR 分区设定好。一个常见的 GPT 分区布局是，ESP 分区位于磁盘首，而 MSR 稍显随意，相邻地位于系统所在分区之前或者之后（暂不考虑 Windows 的恢复分区）。现实情况是在 MBR 布局下已经存在于系统所在分区之后，因此仅需将其分区类型在 DiskGenius 的编辑分区参数界面中重新调整为 MSR 类型即可。ESP 分区需要单独处理，主要原因是当前磁盘上没有空余空间可以使用，因此需要先用 DiskGenius 调整系统所在分区的大小，在其之前缩出 500MB 即可。此时即可通过 DiskGenius 的“建立ESP/MSR分区”菜单完成 ESP 分区的创建。</p>



<p class="wp-block-paragraph">接下来还有两步路走。</p>



<p class="wp-block-paragraph">首先是把刚刚创建好的 ESP 分区上置入引导文件。正好当前运行的主机也是 Windows 10，因此可以在管理员权限的命令行下执行 
			<span id="crayon-6ade82fc17a3c241619833" class="crayon-syntax crayon-syntax-inline  crayon-theme-visual-assist crayon-theme-visual-assist-inline crayon-font-droid-sans-mono" style="font-size: 13px !important; line-height: 16px !important;font-size: 13px !important;"><span class="crayon-pre crayon-code" style="font-size: 13px !important; line-height: 16px !important;font-size: 13px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;"><span class="crayon-i">bcdboot</span><span class="crayon-h"> </span><span class="crayon-v">C</span><span class="crayon-o">:</span><span class="crayon-sy">\</span><span class="crayon-v">Windows</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-i">s</span><span class="crayon-h"> </span><span class="crayon-v">S</span><span class="crayon-o">:</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-i">f</span><span class="crayon-h"> </span><span class="crayon-v">UEFI</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-i">l</span><span class="crayon-h"> </span><span class="crayon-v">zh</span><span class="crayon-o">-</span><span class="crayon-v">cn</span></span></span> 来完成。其中的 
			<span id="crayon-6ade82fc17a43113816218" class="crayon-syntax crayon-syntax-inline  crayon-theme-visual-assist crayon-theme-visual-assist-inline crayon-font-droid-sans-mono" style="font-size: 13px !important; line-height: 16px !important;font-size: 13px !important;"><span class="crayon-pre crayon-code" style="font-size: 13px !important; line-height: 16px !important;font-size: 13px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;"><span class="crayon-v">S</span><span class="crayon-o">:</span></span></span> 是给 ESP 分区分配的盘符，这个工作也可以在 DiskGenius 里做。</p>



<p class="wp-block-paragraph">第二步则是确认上一步创建出来的 BCD 文件是否可用，最佳工具首推 Bootice。使用它打开 <code>S:\EFI\Microsoft\Boot\BCD</code> 文件，在高级编辑模式下，把左侧栏中的对象树里的必要节点仔细检查一遍（主要是 <code>Windows Boot Manager</code> 节点以及 <code>Application Objects</code> 和 <code>Windows Resume Objects</code> 分支下的节点，把带有 <code>Device</code> 字样的 key（一般是 <code>ApplicationDevice</code> 或者 <code>OSDevice</code>）都要仔细审视一番，把对应的磁盘和分区（要特别注意：<strong>引导分区和系统分区是不一样的！</strong>）选择正确后保存。理论上，这块磁盘应该准备就绪了。</p>



<p class="wp-block-paragraph">在实践中，上述操作一次性成功。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/03/12/bios-mbr-%e5%bc%95%e5%af%bc%e7%9a%84-windows-10-%e5%b0%b1%e5%9c%b0%e8%bd%ac%e6%8d%a2%e4%b8%ba-uefi-gpt-%e5%bc%95%e5%af%bc%e5%ae%9e%e6%93%8d/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>路由器上跑 Debian？</title>
		<link>https://www.somedoc.net/2026/02/15/%e8%b7%af%e7%94%b1%e5%99%a8%e4%b8%8a%e8%b7%91-debian%ef%bc%9f/</link>
					<comments>https://www.somedoc.net/2026/02/15/%e8%b7%af%e7%94%b1%e5%99%a8%e4%b8%8a%e8%b7%91-debian%ef%bc%9f/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 15 Feb 2026 04:04:09 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[硬件]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6654</guid>

					<description><![CDATA[开年以来，眼看着身边的杂物越来越多，左抬脚右迈步，一碰一个纸 <a href="https://www.somedoc.net/2026/02/15/%e8%b7%af%e7%94%b1%e5%99%a8%e4%b8%8a%e8%b7%91-debian%ef%bc%9f/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">开年以来，眼看着身边的杂物越来越多，左抬脚右迈步，一碰一个纸箱子，遂决定今年以“出”为基本指导方针，“入”则需要慎之又慎。</p>



<p class="wp-block-paragraph">收拾，隔三岔五上架到小黄鱼上。一下子就从灰堆里扒拉出来几个路由器。内存 512MB，即使放在今天，也不能算小。之前是在 TF 卡上烧的 OpenWrt，昨晚又想到，有没可能给它上面跑个 Debian？</p>



<p class="wp-block-paragraph">搜了一下，竟然搜到一个博主，其博客地址是 <a href="https://blog.hackpascal.net/">HackPascal @ Home</a>，在数年前写过一个系列文章，就是在讲这个目标。大体看了一下，还挺复杂的，真要搞需要投入相当多的时间和精力，暂备于此。</p>



<ul class="wp-block-list">
<li><a href="https://blog.hackpascal.net/2019/09/debian-for-mt7621-%e6%8a%98%e8%85%be%e8%ae%b0-1-%e5%ae%9e%e7%8e%b0%e5%9f%ba%e7%a1%80%e7%b3%bb%e7%bb%9f%e6%ad%a3%e5%b8%b8%e5%90%af%e5%8a%a8/">Debian for MT7621 折腾记 (1) – 实现基础系统正常启动 | HackPascal @ Home</a></li>



<li><a href="https://blog.hackpascal.net/2019/10/debian-for-mt7621-%e6%8a%98%e8%85%be%e8%ae%b0-2-%e5%ae%9e%e7%8e%b0-mt7530-dsa/">Debian for MT7621 折腾记 (2) – 实现 MT7530 DSA | HackPascal @ Home</a></li>



<li><a href="https://blog.hackpascal.net/2019/10/debian-for-mt7621-%e6%8a%98%e8%85%be%e8%ae%b0-3-%e7%bc%96%e8%af%91-debian-kernel-package/">Debian for MT7621 折腾记 (3) – 编译 Debian Kernel Package | HackPascal @ Home</a></li>
</ul>



<p class="wp-block-paragraph">顺便提一下，在查找阅读过程中发现，该博主似乎是所谓的不死引导器 Breed 的作者。未予追证，后续再说。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/02/15/%e8%b7%af%e7%94%b1%e5%99%a8%e4%b8%8a%e8%b7%91-debian%ef%bc%9f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>老树孕苞待春回</title>
		<link>https://www.somedoc.net/2026/02/09/%e8%80%81%e6%a0%91%e5%ad%95%e8%8b%9e%e5%be%85%e6%98%a5%e5%9b%9e/</link>
					<comments>https://www.somedoc.net/2026/02/09/%e8%80%81%e6%a0%91%e5%ad%95%e8%8b%9e%e5%be%85%e6%98%a5%e5%9b%9e/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 08 Feb 2026 16:54:03 +0000</pubDate>
				<category><![CDATA[Windows]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[软件]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6651</guid>

					<description><![CDATA[最近 AI 的世界里，agent 作为明星是非常耀眼的存在。 <a href="https://www.somedoc.net/2026/02/09/%e8%80%81%e6%a0%91%e5%ad%95%e8%8b%9e%e5%be%85%e6%98%a5%e5%9b%9e/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">最近 AI 的世界里，agent 作为明星是非常耀眼的存在。微软公司不甘寂寞，也推出了自己的 agent 相关的贡献。作为一名上了岁数的 Windows 程序员，当再次看到 Microsoft Agent 这个词组时，却不由自主地想到了多年前的另一个同名技术。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Microsoft Agent 是 Microsoft 开发的一项技术，允许用户与应用程序和网页内的动画角色进行交互。它使用文本到语音（TTS）引擎来允许动画角色与最终用户对话。这些动画角色被称为“Microsoft Agent”。从 Windows 98 开始，它就预装在 Windows 中。Microsoft Agent 持续与所有 Windows 版本一起发布，直到 2009 年的 Windows 7，此后 Microsoft 停止提供 Microsoft Agent。Microsoft Agent 的最新版本是 1998 年 10 月 12 日发布的 2.0 版。</p>
</blockquote>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Microsoft Agent 允许软件用户通过 Microsoft Agent 角色编辑器创建角色。微软在原始页面上制作了四个默认角色，包括精灵、梅林、皮迪和罗比。其他一些值得注意的角色是 1997 年至 2003 年 Microsoft Office 套件中出现的角色，包括 Clippy、Dot 或 Office Logo。Windows XP 搜索助手也是 Microsoft Agent，包括 Rover。另一个值得一提的 Microsoft Agent 是 Bonzi，一只紫色大猩猩，它与 BonziBUDDY 捆绑在一起，直到其停产。Bonzi 角色后来因其多个 meme 视频而成为 meme，随后受到了广泛关注，最后又因一名主播直播了 XP 损毁而导致 Bonzi 角色的受欢迎程度上升。</p>
</blockquote>



<p class="wp-block-paragraph">上述引言来自于一个围绕 Microsoft Agent 技术而创建的<a href="https://tmafe.com/" data-type="link" data-id="https://tmafe.com/">社区</a>页面，比较全面地介绍了 Microsoft Agent 是什么这一命题。</p>



<p class="wp-block-paragraph">对于国人来讲，最知名的一个 Microsoft Agent 其实应该是瑞星杀毒软件中的小狮子，尽管在后期，由于各种原因，把小狮子的展示技术迁移到了 Adobe Flash。由于小狮子的形象备受人民群众的喜爱，甚至有爱好者自行制作了不包含瑞星杀毒软件本体的独立的小狮子安装包，如果你想体验的话，可以到这里下载：<a href="https://m.crsky.com/mip/soft/94024.html">瑞星小狮子独立版下载 &#8211; 非凡手机软件</a>。</p>



<p class="wp-block-paragraph">当对技术的追思到二这个份儿上的时候，老夫突然想把非 SWF 版本的原始小狮子找到再看看。这个工作稍微绕了点，因为首先就是要找一个更老版本的瑞星杀毒软件的安装包。当然功夫不负有心人，还是被俺找到一份：<a href="https://mydown.yesky.com/pcsoft/204802.html">瑞星杀毒软件 2004 官方 PC 版免费下载-天极下载</a>。下载解包后看到里面确实存在 RsAgent.acs，这就和记忆对上了。</p>



<p class="wp-block-paragraph">acs 文件是微软的角色构建工具（可以在<a href="https://agentpedia.tmafe.com/wiki/Microsoft%20Agent%20Character%20Editor" data-type="link" data-id="https://agentpedia.tmafe.com/wiki/Microsoft%20Agent%20Character%20Editor">这里</a>下载）生成的，而制作的原始素材会用到 bmp 位图文件以及 wav 音频文件。如果你想把现成的 acs 文件逆向恢复成素材集，最妥善的方法当然是去了解 acs 的文件格式，尽管微软官方并未公开，好在前人栽过这棵树的，可以参看这个<a href="http://www.lebeausoftware.org/download.aspx?ID=25001fc7-18e9-49a4-90dc-21e8ff46aa1d" data-type="link" data-id="http://www.lebeausoftware.org/download.aspx?ID=25001fc7-18e9-49a4-90dc-21e8ff46aa1d">链接</a>。文档作者也写好了一个现成的工具，在<a href="http://www.lebeausoftware.org/download.aspx?ID=83b03bd5-18dc-467d-9778-e941536897e4">这儿</a>下载。</p>



<p class="wp-block-paragraph">微软官方文档站里目前还有相关内存，可以在<a href="https://learn.microsoft.com/en-us/windows/win32/lwef/microsoft-agent">这儿</a>参阅；一个简单的入门介绍文档可以参阅<a href="https://github.com/Planet-Source-Code/mahangu-the-complete-guide-to-ms-agent__1-13181">这儿</a>。</p>



<p class="wp-block-paragraph">有了这些，俺又不免想到，怎么在这新时代里，旧物利用一下儿呢？</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/02/09/%e8%80%81%e6%a0%91%e5%ad%95%e8%8b%9e%e5%be%85%e6%98%a5%e5%9b%9e/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AV 到底有几个意思</title>
		<link>https://www.somedoc.net/2026/02/05/av-%e5%88%b0%e5%ba%95%e6%9c%89%e5%87%a0%e4%b8%aa%e6%84%8f%e6%80%9d/</link>
					<comments>https://www.somedoc.net/2026/02/05/av-%e5%88%b0%e5%ba%95%e6%9c%89%e5%87%a0%e4%b8%aa%e6%84%8f%e6%80%9d/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Thu, 05 Feb 2026 13:00:08 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[硬件]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6648</guid>

					<description><![CDATA[刚学会上网的时候，凭着不甚熟练的技能，也不免要去访问一些不宜 <a href="https://www.somedoc.net/2026/02/05/av-%e5%88%b0%e5%ba%95%e6%9c%89%e5%87%a0%e4%b8%aa%e6%84%8f%e6%80%9d/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">刚学会上网的时候，凭着不甚熟练的技能，也不免要去访问一些不宜大庭广众之下万首瞩目的网站，这些站点有个类别，就是 AV 站。AV，是 Adult Video 的缩写，所谓“成人视频”。</p>



<p class="wp-block-paragraph">后些年间，在一个还算比较知名的厂里写软件，繁中区管那类软件叫“防病毒软件”，简中区叫“反病毒软件”，更多的时候叫“杀毒软件”，英文就是 Anti-Virus Software，因此在那些年里，AV 会让三太爷第一时间想到反病毒。</p>



<p class="wp-block-paragraph">再后来，发现不少设备上有个接口，赫然就叫 AV，比如电视机、有线电视机顶盒，以及现在还有的电视盒子等等，原来它是 Audio-Video 输入/输出接口。</p>



<p class="wp-block-paragraph">这个接口见的不少，不过用的却不多，原因无他，实在是科技进步迅速，数字信号传输的协议和接口都已不知进化了多少版本，而还处于模拟信号世界的 AV 传输就基本都在默默吃灰的状态下了。模拟信号的音视频传输除了 AV 口之外，还有一些别的接口，例如 BNC（中文叫同轴电缆）接口，以及与之经常同时使用的 RCA 接口。</p>



<p class="wp-block-paragraph">AV 接口的插头跟更常使用的音频线（例如耳机）很容易混淆，因为它们的接口物理规格基本就一样，大体来说，AV 接口是直接把最高级的音频线接口拿来使用了。</p>



<p class="wp-block-paragraph">音频线接口的粗细有两种规格，3.5mm 和 2.5mm，这个规格指标不影响传输的数据定义。此外的变化就是接口分为几段了。拿音频线来说，一开始只有两段，对应的通路一条传输音频模拟信号，一条是地线；后来为了支持立体声，就成为三段，音频模拟信号分为左声道和右声道，再加地线；再后来又要满足耳机和麦克风一体化传输的需求，再扩充一段成为四段，新增的为语音输入的模拟信号。这种四段规格的音频线在物理上就等同于 AV 线了，只不过 AV 线上传输的是视频模拟信号、左右声道音频模拟信号，以及接地。</p>



<p class="wp-block-paragraph">几年前鼓捣某一种 Pi（大概就是现在手上还留着有那么一两个的 Orange Pi Zero），发现这类板子上好些个都有 AV 输出能力，只不过有的是只定义在 GPIO 阵脚上，有的则已经把 AV 插口一并落实到板子上了。依稀记得有一回为了测试 Raspberry Pi OS，还特意在闲鱼上购买了一个小尺寸的显示器。回来后把它和板子连起来，确实看到了那个简陋的桌面。至于物理接口，板子上的输出端是音频线接口制式的插口（即母口），显示器上的输入端是黄白红三分量莲花头接口。</p>



<p class="wp-block-paragraph">几天前拾掇杂物，发现了 AD 送我的一块十六七年前的 GPS 卫星导航仪，系统是 Windows CE 6.0。被他折腾得似乎导航软件已经进不去了，开机就是 Windows 桌面。心想它能干啥呢，咦，发现它有一个 AV 输入口，只不过不是 3.5mm 口径，而是 2.5mm。不过我疑惑的是，既然开机就是 Windows CE 系统一直在输出视频画面，那么 AV 口的输入何时切换？问了一下 AI，回答是这类导航仪的 AV 口通常用于连接倒车影像的信号输入，此信号优先级被定义得比较高，一旦有信号传入，或抢占屏幕画面。</p>



<p class="wp-block-paragraph">也就是说，如果当年这个型号为乐驾/ROGA A1800KIT 的导航仪在手的话，那个小显示器应该是不需要的。为了验证这个设想，不得不下单购置合适的连接线：连接输出端的是 3.5mm 四段音频线公头，而连接输入端的是 2.5mm 四段音频线公头。在淘宝上搜索，没有成品，但是有两端都是 3.5mm 四段音频线公头的成品线，以及 3.5mm 母转 2.5mm 公的四段音频线转接头，价格分别为 6.29 元包邮，以及 3.43 元包邮。加起来还不到 10 块钱，毫不手软地下单了。</p>



<p class="wp-block-paragraph">到手后先串接起来，再用它把导航仪和  Orange Pi Zero 连接起来，Pi 加电引导成功后导航仪上未检测到有 AV 信号，这是意识到，恐怕 Pi 里的系统（应该是 Debian 或者 OpenWRT）根本就没有驱动 AV 输出。那怎么检测这根组合线是否能正常工作呢？</p>



<p class="wp-block-paragraph">先是跑到客厅，用正在服役的联通 IPTV 盒子的 AV 输出口测试，仍然没有接收到输出的信号，把 HDMI 输出线拔掉也不行，把盒子待机后唤醒还不行。沮丧地回到书房，蓦然想到还有一个闲置的比较新的联通宽带赠送的盒子，于是把导航仪直接和它连接后开机。吼吼，导航仪屏幕上果然出现了联通盒子的输出画面。至此，上面开始的想法验证通过，这个导航仪确实可以当个显示器（准确地说是电视/监视器）使唤。</p>



<figure class="wp-block-image size-large"><a href="https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a.jpg" class="highslide-image" onclick="return hs.expand(this);"><img decoding="async" width="1024" height="768" src="https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-1024x768.jpg" alt="" class="wp-image-6649" srcset="https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-1024x768.jpg 1024w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-300x225.jpg 300w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-768x576.jpg 768w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-1536x1152.jpg 1536w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-175x131.jpg 175w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a-667x500.jpg 667w, https://www.somedoc.net/wp-content/uploads/2026/02/d1ad32250b83e023ee3cd1d35fc5920a.jpg 1706w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure>



<p class="wp-block-paragraph">上图中，右上方的导航仪屏幕上显示的正是下面黑色的联通盒子输出的画面，左上的小显示器并未投入使用，只是出镜以供参看。导航仪的内置电池已经完全耗尽，因此需要 USB 供电，以后可以考虑拆开换一块电池。</p>



<p class="wp-block-paragraph">最后的一个问题是，为什么客厅的盒子没能成功输出画面？猜想是跟盒子的状态有关，需要它在开机时就能检测到 AV 输出端口有线材接入了，从而激活输出信号。懒得去试了。</p>



<p class="wp-block-paragraph">尽管已经证实，导航仪在一定程度上能代替小显示器，不过考虑到小显示器可以支持的信号输入硬件接口多样，暂时打消了断舍离将其脱手的念头。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/02/05/av-%e5%88%b0%e5%ba%95%e6%9c%89%e5%87%a0%e4%b8%aa%e6%84%8f%e6%80%9d/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[转] 一篇 n 年前的固件安全帖</title>
		<link>https://www.somedoc.net/2026/01/07/%e8%bd%ac-%e4%b8%80%e7%af%87-n-%e5%b9%b4%e5%89%8d%e7%9a%84%e5%9b%ba%e4%bb%b6%e5%ae%89%e5%85%a8%e5%b8%96/</link>
					<comments>https://www.somedoc.net/2026/01/07/%e8%bd%ac-%e4%b8%80%e7%af%87-n-%e5%b9%b4%e5%89%8d%e7%9a%84%e5%9b%ba%e4%bb%b6%e5%ae%89%e5%85%a8%e5%b8%96/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Tue, 06 Jan 2026 16:46:33 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[电脑]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6614</guid>

					<description><![CDATA[地址在 Thunderstrike at 31C3 &#82 <a href="https://www.somedoc.net/2026/01/07/%e8%bd%ac-%e4%b8%80%e7%af%87-n-%e5%b9%b4%e5%89%8d%e7%9a%84%e5%9b%ba%e4%bb%b6%e5%ae%89%e5%85%a8%e5%b8%96/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">地址在 <a href="https://trmm.net/Thunderstrike_31c3/">Thunderstrike at 31C3 &#8211; Trammell Hudson&#8217;s Projects</a>。以下是 Copilot 帮助做的总结。</p>



<p class="wp-block-paragraph"><strong>简要总结：</strong> 这个页面是 Trammell Hudson 在 31C3 大会上关于 <em>Thunderstrike</em> 漏洞的演讲内容整理。它介绍了苹果 Mac EFI 固件的逆向工程过程、Thunderstrike 漏洞的原理，以及攻击方式和可能的防御措施。</p>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 内容要点</h3>



<ul class="wp-block-list">
<li><strong>背景与动机</strong>
<ul class="wp-block-list">
<li>Thunderstrike 是一种针对苹果 EFI 固件的攻击方法。</li>



<li>攻击者只需几秒钟的物理访问，就能将恶意代码写入 Mac 的启动 ROM。</li>



<li>这种固件级 rootkit 很难清除，甚至能抵抗系统重装。</li>
</ul>
</li>



<li><strong>逆向工程过程</strong>
<ul class="wp-block-list">
<li>演讲前半部分详细展示了如何逆向苹果的 EFI 启动 ROM，包括固件结构、校验机制和压缩方式。</li>



<li>发现苹果使用 <strong>CRC32 校验</strong>而非加密签名来验证 ROM 内容，这意味着攻击者可以轻松绕过。</li>
</ul>
</li>



<li><strong>Thunderstrike 漏洞原理</strong>
<ul class="wp-block-list">
<li>Thunderbolt 接口允许外部设备在启动时加载 Option ROM。</li>



<li>攻击者可通过修改 Option ROM，在固件更新流程中替换苹果的 RSA 公钥，从而让系统接受伪造的固件。</li>



<li>一旦攻击成功，ROM 中的公钥被替换，只有攻击者签名的更新才能被接受，形成持久控制。</li>
</ul>
</li>



<li><strong>演示与影响</strong>
<ul class="wp-block-list">
<li>演讲展示了概念验证攻击：利用 Thunderbolt 设备在恢复模式下注入恶意固件。</li>



<li>成功后，攻击者能安装不可移除的 rootkit，绕过密码、记录键盘输入等。</li>



<li>这是典型的“evil maid attack”（恶意女仆攻击），即在用户不在场时通过外部设备入侵。</li>
</ul>
</li>



<li><strong>防御与缓解措施</strong>
<ul class="wp-block-list">
<li>Hudson 提到的防御策略包括：
<ul class="wp-block-list">
<li>改进固件签名机制（使用真正的加密验证）。</li>



<li>限制 Option ROM 的加载。</li>



<li>加强物理安全（例如封堵接口）。</li>
</ul>
</li>



<li>他批评苹果当时的防御不足，强调需要硬件级的安全检查。</li>
</ul>
</li>
</ul>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 总结</h3>



<p class="wp-block-paragraph"><strong>Thunderstrike 漏洞揭示了苹果 Mac 在固件安全上的重大缺陷：攻击者可通过 Thunderbolt 接口在启动阶段写入恶意代码，获得持久控制。</strong> 这次演讲不仅展示了攻击过程，还呼吁业界加强固件层面的安全防护。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2026/01/07/%e8%bd%ac-%e4%b8%80%e7%af%87-n-%e5%b9%b4%e5%89%8d%e7%9a%84%e5%9b%ba%e4%bb%b6%e5%ae%89%e5%85%a8%e5%b8%96/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
