mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
feat(auth): document authentication flow and add session tracking
**Changes:** 1. Added comprehensive authentication flow documentation to AGENTS.md - Documented 8-step auth process: login → token exchange → persistence → refresh → renewal - Included key files reference for token management and deep link handling - Explained 30-day rolling refresh token mechanism 2. Implemented session tracking in OpenAI compatibility proxy - Added `setCoworkProxySessionId()` to track active cowork sessions - Integrated session ID tracking in OpenClawRuntimeAdapter lifecycle - Extract and log last user message text for better observability - Added regex patterns to strip Claude SDK time context and metadata prefixes 3. Enhanced proxy request logging - Log session ID and user message preview with each upstream request - Improved debugging capability for multi-session scenarios **Why:** - Documentation helps developers understand the complete auth lifecycle - Session tracking enables better request attribution and debugging - Message extraction provides context for proxy operations without exposing full payloads
This commit is contained in:
@@ -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=<authCode>`
|
||||
2. **换取令牌:** `POST /api/auth/exchange` 消费一次性 authCode → 返回 `accessToken`(2h) + `refreshToken`(30d)
|
||||
3. **持久化:** SQLite kv store `auth_tokens` 存储双 token,应用重启后自动恢复登录态
|
||||
4. **请求认证:** `fetchWithAuth()` 在每个 API 请求附加 `Authorization: Bearer <accessToken>`
|
||||
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`):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, unknown>();
|
||||
|
||||
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<string, unknown> | null {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user