@wu529778790/open-im 1.10.5 → 1.10.6-beta.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.md CHANGED
@@ -75,16 +75,21 @@ Default: root **`aiCommand`**. Override with **`platforms.<name>.aiCommand`**:
75
75
 
76
76
  ### Claude (Agent SDK)
77
77
 
78
- No local `claude` binary required. Credentials: env → **`config.json`** `env` → **`~/.claude/settings.json`**.
78
+ No local `claude` binary required. Credentials: env → **`config.json`** **`tools.claude.env`** → **`~/.claude/settings.json`** (dashboard saves API fields here).
79
79
 
80
80
  Third-party / compatible API example:
81
81
 
82
82
  ```json
83
83
  {
84
- "env": {
85
- "ANTHROPIC_AUTH_TOKEN": "your-token",
86
- "ANTHROPIC_BASE_URL": "https://your-api-endpoint",
87
- "ANTHROPIC_MODEL": "glm-4.7"
84
+ "tools": {
85
+ "claude": {
86
+ "workDir": "/path/to/project",
87
+ "env": {
88
+ "ANTHROPIC_AUTH_TOKEN": "your-token",
89
+ "ANTHROPIC_BASE_URL": "https://your-api-endpoint",
90
+ "ANTHROPIC_MODEL": "glm-4.7"
91
+ }
92
+ }
88
93
  }
89
94
  }
90
95
  ```
@@ -114,7 +119,7 @@ Add Feishu, QQ, WeCom, DingTalk, WorkBuddy under **`platforms`** as needed. Run
114
119
 
115
120
  ### Environment variables
116
121
 
117
- Use **`config.json`** or environment variables; the dashboard exposes common options. Typical keys: **`ANTHROPIC_*`**, **`TELEGRAM_BOT_TOKEN`**, **`OPEN_IM_WEB_PORT`**, **`OPEN_IM_WEB_HOST`**, plus platform-specific `*_APP_ID`, `*_SECRET`, `WORKBUDDY_*`, etc.
122
+ Use **`config.json`** (platforms, `tools.*`, etc.) or environment variables; the dashboard exposes common options. Typical keys: **`ANTHROPIC_*`** (shell or **`tools.claude.env`**), **`TELEGRAM_BOT_TOKEN`**, **`OPEN_IM_WEB_PORT`**, **`OPEN_IM_WEB_HOST`**, plus platform-specific `*_APP_ID`, `*_SECRET`, `WORKBUDDY_*`, etc. The root-level **`config.json` `env`** field is no longer read.
118
123
 
119
124
  ### Privacy
120
125
 
package/README.zh-CN.md CHANGED
@@ -75,16 +75,21 @@ npx @wu529778790/open-im start
75
75
 
76
76
  ### Claude(Agent SDK)
77
77
 
78
- 无需本地 `claude` 可执行文件。凭证顺序:环境变量 → **`config.json`** 的 **`env`** → **`~/.claude/settings.json`**。
78
+ 无需本地 `claude` 可执行文件。凭证顺序:环境变量 → **`config.json`** 的 **`tools.claude.env`** → **`~/.claude/settings.json`**(控制台保存的 API 配置写入后者)。
79
79
 
80
80
  第三方兼容接口示例:
81
81
 
82
82
  ```json
83
83
  {
84
- "env": {
85
- "ANTHROPIC_AUTH_TOKEN": "your-token",
86
- "ANTHROPIC_BASE_URL": "https://your-api-endpoint",
87
- "ANTHROPIC_MODEL": "glm-4.7"
84
+ "tools": {
85
+ "claude": {
86
+ "workDir": "/path/to/project",
87
+ "env": {
88
+ "ANTHROPIC_AUTH_TOKEN": "your-token",
89
+ "ANTHROPIC_BASE_URL": "https://your-api-endpoint",
90
+ "ANTHROPIC_MODEL": "glm-4.7"
91
+ }
92
+ }
88
93
  }
89
94
  }
90
95
  ```
@@ -114,7 +119,7 @@ codebuddy login
114
119
 
115
120
  ### 环境变量
116
121
 
