Initial project setup with Go backend, React frontend, and Neutralino desktop shell
This commit is contained in:
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal file
@@ -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
|
||||||
256
README.md
Normal file
256
README.md
Normal file
@@ -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/)
|
||||||
8
backend/build.cmd
Normal file
8
backend/build.cmd
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
set DST=nex.exe
|
||||||
|
|
||||||
|
echo Building %DST% ...
|
||||||
|
go build -ldflags="-s -w" -o %DST% ./cmd/nex
|
||||||
|
|
||||||
|
echo DONE!
|
||||||
8
backend/build.sh
Normal file
8
backend/build.sh
Normal file
@@ -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!"
|
||||||
40
backend/cmd/nex/main.go
Normal file
40
backend/cmd/nex/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
10
backend/go.mod
Normal file
10
backend/go.mod
Normal file
@@ -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
|
||||||
6
backend/go.sum
Normal file
6
backend/go.sum
Normal file
@@ -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=
|
||||||
161
backend/internal/neutralino/client.go
Normal file
161
backend/internal/neutralino/client.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/biome.json
Normal file
25
frontend/biome.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
@@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Nex</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
frontend/package.json
Normal file
36
frontend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/favicon.svg
Normal file
1
frontend/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
5
frontend/src/App.tsx
Normal file
5
frontend/src/App.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function App() {
|
||||||
|
return <>hello</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/components/index.ts
Normal file
1
frontend/src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
1
frontend/src/hooks/index.ts
Normal file
1
frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
14
frontend/src/main.tsx
Normal file
14
frontend/src/main.tsx
Normal file
@@ -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(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
|
||||||
|
init();
|
||||||
1
frontend/src/pages/index.ts
Normal file
1
frontend/src/pages/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
25
frontend/src/styles/global.scss
Normal file
25
frontend/src/styles/global.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
1
frontend/src/types/index.ts
Normal file
1
frontend/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
1
frontend/src/utils/index.ts
Normal file
1
frontend/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
19
frontend/vite.config.ts
Normal file
19
frontend/vite.config.ts
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
89
neutralino.config.json
Normal file
89
neutralino.config.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
openspec/config.yaml
Normal file
14
openspec/config.yaml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user