feat: remove Learning module
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Bot, Code2, Paintbrush, Sigma } from 'lucide-react';
|
||||
import { Bot, Code2, Paintbrush } from 'lucide-react';
|
||||
import type { ModuleAccess, ModuleAccessKey } from '../../shared/module-access';
|
||||
|
||||
export const AI_MODULE_SELECTION_PATH = '/module-select';
|
||||
|
||||
export type AiModuleId = 'programming' | 'painting' | 'learning' | 'robot';
|
||||
export type AiModuleId = 'programming' | 'painting' | 'robot';
|
||||
|
||||
export type AiModuleDefinition = {
|
||||
id: AiModuleId;
|
||||
@@ -36,15 +36,6 @@ export const aiModules: readonly AiModuleDefinition[] = [
|
||||
enabled: true,
|
||||
Icon: Paintbrush,
|
||||
},
|
||||
{
|
||||
id: 'learning',
|
||||
title: 'Makelore Learning',
|
||||
subtitle: 'AI 学习',
|
||||
description: '用顶尖的方法解锁万物规律',
|
||||
route: '/learning',
|
||||
enabled: true,
|
||||
Icon: Sigma,
|
||||
},
|
||||
{
|
||||
id: 'robot',
|
||||
title: 'Makelore Robot',
|
||||
@@ -60,7 +51,6 @@ export const aiModules: readonly AiModuleDefinition[] = [
|
||||
const moduleAccessKeyById: Record<AiModuleId, ModuleAccessKey> = {
|
||||
programming: 'programming',
|
||||
painting: 'design',
|
||||
learning: 'learning',
|
||||
robot: 'robot',
|
||||
};
|
||||
|
||||
@@ -113,9 +103,6 @@ export function getGuardedAiModuleForPath(pathname: string): AiModuleId | null {
|
||||
if (pathname === '/ai-hardware' || pathname.startsWith('/ai-hardware/')) {
|
||||
return 'robot';
|
||||
}
|
||||
if (pathname === '/learning' || pathname.startsWith('/learning/')) {
|
||||
return 'learning';
|
||||
}
|
||||
if (PROGRAMMING_ROUTE_PREFIXES.some((route) => matchesRoute(pathname, route))) {
|
||||
return 'programming';
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function ensureHostApiToken(): Promise<string> {
|
||||
return token;
|
||||
}
|
||||
|
||||
export type DesktopActivityModule = 'programming' | 'painting' | 'learning' | 'robot' | 'other';
|
||||
export type DesktopActivityModule = 'programming' | 'painting' | 'robot' | 'other';
|
||||
|
||||
export function reportDesktopActivity(input: {
|
||||
visible: boolean;
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { AppError } from '@/lib/error-model';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import {
|
||||
LEARNING_API_PATH,
|
||||
LEARNING_MEDIA_MAX_BYTES,
|
||||
type LearningProjectDetail,
|
||||
type LearningProjectDownloadResult,
|
||||
type LearningProjectListQuery,
|
||||
type LearningProjectPage,
|
||||
} from '../../shared/learning';
|
||||
|
||||
type LearningEnvelope<T> = {
|
||||
success?: boolean;
|
||||
status?: number;
|
||||
code?: string;
|
||||
error?: string;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
type LearningProjectMedia = {
|
||||
dataBase64: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
const PROJECT_MEDIA_URL_PATTERN = /^\/api\/learning\/projects\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/media\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
const PROJECT_MEDIA_MIME_TYPES = new Set([
|
||||
'image/avif',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
]);
|
||||
const MAX_MEDIA_BASE64_LENGTH = Math.ceil(LEARNING_MEDIA_MAX_BYTES / 3) * 4;
|
||||
|
||||
export class LearningApiError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'LearningApiError';
|
||||
}
|
||||
}
|
||||
|
||||
function errorStatus(error: unknown): number {
|
||||
return error instanceof AppError && typeof error.details?.status === 'number'
|
||||
? error.details.status
|
||||
: 502;
|
||||
}
|
||||
|
||||
function errorCode(error: unknown, status: number): string {
|
||||
if (error instanceof AppError && typeof error.details?.backendCode === 'string') {
|
||||
return error.details.backendCode;
|
||||
}
|
||||
if (status === 401) return 'LEARNING_AUTH_REQUIRED';
|
||||
if (status === 404) return 'LEARNING_PROJECT_NOT_FOUND';
|
||||
return 'LEARNING_REQUEST_FAILED';
|
||||
}
|
||||
|
||||
async function requestData<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
let response: LearningEnvelope<T>;
|
||||
try {
|
||||
response = await hostApiFetch<LearningEnvelope<T>>(path, init);
|
||||
} catch (error) {
|
||||
const status = errorStatus(error);
|
||||
throw new LearningApiError(
|
||||
status,
|
||||
errorCode(error, status),
|
||||
error instanceof Error ? error.message : '学习项目请求失败',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.success || response.data === undefined) {
|
||||
throw new LearningApiError(
|
||||
response.status ?? 502,
|
||||
response.code ?? 'LEARNING_REQUEST_FAILED',
|
||||
response.error ?? '学习项目请求失败',
|
||||
);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function listQuery(query: LearningProjectListQuery): string {
|
||||
const params = new URLSearchParams();
|
||||
if (query.cursor?.trim()) params.set('cursor', query.cursor.trim());
|
||||
if (query.limit !== undefined) params.set('limit', String(query.limit));
|
||||
const encoded = params.toString();
|
||||
return encoded ? `?${encoded}` : '';
|
||||
}
|
||||
|
||||
export function fetchLearningProjects(
|
||||
query: LearningProjectListQuery = {},
|
||||
): Promise<LearningProjectPage> {
|
||||
return requestData(`${LEARNING_API_PATH}/projects${listQuery(query)}`);
|
||||
}
|
||||
|
||||
export function fetchLearningProject(projectId: string): Promise<LearningProjectDetail> {
|
||||
return requestData(`${LEARNING_API_PATH}/projects/${encodeURIComponent(projectId)}`);
|
||||
}
|
||||
|
||||
export function downloadLearningProject(
|
||||
projectId: string,
|
||||
): Promise<LearningProjectDownloadResult> {
|
||||
return requestData(`${LEARNING_API_PATH}/projects/${encodeURIComponent(projectId)}/download`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchLearningProjectMedia(mediaUrl: string): Promise<string> {
|
||||
if (!PROJECT_MEDIA_URL_PATTERN.test(mediaUrl)) {
|
||||
throw new LearningApiError(400, 'LEARNING_INVALID_MEDIA_URL', '项目图片地址无效');
|
||||
}
|
||||
const localPath = mediaUrl.replace('/api/learning/', `${LEARNING_API_PATH}/`);
|
||||
const payload = await hostApiFetch<LearningProjectMedia>(localPath);
|
||||
const mimeType = typeof payload?.mimeType === 'string' ? payload.mimeType.trim().toLowerCase() : '';
|
||||
const dataBase64 = typeof payload?.dataBase64 === 'string' ? payload.dataBase64 : '';
|
||||
if (
|
||||
!PROJECT_MEDIA_MIME_TYPES.has(mimeType)
|
||||
|| !dataBase64
|
||||
|| dataBase64.length > MAX_MEDIA_BASE64_LENGTH
|
||||
|| !/^[A-Za-z0-9+/]*={0,2}$/.test(dataBase64)
|
||||
|| dataBase64.length % 4 === 1
|
||||
) {
|
||||
throw new LearningApiError(502, 'LEARNING_INVALID_MEDIA_RESPONSE', '项目图片返回了无效数据');
|
||||
}
|
||||
return `data:${mimeType};base64,${dataBase64}`;
|
||||
}
|
||||
|
||||
export function isLearningProjectMediaUrl(value: string): boolean {
|
||||
return PROJECT_MEDIA_URL_PATTERN.test(value);
|
||||
}
|
||||
|
||||
export function isSafeLearningImageUrl(value: string): boolean {
|
||||
if (PROJECT_MEDIA_URL_PATTERN.test(value)) return true;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'https:' && !url.username && !url.password;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openLearningExternalLink(value: string): Promise<void> {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new LearningApiError(400, 'LEARNING_INVALID_LINK', '项目链接无效');
|
||||
}
|
||||
if (url.protocol !== 'https:' || url.username || url.password) {
|
||||
throw new LearningApiError(400, 'LEARNING_INVALID_LINK', '项目链接无效');
|
||||
}
|
||||
await invokeIpc('shell:openExternal', url.toString());
|
||||
}
|
||||
Reference in New Issue
Block a user