mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对 - agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式 - agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch - webui: server 输出通道适配器(保留 reasoning_content/disable_thinking) - GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
390 lines
10 KiB
Go
390 lines
10 KiB
Go
//go:build windows
|
||
// +build windows
|
||
|
||
package ptywin
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"unicode/utf16"
|
||
"unsafe"
|
||
|
||
"golang.org/x/sys/windows"
|
||
)
|
||
|
||
var (
|
||
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||
fCreatePseudoConsole = modKernel32.NewProc("CreatePseudoConsole")
|
||
fResizePseudoConsole = modKernel32.NewProc("ResizePseudoConsole")
|
||
fClosePseudoConsole = modKernel32.NewProc("ClosePseudoConsole")
|
||
fInitializeProcThreadAttributeList = modKernel32.NewProc("InitializeProcThreadAttributeList")
|
||
fUpdateProcThreadAttribute = modKernel32.NewProc("UpdateProcThreadAttribute")
|
||
ErrConPtyUnsupported = errors.New("ConPty is not available on this version of Windows")
|
||
)
|
||
|
||
func IsConPtyAvailable() bool {
|
||
return fCreatePseudoConsole.Find() == nil &&
|
||
fResizePseudoConsole.Find() == nil &&
|
||
fClosePseudoConsole.Find() == nil &&
|
||
fInitializeProcThreadAttributeList.Find() == nil &&
|
||
fUpdateProcThreadAttribute.Find() == nil
|
||
}
|
||
|
||
const (
|
||
_STILL_ACTIVE uint32 = 259
|
||
_S_OK uintptr = 0
|
||
_PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE uintptr = 0x20016
|
||
defaultConsoleWidth = 80 // in characters
|
||
defaultConsoleHeight = 40 // in characters
|
||
)
|
||
|
||
type _COORD struct {
|
||
X, Y int16
|
||
}
|
||
|
||
func (c *_COORD) Pack() uintptr {
|
||
return uintptr((int32(c.Y) << 16) | int32(c.X))
|
||
}
|
||
|
||
type _HPCON windows.Handle
|
||
|
||
type handleIO struct {
|
||
handle windows.Handle
|
||
}
|
||
|
||
func (h *handleIO) Read(p []byte) (int, error) {
|
||
var numRead uint32 = 0
|
||
err := windows.ReadFile(h.handle, p, &numRead, nil)
|
||
return int(numRead), err
|
||
}
|
||
|
||
func (h *handleIO) Write(p []byte) (int, error) {
|
||
var numWritten uint32 = 0
|
||
err := windows.WriteFile(h.handle, p, &numWritten, nil)
|
||
return int(numWritten), err
|
||
}
|
||
|
||
func (h *handleIO) Close() error {
|
||
return windows.CloseHandle(h.handle)
|
||
}
|
||
|
||
type ConPty struct {
|
||
hpc _HPCON
|
||
pi *windows.ProcessInformation
|
||
ptyIn, ptyOut, cmdIn, cmdOut *handleIO
|
||
}
|
||
|
||
func win32ClosePseudoConsole(hPc _HPCON) {
|
||
if fClosePseudoConsole.Find() != nil {
|
||
return
|
||
}
|
||
// this kills the attached process. there is no return value.
|
||
fClosePseudoConsole.Call(uintptr(hPc))
|
||
}
|
||
|
||
func win32ResizePseudoConsole(hPc _HPCON, coord *_COORD) error {
|
||
if fResizePseudoConsole.Find() != nil {
|
||
return fmt.Errorf("ResizePseudoConsole not found")
|
||
}
|
||
ret, _, _ := fResizePseudoConsole.Call(uintptr(hPc), coord.Pack())
|
||
if ret != _S_OK {
|
||
return fmt.Errorf("ResizePseudoConsole failed with status 0x%x", ret)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func win32CreatePseudoConsole(c *_COORD, hIn, hOut windows.Handle) (_HPCON, error) {
|
||
if fCreatePseudoConsole.Find() != nil {
|
||
return 0, fmt.Errorf("CreatePseudoConsole not found")
|
||
}
|
||
var hPc _HPCON
|
||
ret, _, _ := fCreatePseudoConsole.Call(
|
||
c.Pack(),
|
||
uintptr(hIn),
|
||
uintptr(hOut),
|
||
0,
|
||
uintptr(unsafe.Pointer(&hPc)))
|
||
if ret != _S_OK {
|
||
return 0, fmt.Errorf("CreatePseudoConsole() failed with status 0x%x", ret)
|
||
}
|
||
return hPc, nil
|
||
}
|
||
|
||
type _StartupInfoEx struct {
|
||
startupInfo windows.StartupInfo
|
||
attributeList []byte
|
||
}
|
||
|
||
func getStartupInfoExForPTY(hpc _HPCON) (*_StartupInfoEx, error) {
|
||
if fInitializeProcThreadAttributeList.Find() != nil {
|
||
return nil, fmt.Errorf("InitializeProcThreadAttributeList not found")
|
||
}
|
||
if fUpdateProcThreadAttribute.Find() != nil {
|
||
return nil, fmt.Errorf("UpdateProcThreadAttribute not found")
|
||
}
|
||
var siEx _StartupInfoEx
|
||
siEx.startupInfo.Cb = uint32(unsafe.Sizeof(windows.StartupInfo{}) + unsafe.Sizeof(&siEx.attributeList[0]))
|
||
siEx.startupInfo.Flags |= windows.STARTF_USESTDHANDLES
|
||
var size uintptr
|
||
|
||
// first call is to get required size. this should return false.
|
||
ret, _, _ := fInitializeProcThreadAttributeList.Call(0, 1, 0, uintptr(unsafe.Pointer(&size)))
|
||
siEx.attributeList = make([]byte, size, size)
|
||
ret, _, err := fInitializeProcThreadAttributeList.Call(
|
||
uintptr(unsafe.Pointer(&siEx.attributeList[0])),
|
||
1,
|
||
0,
|
||
uintptr(unsafe.Pointer(&size)))
|
||
if ret != 1 {
|
||
return nil, fmt.Errorf("InitializeProcThreadAttributeList: %v", err)
|
||
}
|
||
|
||
ret, _, err = fUpdateProcThreadAttribute.Call(
|
||
uintptr(unsafe.Pointer(&siEx.attributeList[0])),
|
||
0,
|
||
_PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||
uintptr(hpc),
|
||
unsafe.Sizeof(hpc),
|
||
0,
|
||
0)
|
||
if ret != 1 {
|
||
return nil, fmt.Errorf("InitializeProcThreadAttributeList: %v", err)
|
||
}
|
||
return &siEx, nil
|
||
}
|
||
|
||
func createConsoleProcessAttachedToPTY(hpc _HPCON, commandLine, workDir string, env []string) (*windows.ProcessInformation, error) {
|
||
cmdLine, err := windows.UTF16PtrFromString(commandLine)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var currentDirectory *uint16
|
||
if workDir != "" {
|
||
currentDirectory, err = windows.UTF16PtrFromString(workDir)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
var envBlock *uint16
|
||
flags := uint32(windows.EXTENDED_STARTUPINFO_PRESENT)
|
||
if env != nil {
|
||
flags |= uint32(windows.CREATE_UNICODE_ENVIRONMENT)
|
||
envBlock = createEnvBlock(env)
|
||
}
|
||
siEx, err := getStartupInfoExForPTY(hpc)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var pi windows.ProcessInformation
|
||
err = windows.CreateProcess(
|
||
nil, // use this if no args
|
||
cmdLine,
|
||
nil,
|
||
nil,
|
||
false, // inheritHandle
|
||
flags,
|
||
envBlock,
|
||
currentDirectory,
|
||
&siEx.startupInfo,
|
||
&pi)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &pi, nil
|
||
}
|
||
|
||
// createEnvBlock refers to syscall.createEnvBlock in go/src/syscall/exec_windows.go
|
||
// Sourced From: https://github.com/creack/pty/pull/155
|
||
func createEnvBlock(envv []string) *uint16 {
|
||
if len(envv) == 0 {
|
||
return &utf16.Encode([]rune("\x00\x00"))[0]
|
||
}
|
||
length := 0
|
||
for _, s := range envv {
|
||
length += len(s) + 1
|
||
}
|
||
length += 1
|
||
|
||
b := make([]byte, length)
|
||
i := 0
|
||
for _, s := range envv {
|
||
l := len(s)
|
||
copy(b[i:i+l], []byte(s))
|
||
copy(b[i+l:i+l+1], []byte{0})
|
||
i = i + l + 1
|
||
}
|
||
copy(b[i:i+1], []byte{0})
|
||
|
||
return &utf16.Encode([]rune(string(b)))[0]
|
||
}
|
||
|
||
// This will only return the first error.
|
||
func closeHandles(handles ...windows.Handle) error {
|
||
var err error
|
||
for _, h := range handles {
|
||
if h != windows.InvalidHandle {
|
||
if err == nil {
|
||
err = windows.CloseHandle(h)
|
||
} else {
|
||
windows.CloseHandle(h)
|
||
}
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
// Close all open handles and terminate the process.
|
||
func (cpty *ConPty) Close() error {
|
||
// there is no return code
|
||
win32ClosePseudoConsole(cpty.hpc)
|
||
return closeHandles(
|
||
cpty.pi.Process,
|
||
cpty.pi.Thread,
|
||
cpty.ptyIn.handle,
|
||
cpty.ptyOut.handle,
|
||
cpty.cmdIn.handle,
|
||
cpty.cmdOut.handle)
|
||
}
|
||
|
||
// Wait for the process to exit and return the exit code. If context is canceled,
|
||
// Wait() will return STILL_ACTIVE and an error indicating the context was canceled.
|
||
func (cpty *ConPty) Wait(ctx context.Context) (uint32, error) {
|
||
var exitCode uint32 = _STILL_ACTIVE
|
||
for {
|
||
if err := ctx.Err(); err != nil {
|
||
return _STILL_ACTIVE, fmt.Errorf("wait canceled: %v", err)
|
||
}
|
||
ret, _ := windows.WaitForSingleObject(cpty.pi.Process, 1000)
|
||
if ret != uint32(windows.WAIT_TIMEOUT) {
|
||
err := windows.GetExitCodeProcess(cpty.pi.Process, &exitCode)
|
||
return exitCode, err
|
||
}
|
||
}
|
||
}
|
||
|
||
func (cpty *ConPty) Resize(width, height int) error {
|
||
coords := _COORD{
|
||
int16(width),
|
||
int16(height),
|
||
}
|
||
|
||
return win32ResizePseudoConsole(cpty.hpc, &coords)
|
||
}
|
||
|
||
func (cpty *ConPty) Read(p []byte) (int, error) {
|
||
return cpty.cmdOut.Read(p)
|
||
}
|
||
|
||
func (cpty *ConPty) Write(p []byte) (int, error) {
|
||
return cpty.cmdIn.Write(p)
|
||
}
|
||
|
||
func (cpty *ConPty) Pid() int {
|
||
return int(cpty.pi.ProcessId)
|
||
}
|
||
|
||
// Running 报告子进程是否仍在运行(GetExitCodeProcess == STILL_ACTIVE)。
|
||
func (cpty *ConPty) Running() bool {
|
||
if cpty.pi == nil || cpty.pi.Process == 0 {
|
||
return false
|
||
}
|
||
var code uint32
|
||
err := windows.GetExitCodeProcess(cpty.pi.Process, &code)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return code == _STILL_ACTIVE
|
||
}
|
||
|
||
// Kill 强制终止子进程并关闭进程/线程句柄。
|
||
func (cpty *ConPty) Kill() error {
|
||
if cpty.pi == nil || cpty.pi.Process == 0 {
|
||
return nil
|
||
}
|
||
if err := windows.TerminateProcess(cpty.pi.Process, 1); err != nil {
|
||
return err
|
||
}
|
||
windows.CloseHandle(cpty.pi.Process)
|
||
windows.CloseHandle(cpty.pi.Thread)
|
||
cpty.pi.Process = 0
|
||
cpty.pi.Thread = 0
|
||
return nil
|
||
}
|
||
|
||
type conPtyArgs struct {
|
||
coords _COORD
|
||
workDir string
|
||
env []string
|
||
}
|
||
|
||
type ConPtyOption func(args *conPtyArgs)
|
||
|
||
func ConPtyDimensions(width, height int) ConPtyOption {
|
||
return func(args *conPtyArgs) {
|
||
args.coords.X = int16(width)
|
||
args.coords.Y = int16(height)
|
||
}
|
||
}
|
||
|
||
func ConPtyWorkDir(workDir string) ConPtyOption {
|
||
return func(args *conPtyArgs) {
|
||
args.workDir = workDir
|
||
}
|
||
}
|
||
|
||
func ConPtyEnv(env []string) ConPtyOption {
|
||
return func(args *conPtyArgs) {
|
||
args.env = env
|
||
}
|
||
}
|
||
|
||
// Start a new process specified in `commandLine` and attach a pseudo console using the Windows
|
||
// ConPty API. If ConPty is not available, ErrConPtyUnsupported will be returned.
|
||
//
|
||
// On successful return, an instance of ConPty is returned. You must call Close() on this to release
|
||
// any resources associated with the process. To get the exit code of the process, you can call Wait().
|
||
func Start(commandLine string, options ...ConPtyOption) (*ConPty, error) {
|
||
if !IsConPtyAvailable() {
|
||
return nil, ErrConPtyUnsupported
|
||
}
|
||
args := &conPtyArgs{
|
||
coords: _COORD{defaultConsoleWidth, defaultConsoleHeight},
|
||
}
|
||
for _, opt := range options {
|
||
opt(args)
|
||
}
|
||
|
||
var cmdIn, cmdOut, ptyIn, ptyOut windows.Handle
|
||
if err := windows.CreatePipe(&ptyIn, &cmdIn, nil, 0); err != nil {
|
||
return nil, fmt.Errorf("CreatePipe: %v", err)
|
||
}
|
||
if err := windows.CreatePipe(&cmdOut, &ptyOut, nil, 0); err != nil {
|
||
closeHandles(ptyIn, cmdIn)
|
||
return nil, fmt.Errorf("CreatePipe: %v", err)
|
||
}
|
||
|
||
hPc, err := win32CreatePseudoConsole(&args.coords, ptyIn, ptyOut)
|
||
if err != nil {
|
||
closeHandles(ptyIn, ptyOut, cmdIn, cmdOut)
|
||
return nil, err
|
||
}
|
||
|
||
pi, err := createConsoleProcessAttachedToPTY(hPc, commandLine, args.workDir, args.env)
|
||
if err != nil {
|
||
closeHandles(ptyIn, ptyOut, cmdIn, cmdOut)
|
||
win32ClosePseudoConsole(hPc)
|
||
return nil, fmt.Errorf("Failed to create console process: %v", err)
|
||
}
|
||
|
||
cpty := &ConPty{
|
||
hpc: hPc,
|
||
pi: pi,
|
||
ptyIn: &handleIO{ptyIn},
|
||
ptyOut: &handleIO{ptyOut},
|
||
cmdIn: &handleIO{cmdIn},
|
||
cmdOut: &handleIO{cmdOut},
|
||
}
|
||
return cpty, nil
|
||
}
|