diff --git a/AGENTS.md b/AGENTS.md index 76598dc..4572ed2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,22 @@ LobsterAI is an Electron + React desktop application with two primary modes: Uses strict process isolation with IPC communication. +### Authentication Flow + +1. **登录:** 打开系统浏览器 → Portal 登录页 → URS 登录成功 → deep link `lobsterai://auth/callback?code=` +2. **换取令牌:** `POST /api/auth/exchange` 消费一次性 authCode → 返回 `accessToken`(2h) + `refreshToken`(30d) +3. **持久化:** SQLite kv store `auth_tokens` 存储双 token,应用重启后自动恢复登录态 +4. **请求认证:** `fetchWithAuth()` 在每个 API 请求附加 `Authorization: Bearer ` +5. **被动刷新:** 收到 HTTP 401 → 使用 refreshToken 调用 `POST /api/auth/refresh` → 获取新 accessToken → 重试原请求 +6. **主动刷新:** 定期检查 accessToken 距 exp < 5 分钟 → 后台静默刷新,避免请求失败 +7. **滚动续期:** 每次 refresh 签发新 refreshToken(新 30 天有效期),连续使用不掉线 +8. **退出条件:** 连续 30 天不使用(refreshToken 过期)→ 清除本地 token → 用户需重新登录 + +**关键文件:** +- Token 存储与请求:`src/renderer/services/api.ts`(`fetchWithAuth()`、token 管理) +- 登录流程:`src/main/main.ts`(deep link 处理 `lobsterai://` 协议) +- 持久化:`src/main/sqliteStore.ts`(kv 表存储 `auth_tokens`) + ### Process Model **Main Process** (`src/main/main.ts`): diff --git a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts index 98e509f..5776daa 100644 --- a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts +++ b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts @@ -27,6 +27,7 @@ import { extractGatewayMessageText, } from '../openclawHistory'; import { buildOpenClawLocalTimeContextPrompt } from '../openclawLocalTimeContextPrompt'; +import { setCoworkProxySessionId } from '../coworkOpenAICompatProxy'; import { OPENCLAW_AGENT_TIMEOUT_SECONDS } from '../openclawConfigSync'; import { t } from '../../i18n'; @@ -964,6 +965,7 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim this.rememberSessionKey(sessionId, sessionKey); this.store.updateSession(sessionId, { status: 'running' }); + setCoworkProxySessionId(sessionId); await this.ensureGatewayClientReady(); this.startChannelPolling(); @@ -3287,6 +3289,7 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim }); } this.activeTurns.delete(sessionId); + setCoworkProxySessionId(null); // NOTE: Do NOT clear lastSystemPromptBySession here — it must persist // across turns so that the system prompt is only injected on the first // turn of a session (or when it actually changes). Cleanup happens in diff --git a/src/main/libs/coworkOpenAICompatProxy.ts b/src/main/libs/coworkOpenAICompatProxy.ts index af2a6d7..5167cb1 100644 --- a/src/main/libs/coworkOpenAICompatProxy.ts +++ b/src/main/libs/coworkOpenAICompatProxy.ts @@ -75,9 +75,50 @@ let proxyServer: http.Server | null = null; let proxyPort: number | null = null; let upstreamConfig: OpenAICompatUpstreamConfig | null = null; let lastProxyError: string | null = null; +let currentCoworkSessionId: string | null = null; const toolCallExtraContentById = new Map(); + +export function setCoworkProxySessionId(sessionId: string | null): void { + currentCoworkSessionId = sessionId; +} const MAX_TOOL_CALL_EXTRA_CONTENT_CACHE = 1024; +// Regex to strip Claude SDK-injected time context prefix from user messages +const TIME_CONTEXT_PREFIX_RE = /^\[.*?\]\s*##\s*Local Time Context[\s\S]*?(?=\n\S|$)/; +// Regex to strip "Sender (untrusted metadata):" JSON block prefix +const METADATA_PREFIX_RE = /^Sender \(untrusted metadata\):\s*```json\s*\{[^}]*}\s*```\s*/s; + +function extractLastUserMessageText(body: unknown): string | null { + const obj = toOptionalObject(body); + if (!obj) return null; + const messages = obj.messages; + if (!Array.isArray(messages)) return null; + + for (let i = messages.length - 1; i >= 0; i--) { + const msg = toOptionalObject(messages[i]); + if (!msg || msg.role !== 'user') continue; + + let text: string | null = null; + if (typeof msg.content === 'string') { + text = msg.content; + } else if (Array.isArray(msg.content)) { + for (const block of msg.content) { + const b = toOptionalObject(block); + if (b && b.type === 'text' && typeof b.text === 'string') { + text = b.text; + break; + } + } + } + if (text) { + text = text.replace(METADATA_PREFIX_RE, '').replace(TIME_CONTEXT_PREFIX_RE, '').trim(); + if (text.length > 100) text = text.substring(0, 100); + return text || null; + } + } + return null; +} + function toOptionalObject(value: unknown): Record | null { if (value && typeof value === 'object' && !Array.isArray(value)) { return value as Record; @@ -2196,6 +2237,16 @@ async function handleRequest( const upstreamAPIType = resolveUpstreamAPIType(upstreamConfig.provider); const openAIRequest = anthropicToOpenAI(parsedRequestBody); + + // Inject session_id and user_message for server-side logging + if (currentCoworkSessionId) { + openAIRequest.session_id = currentCoworkSessionId; + } + const extractedUserMessage = extractLastUserMessageText(parsedRequestBody); + if (extractedUserMessage) { + openAIRequest.user_message = extractedUserMessage; + } + if (!openAIRequest.model) { openAIRequest.model = upstreamConfig.model; } diff --git a/src/main/libs/coworkRunner.ts b/src/main/libs/coworkRunner.ts index 002eef3..6681c5b 100644 --- a/src/main/libs/coworkRunner.ts +++ b/src/main/libs/coworkRunner.ts @@ -13,6 +13,7 @@ import { coworkLog, getCoworkLogPath } from './coworkLogger'; import { ensurePythonPipReady, ensurePythonRuntimeReady } from './pythonRuntime'; import { isQuestionLikeMemoryText, type CoworkMemoryGuardLevel } from './coworkMemoryExtractor'; import { SCHEDULED_TASK_SWITCH_MESSAGE } from './scheduledTaskEnginePrompt'; +import { setCoworkProxySessionId } from './coworkOpenAICompatProxy'; import { z } from 'zod'; const ATTACHMENT_LINE_RE = /^\s*(?:[-*]\s*)?(输入文件|input\s*file)\s*[::]\s*(.+?)\s*$/i; @@ -1499,6 +1500,7 @@ export class CoworkRunner extends EventEmitter { effectivePrompt = this.injectLocalHistoryPrompt(sessionId, prompt, effectivePrompt); } + setCoworkProxySessionId(sessionId); await this.runClaudeCode(activeSession, effectivePrompt, sessionCwd, effectiveSystemPrompt, options.imageAttachments); } catch (error) { console.error('Cowork session error:', error); @@ -1584,6 +1586,7 @@ export class CoworkRunner extends EventEmitter { try { const promptPrefix = this.buildPromptPrefix(); const effectivePrompt = promptPrefix ? `${promptPrefix}\n\n---\n\n${prompt}` : prompt; + setCoworkProxySessionId(sessionId); await this.runClaudeCode(activeSession, effectivePrompt, sessionCwd, effectiveSystemPrompt, options.imageAttachments); } catch (error) { console.error('Cowork continue error:', error); @@ -1600,6 +1603,7 @@ export class CoworkRunner extends EventEmitter { } this.clearPendingPermissions(sessionId); this.store.updateSession(sessionId, { status: 'idle' }); + setCoworkProxySessionId(null); } onSessionDeleted(sessionId: string): void {