diff --git a/.changeset/docs-performance-limits-measure-every-claim.md b/.changeset/docs-performance-limits-measure-every-claim.md new file mode 100644 index 00000000..e0d93d59 --- /dev/null +++ b/.changeset/docs-performance-limits-measure-every-claim.md @@ -0,0 +1,42 @@ +--- +'hotcrm': patch +--- + +Measure every claim on the Performance & Limits reference page against the app +and the installed platform, in all three locales. The page carried roughly sixty +quota figures, an archive API, a per-field indexing toggle and a pagination +recipe — none of which had ever been checked against anything, and four of which +were advice a reader could act on and get a wrong answer from: + +- **The pagination sample could not have worked.** It printed + `GET /api/v1/opportunities?limit=200&cursor=…`. `query.cursor` was removed in + `@objectstack/spec` 17 (#4286, ADR-0049) and is now rejected by name — the + removal note records that no driver ever implemented keyset pagination, so the + cursor was accepted and ignored and every page came back identical, meaning a + caller looping "until `hasMore` is false" never terminated. The path was wrong + too: the data plane is `/api/v1/data/` with the `crm_`-prefixed name. + Now `limit` / `offset`, plus a keyset expressed as an ordinary `where` + predicate posted to `POST /api/v1/data/crm_opportunity/query`. +- **"Mark the field indexed" named a flag that never built an index.** + `FieldSchema` rejects `indexed` by name — *"a field-level index flag built no + index (#2377). Declare the index in the object's `indexes[]`."* Indexes are an + object-level declaration, and HotCRM already ships sixteen of them across + fifteen objects. +- **There is no archive API.** Archiving itself is real — + `ObjectSchema.lifecycle.archive` with `after` / `to` / `keep`, swept by + ObjectQL's ADR-0057 archiver — but it is a metadata declaration, no object in + `src/` carries one, and archived rows live in the datasource named by + `archive.to` rather than behind a record route. +- **The session defaults were the opposite of the truth.** Idle, absolute and + concurrent-session controls exist (ADR-0069 D4, `@objectstack/plugin-auth`), + but `0` means off for each and off is the default; the page presented + 5 sessions / 30 min / 12 hr as active defaults. + +The quota tables stay, and nothing was deleted — a removed claim and a false one +look identical to the next reader. Each table now states whether anything +enforces it, and every row with a source names it: the 10,000-row bulk ceiling is +`MAX_BULK_PER_ROW_HOOK_ROWS`, search fields come from each object's +`searchableFields` (no top-30 cap), the flow loop ceiling is 100,000 rather than +2,000, file sizes are per-field `maxSize` values this app really sets, and no +inbound rate limit is configured at all. The email-and-calendar quota table is +marked *(not shipped)*, matching the guide. Part of #1119. diff --git a/content/docs/reference/performance-and-limits.mdx b/content/docs/reference/performance-and-limits.mdx index 3021c7e5..849cca8a 100644 --- a/content/docs/reference/performance-and-limits.mdx +++ b/content/docs/reference/performance-and-limits.mdx @@ -5,84 +5,142 @@ description: Records, API rate limits, AI quotas, file sizes — what HotCRM all # Performance & Limits -The limits below are the **default Enterprise plan**. Other plans differ; self-hosted has no platform-imposed caps but bottlenecks shift to your infrastructure. +> **Read this before you plan against a number below.** Every figure on this page was +> checked against the app's own metadata (`src/`) and the installed `@objectstack/*` +> packages (17.0.0-rc.6). Most of them turn out to be **enforced by nothing**: HotCRM +> declares no such quota and the platform applies none, so the figure describes a plan +> or a deployment rather than anything this app does. Each table below now says which +> of the two it is, and every row that *does* have a source names it. Headings marked +> *(not shipped)* describe a capability the app does not have today. Nothing has been +> quietly dropped — a deleted claim and a false one look identical to the next reader. + +The figures were written as the **default Enterprise plan**. Plan tiers are a commercial +arrangement and cannot be read out of the app, so treat every unsourced number as a +target your own deployment has to set and hold, not as a ceiling HotCRM will apply for +you. Self-hosted has no platform-imposed caps at all; the bottlenecks are your +infrastructure's. ## Data volumes | Item | Limit | Notes | | --- | --- | --- | -| Records per object | **100 million** | Practical; can go higher with archiving | -| Custom objects | **1,000** per tenant | | -| Custom fields per object | **800** | Standard + custom combined | -| Picklist values per field | **2,000** | | -| Relationships per object | **40** lookup / **2** master-detail | | -| Validation rules per object | **500** | | +| Records per object | **100 million** | Unsourced — a practical figure, not a cap | +| Custom objects | **1,000** per tenant | Unsourced | +| Custom fields per object | **800** | Unsourced; standard + custom combined | +| Picklist values per field | **2,000** | Unsourced | +| Relationships per object | **40** lookup / **2** master-detail | Unsourced | +| Validation rules per object | **500** | Unsourced | | Workflow rules per object | — | Not an ObjectStack type: ADR-0019 / ADR-0020 retired it, so there is no `workflow` metadata type to cap. See *Flows per object* below | | Flows per object | Unlimited | Each flow has its own complexity limits | -| Page layouts per object | **200** | | -| Record types per object | **200** | | +| Page layouts per object | **200** | Unsourced | +| Record types per object | **200** | Unsourced | + +*Not enforced.* `ObjectSchema` accepts as many fields, picklist options, relationships +and validation rules as you declare, and neither `src/` nor the installed platform +carries a counter for any row marked *Unsourced*. The real ceiling is your database's. +The two unmarked rows are properties of the metadata model and are stated above. ## File storage | Item | Limit | | --- | --- | -| Single file upload | **2 GB** | +| Single file upload | **2 GB** (unsourced) | | Attachments per record | Unlimited | | Total file storage | Per plan (typically TB per tenant) | | Image preview formats | jpg, png, gif, webp, svg | | Document preview formats | pdf, docx, xlsx, pptx | -Files over 100 MB upload via multi-part; the platform handles chunking. +*Partly sourced.* The file limit HotCRM really enforces is **per field**, not per tenant: +`Field.file()` and `Field.image()` take a server-enforced `maxSize`, and this app sets +three — 2 MB on the account logo (`src/objects/account.object.ts:358`), 5 MB and 20 MB on +product media (`src/objects/product.object.ts:157` and `:164`). No 2 GB platform cap, no +multi-part chunking threshold and no preview-format list is declared in the app or in the +installed packages; the preview formats above describe the viewer your deployment runs. ## API limits | Limit | Default | | --- | --- | -| Requests per minute (per user) | **6,000** | -| Requests per minute (per tenant) | **60,000** | -| Concurrent connections (per user) | **25** | -| Request body size | **20 MB** | -| Response size (single record) | **10 MB** | -| Max records per query | **2,000** (use cursor pagination for more) | -| Max records per bulk write | **10,000** per batch | -| Long-running query timeout | **120 s** | - -Rate-limit responses use HTTP **429** with `Retry-After` header. Always implement exponential back-off. +| Requests per minute (per user) | **6,000** (unsourced) | +| Requests per minute (per tenant) | **60,000** (unsourced) | +| Concurrent connections (per user) | **25** (unsourced) | +| Request body size | **20 MB** (unsourced) | +| Response size (single record) | **10 MB** (unsourced) | +| Max records per query | Set it yourself with `limit` / `offset`; there is no platform default and no cap | +| Max records per bulk write | **10,000** per predicate write when a per-row hook is contracted — see below | +| Long-running query timeout | **120 s** (unsourced) | + +*Not enforced by HotCRM.* The app declares no `server.security.rateLimit` +(`objectstack.config.ts` has no `server` block at all), so **this app applies no inbound +rate limit**. The platform's own reference budgets — `DEFAULT_RATE_LIMITS` in +`@objectstack/runtime` — are **auth 10/min, write 60/min, read 600/min**, and that +constant's comment states in as many words that nothing reads it: the live seam is a +*single* token bucket per caller, sized by an authored `server.security.rateLimit`. The +per-minute figures above sit one to two orders of magnitude above that reference scale +and have no source anywhere. + +The **10,000** row ceiling is real and is the one number in this table with a source: +`MAX_BULK_PER_ROW_HOOK_ROWS` in `@objectstack/spec/data`. A predicate bulk write matching +more than 10,000 rows on an object whose `beforeUpdate` / `afterUpdate` / `beforeDelete` / +`afterDelete` hook is contracted to fire per row is **refused whole** with +`ERR_BULK_PER_ROW_HOOK_LIMIT` — nothing is written, and the write is never silently +downgraded to one hook call for the batch. Narrow the predicate and paginate the write. + +Rate-limit responses use HTTP **429**: `THROTTLED` maps to 429 in the REST error table +(`@objectstack/rest`). Whether a `Retry-After` header comes with it depends on the limiter +your deployment configures — HotCRM configures none. Implement exponential back-off +anyway. ## Search limits | Item | Limit | | --- | --- | -| Global search results | Top **200** per object | -| Search query length | **2,000 chars** | -| Indexed fields per object | Top **30** by default (configurable) | -| Search latency (p95) | **<500 ms** | +| Global search results | Top **200** per object (unsourced) | +| Search query length | **2,000 chars** (unsourced) | +| Indexed fields per object | Declared per object as `searchableFields` — no default cap | +| Search latency (p95) | Unsourced — depends on your database and data volume | + +*Partly sourced.* Which fields a search reads **is** real and is declared per object: +`searchableFields` on the object, set on eight of them today +(`src/objects/account.object.ts:25`, `lead.object.ts:435`, `case.object.ts:387`, +`opportunity.object.ts:28`, `product.object.ts:27`, `quote.object.ts:29`, +`knowledge_article.object.ts:82`, `event.object.ts:224`). There is **no top-30 default and +no configurable cap** — `resolveSearchFields` in `@objectstack/spec/data` returns exactly +the declared list, so the "Top 30 by default (configurable)" this page used to quote +described nothing. The result-count, query-length and latency figures have no source. ## AI Copilot limits | Item | Default | | --- | --- | -| Skill invocations per user per day | **500** | -| Skill invocations per tenant per day | **50,000** | -| Knowledge base size | **100,000 chunks** per base | -| Knowledge base re-index | **Nightly** or on-demand | -| Max tokens per skill prompt | **8,000** (input) / **2,000** (output) | -| Max simultaneous skill executions | **10** per user | +| Skill invocations per user per day | **500** (unsourced) | +| Skill invocations per tenant per day | **50,000** (unsourced) | +| Knowledge base size | **100,000 chunks** per base (unsourced) | +| Knowledge base re-index | Unsourced — no re-index schedule is declared | +| Max tokens per skill prompt | **8,000** (input) / **2,000** (output) (unsourced) | +| Max simultaneous skill executions | **10** per user (unsourced) | | Embedding storage | Per plan (typically GB per tenant) | -Bulk-skill invocations (e.g., qualify 500 leads) count as 500 skill calls — be mindful of quotas. +*Not enforced.* No invocation quota, token budget, concurrency cap or re-index schedule +exists for the six skills in `src/skills/` or anywhere in the installed packages. Whatever +metering you get is your model provider's and your deployment's, not the app's. ## Reporting & analytics | Item | Limit | | --- | --- | -| Report rows displayed | **10,000** | -| Report rows exported | **1,000,000** | +| Report rows displayed | **10,000** (unsourced) | +| Report rows exported | **1,000,000** (unsourced) | | Dashboards per user | Unlimited | -| Widgets per dashboard | **50** | +| Widgets per dashboard | **50** (unsourced) | | Dashboard refresh interval | **60 s** on Customer Service, **180 s** on Sales Performance, **300 s** on the other three — declared per dashboard in `src/dashboards/` | | Cube refresh frequency | Owned by the analytics service. HotCRM declares no refresh schedule and no incremental-versus-full policy anywhere in `src/`, so there is no app-side figure to quote — check your deployment | -| Concurrent report runs (per user) | **5** | +| Concurrent report runs (per user) | **5** (unsourced) | + +*Partly sourced.* The two refresh rows are read out of `src/`. A row cap on reports does +exist as a **host option** — `ReportServiceOptions.maxRows` in `@objectstack/plugin-reports`, +"cap rows per report to protect both DB and email size" — but it has no default and HotCRM +does not set it, so no report row limit is in force today. For deeper analysis, [export to a data warehouse](/docs/guides/import-and-export#scheduled-export). @@ -90,60 +148,113 @@ For deeper analysis, [export to a data warehouse](/docs/guides/import-and-export | Item | Limit | | --- | --- | -| Visible rows per page | **200** (configurable up to 500) | -| Filter clauses | **20** per view | -| Columns | **40** per view | -| Saved views per user | **500** | +| Visible rows per page | **200** (configurable up to 500) (unsourced) | +| Filter clauses | **20** per view (unsourced) | +| Columns | **40** per view (unsourced) | +| Saved views per user | **500** (unsourced) | + +*Not enforced.* `ObjectSchema.listViews` and the `*.view.ts` metadata accept as many +columns and filter clauses as you declare, and no page-size ceiling is applied by the app +or by the installed platform. ## Automation limits | Item | Limit | | --- | --- | | Flow re-evaluations per save | No fixed budget — a flow's own writes re-enter the trigger order; the engine breaks self-trigger loops with a **re-entrancy guard** rather than a counter (a backstop, not a cap). See [Automation](/docs/administration/automation) | -| Flow steps | **500** per flow | -| Flow loop iterations | **2,000** | -| Scheduled job concurrency | **20** simultaneous tenant-wide | -| Email alerts per hour | **1,000** per user / **50,000** per tenant | -| Approval steps per process | **30** | - -## Email & calendar sync - -| Item | Limit | +| Flow steps | No cap | +| Flow loop iterations | **100,000** ceiling per loop node, with each node's own `maxIterations` under it | +| Scheduled job concurrency | Unsourced — no concurrency knob is declared | +| Email alerts per hour | Unsourced | +| Approval steps per process | Unsourced — no step cap exists | + +*Partly sourced.* The loop ceiling is `LOOP_MAX_ITERATIONS_CEILING` in +`@objectstack/spec/automation`; a loop node's authored `maxIterations` sits under it. The +**2,000** this page used to quote was 50× below the real ceiling, and the **500** flow-step +cap it quoted does not exist at all. The re-entrancy backstop referenced in the first row +is `MAX_NODE_REENTRIES = 100` per node, with `DEFAULT_MAX_TERMINAL_RUNS_PER_FLOW = 100` +(`@objectstack/service-automation`) — a loop-breaker, not a budget you can spend. + +## Email & calendar sync (not shipped) + +The intended surface: a mailbox connector that logs inbound and outbound mail against the +record, calendar two-way sync, and open/click tracking — each with the quotas below. None +of it ships today, so none of these limits binds anything. See +[Email & Calendar](/docs/guides/email-and-calendar) for what the app really writes. + +| Item | Limit (intended) | | --- | --- | | Email body size logged | **5 MB** | | Calendar events synced per user | **5,000** active | | Email retention | **2 years** default (configurable) | | Tracking pixel events stored | Per email indefinitely | +What ships instead: **Send Email** on a contact writes one queued message row through the +platform email service, and **Log a Call / Log a Meeting / Schedule a Meeting** write real +`crm_event` records with attendee rows. Nothing is retained on a schedule, nothing is +tracked, and nothing syncs from your mailbox — so there is no retention window to +configure and no tracking-event store to fill. + ## Webhook & event bus | Item | Limit | | --- | --- | -| Webhook payload | **1 MB** | -| Webhook retries | **5** with exponential back-off | -| Webhook timeout | **10 s** | -| Event bus throughput | **1,000 events/sec** per tenant | -| Event retention in bus | **7 days** | +| Webhook payload | **1 MB** (unsourced) | +| Webhook retries | **5** with exponential back-off (unsourced) | +| Webhook timeout | Per webhook, via its own `timeoutMs` — no platform default is declared | +| Event bus throughput | **1,000 events/sec** per tenant (unsourced) | +| Event retention in bus | **7 days** (unsourced) | + +*Not enforced by HotCRM.* `@objectstack/plugin-webhooks` is installed and carries a +per-webhook `timeoutMs`, but **this app registers no webhooks** — `src/` contains no +webhook declaration, so every figure here describes a subscription your deployment would +have to create and configure itself. ## User & session limits | Item | Limit | | --- | --- | +| Concurrent sessions per user | `maxConcurrentSessions` — **off** unless your deployment sets it | +| Session idle timeout | `sessionIdleTimeoutMinutes` — **off** unless your deployment sets it | +| Session absolute timeout | `sessionAbsoluteMaxHours` — **off** unless your deployment sets it | | Users per tenant | Unlimited (subject to plan) | -| Concurrent sessions per user | **5** (configurable) | -| Session idle timeout | **30 min** (configurable) | -| Session absolute timeout | **12 hr** (configurable) | + +*Sourced, and the defaults are the opposite of what this page used to say.* All three +controls are real (ADR-0069 D4, `@objectstack/plugin-auth`): idle and absolute limits are +enforced per request and the concurrent cap on sign-in, revoking the oldest sessions over +the limit. But **`0` means off for each, and off is the default** — HotCRM sets none of +them, so the **5 sessions / 30 min / 12 hr** this page used to present as defaults are in +force nowhere. They are deployment settings, and they are not editable from any screen in +this app. ## How to stay within limits ### Pagination -When fetching >2,000 records: +Page with `limit` and `offset` against the object's own REST path — the `crm_`-prefixed +object name, under `/data`: + +```http +GET /api/v1/data/crm_opportunity?limit=200&offset=400 +``` + +For a stable walk over a large result set, express the keyset as an ordinary predicate on +your sort key and post it to the query route: ```http -GET /api/v1/opportunities?limit=200&cursor=eyJpZCI6... +POST /api/v1/data/crm_opportunity/query +{ "where": { "created_at": { "$gt": "2026-01-01T00:00:00Z" } }, + "orderBy": "created_at", "limit": 200 } ``` +> **`cursor` was removed.** `query.cursor` and `QueryBuilder.cursor()` were removed in +> `@objectstack/spec` 17 (#4286, ADR-0049) and the key is now rejected by name: no driver +> ever implemented keyset pagination, so the cursor was accepted and ignored and every +> page came back identical — a caller looping "until `hasMore` is false" never terminated. +> The `?cursor=…` sample this page used to print could not have worked. A first-class +> cursor, if one is ever built, will be a response-minted opaque token, not a caller-built +> record. + Always paginate; never load all records at once. ### Back-off @@ -167,23 +278,59 @@ Or use the platform SDK which handles back-off automatically. ### Bulk operations -For >100 records, use the **bulk API** rather than 100 single calls. +For many records at once, use the batch routes rather than one call per record. All four +ship: `POST /api/v1/data/:object/batch`, `/createMany`, `/updateMany` and `/deleteMany`, +plus the cross-object `POST /api/v1/batch`. Mind the 10,000-row per-row-hook ceiling in +*API limits* above. ### AI invocation batching -When triaging many records, use a **bulk skill** invocation — it batches under the hood and counts as N invocations (so still respects per-user quota), but uses connection pooling for ~10× speedup. +*Not shipped.* There is no bulk-skill invocation surface: the six skills in `src/skills/` +are invoked one call at a time, and no batching, connection pooling or "~10× speedup" path +exists in the app or the installed packages. Triaging many records means many invocations. +Bulk **actions** on a list view are a different thing and are real — see +[Automation](/docs/administration/automation). ### Field indexing -For custom fields you query frequently, mark them **indexed** — speeds list filtering and report execution. +Indexes are declared **on the object, not on the field**, and by a developer in metadata +rather than by an admin in a screen: + +```ts +indexes: [{ name: 'idx_account_name', fields: ['name'] }], +``` + +HotCRM already declares sixteen such blocks across fifteen objects (for example +`src/objects/account.object.ts:442`); the SQL driver materialises them through +`syncDeclaredIndexes`. There is **no per-field `indexed` toggle** — `FieldSchema` rejects +the key by name, with the reason attached: *"never a FieldSchema key; a field-level index +flag built no index (#2377). Declare the index in the object's `indexes[]`."* So the advice +this page used to give — "mark the field indexed" — named a flag that never built an index. +`indexes[].type` and `indexes[].partial` were removed in spec 17 (#5248) for the same +reason; leave both out. ### Archive old data -Records older than your operational window (e.g., closed cases >2 years) can be **archived**: +Archiving is a real platform capability and a real **per-object** policy, but it is +declared in the object's metadata — not configured in a screen — and **HotCRM declares it +nowhere**: + +```ts +lifecycle: { class: 'record', archive: { after: '2y', to: 'cold', keep: '7y' } }, +``` -- Removed from hot storage; still queryable via the **archive API**. -- Frees indexes and improves performance. -- Auto-archive policies configurable per object. +`ObjectSchema.lifecycle.archive` takes `after` (how old), `to` (the datasource to copy to) +and an optional `keep` (how long cold rows survive). ObjectQL's lifecycle sweep then copies +rows past `after` from the hot store to that archive datasource and deletes the copied rows +hot (ADR-0057 §3.3), which is what frees the indexes. No object in `src/` carries a +`lifecycle` block today, so nothing in HotCRM is archived and nothing is expired. + +> **There is no archive API.** Archived rows do not stay reachable through the ordinary +> record routes: they live in the datasource named by `archive.to`, and reading them means +> querying that datasource. The REST surface has no archive route — the data plane is +> `/data/:object`, `/:id`, `/query`, `/clone`, `/import*`, `/export`, `/batch`, +> `/createMany`, `/updateMany`, `/deleteMany`, `/shares`, plus the reports and approvals +> routes, and nothing else. ## Monitoring your usage @@ -193,7 +340,11 @@ What Setup does ship is **Setup → System Overview**: total users, organization ## When you outgrow these limits -- **Vertical** — Enterprise+ plan extends most quotas. +These three are properties of your **plan and your deployment**, not of the app, and +nothing in `src/` or the installed packages can confirm them — take them up with whoever +runs your instance: + +- **Vertical** — an Enterprise+ plan extends most quotas. - **Sharded** — multi-region deployments for >1 billion records. - **Self-hosted** — eliminate platform caps; bottlenecks become infrastructure. diff --git a/content/docs/reference/performance-and-limits.zh-Hans.mdx b/content/docs/reference/performance-and-limits.zh-Hans.mdx index 271cb37f..07435ebb 100644 --- a/content/docs/reference/performance-and-limits.zh-Hans.mdx +++ b/content/docs/reference/performance-and-limits.zh-Hans.mdx @@ -5,84 +5,124 @@ description: 记录、API 速率限制、AI 配额、文件大小 —— HotCRM # 性能与限制 -以下限制为**默认企业版套餐**。其他套餐有所不同;自托管没有平台施加的上限,但瓶颈会转移到你的基础设施上。 +> **在你依据下面任何一个数字做规划之前,请先读这段。** 本页的每一个数字都对着应用自身的元数据(`src/`) +> 和已安装的 `@objectstack/*` 包(17.0.0-rc.6)核过了。结论是:其中大多数**没有任何东西在执行** —— +> HotCRM 没有声明这样的配额,平台也不施加,所以那个数字描述的是某个套餐或某套部署,而不是这个应用真做的事。 +> 下面每张表都会说明它属于哪一类,凡是**有**出处的行都会点名出处。标记 *(未提供)* 的小节描述的是应用 +> 今天并不具备的能力。没有任何东西被悄悄删掉 —— 对下一个读者来说,被删掉的断言和错误的断言长得一模一样。 + +这些数字当初是按**默认企业版套餐**写的。套餐分层是商业安排,读应用是读不出来的,所以请把每一个没有出处的 +数字当作你自己的部署需要去设定并守住的目标,而不是 HotCRM 会替你施加的上限。自托管根本没有平台施加的 +上限;瓶颈是你自己的基础设施。 ## 数据量 | 项目 | 限制 | 备注 | | --- | --- | --- | -| 每个对象的记录数 | **1 亿** | 实际值;通过归档可以更高 | -| 自定义对象 | 每个租户 **1,000** 个 | | -| 每个对象的自定义字段 | **800** | 标准 + 自定义合计 | -| 每个字段的选择列表值 | **2,000** | | -| 每个对象的关系 | **40** 个 lookup / **2** 个 master-detail | | -| 每个对象的验证规则 | **500** | | +| 每个对象的记录数 | **1 亿** | 无出处 —— 是个经验值,不是上限 | +| 自定义对象 | 每个租户 **1,000** 个 | 无出处 | +| 每个对象的自定义字段 | **800** | 无出处;标准 + 自定义合计 | +| 每个字段的选择列表值 | **2,000** | 无出处 | +| 每个对象的关系 | **40** 个 lookup / **2** 个 master-detail | 无出处 | +| 每个对象的验证规则 | **500** | 无出处 | | 每个对象的工作流规则 | — | 并非 ObjectStack 的类型:ADR-0019 / ADR-0020 已将其退休,不存在可设上限的 `workflow` 元数据类型。参见下方*每个对象的流程* | | 每个对象的流程 | 无限制 | 每个流程都有自己的复杂度限制 | -| 每个对象的页面布局 | **200** | | -| 每个对象的记录类型 | **200** | | +| 每个对象的页面布局 | **200** | 无出处 | +| 每个对象的记录类型 | **200** | 无出处 | + +*没有执行。* 你声明多少字段、多少选项、多少关系、多少验证规则,`ObjectSchema` 就接受多少;`src/` 和已安装 +的平台都没有为任何一个标了*无出处*的行准备计数器。真正的天花板是你的数据库。没有标注的那两行是元数据模型 +自身的性质,上面已经写明。 ## 文件存储 | 项目 | 限制 | | --- | --- | -| 单文件上传 | **2 GB** | +| 单文件上传 | **2 GB**(无出处) | | 每条记录的附件 | 无限制 | | 总文件存储 | 按套餐(通常为每个租户 TB 级) | | 图片预览格式 | jpg、png、gif、webp、svg | | 文档预览格式 | pdf、docx、xlsx、pptx | -超过 100 MB 的文件通过分段上传;平台负责分块处理。 +*部分有出处。* HotCRM 真正在执行的文件限制是**按字段**的,不是按租户的:`Field.file()` 和 `Field.image()` +接受一个由服务端执行的 `maxSize`,本应用设了三处 —— 客户 logo 2 MB(`src/objects/account.object.ts:358`)、 +产品素材 5 MB 与 20 MB(`src/objects/product.object.ts:157` 与 `:164`)。应用和已安装的包里都没有声明 2 GB +的平台上限、没有分段上传阈值、也没有预览格式清单;上面的预览格式说的是你部署里跑的那个查看器。 ## API 限制 | 限制 | 默认值 | | --- | --- | -| 每分钟请求数(每用户) | **6,000** | -| 每分钟请求数(每租户) | **60,000** | -| 并发连接数(每用户) | **25** | -| 请求体大小 | **20 MB** | -| 响应大小(单条记录) | **10 MB** | -| 每次查询的最大记录数 | **2,000**(更多请使用游标分页) | -| 每次批量写入的最大记录数 | 每批 **10,000** | -| 长时间运行查询的超时 | **120 秒** | - -速率限制响应使用 HTTP **429** 及 `Retry-After` 标头。请始终实现指数退避。 +| 每分钟请求数(每用户) | **6,000**(无出处) | +| 每分钟请求数(每租户) | **60,000**(无出处) | +| 并发连接数(每用户) | **25**(无出处) | +| 请求体大小 | **20 MB**(无出处) | +| 响应大小(单条记录) | **10 MB**(无出处) | +| 每次查询的最大记录数 | 自己用 `limit` / `offset` 定;平台既没有默认值也没有上限 | +| 每次批量写入的最大记录数 | 当对象契约了逐行钩子时,一次谓词写入 **10,000** 行 —— 见下文 | +| 长时间运行查询的超时 | **120 秒**(无出处) | + +*HotCRM 没有执行。* 本应用没有声明 `server.security.rateLimit`(`objectstack.config.ts` 里根本没有 `server` +块),所以**这个应用不施加任何入站速率限制**。平台自己的参考预算 —— `@objectstack/runtime` 里的 +`DEFAULT_RATE_LIMITS` —— 是 **auth 每分钟 10 次、write 每分钟 60 次、read 每分钟 600 次**,而且那个常量的 +注释明白写着没有任何代码读它:真正生效的接缝是**每个调用方一个**令牌桶,由声明式的 +`server.security.rateLimit` 定尺寸。上面那些每分钟的数字比这个参考量级高出一到两个数量级,哪里都没有出处。 + +**10,000** 这个行数上限是真的,也是本表唯一有出处的数字:`@objectstack/spec/data` 里的 +`MAX_BULK_PER_ROW_HOOK_ROWS`。如果一次谓词批量写入命中超过 10,000 行,而该对象的 `beforeUpdate` / +`afterUpdate` / `beforeDelete` / `afterDelete` 钩子被契约为逐行触发,这次写入会被**整体拒绝**并报 +`ERR_BULK_PER_ROW_HOOK_LIMIT` —— 什么都不会写入,而且引擎绝不会把它悄悄降级成整批只调一次钩子。请收窄 +谓词、把写入分页。 + +速率限制响应使用 HTTP **429**:`THROTTLED` 在 REST 错误表(`@objectstack/rest`)里映射为 429。是否附带 +`Retry-After` 标头取决于你部署所配置的限流器 —— HotCRM 没有配置任何一个。无论如何都请实现指数退避。 ## 搜索限制 | 项目 | 限制 | | --- | --- | -| 全局搜索结果 | 每个对象前 **200** 条 | -| 搜索查询长度 | **2,000 字符** | -| 每个对象的索引字段 | 默认前 **30** 个(可配置) | -| 搜索延迟(p95) | **<500 毫秒** | +| 全局搜索结果 | 每个对象前 **200** 条(无出处) | +| 搜索查询长度 | **2,000 字符**(无出处) | +| 每个对象的索引字段 | 按对象声明为 `searchableFields` —— 没有默认上限 | +| 搜索延迟(p95) | 无出处 —— 取决于你的数据库和数据量 | + +*部分有出处。* 搜索读哪些字段**是**真的,而且是按对象声明的:对象上的 `searchableFields`,今天有八个对象 +设了它(`src/objects/account.object.ts:25`、`lead.object.ts:435`、`case.object.ts:387`、 +`opportunity.object.ts:28`、`product.object.ts:27`、`quote.object.ts:29`、`knowledge_article.object.ts:82`、 +`event.object.ts:224`)。**既没有前 30 个的默认值,也没有可配置的上限** —— `@objectstack/spec/data` 里的 +`resolveSearchFields` 原样返回声明的那份清单,所以本页原先写的"默认前 30 个(可配置)"什么也没有描述。 +结果条数、查询长度和延迟这三个数字都没有出处。 ## AI Copilot 限制 | 项目 | 默认值 | | --- | --- | -| 每用户每天的技能调用 | **500** | -| 每租户每天的技能调用 | **50,000** | -| 知识库大小 | 每个库 **100,000** 个块 | -| 知识库重新索引 | **每晚**或按需 | -| 每个技能提示的最大 token | **8,000**(输入)/ **2,000**(输出) | -| 最大同时技能执行数 | 每用户 **10** | +| 每用户每天的技能调用 | **500**(无出处) | +| 每租户每天的技能调用 | **50,000**(无出处) | +| 知识库大小 | 每个库 **100,000** 个块(无出处) | +| 知识库重新索引 | 无出处 —— 没有声明任何重新索引计划 | +| 每个技能提示的最大 token | **8,000**(输入)/ **2,000**(输出)(无出处) | +| 最大同时技能执行数 | 每用户 **10**(无出处) | | 嵌入存储 | 按套餐(通常为每个租户 GB 级) | -批量技能调用(例如,审定 500 条线索)计为 500 次技能调用 —— 请留意配额。 +*没有执行。* 对于 `src/skills/` 里的六个技能,以及已安装的任何包,都不存在调用配额、token 预算、并发上限 +或重新索引计划。你能拿到的计量来自你的模型供应商和你的部署,不来自这个应用。 ## 报表与分析 | 项目 | 限制 | | --- | --- | -| 显示的报表行数 | **10,000** | -| 导出的报表行数 | **1,000,000** | +| 显示的报表行数 | **10,000**(无出处) | +| 导出的报表行数 | **1,000,000**(无出处) | | 每用户的仪表盘 | 无限制 | -| 每个仪表盘的小部件 | **50** | +| 每个仪表盘的小部件 | **50**(无出处) | | 仪表盘刷新间隔 | Customer Service **60 秒**、Sales Performance **180 秒**、其余三个 **300 秒** —— 由 `src/dashboards/` 逐个仪表盘声明 | | cube 刷新频率 | 归分析服务管。HotCRM 在 `src/` 里没有声明任何刷新计划,也没有增量与全量刷新策略,因此应用侧给不出这个数字 —— 请对着你自己的部署去核 | -| 并发报表运行(每用户) | **5** | +| 并发报表运行(每用户) | **5**(无出处) | + +*部分有出处。* 那两行刷新间隔是从 `src/` 里读出来的。报表的行数上限确实存在,但它是个**宿主选项** —— +`@objectstack/plugin-reports` 里的 `ReportServiceOptions.maxRows`,"限制每份报表的行数,以同时保护数据库和 +邮件体积" —— 它没有默认值,而 HotCRM 也没有设它,所以今天没有任何报表行数限制在生效。 如需更深入的分析,请[导出到数据仓库](/zh-Hans/docs/guides/import-and-export)。 @@ -90,60 +130,100 @@ description: 记录、API 速率限制、AI 配额、文件大小 —— HotCRM | 项目 | 限制 | | --- | --- | -| 每页可见行数 | **200**(可配置至 500) | -| 筛选子句 | 每个视图 **20** 个 | -| 列 | 每个视图 **40** 列 | -| 每用户保存的视图 | **500** | +| 每页可见行数 | **200**(可配置至 500)(无出处) | +| 筛选子句 | 每个视图 **20** 个(无出处) | +| 列 | 每个视图 **40** 列(无出处) | +| 每用户保存的视图 | **500**(无出处) | + +*没有执行。* `ObjectSchema.listViews` 和 `*.view.ts` 元数据你声明多少列、多少筛选子句就接受多少,应用和 +已安装的平台都不施加每页行数的天花板。 ## 自动化限制 | 项目 | 限制 | | --- | --- | | 每次保存的流程重新评估 | 没有固定预算 —— 流程自身的写入会重新进入触发顺序;引擎用**重入守卫**打断自触发环,而不是计次(兜底,不是上限)。参见[自动化](/zh-Hans/docs/administration/automation) | -| 流程步骤 | 每个流程 **500** | -| 流程循环迭代 | **2,000** | -| 计划作业并发 | 全租户同时 **20** | -| 每小时的邮件提醒 | 每用户 **1,000** / 每租户 **50,000** | -| 每个流程的审批步骤 | **30** | +| 流程步骤 | 没有上限 | +| 流程循环迭代 | 每个循环节点 **100,000** 的天花板,其下是该节点自己声明的 `maxIterations` | +| 计划作业并发 | 无出处 —— 没有声明任何并发旋钮 | +| 每小时的邮件提醒 | 无出处 | +| 每个流程的审批步骤 | 无出处 —— 不存在步骤上限 | -## 邮件与日历同步 +*部分有出处。* 循环天花板是 `@objectstack/spec/automation` 里的 `LOOP_MAX_ITERATIONS_CEILING`;循环节点自己 +声明的 `maxIterations` 位于其下。本页原先写的 **2,000** 比真实天花板低 50 倍,而它写的 **500** 流程步骤上限 +根本不存在。第一行提到的重入兜底是每个节点 `MAX_NODE_REENTRIES = 100`,配合 +`DEFAULT_MAX_TERMINAL_RUNS_PER_FLOW = 100`(`@objectstack/service-automation`)—— 那是个断环器,不是给你花 +的预算。 -| 项目 | 限制 | +## 邮件与日历同步(未提供) + +设想中的形态:一个信箱连接器,把收发的邮件登记到记录上,日历双向同步,外加打开/点击跟踪 —— 每一项都配着 +下面这些配额。这些今天一个都没有提供,所以这些限制什么也约束不到。应用真正写入什么,见 +[邮件与日历](/zh-Hans/docs/guides/email-and-calendar)。 + +| 项目 | 限制(设想中) | | --- | --- | | 记录的邮件正文大小 | **5 MB** | | 每用户同步的日历事件 | **5,000** 个活跃 | | 邮件保留 | 默认 **2 年**(可配置) | | 存储的跟踪像素事件 | 每封邮件无限期 | +真正提供的是:联系人上的**发送邮件**会通过平台邮件服务写入一条排队中的邮件记录,而**记录通话/记录会议/ +安排会议**会写入真实的 `crm_event` 记录及其参与人行。没有任何东西按计划保留、没有任何东西被跟踪、也没有 +任何东西从你的信箱同步过来 —— 所以既没有保留窗口可配置,也没有跟踪事件库可填。 + ## Webhook 与事件总线 | 项目 | 限制 | | --- | --- | -| Webhook 负载 | **1 MB** | -| Webhook 重试 | **5** 次,采用指数退避 | -| Webhook 超时 | **10 秒** | -| 事件总线吞吐量 | 每租户 **1,000 个事件/秒** | -| 总线中的事件保留 | **7 天** | +| Webhook 负载 | **1 MB**(无出处) | +| Webhook 重试 | **5** 次,采用指数退避(无出处) | +| Webhook 超时 | 每个 webhook 自己的 `timeoutMs` —— 没有声明平台默认值 | +| 事件总线吞吐量 | 每租户 **1,000 个事件/秒**(无出处) | +| 总线中的事件保留 | **7 天**(无出处) | + +*HotCRM 没有执行。* `@objectstack/plugin-webhooks` 是装上了,也带有按 webhook 的 `timeoutMs`,但**本应用没有 +注册任何 webhook** —— `src/` 里没有任何 webhook 声明,所以这里每一个数字说的都是一个得由你的部署自己去创建 +和配置的订阅。 ## 用户与会话限制 | 项目 | 限制 | | --- | --- | +| 每用户的并发会话 | `maxConcurrentSessions` —— 除非你的部署设了它,否则**关闭** | +| 会话空闲超时 | `sessionIdleTimeoutMinutes` —— 除非你的部署设了它,否则**关闭** | +| 会话绝对超时 | `sessionAbsoluteMaxHours` —— 除非你的部署设了它,否则**关闭** | | 每个租户的用户 | 无限制(受套餐约束) | -| 每用户的并发会话 | **5**(可配置) | -| 会话空闲超时 | **30 分钟**(可配置) | -| 会话绝对超时 | **12 小时**(可配置) | + +*有出处,而且默认值和本页原先说的正好相反。* 这三个控制都是真的(ADR-0069 D4,`@objectstack/plugin-auth`): +空闲与绝对超时在每个请求上执行,并发上限在登录时执行,把超出数量的最旧会话吊销掉。但**每一项 `0` 都表示关闭, +而关闭就是默认值** —— HotCRM 一项都没有设,所以本页原先当作默认值呈现的 **5 个会话 / 30 分钟 / 12 小时** +在任何地方都没有生效。它们是部署设置,而且在这个应用的任何界面里都改不了。 ## 如何保持在限制范围内 ### 分页 -当获取 >2,000 条记录时: +用 `limit` 和 `offset` 对着对象自己的 REST 路径分页 —— 带 `crm_` 前缀的对象名,挂在 `/data` 下: ```http -GET /api/v1/opportunities?limit=200&cursor=eyJpZCI6... +GET /api/v1/data/crm_opportunity?limit=200&offset=400 ``` +若要在大结果集上稳定地走一遍,把 keyset 表达成排序键上的普通谓词,发给查询路由: + +```http +POST /api/v1/data/crm_opportunity/query +{ "where": { "created_at": { "$gt": "2026-01-01T00:00:00Z" } }, + "orderBy": "created_at", "limit": 200 } +``` + +> **`cursor` 已被移除。** `query.cursor` 和 `QueryBuilder.cursor()` 在 `@objectstack/spec` 17 +> (#4286、ADR-0049)中被移除,这个键现在会被按名拒绝:从来没有驱动实现过 keyset 分页,所以 cursor 被接受 +> 之后就被忽略,每一页返回的都是同一批 —— 一个"循环到 `hasMore` 为 false 为止"的调用方永远不会停。本页原先 +> 印的那个 `?cursor=…` 示例根本不可能奏效。将来若真做出一等公民的 cursor,它会是响应方铸造的不透明 token, +> 而不是调用方自己拼的记录。 + 请始终分页;切勿一次性加载所有记录。 ### 退避 @@ -167,23 +247,48 @@ async function callApi(fn, retries = 5) { ### 批量操作 -对于 >100 条记录,使用**批量 API**而非 100 次单独调用。 +一次处理很多条记录时,请用批量路由,而不是一条记录一次调用。四个都提供了: +`POST /api/v1/data/:object/batch`、`/createMany`、`/updateMany` 和 `/deleteMany`,外加跨对象的 +`POST /api/v1/batch`。注意上面 *API 限制* 里那个 10,000 行的逐行钩子上限。 ### AI 调用批处理 -在分流大量记录时,使用**批量技能**调用 —— 它在底层进行批处理并计为 N 次调用(因此仍遵守每用户配额),但使用连接池实现约 10× 的加速。 +*未提供。* 不存在批量技能调用这个面:`src/skills/` 里的六个技能是一次一调的,应用和已安装的包里都不存在 +批处理、连接池或"约 10× 加速"这条路径。要分流很多记录,就意味着很多次调用。列表视图上的批量**操作**是另一 +回事,它是真的 —— 参见[自动化](/zh-Hans/docs/administration/automation)。 ### 字段索引 -对于你频繁查询的自定义字段,将其标记为**索引** —— 加快列表筛选和报表执行。 +索引声明**在对象上,不在字段上**,而且是由开发者写在元数据里,不是由管理员在界面里点: + +```ts +indexes: [{ name: 'idx_account_name', fields: ['name'] }], +``` + +HotCRM 今天已经在十五个对象上声明了十六个这样的块(例如 `src/objects/account.object.ts:442`);SQL 驱动通过 +`syncDeclaredIndexes` 把它们物化出来。**不存在按字段的 `indexed` 开关** —— `FieldSchema` 会按名拒绝这个键, +并附上理由:*"从来不是 FieldSchema 的键;字段级的索引标志没有建出过任何索引(#2377)。请在对象的 +`indexes[]` 里声明索引。"* 所以本页原先给的建议 ——"把字段标记为索引"—— 点的是一个从未建出索引的标志。 +`indexes[].type` 和 `indexes[].partial` 出于同样的理由在 spec 17(#5248)中被移除;两个都别写。 ### 归档旧数据 -超出你运营窗口的记录(例如,关闭超过 2 年的案例)可以被**归档**: +归档是一项真实的平台能力,也确实是一条**按对象**的策略,但它声明在对象的元数据里 —— 不是在界面里配置的 —— +而且 **HotCRM 哪里都没有声明它**: + +```ts +lifecycle: { class: 'record', archive: { after: '2y', to: 'cold', keep: '7y' } }, +``` -- 从热存储中移除;仍可通过**归档 API** 查询。 -- 释放索引并提升性能。 -- 可按对象配置自动归档策略。 +`ObjectSchema.lifecycle.archive` 接受 `after`(多旧)、`to`(要复制到的数据源)以及可选的 `keep`(冷数据保留 +多久)。随后 ObjectQL 的生命周期清扫会把超过 `after` 的行从热存储复制到那个归档数据源,并把已复制的行从热 +存储删除(ADR-0057 §3.3)—— 这就是释放索引的那一步。今天 `src/` 里没有任何对象带 `lifecycle` 块,所以 +HotCRM 里没有任何东西被归档,也没有任何东西被过期。 + +> **不存在归档 API。** 归档后的行不会继续通过普通记录路由可达:它们住在 `archive.to` 指定的那个数据源里, +> 要读它们就得去查那个数据源。REST 面上没有任何归档路由 —— 数据面就是 `/data/:object`、`/:id`、`/query`、 +> `/clone`、`/import*`、`/export`、`/batch`、`/createMany`、`/updateMany`、`/deleteMany`、`/shares`,加上 +> 报表与审批的那些路由,再没有别的了。 ## 监控你的使用情况 @@ -193,6 +298,9 @@ async function callApi(fn, retries = 5) { ## 当你超出这些限制时 +下面这三条是你的**套餐和部署**的性质,不是应用的性质,`src/` 和已安装的包都无法证实它们 —— 请找运维你这套 +实例的人去谈: + - **纵向** —— Enterprise+ 套餐扩展大多数配额。 - **分片** —— 用于 >10 亿条记录的多区域部署。 - **自托管** —— 消除平台上限;瓶颈变为基础设施。 diff --git a/content/docs/reference/performance-and-limits.zh-Hant.mdx b/content/docs/reference/performance-and-limits.zh-Hant.mdx index ee6dedf2..5fbc194c 100644 --- a/content/docs/reference/performance-and-limits.zh-Hant.mdx +++ b/content/docs/reference/performance-and-limits.zh-Hant.mdx @@ -5,84 +5,124 @@ description: 記錄、API 速率限制、AI 配額、檔案大小 —— HotCRM # 效能與限制 -以下限制為**預設企業版方案**。其他方案有所不同;自託管沒有平台施加的上限,但瓶頸會轉移到你的基礎設施上。 +> **在你依據下面任何一個數字做規劃之前,請先讀這段。** 本頁的每一個數字都對著應用自身的中繼資料(`src/`) +> 和已安裝的 `@objectstack/*` 套件(17.0.0-rc.6)核過了。結論是:其中大多數**沒有任何東西在執行** —— +> HotCRM 沒有宣告這樣的配額,平台也不施加,所以那個數字描述的是某個方案或某套部署,而不是這個應用真做的事。 +> 下面每張表都會說明它屬於哪一類,凡是**有**出處的列都會點名出處。標記 *(未提供)* 的小節描述的是應用 +> 今天並不具備的能力。沒有任何東西被悄悄刪掉 —— 對下一個讀者來說,被刪掉的斷言和錯誤的斷言長得一模一樣。 + +這些數字當初是按**預設企業版方案**寫的。方案分層是商業安排,讀應用是讀不出來的,所以請把每一個沒有出處的 +數字當作你自己的部署需要去設定並守住的目標,而不是 HotCRM 會替你施加的上限。自託管根本沒有平台施加的 +上限;瓶頸是你自己的基礎設施。 ## 資料量 | 項目 | 限制 | 備註 | | --- | --- | --- | -| 每個物件的記錄數 | **1 億** | 實際值;透過歸檔可以更高 | -| 自訂物件 | 每個租戶 **1,000** 個 | | -| 每個物件的自訂欄位 | **800** | 標準 + 自訂合計 | -| 每個欄位的選擇清單值 | **2,000** | | -| 每個物件的關係 | **40** 個 lookup / **2** 個 master-detail | | -| 每個物件的驗證規則 | **500** | | +| 每個物件的記錄數 | **1 億** | 無出處 —— 是個經驗值,不是上限 | +| 自訂物件 | 每個租戶 **1,000** 個 | 無出處 | +| 每個物件的自訂欄位 | **800** | 無出處;標準 + 自訂合計 | +| 每個欄位的選擇清單值 | **2,000** | 無出處 | +| 每個物件的關係 | **40** 個 lookup / **2** 個 master-detail | 無出處 | +| 每個物件的驗證規則 | **500** | 無出處 | | 每個物件的工作流規則 | — | 並非 ObjectStack 的類型:ADR-0019 / ADR-0020 已將其退休,不存在可設上限的 `workflow` 中繼資料類型。參見下方*每個物件的流程* | | 每個物件的流程 | 無限制 | 每個流程都有自己的複雜度限制 | -| 每個物件的頁面版面 | **200** | | -| 每個物件的記錄類型 | **200** | | +| 每個物件的頁面版面 | **200** | 無出處 | +| 每個物件的記錄類型 | **200** | 無出處 | + +*沒有執行。* 你宣告多少欄位、多少選項、多少關係、多少驗證規則,`ObjectSchema` 就接受多少;`src/` 和已安裝 +的平台都沒有為任何一個標了*無出處*的列準備計數器。真正的天花板是你的資料庫。沒有標註的那兩列是中繼資料 +模型自身的性質,上面已經寫明。 ## 檔案儲存 | 項目 | 限制 | | --- | --- | -| 單檔案上傳 | **2 GB** | +| 單檔案上傳 | **2 GB**(無出處) | | 每筆記錄的附件 | 無限制 | | 總檔案儲存 | 按方案(通常為每個租戶 TB 級) | | 圖片預覽格式 | jpg、png、gif、webp、svg | | 文件預覽格式 | pdf、docx、xlsx、pptx | -超過 100 MB 的檔案透過分段上傳;平台負責分塊處理。 +*部分有出處。* HotCRM 真正在執行的檔案限制是**按欄位**的,不是按租戶的:`Field.file()` 和 `Field.image()` +接受一個由伺服端執行的 `maxSize`,本應用設了三處 —— 客戶 logo 2 MB(`src/objects/account.object.ts:358`)、 +產品素材 5 MB 與 20 MB(`src/objects/product.object.ts:157` 與 `:164`)。應用和已安裝的套件裡都沒有宣告 +2 GB 的平台上限、沒有分段上傳門檻、也沒有預覽格式清單;上面的預覽格式說的是你部署裡跑的那個檢視器。 ## API 限制 | 限制 | 預設值 | | --- | --- | -| 每分鐘請求數(每使用者) | **6,000** | -| 每分鐘請求數(每租戶) | **60,000** | -| 並行連線數(每使用者) | **25** | -| 請求體大小 | **20 MB** | -| 回應大小(單筆記錄) | **10 MB** | -| 每次查詢的最大記錄數 | **2,000**(更多請使用游標分頁) | -| 每次批量寫入的最大記錄數 | 每批 **10,000** | -| 長時間執行查詢的逾時 | **120 秒** | - -速率限制回應使用 HTTP **429** 及 `Retry-After` 標頭。請始終實作指數退避。 +| 每分鐘請求數(每使用者) | **6,000**(無出處) | +| 每分鐘請求數(每租戶) | **60,000**(無出處) | +| 並行連線數(每使用者) | **25**(無出處) | +| 請求體大小 | **20 MB**(無出處) | +| 回應大小(單筆記錄) | **10 MB**(無出處) | +| 每次查詢的最大記錄數 | 自己用 `limit` / `offset` 定;平台既沒有預設值也沒有上限 | +| 每次批量寫入的最大記錄數 | 當物件契約了逐列鉤子時,一次述詞寫入 **10,000** 列 —— 見下文 | +| 長時間執行查詢的逾時 | **120 秒**(無出處) | + +*HotCRM 沒有執行。* 本應用沒有宣告 `server.security.rateLimit`(`objectstack.config.ts` 裡根本沒有 `server` +區塊),所以**這個應用不施加任何入站速率限制**。平台自己的參考預算 —— `@objectstack/runtime` 裡的 +`DEFAULT_RATE_LIMITS` —— 是 **auth 每分鐘 10 次、write 每分鐘 60 次、read 每分鐘 600 次**,而且那個常數的 +註解明白寫著沒有任何程式碼讀它:真正生效的接縫是**每個呼叫方一個**權杖桶,由宣告式的 +`server.security.rateLimit` 定尺寸。上面那些每分鐘的數字比這個參考量級高出一到兩個數量級,哪裡都沒有出處。 + +**10,000** 這個列數上限是真的,也是本表唯一有出處的數字:`@objectstack/spec/data` 裡的 +`MAX_BULK_PER_ROW_HOOK_ROWS`。如果一次述詞批量寫入命中超過 10,000 列,而該物件的 `beforeUpdate` / +`afterUpdate` / `beforeDelete` / `afterDelete` 鉤子被契約為逐列觸發,這次寫入會被**整體拒絕**並報 +`ERR_BULK_PER_ROW_HOOK_LIMIT` —— 什麼都不會寫入,而且引擎絕不會把它悄悄降級成整批只呼叫一次鉤子。請收窄 +述詞、把寫入分頁。 + +速率限制回應使用 HTTP **429**:`THROTTLED` 在 REST 錯誤表(`@objectstack/rest`)裡映射為 429。是否附帶 +`Retry-After` 標頭取決於你部署所設定的限流器 —— HotCRM 沒有設定任何一個。無論如何都請實作指數退避。 ## 搜尋限制 | 項目 | 限制 | | --- | --- | -| 全域搜尋結果 | 每個物件前 **200** 筆 | -| 搜尋查詢長度 | **2,000 字元** | -| 每個物件的索引欄位 | 預設前 **30** 個(可設定) | -| 搜尋延遲(p95) | **<500 毫秒** | +| 全域搜尋結果 | 每個物件前 **200** 筆(無出處) | +| 搜尋查詢長度 | **2,000 字元**(無出處) | +| 每個物件的索引欄位 | 按物件宣告為 `searchableFields` —— 沒有預設上限 | +| 搜尋延遲(p95) | 無出處 —— 取決於你的資料庫和資料量 | + +*部分有出處。* 搜尋讀哪些欄位**是**真的,而且是按物件宣告的:物件上的 `searchableFields`,今天有八個物件 +設了它(`src/objects/account.object.ts:25`、`lead.object.ts:435`、`case.object.ts:387`、 +`opportunity.object.ts:28`、`product.object.ts:27`、`quote.object.ts:29`、`knowledge_article.object.ts:82`、 +`event.object.ts:224`)。**既沒有前 30 個的預設值,也沒有可設定的上限** —— `@objectstack/spec/data` 裡的 +`resolveSearchFields` 原樣回傳宣告的那份清單,所以本頁原先寫的「預設前 30 個(可設定)」什麼也沒有描述。 +結果筆數、查詢長度和延遲這三個數字都沒有出處。 ## AI Copilot 限制 | 項目 | 預設值 | | --- | --- | -| 每使用者每天的技能呼叫 | **500** | -| 每租戶每天的技能呼叫 | **50,000** | -| 知識庫大小 | 每個庫 **100,000** 個區塊 | -| 知識庫重新索引 | **每晚**或按需 | -| 每個技能提示的最大 token | **8,000**(輸入)/ **2,000**(輸出) | -| 最大同時技能執行數 | 每使用者 **10** | +| 每使用者每天的技能呼叫 | **500**(無出處) | +| 每租戶每天的技能呼叫 | **50,000**(無出處) | +| 知識庫大小 | 每個庫 **100,000** 個區塊(無出處) | +| 知識庫重新索引 | 無出處 —— 沒有宣告任何重新索引計畫 | +| 每個技能提示的最大 token | **8,000**(輸入)/ **2,000**(輸出)(無出處) | +| 最大同時技能執行數 | 每使用者 **10**(無出處) | | 嵌入儲存 | 按方案(通常為每個租戶 GB 級) | -批量技能呼叫(例如,審定 500 條線索)計為 500 次技能呼叫 —— 請留意配額。 +*沒有執行。* 對於 `src/skills/` 裡的六個技能,以及已安裝的任何套件,都不存在呼叫配額、token 預算、並行上限 +或重新索引計畫。你能拿到的計量來自你的模型供應商和你的部署,不來自這個應用。 ## 報表與分析 | 項目 | 限制 | | --- | --- | -| 顯示的報表列數 | **10,000** | -| 匯出的報表列數 | **1,000,000** | +| 顯示的報表列數 | **10,000**(無出處) | +| 匯出的報表列數 | **1,000,000**(無出處) | | 每使用者的儀表板 | 無限制 | -| 每個儀表板的小工具 | **50** | +| 每個儀表板的小工具 | **50**(無出處) | | 儀表板重新整理間隔 | Customer Service **60 秒**、Sales Performance **180 秒**、其餘三個 **300 秒** —— 由 `src/dashboards/` 逐個儀表板宣告 | | cube 重新整理頻率 | 歸分析服務管。HotCRM 在 `src/` 裡沒有宣告任何重新整理計畫,也沒有增量與全量重新整理策略,因此應用側給不出這個數字 —— 請對著你自己的部署去核 | -| 並行報表執行(每使用者) | **5** | +| 並行報表執行(每使用者) | **5**(無出處) | + +*部分有出處。* 那兩列重新整理間隔是從 `src/` 裡讀出來的。報表的列數上限確實存在,但它是個**宿主選項** —— +`@objectstack/plugin-reports` 裡的 `ReportServiceOptions.maxRows`,「限制每份報表的列數,以同時保護資料庫和 +郵件體積」—— 它沒有預設值,而 HotCRM 也沒有設它,所以今天沒有任何報表列數限制在生效。 如需更深入的分析,請[匯出到資料倉儲](/zh-Hant/docs/guides/import-and-export)。 @@ -90,60 +130,100 @@ description: 記錄、API 速率限制、AI 配額、檔案大小 —— HotCRM | 項目 | 限制 | | --- | --- | -| 每頁可見列數 | **200**(可設定至 500) | -| 篩選子句 | 每個檢視 **20** 個 | -| 欄 | 每個檢視 **40** 欄 | -| 每使用者儲存的檢視 | **500** | +| 每頁可見列數 | **200**(可設定至 500)(無出處) | +| 篩選子句 | 每個檢視 **20** 個(無出處) | +| 欄 | 每個檢視 **40** 欄(無出處) | +| 每使用者儲存的檢視 | **500**(無出處) | + +*沒有執行。* `ObjectSchema.listViews` 和 `*.view.ts` 中繼資料你宣告多少欄、多少篩選子句就接受多少,應用和 +已安裝的平台都不施加每頁列數的天花板。 ## 自動化限制 | 項目 | 限制 | | --- | --- | | 每次儲存的流程重新評估 | 沒有固定預算 —— 流程自身的寫入會重新進入觸發順序;引擎用**重入守衛**打斷自觸發環,而不是計次(兜底,不是上限)。參見[自動化](/zh-Hant/docs/administration/automation) | -| 流程步驟 | 每個流程 **500** | -| 流程迴圈迭代 | **2,000** | -| 排程作業並行 | 全租戶同時 **20** | -| 每小時的郵件提醒 | 每使用者 **1,000** / 每租戶 **50,000** | -| 每個流程的審批步驟 | **30** | +| 流程步驟 | 沒有上限 | +| 流程迴圈迭代 | 每個迴圈節點 **100,000** 的天花板,其下是該節點自己宣告的 `maxIterations` | +| 排程作業並行 | 無出處 —— 沒有宣告任何並行旋鈕 | +| 每小時的郵件提醒 | 無出處 | +| 每個流程的審批步驟 | 無出處 —— 不存在步驟上限 | -## 郵件與行事曆同步 +*部分有出處。* 迴圈天花板是 `@objectstack/spec/automation` 裡的 `LOOP_MAX_ITERATIONS_CEILING`;迴圈節點自己 +宣告的 `maxIterations` 位於其下。本頁原先寫的 **2,000** 比真實天花板低 50 倍,而它寫的 **500** 流程步驟上限 +根本不存在。第一列提到的重入兜底是每個節點 `MAX_NODE_REENTRIES = 100`,配合 +`DEFAULT_MAX_TERMINAL_RUNS_PER_FLOW = 100`(`@objectstack/service-automation`)—— 那是個斷環器,不是給你花 +的預算。 -| 項目 | 限制 | +## 郵件與行事曆同步(未提供) + +設想中的形態:一個信箱連接器,把收發的郵件登記到記錄上,行事曆雙向同步,外加開啟/點擊追蹤 —— 每一項都配著 +下面這些配額。這些今天一個都沒有提供,所以這些限制什麼也約束不到。應用真正寫入什麼,見 +[郵件與行事曆](/zh-Hant/docs/guides/email-and-calendar)。 + +| 項目 | 限制(設想中) | | --- | --- | | 記錄的郵件正文大小 | **5 MB** | | 每使用者同步的行事曆事件 | **5,000** 個活躍 | | 郵件保留 | 預設 **2 年**(可設定) | | 儲存的追蹤像素事件 | 每封郵件無限期 | +真正提供的是:聯絡人上的**傳送郵件**會透過平台郵件服務寫入一筆排隊中的郵件記錄,而**記錄通話/記錄會議/ +安排會議**會寫入真實的 `crm_event` 記錄及其參與人列。沒有任何東西按計畫保留、沒有任何東西被追蹤、也沒有 +任何東西從你的信箱同步過來 —— 所以既沒有保留視窗可設定,也沒有追蹤事件庫可填。 + ## Webhook 與事件匯流排 | 項目 | 限制 | | --- | --- | -| Webhook 負載 | **1 MB** | -| Webhook 重試 | **5** 次,採用指數退避 | -| Webhook 逾時 | **10 秒** | -| 事件匯流排吞吐量 | 每租戶 **1,000 個事件/秒** | -| 匯流排中的事件保留 | **7 天** | +| Webhook 負載 | **1 MB**(無出處) | +| Webhook 重試 | **5** 次,採用指數退避(無出處) | +| Webhook 逾時 | 每個 webhook 自己的 `timeoutMs` —— 沒有宣告平台預設值 | +| 事件匯流排吞吐量 | 每租戶 **1,000 個事件/秒**(無出處) | +| 匯流排中的事件保留 | **7 天**(無出處) | + +*HotCRM 沒有執行。* `@objectstack/plugin-webhooks` 是裝上了,也帶有按 webhook 的 `timeoutMs`,但**本應用沒有 +註冊任何 webhook** —— `src/` 裡沒有任何 webhook 宣告,所以這裡每一個數字說的都是一個得由你的部署自己去建立 +和設定的訂閱。 ## 使用者與工作階段限制 | 項目 | 限制 | | --- | --- | +| 每使用者的並行工作階段 | `maxConcurrentSessions` —— 除非你的部署設了它,否則**關閉** | +| 工作階段閒置逾時 | `sessionIdleTimeoutMinutes` —— 除非你的部署設了它,否則**關閉** | +| 工作階段絕對逾時 | `sessionAbsoluteMaxHours` —— 除非你的部署設了它,否則**關閉** | | 每個租戶的使用者 | 無限制(受方案約束) | -| 每使用者的並行工作階段 | **5**(可設定) | -| 工作階段閒置逾時 | **30 分鐘**(可設定) | -| 工作階段絕對逾時 | **12 小時**(可設定) | + +*有出處,而且預設值和本頁原先說的正好相反。* 這三個控制都是真的(ADR-0069 D4,`@objectstack/plugin-auth`): +閒置與絕對逾時在每個請求上執行,並行上限在登入時執行,把超出數量的最舊工作階段撤銷掉。但**每一項 `0` 都表示 +關閉,而關閉就是預設值** —— HotCRM 一項都沒有設,所以本頁原先當作預設值呈現的 **5 個工作階段 / 30 分鐘 / +12 小時**在任何地方都沒有生效。它們是部署設定,而且在這個應用的任何介面裡都改不了。 ## 如何保持在限制範圍內 ### 分頁 -當擷取 >2,000 筆記錄時: +用 `limit` 和 `offset` 對著物件自己的 REST 路徑分頁 —— 帶 `crm_` 前綴的物件名,掛在 `/data` 下: ```http -GET /api/v1/opportunities?limit=200&cursor=eyJpZCI6... +GET /api/v1/data/crm_opportunity?limit=200&offset=400 ``` +若要在大結果集上穩定地走一遍,把 keyset 表達成排序鍵上的普通述詞,發給查詢路由: + +```http +POST /api/v1/data/crm_opportunity/query +{ "where": { "created_at": { "$gt": "2026-01-01T00:00:00Z" } }, + "orderBy": "created_at", "limit": 200 } +``` + +> **`cursor` 已被移除。** `query.cursor` 和 `QueryBuilder.cursor()` 在 `@objectstack/spec` 17 +> (#4286、ADR-0049)中被移除,這個鍵現在會被按名拒絕:從來沒有驅動實作過 keyset 分頁,所以 cursor 被接受 +> 之後就被忽略,每一頁回傳的都是同一批 —— 一個「迴圈到 `hasMore` 為 false 為止」的呼叫方永遠不會停。本頁原先 +> 印的那個 `?cursor=…` 範例根本不可能奏效。將來若真做出一等公民的 cursor,它會是回應方鑄造的不透明 token, +> 而不是呼叫方自己拼的記錄。 + 請始終分頁;切勿一次性載入所有記錄。 ### 退避 @@ -167,23 +247,48 @@ async function callApi(fn, retries = 5) { ### 批量操作 -對於 >100 筆記錄,使用**批量 API**而非 100 次單獨呼叫。 +一次處理很多筆記錄時,請用批量路由,而不是一筆記錄一次呼叫。四個都提供了: +`POST /api/v1/data/:object/batch`、`/createMany`、`/updateMany` 和 `/deleteMany`,外加跨物件的 +`POST /api/v1/batch`。注意上面 *API 限制* 裡那個 10,000 列的逐列鉤子上限。 ### AI 呼叫批次處理 -在分流大量記錄時,使用**批量技能**呼叫 —— 它在底層進行批次處理並計為 N 次呼叫(因此仍遵守每使用者配額),但使用連線池實現約 10× 的加速。 +*未提供。* 不存在批量技能呼叫這個面:`src/skills/` 裡的六個技能是一次一呼的,應用和已安裝的套件裡都不存在 +批次處理、連線池或「約 10× 加速」這條路徑。要分流很多記錄,就意味著很多次呼叫。清單檢視上的批量**操作**是 +另一回事,它是真的 —— 參見[自動化](/zh-Hant/docs/administration/automation)。 ### 欄位索引 -對於你頻繁查詢的自訂欄位,將其標記為**索引** —— 加快清單篩選和報表執行。 +索引宣告**在物件上,不在欄位上**,而且是由開發者寫在中繼資料裡,不是由管理員在介面裡點: + +```ts +indexes: [{ name: 'idx_account_name', fields: ['name'] }], +``` + +HotCRM 今天已經在十五個物件上宣告了十六個這樣的區塊(例如 `src/objects/account.object.ts:442`);SQL 驅動 +透過 `syncDeclaredIndexes` 把它們物化出來。**不存在按欄位的 `indexed` 開關** —— `FieldSchema` 會按名拒絕這個 +鍵,並附上理由:*「從來不是 FieldSchema 的鍵;欄位級的索引旗標沒有建出過任何索引(#2377)。請在物件的 +`indexes[]` 裡宣告索引。」* 所以本頁原先給的建議 ——「把欄位標記為索引」—— 點的是一個從未建出索引的旗標。 +`indexes[].type` 和 `indexes[].partial` 出於同樣的理由在 spec 17(#5248)中被移除;兩個都別寫。 ### 歸檔舊資料 -超出你營運視窗的記錄(例如,關閉超過 2 年的案例)可以被**歸檔**: +歸檔是一項真實的平台能力,也確實是一條**按物件**的策略,但它宣告在物件的中繼資料裡 —— 不是在介面裡設定的 +—— 而且 **HotCRM 哪裡都沒有宣告它**: + +```ts +lifecycle: { class: 'record', archive: { after: '2y', to: 'cold', keep: '7y' } }, +``` -- 從熱儲存中移除;仍可透過**歸檔 API** 查詢。 -- 釋放索引並提升效能。 -- 可按物件設定自動歸檔策略。 +`ObjectSchema.lifecycle.archive` 接受 `after`(多舊)、`to`(要複製到的資料來源)以及可選的 `keep`(冷資料 +保留多久)。隨後 ObjectQL 的生命週期清掃會把超過 `after` 的列從熱儲存複製到那個歸檔資料來源,並把已複製的 +列從熱儲存刪除(ADR-0057 §3.3)—— 這就是釋放索引的那一步。今天 `src/` 裡沒有任何物件帶 `lifecycle` 區塊, +所以 HotCRM 裡沒有任何東西被歸檔,也沒有任何東西被過期。 + +> **不存在歸檔 API。** 歸檔後的列不會繼續透過普通記錄路由可達:它們住在 `archive.to` 指定的那個資料來源裡, +> 要讀它們就得去查那個資料來源。REST 面上沒有任何歸檔路由 —— 資料面就是 `/data/:object`、`/:id`、`/query`、 +> `/clone`、`/import*`、`/export`、`/batch`、`/createMany`、`/updateMany`、`/deleteMany`、`/shares`,加上 +> 報表與審批的那些路由,再沒有別的了。 ## 監控你的使用情況 @@ -193,6 +298,9 @@ async function callApi(fn, retries = 5) { ## 當你超出這些限制時 +下面這三條是你的**方案和部署**的性質,不是應用的性質,`src/` 和已安裝的套件都無法證實它們 —— 請找維運你這套 +執行個體的人去談: + - **縱向** —— Enterprise+ 方案擴展大多數配額。 - **分片** —— 用於 >10 億筆記錄的多區域部署。 - **自託管** —— 消除平台上限;瓶頸變為基礎設施。