feat: PNG头像替换SVG + 支持/init命令
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
BIN
public/claude.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/codex.png
Normal file
BIN
public/codex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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
123
server.js
@@ -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 仅支持 Claude,Codex 暂不支持该命令。' });
|
||||||
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 = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user