@@ -11,7 +11,6 @@ import (
1111 "net/http"
1212 "net/url"
1313 "os"
14-
1514 //"os/user"
1615 "crypto/tls"
1716 "encoding/base64"
@@ -28,7 +27,7 @@ import (
2827
2928// 所有常量定义
3029const (
31- Version = "v1.6-go (2025-06-17 )"
30+ Version = "v1.6.1 -go (2025-06-19 )"
3231 ConfigFile = "config.json"
3332 HistoryFile = "history.json"
3433 PromptsFile = "prompts.txt"
@@ -411,9 +410,9 @@ func (iw *InkWell) SaveConfig(cfg *AppConfig) {
411410 b , _ := json .MarshalIndent (cfg , "" , " " )
412411 err := os .WriteFile (iw .CfgFile , b , 0644 )
413412 if err != nil {
414- Styled ("Failed to write %s: %v" ).Sprintf (Styled (iw .CfgFile ).Bold (), err ). Println ( )
413+ Styled ("Failed to write %s: %v\n " ).Printf (Styled (iw .CfgFile ).Bold (), err )
415414 } else {
416- Styled ("Config have been saved to file: %s" ).Sprintf (Styled (iw .CfgFile ).Bold ()). Println ( )
415+ Styled ("Config have been saved to file: %s\n " ).Printf (Styled (iw .CfgFile ).Bold ())
417416 }
418417}
419418
@@ -742,10 +741,11 @@ func (iw *InkWell) PrintChatBubble(role, topic string) {
742741// 如果传入msg,则使用msg前5个单词作为主题,否则让AI进行当前对话的总结
743742func (iw * InkWell ) UpdateTopic (msg string ) {
744743 topic := DefaultTopic
745- replacer := strings .NewReplacer ("\n " , " " , "\" " , " " , "/" , " " , "\\ " , " " , "'" , " " , "`" , " " )
744+ // 这个 replacer 每次对话最多使用两次,就不提前创建为全局变量了
745+ titleReplacer := strings .NewReplacer ("\n " , " " , "\" " , " " , "/" , " " , "\\ " , " " , "'" , " " , "`" , " " )
746746 if msg != "" {
747747 // 直接从消息中提取前5个单词作为主题
748- words := strings .Fields (replacer .Replace (msg ))
748+ words := strings .Fields (titleReplacer .Replace (msg ))
749749 if len (words ) > 5 {
750750 words = words [:5 ]
751751 }
@@ -755,7 +755,7 @@ func (iw *InkWell) UpdateTopic(msg string) {
755755 messages := append (iw .Messages , ChatItem {Role : "user" , Content : PromptGetTopic })
756756 resp := iw .FetchAiResponse (messages )
757757 if resp .Success {
758- topic = replacer .Replace (resp .Content )
758+ topic = titleReplacer .Replace (resp .Content )
759759 }
760760 }
761761 if len (topic ) > 40 {
@@ -1352,10 +1352,10 @@ func (iw *InkWell) ExportHistory(expName string, indexList []int) {
13521352
13531353 // 生成html文件内容
13541354 var htmlContent strings.Builder
1355- htmlContent .WriteString ("<!DOCTYPE html>\n <html>\n <head>\n <meta charset=\" UTF-8\" ><title>AI Chat History</title></head><body>" )
1355+ htmlContent .WriteString ("<!DOCTYPE html>\n <html>\n <head>\n <meta charset=\" UTF-8\" ><title>AI Chat History</title></head><body>\n " )
13561356
13571357 for _ , item := range history {
1358- htmlContent .WriteString (fmt .Sprintf ("<h1>%s</h1><hr/>" , item .Topic ))
1358+ htmlContent .WriteString (fmt .Sprintf ("<h1>%s</h1><hr/>\n " , item .Topic ))
13591359
13601360 for _ , msg := range item .Messages {
13611361 content := iw .MarkdownToHtml (msg .Content , ! isEmail )
@@ -1512,20 +1512,27 @@ func isWriteableDir(dir_ string) bool {
15121512 return false
15131513 }
15141514
1515- // Windows 下如果目录存在就认为可写,尽管不准确,但是方便
1516- // 否则可能需要通过创建一个临时文件的方式来判断
1517- // runtime.GOOS 常量也可以用来判断,但是不想引入更多的库了
1518- if os .PathSeparator == '\\' {
1519- return true
1520- }
1521-
1522- // 尝试打开目录句柄进行写入检查
1523- file , err := os .OpenFile (dir_ , os .O_WRONLY , 0 )
1524- if err != nil {
1525- return false
1526- }
1527- file .Close ()
1528- return true
1515+ tempFile := filepath .Join (dir_ , ".write_test" )
1516+ file , err := os .Create (tempFile )
1517+ if err != nil {
1518+ return false
1519+ }
1520+ file .Close ()
1521+ os .Remove (tempFile )
1522+ return true
1523+ }
1524+
1525+ // html转义
1526+ func htmlEscape (s string ) string {
1527+ // 导出聊天历史才会使用,效率不是很重要,启动速度才重要,就不创建为全局变量了
1528+ var htmlReplacer = strings .NewReplacer (
1529+ "&" , "&" ,
1530+ "<" , "<" ,
1531+ ">" , ">" ,
1532+ `"` , """ ,
1533+ "'" , "'" ,
1534+ )
1535+ return htmlReplacer .Replace (s )
15291536}
15301537
15311538// 检查字符串是否为数字
@@ -1536,81 +1543,126 @@ func isNumeric(s string) bool {
15361543
15371544// 生成一个唯一标识符
15381545func generateUID () string {
1539- return fmt .Sprintf ("%d%d" , time .Now ().UnixNano (), rand .Int63 ())
1546+ //return fmt.Sprintf("%d%d", time.Now().UnixNano(), rand.Int63())
1547+ const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
1548+ b := make ([]byte , 16 )
1549+ for i := range b {
1550+ b [i ] = letters [rand .Intn (len (letters ))]
1551+ }
1552+ return string (b )
15401553}
15411554
15421555// 简单的markdown转换为html
15431556// wrapCode: 使用table套在code代码段外模拟一个边框
15441557func (iw * InkWell ) MarkdownToHtml (content string , wrapCode bool ) string {
1545- // 先把多行代码块中的文本提取出来,避免下面其他的处理搞乱代码
1558+ reCodeBlock := regexp .MustCompile ("(?s)```(?:([\\ w\\ -\\ +]*)\\ n)?(.*?)```" )
1559+ reInlineCode := regexp .MustCompile ("`([^`]+)`" )
1560+ reHeader := regexp .MustCompile ("(?m)^(#{1,6})\\ s+(.+)$" )
1561+ reBold := regexp .MustCompile (`\*\*(.+?)\*\*|__(.+?)__` )
1562+ reItalic := regexp .MustCompile (`\*(.+?)\*|_(.+?)_` )
1563+ reStrike := regexp .MustCompile (`~~(.+?)~~` )
1564+ reUL := regexp .MustCompile (`(?m)^\s*[\*\-]\s+(.+)$` )
1565+ reOL := regexp .MustCompile (`(?m)^\s*(\d+)\.\s+(.+)$` )
1566+ reQuote := regexp .MustCompile (`(?m)^\s*>\s+(.+)$` )
1567+ reLink := regexp .MustCompile (`\[(.+?)\]\((.+?)\)` )
1568+
1569+ // 提取代码块
15461570 codeBlocks := make (map [string ]struct {
15471571 lang string
15481572 code string
15491573 })
1550- content = regexp .MustCompile ("```(\\ w+)?\\ n([\\ s\\ S]*?)```" ).ReplaceAllStringFunc (content , func (match string ) string {
1551- id := fmt .Sprintf ("{{%s}}" , generateUID ())
1552- parts := regexp .MustCompile ("```(\\ w+)?\\ n([\\ s\\ S]*?)```" ).FindStringSubmatch (match )
1553- codeBlocks [id ] = struct {
1574+ content = reCodeBlock .ReplaceAllStringFunc (content , func (match string ) string {
1575+ parts := reCodeBlock .FindStringSubmatch (match )
1576+ lang := strings .TrimSpace (parts [1 ])
1577+ code := parts [2 ]
1578+ uid := fmt .Sprintf ("[[CODEBLOCK_%s]]" , generateUID ())
1579+ codeBlocks [uid ] = struct {
15541580 lang string
15551581 code string
15561582 }{
1557- lang : parts [ 1 ] ,
1558- code : parts [ 2 ] ,
1583+ lang : lang ,
1584+ code : code ,
15591585 }
1560- return id
1586+ return uid
15611587 })
15621588
1563- // 行内代码 (`code`)
1564- content = regexp . MustCompile ( "`([^`]+)`" ) .ReplaceAllString (content , "<code>$1</code>" )
1589+ // 行内代码
1590+ content = reInlineCode .ReplaceAllString (content , "<code>$1</code>" )
15651591
1566- // 表格处理
1567- content = iw . MdTableToHtml (content )
1592+ // 链接
1593+ content = reLink . ReplaceAllString (content , `<a href="$2">$1</a>` )
15681594
1569- // 标题 (# 或 ## 等)
1570- content = regexp . MustCompile ( "(?m)^(#{1,6}) \\ s+?(.*)$" ). ReplaceAllStringFunc (content , func (match string ) string {
1571- parts := regexp . MustCompile ( "^(#{1,6}) \\ s+?(.*)$" ). FindStringSubmatch (match )
1595+ // 标题
1596+ content = reHeader . ReplaceAllStringFunc (content , func (m string ) string {
1597+ parts := reHeader . FindStringSubmatch (m )
15721598 level := len (parts [1 ])
15731599 return fmt .Sprintf ("<h%d>%s</h%d>" , level , strings .TrimSpace (parts [2 ]), level )
15741600 })
15751601
1576- // 加粗 (**bold** 或 __bold__)
1577- content = regexp .MustCompile (`\*\*(.*?)\*\*` ).ReplaceAllString (content , "<strong>$1</strong>" )
1578- content = regexp .MustCompile (`__(.*?)__` ).ReplaceAllString (content , "<strong>$1</strong>" )
1602+ // 引用
1603+ content = reQuote .ReplaceAllString (content , `<blockquote>$1</blockquote>` )
15791604
1580- // 斜体 (*italic* 或 _italic_)
1581- content = regexp .MustCompile (`\*(.*?)\*` ).ReplaceAllString (content , "<em>$1</em>" )
1582- content = regexp .MustCompile (`_(.*?)_` ).ReplaceAllString (content , "<em>$1</em>" )
1605+ // 无序列表
1606+ content = reUL .ReplaceAllString (content , `<div><strong>• </strong>$1</div>` )
15831607
1584- // 删除线 (~~text~~)
1585- content = regexp . MustCompile ( "~{1,2}(.*?)~{1,2}" ). ReplaceAllString (content , "<s>$1</s>" )
1608+ // 有序列表
1609+ content = reOL . ReplaceAllString (content , `<div><strong>$1. </strong>$2</div>` )
15861610
1587- // 无序列表 (- 或 * 开头)
1588- content = regexp .MustCompile ("(?m)^ *[\\ *\\ -]\\ s+?(.*)$" ).ReplaceAllString (content , "<div><strong>• </strong>$1</div>" )
1589-
1590- // 有序列表 (数字加点开头)
1591- content = regexp .MustCompile ("(?m)^ *(\\ d+\\ .\\ s+?)(.*)$" ).ReplaceAllString (content , "<div><strong>$1</strong>$2</div>" )
1611+ // 加粗
1612+ content = reBold .ReplaceAllStringFunc (content , func (m string ) string {
1613+ parts := reBold .FindStringSubmatch (m )
1614+ if parts [1 ] != "" {
1615+ return "<strong>" + parts [1 ] + "</strong>"
1616+ }
1617+ return "<strong>" + parts [2 ] + "</strong>"
1618+ })
15921619
1593- // 引用 (大于号开头)
1594- content = regexp .MustCompile ("(?m)^\\ s*>+\\ s+?(.*)$" ).ReplaceAllString (content , "<blockquote>$1</blockquote>" )
1620+ // 斜体
1621+ content = reItalic .ReplaceAllStringFunc (content , func (m string ) string {
1622+ parts := reItalic .FindStringSubmatch (m )
1623+ if parts [1 ] != "" {
1624+ return "<em>" + parts [1 ] + "</em>"
1625+ }
1626+ return "<em>" + parts [2 ] + "</em>"
1627+ })
15951628
1596- // 链接 [text](url)
1597- content = regexp . MustCompile ( " \\ [([^ \\ ]]+) \\ ] \\ (([^)]+) \\ )" ). ReplaceAllString (content , "<a href= \" $2 \" >$1</a>" )
1629+ // 删除线
1630+ content = reStrike . ReplaceAllString (content , `<s >$1</s>` )
15981631
1599- // 段落 (保持换行)
1600- content = regexp .MustCompile ("([^\\ n]+)" ).ReplaceAllString (content , "<div>$1</div>" )
1632+ // 段落包裹(在恢复代码块之前)
1633+ lines := strings .Split (content , "\n " )
1634+ for i , line := range lines {
1635+ lineTrim := strings .TrimSpace (line )
1636+ if lineTrim == "" {
1637+ continue
1638+ }
1639+ if strings .HasPrefix (lineTrim , "<h" ) ||
1640+ strings .HasPrefix (lineTrim , "<pre" ) ||
1641+ strings .HasPrefix (lineTrim , "<blockquote" ) ||
1642+ strings .HasPrefix (lineTrim , "<div>" ) ||
1643+ strings .HasPrefix (lineTrim , "<table" ) {
1644+ continue
1645+ }
1646+ lines [i ] = "<div>" + lineTrim + "</div>"
1647+ }
1648+ content = strings .Join (lines , "\n " )
16011649
1602- // 恢复代码块
1603- var tpl string
1650+ // 最后恢复代码块
1651+ codeTpl := ""
16041652 if wrapCode {
1605- tpl = `<table border="1" cellspacing="0" width="100%" style="background-color:#f9f9f9;">` +
1606- ` <tr><td><pre><code class="%s">%s</code></pre></td></tr></table>`
1653+ codeTpl = `<table border="1" bordercolor="silver" cellspacing="0" width="100%% " style="background-color:#f9f9f9;border:1px solid silver;">
1654+ <tr><td style="padding:10px;" ><pre><code class="%s">%s</code></pre></td></tr></table>`
16071655 } else {
1608- tpl = `<pre style="border:1px solid #555555 ;padding:10px;background-color:#f9f9f9;"><code class="%s" >%s</code></pre>`
1656+ codeTpl = `<pre style="border:1px solid silver ;padding:10px;background-color:#f9f9f9;"><code%s >%s</code></pre>`
16091657 }
1610-
1611- for id , block := range codeBlocks {
1612- code := strings .ReplaceAll (block .code , " " , " " )
1613- content = strings .ReplaceAll (content , id , fmt .Sprintf (tpl , block .lang , code ))
1658+ for uid , block := range codeBlocks {
1659+ langAttr := block .lang
1660+ if langAttr != "" {
1661+ langAttr = "lang"
1662+ }
1663+ escaped := strings .ReplaceAll (htmlEscape (block .code ), " " , " " )
1664+ html := fmt .Sprintf (codeTpl , langAttr , escaped )
1665+ content = strings .ReplaceAll (content , uid , html )
16141666 }
16151667
16161668 return content
@@ -1725,7 +1777,7 @@ func (iw *InkWell) ShowCmdList() {
17251777func (iw * InkWell ) ProcessMenu () string {
17261778 iw .ShowMenu ()
17271779 for {
1728- input := Input ("[num, c, d, e, m, n, p, q, ?] » " )
1780+ input := strings . ToLower ( Input ("[num, c, d, e, m, n, p, q, ?] » " ) )
17291781 switch input {
17301782 case "q" : // 退出
17311783 return "quit"
@@ -1814,13 +1866,13 @@ func (iw *InkWell) SwitchModel() {
18141866 index := toInt (input , 0 )
18151867
18161868 if 1 <= index && index <= len (models ) {
1817- iw .Provider .Model = models [index - 1 ].Name
1869+ iw .Provider .Model = models [index - 1 ].Name
18181870 iw .Config .Model = iw .Provider .Model
18191871 if needSave {
18201872 iw .SaveConfig (nil )
18211873 }
18221874 break
1823- } else if index == len (models ) + 1 {
1875+ } else if index == len (models )+ 1 {
18241876 if modelName := Input ("Model Name » " ); modelName != "" {
18251877 iw .Provider .Model = modelName
18261878 iw .Config .Model = modelName
@@ -2164,7 +2216,7 @@ func (iw *InkWell) MdTableToTerm(content string) string {
21642216 for idx , row := range lines {
21652217 if strings .HasPrefix (row , "|" ) && strings .HasSuffix (row , "|" ) {
21662218 // 必须要连续
2167- if prevTableRowIdx >= 0 && (prevTableRowIdx + 1 ) != idx {
2219+ if prevTableRowIdx >= 0 && (prevTableRowIdx + 1 ) != idx {
21682220 colNums = nil
21692221 break
21702222 }
0 commit comments