filippofinke/tutti-api

GitHub: filippofinke/tutti-api

非官方零依赖的 TypeScript SDK,封装了 tutti.ch 分类信息平台的逆向 API,支持搜索、商品浏览、用户认证及实时消息。

Stars: 13 | Forks: 2

Welcome to tutti-api 👋

Version Documentation License: MIT CI Twitter: filippofinke

### 🏠 [主页](https://github.com/filippofinke/tutti-api) ### 📖 [文档](https://filippofinke.github.io/tutti-api/) ## 功能 - 🔍 流畅的**搜索**,支持过滤器(类别、价格、位置、区间、单选/多选)+ 游标分页 - 📦 **列表**、卖家**个人资料**、**类别**、精选类别、搜索**建议** - 💬 实时**消息** — 对话和消息流作为 async iterators(发送、已读回执、发起聊天) - 🔐 **Auth0 登录**(authorization-code + PKCE),带有可替换的 captcha 提供商(手动,或 Google Gemini vision) - 🖼️ 内置**零依赖的 SVG→PNG** 引擎(渲染登录 captcha 以进行 OCR) - 💾 可插拔的**会话持久化**(内存 / 文件 / 您自己的存储) - 🧩 面向对象(每个账户一个实例),**零运行时依赖**,支持 ESM + CJS + 类型 ## 安装 ``` npm install tutti-api ``` 需要 Node 18+(全局 `fetch`)或浏览器。 ## 用法 ``` import { TuttiClient } from "tutti-api"; const client = new TuttiClient(); // anonymous; random device hash // Fluent search with filters const result = await client .search("ledersofa") .category("furniture") .price({ min: 100, max: 5000 }) // or .freeOnly() .location(locality) // from client.localities.search() .select("companyAd", "private") // generic single-select .multiSelect("language", ["de"]) // generic multi-select .interval("year", { min: 2015 }) // generic numeric range .sort("timestamp", "desc") .fetch(); result.totalCount; // number result.listings; // Listing[] (this page) result.availableFilters; // filter names/options for this category await result.next(); // next page (or null) for await (const l of result.paginate()) { /* every listing across pages */ } // Listing detail + locality autocomplete + token browse const listing = await client.listings.get("81078697"); const locs = await client.localities.search("zür"); const page = await client.browse(searchToken).fetch(); // Filters for a category without fetching a listings page const { availableFilters } = await client.search().category("cars").updateFilters(); // Categories, featured, seller profiles, autocomplete await client.categories.tree(); await client.categories.featured(); await client.profiles.get(publicAccountID); await client.profiles.listings(publicAccountID, { offset: 0, size: 30 }); await client.suggestions.search("sof"); ``` ### 身份验证 tutti 使用 Auth0,然后将 JWT 交换为 tutti **会话 token**,作为 `X-Tutti-Auth` 发送(有效期约 1 年)。 ``` import { LLMCaptchaProvider, ManualCaptchaProvider, Session } from "tutti-api"; // Full login. The Auth0 page has a captcha, solved by a swappable provider: // ManualCaptchaProvider (default) — saves the image, you type the text // LLMCaptchaProvider — Google Gemini vision (needs GEMINI_API_KEY) await client.account.login({ username, password, captcha: new LLMCaptchaProvider() }); // Or bring your own token / Auth0 access token: client.account.useToken("mc1x…"); await client.account.authenticateJWT(auth0AccessToken); ``` ### 会话持久化 `Session.toJSON()/fromJSON()` 提供一个普通快照;**`SessionStore`** 将其持久化到一个键下。各个提供商可互换 — 内置提供 `InMemorySessionStore` 和 `FileSessionStore`;可以通过实现 `save / load / delete / keys` 来添加 Redis/DB。 ``` import { FileSessionStore, Session, TuttiClient } from "tutti-api"; const store = new FileSessionStore("./sessions"); await store.save("alice", client.session.toJSON()); const snap = await store.load("alice"); // restore later, no re-login const restored = new TuttiClient({ session: snap ? Session.fromJSON(snap) : undefined }); ``` ### 消息 实时聊天通过一个长连接请求以 NDJSON 流的形式传输,并作为 **async iterators** 暴露出来(先读取历史记录,然后是实时消息)。需要经过身份验证的会话。 ``` const ac = new AbortController(); for await (const m of client.messaging.streamMessages(convId, { signal: ac.signal })) { const mine = m.senderPublicAccountId === client.session.auth?.accountId; console.log(mine ? "→" : "←", m.content.text); } // ac.abort() to stop await client.messaging.send(convId, "Hello!"); await client.messaging.markRead(convId, offset); await client.messaging.reply({ itemId, name, email, body }); // start a chat from a listing ``` ## 演示 只需存储一次会话;每个演示都会从 `./.tutti-sessions`(已 gitignore)中加载它: ``` # 1) 存储 session(token fast-path,或完整 login) TUTTI_TOKEN= npm run demo:session # 或:GEMINI_API_KEY=… TUTTI_USER=… TUTTI_PASS=… npm run demo:session # 2) 其余部分加载它(如果没有则为 anonymous fallback) npm run demo # search "ubiquiti" + pagination npm run demo:queries # categories, featured, suggestions, updateFilters, profiles npm run demo:messages # live conversation + message streams (needs auth) ``` ## 脚本 ``` npm run build # bundle ESM + CJS + .d.ts into dist/ npm run typecheck # tsc --noEmit npm run check # Biome lint + format (write) npm run docs # generate API docs (TypeDoc) into docs/ ``` ## 注意事项 - **过滤元素形状是推断出来的** — 捕获的请求只发送过空的约束数组。关键字搜索不受影响;请根据实时流量验证 `price`/`location`/`interval`/`select` 的 payload。 - **登录 captcha 默认是交互式的** — 生成的会话 token 是长期有效的(约 1 年),因此您很少需要登录;`useToken()` 会跳过它。`auth.tutti.ch` 位于 Cloudflare 之后。 - **默认请求头**反映了捕获的 Android 客户端;可以通过 `new TuttiClient({ app: { … } })` 进行覆盖。 ## 作者 👤 **Filippo Finke** * 网站: [https://filippofinke.ch](https://filippofinke.ch) * Twitter: [@filippofinke](https://twitter.com/filippofinke) * Github: [@filippofinke](https://github.com/filippofinke) * LinkedIn: [@filippofinke](https://linkedin.com/in/filippofinke) ## 📝 许可证 Copyright © 2026 [Filippo Finke](https://github.com/filippofinke).
本项目基于 [MIT](./LICENSE) 许可证授权。 _出于教育目的进行的逆向工程 — 与 tutti.ch 无关。_
标签:API, BeEF, MITM代理, TypeScript, 云资产清单, 安全插件, 爬虫, 网络调试, 自动化, 自动化攻击, 逆向工程