Compare commits

..

6 Commits

Author SHA1 Message Date
zhayujie
5d55ec0f8c feat(browser): reuse system Chrome/Edge, bundle playwright for desktop 2026-07-14 18:02:27 +08:00
zhayujie
eeb4b7981e feat(model): add gpt-5.6-luna, terra, sol 2026-07-14 11:27:03 +08:00
zhayujie
5f1c98881d feat(model): restore the claude-fable-5 model 2026-07-14 11:07:36 +08:00
zhayujie
94d0f56689 fix(desktop): make Windows dry-run signing work 2026-07-13 17:49:50 +08:00
zhayujie
4d690341a7 feat(desktop): add Windows code signing 2026-07-13 16:40:32 +08:00
zhayujie
d8c419227c Merge pull request #2950 from zhayujie/feat-mcp-oauth
feat(mcp): auto OAuth authorization for remote MCP servers
2026-07-13 12:01:32 +08:00
37 changed files with 927 additions and 120 deletions

View File

@@ -113,6 +113,31 @@ jobs:
shell: bash
run: npm run build
# Download the Windows signing CLI. The URL comes from a repo variable, so
# nothing about the signing setup is hardcoded in a public workflow. Only
# runs on the Windows leg and only when a URL is set; otherwise the build
# stays unsigned. SIGNTOOL_PATH is exported for the next step's
# electron-builder.win.js to invoke.
- name: Download Windows signing CLI
if: matrix.platform == 'win' && vars.SIGNTOOL_CLI_URL != ''
shell: bash
env:
SIGNTOOL_CLI_URL: ${{ vars.SIGNTOOL_CLI_URL }}
run: |
mkdir -p "$RUNNER_TEMP/signtool"
curl -fsSL "$SIGNTOOL_CLI_URL" -o "$RUNNER_TEMP/signtool/cli.zip"
# Unzip and locate the signtool executable regardless of nesting.
unzip -o "$RUNNER_TEMP/signtool/cli.zip" -d "$RUNNER_TEMP/signtool" >/dev/null
exe="$(find "$RUNNER_TEMP/signtool" -type f -iname 'signtool*.exe' | head -n1)"
if [ -z "$exe" ]; then
echo "signtool.exe not found in downloaded archive" >&2
find "$RUNNER_TEMP/signtool" -type f >&2
exit 1
fi
# Normalize to a Windows-style path for execFileSync in Node.
echo "SIGNTOOL_PATH=$(cygpath -w "$exe")" >> "$GITHUB_ENV"
echo "resolved signtool: $exe"
- name: Build & publish (electron-builder)
working-directory: desktop
shell: bash
@@ -124,8 +149,14 @@ jobs:
# is the correct state for unsigned builds.
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
# Windows code signing via the signing CLI. Credentials are
# secrets; SIGNTOOL_PATH was exported by the download step above.
# COW_SIGN_DRY_RUN (repo variable) lets us validate the whole pipeline
# with a self-signed cert before buying a real one — no quota used.
SIGNTOOL_ACCESS_KEY: ${{ secrets.SIGNTOOL_ACCESS_KEY }}
SIGNTOOL_ACCESS_SECRET: ${{ secrets.SIGNTOOL_ACCESS_SECRET }}
SIGNTOOL_CERT_CODE: ${{ secrets.SIGNTOOL_CERT_CODE }}
COW_SIGN_DRY_RUN: ${{ vars.COW_SIGN_DRY_RUN }}
run: |
# Pick the signing cert for THIS platform only. The mac and win secrets
# are both present in the job env, but a mac cert must never leak into a
@@ -137,6 +168,10 @@ jobs:
#
# NOTE: we only ever `export`, never `unset`, GitHub-injected env vars
# (an `unset` can return non-zero and abort under errexit).
# macOS keeps the classic CSC_LINK (.p12) flow. Windows no longer uses
# a local .pfx (EV private keys can't be exported since 2023); it signs
# via the CLI wired into electron-builder.win.js instead, using the
# SIGNTOOL_* env already set above — nothing to export here.
case "${{ matrix.platform }}" in
mac)
if [ -n "$MAC_CSC_LINK" ]; then
@@ -144,12 +179,6 @@ jobs:
export CSC_KEY_PASSWORD="$MAC_CSC_KEY_PASSWORD"
fi
;;
win)
if [ -n "$WIN_CSC_LINK" ]; then
export CSC_LINK="$WIN_CSC_LINK"
export CSC_KEY_PASSWORD="$WIN_CSC_KEY_PASSWORD"
fi
;;
esac
# Never let electron-builder publish: our publish target is a generic
@@ -157,19 +186,25 @@ jobs:
# installers to R2 and register them in D1 ourselves (publish-r2 job).
# `--publish never` still emits the latest*.yml files.
#
# CONFIG PER PLATFORM: the dynamic electron-builder.js only exists to
# inject mac.binaries (the backend Mach-O files to hardened-sign for
# notarization) — it's a pure no-op on Windows. Passing --config on
# Windows was what silently broke the Windows build (it produced no
# installer while the job still reported success; Windows worked fine
# before --config was introduced). So Windows uses the plain
# package.json build config and only mac uses the dynamic one.
# CONFIG PER PLATFORM: each platform loads its OWN dynamic config.
# mac -> electron-builder.js (injects mac.binaries for signing)
# win -> electron-builder.win.js (wires the sign hook; electron-builder
# signs the app, backend and installer)
# HISTORY: passing --config on Windows previously broke the build (no
# installer, job still green). That happened because the MAC config
# (electron-builder.js) was a no-op on Windows yet still disturbed the
# run. The fix is a DEDICATED win config that correctly extends
# config.win — not sharing the mac one. If a build ever runs WITHOUT
# signing configured, electron-builder.win.js still returns the base
# config unchanged (sign hook just skips), so the installer is still
# produced.
#
# Invoke via `node <cli.js>` rather than `npx`: on Windows `npx` is
# npx.cmd (a batch wrapper) and running it from this Git Bash step can
# make bash return before the wrapped process finishes. node skips it.
case "${{ matrix.platform }}" in
mac) config_arg="--config electron-builder.js" ;;
win) config_arg="--config electron-builder.win.js" ;;
*) config_arg="" ;;
esac
node node_modules/electron-builder/cli.js ${{ matrix.eb_flags }} $config_arg --publish never

View File

