本文记录如何把 Codex CLI 的 Ctrl+G 外部编辑器改造成 Typora 图文编辑流程,并保留可回顾的历史图文 Markdown。
解决的痛点
Codex 桌面客户端和 Codex CLI 都不方便在发送任务前直观看到完整的图文组合效果:
- 桌面客户端可以上传图片和输入文字,但图片与文字是分开的,通常需要人为在描述里说明“第几张图对应哪段内容”,图文关系不够直观。
- Codex CLI 中图片会被转换成类似
[Image #1]、[Image #2]的引用,发送前很难确认图片是否插在了预期的位置,也不方便检查多张图与文字上下文是否组合正确。 - 多轮或历史图文编辑任务不方便回溯。客户端或 CLI 使用久了之后,会话容易变得又大又杂;为了保持清爽,有些会话需要及时归档,但归档后再回看某次图文任务的原始上下文会更困难。
这个方案把每次 Ctrl+G 编辑都落成一个普通 Markdown 文件:编辑时可以在 Typora 里直接确认图文排版,提交时自动生成 Codex 可识别的 .codex.md 副本,事后也可以从指定目录重新打开原始 .md 文件回顾完整图文上下文。
目标效果:
- 在 Codex CLI 中按
Ctrl+G,自动用 Typora 打开一个持久化 Markdown 文件。 - 可以在 Typora 中输入长文本、粘贴截图、整理图文上下文。
- 保存或关闭 Typora 后,Codex CLI 自动继续,不需要手动按任意键。
- 给 Codex 提交的是兼容其图片引用解析的
.codex.md副本。 - 自己回顾历史时,打开普通
.md文件即可在 Typora 中直接看到图片。
适用场景
适用于 Windows 上的 Codex CLI。本文方案基于 Codex CLI 会读取 VISUAL / EDITOR 环境变量来启动外部编辑器这一机制。
Codex 桌面客户端目前不走 CLI 的 VISUAL / EDITOR 机制,无法直接用同一套 Ctrl+G 外部编辑器方案替代客户端底部输入框。
桌面客户端可以通过侧边栏打开 Terminal 走 CLI 来触发 Ctrl + G
最终目录
先选择一个你自己的工作目录,用来存放脚本和历史图文 Markdown。目录可以放在任意稳定位置,例如:
1
C:\Users\<你的用户名>\Documents\CodexImagePrompts
下文统一用 <图文任务目录> 代表这个目录。实际配置时,把它替换成你自己的真实路径即可。
目录中主要有两个脚本:
1
2
<图文任务目录>\codex-typora-editor.cmd
<图文任务目录>\codex-typora-editor.ps1
使用过程中会生成两类 Markdown:
1
2
codex-prompt-20260821-153000-123.md
codex-prompt-20260821-153000-123.codex.md
含义:
.md:给人看的原始图文文件,Typora 可以直接显示图片。.codex.md:给 Codex 提交的转换副本,图片改成 Codex 可解析的引用格式。
环境变量配置
把当前用户级环境变量设置为:
1
2
VISUAL=<图文任务目录>\codex-typora-editor.cmd
EDITOR=<图文任务目录>\codex-typora-editor.cmd
PowerShell 中也建议在 profile 里覆盖一次,避免新开终端仍继承旧的 notepad:
1
2
3
4
# region Codex Ctrl+G Typora editor
$env:VISUAL = '<图文任务目录>\codex-typora-editor.cmd'
$env:EDITOR = '<图文任务目录>\codex-typora-editor.cmd'
# endregion Codex Ctrl+G Typora editor
常见 profile 路径:
1
2
C:\Users\1\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
C:\Users\1\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
验证:
1
2
3
$env:VISUAL
$env:EDITOR
codex doctor | Select-String -Pattern 'VISUAL|EDITOR'
应该看到:
1
<图文任务目录>\codex-typora-editor.cmd
CMD 入口脚本
文件:
1
<图文任务目录>\codex-typora-editor.cmd
内容:
1
2
3
4
5
6
@echo off
setlocal EnableExtensions
set "SCRIPT_DIR=%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%codex-typora-editor.ps1" "%~1"
exit /b %ERRORLEVEL%
这个 .cmd 是给 Codex CLI 调用的入口。它只负责转发参数给 PowerShell 脚本。
PowerShell 主脚本
文件:
1
<图文任务目录>\codex-typora-editor.ps1
内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
param(
[Parameter(Position = 0)]
[string]$CodexSource
)
$ErrorActionPreference = 'Stop'
$TargetDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CustomTyporaExe = ''
$TyporaExeCandidates = @(
$CustomTyporaExe,
"$env:LOCALAPPDATA\Programs\Typora\Typora.exe",
"$env:ProgramFiles\Typora\Typora.exe",
"${env:ProgramFiles(x86)}\Typora\Typora.exe"
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
function Open-Editor {
param([string]$Path)
$typoraExe = $TyporaExeCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if ($typoraExe) {
return Start-Process -FilePath $typoraExe -ArgumentList @($Path) -PassThru
}
$typoraCommand = Get-Command Typora.exe -ErrorAction SilentlyContinue
if ($typoraCommand) {
return Start-Process -FilePath $typoraCommand.Source -ArgumentList @($Path) -PassThru
} else {
return Start-Process -FilePath $Path -PassThru
}
}
function Test-TyporaWindowForFile {
param([string]$Path)
$leaf = [System.IO.Path]::GetFileName($Path)
$stem = [System.IO.Path]::GetFileNameWithoutExtension($Path)
$windows = Get-Process -Name Typora -ErrorAction SilentlyContinue |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.MainWindowTitle) }
foreach ($window in $windows) {
if ($window.MainWindowTitle -like "*$leaf*" -or $window.MainWindowTitle -like "*$stem*") {
return $true
}
}
return $false
}
function Convert-TyporaImagesForCodex {
param(
[string]$SourcePath,
[string]$OutputPath
)
$content = Get-Content -Raw -LiteralPath $SourcePath -Encoding UTF8
$images = New-Object System.Collections.Generic.List[string]
$state = @{ Index = 0 }
$pattern = '(!?)\[([^\]]*)\]\(([^)\r\n]+)\)'
$content = [regex]::Replace($content, $pattern, {
param($match)
$rawPath = $match.Groups[3].Value.Trim()
if ($rawPath -match '^https?://') {
return $match.Value
}
if ($rawPath.StartsWith('<') -and $rawPath.EndsWith('>')) {
$rawPath = $rawPath.Substring(1, $rawPath.Length - 2)
}
try {
$rawPath = [System.Uri]::UnescapeDataString($rawPath)
} catch {
# Keep the original path if it is not a valid escaped string.
}
$imagePath = $rawPath
if ($imagePath -match '^file:///') {
$imagePath = ([System.Uri]$imagePath).LocalPath
} elseif (-not [System.IO.Path]::IsPathRooted($imagePath)) {
$imagePath = Join-Path $TargetDir $imagePath
}
$extension = [System.IO.Path]::GetExtension($imagePath).ToLowerInvariant()
if (@('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp') -notcontains $extension) {
return $match.Value
}
$state.Index += 1
$index = $state.Index
$images.Add("- [Image #$index]: $imagePath") | Out-Null
return "[Image #$index]"
})
if ($images.Count -gt 0) {
$content = $content.TrimEnd() + "`r`n`r`nReferenced image files:`r`n" + ($images -join "`r`n") + "`r`n"
}
Set-Content -LiteralPath $OutputPath -Value $content -Encoding UTF8
}
if (-not (Test-Path -LiteralPath $TargetDir)) {
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
}
if ([string]::IsNullOrWhiteSpace($CodexSource)) {
$defaultFile = Join-Path $TargetDir 'codex-prompt.md'
if (-not (Test-Path -LiteralPath $defaultFile)) {
New-Item -ItemType File -Path $defaultFile | Out-Null
}
Open-Editor -Path $defaultFile | Out-Null
exit 0
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
$targetFile = Join-Path $TargetDir "codex-prompt-$stamp.md"
Copy-Item -LiteralPath $CodexSource -Destination $targetFile -Force
$initialWrite = (Get-Item -LiteralPath $targetFile).LastWriteTimeUtc
$editorProcess = Open-Editor -Path $targetFile
$deadline = (Get-Date).AddMinutes(30)
$windowCheckStartsAt = (Get-Date).AddSeconds(2)
$sawTargetWindow = $false
$savedOrClosed = $false
do {
Start-Sleep -Milliseconds 500
$currentWrite = (Get-Item -LiteralPath $targetFile).LastWriteTimeUtc
if ($currentWrite -gt $initialWrite) {
$savedOrClosed = $true
break
}
if ((Get-Date) -ge $windowCheckStartsAt) {
$targetWindowOpen = Test-TyporaWindowForFile -Path $targetFile
if ($targetWindowOpen) {
$sawTargetWindow = $true
} elseif ($sawTargetWindow) {
$savedOrClosed = $true
break
} elseif ($editorProcess -and $editorProcess.HasExited -and -not (Get-Process -Name Typora -ErrorAction SilentlyContinue)) {
$savedOrClosed = $true
break
}
}
} while (-not $savedOrClosed -and (Get-Date) -lt $deadline)
Start-Sleep -Milliseconds 800
$codexSubmitFile = [System.IO.Path]::ChangeExtension($targetFile, '.codex.md')
Convert-TyporaImagesForCodex -SourcePath $targetFile -OutputPath $codexSubmitFile
Copy-Item -LiteralPath $codexSubmitFile -Destination $CodexSource -Force
使用流程
- 新开 PowerShell 或 Windows Terminal。
- 进入项目目录。
- 运行
codex。 - 在 Codex CLI 输入框按
Ctrl+G。 - Typora 自动打开一个新 Markdown。
- 在 Typora 中输入长文本、粘贴图片、整理上下文。
- 保存 Typora 文件,Codex CLI 自动继续。
如果直接关闭 Typora:
- 脚本会检测目标窗口关闭。
- 不再卡死在
Save and close external editor to continue.。 - 会提交最后一次已保存的内容;如果没保存,则提交初始内容。
图片转换规则
Typora 中可直接粘贴图片,常见形式:
1

或:
1
[image-xxx](C:\Users\1\AppData\Roaming\Typora\typora-user-images\xxx.png)
提交给 Codex 前会转换为:
1
2
3
4
[Image #1]
Referenced image files:
- [Image #1]: C:\Users\1\AppData\Roaming\Typora\typora-user-images\xxx.png
多张图按文档从上到下编号:
1
2
3
4
5
6
7
8
9
10
整体桌面[Image #1]
软件[Image #2]
系统固定[Image #3]
Referenced image files:
- [Image #1]: 第一张图片路径
- [Image #2]: 第二张图片路径
- [Image #3]: 第三张图片路径
历史图文回顾
回顾时打开普通 .md 文件,不要打开 .codex.md:
1
codex-prompt-20260821-153000-123.md
普通 .md 保留 Typora 原始图片语法,可以直接看到图文。
.codex.md 是提交副本,主要给 Codex 解析,不适合当作人类阅读版本:
1
codex-prompt-20260821-153000-123.codex.md
故障排查
Ctrl+G 仍然打开记事本
原因通常是当前终端进程还继承旧环境变量。
检查:
1
2
$env:VISUAL
$env:EDITOR
如果仍是 notepad,重新打开 PowerShell,再检查一次。
Typora 显示 Failed to load file
不要把目录路径直接传给 Typora。脚本现在会传入具体 .md 文件,例如:
1
<图文任务目录>\codex-prompt.md
Codex 报 ParserError
通常是 Windows 图片路径被放在普通 Markdown 链接里,例如:
1
[image](C:\Users\1\...\image.png)
本方案会在 .codex.md 中自动转换成本地图片引用清单,避免这个错误。
关闭 Typora 后 Codex 还在等
当前脚本已经加入窗口关闭检测和 30 分钟兜底超时。若某一次已经卡在旧脚本里,需要关闭那次 Codex CLI 或终端;下一次 Ctrl+G 会走新脚本。
PowerShell 执行策略拦截
.cmd 中使用:
1
powershell -NoProfile -ExecutionPolicy Bypass -File ...
一般不会被普通执行策略拦截。
当前方案边界
- 该方案主要服务 Codex CLI。
- Codex 桌面客户端没有直接读取
VISUAL/EDITOR的外部编辑器入口。 - Typora 图片默认保存在 Typora 用户图片目录;如需长期归档,可在 Typora 设置中把图片复制到当前文档同目录或固定资产目录。
.codex.md是自动提交副本,不建议手工编辑。
推荐习惯
- 长任务、带截图任务:优先用 CLI
Ctrl+G+ Typora。 - 短输入、查看任务状态:使用 Codex 桌面客户端。
- 回顾历史:打开
<图文任务目录>中普通.md文件。 - 不要删除 Typora 的图片目录,否则历史
.md中的图片可能失效。