feat: complete LinkPort connection and storage overhaul

This commit is contained in:
曾志威
2026-09-29 14:53:49 +08:00
parent 5de9d1f30e
commit 8eae1c42fd
515 changed files with 17738 additions and 1680 deletions
+1 -1
View File
@@ -57,6 +57,6 @@ steps:
commands:
- apk add --no-cache libarchive-tools curl
- node --test scripts/build-ci-release.test.mjs scripts/ci-release.test.mjs
- node scripts/verify-package.mjs bin/build-cache/ci-release/linux-amd64/serviceport
- node scripts/verify-package.mjs bin/build-cache/ci-release/linux-amd64/linkport
- node scripts/ci-release.mjs package
- node scripts/ci-release.mjs upload
+12 -12
View File
@@ -1,22 +1,22 @@
# Copy values into the service environment; this file is not loaded automatically.
# Development default: skip login, loopback management only.
# Set true to require administrator login (required for remote deployment).
SERVICEPORT_AUTH_ENABLED=false
LINKPORT_AUTH_ENABLED=false
# Encryption material is generated inside SQLite; no external master key.
SERVICEPORT_ADMIN_USER=admin
SERVICEPORT_ADMIN_PASSWORD=
SERVICEPORT_DATA=
LINKPORT_ADMIN_USER=admin
LINKPORT_ADMIN_PASSWORD=
LINKPORT_DATA=
# Port 0 lets the OS assign a free port. Set a fixed port for stable API/proxy URLs.
SERVICEPORT_HTTP_ADDR=127.0.0.1:0
SERVICEPORT_SECURE_COOKIE=false
LINKPORT_HTTP_ADDR=127.0.0.1:0
LINKPORT_SECURE_COOKIE=false
# Database gateways require a certificate and key. SSH does not use these.
SERVICEPORT_TLS_CERT=
SERVICEPORT_TLS_KEY=
LINKPORT_TLS_CERT=
LINKPORT_TLS_KEY=
# Optional external sing-box binary for sing-box connection assets.
# Leave empty to resolve "sing-box" from PATH. It is not bundled with serviceport.
SERVICEPORT_SING_BOX=
# Leave empty to resolve "sing-box" from PATH. It is not bundled with linkport.
LINKPORT_SING_BOX=
# Windows/macOS/Linux loopback: automatic local desktop startup, no external key.
# Existing databases: ./data first, then the OS user config directory/serviceport.
# Existing databases: ./data first, then the OS user config directory/linkport.
# New data uses ./data; encryption, audit and automatic TLS stay in SQLite.
# SERVICEPORT_NO_BROWSER=1
# LINKPORT_NO_BROWSER=1
+2 -1
View File
@@ -11,7 +11,7 @@ Desktop.ini
*.dll
*.so
*.dylib
/serviceport
/linkport
# Test binary, built with `go test -c`
*.test
@@ -43,6 +43,7 @@ go.work.sum
/.tmp/
/web/.tmp/
/.tmp-test/
/.tmp-go-cache/
*.sqlite
*.sqlite-journal
*.sqlite-shm
+3 -3
View File
@@ -1,4 +1,4 @@
# serviceport collaboration
# linkport collaboration
## Work ownership
@@ -15,12 +15,12 @@
- Use Go 1.25.0 (`GOTOOLCHAIN=go1.25.0`); newer installed toolchains may not compile the pinned Vitess dependency.
- Backend: `go test ./... -count=1 -timeout=120s` and `go vet ./...`.
- Frontend: `npm.cmd --prefix web run build`. Run the Playwright suite in `web` against a freshly built executable using `SERVICEPORT_E2E_BINARY`.
- Frontend: `npm.cmd --prefix web run build`. Run the Playwright suite in `web` against a freshly built executable using `LINKPORT_E2E_BINARY`.
- Do not build embedded Go assets while Vite is replacing `web/dist`. Restore tracked `web/dist/.gitkeep` after a frontend build if needed.
- Describe whether protocol tests used a real server or a fixture; do not present fixture coverage as live-server validation.
## Local data
- Preserve `data/serviceport.sqlite`, `data/audit`, TLS material, and `data/master-key.dpapi`.
- Preserve `data/linkport.sqlite`, `data/audit`, TLS material, and `data/master-key.dpapi`.
- Stop the owned local service and back up the complete `data` directory before deploying a migration. Do not modify real target data for smoke tests.
- Use isolated test databases and ports. Do not start subagents unless the user has enabled them for the active work.
+6 -6
View File
@@ -15,13 +15,13 @@ ARG VERSION
ARG COMMIT=unknown
ARG BUILD_TIME
RUN version="${VERSION:-$(cat VERSION)}" && build_time="${BUILD_TIME:-$(date -u '+%Y-%m-%dT%H:%M:%SZ')}" && \
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X serviceport/internal/buildinfo.Version=$version -X serviceport/internal/buildinfo.Commit=$COMMIT -X serviceport/internal/buildinfo.BuildTime=$build_time" -o /serviceport ./cmd/serviceport
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X linkport/internal/buildinfo.Version=$version -X linkport/internal/buildinfo.Commit=$COMMIT -X linkport/internal/buildinfo.BuildTime=$build_time" -o /linkport ./cmd/linkport
FROM alpine:3.22.1
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 serviceport && mkdir /data && chown serviceport /data
USER serviceport
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 linkport && mkdir /data && chown linkport /data
USER linkport
WORKDIR /app
COPY --from=backend /serviceport /app/serviceport
ENV SERVICEPORT_HTTP_ADDR=0.0.0.0:8080 SERVICEPORT_DATA=/data/serviceport.sqlite SERVICEPORT_AUTH_ENABLED=true
COPY --from=backend /linkport /app/linkport
ENV LINKPORT_HTTP_ADDR=0.0.0.0:8080 LINKPORT_DATA=/data/linkport.sqlite LINKPORT_AUTH_ENABLED=true
EXPOSE 8080
ENTRYPOINT ["/app/serviceport"]
ENTRYPOINT ["/app/linkport"]
+42 -42
View File
@@ -1,10 +1,10 @@
# serviceport
# linkport
<img src="web/public/serviceport.svg" alt="serviceport 项目图标" width="72" height="72" />
<img src="web/public/linkport.svg" alt="linkport 项目图标" width="72" height="72" />
正常启动时打印项目标识与版本信息;图标设计和命令输出兼容性见 [项目标识说明](docs/branding.md)。
开发时双击根目录 `start-dev.cmd`,系统自动分配空闲管理端口,并打开实际地址(默认免登录)。启动窗口会显示 `http://127.0.0.1:<实际端口>`,每次启动端口可能不同。该入口先切换到自身目录,不覆盖 `SERVICEPORT_DATA`;Windows 本地模式优先指定路径,否则依次查找当前目录 `data/serviceport.sqlite`、旧 `%APPDATA%\serviceport\serviceport.sqlite`,均不存在才在当前目录新建。Windows、Linux、macOS 默认均使用单 SQLite 保存配置、主密钥、审计和自动 TLS,不依赖 `master-key.dpapi` 或主密钥环境变量。升级旧库前须停服并备份完整旧目录,详见 [单文件存储与迁移](docs/single-file-storage.md)。
开发时双击根目录 `start-dev.cmd`,系统自动分配空闲管理端口,并打开实际地址(默认免登录)。启动窗口会显示 `http://127.0.0.1:<实际端口>`,每次启动端口可能不同。该入口先切换到自身目录,不覆盖 `LINKPORT_DATA`;Windows 本地模式优先指定路径,否则依次查找当前目录 `data/linkport.sqlite`、旧 `%APPDATA%\linkport\linkport.sqlite`,均不存在才在当前目录新建。Windows、Linux、macOS 默认均使用单 SQLite 保存配置、主密钥、审计和自动 TLS,不依赖 `master-key.dpapi` 或主密钥环境变量。升级旧库前须停服并备份完整旧目录,详见 [单文件存储与迁移](docs/single-file-storage.md)。
使用 Go 和 Vue 3 构建的 MySQL、MongoDB、PostgreSQL、SQL Server、Redis、RabbitMQ、SSH、SFTP、HTTP/HTTPS 连接代理管理后台。目标服务独立保存地址、凭据及直连 / SOCKS5 / SSH / sing-box 出口;网关引用目标,独立管理监听端口、访问凭据及可选只读策略。一个目标可被多个同协议网关复用。
@@ -34,7 +34,7 @@
- HTTP/HTTPS:网关 Basic Auth、目标 Basic Auth 替换、TLS、请求审计和来源限制;仅转发普通 HTTP 请求,不支持 CONNECT 或 WebSocket 升级。
- SFTP:复用 SSH 主机指纹和账号认证,仅开放 SFTP 子系统;文件操作写入审计,不记录文件内容。
- SOCKS5:每条代理独立配置,支持无认证和用户名密码认证、远端 DNS;出口失败不回退直连。
- sing-box:连接资产保存一个受限的远程 outbound JSON;serviceport 管理 loopback SOCKS5 入站与外部子进程。binary 不随发布包提供,通过 `SERVICEPORT_SING_BOX` 或 `PATH` 定位;配置检查不等于真实目标连通。详见 [sing-box 连接资产](docs/sing-box.md)。
- sing-box:连接资产保存一个受限的远程 outbound JSON;linkport 管理 loopback SOCKS5 入站与外部子进程。binary 不随发布包提供,通过 `LINKPORT_SING_BOX` 或 `PATH` 定位;配置检查不等于真实目标连通。详见 [sing-box 连接资产](docs/sing-box.md)。
- 个人工作台:可选个人登录,SQLite 加密配置、CSRF/会话、密码重新认证查看、来源白名单、连接限额、启停及 7 天审计。支持短期 API Token、作用域控制和 Prometheus 指标,不提供管理用户或角色分配。
网关支持多个独立账号,可暂不关联目标;账号支持禁用、来源限制,MySQL/MongoDB 支持账号只读策略。新增批量创建和启停、健康检查与指标、加密配置备份及历史恢复。使用方式、旧配置迁移与验证边界见 [网关账号与运维](docs/gateway-operations.md),双实例部署示例见 [高可用部署](docs/high-availability.md)。
@@ -43,19 +43,19 @@
配置中心支持 WebDAV 多端同步,在设备间加密同步目标、代理、资产、凭据和网关配置,支持手动及定时同步、独立加密备份、删除传播与冲突选择,使用 ETag 条件写入防止并发覆盖。设置方式见 [WebDAV 多端同步](docs/webdav-sync.md)。
运行中的网关支持通过「切换目标」或「拖拽连线」调整目标服务及共用账号。监听保持运行,新连接使用新目标,已有连接保留原目标。详见 [运行中切换目标](docs/live-target-switch.md)。
运行中的网关支持通过「切换目标」或「拖拽连线」调整目标服务及账号映射。保存成功后监听保持运行,现有客户端连接会断开,客户端重连后使用新目标;保存失败不影响原连接。详见 [运行中切换目标](docs/live-target-switch.md)。
「密码解密」支持选择算法、分组模式、填充方式、Key、IV 和编码;选择 Navicat 12 自动填入 AES / CBC / PKCS7 参数,也支持 Navicat 11 格式。详见 [密码解密工具](docs/navicat-decryption.md)。全项目页面支持手机、平板及宽屏布局,见 [响应式界面](docs/responsive-ui.md)。
目标版本与实测版本请区分,见 [验证说明](docs/testing.md)。
「拖拽连接」可将网关拖到同协议目标并保存关联,也支持点击选择;运行中切换仅对新连接生效。命令审计与连接事件按 UTC 日期保存至 `data/audit/YYYY-MM-DD.sqlite`(每库分别包含 commands/events),保留 7 天。旧记录启动时自动迁移;配置库继续固定使用 `data/serviceport.sqlite`。两类日志均支持跨天筛选、分页和 CSV 全量导出;命令内容仍加密保存,导出只包含当前保留且已记录的内容。备份请停止服务并复制整个 `data` 目录。
「拖拽连接」可从网关账号连接到同协议目标账号并保存关联,也支持点击选择;运行中切换会断开该网关的现有连接,重连后使用新映射。命令审计与连接事件按 UTC 日期保存至 `data/audit/YYYY-MM-DD.sqlite`(每库分别包含 commands/events),保留 7 天。旧记录启动时自动迁移;配置库继续固定使用 `data/linkport.sqlite`。两类日志均支持跨天筛选、分页和 CSV 全量导出;命令内容仍加密保存,导出只包含当前保留且已记录的内容。备份请停止服务并复制整个 `data` 目录。
## 构建
项目包含 `.drone.yml`,执行前端构建、Go 1.25.0 无 CGO 六平台编译、发布脚本测试及 Linux amd64 隔离启动验证。linux/amd64、linux/arm64、windows/amd64、windows/arm64、darwin/amd64、darwin/arm64 依次构建,平台并发和 Go 编译并发默认均为 1。六个独立压缩包保留在 `bin/releases/ci/`,附发布清单和 SHA256;打包成功后直接上传 IPFS,再发布下载索引目录并输出完整下载地址,不按构建事件跳过。API 与认证令牌直接配置在 YAML 中,无需 Drone Secret;令牌对仓库读者可见。不再构建或推送 Docker 镜像,也不使用 Gitea 包仓库,详见 [Drone/IPFS 发布说明](docs/ipfs.md)。流水线不读取真实 `data/`;真实服务矩阵仍使用 `scripts/test-matrix.ps1` 或 `scripts/test-matrix.sh`。
项目版本统一维护在根目录 `VERSION`。`serviceport.exe version` 或 `serviceport.exe --version --json` 可查看版本、Git 提交、构建时间与平台,管理界面顶部也可查看版本信息。构建/打包脚本自动注入元数据;直接 `go build` 仍使用 VERSION 的版本值,但提交和时间可能显示 `unknown`。
项目版本统一维护在根目录 `VERSION`。`linkport.exe version` 或 `linkport.exe --version --json` 可查看版本、Git 提交、构建时间与平台,管理界面顶部也可查看版本信息。构建/打包脚本自动注入元数据;直接 `go build` 仍使用 VERSION 的版本值,但提交和时间可能显示 `unknown`。
目标出口新增内置 `K8s port-forward`,不依赖 kubectl,支持 Pod、Service、Deployment 和 StatefulSet。配置与权限见 [Kubernetes 说明](docs/kubernetes.md)。
@@ -67,7 +67,7 @@ Windows PowerShell:
.\scripts\build.ps1
```
源码目录的 `start-dev.cmd` 需要 Go、Node.js 和 npm;它不是免编译的发行版启动器。脚本以项目自身位置为基准,优先使用 `tools/go/bin/go.exe` 和 `tools/node/node.exe`(完整工具链含 npm),否则从 PATH 查找,不写死机器安装路径。可将项目和工具目录一起复制到其他电脑;详见 [相对路径与便携构建](docs/build-output.md)。不需要开发环境时直接运行预编译发行版 `serviceport.exe`。
源码目录的 `start-dev.cmd` 需要 Go、Node.js 和 npm;它不是免编译的发行版启动器。脚本以项目自身位置为基准,优先使用 `tools/go/bin/go.exe` 和 `tools/node/node.exe`(完整工具链含 npm),否则从 PATH 查找,不写死机器安装路径。可将项目和工具目录一起复制到其他电脑;详见 [相对路径与便携构建](docs/build-output.md)。不需要开发环境时直接运行预编译发行版 `linkport.exe`。
Linux / macOS:
@@ -75,47 +75,47 @@ Linux / macOS:
sh scripts/build.sh
```
脚本安装前端依赖、构建 Vue、执行 Go 测试并生成 `bin/serviceport`(Windows 为 `.exe`)。前端嵌入可执行文件,部署不需要 Node.js。
脚本安装前端依赖、构建 Vue、执行 Go 测试并生成 `bin/linkport`(Windows 为 `.exe`)。前端嵌入可执行文件,部署不需要 Node.js。
构建缓存、发布包和升级备份也统一放在 `bin/` 子目录,详见 [本地输出目录](docs/build-output.md)。`bin/backups/` 含用户数据,清理构建产物时应保留或先转存。
## 启动
Windows 本地使用:解压发行包后双击 `serviceport.exe`,浏览器自动打开,**开发阶段默认免登录**,直接进入控制台。无需安装 Go/Node.js,无需设置环境变量。保持程序窗口运行,按 Ctrl+C 停止服务。
Windows 本地使用:解压发行包后双击 `linkport.exe`,浏览器自动打开,**开发阶段默认免登录**,直接进入控制台。无需安装 Go/Node.js,无需设置环境变量。保持程序窗口运行,按 Ctrl+C 停止服务。
先在「目标服务」中添加 MySQL/MongoDB/SSH 目标,再在「网关服务」中选择目标并配置监听、网关账号和访问策略。目标被引用时不能删除;编辑目标前需要停止所有引用它的网关。旧版配置会在事务中自动拆分,保留网关 ID、凭据、SSH 指纹和启用状态,升级前先停止服务并备份数据目录。
右上角可切换浅色、深色、海蓝及跟随系统主题,选择保存在当前浏览器中。
免登录模式仅允许回环地址访问,仍校验请求来源与 CSRF。设置 `SERVICEPORT_AUTH_ENABLED=true` 可启用个人登录;本地模式会在尚无账号时打开首次设置页面,已有账号直接登录。旧版附加用户及其 Token 不再可用,初始账号继续使用。远程服务器部署必须开启认证。
免登录模式仅允许回环地址访问,仍校验请求来源与 CSRF。设置 `LINKPORT_AUTH_ENABLED=true` 可启用个人登录;本地模式会在尚无账号时打开首次设置页面,已有账号直接登录。旧版附加用户及其 Token 不再可用,初始账号继续使用。远程服务器部署必须开启认证。
Windows、Linux、macOS 的回环管理地址自动使用本地模式;所有平台的随机主密钥和自动 TLS 均保存在 SQLite 内。`SERVICEPORT_DATA` 支持相对或绝对文件路径,显式指定后不再搜索。选中的库被占用、损坏,或旧库缺少迁移密钥时直接报错,不另建空库。可用 `.\serviceport.exe data-path` 查看最终路径,该命令不创建数据库。升级前备份完整旧目录;完成迁移且正常停服后,仅复制 SQLite 即可跨机器使用。数据库文件包含完整解密材料,必须限制访问。详见 [Windows 使用说明](packaging/windows/README.md) 和 [单文件存储](docs/single-file-storage.md)。
Windows、Linux、macOS 的回环管理地址自动使用本地模式;所有平台的随机主密钥和自动 TLS 均保存在 SQLite 内。`LINKPORT_DATA` 支持相对或绝对文件路径,显式指定后不再搜索。选中的库被占用、损坏,或旧库缺少迁移密钥时直接报错,不另建空库。可用 `.\linkport.exe data-path` 查看最终路径,该命令不创建数据库。升级前备份完整旧目录;完成迁移且正常停服后,仅复制 SQLite 即可跨机器使用。数据库文件包含完整解密材料,必须限制访问。详见 [Windows 使用说明](packaging/windows/README.md) 和 [单文件存储](docs/single-file-storage.md)。
以下为启用登录的 PowerShell 示例(无需配置主密钥):
```powershell
$env:SERVICEPORT_AUTH_ENABLED = 'true'
$env:SERVICEPORT_ADMIN_USER = 'admin'
$env:LINKPORT_AUTH_ENABLED = 'true'
$env:LINKPORT_ADMIN_USER = 'admin'
$adminSecret = Read-Host '初始登录密码(12–72 字节)' -AsSecureString
$env:SERVICEPORT_ADMIN_PASSWORD = [Net.NetworkCredential]::new('', $adminSecret).Password
.\bin\serviceport.exe
$env:LINKPORT_ADMIN_PASSWORD = [Net.NetworkCredential]::new('', $adminSecret).Password
.\bin\linkport.exe
```
打开启动窗口打印的管理页面地址。默认由系统分配空闲端口;需要固定地址时,可在启动前设置 `SERVICEPORT_HTTP_ADDR=127.0.0.1:8080`。个人账号只在首次运行创建,之后不会用环境变量覆盖密码。`SERVICEPORT_ADMIN_USER` 和 `SERVICEPORT_ADMIN_PASSWORD` 保留原名称以兼容现有启动配置。
打开启动窗口打印的管理页面地址。默认由系统分配空闲端口;需要固定地址时,可在启动前设置 `LINKPORT_HTTP_ADDR=127.0.0.1:8080`。个人账号只在首次运行创建,之后不会用环境变量覆盖密码。`LINKPORT_ADMIN_USER` 和 `LINKPORT_ADMIN_PASSWORD` 保留原名称以兼容现有启动配置。
主密钥由程序在首次初始化时写入 SQLite,后续自动复用,不再读取 `SERVICEPORT_MASTER_KEY`。旧外部密钥库使用 `migrate-key` 命令做一次性迁移;运行中的 WAL/SHM 文件不能忽略,单文件复制须先正常停服。
主密钥由程序在首次初始化时写入 SQLite,后续自动复用,不再读取 `LINKPORT_MASTER_KEY`。旧外部密钥库使用 `migrate-key` 命令做一次性迁移;运行中的 WAL/SHM 文件不能忽略,单文件复制须先正常停服。
| 环境变量 | 默认值 / 用途 |
| --- | --- |
| `SERVICEPORT_AUTH_ENABLED` | 默认 `false`,开发阶段免登录且仅限本机;`true` 启用个人登录 |
| `SERVICEPORT_ADMIN_USER` | 非桌面模式启用认证时,首次启动必填 |
| `SERVICEPORT_ADMIN_PASSWORD` | 非桌面模式启用认证时,首次启动必填,12–72 字节 |
| `SERVICEPORT_DATA` | 留空自动选择;Windows 本地模式优先当前目录已有库,再查 AppData,新库使用 `data/serviceport.sqlite` |
| `SERVICEPORT_HTTP_ADDR` | `127.0.0.1:0`(系统分配空闲端口;可显式指定固定端口) |
| `SERVICEPORT_NO_BROWSER` | 本地模式设为 `1` 可禁用自动打开浏览器 |
| `SERVICEPORT_SECURE_COOKIE` | 默认 `false`;管理界面经 HTTPS 访问时设为 `true` |
| `SERVICEPORT_TLS_CERT` / `SERVICEPORT_TLS_KEY` | 数据库网关客户端 TLS 证书和私钥路径 |
| `SERVICEPORT_SING_BOX` | 可选的 sing-box binary 绝对路径或可由 `PATH` 解析的命令;留空时查找 `sing-box`。仅使用 sing-box 连接资产时需要,发布包不捆绑 |
| `LINKPORT_AUTH_ENABLED` | 默认 `false`,开发阶段免登录且仅限本机;`true` 启用个人登录 |
| `LINKPORT_ADMIN_USER` | 非桌面模式启用认证时,首次启动必填 |
| `LINKPORT_ADMIN_PASSWORD` | 非桌面模式启用认证时,首次启动必填,12–72 字节 |
| `LINKPORT_DATA` | 留空自动选择;Windows 本地模式优先当前目录已有库,再查 AppData,新库使用 `data/linkport.sqlite` |
| `LINKPORT_HTTP_ADDR` | `127.0.0.1:0`(系统分配空闲端口;可显式指定固定端口) |
| `LINKPORT_NO_BROWSER` | 本地模式设为 `1` 可禁用自动打开浏览器 |
| `LINKPORT_SECURE_COOKIE` | 默认 `false`;管理界面经 HTTPS 访问时设为 `true` |
| `LINKPORT_TLS_CERT` / `LINKPORT_TLS_KEY` | 数据库网关客户端 TLS 证书和私钥路径 |
| `LINKPORT_SING_BOX` | 可选的 sing-box binary 绝对路径或可由 `PATH` 解析的命令;留空时查找 `sing-box`。仅使用 sing-box 连接资产时需要,发布包不捆绑 |
`.env.example` 仅为示例,程序不自动加载 `.env`。远程管理使用 HTTPS 反向代理。
@@ -146,16 +146,16 @@ MySQL/MongoDB 网关允许普通连接;配置证书后同一端口也支持客
```powershell
$env:GOTOOLCHAIN = 'go1.25.0'
go run ./cmd/devcert
$env:SERVICEPORT_TLS_CERT = 'data/tls/cert.pem'
$env:SERVICEPORT_TLS_KEY = 'data/tls/key.pem'
.\bin\serviceport.exe
$env:LINKPORT_TLS_CERT = 'data/tls/cert.pem'
$env:LINKPORT_TLS_KEY = 'data/tls/key.pem'
.\bin\linkport.exe
```
客户端将 `cert.pem` 作为 CA 验证;局域网正式使用时换成覆盖实际网关域名的受信任证书。目标数据库 TLS 独立配置,默认验证系统 CA 或配置中提供的 PEM CA,不提供跳过验证模式。
### SSH 主机身份
同一份 serviceport 数据库中的全部 SSH/SFTP 网关共用一份 Ed25519 主机密钥,加密独立保存。停启服务、修改配置、新建网关,以及删除全部网关后重新创建,均保留同一指纹;目标服务器的主机指纹校验仍各自独立。
同一份 linkport 数据库中的全部 SSH/SFTP 网关共用一份 Ed25519 主机密钥,加密独立保存。停启服务、修改配置、新建网关,以及删除全部网关后重新创建,均保留同一指纹;目标服务器的主机指纹校验仍各自独立。
升级前停止服务并备份完整 `data` 目录(含数据库与主密钥)。首次升级按网关 ID 排序,沿用第一个有效的 Ed25519 主机密钥并统一现有网关;没有有效密钥时生成一份。原指纹不同的端口需核对界面中的“网关指纹”,再更新客户端 `known_hosts`。向已有实例导入配置或恢复历史不会替换共用密钥;向空实例恢复含 SSH/SFTP 网关的加密备份时沿用备份中的密钥。完整数据目录备份也会保留没有任何网关时的共用密钥。
@@ -166,7 +166,7 @@ $env:SERVICEPORT_TLS_KEY = 'data/tls/key.pem'
目标、网关、SOCKS5 密码和 SSH 私钥口令可在重新验证登录密码后查看,30 秒自动隐藏并记录审计。SSH 私钥不可下载。个人登录密码仅保存 bcrypt 哈希,只能重置:
```powershell
.\bin\serviceport.exe reset-password
.\bin\linkport.exe reset-password
```
重置命令使用同一个数据路径和主密钥,交互输入密码并使旧会话失效。
@@ -178,14 +178,14 @@ $env:GOTOOLCHAIN = 'go1.25.0'
go test ./...
go vet ./...
npm.cmd --prefix web run build
go build -o bin/serviceport.exe ./cmd/serviceport
node scripts/verify-random-port.mjs bin/serviceport.exe
go build -o bin/linkport.exe ./cmd/linkport
node scripts/verify-random-port.mjs bin/linkport.exe
npm.cmd --prefix web test
```
前端独立开发:先设置 `$env:SERVICEPORT_HTTP_ADDR='127.0.0.1:8080'` 再启动后端,然后运行 `npm --prefix web run dev`;Vite 将 `/api` 转发到该固定端口。直接使用 `start-dev.cmd` 的内嵌页面则默认使用随机空闲端口。浏览器测试启动隔离的临时数据服务,默认使用已安装的 Edge;其他平台可设置 `SERVICEPORT_BROWSER_CHANNEL=chromium` 并先运行 `npx playwright install chromium`。
前端独立开发:先设置 `$env:LINKPORT_HTTP_ADDR='127.0.0.1:8080'` 再启动后端,然后运行 `npm --prefix web run dev`;Vite 将 `/api` 转发到该固定端口。直接使用 `start-dev.cmd` 的内嵌页面则默认使用随机空闲端口。浏览器测试启动隔离的临时数据服务,默认使用已安装的 Edge;其他平台可设置 `LINKPORT_BROWSER_CHANNEL=chromium` 并先运行 `npx playwright install chromium`。
Docker 构建:`docker build -t serviceport .`。容器默认以 UID 10001 运行,数据卷须可写;代理监听端口需要自行映射。数据库版本矩阵见 [验证说明](docs/testing.md)。
Docker 构建:`docker build -t linkport .`。容器默认以 UID 10001 运行,数据卷须可写;代理监听端口需要自行映射。数据库版本矩阵见 [验证说明](docs/testing.md)。
## Windows 便携包
@@ -199,11 +199,11 @@ Docker 构建:`docker build -t serviceport .`。容器默认以 UID 10001 运
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1
```
安装 Go、Node.js/npm 后,可双击根目录 `package.cmd` 打包;窗口保留结果,命令行自动化可设置 `SERVICEPORT_PACKAGE_NO_PAUSE=1` 取消暂停。版本默认读取 `VERSION`。
安装 Go、Node.js/npm 后,可双击根目录 `package.cmd` 打包;窗口保留结果,命令行自动化可设置 `LINKPORT_PACKAGE_NO_PAUSE=1` 取消暂停。版本默认读取 `VERSION`。
最终交付目录 `bin/releases/<时间与唯一标识>/` 只包含 `serviceport.exe`,直接双击即可运行。网页已嵌入程序,不再输出 ZIP、额外工具、脚本、说明文件或校验文件;旧 `-Compression` 参数已移除。版本、提交和构建时间可通过 `serviceport.exe --version --json` 查看,EXE 的 SHA-256 显示在打包日志中。前端按源码、配置、依赖锁文件、Node 版本及产物内容校验缓存,变化或缺失时自动重建;依赖变化自动执行 `npm ci`。可用 `.\package.cmd -RebuildWeb` 强制重建前端。缓存位于 `bin/build-cache`。Go 每次构建当前代码并注入版本信息;构建和验证均在临时构建目录完成,只有验证成功才生成最终交付目录。
最终交付目录 `bin/releases/<时间与唯一标识>/` 只包含 `linkport.exe`,直接双击即可运行。网页已嵌入程序,不再输出 ZIP、额外工具、脚本、说明文件或校验文件;旧 `-Compression` 参数已移除。版本、提交和构建时间可通过 `linkport.exe --version --json` 查看,EXE 的 SHA-256 显示在打包日志中。前端按源码、配置、依赖锁文件、Node 版本及产物内容校验缓存,变化或缺失时自动重建;依赖变化自动执行 `npm ci`。可用 `.\package.cmd -RebuildWeb` 强制重建前端。缓存位于 `bin/build-cache`。Go 每次构建当前代码并注入版本信息;构建和验证均在临时构建目录完成,只有验证成功才生成最终交付目录。
`start-dev.cmd` 用于直接启动。源码目录默认快速构建:复用前端依赖和构建缓存,跳过启动前的 Go 全量测试;Go 每次编译当前源码、注入最新构建信息,并复用 Go 编译缓存。先生成 `bin/dev-build/serviceport.exe`,成功后才停止当前目录中的旧服务并等待退出,将整个 `data` 备份到 `bin/backups/<时间>/`,再替换并运行 `bin/serviceport.exe`。构建失败时保留原服务运行;数据备份始终在停服后进行。日志显示前端、测试(启用时)、Go 构建和总耗时。
`start-dev.cmd` 用于直接启动。源码目录默认快速构建:复用前端依赖和构建缓存,跳过启动前的 Go 全量测试;Go 每次编译当前源码、注入最新构建信息,并复用 Go 编译缓存。先生成 `bin/dev-build/linkport.exe`,成功后才停止当前目录中的旧服务并等待退出,将整个 `data` 备份到 `bin/backups/<时间>/`,再替换并运行 `bin/linkport.exe`。构建失败时保留原服务运行;数据备份始终在停服后进行。日志显示前端、测试(启用时)、Go 构建和总耗时。
```powershell
.\start-dev.cmd # 缓存构建并启动
@@ -217,9 +217,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1
Windows 开发构建和发布打包共用构建锁;已有构建运行时,第二次构建会明确报错,完成后可重试。构建期间不要另行运行 Vite,以免替换 Go 正在嵌入的前端资源。
单 EXE 交付不附带启动脚本,也不捆绑可选的 sing-box binary。仓库中的启动脚本仍兼容旧发布目录(没有构建脚本时直接运行根目录的 `serviceport.exe`);构建参数仅适用于源码目录。`start-dev.cmd` 先切换到入口所在目录,通过相对路径调用 PowerShell,退出后恢复调用者目录;直接执行 `scripts/start-dev.ps1` 也会先切到项目/解压根目录。脚本不设置或覆盖 `SERVICEPORT_DATA`,使用程序的 `data-path` 命令确定备份目录;先停本项目旧服务,再锁定并备份实际数据目录,然后启动。其他实例占用数据或数据目录包含备份目标时拒绝备份启动。默认免登录,自动分配空闲端口并打开浏览器;实际地址见启动窗口。启动脚本尊重显式设置的 `SERVICEPORT_HTTP_ADDR`,需要稳定 API 地址或反向代理时请指定固定端口。快速构建不会省略完整数据备份,大审计目录的备份仍可能花费时间。
单 EXE 交付不附带启动脚本,也不捆绑可选的 sing-box binary。仓库中的启动脚本仍兼容旧发布目录(没有构建脚本时直接运行根目录的 `linkport.exe`);构建参数仅适用于源码目录。`start-dev.cmd` 先切换到入口所在目录,通过相对路径调用 PowerShell,退出后恢复调用者目录;直接执行 `scripts/start-dev.ps1` 也会先切到项目/解压根目录。脚本不设置或覆盖 `LINKPORT_DATA`,使用程序的 `data-path` 命令确定备份目录;先停本项目旧服务,再锁定并备份实际数据目录,然后启动。其他实例占用数据或数据目录包含备份目标时拒绝备份启动。默认免登录,自动分配空闲端口并打开浏览器;实际地址见启动窗口。启动脚本尊重显式设置的 `LINKPORT_HTTP_ADDR`,需要稳定 API 地址或反向代理时请指定固定端口。快速构建不会省略完整数据备份,大审计目录的备份仍可能花费时间。
打包脚本编译 Windows x64 单文件程序,自动验证目录只有一个 EXE、内嵌版本信息、隔离启动、网页资源与个人登录。可用 `node scripts/verify-package.mjs <交付目录或serviceport.exe路径>` 重复验证,测试数据写入独立临时目录。文档和配置示例保留在源码仓库。运行后使用单 SQLite 保存配置、密钥、审计及自动 TLS;“单 EXE”指交付文件,不表示运行数据被写进 EXE。打包不包含实际数据或凭据,不替换正在运行的服务;更新已有数据前仍需停服并备份完整数据目录。
打包脚本编译 Windows x64 单文件程序,自动验证目录只有一个 EXE、内嵌版本信息、隔离启动、网页资源与个人登录。可用 `node scripts/verify-package.mjs <交付目录或linkport.exe路径>` 重复验证,测试数据写入独立临时目录。文档和配置示例保留在源码仓库。运行后使用单 SQLite 保存配置、密钥、审计及自动 TLS;“单 EXE”指交付文件,不表示运行数据被写进 EXE。打包不包含实际数据或凭据,不替换正在运行的服务;更新已有数据前仍需停服并备份完整数据目录。
## 文档
@@ -236,7 +236,7 @@ Windows 开发构建和发布打包共用构建锁;已有构建运行时,第
## 目标压缩与命令审计
MySQL/MongoDB 目标可选择 zlib 压缩(默认关闭),通过协议协商启用,目标不支持时保持未压缩连接;连接测试显示实际协商结果。压缩仅作用于 serviceport 到目标服务一侧,直连和 SOCKS5 均适用。当前 SSH 库仅支持未压缩传输。
MySQL/MongoDB 目标可选择 zlib 压缩(默认关闭),通过协议协商启用,目标不支持时保持未压缩连接;连接测试显示实际协商结果。压缩仅作用于 linkport 到目标服务一侧,直连和 SOCKS5 均适用。当前 SSH 库仅支持未压缩传输。
「命令审计」记录 MySQL SQL/预编译模板、MongoDB 请求、SSH exec、交互输入行和 SFTP 操作,可按网关/协议筛选并分页查看。内容加密保存,保留 7 天。SSH 输入保留完整文本及原始按键,结合终端回显补齐 Tab 补全和历史召回;交互输入超过 1 MiB 时中止会话,不静默截断。其他协议沿用每条 16 KiB 的采集上限并标记截断。放行/拦截是网关决策,不表示目标执行成功。原始输入审计写入失败时不继续转发该命令。
+1 -1
View File
@@ -2,8 +2,8 @@
package main
import (
"serviceport/internal/localcert"
"fmt"
"linkport/internal/localcert"
"os"
"time"
)
@@ -2,7 +2,7 @@ package main
const startupBanner = `
+----------------+
| ____ | serviceport
| ____ | linkport
| o---| _ \ | SERVICE GATEWAY
| | |_) | |
| | __/ |
@@ -21,7 +21,7 @@ func TestStartupBannerCommandBoundary(testContext *testing.T) {
if err := os.WriteFile(blocker, []byte("fixture"), 0600); err != nil {
testContext.Fatal(err)
}
testContext.Setenv("SERVICEPORT_DATA", filepath.Join(blocker, "test.sqlite"))
testContext.Setenv("LINKPORT_DATA", filepath.Join(blocker, "test.sqlite"))
previousArgs, previousStdout, previousLog := os.Args, os.Stdout, log.Writer()
reader, writer, err := os.Pipe()
if err != nil {
@@ -36,7 +36,7 @@ func TestStartupBannerCommandBoundary(testContext *testing.T) {
var messages bytes.Buffer
log.SetOutput(&messages)
os.Stdout = writer
os.Args = []string{"serviceport"}
os.Args = []string{"linkport"}
if command != "" {
os.Args = append(os.Args, command)
}
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"crypto/tls"
"fmt"
"log"
"os"
"path/filepath"
"linkport/internal/gateway"
"linkport/internal/store"
)
func gatewayTLS(s *store.Store, path string) (*tls.Config, error) {
var tlsConfig *tls.Config
cert, key := os.Getenv("LINKPORT_TLS_CERT"), os.Getenv("LINKPORT_TLS_KEY")
if cert == "" && key == "" {
pair, err := s.LocalTLS(filepath.Dir(path))
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{pair}, MinVersion: tls.VersionTLS12}
}
if cert != "" || key != "" {
pair, e := tls.LoadX509KeyPair(cert, key)
if e != nil {
return nil, fmt.Errorf("load gateway TLS certificate: %w", e)
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{pair}, MinVersion: tls.VersionTLS12}
}
return tlsConfig, nil
}
func restoreGateways(s *store.Store, m *gateway.Manager) error {
if err := m.ResumeHealthChecks(); err != nil {
return fmt.Errorf("restore health settings: %w", err)
}
ps, err := s.List()
if err != nil {
return fmt.Errorf("cannot load configurations; verify master key: %w", err)
}
for _, p := range ps {
if p.Enabled {
if e := m.Start(p); e != nil {
log.Printf("proxy %s restore failed: %v", p.ID, e)
}
}
}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package main
import (
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"golang.org/x/term"
"linkport/internal/buildinfo"
"linkport/internal/desktop"
"linkport/internal/store"
)
// Standalone commands run before opening or migrating application data.
type standaloneCommand func([]string) error
var standaloneCommands = map[string]standaloneCommand{
"inspect-database": databaseCommand,
"restore-database": databaseCommand,
"version": versionCommand,
"--version": versionCommand,
"-version": versionCommand,
"data-path": dataPathCommand,
}
func runStandaloneCommand(args []string) (bool, error) {
if len(args) == 0 {
return false, nil
}
command := standaloneCommands[args[0]]
if command == nil {
return false, nil
}
return true, command(args)
}
func versionCommand(args []string) error {
info := buildinfo.Current()
if len(args) == 2 && args[1] == "--json" {
return json.NewEncoder(os.Stdout).Encode(info)
}
if len(args) != 1 {
return fmt.Errorf("usage: linkport version [--json]")
}
fmt.Println(info.String())
return nil
}
func dataPathCommand(args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: linkport data-path")
}
path, err := desktop.ResolveDataPath()
if err != nil {
return err
}
fmt.Println(path)
return nil
}
// Maintenance commands share the prepared data lock and portable store lifetime.
type maintenanceCommand func(*store.Store, string) error
var maintenanceCommands = map[string]maintenanceCommand{
"export-ca": exportCA,
"migrate-key": completeMigration,
"migrate-data": completeMigration,
"reset-password": resetPassword,
}
func exportCA(s *store.Store, path string) error {
pair, err := s.LocalTLS(filepath.Dir(path))
if err != nil {
return err
}
return pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate[0]})
}
func completeMigration(s *store.Store, path string) error {
if _, err := s.LocalTLS(filepath.Dir(path)); err != nil {
return err
}
fmt.Println("Single-file migration complete:", path)
return nil
}
func resetPassword(s *store.Store, _ string) error {
fmt.Print("New administrator password: ")
a, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
fmt.Print("Confirm password: ")
b, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
if string(a) != string(b) {
return fmt.Errorf("passwords do not match")
}
return s.ResetPassword(string(a))
}
@@ -11,8 +11,8 @@ import (
func TestDataPathWithoutInitialization(testContext *testing.T) {
root := testContext.TempDir()
testContext.Chdir(root)
testContext.Setenv("SERVICEPORT_DATA", filepath.Join("uninitialized", "serviceport.sqlite"))
testContext.Setenv("SERVICEPORT_MASTER_KEY", "invalid")
testContext.Setenv("LINKPORT_DATA", filepath.Join("uninitialized", "linkport.sqlite"))
testContext.Setenv("LINKPORT_MASTER_KEY", "invalid")
previousArgs, previousStdout := os.Args, os.Stdout
reader, writer, err := os.Pipe()
if err != nil {
@@ -21,7 +21,7 @@ func TestDataPathWithoutInitialization(testContext *testing.T) {
defer reader.Close()
defer writer.Close()
testContext.Cleanup(func() { os.Args, os.Stdout = previousArgs, previousStdout })
os.Args, os.Stdout = []string{"serviceport", "data-path"}, writer
os.Args, os.Stdout = []string{"linkport", "data-path"}, writer
if err := run(); err != nil {
testContext.Fatal(err)
}
@@ -30,7 +30,7 @@ func TestDataPathWithoutInitialization(testContext *testing.T) {
if err != nil {
testContext.Fatal(err)
}
expected := filepath.Join(root, "uninitialized", "serviceport.sqlite")
expected := filepath.Join(root, "uninitialized", "linkport.sqlite")
if strings.TrimSpace(string(output)) != expected {
testContext.Fatalf("data-path returned %q, want %q", output, expected)
}
@@ -42,8 +42,8 @@ func TestDataPathWithoutInitialization(testContext *testing.T) {
func TestDataPathRejectsArguments(testContext *testing.T) {
previousArgs := os.Args
testContext.Cleanup(func() { os.Args = previousArgs })
os.Args = []string{"serviceport", "data-path", "unexpected"}
if err := run(); err == nil || !strings.Contains(err.Error(), "usage: serviceport data-path") {
os.Args = []string{"linkport", "data-path", "unexpected"}
if err := run(); err == nil || !strings.Contains(err.Error(), "usage: linkport data-path") {
testContext.Fatalf("unexpected command validation: %v", err)
}
}
@@ -5,9 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
"linkport/internal/desktop"
"linkport/internal/store"
"os"
"serviceport/internal/desktop"
"serviceport/internal/store"
"time"
)
@@ -22,7 +22,7 @@ func databaseCommand(args []string) error {
return json.NewEncoder(os.Stdout).Encode(info)
}
if len(args) != 4 || args[0] != "restore-database" || args[2] != "--confirm-sha256" {
return errors.New("usage: serviceport inspect-database <snapshot.sqlite> | restore-database <snapshot.sqlite> --confirm-sha256 <sha256>")
return errors.New("usage: linkport inspect-database <snapshot.sqlite> | restore-database <snapshot.sqlite> --confirm-sha256 <sha256>")
}
// Acquire the same lock as a running service. Never restore an open service DB.
prepared, err := desktop.Prepare()
@@ -37,6 +37,6 @@ func databaseCommand(args []string) error {
if err != nil {
return err
}
fmt.Fprintln(os.Stdout, "Database restored. Restart serviceport; accounts, credentials, audit and TLS now come from the snapshot.")
fmt.Fprintln(os.Stdout, "Database restored. Restart linkport; accounts, credentials, audit and TLS now come from the snapshot.")
return nil
}
@@ -2,15 +2,15 @@ package main
import (
"context"
"linkport/internal/desktop"
"linkport/internal/store"
"path/filepath"
"serviceport/internal/desktop"
"serviceport/internal/store"
"testing"
)
func TestRestoreCommandRejectsRunningService(t *testing.T) {
path := filepath.Join(t.TempDir(), "data.sqlite")
t.Setenv("SERVICEPORT_DATA", path)
t.Setenv("LINKPORT_DATA", path)
prepared, err := desktop.Prepare()
if err != nil {
t.Fatal(err)
@@ -7,11 +7,11 @@ import (
"strings"
"golang.org/x/term"
"serviceport/internal/buildinfo"
"serviceport/internal/desktop"
"serviceport/internal/gateway"
"serviceport/internal/protocol"
"serviceport/internal/store"
"linkport/internal/buildinfo"
"linkport/internal/desktop"
"linkport/internal/gateway"
"linkport/internal/protocol"
"linkport/internal/store"
)
func main() {
@@ -36,7 +36,7 @@ func run() error {
if len(os.Args) > 1 {
maintenance = maintenanceCommands[os.Args[1]]
if len(os.Args) != 2 || maintenance == nil {
return fmt.Errorf("usage: serviceport [version|data-path|reset-password|migrate-data|migrate-key|export-ca]")
return fmt.Errorf("usage: linkport [version|data-path|reset-password|migrate-data|migrate-key|export-ca]")
}
}
prepared, err := desktop.Prepare()
@@ -68,9 +68,9 @@ func run() error {
if maintenance != nil {
return maintenance(s, path)
}
authEnabled := strings.EqualFold(os.Getenv("SERVICEPORT_AUTH_ENABLED"), "true")
if (authEnabled && local == nil) || os.Getenv("SERVICEPORT_ADMIN_PASSWORD") != "" {
if err = s.InitAdmin(os.Getenv("SERVICEPORT_ADMIN_USER"), os.Getenv("SERVICEPORT_ADMIN_PASSWORD")); err != nil {
authEnabled := strings.EqualFold(os.Getenv("LINKPORT_AUTH_ENABLED"), "true")
if (authEnabled && local == nil) || os.Getenv("LINKPORT_ADMIN_PASSWORD") != "" {
if err = s.InitAdmin(os.Getenv("LINKPORT_ADMIN_USER"), os.Getenv("LINKPORT_ADMIN_PASSWORD")); err != nil {
return err
}
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"linkport/internal/api"
"linkport/internal/desktop"
"linkport/internal/gateway"
"linkport/internal/store"
"linkport/web"
)
// runManagement owns the listener, desktop setup, serving and graceful shutdown.
func runManagement(s *store.Store, m *gateway.Manager, local *desktop.Config, authEnabled bool) error {
server := api.New(s, m, strings.EqualFold(os.Getenv("LINKPORT_SECURE_COOKIE"), "true"), authEnabled)
srv := &http.Server{Addr: env("LINKPORT_HTTP_ADDR", "127.0.0.1:0"), Handler: server.Handler(web.Handler()), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 45 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16}
if local != nil || !authEnabled {
host, _, e := net.SplitHostPort(srv.Addr)
if e != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() {
return fmt.Errorf("desktop/development mode requires a loopback LINKPORT_HTTP_ADDR, such as 127.0.0.1:0; enable LINKPORT_AUTH_ENABLED for remote deployment")
}
}
listener, err := net.Listen("tcp", srv.Addr)
if err != nil {
return fmt.Errorf("cannot listen on %s (linkport may already be running): %w", srv.Addr, err)
}
defer listener.Close()
url := "http://" + listener.Addr().String()
if local != nil {
var setupPath string
if authEnabled {
srv.Handler, setupPath, err = desktop.Setup(s, srv.Handler, listener.Addr().String())
if err != nil {
return err
}
}
url += setupPath
fmt.Printf("数据目录:%s\n关闭服务请按 Ctrl+C。\n", local.Dir)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
done := make(chan error, 1)
go func() {
log.Printf("linkport management listening on %s", listener.Addr())
done <- srv.Serve(listener)
}()
fmt.Println("管理页面:" + url)
if local != nil {
if os.Getenv("LINKPORT_NO_BROWSER") != "1" {
if e := desktop.OpenBrowser(url); e != nil {
log.Printf("请手动打开管理页面:%v", e)
}
}
}
s.Prune()
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case e := <-done:
if e == http.ErrServerClosed {
return nil
}
return e
case <-ticker.C:
s.Prune()
case <-ctx.Done():
shutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return srv.Shutdown(shutdown)
}
}
}
@@ -1,18 +1,18 @@
package main
import (
"serviceport/internal/buildinfo"
"encoding/json"
"io"
"linkport/internal/buildinfo"
"os"
"path/filepath"
"testing"
)
func TestVersionWithoutDataInitialization(t *testing.T) {
data := filepath.Join(t.TempDir(), "must-not-exist", "serviceport.sqlite")
t.Setenv("SERVICEPORT_DATA", data)
t.Setenv("SERVICEPORT_MASTER_KEY", "invalid")
data := filepath.Join(t.TempDir(), "must-not-exist", "linkport.sqlite")
t.Setenv("LINKPORT_DATA", data)
t.Setenv("LINKPORT_MASTER_KEY", "invalid")
oldArgs, oldStdout := os.Args, os.Stdout
r, w, err := os.Pipe()
if err != nil {
@@ -21,7 +21,7 @@ func TestVersionWithoutDataInitialization(t *testing.T) {
defer r.Close()
defer w.Close()
t.Cleanup(func() { os.Args, os.Stdout = oldArgs, oldStdout })
os.Args, os.Stdout = []string{"serviceport", "--version", "--json"}, w
os.Args, os.Stdout = []string{"linkport", "--version", "--json"}, w
if err := run(); err != nil {
t.Fatal(err)
}
+14 -14
View File
@@ -1,6 +1,6 @@
# Disposable integration targets only. All passwords below are public TEST credentials.
# No production directories or data volumes are mounted.
name: serviceport-test
name: linkport-test
services:
cert-init:
build: ./fixtures
@@ -11,7 +11,7 @@ services:
image: mysql:5.7.44
ports: ["127.0.0.1:13357:3306"]
environment:
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_PASSWORD: linkport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -22,7 +22,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=linkport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -30,7 +30,7 @@ services:
image: mysql:8.0.43
ports: ["127.0.0.1:13380:3306"]
environment:
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_PASSWORD: linkport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -41,7 +41,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=linkport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -49,7 +49,7 @@ services:
image: mysql:8.4.6
ports: ["127.0.0.1:13384:3306"]
environment:
MYSQL_ROOT_PASSWORD: serviceport-test-password
MYSQL_ROOT_PASSWORD: linkport-test-password
MYSQL_ROOT_HOST: "%"
command: ["--ssl-ca=/certs/ca.pem", "--ssl-cert=/certs/server-cert.pem", "--ssl-key=/certs/server-key.pem", "--require-secure-transport=ON"]
volumes:
@@ -60,7 +60,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=serviceport-test-password mysqladmin ping -uroot --silent"]
test: ["CMD-SHELL", "MYSQL_PWD=linkport-test-password mysqladmin ping -uroot --silent"]
interval: 5s
timeout: 5s
retries: 60
@@ -69,7 +69,7 @@ services:
ports: ["127.0.0.1:27060:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
MONGO_INITDB_ROOT_PASSWORD: linkport-test-password
command: ["mongod", "--bind_ip_all", "--tlsMode", "requireTLS", "--tlsCertificateKeyFile", "/certs/mongo.pem", "--tlsCAFile", "/certs/ca.pem", "--tlsAllowConnectionsWithoutCertificates"]
volumes:
- fixture-certs:/certs:ro
@@ -80,7 +80,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "linkport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
@@ -89,7 +89,7 @@ services:
ports: ["127.0.0.1:27070:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
MONGO_INITDB_ROOT_PASSWORD: linkport-test-password
command: ["mongod", "--bind_ip_all", "--tlsMode", "requireTLS", "--tlsCertificateKeyFile", "/certs/mongo.pem", "--tlsCAFile", "/certs/ca.pem", "--tlsAllowConnectionsWithoutCertificates"]
volumes:
- fixture-certs:/certs:ro
@@ -100,7 +100,7 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "linkport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
@@ -109,7 +109,7 @@ services:
ports: ["127.0.0.1:27080:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: serviceport-test-password
MONGO_INITDB_ROOT_PASSWORD: linkport-test-password
entrypoint: ["bash", "/fixture-start.sh"]
volumes:
- fixture-certs:/certs:ro
@@ -121,14 +121,14 @@ services:
cert-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
test: ["CMD", "mongosh", "--quiet", "--host", "localhost", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "linkport-test-password", "--authenticationDatabase", "admin", "--eval", "db.adminCommand({ping:1})"]
interval: 5s
timeout: 5s
retries: 60
mongo80-init:
image: mongo:8.0.13
profiles: ["replica-init"]
entrypoint: ["mongosh", "--quiet", "--host", "mongo80", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "serviceport-test-password", "--authenticationDatabase", "admin", "/init-replica.js"]
entrypoint: ["mongosh", "--quiet", "--host", "mongo80", "--tls", "--tlsCAFile", "/certs/ca.pem", "-u", "root", "-p", "linkport-test-password", "--authenticationDatabase", "admin", "/init-replica.js"]
volumes:
- fixture-certs:/certs:ro
- ./mongo/init-replica.js:/init-replica.js:ro
+1 -1
View File
@@ -65,7 +65,7 @@ func certs(dir string) error {
if err != nil {
return err
}
ca := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "serviceport disposable test CA"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature}
ca := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "linkport disposable test CA"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature}
der, err := x509.CreateCertificate(rand.Reader, ca, ca, &key.PublicKey, key)
if err != nil {
return err
+3 -3
View File
@@ -9,12 +9,12 @@ defaults
timeout server 1h
# Replace these private addresses and the gateway port for your environment.
# TLS passes through to the serviceport gateway on each node.
# TLS passes through to the linkport gateway on each node.
frontend mysql_gateway
bind :3307
default_backend serviceport_mysql
default_backend linkport_mysql
backend serviceport_mysql
backend linkport_mysql
balance leastconn
option httpchk GET /readyz
http-check expect status 200
+6 -6
View File
@@ -1,8 +1,8 @@
SERVICEPORT_AUTH_ENABLED=true
SERVICEPORT_HTTP_ADDR=10.20.0.11:8080
SERVICEPORT_DATA=/var/lib/serviceport-a/serviceport.sqlite
SERVICEPORT_MASTER_KEY=REPLACE_WITH_EXTERNALLY_MANAGED_BASE64_KEY
SERVICEPORT_TLS_CERT=/etc/serviceport/gateway-cert.pem
SERVICEPORT_TLS_KEY=/etc/serviceport/gateway-key.pem
LINKPORT_AUTH_ENABLED=true
LINKPORT_HTTP_ADDR=10.20.0.11:8080
LINKPORT_DATA=/var/lib/linkport-a/linkport.sqlite
LINKPORT_MASTER_KEY=REPLACE_WITH_EXTERNALLY_MANAGED_BASE64_KEY
LINKPORT_TLS_CERT=/etc/linkport/gateway-cert.pem
LINKPORT_TLS_KEY=/etc/linkport/gateway-key.pem
# Supply first-start administrator credentials through your secret manager.
# When cloning a provisioned snapshot, the administrator is already configured.
+6 -6
View File
@@ -1,8 +1,8 @@
SERVICEPORT_AUTH_ENABLED=true
SERVICEPORT_HTTP_ADDR=10.20.0.12:8080
SERVICEPORT_DATA=/var/lib/serviceport-b/serviceport.sqlite
SERVICEPORT_MASTER_KEY=REPLACE_WITH_EXTERNALLY_MANAGED_BASE64_KEY
SERVICEPORT_TLS_CERT=/etc/serviceport/gateway-cert.pem
SERVICEPORT_TLS_KEY=/etc/serviceport/gateway-key.pem
LINKPORT_AUTH_ENABLED=true
LINKPORT_HTTP_ADDR=10.20.0.12:8080
LINKPORT_DATA=/var/lib/linkport-b/linkport.sqlite
LINKPORT_MASTER_KEY=REPLACE_WITH_EXTERNALLY_MANAGED_BASE64_KEY
LINKPORT_TLS_CERT=/etc/linkport/gateway-cert.pem
LINKPORT_TLS_KEY=/etc/linkport/gateway-key.pem
# This is a separate machine with its own database and audit directory.
# An encrypted snapshot clone requires the source encryption key.
+1 -1
View File
@@ -2,7 +2,7 @@ try {
rs.status();
} catch (e) {
if (e.code !== 94) throw e;
const result = rs.initiate({_id: "serviceport-test", members: [{_id: 0, host: "mongo80:27017"}]});
const result = rs.initiate({_id: "linkport-test", members: [{_id: 0, host: "mongo80:27017"}]});
if (result.ok !== 1) throw new Error(JSON.stringify(result));
}
let primary = false;
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
cp /certs/replica-key /tmp/serviceport-replica-key
chown mongodb:mongodb /tmp/serviceport-replica-key
chmod 400 /tmp/serviceport-replica-key
exec /usr/local/bin/docker-entrypoint.sh mongod --bind_ip_all --replSet serviceport-test --keyFile /tmp/serviceport-replica-key --tlsMode requireTLS --tlsCertificateKeyFile /certs/mongo.pem --tlsCAFile /certs/ca.pem --tlsAllowConnectionsWithoutCertificates
cp /certs/replica-key /tmp/linkport-replica-key
chown mongodb:mongodb /tmp/linkport-replica-key
chmod 400 /tmp/linkport-replica-key
exec /usr/local/bin/docker-entrypoint.sh mongod --bind_ip_all --replSet linkport-test --keyFile /tmp/linkport-replica-key --tlsMode requireTLS --tlsCertificateKeyFile /certs/mongo.pem --tlsCAFile /certs/ca.pem --tlsAllowConnectionsWithoutCertificates
+2 -2
View File
@@ -1,6 +1,6 @@
FROM debian:12.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssh-server ca-certificates && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /run/sshd && useradd -m -s /bin/bash serviceport \
&& echo 'serviceport:serviceport-test-password' | chpasswd
&& mkdir -p /run/sshd && useradd -m -s /bin/bash linkport \
&& echo 'linkport:linkport-test-password' | chpasswd
COPY sshd_config /etc/ssh/sshd_config
CMD ["sh", "-c", "ssh-keygen -A && exec /usr/sbin/sshd -D -e"]
+1 -1
View File
@@ -5,7 +5,7 @@ PasswordAuthentication yes
PubkeyAuthentication yes
PermitRootLogin no
UsePAM no
AllowUsers serviceport
AllowUsers linkport
Subsystem sftp internal-sftp
PrintMotd no
LogLevel VERBOSE
+118
View File
@@ -0,0 +1,118 @@
# 全模块设计模式与扩展边界
2026-09-27 的重构覆盖启动、领域模型、API、运行网关、协议、出口、集群、存储、后台任务和前端,并逐项审查工具与基础设施边界。采用 Go 函数、消费者侧接口和 Vue composable 表达设计模式,没有增加依赖或通用框架。下表区分实际拆分与已经具备清晰边界而保留的模块;不要求每个文件都发生改动。
| 模块 | 模式 | 解决的问题 | 实现入口 |
| --- | --- | --- | --- |
| 网络出口 | 策略 + 工厂 | 选择出口与 SOCKS5 字节握手混在同一函数,新增出口容易碰到其他协议 | `internal/outbound/strategy.go` 的 `newDialStrategy` |
| 公共拨号策略 | 装饰器 | 各出口都需要按调用校验配置、设置建连期限并释放 context | `internal/outbound/dial.go` 的 `withConnectionOptions` |
| 网关操作 | 命令 | `server.go` 同时处理认证、分发和启停/测试/主机指纹业务 | `internal/api/gateway_actions.go` 的 `gatewayActions` |
| 前端资源目录 | 门面 + 依赖注入 | `App.vue` 在刷新、保存、401 和退出中分别维护缓存及竞态控制 | `web/src/composables/useResourceCatalog.ts` |
| 启动与命令 | 组合根 + 命令 | 数据初始化、维护命令、TLS、运行恢复和 HTTP 生命周期混在 `run` 中 | `cmd/linkport/{main,commands,bootstrap,management}.go` |
| 协议注册 | 工厂 | 启动入口直接依赖全部协议实现 | `internal/protocol/handlers.go` 的 `NewHandlers` |
| API 请求 | 管线 + 适配器 | 身份验证、会话绑定和锁内路由与领域操作耦合 | `internal/api/{authorization,routes}.go` |
| 网关会话 | 会话对象 + 装饰器 | 接受循环同时管理审计、认证、流量和异常释放 | `internal/gateway/{session,instance}.go` |
| 集群发现 | 策略 + 遍历模板 | 节点调度直接绑定各协议探测函数 | `internal/cluster/{cluster,strategies}.go` |
| 告警 | 规则评估 + 通知适配器 | 生命周期、告警规则和 HTTP 投递混合 | `internal/alerts/{engine,conditions,notifier,repository}.go` |
| 同步与备份 | 端口与适配器 | 调度/租约与 WebDAV 传输、冲突流程混合 | `internal/syncjob/{engine,transfer,repository}.go`、`internal/backup/object_store.go` |
| 备份编码 | 编解码边界 | 数据快照和加密信封难以独立维护 | `internal/store/backup_codec.go` |
| 连接编辑 | 草稿工厂 + 载荷构建器 | 默认值、旧账号适配与提交白名单集中在 App | `web/src/domain/connectionDraft.ts` |
| 凭据、分页与画板 | 会话状态 + 加载模板 + 浏览器适配器 | 请求失效、计时清理、游标和 DOM 生命周期散落在视图 | `useSecretReveal`、`useCursorPage`、`useCanvasFullscreen`、`useCanvasMeasurements` |
## 全模块覆盖清单
| 范围 | 处理结果与保留边界 |
| --- | --- |
| `cmd/linkport` | 拆分组合根、独立/维护命令、证书/运行恢复和管理服务生命周期;独立命令仍先于数据初始化 |
| `internal/model` | `proxy.go` 保留配置快照;网关账号结构及应用逻辑归入 `gateway_account.go`;`runtime.go` 集中 Handler/DialFunc/TestResult;其余配置、校验、脱敏、凭据规则按原领域划分保留 |
| `internal/api` | 认证适配、路由及锁边界和网关命令拆分;目标、出口、资产、配置、审计、监控、令牌和工具端点继续归属各领域文件 |
| `internal/gateway` | 会话审计/认证/释放与实例停止独立;健康检查、持久目标、连接列表、运行中更新和目标切换保留专门模块 |
| `internal/protocol` | 工厂集中全部 11 个协议名;9 个处理目录继续实现 Handler,可准备会话的协议继续实现 SessionPreparer;wire/auth/SQL policy 不改写 |
| `protocol/transport`、`protocol/amqpprobe` | 保留公共连接装饰器、账号检查与 AMQP 探测适配器,避免重复包装 |
| `internal/outbound` | 已完成策略/工厂/选项装饰器;SSH、SOCKS5、Kubernetes、sing-box、持久传输及诊断继续按传输划分 |
| `internal/cluster` | 固定协议探测策略与遍历模板分离;Redis 与数据库探测实现保留 |
| `internal/store` | 备份编解码独立;资源仓储、历史、迁移、审计分片与快照一致性维持原领域边界,没有泛型仓储或 SQL/schema 变更 |
| `internal/alerts` | Repository 接口、规则与通知分离,Engine 只编排生命周期、持久化和重试 |
| `internal/syncjob` | Repository 接口、调度/租约与传输流程分离,配置修订和完整替换仍由 store 保证 |
| `internal/backup` | 增加条件对象存储接口与 WebDAV 适配器;已有 S3 签名、WebDAV HTTP 和加密同步格式保持独立 |
| `web/src` | App 协调页面;资源目录、连接草稿、凭据会话、游标和画板浏览器逻辑抽离;既有 API/CSRF、页签、作用域、未保存守卫、配置历史、资产、监控、同步及领域函数继续复用 |
| `internal/desktop` | 保留数据路径策略、平台构建标签、进程锁、设置页面与打开浏览器的独立适配文件 |
| `internal/security` | 保留 Vault 加密与平台私有文件权限适配,未更改密码学或权限策略 |
| `internal/localcert`、`cmd/devcert` | 保留单用途证书生成模块与薄 CLI 入口 |
| `internal/cryptotool`、`internal/navicat` | 保留独立解密算法/兼容格式边界,API 仍只做输入与响应适配 |
| `internal/buildinfo`、根 `version.go` | 保留构建元信息与嵌入默认版本的单向依赖 |
| `internal/testutil`、`deploy/fixtures` | 保留测试出口适配器与隔离 fixture;不进入生产业务流程 |
| `web/embed.go`、`scripts`、部署配置 | 保留静态资源适配器、构建锁、工具链、路径/版本、缓存、打包和验收脚本的独立边界;不修改发布和部署行为 |
已移除的 SQLite 文件浏览模块不重新引入;残留空目录不视为待重写模块。第三方依赖、生成文件和真实 `data` 不属于本轮重构对象。
## 启动、模型与请求管线
`run` 是组合根:选择命令、准备带进程锁的数据目录、打开 Store、配置管理员、创建 TLS 和协议工厂、恢复 Manager,最后进入管理服务。`commands.go` 区分无需打开应用数据的命令与需要已打开 Store 的维护命令;保留版本输出、参数错误、迁移提示、密码输入和退出顺序。`management.go` 独占监听器、首次设置、浏览器启动、小时清理和 30 秒关闭期限。
模型配置不依赖运行模块。`runtime.go` 集中协议协作接口,`gateway_account.go` 集中账号的有效性、目标映射、应用与校验;只是文件职责移动,公开名称、JSON 标签、秘密字段和只读上限不变。
API 顺序保持为来源检查、登录特例、Bearer/本地开发/cookie 身份验证、作用域与 CSRF、路由分发。`requestIdentity` 按请求传递会话和确认绑定 ID;Token 响应继续经过脱敏 writer。`dispatchMutation` 保留原 `ops` 锁范围,列表与 WebDAV 等原先锁外流程不移动到锁内。新增端点应在领域文件实现并显式选择路由和锁边界。
## 运行会话、协议与集群
`connectionSession` 包装 Handler 执行,集中审计上下文、账号认证计数、流量统计和完成事件。保留接受时快照、Manager.mu/auditMu 顺序、panic 恢复以及释放连接名额、关闭 socket、完成 WaitGroup、取消 context 的顺序。`instance.stopLocked` 只负责停止标记/取消/关闭监听;`waitStopped` 必须在 Manager 锁外等待和关闭持久目标。
`protocol.NewHandlers` 每次返回独立 map,HTTP/HTTPS、SSH/SFTP 仍分别调用构造器。新增业务协议先实现 `model.Handler`,再加入工厂及协议模型/界面,不向 main.go 增加协议依赖。
集群遍历注入探测函数,生产入口选用私有策略目录。节点顺序、拓扑筛选、Sentinel 到数据节点的账号切换、20 秒总体期限、3 秒单次探测和 64 节点限制不变。
## 告警、同步和持久化
告警的 Repository 是消费者需要的存储端口,具体的条件更新和去重仍由 Store 完成。规则评估器按既有顺序产生活跃条件;Webhook 适配器保持 HMAC、5 秒期限、禁重定向及最多三次投递。手动 Check 不发送通知。
同步 Engine 保留 TryLock、外部操作 guard、持久 lease、60 秒期限及完成记录;`transfer.go` 负责基线、冲突和条件对象读写。WebDAV ObjectStore 直接复用原传输,强 ETag、路径限制和取消语义不变;存储端继续保证快照一致性、配置 revision 和完整替换原子性。
`backup_codec.go` 负责现有 PBKDF2/AES-GCM 信封。口令检查、密钥派生、数据库快照读取的顺序与密文格式保留;同步加密也复用此编解码边界。依据 `safe-sql-updates` 与 `query-before-update` 检查并保留已有 SQL、锁和事务边界,本轮没有引入更新语句或迁移。
## 出口策略与装饰器
```mermaid
flowchart LR
P[Proxy 配置] --> F[newDialStrategy]
F --> S[直连 / SOCKS5 / SSH / Kubernetes / sing-box]
S --> D[withConnectionOptions]
D --> C[统一 DialFunc]
```
`Dialer` 仍是协议处理器、健康检查和网关调用的公共入口。工厂只选择函数和默认超时,不创建连接或外部进程;SOCKS5 握手实现独立在 `socks5.go`。装饰器每次调用校验 `ConnectionOptions`,然后创建有期限的子 context。各策略继续在调用时读取 KeepAlive,避免指针选项变化后使用缓存值。
默认期限仍为 Kubernetes 30 秒、其他出口 5 秒;显式配置优先,调用者更短的期限不会被延长。成功连接的生命周期仍由原有传输实现负责。保留 SOCKS5 认证/目标协商的错误包装与诊断阶段、域名透传和失败清理;未知出口模式仍直接拒绝,失败不回退直连。
增加出口时,在对应传输文件实现拨号函数,再在工厂中增加选择分支,并验证域名传递、期限、错误诊断和取消清理。无需修改协议 Handler。
## 网关命令分发
`server.go` 组织公共请求管线,`authorization.go` 适配身份与 CSRF,`routes.go` 保留路由顺序及 `ops` 串行锁。网关列表交给 `proxies.go`,资源路径及方法校验交给 `proxyRoutes`;只有通过校验的 POST 操作才从私有命令表选择处理函数。
命令覆盖 `target`、`start`、`stop`、`test`、`probe`、`confirm-host-key`、`reveal-password`。`gatewayActionRequest` 按请求传递配置快照、会话和会话 ID,不把凭据或状态存入全局命令表。
启停仍使用 `Manager.StartFrom` / `StopAndPersist` 的原有保存回调,保留保存失败与生命周期冲突的 HTTP 状态区别。SSH 指纹确认继续校验会话、目标地址、过期时间与指纹。路由仍先查询网关,再检查操作及方法,保持既有 404/405 优先级。
新增网关动作应放入对应领域函数,在命令表声明入口,并检查 Token 权限与请求校验。命令表不负责授权。
## 前端资源门面
`useResourceCatalog` 注入 `request` 和 `report`,持有网关、目标、出口及文件资产列表。对外提供 `refresh`、`acceptSavedResponse` 和 `clear`,隐藏刷新代次、整体提交和加载状态维护。
- 五个读取接口全部成功且代次仍有效时,一次性发布四类列表和范围元数据;部分失败保留先前缓存。
- 创建/保存响应按资源 ID 更新对应列表,并使此前在途读取失效,旧读取不能覆盖已保存版本。
- 退出或 401 调用 `clear`,清空资源并使在途读取失效。
- 页面协调回调拿到 `isCurrent`,概览异步数据仍同时检查范围与请求代次。
`App.vue` 继续负责作用域、页面、标签、会话、审计加载及导航。门面不依赖 Element Plus、路由或 localStorage,测试可以直接注入可控 Promise 验证竞争顺序。`useApi` 的 CSRF 限定重试机制未改动。
连接草稿工厂接收范围和 ID 生成器;编辑适配器深复制配置,兼容旧账号;保存构建器维持目标/网关白名单、空秘密字段省略、SSH 和无账号规则。凭据会话通过请求代次避免已关闭页面被旧响应填回,并保持 30 秒清理。事件与命令复用游标加载模板,旧响应/失败不能覆盖新筛选。画板全屏与 DOM 测量分开处理事件、ResizeObserver 和卸载清理;绑定规则与布局不变。
本轮按 `multilingual-ui` 审查前端;该技能的 Shopee 专用 i18n 路径不适用于本项目,且没有新增或修改产品文案。现有持久连接 SSR 测试中“必须重启网关”的旧断言已对齐此前运行中编辑功能的“修正配置并保存后重试”指引,未更改界面或运行行为。
## 兼容与验证
这是结构重构,不变更产品文案、HTTP 路径/载荷/状态约定、配置字段、数据库结构、协议或已有功能。没有数据迁移,也没有操作真实 `data`。旧配置及本轮之前的工作区改动均保留。
验证使用隔离数据库、本机协议 fixture 和注入请求,不作为真实远端服务器验收。新增回归检查出口期限及无直连回退、命令路由/会话绑定、资源刷新的并发次序;全量测试和构建的实际结果见 [测试记录](testing.md)。
+4 -2
View File
@@ -18,8 +18,10 @@ authority if configuration changes after the preview.
Asset edits require stopping dependent gateways; restarted/new connections use
the new configuration. They do not replace material inside existing connections.
By contrast, online gateway target switching preserves existing connections and
changes only subsequent connections. The UI explicitly distinguishes these cases.
By contrast, a successful online gateway target switch keeps the listener running
but disconnects every existing client of that gateway. Reconnecting clients use
the newly published route. A failed switch preserves both the route and clients.
The UI explicitly distinguishes these cases.
Account mappings are displayed as gateway username → target / target account
username → **target-level** authSource. Independent account targets do not inherit
+3 -3
View File
@@ -1,8 +1,8 @@
# 项目标识与启动展示
serviceport 的标识由蓝色圆角底、白色字母 P 和两端青色连接点组成:P 表示 port / portal,左右接入与出口表示服务通过网关连接。源图为 `web/public/serviceport.svg`,保持简洁轮廓以适应浏览器标签和侧栏的小尺寸。图标使用固定品牌配色,在浅色和深色主题保持一致。
linkport 的标识由蓝色圆角底、白色字母 P 和两端青色连接点组成:P 表示 port / portal,左右接入与出口表示服务通过网关连接。源图为 `web/public/linkport.svg`,保持简洁轮廓以适应浏览器标签和侧栏的小尺寸。图标使用固定品牌配色,在浅色和深色主题保持一致。
登录页、工作区左上角和浏览器 favicon 共用这份 SVG,随 Vite 构建复制到 `web/dist` 并嵌入 Go 单文件程序,无外部字体、图片服务或额外部署文件。页面图标旁已有 serviceport 名称,因此内联图片使用空 alt,避免辅助阅读器重复朗读;README 图标保留替代文字。此次不修改 Windows EXE 的原生图标资源。
登录页、工作区左上角和浏览器 favicon 共用这份 SVG,随 Vite 构建复制到 `web/dist` 并嵌入 Go 单文件程序,无外部字体、图片服务或额外部署文件。页面图标旁已有 linkport 名称,因此内联图片使用空 alt,避免辅助阅读器重复朗读;README 图标保留替代文字。此次不修改 Windows EXE 的原生图标资源。
正常服务启动时在日志流打印一次对应 P 形端口的 ASCII 标识,随后打印现有版本、提交、构建时间和监听信息。终端标识使用纯 ASCII,不依赖 ANSI 色彩或特殊字体,支持 Windows、Linux、macOS 和重定向日志。标识出现在初始化阶段,不代表服务已成功监听;以随后监听日志和健康检查为准。
@@ -12,4 +12,4 @@ serviceport 的标识由蓝色圆角底、白色字母 P 和两端青色连接
Node 22.22.3 前端生产构建通过(仅既有 chunk 大小提示),Go 1.25.0 全量测试及 `go vet ./...` 通过。命令边界测试使用临时无效数据路径在初始化阶段结束,验证正常启动标识出现一次、维护命令没有标识且 stdout 不被污染;原 JSON 版本及路径查询测试也通过。
构建 `bin/dev-build/branding-20260925/serviceport.exe` 后完成隔离启动、嵌入页面、登录冒烟;全部 124 项 Playwright 通过,包含 SVG HTTP 类型/加载、favicon 引用、主题和手机布局检查,日志实际出现 ASCII 标识及随后版本/监听信息。使用临时数据库及本机 fixture,未验证真实上游服务,也未改动真实 data。图标 PNG 及界面预览保存在 `bin/previews/branding-20260925/`,源文件仍为可缩放 SVG。
构建 `bin/dev-build/branding-20260925/linkport.exe` 后完成隔离启动、嵌入页面、登录冒烟;全部 124 项 Playwright 通过,包含 SVG HTTP 类型/加载、favicon 引用、主题和手机布局检查,日志实际出现 ASCII 标识及随后版本/监听信息。使用临时数据库及本机 fixture,未验证真实上游服务,也未改动真实 data。图标 PNG 及界面预览保存在 `bin/previews/branding-20260925/`,源文件仍为可缩放 SVG。
+8 -8
View File
@@ -6,25 +6,25 @@
Drone 将 linux/amd64、linux/arm64、windows/amd64、windows/arm64、darwin/amd64、darwin/arm64 的二进制写入 `bin/build-cache/ci-release/<系统>-<架构>/`,六个独立压缩包和清单写入 `bin/releases/ci/`;六包 CID 和四文件索引目录 CID 记录在 `bin/releases/ci.ipfs.json`。平台和 Go 编译并发默认均为 1。已有目录不会被自动删除或覆盖。发布分别请求保留包及索引;索引文本中的 CID 不是 DAG 链接,仅 pin 索引不会递归保留包。迁移备份应保留这七个 CID,不能依赖临时工作区;详见 [IPFS 发布说明](ipfs.md)。
`bin/backups` 包含用户数据,不是可随意删除的编译缓存。旧 `artifacts/` 中的测试产物和备份仍保留,不在合并时搬移或清理。验证可使用 `artifacts/` 下的独立程序、临时端口和临时数据库,不覆盖运行中的 `bin/serviceport.exe`。
`bin/backups` 包含用户数据,不是可随意删除的编译缓存。旧 `artifacts/` 中的测试产物和备份仍保留,不在合并时搬移或清理。验证可使用 `artifacts/` 下的独立程序、临时端口和临时数据库,不覆盖运行中的 `bin/linkport.exe`。
## 相对路径与跨机器使用
脚本不保存开发者盘符、用户目录或固定系统安装位置。项目根目录从脚本自身位置推导,与从哪个目录调用无关;复制整个项目到另一台电脑或带空格的其他目录后,不需要改脚本中的路径。
- CMD 入口通过 `%~dp0` 找脚本;PowerShell 通过 `$PSScriptRoot`;Shell 先切换到自身所在项目根目录;Node 通过 `import.meta.url` 定位项目。
- 默认输入、输出、缓存和测试二进制都相对项目根目录。`build.ps1 -OutputPath`、验证脚本的文件参数及 `SERVICEPORT_E2E_BINARY` 的相对值也按项目根目录解释,不按调用者的当前目录。
- 用户主动传入的外部文件路径仍兼容;脚本不会把它们作为跨机器默认值。已有 `SERVICEPORT_DATA` 自定义数据库路径和 AppData 兼容逻辑不改动。
- 默认输入、输出、缓存和测试二进制都相对项目根目录。`build.ps1 -OutputPath`、验证脚本的文件参数及 `LINKPORT_E2E_BINARY` 的相对值也按项目根目录解释,不按调用者的当前目录。
- 用户主动传入的外部文件路径仍兼容;脚本不会把它们作为跨机器默认值。已有 `LINKPORT_DATA` 自定义数据库路径和 AppData 兼容逻辑不改动。
- 运行时仍会把路径规范化用于启动进程、校验删除范围和防止递归备份。这些是动态计算的路径,不是写死在脚本中的绝对路径。Docker fixture 的容器内挂载点和入口路径属于容器契约,不是宿主机器路径,不随本次修改改变。
## Windows 构建工具查找
源码中的 `start-dev.cmd` 每次先准备当前源码对应的程序,因此仍需 Go、Node.js 和 npm;缓存不会免除开发工具依赖。只运行软件时,使用预编译发行版的 `serviceport.exe`,无需安装这些工具。
源码中的 `start-dev.cmd` 每次先准备当前源码对应的程序,因此仍需 Go、Node.js 和 npm;缓存不会免除开发工具依赖。只运行软件时,使用预编译发行版的 `linkport.exe`,无需安装这些工具。
可以把对应操作系统与架构的完整工具链放进项目:
```text
serviceport/
linkport/
tools/
go/bin/go.exe
node/node.exe
@@ -38,7 +38,7 @@ serviceport/
Windows 查找顺序为:
1. 可选 `SERVICEPORT_GO` / `SERVICEPORT_NODE`,建议使用项目相对路径;显式路径无效时直接报错,不偷偷降级。
1. 可选 `LINKPORT_GO` / `LINKPORT_NODE`,建议使用项目相对路径;显式路径无效时直接报错,不偷偷降级。
2. 项目内 `tools/go/bin/go.exe`、`tools/node/node.exe`,优先于机器 PATH。
3. 当前机器 PATH 中的对应应用程序,不再扫描 Program Files、AppData、用户目录或 SDK 安装位置。
@@ -47,8 +47,8 @@ Node 便携目录必须包含配套 npm。选定的 Node/npm 目录只在构建
默认工具目录齐全时直接双击 `start-dev.cmd`,无需设置路径。自定义相对位置示例:
```powershell
$env:SERVICEPORT_GO = 'tools/go/bin/go.exe'
$env:SERVICEPORT_NODE = 'tools/node/node.exe'
$env:LINKPORT_GO = 'tools/go/bin/go.exe'
$env:LINKPORT_NODE = 'tools/node/node.exe'
.\start-dev.cmd -BuildOnly
```
+294 -45
View File
@@ -1,5 +1,13 @@
# 变更原因与设计记录
## Redis 与 MongoDB 网关允许无客户端账号(2026-09-28)
原因与结果:Redis 与 MongoDB 上游都可关闭认证,但新建网关原先仍强制创建客户端账号和密码,无法直接提供无认证入口。现在这两类网关都可保存空的 `gatewayAccounts`,客户端无需执行本地认证即可连接;若配置任意网关账号,则恢复原有协议认证。上游账号是否需要认证仍由目标服务的账号列表独立决定。
范围与兼容:放开 Redis、MongoDB 网关的空账号列表、模型校验、创建草稿及对应协议认证分支;MySQL 等其他网关及已有带账号网关行为不变。沿用现有加密配置,无数据库迁移;旧版本不识别该无客户端认证配置,回退前需停服并核对备份。
实际验证:Go 1.25.0 模型、API 和 Redis 协议定向测试,以及前端 Redis 草稿逻辑测试通过;协议覆盖使用本机 RESP/net.Pipe fixture,不是 live-server Redis 验证。未修改真实 `data`,未部署或重启服务。
## 资产入口改为网络代理(2026-09-27)
原因:用户希望管理网络连通方式,Kubeconfig 应与 SSH、SOCKS5 一样独立保存,再关联目标服务和网关;文件分类和内联配置使同一用途出现不同操作路径。
@@ -46,15 +54,15 @@
## sing-box 单 outbound 连接资产(2026-09-27)
**原因**:资产管理原有 direct、SOCKS5、SSH 和 Kubernetes 路径无法表达 VLESS、VMess、Trojan、Hysteria 2、TUIC、Shadowsocks 等 sing-box outbound。直接逐个实现协议会扩大长期维护面;本次官方资料核对未找到 Go library API 的跨版本稳定性承诺,嵌入 module 还会与本仓库固定 Go 1.25.0 发生现实工具链冲突(官方 v1.14.2 `go.mod` 要求 1.25.5)。连接用户自行运行的常驻代理会把端口、认证和生命周期交给部署者,因此选择由 serviceport 管理外部 binary。
**原因**:资产管理原有 direct、SOCKS5、SSH 和 Kubernetes 路径无法表达 VLESS、VMess、Trojan、Hysteria 2、TUIC、Shadowsocks 等 sing-box outbound。直接逐个实现协议会扩大长期维护面;本次官方资料核对未找到 Go library API 的跨版本稳定性承诺,嵌入 module 还会与本仓库固定 Go 1.25.0 发生现实工具链冲突(官方 v1.14.2 `go.mod` 要求 1.25.5)。连接用户自行运行的常驻代理会把端口、认证和生命周期交给部署者,因此选择由 linkport 管理外部 binary。
**结果行为**:连接资产新增 `sing-box` mode,只接收最多 64 KiB 的单个 outbound JSON object。serviceport 删除用户 tag、拒绝完整配置和宿主文件/进程/路由能力,生成只监听 `127.0.0.1` 且带随机认证的 SOCKS5 入站,并把最终路由固定到该 outbound。首次目标拨号执行 `sing-box run -c`,相同规范化配置与 binary 路径共享进程,最后一条连接关闭后清理。失败不回退直连。资产“检查配置”执行 10 秒上限的 `sing-box check -c`,只验证当前 binary 接受配置,不启动 `run`、不拨代理或目标;失败响应只含安全错误常量和耗时,不附加 `diagnostics` 或 `sing_box_start` 阶段。
**结果行为**:连接资产新增 `sing-box` mode,接收最多 64 KiB 的单个 outbound JSON object,也支持粘贴或导入完整配置后自动提取远程 outbound。linkport 删除用户 tag、拒绝未能提取远程 outbound 的完整配置和宿主文件/进程/路由能力,生成只监听 `127.0.0.1` 且带随机认证的 SOCKS5 入站,并把最终路由固定到该 outbound。首次目标拨号执行 `sing-box run -c`,相同规范化配置与 binary 路径共享进程,最后一条连接关闭后清理。失败不回退直连。资产“检查配置”执行 10 秒上限的 `sing-box check -c`,只验证当前 binary 接受配置,不启动 `run`、不拨代理或目标;失败响应只含安全错误常量和耗时,不附加 `diagnostics` 或 `sing_box_start` 阶段。
**安全与秘密**:binary 通过 `SERVICEPORT_SING_BOX` 定位,未设置时从 `PATH` 查找,发布包不捆绑。整个 `singBoxOutbound` 视为秘密,加密持久化且不经 API 回显;历史差异和导入预览只显示 `secretChanged`,脱敏 bundle 清空正文。运行配置关闭日志,命令 stdout/stderr 丢弃,临时目录/文件请求 `0700`/`0600` 并在结束后删除。递归拒绝 `*_path`、detour、plugin/外部进程、自定义 resolver、network namespace、bind/routing mark 等能力,因为子进程继承 serviceport OS 身份。
**安全与秘密**:binary 通过 `LINKPORT_SING_BOX` 定位,未设置时从 `PATH` 查找,发布包不捆绑。整个 `singBoxOutbound` 视为秘密,加密持久化且不经 API 回显;历史差异和导入预览只显示 `secretChanged`,脱敏 bundle 清空正文。运行配置关闭日志,命令 stdout/stderr 丢弃,临时目录/文件请求 `0700`/`0600` 并在结束后删除。递归拒绝 `*_path`、detour、plugin/外部进程、自定义 resolver、network namespace、bind/routing mark 等能力,因为子进程继承 linkport OS 身份。
**影响范围**:普通目标可引用新的共享连接资产,路径和诊断增加 `sing_box_start/connect/negotiation/authentication`,随后沿用 `target_connect` 和目标协议阶段。当前只承载 TCP,不启用 mixed/TUN/UDP/系统代理;独立 TCP 网关仍只使用 SOCKS5 或 Kubernetes,不接受 sing-box 资产。运行时可能产生多个子进程,因此“单 EXE”不是“单 OS 进程”。
**兼容与迁移**:沿用加密 `outbounds` 记录,无 SQLite 表结构迁移,旧 direct/SOCKS5/SSH/K8s 数据不变。旧 serviceport 不认识新 mode,回退前应停服并保留完整加密备份。serviceport 不声明 sing-box 最低/最高版本;部署应固定 binary,并在升级后先检查配置再测试目标。2026-09-27 官方最新稳定版为 v1.14.2;sing-box 的 GPL v3-or-later 及附加名称条款归其上游,serviceport 不重新分发该 binary。
**兼容与迁移**:沿用加密 `outbounds` 记录,无 SQLite 表结构迁移,旧 direct/SOCKS5/SSH/K8s 数据不变。旧 linkport 不认识新 mode,回退前应停服并保留完整加密备份。linkport 不声明 sing-box 最低/最高版本;部署应固定 binary,并在升级后先检查配置再测试目标。2026-09-27 官方最新稳定版为 v1.14.2;sing-box 的 GPL v3-or-later 及附加名称条款归其上游,linkport 不重新分发该 binary。
**实际验证**:Go 1.25.0 聚焦 model/outbound/store/api 的 `SingBox` 测试、model/outbound 全包及全仓 `go vet -p=1 ./...` 通过;前端单进程 `sing-box.test.mjs` 10 项、`connection-stages.test.mjs` 6 项及 Vue 类型检查通过。覆盖大小/结构/危险字段、tag 规范化、受控 SOCKS5 配置、进程共享与重启、目标域名透传、API/bundle/历史差异脱敏和三语闭合错误映射。全仓 Go 测试除 6 个 Windows 文件权限/便携 SQLite 快照用例返回 `Access is denied` 外通过;生产前端构建受 esbuild `spawn EPERM` 阻塞,未生成新 executable 或执行 Playwright。当前机器未安装 sing-box,也未连接真实代理或目标;上述结果是 fixture 验证,不是 live-server 或远程协议验收。详细命令、沙箱限制和官方依据见 [测试记录](testing.md) 与 [sing-box 连接资产](sing-box.md)。
@@ -64,7 +72,7 @@
结果:Kubernetes 表单把发现到的端口作为候选,在不会覆盖已保存值或用户手工值的前提下自动建议单个/首个端口。Service 候选和手工值明确为 Service 端口,其他资源为容器端口,界面统一称为“资源端口”。网关 Kubeconfig 模式增加非阻断权限预检,后端使用 `SelfSubjectAccessReview` 分别检查目标资源 `get`、必要的 Pod `list` 和 `create pods/portforward`,严格区分允许、明确拒绝和无法判断。预检只返回白名单化元数据与稳定错误码,不返回凭据或 Kubernetes 原始错误。上传/内联 Kubeconfig 的共享校验会在保存、发现、预检和正式运行前拒绝可能读取服务端文件或执行本机命令的配置,资产保存错误也通过稳定 code 传到前端翻译。
范围与兼容:新增只读资产子接口及前端诊断控件,不改变网关保存、备份格式或数据库结构。上传/内联 Kubeconfig 新增自包含安全约束:拒绝 CA、客户端证书/密钥及 Token 的外部文件引用,并拒绝 `exec`/`auth-provider` 登录插件,避免以 serviceport 身份读取主机/UNC 路径或执行命令;现有此类资产需先 flatten、改用内嵌凭据后重新上传。旧不安全资产仍可原样导出加密备份,但重命名、恢复、导入预览和包含它的合并导入都会重新校验并拒绝。显式本地 Kubeconfig 文件路径及其中的登录插件仍作为可信部署配置保持兼容;既有网关级持久隧道复用、断线重解析 Pod 和指数退避不变。预检结果不持久化、不阻止保存;授权允许也不证明对象存在、Pod Ready、端口匹配或 SPDY 成功,集群禁用权限审查时显示未知并继续允许实际测试。
范围与兼容:新增只读资产子接口及前端诊断控件,不改变网关保存、备份格式或数据库结构。上传/内联 Kubeconfig 新增自包含安全约束:拒绝 CA、客户端证书/密钥及 Token 的外部文件引用,并拒绝 `exec`/`auth-provider` 登录插件,避免以 linkport 身份读取主机/UNC 路径或执行命令;现有此类资产需先 flatten、改用内嵌凭据后重新上传。旧不安全资产仍可原样导出加密备份,但重命名、恢复、导入预览和包含它的合并导入都会重新校验并拒绝。显式本地 Kubeconfig 文件路径及其中的登录插件仍作为可信部署配置保持兼容;既有网关级持久隧道复用、断线重解析 Pod 和指数退避不变。预检结果不持久化、不阻止保存;授权允许也不证明对象存在、Pod Ready、端口匹配或 SPDY 成功,集群禁用权限审查时显示未知并继续允许实际测试。
实际验证:Go 1.25.0 下聚焦测试、`internal/outbound` 全包和全量 `go vet` 通过;受控 TLS fixture 覆盖允许、拒绝、无 authorizer 意见、评估错误、权限审查被拒绝及登录插件阻断,API fixture 覆盖认证、资产类型、查询校验、稳定响应和凭据不泄漏。前端类型检查及 7 项专项逻辑测试通过,相关 Playwright 场景可发现;Vite/esbuild 被当前沙箱的 `spawn EPERM` 阻止,没有生成新程序或执行浏览器回归。测试使用隔离资产与本机 fixture,不是对真实 Kubernetes RBAC、网络策略或 SPDY 成功链路的验收;全量 Go 中 6 个既有 Windows ACL/SQLite 快照用例仍失败。详细命令见 [测试记录](testing.md)。
@@ -88,7 +96,7 @@
SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主机密钥,不发送目标认证凭据。首次保存返回 `409`、`stage: target_host_confirmation` 和 SHA256 指纹;用户核对后前端带临时 `hostFingerprint` 重试一次,服务端重新探测且一致才保存实际 host key。取消确认保留编辑草稿且不报保存失败。该临时字段不进入模型、历史或 API 回读。
兼容与迁移:沿用现有目标、网关和监听 API;新增的结构化 409 只出现在需要 SSH/SFTP 候选身份确认时。没有 SQLite schema 或数据迁移,旧记录无需重写。现有连接不迁移目标、不替换凭据,也不主动断开;要让旧客户端使用新配置仍需客户端重连或手动断开会话。
兼容与迁移:沿用现有目标、网关和监听 API;新增的结构化 409 只出现在需要 SSH/SFTP 候选身份确认时。没有 SQLite schema 或数据迁移,旧记录无需重写。普通目标或网关配置编辑仍不迁移目标、不替换凭据,也不主动断开;显式「切换目标」操作从 2026-09-28 起会在保存成功后主动断开该网关的现有连接,详见后文记录。
实际验证:Go 1.25.0 下,运行配置/生命周期/SSH 定向回归对 `internal/gateway`、`internal/api` 通过;`go test -p=1 ./internal/gateway ./internal/api -skip '^TestDatabaseSnapshotSessionAndPreview$' -count=1 -timeout=120s` 通过,`go vet ./...` 通过。规定的全量 `go test ./... -count=1 -timeout=120s` 未全绿:复现 6 项既有 Windows ACL/便携 SQLite 快照环境失败,另有未修改的 `internal/protocol/pgproxy/TestPreparedRejectsDifferentIdentity/account` 间歇 fixture 失败;该子项随后单次复跑通过,20 轮复跑仍间歇失败。本功能涉及的 gateway 全包通过,API 除既有快照项外通过。
@@ -112,7 +120,7 @@ SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主
范围与兼容:只改变前端展示与导航容器,原 API、请求顺序、权限、校验、草稿、定时清密、资源标识、配置格式和数据存储不变,无迁移要求。沿用现有内存工作区模型,不新增 URL 路由;浏览器刷新行为不变。关闭页面仍恢复入口焦点,运行保护、引用保护和冲突处理继续生效。
实际验证:`vue-tsc --noEmit`、2 项页面确认队列测试、工作区页面源码禁止项测试和 `git diff --check` 通过;静态检查确认 `web/src` 已移除 `el-dialog`、`el-drawer`、`el-popover` 和 `ElMessageBox` 弹层实现。Playwright 成功发现 56 个文件、125 项用例。`npm.cmd --prefix web run build` 的类型阶段通过,Vite 因当前 Windows 沙箱禁止 esbuild 子进程而报 `spawn EPERM`,开发服务器也在相同阶段失败,因此没有新前端产物或本地预览 URL;未构建新嵌入程序或运行 Playwright,不使用测试发现或旧程序结果代替本轮验收。Go 1.25.0、工作区内缓存/临时目录及串行编译下,`go vet ./...` 通过;`go test -p=1 ./... -count=1 -timeout=120s` 的其余包通过,但仍有 6 项既有 Windows 文件权限或 SQLite 快照环境失败,分布于 `cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 和 `internal/store`,全量测试不记为通过。测试只使用隔离临时目录,未停止本地服务或修改真实 `data`。
实际验证:`vue-tsc --noEmit`、2 项页面确认队列测试、工作区页面源码禁止项测试和 `git diff --check` 通过;静态检查确认 `web/src` 已移除 `el-dialog`、`el-drawer`、`el-popover` 和 `ElMessageBox` 弹层实现。Playwright 成功发现 56 个文件、125 项用例。`npm.cmd --prefix web run build` 的类型阶段通过,Vite 因当前 Windows 沙箱禁止 esbuild 子进程而报 `spawn EPERM`,开发服务器也在相同阶段失败,因此没有新前端产物或本地预览 URL;未构建新嵌入程序或运行 Playwright,不使用测试发现或旧程序结果代替本轮验收。Go 1.25.0、工作区内缓存/临时目录及串行编译下,`go vet ./...` 通过;`go test -p=1 ./... -count=1 -timeout=120s` 的其余包通过,但仍有 6 项既有 Windows 文件权限或 SQLite 快照环境失败,分布于 `cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 和 `internal/store`,全量测试不记为通过。测试只使用隔离临时目录,未停止本地服务或修改真实 `data`。
新建服务入口补充:目标列表和网关列表分别固定新建类别,不再在编辑器内提供“资源类别”下拉;“服务类型”是两类新建表单的第一个配置字段,编辑已有服务时继续锁定协议。这样先确定协议后再展示对应参数,也避免从明确入口进入后重复选择类别。只影响表单顺序与展示,不改变 API、保存结构或已有数据;对应浏览器回归已加入 `target-editor-test.spec.ts`,本轮仅完成用例发现,未在受 `spawn EPERM` 限制的环境中执行。
@@ -122,7 +130,7 @@ SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主
约束与兼容:Redis 一旦添加任意目标账号,所有测试、识别和映射立即恢复显式选择有效账号的规则;普通节点、Cluster/replication 及显式 Sentinel 账号认证失败时不降级。未选择 Sentinel 账号时,原有“自动尝试目标账号或无认证”兼容模式仍只针对 Sentinel 保留。只有 Sentinel 需要认证而数据节点不需要时,需创建一个 `authMethod: "none"` 的数据账号并另选 Sentinel 凭据账号。沿用现有加密目标 JSON、空 `targetAccountId` 和配置备份格式,无 SQLite 迁移;响应中的空 `accounts` 可能因 `omitempty` 被省略,前端按空列表处理。给已绑定的零账号 Redis 添加首个账号后,需为相关网关补选账号再启动。
实际验证:Go 1.25.0、工作区临时目录和串行编译下,model、cluster、gateway、redisproxy 全包及 Redis API 定向回归通过,`go vet -p=1 ./...` 通过。最终 `go test -p=1 ./... -count=1 -timeout=120s` 未全绿:功能相关包均通过,失败为 6 项既有 Windows 权限/SQLite 快照环境问题,分布于 `cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 和 `internal/store`;首次未限制并发的全量运行还遇到编译子进程 Access denied,因此以 `-p=1` 结果为准。前端 `vue-tsc --noEmit`、Redis 纯逻辑 4 项、持续连接 6 项、账号映射 5 项通过;Playwright `--list` 在工作区 TEMP 下成功发现 `target-auth` 与 `target-editor-test` 两个文件的 22 项用例。Vite/esbuild 受当前沙箱 `spawn EPERM` 阻止,无法生成新的 `dist` 和嵌入式可执行程序,因此按必须使用新构建程序的规则未运行 E2E。协议覆盖使用本机 RESP/net.Pipe fixture,验证普通连接、Cluster、Sentinel、认证降级边界和 `HELLO AUTH` 重写,不是实际 Redis/Sentinel 服务器兼容验收;未修改真实 `data`。
实际验证:Go 1.25.0、工作区临时目录和串行编译下,model、cluster、gateway、redisproxy 全包及 Redis API 定向回归通过,`go vet -p=1 ./...` 通过。最终 `go test -p=1 ./... -count=1 -timeout=120s` 未全绿:功能相关包均通过,失败为 6 项既有 Windows 权限/SQLite 快照环境问题,分布于 `cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 和 `internal/store`;首次未限制并发的全量运行还遇到编译子进程 Access denied,因此以 `-p=1` 结果为准。前端 `vue-tsc --noEmit`、Redis 纯逻辑 4 项、持续连接 6 项、账号映射 5 项通过;Playwright `--list` 在工作区 TEMP 下成功发现 `target-auth` 与 `target-editor-test` 两个文件的 22 项用例。Vite/esbuild 受当前沙箱 `spawn EPERM` 阻止,无法生成新的 `dist` 和嵌入式可执行程序,因此按必须使用新构建程序的规则未运行 E2E。协议覆盖使用本机 RESP/net.Pipe fixture,验证普通连接、Cluster、Sentinel、认证降级边界和 `HELLO AUTH` 重写,不是实际 Redis/Sentinel 服务器兼容验收;未修改真实 `data`。
## MySQL 放行 character_set_results 会话设置(2026-09-26)
@@ -154,7 +162,7 @@ SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主
范围与兼容:只改 MySQL 协议模块和相关 fixture,不增加开关、数据库迁移或业务 SQL 改写。为使严格检查与上游词法一致,读写连接也在初始化/重置时固定 SQL 模式和安全字符集;文本动态 SQL、可执行注释、优化器提示、解析失败和修改解析环境的请求被拒绝。WHERE 1=1 仍允许,存储例程/触发器等服务器内部行为不在保证范围。完整边界及客户端适配见 [MySQL 写入保护](mysql-write-safety.md)。
实际验证:Go 1.25.0、工作区内缓存/临时目录及 `GOFLAGS=-p=2` 下,`go test ./internal/protocol/mysqlproxy -count=1 -timeout=120s` 和 `go vet ./...` 通过;新增策略用例补充 SET LOCAL、系统变量引号、排序规则及可见过程定义后,标准定向测试再次通过。`go test ./... -count=1 -timeout=120s` 中 MySQL 及其他协议包通过,全量仍复现此前 6 个本机权限/快照失败,位于 cmd/serviceport、internal/api、internal/desktop、internal/security、internal/store,未宣称全量通过。日志见 `.cache/sqlite-removal/mysql-where-full-test.log` 和 `mysql-where-vet.log`。`git diff --check` 通过。测试使用本机协议 fixture 和隔离临时库,没有连接外部真实 MySQL,也没有执行真实业务数据更新;本轮无前端改动,未运行前端构建/Playwright,未部署或重启服务。
实际验证:Go 1.25.0、工作区内缓存/临时目录及 `GOFLAGS=-p=2` 下,`go test ./internal/protocol/mysqlproxy -count=1 -timeout=120s` 和 `go vet ./...` 通过;新增策略用例补充 SET LOCAL、系统变量引号、排序规则及可见过程定义后,标准定向测试再次通过。`go test ./... -count=1 -timeout=120s` 中 MySQL 及其他协议包通过,全量仍复现此前 6 个本机权限/快照失败,位于 cmd/linkport、internal/api、internal/desktop、internal/security、internal/store,未宣称全量通过。日志见 `.cache/sqlite-removal/mysql-where-full-test.log` 和 `mysql-where-vet.log`。`git diff --check` 通过。测试使用本机协议 fixture 和隔离临时库,没有连接外部真实 MySQL,也没有执行真实业务数据更新;本轮无前端改动,未运行前端构建/Playwright,未部署或重启服务。
## 目标账号使用下拉切换编辑(2026-09-25)
@@ -182,7 +190,7 @@ SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主
## 临时仅发布 Linux amd64(2026-09-25)
原因与结果:用户要求缩短打包等待,以快速验证 IPFS 上传与最终下载地址。编译脚本和发布脚本的目标列表同步收敛为 linux/amd64,当前只生成 `serviceport-<VERSION>-linux-amd64.tar.gz`,不编译 Windows/macOS;前端构建、Linux 单文件验证、校验清单和直接上传流程继续执行。IPFS 请求由三包加索引共四次降为一包加索引共两次,成功日志仍包含下载总页和 Linux 包完整直链。
原因与结果:用户要求缩短打包等待,以快速验证 IPFS 上传与最终下载地址。编译脚本和发布脚本的目标列表同步收敛为 linux/amd64,当前只生成 `linkport-<VERSION>-linux-amd64.tar.gz`,不编译 Windows/macOS;前端构建、Linux 单文件验证、校验清单和直接上传流程继续执行。IPFS 请求由三包加索引共四次降为一包加索引共两次,成功日志仍包含下载总页和 Linux 包完整直链。
范围与兼容:仅影响 Drone 发布目标、清单校验和对应回归测试;应用运行行为及本地 Windows 打包入口不变,无数据迁移,旧包不删除。当前脚本拒绝旧多平台清单,需在新工作区构建;回执字段结构不变,`files` 由七项变为五项(一个包和四个索引文件)。恢复多平台时需同步修改两个目标列表和测试。README、[发布说明](ipfs.md) 及 [输出目录](build-output.md) 已更新。
@@ -228,19 +236,19 @@ SSH/SFTP 候选地址变化会清除旧 pin,并在保存前只探测公开主
实际验证:Node 22.22.3 下 `vue-tsc --noEmit` 及 `git diff --check` 通过;Playwright `test --list` 成功发现剩余 122 项测试。独立子智能体只读复查未发现入口或实现残留。`npm.cmd --prefix web run build` 在 Vite 启动 esbuild 时被当前环境的 `spawn EPERM` 阻断,改用工作区内临时目录仍失败,因此未生成新嵌入程序,未运行需要新程序的 Playwright 浏览器套件。
Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次被工作区外缓存/临时目录写权限阻断;将缓存和临时目录移到 `.cache/sqlite-removal` 后完成重试,仍遇到 `vet.exe` 启动拒绝。Go 测试有 6 项失败,分布于 cmd/serviceport、internal/api、internal/desktop、internal/security 和 internal/store:5 项报告文件权限拒绝,API 备份测试返回无法生成完整快照;其余包测试通过,全量检查未通过。验证日志位于 `.cache/sqlite-removal/go-test.log`、`.cache/sqlite-removal/go-vet.log`。验证仅使用隔离临时数据和本机 fixture,不是外部真实协议服务器验证;未操作真实 `data`。
Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次被工作区外缓存/临时目录写权限阻断;将缓存和临时目录移到 `.cache/sqlite-removal` 后完成重试,仍遇到 `vet.exe` 启动拒绝。Go 测试有 6 项失败,分布于 cmd/linkport、internal/api、internal/desktop、internal/security 和 internal/store:5 项报告文件权限拒绝,API 备份测试返回无法生成完整快照;其余包测试通过,全量检查未通过。验证日志位于 `.cache/sqlite-removal/go-test.log`、`.cache/sqlite-removal/go-vet.log`。验证仅使用隔离临时数据和本机 fixture,不是外部真实协议服务器验证;未操作真实 `data`。
## 项目图标与启动标识(2026-09-24)
原因:用户要求设计项目图标并在启动时打印。新增蓝底白色 P 与青色连接点的 SVG,复用于 README、登录/侧栏和 favicon,随前端嵌入程序;正常服务启动打印纯 ASCII 对应标识,沿用现有版本日志。命令行查询、JSON、路径、证书和维护命令不打印图案,避免污染机器可读输出。只改变品牌展示及正常启动日志,无数据或配置迁移,不修改 EXE 原生资源图标。设计和使用边界见 `docs/branding.md`。
实际验证(2026-09-25):Node 22.22.3 生产构建、Go 1.25.0 `go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 通过。新增命令边界测试验证正常启动打印一次、维护命令及 stdout 不含标识,原 JSON/路径测试仍通过。新嵌入程序 `bin/dev-build/branding-20260925/serviceport.exe` 通过隔离启动/登录验证,全部 124 项 Playwright 通过;确认 SVG 正常加载及 favicon 引用,检查实际 ASCII 启动日志和桌面/登录截图,预览保存在 `bin/previews/branding-20260925/`。测试使用临时数据与本机 fixture,不是上游真实服务器验证。差异检查通过,未部署或修改真实 data。
实际验证(2026-09-25):Node 22.22.3 生产构建、Go 1.25.0 `go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 通过。新增命令边界测试验证正常启动打印一次、维护命令及 stdout 不含标识,原 JSON/路径测试仍通过。新嵌入程序 `bin/dev-build/branding-20260925/linkport.exe` 通过隔离启动/登录验证,全部 124 项 Playwright 通过;确认 SVG 正常加载及 favicon 引用,检查实际 ASCII 启动日志和桌面/登录截图,预览保存在 `bin/previews/branding-20260925/`。测试使用临时数据与本机 fixture,不是上游真实服务器验证。差异检查通过,未部署或修改真实 data。
## 功能重要性与语义颜色(2026-09-24)
原因:用户反馈界面只有白底黑字,功能重要性难以区分。统一主题主色为蓝色(深色采用亮蓝),浅色背景使用淡蓝灰;核心导航显示文字标记,配置/监控图标使用紫/青区分,统计卡增加语义色边框、浅底和数字,网关启动/停止用绿/橙区分,状态色块及异常面板突出成功/异常。保留文字和图标、焦点与禁用态,不以颜色替代状态说明。覆盖公共主题、导航、首页、网关统计/操作及连接编辑器选中页签,不改数据、权限、网络或操作确认,无迁移。主题偏好存储值不变。
实际验证:`npm.cmd --prefix web run build` 通过(本机 Node 22.10 提示 Vite 引擎版本及既有 chunk 大小警告);随后 Go 1.25.0 构建新嵌入程序 `bin/dev-build/ui-colors-20260924/serviceport.exe`,以 SERVICEPORT_E2E_BINARY 指向它,在隔离数据库和 18381/18382 端口运行全部 Playwright:主题/响应式 5 项通过,其余 119 项通过。后半套因 Node 22.10 缺少 node:sqlite 改用已安装的 Node 22.22.3 执行;未修改系统默认版本。检查浅色桌面、深色手机真实截图,预览位于同目录的 light-desktop.png / dark-mobile.png;代表性主按钮和语义文字在 8% 色底上的配色计算最低对比度为浅色/海蓝 4.51:1、深色 4.99:1,不宣称已覆盖所有状态的无障碍审计。`git diff --check` 通过。浏览器后端/协议验证使用隔离本机服务或 fixture,未连接真实目标或修改真实 data;无后端业务改动,未重复运行全量 Go 单测。
实际验证:`npm.cmd --prefix web run build` 通过(本机 Node 22.10 提示 Vite 引擎版本及既有 chunk 大小警告);随后 Go 1.25.0 构建新嵌入程序 `bin/dev-build/ui-colors-20260924/linkport.exe`,以 LINKPORT_E2E_BINARY 指向它,在隔离数据库和 18381/18382 端口运行全部 Playwright:主题/响应式 5 项通过,其余 119 项通过。后半套因 Node 22.10 缺少 node:sqlite 改用已安装的 Node 22.22.3 执行;未修改系统默认版本。检查浅色桌面、深色手机真实截图,预览位于同目录的 light-desktop.png / dark-mobile.png;代表性主按钮和语义文字在 8% 色底上的配色计算最低对比度为浅色/海蓝 4.51:1、深色 4.99:1,不宣称已覆盖所有状态的无障碍审计。`git diff --check` 通过。浏览器后端/协议验证使用隔离本机服务或 fixture,未连接真实目标或修改真实 data;无后端业务改动,未重复运行全量 Go 单测。
## 完整下载地址集中输出(2026-09-24)
@@ -258,7 +266,7 @@ Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次
原因:原脚本六个平台串行且每次 `go build -p 1`,用户反馈耗时。先统一下载模块,默认每批并行两个平台、每平台包编译并发为 2,默认 GOMAXPROCS=2;环境变量允许调节,内存不足可将 BUILD_JOBS/GO_BUILD_PARALLELISM 均退回 1。等待同批全部子任务并检查退出码,失败不生成发布元数据。仅调整编译步骤环境、脚本和打包阶段测试列表,保留用户验证可用的流水线头部、触发、归档格式和 IPFS 行为,无应用/数据库迁移。不添加宿主机挂载或跨构建缓存,不保证首次依赖下载加速;真实速度与峰值内存尚需 runner 实测。
实际验证:新增 15 项隔离 fake Go/shell 测试通过,覆盖六目标、并发重叠及上限、参数边界、模块下载次数、失败等待同批任务与 guard 清理;既有发布及路径 13 项测试通过,shell 语法及差异检查通过。测试不是实际编译性能基准。用户提供的 `logs_zoe_serviceport_17_1_3.log` 仍为旧串行脚本输出,打印到六平台但无退出状态,不能据此确认最终发布成功。
实际验证:新增 15 项隔离 fake Go/shell 测试通过,覆盖六目标、并发重叠及上限、参数边界、模块下载次数、失败等待同批任务与 guard 清理;既有发布及路径 13 项测试通过,shell 语法及差异检查通过。测试不是实际编译性能基准。用户提供的 `logs_zoe_linkport_17_1_3.log` 仍为旧串行脚本输出,打印到六平台但无退出状态,不能据此确认最终发布成功。
## 打包恢复后的上传可见性(2026-09-24)
@@ -276,7 +284,7 @@ Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次
## Drone 六平台小请求 IPFS 发布(2026-09-24)
原因:用户要求保留 Windows/Linux/macOS × amd64/arm64 六个平台独立包并发布到 IPFS,不构建或推送 Docker 镜像。最初八文件整批上传约 117MB 时连接中断,而单包约 18MB 请求成功,因此改用小请求作为规避策略,尚未证实 Cloudflare 具体大小上限。节点组合目录接口 object/new、files/stat、dag/put 返回 404,不依赖它们。仅使用 `/api/v0/add`:六包各自 `wrap-with-directory=false`、`pin=true`、CIDv1,严格校验每包 Name/CID;随后把安全转义下载页、含六包 CID/SHA256 的 JSON 索引、原 release.json 和 SHA256SUMS.txt 四个小文件包装为目录上传,同样请求 pin。每次显式使用 `serviceport-ci-release/1` User-Agent。
原因:用户要求保留 Windows/Linux/macOS × amd64/arm64 六个平台独立包并发布到 IPFS,不构建或推送 Docker 镜像。最初八文件整批上传约 117MB 时连接中断,而单包约 18MB 请求成功,因此改用小请求作为规避策略,尚未证实 Cloudflare 具体大小上限。节点组合目录接口 object/new、files/stat、dag/put 返回 404,不依赖它们。仅使用 `/api/v0/add`:六包各自 `wrap-with-directory=false`、`pin=true`、CIDv1,严格校验每包 Name/CID;随后把安全转义下载页、含六包 CID/SHA256 的 JSON 索引、原 release.json 和 SHA256SUMS.txt 四个小文件包装为目录上传,同样请求 pin。每次显式使用 `linkport-ci-release/1` User-Agent。
范围与兼容:修改发布脚本、专项测试及 IPFS 文档,不改应用运行时、数据库、六平台打包方式或本地单 EXE 入口。固定 API https://ipfs.freeicu.top、必需的 Drone ipfs_token Secret、现有 http://192.168.9.1:7893 代理保持不变,不要求 ipfs_api Secret;本次不改 .drone.yml。令牌通过 curl stdin 配置传递,不写仓库、不进 argv 或日志,保留 TLS 校验并禁用隐式 curl 配置。原六包和两份清单保持不变,生成索引只放受检临时目录。失败立即终止,不写最终成功回执;已成功请求 pin 的单包不自动撤销。已有回执时在上传前拒绝覆盖。
@@ -294,7 +302,7 @@ Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次
## Drone 保留镜像构建、移除推送(2026-09-24)
原因:镜像构建已成功,但远端仓库拒绝推送,用户明确要求删除 Docker 推送。Docker 插件设置 `dry_run: true`,保留真实镜像构建而不执行 push;镜像名改为本地 `serviceport:latest`,移除 registry、username、password 以及对 `docker_username` / `docker_password` Secret 的依赖。SHA-256 克隆、精确提交校验及全局/构建代理参数不变。仅影响 CI 发布行为,无应用运行时和数据迁移;此后成功构建不再发布镜像,也未新增镜像导出或持久化,临时 Docker 环境中的镜像不作为可下载产物交付。
原因:镜像构建已成功,但远端仓库拒绝推送,用户明确要求删除 Docker 推送。Docker 插件设置 `dry_run: true`,保留真实镜像构建而不执行 push;镜像名改为本地 `linkport:latest`,移除 registry、username、password 以及对 `docker_username` / `docker_password` Secret 的依赖。SHA-256 克隆、精确提交校验及全局/构建代理参数不变。仅影响 CI 发布行为,无应用运行时和数据迁移;此后成功构建不再发布镜像,也未新增镜像导出或持久化,临时 Docker 环境中的镜像不作为可下载产物交付。
实际验证:按 Docker 插件官方 `dry_run` 定义配置;YAML 解析、禁止推送/无发布凭据断言、原克隆命令与代理参数保持不变检查、`git diff --check` 通过。未运行新的完整 Drone/Docker 构建,不将静态检查视为真实 CI 验证。
@@ -328,7 +336,7 @@ Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 首次
## Drone CI Linux 打包修复(2026-09-23)
原因:现有流水线仍引用旧 `agentdb` 模块和命令入口,不能编译本项目。改为固定 Go 1.25.0 和 Node 22.19.0,先测试并构建前端,再以受限编译并发生成内嵌页面的 `serviceport` Linux amd64 无 CGO 单文件程序,使用既有 `verify-package.mjs` 在隔离临时数据库与端口上验证后归档并生成 SHA256;可选标签 IPFS 上传保留且 API 路径不重复拼接。仅影响 CI 构建与发布,无运行时配置或数据迁移。本地 YAML 解析及 Go 1.25.0 Linux amd64 交叉编译通过;Windows 宿主无法运行 Linux 验证程序,未在真实 Drone runner/IPFS 节点发布,也未连接真实目标服务。
原因:现有流水线仍引用旧 `agentdb` 模块和命令入口,不能编译本项目。改为固定 Go 1.25.0 和 Node 22.19.0,先测试并构建前端,再以受限编译并发生成内嵌页面的 `linkport` Linux amd64 无 CGO 单文件程序,使用既有 `verify-package.mjs` 在隔离临时数据库与端口上验证后归档并生成 SHA256;可选标签 IPFS 上传保留且 API 路径不重复拼接。仅影响 CI 构建与发布,无运行时配置或数据迁移。本地 YAML 解析及 Go 1.25.0 Linux amd64 交叉编译通过;Windows 宿主无法运行 Linux 验证程序,未在真实 Drone runner/IPFS 节点发布,也未连接真实目标服务。
## SSH 连接测试拨号前诊断(2026-09-23)
@@ -372,7 +380,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
实际验证:Go 1.25.0 全量 `go test ./... -count=1 -timeout=120s`、`go vet ./...` 通过;新增 API 边界测试命令 `go test ./internal/api -run '^TestTCPIndependentBoundary' -count=1 -timeout=120s -v` 通过(5 项测试、21 项子测试),涵盖无效代理/字段、账号冲突、部分更新、模式切换、引用重绑定、代理改类型及影响范围响应脱敏。`TestIndependentTCPForwardTwoRoutes` 经本地带密码 SOCKS5 fixture 转发两条映射到本地 TCP fixtures 并校验二进制字节,不是真实用户 `192.168.11.16` 网络验证。
构建与浏览器验证:首次 Node 22.10 构建成功但存在 Vite 版本警告;补充图视图后,使用机器已有 Node 22.22.3 重新执行 `scripts/build.ps1 -SkipTests`,完成 npm build 与 Go 1.25.0 编译,生成最终程序 `artifacts/tcp-independent/serviceport.exe`,无 Node 版本警告,仅有 chunk 大小警告。最终重建前两项定向 Playwright 已通过。此前全量在 Node 22.10 下因 `node:sqlite` 不可用而在测试发现阶段失败;仅在当前 shell 通过 `fnm use 22.22.3` 切换已有版本后,一次运行在 59 项时因工具 200 秒时限中断,并非用例失败结论。最终使用 Node 22.22.3、`SERVICEPORT_E2E_BINARY=artifacts/tcp-independent/serviceport.exe`,在隔离端口 `19281/19282` 延长时限完整重跑,**121/121 通过(6.6 分钟)**,包含最终图视图修复;日志 `artifacts/tcp-independent-playwright-final.log`,详见 `docs/testing.md`。
构建与浏览器验证:首次 Node 22.10 构建成功但存在 Vite 版本警告;补充图视图后,使用机器已有 Node 22.22.3 重新执行 `scripts/build.ps1 -SkipTests`,完成 npm build 与 Go 1.25.0 编译,生成最终程序 `artifacts/tcp-independent/linkport.exe`,无 Node 版本警告,仅有 chunk 大小警告。最终重建前两项定向 Playwright 已通过。此前全量在 Node 22.10 下因 `node:sqlite` 不可用而在测试发现阶段失败;仅在当前 shell 通过 `fnm use 22.22.3` 切换已有版本后,一次运行在 59 项时因工具 200 秒时限中断,并非用例失败结论。最终使用 Node 22.22.3、`LINKPORT_E2E_BINARY=artifacts/tcp-independent/linkport.exe`,在隔离端口 `19281/19282` 延长时限完整重跑,**121/121 通过(6.6 分钟)**,包含最终图视图修复;日志 `artifacts/tcp-independent-playwright-final.log`,详见 `docs/testing.md`。
## 连接诊断、资产影响与完整备份优化(2026-09-22)
@@ -397,7 +405,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
原因:用户在其他工作副本启动时遇到 `Go is required`,并要求所有脚本跨机器使用、删除固定绝对路径。原脚本包含固定 Go 安装位置,若干 Node 验证入口与浏览器服务还依赖调用者当前目录。
结果:移除 Go 固定安装目录与用户目录扫描,Go/Node 默认优先使用项目 tools 目录,再从 PATH 获取;可选显式覆盖按项目根目录解析相对值,错误覆盖不降级。构建、打包和矩阵脚本共享工具查找,Node 子进程使用配套运行环境。验证脚本与浏览器测试服务统一按脚本位置定位项目,文件参数和 SERVICEPORT_E2E_BINARY 的相对值不再依赖调用目录;Shell 入口增加项目便携工具链优先级,工具目录不进入 Git/Docker 上下文。
结果:移除 Go 固定安装目录与用户目录扫描,Go/Node 默认优先使用项目 tools 目录,再从 PATH 获取;可选显式覆盖按项目根目录解析相对值,错误覆盖不降级。构建、打包和矩阵脚本共享工具查找,Node 子进程使用配套运行环境。验证脚本与浏览器测试服务统一按脚本位置定位项目,文件参数和 LINKPORT_E2E_BINARY 的相对值不再依赖调用目录;Shell 入口增加项目便携工具链优先级,工具目录不进入 Git/Docker 上下文。
范围与兼容:涉及开发/打包/验证脚本和测试启动器,不改变业务 API、Go 1.25.0 约束、数据库选择或发行版运行。工具只安装在未加入 PATH 的系统目录时,需要自行加入 PATH 或放入项目 tools;验证脚本的相对参数现在统一以项目根目录为基准。用户显式外部路径仍兼容。运行时规范化、临时目录清理范围检查、停止服务前构建保护与容器内挂载契约保留;不自动安装软件或修改系统 PATH。
@@ -437,7 +445,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
结果与范围:整合三平台回环本地模式、Windows 新库 ACL、Unix 数据库权限检查、单 EXE 打包及 bin 构建目录、范围输入与保存后缓存更新、请求路径诊断和响应式布局。补齐远端引用但未提交的 Unix 平台实现及请求诊断模块;诊断不保留请求体、凭据或 URL 查询参数。修正桌面验证脚本对已退出子进程的重复等待。
兼容与迁移:沿用 local_metadata 中的库内主密钥,不引入远端所述 local_key 格式,不恢复 SERVICEPORT_MASTER_KEY 或外部审计/TLS 文件。保留显式旧库迁移能力及旧 DPAPI 文件,不改动真实 data,不部署。Unix 本地模式会拒绝组或其他用户可访问的已有数据库。下方合入的历史记录仅描述当时分支状态;当前格式和实际脚本以 single-file-storage.md、platform-support.md、build-output.md 为准,不表示仓库存在六目标 package.mjs。
兼容与迁移:沿用 local_metadata 中的库内主密钥,不引入远端所述 local_key 格式,不恢复 LINKPORT_MASTER_KEY 或外部审计/TLS 文件。保留显式旧库迁移能力及旧 DPAPI 文件,不改动真实 data,不部署。Unix 本地模式会拒绝组或其他用户可访问的已有数据库。下方合入的历史记录仅描述当时分支状态;当前格式和实际脚本以 single-file-storage.md、platform-support.md、build-output.md 为准,不表示仓库存在六目标 package.mjs。
实际验证:见 docs/testing.md 的同日合并记录;测试失败如实保留,不把交叉编译或本机 fixture 当作异系统或生产服务实测。
@@ -445,7 +453,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
原因:用户要求纯 TCP 端口映射,并明确默认监听 0.0.0.0;现有协议网关强制账号与协议认证,不适合透传。
结果:新增 tcp handler,使用现有目标、出口、监听生命周期和来源/连接数限制。TCP 目标与网关无需账号,普通及拖拽绑定不要求 targetAccountId;接口拒绝 TCP 账号配置、TLS 终止及只读策略,普通协议的账号约束不变。界面隐藏 TCP 的账号/TLS 终止设置,明确提示无网关认证和全网卡监听风险;连接测试与定时探测仅检查 TCP 可达。运行中切换保留旧连接,停止时断开两端。SSH/K8s 包装透出底层写半关闭,避免隐藏已有能力。
结果:新增 tcp handler,使用现有目标、出口、监听生命周期和来源/连接数限制。TCP 目标与网关无需账号,普通及拖拽绑定不要求 targetAccountId;接口拒绝 TCP 账号配置、TLS 终止及只读策略,普通协议的账号约束不变。界面隐藏 TCP 的账号/TLS 终止设置,明确提示无网关认证和全网卡监听风险;连接测试与定时探测仅检查 TCP 可达。停止时断开两端;2026-09-28 起运行中显式切换目标也会断开现有连接,让客户端重连后使用新目标。SSH/K8s 包装透出底层写半关闭,避免隐藏已有能力。
范围与兼容:新增协议类型,无数据库迁移,不编辑真实配置或启动正式映射。默认开放监听网卡不等于绕过来源白名单;应用认证和 TLS 由客户端及目标负责。旧程序不识别 tcp 配置。未实现 UDP、应用命令审计、账号级映射或协议集群发现。本项目无 multilingual-ui 指定的 Shopee i18n 机制,沿用现有中文文案模式。
@@ -461,11 +469,11 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
## 拖拽账号映射的连接路径与测试(2026-09-21)
多账号显示补充:用户要求目标服务多账号在拖拽时直接可见,因此目标账号列表改为常驻展开,显示名称和用户名。连线固定落到对应账号行,选择源账号不再触发布局变化;仅同协议账号可接收关联。影响仅为连线图展示与交互,不迁移数据、不修改其他账号映射。最新 serviceport-multi-account.exe 的五项专项回归通过,account-targets 截图复跑通过,已检查桌面与窄屏显示。未替换正在运行的服务。
多账号显示补充:用户要求目标服务多账号在拖拽时直接可见,因此目标账号列表改为常驻展开,显示名称和用户名。连线固定落到对应账号行,选择源账号不再触发布局变化;仅同协议账号可接收关联。影响仅为连线图展示与交互,不迁移数据、不修改其他账号映射。最新 linkport-multi-account.exe 的五项专项回归通过,account-targets 截图复跑通过,已检查桌面与窄屏显示。未替换正在运行的服务。
原因:单个网关账号拖拽关联目标后,网关本身不一定有默认目标;原 UI 读取网关顶层字段生成路径,出现“直连 → 未配置地址/端口”,原测试 API 也只允许默认目标。用户明确要求网关账号对应目标账号。
结果:新增 GatewayConnectionTest 与 gatewayTest helper,网关测试先选择账号映射,按实际目标配置显示路径;多账号显式选择,不随机测试第一条映射。API 支持 gatewayAccountId,通过现有账号 Apply 规则解析目标凭据、TLS 和出口;不新增账号解析规则,不回退未知/禁用账号。新增测试领域逻辑位于 internal/api/gateway_connection.go,server.go 仅调用。拖拽图增加测试入口和 K8s 资源说明;目标只有一个账号也显示账号入口。单账号拖拽仍调用原有按账号保存接口,其他账号不受影响,整卡批量绑定保留兼容。
结果:新增 GatewayConnectionTest 与 gatewayTest helper,网关测试先选择账号映射,按实际目标配置显示路径;多账号显式选择,不随机测试第一条映射。API 支持 gatewayAccountId,通过现有账号 Apply 规则解析目标凭据、TLS 和出口;不新增账号解析规则,不回退未知/禁用账号。新增测试领域逻辑位于 internal/api/gateway_connection.go,server.go 仅调用。拖拽图增加测试入口和 K8s 资源说明;目标只有一个账号也显示账号入口。单账号拖拽仍调用原有按账号保存接口,其他账号不受影响;当前界面已取消整卡批量绑定,仅保留旧 API 的兼容语义。
范围与兼容:只改变测试选路和界面路径,未修改真实账号、存储、既有路由和网关监听,无数据迁移。无 query 的旧测试 API 保持默认目标行为。列表和请求不新增密码/Kubeconfig 内容,诊断仍为固定阶段白名单。本项目没有 multilingual-ui 指定的 Shopee i18n 框架,沿用现有中文文案模式。
@@ -479,7 +487,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
兼容与保护:不修改资产载荷、后端接口、存储、项目/环境规则、校验与未保存守护;保留工作区插槽、内联 append-to-body、关闭清理和读取中禁用保存。无需迁移,无真实数据写入。新建和编辑使用同一组件路径,避免两种交互继续分叉。
实际验证:生产前端构建、Go 1.25.0 全量测试、vet 通过。新增两项浏览器用例通过,覆盖桌面/手机尺寸与目标弹框一致、底部按钮可见、关闭守护和草稿重置;既有资产备注保存及内联资产用例共三项通过。响应式测试更新“所有共享弹框均有连接页签”的旧假定后,两项重跑通过,仍验证各表单固定底部按钮和滚动布局。首轮新增用例捕获了打开/关闭动画尚未结束时的测量和重叠断言,改为等待状态稳定并关闭截图动画后通过,未通过修改产品守护逻辑来绕过测试。基于新构建 serviceport-asset-dialog.exe 的全量浏览器结果见 docs/testing.md;未部署替换用户运行程序。
实际验证:生产前端构建、Go 1.25.0 全量测试、vet 通过。新增两项浏览器用例通过,覆盖桌面/手机尺寸与目标弹框一致、底部按钮可见、关闭守护和草稿重置;既有资产备注保存及内联资产用例共三项通过。响应式测试更新“所有共享弹框均有连接页签”的旧假定后,两项重跑通过,仍验证各表单固定底部按钮和滚动布局。首轮新增用例捕获了打开/关闭动画尚未结束时的测量和重叠断言,改为等待状态稳定并关闭截图动画后通过,未通过修改产品守护逻辑来绕过测试。基于新构建 linkport-asset-dialog.exe 的全量浏览器结果见 docs/testing.md;未部署替换用户运行程序。
## 侧栏菜单与资源互斥切换(2026-09-21)
@@ -543,11 +551,11 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
验证:Go 1.25.0 的 model/store/api 定向测试、`go vet ./...`、Windows 后端编译及前端 `vue-tsc --noEmit` 通过。覆盖运行中代理项目更新、拒绝混入连接参数、CSRF、凭据/未知字段/引用保留、历史、资产全局列举和旧备份归一化。两次全量 `go test ./... -count=1 -timeout=120s` 均仅在未改动的 `internal/cluster/TestMySQLReplicationAndGroupDiscovery/mysql-group-replication` fixture 报“cluster has no available primary or router”,其余包通过;该用例单独重跑通过,不将全量回归记为通过。测试仅使用隔离 SQLite/协议 fixture,没有连接真实上游。
前端验证限制:`npm.cmd --prefix web run build` 通过类型检查后在 Vite/esbuild 子进程启动处遭遇 `spawn EPERM`;Playwright 已指定新编译的 `SERVICEPORT_E2E_BINARY` 及隔离端口,同样在启动阶段受阻,未执行用例。已补充全局资产/代理修改项目的 Playwright 用例,并调整旧项目范围断言,但不宣称浏览器验收通过。验证程序位于 `artifacts/project-asset-validation/serviceport.exe`,嵌入既有前端产物;本次未替换 `bin/serviceport.exe` 或更改实际 `data`。相关使用说明见 [文件资产](file-assets.md) 和 [项目与环境](project-environments.md)。
前端验证限制:`npm.cmd --prefix web run build` 通过类型检查后在 Vite/esbuild 子进程启动处遭遇 `spawn EPERM`;Playwright 已指定新编译的 `LINKPORT_E2E_BINARY` 及隔离端口,同样在启动阶段受阻,未执行用例。已补充全局资产/代理修改项目的 Playwright 用例,并调整旧项目范围断言,但不宣称浏览器验收通过。验证程序位于 `artifacts/project-asset-validation/linkport.exe`,嵌入既有前端产物;本次未替换 `bin/linkport.exe` 或更改实际 `data`。相关使用说明见 [文件资产](file-assets.md) 和 [项目与环境](project-environments.md)。
## 跨平台单 SQLite 存储(2026-09-21)
原因:用户要求所有平台只需一个 SQLite,不保留 `SERVICEPORT_MASTER_KEY` 或独立 DPAPI 密钥依赖。仅移除密钥文件无法覆盖旧有审计分库和自动 TLS,因此本次同时统一其持久化位置。
原因:用户要求所有平台只需一个 SQLite,不保留 `LINKPORT_MASTER_KEY` 或独立 DPAPI 密钥依赖。仅移除密钥文件无法覆盖旧有审计分库和自动 TLS,因此本次同时统一其持久化位置。
结果与范围:新增单库启动入口,随机主密钥保存在内部元数据表;命令、输出、连接事件合并到主库,自动 TLS 身份入库。Windows 命名互斥量和 Linux/macOS 主文件 flock 替代新建锁文件。移除主密钥环境变量读取与 generate-key 命令;保留离线 migrate-data、交互 migrate-key,以及只导出公开证书的 export-ca。启动脚本同步备份锁,新库不生成 DPAPI/audit/tls/desktop.lock。
@@ -557,7 +565,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
验证限制:`npm.cmd --prefix web run build` 的类型检查完成,但 Vite 启动 esbuild 时因 `spawn EPERM` 失败;指定新可执行文件和隔离端口执行完整 Playwright 同样在启动阶段因 `spawn EPERM` 受阻,未执行用例。启动脚本回归完成 build-only 阶段,在 WMI `Get-CimInstance` 被拒绝访问处停止,不能称为全量通过。新 Windows 程序嵌入现有 `web/dist`,本次未成功重新生成前端资源。
本机升级:用户确认停服并明确要求删除密钥后,先备份完整旧 `data` 与旧程序到 `artifacts/backups/single-file-migration-20260921-140105`;副本演练后对本机库执行离线迁移,再仅复制 SQLite 到隔离目录验证成功。安装新 `bin/serviceport.exe`,删除运行目录 `data/master-key.dpapi`,再次执行离线迁移成功且文件未重建。旧密钥只在升级前备份中保留以便恢复;旧审计/TLS 文件未删除,程序已不依赖这些迁移输入。服务保持停止,没有启动真实网关或修改目标服务器数据。
本机升级:用户确认停服并明确要求删除密钥后,先备份完整旧 `data` 与旧程序到 `artifacts/backups/single-file-migration-20260921-140105`;副本演练后对本机库执行离线迁移,再仅复制 SQLite 到隔离目录验证成功。安装新 `bin/linkport.exe`,删除运行目录 `data/master-key.dpapi`,再次执行离线迁移成功且文件未重建。旧密钥只在升级前备份中保留以便恢复;旧审计/TLS 文件未删除,程序已不依赖这些迁移输入。服务保持停止,没有启动真实网关或修改目标服务器数据。
本文件记录本轮累计改动的原因、最终行为和验证边界。2026-09-18 为本次归档日期,不代表所有功能都在当天完成。具体操作方式以链接的功能文档为准;后续行为变更应同步补充原因,避免只留下“改了什么”。
@@ -589,7 +597,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
## 主密钥入库与指定历史目录整合(2026-09-21)
原因:用户进一步要求去掉独立 `master-key`,明确选择“密钥存入数据库,保留现有加密字段”,随后要求整合 `C:\work\serviceport\data` 的历史数据库。本节取代下文暂定明文外置密钥方案;历史记录保留用于解释决策变化。
原因:用户进一步要求去掉独立 `master-key`,明确选择“密钥存入数据库,保留现有加密字段”,随后要求整合 `C:\work\linkport\data` 的历史数据库。本节取代下文暂定明文外置密钥方案;历史记录保留用于解释决策变化。
结果与范围:本地模式只在空库的事务中生成随机密钥,保存于 `local_key`,随后照常初始化加密配置表。正常启动不创建、读取或删除外置密钥。Windows 新建主库使用当前用户/SYSTEM ACL;Unix 使用 `0600` 并校验属主和文件类型。数据库已有密钥时,显式环境密钥必须匹配;记录缺失、损坏或旧库无库内密钥时拒绝启动,不生成替代密钥。数据库敏感字段仍为 AES-GCM 密文,但密钥与密文同库,数据库泄漏即可解密。独立审计库、TLS 和现有环境密钥服务器模式保留。详细行为见 [跨平台支持](platform-support.md)。
@@ -611,7 +619,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
原因:用户要求整个项目支持 Windows、macOS、Linux,并明确选择不加密密钥文件、不兼容 DPAPI。原来的 Windows 专用密钥封装和启动实现无法直接用于其他系统。
结果与范围:三个系统未设置 `SERVICEPORT_MASTER_KEY` 时均可直接启动,使用数据目录中的 `master-key`(32 字节随机密钥的 Base64 明文),无需解锁口令。数据库内的密码、私钥与资产内容继续按原逻辑加密。Windows 新建密钥限制 ACL 为当前用户和 SYSTEM;Unix 创建 `0600` 文件,检查所有者、文件类型和权限并拒绝符号链接,以 `flock` 防止同一目录多开。浏览器启动分别调用 Windows 系统工具、macOS `open` 和 Linux `xdg-open`;没有桌面时可使用终端打印的地址。
结果与范围:三个系统未设置 `LINKPORT_MASTER_KEY` 时均可直接启动,使用数据目录中的 `master-key`(32 字节随机密钥的 Base64 明文),无需解锁口令。数据库内的密码、私钥与资产内容继续按原逻辑加密。Windows 新建密钥限制 ACL 为当前用户和 SYSTEM;Unix 创建 `0600` 文件,检查所有者、文件类型和权限并拒绝符号链接,以 `flock` 防止同一目录多开。浏览器启动分别调用 Windows 系统工具、macOS `open` 和 Linux `xdg-open`;没有桌面时可使用终端打印的地址。
数据选择扩展到三个系统:显式路径优先,其次当前目录已有库,再次系统用户配置目录已有库;均无已有库时在当前目录新建。新建密钥失败会清理本次创建的文件;已有库缺密钥或密钥损坏直接失败,不生成替代密钥。新增 Unix 构建/启动入口、通用 Node 打包脚本、三系统原生 CI 和六目标编译 CI;前端构建根据宿主选择 npm,构建锁覆盖前端生成到 Go 嵌入。各目标目录交付单个可执行文件,多目标发布根目录附版本清单及 SHA256,现有 Windows PowerShell 单 EXE 入口保留。
@@ -627,7 +635,7 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
原因:用户明确要求最终打包输出只保留一个 EXE。主程序已经内嵌前端并支持本地密钥、证书生成,原流程附带的 ZIP、证书工具、启动脚本、说明、版本及校验文件增加了分发文件数量。
结果与范围:`package.cmd` / `scripts/package.ps1` 保留版本注入、前端缓存、构建锁和隔离验证,最终新建的 `artifacts/releases/<时间与唯一标识>/` 中只输出 `serviceport.exe`。构建在独立缓存目录进行,验证通过后才将已核实处于仓库内的暂存目录移动到交付目录;失败时不会发布半成品或覆盖历史版本。校验器要求目录中恰有一个 EXE,从程序读取内嵌版本,并用独立临时数据验证启动、网页静态资源与登录。SHA-256 只打印到控制台,版本信息通过 `--version --json` 查询,文档和配置示例保留在仓库。
结果与范围:`package.cmd` / `scripts/package.ps1` 保留版本注入、前端缓存、构建锁和隔离验证,最终新建的 `artifacts/releases/<时间与唯一标识>/` 中只输出 `linkport.exe`。构建在独立缓存目录进行,验证通过后才将已核实处于仓库内的暂存目录移动到交付目录;失败时不会发布半成品或覆盖历史版本。校验器要求目录中恰有一个 EXE,从程序读取内嵌版本,并用独立临时数据验证启动、网页静态资源与登录。SHA-256 只打印到控制台,版本信息通过 `--version --json` 查询,文档和配置示例保留在仓库。
兼容与迁移:移除已无用途的 `-Compression` 参数;`-Version` 和 `-RebuildWeb` 保留。旧 ZIP 产物不自动删除。双击 EXE 沿用现有数据路径与自动生成密钥、证书的规则,不需要数据库迁移;运行产生的数据库、日志、密钥和 TLS 文件仍在选中的数据目录。“单文件”仅指交付产物。单 EXE 不执行源码启动脚本的升级备份流程,更新前仍须停服并完整备份数据。主程序逻辑、开发启动脚本和独立证书工具源码未变。
@@ -637,11 +645,11 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
原因:用户要求优先使用当前目录,同时兼容 AppData 多路径遍历,并明确启动脚本不再指定数据库路径。需要统一程序启动与备份的数据选择,避免找到旧库却备份了空的本地目录。
结果与范围:新增统一路径解析器;显式 `SERVICEPORT_DATA` 优先,否则 Windows 本地模式依次查找当前目录 `data/serviceport.sqlite` 和 `%APPDATA%\serviceport\serviceport.sqlite`,复用第一个已有文件,均不存在时新建当前目录库。选中后不因缺密钥、占用或损坏而跳库。数据库、DPAPI 密钥、运行锁、审计和自动 TLS 文件保持同目录关联。新增无初始化副作用的 `data-path` CLI,供用户查询及启动脚本确定实际备份目录。CMD 入口先进入自身目录,以相对路径调用 PowerShell 并在退出时恢复调用者目录;PowerShell 也定位项目/解压根目录,但不设置或覆盖数据环境变量。源码模式以新构建程序查询,发布模式使用配套新版程序;停本项目旧服务后持锁备份选中目录,拒绝被其他实例占用的数据及包含备份目标的递归复制。
结果与范围:新增统一路径解析器;显式 `LINKPORT_DATA` 优先,否则 Windows 本地模式依次查找当前目录 `data/linkport.sqlite` 和 `%APPDATA%\linkport\linkport.sqlite`,复用第一个已有文件,均不存在时新建当前目录库。选中后不因缺密钥、占用或损坏而跳库。数据库、DPAPI 密钥、运行锁、审计和自动 TLS 文件保持同目录关联。新增无初始化副作用的 `data-path` CLI,供用户查询及启动脚本确定实际备份目录。CMD 入口先进入自身目录,以相对路径调用 PowerShell 并在退出时恢复调用者目录;PowerShell 也定位项目/解压根目录,但不设置或覆盖数据环境变量。源码模式以新构建程序查询,发布模式使用配套新版程序;停本项目旧服务后持锁备份选中目录,拒绝被其他实例占用的数据及包含备份目标的递归复制。
兼容与迁移:已有项目 `data` 优先,旧 AppData-only 用户直接复用原库;两个库都存在时选择当前目录,不合并、不移动、不删除。启动脚本现在保留用户显式数据路径,`.env.example` 的数据路径改为留空,程序仍不自动读取 `.env`。手动主密钥/非 Windows 模式保持当前目录默认且不遍历 AppData。无需 SQLite 表结构迁移,不改变 DPAPI 账户/机器绑定或丢失密钥保护。桌面验证脚本改用隔离工作目录和隔离 AppData,并将失效的管理员字段定位更新为实际页面上的“登录账号/登录密码”,以执行新增的旧库复用验收。详见 [Windows 使用说明](../packaging/windows/README.md)。
实际验证:Go 1.25.0 的 `internal/desktop`、`cmd/serviceport` 测试、`go vet ./...`、启动脚本 fixture 回归、Vue 类型检查和前端生产构建通过;新构建 EXE 的 Edge 桌面验收通过当前目录新库、隔离 AppData 旧库、DPAPI 密钥保持、TLS 与原账号重启登录。全量 Go 测试未全绿:`internal/cluster` 的 `TestMySQLReplicationAndGroupDiscovery` 在全量及单包复跑中出现无可用主节点或拓扑不符,其余包通过,本次未改动集群实现。Playwright 使用同一新 EXE、隔离数据及 19091/19092 端口,结果为 77 通过、3 失败、8 未运行(达到 `--max-failures=3` 后停止):账号编辑器 320×568 权限页出现额外滚动,URL 参数和搜索/树导航两项用例超时;未改动这些前端实现。完整边界见 [测试记录](testing.md)。脚本测试使用不启动服务的可执行 fixture,桌面验证使用实际新 EXE 和隔离目录;协议测试为本地 fixture,不接入真实上游服务,未停止实际用户服务、未操作真实数据。
实际验证:Go 1.25.0 的 `internal/desktop`、`cmd/linkport` 测试、`go vet ./...`、启动脚本 fixture 回归、Vue 类型检查和前端生产构建通过;新构建 EXE 的 Edge 桌面验收通过当前目录新库、隔离 AppData 旧库、DPAPI 密钥保持、TLS 与原账号重启登录。全量 Go 测试未全绿:`internal/cluster` 的 `TestMySQLReplicationAndGroupDiscovery` 在全量及单包复跑中出现无可用主节点或拓扑不符,其余包通过,本次未改动集群实现。Playwright 使用同一新 EXE、隔离数据及 19091/19092 端口,结果为 77 通过、3 失败、8 未运行(达到 `--max-failures=3` 后停止):账号编辑器 320×568 权限页出现额外滚动,URL 参数和搜索/树导航两项用例超时;未改动这些前端实现。完整边界见 [测试记录](testing.md)。脚本测试使用不启动服务的可执行 fixture,桌面验证使用实际新 EXE 和隔离目录;协议测试为本地 fixture,不接入真实上游服务,未停止实际用户服务、未操作真实数据。
## UI 待办十一项实现(2026-09-20)
@@ -691,13 +699,13 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
范围与兼容:新增 internal/sqlitebrowser 模块、独立 API 域文件和 Vue 组件/状态模块;App.vue 与 server.go 仅接入导航和路由。无新增依赖、数据库迁移或网关协议变更,不读取或修改真实业务数据库做测试。页面路径和查询不持久化、不进入备份或同步;不支持加密 SQLite 文件和远程 SQLite 网关。操作与接口说明见 [本地 SQLite 文件](sqlite.md)。
验证:SQLite 模块与 API 专项测试通过,使用隔离的真实 SQLite 文件(不是模拟 SQL 响应),覆盖只读保护、特殊字符路径、表/视图、结果类型、截断、取消、空数据库及 WAL 中已提交记录。npm.cmd --prefix web run build、Go 1.25.0 编译及 go vet ./... 通过。go test ./... -count=1 -timeout=120s 仅复现原有 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用新构建程序和隔离数据库运行 71 项 Playwright:70 项首次通过,主题测试因 browser.newContext 报浏览器已关闭而重试通过,报告 1 flaky。随后修正系统表前缀过滤,避免误隐藏 sqliteXuser 等用户表,模块测试及最终程序上的两项 SQLite 浏览器测试再次通过。浏览器验证覆盖真实文件、表结构、只读拒绝、空结果、截断、小屏幕和切换路径后的迟到响应;无真实业务数据库操作。最终可执行文件为 artifacts/sqlite-final/serviceport.exe,测试实例已停止。
验证:SQLite 模块与 API 专项测试通过,使用隔离的真实 SQLite 文件(不是模拟 SQL 响应),覆盖只读保护、特殊字符路径、表/视图、结果类型、截断、取消、空数据库及 WAL 中已提交记录。npm.cmd --prefix web run build、Go 1.25.0 编译及 go vet ./... 通过。go test ./... -count=1 -timeout=120s 仅复现原有 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用新构建程序和隔离数据库运行 71 项 Playwright:70 项首次通过,主题测试因 browser.newContext 报浏览器已关闭而重试通过,报告 1 flaky。随后修正系统表前缀过滤,避免误隐藏 sqliteXuser 等用户表,模块测试及最终程序上的两项 SQLite 浏览器测试再次通过。浏览器验证覆盖真实文件、表结构、只读拒绝、空结果、截断、小屏幕和切换路径后的迟到响应;无真实业务数据库操作。最终可执行文件为 artifacts/sqlite-final/linkport.exe,测试实例已停止。
## 目标配置无需保存即可测试
此前新目标和修改后的目标必须先保存才能测试,MongoDB 连接串及其他协议排错因此会提前改变持久配置。现在测试读取点击时的草稿快照,各协议统一支持新建与编辑状态测试;不创建目标、不更新历史、不改变运行中网关映射。已有凭据按稳定账号 ID 复用,其他未完成账号不阻塞所选账号,SSH/SFTP 的新主机身份通过仅本次有效的指纹确认处理。前端保存与测试共用凭据处理规则,后端复用协议测试而不复用保存接口,无数据迁移。具体行为与验证边界见 [未保存配置的连接测试](target-connection-test.md)。
验证时发现默认浏览器测试端口被其他测试进程占用,因此增加 `SERVICEPORT_E2E_PORT`、`SERVICEPORT_E2E_AUTH_PORT` 两个测试环境变量。默认仍是 18081/18082,独立验证可以更换端口并使用单独临时目录,无需停止其他服务;测试脚本及认证页面用例统一读取这两个值。
验证时发现默认浏览器测试端口被其他测试进程占用,因此增加 `LINKPORT_E2E_PORT`、`LINKPORT_E2E_AUTH_PORT` 两个测试环境变量。默认仍是 18081/18082,独立验证可以更换端口并使用单独临时目录,无需停止其他服务;测试脚本及认证页面用例统一读取这两个值。
## 网关账号编辑无需反复下拉
@@ -719,15 +727,15 @@ SSH/K8s 出口维护独立于客户端的共享隧道,每个客户端获取独
## 网关账号独立路由与拖拽连线
一个网关端口需要让不同登录账号访问不同目标服务。只有网关级目标引用无法表达这一需求,因此在账号上增加可选目标引用,同时保留默认目标的继承方式。
一个网关端口需要让不同登录账号访问不同目标服务。只有网关级目标引用无法表达这一需求,因此真实账号各自保存目标引用;Redis、MongoDB、TCP 的空账号也作为明确端点。
| 改动 | 原因与结果 |
| --- | --- |
| 每个账号可指定同协议目标服务及其账号 | 登录后选择该账号的地址、出口、TLS 和上游凭据,实现一个监听端口对应多个后端。 |
| 指定独立目标时必须选择该目标自己的账号 | 不同目标可能使用相同账号 ID;继承默认目标的账号会产生错误映射。 |
| 每次指定目标时必须选择该目标自己的账号 | 不同目标可能使用相同账号 ID;复用网关级默认账号会产生错误映射。 |
| 允许没有默认目标,停用状态下允许逐条保存连线 | 用户可以先创建网关,再逐个账号配置;启动时仍要求所有启用账号的映射完整。 |
| 每个网关账号都有拖拽入口和独立连线 | 仅从整张网关卡片出线无法看出每个账号的去向;即使共享目标,也保留独立连线。 |
| 单条连线只更新选中账号,整张网关卡片仍可统一映射 | 同时满足独立分流和多个账号共用目标的场景,避免修改一条线时覆盖其他账号。 |
| 单条连线只更新选中账号,服务卡片只展示 | 多个账号需要共用目标时也逐条选择,避免整卡操作覆盖其他账号。 |
| 点击两端也能完成连线 | 手机触摸及键盘操作不依赖鼠标拖拽。 |
| 引用检查、备份、同步、会话审计和监控识别账号级目标 | 防止删除仍被使用的目标或资产,并让恢复配置及审计记录反映实际路由。 |
@@ -758,13 +766,21 @@ MongoDB 和 RabbitMQ 的账号路由在本地认证后才连接所选目标,
以上说明记录最终选择。功能限制、恢复范围和协议兼容边界以对应专题文档为准,不把测试用凭据或实际连接密码写入变更记录。
## 网关服务草稿连接测试(2026-09-28)
原因:网关列表虽然已有已保存配置的测试入口,但新建或编辑网关必须先保存才能验证监听配置、网关账号和目标映射。这样会把尚未确认的地址或凭据提前写入配置,也无法测试新建网关。
结果:网关编辑器底部增加“测试连接”,复用目标草稿测试的结果页和诊断脱敏。测试按当前网关账号选择目标映射;Redis 空客户端账号直接使用无认证入口。编辑草稿的省略密码按稳定网关账号 ID 从已保存配置恢复,新建草稿直接使用当前输入。服务端新增 `POST /api/v1/proxies/test`,只在内存合并、解析和验证草稿后调用现有协议测试,不落库、不写历史、不启动或修改运行中的网关;已保存网关列表测试接口保持不变。
兼容与范围:没有配置格式或数据库迁移。其他账号映射不完整时,测试只校验所选账号;保存和启动仍按完整网关配置校验。API 与前端验证使用隔离 fixture,未连接真实生产目标,也未修改真实 `data` 目录。
## 连接配置页签与 URL 参数(2026-09-20)
原因:连接信息、TLS、出口代理及高级设置混在同一页,除 MongoDB 外的连接 URL 不能直接填入配置。现在目标弹窗提供连接信息、TLS/SSL、SSH 隧道/代理、高级及账号分组,切换保留草稿;通用 URL 导入支持现有网络协议,解析账号、地址、数据库和受支持的 TLS 参数。不支持或有歧义的参数明确拒绝,避免静默丢弃配置含义。高级页归集已有设置,不新增超时或保活选项。详细范围见 [连接配置](connection-editor.md)。
兼容:目标/运行时新增可选 database 字段,用于 MySQL 默认数据库和 Redis SELECT;旧配置为空时保持原行为,无表结构迁移。MySQL 客户端显式指定数据库优先,Redis Cluster 仅支持 0。其他数据库沿用 authSource。凭据继续使用现有多账号、加密及留空保留规则,原始 URL 不持久化。只读权限仍在网关设置,代理连接仍选择已有出口。
验证:npm.cmd --prefix web run build 通过;Go 1.25.0 下 go vet ./... 通过。go test ./... -count=1 -timeout=120s 除已知 internal/desktop/TestLocalKeyPersistenceAndLock(系统找不到指定文件)外全部通过;新增模型校验/继承测试单独通过。MySQL/Redis 本地协议 fixture 验证认证后选择数据库、默认库与客户端指定库优先级、服务端拒绝及 DB0 兼容,不代表真实上游服务器验收。通过 SERVICEPORT_E2E_BINARY 指向新构建 artifacts/connection-tabs/serviceport.exe,用随机管理端口和隔离数据库执行完整 Playwright:73 passed、1 failed。新增 URL 参数矩阵、页签草稿、测试请求、保存重开和脱敏用例均通过;SFTP 用例在打开编辑器后超时,自动重试因超时遗留同名数据触发严格定位错误。更换全新隔离数据库和浏览器,SSH/SFTP 两项专项复跑均通过(2 passed);全量首次运行不记为全部通过。测试服务均已停止,真实 data 目录未修改。
验证:npm.cmd --prefix web run build 通过;Go 1.25.0 下 go vet ./... 通过。go test ./... -count=1 -timeout=120s 除已知 internal/desktop/TestLocalKeyPersistenceAndLock(系统找不到指定文件)外全部通过;新增模型校验/继承测试单独通过。MySQL/Redis 本地协议 fixture 验证认证后选择数据库、默认库与客户端指定库优先级、服务端拒绝及 DB0 兼容,不代表真实上游服务器验收。通过 LINKPORT_E2E_BINARY 指向新构建 artifacts/connection-tabs/linkport.exe,用随机管理端口和隔离数据库执行完整 Playwright:73 passed、1 failed。新增 URL 参数矩阵、页签草稿、测试请求、保存重开和脱敏用例均通过;SFTP 用例在打开编辑器后超时,自动重试因超时遗留同名数据触发严格定位错误。更换全新隔离数据库和浏览器,SSH/SFTP 两项专项复跑均通过(2 passed);全量首次运行不记为全部通过。测试服务均已停止,真实 data 目录未修改。
## 集群部署模式改为手动选择(2026-09-20)
@@ -772,7 +788,7 @@ MongoDB 和 RabbitMQ 的账号路由在本地认证后才连接所选目标,
兼容:已有 auto 记录不自动改写或清空,打开后显示待选择并保留节点、名称及账号;保存或草稿测试前必须手动选择。后端保留旧格式读取、运行和 API/备份兼容,不做数据迁移。MongoDB URI 明确 replicaSet 或 directConnection=true 时沿用其指定模式,其他 URI 使用仅在草稿中的待选择标记,未经手动选择不会发出保存或测试请求。详见 [数据库集群](database-clusters.md)。
验证:npm.cmd --prefix web run build、Go 1.25.0 的 go vet ./... 通过,新程序为 artifacts/manual-cluster/serviceport.exe。go test ./... -count=1 -timeout=120s 仅已知 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用 SERVICEPORT_E2E_BINARY、随机端口和隔离数据库完成 75 项 Playwright,首次为 71 passed、4 failed:两个新增用例重复点击了校验时已展开的下拉框;RabbitMQ fixture 指定集群类型后仍断言未知类型;工作区用例按协议取第一条数据,受前面超时残留的 fixture 影响。修正下拉操作、RabbitMQ 改为手动故障切换模式,并让测试按自身目标名称定位。最终对集群、MongoDB、RabbitMQ 和工作区相关 7 项用例在全新隔离数据库复跑全部通过;保留首次全量失败记录,不宣称一次全量零失败。覆盖未选择时阻止保存/测试、选择后节点凭据保留及 MongoDB URI 手动选择。Redis/AMQP 使用本地协议 fixture,MongoDB 草稿测试模拟远端结果,未连接真实上游服务或修改实际 data。
验证:npm.cmd --prefix web run build、Go 1.25.0 的 go vet ./... 通过,新程序为 artifacts/manual-cluster/linkport.exe。go test ./... -count=1 -timeout=120s 仅已知 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用 LINKPORT_E2E_BINARY、随机端口和隔离数据库完成 75 项 Playwright,首次为 71 passed、4 failed:两个新增用例重复点击了校验时已展开的下拉框;RabbitMQ fixture 指定集群类型后仍断言未知类型;工作区用例按协议取第一条数据,受前面超时残留的 fixture 影响。修正下拉操作、RabbitMQ 改为手动故障切换模式,并让测试按自身目标名称定位。最终对集群、MongoDB、RabbitMQ 和工作区相关 7 项用例在全新隔离数据库复跑全部通过;保留首次全量失败记录,不宣称一次全量零失败。覆盖未选择时阻止保存/测试、选择后节点凭据保留及 MongoDB URI 手动选择。Redis/AMQP 使用本地协议 fixture,MongoDB 草稿测试模拟远端结果,未连接真实上游服务或修改实际 data。
## .gitignore 维护(2026-09-20)
@@ -788,9 +804,9 @@ MongoDB 和 RabbitMQ 的账号路由在本地认证后才连接所选目标,
实际验证:
- `npm.cmd --prefix web run build` 与 Go 1.25.0 的 `go vet ./...` 通过;前端构建后恢复 `web/dist/.gitkeep`,随后构建 `artifacts/environment-save/serviceport.exe`。`git diff --check` 通过。
- `npm.cmd --prefix web run build` 与 Go 1.25.0 的 `go vet ./...` 通过;前端构建后恢复 `web/dist/.gitkeep`,随后构建 `artifacts/environment-save/linkport.exe`。`git diff --check` 通过。
- 新增 `environment-save.spec.ts` 的三个用例在修复前程序上均复现旧值提交,修复后均通过:检查实际 PUT 请求、后端回读以及刷新后显示;同时覆盖自定义项目和环境、未保存提示、选择正式环境、清空环境。测试使用隔离数据库,无真实目标服务连接或数据修改。
- 使用上述新程序设置 `SERVICEPORT_E2E_BINARY` 运行全部 91 项 Playwright 用例(允许一次重试):86 项通过,1 项主题用例因浏览器上下文退出而重试通过,4 项失败。失败为账号编辑器在 320×568 下的权限页滚动高度断言,以及 `workspace-features.spec.ts` 的 URL、搜索树导航、表格列设置三个超时;四项都在修复前的 `baseline.exe` 上单独复现相同失败,未因本次环境输入修复而引入。结果位于 `artifacts/.environment-save/` 的 `test-results`、`baseline-layout`、`baseline-url`、`baseline-navigation`。
- 使用上述新程序设置 `LINKPORT_E2E_BINARY` 运行全部 91 项 Playwright 用例(允许一次重试):86 项通过,1 项主题用例因浏览器上下文退出而重试通过,4 项失败。失败为账号编辑器在 320×568 下的权限页滚动高度断言,以及 `workspace-features.spec.ts` 的 URL、搜索树导航、表格列设置三个超时;四项都在修复前的 `baseline.exe` 上单独复现相同失败,未因本次环境输入修复而引入。结果位于 `artifacts/.environment-save/` 的 `test-results`、`baseline-layout`、`baseline-url`、`baseline-navigation`。
- Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 已执行;除 `internal/desktop` 外其他包通过。`TestLocalDataDirectory` 的四个子用例及 `TestLocalKeyPersistenceAndLock` 报 Windows “The system cannot find the file specified.”,全量后端测试未通过;本次未修改后端,协议测试结果不代表真实服务器联调。
## 保存成功后旧环境显示与请求诊断(2026-09-20)
@@ -799,7 +815,7 @@ MongoDB 和 RabbitMQ 的账号路由在本地认证后才连接所选目标,
行为与范围:目标、网关、代理和资产的 POST/PUT 成功后,App 立即将服务端返回的完整资源视图写入对应缓存,并使保存前已启动的刷新失效,防止旧快照覆盖新值。列表、资源树和重开编辑器共享新值;后台刷新仍执行,失败仍报告。管理 API 错误新增独立的请求地址、路径和方法,HTTP、网络和解析错误都保留诊断,中文错误翻译继续生效,查询参数及请求凭据不加入诊断。无 API、数据库格式或迁移变更,资产文件更新时间语义不变。说明见 [项目与环境](project-environments.md) 和 [配置易用性](usability.md)。
实际验证:前端构建、类型检查及 Go 1.25.0 的 `go vet ./...` 通过,生成 `artifacts/environment-save/serviceport-refresh.exe`。保存后刷新失败的三个用例在修改前均失败(后端 `test`、界面「正式」),修改后针对性运行共 7 项通过,覆盖手输环境、三个入口刷新失败后列表与重开编辑器、新增错误定位、中文翻译、网络错误和失败时保留草稿。请求诊断不包含测试文件内容或 CSRF 令牌。使用该新程序及 `SERVICEPORT_E2E_BINARY` 运行完整 95 项 Playwright(不自动重试):90 项通过,5 项失败;其中账号小屏权限布局和三个工作区用例与上一轮已在修复前程序复现的失败相同,主题用例遭遇浏览器退出,单独新开浏览器重跑通过。新增四项在全量运行中均通过,CSRF 过期重试、项目环境筛选及响应式布局也通过;全量仍非全绿。结果位于 `artifacts/.environment-save/refresh-fixed`、`refresh-full`、`refresh-theme`,修改前复现位于 `stale-baseline`。
实际验证:前端构建、类型检查及 Go 1.25.0 的 `go vet ./...` 通过,生成 `artifacts/environment-save/linkport-refresh.exe`。保存后刷新失败的三个用例在修改前均失败(后端 `test`、界面「正式」),修改后针对性运行共 7 项通过,覆盖手输环境、三个入口刷新失败后列表与重开编辑器、新增错误定位、中文翻译、网络错误和失败时保留草稿。请求诊断不包含测试文件内容或 CSRF 令牌。使用该新程序及 `LINKPORT_E2E_BINARY` 运行完整 95 项 Playwright(不自动重试):90 项通过,5 项失败;其中账号小屏权限布局和三个工作区用例与上一轮已在修复前程序复现的失败相同,主题用例遭遇浏览器退出,单独新开浏览器重跑通过。新增四项在全量运行中均通过,CSRF 过期重试、项目环境筛选及响应式布局也通过;全量仍非全绿。结果位于 `artifacts/.environment-save/refresh-fixed`、`refresh-full`、`refresh-theme`,修改前复现位于 `stale-baseline`。
后端 `go test ./... -count=1 -timeout=120s` 已执行:源码包除已知 `internal/desktop` 的 Windows 本地目录/密钥测试失败外均通过;本轮还遇到另一个打包任务清理 `tmp/package-validation/go-build...` 导致 `./...` 枚举报错。首次全量 vet 同样受临时目录影响,源码包单独检查及随后全量 vet 重跑通过。测试使用隔离数据库与模拟失败响应,未改真实资产或连接真实上游服务。
@@ -827,8 +843,241 @@ MongoDB 和 RabbitMQ 的账号路由在本地认证后才连接所选目标,
## 启动时自动分配管理端口(2026-09-18)
原因:固定 8080 容易与本机其他项目冲突。程序未指定 SERVICEPORT_HTTP_ADDR 时,以及 start-dev.cmd 未收到显式地址时,默认使用 127.0.0.1:0,由操作系统在实际绑定监听器时分配空闲端口;不会先探测端口再关闭重开。启动窗口打印实际管理 URL,本地模式的浏览器和首次设置地址继续取自已绑定监听器。
原因:固定 8080 容易与本机其他项目冲突。程序未指定 LINKPORT_HTTP_ADDR 时,以及 start-dev.cmd 未收到显式地址时,默认使用 127.0.0.1:0,由操作系统在实际绑定监听器时分配空闲端口;不会先探测端口再关闭重开。启动窗口打印实际管理 URL,本地模式的浏览器和首次设置地址继续取自已绑定监听器。
范围与兼容:改变管理 HTTP 默认端口;已有数据库、网关监听端口、目标配置和启动备份流程保持原行为,无数据库迁移。显式配置固定端口仍生效,启动脚本不再覆盖 SERVICEPORT_HTTP_ADDR;使用书签、API、反向代理或 Vite 固定代理的用户应显式设置固定地址。随机端口可能在不同启动之间重复,不保证每次都与上一次不同。使用说明同步到 README、Windows 包说明和 .env.example。
范围与兼容:改变管理 HTTP 默认端口;已有数据库、网关监听端口、目标配置和启动备份流程保持原行为,无数据库迁移。显式配置固定端口仍生效,启动脚本不再覆盖 LINKPORT_HTTP_ADDR;使用书签、API、反向代理或 Vite 固定代理的用户应显式设置固定地址。随机端口可能在不同启动之间重复,不保证每次都与上一次不同。使用说明同步到 README、Windows 包说明和 .env.example。
验证:新增 scripts/verify-random-port.mjs,使用新构建的真实可执行文件和隔离数据库,验证两个未配置监听地址的实例同时获得不同非零端口(本次为 11324、11327),打印的 URL 可访问页面和健康接口,重启时显式固定端口生效。scripts/start-dev.test.ps1、前端构建和 Go 1.25.0 的 go vet ./... 通过。go test ./... -count=1 -timeout=120s 仅已知的 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用 SERVICEPORT_E2E_BINARY 指向新程序、两个随机管理端口 8551/8552 和隔离数据库运行完整 Playwright:68 项首次通过,主题测试因浏览器上下文关闭失败后自动重试通过,报告为 1 flaky;运行器正常退出。启动冒烟使用测试主密钥,不读取实际项目数据,也不测试真实上游协议或 Windows DPAPI/浏览器自动打开流程。
验证:新增 scripts/verify-random-port.mjs,使用新构建的真实可执行文件和隔离数据库,验证两个未配置监听地址的实例同时获得不同非零端口(本次为 11324、11327),打印的 URL 可访问页面和健康接口,重启时显式固定端口生效。scripts/start-dev.test.ps1、前端构建和 Go 1.25.0 的 go vet ./... 通过。go test ./... -count=1 -timeout=120s 仅已知的 internal/desktop/TestLocalKeyPersistenceAndLock 失败(系统找不到指定文件),其余包通过。使用 LINKPORT_E2E_BINARY 指向新程序、两个随机管理端口 8551/8552 和隔离数据库运行完整 Playwright:68 项首次通过,主题测试因浏览器上下文关闭失败后自动重试通过,报告为 1 flaky;运行器正常退出。启动冒烟使用测试主密钥,不读取实际项目数据,也不测试真实上游协议或 Windows DPAPI/浏览器自动打开流程。
## 目标保存后保持编辑与已保存连接测试(2026-09-28)
原因:目标编辑器保存成功后关闭编辑页返回目标列表,用户无法直接确认刚保存的账号密码或继续测试;目标底部测试按钮还会提交未保存草稿,连接结果可能与后端实际保存配置不一致。该行为导致 RabbitMQ 等目标的排错难以区分“草稿内容”和“已保存凭据”。
结果:目标保存成功后前端使用服务端返回的资源视图刷新当前编辑器并保持打开。目标连接测试仅调用 `POST /api/v1/targets/{id}/test`,读取后端保存的目标、账号、凭据和出口;新建目标或编辑器存在未保存修改时不发起测试请求,提示先保存。网关草稿测试保持原有行为。新增提示通过中文、英文、日文字典提供。范围仅涉及目标编辑器前端交互和说明文档,无 API、数据库格式或迁移变更。
验证:`vue-tsc --noEmit` 通过;Vite 构建在当前 Windows 环境中因 esbuild 子进程 `spawn EPERM` 未完成。目标 Playwright 回归用例需按新的“先保存、再测试”流程更新后执行;未连接真实 RabbitMQ,也未修改真实 `data` 目录。
## 拖拽连线协议筛选(2026-09-28)
原因:拖拽连线页面同时展示多种协议时,节点和连线过多,用户需要快速查看同协议的网关、目标及账号关联。
结果与范围:`Connections.vue` 增加包含全部协议和 TCP、MySQL、MongoDB、SSH、SFTP、HTTP、HTTPS、SQL Server、Redis、RabbitMQ、PostgreSQL 全部 11 种类型的下拉筛选。筛选同时作用于可见网关、目标、账号和连线;节点按统一协议顺序、名称和 ID 排列。切换筛选只改变当前视图,不修改服务绑定、已有拖拽位置、布局存储格式、后端接口或数据库,因此无需迁移。筛选控件文案加入中文、英文、日文资源字典。
验证:`vue-tsc --noEmit -p web/tsconfig.json`、`git diff --check` 和 8 项 `connection-canvas.test.mjs` 测试通过;Playwright 从正确的 `web` 配置发现 60 个文件中的 138 项测试,`connection-canvas.spec.ts` 逐项检查 11 种协议选项并覆盖三种筛选状态及连线数量。当前 Windows 环境 Vite 构建阶段仍受 esbuild `spawn EPERM` 阻断,未完成 Playwright 浏览器运行;测试未连接真实目标服务。
## 通用连接失败诊断细化(2026-09-28)
原因:RabbitMQ、Redis 及其他协议的连接测试在部分底层错误下只返回 `target_connection_or_protocol`,用户无法判断是端口未监听、网络超时、TLS/协议握手、认证、权限还是查询阶段失败。
结果:后端按已观察阶段返回稳定诊断码。TCP 连接可细分为拒绝、超时、DNS 解析失败、网络不可达和连接重置;其他协议统一区分 TLS、握手、认证、权限、协议会话、查询/健康检查及 HTTP 响应。诊断领域函数提供中文、英文、日文阶段标签和排查建议,当前界面默认使用中文;原始服务端错误仍不回显,避免泄露地址、账号、密码或业务数据。
兼容与范围:保留 `target_connection_or_protocol`、`connection_timeout` 等旧代码;旧历史记录不变,新版后端重新测试后才产生新代码。没有数据库或配置迁移,不改变连接逻辑、凭据保存或目标数据。
验证:Go 1.25.0 下 `internal/gateway` 定向测试通过;前端诊断 Playwright 单测 1/1 通过;`vue-tsc --noEmit` 和 `git diff --check` 通过。测试使用错误和诊断 fixture,未连接真实 RabbitMQ、Redis 或其他生产服务。
## Redis and MongoDB anonymous accounts (2026-09-28)
Redis and MongoDB deployments can disable authentication. Empty target and
gateway account lists now represent that mode across model validation, API
tests, frontend drafts, startup readiness, and protocol serving. MongoDB
anonymous handshakes skip SCRAM while configured gateway accounts retain local
SCRAM and target-account mapping. No schema migration or production data
change is required.
## Redis password-only authentication and protocol audit (2026-09-28)
**Reason.** Redis has a protocol-level password-only form (`AUTH password`) in
addition to ACL username/password authentication. The transport already emitted
the correct one-argument command, but shared model and frontend validation still
rejected an empty username. Treating every database alike therefore prevented a
valid Redis configuration.
**Resulting behavior.** Redis target and gateway accounts now support three
distinct modes: no authentication, password only, and username plus password.
The password-only mode requires an exactly empty username and a non-empty secret;
whitespace is rejected. A blank Redis gateway username maps to the protocol's
`default` identity, and cannot coexist with an explicit `default` account. All
other service types retain their protocol-specific username requirements.
Non-SSH/SFTP targets now also reject `privatekey` and unknown authentication
methods instead of saving fields their protocol handler would ignore.
**Scope and compatibility.** The change is limited to credential validation,
Redis gateway identity lookup, frontend checks/examples, tests and documentation.
Existing named Redis accounts and anonymous Redis/MongoDB configurations remain
unchanged. There is no data migration, schema change, new dependency, production
data modification or authentication fallback after a rejected login.
**Validation.** Go 1.25.0 model, API and Redis protocol fixture tests cover both
target and gateway paths, including direct and Cluster connections. A full
service-type matrix verifies that password-only accounts are accepted only for
Redis. Cluster tests include the `HELLO AUTH` rewrite to the Redis `default`
identity. Frontend validation, startup readiness, connection examples and legacy
draft hydration have focused domain tests. The RESP checks use local `net.Pipe`
fixtures and do not claim live Redis or Sentinel coverage. Detailed command
results and environment limitations are recorded in [testing](testing.md).
## One-to-one gateway-account associations (2026-09-28)
Reason: associating from either service card implicitly selected or replaced
account mappings. Multi-account gateways could therefore overwrite unrelated
routes, while account-free services had no explicit endpoint at all.
Result: both gateway and target service cards are display-only. Every editor,
live-switch and canvas operation selects one gateway account and one target
account, and changes only that route. Redis and MongoDB empty account lists use
synthetic UI-only anonymous account rows; TCP uses synthetic empty-account rows.
Their IDs remain empty strings and no credential record or fake identifier is
created. Existing account, disabled-account, protocol and conflict validation
remains unchanged.
Scope and compatibility: the gateway editor no longer exposes a service-level
default. Editing or saving a legacy mapping with real gateway accounts expands
the top-level target into each account and clears the top-level fields. Empty
Redis, MongoDB and TCP gateway accounts keep the existing top-level persistence
format. Existing stored data and backend compatibility remain readable, so no
database migration is required.
Validation: focused domain tests cover all 11 protocol drafts, legacy expansion,
and empty-account persistence. Playwright fixtures cover per-account mapping,
both display-only service cards, Redis/MongoDB anonymous rows, TCP empty rows,
and all 11 editor protocol choices. These are isolated configuration fixtures,
not live upstream-server validation.
## Live target switches disconnect existing clients (2026-09-28)
Reason: keeping accepted sessions pinned to the previous route made a successful
target switch leave old-target connections active indefinitely. Operators could
not rely on a client reconnect policy to move all traffic to the published route.
Result: `Manager.SwitchTarget` still validates and persists before publishing.
After a successful publish it snapshots every accepted client of that gateway,
marks registered sessions as disconnecting, and cancels/closes them outside the
manager lock. The listener remains active, so reconnecting clients immediately
capture the new route. A persistence or validation failure leaves the old route
and all existing clients untouched. Switching one account changes only that
account's mapping, but disconnects all current clients of the gateway so no old
configuration snapshot remains in use.
Scope and compatibility: this applies to the shared gateway lifecycle and thus
all supported protocols, including TCP and multi-channel SSH/SFTP. It changes the
connection-interruption behavior of the existing target-switch endpoint without
changing its request, response, storage format, or listener address. In-flight
requests, transactions, tunnels, and sessions can be interrupted. There is no
database or configuration migration.
Actual validation: Go 1.25.0 targeted gateway tests for generic relay, TCP, SSH
and multi-account SSH routing passed, as did the API fixture for failed
persistence, successful disconnect, stable listener and reconnect routing. The
complete `internal/gateway` package passed. A broader `internal/api` run retained
unrelated Windows sandbox failures where existing SSH and SOCKS5 fixtures could
not establish their local test connections; the focused target-switch API test
passed. These are isolated loopback protocol fixtures, not live database or
broker validation. Frontend compilation and browser execution were not run for
this development-stage adjustment at the user's request.
## Persistent connections enabled for new targets (2026-09-28)
Reason: all supported target protocols now provide either a standby session,
persistent tunnel or background probe, so requiring users to enable the same
setting on every newly created target adds repetitive setup and leaves targets
without background readiness checks by default.
Result: `createConnectionDraft` initializes `connectionOptions.persistent` to
`true` for all 11 target types. Switching the protocol while creating a target
rebuilds the draft with the same enabled default. The advanced settings text and
labels use the Chinese, English and Japanese dictionaries.
Scope and compatibility: this is a new-draft UI default, not a storage migration.
Existing targets use the value returned by the backend; a missing field or saved
`false` remains off. Direct API clients that omit the field keep the backend's
existing zero-value behavior. Enabling the setting can create periodic target
connections only while a referencing gateway is running.
Validation: all 8 connection-draft tests and all 6 persistent-target component
tests pass; the draft matrix checks the enabled default for all 11 types, confirms
legacy targets do not inherit it, and the persistent matrix verifies every
protocol's mode. Vue type checking also passes.
All 16 single-process frontend domain test files and serial
`go vet -p=1 ./...` pass as well.
The browser fixture cycles through all 11 type controls before saving and reading
back the enabled value, and Playwright discovers it from the web configuration.
The browser run was not executed because Vite cannot spawn esbuild in the current
Windows environment (`spawn EPERM`). Fixture checks do not establish live target
availability.
## All-protocol connection canvas filtering and ordering (2026-09-28)
**Reason.** The canvas filter was limited to MySQL and RabbitMQ, while the
workspace supports TCP, MongoDB, SSH, SFTP, HTTP, HTTPS, SQL Server, Redis and
PostgreSQL as well. Users also need the same project and environment scope
selection that applies to the other workspace pages.
**Resulting behavior.** The connection canvas uses one dropdown containing all
supported connection types plus an all-types option. The existing shared
`WorkspaceScopeFilter` remains the only project/environment selector; its
selection is applied before rows reach the canvas. Visible gateways and targets
are sorted by the shared protocol order, then by name and ID for stable
placement. Existing manually saved node positions remain respected; reset and
new layouts follow the type order.
**Scope and compatibility.** This is a frontend-only view, sorting and
translation change. Filtering and ordering do not alter bindings, credentials,
backend requests, layout storage format or database data. No migration is
required. All new protocol labels are present in the Chinese, English and
Japanese dictionaries.
**Validation.** The in-memory canvas test covers all 11 protocol values,
unknown types placed last, and stable name ordering. The Playwright fixture
checks the dropdown options and MySQL/RabbitMQ filtering. It uses isolated
fixtures and does not connect to live upstream services.
## MySQL slow-query and full-scan highlighting (2026-09-29)
**Reason.** MySQL command audit stored result rows and server status values but
did not expose execution duration or index-risk signals in the command list.
Operators could not quickly identify a slow response or a server report that no
index, or no suitable index, was used.
**Resulting behavior.** The gateway records elapsed duration observed from
accepted command to complete, failed or interrupted response. It accumulates
MySQL `SERVER_STATUS_NO_INDEX_USED` and
`SERVER_STATUS_NO_GOOD_INDEX_USED` flags from OK and EOF packets. Command audit
desktop and mobile views, plus query-result details, highlight queries at or
above 3000 ms as slow, no-index reports as a red full-scan risk, and no-good-
index reports as a warning. User-visible labels are available in Chinese,
English and Japanese.
**Scope and compatibility.** Command-list reads now left-join the existing
one-to-one encrypted result summary, avoiding per-row database reads. The new
optional JSON fields require no SQLite migration, and old records remain
readable with performance metrics unavailable. No `EXPLAIN` query is issued:
the normal response cannot identify the exact index, and an automatic explain
would add load, permissions and transaction side effects. The full-scan label
is therefore explicitly a risk signal rather than a guaranteed execution-plan
claim.
**Validation.** Protocol fixture tests were added for OK and EOF status-bit
capture and observed duration, a store test covers list summary hydration, and
a frontend domain test fixes the slow threshold at 3000 ms and covers risk
classification and duration formatting. At the user's request for this
development-stage adjustment, no compilation, test suite, server or browser was
run. Static source review and `git diff --check` are the claimed validation; no
live MySQL server or real data was used.
## 配置库与日志分片库分离(2026-09-29)
原因:审计、命令结果和连接事件只需要追加与查询,却会持续放大同时承载配置的 `data/linkport.sqlite`。大体积单库会增加配置备份、检查和空间回收成本;为只增不改的日志引入外部日志服务,又会破坏 LinkPort 的单程序、低运维部署方式。
实现结果:配置状态保留在 `data/linkport.sqlite`,命令审计、命令结果与输出、连接事件写入 `data/audit/*.sqlite`。活动日志分片使用 SQLite WAL;写入前按 `.sqlite`、`-wal`、`-shm` 当前总大小加本次估算值判断是否轮转。命令及其结果、输出固定在同一分片,不为满足上限而拆开。轮转依次执行 WAL checkpoint、切换 `journal_mode=DELETE`、关闭句柄、清理边车文件,再按同日已有最大 `partNNNN` 加一归档并创建新活动分片。默认阈值为 `256 MiB`,UI 可在 `16 MiB` 到 `4 GiB` 范围调整。完整行为见 [配置库与日志库分离](log-storage.md)。
迁移:旧命令、结果、输出和事件按明确 ID 小批复制;目标端对持久化字段进行一致性校验并先提交,之后源端才按该批 ID 删除并核对影响行数。迁移使用 `pending`/`done` 标记保持可重试:旧表清空并删除后执行一次 `VACUUM`,只有压缩成功才完成 `done` 并清除 `pending`;压缩失败时,下一次启动会根据 `pending` 再次尝试,不重复或扩大源端删除。
范围与兼容:日志写入、查询、导出和清理已按分片工作,分片上限保存在配置库并由 UI 调整;旧配置缺少阈值时使用 `256 MiB`,调整阈值不重写旧分片。配置快照只包含 `data/linkport.sqlite`,不包含日志分片。完整运维备份必须同时包含配置库和所有日志分片;迁移后旧版本不能从新分片读取日志,回退必须恢复迁移前的完整数据目录,不能只回退程序。现有归档文件不会被轮转覆盖。
验证状态:日志拆分已有运行时代码。用户运行前端构建时,`vue-tsc` 在 `OpsPanel.vue` 的日志大小边界计算处报告可选值参与除法的类型错误;该处已改为显式存在性分支。静态审查还补充了仅剩 `pending` 标记时的重启恢复用例,修正事件旧表断言遗漏的查询错误检查,并将便携密钥迁移测试名称调整为其实际覆盖范围。修复后按用户要求未重跑类型检查、构建或测试,未启动服务,也未执行真实数据库迁移;当前仅完成实现源码与文档的一致性静态审查及 `git diff --check`。主代理此前已只读列出 `data` 下相关文件,本轮没有修改真实数据。
## LinkPort 非 Go 项目名称收口(2026-09-29)
原因与结果:项目名称、命令目录、构建注入路径、图标路径和测试记录仍有旧名称残留,可能使构建脚本引用错误入口,也会让当前文档与产品名称不一致。本次将 `scripts/**`、`deploy/**`、根目录 README 和 `docs/**` 中的当前产品、命令及环境变量名称统一为 `linkport`、`LinkPort` 和 `LINKPORT`。
范围与兼容:只调整非 Go 文本和脚本中的项目名称引用,不修改 `web/**`、`go.mod`、任何 Go 源码、真实数据、历史备份、二进制或第三方产物。脚本现在引用 `./cmd/linkport` 与 `linkport/internal/buildinfo`,文档引用 `web/public/linkport.svg`;这些路径需要与其他协作者负责的命令目录、模块路径和前端资产重命名一同落地,单独使用本批脚本会在对应文件尚未重命名时失败。配置和数据格式不受影响。
验证状态:按用户要求未运行构建、测试或服务。仅使用只读文本搜索复核指定范围,三种大小写形式均无残留;未读取或修改真实 `data`。
+1 -1
View File
@@ -1,6 +1,6 @@
# 配置备份与资产文件恢复
从侧边栏「配置管理」的「配置备份」,点击「导出配置」,填写至少 12 字节的导出口令后点击「确认导出」。下载的 `serviceport-backup.json` 固定为加密文件,包含当前目标服务、网关、代理出口、凭据和资产管理中的全部文件,包括尚未被引用的资产。
从侧边栏「配置管理」的「配置备份」,点击「导出配置」,填写至少 12 字节的导出口令后点击「确认导出」。下载的 `linkport-backup.json` 固定为加密文件,包含当前目标服务、网关、代理出口、凭据和资产管理中的全部文件,包括尚未被引用的资产。
资产原始文件、上传文件名、备注和引用关系一并加密保存。恢复时填写导出时设置的口令,资产文件自动恢复至资产管理;引用同一资产的目标、账号和代理继续通过资产 ID 关联,无需另外逐个下载或上传文件。
+40
View File
@@ -0,0 +1,40 @@
# 服务连线画板
「拖拽连线」使用可自由摆放节点的画板,解决固定两列视图无法整理复杂连接关系的问题。网关服务使用蓝色,目标服务使用绿色,同时显示服务类型文字;颜色随明暗主题调整。
## 操作
- 拖动卡片顶部移动服务;聚焦顶部后可以用方向键移动,Shift + 方向键加大步长。连线端点随实际账号按钮和节点位置移动。
- 拖动空白处或滚动滚轮平移。Ctrl / Command + 滚轮围绕指针缩放,也可使用放大、缩小按钮,范围为 20%–160%。
- 「适应画板」根据全部节点边界调整视角;超大布局受最小缩放限制,可以继续平移查看。「重置布局」恢复网关和目标的默认两列排列。
- 「全屏画板」展开整个画板,保留移动、缩放、连线及服务操作;点击「退出全屏」或按 Esc 返回。浏览器不提供全屏接口或拒绝请求时,使用窗口内铺满显示。全屏时隐藏画板下方重复的关联明细,为节点留出空间。
- 画板顶部的「连接协议」下拉框支持全部协议以及 TCP、MySQL、MongoDB、SSH、SFTP、HTTP、HTTPS、SQL Server、Redis、RabbitMQ、PostgreSQL 全部 11 种目标类型。筛选会同时隐藏不匹配的网关、目标、账号和连线;切换筛选不会修改绑定或布局,切回「全部协议」即可恢复完整视图。
- 修改绑定时,只能拖动或点击网关账号行,再选择目标账号行。网关服务卡片和目标服务卡片都只用于展示,不能绕过账号端点直接关联。Redis、MongoDB 的空账号列表显示「无认证账号」,TCP 显示「空账号」;这些 UI 端点的账号 ID 都是空字符串,保存时沿用既有无账号格式,不创建虚构账号。移动卡片顶部只改变布局。运行中保存连线后监听保持运行,该网关的现有客户端连接会断开,客户端重连后使用新目标;保存失败则保留连接。原有协议、账号、权限校验以及测试、启动、停止、断开和维护目标操作继续生效。
节点位置和视角按当前项目、环境范围存入本浏览器的 localStorage,刷新或切回该范围后恢复;只保存资源 ID 和坐标,不保存服务配置或凭据。不可用或损坏的浏览器存储会回退到默认布局。清除浏览器数据会清除布局。
## 实现范围与兼容性
`web/src/Connections.vue` 负责卡片、真实端点测量、SVG 连线和全屏;`composables/useConnectionCanvas.ts` 管理交互及本地偏好;`domain/connectionCanvas.ts` 提供布局和坐标计算;`App.vue` 传入项目/环境范围。界面把可操作入口收窄为账号端点,但未修改后端接口、数据库结构及已有服务数据,无迁移步骤。
## 验证(2026-09-28)
- `vue-tsc --noEmit -p web/tsconfig.json` 通过。
- `node web/tests/connection-canvas.test.mjs` 的 8 项测试通过,覆盖 11 种协议筛选与排序、布局、缩放锚点、小屏适配、存储清洗、范围隔离、首次测量/视口自适应和拖动清理;使用内存节点与事件 fixture,不连接真实服务。
- Playwright 从 `web` 配置成功发现 60 个文件中的 138 项测试;`connection-canvas.spec.ts` 逐项检查全部协议下拉选项,并覆盖 MySQL、RabbitMQ、全部协议三种可见状态及账号连线。当前环境禁止创建 esbuild 子进程(`spawn EPERM`),`npm.cmd --prefix web run build` 在 Vite 阶段失败,无法生成新的嵌入式二进制并执行浏览器回归;配置夹具不代表真实服务验证。
## 协议筛选(2026-09-28)
原因:拖拽连线页面在项目中同时存在多种协议时,节点和连线过多,难以快速定位同协议的网关与目标。
结果与范围:新增包含全部协议及 11 种目标类型的下拉筛选。项目和环境继续使用 `App.vue` 的共享工作区筛选;筛选后的网关和目标按统一协议顺序排列,同协议再按名称和 ID 排列。协议筛选只改变当前页面的节点、账号和 SVG 连线可见性,选择、拖拽、断开、测试、启动、停止和目标维护仍使用原有配置与接口;筛选状态不写入后端,已有拖拽位置、布局存储键和服务数据不变,无迁移要求。新增控件文案通过中文、英文、日文字典提供。
验证:`connection-canvas.test.mjs` 的 8 项测试通过,覆盖 11 种筛选值及协议排序;Playwright 配置成功发现相关用例,夹具逐项检查下拉选项,并覆盖 MySQL、RabbitMQ 和全部协议三种可见状态。因上述 Vite `spawn EPERM` 限制未实际执行浏览器用例。测试只使用隔离配置夹具,不连接真实目标服务。
## 网关账号到目标账号的一对一关联(2026-09-28)
原因:网关或目标服务整卡关联会隐式选择一端或两端的账号,多账号场景可能覆盖其他账号映射;无认证目标和无账号 TCP 又没有可拖放端点,导致不同协议的操作语义不一致。
结果与范围:网关和目标服务卡片均为展示节点,点击或拖放必须从一个网关账号端点落到一个目标账号端点,每次只更新所选网关账号。Redis、MongoDB 的空账号列表生成 UI 专用「无认证账号」端点,TCP 生成「空账号」端点;两端均提交空账号 ID,后端继续使用既有顶层字段保存无账号映射。真实账号、账号禁用校验、协议匹配和映射冲突校验保持不变。没有数据库或配置迁移。
验证:补充画布 Playwright 配置夹具,逐一切换 11 种协议确认网关编辑器没有服务级关联入口,并覆盖真实账号逐条映射、MongoDB/Redis 无认证端点、TCP 空账号端点、两侧整卡不可关联以及空账号 ID 持久化;测试使用隔离配置,不连接真实服务。
+15 -5
View File
@@ -1,5 +1,15 @@
# 连接路径与失败阶段
## 通用网络与协议错误细化(2026-09-28)
此前除少数已结构化协议外,连接测试可能只返回 `target_connection_or_protocol`,无法区分端口拒绝、协议握手失败、权限拒绝和查询失败。现在所有协议复用已执行阶段进行分类:TCP 连接、TLS、协议握手、认证、权限、协议会话、查询/健康检查和 HTTP 响应。
TCP 连接还会在底层错误可安全识别时细分为 `target_connect_refused`(例如 Windows 10061)、`target_connect_timeout`、`target_connect_dns`、`target_connect_unreachable` 和 `target_connect_reset`。例如 RabbitMQ 的 `192.168.9.9:5672` 被拒绝时,重新测试会显示目标 TCP 连接被拒绝,而不是泛化的 `target_connection_or_protocol`;这表示尚未进入 RabbitMQ 用户密码或 vhost 认证。
管理 API 和前端只使用稳定诊断码、阶段和排查提示,不透传服务端原始错误,以避免泄露地址、账号、密码或业务数据。旧诊断码保持兼容;历史记录不会自动重分类,需使用新版后端重新测试。
验证覆盖拒绝、重置、不可达、超时、协议、认证、权限和 HTTP 阶段,使用 Go 错误/诊断 fixture,不代表真实目标服务联调。
## SSH 本地主机密钥校验(2026-09-23)
补充:SSH 服务端发送的是主机公钥,指纹比较由本地 SSH 库完成;不匹配时现在明确返回 `ssh_host_identity`,显示库的原始安全判定 `ssh: host key mismatch`,并在阶段记录中标记身份校验失败,账号认证尚未执行。缺失/无效的已确认密钥另用 `ssh_host_key_unconfirmed`,且目标 TCP 尚未尝试。只允许该精确的无凭据库错误进入 `rawError`;任意服务端原始文本可能含密码、令牌、地址或业务数据,不透传到管理 API、审计或复制的脱敏诊断。
@@ -28,10 +38,10 @@ MongoDB 的认证库使用 `authSource`,不是 MySQL/Redis 专用的 `database
网关测试按“网关账号 → 目标账号”选择实际目标。拖拽到单个网关账号后,即使网关默认 targetId 为空,也使用该账号映射中的目标配置计算路径;多账号先选择测试账号。不能因为网关默认地址为空就显示为“直连 → 未配置地址/端口”。目标/账号缺失时阻止发送测试,连接资产缺失则明确显示不可用,不能悄悄回退直连。
- 直连:`ServicePort → 直连 → 目标地址:端口`。
- SSH/SOCKS5:`ServicePort → 连接资产的主机:端口 → 目标地址:端口`。
- sing-box:`ServicePort → sing-box <协议类型>(受控本地 SOCKS5 入口)→ 目标地址:端口`。资产 JSON、binary 路径、随机本地凭据和 stderr 不进入路径或复制诊断。
- K8s:`ServicePort → Kubeconfig 资产名/context → 命名空间/资源 → 容器内服务端口 → MongoDB 认证库(适用时)`。
- 直连:`LinkPort → 直连 → 目标地址:端口`。
- SSH/SOCKS5:`LinkPort → 连接资产的主机:端口 → 目标地址:端口`。
- sing-box:`LinkPort → sing-box <协议类型>(受控本地 SOCKS5 入口)→ 目标地址:端口`。资产 JSON、binary 路径、随机本地凭据和 stderr 不进入路径或复制诊断。
- K8s:`LinkPort → Kubeconfig 资产名/context → 命名空间/资源 → 容器内服务端口 → MongoDB 认证库(适用时)`。
例如 `wall-e-test/statefulset/mongo-shopee` 会显示所选资源,不编造实际 Pod 名或 IP。K8s 不通过目标 IP 路由,非 TLS 路径不显示目标 IP;TLS/HTTPS 显示证书域名。资产缺失显示名称不可用,不用长 ID 代替可读名称。
@@ -60,7 +70,7 @@ MongoDB 的认证库使用 `authSource`,不是 MySQL/Redis 专用的 `database
| 诊断代码 | 已观察的边界 | 不能据此断言 |
| --- | --- | --- |
| `sing_box_start` | 外部 binary 已定位并尝试启动,或启动/本地端口就绪失败 | 不能证明远程代理或目标可达 |
| `sing_box_connect` | serviceport 连接受控 `127.0.0.1` SOCKS5 入口 | 不能证明 outbound 建链成功 |
| `sing_box_connect` | linkport 连接受控 `127.0.0.1` SOCKS5 入口 | 不能证明 outbound 建链成功 |
| `sing_box_negotiation` | 本地 SOCKS5 方法协商 | 不是远程协议或目标账号认证 |
| `sing_box_authentication` | 随机生成的本地 SOCKS5 凭据响应 | 不是资产中的代理凭据,也不是目标账号结论 |
| `target_connect` | sing-box 返回目标 CONNECT 结果 | 成功后仍需目标 TLS、协议握手、认证和查询 |
+4 -4
View File
@@ -22,11 +22,11 @@
MongoDB 使用 Kubernetes 固定资源时,部署模式须选择「单节点」,不启用副本集自动发现,也不会自动识别主节点;这不要求真实 MongoDB 是独立实例。本次不批量迁移既有资产或引用,兼容与本轮验证边界见 [统一资产与目标连接](unified-assets.md)。
sing-box 编辑器使用密码型输入保存一个单 outbound JSON;不解析 URI,也不允许填写 binary 路径。原始输入应用后立即清空,已保存正文不回填,只显示协议摘要。64 KiB、宿主能力限制、`SERVICEPORT_SING_BOX`/`PATH` 查找和“检查配置”与目标真实连接的边界见 [sing-box 连接资产](sing-box.md)。
sing-box 编辑器使用密码型输入保存一个单 outbound JSON;不解析 URI,也不允许填写 binary 路径。原始输入应用后立即清空,已保存正文不回填,只显示协议摘要。64 KiB、宿主能力限制、`LINKPORT_SING_BOX`/`PATH` 查找和“检查配置”与目标真实连接的边界见 [sing-box 连接资产](sing-box.md)。
## 独立 URL 参数与提交一致性
完整 URL 下方提供独立“URL 参数”输入区,示例为 MySQL `charset=utf8mb4&tls=true`、PostgreSQL `application_name=serviceport&sslmode=verify-full`。参数区使用与完整 URL 一致的协议白名单,把值规范化到已有字段及 `connectionOptions`,不保存任意驱动参数字符串,也不透传未支持选项。
完整 URL 下方提供独立“URL 参数”输入区,示例为 MySQL `charset=utf8mb4&tls=true`、PostgreSQL `application_name=linkport&sslmode=verify-full`。参数区使用与完整 URL 一致的协议白名单,把值规范化到已有字段及 `connectionOptions`,不保存任意驱动参数字符串,也不透传未支持选项。
- 点击“应用参数”后同步表单;测试和保存也会先应用待处理输入。显式参数覆盖对应字段,缺省的数据库、TLS、认证来源、压缩或集群参数保持表单当前值;删除 `charset` / `application_name` 则恢复协议默认值。参数应用后可继续在其他页签修改字段,回读文本由当前结构化字段生成。
- 重复参数(包含大小写变体)、冲突同义参数、非法编码和未支持项统一报错;解析失败不部分修改配置,不发送测试或保存请求。百分号编码用于特殊字符;允许的空值遵循协议语义,例如 MySQL `database=` 清除默认库、MongoDB `compressors=` 关闭压缩,`charset=` 无效。
@@ -55,7 +55,7 @@ sing-box 编辑器使用密码型输入保存一个单 outbound JSON;不解析
MySQL 的默认数据库用于测试连接和客户端未指定数据库的新连接;客户端指定数据库优先。Redis 在认证成功后发送 `SELECT`,服务端拒绝时连接失败,空值/0 保持默认库;Redis Cluster 仅支持 0。PostgreSQL、SQL Server 继续使用现有 `authSource` 字段保存默认库;RabbitMQ 的 vhost 用于连接测试,网关客户端仍自行指定 vhost。
MySQL `charset` 映射为上游握手的 `utf8mb4_general_ci` / `utf8_general_ci` / `latin1_swedish_ci` / `ascii_general_ci`;空设置沿用原来的 utf8mb4。PostgreSQL `application_name` 进入上游启动参数,缺省为 `serviceport`。客户端后续会话语句仍可能修改这些设置,它们不是权限策略。
MySQL `charset` 映射为上游握手的 `utf8mb4_general_ci` / `utf8_general_ci` / `latin1_swedish_ci` / `ascii_general_ci`;空设置沿用原来的 utf8mb4。PostgreSQL `application_name` 进入上游启动参数,缺省为 `linkport`。客户端后续会话语句仍可能修改这些设置,它们不是权限策略。
## 高级选项能力矩阵
@@ -67,7 +67,7 @@ MySQL `charset` 映射为上游握手的 `utf8mb4_general_ci` / `utf8_general_ci
| `queryTimeoutSeconds` | 仅 PostgreSQL | 0–86400;非零作为新上游会话 `statement_timeout`(转换为毫秒)。0 不覆盖服务端默认。由 PostgreSQL 执行查询超时,客户端仍可调整;没有把原始套接字读超时冒充多协议查询超时。 |
| `idleTimeoutSeconds` | 仅 MongoDB 网关会话 | 0–86400;0 关闭。在认证后等待下一条完整请求时设置下游读期限,收到请求后立即清除,不中断正在执行的查询。不是连接池回收,也不是目标测试的空闲设置。 |
| `keepAliveSeconds` | 全部目标协议的直连 / SOCKS5 / SSH 底层 TCP | 缺省/`null` 使用 Go / 系统默认,0 禁用,1–3600 为覆盖值;“使用默认”会明确保存为 `null`,不保留之前的覆盖。Kubernetes port-forward 不支持,必须使用默认。不是数据库心跳、SSH 应用层探测或会话有效性保证。 |
| `persistent` | 单节点 MySQL/MongoDB/PostgreSQL/Redis 的备用会话,或 SSH/K8s 共享隧道 | 布尔值,默认 false。关联运行网关时无客户端也预建、心跳及重连;会话借出后不回收,停服释放。明确认证/身份拒绝暂停重试。集群不预认证,原 K8s 固定资源限制保持。详见 `persistent-targets.md`。 |
| `persistent` | 全部目标协议;单节点 MySQL/MongoDB/PostgreSQL/Redis 使用备用会话,SSH/K8s 使用共享隧道,其余使用后台探测 | 布尔值;11 种类型的新建界面草稿默认 `true`,旧目标沿用保存值。关联运行网关时无客户端也预建或探测、心跳及重连;会话借出后不回收,停服释放。明确认证/身份拒绝暂停重试。集群不预认证,原 K8s 固定资源限制保持。详见 `persistent-targets.md`。 |
旧配置省略 `connectionOptions` 时不改变连接行为。高级页清空数值恢复默认语义;编辑已保存目标或网关时协议保持锁定。运行中保存的新值只影响随后建立的连接,已有连接保持接受时的快照。若运行配置已被其他操作更新,保存返回 409,刷新后再编辑。
+1 -1
View File
@@ -12,7 +12,7 @@ The `diagnostics` object accompanies successful responses and HTTP 422 failures:
```json
{
"configuredPath": ["serviceport", "ssh", "target"],
"configuredPath": ["linkport", "ssh", "target"],
"elapsedMs": 12.5,
"stages": [
{"id":"ssh_forward_connection","status":"success","elapsedMs":1.2,"attempt":1},
+5 -5
View File
@@ -5,21 +5,21 @@
在线下载采用流式响应;恢复预览上传限 32 MiB。浏览器可检查结构完整性、库内密钥匹配并显示资源数量、SQLite user_version、格式及 SHA256。大文件用命令检查:
```powershell
.\serviceport.exe inspect-database "D:\backup\serviceport.sqlite"
.\linkport.exe inspect-database "D:\backup\linkport.sqlite"
```
## 完整恢复
1. 检查备份文件并保存 SHA256;在界面勾选理解完整替换风险后显示恢复指令。
2. 停止整个 ServicePort 程序,不只是停止网关;保持与原服务相同的工作目录、SERVICEPORT_DATA 等数据路径配置。
2. 停止整个 LinkPort 程序,不只是停止网关;保持与原服务相同的工作目录、LINKPORT_DATA 等数据路径配置。
3. 执行(将路径、SHA256 替换为实际值):
```powershell
.\serviceport.exe restore-database "D:\backup\serviceport.sqlite" --confirm-sha256 <SHA256>
.\linkport.exe restore-database "D:\backup\linkport.sqlite" --confirm-sha256 <SHA256>
```
4. 命令获取与服务相同的独占锁;运行中的服务持锁时拒绝恢复。私有暂存并再次核验备份,防止确认后文件变化。先在线备份当前数据库到 `数据目录/backups/before-restore-时间-随机值/serviceport.sqlite`,完整性检查通过且源/目标 schema 指纹与 user_version 一致后,再由 SQLite 备份恢复事务原子替换页面,不重命名被锁 inode,也不直接覆盖 WAL。失败时保留已生成的保底备份,并打印位置。
5. 重新启动 ServicePort;账号、凭据、审计、TLS 及同步设置来自备份。网关启动行为按备份中的启用状态执行。跨版本 schema 不一致时拒绝恢复,先使用匹配版本处理,不能绕过检查。
4. 命令获取与服务相同的独占锁;运行中的服务持锁时拒绝恢复。私有暂存并再次核验备份,防止确认后文件变化。先在线备份当前数据库到 `数据目录/backups/before-restore-时间-随机值/linkport.sqlite`,完整性检查通过且源/目标 schema 指纹与 user_version 一致后,再由 SQLite 备份恢复事务原子替换页面,不重命名被锁 inode,也不直接覆盖 WAL。失败时保留已生成的保底备份,并打印位置。
5. 重新启动 LinkPort;账号、凭据、审计、TLS 及同步设置来自备份。网关启动行为按备份中的启用状态执行。跨版本 schema 不一致时拒绝恢复,先使用匹配版本处理,不能绕过检查。
源必须是完整、独立的普通 SQLite 文件,拒绝符号链接及带 WAL/SHM/journal 的输入。不会在检查时初始化、迁移或生成密钥。只能在已初始化的当前数据库上恢复。API 限管理会话并要求 CSRF,不允许 API token 导出或检查全库。Web 端不提供在线覆盖数据库的操作;真正恢复必须显式停服执行命令,避免在活动连接、登录会话和 TLS 上下文存在时换库。
+5 -5
View File
@@ -1,4 +1,4 @@
# serviceport 开发计划
# linkport 开发计划
本文保留历史设计与阶段计划,部分协议数量及“未包含”描述已随实现演进。当前模块职责和代码入口以 [模块说明](modules.md) 为准,已实现功能及使用方式参见 [README](../README.md) 与 [网关账号与运维](gateway-operations.md)。
@@ -55,7 +55,7 @@
## 3. 密码、密钥与登录认证
- 开发阶段默认关闭个人登录;设置 SERVICEPORT_AUTH_ENABLED=true 后,通过环境变量或 Windows 首次设置页面初始化管理员。登录密码使用 bcrypt,不保存可恢复副本,不提供原文查看。
- 开发阶段默认关闭个人登录;设置 LINKPORT_AUTH_ENABLED=true 后,通过环境变量或 Windows 首次设置页面初始化管理员。登录密码使用 bcrypt,不保存可恢复副本,不提供原文查看。
- 提供本机交互式密码重置命令,重置后注销已有会话。
- 使用服务端会话、HttpOnly Cookie 和 CSRF 校验。
- 目标密码、网关密码、SOCKS5 密码、SSH 上游私钥及私钥口令使用 AES-GCM 加密保存,每次加密使用独立随机 nonce。
@@ -213,7 +213,7 @@
### Windows 双击启动(0.1.1)
- 已实现无环境凭据启动:自动打开浏览器,首次页面设置管理员,后续复用账号。
- 本地数据路径按“显式 `SERVICEPORT_DATA` → 当前目录已有 `data/serviceport.sqlite` → AppData 已有库”选择,都没有才在当前目录 `data` 新建;不自动搬迁或合并。选中库出错不跳过,手动主密钥模式不搜索 AppData。随机主密钥使用当前 Windows 用户 DPAPI 加密保存,独占锁阻止多个进程同时启动。
- 本地数据路径按“显式 `LINKPORT_DATA` → 当前目录已有 `data/linkport.sqlite` → AppData 已有库”选择,都没有才在当前目录 `data` 新建;不自动搬迁或合并。选中库出错不跳过,手动主密钥模式不搜索 AppData。随机主密钥使用当前 Windows 用户 DPAPI 加密保存,独占锁阻止多个进程同时启动。
- 自动生成一年有效的 localhost TLS 证书;保持服务器环境变量部署兼容。
- 本地初始化仅监听回环地址,校验 Host、Origin 和一次性随机 token;初始化完成后关闭设置入口。
- 错误保留在控制台窗口;提供打包后的首次启动、浏览器设置、密钥持久化和重启验证脚本 `scripts/verify-desktop.mjs`。
@@ -228,7 +228,7 @@
- 旧数据通过事务自动拆分为 targets/gateways 两张加密配置表,引用有外键约束;迁移保留原网关 ID、密码、SSH 主机身份和启用状态。回滚版本须恢复升级前备份。
- 新增 /api/v1/targets CRUD、连接测试、密码查看和 SSH 指纹确认;网关仍使用 /api/v1/proxies。旧版内联创建请求兼容导入为独立目标,但网关更新禁止修改目标参数;新界面只使用 targetId 创建网关。
- 管理界面增加浅色、深色、海蓝及跟随系统主题,浏览器持久化选择。
- 开发阶段默认 SERVICEPORT_AUTH_ENABLED=false:跳过管理员创建与登录,直达控制台;仅限回环访问,保留 Host/Origin/CSRF 校验。设置 true 恢复 bcrypt 个人登录、会话及密码查看重新认证。远程部署必须显式开启认证。
- 开发阶段默认 LINKPORT_AUTH_ENABLED=false:跳过管理员创建与登录,直达控制台;仅限回环访问,保留 Host/Origin/CSRF 校验。设置 true 恢复 bcrypt 个人登录、会话及密码查看重新认证。远程部署必须显式开启认证。
### 目标压缩与逐命令审计
@@ -242,7 +242,7 @@
### 审计分库、导出与连接编排
- 配置固定在 `data/serviceport.sqlite`;命令审计与连接/安全事件写入 `data/audit/YYYY-MM-DD.sqlite`,按 UTC 日期分库,同日分别使用 commands/events 表。
- 配置固定在 `data/linkport.sqlite`;命令审计与连接/安全事件写入 `data/audit/YYYY-MM-DD.sqlite`,按 UTC 日期分库,同日分别使用 commands/events 表。
- 启动自动迁移旧日志,命令密文保持不变,迁移中断可重试;升级前停止服务并备份整个 data 目录。日志保留 7 天,过期分库清理;查询和导出跨分库进行。
- 命令审计支持网关、协议、目标、来源 IP、决策、操作类型、关键词和时间筛选;连接与审计支持服务、来源 IP、事件类型、关键词和时间筛选。两页均支持游标分页和按筛选条件导出全部 CSV,不受当前页限制。
- CSV 使用 UTF-8 BOM、正确转义及公式防护,通过临时文件下载,避免完整导出占用服务端内存。
+8 -10
View File
@@ -1,16 +1,14 @@
# 网关账号与运维
网关支持先保存后关联目标。创建时可以省略 `targetId` 和
`targetAccountId`;绑定时指定默认目标账号,或给每个启用的网关账号单独指定目标账号。更新时发送 `targetId: ""`
解除关联,同时清空目标账号。未绑定网关不能启动或测试连接。网关运行中仍可打开配置并保存监听、认证账号、来源限制和访问策略;新连接使用新配置,已有连接保持接受时的配置。监听地址变化会先绑定新端口,绑定或保存失败不影响原监听。除 Redis 可在上游关闭认证时保存零账号外,目标服务至少保留一个账号。Redis 一旦添加账号,默认 ACL 账号请显式填写 `default` 并选择对应账号。网关客户端账号、用户名和密码始终必填;Redis 零账号只表示网关连接上游时不发送 `AUTH`。
网关支持先保存后逐账号关联目标。创建时可以省略目标映射;真实网关账号分别保存自己的 `targetId` 和 `targetAccountId`。更新时清空这些字段即可解除对应映射,管理界面的“断开连接”会清空该网关的全部账号映射。未完成所有启用账号映射的网关不能启动,缺少映射的账号也不能测试连接。网关运行中仍可打开配置并保存监听、认证账号、来源限制和访问策略;新连接使用新配置,已有连接保持接受时的配置。监听地址变化会先绑定新端口,绑定或保存失败不影响原监听。Redis、MongoDB 可在关闭认证时保存零账号,TCP 固定无协议账号;其他目标服务至少保留一个账号。Redis 一旦添加账号,默认 ACL 账号请显式填写 `default` 并选择对应账号。Redis、MongoDB 网关也可保存零账号以允许无认证客户端,其他账号型网关仍要求用户名和凭据。
运行中可通过「切换目标」或「拖拽连线」更换绑定目标及共用账号,监听保持运行,已有连接继续使用原目标。详见 [运行中切换目标](live-target-switch.md)。
运行中可通过「切换目标」或「拖拽连线」更换某个网关账号的一对一目标账号映射。保存成功后监听保持运行,但该网关的全部现有客户端连接会断开;客户端重连后使用新快照,被切换账号直接连接新目标。保存失败不会断开连接。详见 [运行中切换目标](live-target-switch.md)。
## 多账号与权限
### 账号映射与连接测试
关联单位通常为“网关账号 → 目标服务中的目标账号”。拖动单个网关账号只修改该账号的 `targetId/targetAccountId`,不会改变其他网关账号的映射、密码和权限;目标只有一个账号时也显示可明确选择的账号入口。Redis 目标没有账号时,关联单位为“网关账号 → Redis 目标服务”,`targetAccountId` 保持为空。原有整张网关卡片的批量关联入口保留兼容,不等同于单账号连线。
关联单位统一为“网关账号 → 目标账号”。拖动单个网关账号只修改该账号的 `targetId/targetAccountId`,不会改变其他网关账号的映射、密码和权限;目标只有一个账号时也显示可明确选择的账号入口。Redis、MongoDB 无认证配置显示“无认证账号”,TCP 显示“空账号”,这些端点的账号 ID 为空。网关和目标服务卡片均只展示,不提供整卡或批量关联。
拖拽页面新增“测试连接”,网关列表的测试入口使用相同流程。只有一个启用账号时直接测试;多个账号先选择要测试的网关账号,列表显示对应目标和目标账号,缺失映射不可选,禁用账号不参与。路径从选中账号关联的目标取得地址、TLS、认证库和连接资产,而不是读取网关本身可能为空的默认目标字段。K8s 卡片显示资源与 Kubernetes 资源端口,不要求维护 Pod IP。
@@ -29,7 +27,7 @@ SSH 可使用账号公钥代替密码。
旧 `gatewayUser/gatewayPassword` 配置仍可使用;一旦配置账号列表,认证仅
使用列表,不再回退到旧账号。MySQL、MongoDB 支持账号只读权限,账号无法
提升网关本身的只读限制。其他协议不提供只读模式。账号来源限制与网关来源
限制同时生效。账号可用 `targetAccountId` 选择同一目标下的独立账号;留空时使用网关默认目标账号。解除目标关联会清空默认和账号级映射。
限制同时生效。每个真实网关账号必须用自己的 `targetId/targetAccountId` 选择同协议目标及其账号,不再从网关默认目标继承。旧顶层映射仍可读取;在界面编辑保存时会展开到各真实账号并清空顶层字段。Redis、MongoDB、TCP 的空网关账号继续使用顶层字段保存,空账号本身就是明确端点。解除网关关联会清空全部账号映射。
网关账号和目标账号支持 `expiresAt`(RFC3339)及 `rotationReminderDays`(0 默认 7 天,最多 365 天)。到期后拒绝新的认证,不主动终止既有会话。凭据仍需手动轮换;运行中保存轮换结果后,新连接使用新凭据,已有会话不主动终止。
@@ -101,13 +99,13 @@ TCP 连接和协议认证分别计数。手动健康检查最多尝试 3 次,
## 拖拽图中的目标多账号
目标服务的全部账号常驻显示账号名称和用户名,未选择源网关账号时也不折叠。选择或拖动源账号后,仅同协议目标账号可关联;连线终点为具体目标账号行,选择源账号不会改变目标位置。单账号关联仅修改该网关账号,保留其他映射;整张网关卡片仍保留批量关联的兼容行为。窄屏可横向滚动并点击两端完成关联。
目标服务的全部账号常驻显示账号名称和用户名,未选择源网关账号时也不折叠。选择或拖动源账号后,仅同协议目标账号可关联;连线起点和终点都为具体账号行,选择源账号不会改变目标位置。单账号关联仅修改该网关账号并保留其他映射,服务卡片不响应关联操作。窄屏可点击两端完成关联。
此显示调整无需数据迁移,不改变既有账号映射。最新嵌入程序的五项专项回归通过,桌面和窄屏截图已检查;完整验证范围见 docs/testing.md。
## 验证范围
TCP 端口映射为无账号的独立服务类型,默认监听 `0.0.0.0`;来源白名单和连接数限制仍生效。目标需同为 TCP 类型,卡片到卡片绑定即可。完整操作与安全边界见 `tcp-port-mapping.md`。
TCP 端口映射为无协议账号的独立服务类型,默认监听 `0.0.0.0`;来源白名单和连接数限制仍生效。目标需同为 TCP 类型,画板通过网关“空账号”连接到目标“空账号”,不使用服务卡片建立绑定。完整操作与安全边界见 `tcp-port-mapping.md`。
新增后端测试使用临时 SQLite 和本地协议测试桩,前端测试使用独立数据目录
及新构建程序。角色界面的部分测试使用模拟接口,后端另有真实接口权限测试。
@@ -116,8 +114,8 @@ TCP 端口映射为无账号的独立服务类型,默认监听 `0.0.0.0`;来
2026-09-12 验证结果:Go 1.25.0 全量 `go test ./... -count=1 -timeout=120s`
及 `go vet ./...` 通过,`npm.cmd --prefix web run build` 通过。
Playwright 使用新构建的 `artifacts/serviceport-e2e.exe`,在
`SERVICEPORT_BROWSER_CHANNEL=chromium` 下 22 项全部通过。套件启动两个独立
Playwright 使用新构建的 `artifacts/linkport-e2e.exe`,在
`LINKPORT_BROWSER_CHANNEL=chromium` 下 22 项全部通过。套件启动两个独立
临时数据库实例,18081 用于开发模式流程,18082 用于真实个人登录和 Token
创建、指标调用、权限拒绝及撤销;不会复用现有服务。
+8 -8
View File
@@ -1,6 +1,6 @@
# High availability deployment
Serviceport can run as two independent gateway nodes behind an external TCP
LinkPort can run as two independent gateway nodes behind an external TCP
load balancer. The example in `deploy/ha/haproxy.cfg` uses node A as primary and
node B as standby. It is a deployment recipe, not an embedded cluster or a
validated production cluster.
@@ -12,12 +12,12 @@ validated production cluster.
| Host | `10.20.0.11` | `10.20.0.12` |
| Gateway listener | `0.0.0.0:3307` | `0.0.0.0:3307` |
| Management listener | `10.20.0.11:8080` | `10.20.0.12:8080` |
| SQLite | `/var/lib/serviceport-a/serviceport.sqlite` | `/var/lib/serviceport-b/serviceport.sqlite` |
| SQLite | `/var/lib/linkport-a/linkport.sqlite` | `/var/lib/linkport-b/linkport.sqlite` |
| Audit | Node A data directory | Node B data directory |
Use the same serviceport release and gateway/target configuration on both
Use the same linkport release and gateway/target configuration on both
nodes. Adapt the environment examples to your service manager and load values
from your secret manager. The `.env.example` files are templates; serviceport
from your secret manager. The `.env.example` files are templates; linkport
does not load them automatically. Restrict network access to gateway ports and
use an authenticated HTTPS reverse proxy or an SSH tunnel for management.
Firewall management port 8080 to the load balancer and operator network only;
@@ -39,7 +39,7 @@ or shared session state.
3. Restore a copy of the snapshot into the standby's own data directory.
The portable SQLite database contains its key, audit and automatic TLS
material; preserve private database permissions (0600 on macOS/Linux).
`SERVICEPORT_MASTER_KEY` is not used. Migrate legacy external-key databases
`LINKPORT_MASTER_KEY` is not used. Migrate legacy external-key databases
offline before deployment, retaining the complete original backup; see
[single-file storage](single-file-storage.md).
4. Configure each node's gateway bind address and port. Provision gateway TLS
@@ -97,15 +97,15 @@ rates separately.
The standby receives new connections after the primary fails its checks.
Existing database transactions, subscriptions, cursors and SSH sessions cannot
be transferred: clients must reconnect, and application owners must decide
whether an operation can be retried. Serviceport does not replay commands.
whether an operation can be retried. LinkPort does not replay commands.
With the example's 5-second interval and three failures, detection takes
roughly 15 seconds plus probe timeout. Tune timeouts for the application's
long-lived sessions. The load balancer itself also needs redundancy.
TCP proxying changes the peer IP visible to serviceport to the load balancer's
TCP proxying changes the peer IP visible to linkport to the load balancer's
address. Source-IP rules therefore match that address; enforce original-client
IP restrictions at the load balancer/firewall. Do not enable HAProxy
`send-proxy`: serviceport does not consume the PROXY protocol preamble.
`send-proxy`: linkport does not consume the PROXY protocol preamble.
## Validation before production
+8 -8
View File
@@ -17,16 +17,16 @@
| 系统 | 架构 | 文件名 |
| --- | --- | --- |
| Linux | amd64 | `serviceport-<VERSION>-linux-amd64.tar.gz` |
| Linux | arm64 | `serviceport-<VERSION>-linux-arm64.tar.gz` |
| Windows | amd64 | `serviceport-<VERSION>-windows-amd64.zip` |
| Windows | arm64 | `serviceport-<VERSION>-windows-arm64.zip` |
| macOS | amd64 | `serviceport-<VERSION>-darwin-amd64.tar.gz` |
| macOS | arm64 | `serviceport-<VERSION>-darwin-arm64.tar.gz` |
| Linux | amd64 | `linkport-<VERSION>-linux-amd64.tar.gz` |
| Linux | arm64 | `linkport-<VERSION>-linux-arm64.tar.gz` |
| Windows | amd64 | `linkport-<VERSION>-windows-amd64.zip` |
| Windows | arm64 | `linkport-<VERSION>-windows-arm64.zip` |
| macOS | amd64 | `linkport-<VERSION>-darwin-amd64.tar.gz` |
| macOS | arm64 | `linkport-<VERSION>-darwin-arm64.tar.gz` |
二进制保存在 `bin/build-cache/ci-release/<系统>-<架构>/`;六个压缩包保存在 `bin/releases/ci/`。已有输出目录会使打包失败,不会自动清理之前的产物;Drone 应使用新的构建工作区。 恢复 Linux、Windows、macOS 的 amd64/arm64 全部六个目标,已有旧包不删除。
每包包含 `serviceport` 或 `serviceport.exe`、`VERSION`、`BUILDINFO.json`、二进制 `SHA256SUMS.txt`、README 和构建/测试文档。顶层另保留 `release.json`(当前平台清单、版本、提交及校验值)和 `SHA256SUMS.txt`(所有压缩包及清单的校验值)。不打包真实 `data/`、数据库、密钥、源码、node_modules 或仓库凭据。
每包包含 `linkport` 或 `linkport.exe`、`VERSION`、`BUILDINFO.json`、二进制 `SHA256SUMS.txt`、README 和构建/测试文档。顶层另保留 `release.json`(当前平台清单、版本、提交及校验值)和 `SHA256SUMS.txt`(所有压缩包及清单的校验值)。不打包真实 `data/`、数据库、密钥、源码、node_modules 或仓库凭据。
tag 必须为 `v<VERSION>`,例如 `VERSION=0.3.0` 对应 `v0.3.0`;不匹配时失败。交叉编译不代表六个平台全部完成原生运行验证;CI 仅运行 Linux amd64 冒烟,不连接真实上游数据库或 SSH 服务。
@@ -40,7 +40,7 @@ tag 必须为 `v<VERSION>`,例如 `VERSION=0.3.0` 对应 `v0.3.0`;不匹配
上传日志在每次请求前显示包序号、总数和文件名,随后显示 add 接受结果;这只表示请求了 pin,并非独立确认 pin 状态。六包和索引全部成功后输出同源网关的 `Download page` 地址,以及六个平台包、四个索引/清单文件的 HTTP(S) 下载直链,保留原 `ipfs://` URI。包直链使用独立包 CID,并附带 filename 参数保留文件名,不误拼到索引目录下。失败时不输出最终下载页或直链。API 所在域名需同时提供 `/ipfs/` 网关,脚本不额外探测该地址。
上传只使用 `/api/v0/add`,显式发送 `User-Agent: serviceport-ci-release/1`。保留代理环境和 TLS 证书校验;Bearer token 仅通过 curl stdin 配置传入,不进入命令行参数或日志,禁用隐式 curl 配置以免本机配置改变安全选项。
上传只使用 `/api/v0/add`,显式发送 `User-Agent: linkport-ci-release/1`。保留代理环境和 TLS 证书校验;Bearer token 仅通过 curl stdin 配置传入,不进入命令行参数或日志,禁用隐式 curl 配置以免本机配置改变安全选项。
1. 先核对全部六包、平台元数据与原始校验清单;校验失败不发起上传。
2. 六个 archive 各发送一次 multipart 请求,均设置 `pin=true`、`cid-version=1`、`wrap-with-directory=false`,校验响应恰好包含对应文件 Name 和有效 CIDv1。每个包分别请求 pin,并保留各自 CID。
+3 -3
View File
@@ -1,6 +1,6 @@
# Kubernetes port-forward
serviceport 内置 Kubernetes 客户端,不需要安装 `kubectl`。在「代理管理 → 新建代理」选择 Kubeconfig,选择或上传配置,确定 Context、命名空间和 StatefulSet/Pod/Service/Deployment。保存后,目标或 TCP 纯端口转发网关可像选择 SSH、SOCKS5 一样引用该代理。代理拥有固定资源,服务拥有目的端口;非 TLS 目标无需维护 IP,TLS 目标仍需证书域名。旧内联配置与旧代理 ID 继续兼容,详见 [网络代理管理](network-proxies.md)。
linkport 内置 Kubernetes 客户端,不需要安装 `kubectl`。在「代理管理 → 新建代理」选择 Kubeconfig,选择或上传配置,确定 Context、命名空间和 StatefulSet/Pod/Service/Deployment。保存后,目标或 TCP 纯端口转发网关可像选择 SSH、SOCKS5 一样引用该代理。代理拥有固定资源,服务拥有目的端口;非 TLS 目标无需维护 IP,TLS 目标仍需证书域名。旧内联配置与旧代理 ID 继续兼容,详见 [网络代理管理](network-proxies.md)。
## 下拉中找不到 Kubeconfig
@@ -30,13 +30,13 @@ MongoDB 同样引用已保存的 K8s 代理:例如代理选择 `pod/mongo-0`
## 权限与连接
集群 API 必须可从 serviceport 所在机器访问。凭据需要所选资源的读取权限、Service/Deployment/StatefulSet 对应 Pod 的列表权限,以及 `pods/portforward` 的创建权限。Service 必须有 Pod selector,选出的 Pod 必须处于 Running 状态且未被删除,优先选择 Ready Pod。
集群 API 必须可从 linkport 所在机器访问。凭据需要所选资源的读取权限、Service/Deployment/StatefulSet 对应 Pod 的列表权限,以及 `pods/portforward` 的创建权限。Service 必须有 Pod selector,选出的 Pod 必须处于 Running 状态且未被删除,优先选择 Ready Pod。
例如 MongoDB StatefulSet:从下拉框选择实际部署的命名空间,查询后选择 `statefulset/mongo`,目标端口填 `27017`。需要 `apps` API 组下 `statefulsets` 的 `get` 和 `list` 权限。按 StatefulSet 选择 Pod 不会识别 MongoDB 主节点;如需固定成员,可在查询结果中选择 `pod/mongo-0`。切换资产、Context 或命名空间后需重新查询和选择;编辑已有配置时可以看到已保存的资源,未查询验证时会明确标注。
每条上游连接使用独立的 port-forward 通道和随机本地回环端口。连接关闭、网关停止或建连失败时清理通道和监听端口,不回退直连。建立成功的数据库会话不受建连超时影响。Pod 重启或网络中断后,原会话断开;客户端重连时重新选择 Pod,不自动重放 SQL 或消息。
当前使用 Kubernetes SPDY port-forward 协议,仅支持 TCP,要求 serviceport 直接访问集群 API;暂不支持 kubeconfig 的 `proxy-url` 或匹配 API 地址的 HTTP(S) 代理环境变量,配置这些代理时明确返回失败。集群 API 自身的反向代理需要允许流式连接升级。普通 kubeconfig 证书、Token 认证无需 kubectl;特定云平台认证和真实集群兼容性需在实际环境验证。
当前使用 Kubernetes SPDY port-forward 协议,仅支持 TCP,要求 linkport 直接访问集群 API;暂不支持 kubeconfig 的 `proxy-url` 或匹配 API 地址的 HTTP(S) 代理环境变量,配置这些代理时明确返回失败。集群 API 自身的反向代理需要允许流式连接升级。普通 kubeconfig 证书、Token 认证无需 kubectl;特定云平台认证和真实集群兼容性需在实际环境验证。
## 本轮验证
+7 -7
View File
@@ -1,16 +1,16 @@
# 运行中切换网关目标
此功能用于解决连接关系已修改但运行中的监听仍使用旧目标,以及不同网关账号需要访问不同目标的问题。账号级连线表达独立路由;新连接使用保存后的配置快照,已有会话保留原目标以免中断事务。详细设计原因见 [变更原因与设计记录](change-rationale.md)。
此功能用于解决连接关系已修改但运行中的监听仍使用旧目标,以及不同网关账号需要访问不同目标的问题。账号级连线表达独立路由;切换保存成功后会断开该网关的现有客户端连接,客户端重连后直接使用保存后的新配置快照。详细设计原因见 [变更原因与设计记录](change-rationale.md)。
你可以在「网关服务」点击「切换目标」,选择相同协议的目标服务和目标账号,点击「保存并切换」。Redis 目标的账号列表为空时不显示目标账号选择器,直接选择目标服务即可。也可以在「拖拽连线」选择运行中的网关,再选择目标账号或零账号 Redis 目标。
你可以在「网关服务」点击「切换目标」,先选择具体网关账号,再选择相同协议的目标服务和目标账号,点击「保存并切换」。Redis、MongoDB 无认证配置和 TCP 映射会显示一个明确但不可编辑的空账号选项。也可以在「拖拽连线」从运行中网关的账号行连接到目标账号行。
在切换子页面中选择具体网关账号,或在「拖拽连线」拖动蓝色网关卡片内的账号按钮,可以只修改该账号的目标服务和目标账号,其他账号不变。每个账号有自己的连线,支持依次点击两端,便于手机操作。拖动卡片顶部只移动服务位置,画板支持平移、缩放和全屏,详见 [服务连线画板](connection-canvas.md)。未启动的网关可以逐条连线,所有启用账号完成映射后才能启动。
选择「全部网关账号」或拖动整张网关卡片,会统一映射到所选目标账号并清除独立映射。这两种操作均保留监听 IP、端口、密码、来源限制和读写权限。
网关服务卡片和目标服务卡片均不建立关联;界面不提供「全部网关账号」批量映射。每次切换只更新一个明确的网关账号到一个目标账号,其他账号的目标、密码、来源限制和读写权限均保持不变。为保证运行实例不再保留任何旧目标会话,保存成功后该网关所有账号的现有客户端连接都会断开。
切换不关闭监听端口。保存成功后,随后接受的新连接使用新目标;已经接受的连接(包括尚在认证的连接)继续使用各自的旧配置,直到客户端断开。已有事务和 SSH/SFTP 会话不会迁移到新目标。若希望旧客户端也使用新目标,需要让它们重新连接,可在「活跃会话」中查看连接时目标配置地址并按需断开。
切换不关闭监听端口,也不停止网关。服务器发布新配置后会取消并关闭该网关已经接受的全部客户端连接,包括尚在认证的连接以及其他账号的连接;正在执行的请求、事务和 SSH/SFTP 会话会中断。客户端重新连接同一监听地址时直接使用新配置,其中被切换的账号路由到新目标,其他账号仍使用各自未变的映射。
服务器先校验目标协议、账号映射及 SSH 主机指纹,再保存并发布运行配置。保存失败时继续使用原目标。切换不等于连通性验证,可在目标服务页面先测试连接。SSH/SFTP 目标仍使用已确认的主机指纹校验策略。
服务器先校验目标协议、账号映射及 SSH 主机指纹,再保存并发布运行配置;只有这些步骤全部成功后才断开现有连接。保存失败时继续使用原目标并保留原连接。切换不等于连通性验证,可在目标服务页面先测试连接。SSH/SFTP 目标仍使用已确认的主机指纹校验策略。
目标与网关的完整配置编辑器在运行中也可保存。目标地址、账号、TLS、出口和高级选项更新后,所有引用该目标的运行网关会在同一次保存成功后发布新快照;网关监听、认证账号、来源限制和访问策略也可直接更新。新连接使用新快照,已经接受的连接继续使用接受时的旧快照。修改监听 IP 或端口时,服务端先绑定候选 listener,再持久化并切换;候选绑定或持久化失败时,数据库、运行快照和原 listener 均保持不变。切换成功后原 listener 不再接受新连接,但其已接受连接继续运行。
@@ -18,6 +18,6 @@
SSH/SFTP 目标的主机、端口或出口改变且会影响关联网关时,保存前只探测候选主机公开密钥,不提交认证凭据。接口以 `target_host_confirmation` 返回 SHA256 指纹;用户核对后带该指纹重试,服务端再次探测且结果一致才保存并发布。
API:`POST /api/v1/proxies/{id}/target`,请求包含 `targetId` 和 `targetAccountId`;可选 `gatewayAccountId` 指定单个账号,省略则统一设置全部账号。仅当所选目标是账号列表为空的 Redis 时,`targetAccountId: ""` 才表示有效绑定;Redis 一旦添加账号以及其他账号型协议仍必须提交有效账号 ID。服务端按当前目标配置校验,失败不会修改已保存绑定。接口适用于已停止和运行中的网关;个人会话写请求需要 CSRF,API Token 需要写入作用域。不接受网关密码或监听字段。成功返回脱敏配置及当前运行状态,绑定会持久保存。
API:`POST /api/v1/proxies/{id}/target`,界面请求始终包含 `gatewayAccountId`、`targetId` 和 `targetAccountId`。真实网关账号提交稳定账号 ID;Redis、MongoDB、TCP 的 UI 空账号提交空字符串。目标账号列表为空的 Redis/MongoDB 及 TCP 目标允许空 `targetAccountId`,目标一旦配置真实账号就必须提交有效账号 ID。省略 `gatewayAccountId` 的旧 API 批量语义仅为兼容既有调用保留,管理界面不再暴露。服务端按当前目标配置校验,失败不会修改已保存绑定。接口适用于已停止和运行中的网关;个人会话写请求需要 CSRF,API Token 需要写入作用域。不接受网关密码或监听字段。成功返回脱敏配置及当前运行状态,绑定会持久保存。
验证使用隔离 SQLite、本机 TCP/SSH fixture,覆盖新旧连接分流、目标批量发布、监听预绑定切换、保存失败回滚、启停持久化失败、CAS、SSH 候选指纹及接口权限;未连接真实数据库。浏览器相关用例只完成新场景发现,生产构建受 `spawn EPERM` 阻止,未使用旧程序代替执行,完整结果见 [测试记录](testing.md)。
验证使用隔离 SQLite、本机 TCP/SSH fixture,覆盖显式切换成功后断开旧连接、重连使用新目标、保存失败保留旧连接、监听保持运行,以及普通配置更新的新旧快照分流、监听预绑定、CAS、SSH 候选指纹和接口权限;未连接真实数据库。本次开发阶段调整按要求未执行前端构建和浏览器回归,完整结果见 [测试记录](testing.md)。
+46
View File
@@ -0,0 +1,46 @@
# 配置库与日志库分离
## 目标
LinkPort 将配置数据与持续增长的日志数据拆分,避免审计记录、命令结果和连接事件持续放大配置库,进而拖慢配置备份、启动检查和空间回收。
- `data/linkport.sqlite` 是配置库,保存目标、网关、账号、加密配置和系统设置等需要一致备份的状态。
- `data/audit/*.sqlite` 是日志分片库,保存审计与日志记录,只提供追加写入和查询,不作为配置状态的来源。
日志仍使用 SQLite。当前负载以单机追加和按时间、主键查询为主,SQLite WAL 能保留单程序部署、无需额外服务和可直接备份的特性;按文件分片后也能限制单库增长。引入独立日志服务会增加部署、认证、升级和恢复成本,当前没有足以抵消这些成本的查询或吞吐需求。运行时已将命令审计、命令结果与输出、连接事件写入日志分片,并跨活动及归档分片查询。
## WAL 与按大小切分
每个活动日志分片使用 SQLite WAL 模式。当天活动文件名为 `YYYY-MM-DD.sqlite`,轮转后的归档名为 `YYYY-MM-DD.partNNNN.sqlite`。切分对象是 `data/audit` 下的完整 SQLite 分片,不是直接截断或单独归档 SQLite 边车文件。
- 默认分片上限为 `256 MiB`。
- UI 提供“日志分片大小”配置,可在 `16 MiB` 到 `4 GiB` 之间调整;边界按二进制字节计算,即 `1 MiB = 1024 * 1024` 字节。
- 每次为新命令、事件或迁移批次选择写入分片前,核算活动 `.sqlite`、`-wal`、`-shm` 三类文件的当前总大小,并以“当前大小 + 本次写入估算值”判断是否超过上限;预计超过时先轮转,再写入新的活动分片。
- 命令记录及其结果、输出始终保存在同一个分片。结果和输出不会为满足上限而拆到其他分片;单条记录、一个提交批次或一组命令数据可能使分片超过上限,后续新写入再触发轮转。
- 调小上限后,活动分片在下一次写入前检查时轮转;调大上限只影响活动分片和后续分片。既有归档分片不自动拆分、合并或重写。
- 大小切分不是保留期策略。轮转不得自行删除旧分片;查询需要覆盖活动分片和所有封存分片,并保持既有时间、主键排序与分页语义。
轮转由单一写入协调路径串行化。实现先对活动分片执行 `PRAGMA wal_checkpoint(TRUNCATE)`,再切换到 `journal_mode=DELETE`、关闭数据库句柄并清理遗留的 `-wal` 与 `-shm` 文件。随后扫描同一天已有的 `partNNNN`,按当前最大编号加一归档活动 `.sqlite`,再以原日期文件名创建新的 WAL 活动分片。该顺序使归档文件自包含,并避免复用已有 part 名称。
## 迁移流程
从现有 `data/linkport.sqlite` 迁出日志前,必须停止相关服务或进入禁止日志写入的维护状态,并备份完整 `data` 目录。迁移使用可恢复的小批次,不执行按时间或其他宽条件的一次性删除:
1. 首次迁移先写入 `pending` 标记。旧命令和事件以明确主键、固定小批量读取;命令与其结果、输出作为一组选择同一个目标分片。
2. 将小批记录写入 `data/audit/*.sqlite`。插入冲突时逐字段核对既有目标记录;命令、结果、输出和事件的持久化字段不一致即停止迁移,不覆盖冲突数据。
3. 目标分片事务提交成功后,才在配置库事务中按该批明确的命令 ID 或事件 ID 删除源记录,并核对命令或事件主记录的受影响行数。跨库提交中断后,下一次启动会识别目标已存在且字段相同的记录,继续完成源端删除,不会扩大删除条件。
4. 所有源日志表清空后才删除旧表,并对 `data/linkport.sqlite` 执行一次 `VACUUM`。`VACUUM` 成功后写入 `done` 标记并移除 `pending`;如果 `VACUUM` 失败,`pending` 保留,下一次启动会再次执行压缩,直到成功完成。
`VACUUM` 不在每批迁移后执行;它需要额外临时磁盘空间并会独占配置库。部署迁移前应确认可用空间、保持服务停止,并保留迁移前备份,直至新配置库和全部日志分片验收完成。
## 兼容性与运维影响
- 完整备份和恢复必须同时覆盖 `data/linkport.sqlite`、`data/audit/*.sqlite` 及活动分片的 SQLite 边车文件;仅复制配置库不再包含日志。
- 迁移后,旧版本无法读取新日志分片;由于已校验记录会从配置库删除,回退必须停服并恢复迁移前的完整数据目录,不能只替换可执行文件。
- 日志查询、导出和清理显式遍历分片。配置快照只包含 `data/linkport.sqlite` 的配置状态,不包含 `data/audit/*.sqlite` 日志;需要完整日志的运维备份必须单独覆盖全部日志分片及活动边车文件。
- 分片大小设置保存在配置库。超出 `16 MiB` 到 `4 GiB` 范围的 API 或导入值应拒绝,不能静默截断;旧配置缺少该值时使用 `256 MiB`。
- 迁移和轮转不得删除、覆盖现有 `data/audit` 文件。文件命名必须稳定且避免复用已有名称,便于中断后恢复和按顺序查询。
## 验证状态
上述日志拆分、轮转、迁移恢复、设置接口和配置快照边界均已有运行时代码。用户运行前端构建时,`vue-tsc` 在 `OpsPanel.vue` 的日志大小边界计算处报告可选值参与除法的类型错误;该处现已改为先判断设置是否存在,再读取 `minBytes` 和 `maxAllowedBytes`。静态审查还补充了仅剩 `pending` 标记时的重启恢复用例,并修正事件旧表断言遗漏的查询错误检查。修复后按要求未重跑类型检查、构建或测试,未启动服务,也未执行真实数据库迁移,因此当前只声明源码与本文的一致性静态审查以及 `git diff --check` 通过。主代理此前已只读列出 `data` 下相关文件,本轮未向真实 `data/linkport.sqlite` 或 `data/audit` 写入数据。
+10 -10
View File
@@ -1,4 +1,4 @@
# serviceport 模块说明
# linkport 模块说明
本文按当前源码整理,帮助使用者理解功能,也帮助维护者找到修改入口。核对日期:2026-09-27。
@@ -8,12 +8,12 @@
| 对象 | 负责什么 | 典型配置 | 使用的账号 |
| --- | --- | --- | --- |
| 网关服务 Gateway | 对客户端开放监听端口,认证客户端,执行访问策略,再转发请求 | 监听地址和端口、协议、目标引用、来源限制、连接上限 | 网关账号:客户端连接 serviceport 时填写 |
| 目标服务 Target | 描述真正要访问的数据库、服务器或 HTTP 服务 | 目标地址和端口、TLS、目标账号、SSH 身份校验、出口引用 | 目标账号:serviceport 登录上游服务时使用 |
| 代理出口 Outbound | 决定 serviceport 通过什么网络路径到达目标 | 直连、SOCKS5、SSH 跳板机、sing-box 单 outbound、Kubernetes port-forward | 出口凭据:登录代理或访问 Kubernetes 时使用 |
| 网关服务 Gateway | 对客户端开放监听端口,认证客户端,执行访问策略,再转发请求 | 监听地址和端口、协议、目标引用、来源限制、连接上限 | 网关账号:客户端连接 linkport 时填写 |
| 目标服务 Target | 描述真正要访问的数据库、服务器或 HTTP 服务 | 目标地址和端口、TLS、目标账号、SSH 身份校验、出口引用 | 目标账号:linkport 登录上游服务时使用 |
| 代理出口 Outbound | 决定 linkport 通过什么网络路径到达目标 | 直连、SOCKS5、SSH 跳板机、sing-box 单 outbound、Kubernetes port-forward | 出口凭据:登录代理或访问 Kubernetes 时使用 |
| 文件资产 Asset | 集中保存可复用的文件凭据与证书 | Kubeconfig、SSH 私钥、公钥列表、用户证书、TLS/CA 证书、其他文本配置 | 由目标、账号和出口按类型引用 |
一个目标可以被多个同协议网关复用,一个网络代理可以被多个目标或 TCP 纯转发网关引用。「代理管理」独立维护 Kubeconfig、SSH、SOCKS5 和 sing-box,类型在创建表单顶部直接选择;文件仅作为凭据保留。普通协议网关先关联目标再启动,沿用目标的代理;TCP 纯转发可用 `tcpForward.mode="proxy"` 直接引用网络代理。网关可以设置默认目标账号,也可以让不同网关账号分别映射到不同目标账号。详见 [网络代理管理](network-proxies.md)。
一个目标可以被多个同协议网关复用,一个网络代理可以被多个目标或 TCP 纯转发网关引用。「代理管理」独立维护 Kubeconfig、SSH、SOCKS5 和 sing-box,类型在创建表单顶部直接选择;文件仅作为凭据保留。普通协议网关的每个网关账号一对一选择目标账号后才能启动,并沿用目标的代理;Redis、MongoDB 和 TCP 的空账号也作为明确映射端点。TCP 纯转发可用 `tcpForward.mode="proxy"` 直接引用网络代理。详见 [网络代理管理](network-proxies.md)。
例如:客户端用网关账号 `reporter` 连接本机 MySQL 网关,网关选择目标账号 `db_reader`,通过 SSH 跳板机连接内网 MySQL。网关密码、数据库密码和跳板机密码是三组独立凭据。
@@ -42,8 +42,8 @@ flowchart LR
| 模块 / 入口 | 职责 | 什么时候看这里 |
| --- | --- | --- |
| [cmd/serviceport/main.go](../cmd/serviceport/main.go) | 组合根;准备数据锁与存储、创建协议工厂和运行管理器,协调命令与服务生命周期 | 修改启动编排 |
| [commands.go](../cmd/serviceport/commands.go)、[bootstrap.go](../cmd/serviceport/bootstrap.go)、[management.go](../cmd/serviceport/management.go) | 独立/维护命令分派、TLS 与网关恢复、管理 HTTP/桌面/信号/定期清理 | 调整命令或运行生命周期 |
| [cmd/linkport/main.go](../cmd/linkport/main.go) | 组合根;准备数据锁与存储、创建协议工厂和运行管理器,协调命令与服务生命周期 | 修改启动编排 |
| [commands.go](../cmd/linkport/commands.go)、[bootstrap.go](../cmd/linkport/bootstrap.go)、[management.go](../cmd/linkport/management.go) | 独立/维护命令分派、TLS 与网关恢复、管理 HTTP/桌面/信号/定期清理 | 调整命令或运行生命周期 |
| [cmd/devcert/main.go](../cmd/devcert/main.go) | 独立开发证书工具,调用证书模块生成本机测试证书 | 生成开发 TLS 材料 |
| [internal/desktop](../internal/desktop) | Windows/macOS/Linux 本地模式;数据目录、数据库文件权限与进程锁、首次管理员设置和打开浏览器 | 排查本机启动、首次设置与跨平台运行 |
| [internal/localcert/cert.go](../internal/localcert/cert.go) | 生成 localhost 开发 TLS 证书及私钥 | 调整开发证书生成规则;正式证书由部署者提供 |
@@ -137,7 +137,7 @@ flowchart LR
| [singbox.go](../internal/outbound/singbox.go) | 定位外部 binary、生成私有 loopback SOCKS5 运行配置、执行 10 秒 `check`、按规范化 outbound 共享 `run` 子进程,并记录独立启动/本地协商阶段;不嵌入 sing-box module |
| [kubernetes.go](../internal/outbound/kubernetes.go) | 使用 client-go 读取集群配置、解析 Pod/Service/Deployment/StatefulSet、建立 port-forward,并在连接结束时清理资源;不依赖 kubectl |
`protocol/sshproxy` 是客户端要使用的 SSH/SFTP 服务代理;`outbound/ssh.go` 是访问任意目标时可选的 SSH 跳板路径。sing-box 是 serviceport 管理的外部子进程,发布包不捆绑它。所有出口故障都不会自动回退直连。
`protocol/sshproxy` 是客户端要使用的 SSH/SFTP 服务代理;`outbound/ssh.go` 是访问任意目标时可选的 SSH 跳板路径。sing-box 是 linkport 管理的外部子进程,发布包不捆绑它。所有出口故障都不会自动回退直连。
### 6.4 集群发现:internal/cluster
@@ -239,7 +239,7 @@ App.vue 协调目标和网关编辑、导航与提交;草稿构造和载荷规
| [deploy/compose.test.yml](../deploy/compose.test.yml)、[deploy/mongo](../deploy/mongo)、[deploy/ssh](../deploy/ssh)、[deploy/fixtures](../deploy/fixtures) | 矩阵使用的容器、MongoDB 副本集初始化、SSH 配置和测试辅助服务 |
| [internal/testutil](../internal/testutil) | 后端测试共用的网络出口等辅助设施;不属于生产功能模块 |
| 各 Go 目录下的 `*_test.go` | 本模块单元、回归和集成测试;带真实服务环境要求的用例需要相应测试环境 |
| [web/tests](../web/tests)、[web/playwright.config.ts](../web/playwright.config.ts) | 页面与接口交互回归;start-server.mjs 使用临时数据启动开发模式和认证模式实例,通过 SERVICEPORT_E2E_BINARY 指定被测程序 |
| [web/tests](../web/tests)、[web/playwright.config.ts](../web/playwright.config.ts) | 页面与接口交互回归;start-server.mjs 使用临时数据启动开发模式和认证模式实例,通过 LINKPORT_E2E_BINARY 指定被测程序 |
| `data/` | 项目本地运行数据:SQLite、audit、TLS 和 Windows 主密钥材料;保留并按完整目录备份 |
| `bin/` | 程序、发布包、验证产物、构建缓存与完整备份,见 [输出目录](build-output.md) |
| `web/dist/`、`web/node_modules/`、`web/test-results/`、`web/playwright-report/` | 前端产物、依赖和测试结果 |
@@ -258,7 +258,7 @@ App.vue 协调目标和网关编辑、导航与提交;草稿构造和载荷规
| 调整身份或 Token 权限 | store/identity、tokens → api/authorization、auth、tokens、token_response → TokenManager → 权限与秘密隔离回归 |
| 增加监控指标或告警 | gateway/monitoring、connections → alerts/engine 与 store/alerts → api/monitoring、prometheus、alerts → OpsPanel/AlertsPanel |
| 增加命令审计能力 | protocol 对应协议解析 → model.AuditCommand → store/commands → api/commands → useAudit 与审计页面 |
| 排查启动或打包 | cmd/serviceport → desktop 或 buildinfo → scripts 构建/验证入口;先确认实际数据目录与程序版本 |
| 排查启动或打包 | cmd/linkport → desktop 或 buildinfo → scripts 构建/验证入口;先确认实际数据目录与程序版本 |
## 11. 相关文档
+39
View File
@@ -0,0 +1,39 @@
# MySQL 账号模式与协议命令
## 模式选择
在网关编辑器的「账号 → 权限 → 访问模式」中选择。为解决复制客户端被协议白名单拒绝,以及不同账号需要不同 SQL 检查强度的问题,MySQL 提供以下三种模式:
| 模式 | 保存值 | SQL 行为 |
| --- | --- | --- |
| 只读 | `readonly` | 保留只读 AST 白名单,拒绝写入,包括带 WHERE 的 UPDATE/DELETE。 |
| 安全读写(默认) | `readwrite` | UPDATE/DELETE 必须有自身的外层 WHERE;其他检查和边界见[写入保护](mysql-write-safety.md)。 |
| 普通读写 | `unrestricted` | 不执行网关 SQL AST/WHERE 检查,原样转发 SQL、预处理和批次,由目标账号权限及数据库规则决定是否执行。 |
旧配置中的空值或 `readwrite` 保持安全读写行为,不会自动变成普通读写。网关级只读限制优先,不能通过账号选择普通读写绕过;普通读写模式只适用于 MySQL,MongoDB 等其他协议不增加此选项。模式仍使用现有 JSON 配置字段,无数据库迁移;已有连接沿用建立时的策略,修改后重新建立连接使用新模式。
普通读写模式不固定目标会话 SQL mode,也不在 RESET CONNECTION 后重新设置解析兼容模式,支持数据库自身认可的语法、SQL 动态预处理及会话设置。鉴权、账号目标映射、来源限制、命令审计失败关闭连接和报文格式检查继续适用。它不是绕过网关认证的 TCP 透传模式。
## 常用协议命令
三种模式均支持 PING、INIT DB、RESET CONNECTION、FIELD LIST、预处理命令,以及以下补齐的命令:
- `COM_STATISTICS`:按单个文本报文转发,不作为结果集解析。
- `COM_PROCESS_INFO`:转发完整结果集。
- `COM_SET_OPTION`:接受多语句开关 0/1,转发其 EOF/错误响应;安全模式仍检查 SQL 批次。
- `COM_REGISTER_SLAVE`:转发复制注册,审计不记录注册报文中的凭据。
- `COM_BINLOG_DUMP`、`COM_BINLOG_DUMP_GTID`:转发复制请求和连续事件流,允许只读账号读取;最终权限由上游复制权限控制。
CREATE DB、DROP DB、REFRESH、SHUTDOWN、PROCESS KILL 和 DEBUG 协议命令在两种读写模式下转发,由上游权限判断;只读模式拒绝这些管理操作。畸形报文和未实现的命令仍返回协议拒绝,不会把未知响应盲目解释为 SQL 结果集。
客户端初始化常用的 `SET character_set_results = NULL`、`= DEFAULT` 以及 `utf8mb4`、`utf8`、`utf8mb3`、`ascii`、`latin1` 静态值在安全读写和只读模式放行,因为它只决定返回编码、不改变 SQL 解析;其他结果字符集、用户变量、动态值和 `SET GLOBAL` 仍拒绝。普通读写模式不执行该检查。
Binlog 流逐包转发,包括以 `0x00` 开头的事件,不能将首个事件误认为 SQL OK 后停止。流以 EOF、上游错误或连接关闭结束;客户端断开及网关上下文取消会关闭闲置上游。dump 使用专用连接,流结束后关闭连接,后续命令需重新连接。当前支持标准异步复制,不支持流中的半同步 ACK;CHANGE USER 重新认证、服务端游标等此前未实现的交互仍不开放。
## 实现与验证
协议实现位于 `internal/protocol/mysqlproxy/gateway.go`、`binlog.go` 和 `audit.go`;模型在 `internal/model` 验证模式范围并合成有效账号权限,账号编辑器提供下拉选项,列表显示实际模式。相关行为及验证结果同步记录在[变更记录](change-rationale.md)。
本机 TCP 协议 fixture 覆盖连续 Binlog 事件、序号回绕、GTID、终止错误、断开清理、管理命令和格式拒绝;SQL fixture 覆盖普通模式无 WHERE/会话设置/预处理及重置,保留安全模式拒绝测试。fixture 不执行真实数据库写入,不代表已通过真实 MySQL 复制服务器验证。
2026-09-25 实际结果:Go 1.25.0 的 MySQL、模型包测试、账号模式 API 保存回归和 `go vet ./...` 通过;全量 `go test ./... -count=1 -timeout=120s` 的 6 项既有文件权限/SQLite 快照测试失败,其余包通过,日志位于 `.cache/sqlite-removal/mysql-modes-full-test.log`。前端类型检查及 2 项模式展示逻辑测试通过,新增 Playwright 用例可发现;`npm.cmd --prefix web run build` 在 Vite/esbuild 创建子进程时遇到 `spawn EPERM`,无法生成新的嵌入式二进制运行浏览器套件。
+10 -1
View File
@@ -8,11 +8,20 @@ MySQL 网关为 `COM_QUERY` 普通查询和 `COM_STMT_EXECUTE` 预编译执行
- 非结果集响应:影响行数、新增 ID、警告数量、服务器状态及消息。
- 数据库错误:错误码、SQLSTATE、错误消息;中途返回错误时保留之前已经采集的行。
- 多个结果:网关已支持的查询批次及返回流分别按结果序号保存。该功能不扩大现有只读策略、批次或服务端游标的支持范围。
- 性能摘要:记录网关观察到的完整响应耗时,并累计 OK/EOF 包中的 `SERVER_STATUS_NO_INDEX_USED` 与 `SERVER_STATUS_NO_GOOD_INDEX_USED` 状态位。
数值使用字符串保存,避免 JavaScript 大整数和小数精度丢失。SQL NULL 使用 JSON null,空字符串仍为空字符串。二进制、非 UTF-8 字符集和无法用 UTF-8 表示的值保存为 `{"base64":"..."}`,保留字节。无法解码的返回包保存完整 Base64 载荷并标记解码不完整,不把猜测的数据当成正常结果。
只采集经过网关返回的数据,不重新执行查询。查询结果先写入加密审计库,再向客户端转发;写入失败会中止连接。大结果会增加存储占用与转发开销。异常断开标记为采集中断;进程异常退出时可能保留“尚未收到完整结果”,不视为完整成功。
## 执行性能与扫描风险
执行时间从网关接受并记录命令后开始,到完整响应、数据库错误或连接中断时结束,包含网关审计和结果转发开销。达到 `3000ms` 的查询在命令列表与结果详情中显示红色“慢查询”。
MySQL 返回 `SERVER_STATUS_NO_INDEX_USED` 时显示红色“全表扫描风险”,返回 `SERVER_STATUS_NO_GOOD_INDEX_USED` 时显示黄色“索引效果不佳”。状态位是服务端针对该语句给出的风险信号,不等同于对执行计划的完整证明,小表扫描等情况也可能出现,因此界面不显示为确定的“全表扫描”。
网关不会自动执行 `EXPLAIN`:额外查询会增加目标负载、可能要求额外权限,并可能改变事务和锁行为。普通查询响应也不包含实际索引名称;需要确认使用了哪个索引时,应由有权限的操作者对相同 SQL 和参数单独执行 `EXPLAIN` 或 `EXPLAIN ANALYZE`。
## 查看与下载
打开「命令审计 → 查看」,在“查询返回结果”中查看数据、切换结果集并加载更多。宽表支持横向滚动,工具栏随窗口宽度换行;单页显示 50 行、20 列,可分别翻页。页面预览最多约 8 MiB,数据库中的结果不按该大小截断。
@@ -21,4 +30,4 @@ MySQL 网关为 `COM_QUERY` 普通查询和 `COM_STMT_EXECUTE` 预编译执行
结果和状态加密保存在原命令所在的 UTC 日期审计库中,随命令的 7 天保留期清理;要求个人会话或 audit API Token。配置 WebDAV 备份不包含审计结果,完整审计备份需停服后复制整个数据目录。
旧记录无法补录查询结果。部署新版后重新建立 MySQL 连接,后续查询才会记录;仅预编译、发送长参数、释放语句、被拦截的请求和其他管理命令不记录查询返回行。
旧记录无法补录查询结果或性能指标;已有结果摘要但缺少新字段的历史记录会在详情显示指标不可用,没有结果摘要的记录仍显示未采集结果。新增字段保存在既有加密结果摘要 JSON 中,不修改 SQLite 表结构,也不需要迁移。部署新版后重新建立 MySQL 连接,后续查询才会记录;仅预编译、发送长参数、释放语句、被拦截的请求和其他管理命令不记录查询返回行。
+36
View File
@@ -0,0 +1,36 @@
# MySQL 更新和删除必须包含 WHERE
MySQL 网关的「安全读写」模式(默认,保存值 `readwrite`)检查 `UPDATE` 和 `DELETE`,无需开启只读模式。旧读写配置继续执行本规则;明确选择「普通读写」(`unrestricted`)的账号跳过 SQL 检查,由上游数据库控制权限。「只读」仍拒绝全部写入。模式选择和协议命令支持见 [MySQL 账号模式](mysql-access-modes.md)。通过该网关的直连、SOCKS5、SSH 和 Kubernetes 出口使用相同的账号策略。
语句必须有自身的外层 `WHERE`。单独的 `LIMIT`、`ORDER BY`、JOIN 的 `ON`、子查询或 CTE 内的 `WHERE`、字符串及注释中的 WHERE 文本均不能替代它。例如:
```sql
-- 拒绝
UPDATE users SET enabled = 0;
DELETE FROM users LIMIT 1;
UPDATE users SET enabled = (SELECT enabled FROM source WHERE id = 1);
-- 通过本规则,仍需符合账号权限及数据库约束
UPDATE users SET enabled = 0 WHERE id = 1;
DELETE FROM users WHERE id = ?;
```
检查使用 Vitess SQL AST,覆盖大小写、CTE、别名和多表语法。普通查询与二进制预处理的准备阶段都在转发上游前检查,执行阶段只接受已登记的预处理语句 ID。多语句请求整体预检查,不会先执行前半段再拒绝后半段;现有网关仍只支持符合只读白名单的批次,不因此放开写入批次。
为限制解析开销,单次 SQL 文本最多 1 MiB、批次最多 100 条语句、单条 AST 最多遍历 10,000 个节点;超过上限拒绝。大批量导入客户端需要相应缩小请求。
缺少外层条件时返回 MySQL 原生错误码 1290(`ER_OPTION_PREVENTS_STATEMENT`),消息说明 UPDATE/DELETE 缺少外层 WHERE。命令审计记为 `blocked`,连接事件记录 `mysql:missing_where`;其他无法安全检查的 SQL 记录 `mysql:unsafe_sql`。拒绝请求不会转发上游,正常情况下连接继续可用;既有审计失败关闭连接的行为保留。只读账号仍执行更严格的只读白名单,继续拒绝带 WHERE 的写操作。
## 安全读写模式的兼容边界
- `WHERE TRUE`、`WHERE 1=1` 有外层 WHERE,仍可通过。该规则不评估条件选择性、索引、影响行数或锁范围,也不是 MySQL `sql_safe_updates` 的替代实现。
- SQL 文本形式的 `PREPARE`、`EXECUTE`(含不能解析的 `EXECUTE IMMEDIATE`)被拒绝,防止动态 SQL 绕过;客户端二进制协议预处理继续可用。
- 拒绝可执行版本注释 `/*!...*/`、MariaDB `/*M!...*/`、优化器提示 `/*+...*/` 以及不能可靠解析的语句。字符串内相同文本不按可执行注释处理。正常注释可用;MySQL 与 Vitess 对特殊控制字符注释的解释不一致时拒绝。
- 所有上游连接初始化以及 `COM_RESET_CONNECTION` 成功后,固定会话 `sql_mode='STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'`,并恢复目标配置的客户端、连接、结果字符集及排序规则。读写连接不再继承服务器默认 SQL 模式;依赖其他 SQL 模式的客户端需适配。禁止客户端更改 `sql_mode`、`character_set_client`、`character_set_connection`、`collation_connection` 或使用 `SET CHARACTER SET`。`SET character_set_results` 允许 NULL、DEFAULT 及静态 utf8mb4、utf8、utf8mb3、ascii、latin1;该设置只影响返回编码,不改变后续 SQL 解析,其他字符集、动态值或 SET GLOBAL 仍拒绝。`SET NAMES` 仅允许静态 `utf8mb4`、`utf8`、`utf8mb3`、`ascii`、`latin1`,其他编码或动态值拒绝;普通 `autocommit`、时区等会话设置保留。
- 规则约束网关收到并可解析的 SQL。既有存储过程、函数、触发器等服务器内部执行及其会话环境副作用属于受信任的服务器边界,不作检查保证;不影响绕过网关直连数据库、其他数据库协议或 TCP 透传。
没有数据库迁移,也不改写或拆成先查后写的 SQL。升级运行程序后重新连接 MySQL 客户端使用新行为;不需要修改保存的账号或目标配置。
## 验证
新增 AST 策略测试覆盖正常/缺失条件、多表、CTE、伪 WHERE、批次、动态 SQL、注释、会话设置、解析失败及固定错误信息;本机 MySQL 协议 fixture 验证默认/读写账号、普通与预处理请求、阻断前未转发、审计记录、会话重置和拒绝后的连接可用性。这些不是外部真实 MySQL 服务器验证。实际检查结果见 [变更记录](change-rationale.md#mysql-严格拒绝无-where-的更新和删除2026-09-25)。
+45
View File
@@ -0,0 +1,45 @@
# 网络代理管理
## 目的与入口
「代理管理 → 新建代理」用于管理网络连通方式。页面顶部直接选择 Kubeconfig、SSH、SOCKS5 或 sing-box,随后填写名称、环境及该类型的连接配置。不再显示文件资产/连接资产分类,也不提供通用文件管理列表。新建和编辑仍使用工作区独立页面。
Kubeconfig 与 SSH、SOCKS5 一样是独立保存、可被多个服务引用的代理。Kubeconfig 文件的上传是配置代理凭据的一步:在代理内选择或「上传 Kubeconfig」,再确定 Context、命名空间与固定资源。SSH 私钥、证书等也仍在需要它们的表单中选择或上传。已有文件通过资源搜索、引用跳转及配置历史继续维护,不删除文件或丢弃凭据。
## 关联服务
| 使用方 | 关联方式 | 目的地址与端口 |
| --- | --- | --- |
| 目标服务 | 连接方式中先选类型,再选择已保存代理;直连单独选择 | 目标保存服务地址、端口及业务账号。Kubeconfig 固定资源不要求普通主机地址;TLS 校验仍使用目标域名 |
| 普通协议网关 | 每个网关账号关联目标账号,并沿用该目标选择的代理 | 沿用目标服务配置,不增加另一份覆盖代理 |
| TCP 纯端口转发网关 | 直接选择代理类型和已保存代理,无需目标服务 | SSH/SOCKS5/sing-box 填目的地址及端口;Kubeconfig 使用代理固定资源,只填资源端口 |
代理拥有网络认证信息,目标账号仍属于目标,客户端账号仍属于网关。类型按钮只决定候选代理范围;仅选择类型未选择有效代理时阻止保存。已引用代理失效时显示不可用,不能静默退回直连。代理选择器按类型列出全部范围的配置,维持已有跨项目/环境引用行为。
Kubernetes Pod/Deployment/StatefulSet 使用容器 TCP 端口,Service 使用 Service 端口,运行时解析 targetPort。固定资源不会自动识别数据库主节点,也不提供数据库多节点发现。权限预检只表示授权检查结果;实际网络、资源状态及业务认证需要连接测试。
## API 与保护
代理继续使用 `/api/v1/outbounds`,目标通过 `outboundId` 引用。TCP 纯端口转发增加以下引用形式:
```json
{
"type": "tcp",
"tcpForward": {
"mode": "proxy",
"outboundId": "saved-proxy-id",
"host": "destination.internal",
"port": 8080
}
}
```
Kubeconfig 代理的 `host` 应为空或省略,资源信息由代理提供;不能同时提交独立的 kubeConfigAssetId/context/namespace/resource。网关只持久化引用及端口,不复制代理密码、Kubeconfig 正文或 sing-box JSON。保存、列表、单条读取、连接测试和导入都解析引用并校验兼容性;管理响应不返回秘密正文。
引用保护覆盖 Kubeconfig → 代理 → 目标/网关:有引用时禁止删除,关联网关运行时阻止修改共享代理及文件。修改代理类型会重新校验引用网关,不能使旧 SOCKS5 网关使用其他类型。SSH 主机指纹确认与会话绑定、凭据加密、配置历史脱敏、完整加密备份保持。
## 兼容与验证
没有数据库迁移,也不自动搬移或删除已有数据。旧 TCP 空 mode/SOCKS5、目标内联 K8s、TCP 内联 K8s 和已有代理 ID 继续可读写;只有编辑旧内联 K8s 时显示兼容配置。新 UI 使用共享代理。旧执行程序不识别新增 `tcpForward.mode="proxy"`,回退前应使用适配旧版本的备份并核对配置。
新增 model/store/API 测试覆盖四种代理引用、持久化重开、备份导入、脱敏、间接引用及运行保护;本地 SOCKS5 fixture 验证字节转发。前端逻辑覆盖类型切换、未选/丢失引用、K8s 无 host 和旧配置兼容。实际命令、全量结果及生产构建/浏览器限制见 [测试记录](testing.md)。没有将 fixture 结果当作真实 Kubernetes、SSH 或 sing-box 服务器验收。
+4 -4
View File
@@ -6,11 +6,11 @@
结果与范围:全部现有目标协议均可配置 `connectionOptions.persistent`。单节点 MySQL、MongoDB、PostgreSQL、Redis 仍使用独占交接的预认证备用会话;SSH/K8s 出口仍保持共享隧道。无共享隧道且不能安全交接会话的 SQL Server、RabbitMQ、HTTP(S)、SSH/SFTP、TCP 与数据库集群使用 `probe` 模式:网关运行期间每 15 秒以目标协议的现有连接测试独立探测,完成后关闭临时连接;不复用业务会话、不保持这些协议的长连接,也不计入复用次数。对不能安全保持会话的协议,保活含义仅为持续探测,不保证客户端免建连。SSH/K8s 出口保持原有隧道模式,集群不缓存可变节点的会话。
兼容性与迁移:布尔配置和默认关闭行为不变,无数据迁移;旧配置不自动开启。仅以前被拒绝的组合现在可保存。探测额外产生周期性连接和认证请求,明确的认证、权限或身份拒绝继续暂停重试,避免锁号。连接测试本身仍使用临时资源;后台探测不影响客户端原有路由或事务。模型与网关针对性 Go 测试、前端组件测试、`go vet ./...` 及前端类型检查通过;完整 Go 测试受 Windows 沙箱文件权限限制,Vite 构建受 `spawn EPERM` 限制,Playwright 未运行。网关测试使用 fixture,不代表真实目标的在线验证。
兼容性与迁移:管理界面新建目标时,全部现有目标协议默认写入 `persistent: true`;已有目标继续使用已保存值,缺少字段或显式为 `false` 的旧配置不会自动开启,无数据迁移。探测额外产生周期性连接和认证请求,明确的认证、权限或身份拒绝继续暂停重试,避免锁号。连接测试本身仍使用临时资源;后台探测不影响客户端原有路由或事务。模型与网关包测试、全部 16 个前端领域测试文件、前端类型检查及串行 `go vet -p=1 ./...` 通过;其中 6 项持续连接组件测试和 8 项连接草稿测试逐一检查全部 11 种类型的模式及默认值,并验证旧目标不继承新建默认值。完整 Go 测试仍有 6 项 Windows 文件权限/SQLite 快照环境失败,Vite 构建受 esbuild `spawn EPERM` 限制;Playwright 成功发现测试但未运行浏览器。网关测试使用 fixture,不代表真实目标的在线验证。
## 行为与用途(2026-09-22)
目标编辑器的“高级”页提供“后台持续连接”开关,对应 `connectionOptions.persistent`,默认 `false`。旧配置保持原有按客户端会话连接的行为,无需数据迁移。保存和测试共用的目标 payload 保留此布尔值。
目标编辑器的“高级”页提供“后台持续连接”开关,对应 `connectionOptions.persistent`。全部 11 种目标类型的新建草稿默认 `true`;编辑已有目标时严格使用后端保存值,旧配置缺少字段时仍显示关闭。保存和测试共用的目标 payload 保留此布尔值。
- 单节点 MySQL、MongoDB、PostgreSQL、Redis 支持预认证备用池,按网关绑定及账号权限范围隔离。仅交接尚未服务客户端的连接;客户端用过的会话不回收,避免事务、游标和订阅状态串用。
- 任意协议使用 SSH 或 K8s 出口时可保持持续隧道,但仍遵守原有协议及固定资源规则:K8s 固定资源仍要求单节点,不因 persistent 放宽集群校验。SSH 集群只保持隧道,不建立预认证备用池。其他协议的直连或 SOCKS5 方式仅周期性探测,不保持或复用业务连接。
@@ -29,11 +29,11 @@
`leases` 显示为“累计成功复用次数”,指备用会话独占交接或隧道新流,不是当前连接数;`reconnects` 为重连次数。界面不读取或展示原始错误、密码或其他未约定字段。
仓库无 Shopee 三语字典或对应 i18n API,本次沿用现有中文文案。后端生命周期由后端实现提供;组件测试不证明真实目标、认证、心跳或隧道连接成功。
持续连接标签、模式说明和默认行为说明使用项目现有中文、英文、日文字典。后端生命周期由后端实现提供;组件测试不证明真实目标、认证、心跳或隧道连接成功。
## 使用方法
1. 在「目标服务 → 配置目标 → 高级」开启「后台持续连接」并保存;引用网关正在运行时无需先停止。
1. 新建目标时「目标服务 → 配置目标 → 高级」中的「后台持续连接」默认开启,可按需关闭后保存;编辑旧目标时沿用已保存状态。引用网关正在运行时无需先停止。
2. 启动网关。即使没有客户端接入,也会开始预建后台资源;没有运行网关的目标不会主动连接。
3. 在网关列表展开该行,查看「目标后台持续连接」的状态、复用和重连计数。首次建立或心跳检查期间可能短暂显示不可用;前台连接仍按原配置建立,不降级直连。
4. 明确认证/权限/身份拒绝会暂停后台重试;核对并保存目标配置后按新快照重试。不能通过开启持续连接绕过 SSH 指纹校验。
+1 -1
View File
@@ -1,6 +1,6 @@
# 跨平台运行
Windows、Linux、macOS 使用同一单 SQLite 格式:local_metadata 内的主密钥、配置、审计和自动 TLS 均在库内,不读取 SERVICEPORT_MASTER_KEY。旧库迁移见 [单文件存储](single-file-storage.md)。
Windows、Linux、macOS 使用同一单 SQLite 格式:local_metadata 内的主密钥、配置、审计和自动 TLS 均在库内,不读取 LINKPORT_MASTER_KEY。旧库迁移见 [单文件存储](single-file-storage.md)。
回环管理地址启用本地模式,包括随机端口、可选管理员设置和打开浏览器;非回环部署仍要求认证。Windows 使用命名互斥锁并保留旧锁兼容;Linux/macOS 使用数据库文件锁。新 Windows 数据库 ACL 限制当前用户和 SYSTEM,Unix 数据库权限为 0600,已有过宽权限会拒绝启动,不自动改写。
+124
View File
@@ -0,0 +1,124 @@
# sing-box 连接资产
当前 UI 称为「sing-box 代理」,在「代理管理」创建并供目标或 TCP 纯端口转发网关复用。共享 TCP 网关使用 `tcpForward.mode="proxy"` 与 `outboundId`,沿用本文相同的进程、安全和凭据边界,详见 [网络代理管理](network-proxies.md)。
## 目的与架构
sing-box 连接资产让目标服务复用 sing-box 的远程 outbound,而不把各协议实现嵌入 linkport。资产只保存一个 outbound JSON 对象;目标仍通过现有 `outboundId` 引用连接资产。业务链路为:
```text
linkport DialFunc -> 127.0.0.1 随机端口的受控 SOCKS5 入站
-> linkport 管理的 sing-box 子进程
-> 资产中的单个远程 outbound
-> 目标主机:端口
```
当前实现选择**管理外部 binary**:
- linkport 不链接 sing-box Go module,也不复用其内部 Go API。
- linkport 不连接用户自行维护的常驻 sing-box、SOCKS 或 mixed 入口;监听地址、端口和认证由 linkport 每次生成。
- 首次实际拨号时启动 `sing-box run -c <临时配置> --disable-color`。规范化 outbound JSON 与 binary 路径相同的并发连接共享子进程;最后一个连接关闭后终止该进程并删除临时目录。
- 运行时因此可能有 linkport 和一个或多个 sing-box OS 进程。“单 EXE”只描述 linkport 交付物,不表示启用该出口后仍只有一个进程。
选择这个边界的原因是 sing-box 的 CLI/JSON 配置比其 Go 包更适合作为跨版本集成契约。官方面向部署的文档以 CLI 和配置 schema 为主;本次核对未找到对 Go library API 的跨版本稳定性承诺。官方 v1.14.2 源码的 `go.mod` 要求 Go 1.25.5,而本仓库因固定 Vitess 依赖使用 Go 1.25.0;直接嵌入当前 sing-box module 会把两套工具链和大量可选协议依赖带入同一构建。让用户自行运行代理虽可减少子进程管理,但会要求另行维护监听端口、认证、配置文件和生命周期,当前版本没有采用该模式。
## 安装与定位
sing-box **不包含在 linkport 源码、Windows 单 EXE 或其他发布包中**。在运行 linkport 的同一台机器安装一个与配置匹配的 sing-box binary:
1. 推荐把绝对路径写入进程环境变量 `LINKPORT_SING_BOX`。
2. 未设置时,linkport 用 `PATH` 查找名为 `sing-box` 的程序(Windows 可解析 `sing-box.exe`)。
3. 修改环境变量或 binary 后重启 linkport;路径不写入资产或 SQLite。
示例:
```powershell
$env:LINKPORT_SING_BOX = 'C:\Tools\sing-box\sing-box.exe'
.\linkport.exe
```
```sh
LINKPORT_SING_BOX=/usr/local/bin/sing-box ./linkport
```
部署应固定并记录 binary 版本。linkport 当前不声明 sing-box 最低或最高版本,也不会根据版本号改写字段;最终兼容性由该 binary 对生成配置及 outbound schema 的解析结果决定。升级 binary 后先重新执行资产“检查配置”,再执行目标连接测试。
## 配置契约
输入可以是一个 UTF-8 outbound JSON object,也可以是包含 `outbounds` 的完整 sing-box JSON 配置或 JSON 文件。程序按 `route.final` 优先、否则按列表顺序自动提取一个远程 outbound;`direct`、`selector` 等本地/组合 outbound 不会被提取。不是分享 URI、订阅链接或数组。原始输入去除首尾空白后最多 **64 KiB(65,536 字节)**,提取结果必须包含小写 `type`;用户提供的 `tag` 会移除,运行时统一改为 `linkport-out`。
例如:
```json
{
"type": "vless",
"server": "proxy.example.com",
"server_port": 443,
"uuid": "00000000-0000-0000-0000-000000000000",
"tls": {
"enabled": true,
"server_name": "proxy.example.com"
}
}
```
linkport 不维护会随 sing-box 发布变化的远程协议白名单。官方 v1.14.2 outbound 目录包含 AnyTLS、HTTP、Hysteria/Hysteria 2、Naive、Shadowsocks、ShadowTLS、Snell、SOCKS、SSH、Trojan、TUIC、VLESS、VMess、WireGuard 等远程类型;具体字段、编译特性和废弃状态仍以部署的 binary 为准。未知但格式合法的远程类型可以进入 binary 检查,以便未来协议不必等待 linkport 发版。
以下能力会在交给 binary 前拒绝:
- 未包含可提取远程 outbound 的完整配置;完整配置中的其他字段只用于定位 outbound,不会保存或传入运行时。
- 单个 outbound 中的完整配置顶层字段,如 `$schema`、`log`、`dns`、`inbounds`、`outbounds`、`route`、`services` 和 `experimental`。
- 不建立单一远程连接或可组合本地路由的类型,如 `direct`、`block`、`bridge`、`selector`、`urltest`、`tor`、`tun` 和 `tailscale`。
- 任意层级的本地文件路径、外部进程、outbound detour、自定义解析器或宿主网络控制字段,包括 `*_path`、`detour`、`*_detour`、`plugin`、`executable_path`、`data_directory`、`extra_args`、`bind_interface`、`routing_mark`、`netns`、`domain_resolver`,以及 WireGuard 的 `system_interface`、`interface_name` 等。
- 深度超过 24(根对象为 0)、总节点超过 4096、单数组超过 1024 项、单对象超过 256 个字段的结构,以及 `__proto__`、`constructor`、`prototype` 或含控制字符的对象键。
这些限制是 linkport 的宿主安全边界,不表示对应字段在 sing-box 中无效。因为子进程继承 linkport 的 OS 身份,允许文件、进程或宿主路由字段会把资产编辑权限扩大为本机资源访问权限。
sing-box 1.14 起可用 `sing-box schema` 生成与该 binary 构建特性一致的 Draft 2020-12 schema;在线 `$schema` 只供编辑器补全,不参与运行时校验。资产本身不能携带 `$schema`。先用对应版本文档编写单 outbound,再以资产“检查配置”作为部署侧校验。
## 运行与测试边界
资产页面的“检查配置”执行:
```text
sing-box check -c <临时配置>/config.json --disable-color
```
检查最多等待 10 秒。它会让安装的 binary 解析完整生成配置并构造实例,但不会调用长期 `run`,也不向用户目标发起 linkport 协议认证;因此通过只证明“当前 binary 接受这份受控配置”,**不证明代理服务器、目标服务或账号可达**。检查失败响应只包含固定安全 `error` 类别和 `durationMs`,不附带连接 `diagnostics` 或 `sing_box_start` 阶段,也不返回 binary stderr 或原配置。
要验证网络链路,应在引用该代理的目标或 TCP 纯转发网关上执行“测试连接”。实际拨号会启动/复用子进程,并记录 `sing_box_start`、`sing_box_connect`、`sing_box_negotiation` 和 `sing_box_authentication`:它完成本地 SOCKS5 连接、协商和随机凭据认证,把目的主机名原样交给 sing-box;协议目标再完成 TLS、握手、认证和查询,纯 TCP 网关只检查连接建立。任何阶段失败都不会回退直连。当前只承载 TCP,不提供 UDP、TUN 或系统代理。
资产检查与运行命令的 stdout/stderr 均丢弃,因为 binary 的错误可能复述 outbound 秘密。对外只返回稳定错误类别。配置检查、进程本地端口就绪和目标连接是三个不同证据,不应互相替代。
## 秘密与本机安全
整个 `singBoxOutbound` 都按秘密处理,而不尝试猜测各协议的密码、UUID、私钥或令牌字段:
- 保存时规范化后随连接资产密封存入 SQLite;管理 API 永不回显正文,只返回 `secretStatus.singBoxOutbound` 和派生的 `singBoxType` 摘要。
- 编辑已有资产时输入框为空;留空保留旧值,只有应用新 JSON 才替换。前端应用后立即清空原始输入。
- 配置历史和导入预览只返回 `secretChanged`,不展示前后 JSON。调用 `RedactBundle` 的模板和脱敏视图会清空正文;口令加密的完整备份和加密 WebDAV 同步仍包含恢复所需秘密,应按凭据材料保护。
- 运行配置关闭 sing-box 日志,并为本地 SOCKS5 入口生成随机账号密码。入口只监听 `127.0.0.1`,不设置系统代理。
- 临时目录请求 `0700`、配置文件请求 `0600`,并在检查结束、进程退出或启动失败后删除。Windows 上最终访问控制还取决于临时目录 ACL 和运行 linkport 的账号;不要与不可信进程共用高权限服务账号。
输入限制降低宿主能力暴露,但 sing-box 仍是与 linkport 同身份运行的第三方程序。只安装可信来源的 binary,核对发布校验值,限制 `LINKPORT_SING_BOX` 和 `PATH` 的写权限,并避免以管理员/root 身份运行。
## 版本、许可证与升级
截至 **2026-09-27**,官方 GitHub 最新非预发布版本是 **v1.14.2**(2026-09-24 发布)。该源码声明 `go 1.25.5`;这是从源码构建或嵌入 module 的要求,不是运行官方预编译 binary 时对 linkport Go 版本的要求。
sing-box 由 SagerNet 上游仓库发布;官方 `LICENSE` 的版权声明人为 nekohasekai,正文采用 GNU GPL v3 或更高版本,并另有衍生作品名称/关联声明限制。linkport 不捆绑或重新发布该程序;若部署者另行分发 sing-box,应自行保留其许可证、版权声明并履行对应版本的发布义务。本文是工程边界说明,不替代法律意见。
官方依据:
- [v1.14.2 release](https://github.com/SagerNet/sing-box/releases/tag/v1.14.2)
- [v1.14.2 go.mod](https://github.com/SagerNet/sing-box/blob/v1.14.2/go.mod)
- [v1.14.2 LICENSE](https://github.com/SagerNet/sing-box/blob/v1.14.2/LICENSE)
- [Outbound 配置目录](https://sing-box.sagernet.org/configuration/outbound/)
- [JSON Schema 说明](https://sing-box.sagernet.org/configuration/schema/)
- [`check` 命令实现](https://github.com/SagerNet/sing-box/blob/v1.14.2/cmd/sing-box/cmd_check.go) 与 [`run -c` 配置读取](https://github.com/SagerNet/sing-box/blob/v1.14.2/cmd/sing-box/cmd_run.go)
## 兼容、迁移与验证
新增字段位于既有加密 `outbounds` 记录,没有 SQLite 表结构迁移。已有 direct、SOCKS5、SSH 和 Kubernetes 配置不变;新版本可以把 sing-box 资产绑定到普通目标。独立 `tcp` 网关当前仍只接受 SOCKS5 或 Kubernetes 转发,不能引用 sing-box 资产。旧版 linkport 不认识新 mode,跨版本回退前须停止服务并保留完整加密备份。
本实现的自动化验证覆盖:单对象/64 KiB/复杂度与危险字段拒绝、tag 规范化、API 和配置差异脱敏、脱敏配置包、受控 loopback SOCKS 配置、随机认证接线,以及 SOCKS5 目标域名不在 linkport 本地解析。测试使用本地 fixture 和伪造协议交互;本次环境未安装 sing-box,也未连接真实代理或目标服务器,因此没有把 fixture 覆盖描述为 live-server 验证。实际命令与结果见 [测试记录](testing.md)。
+6 -6
View File
@@ -2,9 +2,9 @@
## 当前行为(2026-09-21)
Windows、Linux、macOS 使用同一套单文件存储。新建数据目录只需要 `serviceport.sqlite`;管理员、目标、网关、代理、文件资产、历史配置、审计命令、输出、连接事件、内部随机主密钥和自动 TLS 证书/私钥都保存在其中。应用不再读取 `SERVICEPORT_MASTER_KEY`,不提供外部主密钥运行模式,也不再提供 `generate-key` 命令。
Windows、Linux、macOS 使用同一套单文件存储。新建数据目录只需要 `linkport.sqlite`;管理员、目标、网关、代理、文件资产、历史配置、审计命令、输出、连接事件、内部随机主密钥和自动 TLS 证书/私钥都保存在其中。应用不再读取 `LINKPORT_MASTER_KEY`,不提供外部主密钥运行模式,也不再提供 `generate-key` 命令。
默认位置为工作目录 `data/serviceport.sqlite`,可用 `SERVICEPORT_DATA` 指定。Windows 回环管理模式保留当前目录优先、旧 AppData 次之的查找行为;`serviceport data-path` 只查询路径,不初始化数据。Linux/macOS 不使用 Windows AppData。登录、远程管理认证、CSRF 与协议权限限制不因密钥入库而放宽。
默认位置为工作目录 `data/linkport.sqlite`,可用 `LINKPORT_DATA` 指定。Windows 回环管理模式保留当前目录优先、旧 AppData 次之的查找行为;`linkport data-path` 只查询路径,不初始化数据。Linux/macOS 不使用 Windows AppData。登录、远程管理认证、CSRF 与协议权限限制不因密钥入库而放宽。
SQLite 在运行、写入或异常退出后可能存在 `-wal`、`-shm`、`-journal` 文件。这些不是额外的业务数据库,**但不能在运行时删除,也不能在未完成 checkpoint 时只复制主文件**。正常关闭所有数据库连接后才可仅备份/迁移 SQLite。程序被强制终止后应先恢复并正常关闭,或使用 SQLite 一致性备份工具。Windows 使用命名互斥量,Linux/macOS 使用主数据库文件的 advisory flock,不为新库创建 `desktop.lock`。
@@ -15,8 +15,8 @@ SQLite 在运行、写入或异常退出后可能存在 `-wal`、`-shm`、`-jour
## 从旧版本迁移
1. 停止旧服务,备份**整个旧数据目录**,包括 SQLite、WAL、`audit/`、`tls/` 和 `master-key.dpapi`。不要让新旧版本同时运行。
2. Windows DPAPI 旧库:在原 Windows 机器、原用户下运行 `serviceport migrate-data`。该命令迁移后退出,不监听端口、不启动网关、不连接真实目标。正常启动也可执行同样的初始化,但升级建议先运行离线命令。
3. 旧外部主密钥库:运行 `serviceport migrate-key`,在不回显的终端提示中输入原 Base64 密钥。它仅用于一次性解密校验/入库,不重新引入环境变量运行模式。原密钥丢失无法恢复;不会生成替代密钥覆盖旧库。
2. Windows DPAPI 旧库:在原 Windows 机器、原用户下运行 `linkport migrate-data`。该命令迁移后退出,不监听端口、不启动网关、不连接真实目标。正常启动也可执行同样的初始化,但升级建议先运行离线命令。
3. 旧外部主密钥库:运行 `linkport migrate-key`,在不回显的终端提示中输入原 Base64 密钥。它仅用于一次性解密校验/入库,不重新引入环境变量运行模式。原密钥丢失无法恢复;不会生成替代密钥覆盖旧库。
4. 审计分库按日期顺序合并,命令输出分块保持顺序。每个分库在一个事务中写入及记录迁移标记,中断后重试不重复导入;冲突报错。全部完成后不再读取旧审计目录,新日志只写主库。审计分页游标/输出序号是内部定位值,迁移后须重新加载列表,不复用旧页面游标。
5. 自动 TLS 导入旧 `tls/cert.pem` 和 `tls/key.pem`,不更换有效身份;新库自动生成 localhost 自签名证书。损坏、缺半边的旧证书直接报错,不偷偷重建。
6. 迁移不删除旧 DPAPI、审计或 TLS 文件;保留作回退材料。确认迁移成功并停服后,复制**只有 SQLite**到一个新目录即可独立运行,旧目录可作为归档。后续不再需要外部密钥文件。不要用旧版程序直接打开升级后的数据库;回退请恢复完整升级前备份。
@@ -25,9 +25,9 @@ SQLite 在运行、写入或异常退出后可能存在 `-wal`、`-shm`、`-jour
## TLS 与可移植性的范围
自动证书默认覆盖 localhost、127.0.0.1、::1,有效期一年。停止服务后运行 `serviceport export-ca`,标准输出为公开 CA PEM,可另存后配置客户端信任,私钥不导出。该导出文件只供客户端使用,不是服务启动依赖。
自动证书默认覆盖 localhost、127.0.0.1、::1,有效期一年。停止服务后运行 `linkport export-ca`,标准输出为公开 CA PEM,可另存后配置客户端信任,私钥不导出。该导出文件只供客户端使用,不是服务启动依赖。
显式的 `SERVICEPORT_TLS_CERT` / `SERVICEPORT_TLS_KEY` 自定义证书仍优先,适用于正式域名/证书轮换;使用者主动配置的外部文件仍需自行管理。目标中主动填写的 SSH/kubeconfig 等文件路径也不会自动吸收入库;要完全可移植,请上传为文件资产并使用资产引用。WebDAV 配置备份仍是原配置包语义,不等同于包含审计和主密钥的 SQLite 灾备。
显式的 `LINKPORT_TLS_CERT` / `LINKPORT_TLS_KEY` 自定义证书仍优先,适用于正式域名/证书轮换;使用者主动配置的外部文件仍需自行管理。目标中主动填写的 SSH/kubeconfig 等文件路径也不会自动吸收入库;要完全可移植,请上传为文件资产并使用资产引用。WebDAV 配置备份仍是原配置包语义,不等同于包含审计和主密钥的 SQLite 灾备。
## 验证
+1 -1
View File
@@ -4,6 +4,6 @@
原 `POST /api/v1/tools/sqlite/inspect` 和 `POST /api/v1/tools/sqlite/query` 接口已撤销;通过正常认证及 CSRF 校验后的请求返回 404。依赖这些接口的调用方应停止调用。
本次清理包含专用 Vue 组件、状态模块、API 域文件、`internal/sqlitebrowser` 模块及其专用测试,并更新导航测试。没有数据库迁移,不删除或修改任何本地数据库文件;ServicePort 自身 SQLite 持久化、配置管理中的一致性备份和停服恢复功能继续保留,见 [单 SQLite 持久化](single-file-storage.md) 和 [数据库备份与恢复](database-snapshots.md)。
本次清理包含专用 Vue 组件、状态模块、API 域文件、`internal/sqlitebrowser` 模块及其专用测试,并更新导航测试。没有数据库迁移,不删除或修改任何本地数据库文件;LinkPort 自身 SQLite 持久化、配置管理中的一致性备份和停服恢复功能继续保留,见 [单 SQLite 持久化](single-file-storage.md) 和 [数据库备份与恢复](database-snapshots.md)。
实际验证结果见 [变更记录](change-rationale.md#移除-sqlite-文件菜单及功能2026-09-25)。
+88 -40
View File
@@ -1,24 +1,34 @@
# Target Accounts
New and edited target drafts can be tested before saving. Select an account in
the editor footer; an account-free Redis draft has no selector and tests with an
empty `accountId`. The test uses the current draft and retains omitted saved
the editor footer; an account-free Redis or MongoDB draft has no selector and
tests with an empty `accountId`. The test uses the current draft and retains omitted saved
credentials by account ID. This avoids persisting trial connection settings.
See [draft connection testing](target-connection-test.md) for protocol coverage,
temporary SSH host confirmation and persistence boundaries.
A target maintains one account list for all protocols. Accounts share the endpoint,
proxy route and SSH host identity. Each account has a stable ID, label, username
and password, or SSH private key and passphrase. Redis supports 0 to 32 accounts;
all other account-based protocols support 1 to 32. TCP remains account-free and
does not expose this list. There is no separate primary credential form.
and password, or SSH private key and passphrase. Redis and MongoDB support 0 to
32 accounts; all other account-based protocols support 1 to 32. TCP remains
account-free and does not expose this list. There is no separate primary
credential form.
## Empty Passwords And Unauthenticated Targets
In the target editor's accounts tab, a new password-based account may leave its
password empty. A username is still required, and the upstream server decides
whether that account may connect with an empty password. This does not bypass
the server's authentication rules. SSH/SFTP private-key login remains available.
password empty. A username is required except for Redis password-only
authentication, and the upstream server decides whether a named account may
connect with an empty password. This does not bypass the server's authentication
rules. SSH/SFTP private-key login remains available.
Redis follows its protocol's three authentication forms. An empty account list
or an account using `authMethod: "none"` sends no `AUTH`; an empty username with
a non-empty password sends `AUTH password`; and a non-empty ACL username sends
`AUTH username password` (the Redis `default` identity is compatible with the
single-argument form). A password-mode Redis account with both fields empty is
invalid, and whitespace is not treated as an empty username. MongoDB and every
other named-account protocol continue to require a username for password mode.
For an existing account, a blank password input retains the saved password. Select
“使用空密码(清除已保存密码)” to explicitly use an empty password. Draft tests and saving
@@ -37,15 +47,42 @@ anonymously. Redis Sentinel keeps its existing explicit compatibility option
the selected data account and may retry the Sentinel without authentication.
Selecting a Sentinel account disables that fallback.
Redis can also save an empty account list when the upstream service has no
authentication configured. In that state, draft tests, saved-target tests,
Redis and MongoDB can also save an empty account list when the upstream service
has no authentication configured. In that state, draft tests, saved-target tests,
topology discovery, health checks and gateway connections use an empty account
selection and never send upstream `AUTH`. Adding any Redis account immediately
restores the normal rule: every gateway, test and discovery operation must select
a valid account ID. An empty list is therefore not a fallback after rejected
credentials, and removing all accounts remains blocked while a saved account is
still referenced by a gateway. Gateway client accounts and passwords are always
required; this option changes only upstream Redis authentication.
selection. Redis sends no upstream `AUTH`, and MongoDB skips SCRAM. Adding any
account immediately restores the normal rule: every gateway, test and discovery
operation must select a valid account ID. An empty list is therefore not a
fallback after rejected credentials, and removing all accounts remains blocked
while a saved account is still referenced by a gateway.
Redis and MongoDB gateways may likewise leave the gateway account list empty and
accept clients without local authentication. A Redis gateway account may instead
have an empty username and a non-empty password; clients then use `AUTH password`
or the equivalent `HELLO ... AUTH default password`. That password-only identity
and an explicit `default` gateway username cannot coexist because both represent
the same Redis protocol identity. Other account-based gateway protocols still
require at least one client account and a username; TCP remains account-free.
Gateway authentication remains independent of upstream authentication.
## Authentication Compatibility Matrix
| Service | Target authentication | Gateway client authentication |
| --- | --- | --- |
| Redis | None, password only, or ACL username and password | None, password only, or ACL username and password |
| MongoDB | None, or SCRAM username and password | None, or local SCRAM username and password |
| MySQL | Username and optional password | Username and password |
| PostgreSQL | Username and optional password | Username and password |
| SQL Server | Username and optional password | Username and password |
| RabbitMQ | Username and optional password | Username and password |
| HTTP / HTTPS | Explicit no-auth account, or Basic username and optional password | Basic username and password |
| SSH / SFTP | Username with password or private key | Username with password or authorized key |
| TCP | No protocol account | No protocol account |
"Optional password" means linkport can send an empty password for a named
account; the target server still decides whether it is valid. It does not mean
linkport retries a rejected login anonymously. Non-SSH/SFTP services reject
private-key and unknown authentication methods instead of silently ignoring them.
An unauthenticated target must actually permit the requested operation. A successful
handshake or metadata test does not grant access to all databases or commands.
@@ -76,15 +113,20 @@ repeated scrolling. The selector and grouped fields keep editor height independe
of account count. See the [change rationale](change-rationale.md) for design choices,
data compatibility and validation boundaries.
An empty account `targetId` inherits the gateway default. An explicit `targetId`
requires an account belonging to that target; it never inherits an account from
the default target, even when account IDs match. A gateway needs no default target
when every enabled account has an independent mapping. Each route uses its own
endpoint, outbound transport, TLS settings and credentials. Backups and WebDAV sync
include these mappings; referenced targets and credentials remain protected.
For an account-free Redis target, `targetAccountId` is intentionally empty and
the target service itself is the valid destination. This empty ID is accepted
only while that Redis target has no accounts.
New and edited mappings are selected from each gateway account. An explicit
account `targetId` requires an account belonging to that target; it never reuses
an account from another target, even when account IDs match. Legacy gateway-level
`targetId` and `targetAccountId` values remain readable for API and stored-data
compatibility, but the gateway editor no longer exposes a shared service selector
for any protocol. When a legacy gateway with real gateway accounts is edited and
saved, the draft expands the legacy target into every account's `targetId` and
`targetAccountId`, then clears the top-level mapping. Account-free Redis, MongoDB
and TCP gateways retain the top-level fields because an empty gateway-account ID
is their persisted account endpoint. Each account route uses its own endpoint,
outbound transport, TLS settings and credentials. Backups and WebDAV sync include
these mappings; referenced targets and credentials remain protected. An empty
target-account ID is valid only for TCP or while a Redis/MongoDB target still has
no accounts.
Drag an individual gateway account in “拖拽连线” to change only its route. Each
account has a separate line, including accounts sharing a destination. Clicking
@@ -93,29 +135,26 @@ changes for new connections without restarting the listener. Redis clients must
reconnect to authenticate as an account routed to a different target.
Many gateway accounts can map to the same target account, even when the target
has multiple accounts. Select the default target account in the gateway editor;
accounts without an override inherit it. Use “统一应用到全部网关账号” to clear all
per-account overrides in the draft, then save. New gateway accounts also inherit
the default. Each account displays its effective target account. Gateway passwords,
has multiple accounts. Select the destination in each gateway account's target
tab. Each account displays its effective target account. Gateway passwords,
source restrictions and access policies remain separate; sharing an upstream
account does not merge gateway identities or grant access to other target accounts.
In the target editor, use the accounts tab to maintain accounts. In the gateway
editor, select the target and then its target account. This development version
editor, select the target and then its target account for each gateway account.
This development version
does not support the old credential schema. Old account-based targets require
accounts to be configured again; old gateways require an explicit account
selection before they can start, except when they bind an account-free Redis
target. There is no reserved default account or credential migration.
selection before they can start, except when they bind an account-free Redis or
MongoDB target. There is no reserved default account or credential migration.
Changing targets resets the account selection: a single account is selected
automatically, while multiple accounts require an explicit choice. In “拖拽连线”,
drag a gateway directly onto a target account, or click the gateway and then the
account. This saves the shared default and clears all per-account overrides while
preserving gateway credentials and policies. No account selection dialog is needed.
Single-account targets show their username inside the card while connecting and
accept drops on the card without expanding a separate account list.
Target accounts stay collapsed during normal browsing and expand for compatible
targets when a gateway is selected or dragged. Saving or cancelling the connection
collapses them again; gateway cards summarize their current target accounts.
drag or click a gateway-account row and then a target-account row. Both service
cards are informational and do not implicitly select an account. Redis and
MongoDB configurations with empty account arrays display a UI-only “无认证账号”
row on the corresponding side; TCP displays “空账号”. Saving any of these routes
keeps the account ID empty and does not create a credential record. Gateway cards
continue to summarize current routes.
The gateway authenticates clients with gateway credentials and connects upstream
with the selected account. A root account is a direct root login, not sudo or
@@ -159,3 +198,12 @@ reveal functions. Gateway authentication does not grant management API access.
Validation covers account selection, redaction, credential retention, deletion
protection and local SSH fixture authentication. It does not establish that a
particular production host permits root login.
## Account-free Redis and MongoDB
Redis and MongoDB targets may use an empty `accounts` array to represent an
upstream that accepts anonymous connections. Redis and MongoDB gateways may
also use an empty `gatewayAccounts` array to accept clients without gateway
credentials. Once an account is configured, the existing account-selection
and credential requirements apply again. The connection canvas represents each
empty Redis/MongoDB list as one “无认证账号” endpoint; TCP uses an equivalent
“空账号” endpoint. Their persisted IDs remain empty.
+16 -13
View File
@@ -1,21 +1,18 @@
# 未保存配置的连接测试
# 已保存配置的连接测试
## 改动原因
原目标编辑器只允许测试已保存配置,地址或账号一有修改就要求先保存。这会迫使用户为了验证 MongoDB 连接串、集群、代理或凭据而先改动正式配置,也让新建目标的排错多一次保存步骤。现在新建和编辑目标均可直接测试当前草稿,覆盖 MySQL、MongoDB、PostgreSQL、SQL Server、Redis、RabbitMQ、HTTP、HTTPS、SSH、SFTP。
草稿测试会让连接结果与后端实际保存的数据产生偏差,尤其是账号密码、出口代理和集群设置。现在目标必须先保存,连接测试只调用已保存目标的测试接口;保存成功后目标编辑页保持打开,便于立即测试和继续调整。
## 使用方式与行为
在目标服务编辑窗口填写连接参数,选择底部的测试账号,点击“测试连接”。只有一个账号时自动选中;多个账号时可以选择尚未保存的新账号。Redis 未配置任何账号时不显示测试账号选择器,直接按无上游认证测试。测试不要求先填写目标名称和账号名称,仍要求有效地址、端口及所选认证方式需要的凭据。MongoDB、Redis、HTTP(S) 选择“无认证”时不要求用户名或密码;其他具名账号可使用空密码,是否成功由目标服务决定。
在目标服务编辑窗口保存配置后,选择底部的测试账号并点击“测试连接”。只有一个账号时自动选中;多个账号时选择已保存账号。Redis 或 MongoDB 未配置任何账号时不显示测试账号选择器,直接按无上游认证测试。编辑器存在未保存修改、或目标尚未创建时,测试按钮不会发起网络请求,而是提示先保存。
- 使用点击时的地址、端口、TLS/CA、认证库、集群节点、代理和所选账号快照。MongoDB 连接串解析后即可测试副本集参数。
- 已有账号的密码、私钥或口令留空时,按账号 ID 使用已保存凭据;填写新值只用于本次测试。私钥、证书、CA、Kubeconfig 资产及代理引用在服务端解析。
- 勾选“使用空密码(清除已保存密码)”时测试显式空密码;选择“无认证”时测试不发送该账号原有凭据。两者均只影响当前草稿,点击保存后才清除已存密码或凭据。网关登录认证不受影响;普通目标认证失败不会降级为无认证重试,Redis Sentinel 的明确兼容例外见下一项。
- 未选中且未完成的其他账号不阻止测试;集群使用独立 Sentinel 认证账号时也校验并带入该账号。
- Redis 账号列表为空时,单节点、Cluster 与 Sentinel 数据连接均不发送 `AUTH`。若只有 Sentinel 要求独立认证,添加 Sentinel 账号会使列表不再为空,因此还需创建一个 `authMethod: "none"` 的数据账号并在测试时选择它,再单独选择 Sentinel 账号。普通节点、Cluster/replication 及显式 Sentinel 账号认证失败时不会降级;Sentinel 的“自动尝试目标账号或无认证”选项保留既有兼容回退。
- 测试成功和失败都保留编辑草稿。结果显示本次测试的目标与账号;配置仍需点击“保存配置”才会落库。
- 关闭连接测试结果页会同时清除编辑器中的本次成功/失败摘要和“实测连接阶段”,避免旧结果在返回编辑页后继续显示;当前草稿与测试账号选择保持不变,再次测试会生成新结果。
- 测试不创建目标、不写配置历史、不保存主机探测记录,也不修改正在使用的网关路由。测试会按现有协议实现连接并认证目标服务;外部服务可能记录连接或认证日志。
- 使用后端保存的地址、端口、TLS/CA、认证库、集群节点、代理和所选账号。MongoDB 连接串等修改必须保存后才会参与测试。
- 账号密码、私钥或口令由服务端从已保存凭据解析;测试请求不携带草稿配置或明文凭据。
- 测试成功和失败都保留编辑页。保存成功后编辑器刷新为服务端返回的配置视图,并继续保持打开。
- 关闭连接测试结果页会清除编辑器中的本次成功/失败摘要和“实测连接阶段”,避免旧结果继续显示;再次测试会生成新结果。
- 测试不修改配置历史、不保存主机探测记录,也不修改正在使用的网关路由。测试会按现有协议实现连接并认证目标服务;外部服务可能记录连接或认证日志。
SSH/SFTP 仍核对主机身份。已有目标且地址、端口、代理未变时沿用已确认主机密钥;新目标或连接位置改变时先探测公开指纹,不提交认证凭据。用户核对并确认后,再用该指纹固定本次测试连接。此确认仅对本次测试有效,不代替保存后启动网关所需的持久指纹确认;取消可继续编辑。
@@ -23,8 +20,14 @@ SSH/SFTP 仍核对主机身份。已有目标且地址、端口、代理未变
## API 与兼容性
新增 `POST /api/v1/targets/test`,请求包含 `config`(目标草稿)、`accountId`(所选账号;零账号 Redis 为空)、可选 `targetId`(用于读取已有凭据)和 `hostFingerprint`(SSH 本次确认)。接口沿用登录、令牌权限和 CSRF 防护,拒绝注入目标 ID 或主机密钥。除 TCP 和账号列表为空的 Redis 外,空 `accountId` 均被拒绝。需要核对 SSH 指纹时返回 `409`、`stage: draft_host_confirmation` 和公开指纹;连接失败返回脱敏错误阶段。
目标测试使用 `POST /api/v1/targets/{id}/test?accountId=...`,只读取后端已保存的目标和账号。接口沿用登录、令牌权限和 CSRF 防护;连接失败返回脱敏错误阶段。草稿测试接口 `POST /api/v1/targets/test` 不再由目标编辑器调用,保留后端兼容性以避免影响其他客户端。
接口在内存中合并、解析并校验草稿,复用各协议的连接测试实现,最长请求上下文为 25 秒;无数据库迁移。列表中的已保存目标测试入口保持原行为。测试及保存共用前端凭据省略规则,避免测试成功但保存时使用另一套凭据规则。
保存接口返回完整资源视图后,前端刷新目标缓存并保留当前编辑页;无 API、数据库格式或迁移变更。测试与网关列表中的已保存目标测试共用服务端连接实现。
验证见 [测试记录](testing.md)。接口协议分派使用记录型 fixture,SSH 身份确认使用本地隔离 SSH 服务;浏览器测试模拟远端结果,不表示已验证所有生产数据库版本。
## 网关草稿测试
网关新建和编辑弹窗的底部也提供“测试连接”。测试使用当前网关草稿、当前选中的网关账号及其目标映射;Redis 或 MongoDB 无客户端账号时不显示账号选择器,并使用空的上游账号选择。编辑已保存网关时,留空的密码按网关账号 ID 复用已保存凭据。测试不会保存草稿、创建网关、修改历史或启动网关。
新增 `POST /api/v1/proxies/test`,请求包含可选 `proxyId`、`gatewayAccountId` 和 `config`。服务端只在内存中合并草稿,解析目标和资产后复用已保存网关的协议测试流程;单次测试只提交所选网关账号的映射,避免未完成的其他账号阻塞当前测试。接口继续使用登录、令牌权限和 CSRF 防护,连接失败返回脱敏阶段诊断。网关列表上已有的 `POST /api/v1/proxies/{id}/test` 行为不变。
+6 -6
View File
@@ -12,7 +12,7 @@ Kubernetes 资源查询会返回声明的 TCP 端口。选择资源后,界面
Kubeconfig 纯端口转发新增可选的“预检 Kubernetes 权限”。预检通过 `SelfSubjectAccessReview` 检查运行时所需的目标资源读取权限、非 Pod 资源的 Pod 列表权限,以及 `create pods/portforward` 权限。返回仅包含固定的动作、资源、子资源、`allowed/denied/unknown` 状态和稳定错误码,不回传 Kubeconfig、Token、证书、API Server 原始原因或评估错误。
预检是配置阶段的诊断,不参与保存,也不会替代启动和连接测试:`ready` 只说明授权检查允许,不证明资源存在、Pod 正在运行、Service 端口匹配或 SPDY 转发成功;`unknown` 表示集群不支持或不允许权限审查,仍应以实际启动/测试为准。上传/内联的 Kubeconfig 必须使用 `certificate-authority-data`、`client-certificate-data`、`client-key-data` 或内联 Token,不能通过 `tokenFile`、`certificate-authority`、`client-certificate` 或 `client-key` 读取 serviceport 主机文件,也不能使用 `exec`/`auth-provider` 登录插件。显式本地 Kubeconfig 文件路径及其中登录插件的兼容边界保持不变。
预检是配置阶段的诊断,不参与保存,也不会替代启动和连接测试:`ready` 只说明授权检查允许,不证明资源存在、Pod 正在运行、Service 端口匹配或 SPDY 转发成功;`unknown` 表示集群不支持或不允许权限审查,仍应以实际启动/测试为准。上传/内联的 Kubeconfig 必须使用 `certificate-authority-data`、`client-certificate-data`、`client-key-data` 或内联 Token,不能通过 `tokenFile`、`certificate-authority`、`client-certificate` 或 `client-key` 读取 linkport 主机文件,也不能使用 `exec`/`auth-provider` 登录插件。显式本地 Kubeconfig 文件路径及其中登录插件的兼容边界保持不变。
验证状态:权限预检后端 fixture、API 回归、前端类型检查和 7 项端口/预检逻辑测试通过,全量 `go vet` 通过。生产前端构建受当前 Windows 沙箱 `spawn EPERM` 阻止,未生成包含本次界面的新程序,因此没有执行 Playwright 或真实 Kubernetes 集群验收;详细结果见 [测试记录](testing.md)。
@@ -118,11 +118,11 @@ Kubernetes 网络验证使用本机隔离 TLS API fixture,检查实际拨号
### 本轮实际验证
Go 1.25.0 的全量 `go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 已通过。首次 `scripts/build.ps1 -SkipTests` 在 Node 22.10 下构建成功,但触发 Vite 版本要求警告;这不表示该 Node 版本满足 Vite 支持要求。补充图视图调整后,使用机器已有 Node 22.22.3 重新执行同一构建脚本,完成 npm build 和 Go 1.25.0 编译,生成最终程序 `artifacts/tcp-independent/serviceport.exe`;此次无 Node 版本警告,仅保留 chunk 大小警告。
Go 1.25.0 的全量 `go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 已通过。首次 `scripts/build.ps1 -SkipTests` 在 Node 22.10 下构建成功,但触发 Vite 版本要求警告;这不表示该 Node 版本满足 Vite 支持要求。补充图视图调整后,使用机器已有 Node 22.22.3 重新执行同一构建脚本,完成 npm build 和 Go 1.25.0 编译,生成最终程序 `artifacts/tcp-independent/linkport.exe`;此次无 Node 版本警告,仅保留 chunk 大小警告。
最终重建前,Playwright `tcp-independent.spec.ts`、`tcp-forwarding.spec.ts` 共 2 项通过。新增 API 测试 `TestIndependentTCPForwardTwoRoutes` 使用本地带密码 SOCKS5 fixture,将两条映射转发到本地 TCP fixtures 并校验二进制字节;不是对用户真实 `192.168.11.16` 网络或业务服务的验收。新增 `tcp_independent_validation_test.go` 的 5 项边界测试(21 项子测试)通过,覆盖无效代理/字段、账号冲突、部分更新、模式切换、引用重绑定、代理改类型及影响范围响应脱敏。
最终使用 Node 22.22.3,设置 `SERVICEPORT_E2E_BINARY=artifacts/tcp-independent/serviceport.exe`,在隔离端口 `19281/19282` 完整运行 Playwright,**121/121 通过(6.6 分钟)**,包含最终图视图修复;日志为 `artifacts/tcp-independent-playwright-final.log`。此前 Node 22.10 因 `node:sqlite` 不可用导致测试发现失败;仅在当前 shell 通过 `fnm use 22.22.3` 切换机器已有版本后,一次运行在 59 项时被工具 200 秒时限中断,该中断不代表用例失败。最终延长时限后的完整重跑已通过;详细记录见 `testing.md`。
最终使用 Node 22.22.3,设置 `LINKPORT_E2E_BINARY=artifacts/tcp-independent/linkport.exe`,在隔离端口 `19281/19282` 完整运行 Playwright,**121/121 通过(6.6 分钟)**,包含最终图视图修复;日志为 `artifacts/tcp-independent-playwright-final.log`。此前 Node 22.10 因 `node:sqlite` 不可用导致测试发现失败;仅在当前 shell 通过 `fnm use 22.22.3` 切换机器已有版本后,一次运行在 59 项时被工具 200 秒时限中断,该中断不代表用例失败。最终延长时限后的完整重跑已通过;详细记录见 `testing.md`。
## 用途和边界
@@ -136,9 +136,9 @@ Go 1.25.0 的全量 `go test ./... -count=1 -timeout=120s` 和 `go vet ./...`
2. 连接方式可选直连、SSH/SOCKS5 连接资产,或 K8s 资源 port-forward。K8s 目标配置 Kubeconfig、命名空间和 StatefulSet 等资源以及 Kubernetes 资源端口(工作负载资源对应容器端口),不维护 Pod IP。
3. 在「网关服务」新建相同类型,填写监听端口、来源白名单和最大连接数。默认监听地址为 `0.0.0.0`,不需要网关账号。
4. 选择 TCP 目标,或在「拖拽连线」将网关卡片内的连接区拖到 TCP 目标卡片。卡片顶部用于移动节点位置,画板支持缩放和全屏,见 [服务连线画板](connection-canvas.md)。TCP 映射不使用账号行,也不绑定其他协议类型的目标;已有数据库目标保持原来的账号映射语义。
5. 测试连接后启动。客户端连接网关的地址和监听端口,由目标服务完成认证。运行中切换目标仅影响新连接,已有连接保留原目标;停止网关会断开现有连接。
5. 测试连接后启动。客户端连接网关的地址和监听端口,由目标服务完成认证。运行中切换目标会保持监听,但保存成功后断开现有连接;客户端重连后使用新目标。停止网关同样会断开现有连接。
例如:`0.0.0.0:27018 → K8s StatefulSet:27017`。客户端的 MongoDB authSource、认证机制和密码直接交给 MongoDB,此模式不执行 ServicePort 的 MongoDB SCRAM 认证。
例如:`0.0.0.0:27018 → K8s StatefulSet:27017`。客户端的 MongoDB authSource、认证机制和密码直接交给 MongoDB,此模式不执行 LinkPort 的 MongoDB SCRAM 认证。
## 测试、监控与审计
@@ -181,6 +181,6 @@ go test ./internal/gateway ./internal/api ./internal/protocol/tcpproxy -run 'Tes
## 验证范围
新增后端测试使用隔离 SQLite、本机 TCP 测试服务和受控 handler,覆盖字节透传、半关闭、取消清理、来源/连接数限制、旧会话保留、新目标切换、无账号创建与测试、各类出口配置和定时健康检查。
新增后端测试使用隔离 SQLite、本机 TCP 测试服务和受控 handler,覆盖字节透传、半关闭、取消清理、来源/连接数限制、目标切换断开旧会话并让重连使用新目标、无账号创建与测试、各类出口配置和定时健康检查。
Playwright 使用新构建程序,验证界面默认 `0.0.0.0`,运行测试时显式改为 loopback 并分配临时端口,实际传输包含 NUL 和非 UTF-8 的测试字节。不是生产服务、真实 Kubernetes 或 MongoDB 的连接验收;验证命令与结果见 `testing.md`。
+145 -71
View File
@@ -4,8 +4,8 @@
- 前端管理入口改为代理;Kubeconfig、SSH、SOCKS5、sing-box 独立创建,目标和 TCP 网关按类型引用。新增 `network-proxies.test.mjs` 11/11,覆盖切换清空、不完整/失效引用阻止保存、只提交 ID、Kubeconfig 无 host、测试可用性、旧内联兼容和新 API 错误的中文/英文/日文映射。
- 逐文件执行全部 15 个 `web/tests/*.test.mjs`。首轮仅 `tcp-kube-forward` 和 `sing-box` 中两个旧“资产”文案断言失败,更新为当前代理/配置称呼后分别 7/7、10/10 通过,其余 13 个文件通过。
- `npm.cmd --prefix web run build` 的 Vue 类型检查通过;Vite 启动 esbuild 报 `spawn EPERM`。收尾文案修改后单独 `vue-tsc --noEmit` 也通过。`web/dist/.gitkeep` 保留,没有生成包含当前界面的新 executable,未用旧程序代替要求 `SERVICEPORT_E2E_BINARY` 的浏览器验收。相关 Playwright 用例已适配代理创建、上传、目标/TCP 关联及旧文件搜索编辑;使用工作区 TEMP/TMP/PWTEST_CACHE_DIR 执行全量 `--list` 发现 59 文件、134 项,仅是解析/发现检查,不代表浏览器通过。
- 使用 `GOTOOLCHAIN=go1.25.0`,TEMP/TMP/GOTMPDIR 指向 `.tmp/refactor-full-test`、GOCACHE 指向 `.tmp/kube-test/gocache`,执行 `go test -p=1 ./... -count=1 -timeout=120s`。只有六项已知环境失败:`cmd/serviceport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore`、`internal/store/TestSnapshotRejectsIncompleteAndSidecars`。错误分别是 Windows Access is denied 或不能创建完整便携 SQLite 快照;其余包/用例通过,全量不记为通过。
- `npm.cmd --prefix web run build` 的 Vue 类型检查通过;Vite 启动 esbuild 报 `spawn EPERM`。收尾文案修改后单独 `vue-tsc --noEmit` 也通过。`web/dist/.gitkeep` 保留,没有生成包含当前界面的新 executable,未用旧程序代替要求 `LINKPORT_E2E_BINARY` 的浏览器验收。相关 Playwright 用例已适配代理创建、上传、目标/TCP 关联及旧文件搜索编辑;使用工作区 TEMP/TMP/PWTEST_CACHE_DIR 执行全量 `--list` 发现 59 文件、134 项,仅是解析/发现检查,不代表浏览器通过。
- 使用 `GOTOOLCHAIN=go1.25.0`,TEMP/TMP/GOTMPDIR 指向 `.tmp/refactor-full-test`、GOCACHE 指向 `.tmp/kube-test/gocache`,执行 `go test -p=1 ./... -count=1 -timeout=120s`。只有六项已知环境失败:`cmd/linkport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore`、`internal/store/TestSnapshotRejectsIncompleteAndSidecars`。错误分别是 Windows Access is denied 或不能创建完整便携 SQLite 快照;其余包/用例通过,全量不记为通过。
- `go vet -p=1 ./...` 通过。新增 model/store/API 覆盖全部四种代理、普通网关继承、持久化重开、备份恢复、缺失引用、代理改型校验、Kubeconfig 间接影响与运行拦截、API 脱敏。原 SOCKS5 本地两路字节转发 fixture 同时覆盖 legacy 与 proxy 模式。
- 源码交叉检查确认保存/Get/List/Test/导入均解析共享 Kubeconfig,持久化只留引用,管理 API 清除秘密。没有连接真实 Kubernetes 集群、SSH/sing-box 服务或真实目标,也没有部署、迁移或修改真实 data。
@@ -14,7 +14,7 @@
## 2026-09-27 新建资产类型直选
- `AssetsManager.vue` 的类型下拉改为表单顶部可换行的六个单选项,沿用既有类型数据、切换清理和编辑锁定逻辑。
- `npm.cmd --prefix web run build` 的 Vue 类型检查通过;Vite 加载配置时启动 esbuild 报 `spawn EPERM`,没有新生产资源和 executable。`web/dist/.gitkeep` 保留,未用旧程序执行要求 `SERVICEPORT_E2E_BINARY` 的浏览器验收。
- `npm.cmd --prefix web run build` 的 Vue 类型检查通过;Vite 加载配置时启动 esbuild 报 `spawn EPERM`,没有新生产资源和 executable。`web/dist/.gitkeep` 保留,未用旧程序执行要求 `LINKPORT_E2E_BINARY` 的浏览器验收。
- 更新 `asset-remarks.spec.ts`、`file-assets.spec.ts`、`project-scope.spec.ts`、`scope-catalog.spec.ts` 的既有选择方式及编辑类型禁用断言。Playwright `--list` 发现 4 项,仅表示用例可发现,不代表执行通过。
- `git diff --check` 通过。纯前端控件调整,没有后端代码、数据库或真实 data 变更,未重复执行上一节已记录的 Go 全仓测试。
@@ -24,13 +24,13 @@
实际验证:
- `GOTOOLCHAIN=go1.25.0`,Go 的 `TEMP`、`TMP`、`GOTMPDIR` 指向工作区 `.tmp/refactor-full-test`,`GOCACHE` 使用 `.tmp/kube-test/gocache`。包级检查后执行 `go test -p=1 ./... -count=1 -timeout=120s`。仅复现原有 6 项环境失败:`cmd/serviceport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore`、`internal/store/TestSnapshotRejectsIncompleteAndSidecars`;分别为 Windows 权限拒绝或无法生成完整便携快照。其余用例/包通过,全量结果不记为通过。
- `GOTOOLCHAIN=go1.25.0`,Go 的 `TEMP`、`TMP`、`GOTMPDIR` 指向工作区 `.tmp/refactor-full-test`,`GOCACHE` 使用 `.tmp/kube-test/gocache`。包级检查后执行 `go test -p=1 ./... -count=1 -timeout=120s`。仅复现原有 6 项环境失败:`cmd/linkport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore`、`internal/store/TestSnapshotRejectsIncompleteAndSidecars`;分别为 Windows 权限拒绝或无法生成完整便携快照。其余用例/包通过,全量结果不记为通过。
- `go vet -p=1 ./...` 通过。Go telemetry 尝试写入用户目录的 `Access is denied` 提示未影响 vet 和通过包的退出码。
- 新增运行回归覆盖协议工厂映射/注册表隔离、panic 后释放名额、重复认证计数、TCP 不计认证、Sentinel 账号切换、拓扑拒绝、64 节点上限、取消与未知协议。全部协议子包 fixture 通过;没有配置真实协议服务器环境变量。
- 新增后台回归覆盖手动告警检查不投递、拒绝 HTTP 重定向、成功后不重复投递;同步取消在途请求、拒绝并发运行、关闭后释放租约和记录失败原因。alerts、backup、syncjob 全包通过;store 除上述既有快照项外通过。
- 启动搬移后,已有 version JSON/无数据初始化、data-path 参数与无初始化、startup banner 边界测试通过。API 的会话/Token/CSRF/网关操作及确认绑定回归通过;后续全仓执行仅 API 快照项失败。
- 逐文件使用 `node web/tests/<file>.test.mjs` 执行全部 14 个逻辑测试文件。首轮 13 个文件通过,`persistent-target.test.mjs` 中旧重启提示断言失败;将测试对齐此前已实现的保存配置后重试指引后,该文件 6/6 通过。没有改产品行为。新增 `connection-draft.test.mjs` 5、`secret-reveal.test.mjs` 5、`audit-pages.test.mjs` 4 项全部通过;通过直接加载实际 TypeScript/Vue 模块验证载荷和延迟响应。
- `npm.cmd --prefix web run build` 的 `vue-tsc --noEmit` 阶段通过,Vite 加载配置时 esbuild 子进程仍报 `spawn EPERM`。`web/dist/.gitkeep` 保留。没有生成包含本轮前端的新 executable,因此未运行要求 `SERVICEPORT_E2E_BINARY` 指向新程序的 Playwright,未用旧产物代替验收。原生全屏/ResizeObserver 和真实页面生命周期仍需浏览器验收。
- `npm.cmd --prefix web run build` 的 `vue-tsc --noEmit` 阶段通过,Vite 加载配置时 esbuild 子进程仍报 `spawn EPERM`。`web/dist/.gitkeep` 保留。没有生成包含本轮前端的新 executable,因此未运行要求 `LINKPORT_E2E_BINARY` 指向新程序的 Playwright,未用旧产物代替验收。原生全屏/ResizeObserver 和真实页面生命周期仍需浏览器验收。
- 启动/API/model、runtime、后台、前端分别进行独立源码交叉审查,未发现本轮具体回归;`git diff --check` 通过。源码审查不等于 race 或浏览器执行。
所有自动化使用隔离数据库、本机协议/HTTP fixture 或注入请求,没有连接真实目标、部署或执行数据迁移。
@@ -43,60 +43,60 @@
- 使用 Go 1.25.0,SOCKS5 搬移后先执行已有聚焦回归;策略完成后 `go test -p=1 ./internal/outbound -count=1 -timeout=120s` 与该包 vet 通过。新增用例检查每次拨号校验、未知模式不触达目标、配置和调用者期限下握手关闭、5 秒/30 秒默认及子 context 释放。
- 网关业务搬离 server.go 后,API 原有会话、CSRF、凭据脱敏、启停保存失败和账号映射聚焦回归通过;完成命令表后新增 `TestGatewayActionRoutingBoundaries` 与 `TestGatewayActionConfirmationRetainsSessionBinding` 通过。后者注入隔离 probe,验证其他会话拒绝、本会话确认成功及一次性消费,不会连接真实 SSH 服务。
- `go test -p=1 ./... -count=1 -timeout=120s` 已执行。仍只有 6 项既有 Windows ACL/SQLite 快照失败:`cmd/serviceport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore` 与 `TestSnapshotRejectsIncompleteAndSidecars`。其余包/用例通过,不记为全量通过。
- `go test -p=1 ./... -count=1 -timeout=120s` 已执行。仍只有 6 项既有 Windows ACL/SQLite 快照失败:`cmd/linkport/TestRestoreCommandRejectsRunningService`、`internal/api/TestDatabaseSnapshotSessionAndPreview`、`internal/desktop/TestLocalSingleFileAndLock`、`internal/security/TestPrivateFileNeverOverwrites`、`internal/store/TestDatabaseSnapshotAndOfflineRestore` 与 `TestSnapshotRejectsIncompleteAndSidecars`。其余包/用例通过,不记为全量通过。
- `go vet -p=1 ./...` 通过。Go 的 TEMP/TMP/GOTMPDIR 与缓存位于工作区隔离目录;telemetry 写用户目录被拒绝的提示不影响聚焦测试和 vet 的退出码。
- `node web/tests/resource-catalog.test.mjs` 10/10,通过真实 Vue refs 和可控 Promise 覆盖整体发布、交错刷新、旧失败、新失败、保存优先、非资源写入不影响缓存、退出失效和页面协调。`workspace-pages.test.mjs` 1/1、`page-dialogs.test.mjs` 2/2 通过。`node --test` 多文件执行受沙箱子进程 EPERM 阻止,实际通过记录来自逐文件单进程运行。
- Vue 类型检查通过;`npm.cmd --prefix web run build` 在 Vite 启动 esbuild 时仍报 `spawn EPERM`。`web/dist/.gitkeep` 保留,没有生成包含本次改动的新 executable,因此未执行要求 `SERVICEPORT_E2E_BINARY` 的浏览器回归。Playwright `--list` 成功发现 `workspace.spec.ts`、`running-edit.spec.ts`、`global-assets-proxy-project.spec.ts`、`csrf.spec.ts` 共 11 项,仅表示发现成功。
- Vue 类型检查通过;`npm.cmd --prefix web run build` 在 Vite 启动 esbuild 时仍报 `spawn EPERM`。`web/dist/.gitkeep` 保留,没有生成包含本次改动的新 executable,因此未执行要求 `LINKPORT_E2E_BINARY` 的浏览器回归。Playwright `--list` 成功发现 `workspace.spec.ts`、`running-edit.spec.ts`、`global-assets-proxy-project.spec.ts`、`csrf.spec.ts` 共 11 项,仅表示发现成功。
- `git diff --check` 通过。测试使用隔离数据库、本机协议 fixture 和注入 API,没有连接真实目标,也未部署、操作真实 data 或执行迁移。
设计与扩展入口见 [核心设计模式](architecture-patterns.md)。
## 2026-09-27 sing-box 连接资产
原因与范围:验证新增的单 outbound JSON、安全过滤、外部进程适配、SOCKS5 本地桥接、秘密脱敏和三语界面逻辑。serviceport 不捆绑 sing-box;当前测试机执行 `Get-Command sing-box` 无结果,因此本轮没有运行真实 `sing-box check/run`,也没有连接真实代理或目标服务器。
原因与范围:验证新增的单 outbound JSON、完整配置/JSON 文件自动提取、安全过滤、外部进程适配、SOCKS5 本地桥接、秘密脱敏和三语界面逻辑。linkport 不捆绑 sing-box;当前测试机执行 `Get-Command sing-box` 无结果,因此本轮没有运行真实 `sing-box check/run`,也没有连接真实代理或目标服务器。
实际通过:
- `GOTOOLCHAIN=go1.25.0 go test ./internal/model ./internal/outbound ./internal/store ./internal/api -run SingBox -count=1 -timeout=120s`。四个包均通过。受管沙箱把 `GOCACHE`、`GOTMPDIR`、`TEMP`、`APPDATA` 指向工作区后测试退出码为 0;Go telemetry 仍尝试访问用户目录并输出权限警告,但不影响测试结果。首次未隔离系统临时目录时的拒绝访问不是代码失败。
- `GOTOOLCHAIN=go1.25.0 go vet -p=1 ./...` 通过。`go test -p=1 ./... -count=1 -timeout=120s` 使用本机已有模块缓存离线执行,所有包完成编译;`cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项在 Windows 文件权限或便携 SQLite 快照操作处返回 `Access is denied`,其余包通过。上述 6 项不位于 sing-box 实现;sing-box 的 model/outbound 全包及 store/api 专项均通过。
- `node web/tests/sing-box.test.mjs`:10 项通过,覆盖协议非硬编码 allowlist、URI/完整配置/本地能力拒绝、64 KiB 与复杂度限制、安全摘要、三语文本、闭合错误映射、编辑时不回填秘密和“检查配置”文案边界。`node --test` 在当前沙箱因 runner 创建子进程时报 `spawn EPERM`,使用同一测试文件的单进程入口通过。
- `GOTOOLCHAIN=go1.25.0 go test -p=1 ./internal/model ./internal/outbound ./internal/store ./internal/api -run 'SingBox|GatewayDraftConnection|Redis.*NoAccount|NoAccount' -count=1 -timeout=120s`。四个包均通过。受管沙箱把 `GOCACHE`、`GOTMPDIR`、`TEMP`、`GOMODCACHE` 指向工作区后测试退出码为 0;Go telemetry 仍尝试访问用户目录并输出权限警告,但不影响测试结果。首次未隔离系统临时目录时的拒绝访问不是代码失败。
- `GOTOOLCHAIN=go1.25.0 go vet -p=1 ./...` 通过。`go test -p=1 ./... -count=1 -timeout=120s` 使用本机已有模块缓存离线执行,所有包完成编译;`cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项在 Windows 文件权限或便携 SQLite 快照操作处返回 `Access is denied`,其余包通过。上述 6 项不位于 sing-box 实现;sing-box 的 model/outbound 全包及 store/api 专项均通过。
- `node web/tests/sing-box.test.mjs`:10 项通过,覆盖协议非硬编码 allowlist、单 outbound/完整配置提取、无远程 outbound 和 URI/本地能力拒绝、64 KiB 与复杂度限制、安全摘要、三语文本、闭合错误映射、编辑时不回填秘密和“检查配置”文案边界。文件导入使用同一 JSON 解析/校验路径;未运行真实 sing-box binary。`node --test` 在当前沙箱因 runner 创建子进程时报 `spawn EPERM`,使用同一测试文件的单进程入口通过。
- `node web/tests/connection-stages.test.mjs` 6 项通过;逐个直接执行 `web/tests/*.test.mjs` 的 10 个单进程逻辑文件时 9 个文件通过,`persistent-target.test.mjs` 的既有重试指引断言 1 项失败,该文件不经过 sing-box 路径。`vue-tsc --noEmit -p web/tsconfig.json` 与 `git diff --check` 通过。
- GitHub Releases API 于 2026-09-27 返回最新非预发布版本 `v1.14.2`(发布时间 2026-09-24);对应官方 `go.mod` 为 `go 1.25.5`,官方 `LICENSE` 为 GPL v3 或更高版本正文并附名称/关联限制。这里只用于文档兼容判断,没有下载或执行该发布 binary。
前端生产构建执行到 Vue 类型检查后,Vite 启动 esbuild 子进程被当前沙箱以 `spawn EPERM` 拒绝。没有生成包含本次界面的新 `web/dist` 或新 executable,因此未运行要求 `SERVICEPORT_E2E_BINARY` 的 Playwright,也没有用旧程序代替验收。Windows 当前 `CGO_ENABLED=0` 且无 GCC/Clang,未执行 race detector。
前端生产构建执行到 Vue 类型检查后,Vite 启动 esbuild 子进程被当前沙箱以 `spawn EPERM` 拒绝。没有生成包含本次界面的新 `web/dist` 或新 executable,因此未运行要求 `LINKPORT_E2E_BINARY` 的 Playwright,也没有用旧程序代替验收。Windows 当前 `CGO_ENABLED=0` 且无 GCC/Clang,未执行 race detector。
fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入站、强制 route final、目标域名透传、进程共享/退出重启、API 不回显、配置包和历史差异脱敏。伪造 binary/本地协议 fixture 只能证明 serviceport 自身编排,不证明某个 sing-box 协议、远端代理、DNS、TLS 或目标账号可用。资产“检查配置”只应解释为 installed binary 接受配置;其失败响应只有固定安全错误类别和耗时,不携带 `sing_box_start` 阶段。目标连接测试才覆盖 `run`、四个 sing-box 运行时阶段和目标协议链路。完整边界见 [sing-box 连接资产](sing-box.md)。
fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入站、强制 route final、目标域名透传、进程共享/退出重启、API 不回显、配置包和历史差异脱敏。伪造 binary/本地协议 fixture 只能证明 linkport 自身编排,不证明某个 sing-box 协议、远端代理、DNS、TLS 或目标账号可用。资产“检查配置”只应解释为 installed binary 接受配置;其失败响应只有固定安全错误类别和耗时,不携带 `sing_box_start` 阶段。目标连接测试才覆盖 `run`、四个 sing-box 运行时阶段和目标协议链路。完整边界见 [sing-box 连接资产](sing-box.md)。
## 2026-09-27 Kubernetes 端口候选与权限预检
- Go 1.25.0 聚焦测试通过:`go test -p=1 ./internal/model ./internal/outbound ./internal/api -run 'TestUploadedKubeconfig|TestKubernetesPermissionPreflight|TestKubernetesPreflightEndpoint|TestKubernetesDiscovery|TestKubeconfigAssetLifecycleAndReferences|TestAssetMetadataAndKubernetesReadPermissions' -count=1 -timeout=120s`;改动包执行 `go vet -p=1 ./internal/model ./internal/outbound ./internal/api` 通过,完整仓库执行的 `go vet -p=1 ./...` 也通过。测试使用工作区 `TEMP/TMP/GOTMPDIR/GOCACHE`,Go telemetry 写入用户目录被拒绝的警告不影响测试结果。
- `go test -p=1 ./... -count=1 -timeout=120s` 未全绿:`cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项因既有 Windows ACL/便携 SQLite 快照环境失败;其余包通过,`internal/outbound` 全包通过。
- `go test -p=1 ./... -count=1 -timeout=120s` 未全绿:`cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项因既有 Windows ACL/便携 SQLite 快照环境失败;其余包通过,`internal/outbound` 全包通过。
- 后端 fixture 使用本机 TLS HTTP Server 模拟 Kubernetes `SelfSubjectAccessReview`,验证允许、拒绝、未知、评估错误、API 禁止审查、Pod 名称限定、资产类型/端口校验、认证保护及不回显 Token/上游原因。模型、发现、预检、资产 API 和正式启动入口回归另覆盖 `certificate-authority`、`client-certificate`、`client-key`、`tokenFile`、`exec` 与 `auth-provider` 在客户端构造或网络请求前被拒绝,并检查资产 API 的稳定错误码。没有连接真实集群,不证明现场 RBAC 或 SPDY port-forward 成功。
- `vue-tsc --noEmit -p web/tsconfig.json` 和单进程 `node web/tests/tcp-kube-forward.test.mjs` 7/7 通过;Playwright `--list` 发现 `tcp-kube-forward.spec.ts` 2 项,其中新断言覆盖自动端口建议、候选切换、预检查询参数与逐项结果。
- `npm.cmd --prefix web run build` 完成 Vue 类型阶段后,Vite 默认配置加载和 `--configLoader native` 都因当前沙箱禁止 Node 创建子进程而报 `spawn EPERM`;`web/dist/.gitkeep` 保留。未生成包含本次界面的新程序,因此未执行需要 `SERVICEPORT_E2E_BINARY` 的 Playwright,不把用例发现或旧程序结果记为浏览器通过。
- `npm.cmd --prefix web run build` 完成 Vue 类型阶段后,Vite 默认配置加载和 `--configLoader native` 都因当前沙箱禁止 Node 创建子进程而报 `spawn EPERM`;`web/dist/.gitkeep` 保留。未生成包含本次界面的新程序,因此未执行需要 `LINKPORT_E2E_BINARY` 的 Playwright,不把用例发现或旧程序结果记为浏览器通过。
## 2026-09-26 运行中编辑目标与网关配置
- 行为覆盖:运行中的目标与网关编辑入口保持可用;目标更新为所有引用网关批量发布新快照,监听变更先预绑定再提交;旧连接继续使用旧快照,新连接使用新快照。启动/停止状态与配置在同一生命周期锁内持久化,保存失败保持原运行状态。CAS 冲突返回 409;SSH/SFTP 候选主机先匿名探测并二次核对指纹。
- Go 1.25.0 定向回归通过:`go test -p=1 ./internal/gateway ./internal/api -run 'Test(UpdateConfig|StartFrom|StopAndPersist|StoppedUpdate|GatewayLifecyclePersistence|RunningConfiguration|LiveConfigCAS|RunningSSHTarget|RunningSFTPTarget)' -count=1 -timeout=120s`。随后 gateway 全包和跳过既有 portable snapshot 环境项的 API 全包通过;新增 API 生命周期测试确认 enabled 保存失败释放候选端口、disabled 保存失败保持监听。
- `go vet ./...` 在 Go 1.25.0 下通过。规定的 `go test ./... -count=1 -timeout=120s` 未全绿:`cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项因既有 Windows ACL/便携 SQLite 快照环境失败;未修改的 PostgreSQL prepared-session identity fixture 另有间歇失败,单次复跑通过,20 轮复跑仍可间歇复现。`internal/gateway` 全包通过,`internal/api` 除上述既有快照项外通过。
- `go vet ./...` 在 Go 1.25.0 下通过。规定的 `go test ./... -count=1 -timeout=120s` 未全绿:`cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项及 `internal/store` 2 项因既有 Windows ACL/便携 SQLite 快照环境失败;未修改的 PostgreSQL prepared-session identity fixture 另有间歇失败,单次复跑通过,20 轮复跑仍可间歇复现。`internal/gateway` 全包通过,`internal/api` 除上述既有快照项外通过。
- `web/node_modules/.bin/vue-tsc.cmd --noEmit -p web/tsconfig.json` 通过。Playwright `--list` 发现 3 个相关文件共 19 项,包括运行中目标/网关编辑、SSH 候选确认取消后重试,以及关闭结果页后“连接成功”和“实测连接阶段”均消失。
- `npm.cmd --prefix web run build` 的类型检查完成,Vite 加载配置时因当前 Windows 沙箱禁止 Node 启动 esbuild 而报 `spawn EPERM`;`web/dist/.gitkeep` 保持存在。未生成包含本次改动的新前端或可执行程序,故没有运行要求 `SERVICEPORT_E2E_BINARY` 的 Playwright,也不把用例发现或旧程序结果记作通过。
- 所有后端新增测试使用隔离 SQLite、本机 TCP 或 SSH fixture;`running-edit.spec.ts` 只启动隔离本地 listener,目标为不会实际连接的占位地址,不是真实 MySQL 服务。本轮未读取或修改真实 `data/serviceport.sqlite`、审计、TLS 或主密钥文件。
- `npm.cmd --prefix web run build` 的类型检查完成,Vite 加载配置时因当前 Windows 沙箱禁止 Node 启动 esbuild 而报 `spawn EPERM`;`web/dist/.gitkeep` 保持存在。未生成包含本次改动的新前端或可执行程序,故没有运行要求 `LINKPORT_E2E_BINARY` 的 Playwright,也不把用例发现或旧程序结果记作通过。
- 所有后端新增测试使用隔离 SQLite、本机 TCP 或 SSH fixture;`running-edit.spec.ts` 只启动隔离本地 listener,目标为不会实际连接的占位地址,不是真实 MySQL 服务。本轮未读取或修改真实 `data/linkport.sqlite`、审计、TLS 或主密钥文件。
## 2026-09-26 连接测试结果关闭清理
- 目标草稿连接测试关闭结果页后会清空全局详情、编辑器内联成功/失败摘要及“实测连接阶段”,但保留草稿、测试账号选择和未保存状态。`workspace-features.spec.ts` 已改为回归结果页和两类残留状态均消失。
- `web/node_modules/.bin/vue-tsc.cmd --noEmit -p web/tsconfig.json` 通过。把 `TEMP`/`TMP` 指向工作区隔离目录后,Playwright `--list` 成功发现 `workspace-features.spec.ts` 的 3 项用例,包括本次回归。
- `npm.cmd --prefix web run build` 的 Vue 类型检查通过,Vite 启动 esbuild 时因当前 Windows 沙箱禁止 Node 创建子进程而报 `spawn EPERM`;直接执行同一 esbuild 程序成功、由 Node `spawnSync` 执行则稳定返回 `EPERM`。因此本轮没有新的前端产物或嵌入式程序,未运行要求新程序的 Playwright,也不把测试发现记为浏览器通过。`web/dist/.gitkeep` 仍保留。
- Go 1.25.0 使用工作区隔离的 `TEMP`、`GOTMPDIR` 和 `GOCACHE`。`go test ./... -count=1 -timeout=120s` 中除 6 项既有 Windows ACL/SQLite 快照环境失败外其余包通过:`cmd/serviceport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项,`internal/store` 2 项;整套不记为通过。`go vet ./...` 首次因并发 vet 子进程 `exit status 9` 中止,串行 `go vet -p=1 ./...` 通过。
- Go 1.25.0 使用工作区隔离的 `TEMP`、`GOTMPDIR` 和 `GOCACHE`。`go test ./... -count=1 -timeout=120s` 中除 6 项既有 Windows ACL/SQLite 快照环境失败外其余包通过:`cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 各 1 项,`internal/store` 2 项;整套不记为通过。`go vet ./...` 首次因并发 vet 子进程 `exit status 9` 中止,串行 `go vet -p=1 ./...` 通过。
- 浏览器回归中的远端连接响应为路由 fixture,但本轮未实际执行该用例;完整 Go 测试中的协议包使用仓库本地 fixture,不是真实服务器验收。未停止本地服务,未读取或修改真实 `data`。
## 2026-09-26 网关 TCP Kubeconfig 独立端口转发
### 主代理最终复验
- `GOTOOLCHAIN=go1.25.0 go test ./... -count=1 -timeout=120s` 已实际执行;本功能涉及的 `model`、`gateway`、`outbound`、`tcpproxy` 及新增 API/存储用例通过。全量仍有 6 个既有 Windows ACL/SQLite 快照环境失败:`cmd/serviceport` 1 个、`internal/api` 1 个、`internal/desktop` 1 个、`internal/security` 1 个、`internal/store` 2 个,因此不记为全量通过。
- `GOTOOLCHAIN=go1.25.0 go test ./... -count=1 -timeout=120s` 已实际执行;本功能涉及的 `model`、`gateway`、`outbound`、`tcpproxy` 及新增 API/存储用例通过。全量仍有 6 个既有 Windows ACL/SQLite 快照环境失败:`cmd/linkport` 1 个、`internal/api` 1 个、`internal/desktop` 1 个、`internal/security` 1 个、`internal/store` 2 个,因此不记为全量通过。
- `go vet ./...` 首次因环境并行启动 `vet.exe` 被拒绝;使用同一 Go 1.25.0 工具链执行 `go vet -p=1 ./...` 通过。原有 SOCKS5 边界回归 `go test -p=1 ./internal/api -run '^TestTCPIndependentBoundary' -count=1 -timeout=120s -v` 的 5 项测试及全部子测试通过。
- 前端最终复跑:`node tests/tcp-kube-forward.test.mjs` 6/6、`node tests/account-mapping.test.mjs` 5/5,生产构建命令的 Vue 类型检查阶段通过;Vite 启动 esbuild 时被当前 Windows 环境以 `spawn EPERM` 拒绝,未生成新前端产物。
- 工作区 `TEMP`/`TMP` 下的 Playwright `--list` 成功发现 `tcp-kube-forward.spec.ts` 与 `tcp-independent.spec.ts` 共 3 项。由于无法构建包含本次界面的新可执行程序,未执行这些浏览器用例,也未用旧程序替代验收。`web/dist/.gitkeep` 保持完好。
@@ -109,24 +109,26 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- 第一次列举 Playwright 用例时,系统临时目录被拒绝并显示 0 项;把 `TEMP`/`TMP` 指向工作区后,`npm.cmd test -- --list tests/tcp-kube-forward.spec.ts` 成功发现 2 项,分别覆盖 Kubeconfig 新建/回读和旧无 `mode` SOCKS5 回读、原样保存后切换 K8s。这里只完成用例发现,尚未使用本轮新构建程序执行浏览器用例。
- 首次 Go 专项尚未进入测试:系统临时目录创建被环境拒绝;改设 `GOTMPDIR`/`GOCACHE` 后并行首次编译又因资源限制出现 `compile.exe: exit status 9`。最终把 `TEMP`、`TMP`、`GOTMPDIR` 和 `GOCACHE` 指向工作区隔离目录,并设置 `GOMAXPROCS=2`、`-p=1` 后,上述模型和存储专项分别通过。这里记录的是环境失败及成功复跑,不把未执行测试的命令算作业务失败。
- 存储拨号验证使用本机 `httptest` TLS Kubernetes API fixture,确认运行时读取引用资产中的 Bearer Token,并请求所选命名空间和 Deployment;fixture 主动返回 403,因此没有建立 SPDY 数据流。仓库既有 Kubernetes 传输测试使用模拟 API 与 SPDY fixture 验证 port-forward;这些均不是实际 Kubernetes 集群验收,不覆盖现场证书、RBAC、网络策略、代理或具体工作负载。
- 所有新增存储和 API 测试使用隔离 SQLite、临时端口与测试资产;未读取、修改、迁移或部署真实 `data/serviceport.sqlite`、审计、TLS 或主密钥。最终全量 Go、前端生产构建和 Playwright 结果在完成后继续补充到本节。
- 所有新增存储和 API 测试使用隔离 SQLite、临时端口与测试资产;未读取、修改、迁移或部署真实 `data/linkport.sqlite`、审计、TLS 或主密钥。最终全量 Go、前端生产构建和 Playwright 结果在完成后继续补充到本节。
## 2026-09-26 Redis 上游零账号
## 2026-09-26 Redis 上游零账号(2026-09-28 扩展)
- Redis 目标可保存 `accounts: []`,草稿/已保存测试、集群识别、手动与周期健康检查、网关默认或账号级绑定、启动和切换均接受空 `targetAccountId`。网关客户端账号和密码仍必填;Redis 添加任意目标账号后继续强制选择有效账号。
- Go 1.25.0 下 model、cluster、gateway、redisproxy 全包及 Redis API 定向回归通过,`go vet -p=1 ./...` 通过。最终 `go test -p=1 ./... -count=1 -timeout=120s` 中功能相关包均通过,但全量仍有 6 项既有 Windows 权限/SQLite 快照失败:`cmd/serviceport` 的运行服务恢复保护、`internal/api` 的 portable SQLite snapshot、`internal/desktop` 的单文件/锁、`internal/security` 的私有文件覆盖保护,以及 `internal/store` 的两个 snapshot 用例。首次未限制并发的全量运行另遇编译子进程 Access denied,最终以串行结果为准,不宣称 Go 全量通过。
- `node web/tests/redis-accountless.test.mjs` 以单进程执行,4 项通过;前端子任务的 `vue-tsc --noEmit`、持续连接 6 项和账号映射 5 项通过。Playwright `--list` 在工作区 TEMP 下成功发现 `target-auth` 与 `target-editor-test` 两个文件的 22 项用例。Vite/esbuild 被当前 Windows 沙箱的 `spawn EPERM` 阻止,无法生成新的 `dist` 和嵌入式可执行程序,因此按必须使用新构建程序的规则未运行 E2E,不宣称前端生产构建或 E2E 通过。
- Redis 和 MongoDB 目标及网关均可保存空账号列表;草稿/已保存测试、集群识别、手动与周期健康检查、启动、切换和画板关联把空字符串账号 ID 作为明确的无认证端点。TCP 使用相同空 ID 表示空账号。任一服务配置真实账号后继续强制选择有效账号,不把空端点与缺失映射混淆。
- Go 1.25.0 下 model、cluster、gateway、redisproxy 全包及 Redis API 定向回归通过,`go vet -p=1 ./...` 通过。最终 `go test -p=1 ./... -count=1 -timeout=120s` 中功能相关包均通过,但全量仍有 6 项既有 Windows 权限/SQLite 快照失败:`cmd/linkport` 的运行服务恢复保护、`internal/api` 的 portable SQLite snapshot、`internal/desktop` 的单文件/锁、`internal/security` 的私有文件覆盖保护,以及 `internal/store` 的两个 snapshot 用例。首次未限制并发的全量运行另遇编译子进程 Access denied,最终以串行结果为准,不宣称 Go 全量通过。
- 全部 16 个 `web/tests/*.test.mjs` 单进程领域测试文件通过,其中 Redis/MongoDB/TCP 空账号 13 项、持续连接组件 6 项、连接草稿 8 项、连接画板 8 项;`vue-tsc --noEmit -p web/tsconfig.json` 与串行 `go vet -p=1 ./...` 通过。草稿和编辑器夹具逐一覆盖全部 11 种协议,并验证旧目标不继承新建默认值。Playwright `--list` 在工作区 TEMP 下成功发现 60 个文件中的 138 项用例。Vite/esbuild 被当前 Windows 沙箱的 `spawn EPERM` 阻止,无法生成新的 `dist` 和嵌入式可执行程序,因此按必须使用新构建程序的规则未运行 E2E,不宣称前端生产构建或 E2E 通过。
- 普通 Redis、Cluster、Sentinel 和 `HELLO AUTH` 覆盖使用本机 RESP/net.Pipe fixture,确认零账号路径不发送上游 `AUTH`;没有连接真实 Redis/Sentinel,也没有停止本地服务、部署迁移或修改真实 `data`。
## 2026-09-22 目标后台持续连接
本节对应 `connectionOptions.persistent` 与网关持续会话/SSH、K8s 隧道生命周期,使用入口、兼容边界与原因见 `persistent-targets.md` 和 `change-rationale.md`。默认关闭,无数据迁移;仅在引用网关运行时维持后台资源。
本节记录 2026-09-22 引入 `connectionOptions.persistent` 时的验证;当时默认关闭。2026-09-28 起,管理界面的 11 种新建目标草稿默认开启,旧配置不自动改写;仍仅在引用网关运行时维持后台资源。当前使用入口、兼容边界与原因见 `persistent-targets.md` 和 `change-rationale.md`。
2026-09-28 默认值调整的实际验证为连接草稿 8/8、持续连接组件 6/6、Vue 类型检查、串行 `go vet -p=1 ./...` 和 `git diff --check` 通过;Playwright 已发现逐一切换 11 种类型的用例,但因 Vite/esbuild `spawn EPERM` 未运行浏览器。以下结果保留为 2026-09-22 首次实现时的历史验收记录。
- Go 1.25.0:`go test ./... -count=1 -timeout=120s` 全量通过,日志 `artifacts/persistent-final-go-test.log`;`go vet ./...` 通过。
- Node 22.22.3:`npm.cmd --prefix web run build` 通过,仅有 Vite chunk 大小警告。构建完成后恢复 `web/dist/.gitkeep`,再编译嵌入前端的最终程序 `artifacts/serviceport-persistent-final.exe`。
- Node 22.22.3:`npm.cmd --prefix web run build` 通过,仅有 Vite chunk 大小警告。构建完成后恢复 `web/dist/.gitkeep`,再编译嵌入前端的最终程序 `artifacts/linkport-persistent-final.exe`。
- 最终程序 SHA256:`D52C30CCF34A4FB785B0F0433299B12139342E97CDBC315729A047E83A9B69AC`。
- `node --test web/tests/persistent-target.test.mjs`:**6/6 通过**。针对持续连接和工作区列设置的 Playwright:**6/6 通过(16.1 秒)**,日志 `artifacts/persistent-verified-focused.log`。
- 全量 Playwright 通过 `SERVICEPORT_E2E_BINARY` 指向上述最终程序,使用隔离 SQLite 和端口 19481/19482:**124/124 通过(6.6 分钟)**。日志 `artifacts/persistent-full.log`,辅助日志 `artifacts/persistent-full.err.log`,产物目录 `artifacts/persistent-full-results`。完成后两个测试监听端口均已释放。
- 全量 Playwright 通过 `LINKPORT_E2E_BINARY` 指向上述最终程序,使用隔离 SQLite 和端口 19481/19482:**124/124 通过(6.6 分钟)**。日志 `artifacts/persistent-full.log`,辅助日志 `artifacts/persistent-full.err.log`,产物目录 `artifacts/persistent-full-results`。完成后两个测试监听端口均已释放。
- 初步定向浏览器运行暴露不完整 runtime mock 及表格 expand 结构列被过滤的问题;已改用真实 API 基础配置构造 mock,并修复表格保留结构列。以上成功结果来自修复后的最终程序。状态展示测试模拟 runtime,不等同于真实远端可用性验收。
- 四种数据库协议和 SSH/K8s 使用本地受控 fixture;Redis 网关集成验证无客户端预认证、首次借用、补充新备用会话及停止释放,重复 5 次通过;隧道生命周期相关用例重复 20 次通过。未连接用户真实 MongoDB/K8s,不修改真实目标数据。
- Linux/amd64、Darwin/amd64 的 gateway/outbound 测试程序交叉编译通过,未在对应系统执行。当前 CGO 禁用,未运行 race detector,不宣称 race 检查通过。
@@ -140,12 +142,12 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- 使用 Go 1.25.0 执行 `go vet ./...`:通过。
- 新增 `internal/api/tcp_independent_validation_test.go`,执行 `go test ./internal/api -run '^TestTCPIndependentBoundary' -count=1 -timeout=120s -v`:**5 项测试、21 项子测试通过**。覆盖无效代理/字段、账号冲突、部分更新、模式切换、引用重绑定、代理改类型及 impact 影响范围响应脱敏。
- 首次执行 `scripts/build.ps1 -SkipTests`:成功完成前端 npm build 和 Go 1.25.0 编译;当时 Node 22.10 触发 Vite 版本要求警告,不将成功构建等同于该版本符合 Vite 支持要求。
- 补充图视图调整后,使用机器已有 Node 22.22.3 再次执行 `scripts/build.ps1 -SkipTests`:成功完成 npm build 和 Go 1.25.0 编译,生成同一路径的最终程序 `artifacts/tcp-independent/serviceport.exe`。此次没有 Node 版本警告,仅保留 chunk 大小警告。`-SkipTests` 不表示构建脚本执行了测试,Go 测试结果来自独立执行。
- 最终重建前,Playwright 通过 `SERVICEPORT_E2E_BINARY` 指定当时的新构建程序,`tcp-independent.spec.ts` 与 `tcp-forwarding.spec.ts` 共 **2 项通过**;最终程序的全量回归结果另列如下。
- 补充图视图调整后,使用机器已有 Node 22.22.3 再次执行 `scripts/build.ps1 -SkipTests`:成功完成 npm build 和 Go 1.25.0 编译,生成同一路径的最终程序 `artifacts/tcp-independent/linkport.exe`。此次没有 Node 版本警告,仅保留 chunk 大小警告。`-SkipTests` 不表示构建脚本执行了测试,Go 测试结果来自独立执行。
- 最终重建前,Playwright 通过 `LINKPORT_E2E_BINARY` 指定当时的新构建程序,`tcp-independent.spec.ts` 与 `tcp-forwarding.spec.ts` 共 **2 项通过**;最终程序的全量回归结果另列如下。
- 新增 API 测试 `TestIndependentTCPForwardTwoRoutes` 使用本地带密码 SOCKS5 fixture,把两条独立网关映射转发到本地 TCP fixtures,并校验二进制字节。该结果证明受控环境中的 SOCKS5 认证与两条 TCP 数据转发链路,不是对真实用户 `192.168.11.16:8080/8082`、生产代理、业务认证或 TLS 的验收。
- 全量 Playwright 首次使用默认 Node 22.10,因 `node:sqlite` 不可用而在测试发现阶段失败。随后执行 `fnm use 22.22.3`,仅对当前 shell 切换至机器已有版本,成功发现并启动 121 项测试;未安装新版本或修改持久系统环境。
- 上述一次全量运行执行到 59 项时,被执行工具的 **200 秒时限中断**,未获得完整结果;这是执行时限中断,不是测试用例失败结论。随后已延长时限完成最终重跑。
- **最终完整 Playwright:121/121 通过(6.6 分钟)。** 使用 Node 22.22.3,设置 `SERVICEPORT_E2E_BINARY=artifacts/tcp-independent/serviceport.exe`,在隔离端口 `19281/19282` 运行;日志 `artifacts/tcp-independent-playwright-final.log`。该程序包含最终图视图修复,本条为本轮最终全量结果,不借用其他批次的成功记录。
- **最终完整 Playwright:121/121 通过(6.6 分钟)。** 使用 Node 22.22.3,设置 `LINKPORT_E2E_BINARY=artifacts/tcp-independent/linkport.exe`,在隔离端口 `19281/19282` 运行;日志 `artifacts/tcp-independent-playwright-final.log`。该程序包含最终图视图修复,本条为本轮最终全量结果,不借用其他批次的成功记录。
本次图视图还调整为纯端口转发不显示“未关联目标”提示、禁止拖到目标绑定并保留手动测试按钮;这些调整已纳入最终构建,并随上述 121 项完整浏览器回归通过。
@@ -157,8 +159,8 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- MySQL 集群发现修复后,主节点发现与结果字符串生命周期测试重复 1,000 次、cluster 包重复 20 次通过;不增加重试或放宽断言,详见 `internal/cluster/README.md`。
- `node --test web/tests/account-mapping.test.mjs web/tests/connection-stages.test.mjs`:8/8 通过,覆盖账号选择、协议匹配、authSource、闭集诊断清理与中英日阶段说明。Node 22.10 的直接 `.ts` 导入失败已通过与既有测试一致的内存转译加载修复。
- 前端 `npm.cmd --prefix web run build` 通过,最终使用已有 Node 22.22.3,仅保留 Vite 大 chunk 警告;恢复 `web/dist/.gitkeep` 后构建内嵌资源的 Windows executable。曾因默认 PATH 的 Node 22.10 缺少无标志 `node:sqlite` 导致全量用例发现失败,切换已有兼容运行时后重跑,未修改系统 PATH 或安装工具。
- 首轮 Playwright 为 113 通过、6 失败;修复窄屏布局、草稿类别取消、旧 K8s 出口残留,以及过时的确认流程/Element Plus 定位断言。随后在隔离端口 19181/19182 使用 `SERVICEPORT_E2E_BINARY=artifacts/serviceport-optimizations-final.exe` 完整回归:**120/120 通过(6.6 分钟)**,日志 `artifacts/optimizations-final-playwright.log`。该二进制包含界面与诊断修复,但早于最后的 MySQL 结果生命周期修复,因此另行构建最终 verified 程序复验。
- 最终程序 `artifacts/serviceport-optimizations-verified.exe` 包含 MySQL 修复,在隔离端口 19281/19282 再跑完整 Playwright:**120/120 通过(6.4 分钟)**,日志 `artifacts/optimizations-verified-playwright.log`,截图目录 `artifacts/optimizations-verified-results`。程序 SHA256 为 `a51cc6a5e05070c663da0e3f385a02b0c6646a2a7c44d0f7ae2d7f91307e6012`。最终 `git diff --check` 与变更 Go 文件的 gofmt 检查通过。
- 首轮 Playwright 为 113 通过、6 失败;修复窄屏布局、草稿类别取消、旧 K8s 出口残留,以及过时的确认流程/Element Plus 定位断言。随后在隔离端口 19181/19182 使用 `LINKPORT_E2E_BINARY=artifacts/linkport-optimizations-final.exe` 完整回归:**120/120 通过(6.6 分钟)**,日志 `artifacts/optimizations-final-playwright.log`。该二进制包含界面与诊断修复,但早于最后的 MySQL 结果生命周期修复,因此另行构建最终 verified 程序复验。
- 最终程序 `artifacts/linkport-optimizations-verified.exe` 包含 MySQL 修复,在隔离端口 19281/19282 再跑完整 Playwright:**120/120 通过(6.4 分钟)**,日志 `artifacts/optimizations-verified-playwright.log`,截图目录 `artifacts/optimizations-verified-results`。程序 SHA256 为 `a51cc6a5e05070c663da0e3f385a02b0c6646a2a7c44d0f7ae2d7f91307e6012`。最终 `git diff --check` 与变更 Go 文件的 gofmt 检查通过。
- `internal/security` 和 `internal/gateway` 在 Linux/amd64、Darwin/amd64 的 `go test -c` 交叉编译均通过;没有在这些系统运行测试,不能等同于平台实机验收。
- 协议验证使用隔离 SQLite、本机 TCP/SSH/Kubernetes/数据库 fixture;完整备份测试覆盖已提交 WAL、库内密钥、管理员、目标、审计、自动 TLS、摘要确认、保底快照、停服锁和拒绝 API token。浏览器覆盖完整库下载、检查与风险确认后显示离线恢复指令;没有恢复真实 data 或修改真实目标数据。阶段 UI 与 SSH 探测 UI 的部分响应为模拟,不声称真实远程服务认证成功。
- 未部署、未提交或推送;真实 `data`、审计及 TLS/旧 DPAPI 材料保留。恢复为显式停服命令,不提供运行中覆盖数据库。服务端含密钥的暂存文件写入前应用 Windows 当前用户/SYSTEM DACL 或 Unix 0600;浏览器下载目录权限仍由用户管理。
@@ -175,8 +177,8 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
## 2026-09-22 WebDAV 同步备份替代 S3
- 使用 Go 1.25.0 运行 `go test ./... -count=1 -timeout=120s`:本次变更涉及的 backup、syncjob、store、api、model、MongoDB/HTTP 协议包均通过;全量仅 internal/cluster 的 TestMySQLReplicationAndGroupDiscovery/mysql-group-replication 失败,报 `cluster has no available primary or router`,与本文件此前记录的既有间歇失败一致。本轮未修改该逻辑,不将全量 Go 标记通过。日志 artifacts/webdav-sync/go-test.log。
- `go vet ./...`、`npm.cmd --prefix web run build` 通过,前端只有现有大 chunk 警告。Vite 完成后恢复 web/dist/.gitkeep,再构建 artifacts/webdav-sync/serviceport.exe,未覆盖正式程序。
- 设置 SERVICEPORT_E2E_BINARY 指向新程序,使用隔离 SQLite 与 19491/19492 端口:全量 Playwright **113 通过、4 失败(6.4 分钟)**。四项仍为 global-assets-proxy-project:98、kubeconfig-selection:23、target-outbound-selection:3、workspace:98,分别涉及资产草稿、目标字段显示及连接错误文本;与之前合并及空密码版本相同,不宣称全量浏览器通过。日志 artifacts/webdav-sync/full-web.log;最终退出状态见 results.json。
- `go vet ./...`、`npm.cmd --prefix web run build` 通过,前端只有现有大 chunk 警告。Vite 完成后恢复 web/dist/.gitkeep,再构建 artifacts/webdav-sync/linkport.exe,未覆盖正式程序。
- 设置 LINKPORT_E2E_BINARY 指向新程序,使用隔离 SQLite 与 19491/19492 端口:全量 Playwright **113 通过、4 失败(6.4 分钟)**。四项仍为 global-assets-proxy-project:98、kubeconfig-selection:23、target-outbound-selection:3、workspace:98,分别涉及资产草稿、目标字段显示及连接错误文本;与之前合并及空密码版本相同,不宣称全量浏览器通过。日志 artifacts/webdav-sync/full-web.log;最终退出状态见 results.json。
- 新 WebDAV 两项、目标认证七项均在完整浏览器运行中通过;此前 WebDAV/menu-search/target-auth 专项组合 11 项通过(26.3 秒,focused.log)。覆盖响应式布局、保存和秘密清空、冲突确认、独立备份以及实际隔离 API 到本机 WebDAV 上传的链路。
- Go HTTP fixture 覆盖目录和路径编码、Basic/匿名、强 ETag、条件创建/覆盖、拒绝重定向与错误正文泄漏、大小限制、两设备同步与冲突、租约、旧 S3 设置隔离。独立备份测试验证加密可解密、凭据可恢复、文件名唯一、共享文件不被访问且基线/冲突不变;API 测试覆盖跨地址/用户名不继承旧密码、显式清除及会话/CSRF/API Token 边界。
- 协议测试是 Go fixture,端到端 WebDAV 服务是 Node fixture,未连接外部真实 WebDAV 服务,不能据此保证任意服务商的条件写入兼容。智能体只读复核未发现阻断问题;同步和备份共用最近执行状态的边界已写入 docs/webdav-sync.md。
@@ -193,8 +195,8 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
## 2026-09-22 SSH 转发可选跳过指纹
- Go 1.25.0 `go test ./... -count=1 -timeout=120s` 全部通过(本轮 cluster 亦通过),日志 artifacts/ssh-skip-go-test.log;`go vet ./...` 通过。
- `npm.cmd --prefix web run build` 通过,仅大 chunk 提示;恢复 web/dist/.gitkeep 后构建新程序 artifacts/serviceport-ssh-skip.exe。未替换正式程序或访问真实数据库。
- 使用 SERVICEPORT_E2E_BINARY 指定新程序,在隔离端口/SQLite 运行 ssh-forward-skip.spec.ts、host-identity.spec.ts:3 项通过(13.5 秒)。新增用例首次因 Element Plus 隐藏 checkbox 的操作方式及校验文本定位失败,修正测试定位后重跑通过。本次未重跑全量浏览器套件。
- `npm.cmd --prefix web run build` 通过,仅大 chunk 提示;恢复 web/dist/.gitkeep 后构建新程序 artifacts/linkport-ssh-skip.exe。未替换正式程序或访问真实数据库。
- 使用 LINKPORT_E2E_BINARY 指定新程序,在隔离端口/SQLite 运行 ssh-forward-skip.spec.ts、host-identity.spec.ts:3 项通过(13.5 秒)。新增用例首次因 Element Plus 隐藏 checkbox 的操作方式及校验文本定位失败,修正测试定位后重跑通过。本次未重跑全量浏览器套件。
- 新增模型测试验证仅显式开关允许空指纹、仍要求密码。SSH fixture 验证空/错误指纹跳过、实际转发字节回显、返回实际服务器指纹、错误密码拒绝及关闭跳过后的严格校验;不声称真实 SSH 跳板机验证。
## 2026-09-22 合并 origin/main 943ea75
@@ -202,9 +204,9 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- 前端 `npm.cmd --prefix web run build` 通过(仅大 chunk 提示);Windows Go 1.25.0 新程序构建通过,恢复并保留 web/dist/.gitkeep。
- `go test ./... -count=1 -timeout=120s`:除 internal/cluster 的 TestMySQLReplicationAndGroupDiscovery/mysql-group-replication 返回 `cluster has no available primary or router` 外全部包通过。该失败在合并前已有记录;本次未改集群发现代码。`go vet ./...` 通过。
- Linux/amd64、Darwin/amd64 的 `go test -c ./internal/desktop` 通过;仅交叉编译,未在对应系统实际启动。
- `node scripts/verify-package.mjs artifacts/merge-verification-20260922/package`、`node scripts/verify-desktop.mjs artifacts/merge-verification-20260922/package/serviceport.exe` 均通过,覆盖单 EXE、内嵌资源、隔离登录、库内主密钥、自动 TLS、重启与目录迁移。桌面脚本首次在清理阶段重复等待已退出进程,修复后完整重跑通过。
- `node scripts/verify-package.mjs artifacts/merge-verification-20260922/package`、`node scripts/verify-desktop.mjs artifacts/merge-verification-20260922/package/linkport.exe` 均通过,覆盖单 EXE、内嵌资源、隔离登录、库内主密钥、自动 TLS、重启与目录迁移。桌面脚本首次在清理阶段重复等待已退出进程,修复后完整重跑通过。
- 启动脚本隔离回归使用 Windows PowerShell 运行 `scripts/start-dev.test.ps1` 通过。PowerShell 7 的 Add-Type 不支持测试 fixture 所需 ConsoleApplication;Windows PowerShell 首次又发现 fixture 未创建旧 desktop.lock,改为在隔离目录 OpenOrCreate 后通过。不修改真实数据库或服务。
- Playwright 使用 SERVICEPORT_E2E_BINARY 指定本次新编译程序、隔离 SQLite 和 18681/18682 端口:**104 通过、4 失败(6.1 分钟)**。失败位于 global-assets-proxy-project、kubeconfig-selection、target-outbound-selection、workspace 各一项,涉及资产草稿断言、目标字段显示及连接错误文本;不将全量浏览器回归标记为通过。TCP 映射、请求诊断元数据、范围维护、保存/标签页和响应式专项均在本轮通过。
- Playwright 使用 LINKPORT_E2E_BINARY 指定本次新编译程序、隔离 SQLite 和 18681/18682 端口:**104 通过、4 失败(6.1 分钟)**。失败位于 global-assets-proxy-project、kubeconfig-selection、target-outbound-selection、workspace 各一项,涉及资产草稿断言、目标字段显示及连接错误文本;不将全量浏览器回归标记为通过。TCP 映射、请求诊断元数据、范围维护、保存/标签页和响应式专项均在本轮通过。
- 日志、截图和测试程序位于忽略目录 artifacts/merge-verification-20260922;协议测试使用本机 fixture,并非真实 MongoDB/K8s 生产连接验证。没有部署、推送或真实数据迁移。
以下为历史分支验证记录,不替代本节合并结果。
@@ -213,47 +215,47 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- Go 1.25.0 全量命令 `go test ./... -count=1 -timeout=120s` 已运行两次;除 `internal/cluster` 的 `TestMySQLReplicationAndGroupDiscovery` 外其他包通过。该未改动测试在 mysql-replication / mysql-group-replication 子用例间歇返回 `cluster has no available primary or router`,单独 `-count=20` 也出现五次失败。本轮没有修改集群发现逻辑,不将全量 Go 测试标记为通过。日志:artifacts/tcp-go-test.log、artifacts/tcp-go-test-final.log。
- 本次相关 API、model、gateway、outbound、tcpproxy 包测试通过;最后补充断连必须非超时、认证计数不伪造以及出口包装半关闭测试后,model/gateway/outbound/tcpproxy 四包复跑通过。`go vet ./...` 通过。
- 新测试覆盖 TCP 无账号创建/保存/测试/断开、默认 0.0.0.0、拒绝只读及 TLS 终止、普通协议约束保留、直连/SOCKS5/SSH/K8s 配置、真实 loopback 字节透传与半关闭、取消清理、来源白名单与连接数限制、运行中切换保留旧连接、停服断连及无账号定时健康检查。上游和 SSH/K8s 测试均为本地 fixture,并非生产目标验证。
- `npm.cmd --prefix web run build` 通过(仅 chunk 大小警告),Vite 完成后生成 artifacts/serviceport-tcp-forwarding.exe;恢复 web/dist/.gitkeep。使用该新程序和隔离端口/SQLite,TCP、账号级连线、网关路径、断开重连、多对一六项回归通过(20.9s)。检查了 TCP 连线图截图。
- 新测试覆盖 TCP 无账号创建/保存/测试/断开、默认 0.0.0.0、拒绝只读及 TLS 终止、普通协议约束保留、直连/SOCKS5/SSH/K8s 配置、真实 loopback 字节透传与半关闭、取消清理、来源白名单与连接数限制、运行中切换断开旧连接并让重连使用新目标、停服断连及无账号定时健康检查。上游和 SSH/K8s 测试均为本地 fixture,并非生产目标验证。
- `npm.cmd --prefix web run build` 通过(仅 chunk 大小警告),Vite 完成后生成 artifacts/linkport-tcp-forwarding.exe;恢复 web/dist/.gitkeep。使用该新程序和隔离端口/SQLite,TCP、账号级连线、网关路径、断开重连、多对一六项回归通过(20.9s)。检查了 TCP 连线图截图。
- 全量浏览器运行中,新增 TCP 用例受到其他测试遗留资源影响,长画布拖拽定位失败;为用例的目标和网关增加独立项目筛选后,与 gateway-management、target-accounts 连续复跑六项通过(17.1s),含本机二进制字节回显。产品源码未因此变化。完整 UI 结果记录见下方补充;不能用专项通过替代全量通过。
- 完整 Playwright:97 通过、10 失败(10.0m,日志 artifacts/tcp-playwright.log)。除上述随后修正并复跑通过的 TCP 测试外,其余九项为此前相同失败集合:account-editor 一项、global-assets-proxy-project 两项、kubeconfig-selection 一项、target-outbound-selection 一项、workspace-features 三项、workspace 一项。修正测试隔离后未再次运行全量,只复跑上述相关组合;不将完整 suite 标记通过。
- 未替换 bin/serviceport.exe,未操作正式数据,也未提交本次改动。新程序需单独部署后功能才生效。
- 未替换 bin/linkport.exe,未操作正式数据,也未提交本次改动。新程序需单独部署后功能才生效。
## 2026-09-21 MongoDB 网关非 admin 认证库
- Go 1.25.0:`go test ./... -count=1 -timeout=120s`、`go vet ./...` 通过。
- `TestGatewayAuthenticationDatabase` 覆盖 admin、ttx-xwms-test、speculative SCRAM;拒绝错误密码、未知用户名、空认证库和会话中切换认证库。`TestOfficialDriver` 扩展为两种认证库与读写/只读的四组合,均通过。使用官方 Go 驱动和本地上游 fixture,未连接真实目标,未测试 Java 驱动。
- `npm.cmd --prefix web run build` 通过(仅 chunk 大小警告);Vite 完成后构建 `artifacts/serviceport-mongo-authsource.exe`,恢复 dist/.gitkeep。
- 设置 `SERVICEPORT_E2E_BINARY` 指向该新程序,隔离端口 18381/18382 和临时 SQLite;gateway-connection-path、account-targets、connection-diagnostics 六项 Playwright 通过(12.3s)。本次仅复跑相关浏览器用例,未重跑全量 UI;此前全量失败记录仍保留,不宣称全量 UI 通过。
- 未替换运行中的 bin/serviceport.exe,未修改实际数据库或目标数据。
- `npm.cmd --prefix web run build` 通过(仅 chunk 大小警告);Vite 完成后构建 `artifacts/linkport-mongo-authsource.exe`,恢复 dist/.gitkeep。
- 设置 `LINKPORT_E2E_BINARY` 指向该新程序,隔离端口 18381/18382 和临时 SQLite;gateway-connection-path、account-targets、connection-diagnostics 六项 Playwright 通过(12.3s)。本次仅复跑相关浏览器用例,未重跑全量 UI;此前全量失败记录仍保留,不宣称全量 UI 通过。
- 未替换运行中的 bin/linkport.exe,未修改实际数据库或目标数据。
## 2026-09-21 网关账号到目标账号的路径测试
- 最终目标多账号常驻展开版本:前端构建后生成 serviceport-multi-account.exe;gateway-connection-path、account-targets、many-to-one、disconnect 五项专项回归通过(18.3s)。account-targets 另行截图复跑一项通过(4.5s),已检查 desktop/mobile 截图;覆盖未选择源时账号可见、选择前后位置稳定、账号级连线及独立映射。
- 前一个 serviceport-account-path.exe 的完整 Playwright 结果为 97 通过、9 失败(9.9m)。失败为 account-editor 一项、global-assets-proxy-project 两项、kubeconfig-selection 一项、target-outbound-selection 一项、workspace-features 三项、workspace 一项,与此前记录的失败集合一致。该全量运行不包含最后的目标账号常驻展开改动,不能代替最终版本的全量验证;最终版本仅运行上述专项回归。两份程序均未部署覆盖正在运行的服务。
- 最终目标多账号常驻展开版本:前端构建后生成 linkport-multi-account.exe;gateway-connection-path、account-targets、many-to-one、disconnect 五项专项回归通过(18.3s)。account-targets 另行截图复跑一项通过(4.5s),已检查 desktop/mobile 截图;覆盖未选择源时账号可见、选择前后位置稳定、账号级连线及独立映射。
- 前一个 linkport-account-path.exe 的完整 Playwright 结果为 97 通过、9 失败(9.9m)。失败为 account-editor 一项、global-assets-proxy-project 两项、kubeconfig-selection 一项、target-outbound-selection 一项、workspace-features 三项、workspace 一项,与此前记录的失败集合一致。该全量运行不包含最后的目标账号常驻展开改动,不能代替最终版本的全量验证;最终版本仅运行上述专项回归。两份程序均未部署覆盖正在运行的服务。
- `TestGatewayConnectionUsesAccountMapping` 与原有 `TestConnectionErrorsExposeStageWithoutDiagnostic` 通过,使用隔离 SQLite 和 recording handler;覆盖无默认目标的账号选路、目标凭据/地址/SOCKS5 出口、拒绝未知和禁用账号、测试不改映射。
- `go test ./... -count=1 -timeout=120s`(Go 1.25.0)、`go vet ./...`、前端生产构建通过。构建仅有 chunk 大小警告。
- account-targets、connection-diagnostics、gateway-connection-path 共六项 Playwright 通过。补充多账号显式选择后,gateway-connection-path 两项复跑通过,验证请求 query、实际目标账号、K8s 资源路径及不泄露凭据/原始错误;单账号拖拽不改其他映射。新增 fixture 最初漏填 TargetTLS=false 被正常拒绝,修正测试配置后通过;多账号选择测试改为点击 Element Plus select 容器后通过。
- 使用新嵌入式 serviceport-account-path.exe、隔离端口/SQLite;完整日志位于 artifacts/mongo-live-check-20260921-160801/account-path-playwright.log。本轮没有真实 MongoDB 连接测试或正式数据变更,不将模拟响应当作真实连接成功。
- 使用新嵌入式 linkport-account-path.exe、隔离端口/SQLite;完整日志位于 artifacts/mongo-live-check-20260921-160801/account-path-playwright.log。本轮没有真实 MongoDB 连接测试或正式数据变更,不将模拟响应当作真实连接成功。
## 2026-09-21 资产统一居中弹框
- `npm.cmd --prefix web run build` 通过,仅有 chunk 大小警告;恢复 `.gitkeep` 后构建 serviceport-asset-dialog.exe。
- `npm.cmd --prefix web run build` 通过,仅有 chunk 大小警告;恢复 `.gitkeep` 后构建 linkport-asset-dialog.exe。
- Go 1.25.0 的 `go test ./... -count=1 -timeout=120s` 与 `go vet ./...` 均通过。
- 新增 asset-editor-dialog.spec.ts 两项通过:文件/连接资产与目标弹框尺寸一致、居中、无抽屉、无空页签、底部按钮可见;覆盖 1440/390/320px;取消保留草稿、Escape 放弃、再次打开清空。截图禁用过渡动画后已检查。
- 既有 asset-remarks.spec.ts 一项、usability-assets.spec.ts 两项通过,覆盖上传保存、编辑/清空备注、内联创建回填与父草稿保护。
- 共享弹框复用后,responsive.spec.ts 原先假定全部 connection-editor 都有连接页签和“保存配置”按钮;更新为保留尺寸/滚动/固定操作区断言,仅对存在的页签验证键盘导航,并识别各表单保存按钮。单独重跑 responsive.spec.ts 两项通过,覆盖 320/390/768/1920px。一次组合回归命令被终端 60 秒时限截断并报 EPIPE,不作为通过记录。
- 测试使用 SERVICEPORT_E2E_BINARY 指向新嵌入式程序、隔离 SQLite 和端口。全量执行日志为 artifacts/mongo-live-check-20260921-160801/asset-dialog-playwright.log。未修改真实资产作测试,未停止/覆盖用户运行服务。
- 测试使用 LINKPORT_E2E_BINARY 指向新嵌入式程序、隔离 SQLite 和端口。全量执行日志为 artifacts/mongo-live-check-20260921-160801/asset-dialog-playwright.log。未修改真实资产作测试,未停止/覆盖用户运行服务。
- 全量首跑结果为 94 通过、10 失败(9.6 分钟):包括上一轮已失败的 9 项,以及本次共享容器导致 responsive 用例旧假定失效的 1 项。后者更新断言后所在 responsive.spec.ts 两项已单独通过,其余 9 项未在本任务中修复。不将单独复跑结果合并成一次全量通过;git diff --check 通过。
## 2026-09-21 菜单与资源侧栏切换
- `npm.cmd --prefix web run build` 通过(只有既有 chunk 大小警告),构建后恢复 `.gitkeep` 并生成 serviceport-sidebar.exe。
- `SERVICEPORT_E2E_BINARY` 指向该新程序,使用独立 SQLite 和 18181/18182 端口。`sidebar-panels.spec.ts` 两项通过,覆盖桌面/手机、键盘及本地偏好;桌面菜单和资源截图已检查。
- `npm.cmd --prefix web run build` 通过(只有既有 chunk 大小警告),构建后恢复 `.gitkeep` 并生成 linkport-sidebar.exe。
- `LINKPORT_E2E_BINARY` 指向该新程序,使用独立 SQLite 和 18181/18182 端口。`sidebar-panels.spec.ts` 两项通过,覆盖桌面/手机、键盘及本地偏好;桌面菜单和资源截图已检查。
- 更新既有 DBX 宽度期望,以及 workspace-features/global-assets 用例中的资源视图切换入口。未更改真实业务数据来运行 UI 测试。
- 最新全量 Playwright:93 项通过、9 项失败(9.6 分钟)。侧栏新增两项、DBX 三项、菜单搜索及响应式用例通过。失败为 account-editor、global-assets-proxy-project 两项、kubeconfig-selection、target-outbound-selection、workspace-features 三项和 workspace 的旧诊断断言;涉及小屏表单高度、重复未保存确认、超时和旧显示期望,本轮未扩大范围修复。全量执行日志位于 artifacts/mongo-live-check-20260921-160801/sidebar-playwright.log。不能声明全量浏览器回归通过。
- 嵌入新前端后的全量 Go 测试与 vet 再次通过,git diff --check 通过。新版程序已生成,但未停止或替换用户当前运行的 bin/serviceport.exe。
- 嵌入新前端后的全量 Go 测试与 vet 再次通过,git diff --check 通过。新版程序已生成,但未停止或替换用户当前运行的 bin/linkport.exe。
## 2026-09-21 MongoDB 兼容握手与真实连接
@@ -263,7 +265,7 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- 最新 `GOTOOLCHAIN=go1.25.0 go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 均通过。中间一次 cluster MySQL fixture 间歇失败,最新全量通过;不代表间歇性根因已修复。
- `npm.cmd --prefix web run build` 已通过(有 chunk 大小警告),不再是此前的 esbuild EPERM 阻塞。构建后恢复 web/dist/.gitkeep,再构建嵌入新前端的隔离测试程序。
- 实际使用已保存 Kubeconfig,经 K8s port-forward 对 wall-e-test/statefulset/mongo-shopee:27017 进行握手、SCRAM、buildInfo 查询。用户提供认证库/凭据后成功返回版本 4.1.13、standalone、TLS=false、zlib。保存配置并重新读取后再次成功。未执行业务数据写入。
- Playwright 首轮使用 SERVICEPORT_E2E_BINARY 指向 artifacts/mongo-live-check-20260921-160801/serviceport.exe,独立临时 SQLite 和端口,91 项通过、9 项失败;其中部分工作区用例执行期间又加入了后续侧栏变更,因此以后续 serviceport-sidebar.exe 全量运行结果为准,不将首轮当作干净的对照基线。
- Playwright 首轮使用 LINKPORT_E2E_BINARY 指向 artifacts/mongo-live-check-20260921-160801/linkport.exe,独立临时 SQLite 和端口,91 项通过、9 项失败;其中部分工作区用例执行期间又加入了后续侧栏变更,因此以后续 linkport-sidebar.exe 全量运行结果为准,不将首轮当作干净的对照基线。
## 2026-09-21 连接路径与分阶段诊断
@@ -285,11 +287,11 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
## 2026-09-21 新建目标选择代理验证
- 新增 `target-outbound-selection.spec.ts`,先对上一轮构建的程序执行首个用例,确认默认「连接信息」页没有可见的代理选择器;修改后新增 3 项用例通过。
- `npm.cmd --prefix web run build` 通过,包含 Vue 类型检查与 Vite 构建,仅有原有主包体积警告。待构建完成后使用 Go 1.25.0 生成 `artifacts/target-outbound-selection/serviceport.exe`,保留 `web/dist/.gitkeep`。
- `npm.cmd --prefix web run build` 通过,包含 Vue 类型检查与 Vite 构建,仅有原有主包体积警告。待构建完成后使用 Go 1.25.0 生成 `artifacts/target-outbound-selection/linkport.exe`,保留 `web/dist/.gitkeep`。
- 使用上述新程序、独立数据及 19291/19292 端口执行 `target-outbound-selection.spec.ts`、`kubeconfig-selection.spec.ts`、`kubernetes.spec.ts`、`project-scope.spec.ts`、`target-editor-test.spec.ts`,21 项全部通过。覆盖首屏直连/SSH/SOCKS5/K8s 选择、服务类型和页签切换保留引用、Mongo 草稿测试参数、实际 API 保存与重新编辑回读、改回直连、跨项目选择、空列表、不可用引用及 390px 小屏。
- Go 1.25.0:`go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 全部通过,先完成后端检查再执行全量浏览器测试,避免目录清理竞争。
- 全量 Playwright 使用新程序执行 `npm.cmd --prefix web test -- --max-failures=3`:82 通过、3 失败、8 未运行。失败仍是已记录的 `account-editor.spec.ts` 320×568 权限页多出 14px 滚动,以及 `workspace-features.spec.ts` 的 URL 参数和搜索/树导航两项超时。
- 对上述未运行的 8 项补跑:`workspace.spec.ts` 7 项全部通过,覆盖 SSH/SOCKS5 目标配置、共享代理引用、凭据保留与删除保护等;`workspace-features.spec.ts` 的表格列设置用例在对隐藏 checkbox 执行 `uncheck` 时超时。再用本次修改前的 `artifacts/kubeconfig-selection/serviceport.exe` 单独复跑该列设置用例,同样失败,确认不是本次代理入口引入。两轮合计 89 项通过、4 项既有失败,未扩展修复这些无关功能。
- 对上述未运行的 8 项补跑:`workspace.spec.ts` 7 项全部通过,覆盖 SSH/SOCKS5 目标配置、共享代理引用、凭据保留与删除保护等;`workspace-features.spec.ts` 的表格列设置用例在对隐藏 checkbox 执行 `uncheck` 时超时。再用本次修改前的 `artifacts/kubeconfig-selection/linkport.exe` 单独复跑该列设置用例,同样失败,确认不是本次代理入口引入。两轮合计 89 项通过、4 项既有失败,未扩展修复这些无关功能。
- Mongo 草稿连接结果、跨范围列表和不可用引用场景使用浏览器模拟;三类代理创建、Kubeconfig 资产保存、Mongo 目标保存与切回直连通过隔离 API。未连接真实 MongoDB、SSH/SOCKS5 代理或 Kubernetes 集群;Go 协议测试使用本地 fixture。未替换现用程序或操作真实数据。
- `git diff --check` 通过,构建占位文件恢复原样;所有测试结束后确认隔离端口 19291/19292 不再监听。
- 验证边界:本轮程序于 2026-09-21 13:21 构建,Go 检查于 13:25 完成。收尾发现 `internal/store/audit_shards.go`、`commands.go`、`event_shards.go`、`store.go` 在 13:34 出现其他修改;本任务未触碰或回退它们,上述构建及测试结果不覆盖这些后续修改。
@@ -297,7 +299,7 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
## 2026-09-21 Kubeconfig 资产选择验证
- 修改前,在新构建 EXE 上创建“数据库平台 · 正式”的 Kubeconfig,再以“默认项目 · 测试”新建代理,复现资产被范围过滤且无提示/展开入口;回归用例明确失败。
- `npm.cmd --prefix web run build` 通过,包含 `vue-tsc` 与 Vite 生产构建;仅有原有主包体积警告。构建后保留 `web/dist/.gitkeep`,再使用 Go 1.25.0 构建 `artifacts/kubeconfig-selection/serviceport.exe`,未并发替换嵌入资产。
- `npm.cmd --prefix web run build` 通过,包含 `vue-tsc` 与 Vite 生产构建;仅有原有主包体积警告。构建后保留 `web/dist/.gitkeep`,再使用 Go 1.25.0 构建 `artifacts/kubeconfig-selection/linkport.exe`,未并发替换嵌入资产。
- 使用该新 EXE、独立数据及 19191/19192 端口,`kubeconfig-selection.spec.ts`、`kubernetes.spec.ts`、`usability-assets.spec.ts`、`project-scope.spec.ts` 共 7 项通过。新增 2 项覆盖跨范围 Kubeconfig 显式选择、排除普通文件、隐藏范围后保留引用、保存代理与 Mongo 目标并回读、空态类型提示、刷新失败重试和 390px 小屏。
- Kubernetes 命名空间/资源查询使用浏览器模拟响应;资产、代理和 Mongo 目标保存经过真实隔离 API。没有调用真实 Kubernetes API 或连接真实 MongoDB,不将 UI/引用验证表述为实时 port-forward 成功。
- Go 1.25.0:`go test ./... -count=1 -timeout=120s` 和 `go vet ./...` 串行复跑全部通过,包括 `internal/cluster`。首次与 Playwright 并发运行时,Go 枚举目录遇到正在清理的 `web/test-results`,报 `pattern ./...: open web\test-results: The system cannot find the file specified.`;待浏览器结束后重跑消除该竞争。协议测试使用本地 fixture,未连接真实数据库或 SSH 服务。
@@ -312,7 +314,7 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- 真实整合结果复核:`consolidate-20260920T171719.754430900Z/report.json` 为 `applied:true`;16 个备份文件及 10 个保留原文件哈希通过。`ready/` 的主库复制件完整性、库内密钥和全部资源解密通过,8 个目标、4 个网关、1 个资产,2 个导入网关停止。实际安装主库在代理环境读取返回 `EPERM`,未直接核对其内容或启动真实服务。
- 前端 vue-tsc/Vite 构建通过并保留 `.gitkeep`,存在已知 bundle 体积警告。账号编辑器 320×568 全页签以及 URL、搜索导航、列设置专项通过;测试未放宽滚动阈值,使用可见 checkbox 标签并保留状态断言。
- `node scripts/package.mjs --all` 完成六目标构建,发布目录 `artifacts/releases/2026-09-20T16-40-11-688Z-701d60e4/` 的六份 SHA256 均与清单一致。Windows x64 原生包与本地启动(无外置密钥、可选认证、TLS、重启与搬移)验收通过。Linux/macOS × amd64/arm64 的桌面测试二进制也编译通过,未实机执行;远程 CI 未执行。
- 全量 Playwright 使用此次新程序 `artifacts/db-key-verification/serviceport.exe`,通过 `SERVICEPORT_E2E_BINARY` 指定,隔离数据库和端口 18485/24687。首轮 91 通过、4 失败:工作区 helper 仍匹配精简前的白名单说明文字,重试又遇到前次创建的重复 fixture。已同步文案断言并重建两套隔离测试库重跑;首轮目录为 `artifacts/db-key-verification/full`,最终目录为 `artifacts/db-key-verification/full-final`。
- 全量 Playwright 使用此次新程序 `artifacts/db-key-verification/linkport.exe`,通过 `LINKPORT_E2E_BINARY` 指定,隔离数据库和端口 18485/24687。首轮 91 通过、4 失败:工作区 helper 仍匹配精简前的白名单说明文字,重试又遇到前次创建的重复 fixture。已同步文案断言并重建两套隔离测试库重跑;首轮目录为 `artifacts/db-key-verification/full`,最终目录为 `artifacts/db-key-verification/full-final`。
- 最终完整运行设置 `--retries=0`,95 项全部通过(7.0 分钟),包含此前失败的四项工作区/小屏用例及主题切换。测试结束后停止本轮两套隔离服务;未部署到真实数据目录。
## 2026-09-20 三平台与明文主密钥验证
@@ -321,15 +323,15 @@ fixture 覆盖包括生成仅监听 `127.0.0.1` 且带随机认证的 SOCKS5 入
- `node scripts/build-web.mjs --force` 完成前端类型检查及 Vite 生产构建,保留 `web/dist/.gitkeep`;仅有 bundle 体积警告。之后执行 `node scripts/package.mjs --all`,生成 Windows/Linux/macOS × amd64/arm64 六个平台的独立程序,文件校验和均与清单一致。
- Windows x64 使用此次实际程序通过单文件发布验收(版本信息、内嵌网页、隔离启动和登录)及 `verify-desktop.mjs`(免登录、可选管理员设置、自动 TLS、未加密密钥持久化、认证重启和搬移数据)。其他五个目标为编译验证。另对 Linux/macOS × amd64/arm64 的 `internal/desktop` 测试代码执行 `go test -c`,全部编译成功,未在异系统执行。
- Node 脚本语法、三个 Unix shell 入口的 Git Bash `bash -n`、CI YAML 解析及矩阵检查、`git diff --check` 通过。Windows 启动脚本 fixture 回归在 `Get-CimInstance Win32_Process` 遇到当前环境权限拒绝,未完成源码启动和备份验收;不计为通过。远程三系统 CI 已配置但未执行。
- 产物:`artifacts/releases/2026-09-20T13-38-02-849Z-dc352132/`;浏览器验证使用复制出的新程序 `artifacts/platform-verification/serviceport.exe`,通过 `SERVICEPORT_E2E_BINARY` 指定,测试服务使用隔离数据及 8485/14687 端口。未部署到真实数据目录,未转换或删除实际 `master-key.dpapi`。
- 产物:`artifacts/releases/2026-09-20T13-38-02-849Z-dc352132/`;浏览器验证使用复制出的新程序 `artifacts/platform-verification/linkport.exe`,通过 `LINKPORT_E2E_BINARY` 指定,测试服务使用隔离数据及 8485/14687 端口。未部署到真实数据目录,未转换或删除实际 `master-key.dpapi`。
- 全量 Playwright 95 项、不重试:90 通过、5 失败。最后的主题用例在 `browser.newContext` 阶段因浏览器关闭失败,用新浏览器单独复跑后通过;剩余 4 项为此前已在旧程序复现的账号权限页 320×568 多出 14px 滚动,以及 `workspace-features.spec.ts` 的 URL 参数、搜索/树导航、表格列设置三项超时。整套结果不计为通过。本轮环境保存、请求报错信息、SQLite、RabbitMQ 版本、Sentinel 认证用例均通过。报告目录为 `artifacts/platform-verification/playwright` 和 `artifacts/platform-verification/theme`。
## 2026-09-20 Windows 多路径启动验证
- Go 1.25.0:`go test ./internal/desktop ./cmd/serviceport -count=1 -timeout=120s` 通过,覆盖当前目录新库、AppData 已有库、双库优先级、相对/绝对显式路径、不依赖 AppData 环境、手动主密钥模式不遍历、目录占位错误、已有库缺密钥,以及 `data-path` 命令无初始化副作用与参数校验。
- Go 1.25.0:`go test ./internal/desktop ./cmd/linkport -count=1 -timeout=120s` 通过,覆盖当前目录新库、AppData 已有库、双库优先级、相对/绝对显式路径、不依赖 AppData 环境、手动主密钥模式不遍历、目录占位错误、已有库缺密钥,以及 `data-path` 命令无初始化副作用与参数校验。
- `powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/start-dev.test.ps1` 通过:临时含空格项目、异目录调用、调用者目录恢复、构建锁、BuildOnly/FullCheck/RebuildWeb、构建/路径解析失败隔离;源码/发布入口不强制数据路径且保留显式设置;完整备份实际选中目录,并拒绝递归备份和被其他实例占用的数据。该脚本使用只记录环境变量的可执行 fixture,不启动真实服务。
- `go vet ./...`、`npm.cmd --prefix web run build`、`git diff --check` 通过。Vite 报告主包体积警告,未作为失败处理;构建后保留 `web/dist/.gitkeep`,待 Vite 完成后再构建嵌入前端的新 EXE。
- `node scripts/verify-desktop.mjs artifacts/path-validation/serviceport.exe` 使用此次新构建程序及 Edge 无头浏览器,通过当前目录创建、首次账号设置、自动 TLS、DPAPI 密钥保持、认证重启,以及将测试库移至隔离 AppData 后复用旧账号且不另建本地库。原桌面脚本的管理员字段定位已更新为实际表单标签“登录账号/登录密码”。
- `node scripts/verify-desktop.mjs artifacts/path-validation/linkport.exe` 使用此次新构建程序及 Edge 无头浏览器,通过当前目录创建、首次账号设置、自动 TLS、DPAPI 密钥保持、认证重启,以及将测试库移至隔离 AppData 后复用旧账号且不另建本地库。原桌面脚本的管理员字段定位已更新为实际表单标签“登录账号/登录密码”。
- 全量 `go test ./... -count=1 -timeout=120s` 未全绿:未改动的 `internal/cluster` 中 `TestMySQLReplicationAndGroupDiscovery` 出现“无可用主节点”,单包复跑还出现拓扑不符;其余包通过。协议覆盖全部为本地 fixture,未连接真实 MySQL/MongoDB/SSH 服务。
- Playwright 使用新 EXE、独立数据和 19091/19092 端口,执行 `npm.cmd test -- --max-failures=3`:77 通过、3 失败、8 未运行,达到失败上限后停止。失败为 `account-editor.spec.ts` 的 320×568 权限页多出 14px 滚动,以及 `workspace-features.spec.ts` 的 URL 参数、搜索/树导航两项超时;未修改这些前端功能或把此轮报告为全套通过。
- 未运行实际项目的启动入口、未替换现用 EXE、未读写真实项目数据或用户 AppData 数据;路径变更无需数据库表结构迁移。
@@ -348,7 +350,7 @@ SQLite 的 `.sqlite`、`.sqlite3` 及其 journal/WAL/SHM 文件忽略;根目
- SSH 使用隔离的本机协议 fixture,验证未确认/错误指纹时不进行认证、确认后连接成功、后续测试不会继承临时信任。其他协议的新增接口测试使用记录型 handler;浏览器测试模拟远端结果。未连接生产服务或进行真实数据库版本验收。
- `go test ./... -count=1 -timeout=120s` 中 API、存储、模型及协议包通过;Windows 桌面 `TestLocalKeyPersistenceAndLock` 仍报 `The system cannot find the file specified.`。网关 `TestShutdownClosesAfterGracePeriod` 首次报 `handler not started`,随后单独重跑整个 `internal/gateway` 包通过,因此不宣称首次全量通过。
- `go vet ./...`、Vue 类型检查与 Vite 构建通过。Vite 保留大包体积提示;前端构建完成后用 Go 1.25.0 生成新的嵌入式测试程序,恢复 `web/dist/.gitkeep`。
- 默认 18081/18082 被另一轮测试占用,本轮设置 `SERVICEPORT_E2E_PORT=19081`、`SERVICEPORT_E2E_AUTH_PORT=19082` 并通过 `SERVICEPORT_E2E_BINARY` 指定新程序;使用独立临时数据和结果目录,未停止其他服务、未部署实际数据目录。
- 默认 18081/18082 被另一轮测试占用,本轮设置 `LINKPORT_E2E_PORT=19081`、`LINKPORT_E2E_AUTH_PORT=19082` 并通过 `LINKPORT_E2E_BINARY` 指定新程序;使用独立临时数据和结果目录,未停止其他服务、未部署实际数据目录。
- `target-editor-test.spec.ts` 的 13 项用例全部通过:修改后按指定账号测试且不保存、10 种协议新建草稿测试、390px 手机下 MongoDB URI 副本集草稿测试、SSH 临时指纹取消与确认。其他协议的远端连接结果由浏览器路由模拟,后端 SSH 认证由上面的本地 fixture 验证。
- 整套浏览器运行先通过 35 项,随后并行工作新增的 `rabbit-version.spec.ts` 在创建测试目标时失败:fixture 账号缺少保存接口必需的名称,尚未进入连接测试。未修改该并行文件,余下用例另起隔离服务继续运行;不能把这一轮称为首次全套通过。
- 余下 32 项先通过 31 项,最后的主题用例在创建浏览器上下文时遇到 Edge 退出;用新浏览器单独重跑通过。最终本轮范围内的 67 项浏览器用例分批通过,另有上述并行 RabbitMQ 用例失败记录。Windows 测试服务退出等待期间仅停止本轮指定路径的测试程序,未停止用户服务或另一轮测试程序。
@@ -359,7 +361,7 @@ SQLite 的 `.sqlite`、`.sqlite3` 及其 journal/WAL/SHM 文件忽略;根目
以下是本轮实际执行记录;下方早期版本的“全量通过”不代表本轮 Windows 环境所有测试通过。
- 前端:`npm.cmd --prefix web run build`(Vue 类型检查及 Vite 生产构建)通过。使用 Go 1.25.0 在前端构建结束后生成新的嵌入式测试程序,并通过 `SERVICEPORT_E2E_BINARY` 运行 Playwright;恢复了受版本控制的 `web/dist/.gitkeep`。
- 前端:`npm.cmd --prefix web run build`(Vue 类型检查及 Vite 生产构建)通过。使用 Go 1.25.0 在前端构建结束后生成新的嵌入式测试程序,并通过 `LINKPORT_E2E_BINARY` 运行 Playwright;恢复了受版本控制的 `web/dist/.gitkeep`。
- 最终 55 项浏览器用例分批验证通过。全量运行先通过 48 项;旧工作区测试仍假设白名单位于登录表单,按新分组入口修正后,该文件的 6 项业务用例通过。最后的主题用例在创建浏览器上下文时遇到 Edge 退出,使用新浏览器单独重跑通过。因此不是一次完整运行零失败。
- 新增免滚动用例使用 12 个已有账号,检查 1440×900、375×812、320×568 的登录、目标、权限、更多分组内容高度;验证新增第 13 个账号立即聚焦、切换保留草稿、跨账号错误定位及保存后凭据保留。小屏溢出修正后该用例通过。
- 账号路由后端改动执行 `go test ./... -count=1 -timeout=120s`。相关模型、存储、API、网关及协议包通过;`internal/desktop` 的 `TestLocalKeyPersistenceAndLock` 仍报 `The system cannot find the file specified.`,因此完整命令返回失败。`go vet ./...` 通过。
@@ -400,11 +402,11 @@ $env:GOTOOLCHAIN = 'go1.25.0'
go test ./...
go vet ./...
npm.cmd --prefix web run build
go build -o bin/serviceport.exe ./cmd/serviceport
go build -o bin/linkport.exe ./cmd/linkport
npm.cmd --prefix web test
```
浏览器测试使用隔离的临时 SQLite、随机主密钥和仅供测试的登录密码,监听 `127.0.0.1:18081`,结束后清理。不连接用户的数据库。默认使用已安装的 Edge;其他平台可安装 Playwright Chromium 并设置 `SERVICEPORT_BROWSER_CHANNEL=chromium`。截图输出到忽略版本控制的 `web/test-results`。
浏览器测试使用隔离的临时 SQLite、随机主密钥和仅供测试的登录密码,监听 `127.0.0.1:18081`,结束后清理。不连接用户的数据库。默认使用已安装的 Edge;其他平台可安装 Playwright Chromium 并设置 `LINKPORT_BROWSER_CHANNEL=chromium`。截图输出到忽略版本控制的 `web/test-results`。
## 已有自动化覆盖
@@ -453,11 +455,11 @@ docker compose -f deploy/compose.test.yml run --rm --no-deps mongo80-init
独立连接已准备好的目标测试实例:
- MySQL:`SERVICEPORT_MYSQL_TEST_ADDR`、`SERVICEPORT_MYSQL_TEST_USER`、`SERVICEPORT_MYSQL_TEST_PASSWORD`、可选 `SERVICEPORT_MYSQL_TEST_CA`(CA 文件路径),执行 `go test ./internal/protocol/mysqlproxy -run TestRealMySQL -count=1 -v`。
- MongoDB:`SERVICEPORT_MONGO_TEST_ADDR`、`SERVICEPORT_MONGO_TEST_USER`、`SERVICEPORT_MONGO_TEST_PASSWORD`、可选 `SERVICEPORT_MONGO_TEST_CA`(PEM 内容),执行 `go test ./internal/protocol/mongoproxy -run TestRealMongo -count=1 -v`。
- SSH:`SERVICEPORT_SSH_TEST_ADDR`、`SERVICEPORT_SSH_TEST_USER`、`SERVICEPORT_SSH_TEST_PASSWORD`,执行 `go test ./internal/protocol/sshproxy -run TestRealSSH -count=1 -v`。验证远程输出和退出码、SFTP 上传下载及清理、禁止 TCP 转发。
- MySQL:`LINKPORT_MYSQL_TEST_ADDR`、`LINKPORT_MYSQL_TEST_USER`、`LINKPORT_MYSQL_TEST_PASSWORD`、可选 `LINKPORT_MYSQL_TEST_CA`(CA 文件路径),执行 `go test ./internal/protocol/mysqlproxy -run TestRealMySQL -count=1 -v`。
- MongoDB:`LINKPORT_MONGO_TEST_ADDR`、`LINKPORT_MONGO_TEST_USER`、`LINKPORT_MONGO_TEST_PASSWORD`、可选 `LINKPORT_MONGO_TEST_CA`(PEM 内容),执行 `go test ./internal/protocol/mongoproxy -run TestRealMongo -count=1 -v`。
- SSH:`LINKPORT_SSH_TEST_ADDR`、`LINKPORT_SSH_TEST_USER`、`LINKPORT_SSH_TEST_PASSWORD`,执行 `go test ./internal/protocol/sshproxy -run TestRealSSH -count=1 -v`。验证远程输出和退出码、SFTP 上传下载及清理、禁止 TCP 转发。
三类测试可额外设置 `SERVICEPORT_TEST_SOCKS_ADDR` 和可选 `SERVICEPORT_TEST_SOCKS_USER/PASSWORD`,使用真实生产出站层经 SOCKS5 连接上述目标。测试中的首次 SSH 指纹探测仅适用于已明确指定的一次性测试服务,不替代生产环境的管理员独立核对流程。
三类测试可额外设置 `LINKPORT_TEST_SOCKS_ADDR` 和可选 `LINKPORT_TEST_SOCKS_USER/PASSWORD`,使用真实生产出站层经 SOCKS5 连接上述目标。测试中的首次 SSH 指纹探测仅适用于已明确指定的一次性测试服务,不替代生产环境的管理员独立核对流程。
测试账号须允许创建和删除独立测试库;未设置地址时真实版本测试明确跳过。省略上游 CA 时测试按明文上游运行,但网关客户端仍执行 TLS。
@@ -474,8 +476,8 @@ docker compose -f deploy/compose.test.yml run --rm --no-deps mongo80-init
对解压后的发行包运行:
```powershell
node scripts/verify-package.mjs <单EXE交付目录或serviceport.exe路径>
node scripts/verify-desktop.mjs <解压后的目录>/serviceport.exe
node scripts/verify-package.mjs <单EXE交付目录或linkport.exe路径>
node scripts/verify-desktop.mjs <解压后的目录>/linkport.exe
```
脚本只在独立临时工作目录中测试:无已有库时创建 `data` 且不创建隔离的 AppData;随后将测试数据移至隔离的 AppData,验证旧库复用、原账号登录、密钥不变且不另建本地库。整个过程不读取实际用户配置。桌面脚本用 Edge 自动操作首次设置页面,禁用系统浏览器弹窗并使用随机本机端口;系统默认浏览器关联及资源管理器双击交互未由此脚本覆盖。
@@ -495,3 +497,75 @@ node scripts/verify-desktop.mjs <解压后的目录>/serviceport.exe
- Redis 使用可控 RESP 服务验证认证、HELLO、流水线、订阅、取消及审计;RabbitMQ 使用 amqp091-go 客户端连接模拟 broker;PostgreSQL 使用 pgconn 客户端连接模拟后端,验证简单/扩展查询、参数化、认证及审计。尚未对真实 Redis、RabbitMQ、PostgreSQL 部署做版本兼容验收。
- 审计测试覆盖跨三日 411 条记录分页、跨日关键词和完整导出、密文迁移、失败重试/冲突保护、7 天清理、取消和并发。连接事件覆盖旧记录迁移、跨日筛选/分页及 CSV 导出。未执行 Go race 检查(当前未配置所需 C 工具链)。
- Edge 六项流程通过,覆盖拖拽连接、审计筛选/导出、共享目标、可选只读、SSH/SOCKS5 出口、三种新增服务配置、主题及移动布局。最后一项首次因 Edge 进程退出失败,单独重跑通过。
## Account-free protocol coverage
Model, API, readiness and protocol fixture tests cover anonymous Redis and
MongoDB targets and gateways. MongoDB gateway tests use `net.Pipe` fixtures;
they do not claim validation against a live MongoDB server.
## Redis password-only authentication (2026-09-28)
Using Go 1.25.0 with serial package compilation and workspace-local temporary,
build-cache and module-cache directories:
- `go test ./internal/model -count=1 -timeout=120s` passed.
- Focused API and Redis protocol tests for password-only Redis and the MongoDB
rejection boundary passed.
- `internal/cluster`, `internal/gateway`, `internal/protocol/redisproxy` and
`internal/protocol/mongoproxy` package tests passed in the related-package run.
- `go vet ./...` passed. `vue-tsc --noEmit`, the 13-case frontend authentication
matrix, the three connection-example cases and the legacy draft regression
passed. All non-browser `web/tests/*.test.mjs` domain suites also passed.
- `npm.cmd --prefix web run build` completed type checking, then Vite could not
start esbuild in this Windows sandbox (`spawn EPERM`). No fresh executable or
Playwright run is claimed.
- The full `go test ./... -count=1 -timeout=120s` run completed with unrelated
environment failures in `cmd/linkport`, `internal/desktop`,
`internal/security` and `internal/store` due denied local-file permissions,
plus `internal/api` failures for a portable SQLite snapshot and an OS-denied
loopback listener. All protocol packages passed, and the focused authentication
API tests passed.
Coverage includes target creation/redaction/persistence, draft and saved gateway
tests, Redis-only model validation across every supported service type, direct
and Cluster target AUTH command forms, and direct/Cluster gateway acceptance of
`AUTH password` with rejection before upstream dialing for a wrong password.
Cluster fixtures also verify that both authenticated and account-free gateways
rewrite password-only target credentials to `HELLO ... AUTH default password`.
Protocol coverage uses local RESP/`net.Pipe` fixtures, not live Redis or Sentinel.
## Live target switch disconnect behavior (2026-09-28)
- Go 1.25.0 targeted `internal/gateway` tests passed for the generic relay, TCP,
SSH and multi-account SSH paths. They verify that a failed persistence callback
preserves old clients, a successful switch closes all current clients without
replacing the listener, and reconnecting clients use the new target.
- The focused `internal/api/TestLiveTargetSwitchDisconnectsClientsOnlyAfterSave`
test passed against isolated SQLite and loopback TCP fixtures. It covers the
existing `POST /api/v1/proxies/{id}/target` endpoint, including failed save,
successful disconnect, stable bound address and reconnect routing.
- The complete `internal/gateway` package passed. In the broader API package run,
unrelated existing SSH host-key and independent TCP/SOCKS5 fixture tests failed
because their local test connections returned 422 in the Windows sandbox; the
target-switch API test itself passed.
- Frontend compilation, a development server and Playwright execution were not
run for this development-stage adjustment at the user's request. Source review
confirms the changed switch notice exists in the Chinese, English and Japanese
dictionaries and both target-switch UI entry points use those keys.
- All protocol behavior checks above use controlled loopback fixtures. They do
not claim validation against live MySQL, Redis, RabbitMQ, SSH or other targets,
and no real `data` directory was modified.
## MySQL performance audit highlighting (2026-09-29)
- Added protocol fixture coverage for no-index and no-good-index status bits in
OK, metadata EOF and row EOF packets, plus deterministic observed-duration
capture without sleeping.
- Added store coverage that writes an encrypted command result and verifies the
command list hydrates its duration and full-scan risk in the same query.
- Added frontend pure-logic coverage for the 3000 ms slow-query boundary,
independent index-risk flags and compact duration formatting.
- Per the development-stage instruction, these tests, compilation, a local
server and browser checks were not run. Only static source inspection and
`git diff --check` are claimed. The fixtures do not connect to a live MySQL
server, and no real target or `data` directory was modified.
+3 -3
View File
@@ -6,7 +6,7 @@
## 实施顺序与前置验收
- [ ] **UI-000|待完成:生产构建与运行态验收。** Go 1.25.0 全部后端测试、vet、前端类型检查已通过;Vite/esbuild 与 Playwright worker 均受 `spawn EPERM` 阻塞。仍需构建前端和新嵌入程序,将 `SERVICEPORT_E2E_BINARY` 指向新程序,在隔离数据库/空闲端口运行全套测试,检查全部页面、四类编辑器、登录/退出、主题及 320/390/768/1920px 截图。测试发现与纯逻辑断言不能替代此验收。
- [ ] **UI-000|待完成:生产构建与运行态验收。** Go 1.25.0 全部后端测试、vet、前端类型检查已通过;Vite/esbuild 与 Playwright worker 均受 `spawn EPERM` 阻塞。仍需构建前端和新嵌入程序,将 `LINKPORT_E2E_BINARY` 指向新程序,在隔离数据库/空闲端口运行全套测试,检查全部页面、四类编辑器、登录/退出、主题及 320/390/768/1920px 截图。测试发现与纯逻辑断言不能替代此验收。
- 优先补齐连接配置:独立 URL 参数编辑(UI-007)→ 未解析 URL 与测试/保存的一致性(UI-008)。先明确协议支持和参数生效链路,不交付只有输入框、实际连接不生效的功能。
- 工作区第一阶段:全局资源搜索 → 树形资源导航 → 表格列设置。
- 后续阶段:统一操作菜单、首页快捷入口、连接标识与测试反馈;高级参数须先确认后端语义,多标签工作区须先完成状态与生命周期设计,再分别实施。
@@ -30,9 +30,9 @@
## 连接配置 UI 差异核对
以下依据 DBX 的 `apps/desktop/src/components/connection/ConnectionDialog.vue` 和 serviceport 当前源码核对;完整 URL 导入、独立 URL 参数和高级连接配置是不同功能。
以下依据 DBX 的 `apps/desktop/src/components/connection/ConnectionDialog.vue` 和 linkport 当前源码核对;完整 URL 导入、独立 URL 参数和高级连接配置是不同功能。
| 项目 | serviceport 当前状态 | 待补范围 |
| 项目 | linkport 当前状态 | 待补范围 |
| --- | --- | --- |
| 完整连接 URL 导入 | 提供待解析/已填入/错误状态,测试及保存前统一应用;解析后清空原始 URL | 浏览器验证同时存在待处理输入和错误重试,见 UI-008 |
| 独立 URL 参数输入 | 已新增独立输入、原子校验、结构化字段映射和回读,不存任意原始字符串 | 浏览器验证字段联动、保存重开,见 UI-007 |
+6 -6
View File
@@ -10,19 +10,19 @@
本次生产构建及基于新嵌入程序的全部 124 项 Playwright 测试通过,覆盖主题、移动布局和原有交互;下方 `spawn EPERM` 为此前记录,不代表当前验证状态。隔离预览位于 `bin/dev-build/ui-colors-20260924/light-desktop.png`、`dark-mobile.png`。
serviceport 的全部工作区页面采用参考本地 `C:\work\github\dbx` 的紧凑桌面工具风格。参考范围是视觉体系与通用控件,而不是把 DBX 的数据库编辑器、Tauri 运行时、连接状态或业务功能移植进来。serviceport 继续使用 Vue 3、Element Plus 和原有管理 API。
linkport 的全部工作区页面采用参考本地 `C:\work\github\dbx` 的紧凑桌面工具风格。参考范围是视觉体系与通用控件,而不是把 DBX 的数据库编辑器、Tauri 运行时、连接状态或业务功能移植进来。linkport 继续使用 Vue 3、Element Plus 和原有管理 API。
2026-09-20 已进一步实现 [清单](todo.md) 中 UI-001 至 UI-011 的代码:资源搜索、资源树、列设置、工作区标签、操作菜单、最近访问,以及独立 URL 参数、高级连接选项、连接标识与测试反馈。工作区操作及生命周期见 [工作区交互](workspace-ui.md),协议生效范围见 [连接配置](connection-editor.md)。代码实现与运行态验收分开记录,当前浏览器验证仍受环境限制。
## 设计来源与对应关系
| DBX 参考位置 | serviceport 实现 |
| DBX 参考位置 | linkport 实现 |
| --- | --- |
| `apps/desktop/src/styles/tokens.css` | `web/src/theme.css`:中性灰配色、深色石墨背景、文字层级、4px 控件圆角、6px 面板圆角 |
| `apps/desktop/src/components/layout/AppToolbar.vue`、`AppSidebar.vue` | `web/src/App.vue`、`style.css`:紧凑工具栏、浅灰/深灰侧栏、小尺寸线性图标、选中标记 |
| `apps/desktop/src/components/ui/button/index.ts`、`input/Input.vue` | `web/src/controls.css`:按钮、输入框、焦点状态、菜单、表格和浮层的统一外观 |
不增加 UI 框架或图标依赖,不复制 DBX 品牌标识。`AppIcon.vue` 提供 serviceport 自有的线性 SVG 图标。字体优先使用本地 Geist,未安装时回退到系统中文字体和 Segoe UI;没有新增网络字体请求。
不增加 UI 框架或图标依赖,不复制 DBX 品牌标识。`AppIcon.vue` 提供 linkport 自有的线性 SVG 图标。字体优先使用本地 Geist,未安装时回退到系统中文字体和 Segoe UI;没有新增网络字体请求。
## 覆盖范围
@@ -33,7 +33,7 @@ serviceport 的全部工作区页面采用参考本地 `C:\work\github\dbx` 的
浅色主题采用白色内容区、浅灰侧栏及深色主按钮;深色主题使用分层石墨背景及浅色主按钮,文字、勾选标记和按钮文字同时适配。成功、警告和失败仍以语义颜色区分,不把所有状态改成灰色。
主题选择器位于工作区顶部工具栏中,不再悬浮遮挡页面信息。登录页保留独立选择器。原有 `serviceport-theme` 存储键以及 `light`、`dark`、`ocean`、`system` 值不变;`light` 的显示名称从“浅色 · 森绿”改为“浅色 · 经典”,`dark` 改为“深色 · 石墨”。海蓝继续提供蓝色强调色,“跟随系统”继续响应系统深浅色变化。
主题选择器位于工作区顶部工具栏中,不再悬浮遮挡页面信息。登录页保留独立选择器。原有 `linkport-theme` 存储键以及 `light`、`dark`、`ocean`、`system` 值不变;`light` 的显示名称从“浅色 · 森绿”改为“浅色 · 经典”,`dark` 改为“深色 · 石墨”。海蓝继续提供蓝色强调色,“跟随系统”继续响应系统深浅色变化。
## 布局与交互
@@ -47,7 +47,7 @@ serviceport 的全部工作区页面采用参考本地 `C:\work\github\dbx` 的
## 维护入口
项目品牌图标单独使用 `web/public/serviceport.svg`,登录、侧栏和浏览器 favicon 共用;普通功能图标仍由 `AppIcon.vue` 提供。启动 ASCII 标识与资源嵌入说明见 [项目标识](branding.md)。
项目品牌图标单独使用 `web/public/linkport.svg`,登录、侧栏和浏览器 favicon 共用;普通功能图标仍由 `AppIcon.vue` 提供。启动 ASCII 标识与资源嵌入说明见 [项目标识](branding.md)。
`main.ts` 按 Element Plus 基础样式、深色变量、`theme.css`、`style.css`、`controls.css`、`responsive.css` 的顺序加载全局样式。颜色和尺寸定义放在 `theme.css`;布局放在 `style.css`;公共 Element Plus 外观放在 `controls.css`;断点规则放在 `responsive.css`。独立组件的局部样式应引用这些令牌,不再定义一套硬编码主题色。
@@ -61,4 +61,4 @@ serviceport 的全部工作区页面采用参考本地 `C:\work\github\dbx` 的
- Go 1.25.0 全部后端测试及 `go vet ./...` 通过。协议新增验证使用本地 fixture;数据使用隔离测试目录,未接触真实目标或工作区 `data`。
- `npm.cmd --prefix web run build` 在启动 esbuild 时失败(`spawn EPERM`),Playwright worker 启动也被拒绝。尚无本轮新前端产物、嵌入程序或浏览器执行/截图;不能使用旧二进制和下方其他文档的历史截图代替验收。
依赖可用后,先运行前端构建,再编译 Go 程序;不要同时替换 `web/dist` 和构建嵌入资产。随后将 `SERVICEPORT_E2E_BINARY` 指向刚生成的可执行文件,使用隔离数据库和空闲端口运行完整 Playwright 套件。
依赖可用后,先运行前端构建,再编译 Go 程序;不要同时替换 `web/dist` 和构建嵌入资产。随后将 `LINKPORT_E2E_BINARY` 指向刚生成的可执行文件,使用隔离数据库和空闲端口运行完整 Playwright 套件。
+2 -2
View File
@@ -21,9 +21,9 @@
### sing-box 连接资产
sing-box 资产只接受最多 64 KiB 的单个 outbound JSON object,不接受分享链接或包含 `inbounds`、`outbounds`、`route`、`log` 的完整配置。保存后界面只显示派生协议类型和安全摘要;编辑时留空保留旧 JSON,应用新 JSON 才替换。配置历史及导入预览只标记秘密变化,不显示前后正文。
sing-box 资产接受最多 64 KiB 的单个 outbound JSON object,也接受粘贴或导入的完整 sing-box 配置文件。界面会按 `route.final` 或 `outbounds` 顺序自动提取一个远程 outbound,不保存完整配置中的其他字段;分享链接仍不接受。保存后界面只显示派生协议类型和安全摘要;编辑时留空保留旧 JSON,应用新 JSON 才替换。配置历史及导入预览只标记秘密变化,不显示前后正文。
serviceport 不捆绑 sing-box。服务端通过 `SERVICEPORT_SING_BOX` 指定 binary,未设置时从 `PATH` 查找。资产“检查配置”只执行安装版本的 `sing-box check`,不连接代理或目标;目标测试才会启动受管子进程并验证实际链路。安装、允许字段、版本、许可证及安全边界见 [sing-box 连接资产](sing-box.md)。
linkport 不捆绑 sing-box。服务端通过 `LINKPORT_SING_BOX` 指定 binary,未设置时从 `PATH` 查找。资产“检查配置”只执行安装版本的 `sing-box check`,不连接代理或目标;目标测试才会启动受管子进程并验证实际链路。安装、允许字段、版本、许可证及安全边界见 [sing-box 连接资产](sing-box.md)。
## StatefulSet 通过独立 Kubeconfig 代理复用
+1 -1
View File
@@ -47,7 +47,7 @@
选择 Kubeconfig 资产后读取 Context 列表并自动查询命名空间,命名空间仅从下拉框选择,失败时可点击「查询命名空间」重试。切换 Context 后自动刷新命名空间列表;切换命名空间后清空旧资源选择,再查询并选择 pod/service/deployment/statefulset。查询只执行读取请求,命名空间自动分页读取,资源每类最多返回 200 项,单次查询期限为 8 秒。权限不足或集群不可达时显示错误并支持重试,命名空间和资源不提供手动输入。
自动发现暂不支持 Kubeconfig 代理,显示原因后允许手工输入。上传成资产或内联保存的 Kubeconfig 必须自包含,不能引用 serviceport 主机上的 CA、客户端证书/密钥或 Token 文件,也不能使用 `exec`/`auth-provider` 登录插件;该校验同样作用于正式连接,避免绕过发现步骤执行本机命令。显式本地 Kubeconfig 文件路径及其中的登录插件仍保留。测试采用隔离的 Kubernetes API 模拟服务,不代表已验证真实集群。
自动发现暂不支持 Kubeconfig 代理,显示原因后允许手工输入。上传成资产或内联保存的 Kubeconfig 必须自包含,不能引用 linkport 主机上的 CA、客户端证书/密钥或 Token 文件,也不能使用 `exec`/`auth-provider` 登录插件;该校验同样作用于正式连接,避免绕过发现步骤执行本机命令。显式本地 Kubeconfig 文件路径及其中的登录插件仍保留。测试采用隔离的 Kubernetes API 模拟服务,不代表已验证真实集群。
## 资产识别与引用跳转
+1 -1
View File
@@ -5,7 +5,7 @@
## 连接设置
- WebDAV 地址填写已有基础目录的完整 HTTP(S) 地址,例如 `https://dav.example.com/remote.php/dav/files/user/`,而不是仅填写网站首页。基础目录必须存在;不要包含用户名密码、查询参数或 URL 片段。
- 目录前缀默认为 `serviceport/`,允许为空或相对多级目录,不允许绝对路径、`..`、反斜杠或连续斜杠。上传时通过 MKCOL 创建前缀下需要的子目录,不创建基础目录。
- 目录前缀默认为 `linkport/`,允许为空或相对多级目录,不允许绝对路径、`..`、反斜杠或连续斜杠。上传时通过 MKCOL 创建前缀下需要的子目录,不创建基础目录。
- 支持 Basic 用户名/密码(包括服务商应用密码);匿名目录将用户名和密码均留空。建议 HTTPS;HTTP 的登录信息可能被窃听。不支持自动跳转、Digest、OAuth 或自签名证书跳过校验。
- 编辑已保存设置时,登录密码留空且地址、用户名均未改变才保留旧密码;更改地址或用户名后需要重新填写。勾选清除密码可显式移除旧密码。
- **加密口令不是 WebDAV 登录密码**。口令为 12–1024 字节;所有同步设备使用相同口令。保存口令留空保留原值;手动操作可填写仅本次使用的口令,自动同步必须保存口令。更换口令应使用新的前缀,并在所有设备重新配置。
+37
View File
@@ -0,0 +1,37 @@
# 工作区子页面
## 页面行为
需要持续阅读、填写或确认的内容统一在工作区子页面中打开,不再使用带遮罩的居中弹框或侧边抽屉。子页面保留左侧导航和顶部工作区栏,正文使用稳定宽度、独立滚动区、固定标题与底部操作区;手机端沿用 52px 图标侧栏,正文占满剩余宽度。
标题左侧的返回按钮关闭当前子页面。`Esc` 只关闭最上层子页面;存在未保存内容时先打开“未保存的更改”确认页,选择继续编辑后保留草稿。嵌套流程按打开顺序叠放,例如从编辑页新建资产、查看影响范围后确认删除、从历史差异页确认恢复。关闭后焦点返回原入口。
确认和输入请求按队列逐页处理,当前页完成关闭后才显示下一页,重复点击不会操作下一项。确认页未处理完时忽略新的侧栏导航意图;退出登录或页面宿主卸载会取消尚未完成的请求。保存、导出或连接测试进行中时,相关编辑页禁止返回和取消,避免迟到结果写入已关闭的草稿。
以下交互均使用子页面:
- 目标、网关、文件资产和连接资产的新建或编辑;
- 连接示例、启动检查、连接测试、命令详情、版本信息和账号密码;
- 资源搜索、菜单搜索、完整资源树、项目与环境维护、表格列设置;
- 配置导出、导入预览、历史差异、模板内容和 Token 密钥;
- 目标切换、测试账号选择、资产影响范围和所属项目;
- 删除、停止、断开、同步冲突、SSH 指纹、未保存更改及密码输入等确认流程。
选择器下拉、操作菜单和悬停提示仍属于控件本身,不作为独立页面。成功、失败和进度消息仍使用短暂通知,不占用工作区导航层级。浏览器刷新或关闭时的未保存提醒继续使用浏览器原生机制。
## 安全与兼容
页面迁移不改变 API、请求顺序、权限判断、校验、资源 ID、数据库结构或配置文件格式,不需要数据迁移。秘密查看仍按原定时器自动清理,关闭密码或 Token 页面立即清空内存值;确认页不持久化输入。运行中的网关、引用关系、历史恢复和同步冲突保护保持不变。
本次沿用应用现有的内存工作区导航,没有引入 Vue Router 或可持久化的页面历史。刷新浏览器仍回到应用原有默认工作区,未保存草稿不会写入地址或本地存储。
## 验证
- `vue-tsc --noEmit` 通过。
- `node tests/page-dialogs.test.mjs` 的 2 项队列、输入校验和批量取消测试通过;`node tests/workspace-pages.test.mjs` 的源码禁止项检查通过。
- 静态检查确认 `web/src` 不再使用 `el-dialog`、`el-drawer`、`el-popover` 或 `ElMessageBox` 实现独立弹层。
- `npm.cmd --prefix web run build` 的类型检查阶段通过;Vite 在当前 Windows 沙箱启动 esbuild 时被 `spawn EPERM` 阻止,因此未生成新的前端产物。
- Vite 开发服务器同样在加载配置时被 `spawn EPERM` 阻止,本轮没有可用的本地预览 URL。
- Go 1.25.0、工作区内缓存/临时目录及串行编译下,`go vet ./...` 通过。`go test -p=1 ./... -count=1 -timeout=120s` 的其余包通过,但仍有 6 项既有 Windows 文件权限或 SQLite 快照环境失败,分布于 `cmd/linkport`、`internal/api`、`internal/desktop`、`internal/security` 和 `internal/store`;全量测试不记为通过。
- Playwright 在工作区临时目录下成功发现 56 个文件、125 项用例。因无法生成新前端和新嵌入式可执行文件,未运行浏览器套件,也未把发现测试或旧程序结果作为本次验收。
- 变更只涉及前端与文档,没有部署迁移、停止本地服务或修改 `data`。
+5 -5
View File
@@ -41,10 +41,10 @@
| 键 | 内容 |
| --- | --- |
| `serviceport.recent.v1` | 最近资源的类型、ID、时间 |
| `serviceport.tree.v1` | 项目/环境/类型分支的展开键 |
| `serviceport.sidebar-width` | 桌面侧栏像素宽度 |
| `serviceport.columns.v1.<表格 ID>` | 列 ID、显隐、顺序、宽度 |
| `linkport.recent.v1` | 最近资源的类型、ID、时间 |
| `linkport.tree.v1` | 项目/环境/类型分支的展开键 |
| `linkport.sidebar-width` | 桌面侧栏像素宽度 |
| `linkport.columns.v1.<表格 ID>` | 列 ID、显隐、顺序、宽度 |
浏览器禁用存储时功能仍可在当前会话使用;这些偏好不进入服务器备份或 WebDAV 同步。所有标签、原始 URL、未保存表单、解密内容和诊断结果不写入浏览器持久存储。不引入新运行依赖、不更换管理 API、不改变网关账号权限或协议安全策略。
@@ -54,4 +54,4 @@
`npm.cmd --prefix web run build` 在 Vite 启动 esbuild 时遭遇 `spawn EPERM`;Playwright 的 worker 同样无法启动。测试发现与单进程逻辑断言不等于浏览器用例通过。本轮未生成新前端产物/新嵌入二进制,未运行浏览器端到端回归或生成新截图;不能沿用旧二进制或历史截图作为本轮验收。
解除环境限制后,先构建前端,再用 `GOTOOLCHAIN=go1.25.0` 编译新程序;将 `SERVICEPORT_E2E_BINARY` 指向新程序,使用隔离目录、空闲端口运行 `npm.cmd --prefix web test`。纯逻辑用例也可在 `web` 中运行 `npx.cmd playwright test --config playwright.domain.config.ts`。不得并行替换 `web/dist` 与编译 Go 嵌入资源。
解除环境限制后,先构建前端,再用 `GOTOOLCHAIN=go1.25.0` 编译新程序;将 `LINKPORT_E2E_BINARY` 指向新程序,使用隔离目录、空闲端口运行 `npm.cmd --prefix web test`。纯逻辑用例也可在 `web` 中运行 `npx.cmd playwright test --config playwright.domain.config.ts`。不得并行替换 `web/dist` 与编译 Go 嵌入资源。
+1 -1
View File
@@ -1,4 +1,4 @@
module serviceport
module linkport
go 1.25.0
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"crypto/ed25519"
"crypto/rand"
"serviceport/internal/model"
"linkport/internal/model"
"testing"
"time"
+119
View File
@@ -0,0 +1,119 @@
package alerts
import (
"context"
"linkport/internal/model"
"linkport/internal/store"
"time"
)
type conditionSink func(store.Alert, bool) error
// conditionEvaluator owns rule state; persistence and notification stay with Engine.
// Engine serializes evaluations so authentication windows retain their baseline.
type conditionEvaluator struct {
source Source
samples map[string][]sample
}
type sample struct {
at time.Time
count int64
}
func (r *conditionEvaluator) evaluate(ctx context.Context, c store.AlertSettings, ps []model.Proxy, ts []model.Target, now time.Time, put conditionSink) error {
var err error
targets := map[string]bool{}
for _, t := range ts {
targets[t.ID] = true
}
_, health := r.source.HealthSnapshot()
for _, h := range health {
if !targets[h.TargetID] {
continue
}
a := store.Alert{Key: "target_unavailable:" + h.TargetID + ":" + h.AccountID, Kind: "target_unavailable", ResourceID: h.TargetID, AccountID: h.AccountID, Severity: "critical", Message: "Target health check failed"}
if err = put(a, !h.Healthy); err != nil {
return err
}
}
for _, p := range ps {
if err = ctx.Err(); err != nil {
return err
}
count := r.source.Metrics(p.ID).AuthenticationFailed
samples := r.samples[p.ID]
if len(samples) > 0 && count < samples[len(samples)-1].count {
samples = nil
}
samples = append(samples, sample{now, count})
cut := now.Add(-time.Duration(c.AuthFailureWindowSeconds) * time.Second)
// Keep the most recent baseline at or before the window boundary.
for len(samples) > 1 && !samples[1].at.After(cut) {
samples = samples[1:]
}
r.samples[p.ID] = samples
delta := count - samples[0].count
if err = put(store.Alert{Key: "auth_failure_spike:" + p.ID, Kind: "auth_failure_spike", ResourceID: p.ID, Severity: "warning", Message: "Gateway authentication failures exceeded the configured window threshold"}, delta >= int64(c.AuthFailureThreshold)); err != nil {
return err
}
for _, a := range p.GatewayAccounts {
if err = credential(put, "gateway", p.ID, a.ID, a.ExpiresAt, a.RotationReminderDays, a.Disabled, now); err != nil {
return err
}
}
}
for _, t := range ts {
for _, a := range t.Accounts {
if err = credential(put, "target", t.ID, a.ID, a.ExpiresAt, a.RotationReminderDays, false, now); err != nil {
return err
}
}
}
if certs, ok := r.source.(interface{ CertificateExpirations() map[string]time.Time }); ok {
for id, expiry := range certs.CertificateExpirations() {
a := store.Alert{Key: "certificate_expiry:" + id, Kind: "certificate_expiring", ResourceID: id, Severity: "warning", Message: "TLS certificate expires soon"}
if !expiry.After(now) {
a.Kind = "certificate_expired"
a.Severity = "critical"
a.Message = "TLS certificate has expired"
}
if err = put(a, !expiry.After(now.Add(time.Duration(c.CertificateReminderDays)*24*time.Hour))); err != nil {
return err
}
}
}
return nil
}
func evaluateAssetCertificates(assets []model.Asset, c store.AlertSettings, now time.Time, put conditionSink) error {
var err error
for _, asset := range assets {
summary := asset.Summary(now)
expiry, parseErr := time.Parse(time.RFC3339, summary.ExpiresAt)
if parseErr != nil {
continue
}
a := store.Alert{Key: "asset_certificate_expiry:" + asset.ID, Kind: "certificate_expiring", ResourceID: asset.ID, Severity: "warning", Message: "Asset certificate expires soon; upload a renewed certificate"}
if !expiry.After(now) {
a.Kind = "certificate_expired"
a.Severity = "critical"
a.Message = "Asset certificate has expired; upload a renewed certificate"
}
if err = put(a, !expiry.After(now.Add(time.Duration(c.CertificateReminderDays)*24*time.Hour))); err != nil {
return err
}
}
return nil
}
func credential(put func(store.Alert, bool) error, scope, id, account, expiry string, days int, disabled bool, now time.Time) error {
due, expired := model.CredentialDue(expiry, days, now)
a := store.Alert{Key: "credential:" + scope + ":" + id + ":" + account, Kind: "credential_expiring", ResourceID: id, AccountID: account, Severity: "warning", Message: "Account credential expires soon; rotate it before expiry"}
if expired {
a.Kind = "credential_expired"
a.Severity = "critical"
a.Message = "Account credential has expired"
}
return put(a, due && !disabled)
}
+65
View File
@@ -0,0 +1,65 @@
package alerts
import (
"context"
"linkport/internal/gateway"
"linkport/internal/model"
"linkport/internal/store"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
// The HTTP endpoints are local fixtures, not external notification services.
func TestCheckAndDeliveryKeepNotificationsSeparate(t *testing.T) {
s := fixtureStore(t)
var received, redirected atomic.Int64
var accept atomic.Bool
other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
redirected.Add(1)
w.WriteHeader(http.StatusNoContent)
}))
defer other.Close()
webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received.Add(1)
if accept.Load() {
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, other.URL, http.StatusFound)
}))
defer webhook.Close()
c := store.DefaultAlertSettings()
c.WebhookEnabled, c.WebhookURL = true, webhook.URL
if err := s.SaveAlertSettings(c); err != nil {
t.Fatal(err)
}
if err := s.SaveTarget(model.Target{ID: "target"}); err != nil {
t.Fatal(err)
}
e := New(s, &fixtureSource{health: []gateway.HealthResult{{TargetID: "target", Healthy: false}}})
if err := e.Check(context.Background()); err != nil {
t.Fatal(err)
}
if received.Load() != 0 {
t.Fatal("checking conditions contacted the webhook")
}
if err := e.Deliver(context.Background()); err != nil {
t.Fatal(err)
}
items, err := s.Alerts()
if err != nil || len(items) != 1 || items[0].DeliveryStatus != "pending" || items[0].DeliveryAttempts != 1 || received.Load() != 1 || redirected.Load() != 0 {
t.Fatalf("redirect must remain a failed attempt: items=%+v received=%d redirected=%d err=%v", items, received.Load(), redirected.Load(), err)
}
accept.Store(true)
for range 2 {
if err := e.Deliver(context.Background()); err != nil {
t.Fatal(err)
}
}
items, err = s.Alerts()
if err != nil || len(items) != 1 || items[0].DeliveryStatus != "sent" || items[0].DeliveryAttempts != 2 || received.Load() != 2 {
t.Fatalf("successful transition was not terminal: items=%+v received=%d err=%v", items, received.Load(), err)
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ package alerts
import (
"context"
"serviceport/internal/gateway"
"serviceport/internal/store"
"linkport/internal/gateway"
"linkport/internal/store"
"sync"
"time"
)
+5 -5
View File
@@ -8,13 +8,13 @@ import (
"encoding/base64"
"encoding/hex"
"io"
"linkport/internal/gateway"
"linkport/internal/model"
"linkport/internal/security"
"linkport/internal/store"
"net/http"
"net/http/httptest"
"path/filepath"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/security"
"serviceport/internal/store"
"testing"
"time"
)
@@ -114,7 +114,7 @@ func TestWebhookSignatureRetriesAndDisabledDelivery(t *testing.T) {
}
mac := hmac.New(sha256.New, []byte("fixture-signing-key"))
mac.Write(body)
if r.Header.Get("X-Serviceport-Signature") != "sha256="+hex.EncodeToString(mac.Sum(nil)) {
if r.Header.Get("X-Linkport-Signature") != "sha256="+hex.EncodeToString(mac.Sum(nil)) {
t.Error("invalid webhook signature")
}
w.WriteHeader(http.StatusServiceUnavailable)
+50
View File
@@ -0,0 +1,50 @@
package alerts
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"linkport/internal/store"
"net/http"
"time"
)
// A transport failure consumes an attempt; request construction errors abort
// delivery without changing its state. The engine owns that retry policy.
type alertNotifier interface {
Send(context.Context, store.AlertSettings, store.Alert) (bool, error)
}
type webhookNotifier struct {
client *http.Client
}
func newWebhookNotifier() *webhookNotifier {
return &webhookNotifier{client: &http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}}
}
func (n *webhookNotifier) Send(ctx context.Context, c store.AlertSettings, a store.Alert) (bool, error) {
body, _ := json.Marshal(a)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.WebhookURL, bytes.NewReader(body))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
if c.WebhookSecret != "" {
mac := hmac.New(sha256.New, []byte(c.WebhookSecret))
mac.Write(body)
req.Header.Set("X-Linkport-Signature", "sha256="+hex.EncodeToString(mac.Sum(nil)))
}
resp, err := n.client.Do(req)
if err != nil {
return false, nil
}
resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300, nil
}
+22
View File
@@ -0,0 +1,22 @@
package alerts
import (
"linkport/internal/model"
"linkport/internal/store"
"time"
)
// Repository is the persistence boundary required by alert evaluation and delivery.
// Store retains responsibility for transition deduplication and conditional updates.
type Repository interface {
AlertSettings() (store.AlertSettings, error)
List() ([]model.Proxy, error)
Targets() ([]model.Target, error)
Assets() ([]model.Asset, error)
Alerts() ([]store.Alert, error)
ReconcileAlert(store.Alert, bool, bool, time.Time) error
PruneAlerts(time.Time) error
AlertDelivery(int64, string, string) error
}
var _ Repository = (*store.Store)(nil)
@@ -1,7 +1,7 @@
package api
import (
"serviceport/internal/model"
"linkport/internal/model"
"testing"
)
+1 -1
View File
@@ -1,8 +1,8 @@
package api
import (
"linkport/internal/store"
"net/http"
"serviceport/internal/store"
)
// ImpactReference deliberately contains no configuration or credential material.
+1 -1
View File
@@ -2,8 +2,8 @@ package api
import (
"encoding/json"
"linkport/internal/model"
"net/http/httptest"
"serviceport/internal/model"
"strings"
"testing"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"net/http"
"strconv"
"serviceport/internal/outbound"
"linkport/internal/outbound"
)
func (s *Server) assetKubernetes(w http.ResponseWriter, r *http.Request, id, action string) {
@@ -0,0 +1,64 @@
package api
import (
"fmt"
"linkport/internal/model"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestKubernetesPreflightEndpointRedactsSecretsAndReportsChecks(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" || r.Header.Get("Authorization") != "Bearer preflight-private-token" {
t.Fatalf("unexpected preflight request: %s %s", r.Method, r.URL)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","status":{"allowed":true}}`)
}))
defer server.Close()
f := newFixture(t)
f.login()
content := fmt.Sprintf(`apiVersion: v1
kind: Config
current-context: test
clusters:
- name: cluster
cluster:
server: %s
insecure-skip-tls-verify: true
contexts:
- name: test
context:
cluster: cluster
user: user
namespace: application
users:
- name: user
user:
token: preflight-private-token
`, server.URL)
if err := f.store.SaveAsset(model.Asset{ID: "preflight-kube", Name: "preflight", Type: "kubeconfig", Content: content}); err != nil {
t.Fatal(err)
}
path := "/api/v1/assets/preflight-kube/kubernetes-preflight?" + url.Values{
"context": {"test"}, "namespace": {"application"}, "resource": {"deployment/backend"}, "port": {"8080"},
}.Encode()
response := f.request("GET", path, nil, true)
requireStatus(t, response, 200)
if !strings.Contains(response.Body.String(), `"status":"ready"`) || !strings.Contains(response.Body.String(), `"subresource":"portforward"`) || strings.Contains(response.Body.String(), "preflight-private-token") {
t.Fatalf("unexpected or unsafe preflight response: %s", response.Body.String())
}
invalidPort := f.request("GET", "/api/v1/assets/preflight-kube/kubernetes-preflight?resource=pod%2Fbackend&port=invalid", nil, true)
requireStatus(t, invalidPort, 422)
if !strings.Contains(invalidPort.Body.String(), `"code":"invalid_port"`) {
t.Fatalf("invalid port lost stable code: %s", invalidPort.Body.String())
}
if err := f.store.SaveAsset(model.Asset{ID: "preflight-file", Name: "plain", Type: "file", Content: "not a kubeconfig"}); err != nil {
t.Fatal(err)
}
requireStatus(t, f.request("GET", "/api/v1/assets/preflight-file/kubernetes-preflight?resource=pod%2Fbackend&port=8080", nil, true), 400)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package api
import (
"encoding/json"
"serviceport/internal/model"
"linkport/internal/model"
"strings"
"testing"
)
+2 -2
View File
@@ -2,9 +2,9 @@ package api
import (
"errors"
"linkport/internal/model"
"linkport/internal/security"
"net/http"
"serviceport/internal/model"
"serviceport/internal/security"
"strings"
"time"
)
+4 -4
View File
@@ -2,7 +2,7 @@ package api
import (
"encoding/json"
"serviceport/internal/model"
"linkport/internal/model"
"strings"
"testing"
)
@@ -46,9 +46,9 @@ func TestKubeconfigAssetLifecycleAndReferences(t *testing.T) {
if err != nil || stored.Content != assetKubeconfigFixture {
t.Fatal("rename discarded file", err)
}
externalToken := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " tokenFile: /serviceport-must-not-read/token", 1)
execPlugin := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " exec:\n apiVersion: client.authentication.k8s.io/v1\n command: serviceport-must-never-execute\n interactiveMode: Never", 1)
authProvider := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " auth-provider:\n name: serviceport-must-never-load", 1)
externalToken := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " tokenFile: /linkport-must-not-read/token", 1)
execPlugin := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " exec:\n apiVersion: client.authentication.k8s.io/v1\n command: linkport-must-never-execute\n interactiveMode: Never", 1)
authProvider := strings.Replace(assetKubeconfigFixture, " token: asset-private-token", " auth-provider:\n name: linkport-must-never-load", 1)
invalid := []struct{ content, code string }{
{"", ""}, {"not: [yaml", ""}, {strings.Repeat("x", model.MaxKubeConfigContentBytes+1), ""},
{externalToken, "external_file_reference_unsupported"}, {execPlugin, "login_plugin_unsupported"}, {authProvider, "login_plugin_unsupported"},
+4 -4
View File
@@ -1,9 +1,9 @@
package api
import (
"linkport/internal/model"
"linkport/internal/security"
"net/http"
"serviceport/internal/model"
"serviceport/internal/security"
"time"
)
@@ -11,7 +11,7 @@ func (s *Server) logout(w http.ResponseWriter, sessionID string) {
s.mu.Lock()
delete(s.sessions, sessionID)
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{Name: "serviceport_session", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode})
http.SetCookie(w, &http.Cookie{Name: "linkport_session", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode})
respond(w, 200, map[string]bool{"ok": true})
}
@@ -60,7 +60,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
}
s.sessions[token] = sess
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{Name: "serviceport_session", Value: token, Path: "/", HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode, MaxAge: 43200})
http.SetCookie(w, &http.Cookie{Name: "linkport_session", Value: token, Path: "/", HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode, MaxAge: 43200})
respond(w, 200, map[string]string{"username": sess.User, "csrfToken": sess.CSRF})
}
+75
View File
@@ -0,0 +1,75 @@
package api
import (
"net"
"net/http"
"strings"
"time"
)
// requestIdentity belongs to one authorized request, including its confirmation binding.
type requestIdentity struct {
session session
sessionID string
}
// authorize adapts bearer, local-development and cookie authentication to one
// request identity. CSRF validation remains after authentication and scope checks.
func (s *Server) authorize(w http.ResponseWriter, r *http.Request, path string) (http.ResponseWriter, requestIdentity, bool) {
var sess session
cookie := &http.Cookie{Value: "development"}
bearer := r.Header.Get("Authorization") != ""
if bearer {
value := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
credential, err := s.store.VerifyToken(value)
if err != nil {
fail(w, 401, "invalid or expired API token")
return w, requestIdentity{}, false
}
if !tokenPermits(credential, path, r.Method) {
fail(w, 403, "token scope does not permit this operation")
return w, requestIdentity{}, false
}
sess = session{User: credential.Owner, Revision: credential.Revision}
cookie.Value = "token:" + credential.ID
w = tokenResponseWriter{w}
} else if !s.authEnabled {
ip := net.ParseIP(source(r))
host := r.Host
if h, _, e := net.SplitHostPort(host); e == nil {
host = h
}
hostIP := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() || (host != "localhost" && (hostIP == nil || !hostIP.IsLoopback())) {
fail(w, 403, "development mode is local only")
return w, requestIdentity{}, false
}
sess = session{User: "开发者", CSRF: s.devCSRF}
} else {
var err error
cookie, err = r.Cookie("linkport_session")
if err != nil {
fail(w, 401, "login required")
return w, requestIdentity{}, false
}
s.mu.Lock()
var ok bool
sess, ok = s.sessions[cookie.Value]
if time.Now().After(sess.Expires) {
delete(s.sessions, cookie.Value)
ok = false
}
s.mu.Unlock()
rev, err := s.store.PrimaryRevision(sess.User)
if !ok || err != nil || rev != sess.Revision {
fail(w, 401, "session expired")
return w, requestIdentity{}, false
}
}
if !bearer && r.Method != "GET" && r.Header.Get("X-CSRF-Token") != sess.CSRF {
fail(w, 403, "CSRF token required")
return w, requestIdentity{}, false
}
return w, requestIdentity{session: sess, sessionID: cookie.Value}, true
}
+6 -6
View File
@@ -3,9 +3,9 @@ package api
import (
"bytes"
"encoding/json"
"linkport/internal/model"
"linkport/internal/store"
"net/http/httptest"
"serviceport/internal/model"
"serviceport/internal/store"
"strings"
"testing"
"time"
@@ -25,13 +25,13 @@ func TestAPITokenScopesMetricsAndRevocation(t *testing.T) {
if json.Unmarshal(w.Body.Bytes(), &created) != nil || created.Secret == "" {
t.Fatal("token secret missing")
}
req := httptest.NewRequest("GET", "https://serviceport.test/api/v1/metrics", bytes.NewReader(nil))
req := httptest.NewRequest("GET", "https://linkport.test/api/v1/metrics", bytes.NewReader(nil))
req.Header.Set("Authorization", "Bearer "+created.Secret)
req.RemoteAddr = "127.0.0.1:12345"
rw := httptest.NewRecorder()
f.handler.ServeHTTP(rw, req)
requireStatus(t, rw, 200)
if !strings.Contains(rw.Body.String(), "serviceport_") {
if !strings.Contains(rw.Body.String(), "linkport_") {
t.Fatal("metrics output missing")
}
if err := f.store.RevokeToken(created.Token.ID); err != nil {
@@ -53,7 +53,7 @@ func TestAutomationScopesAndCredentialIsolation(t *testing.T) {
t.Fatal(err)
}
call := func(secret, method, path string) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, "https://serviceport.test/api/v1/"+path, nil)
r := httptest.NewRequest(method, "https://linkport.test/api/v1/"+path, nil)
r.Header.Set("Authorization", "Bearer "+secret)
w := httptest.NewRecorder()
f.handler.ServeHTTP(w, r)
@@ -104,7 +104,7 @@ func TestTokenWriteResponseHidesRetainedCredentials(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest("PUT", "https://serviceport.test/api/v1/outbounds/kube-write", strings.NewReader(`{"name":"after"}`))
req := httptest.NewRequest("PUT", "https://linkport.test/api/v1/outbounds/kube-write", strings.NewReader(`{"name":"after"}`))
req.Header.Set("Authorization", "Bearer "+secret)
w := httptest.NewRecorder()
f.handler.ServeHTTP(w, req)
+2 -2
View File
@@ -20,10 +20,10 @@ func (s *Server) commandOutput(w http.ResponseWriter, r *http.Request) {
started := false
err := s.store.DownloadCommandOutput(r.Context(), id, stream, func(size int64) {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-command-output.bin")
w.Header().Set("Content-Disposition", "attachment; filename=linkport-command-output.bin")
if stream == "result" {
w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-mysql-result.ndjson")
w.Header().Set("Content-Disposition", "attachment; filename=linkport-mysql-result.ndjson")
}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
+4 -4
View File
@@ -2,9 +2,9 @@ package api
import (
"encoding/csv"
"linkport/internal/store"
"net/http"
"os"
"serviceport/internal/store"
"strconv"
"strings"
"time"
@@ -77,7 +77,7 @@ func csvCell(value string) string {
return value
}
func (s *Server) exportCommands(w http.ResponseWriter, r *http.Request, f store.CommandFilter) {
file, err := os.CreateTemp("", "serviceport-export-*.csv")
file, err := os.CreateTemp("", "linkport-export-*.csv")
if err != nil {
fail(w, 500, "cannot create export")
return
@@ -104,7 +104,7 @@ func (s *Server) exportCommands(w http.ResponseWriter, r *http.Request, f store.
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-commands.csv")
w.Header().Set("Content-Disposition", "attachment; filename=linkport-commands.csv")
w.Header().Set("Cache-Control", "no-store")
http.ServeContent(w, r, "serviceport-commands.csv", time.Time{}, file)
http.ServeContent(w, r, "linkport-commands.csv", time.Time{}, file)
}
+1 -1
View File
@@ -3,7 +3,7 @@ package api
import (
"encoding/csv"
"encoding/json"
"serviceport/internal/model"
"linkport/internal/model"
"strings"
"testing"
)
+2 -2
View File
@@ -2,8 +2,8 @@ package api
import (
"errors"
"linkport/internal/store"
"net/http"
"serviceport/internal/store"
"strconv"
"strings"
)
@@ -62,7 +62,7 @@ func (s *Server) configuration(w http.ResponseWriter, r *http.Request, path stri
return
}
s.store.Event("", source(r), "configuration_backup", "")
w.Header().Set("Content-Disposition", `attachment; filename="serviceport-backup.json"`)
w.Header().Set("Content-Disposition", `attachment; filename="linkport-backup.json"`)
respond(w, 200, b)
case "import":
if r.Method != "POST" {
@@ -2,8 +2,8 @@ package api
import (
"encoding/json"
"serviceport/internal/model"
"serviceport/internal/store"
"linkport/internal/model"
"linkport/internal/store"
"strings"
"testing"
)
@@ -23,7 +23,7 @@ func TestEncryptedConfigurationAssetRoundTrip(t *testing.T) {
requireStatus(t, f.request("POST", path, nil, false), 403)
w := f.request("POST", path, map[string]any{"passphrase": passphrase}, true)
requireStatus(t, w, 200)
if w.Header().Get("Cache-Control") != "no-store" || w.Header().Get("Content-Disposition") != `attachment; filename="serviceport-backup.json"` {
if w.Header().Get("Cache-Control") != "no-store" || w.Header().Get("Content-Disposition") != `attachment; filename="linkport-backup.json"` {
t.Fatal("encrypted export must be a non-cacheable attachment")
}
var backup store.EncryptedBackup
+2 -2
View File
@@ -3,8 +3,8 @@ package api
import (
"bytes"
"encoding/json"
"serviceport/internal/model"
"serviceport/internal/store"
"linkport/internal/model"
"linkport/internal/store"
"strings"
"testing"
)
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"strings"
"testing"
"serviceport/internal/gateway"
"serviceport/internal/model"
"linkport/internal/gateway"
"linkport/internal/model"
)
type stagedFailureProtocol struct {
+2 -2
View File
@@ -1,8 +1,8 @@
package api
import (
"serviceport/internal/gateway"
"serviceport/internal/model"
"linkport/internal/gateway"
"linkport/internal/model"
"testing"
)
+3 -3
View File
@@ -2,10 +2,10 @@ package api
import (
"encoding/json"
"linkport/internal/gateway"
"linkport/internal/model"
"linkport/internal/protocol/tcpproxy"
"net"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/protocol/tcpproxy"
"strings"
"testing"
)
+5 -5
View File
@@ -4,11 +4,11 @@ import (
"context"
"encoding/base64"
"io"
"linkport/internal/security"
"linkport/internal/store"
"net/http"
"os"
"path/filepath"
"serviceport/internal/security"
"serviceport/internal/store"
"time"
)
@@ -29,13 +29,13 @@ func (s *Server) databaseSnapshot(w http.ResponseWriter, r *http.Request, path s
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
directory, err := os.MkdirTemp("", "serviceport-snapshot-")
directory, err := os.MkdirTemp("", "linkport-snapshot-")
if err != nil {
fail(w, 500, "cannot prepare snapshot workspace")
return
}
defer os.RemoveAll(directory)
file := filepath.Join(directory, "serviceport.sqlite")
file := filepath.Join(directory, "linkport.sqlite")
var info store.DatabaseSnapshotInfo
if path == "database/snapshot" || path == "database/download" {
info, err = s.store.DatabaseSnapshot(ctx, file)
@@ -51,7 +51,7 @@ func (s *Server) databaseSnapshot(w http.ResponseWriter, r *http.Request, path s
}
defer stream.Close()
w.Header().Set("Content-Type", "application/vnd.sqlite3")
w.Header().Set("Content-Disposition", `attachment; filename="serviceport-snapshot.sqlite"`)
w.Header().Set("Content-Disposition", `attachment; filename="linkport-snapshot.sqlite"`)
w.Header().Set("X-Snapshot-SHA256", info.SHA256)
s.store.Event("", source(r), "database_snapshot", "")
io.Copy(w, stream)
+4 -4
View File
@@ -4,12 +4,12 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"linkport/internal/gateway"
"linkport/internal/model"
"linkport/internal/store"
"net/http"
"net/http/httptest"
"path/filepath"
"serviceport/internal/gateway"
"serviceport/internal/model"
"serviceport/internal/store"
"testing"
"time"
)
@@ -58,7 +58,7 @@ func TestDatabaseSnapshotSessionAndPreview(t *testing.T) {
t.Fatal(err)
}
defer s.RevokeToken(token.ID)
req := httptest.NewRequest("POST", "https://serviceport.test/api/v1/database/download", nil)
req := httptest.NewRequest("POST", "https://linkport.test/api/v1/database/download", nil)
req.Header.Set("Authorization", "Bearer "+secret)
w := httptest.NewRecorder()
f.handler.ServeHTTP(w, req)
+2 -2
View File
@@ -1,9 +1,9 @@
package api
import (
"linkport/internal/cryptotool"
"linkport/internal/navicat"
"net/http"
"serviceport/internal/cryptotool"
"serviceport/internal/navicat"
)
func (s *Server) decryptNavicat(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -2,8 +2,8 @@ package api
import (
"encoding/json"
"linkport/internal/cryptotool"
"net/http/httptest"
"serviceport/internal/cryptotool"
"strings"
"testing"
)
+4 -4
View File
@@ -2,9 +2,9 @@ package api
import (
"encoding/csv"
"linkport/internal/store"
"net/http"
"os"
"serviceport/internal/store"
"strconv"
"time"
)
@@ -46,7 +46,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
}
}
if q.Get("export") == "csv" {
file, err := os.CreateTemp("", "serviceport-events-*.csv")
file, err := os.CreateTemp("", "linkport-events-*.csv")
if err != nil {
fail(w, 500, "cannot create export")
return
@@ -73,9 +73,9 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=serviceport-events.csv")
w.Header().Set("Content-Disposition", "attachment; filename=linkport-events.csv")
w.Header().Set("Cache-Control", "no-store")
http.ServeContent(w, r, "serviceport-events.csv", time.Time{}, file)
http.ServeContent(w, r, "linkport-events.csv", time.Time{}, file)
return
}
records, err := s.store.FilterEvents(r.Context(), f, before)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"crypto/rand"
"encoding/json"
"encoding/pem"
"serviceport/internal/model"
"linkport/internal/model"
"strings"
"testing"
"time"
+1 -1
View File
@@ -2,7 +2,7 @@ package api
import (
"encoding/json"
"serviceport/internal/model"
"linkport/internal/model"
"testing"
)

Some files were not shown because too many files have changed in this diff Show More