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
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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(),
+ ],
+});