{"id":"83126281-f8bc-4fd3-9577-98c4cb6109f0","entityType":"agent","slug":"clawhub-laodao-agent-free-model-router-v1","name":"free-model-router-laodao","canonicalUrl":"https://www.xpersona.co/agent/clawhub-laodao-agent-free-model-router-v1","canonicalPath":"/agent/clawhub-laodao-agent-free-model-router-v1","generatedAt":"2026-10-10T01:04:08.543Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":null},"description":"为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Skill: free-model-router-laodao Owner: laodao-agent Summary: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Tags: latest:2.2.0 Version history: v2.2.0 | 2026-08-12T04:04:59.191Z | user v2.2.0 更新内容 改进 - **额度耗尽与限流的恢复更可靠**：402 额度耗尽、长期 429 限流不再走固定冷却时间，改为持续探测成功才恢复，避免额度未恢复时反复失败 - **单次 429 限流不再触发长冷却**：单次限流不再立即把 Provider 长时间禁用，按累计次数逐步升档；累计清零后立即恢复 - **单个模型持续失败不再拖累整个 Provider**：单","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2.3K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s1795r81v2nhm9vmecrs9562t584xfr3:free-model-router-v1","sourceUrl":"https://clawhub.ai/laodao-agent/free-model-router-v1","homepage":"https://clawhub.ai/laodao-agent/skills/free-model-router-v1","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/laodao-agent/free-model-router-v1","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/laodao-agent/skills/free-model-router-v1","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":67,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Skill: free-model-router-laodao Owner: laodao-agent Summary: 为 Op"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":null},"stars":null,"forks":null,"downloads":2338,"packageName":null,"latestVersion":"2.2.0","tractionLabel":"2.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:13:16.317Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T16:13:16.318Z","lastCrawledAt":"2026-10-09T16:13:16.317Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T16:13:16.317Z","lastVerifiedAt":null,"highlights":[{"version":"2.2.0","createdAt":"2026-08-12T04:04:59.191Z","changelog":"## v2.2.0 更新内容 ### 改进 - **额度耗尽与限流的恢复更可靠**：402 额度耗尽、长期 429 限流不再走固定冷却时间，改为持续探测成功才恢复，避免额度未恢复时反复失败 - **单次 429 限流不再触发长冷却**：单次限流不再立即把 Provider 长时间禁用，按累计次数逐步升档；累计清零后立即恢复 - **单个模型持续失败不再拖累整个 Provider**：单个模型持续失败到阈值时自动转入冷池探测恢复，其他模型正常路由 ### 修复 - **模型被误禁用后未恢复**：触发禁用阈值后未正确恢复的回归已修复；连续失败计数跨请求批次累积，避免分散失败被误判为偶发 - **402 额度耗尽不再误降权**：402 属于配额类故障，不再计入 Provider 质量分母 - **管理面板修改 apiKey 后立即生效**：修改 Provider apiKey 后立即重载新 key，不再依赖重启","fileCount":87,"zipByteSize":231774},{"version":"2.1.2","createdAt":"2026-07-29T09:10:28.346Z","changelog":"free-model-router-v1 2.1.2 Changelog - 解决agnes-ai 对国内用户硬熔断的问题","fileCount":87,"zipByteSize":226771},{"version":"2.1.1","createdAt":"2026-07-28T09:50:14.421Z","changelog":"free-model-router-v1 2.1.1 - 修复管理台多 API Key 权益徽章未展示的问题","fileCount":86,"zipByteSize":225422},{"version":"2.1.0","createdAt":"2026-07-28T08:08:57.304Z","changelog":"**多 Provider API Key 管理**：每个 Provider 现支持配置多个 API Key，单 key 失效时自动切换、负载均衡、key 轮换救援，整体可用性大幅提升","fileCount":86,"zipByteSize":225306},{"version":"2.0.2","createdAt":"2026-07-21T05:50:59.063Z","changelog":"1. 修复首次安装（新注册）后聊天必定失败的问题：启动初期可用模型被误过滤，报\"没有可用的模型服务\"；部分免费模型 max_tokens 处理异常导致请求被误拒 2. 新增超时档位体系：随可用渠道状态自动调整请求超时（自适应/适中/宽松），渠道不稳定时更少误判 3. 主模型缺失时自动启用备选模型，减少\"无可用模型\"报错'","fileCount":85,"zipByteSize":216833},{"version":"2.0.1","createdAt":"2026-07-17T14:14:09.043Z","changelog":"1. v2.0.0 核心改造：hot pool 热池 + 能力感知路由 + v1→v2 配置自动迁移，新增支持识图与文生图 2. 稳定性：probe-service 探活/恢复/探索机制、健康检查异步任务 + 动态预算 3. proxy 超时重构：TTFB 弹性、TCP 连接超时、CJK token 估算修复 4. 安全加固：X-Forwarded-For 伪造、XSS 转义 5. admin：上下文窗口展示、健康检查按钮、自定义安装图标","fileCount":81,"zipByteSize":212523},{"version":"1.5.3","createdAt":"2026-06-05T15:46:10.220Z","changelog":"free-model-router-v1.5.3 更新日志 - 新增版本更新指引，强调 skill 更新后Agent需要 stop + setup 才能激活Router新版本功能","fileCount":68,"zipByteSize":165151},{"version":"1.5.2","createdAt":"2026-06-04T14:09:16.289Z","changelog":"free-model-router-v1.5.2 更新日志 修复安全漏洞","fileCount":68,"zipByteSize":165042}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1795r81v2nhm9vmecrs9562t584xfr3:free-model-router-v1","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T01:04:08.539Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-laodao-agent-free-model-router-v1/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":null},"readme":"Skill: free-model-router-laodao\n\nOwner: laodao-agent\n\nSummary: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\n\nTags: latest:2.2.0\n\nVersion history:\n\nv2.2.0 | 2026-08-12T04:04:59.191Z | user\n\n## v2.2.0 更新内容\n\n### 改进\n\n- **额度耗尽与限流的恢复更可靠**：402 额度耗尽、长期 429 限流不再走固定冷却时间，改为持续探测成功才恢复，避免额度未恢复时反复失败\n- **单次 429 限流不再触发长冷却**：单次限流不再立即把 Provider 长时间禁用，按累计次数逐步升档；累计清零后立即恢复\n- **单个模型持续失败不再拖累整个 Provider**：单个模型持续失败到阈值时自动转入冷池探测恢复，其他模型正常路由\n\n### 修复\n\n- **模型被误禁用后未恢复**：触发禁用阈值后未正确恢复的回归已修复；连续失败计数跨请求批次累积，避免分散失败被误判为偶发\n- **402 额度耗尽不再误降权**：402 属于配额类故障，不再计入 Provider 质量分母\n- **管理面板修改 apiKey 后立即生效**：修改 Provider apiKey 后立即重载新 key，不再依赖重启\n\nv2.1.2 | 2026-07-29T09:10:28.346Z | user\n\nfree-model-router-v1 2.1.2 Changelog\n- 解决agnes-ai 对国内用户硬熔断的问题\n\nv2.1.1 | 2026-07-28T09:50:14.421Z | user\n\nfree-model-router-v1 2.1.1\n\n- 修复管理台多 API Key 权益徽章未展示的问题\n\nv2.1.0 | 2026-07-28T08:08:57.304Z | user\n\n**多 Provider API Key 管理**：每个 Provider 现支持配置多个 API Key，单 key 失效时自动切换、负载均衡、key 轮换救援，整体可用性大幅提升\n\nv2.0.2 | 2026-07-21T05:50:59.063Z | user\n\n1. 修复首次安装（新注册）后聊天必定失败的问题：启动初期可用模型被误过滤，报\"没有可用的模型服务\"；部分免费模型 max_tokens 处理异常导致请求被误拒\n 2. 新增超时档位体系：随可用渠道状态自动调整请求超时（自适应/适中/宽松），渠道不稳定时更少误判\n 3. 主模型缺失时自动启用备选模型，减少\"无可用模型\"报错'\n\nv2.0.1 | 2026-07-17T14:14:09.043Z | user\n\n1. v2.0.0 核心改造：hot pool 热池 + 能力感知路由 + v1→v2 配置自动迁移，新增支持识图与文生图\n2. 稳定性：probe-service 探活/恢复/探索机制、健康检查异步任务 + 动态预算\n3. proxy 超时重构：TTFB 弹性、TCP 连接超时、CJK token 估算修复\n4. 安全加固：X-Forwarded-For 伪造、XSS 转义\n5. admin：上下文窗口展示、健康检查按钮、自定义安装图标\n\nv1.5.3 | 2026-06-05T15:46:10.220Z | user\n\nfree-model-router-v1.5.3 更新日志\n- 新增版本更新指引，强调 skill 更新后Agent需要 stop + setup 才能激活Router新版本功能\n\nv1.5.2 | 2026-06-04T14:09:16.289Z | user\n\nfree-model-router-v1.5.2 更新日志\n修复安全漏洞\n\nv1.5.1 | 2026-06-04T13:43:14.057Z | user\n\n1、新增Agnes AI渠道\n2、处理aihubmix限流伪装正常响应的问题\n3、修复若干bug\n\nv1.5.0 | 2026-06-01T01:28:59.634Z | user\n\n1.5.0 版本更新：\n1、智能路由算法升级：基于模型评分系统（延迟、成功率、模型能力加权）、冷池模型自动探测与晋升、ε-探索策略在路由中平衡已知最佳模型与探索新模型、自适应探测预算控制实现稳定性与可用性的大幅提升，在4小时稳定性测试中，1000多次请求成功率100%\n2、Claude Code兼容：修复了Authropic 协议转换中的多个问题，完整支持流式、非流式、思考、非思考及工具调用等特性。claude code 终于可以正常接入FMR\n3、新增并发槽位特性：预防流量集中打到某个provider导致限流，同时提供基于多个provider多槽位的并行调度能力，在集中大量调用保驾护航。\n4、管理台新增社区入口：与优秀的AI时代实践者同行，共同交流成长，优先获得社区技术支持\n5、反馈入口：Bug反馈、功能建议直接提交\n6、公告栏：新功能上线、渠道维护一目了然\n7、问题修复：调用量统计、健康检查问题修复\n8、支持匿名模型：第一时间使用免费优质的匿名模型\n\nv1.1.5 | 2026-05-15T22:26:11.010Z | user\n\n- 优化智能调度算法，大幅提升稳定性\n- 用户管理台更新\n-健康检测优化\n-修复已知问题\n\nv1.0.3 | 2026-05-13T13:33:12.597Z | user\n\nfree-model-router v1.0.3\n\n- 新增「数据流向说明」部分，详细说明本地与外部通信的数据流层次及敏感信息边界。\n- 补充提示，明确模型请求会转发到外部 Provider（如 OpenRouter、NVIDIA NIM），强化用户数据知情权。\n- 增加 `/free-model-router stop` 和 `/free-model-router uninstall` 命令说明，支持停止 Router 进程和一键卸载技能，涵盖自动清理流程和后续操作提示。\n- 更新注意事项和命令列表，更准确反映最新的技能能力和流程。\n- 新增免费模型渠道\n- 智能调度算法升级\n\nv1.0.2 | 2026-05-13T06:25:40.887Z | user\n\nNo file changes detected. No functional or content changes in this version.\n\nv1.0.1 | 2026-05-10T04:37:45.946Z | user\n\n- 增加“安全与隐私声明”章节，详细说明数据同步、外部通信、数据上报、设备指纹、配置修改、本地代理等敏感操作，并强调所有外部请求仅涉及模型元数据，默认关闭匿名健康状态上报。\n- 明确承诺本技能不收集、传输或存储任何用户对话内容或模型响应数据，增强用户信任。\n- 对外部通信、数据上报和推荐码参与等行为增加用户自主控制与默认说明。\n- 其余核心功能、配置流程、命令约束等内容保持不变。\n\nv1.0.0 | 2026-05-10T04:12:41.425Z | user\n\nfree-model-router v1.0.0 — 首次发布，为 OpenClaw 提供智能免费模型路由服务。\n\n- 提供本地代理、自动配置、自动轮询、故障切换、零侵入配置能力\n- 支持多 provider 注册和主备 Key 自动管理\n- 内置事件通知系统，定时推送模型切换和故障等状态\n- 完整意图识别和 Setup 流程，引导配置及日常模型管理\n- 命令幂等性保护，多次安装/运行 setup 不影响现有配置\n\nArchive index:\n\nArchive v2.2.0: 87 files, 231774 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (44964b), core/admin/public/images/community-link.png (70b), core/admin/public/images/community-qrcode.png (2608b), core/admin/public/index.html (22700b), core/admin/public/js/admin.js (65381b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2216b), core/admin/public/js/quick-start.js (4380b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (672b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (28238b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5836b), core/model-scorer.js (1920b), core/monitor-logger.js (7913b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (25169b), core/probe-url.js (1376b), core/provider-ids.js (377b), core/proxy.js (66125b), core/route-engine.js (38484b), core/routing-strategy.js (3806b), core/server-client.js (21816b), core/server.js (51792b), core/stats.js (15640b), core/timeout-profile.js (1986b), core/update-service.js (8778b), node_modules/.package-lock.json (427b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/LICENSE (759b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (97573b), package.json (2659b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b)\n\nFile v2.2.0:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.2.0:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.2.0\",\n  \"publishedAt\": 1786507499191\n}\n\nFile v2.2.0:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```\n\nFile v2.2.0:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效\n\nFile v2.2.0:references/setup-guide.md\n\n# Setup 流程指南\r\n\r\n> Setup 流程的完整步骤,供 Agent 对话引导使用。\r\n\r\n## 核心原则\r\n\r\n- **多 Provider 优于单 Provider** — 引导用户尽可能提供多个 Key\r\n\r\n## 命令执行约束\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式 `node <skill_dir>/scripts/free-model-cli.js <command>`。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定。\r\n\r\n## 第一步:运行 setup\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\nsetup 自动完成:\r\n1. 启动 Router 服务(如果未运行)\r\n2. 注册 API Key、拉取 providers/models、自动选择主备模型\r\n   - **幂等性**:仅对尚未设置主模型的 provider 自动选择,保留用户手动配置\r\n3. 向 openclaw.json 写入 `free-model-router` provider(仅此一次,已存在则跳过)\r\n4. 输出 cron 任务注册指令\r\n\r\n> **已安装检测**:如果 Router 已运行且 OpenClaw 已配置,setup 会输出当前状态摘要而非重新配置。\r\n\r\n## 第二步:注册定时任务\r\n\r\n根据 setup 输出的指令,使用 OpenClaw 内置 cron 工具注册:\r\n\r\n> **幂等提示:** 注册前先用 `cron list` 检查任务是否已存在。如果任务已存在且配置相同,跳过即可。\r\n\r\n**任务 A:事件通知检查(每 30 分钟)**\r\n```\r\ncron add\r\n  name: free-model-router-event-check\r\n  schedule: */30 * * * *\r\n  instruction: 读取 <DATA_DIR>/events.json 中 status=pending 且 shouldNotify=true 的事件。如有待推送事件,向用户推送后标记 status=notified;如无待推送事件,保持静默,不输出任何内容\r\n```\r\n\r\n**任务 B:每日状态汇报(每天 9:00)**\r\n```\r\ncron add\r\n  name: free-model-router-daily-status\r\n  schedule: 0 9 * * *\r\n  instruction: 读取 <DATA_DIR>/router-config.json 中各 provider 的主备模型和运行状态,向用户汇报\r\n```\r\n\r\n> 如果 cron 不可用,提示用户重新注册定时任务。\r\n> 如果任务已存在,Agent 应跳过注册或仅更新指令内容,不创建重复任务。\r\n\r\n## 第三步:展示 Provider 列表,引导设置 API Key\r\n\r\n> **幂等提示:** 如果已有 Provider 配置了 API Key,第三步可以跳过或仅引导添加新的 Provider。\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providers\r\n```\r\n\r\n根据服务器返回的 providers 数据,向用户展示状态和注册网址:\r\n\r\n```\r\n支持的免费模型 Provider:\r\n\r\n{遍历 providers 数组,对每个 provider 显示:}\r\n✓ {name} — 已配置 API Key\r\n  网址: {url}\r\n\r\n? {name} — 未配置 API Key({freeQuota})\r\n  网址: {url}\r\n  注册指引: {keyGuide}\r\n\r\n建议多配置几个 Provider,提升稳定性。\r\n请回复需要配置的 Provider 和对应的 API Key\r\n```\r\n\r\n> **重要:** Provider 列表、网址、注册指引均由 model-server 动态返回,不要写死数据。展示时必须包含:\r\n> - `url` - 注册网址\r\n> - `freeQuota` - 免费额度说明\r\n> - `keyGuide` - API Key 获取指引(如果有)\r\n\r\n收到 Key 后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providerApiKey <provider> <key>\r\n```\r\n\r\n## 第四步:询问用户将 free-model-router 设为OpencClaw的主模型还是备用模型\r\n\r\n> **此步骤在至少一个 Provider 的 API Key 设置完成后执行。**\r\n\r\n向用户询问:\r\n\r\n```\r\nfree-model-router 已配置完成。\r\n请选择 free-model-router 在 OpenClaw 中的角色:\r\n\r\n选项 A:设为主模型(推荐) — 所有请求默认走免费模型路由,原主模型自动降为备用\r\n选项 B:设为备用模型 — 仅当原主模型不可用时使用免费模型路由\r\n\r\n请回复 \"主模型\" 或 \"备用模型\"\r\n```\r\n\r\n收到用户选择(未选择时默认设为备用模型)后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js configureModelRole <primary|fallback>\r\n```\r\n\r\n- 设为主模型后,`agents.defaults.model.primary` 将变为 `free-model-router/model-router`,原主模型自动移入 `fallbacks`\r\n- 设为备用模型后,`free-model-router/model-router` 将添加到 `agents.defaults.model.fallbacks` 列表末尾\r\n\r\n## 第五步:展示各 Provider 主备模型\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js models\r\n```\r\n\r\n向用户展示各 provider 的主备模型选择:\r\n\r\n```\r\n{遍历已配置的 providers:}\r\n{providerName}:\r\n  主模型: {primaryModel}\r\n  备模型: {fallbackModels}\r\n```\r\n\r\n用户可选择切换主模型:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js switchProviderPrimaryModel <provider> <modelId>\r\n```\r\n\r\n## 第六步:完成\r\n\r\n```\r\n全部完成!\r\nRouter 已启动: http://127.0.0.1:5678\r\nOpenClaw 已配置: free-model-router → 本地 Router\r\n主模型: xxx / 备模型: xxx, xxx\r\n事件通知: 每 30 分钟 / 状态汇报: 每天 9:00 / 故障切换: 自动\r\n\r\n所有模型切换在 Router 层透明完成,不再修改 openclaw.json。\r\n```\n\nFile v2.2.0:skill-card.md\n\n## Description:\n\nProvides OpenClaw with a local free-model routing proxy for provider setup, automatic polling, failover, model switching, and availability diagnostics.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[laodao-agent](https://clawhub.ai/user/laodao-agent)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal developers and OpenClaw users use this skill to configure a local router that selects available free model providers, switches models, and diagnoses provider availability issues. It is intended for users who are comfortable running a persistent local proxy and reviewing provider data handling.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill runs a persistent local router and stores provider API keys.\n\nMitigation: Install only when the local router is needed, keep provider keys scoped and revocable, and stop or uninstall the router when it is no longer in use.\n\nRisk: The local admin surface may expose keys or high-impact controls too broadly.\n\nMitigation: Treat the admin UI as sensitive and avoid running the router while browsing untrusted sites until the local admin and CORS behavior has been reviewed and fixed.\n\nRisk: The skill communicates with freemodel control servers for registration, provider metadata, and health reporting.\n\nMitigation: Review the configured control-server communication and leave optional health reporting disabled unless the user intentionally enables it.\n\nRisk: Displayed curl-to-shell update commands can increase execution risk.\n\nMitigation: Avoid curl-to-shell updates; inspect downloaded update content and run explicit commands from trusted release artifacts instead.\n\nRisk: Scheduled tasks may remain after uninstall.\n\nMitigation: After uninstalling, verify and remove any remaining scheduled tasks associated with free-model-router.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/laodao-agent/skills/free-model-router-v1)\n- [Publisher Profile](https://clawhub.ai/user/laodao-agent)\n- [Setup Guide](references/setup-guide.md)\n- [Event System](references/event-system.md)\n- [Idempotency Guide](references/idempotency.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline shell commands and configuration guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May instruct the agent to run Node.js CLI commands, configure OpenClaw provider settings, and report router or provider status.]\n\n## Skill Version(s):\n\n2.2.0 (source: server release evidence and package.json)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v2.2.0:node_modules/.package-lock.json\n\n{\n  \"name\": \"free-model-router\",\n  \"version\": \"2.2.0\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"node_modules/@foxt/js-srp\": {\n      \"version\": \"0.0.3-patch2\",\n      \"resolved\": \"https://registry.npmmirror.com/@foxt/js-srp/-/js-srp-0.0.3-patch2.tgz\",\n      \"integrity\": \"sha512-mCjMIf+/mwmuzSGr8CWWQDjDUqMBnw9TBnzfcBUTR2ySPoXcg6Pfb4/WPklY1TwY3Pe3f3r8q6hzWvvogInuug==\",\n      \"license\": \"ISC\"\n    }\n  }\n}\n\nFile v2.2.0:node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-browser-esbuild-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.2.0:node_modules/@foxt/js-srp/examples/browser-esbuild/package.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"esbuild --minify --bundle --platform=browser --outfile=dist/index.js index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nFile v2.2.0:node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-nodejs-cjs-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.2.0:node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"node index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nArchive v2.1.2: 87 files, 226771 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (44964b), core/admin/public/images/community-link.png (70b), core/admin/public/images/community-qrcode.png (2608b), core/admin/public/index.html (22700b), core/admin/public/js/admin.js (64826b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2216b), core/admin/public/js/quick-start.js (4380b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (672b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (27239b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5836b), core/model-scorer.js (1920b), core/monitor-logger.js (6889b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (22261b), core/probe-url.js (1376b), core/provider-ids.js (377b), core/proxy.js (61783b), core/route-engine.js (37459b), core/routing-strategy.js (3806b), core/server-client.js (21816b), core/server.js (47910b), core/stats.js (10754b), core/timeout-profile.js (1986b), core/update-service.js (8792b), node_modules/.package-lock.json (427b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/LICENSE (759b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (97573b), package.json (2659b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b)\n\nFile v2.1.2:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.1.2:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.1.2\",\n  \"publishedAt\": 1785316228346\n}\n\nFile v2.1.2:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```\n\nFile v2.1.2:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效\n\nFile v2.1.2:references/setup-guide.md\n\n# Setup 流程指南\r\n\r\n> Setup 流程的完整步骤,供 Agent 对话引导使用。\r\n\r\n## 核心原则\r\n\r\n- **多 Provider 优于单 Provider** — 引导用户尽可能提供多个 Key\r\n\r\n## 命令执行约束\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式 `node <skill_dir>/scripts/free-model-cli.js <command>`。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定。\r\n\r\n## 第一步:运行 setup\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\nsetup 自动完成:\r\n1. 启动 Router 服务(如果未运行)\r\n2. 注册 API Key、拉取 providers/models、自动选择主备模型\r\n   - **幂等性**:仅对尚未设置主模型的 provider 自动选择,保留用户手动配置\r\n3. 向 openclaw.json 写入 `free-model-router` provider(仅此一次,已存在则跳过)\r\n4. 输出 cron 任务注册指令\r\n\r\n> **已安装检测**:如果 Router 已运行且 OpenClaw 已配置,setup 会输出当前状态摘要而非重新配置。\r\n\r\n## 第二步:注册定时任务\r\n\r\n根据 setup 输出的指令,使用 OpenClaw 内置 cron 工具注册:\r\n\r\n> **幂等提示:** 注册前先用 `cron list` 检查任务是否已存在。如果任务已存在且配置相同,跳过即可。\r\n\r\n**任务 A:事件通知检查(每 30 分钟)**\r\n```\r\ncron add\r\n  name: free-model-router-event-check\r\n  schedule: */30 * * * *\r\n  instruction: 读取 <DATA_DIR>/events.json 中 status=pending 且 shouldNotify=true 的事件。如有待推送事件,向用户推送后标记 status=notified;如无待推送事件,保持静默,不输出任何内容\r\n```\r\n\r\n**任务 B:每日状态汇报(每天 9:00)**\r\n```\r\ncron add\r\n  name: free-model-router-daily-status\r\n  schedule: 0 9 * * *\r\n  instruction: 读取 <DATA_DIR>/router-config.json 中各 provider 的主备模型和运行状态,向用户汇报\r\n```\r\n\r\n> 如果 cron 不可用,提示用户重新注册定时任务。\r\n> 如果任务已存在,Agent 应跳过注册或仅更新指令内容,不创建重复任务。\r\n\r\n## 第三步:展示 Provider 列表,引导设置 API Key\r\n\r\n> **幂等提示:** 如果已有 Provider 配置了 API Key,第三步可以跳过或仅引导添加新的 Provider。\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providers\r\n```\r\n\r\n根据服务器返回的 providers 数据,向用户展示状态和注册网址:\r\n\r\n```\r\n支持的免费模型 Provider:\r\n\r\n{遍历 providers 数组,对每个 provider 显示:}\r\n✓ {name} — 已配置 API Key\r\n  网址: {url}\r\n\r\n? {name} — 未配置 API Key({freeQuota})\r\n  网址: {url}\r\n  注册指引: {keyGuide}\r\n\r\n建议多配置几个 Provider,提升稳定性。\r\n请回复需要配置的 Provider 和对应的 API Key\r\n```\r\n\r\n> **重要:** Provider 列表、网址、注册指引均由 model-server 动态返回,不要写死数据。展示时必须包含:\r\n> - `url` - 注册网址\r\n> - `freeQuota` - 免费额度说明\r\n> - `keyGuide` - API Key 获取指引(如果有)\r\n\r\n收到 Key 后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providerApiKey <provider> <key>\r\n```\r\n\r\n## 第四步:询问用户将 free-model-router 设为OpencClaw的主模型还是备用模型\r\n\r\n> **此步骤在至少一个 Provider 的 API Key 设置完成后执行。**\r\n\r\n向用户询问:\r\n\r\n```\r\nfree-model-router 已配置完成。\r\n请选择 free-model-router 在 OpenClaw 中的角色:\r\n\r\n选项 A:设为主模型(推荐) — 所有请求默认走免费模型路由,原主模型自动降为备用\r\n选项 B:设为备用模型 — 仅当原主模型不可用时使用免费模型路由\r\n\r\n请回复 \"主模型\" 或 \"备用模型\"\r\n```\r\n\r\n收到用户选择(未选择时默认设为备用模型)后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js configureModelRole <primary|fallback>\r\n```\r\n\r\n- 设为主模型后,`agents.defaults.model.primary` 将变为 `free-model-router/model-router`,原主模型自动移入 `fallbacks`\r\n- 设为备用模型后,`free-model-router/model-router` 将添加到 `agents.defaults.model.fallbacks` 列表末尾\r\n\r\n## 第五步:展示各 Provider 主备模型\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js models\r\n```\r\n\r\n向用户展示各 provider 的主备模型选择:\r\n\r\n```\r\n{遍历已配置的 providers:}\r\n{providerName}:\r\n  主模型: {primaryModel}\r\n  备模型: {fallbackModels}\r\n```\r\n\r\n用户可选择切换主模型:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js switchProviderPrimaryModel <provider> <modelId>\r\n```\r\n\r\n## 第六步:完成\r\n\r\n```\r\n全部完成!\r\nRouter 已启动: http://127.0.0.1:5678\r\nOpenClaw 已配置: free-model-router → 本地 Router\r\n主模型: xxx / 备模型: xxx, xxx\r\n事件通知: 每 30 分钟 / 状态汇报: 每天 9:00 / 故障切换: 自动\r\n\r\n所有模型切换在 Router 层透明完成,不再修改 openclaw.json。\r\n```\n\nFile v2.1.2:skill-card.md\n\n## Description: <br>\nProvides an OpenClaw local model router for automatic free-model provider setup, polling, failover, model switching, and availability diagnostics. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[laodao-agent](https://clawhub.ai/user/laodao-agent) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and OpenClaw users use this skill to configure a persistent local router that selects, switches, and fails over across free model providers. It also helps diagnose provider availability and manage OpenClaw model routing without repeatedly editing the main OpenClaw configuration. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill runs a persistent localhost router that can expose or change sensitive routing data. <br>\nMitigation: Install only on a trusted machine and review the local admin/API surface before relying on it. <br>\nRisk: Provider API keys are stored locally and may be requested during setup. <br>\nMitigation: Use a secure secret-entry path when available and avoid pasting API keys into ordinary chat. <br>\nRisk: The router contacts external freemodel/model provider services and may register scheduled checks. <br>\nMitigation: Confirm the external communication and scheduled checks are acceptable before deployment, and keep reporting disabled unless telemetry is intended. <br>\n\n\n## Reference(s): <br>\n- [Setup Flow Guide](references/setup-guide.md) <br>\n- [Event Notification System](references/event-system.md) <br>\n- [Installation Idempotency Guide](references/idempotency.md) <br>\n- [Qwen3 Coder Free Model Listing](https://openrouter.ai/models/qwen/qwen3-coder:free) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Shell commands, Configuration, Markdown] <br>\n**Output Format:** [Markdown guidance with inline shell commands and configuration steps] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May direct the agent to run local Node.js CLI commands and register scheduled status or event checks.] <br>\n\n## Skill Version(s): <br>\n2.1.2 (source: package.json and server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v2.1.2:node_modules/.package-lock.json\n\n{\n  \"name\": \"free-model-router\",\n  \"version\": \"2.1.2\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"node_modules/@foxt/js-srp\": {\n      \"version\": \"0.0.3-patch2\",\n      \"resolved\": \"https://registry.npmmirror.com/@foxt/js-srp/-/js-srp-0.0.3-patch2.tgz\",\n      \"integrity\": \"sha512-mCjMIf+/mwmuzSGr8CWWQDjDUqMBnw9TBnzfcBUTR2ySPoXcg6Pfb4/WPklY1TwY3Pe3f3r8q6hzWvvogInuug==\",\n      \"license\": \"ISC\"\n    }\n  }\n}\n\nFile v2.1.2:node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-browser-esbuild-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.2:node_modules/@foxt/js-srp/examples/browser-esbuild/package.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"esbuild --minify --bundle --platform=browser --outfile=dist/index.js index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nFile v2.1.2:node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-nodejs-cjs-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.2:node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"node index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nArchive v2.1.1: 86 files, 225422 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (44964b), core/admin/public/images/community-link.png (70b), core/admin/public/images/community-qrcode.png (2608b), core/admin/public/index.html (22700b), core/admin/public/js/admin.js (64826b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2216b), core/admin/public/js/quick-start.js (4380b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (672b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (26057b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5836b), core/model-scorer.js (1920b), core/monitor-logger.js (6879b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (22261b), core/provider-ids.js (377b), core/proxy.js (61783b), core/route-engine.js (37459b), core/routing-strategy.js (3806b), core/server-client.js (21816b), core/server.js (47866b), core/stats.js (10070b), core/timeout-profile.js (1986b), core/update-service.js (8792b), node_modules/.package-lock.json (427b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/LICENSE (759b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (97573b), package.json (2659b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b), scripts/cron-setup.js (3233b)\n\nFile v2.1.1:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.1.1:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.1.1\",\n  \"publishedAt\": 1785232214421\n}\n\nFile v2.1.1:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```\n\nFile v2.1.1:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效\n\nFile v2.1.1:references/setup-guide.md\n\n# Setup 流程指南\r\n\r\n> Setup 流程的完整步骤,供 Agent 对话引导使用。\r\n\r\n## 核心原则\r\n\r\n- **多 Provider 优于单 Provider** — 引导用户尽可能提供多个 Key\r\n\r\n## 命令执行约束\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式 `node <skill_dir>/scripts/free-model-cli.js <command>`。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定。\r\n\r\n## 第一步:运行 setup\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\nsetup 自动完成:\r\n1. 启动 Router 服务(如果未运行)\r\n2. 注册 API Key、拉取 providers/models、自动选择主备模型\r\n   - **幂等性**:仅对尚未设置主模型的 provider 自动选择,保留用户手动配置\r\n3. 向 openclaw.json 写入 `free-model-router` provider(仅此一次,已存在则跳过)\r\n4. 输出 cron 任务注册指令\r\n\r\n> **已安装检测**:如果 Router 已运行且 OpenClaw 已配置,setup 会输出当前状态摘要而非重新配置。\r\n\r\n## 第二步:注册定时任务\r\n\r\n根据 setup 输出的指令,使用 OpenClaw 内置 cron 工具注册:\r\n\r\n> **幂等提示:** 注册前先用 `cron list` 检查任务是否已存在。如果任务已存在且配置相同,跳过即可。\r\n\r\n**任务 A:事件通知检查(每 30 分钟)**\r\n```\r\ncron add\r\n  name: free-model-router-event-check\r\n  schedule: */30 * * * *\r\n  instruction: 读取 <DATA_DIR>/events.json 中 status=pending 且 shouldNotify=true 的事件。如有待推送事件,向用户推送后标记 status=notified;如无待推送事件,保持静默,不输出任何内容\r\n```\r\n\r\n**任务 B:每日状态汇报(每天 9:00)**\r\n```\r\ncron add\r\n  name: free-model-router-daily-status\r\n  schedule: 0 9 * * *\r\n  instruction: 读取 <DATA_DIR>/router-config.json 中各 provider 的主备模型和运行状态,向用户汇报\r\n```\r\n\r\n> 如果 cron 不可用,提示用户重新注册定时任务。\r\n> 如果任务已存在,Agent 应跳过注册或仅更新指令内容,不创建重复任务。\r\n\r\n## 第三步:展示 Provider 列表,引导设置 API Key\r\n\r\n> **幂等提示:** 如果已有 Provider 配置了 API Key,第三步可以跳过或仅引导添加新的 Provider。\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providers\r\n```\r\n\r\n根据服务器返回的 providers 数据,向用户展示状态和注册网址:\r\n\r\n```\r\n支持的免费模型 Provider:\r\n\r\n{遍历 providers 数组,对每个 provider 显示:}\r\n✓ {name} — 已配置 API Key\r\n  网址: {url}\r\n\r\n? {name} — 未配置 API Key({freeQuota})\r\n  网址: {url}\r\n  注册指引: {keyGuide}\r\n\r\n建议多配置几个 Provider,提升稳定性。\r\n请回复需要配置的 Provider 和对应的 API Key\r\n```\r\n\r\n> **重要:** Provider 列表、网址、注册指引均由 model-server 动态返回,不要写死数据。展示时必须包含:\r\n> - `url` - 注册网址\r\n> - `freeQuota` - 免费额度说明\r\n> - `keyGuide` - API Key 获取指引(如果有)\r\n\r\n收到 Key 后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providerApiKey <provider> <key>\r\n```\r\n\r\n## 第四步:询问用户将 free-model-router 设为OpencClaw的主模型还是备用模型\r\n\r\n> **此步骤在至少一个 Provider 的 API Key 设置完成后执行。**\r\n\r\n向用户询问:\r\n\r\n```\r\nfree-model-router 已配置完成。\r\n请选择 free-model-router 在 OpenClaw 中的角色:\r\n\r\n选项 A:设为主模型(推荐) — 所有请求默认走免费模型路由,原主模型自动降为备用\r\n选项 B:设为备用模型 — 仅当原主模型不可用时使用免费模型路由\r\n\r\n请回复 \"主模型\" 或 \"备用模型\"\r\n```\r\n\r\n收到用户选择(未选择时默认设为备用模型)后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js configureModelRole <primary|fallback>\r\n```\r\n\r\n- 设为主模型后,`agents.defaults.model.primary` 将变为 `free-model-router/model-router`,原主模型自动移入 `fallbacks`\r\n- 设为备用模型后,`free-model-router/model-router` 将添加到 `agents.defaults.model.fallbacks` 列表末尾\r\n\r\n## 第五步:展示各 Provider 主备模型\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js models\r\n```\r\n\r\n向用户展示各 provider 的主备模型选择:\r\n\r\n```\r\n{遍历已配置的 providers:}\r\n{providerName}:\r\n  主模型: {primaryModel}\r\n  备模型: {fallbackModels}\r\n```\r\n\r\n用户可选择切换主模型:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js switchProviderPrimaryModel <provider> <modelId>\r\n```\r\n\r\n## 第六步:完成\r\n\r\n```\r\n全部完成!\r\nRouter 已启动: http://127.0.0.1:5678\r\nOpenClaw 已配置: free-model-router → 本地 Router\r\n主模型: xxx / 备模型: xxx, xxx\r\n事件通知: 每 30 分钟 / 状态汇报: 每天 9:00 / 故障切换: 自动\r\n\r\n所有模型切换在 Router 层透明完成,不再修改 openclaw.json。\r\n```\n\nFile v2.1.1:skill-card.md\n\n## Description: <br>\nFree Model Router provides OpenClaw with a local proxy for routing requests across free model providers, with setup guidance, automatic polling, failover, and diagnostics. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[laodao-agent](https://clawhub.ai/user/laodao-agent) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and OpenClaw users use this skill to configure and operate a persistent localhost model router, manage provider keys and model choices, and recover from provider or model failures. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill runs a persistent localhost model proxy with broad admin controls. <br>\nMitigation: Keep the router bound to localhost, avoid reverse proxies or shared hosts, and review admin actions before enabling or changing providers. <br>\nRisk: Provider keys are stored locally and may be exposed if the host or skill data directory is compromised. <br>\nMitigation: Use the skill only on trusted machines, restrict local account and file access, and rotate provider keys if local exposure is suspected. <br>\nRisk: Prompts and model responses are forwarded to third-party model providers. <br>\nMitigation: Review provider terms and avoid sending sensitive or regulated data unless the selected provider is approved for that use. <br>\nRisk: The security review flags under-disclosed update and data-flow behavior. <br>\nMitigation: Review update and admin features carefully, and prefer explicit /free-model-router commands for provider disabling, model switching, and other changes. <br>\n\n\n## Reference(s): <br>\n- [Setup Flow Guide](references/setup-guide.md) <br>\n- [Event Notification System](references/event-system.md) <br>\n- [Installation Idempotency Guide](references/idempotency.md) <br>\n- [ClawHub Skill Page](https://clawhub.ai/laodao-agent/skills/free-model-router-v1) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [guidance, shell commands, configuration, markdown] <br>\n**Output Format:** [Markdown with inline shell commands and concise status or configuration guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May propose OpenClaw configuration changes and local CLI commands that should be reviewed before execution.] <br>\n\n## Skill Version(s): <br>\n2.1.1 (source: server release, package.json, artifact metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v2.1.1:node_modules/.package-lock.json\n\n{\n  \"name\": \"free-model-router\",\n  \"version\": \"2.1.1\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"node_modules/@foxt/js-srp\": {\n      \"version\": \"0.0.3-patch2\",\n      \"resolved\": \"https://registry.npmmirror.com/@foxt/js-srp/-/js-srp-0.0.3-patch2.tgz\",\n      \"integrity\": \"sha512-mCjMIf+/mwmuzSGr8CWWQDjDUqMBnw9TBnzfcBUTR2ySPoXcg6Pfb4/WPklY1TwY3Pe3f3r8q6hzWvvogInuug==\",\n      \"license\": \"ISC\"\n    }\n  }\n}\n\nFile v2.1.1:node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-browser-esbuild-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.1:node_modules/@foxt/js-srp/examples/browser-esbuild/package.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"esbuild --minify --bundle --platform=browser --outfile=dist/index.js index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nFile v2.1.1:node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-nodejs-cjs-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.1:node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"node index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nArchive v2.1.0: 86 files, 225306 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (44964b), core/admin/public/images/community-link.png (70b), core/admin/public/images/community-qrcode.png (2608b), core/admin/public/index.html (22700b), core/admin/public/js/admin.js (64826b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2216b), core/admin/public/js/quick-start.js (4240b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (672b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (26057b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5836b), core/model-scorer.js (1920b), core/monitor-logger.js (6879b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (22261b), core/provider-ids.js (377b), core/proxy.js (61783b), core/route-engine.js (37459b), core/routing-strategy.js (3806b), core/server-client.js (21816b), core/server.js (47866b), core/stats.js (10070b), core/timeout-profile.js (1986b), core/update-service.js (8792b), node_modules/.package-lock.json (427b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/LICENSE (759b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (97573b), package.json (2659b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b), scripts/cron-setup.js (3233b)\n\nFile v2.1.0:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.1.0:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.1.0:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.1.0\",\n  \"publishedAt\": 1785226137304\n}\n\nFile v2.1.0:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```\n\nFile v2.1.0:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效\n\nFile v2.1.0:references/setup-guide.md\n\n# Setup 流程指南\r\n\r\n> Setup 流程的完整步骤,供 Agent 对话引导使用。\r\n\r\n## 核心原则\r\n\r\n- **多 Provider 优于单 Provider** — 引导用户尽可能提供多个 Key\r\n\r\n## 命令执行约束\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式 `node <skill_dir>/scripts/free-model-cli.js <command>`。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定。\r\n\r\n## 第一步:运行 setup\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\nsetup 自动完成:\r\n1. 启动 Router 服务(如果未运行)\r\n2. 注册 API Key、拉取 providers/models、自动选择主备模型\r\n   - **幂等性**:仅对尚未设置主模型的 provider 自动选择,保留用户手动配置\r\n3. 向 openclaw.json 写入 `free-model-router` provider(仅此一次,已存在则跳过)\r\n4. 输出 cron 任务注册指令\r\n\r\n> **已安装检测**:如果 Router 已运行且 OpenClaw 已配置,setup 会输出当前状态摘要而非重新配置。\r\n\r\n## 第二步:注册定时任务\r\n\r\n根据 setup 输出的指令,使用 OpenClaw 内置 cron 工具注册:\r\n\r\n> **幂等提示:** 注册前先用 `cron list` 检查任务是否已存在。如果任务已存在且配置相同,跳过即可。\r\n\r\n**任务 A:事件通知检查(每 30 分钟)**\r\n```\r\ncron add\r\n  name: free-model-router-event-check\r\n  schedule: */30 * * * *\r\n  instruction: 读取 <DATA_DIR>/events.json 中 status=pending 且 shouldNotify=true 的事件。如有待推送事件,向用户推送后标记 status=notified;如无待推送事件,保持静默,不输出任何内容\r\n```\r\n\r\n**任务 B:每日状态汇报(每天 9:00)**\r\n```\r\ncron add\r\n  name: free-model-router-daily-status\r\n  schedule: 0 9 * * *\r\n  instruction: 读取 <DATA_DIR>/router-config.json 中各 provider 的主备模型和运行状态,向用户汇报\r\n```\r\n\r\n> 如果 cron 不可用,提示用户重新注册定时任务。\r\n> 如果任务已存在,Agent 应跳过注册或仅更新指令内容,不创建重复任务。\r\n\r\n## 第三步:展示 Provider 列表,引导设置 API Key\r\n\r\n> **幂等提示:** 如果已有 Provider 配置了 API Key,第三步可以跳过或仅引导添加新的 Provider。\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providers\r\n```\r\n\r\n根据服务器返回的 providers 数据,向用户展示状态和注册网址:\r\n\r\n```\r\n支持的免费模型 Provider:\r\n\r\n{遍历 providers 数组,对每个 provider 显示:}\r\n✓ {name} — 已配置 API Key\r\n  网址: {url}\r\n\r\n? {name} — 未配置 API Key({freeQuota})\r\n  网址: {url}\r\n  注册指引: {keyGuide}\r\n\r\n建议多配置几个 Provider,提升稳定性。\r\n请回复需要配置的 Provider 和对应的 API Key\r\n```\r\n\r\n> **重要:** Provider 列表、网址、注册指引均由 model-server 动态返回,不要写死数据。展示时必须包含:\r\n> - `url` - 注册网址\r\n> - `freeQuota` - 免费额度说明\r\n> - `keyGuide` - API Key 获取指引(如果有)\r\n\r\n收到 Key 后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providerApiKey <provider> <key>\r\n```\r\n\r\n## 第四步:询问用户将 free-model-router 设为OpencClaw的主模型还是备用模型\r\n\r\n> **此步骤在至少一个 Provider 的 API Key 设置完成后执行。**\r\n\r\n向用户询问:\r\n\r\n```\r\nfree-model-router 已配置完成。\r\n请选择 free-model-router 在 OpenClaw 中的角色:\r\n\r\n选项 A:设为主模型(推荐) — 所有请求默认走免费模型路由,原主模型自动降为备用\r\n选项 B:设为备用模型 — 仅当原主模型不可用时使用免费模型路由\r\n\r\n请回复 \"主模型\" 或 \"备用模型\"\r\n```\r\n\r\n收到用户选择(未选择时默认设为备用模型)后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js configureModelRole <primary|fallback>\r\n```\r\n\r\n- 设为主模型后,`agents.defaults.model.primary` 将变为 `free-model-router/model-router`,原主模型自动移入 `fallbacks`\r\n- 设为备用模型后,`free-model-router/model-router` 将添加到 `agents.defaults.model.fallbacks` 列表末尾\r\n\r\n## 第五步:展示各 Provider 主备模型\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js models\r\n```\r\n\r\n向用户展示各 provider 的主备模型选择:\r\n\r\n```\r\n{遍历已配置的 providers:}\r\n{providerName}:\r\n  主模型: {primaryModel}\r\n  备模型: {fallbackModels}\r\n```\r\n\r\n用户可选择切换主模型:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js switchProviderPrimaryModel <provider> <modelId>\r\n```\r\n\r\n## 第六步:完成\r\n\r\n```\r\n全部完成!\r\nRouter 已启动: http://127.0.0.1:5678\r\nOpenClaw 已配置: free-model-router → 本地 Router\r\n主模型: xxx / 备模型: xxx, xxx\r\n事件通知: 每 30 分钟 / 状态汇报: 每天 9:00 / 故障切换: 自动\r\n\r\n所有模型切换在 Router 层透明完成,不再修改 openclaw.json。\r\n```\n\nFile v2.1.0:skill-card.md\n\n## Description: <br>\nRoutes OpenClaw model requests through a local free-model router that helps configure providers, choose models, fail over, and diagnose availability issues. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[laodao-agent](https://clawhub.ai/user/laodao-agent) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nOpenClaw users and developers use this skill to configure a local model-routing proxy, add provider API keys, select primary and fallback models, and recover from provider or model outages. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The persistent local router stores provider keys and its admin surface may expose secrets or broad local controls. <br>\nMitigation: Use only in a trusted local environment, restrict local access, review the admin panel before use, and avoid valuable provider keys until authentication and key redaction controls are confirmed. <br>\nRisk: Prompts and model responses are forwarded to external model providers selected through the router. <br>\nMitigation: Do not route sensitive prompts unless the selected provider's data handling and terms are acceptable for the user's workload. <br>\nRisk: The updater code path is high impact for a local agent skill that can run commands and modify configuration. <br>\nMitigation: Review update behavior and release integrity before enabling or executing update-related actions. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/laodao-agent/skills/free-model-router-v1) <br>\n- [Setup Guide](references/setup-guide.md) <br>\n- [Event System](references/event-system.md) <br>\n- [Idempotency Guide](references/idempotency.md) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown with inline shell commands and configuration guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May instruct the agent to run local Node.js CLI commands and guide OpenClaw configuration changes.] <br>\n\n## Skill Version(s): <br>\n2.1.0 (source: server release metadata and package.json) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v2.1.0:node_modules/.package-lock.json\n\n{\n  \"name\": \"free-model-router\",\n  \"version\": \"2.1.0\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"node_modules/@foxt/js-srp\": {\n      \"version\": \"0.0.3-patch2\",\n      \"resolved\": \"https://registry.npmmirror.com/@foxt/js-srp/-/js-srp-0.0.3-patch2.tgz\",\n      \"integrity\": \"sha512-mCjMIf+/mwmuzSGr8CWWQDjDUqMBnw9TBnzfcBUTR2ySPoXcg6Pfb4/WPklY1TwY3Pe3f3r8q6hzWvvogInuug==\",\n      \"license\": \"ISC\"\n    }\n  }\n}\n\nFile v2.1.0:node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-browser-esbuild-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.0:node_modules/@foxt/js-srp/examples/browser-esbuild/package.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"esbuild --minify --bundle --platform=browser --outfile=dist/index.js index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nFile v2.1.0:node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-nodejs-cjs-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.1.0:node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"node index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nArchive v2.0.2: 85 files, 216833 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (42186b), core/admin/public/images/community-link.png (70b), core/admin/public/images/community-qrcode.png (2608b), core/admin/public/index.html (22700b), core/admin/public/js/admin.js (58821b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2162b), core/admin/public/js/quick-start.js (4240b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (291b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (22906b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5769b), core/model-scorer.js (1920b), core/monitor-logger.js (6691b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (21052b), core/proxy.js (60195b), core/route-engine.js (30004b), core/routing-strategy.js (3784b), core/server-client.js (21816b), core/server.js (42120b), core/stats.js (8306b), core/timeout-profile.js (1986b), core/update-service.js (8792b), node_modules/.package-lock.json (427b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/LICENSE (759b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (97573b), package.json (2659b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b), scripts/cron-setup.js (3233b), scripts/diag-hotpools.js (2008b)\n\nFile v2.0.2:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.0.2:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.0.2\",\n  \"publishedAt\": 1784613059063\n}\n\nFile v2.0.2:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```\n\nFile v2.0.2:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效\n\nFile v2.0.2:references/setup-guide.md\n\n# Setup 流程指南\r\n\r\n> Setup 流程的完整步骤,供 Agent 对话引导使用。\r\n\r\n## 核心原则\r\n\r\n- **多 Provider 优于单 Provider** — 引导用户尽可能提供多个 Key\r\n\r\n## 命令执行约束\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式 `node <skill_dir>/scripts/free-model-cli.js <command>`。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定。\r\n\r\n## 第一步:运行 setup\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\nsetup 自动完成:\r\n1. 启动 Router 服务(如果未运行)\r\n2. 注册 API Key、拉取 providers/models、自动选择主备模型\r\n   - **幂等性**:仅对尚未设置主模型的 provider 自动选择,保留用户手动配置\r\n3. 向 openclaw.json 写入 `free-model-router` provider(仅此一次,已存在则跳过)\r\n4. 输出 cron 任务注册指令\r\n\r\n> **已安装检测**:如果 Router 已运行且 OpenClaw 已配置,setup 会输出当前状态摘要而非重新配置。\r\n\r\n## 第二步:注册定时任务\r\n\r\n根据 setup 输出的指令,使用 OpenClaw 内置 cron 工具注册:\r\n\r\n> **幂等提示:** 注册前先用 `cron list` 检查任务是否已存在。如果任务已存在且配置相同,跳过即可。\r\n\r\n**任务 A:事件通知检查(每 30 分钟)**\r\n```\r\ncron add\r\n  name: free-model-router-event-check\r\n  schedule: */30 * * * *\r\n  instruction: 读取 <DATA_DIR>/events.json 中 status=pending 且 shouldNotify=true 的事件。如有待推送事件,向用户推送后标记 status=notified;如无待推送事件,保持静默,不输出任何内容\r\n```\r\n\r\n**任务 B:每日状态汇报(每天 9:00)**\r\n```\r\ncron add\r\n  name: free-model-router-daily-status\r\n  schedule: 0 9 * * *\r\n  instruction: 读取 <DATA_DIR>/router-config.json 中各 provider 的主备模型和运行状态,向用户汇报\r\n```\r\n\r\n> 如果 cron 不可用,提示用户重新注册定时任务。\r\n> 如果任务已存在,Agent 应跳过注册或仅更新指令内容,不创建重复任务。\r\n\r\n## 第三步:展示 Provider 列表,引导设置 API Key\r\n\r\n> **幂等提示:** 如果已有 Provider 配置了 API Key,第三步可以跳过或仅引导添加新的 Provider。\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providers\r\n```\r\n\r\n根据服务器返回的 providers 数据,向用户展示状态和注册网址:\r\n\r\n```\r\n支持的免费模型 Provider:\r\n\r\n{遍历 providers 数组,对每个 provider 显示:}\r\n✓ {name} — 已配置 API Key\r\n  网址: {url}\r\n\r\n? {name} — 未配置 API Key({freeQuota})\r\n  网址: {url}\r\n  注册指引: {keyGuide}\r\n\r\n建议多配置几个 Provider,提升稳定性。\r\n请回复需要配置的 Provider 和对应的 API Key\r\n```\r\n\r\n> **重要:** Provider 列表、网址、注册指引均由 model-server 动态返回,不要写死数据。展示时必须包含:\r\n> - `url` - 注册网址\r\n> - `freeQuota` - 免费额度说明\r\n> - `keyGuide` - API Key 获取指引(如果有)\r\n\r\n收到 Key 后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js providerApiKey <provider> <key>\r\n```\r\n\r\n## 第四步:询问用户将 free-model-router 设为OpencClaw的主模型还是备用模型\r\n\r\n> **此步骤在至少一个 Provider 的 API Key 设置完成后执行。**\r\n\r\n向用户询问:\r\n\r\n```\r\nfree-model-router 已配置完成。\r\n请选择 free-model-router 在 OpenClaw 中的角色:\r\n\r\n选项 A:设为主模型(推荐) — 所有请求默认走免费模型路由,原主模型自动降为备用\r\n选项 B:设为备用模型 — 仅当原主模型不可用时使用免费模型路由\r\n\r\n请回复 \"主模型\" 或 \"备用模型\"\r\n```\r\n\r\n收到用户选择(未选择时默认设为备用模型)后调用:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js configureModelRole <primary|fallback>\r\n```\r\n\r\n- 设为主模型后,`agents.defaults.model.primary` 将变为 `free-model-router/model-router`,原主模型自动移入 `fallbacks`\r\n- 设为备用模型后,`free-model-router/model-router` 将添加到 `agents.defaults.model.fallbacks` 列表末尾\r\n\r\n## 第五步:展示各 Provider 主备模型\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js models\r\n```\r\n\r\n向用户展示各 provider 的主备模型选择:\r\n\r\n```\r\n{遍历已配置的 providers:}\r\n{providerName}:\r\n  主模型: {primaryModel}\r\n  备模型: {fallbackModels}\r\n```\r\n\r\n用户可选择切换主模型:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js switchProviderPrimaryModel <provider> <modelId>\r\n```\r\n\r\n## 第六步:完成\r\n\r\n```\r\n全部完成!\r\nRouter 已启动: http://127.0.0.1:5678\r\nOpenClaw 已配置: free-model-router → 本地 Router\r\n主模型: xxx / 备模型: xxx, xxx\r\n事件通知: 每 30 分钟 / 状态汇报: 每天 9:00 / 故障切换: 自动\r\n\r\n所有模型切换在 Router 层透明完成,不再修改 openclaw.json。\r\n```\n\nFile v2.0.2:skill-card.md\n\n## Description: <br>\nProvides OpenClaw with a local free-model router that configures providers, routes requests, rotates and fails over models, and helps diagnose model availability issues. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[laodao-agent](https://clawhub.ai/user/laodao-agent) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and OpenClaw users use this skill to set up and operate a local model-routing proxy that selects free providers, switches primary or fallback models, and diagnoses provider availability problems. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill runs a persistent local router with access to OpenClaw configuration, provider API keys, local admin APIs, and device identifiers. <br>\nMitigation: Keep the router bound to localhost, do not expose its port, and review the admin-console behavior before installation. <br>\nRisk: Model prompts and responses are forwarded to external model providers selected through the router. <br>\nMitigation: Use only providers acceptable for the data being sent, and avoid routing sensitive prompts through untrusted external providers. <br>\nRisk: Optional reporting and telemetry can share health metadata and device identifiers with model-server infrastructure. <br>\nMitigation: Leave reporting disabled unless the user accepts the metadata sharing, and disable it with the documented reporting controls when not needed. <br>\nRisk: Update and account-control paths may affect local router behavior after installation. <br>\nMitigation: Review update behavior and account-control functions before use, and restart the router after skill updates so the active process matches the installed files. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/laodao-agent/skills/free-model-router-v1) <br>\n- [Setup guide](references/setup-guide.md) <br>\n- [Event system](references/event-system.md) <br>\n- [Idempotency guide](references/idempotency.md) <br>\n- [Qwen3 Coder free model reference](https://openrouter.ai/models/qwen/qwen3-coder:free) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance] <br>\n**Output Format:** [Markdown guidance with inline shell commands and configuration steps] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May instruct the agent to run local Node.js CLI commands and update OpenClaw model-routing configuration.] <br>\n\n## Skill Version(s): <br>\n2.0.2 (source: server release evidence and package.json) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v2.0.2:node_modules/.package-lock.json\n\n{\n  \"name\": \"free-model-router\",\n  \"version\": \"2.0.2\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"node_modules/@foxt/js-srp\": {\n      \"version\": \"0.0.3-patch2\",\n      \"resolved\": \"https://registry.npmmirror.com/@foxt/js-srp/-/js-srp-0.0.3-patch2.tgz\",\n      \"integrity\": \"sha512-mCjMIf+/mwmuzSGr8CWWQDjDUqMBnw9TBnzfcBUTR2ySPoXcg6Pfb4/WPklY1TwY3Pe3f3r8q6hzWvvogInuug==\",\n      \"license\": \"ISC\"\n    }\n  }\n}\n\nFile v2.0.2:node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-browser-esbuild-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.0.2:node_modules/@foxt/js-srp/examples/browser-esbuild/package.json\n\n{\n  \"name\": \"js-srp-browser-esbuild-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"esbuild --minify --bundle --platform=browser --outfile=dist/index.js index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nFile v2.0.2:node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 2,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"js-srp-nodejs-cjs-example\",\n      \"version\": \"1.0.0\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"js-srp\": \"file:../..\"\n      }\n    },\n    \"../..\": {\n      \"version\": \"0.0.2\",\n      \"license\": \"ISC\",\n      \"devDependencies\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    },\n    \"node_modules/js-srp\": {\n      \"resolved\": \"../..\",\n      \"link\": true\n    }\n  },\n  \"dependencies\": {\n    \"js-srp\": {\n      \"version\": \"file:../..\",\n      \"requires\": {\n        \"@types/node\": \"^18.7.23\",\n        \"esbuild\": \"^0.16.3\",\n        \"typescript\": \"^4\"\n      }\n    }\n  }\n}\n\nFile v2.0.2:node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json\n\n{\n  \"name\": \"js-srp-nodejs-cjs-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"node index.js && tsc\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"js-srp\": \"file:../..\"\n  }\n}\n\nArchive v2.0.1: 81 files, 212523 bytes\n\nFiles: _meta.json (139b), core/admin/admin-routes.js (3598b), core/admin/auth.js (5272b), core/admin/public/css/style.css (41472b), core/admin/public/index.html (21997b), core/admin/public/js/admin.js (57102b), core/admin/public/js/clipboard.js (480b), core/admin/public/js/entitlements-cache.js (1241b), core/admin/public/js/entitlements-render.js (6975b), core/admin/public/js/modal.js (2162b), core/admin/public/js/quick-start.js (4240b), core/admin/public/js/runtime-matrix.js (2301b), core/admin/public/js/toast.js (291b), core/admin/public/login.html (4519b), core/admin/public/setup.html (4448b), core/admin/srp.js (4796b), core/anthropic-transform.js (14544b), core/config.js (22676b), core/context-window.js (1285b), core/entitlements-cache.js (2383b), core/fingerprint.js (1277b), core/health-check-task.js (1281b), core/health-check.js (5617b), core/model-scorer.js (1920b), core/monitor-logger.js (6691b), core/network-compat.js (475b), core/notifications.js (7758b), core/probe-service.js (21000b), core/proxy.js (59958b), core/route-engine.js (29773b), core/routing-strategy.js (3784b), core/server-client.js (21816b), core/server.js (41327b), core/stats.js (8306b), core/update-service.js (8792b), node_modules/.package-lock.json (441b), node_modules/@foxt/js-srp/.github/workflows/nodejs.yml (461b), node_modules/@foxt/js-srp/.github/workflows/publish.yml (696b), node_modules/@foxt/js-srp/dist/index.browser.js (12320b), node_modules/@foxt/js-srp/dist/index.d.ts (112b), node_modules/@foxt/js-srp/dist/index.node.cjs (12767b), node_modules/@foxt/js-srp/dist/index.node.mjs (12345b), node_modules/@foxt/js-srp/dist/srp.d.ts (1561b), node_modules/@foxt/js-srp/dist/test.d.ts (11b), node_modules/@foxt/js-srp/dist/util.d.ts (988b), node_modules/@foxt/js-srp/dist/webcrypto.browser.d.ts (37b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.cjs.d.ts (119b), node_modules/@foxt/js-srp/dist/webcrypto.nodejs.esm.d.ts (114b), node_modules/@foxt/js-srp/examples/browser-esbuild/index.js (97b), node_modules/@foxt/js-srp/examples/browser-esbuild/package-lock.json (790b), node_modules/@foxt/js-srp/examples/browser-esbuild/package.json (319b), node_modules/@foxt/js-srp/examples/nodejs-cjs/index.js (102b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-cjs/package.json (250b), node_modules/@foxt/js-srp/examples/nodejs-cjs/tsconfig.json (170b), node_modules/@foxt/js-srp/examples/nodejs-esm/index.js (97b), node_modules/@foxt/js-srp/examples/nodejs-esm/package-lock.json (780b), node_modules/@foxt/js-srp/examples/nodejs-esm/package.json (270b), node_modules/@foxt/js-srp/examples/nodejs-esm/tsconfig.json (170b), node_modules/@foxt/js-srp/package.json (1674b), node_modules/@foxt/js-srp/README.md (5271b), node_modules/@foxt/js-srp/src/index.ts (115b), node_modules/@foxt/js-srp/src/srp.ts (16771b), node_modules/@foxt/js-srp/src/test.ts (2437b), node_modules/@foxt/js-srp/src/util.ts (7309b), node_modules/@foxt/js-srp/src/webcrypto.browser.ts (35b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.cjs.ts (118b), node_modules/@foxt/js-srp/src/webcrypto.nodejs.esm.ts (70b), node_modules/@foxt/js-srp/tsconfig.json (617b), package-lock.json (100477b), package.json (2571b), references/event-system.md (3346b), references/idempotency.md (1906b), references/setup-guide.md (5164b), scripts/cron-setup.js (3233b), scripts/diag-hotpools.js (2008b), scripts/free-model-cli.js (18013b), scripts/openclaw-setup.js (5560b), scripts/router.js (291b), skill-card.md (2565b)\n\nFile v2.0.1:SKILL.md\n\n---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skills/free-model-router`）。如需切换目录,应使用 OpenClaw 的 `workingDirectory` 参数指定,而不是在命令中使用 `cd &&`。\r\n\r\n## 常用命令\r\n\r\n查看完整命令列表:\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js help\r\n```\r\n\r\n## 意图匹配\r\n\r\n| 用户说 | 动作 |\r\n|--------|------|\r\n| `/free-model-router setup`、\"开始配置\"、\"安装免费模型路由\" | Setup 流程 |\r\n| `/free-model-router`、`/free-model-router status` | 运行 providers 查看状态 |\r\n| `/free-model-router providers`、\"有哪些免费模型provider\" | 运行 providers |\r\n| `/free-model-router models`、\"有哪些免费模型\" | 运行 models |\r\n| `/free-model-router models xxx`、\"xxx 有哪些模型\" | 运行 models xxx |\r\n| `/free-model-router providerApiKey xxx yyy` | 设置 provider API Key |\r\n| `/free-model-router switchProviderPrimaryModel xxx yyy`、\"换成 yyy\" | 切换主模型 |\r\n| `/free-model-router disableProvider xxx`、\"禁用 xxx\" | 禁用 provider |\r\n| `/free-model-router enableProvider xxx`、\"启用 xxx\" | 启用 provider |\r\n| `/free-model-router configureModelRole primary`、\"设为主模型\"、\"作为主模型\" | 设为 OpenClaw 主模型 |\r\n| `/free-model-router configureModelRole fallback`、\"设为备用模型\"、\"作为备用模型\" | 设为 OpenClaw 备用模型 |\r\n| `/free-model-router stop`、\"停止 Router\" | 停止 Router 进程 |\r\n| `/free-model-router uninstall`、\"卸载技能\" | 卸载 free-model-router |\r\n| \"openclaw 挂了\"、\"模型不可用\" | 检查 provider 状态 → 诊断 → 引导修复 |\r\n\r\n## 意图识别规则\r\n\r\n### 1. 用户主动切换模型\r\n\r\n用户说 \"换成/切换到/换用 + 模型名\" → 解析后在已配置 Provider 中查找并调用 switchProviderPrimaryModel\r\n\r\n**歧义处理:**\r\n- 用户提供了完整模型 ID(如 `qwen/qwen3-coder:free`) → 直接切换\r\n- 用户只说了模型名(如 \"qwen3-coder\") → 在已配置 Provider 中查找匹配:\r\n  - 仅一个 Provider 有 → 自动使用该 Provider 的完整 ID\r\n  - 多个 Provider 都有 → 请用户确认使用哪个 Provider\r\n  - 找不到匹配 → 提示用户输入完整 ID 或先运行 models 查看\r\n- 用户未指定 Provider → 先运行 models 列出各 Provider 的主模型,请用户确认\r\n\r\n### 2. 用户引导配置(首次)\r\n\r\n触发 Setup 流程逐步引导,详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n### 3. 兜底机制\r\n\r\nopenclaw 无法回复时:1) 检查 Router 状态 → 2) 检查各 provider 状态 → 3) 如有故障切换,汇报结果\r\n\r\n## Setup 流程\r\n\r\n> **核心原则:** 多 Provider 优于单 Provider。引导用户尽可能提供多个 Key。\r\n\r\n完整 6 步流程详见 [references/setup-guide.md](references/setup-guide.md)\r\n\r\n**流程概览:**\r\n1. 运行 `setup` 命令 — 启动 Router + 初始化 + OpenClaw 配置\r\n2. 注册定时任务 — 事件检查(北京时间 08:00~22:00 每30分钟) + 每日状态汇报(北京时间每天9:00)\r\n3. 展示 Provider 列表,引导设置 API Key\r\n4. [重要]主动询问用户将free-model-router设为主模型还是备用模型，否则技能将不会生效\r\n5. 展示各 Provider 主备模型,可选择切换\r\n6. 完成配置\r\n\r\n## 事件通知系统\r\n\r\nRouter 内置事件通知系统,自动推送模型切换、故障等事件。\r\n\r\n详见 [references/event-system.md](references/event-system.md)\r\n\r\n**核心机制:**\r\n- 事件状态: `pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n- Cron 在北京时间 08:00~22:00 期间每 30 分钟读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n- 推送后调用 `mark-notified` 标记,避免重复推送\r\n- **无事件时保持静默**: 如果没有待推送事件,Cron 不会向用户发送任何消息\r\n\r\n**公告事件超链接:**\r\n- 公告事件可能包含 `metadata.url` 链接\r\n- 推送时使用 Markdown 格式展示: `[🔗 {action}]({url})`\r\n- 详见 [event-system.md 公告格式](references/event-system.md#公告事件格式)\r\n\r\n## 停止与卸载\r\n\r\n### 停止 Router\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n```\r\n\r\n- 读取 PID 文件并发送 SIGTERM 信号\r\n- 自动清理 PID 文件\r\n- 如果 Router 未运行，输出提示信息\r\n\r\n### 卸载\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js uninstall\r\n```\r\n\r\n- 自动停止 Router 进程\r\n- 移除 OpenClaw 配置中的 free-model-router provider\r\n- 卸载后请在 OpenClaw 技能管理界面中移除 free-model-router 技能以清理 cron 任务\r\n\r\n## 注意事项\r\n\r\n1. **API Key 安全:** 仅存本地 router-config.json,不发送到外部\r\n2. **模型可用性:** 免费模型随时可能下架/收费\r\n3. **配置安全:** 所有修改自动备份,失败自动回滚\r\n4. **配置零侵入:** OpenClaw 配置仅在 setup 和配置模型角色时修改,运行期间不再触碰\r\n\r\n## 📚 参考文档\r\n\r\n| 文档 | 内容 |\r\n|------|------|\r\n| [references/setup-guide.md](references/setup-guide.md) | Setup 完整 6 步流程 |\r\n| [references/event-system.md](references/event-system.md) | 事件通知系统详解 |\r\n| [references/idempotency.md](references/idempotency.md) | 安装幂等性说明 |\n\nFile v2.0.1:node_modules/@foxt/js-srp/README.md\n\n# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n            salt, iterations\n        );\n        this.srpClient.p = derived;\n        await this.srpClient.generate(salt, serverPub);\n        let m1 = Buffer.from(this.srpClient._M).toString(\"base64\");\n        let M2 = await this.srpClient.generateM2();\n        let m2 = Buffer.from(M2).toString(\"base64\");\n        return {\n            accountName: this.username,\n            m1,\n            m2,\n            c: serverData.c,\n        };\n    }\n}\n```\n\n### API interop\n```ts\nimport prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})\n\n\n```\n\nFile v2.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.0.1\",\n  \"publishedAt\": 1784297649043\n}\n\nFile v2.0.1:references/event-system.md\n\n# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| ...","readmeExcerpt":"Skill: free-model-router-laodao Owner: laodao-agent Summary: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Tags: latest:2.2.0 Version history: v2.2.0 | 2026-08-12T04:04:59.191Z | user v2.2.0 更新内容 改进 - **额度耗尽与限流的恢复更可靠**：402 额度耗尽、长期 429 限流不再走固定冷却时间，改为持续探测成功才恢复，避免额度未恢复时反复失败 - **单次 429 限流不再触发长冷却**：单次限流不再立即把 Provider 长时间禁用，按累计次数逐步升档；累计清零后立即恢复 - **单个模型持续失败不再拖累整个 Provider**：单","codeSnippets":[],"executableExamples":[{"language":"ts","snippet":"import { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            ut"},{"language":"ts","snippet":"import prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})"},{"language":"text","snippet":"File v2.2.0:references/idempotency.md\n\n# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:"},{"language":"text","snippet":"File v2.2.0:skill-card.md\n\n## Description:\n\nProvides OpenClaw with a local free-model routing proxy for provider setup, automatic polling, failover, model switching, and availability diagnostics.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[laodao-agent](https://clawhub.ai/user/laodao-agent)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal developers and OpenClaw users use this skill to configure a local router that selects available free model providers, switches models, and diagnoses provider availability issues. It is intended for users who are comfortable running a persistent local proxy and reviewing provider data handling.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill runs a persistent local router and stores provider API keys.\n\nMitigation: Install only when the local router is needed, keep provider keys scoped and revocable, and stop or uninstall the router when it is no longer in use.\n\nRisk: The local admin surface may expose keys or high-impact controls too broadly.\n\nMitigation: Treat the admin UI as sensitive and avoid running the router while browsing untrusted sites until the local admin and CORS behavior has been reviewed and fixed.\n\nRisk: The skill communicates with freemodel control servers for registration, provider metadata, and health reporting.\n\nMitigation: Review the configured control-server communication and leave optional health reporting disabled unless the user intentionally enables it.\n\nRisk: Displayed curl-to-shell update commands can increase execution risk.\n\nMitigation: Avoid curl-to-shell updates; inspect downloaded update content and run explicit commands from trusted release artifacts instead.\n\nRisk: Scheduled tasks may remain after uninstall.\n\nMitigation: After uninstalling, verify and remove any remaining scheduled tasks associated with free-model-router.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/laodao-agent/skills/free-model-rout"},{"language":"ts","snippet":"import { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            ut"},{"language":"ts","snippet":"import prompt from \"prompt\";\nimport type { ServerSRPInitResponse } from \"./GSASRPAuthenticator.js\";\nimport { GSASRPAuthenticator } from \"./GSASRPAuthenticator.js\";\n\nasync function request(url: string, body: any) {\n    let req = await fetch(\"https://idmsa.apple.com/appleauth/auth/signin\" + url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        },\n        body: JSON.stringify(body)\n    })\n    console.log()\n    console.log(\"POST\", url)\n    console.log(\"    \", req.status, req.statusText)\n\n    if (!req.ok) throw new Error(\n        \"Failed to get init response \" + req.status + \" \" + req.statusText + \": \" \n        + await req.text()\n    );\n    return await req.json();\n}\n\n\nasync function login(username: string, password: string) {\n    // set up SRP authenticator & get public key\n    let authenticator = new GSASRPAuthenticator(username);\n    let initData = await authenticator.getInit();\n\n    // request SRP init data from server\n    let initResp = await request(\"/init\", initData)\n    \n\n    // get proof of password\n    let proof = await authenticator.getComplete(password, initResp as ServerSRPInitResponse);\n\n    // send proof to server\n    let completeResp = await request(\"/complete\", {\n            ...proof,\n            rememberMe: true,\n            trustTokens: []\n    })\n    console.log(completeResp)\n\n    \n}\nprompt.start();\nprompt.get({\n    properties: {\n        username: {\n            description: \"Apple ID\"\n        },\n        password: {\n            description: \"Password\",\n            hidden: true\n        }\n    }\n}, (err, result) => {\n    if (err) return console.error(err);\n    login(result.username as string, result.password as string).catch(console.error);\n})"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\r\nname: free-model-router\r\ndescription: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。\r\n---\r\n\r\n# free-model-router\r\n\r\n> 免费模型智能路由 — 本地代理,自动配置,自动轮询,故障切换,配置零侵入\r\n\r\n引导注册免费模型,永久在线,实现token自由。自动同步、自动检测、自动切换、自动救援。\r\n\r\n## 数据流向说明\r\n\r\n本技能的数据流向分为三层，所有行为均透明可控：\r\n\r\n```\r\nLayer 1: 用户 → OpenClaw → 127.0.0.1:5678 本地 Router\r\n         （数据不离开本机）\r\n\r\nLayer 2: 本地 Router → 外部模型 Provider（OpenRouter、NVIDIA NIM 等）\r\n         （提示词和模型响应经过 Provider 服务器）\r\n\r\nLayer 3: 本地 Router ↔ freemodel 控制服务器\r\n         （仅同步 provider 列表、模型 ID、健康状态，不含对话内容）\r\n```\r\n\r\n| 行为 | 说明 | 用户控制 |\r\n|------|------|----------|\r\n| **外部通信** | 向 `freemodel.eu.org` 等服务器注册匿名 API Key、拉取 provider 列表和模型元数据 | 仅同步 provider 配置，**不含任何用户对话内容** |\r\n| **数据上报** | 可选的匿名健康状态上报（模型延迟、可用性） | **默认关闭**，可随时通过 `reporting off` 命令或管理面板开启/关闭 |\r\n| **设备指纹** | SHA256(hostname + homedir + platform + arch) 不可逆哈希 | 仅用于区分设备，**无法反推原始信息** |\r\n| **配置修改** | 仅在 setup 时向 `openclaw.json` 添加 free-model-router provider | 自动备份（最多3份）+ 失败自动回滚，幂等设计 |\r\n| **本地代理** | `127.0.0.1:5678` 仅监听本地回环 | **不暴露公网**，仅接收 OpenClaw 内部请求 |\r\n| **推荐码** | 完全自愿的推荐试用系统 | 不参与不影响任何免费功能 |\r\n\r\n> **重要提示**：你的提示词和模型响应会被转发到外部模型 Provider（如 OpenRouter、NVIDIA NIM 等），请知悉。本技能不收集、不传输、不存储任何用户对话内容、提示词或模型响应数据。所有外部请求仅涉及模型配置元数据。\r\n\r\n## 核心架构\r\n\r\n- **OpenClaw 配置只改一次** — 仅在 setup 时写入 `free-model-router` provider 指向本地 Router (127.0.0.1:5678)\r\n- **所有模型切换在 Router 层透明完成** — 运行期间不再触碰 openclaw.json,避免 Gateway 宕机\r\n- **Sticky 粘性路由** — 默认策略，同能力池内优先命中上次成功的 Provider，失败时自动切换并支持跨池救援\r\n- **自动故障切换** — 主模型故障自动切换到备模型,所有 provider 不可用时自动降级\r\n- **事件通知系统** — 模型切换、故障等事件写入 events.json,由 cron 任务推送\r\n\r\n## 安装幂等性\r\n\r\n**本技能支持幂等安装 — 多次触发 setup 不会产生副作用。**\r\n\r\n详见 [references/idempotency.md](references/idempotency.md)\r\n\r\n核心要点:\r\n- Router 已运行则跳过,OpenClaw 已配置则跳过\r\n- 重复执行时仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- 用户手动配置的 API Key 和主模型值始终保留\r\n\r\n## ⚠️ 版本更新后必须重启 Router\r\n\r\n**Skill 更新只会替换磁盘文件，不会自动重启正在运行的 Router 进程。** 旧进程仍在使用旧代码，新功能/修复不会生效。\r\n\r\n**必须在 skill 更新后执行以下操作：**\r\n\r\n```bash\r\n# 1. 停止旧进程\r\nnode <skill_dir>/scripts/free-model-cli.js stop\r\n\r\n# 2. 重新启动（setup 会自动启动 Router 并完成初始化）\r\nnode <skill_dir>/scripts/free-model-cli.js setup\r\n```\r\n\r\n> **注意：** `stop` + `setup` 是安全的——Router 配置、API Key、主备模型等用户数据均持久化在 `~/.free-model-router/data/router-config.json` 中，重启不会丢失。\r\n\r\n## 配置流程触发\r\n\r\n安装/识别技能后,向用户展示:\r\n\r\n**选项 A:现在设置(推荐)** — 回复 \"现在设置\"/\"开始配置\"\r\n**选项 B:延后设置** — 回复 \"延后设置\"/\"稍后再说\"\r\n\r\n配置完成后,Router 注册两个定时任务(北京时间 UTC+8):\r\n- **事件通知检查**: 每天 10:00、15:00、20:00 检查一次(避免夜间打扰)\r\n- **每日状态汇报**: 每天 9:00 自动汇报各 provider 运行状态\r\n\r\n> **注意:** 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置。\r\n\r\n> **CLI 路径:** 下文所有命令中的 `<skill_dir>` 指技能安装目录。默认路径为 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种的安装目录和技能名称（例如 `~/.kimi-openclaw/skills/free-model-router`）。\r\n\r\n## 命令执行约束\r\n\r\n**禁止复合命令:** OpenClaw 的 exec 工具不支持 `cd ... && node ...` 或 `bash -c \"...\"` 等复合命令。所有命令必须使用直接调用格式:\r\n\r\n```bash\r\nnode <skill_dir>/scripts/free-model-cli.js <command>\r\n```\r\n\r\n其中 `<skill_dir>` 的完整路径取决于当前 OpenClaw 变种的安装目录（例如 `~/.openclaw/skills/free-model-router` 或 `~/.kimi-openclaw/skill"},{"path":"node_modules/@foxt/js-srp/README.md","content":"# js-srp-gsa\n\nThis is a fork of [`js-srp`](https://github.com/jchv/js-srp) that adds support for the variant of SRP used by Apple icloud.com.\n\n## Example usage\n\n### SRP wrapper\n```ts\nimport { Client, Hash, Mode, Srp, util } from \"@foxt/js-srp\";\nimport crypto from \"crypto\";\n\nexport type SRPProtocol = \"s2k\" | \"s2k_fo\";\n\nexport interface ServerSRPInitRequest {\n    a: string;\n    accountName: string;\n    protocols: SRPProtocol[];\n}\nexport interface ServerSRPInitResponse {\n    iteration: number;\n    salt: string;\n    protocol: \"s2k\" | \"s2k_fo\";\n    b: string;\n    c: string;\n}\nexport interface ServerSRPCompleteRequest {\n    accountName: string;\n    c: string;\n    m1: string;\n    m2: string;\n    rememberMe: boolean;\n    trustTokens: string[];\n}\n\nlet srp = new Srp(Mode.GSA, Hash.SHA256, 2048);\nconst stringToU8Array = (str: string) => new TextEncoder().encode(str);\nconst base64ToU8Array = (str: string) => Uint8Array.from(Buffer.from(str, \"base64\"));\nexport class GSASRPAuthenticator {\n    constructor(private username: string) { }\n    private srpClient?: Client = undefined;\n\n\n    private async derivePassword(protocol: \"s2k\" | \"s2k_fo\", password: string, salt: Uint8Array, iterations: number) {\n        let passHash = new Uint8Array(await util.hash(srp.h, stringToU8Array(password)));\n        if (protocol == \"s2k_fo\") {\n            passHash = stringToU8Array(util.toHex(passHash));\n        }\n\n        let imported = await crypto.subtle.importKey(\n            \"raw\",\n            passHash,\n            { name: \"PBKDF2\" },\n            false,\n            [\"deriveBits\"]\n        );\n        let derived = await crypto.subtle.deriveBits({\n            name: \"PBKDF2\",\n            hash: { name: \"SHA-256\" },\n            iterations, salt\n        }, imported, 256);\n\n        return new Uint8Array(derived);\n    }\n\n\n    async getInit(): Promise<ServerSRPInitRequest> {\n        if (this.srpClient) throw new Error(\"Already initialized\");\n        this.srpClient = await srp.newClient(\n            stringToU8Array(this.username),\n            // provide fake passsword because we need to get data from server\n            new Uint8Array()\n        );\n        let a = Buffer.from(\n            util.bytesFromBigint(this.srpClient.A)\n        ).toString(\"base64\");\n        return {\n            a, protocols: [\"s2k\", \"s2k_fo\"],\n            accountName: this.username,\n        };\n    }\n    async getComplete(password: string, serverData: ServerSRPInitResponse): Promise<Pick<ServerSRPCompleteRequest, \"m1\" | \"m2\" | \"c\" | \"accountName\">> {\n        if (!this.srpClient) throw new Error(\"Not initialized\");\n        if ((serverData.protocol != \"s2k\") &&\n            (serverData.protocol != \"s2k_fo\")) throw new Error(\"Unsupported protocol \" + serverData.protocol);\n        let salt = base64ToU8Array(serverData.salt);\n        let serverPub = base64ToU8Array(serverData.b);\n        let iterations = serverData.iteration;\n        let derived = await this.derivePassword(\n            serverData.protocol, password,\n         "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn70ctf7xm30wgwwzbemy34ydd84xyqy\",\n  \"slug\": \"free-model-router-v1\",\n  \"version\": \"2.2.0\",\n  \"publishedAt\": 1786507499191\n}"},{"path":"references/event-system.md","content":"# 事件通知系统\r\n\r\n> Router 内置事件通知系统,文件位于 `<skill_dir>/data/events.json`（`<skill_dir>` 为技能安装目录,默认 `~/.openclaw/skills/free-model-router`，具体路径取决于当前 OpenClaw 变种）。\r\n\r\n## 事件类型\r\n\r\n| 类型 | 优先级 | 说明 |\r\n|------|--------|------|\r\n| `announce` | normal | model-server 公告 |\r\n| `modelFailover` | high | 模型故障自动切换 |\r\n| `modelUnavailable` | critical | 模型完全不可用 |\r\n| `dailyStatus` | normal | 每日状态汇报 |\r\n| `providerAdded` | normal | 新增 provider |\r\n| `providerRemoved` | normal | 移除 provider |\r\n\r\n## 事件状态\r\n\r\n`pending`(待推送) → `notified`(已推送) → `read`(已读)\r\n\r\n## 公告事件格式\r\n\r\n公告事件可能包含可点击的链接,格式如下:\r\n\r\n```json\r\n{\r\n  \"id\": \"{eventId}\",\r\n  \"title\": \"{公告标题}\",\r\n  \"content\": \"{公告内容描述}\",\r\n  \"type\": \"{类型: info/important/warning}\",\r\n  \"metadata\": {\r\n    \"url\": \"{链接地址}\",\r\n    \"action\": \"{链接按钮文本}\"\r\n  }\r\n}\r\n```\r\n\r\n### 推送格式规范\r\n\r\n推送公告时,**根据 metadata.url 决定是否展示超链接**:\r\n\r\n**有链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n\r\n[🔗 {metadata.action 或 \"查看详情\"}]({metadata.url})\r\n```\r\n\r\n**无链接时:**\r\n```\r\n📢 公告: {title}\r\n\r\n{content}\r\n```\r\n\r\n### 超链接展示要求\r\n\r\n1. **必须使用 Markdown 链接格式**: `[显示文本](URL)`\r\n2. **链接文本优先级**: `metadata.action` > \"查看详情\"\r\n3. **链接位置**: 放在内容下方，单独一行\r\n4. **链接图标**: 使用 🔗 前缀增强可识别性\r\n\r\n### 示例\r\n\r\n**带链接的公告:**\r\n```markdown\r\n📢 公告: 新模型上线\r\n\r\nQwen3-Coder 免费模型已上线，支持代码补全和对话。\r\n\r\n[🔗 立即体验](https://openrouter.ai/models/qwen/qwen3-coder:free)\r\n```\r\n\r\n**不带链接的公告:**\r\n```markdown\r\n📢 公告: 系统维护通知\r\n\r\n今晚 02:00-04:00 进行系统维护，期间服务可能不稳定。\r\n```\r\n\r\n> **注意:** 公告内容完全由 model-server 动态返回,不要写死数据。如果 `metadata.url` 存在,**在域名校验通过后**向用户展示该链接;`metadata.action` 为链接的可选显示文本。\r\n\r\n### 链接安全\r\n\r\n公告链接的域名必须经过白名单校验。允许的域名：\r\n- `freemodel.eu.org`\r\n- `freemodel.dpdns.org`\r\n\r\n**通过校验**：正常渲染为 Markdown 链接 `[点击查看](url)`。\r\n**未通过校验**：不渲染为可点击链接，改为纯文本展示 `相关链接（未通过安全校验）: url`，并附加警告提示。\r\n\r\n## Cron 读取方式\r\n\r\n1. 读取 `status=pending` 且 `shouldNotify=true` 的事件\r\n2. 向用户推送\r\n3. 推送后调用 `node <skill_dir>/scripts/free-model-cli.js mark-notified <eventId,...>` 标记已推送,避免重复\r\n\r\n## 事件推送示例\r\n\r\n**模型故障切换:**\r\n```\r\n⚠️ 模型故障切换\r\nProvider openrouter 主模型 qwen/qwen3-coder:free 故障\r\n已自动切换到备模型: anthropic/claude-sonnet-4:free\r\n```\r\n\r\n**模型完全不可用:**\r\n```\r\n🚨 模型不可用\r\nProvider openrouter 所有模型均不可用\r\n请检查 API Key 或稍后重试\r\n```\r\n\r\n**每日状态汇报:**\r\n```\r\n📊 每日状态汇报\r\nopenrouter: healthy (主模型 qwen/qwen3-coder:free)\r\nmodelscope: healthy (主模型 qwen/qwen3:free)\r\n```"},{"path":"references/idempotency.md","content":"# 安装幂等性指南\r\n\r\n> 本技能支持幂等安装 — 多次触发 setup 不会产生副作用。\r\n\r\n## 已安装状态检测\r\n\r\nAgent 在触发 setup 前,应检查以下指标判断是否已安装:\r\n\r\n| 检查项 | 判断方式 |\r\n|--------|---------|\r\n| Router 是否运行 | `curl http://127.0.0.1:5678/api/status` 返回 `running: true` |\r\n| OpenClaw 是否已配置 | openclaw.json 中存在 `free-model-router` provider |\r\n| cron 任务是否已注册 | `cron list` 检查 `free-model-router-event-check` 和 `free-model-router-daily-status` |\r\n\r\n## 幂等行为\r\n\r\n- **Router 启动**:已运行则跳过\r\n- **OpenClaw 配置**:已写入 `free-model-router` provider 则跳过\r\n- **初始化**:重复执行时,仅同步 provider 列表,**不会覆盖用户手动设置的主备模型**\r\n- **cron 任务**:先删除旧任务再添加新任务，确保新配置生效\r\n- **API Key / 主模型设置**:用户手动配置的值始终保留\r\n\r\n## 二次安装输出\r\n\r\n当用户第二次触发 setup 时,CLI 会输出当前状态摘要:\r\n\r\n```\r\nfree-model-router 已安装,当前状态:\r\n  Router: 运行中 (http://127.0.0.1:5678)\r\n  OpenClaw: 已配置 (free-model-router provider)\r\n  已配置 Provider: openrouter, modelscope\r\n  事件检查任务: 已注册\r\n  每日状态汇报: 已注册\r\n\r\n提示:如需重新配置,请先运行 providers 查看当前状态\r\n```\r\n\r\n如果仅缺失 cron 任务,会提示缺失的部分:\r\n\r\n```\r\n--- 缺失的定时任务 ---\r\n请使用 OpenClaw 内置 cron 工具注册以下缺失的定时任务:\r\n### 任务: free-model-router-event-check\r\n...\r\n```\r\n\r\n## 注意事项\r\n\r\n- 如果已安装(Router 运行 + OpenClaw 已配置),setup 命令会输出当前状态摘要而非重新配置\r\n- 注册 cron 任务时，先用 `cron delete` 删除旧任务，再用 `cron add` 添加新任务\r\n- 这种方式确保新版本的定时任务配置能够正确生效"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Skill: free-model-router-laodao Owner: laodao-agent Summary: 为 OpenClaw 提供免费模型智能路由服务，支持本地代理、自动配置、自动轮询、故障切换和配置零侵入。在需要设置免费模型路由、配置 Provider、切换模型或诊断模型可用性问题时使用。 Tags: latest:2.2.0 Version history: v2.2.0 | 2026-08-12T04:04:59.191Z | user v2.2.0 更新内容 改进 - **额度耗尽与限流的恢复更可靠**：402 额度耗尽、长期 429 限流不再走固定冷却时间，改为持续探测成功才恢复，避免额度未恢复时反复失败 - **单次 429 限流不再触发长冷却**：单次限流不再立即把 Provider 长时间禁用，按累计次数逐步升档；累计清零后立即恢复 - **单个模型持续失败不再拖累整个 Provider**：单","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":665,"uniquenessScore":54,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T16:13:16.318Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T01:04:08.543Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}