diff --git a/README.md b/README.md index ec06e65..c9ed969 100644 --- a/README.md +++ b/README.md @@ -124,28 +124,47 @@ python3 toolkit/cli.py themes **Claude Code**: ```bash +# macOS/Linux git clone --depth 1 https://github.com/oaker-io/wewrite.git ~/.claude/skills/wewrite cd ~/.claude/skills/wewrite && bash install.sh + +# Windows (PowerShell) +git clone --depth 1 https://github.com/oaker-io/wewrite.git "$env:USERPROFILE\.claude\skills\wewrite" +cd "$env:USERPROFILE\.claude\skills\wewrite" +powershell -ExecutionPolicy Bypass -File install.ps1 ``` **OpenClaw**: ```bash +# macOS/Linux git clone --depth 1 https://github.com/oaker-io/wewrite.git ~/.openclaw/skills/wewrite cd ~/.openclaw/skills/wewrite && bash install.sh + +# Windows (PowerShell) +git clone --depth 1 https://github.com/oaker-io/wewrite.git "$env:USERPROFILE\.openclaw\skills\wewrite" +cd "$env:USERPROFILE\.openclaw\skills\wewrite" +powershell -ExecutionPolicy Bypass -File install.ps1 ``` **Codex**(OpenAI Codex CLI): ```bash +# macOS/Linux git clone --depth 1 https://github.com/oaker-io/wewrite.git ~/.codex/skills/wewrite cd ~/.codex/skills/wewrite && bash install.sh python3 scripts/build_codex.py --install # 装自定义 prompt 到 ~/.codex/prompts/ + +# Windows (PowerShell) +git clone --depth 1 https://github.com/oaker-io/wewrite.git "$env:USERPROFILE\.codex\skills\wewrite" +cd "$env:USERPROFILE\.codex\skills\wewrite" +powershell -ExecutionPolicy Bypass -File install.ps1 +python scripts/build_codex.py --install # 装自定义 prompt 到 ~/.codex/prompts/ ``` 之后在 Codex 里用 `/wewrite 写一篇关于 X 的文章` 触发完整流程。Codex 没有 SKILL.md 自动触发机制,所以通过自定义 prompt 承载;源 `SKILL.md` 更新后重跑 `build_codex.py --install` 同步。详见 [`dist/codex/README.md`](dist/codex/README.md)。 -`install.sh` 会在 `.venv` 里创建隔离环境并安装依赖,自动绕过 macOS Homebrew Python 的 PEP 668 限制。skill 运行时会自动使用该 venv,无需手动 `activate`。 +`install.sh`(macOS/Linux)或 `install.ps1`(Windows)会在 `.venv` 里创建隔离环境并安装依赖,自动绕过 macOS Homebrew Python 的 PEP 668 限制。skill 运行时会自动使用该 venv,无需手动 `activate`。 > 若你的环境没有 PEP 668 限制,也可以照旧直接 `pip install -r requirements.txt`。 diff --git a/SKILL.md b/SKILL.md index a409ec2..7eeea1e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -43,7 +43,11 @@ allowed-tools: **读取/检查约定**:本文档中 `读取: <路径>` / `检查: <路径>` = **用你环境的文件读取工具真实打开该文件、读完其全部内容,然后再继续本步**。这不是描述性注释——未读取前不得执行依赖该文件的步骤;不同 harness 的文件读取工具名不同,按你环境的对应工具执行。 -**Python 解释器约定**:本文档所有 `python3` 命令优先解析为 `{skill_dir}/.venv/bin/python3`(若该文件存在),否则回退系统 `python3`。venv 由 `install.sh` 创建,用于隔离依赖并绕过 macOS Homebrew Python 的 PEP 668 限制。 +**Python 解释器约定**:本文档所有 `python3` 命令优先解析为: +- **macOS/Linux**:`{skill_dir}/.venv/bin/python3`(若该文件存在) +- **Windows**:`{skill_dir}\.venv\Scripts\python.exe`(若该文件存在) + +否则回退系统 `python3`(或 Windows 上的 `python`)。venv 由 `install.sh`(macOS/Linux)或 `install.ps1`(Windows)创建,用于隔离依赖并绕过 macOS Homebrew Python 的 PEP 668 限制。 **Onboard 例外**:Onboard 是交互式的(需要问用户问题),不受"全自动"约束。Onboard 完成后回到全自动管道。 @@ -70,14 +74,19 @@ allowed-tools: ```bash # 优先用 venv 解释器(PEP 668 环境下依赖装在 .venv 里);后续所有 python3 调用同此规则 +# macOS/Linux: PY="{skill_dir}/.venv/bin/python3"; [ -x "$PY" ] || PY="python3" "$PY" -c "import markdown, bs4, cssutils, requests, yaml, pygments, PIL" 2>&1 + +# Windows (PowerShell): +# $venvPython = "{skill_dir}\.venv\Scripts\python.exe" +# if (Test-Path $venvPython) { & $venvPython -c "import markdown, bs4, cssutils, requests, yaml, pygments, PIL" } else { python -c "import markdown, bs4, cssutils, requests, yaml, pygments, PIL" } ``` | 检查项 | 通过 | 不通过 | |--------|------|--------| | `config.yaml` 存在 | 静默 | 引导创建,或设 `skip_publish = true` | -| Python 依赖 | 静默 | 引导执行 `bash {skill_dir}/install.sh`(自动建 .venv 装依赖,解决 macOS PEP 668 报错);若环境无此限制也可 `pip install -r requirements.txt` | +| Python 依赖 | 静默 | 引导执行 `bash {skill_dir}/install.sh`(macOS/Linux)或 `powershell -ExecutionPolicy Bypass -File {skill_dir}\install.ps1`(Windows)(自动建 .venv 装依赖,解决 macOS PEP 668 报错);若环境无此限制也可 `pip install -r requirements.txt` | | `wechat.appid` + `secret` | 静默 | 设 `skip_publish = true` | | `image.api_key` 或 `image.providers` 至少一项有效 | 静默 | 设 `skip_image_gen = true` | | `references/exemplars/index.yaml` | 静默 | 提示:"范文库为空。如果你有已发布的文章(markdown),可以说**'导入范文'**建立风格库,写出来的文章会更像你。没有也不影响使用。" | diff --git a/config.example.yaml b/config.example.yaml index 194c979..f37bc6f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -22,6 +22,7 @@ wechat: # │ gemini │ https://aistudio.google.com/apikey │ 免费额度较多 │ # │ openrouter │ https://openrouter.ai/settings/keys │ 多模型代理 │ # │ replicate │ https://replicate.com/account/api-tokens │ 开源模型丰富 │ +# │ agnes │ https://agnes-ai.com/ │ 新模型,免费额度 │ # └─────────────────┴────────────────────────────────────────────────┴────────────────────┘ # # 支持两种配置方式: @@ -65,6 +66,9 @@ image: # - provider: replicate # api_key: "r8_..." # # model: "google/nano-banana-pro" +# - provider: agnes +# api_key: "your_agnes_key" +# # model: "agnes-image-2.1-flash" # 默认排版主题 theme: "professional-clean" diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..f8c842a --- /dev/null +++ b/install.ps1 @@ -0,0 +1,59 @@ +# WeWrite 依赖安装脚本 (Windows PowerShell 版本) +# 用法:powershell -ExecutionPolicy Bypass -File install.ps1 + +$ErrorActionPreference = "Stop" +Set-Location $PSScriptRoot + +$PYTHON = "python" +if ($env:PYTHON) { $PYTHON = $env:PYTHON } + +if (-not (Get-Command $PYTHON -ErrorAction SilentlyContinue)) { + Write-Host "找不到 $PYTHON,请先安装 Python 3.11+" -ForegroundColor Red + exit 1 +} + +$pythonVersion = & $PYTHON --version 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "无法获取 Python 版本" -ForegroundColor Red + exit 1 +} + +$versionMatch = [regex]::Match($pythonVersion, "Python (\d+)\.(\d+)") +if ($versionMatch.Success) { + $major = [int]$versionMatch.Groups[1].Value + $minor = [int]$versionMatch.Groups[2].Value + if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 11)) { + Write-Host "Python 版本过低:$pythonVersion,需要 Python 3.11+" -ForegroundColor Red + exit 1 + } +} + +if (-not (Test-Path ".venv")) { + Write-Host "创建虚拟环境 .venv ..." -ForegroundColor Cyan + & $PYTHON -m venv .venv + if ($LASTEXITCODE -ne 0) { + Write-Host "创建虚拟环境失败" -ForegroundColor Red + exit 1 + } +} else { + Write-Host "复用已有的 .venv" -ForegroundColor Cyan +} + +Write-Host "安装依赖到 .venv ..." -ForegroundColor Cyan +$venvPython = ".venv\Scripts\python.exe" + +& $venvPython -m pip install --upgrade pip --quiet +if ($LASTEXITCODE -ne 0) { + Write-Host "升级 pip 失败" -ForegroundColor Red + exit 1 +} + +& $venvPython -m pip install -r requirements.txt +if ($LASTEXITCODE -ne 0) { + Write-Host "安装依赖失败" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "完成!依赖已装入 $PWD\.venv" -ForegroundColor Green +Write-Host "无需手动 activate,skill 运行时会自动使用 .venv\Scripts\python.exe" -ForegroundColor Green diff --git a/toolkit/image_gen.py b/toolkit/image_gen.py index dd068e1..3ee34b8 100644 --- a/toolkit/image_gen.py +++ b/toolkit/image_gen.py @@ -12,6 +12,7 @@ - azure_openai — Azure-hosted DALL-E - openrouter — multi-model proxy - jimeng (ByteDance) — good for Chinese prompts + - agnes (Sapiens AI) — agnes-image-2.1-flash, free credits available - Custom providers via ImageProvider base class Usage as CLI: @@ -74,21 +75,25 @@ def _load_config() -> dict: "doubao": "2952x1256", "openai": _DEFAULT, "gemini": _DEFAULT, "dashscope": _DEFAULT, "minimax": _DEFAULT, "replicate": _DEFAULT, "azure_openai": _DEFAULT, "openrouter": _DEFAULT, "jimeng": _DEFAULT, + "agnes": _DEFAULT, }, "article": { "doubao": "2560x1440", "openai": _DEFAULT, "gemini": _DEFAULT, "dashscope": _DEFAULT, "minimax": _DEFAULT, "replicate": _DEFAULT, "azure_openai": _DEFAULT, "openrouter": _DEFAULT, "jimeng": _DEFAULT, + "agnes": _DEFAULT, }, "vertical": { "doubao": "1088x2560", "openai": _DEFAULT_V, "gemini": _DEFAULT_V, "dashscope": _DEFAULT_V, "minimax": _DEFAULT_V, "replicate": _DEFAULT_V, "azure_openai": _DEFAULT_V, "openrouter": _DEFAULT_V, "jimeng": _DEFAULT_V, + "agnes": _DEFAULT_V, }, "square": { "doubao": "2048x2048", "openai": _DEFAULT_SQ, "gemini": _DEFAULT_SQ, "dashscope": _DEFAULT_SQ, "minimax": _DEFAULT_SQ, "replicate": _DEFAULT_SQ, "azure_openai": _DEFAULT_SQ, "openrouter": _DEFAULT_SQ, "jimeng": _DEFAULT_SQ, + "agnes": _DEFAULT_SQ, }, } @@ -612,6 +617,42 @@ def generate(self, prompt: str, size: str) -> bytes: raise ValueError("Jimeng polling timeout") +class AgnesProvider(ImageProvider): + """Agnes Image 2.1 Flash via Sapiens AI APIhub. + + NOTE: Agnes requires `response_format` to be placed inside `extra_body`, + not at the top level of the request body. + """ + + provider_key = "agnes" + + def __init__(self, api_key: str, model: str = "agnes-image-2.1-flash", + base_url: str = "https://apihub.agnes-ai.com", **_kw): + self._api_key = api_key + self._model = model + self._base_url = base_url.rstrip("/") + + def generate(self, prompt: str, size: str) -> bytes: + resp = requests.post( + f"{self._base_url}/v1/images/generations", + headers={"Content-Type": "application/json", + "Authorization": f"Bearer {self._api_key}"}, + json={"model": self._model, "prompt": prompt, "size": size, + "extra_body": {"response_format": "url"}}, + timeout=120, + ) + data = resp.json() + if resp.status_code != 200: + err = data.get("error", {}) + msg = err.get("message") if isinstance(err, dict) else str(err) + raise ValueError(f"Agnes error ({resp.status_code}): " + f"{msg or str(data)}") + url = data.get("data", [{}])[0].get("url") + if not url: + raise ValueError(f"No image URL: {data}") + return _download_image(url) + + # --- Provider registry --- PROVIDERS = { @@ -624,6 +665,7 @@ def generate(self, prompt: str, size: str) -> bytes: "azure_openai": AzureOpenAIProvider, "openrouter": OpenRouterProvider, "jimeng": JimengProvider, + "agnes": AgnesProvider, }