From f5754f982f9da662cae430b010d5aed9377ec508 Mon Sep 17 00:00:00 2001 From: lsy Date: Sun, 17 Nov 2024 17:17:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E4=BA=86=E8=83=BD=E5=8A=9B=E6=9C=8D=E5=8A=A1=E5=92=8Capi?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + .../database/relational/postgresql/init.sql | 4 +- backend/src/main.rs | 2 +- frontend/.env | 4 +- frontend/.eslintrc.cjs | 84 +++++++++++ frontend/app/entry.client.tsx | 18 +++ frontend/app/entry.server.tsx | 140 ++++++++++++++++++ frontend/app/env.d.ts | 22 +++ frontend/app/root.tsx | 45 ++++++ frontend/app/routes/_index.tsx | 138 +++++++++++++++++ frontend/app/tailwind.css | 12 ++ frontend/contracts/capabilityContract.ts | 32 +--- frontend/contracts/pluginContract.ts | 8 +- frontend/contracts/templateContract.ts | 23 +-- frontend/contracts/themeContract.ts | 11 +- frontend/hooks/servicesProvider.tsx | 41 ++--- frontend/package.json | 50 +++++-- frontend/postcss.config.js | 6 + frontend/services/apiService.ts | 118 +++++++++++++++ frontend/services/capabilityService.ts | 105 +++++++++++++ frontend/services/extensionService.ts | 94 ------------ frontend/src/a.ts | 0 frontend/tailwind.config.ts | 22 +++ frontend/tsconfig.json | 45 ++++-- frontend/vite.config.ts | 24 +++ 25 files changed, 843 insertions(+), 207 deletions(-) create mode 100644 README.md create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/app/entry.client.tsx create mode 100644 frontend/app/entry.server.tsx create mode 100644 frontend/app/env.d.ts create mode 100644 frontend/app/root.tsx create mode 100644 frontend/app/routes/_index.tsx create mode 100644 frontend/app/tailwind.css create mode 100644 frontend/postcss.config.js create mode 100644 frontend/services/apiService.ts create mode 100644 frontend/services/capabilityService.ts delete mode 100644 frontend/services/extensionService.ts delete mode 100644 frontend/src/a.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/vite.config.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..20aaf85 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +能力是主题和系统向插件暴露的接口,契约则规定了开发者在开发主题或插件时的限制和模板。 +系统使用 Remix,后端采用 Rust 的 Rocket。 \ No newline at end of file diff --git a/backend/src/database/relational/postgresql/init.sql b/backend/src/database/relational/postgresql/init.sql index 5ad6285..a526382 100644 --- a/backend/src/database/relational/postgresql/init.sql +++ b/backend/src/database/relational/postgresql/init.sql @@ -5,7 +5,7 @@ CREATE DATABASE echoes; --- 安装自动生成uuid插件 CREATE EXTENSION IF NOT EXISTS pgcrypto; --- 用户权限枚举 -CREATE TYPE privilege_level AS ENUM ('visitor', 'contributor', 'administrators'); +CREATE TYPE privilege_level AS ENUM ( 'contributor', 'administrators'); --- 用户信息表 CREATE TABLE persons ( @@ -18,7 +18,7 @@ CREATE TABLE persons person_avatar VARCHAR(255), --- 用户头像URL person_role VARCHAR(50), --- 用户角色 person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 最后登录时间 - person_level privilege_level NOT NULL --- 用户权限 + person_level privilege_level NOT NULL DEFULT 'contributor' --- 用户权限 ); --- 页面状态枚举 CREATE TYPE publication_status AS ENUM ('draft', 'published', 'private','hide'); diff --git a/backend/src/main.rs b/backend/src/main.rs index 1a1e27a..b995672 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -88,7 +88,7 @@ async fn rocket() -> _ { init_db(config.db_config) .await .expect("Failed to connect to database"); // 初始化数据库连接 - rocket::build().mount("/api", routes![install, ssql]) // 挂载API路由 + rocket::build().mount("/", routes![install, ssql]) // 挂载API路由 } diff --git a/frontend/.env b/frontend/.env index 5adf422..6d3542e 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1 @@ -VITE_SERVER_STATUS = false -VITE_SERVER_ADDRESS = "localhost:8000" -VITE_SERVER_TOKEN = "" \ No newline at end of file +VITE_API_BASE_URL = 1 \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..4f6f59e --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx new file mode 100644 index 0000000..94d5dc0 --- /dev/null +++ b/frontend/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/frontend/app/entry.server.tsx b/frontend/app/entry.server.tsx new file mode 100644 index 0000000..45db322 --- /dev/null +++ b/frontend/app/entry.server.tsx @@ -0,0 +1,140 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/frontend/app/env.d.ts b/frontend/app/env.d.ts new file mode 100644 index 0000000..37c0eb2 --- /dev/null +++ b/frontend/app/env.d.ts @@ -0,0 +1,22 @@ +// File path: app/end.d.ts + +/** + * 配置 + */ + +/// + +interface ImportMetaEnv { + readonly VITE_SERVER_API: string; // 用于访问API的基础URL + readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 + readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 + readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 + readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 + readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 + VITE_SYSTEM_USERNAME: string; // 前端账号名称 + VITE_SYSTEM_PASSWORD: string; // 前端账号密码 +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx new file mode 100644 index 0000000..61c8b98 --- /dev/null +++ b/frontend/app/root.tsx @@ -0,0 +1,45 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { LinksFunction } from "@remix-run/node"; + +import "./tailwind.css"; + +export const links: LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx new file mode 100644 index 0000000..13a5c00 --- /dev/null +++ b/frontend/app/routes/_index.tsx @@ -0,0 +1,138 @@ +import type { MetaFunction } from "@remix-run/node"; + +export const meta: MetaFunction = () => { + return [ + { title: "New Remix App" }, + { name: "description", content: "Welcome to Remix!" }, + ]; +}; + +export default function Index() { + return ( +
+
+
+

+ Welcome to Remix +

+
+ Remix + Remix +
+
+ +
+
+ ); +} + +const resources = [ + { + href: "https://remix.run/start/quickstart", + text: "Quick Start (5 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/start/tutorial", + text: "Tutorial (30 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/docs", + text: "Remix Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/frontend/app/tailwind.css b/frontend/app/tailwind.css new file mode 100644 index 0000000..303fe15 --- /dev/null +++ b/frontend/app/tailwind.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/frontend/contracts/capabilityContract.ts b/frontend/contracts/capabilityContract.ts index ce0aa3b..7d95372 100644 --- a/frontend/contracts/capabilityContract.ts +++ b/frontend/contracts/capabilityContract.ts @@ -2,28 +2,12 @@ /** * 能力契约接口 */ -export interface CapabilityProps { - // 能力名称 - name: string; - // 能力描述 - description?: string; - // 能力版本 - version: string; - // 能力参数定义 - parameters?: { - type: 'object'; - properties: Record; - }; - // 能力返回值定义 - returns?: { - type: string; - description?: string; - }; - // 能力执行函数 - execute: (...args: any[]) => Promise; - } +export interface CapabilityProps { + // 能力名称 + name: string; + // 能力描述 + description?: string; + // 能力执行函数 + execute: (...args: any[]) => Promise; +} \ No newline at end of file diff --git a/frontend/contracts/pluginContract.ts b/frontend/contracts/pluginContract.ts index 85c37a9..f55474d 100644 --- a/frontend/contracts/pluginContract.ts +++ b/frontend/contracts/pluginContract.ts @@ -20,16 +20,12 @@ export interface PluginConfig { icon?: string; // 插件图标URL(可选) managePath?: string; // 插件管理页面路径(可选) configuration?: PluginConfiguration; // 插件配置 - /** 能力 */ - capabilities?: Set; + /** 声明需要使用的能力,没有实际作用 */ + capabilities?: Set>; routs: Set<{ description?: string; // 路由描述(可选) path: string; // 路由路径 }>; - // 模块初始化函数 - initialize?: () => Promise; - // 模块销毁函数 - destroy?: () => Promise } /** diff --git a/frontend/contracts/templateContract.ts b/frontend/contracts/templateContract.ts index e5457ae..e8cd1d5 100644 --- a/frontend/contracts/templateContract.ts +++ b/frontend/contracts/templateContract.ts @@ -1,3 +1,5 @@ +import { CapabilityProps } from "contracts/capabilityContract"; + export interface TemplateContract { // 模板名称 name: string; @@ -11,25 +13,10 @@ export interface TemplateContract { styles?: string[]; // 模板脚本 scripts?: string[]; - // 模板区域定义 - zones?: Record; - }; - // 模板数据契约 - dataContract?: { - // 必需的数据字段 - required: string[]; - // 可选的数据字段 - optional?: string[]; - // 数据验证规则 - validation?: Record; }; + /** 声明需要使用的能力,没有实际作用 */ + capabilities?: Set>; + // 渲染函数 render: (props: any) => React.ReactNode; } \ No newline at end of file diff --git a/frontend/contracts/themeContract.ts b/frontend/contracts/themeContract.ts index 598bac8..d9839f8 100644 --- a/frontend/contracts/themeContract.ts +++ b/frontend/contracts/themeContract.ts @@ -8,7 +8,6 @@ * 主题配置接口 * 定义主题的基本信息、模板、全局配置、依赖、钩子和路由。 */ -import { CapabilityProps } from "contracts/capabilityContract"; import { SerializeType } from "contracts/generalContract"; export interface ThemeConfig { name: string; // 主题的名称 @@ -16,7 +15,7 @@ export interface ThemeConfig { version: string; // 主题的版本号 description?: string; // 主题的描述信息 author?: string; // 主题的作者信息 - entry?: string; // 主题的入口路径 + entry: string; // 主题的入口路径 templates: Map; // 主题模板的映射表 /** 主题全局配置 */ globalSettings?: { @@ -29,14 +28,6 @@ export interface ThemeConfig { description?: string; // 属性的描述信息 data: SerializeType; // 属性的默认数据 }>; - /** 依赖 */ - dependencies?: { - plugins?: string[]; // 主题所依赖的插件列表 - assets?: string[]; // 主题所依赖的资源列表 - }; - /** 能力 */ - capabilities?: Set; - /** 路由 */ routes: { index: string; // 首页使用的模板 diff --git a/frontend/hooks/servicesProvider.tsx b/frontend/hooks/servicesProvider.tsx index 9fc38e5..9da328f 100644 --- a/frontend/hooks/servicesProvider.tsx +++ b/frontend/hooks/servicesProvider.tsx @@ -1,22 +1,25 @@ -import { ExtensionService } from 'services/extensionService'; -import { ThemeService } from 'services/themeService'; -import { createServiceContext } from './createServiceContext'; -import { ReactNode } from 'react'; +import { CapabilityService } from "services/capabilityService"; +import { ThemeService } from "services/themeService"; +import { createServiceContext } from "./createServiceContext"; +import { ReactNode } from "react"; -export const { - ExtensionProvider, - useExtension -} = createServiceContext('Extension', () => ExtensionService.getInstance()); +export const { ExtensionProvider, useExtension } = createServiceContext( + "Extension", + () => CapabilityService.getInstance(), +); -export const { - ThemeProvider, - useTheme -} = createServiceContext("Theme", () => ThemeService.getInstance()); +export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => + ThemeService.getInstance(), +); -export const ServiceProvider = ({ children }: { children: ReactNode })=>( - - - {children} - - -); \ No newline at end of file +// File path:hooks/servicesProvider.tsx +/** + * ServiceProvider 组件用于提供扩展和主题上下文给其子组件。 + * + * @param children - 要渲染的子组件。 + */ +export const ServiceProvider = ({ children }: { children: ReactNode }) => ( + + {children} + +); diff --git a/frontend/package.json b/frontend/package.json index fe9796d..8bcbf7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,23 +1,45 @@ { "name": "frontend", - "version": "1.0.0", - "main": "index.js", + "private": true, + "sideEffects": false, + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc" }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0" + "@remix-run/node": "^2.14.0", + "@remix-run/react": "^2.14.0", + "@remix-run/serve": "^2.14.0", + "@types/axios": "^0.14.4", + "axios": "^1.7.7", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@types/react-router-dom": "^5.3.3", - "typescript": "^5.6.3" + "@remix-run/dev": "^2.14.0", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts new file mode 100644 index 0000000..0695a48 --- /dev/null +++ b/frontend/services/apiService.ts @@ -0,0 +1,118 @@ +// File path: /d:/data/echoes/frontend/services/apiService.ts + +/** + * ApiConfig接口用于配置API服务的基本信息。 + */ +interface ApiConfig { + baseURL: string; // API的基础URL + timeout?: number; // 请求超时时间(可选) +} + +export class ApiService { + private static instance: ApiService; // ApiService的单例实例 + private baseURL: string; // API的基础URL + private timeout: number; // 请求超时时间 + + /** + * 构造函数用于初始化ApiService实例。 + * @param config ApiConfig配置对象 + */ + private constructor(config: ApiConfig) { + this.baseURL = config.baseURL; + this.timeout = config.timeout || 10000; // 默认超时时间为10000毫秒 + } + + /** + * 获取ApiService的单例实例。 + * @param config 可选的ApiConfig配置对象 + * @returns ApiService实例 + */ + public static getInstance(config?: ApiConfig): ApiService { + if (!this.instance && config) { + this.instance = new ApiService(config); + } + return this.instance; + } + + /** + * 获取系统令牌。 + * @returns Promise 返回系统令牌 + * @throws Error 如果未找到凭据或请求失败 + */ + private async getSystemToken(): Promise { + const credentials = localStorage.getItem('system_credentials'); + if (!credentials) { + throw new Error('System credentials not found'); + } + + try { + const response = await fetch(`${this.baseURL}/auth/system`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(JSON.parse(credentials)), + }); + + if (!response.ok) { + throw new Error('Failed to get system token'); + } + + const { token } = await response.json(); + return token; + } catch (error) { + console.error('Error getting system token:', error); + throw error; + } + } + + /** + * 发起API请求。 + * @param endpoint 请求的API端点 + * @param options 请求选项 + * @param requiresAuth 是否需要身份验证(默认为true) + * @returns Promise 返回API响应数据 + * @throws Error 如果请求超时或发生其他错误 + */ + public async request( + endpoint: string, + options: RequestInit = {}, + requiresAuth = true + ): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + const headers = new Headers(options.headers); + + if (requiresAuth) { + const token = await this.getSystemToken(); + headers.set('Authorization', `Bearer ${token}`); + } + + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...options, + headers, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.statusText}`); + } + + const data = await response.json(); + return data as T; + } catch (error: any) { + if (error.name === 'AbortError') { + throw new Error('Request timeout'); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } +} + +export default ApiService.getInstance({ + baseURL: import.meta.env.VITE_API_BASE_URL, +}); \ No newline at end of file diff --git a/frontend/services/capabilityService.ts b/frontend/services/capabilityService.ts new file mode 100644 index 0000000..3b5e2c5 --- /dev/null +++ b/frontend/services/capabilityService.ts @@ -0,0 +1,105 @@ +// File path: services/capabilityService.ts + +/** + * CapabilityService 是一个单例类,用于管理能力的实例。 + * 提供注册、执行和移除能力的功能。 + */ +import { CapabilityProps } from "contracts/capabilityContract"; + +export class CapabilityService { + // 存储能力的映射,键为能力名称,值为能力源和能力属性的集合 + private capabilities: Map}>> = new Map(); + + // CapabilityService 的唯一实例 + private static instance: CapabilityService; + + /** + * 私有构造函数,防止外部实例化 + */ + private constructor() { } + + /** + * 获取 CapabilityService 的唯一实例。 + * @returns {CapabilityService} 返回 CapabilityService 的唯一实例。 + */ + public static getInstance(): CapabilityService { + if (!this.instance) { + this.instance = new CapabilityService(); + } + return this.instance; + } + + /** + * 注册能力 + * @param capabilityName 能力名称 + * @param source 能力来源 + * @param capability 能力属性 + */ + private register(capabilityName: string, source: string, capability: CapabilityProps) { + const handlers = this.capabilities.get(capabilityName) || new Set(); + handlers.add({ source, capability }); + } + + /** + * 执行指定能力的方法 + * @param capabilityName 能力名称 + * @param args 方法参数 + * @returns {Set} 执行结果的集合 + */ + private executeCapabilityMethod(capabilityName: string, ...args: any[]): Set { + const results = new Set(); + const handlers = this.capabilities.get(capabilityName); + + if (handlers) { + handlers.forEach(({ capability }) => { + const methodFunction = capability['execute']; + if (methodFunction) { + methodFunction(...args) + .then((data) => results.add(data as T)) + .catch((error) => console.error(`Error executing method ${capabilityName}:`, error)); + } + }); + } + return results; + } + + /** + * 移除指定来源的能力 + * @param source 能力来源 + */ + private removeCapability(source: string) { + this.capabilities.forEach((capability_s, capabilityName) => { + const newHandlers = new Set( + Array.from(capability_s).filter(capability => capability.source !== source) + ); + this.capabilities.set(capabilityName, newHandlers); + }); + } + + /** + * 移除指定能力 + * @param capability 能力名称 + */ + private removeCapabilitys(capability: string) { + this.capabilities.delete(capability); + } + + public validateCapability(capability: CapabilityProps): boolean { + // 验证能力是否符合基本要求 + if (!capability.name || !capability.execute) { + return false; + } + + // 验证能力名称格式 + const namePattern = /^[a-z][a-zA-Z0-9_]*$/; + if (!namePattern.test(capability.name)) { + return false; + } + + return true; + } + + +} diff --git a/frontend/services/extensionService.ts b/frontend/services/extensionService.ts deleted file mode 100644 index fd4f3f7..0000000 --- a/frontend/services/extensionService.ts +++ /dev/null @@ -1,94 +0,0 @@ -// File path: service/extensionService.ts - -/** - * ExtensionManage 是一个单例类,用于管理扩展的实例。 - * 提供注册、触发和移除插件扩展的功能。 - */ -import { ExtensionProps } from "types/extensionRequirement"; -import React from "react"; -import { PluginConfiguration } from "types/pluginRequirement"; - -export class ExtensionService { - /** 存储扩展的映射,键为扩展名称,值为插件名称和扩展的集合 */ - private extensions: Map> = new Map(); - private configuration: Map = new Map(); - /** ExtensionManage 的唯一实例 */ - private static instance: ExtensionService; - - /** 私有构造函数,防止外部实例化 */ - private constructor() { } - - /** - * 获取 ExtensionManage 的唯一实例。 - * @returns {ExtensionManage} 返回 ExtensionManage 的唯一实例。 - */ - public static getInstance(): ExtensionService { - if (!this.instance) { - this.instance = new ExtensionService(); - } - return this.instance; - } - - /** 注册扩展 */ - private register(extensionName: string, pluginName: string, extension: ExtensionProps, pluginConfiguration: PluginConfiguration) { - const handlers = this.extensions.get(extensionName) || new Set(); - this.configuration.has(extensionName) || this.configuration.set(pluginName, pluginConfiguration); - - handlers.add({ pluginName, extension }); - this.extensions.set(extensionName, handlers); - } - - /** 执行扩展方法 */ - private executeExtensionMethod(extensionName: string, method: keyof ExtensionProps, ...args: any[]): Set { - const result = new Set(); - const handlers = this.extensions.get(extensionName); - - if (handlers) { - handlers.forEach(({ extension }) => { - const methodFunction = extension[method]; - if (methodFunction) { - try { - const value = methodFunction(...args); - if (value && (typeof value === 'string' || React.isValidElement(value))) { - result.add(value as T); - } - } catch (error) { - console.error(`Error executing hook ${extensionName}:`, error); - } - } - }); - } - return result; - } - - /** 触发扩展的动作 */ - private triggerAction(extensionName: string, ...args: any[]): void { - this.executeExtensionMethod(extensionName, 'action', ...args); - } - - /** 触发扩展的组件 */ - private triggerComponent(extensionName: string, ...args: any[]): Set { - return this.executeExtensionMethod(extensionName, 'component', ...args); - } - - /** 触发扩展的文本 */ - private triggerText(extensionName: string, ...args: any[]): Set { - return this.executeExtensionMethod(extensionName, 'text', ...args); - } - - /** 移除指定插件的扩展 */ - private removePluginExtensions(pluginName: string) { - this.extensions.forEach((handlers, extensionName) => { - const newHandlers = new Set( - Array.from(handlers).filter(handler => handler.pluginName !== pluginName) - ); - this.extensions.set(extensionName, newHandlers); - }); - this.configuration.delete(pluginName); - } - - //获取指定配置文件 - private getConfiguration(pluginName: string): PluginConfiguration | undefined { - return this.configuration.get(pluginName); - } -} diff --git a/frontend/src/a.ts b/frontend/src/a.ts deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..5f06ad4 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from "tailwindcss"; + +export default { + content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + fontFamily: { + sans: [ + "Inter", + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", + ], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index f3107c8..9d87dd3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,19 +1,32 @@ { + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], "compilerOptions": { - "target": "es6", // 编译到 ES5 - "lib": ["dom", "dom.iterable", "esnext"], // 指定要编译的库 - "allowJs": true, // 允许 JavaScript 文件 - "skipLibCheck": true, // 跳过库检查 - "esModuleInterop": true, // 支持 CommonJS 模块 - "allowSyntheticDefaultImports": true, // 允许导入默认值 - "strict": true, // 启用严格模式 - "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致 - "module": "esnext", // 使用 ES 模块 - "moduleResolution": "node", // 模块解析策略 - "resolveJsonModule": true, // 允许导入 JSON 模块 - "isolatedModules": true, // 每个文件单独编译 - "noEmit": true // 不输出任何文件,只检查类型 - }, - "include": ["src/**/*"], // 包含 src 文件夹中的所有 TypeScript 文件 - "exclude": ["node_modules"] // 排除 node_modules 文件夹 + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e4e8cef --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,24 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +declare module "@remix-run/node" { + interface Future { + v3_singleFetch: true; + } +} + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + }), + tsconfigPaths(), + ], +});