后端:实现jwt,创建和检测 前端:重新定义能力和契约,不使用一键构建的前端框架,约束太多

This commit is contained in:
lsy 2024-11-16 23:03:55 +08:00
parent 88bf3b5592
commit 4b06234df8
35 changed files with 334 additions and 699 deletions

View File

@ -11,5 +11,8 @@ toml = "0.8.19"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres"] }
async-trait = "0.1.83"
anyhow = "1.0"
once_cell = "1.10.0"
once_cell = "1.10.0"
jwt-compact = { version = "0.8.0", features = ["ed25519-dalek"] }
ed25519-dalek = "2.1.1"
rand = "0.8.5"
chrono = "0.4"

View File

@ -32,6 +32,7 @@ impl Config {
/// 读取配置文件
pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
let path = env::current_dir()?
.join("assets")
.join("config.toml");
Ok(toml::from_str(&fs::read_to_string(path)?)?)
}

View File

@ -9,11 +9,15 @@
*/
mod config; // 配置模块
mod database; // 数据库模块
mod database;
mod secret;
use chrono::Duration;
// 数据库模块
use database::relational; // 引入关系型数据库
use once_cell::sync::Lazy; // 用于延迟初始化
use rocket::{get, http::Status, launch, response::status, routes, serde::json::Json}; // 引入Rocket框架相关功能
use std::sync::Arc; // 引入Arc用于线程安全的引用计数
use std::sync::Arc;
// 引入Arc用于线程安全的引用计数
use tokio::sync::Mutex; // 引入Mutex用于异步锁
// 全局数据库连接变量
@ -86,3 +90,24 @@ async fn rocket() -> _ {
.expect("Failed to connect to database"); // 初始化数据库连接
rocket::build().mount("/api", routes![install, ssql]) // 挂载API路由
}
// fn main(){
// // secret::generate_key().expect("msg");
// // 创建claims
// let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
// // 创建 Claims
// let claims = secret::CustomClaims {
// user_id: String::from("lsy"),
// device_ua: String::from("lsy"),
// };
// let t=String::from("eyJhbGciOiJFZERTQSJ9.eyJleHAiOjE3MzE3NTczMDMsIm5iZiI6MTczMTc1NzI4MywiaWF0IjoxNzMxNzU3MjgzLCJ1c2VyX2lkIjoibHN5IiwiZGV2aWNlX3VhIjoibHN5In0.C8t5XZFSKnnDVmc6WkY-gzGNSAP7lNAjP9yBjhdvIRO7r_QjDnfcm0INIqCt5cyvnRlE2rFJIx_axOfLx2QJAw");
// // 生成JWT
// let token = secret::generate_jwt(claims,Duration::seconds(20)).expect("msg");
// println!("{}", token);
// // 验证JWT
// let a=secret::validate_jwt(&t).expect("msg");
// println!("\n\n{}",a.user_id)
// }

119
backend/src/secret.rs Normal file
View File

