2026-04-11 22:32:43 +08:00
|
|
|
package internal
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
|
|
"github.com/ergochat/readline"
|
|
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 错误定义
|
|
|
|
|
var (
|
|
|
|
|
ErrInterrupt = errors.New("interrupt")
|
|
|
|
|
ErrEOF = errors.New("EOF")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Logo 是 hxclaw 的 Logo
|
|
|
|
|
const Logo = "🦐"
|
|
|
|
|
|
|
|
|
|
// GetPicoclawHome 返回 picoclaw 的家目录
|
|
|
|
|
// 优先级: $PICOCLAW_HOME > ~/.picoclaw
|
|
|
|
|
func GetPicoclawHome() string {
|
|
|
|
|
return config.GetHome()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LoadConfig 加载配置文件
|
|
|
|
|
// 复用 picoclaw 的配置加载逻辑
|
|
|
|
|
func LoadConfig() (*config.Config, error) {
|
|
|
|
|
cfg, err := config.LoadConfig(GetConfigPath())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
logger.SetLevelFromString(cfg.Gateway.LogLevel)
|
|
|
|
|
return cfg, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetConfigPath 获取配置文件路径
|
|
|
|
|
func GetConfigPath() string {
|
|
|
|
|
if configPath := os.Getenv(config.EnvConfig); configPath != "" {
|
|
|
|
|
return configPath
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(GetPicoclawHome(), "config.json")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Readline 实例包装
|
|
|
|
|
type Readline struct {
|
2026-04-26 03:01:28 +08:00
|
|
|
rl *readline.Instance
|
|
|
|
|
basePrompt string
|
2026-04-11 22:32:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewReadline 创建一个新的 Readline 实例
|
|
|
|
|
func NewReadline(prompt string) (*Readline, error) {
|
|
|
|
|
// 确保历史文件目录存在
|
|
|
|
|
historyDir := filepath.Dir(filepath.Join(GetPicoclawHome(), ".hxclaw_history"))
|
|
|
|
|
os.MkdirAll(historyDir, 0755)
|
|
|
|
|
|
|
|
|
|
rl, err := readline.NewEx(&readline.Config{
|
|
|
|
|
Prompt: prompt,
|
|
|
|
|
HistoryFile: filepath.Join(GetPicoclawHome(), ".hxclaw_history"),
|
|
|
|
|
HistoryLimit: 100,
|
|
|
|
|
InterruptPrompt: "^C",
|
|
|
|
|
EOFPrompt: "exit",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-26 03:01:28 +08:00
|
|
|
return &Readline{rl: rl, basePrompt: prompt}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetPrompt 更新提示符
|
|
|
|
|
func (r *Readline) SetPrompt(prompt string) {
|
|
|
|
|
r.basePrompt = prompt
|
|
|
|
|
r.rl.SetPrompt(prompt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetBasePrompt 返回基础提示符
|
|
|
|
|
func (r *Readline) GetBasePrompt() string {
|
|
|
|
|
return r.basePrompt
|
2026-04-11 22:32:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Readline 读取一行输入
|
|
|
|
|
func (r *Readline) Readline() (string, error) {
|
|
|
|
|
line, err := r.rl.Readline()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == readline.ErrInterrupt {
|
|
|
|
|
return "", ErrInterrupt
|
|
|
|
|
}
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
return "", ErrEOF
|
|
|
|
|
}
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return line, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close 关闭 Readline 实例
|
|
|
|
|
func (r *Readline) Close() error {
|
|
|
|
|
return r.rl.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SimpleReader 简单输入读取器(无历史记录)
|
|
|
|
|
type SimpleReader struct {
|
|
|
|
|
reader *bufio.Reader
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSimpleReader 创建一个新的简单读取器
|
|
|
|
|
func NewSimpleReader() *SimpleReader {
|
|
|
|
|
return &SimpleReader{
|
|
|
|
|
reader: bufio.NewReader(os.Stdin),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReadString 读取一行输入
|
|
|
|
|
func (r *SimpleReader) ReadString() (string, error) {
|
|
|
|
|
line, err := r.reader.ReadString('\n')
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
return "", ErrEOF
|
|
|
|
|
}
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
// 去掉换行符
|
|
|
|
|
if len(line) > 0 && line[len(line)-1] == '\n' {
|
|
|
|
|
line = line[:len(line)-1]
|
|
|
|
|
}
|
|
|
|
|
return line, nil
|
|
|
|
|
}
|
2026-04-12 02:26:17 +08:00
|
|
|
|
|
|
|
|
func FindParagraphEnd(text string, startPos int) int {
|
|
|
|
|
if startPos >= len(text) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inCodeBlock := false
|
|
|
|
|
inMathBlock := false
|
|
|
|
|
|
|
|
|
|
for i := startPos; i < len(text); i++ {
|
|
|
|
|
if i+3 < len(text) && text[i:i+3] == "```" {
|
|
|
|
|
if !inCodeBlock {
|
|
|
|
|
inCodeBlock = true
|
|
|
|
|
} else {
|
|
|
|
|
inCodeBlock = false
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i+2 < len(text) && (text[i:i+2] == "$$" || text[i:i+2] == "\\[") {
|
|
|
|
|
inMathBlock = !inMathBlock
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if inCodeBlock || inMathBlock {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i+1 < len(text) && text[i] == '\n' && text[i+1] == '\n' {
|
|
|
|
|
return i + 2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
}
|