feat: enhance session UX and codex defaults

This commit is contained in:
shiyue
2026-05-15 18:35:38 +08:00
parent a6f3ab0485
commit 62ab6f358d
5 changed files with 564 additions and 45 deletions

142
server.js
View File

@@ -528,9 +528,16 @@ let MODEL_MAP = {
const VALID_AGENTS = new Set(['claude', 'codex']);
// Codex CLI has its own default model if --model is omitted. We override it for new Codex sessions
// to keep cc-web behavior stable and predictable.
const DEFAULT_CODEX_MODEL = 'gpt-5.4';
// Codex 默认模型优先读取 ~/.codex/config.toml缺失时再回退到旧默认值。
const FALLBACK_CODEX_MODEL = 'gpt-5.4';
const CODEX_REASONING_LEVELS = new Set(['low', 'medium', 'high', 'xhigh']);
function getLocalCodexConfigTomlPath() {
const codexHome = String(process.env.CODEX_HOME || '').trim();
if (codexHome) return path.join(codexHome, 'config.toml');
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
return path.join(homeDir, '.codex', 'config.toml');
}
// === Model Config ===
const DEFAULT_MODEL_CONFIG = {
@@ -547,6 +554,91 @@ const DEFAULT_CODEX_CONFIG = {
supportsSearch: false,
};
function stripTomlInlineComment(value) {
const raw = String(value || '');
let inDoubleQuote = false;
let inSingleQuote = false;
let escaped = false;
for (let i = 0; i < raw.length; i += 1) {
const ch = raw[i];
if (inDoubleQuote) {
if (escaped) {
escaped = false;
continue;
}
if (ch === '\\') {
escaped = true;
continue;
}
if (ch === '"') inDoubleQuote = false;
continue;
}
if (inSingleQuote) {
if (ch === '\'') inSingleQuote = false;
continue;
}
if (ch === '"') {
inDoubleQuote = true;
continue;
}
if (ch === '\'') {
inSingleQuote = true;
continue;
}
if (ch === '#') return raw.slice(0, i).trim();
}
return raw.trim();
}
function parseTomlStringValue(value) {
const raw = stripTomlInlineComment(value);
if (!raw) return '';
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith('\'') && raw.endsWith('\''))) {
if (raw.startsWith('"')) {
try {
return JSON.parse(raw);
} catch {}
}
return raw.slice(1, -1);
}
return raw;
}
function loadLocalCodexTomlConfig() {
try {
const configPath = getLocalCodexConfigTomlPath();
if (!configPath || !fs.existsSync(configPath)) {
return { model: '', reasoningEffort: '' };
}
const text = fs.readFileSync(configPath, 'utf8');
const parsed = { model: '', reasoningEffort: '' };
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (trimmed.startsWith('[')) break;
const eqIndex = trimmed.indexOf('=');
if (eqIndex <= 0) continue;
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1).trim();
if (key === 'model') parsed.model = parseTomlStringValue(value);
if (key === 'model_reasoning_effort') parsed.reasoningEffort = parseTomlStringValue(value).toLowerCase();
}
return parsed;
} catch {
return { model: '', reasoningEffort: '' };
}
}
function getDefaultCodexModel() {
const localConfig = loadLocalCodexTomlConfig();
const model = String(localConfig.model || '').trim() || FALLBACK_CODEX_MODEL;
const reasoningEffort = String(localConfig.reasoningEffort || '').trim().toLowerCase();
if (CODEX_REASONING_LEVELS.has(reasoningEffort)) {
return `${model}(${reasoningEffort})`;
}
return model;
}
function loadModelConfig() {
try {
if (fs.existsSync(MODEL_CONFIG_PATH)) {
@@ -1404,7 +1496,10 @@ function modelShortName(fullModel) {
}
function sessionModelLabel(session) {
if (!session?.model) return null;
if (!session) return null;
if (!session.model) {
return isClaudeSession(session) ? null : getDefaultCodexModel();
}
return isClaudeSession(session) ? (modelShortName(session.model) || session.model) : session.model;
}
@@ -1985,8 +2080,8 @@ const server = http.createServer((req, res) => {
return;
}
if (req.method === 'DELETE' && url.pathname.startsWith('/api/attachments/')) {
const token = extractBearerToken(req);
if (url.pathname.startsWith('/api/attachments/')) {
const token = extractBearerToken(req) || String(url.searchParams.get('token') || '');
if (!token || !activeTokens.has(token)) {
return jsonResponse(res, 401, { ok: false, message: 'Not authenticated' });
}
@@ -1994,8 +2089,30 @@ const server = http.createServer((req, res) => {
if (!id) {
return jsonResponse(res, 400, { ok: false, message: '缺少附件 ID' });
}
removeAttachmentById(id);
return jsonResponse(res, 200, { ok: true });
if (req.method === 'GET') {
const meta = loadAttachmentMeta(id);
const state = currentAttachmentState(meta);
if (state !== 'available' || !meta?.path || !fs.existsSync(meta.path)) {
return jsonResponse(res, 404, { ok: false, message: '附件不存在或已过期' });
}
try {
const stat = fs.statSync(meta.path);
res.writeHead(200, {
'Content-Type': meta.mime || 'application/octet-stream',
'Content-Length': stat.size,
'Content-Disposition': `inline; filename="${(meta.filename || 'image').replace(/"/g, '\\"')}"`,
'Cache-Control': 'private, no-store, max-age=0',
});
fs.createReadStream(meta.path).pipe(res);
} catch (err) {
return jsonResponse(res, 500, { ok: false, message: `读取附件失败: ${err.message}` });
}
return;
}
if (req.method === 'DELETE') {
removeAttachmentById(id);
return jsonResponse(res, 200, { ok: true });
}
}
if (req.method === 'GET' && url.pathname === '/api/fs/list') {
@@ -2480,7 +2597,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
const modelInput = parts[1];
if (agent === 'codex') {
if (!modelInput) {
const current = session?.model || '配置默认模型';
const current = session?.model || getDefaultCodexModel();
wsSend(ws, { type: 'system_message', message: `当前 Codex 模型: ${current}\n用法: /model <模型名>` });
} else {
if (session) {
@@ -2638,9 +2755,9 @@ function handleNewSession(ws, msg) {
agent,
claudeSessionId: null,
codexThreadId: null,
// For Codex: explicitly set a default model on creation so we don't inherit Codex CLI defaults.
// For Codex: 在会话创建时写入 ~/.codex/config.toml 中的默认模型,避免 UI 与运行时脱节。
// For Claude: default to opus (1M) so --model is always passed to CLI.
model: agent === 'codex' ? DEFAULT_CODEX_MODEL : MODEL_MAP.opus,
model: agent === 'codex' ? getDefaultCodexModel() : MODEL_MAP.opus,
permissionMode: requestedMode,
totalCost: 0,
totalUsage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -2954,7 +3071,7 @@ function handleMessage(ws, msg, options = {}) {
agent,
claudeSessionId: null,
codexThreadId: null,
model: agent === 'codex' ? DEFAULT_CODEX_MODEL : null,
model: agent === 'codex' ? getDefaultCodexModel() : null,
permissionMode: mode || 'yolo',
totalCost: 0,
totalUsage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -3179,6 +3296,7 @@ const {
MODEL_MAP,
loadModelConfig,
applyCustomTemplateToSettings,
getDefaultCodexModel,
loadCodexConfig,
prepareCodexCustomRuntime,
wsSend,