@ -0,0 +1,119 @@
// File path: src/secret.rs
/**
* JWT的生成和验证功能
* JWTJWT的相关函数
*/
use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, Token, UntrustedToken, TimeOptions};
use serde::{Serialize, Deserialize};
use chrono::{Duration, Utc};
use ed25519_dalek::{SigningKey, VerifyingKey};
use std::fs::File;
use std::io::Write;
use std::{env, fs};
use std::error::Error;
use rand::{SeedableRng, RngCore};
// 定义JWT的Claims结构体有效载荷
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CustomClaims {
pub user_id: String, // 用户ID
pub device_ua: String, // 用户UA
}
pub enum SecretKey {
Signing, // 签名密钥
Verifying, // 验证密钥
}
impl SecretKey {
fn as_string(&self) -> String {
match self {
Self::Signing => String::from("signing"),
Self::Verifying => String::from("verifying"),
}
}
}
/**
*
*/
pub fn generate_key() -> Result<(), Box<dyn Error>> {
let mut csprng = rand::rngs::StdRng::from_entropy(); // 使用系统熵创建随机数生成器
let mut private_key_bytes = [0u8; 32]; // 存储签名密钥的字节数组
csprng.fill_bytes(&mut private_key_bytes); // 生成签名密钥
let signing_key = SigningKey::from_bytes(&private_key_bytes); // 从签名密钥获取SigningKey
let verifying_key = signing_key.verifying_key(); // 获取验证密钥
let base_path = env::current_dir()?
.join("assets")
.join("key");
fs::create_dir_all(&base_path)?; // 创建目录
File::create(base_path.join(SecretKey::Signing.as_string()))?
.write_all(signing_key.as_bytes())?; // 保存签名密钥
File::create(base_path.join(SecretKey::Verifying.as_string()))?
.write_all(verifying_key.as_bytes())?; // 保存验证密钥
Ok(())
}
/**
*
*/
pub fn get_key(key_type: SecretKey) -> Result<[u8; 32], Box<dyn Error>> {
let path = env::current_dir()?
.join("assets")
.join("key")
.join(key_type.as_string());
let key_bytes = fs::read(path)?; // 读取密钥文件
let mut key = [0u8; 32]; // 固定长度的数组
key.copy_from_slice(&key_bytes[..32]); // 拷贝前32个字节
Ok(key)
}
/**
* JWT
*/
pub fn generate_jwt(claims: CustomClaims, duration: Duration) -> Result<String, Box<dyn Error>> {
let key_bytes = get_key(SecretKey::Signing)?; // 从文件读取私钥
let signing_key = SigningKey::from_bytes(&key_bytes); // 创建SigningKey
let time_options = TimeOptions::new(
Duration::seconds(0), // 设置时间容差为0
Utc::now // 使用当前UTC时间作为时钟函数
); // 设置时间容差为); // 默认时间选项
let claims = jwt_compact::Claims::new(claims) // 创建JWT的有效载荷
.set_duration_and_issuance(&time_options, duration)
.set_not_before(Utc::now()); // 设置不早于时间
let header = Header::empty(); // 创建自定义的头部
let token = Ed25519.token(&header, &claims, &signing_key)?; // 使用Ed25519签名JWT
Ok(token)
}
/**
* JWT并返回自定义声明
*/
pub fn validate_jwt(token: &str) -> Result<CustomClaims, Box<dyn Error>> {
let key_bytes = get_key(SecretKey::Verifying)?; // 从文件读取验证密钥
let verifying = VerifyingKey::from_bytes(&key_bytes)?; // 创建VerifyingKey
let token = UntrustedToken::new(token)?; // 创建未受信任的Token
let token: Token<CustomClaims> = Ed25519.validator(&verifying).validate(&token)?; // 验证Token
let time_options = TimeOptions::new(
Duration::seconds(0), // 设置时间容差为0
Utc::now // 使用当前UTC时间作为时钟函数
); // 设置时间容差为); // 默认时间选项
token.claims()
.validate_expiration(&time_options)? // 验证过期时间
.validate_maturity(&time_options)?; // 验证成熟时间
let claims = token.claims().custom.clone(); // 获取自定义声明
Ok(claims)
}

View File

@ -1,2 +1,3 @@
VITE_SOME_KEY = 1
VITE_APP_SERVER = false
VITE_SERVER_STATUS = false
VITE_SERVER_ADDRESS = "localhost:8000"
VITE_SERVER_TOKEN = ""

View File

@ -1,84 +0,0 @@
/**
* 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,
},
},
],
};

View File

@ -1,25 +0,0 @@
/**
* File path: types/extensionType.ts
*
* ExtensionType
*
*
* - action: 可选的操作函数 void
* - component: 可选的组件函数 React
* - text: 可选的文本生成函数
*/
export class ExtensionProps {
/** 可选的操作函数,接受任意参数并返回 void */
action?: (...args: any[]) => void;
/** 可选的组件函数,接受任意参数并返回一个 React 组件 */
component?: (...args: any[]) => React.FC;
/** 可选的文本生成函数,接受任意参数并返回一个字符串 */
text?: (...args: any[]) => string;
}

