feat(browser): reuse system Chrome/Edge, bundle playwright for desktop

This commit is contained in:
zhayujie
2026-07-14 18:02:27 +08:00
parent eeb4b7981e
commit 5d55ec0f8c
17 changed files with 676 additions and 62 deletions

View File

@@ -105,6 +105,14 @@ hiddenimports += collect_submodules('docx')
hiddenimports += collect_submodules('pptx')
hiddenimports += collect_submodules('openpyxl')
# Playwright powers the browser tool. Only the pure-Python package + its bundled
# Node driver are shipped (~10-15MB); the ~150MB Chromium binary is NOT bundled
# and is either satisfied by the user's system Chrome/Edge (preferred, zero
# download) or downloaded on demand into ~/.cow/ms-playwright at first use.
# Playwright imports its transport/driver lazily, so list submodules explicitly.
hiddenimports += ['playwright', 'playwright.sync_api', 'playwright._impl']
hiddenimports += collect_submodules('playwright')
# --- Data files -----------------------------------------------------------
# Runtime-read files/dirs that must travel with the executable. Paths are
# (source, dest_dir_in_bundle).
@@ -134,6 +142,12 @@ datas += collect_data_files('tiktoken_ext', include_py_files=False)
datas += collect_data_files('docx')
datas += collect_data_files('pptx')
# Playwright ships its Node.js driver + package.json under playwright/driver/.
# These are NOT Python modules, so hiddenimports won't pull them in — collect
# them as data or `playwright install` / launching fails in the frozen build.
# include_py_files=True is required: the driver dir contains .py entrypoints.
datas += collect_data_files('playwright', include_py_files=True)
# --- Excludes -------------------------------------------------------------
# Keep the bundle lean: drop Feishu's heavy SDK, plugins (disabled in desktop
# mode), tests/docs, and dev-only packages.
@@ -143,7 +157,10 @@ excludes = [
'pip',
'wheel',
'pytest',
'playwright', # browser tool is opt-in, not bundled
# NOTE: playwright is now BUNDLED (pure-Python package + Node driver, ~10-15MB)
# so the browser tool works out of the box on desktop. The heavy Chromium
# binary is still NOT bundled: it comes from the user's system Chrome/Edge or
# is downloaded on demand into ~/.cow/ms-playwright. See browser_env.py.
]
block_cipher = None

View File

@@ -42,6 +42,12 @@ python-docx
openpyxl
python-pptx
# ---- browser tool ----
# Only the pure-Python package + Node driver are bundled by PyInstaller (~10-15MB).
# The Chromium binary is NOT bundled: the browser tool drives the user's system
# Chrome/Edge, or downloads Chromium on demand into ~/.cow at first use.
playwright==1.52.0
# ---- IM channels (kept; lightweight). Feishu/lark-oapi intentionally excluded. ----
wechatpy
pycryptodome

View File

@@ -53,6 +53,7 @@ const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function ChatInput
{ cmd: '/memory dream ', desc: t('slash_memory_dream') },
{ cmd: '/knowledge', desc: t('slash_knowledge') },
{ cmd: '/knowledge list', desc: t('slash_knowledge_list') },
{ cmd: '/install-browser', desc: t('slash_install_browser') },
{ cmd: '/config', desc: t('slash_config') },
{ cmd: '/cancel', desc: t('slash_cancel') },
{ cmd: '/logs', desc: t('slash_logs') },

View File

@@ -30,6 +30,34 @@ const md: MarkdownIt = new MarkdownIt({
},
})
// Fix greedy linkify: markdown-it's linkify swallows markdown emphasis (`*`)
// and CJK full-width punctuation glued to a URL (common in LLM output like
// `**https://x**,中文`), turning the whole tail into one broken link. Cut the
// URL at the first such char and spill the remainder back as plain text.
const _GREEDY_LINK_CUT = /[*\u3000-\u303F\uFF00-\uFFEF]/
md.core.ruler.after('linkify', 'fix_greedy_linkify', (state) => {
for (const blk of state.tokens) {
if (blk.type !== 'inline' || !blk.children) continue
const ch = blk.children
for (let i = 0; i < ch.length; i++) {
const open = ch[i]
if (open.type !== 'link_open' || open.markup !== 'linkify') continue
const textTok = ch[i + 1]
const close = ch[i + 2]
if (!textTok || textTok.type !== 'text' || !close || close.type !== 'link_close') continue
const idx = textTok.content.search(_GREEDY_LINK_CUT)
if (idx < 0) continue
const keep = textTok.content.slice(0, idx)
const spill = textTok.content.slice(idx)
textTok.content = keep
open.attrSet('href', keep)
const spillTok = new state.Token('text', '', 0)
spillTok.content = spill
ch.splice(i + 3, 0, spillTok)
}
}
})
// Open links in a new tab safely.
const defaultLinkOpen =
md.renderer.rules.link_open ||

View File

@@ -376,6 +376,7 @@ const translations: Record<string, Record<string, string>> = {
slash_memory_dream: '手动触发记忆蒸馏 (可指定天数, 默认3)',
slash_knowledge: '查看知识库统计',
slash_knowledge_list: '查看知识库文件树',
slash_install_browser: '安装浏览器工具',
slash_config: '查看当前配置',
slash_cancel: '中止当前正在运行的 Agent 任务',
slash_logs: '查看最近日志',
@@ -758,6 +759,7 @@ const translations: Record<string, Record<string, string>> = {
slash_memory_dream: 'Trigger memory distillation (optional days, default 3)',
slash_knowledge: 'Show knowledge base stats',
slash_knowledge_list: 'Show knowledge base file tree',
slash_install_browser: 'Install browser tool',
slash_config: 'Show current config',
slash_cancel: 'Abort the running agent task',
slash_logs: 'Show recent logs',