@@ -106,8 +106,8 @@ CowAgent supports all mainstream LLM providers. **Chat, vision, image generation
| Provider | Featured Models | Chat | Vision | Image Gen | ASR | TTS | Embedding |
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [Claude](https://docs.cowagent.ai/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [OpenAI](https://docs.cowagent.ai/models/openai) | gpt-5.5, o-series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Claude](https://docs.cowagent.ai/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [OpenAI](https://docs.cowagent.ai/models/openai) | gpt-5.6 series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Gemini](https://docs.cowagent.ai/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [DeepSeek](https://docs.cowagent.ai/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [Qwen](https://docs.cowagent.ai/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

View File

@@ -0,0 +1,290 @@
"""
Browser environment detection and capability resolution.
Centralizes everything about *where* a usable browser engine comes from, so
both the runtime (browser_service) and the installer (cli/commands/install)
agree on the same decisions:
- Whether the `playwright` Python package is importable.
- Whether a system Chrome / Edge is installed (Playwright can drive it via
the `channel="chrome"/"msedge"` launcher, no download needed).
- Where Playwright's own Chromium download lives (redirected to the writable
data dir so it survives frozen/desktop app updates).
Resolution priority (see resolve_engine):
1. system-chrome -> drive the user's installed Chrome / Edge (zero download)
2. playwright-chromium -> Playwright's own Chromium, if already downloaded
3. none -> nothing usable yet; caller should trigger onboarding
"""
import os
import sys
import shutil
from typing import Optional, Dict, Any
from common.log import logger
# Playwright browser channels we accept for the "system-chrome" mode, in
# preference order. "chrome" covers stable Google Chrome; "msedge" is the
# Chromium-based Edge shipped on every Windows 10/11.
_PREFERRED_CHANNELS = ("chrome", "msedge", "chrome-beta", "msedge-beta")
def get_data_root() -> str:
"""Writable data root (~/.cow on desktop, else CWD-based).
Mirrors the logic in common/log.py without importing config, to avoid a
circular import. The desktop build sets COW_DATA_DIR; source deployments
fall back to the current working directory.
"""
data_dir = os.environ.get("COW_DATA_DIR")
if data_dir:
return os.path.expanduser(data_dir)
return os.getcwd()
def browsers_download_dir() -> str:
"""Directory Playwright downloads its Chromium into.
We pin it under the writable data root (~/.cow/ms-playwright) rather than
Playwright's default (~/.cache/ms-playwright or %USERPROFILE%). This keeps
the frozen desktop build self-contained and makes the download survive app
updates. Set as PLAYWRIGHT_BROWSERS_PATH for both install and runtime.
"""
return os.path.join(get_data_root(), "ms-playwright")
def apply_browsers_path_env() -> None:
"""Point Playwright at our pinned download dir via env var (idempotent).
Only set it when not already provided by the user, so power users can
override the location. Must run before importing playwright's launcher.
"""
if not os.environ.get("PLAYWRIGHT_BROWSERS_PATH"):
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = browsers_download_dir()
def is_frozen() -> bool:
"""True when running inside a PyInstaller-frozen bundle (desktop backend).
In this mode sys.executable is the frozen exe (no pip), so the installer
must skip `pip install` and only download the browser binary.
"""
return bool(getattr(sys, "frozen", False))
def is_desktop() -> bool:
"""True when running as the Electron desktop client (dev or packaged).
The desktop shell always sets COW_DESKTOP=1 (see python-manager.ts), both in
`npm run dev` (runs app.py with the user's Python) and in the packaged build
(frozen exe). Desktop users have no `cow` CLI, so onboarding must point them
at the in-chat `/install-browser` command rather than a terminal command.
"""
return os.environ.get("COW_DESKTOP") == "1"
def has_playwright_package() -> bool:
"""True if the `playwright` Python package can be imported."""
try:
import playwright # noqa: F401
return True
except Exception:
return False
def _windows_program_dirs() -> list:
dirs = []
for var in ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"):
val = os.environ.get(var)
if val:
dirs.append(val)
return dirs
def detect_system_chrome() -> Optional[Dict[str, str]]:
"""Locate an installed Chromium-based browser Playwright can drive.
Returns a dict {"channel": <playwright channel>, "path": <exe path>} for
the first match, or None. The `channel` is what we hand to Playwright's
launcher; `path` is only informational (Playwright resolves the channel on
its own, but we keep the path for logging / onboarding messages).
"""
candidates = []
if sys.platform == "darwin":
candidates = [
("chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
("msedge", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
("chrome-beta", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"),
]
elif sys.platform == "win32":
prog_dirs = _windows_program_dirs()
for base in prog_dirs:
candidates.append(("chrome", os.path.join(base, "Google", "Chrome", "Application", "chrome.exe")))
candidates.append(("msedge", os.path.join(base, "Microsoft", "Edge", "Application", "msedge.exe")))
else:
# Linux: rely on PATH lookups for the common binaries.
path_lookups = [
("chrome", "google-chrome"),
("chrome", "google-chrome-stable"),
("chrome", "chromium"),
("chrome", "chromium-browser"),
("msedge", "microsoft-edge"),
]
for channel, binary in path_lookups:
found = shutil.which(binary)
if found:
return {"channel": channel, "path": found}
for channel, path in candidates:
if path and os.path.exists(path):
return {"channel": channel, "path": path}
return None
def has_downloaded_chromium() -> bool:
"""True if Playwright already has a Chromium download available.
We check our pinned download dir for a chromium-* folder. This is a
lightweight heuristic (avoids importing/launching Playwright just to probe)
and matches how Playwright lays browsers out on disk.
"""
download_dir = browsers_download_dir()
if not os.path.isdir(download_dir):
return False
try:
for name in os.listdir(download_dir):
# Playwright names its browser dirs like "chromium-1140",
# "chromium_headless_shell-1140".
if name.startswith("chromium"):
return True
except OSError:
pass
return False
def resolve_engine(config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Decide which browser engine to use, given config and environment.
Returns a dict describing the launch strategy:
{
"mode": "system-chrome" | "playwright-chromium" | "none",
"channel": Optional[str], # for system-chrome
"path": Optional[str], # for system-chrome (informational)
"has_playwright": bool,
"reason": str, # human-readable, for logging / onboarding
}
Config keys under tools.browser that influence this:
- engine: "auto" (default) | "system-chrome" | "chromium"
Force a specific engine. "auto" prefers system Chrome, then falls
back to a downloaded Chromium.
- prefer_system_browser: bool (default True). When False under "auto",
skip system Chrome and go straight to Playwright's Chromium.
"""
config = config or {}
apply_browsers_path_env()
has_pw = has_playwright_package()
engine_pref = str(config.get("engine", "auto")).strip().lower()
prefer_system = config.get("prefer_system_browser", True)
if not has_pw:
return {
"mode": "none",
"channel": None,
"path": None,
"has_playwright": False,
"reason": "playwright package not available",
}
system = None
if engine_pref in ("auto", "system-chrome") and prefer_system:
system = detect_system_chrome()
if engine_pref == "system-chrome":
# Explicitly requested: use system Chrome if found, else report none.
if system:
return {
"mode": "system-chrome",
"channel": system["channel"],
"path": system["path"],
"has_playwright": True,
"reason": f"using system browser ({system['channel']})",
}
return {
"mode": "none",
"channel": None,
"path": None,
"has_playwright": True,
"reason": "engine=system-chrome but no Chrome/Edge found",
}
if engine_pref == "chromium":
# Explicitly requested Playwright's own Chromium.
if has_downloaded_chromium():
return {
"mode": "playwright-chromium",
"channel": None,
"path": None,
"has_playwright": True,
"reason": "using downloaded Playwright Chromium",
}
return {
"mode": "none",
"channel": None,
"path": None,
"has_playwright": True,
"reason": "engine=chromium but Chromium not downloaded yet",
}
# auto: system Chrome first, then downloaded Chromium.
if system:
return {
"mode": "system-chrome",
"channel": system["channel"],
"path": system["path"],
"has_playwright": True,
"reason": f"auto: using system browser ({system['channel']})",
}
if has_downloaded_chromium():
return {
"mode": "playwright-chromium",
"channel": None,
"path": None,
"has_playwright": True,
"reason": "auto: using downloaded Playwright Chromium",
}
return {
"mode": "none",
"channel": None,
"path": None,
"has_playwright": True,
"reason": "no system Chrome/Edge and no downloaded Chromium",
}
def capability_summary(config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""High-level browser capability status, for onboarding / diagnostics.
Combines resolve_engine with raw detection flags so the UI / tool layer can
craft a helpful message (e.g. "Chrome detected, click to enable" vs
"no browser, will download ~150MB").
"""
engine = resolve_engine(config)
system = detect_system_chrome()
return {
"ready": engine["mode"] != "none",
"engine": engine,
"has_playwright": engine["has_playwright"],
"has_system_chrome": system is not None,
"system_chrome": system,
"has_downloaded_chromium": has_downloaded_chromium(),
"is_frozen": is_frozen(),
"is_desktop": is_desktop(),
"browsers_dir": browsers_download_dir(),
}

View File

@@ -326,12 +326,19 @@ class BrowserService:
# - persistent: launch with launch_persistent_context using a user_data_dir
# so cookies / login state survive across runs (default).
# - fresh: classic launch + new_context, clean state every run.
#
# Within persistent/fresh, the actual Chromium binary is resolved by
# browser_env.resolve_engine(): a system Chrome/Edge (channel-based, zero
# download) is preferred, falling back to Playwright's own downloaded
# Chromium. `self._channel` is the Playwright channel ("chrome"/"msedge")
# when driving a system browser, else None (bundled Chromium).
cdp_endpoint = self._config.get("cdp_endpoint") or ""
persistent_flag = self._config.get("persistent", True)
user_data_dir_cfg = self._config.get("user_data_dir")
if user_data_dir_cfg is None:
user_data_dir_cfg = _DEFAULT_USER_DATA_DIR
self._channel: Optional[str] = None
self._cdp_endpoint: str = cdp_endpoint.strip() if isinstance(cdp_endpoint, str) else ""
if self._cdp_endpoint:
self._launch_mode = "cdp"
@@ -343,6 +350,22 @@ class BrowserService:
self._launch_mode = "fresh"
self._user_data_dir = ""
# Resolve which browser engine to drive (system Chrome vs downloaded
# Chromium). Deferred detection failures are surfaced at launch time.
if self._launch_mode != "cdp":
try:
from agent.tools.browser.browser_env import resolve_engine
engine = resolve_engine(self._config)
if engine["mode"] == "system-chrome":
self._channel = engine["channel"]
logger.info(f"[Browser] Engine resolved: {engine['reason']}")
elif engine["mode"] == "playwright-chromium":
logger.info(f"[Browser] Engine resolved: {engine['reason']}")
else:
logger.info(f"[Browser] No ready engine yet: {engine['reason']}")
except Exception as e:
logger.debug(f"[Browser] Engine resolution skipped: {e}")
# Idle auto-release
idle_cfg = self._config.get("idle_timeout")
self._idle_timeout: float = float(idle_cfg) if idle_cfg is not None else self._IDLE_TIMEOUT_DEFAULT
@@ -428,6 +451,14 @@ class BrowserService:
def _launch_browser(self):
"""Launch / connect Chromium on the background thread."""
# Point Playwright at our pinned download dir before any launch so a
# bundled-Chromium fallback finds the browser downloaded to ~/.cow.
try:
from agent.tools.browser.browser_env import apply_browsers_path_env
apply_browsers_path_env()
except Exception as e:
logger.debug(f"[Browser] apply_browsers_path_env skipped: {e}")
if self._headless is None:
headless_cfg = self._config.get("headless")
self._headless = headless_cfg if headless_cfg is not None else _should_use_headless()
@@ -475,12 +506,20 @@ class BrowserService:
logger.info("[Browser] Browser ready")
def _launch_fresh(self, launch_args: List[str], viewport: Dict[str, int], user_agent: str):
"""Classic launch: brand new Chromium with an empty context."""
logger.info(f"[Browser] Launching Chromium (fresh, headless={self._headless})")
self._browser = self._playwright.chromium.launch(
headless=self._headless,
args=launch_args,
)
"""Classic launch: brand new Chromium with an empty context.
When `self._channel` is set (e.g. "chrome"/"msedge"), Playwright drives
the user's installed system browser instead of its own Chromium.
"""
engine_label = f"system:{self._channel}" if self._channel else "chromium"
logger.info(f"[Browser] Launching {engine_label} (fresh, headless={self._headless})")
launch_kwargs: Dict[str, Any] = {
"headless": self._headless,
"args": launch_args,
}
if self._channel:
launch_kwargs["channel"] = self._channel
self._browser = self._playwright.chromium.launch(**launch_kwargs)
self._context = self._browser.new_context(
viewport=viewport,
user_agent=user_agent,
@@ -491,18 +530,25 @@ class BrowserService:
def _launch_persistent(self, launch_args: List[str], viewport: Dict[str, int], user_agent: str):
"""Launch Chromium with a persistent user_data_dir so login state survives."""
os.makedirs(self._user_data_dir, exist_ok=True)
engine_label = f"system:{self._channel}" if self._channel else "chromium"
logger.info(
f"[Browser] Launching Chromium (persistent, headless={self._headless}, "
f"[Browser] Launching {engine_label} (persistent, headless={self._headless}, "
f"profile={self._user_data_dir})"
)
persistent_kwargs: Dict[str, Any] = {
"user_data_dir": self._user_data_dir,
"headless": self._headless,
"args": launch_args,
"viewport": viewport,
"user_agent": user_agent,
}
# When driving a system browser, let it use its real UA instead of the
# spoofed Chromium one (avoids UA/engine mismatch on real Chrome/Edge).
if self._channel:
persistent_kwargs["channel"] = self._channel
persistent_kwargs.pop("user_agent", None)
try:
self._context = self._playwright.chromium.launch_persistent_context(
user_data_dir=self._user_data_dir,
headless=self._headless,
args=launch_args,
viewport=viewport,
user_agent=user_agent,
)
self._context = self._playwright.chromium.launch_persistent_context(**persistent_kwargs)
except Exception as e:
# Profile is locked when another Chromium instance already holds it.
msg = str(e).lower()

View File

@@ -185,6 +185,40 @@ class BrowserTool(BaseTool):
f"({ip_str}), request blocked for security"
)
def _check_engine_ready(self) -> Optional[ToolResult]:
"""Return an actionable onboarding message if no browser engine is ready.
Returns None when a system Chrome/Edge or a downloaded Chromium is
available (so the tool can proceed). Otherwise returns a ToolResult with
clear guidance so the agent asks the user to enable the browser instead
of surfacing a raw Playwright launch error. CDP mode is exempt (the
endpoint is external and validated at connect time).
"""
if self.config.get("cdp_endpoint"):
return None
try:
from agent.tools.browser.browser_env import capability_summary
summary = capability_summary(self.config)
except Exception as e:
logger.debug(f"[Browser] capability probe failed: {e}")
return None
if summary.get("ready"):
return None
# Desktop clients (dev or packaged) have no `cow` CLI — onboard via the
# in-chat `/install-browser` command. Source / web / server installs use
# the `cow install-browser` terminal command.
install_hint = (
"reply `/install-browser`" if summary.get("is_desktop")
else "run `cow install-browser` in a terminal"
)
return ToolResult.fail(
f"Browser tool not ready. Ask the user to {install_hint} (installs a browser engine; "
"skipped automatically if Google Chrome is already installed). "
"Do not retry until the user confirms."
)
def execute(self, args: Dict[str, Any]) -> ToolResult:
action = args.get("action", "").strip().lower()
if not action:
@@ -195,6 +229,13 @@ class BrowserTool(BaseTool):
valid = ", ".join(sorted(self._ACTION_MAP.keys()))
return ToolResult.fail(f"Unknown action '{action}'. Valid actions: {valid}")
# Preflight: on desktop the playwright package is bundled but the browser
# binary may be missing; return actionable onboarding instead of a cryptic
# launch failure.
not_ready = self._check_engine_ready()
if not_ready is not None:
return not_ready
try:
return handler(self, args)
except Exception as e:

View File

@@ -1139,6 +1139,33 @@ function createMd() {
return hljsLib.highlightAuto(str).value;
}
});
// 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 text.
var GREEDY_LINK_CUT = /[*\u3000-\u303F\uFF00-\uFFEF]/;
md.core.ruler.after('linkify', 'fix_greedy_linkify', function(state) {
for (var b = 0; b < state.tokens.length; b++) {
var blk = state.tokens[b];
if (blk.type !== 'inline' || !blk.children) continue;
var ch = blk.children;
for (var i = 0; i < ch.length; i++) {
var open = ch[i];
if (open.type !== 'link_open' || open.markup !== 'linkify') continue;
var textTok = ch[i + 1], close = ch[i + 2];
if (!textTok || textTok.type !== 'text' || !close || close.type !== 'link_close') continue;
var idx = textTok.content.search(GREEDY_LINK_CUT);
if (idx < 0) continue;
var keep = textTok.content.slice(0, idx);
var spill = textTok.content.slice(idx);
textTok.content = keep;
open.attrSet('href', keep);
var spillTok = new state.Token('text', '', 0);
spillTok.content = spill;
ch.splice(i + 3, 0, spillTok);
}
}
});
const defaultLinkOpen = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};

View File

@@ -1700,11 +1700,10 @@ class ConfigHandler:
_RECOMMENDED_MODELS = [
const.DEEPSEEK_V4_FLASH, const.DEEPSEEK_V4_PRO,
const.MINIMAX_M3, const.MINIMAX_M2_7_HIGHSPEED, const.MINIMAX_M2_7,
# claude-sonnet-5 is the Claude default; claude-fable-5 is dropped
# from this web console list for now.
const.CLAUDE_SONNET_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS,
# claude-sonnet-5 is the Claude default; claude-fable-5 follows right after it.
const.CLAUDE_SONNET_5, const.CLAUDE_FABLE_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS,
const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE,
const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o,
const.GPT_56_LUNA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o,
const.GLM_5_2, const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7,
const.QWEN37_PLUS, const.QWEN37_MAX, const.QWEN36_PLUS,
const.DOUBAO_SEED_2_1_PRO, const.DOUBAO_SEED_2_1_TURBO, const.DOUBAO_SEED_2_CODE,
@@ -1747,7 +1746,7 @@ class ConfigHandler:
"api_base_key": "claude_api_base",
"api_base_default": "https://api.anthropic.com/v1",
"api_base_placeholder": _PLACEHOLDER_V1,
"models": [const.CLAUDE_SONNET_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS],
"models": [const.CLAUDE_SONNET_5, const.CLAUDE_FABLE_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS],
}),
("gemini", {
"label": "Gemini",
@@ -1763,7 +1762,7 @@ class ConfigHandler:
"api_base_key": "open_ai_api_base",
"api_base_default": "https://api.openai.com/v1",
"api_base_placeholder": _PLACEHOLDER_V1,
"models": [const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o],
"models": [const.GPT_56_LUNA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o],
}),
("zhipu", {
"label": {"zh": "智谱AI", "en": "GLM"},
@@ -2339,9 +2338,12 @@ class ModelsHandler:
# Anything not listed here intentionally hides the model dropdown so
# users cannot pin a chat-only model and silently get a 4xx at runtime.
_VISION_PROVIDER_MODELS = {
# OpenAI ordering matches the recommended GPT-5.4 family first, then
# OpenAI ordering puts the GPT-5.6 family first, then GPT-5.5/5.4,
# GPT-5 and the GPT-4.1/4o backstops.
"openai": [
const.GPT_56_LUNA,
const.GPT_56_TERRA,
const.GPT_56_SOL,
const.GPT_55,
const.GPT_54,
const.GPT_54_MINI,
@@ -2354,7 +2356,7 @@ class ModelsHandler:
"doubao": [const.DOUBAO_SEED_2_1_PRO, const.DOUBAO_SEED_2_1_TURBO, const.DOUBAO_SEED_2_PRO],
"moonshot": [const.KIMI_K2_6],
"dashscope": [const.QWEN37_PLUS, const.QWEN36_PLUS],
"claudeAPI": [const.CLAUDE_SONNET_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS],
"claudeAPI": [const.CLAUDE_SONNET_5, const.CLAUDE_FABLE_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS],
"gemini": [const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
"qianfan": [const.ERNIE_45_TURBO_VL],
# Zhipu's bot hard-codes the call to glm-5v-turbo regardless of what
@@ -2378,6 +2380,7 @@ class ModelsHandler:
const.DOUBAO_SEED_2_1_PRO,
const.KIMI_K2_6,
const.CLAUDE_SONNET_5,
const.CLAUDE_FABLE_5,
const.GEMINI_31_FLASH_LITE_PRE,
],
# Custom OpenAI-compatible providers have no preset list — model

View File

@@ -88,6 +88,69 @@ def _pip_install(package_spec: str, stream: StreamFn) -> int:
return ret
def _is_frozen() -> bool:
"""True when running inside a PyInstaller-frozen bundle (desktop backend).
In this mode ``sys.executable`` is the frozen exe (no pip / no ``-m``), so
playwright is already bundled and we only need to download the browser
binary in-process rather than pip-installing anything.
"""
return bool(getattr(sys, "frozen", False))
def _playwright_cli(args: list, env: Optional[dict] = None) -> int:
"""Invoke the Playwright CLI, working in both source and frozen builds.
Source builds shell out to ``python -m playwright <args>``. Frozen builds
can't use ``-m`` (the exe isn't a Python interpreter), so we call
Playwright's driver entrypoint in-process instead. ``env`` overrides are
applied to os.environ for the duration of the call (frozen path) or passed
through to the subprocess (source path).
"""
if not _is_frozen():
cmd = [sys.executable, "-m", "playwright"] + args
return subprocess.call(cmd, env=env)
# Frozen: run the bundled Playwright driver in-process. compute_driver_executable
# returns the Node driver shipped inside the bundle; we spawn it directly.
prev_env = {}
if env:
for k, v in env.items():
prev_env[k] = os.environ.get(k)
os.environ[k] = v
try:
from playwright._impl._driver import compute_driver_executable, get_driver_env
driver = compute_driver_executable()
# compute_driver_executable may return a tuple (node, cli.js) on newer
# Playwright, or a single path on older ones.
if isinstance(driver, (list, tuple)):
cmd = list(driver) + args
else:
cmd = [str(driver)] + args
# get_driver_env() snapshots os.environ, which we've already patched with
# the caller's overrides (PLAYWRIGHT_BROWSERS_PATH / DOWNLOAD_HOST) above,
# so mirror + pinned browsers dir are honored here too.
return subprocess.call(cmd, env=get_driver_env())
except Exception as e:
# Last resort: try the module main via runpy (works if the frozen build
# kept playwright.__main__ importable).
try:
import runpy
sys.argv = ["playwright"] + args
runpy.run_module("playwright", run_name="__main__")
return 0
except SystemExit as se:
return int(se.code or 0)
except Exception:
return 1
finally:
for k, v in prev_env.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
def _default_stream(msg: str, fg: Optional[str] = None) -> None:
"""CLI: colored click output."""
if fg == "yellow":
@@ -129,6 +192,7 @@ def run_install_browser(
stream = stream or _default_stream
python = sys.executable
legacy_mode = False
frozen = _is_frozen()
_phase(on_phase, _t(
"🔧 开始安装浏览器工具依赖(约几分钟,请耐心等待)…",
@@ -159,7 +223,7 @@ def run_install_browser(
# Windows-only: greenlet 3.2.x ships no Windows wheel, so pip would build it
# from source (needs MSVC) and fail. Pre-install 3.1.x (has win wheels for
# py3.7-3.13) which still satisfies playwright's greenlet>=3.1.1,<4.
if sys.platform == "win32":
if sys.platform == "win32" and not frozen:
stream("[1/3] Pre-installing greenlet (prebuilt wheel) for Windows...", "yellow")
ret = subprocess.call(
[python, "-m", "pip", "install", "--only-binary=:all:", "greenlet>=3.1.1,<3.2"]
@@ -172,22 +236,52 @@ def run_install_browser(
"yellow",
)
_phase(on_phase, _t("📦 [1/3] 正在安装 Playwright Python 包…", "📦 [1/3] Installing Playwright Python package…"))
stream("[1/3] Installing playwright Python package...", "yellow")
ret = _pip_install(f"playwright=={target_version}", stream)
if ret != 0:
stream("Failed to install playwright package.", "red")
_phase(on_phase, _t("❌ [1/3] Playwright Python 包安装失败。", "❌ [1/3] Failed to install Playwright Python package."))
return 1
if frozen:
# Desktop bundle: playwright is already shipped inside the app; there is
# no pip and nothing to install. Skip straight to downloading Chromium.
installed = _get_installed_version()
stream(f"[1/3] Playwright is bundled ({installed or 'ok'}), skipping pip install.", "green")
_phase(on_phase, _t(
"✅ [1/3] Playwright 已内置于客户端,跳过安装。",
"✅ [1/3] Playwright is bundled in the app; skipping install.",
))
else:
_phase(on_phase, _t("📦 [1/3] 正在安装 Playwright Python 包…", "📦 [1/3] Installing Playwright Python package…"))
stream("[1/3] Installing playwright Python package...", "yellow")
ret = _pip_install(f"playwright=={target_version}", stream)
if ret != 0:
stream("Failed to install playwright package.", "red")
_phase(on_phase, _t("❌ [1/3] Playwright Python 包安装失败。", "❌ [1/3] Failed to install Playwright Python package."))
return 1
installed = _get_installed_version()
if installed:
stream(f" playwright {installed} installed.", "green")
stream("")
_phase(on_phase, _t(
f"✅ [1/3] Playwright 包已安装({installed or target_version})。",
f"✅ [1/3] Playwright package installed ({installed or target_version}).",
))
installed = _get_installed_version()
if installed:
stream(f" playwright {installed} installed.", "green")
stream("")
_phase(on_phase, _t(
f"✅ [1/3] Playwright 包已安装({installed or target_version})。",
f"✅ [1/3] Playwright package installed ({installed or target_version}).",
))
# With playwright available, prefer the user's system Chrome/Edge: the browser
# tool drives it directly (channel="chrome"/"msedge"), so we can skip the heavy
# ~150MB Chromium download entirely. Applies to every runtime (desktop, web,
# source) — only headless Linux servers, which usually lack a system browser,
# fall through to the download below. Honors prefer_system_browser via
# resolve_engine, so users who force downloaded Chromium still get it.
try:
from agent.tools.browser import browser_env
summary = browser_env.capability_summary()
if summary.get("ready") and summary.get("engine", {}).get("mode") == "system-chrome":
sc = summary.get("system_chrome") or {}
stream(f"System browser detected ({sc.get('channel')}), skipping Chromium download.", "green")
_phase(on_phase, _t(
f"✅ 检测到系统浏览器({sc.get('channel')}),无需下载 Chromium浏览器工具已就绪。",
f"✅ Detected system browser ({sc.get('channel')}); no Chromium download needed, browser tool is ready.",
))
return 0
except Exception as e:
stream(f" (system browser probe skipped: {e})", None)
if sys.platform == "linux":
_phase(on_phase, _t(
@@ -195,7 +289,7 @@ def run_install_browser(
"🔧 [2/3] Installing Linux system deps and a lightweight CJK font (WenQuanYi Zen Hei; some steps may need sudo)…",
))
stream("[2/3] Installing system dependencies (Linux)...", "yellow")
ret = subprocess.call([python, "-m", "playwright", "install-deps", "chromium"])
ret = _playwright_cli(["install-deps", "chromium"])
if ret != 0:
stream(
" Could not auto-install system deps (may need sudo).\n"
@@ -238,12 +332,12 @@ def run_install_browser(
"🌐 [3/3] Downloading and installing Chromium (large download, please wait)…",
))
stream("[3/3] Installing Chromium browser...", "yellow")
cmd = [python, "-m", "playwright", "install", "chromium"]
pw_args = ["install", "chromium"]
if _is_headless_linux() and not legacy_mode:
ver = _version_tuple(installed or "")
if ver >= (1, 57, 0):
cmd.append("--only-shell")
pw_args.append("--only-shell")
stream(" (headless shell for Linux server)", None)
else:
stream(" (full Chromium)", None)
@@ -251,6 +345,15 @@ def run_install_browser(
stream(" (full browser for Linux desktop)", None)
env = os.environ.copy()
# Pin the download location so it survives desktop app updates and matches
# what the runtime looks up (see browser_env.browsers_download_dir()).
try:
from agent.tools.browser.browser_env import browsers_download_dir
env["PLAYWRIGHT_BROWSERS_PATH"] = browsers_download_dir()
stream(f" (browsers dir: {env['PLAYWRIGHT_BROWSERS_PATH']})", None)
except Exception:
pass
use_mirror = _is_china_network()
if use_mirror:
env["PLAYWRIGHT_DOWNLOAD_HOST"] = CHINA_MIRROR
@@ -260,7 +363,7 @@ def run_install_browser(
"📡 Detected a China pip mirror; Chromium will be downloaded from the China mirror first.",
))
ret = subprocess.call(cmd, env=env)
ret = _playwright_cli(pw_args, env=env)
if ret != 0 and use_mirror:
stream(" Mirror download failed, retrying with official CDN...", "yellow")
@@ -268,9 +371,9 @@ def run_install_browser(
"⚠️ 镜像下载失败,正在改用官方源重试…",
"⚠️ Mirror download failed; retrying with the official CDN…",
))
env_no_mirror = os.environ.copy()
env_no_mirror = dict(env)
env_no_mirror.pop("PLAYWRIGHT_DOWNLOAD_HOST", None)
ret = subprocess.call(cmd, env=env_no_mirror)
ret = _playwright_cli(pw_args, env=env_no_mirror)
if ret != 0:
stream("Failed to install Chromium.", "red")
@@ -282,10 +385,18 @@ def run_install_browser(
stream("Verifying browser installation...", None)
_phase(on_phase, _t("🔍 正在验证 Playwright 能否正常加载…", "🔍 Verifying that Playwright loads correctly…"))
ret = subprocess.call(
[python, "-c", "from playwright.sync_api import sync_playwright; print('OK')"],
stderr=subprocess.DEVNULL,
)
if frozen:
# Frozen: no child interpreter to spawn; import in-process instead.
try:
from playwright.sync_api import sync_playwright # noqa: F401
ret = 0
except Exception:
ret = 1
else:
ret = subprocess.call(
[python, "-c", "from playwright.sync_api import sync_playwright; print('OK')"],
stderr=subprocess.DEVNULL,
)
if ret != 0:
stream(
" Warning: playwright import failed. Browser tool may not work on this system.\n"

View File

@@ -30,7 +30,7 @@ CLAUDE_35_SONNET = "claude-3-5-sonnet-latest" # "latest" tag always points to t
CLAUDE_35_SONNET_1022 = "claude-3-5-sonnet-20241022" # dated name pinned to a specific release
CLAUDE_35_SONNET_0620 = "claude-3-5-sonnet-20240620"
CLAUDE_4_OPUS = "claude-opus-4-0"
CLAUDE_FABLE_5 = "claude-fable-5" # Claude Fable 5 (often restricted by policy)
CLAUDE_FABLE_5 = "claude-fable-5" # Claude Fable 5 - alternative Claude 5 flagship
CLAUDE_4_8_OPUS = "claude-opus-4-8" # Claude Opus 4.8 - Agent recommended model
CLAUDE_4_7_OPUS = "claude-opus-4-7" # Claude Opus 4.7
CLAUDE_4_6_OPUS = "claude-opus-4-6" # Claude Opus 4.6
@@ -80,6 +80,9 @@ GPT_54 = "gpt-5.4" # GPT-5.4 - Agent recommended model
GPT_54_MINI = "gpt-5.4-mini"
GPT_54_NANO = "gpt-5.4-nano"
GPT_55 = "gpt-5.5" # GPT-5.5 - top-tier (expensive), not default
GPT_56_LUNA = "gpt-5.6-luna" # GPT-5.6 Luna - default flagship model for GPT
GPT_56_TERRA = "gpt-5.6-terra" # GPT-5.6 Terra
GPT_56_SOL = "gpt-5.6-sol" # GPT-5.6 Sol - highest intelligence, higher latency
O1 = "o1-preview"
O1_MINI = "o1-mini"
WHISPER_1 = "whisper-1"
@@ -200,7 +203,7 @@ MODEL_LIST = [
MIMO, MIMO_V2_5_PRO, MIMO_V2_5, MIMO_V2_PRO, MIMO_V2_OMNI, MIMO_V2_FLASH,
# Claude
CLAUDE_SONNET_5, CLAUDE3, CLAUDE_4_8_OPUS, CLAUDE_4_7_OPUS, CLAUDE_FABLE_5, CLAUDE_4_6_SONNET, CLAUDE_4_6_OPUS, CLAUDE_4_OPUS, CLAUDE_4_5_SONNET, CLAUDE_4_SONNET, CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229,
CLAUDE_SONNET_5, CLAUDE_FABLE_5, CLAUDE3, CLAUDE_4_8_OPUS, CLAUDE_4_7_OPUS, CLAUDE_4_6_SONNET, CLAUDE_4_6_OPUS, CLAUDE_4_OPUS, CLAUDE_4_5_SONNET, CLAUDE_4_SONNET, CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229,
CLAUDE_35_SONNET, CLAUDE_35_SONNET_1022, CLAUDE_35_SONNET_0620, CLAUDE_3_SONNET, CLAUDE_3_HAIKU,
"claude", "claude-3-haiku", "claude-3-sonnet", "claude-3-opus", "claude-3.5-sonnet",
@@ -214,6 +217,7 @@ MODEL_LIST = [
GPT4_TURBO, GPT4_TURBO_PREVIEW, GPT4_TURBO_01_25, GPT4_TURBO_11_06, GPT4_TURBO_04_09,
GPT_4o, GPT_4O_0806, GPT_4o_MINI,
GPT_41, GPT_41_MINI, GPT_41_NANO,
GPT_56_LUNA, GPT_56_TERRA, GPT_56_SOL,
GPT_5, GPT_5_MINI, GPT_5_NANO,
GPT_54, GPT_55, GPT_54_MINI, GPT_54_NANO,
O1, O1_MINI,

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

@@ -0,0 +1,122 @@
/**
* Dynamic electron-builder config for WINDOWS code signing.
*
* Mirrors electron-builder.js (which handles mac.binaries) but for Windows.
* It wires a signing CLI into electron-builder so that every .exe is signed,
* with the private key kept in hardware per the post-2023 code-signing rules.
*
* A SINGLE sign hook (win.signtoolOptions.sign) covers everything: electron-
* builder calls it for EVERY .exe it processes, which includes the app
* launcher, the packaged PyInstaller backend (extraResources/backend/
* cowagent-backend.exe) and the NSIS installer. We deliberately do NOT add an
* afterPack pass — that would sign the backend a second time and waste a paid
* signing call on every release.
*
* PRIVACY: the CLI path and all credentials come from env vars only. Nothing in
* this file (or the public workflow) is hardcoded, so a public repo never leaks
* any signing configuration.
*
* DRY-RUN / SKIP: when SIGNTOOL_CERT_CODE is absent we skip signing entirely
* (unsigned dev/dry builds keep working). When COW_SIGN_DRY_RUN=1 we pass
* --dry-run so the WHOLE pipeline can be validated in CI with a self-signed
* cert, WITHOUT a real certificate and WITHOUT consuming any signing quota.
*/
const { execFileSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const config = require('./package.json').build
// Absolute path to the signing CLI on the runner. Injected by CI so this file
// never hardcodes a download URL. e.g. C:\signtool\signtool.exe
const SIGNTOOL = process.env.SIGNTOOL_PATH || ''
// Dry-run validates the pipeline with a self-signed cert (no quota, no real
// cert needed). Any truthy value enables it.
const DRY_RUN = !!process.env.COW_SIGN_DRY_RUN
// In dry-run the CLI still requires these flags to be NON-EMPTY (it validates
// presence, not the value, and signs with a self-signed cert). So when no real
// credentials are provided during a dry-run, fall back to harmless placeholders
// to satisfy the CLI's arg check. Real runs pass the actual secrets through.
const PLACEHOLDER = DRY_RUN ? 'dry-run' : ''
const ACCESS_KEY = process.env.SIGNTOOL_ACCESS_KEY || PLACEHOLDER
const ACCESS_SECRET = process.env.SIGNTOOL_ACCESS_SECRET || PLACEHOLDER
const CERT_CODE = process.env.SIGNTOOL_CERT_CODE || PLACEHOLDER
// RFC3161 timestamp server for SHA256. Microsoft's is reliable from CI runners
// worldwide; overridable via env if needed.
const TIMESTAMP = process.env.SIGNTOOL_TIMESTAMP || 'http://timestamp.acs.microsoft.com'
// Signing is possible when we have the CLI plus either a real cert code or
// explicit dry-run mode (dry-run accepts placeholder credentials).
function canSign() {
if (!SIGNTOOL || !fs.existsSync(SIGNTOOL)) return false
if (DRY_RUN) return true
return !!(ACCESS_KEY && ACCESS_SECRET && CERT_CODE)
}
/**
* Sign a single file in place using the signing CLI. The CLI writes to a
* separate --out path (it refuses to overwrite an existing file), so we sign to
* a temp file and atomically move it back over the original.
*/
function signFile(filePath) {
const tmpOut = `${filePath}.signed`
// Remove a stale temp from a previous failed run (CLI errors if --out exists).
try {
if (fs.existsSync(tmpOut)) fs.rmSync(tmpOut)
} catch {
/* ignore */
}
const args = [
'sign',
...(DRY_RUN ? ['--dry-run'] : []),
`--access-key=${ACCESS_KEY}`,
`--access-secret=${ACCESS_SECRET}`,
`--cert-code=${CERT_CODE}`,
`--file=${filePath}`,
`--out=${tmpOut}`,
'--sha1=false',
'--sha2=true',
'--timestamp-rfc3161',
TIMESTAMP,
]
// Never print credentials: log only the file being signed.
console.log(`[win-sign] signing ${path.basename(filePath)}${DRY_RUN ? ' (dry-run)' : ''}`)
execFileSync(SIGNTOOL, args, { stdio: ['ignore', 'inherit', 'inherit'] })
if (!fs.existsSync(tmpOut)) {
throw new Error(`[win-sign] signed output not produced for ${filePath}`)
}
// Replace the original with the signed copy.
fs.rmSync(filePath)
fs.renameSync(tmpOut, filePath)
}
// electron-builder calls this for each artifact it generates (app exe, NSIS
// installer, uninstaller). Signature: (configuration) => void, where
// configuration.path is the file to sign.
async function customSign(configuration) {
if (!canSign()) {
console.warn('[win-sign] signing skipped (no signtool/credentials)')
return
}
signFile(configuration.path)
}
// Extend the base config: attach the sign hook. Only meaningful on Windows
// builds (this config is only passed via --config on the win matrix leg).
//
// electron-builder invokes customSign for EVERY .exe it touches — that already
// includes the packaged backend (extraResources/backend/cowagent-backend.exe)
// and the NSIS installer, not just the app launcher. So there's no separate
// afterPack pass: adding one would sign the backend twice (wasting a paid
// signing call per release). Nested PyInstaller .dll/.pyd files are left
// unsigned, which Windows Authenticode tolerates (unlike macOS, it doesn't
// require deep-signing every nested lib — a signed top-level exe is enough for
// SmartScreen/Defender to attribute the publisher).
config.win = { ...config.win, signtoolOptions: { sign: customSign, signingHashAlgorithms: ['sha256'] } }
module.exports = config

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',

View File

@@ -1,10 +1,10 @@
<p align="center"><img src="https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="420" /></p>
<p align="center">
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release"></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars"></a>
<a href="https://docs.cowagent.ai/ja"><img src="https://img.shields.io/badge/%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="ドキュメント"></a>
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release" /></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT" /></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars" /></a>
<a href="https://docs.cowagent.ai/ja"><img src="https://img.shields.io/badge/%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="ドキュメント" /></a>
</p>
<p align="center">
@@ -106,8 +106,8 @@ CowAgent は主要な LLM プロバイダーすべてに対応しています。
| プロバイダー | 代表的なモデル | チャット | 画像認識 | 画像生成 | ASR | TTS | Embedding |
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [Claude](https://docs.cowagent.ai/ja/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [OpenAI](https://docs.cowagent.ai/ja/models/openai) | gpt-5.5、o シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Claude](https://docs.cowagent.ai/ja/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [OpenAI](https://docs.cowagent.ai/ja/models/openai) | gpt-5.6 シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Gemini](https://docs.cowagent.ai/ja/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [DeepSeek](https://docs.cowagent.ai/ja/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [Qwen](https://docs.cowagent.ai/ja/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
@@ -230,7 +230,7 @@ CowAgent は主要な LLM プロバイダーすべてに対応しています。
GitHub で [Issue を報告](https://github.com/zhayujie/CowAgent/issues) するか、下記 QR コードをスキャンして WeChat コミュニティに参加してください:
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png">
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png" />
<br/>

View File

@@ -20,7 +20,7 @@ Claude は Anthropic が提供するモデルで、テキスト対話と画像
| パラメータ | 説明 |
| --- | --- |
| `model` | `claude-sonnet-5`、`claude-opus-4-8`、`claude-opus-4-7`、`claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` などをサポート。詳細は [公式モデル一覧](https://docs.anthropic.com/en/docs/about-claude/models/overview) を参照 |
| `model` | `claude-sonnet-5`、`claude-fable-5`、`claude-opus-4-8`、`claude-opus-4-7`、`claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` などをサポート。詳細は [公式モデル一覧](https://docs.anthropic.com/en/docs/about-claude/models/overview) を参照 |
| `claude_api_key` | [Claude コンソール](https://console.anthropic.com/settings/keys) で作成 |
| `claude_api_base` | 任意。デフォルトは `https://api.anthropic.com/v1`。サードパーティのプロキシに変更可能 |
@@ -29,6 +29,7 @@ Claude は Anthropic が提供するモデルで、テキスト対話と画像
| モデル | 用途 |
| --- | --- |
| `claude-sonnet-5` | 最新フラッグシップ。デフォルト推奨モデルで、推論性能とコストのバランスが最も良い |
| `claude-fable-5` | Claude 5 シリーズのもう一つのフラッグシップモデル |
| `claude-opus-4-8` | 前世代フラッグシップ。推論性能が最も高いが、価格は高め |
| `claude-opus-4-7` | より以前の Opus フラッグシップ |
| `claude-sonnet-4-6` | コストパフォーマンスと速度のバランスが良く、コストも低い |

View File

@@ -6,7 +6,7 @@ description: CowAgent がサポートするモデルベンダーと機能マト
CowAgent は国内外の主要ベンダーの大規模言語モデルをサポートしており、モデル接続の実装はプロジェクトの `models/` ディレクトリにあります。テキスト対話に加えて、一部のベンダーは画像理解、画像生成、音声認識、音声合成、ベクトルなどの機能も提供しており、Agent フローの中で必要に応じて呼び出すことができます。
<Note>
Agent モードでは、効果とコストのバランスを考慮して以下のモデルの利用を推奨しますdeepseek-v4-flash、MiniMax-M3、claude-sonnet-5、gemini-3.5-flash、glm-5.2、qwen3.7-plus、kimi-k2.7-code、ernie-5.1。
Agent モードでは、効果とコストのバランスを考慮して以下のモデルの利用を推奨しますdeepseek-v4-flash、MiniMax-M3、claude-sonnet-5、claude-fable-5、gemini-3.5-flash、glm-5.2、qwen3.7-plus、kimi-k2.7-code、ernie-5.1。
同時に [LinkAI](https://link-ai.tech) プラットフォームの API もサポートしており、1 つの Key で複数ベンダーを柔軟に切り替えられ、ナレッジベース、ワークフロー、プラグインなどの機能も付属しています。
</Note>
@@ -20,9 +20,9 @@ CowAgent は国内外の主要ベンダーの大規模言語モデルをサポ
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Claude](/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [Gemini](/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](/models/openai) | gpt-5.5、o シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [OpenAI](/models/openai) | gpt-5.6 シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Zhipu GLM](/models/glm) | glm-5.2、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [Tongyi Qianwen](/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Doubao](/models/doubao) | doubao-seed-2.1 シリーズ | ✅ | ✅ | ✅ | | | ✅ |

View File

@@ -40,7 +40,7 @@ description: LinkAI プラットフォーム経由でテキスト、ビジョン
}
```
選択可能なモデル:`gpt-4.1-mini`、`gpt-5.4-mini`、`qwen3.7-plus`、`doubao-seed-2-1-pro-260628`、`kimi-k2.6`、`claude-sonnet-5`、`gemini-3.1-flash-lite-preview` など。
選択可能なモデル:`gpt-4.1-mini`、`gpt-5.4-mini`、`qwen3.7-plus`、`doubao-seed-2-1-pro-260628`、`kimi-k2.6`、`claude-sonnet-5`、`claude-fable-5`、`gemini-3.1-flash-lite-preview` など。
## 画像生成

View File

@@ -14,7 +14,7 @@ OpenAI は最も広範な機能をカバーするベンダーで、テキスト
```json
{
"model": "gpt-5.5",
"model": "gpt-5.6-luna",
"open_ai_api_key": "YOUR_API_KEY",
"open_ai_api_base": "https://api.openai.com/v1"
}
@@ -22,7 +22,7 @@ OpenAI は最も広範な機能をカバーするベンダーで、テキスト
| パラメータ | 説明 |
| --- | --- |
| `model` | OpenAI API の [model パラメータ](https://platform.openai.com/docs/models) と同じです。`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` シリーズ、`gpt-4.1`、o シリーズなどをサポート。Agent モードのデフォルトは `gpt-5.5`、コストパフォーマンスを重視する場合は `gpt-5.4` に変更可能 |
| `model` | OpenAI API の [model パラメータ](https://platform.openai.com/docs/models) と同じです。`gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` シリーズ、`gpt-4.1` などをサポート。Agent モードのデフォルトは `gpt-5.6-luna`、コストパフォーマンスを重視する場合は `gpt-5.4` に変更可能 |
| `open_ai_api_key` | [OpenAI プラットフォーム](https://platform.openai.com/api-keys) で作成 |
| `open_ai_api_base` | 任意。サードパーティのプロキシに接続するために変更可能 |
| `bot_type` | OpenAI 公式モデルを使用する場合は不要。互換プロトコルでベンダーモデルに接続する場合は `openai` に設定 |

View File

@@ -49,6 +49,12 @@ Chromiumブラウザを操作してWebページのナビゲーション、要素
2. ブラウザToolは依存関係が大きい約300MBため、不要な場合はインストールを省略できます。軽量なWebコンテンツ取得には `web_fetch` Toolをご利用ください。
</Note>
<Note>
**デスクトップクライアント利用者**playwright はインストーラーに同梱済みで、別途インストールは不要です。ブラウザToolの初回利用時
- **Google Chrome / Edge** がインストールされていれば、システムのブラウザを直接駆動し、**ダウンロード不要**です(推奨);
- 未インストールの場合は、チャットで `/install-browser` を送信すると、軽量なブラウザエンジンを `~/.cow` にダウンロードします。
</Note>
## ワークフロー
Agentがブラウザを使う典型的な流れ
@@ -105,6 +111,15 @@ Agentがブラウザを使う典型的な流れ
}
```
## ブラウザエンジン
ブラウザエンジンは自動選択され、設定は不要です:
1. マシンに **Google Chrome / Edge** が検出された場合、システムのブラウザを直接駆動し、**Chromium のダウンロードは不要**で、実際のブラウザフィンガープリントを使用します;
2. それ以外の場合は、`install-browser` で `~/.cow` にダウンロードした Chromium エンジンにフォールバックします。
どちらも以下のログイン状態の永続化を使用し、挙動は同一です。
## ログイン状態の永続化
**対象サイトに一度ログインすれば、Agentは以降そのまま利用できます。** 2つの方法があります

View File

@@ -20,7 +20,7 @@ Claude is provided by Anthropic and supports both text chat and image understand
| Parameter | Description |
| --- | --- |
| `model` | Supports `claude-sonnet-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4-0`, `claude-3-5-sonnet-latest`, etc. See [official models](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `model` | Supports `claude-sonnet-5`, `claude-fable-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4-0`, `claude-3-5-sonnet-latest`, etc. See [official models](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `claude_api_key` | Create one in the [Claude Console](https://console.anthropic.com/settings/keys) |
| `claude_api_base` | Optional, defaults to `https://api.anthropic.com/v1`. Can be changed to a third-party proxy |
@@ -29,6 +29,7 @@ Claude is provided by Anthropic and supports both text chat and image understand
| Model | Use Case |
| --- | --- |
| `claude-sonnet-5` | Latest flagship; default recommendation, best balance of reasoning quality and cost |
| `claude-fable-5` | Alternative flagship in the Claude 5 family |
| `claude-opus-4-8` | Previous flagship with the strongest reasoning, at a higher price |
| `claude-opus-4-7` | Earlier Opus flagship |
| `claude-sonnet-4-6` | Balanced cost and speed, lower cost |

View File

@@ -13,9 +13,9 @@ A snapshot of each provider's capabilities. "Text" refers to the main chat model
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Claude](/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [Gemini](/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](/models/openai) | gpt-5.5, o-series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [OpenAI](/models/openai) | gpt-5.6 series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [GLM](/models/glm) | glm-5.2, glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [Qwen](/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Doubao](/models/doubao) | doubao-seed-2.1 series | ✅ | ✅ | ✅ | | | ✅ |

View File

@@ -40,7 +40,7 @@ Once configured, the Agent's Vision tool automatically calls multimodal models v
}
```
Available models: `gpt-4.1-mini`, `gpt-5.4-mini`, `qwen3.7-plus`, `doubao-seed-2-1-pro-260628`, `kimi-k2.6`, `claude-sonnet-5`, `gemini-3.1-flash-lite-preview`, etc.
Available models: `gpt-4.1-mini`, `gpt-5.4-mini`, `qwen3.7-plus`, `doubao-seed-2-1-pro-260628`, `kimi-k2.6`, `claude-sonnet-5`, `claude-fable-5`, `gemini-3.1-flash-lite-preview`, etc.
## Image Generation

View File

@@ -14,7 +14,7 @@ OpenAI offers the most complete coverage and can simultaneously serve text chat,
```json
{
"model": "gpt-5.5",
"model": "gpt-5.6-luna",
"open_ai_api_key": "YOUR_API_KEY",
"open_ai_api_base": "https://api.openai.com/v1"
}
@@ -22,7 +22,7 @@ OpenAI offers the most complete coverage and can simultaneously serve text chat,
| Parameter | Description |
| --- | --- |
| `model` | Same as OpenAI's [model parameter](https://platform.openai.com/docs/models); supports `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, the `gpt-5` series, `gpt-4.1`, the o-series, etc. Agent mode defaults to `gpt-5.5`; use `gpt-5.4` for better cost-efficiency |
| `model` | Same as OpenAI's [model parameter](https://platform.openai.com/docs/models); supports `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, the `gpt-5` series, `gpt-4.1`, etc. Agent mode defaults to `gpt-5.6-luna`; use `gpt-5.4` for better cost-efficiency |
| `open_ai_api_key` | Create one on the [OpenAI Platform](https://platform.openai.com/api-keys) |
| `open_ai_api_base` | Optional; change it to access a third-party proxy |
| `bot_type` | Not required when using OpenAI's official models; set to `openai` when accessing other providers via the compatible protocol |

View File

@@ -49,6 +49,12 @@ Control a Chromium browser for web navigation, element interaction and content e
2. The browser tool has heavy dependencies (~300MB) and is optional. For lightweight web content retrieval, use the `web_fetch` tool.
</Note>
<Note>
**Desktop client users**: playwright is bundled in the installer, no separate install needed. On first use of the browser tool:
- If **Google Chrome / Edge** is installed, it drives the system browser directly with **no download** (recommended);
- Otherwise, send `/install-browser` in chat to download a lightweight browser engine into `~/.cow`.
</Note>
## Workflow
A typical browser workflow for the Agent:
@@ -105,6 +111,15 @@ You can override it in `config.json`:
}
```
## Browser Engine
The browser engine is selected automatically, no configuration needed:
1. If **Google Chrome / Edge** is detected on the machine, it drives the system browser directly, with **no Chromium download**, using real browser fingerprints;
2. Otherwise it falls back to the Chromium engine downloaded into `~/.cow` via `install-browser`.
Both use the persistent login below and behave identically.
## Persistent Login
**Log in to a target site once and the Agent can keep using it.** Two ways are supported:

View File

@@ -1,10 +1,10 @@
<p align="center"><img src= "https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="420" /></p>
<p align="center">
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release"></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars"></a>
<a href="https://docs.cowagent.ai/zh"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="文件"></a>
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release" /></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT" /></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars" /></a>
<a href="https://docs.cowagent.ai/zh"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="文件" /></a>
</p>
<p align="center">
@@ -108,9 +108,9 @@ CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](https://docs.cowagent.ai/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](https://docs.cowagent.ai/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.5、o 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [智譜 GLM](https://docs.cowagent.ai/zh/models/glm) | glm-5.2、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [通義千問](https://docs.cowagent.ai/zh/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [豆包 Doubao](https://docs.cowagent.ai/zh/models/doubao) | doubao-seed-2.1 系列 | ✅ | ✅ | ✅ | | | ✅ |
@@ -233,7 +233,7 @@ CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像
掃碼加入微信開源交流群:
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png">
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png" />
也可透過以下方式獲取支援:
@@ -252,7 +252,7 @@ CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像
## 🏢 企業服務
<a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a>
<a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg" /></a>
> [LinkAI](https://link-ai.tech/) 是面向企業和個人的一站式 AI 智慧體平臺,為 CowAgent 提供雲端託管和企業級支援:
>
@@ -262,7 +262,7 @@ CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像
**產品諮詢和企業服務** 可聯絡產品客服:
<img width="130" src="https://cdn.link-ai.tech/portal/linkai-customer-service.png">
<img width="130" src="https://cdn.link-ai.tech/portal/linkai-customer-service.png" />
<br/>

View File

@@ -1,10 +1,10 @@
<p align="center"><img src= "https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="420" /></p>
<p align="center">
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release"></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars"></a>
<a href="https://docs.cowagent.ai/zh"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="文档"></a>
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release" /></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT" /></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars" /></a>
<a href="https://docs.cowagent.ai/zh"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="文档" /></a>
</p>
<p align="center">
@@ -108,9 +108,9 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](https://docs.cowagent.ai/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](https://docs.cowagent.ai/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.5、o 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [智谱 GLM](https://docs.cowagent.ai/zh/models/glm) | glm-5.2、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [通义千问](https://docs.cowagent.ai/zh/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [豆包 Doubao](https://docs.cowagent.ai/zh/models/doubao) | doubao-seed-2.1 系列 | ✅ | ✅ | ✅ | | | ✅ |
@@ -233,7 +233,7 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
扫码加入微信开源交流群:
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png">
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png" />
也可通过以下方式获取支持:
@@ -252,7 +252,7 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
## 🏢 企业服务
<a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a>
<a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg" /></a>
> [LinkAI](https://link-ai.tech/) 是面向企业和个人的一站式 AI 智能体平台,为 CowAgent 提供云端托管和企业级支持:
>
@@ -262,7 +262,7 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
**产品咨询和企业服务** 可联系产品客服:
<img width="130" src="https://cdn.link-ai.tech/portal/linkai-customer-service.png">
<img width="130" src="https://cdn.link-ai.tech/portal/linkai-customer-service.png" />
<br/>

View File

@@ -20,7 +20,7 @@ Claude 由 Anthropic 提供,支持文本对话与图像理解,主流 Sonnet
| 参数 | 说明 |
| --- | --- |
| `model` | 支持 `claude-sonnet-5`、`claude-opus-4-8`、`claude-opus-4-7`、`claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` 等,参考 [官方模型](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `model` | 支持 `claude-sonnet-5`、`claude-fable-5`、`claude-opus-4-8`、`claude-opus-4-7`、`claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` 等,参考 [官方模型](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `claude_api_key` | 在 [Claude 控制台](https://console.anthropic.com/settings/keys) 创建 |
| `claude_api_base` | 可选,默认为 `https://api.anthropic.com/v1`,可改为第三方代理 |
@@ -29,6 +29,7 @@ Claude 由 Anthropic 提供,支持文本对话与图像理解,主流 Sonnet
| 模型 | 适用场景 |
| --- | --- |
| `claude-sonnet-5` | 最新旗舰,默认推荐模型,推理效果与成本均衡最佳 |
| `claude-fable-5` | Claude 5 系列的另一款旗舰模型 |
| `claude-opus-4-8` | 上一代 Opus 旗舰,推理能力最强,价格较高 |
| `claude-opus-4-7` | 更早的 Opus 旗舰 |
| `claude-sonnet-4-6` | 性价比与速度平衡,成本更低 |

View File

@@ -14,9 +14,9 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](/zh/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Claude](/zh/models/claude) | claude-sonnet-5 / fable-5 | ✅ | ✅ | | | | |
| [Gemini](/zh/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](/zh/models/openai) | gpt-5.5、o 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [OpenAI](/zh/models/openai) | gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [智谱 GLM](/zh/models/glm) | glm-5.2、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [通义千问](/zh/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [豆包 Doubao](/zh/models/doubao) | doubao-seed-2.1 系列 | ✅ | ✅ | ✅ | | | ✅ |

View File

@@ -40,7 +40,7 @@ description: 通过 LinkAI 平台统一接入文本、视觉、图像、语音
}
```
可选模型:`gpt-4.1-mini`、`gpt-5.4-mini`、`qwen3.7-plus`、`doubao-seed-2-1-pro-260628`、`kimi-k2.6`、`claude-sonnet-5`、`gemini-3.1-flash-lite-preview` 等。
可选模型:`gpt-4.1-mini`、`gpt-5.4-mini`、`qwen3.7-plus`、`doubao-seed-2-1-pro-260628`、`kimi-k2.6`、`claude-sonnet-5`、`claude-fable-5`、`gemini-3.1-flash-lite-preview` 等。
## 图像生成

View File

@@ -14,7 +14,7 @@ OpenAI 是覆盖最完整的厂商,可同时承担文本对话、视觉理解
```json
{
"model": "gpt-5.5",
"model": "gpt-5.6-luna",
"open_ai_api_key": "YOUR_API_KEY",
"open_ai_api_base": "https://api.openai.com/v1"
}
@@ -22,7 +22,7 @@ OpenAI 是覆盖最完整的厂商,可同时承担文本对话、视觉理解
| 参数 | 说明 |
| --- | --- |
| `model` | 与 OpenAI 接口的 [model 参数](https://platform.openai.com/docs/models) 一致,支持 `gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` 系列、`gpt-4.1`、o 系列Agent 模式默认 `gpt-5.5`,追求性价比可改为 `gpt-5.4` |
| `model` | 与 OpenAI 接口的 [model 参数](https://platform.openai.com/docs/models) 一致,支持 `gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` 系列、`gpt-4.1` Agent 模式默认 `gpt-5.6-luna`,追求性价比可改为 `gpt-5.4` |
| `open_ai_api_key` | 在 [OpenAI 平台](https://platform.openai.com/api-keys) 创建 |
| `open_ai_api_base` | 可选,修改可接入第三方代理 |
| `bot_type` | 使用 OpenAI 官方模型时无需填写;通过兼容协议接入厂商模型时需设为 `openai` |

View File

@@ -49,6 +49,12 @@ description: 控制浏览器访问和操作网页
2. 浏览器工具依赖较重约300MB为可选安装。轻量的网页内容获取可使用 `web_fetch` 工具。
</Note>
<Note>
**桌面客户端用户**playwright 已内置于安装包,无需单独安装。首次使用浏览器工具时:
- 若系统已安装 **Google Chrome / Edge**,会直接驱动系统浏览器,**无需任何下载**(推荐);
- 若未安装,可在对话中发送 `/install-browser`,自动下载一个精简浏览器内核到 `~/.cow`。
</Note>
## 工作流程
Agent 使用浏览器的典型流程:
@@ -105,6 +111,15 @@ Agent 使用浏览器的典型流程:
}
```
## 浏览器内核
浏览器内核会自动选择,无需配置:
1. 若检测到本机已安装 **Google Chrome / Edge**,直接驱动系统浏览器,**无需下载 Chromium**,并使用真实浏览器指纹;
2. 否则使用 `install-browser` 下载到 `~/.cow` 的 Chromium 内核作为兜底。
两种方式都使用下面的登录态持久化,行为一致。
## 登录态持久化
**只需登录一次目标网站Agent 后续可直接使用**。提供两种方式:

View File

@@ -29,6 +29,24 @@ class OpenAICompatibleBot:
Subclasses only need to override get_api_config() to provide their specific API settings.
"""
@staticmethod
def _is_gpt5_reasoning_model(model_name: str) -> bool:
"""Whether the model is a GPT-5.x / o-series reasoning model.
Covers gpt-5, gpt-5.4/5.5/5.6 (including suffixed variants like
gpt-5.6-sol / gpt-5.6-luna) and the o1/o3/o4 families. These models
only accept default sampling params and, on /v1/chat/completions,
reject reasoning_effort together with function tools.
"""
if not model_name or not isinstance(model_name, str):
return False
name = model_name.lower()
if name.startswith("gpt-5"):
return True
if name.startswith(("o1", "o3", "o4")):
return True
return False
def get_api_config(self):
"""
Get API configuration for this bot.
@@ -99,8 +117,10 @@ class OpenAICompatibleBot:
"presence_penalty": kwargs.get("presence_penalty", api_config.get('default_presence_penalty', 0.0)),
"stream": stream
}
# GPT-5 / GPT-5.5 / o1 series only accept default temperature/top_p and reject penalty params
if model_name in ("gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5.5", "o1", "o1-mini"):
# GPT-5.x / o-series reasoning models only accept default
# temperature/top_p and reject penalty params.
is_gpt5_reasoning = self._is_gpt5_reasoning_model(model_name)
if is_gpt5_reasoning:
for key in ("temperature", "top_p", "frequency_penalty", "presence_penalty"):
request_params.pop(key, None)
@@ -112,6 +132,12 @@ class OpenAICompatibleBot:
if tools:
request_params["tools"] = tools
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
# GPT-5.x reasoning models reject function tools combined with
# reasoning_effort on /v1/chat/completions unless it is "none".
# Force "none" so agent tool calling works without migrating to
# the Responses API.
if is_gpt5_reasoning:
request_params["reasoning_effort"] = "none"
# Make API call with proper configuration
api_key = api_config.get('api_key')

View File

@@ -813,8 +813,8 @@ class CowCliPlugin(Plugin):
"you can also run `cow install-browser` in a terminal.",
)
return _t(
"✅ 安装流程已结束。请重启 CowAgent 后使用 browser 工具(进度见上方消息)",
"✅ Installation finished. Restart CowAgent to use the browser tool (see messages above for progress).",
"✅ 安装流程已结束。请重启 CowAgent 后使用 browser 工具。",
"✅ Installation finished. Restart CowAgent to use the browser tool.",
)
# ------------------------------------------------------------------

4
run.sh
View File

@@ -599,7 +599,7 @@ select_model() {
"DeepSeek (deepseek-v4-flash, deepseek-v4-pro, etc.)" \
"Claude (claude-opus-4-8, claude-fable-5, etc.)" \
"Gemini (gemini-3.5-flash, gemini-3.1-pro-preview, etc.)" \
"OpenAI (gpt-5.5, etc.)" \
"OpenAI (gpt-5.6-luna, etc.)" \
"MiniMax (MiniMax-M3, etc.)" \
"GLM (glm-5.2, etc.)" \
"Qwen (qwen3.7-plus, qwen3.7-max, etc.)" \
@@ -631,7 +631,7 @@ configure_model() {
1) read_model_config "DeepSeek" "deepseek-v4-flash" "DEEPSEEK_KEY" ;;
2) read_model_config "Claude" "claude-opus-4-8" "CLAUDE_KEY" ;;
3) read_model_config "Gemini" "gemini-3.1-pro-preview" "GEMINI_KEY" ;;
4) read_model_config "OpenAI" "gpt-5.5" "OPENAI_KEY" ;;
4) read_model_config "OpenAI" "gpt-5.6-luna" "OPENAI_KEY" ;;
5) read_model_config "MiniMax" "MiniMax-M3" "MINIMAX_KEY" ;;
6) read_model_config "GLM" "glm-5.2" "ZHIPU_KEY" ;;
7) read_model_config "Qwen (DashScope)" "qwen3.7-plus" "DASHSCOPE_KEY" ;;

View File

@@ -456,7 +456,7 @@ $ModelChoices = @{
1 = @{ Provider = "DeepSeek"; Default = "deepseek-v4-flash"; Field = "deepseek_api_key" }
2 = @{ Provider = "Claude"; Default = "claude-opus-4-8"; Field = "claude_api_key"; BaseField = "claude_api_base" }
3 = @{ Provider = "Gemini"; Default = "gemini-3.1-pro-preview"; Field = "gemini_api_key"; BaseField = "gemini_api_base" }
4 = @{ Provider = "OpenAI"; Default = "gpt-5.5"; Field = "open_ai_api_key"; BaseField = "open_ai_api_base" }
4 = @{ Provider = "OpenAI"; Default = "gpt-5.6-luna"; Field = "open_ai_api_key"; BaseField = "open_ai_api_base" }
5 = @{ Provider = "MiniMax"; Default = "MiniMax-M3"; Field = "minimax_api_key" }
6 = @{ Provider = "GLM"; Default = "glm-5.2"; Field = "zhipu_ai_api_key" }
7 = @{ Provider = "Qwen (DashScope)"; Default = "qwen3.7-plus"; Field = "dashscope_api_key" }
@@ -473,7 +473,7 @@ function Select-Model {
"DeepSeek (deepseek-v4-flash, deepseek-v4-pro, etc.)",
"Claude (claude-opus-4-8, claude-fable-5, etc.)",
"Gemini (gemini-3.5-flash, gemini-3.1-pro-preview, etc.)",
"OpenAI (gpt-5.5, etc.)",
"OpenAI (gpt-5.6-luna, etc.)",
"MiniMax (MiniMax-M3, etc.)",
"GLM (glm-5.2, etc.)",
"Qwen (qwen3.7-plus, qwen3.7-max, etc.)",