fix(web): resolve project id from session id for task runs

Task template messages were posting to project_id=1 when local session cache lacked projectId, causing 403 on POST /api/v1/tasks/runs. Parse sess-p{id}-u{user} encoding as fallback and surface HTTP status on task failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 03:23:53 +00:00
co-authored by Cursor
parent 8032dce435
commit 3afd606635
4 changed files with 87 additions and 7 deletions
+20 -4
View File
@@ -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,
);
+14 -1
View File
@@ -21,8 +21,21 @@ export async function createTaskRun(body: CreateTaskRunBody): Promise<CreateTask
headers: getBearerAuthHeaders('application/json'),
body: JSON.stringify(body),
});
// #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:'taskRuns.ts:createTaskRun',message:'create task run response',data:{status:res.status,ok:res.ok,projectId:body.project_id,taskKey:body.task_key,sessionId:body.session_id},timestamp:Date.now(),hypothesisId:'H5'})}).catch(()=>{});
// #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();
}
+22 -1
View File
@@ -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(
{
+31 -1
View File
@@ -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<ProjectSession, 'sessionId' | 'projectId'> | 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,