Skip to content

Commit a4d2e74

Browse files
committed
v1.6.1
1 parent fbacfac commit a4d2e74

3 files changed

Lines changed: 129 additions & 75 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ __pycache__/
88
/tests
99
*.exe
1010
inkwell
11+
inkwell-arm5
12+
inkwell-arm7
1113

1214
# C extensions
1315
*.so

inkwell.go

Lines changed: 123 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -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
// 所有常量定义
3029
const (
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进行当前对话的总结
743742
func (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+
"&", "&amp;",
1530+
"<", "&lt;",
1531+
">", "&gt;",
1532+
`"`, "&quot;",
1533+
"'", "&#39;",
1534+
)
1535+
return htmlReplacer.Replace(s)
15291536
}
15301537

15311538
// 检查字符串是否为数字
@@ -1536,81 +1543,126 @@ func isNumeric(s string) bool {
15361543

15371544
// 生成一个唯一标识符
15381545
func 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代码段外模拟一个边框
15441557
func (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, " ", "&nbsp;")
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), " ", "&nbsp;")
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() {
17251777
func (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
}

inkwell.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import http.client
2424
from urllib.parse import urlsplit
2525

26-
__Version__ = 'v1.6 (2025-06-17)'
26+
__Version__ = 'v1.6.1 (2025-06-19)'
2727
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
2828
CONFIG_JSON = f"{BASE_PATH}/config.json"
2929
HISTORY_JSON = "history.json" #历史文件会自动跟随程序传入的配置文件路径
@@ -407,13 +407,13 @@ def markdownToHtml(self, content, wrapCode=True):
407407

408408
#恢复多行代码块,Kindle不支持div边框,所以在代码块外套一个table,使用table的外框
409409
if wrapCode:
410-
tpl = ('<table border="1" cellspacing="0" width="100%" style="background-color:#f9f9f9;">'
411-
'<tr><td><pre><code class="{lang}">{code}</code></pre></td></tr></table>')
410+
tpl = ('<table border="1" bordercolor="silver" cellspacing="0" width="100%" style="background-color:#f9f9f9;border:1px solid silver;">'
411+
'<tr><td style="padding:5px;"><pre><code class="{lang}">{code}</code></pre></td></tr></table>')
412412
else:
413413
tpl = '<pre style="border:1px solid #555555;padding:10px;background-color:#f9f9f9;"><code class="{lang}">{code}</code></pre>'
414414
for id_, (lang, code) in codes.items():
415415
code = code.replace(' ', '&nbsp;')
416-
content = content.replace(id_, tpl.format(lang=lang, code=code))
416+
content = content.replace(id_, tpl.format(lang=lang or "lang", code=code))
417417

418418
return content
419419

0 commit comments

Comments
 (0)