117
- 可在 **`config.json`** 或环境变量中设置;控制台会展示常用项。常见:**`ANTHROPIC_*`**、**`TELEGRAM_BOT_TOKEN`**、**`OPEN_IM_WEB_PORT`**、**`OPEN_IM_WEB_HOST`**,以及各平台的 `*_APP_ID`、`*_SECRET`、`WORKBUDDY_*` 等。
122
+ 可在 **`config.json`**(平台与 `tools.*` 等)或环境变量中设置;控制台会展示常用项。常见:**`ANTHROPIC_*`**(shell 或 **`tools.claude.env`**)、**`TELEGRAM_BOT_TOKEN`**、**`OPEN_IM_WEB_PORT`**、**`OPEN_IM_WEB_HOST`**,以及各平台的 `*_APP_ID`、`*_SECRET`、`WORKBUDDY_*` 等。根级 **`config.json` `env`** 已不再使用。
118
123
 
119
124
  ### 隐私
120
125
 
@@ -37,7 +37,7 @@ export class CodexAdapter {
37
37
  onError: (err) => {
38
38
  const msg = typeof err === "string" ? err : String(err);
39
39
  const friendly = msg.includes("Authentication") || msg.includes("login")
40
- ? "Codex 需要先登录。请在终端运行 codex login,或在 ~/.open-im/config.json env 中添加 OPENAI_API_KEY。"
40
+ ? "Codex 需要先登录。请在终端运行 codex login,或在 shell export OPENAI_API_KEY。"
41
41
  : msg.includes("stream disconnected") ||
42
42
  msg.includes("error sending request") ||
43
43
  msg.includes("Connection refused") ||
@@ -2,6 +2,7 @@ import { execFileSync, spawn } from 'node:child_process';
2
2
  import { accessSync, constants } from 'node:fs';
3
3
  import { isAbsolute, join } from 'node:path';
4
4
  import { createLogger } from '../logger.js';
5
+ import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
5
6
  const log = createLogger('CodeBuddyCli');
6
7
  export function buildCodeBuddyArgs(prompt, sessionId, options) {
7
8
  const args = ['--print', '--output-format', 'stream-json'];
@@ -164,11 +165,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
164
165
  ...options,
165
166
  permissionMode: normalizePermissionMode(options?.permissionMode),
166
167
  });
167
- const env = {};
168
- for (const [key, value] of Object.entries(process.env)) {
169
- if (value !== undefined)
170
- env[key] = value;
171
- }
168
+ const env = processEnvForNonClaudeCliChild();
172
169
  if (process.platform === 'win32') {
173
170
  env.LANG = env.LANG || 'C.UTF-8';
174
171
  env.LC_ALL = env.LC_ALL || 'C.UTF-8';
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from 'node:fs';
6
6
  import { dirname, join } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { createLogger } from '../logger.js';
9
+ import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
9
10
  const log = createLogger('CodexCli');
10
11
  const windowsCodexLaunchCache = new Map();
11
12
  const SUPPORTED_IMAGE_EXTENSIONS = new Set([
@@ -161,11 +162,7 @@ function resolveWindowsCodexLaunch(cliPath, args) {
161
162
  }
162
163
  export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options) {
163
164
  const args = buildCodexArgs(prompt, sessionId, workDir, options);
164
- const env = {};
165
- for (const [k, v] of Object.entries(process.env)) {
166
- if (v !== undefined)
167
- env[k] = v;
168
- }
165
+ const env = processEnvForNonClaudeCliChild();
169
166
  if (options?.chatId)
170
167
  env.CC_IM_CHAT_ID = options.chatId;
171
168
  if (options?.hookPort)
@@ -3,6 +3,10 @@ export declare const CONFIG_PATH: string;
3
3
  export declare const CODEX_AUTH_PATHS: string[];
4
4
  /** Claude 认证相关的环境变量 key 列表 */
5
5
  export declare const CLAUDE_AUTH_ENV_KEYS: readonly ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_MODEL"];
6
+ /**
7
+ * 供非 Claude CLI 子进程使用的环境:拷贝当前 process.env,并移除 Anthropic/Claude 专用项。
8
+ */
9
+ export declare function processEnvForNonClaudeCliChild(): Record<string, string>;
6
10
  export declare function loadFileConfig(): FileConfig;
7
11
  export declare function saveFileConfig(raw: FileConfig): void;
8
12
  export declare function getClaudeConfigHome(): string;
@@ -13,7 +17,8 @@ export declare function hasCodexAuth(): boolean;
13
17
  export declare function parseCommaSeparated(value: string): string[];
14
18
  /**
15
19
  * 将最新的 Claude 认证环境变量按优先级合并到 process.env。
16
- * 优先级:shell 环境变量 > tools.claude.env(config.json) > ~/.claude/settings.json
17
- * 启动时调用一次;cc switch 后再次调用即可生效。
20
+ * 优先级:shell 环境变量 > ~/.open-im/config.json 的 tools.claude.env >
21
+ * 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)。
22
+ * 多数用户只维护本机 settings;每次创建 Claude SDK 会话前调用,本机文件变更后下次会话即生效。
18
23
  */
19
24
  export declare function refreshClaudeEnvToProcess(): void;
@@ -24,6 +24,29 @@ export const CLAUDE_AUTH_ENV_KEYS = [
24
24
  'ANTHROPIC_BASE_URL',
25
25
  'ANTHROPIC_MODEL',
26
26
  ];
27
+ /**
28
+ * 不应传入 Codex / CodeBuddy 等子进程的环境变量。
29
+ * Claude 适配器会通过 refreshClaudeEnvToProcess 把这些写入 process.env;
30
+ * 若原样拷贝给 CLI 子进程,可能导致错误使用 ANTHROPIC_BASE_URL 等「访问地址」。
31
+ */
32
+ const NON_CLAUDE_CLI_STRIP_KEYS = new Set([
33
+ ...CLAUDE_AUTH_ENV_KEYS,
34
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
35
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
36
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
37
+ ]);
38
+ /**
39
+ * 供非 Claude CLI 子进程使用的环境:拷贝当前 process.env,并移除 Anthropic/Claude 专用项。
40
+ */
41
+ export function processEnvForNonClaudeCliChild() {
42
+ const env = {};
43
+ for (const [k, v] of Object.entries(process.env)) {
44
+ if (v === undefined || NON_CLAUDE_CLI_STRIP_KEYS.has(k))
45
+ continue;
46
+ env[k] = v;
47
+ }
48
+ return env;
49
+ }
27
50
  // Config cache with mtime tracking
28
51
  let cachedConfig = null;
29
52
  let cachedClaudeEnv = null;
@@ -109,39 +132,61 @@ export function saveFileConfig(raw) {
109
132
  export function getClaudeConfigHome() {
110
133
  return process.env.HOME || process.env.USERPROFILE || homedir();
111
134
  }
135
+ function claudeSettingsJsonPath(home) {
136
+ return join(home, '.claude', 'settings.json');
137
+ }
138
+ /**
139
+ * 从单个 Claude settings JSON 根对象解析 env(与 Claude Code 行为对齐)。
140
+ * - `env` 对象内字段优先于根上同名认证键
141
+ * - 根上可存在 ANTHROPIC_* / CLAUDE_CODE_OAUTH_TOKEN(部分用户或旧版会写在顶层)
142
+ */
143
+ function extractAuthEnvFromClaudeSettingsRoot(raw) {
144
+ const out = {};
145
+ for (const key of CLAUDE_AUTH_ENV_KEYS) {
146
+ const v = raw[key];
147
+ if (v != null && typeof v === 'string' && v.length > 0) {
148
+ out[key] = v;
149
+ }
150
+ }
151
+ const env = raw.env;
152
+ if (env && typeof env === 'object' && !Array.isArray(env)) {
153
+ for (const [k, v] of Object.entries(env)) {
154
+ if (v != null && typeof k === 'string') {
155
+ out[k] = String(v);
156
+ }
157
+ }
158
+ }
159
+ return out;
160
+ }
112
161
  export function loadClaudeSettingsEnv() {
113
162
  const home = getClaudeConfigHome();
114
- const paths = [
115
- join(home, '.claude', 'settings.json'),
116
- join(home, '.claude.json'),
117
- ];
118
- for (const p of paths) {
119
- try {
120
- if (existsSync(p)) {
121
- const stats = statSync(p);
122
- const currentMtime = stats.mtimeMs;
123
- if (cachedClaudeEnv && cachedClaudeEnv.mtime === currentMtime && cachedClaudeEnv.env) {
124
- return cachedClaudeEnv.env;
125
- }
126
- const raw = JSON.parse(readFileSync(p, 'utf-8'));
127
- const env = raw?.env;
128
- if (env && typeof env === 'object') {
129
- const result = {};
130
- for (const [k, v] of Object.entries(env)) {
131
- if (v != null && typeof k === 'string') {
132
- result[k] = String(v);
133
- }
134
- }
135
- cachedClaudeEnv = { env: result, mtime: currentMtime };
136
- return result;
137
- }
138
- }
163
+ const p = claudeSettingsJsonPath(home);
164
+ let fingerprint = '';
165
+ try {
166
+ if (existsSync(p)) {
167
+ fingerprint = `${p}:${statSync(p).mtimeMs}`;
139
168
  }
140
- catch {
141
- /* file not found or parse error, try next path */
169
+ }
170
+ catch {
171
+ /* ignore */
172
+ }
173
+ if (cachedClaudeEnv && cachedClaudeEnv.fingerprint === fingerprint) {
174
+ return cachedClaudeEnv.env;
175
+ }
176
+ let merged = {};
177
+ try {
178
+ if (existsSync(p)) {
179
+ const raw = JSON.parse(readFileSync(p, 'utf-8'));
180
+ if (raw && typeof raw === 'object') {
181
+ merged = extractAuthEnvFromClaudeSettingsRoot(raw);
182
+ }
142
183
  }
143
184
  }
144
- return {};
185
+ catch {
186
+ /* 文件损坏或不可读 */
187
+ }
188
+ cachedClaudeEnv = { env: merged, fingerprint };
189
+ return merged;
145
190
  }
146
191
  export function saveClaudeSettingsEnv(env) {
147
192
  const home = getClaudeConfigHome();
@@ -191,8 +236,9 @@ export function parseCommaSeparated(value) {
191
236
  }
192
237
  /**
193
238
  * 将最新的 Claude 认证环境变量按优先级合并到 process.env。
194
- * 优先级:shell 环境变量 > tools.claude.env(config.json) > ~/.claude/settings.json
195
- * 启动时调用一次;cc switch 后再次调用即可生效。
239
+ * 优先级:shell 环境变量 > ~/.open-im/config.json 的 tools.claude.env >
240
+ * 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)。
241
+ * 多数用户只维护本机 settings;每次创建 Claude SDK 会话前调用,本机文件变更后下次会话即生效。
196
242
  */
197
243
  export function refreshClaudeEnvToProcess() {
198
244
  const file = loadFileConfig();
@@ -182,7 +182,6 @@ export interface FileConfig {
182
182
  dingtalk?: FilePlatformDingtalk;
183
183
  workbuddy?: FilePlatformWorkBuddy;
184
184
  };
185
- env?: Record<string, string>;
186
185
  aiCommand?: string;
187
186
  tools?: {
188
187
  claude?: FileToolClaude;
@@ -99,6 +99,8 @@ export declare const PAGE_TEXTS: {
99
99
  readonly codexCli: "Codex CLI path";
100
100
  readonly codebuddyCli: "CodeBuddy CLI path";
101
101
  readonly codexProxy: "Codex proxy";
102
+ readonly codexApiKey: "OPENAI_API_KEY";
103
+ readonly codexApiKeyTip: "Set the OpenAI API key used by Codex. You can also edit the auth file below.";
102
104
  readonly claudeConfigPath: "Config file location";
103
105
  readonly claudeAuthToken: "ANTHROPIC_AUTH_TOKEN";
104
106
  readonly claudeBaseUrl: "ANTHROPIC_BASE_URL";
@@ -129,8 +131,9 @@ export declare const PAGE_TEXTS: {
129
131
  readonly configEditorTitle: "Config Editor";
130
132
  readonly configEditorHint: "Edit ~/.open-im/config.json directly";
131
133
  readonly claudeSettingsLabel: "~/.claude/settings.json";
134
+ readonly codexSettingsLabel: "~/.codex/auth.json";
135
+ readonly codexSettingsCardHint: "Codex CLI auth (OPENAI_API_KEY etc.). Set API access here, or use the OPENAI_API_KEY field in the Codex tool tab.";
132
136
  readonly configJson: "~/.open-im/config.json";
133
- readonly configJsonHint: "Edit the configuration JSON. Changes will be saved when you click \"Save Config\" in the Service section.";
134
137
  readonly formatJson: "Format";
135
138
  readonly resetJson: "Reset";
136
139
  readonly saveBtn: "Save";
@@ -248,6 +251,8 @@ export declare const PAGE_TEXTS: {
248
251
  readonly codexCli: "Codex CLI 路径";
249
252
  readonly codebuddyCli: "CodeBuddy CLI 路径";
250
253
  readonly codexProxy: "Codex 代理";
254
+ readonly codexApiKey: "OPENAI_API_KEY";
255
+ readonly codexApiKeyTip: "设置 Codex 使用的 OpenAI API Key。也可以在下方编辑 auth 文件。";
251
256
  readonly claudeConfigPath: "配置文件位置";
252
257
  readonly claudeProxy: "代理(可选)";
253
258
  readonly hookPort: "Hook 端口";
@@ -275,6 +280,8 @@ export declare const PAGE_TEXTS: {
275
280
  readonly configEditorTitle: "JSON 配置编辑器";
276
281
  readonly configEditorHint: "直接编辑 ~/.open-im/config.json";
277
282
  readonly claudeSettingsLabel: "~/.claude/settings.json";
283
+ readonly codexSettingsLabel: "~/.codex/auth.json";
284
+ readonly codexSettingsCardHint: "Codex CLI 认证信息(OPENAI_API_KEY 等)。在此配置 API 访问,或在 Codex 工具标签页填写 OPENAI_API_KEY。";
278
285
  readonly configJson: "~/.open-im/config.json";
279
286
  readonly configJsonHint: "编辑配置 JSON。点击服务区的“保存配置”按钮后侘存更改。";
280
287
  readonly formatJson: "格式化";
@@ -99,6 +99,8 @@ export const PAGE_TEXTS = {
99
99
  codexCli: "Codex CLI path",
100
100
  codebuddyCli: "CodeBuddy CLI path",
101
101
  codexProxy: "Codex proxy",
102
+ codexApiKey: "OPENAI_API_KEY",
103
+ codexApiKeyTip: "Set the OpenAI API key used by Codex. You can also edit the auth file below.",
102
104
  claudeConfigPath: "Config file location",
103
105
  claudeAuthToken: "ANTHROPIC_AUTH_TOKEN",
104
106
  claudeBaseUrl: "ANTHROPIC_BASE_URL",
@@ -129,8 +131,9 @@ export const PAGE_TEXTS = {
129
131
  configEditorTitle: "Config Editor",
130
132
  configEditorHint: "Edit ~/.open-im/config.json directly",
131
133
  claudeSettingsLabel: "~/.claude/settings.json",
134
+ codexSettingsLabel: "~/.codex/auth.json",
135
+ codexSettingsCardHint: "Codex CLI auth (OPENAI_API_KEY etc.). Set API access here, or use the OPENAI_API_KEY field in the Codex tool tab.",
132
136
  configJson: "~/.open-im/config.json",
133
- configJsonHint: "Edit the configuration JSON. Changes will be saved when you click \"Save Config\" in the Service section.",
134
137
  formatJson: "Format",
135
138
  resetJson: "Reset",
136
139
  saveBtn: "Save",
@@ -248,6 +251,8 @@ export const PAGE_TEXTS = {
248
251
  codexCli: "Codex CLI \u8def\u5f84",
249
252
  codebuddyCli: "CodeBuddy CLI \u8def\u5f84",
250
253
  codexProxy: "Codex \u4ee3\u7406",
254
+ codexApiKey: "OPENAI_API_KEY",
255
+ codexApiKeyTip: "\u8bbe\u7f6e Codex \u4f7f\u7528\u7684 OpenAI API Key\u3002\u4e5f\u53ef\u4ee5\u5728\u4e0b\u65b9\u7f16\u8f91 auth \u6587\u4ef6\u3002",
251
256
  claudeConfigPath: "\u914d\u7f6e\u6587\u4ef6\u4f4d\u7f6e",
252
257
  claudeProxy: "\u4ee3\u7406\uff08\u53ef\u9009\uff09",
253
258
  hookPort: "Hook \u7aef\u53e3",
@@ -275,6 +280,8 @@ export const PAGE_TEXTS = {
275
280
  configEditorTitle: "JSON \u914d\u7f6e\u7f16\u8f91\u5668",
276
281
  configEditorHint: "\u76f4\u63a5\u7f16\u8f91 ~/.open-im/config.json",
277
282
  claudeSettingsLabel: "~/.claude/settings.json",
283
+ codexSettingsLabel: "~/.codex/auth.json",
284
+ codexSettingsCardHint: "Codex CLI \u8ba4\u8bc1\u4fe1\u606f\uff08OPENAI_API_KEY \u7b49\uff09\u3002\u5728\u6b64\u914d\u7f6e API \u8bbf\u95ee\uff0c\u6216\u5728 Codex \u5de5\u5177\u6807\u7b7e\u9875\u586b\u5199 OPENAI_API_KEY\u3002",
278
285
  configJson: "~/.open-im/config.json",
279
286
  configJsonHint: "\u7f16\u8f91\u914d\u7f6e JSON\u3002\u70b9\u51fb\u670d\u52a1\u533a\u7684\u201c\u4fdd\u5b58\u914d\u7f6e\u201d\u6309\u94ae\u540e\u4f98\u5b58\u66f4\u6539\u3002",
280
287
  formatJson: "\u683c\u5f0f\u5316",
@@ -6,7 +6,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { DWClient } from "dingtalk-stream";
8
8
  import { WEB_CONFIG_PORT, getPublicWebDashboardUrl } from "./constants.js";
9
- import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
9
+ import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig, CODEX_AUTH_PATHS } from "./config.js";
10
10
  import { getWebDistDir, tryServeDashboardStatic } from "./config-web-static.js";
11
11
  import { getServiceStatus, startBackgroundService, stopBackgroundService } from "./service-control.js";
12
12
  import { initWeWork, stopWeWork } from "./wework/client.js";
@@ -333,6 +333,22 @@ function buildInitialPayload(file) {
333
333
  codexCliPath: file.tools?.codex?.cliPath ?? "codex",
334
334
  codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
335
335
  codexProxy: file.tools?.codex?.proxy ?? "",
336
+ codexApiKey: (() => {
337
+ if (process.env.OPENAI_API_KEY)
338
+ return maskSecret(process.env.OPENAI_API_KEY);
339
+ for (const p of CODEX_AUTH_PATHS) {
340
+ try {
341
+ if (existsSync(p)) {
342
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
343
+ const key = raw?.openai_api_key ?? raw?.apiKey;
344
+ if (typeof key === "string" && key)
345
+ return maskSecret(key);
346
+ }
347
+ }
348
+ catch { /* ignore */ }
349
+ }
350
+ return "";
351
+ })(),
336
352
  logDir: file.logDir ?? "",
337
353
  logLevel: file.logLevel ?? "default",
338
354
  },
@@ -615,8 +631,9 @@ function toFileConfig(payload, existing) {
615
631
  saveClaudeSettingsEnv(claudeEnv);
616
632
  }
617
633
  // claudeConfigPath is informational only, not saved
634
+ const { env: _discardLegacyRootEnv, ...existingWithoutRootEnv } = existing;
618
635
  return {
619
- ...existing,
636
+ ...existingWithoutRootEnv,
620
637
  aiCommand: payload.ai.aiCommand,
621
638
  logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
622
639
  logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
@@ -883,6 +900,15 @@ export async function startWebConfigServer(options) {
883
900
  return;
884
901
  }
885
902
  saveFileConfig(toFileConfig(body, loadFileConfig()));
903
+ // Save Codex OPENAI_API_KEY to ~/.codex/auth.json if provided
904
+ const codexApiKey = clean(body.ai.codexApiKey);
905
+ if (codexApiKey && !isMasked(codexApiKey)) {
906
+ const codexAuthPath = CODEX_AUTH_PATHS[0];
907
+ const codexDir = dirname(codexAuthPath);
908
+ if (!existsSync(codexDir))
909
+ mkdirSync(codexDir, { recursive: true });
910
+ writeFileSync(codexAuthPath, JSON.stringify({ openai_api_key: codexApiKey }, null, 2), "utf-8");
911
+ }
886
912
  loadConfig();
887
913
  json(response, 200, { message: "Configuration saved." }, request);
888
914
  if (!options.persistent && requestUrl.searchParams.get("final") === "1") {
@@ -949,6 +975,56 @@ export async function startWebConfigServer(options) {
949
975
  }
950
976
  return;
951
977
  }
978
+ // --- Codex settings (auth.json) ---
979
+ if (request.method === "GET" && requestUrl.pathname === "/api/codex/settings") {
980
+ try {
981
+ let foundPath = "";
982
+ let contents = "{}";
983
+ for (const p of CODEX_AUTH_PATHS) {
984
+ if (existsSync(p)) {
985
+ foundPath = p;
986
+ contents = readFileSync(p, "utf-8");
987
+ break;
988
+ }
989
+ }
990
+ if (!foundPath && CODEX_AUTH_PATHS.length > 0) {
991
+ foundPath = CODEX_AUTH_PATHS[0];
992
+ }
993
+ json(response, 200, { path: foundPath, contents }, request);
994
+ }
995
+ catch (error) {
996
+ json(response, 500, { error: error instanceof Error ? error.message : String(error) }, request);
997
+ }
998
+ return;
999
+ }
1000
+ if (request.method === "POST" && requestUrl.pathname === "/api/codex/settings") {
1001
+ try {
1002
+ const body = await readJson(request);
1003
+ const raw = body.contents ?? "";
1004
+ if (!raw.trim()) {
1005
+ json(response, 400, { error: "contents is required" }, request);
1006
+ return;
1007
+ }
1008
+ try {
1009
+ JSON.parse(raw);
1010
+ }
1011
+ catch (err) {
1012
+ json(response, 400, { error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` }, request);
1013
+ return;
1014
+ }
1015
+ const settingsPath = CODEX_AUTH_PATHS[0];
1016
+ const dir = dirname(settingsPath);
1017
+ if (!existsSync(dir)) {
1018
+ mkdirSync(dir, { recursive: true });
1019
+ }
1020
+ writeFileSync(settingsPath, raw, "utf-8");
1021
+ json(response, 200, { message: "Codex settings saved.", path: settingsPath }, request);
1022
+ }
1023
+ catch (error) {
1024
+ json(response, 500, { error: error instanceof Error ? error.message : String(error) }, request);
1025
+ }
1026
+ return;
1027
+ }
952
1028
  if (request.method === "GET" && requestUrl.pathname === "/api/health") {
953
1029
  const file = loadFileConfig();
954
1030
  const platforms = getHealthPlatformSnapshot(file);
package/dist/config.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
2
2
  import type { Platform, AiCommand, Config } from './config/types.js';
3
- export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, } from './config/file-io.js';
3
+ export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
4
4
  /** 检测是否需要交互式配置(无 token 且无环境变量) */
5
5
  export declare function needsSetup(): boolean;
6
6
  export declare function loadConfig(): Config;
package/dist/config.js CHANGED
@@ -11,7 +11,7 @@ import { createLogger } from './logger.js';
11
11
  import { APP_HOME, DEFAULT_TELEMETRY_INGEST_URL, DEFAULT_TELEMETRY_INGEST_TOKEN, } from './constants.js';
12
12
  const log = createLogger('config');
13
13
  // Re-export file I/O and credential helpers from sub-modules
14
- export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, } from './config/file-io.js';
14
+ export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
15
15
  import { loadFileConfig, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, loadClaudeSettingsEnv, } from './config/file-io.js';
16
16
  /** 检测是否需要交互式配置(无 token 且无环境变量) */
17
17
  export function needsSetup() {
@@ -50,20 +50,8 @@ export function needsSetup() {
50
50
  }
51
51
  export function loadConfig() {
52
52
  const file = loadFileConfig();
53
- // 将配置文件中的 env 设置到环境变量(优先级低于现有环境变量)
54
- const mergeEnv = (env) => {
55
- for (const [key, value] of Object.entries(env)) {
56
- if (!(key in process.env) && value != null && typeof key === 'string') {
57
- process.env[key] = String(value);
58
- }
59
- }
60
- };
61
- // 1. 全局 env(最低优先级之一)
62
- if (file.env)
63
- mergeEnv(file.env);
64
- // 2. tools.claude.env 和 Claude Code 的 ~/.claude/settings.json 不在此处合并到 process.env,
65
- // 改由 Claude adapter 在创建 session 前按需加载(refreshClaudeEnvToProcess),
66
- // 避免 Claude 凭证泄漏到 Codex / CodeBuddy 等子进程。
53
+ // tools.claude.env 与 ~/.claude/settings.json 不在此处合并到 process.env
54
+ // Claude adapter 在创建 session 前 refreshClaudeEnvToProcess,避免污染 Codex/CodeBuddy 子进程。
67
55
  const fileTelegram = file.platforms?.telegram;
68
56
  const fileFeishu = file.platforms?.feishu;
69
57
  const fileQQ = file.platforms?.qq;
@@ -309,7 +297,7 @@ export function loadConfig() {
309
297
  }
310
298
  if (!hasCodexAuth()) {
311
299
  log.warn('Codex 模式:未检测到 OPENAI_API_KEY 或 Codex 登录态。首次使用请先运行 codex login,' +
312
- '或在 ~/.open-im/config.json env 中添加 "OPENAI_API_KEY": "你的 API Key"。');
300
+ '或在 shell export OPENAI_API_KEY。');
313
301
  }
314
302
  }
315
303
  // 8. 校验 CodeBuddy CLI(使用 codebuddy 时)
package/dist/setup.js CHANGED
@@ -709,23 +709,25 @@ export async function runInteractiveSetup() {
709
709
  // 如果选择 Claude,询问 API 配置
710
710
  let claudeApiConfig = {};
711
711
  if (commonResp.aiCommand === "claude") {
712
- // 检查是否已配置 API 密钥(环境变量、open-im config、或 ~/.claude/settings.json)
712
+ const legacyRootEnv = existing?.env;
713
+ const claudeFileEnv = existing?.tools?.claude?.env ?? legacyRootEnv;
714
+ // 检查是否已配置 API 密钥(环境变量、tools.claude.env、旧版根 env、或 ~/.claude/settings.json)
713
715
  const hasExistingApiKey = !!(process.env.ANTHROPIC_API_KEY ||
714
716
  process.env.ANTHROPIC_AUTH_TOKEN ||
715
717
  process.env.CLAUDE_CODE_OAUTH_TOKEN ||
716
- existing?.env?.ANTHROPIC_API_KEY ||
717
- existing?.env?.ANTHROPIC_AUTH_TOKEN ||
718
+ claudeFileEnv?.ANTHROPIC_API_KEY ||
719
+ claudeFileEnv?.ANTHROPIC_AUTH_TOKEN ||
718
720
  hasClaudeCredsInSettings());
719
721
  if (hasExistingApiKey) {
720
722
  // 已经配置过,直接保留原有配置,跳过询问
721
- if (existing?.env) {
723
+ if (claudeFileEnv && Object.keys(claudeFileEnv).length > 0) {
722
724
  claudeApiConfig = {
723
- apiKey: existing.env.ANTHROPIC_API_KEY || existing.env.ANTHROPIC_AUTH_TOKEN,
724
- baseUrl: existing.env.ANTHROPIC_BASE_URL,
725
- model: existing.env.ANTHROPIC_MODEL,
726
- haikuModel: existing.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
727
- sonnetModel: existing.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
728
- opusModel: existing.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
725
+ apiKey: claudeFileEnv.ANTHROPIC_API_KEY || claudeFileEnv.ANTHROPIC_AUTH_TOKEN,
726
+ baseUrl: claudeFileEnv.ANTHROPIC_BASE_URL,
727
+ model: claudeFileEnv.ANTHROPIC_MODEL,
728
+ haikuModel: claudeFileEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL,
729
+ sonnetModel: claudeFileEnv.ANTHROPIC_DEFAULT_SONNET_MODEL,
730
+ opusModel: claudeFileEnv.ANTHROPIC_DEFAULT_OPUS_MODEL,
729
731
  };
730
732
  }
731
733
  }
@@ -805,18 +807,7 @@ export async function runInteractiveSetup() {
805
807
  const base = existing
806
808
  ? JSON.parse(JSON.stringify(existing))
807
809
  : null;
808
- const { telegramBotToken: _, feishuAppId: __, feishuAppSecret: ___, claudeWorkDir: _cwd, claudeTimeoutMs: _ctm, claudeModel: _cm, ...baseRest } = (base ?? {});
809
- // Claude API 凭证不存入 config.json,仅从 ~/.claude/settings.json 或环境变量读取
810
- const ANTHROPIC_KEYS = [
811
- "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
812
- "ANTHROPIC_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL",
813
- ];
814
- const envConfig = {};
815
- for (const [k, v] of Object.entries(base?.env ?? {})) {
816
- if (v != null && typeof v === "string" && !ANTHROPIC_KEYS.includes(k)) {
817
- envConfig[k] = v;
818
- }
819
- }
810
+ const { telegramBotToken: _, feishuAppId: __, feishuAppSecret: ___, claudeWorkDir: _cwd, claudeTimeoutMs: _ctm, claudeModel: _cm, env: _stripLegacyRootEnv, ...baseRest } = (base ?? {});
820
811
  // 若用户在向导中输入了 Claude 配置,写入 ~/.claude/settings.json(与 Claude Code 共用)
821
812
  if (claudeApiConfig.apiKey || claudeApiConfig.baseUrl || claudeApiConfig.model) {
822
813
  const claudeExisting = loadClaudeSettings();
@@ -854,7 +845,6 @@ export async function runInteractiveSetup() {
854
845
  const out = {
855
846
  ...baseRest,
856
847
  platforms: { ...(base?.platforms ?? {}) },
857
- env: Object.keys(envConfig).length > 0 ? envConfig : undefined,
858
848
  aiCommand: aiCmd,
859
849
  tools: {
860
850
  claude: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.5",
3
+ "version": "1.10.6-beta.0",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",