View File

@ -1,34 +0,0 @@
// File path: types/templateType.ts
/**
*
*
*
*/
import React from "react";
import { ExtensionProps } from "types/extensionRequirement";
export interface TemplateConfig {
/**
*
*
*
*/
dependencies: Record<string, {
name: string; // 依赖字段的名称
description?: string; // 依赖字段的描述信息
required?: boolean; // 依赖字段是否必填
}>;
extensions?: Record<string, {
description?: string;
extension: ExtensionProps;
}>;
/**
*
*
* React
*/
page(params: Map<string, string>): React.FC;
}

View File

@ -1,18 +0,0 @@
/**
* 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,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

View File

@ -1,140 +0,0 @@
/**
* 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(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
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(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
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);
});
}

20
frontend/app/env.d.ts vendored
View File

@ -1,20 +0,0 @@
// File path: app/end.d.ts
/**
*
*/
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_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; // 存储静态资源的目录路径
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,36 +0,0 @@
import { Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import type { LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createContext, useContext, ReactNode } from 'react';
import { ServiceProvider } from "hooks/servicesProvider";
import "./tailwind.css";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta />
</head>
<body>
<ServiceProvider>
{children}
</ServiceProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return (
<Layout>
<Outlet />
</Layout>
);
}

View File

@ -1,138 +0,0 @@
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 (
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center gap-16">
<header className="flex flex-col items-center gap-9">
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
Welcome to <span className="sr-only">Remix</span>
</h1>
<div className="h-[144px] w-[434px]">
<img
src="/logo-light.png"
alt="Remix"
className="block w-full dark:hidden"
/>
<img
src="/logo-dark.png"
alt="Remix"
className="hidden w-full dark:block"
/>
</div>
</header>
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
<p className="leading-6 text-gray-700 dark:text-gray-200">
What&apos;s next?
</p>
<ul>
{resources.map(({ href, text, icon }) => (
<li key={href}>
<a
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
href={href}
target="_blank"
rel="noreferrer"
>
{icon}
{text}
</a>
</li>
))}
</ul>
</nav>
</div>
</div>
);
}
const resources = [
{
href: "https://remix.run/start/quickstart",
text: "Quick Start (5 min)",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M8.51851 12.0741L7.92592 18L15.6296 9.7037L11.4815 7.33333L12.0741 2L4.37036 10.2963L8.51851 12.0741Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
href: "https://remix.run/start/tutorial",
text: "Tutorial (30 min)",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M4.561 12.749L3.15503 14.1549M3.00811 8.99944H1.01978M3.15503 3.84489L4.561 5.2508M8.3107 1.70923L8.3107 3.69749M13.4655 3.84489L12.0595 5.2508M18.1868 17.0974L16.635 18.6491C16.4636 18.8205 16.1858 18.8205 16.0144 18.6491L13.568 16.2028C13.383 16.0178 13.0784 16.0347 12.915 16.239L11.2697 18.2956C11.047 18.5739 10.6029 18.4847 10.505 18.142L7.85215 8.85711C7.75756 8.52603 8.06365 8.21994 8.39472 8.31453L17.6796 10.9673C18.0223 11.0653 18.1115 11.5094 17.8332 11.7321L15.7766 13.3773C15.5723 13.5408 15.5554 13.8454 15.7404 14.0304L18.1868 16.4767C18.3582 16.6481 18.3582 16.926 18.1868 17.0974Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
href: "https://remix.run/docs",
text: "Remix Docs",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
),
},
{
href: "https://rmx.as/discord",
text: "Join Discord",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 24 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
strokeWidth="1.5"
/>
</svg>
),
},
];

View File

@ -1,12 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
@apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}

View File

