reset accounts.svc.plus repo

This commit is contained in:
Haitao Pan 2026-01-23 23:16:37 +08:00
parent e0915541a7
commit 29ee491acf
229 changed files with 27 additions and 21434 deletions

View File

@ -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)

View File

@ -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"]

View File

@ -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
View File

@ -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>.

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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&nbsp;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).

View File

@ -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

View File

@ -1,343 +0,0 @@
# Token Auth 双层签发 - 实现总结
xcontrol-accountGo 后端)路由接口
Endpoint Method 使用密钥 说明
/api/auth/exchange POST publicToken 验证 从公共令牌换取 Access Token
/api/auth/refresh POST refreshSecret 签发 刷新 Access Token
/api/auth/verify GET accessSecret 验证 验证 Access Token
# xcontrol-accountGo 后端)配置
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-SeverGo 后端)配置
只保留公钥部分:
auth:
enable: true
token:
publicToken: "xcontrol-public-token-2025"
apiBaseUrl: "https://api.svc.plus"
authUrl: "https://accounts.svc.plus"
# dashboard-freshDeno 前端)配置
✅ 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 ❌ 否 ✅ 是

View File

@ -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
}

View File

@ -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)

View File

@ -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() {

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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."
}
}
}

View File

@ -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)

View File

@ -1,5 +0,0 @@
[defaults]
roles_path = roles
stdout_callback = yaml
host_key_checking = False
callback_plugins = callback_plugins

View File

@ -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}")

View File

@ -1,4 +0,0 @@
---
- name: Placeholder OpenResty deployment
ansible.builtin.debug:
msg: "Applying OpenResty configuration for {{ inventory_hostname }}"

View File

@ -1,4 +0,0 @@
---
- name: Placeholder PostgreSQL deployment
ansible.builtin.debug:
msg: "Applying PostgreSQL configuration for {{ inventory_hostname }}"

View File

@ -1,4 +0,0 @@
---
- name: Placeholder Redis deployment
ansible.builtin.debug:
msg: "Applying Redis configuration for {{ inventory_hostname }}"

View File

@ -1,9 +0,0 @@
---
- name: Provision homepage vhosts
hosts: all
become: true
gather_facts: false
roles:
- vhosts/OpenResty
- vhosts/Redis
- vhosts/Postgresql

View File

@ -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"

View File

@ -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
```

View File

@ -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"]

View File

@ -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

View File

@ -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"]

View File

@ -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"
}

View File

@ -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

View File

@ -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:

View File

@ -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"]

View File

@ -1,3 +0,0 @@
protocol imap {
mail_plugins = $mail_plugins
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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"]

View File

@ -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"]

View File

@ -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;"]

View File

@ -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"]

View File

@ -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.

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -1,6 +0,0 @@
apiVersion: v2
name: xcontrol
version: 0.1.0
description: XControl platform chart
type: application
appVersion: "1.0"

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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: ""

View File

@ -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:

View File

@ -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"]

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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";
}
}

View File

@ -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;
}

View File

@ -1,12 +0,0 @@
server {
listen 80;
server_name _;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 200 "bootstrap";
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -1,6 +0,0 @@
events {}
http {
include /etc/nginx/conf.d/*.conf;
}

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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如页眉、版权声明
- MMRMaximal Marginal Relevance去冗余提升多样性。
- 对候选结果进行轻量 Re-ranking如 bge-reranker
- 融合多模态信息(如图片描述)。
### 检索优化
- 在 Query 中启用 Hybrid Search向量 + BM25权重可配置。
- 支持多向量查询ColBERT 思路)匹配文档不同部分。

View File

@ -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**:完善自动化测试与文档,准备下一阶段迭代。

View File

@ -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).

View File

@ -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 工程体系提供智能层。

View File

@ -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
```

View File

@ -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.

View File

@ -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 对应 STARTTLS465 可用于 SMTPS
username: "apikey" # 登录用户名或 API Key
p: "s" # 登录密码,生产环境建议使用 Secret 管理
from: "XControl <no-reply@example.com>" # 发件人展示名称+地址
replyTo: "" # 可选Reply-To 地址
timeout: 10s # 连接与发送超时
tls:
mode: "starttls" # 可选 starttls 或 implicitSMTPS
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 的账号服务地址。
随着服务演进,请在更新配置结构或新字段时同步维护本文档。

View File

@ -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 clientAPI/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` 或上述迁移脚本,即可重新引入角色元数据。
---
以上步骤覆盖从开发到生产的核心流程,可根据企业环境补充额外的安全、审计或合规要求。

View File

@ -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 行为符合预期。
---
本文档需根据功能演进持续维护,以确保服务的设计意图与实现保持一致。

View File

@ -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 中加入静态扫描与单元测试,确保安全与稳定。
---
本设计文档为初步方案,后续可根据实际需求迭代更新。

View File

@ -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!"

View File

@ -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 均可按部就班地完成改造,且复用现有模块,避免过度设计。跨项目团队只需围绕接口契约与数据包格式协同,即可实现安全、可复用的桌面同步能力。

View File

@ -1,3 +0,0 @@
# Agent Design
Details how each node pulls config and reports usage.

View File

@ -1,4 +0,0 @@
{
"inbounds": [...],
"outbounds": [...]
}

View File

@ -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 并行内核,保留默认即可;如需落回 CPUDEVICE=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
}'

View File

@ -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 测试指定节点状态

View File

@ -1,3 +0,0 @@
# Subscription API
Returns vless:// links and QR codes.

View File

@ -1,91 +0,0 @@
# AI 问答知识库系统设计
本文档描述如何在 XControl 项目中构建一个基于 RAGRetrieval 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 问答知识库实现思路,可在此基础上逐步完善。

View File

@ -1,24 +0,0 @@
# Data Flow
Explain how traffic, configs, and subscriptions flow through the system.
数据模型设计PostgreSQL 简化版)
users 表
字段 类型 说明
id UUID 用户 UUIDVLESS使用
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 是否可用

View File

@ -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.

View File

@ -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
每个节点可支持不同出口、地域、性能策略

View File

@ -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