diff --git a/web/src/App.tsx b/web/src/App.tsx index f1121192..2bbc4c3f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -142,7 +142,11 @@ import { fetchChatTurnStatus } from './api/chatTurn'; import { createClientTurnId } from './lib/createClientTurnId'; import { mergeSessionUser } from './lib/mergeSessionUser'; import { mapServerProjectToProjectInfo, mergeProjectLists } from './lib/projectAdapter'; -import { mapServerSessionToProjectSession, mergeSessionLists } from './lib/sessionAdapter'; +import { + mapServerSessionToProjectSession, + mergeSessionLists, + resolveSessionProjectId, +} from './lib/sessionAdapter'; import { SendMessageResult } from './types/sendMessage'; import { CategoryFilterBar } from './components/history/CategoryFilterBar'; import { HistoryCalendar } from './components/history/HistoryCalendar'; @@ -600,7 +604,8 @@ export default function App() { // 获取当前激活会话的项目ID (默认为 1) const currentProjectId = useMemo(() => { const activeSess = sessions.find((s) => s.sessionId === activeSessionId); - return activeSess ? activeSess.projectId : 1; + const resolved = resolveSessionProjectId(activeSess); + return resolved > 0 ? resolved : 1; }, [sessions, activeSessionId]); // 获取当前项目与当前用户在其中的成员职责 @@ -2444,7 +2449,14 @@ export default function App() { // fall through to network retry path } } - failOrRetry(buildNetworkErrorInfo()); + const httpStatus = + typeof _err === 'object' && _err !== null && 'httpStatus' in _err + ? Number((_err as { httpStatus?: number }).httpStatus) + : undefined; + // #region agent log + fetch('http://localhost:7284/ingest/86170f40-1203-4130-a0c7-f6e39ba00464',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6c6caa'},body:JSON.stringify({sessionId:'6c6caa',location:'App.tsx:runTaskAttempt.catch',message:'task run failed',data:{httpStatus,taskProjectId:task.projectId,sessionId:task.sessionId,taskKey:task.taskKey},timestamp:Date.now(),hypothesisId:'H5'})}).catch(()=>{}); + // #endregion + failOrRetry(buildNetworkErrorInfo(httpStatus)); }); }; @@ -2680,7 +2692,11 @@ export default function App() { const currentProj = sessions.find((s) => s.sessionId === activeSessionId); // 项目五要素暂取会话标题/设备类型;客户名沿用 bizdev-cli 演示默认值,五要素完整建模留待后续 const clientTurnId = `turn_${Date.now()}`; - const projId = currentProj?.projectId || 1; + const projId = resolveSessionProjectId(currentProj, currentProjectId); + if (projId <= 0) { + console.error('[task] 无法解析会话所属项目', activeSessionId); + return { accepted: false }; + } const matchedDeliverable = deliverables.find( (d) => d.projectId === projId && d.taskKey === matchedTemplate.taskKey, ); diff --git a/web/src/api/taskRuns.ts b/web/src/api/taskRuns.ts index 7896ea89..bc0add34 100644 --- a/web/src/api/taskRuns.ts +++ b/web/src/api/taskRuns.ts @@ -21,8 +21,21 @@ export async function createTaskRun(body: CreateTaskRunBody): Promise{}); + // #endregion if (!res.ok) { - throw new Error(`create task run failed: HTTP ${res.status}`); + let detail = ''; + try { + const payload = (await res.json()) as { message?: string; error?: string; error_code?: string }; + detail = payload.message || payload.error || payload.error_code || ''; + } catch { + // ignore non-json body + } + throw Object.assign( + new Error(`create task run failed: HTTP ${res.status}${detail ? `: ${detail}` : ''}`), + { httpStatus: res.status }, + ); } return res.json(); } diff --git a/web/src/lib/sessionAdapter.test.ts b/web/src/lib/sessionAdapter.test.ts index 866910d4..faa5712d 100644 --- a/web/src/lib/sessionAdapter.test.ts +++ b/web/src/lib/sessionAdapter.test.ts @@ -1,6 +1,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { mapServerSessionToProjectSession, mergeSessionLists } from './sessionAdapter'; +import { + mapServerSessionToProjectSession, + mergeSessionLists, + parseProjectIdFromSessionId, + resolveSessionProjectId, +} from './sessionAdapter'; import { ProjectInfo, ProjectSession } from '../types/workbench'; const projects: ProjectInfo[] = [ @@ -16,6 +21,22 @@ const projects: ProjectInfo[] = [ }, ]; +test('parseProjectIdFromSessionId decodes sess-p{id}-u{user}', () => { + assert.equal(parseProjectIdFromSessionId('sess-p143-u9'), 143); + assert.equal(parseProjectIdFromSessionId('sess-neuropulse-u1'), undefined); +}); + +test('resolveSessionProjectId prefers explicit id then session id encoding', () => { + assert.equal( + resolveSessionProjectId({ sessionId: 'sess-p143-u9', projectId: 0 }), + 143, + ); + assert.equal( + resolveSessionProjectId({ sessionId: 'sess-p143-u9', projectId: 143 }), + 143, + ); +}); + test('mapServerSessionToProjectSession fills project metadata', () => { const mapped = mapServerSessionToProjectSession( { diff --git a/web/src/lib/sessionAdapter.ts b/web/src/lib/sessionAdapter.ts index 45c6dc7a..e30d8fa3 100644 --- a/web/src/lib/sessionAdapter.ts +++ b/web/src/lib/sessionAdapter.ts @@ -1,6 +1,32 @@ import { ChatSessionSummary } from '../api/chatSessions'; import { ProjectInfo, ProjectSession, StageKey } from '../types/workbench'; +/** 从 sess-p{projectId}-u{userId} 私聊会话 ID 解析项目 ID。 */ +export function parseProjectIdFromSessionId(sessionId: string): number | undefined { + const match = sessionId.match(/^sess-p(\d+)-u\d+$/); + if (!match) return undefined; + const id = Number.parseInt(match[1], 10); + return Number.isFinite(id) && id > 0 ? id : undefined; +} + +/** 解析会话所属项目:优先显式 projectId,其次 sessionId 编码,最后外部 fallback。 */ +export function resolveSessionProjectId( + session: Pick | undefined, + fallbackProjectId?: number, +): number { + if (session?.projectId && session.projectId > 0) { + return session.projectId; + } + if (session?.sessionId) { + const parsed = parseProjectIdFromSessionId(session.sessionId); + if (parsed) return parsed; + } + if (fallbackProjectId && fallbackProjectId > 0) { + return fallbackProjectId; + } + return 0; +} + function formatSessionUpdatedAt(iso: string): string { const parsed = Date.parse(iso); if (Number.isNaN(parsed)) { @@ -31,7 +57,11 @@ export function mapServerSessionToProjectSession( return { sessionId: session.session_id, userId: session.user_id, - projectId: session.project_id ?? existing?.projectId ?? 0, + projectId: + session.project_id ?? + existing?.projectId ?? + parseProjectIdFromSessionId(session.session_id) ?? + 0, title: session.title || session.project_name || project?.title || existing?.title || '对话协同会话', deviceType: project?.deviceType || existing?.deviceType || '', stage,