mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持
- 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 等打磨
This commit is contained in:
389
internal/ptywin/conpty.go
Normal file
389
internal/ptywin/conpty.go
Normal file
@ -0,0 +1,389 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user