@qqq123456789/codex-doctor 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -14,6 +14,8 @@ English | [中文](README.md)
14
14
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20WSL-lightgrey)](docs/06-sandbox-windows.md)
15
15
 
16
16
  > 📖 **Read online**:<https://qlw088697-ui.github.io/codex-troubleshooting/> (docsify site with full-text search, kept in sync with this repo)
17
+ >
18
+ > 📄 Prefer offline? Every release attaches a merged **PDF handbook** (`node scripts/build-pdf.mjs` regenerates it) — see [Releases](https://github.com/qlw088697-ui/codex-troubleshooting/releases).
17
19
 
18
20
  ## Quick start: run the environment self-check first
19
21
 
package/README.md CHANGED
@@ -45,6 +45,8 @@ curl -fsSL https://raw.githubusercontent.com/qlw088697-ui/codex-troubleshooting/
45
45
 
46
46
  (也可以下载 `scripts/` 目录里的脚本本地运行。脚本只读环境信息,不会修改任何配置。)
47
47
 
48
+ > 📄 喜欢离线阅读?每次发版都会在 [Releases](https://github.com/qlw088697-ui/codex-troubleshooting/releases) 附上合并排版的 PDF 手册(`node scripts/build-pdf.mjs` 可自行生成)。
49
+
48
50
  ## 🛠️ 配套工具:codex-doctor CLI
49
51
 
50
52
  把本手册的检查与维护做成了一条条命令(零依赖,Node 18.15+):
@@ -31,7 +31,10 @@ node codex-troubleshooting/tool/cli.mjs --help
31
31
  | `archive list` | List archive folders with sizes | read-only |
32
32
  | `archive delete <name\|--all>` | Delete archives (interactive confirm by default, `--yes` to skip) | medium (deletes, requires confirm) |
33
33
  | `versions [-n 10]` | List the latest openai/codex releases (prerelease flagged) | read-only |
34
+ | `config` | Read-only summary: model / provider / approval & sandbox / profiles / relays / MCP / auth mode (secrets masked) | read-only |
34
35
  | `sessions [-n 10] [--dir keyword]` | Browse past sessions: time, workdir, source, first-prompt preview; `--dir` filters by directory — find "that conversation" | read-only |
36
+ | `sessions --search keyword [--deep]` | Search sessions by keyword (first 256KB of each file by default, `--deep` scans fully) | read-only |
37
+ | `sessions --show [--search keyword] [--pick N] [--full]` | Print a session's full transcript (latest by default; messages truncated to 400 chars unless `--full`) | read-only |
35
38
  | `update` | Check the latest npm version and how to update | read-only |
36
39
 
37
40
  ## Design principles
@@ -31,7 +31,10 @@ node codex-troubleshooting/tool/cli.mjs --help
31
31
  | `archive list` | 查看归档目录与体积 | 只读 |
32
32
  | `archive delete <名称\|--all>` | 删除归档(默认需交互确认,`--yes` 跳过) | 中(删除,需确认) |
33
33
  | `versions [-n 10]` | 查看 openai/codex 最近 N 个版本(含预发布标记) | 只读 |
34
+ | `config` | 只读摘要:模型 / provider / 审批沙箱 / profiles / 中转 / MCP / 认证方式(敏感值脱敏) | 只读 |
34
35
  | `sessions [-n 10] [--dir 关键字]` | 浏览历史会话:时间、工作目录、来源、首条提问预览;`--dir` 按目录过滤,找回「上次那个对话」 | 只读 |
36
+ | `sessions --search 关键词 [--deep]` | 按关键词搜索会话(默认搜每个文件开头 256KB,`--deep` 全文扫描) | 只读 |
37
+ | `sessions --show [--search 关键词] [--pick N] [--full]` | 查看会话完整对话(默认最近一次;默认单条截断 400 字) | 只读 |
35
38
  | `update` | 查询 npm 上工具的最新版本与更新方式 | 只读 |
36
39
 
37
40
  ## 设计原则
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@qqq123456789/codex-doctor",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "test": "bash scripts/test-codex-doctor.sh",
6
+ "check:links": "bash scripts/check-links.sh",
7
+ "check:consistency": "bash scripts/check-consistency.sh"
8
+ },
4
9
  "description": "Codex CLI 维护与排障工具:环境自检、会话/日志归档、配置与凭据备份恢复、版本追踪(零依赖)",
5
10
  "keywords": [
6
11
  "codex",
package/tool/cli.mjs CHANGED
@@ -3,11 +3,12 @@
3
3
  import { collectChecks, summarize, renderHuman } from './checks.mjs';
4
4
  import { cleanTarget } from './clean.mjs';
5
5
  import { backupConfig, restoreBackup, resetAuth, listVersions, listArchives, deleteArchive, checkUpdate } from './ops.mjs';
6
- import { listSessions, searchSessions, readTranscript } from './sessions.mjs';
6
+ import { listSessions, searchSessions, readTranscript, exportTranscriptMarkdown } from './sessions.mjs';
7
+ import { configSummary } from './config.mjs';
7
8
  import { CODEX_DIR } from './util.mjs';
8
9
  import path from 'node:path';
9
10
 
10
- const VERSION = '0.8.0';
11
+ const VERSION = '1.0.0';
11
12
 
12
13
  const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零依赖)
13
14
 
@@ -27,6 +28,7 @@ const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零
27
28
  archive list 查看归档目录与体积
28
29
  archive delete <名称|--all> 删除归档(需 --yes 或交互确认)
29
30
  versions [-n N] 查看 openai/codex 最近 N 个版本(默认 10)
31
+ config 只读摘要:模型 / provider / 审批沙箱 / profiles / 中转 / MCP / 认证方式
30
32
  sessions [-n N] [--dir 关键字] 浏览历史会话:时间、目录、来源、首条提问预览
31
33
  [--search 关键词] [--deep] 按关键词搜索会话(--deep 全文扫描)
32
34
  --show [--search 关键词] [--pick N] [--full] 查看会话完整对话
@@ -151,6 +153,16 @@ async function main() {
151
153
  const item = items[pick - 1];
152
154
  console.log(`== 会话 ${item.date} @ ${item.dirName} ==`);
153
155
  console.log(`文件: ${item.file}\n`);
156
+ if (flags.out) {
157
+ // 导出为 Markdown 文件
158
+ const r = exportTranscriptMarkdown(item.file, flags.out, { maxLen: flags.full ? Infinity : 400 });
159
+ if (!r) {
160
+ console.log('(该会话没有可导出的对话消息)');
161
+ break;
162
+ }
163
+ console.log(`已导出 ${r.count} 条消息 → ${r.outFile}`);
164
+ break;
165
+ }
154
166
  const transcript = readTranscript(item.file, { maxLen: flags.full ? Infinity : 400 });
155
167
  if (!transcript || transcript.length === 0) {
156
168
  console.log('(该会话没有可展示的对话消息)');
@@ -199,6 +211,10 @@ async function main() {
199
211
  console.log(`\n会话文件位于 ${path.join(CODEX_DIR, 'sessions')}(-n 条数 / --dir 按目录关键字过滤)`);
200
212
  break;
201
213
  }
214
+ case 'config': {
215
+ for (const l of configSummary()) console.log(l);
216
+ break;
217
+ }
202
218
  case 'update': {
203
219
  const r = await checkUpdate(VERSION);
204
220
  print(r.lines);
@@ -0,0 +1,82 @@
1
+ // config:只读摘要——一条命令看懂 codex 当前有效配置(敏感信息脱敏)
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { CODEX_DIR, exists } from './util.mjs';
5
+
6
+ function host(u) {
7
+ try {
8
+ return new URL(u).host;
9
+ } catch {
10
+ return '(无效 URL)';
11
+ }
12
+ }
13
+
14
+ export function configSummary() {
15
+ const lines = [];
16
+ const cfg = path.join(CODEX_DIR, 'config.toml');
17
+ if (!exists(cfg)) {
18
+ lines.push('config.toml 不存在(全部使用默认配置)');
19
+ return lines;
20
+ }
21
+ const content = fs.readFileSync(cfg, 'utf8');
22
+
23
+ // 极简 TOML 分段解析:根级键 + 各表的 key=value(仅取本工具关心的键)
24
+ const root = {};
25
+ const tables = [];
26
+ let cur = null;
27
+ for (const raw of content.split(/\r?\n/)) {
28
+ const l = raw.trim();
29
+ if (!l || l.startsWith('#')) continue;
30
+ const tm = l.match(/^\[([^\]]+)\]$/);
31
+ if (tm) {
32
+ cur = tm[1].trim();
33
+ tables.push({ name: cur, kv: {} });
34
+ continue;
35
+ }
36
+ const kv = l.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
37
+ if (kv) {
38
+ const key = kv[1];
39
+ const val = kv[2].trim().replace(/^["']|["']$/g, '');
40
+ if (cur) tables[tables.length - 1].kv[key] = val;
41
+ else root[key] = val;
42
+ }
43
+ }
44
+
45
+ lines.push(`模型: ${root.model || '(默认)'}`);
46
+ lines.push(`provider: ${root.model_provider || 'openai(官方)'}`);
47
+ lines.push(`审批策略: ${root.approval_policy || '(默认)'}`);
48
+ lines.push(`沙箱模式: ${root.sandbox_mode || '(默认)'}`);
49
+
50
+ // 只显示「两段式」表名;更深的嵌套表(如 mcp_servers.x.env)是子配置,不是实体
51
+ const seg = (t) => t.name.split('.').length;
52
+ const profiles = tables.filter((t) => t.name.startsWith('profiles.') && seg(t) === 2);
53
+ if (profiles.length > 0) {
54
+ lines.push(`profiles: ${profiles.map((t) => t.name.replace('profiles.', '')).join(', ')}`);
55
+ }
56
+
57
+ const providers = tables.filter((t) => t.name.startsWith('model_providers.') && seg(t) === 2);
58
+ for (const p of providers) {
59
+ const id = p.name.replace('model_providers.', '');
60
+ const bu = p.kv.base_url ? host(p.kv.base_url) : '(未配置 base_url)';
61
+ const ek = p.kv.env_key ? `(Key 在环境变量 ${p.kv.env_key},不显示)` : '';
62
+ lines.push(`中转 ${id}: ${bu}${ek}`);
63
+ }
64
+
65
+ const mcp = tables.filter((t) => t.name.startsWith('mcp_servers.') && seg(t) === 2);
66
+ if (mcp.length > 0) {
67
+ lines.push(`MCP servers: ${mcp.map((t) => t.name.replace('mcp_servers.', '')).join(', ')}`);
68
+ }
69
+
70
+ // 认证方式(不看内容,只看结构)
71
+ const auth = path.join(CODEX_DIR, 'auth.json');
72
+ if (exists(auth)) {
73
+ try {
74
+ const j = JSON.parse(fs.readFileSync(auth, 'utf8'));
75
+ if (j.tokens) lines.push('认证: ChatGPT 账号登录');
76
+ else if (j.OPENAI_API_KEY) lines.push('认证: API Key(已脱敏)');
77
+ } catch {
78
+ lines.push('auth.json 无法解析——可运行 codex-doctor auth reset 重置');
79
+ }
80
+ }
81
+ return lines;
82
+ }
package/tool/sessions.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // sessions:浏览 ~/.codex/sessions 下的历史会话(只读)
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { CODEX_DIR, exists, walkFiles } from './util.mjs';
4
+ import { CODEX_DIR, exists, walkFiles, ensureDir } from './util.mjs';
5
5
 
6
6
  // 只读每个文件头部 64KB:meta 在第 1 行,真实提问通常也在最前面
7
7
  function readHead(file, bytes = 65536) {
@@ -154,3 +154,29 @@ export function readTranscript(file, { maxLen = 400 } = {}) {
154
154
  }
155
155
  return out;
156
156
  }
157
+
158
+ // 把会话导出为 Markdown 文件(找回的对话可存档分享)
159
+ export function exportTranscriptMarkdown(file, outFile, { maxLen = 400 } = {}) {
160
+ if (!exists(file)) return null;
161
+ const content = fs.readFileSync(file, 'utf8');
162
+ const { meta } = extract(content);
163
+ const transcript = readTranscript(file, { maxLen });
164
+ if (!transcript || transcript.length === 0) return null;
165
+
166
+ const lines = ['# Codex 会话记录', ''];
167
+ lines.push(`- 时间:${meta?.date || '?'}`);
168
+ lines.push(`- 工作目录:${meta?.cwd || '?'}`);
169
+ lines.push(`- 来源:${meta?.originator || '?'}`);
170
+ lines.push(`- 导出自:${file}`);
171
+ lines.push('');
172
+ for (const msg of transcript) {
173
+ lines.push(`## ${msg.role === 'user' ? '用户' : 'Codex'}`);
174
+ lines.push('');
175
+ lines.push(msg.text);
176
+ lines.push('');
177
+ }
178
+ const dest = path.resolve(outFile);
179
+ ensureDir(path.dirname(dest));
180
+ fs.writeFileSync(dest, lines.join('\n'), 'utf8');
181
+ return { outFile: dest, count: transcript.length };
182
+ }