reset accounts.svc.plus repo
This commit is contained in:
parent
e0915541a7
commit
29ee491acf
65
AGENTS.md
65
AGENTS.md
@ -1,65 +0,0 @@
|
|||||||
# Agent Guidelines for XControl
|
|
||||||
|
|
||||||
## Repository scope
|
|
||||||
These instructions apply to the entire repository. Create a more specific `AGENTS.md`
|
|
||||||
inside a subdirectory only when you need to override or augment the guidance below for
|
|
||||||
that subtree.
|
|
||||||
|
|
||||||
## Project overview
|
|
||||||
XControl is a polyglot monorepo that ships:
|
|
||||||
- Multiple Go services (API server, account service, RAG server, supporting CLIs) under
|
|
||||||
the top-level Go module `xcontrol`.
|
|
||||||
- A Next.js dashboard (`dashboard/`) implemented in TypeScript with Tailwind CSS and
|
|
||||||
Vitest/Playwright tests.
|
|
||||||
- CMS configuration, SQL migrations, deployment manifests, and documentation that are
|
|
||||||
consumed by the services and UI.
|
|
||||||
|
|
||||||
## General expectations
|
|
||||||
- Match the existing language of the file (English vs. Chinese or bilingual) and retain
|
|
||||||
the bilingual structure when you touch documentation that already mixes both.
|
|
||||||
- Prefer structured logging (`log/slog`) or existing helper utilities over raw
|
|
||||||
`fmt.Println` in Go code.
|
|
||||||
- Keep configuration files and generated assets deterministic. If you edit files under
|
|
||||||
`config/`, `docs/cms/`, or `scripts/`, mention any required regeneration steps in your
|
|
||||||
commit message or PR description.
|
|
||||||
|
|
||||||
## Go code (all directories except `dashboard/`)
|
|
||||||
- Format Go code with `gofmt` (or `go fmt ./...`) before committing.
|
|
||||||
- Organize imports using `goimports` if available; otherwise maintain the existing
|
|
||||||
standard library / third-party separation.
|
|
||||||
- Run `go test ./...` from the repository root (or a narrower package path) after
|
|
||||||
changing Go files. Use `make test` in submodules such as `rag-server/` when you need the
|
|
||||||
module-specific workflow.
|
|
||||||
- Keep configuration structs in sync with their YAML/JSON sources and update default
|
|
||||||
values when you add new fields.
|
|
||||||
|
|
||||||
## TypeScript / Next.js dashboard (`dashboard/`)
|
|
||||||
- Use `yarn` (not `npm` or `pnpm`). Install dependencies with `yarn install` and run
|
|
||||||
scripts with `yarn --cwd dashboard <script>`.
|
|
||||||
- Format code with the existing ESLint rules by running `yarn --cwd dashboard lint
|
|
||||||
--fix` when possible. Follow the 2-space indentation style and single-quote string
|
|
||||||
literals you see in the current codebase.
|
|
||||||
- Run `yarn --cwd dashboard lint` and the relevant tests (`yarn --cwd dashboard test`
|
|
||||||
and/or `yarn --cwd dashboard test:e2e`) when you touch dashboard code.
|
|
||||||
- Avoid introducing runtime-only environment variables; prefer adding entries to
|
|
||||||
`dashboard/config/runtime-service-config.yaml` so that environments stay declarative.
|
|
||||||
|
|
||||||
## Documentation and Markdown (`docs/`, `README.md`, etc.)
|
|
||||||
- Wrap prose at a reasonable width (~100 characters) and preserve existing heading
|
|
||||||
hierarchies.
|
|
||||||
- When documenting commands or configuration, prefer fenced code blocks with explicit
|
|
||||||
language identifiers (e.g., `bash`, `go`, `json`).
|
|
||||||
- Update cross-references if you rename or relocate files that are linked in the docs.
|
|
||||||
|
|
||||||
## Database and migrations
|
|
||||||
- For schema changes, update both the SQL migration under the relevant `sql/` directory
|
|
||||||
and any Go structs/DTOs that map to the same tables.
|
|
||||||
- Provide idempotent migration steps where possible and document required manual steps
|
|
||||||
in the accompanying README or commit message.
|
|
||||||
|
|
||||||
## Testing summary
|
|
||||||
Before shipping changes, run the narrowest applicable subset of these commands:
|
|
||||||
- `go test ./...` (Go services)
|
|
||||||
- `yarn --cwd dashboard lint`
|
|
||||||
- `yarn --cwd dashboard test`
|
|
||||||
- `yarn --cwd dashboard test:e2e` (when you modify Playwright specs or end-to-end flows)
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
# ------------------------------
|
|
||||||
# Stage 1 — Build
|
|
||||||
# ------------------------------
|
|
||||||
FROM golang:1.25 AS builder
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -o accounts-api ./cmd/accountsapi
|
|
||||||
|
|
||||||
# ------------------------------
|
|
||||||
# Stage 2 — Runtime
|
|
||||||
# ------------------------------
|
|
||||||
FROM ubuntu:24.04
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=builder /src/accounts-api /usr/local/bin/accounts-api
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["/usr/local/bin/accounts-api"]
|
|
||||||
@ -1,367 +0,0 @@
|
|||||||
# Token Auth 实现指南
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
本项目实现了 Public + Refresh + JWT access_token 双层签发认证机制。
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/
|
|
||||||
├── dashboard-fresh/
|
|
||||||
│ ├── config/
|
|
||||||
│ │ └── runtime-service-config.base.yaml
|
|
||||||
│ └── lib/
|
|
||||||
│ └── auth/
|
|
||||||
│ ├── token_service.ts # Deno 前端认证模块
|
|
||||||
│ └── use_auth.ts # React Hook
|
|
||||||
│
|
|
||||||
├── account/
|
|
||||||
│ ├── config/
|
|
||||||
│ │ └── account.yaml
|
|
||||||
│ └── internal/
|
|
||||||
│ └── auth/
|
|
||||||
│ ├── token_service.go # JWT 签发与验证
|
|
||||||
│ ├── mfa_service.go # MFA 服务
|
|
||||||
│ └── middleware.go # HTTP 中间件
|
|
||||||
│
|
|
||||||
├── rag-server/
|
|
||||||
│ ├── config/
|
|
||||||
│ │ └── server.yaml
|
|
||||||
│ └── internal/
|
|
||||||
│ └── auth/
|
|
||||||
│ ├── token_service.go # JWT 签发与验证
|
|
||||||
│ └── middleware.go # HTTP 中间件
|
|
||||||
│
|
|
||||||
├── scripts/
|
|
||||||
│ └── update_token_auth.sh # 自动更新脚本
|
|
||||||
│
|
|
||||||
├── TOKEN_AUTH_MANUAL.md # 完整维护手册
|
|
||||||
└── IMPLEMENTATION_GUIDE.md # 本文件
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装依赖
|
|
||||||
|
|
||||||
### Go 服务
|
|
||||||
|
|
||||||
在 `account/` 和 `rag-server/` 目录下添加 `go.mod` 文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# account/go.mod
|
|
||||||
module account
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.9.1
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
|
||||||
github.com/pquerna/otp v1.4.0
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# rag-server/go.mod
|
|
||||||
module rag-server
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.9.1
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
安装依赖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd account && go mod tidy
|
|
||||||
cd rag-server && go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 1. Go 服务 (account)
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"account/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 初始化 Token 服务
|
|
||||||
tokenService := auth.NewTokenService(auth.TokenConfig{
|
|
||||||
PublicToken: "xcontrol-public-token-2024",
|
|
||||||
RefreshSecret: "xcontrol-refresh-secret-2024",
|
|
||||||
AccessSecret: "xcontrol-access-secret-2024",
|
|
||||||
AccessExpiry: time.Hour, // 1小时
|
|
||||||
RefreshExpiry: time.Hour * 24 * 7, // 7天
|
|
||||||
})
|
|
||||||
|
|
||||||
r := gin.Default()
|
|
||||||
|
|
||||||
// 登录接口 - 生成令牌
|
|
||||||
r.POST("/api/auth/login", func(c *gin.Context) {
|
|
||||||
// 验证用户凭据...
|
|
||||||
|
|
||||||
// 生成令牌
|
|
||||||
tokenPair, err := tokenService.GenerateTokenPair(
|
|
||||||
"user123",
|
|
||||||
"user@example.com",
|
|
||||||
[]string{"user"},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, tokenPair)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 刷新接口
|
|
||||||
r.POST("/api/auth/refresh", func(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := tokenService.RefreshAccessToken(req.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(401, gin.H{"error": "Invalid refresh token"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"access_token": accessToken,
|
|
||||||
"expires_in": int64(tokenService.GetAccessTokenExpiry().Seconds()),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 受保护的接口
|
|
||||||
protected := r.Group("/api")
|
|
||||||
protected.Use(tokenService.AuthMiddleware())
|
|
||||||
{
|
|
||||||
protected.GET("/user/profile", func(c *gin.Context) {
|
|
||||||
userID := auth.GetUserID(c)
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"user_id": userID,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 需要 MFA 的接口
|
|
||||||
protected.GET("/admin/dashboard", auth.RequireMFA(), auth.RequireRole("admin"), func(c *gin.Context) {
|
|
||||||
c.JSON(200, gin.H{"message": "Admin dashboard"})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Run(":8080")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Go 服务 (rag-server)
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"rag-server/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
tokenService := auth.NewTokenService(auth.TokenConfig{
|
|
||||||
PublicToken: "xcontrol-public-token-2024",
|
|
||||||
RefreshSecret: "xcontrol-refresh-secret-2024",
|
|
||||||
AccessSecret: "xcontrol-access-secret-2024",
|
|
||||||
AccessExpiry: time.Hour,
|
|
||||||
RefreshExpiry: time.Hour * 24 * 7,
|
|
||||||
})
|
|
||||||
|
|
||||||
r := gin.Default()
|
|
||||||
|
|
||||||
// 保护 RAG API
|
|
||||||
r.Use(tokenService.AuthMiddleware())
|
|
||||||
|
|
||||||
r.POST("/api/rag/query", func(c *gin.Context) {
|
|
||||||
userID := auth.GetUserID(c)
|
|
||||||
email := auth.GetEmail(c)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"user_id": userID,
|
|
||||||
"email": email,
|
|
||||||
"result": "RAG query processed",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Run(":8090")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 前端 (Deno + Preact)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useAuth } from '../lib/auth/use_auth.ts';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const { user, login, logout, loading } = useAuth();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <LoginForm onLogin={login} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Welcome, {user.email}</h1>
|
|
||||||
<button onClick={logout}>Logout</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoginForm({ onLogin }: { onLogin: (email: string, password: string) => Promise<boolean> }) {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const success = await onLogin(email, password);
|
|
||||||
if (!success) {
|
|
||||||
alert('Login failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
|
||||||
placeholder="Email"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 维护操作
|
|
||||||
|
|
||||||
### 1. 验证配置一致性
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --validate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 生成新密钥
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --generate-new
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 轮换密钥
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --rotate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 预览模式(不实际更新)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --rotate --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 更新维护手册版本号
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --update-manual
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 清理旧备份
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/update_token_auth.sh --cleanup
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 如何修改令牌过期时间?
|
|
||||||
|
|
||||||
**A:** 修改各服务配置中的 `accessExpiry` 和 `refreshExpiry`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
tokenService := auth.NewTokenService(auth.TokenConfig{
|
|
||||||
AccessExpiry: time.Hour * 2, // 2小时
|
|
||||||
RefreshExpiry: time.Hour * 24 * 30, // 30天
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何添加自定义 Claims?
|
|
||||||
|
|
||||||
**A:** 在 `Claims` 结构体中添加字段:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Claims struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Roles []string `json:"roles"`
|
|
||||||
MFA bool `json:"mfa_verified"`
|
|
||||||
// 添加自定义字段
|
|
||||||
Department string `json:"department"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何处理多个环境(开发、测试、生产)?
|
|
||||||
|
|
||||||
**A:** 使用不同的配置文件:
|
|
||||||
|
|
||||||
- `config.development.yaml`
|
|
||||||
- `config.test.yaml`
|
|
||||||
- `config.production.yaml`
|
|
||||||
|
|
||||||
每个环境使用不同的密钥。
|
|
||||||
|
|
||||||
### Q: 如何集成 Redis 缓存?
|
|
||||||
|
|
||||||
**A:** 在中间件中添加 Redis 检查:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *TokenService) AuthMiddlewareWithRedis() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// 检查 Redis 中的黑名单
|
|
||||||
if isTokenBlacklisted(token) {
|
|
||||||
c.JSON(401, gin.H{"error": "Token revoked"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 验证令牌...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
## 贡献
|
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request!
|
|
||||||
|
|
||||||
## 支持
|
|
||||||
|
|
||||||
如有问题,请联系开发团队或查看完整维护手册。
|
|
||||||
674
LICENSE
674
LICENSE
@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
20
Makefile
20
Makefile
@ -40,8 +40,7 @@ export PATH := /usr/local/go/bin:$(PATH)
|
|||||||
|
|
||||||
.PHONY: all init build clean start stop restart dev test help \
|
.PHONY: all init build clean start stop restart dev test help \
|
||||||
init-db-core init-db-replication init-db-pglogical \
|
init-db-core init-db-replication init-db-pglogical \
|
||||||
reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset \
|
reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset
|
||||||
gcp-deploy gcp-replace-service
|
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@ -293,20 +292,3 @@ test:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(APP_NAME) *.pid *.log
|
rm -f $(APP_NAME) *.pid *.log
|
||||||
|
|
||||||
# =========================================
|
|
||||||
# ☁️ Google Cloud Run
|
|
||||||
# =========================================
|
|
||||||
|
|
||||||
CLOUD_RUN_SERVICE := accounts-svc-plus
|
|
||||||
GCP_REGION := asia-northeast1
|
|
||||||
|
|
||||||
gcp-deploy:
|
|
||||||
gcloud run deploy $(CLOUD_RUN_SERVICE) \
|
|
||||||
--source . \
|
|
||||||
--region $(GCP_REGION) \
|
|
||||||
--update-secrets="PGADMIN_PASSWORD=admin_password:latest,DB_PASSWORD=admin_password:latest" \
|
|
||||||
--set-env-vars="DB_TLS_HOST=postgresql.onwalk.net,DB_TLS_PORT=443,DB_USER=postgres,DB_NAME=postgres"
|
|
||||||
|
|
||||||
gcp-replace-service:
|
|
||||||
gcloud run services replace deploy/gcp/cloud-run/service.yaml --region $(GCP_REGION)
|
|
||||||
|
|||||||
333
Makefile.account
333
Makefile.account
@ -1,333 +0,0 @@
|
|||||||
OS := $(shell uname -s)
|
|
||||||
SHELL := /bin/bash
|
|
||||||
O_BIN ?= /usr/local/go/bin
|
|
||||||
PG_MAJOR ?= 16
|
|
||||||
NODE_MAJOR ?= 22
|
|
||||||
BASE_IMAGE_DIR ?= deploy/base-images
|
|
||||||
OPENRESTY_IMAGE ?= xcontrol/openresty-geoip:latest
|
|
||||||
POSTGRES_EXT_IMAGE ?= xcontrol/postgres-extensions:16
|
|
||||||
NODE_BUILDER_IMAGE ?= xcontrol/node-builder:22
|
|
||||||
NODE_RUNTIME_IMAGE ?= xcontrol/node-runtime:22
|
|
||||||
GO_BUILDER_IMAGE ?= xcontrol/go-builder:1.23
|
|
||||||
GO_RUNTIME_IMAGE ?= xcontrol/go-runtime:1.23
|
|
||||||
ARCH := $(shell dpkg --print-architecture)
|
|
||||||
PG_DSN ?= postgres://shenlan:password@127.0.0.1:5432/xserver?sslmode=disable
|
|
||||||
|
|
||||||
ifeq ($(shell id -u),0)
|
|
||||||
SUDO :=
|
|
||||||
else
|
|
||||||
SUDO ?= sudo
|
|
||||||
endif
|
|
||||||
|
|
||||||
HOSTS_FILE ?= /etc/hosts
|
|
||||||
HOSTS_IP ?= 127.0.0.1
|
|
||||||
HOSTS_DOMAINS ?= dev-accounts.svc.plus dev-api.svc.plus
|
|
||||||
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
NGINX_PREFIX ?= /opt/homebrew/openresty/nginx
|
|
||||||
NGINX_MAIN_TEMPLATE ?= example/macos/openresty/nginx.conf
|
|
||||||
else
|
|
||||||
NGINX_PREFIX ?= /usr/local/openresty/nginx
|
|
||||||
endif
|
|
||||||
|
|
||||||
NGINX_CONF_ROOT ?= $(NGINX_PREFIX)/conf
|
|
||||||
NGINX_CONF_DIR ?= $(NGINX_CONF_ROOT)/conf.d
|
|
||||||
NGINX_MAIN_CONF ?= $(NGINX_CONF_ROOT)/nginx.conf
|
|
||||||
|
|
||||||
NGINX_SIT_CONFIGS := example/sit/nginx/nginx.conf
|
|
||||||
NGINX_SIT_CONFIGS += example/sit/nginx/dev.svc.plus.conf
|
|
||||||
NGINX_SIT_CONFIGS += example/sit/nginx/dev-api.svc.plus.conf
|
|
||||||
NGINX_SIT_CONFIGS := example/sit/nginx/dev-accounts.svc.plus.conf
|
|
||||||
|
|
||||||
NGINX_PROD_CONFIGS := example/prod/nginx/nginx.conf
|
|
||||||
NGINX_PROD_CONFIGS := example/prod/nginx/dev.svc.plus.conf
|
|
||||||
NGINX_PROD_CONFIGS := example/prod/nginx/api.svc.plus.conf
|
|
||||||
NGINX_PROD_CONFIGS := example/prod/nginx/accounts.svc.plus.conf
|
|
||||||
|
|
||||||
NGINX_ALL_CONFIGS := $(NGINX_SIT_CONFIGS) $(NGINX_PROD_CONFIGS)
|
|
||||||
|
|
||||||
export PATH := $(GO_BIN):$(PATH)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Environment bootstrap (hosts & services)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
init: configure-hosts init-nginx init-account init-rag-server
|
|
||||||
|
|
||||||
install-services: configure-hosts install-nginx install-account install-rag-server
|
|
||||||
|
|
||||||
upgrade-services: configure-hosts upgrade-nginx upgrade-account upgrade-rag-server
|
|
||||||
|
|
||||||
configure-hosts:
|
|
||||||
@set -e; \
|
|
||||||
if [ ! -f "$(HOSTS_FILE)" ]; then \
|
|
||||||
echo "⚠️ Hosts file $(HOSTS_FILE) not found; skipping host configuration."; \
|
|
||||||
else \
|
|
||||||
for domain in $(HOSTS_DOMAINS); do \
|
|
||||||
if grep -qE "^[[:space:]]*$(HOSTS_IP)[[:space:]]+.*\b$$domain\b" "$(HOSTS_FILE)"; then \
|
|
||||||
echo "✅ Hosts entry exists for $$domain"; \
|
|
||||||
else \
|
|
||||||
echo "➕ Adding $(HOSTS_IP) $$domain to $(HOSTS_FILE)"; \
|
|
||||||
echo "$(HOSTS_IP) $$domain" | $(SUDO) tee -a "$(HOSTS_FILE)" >/dev/null; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
init-nginx:
|
|
||||||
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
|
|
||||||
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
|
|
||||||
if [ -f "$(NGINX_MAIN_CONF)" ]; then \
|
|
||||||
if cmp -s "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; then \
|
|
||||||
echo "✅ $(NGINX_MAIN_CONF) already up to date"; \
|
|
||||||
else \
|
|
||||||
echo "⬆️ Updating $(NGINX_MAIN_CONF) from template"; \
|
|
||||||
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
|
|
||||||
fi; \
|
|
||||||
else \
|
|
||||||
echo "➕ Installing $(NGINX_MAIN_CONF)"; \
|
|
||||||
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
|
|
||||||
fi; \
|
|
||||||
fi
|
|
||||||
@for file in $(NGINX_ALL_CONFIGS); do \
|
|
||||||
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
|
|
||||||
if [ -f "$$dest" ]; then \
|
|
||||||
echo "✅ $$dest already exists; skipping"; \
|
|
||||||
else \
|
|
||||||
echo "➕ Installing $$dest"; \
|
|
||||||
$(SUDO) install -m 0644 "$$file" "$$dest"; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
|
|
||||||
install-nginx: init-nginx reload-openresty
|
|
||||||
|
|
||||||
upgrade-nginx:
|
|
||||||
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
|
|
||||||
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
|
|
||||||
echo "⬆️ Updating $(NGINX_MAIN_CONF)"; \
|
|
||||||
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
|
|
||||||
fi
|
|
||||||
@for file in $(NGINX_ALL_CONFIGS); do \
|
|
||||||
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
|
|
||||||
echo "⬆️ Updating $$dest"; \
|
|
||||||
$(SUDO) install -m 0644 "$$file" "$$dest"; \
|
|
||||||
done
|
|
||||||
@$(MAKE) reload-openresty
|
|
||||||
|
|
||||||
reload-openresty:
|
|
||||||
@echo "🔄 Reloading OpenResty/Nginx if available..."
|
|
||||||
@command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^openresty.service' && { \
|
|
||||||
$(SUDO) systemctl reload openresty 2>/dev/null || $(SUDO) systemctl restart openresty 2>/dev/null || true; \
|
|
||||||
echo "✅ openresty.service reloaded"; \
|
|
||||||
} || echo "ℹ️ openresty.service not managed by systemd or systemctl missing; please reload manually."
|
|
||||||
|
|
||||||
init-account:
|
|
||||||
@$(MAKE) -C account init
|
|
||||||
|
|
||||||
install-account:
|
|
||||||
@$(MAKE) -C account build
|
|
||||||
|
|
||||||
upgrade-account:
|
|
||||||
@$(MAKE) -C account upgrade
|
|
||||||
|
|
||||||
init-rag-server:
|
|
||||||
@$(MAKE) -C rag-server init
|
|
||||||
|
|
||||||
install-rag-server:
|
|
||||||
@$(MAKE) -C rag-server build
|
|
||||||
|
|
||||||
upgrade-rag-server:
|
|
||||||
@$(MAKE) -C rag-server build
|
|
||||||
@$(MAKE) -C rag-server restart
|
|
||||||
|
|
||||||
.PHONY: install install-openresty install-redis install-postgresql init-db \
|
|
||||||
build update-dashboard-manifests build-server build-dashboard \
|
|
||||||
start start-openresty start-server start-dashboard \
|
|
||||||
stop stop-server stop-dashboard stop-openresty restart lint-cms \
|
|
||||||
init init-nginx install-nginx upgrade-nginx reload-openresty \
|
|
||||||
init-account install-account upgrade-account \
|
|
||||||
init-rag-server install-rag-server upgrade-rag-server \
|
|
||||||
configure-hosts install-services upgrade-services \
|
|
||||||
build-base-images docker-openresty-geoip docker-postgres-extensions \
|
|
||||||
docker-node-builder docker-node-runtime docker-go-builder docker-go-runtime
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Dependency installation
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
install: install-nodejs install-go install-openresty install-redis install-postgresql
|
|
||||||
|
|
||||||
# --- Node.js ---------------------------------------------------------------
|
|
||||||
install-nodejs:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
( brew install node@22 && brew link --overwrite --force node@22 ) || brew install node
|
|
||||||
corepack enable || true
|
|
||||||
corepack prepare yarn@stable --activate || true
|
|
||||||
@echo "✅ Node: $$(node -v)"; echo "✅ Yarn: $$(yarn -v 2>/dev/null || echo n/a)"
|
|
||||||
else
|
|
||||||
@echo "🟦 Installing Node.js $(NODE_MAJOR) via setup_ubuntu_2204.sh..."
|
|
||||||
NODE_MAJOR=$(NODE_MAJOR) bash scripts/setup_ubuntu_2204.sh install-nodejs
|
|
||||||
endif
|
|
||||||
|
|
||||||
# --- Go --------------------------------------------------------------------
|
|
||||||
install-go:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
brew install go
|
|
||||||
else
|
|
||||||
GO_VERSION=$(GO_VERSION) bash scripts/setup_ubuntu_2204.sh install-go
|
|
||||||
endif
|
|
||||||
|
|
||||||
# --- OpenResty -------------------------------------------------------------
|
|
||||||
install-openresty:
|
|
||||||
@echo "🚀 Installing OpenResty using external script..."
|
|
||||||
@bash scripts/install-openresty.sh; \
|
|
||||||
|
|
||||||
# --- Redis -----------------------------------------------------------------
|
|
||||||
install-redis:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
brew install redis && brew services start redis
|
|
||||||
else
|
|
||||||
@echo "🟥 Installing Redis via setup_ubuntu_2204.sh..."
|
|
||||||
bash scripts/setup_ubuntu_2204.sh install-redis
|
|
||||||
endif
|
|
||||||
|
|
||||||
# --- PostgreSQL ------------------------------------------------------------
|
|
||||||
install-postgresql:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
@set -e; \
|
|
||||||
echo "🍎 Installing PostgreSQL 16 via Homebrew..."; \
|
|
||||||
brew install postgresql@16 || true; \
|
|
||||||
brew services start postgresql@16; \
|
|
||||||
echo "📦 Installing pgvector extension..."; \
|
|
||||||
brew install pgvector || true; \
|
|
||||||
echo "📦 Installing pg_jieba (替代 zhparser + scws)..."; \
|
|
||||||
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
|
|
||||||
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
|
|
||||||
cd pg_jieba && mkdir build && cd build && \
|
|
||||||
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=$$(brew --prefix postgresql@16)/include/postgresql/server .. && \
|
|
||||||
make -j$$(sysctl -n hw.ncpu) && sudo make install && \
|
|
||||||
cd / && rm -rf $$tmp_dir; \
|
|
||||||
echo "✅ PostgreSQL extensions installed successfully!"
|
|
||||||
else
|
|
||||||
@set -e; \
|
|
||||||
echo "🟨 Installing PostgreSQL 16..."; \
|
|
||||||
bash scripts/setup_ubuntu_2204.sh install-postgresql; \
|
|
||||||
echo "🟨 Installing pgvector extension..."; \
|
|
||||||
bash scripts/setup_ubuntu_2204.sh install-pgvector; \
|
|
||||||
echo "🟨 Installing pg_jieba extension (替代 zhparser + scws)..."; \
|
|
||||||
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
|
|
||||||
sudo apt-get install -y cmake g++ git postgresql-server-dev-${PG_MAJOR}; \
|
|
||||||
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
|
|
||||||
cd pg_jieba && mkdir build && cd build && \
|
|
||||||
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql/${PG_MAJOR}/server .. && \
|
|
||||||
make -j$$(nproc) && sudo make install && \
|
|
||||||
cd / && rm -rf $$tmp_dir; \
|
|
||||||
echo "✅ PostgreSQL extensions installed successfully!"
|
|
||||||
endif
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Base container images
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
build-base-images:
|
|
||||||
@OPENRESTY_IMAGE=$(OPENRESTY_IMAGE) POSTGRES_EXT_IMAGE=$(POSTGRES_EXT_IMAGE) \
|
|
||||||
NODE_BUILDER_IMAGE=$(NODE_BUILDER_IMAGE) NODE_RUNTIME_IMAGE=$(NODE_RUNTIME_IMAGE) \
|
|
||||||
GO_BUILDER_IMAGE=$(GO_BUILDER_IMAGE) GO_RUNTIME_IMAGE=$(GO_RUNTIME_IMAGE) \
|
|
||||||
bash scripts/build-base-images.sh
|
|
||||||
|
|
||||||
docker-openresty-geoip:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/openresty-geoip.Dockerfile -t $(OPENRESTY_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
docker-postgres-extensions:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/postgres-extensions.Dockerfile -t $(POSTGRES_EXT_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
docker-node-builder:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/node-builder.Dockerfile -t $(NODE_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
docker-node-runtime:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/node-runtime.Dockerfile -t $(NODE_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
docker-go-builder:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/go-builder.Dockerfile -t $(GO_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
docker-go-runtime:
|
|
||||||
docker build -f $(BASE_IMAGE_DIR)/go-runtime.Dockerfile -t $(GO_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Database initialization
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
init-db:
|
|
||||||
@psql $(PG_DSN) -f rag-server/sql/schema.sql
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Build targets
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
build: update-dashboard-manifests build-cli build-server build-dashboard
|
|
||||||
|
|
||||||
build-cli:
|
|
||||||
$(MAKE) -C rag-server/cmd/rag-server-cli build
|
|
||||||
|
|
||||||
build-server:
|
|
||||||
$(MAKE) -C rag-server build
|
|
||||||
|
|
||||||
build-dashboard:
|
|
||||||
$(MAKE) -C dashboard build SKIP_SYNC=1
|
|
||||||
|
|
||||||
update-dashboard-manifests:
|
|
||||||
$(MAKE) -C dashboard sync-dl-index
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Run targets
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
start: start-openresty start-server start-dashboard
|
|
||||||
|
|
||||||
start-server:
|
|
||||||
$(MAKE) -C rag-server start
|
|
||||||
|
|
||||||
start-dashboard:
|
|
||||||
$(MAKE) -C dashboard start
|
|
||||||
|
|
||||||
stop: stop-server stop-dashboard stop-openresty
|
|
||||||
|
|
||||||
stop-server:
|
|
||||||
$(MAKE) -C rag-server stop
|
|
||||||
|
|
||||||
stop-dashboard:
|
|
||||||
$(MAKE) -C dashboard stop
|
|
||||||
|
|
||||||
start-openresty:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
@brew services start openresty >/dev/null 2>&1 || \
|
|
||||||
( echo "Creating LaunchAgent for OpenResty..." && \
|
|
||||||
mkdir -p ~/Library/LaunchAgents && \
|
|
||||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8?>' \
|
|
||||||
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
|
|
||||||
'<plist version="1.0"><dict>' \
|
|
||||||
' <key>Label</key><string>homebrew.mxcl.openresty</string>' \
|
|
||||||
' <key>ProgramArguments</key>' \
|
|
||||||
' <array>' \
|
|
||||||
' <string>/opt/homebrew/openresty/nginx/sbin/nginx</string>' \
|
|
||||||
' <string>-g</string>' \
|
|
||||||
' <string>daemon off;</string>' \
|
|
||||||
' </array>' \
|
|
||||||
' <key>RunAtLoad</key><true/>' \
|
|
||||||
'</dict></plist>' \
|
|
||||||
> ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist && \
|
|
||||||
brew services start ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist )
|
|
||||||
else
|
|
||||||
sudo systemctl enable --now openresty || echo "⚠️ openresty.service missing or inactive"
|
|
||||||
endif
|
|
||||||
|
|
||||||
stop-openresty:
|
|
||||||
ifeq ($(OS),Darwin)
|
|
||||||
-brew services stop openresty >/dev/null 2>&1
|
|
||||||
else
|
|
||||||
-sudo systemctl stop openresty >/dev/null 2>&1
|
|
||||||
endif
|
|
||||||
|
|
||||||
restart: stop start
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# CMS configuration validation
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
lint-cms:
|
|
||||||
python3 scripts/validate_cms_config.py
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
# ✅ 路径验证报告
|
|
||||||
|
|
||||||
## 📁 目录结构验证
|
|
||||||
|
|
||||||
所有代码均按要求放入正确目录,以下是详细验证:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1️⃣ rag-server/ 目录
|
|
||||||
|
|
||||||
### 认证模块 (internal/auth/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/rag-server/
|
|
||||||
└── internal/
|
|
||||||
└── auth/
|
|
||||||
├── client.go ✅ 新增:认证客户端
|
|
||||||
├── middleware_verify.go ✅ 新增:Gin 验证中间件
|
|
||||||
├── cache.go ✅ 新增:缓存机制
|
|
||||||
├── example_test.go ✅ 新增:使用示例
|
|
||||||
├── README.md ✅ 新增:完整文档
|
|
||||||
├── IMPLEMENTATION.md ✅ 新增:实现总结
|
|
||||||
├── COMPLETION_REPORT.md ✅ 新增:完成报告
|
|
||||||
├── middleware.go ✅ 已有:旧版中间件
|
|
||||||
└── token_service.go ✅ 已有:Token 服务
|
|
||||||
```
|
|
||||||
|
|
||||||
### 主程序 (cmd/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/rag-server/
|
|
||||||
└── cmd/
|
|
||||||
└── xcontrol-server/
|
|
||||||
└── main.go ✅ 修改:启用认证中间件
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置 (config/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/rag-server/
|
|
||||||
└── config/
|
|
||||||
├── config.go ✅ 修改:添加 AuthCfg
|
|
||||||
└── server.yaml ✅ 修改:移除私钥,添加认证 URL
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2️⃣ account/ 目录
|
|
||||||
|
|
||||||
### 认证模块 (internal/auth/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/account/
|
|
||||||
└── internal/
|
|
||||||
└── auth/
|
|
||||||
├── token_service.go ✅ 已有:Token 服务实现
|
|
||||||
├── middleware.go ✅ 已有:认证中间件
|
|
||||||
└── mfa_service.go ✅ 已有:MFA 服务
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 服务 (api/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/account/
|
|
||||||
└── api/
|
|
||||||
└── api.go ✅ 已有:认证接口实现
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置 (config/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/account/
|
|
||||||
└── config/
|
|
||||||
└── account.yaml ✅ 已有:服务配置
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3️⃣ dashboard-fresh/ 目录
|
|
||||||
|
|
||||||
### 认证模块 (lib/auth/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/dashboard-fresh/
|
|
||||||
└── lib/
|
|
||||||
└── auth/
|
|
||||||
└── token_service.ts ✅ 已有:前端 Token 服务
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置 (config/)
|
|
||||||
```
|
|
||||||
/Users/shenlan/workspaces/XControl/dashboard-fresh/
|
|
||||||
└── config/
|
|
||||||
├── runtime-service-config.base.yaml ✅ 已有:基础配置
|
|
||||||
└── runtime-service-config.prod.yaml ✅ 已有:生产配置
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 关键实现文件
|
|
||||||
|
|
||||||
### rag-server 核心文件
|
|
||||||
|
|
||||||
| 文件路径 | 行数 | 功能 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `/rag-server/internal/auth/client.go` | 350 | 认证客户端,远程调用 accounts-service |
|
|
||||||
| `/rag-server/internal/auth/middleware_verify.go` | 280 | Gin 中间件,验证 JWT token |
|
|
||||||
| `/rag-server/internal/auth/cache.go` | 180 | 缓存机制,TTL 60s |
|
|
||||||
| `/rag-server/cmd/xcontrol-server/main.go` | +30 | 启用认证中间件 |
|
|
||||||
| `/rag-server/config/config.go` | +15 | 添加 AuthCfg 配置结构 |
|
|
||||||
|
|
||||||
### account 核心文件
|
|
||||||
|
|
||||||
| 文件路径 | 行数 | 功能 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `/account/internal/auth/token_service.go` | 190 | Token 签发与验证 |
|
|
||||||
| `/account/internal/auth/middleware.go` | 161 | 认证中间件 |
|
|
||||||
| `/account/api/api.go` | 2030 | 认证接口实现 |
|
|
||||||
| `/account/config/account.yaml` | 96 | 服务配置 |
|
|
||||||
|
|
||||||
### dashboard-fresh 核心文件
|
|
||||||
|
|
||||||
| 文件路径 | 行数 | 功能 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `/dashboard-fresh/lib/auth/token_service.ts` | 270 | 前端 Token 管理 |
|
|
||||||
| `/dashboard-fresh/config/runtime-service-config.base.yaml` | 13 | 基础配置(仅 publicToken) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 路径验证清单
|
|
||||||
|
|
||||||
### rag-server 路径
|
|
||||||
- [x] ✅ `rag-server/internal/auth/` - 认证模块目录
|
|
||||||
- [x] ✅ `rag-server/cmd/xcontrol-server/main.go` - 主程序
|
|
||||||
- [x] ✅ `rag-server/config/config.go` - 配置结构
|
|
||||||
- [x] ✅ `rag-server/config/server.yaml` - 服务配置
|
|
||||||
|
|
||||||
### account 路径
|
|
||||||
- [x] ✅ `account/internal/auth/` - 认证模块目录
|
|
||||||
- [x] ✅ `account/api/api.go` - API 服务
|
|
||||||
- [x] ✅ `account/config/account.yaml` - 服务配置
|
|
||||||
|
|
||||||
### dashboard-fresh 路径
|
|
||||||
- [x] ✅ `dashboard-fresh/lib/auth/` - 认证模块目录
|
|
||||||
- [x] ✅ `dashboard-fresh/config/` - 配置文件目录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 统计信息
|
|
||||||
|
|
||||||
### 按项目统计
|
|
||||||
|
|
||||||
```
|
|
||||||
rag-server:
|
|
||||||
- Go 文件: 6
|
|
||||||
- Markdown: 3
|
|
||||||
- 总代码: ~1000 行
|
|
||||||
|
|
||||||
account:
|
|
||||||
- Go 文件: 3
|
|
||||||
- 总代码: ~2400 行
|
|
||||||
|
|
||||||
dashboard-fresh:
|
|
||||||
- TypeScript: 1
|
|
||||||
- YAML: 2
|
|
||||||
- 总代码: ~300 行
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件位置验证
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 验证 rag-server 路径
|
|
||||||
ls /Users/shenlan/workspaces/XControl/rag-server/internal/auth/*.go ✅ 所有文件存在
|
|
||||||
ls /Users/shenlan/workspaces/XControl/rag-server/cmd/xcontrol-server/main.go ✅ 存在
|
|
||||||
|
|
||||||
# 验证 account 路径
|
|
||||||
ls /Users/shenlan/workspaces/XControl/account/internal/auth/*.go ✅ 所有文件存在
|
|
||||||
ls /Users/shenlan/workspaces/XControl/account/api/api.go ✅ 存在
|
|
||||||
|
|
||||||
# 验证 dashboard-fresh 路径
|
|
||||||
ls /Users/shenlan/workspaces/XControl/dashboard-fresh/lib/auth/*.ts ✅ 所有文件存在
|
|
||||||
ls /Users/shenlan/workspaces/XControl/dashboard-fresh/config/*.yaml ✅ 所有文件存在
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 结论
|
|
||||||
|
|
||||||
✅ **所有代码均在正确路径**
|
|
||||||
|
|
||||||
- rag-server 代码全部位于 `/Users/shenlan/workspaces/XControl/rag-server/`
|
|
||||||
- account 代码全部位于 `/Users/shenlan/workspaces/XControl/account/`
|
|
||||||
- dashboard-fresh 代码全部位于 `/Users/shenlan/workspaces/XControl/dashboard-fresh/`
|
|
||||||
|
|
||||||
路径结构清晰,便于维护和管理。
|
|
||||||
|
|
||||||
---
|
|
||||||
*验证日期: 2025-11-05*
|
|
||||||
149
README.md
149
README.md
@ -1,149 +0,0 @@
|
|||||||
# XControl
|
|
||||||
|
|
||||||
XControl is a modular multi-tenant management platform written in Go. The project integrates several optional components to provide a visual control plane for traffic statistics, configuration export and multi-node management.
|
|
||||||
|
|
||||||
This repository contains the API server, agent code and a Next.js-based UI.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
- **dashboard**
|
|
||||||
- **ui-panel**
|
|
||||||
- **xcontrol-cli**
|
|
||||||
- **xcontrol-server**
|
|
||||||
- **markdown studio** (NeuraPress-based, MIT-licensed) available at `/editor` (public)
|
|
||||||
and `/dashboard/cms` (SaaS shell). The upstream license and NOTICE live under
|
|
||||||
`packages/neurapress`, keeping attribution to
|
|
||||||
[tianyaxiang](https://github.com/tianyaxiang/neurapress).
|
|
||||||
|
|
||||||
### NeuraPress integration · 集成说明
|
|
||||||
|
|
||||||
The `/editor` route ships the original NeuraPress online editing core vendored under
|
|
||||||
`packages/neurapress`. Routing, authentication, and storage selection are layered on
|
|
||||||
top inside XControl, while the editing experience stays aligned with the upstream project.
|
|
||||||
|
|
||||||
上游 NeuraPress 由 tianyaxiang 以 MIT 协议发布。本项目在 `packages/neurapress` 中保留
|
|
||||||
LICENSE 与 NOTICE 以持续标注版权与来源。
|
|
||||||
|
|
||||||
|
|
||||||
All UI components provide both Chinese and English interfaces.
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
| Category | Technology | Version |
|
|
||||||
|------------------|----------------------------|----------------------------|
|
|
||||||
| Gateway | OpenResty | 1.27.1.2 |
|
|
||||||
| BackendFramework | Go | 1.24 |
|
|
||||||
| FrontFramework | Deno/Fresh/Preact/signals | 2.5.6/v1.7.3/10.22.0/1.2.2 |
|
|
||||||
| Cache | Redis | 8.2.0 |
|
|
||||||
| Database | PostgreSQL + pgvector | 16 |
|
|
||||||
| Model (Local) | HuggingFace Hub + Ollama | baai/bge-m3, llama2:13b |
|
|
||||||
| Model (Online) | Chutes.AI | baai/bge-m3, moonshotai/Kimi-K2-Instruct |
|
|
||||||
|
|
||||||
## LangChainGo 核心功能集成一览
|
|
||||||
|
|
||||||
XControl 通过 LangChainGo 统一接入多种大模型,并为 AskAI、CLI 与 Server 提供链式调用能力:
|
|
||||||
|
|
||||||
- **LLM 接口层(Model I/O)**:统一调用 Hugging Face、Ollama、OpenAI 兼容模型接口。
|
|
||||||
- **Chains(链式流程)**:将 prompt、检索结果、工具调用等组合成完整流程,支持 RAG、聊天、代码生成等场景。
|
|
||||||
- **工具与 Agent 体系**:定义 Web 搜索、实现 ReAct 风格的工具调用。
|
|
||||||
- **向量检索与数据接入**:适配 PGVector 向量存储。
|
|
||||||
- **文档加载与分块**:提供 Document Loaders 与 Text Splitters,用于处理长文本与构建向量检索块。
|
|
||||||
- **Memory 与历史追踪**:支持 Conversation Buffer 等对话记忆机制,增强交互体验。
|
|
||||||
|
|
||||||
|
|
||||||
## CMS configuration
|
|
||||||
|
|
||||||
A unified CMS setup is defined in [`config/cms.json`](config/cms.json). The schema at [`config/cms.schema.json`](config/cms.schema.json) ensures templates, themes, extensions and content sources stay in sync across deployments.
|
|
||||||
|
|
||||||
- Refer to [`docs/cms/README.md`](docs/cms/README.md) for usage instructions, extension development notes and theme customization guidelines.
|
|
||||||
- Follow the migration playbook in [`docs/cms/migration-guide.md`](docs/cms/migration-guide.md) when switching existing sites to the CMS architecture.
|
|
||||||
|
|
||||||
## Supported Platforms
|
|
||||||
|
|
||||||
Tested on **Ubuntu 22.04 x64** and **macOS 26 arm64**.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install
|
|
||||||
make init-db # initialize database (optional)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend configuration
|
|
||||||
|
|
||||||
The Next.js dashboard now resolves service endpoints through `dashboard/config/runtime-service-config.yaml`. The runtime
|
|
||||||
configuration selects values based on `NEXT_PUBLIC_RUNTIME_ENV` (falling back to `NODE_ENV` and the file's
|
|
||||||
`defaultEnvironment`). Use `NEXT_PUBLIC_ACCOUNT_SERVICE_URL` for ad-hoc overrides, otherwise adjust the YAML file to specify
|
|
||||||
environment-specific URLs such as `http://localhost:8080` for development/test and `https://accounts.svc.plus` for production.
|
|
||||||
|
|
||||||
## Account service configuration
|
|
||||||
|
|
||||||
`account/config/account.yaml` now accepts a `server.publicUrl` value such as `https://accounts.svc.plus:8443`. The account service
|
|
||||||
uses this URL to derive a default CORS origin and to document the externally reachable host. Set `server.allowedOrigins` when you
|
|
||||||
need to expose additional browser clients; omit it to fall back to the public URL or the local development origins
|
|
||||||
(`http://localhost:3001` and `http://127.0.0.1:3001`).
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- **XCloudFlow** Multi-cloud IaC engine built with Pulumi SDK and Go. GitHub →
|
|
||||||
- **KubeGuard** Kubernetes cluster application and node-level backup system. GitHub →
|
|
||||||
- **XConfig** Lightweight task execution & configuration orchestration engine. GitHub →
|
|
||||||
- **CodePRobot** AI-driven GitHub Issue to Pull Request generator and code patching tool. GitHub →
|
|
||||||
- **OpsAgent** AIOps-powered intelligent monitoring, anomaly detection and RCA. GitHub →
|
|
||||||
- **XStream** Cross-border developer proxy accelerator for global accessibility. GitHub →
|
|
||||||
|
|
||||||
The [docs](./docs) directory contains a more detailed [overview](./docs/overview.md) and design documents for each module.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
```
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
This produces a binary under `bin/xcontrol`. Run `make agent` to build the node agent.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
```
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make start
|
|
||||||
```
|
|
||||||
|
|
||||||
This launches the server, dashboard and panel. Use `make stop` to stop all components.
|
|
||||||
|
|
||||||
The API server also accepts a custom configuration file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcontrol-server --config path/to/server.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
Both `xcontrol-cli` and `xcontrol-server` accept a `--log-level` flag to control verbosity. The level may be one of `debug`, `info`, `warn`, or `error`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcontrol-cli --log-level debug
|
|
||||||
xcontrol-server --log-level warn
|
|
||||||
```
|
|
||||||
|
|
||||||
The server's log level can also be set in the configuration file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
log:
|
|
||||||
level: info
|
|
||||||
```
|
|
||||||
|
|
||||||
The flag value takes precedence over the configuration file.
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
See [docs/changelog.md](./docs/changelog.md) for a list of completed changes, including all work from Milestone 1.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
The roadmap below is also available in [docs/Roadmap.md](./docs/Roadmap.md).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the terms of the [MIT License](./LICENSE).
|
|
||||||
@ -1,429 +0,0 @@
|
|||||||
# Public + Refresh + JWT Access Token 双层签发维护手册
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本系统实现了基于 Public Token、Refresh Token 和 JWT Access Token 的三层认证机制,提供安全、灵活的用户认证解决方案。
|
|
||||||
|
|
||||||
## 架构设计
|
|
||||||
|
|
||||||
### 1. 认证流程
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ 1. Login Request ┌──────────────┐
|
|
||||||
│ Client │ ────────────────────────→ │ Account │
|
|
||||||
│ (Dashboard) │ │ Service │
|
|
||||||
└─────────────┘ └──────────────┘
|
|
||||||
↑ │
|
|
||||||
│ 2. TokenPair (Public+Refresh+JWT) │
|
|
||||||
│ ▼
|
|
||||||
│ ┌──────────────┐
|
|
||||||
│ │ TokenService │
|
|
||||||
│ │ (JWT Sign) │
|
|
||||||
│ └──────────────┘
|
|
||||||
│ │
|
|
||||||
│ 3. API Request │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 4. Access Token Verification │
|
|
||||||
│ (Middleware) ▼
|
|
||||||
│ ┌──────────────┐
|
|
||||||
│ 5. Response │ Protected │
|
|
||||||
│ ←────────────────────────────── │ Resources │
|
|
||||||
│ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 三层 Token 说明
|
|
||||||
|
|
||||||
#### Public Token
|
|
||||||
- **用途**: 标识客户端身份,用于初次认证
|
|
||||||
- **特征**: 固定值,存储在配置文件中
|
|
||||||
- **示例**: `xcontrol-public-token-2024`
|
|
||||||
- **安全性**: 低,仅作为入口验证
|
|
||||||
|
|
||||||
#### Refresh Token
|
|
||||||
- **用途**: 长期有效的刷新令牌
|
|
||||||
- **格式**: JWT
|
|
||||||
- **过期时间**: 7-30 天(可配置)
|
|
||||||
- **存储**: 客户端安全存储
|
|
||||||
- **安全性**: 中等,用于获取新的 Access Token
|
|
||||||
|
|
||||||
#### Access Token (JWT)
|
|
||||||
- **用途**: API 访问令牌
|
|
||||||
- **格式**: JWT with HS256
|
|
||||||
- **过期时间**: 15-60 分钟(可配置)
|
|
||||||
- **载荷**: 包含用户信息、角色、MFA 状态等
|
|
||||||
- **安全性**: 高,短期有效减少泄露风险
|
|
||||||
|
|
||||||
## 配置文件
|
|
||||||
|
|
||||||
### 1. dashboard-fresh/config/runtime-service-config.base.yaml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2024"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2024"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. account/config/account.yaml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2024"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2024"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. rag-server/config/server.yaml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2024"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2024"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Go 服务实现
|
|
||||||
|
|
||||||
### account/internal/auth/
|
|
||||||
|
|
||||||
#### 1. token_service.go
|
|
||||||
|
|
||||||
**功能**: 负责 Token 的生成、验证和刷新
|
|
||||||
|
|
||||||
**主要方法**:
|
|
||||||
- `NewTokenService(config TokenConfig)`: 创建服务实例
|
|
||||||
- `ValidatePublicToken(publicToken string)`: 验证公共令牌
|
|
||||||
- `GenerateTokenPair(userID, email string, roles []string)`: 生成三层令牌
|
|
||||||
- `ValidateAccessToken(accessToken string)`: 验证访问令牌
|
|
||||||
- `RefreshAccessToken(refreshToken string)`: 使用刷新令牌获取新访问令牌
|
|
||||||
|
|
||||||
**配置示例**:
|
|
||||||
```go
|
|
||||||
tokenService := auth.NewTokenService(auth.TokenConfig{
|
|
||||||
PublicToken: "xcontrol-public-token-2024",
|
|
||||||
RefreshSecret: "xcontrol-refresh-secret-2024",
|
|
||||||
AccessSecret: "xcontrol-access-secret-2024",
|
|
||||||
AccessExpiry: time.Hour, // 1小时
|
|
||||||
RefreshExpiry: time.Hour * 24 * 7, // 7天
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. mfa_service.go
|
|
||||||
|
|
||||||
**功能**: 多因素认证服务
|
|
||||||
|
|
||||||
**主要方法**:
|
|
||||||
- `GenerateSecret()`: 生成 TOTP 密钥
|
|
||||||
- `GenerateQRCode(accountName, secret string)`: 生成二维码
|
|
||||||
- `ValidateTOTP(secret, code string)`: 验证 TOTP 码
|
|
||||||
- `GenerateBackupCodes(count int)`: 生成备用码
|
|
||||||
|
|
||||||
#### 3. middleware.go
|
|
||||||
|
|
||||||
**功能**: HTTP 中间件,用于保护 API 端点
|
|
||||||
|
|
||||||
**中间件**:
|
|
||||||
- `AuthMiddleware()`: 验证 JWT 访问令牌
|
|
||||||
- `RequireMFA()`: 要求 MFA 验证
|
|
||||||
- `RequireRole(role string)`: 要求特定角色
|
|
||||||
|
|
||||||
**使用示例**:
|
|
||||||
```go
|
|
||||||
r := gin.Default()
|
|
||||||
r.Use(tokenService.AuthMiddleware())
|
|
||||||
r.GET("/api/protected", RequireMFA(), RequireRole("admin"), handler)
|
|
||||||
```
|
|
||||||
|
|
||||||
### rag-server/internal/auth/
|
|
||||||
|
|
||||||
#### 1. token_service.go
|
|
||||||
- 与 account 类似,但 `Issuer` 字段为 `"xcontrol-rag"`
|
|
||||||
- Audience 为 `"xcontrol-rag-access"` 和 `"xcontrol-rag-refresh"`
|
|
||||||
- Claim 中包含 `service` 字段用于区分服务
|
|
||||||
|
|
||||||
#### 2. middleware.go
|
|
||||||
- 同样提供认证中间件
|
|
||||||
- 验证 `service` 字段是否为 `"rag-server"`
|
|
||||||
|
|
||||||
## Deno 前端实现
|
|
||||||
|
|
||||||
### lib/auth/token_service.ts
|
|
||||||
|
|
||||||
**功能**: 前端 Token 管理服务
|
|
||||||
|
|
||||||
**主要方法**:
|
|
||||||
- `setTokens(tokenPair)`: 设置令牌
|
|
||||||
- `getAccessToken()`: 获取当前访问令牌
|
|
||||||
- `isTokenExpired()`: 检查令牌是否过期
|
|
||||||
- `decodeToken()`: 解码 JWT(不验证)
|
|
||||||
- `refreshAccessToken()`: 刷新访问令牌
|
|
||||||
- `ensureValidToken()`: 自动验证和刷新令牌
|
|
||||||
|
|
||||||
### lib/auth/use_auth.ts
|
|
||||||
|
|
||||||
**功能**: React Hook,提供认证状态管理
|
|
||||||
|
|
||||||
**主要功能**:
|
|
||||||
- `login(email, password)`: 登录
|
|
||||||
- `logout()`: 登出
|
|
||||||
- `refreshToken()`: 刷新令牌
|
|
||||||
- `hasRole(role)`: 检查角色
|
|
||||||
- 自动加载和保存令牌到 localStorage
|
|
||||||
|
|
||||||
**使用示例**:
|
|
||||||
```typescript
|
|
||||||
import { useAuth } from '../lib/auth/use_auth.ts';
|
|
||||||
|
|
||||||
function LoginComponent() {
|
|
||||||
const { login, loading, error } = useAuth();
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
const success = await login('user@example.com', 'password');
|
|
||||||
if (success) {
|
|
||||||
// 登录成功
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleLogin}>
|
|
||||||
{/* 表单内容 */}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 接口
|
|
||||||
|
|
||||||
### 1. 登录接口
|
|
||||||
|
|
||||||
**POST** `/api/auth/login`
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"p": "s"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"public_token": "xcontrol-public-token-2024",
|
|
||||||
"access_token": "JWT_HEADER_PLACEHOLDER...",
|
|
||||||
"refresh_token": "JWT_HEADER_PLACEHOLDER...",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 3600
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 刷新令牌接口
|
|
||||||
|
|
||||||
**POST** `/api/auth/refresh`
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"refresh_token": "JWT_HEADER_PLACEHOLDER..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "JWT_HEADER_PLACEHOLDER...",
|
|
||||||
"expires_in": 3600
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 验证接口
|
|
||||||
|
|
||||||
**GET** `/api/auth/verify`
|
|
||||||
|
|
||||||
**请求头**:
|
|
||||||
```
|
|
||||||
Authorization: Bearer <access_token>
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"valid": true,
|
|
||||||
"user_id": "12345",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"roles": ["user", "admin"],
|
|
||||||
"mfa_verified": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全最佳实践
|
|
||||||
|
|
||||||
### 1. Token 安全
|
|
||||||
- ✅ Access Token 短期有效(15-60 分钟)
|
|
||||||
- ✅ Refresh Token 长期有效(7-30 天)
|
|
||||||
- ✅ 使用强随机密钥
|
|
||||||
- ✅ 定期轮换密钥
|
|
||||||
- ❌ 不在 URL 中传递令牌
|
|
||||||
- ❌ 不在客户端永久存储 Access Token
|
|
||||||
|
|
||||||
### 2. 存储策略
|
|
||||||
- **Access Token**: 内存或短期存储
|
|
||||||
- **Refresh Token**: 安全存储(HttpOnly Cookie 或加密存储)
|
|
||||||
- **Public Token**: 可公开存储
|
|
||||||
|
|
||||||
### 3. 传输安全
|
|
||||||
- ✅ 所有 API 调用使用 HTTPS
|
|
||||||
- ✅ 使用 Authorization Header
|
|
||||||
- ✅ 设置适当的 CORS 策略
|
|
||||||
|
|
||||||
### 4. 刷新策略
|
|
||||||
- ✅ 提前刷新(剩余时间 < 5 分钟)
|
|
||||||
- ✅ 失败时清理令牌并重定向登录
|
|
||||||
- ✅ 限制刷新频率
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 1. 常见错误
|
|
||||||
|
|
||||||
#### 401 Unauthorized
|
|
||||||
- **原因**: Access Token 过期或无效
|
|
||||||
- **解决**: 调用刷新接口获取新令牌
|
|
||||||
|
|
||||||
#### 403 Forbidden
|
|
||||||
- **原因**: 权限不足
|
|
||||||
- **解决**: 检查用户角色和中间件配置
|
|
||||||
|
|
||||||
#### 400 Bad Request
|
|
||||||
- **原因**: 请求格式错误
|
|
||||||
- **解决**: 检查请求体和头部
|
|
||||||
|
|
||||||
### 2. 调试命令
|
|
||||||
|
|
||||||
#### 检查令牌有效性
|
|
||||||
```bash
|
|
||||||
# 使用 jq 解码 JWT
|
|
||||||
echo "<token>" | cut -d. -f2 | base64 -d | jq
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 验证令牌签名
|
|
||||||
```bash
|
|
||||||
# 使用 OpenSSL 验证 HMAC
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 日志分析
|
|
||||||
|
|
||||||
#### Go 服务日志
|
|
||||||
```
|
|
||||||
[INFO] Token validated for user: user_id
|
|
||||||
[WARN] Token refresh failed: invalid signature
|
|
||||||
[ERROR] Middleware blocked request: missing authorization
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 前端控制台
|
|
||||||
```
|
|
||||||
Token refreshed successfully
|
|
||||||
Token is expired, attempting refresh...
|
|
||||||
Authentication failed: 401
|
|
||||||
```
|
|
||||||
|
|
||||||
## 密钥管理
|
|
||||||
|
|
||||||
### 1. 生成强随机密钥
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用 OpenSSL 生成 32 字节随机密钥
|
|
||||||
openssl rand -base64 32
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 密钥轮换流程
|
|
||||||
|
|
||||||
1. 生成新密钥
|
|
||||||
2. 更新配置文件
|
|
||||||
3. 同时接受新旧密钥(过渡期)
|
|
||||||
4. 逐步淘汰旧密钥
|
|
||||||
5. 完全切换到新密钥
|
|
||||||
|
|
||||||
### 3. 环境分离
|
|
||||||
|
|
||||||
- **开发环境**: 使用开发专用密钥
|
|
||||||
- **测试环境**: 使用测试专用密钥
|
|
||||||
- **生产环境**: 使用生产密钥(严格保密)
|
|
||||||
|
|
||||||
## 监控和告警
|
|
||||||
|
|
||||||
### 1. 监控指标
|
|
||||||
- Token 刷新成功率
|
|
||||||
- 认证失败次数
|
|
||||||
- Token 过期频率
|
|
||||||
- 并发用户数
|
|
||||||
|
|
||||||
### 2. 告警规则
|
|
||||||
- 认证失败率 > 5%
|
|
||||||
- 连续 3 次刷新失败
|
|
||||||
- Token 解析错误
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
### 1. 缓存策略
|
|
||||||
- 将用户信息缓存在 Redis
|
|
||||||
- 使用本地内存缓存(短期)
|
|
||||||
- 实现分布式缓存(多实例)
|
|
||||||
|
|
||||||
### 2. 令牌预刷新
|
|
||||||
- 前台定时检查令牌剩余时间
|
|
||||||
- 后台预刷新机制
|
|
||||||
- 智能延迟刷新
|
|
||||||
|
|
||||||
## 迁移指南
|
|
||||||
|
|
||||||
### 从旧版迁移
|
|
||||||
|
|
||||||
1. **评估现有系统**
|
|
||||||
- 记录当前认证流程
|
|
||||||
- 识别依赖的 API
|
|
||||||
- 制定迁移计划
|
|
||||||
|
|
||||||
2. **分阶段部署**
|
|
||||||
- 第一阶段:实现新认证模块
|
|
||||||
- 第二阶段:更新 API 端点
|
|
||||||
- 第三阶段:更新前端代码
|
|
||||||
- 第四阶段:移除旧认证
|
|
||||||
|
|
||||||
3. **兼容性**
|
|
||||||
- 同时支持新旧认证
|
|
||||||
- 渐进式切换
|
|
||||||
- 回滚方案
|
|
||||||
|
|
||||||
## 维护任务
|
|
||||||
|
|
||||||
### 日常检查清单
|
|
||||||
- [ ] 检查认证错误日志
|
|
||||||
- [ ] 监控 Token 刷新成功率
|
|
||||||
- [ ] 验证配置一致性
|
|
||||||
- [ ] 测试自动刷新机制
|
|
||||||
|
|
||||||
### 周度任务
|
|
||||||
- [ ] 分析认证统计数据
|
|
||||||
- [ ] 检查密钥轮换计划
|
|
||||||
- [ ] 更新 MFA 备用码
|
|
||||||
|
|
||||||
### 月度任务
|
|
||||||
- [ ] 安全审计
|
|
||||||
- [ ] 性能评估
|
|
||||||
- [ ] 更新文档
|
|
||||||
- [ ] 备份配置
|
|
||||||
|
|
||||||
## 联系信息
|
|
||||||
|
|
||||||
如有问题或需要支持,请联系:
|
|
||||||
|
|
||||||
- **开发团队**: dev@svc.plus
|
|
||||||
- **安全团队**: security@svc.plus
|
|
||||||
- **运维团队**: ops@svc.plus
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档版本**: v1.0
|
|
||||||
**最后更新**: 2025-11-05
|
|
||||||
**维护者**: XControl Team
|
|
||||||
@ -1,343 +0,0 @@
|
|||||||
# Token Auth 双层签发 - 实现总结
|
|
||||||
|
|
||||||
xcontrol-account(Go 后端)路由接口
|
|
||||||
|
|
||||||
Endpoint Method 使用密钥 说明
|
|
||||||
/api/auth/exchange POST publicToken 验证 从公共令牌换取 Access Token
|
|
||||||
/api/auth/refresh POST refreshSecret 签发 刷新 Access Token
|
|
||||||
/api/auth/verify GET accessSecret 验证 验证 Access Token
|
|
||||||
|
|
||||||
|
|
||||||
# xcontrol-account(Go 后端)配置
|
|
||||||
|
|
||||||
auth:
|
|
||||||
enable: true
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2025"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2025"
|
|
||||||
accessSecret: "xcontrol-access-secret-2025"
|
|
||||||
accessExpiry: "1h" # access token 生命周期
|
|
||||||
refreshExpiry: "168h" # refresh token 生命周期 (7 天)
|
|
||||||
|
|
||||||
环境变量加载
|
|
||||||
|
|
||||||
export PUBLIC_TOKEN="xcontrol-public-token-2025"
|
|
||||||
export REFRESH_SECRET="xcontrol-refresh-secret-2025"
|
|
||||||
export ACCESS_SECRET="xcontrol-access-secret-2025"
|
|
||||||
|
|
||||||
# RAG-Sever(Go 后端)配置
|
|
||||||
|
|
||||||
只保留公钥部分:
|
|
||||||
auth:
|
|
||||||
enable: true
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2025"
|
|
||||||
apiBaseUrl: "https://api.svc.plus"
|
|
||||||
authUrl: "https://accounts.svc.plus"
|
|
||||||
|
|
||||||
# dashboard-fresh(Deno 前端)配置
|
|
||||||
✅ 1. config/runtime-service-config.prod.yaml
|
|
||||||
|
|
||||||
只保留公钥部分:
|
|
||||||
|
|
||||||
auth:
|
|
||||||
enable: true
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2025"
|
|
||||||
apiBaseUrl: "https://api.svc.plus"
|
|
||||||
authUrl: "https://accounts.svc.plus"
|
|
||||||
|
|
||||||
|
|
||||||
🚫 不要保存 refreshSecret 或 accessSecret,前端永远不持有私钥。
|
|
||||||
|
|
||||||
## 🎉 完成项目
|
|
||||||
|
|
||||||
本项目成功实现了 **Public + Refresh + JWT access_token** 三层认证机制,涵盖 Go 后端和 Deno 前端。
|
|
||||||
|
|
||||||
## 📁 已创建文件
|
|
||||||
|
|
||||||
### 1. 配置文件更新
|
|
||||||
|
|
||||||
✅ **dashboard-fresh/config/runtime-service-config.base.yaml**
|
|
||||||
- 添加 `auth.token` 配置块
|
|
||||||
- 使用固定 Public Token 和 Refresh Secret
|
|
||||||
|
|
||||||
✅ **account/config/account.yaml**
|
|
||||||
- 添加 `auth.token` 配置块
|
|
||||||
- 与 Dashboard 配置保持一致
|
|
||||||
|
|
||||||
✅ **rag-server/config/server.yaml**
|
|
||||||
- 添加 `auth.token` 配置块
|
|
||||||
- 与其他服务配置一致
|
|
||||||
|
|
||||||
### 2. Go 后端实现 (account/)
|
|
||||||
|
|
||||||
✅ **internal/auth/token_service.go** - 142 行
|
|
||||||
- `TokenService` 结构体
|
|
||||||
- JWT 签发、验证、刷新
|
|
||||||
- Public Token 验证
|
|
||||||
- 支持 MFA 状态
|
|
||||||
|
|
||||||
✅ **internal/auth/mfa_service.go** - 60 行
|
|
||||||
- TOTP 生成和验证
|
|
||||||
- QR 码生成
|
|
||||||
- 备用码管理
|
|
||||||
|
|
||||||
✅ **internal/auth/middleware.go** - 108 行
|
|
||||||
- 身份验证中间件
|
|
||||||
- MFA 验证中间件
|
|
||||||
- 角色验证中间件
|
|
||||||
- 上下文提取函数
|
|
||||||
|
|
||||||
### 3. Go 后端实现 (rag-server/)
|
|
||||||
|
|
||||||
✅ **internal/auth/token_service.go** - 120 行
|
|
||||||
- 适配 RAG 服务的 Token 服务
|
|
||||||
- 服务标识区分
|
|
||||||
|
|
||||||
✅ **internal/auth/middleware.go** - 84 行
|
|
||||||
- 身份验证中间件
|
|
||||||
- 角色验证中间件
|
|
||||||
|
|
||||||
### 4. Deno 前端实现 (dashboard-fresh/)
|
|
||||||
|
|
||||||
✅ **lib/auth/token_service.ts** - 180 行
|
|
||||||
- Token 管理类
|
|
||||||
- 自动令牌刷新
|
|
||||||
- Token 解码和验证
|
|
||||||
- authFetch 包装函数
|
|
||||||
|
|
||||||
✅ **lib/auth/use_auth.ts** - 98 行
|
|
||||||
- React Hook
|
|
||||||
- 登录/登出功能
|
|
||||||
- 自动令牌管理
|
|
||||||
- 角色检查
|
|
||||||
|
|
||||||
### 5. 文档和脚本
|
|
||||||
|
|
||||||
✅ **TOKEN_AUTH_MANUAL.md** - 完整维护手册 (450+ 行)
|
|
||||||
- 架构设计说明
|
|
||||||
- API 接口文档
|
|
||||||
- 安全最佳实践
|
|
||||||
- 故障排除指南
|
|
||||||
- 监控和告警
|
|
||||||
- 维护任务清单
|
|
||||||
|
|
||||||
✅ **IMPLEMENTATION_GUIDE.md** - 实现指南 (200+ 行)
|
|
||||||
- 快速开始
|
|
||||||
- 使用示例
|
|
||||||
- 常见问题
|
|
||||||
- 集成指导
|
|
||||||
|
|
||||||
✅ **scripts/update_token_auth.sh** - 自动更新脚本 (280+ 行)
|
|
||||||
- 生成新密钥
|
|
||||||
- 密钥轮换
|
|
||||||
- 配置验证
|
|
||||||
- 备份管理
|
|
||||||
- 预览模式
|
|
||||||
|
|
||||||
✅ **TOKEN_AUTH_SUMMARY.md** - 本文件
|
|
||||||
|
|
||||||
## 🔑 密钥配置
|
|
||||||
|
|
||||||
所有服务使用统一的密钥配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2024"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2024"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ 架构特性
|
|
||||||
|
|
||||||
### 三层认证机制
|
|
||||||
|
|
||||||
1. **Public Token** (最外层)
|
|
||||||
- 固定值,配置在 YAML 文件中
|
|
||||||
- 用于初次身份验证
|
|
||||||
|
|
||||||
2. **Refresh Token** (中间层)
|
|
||||||
- JWT 格式
|
|
||||||
- 长期有效 (7-30 天)
|
|
||||||
- 用于获取新的 Access Token
|
|
||||||
|
|
||||||
3. **Access Token** (最内层)
|
|
||||||
- JWT 格式
|
|
||||||
- 短期有效 (15-60 分钟)
|
|
||||||
- 用于 API 调用
|
|
||||||
|
|
||||||
### 安全特性
|
|
||||||
|
|
||||||
- ✅ HS256 JWT 签名
|
|
||||||
- ✅ issuer 和 audience 验证
|
|
||||||
- ✅ 自动令牌刷新
|
|
||||||
- ✅ MFA 支持
|
|
||||||
- ✅ 角色基础访问控制
|
|
||||||
- ✅ 过期时间管理
|
|
||||||
|
|
||||||
## 🚀 使用示例
|
|
||||||
|
|
||||||
### Go 服务初始化
|
|
||||||
|
|
||||||
```go
|
|
||||||
tokenService := auth.NewTokenService(auth.TokenConfig{
|
|
||||||
PublicToken: "xcontrol-public-token-2024",
|
|
||||||
RefreshSecret: "xcontrol-refresh-secret-2024",
|
|
||||||
AccessSecret: "xcontrol-access-secret-2024",
|
|
||||||
AccessExpiry: time.Hour,
|
|
||||||
RefreshExpiry: time.Hour * 24 * 7,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用中间件保护路由
|
|
||||||
r.Use(tokenService.AuthMiddleware())
|
|
||||||
```
|
|
||||||
|
|
||||||
### 前端 Hook 使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { user, login, logout } = useAuth();
|
|
||||||
|
|
||||||
// 登录
|
|
||||||
await login('user@example.com', 'password');
|
|
||||||
|
|
||||||
// 自动刷新
|
|
||||||
await tokenService.ensureValidToken();
|
|
||||||
|
|
||||||
// 发起带认证的请求
|
|
||||||
const response = await authFetch('/api/data');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 维护操作
|
|
||||||
|
|
||||||
### 验证配置一致性
|
|
||||||
```bash
|
|
||||||
bash scripts/update_token_auth.sh --validate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 生成新密钥
|
|
||||||
```bash
|
|
||||||
bash scripts/update_token_auth.sh --generate-new
|
|
||||||
```
|
|
||||||
|
|
||||||
### 轮换密钥
|
|
||||||
```bash
|
|
||||||
bash scripts/update_token_auth.sh --rotate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 预览模式
|
|
||||||
```bash
|
|
||||||
bash scripts/update_token_auth.sh --rotate --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 测试结果
|
|
||||||
|
|
||||||
✅ 配置验证通过
|
|
||||||
✅ 脚本运行正常
|
|
||||||
✅ 所有文件创建成功
|
|
||||||
|
|
||||||
## 🔄 后续步骤
|
|
||||||
|
|
||||||
1. **添加依赖**
|
|
||||||
```bash
|
|
||||||
cd account && go mod tidy
|
|
||||||
cd rag-server && go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **集成到现有服务**
|
|
||||||
- 在 API 处理器中注入 `TokenService`
|
|
||||||
- 在路由中应用中间件
|
|
||||||
- 更新配置文件
|
|
||||||
|
|
||||||
3. **前端集成**
|
|
||||||
- 导入 `useAuth` Hook
|
|
||||||
- 包装 API 调用
|
|
||||||
- 处理认证状态
|
|
||||||
|
|
||||||
4. **测试**
|
|
||||||
- 单元测试
|
|
||||||
- 集成测试
|
|
||||||
- 端到端测试
|
|
||||||
|
|
||||||
## 📚 更多文档
|
|
||||||
|
|
||||||
- **完整手册**: `TOKEN_AUTH_MANUAL.md`
|
|
||||||
- **实现指南**: `IMPLEMENTATION_GUIDE.md`
|
|
||||||
- **API 文档**: 见维护手册
|
|
||||||
|
|
||||||
## ✨ 特性亮点
|
|
||||||
|
|
||||||
- 🔐 三层安全认证
|
|
||||||
- 🔄 自动令牌刷新
|
|
||||||
- 🎯 角色基础访问控制
|
|
||||||
- 📱 多因素认证支持
|
|
||||||
- 🛡️ 安全最佳实践
|
|
||||||
- 📖 完整文档和示例
|
|
||||||
- 🔧 自动化维护脚本
|
|
||||||
|
|
||||||
## 📞 支持
|
|
||||||
|
|
||||||
如有问题,请参考:
|
|
||||||
1. 完整维护手册
|
|
||||||
2. 实现指南
|
|
||||||
3. 常见问题解答
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**项目状态**: ✅ 完成
|
|
||||||
**创建日期**: 2025-11-05
|
|
||||||
**版本**: v1.0
|
|
||||||
|
|
||||||
|
|
||||||
实现的功能
|
|
||||||
|
|
||||||
1. 双层签发机制 (JWT + Exchange Endpoint) ✓
|
|
||||||
- Public Token: 客户端标识和认证
|
|
||||||
- Access Token: JWT (HS256) 用于 API 访问
|
|
||||||
- Refresh Token: JWT 用于刷新 access token
|
|
||||||
- Exchange Endpoint: /api/auth/token/exchange - 将 public token 转换为 token 对
|
|
||||||
- Refresh Endpoint: /api/auth/token/refresh - 刷新 access token
|
|
||||||
|
|
||||||
2. 配置支持 ✓
|
|
||||||
- auth.enable: true - 默认开启,可选关闭
|
|
||||||
- auth.token.publicToken - Public token
|
|
||||||
- auth.token.refreshSecret - Refresh token 密钥
|
|
||||||
- auth.token.accessSecret - Access token 密钥
|
|
||||||
- auth.token.accessExpiry: "1h" - Access token 过期时间
|
|
||||||
- auth.token.refreshExpiry: "168h" - Refresh token 过期时间 (7天)
|
|
||||||
|
|
||||||
3. 服务集成 ✓
|
|
||||||
- account 服务: 完整实现 TokenService 和认证中间件
|
|
||||||
- rag-server 服务: 配置已同步
|
|
||||||
- dashboard-fresh 服务: 前端配置已同步
|
|
||||||
|
|
||||||
4. 测试验证 ✓
|
|
||||||
- 所有 dry-run 测试通过 (6/6)
|
|
||||||
- 配置文件一致性验证通过
|
|
||||||
- 更新脚本正常工作
|
|
||||||
|
|
||||||
Commit: 3e4fc9cFiles modified: 7 files, 212 insertions(+), 26 deletions(-)
|
|
||||||
|
|
||||||
API 端点
|
|
||||||
|
|
||||||
- POST /api/auth/token/exchange - 交换 token
|
|
||||||
- POST /api/auth/token/refresh - 刷新 token
|
|
||||||
- POST /api/auth/login - 登录
|
|
||||||
- Protected routes 使用 JWT middleware 认证
|
|
||||||
|
|
||||||
所有功能已实现并测试通过! ✓
|
|
||||||
|
|
||||||
# 总结
|
|
||||||
|
|
||||||
Accounts 是 “造令牌者”;
|
|
||||||
API/ Deno 是 “持令牌者”;
|
|
||||||
RefreshSecret 与 AccessSecret 是“根安全”;
|
|
||||||
PublicToken 是 “门禁卡”;
|
|
||||||
两者通过 /api/auth/exchange 实现零信任连接。
|
|
||||||
|
|
||||||
# 角色定位对照
|
|
||||||
服务 职责 持有密钥 能否签发 Token 是否验证 Token
|
|
||||||
accounts-service (Go) 认证中心 ✅ public + access + refresh ✅ 是 ✅ 是
|
|
||||||
dashboard-fresh (Deno) 前端控制台 ✅ public ❌ 否 ❌ 否(委托后端)
|
|
||||||
rag-server (Go) RAG 后端(中间层 API) ✅ public ❌ 否 ✅ 可验证 access token
|
|
||||||
api-service (Go) 业务服务 ✅ accessSecret ❌ 否 ✅ 是
|
|
||||||
@ -1,482 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultAddr = "127.0.0.1:8080"
|
|
||||||
defaultBodyLimit = 1 << 20 // 1 MiB
|
|
||||||
defaultSessionTTL = 24 * time.Hour
|
|
||||||
defaultRateLimitPerMin = 60
|
|
||||||
cookieName = "accounts_session"
|
|
||||||
)
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
DBUser string
|
|
||||||
DBPassword string
|
|
||||||
DBName string
|
|
||||||
DBSSLMode string
|
|
||||||
BodyLimit int64
|
|
||||||
SessionTTL time.Duration
|
|
||||||
RateLimitRPM int
|
|
||||||
}
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
log *slog.Logger
|
|
||||||
pool *pgxpool.Pool
|
|
||||||
sessions *sessionStore
|
|
||||||
bodyLimit int64
|
|
||||||
limiter *rateLimiter
|
|
||||||
sessionTTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type session struct {
|
|
||||||
userID int64
|
|
||||||
expiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type sessionStore struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
data map[string]session
|
|
||||||
}
|
|
||||||
|
|
||||||
type rateLimiter struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
limit int
|
|
||||||
window time.Duration
|
|
||||||
clients map[string]rateState
|
|
||||||
disabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type rateState struct {
|
|
||||||
count int
|
|
||||||
resetAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type loginRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cfg, err := loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("config error", "err", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
||||||
|
|
||||||
pool, err := openPool(cfg)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("db connection failed", "err", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := pool.Ping(ctx); err != nil {
|
|
||||||
logger.Error("db health check failed", "err", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := &server{
|
|
||||||
log: logger,
|
|
||||||
pool: pool,
|
|
||||||
sessions: newSessionStore(),
|
|
||||||
bodyLimit: cfg.BodyLimit,
|
|
||||||
limiter: newRateLimiter(cfg.RateLimitRPM, time.Minute),
|
|
||||||
sessionTTL: cfg.SessionTTL,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: defaultAddr,
|
|
||||||
Handler: srv.routes(),
|
|
||||||
ReadTimeout: 10 * time.Second,
|
|
||||||
WriteTimeout: 15 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
logger.Info("accounts api listening", "addr", defaultAddr)
|
|
||||||
if err := httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
logger.Error("server failed", "err", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
waitForShutdown(logger, httpServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig() (config, error) {
|
|
||||||
cfg := config{
|
|
||||||
DBUser: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_USER")),
|
|
||||||
DBPassword: os.Getenv("ACCOUNTS_DB_PASSWORD"),
|
|
||||||
DBName: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_NAME")),
|
|
||||||
DBSSLMode: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_SSLMODE")),
|
|
||||||
BodyLimit: defaultBodyLimit,
|
|
||||||
SessionTTL: defaultSessionTTL,
|
|
||||||
RateLimitRPM: defaultRateLimitPerMin,
|
|
||||||
}
|
|
||||||
if cfg.DBSSLMode == "" {
|
|
||||||
cfg.DBSSLMode = "disable"
|
|
||||||
}
|
|
||||||
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_BODY_LIMIT")); v != "" {
|
|
||||||
n, err := strconv.ParseInt(v, 10, 64)
|
|
||||||
if err != nil || n <= 0 {
|
|
||||||
return config{}, fmt.Errorf("invalid ACCOUNTS_BODY_LIMIT: %q", v)
|
|
||||||
}
|
|
||||||
cfg.BodyLimit = n
|
|
||||||
}
|
|
||||||
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_SESSION_TTL")); v != "" {
|
|
||||||
d, err := time.ParseDuration(v)
|
|
||||||
if err != nil || d <= 0 {
|
|
||||||
return config{}, fmt.Errorf("invalid ACCOUNTS_SESSION_TTL: %q", v)
|
|
||||||
}
|
|
||||||
cfg.SessionTTL = d
|
|
||||||
}
|
|
||||||
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_RATE_LIMIT_RPM")); v != "" {
|
|
||||||
if v == "0" {
|
|
||||||
cfg.RateLimitRPM = 0
|
|
||||||
} else {
|
|
||||||
n, err := strconv.Atoi(v)
|
|
||||||
if err != nil || n < 0 {
|
|
||||||
return config{}, fmt.Errorf("invalid ACCOUNTS_RATE_LIMIT_RPM: %q", v)
|
|
||||||
}
|
|
||||||
cfg.RateLimitRPM = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cfg.DBUser == "" || cfg.DBName == "" {
|
|
||||||
return config{}, errors.New("ACCOUNTS_DB_USER and ACCOUNTS_DB_NAME are required")
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openPool(cfg config) (*pgxpool.Pool, error) {
|
|
||||||
dsn := (&url.URL{
|
|
||||||
Scheme: "postgres",
|
|
||||||
User: url.UserPassword(cfg.DBUser, cfg.DBPassword),
|
|
||||||
Host: "127.0.0.1:15432",
|
|
||||||
Path: cfg.DBName,
|
|
||||||
RawQuery: "sslmode=" + url.QueryEscape(cfg.DBSSLMode),
|
|
||||||
}).String()
|
|
||||||
pgxCfg, err := pgxpool.ParseConfig(dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pgxCfg.MaxConns = 10
|
|
||||||
pgxCfg.MinConns = 2
|
|
||||||
pgxCfg.MaxConnIdleTime = 5 * time.Minute
|
|
||||||
pgxCfg.MaxConnLifetime = 30 * time.Minute
|
|
||||||
return pgxpool.NewWithConfig(context.Background(), pgxCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForShutdown(logger *slog.Logger, httpServer *http.Server) {
|
|
||||||
signals := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-signals
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
logger.Info("shutting down")
|
|
||||||
if err := httpServer.Shutdown(ctx); err != nil {
|
|
||||||
logger.Error("shutdown error", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) routes() http.Handler {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/api/login", s.handleLogin)
|
|
||||||
mux.HandleFunc("/api/logout", s.handleLogout)
|
|
||||||
mux.HandleFunc("/api/me", s.handleMe)
|
|
||||||
return s.middleware(mux)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) middleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
wrapped := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
|
||||||
|
|
||||||
if s.limiter != nil {
|
|
||||||
if ok := s.limiter.Allow(clientIP(r)); !ok {
|
|
||||||
writeJSON(wrapped, http.StatusTooManyRequests, map[string]string{"error": "rate_limited"})
|
|
||||||
s.logRequest(r, wrapped.status, start)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Body != nil && s.bodyLimit > 0 {
|
|
||||||
r.Body = http.MaxBytesReader(wrapped, r.Body, s.bodyLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(wrapped, r)
|
|
||||||
s.logRequest(r, wrapped.status, start)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req loginRequest
|
|
||||||
if err := decodeJSON(r, &req); err != nil {
|
|
||||||
if isBodyTooLarge(err) {
|
|
||||||
writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "body_too_large"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_json"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
|
||||||
if email == "" || req.Password == "" {
|
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "email_and_password_required"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
var (
|
|
||||||
userID int64
|
|
||||||
passwordHash string
|
|
||||||
)
|
|
||||||
err := s.pool.QueryRow(ctx, "SELECT id, password_hash FROM users WHERE email=$1", email).Scan(&userID, &passwordHash)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
s.log.Error("login query failed", "err", err)
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid_credentials"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)) != nil {
|
|
||||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid_credentials"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, err := generateToken(32)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error("session token generation failed", "err", err)
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
expiresAt := time.Now().Add(s.sessionTTL)
|
|
||||||
s.sessions.Set(sessionID, session{userID: userID, expiresAt: expiresAt})
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: sessionID,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Expires: expiresAt,
|
|
||||||
})
|
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cookie, err := r.Cookie(cookieName); err == nil && cookie.Value != "" {
|
|
||||||
s.sessions.Delete(cookie.Value)
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
MaxAge: -1,
|
|
||||||
})
|
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) handleMe(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cookie, err := r.Cookie(cookieName)
|
|
||||||
if err != nil || cookie.Value == "" {
|
|
||||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sess, ok := s.sessions.Get(cookie.Value)
|
|
||||||
if !ok || time.Now().After(sess.expiresAt) {
|
|
||||||
s.sessions.Delete(cookie.Value)
|
|
||||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
var user userResponse
|
|
||||||
err = s.pool.QueryRow(ctx, "SELECT id, email, created_at FROM users WHERE id=$1", sess.userID).
|
|
||||||
Scan(&user.ID, &user.Email, &user.CreatedAt)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
s.log.Error("me query failed", "err", err)
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"user": user})
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeJSON(r *http.Request, dst any) error {
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
if err := decoder.Decode(dst); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if decoder.More() {
|
|
||||||
return errors.New("extra json fields")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBodyTooLarge(err error) bool {
|
|
||||||
var maxErr *http.MaxBytesError
|
|
||||||
return errors.As(err, &maxErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
if payload != nil {
|
|
||||||
_ = json.NewEncoder(w).Encode(payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateToken(size int) (string, error) {
|
|
||||||
buf := make([]byte, size)
|
|
||||||
if _, err := rand.Read(buf); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.RawURLEncoding.EncodeToString(buf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSessionStore() *sessionStore {
|
|
||||||
return &sessionStore{data: make(map[string]session)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sessionStore) Get(token string) (session, bool) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
val, ok := s.data[token]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sessionStore) Set(token string, sess session) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.data[token] = sess
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sessionStore) Delete(token string) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
delete(s.data, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRateLimiter(limit int, window time.Duration) *rateLimiter {
|
|
||||||
if limit <= 0 {
|
|
||||||
return &rateLimiter{disabled: true}
|
|
||||||
}
|
|
||||||
return &rateLimiter{
|
|
||||||
limit: limit,
|
|
||||||
window: window,
|
|
||||||
clients: make(map[string]rateState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rateLimiter) Allow(ip string) bool {
|
|
||||||
if r == nil || r.disabled {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
state := r.clients[ip]
|
|
||||||
if state.resetAt.IsZero() || now.After(state.resetAt) {
|
|
||||||
state.resetAt = now.Add(r.window)
|
|
||||||
state.count = 0
|
|
||||||
}
|
|
||||||
if state.count >= r.limit {
|
|
||||||
r.clients[ip] = state
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
state.count++
|
|
||||||
r.clients[ip] = state
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type statusWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
status int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *statusWriter) WriteHeader(status int) {
|
|
||||||
w.status = status
|
|
||||||
w.ResponseWriter.WriteHeader(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) logRequest(r *http.Request, status int, start time.Time) {
|
|
||||||
s.log.Info("request",
|
|
||||||
"method", r.Method,
|
|
||||||
"path", r.URL.Path,
|
|
||||||
"status", status,
|
|
||||||
"latency", time.Since(start),
|
|
||||||
"ip", clientIP(r),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clientIP(r *http.Request) string {
|
|
||||||
if r == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
||||||
parts := strings.Split(xff, ",")
|
|
||||||
return strings.TrimSpace(parts[0])
|
|
||||||
}
|
|
||||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
@ -256,9 +256,6 @@ func runServer(ctx context.Context, cfg *config.Config, logger *slog.Logger) err
|
|||||||
if addr == "" {
|
if addr == "" {
|
||||||
addr = ":8080"
|
addr = ":8080"
|
||||||
}
|
}
|
||||||
if port := strings.TrimSpace(os.Getenv("PORT")); port != "" {
|
|
||||||
addr = "0.0.0.0:" + port
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsSettings := cfg.Server.TLS
|
tlsSettings := cfg.Server.TLS
|
||||||
certFile := strings.TrimSpace(tlsSettings.CertFile)
|
certFile := strings.TrimSpace(tlsSettings.CertFile)
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultMigrationDir = "sql/migrations"
|
defaultMigrationDir = "account/sql/migrations"
|
||||||
defaultSchemaFile = "sql/schema.sql"
|
defaultSchemaFile = "account/sql/schema.sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ xray:
|
|||||||
enabled: true
|
enabled: true
|
||||||
interval: 5m
|
interval: 5m
|
||||||
outputPath: "/usr/local/etc/xray/config.json"
|
outputPath: "/usr/local/etc/xray/config.json"
|
||||||
templatePath: "config/xray.config.template.json"
|
templatePath: "account/config/xray.config.template.json"
|
||||||
validateCommand: []
|
validateCommand: []
|
||||||
restartCommand:
|
restartCommand:
|
||||||
- "systemctl"
|
- "systemctl"
|
||||||
|
|||||||
@ -59,7 +59,7 @@ xray:
|
|||||||
enabled: false
|
enabled: false
|
||||||
interval: 5m
|
interval: 5m
|
||||||
outputPath: "/usr/local/etc/xray/config.json"
|
outputPath: "/usr/local/etc/xray/config.json"
|
||||||
templatePath: "config/xray.config.template.json"
|
templatePath: "account/config/xray.config.template.json"
|
||||||
validateCommand: []
|
validateCommand: []
|
||||||
restartCommand:
|
restartCommand:
|
||||||
- "systemctl"
|
- "systemctl"
|
||||||
|
|||||||
@ -14,7 +14,7 @@ auth:
|
|||||||
refreshExpiry: "168h"
|
refreshExpiry: "168h"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
addr: "0.0.0.0:8080"
|
addr: ":8080"
|
||||||
readTimeout: 15s
|
readTimeout: 15s
|
||||||
writeTimeout: 15s
|
writeTimeout: 15s
|
||||||
publicUrl: "https://accounts.svc.plus"
|
publicUrl: "https://accounts.svc.plus"
|
||||||
@ -69,7 +69,7 @@ xray:
|
|||||||
enabled: false
|
enabled: false
|
||||||
interval: 5m
|
interval: 5m
|
||||||
outputPath: "/usr/local/etc/xray/config.json"
|
outputPath: "/usr/local/etc/xray/config.json"
|
||||||
templatePath: "config/xray.config.template.json"
|
templatePath: "account/config/xray.config.template.json"
|
||||||
validateCommand: []
|
validateCommand: []
|
||||||
restartCommand:
|
restartCommand:
|
||||||
- "systemctl"
|
- "systemctl"
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./cms.schema.json",
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "marketing-landing",
|
|
||||||
"entry": "templates/marketing/index.tsx",
|
|
||||||
"description": "Default landing page for campaign microsites.",
|
|
||||||
"previewPath": "previews/marketing-landing.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "docs-home",
|
|
||||||
"entry": "templates/docs/home.tsx",
|
|
||||||
"description": "Documentation homepage wiring search, changelog and highlights."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme": {
|
|
||||||
"name": "xcontrol-galaxy",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "XControl Design Systems",
|
|
||||||
"variables": {
|
|
||||||
"primaryColor": "#4055ff",
|
|
||||||
"accentColor": "#39c2f0",
|
|
||||||
"fontFamily": "Inter, system-ui, sans-serif"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extensions": [
|
|
||||||
{
|
|
||||||
"name": "search",
|
|
||||||
"package": "@xcontrol/cms-extension-search",
|
|
||||||
"enabled": true,
|
|
||||||
"config": {
|
|
||||||
"provider": "algolia",
|
|
||||||
"indexName": "xcontrol_docs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ab-testing",
|
|
||||||
"package": "@xcontrol/cms-extension-experiments",
|
|
||||||
"enabled": false,
|
|
||||||
"config": {
|
|
||||||
"allocation": "5%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contentSources": [
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"name": "marketing-site",
|
|
||||||
"readOnly": false,
|
|
||||||
"options": {
|
|
||||||
"remote": "git@github.com:xcontrol/marketing-site.git",
|
|
||||||
"branch": "main",
|
|
||||||
"contentPath": "content/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "filesystem",
|
|
||||||
"name": "product-docs",
|
|
||||||
"readOnly": true,
|
|
||||||
"options": {
|
|
||||||
"path": "../docs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"deployment": {
|
|
||||||
"preview": true,
|
|
||||||
"defaultLocale": "en-US"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
{
|
|
||||||
"$id": "https://xcontrol.dev/schemas/cms.schema.json",
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "XControl CMS Configuration",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"templates",
|
|
||||||
"theme",
|
|
||||||
"extensions",
|
|
||||||
"contentSources"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"templates": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"entry"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"description": "The relative path to the template entry point.",
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"previewPath": {
|
|
||||||
"description": "Optional static preview asset path.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"version"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"variables": {
|
|
||||||
"description": "Theme tokens exposed to templates.",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"number",
|
|
||||||
"boolean"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extensions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"package"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"package": {
|
|
||||||
"description": "Resolvable package name or path.",
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contentSources": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"options"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"git",
|
|
||||||
"filesystem",
|
|
||||||
"api",
|
|
||||||
"database"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"description": "Source specific configuration payload.",
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"readOnly": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deployment": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"preview": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"defaultLocale": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Optional JSON Schema declaration for tooling support."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -156,12 +156,12 @@ type AgentCredential struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load reads the configuration file at the provided path. When path is empty,
|
// Load reads the configuration file at the provided path. When path is empty,
|
||||||
// it defaults to config/account.yaml. If the file does not exist an
|
// it defaults to account/config/account.yaml. If the file does not exist an
|
||||||
// empty configuration is returned.
|
// empty configuration is returned.
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
p := path
|
p := path
|
||||||
if p == "" {
|
if p == "" {
|
||||||
p = filepath.Join("config", "account.yaml")
|
p = filepath.Join("account", "config", "account.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := os.ReadFile(p)
|
b, err := os.ReadFile(p)
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
[defaults]
|
|
||||||
roles_path = roles
|
|
||||||
stdout_callback = yaml
|
|
||||||
host_key_checking = False
|
|
||||||
callback_plugins = callback_plugins
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"""Ansible callback plugin providing a short summary of skipped tasks.
|
|
||||||
|
|
||||||
This implements a small subset of behaviour expected by the CI pipeline
|
|
||||||
which references the historic ``skippy`` plugin. The implementation keeps
|
|
||||||
track of the names of skipped tasks during the playbook run and prints a
|
|
||||||
concise summary at the end. The plugin is intentionally lightweight so
|
|
||||||
that it can run in environments where the original plugin is unavailable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ansible.plugins.callback import CallbackBase
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
|
||||||
"""Collect skipped tasks and report them at the end of the playbook."""
|
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
|
||||||
CALLBACK_TYPE = "notification"
|
|
||||||
CALLBACK_NAME = "skippy"
|
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self._skipped_tasks: list[str] = []
|
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result) -> None: # type: ignore[override]
|
|
||||||
"""Record the name of tasks skipped during execution."""
|
|
||||||
|
|
||||||
task_name = result._task.get_name() # pylint: disable=protected-access
|
|
||||||
if task_name:
|
|
||||||
self._skipped_tasks.append(task_name)
|
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats) -> None: # type: ignore[override]
|
|
||||||
"""Display a summary of skipped tasks at the end of the playbook."""
|
|
||||||
|
|
||||||
if not self._skipped_tasks:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._display.banner("Skipped tasks")
|
|
||||||
for task in self._skipped_tasks:
|
|
||||||
self._display.display(f"- {task}")
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Placeholder OpenResty deployment
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "Applying OpenResty configuration for {{ inventory_hostname }}"
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Placeholder PostgreSQL deployment
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "Applying PostgreSQL configuration for {{ inventory_hostname }}"
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Placeholder Redis deployment
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "Applying Redis configuration for {{ inventory_hostname }}"
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Provision homepage vhosts
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
gather_facts: false
|
|
||||||
roles:
|
|
||||||
- vhosts/OpenResty
|
|
||||||
- vhosts/Redis
|
|
||||||
- vhosts/Postgresql
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Deploy XControl on VM
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
tasks:
|
|
||||||
- name: Install Docker
|
|
||||||
apt:
|
|
||||||
name: docker.io
|
|
||||||
state: present
|
|
||||||
update_cache: yes
|
|
||||||
|
|
||||||
- name: Ensure Docker service is running
|
|
||||||
service:
|
|
||||||
name: docker
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Run XControl container
|
|
||||||
community.docker.docker_container:
|
|
||||||
name: xcontrol
|
|
||||||
image: ghcr.io/example/xcontrol:latest
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
env:
|
|
||||||
KB_DSN: "postgres://user:pass@db:5432/xcontrol?sslmode=disable"
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
# Base container images
|
|
||||||
|
|
||||||
This directory provides Dockerfiles for the foundational images used across the
|
|
||||||
project. Each image is designed to keep commonly reused dependencies bundled so
|
|
||||||
service-specific images can build faster and remain consistent.
|
|
||||||
|
|
||||||
## Available images
|
|
||||||
|
|
||||||
- **OpenResty + GeoIP** (`openresty-geoip.Dockerfile`): OpenResty with GeoIP2
|
|
||||||
libraries and `lua-resty-maxminddb` for MaxMind database lookups.
|
|
||||||
- **PostgreSQL 16 + extensions** (`postgres-extensions.Dockerfile`): PostgreSQL
|
|
||||||
with `pgvector`, `pg_jieba`, and `pg_cache` compiled into the server for
|
|
||||||
vector search and full-text tokenization.
|
|
||||||
- **Go 1.23 builder** (`go-builder.Dockerfile`): Ubuntu 24.04 with the Go
|
|
||||||
toolchain and build dependencies for the Account service and RAG server.
|
|
||||||
- **Go runtime** (`go-runtime.Dockerfile`): Slim Ubuntu 24.04 runtime with CA
|
|
||||||
certificates for running statically linked Go binaries.
|
|
||||||
- **Node.js builder** (`node-builder.Dockerfile`): Node.js 22 with Yarn, the
|
|
||||||
latest npm, and build essentials for compiling native Next.js dependencies.
|
|
||||||
- **Node.js runtime** (`node-runtime.Dockerfile`): Slim Node.js 22 runtime ready
|
|
||||||
for production Next.js deployments.
|
|
||||||
|
|
||||||
## Build commands
|
|
||||||
|
|
||||||
You can build all base images at once via the repository `Makefile`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build-base-images
|
|
||||||
```
|
|
||||||
|
|
||||||
Or build individual images manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# OpenResty with GeoIP
|
|
||||||
make docker-openresty-geoip
|
|
||||||
|
|
||||||
# PostgreSQL 16 with extensions
|
|
||||||
make docker-postgres-extensions
|
|
||||||
|
|
||||||
# Node.js builder (Node 22 + Yarn)
|
|
||||||
make docker-node-builder
|
|
||||||
|
|
||||||
# Node.js 22 runtime
|
|
||||||
make docker-node-runtime
|
|
||||||
```
|
|
||||||
|
|
||||||
Each target accepts an optional tag override, for example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make docker-postgres-extensions POSTGRES_EXT_IMAGE=my-registry/postgres-extensions:16
|
|
||||||
|
|
||||||
# Go builder (Go 1.23 + build tools)
|
|
||||||
make docker-go-builder GO_BUILDER_IMAGE=my-registry/go-builder:1.23
|
|
||||||
|
|
||||||
# Go runtime (Ubuntu 24.04 + CA certificates)
|
|
||||||
make docker-go-runtime GO_RUNTIME_IMAGE=my-registry/go-runtime:1.23
|
|
||||||
```
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# =======================================================
|
|
||||||
# XControl Go Runtime Base Image
|
|
||||||
# - 用于所有静态编译的 Go 服务
|
|
||||||
# - 可选安装 Go SDK(用于 build 阶段)
|
|
||||||
# - 多架构安全(amd64/arm64 自动识别)
|
|
||||||
# =======================================================
|
|
||||||
|
|
||||||
FROM golang:1.25
|
|
||||||
|
|
||||||
LABEL maintainer="XControl" \
|
|
||||||
org.opencontainers.image.title="go-runtime" \
|
|
||||||
org.opencontainers.image.description="APP runtime base for golang:1.25 with TLS certificates + optional Go SDK" \
|
|
||||||
org.opencontainers.image.licenses="Apache-2.0"
|
|
||||||
|
|
||||||
# ---- Runtime 基础环境 ----
|
|
||||||
ENV CGO_ENABLED=0 \
|
|
||||||
TZ=Etc/UTC
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
wget \
|
|
||||||
tar; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
CMD ["/bin/sh"]
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
# Mail Stack – Chasquid + Dovecot + Certbot (Split Containers)
|
|
||||||
|
|
||||||
架构图
|
|
||||||
```
|
|
||||||
INBOUND EMAIL
|
|
||||||
↓ 25 (SMTP)
|
|
||||||
+-----------+
|
|
||||||
INTERNET →→→→→ | chasquid | →→→ outbound relay (optional)
|
|
||||||
+-----------+
|
|
||||||
↑ 587 (STARTTLS) | 465 (TLS)
|
|
||||||
| |
|
|
||||||
CLIENTS -----------------+
|
|
||||||
\----→ dovecot →→ IMAP 993 / POP SSL 995
|
|
||||||
↑
|
|
||||||
chasquid → dovecot-auth → 用户认证
|
|
||||||
```
|
|
||||||
|
|
||||||
# Mail Stack: Chasquid + Dovecot + Certbot
|
|
||||||
|
|
||||||
This stack provides:
|
|
||||||
|
|
||||||
- SMTP (25)
|
|
||||||
- Submission (587)
|
|
||||||
- SMTPS (465)
|
|
||||||
- IMAPS (993)
|
|
||||||
|
|
||||||
Certbot (TLS) and nginx (ACME validation) use **official images**.
|
|
||||||
|
|
||||||
|
|
||||||
Certbot (TLS) and nginx (ACME validation) use **official images**.
|
|
||||||
|
|
||||||
## Start
|
|
||||||
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
## Initialize user:
|
|
||||||
|
|
||||||
docker exec chasquid chasquid-util domain-add svc.plus
|
|
||||||
docker exec chasquid chasquid-util user-add admin@svc.plus
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
FROM alpine:3.20
|
|
||||||
|
|
||||||
RUN apk add --no-cache chasquid bash ca-certificates tzdata openssl shadow
|
|
||||||
|
|
||||||
WORKDIR /chasquid
|
|
||||||
|
|
||||||
COPY config/ /etc/chasquid-tmpl/
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 25 465 587
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
hostname = "{{MAIL_HOSTNAME}}"
|
|
||||||
|
|
||||||
submission_address = ":587"
|
|
||||||
smtps_address = ":465"
|
|
||||||
|
|
||||||
dovecot_auth = true
|
|
||||||
|
|
||||||
tls {
|
|
||||||
cert_file = "/etc/chasquid/certs/fullchain.pem"
|
|
||||||
key_file = "/etc/chasquid/certs/privkey.pem"
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
MAIL_HOSTNAME=${MAIL_HOSTNAME:-smtp.svc.plus}
|
|
||||||
CERT_DIR="/etc/letsencrypt/live/$MAIL_HOSTNAME"
|
|
||||||
CERT_DST="/etc/chasquid/certs"
|
|
||||||
|
|
||||||
mkdir -p $CERT_DST
|
|
||||||
|
|
||||||
while [[ ! -f "$CERT_DIR/fullchain.pem" ]]; do
|
|
||||||
echo "[chasquid] Waiting for TLS cert..."
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
ln -sf $CERT_DIR/fullchain.pem $CERT_DST/fullchain.pem
|
|
||||||
ln -sf $CERT_DIR/privkey.pem $CERT_DST/privkey.pem
|
|
||||||
chmod 640 $CERT_DST/* || true
|
|
||||||
|
|
||||||
envsubst < /etc/chasquid-tmpl/chasquid.conf.tmpl > /etc/chasquid/chasquid.conf
|
|
||||||
|
|
||||||
echo "[chasquid] Starting..."
|
|
||||||
exec chasquid
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
volumes:
|
|
||||||
- ./certbot/www:/var/www/certbot
|
|
||||||
- letsencrypt:/etc/letsencrypt
|
|
||||||
- ./nginx-default.conf:/etc/nginx/conf.d/default.conf
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
certbot:
|
|
||||||
image: certbot/certbot:latest
|
|
||||||
command: certonly --webroot -w /var/www/certbot \
|
|
||||||
-d smtp.svc.plus \
|
|
||||||
--non-interactive --agree-tos \
|
|
||||||
-m admin@svc.plus
|
|
||||||
volumes:
|
|
||||||
- ./certbot/www:/var/www/certbot
|
|
||||||
- letsencrypt:/etc/letsencrypt
|
|
||||||
depends_on:
|
|
||||||
- nginx
|
|
||||||
|
|
||||||
chasquid:
|
|
||||||
build: ./chasquid
|
|
||||||
environment:
|
|
||||||
MAIL_HOSTNAME: smtp.svc.plus
|
|
||||||
volumes:
|
|
||||||
- letsencrypt:/etc/letsencrypt
|
|
||||||
ports:
|
|
||||||
- "25:25"
|
|
||||||
- "465:465"
|
|
||||||
- "587:587"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
dovecot:
|
|
||||||
build: ./dovecot
|
|
||||||
environment:
|
|
||||||
MAIL_HOSTNAME: smtp.svc.plus
|
|
||||||
volumes:
|
|
||||||
- letsencrypt:/etc/letsencrypt
|
|
||||||
ports:
|
|
||||||
- "993:993"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
letsencrypt:
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
FROM alpine:3.20
|
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
dovecot dovecot-lmtpd dovecot-pigeonhole-plugin \
|
|
||||||
bash ca-certificates tzdata openssl
|
|
||||||
|
|
||||||
WORKDIR /dovecot
|
|
||||||
|
|
||||||
COPY config/ /etc/dovecot-tmpl/
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 993
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
protocol imap {
|
|
||||||
mail_plugins = $mail_plugins
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
protocols = imap pop3
|
|
||||||
ssl = required
|
|
||||||
|
|
||||||
ssl_cert = </etc/letsencrypt/live/{{MAIL_HOSTNAME}}/fullchain.pem
|
|
||||||
ssl_key = </etc/letsencrypt/live/{{MAIL_HOSTNAME}}/privkey.pem
|
|
||||||
|
|
||||||
auth_mechanisms = plain login
|
|
||||||
disable_plaintext_auth = yes
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
service auth {
|
|
||||||
unix_listener auth-userdb {
|
|
||||||
mode = 0660
|
|
||||||
user = chasquid
|
|
||||||
group = chasquid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imaps {
|
|
||||||
port = 993
|
|
||||||
ssl = yes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
MAIL_HOSTNAME=${MAIL_HOSTNAME:-smtp.svc.plus}
|
|
||||||
CERT_DIR="/etc/letsencrypt/live/$MAIL_HOSTNAME"
|
|
||||||
|
|
||||||
while [[ ! -f "$CERT_DIR/fullchain.pem" ]]; do
|
|
||||||
echo "[dovecot] Waiting for TLS cert..."
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
envsubst < /etc/dovecot-tmpl/dovecot.conf.tmpl > /etc/dovecot/dovecot.conf
|
|
||||||
envsubst < /etc/dovecot-tmpl/local.conf.tmpl > /etc/dovecot/local.conf
|
|
||||||
envsubst < /etc/dovecot-tmpl/10-master.conf.tmpl > /etc/dovecot/conf.d/10-master.conf
|
|
||||||
|
|
||||||
echo "[dovecot] Starting..."
|
|
||||||
exec dovecot -F
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
FROM node:22-bookworm
|
|
||||||
|
|
||||||
LABEL maintainer="XControl" \
|
|
||||||
description="Node.js 22 builder image with Yarn and Next.js tooling"
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
corepack enable; \
|
|
||||||
corepack prepare yarn@stable --activate; \
|
|
||||||
npm install -g npm@latest; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
python3 \
|
|
||||||
ca-certificates; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
CMD ["bash"]
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
FROM node:22-slim
|
|
||||||
|
|
||||||
LABEL maintainer="XControl" \
|
|
||||||
description="Slim Node.js 22 runtime for production Next.js deployments"
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates; \
|
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
|
||||||
corepack enable
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
CMD ["node"]
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
FROM openresty/openresty:1.27.1.2-5-bookworm
|
|
||||||
|
|
||||||
LABEL maintainer="XControl" \
|
|
||||||
description="OpenResty base image with GeoIP2 libraries and lua-resty-maxminddb"
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends ca-certificates libmaxminddb0 libmaxminddb-dev mmdb-bin luarocks; \
|
|
||||||
apt-get install -y --only-upgrade libpam-modules libpam-modules-bin libpam-runtime libpam0g zlib1g; \
|
|
||||||
apt-get purge -y --auto-remove git luarocks; \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# OpenResty 配置(nginx.conf, conf.d/*.conf, lua/)
|
|
||||||
VOLUME ["/etc/openresty/conf"]
|
|
||||||
|
|
||||||
# GeoIP 数据目录(mmdb 文件)
|
|
||||||
VOLUME ["/usr/local/openresty/geoip"]
|
|
||||||
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
# ---------------------------------------------------------
|
|
||||||
# Version Definitions (Can be overridden by build args)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
ARG PG_MAJOR=16
|
|
||||||
ARG PG_VERSION=16.4
|
|
||||||
|
|
||||||
# Extension versions
|
|
||||||
ARG PG_JIEBA_VERSION=v2.0.1 # or commit SHA
|
|
||||||
ARG PG_VECTOR_VERSION=v0.8.1
|
|
||||||
ARG PGMQ_VERSION=v1.8.0
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Stage 0 — Base with PGDG Repository
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
FROM ubuntu:24.04 AS pgdg-base
|
|
||||||
ARG PG_MAJOR
|
|
||||||
ARG PG_VERSION
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
wget curl gnupg ca-certificates lsb-release unzip; \
|
|
||||||
mkdir -p /usr/share/keyrings; \
|
|
||||||
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
|
||||||
| gpg --dearmor >/usr/share/keyrings/pgdg.gpg; \
|
|
||||||
echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] \
|
|
||||||
http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
|
|
||||||
> /etc/apt/sources.list.d/pgdg.list; \
|
|
||||||
apt-get update;
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Stage 1 — Build Extensions (pg_jieba + pgmq + pgvector)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
FROM pgdg-base AS builder
|
|
||||||
ARG PG_MAJOR
|
|
||||||
ARG PG_JIEBA_VERSION
|
|
||||||
ARG PG_VECTOR_VERSION
|
|
||||||
ARG PGMQ_VERSION
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
git \
|
|
||||||
pkg-config \
|
|
||||||
libicu-dev \
|
|
||||||
postgresql-server-dev-${PG_MAJOR}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Build pg_jieba
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
RUN tmp=$(mktemp -d) && \
|
|
||||||
git clone --branch "${PG_JIEBA_VERSION}" \
|
|
||||||
https://github.com/jaiminpan/pg_jieba.git "$tmp/pg_jieba" && \
|
|
||||||
cd "$tmp/pg_jieba" && \
|
|
||||||
git submodule update --init --recursive || true && \
|
|
||||||
ln -s "$tmp/pg_jieba/third_party/cppjieba" "$tmp/pg_jieba/cppjieba" && \
|
|
||||||
cmake -S "$tmp/pg_jieba" \
|
|
||||||
-B "$tmp/pg_jieba/build" \
|
|
||||||
-DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql/${PG_MAJOR}/server && \
|
|
||||||
cmake --build "$tmp/pg_jieba/build" --config Release -- -j"$(nproc)" && \
|
|
||||||
cmake --install "$tmp/pg_jieba/build" && \
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Build pgmq
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
RUN tmp=$(mktemp -d) && \
|
|
||||||
git clone --depth 1 --branch "${PGMQ_VERSION}" \
|
|
||||||
https://github.com/tembo-io/pgmq.git "$tmp/pgmq" && \
|
|
||||||
cd "$tmp/pgmq/pgmq-extension" && \
|
|
||||||
make && make install && \
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Build pgvector
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
RUN tmp=$(mktemp -d) && \
|
|
||||||
git clone --depth 1 --branch "${PG_VECTOR_VERSION}" \
|
|
||||||
https://github.com/pgvector/pgvector.git "$tmp/pgvector" && \
|
|
||||||
cd "$tmp/pgvector" && \
|
|
||||||
make && make install && \
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Stage 2 — Runtime
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
FROM pgdg-base AS runtime
|
|
||||||
ARG PG_MAJOR
|
|
||||||
ARG PG_VERSION
|
|
||||||
|
|
||||||
LABEL maintainer="Cloud-Neutral Toolkit" \
|
|
||||||
description="PostgreSQL ${PG_VERSION} + pgvector + pg_jieba + pgmq"
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
postgresql-${PG_MAJOR} \
|
|
||||||
postgresql-client-${PG_MAJOR} \
|
|
||||||
postgresql-contrib-${PG_MAJOR}; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Copy .so + extension files from builder
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/ \
|
|
||||||
/usr/lib/postgresql/${PG_MAJOR}/lib/
|
|
||||||
|
|
||||||
COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/extension/ \
|
|
||||||
/usr/share/postgresql/${PG_MAJOR}/extension/
|
|
||||||
|
|
||||||
USER postgres
|
|
||||||
EXPOSE 5432
|
|
||||||
|
|
||||||
CMD ["postgres"]
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# TL;DR – PostgreSQL Multi-Model Runtime
|
|
||||||
|
|
||||||
A lightweight PostgreSQL build providing **Search + Vector + KV + MQ + JSONB**
|
|
||||||
as a unified data engine for CloudNeutral-Suite applications.
|
|
||||||
|
|
||||||
## Includes
|
|
||||||
- **pg_jieba** – Chinese full-text tokenizer
|
|
||||||
- **pg_trgm** – fuzzy search and typo tolerance
|
|
||||||
- **pgvector** – embeddings and semantic search
|
|
||||||
- **pgmq** – lightweight message queue (Kafka-lite)
|
|
||||||
- **JSONB + GIN** – document store and structured filtering
|
|
||||||
- **hstore + UNLOGGED tables** – high-speed key/value cache
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
- Documentation, product, and FAQ search
|
|
||||||
- RAG and embedding-based retrieval
|
|
||||||
- Application-level KV/session/cache
|
|
||||||
- Lightweight event queues and workflows
|
|
||||||
- JSONB content and metadata storage
|
|
||||||
- Hybrid keyword + semantic search
|
|
||||||
|
|
||||||
## Not Included
|
|
||||||
Platform-level or DBA-oriented extensions are intentionally excluded:
|
|
||||||
- timescaledb
|
|
||||||
- pg_partman
|
|
||||||
- pg_cron
|
|
||||||
- pg_net
|
|
||||||
|
|
||||||
## Why
|
|
||||||
Keeps the runtime focused, predictable, and portable —
|
|
||||||
a single ACID engine replacing MongoDB + Redis + Kafka + Elasticsearch + Pinecone
|
|
||||||
for application-scale workloads.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
# Replace with your ops mailbox for ACME.
|
|
||||||
email ops@example.com
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts.svc.plus {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
# Account service upstream (plain HTTP inside).
|
|
||||||
reverse_proxy 127.0.0.1:8080
|
|
||||||
|
|
||||||
# Optional: keep HSTS managed in a single place.
|
|
||||||
# header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
accounts.svc.plus {
|
|
||||||
@api path /api/*
|
|
||||||
reverse_proxy @api 127.0.0.1:8080
|
|
||||||
|
|
||||||
handle {
|
|
||||||
reverse_proxy 127.0.0.1:3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: xcontrol
|
|
||||||
version: 0.1.0
|
|
||||||
description: XControl platform chart
|
|
||||||
type: application
|
|
||||||
appVersion: "1.0"
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: xcontrol
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: xcontrol
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: xcontrol
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: xcontrol
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
env:
|
|
||||||
{{- if .Values.postgresql.enabled }}
|
|
||||||
- name: KB_DSN
|
|
||||||
value: "postgres://{{ .Values.postgresql.auth.user }}:{{ .Values.postgresql.auth.password }}@xcontrol-postgres:5432/{{ .Values.postgresql.auth.database }}?sslmode=disable"
|
|
||||||
{{- else if .Values.externalPostgresql.enabled }}
|
|
||||||
- name: KB_DSN
|
|
||||||
value: "postgres://{{ .Values.externalPostgresql.user }}:{{ .Values.externalPostgresql.password }}@{{ .Values.externalPostgresql.host }}/{{ .Values.externalPostgresql.database }}"
|
|
||||||
{{- end }}
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
{{- if .Values.postgresql.enabled }}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: xcontrol-postgres
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: xcontrol-postgres
|
|
||||||
ports:
|
|
||||||
- port: 5432
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: xcontrol-postgres
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: xcontrol-postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: xcontrol-postgres
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: postgres
|
|
||||||
image: {{ .Values.postgresql.image }}
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: {{ .Values.postgresql.auth.database }}
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: {{ .Values.postgresql.auth.user }}
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
value: {{ .Values.postgresql.auth.password }}
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
emptyDir: {}
|
|
||||||
{{- end }}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: xcontrol
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
selector:
|
|
||||||
app: xcontrol
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: 8080
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
image:
|
|
||||||
repository: ghcr.io/example/xcontrol
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
postgresql:
|
|
||||||
enabled: true
|
|
||||||
image: postgres:16
|
|
||||||
auth:
|
|
||||||
user: xcontrol
|
|
||||||
password: xcontrol
|
|
||||||
database: xcontrol
|
|
||||||
|
|
||||||
externalPostgresql:
|
|
||||||
enabled: false
|
|
||||||
host: ""
|
|
||||||
user: ""
|
|
||||||
password: ""
|
|
||||||
database: ""
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
caddy:
|
|
||||||
image: caddy:2
|
|
||||||
container_name: caddy-accounts
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ../../caddy/Caddyfile.accounts.svc.plus:/etc/caddy/Caddyfile:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
|
|
||||||
stunnel_db_client:
|
|
||||||
image: stunnel/stunnel:latest
|
|
||||||
container_name: stunnel-account-db-client
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ../../stunnel/stunnel-account-db-client.conf:/etc/stunnel/stunnel.conf:ro
|
|
||||||
- /etc/ssl/certs:/etc/ssl/certs:ro
|
|
||||||
command: ["/etc/stunnel/stunnel.conf"]
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
caddy_data:
|
|
||||||
caddy_config:
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
stunnel_db_server:
|
|
||||||
image: stunnel/stunnel:latest
|
|
||||||
container_name: stunnel-account-db-server
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ../../stunnel/stunnel-account-db-server.conf:/etc/stunnel/stunnel.conf:ro
|
|
||||||
- /etc/stunnel:/etc/stunnel:ro
|
|
||||||
- /etc/ssl/certs:/etc/ssl/certs:ro
|
|
||||||
command: ["/etc/stunnel/stunnel.conf"]
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
This directory contains your keys and certificates.
|
|
||||||
|
|
||||||
`[cert name]/privkey.pem` : the private key for your certificate.
|
|
||||||
`[cert name]/fullchain.pem`: the certificate file used in most server software.
|
|
||||||
`[cert name]/chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
|
|
||||||
`[cert name]/cert.pem` : will break many server configurations, and should not be used
|
|
||||||
without reading further documentation (see link below).
|
|
||||||
|
|
||||||
WARNING: DO NOT MOVE OR RENAME THESE FILES!
|
|
||||||
Certbot expects these files to remain in this location in order
|
|
||||||
to function properly!
|
|
||||||
|
|
||||||
We recommend not moving these files. For more information, see the Certbot
|
|
||||||
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
mode: "server-agent"
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: info
|
|
||||||
|
|
||||||
auth:
|
|
||||||
enable: true
|
|
||||||
token:
|
|
||||||
publicToken: "xcontrol-public-token-2024"
|
|
||||||
refreshSecret: "xcontrol-refresh-secret-2024"
|
|
||||||
accessSecret: "xcontrol-access-secret-2024"
|
|
||||||
accessExpiry: "1h"
|
|
||||||
refreshExpiry: "168h"
|
|
||||||
|
|
||||||
server:
|
|
||||||
addr: ":8080"
|
|
||||||
readTimeout: 15s
|
|
||||||
writeTimeout: 15s
|
|
||||||
publicUrl: "https://accounts.svc.plus"
|
|
||||||
allowedOrigins:
|
|
||||||
- "https://accounts.svc.plus"
|
|
||||||
- "https://api.svc.plus"
|
|
||||||
- "https://www.svc.plus"
|
|
||||||
- "http://localhost:3000"
|
|
||||||
- "http://127.0.0.1:3000"
|
|
||||||
- "http://localhost:8080"
|
|
||||||
- "http://127.0.0.1:8080"
|
|
||||||
tls:
|
|
||||||
enabled: false
|
|
||||||
redirectHttp: false
|
|
||||||
|
|
||||||
store:
|
|
||||||
driver: "postgres"
|
|
||||||
dsn: "postgres://xcontrol:xcontrol@db:5432/account?sslmode=disable"
|
|
||||||
maxOpenConns: 30
|
|
||||||
maxIdleConns: 10
|
|
||||||
|
|
||||||
session:
|
|
||||||
ttl: 24h
|
|
||||||
cache: "memory"
|
|
||||||
|
|
||||||
smtp:
|
|
||||||
host: "smtp.example.com"
|
|
||||||
port: 587
|
|
||||||
username: "apikey"
|
|
||||||
p: "s"
|
|
||||||
from: "XControl Account <no-reply@example.com>"
|
|
||||||
timeout: 10s
|
|
||||||
tls:
|
|
||||||
mode: "auto"
|
|
||||||
insecureSkipVerify: false
|
|
||||||
|
|
||||||
xray:
|
|
||||||
sync:
|
|
||||||
enabled: false
|
|
||||||
interval: 5m
|
|
||||||
outputPath: "/usr/local/etc/xray/config.json"
|
|
||||||
templatePath: "config/xray.config.template.json"
|
|
||||||
validateCommand: []
|
|
||||||
restartCommand:
|
|
||||||
- "systemctl"
|
|
||||||
- "restart"
|
|
||||||
- "xray.service"
|
|
||||||
|
|
||||||
agent:
|
|
||||||
id: "account-primary"
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
server:
|
|
||||||
addr: ":8090"
|
|
||||||
readTimeout: 120s
|
|
||||||
writeTimeout: 120s
|
|
||||||
publicUrl: "https://api.svc.plus"
|
|
||||||
allowedOrigins:
|
|
||||||
- "https://api.svc.plus"
|
|
||||||
- "https://www.svc.plus"
|
|
||||||
- "https://accounts.svc.plus"
|
|
||||||
- "http://localhost:3000"
|
|
||||||
- "http://127.0.0.1:3000"
|
|
||||||
|
|
||||||
auth:
|
|
||||||
enable: false
|
|
||||||
authUrl: "https://accounts.svc.plus"
|
|
||||||
apiBaseUrl: "https://api.svc.plus"
|
|
||||||
publicToken: "xcontrol-public-token-2025"
|
|
||||||
|
|
||||||
global:
|
|
||||||
redis:
|
|
||||||
addr: ""
|
|
||||||
password: ""
|
|
||||||
vectordb:
|
|
||||||
pgurl: "postgres://xcontrol:xcontrol@db:5432/rag?sslmode=disable"
|
|
||||||
datasources:
|
|
||||||
- name: XControl
|
|
||||||
repo: https://github.com/svc-design/XControl
|
|
||||||
path: docs
|
|
||||||
|
|
||||||
sync:
|
|
||||||
repo:
|
|
||||||
proxy: ""
|
|
||||||
|
|
||||||
models:
|
|
||||||
embedder:
|
|
||||||
provider: "chutes"
|
|
||||||
models:
|
|
||||||
- "bge-m3"
|
|
||||||
baseurl: "http://127.0.0.1:9000"
|
|
||||||
endpoint: "http://127.0.0.1:9000/v1/embeddings"
|
|
||||||
generator:
|
|
||||||
provider: "chutes"
|
|
||||||
models:
|
|
||||||
- "deepseek-r1:8b"
|
|
||||||
baseurl: "http://127.0.0.1:11434"
|
|
||||||
endpoint: "http://127.0.0.1:11434/v1/chat/completions"
|
|
||||||
|
|
||||||
embedding:
|
|
||||||
max_batch: 64
|
|
||||||
dimension: 1024
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
services:
|
|
||||||
db:
|
|
||||||
image: cloudneutral/postgres-runtime:latest
|
|
||||||
container_name: xcontrol-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${XCONTROL_DB_NAME:-xcontrol}
|
|
||||||
POSTGRES_USER: ${XCONTROL_DB_USER:-xcontrol}
|
|
||||||
POSTGRES_PASSWORD: ${XCONTROL_DB_PASSWORD:-xcontrol}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${XCONTROL_DB_USER:-xcontrol}"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 60s
|
|
||||||
retries: 10
|
|
||||||
start_period: 5s
|
|
||||||
volumes:
|
|
||||||
- data:/var/lib/postgresql/data:rw
|
|
||||||
networks:
|
|
||||||
- db
|
|
||||||
|
|
||||||
account:
|
|
||||||
image: ghcr.io/cloud-neutral-toolkit/account:latest
|
|
||||||
container_name: account
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PORT: 8080
|
|
||||||
CONFIG_PATH: /etc/xcontrol/account-compose.yaml
|
|
||||||
volumes:
|
|
||||||
- ./config/account.yaml:/etc/xcontrol/account-compose.yaml:ro
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
- db
|
|
||||||
|
|
||||||
rag-server:
|
|
||||||
image: cloudneutral/rag-server:latest
|
|
||||||
container_name: rag-server
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PORT: 8090
|
|
||||||
CONFIG_PATH: /etc/rag-server/server-compose.yaml
|
|
||||||
volumes:
|
|
||||||
- ./config/server.yaml:/etc/rag-server/server-compose.yaml:ro
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "8090:8090"
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
- db
|
|
||||||
|
|
||||||
dashboard:
|
|
||||||
image: cloudneutral/dashboard:latest
|
|
||||||
container_name: dashboard
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PORT: 3000
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
depends_on:
|
|
||||||
account:
|
|
||||||
condition: service_started
|
|
||||||
rag-server:
|
|
||||||
condition: service_started
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
|
|
||||||
proxy-external-tls:
|
|
||||||
image: nginx:mainline-alpine
|
|
||||||
container_name: proxy-external-tls
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
|
||||||
- ./certbot/conf:/etc/letsencrypt
|
|
||||||
- ./certbot/www:/var/www/certbot
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
depends_on:
|
|
||||||
account:
|
|
||||||
condition: service_started
|
|
||||||
rag-server:
|
|
||||||
condition: service_started
|
|
||||||
dashboard:
|
|
||||||
condition: service_started
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: redis
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
|
|
||||||
bootstrap-nginx:
|
|
||||||
profiles: ["bootstrap"]
|
|
||||||
image: nginx:mainline-alpine
|
|
||||||
container_name: bootstrap-nginx
|
|
||||||
volumes:
|
|
||||||
- ./certbot/www:/var/www/certbot
|
|
||||||
- ./certbot/conf:/etc/letsencrypt
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
- ./nginx/conf.d/bootstrap-nginx.conf:/etc/nginx/conf.d/default.conf
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost"]
|
|
||||||
interval: 3s
|
|
||||||
timeout: 2s
|
|
||||||
retries: 10
|
|
||||||
start_period: 3s
|
|
||||||
|
|
||||||
certbot:
|
|
||||||
profiles: ["bootstrap"]
|
|
||||||
image: certbot/certbot
|
|
||||||
container_name: certbot
|
|
||||||
command: >
|
|
||||||
certonly --webroot
|
|
||||||
--webroot-path=/var/www/certbot
|
|
||||||
--email ${XCONTROL_CERTBOT_EMAIL:-cloudneutral@qq.com}
|
|
||||||
--agree-tos
|
|
||||||
--no-eff-email
|
|
||||||
--keep-until-expiring
|
|
||||||
--non-interactive
|
|
||||||
-d ${XCONTROL_CERTBOT_DOMAINS:-svc.plus}
|
|
||||||
volumes:
|
|
||||||
- ./certbot/conf:/etc/letsencrypt
|
|
||||||
- ./certbot/www:/var/www/certbot
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app:
|
|
||||||
db:
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
data:
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name accounts.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name accounts.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/svc.plus/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/svc.plus/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location ^~ /api/auth/ {
|
|
||||||
proxy_pass http://account:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
|
||||||
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
||||||
add_header Pragma "no-cache";
|
|
||||||
add_header Expires "0";
|
|
||||||
|
|
||||||
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name dl.svc.plus cn-dl.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/svc.plus/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/svc.plus/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
root /data/update-server;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location ^~ /.well-known/ { allow all; }
|
|
||||||
|
|
||||||
# ✅ JSON 专用——放在 / 之前
|
|
||||||
location ~* \.json$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
add_header Cache-Control "public, max-age=60, s-maxage=60, stale-while-revalidate=300";
|
|
||||||
default_type application/json;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 目录浏览
|
|
||||||
location / {
|
|
||||||
autoindex on;
|
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 大包直出
|
|
||||||
location ~* \.(?:dmg|zip|tar\.gz|deb|rpm|exe|pkg|appimage|apk|ipa)$ {
|
|
||||||
expires 7d;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 隐藏 dotfiles(不拦 /.well-known/)
|
|
||||||
location ~ /\.(?!well-known/)[^/]+ { deny all; }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name dl.svc.plus cn-dl.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 200 "bootstrap";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name www.svc.plus cn-homepage.svc.plus;
|
|
||||||
|
|
||||||
# Certbot HTTP-01 challenge
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
# All HTTP → HTTPS
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name www.svc.plus cn-homepage.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/svc.plus/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/svc.plus/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
# ====== 静态根目录(Next.js export 产物)======
|
|
||||||
root /dashboard/;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# (可选)放行 ACME/健康检查等
|
|
||||||
location ^~ /.well-known/ { allow all; }
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# API 反向代理(保持原样)
|
|
||||||
# =======================
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://account:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# /api/askai 接口限流(保持原样)
|
|
||||||
location = /api/askai {
|
|
||||||
access_by_lua_block {
|
|
||||||
local redis = require "resty.redis"
|
|
||||||
local r = redis:new()
|
|
||||||
r:set_timeout(200)
|
|
||||||
local ok, err = r:connect("redis", 6379)
|
|
||||||
if not ok then
|
|
||||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
|
||||||
return ngx.exit(500)
|
|
||||||
end
|
|
||||||
|
|
||||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
|
||||||
local today = os.date("%Y%m%d")
|
|
||||||
local key = "limit:user:" .. user .. ":" .. today
|
|
||||||
|
|
||||||
local count, err = r:incr(key)
|
|
||||||
if count == 1 then r:expire(key, 86400) end
|
|
||||||
if count > 200 then
|
|
||||||
ngx.status = 429
|
|
||||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
|
||||||
ngx.say("Too Many Requests: daily limit reached")
|
|
||||||
return ngx.exit(429)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_pass http://account:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# 静态文件直出(替换原先的 Next.js 动态代理)
|
|
||||||
# =======================
|
|
||||||
|
|
||||||
# Next 导出的静态资源(hash 不变 -> 长缓存)
|
|
||||||
location ^~ /_next/static/ {
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable, max-age=31536000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 其他常见静态资源:中等缓存
|
|
||||||
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf)$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
expires 7d;
|
|
||||||
add_header Cache-Control "public, max-age=604800";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主页与已导出的所有路由:按文件/目录匹配
|
|
||||||
# 未命中的交给 404.html(保持静态站语义)
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 显式处理 404/500 路由目录(Next export 会生成 404/、500/ 与同名 .html)
|
|
||||||
location = /404.html { internal; }
|
|
||||||
error_page 404 /404.html;
|
|
||||||
|
|
||||||
# 如果有 /favicon.ico,则直接给文件
|
|
||||||
location = /favicon.ico {
|
|
||||||
try_files /favicon.ico =204;
|
|
||||||
access_log off;
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# (可选)为某些目录开启目录索引(你有 dl-index、docs、download)
|
|
||||||
# 若需要列表页可以这样做;不需要则删除本段
|
|
||||||
location ^~ /dl-index/ {
|
|
||||||
autoindex on;
|
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 拒绝访问隐藏文件(如 .env)
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
# (可选)开启 gzip(如启用 ngx_brotli,也可再加 br)
|
|
||||||
gzip on;
|
|
||||||
gzip_comp_level 5;
|
|
||||||
gzip_min_length 1k;
|
|
||||||
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
|
|
||||||
gzip_vary on;
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name rag-server.svc.plus api.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name rag-server.svc.plus api.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/svc.plus/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/svc.plus/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
proxy_pass http://rag-server:8090;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
|
||||||
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_header Cache-Control "no-store";
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /api/askai {
|
|
||||||
access_by_lua_block {
|
|
||||||
local redis = require "resty.redis"
|
|
||||||
local r = redis:new()
|
|
||||||
r:set_timeout(200)
|
|
||||||
local ok, err = r:connect("redis", 6379)
|
|
||||||
if not ok then
|
|
||||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
|
||||||
return ngx.exit(500)
|
|
||||||
end
|
|
||||||
|
|
||||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
|
||||||
local today = os.date("%Y%m%d")
|
|
||||||
local key = "limit:user:" .. user .. ":" .. today
|
|
||||||
|
|
||||||
local count, err = r:incr(key)
|
|
||||||
if count == 1 then r:expire(key, 86400) end
|
|
||||||
if count > 200 then
|
|
||||||
ngx.status = 429
|
|
||||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
|
||||||
ngx.say("Too Many Requests: daily limit reached")
|
|
||||||
return ngx.exit(429)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_pass http://rag-server:8090;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
events {}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
COMPOSE_FILE="docker-compose.yaml"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 {up|init|certbot|reset|down}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_all() {
|
|
||||||
docker compose -f "${COMPOSE_FILE}" down -v || true
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
up)
|
|
||||||
docker compose -f "${COMPOSE_FILE}" up -d --build
|
|
||||||
;;
|
|
||||||
init)
|
|
||||||
docker compose -f "${COMPOSE_FILE}" up -d db redis
|
|
||||||
docker compose -f "${COMPOSE_FILE}" --profile init run --rm init
|
|
||||||
;;
|
|
||||||
certbot)
|
|
||||||
docker compose -f "${COMPOSE_FILE}" --profile bootstrap up --abort-on-container-exit certbot
|
|
||||||
;;
|
|
||||||
reset)
|
|
||||||
stop_all
|
|
||||||
rm -rf ./certbot/conf/live ./certbot/www
|
|
||||||
mkdir -p ./certbot/conf/live ./certbot/www
|
|
||||||
;;
|
|
||||||
down)
|
|
||||||
stop_all
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: xcontrol
|
|
||||||
POSTGRES_USER: xcontrol
|
|
||||||
POSTGRES_PASSWORD: xcontrol
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
xcontrol:
|
|
||||||
image: ghcr.io/example/xcontrol:latest
|
|
||||||
environment:
|
|
||||||
KB_DSN: postgres://xcontrol:xcontrol@postgres:5432/xcontrol?sslmode=disable
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Next.js (XControl dashboard - DEV)
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Environment=NODE_ENV=development
|
|
||||||
WorkingDirectory=/var/www/XControl/dashboard
|
|
||||||
|
|
||||||
# 使用 Yarn 启动开发模式
|
|
||||||
ExecStart=/usr/bin/yarn next dev -p 3000
|
|
||||||
|
|
||||||
Restart=always
|
|
||||||
LimitNOFILE=65535
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name accounts.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name accounts.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name accounts.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location ^~ /api/auth/ {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
|
||||||
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
||||||
add_header Pragma "no-cache";
|
|
||||||
add_header Expires "0";
|
|
||||||
|
|
||||||
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name artifact.svc.plus cn-artifact.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
root /data/update-server;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# 建议:放行 ACME/健康检查等(避免被 dotfile 规则误伤)
|
|
||||||
location ^~ /.well-known/ { allow all; }
|
|
||||||
|
|
||||||
|
|
||||||
# 目录浏览(打开 autoindex)—可列出整个 /data/update-server
|
|
||||||
location / {
|
|
||||||
autoindex on;
|
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
try_files $uri $uri/ =404; # 保持原有 404 语义
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 常见安装包直下读文件(大小写不敏感)
|
|
||||||
# 这里无需 try_files,命中即直接读文件;减少一次磁盘判断
|
|
||||||
location ~* \.(?:dmg|zip|tar\.gz|deb|rpm|exe|pkg|appimage|apk|ipa)$ {
|
|
||||||
expires 7d;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 隐藏 dotfiles(但不拦 /.well-known/,已在上面放行)
|
|
||||||
location ~ /\.(?!well-known/)[^/]+ {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name artifact.svc.plus cn-artifact.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name dl.svc.plus cn-dl.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
root /data/update-server;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location ^~ /.well-known/ { allow all; }
|
|
||||||
|
|
||||||
# ✅ JSON 专用——放在 / 之前
|
|
||||||
location ~* \.json$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
add_header Cache-Control "public, max-age=60, s-maxage=60, stale-while-revalidate=300";
|
|
||||||
default_type application/json;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 目录浏览
|
|
||||||
location / {
|
|
||||||
autoindex on;
|
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 大包直出
|
|
||||||
location ~* \.(?:dmg|zip|tar\.gz|deb|rpm|exe|pkg|appimage|apk|ipa)$ {
|
|
||||||
expires 7d;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
add_header Accept-Ranges bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 隐藏 dotfiles(不拦 /.well-known/)
|
|
||||||
location ~ /\.(?!well-known/)[^/]+ { deny all; }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name dl.svc.plus cn-dl.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name www.svc.plus cn-homepage.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name www.svc.plus cn-homepage.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
# ====== 静态根目录(Next.js export 产物)======
|
|
||||||
root /data/update-server/dashboard/;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# (可选)放行 ACME/健康检查等
|
|
||||||
location ^~ /.well-known/ { allow all; }
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# API 反向代理(保持原样)
|
|
||||||
# =======================
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# /api/askai 接口限流(保持原样)
|
|
||||||
location = /api/askai {
|
|
||||||
access_by_lua_block {
|
|
||||||
local redis = require "resty.redis"
|
|
||||||
local r = redis:new()
|
|
||||||
r:set_timeout(200)
|
|
||||||
local ok, err = r:connect("127.0.0.1", 6379)
|
|
||||||
if not ok then
|
|
||||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
|
||||||
return ngx.exit(500)
|
|
||||||
end
|
|
||||||
|
|
||||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
|
||||||
local today = os.date("%Y%m%d")
|
|
||||||
local key = "limit:user:" .. user .. ":" .. today
|
|
||||||
|
|
||||||
local count, err = r:incr(key)
|
|
||||||
if count == 1 then r:expire(key, 86400) end
|
|
||||||
if count > 200 then
|
|
||||||
ngx.status = 429
|
|
||||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
|
||||||
ngx.say("Too Many Requests: daily limit reached")
|
|
||||||
return ngx.exit(429)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# 静态文件直出(替换原先的 Next.js 动态代理)
|
|
||||||
# =======================
|
|
||||||
|
|
||||||
# Next 导出的静态资源(hash 不变 -> 长缓存)
|
|
||||||
location ^~ /_next/static/ {
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable, max-age=31536000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 其他常见静态资源:中等缓存
|
|
||||||
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf)$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
expires 7d;
|
|
||||||
add_header Cache-Control "public, max-age=604800";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主页与已导出的所有路由:按文件/目录匹配
|
|
||||||
# 未命中的交给 404.html(保持静态站语义)
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 显式处理 404/500 路由目录(Next export 会生成 404/、500/ 与同名 .html)
|
|
||||||
location = /404.html { internal; }
|
|
||||||
error_page 404 /404.html;
|
|
||||||
|
|
||||||
# 如果有 /favicon.ico,则直接给文件
|
|
||||||
location = /favicon.ico {
|
|
||||||
try_files /favicon.ico =204;
|
|
||||||
access_log off;
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# (可选)为某些目录开启目录索引(你有 dl-index、docs、download)
|
|
||||||
# 若需要列表页可以这样做;不需要则删除本段
|
|
||||||
location ^~ /dl-index/ {
|
|
||||||
autoindex on;
|
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 拒绝访问隐藏文件(如 .env)
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
# (可选)开启 gzip(如启用 ngx_brotli,也可再加 br)
|
|
||||||
gzip on;
|
|
||||||
gzip_comp_level 5;
|
|
||||||
gzip_min_length 1k;
|
|
||||||
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
|
|
||||||
gzip_vary on;
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name www.svc.plus frontend.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name www.svc.plus frontend.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location ^~ /_next/ {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /favicon.ico {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name rag-server.svc.plus api.svc.plus;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name rag-server.svc.plus api.svc.plus;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
proxy_pass http://127.0.0.1:8090;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
|
||||||
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_header Cache-Control "no-store";
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /api/askai {
|
|
||||||
access_by_lua_block {
|
|
||||||
local redis = require "resty.redis"
|
|
||||||
local r = redis:new()
|
|
||||||
r:set_timeout(200)
|
|
||||||
local ok, err = r:connect("127.0.0.1", 6379)
|
|
||||||
if not ok then
|
|
||||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
|
||||||
return ngx.exit(500)
|
|
||||||
end
|
|
||||||
|
|
||||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
|
||||||
local today = os.date("%Y%m%d")
|
|
||||||
local key = "limit:user:" .. user .. ":" .. today
|
|
||||||
|
|
||||||
local count, err = r:incr(key)
|
|
||||||
if count == 1 then r:expire(key, 86400) end
|
|
||||||
if count > 200 then
|
|
||||||
ngx.status = 429
|
|
||||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
|
||||||
ngx.say("Too Many Requests: daily limit reached")
|
|
||||||
return ngx.exit(429)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8090;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=accounts.svc.plus API
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=accounts
|
|
||||||
Group=accounts
|
|
||||||
WorkingDirectory=/opt/xcontrol
|
|
||||||
Environment=ACCOUNTS_DB_USER=accounts
|
|
||||||
Environment=ACCOUNTS_DB_PASSWORD=scrubbed
|
|
||||||
Environment=ACCOUNTS_DB_NAME=accounts
|
|
||||||
Environment=ACCOUNTS_DB_SSLMODE=disable
|
|
||||||
Environment=ACCOUNTS_RATE_LIMIT_RPM=60
|
|
||||||
Environment=ACCOUNTS_SESSION_TTL=24h
|
|
||||||
ExecStart=/opt/xcontrol/accounts-api
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=3
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Caddy (accounts.svc.plus)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
ExecStart=/usr/bin/caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
TimeoutStopSec=5s
|
|
||||||
Restart=on-failure
|
|
||||||
LimitNOFILE=1048576
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=stunnel client (account DB)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/bin/stunnel /etc/stunnel/stunnel-account-db-client.conf
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=2s
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=stunnel server (account DB)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/bin/stunnel /etc/stunnel/stunnel-account-db-server.conf
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=2s
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# Milestone 2 TODO
|
|
||||||
|
|
||||||
使用 LangChainGo 框架优化 CLI、Server 以及 AskAI 接口的子任务规划:
|
|
||||||
|
|
||||||
1. **LLM 接口层(Model I/O)**
|
|
||||||
- [ ] 构建 OpenAI、Hugging Face、Ollama、Google AI、Cohere 等模型的 provider registry。
|
|
||||||
- [ ] 在 CLI 与 Server 配置中暴露模型提供商切换能力。
|
|
||||||
- [ ] 编写单元测试验证不同 provider 间的切换。
|
|
||||||
- [ ] 补充配置和环境变量使用文档。
|
|
||||||
2. **Chains(链式流程)**
|
|
||||||
- [ ] 将 prompt、检索结果、工具调用组合成 RAG 与聊天链。
|
|
||||||
- [ ] 为 AskAI 提供可复用的链式定义,支持复杂任务编排。
|
|
||||||
- [ ] 在 CLI 中提供链式调用示例。
|
|
||||||
- [ ] 编写链式流程的集成测试。
|
|
||||||
3. **工具与 Agent 体系**
|
|
||||||
- [ ] 实现 Web 搜索、Scraper、SQL 查询等常用工具。
|
|
||||||
- [ ] 将工具注册到 Agent 框架中,支持动态调用。
|
|
||||||
- [ ] 在 CLI 中演示 ReAct 风格的工具调用。
|
|
||||||
- [ ] 为工具与 Agent 交互添加测试用例。
|
|
||||||
4. **向量检索与数据接入**
|
|
||||||
- [ ] 接入 PGVector、Weaviate、Qdrant、Chroma、Pinecone、Redis Vector 等存储。
|
|
||||||
- [ ] 支持自定义向量维度与检索参数。
|
|
||||||
- [ ] 为不同向量存储编写基准测试与比较。
|
|
||||||
- [ ] 提供检索参数调优的文档示例。
|
|
||||||
5. **文档加载与分块**
|
|
||||||
- [ ] 提供 Markdown、代码、HTML 等多格式的 Document Loader。
|
|
||||||
- [ ] 支持按 token 或递归策略的 Text Splitter。
|
|
||||||
- [ ] 统一存储分块结果并支持增量更新 API。
|
|
||||||
- [ ] 为 loader 与 splitter 编写测试。
|
|
||||||
6. **Memory 与历史追踪**
|
|
||||||
- [ ] 为 AskAI 增加 conversation buffer 等对话记忆。
|
|
||||||
- [ ] 在 Server 中持久化会话历史并提供配置项。
|
|
||||||
- [ ] 支持调整记忆长度与清理策略。
|
|
||||||
- [ ] 编写端到端测试验证记忆保留。
|
|
||||||
|
|
||||||
以上任务将逐步落实,以完成混合检索与多模型支持目标。
|
|
||||||
|
|
||||||
## 文档 QA embedding 最佳实践
|
|
||||||
|
|
||||||
### 结构提取
|
|
||||||
- 为每篇文档生成目录(Table of Contents)并单独 embedding,用于导航检索。
|
|
||||||
- 将每个标题/小节标题单独 embedding,支持快速定位。
|
|
||||||
- 将标签、时间、来源等元数据转成文本并 embedding,参与 Hybrid Search。
|
|
||||||
|
|
||||||
### 切分策略
|
|
||||||
- 按段落切分,保持上下文一致性。
|
|
||||||
- 采用语义切分(基于句子边界或语义相似度)。
|
|
||||||
- 启用滑动窗口切分(20%~30% 重叠)减少边界信息丢失。
|
|
||||||
- 多粒度切分(同时存储小块和大块向量)。
|
|
||||||
|
|
||||||
### 信息增强
|
|
||||||
- 实现 Query Expansion / HyDE,在检索前扩展问题或生成假设文档。
|
|
||||||
- 为每个 chunk 存储摘要向量,提升跨领域匹配效果。
|
|
||||||
- 融合跨文档引用上下文 embedding。
|
|
||||||
|
|
||||||
### 向量优化与后处理
|
|
||||||
- 去重无意义 chunk(如页眉、版权声明)。
|
|
||||||
- MMR(Maximal Marginal Relevance)去冗余,提升多样性。
|
|
||||||
- 对候选结果进行轻量 Re-ranking(如 bge-reranker)。
|
|
||||||
- 融合多模态信息(如图片描述)。
|
|
||||||
|
|
||||||
### 检索优化
|
|
||||||
- 在 Query 中启用 Hybrid Search(向量 + BM25),权重可配置。
|
|
||||||
- 支持多向量查询(ColBERT 思路)匹配文档不同部分。
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# Milestone 2: Hybrid Search
|
|
||||||
|
|
||||||
RAG 第二阶段优化规划
|
|
||||||
|
|
||||||
参考 GitHub issue "RAG 第二优化节点阶段",本阶段围绕现有 RAG 系统继续迭代,目标是提升检索效果与服务稳定性,并扩展多模型与多数据源支持。
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
- 提升向量检索精准度与性能。
|
|
||||||
- 支持增量同步与多仓库数据接入。
|
|
||||||
- 提供多种嵌入与大模型选择,方便灵活部署。
|
|
||||||
- 加强 API/CLI 的错误处理、监控与自动化测试。
|
|
||||||
|
|
||||||
## 主要任务
|
|
||||||
|
|
||||||
1. **向量检索优化**
|
|
||||||
- 对比评估不同嵌入模型与相似度度量。
|
|
||||||
- 引入向量索引/压缩策略,减少查询延迟。
|
|
||||||
2. **数据同步管道**
|
|
||||||
- 实现增量更新机制,按需重建向量。
|
|
||||||
- 支持同步进度追踪与失败重试。
|
|
||||||
3. **多模型与配置**
|
|
||||||
- 通过 LangChainGo 统一接入本地及云端模型。
|
|
||||||
- 允许针对不同模型自定义参数与超时配置。
|
|
||||||
4. **API 与 CLI 稳定性**
|
|
||||||
- 改进异常处理与日志记录,暴露更多诊断信息。
|
|
||||||
- 完善集成测试,覆盖 RAG upsert 与查询流程。
|
|
||||||
5. **监控与观测**
|
|
||||||
- 接入指标与日志上报,便于性能分析。
|
|
||||||
- 构建健康检查与告警机制。
|
|
||||||
|
|
||||||
## 里程碑
|
|
||||||
|
|
||||||
- **M2.1**:完成增量同步与检索优化的原型验证。
|
|
||||||
- **M2.2**:集成多模型支持并上线监控体系。
|
|
||||||
- **M2.3**:完善自动化测试与文档,准备下一阶段迭代。
|
|
||||||
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# Roadmap
|
|
||||||
|
|
||||||
## Milestone 1: MVP (Completed)
|
|
||||||
- Use default Redis port (#98) and establish PostgreSQL & Redis baseline.
|
|
||||||
- Stream RAG sync progress for GitHub repository synchronization (#100).
|
|
||||||
- Add client-side Markdown parsing to the CLI (#104).
|
|
||||||
- Refactor RAG ingestion into the CLI with a server upsert endpoint (#103).
|
|
||||||
- RAG API functional tests and per-file ingestion workflow (#115).
|
|
||||||
- Allow RAG upsert to migrate embedding dimensions (#119) and document pgvector initialization (#120).
|
|
||||||
- Ingest files automatically (#123).
|
|
||||||
|
|
||||||
## Milestone 2: Hybrid Search
|
|
||||||
- CLI and server dynamically support 1024-dimensional embeddings.
|
|
||||||
- Update docs and configs to vector(1024) (#130).
|
|
||||||
- Add embedding configuration fields (#131).
|
|
||||||
- Add RAG API integration tests for vectors (#132).
|
|
||||||
- Add allama support (#136).
|
|
||||||
- Deploy homepage via rsync from CI and fix SSH directory creation (#18, #19).
|
|
||||||
- Deploy XControl panel via GitHub Actions (#20).
|
|
||||||
- Fix yarn lock context concatenation (#21).
|
|
||||||
|
|
||||||
## Milestone 3: Production Monitoring & Optimization
|
|
||||||
- Switch server and CLI to Cobra (#133).
|
|
||||||
- Add repo sync proxy configuration (#135).
|
|
||||||
- Allow custom AskAI timeout (#141).
|
|
||||||
- Add log level support to CLI and server and log AskAI errors (#125, #140).
|
|
||||||
- Continue performance optimization, error handling, multi-model support, permission control, hot reload, and improve RAG upsert docs (#129).
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
Agent Framework 设计文档
|
|
||||||
|
|
||||||
版本:v1.0
|
|
||||||
项目代号:Project Codexium
|
|
||||||
作者:svc.plus 架构组(Pan Haitao)
|
|
||||||
目标:统一代码智能、运维智能与模型桥接能力的开发者工作台
|
|
||||||
|
|
||||||
一、系统愿景
|
|
||||||
|
|
||||||
“让 AI 不只是写代码,而是理解系统。”
|
|
||||||
|
|
||||||
Codexium 的目标是构建一个统一的智能代理层,使得开发者在编程、测试、运维的整个生命周期中,都能通过 /api/agent/* 接口访问具备上下文记忆与验证能力的 LLM 工具链。
|
|
||||||
|
|
||||||
系统分为三大支柱:
|
|
||||||
|
|
||||||
模块 名称 职责
|
|
||||||
/api/agent/code CodeSmith 编码智能:分析、重构、生成、解释代码
|
|
||||||
/api/agent/ops OpsMind 运维智能:测试验证、性能剖析、异常诊断
|
|
||||||
/api/agent/bridge LLM-Bridge 桥接智能:与国内外大模型生态互联
|
|
||||||
二、系统架构概览
|
|
||||||
flowchart TB
|
|
||||||
subgraph UI["🖥️ Web IDE / Dashboard"]
|
|
||||||
C1[任务列表] --> C2[任务详情与日志]
|
|
||||||
C2 --> C3[运行面板(Playwright / DevTools / LLM)]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph API["🧠 Agent Gateway (Node/Deno)"]
|
|
||||||
A1[/api/agent/code/]:::code --> A3
|
|
||||||
A2[/api/agent/ops/]:::ops --> A3
|
|
||||||
A4[/api/agent/bridge/]:::bridge --> A3
|
|
||||||
A3[(Runs Registry + Storage)]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Runtime["⚙️ Runners / Services"]
|
|
||||||
R1[Codex-CLI / Claude-CLI / Gemini-CLI]
|
|
||||||
R2[Playwright MCP Server]
|
|
||||||
R3[DevTools MCP Server]
|
|
||||||
R4[LLM-Bridge Adapter]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Infra["💾 Backend Infra"]
|
|
||||||
D1[(PostgreSQL / SQLite)]
|
|
||||||
D2[(S3 / Local Storage)]
|
|
||||||
end
|
|
||||||
|
|
||||||
UI --> API
|
|
||||||
API --> Runtime
|
|
||||||
API --> Infra
|
|
||||||
Runtime --> D2
|
|
||||||
classDef code fill=#C6E2FF,stroke=#4582EC;
|
|
||||||
classDef ops fill=#FFF2CC,stroke=#D6B656;
|
|
||||||
classDef bridge fill=#D9EAD3,stroke=#6AA84F;
|
|
||||||
|
|
||||||
三、API 模块划分与命名语义
|
|
||||||
模块路径 名称 职责描述 核心子接口
|
|
||||||
/api/agent/code CodeSmith 面向代码智能任务 analyze, refactor, generate, explain, review
|
|
||||||
/api/agent/ops OpsMind 面向运维与测试任务 playwright, devtools, profile, report
|
|
||||||
/api/agent/bridge LLM-Bridge 模型桥接与分发层 invoke, list-models, proxy
|
|
||||||
四、功能说明
|
|
||||||
1. CodeSmith(编码智能)
|
|
||||||
|
|
||||||
目标: 让 LLM 理解项目上下文并参与重构。
|
|
||||||
功能示例:
|
|
||||||
|
|
||||||
功能 说明 CLI 或工具
|
|
||||||
analyze 分析文件结构与依赖 codex-cli analyze
|
|
||||||
refactor 自动重构、删除死代码 codex-cli refactor
|
|
||||||
generate 根据提示生成新代码 claude-cli generate
|
|
||||||
explain 对复杂逻辑生成自然语言解释 gemini-cli explain
|
|
||||||
|
|
||||||
运行模式:
|
|
||||||
通过 child_process.spawn 调用 CLI,并以 SSE 流式推送执行日志。
|
|
||||||
每次执行结果存储为 run 记录,并关联到任务。
|
|
||||||
|
|
||||||
2. OpsMind(运维智能)
|
|
||||||
|
|
||||||
目标: 自动化验证与性能剖析。
|
|
||||||
|
|
||||||
MCP 接口:
|
|
||||||
|
|
||||||
功能 MCP Server 说明
|
|
||||||
playwright mcp-playwright 执行端到端测试、截图、trace
|
|
||||||
devtools mcp-devtools 运行 CPU/Heap Profiler
|
|
||||||
profile mcp-devtools 生成 trace.json 与指标摘要
|
|
||||||
report 内部 汇总生成性能分析报告(HTML/PDF)
|
|
||||||
|
|
||||||
示例工作流:
|
|
||||||
|
|
||||||
POST /api/agent/ops/playwright
|
|
||||||
→ 启动 Playwright MCP → trace.zip → 存储为附件 → 任务run状态更新
|
|
||||||
|
|
||||||
3. LLM-Bridge(模型桥接层)
|
|
||||||
|
|
||||||
目标: 在不暴露私钥的前提下,统一访问国内外大模型生态。
|
|
||||||
|
|
||||||
功能 说明 适配对象
|
|
||||||
invoke 通用调用接口(支持 OpenAI 格式) ChatGPT / Claude / Qwen / Yi / Baichuan / Moonshot
|
|
||||||
list-models 返回所有可用模型及状态 从注册表动态读取
|
|
||||||
proxy 将 REST 调用转为相应 API 代理 可接入 WebSocket 流式返回
|
|
||||||
|
|
||||||
配置结构示例:
|
|
||||||
|
|
||||||
llm_bridge:
|
|
||||||
providers:
|
|
||||||
openai:
|
|
||||||
endpoint: https://api.openai.com/v1
|
|
||||||
api_key: $OPENAI_API_KEY
|
|
||||||
qwen:
|
|
||||||
endpoint: https://dashscope.aliyuncs.com/api/v1
|
|
||||||
api_key: $DASHSCOPE_API_KEY
|
|
||||||
moonshot:
|
|
||||||
endpoint: https://api.moonshot.cn/v1
|
|
||||||
api_key: $MOONSHOT_API_KEY
|
|
||||||
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
对上:所有 /api/agent/code、/api/agent/ops 请求可指定 provider 参数;
|
|
||||||
|
|
||||||
对下:桥接各类 LLM API,兼容 JSON Schema 响应;
|
|
||||||
|
|
||||||
提供统一上下文缓存机制(如 KV/Redis session)。
|
|
||||||
|
|
||||||
五、数据库模型
|
|
||||||
CREATE TABLE tasks (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT,
|
|
||||||
summary TEXT,
|
|
||||||
status TEXT CHECK (status IN ('todo','doing','done','archived')),
|
|
||||||
tags TEXT[],
|
|
||||||
created_at TIMESTAMP DEFAULT now(),
|
|
||||||
updated_at TIMESTAMP DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE runs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
task_id TEXT REFERENCES tasks(id),
|
|
||||||
runner TEXT,
|
|
||||||
input JSONB,
|
|
||||||
output JSONB,
|
|
||||||
status TEXT CHECK (status IN ('queued','running','passed','failed')),
|
|
||||||
artifacts TEXT[],
|
|
||||||
started_at TIMESTAMP,
|
|
||||||
finished_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE attachments (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
task_id TEXT REFERENCES tasks(id),
|
|
||||||
name TEXT,
|
|
||||||
kind TEXT,
|
|
||||||
mime TEXT,
|
|
||||||
url TEXT,
|
|
||||||
size INT,
|
|
||||||
created_at TIMESTAMP DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
六、前端设计(Web 工作台)
|
|
||||||
|
|
||||||
结构:
|
|
||||||
|
|
||||||
区域 功能 技术栈
|
|
||||||
左栏 任务列表(搜索、筛选、状态切换) Zustand + SWR
|
|
||||||
右栏 任务详情、运行面板、上传区 Tailwind + shadcn/ui
|
|
||||||
底部 实时日志控制台 WebSocket/SSE
|
|
||||||
顶部 模型选择(LLM-Bridge 模式切换) Select + Context Provider
|
|
||||||
七、安全与隔离设计
|
|
||||||
|
|
||||||
Runner 容器隔离:
|
|
||||||
所有 Playwright/DevTools Runner 运行于独立容器,带文件系统隔离。
|
|
||||||
|
|
||||||
命令白名单:
|
|
||||||
/api/agent/code 仅能执行 codex-cli 等注册命令。
|
|
||||||
|
|
||||||
模型访问控制:
|
|
||||||
LLM-Bridge 支持:
|
|
||||||
|
|
||||||
统一 API Key 管理;
|
|
||||||
|
|
||||||
访问审计;
|
|
||||||
|
|
||||||
模型路由黑白名单(例如禁止访问海外模型)。
|
|
||||||
|
|
||||||
上传验证:
|
|
||||||
图片/trace 文件 MIME 检查 + 大小上限(默认 50MB)。
|
|
||||||
|
|
||||||
八、部署与扩展
|
|
||||||
|
|
||||||
推荐部署模式:
|
|
||||||
|
|
||||||
服务 类型 部署建议
|
|
||||||
API Gateway Node 18+ Docker 容器,内网访问 MCP
|
|
||||||
Playwright MCP Sidecar mcr.microsoft.com/playwright 镜像
|
|
||||||
DevTools MCP Sidecar chrome-launcher 环境
|
|
||||||
LLM-Bridge 统一服务 可独立部署,实现模型代理
|
|
||||||
Storage S3 或 MinIO 附件与 trace 存储
|
|
||||||
DB PostgreSQL 任务、运行、模型状态持久化
|
|
||||||
九、未来路线图(v1 → v3)
|
|
||||||
阶段 特性 说明
|
|
||||||
v1.0 CodeSmith + OpsMind 基础实现 完成 CLI/MCP 集成、运行记录体系
|
|
||||||
v1.1 LLM-Bridge 接入国内模型 Qwen、Yi、Baichuan、Moonshot
|
|
||||||
v2.0 会话上下文与任务记忆 Redis + Vector Store
|
|
||||||
v2.1 Web 工作流可视化(Flow View) 支持多步骤组合任务
|
|
||||||
v3.0 多代理协作模式 让 CodeSmith 与 OpsMind 自动协作验证
|
|
||||||
十、总结与命名哲学
|
|
||||||
|
|
||||||
CodeSmith → “写得比人快一点”
|
|
||||||
|
|
||||||
OpsMind → “想得比机器深一点”
|
|
||||||
|
|
||||||
LLM-Bridge → “连接的不是模型,而是生态”
|
|
||||||
|
|
||||||
三者共同组成一个统一的“Agent Infra”,能运行在开发机、CI/CD 管道、甚至本地容器中,为 Cloud-Neutral 工程体系提供智能层。
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
# Account Service Admin Settings API
|
|
||||||
|
|
||||||
This document summarizes the new `/api/auth/admin/settings` endpoints for managing the permission matrix used by the account service.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
- `GET /api/auth/admin/settings`
|
|
||||||
- Requires the caller to present `X-User-Role` or `X-Role` headers with value `admin` or `operator`.
|
|
||||||
- Returns the latest permission matrix and associated version. The handler responds with `503 Service Unavailable` when the admin settings database has not been initialised.
|
|
||||||
|
|
||||||
- `POST /api/auth/admin/settings`
|
|
||||||
- Accepts a JSON payload containing a `version` and `matrix`. The matrix is validated to ensure module keys are non-empty and roles are within the supported set (`admin`, `operator`, `user`).
|
|
||||||
- Uses optimistic locking on the `version` field. When the provided version does not match the stored version the handler responds with `409 Conflict` and includes the authoritative matrix.
|
|
||||||
|
|
||||||
## Storage Model
|
|
||||||
|
|
||||||
- The permission matrix is stored in the `admin_settings` table. GORM manages the model via `internal/model/admin_setting.go` and a dedicated migration script (`sql/20250305-admin-settings.sql`).
|
|
||||||
- Each cell records `module_key`, `role`, `enabled`, and a monotonically increasing `version` value. Updates occur inside a single transaction that replaces the existing matrix to guarantee consistency across modules and roles.
|
|
||||||
- The service layer (`internal/service/admin_settings.go`) caches the most recent matrix in-memory and invalidates the cache whenever a write occurs or fails due to a version conflict.
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
|
|
||||||
Integration tests are provided in `api/admin_settings_test.go`:
|
|
||||||
|
|
||||||
- `TestAdminSettingsReadWrite` exercises a full write followed by a read using the operator role.
|
|
||||||
- `TestAdminSettingsUnauthorized` verifies that callers without an admin/operator role receive `403 Forbidden` responses for both GET and POST.
|
|
||||||
- `TestAdminSettingsVersionConflict` validates the optimistic locking path by replaying a stale version and asserting a `409 Conflict` response that echoes the authoritative version.
|
|
||||||
|
|
||||||
Run the suite with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go test ./api -run AdminSettings
|
|
||||||
```
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
# Account Import/Export Enhancement Plan
|
|
||||||
|
|
||||||
## Objectives
|
|
||||||
|
|
||||||
- Extend `account-import` to support merge semantics when replaying account snapshots in existing environments.
|
|
||||||
- Introduce a long-running service mode for `cmd/migratectl` that performs periodic export/import cycles for multi-node synchronization.
|
|
||||||
- Ensure multi-node deployments can exchange account data with predictable consistency and minimal operator intervention.
|
|
||||||
|
|
||||||
## Current Implementation Review
|
|
||||||
|
|
||||||
### Export Flow
|
|
||||||
|
|
||||||
- Command: `go run ./cmd/migratectl/main.go export` (aliased by `make account-export`).
|
|
||||||
- Queries the `users`, `identities`, and `sessions` tables using `internal/migrate/transfer.go`.
|
|
||||||
- Supports optional email keyword filtering and writes a YAML snapshot.
|
|
||||||
|
|
||||||
### Import Flow
|
|
||||||
|
|
||||||
- Command: `go run ./cmd/migratectl/main.go import` (aliased by `make account-import`).
|
|
||||||
- Loads the snapshot, upserts users, then **clears all identities and sessions for the listed users** before re-inserting them.
|
|
||||||
- Operates inside a single transaction and does not currently differentiate between full replacement and merge operations.
|
|
||||||
- Lacks conflict resolution strategies (last-writer-wins, field-level merge, etc.) and does not track provenance or versioning.
|
|
||||||
|
|
||||||
### Operational Constraints
|
|
||||||
|
|
||||||
- CLI-driven; no daemon/service mode.
|
|
||||||
- Operators must manually schedule exports/imports (e.g., via cron) for multi-node synchronization.
|
|
||||||
- No change detection or incremental sync; each import is effectively a full overwrite for included records.
|
|
||||||
|
|
||||||
## Gap Analysis
|
|
||||||
|
|
||||||
1. **Merge Semantics**: Current importer treats incoming snapshot as source of truth. For environments with local mutations, this can cause data loss (e.g., local sessions or new MFA state overwritten).
|
|
||||||
2. **Service Automation**: Requiring cron/scripts for periodic sync increases operational risk and complicates deployments across multiple regions.
|
|
||||||
3. **Configuration**: There is no unified configuration model for describing peer nodes, auth, or scheduling.
|
|
||||||
4. **Observability & Safety**: Missing structured logging, metrics, dry-run safeguards, and auditing for cross-node sync.
|
|
||||||
|
|
||||||
## Proposed Enhancements
|
|
||||||
|
|
||||||
### 1. Merge-capable Importer
|
|
||||||
|
|
||||||
- **CLI UX**: Add a `--merge` (bool) flag to `migratectl import` / `make account-import`. Default remains "replace" for backwards compatibility.
|
|
||||||
- **Merge Strategy**:
|
|
||||||
- User rows: use upsert but preserve missing fields from target when snapshot omits them; optionally track `updated_at` to prefer newer records.
|
|
||||||
- Identities/Sessions: support additive merge. Instead of wholesale delete, diff on primary keys (`uuid`, `token`) and upsert missing/changed rows. Provide `--merge-strategy` (`replace`, `append`, `timestamp`) for future extensibility.
|
|
||||||
- Record conflicts: log decisions and expose counters.
|
|
||||||
- **Safety Mechanisms**:
|
|
||||||
- Optional dry-run mode to preview actions (counts of inserts/updates/deletes).
|
|
||||||
- Configurable allowlist to limit which user UUIDs are eligible for merge.
|
|
||||||
- Validation of snapshot version/schema hash before applying.
|
|
||||||
|
|
||||||
### 2. `migratectl` Service Mode
|
|
||||||
|
|
||||||
- **Command**: `migratectl service` with flags/env for:
|
|
||||||
- `--config` pointing to a YAML/JSON file describing peers (source/target DSN, direction).
|
|
||||||
- `--interval` duration (minimum granularity: minutes) controlling sync frequency.
|
|
||||||
- `--mode` (`export`, `import`, `bi-sync`).
|
|
||||||
- `--once` to run a single cycle for debugging.
|
|
||||||
- **Runtime Behavior**:
|
|
||||||
- Background loop orchestrating export/import using existing logic.
|
|
||||||
- Graceful shutdown via context cancellation/OS signals.
|
|
||||||
- Structured logging (JSON) and optional Prometheus metrics endpoint.
|
|
||||||
- **Deployment Considerations**:
|
|
||||||
- Container-friendly: accept environment variables for DSNs/credentials.
|
|
||||||
- Support multi-node scheduling with leader election toggle (e.g., using advisory locks or external lock service). Initial phase can rely on manual coordination.
|
|
||||||
|
|
||||||
### 3. Configuration & Sync Topology
|
|
||||||
|
|
||||||
- Introduce configuration struct (e.g., `SyncConfig`) with fields:
|
|
||||||
- `Source` / `Target` (DSNs, credentials, TLS options).
|
|
||||||
- `Filters` (email keyword, user groups).
|
|
||||||
- `Merge` options (strategy, dry-run, allowlists).
|
|
||||||
- `Retry` policy (max attempts, backoff).
|
|
||||||
- Allow multiple sync jobs in one config file to support hub-and-spoke replication.
|
|
||||||
- Document reference configuration in `docs/` and provide sample manifest.
|
|
||||||
|
|
||||||
### 4. Observability & Reliability
|
|
||||||
|
|
||||||
- Add per-cycle metrics (duration, records processed, conflicts, errors).
|
|
||||||
- Emit structured logs with job ID, peer names, counts.
|
|
||||||
- Provide health-check endpoint when running in service mode for Kubernetes readiness.
|
|
||||||
- Implement exponential backoff on failures and optional alert hook (e.g., webhook).
|
|
||||||
|
|
||||||
## Evaluation & Next Steps
|
|
||||||
|
|
||||||
1. **Design Review**: Finalize merge semantics and configuration schema with stakeholders.
|
|
||||||
2. **Prototype**: Implement importer merge flag with dry-run to validate data model impact.
|
|
||||||
3. **Service Mode MVP**:
|
|
||||||
- Introduce `service` command using `cobra.Command`.
|
|
||||||
- Implement scheduler loop (`time.Ticker`) with graceful shutdown.
|
|
||||||
- Load sync jobs from config and execute sequentially; parallelism as follow-up.
|
|
||||||
4. **Testing Strategy**:
|
|
||||||
- Unit tests for merge logic (conflict resolution, diffing).
|
|
||||||
- Integration tests using ephemeral PostgreSQL (e.g., Testcontainers) to verify import/export symmetry.
|
|
||||||
- End-to-end acceptance: run service across two DB instances with simulated updates.
|
|
||||||
5. **Documentation**: Update `docs/account-service-deployment.md` with new service mode and configuration guidance.
|
|
||||||
6. **Rollout Plan**: Stage in non-production environment, capture metrics, then enable merge mode incrementally per node.
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
- Do we need bi-directional conflict resolution (two-way merges) or is one node authoritative?
|
|
||||||
- Should session records be merged or always regenerated by local services?
|
|
||||||
- What is the desired behavior for password/MFA conflicts (prefer freshest timestamp, external authority)?
|
|
||||||
- Is there a requirement for encryption/signing of snapshots during transit/storage?
|
|
||||||
|
|
||||||
Addressing these clarifications will shape the detailed implementation plan.
|
|
||||||
|
|
||||||
## Execution Task Breakdown
|
|
||||||
|
|
||||||
To operationalize the plan, execute the following sub-tasks sequentially. Each task lists its prerequisites and expected outputs to simplify coordination across contributors.
|
|
||||||
|
|
||||||
1. **Clarify Merge Semantics (Design Task)**
|
|
||||||
- **Prerequisites**: Gather input from stakeholders on merge requirements, including conflict rules for users, identities, and sessions.
|
|
||||||
- **Actions**: Document chosen merge strategy (field preservation rules, conflict resolution order, snapshot validation needs) and update this plan accordingly.
|
|
||||||
- **Deliverables**: Design decision record or updated specification ready for implementation sign-off.
|
|
||||||
|
|
||||||
2. **Define Configuration Schema**
|
|
||||||
- **Prerequisites**: Approved merge semantics and understanding of deployment environments.
|
|
||||||
- **Actions**: Draft `SyncConfig` Go structs, configuration file layout, and validation rules; prepare example manifests in `docs/`.
|
|
||||||
- **Deliverables**: Schema proposal merged into repository, including sample configuration and documentation references.
|
|
||||||
|
|
||||||
3. **Prototype Merge-capable Importer**
|
|
||||||
- **Prerequisites**: Finalized merge semantics and schema guidance for importer flags/options.
|
|
||||||
- **Actions**: Implement `--merge` flag, dry-run support, identity/session diffing, and logging counters. Add unit tests covering merge scenarios.
|
|
||||||
- **Deliverables**: Pull request containing importer changes, tests, and updated CLI documentation.
|
|
||||||
|
|
||||||
4. **Implement Service Mode MVP**
|
|
||||||
- **Prerequisites**: Prototype importer merged; configuration structs available.
|
|
||||||
- **Actions**: Add `service` command with scheduling loop, context cancellation, and structured logging. Integrate configuration loading and sequential job execution.
|
|
||||||
- **Deliverables**: Running service mode executable with basic observability hooks and documentation updates.
|
|
||||||
|
|
||||||
5. **Add Observability and Reliability Enhancements**
|
|
||||||
- **Prerequisites**: Service mode MVP operational.
|
|
||||||
- **Actions**: Introduce metrics emission, health endpoints, retry/backoff behavior, and optional alerting hooks.
|
|
||||||
- **Deliverables**: Instrumented service with documented metrics/alerts plus corresponding tests where applicable.
|
|
||||||
|
|
||||||
6. **Expand Testing & Automation**
|
|
||||||
- **Prerequisites**: Importer and service features in place.
|
|
||||||
- **Actions**: Build integration tests using ephemeral PostgreSQL, end-to-end service sync scenarios, and update CI pipelines to run them.
|
|
||||||
- **Deliverables**: Automated test suite covering merge/service workflows with CI integration notes.
|
|
||||||
|
|
||||||
7. **Documentation & Rollout Preparation**
|
|
||||||
- **Prerequisites**: Feature implementation stabilized.
|
|
||||||
- **Actions**: Update `docs/account-service-deployment.md`, produce operator runbooks, and outline staged rollout/monitoring steps.
|
|
||||||
- **Deliverables**: Comprehensive documentation bundle and rollout checklist ready for production enablement.
|
|
||||||
|
|
||||||
8. **Operational Review & Handoff**
|
|
||||||
- **Prerequisites**: All technical tasks completed and documented.
|
|
||||||
- **Actions**: Conduct readiness review, gather final approvals, and schedule deployment. Ensure metrics and alerting are monitored during rollout.
|
|
||||||
- **Deliverables**: Signed-off deployment plan and ownership handoff notes.
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
# Account Service 配置指南
|
|
||||||
|
|
||||||
本文档说明账号服务可用的配置项、加载顺序以及示例,方便在不同环境中快速调整运行参数。
|
|
||||||
|
|
||||||
## 1. 配置加载策略
|
|
||||||
|
|
||||||
账号服务入口(`cmd/accountsvc/main.go`)会调用 `config.Load` 读取 YAML 配置,并允许通过命令行参数覆盖默认路径。当未提供配置文件时,服务会以零值启动,此时可结合环境变量填充关键字段。
|
|
||||||
|
|
||||||
当前推荐的覆盖顺序如下:
|
|
||||||
|
|
||||||
1. **命令行参数**:用于指定配置文件路径或运行模式。
|
|
||||||
2. **配置文件**:默认从 `config/account.yaml` 读取,适合提交到仓库或挂载到容器内。
|
|
||||||
3. **代码默认值**:`config.Config` 结构体中的零值,保证最小可运行。
|
|
||||||
|
|
||||||
> 注:目前服务尚未内置环境变量映射逻辑,如需按环境注入配置,可在部署流程中提前生成 YAML 文件或扩展 `config.Load`。
|
|
||||||
|
|
||||||
## 2. 配置字段参考
|
|
||||||
|
|
||||||
`config/config.go` 定义了配置结构,主要包含以下几个部分:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
log:
|
|
||||||
level: info # 可选:debug、info、warn、error
|
|
||||||
|
|
||||||
server:
|
|
||||||
addr: ":8080" # 监听地址
|
|
||||||
readTimeout: 15s # 读取超时
|
|
||||||
writeTimeout: 15s # 写入超时
|
|
||||||
tls: # 启用 HTTPS 时的证书配置
|
|
||||||
enabled: true # 显式启用/关闭 TLS(为空时仍根据证书路径推断)
|
|
||||||
certFile: "/etc/ssl/certs/account.pem"
|
|
||||||
keyFile: "/etc/ssl/private/account.key"
|
|
||||||
clientCAFile: "" # (可选)双向 TLS CA
|
|
||||||
redirectHttp: false # 当启用 TLS 时是否同时监听 HTTP 做 301 重定向
|
|
||||||
|
|
||||||
store:
|
|
||||||
driver: "postgres" # 可选:memory、postgres
|
|
||||||
dsn: "postgres://user:pass@db:5432/account?sslmode=disable"
|
|
||||||
maxOpenConns: 30
|
|
||||||
maxIdleConns: 10
|
|
||||||
|
|
||||||
session:
|
|
||||||
ttl: 24h # 登录会话有效期
|
|
||||||
|
|
||||||
smtp:
|
|
||||||
host: "smtp.example.com" # SMTP 服务地址
|
|
||||||
port: 587 # 端口,587 对应 STARTTLS,465 可用于 SMTPS
|
|
||||||
username: "apikey" # 登录用户名或 API Key
|
|
||||||
p: "s" # 登录密码,生产环境建议使用 Secret 管理
|
|
||||||
from: "XControl <no-reply@example.com>" # 发件人展示名称+地址
|
|
||||||
replyTo: "" # (可选)Reply-To 地址
|
|
||||||
timeout: 10s # 连接与发送超时
|
|
||||||
tls:
|
|
||||||
mode: "starttls" # 可选 starttls 或 implicit(SMTPS)
|
|
||||||
insecureSkipVerify: false # 是否跳过证书校验,默认 false
|
|
||||||
```
|
|
||||||
|
|
||||||
**TLS 提示**:当 `tls.enabled` 显式为 `true` 时或 `certFile` 与 `keyFile` 均提供时,`accountsvc` 会调用 `ListenAndServeTLS` 启动 HTTPS。需要在开发环境暂时关闭 TLS,可将 `tls.enabled` 设为 `false`,此时服务会忽略证书路径并仅监听 HTTP。如果同时希望保留 80 端口,可将 `redirectHttp` 置为 `true`,服务会开启一个额外的明文监听,将请求 301 重定向到 HTTPS。
|
|
||||||
|
|
||||||
**MFA 相关接口**:账号服务在 `/api/auth/mfa/*` 下提供 MFA 绑定与验证接口,默认无需额外配置即可使用,但生产环境建议将 `server.tls` 打开,确保 MFA 秘钥与 TOTP 码在传输过程中被加密。MFA 挑战 token 默认 10 分钟过期,服务器会接受 ±1 个 30 秒窗口的 TOTP 漂移,因此务必启用 NTP 等时间同步手段,避免合法验证码因时钟偏差被拒绝。
|
|
||||||
|
|
||||||
## 3. 配置示例
|
|
||||||
|
|
||||||
### 3.1 开发环境(HTTP + 内存存储)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
log:
|
|
||||||
level: debug
|
|
||||||
server:
|
|
||||||
addr: ":8080"
|
|
||||||
readTimeout: 0s
|
|
||||||
writeTimeout: 0s
|
|
||||||
store:
|
|
||||||
driver: "memory"
|
|
||||||
session:
|
|
||||||
ttl: 8h
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 生产环境(PostgreSQL + HTTPS + MFA)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
log:
|
|
||||||
level: info
|
|
||||||
server:
|
|
||||||
addr: ":8443"
|
|
||||||
readTimeout: 15s
|
|
||||||
writeTimeout: 15s
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
certFile: "/etc/ssl/certs/account.pem"
|
|
||||||
keyFile: "/etc/ssl/private/account.key"
|
|
||||||
redirectHttp: true
|
|
||||||
store:
|
|
||||||
driver: "postgres"
|
|
||||||
dsn: "postgres://account:strongpass@db:5432/account?sslmode=require"
|
|
||||||
maxOpenConns: 50
|
|
||||||
maxIdleConns: 10
|
|
||||||
session:
|
|
||||||
ttl: 24h
|
|
||||||
```
|
|
||||||
|
|
||||||
在生产环境中,建议通过 Kubernetes Secret、Vault 等方式挂载证书文件,并使用 `redirectHttp` 确保历史链接能够自动切换到 HTTPS。
|
|
||||||
|
|
||||||
## 4. 配置校验与回滚
|
|
||||||
|
|
||||||
- 启动时若启用 PostgreSQL,请确保 `dsn` 可用,否则服务会在初始化阶段返回错误。
|
|
||||||
- TLS 文件路径错误会导致启动失败,建议在 CI/CD 中加入探针验证。
|
|
||||||
- 通过 Git 管理配置文件,配合版本标签可实现快速回滚。
|
|
||||||
|
|
||||||
## 5. 与其他模块的协同
|
|
||||||
|
|
||||||
- 登录会话 TTL 会同步影响 `/api/auth/login`、`/api/auth/session` 等接口返回的 cookie 过期时间。
|
|
||||||
- `smtp` 配置用于注册验证、密码重置等事务性邮件发送,支持 STARTTLS 与 SMTPS(将 `mode` 设为 `implicit` 并将端口改为 465)。在生产环境建议关闭 `insecureSkipVerify` 并使用专用发信账户或 API Key。
|
|
||||||
- 新增的 MFA 接口(`/api/auth/mfa/totp/provision`、`/api/auth/mfa/totp/verify`、`/api/auth/mfa/status`)在 HTTPS 环境下可与前端 MFA 向导配合使用,保证首次登录后必须完成绑定。
|
|
||||||
- 如果部署了前端 Next.js 应用,请确保其 `.env` 中的 `ACCOUNT_API_BASE` 指向启用了 TLS 的账号服务地址。
|
|
||||||
|
|
||||||
随着服务演进,请在更新配置结构或新字段时同步维护本文档。
|
|
||||||
@ -1,371 +0,0 @@
|
|||||||
# Account Service 部署指南
|
|
||||||
|
|
||||||
本文档介绍如何在不同环境中部署 XControl 账号服务,包括本地开发、容器化以及生产环境的关键注意事项。
|
|
||||||
|
|
||||||
## 1. 运行时依赖
|
|
||||||
|
|
||||||
- Go 1.22 及以上版本,用于编译和运行服务。
|
|
||||||
- PostgreSQL(推荐)或内存存储:MFA 状态、TOTP 秘钥等信息会持久化在用户表中,生产环境请使用数据库。
|
|
||||||
- (可选)反向代理或负载均衡器,用于在 TLS 终止后分发流量。
|
|
||||||
|
|
||||||
## 2. 本地开发部署
|
|
||||||
|
|
||||||
1. **拉取代码**
|
|
||||||
```bash
|
|
||||||
git clone <repo-url>
|
|
||||||
cd XControl
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **准备配置**
|
|
||||||
使用仓库提供的 `config/account.yaml`,或根据需要拷贝一份修改端口、数据库连接等字段。
|
|
||||||
|
|
||||||
3. **启动服务(HTTP)**
|
|
||||||
```bash
|
|
||||||
go run ./cmd/accountsvc --config config/account.yaml
|
|
||||||
```
|
|
||||||
默认监听 `:8080`,可通过 `curl http://127.0.0.1:8080/healthz` 检查服务状态。
|
|
||||||
|
|
||||||
4. **交互测试:注册、绑定 MFA 与登录**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 注册账号
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/register \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"name":"demo","email":"demo@example.com","password":"Secret123"}'
|
|
||||||
|
|
||||||
# 初次登录以获取 MFA 挑战 token(返回 401,并携带 mfaToken)
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/login \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"identifier":"demo@example.com","password":"Secret123"}'
|
|
||||||
|
|
||||||
# 请求 TOTP 秘钥(返回二维码和 Base32 密钥)
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/mfa/totp/provision \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"token":"<MFA_TOKEN_FROM_LOGIN>"}'
|
|
||||||
|
|
||||||
# 使用 oathtool 或 Google Authenticator 生成一次性验证码
|
|
||||||
oathtool --totp -b <BASE32_SECRET>
|
|
||||||
|
|
||||||
# 验证并启用 MFA(首次会返回会话 token)
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/mfa/totp/verify \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"token":"<MFA_TOKEN_FROM_LOGIN>","code":"123456"}'
|
|
||||||
|
|
||||||
# 带口令 + TOTP 登录
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/login \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-c cookies.txt \
|
|
||||||
-d '{"identifier":"demo@example.com","password":"Secret123","totpCode":"123456"}'
|
|
||||||
|
|
||||||
# 或使用邮箱 + TOTP 极简模式
|
|
||||||
curl -X POST http://127.0.0.1:8080/api/auth/login \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-c cookies.txt \
|
|
||||||
-d '{"identifier":"demo@example.com","totpCode":"123456"}'
|
|
||||||
|
|
||||||
# 查看当前会话
|
|
||||||
curl -b cookies.txt http://127.0.0.1:8080/api/auth/session | jq
|
|
||||||
|
|
||||||
# 预期响应示例(展示角色、用户组与权限列表)
|
|
||||||
# {
|
|
||||||
# "user": {
|
|
||||||
# "uuid": "72c70df9-b7b6-4e81-84ef-5f0e5b1fc7c6",
|
|
||||||
# "name": "demo",
|
|
||||||
# "email": "demo@example.com",
|
|
||||||
# "role": "user",
|
|
||||||
# "groups": ["User"],
|
|
||||||
# "permissions": ["session:read"]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
若需要重新绑定 MFA,可再次发起登录以获取新的 `mfaToken`,然后重复 `provision` → `verify` 流程;如需彻底重置,可在数据库中清理相关 MFA 字段后重新执行上述步骤。
|
|
||||||
|
|
||||||
> 时间同步提示:TOTP 验证允许 ±1 个 30 秒时间片的偏移,但依赖服务器与客户端时钟保持一致。请在部署环境中启用 NTP/Chrony 等服务,并注意 `mfaToken` 默认 10 分钟后失效。
|
|
||||||
|
|
||||||
## 3. 启用 HTTPS/TLS
|
|
||||||
|
|
||||||
账号服务内置 TLS 支持,只要在配置文件中提供证书即可:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
addr: ":8443"
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
certFile: "/etc/ssl/certs/account.pem"
|
|
||||||
keyFile: "/etc/ssl/private/account.key"
|
|
||||||
clientCAFile: "" # (可选)配置客户端证书验证
|
|
||||||
redirectHttp: true
|
|
||||||
```
|
|
||||||
|
|
||||||
启动命令保持不变:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/accountsvc --config /path/to/secure-account.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
常见验证步骤:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 生成测试证书(示例)
|
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
||||||
-keyout account.key -out account.crt \
|
|
||||||
-subj "/CN=localhost"
|
|
||||||
|
|
||||||
# 更新配置后启动服务
|
|
||||||
ACCOUNT_CONFIG=/tmp/account-secure.yaml go run ./cmd/accountsvc --config $ACCOUNT_CONFIG
|
|
||||||
|
|
||||||
# 使用 curl 验证 HTTPS(开发环境可加 -k 跳过校验)
|
|
||||||
curl -k https://127.0.0.1:8443/healthz
|
|
||||||
```
|
|
||||||
|
|
||||||
当 `redirectHttp` 为 `true` 时,服务会自动监听对应的 HTTP 端口(通常是 80),并将请求 301 重定向到 HTTPS,方便旧链接或未更新的客户端。
|
|
||||||
如需启用双向 TLS,可将 `clientCAFile` 指向受信任的 CA 证书,服务会校验客户端证书并拒绝未签发的连接。
|
|
||||||
|
|
||||||
> **反向代理提示**:若在 Nginx、Envoy 等反向代理后运行账号服务,可选择在代理层终止 TLS,并将 `server.tls` 字段留空。此时应确保代理转发 `X-Forwarded-Proto`/`X-Forwarded-Host` 等头部,以便后端生成正确回调地址。若代理和服务都启用了 HTTPS,则保持 `redirectHttp=false`,避免出现重复重定向。
|
|
||||||
|
|
||||||
## 3.1 Caddy + stunnel 入口与数据库隧道
|
|
||||||
|
|
||||||
适用于以下目标:
|
|
||||||
|
|
||||||
- 入口域名为 `https://accounts.svc.plus`,由 Caddy 统一签发和续期证书。
|
|
||||||
- PostgreSQL 永不暴露公网,只通过 stunnel 建立 TLS 隧道。
|
|
||||||
- 架构位置无关、平台无关,跨云复用同一套配置。
|
|
||||||
|
|
||||||
示意路径:
|
|
||||||
|
|
||||||
```
|
|
||||||
入口: https://accounts.svc.plus
|
|
||||||
API
|
|
||||||
│
|
|
||||||
│ localhost:15432
|
|
||||||
▼
|
|
||||||
stunnel (TLS)
|
|
||||||
│
|
|
||||||
│ 明文
|
|
||||||
▼
|
|
||||||
PostgreSQL :5432
|
|
||||||
```
|
|
||||||
|
|
||||||
工程师式总结:
|
|
||||||
|
|
||||||
> Caddy 管“对外身份”,stunnel 管“对内通道”。
|
|
||||||
|
|
||||||
模板文件:
|
|
||||||
|
|
||||||
- `deploy/caddy/Caddyfile.accounts.svc.plus`
|
|
||||||
- `deploy/stunnel/stunnel-account-db-client.conf`
|
|
||||||
- `deploy/stunnel/stunnel-account-db-server.conf`
|
|
||||||
- `deploy/systemd/caddy-accounts.service`
|
|
||||||
- `deploy/systemd/stunnel-account-db-client.service`
|
|
||||||
- `deploy/systemd/stunnel-account-db-server.service`
|
|
||||||
- `deploy/docker-compose/caddy-stunnel/docker-compose.account.yaml`
|
|
||||||
- `deploy/docker-compose/caddy-stunnel/docker-compose.db.yaml`
|
|
||||||
|
|
||||||
示例 Caddyfile(外部 TLS 入口):
|
|
||||||
|
|
||||||
```caddyfile
|
|
||||||
accounts.svc.plus {
|
|
||||||
reverse_proxy 127.0.0.1:8080
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
示例 stunnel client(API/Account 服务所在机器):
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[postgres-client]
|
|
||||||
client = yes
|
|
||||||
accept = 127.0.0.1:15432
|
|
||||||
connect = postgresql.onwalk.net:443
|
|
||||||
verify = 2
|
|
||||||
CAfile = /etc/ssl/certs/ca-certificates.crt
|
|
||||||
checkHost = postgresql.onwalk.net
|
|
||||||
```
|
|
||||||
|
|
||||||
示例 stunnel server(数据库所在机器):
|
|
||||||
|
|
||||||
```ini
|
|
||||||
accept = 0.0.0.0:8443
|
|
||||||
connect = 127.0.0.1:5432
|
|
||||||
```
|
|
||||||
|
|
||||||
将账号服务的数据库连接指向 `127.0.0.1:15432`,即可通过 stunnel 访问远端
|
|
||||||
PostgreSQL,且对外只暴露 Caddy 的 HTTPS 入口。
|
|
||||||
|
|
||||||
Systemd 示例(可按需调整路径与二进制):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 入口机:Caddy + stunnel client
|
|
||||||
sudo install -d /etc/caddy /etc/stunnel
|
|
||||||
sudo cp deploy/caddy/Caddyfile.accounts.svc.plus /etc/caddy/Caddyfile
|
|
||||||
sudo cp deploy/stunnel/stunnel-account-db-client.conf /etc/stunnel/
|
|
||||||
sudo cp deploy/systemd/caddy-accounts.service /etc/systemd/system/
|
|
||||||
sudo cp deploy/systemd/stunnel-account-db-client.service /etc/systemd/system/
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now caddy-accounts.service
|
|
||||||
sudo systemctl enable --now stunnel-account-db-client.service
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 数据库机:stunnel server
|
|
||||||
sudo install -d /etc/stunnel
|
|
||||||
sudo cp deploy/stunnel/stunnel-account-db-server.conf /etc/stunnel/
|
|
||||||
sudo cp deploy/systemd/stunnel-account-db-server.service /etc/systemd/system/
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now stunnel-account-db-server.service
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker Compose 示例(使用 host 网络,便于绑定本机端口):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 入口机:Caddy + stunnel client
|
|
||||||
docker compose -f deploy/docker-compose/caddy-stunnel/docker-compose.account.yaml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 数据库机:stunnel server
|
|
||||||
docker compose -f deploy/docker-compose/caddy-stunnel/docker-compose.db.yaml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Docker 部署
|
|
||||||
|
|
||||||
1. **构建镜像(示例)**
|
|
||||||
```bash
|
|
||||||
docker build -t xcontrol/account-service -f Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **运行容器(挂载配置与证书)**
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name account-service \
|
|
||||||
-p 8443:8443 \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-v $(pwd)/config/account.yaml:/etc/xcontrol/account.yaml \
|
|
||||||
-v $(pwd)/certs:/etc/ssl/xcontrol \
|
|
||||||
xcontrol/account-service \
|
|
||||||
--config /etc/xcontrol/account.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
如果未启用 `redirectHttp`,可省略 `-p 8080:8080`。
|
|
||||||
|
|
||||||
3. **查看日志**
|
|
||||||
```bash
|
|
||||||
docker logs -f account-service
|
|
||||||
```
|
|
||||||
|
|
||||||
确保容器内路径与配置文件中的 `certFile`/`keyFile` 一致,必要时可通过 Docker Secret 或 Kubernetes Secret 注入敏感文件。
|
|
||||||
|
|
||||||
## 5. Kubernetes/Helm 部署
|
|
||||||
|
|
||||||
- 在 `deploy/account` 目录中维护 Helm Chart 或 Kustomize 模板,定义 Service、Deployment、ConfigMap 等资源。
|
|
||||||
- 关键参数:
|
|
||||||
- 副本数 `replicaCount`,生产环境建议至少 2 个副本以实现高可用。
|
|
||||||
- 探针:配置 `livenessProbe` 与 `readinessProbe` 指向 `/healthz`。
|
|
||||||
- 证书管理:使用 Secret 存储 TLS 证书与私钥,挂载到容器后与配置文件对应。
|
|
||||||
- 数据库凭证:同样通过 Secret 注入 `ACCOUNT_STORE_DSN` 或配置文件。
|
|
||||||
|
|
||||||
## 6. 灰度与回滚策略
|
|
||||||
|
|
||||||
- 建议采用 RollingUpdate 策略滚动发布,确保新旧副本并行运行。
|
|
||||||
- 配置 `maxUnavailable=0`、`maxSurge=1`(或按需调整),避免服务中断。
|
|
||||||
- 通过标记镜像版本或 Git Commit Hash 追踪上线版本,出问题时可快速回滚至上一版本。
|
|
||||||
|
|
||||||
## 7. 监控与日志
|
|
||||||
|
|
||||||
- 日志:默认输出到标准输出,可挂载至日志采集系统(如 Loki、ELK)。
|
|
||||||
- 指标:可在后续版本中集成 Prometheus 指标,关注登录成功率、MFA 启用率等核心指标。
|
|
||||||
- 告警:基于探针失败、登录失败率飙升、TOTP 验证异常等指标配置告警策略。
|
|
||||||
|
|
||||||
## 8. 安全加固建议
|
|
||||||
|
|
||||||
- 在容器或集群层启用网络策略,仅开放必要端口。
|
|
||||||
- 对外提供服务时务必启用 HTTPS,保护登录口令与 TOTP 码。
|
|
||||||
- 对数据库、证书等敏感资源使用最小权限原则,并定期轮换。
|
|
||||||
- 定期回顾 `api/api_test.go` 中的场景测试,确保关键登录链路持续可用。
|
|
||||||
|
|
||||||
## 9. 数据库备份、迁移与回滚示例
|
|
||||||
|
|
||||||
> 以下示例假设 PostgreSQL 运行在 `localhost`,数据库名称为 `account`, 用户为 `xcontrol`。根据实际环境替换连接信息。
|
|
||||||
|
|
||||||
1. **迁移前备份**
|
|
||||||
|
|
||||||
在应用任何结构变更前,先导出当前库或指定表:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pg_dump -h localhost -U xcontrol -d account > backup_before_role_metadata.sql
|
|
||||||
# 仅备份 users 表可使用:
|
|
||||||
pg_dump -h localhost -U xcontrol -d account -t public.users > backup_users_only.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **执行角色元数据迁移**
|
|
||||||
|
|
||||||
若数据库仍是旧版本(缺少 `role`、`groups`、`permissions` 列),可通过 `psql` 在事务中执行以下语句:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE public.users
|
|
||||||
ADD COLUMN IF NOT EXISTS level INTEGER DEFAULT 20 NOT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user' NOT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS groups JSONB DEFAULT '[]'::jsonb NOT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS permissions JSONB DEFAULT '[]'::jsonb NOT NULL;
|
|
||||||
|
|
||||||
UPDATE public.users
|
|
||||||
SET role = CASE level
|
|
||||||
WHEN 0 THEN 'admin'
|
|
||||||
WHEN 10 THEN 'operator'
|
|
||||||
ELSE 'user'
|
|
||||||
END,
|
|
||||||
groups = CASE level
|
|
||||||
WHEN 0 THEN '["Admin"]'::jsonb
|
|
||||||
WHEN 10 THEN '["Operator"]'::jsonb
|
|
||||||
ELSE '["User"]'::jsonb
|
|
||||||
END,
|
|
||||||
permissions = CASE level
|
|
||||||
WHEN 0 THEN '["session:read","session:write","user:manage"]'::jsonb
|
|
||||||
WHEN 10 THEN '["session:read","session:write"]'::jsonb
|
|
||||||
ELSE '["session:read"]'::jsonb
|
|
||||||
END
|
|
||||||
WHERE role IS NULL OR role = '' OR groups = '[]'::jsonb;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
> **提示**:如已在 CI/CD 中托管 `sql/schema.sql`,也可直接执行 `psql -h ... -f sql/schema.sql`,该脚本为幂等实现,会自动跳过已有对象。
|
|
||||||
|
|
||||||
3. **验证数据**
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT username, level, role, groups, permissions
|
|
||||||
FROM public.users
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 5;
|
|
||||||
```
|
|
||||||
|
|
||||||
预期 `level` 与 `role` 一致,并且新注册用户属于 `User` 组。
|
|
||||||
|
|
||||||
4. **回滚策略**
|
|
||||||
|
|
||||||
- **快速恢复**:如迁移失败,可直接用备份文件恢复:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql -h localhost -U xcontrol -d account < backup_before_role_metadata.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
- **局部回退**:若仅需删除新增列,可执行:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE public.users
|
|
||||||
DROP COLUMN IF EXISTS permissions,
|
|
||||||
DROP COLUMN IF EXISTS groups,
|
|
||||||
DROP COLUMN IF EXISTS role,
|
|
||||||
DROP COLUMN IF EXISTS level;
|
|
||||||
COMMIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
恢复后重新运行 `schema.sql` 或上述迁移脚本,即可重新引入角色元数据。
|
|
||||||
|
|
||||||
---
|
|
||||||
以上步骤覆盖从开发到生产的核心流程,可根据企业环境补充额外的安全、审计或合规要求。
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
# Account Service 设计说明
|
|
||||||
|
|
||||||
本文档描述 `account` 目录下账号服务的现状与演进方向,帮助研发、测试与运维人员快速理解该组件的职责、核心流程与可扩展性。
|
|
||||||
|
|
||||||
## 1. 背景与目标
|
|
||||||
|
|
||||||
账号服务用于在 XControl 生态内提供统一的注册、登录与会话查询能力,为后续的权限管控、业务系统集成提供基础身份数据。
|
|
||||||
|
|
||||||
主要目标:
|
|
||||||
|
|
||||||
- 提供面向用户的注册与登录接口,支持以邮箱为主键的账号体系。
|
|
||||||
- 提供标准的健康检查与会话管理接口,便于其它服务探活与拉取当前登录用户信息。
|
|
||||||
- 提供可替换的存储与认证接口,满足从 PoC 到生产的不同部署需求。
|
|
||||||
|
|
||||||
## 2. 系统架构
|
|
||||||
|
|
||||||
账号服务采用 Go 语言实现,入口位于 `cmd/accountsvc/main.go`,默认使用 Gin 框架启动 HTTP 服务并注册 REST API 路由。【F:cmd/accountsvc/main.go†L1-L12】
|
|
||||||
|
|
||||||
核心模块划分如下:
|
|
||||||
|
|
||||||
- `api`: 定义 REST API,并实现用户注册、登录、会话维护等业务逻辑。【F:api/api.go†L1-L190】
|
|
||||||
- `internal/store`: 提供用户数据的读写接口与内存实现,后续可扩展至数据库存储。【F:internal/store/store.go†L1-L109】
|
|
||||||
- `internal/auth`: 声明可插拔的第三方认证提供方接口,为接入 LDAP/OIDC 等外部系统提供抽象。【F:internal/auth/auth.go†L1-L6】
|
|
||||||
- `internal/cache`: 预留会话缓存接口,便于集成 Redis 等缓存组件。【F:internal/cache/cache.go†L1-L6】
|
|
||||||
- `config`: 管理服务配置结构体(当前为空定义,未来将扩展字段)。【F:config/config.go†L1-L5】
|
|
||||||
|
|
||||||
内部调用关系示意:
|
|
||||||
|
|
||||||
```
|
|
||||||
Gin Router → API Handler → Store / Session Manager → 数据存储
|
|
||||||
↘ Auth Provider (可选)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 接口设计
|
|
||||||
|
|
||||||
### 3.1 健康检查
|
|
||||||
- `GET /healthz`
|
|
||||||
- 返回 `{ "status": "ok" }`,供探活或依赖服务检测。
|
|
||||||
|
|
||||||
### 3.2 用户注册
|
|
||||||
|
|
||||||
账号服务现在将注册拆分为明确的三个阶段,先验证邮箱可达性,再完成账户写入:
|
|
||||||
|
|
||||||
1. `POST /api/auth/register/send`
|
|
||||||
- 请求体:`{ "email": string }`
|
|
||||||
- 功能:检查邮箱是否已被注册或已验证的用户占用。若邮箱可用,则生成 6 位验证码、写入内存映射 `registrationVerifications` 并通过配置的邮件发送器下发验证码。
|
|
||||||
2. `POST /api/auth/register/verify`
|
|
||||||
- 请求体:`{ "email": string, "code": string }`
|
|
||||||
- 功能:校验验证码是否匹配且未过期,成功后将对应记录标记为 `verified`,供下一步注册使用。若邮箱已经存在且仍待验证,则沿用旧逻辑,对存量用户执行验证并返回登录会话。
|
|
||||||
3. `POST /api/auth/register`
|
|
||||||
- 请求体:`{ "email": string, "password": string, "code": string, "name"?: string }`
|
|
||||||
- 功能:要求验证码已经过第二步确认;通过 `bcrypt` 哈希密码、写入用户信息,并在成功后清理 `registrationVerifications` 中的临时记录。创建完成时直接将 `EmailVerified` 置为 `true`,避免重复发信。
|
|
||||||
|
|
||||||
该流程确保前端可以先提示“验证码已发送”,再指引用户输入验证码并解锁“完成注册”按钮,避免旧版“先注册、后发验证码”导致的混淆。服务端新增的 `registrationVerification` 结构体用来管理待注册邮箱的验证码、过期时间与校验状态,并与既有的已注册用户邮箱验证逻辑共存。
|
|
||||||
|
|
||||||
### 3.3 用户登录
|
|
||||||
- `POST /v1/login`
|
|
||||||
- 请求体:`{ "email": string, "password": string }`
|
|
||||||
- 功能:校验凭据,通过内存存储读取用户并验证哈希密码,成功后生成 24 小时有效的会话 token。【F:api/api.go†L65-L136】
|
|
||||||
|
|
||||||
### 3.4 查询会话
|
|
||||||
- `GET /v1/session`
|
|
||||||
- Header 中提供 `Authorization: Bearer <token>` 或查询参数 `token`。
|
|
||||||
- 功能:校验 token,返回关联用户信息。【F:api/api.go†L138-L176】
|
|
||||||
|
|
||||||
### 3.5 注销会话
|
|
||||||
- `DELETE /v1/session`
|
|
||||||
- Header 或查询参数传入 token,删除内存中的会话记录。【F:api/api.go†L178-L190】
|
|
||||||
|
|
||||||
## 4. 数据模型
|
|
||||||
|
|
||||||
当前实现使用内存存储,结构体 `store.User` 定义了最小必要字段:`ID`、`Name`、`Email`、`PasswordHash` 与 `CreatedAt` 时间戳。【F:internal/store/store.go†L12-L18】
|
|
||||||
|
|
||||||
`memoryStore` 负责提供线程安全的增删查能力,并在创建用户时自动生成 UUID 与 UTC 时间,保证多实例场景中的唯一性。未来替换为数据库时,可在 `Store` 接口的基础上新增实现即可。【F:internal/store/store.go†L31-L109】
|
|
||||||
|
|
||||||
## 5. 安全与扩展
|
|
||||||
|
|
||||||
- **密码存储**:使用 `bcrypt` 哈希,防止明文泄露。【F:api/api.go†L90-L108】
|
|
||||||
- **会话管理**:会话 token 为 32 字节随机数生成的十六进制字符串,并设置 24 小时过期,过期后自动清理。【F:api/api.go†L112-L171】
|
|
||||||
- **扩展点**:
|
|
||||||
- 可在 `Store` 接口层新增 PostgreSQL、MySQL 等实现。
|
|
||||||
- 可实现 `auth.Provider` 接口以支持外部身份源认证,再与内部用户绑定。
|
|
||||||
- 可基于 `cache.Cache` 抽象接入 Redis,实现跨实例的会话共享。
|
|
||||||
|
|
||||||
## 6. 后续计划
|
|
||||||
|
|
||||||
1. 丰富 `config.Config` 字段,支持从 YAML/ENV 读取监听端口、数据库、缓存等配置。
|
|
||||||
2. 将内存会话迁移到可持久化/分布式缓存,支持水平扩展。
|
|
||||||
3. 引入审计日志、登录失败限制等安全机制。
|
|
||||||
4. 整合统一的错误码与 API 文档输出,便于前后端协同。
|
|
||||||
|
|
||||||
## 7. 自动化测试建议
|
|
||||||
|
|
||||||
- **Playwright MCP 录制**:使用 Playwright MCP 录制“提交邮箱和密码 → 请求验证码 → 在临时邮箱读取验证码 → 回填并完成注册”的完整流程,可在桌面浏览器和移动端视口下回放,验证 UI 状态和接口契约是否一致。
|
|
||||||
- **语言偏好**:可以选用 Node.js 或 Python 版 Playwright 脚手架生成脚本,结合 MCP 的断言与截图能力对验证码弹窗、按钮禁用态、错误提示进行校验,并在 CI 中复用。
|
|
||||||
- **服务端校验**:配合脚本断言账号服务的 `/api/auth/register/send`、`/verify`、`/register` 依次返回 200/200/201,确保验证码会话的生命周期与 TTL 行为符合预期。
|
|
||||||
|
|
||||||
---
|
|
||||||
本文档需根据功能演进持续维护,以确保服务的设计意图与实现保持一致。
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# accounts.svc.plus 设计文档
|
|
||||||
|
|
||||||
本文档基于现有项目结构,描述一个轻量级的账号服务 **accounts.svc.plus** 的设计方案。
|
|
||||||
|
|
||||||
## 1. 功能概述
|
|
||||||
|
|
||||||
- 提供统一的用户身份认证与授权接口。
|
|
||||||
- 支持企业常用的三种协议:LDAP、OIDC、SAML2.0。
|
|
||||||
- 采用 PostgreSQL 作为持久化存储,Redis 作为缓存与会话存储。
|
|
||||||
- 以 Go 语言实现,延续项目中 `gin` 框架的使用,确保高并发与安全性。
|
|
||||||
- 预留模块化扩展能力,方便未来接入更多身份源或业务逻辑。
|
|
||||||
|
|
||||||
## 2. 总体架构
|
|
||||||
|
|
||||||
```
|
|
||||||
+---------------+ +------------------+
|
|
||||||
| LDAP / OIDC / | Auth | accounts.svc |
|
|
||||||
| SAML IdP +-------->+------------------+-----> PostgreSQL
|
|
||||||
+---------------+ | REST / gRPC |
|
|
||||||
| gin + goroutine|
|
|
||||||
+------------------+-----> Redis
|
|
||||||
```
|
|
||||||
|
|
||||||
服务以 `cmd/accountsvc/main.go` 作为入口,内部划分如下模块:
|
|
||||||
|
|
||||||
- `internal/auth`: 封装 LDAP、OIDC、SAML2.0 适配器,统一认证接口。
|
|
||||||
- `internal/store`: 使用 `pgx` 连接 PostgreSQL,定义用户、会话、绑定等模型。
|
|
||||||
- `internal/cache`: 基于 `go-redis` 实现 token、会话的缓存与黑名单。
|
|
||||||
- `api`: 提供 `/login`、`/logout`、`/userinfo` 等 REST 接口,可按需扩展 gRPC。
|
|
||||||
- `config`: 参照 `rag-server/config` 风格,提供 YAML/ENV 配置解析。
|
|
||||||
|
|
||||||
## 3. 协议支持
|
|
||||||
|
|
||||||
### 3.1 LDAP
|
|
||||||
- 使用 `github.com/go-ldap/ldap`,支持绑定验证和属性同步。
|
|
||||||
- 可配置多个 LDAP 服务器与搜索基准,具备 failover 能力。
|
|
||||||
|
|
||||||
### 3.2 OIDC
|
|
||||||
- 基于 `github.com/coreos/go-oidc` 实现授权码流程。
|
|
||||||
- 通过 JWT 校验、State/Nonce 防重放,结合 Redis 存储 session。
|
|
||||||
|
|
||||||
### 3.3 SAML2.0
|
|
||||||
- 使用 `github.com/crewjam/saml` 适配 SAML 认证。
|
|
||||||
- 支持元数据导入和签名校验,回调地址与证书在配置中管理。
|
|
||||||
|
|
||||||
各协议通过 `internal/auth` 中的接口抽象统一输出用户信息,便于后续扩展更多身份源。
|
|
||||||
|
|
||||||
## 4. 数据与缓存
|
|
||||||
|
|
||||||
- **PostgreSQL**:
|
|
||||||
- 表结构包括 `users`、`identities`、`sessions` 等。
|
|
||||||
- 使用 `pgxpool` 管理连接池,利用事务保障一致性。
|
|
||||||
- **Redis**:
|
|
||||||
- 保存登录 session、验证码、临时 token。
|
|
||||||
- 设置合理的过期策略并启用哨兵或集群模式提高可用性。
|
|
||||||
|
|
||||||
### 4.1 表结构草案
|
|
||||||
|
|
||||||
`sql/schema.sql` 维护初始建表脚本:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
email TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS identities (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
provider TEXT NOT NULL,
|
|
||||||
external_id TEXT NOT NULL,
|
|
||||||
UNIQUE(provider, external_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token TEXT NOT NULL,
|
|
||||||
expires_at TIMESTAMPTZ NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 高并发与安全
|
|
||||||
|
|
||||||
- `gin` 提供高性能路由与中间件机制,结合 `goroutine` 实现并发处理。
|
|
||||||
- 使用 `net/http` 标准库的 `http.Server` 配置 `Read/Write/Idle Timeout`,防止慢连接攻击。
|
|
||||||
- 中间件:
|
|
||||||
- 访问日志、限流、CORS、CSRF、Panic 恢复。
|
|
||||||
- 基于 `jwt` 的认证中间件,支持多租户隔离。
|
|
||||||
- 输入输出严格校验,避免 SQL 注入与 XSS。
|
|
||||||
|
|
||||||
## 6. 扩展性
|
|
||||||
|
|
||||||
- 各模块以接口形式定义,新增认证协议只需实现 `auth.Provider` 接口并在配置中注册。
|
|
||||||
- 数据库与缓存层均预留版本迁移脚本,支持通过 `migrate` 工具升级。
|
|
||||||
- 通过 `plugins/` 目录支持业务插件,API 层暴露 Hook 以注入自定义逻辑。
|
|
||||||
- 与现有 `xcontrol/rag-server` 共享部分通用库(如 `config`、`logging`),保持代码风格一致。
|
|
||||||
|
|
||||||
## 8. 代码目录规划
|
|
||||||
|
|
||||||
后端代码位于根目录下:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmd/accountsvc/main.go # 服务入口
|
|
||||||
api/ # REST 接口
|
|
||||||
config/ # 配置解析
|
|
||||||
internal/
|
|
||||||
auth/ # LDAP/OIDC/SAML 适配器
|
|
||||||
store/ # PostgreSQL 持久化
|
|
||||||
cache/ # Redis 会话缓存
|
|
||||||
sql/schema.sql # 数据库表结构
|
|
||||||
```
|
|
||||||
|
|
||||||
前端目录扩展:
|
|
||||||
|
|
||||||
- `ui/panel/app/`:控制台新增账号模块页面。
|
|
||||||
- `dashboard/app/login/` 与 `dashboard/app/register/`:提供登录/注册页面,登录后根据身份跳转至用户或管理员界面。
|
|
||||||
|
|
||||||
## 7. 部署建议
|
|
||||||
|
|
||||||
- 提供 Dockerfile 与 Helm Chart,方便容器化部署。
|
|
||||||
- 通过 `Makefile` 集成构建、测试、Lint 等命令。
|
|
||||||
- 在 CI/CD 中加入静态扫描与单元测试,确保安全与稳定。
|
|
||||||
|
|
||||||
---
|
|
||||||
本设计文档为初步方案,后续可根据实际需求迭代更新。
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
swaks --server smtp.qq.com --port 465 --tls-on-connect \
|
|
||||||
--auth LOGIN \
|
|
||||||
--auth-user "manbuzhe2009@qq.com" \
|
|
||||||
--auth-password "xxxxxxxxxxxxxxx" \
|
|
||||||
--from "manbuzhe2009@qq.com" \
|
|
||||||
--to "manbuzhe2008@gmail.com" \
|
|
||||||
--header "From: XControl Account <manbuzhe2009@qq.com>" \
|
|
||||||
--header "Reply-To: no-reply@svc.plus" \
|
|
||||||
--data "Subject: XControl SMTP Test via QQ 465
|
|
||||||
|
|
||||||
Hello, this is a test email via smtp.qq.com SSL port 465.
|
|
||||||
✅ From header added correctly!"
|
|
||||||
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
# XStream Desktop 同步集成方案(跨项目执行手册)
|
|
||||||
|
|
||||||
本手册将 `account` 服务与 XStream Desktop App 的改造步骤拆分为两条执行线,并给出跨项目协作时所需的接口契约、目录定位和数据格式。目标是在托管域名 `accounts.svc.plus` 以及自建部署中,以最小增量实现安全的 xray-core 配置同步,且在 URL 层不泄露任何敏感字段。
|
|
||||||
|
|
||||||
## 1. 账户服务改造(xcontrol/account)
|
|
||||||
|
|
||||||
### 1.1 HTTP 接口扩展
|
|
||||||
|
|
||||||
仅新增 `POST /api/config/sync`,位于 `api` 路由注册:
|
|
||||||
|
|
||||||
- **Handler 位置**:`api/config_sync.go`(新建文件),由 `api.RegisterRoutes` 中挂载到 `auth` 保护下的子路由组。
|
|
||||||
- **认证复用**:沿用 `xc_session` Cookie。若桌面端后续需要无 Cookie 调用,可在 `api/auth` 中增加“设备 Token”生成接口,但不影响本次实现。
|
|
||||||
- **请求结构**:
|
|
||||||
```text
|
|
||||||
POST /api/config/sync
|
|
||||||
Content-Type: application/octet-stream
|
|
||||||
Body: <BinaryPayload>
|
|
||||||
```
|
|
||||||
`BinaryPayload` 为私有格式,包含下列字段(序列化后整体加密):`version(1B)`、`deviceFingerprint(32B)`、`clientVersion(string)`、`nonce(24B)`、`timestamp(int64)`、`lastConfigVersion(int32)`。
|
|
||||||
- **响应结构**:与请求相同为二进制包,字段包括:`version`、`status(OK|NO_PRIVILEGE|ERROR)`、`configVersion`、`xrayConfigJSON`(gzip 后再加密)、`subscriptionMetadata`(可选)。其中 `xrayConfigJSON` 直接复用 `xrayconfig.Generator` 输出,仅需确保 `outbounds` 中包含桌面端所需的最小字段集,例如:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"outbounds": [
|
|
||||||
{
|
|
||||||
"protocol": "vless",
|
|
||||||
"settings": {
|
|
||||||
"vnext": [
|
|
||||||
{
|
|
||||||
"address": "xlts-aws-tky.svc.plus",
|
|
||||||
"port": 1443,
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"id": "<user uuid>",
|
|
||||||
"encryption": "none",
|
|
||||||
"flow": "xtls-rprx-vision"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"streamSettings": {
|
|
||||||
"network": "tcp",
|
|
||||||
"security": "tls",
|
|
||||||
"tlsSettings": {
|
|
||||||
"serverName": "xlts-aws-tky.svc.plus",
|
|
||||||
"allowInsecure": false,
|
|
||||||
"fingerprint": "chrome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **重放保护**:服务端在 handler 中校验 `timestamp` ±5 分钟及 `nonce` 是否重复。重放窗口可复用现有 Redis/内存缓存(`internal/cache`)。
|
|
||||||
|
|
||||||
### 1.2 加密模块复用
|
|
||||||
|
|
||||||
- **Key 派发**:在 `internal/store/user.go` 中新增 `SyncSecret` 字段(可选),默认读取已有 `users.sync_secret` 列;若列不存在,可在迁移脚本中与 UUID 一致生成,确保最少改动。
|
|
||||||
- **算法实现**:在 `internal/crypto/syncpayload`(新目录)封装 `Encrypt(payload []byte, secret []byte)` 与 `Decrypt`,使用 `XChaCha20-Poly1305`。该算法 Go 侧可复用 `golang.org/x/crypto/chacha20poly1305`。
|
|
||||||
- **密钥管理**:管理员通过 `GET/POST /api/auth/admin/settings`(已存在)调整“桌面同步”开关;密钥不在接口返回,仅在数据库存储,客户端登录成功后通过 `/api/config/sync` 解包获得配置。
|
|
||||||
|
|
||||||
### 1.3 配置生成复用
|
|
||||||
|
|
||||||
- **数据来源**:继续使用 `internal/xrayconfig`。根据 `user.UUID` 作为 tenant_id,从 `Generator.Generate()` 获得完整 JSON。
|
|
||||||
- **差异化控制**:在 `xrayconfig` 中新增 `HasDesktopPrivilege(uuid string) bool`(读取管理员设置或用户标记),若返回 false,则 handler 返回 `status=NO_PRIVILEGE`,客户端保持现状。
|
|
||||||
- **审计 & 日志**:复用现有的 `logger.WithContext(ctx)`,记录 `uuid`、`deviceFingerprint`、`configVersion`。
|
|
||||||
|
|
||||||
### 1.4 自建部署兼容
|
|
||||||
|
|
||||||
- 配置文件 `config/account.yaml` 中新增:
|
|
||||||
```yaml
|
|
||||||
desktopSync:
|
|
||||||
enabled: true
|
|
||||||
encryptionKeyTTL: 365d
|
|
||||||
rateLimitPerDevicePerDay: 200
|
|
||||||
```
|
|
||||||
- `cmd/accountsvc/main.go` 读取上述配置,若关闭则在路由层直接返回 `404`。
|
|
||||||
- 所有其他同步流程(生成文件、写入磁盘、触发重启命令)保持不变。
|
|
||||||
|
|
||||||
## 2. XStream Desktop 客户端改造(xstream-desktop 仓库)
|
|
||||||
|
|
||||||
### 2.1 模块划分
|
|
||||||
|
|
||||||
| 模块 | 目录建议 | 说明 |
|
|
||||||
| ---- | -------- | ---- |
|
|
||||||
| Session 管理 | `app/core/session.ts` | 调用 `/api/auth/login`,保存 Cookie 或换取长期 Token。|
|
|
||||||
| Sync 客户端 | `app/sync/syncClient.ts` | 负责序列化请求包、调用 `POST /api/config/sync`、解密响应。|
|
|
||||||
| Xray 写盘 | `app/xray/configWriter.ts` | 将 `xrayConfigJSON` 写入本地文件,并在成功后调用已有的守护进程重启逻辑。|
|
|
||||||
| 状态管理 | `app/state/syncSlice.ts` | Redux/Pinia 等状态库中记录最近同步时间、配置版本。|
|
|
||||||
|
|
||||||
### 2.2 请求与加密
|
|
||||||
|
|
||||||
- 使用 `tweetnacl` 或 `libsodium` 的 XChaCha20-Poly1305 封装,与服务端保持一致。
|
|
||||||
- 请求前从持久层读取 `deviceFingerprint`(首次安装随机生成 32B 并写入 `~/.xstream/device_id`)。
|
|
||||||
- `nonce` 每次随机 24B,`timestamp` 为 Unix 毫秒。`lastConfigVersion` 取自本地缓存,便于服务端快速判断是否需要下发完整配置。
|
|
||||||
- 响应解析后根据 `status` 做差异化处理:
|
|
||||||
- `OK`:写盘并在 UI 中显示“已同步”。
|
|
||||||
- `NO_PRIVILEGE`:保持旧配置,提示用户检查订阅资格。
|
|
||||||
- `ERROR`:记录日志并指数退避重试。
|
|
||||||
|
|
||||||
### 2.3 同步流程
|
|
||||||
|
|
||||||
1. **启动阶段**:应用启动时调用一次同步,并加载本地缓存的 `configVersion`。若解密失败则提示用户重新登录。
|
|
||||||
2. **定时任务**:使用 Electron/Node `setInterval`(建议 10 分钟)触发。后台静默发送请求,失败时最多连续重试 3 次。
|
|
||||||
3. **手动触发**:在设置页新增“立即同步”按钮,调用同一模块。
|
|
||||||
4. **降级策略**:若连续 24 小时失败,回退到“使用本地缓存配置”模式,但仍保留重试。
|
|
||||||
|
|
||||||
### 2.4 自建服务兼容
|
|
||||||
|
|
||||||
- 将域名与端口作为配置项放入 `app/config/default.json`,允许用户在 UI 中指定自建 `account.xxx.xxx`。
|
|
||||||
- 密钥管理完全依赖服务端:客户端只保存 `deviceFingerprint` 和 `configVersion`,其他敏感信息通过加密包下发。
|
|
||||||
- 若自建管理员关闭 `desktopSync.enabled`,客户端收到 404 时提示“服务未启用”。
|
|
||||||
|
|
||||||
## 3. 数据包格式(参考实现)
|
|
||||||
|
|
||||||
以下为双方共有的序列化格式,便于跨项目协作:
|
|
||||||
|
|
||||||
```text
|
|
||||||
struct SyncRequest {
|
|
||||||
uint8 version; // 固定 1
|
|
||||||
bytes deviceFingerprint[32];
|
|
||||||
string clientVersion; // UTF-8,前置 1 字节长度,最长 32
|
|
||||||
bytes nonce[24];
|
|
||||||
int64 timestamp; // Unix milliseconds
|
|
||||||
int32 lastConfigVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SyncResponse {
|
|
||||||
uint8 version; // 固定 1
|
|
||||||
uint8 status; // 0=OK,1=NO_PRIVILEGE,2=ERROR
|
|
||||||
int32 configVersion;
|
|
||||||
bytes xrayConfigGzip; // gzip(JSON)
|
|
||||||
string subscriptionMetadata; // UTF-8,可为空
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Encrypt(SyncRequest)` 与 `Decrypt(SyncResponse)` 均使用 `XChaCha20-Poly1305(secret=User.SyncSecret, nonce)`;`nonce` 随请求发送,响应重新生成新的随机 `nonce` 并放入包头,避免重放。
|
|
||||||
|
|
||||||
## 4. 联调步骤
|
|
||||||
|
|
||||||
1. **准备环境**:在 `xcontrol/account` 启动 `make dev`,并在数据库中为测试账号写入 `sync_secret`。同时在本地运行 XStream Desktop Dev 构建。
|
|
||||||
2. **接口自测**:使用 `curl`/`httpie` 构造加密请求验证 `/api/config/sync`,确认返回状态正确。
|
|
||||||
3. **桌面端接入**:在 Desktop 项目中实现 `syncClient.ts`,确保能与本地 account 服务交互。
|
|
||||||
4. **端到端演示**:登录桌面端 → 手动同步 → 核验本地生成的 `config.json` 与 account 服务生成的文件一致。
|
|
||||||
5. **回归**:验证旧有管理员界面、注册/登录流程不受影响。
|
|
||||||
|
|
||||||
## 5. 安全与运维要点
|
|
||||||
|
|
||||||
- **最小数据面**:所有敏感字段都封装在加密包内,URL 与 Header 仅携带基础信息(Cookie)。
|
|
||||||
- **限流**:继续复用 `api/middleware/ratelimit`(若已有)或在 handler 中增加 per-device 限流。
|
|
||||||
- **审计**:在服务端日志中记录 `uuid`、`deviceFingerprint` hash、`status`,便于定位问题而不过度存储。
|
|
||||||
- **滚动升级**:版本字段可确保前后端同时升级;旧客户端仍可解析 version=1。
|
|
||||||
|
|
||||||
通过以上拆解,`account` 服务与 XStream Desktop App 均可按部就班地完成改造,且复用现有模块,避免过度设计。跨项目团队只需围绕接口契约与数据包格式协同,即可实现安全、可复用的桌面同步能力。
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# Agent Design
|
|
||||||
|
|
||||||
Details how each node pulls config and reports usage.
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"inbounds": [...],
|
|
||||||
"outbounds": [...]
|
|
||||||
}
|
|
||||||
@ -1,285 +0,0 @@
|
|||||||
# API Endpoints
|
|
||||||
|
|
||||||
This document describes the HTTP endpoints provided by the XControl platform. Each entry lists the request method and path, required parameters, and a sample curl command for verification.
|
|
||||||
|
|
||||||
## Authentication Gateway (Next.js)
|
|
||||||
|
|
||||||
The XControl web frontend exposes authentication APIs under `dashboard/app/api/auth`. These endpoints act as a secure gateway that proxies requests to the shared Account Service (`/api/auth/register`, `/api/auth/register/send`, `/api/auth/register/verify`, `/api/auth/login`, `/api/auth/mfa/setup`, `/api/auth/mfa/verify`). Responses always include `{ "success": boolean, "error": string | null, "needMfa": boolean }` so that multiple frontends can share the same Account Service behaviour.
|
|
||||||
|
|
||||||
Gateway-managed session cookies (`xc_session`) and MFA challenge cookies (`xc_mfa_challenge`) are issued with `HttpOnly`, `Secure`, and `SameSite=Strict` attributes. Cookies are HTTPS-only and never expose raw secrets to JavaScript.
|
|
||||||
|
|
||||||
### POST /api/auth/register
|
|
||||||
- **Description:** Register a new account through the gateway. The Account Service creates the pending user and sends a verification code via email.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `name` – Optional display name.
|
|
||||||
- `email` – Required email address; normalized to lowercase.
|
|
||||||
- `password` / `confirmPassword` – Required password fields. Values must match before proxying.
|
|
||||||
- **Response:** `{ "success": true, "error": null, "needMfa": false }` on success. On failure `error` contains the Account Service error code.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/register \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"name":"demo","email":"demo@example.com","password":"Secret123","confirmPassword":"Secret123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/register/send
|
|
||||||
- **Description:** Trigger a verification email for an existing pending registration. This endpoint may be used to send the initial code when the frontend wants to separate registration from verification, or to resend a code if the user did not receive the previous email.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `email` – The pending account email address.
|
|
||||||
- **Response:** `{ "success": true, "error": null, "needMfa": false }` on success. On failure `error` contains the Account Service error code.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/register/send \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"demo@example.com"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/verify-email
|
|
||||||
- **Description:** Confirm the 6-digit email verification code issued during registration. Activates the account when the code matches and has not expired.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `email` – Registered email address.
|
|
||||||
- `code` – Verification code from the email message.
|
|
||||||
- **Response:** `{ "success": true, "error": null, "needMfa": false }` once the account transitions to `active`.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/verify-email \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"demo@example.com","code":"123456"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/login
|
|
||||||
- **Description:** Authenticate email + password credentials. When MFA is enabled the response sets `needMfa: true` and stores the temporary challenge token in an HttpOnly cookie.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `email` – Account email.
|
|
||||||
- `password` – Password. Never logged or stored in plaintext.
|
|
||||||
- `totp` *(optional)* – 6-digit TOTP if already known (legacy compatibility when `mfa_enabled=false`).
|
|
||||||
- `remember` *(optional)* – Extends the session cookie lifetime to 30 days.
|
|
||||||
- **Response:**
|
|
||||||
- `{ "success": true, "needMfa": false }` and a `xc_session` cookie when MFA succeeds or is disabled.
|
|
||||||
- `{ "success": false, "needMfa": true }` and a `xc_mfa_challenge` cookie when additional MFA verification is required.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-c cookies.txt \
|
|
||||||
-d '{"email":"demo@example.com","password":"Secret123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/mfa/setup
|
|
||||||
- **Description:** Generate a TOTP secret and provisioning URI for the authenticated challenge token. The challenge token is read from the `xc_mfa_challenge` cookie or the JSON payload.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `token` *(optional)* – MFA challenge token override. Defaults to the cookie value.
|
|
||||||
- `issuer` *(optional)* – Overrides the issuer label in authenticator apps.
|
|
||||||
- `account` *(optional)* – Overrides the account label.
|
|
||||||
- **Response:** `{ "success": true, "needMfa": true, "data": { ... } }` with the Account Service payload (e.g., `otpauth` URI, recovery codes). Errors keep `needMfa: true` and include `error` codes from the backend.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/mfa/setup \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-b cookies.txt \
|
|
||||||
-d '{}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/mfa/verify
|
|
||||||
- **Description:** Validate the 6-digit TOTP code. On success the gateway issues the final session cookie and removes the MFA challenge cookie.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `token` *(optional)* – MFA challenge token override.
|
|
||||||
- `code` – 6-digit TOTP value.
|
|
||||||
- **Response:** `{ "success": true, "needMfa": false }` with `xc_session` cookie on success. Errors reuse the challenge token and return `{ "success": false, "needMfa": true }`.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/auth/mfa/verify \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-b cookies.txt \
|
|
||||||
-d '{"code":"123456"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Session Lookup
|
|
||||||
- **GET /api/auth/session** – Returns `{ "user": { ... } }` when the `xc_session` cookie is present. The payload now mirrors the Account Service metadata and exposes the `role`, `groups`, and `permissions` arrays that are derived from the server-side `level` field. Clears the cookie automatically if the Account Service rejects the session.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -b cookies.txt http://localhost:3000/api/auth/session | jq
|
|
||||||
```
|
|
||||||
Example response after a successful login:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"uuid": "72c70df9-b7b6-4e81-84ef-5f0e5b1fc7c6",
|
|
||||||
"name": "demo",
|
|
||||||
"email": "demo@example.com",
|
|
||||||
"role": "user",
|
|
||||||
"groups": ["User"],
|
|
||||||
"permissions": ["session:read"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **DELETE /api/auth/session** – Revokes the active session both at the gateway and the Account Service.
|
|
||||||
|
|
||||||
> **TLS note:** Deploy the frontend behind HTTPS so that `Secure` cookies are accepted by browsers. When testing with curl, add `-k` only if using a self-signed development certificate.
|
|
||||||
|
|
||||||
> Unless otherwise noted, the examples below target the RAG server listening on
|
|
||||||
> `127.0.0.1:8090`. The default base URL for local testing is
|
|
||||||
> `http://localhost:8090`.
|
|
||||||
|
|
||||||
## GET /api/users
|
|
||||||
- **Description:** Return all users.
|
|
||||||
- **Parameters:** None.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:8090/api/users
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /api/nodes
|
|
||||||
- **Description:** Return all nodes.
|
|
||||||
- **Parameters:** None.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:8090/api/nodes
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /api/sync
|
|
||||||
- **Description:** Clone or update a knowledge repository.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `repo_url` – Git repository URL.
|
|
||||||
- `local_path` – Destination directory on the server.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8090/api/sync \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"repo_url": "https://github.com/example/repo.git", "local_path": "/tmp/repo"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /api/rag/sync
|
|
||||||
- **Description:** Trigger RAG background synchronization. The endpoint streams
|
|
||||||
plain-text progress logs during the sync.
|
|
||||||
- **Parameters:** None.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -N -X POST http://localhost:8090/api/rag/sync
|
|
||||||
```
|
|
||||||
- **Notes:** A future evolution could expose this operation via a gRPC
|
|
||||||
streaming RPC. That approach would allow high-speed synchronization, rate
|
|
||||||
limiting, and resumable transfers over long-lived connections while
|
|
||||||
supporting dynamic, lossless queues for weak networks.
|
|
||||||
|
|
||||||
## POST /api/rag/upsert
|
|
||||||
- **Description:** Upsert pre-embedded document chunks into the RAG database.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `docs` – Array of documents each containing `repo`, `path`, `chunk_id`, `content`, `embedding`, `metadata`, and `content_sha`.
|
|
||||||
- **Test:**
|
|
||||||
|
|
||||||
curl -X POST http://localhost:8090/api/rag/upsert \
|
|
||||||
-H "Content-Type: application/json" --data-binary @/Users/shenlan/workspaces/XControl/docs/upsert_1024.json
|
|
||||||
```bash
|
|
||||||
Expected response on success: `{"rows":1}`. If the vector database is unavailable, the endpoint returns `{"rows":0,"error":"..."}`.
|
|
||||||
|
|
||||||
## POST /api/rag/query
|
|
||||||
- **Description:** Query the RAG service.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `question` – Query text.
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8090/api/rag/query \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"question": "What is XControl?"}'
|
|
||||||
```
|
|
||||||
When copying the multi-line example above, ensure your shell treats the trailing
|
|
||||||
`\` characters as line continuations. Copying literal `\n` sequences will cause
|
|
||||||
`curl: (3) URL rejected: Bad hostname` errors. You can also run the command on a
|
|
||||||
single line without the backslashes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8090/api/rag/query -H "Content-Type: application/json" -d '{"question": "What is XControl?"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /api/askai
|
|
||||||
- **Description:** Ask the AI service for an answer. The endpoint uses [LangChainGo](https://github.com/tmc/langchaingo) to communicate with the configured model provider (e.g., OpenAI-compatible services or a local Ollama instance). Ensure the server configuration includes the proper token or local server URL.
|
|
||||||
- **Body Parameters (JSON):**
|
|
||||||
- `question` – Question text.
|
|
||||||
**Configuration:** In `rag-server/config/server.yaml` the `models` section selects the LLM and embedding providers.
|
|
||||||
For local debugging with HuggingFace and Ollama:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
models:
|
|
||||||
embedder:
|
|
||||||
models: "bge-m3"
|
|
||||||
endpoint: "http://127.0.0.1:9000/v1/embeddings"
|
|
||||||
generator:
|
|
||||||
models:
|
|
||||||
- 'llama2:13b'
|
|
||||||
endpoint: "http://127.0.0.1:11434"
|
|
||||||
```
|
|
||||||
|
|
||||||
For online services using Chutes:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
#models:
|
|
||||||
# embedder:
|
|
||||||
# models: "bge-m3"
|
|
||||||
# endpoint: "https://chutes-baai-bge-m3.chutes.ai/embed"
|
|
||||||
# token: "cpk_xxxx"
|
|
||||||
# generator:
|
|
||||||
# models:
|
|
||||||
# - 'moonshotai/Kimi-K2-Instruct'
|
|
||||||
# endpoint: "https://llm.chutes.ai/v1"
|
|
||||||
# token: "cpk_xxxx"
|
|
||||||
```
|
|
||||||
|
|
||||||
The `api.askai` section controls request behaviour:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
api:
|
|
||||||
askai:
|
|
||||||
timeout: 60 # seconds
|
|
||||||
retries: 3 # retry attempts
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Test:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8090/api/askai \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"question": "Hello"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET Localhost embeddings API
|
|
||||||
|
|
||||||
1. 运行(首次会自动下载模型)
|
|
||||||
python offline_embed_server.py
|
|
||||||
2. 测试接口
|
|
||||||
|
|
||||||
1) 健康检查(端口就绪即返回 ok) curl -v http://127.0.0.1:9000/healthz
|
|
||||||
2) 就绪检查(模型加载完成后返回 ready) curl -v http://127.0.0.1:9000/readyz
|
|
||||||
3) 调用 embeddings
|
|
||||||
|
|
||||||
curl http://127.0.0.1:9000/v1/embeddings \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"model":"BAAI/bge-m3","input":["你好","PGVector 怎么建 HNSW?"]}'
|
|
||||||
|
|
||||||
如果你要把 DEVICE 固定为 mps 并行内核,保留默认即可;如需落回 CPU:DEVICE=cpu python docs/offline_embed_server.py。
|
|
||||||
|
|
||||||
## GET Localhost Ollama API
|
|
||||||
|
|
||||||
用流式接收(推荐):
|
|
||||||
|
|
||||||
curl http://127.0.0.1:11434/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "gpt-oss:20b",
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
|
||||||
{"role": "user", "content": "Tell me three tips for optimizing HNSW in PostgreSQL."}
|
|
||||||
],
|
|
||||||
"max_tokens": 512,
|
|
||||||
"stream": true
|
|
||||||
}'
|
|
||||||
这样会实时输出分块数据
|
|
||||||
|
|
||||||
curl http://127.0.0.1:11434/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "llama3:latest",
|
|
||||||
"messages": [{"role":"user","content":"你好,简要介绍一下自己"}],
|
|
||||||
"max_tokens": 200,
|
|
||||||
"temperature": 0.7
|
|
||||||
}'
|
|
||||||
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
# REST API Design
|
|
||||||
|
|
||||||
## GET /api/users
|
|
||||||
Returns list of users.
|
|
||||||
|
|
||||||
|
|
||||||
🔗 REST API 接口设计(Gin)
|
|
||||||
方法 路径 功能描述
|
|
||||||
GET /api/users 获取用户列表
|
|
||||||
POST /api/users 创建新用户
|
|
||||||
GET /api/users/:id/stats 获取单用户流量
|
|
||||||
GET /api/users/:id/sub 获取订阅链接(vless://)
|
|
||||||
GET /api/nodes 获取所有节点
|
|
||||||
POST /api/nodes/:id/ping 测试指定节点状态
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# Subscription API
|
|
||||||
|
|
||||||
Returns vless:// links and QR codes.
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
# AI 问答知识库系统设计
|
|
||||||
|
|
||||||
本文档描述如何在 XControl 项目中构建一个基于 RAG(Retrieval Augmented Generation)的 AI 问答知识库系统。参考 `ASK AI` 中的模块化建议,系统采用 Go 实现,注重可扩展和易维护。
|
|
||||||
|
|
||||||
## 1. 选用服务
|
|
||||||
|
|
||||||
- **文档同步**:使用 `go-git` 拉取或更新 GitHub 仓库,可定时执行或通过 Webhook 触发。
|
|
||||||
- **文档转文本**:利用 `Pandoc` CLI 或 `goldmark` 将 Markdown 等格式转为纯文本。
|
|
||||||
- **分块策略**:按标题或段落切割,生成 `Chunk` 结构体并记录位置信息。
|
|
||||||
- **向量化**:可调用 OpenAI `text-embedding-3-small`,或本地部署 `bge-large-zh` 通过 HTTP 服务提供 Embedding。
|
|
||||||
- **向量存储**:PostgreSQL + `pgvector` 扩展,使用 `pgx` 进行读写。
|
|
||||||
- **检索与问答**:相似度查询后构建 Prompt,调用 GPT/Claude 等模型生成回答。
|
|
||||||
- **Web UI (可选)**:Gin 提供 REST API,前端可使用 React/Next.js。
|
|
||||||
|
|
||||||
## 2. 接口与配置文件
|
|
||||||
|
|
||||||
接口示例:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// rag-server/api.go
|
|
||||||
func RegisterRoutes(r *gin.Engine, db *pgx.Conn) {
|
|
||||||
r.POST("/sync", SyncHandler)
|
|
||||||
r.POST("/ask", AskHandler)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
配置文件示例 `config/repos.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
repos:
|
|
||||||
- url: https://github.com/example/docs.git
|
|
||||||
branch: main
|
|
||||||
path: data/docs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 数据结构
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ingest/chunk.go
|
|
||||||
// Chunk 表示切分后的文档片段
|
|
||||||
struct Chunk {
|
|
||||||
DocID string
|
|
||||||
Content string
|
|
||||||
Meta map[string]any
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
数据库表 `chunks`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE EXTENSION IF NOT EXISTS vector;
|
|
||||||
CREATE TABLE chunks (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
doc_id TEXT,
|
|
||||||
content TEXT,
|
|
||||||
vector vector(1024),
|
|
||||||
metadata JSONB
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Ingest 流程
|
|
||||||
|
|
||||||
1. 调用 `SyncRepo()` 同步文档。
|
|
||||||
2. 通过 `Pandoc` 或 `goldmark` 转为纯文本。
|
|
||||||
3. 按标题/段落切割,生成 `Chunk` 对象。
|
|
||||||
4. 调用 `Embed()` 得到向量并写入 `chunks` 表。
|
|
||||||
|
|
||||||
示例代码:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// sync/sync.go
|
|
||||||
func SyncRepo(ctx context.Context, url, workdir string) (string, error) { /* ... */ }
|
|
||||||
|
|
||||||
// ingest/embed.go
|
|
||||||
func Embed(text string) ([]float32, error) { /* 调用 Embedding 模型 */ }
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 项目代码规划
|
|
||||||
|
|
||||||
```
|
|
||||||
rag-server/internal/rag/
|
|
||||||
├── sync/ # Git 克隆/更新
|
|
||||||
├── ingest/ # 文档转换与分块
|
|
||||||
├── embed/ # 向量化
|
|
||||||
├── store/ # 向量存储封装
|
|
||||||
├── llm/ # Prompt 构造与问答流程
|
|
||||||
├── api/ # REST API
|
|
||||||
└── config/ # 同步仓库配置
|
|
||||||
```
|
|
||||||
|
|
||||||
以上规划提供了最小可用的 AI 问答知识库实现思路,可在此基础上逐步完善。
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# Data Flow
|
|
||||||
|
|
||||||
Explain how traffic, configs, and subscriptions flow through the system.
|
|
||||||
|
|
||||||
数据模型设计(PostgreSQL 简化版)
|
|
||||||
users 表
|
|
||||||
字段 类型 说明
|
|
||||||
id UUID 用户 UUID(VLESS使用)
|
|
||||||
email TEXT 用户识别标识
|
|
||||||
level INT 对应 policy.level
|
|
||||||
active BOOLEAN 是否启用
|
|
||||||
upload BIGINT 累计上行流量
|
|
||||||
download BIGINT 累计下行流量
|
|
||||||
expire_at TIMESTAMP 到期时间(可空)
|
|
||||||
|
|
||||||
|
|
||||||
nodes 表(支持多节点)
|
|
||||||
字段 类型 说明
|
|
||||||
id UUID 节点唯一 ID
|
|
||||||
name TEXT 展示用名称
|
|
||||||
location TEXT 地区
|
|
||||||
protocols TEXT[] 支持的传输方式(ws, grpc)
|
|
||||||
address TEXT 连接地址
|
|
||||||
available BOOLEAN 是否可用
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
# Design Framework
|
|
||||||
|
|
||||||
This document outlines the high level design of **XControl** and how the project uses a collection of open source components, provided as optional extension modules, to build a multi-tenant, multi-service platform.
|
|
||||||
|
|
||||||
## Open Source Components
|
|
||||||
|
|
||||||
- **PulumiGo** is used to provision cloud resources across multiple providers using the Pulumi SDK with Go.
|
|
||||||
- **KubeGuard** provides Kubernetes cluster application backups and node-level recovery.
|
|
||||||
- **CraftWeave** orchestrates lightweight tasks and configuration changes for each service module.
|
|
||||||
- **CodePRobot** automates GitHub Issue to Pull Request workflows and assists with code patching.
|
|
||||||
- **OpsAgent** offers intelligent monitoring, anomaly detection and root cause analysis.
|
|
||||||
- **XStream** accelerates developer connectivity across regions.
|
|
||||||
These extension modules can be enabled individually, letting deployments choose only the features they need.
|
|
||||||
|
|
||||||
### Component Integration Status
|
|
||||||
|
|
||||||
| Component | Status |
|
|
||||||
|-----------|--------|
|
|
||||||
| PulumiGo | Planned |
|
|
||||||
| KubeGuard | Planned |
|
|
||||||
| CraftWeave | Planned |
|
|
||||||
| CodePRobot | Planned |
|
|
||||||
| OpsAgent | Planned |
|
|
||||||
| XStream | Planned |
|
|
||||||
|
|
||||||
## Core Design Principles
|
|
||||||
|
|
||||||
1. **Multi-Tenant** – users are isolated in data and configuration while sharing the same control plane.
|
|
||||||
2. **Multi-Service** – each component runs as an independent service that can be enabled or disabled per tenant.
|
|
||||||
3. **Multi-Node Control** – agents deployed on nodes pull configuration, report usage and manage local services.
|
|
||||||
4. **Subscription Configuration** – users export service configs (such as `vless://` links) via a unified API.
|
|
||||||
5. **Modular Visual Panel** – the web UI is built from modules so features can be added as needed.
|
|
||||||
|
|
||||||
These principles allow XControl to scale from a single deployment to a complex environment spanning multiple clouds and Kubernetes clusters.
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
# Module Definitions
|
|
||||||
|
|
||||||
Define core components: controller, agents, web panel.
|
|
||||||
|
|
||||||
🧩 模块说明
|
|
||||||
模块 功能
|
|
||||||
- 用户面板 提供订阅配置导出、流量图表、可用节点展示
|
|
||||||
- 控制器后端 Go 编写,提供 REST API,管理用户/节点/策略,控制节点配置
|
|
||||||
- 数据库 PostgreSQL 存储用户、节点、流量等业务数据
|
|
||||||
- 多节点 Agent 拉取配置并重启 Xray、采集流量(Xray stats 或 DeepFlow)
|
|
||||||
- Web 面板 Vue3 + TailwindCSS,内嵌至 Go 二进制
|
|
||||||
## 开源扩展模块
|
|
||||||
|
|
||||||
以下组件可按需启用,集成状态如下:
|
|
||||||
|
|
||||||
| 模块 | 功能 | 集成状态 |
|
|
||||||
| ---- | ---- | ---- |
|
|
||||||
| PulumiGo | 多云基础设施自动化 | 计划集成 |
|
|
||||||
| KubeGuard | K8s 集群备份与恢复 | 计划集成 |
|
|
||||||
| CraftWeave | 任务执行与配置编排 | 计划集成 |
|
|
||||||
| CodePRobot | Issue 到 PR 自动化 | 计划集成 |
|
|
||||||
| OpsAgent | 智能监控与异常分析 | 计划集成 |
|
|
||||||
| XStream | 开发者跨境代理加速 | 计划集成 |
|
|
||||||
|
|
||||||
模块拆分建议(Go)
|
|
||||||
|
|
||||||
internal/
|
|
||||||
├── api/ # Gin API 实现
|
|
||||||
├── model/ # GORM 数据模型
|
|
||||||
├── service/ # 用户管理、节点控制逻辑
|
|
||||||
├── agent/ # 多节点配置管理逻辑
|
|
||||||
├── subscription/ # vless:// 链接生成器
|
|
||||||
├── stats/ # 统一流量处理器(xray or deepflow)
|
|
||||||
|
|
||||||
📌 模块说明
|
|
||||||
✅ 用户面
|
|
||||||
通过浏览器访问 WebUI;
|
|
||||||
|
|
||||||
获取订阅信息、流量使用情况;
|
|
||||||
|
|
||||||
支持扫码/复制/查看节点;
|
|
||||||
|
|
||||||
✅ 控制面
|
|
||||||
Go 实现的后端服务(vless-admin);
|
|
||||||
|
|
||||||
管理用户、策略、节点;
|
|
||||||
|
|
||||||
提供 REST API 和订阅地址;
|
|
||||||
|
|
||||||
内嵌 Vue3 面板、连接 PostgreSQL、采集多节点流量;
|
|
||||||
|
|
||||||
✅ 多节点
|
|
||||||
每个节点部署 Xray + Agent;
|
|
||||||
|
|
||||||
Agent 负责:
|
|
||||||
|
|
||||||
拉取配置文件;
|
|
||||||
|
|
||||||
上报 UUID 使用流量;
|
|
||||||
|
|
||||||
定期向控制面同步状态;
|
|
||||||
|
|
||||||
Xray 开启 stats + api;
|
|
||||||
|
|
||||||
每个节点可支持不同出口、地域、性能策略
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
# System Architecture
|
|
||||||
|
|
||||||
Describe the overall architecture with user, controller, agents.
|
|
||||||
|
|
||||||
🌐 User Panel ──► XControl API ◄───────┐
|
|
||||||
▲ │ │
|
|
||||||
│ REST API ▼ │
|
|
||||||
[浏览器] ┌─────────────┐ gRPC/HTTP
|
|
||||||
│ PostgreSQL │ ┌────────────────────┐
|
|
||||||
└─────────────┘ │ Xray 节点 Agent #1 │
|
|
||||||
│ Xray + stats/api │
|
|
||||||
└────────────────────┘
|
|
||||||
...
|
|
||||||
┌────────────────────┐
|
|
||||||
│ DeepFlow Agent #N │
|
|
||||||
│ 采集节点流量与指标 │
|
|
||||||
└────────────────────┘
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
🧭 多租户 VLESS 管理系统架构图 (用户面 + 控制面 + 多节点)
|
|
||||||
|
|
||||||
🌐 用户面(User Panel)
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ [User Browser] │
|
|
||||||
│ ├─📄 查看配置导出(vless:// + QR 码) │
|
|
||||||
│ ├─📊 当前使用量(上/下行流量、图表) │
|
|
||||||
│ └─🌍 可用节点列表(选择订阅) │
|
|
||||||
└──────────────┬──────────────────────────────────────────────┘
|
|
||||||
│ HTTPS API 请求
|
|
||||||
▼
|
|
||||||
|
|
||||||
🧠 控制面(vless-admin Controller)
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ REST API (Gin) │
|
|
||||||
│ ├─ /api/users → 用户注册/添加/流量查询 │
|
|
||||||
│ ├─ /api/subscription → vless:// 订阅链接生成 │
|
|
||||||
│ ├─ /api/nodes → 多节点信息展示 │
|
|
||||||
│ └─ /api/stats → 后台流量采集、流控策略 │
|
|
||||||
│ │
|
|
||||||
│ PostgreSQL │
|
|
||||||
│ ├─ 用户表 (email + UUID) │
|
|
||||||
│ ├─ 节点表 │
|
|
||||||
│ └─ 流量表(每日/每小时) │
|
|
||||||
│ │
|
|
||||||
│ 可选管理界面(如需) │
|
|
||||||
└──────────────┬──────────────────────────────────────────────┘
|
|
||||||
│ HTTP/gRPC 控制与拉取配置
|
|
||||||
▼
|
|
||||||
|
|
||||||
🛰️ 多节点(Xray-core + Agent)
|
|
||||||
|
|
||||||
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
|
|
||||||
│ Agent: Node #1 │ │ Agent: Node #2 │ │ Agent: Node #N │
|
|
||||||
│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
|
|
||||||
│ │ Xray-core │ │ │ │ Xray-core │ │ │ │ Xray-core │ │
|
|
||||||
│ │ + stats + api │ │ │ │ + stats + api │ │ │ │ + stats + api │ │
|
|
||||||
│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
|
|
||||||
│ ⬆ 上报用户流量 │ │ ⬆ 上报用户流量 │ │ ⬆ 上报用户流量 │
|
|
||||||
│ ⬇ 拉取用户配置 │ │ ⬇ 拉取用户配置 │ │ ⬇ 拉取用户配置 │
|
|
||||||
└────────────────────┘ └────────────────────┘ └────────────────────┘
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user