@ -0,0 +1,29 @@
// src/contracts/CapabilityContract.ts
/**
*
*/
export interface CapabilityProps {
// 能力名称
name: string;
// 能力描述
description?: string;
// 能力版本
version: string;
// 能力参数定义
parameters?: {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
required?: boolean;
}>;
};
// 能力返回值定义
returns?: {
type: string;
description?: string;
};
// 能力执行函数
execute: (...args: any[]) => Promise<any>;
}

View File

@ -1,4 +1,4 @@
// File path: /d:/data/echoes/frontend/Requirements/generalRequirement.ts
// File path: contracts\generalContract.ts
/**
*
*

View File

@ -1,4 +1,4 @@
// File path: ../Requirements/pluginRequirement.ts
// File path: contracts\pluginContract.ts
/**
*
@ -6,10 +6,9 @@
*
*
*/
import { SerializeType } from "./generalRequirement";
import { ExtensionProps } from "types/extensionRequirement";
import { ExtensionService } from "service/extensionService";
import { useExtension } from "hooks/servicesProvider";
import { SerializeType } from "contracts/generalContract";
import { CapabilityProps } from "contracts/capabilityContract";
export interface PluginConfig {
name: string; // 插件名称
@ -21,16 +20,16 @@ export interface PluginConfig {
icon?: string; // 插件图标URL可选
managePath?: string; // 插件管理页面路径(可选)
configuration?: PluginConfiguration; // 插件配置
hooks?: {
onInstall?: (context: any) => {}; // 安装时调用的钩子(可选)
onUninstall?: (context: any) => {}; // 卸载时调用的钩子(可选)
onEnable?: (context: any) => {}; // 启用时调用的钩子(可选)
onDisable?: (context: any) => {}; // 禁用时调用的钩子(可选)
};
/** 能力 */
capabilities?: Set<CapabilityProps>;
routs: Set<{
description?: string; // 路由描述(可选)
path: string; // 路由路径
}>;
// 模块初始化函数
initialize?: () => Promise<void>;
// 模块销毁函数
destroy?: () => Promise<void>
}
/**
@ -46,14 +45,3 @@ export interface PluginConfiguration {
};
}
/**
*
*
*
*/
export class usePluginProps extends ExtensionProps {
}

View File

@ -0,0 +1,35 @@
export interface TemplateContract {
// 模板名称
name: string;
// 模板描述
description?: string;
// 模板配置
config: {
// 模板布局
layout?: string;
// 模板样式
styles?: string[];
// 模板脚本
scripts?: string[];
// 模板区域定义
zones?: Record<string, {
name: string;
description?: string;
allowedComponents?: string[];
}>;
};
// 模板数据契约
dataContract?: {
// 必需的数据字段
required: string[];
// 可选的数据字段
optional?: string[];
// 数据验证规则
validation?: Record<string, {
type: string;
rules: any[];
}>;
};
// 渲染函数
render: (props: any) => React.ReactNode;
}

View File

@ -1,4 +1,4 @@
// File path: types/themeType.ts
// File path: contracts\themeTypeContract.ts
/**
*
*
@ -8,7 +8,8 @@
*
*
*/
import { SerializeType } from "./generalRequirement";
import { CapabilityProps } from "contracts/capabilityContract";
import { SerializeType } from "contracts/generalContract";
export interface ThemeConfig {
name: string; // 主题的名称
displayName: string; // 主题的显示名称
@ -33,11 +34,9 @@ export interface ThemeConfig {
plugins?: string[]; // 主题所依赖的插件列表
assets?: string[]; // 主题所依赖的资源列表
};
/** 钩子 */
hooks?: {
onActivate?: () => {}; // 主题激活时执行的钩子
onDeactivate?: () => {}; // 主题停用时执行的钩子
};
/** 能力 */
capabilities?: Set<CapabilityProps>;
/** 路由 */
routes: {
index: string; // 首页使用的模板

View File

@ -1,5 +1,5 @@
import { ExtensionService } from 'service/extensionService';
import { ThemeService } from 'service/themeService';
import { ExtensionService } from 'services/extensionService';
import { ThemeService } from 'services/themeService';
import { createServiceContext } from './createServiceContext';
import { ReactNode } from 'react';

View File

@ -1,43 +1,23 @@
{
"name": "frontend",
"private": true,
"sideEffects": false,
"type": "module",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"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"
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@remix-run/node": "^2.14.0",
"@remix-run/react": "^2.14.0",
"@remix-run/serve": "^2.14.0",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@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"
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-router-dom": "^5.3.3",
"typescript": "^5.6.3"
}
}
}

View File

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,34 +0,0 @@
// File path: /service/pluginService.ts
/**
*
*
*/
import { PluginConfiguration } from "types/pluginRequirement";
export class PluginService {
/** 单例实例 */
private static pluginInstance: PluginService | null = null;
/** 插件组件缓存 */
private pluginComponents: Map<string, Set<{
name: string, // 插件名称
configuration?: PluginConfiguration, // 插件配置
managePath?: string, // 管理路径
}>> = new Map();
/**
*
*/
private constructor() {};
/**
*
* @returns {PluginService}
*/
public static getInstance(): PluginService {
if (!this.pluginInstance) {
this.pluginInstance = new PluginService();
}
return this.pluginInstance;
}
}

View File

@ -0,0 +1,64 @@
// src/core/PluginManager.ts
import { PluginConfiguration } from 'types/pluginRequirement';
import { Contracts } from 'contracts/capabilityContract';
export class PluginManager {
private plugins: Map<string, PluginProps> = new Map();
private configurations: Map<string, PluginConfiguration> = new Map();
private extensions: Map<string, ExtensionProps> = new Map();
async loadPlugins() {
// 扫描插件目录
const pluginDirs = await this.scanPluginDirectory();
for (const dir of pluginDirs) {
try {
const config = await import(`@/plugins/${dir}/plugin.config.ts`);
const plugin: PluginProps = config.default;
// 注册插件
this.plugins.set(plugin.name, plugin);
// 加载默认配置
if (plugin.settingsSchema) {
this.configurations.set(plugin.name, plugin.settingsSchema);
}
// 注册扩展
if (plugin.extensions) {
Object.entries(plugin.extensions).forEach(([key, value]) => {
this.extensions.set(`${plugin.name}.${key}`, value.extension);
});
}
// 执行安装钩子
if (plugin.hooks?.onInstall) {
await plugin.hooks.onInstall({});
}
} catch (error) {
console.error(`Failed to load plugin from directory ${dir}:`, error);
}
}
}
// 获取插件配置
async getPluginConfig(pluginName: string): Promise<PluginConfiguration | undefined> {
// 先尝试从数据库获取
const dbConfig = await this.fetchConfigFromDB(pluginName);
if (dbConfig) {
return dbConfig;
}
// 返回默认配置
return this.configurations.get(pluginName);
}
private async fetchConfigFromDB(pluginName: string) {
// 实现数据库查询逻辑
return null;
}
private async scanPluginDirectory(): Promise<string[]> {
// 实现插件目录扫描逻辑
return [];
}
}

View File

@ -1,5 +1,5 @@
// service/theme/themeService.ts
import type { ThemeConfig } from 'types/themeTypeRequirement';
import { ThemeConfig } from "contracts/themeContract"
export class ThemeService {
private static themeInstance: ThemeService; // 单例实例

0
frontend/src/a.ts Normal file
View File

View File

@ -1,22 +0,0 @@
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;

View File

@ -1,35 +1,19 @@
{
"include": [
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"compilerOptions": {
"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": {
"theme/*":["./theme/*"],
"types/*":["Requirements/*"],
"service/*":["./service/*"],
"hooks/*":["./hooks/*"],
},
// Vite takes care of building everything, not tsc.
"noEmit": true
}
"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
}

View File

@ -1,24 +0,0 @@
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(),
],
});