docs(app): explain common API session workspace and geometry helpers
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* 应用通用JSON/FormData请求与登录跳转辅助。请求默认经过同源Foundry代理,错误封装为ApiError,
|
||||
* 界面通过errorText映射中文说明;业务schema和账户代次等更强校验由各领域适配器处理。
|
||||
* 本函数本身不设置超时或自动重试,取消依赖传入signal,代理传输时限在对应路由/响应封装中管理。
|
||||
*/
|
||||
/** 账户资料响应的静态类型,不能用它代替登录会话有效性或权限校验。 */
|
||||
export type Profile = {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -5,20 +11,25 @@ export type Profile = {
|
||||
email: string | null;
|
||||
avatar_url?: string | null;
|
||||
};
|
||||
/** 保留HTTP状态、服务details和原Retry-After头,message使用稳定错误码,供上层决定显示/重试。 */
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly details?: unknown;
|
||||
readonly retryAfter?: string | null;
|
||||
/** 以Error兼容方式封装服务错误,不把details自动拼进面向用户文案。 */
|
||||
constructor(code: string, status: number, details?: unknown, retryAfter?: string | null) {
|
||||
super(code); this.name = "ApiError"; this.status = status; this.details = details; this.retryAfter = retryAfter;
|
||||
}
|
||||
}
|
||||
/** 泛型T只是调用方的响应形状声明,不执行运行时schema检查。
|
||||
* 204返回null;其它响应必须可解析JSON,非2xx统一抛ApiError,成功body直接交业务层验证。 */
|
||||
export async function api<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const res = await fetch("/api/foundry/" + path, {
|
||||
...options,
|
||||
// FormData让浏览器生成boundary,其它请求默认JSON;调用方headers可覆盖默认Content-Type。
|
||||
headers: { ...(options.body instanceof FormData ? {} : { "Content-Type": "application/json" }), ...options.headers },
|
||||
});
|
||||
let body: unknown;
|
||||
@@ -26,21 +37,27 @@ export async function api<T>(
|
||||
else {
|
||||
try { body = await res.json(); }
|
||||
catch (error) {
|
||||
// 消费body失败时优先传播明确取消原因;语法坏JSON与传输中断使用不同错误码。
|
||||
if (options.signal?.aborted) throw options.signal.reason;
|
||||
const malformed = error instanceof SyntaxError;
|
||||
throw new ApiError(malformed ? "invalid_api_response" : "response_interrupted", res.ok ? 502 : res.status);
|
||||
}
|
||||
}
|
||||
const payload = body as { error?: string | { code?: string }; details?: unknown } | null;
|
||||
// 服务error兼容字符串或{code},无可用码则request_failed,并保留Retry-After原值供上层解释。
|
||||
if (!res.ok) {
|
||||
const code = typeof payload?.error === "object" ? payload.error?.code : payload?.error;
|
||||
throw new ApiError(typeof code === "string" ? code : "request_failed", res.status, payload?.details, res.headers.get("retry-after"));
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
/** 公开项目标识,来自构建时NEXT_PUBLIC配置;不是密码或用户账户身份。 */
|
||||
export const PROJECT = process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap";
|
||||
/** 创建PKCE verifier和SHA-256 challenge:verifier留在当前标签页sessionStorage,
|
||||
* 只把challenge带到同源OAuth入口,后续交换流程负责取回verifier。此处不直接取得令牌。 */
|
||||
export async function startOAuth(provider: "google" | "slack") {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
// 32随机字节编码为无填充base64url,避免普通base64字符影响URL参数。
|
||||
const verifier = btoa(String.fromCharCode(...bytes))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
@@ -53,11 +70,14 @@ export async function startOAuth(provider: "google" | "slack") {
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
// 页面跳转前保存原verifier;使用单一固定键,同标签页后一次发起会替换上一次值。
|
||||
sessionStorage.setItem("foundry_verifier", verifier);
|
||||
window.location.assign("/api/oauth/" + provider + "?challenge=" + challenge);
|
||||
}
|
||||
/** 应用错误码→用户文案适配,不改变任务状态或执行恢复;未知错误给通用提示,不直接泄漏内部异常文本。 */
|
||||
export const errorText = (error: unknown) => {
|
||||
const code = error instanceof Error ? error.message : "";
|
||||
// 文案只解释既有领域状态;“保留/暂停”等实际行为须由触发错误的模块保证,不能由映射表证明。
|
||||
const messages: Record<string, string> = {
|
||||
invalid_model_stream: "画布返回的数据无法解析,请重新加载。",
|
||||
incomplete_model_stream: "画布数据未加载完整,请重新加载。",
|
||||
|
||||
Reference in New Issue
Block a user