diff --git a/lib/agent-runtime.js b/lib/agent-runtime.js index ee9551d..ca759b6 100644 --- a/lib/agent-runtime.js +++ b/lib/agent-runtime.js @@ -70,29 +70,29 @@ function createAgentRuntime(deps) { const runtimeConfig = prepareCodexCustomRuntime(codexConfig); if (runtimeConfig?.error) { return { error: runtimeConfig.error }; - } - const runtimeId = getRuntimeSessionId(session); - const args = ['exec']; - args.push('--json', '--skip-git-repo-check'); + } + const runtimeId = getRuntimeSessionId(session); + const args = ['exec']; + args.push('--json', '--skip-git-repo-check'); - const permMode = session.permissionMode || 'yolo'; - // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`. - // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors - // with: "unexpected argument '-s' found". - if (runtimeId && permMode === 'plan') { - args.push('-s', 'read-only'); - } - if (runtimeId) args.push('resume'); - switch (permMode) { - case 'yolo': - args.push('--dangerously-bypass-approvals-and-sandbox'); - break; - case 'plan': - if (!runtimeId) args.push('-s', 'read-only'); - break; - case 'default': - default: - args.push('--full-auto'); + const permMode = session.permissionMode || 'yolo'; + // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`. + // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors + // with: "unexpected argument '-s' found". + if (runtimeId && permMode === 'plan') { + args.push('-s', 'read-only'); + } + if (runtimeId) args.push('resume'); + switch (permMode) { + case 'yolo': + args.push('--dangerously-bypass-approvals-and-sandbox'); + break; + case 'plan': + if (!runtimeId) args.push('-s', 'read-only'); + break; + case 'default': + default: + args.push('--full-auto'); break; } diff --git a/public/app.js b/public/app.js index 3d29f91..1a62566 100644 --- a/public/app.js +++ b/public/app.js @@ -11,6 +11,7 @@ { cmd: '/mode', desc: '查看/切换权限模式' }, { cmd: '/cost', desc: '查看会话费用' }, { cmd: '/compact', desc: '压缩上下文' }, + { cmd: '/init', desc: '生成/更新 CLAUDE.md' }, { cmd: '/help', desc: '显示帮助' }, ]; @@ -3260,13 +3261,13 @@
-
系统
-
- - -
-
- `; +
系统
+
+ + +
+
+ `; overlay.appendChild(panel); document.body.appendChild(overlay); @@ -3281,9 +3282,9 @@ const codexStatus = panel.querySelector('#codex-status'); const codexSaveBtn = panel.querySelector('#codex-save-btn'); - const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); - const checkUpdateBtn = panel.querySelector('#check-update-btn'); - const updateStatusEl = panel.querySelector('#update-status'); + const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); + const checkUpdateBtn = panel.querySelector('#check-update-btn'); + const updateStatusEl = panel.querySelector('#update-status'); let currentCodexConfig = null; let codexEditingProfiles = []; @@ -3822,12 +3823,12 @@ renderModelCustomArea(); }; - // === Notify Config UI (moved to subpage) === - // notify config is handled by openNotifySubpage() + // === Notify Config UI (moved to subpage) === + // notify config is handled by openNotifySubpage() - const closeBtn = panel.querySelector('.settings-close'); - const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); - pwOpenModalBtn.addEventListener('click', openPasswordModal); + const closeBtn = panel.querySelector('.settings-close'); + const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); + pwOpenModalBtn.addEventListener('click', openPasswordModal); // Check update button const checkUpdateBtn = panel.querySelector('#check-update-btn'); diff --git a/public/claude.png b/public/claude.png new file mode 100644 index 0000000..ae85771 Binary files /dev/null and b/public/claude.png differ diff --git a/public/codex.png b/public/codex.png new file mode 100644 index 0000000..7f5f24d Binary files /dev/null and b/public/codex.png differ diff --git a/public/style.css b/public/style.css index 5aed2b0..ef4a478 100644 --- a/public/style.css +++ b/public/style.css @@ -1021,10 +1021,10 @@ body.session-loading-active { background: transparent; border: none; } -/* Codex avatar: GPT logo on green bg */ +/* Codex avatar: transparent bg to match the supplied asset */ .msg.assistant.agent-codex .msg-avatar { - background: #10a37f; - color: #fff; + background: transparent; + border: none; } .msg-bubble { diff --git a/server.js b/server.js index 6a0bb19..dde6dac 100644 --- a/server.js +++ b/server.js @@ -437,27 +437,27 @@ const activeTokens = new Set(); const AUTH_FAIL_WINDOW = 5 * 60 * 1000; // 5 minutes const AUTH_FAIL_MAX = 3; const authFailures = new Map(); // ip -> [timestamp, ...] - let bannedIPs = new Set(); +let bannedIPs = new Set(); - // Tailscale / loopback whitelist — never ban these IPs. - // Extra whitelist can be provided via env var (comma/space separated): - // CC_WEB_IP_WHITELIST="," - const EXTRA_WHITELIST_IPS = new Set( - String(process.env.CC_WEB_IP_WHITELIST || '') - .split(/[\s,]+/) - .map(s => s.trim()) - .filter(Boolean) - .map(s => s.replace(/^::ffff:/, '')) - ); +// Tailscale / loopback whitelist — never ban these IPs. +// Extra whitelist can be provided via env var (comma/space separated): +// CC_WEB_IP_WHITELIST="," +const EXTRA_WHITELIST_IPS = new Set( + String(process.env.CC_WEB_IP_WHITELIST || '') + .split(/[\s,]+/) + .map(s => s.trim()) + .filter(Boolean) + .map(s => s.replace(/^::ffff:/, '')) +); - function isWhitelistedIP(ip) { - if (!ip) return false; - const cleaned = ip.replace(/^::ffff:/, ''); - return cleaned === '127.0.0.1' - || cleaned === '::1' - || cleaned.startsWith('100.') - || EXTRA_WHITELIST_IPS.has(cleaned); - } +function isWhitelistedIP(ip) { + if (!ip) return false; + const cleaned = ip.replace(/^::ffff:/, ''); + return cleaned === '127.0.0.1' + || cleaned === '::1' + || cleaned.startsWith('100.') + || EXTRA_WHITELIST_IPS.has(cleaned); +} function loadBannedIPs() { try { @@ -2055,24 +2055,43 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) { break; } - case '/mode': { - const modeInput = parts[1]; - const VALID_MODES = ['default', 'plan', 'yolo']; - const MODE_DESC = { default: '默认(需权限审批,受限操作)', plan: 'Plan(需确认计划后执行)', yolo: 'YOLO(跳过所有权限检查)' }; - if (!modeInput) { - const cur = session?.permissionMode || 'yolo'; - wsSend(ws, { type: 'system_message', message: `当前模式: ${MODE_DESC[cur] || cur}\n可选: default, plan, yolo` }); - } else if (VALID_MODES.includes(modeInput.toLowerCase())) { - const mode = modeInput.toLowerCase(); - if (session) { - session.permissionMode = mode; - // Mode switching should not reset runtime context (Claude/Codex both resume). - session.updated = new Date().toISOString(); - saveSession(session); - } - wsSend(ws, { type: 'system_message', message: `权限模式已切换为: ${MODE_DESC[mode]}` }); - wsSend(ws, { type: 'mode_changed', mode }); - } else { + case '/init': { + if (agent !== 'claude') { + wsSend(ws, { type: 'system_message', message: '/init 仅支持 Claude,Codex 暂不支持该命令。' }); + break; + } + if (!sessionId || !session) { + wsSend(ws, { type: 'system_message', message: '请先进入一个会话后再执行 /init。' }); + break; + } + if (activeProcesses.has(sessionId)) { + wsSend(ws, { type: 'system_message', message: '当前会话正在处理中,请先等待完成或点击停止。' }); + break; + } + wsSend(ws, { type: 'system_message', message: '正在分析项目并生成 CLAUDE.md ...' }); + pendingSlashCommands.set(session.id, { kind: 'init' }); + handleMessage(ws, { text: '/init', sessionId: session.id, mode: session.permissionMode || 'yolo' }, { hideInHistory: true }); + break; + } + + case '/mode': { + const modeInput = parts[1]; + const VALID_MODES = ['default', 'plan', 'yolo']; + const MODE_DESC = { default: '默认(需权限审批,受限操作)', plan: 'Plan(需确认计划后执行)', yolo: 'YOLO(跳过所有权限检查)' }; + if (!modeInput) { + const cur = session?.permissionMode || 'yolo'; + wsSend(ws, { type: 'system_message', message: `当前模式: ${MODE_DESC[cur] || cur}\n可选: default, plan, yolo` }); + } else if (VALID_MODES.includes(modeInput.toLowerCase())) { + const mode = modeInput.toLowerCase(); + if (session) { + session.permissionMode = mode; + // Mode switching should not reset runtime context (Claude/Codex both resume). + session.updated = new Date().toISOString(); + saveSession(session); + } + wsSend(ws, { type: 'system_message', message: `权限模式已切换为: ${MODE_DESC[mode]}` }); + wsSend(ws, { type: 'mode_changed', mode }); + } else { wsSend(ws, { type: 'system_message', message: `无效模式: ${modeInput}\n可选: default, plan, yolo` }); } break; @@ -2088,7 +2107,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) { type: 'system_message', message: agent === 'codex' ? base + '\n/model [名称] — 查看/切换 Codex 模型(自由输入)\n/compact — 执行 Codex /compact 压缩上下文' - : base + '\n/model [名称] — 查看/切换模型(opus, sonnet, haiku)\n/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑)', + : base + '\n/model [名称] — 查看/切换模型(opus, sonnet, haiku)\n/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑)\n/init — 分析项目并生成/更新 CLAUDE.md', }); break; } @@ -2330,20 +2349,20 @@ function handleRenameSession(ws, sessionId, title) { } } - function handleSetMode(ws, sessionId, mode) { - const VALID_MODES = ['default', 'plan', 'yolo']; - if (!mode || !VALID_MODES.includes(mode)) return; - if (sessionId) { - const session = loadSession(sessionId); - if (session) { - session.permissionMode = mode; - // Same rule as /mode: don't clear runtime context on mode changes. - session.updated = new Date().toISOString(); - saveSession(session); - } - } - wsSend(ws, { type: 'mode_changed', mode }); - } + function handleSetMode(ws, sessionId, mode) { + const VALID_MODES = ['default', 'plan', 'yolo']; + if (!mode || !VALID_MODES.includes(mode)) return; + if (sessionId) { + const session = loadSession(sessionId); + if (session) { + session.permissionMode = mode; + // Same rule as /mode: don't clear runtime context on mode changes. + session.updated = new Date().toISOString(); + saveSession(session); + } + } + wsSend(ws, { type: 'mode_changed', mode }); + } function handleDisconnect(ws, wsId) { const affectedSessions = [];