merge: 0629001 into 0629002
合并反馈、语音 ASR、MindSpace 修复等 0629001 发布改动,并与 Agent Runs 网关改动完成冲突解决。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+139
-24
@@ -36,8 +36,16 @@ import type {
|
||||
PlazaPostBrief,
|
||||
MindSpaceRedactedCopyResult,
|
||||
MindSpaceUpload,
|
||||
FeedbackContextInput,
|
||||
FeedbackImageInput,
|
||||
FeedbackSubmission,
|
||||
FeedbackSubmissionType,
|
||||
FeedbackBoardPage,
|
||||
PortalUser,
|
||||
Session,
|
||||
SessionConversationPage,
|
||||
SessionListPage,
|
||||
SessionSummary,
|
||||
SessionEvent,
|
||||
SessionListResponse,
|
||||
UsageRecord,
|
||||
@@ -45,8 +53,9 @@ import type {
|
||||
UserNotification,
|
||||
PlanDefinition,
|
||||
ActiveSubscription,
|
||||
UserMemorySyncResponse,
|
||||
} from '../types';
|
||||
import { CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES } from '../utils/imageUpload';
|
||||
import { CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES } from '../utils/imageUpload';
|
||||
import { normalizeConversationMessages, normalizeUserMessageForApi } from '../utils/message';
|
||||
|
||||
const API = '/api';
|
||||
@@ -229,7 +238,10 @@ async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
if (res.status === 204) return undefined as T;
|
||||
const text = await res.text().catch(() => '');
|
||||
if (text.trimStart().startsWith('<')) {
|
||||
throw new ApiError(res.status, '服务器返回了意外的页面,请确认后端服务已启动并已重启');
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
@@ -287,7 +299,10 @@ async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
if (res.status === 204) return undefined as T;
|
||||
const rawText = await res.text().catch(() => '');
|
||||
if (rawText.trimStart().startsWith('<')) {
|
||||
throw new ApiError(res.status, '服务器返回了意外的页面,请确认后端服务已启动并已重启');
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${API}${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(rawText) as T;
|
||||
@@ -325,6 +340,20 @@ export async function rememberProjectContext(
|
||||
});
|
||||
}
|
||||
|
||||
export async function rememberUserMemory(sessionId: string): Promise<UserMemorySyncResponse> {
|
||||
return apiFetch<UserMemorySyncResponse>('/user-memory/v1/remember-recent', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sessionId }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function syncUserMemory(sessionId: string): Promise<UserMemorySyncResponse> {
|
||||
return apiFetch<UserMemorySyncResponse>('/user-memory/v1/sync', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sessionId }),
|
||||
});
|
||||
}
|
||||
|
||||
export type WechatAuthConfig = {
|
||||
enabled: boolean;
|
||||
inWechat?: boolean;
|
||||
@@ -395,8 +424,11 @@ export async function getWechatJsSdkSignature(url: string): Promise<WechatJsSdkS
|
||||
export async function checkAuth(): Promise<AuthStatus> {
|
||||
try {
|
||||
const response = await fetch('/auth/status');
|
||||
if (!response.ok) return { authenticated: false };
|
||||
const status = (await response.json()) as AuthStatus;
|
||||
if (!response.ok) {
|
||||
if (status.mode === 'unavailable') return status;
|
||||
return { authenticated: false };
|
||||
}
|
||||
if (status.authenticated) resetUnauthorizedGuard();
|
||||
if (status.authenticated && status.mode === 'user' && !status.capabilities) {
|
||||
try {
|
||||
@@ -431,6 +463,53 @@ export async function getMyUsage(): Promise<UsageRecord[]> {
|
||||
return result.records ?? [];
|
||||
}
|
||||
|
||||
export async function submitFeedback(input: {
|
||||
type: FeedbackSubmissionType;
|
||||
title: string;
|
||||
description: string;
|
||||
contact?: string;
|
||||
images?: FeedbackImageInput[];
|
||||
context?: FeedbackContextInput;
|
||||
}): Promise<FeedbackSubmission> {
|
||||
const result = await portalFetch<{ feedback: FeedbackSubmission }>('/auth/feedback', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: input.type,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
contact: input.contact,
|
||||
images: input.images,
|
||||
context: input.context,
|
||||
}),
|
||||
});
|
||||
return result.feedback;
|
||||
}
|
||||
|
||||
export async function getMyFeedback(limit = 20): Promise<FeedbackSubmission[]> {
|
||||
const result = await portalFetch<{ items: FeedbackSubmission[] }>(
|
||||
`/auth/feedback?limit=${encodeURIComponent(String(limit))}`,
|
||||
);
|
||||
return result.items ?? [];
|
||||
}
|
||||
|
||||
export async function listFeedbackBoard(page = 1, limit = 10): Promise<FeedbackBoardPage> {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
limit: String(limit),
|
||||
});
|
||||
return portalFetch<FeedbackBoardPage>(`/auth/feedback/board?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getFeedbackDetail(feedbackId: string): Promise<{
|
||||
item: FeedbackSubmission;
|
||||
isMine: boolean;
|
||||
}> {
|
||||
return portalFetch<{ item: FeedbackSubmission; isMine: boolean }>(
|
||||
`/auth/feedback/${encodeURIComponent(feedbackId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMyBillingLedger(limit = 30): Promise<LedgerEntry[]> {
|
||||
const query = limit ? `?limit=${encodeURIComponent(String(limit))}` : '';
|
||||
const result = await portalFetch<{ entries: LedgerEntry[] }>(`/auth/billing/ledger${query}`);
|
||||
@@ -536,11 +615,13 @@ export async function listMindSpaceAssets(
|
||||
export async function uploadMindSpaceAsset(
|
||||
categoryId: string,
|
||||
file: File,
|
||||
options: { maxImageBytes?: number; onProgress?: (progress: number) => void } = {},
|
||||
): Promise<MindSpaceAsset> {
|
||||
if (file.type.startsWith('image/') && file.size > CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES) {
|
||||
const maxImageBytes = options.maxImageBytes ?? CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES;
|
||||
if (file.type.startsWith('image/') && file.size > maxImageBytes) {
|
||||
throw new ApiError(
|
||||
413,
|
||||
`图片文件过大,当前 ${(file.size / 1024 / 1024).toFixed(2)}MB,请先压缩到 ${CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES / 1024 / 1024}MB 以下再上传。`,
|
||||
`图片文件过大,当前 ${(file.size / 1024 / 1024).toFixed(2)}MB,超过 ${maxImageBytes / 1024 / 1024}MB 上传上限。`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -555,20 +636,7 @@ export async function uploadMindSpaceAsset(
|
||||
});
|
||||
|
||||
try {
|
||||
const contentResponse = await fetch(created.data.uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
body: file,
|
||||
});
|
||||
if (!contentResponse.ok) {
|
||||
const body = (await contentResponse.json().catch(() => null)) as {
|
||||
error?: { message?: string };
|
||||
} | null;
|
||||
throw new ApiError(
|
||||
contentResponse.status,
|
||||
body?.error?.message ?? '文件内容上传失败',
|
||||
);
|
||||
}
|
||||
await uploadFileContent(created.data.uploadUrl, file, options.onProgress);
|
||||
const completed = await apiFetch<{ data: MindSpaceAsset }>(
|
||||
`/mindspace/v1/uploads/${created.data.id}/complete`,
|
||||
{ method: 'POST', body: JSON.stringify({}) },
|
||||
@@ -582,6 +650,40 @@ export async function uploadMindSpaceAsset(
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFileContent(
|
||||
url: string,
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', url);
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (!event.lengthComputable || !onProgress) return;
|
||||
onProgress(Math.min(0.99, Math.max(0, event.loaded / event.total)));
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
onProgress?.(1);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let message = '文件内容上传失败';
|
||||
try {
|
||||
const body = JSON.parse(xhr.responseText || '{}') as { error?: { message?: string } };
|
||||
message = body?.error?.message ?? message;
|
||||
} catch {
|
||||
// Keep the generic upload error when the response is not JSON.
|
||||
}
|
||||
reject(new ApiError(xhr.status, message));
|
||||
};
|
||||
xhr.onerror = () => reject(new ApiError(0, '文件内容上传失败'));
|
||||
xhr.onabort = () => reject(new ApiError(0, '文件上传已取消'));
|
||||
xhr.send(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMindSpaceAsset(assetId: string): Promise<void> {
|
||||
await apiFetch(`/mindspace/v1/assets/${assetId}`, { method: 'DELETE' });
|
||||
}
|
||||
@@ -2018,9 +2120,18 @@ export async function applyLocalLlmFallback(sessionId: string): Promise<{
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSessions(): Promise<Session[]> {
|
||||
const result = await apiFetch<SessionListResponse>('/sessions');
|
||||
return result.sessions ?? [];
|
||||
export async function listSessions(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
query?: string;
|
||||
}): Promise<{ items: SessionSummary[]; page: SessionListPage }> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.limit != null) params.set('limit', String(options.limit));
|
||||
if (options?.offset != null) params.set('offset', String(options.offset));
|
||||
if (options?.query?.trim()) params.set('query', options.query.trim());
|
||||
const qs = params.size ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<SessionListResponse>(`/sessions${qs}`);
|
||||
return { items: result.sessions ?? [], page: result.page ?? {} };
|
||||
}
|
||||
|
||||
function sessionPath(sessionId: string, suffix = '') {
|
||||
@@ -2038,19 +2149,23 @@ export async function deleteChatSession(sessionId: string): Promise<void> {
|
||||
export async function loadSessionDetail(
|
||||
sessionId: string,
|
||||
hints?: { messageCount?: number; updatedAt?: string },
|
||||
history?: { before?: number; limit?: number },
|
||||
): Promise<{
|
||||
session: Session;
|
||||
messages: Message[];
|
||||
page: SessionConversationPage;
|
||||
}> {
|
||||
const params = new URLSearchParams();
|
||||
if (hints?.messageCount != null) params.set('hint_mc', String(hints.messageCount));
|
||||
if (hints?.updatedAt) params.set('hint_ua', hints.updatedAt);
|
||||
if (history?.before != null) params.set('history_before', String(history.before));
|
||||
if (history?.limit != null) params.set('history_limit', String(history.limit));
|
||||
const qs = params.size ? `?${params.toString()}` : '';
|
||||
const detail = await apiFetch<Session>(`${sessionPath(sessionId)}${qs}`);
|
||||
const messages = normalizeConversationMessages(
|
||||
(detail.conversation ?? []).filter((m) => m.metadata?.userVisible),
|
||||
);
|
||||
return { session: detail, messages };
|
||||
return { session: detail, messages, page: detail.conversation_page ?? {} };
|
||||
}
|
||||
|
||||
export async function createAgentRun(
|
||||
|
||||
Reference in New Issue
Block a user