feat: PNG头像替换SVG + 支持/init命令

This commit is contained in:
cc-dan
2026-03-18 01:46:09 +00:00
parent 7ec87714c5
commit 41b7757c19
6 changed files with 112 additions and 92 deletions

View File

@@ -70,29 +70,29 @@ function createAgentRuntime(deps) {
const runtimeConfig = prepareCodexCustomRuntime(codexConfig); const runtimeConfig = prepareCodexCustomRuntime(codexConfig);
if (runtimeConfig?.error) { if (runtimeConfig?.error) {
return { error: runtimeConfig.error }; return { error: runtimeConfig.error };
} }
const runtimeId = getRuntimeSessionId(session); const runtimeId = getRuntimeSessionId(session);
const args = ['exec']; const args = ['exec'];
args.push('--json', '--skip-git-repo-check'); args.push('--json', '--skip-git-repo-check');
const permMode = session.permissionMode || 'yolo'; const permMode = session.permissionMode || 'yolo';
// `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`. // `-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 // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors
// with: "unexpected argument '-s' found". // with: "unexpected argument '-s' found".
if (runtimeId && permMode === 'plan') { if (runtimeId && permMode === 'plan') {
args.push('-s', 'read-only'); args.push('-s', 'read-only');
} }
if (runtimeId) args.push('resume'); if (runtimeId) args.push('resume');
switch (permMode) { switch (permMode) {
case 'yolo': case 'yolo':
args.push('--dangerously-bypass-approvals-and-sandbox'); args.push('--dangerously-bypass-approvals-and-sandbox');
break; break;
case 'plan': case 'plan':
if (!runtimeId) args.push('-s', 'read-only'); if (!runtimeId) args.push('-s', 'read-only');
break; break;
case 'default': case 'default':
default: default:
args.push('--full-auto'); args.push('--full-auto');
break; break;
} }

View File

@@ -11,6 +11,7 @@
{ cmd: '/mode', desc: '查看/切换权限模式' }, { cmd: '/mode', desc: '查看/切换权限模式' },
{ cmd: '/cost', desc: '查看会话费用' }, { cmd: '/cost', desc: '查看会话费用' },
{ cmd: '/compact', desc: '压缩上下文' }, { cmd: '/compact', desc: '压缩上下文' },
{ cmd: '/init', desc: '生成/更新 CLAUDE.md' },
{ cmd: '/help', desc: '显示帮助' }, { cmd: '/help', desc: '显示帮助' },
]; ];
@@ -3260,13 +3261,13 @@
<div class="settings-divider"></div> <div class="settings-divider"></div>
<div class="settings-section-title">系统</div> <div class="settings-section-title">系统</div>
<div class="settings-actions" style="margin-top:0;flex-wrap:wrap;gap:10px"> <div class="settings-actions" style="margin-top:0;flex-wrap:wrap;gap:10px">
<button class="btn-test" id="pw-open-modal-btn" style="padding:6px 16px">修改密码</button> <button class="btn-test" id="pw-open-modal-btn" style="padding:6px 16px">修改密码</button>
<button class="btn-test" id="check-update-btn" style="padding:6px 16px">检查更新</button> <button class="btn-test" id="check-update-btn" style="padding:6px 16px">检查更新</button>
</div> </div>
<div class="settings-status" id="update-status" style="margin-top:8px"></div> <div class="settings-status" id="update-status" style="margin-top:8px"></div>
`; `;
overlay.appendChild(panel); overlay.appendChild(panel);
document.body.appendChild(overlay); document.body.appendChild(overlay);
@@ -3281,9 +3282,9 @@
const codexStatus = panel.querySelector('#codex-status'); const codexStatus = panel.querySelector('#codex-status');
const codexSaveBtn = panel.querySelector('#codex-save-btn'); const codexSaveBtn = panel.querySelector('#codex-save-btn');
const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
const checkUpdateBtn = panel.querySelector('#check-update-btn'); const checkUpdateBtn = panel.querySelector('#check-update-btn');
const updateStatusEl = panel.querySelector('#update-status'); const updateStatusEl = panel.querySelector('#update-status');
let currentCodexConfig = null; let currentCodexConfig = null;
let codexEditingProfiles = []; let codexEditingProfiles = [];
@@ -3822,12 +3823,12 @@
renderModelCustomArea(); renderModelCustomArea();
}; };
// === Notify Config UI (moved to subpage) === // === Notify Config UI (moved to subpage) ===
// notify config is handled by openNotifySubpage() // notify config is handled by openNotifySubpage()
const closeBtn = panel.querySelector('.settings-close'); const closeBtn = panel.querySelector('.settings-close');
const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
pwOpenModalBtn.addEventListener('click', openPasswordModal); pwOpenModalBtn.addEventListener('click', openPasswordModal);
// Check update button // Check update button
const checkUpdateBtn = panel.querySelector('#check-update-btn'); const checkUpdateBtn = panel.querySelector('#check-update-btn');

