codexmate 0.0.24 → 0.0.25
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.md +28 -6
- package/README.zh.md +29 -7
- package/cli/builtin-proxy.js +35 -0
- package/cli/claude-proxy.js +24 -0
- package/cli/import-skills-url.js +23 -1
- package/cli/openai-bridge.js +51 -4
- package/cli/session-usage.js +8 -2
- package/cli.js +1543 -117
- package/lib/automation.js +404 -0
- package/lib/cli-path-utils.js +21 -5
- package/lib/cli-sessions.js +32 -1
- package/lib/download-artifacts.js +17 -2
- package/lib/mcp-stdio.js +13 -0
- package/package.json +2 -5
- package/web-ui/app.js +6 -3
- package/web-ui/logic.sessions.mjs +2 -2
- package/web-ui/modules/app.computed.dashboard.mjs +2 -0
- package/web-ui/modules/app.computed.session.mjs +17 -0
- package/web-ui/modules/app.methods.install.mjs +28 -0
- package/web-ui/modules/app.methods.session-actions.mjs +8 -1
- package/web-ui/modules/app.methods.session-browser.mjs +28 -4
- package/web-ui/modules/app.methods.session-trash.mjs +4 -2
- package/web-ui/modules/i18n.dict.mjs +24 -8
- package/web-ui/partials/index/layout-header.html +12 -2
- package/web-ui/partials/index/panel-sessions.html +4 -2
- package/web-ui/partials/index/panel-usage.html +7 -0
- package/web-ui/styles/controls-forms.css +49 -2
- package/web-ui/styles/layout-shell.css +1 -0
- package/web-ui/styles/responsive.css +0 -2
- package/web-ui/styles/sessions-list.css +2 -4
- package/web-ui/styles/sessions-toolbar-trash.css +4 -4
- package/web-ui/styles/sessions-usage.css +24 -0
- /package/{res → web-ui/res}/json5.min.js +0 -0
- /package/{res → web-ui/res}/logo-pack.webp +0 -0
- /package/{res → web-ui/res}/vue.global.prod.js +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="site/.vitepress/public/images/logo.png" alt="Codex Mate logo" width="180" />
|
|
4
4
|
|
|
5
5
|
# Codex Mate
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ Codex Mate is a local-first CLI + Web UI for unified management of:
|
|
|
29
29
|
- Claude Code `CLAUDE.md` editing (writes to `~/.claude/CLAUDE.md`)
|
|
30
30
|
- OpenClaw JSON5 profiles and workspace `AGENTS.md`
|
|
31
31
|
- Local skills market for Codex / Claude Code (target switching, local skills management, cross-app import, ZIP distribution)
|
|
32
|
-
- Local Codex/Claude sessions (list/filter/export/delete) with Usage analytics overview
|
|
32
|
+
- Local Codex/Claude/Gemini CLI/CodeBuddy Code sessions (list/filter/export/delete) with Usage analytics overview
|
|
33
33
|
- Plugins (Prompt templates): reusable templates with variables and one-click copy
|
|
34
34
|
- Task orchestration: plan/queue/run/review local tasks
|
|
35
35
|
|
|
@@ -57,12 +57,18 @@ It works on local files directly and does not require cloud hosting. The skills
|
|
|
57
57
|
- OpenClaw JSON5 profile management
|
|
58
58
|
|
|
59
59
|
**Session Management**
|
|
60
|
-
- Unified Codex + Claude session list
|
|
60
|
+
- Unified Codex + Claude + Gemini CLI + CodeBuddy Code session list
|
|
61
|
+
- Session locations (local-first, configurable):
|
|
62
|
+
- Codex: `~/.codex/sessions/*.jsonl` (or `$CODEX_HOME/sessions`, `$XDG_CONFIG_HOME/codex/sessions`)
|
|
63
|
+
- Claude: `~/.claude/projects/**/**/*.jsonl` (or `$CLAUDE_HOME/projects`, `$XDG_CONFIG_HOME/claude/projects`)
|
|
64
|
+
- Gemini: `~/.gemini/tmp/*/chats/*.json` (or `$GEMINI_HOME/tmp`, `$XDG_CONFIG_HOME/gemini/tmp`)
|
|
65
|
+
- CodeBuddy: `~/.codebuddy/projects/**/**/*.jsonl` (or `$CODEBUDDY_CODE_HOME_DIR/projects`)
|
|
61
66
|
- Local session pinning with persistent pinned state and pinned-first ordering
|
|
62
|
-
- Keyword/source/cwd filters
|
|
67
|
+
- Keyword/source/cwd/role/time filters, plus shareable filter links
|
|
68
|
+
- Copy resume command (Codex/Gemini/CodeBuddy): `codex resume <sessionId>` / `gemini -r <sessionId>` / `codebuddy -r <sessionId>`
|
|
63
69
|
- Fast search UX: short-lived query result caching to avoid rescanning on each keystroke
|
|
64
70
|
- Usage subview with 7d / 30d session trends, message trends, source share, and top paths
|
|
65
|
-
- Markdown export
|
|
71
|
+
- Markdown export (Web UI + `codexmate export-session`, supports `--session-id` or `--file`)
|
|
66
72
|
- Session-level and message-level delete (supports batch), with a local recycle bin for restore/purge
|
|
67
73
|
- Large-session preview optimization (fast tail preview path)
|
|
68
74
|
|
|
@@ -78,10 +84,20 @@ It works on local files directly and does not require cloud hosting. The skills
|
|
|
78
84
|
|
|
79
85
|
**Engineering Utilities**
|
|
80
86
|
- MCP stdio domains (`tools`, `resources`, `prompts`)
|
|
87
|
+
- Automation hooks (`/hooks/*`) + outbound webhook notifiers
|
|
81
88
|
- Built-in proxy controls (`proxy`)
|
|
82
89
|
- Auth profile management (`auth`)
|
|
83
90
|
- Zip/unzip utilities
|
|
84
91
|
|
|
92
|
+
## Automation (signal → action)
|
|
93
|
+
|
|
94
|
+
When running `codexmate run`, you can accept external webhooks and convert them into queued tasks:
|
|
95
|
+
|
|
96
|
+
- Webhook entry: `POST /hooks/<source>` (currently `github`, `gitlab`)
|
|
97
|
+
- Rule config: `~/.codex/codexmate-automation.json`
|
|
98
|
+
- Supported action: `task.queue.add` (optionally `startQueue: true`)
|
|
99
|
+
- Notifications: `notifiers[]` supports `type: "webhook"` for Slack/Feishu-style incoming webhooks
|
|
100
|
+
|
|
85
101
|
## Architecture
|
|
86
102
|
|
|
87
103
|
### At a glance (what it does → what you get)
|
|
@@ -166,6 +182,12 @@ npm install -g @mmmbuto/codex-cli-termux@latest
|
|
|
166
182
|
|
|
167
183
|
# Claude Code
|
|
168
184
|
npm install -g @anthropic-ai/claude-code
|
|
185
|
+
|
|
186
|
+
# Gemini CLI
|
|
187
|
+
npm install -g @google/gemini-cli
|
|
188
|
+
|
|
189
|
+
# CodeBuddy Code
|
|
190
|
+
npm install -g @tencent-ai/codebuddy-code
|
|
169
191
|
```
|
|
170
192
|
|
|
171
193
|
### Run from source
|
|
@@ -213,7 +235,7 @@ npm run reset 79
|
|
|
213
235
|
| `codexmate qwen [args...]` | Qwen CLI passthrough entrypoint |
|
|
214
236
|
| `codexmate run [--host <HOST>] [--no-browser]` | Start Web UI |
|
|
215
237
|
| `codexmate mcp serve [--read-only\|--allow-write]` | Start MCP stdio server |
|
|
216
|
-
| `codexmate export-session --source <codex\|claude> ...` | Export session to Markdown |
|
|
238
|
+
| `codexmate export-session --source <codex\|claude\|gemini\|codebuddy> ...` | Export session to Markdown |
|
|
217
239
|
| `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | Zip / unzip |
|
|
218
240
|
| `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | Extract files with target suffixes from ZIP files in a directory (default `.json`, recursive by default) |
|
|
219
241
|
|
package/README.zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="site/.vitepress/public/images/logo.png" alt="Codex Mate logo" width="180" />
|
|
4
4
|
|
|
5
5
|
# Codex Mate
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
|
|
|
29
29
|
- Claude Code `CLAUDE.md` 编辑(写入 `~/.claude/CLAUDE.md`)
|
|
30
30
|
- OpenClaw JSON5 配置与 Workspace `AGENTS.md`
|
|
31
31
|
- Codex / Claude Code Skills 市场(安装目标切换、本地 skills 管理、跨应用导入、ZIP 分发)
|
|
32
|
-
- Codex / Claude 本地会话浏览、筛选、导出、删除与 Usage 统计概览
|
|
32
|
+
- Codex / Claude / Gemini CLI / CodeBuddy Code 本地会话浏览、筛选、导出、删除与 Usage 统计概览
|
|
33
33
|
- 插件(提示词模板):模板复用、变量填写、一键复制
|
|
34
34
|
- 任务编排:规划 / 排队 / 执行 / 回看
|
|
35
35
|
|
|
@@ -58,12 +58,18 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
|
|
|
58
58
|
- OpenClaw JSON5 配置方案管理
|
|
59
59
|
|
|
60
60
|
**会话管理**
|
|
61
|
-
- 同页查看 Codex 与
|
|
61
|
+
- 同页查看 Codex、Claude、Gemini CLI 与 CodeBuddy Code 会话
|
|
62
|
+
- 会话来源与默认路径(本地优先,可通过环境变量覆盖):
|
|
63
|
+
- Codex:`~/.codex/sessions/*.jsonl`(或 `$CODEX_HOME/sessions`、`$XDG_CONFIG_HOME/codex/sessions`)
|
|
64
|
+
- Claude:`~/.claude/projects/**/**/*.jsonl`(或 `$CLAUDE_HOME/projects`、`$XDG_CONFIG_HOME/claude/projects`)
|
|
65
|
+
- Gemini:`~/.gemini/tmp/*/chats/*.json`(或 `$GEMINI_HOME/tmp`、`$XDG_CONFIG_HOME/gemini/tmp`)
|
|
66
|
+
- CodeBuddy:`~/.codebuddy/projects/**/**/*.jsonl`(或 `$CODEBUDDY_CODE_HOME_DIR/projects`)
|
|
62
67
|
- 支持本地会话置顶,置顶状态持久化保存并优先排序显示
|
|
63
|
-
- 关键词搜索、来源筛选、cwd
|
|
68
|
+
- 关键词搜索、来源筛选、cwd/角色/时间筛选,并支持复制筛选链接
|
|
69
|
+
- 复制恢复命令(Codex/Gemini/CodeBuddy):`codex resume <sessionId>` / `gemini -r <sessionId>` / `codebuddy -r <sessionId>`
|
|
64
70
|
- 搜索体验优化:短周期结果缓存,避免输入时重复扫描
|
|
65
71
|
- Usage 子页:近 7 天 / 近 30 天会话趋势、消息趋势、来源占比、高频路径
|
|
66
|
-
- 会话导出 Markdown
|
|
72
|
+
- 会话导出 Markdown(Web UI + `codexmate export-session`,支持 `--session-id` 或 `--file`)
|
|
67
73
|
- 会话与消息级删除(支持批量),并提供本地回收站用于恢复/彻底删除
|
|
68
74
|
- 大会话预览优化(快速 tail 预览路径)
|
|
69
75
|
|
|
@@ -83,8 +89,18 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
|
|
|
83
89
|
|
|
84
90
|
**工程能力**
|
|
85
91
|
- MCP stdio 能力(tools/resources/prompts)
|
|
92
|
+
- 自动化钩子(`/hooks/*`)+ 外发 webhook 通知
|
|
86
93
|
- Zip 压缩/解压(优先系统工具,失败回退 JS 库)
|
|
87
94
|
|
|
95
|
+
## 自动化(信号 → 行动)
|
|
96
|
+
|
|
97
|
+
运行 `codexmate run` 后,可接收外部 webhook 并转为任务队列:
|
|
98
|
+
|
|
99
|
+
- 入口:`POST /hooks/<source>`(当前支持 `github`、`gitlab`)
|
|
100
|
+
- 规则:`~/.codex/codexmate-automation.json`
|
|
101
|
+
- 动作:`task.queue.add`(可选 `startQueue: true`)
|
|
102
|
+
- 通知:`notifiers[]` 支持 `type: "webhook"`(适配 Slack/飞书等入站 webhook)
|
|
103
|
+
|
|
88
104
|
## 架构总览
|
|
89
105
|
|
|
90
106
|
### 一图看懂(从“做什么”到“产生什么效果”)
|
|
@@ -156,7 +172,7 @@ codexmate run
|
|
|
156
172
|
|
|
157
173
|
> 安全提示:默认监听会在当前局域网暴露未鉴权的管理界面。若包含 API Key、provider 配置或 skills 管理,请仅在可信网络中使用;如需仅本机访问,可设置 `CODEXMATE_HOST=127.0.0.1` 或启动时传入 `--host 127.0.0.1`。
|
|
158
174
|
|
|
159
|
-
### 安装 Codex CLI / Claude Code(可选)
|
|
175
|
+
### 安装 Codex CLI / Claude Code / Gemini CLI / CodeBuddy Code(可选)
|
|
160
176
|
|
|
161
177
|
Codex Mate 支持透传调用官方 CLI(例如 `codexmate codex ...`),建议先安装:
|
|
162
178
|
|
|
@@ -169,6 +185,12 @@ npm install -g @mmmbuto/codex-cli-termux@latest
|
|
|
169
185
|
|
|
170
186
|
# Claude Code
|
|
171
187
|
npm install -g @anthropic-ai/claude-code
|
|
188
|
+
|
|
189
|
+
# Gemini CLI
|
|
190
|
+
npm install -g @google/gemini-cli
|
|
191
|
+
|
|
192
|
+
# CodeBuddy Code
|
|
193
|
+
npm install -g @tencent-ai/codebuddy-code
|
|
172
194
|
```
|
|
173
195
|
|
|
174
196
|
### 从源码运行
|
|
@@ -215,7 +237,7 @@ npm run reset 79
|
|
|
215
237
|
| `codexmate qwen [args...]` | Qwen CLI 透传入口 |
|
|
216
238
|
| `codexmate run [--host <HOST>] [--no-browser]` | 启动 Web UI |
|
|
217
239
|
| `codexmate mcp serve [--read-only\|--allow-write]` | 启动 MCP stdio 服务 |
|
|
218
|
-
| `codexmate export-session --source <codex\|claude> ...` | 导出会话为 Markdown |
|
|
240
|
+
| `codexmate export-session --source <codex\|claude\|gemini\|codebuddy> ...` | 导出会话为 Markdown |
|
|
219
241
|
| `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | 压缩 / 解压 |
|
|
220
242
|
| `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | 批量提取目录下 ZIP 内指定后缀文件(默认 `.json`,默认递归) |
|
|
221
243
|
|
package/cli/builtin-proxy.js
CHANGED
|
@@ -674,6 +674,41 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
674
674
|
return;
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
678
|
+
const isLoopback = !remoteAddr
|
|
679
|
+
|| remoteAddr === '127.0.0.1'
|
|
680
|
+
|| remoteAddr === '::1'
|
|
681
|
+
|| remoteAddr === '::ffff:127.0.0.1';
|
|
682
|
+
if (!isLoopback) {
|
|
683
|
+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
|
|
684
|
+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
|
|
685
|
+
: '';
|
|
686
|
+
if (!expected) {
|
|
687
|
+
const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
|
|
688
|
+
res.writeHead(403, {
|
|
689
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
690
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
691
|
+
});
|
|
692
|
+
res.end(body, 'utf-8');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
696
|
+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
697
|
+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
698
|
+
const actual = match && match[1]
|
|
699
|
+
? match[1].trim()
|
|
700
|
+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
701
|
+
if (!actual || actual !== expected) {
|
|
702
|
+
const body = JSON.stringify({ error: 'Unauthorized' });
|
|
703
|
+
res.writeHead(401, {
|
|
704
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
705
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
706
|
+
});
|
|
707
|
+
res.end(body, 'utf-8');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
677
712
|
const incomingPath = parsedIncoming.pathname || '/';
|
|
678
713
|
if (incomingPath === '/health' || incomingPath === '/status') {
|
|
679
714
|
const body = JSON.stringify({
|
package/cli/claude-proxy.js
CHANGED
|
@@ -864,6 +864,30 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
|
|
|
864
864
|
function createBuiltinClaudeProxyServer(settings, upstream) {
|
|
865
865
|
const connections = new Set();
|
|
866
866
|
const server = http.createServer((req, res) => {
|
|
867
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
868
|
+
const isLoopback = !remoteAddr
|
|
869
|
+
|| remoteAddr === '127.0.0.1'
|
|
870
|
+
|| remoteAddr === '::1'
|
|
871
|
+
|| remoteAddr === '::ffff:127.0.0.1';
|
|
872
|
+
if (!isLoopback) {
|
|
873
|
+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
|
|
874
|
+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
|
|
875
|
+
: '';
|
|
876
|
+
if (!expected) {
|
|
877
|
+
writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
881
|
+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
882
|
+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
883
|
+
const actual = match && match[1]
|
|
884
|
+
? match[1].trim()
|
|
885
|
+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
886
|
+
if (!actual || actual !== expected) {
|
|
887
|
+
writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
867
891
|
handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
|
|
868
892
|
if (res.headersSent) {
|
|
869
893
|
try { res.destroy(err); } catch (_) {}
|
package/cli/import-skills-url.js
CHANGED
|
@@ -97,6 +97,17 @@ function extractHttpStatusFromError(err) {
|
|
|
97
97
|
return Number.isFinite(value) ? value : 0;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function isAllowedSkillsRedirectHost(originHost, nextHost) {
|
|
101
|
+
const origin = typeof originHost === 'string' ? originHost.trim().toLowerCase() : '';
|
|
102
|
+
const next = typeof nextHost === 'string' ? nextHost.trim().toLowerCase() : '';
|
|
103
|
+
if (!origin || !next) return false;
|
|
104
|
+
if (origin === next) return true;
|
|
105
|
+
if (process.env.CODEXMATE_ALLOW_SKILLS_REDIRECT === '1') return true;
|
|
106
|
+
if (origin === 'github.com' && next === 'codeload.github.com') return true;
|
|
107
|
+
if (origin === 'github.com' && next.endsWith('.githubusercontent.com')) return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
function downloadUrlToFile(targetUrl, filePath, options = {}) {
|
|
101
112
|
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
102
113
|
? Math.floor(options.maxBytes)
|
|
@@ -141,8 +152,19 @@ function downloadUrlToFile(targetUrl, filePath, options = {}) {
|
|
|
141
152
|
const nextUrl = redirectLocation.startsWith('http')
|
|
142
153
|
? redirectLocation
|
|
143
154
|
: `${parsed.origin}${redirectLocation}`;
|
|
155
|
+
let originHost = typeof options.originHost === 'string' && options.originHost.trim()
|
|
156
|
+
? options.originHost.trim()
|
|
157
|
+
: parsed.host;
|
|
158
|
+
try {
|
|
159
|
+
const nextParsed = new URL(nextUrl);
|
|
160
|
+
if (!isAllowedSkillsRedirectHost(originHost, nextParsed.host)) {
|
|
161
|
+
res.resume();
|
|
162
|
+
reject(new Error('Cross-origin redirect is not allowed'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} catch (_) {}
|
|
144
166
|
res.resume();
|
|
145
|
-
downloadUrlToFile(nextUrl, filePath, { maxBytes, timeoutMs, maxRedirects: maxRedirects - 1 })
|
|
167
|
+
downloadUrlToFile(nextUrl, filePath, { maxBytes, timeoutMs, maxRedirects: maxRedirects - 1, originHost })
|
|
146
168
|
.then(resolve)
|
|
147
169
|
.catch(reject);
|
|
148
170
|
return;
|
package/cli/openai-bridge.js
CHANGED
|
@@ -238,6 +238,10 @@ function normalizeResponsesInputToChatMessages(input) {
|
|
|
238
238
|
out.push({ type: 'text', text: block.text });
|
|
239
239
|
continue;
|
|
240
240
|
}
|
|
241
|
+
if ((type === 'reasoning' || type === 'reasoning_text' || type === 'reasoning_content') && typeof block.text === 'string') {
|
|
242
|
+
out.push({ type: 'text', text: block.text });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
241
245
|
if (type === 'input_image') {
|
|
242
246
|
const raw = block.image_url != null ? block.image_url : block.imageUrl;
|
|
243
247
|
const url = typeof raw === 'string'
|
|
@@ -255,7 +259,21 @@ function normalizeResponsesInputToChatMessages(input) {
|
|
|
255
259
|
}
|
|
256
260
|
if (type === 'image_url' && block.image_url) {
|
|
257
261
|
out.push({ type: 'image_url', image_url: block.image_url });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const text = typeof block.text === 'string'
|
|
265
|
+
? block.text
|
|
266
|
+
: (typeof block.content === 'string' ? block.content : '');
|
|
267
|
+
if (text) {
|
|
268
|
+
out.push({ type: 'text', text });
|
|
269
|
+
continue;
|
|
258
270
|
}
|
|
271
|
+
try {
|
|
272
|
+
const raw = JSON.stringify(block);
|
|
273
|
+
if (raw) {
|
|
274
|
+
out.push({ type: 'text', text: raw.slice(0, 4000) });
|
|
275
|
+
}
|
|
276
|
+
} catch (_) {}
|
|
259
277
|
}
|
|
260
278
|
if (out.length === 0) return '';
|
|
261
279
|
return out;
|
|
@@ -635,6 +653,9 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
635
653
|
const parsed = new URL(targetUrl);
|
|
636
654
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
637
655
|
const bodyText = options.body ? JSON.stringify(options.body) : '';
|
|
656
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
657
|
+
? Math.floor(options.maxBytes)
|
|
658
|
+
: 0;
|
|
638
659
|
const headers = {
|
|
639
660
|
'Accept': 'application/json',
|
|
640
661
|
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
@@ -664,7 +685,21 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
664
685
|
agent: parsed.protocol === 'https:' ? options.httpsAgent : options.httpAgent
|
|
665
686
|
}, (upstreamRes) => {
|
|
666
687
|
const chunks = [];
|
|
667
|
-
|
|
688
|
+
let size = 0;
|
|
689
|
+
upstreamRes.on('data', (chunk) => {
|
|
690
|
+
if (!chunk) return;
|
|
691
|
+
if (maxBytes > 0) {
|
|
692
|
+
size += chunk.length;
|
|
693
|
+
if (size > maxBytes) {
|
|
694
|
+
chunks.length = 0;
|
|
695
|
+
try { upstreamRes.destroy(new Error('response too large')); } catch (_) {}
|
|
696
|
+
try { req.destroy(new Error('response too large')); } catch (_) {}
|
|
697
|
+
finish({ ok: false, error: 'response too large' });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
chunks.push(chunk);
|
|
702
|
+
});
|
|
668
703
|
upstreamRes.on('end', () => {
|
|
669
704
|
const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
670
705
|
finish({
|
|
@@ -689,12 +724,16 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
689
724
|
|
|
690
725
|
function createOpenaiBridgeHttpHandler(options = {}) {
|
|
691
726
|
const settingsFile = options.settingsFile;
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
727
|
+
const expectedTokenRaw = typeof options.expectedToken === 'string' ? options.expectedToken.trim() : '';
|
|
728
|
+
const expectedToken = Object.prototype.hasOwnProperty.call(options, 'expectedToken')
|
|
729
|
+
? expectedTokenRaw
|
|
730
|
+
: (expectedTokenRaw || DEFAULT_BRIDGE_TOKEN);
|
|
695
731
|
const maxBodySize = Number.isFinite(options.maxBodySize) ? options.maxBodySize : 0;
|
|
696
732
|
const httpAgent = options.httpAgent;
|
|
697
733
|
const httpsAgent = options.httpsAgent;
|
|
734
|
+
const maxUpstreamBytes = Number.isFinite(options.maxUpstreamBytes) && options.maxUpstreamBytes > 0
|
|
735
|
+
? Math.floor(options.maxUpstreamBytes)
|
|
736
|
+
: Math.max(16 * 1024 * 1024, maxBodySize > 0 ? maxBodySize * 4 : 0);
|
|
698
737
|
|
|
699
738
|
if (!settingsFile) {
|
|
700
739
|
throw new Error('createOpenaiBridgeHttpHandler 缺少 settingsFile');
|
|
@@ -730,6 +769,11 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
730
769
|
// 为避免在 LAN 暴露无鉴权的代理,这里仅允许 loopback 连接缺省 token。
|
|
731
770
|
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
732
771
|
const isLoopback = isLoopbackAddress(remoteAddr);
|
|
772
|
+
if (!isLoopback && !expectedToken) {
|
|
773
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
774
|
+
res.end(JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' }));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
733
777
|
if (!token && !isLoopback) {
|
|
734
778
|
res.writeHead(401, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
735
779
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
@@ -774,6 +818,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
774
818
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
775
819
|
...upstreamHeaders
|
|
776
820
|
},
|
|
821
|
+
maxBytes: maxUpstreamBytes,
|
|
777
822
|
httpAgent,
|
|
778
823
|
httpsAgent
|
|
779
824
|
});
|
|
@@ -827,6 +872,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
827
872
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
828
873
|
...upstreamHeaders
|
|
829
874
|
},
|
|
875
|
+
maxBytes: maxUpstreamBytes,
|
|
830
876
|
httpAgent,
|
|
831
877
|
httpsAgent
|
|
832
878
|
});
|
|
@@ -887,6 +933,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
887
933
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
888
934
|
...upstreamHeaders
|
|
889
935
|
},
|
|
936
|
+
maxBytes: maxUpstreamBytes,
|
|
890
937
|
httpAgent,
|
|
891
938
|
httpsAgent
|
|
892
939
|
});
|
package/cli/session-usage.js
CHANGED
|
@@ -7,11 +7,13 @@ async function listSessionUsageCore(params = {}, deps = {}) {
|
|
|
7
7
|
listSessionBrowse,
|
|
8
8
|
parseCodexSessionSummary,
|
|
9
9
|
parseClaudeSessionSummary,
|
|
10
|
+
parseCodeBuddySessionSummary,
|
|
11
|
+
parseGeminiSessionSummary,
|
|
10
12
|
MAX_SESSION_USAGE_LIST_SIZE,
|
|
11
13
|
SESSION_BROWSE_SUMMARY_READ_BYTES
|
|
12
14
|
} = deps;
|
|
13
15
|
|
|
14
|
-
const source = params.source === 'codex' || params.source === 'claude'
|
|
16
|
+
const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
|
|
15
17
|
? params.source
|
|
16
18
|
: 'all';
|
|
17
19
|
const rawLimit = Number(params.limit);
|
|
@@ -81,7 +83,11 @@ async function listSessionUsageCore(params = {}, deps = {}) {
|
|
|
81
83
|
try {
|
|
82
84
|
summary = normalized.source === 'claude'
|
|
83
85
|
? parseClaudeSessionSummary(filePath, summaryOptions)
|
|
84
|
-
:
|
|
86
|
+
: (normalized.source === 'gemini'
|
|
87
|
+
? parseGeminiSessionSummary(filePath, summaryOptions)
|
|
88
|
+
: (normalized.source === 'codebuddy'
|
|
89
|
+
? parseCodeBuddySessionSummary(filePath, summaryOptions)
|
|
90
|
+
: parseCodexSessionSummary(filePath, summaryOptions)));
|
|
85
91
|
} catch (_) {
|
|
86
92
|
summary = null;
|
|
87
93
|
}
|