diff --git a/scripts/publication-contract.mjs b/scripts/publication-contract.mjs index e056c0f..6555f4c 100644 --- a/scripts/publication-contract.mjs +++ b/scripts/publication-contract.mjs @@ -3,6 +3,10 @@ import MarkdownIt from "markdown-it"; import { EXPECTED_DESCRIPTION } from "./artifact-contract.mjs"; const issuesUrl = "https://github.com/TiantianFlow/ai-limits/issues"; +const supportedProvidersUrl = + "https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.md"; +const supportedProvidersUrlZh = + "https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.zh-CN.md"; const storeUrl = "https://chromewebstore.google.com/detail/ai-limits/hcfdchpajckemcdflcjhigngpipdkdeo"; const releaseUrl = "https://github.com/TiantianFlow/ai-limits/releases/latest"; @@ -29,6 +33,8 @@ const publicRouteGate = "After the repository is public and before Chrome Web Store submission, verify the homepage, privacy policy, and support URLs are reachable in a signed-out browser."; const paceAvailabilityStatement = "For quota windows with a reliable reset time plus either a start time or window duration, a pace signal compares quota consumed with elapsed time."; +const providerBrandPattern = + /\b(?:ChatGPT|Claude|Kimi|Cursor|Grok|Mistral|Perplexity|ElevenLabs|New API|LiteLLM|ClawRouter|sub2api|LLM Proxy|DeepSeek|Moonshot|DeepInfra|Fireworks|OpenAI|GroqCloud|OpenRouter)\b/i; const requiredVisibleFaqStatements = [ { key: "faq", @@ -306,12 +312,6 @@ const requiredVisibleV030Statements = [ "Disconnect deletes that instance's credential, configuration, usage, refresh status, and History before permission cleanup; a shared origin remains granted while another active instance owns it, and final-owner removal is best-effort with durable retry evidence.", error: "Privacy policy is missing the instance disconnect and final-owner behavior.", }, - { - key: "listing", - statement: - "AI Limits supports multiple independent New API instances, including multiple separately labeled keys on the same origin.", - error: "Store listing is missing multi-instance New API support.", - }, { key: "listing", statement: @@ -562,6 +562,16 @@ function extractQualifyingMarkdownProse(document) { return normalizeWhitespace(visible.join(" ")); } +function extractMarkdownSection(document, startHeading, endHeading) { + const source = document ?? ""; + const start = source.indexOf(startHeading); + if (start === -1) return ""; + + const afterStart = source.slice(start + startHeading.length); + const end = afterStart.indexOf(endHeading); + return end === -1 ? afterStart : afterStart.slice(0, end); +} + export function validatePublicationDocuments(documents) { const errors = []; if (Object.values(documents).some((source) => { @@ -656,6 +666,21 @@ export function validatePublicationDocuments(documents) { const listing = normalizeWhitespace(listingSource); const visibleListing = extractQualifyingMarkdownProse(listingSource); const listingDestinations = extractInlineMarkdownLinkDestinations(listingSource); + const listingZhSource = documents.listingZh ?? ""; + const detailedListing = extractMarkdownSection( + listingSource, + "## Detailed description", + "## Permission justifications", + ); + const detailedListingZh = extractMarkdownSection( + listingZhSource, + "## 详细说明", + "## 权限说明", + ); + const visibleDetailedListing = extractQualifyingMarkdownProse(detailedListing); + const visibleDetailedListingZh = extractQualifyingMarkdownProse( + detailedListingZh, + ); if (!privacy.includes(limitedUseStatement)) { errors.push("Privacy policy is missing the Limited Use compliance statement."); @@ -719,6 +744,30 @@ export function validatePublicationDocuments(documents) { ); } + if (!visibleDetailedListing.includes(supportedProvidersUrl)) { + errors.push( + "English Store detailed description is missing the direct supported-providers URL.", + ); + } + + if (providerBrandPattern.test(visibleDetailedListing)) { + errors.push( + "English Store detailed description must not enumerate provider brands.", + ); + } + + if (!visibleDetailedListingZh.includes(supportedProvidersUrlZh)) { + errors.push( + "Simplified Chinese Store detailed description is missing the direct supported-providers URL.", + ); + } + + if (providerBrandPattern.test(visibleDetailedListingZh)) { + errors.push( + "Simplified Chinese Store detailed description must not enumerate provider brands.", + ); + } + if (!documents.license?.includes("Copyright (c) 2026 TiantianFlow")) { errors.push("LICENSE is missing the 2026 TiantianFlow copyright notice."); } diff --git a/scripts/publication-contract.test.js b/scripts/publication-contract.test.js index a6c5870..7ed412a 100644 --- a/scripts/publication-contract.test.js +++ b/scripts/publication-contract.test.js @@ -3,6 +3,10 @@ import { validatePublicationDocuments } from "./publication-contract.mjs"; const issuesUrl = "https://github.com/TiantianFlow/ai-limits/issues"; const issuesLink = `[GitHub Issues](${issuesUrl})`; +const supportedProvidersUrl = + "https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.md"; +const supportedProvidersUrlZh = + "https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.zh-CN.md"; const storeUrl = "https://chromewebstore.google.com/detail/ai-limits/hcfdchpajckemcdflcjhigngpipdkdeo"; const storeLink = `[Chrome Web Store](${storeUrl})`; @@ -294,12 +298,6 @@ const v030PublicationStatements = [ "Disconnect deletes that instance's credential, configuration, usage, refresh status, and History before permission cleanup; a shared origin remains granted while another active instance owns it, and final-owner removal is best-effort with durable retry evidence.", error: "Privacy policy is missing the instance disconnect and final-owner behavior.", }, - { - key: "listing", - statement: - "AI Limits supports multiple independent New API instances, including multiple separately labeled keys on the same origin.", - error: "Store listing is missing multi-instance New API support.", - }, { key: "listing", statement: @@ -412,6 +410,14 @@ const valid = { ...v030PublicationStatements .filter(({ key }) => key === "listing") .map(({ statement }) => statement), + "## Detailed description", + supportedProvidersUrl, + "## Permission justifications", + ].join("\n"), + listingZh: [ + "## 详细说明", + supportedProvidersUrlZh, + "## 权限说明", ].join("\n"), license: "MIT License\n\nCopyright (c) 2026 TiantianFlow", }; @@ -615,6 +621,36 @@ describe("publication content", () => { ); }); + it("requires direct compatibility links and rejects provider inventories in Store descriptions", () => { + expect( + validatePublicationDocuments({ + ...valid, + listing: valid.listing.replace(supportedProvidersUrl, ""), + }), + ).toContain( + "English Store detailed description is missing the direct supported-providers URL.", + ); + expect( + validatePublicationDocuments({ + ...valid, + listingZh: valid.listingZh.replace(supportedProvidersUrlZh, ""), + }), + ).toContain( + "Simplified Chinese Store detailed description is missing the direct supported-providers URL.", + ); + expect( + validatePublicationDocuments({ + ...valid, + listing: valid.listing.replace( + supportedProvidersUrl, + `${supportedProvidersUrl}\nChatGPT`, + ), + }), + ).toContain( + "English Store detailed description must not enumerate provider brands.", + ); + }); + it.each(elevenLabsPublicationStatements)( "requires visible ElevenLabs disclosure in $key: $error", ({ key, statement, error }) => { diff --git a/scripts/verify-publication.mjs b/scripts/verify-publication.mjs index 91e7c34..50862d7 100644 --- a/scripts/verify-publication.mjs +++ b/scripts/verify-publication.mjs @@ -15,6 +15,10 @@ const documents = { resolve("store-assets/chrome-web-store/listing-en.md"), "utf8", ), + listingZh: await readFile( + resolve("store-assets/chrome-web-store/listing-zh_CN.md"), + "utf8", + ), license: await readFile(resolve("LICENSE"), "utf8"), }; diff --git a/store-assets/chrome-web-store/listing-en.md b/store-assets/chrome-web-store/listing-en.md index c9c08ff..c66a5e3 100644 --- a/store-assets/chrome-web-store/listing-en.md +++ b/store-assets/chrome-web-store/listing-en.md @@ -41,10 +41,9 @@ Regeneration and validation instructions are in AI Limits gives a user one Chrome side panel for viewing current subscription usage as **Used** or **Left**, reset timing, pace, and local quota-history -graphs across twenty supported providers (see the supported-providers document -for the full roster). Browser-session providers use an account already signed in within the -user's browser profile; API-key providers use a key the user connects through -guided setup. +graphs across twenty supported services. Browser-session services use an account +already signed in within the user's browser profile; API-key services use a key +the user connects through guided setup. ## Short description @@ -65,29 +64,12 @@ compares quota consumed with elapsed time. Every quota window can open its own local History screen, so you can see how your usage has trended over time, right on your device. -**ChatGPT** and **Claude** show your plan, usage windows, resets, and available -credits straight from your signed-in account. **Kimi** reads your usage and -subscription status directly, with a brief, visible recovery step if your -session ever needs refreshing. **Cursor** shows monthly Cursor-model and -Other-model limits plus weekly Grok Bot usage, and a per-model included-usage -breakdown grouped into Cursor Models and Other Models with input, output, cost, -and provider totals. That detail view is a generic per-provider surface; today -only Cursor fills it. **Grok** reads your signed-in consumer plan and -weekly or monthly usage pool; short per-mode windows are not shown. -**Mistral** shows month-to-date spend and available credits, -while **Perplexity** shows recurring, purchased, and promotional credit pools. -**ElevenLabs** connects with a scoped, read-only API key you create yourself. -**New API** connects self-hosted or third-party API-compatible instances with a -key you provide. AI Limits supports multiple independent New API instances, -including multiple separately labeled keys on the same origin. -**LiteLLM**, **ClawRouter**, **sub2api**, and **LLM Proxy** connect configurable -instances with a key you provide. **DeepSeek** and **Moonshot** show developer -platform balances, **DeepInfra** shows monthly spend against a limit or its -reported balance, and **Fireworks** shows rated spend over the last 30 days. -**OpenAI** reads the Platform organization and developer billing surface -(organization costs and completion usage, with a credit-grants fallback), -**GroqCloud** shows five-minute usage rates, and **OpenRouter** shows credits and -an optional key budget. +AI Limits supports 20 AI services. Connect only the services you use: every +connection is opt-in. Browser-session services use the account already signed +in to Chrome, while API services use a key you provide. + +Full compatibility details, connection methods, and known limitations: +https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.md The side panel supports English and Simplified Chinese. It follows Chrome when Chrome is set to one of those languages, and falls back to English for other diff --git a/store-assets/chrome-web-store/listing-zh_CN.md b/store-assets/chrome-web-store/listing-zh_CN.md index 9e31d27..ce90b6f 100644 --- a/store-assets/chrome-web-store/listing-zh_CN.md +++ b/store-assets/chrome-web-store/listing-zh_CN.md @@ -28,7 +28,7 @@ ## 单一用途 -AI Limits 在 Chrome 侧边栏中为你提供一个统一视图,用来跟踪已连接 AI 服务的订阅用量(已用或剩余)、重置时间、消耗节奏和本地用量历史,覆盖二十个受支持的服务(完整名单见支持的服务文档)。基于浏览器会话的服务使用你在浏览器配置中已经登录的账号;基于 API 密钥的服务通过引导流程连接你自己创建的密钥。 +AI Limits 在 Chrome 侧边栏中为你提供一个统一视图,用来跟踪已连接 AI 服务的订阅用量(已用或剩余)、重置时间、消耗节奏和本地用量历史,覆盖二十个受支持的服务。基于浏览器会话的服务使用你在浏览器配置中已经登录的账号;基于 API 密钥的服务通过引导流程连接你自己创建的密钥。 ## 简短说明 @@ -42,7 +42,10 @@ AI Limits 把你的 AI 订阅用量集中显示在同一个 Chrome 侧边栏中 每个用量周期都可以打开自己的本地历史界面,方便你直接在自己的设备上查看用量随时间的变化趋势。 -**ChatGPT** 和 **Claude** 直接从你已登录的账号显示套餐、用量周期、重置时间和可用额度。**Kimi** 直接读取你的用量和订阅状态;如果会话需要刷新,会有一个简短、可见的恢复步骤。**Cursor** 会显示 Cursor 模型与其他模型的月度限额以及 Grok Bot 每周用量,并按 Cursor Models 和 Other Models 分组列出各模型的包含用量(输入、输出、费用和服务商合计)。该明细视图是通用的分服务界面;目前只有 Cursor 会填充它。**Grok** 读取你已登录的消费者版套餐和每周或每月用量池,不显示按模式返回的短窗。**Mistral** 显示本月至今支出和可用额度,**Perplexity** 显示周期性、已购买和促销额度池。**ElevenLabs** 通过你自己创建的、权限受限的只读 API 密钥连接。**New API** 用你提供的密钥连接自建或第三方 API 兼容实例。AI Limits 支持多个相互独立的 New API 实例,包括同一来源下多个分别标注的密钥。**LiteLLM**、**ClawRouter**、**sub2api** 和 **LLM Proxy** 使用你提供的密钥连接可配置实例。**DeepSeek** 和 **Moonshot** 显示开发平台余额,**DeepInfra** 显示当月支出相对上限的配额或服务商报告的余额,**Fireworks** 显示最近 30 天已计价支出。**OpenAI** 读取平台组织/开发者账单(组织支出和补全用量,并在需要时回退到额度接口),**GroqCloud** 显示五分钟用量速率,**OpenRouter** 显示额度和可选密钥预算。 +AI Limits 支持 20 个 AI 服务。你可以逐个连接实际使用的服务;每项连接都需要主动启用。基于浏览器会话的服务使用你已登录的 Chrome 账号;基于 API 的服务使用你自行提供的密钥。 + +完整的受支持服务、连接方式和已知限制: +https://github.com/TiantianFlow/ai-limits/blob/main/SUPPORTED_PROVIDERS.zh-CN.md 侧边栏仅支持英文和简体中文。跟随 Chrome 时,仅在 Chrome 为英文或简体中文时使用对应语言,其他 Chrome 语言会回退到英文。也可以在 Settings 中为侧边栏固定英文或简体中文。Chrome 自身的菜单和扩展名称仍跟随 Chrome 的语言。