BIN
public/claude.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/codex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1021,10 +1021,10 @@ body.session-loading-active {
background: transparent; background: transparent;
border: none; border: none;
} }
/* Codex avatar: GPT logo on green bg */ /* Codex avatar: transparent bg to match the supplied asset */
.msg.assistant.agent-codex .msg-avatar { .msg.assistant.agent-codex .msg-avatar {
background: #10a37f; background: transparent;
color: #fff; border: none;
} }
.msg-bubble { .msg-bubble {

123
server.js
View File

@@ -437,27 +437,27 @@ const activeTokens = new Set();
const AUTH_FAIL_WINDOW = 5 * 60 * 1000; // 5 minutes const AUTH_FAIL_WINDOW = 5 * 60 * 1000; // 5 minutes
const AUTH_FAIL_MAX = 3; const AUTH_FAIL_MAX = 3;
const authFailures = new Map(); // ip -> [timestamp, ...] const authFailures = new Map(); // ip -> [timestamp, ...]
let bannedIPs = new Set(); let bannedIPs = new Set();
// Tailscale / loopback whitelist — never ban these IPs. // Tailscale / loopback whitelist — never ban these IPs.
// Extra whitelist can be provided via env var (comma/space separated): // Extra whitelist can be provided via env var (comma/space separated):
// CC_WEB_IP_WHITELIST="<ip1>,<ip2>" // CC_WEB_IP_WHITELIST="<ip1>,<ip2>"
const EXTRA_WHITELIST_IPS = new Set( const EXTRA_WHITELIST_IPS = new Set(
String(process.env.CC_WEB_IP_WHITELIST || '') String(process.env.CC_WEB_IP_WHITELIST || '')
.split(/[\s,]+/) .split(/[\s,]+/)
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean) .filter(Boolean)
.map(s => s.replace(/^::ffff:/, '')) .map(s => s.replace(/^::ffff:/, ''))
); );
function isWhitelistedIP(ip) { function isWhitelistedIP(ip) {
if (!ip) return false; if (!ip) return false;
const cleaned = ip.replace(/^::ffff:/, ''); const cleaned = ip.replace(/^::ffff:/, '');
return cleaned === '127.0.0.1' return cleaned === '127.0.0.1'
|| cleaned === '::1' || cleaned === '::1'
|| cleaned.startsWith('100.') || cleaned.startsWith('100.')
|| EXTRA_WHITELIST_IPS.has(cleaned); || EXTRA_WHITELIST_IPS.has(cleaned);
} }
function loadBannedIPs() { function loadBannedIPs() {
try { try {
@@ -2055,24 +2055,43 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
break; break;
} }
case '/mode': { case '/init': {
const modeInput = parts[1]; if (agent !== 'claude') {
const VALID_MODES = ['default', 'plan', 'yolo']; wsSend(ws, { type: 'system_message', message: '/init 仅支持 ClaudeCodex 暂不支持该命令。' });
const MODE_DESC = { default: '默认(需权限审批,受限操作)', plan: 'Plan需确认计划后执行', yolo: 'YOLO跳过所有权限检查' }; break;
if (!modeInput) { }
const cur = session?.permissionMode || 'yolo'; if (!sessionId || !session) {
wsSend(ws, { type: 'system_message', message: `当前模式: ${MODE_DESC[cur] || cur}\n可选: default, plan, yolo` }); wsSend(ws, { type: 'system_message', message: '请先进入一个会话后再执行 /init。' });
} else if (VALID_MODES.includes(modeInput.toLowerCase())) { break;
const mode = modeInput.toLowerCase(); }
if (session) { if (activeProcesses.has(sessionId)) {
session.permissionMode = mode; wsSend(ws, { type: 'system_message', message: '当前会话正在处理中,请先等待完成或点击停止。' });
// Mode switching should not reset runtime context (Claude/Codex both resume). break;
session.updated = new Date().toISOString(); }
saveSession(session); wsSend(ws, { type: 'system_message', message: '正在分析项目并生成 CLAUDE.md ...' });
} pendingSlashCommands.set(session.id, { kind: 'init' });
wsSend(ws, { type: 'system_message', message: `权限模式已切换为: ${MODE_DESC[mode]}` }); handleMessage(ws, { text: '/init', sessionId: session.id, mode: session.permissionMode || 'yolo' }, { hideInHistory: true });
wsSend(ws, { type: 'mode_changed', mode }); break;
} else { }
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` }); wsSend(ws, { type: 'system_message', message: `无效模式: ${modeInput}\n可选: default, plan, yolo` });
} }
break; break;
@@ -2088,7 +2107,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
type: 'system_message', type: 'system_message',
message: agent === 'codex' message: agent === 'codex'
? base + '\n/model [名称] — 查看/切换 Codex 模型(自由输入)\n/compact — 执行 Codex /compact 压缩上下文' ? 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; break;
} }
@@ -2330,20 +2349,20 @@ function handleRenameSession(ws, sessionId, title) {
} }
} }
function handleSetMode(ws, sessionId, mode) { function handleSetMode(ws, sessionId, mode) {
const VALID_MODES = ['default', 'plan', 'yolo']; const VALID_MODES = ['default', 'plan', 'yolo'];
if (!mode || !VALID_MODES.includes(mode)) return; if (!mode || !VALID_MODES.includes(mode)) return;
if (sessionId) { if (sessionId) {
const session = loadSession(sessionId); const session = loadSession(sessionId);
if (session) { if (session) {
session.permissionMode = mode; session.permissionMode = mode;
// Same rule as /mode: don't clear runtime context on mode changes. // Same rule as /mode: don't clear runtime context on mode changes.
session.updated = new Date().toISOString(); session.updated = new Date().toISOString();
saveSession(session); saveSession(session);
} }
} }
wsSend(ws, { type: 'mode_changed', mode }); wsSend(ws, { type: 'mode_changed', mode });
} }
function handleDisconnect(ws, wsId) { function handleDisconnect(ws, wsId) {
const affectedSessions = []; const affectedSessions = [];