English | 简体中文
Welcome to SublinkPro development. This guide focuses on:
- How to run the backend and frontend locally
- What the production build flow actually does
- Which files and directories are high value entry points
- Where unlock check extension points live
sublinkPro/
├── api/ # HTTP API / controller
├── models/ # Data models, persistence, migrations
├── services/ # Business services and background subsystems
│ ├── scheduler/ # Scheduled tasks and task scheduler
│ ├── mihomo/ # mihomo integration, speed test, DNS, Host, proxied outbound
│ └── unlock/ # Unlock registry, runtime, checker modules
├── routers/ # Route registration
├── node/ # Subscription and protocol parsing/conversion
├── utils/ # Shared helpers
├── database/ # Database connection and dialect support
├── cache/ # Cache layer
├── dto/ # DTO / form structures
├── webs/ # React + Vite frontend
│ └── src/
│ ├── api/ # Frontend request boundary
│ ├── views/ # Page level features
│ ├── components/ # Shared components
│ ├── utils/ # Frontend helpers
│ ├── themes/ # Theme and MUI overrides
│ └── routes/ # Route definitions
├── template/ # Template files
├── docs/ # Documentation
├── skill-sublinkpro/ # AI agent skill: AI interface over the REST API (portable SKILL.md format, user-facing, distributable)
├── static/ # Frontend build assets for production builds
├── main.go # Application entry
├── Dockerfile # Docker build
└── README.md
Frontend components must live where their ownership is clear. Do not place components randomly just because an import path is convenient.
- Put cross-page or layout-independent reusable components in
webs/src/components/. - Put feature/page-specific components next to their feature under
webs/src/views/<feature>/component/orwebs/src/views/<feature>/components/, following the existing folder name used by that feature. - Keep page entry files such as
webs/src/views/<feature>/index.jsxfocused on page composition, data loading, and feature orchestration. - Do not export feature-local components from unrelated pages. If a component is needed by a different page, move it to
webs/src/components/or a more appropriate shared module first. - Before adding a new shared component, check whether the same pattern already exists in nearby feature folders,
webs/src/components/, orwebs/src/ui-component/.
Examples:
- A reusable status panel used by multiple pages belongs in
webs/src/components/. - A dialog used only by airport management belongs under
webs/src/views/airports/component/. - A panel used only by a new
system-updatespage belongs underwebs/src/views/system-updates/components/unless it is intentionally shared elsewhere.
| Layer | Technology |
|---|---|
| Backend framework | Go + Gin |
| ORM | GORM |
| Database | SQLite, default / MySQL / PostgreSQL |
| Frontend framework | React 19 + Vite |
| UI | Material UI |
| Frontend package manager | Yarn 4 |
| Scheduler | robfig/cron |
git clone https://github.com/ZeroDeng01/sublinkPro.git
cd sublinkProGo 1.26.4 or newer is recommended, matching the repository, Docker, and CI.
go mod download
go run main.goThe backend listens on :8000 by default.
Run under webs/:
yarn install
yarn run startThe default Vite dev port is 3000, with /api proxied to the backend.
Run under webs/:
yarn run lint
yarn run build
yarn run lint:fix
yarn run prettierAfter frontend changes, run at least yarn run lint. Also run yarn run build when build output, asset paths, routing, base path behavior, or production integration is affected.
Note
This repository has no authoritative frontend test or typecheck script. Don't invent validation flows in docs or automation.
After backend changes, Go files must be formatted with gofmt, and golangci-lint plus related tests must run:
gofmt -w <changed-go-files>
golangci-lint run
go test ./...When adding or changing key business logic, API contracts, permission checks, configuration semantics, migrations, scheduled tasks, mihomo integration, protocol parsing, or data conversion, add or update matching Go tests. The GitHub release build runs golangci-lint and full repository go test ./... before building binaries.
.github/workflows/pr-checks.yml runs automatically when a PR is opened, reopened, or marked ready for review. It gives a quick baseline quality signal. Later fix commits do not automatically consume Actions again. After fixes are ready, the PR author or a repository admin can comment /recheck on the PR to trigger another round.
- Backend:
golangci-lint,go test ./... - Frontend:
yarn run lint,yarn run build
Each check job writes to the GitHub Step Summary, showing pass or failure status for each item so authors and reviewers can see what is done and what still needs work.
go build -o sublinkpro main.goThis is useful for development or a quick local compile. It is not the production embedded build.
Production build has two stages. Before running a production style local build, finish frontend lint/build and backend format/lint/tests first:
# 1) Frontend lint and build
cd webs
yarn run lint
yarn run build
# 2) Backend format, lint, and tests
cd ..
gofmt -w <changed-go-files>
golangci-lint run
go test ./...
# 3) Prepare production static assets
rm -rf static && mkdir -p static
cp -R webs/dist/. static/
# 4) Build production backend with embedded frontend assets
CGO_ENABLED=0 go build -tags=prod -ldflags="-s -w" -o sublinkProImportant
If you change frontend asset paths, PWA assets, base path behavior, embedding logic, or static file serving, verify all of these:
webslocal development mode- Frontend build output
- Production embedded build after copying assets into
static/
- Frontend UI:
/or the path set bySUBLINK_WEB_BASE_PATH - API: always under
/api/* - Subscription/share access: always under
/c/*
SUBLINK_WEB_BASE_PATH affects only the Web UI. It does not affect API or subscription fetch paths.
These directories contain runtime state. Handle them carefully:
db/logs/template/out/
Where:
db/: database, config files, GeoIP, and other local datatemplate/: template fileslogs/: runtime logs
| Module | File | Notes |
|---|---|---|
| Node speed tests | services/scheduler/speedtest_task.go |
Main flow for latency, speed, quality, and unlock checks |
| Unlock checks | services/unlock/*.go |
Provider registry / runtime / orchestrator / checkers |
| Tag rules | services/tag_service.go |
Automatic tag rule execution |
| Subscription generation | api/clients.go |
Subscription output, node filtering, rename |
| Chain proxy | api/subscription_chain.go / models/subscription_chain_rule.go |
Subscription chain proxy rules and condition based node selection |
| Host management | models/host.go |
Host mappings, batch writes, cache management |
| DNS resolution | services/mihomo/dns_resolver.go |
Custom DNS and proxy based resolution |
| Data migration | models/db_migrate.go |
Database migration scripts |
The protocol system has been refactored into a self registration + capability interface model. The goal is:
When adding a protocol, a developer should only need to add one protocol file under
node/protocol/, implement the protocol, export capabilities, and register it.
Use these as references:
node/protocol/protocol_demo.go: standard sample protocol- Real protocol files such as:
node/protocol/vmess.gonode/protocol/ss.gonode/protocol/http.go
Core capabilities live in node/protocol/protocol_meta.go:
Protocol: core protocol specificationProxyCapable: supports conversion to Clash Proxy structsSurgeCapable: supports Surge line exportSupportsClient(...): declares subscription output compatibility for Clash / mihomo / v2ray / Surge and other clientsMustRegisterProtocol(...): protocol registration entry point
After adding a protocol, these flows are connected automatically without adding extra switches:
- Protocol recognition, alias / scheme
- Node raw parsing
- Node raw field updates
- Node identity extraction, name / host / port / address
- Deduplication field reads
- Node link renaming
LinkToProxydispatchEncodeSurgedispatchEncodeProxyLinkdispatch- v2ray raw output compatibility filtering, through client support declared in the protocol file
- Protocol UI metadata output
-
Add a protocol file under
node/protocol/, for example:node/protocol/myprotocol.go -
Define the protocol struct.
Struct fields are the default source for UI field metadata, so names should be stable and clear.
-
Implement link
Decode/Encode.At minimum:
DecodeXxxURL(string) (Xxx, error)EncodeXxxURL(Xxx) string
-
If you need to convert back from Clash Proxy to a link, add
ConvertProxyToXxx(proxy Proxy) Xxx. -
If the protocol supports Clash export, implement
buildXxxProxy(link Urls, config OutputConfig)in the same file. -
If the protocol supports Surge export, implement
buildXxxSurgeLine(link string, config OutputConfig)in the same file. -
Self register in
init()in the same file. -
Declare client compatibility in the protocol file. Defaults are:
newProtocolSpec(...)supportsClientV2rayby default.newProxyProtocolSpec(...)supportsClientClash,ClientMihomo, andClientV2rayby default.newProxySurgeProtocolSpec(...)supportsClientClash,ClientMihomo,ClientV2ray, andClientSurgeby default.- If the protocol is suitable for only some clients, call
WithClientSupport(...)onbaseto override defaults. For example, Mieru declares onlyClientClashandClientMihomo, so v2ray / Surge output skips it.
Available client constants currently include
ClientClash,ClientMihomo,ClientV2ray, andClientSurge. Before adding a new client renderer, don't add protocol special cases only inapi/clients.go; first let protocol registration files declare support relationships.
func init() {
base := newProtocolSpec(
"myprotocol",
[]string{"myprotocol://"},
"MyProtocol",
"#1976d2",
"M",
MyProtocol{},
"Name",
DecodeMyProtocolURL,
EncodeMyProtocolURL,
func(p MyProtocol) LinkIdentity {
return buildIdentity("myprotocol", p.Name, p.Server, utils.GetPortString(p.Port))
},
// Optional: manual field schema. If omitted, reflection generates it from the struct.
)
// Optional: override client compatibility. If omitted, constructor defaults are used.
// base = base.WithClientSupport(ClientClash, ClientMihomo)
MustRegisterProtocol(newProxySurgeProtocolSpec(
base,
buildMyProtocolProxy,
func(proxy Proxy) bool {
return proxyTypeMatches(proxy, "myprotocol")
},
ConvertProxyToMyProtocol,
EncodeMyProtocolURL,
buildMyProtocolSurgeLine,
))
}If the protocol supports Clash but not Surge, use:
MustRegisterProtocol(newProxyProtocolSpec(...))If the protocol is only a demo protocol and needs only parsing plus UI metadata, registering only newProtocolSpec(...) is also fine.
If a protocol has extra share link prefixes that are not suitable for full Decode / Import, but still need to participate in client compatibility checks, use WithClientSupportAliases(...) to add aliases for compatibility checks only. This does not register that prefix as a full parser entry. For example, Mieru uses mierus:// only to decide that v2ray should not output it. It does not claim full field by field parsing support for the official mierus:// share link.
This repository handles vless + xhttp with these rules:
- Top level URL fields:
type=xhttpmaps to Clash / mihomonetwork: xhttpencryptionmaps to top level Clash / mihomoencryptionpathmaps toxhttp-opts.pathhostmaps toxhttp-opts.hostmodemaps toxhttp-opts.modeextrais decoded as JSON first, then mapped toxhttp-opts
- Supported fields inside
extra:headersmaps toxhttp-opts.headersnoGRPCHeadermaps toxhttp-opts.no-grpc-headerxPaddingBytesmaps toxhttp-opts.x-padding-bytesdownloadSettingsmaps toxhttp-opts.download-settings
- Common supported subfields inside
downloadSettingsinclude:path,host,headers,server,port,tls,alpnskipCertVerifymaps toskip-cert-verifyclientFingerprintmaps toclient-fingerprintprivateKeymaps toprivate-keyrealityOptsmaps toreality-optsechOptsmaps toech-opts
Two ECH meanings must be kept separate:
- Top level VLESS URL
ech=...corresponds to Xray/VLESSechConfigListsemantics. Not every form can round trip losslessly with mihomoech-opts. - When
echis a fixed base64 ECHConfig, it maps to top levelech-opts.enable: true+ech-opts.config. - When
echis Xray DNS / URI style, such asdomain+https://..., it is mapped on a best effort basis to what mihomo can express.enable: trueis preserved, andquery-server-nameis written when recognizable. The resolver URI itself is not preserved. - When a node comes from the Clash/mihomo YAML import flow, including Clash YAML airport subscriptions and manual Clash YAML import, and only top level
ech-opts.query-server-namecan be restored, the system rebuilds it before writingNode.Linkasech=<query-server-name>+https://dns.alidns.com/dns-queryusing local compatibility rules. extra.downloadSettings.echOptsis used only for nestedxhttpdownload settings and maps to mihomoxhttp-opts.download-settings.ech-opts.- Top level
echandextra.downloadSettings.echOptseach map to their ownech-optslevel. They are not merged or overwritten with each other.
Implementation notes:
xhttpis allowed only on VLESS. Don't reuse it for other protocols.- Don't silently downgrade
xhttptohttp,h2, orgrpc. - When users enable “skip certificate verification” in subscription settings,
OutputConfig.Certforce overrides output configuration. Forxhttp, this applies to both top levelskip-cert-verifyanddownload-settings.skip-cert-verify.
newProtocolSpec(...) can take optional FieldMeta entries at the end to drive frontend field display:
Name: field nameLabel: display labelType:string/int/boolGroup: group, such asbasic/auth/transport/tls/advancedDescription: field descriptionPlaceholder: placeholder textOptions: enum optionsAdvanced: whether this is an advanced fieldSecret: whether this is sensitiveMultiline: whether multiline display is recommended
If FieldMeta is omitted, the system falls back to struct reflection metadata, which enables minimal integration.
The ideal target is: only add and register the protocol file.
A small amount of “outside protocol” work still remains, but it is not core protocol integration:
- Add unit tests for the protocol
- Update README / docs support matrix if public docs need it
- Add better field metadata for frontend interaction, still in the protocol file when possible
Normally you should no longer change:
- Protocol dispatch in
node/protocol/clash.go - Protocol dispatch in
node/protocol/surge.go - Link generation switches in
node/sub.go - Protocol detection in
api/node.go - Name extraction switches in
api/node_raw.go
If adding a protocol still requires changes there, the abstraction has regressed. Fix the abstraction before adding more cases.
node/protocol/protocol_demo.go is not a production protocol. It is a protocol extension template.
It shows:
- How to define a protocol struct
- How to implement Decode / Encode
- How to add
LinkIdentity - How to declare field metadata
- How to implement Clash / Surge export capabilities
- How to complete registration in one file
When adding a real protocol, it is recommended to copy the ProtocolDemo structure and adapt it, instead of building everything from scratch.
SublinkPro uses a modular scheduled task system based on robfig/cron.
services/scheduler/
├── manager.go
├── job_ids.go
├── subscription_task.go
├── speedtest_task.go
├── host_cleanup_task.go
├── reporter.go
├── utils.go
└── bridge.go
- Define the task ID in
job_ids.go. - Add a task file under
services/scheduler/. - Wire it into the loading logic in
manager.go. - If the frontend needs task progress, connect it to
TaskManager.
func ExecuteYourTaskWithProgress() {
tm := getTaskManager()
task, ctx, err := tm.CreateTask(
models.TaskTypeYourType,
"你的任务名称",
models.TaskTriggerScheduled,
100,
)
if err != nil {
utils.Error("创建任务失败: %v", err)
return
}
taskID := task.ID
for i := 1; i <= 100; i++ {
select {
case <-ctx.Done():
utils.Info("任务被取消")
return
default:
}
tm.UpdateProgress(taskID, i, "当前处理项", map[string]interface{}{
"status": "success",
})
}
tm.CompleteTask(taskID, "任务完成", map[string]interface{}{
"total": 100,
})
}Unlock checks reuse the node check / speed test strategy flow. They don't start a separate task system.
api/node_check.gomodels/node_check_profile.gomodels/node.gomodels/unlock.goservices/scheduler/speedtest_config.goservices/scheduler/speedtest_task.goservices/unlock/registry.goservices/unlock/runtime.goservices/unlock/orchestrator.goservices/unlock/checker_*.go
- One independent Checker per Provider
- Unified registry / orchestrator
- Shared runtime, including proxy HTTP client, timeout, and landing country
- Unified result structure:
models.UnlockProviderResult
- Add
services/unlock/checker_<provider>.go. - Implement:
type UnlockChecker interface {
Key() string
Aliases() []string
Check(runtime UnlockRuntime) models.UnlockProviderResult
}- Register it with
RegisterUnlockChecker(...)ininit(). - Declare Provider metadata in the checker, including display name, category, rename variables, and related fields.
- If new status semantics are added, add status metadata in
services/unlock/meta.go. - Update
docs/features/unlock-check.mdonly when the docs need to list the current built in Providers.
Important
The current frontend node filters, tag rules, chain proxy conditions, and unlock options in subscription editing all consume backend metadata dynamically. Normally, adding one checker does not require adding Provider or status enums to the frontend or manually syncing option lists across multiple pages.
Provider specific forms are recommended:
$Unlock(gemini)$Unlock(openai)$Unlock(netflix)
These variables are delivered dynamically through backend metadata.
Node lists and subscription filters currently support multiple rules.
- Inside one rule: AND
- Between multiple rules: OR / AND, user selectable
- No rules: unlock filtering is disabled
Current automatic Tag rules and Chain rules support:
unlock_providerunlock_statusunlock_keywordunlock_result
Prefer unlock_provider and unlock_status for exact matches. unlock_keyword is better for fuzzy search.
Schemas, operators, and enum values for these fields are all delivered by the backend:
unlock_providerreads the list of registered checker Providers dynamicallyunlock_statusreads backend status metadata dynamicallyunlock_keyword/unlock_resultare treated as text fields
Multiple Provider checks for a single node are run with controlled parallelism in services/unlock/orchestrator.go.
- Inside each node: multiple Providers run in parallel
- A small concurrency limit is used
- Result order stays stable
This project uses a 5 field Cron format, without seconds:
| Field | Range | Description |
|---|---|---|
| Minute | 0-59 | Minute of the hour |
| Hour | 0-23 | Hour of the day |
| Day | 1-31 | Day of the month |
| Month | 1-12 | Month of the year |
| Weekday | 0-6 | Day of the week, 0=Sunday |
Common examples:
| Expression | Description |
|---|---|
*/5 * * * * |
Every 5 minutes |
0 */2 * * * |
Every 2 hours |
30 8 * * * |
Every day at 08:30 |
0 0 * * 0 |
Every Sunday at 00:00 |
0 2 1 * * |
Every month on day 1 at 02:00 |
- Tasks should be idempotent when possible.
- Long running tasks should support cancellation through
ctx.Done(). - Update docs when configuration semantics change.
- Frontend commands and production build flow should follow
webs/package.json, CI, and Dockerfile first. - Don't document commands that don't exist in the repository.
Clear, maintainable comments are part of good engineering practice, but comments should explain intent, constraints, and boundaries rather than restating the code.
Add necessary comments when adding or changing:
- Key business logic
- Cross-layer contracts
- Complex condition branches
- Scheduled tasks
- Migrations
- Concurrency flows
- Caching strategies
- mihomo integrations
- Auth/security logic
- Configuration precedence
- Non-obvious algorithms
Comments should primarily use Chinese. Keep English terms when they refer to:
- Upstream libraries
- Protocol fields
- Standard terminology
- Public API names
- Concepts that must match English documentation
- Update nearby comments when changing code
- Avoid comments that describe old behavior, fields, constraints, or flows
- Don't add noisy comments just to increase comment count
- Comments must not hide design problems
- For TODOs, FIXMEs, temporary compatibility logic, or known limitations, document the reason, trigger condition, and follow-up direction
- Exported packages, types, functions, methods, interfaces, constants, and variables should follow Go documentation conventions
- Package comments should live in
doc.goor an appropriate package file header - Non-exported but business-critical functions, struct fields, state enums, migration steps, and goroutine lifecycles should have concise Chinese comments
- Error-handling comments should explain business semantics or recovery strategy
- Comments should remain tidy under
gofmt
- React components, hooks, API wrappers, complex
useMemo/useEffectlogic, permission checks, responsive layout branches, theme helpers, and cross-component data flow should have Chinese comments when their intent is not obvious - Avoid piling comments inside JSX; prefer extracting complex conditions into named variables or small components
- Theme and style comments should explain semantic layering, light/dark differences, or relationship to existing patterns
- Comments in the frontend API layer must stay aligned with backend API semantics
- Temporary UI constraints, browser compatibility behavior, mobile-specific handling, and accessibility tradeoffs must explain their reason
Clear, maintainable tests and predictable test file organization are basic requirements for code quality. Tests should verify real behavior and boundaries rather than implementation details or fragile coverage-only cases.
Add or update Go tests when adding or changing:
- Backend key business logic
- API contracts
- Permission checks
- Configuration semantics
- Migrations
- Scheduled jobs
- mihomo integrations
- Protocol parsing
- Data transformations
Test names should describe the scenario and expected outcome. Avoid names like TestSuccess, TestError, or should work that do not express business intent.
- Cover happy paths, boundaries, error paths, and permission/configuration differences
- Don't test only the easiest happy path
- Test data should be local, readable, minimal, and expressed through helpers or fixtures
- Tests must be isolated from each other
- Assertions should explicitly verify important outputs, state changes, side effects, and error semantics
- When fixing a bug, write a regression test that reproduces the issue before changing implementation
- Never delete, skip, or weaken failing tests to hide problems
- Reuse existing test style, fixtures, mocks, or helpers when they exist
- Don't document hypothetical test frameworks or CI flows that are not wired into the repo
- Go test files must use the
_test.gosuffix and live in the same package directory as the code under test - Go test functions should follow standard names:
TestXxx,BenchmarkXxx,FuzzXxx, andExampleXxx - Prefer table-driven tests for multiple inputs, boundaries, and error cases
- Test helpers should call
t.Helper() - Temporary directories and files should prefer
t.TempDir() - Cleanup should be registered with
t.Cleanup() - Tests involving time, randomness, network, databases, filesystems, or goroutines should explicitly control dependencies
- HTTP handler tests should prefer
httptestwith explicit request/response assertions - Database tests should use isolated test databases, transactions, or temporary storage
- Concurrency tests should not rely on
time.Sleepto guess timing
This repository does not require frontend tests by default. Frontend quality is primarily guarded by lint, build, manual interaction checks, and light/dark/responsive verification.
Add or update frontend tests only when:
- Explicitly requested
- A frontend test framework is later wired into the repo
- The touched module already has frontend tests
If frontend tests are written:
- Test files should keep traceable names to the code under test
- Prefer
*.test.jsx,*.test.js,*.spec.jsx, or*.spec.js - Place tests near the component/hook/helper
- Test from user-observable behavior
- Verify text, roles, state changes, interaction results, error messages, and loading/empty/disabled states
- Don't assert internal state or fragile MUI-generated class names