diff --git a/.pi/agent/extensions/sproutclaw-setup/index.ts b/.pi/agent/extensions/sproutclaw-setup/index.ts index f440a90c..59260b7c 100644 --- a/.pi/agent/extensions/sproutclaw-setup/index.ts +++ b/.pi/agent/extensions/sproutclaw-setup/index.ts @@ -1,44 +1,48 @@ /** * sproutclaw / mengya 命令安装扩展 * - * 启动后自动在 /usr/local/bin/ 下创建 mengya 和 sproutclaw 命令: - * - mengya:源码版(./pi-test.sh,tsx + src) - * - sproutclaw:构建版(./pi-built.sh,node dist/cli.js) + * 在 Linux 和 Windows 上分别创建/更新以下命令: + * - mengya:源码版(Linux: ./pi-test.sh,Windows: pi-test.bat) + * - sproutclaw:构建版(Linux: ./pi-built.sh,Windows: pi-built.bat) + * + * Linux 命令安装到 /usr/local/bin/。 + * Windows 批处理文件创建在项目根目录,需要把项目根目录加入 PATH 后全局使用。 * * 命令 /install-commands 可随时手动重装。 */ import { existsSync, writeFileSync, chmodSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -const SPROUTCLAW_DIR = "/shumengya/project/agent/sproutclaw"; -const BIN_DIR = "/usr/local/bin"; +const extensionDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url)); +const SPROUTCLAW_DIR = dirname(dirname(dirname(dirname(extensionDir)))); +const LINUX_BIN_DIR = "/usr/local/bin"; -const COMMAND_LAUNCHERS: Record = { - mengya: "./pi-test.sh", -}; +// ---------------------------- Linux scripts ---------------------------- -function commandScript(launcher: string): string { +function linuxMengyaScript(): string { return `#!/usr/bin/env bash set -euo pipefail -cd ${SPROUTCLAW_DIR} -exec ${launcher} "$@" +cd "${SPROUTCLAW_DIR}" +exec ./pi-test.sh "$@" `; } -function sproutclawScript(): string { +function linuxSproutclawScript(): string { return `#!/usr/bin/env bash set -euo pipefail -cd ${SPROUTCLAW_DIR} +cd "${SPROUTCLAW_DIR}" case "\${1:-}" in \tbuild) \t\tshift -\t\texec npm run build "\$@" +\t\texec npm run build "$@" \t\t;; \trun) \t\tshift -\t\texec ./pi-built.sh "\$@" +\t\texec ./pi-built.sh "$@" \t\t;; \thelp|-h|--help) \t\tcat <<'EOF' @@ -53,66 +57,133 @@ sproutclaw - SproutClaw pi 构建版启动器 EOF \t\t;; \t*) -\t\texec ./pi-built.sh "\$@" +\t\texec ./pi-built.sh "$@" \t\t;; esac `; } -/** 创建或修复命令脚本 */ -function installCommand(name: string, script: string): boolean { - const path = `${BIN_DIR}/${name}`; +// ---------------------------- Windows scripts ---------------------------- +function windowsMengyaBat(): string { + return `@echo off +setlocal +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" +if not defined PI_CODING_AGENT_DIR ( + set "PI_CODING_AGENT_DIR=%SCRIPT_DIR%\.pi\agent" +) +call "%SCRIPT_DIR%\pi-test.bat" %* +exit /b %ERRORLEVEL% +`; +} + +function windowsSproutclawBat(): string { + return `@echo off +setlocal +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" +if not defined PI_CODING_AGENT_DIR ( + set "PI_CODING_AGENT_DIR=%SCRIPT_DIR%\.pi\agent" +) + +if "%~1"=="" goto run +if /I "%~1"=="run" ( + shift + goto run +) +if /I "%~1"=="build" ( + shift + npm run build %* + exit /b %ERRORLEVEL% +) +if /I "%~1"=="help" goto help +if "%~1"=="-h" goto help +if "%~1"=="--help" goto help + +:run +call "%SCRIPT_DIR%\pi-built.bat" %* +exit /b %ERRORLEVEL% + +:help +echo sproutclaw - SproutClaw pi 构建版启动器 +echo. +echo 用法: +echo sproutclaw 启动 pi(构建版) +echo sproutclaw run [args] 同 sproutclaw +echo sproutclaw build 从源码构建所有包 +echo. +echo 其余参数透传给 pi,例如: sproutclaw --version +exit /b 0 +`; +} + +// ---------------------------- Install helpers ---------------------------- + +function writeIfChanged(path: string, content: string, mode?: number): boolean { if (existsSync(path)) { try { const current = readFileSync(path, "utf-8"); - if (current === script) return false; + if (current === content) return false; } catch { - // Rewrite unreadable or invalid command files below. + // Rewrite unreadable or invalid files below. } } - writeFileSync(path, script, "utf-8"); - chmodSync(path, 0o755); + writeFileSync(path, content, "utf-8"); + if (mode !== undefined) chmodSync(path, mode); return true; } +function installLinuxCommands(): string[] { + const created: string[] = []; + if (writeIfChanged(join(LINUX_BIN_DIR, "mengya"), linuxMengyaScript(), 0o755)) { + created.push("/usr/local/bin/mengya"); + } + if (writeIfChanged(join(LINUX_BIN_DIR, "sproutclaw"), linuxSproutclawScript(), 0o755)) { + created.push("/usr/local/bin/sproutclaw"); + } + return created; +} + +function installWindowsCommands(): string[] { + const created: string[] = []; + if (writeIfChanged(join(SPROUTCLAW_DIR, "mengya.bat"), windowsMengyaBat())) { + created.push(join(SPROUTCLAW_DIR, "mengya.bat")); + } + if (writeIfChanged(join(SPROUTCLAW_DIR, "sproutclaw.bat"), windowsSproutclawBat())) { + created.push(join(SPROUTCLAW_DIR, "sproutclaw.bat")); + } + return created; +} + +// ---------------------------- Extension entry ---------------------------- + export default function (pi: ExtensionAPI) { - if (process.platform === "win32") { - // On Windows, /usr/local/bin doesn't exist. Use pi-built.bat from the repo root instead. - pi.registerCommand("install-commands", { - description: "N/A on Windows: use pi-built.bat from the repo root", - handler: async (_args, ctx) => { - ctx.ui.notify("Windows 下无需安装全局命令,请直接使用项目根目录的 pi-built.bat", "info"); - }, - }); - return; + const isWindows = process.platform === "win32"; + + function install(): string[] { + return isWindows ? installWindowsCommands() : installLinuxCommands(); } - for (const [name, launcher] of Object.entries(COMMAND_LAUNCHERS)) { - const created = installCommand(name, commandScript(launcher)); - if (created) { - console.log(`[sproutclaw-setup] Created /usr/local/bin/${name}`); - } - } - - const sproutclawCreated = installCommand("sproutclaw", sproutclawScript()); - if (sproutclawCreated) { - console.log("[sproutclaw-setup] Created /usr/local/bin/sproutclaw"); + const created = install(); + for (const path of created) { + console.log(`[sproutclaw-setup] Created/updated ${path}`); } pi.registerCommand("install-commands", { - description: "Install mengya (source) and sproutclaw (built) commands to /usr/local/bin", + description: isWindows + ? "Install mengya.bat (source) and sproutclaw.bat (built) to the repo root" + : "Install mengya (source) and sproutclaw (built) to /usr/local/bin", handler: async (_args, ctx) => { - let count = 0; - for (const [name, launcher] of Object.entries(COMMAND_LAUNCHERS)) { - if (installCommand(name, commandScript(launcher))) count++; - } - if (installCommand("sproutclaw", sproutclawScript())) count++; - if (count > 0) { - ctx.ui.notify(`Installed ${count} command(s): mengya (source), sproutclaw (build/run)`, "success"); + const paths = install(); + if (paths.length > 0) { + ctx.ui.notify(`Installed/updated ${paths.length} command(s):\n${paths.join("\n")}`, "success"); } else { ctx.ui.notify("Commands already up to date", "info"); } + if (isWindows) { + ctx.ui.notify(`请把项目目录加入 PATH 以全局使用:\n${SPROUTCLAW_DIR}`, "info"); + } }, }); } diff --git a/.pi/agent/skills/quark-sign-skill/SKILL.md b/.pi/agent/skills/quark-sign-skill/SKILL.md new file mode 100644 index 00000000..5826eeb6 --- /dev/null +++ b/.pi/agent/skills/quark-sign-skill/SKILL.md @@ -0,0 +1,114 @@ +--- +name: quark-sign-skill +description: 夸克网盘自动签到。支持通过 URL 参数执行每日签到获取容量奖励,零外部依赖(Python 标准库即可运行)。 +--- + +# 夸克网盘签到 + +自动执行夸克网盘每日签到,获取容量奖励。 + +## 触发场景 + +- 用户提到「夸克签到」「夸克网盘签到」「签到」且上下文明确指向夸克网盘 +- 用户要求配置夸克网盘自动签到 + +## 前置要求 + +- Python 3.7+(已预装) +- 夸克网盘签到 URL(从夸克 App 或网页获取) + +## 获取签到 URL + +1. 打开夸克网盘 App 或网页版 +2. 进入「容量管理」或「签到」页面 +3. 浏览器开发者工具 → Network → 找到类似 `growth/sign` 或 `growth/reward_record` 的请求 +4. 复制完整 URL(包含 `kps`、`sign`、`vcode` 参数) + +URL 格式示例: +``` +https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=xxx&sign=yyy&vcode=zzz +``` + +## 使用方式 + +### 方式一:直接运行(推荐) + +如果 skill 目录下已配置了默认 `config.json`,无需任何参数即可运行: + +```bash +python scripts/quark_sign.py +``` + +### 方式二:直接传 URL + +```bash +python scripts/quark_sign.py --url "你的完整URL" +``` + +### 方式三:配置文件 + +```bash +# 1. 复制示例配置并编辑 +cp config.json.example ~/.config/quark_sign.json +# 填入你的 URL + +# 2. 运行 +python scripts/quark_sign.py --config ~/.config/quark_sign.json +``` + +### 方式四:环境变量 + +```bash +export QUARK_SIGN_URL="你的完整URL" +python scripts/quark_sign.py +``` + +## 常用命令 + +```bash +# 执行签到 +python scripts/quark_sign.py --url "URL" + +# 仅检查今日是否已签到 +python scripts/quark_sign.py --url "URL" --check + +# 查看签到信息 +python scripts/quark_sign.py --url "URL" --info +``` + +## 输出示例 + +``` +🚀 执行签到... +✅ 签到成功!获得 100.00 MB +``` + +``` +🚀 执行签到... +ℹ️ 今日已签到过,无需重复签到 +``` + +## 默认配置 + +skill 目录下已内置 `config.json`,作为默认 URL 来源。URL 查找优先级: + +1. `--url` 参数 +2. `--config` 指定的配置文件 +3. 环境变量 `QUARK_SIGN_URL` +4. skill 内置 `config.json`(默认使用) + +## 注意事项 + +- URL 中的 `kps` 和 `sign` 参数有有效期,如失效需重新获取 +- 签到奖励通常为 100MB~1GB 不等,随机发放 +- 脚本支持 `requests` 库(如已安装)或纯 Python 标准库(零依赖) +- 默认配置适合个人使用,多用户场景建议各自使用 `--config` 或 `--url` + +## 配置 cron 定时任务 + +```bash +# 每天 9 点自动签到 +crontab -e +# 添加: +0 9 * * * python3 /path/to/quark-sign-skill/scripts/quark_sign.py --config /root/.config/quark_sign.json >> /var/log/quark_sign.log 2>&1 +``` diff --git a/.pi/agent/skills/quark-sign-skill/config.json b/.pi/agent/skills/quark-sign-skill/config.json new file mode 100644 index 00000000..a48a76cd --- /dev/null +++ b/.pi/agent/skills/quark-sign-skill/config.json @@ -0,0 +1,3 @@ +{ + "url": "https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=Tj6VYdauGenq2rmPE33ohRAtDymhNFX6jKshCB38E3zVZiDAcGttvmkoAgTNU4n%252B8d7JiORQFKb%252F%252BEU524TsTfP1zN4ZWMXH5UMsxTD%252BLuW%252F8w%253D%253D&sign=Tj7N%252BNUA%252ByXE53C%252FPtR0xBpqepeIHSWt9RtzS66kyDIp9MNlfHO2OJ7zvOZQeK5QuAg%253D&vcode=1780291057288&_size=10&uc_param_str=dnfrpfbivessbtbmnilauputogpintnwmtsvcppcprsnnnchmicckp&fr=android&pf=3300&bi=37280&ve=10.2.3.104&ss=411x833&la=zh&ut=Tj5VTPsW0B0jX%2BOb3U5q%2BcX97R%2Bo%2FcYuH5nOtiswJy9O9Q%3D%3D&nt=6&nw=5G&mt=qUMBdVdLPFs5VQKefOxlxCMt4YMRf%2FRe&sv=release&pr=qk_clouddrive&ch=kkcloud%40store_xiaomi&mi=22127RK46C&kp=Tj6VYdauGenq2rmPE33ohRAtDymhNFX6jKshCB38E3zVZiDAcGttvmkoAgTNU4n%2B8d7JiORQFKb%2F%2BEU524TsTfP1zN4ZWMXH5UMsxTD%2BLuW%2F8w%3D%3D" +} diff --git a/.pi/agent/skills/quark-sign-skill/config.json.example b/.pi/agent/skills/quark-sign-skill/config.json.example new file mode 100644 index 00000000..6b86ceb3 --- /dev/null +++ b/.pi/agent/skills/quark-sign-skill/config.json.example @@ -0,0 +1,3 @@ +{ + "url": "https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=...&sign=...&vcode=..." +} diff --git a/.pi/agent/skills/quark-sign-skill/scripts/quark_sign.py b/.pi/agent/skills/quark-sign-skill/scripts/quark_sign.py new file mode 100644 index 00000000..a9600bd6 --- /dev/null +++ b/.pi/agent/skills/quark-sign-skill/scripts/quark_sign.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +夸克网盘自动签到脚本 + +用法: + python quark_sign.py --url "你的夸克URL" [--check] + python quark_sign.py --config ~/.config/quark_sign.json +""" +import argparse +import json +import os +import sys +from urllib.parse import urlparse, parse_qs, unquote + +# 尝试导入 requests,如果没有则使用 urllib +HAS_REQUESTS = True +try: + import requests +except ImportError: + HAS_REQUESTS = False + import urllib.request + import urllib.parse + import urllib.error + import ssl + + +class QuarkSign: + BASE_URL = "https://drive-m.quark.cn/1/clouddrive/capacity/growth" + + def __init__(self, url: str): + self.params = self._extract_params(url) + if not self.params.get("kps") or not self.params.get("sign"): + raise ValueError("URL 中缺少 kps 或 sign 参数,请检查 URL 是否完整") + + def _extract_params(self, url: str) -> dict: + parsed = urlparse(url) + params = parse_qs(parsed.query) + return {k: unquote(v[0]) for k, v in params.items()} + + def _build_query(self, extra: dict = None) -> dict: + q = { + "pr": "ucpro", + "fr": "android", + "kps": self.params.get("kps"), + "sign": self.params.get("sign"), + "vcode": self.params.get("vcode", "1780291057288"), + } + if extra: + q.update(extra) + return q + + def _post(self, endpoint: str, data: dict = None, params: dict = None) -> dict: + url = f"{self.BASE_URL}/{endpoint}" + query = self._build_query(params) + + if HAS_REQUESTS: + resp = requests.post(url, json=data or {}, params=query, timeout=10) + return resp.json() + else: + # 使用 urllib fallback + full_url = url + "?" + urllib.parse.urlencode(query) + payload = json.dumps(data or {}).encode("utf-8") + req = urllib.request.Request( + full_url, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, context=ctx, timeout=10) as resp: + return json.loads(resp.read().decode("utf-8")) + + def _get(self, endpoint: str, params: dict = None) -> dict: + url = f"{self.BASE_URL}/{endpoint}" + query = self._build_query(params) + + if HAS_REQUESTS: + resp = requests.get(url, params=query, timeout=10) + return resp.json() + else: + full_url = url + "?" + urllib.parse.urlencode(query) + req = urllib.request.Request(full_url, method="GET") + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, context=ctx, timeout=10) as resp: + return json.loads(resp.read().decode("utf-8")) + + def sign(self) -> dict: + """执行签到""" + return self._post("sign", data={"sign_cyclic": True}) + + def info(self) -> dict: + """获取签到信息""" + return self._get("sign", params={"_size": 1}) + + def reward_info(self) -> dict: + """获取奖励记录""" + return self._get("reward_record", params={"_size": 10}) + + +def format_reward(reward_bytes: int) -> str: + if reward_bytes >= 1024 ** 3: + return f"{reward_bytes / (1024 ** 3):.2f} GB" + return f"{reward_bytes / (1024 ** 2):.2f} MB" + + +def main(): + parser = argparse.ArgumentParser(description="夸克网盘自动签到") + parser.add_argument("--url", help="夸克网盘分享/签到 URL(包含 kps 和 sign 参数)") + parser.add_argument("--config", "-c", help="配置文件路径(JSON 格式)") + parser.add_argument("--check", action="store_true", help="仅检查签到状态,不执行签到") + parser.add_argument("--info", action="store_true", help="显示签到信息") + args = parser.parse_args() + + # 获取 URL + url = args.url + if not url and args.config: + with open(os.path.expanduser(args.config)) as f: + cfg = json.load(f) + url = cfg.get("url") + if not url: + # 尝试环境变量 + url = os.environ.get("QUARK_SIGN_URL") + if not url: + # 尝试读取默认配置(skill 内置) + script_dir = os.path.dirname(os.path.abspath(__file__)) + default_cfg = os.path.join(script_dir, "..", "config.json") + if os.path.exists(default_cfg): + with open(default_cfg) as f: + cfg = json.load(f) + url = cfg.get("url") + if not url: + print("❌ 请提供 URL:--url、--config 或环境变量 QUARK_SIGN_URL") + print(" 提示:可在 skill 目录下创建 config.json 写入默认 URL") + sys.exit(1) + + try: + qs = QuarkSign(url) + except ValueError as e: + print(f"❌ {e}") + sys.exit(1) + + if args.info: + print("📋 获取签到信息...") + info = qs.info() + print(json.dumps(info, indent=2, ensure_ascii=False)) + return + + if args.check: + print("🔍 检查签到状态...") + info = qs.info() + data = info.get("data", {}) + if data.get("sign_daily"): + print("✅ 今日已签到") + else: + print("⏳ 今日未签到") + return + + # 执行签到 + print("🚀 执行签到...") + result = qs.sign() + + status = result.get("status") + code = result.get("code") + msg = result.get("message", "") + data = result.get("data", {}) + + if data and status == 200: + reward = data.get("sign_daily_reward", 0) + print(f"✅ 签到成功!获得 {format_reward(reward)}") + elif "repeat" in msg.lower() or code == 44210: + print("ℹ️ 今日已签到过,无需重复签到") + else: + print(f"❌ 签到失败: {msg} (code={code}, status={status})") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.pi/agent/skills/skills/add-llm-provider.md b/.pi/agent/skills/skills/add-llm-provider.md deleted file mode 100644 index 97cd9a3e..00000000 --- a/.pi/agent/skills/skills/add-llm-provider.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: add-llm-provider -description: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs. ---- - -# Adding a New LLM Provider (packages/ai) - -A new provider touches multiple files. Work through these steps in order. - -## 1. Core Types (`packages/ai/src/types.ts`) - -- Add API identifier to `Api` type union (e.g. `"bedrock-converse-stream"`). -- Create options interface extending `StreamOptions`. -- Add mapping to `ApiOptionsMap`. -- Add provider name to `KnownProvider` type union. - -## 2. Provider Implementation (`packages/ai/src/providers/`) - -Create a provider file exporting: - -- `stream()` returning `AssistantMessageEventStream`. -- `streamSimple()` for `SimpleStreamOptions` mapping. -- Provider-specific options interface. -- Message/tool conversion functions. -- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`). - -## 3. Provider Exports and Lazy Registration - -- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/.js`. -- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry. -- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there. -- Add credential detection in `packages/ai/src/env-api-keys.ts`. - -## 4. Model Generation (`packages/ai/scripts/generate-models.ts`) - -- Add logic to fetch/parse models from the provider source. -- Map to the standardized `Model` interface. - -## 5. Tests (`packages/ai/test/`) - -- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`. -- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`. -- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family. -- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection. - -## 6. Coding Agent (`packages/coding-agent/`) - -- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`. -- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth. -- `src/cli/args.ts`: add env var documentation. -- `README.md`: add provider setup instructions. -- `docs/providers.md`: add setup instructions, env var, and `auth.json` key. - -## 7. Documentation - -- `packages/ai/README.md`: add to providers table, document options/auth, add env vars. -- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`.