From 883ff74cf4dbbaafedc29a8d5d98fbb2dd83a4f5 Mon Sep 17 00:00:00 2001 From: baiqing Date: Thu, 14 May 2026 14:59:36 +0800 Subject: [PATCH] docs(marketplace): add style pack marketplace planning doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 预留风格包市场的 HTTP API + IPC 契约 + DTO + 鉴权/缓存/安全策略。 IPC stub 未实装;后端 endpoint 选型 + 服务端工程化待定。 --- docs/style-pack-marketplace.md | 299 +++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 docs/style-pack-marketplace.md diff --git a/docs/style-pack-marketplace.md b/docs/style-pack-marketplace.md new file mode 100644 index 00000000..eba69293 --- /dev/null +++ b/docs/style-pack-marketplace.md @@ -0,0 +1,299 @@ +# Style Pack Marketplace — 规划文档 + +**状态**:规划中(API 已预留 stub,未实装) +**起草日期**:2026-05-14 +**owner**:待定 + +## 1. 目标 + +把现在「ZIP 包本地导入 / 导出」的体验扩展成一个公开的风格包市场: + +- 用户可以把自己调好的风格包**上传**到云端,附带名称、描述、作者署名、标签、效果示例 +- 其他用户可以**浏览 / 搜索 / 下载**别人的风格包,一键安装到本地 +- 后期支持**版本升级提醒**、**收藏 / 评分**等基础社交属性 + +非目标(v1 不做): +- 付费 / 抽成 +- 风格包内嵌外部 prompt 注入 / 跨域 fetch(安全考虑,风格包始终是纯文本 prompt) +- 多人协作编辑 / fork + +## 2. 架构概览 + +``` +┌──────────────────┐ HTTPS ┌─────────────────────┐ +│ OpenLess client │ ◄──────────────────► │ marketplace API │ +│ (Tauri 2) │ JSON over TLS │ (TBD: Cloudflare │ +│ │ │ Workers / D1 / │ +│ Rust IPC → │ │ R2 for blobs) │ +│ reqwest client │ │ │ +└──────────────────┘ └─────────────────────┘ + │ │ + │ local cache (~/Library/Application │ + │ Support/OpenLess/market_cache/) │ + ▼ ▼ + StylePackStore Postgres / D1 + (existing local listings + R2 blobs + persistence layer) +``` + +**关键约束**: +- 客户端只能上传 / 下载 ZIP **bundle**(不直接传 JSON),保持跟现有 ZIP import/export 同构 +- 服务端 ZIP 验证:解压后必须能反序列化成 `StylePack`、`prompt.chars().count() <= 50_000`、没有可执行附件 +- 风格包 ID 上传后由服务端分配(`{author_slug}-{name_slug}-{version}`),跟本地 ID 解耦 +- 客户端始终拿 ZIP 走现有 `import_style_pack_from_zip` 路径入库 —— 不另开一条「从市场直接写 Pack」的代码路径,避免双入口 + +## 3. HTTP API 规约 + +Base URL(待定):`https://api.openless.app/v1/marketplace/` + +所有响应统一信封: +```json +{ + "ok": true, + "data": | null, + "error": null | { "code": "ERR_XXX", "message": "..." } +} +``` + +### 3.1 GET `/packs` — 列表 / 搜索 + +Query: +| 参数 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `q` | string | `""` | 关键词(名称 / 描述 / 标签) | +| `tag` | string | `""` | 单标签筛选 | +| `sort` | `recent` \| `popular` \| `name` | `recent` | 排序 | +| `cursor` | string | `null` | 分页游标 | +| `limit` | int (1-100) | `20` | 每页条数 | + +Response data: +```typescript +{ + packs: MarketPackListing[]; + next_cursor: string | null; +} +``` + +`MarketPackListing`: +```typescript +{ + id: string; // server-assigned, e.g. "alice-formal-v2.1" + name: string; + description: string; + author: string; + version: string; // semver + tags: string[]; + base_mode: "raw" | "light" | "structured" | "professional"; + recommended_model: string | null; + compatible_app_version: string | null; + downloads: number; + rating_avg: number | null; + rating_count: number; + updated_at: string; // ISO8601 + zip_size_bytes: number; + zip_sha256: string; // 客户端下载后校验 +} +``` + +### 3.2 GET `/packs/{id}` — 详情 + +Response data:`MarketPackListing` + 额外字段: +```typescript +{ + ...listing, + examples: StylePackExample[]; // 解压 ZIP 前的预览 + changelog: string | null; + homepage_url: string | null; +} +``` + +### 3.3 GET `/packs/{id}/download` — 下载 ZIP + +Response:`application/zip` 二进制流,带 `X-Pack-SHA256` header 用于校验。 + +服务端通过 redirect 直接指向 R2 / S3 预签 URL,避免代理流量。 + +### 3.4 POST `/packs` — 上传(需鉴权) + +Headers:`Authorization: Bearer ` +Body:`multipart/form-data` with field `pack=@xxx.zip` + +Response data:`MarketPackListing`(含新分配 id) + +错误码: +- `ERR_INVALID_ZIP` — ZIP 解压失败 / 不是合法 StylePack JSON +- `ERR_PROMPT_TOO_LARGE` — prompt 字数超 50k +- `ERR_DUPLICATE_VERSION` — 同 author+name+version 已存在 +- `ERR_RATE_LIMITED` — 触发限频 + +### 3.5 DELETE `/packs/{id}` — 撤回(需鉴权 + 必须是上传者) + +### 3.6 POST `/packs/{id}/rate` — 评分(需鉴权) + +Body:`{ score: 1..5, comment?: string }` + +## 4. IPC 契约(Rust ↔ TS) + +在 `src-tauri/src/commands.rs` 新增以下 stub(暂返回 `Err("not implemented yet")`,等服务端落地后实装): + +```rust +// 列表 / 搜索 +#[tauri::command] +pub async fn market_list_packs( + query: Option, + tag: Option, + sort: Option, + cursor: Option, + limit: Option, +) -> Result; + +// 详情 +#[tauri::command] +pub async fn market_get_pack(id: String) -> Result; + +// 下载 + 自动调用现有的 import_style_pack_from_zip 入库 +#[tauri::command] +pub async fn market_download_pack( + coord: CoordinatorState<'_>, + app: AppHandle, + id: String, +) -> Result; + +// 上传(dirty 字段 = 已编辑、未保存) +#[tauri::command] +pub async fn market_upload_pack( + coord: CoordinatorState<'_>, + pack_id: String, + api_key: String, +) -> Result; + +// 撤回 +#[tauri::command] +pub async fn market_delete_pack(id: String, api_key: String) -> Result<(), String>; + +// 评分 +#[tauri::command] +pub async fn market_rate_pack( + id: String, + api_key: String, + score: u8, + comment: Option, +) -> Result<(), String>; +``` + +DTO(在 `types.rs` 新增): +```rust +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketPackListing { + pub id: String, + pub name: String, + pub description: String, + pub author: String, + pub version: String, + pub tags: Vec, + pub base_mode: PolishMode, + pub recommended_model: Option, + pub compatible_app_version: Option, + pub downloads: u64, + pub rating_avg: Option, + pub rating_count: u32, + pub updated_at: String, + pub zip_size_bytes: u64, + pub zip_sha256: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketPackDetail { + #[serde(flatten)] + pub listing: MarketPackListing, + pub examples: Vec, + pub changelog: Option, + pub homepage_url: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketListResponse { + pub packs: Vec, + pub next_cursor: Option, +} +``` + +TS wrappers(`src/lib/ipc.ts`): +```typescript +export interface MarketPackListing { /* same shape */ } +export interface MarketPackDetail extends MarketPackListing { /* + examples, changelog, homepage_url */ } +export interface MarketListResponse { packs: MarketPackListing[]; next_cursor: string | null; } + +export function marketListPacks(opts: { + query?: string; tag?: string; sort?: 'recent' | 'popular' | 'name'; + cursor?: string; limit?: number; +}): Promise; +export function marketGetPack(id: string): Promise; +export function marketDownloadPack(id: string): Promise; +export function marketUploadPack(packId: string, apiKey: string): Promise; +export function marketDeletePack(id: string, apiKey: string): Promise; +export function marketRatePack(id: string, apiKey: string, score: number, comment?: string): Promise; +``` + +## 5. 鉴权模型 + +**v1 简化方案**: +- 用户在设置页输入个人 API key(服务端发放) +- API key 存到 OS Keychain,账户名 `com.openless.app.market_api_key` +- 客户端在 Header 加 `Authorization: Bearer ` +- 服务端校验 + 限频(每小时 60 次写、600 次读) + +**v2 升级路径**(暂不做): +- OAuth via GitHub / Google +- 上传时自动签名 ZIP,下载端校验签名 + +## 6. 缓存与版本检查 + +本地缓存目录:`/market_cache/` +- `listings.json` — 上次拉的 listings(带 ETag) +- `packs/{id}.zip` — 已下载的 ZIP(按需保留,30 天自动清理) + +版本升级提示: +- 启动时(带 dev-cap 24h 节流)调用 `/packs?ids=<已安装的 market_id...>` 拉对比 +- 本地包记录 `installed_market_id` 和 `installed_market_version` 字段,新建 `StylePack` 时填,本地从 ZIP 安装也填 +- 发现新版本 → 在 Style 页该包卡片角标显示 `New version: 2.3.0 →` + +## 7. 客户端 UI 入口(v1 不做,先留位) + +- Style 页头部加一个 tab:`本地 / 市场` +- 市场页:搜索栏 + tag 过滤 + 卡片列表 + 详情抽屉 +- 上传:编辑某个本地包时,"导出 ZIP" 按钮旁边出现 "上传到市场"(需要先在设置里填 API key) + +## 8. 安全 / 滥用对策 + +- ZIP 解压走 streaming,限制最大解压后大小 5 MB +- prompt 字段过滤明显的 prompt injection / 越狱(关键词预扫描 + 异步内容审核) +- 每用户每天上传上限 10 包,单包大小 ≤ 2 MB +- 上传后挂 24h 公开延迟(防恶意刷榜) + +## 9. 实装 TODO(按优先级) + +- [ ] 服务端选型(CF Workers + D1 + R2 vs Supabase vs 自托管 FastAPI) +- [ ] 服务端实装 + 部署环境(dev / staging / prod) +- [ ] 客户端 `types.rs` 加 DTO +- [ ] `commands.rs` 加 6 个 stub(**已完成**,返回 `not implemented yet`) +- [ ] `lib/ipc.ts` 加 wrapper(**已完成**) +- [ ] 实装 `market_download_pack`(先做单条路径打通:URL → 下载 → 走现有 import_style_pack_from_zip) +- [ ] 加凭据存储(Keychain 复用现有 `CredentialsVault`) +- [ ] UI:本地 / 市场 tab +- [ ] UI:搜索 + 卡片 +- [ ] UI:详情面板 +- [ ] UI:上传流程 +- [ ] 升级提醒 badge +- [ ] 缓存清理 + ETag + +## 10. 决策 / 风险记录 + +| 项 | 决策 | Why | +|---|---|---| +| ZIP 而非 JSON 上传 | 用 ZIP | 跟现有 import/export 同构;prompt 长文 + examples 用 ZIP 包压缩 | +| 服务端分配 ID | 是 | 防本地 ID 碰撞、用户重命名包不影响订阅 | +| 上传立刻可见 vs 审核 | 24h 公开延迟 | 防刷榜 + 给审核留空间 | +| API key vs OAuth | 先 API key | 简化 v1;登录态可 v2 升级 | +| 客户端缓存策略 | listings ETag + 已下载 ZIP 30 天 | 平衡流量和体验 | +| 国际化 / 跨境 | API 全英文 + 客户端 i18n | 服务端不存翻译,名称/描述支持任意 UTF-8 |