From 9e6324606c8ff0304ff2f3bf724f8481cb1508a0 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 14 Apr 2026 23:25:15 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=8C=E6=90=AD=E5=BB=BAGo=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E3=80=81React=E5=89=8D=E7=AB=AF=E4=B8=8ENeutralino=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E5=A3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 75 ++++++++ AGENTS.md | 1 + CLAUDE.md | 1 + README.md | 256 ++++++++++++++++++++++++++ backend/build.cmd | 8 + backend/build.sh | 8 + backend/cmd/nex/main.go | 40 ++++ backend/go.mod | 10 + backend/go.sum | 6 + backend/internal/neutralino/client.go | 161 ++++++++++++++++ frontend/biome.json | 25 +++ frontend/eslint.config.js | 23 +++ frontend/index.html | 13 ++ frontend/package.json | 36 ++++ frontend/public/favicon.svg | 1 + frontend/src/App.tsx | 5 + frontend/src/components/index.ts | 1 + frontend/src/hooks/index.ts | 1 + frontend/src/main.tsx | 14 ++ frontend/src/pages/index.ts | 1 + frontend/src/styles/global.scss | 25 +++ frontend/src/types/index.ts | 1 + frontend/src/utils/index.ts | 1 + frontend/tsconfig.json | 27 +++ frontend/vite.config.ts | 19 ++ neutralino.config.json | 89 +++++++++ openspec/config.yaml | 14 ++ 27 files changed, 862 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 backend/build.cmd create mode 100644 backend/build.sh create mode 100644 backend/cmd/nex/main.go create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/neutralino/client.go create mode 100644 frontend/biome.json create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/src/hooks/index.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/index.ts create mode 100644 frontend/src/styles/global.scss create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 neutralino.config.json create mode 100644 openspec/config.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2032bc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Go +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.prof +go.work +go.work.sum + +# Bun +bun.lock + +# Node +**/node_modules/ + +# Vite +frontend/dist/ +frontend/.vite/ + +# Neutralino +.tmp/ +bin/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes +Icon? + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +Desktop.ini +$RECYCLE.BIN/ + +# Linux +*~ +.directory +.Trash-* + +# VSCode +.vscode/ +*.code-workspace +.history/ + +# IDE +.idea/ +*.swp +*.swo +*~ + +# Env +.env +.env.* +!.env.example + +# Claude Code +.claude/ + +# opencode +.opencode/ + +# openspec +openspec/changes/archive + +# Custom +temp diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..874fa8e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +严格遵守openspec/config.yaml中context声明的项目规范 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..874fa8e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +严格遵守openspec/config.yaml中context声明的项目规范 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be28f93 --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# Nex + +基于 [NeutralinoJS](https://neutralino.js.org) 的轻量级桌面应用框架,前端使用 React + TypeScript + Vite,后端使用 Go 作为 Native Extension。 + +## 项目结构 + +``` +nex/ +├── neutralino.config.json # Neutralino 主配置文件 +├── README.md +├── bin/ # Neutralino 运行时二进制文件(各平台) +│ ├── neutralino-win_x64.exe +│ ├── neutralino-linux_x64 +│ ├── neutralino-linux_arm64 +│ ├── neutralino-linux_armhf +│ ├── neutralino-mac_x64 +│ ├── neutralino-mac_arm64 +│ └── neutralino-mac_universal +├── frontend/ # 前端项目(React + TypeScript + Vite) +│ ├── package.json +│ ├── index.html # Vite 入口 HTML +│ ├── vite.config.ts +│ ├── tsconfig.json +│ ├── biome.json # Biome 格式化配置 +│ ├── eslint.config.js # ESLint 配置 +│ ├── public/ # 静态资源 +│ │ └── favicon.svg +│ ├── dist/ # 构建产物(由 vite build 生成) +│ └── src/ +│ ├── main.tsx # 应用入口,初始化 React 和 Neutralino +│ ├── App.tsx # 根组件 +│ ├── components/ # 可复用 UI 组件 +│ ├── pages/ # 页面组件 +│ ├── hooks/ # 自定义 React Hooks +│ ├── utils/ # 工具函数 +│ ├── types/ # TypeScript 类型定义 +│ └── styles/ +│ └── global.scss # 全局样式 +└── backend/ # 后端项目(Go Native Extension) + ├── go.mod / go.sum # Go 模块依赖 + ├── build.sh # macOS / Linux 构建脚本 + ├── build.cmd # Windows 构建脚本 + ├── cmd/ + │ └── nex/ + │ └── main.go # Go 扩展入口,定义事件处理与业务逻辑 + └── internal/ + └── neutralino/ + └── client.go # Neutralino WebSocket 客户端通信库 +``` + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 桌面框架 | NeutralinoJS 6.7.0 | +| 前端 | React 19 + TypeScript 6 | +| 构建工具 | Vite 8 + Bun | +| 样式 | SCSS | +| 代码检查 | ESLint + Biome | +| 后端 | Go 1.22 | +| 后端通信 | WebSocket (gorilla/websocket) | + +## 架构说明 + +本项目采用 Neutralino Extension 架构,前后端通过 WebSocket 进行异步通信: + +``` +┌─────────────────┐ WebSocket ┌──────────────┐ +│ React Frontend │ ◄────────────► │ Go Backend │ +│ (浏览器渲染) │ │ (Extension) │ +└─────────────────┘ └──────────────┘ + │ │ + @neutralinojs/lib internal/neutralino + (JS API 调用) (WSClient) +``` + +- **前端** 通过 `@neutralinojs/lib` 的 `init()` 初始化 Neutralino 运行时,使用 `Neutralino.events` 发送和监听事件。 +- **后端** 作为 Neutralino Extension 启动,通过 WebSocket 与 Neutralino 核心通信,接收前端事件并响应。 +- 所有事件通信均为异步,事件队列保证不丢失。 + +## 快速开始 + +### 环境要求 + +- [Go](https://go.dev/dl/) >= 1.22 +- [Bun](https://bun.sh/)(推荐)或 Node.js >= 18 +- [Neutralino CLI](https://neutralino.js.org/docs/cli/neu-cli/):`npm install -g @neutralinojs/neu` + +### 1. 安装前端依赖 + +```bash +cd frontend +bun install +``` + +### 2. 编译 Go 后端 + +```bash +cd backend + +# macOS / Linux +./build.sh + +# Windows +build.cmd +``` + +构建产物 `nex`(或 `nex.exe`)输出到 `backend/` 目录下。 + +### 3. 安装 Neutralino 二进制 + +```bash +neu update +``` + +### 4. 开发模式运行 + +```bash +neu run +``` + +此命令会启动 Vite 开发服务器(端口 5173)和 Neutralino 窗口,前端改动自动热更新。 + +### 5. 构建生产包 + +```bash +# 构建前端 +cd frontend && bun run build + +# 构建 Neutralino 应用 +neu build +``` + +## 前端开发指引 + +### 路径别名 + +项目配置了 `@` 指向 `src/` 目录,在导入时可直接使用: + +```ts +import App from "@/App.tsx"; +import "@/styles/global.scss"; +``` + +### 目录规范 + +| 目录 | 用途 | +|------|------| +| `src/components/` | 可复用 UI 组件,通过 `index.ts` 统一导出 | +| `src/pages/` | 页面级组件 | +| `src/hooks/` | 自定义 React Hooks | +| `src/utils/` | 工具函数 | +| `src/types/` | TypeScript 类型定义 | +| `src/styles/` | 全局及局部样式(SCSS) | + +### 代码质量工具 + +```bash +# ESLint 检查 +bun run lint + +# ESLint 自动修复 +bun run lint:fix + +# Biome 格式化检查 +bun run format:check + +# Biome 格式化(写入) +bun run format + +# TypeScript 类型检查 +bun run typecheck +``` + +### 调用 Go 后端 + +前端通过 Neutralino 事件系统与 Go 后端通信: + +```ts +// 发送事件到 Go 后端 +Neutralino.events.dispatch("runGo", { + function: "ping", + parameter: "Hello from Frontend", +}); + +// 监听 Go 后端返回的事件 +Neutralino.events.on("pingResult", (event) => { + console.log(event.detail); +}); +``` + +## 后端开发指引 + +### 目录结构 + +``` +backend/ +├── cmd/nex/main.go # 入口,注册事件回调 +├── internal/neutralino/client.go # WSClient 通信库 +├── build.sh / build.cmd # 构建脚本 +└── go.mod / go.sum # 依赖管理 +``` + +### 添加新的 Go 函数 + +在 `cmd/nex/main.go` 的 `processAppEvent` 回调中添加新的函数分支: + +```go +func processAppEvent(data neutralino.EventMessage) { + if ext.IsEvent(data, "runGo") { + if d, ok := data.Data.(map[string]interface{}); ok { + if d["function"] == "myFunc" { + result := make(map[string]interface{}) + result["result"] = "处理结果" + ext.Send("myFuncResult", result) + } + } + } +} +``` + +### WSClient API + +| 方法 | 说明 | +|------|------| +| `Run(callback, debug)` | 启动扩展主循环,每条消息触发 callback | +| `IsEvent(data, eventName)` | 判断事件名是否匹配 | +| `Send(event, data)` | 向前端发送结构化事件(`map[string]interface{}`) | +| `SendMessageString(event, data)` | 向前端发送字符串事件 | + +修改后端代码后需重新编译: + +```bash +cd backend && ./build.sh # 或 build.cmd +``` + +## Neutralino 配置说明 + +`neutralino.config.json` 关键配置: + +| 字段 | 说明 | +|------|------| +| `applicationId` | 应用标识 `com.lanyuanxiaoyao.nex` | +| `defaultMode` | 默认以窗口模式运行 | +| `documentRoot` | 前端资源根路径 `/frontend/` | +| `cli.resourcesPath` | 构建时打包的前端产物路径 `/frontend/dist` | +| `cli.extensionsPath` | 后端扩展路径 `/backend/` | +| `extensions` | 扩展定义,指定各平台启动命令 | +| `nativeAllowList` | Native API 白名单 | + +## 参考链接 + +- [NeutralinoJS 官方文档](https://neutralino.js.org/docs/) +- [React 官方文档](https://react.dev/) +- [Vite 官方文档](https://vite.dev/) +- [Go 官方文档](https://go.dev/doc/) diff --git a/backend/build.cmd b/backend/build.cmd new file mode 100644 index 0000000..7668bfa --- /dev/null +++ b/backend/build.cmd @@ -0,0 +1,8 @@ +@echo off + +set DST=nex.exe + +echo Building %DST% ... +go build -ldflags="-s -w" -o %DST% ./cmd/nex + +echo DONE! diff --git a/backend/build.sh b/backend/build.sh new file mode 100644 index 0000000..c2a3521 --- /dev/null +++ b/backend/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +DST=nex + +echo "Building $DST ..." +go build -ldflags="-s -w" -o $DST ./cmd/nex +chmod +x $DST +echo "DONE!" diff --git a/backend/cmd/nex/main.go b/backend/cmd/nex/main.go new file mode 100644 index 0000000..6724e90 --- /dev/null +++ b/backend/cmd/nex/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "nex/internal/neutralino" + "time" +) + +const extDebug = true + +var ext = new(neutralino.WSClient) + +func processAppEvent(data neutralino.EventMessage) { + if ext.IsEvent(data, "runGo") { + if d, ok := data.Data.(map[string]interface{}); ok { + + if d["function"] == "ping" { + var out = make(map[string]interface{}) + out["result"] = fmt.Sprintf("Go says PONG in reply to '%s'", d["parameter"]) + ext.Send("pingResult", out) + } + + if d["function"] == "longRun" { + go longRun() + } + } + } +} + +func longRun() { + for i := 1; i <= 10; i++ { + s := fmt.Sprintf("Long running task progress %d / 10", i) + ext.SendMessageString("pingResult", s) + time.Sleep(time.Second * 1) + } +} + +func main() { + ext.Run(processAppEvent, extDebug) +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..89c39b0 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,10 @@ +module nex + +go 1.22 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.1 +) + +require golang.org/x/net v0.17.0 // indirect diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..40286fc --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= diff --git a/backend/internal/neutralino/client.go b/backend/internal/neutralino/client.go new file mode 100644 index 0000000..124fdd7 --- /dev/null +++ b/backend/internal/neutralino/client.go @@ -0,0 +1,161 @@ +package neutralino + +import ( + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "os/signal" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +const Version = "1.0.6" + +type Config struct { + NlPort string `json:"nlPort"` + NlToken string `json:"nlToken"` + NlExtensionId string `json:"nlExtensionId"` + NlConnectToken string `json:"nlConnectToken"` +} + +type EventMessage struct { + Event string `json:"event"` + Data interface{} `json:"data"` +} + +type DataPacket struct { + Id string `json:"id"` + Method string `json:"method"` + AccessToken string `json:"accessToken"` + Data EventMessage `json:"data"` +} + +type WSClient struct { + url url.URL + socket *websocket.Conn + debug bool +} + +var ExtConfig = Config{} + +func (wsclient *WSClient) Send(event string, data map[string]interface{}) { + var msg = DataPacket{} + msg.Id = uuid.New().String() + msg.Method = "app.broadcast" + msg.AccessToken = ExtConfig.NlToken + msg.Data.Event = event + msg.Data.Data = data + + var d, err = json.Marshal(msg) + if err != nil { + fmt.Println("Error in marshaling data-packet.") + return + } + + if wsclient.debug { + fmt.Printf("%sSent: %s%s\n", "\u001B[32m", string(d), "\u001B[0m") + } + + err = wsclient.socket.WriteMessage(websocket.TextMessage, []byte(d)) + if err != nil { + if wsclient.debug { + fmt.Println("Error in Send(): ", err) + } + return + } +} + +func (wsclient *WSClient) SendMessageString(event string, data string) { + msg := make(map[string]interface{}) + msg["result"] = data + wsclient.Send(event, msg) +} + +func (wsclient *WSClient) Run(callback func(message EventMessage), debug bool) { + wsclient.debug = debug + + decoder := json.NewDecoder(os.Stdin) + + err := decoder.Decode(&ExtConfig) + if err != nil { + if err != io.EOF { + fmt.Println(err) + } + } + + sigInt := make(chan os.Signal, 1) + if debug { + signal.Notify(sigInt, os.Interrupt) + } + + var addr = "127.0.0.1:" + ExtConfig.NlPort + var path = "?extensionId=" + ExtConfig.NlExtensionId + "&connectToken=" + ExtConfig.NlConnectToken + + wsclient.url = url.URL{Scheme: "ws", Host: addr, Path: path} + if wsclient.debug { + fmt.Printf("Connecting to %s\n", wsclient.url.String()) + } + + wsclient.socket, _, err = websocket.DefaultDialer.Dial(wsclient.url.String(), nil) + if err != nil { + if wsclient.debug { + fmt.Println("Connect: ", err) + } + } + defer wsclient.socket.Close() + + go func() { + for { + _, msg, err := wsclient.socket.ReadMessage() + if err != nil { + if wsclient.debug { + fmt.Println("ERROR in read loop: ", err) + continue + } + } + if wsclient.debug { + fmt.Printf("%sReceived: %s%s\n", "\u001B[91m", msg, "\u001B[0m") + } + + var d EventMessage + err = json.Unmarshal([]byte(msg), &d) + if err != nil { + if wsclient.debug { + fmt.Println("ERROR in read loop, while unmarshalling JSON: ", err) + continue + } + } + + if wsclient.IsEvent(d, "windowClose") || wsclient.IsEvent(d, "appClose") { + wsclient.quit() + continue + } + + callback(d) + } + }() + + for { + if <-sigInt != nil { + fmt.Println("Interrupted by keyboard interaction ...") + wsclient.quit() + } + } +} + +func (wsclient *WSClient) IsEvent(data EventMessage, event string) bool { + return data.Event == event +} + +func (wsclient *WSClient) quit() { + var pid = os.Getpid() + fmt.Println("Killing own process with PID ", pid) + process, _ := os.FindProcess(pid) + err := process.Signal(os.Kill) + if err != nil { + return + } +} diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..04fc0b5 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "files": { + "includes": ["**", "!dist", "!node_modules"] + } +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..e415f7b --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, +); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f56ca42 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Nex + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3e7cba1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "format": "biome format --write src", + "format:check": "biome format src", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@neutralinojs/lib": "^6.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.11", + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "sass": "^1.99.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.4" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..b3acbb8 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,5 @@ +function App() { + return <>hello; +} + +export default App; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..3c32ab3 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "@/App.tsx"; +import "@/styles/global.scss"; + +import { init } from "@neutralinojs/lib"; + +createRoot(document.getElementById("root")!).render( + + + , +); + +init(); diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/frontend/src/pages/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss new file mode 100644 index 0000000..87fcdec --- /dev/null +++ b/frontend/src/styles/global.scss @@ -0,0 +1,25 @@ +$font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +$font-size-base: 16px; +$color-text: #1a1a1a; +$color-bg: #ffffff; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: $font-size-base; +} + +body { + font-family: $font-family; + color: $color-text; + background-color: $color-bg; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..491d970 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..d869042 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,19 @@ +import path from "node:path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + css: { + preprocessorOptions: { + scss: { + api: "modern-compiler", + }, + }, + }, +}); diff --git a/neutralino.config.json b/neutralino.config.json new file mode 100644 index 0000000..e24066f --- /dev/null +++ b/neutralino.config.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", + "applicationId": "com.lanyuanxiaoyao.nex", + "version": "1.0.0", + "defaultMode": "window", + "port": 0, + "documentRoot": "/frontend/", + "url": "/", + "enableServer": true, + "enableNativeAPI": true, + "enableExtensions": true, + "exportAuthInfo": true, + "tokenSecurity": "one-time", + "logging": { + "enabled": false, + "writeToLogFile": false + }, + "globalVariables": {}, + "modes": { + "window": { + "title": "", + "width": 800, + "height": 900, + "minWidth": 500, + "minHeight": 200, + "fullScreen": false, + "alwaysOnTop": false, + "enableInspector": true, + "borderless": false, + "maximize": false, + "hidden": false, + "center": true, + "useSavedState": false, + "resizable": true, + "exitProcessOnClose": true + } + }, + "cli": { + "binaryName": "nex", + "resourcesPath": "/frontend/dist", + "extensionsPath": "/backend/", + "binaryVersion": "6.7.0", + "clientVersion": "6.7.0", + "frontendLibrary": { + "patchFile": "/frontend/index.html", + "devUrl": "http://localhost:5173", + "projectPath": "/frontend/", + "initCommand": "bun install", + "devCommand": "bun dev", + "buildCommand": "bun run build", + "waitTimeout": 20000 + } + }, + "nativeAllowList": [ + "app.*", + "os.*", + "window.*", + "events.*", + "extensions.*", + "debug.log" + ], + "extensions": [ + { + "id": "nex", + "commandDarwin": "\"${NL_PATH}/backend/nex\" \"${NL_PATH}\"", + "commandLinux": "\"${NL_PATH}/backend/nex\" \"${NL_PATH}\"", + "commandWindows": "\"${NL_PATH}\\backend\\nex.exe\" \"${NL_PATH}\"" + } + ], + "buildScript": { + "mac": { + "architecture": [ + "x64", + "arm64" + ], + "minimumOS": "10.13.0", + "appName": "Nex", + "appBundleName": "Nex", + "appIdentifier": "com.lanyuanxiaoyao.nex", + "appIcon": "icon.icns" + }, + "win": { + "architecture": [ + "x64" + ], + "appName": "Nex.exe" + } + } +} \ No newline at end of file diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..af3ede9 --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,14 @@ +schema: spec-driven + +context: | + - **优先阅读README.md**获取项目结构与开发规范,所有代码风格、命名、注解、依赖、API等规范以README为准 + - 新增代码优先复用已有组件、工具、依赖库,不引入新依赖 + - 涉及模块结构、API、实体等变更时同步更新README.md + - Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明 + - 禁止创建git操作task + - 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行 + - 优先使用提问工具对用户进行提问 + +rules: + proposal: + - 仔细审查每一个过往spec判断是否存在Modified Capabilities