Files
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 01:50:36 +08:00

146 lines
3.5 KiB
Go

package display
import (
"bytes"
"encoding/base64"
"encoding/binary"
"image"
"image/draw"
"image/png"
)
// Bitmap 表示协议层解码后的 BGRA/RGB565 小块,display 只负责合成和输出 // Logan.Li
type Bitmap struct {
X int
Y int
Width int
Height int
BitsPerPixel int
Data []byte
}
// Frame 表示一次合成后的脏区域输出 // Logan.Li
type Frame struct {
X int
Y int
Width int
Height int
Image string
RGBA []byte
}
// Framebuffer 借鉴 Guacamole/bring 的 display/layer 思路,在后端维护完整画布 // Logan.Li
type Framebuffer struct {
width int
height int
img *image.RGBA
dirty image.Rectangle
}
// NewFramebuffer 创建固定尺寸远端画布,所有 RDP 更新先合成到这里 // Logan.Li
func NewFramebuffer(width, height int) *Framebuffer {
if width <= 0 {
width = 1280
}
if height <= 0 {
height = 800
}
return &Framebuffer{
width: width,
height: height,
img: image.NewRGBA(image.Rect(0, 0, width, height)),
}
}
// Draw 合成一个 bitmap 小块,并扩大脏矩形 // Logan.Li
func (f *Framebuffer) Draw(b Bitmap) {
if b.Width <= 0 || b.Height <= 0 || len(b.Data) == 0 {
return
}
dstRect := image.Rect(b.X, b.Y, b.X+b.Width, b.Y+b.Height).Intersect(f.img.Bounds())
if dstRect.Empty() {
return
}
src := bitmapToRGBA(b)
srcPoint := image.Pt(dstRect.Min.X-b.X, dstRect.Min.Y-b.Y)
draw.Draw(f.img, dstRect, src, srcPoint, draw.Src)
if f.dirty.Empty() {
f.dirty = dstRect
} else {
f.dirty = f.dirty.Union(dstRect)
}
}
// Flush 输出当前脏区域 PNG,并清空脏矩形 // Logan.Li
func (f *Framebuffer) Flush() (Frame, bool) {
frame, ok := f.FlushRGBA()
if !ok {
return Frame{}, false
}
img := image.NewRGBA(image.Rect(0, 0, frame.Width, frame.Height))
copy(img.Pix, frame.RGBA)
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return Frame{}, false
}
frame.Image = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
return frame, true
}
// FlushRGBA 输出当前脏区域原始 RGBA,避免实时 RDP 路径重复 PNG/base64 编解码。 // Logan.Li
func (f *Framebuffer) FlushRGBA() (Frame, bool) {
if f.dirty.Empty() {
return Frame{}, false
}
rect := f.dirty.Intersect(f.img.Bounds())
f.dirty = image.Rectangle{}
if rect.Empty() {
return Frame{}, false
}
crop := image.NewRGBA(image.Rect(0, 0, rect.Dx(), rect.Dy()))
draw.Draw(crop, crop.Bounds(), f.img, rect.Min, draw.Src)
return Frame{
X: rect.Min.X,
Y: rect.Min.Y,
Width: rect.Dx(),
Height: rect.Dy(),
RGBA: append([]byte(nil), crop.Pix...),
}, true
}
func bitmapToRGBA(b Bitmap) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, b.Width, b.Height))
pix := img.Pix
switch b.BitsPerPixel {
case 1, 2:
n := b.Width * b.Height
for i := 0; i < n && i*2+1 < len(b.Data); i++ {
// grdp 的 RLE 输出 16bpp 使用大端 RGB565,按小端会导致蓝紫/绿色偏色。 // Logan.Li
c := binary.BigEndian.Uint16(b.Data[i*2 : i*2+2])
r := uint8((c >> 11) & 0x1F)
g := uint8((c >> 5) & 0x3F)
bl := uint8(c & 0x1F)
off := i * 4
pix[off] = (r << 3) | (r >> 2)
pix[off+1] = (g << 2) | (g >> 4)
pix[off+2] = (bl << 3) | (bl >> 2)
pix[off+3] = 0xFF
}
default:
stride := b.BitsPerPixel
n := b.Width * b.Height
for i := 0; i < n; i++ {
s := i * stride
if s+2 >= len(b.Data) {
break
}
off := i * 4
pix[off] = b.Data[s+2]
pix[off+1] = b.Data[s+1]
pix[off+2] = b.Data[s]
pix[off+3] = 0xFF
}
}
return img
}