diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/docs/next-env.d.ts +++ b/apps/docs/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/content/docs/build/automation/approvals.zh-Hans.mdx b/content/docs/build/automation/approvals.zh-Hans.mdx new file mode 100644 index 0000000..ae1a591 --- /dev/null +++ b/content/docs/build/automation/approvals.zh-Hans.mdx @@ -0,0 +1,143 @@ +--- +title: 审批流程 +description: 把记录路由给人签核 —— 同时不让自动化悄悄绕过行级安全。 +--- + +# 审批流程 + +**审批就是带审批节点的[流程](/docs/build/automation/flows):流程暂停,直到有人批准或拒绝,然后沿匹配的分支继续。**没有需要另学的独立审批引擎 —— 触发器、分支、错误处理都与任何其他流程完全一致。本页新增的内容是审批节点本身,以及两个与路由同样重要的访问决策。 + +## 谁做什么 + +三个角色,刻意分离: + +| 角色 | 能做 | 需要 | +|---|---|---| +| **构建者** | 编写和编辑审批流程 | `manage_metadata`(通常是 Studio 用户) | +| **提交人** | 提交进入流程的记录 | 正常的记录访问 —— 不需要任何自动化配置权限 | +| **审批人** | 处理审批请求(批准 / 拒绝) | 由其权限集授予的能力 | + +最终用户提交记录、处理请求;他们从不编辑自动化。把自动化配置界面挡在消费者应用之外。 + +## 以谁的身份运行 —— 安全决策 + +流程通过 `runAs` 声明运行身份,对审批而言,正是这个决策防止流程悄悄绕过行级安全: + +| `runAs` | 数据操作以谁运行 | 何时使用 | +|---|---|---| +| `'user'`(默认) | **提交人**,遵守其 RLS | 流程只触碰提交人本来就能看到的记录 | +| `'system'` | **提权** —— 绕过 RLS | 流程必须读写提交人看不到的记录(写入总账、通知一个拥有提交人不可见行的审批人) | + +**显式**声明提权,让它可见而非意外。默认的 `'user'` 意味着审批流程无法悄悄给提交人跨租户或跨所有者的触达 —— 提权是自愿开启且可审计的。 + +> **提示:**由定时触发的升级流程没有触发用户 —— 它必须 `runAs: 'system'` 才能行动。这是提权的正当场景;"到处 `system` 好让它能跑"才是反模式。 + +## 审批节点 + +节点声明**谁来审批**以及多个决策如何聚合: + +```ts +{ + id: 'manager_approval', + type: 'approval', + label: 'Manager Approval', + config: { + approvers: [{ type: 'field', value: 'owner_manager_id' }], + behavior: 'unanimous', + approvalStatusField: 'approval_status', + lockRecord: true, + }, +} +``` + +| 配置 | 作用 | +|---|---| +| `approvers` | 谁必须决策 —— 具名用户、岗位,或从记录字段解析出的用户(如提交人的经理) | +| `behavior` | 多个审批人如何聚合,如 `'unanimous'`(一致同意) | +| `approvalStatusField` | 可选的记录字段,插件把请求状态镜像进去 | +| `lockRecord` | 请求待定期间锁定记录、禁止编辑 | + +> **警告 —— `position` vs `role`。**`{ type: 'position', value: 'finance_manager' }` 路由给某个岗位的持有者(`sys_user_position`)。而 `role` 审批人类型是组织成员层级(`sys_member.role`:`owner`/`admin`/`member`)—— 把岗位名写成 `type: 'role'` 谁也匹配不上,请求就会卡住。`os lint` 会标记这个问题(`approval-role-not-membership-tier`)。 + +## 一个完整的审批流程 + +把大额提案路由给负责人的经理,再按决策分支: + +```ts +export const opportunityApproval = defineFlow({ + name: 'opportunity_approval', + label: 'Opportunity Approval', + type: 'record_change', + status: 'active', + runAs: 'user', // 用提交人的 RLS,除非某一步确实需要更多权限 + nodes: [ + { + id: 'start', + type: 'start', + config: { + triggerType: 'record-after-update', + objectName: 'opportunity', + condition: "record.amount >= 50000 && record.stage == 'proposal'", + }, + }, + { + id: 'manager_approval', + type: 'approval', + label: 'Manager Approval', + config: { + approvers: [{ type: 'field', value: 'owner_manager_id' }], + behavior: 'unanimous', + approvalStatusField: 'approval_status', + lockRecord: true, + }, + }, + { id: 'mark_approved', type: 'update_record', label: 'Mark Approved' }, + { id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' }, + { id: 'end', type: 'end' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'manager_approval' }, + { id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' }, + { id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' }, + { id: 'e4', source: 'mark_approved', target: 'end' }, + { id: 'e5', source: 'mark_rejected', target: 'end' }, + ], +}); +``` + +注意带标签的边:`approve` 和 `reject` 命名了决策之后的分支。永远要建模拒绝路径 —— 只有批准分支的流程会让被拒绝的记录搁浅。 + +**多级审批:**串联多个 `approval` 节点。**并行审批:**见[流程](/docs/build/automation/flows)中的聚合节点模式。 + +## 审批人体验到什么 + +`@objectstack/plugin-approvals` 包持有持久化的审批状态。请求待定期间: + +1. 插件持久化请求(`sys_approval_request`)以及针对它的每个决策(`sys_approval_action`)—— 你的审批历史是可查询的数据。 +2. 设置 `lockRecord: true` 时,记录被锁定、禁止编辑,直到决策落地。 +3. 如果设置了 `approvalStatusField`,记录自身的字段会镜像请求状态,视图和报表就能按它筛选。 +4. 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。 + +批准本身也是被门控的操作。把"可以批准"建模为一个能力(如 `approve_invoice`),由审批人的权限集授予,并把批准操作的 `requiredPermissions` 建立在它之上 —— 这样门控就在 **UI 和服务端**两侧同时强制,而不只是从屏幕上藏起来。 + +## 最佳实践 + +| 应该 | 不应该 | +|---|---| +| 定义清晰的进入条件 | 设置过多的审批环节 | +| 设置合理的超时时间 | 把审批做得过于复杂 | +| 在合适的场景允许撤回 | 忘记拒绝路径 | +| 通知所有相关方 | 把审批人写死 | +| 追踪审批历史 | 只在 UI 层门控"批准" | +| 默认 `runAs: 'user'`,一步一步地提权 | 到处设置 `runAs: 'system'` "好让它能跑" | + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [流程](/docs/build/automation/flows) | 本页所基于的流程参考 —— 触发器、步骤、错误处理 | +| [工作流](/docs/build/automation/workflows) | 约束审批所处的生命周期 | +| [操作](/docs/build/interface/actions) | 把提交和批准做成界面上的按钮 | +| [CEL 表达式](/docs/reference/cel) | 进入条件背后的语言 | +| [Email](/docs/configure/email) | 通知审批人与相关方 | +| [自动化总览](/docs/build/automation) | 选择器:流程 vs 工作流 vs 审批 | diff --git a/content/docs/build/automation/index.zh-Hans.mdx b/content/docs/build/automation/index.zh-Hans.mdx new file mode 100644 index 0000000..ff2e176 --- /dev/null +++ b/content/docs/build/automation/index.zh-Hans.mdx @@ -0,0 +1,39 @@ +--- +title: 自动化 +description: 为任务选对工具 —— 流程管步骤,工作流管状态,审批管人工签核。 +--- + +# 自动化 + +**自动化以声明的方式把业务逻辑挂到数据模型上 —— 作为由运行时执行的元数据 —— 而不是散落在应用代码里。**三个工具覆盖全部场景,一开始就选对,能省去日后返工。 + +## 我该用哪一个? + +| 你需要 | 用 | 阅读 | +|---|---|---| +| "X 发生时,做 Y" —— 由记录变更、定时或按钮点击驱动的步骤 | **流程** | [流程](/docs/build/automation/flows) | +| "这条记录只能按这些事件在这些状态间移动" —— 受控的生命周期 | **工作流**(状态机) | [工作流](/docs/build/automation/workflows) | +| "必须有人签核后才能继续" —— 路由给人的决策 | **审批** | [审批流程](/docs/build/automation/approvals) | + +经验法则:**状态**用工作流建模,**步骤**用流程建模。审批不是独立引擎 —— 审批就是在审批节点暂停、等人决策的流程。 + +## 它们如何组合 + +三者是互补而非竞争: + +- **工作流**约束哪些生命周期迁移是合法的 —— 状态、迁移、守卫条件,仅此而已。 +- **流程**执行这些迁移周边的副作用:发邮件、更新记录、调用外部服务、等待、分支。 +- 流程内的**审批节点**阻塞执行直到审批人行动,然后沿批准或拒绝分支继续。 + +所以"工单从 new → assigned → resolved"是工作流;"升级时通知经理"是流程;"超过 5 万美元的折扣需要财务经理签核"是带审批节点的流程。 + +> **提示:**如果需求读起来是"X 发生时,做 Y",那就是流程。如果读起来是"这条记录绝不能跳过某个状态",那就是工作流。从这里出发,你几乎不会选错。 + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [流程](/docs/build/automation/flows) | 完整的流程参考 —— 触发器、步骤类型、错误处理、CEL | +| [工作流](/docs/build/automation/workflows) | 严格生命周期的状态、迁移与守卫条件 | +| [审批流程](/docs/build/automation/approvals) | 审批节点、运行身份与审批人体验 | +| [CEL 表达式](/docs/reference/cel) | 条件与守卫条件所用的表达式语言 | diff --git a/content/docs/build/automation/workflows.zh-Hans.mdx b/content/docs/build/automation/workflows.zh-Hans.mdx new file mode 100644 index 0000000..3d67c5d --- /dev/null +++ b/content/docs/build/automation/workflows.zh-Hans.mdx @@ -0,0 +1,139 @@ +--- +title: 工作流 +description: 把记录的生命周期建模成状态机 —— 合法状态、带守卫条件的迁移,其余的交给流程。 +--- + +# 工作流 + +**工作流把记录的生命周期建模为有限状态机:记录可以处于的状态、在状态间移动它的事件,以及移动成立所必须满足的守卫条件。**当核心需求是"这个对象只能按这些事件在这些状态间移动"时,就用它。 + +不存在独立的 Salesforce 式 Workflow Rule 编写类型。旧的"工作流"概念被干净地一分为二: + +- **状态机元数据**负责严格的生命周期迁移 —— 即本页。 +- **[流程](/docs/build/automation/flows)**负责事件触发或定时的自动化,包括[审批](/docs/build/automation/approvals)暂停。 + +## 工作流 vs 流程 + +| | 工作流(状态机) | 流程 | +|---|---|---| +| 建模 | *状态* —— 记录处于生命周期的哪里 | *步骤* —— 某事发生时做什么 | +| 回答 | "此刻允许这个迁移吗?" | "我们要对它做什么?" | +| 形态 | 状态、迁移、守卫条件 | 节点与边:触发器、动作、分支 | +| 副作用 | 无 —— 它只做约束 | 全都有 —— 邮件、更新、HTTP、等待 | + +两者组合使用:状态机约束迁移;当你需要通知、更新记录或调用外部系统时,由流程执行迁移周边的副作用。 + +## 定义状态机 + +一个必须按 `new → assigned → resolved` 移动、且带升级路径的支持工单: + +```ts +import type { StateMachineConfig } from '@objectstack/spec/automation'; + +export const caseLifecycle: StateMachineConfig = { + id: 'case_lifecycle', + initial: 'new', + states: { + new: { + on: { + ASSIGN: { target: 'assigned' }, + }, + }, + assigned: { + on: { + RESOLVE: { target: 'resolved', cond: 'has_resolution' }, + ESCALATE: { target: 'escalated' }, + }, + }, + escalated: { + on: { + RESOLVE: { target: 'resolved', cond: 'has_resolution' }, + }, + }, + resolved: { + type: 'final', + }, + }, +}; +``` + +把它当契约来读:`new` 状态的工单只能被分派。`assigned` 状态的工单可以被解决 —— 但只有 `has_resolution` 守卫条件通过时 —— 或者被升级。`resolved` 是终态;再没有什么能移动它。 + +## 解剖 + +| 键 | 声明什么 | +|---|---| +| `id` | 状态机的标识符 | +| `initial` | 每条新记录的初始状态 | +| `states` | 状态名 → 其出向迁移的映射 | +| `on` | 该状态响应的事件(`ASSIGN`、`RESOLVE`……) | +| `target` | 事件把记录移动到的状态 | +| `cond` | 迁移触发前必须满足的守卫条件 | +| `type: 'final'` | 终态 —— 没有出向迁移 | + +### 守卫条件 + +守卫条件(`cond`)让迁移带上前提:上例中,只有当 `has_resolution` 成立时 `RESOLVE` 才能到达 `resolved`。守卫条件把"没有解决方案就不能关单"编码成结构性规则,而不是散落在 UI 代码里的验证。 + +### 事件,而非字段写入 + +迁移由具名**事件**(`ASSIGN`、`ESCALATE`)触发,而不是对状态字段的任意编辑。这正是重点:状态机定义了合法移动的完整集合,未声明的一律不可能发生。 + +> **提示:**保持状态机最小 —— 状态、迁移、守卫条件。一旦你想"然后再发封邮件",你就已经离开了工作流的领地:把副作用放进一个响应该迁移的[流程](/docs/build/automation/flows)。 + +## 副作用属于流程 + +状态机只描述合法迁移与守卫条件 —— 别无其他。当某次迁移应该*做*点什么(通知销售、写入关闭日期、调用外部系统)时,给状态机配上一个由记录变更触发的流程: + +```ts +export const dealClosedWon = defineFlow({ + name: 'deal_closed_won', + type: 'record_change', + nodes: [ + { + id: 'start', + type: 'start', + config: { + triggerType: 'record-after-update', + objectName: 'opportunity', + condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'", + }, + }, + { id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' }, + { id: 'notify_sales', type: 'notify', label: 'Notify Sales' }, + { id: 'end', type: 'end' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'set_closed_date' }, + { id: 'e2', source: 'set_closed_date', target: 'notify_sales' }, + { id: 'e3', source: 'notify_sales', target: 'end' }, + ], +}); +``` + +状态机保证这笔交易是合法地*到达* `closed_won` 的;流程处理接下来发生的事。上面这样的条件是 CEL 表达式 —— 见 [CEL](/docs/reference/cel)。 + +## 从 Workflow Rules 迁移 + +如果你来自有 Workflow Rules 的平台,对照表如下: + +| 旧概念 | 当前对应 | +|---|---| +| Workflow Rule | 流程 | +| 时间触发器 | 定时流程 | +| 字段更新动作 | `update_record` 节点 | +| 邮件提醒 | `notify` 节点 | +| HTTP 调用 | `http` 节点 | +| Approval Process | 带一个或多个 `approval` 节点的流程 | + +判断标准:如果旧规则是"X 发生时,做 Y",就建成一个小流程。如果它是"这条记录必须在受控状态间移动",就把生命周期建成状态机,副作用交给流程。 + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [流程](/docs/build/automation/flows) | 迁移周边的副作用 —— 触发器、步骤、错误处理 | +| [审批流程](/docs/build/automation/approvals) | 作为流程内暂停的人工签核 | +| [CEL 表达式](/docs/reference/cel) | 守卫条件与流程条件背后的语言 | +| [数据建模](/docs/build/data) | 你正在约束其生命周期的对象 | +| [自动化总览](/docs/build/automation) | 选择器:流程 vs 工作流 vs 审批 | diff --git a/content/docs/build/data/formulas.zh-Hans.mdx b/content/docs/build/data/formulas.zh-Hans.mdx new file mode 100644 index 0000000..795aced --- /dev/null +++ b/content/docs/build/data/formulas.zh-Hans.mdx @@ -0,0 +1,194 @@ +--- +title: 公式 +description: 计算字段、动态默认值与条件逻辑 —— 元数据需要"思考"的地方,都用同一门 CEL 表达式语言。 +--- + +# 公式 + +**一门表达式语言,覆盖所有场景。**只要有一段元数据需要计算值或求值条件,ObjectOS 就使用 [CEL](/docs/reference/cel)(谷歌的 Common Expression Language)—— 公式字段、动态默认值、条件可见性、验证条件、流程决策。语法学一次,处处能用。 + +| 场景 | CEL 在那里做什么 | +|---|---| +| **公式字段**(`type: 'formula'`) | 读取时由其他字段计算出一个值 | +| **动态默认值**(``defaultValue: cel`...` ``) | 在插入时求值,而不是编译时 | +| **字段谓词**(`visibleWhen`、`readonlyWhen`、`requiredWhen`) | 按条件显示 / 锁定 / 必填某个字段 | +| **[验证规则](/docs/build/data/validation-rules)**(`condition`) | 标记无效记录 | +| **[视图](/docs/build/interface/views)**(`visibleOn`、`conditionalFormatting`) | 条件区块与行样式 | +| **[流程](/docs/build/automation/flows)**(决策、Hook 条件) | 按记录状态分支 | + +本页讲构建者如何*使用*公式。完整的语言 —— 运算符、标准库、`Expression` 信封 —— 见 [CEL 参考](/docs/reference/cel)。 + +## 公式字段 + +公式字段在读取时由一条 CEL 表达式计算出它的值。它是只读的 —— 永远不能直接写入: + +```ts +import { F } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Invoice = ObjectSchema.create({ + name: 'invoice', + fields: { + subtotal: Field.decimal({ label: 'Subtotal' }), + tax_rate: Field.decimal({ label: 'Tax Rate' }), + + total: Field.formula({ + label: 'Total', + returnType: 'number', + expression: F`record.subtotal + record.subtotal * record.tax_rate`, + }), + }, +}); +``` + +两个关键的键: + +| 键 | 作用 | +|---|---| +| `expression` | CEL 源码 —— 运行时**唯一**求值的键 | +| `returnType` | 公式返回什么:`'number'`、`'text'`、`'boolean'`、`'date'` —— 供仪表盘与格式化读取 | + +> **警告:**公式字段的 `type` 永远是 `formula`。**不要**给 `Field.formula` 传 `type: 'currency'` 或 `type: 'number'` —— 那会覆盖 `type: 'formula'`,字段就静默地永远不再计算。值的类型请用 `returnType` 声明。 + +### 日常模式 + +```ts +// 分档标签 +Field.formula({ + label: 'Discount Tier', + returnType: 'text', + expression: ` + record.amount > 1000000 ? "Platinum" : + record.amount > 500000 ? "Gold" : + record.amount > 100000 ? "Silver" : "Bronze" + `, +}) + +// 距关闭还有几天 +Field.formula({ + label: 'Days to Close', + returnType: 'number', + expression: 'daysBetween(record.close_date, today())', +}) + +// 全名 —— joinNonEmpty 会跳过空白部分 +Field.formula({ + label: 'Full Name', + returnType: 'text', + expression: F`joinNonEmpty([record.salutation, record.first_name, record.last_name], ' ')`, +}) + +// 毛利率,保留 2 位小数 +Field.formula({ + label: 'Gross Margin %', + returnType: 'number', + expression: 'record.revenue > 0 ? ((record.revenue - record.cost) / record.revenue) * 100 : 0', + scale: 2, +}) +``` + +> **提示:**CEL 遇到 `null + string` 会抛错,所以拼接时把可能为空的操作数包进 `coalesce(record.x, '')` —— 或者直接用 `joinNonEmpty`,省掉这套仪式。 + +### 公式作为记录标题 + +自 ADR-0079 起,记录的标题由 `nameField` 指定的字段决定 —— 组合式标题可以用文本公式: + +```ts +ObjectSchema.create({ + name: 'invoice', + nameField: 'display_title', + fields: { + display_title: Field.formula({ + returnType: 'text', + expression: F`"Invoice " + string(record.invoice_no) + " – " + record.customer.name`, + }), + }, +}); +``` + +## 动态默认值 + +包在 `` cel`...` `` 里的 `defaultValue` 在**插入时**求值 —— 用的是用户的时钟和身份,不是你笔记本的: + +```ts +import { cel } from '@objectstack/spec'; + +issued_date: Field.date({ + defaultValue: cel`today()`, +}), +due_date: Field.date({ + defaultValue: cel`daysFromNow(30)`, +}), +``` + +同样的技巧也适用于种子数据:一条带 ``close_date: cel`daysFromNow(45)` `` 的种子记录会在包安装时解析,因此演示数据永远新鲜,而不是把编译时的时间戳固化进包里。 + +## 条件式字段行为 + +谓词让字段随记录的其余部分而变化: + +```ts +import { P } from '@objectstack/spec'; + +po_number: Field.text({ + label: 'PO Number', + requiredWhen: P`record.amount > 10000`, +}), +rating: Field.select({ + label: 'Rating', + options: [ /* ... */ ], + visibleWhen: P`record.status == 'qualified'`, +}), +notes: Field.textarea({ + readonlyWhen: P`record.status == 'approved'`, +}), +``` + +`F`(公式)、`P`(谓词)、`cel` 这几个标签模板 helper 可以互换 —— 在调用处哪个读起来顺就用哪个。 + +## 30 秒学会 CEL + +| 概念 | 语法 | +|---|---| +| 当前记录的字段 | `record.amount` | +| 更新前的值 | `previous.status` | +| 相等 / 逻辑 | `==` `!=` `&&` `\|\|` `!` | +| 条件表达式 | `cond ? then : else` | +| 成员判断 | `record.region in ['us', 'eu']` | +| 空白检查 | `isBlank(record.phone)` | +| 日期 | `today()`、`now()`、`daysBetween(a, b)`、`addDays(d, n)` | +| 字符串 | `upper`、`lower`、`trim`、`contains`、`matches` | + +完整的运算符表与标准库:[CEL 参考](/docs/reference/cel)。 + +> **警告:**谓词是裸 CEL —— 千万别把字段引用包进花括号。`{record.rating} >= 4` 是一个 **map 字面量**,会导致解析错误;应写成 `record.rating >= 4`。花括号只属于 `{{ … }}` 文本模板。畸形的表达式会导致构建失败并在运行时抛错 —— 它绝不会静默地求值为 `false`。 + +> **提示:**`has(record.x)` 只要键存在就为 true —— 哪怕值是 `null`。要检查"非空白",用 `isBlank(...)` 或 `record.x != null`。 + +## 最佳实践 + +| 应该 | 不应该 | +|---|---| +| 保持公式简洁易读 | 制造循环引用 | +| 测试边界情况(null、零、空) | 把三元表达式嵌套五层深 | +| 用 `coalesce` / `isBlank` 处理空值 | 把公式用在频繁变化的数据上 | +| 给每个公式声明 `returnType` | 重新实现本该由验证规则负责的逻辑 | + +## 用对话构建 + +你很少需要手写公式。告诉 [AI Builder](/docs/build/ai-builder): + +> *"在 opportunity 上加一个 Days to Close 公式:close_date 与今天之间的天数。"* + +它会生成 CEL,推断并写好 `returnType`,表达式在发布前会先经过校验 —— 无效的公式会带着定位信息使构建失败。 + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [CEL 参考](/docs/reference/cel) | 完整的语言:运算符、标准库、表达式信封 | +| [数据模型](/docs/build/data) | 公式字段所在的地方 | +| [验证规则](/docs/build/data/validation-rules) | 用 CEL 条件拦截坏写入 | +| [视图](/docs/build/interface/views) | `visibleOn` 与 `conditionalFormatting` 中的 CEL | +| [流程](/docs/build/automation/flows) | 决策与 Hook 条件中的 CEL | +| [字段类型](/docs/reference/field-types) | `formula`、`summary`、`autonumber` 等 | diff --git a/content/docs/build/data/relationships.zh-Hans.mdx b/content/docs/build/data/relationships.zh-Hans.mdx new file mode 100644 index 0000000..99aef8e --- /dev/null +++ b/content/docs/build/data/relationships.zh-Hans.mdx @@ -0,0 +1,167 @@ +--- +title: 关系 +description: 用查找(lookup)与主从(master-detail)连接对象 —— 级联规则、过滤选择器、层级结构、连接对象与汇总字段。 +--- + +# 关系 + +**关系就是一个字段。**把 `lookup` 或 `masterDetail` 字段指向另一个对象,ObjectOS 会把上层的一切都接好:外键完整性、Console 里的记录选择器、父记录页面上的相关列表,以及查询中的 `expand`。 + +| 类型 | 基数 | 删除默认行为 | 用于 | +|---|---|---|---| +| `lookup` | 多对一 | `set_null` | 松散引用(工单 → 负责人) | +| `masterDetail` | 多对一、有归属 | `cascade` | 父子归属(订单 → 明细行) | +| `tree` | 自引用 | — | 层级结构(分类、组织架构图) | +| `user` | 多对一 | `set_null` | 人员选择器 —— 特化为 `sys_user` 的查找 | +| `summary` | 汇总到父记录 | — | 聚合子记录(sum、count、avg) | + +## 查找(多对一) + +主力字段。引用另一个对象中的一条记录: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Contact = ObjectSchema.create({ + name: 'contact', + fields: { + account: Field.lookup('account', { + label: 'Account', + required: true, + }), + }, +}); +``` + +平台会校验被引用的记录确实存在 —— 每次写入都强制外键完整性。 + +> **提示:**按所指向的对象给字段命名 —— 用 `account`,而不是 `account_id`。这样相关列表、`expand` 和 AI Builder 读起来都更顺。 + +### 父记录被删除时会发生什么 + +用 `deleteBehavior` 控制级联行为: + +| 值 | 父记录删除时 | +|---|---| +| `set_null` | 子记录的引用被清空(lookup 默认) | +| `cascade` | 子记录也一并删除 | +| `restrict` | 只要还有子记录,就阻止删除 | + +## 过滤查找 + +约束选择器提供哪些记录。静态条件用 `lookupFilters`;用 `dependsOn` 让候选记录随同一记录上另一个字段的取值收窄: + +```ts +contact: Field.lookup('contact', { + label: 'Contact', + dependsOn: ['account'], // 只显示所选 account 下的联系人 + lookupFilters: [ + { field: 'is_active', operator: 'eq', value: true }, + ], +}) +``` + +> **警告:**旧的 `referenceFilters: string[]` 属性(如 `['is_active = true']`)虽然能通过 Schema 校验,但**记录选择器 UI 并不读取它** —— 它什么都不过滤。请始终使用上面展示的结构化 `lookupFilters` + `dependsOn`。 + +## 主从(有归属的子记录) + +`masterDetail` 是带归属语义的查找:子记录属于父记录,默认随父级联删除,并且可以在父表单上以明细行的形式内联编辑。 + +```ts +export const OrderLine = ObjectSchema.create({ + name: 'order_line', + fields: { + order: Field.masterDetail({ reference: 'order', label: 'Order' }), + product: Field.lookup('product', { label: 'Product' }), + quantity: Field.number({ label: 'Quantity', min: 1 }), + }, +}); +``` + +| 方面 | 行为 | +|---|---| +| 删除 | 运行时级联到子记录,除非 `deleteBehavior: 'restrict'` | +| 归属 | 子记录归父记录所有 | +| 编辑 | 可选:在父表单上内联编辑明细行 | +| 汇总 | `summary` 字段只在主(master)侧有效 | + +引用是可选或共享的,用 `lookup`;子记录离开父记录就没有意义的,用 `masterDetail`。 + +## 自引用查找与树 + +查找可以指回自己的对象,用来构建层级结构: + +```ts +parent_account: Field.lookup('account', { + label: 'Parent Account', + description: 'Parent company in hierarchy', +}) +``` + +对于专门的层级结构(分类、组织架构图),使用 `tree` 字段类型 —— 一个存储和展开方式与 `lookup` 相同的自引用查找。注意引擎在写入时**不做**环路检查,所以一条绕回自身的链不会被自动拒绝。 + +> **提示:**把 `tree` / 自引用查找字段与[树形列表视图](/docs/build/interface/views)搭配使用,即可在 Console 中把层级渲染为嵌套行。 + +## 相关列表 + +这些是免费得到的。当查找指向父对象时,子记录会自动以相关列表的形式出现在父记录的详情页上: + +- Contact 有 `account: Field.lookup('account')` +- → Account 详情页会显示一个 **Contacts** 相关列表 + +无需任何额外配置。 + +## 多对多:连接对象 + +ObjectOS 没有直接的多对多字段 —— 用**连接对象**来建模:一个带两个关系字段的对象,各指向一侧。 + +```ts +// Student ↔ Course,通过 Enrollment 连接 +export const Enrollment = ObjectSchema.create({ + name: 'enrollment', + fields: { + student: Field.masterDetail({ reference: 'student', label: 'Student' }), + course: Field.masterDetail({ reference: 'course', label: 'Course' }), + grade: Field.select({ label: 'Grade', options: [ /* ... */ ] }), + }, + indexes: [ + { fields: ['student', 'course'], unique: true }, // 每对组合只允许一条报名记录 + ], +}); +``` + +两侧都会把连接记录看作一个相关列表,而连接对象本身正是存放这段配对属性(成绩、角色、加入日期)的天然位置。 + +## 汇总(`summary` 字段) + +用 `summary` 字段把子记录聚合到父记录上。只在主对象上有效 —— 即主从关系的父侧: + +```ts +// 在 Order 对象上 +total_lines: { + type: 'summary', + label: 'Line Count', + summaryOperations: { + object: 'order_line', // 要聚合的子对象 + field: 'quantity', // 要聚合的字段 + function: 'sum', // count | sum | min | max | avg + }, +} +``` + +Summary 字段是只读的,由子记录计算得出 —— 你永远不会直接写它。 + +## 跨关系查询 + +用 `expand` 在一次调用里加载关联记录 —— 查询 API 会顺着查找字段把被引用的记录内联进来,客户端不必发出 N+1 次请求。 + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [数据模型](/docs/build/data) | 这些关系所在的对象、字段与 Schema | +| [公式](/docs/build/data/formulas) | 用 CEL 在记录内计算值 | +| [验证规则](/docs/build/data/validation-rules) | 跨字段规则与唯一约束 | +| [视图](/docs/build/interface/views) | 树形视图、Kanban(看板)与相关列表界面 | +| [字段类型](/docs/reference/field-types) | `lookup`、`master_detail`、`tree`、`summary` 的完整参考 | +| [AI Builder](/docs/build/ai-builder) | 在对话中描述关系,让平台自动接线 | diff --git a/content/docs/build/data/validation-rules.zh-Hans.mdx b/content/docs/build/data/validation-rules.zh-Hans.mdx new file mode 100644 index 0000000..64fe4cf --- /dev/null +++ b/content/docs/build/data/validation-rules.zh-Hans.mdx @@ -0,0 +1,198 @@ +--- +title: 验证规则 +description: 必填字段、唯一约束与 CEL 驱动的规则,在平台层拦住坏数据 —— 错误消息由你掌控。 +--- + +# 验证规则 + +**验证在每条写入路径上都会执行。**REST、Console 表单、ObjectQL —— 同一套规则处处生效,坏数据没有后门可钻。验证规则是作用于单条记录的确定性、同步、无副作用谓词:仅凭这次写入(更新时再加上更新前的记录)即可判定,不做任何 I/O。 + +验证是分层的 —— 能表达规则的最低层就是该用的层: + +| 层 | 表达什么 | 示例 | +|---|---|---| +| **字段修饰符** | 是否必填、是否唯一 | `required: true`、`unique: true` | +| **字段约束** | 单字段形态 | `min`、`max`、`maxLength`、`format` | +| **条件修饰符** | 依赖上下文的必填 | ``requiredWhen: P`record.amount > 10000` `` | +| **对象级验证规则** | 跨字段业务逻辑 | "折扣不能超过总额" | +| **唯一索引** | 组合 / 限定范围的唯一性 | `{ fields: ['code', 'organization'], unique: true }` | +| **生命周期 Hook** | 任意验证代码 | `beforeInsert` / `beforeUpdate` | + +## 字段级:required、unique 与约束 + +通用修饰符适用于所有字段类型: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +fields: { + email: Field.email({ label: 'Contact Email', required: true, unique: true }), + quantity: Field.number({ label: 'Quantity', min: 1, max: 9999 }), + code: Field.text({ label: 'Code', minLength: 3, maxLength: 20 }), +} +``` + +| 修饰符 | 行为 | +|---|---| +| `required: true` | 在运行时拒绝 `null` / `undefined` | +| `unique: true` | 数据库层唯一约束 —— 而不是有竞态的应用层检查 | +| `min` / `max` | 数值范围(number、currency、percent、rating、slider……) | +| `minLength` / `maxLength` | 文本类型的字符长度上下限 | +| `format` | 内置形态检查 —— `email`、`url`、`phone` 字段类型默认自带 | +| `readonly: true` | 更新时由服务端强制 —— 非系统写入该字段会被静默丢弃(插入不受限) | + +字段类型本身也自带免费验证:`email` 检查 `local@domain` 形态,`url` 要求带协议,`select` 的值必须匹配某个选项,`lookup` 强制被引用记录存在,`json` 必须能解析。每种默认行为见[字段类型参考](/docs/reference/field-types)。 + +### 条件式必填 / 只读 / 可见 + +是否必填可以通过 CEL 谓词依赖记录的其余部分: + +```ts +import { P } from '@objectstack/spec'; + +po_number: Field.text({ + label: 'PO Number', + requiredWhen: P`record.amount > 10000`, +}), +``` + +`visibleWhen`、`readonlyWhen`、`requiredWhen` 都接受一个 CEL 谓词 —— 见[公式](/docs/build/data/formulas)。 + +## 对象级验证规则 + +跨字段业务逻辑放在对象的 `validations` 数组里: + +```ts +export const Order = ObjectSchema.create({ + name: 'order', + fields: { + amount: Field.currency({ label: 'Amount', required: true }), + status: Field.select({ label: 'Status', options: [ /* ... */ ] }), + }, + + validations: [ + { + name: 'amount_positive', + type: 'script', + severity: 'error', + message: 'Amount must be greater than zero', + // CEL 谓词 —— TRUE 表示记录无效。 + condition: 'record.amount <= 0', + events: ['insert', 'update'], + }, + ], +}); +``` + +> **警告:**对 `script` 规则来说,`condition` 是**失败**谓词 —— 它求值为 TRUE 时,验证失败。要针对*坏*状态来写判断:`record.amount <= 0` 拒绝非正数金额。 + +### 公共属性 + +每种规则类型都共享这套基础形态: + +| 属性 | 必填 | 说明 | +|---|---|---| +| `name` | 是 | 唯一的规则名(`snake_case`) | +| `message` | 是 | 面向用户的错误消息 | +| `type` | 是 | `script`、`state_machine`、`format`、`cross_field`、`json_schema`、`conditional` | +| `severity` | 否 | `error`(阻止保存,默认)、`warning`(允许保存)、`info` | +| `events` | 否 | `insert`、`update`、`delete` —— 默认 `['insert', 'update']` | +| `priority` | 否 | 0–9999,数字越小越先执行(默认 100) | +| `active` | 否 | 不删除即可开关(默认 `true`) | + +### 六种规则类型 + +| 类型 | 检查什么 | 关键配置 | +|---|---|---| +| `script` | 任意 CEL 谓词 | `condition`(TRUE = 无效) | +| `state_machine` | 允许的状态迁移 | `field`、`transitions` 映射 | +| `format` | 单字段匹配正则或内置格式 | `field`、`regex` 或 `format: 'email' \| 'url' \| 'phone' \| 'json'` | +| `cross_field` | 字段之间的关系 | `fields`、`condition` | +| `json_schema` | JSON 字段匹配 JSON Schema | `field`、`schema` | +| `conditional` | 仅在谓词成立时应用嵌套规则 | `when`、`then`、可选 `otherwise` | + +两条你会反复用到的规则: + +```ts +// 状态机 —— 强制状态流转 +{ + name: 'order_status_transitions', + type: 'state_machine', + severity: 'error', + message: 'Invalid status transition', + field: 'status', + transitions: { + draft: ['submitted', 'cancelled'], + submitted: ['approved', 'rejected', 'cancelled'], + approved: ['completed'], + rejected: ['draft'], + cancelled: [], + completed: [], + }, + events: ['update'], +} + +// 跨字段 —— 日期先后有序 +{ + name: 'date_range_valid', + type: 'cross_field', + severity: 'error', + message: 'End date must be after start date', + fields: ['start_date', 'end_date'], + condition: 'record.end_date <= record.start_date', + events: ['insert', 'update'], +} +``` + +在更新时的条件里,`previous` 持有变更前的快照 —— `record.stage != previous.stage` 可检测到变更。 + +## 唯一性:用索引,不用规则 + +**没有** `uniqueness` 验证类型 —— 这是有意为之。先 SELECT 再 INSERT 的检查天然有竞态(TOCTOU);数据库唯一约束没有。唯一性要在数据层强制: + +```ts +// 字段级 +email: Field.email({ label: 'Contact Email', unique: true }), + +// 组合 / 限定范围的唯一性用索引 +indexes: [ + { fields: ['code', 'organization'], unique: true }, + // 需要限定范围 / 条件约束时加 `partial` +] +``` + +同样的逻辑也排除了异步 / 远程验证(那是客户端表单的关注点,放在写路径上还是 SSRF 与延迟隐患)和自定义处理器 —— 任意验证代码应该放进 `beforeInsert` / `beforeUpdate` 生命周期 Hook。 + +## 自定义错误消息 + +`message` 就是规则触发时用户看到的内容 —— 出现在 Console 表单和 API 错误响应中。要写得可执行: + +| 弱 | 强 | +|---|---| +| "Invalid input" | "Close date is required for closed deals" | +| "Validation failed" | "Phone must match format: +1-XXX-XXX-XXXX" | +| "Error" | "Discount cannot exceed total" | + +用 `severity` 来校准:`error` 阻止保存,`warning` 显示消息但放行保存,`info` 纯提示。 + +> **提示:**无法求值的谓词(解析错误、未绑定变量)会被当作损坏的规则处理 —— 记录日志后跳过,而不是拦下每一次写入。 + +## 最佳实践 + +| 应该 | 不应该 | +|---|---| +| 在尽可能低的层做验证(字段 → 规则 → Hook) | 用 Hook 做字段修饰符就能表达的事 | +| 写清晰、可执行的消息 | 在多层重复同一个检查 | +| 用 `priority` 让廉价的格式检查先跑 | 构造过于复杂的条件 | +| 用真实数据测试规则 | 拦住正当的边界情况 | + +## 下一步 + +| 页面 | 原因 | +|---|---| +| [数据模型](/docs/build/data) | 这些规则所保护的对象与字段 | +| [公式](/docs/build/data/formulas) | `condition` 与 `requiredWhen` 背后的 CEL 语言 | +| [关系](/docs/build/data/relationships) | 跨对象的引用完整性 | +| [CEL 参考](/docs/reference/cel) | 条件可用的完整运算符与标准库参考 | +| [字段类型](/docs/reference/field-types) | 各字段类型的内置验证默认值 | +| [流程](/docs/build/automation/flows) | 用自动化响应(合法的)记录变更 | diff --git a/content/docs/build/index.zh-Hans.mdx b/content/docs/build/index.zh-Hans.mdx index 5a0aa19..eea9bf8 100644 --- a/content/docs/build/index.zh-Hans.mdx +++ b/content/docs/build/index.zh-Hans.mdx @@ -23,6 +23,7 @@ description: 在 ObjectOS 里应用是怎么诞生的 —— 跟 AI 对话、在 |---|---|---| | **[包](/docs/build/packages)** | 组织单位 —— `com.acme.crm`,带版本,可安装 | Packages | | **[数据模型](/docs/build/data)** | 对象 + 字段 + 关系 + 状态机 | Data Model | +| **[界面](/docs/build/interface)** | 应用、视图、表单、仪表盘与页面 —— 用户看到的一切 | Interface | | **[Actions](/docs/build/interface/actions)** | 命名操作,可被 REST、Console 按钮、流程或 AI Agent 调用 | Actions | | **[流程](/docs/build/automation/flows)** | 声明式业务逻辑(自动启动 / 定时 / 手动) | Flows | | **[Agents](/docs/build/agents)** | 面向终端用户的 AI 助手 —— Agent → Skill → Tool | Agents | diff --git a/content/docs/build/interface/actions.zh-Hans.mdx b/content/docs/build/interface/actions.zh-Hans.mdx index 6f40ef4..dada8d4 100644 --- a/content/docs/build/interface/actions.zh-Hans.mdx +++ b/content/docs/build/interface/actions.zh-Hans.mdx @@ -1,9 +1,9 @@ --- -title: Actions +title: 操作 description: 平台从单一声明出发,将命名操作同时暴露为 REST 端点、Console 按钮、流程步骤和 AI 工具。 --- -# Actions +# 操作 **Action** 是对象上的一个命名操作。只需声明一次,它就会以以下形式出现: diff --git a/content/docs/build/interface/apps.zh-Hans.mdx b/content/docs/build/interface/apps.zh-Hans.mdx new file mode 100644 index 0000000..4e23918 --- /dev/null +++ b/content/docs/build/interface/apps.zh-Hans.mdx @@ -0,0 +1,137 @@ +--- +title: 应用与导航 +description: 把对象、视图、页面和仪表盘打包成一个带品牌、可导航的外壳 —— 并精确控制谁能看到什么。 +--- + +# 应用与导航 + +**应用**是一个逻辑容器,把对象、视图、页面和仪表盘打包成一体化的体验。它定义导航树、品牌,以及 —— 最关键的 —— 谁能进来。 + +```ts +import { App } from '@objectstack/spec/ui' + +export const CrmApp = App.create({ + name: 'crm_app', + label: 'CRM', + icon: 'briefcase', + branding: { primaryColor: '#2563EB' }, + navigation: [ + { id: 'group_sales', type: 'group', label: 'Sales', icon: 'briefcase', children: [ + { id: 'nav_leads', type: 'object', objectName: 'crm_lead', label: 'Leads', icon: 'funnel' }, + { id: 'nav_accounts', type: 'object', objectName: 'crm_account', label: 'Accounts', icon: 'building' }, + ]}, + ], + requiredPermissions: ['crm_access'], +}) +``` + +## 应用属性 + +| 属性 | 类型 | 必填 | 说明 | +|:--|:--|:--|:--| +| `name` | `string` | 是 | 机器名(`snake_case`) | +| `label` | `string` | 是 | 显示名 | +| `icon` | `string` | — | 应用图标(Lucide) | +| `description` / `version` | `string` | — | 用于列表展示的元数据 | +| `active` | `boolean` | — | 应用是否激活(默认 `true`) | +| `isDefault` | `boolean` | — | 是否为默认应用 | +| `navigation` | `NavigationItem[]` | — | 导航树 | +| `branding` | `AppBranding` | — | `primaryColor`、`logo`、`favicon` | +| `requiredPermissions` | `string[]` | — | 谁可以打开这个应用 | +| `homePageId` | `string` | — | 用作着陆页的导航项 `id` | +| `mobileNavigation` | `object` | — | 移动端专属导航 | + +## 导航项 + +导航树支持八种项目类型。常用的五种是 `object`、`dashboard`、`page`、`url` 和 `group`;规范还定义了 `report`、`action` 和 `component` 三种。 + +| `type` | 落到哪里 | 关键配置 | +|:--|:--|:--| +| `object` | 对象的列表视图 | `objectName`,外加可选的定位配置(见下) | +| `dashboard` | 一个仪表盘 | `dashboardName` | +| `page` | 一个自定义页面 | `pageName`、可选 `params` | +| `url` | 一个外部 URL | `url`、`target: '_blank'` | +| `group` | 可折叠分组 | `children`、`expanded` | + +每个导航项**必须**声明唯一的 `id`(小写 `snake_case`)—— `homePageId` 和 `mobileNavigation.bottomNavItems` 引用的就是它。公共属性:`label`、`icon`、`order`、`badge`,再加下面的门控三件套。 + +### 定位 `object` 入口 + +三个可选字段可以细化 `object` 入口落到哪里,优先级为 `recordId` → `filters` → `viewName`: + +- `viewName` —— 把入口锚定到某个具名列表视图。 +- `recordId` —— 深链到单条记录("我的资料");支持 `{current_user_id}` / `{current_org_id}` 模板变量。 +- `filters` —— 一次性的参数化切片:入口落到裸数据界面上,每个条件是一个可移除的 URL 筛选标签。适合"分派给我"这类链接,不必专门写视图。这不是安全特性 —— 显示什么仍由行级权限决定。 + +```ts +{ id: 'nav_my_open', type: 'object', label: 'My Open Deals', objectName: 'opportunity', + filters: { owner_id: '{current_user_id}', status: 'open' }, icon: 'user-check' } +``` + +### 移动端导航 + +```ts +mobileNavigation: { + mode: 'bottom_nav', // 'drawer'(默认)| 'bottom_nav' | 'hamburger' + bottomNavItems: ['nav_home', 'nav_accounts', 'nav_contacts'], // 导航项 id,最多 5 个 +} +``` + +## 按受众门控应用 + +同一份数据服务于两类截然不同的受众:设计 Schema 的**构建者**,和只录入、查看数据的**最终用户**。默认就把这两类界面分开 —— 别指望每位管理员手动把东西藏起来。 + +| 受众 | 界面 | 门控方式 | +|:--|:--|:--| +| 最终用户(消费者) | 精心组织的应用 → `page` / `view` 入口 | `App.requiredPermissions`、导航项门控 | +| 构建者 / 管理员 | Setup / Studio、原始对象表格 | 能力:`setup.access`、`studio.access`、`manage_metadata` | + +内置权限集已经编码了这种分割:`member_default` 和 `viewer_readonly` **不**携带 `studio.access` / `manage_metadata`,因此构建者界面对他们不可见;`admin_full_access` 和 `organization_admin` 则携带。 + +每个导航项支持三种相互独立的门控: + +| 门控 | 类型 | 隐藏该项,除非…… | +|:--|:--|:--| +| `requiredPermissions` | `string[]` | 用户持有该 RBAC 能力(如 `manage_metadata`) | +| `visible` | CEL 表达式 | 谓词求值为 true(如 `'org_admin' in current_user.positions`) | +| `requiresObject` / `requiresService` | `string` | 具名对象 / 内核服务已安装 | + +```ts +navigation: [ + { id: 'nav_contacts', type: 'object', label: 'Contacts', objectName: 'showcase_contact' }, + // 仅构建者可见的入口 —— 消费者永远不会渲染它: + { id: 'nav_designer', type: 'component', label: 'Object Designer', + componentRef: 'metadata:resource', params: { type: 'object' }, + requiredPermissions: ['manage_metadata'] }, +] +``` + +> **隐藏,而不是禁用。**禁用但可见的构建者入口仍是噪音。被门控的导航项对缺少该能力的用户*根本不渲染* —— 不留下让最终用户困惑的灰色摆设。 + +还有两个习惯能让消费者界面保持干净: + +- **给最终用户一个页面,而不是原始表格。**`page` 入口让你精确策划暴露的内容 —— 精选的列、固定的可视化、只有你启用的筛选和操作。`object` 入口给出的则是宽松的表格:可切换视图、个人视图、完整工具栏。 +- **也要策划数据界面本身。**优先用[精心组织的视图](/docs/build/interface/views),而不是放开原始对象读取,把用户扔到一个 40 列的表格上。 + +操作门控是双面的:UI 隐藏或禁用按钮,**同时**服务端拒绝调用 —— 不存在"只在 UI 门控、服务端敞开"的坑。见[操作](/docs/build/interface/actions)。 + +## Setup 应用 —— 一个内置示例 + +平台自身的管理 UI —— **Setup(管理后台)应用** —— 本身就是用同一套应用元数据协议渲染的:导航、页面与门控都声明为数据,由渲染你的应用的同一个渲染器绘制。它的门控方式正是你的构建者界面应有的方式:藏在消费者权限集不携带的 `setup.access` 能力后面。如果你想要"应用即元数据"可以规模化的证据 —— 你已经在用了。 + +## 反模式 + +- **把每个最终用户都当成构建者级协作者**,再一个个隐藏选项卡。应该让消费者 / 构建者分割成为默认。 +- **禁用而不是隐藏构建者入口。**可见但失效的摆设照样让人困惑。 +- **放开原始对象读取**,而其实一个精心组织的页面就能恰好暴露最终用户需要的内容。 + +## 下一步 + +| 页面 | 原因 | +|:--|:--| +| [视图](/docs/build/interface/views) | `object` 导航入口落到的地方 | +| [页面](/docs/build/interface/pages) | 面向最终用户的精心组织的 `page` 入口 | +| [仪表盘](/docs/build/interface/dashboards) | `dashboard` 入口渲染的内容 | +| [操作](/docs/build/interface/actions) | 按钮及其双面门控 | +| [权限](/docs/configure/permissions) | `requiredPermissions` 背后的权限集 | +| [界面总览](/docs/build/interface) | 各部分如何组合 | diff --git a/content/docs/build/interface/dashboards.zh-Hans.mdx b/content/docs/build/interface/dashboards.zh-Hans.mdx new file mode 100644 index 0000000..a72fa62 --- /dev/null +++ b/content/docs/build/interface/dashboards.zh-Hans.mdx @@ -0,0 +1,190 @@ +--- +title: 仪表盘 +description: 由绑定具名数据集的图表组件构成的分析页面 —— 带全局筛选、自动刷新,以及到底层记录的下钻。 +--- + +# 仪表盘 + +**仪表盘是一张组件网格;每个组件都绑定到一个数据集。**数据集是语义层:它拥有基础对象、连接、维度和经过认证的度量。组件按名称选用它们,因此"revenue"在每个用到它的仪表盘和报表上含义都相同。 + +```ts +const salesDashboard = { + name: 'sales_overview', + label: 'Sales Overview', + description: 'Key sales metrics and pipeline analysis', + refreshInterval: 300, // 每 5 分钟自动刷新 + + dateRange: { + field: 'close_date', + defaultRange: 'this_quarter', + allowCustomRange: true, + }, + + widgets: [ + { id: 'total_revenue', title: 'Total Revenue', type: 'metric', + dataset: 'sales', values: ['revenue'], + layout: { x: 0, y: 0, w: 3, h: 2 } }, + { id: 'revenue_by_region', title: 'Revenue by Region', type: 'bar', + dataset: 'sales', dimensions: ['region'], values: ['revenue'], + layout: { x: 3, y: 0, w: 6, h: 4 } }, + { id: 'deals_by_month', title: 'Deals by Month', type: 'pie', + dataset: 'sales', dimensions: ['close_month'], values: ['deal_count'], + layout: { x: 9, y: 0, w: 3, h: 4 } }, + ], +} +``` + +## 仪表盘属性 + +| 属性 | 类型 | 必填 | 说明 | +|:--|:--|:--|:--| +| `name` | `string` | 是 | 机器名(`snake_case`) | +| `label` | `string` | 是 | 显示标签 | +| `description` | `string` | — | 仪表盘描述 | +| `widgets` | `DashboardWidget[]` | 是 | 图表与指标组件 | +| `refreshInterval` | `number` | — | 自动刷新间隔(秒) | +| `dateRange` | `object` | — | 全局日期范围筛选 | +| `globalFilters` | `GlobalFilter[]` | — | 交互式筛选控件 | + +## 数据集优先 + +数据集定义一次;每个组件都绑定到它: + +```ts +import { defineDataset } from '@objectstack/spec/ui' + +export const salesDataset = defineDataset({ + name: 'sales', + label: 'Sales', + object: 'opportunity', + include: ['account'], + dimensions: [ + { name: 'region', field: 'account.region', type: 'string' }, + { name: 'close_month', field: 'close_date', type: 'date', dateGranularity: 'month' }, + ], + measures: [ + { name: 'revenue', field: 'amount', aggregate: 'sum', certified: true }, + { name: 'deal_count', aggregate: 'count' }, + ], +}) +``` + +聚合放在数据集的度量上(`aggregate`),而不是组件上:`count`、`sum`、`avg`、`min`、`max`、`count_distinct`、`array_agg`、`string_agg`。组件的 `dimensions` 和 `values` 必须引用其所绑定数据集上声明的名称。 + +运行时,数据集查询经由分析服务执行,并自动把调用者的行级安全范围应用到基础对象和被连接的对象上 —— 仪表盘绝不会向用户展示其[权限](/docs/configure/permissions)不允许看的记录。 + +> 旧的内联查询形态(在组件上直接写 `object` + `categoryField` + `valueField` + `aggregate`)已经移除。请定义数据集并把组件绑定上去。 + +## 组件 + +| 属性 | 类型 | 必填 | 说明 | +|:--|:--|:--|:--| +| `id` | `string` | 是 | 唯一的组件 id(`snake_case`) | +| `dataset` | `string` | 是 | 要绑定的数据集名 | +| `values` | `string[]` | 是 | 度量名(至少一个) | +| `dimensions` | `string[]` | — | 维度名 —— X 轴 / 分组 / 拆分 | +| `type` | `ChartType` | — | 可视化类型(默认 `metric`) | +| `title` / `description` | `string` | — | 显示标题与副标题 | +| `filter` | `FilterCondition` | — | 展示层筛选 | +| `layout` | `object` | — | 网格位置(省略时自动排布) | +| `colorVariant` | `enum` | — | KPI / 卡片强调色 | +| `compareTo` | `enum \| object` | — | 同比 / 环比对比窗口 | +| `filterBindings` | `object` | — | 组件级全局筛选映射(见下) | + +### 图表类型 + +| 类型 | 最适合 | +|:--|:--| +| `metric`*(默认)* | 单数字 KPI —— 营收、数量、百分比 | +| `bar` / `horizontal-bar` / `column` | 类别对比 | +| `line` | 随时间的趋势 | +| `pie` / `donut` | 分布 | +| `area` | 随时间的体量 | +| `scatter` | 相关性 | +| `radar` | 多维对比 | +| `funnel` | 转化阶段 | +| `gauge` / `solid-gauge` / `bullet` / `kpi` | 目标进度 | +| `treemap` / `sankey` | 层级占比 / 流向 | +| `table` | 明细记录 —— **可下钻** | +| `pivot` | 交叉汇总 —— **可下钻** | + +### 布局 + +组件排布在 12 列网格上: + +```ts +layout: { + x: 0, // 列位置(0-11) + y: 0, // 行位置 + w: 6, // 宽度,按列计(1-12) + h: 4, // 高度,按行计 +} +``` + +## 下钻 + +`table` 和 `pivot` 组件支持**下钻**:点击一行聚合数据或一个单元格,会打开一个侧边抽屉,列出该分组背后的*底层记录*。数据集保留了原始分组键,所以抽屉的筛选精确匹配那些记录 —— 不做标签到 id 的猜测。点击抽屉里的任意一行即可打开该记录的详情:完整的**分组 → 记录列表 → 单条记录**链路。 + +抽屉还提供一个 **"Open in list →"**(在列表中打开)逃生口,把速览升级为该对象的完整列表页(排序、批量选择、导出、可分享 URL),并沿用同一个下钻筛选。 + +下钻是自动的 —— 无需按组件配置 —— 只要数据集暴露了基础对象,且组件按至少一个维度分组。`metric` 与图表类组件只渲染聚合值;要展示明细,请改用 `table` 或 `pivot` 组件。 + +## 全局日期范围与筛选 + +全局时间筛选作用于所有组件: + +```ts +dateRange: { field: 'created_at', defaultRange: 'this_month', allowCustomRange: true } +``` + +预设范围:`today`、`yesterday`、`this_week`、`last_week`、`this_month`、`last_month`、`this_quarter`、`last_quarter`、`this_year`、`last_year`、`last_7_days`、`last_30_days`、`last_90_days`、`custom`。 + +交互式全局筛选的用法相同: + +```ts +globalFilters: [ + { name: 'region', field: 'region', label: 'Region', type: 'select' }, + { field: 'owner', label: 'Sales Rep', type: 'lookup' }, +] +``` + +每个筛选的 `name`(默认取 `field`)是它的稳定身份 —— 组件在 `filterBindings` 里引用的键,也是组件表达式中可通过 `page.` 读取的仪表盘级变量。名称 `dateRange` 保留给内置日期范围。 + +### 组件级筛选绑定 + +默认情况下,筛选按其自身的 `field` 作用于每个组件。当某个组件用不同的字段存储同一概念 —— 或应忽略某个筛选 —— 时,声明 `filterBindings`: + +```ts +widgets: [ + // 默认绑定:dateRange → created_at,region → region。 + { id: 'invoices_by_status', /* … */ }, + // 这个组件的字段不同 —— 逐个显式映射筛选。 + { id: 'accounts_signed', + filterBindings: { dateRange: 'signed_at', region: 'sales_region' }, /* … */ }, + // 用 `false` 退出某个筛选。 + { id: 'total_invoices', filterBindings: { region: false }, /* … */ }, +] +``` + +> 优先级:显式的 `filterBindings` 条目(字符串覆盖或 `false` 退出)→ 筛选的旧式 `targetWidgets` 白名单 → 筛选自身的 `field`。 + +## 让仪表盘出现在界面上 + +给[应用](/docs/build/interface/apps)添加一个 `dashboard` 导航入口: + +```ts +{ id: 'nav_analytics', type: 'dashboard', label: 'Analytics', + dashboardName: 'sales_overview', icon: 'bar-chart' } +``` + +或者描述给 [AI Builder](/docs/build/ai-builder) —— *"一个销售总览仪表盘,含按区域的营收和月度成交趋势"* —— 然后在批准前审阅生成的数据集 + 仪表盘元数据。 + +## 下一步 + +| 页面 | 原因 | +|:--|:--| +| [应用](/docs/build/interface/apps) | 把仪表盘放进导航 | +| [视图](/docs/build/interface/views) | 基于同一数据集的内联 `chart` 列表视图 | +| [页面](/docs/build/interface/pages) | 当组件网格不够用时的自由布局 | +| [数据模型](/docs/build/data) | 数据集聚合的对象 | +| [权限](/docs/configure/permissions) | 仪表盘继承的行级安全 | diff --git a/content/docs/build/interface/forms.zh-Hans.mdx b/content/docs/build/interface/forms.zh-Hans.mdx new file mode 100644 index 0000000..f36202b --- /dev/null +++ b/content/docs/build/interface/forms.zh-Hans.mdx @@ -0,0 +1,161 @@ +--- +title: 表单 +description: 从一份扁平字段集派生创建与编辑表单,无漂移地把字段分组成区块,并控制提交后的去向。 +--- + +# 表单 + +**表单是对象字段的一个投影 —— 不是第二份字段列表。**字段在对象上只声明一次;表单只决定*哪些字段、放在哪里*。数据语义(类型、验证、默认值、字段级安全)从不放在表单上,所以表单不可能漂移到对数据"说谎"。 + +表单视图的*类型*(`simple`、`tabbed`、`wizard`、`modal`……)与公开表单的 REST 契约在[视图](/docs/build/interface/views)中讲。本页讲布局问题:创建与编辑、分组、排序。 + +## 创建表单 ≠ 编辑表单 + +新建记录的表单应该只问几个要点;编辑表单则分区块展示完整记录。**别写两份表单。**创建表单的字段子集可以从每个字段上已有的意图*派生*出来: + +| 字段信号 | 对创建表单的影响 | +|:--|:--| +| `required: true` | **必须**出现在创建表单上 | +| `readonly` / 公式 / 汇总 / 自动编号 / 系统写入 | **绝不**出现在创建表单上 —— 你设置不了它 | +| 有 `defaultValue` | **可省略** —— 它会自动填充 | +| `hidden` | 默认处处不显示 | +| `group` | 字段属于哪个区块 | +| *声明顺序* | **就是**默认显示顺序 —— 没有 `field.order` | + +于是合理的创建表单 —— *可编辑、必填或核心的字段,按声明顺序* —— 零编写就从对象里长出来了。省略即正确:什么都不多写,你依然得到一份完整、正确的表单。完整的编辑表单同样是派生的,把每个 `field.group` 实体化为一个区块。 + +### 只在布局或流程真的不同时,才手工塑造创建表单 + +逃生舱是把一个具名表单视图绑定到创建入口。把它写成一份**稀疏覆盖** —— 基本上只是一串字段名: + +```ts +import { defineView } from '@objectstack/spec' + +const data = { provider: 'object' as const, object: 'showcase_contact' } + +export const ContactViews = defineView({ + list: { + type: 'grid', data, + columns: [{ field: 'name' }, { field: 'email' }, { field: 'company' }, { field: 'stage' }], + // 把 "+ Add record" 入口绑定到精简的创建表单: + addRecord: { enabled: true, mode: 'form', formView: 'create' }, + }, + + // 完整编辑表单 —— 按 field.group 分组;裸字符串继承字段定义。 + form: { + type: 'simple', data, + sections: [ + { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] }, + { name: 'work', label: 'Work', columns: 2, fields: ['company', 'title'] }, + { name: 'status', label: 'Status', columns: 2, fields: ['stage', 'lead_score'] }, + ], + }, + + formViews: { + // 稀疏的创建覆盖:只留核心字段,单个区块。 + create: { + type: 'simple', data, title: 'New contact', + sections: [ + { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] }, + ], + }, + }, +}) +``` + +绑定方式是 `addRecord.mode: 'form'` + `addRecord.formView: 'create'`。没有 `formViews.create` → 创建入口派生默认表单。有 → 创建时它生效。 + +| 你的真实需求 | 用 | +|:--|:--| +| 创建时少问几个字段(必填 + 少数核心) | **纯派生** —— 不要手写 | +| 创建时分组不同,但仍只是更小的子集 | 通常仍然派生 | +| 创建是向导 / 多步、有创建专属文案、条件展开 | **手写 `formViews.create`** | + +> 经验法则:*字段子集不同* → 派生。*布局或流程不同* → 覆盖。为了删几个字段就写一份完整的创建表单,等于直接走进"双份产物漂移"的陷阱:新增一个必填字段、忘了改创建表单,创建时就得到运行时的"缺少必填字段"错误。 + +因为创建表单的字段是编辑表单字段的子集,*顺序和分组也相同*,"快速创建 4 个字段 → 保存 → 落到完整记录页"在视觉上是连续的。派生免费保住了这一点。 + +## 字段分组与顺序 + +模型是一份扁平字段集。表格显示扁平的列。表单需要区块。这不矛盾 —— 是同一份扁平集合透过不同的镜头: + +| 镜头 | 字段形态 | 原因 | +|:--|:--|:--| +| 对象定义 | 扁平 | 模型描述*存在哪些数据* | +| 表格 / Grid | 扁平的列 | 表格是记录 × 字段的矩阵 | +| 表单 / 记录页 | 分组区块 | 人阅读单条记录时需要分块 | + +分组概念有**两个**。保持区分: + +**1. 语义分组 —— `field.group`(在对象上)。**字段的逻辑归属。它随模型走,并为自动生成表单的*默认*分区提供种子: + +```ts +fields: { + name: Field.text({ label: 'Full name', group: 'contact' }), + email: Field.email({ label: 'Email', group: 'contact' }), + stage: Field.select({ label: 'Stage', group: 'status', options: [/* … */] }), +} +``` + +**2. 布局分组 —— 表单 `sections`(在视图上)。**某个具体表单的显式编排:哪些字段、哪个区块、几个 `columns`、可否折叠。它以 `field.group` 为默认继承来源,需要时按表单覆盖。 + +```ts +form: { + type: 'simple', + sections: [ + { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] }, + { name: 'status', label: 'Status', columns: 2, fields: ['stage'] }, + ], +} +``` + +**顺序就是你书写的顺序。**没有 `field.order` —— 对象上字段的声明顺序就是处处的默认显示顺序,而 `sections`(以及其中的字段)是有序列表。 + +> **"group" 陷阱。**表格视图的 `groupByField` 按字段的*值*给**记录(行)**分组 —— 所有 `stage = qualified` 的行归到一起。表单区块在视觉上给**字段(列)**分组。同一个词,两条互不相关的轴。表格分组永远不会给你的表单分区。 + +## 提交之后会发生什么 + +给表单视图加 `submitBehavior` 来控制提交后的体验: + +```ts +submitBehavior: { + kind: 'thank-you', + title: 'Thanks!', + message: 'A specialist will reach out within 24 hours.', +} +``` + +| `kind` | 行为 | +|:--|:--| +| `thank-you`*(默认)* | 用一块确认面板替换表单(`title`、`message`) | +| `redirect` | 在 `delayMs`(默认 0)之后跳转到 `url` —— 适合营销页 | +| `continue` | 重置表单以填写下一份 —— 自助终端、批量录入 | +| `next-record` | 前进到队列中的下一条记录(内部模式) | + +公开表单和内部表单都支持 `?prefill_=` URL 参数 —— 从邮件链接或活动页预填表单。预填是体验捷径,不是权限绕过:值仍要经过验证,公开表单还要过服务端字段白名单。 + +``` +/console/f/contact-us?prefill_company=Acme&prefill_email=ada@example.com +/console/forms/quick_create?prefill_lead_source=event_booth_2026 +``` + +## 记录详情由角色驱动,而非表单绑定 + +没有哪个对象级键能把表单视图钉到记录详情屏上。详情渲染由对象的跨界面**语义角色**派生:`nameField`(显示名)、`highlightFields`(最重要字段组成的条带)、`stageField`(生命周期进度条)、以及 `fieldGroups` + `Field.group`(表单、弹窗与详情页共享的分区)。当记录页需要的定制布局超出这些角色所能派生的范围时,给对象指定一个自定义[页面](/docs/build/interface/pages)。 + +## 反模式 + +- **两份完整手写的字段列表**(`contact_create_form` + `contact_edit_form`)。必然漂移。 +- **在表单上复述字段类型 / 验证 / 选项。**数据语义只属于对象。 +- **在每个表单里重新敲一遍分组。**`field.group` 声明一次;只在真正分歧时覆盖。 +- **为了迁就表单给数据模型加结构性嵌套。**模型保持扁平;分组交给表单。 + +## 下一步 + +| 页面 | 原因 | +|:--|:--| +| [视图](/docs/build/interface/views) | 表单视图类型、公开表单分享、完整的视图 Schema | +| [数据模型](/docs/build/data) | 字段 —— 以及它们的意图 —— 声明的地方 | +| [操作](/docs/build/interface/actions) | 启动表单视图的 `type: 'form'` 操作 | +| [页面](/docs/build/interface/pages) | 超越派生布局的自定义记录页 | +| [权限](/docs/configure/permissions) | 表单遵守的字段级安全 | diff --git a/content/docs/build/interface/index.zh-Hans.mdx b/content/docs/build/interface/index.zh-Hans.mdx new file mode 100644 index 0000000..f1dd9ee --- /dev/null +++ b/content/docs/build/interface/index.zh-Hans.mdx @@ -0,0 +1,45 @@ +--- +title: 界面 +description: 应用、视图、表单、仪表盘、页面与操作 —— 每个面向用户的界面都声明为元数据,由 Console 渲染。 +--- + +# 界面 + +**用户看到的一切都是元数据。**你把界面声明为数据 —— 与对象同一套生命周期:可以版本化、打进包里发布、让 AI Builder 生成,任何符合协议的渲染器都能把它画出来。 + +各个部分自上而下组合: + +1. **应用**是外壳 —— 导航、品牌,以及受众门槛。 +2. 它的**导航**指向对象、仪表盘、页面和 URL。 +3. 对象入口落到一个**视图**上 —— Grid(表格)、Kanban(看板)、Calendar(日历)等等。 +4. 打开或创建记录时渲染**表单** —— 区块由字段元数据派生,可按模式覆盖。 +5. **仪表盘**把同样的数据聚合成绑定到具名数据集的图表组件。 +6. **页面**完全跳出对象框架 —— 由组件和区域组合的自由布局。 +7. **操作**给上述一切装上按钮 —— 一次声明,可从 Console、REST、流程和 AI Agents 调用。 + +## 核心概念 + +| 概念 | 声明什么 | 页面 | +|:--|:--|:--| +| 应用 | 导航外壳、品牌、受众门控 | [应用](/docs/build/interface/apps) | +| 视图 | 记录如何渲染:Grid、Kanban、Calendar、Gantt…… | [视图](/docs/build/interface/views) | +| 表单 | 创建 / 编辑布局、区块、字段分组 | [表单](/docs/build/interface/forms) | +| 仪表盘 | 图表组件、全局筛选、下钻 | [仪表盘](/docs/build/interface/dashboards) | +| 页面 | 自由组件布局;随包发布的文档页 | [页面](/docs/build/interface/pages) | +| 操作 | 处处以按钮形式呈现的具名操作 | [操作](/docs/build/interface/actions) | + +因为 UI 是数据,它遵守与其所展示的记录相同的[权限](/docs/configure/permissions);而 [AI Builder](/docs/build/ai-builder) 可以通过同一个类型化接口生成或修改其中任何部分 —— 描述你想要的界面,审阅 diff,批准。 + +> 从数据开始。视图和表单会从你的[对象定义](/docs/build/data)派生出合理的默认值 —— 只有当默认不是你想要的时,才需要写界面元数据。 + +## 下一步 + +| 页面 | 原因 | +|:--|:--| +| [应用](/docs/build/interface/apps) | 把对象包进可导航的外壳 | +| [视图](/docs/build/interface/views) | 选择记录的渲染方式 | +| [表单](/docs/build/interface/forms) | 塑造创建与编辑体验 | +| [仪表盘](/docs/build/interface/dashboards) | 添加分析组件 | +| [页面](/docs/build/interface/pages) | 组合自由布局 | +| [操作](/docs/build/interface/actions) | 把操作放到按钮后面 | +| [数据模型](/docs/build/data) | 每个界面读取的对象 | diff --git a/content/docs/build/interface/pages.zh-Hans.mdx b/content/docs/build/interface/pages.zh-Hans.mdx new file mode 100644 index 0000000..af51ab1 --- /dev/null +++ b/content/docs/build/interface/pages.zh-Hans.mdx @@ -0,0 +1,156 @@ +--- +title: 页面 +description: 由区域、组件与本地状态组合的自由布局 —— 外加随包发布的 Markdown 文档页。 +--- + +# 页面 + +**页面是一个自由容器。**不同于绑定到单个对象的[视图](/docs/build/interface/views),页面组合多个组件、嵌入视图并管理本地状态 —— 你的主屏、自定义记录布局和工具面板都靠它。 + +```ts +const homePage = { + name: 'sales_home', + label: 'Sales Home', + type: 'home', + regions: [ + { name: 'header', width: 'full', components: [ + { type: 'metric_card', id: 'total_revenue', label: 'Total Revenue', + properties: { object: 'opportunity', field: 'amount', aggregate: 'sum', format: 'currency' } }, + ]}, + { name: 'main', width: 'large', components: [ + { type: 'list_view', id: 'recent_deals', label: 'Recent Deals', + properties: { object: 'opportunity', view: 'recent_open', limit: 10 } }, + ]}, + ], +} +``` + +## 页面属性 + +| 属性 | 类型 | 必填 | 说明 | +|:--|:--|:--|:--| +| `name` | `string` | 是 | 机器名(`snake_case`) | +| `label` | `string` | 是 | 显示标签 | +| `type` | `enum` | — | 页面类型(默认 `'record'`,见下) | +| `object` | `string` | — | 关联对象(`record` 类型用) | +| `template` | `string` | — | 布局模板名(默认 `'default'`) | +| `regions` | `PageRegion[]` | — | 承载组件的布局区域 | +| `variables` | `PageVariable[]` | — | 本地状态变量 | +| `isDefault` | `boolean` | — | 是否为该类型的默认页面 | +| `assignedProfiles` | `string[]` | — | 可访问此页面的简档 | + +### 页面类型 + +| 类型 | 用于 | +|:--|:--| +| `record` | 绑定到某个对象记录的自定义详情页 | +| `home` | 应用着陆 / 入口 | +| `app` | 通用应用布局 | +| `utility` | 工具、设置、向导 | +| `list` | 数据驱动的界面 | + +只有这五种类型有效 —— 早期路线图上那些从未交付渲染器的类型已从 Schema 中移除。 + +## 区域 + +区域是布局分区;每个区域承载组件: + +```ts +regions: [ + { name: 'sidebar', width: 'small', components: [/* … */] }, + { name: 'content', width: 'large', components: [/* … */] }, +] +``` + +`width` 接受 `'small'`、`'medium'`、`'large'` 或 `'full'`。 + +## 组件 + +组件是区域内部的积木: + +```ts +{ + type: 'chart', + id: 'revenue_chart', + label: 'Revenue Trend', + properties: { chartType: 'line', object: 'opportunity', + categoryField: 'close_date', valueField: 'amount' }, + events: { onClick: "navigate_to('opportunity_detail', { id: $event.id })" }, + visibility: "os.user.profile == 'sales_manager'", +} +``` + +| 属性 | 类型 | 说明 | +|:--|:--|:--| +| `type` | `string` | 标准组件类型,或自定义字符串 | +| `id` | `string` | 唯一的组件实例 id | +| `properties` | `object` | 组件专属配置 | +| `events` | `object` | 事件处理器(操作表达式) | +| `visibility` | `string` | CEL 可见性谓词 | +| `style` / `className` | — | 临时 CSS | +| `responsiveStyles` | `object` | **首选**样式通道:桌面优先、按断点的样式映射(`large` 为基础,再叠加 `medium` / `small` / `xsmall` 覆盖),编译为作用域 CSS —— 优先用 `var(--space-8)` 这样的设计令牌 | + +标准的、带命名空间的组件类型: + +| 命名空间 | 类型 | +|:--|:--| +| 结构 | `page:header`、`page:footer`、`page:sidebar`、`page:tabs`、`page:accordion`、`page:card`、`page:section` | +| 记录上下文 | `record:details`、`record:highlights`、`record:related_list`、`record:activity`、`record:chatter`、`record:path`、`record:alert`、`record:quick_actions`、`record:reference_rail`、`record:history` | +| 导航 | `app:launcher`、`nav:menu`、`nav:breadcrumb` | +| 工具 | `global:search`、`global:notifications`、`user:profile` | +| AI | `ai:chat_window`、`ai:suggestion` | +| 元素 | `element:text`、`element:number`、`element:image`、`element:divider`、`element:button`、`element:filter`、`element:form`、`element:record_picker`、`element:text_input` | + +组件还可以携带 `dataSource`(多对象页面中按元素绑定对象)、`responsive` 与 `aria` 配置。项目专属的组件可以使用自定义字符串类型。 + +## 变量 + +页面持有跨组件共享的本地状态: + +```ts +variables: [ + { name: 'selected_tab', type: 'string', defaultValue: 'overview' }, + { name: 'date_range', type: 'object', defaultValue: { start: null, end: null } }, +] +``` + +`type` 接受 `'string'`(默认)、`'number'`、`'boolean'`、`'object'`、`'array'` 或 `'record_id'`。 + +> `record` 类型的页面是定制记录布局的受支持路径 —— 当由角色派生的详情页(见[表单](/docs/build/interface/forms))不够用时就用它。把 `record:highlights` 组合进全宽头部,把 `record:details` 和 `record:activity` 放进侧栏,把 `record:related_list` 组件放进主区域。 + +通过 `page` 导航入口把页面挂到[应用](/docs/build/interface/apps)上,或者用 `homePageId` 把它设为应用的着陆页。 + +## 文档页 + +**doc** 是一页包文档:放在扁平的 `src/docs/` 目录里的纯 Markdown 文件,编译进包产物,在 Console 中渲染于 `/docs/`。文档还会在 AI 助手回答关于你的包的问题时充当依据。 + +``` +src/docs/ + crm_index.md → 文档名 "crm_index" + crm_user_guide.md → 文档名 "crm_user_guide" +``` + +文件名主干就是文档 `name` —— 没有目录分级,没有排序文件。扁平布局让交叉引用保持稳定:链接按 basename 解析,从不按路径。 + +| 规则 | 细节 | +|:--|:--| +| 命名 | `snake_case`,且构建 lint 要求**命名空间前缀**(`crm_user_guide`,而非 `user_guide`) | +| Frontmatter | 可选;读取 `title` 和 `description`。标题解析顺序:frontmatter → 第一个 `#` 标题 → 文档名 | +| 交叉引用 | 普通相对链接(`[overview](./crm_index.md)`)—— 在 Console 里重写为 `/docs/`,在 GitHub 上原生可用。**同包内的坏链会导致构建失败** | +| Markdown | CommonMark + GFM:表格、带高亮的围栏代码、标题锚点、GitHub alerts(`> [!TIP]`) | +| 构建时拒绝 | **MDX / 内嵌组件**(文档是数据,不是代码 —— 这是信任边界)以及**图片引用**(尚无资产服务;快速失败胜过碎图) | + +文档解析按包隔离:两个已安装的包可以各自发布同名裸文档并共存 —— 谁也不会覆盖谁。 + +> 需要动态内容 —— 实时流程图、记录表格 —— 时别试图内嵌组件。用 URL 链接到元数据;平台渲染实时视图,文档只负责指向它。 + +## 下一步 + +| 页面 | 原因 | +|:--|:--| +| [应用](/docs/build/interface/apps) | 把页面放进导航、设置 `homePageId` | +| [视图](/docs/build/interface/views) | 页面所嵌入的对象界面 | +| [表单](/docs/build/interface/forms) | 角色派生的记录布局 vs 自定义记录页 | +| [仪表盘](/docs/build/interface/dashboards) | 以分析为中心的布局,而非自由布局 | +| [操作](/docs/build/interface/actions) | `element:button` 的目标与 `modal` 类型操作 | +| [AI Builder](/docs/build/ai-builder) | 用对话生成页面元数据 | diff --git a/content/docs/build/interface/views.zh-Hans.mdx b/content/docs/build/interface/views.zh-Hans.mdx index 64a0327..d46e085 100644 --- a/content/docs/build/interface/views.zh-Hans.mdx +++ b/content/docs/build/interface/views.zh-Hans.mdx @@ -1,9 +1,9 @@ --- -title: Views +title: 视图 description: List、Form、Kanban、Calendar、Gantt 等等 —— Console 中每个对象表面是如何声明的。 --- -# Views +# 视图 **view** 是用户在 Console 中查看和编辑记录的方式。视图是声明式元数据 —— 生命周期与对象相同:声明一次,随你的 package 发布,在任何地方渲染。 diff --git a/content/docs/build/marketplace.zh-Hans.mdx b/content/docs/build/marketplace.zh-Hans.mdx index 56d8a6b..0b3670f 100644 --- a/content/docs/build/marketplace.zh-Hans.mdx +++ b/content/docs/build/marketplace.zh-Hans.mdx @@ -9,7 +9,30 @@ ObjectOS marketplace 让你能够将预构建的应用安装到运行中的运 ## 工作原理 -每个 ObjectOS 运行时默认都启用了 `MarketplaceProxy` 和 `MarketplaceInstallLocal` 插件。当你打开 Console(`/_console/`)时,marketplace 标签页会查询已配置的应用目录,并展示可安装的应用。 +marketplace 客户端是开源的 `@objectstack/cloud-connection` 包。云托管环境和自托管的 ObjectOS 镜像开箱即已接好;如果是你自己组装的技术栈,用几行配置即可启用: + +```ts +import { + MarketplaceProxyPlugin, + MarketplaceInstallLocalPlugin, + RuntimeConfigPlugin, + resolveCloudUrl, +} from '@objectstack/cloud-connection'; + +const catalogUrl = resolveCloudUrl(); // OS_CLOUD_URL; 'off' disables + +plugins: [ + ...(catalogUrl ? [ + new MarketplaceProxyPlugin({ controlPlaneUrl: catalogUrl }), + new MarketplaceInstallLocalPlugin({ controlPlaneUrl: catalogUrl }), + ] : []), + new RuntimeConfigPlugin({ controlPlaneUrl: '', singleEnvironment: true, installLocal: true }), +] +``` + +浏览 / 安装的*机制*是开放的;目录*服务*(组织目录、审核、付费分发)由 `OS_CLOUD_URL` 所指向的控制平面提供。应用一经安装就存在于你运行时自己的内核中 —— 运行时的任何部分都不依赖目录保持可达。 + +当你打开 Console(`/_console/`)时,marketplace 标签页会查询已配置的应用目录,并展示可安装的应用。 ```text You ─→ Console ─→ Marketplace tab ─→ pick app ─→ Install @@ -82,6 +105,22 @@ os package publish # publish to a catalog 每个已发布的应用都是不可变的。更新会产生一个新版本。运行时会按应用跟踪已安装的版本以及可用的更新。当某个应用在用户所跟踪的目录中发布了新版本时,用户会在 Console 中看到一个 "Update available" 徽标。 +## 通过 CLI 安装 + +Console 是主要的安装入口,但同一个 install-local 端点也可以脚本化,用于 CI 和隔离网络环境下的运维: + +```bash +# 目录模式 —— 运行时从其配置的目录解析包 +os package install com.acme.crm --runtime http://localhost:3000 \ + --email admin@example.com --password … + +# 隔离网络模式 —— 内联发送编译好的制品,不经过目录往返 +os package install ./dist/objectstack.json --runtime http://localhost:3000 \ + --email admin@example.com --password … +``` + +凭证是**目标运行时**上的账户(不是你的云端登录)—— 与 Console 安装使用同一套授权。 + ## 权限 安装应用需要 `manage_marketplace` 系统权限——默认情况下只有 **Setup Administrator** 权限集的成员拥有该权限。普通用户看到的 marketplace 是只读的。 diff --git a/content/docs/configure/index.zh-Hans.mdx b/content/docs/configure/index.zh-Hans.mdx new file mode 100644 index 0000000..d5f44e7 --- /dev/null +++ b/content/docs/configure/index.zh-Hans.mdx @@ -0,0 +1,59 @@ +--- +title: 管理 +description: 系统管理员在哪里管理用户、访问、设置与集成 —— 以及哪个页面解决哪类任务。 +--- + +# 管理 + +本章面向 ObjectOS 部署的**系统管理员**:负责用户入职、授予访问权限、接通登录、邮件、存储与集成,并保持系统健康运转的人。你日常管理的是人和他们的权限;应用、对象和权限集本身则随平台及你安装的应用包一起交付。 + +> **权限在 Studio 中设计,在 Setup 中分配。**绝大多数管理工作都不会离开 Setup(管理后台)——只有编写权限集或运行解释引擎时才需要进入 Studio。 + +## Setup 控制台 + +内置的管理控制台位于 **`/apps/setup`**,需要 `setup.access` 权限。其左侧导航由运行时已加载的能力插件动态提供,因此你看到的菜单精确反映当前部署所运行的内容——未启用的能力不贡献任何菜单项,其分组保持为空。 + +稳定的导航分组: + +| 分组 | 里面有什么 | +|---|---| +| **Overview**(概览) | System Overview 系统概览仪表盘 | +| **People & Organization**(人员与组织) | 用户、业务单元、团队、组织、邀请 | +| **Access Control**(访问控制) | 岗位、权限集、共享规则、记录共享、API Key | +| **Approvals**(审批) | 审批流程(加载 approvals 插件时) | +| **Configuration**(配置) | 全部设置、品牌、认证、邮件、文件存储、AI 与 Embedder、知识库、功能开关 | +| **Diagnostics**(诊断) | 会话、通知事件、审计日志 | +| **Integrations**(集成) | Webhooks | +| **Advanced**(高级) | OAuth 应用、签名密钥(JWKS)、身份关联、用户偏好 | + +## 任务地图 + +| 我需要 …… | 阅读 | +|---|---| +| 添加用户、搭建组织树、管理团队 | [用户与组织](/docs/configure/users) | +| 为某人办理入职、离职或调整访问权限 | [权限分配](/docs/configure/permissions/managing-access) | +| 理解整套访问模型 | [权限](/docs/configure/permissions) | +| 配置登录、OAuth、SSO、双因素认证 | [认证](/docs/configure/authentication) | +| 修改运行时和租户设置 | [系统设置](/docs/configure/system-settings) | +| 配置事务性邮件发送 | [邮件](/docs/configure/email) | +| 配置文件存储(S3、本地磁盘) | [存储](/docs/configure/storage) | +| 连接外部业务数据库 | [数据源](/docs/configure/data-sources) | +| 使用 REST API 和 API Key | [API 访问](/docs/configure/api-access) | +| 发送出站 Webhook | [Webhooks](/docs/configure/webhooks) | +| 配置 AI Provider、Embedder、RAG | [AI 服务](/docs/configure/ai) | +| 接入 Claude 或其他 MCP 客户端 | [接入 AI 工具(MCP)](/docs/configure/mcp) | +| 配置制品加载、数据库、缓存 | [运行时配置](/docs/configure/runtime) | +| 规划备份与灾难恢复 | [备份](/docs/operate/backup) | +| 安全地升级或回滚 | [升级](/docs/operate/upgrade) | +| 为生产环境加固 | [生产就绪](/docs/operate/production) | +| 查看日志、指标和审计记录 | [可观测性](/docs/operate/observability) | +| 诊断故障部署 | [故障排查](/docs/operate/troubleshooting) | + +## 下一步 + +| 任务 | 页面 | +|---|---| +| 你的第一项管理任务:添加人员并搭建结构 | [用户与组织](/docs/configure/users) | +| 给新员工开通访问权限 | [权限分配](/docs/configure/permissions/managing-access) | +| 学习分层访问模型 | [权限](/docs/configure/permissions) | +| 上线前检查清单 | [生产就绪](/docs/operate/production) | diff --git a/content/docs/configure/mcp.zh-Hans.mdx b/content/docs/configure/mcp.zh-Hans.mdx new file mode 100644 index 0000000..217f15b --- /dev/null +++ b/content/docs/configure/mcp.zh-Hans.mdx @@ -0,0 +1,129 @@ +--- +title: 接入 AI 工具(MCP) +description: 把 Claude Code、Claude Desktop 或任意 MCP 客户端指向你的 ObjectOS 应用,让 Agent 在你的权限模型约束下处理你的数据。 +--- + +# 接入 AI 工具(MCP) + +每个 ObjectOS 部署天生就是一个 MCP 服务器。运行时在 **`/api/v1/mcp`** 上提供 [Model Context Protocol](https://modelcontextprotocol.io) 服务——默认开启,无需安装插件,无需配置步骤。你的对象和已暴露的操作在定义的那一刻就成为带类型的工具;剩下唯一要做的就是接入一个客户端并验证它能用。 + +> 要关闭这个入口,设置 `OS_MCP_SERVER_ENABLED=false`——端点将返回 404,**Setup → Connect an Agent**(接入 Agent)页面也随之消失。 + +本页讲的是把*外部* AI 工具接入你的应用。服务端 AI 栈——聊天 Provider、Embedder、RAG,以及在代码中显式注册 MCP 服务器插件——参见 [AI 服务](/docs/configure/ai)。 + +## Claude Code(一条命令) + +交互式客户端使用 OAuth——每个部署本身就是一个 OAuth 2.1 授权服务器,因此不存在需要管理员签发再分发的凭据。第一次工具调用会打开浏览器登录,你**以自己的身份**接入: + +```bash +# 本地开发服务器 +claude mcp add --transport http my-app http://localhost:3000/api/v1/mcp + +# 已部署的实例 +claude mcp add --transport http my-app https://your-deployment.example.com/api/v1/mcp +``` + +无头场景(CI、容器)跳过 OAuth,改为附加 [API Key](#headless-api-keys): + +```bash +claude mcp add --transport http my-app https://your-deployment.example.com/api/v1/mcp \ + --header "x-api-key: osk_..." +``` + +## Claude Desktop 与 claude.ai + +**Settings → Connectors → Add custom connector**(设置 → 连接器 → 添加自定义连接器),然后粘贴 MCP URL(`https://your-deployment.example.com/api/v1/mcp`)。首次使用时会走同样的浏览器登录流程。 + +## 任意 MCP 客户端(`.mcp.json`) + +读取 `mcpServers` 映射的客户端以同样方式接入。使用 API Key: + +```json +{ + "mcpServers": { + "objectstack": { + "type": "http", + "url": "https://your-deployment.example.com/api/v1/mcp", + "headers": { "x-api-key": "osk_..." } + } + } +} +``` + +## 无头场景:API Key + +在 **Setup → Connect an Agent**(接入 Agent,那里还提供各客户端可直接复制粘贴的接入片段)中签发 Key,或通过 REST: + +```bash +curl -b cookies.txt -X POST https://your-deployment.example.com/api/v1/keys +# → { "key": "osk_..." } —— 只显示一次;存入你的 secret manager +``` + +每次请求以三种等价形式之一携带它: + +| 请求头 | 示例 | +|---|---| +| `x-api-key` | `x-api-key: osk_...` | +| `Authorization: ApiKey` | `Authorization: ApiKey osk_...` | +| `Authorization: Bearer` | `Authorization: Bearer osk_...`(通过 `osk_` 前缀识别) | + +> OAuth 要求 TLS——纯 HTTP 部署(`localhost` 除外)会回退到**仅 API Key** 模式:浏览器登录通道被禁用,而不是被允许以不安全的方式运行。 + +对于长期集成,把 Key 绑定到带最小权限集的专用服务用户——参见[服务账号与 API Key](/docs/configure/users#service-accounts--api-keys)。 + +## Agent 能得到什么 + +十个数据与操作工具,由你的元数据生成: + +| 工具 | 用途 | +|---|---| +| `list_objects` / `describe_object` | 发现有哪些对象及其字段 | +| `query_records` / `get_record` | 读取数据(列表查询默认每页上限 50 行) | +| `aggregate_records` | 分组聚合(当前驱动支持时才注册) | +| `create_record` / `update_record` / `delete_record` | 写入数据 | +| `list_actions` / `run_action` | 按名称发现并调用你的业务操作 | + +要了解的两条暴露规则: + +- **对象自动暴露**——但 `sys_*` 系统对象除外,它们以 fail-closed 方式被拦截。 +- **操作需要作者显式选择加入**:`ai: { exposed: true }` 加上不少于 40 个字符的 `ai.description`,且该操作必须可以在无 UI 的情况下调用(带 body 或已注册 handler 的 `script`,或 `flow`)。 + +## 权限强制执行 + +- **每次调用都以调用者身份运行。**MCP 桥接解析的执行上下文与 REST 请求相同,因此对象权限、[记录访问](/docs/configure/permissions/record-access)和[字段级安全](/docs/configure/permissions/field-level-security)对 Agent 的作用与对 UI 中的真人完全一致。结果稀疏或写入被拒,通常意味着治理在*正常工作*,而不是连接坏了。 +- **OAuth scope 会收窄工具集。**Token 携带 `data:read`、`data:write` 和 `actions:execute` 这些 scope——不在已授予 scope 内的工具,在该会话中根本不会被注册。API Key 和会话调用者获得完整工具集,但每次调用仍会做权限检查。 +- **操作体一经调用即作为可信应用代码运行**(`ai.exposed` 门槛和 `requiredPermissions` 在*调用*时检查)。把编写操作当作值得代码评审的行为——那才是真正的安全边界。 +- 操作还可以声明 `ai.requiresConfirmation`;看起来具有破坏性的操作默认要求确认。 + +## 验证连接 + +问 Agent 一个只有实时 schema 才能回答的问题: + +```text +What objects does this app have, and what fields does the main one carry? +``` + +你应该看到 `list_objects` 和 `describe_object` 被触发。Agent 的自然工作模式是 `list_objects` → `describe_object` → `query_records` → `run_action`——四者都能跑通,连接就完全就绪了。 + +> 配上应用的 **skill 文件**,Agent 的表现会明显更好:从 `GET /api/v1/mcp/skill` 下载它,或安装[官方 Claude 插件](https://github.com/objectstack-ai/claude-plugin)(`claude plugin marketplace add objectstack-ai/claude-plugin`),后者打包了该 skill 和一个引导式的 `/objectstack:connect` 命令。 + +## 故障排查 + +| 症状 | 原因 → 解决 | +|---|---| +| `/api/v1/mcp` 返回 `404` | 入口被禁用——取消设置 `OS_MCP_SERVER_ENABLED`(默认开启) | +| `501 Not Implemented` | 此构建不包含 MCP 插件——检查你的栈的插件配置 | +| 每次调用都 `401` | 匿名或凭据无效。交互式客户端:完成浏览器登录。无头场景:检查 `osk_` Key 和请求头拼写 | +| `403 insufficient_scope` | OAuth token 缺少该工具族所需的 scope(例如没有 `data:write` 却尝试写入)——重新连接并授予该 scope | +| 某个操作没出现在 `list_actions` 中 | `ai.exposed` 不为 `true`、`ai.description` 短于 40 个字符、类型不可无头调用(`url` / `modal` / `form` 永远不会出现)、目标是 `sys_*` 对象,或调用者未通过其 `requiredPermissions` | +| 读取返回的行很少 / 写入被拒 | 符合设计——调用者的权限和记录访问在生效。用同一用户在 UI 中验证 | + +## 下一步 + +| 任务 | 页面 | +|---|---| +| 配置 AI Provider、Embedder 和 MCP 服务器插件 | [AI 服务](/docs/configure/ai) | +| 创建服务用户和 API Key | [用户与组织](/docs/configure/users) | +| 理解 Agent 被允许看到什么 | [权限](/docs/configure/permissions) | +| REST API 与 Key 管理 | [API 访问](/docs/configure/api-access) | +| 验证某个用户的访问权限 | [权限分配](/docs/configure/permissions/managing-access) | diff --git a/content/docs/configure/permissions/field-level-security.zh-Hans.mdx b/content/docs/configure/permissions/field-level-security.zh-Hans.mdx new file mode 100644 index 0000000..4ca3749 --- /dev/null +++ b/content/docs/configure/permissions/field-level-security.zh-Hans.mdx @@ -0,0 +1,110 @@ +--- +title: 字段级安全 +description: 隐藏或锁定单个字段 —— 授予语义、服务端强制执行,以及 FLS 在表单、视图和 API 中的行为。 +--- + +# 字段级安全 + +字段级安全(FLS)控制单个字段的可见性与可编辑性,作用于对象权限和[记录访问](/docs/configure/permissions/record-access)已经允许用户触达该记录*之后*。它是实现"支持人员能看到客户,但看不到其 `annual_revenue`"和"销售代表能读外部 id 但永远不能改"的那一层。 + +FLS 规则存在[权限集](/docs/configure/permissions/permission-sets)中——本页比那里的[字段安全附录](/docs/configure/permissions/permission-sets#field-security-appendix)更深入地讲解授予语义与强制执行。 + +## 字段权限授予 + +字段权限以 `.` 为键,使用 `readable` / `editable`: + +```ts +fields: { + // 只读:可见但不可编辑 + 'account.annual_revenue': { readable: true, editable: false }, + 'account.description': { readable: true, editable: true }, + // 隐藏:完全不可见 + 'account.ssn': { readable: false, editable: false }, + 'opportunity.amount': { readable: true, editable: true }, + 'opportunity.probability': { readable: true, editable: false }, +} +``` + +两个标志产生三种状态: + +| 状态 | 规则 | 效果 | +|---|---|---| +| **隐藏** | `{ readable: false, editable: false }` | 字段完全不可见——从每个响应中剥离 | +| **只读** | `{ readable: true, editable: false }` | 字段会返回,但对它的写入会被拒绝 | +| **可编辑** | `{ readable: true, editable: true }` | 字段可见且可写 | + +> 字段权限键务必写成**带对象限定**的形式(`crm_lead.budget`,而不是 `budget`)——自 ObjectStack 14.4 起,`security-fls-unqualified-key` lint 会在编译时拒绝裸键,因为它们会静默地匹配不到任何东西。 + +## 授予如何合并 + +FLS 使用**默认可见(黑名单)语义**:没有显式规则的字段原样通过——既可读*又*可写。权限集只约束它显式列出的字段。 + +字段授予在用户的多个权限集之间按**最宽松**方式取并集:一个权限集的 `readable: true` 会压过另一个权限集的 `false`。在减法式屏蔽层落地之前(已保留为 ADR-0066 ⑧),`{ readable: false }` 规则只有在**用户持有的其他任何权限集**都没有声明该字段 `readable: true` 时才会遮蔽它。实际影响: + +- 保护敏感字段的方式是**只**在需要它们的权限集中授予——绝不要指望某个权限集里的 `false` 规则去覆盖别处的 `true`。 +- 把出现在广泛授予的对象上的敏感字段视为评审警讯。 + +已声明的规则本身以 fail-closed 方式强制执行:被遮蔽的字段在读取时被剥离,对不可编辑字段的写入会抛错。 + +## API 中的强制执行 + +SecurityPlugin 中间件在服务端强制执行字段规则,与请求来路无关——REST、ObjectQL 或任何其他路径。不存在通过更底层 API 的后门。 + +**读取时**——`find` / `findOne` 的结果在响应离开引擎之前,会从每条记录中剥离不可读字段。 + +**写入时**——`insert` / `update` 请求在操作到达驱动**之前**被检查。如果请求体包含任何调用者无权编辑的字段,引擎抛出 `PermissionDeniedError`(HTTP 403),并附上违规字段名: + +```json +{ + "error": { + "code": "PERMISSION_DENIED", + "message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'", + "details": { + "operation": "insert", + "object": "employee", + "forbiddenFields": ["salary", "ssn"] + } + } +} +``` + +**为什么抛错而不是静默剥离?**静默剥离对诚实的客户端隐藏了安全边界(它们的更新"存不上"却不知道为什么),*同时*对探测型客户端也不给任何信号。抛错让边界在两个方向上都可观测——正当的 UI 得到可据以修复的错误;探测型客户端学不到任何它本来推断不出的东西。 + +另外两个强制执行细节: + +- **批量插入**逐行检查;任意一行中出现一个违规字段,整个批次会被原子性拒绝。 +- **系统操作**(`ExecutionContext { isSystem: true }`)完全绕过该检查——用于迁移、种子数据加载和审计日志写入。 + +## FLS 在表单和视图中 + +生成的表单和内联表格会在 UI 中隐藏不可编辑字段——但那只是 **UX 层**。上文的服务端检查才是事实来源,因此行为在所有地方保持一致: + +| 界面 | 隐藏字段 | 只读字段 | +|---|---|---| +| 记录表单 / 内联表格 | 不渲染 | 渲染但无可编辑控件;直接尝试写入会被 403 拒绝 | +| 列表视图、相关列表、导出 | 列值从响应中剥离 | 值正常显示 | +| REST / ObjectQL | 从结果中剥离 | 读取时返回;写入抛出带 `forbiddenFields` 的 `PERMISSION_DENIED` | +| MCP / AI Agent | 剥离——Agent [以调用用户身份运行](/docs/configure/mcp#permission-enforcement) | 与 REST 相同 | + +由于读取是剥离而不是报错,隐藏字段对该用户来说就像不存在一样——这正是目的所在。 + +## 验证与评审 FLS + +- **按判定、在运行时**——[解释引擎](/docs/configure/permissions#diagnose--audit)会连同其他每一层一起报告 FLS 层的判定结果,并点名起作用的权限集。当用户反馈字段"不见了"时用它。 +- **按变更、在构建时**——如果你的应用启用了访问矩阵快照门禁,`os compile` 会把推导出的(权限集 × 对象)能力矩阵与已提交的 `access-matrix.json` 做 diff,并在漂移时失败,让能力变更以可评审的语义 diff 形式随 Pull Request 流转: + +```bash +os compile --update-access-matrix +``` + +> 当字段承载敏感数据时,默认选**隐藏**而不是只读——只读仍会把值泄漏进响应和日志。把字段规则打包进匹配真实职能的权限集,并为合规场景配套审计日志留存。更多编写模式:[权限集](/docs/configure/permissions/permission-sets#field-security-appendix)。 + +## 下一步 + +| 任务 | 页面 | +|---|---| +| 在权限集中编写字段规则 | [权限集](/docs/configure/permissions/permission-sets) | +| 控制哪些行完全可触达 | [记录访问](/docs/configure/permissions/record-access) | +| 纵览整套分层模型 | [权限](/docs/configure/permissions) | +| 验证某个用户的访问权限 | [权限分配](/docs/configure/permissions/managing-access) | +| 检查 AI Agent 能读到什么 | [接入 AI 工具(MCP)](/docs/configure/mcp) | diff --git a/content/docs/configure/permissions/managing-access.zh-Hans.mdx b/content/docs/configure/permissions/managing-access.zh-Hans.mdx new file mode 100644 index 0000000..ce9d853 --- /dev/null +++ b/content/docs/configure/permissions/managing-access.zh-Hans.mdx @@ -0,0 +1,140 @@ +--- +title: 权限分配 +description: 日常管理手册 —— 新员工入职、角色变更、验证某人能看到什么,以及干净地办理离职。 +--- + +# 权限分配 + +这是管理员每周都会遇到的访问问题的任务指南。[模型总览](/docs/configure/permissions)解释各层*如何*工作;本页告诉你*该点哪里*。 + +> **90% 的日常管理就是把人分配到岗位。**岗位、其背后的权限集以及安全基线都随平台和你安装的应用一起交付。你几乎从不需要从零构建能力——也不应该那么做。 + +用户的有效访问是叠加式的: + +```text +effective access = union of permission sets reached through positions + + direct grants + + the built-in member_default baseline +``` + +限制通过*不授予*来实现——没有需要维护的减法规则。 + +## 新员工入职 + +四个有序步骤,每一步依赖上一步: + +| 步骤 | 位置 | 内容 | +|---|---|---| +| 1. 创建用户 | Setup → People & Organization → Users | **Invite**(邀请)、**Create**(创建)或 **Import**(导入)——见[用户与组织](/docs/configure/users#add-people) | +| 2. 放进组织树 | 同一列表 → Business Unit Member 行 | 用户 + 业务单元,其中一条标记为 *primary*;基于深度的可见性通过它解析 | +| 3. 分配岗位 | Setup → Access Control → Positions | 那日常的 90%——见下文 | +| 4. 验证 | 模拟 + 解释 | 绝不在未测试的情况下宣布"都配好了"——见下文 | + +### 分配岗位 + +一次分配就是一行记录:**User Position**(用户岗位,`sys_user_position`)= *用户* + *岗位* + 可选的任职**业务单元**。这个锚点决定岗位的深度授权*在哪里*生效——"**东区**销售经理"看到的是东区的记录,而不是整个公司的。 + +在 **Setup → Access Control → Positions**(岗位)中打开某个岗位,从其相关列表添加分配(或直接创建 User Position 行)。这些写入受治理约束:租户级管理员可以通过;委派管理员只能在自己的子树内分配白名单中的权限集——自我提权在结构上就会被拒绝(见[委派管理](/docs/configure/permissions/permission-sets#delegated-administration))。 + +两个相邻的界面补全全貌: + +- **直接授予**——在权限集的记录页(**Setup → Access Control → Permission Sets**)上,**Assigned Users**(已分配用户)面板可添加不经岗位的按用户授予。*经岗位*持有的行也会显示,但要在岗位上移除,不能在这里移除。 +- **业务单元成员资格**——来自第 2 步;独立于分配锚点。 + +## 变更某人的角色 + +当某人调换部门或职能时: + +1. 把其 **Business Unit Member**(业务单元成员)行更新为新单元。 +2. **重新锚定其 User Position 行**——移除或编辑锚定到旧单元的分配,为新单元添加分配。 +3. 移除属于旧角色的所有直接授予。 +4. 验证(见下文)。 + +分配可以携带 `valid_from` / `valid_until` 时间窗口——过期的授予立即停止解析,这是处理计划内交接或临时代理角色的干净方式。参见[权限集](/docs/configure/permissions/permission-sets)。 + +## 验证某人能看到什么 + +两个内置的验证工具: + +- **模拟。**用户列表 → 行菜单 → **Impersonate User**(模拟用户)。你会获得该用户的会话——打开他们会打开的应用,确认他们能看到该看的(且*看不到*不该看的)。模拟仅用于正当的支持与验证;会话会被记录。 +- **解释。**Studio 的 **Access**(访问)板块 → **Explain access**(解释访问):选定用户、对象和操作,引擎返回判定结果以及每个求值层——哪个权限集授予了权限、经由哪个岗位持有、哪条共享规则放宽了范围、哪条策略收窄了范围。 + +同样的报告也可通过 REST 获取: + +```bash +# GET,查询字符串形式 +curl -H "Authorization: Bearer $TOKEN" \ + "$BASE/api/v1/security/explain?object=crm_lead&operation=read&userId=usr_123" + +# POST,请求体形式 +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"object":"crm_lead","operation":"read","userId":"usr_123"}' \ + "$BASE/api/v1/security/explain" +``` + +`operation` 取 `read | create | update | delete | transfer | restore | purge` 之一(默认为 `read`);省略 `userId` 则解释**你自己**。解释自己始终被允许;解释*其他*用户需要 `manage_users` 能力,或覆盖该用户的委派管理作用域。 + +> 当权限解析结果不对时,先解释胜过瞎猜乱试:报告会点名需要修正的确切权限集和层。参见[诊断与审计一节](/docs/configure/permissions#diagnose--audit)。 + +## 办理离职 + +1. **Ban User**(封禁用户)——立即阻止登录。这是唯一紧急的一步。 +2. 之后从容移除其岗位分配和直接授予。 +3. 移除或改挂其业务单元成员行。 +4. 撤销绑定到该用户的所有 API Key——Key 以该用户身份行事,只要底层授予还在,它就还能用。 + +## 当自带的岗位不合用时 + +权限在 Studio 中*设计*,在 Setup 中*分配*。如果没有自带岗位能满足需求,优先选择能解决问题的最小改动,顺序如下: + +1. **把现有权限集绑定到岗位。** +2. **克隆一个自带权限集并调整。** +3. **在 Studio 的权限集矩阵编辑器中编写新权限集**——一张结构化的电子表格,覆盖对象增删改查、字段级安全和能力;你永远不需要手写 JSON。 + +编辑随应用交付的权限集会创建一个*环境覆盖层*(environment overlay)——你的修改优先生效、在升级后保留,并可重置回厂商基线。牢记叠加模型:编写内聚的能力包,绝不要写"减法权限集"。参见[权限集](/docs/configure/permissions/permission-sets)。 + +把需求转化为改动时,选择匹配的层: + +| 需求 | 层 | 参考 | +|---|---|---| +| 能读/建/改/删某类对象 | 对象权限 | [权限集](/docs/configure/permissions/permission-sets) | +| 能看到 / 编辑某个特定字段 | 字段级安全 | [字段级安全](/docs/configure/permissions/field-level-security) | +| 用户能看到哪些*行* | 记录访问 | [记录访问](/docs/configure/permissions/record-access) | +| 某项功能性能力(导出、管理用户) | 系统权限 | [权限集](/docs/configure/permissions/permission-sets#system-permissions) | +| 能打开某个应用或导航项 | 应用访问 | [权限集](/docs/configure/permissions/permission-sets) | + +## 快捷路径 + +| 我需要 …… | 这样做 | +|---|---| +| 新员工入职 | 邀请/创建用户 → 业务单元成员行 → 分配岗位 | +| 办理离职 | **Ban User**(立即阻止登录)——之后再从容移除分配 | +| "我登录不了" | 用户记录 → 是被封禁了?被锁定了?**Unlock Account**(解锁账户)或 **Set Password**(临时密码、强制修改) | +| "我为什么看不到 X?" | Studio → Access → 对该用户 + 对象运行 **Explain access** | +| 有人调换了部门 | 更新其业务单元成员行;重新锚定其 User Position 行 | +| 让子公司自行管理员工 | 授予携带[委派管理作用域](/docs/configure/permissions/permission-sets#delegated-administration)的权限集 | +| 给某个用户加一项额外能力 | 权限集记录 → **Assigned Users** → 添加(直接授予) | + +## 各项在哪里 + +| 事项 | 位置 | +|---|---| +| 用户、邀请 | Setup → People & Organization → Users / Invitations | +| 业务单元树 | Setup → People & Organization → Business Units | +| 团队 | Setup → People & Organization → Teams | +| 岗位、权限集 | Setup → Access Control | +| 共享规则、记录共享 | Setup → Access Control | +| 密码策略、MFA、锁定、SSO | Setup → Configuration → Authentication | +| 会话、通知事件、审计日志 | Setup → Diagnostics | +| 权限矩阵编辑器、Explain access | Studio → Access | + +## 下一步 + +| 任务 | 页面 | +|---|---| +| 创建用户并搭建组织树 | [用户与组织](/docs/configure/users) | +| 理解岗位与受众锚点 | [岗位](/docs/configure/permissions/positions) | +| 编写或调整权限集 | [权限集](/docs/configure/permissions/permission-sets) | +| 控制人们能看到哪些行 | [记录访问](/docs/configure/permissions/record-access) | +| 隐藏或锁定敏感字段 | [字段级安全](/docs/configure/permissions/field-level-security) | +| 登录策略与 SSO | [认证](/docs/configure/authentication) | diff --git a/content/docs/configure/users.zh-Hans.mdx b/content/docs/configure/users.zh-Hans.mdx new file mode 100644 index 0000000..44b9916 --- /dev/null +++ b/content/docs/configure/users.zh-Hans.mdx @@ -0,0 +1,116 @@ +--- +title: 用户与组织 +description: 搭建业务单元树、添加和邀请用户、管理成员资格与团队,以及开通服务账号。 +--- + +# 用户与组织 + +关于你的部署中*都有谁*的一切——人员、他们所在的组织树、他们协作的团队,以及代表他们行事的服务账号——都在 **Setup → People & Organization**(人员与组织,`/apps/setup`)中管理。 + +底层的身份对象(`sys_user`、`sys_organization`、`sys_member`、`sys_business_unit`、`sys_team`、`sys_invitation`、`sys_api_key` ……)存在你的项目数据库中;每个对象表示什么,参见[身份层表格](/docs/configure/permissions#layer-1--identity)。 + +> **90% 的日常管理就是把人分配到岗位。**岗位、其背后的权限集以及安全基线都随平台和你安装的应用一起交付。你的工作是人这一侧——本页所讲的内容——以及分配这一侧,后者在[权限分配](/docs/configure/permissions/managing-access)中讲解。 + +## 搭建组织(业务单元) + +**Setup → People & Organization → Business Units**(业务单元)。 + +业务单元树是访问模型中*唯一*的层级结构:它决定可见性深度(`unit`、`unit_and_below`)和委派管理边界。在初始上线时搭好它,之后仅在组织架构调整时再修改。 + +- 默认的 **Org Chart**(组织架构图)标签页把树渲染为可展开/收起的缩进树形表格。 +- 先创建根节点(kind 为 `company`),再用 **New**(新建)添加子节点,并在表单中选择 **Parent Business Unit**(上级业务单元)。`division` / `department` / `office` / `cost_center` 这些 kind 只是显示提示——无论选哪种,树的工作方式都一样。 +- 调整架构时,**Edit**(编辑)某个单元并修改其上级即可。树视图是只读展示——变更上级要通过记录表单完成,而不是拖拽。 + +三个名字相近的东西,三种不同的职责——别混淆: + +| 对象 | 职责 | +|---|---| +| **业务单元**(`sys_business_unit`) | 层级结构。驱动可见性深度和委派管理边界 | +| **团队**(`sys_team`) | 扁平的协作组。团队只*接收*共享;从不承载能力 | +| **组织**(`sys_organization`) | 租户本身——你公司的账户,而不是其中的一个节点 | + +> 让树保持浅层、如实反映你的真实结构。如果某个东西应该按组织结构影响*人们能看到哪些记录*,它是业务单元;如果只是一个供人共享记录的群组,它是团队。 + +## 添加人员 + +**Setup → People & Organization → Users**(用户)。三条入口,都是一等公民: + +| 路径 | 何时使用 | 会发生什么 | +|---|---|---| +| **Invite User**(邀请用户,工具栏) | 此人有可达的邮箱 | 发送邀请;对方接受时自行设置密码 | +| **Create User**(创建用户,工具栏) | 不想走邮件流程——或只有手机号的员工 | 创建可直接登录的账户(邮箱和/或手机号);生成的临时密码只显示**一次**,并强制首次登录时修改密码 | +| **Import**(导入,工具栏) | 从 CSV/Excel 批量入职 | 带列映射和 dry-run 预览的向导;每批最多 500 行。可选择登录策略:无密码(首次通过 OTP/魔法链接/重置链接登录)、发送邀请,或一次性临时密码 | + +待接受的邀请列在 **Setup → People & Organization → Invitations**(邀请)下。 + +同样的流程也可通过 REST 完成(ObjectStack 14.3+),用于脚本化入职: + +```bash +# 创建可直接登录的账户,可选生成一次性密码 +curl -X POST https://crm.example.com/api/v1/auth/admin/create-user + +# 批量导入行 / CSV / XLSX(最多 500 行),支持 dry-run 和 upsert 模式 +curl -X POST https://crm.example.com/api/v1/auth/admin/import-users +``` + +请求体、密码策略和仅手机号账户的细节,参见[认证 → 管理员用户管理](/docs/configure/authentication#admin-user-management-objectstack-143)。 + +## 把人放进组织树(成员资格) + +把一个人放进组织树是一条独立的记录:**Business Unit Member**(业务单元成员)行(`sys_business_unit_member`——用户 + 业务单元,其中一条标记为 *primary*)。基于深度的可见性和 `unit_and_subordinates` 共享都通过这条成员资格解析,所以不要跳过它——没有成员资格行的用户对基于深度的规则来说是不可见的。 + +当某人调换部门时,更新其业务单元成员行,并重新锚定其岗位分配——参见[权限分配](/docs/configure/permissions/managing-access)。 + +## 团队 + +**Setup → People & Organization → Teams**(团队)。团队是扁平的协作组:共享规则和记录共享可以指向它们,但它们从不承载权限集。当一个跨部门的群组需要看到某批记录,而又不想改变任何人的职能或组织树时,就用团队。 + +## 用户生命周期 + +日常生命周期操作位于每个用户的行菜单和记录头部: + +| 操作 | 效果 | +|---|---| +| **Ban / Unban**(封禁 / 解封) | 立即阻止(或恢复)登录 | +| **Unlock Account**(解锁账户) | 提前清除暴力破解锁定 | +| **Set Password**(设置密码) | 直接设置密码;也能生成强制轮换的一次性临时密码 | +| **Impersonate User**(模拟用户) | 以该用户身份打开会话,用于支持和验证(会话会被记录) | + +要停用某人,先 **Ban**(封禁)——登录立即被阻止——之后再从容移除其岗位分配和成员资格。完整的离职流程见[权限分配](/docs/configure/permissions/managing-access)。 + +> 用户的自助密码找回依赖已配置的[邮件](/docs/configure/email)或短信发送服务。在接通之前,管理员的 **Set Password**(临时密码、强制修改)是可靠的兜底手段。 + +## 服务账号与 API Key + +集成和无头 Agent 使用 API Key(`sys_api_key`)认证——一种**绑定到用户**的长期编程凭据。用该 Key 发起的每次调用都以那个用户的身份、按那个用户的权限运行。 + +凡是超出个人脚本用途的场景,都请创建专用的服务用户: + +1. 为该集成 **Create User**(创建用户,无需邀请)。 +2. 为它分配能覆盖该集成所涉对象的最小权限集——绝不要用管理员权限集。 +3. 为它签发 API Key,可在 **Setup → Connect an Agent**(接入 Agent)中操作,或通过 REST: + +```bash +curl -b cookies.txt -X POST https://crm.example.com/api/v1/keys +# → { "key": "osk_..." } —— 只显示一次;存入你的 secret manager +``` + +Key 的用法、请求头和轮换见 [API 访问](/docs/configure/api-access);用 Key 接入 AI Agent 见[接入 AI 工具(MCP)](/docs/configure/mcp)。 + +## FAQ + +**新用户几乎什么都看不到——是坏了吗?**不是:这正是基线在正常工作。每个已认证用户都获得叠加式的 `member_default` 基线;真正的访问权限在你分配岗位时到来。 + +**部门还是团队?**部门 = 业务单元(层级、可见性)。团队 = 扁平的共享群组。 + +**能删除内置权限集吗?**不能——`member_default`、`organization_admin` 这类权限集是平台基线。随应用交付的权限集可以被覆盖(overlay),或干脆不分配。 + +## 下一步 + +| 任务 | 页面 | +|---|---| +| 分配岗位和权限集 | [权限分配](/docs/configure/permissions/managing-access) | +| 理解分层访问模型 | [权限](/docs/configure/permissions) | +| 配置登录、SSO 和密码策略 | [认证](/docs/configure/authentication) | +| 配置邮件,让邀请和重置邮件可送达 | [邮件](/docs/configure/email) | +| 集成用的 API Key | [API 访问](/docs/configure/api-access) | diff --git a/content/docs/deploy/air-gapped.zh-Hans.mdx b/content/docs/deploy/air-gapped.zh-Hans.mdx index bb9c4ca..d53c623 100644 --- a/content/docs/deploy/air-gapped.zh-Hans.mdx +++ b/content/docs/deploy/air-gapped.zh-Hans.mdx @@ -51,6 +51,19 @@ ObjectOS 会将每个请求解析到打包的项目,并从磁盘加载制品 如果客户使用 OIDC/SSO,身份提供方必须能从隔离网络中访问。如果无法访问, 请使用本地的邮箱/密码身份验证,或在同一网络内托管的身份提供方。 +## 安装额外的包 + +运行中的隔离网络实例无需任何目录连接即可安装额外的包 —— 把编译好的 +制品交给安装 CLI,它会内联发送并合并进正在运行的内核(无需重启): + +```bash +os package install ./dist/objectstack.json --runtime https://os.internal.example \ + --email admin@example.com --password … +``` + +清单会缓存在运行时主机的 `.objectstack/installed-packages/` 目录下, +并在每次启动时重新注册。 + ## 升级流程 将制品视为不可变: diff --git a/content/docs/index.zh-Hans.mdx b/content/docs/index.zh-Hans.mdx index 82d0aa2..067a068 100644 --- a/content/docs/index.zh-Hans.mdx +++ b/content/docs/index.zh-Hans.mdx @@ -55,6 +55,16 @@ os start ObjectOS 从不回传数据。无遥测。无授权服务器。气隙网络是头等部署目标 —— 参见 [Air-gapped](/docs/deploy/air-gapped)。 +## 按角色找文档 + +本文档服务三类人群 —— 从与你使用 ObjectOS 的方式匹配的部分开始: + +| 你是… | 你想… | 从这里开始 | +|---|---|---| +| **普通用户** | 在应用中工作:记录、视图、仪表盘、审批 | [使用](/docs/use) | +| **构建者** | 创建应用:数据模型、界面、自动化、AI Agent | [构建](/docs/build) | +| **管理员** | 运行平台:用户、权限、设置、部署 | [管理](/docs/configure)、[部署](/docs/deploy)、[运维](/docs/operate/production) | + ## 下一步去哪里 | 如果你想…… | 阅读 | @@ -70,6 +80,6 @@ ObjectOS 从不回传数据。无遥测。无授权服务器。气隙网络是 | 向其他系统发送事件 | [Webhooks](/docs/configure/webhooks) | | 上线生产 | [Production Readiness](/docs/operate/production) | -## 许可与价格 +## 许可与定价 -ObjectOS 运行时采用 **Apache-2.0** —— 主流 OSS 许可证中最宽松的一种。可用于商业产品、可嵌入、可再发布,你的修改可以保持私有。无人头费、无授权服务器、无 "起价 5 万美元/年"。可选的商业支持和托管服务单独提供 —— 参见 [License](/docs/resources/license)。 +ObjectOS 是构建在开源(Apache-2.0)**[ObjectStack 框架](https://github.com/objectstack-ai/framework)** 之上的**商业产品** —— 框架可免费用于商业产品、可嵌入、可自托管,没有按席位收费,也没有许可证服务器。ObjectOS 在其上增加内嵌的界面内 AI、治理与官方运维,**仅按 AI 席位**计价(只读用户与不使用 AI 的用户免费)—— 没有 "起价 5 万美元/年"。参见 [许可与定价](/docs/resources/license)。 diff --git a/content/docs/resources/license.zh-Hans.mdx b/content/docs/resources/license.zh-Hans.mdx index b8b3f62..e3d4052 100644 --- a/content/docs/resources/license.zh-Hans.mdx +++ b/content/docs/resources/license.zh-Hans.mdx @@ -1,97 +1,138 @@ --- -title: 许可证 -description: ObjectOS 的许可 —— Apache-2.0、商业选项与 FAQ。 +title: 许可与定价 +description: ObjectOS 是商业产品 —— 版本、定价,以及它与开源 ObjectStack 框架的关系。 --- -# 许可证 - -## ObjectOS 运行时 —— Apache-2.0 - -ObjectOS 运行时与所有 `@objectstack/*` 开源包采用 -[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)。 - -通俗地讲: - -| 你可以 | 你必须 | +# 许可与定价 + +## ObjectOS 是商业产品 + +ObjectOS 是 **ObjectStack 应用的官方运行环境**,以两种交付形式销售: +**ObjectOS Cloud**(我们运维)和 **ObjectOS Self-Managed**(自管:你运维, +采用商业许可)。**不存在开源版的 ObjectOS**。 + +开源的故事在它自己的品牌之下: +**[ObjectStack 框架](https://github.com/objectstack-ai/framework)** +(Apache-2.0)包含你**构建、运行、自托管自有应用**所需的一切 —— +协议、内核、CLI、生产运行时、Console 和 MCP 服务器 —— 免费,且开发与 +生产之间没有任何功能门槛。如果你想要免费自托管,你要的就是 ObjectStack, +而且它非常出色。当你想在这套机制之上获得**内嵌 AI** 与**官方运维 / +官方支持**的平台时,才需要购买 ObjectOS。 + +## 版本 + +一张价目表,两种交付。**AI 席位**是所有版本中唯一的计价单位。 + +| | **Free** | **Team** | **Business** | **Enterprise** | +|---|---|---|---|---| +| 交付方式 | 云 | 云 | 云**或自管(单节点)** | 云**或私有部署** | +| 价格 | **免费** | **$24** / AI 席位 / 月 | **$54** / AI 席位 / 月 · 自管:**$540** / AI 席位 / 年 | 定制(按量)—— [联系销售](mailto:sales@objectstack.ai) | +| 界面内 AI(AI Builder + "问数据") | 尝鲜额度 | ✓ | ✓ | ✓ | +| AI 治理(操作审计、护栏、权限限定) | — | — | ✓ | ✓ | +| 集群 / 高可用、多组织自托管、隔离网络、SCIM | — | — | — | ✓ | +| 支持 | 社区 | 标准 | 优先(云)/ 标准(自管) | SLA + 专属支持 | + +想**免费**自托管?那是开源的 **ObjectStack 框架**(AI 仅通过 MCP 提供) +—— 是另一个品牌,不是 ObjectOS 的某个版本。见下文 +[开源替代方案](#开源替代方案)。 + +**机制开源,智能闭源。**建模数据、运行应用所需的一切都是开源的 +(ObjectStack)。ObjectOS 在其上叠加*内嵌智能* —— 产品内的 AI Builder +和"问数据"助手,可治理、可审计 —— 外加运营规模与官方支持。 + +**你只为 AI 席位付费。**唯一计费的席位是 **AI 席位**;只读用户、 +不使用 AI 的应用用户、以及通过 MCP 构建的开发者都免费,没有按用户 +收取的人头税(与每用户 $150–300 的既有厂商形成鲜明对比)。 + +## 云或自管 + +- **云(Free / Team / Business)** —— 全托管、自助开通。价格包含托管 + 和每月慷慨的 AI 用量额度(模型由我们运行)。 +- **Business Self-Managed(自管)** —— 以**单节点 Docker 许可**交付的 + Business 计划,面向 **10–25 个 AI 席位**($540/AI 席位/年,按年付): + 在你自己的基础设施上获得界面内 AI 与 AI 治理,**自带模型 / 自带 Key** + (LLM 成本由你自己承担 —— 没有平台 AI 账单,也不计量),刷卡支付, + 标准支持。许可过期后,运行时继续运行、数据保持完好 —— 只是付费功能 + 关闭(永远不会变砖)。 +- **ObjectOS Enterprise** —— 覆盖其余一切需求的完整私有部署:集群 / + 高可用 / 多节点、多组织自托管、数据驻留与**隔离网络**运行、SCIM 与 + 身份生命周期、合规证明、SLA 与专属支持,以及线下 / 发票付款。以 + **年度许可**形式销售,可选多年锁价;不续费时你的部署**永久保留最后 + 付费的版本**(你失去的是更新与支持,永远不是你的系统或数据)。 + 定价按量 —— [联系销售](mailto:sales@objectstack.ai)。 + +**没有按节点收费** —— 集群 / 扩展已并入 Enterprise。 + +## 各版本适用场景 + +| 场景 | 选择 | |---|---| -| 用于商业产品 | 再分发时附带 LICENSE 与 NOTICE 文件 | -| 修改而不公开变更 | 把修改过的文件标注为已修改 | -| 嵌入到专有软件中 | 不能未经许可使用 ObjectStack 商标 | -| 在任何场景、任意位置运行 | — | -| 作为更大作品的一部分再许可 | — | - -**没有 copyleft**。你为内部使用所做的修改,或者在闭源产品中发布的 -修改,**不必**回馈上游。Apache-2.0 还包含贡献者的**显式专利授权** —— -这对企业法务审查很重要。 +| 你想要平台**由我们托管和运维** | ObjectOS Cloud(Free → Team → Business) | +| 你必须运行在**自己的单节点**上,并想要产品内 AI,≤25 个 AI 席位 | **Business Self-Managed** | +| 你需要**集群 / 高可用**、**多组织**、**隔离网络**、**SCIM**、合规证明、SLA 或线下付款 | **ObjectOS Enterprise** | +| 你自托管免费的 ObjectStack 运行时,但需要**带商业合同的受支持发行版**(SLA、安全补丁优先、指定升级对接人) | **ObjectOS Runtime Subscription** —— [联系销售](mailto:sales@objectstack.ai) | +| 你想要**免费自托管**,并通过 MCP 自带 AI | **ObjectStack 框架**(开源) | +| 你需要以 ObjectOS 或 ObjectStack 名义的 **OEM / 再分发**权利 | 商业协议 —— [联系销售](mailto:sales@objectstack.ai) | + +## 开源替代方案 + +**ObjectStack 框架**采用 +[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0): +没有 copyleft,带显式专利授权,永久免费 —— 没有席位计费、没有用量层级、 +没有许可证服务器、没有 key、没有遥测。运行在你的笔记本、你的服务器、 +你客户的服务器,或一个 1000 个 Pod 的集群上;所有情形下许可一致。 +那里的 AI 路径是开放的 **MCP 服务器**:把你自己的 Claude Code(或任何 +MCP 客户端)指向你的部署,它就能用你自己的模型查询和操作你的数据。 + +Apache-2.0 涵盖的内容(位于框架仓库):运行时镜像、所有 +`@objectstack/*` npm 包(运行时、插件、服务、驱动、adapter、CLI、 +客户端 SDK)、模板仓库,以及 Console 与 Account UI。文档采用 CC-BY-4.0。 完整文本:[LICENSE](https://github.com/objectstack-ai/framework/blob/main/LICENSE) -## Apache-2.0 涵盖范围 - -| | 许可证 | -|---|---| -| ObjectOS 运行时镜像 | Apache-2.0 | -| 所有 `@objectstack/*` npm 包(运行时、插件、服务、驱动、adapter、CLI、客户端 SDK) | Apache-2.0 | -| 模板仓库(`objectstack-ai/templates`) | Apache-2.0 | -| Console、Account UI | Apache-2.0 | -| 文档 | CC-BY-4.0 | - -## 何时可能需要商业协议 - -Apache-2.0 已经覆盖大多数场景。在以下情况下你可能仍想要商业协议: - -| 场景 | 我们的提供 | -|---|---| -| 你需要**带 SLA 的支持** | 商业支持 | -| 你想要**我们替你托管** | 托管云(ObjectStack Cloud) | -| 你需要 **OEM 权利**(使用 ObjectStack 商标,作为你自家品牌发布) | OEM 协议 | -| 你需要**保修 / 赔偿** | 商业协议(Apache-2.0 不提供保修) | -| 你需要**合规证明**(运行时镜像的 SOC 2 报告、ISO 证书) | 企业版 | - -请联系 **sales@objectstack.ai** 进一步沟通。 - -## 开源运行时的定价 - -**免费**。永远免费。没有席位计费、没有用量层级、没有许可证服务器、 -没有 key。 - -可以运行在: -- 你的笔记本上 -- 你的服务器上 -- 你客户的服务器上 -- 一台树莓派上 -- 一个 1000 个 Pod 的 Kubernetes 集群上 +## 商标 -所有情形下许可一致。 +"ObjectOS" 与 ObjectOS Logo 是**商标**,不在任何代码许可的覆盖范围内。 +你可以如实声明你的产品*构建于* ObjectOS / ObjectStack 之上或与之*兼容*; +但在你自己的产品名、公司名或域名中使用这些名称或 Logo 需要书面许可。 +见 [TRADEMARK.md](https://github.com/objectstack-ai/objectos/blob/main/TRADEMARK.md)。 ## 第三方组件 -ObjectOS 依赖若干上游开源项目(Node.js、Hono、Zod、Better Auth、 -AI SDK……)。每个项目都有各自的许可证 —— 都是与商业使用兼容的 -Apache-2.0、MIT 或 BSD 风格的宽松许可。完整清单见每次发布附带的 -SBOM。 +ObjectOS 构建在 ObjectStack 框架和若干上游开源项目之上(Node.js、Hono、 +Zod、Better Auth、AI SDK……)。每个项目都有各自的许可证 —— 都是与商业 +使用兼容的 Apache-2.0、MIT 或 BSD 风格的宽松许可。完整清单见每次发布 +附带的 SBOM。 ## FAQ -**Q:可以在我对外销售的闭源 SaaS 里使用 ObjectOS 吗?** -A:可以。Apache-2.0 没有 copyleft。你不必公开自己的代码。 +**Q:ObjectOS 是开源的吗?** +A:不是。ObjectOS 是商业产品,没有开源版本。它所运行其上的开源平台是 +**ObjectStack 框架**(Apache-2.0)—— 用它免费构建并自托管你自己的应用。 -**Q:可以修改 ObjectOS 而不公开修改吗?** -A:可以。Apache-2.0 不要求公开修改。 +**Q:可以在我对外销售的闭源 SaaS 里使用 ObjectStack 吗?** +A:可以。Apache-2.0 没有 copyleft、没有版税 —— 你不必公开自己的代码, +产品赚钱也分文不欠。 -**Q:能把 Console 重新品牌化成我产品的一部分吗?** -A:UI 本身可以。但若要用 "ObjectStack" 的名称或 Logo 推广你的 -产品,则需要 OEM 协议 —— 请联系销售。 +**Q:可以不付费自托管 ObjectOS 吗?** +A:不可以 —— 自管的 ObjectOS(Business Self-Managed 或 Enterprise) +需要许可。免费自托管对应的是 ObjectStack 框架,即同一套开放机制, +AI 仅通过 MCP 提供。 -**Q:如果我的产品赚钱了需要付费给你们吗?** -A:不需要。没有版税或营收分成。 +**Q:不续费的话软件会停止工作吗?** +A:永远不会变砖。Business Self-Managed 会降级为免费机制的行为 +(付费功能关闭;你的数据和运行时继续工作)。Enterprise **永久保留 +最后付费的版本** —— 你失去的是更新、新的 AI 能力和支持,而不是你的系统。 -**Q:ObjectOS 会回传使用数据吗?** -A:不会。没有遥测、没有许可证校验、没有更新探活。见 -[安全与合规](/docs/reference/security#data-residency)。 +**Q:ObjectOS 会回传数据吗?** +A:ObjectOS Self-Managed 会在线校验许可证(Enterprise 隔离网络许可证 +为离线校验)。开源的 ObjectStack 运行时没有遥测、没有许可证校验、 +没有更新探活。 -**Q:如果我想要合同保修怎么办?** -A:Apache-2.0 不提供保修。可购买商业协议,我们会提供保修。 +**Q:如果我想为免费运行时获得合同保修怎么办?** +A:那就是 **ObjectOS Runtime Subscription** —— 针对开源 ObjectStack +运行时的支持合同(官方发行版、SLA、安全补丁优先),功能上零差异。 +请联系 [sales@objectstack.ai](mailto:sales@objectstack.ai)。 -**Q:开源版本会一直免费吗?** -A:会。Apache-2.0 授权不可撤销。 +**Q:开源框架会一直免费吗?** +A:会。Apache-2.0 的授权不可撤销,已发布的一切将保持其发布时的许可。 diff --git a/content/docs/use/approvals.zh-Hans.mdx b/content/docs/use/approvals.zh-Hans.mdx new file mode 100644 index 0000000..eddf06c --- /dev/null +++ b/content/docs/use/approvals.zh-Hans.mdx @@ -0,0 +1,70 @@ +--- +title: 审批 +description: 提交记录等待签核、处理指派给你的请求,并随时掌握每个审批的进展。 +--- + +# 审批 + +有些记录在往前推进之前需要一次签核 —— 超额的报销、一个折扣、一张请假单。此时 ObjectOS 会创建一条**审批请求**:一条由系统管理的实时记录,从提交那一刻起追踪这次送审,直到有人批准或拒绝。这些追踪记录你永远不需要自己创建或编辑 —— 系统会随着决定的做出自动保持它们最新。 + +本页覆盖流程的两端:把东西提交审批,以及处理等你决定的请求。 + +## 你的审批收件箱 + +所有与审批相关的东西都在一个地方:**Account → Inbox → Approvals**(账户 → 收件箱 → 审批)。打开 **Account** 应用(所有人都能用),在左侧边栏展开 **Inbox**,选择 **Approvals**。 + +列表顶部有视图标签页,让你从四个角度切分同一个收件箱: + +| 视图标签页 | 显示什么 | +|---|---| +| **My Pending**(待我处理) | 等*你*来决定的请求。这就是你的待办清单。 | +| **I Submitted**(我提交的) | 你发出的审批请求 —— 在这里查看自己的送审进展到哪一步。 | +| **Completed**(已完成) | 已有结论的请求,无论批准还是拒绝。 | +| **All**(全部) | 你有权看到的每一条审批请求。 | + +当没有任何事项等你处理时,**My Pending** 会显示空状态 *"No pending approvals — You're all caught up."* 这正是你希望在一天结束时看到的画面。 + +> **提示:**收藏审批收件箱,或者直接瞟一眼 Console 首页的 **Needs your attention** 列表 —— 待处理的审批也会出现在那里,并标注已经等了多久。 + +## 审批请求是什么 + +| 它是…… | 它不是…… | +|---|---| +| 按每次提交追踪的实时实例 —— 每条送审记录对应一个请求 | 记录本身的一份副本 | +| 系统管理 —— 随着决定的做出自动创建、更新和关闭 | 需要你手工编辑的东西 | +| 你的审计线索 —— 谁提交的、指派给谁、决定了什么、什么时候 | 一条看完就消失的一次性通知 | + +因为每次提交都会得到自己的请求,被拒绝后重新提交记录会开启一条全新的请求 —— 旧的那条会作为历史留在 **Completed** 里。 + +## 处理请求 + +当一条请求被路由给你时,会发生两件事: + +1. **你会收到通知** —— 顶栏的铃铛出现未读角标,事项会出现在 Console 首页的 **Needs your attention** 和 **Account → Inbox → Notifications** 中。 +2. **相关记录会向你显示审批操作** —— 打开这条记录,你会看到 **Approve**(批准)和 **Reject**(拒绝)两个记录操作。这些按钮只对被指派的审批人显示;其他查看这条记录的人看不到。 + +| 想要…… | 这样做 | +|---|---| +| 看有什么在等我 | **Account → Inbox → Approvals** → **My Pending**,或看铃铛 / **Needs your attention** | +| 审阅详情 | 从请求打开对应的记录 —— 像读任何记录一样阅读它的要点栏和字段分组 | +| 做出决定 | 使用 **Approve** 或 **Reject** 记录操作 | +| 确认已生效 | 请求移入 **Completed**,提交人收到通知 | + +> **提示:**读记录,而不只是读请求。审批请求只告诉你*有*事情需要决定;记录本身才告诉你*该不该*批准。 + +## 待审期间记录被锁定 + +取决于审批的配置方式,记录在请求待处理期间可能被**锁定** —— 在做出决定之前字段无法编辑。这是在保护审批人:他们批准的正是当初提交的内容,中间没有悄悄的改动。如果你需要修改一条被锁定的记录,请让审批人拒绝它(或在应用支持时撤回它),改完后重新提交。 + +## 应用自带的审阅队列 + +有些应用会添加自己精心组织的**审阅队列页面** —— 比如应用导航里的一个 "Approvals" 页面,只显示与该团队相关的事项,已经预先过滤好、可以直接逐条处理。有就用它;底层是同样的请求,只是换了一个更友好的视角。你的 **Account → Inbox → Approvals** 收件箱始终是完整的、一站式的总览。 + +## 下一步 + +| 现在做什么 | 阅读 | +|---|---| +| 以日常用户身份导览 Console | [使用 ObjectOS](/docs/use) | +| 处理你正在审批的记录 | [记录](/docs/use/records) | +| 跟进路由给你的其他一切 | [通知](/docs/use/notifications) | +| 像审批人一样切分列表 | [视图](/docs/use/views) | diff --git a/content/docs/use/dashboards.zh-Hans.mdx b/content/docs/use/dashboards.zh-Hans.mdx new file mode 100644 index 0000000..61c0a3f --- /dev/null +++ b/content/docs/use/dashboards.zh-Hans.mdx @@ -0,0 +1,67 @@ +--- +title: 仪表盘 +description: 一眼读懂团队的 KPI、图表和表格 —— 两次点击就能按日期或过滤条件收窄范围。 +--- + +# 仪表盘 + +**仪表盘**把你的记录变成数字和图表:有多少任务未完成、工作的趋势如何、谁的负荷最重。你不在这里构建仪表盘 —— 你只负责读;记录变化时它们会自动更新。 + +## 仪表盘在哪里 + +仪表盘是应用**导航侧边栏**中的菜单项,通常放在 **Analytics** 之类的分组里。点击一个,它会像普通页面一样打开。 + +## 仪表盘头部 + +每个仪表盘的顶部都有: + +| 元素 | 作用 | +|---|---| +| **标题和描述** | 这个仪表盘在衡量什么、为什么 | +| **日期范围预设** | 类似 **Last 90 days**(最近 90 天)的下拉框 —— 一次改变所有组件的时间窗口 | +| **仪表盘过滤器** | 类似 **Task Status: All** 的下拉框 —— 把所有组件收窄到一个切片(如只看 *In Progress*) | + +> **提示:**如果某个数字看起来不对,先检查日期范围和过滤条件 —— "Last 90 days" 加 **Task Status: Done** 呈现的画面,和全时段、全部状态截然不同。 + +日期范围和过滤条件作用于**整个仪表盘**,所以每个组件回答的都是关于同一片数据切片的同一个问题 —— 你永远不用怀疑两张图表统计的是不是不同的东西。 + +## 组件类型 + +仪表盘是由**组件**排成的网格。你会遇到三种: + +| 组件 | 长什么样 | 适合什么 | +|---|---|---| +| **KPI 指标卡** | 一个大数字加一个标签 | 头条数字 —— *Open Tasks: 42* | +| **图表** | 柱状图、饼图等可视化 | 比较分组、发现趋势 | +| **表格** | 行与列 | 数字背后的明细 —— 头部条目、最近的记录 | + +典型布局是顶部一排指标卡(头条),中间是图表(数据的形态),底部是表格(凭据)。按这个顺序读下来,不到一分钟你就掌握了全貌。 + +## 仪表盘与视图的区别 + +两者都展示你的记录 —— 只是高度不同: + +| 界面 | 展示 | 适用场景 | +|---|---|---| +| **仪表盘** | 聚合值 —— 计数、合计、趋势 | 你想看全局 | +| **视图** | 一行行的具体记录 | 你想逐条处理列表 —— 参见[使用视图](/docs/use/views) | + +## 空状态 + +没有数据可显示的组件会直说 —— 比如一张显示 **No rows**(无数据)的表格。这不是错误:在当前的日期范围和过滤条件下,没有记录匹配。放宽范围或重置一个过滤器,数据通常就回来了。 + +## 从数字到记录 + +仪表盘汇总的是记录,有些组件支持**下钻**到记录本身:凡是可点击的指标卡、图表分段或表格行,点开就是匹配的记录列表,让你能对数字背后的问题直接采取行动。之后你就在一个普通列表里了 —— 参见[使用视图](/docs/use/views)。 + +> **提示:**读仪表盘是一个循环:发现异常数字,用过滤器收窄,下钻到记录,修好该修的东西。 + +## 下一步 + +| 我想…… | 阅读 | +|---|---| +| 处理数字背后的记录 | [记录操作](/docs/use/records) | +| 自己动手过滤和分组列表 | [使用视图](/docs/use/views) | +| 处理等我审批的请求 | [审批](/docs/use/approvals) | +| 管理我收到的通知 | [通知](/docs/use/notifications) | +| 回到 Console 导览 | [使用 ObjectOS](/docs/use) | diff --git a/content/docs/use/index.zh-Hans.mdx b/content/docs/use/index.zh-Hans.mdx new file mode 100644 index 0000000..34bb498 --- /dev/null +++ b/content/docs/use/index.zh-Hans.mdx @@ -0,0 +1,90 @@ +--- +title: 使用 ObjectOS +description: 登录一次,就能找到团队共享的每个应用、记录和通知 —— 五分钟带你逛完 Console。 +--- + +# 使用 ObjectOS + +ObjectOS 是你的团队业务应用的家 —— 项目、任务、审批、仪表盘都在这里。你只需登录一次,就能在同一个地方看到你有权访问的每个应用,共用一个搜索框。 + +本章面向**日常用户**。如果你要构建应用或管理系统,请前往[构建](/docs/build)或[配置](/docs/configure)。 + +## 登录 + +打开团队给你的 URL,用你的**邮箱和密码**登录。会话会被记住,所以在这个浏览器上你会一直保持登录状态,直到你从头像菜单中退出。 + +## Console 首页 + +登录后你会来到首页。从上到下依次是: + +| 区域 | 显示什么 | +|---|---| +| **问候语** | 一句个人化的问候("Good afternoon, Ada") | +| **你的应用** | 一组磁贴 —— 每个你有权访问的应用一块。点击磁贴即可打开应用。 | +| **Needs your attention**(待你处理) | 待处理的通知和审批,并标注每条到达了多久 | +| **Recently Accessed**(最近访问) | 你最近打开过的对象和记录 —— 回到昨天工作的最快通道 | +| **活动流** | 工作区内的近期动态 | + +> **提示:**把 **Needs your attention** 当作你的晨间收件箱 —— 它会在你打开任何应用之前,先把等你审批的事项摆到面前。 + +## 在应用之间切换 + +左上角的**面包屑**始终告诉你身在何处:*应用 → 对象 → 记录*。第一段是**应用切换器** —— 点击它会展开你的应用列表,无需回到首页就能在应用间跳转。 + +除了为你的团队构建的业务应用,每个安装实例还自带: + +| 应用 | 谁能看到 | 用来做什么 | +|---|---|---| +| **Account**(账户) | 所有人 | 你的个人空间 —— 个人资料、通知收件箱、审批、安全 | +| **Setup**(管理后台) | 仅管理员 | 系统管理 | + +## 应用导航侧边栏 + +进入应用后,左侧边栏就是该应用的菜单。它包含平铺的菜单项和**可折叠的分组**(例如 *Workspace* 或 *Analytics*)。每个菜单项打开的是以下三种之一: + +| 菜单项类型 | 点击后打开什么 | +|---|---| +| 对象(如 *Tasks*) | 它的列表视图 —— 参见[视图](/docs/use/views) | +| 页面 | 为你的应用构建的自定义页面 | +| 仪表盘 | 图表和 KPI —— 参见[仪表盘](/docs/use/dashboards) | + +当前菜单项旁会出现一个**图钉图标**,让你把最常用的位置固定下来。 + +## 全局搜索 + +按 `⌘K`(Mac)或 `Ctrl-K`(Windows/Linux),或点击顶栏的 **Search…**(搜索)。弹出的对话框会搜索**你所有对象中的记录** —— 输入任务名、项目名或客户名的几个字母,就能直接跳过去。 + +## 顶栏 + +顶栏在每个页面都可见: + +| 控件 | 作用 | +|---|---| +| **Search…** | 打开全局搜索(`⌘K` / `Ctrl-K`) | +| **铃铛** | 通知,红色角标显示未读数量。完整收件箱在 **Account → Inbox → Notifications**(账户 → 收件箱 → 通知)。 | +| **?** | 帮助 | +| **头像** | 你的账户菜单(见下) | + +**头像菜单**显示你的姓名和邮箱,并包含: + +| 入口 | 作用 | +|---|---| +| **Profile**(个人资料) | 在 Account 应用中打开你的个人资料 | +| **Preferences → Theme**(偏好设置 → 主题) | 在浅色与深色之间切换 | +| **Preferences → Language**(偏好设置 → 语言) | 更改界面语言 | +| **Log out**(退出登录) | 结束你的会话 | + +> **提示:**主题和语言在**头像菜单 → Preferences** 里,而不在你的个人资料页 —— 很多人会先去错地方找。 + +## 下一步 + +| 我想…… | 阅读 | +|---|---| +| 打开、创建和编辑记录 | [记录操作](/docs/use/records) | +| 在表格、看板、日历等之间切换 | [使用视图](/docs/use/views) | +| 阅读图表和 KPI | [仪表盘](/docs/use/dashboards) | +| 批准或拒绝请求 | [审批](/docs/use/approvals) | +| 管理我的通知 | [通知](/docs/use/notifications) | +| 更新我的姓名、照片或密码 | [个人资料与安全](/docs/use/profile) | +| 构建或定制应用 | [构建](/docs/build) | +| 管理系统 | [配置](/docs/configure) | diff --git a/content/docs/use/notifications.zh-Hans.mdx b/content/docs/use/notifications.zh-Hans.mdx new file mode 100644 index 0000000..bb3bc90 --- /dev/null +++ b/content/docs/use/notifications.zh-Hans.mdx @@ -0,0 +1,64 @@ +--- +title: 通知 +description: 不必守着收件箱,也能接住路由给你的一切 —— 审批、摘要和更新。 +--- + +# 通知 + +有事需要你时,ObjectOS 会告诉你:一条审批落到你桌上、一份定时摘要总结了你的项目、一条你关注的记录发生了变化。这些消息会出现在三个地方,从最快速的一瞥到完整的历史: + +| 界面 | 在哪里 | 适合什么 | +|---|---|---| +| **铃铛** | 每个页面的顶栏 | 快速看一眼"有新消息吗?"—— 红色角标显示未读数量 | +| **Needs your attention**(待你处理) | Console 首页 | 你的晨间分拣 —— 近期事项,并标注已等待多久 | +| **完整收件箱** | **Account → Inbox → Notifications**(账户 → 收件箱 → 通知) | 阅读、搜索和逐条处理所有通知 | + +## 铃铛与首页 + +无论你在 Console 的哪个位置,**铃铛图标**都在顶栏。上面的红色角标统计你的未读通知 —— 点击即可查看新消息,无需离开当前页面。 + +Console 首页还额外提供 **Needs your attention** 列表:待处理的通知和审批,各自标注时间,让你一眼看出哪条最新、哪条等得最久。 + +> **提示:**每天从 Console 首页开始。**Needs your attention** 加上 **Recently Accessed**,通常就是你投入工作前所需的全部分拣。 + +## 完整收件箱 + +想看全貌,打开 **Account → Inbox → Notifications** —— 在应用切换器中进入 **Account** 应用,再点侧边栏的 **Inbox**。默认视图以列表形式显示你自己的通知: + +| 列 | 告诉你什么 | +|---|---| +| **Title**(标题) | 发生了什么,一行说清 | +| **Topic**(主题) | 这是哪一类消息 —— 比如项目摘要还是审批 | +| **Severity**(严重级别) | 有多紧急(Info 及以上) | +| **Created At**(创建时间) | 它是什么时候到的 | + +这是一个普通的列表视图,所以你对列表的所有认知都适用:过滤它、排序它、在其中搜索。 + +## 通知从哪里来 + +这些都不需要你配置 —— 通知之所以到达,是因为你的应用里有什么东西认为你应该知道: + +| 来源 | 示例 | +|---|---| +| **自动化** | 每天早上总结你未完成项目任务的定时摘要 | +| **审批** | 一条请求被指派给你来决定,或者你自己的送审被批准或拒绝 | +| **订阅** | 你关注的对象或记录发生变化 | + +## 屏蔽吵闹的对象 + +如果某个对象对你来说噪音多于信号,把它静音:打开该对象的列表,点击列表头部的**铃铛静音开关**。你将不再收到这个对象的通知;再点一次即可取消静音。静音按对象生效,并且只影响*你* —— 你的同事照常收到他们的通知。 + +> **提示:**与其无视,不如静音。静音后的对象完全照常可用 —— 你只是不再被它打扰 —— 而你的未读角标也重新变得有意义。 + +## 桌面通知 + +如果你使用 ObjectOS 桌面应用,它可以把应用内通知镜像为操作系统的原生通知 —— 与其他桌面应用一样的横幅和通知中心条目。即使 Console 窗口在后台,你也能看到新事项,点击即可直达。 + +## 下一步 + +| 现在做什么 | 阅读 | +|---|---| +| 处理通知指向的审批 | [审批](/docs/use/approvals) | +| 像操作任何列表一样过滤和排序收件箱 | [视图](/docs/use/views) | +| 调整我的个人设置 | [个人资料与设置](/docs/use/profile) | +| 以日常用户身份导览 Console | [使用 ObjectOS](/docs/use) | diff --git a/content/docs/use/profile.zh-Hans.mdx b/content/docs/use/profile.zh-Hans.mdx new file mode 100644 index 0000000..f9567c4 --- /dev/null +++ b/content/docs/use/profile.zh-Hans.mdx @@ -0,0 +1,75 @@ +--- +title: 个人资料与设置 +description: 让 ObjectOS 更像你的 —— 你的姓名和头像、你的主题和语言,以及对每个已登录会话的掌控。 +--- + +# 个人资料与设置 + +所有个人化的东西都在两个地方:右上角的**头像菜单**(快捷偏好设置,每个页面都能打开)和 **Account**(账户)应用(你的个人资料、安全和收件箱)。这里的任何改动都不影响别人 —— 这些是你的设置,只属于你的账户。 + +| 我想…… | 去哪里 | +|---|---| +| 修改姓名或头像 | **Account → Profile**(账户 → 个人资料) | +| 修改密码 | **Account → Profile** → Change Password(修改密码) | +| 切换浅色/深色模式或语言 | 头像菜单 → Preferences(偏好设置) | +| 查看或退出我的其他会话 | **Account → Security**(账户 → 安全) → Active Sessions(活跃会话) | +| 查看关联了哪些登录方式 | **Account → Security** → Linked Accounts(已关联账号) | +| 退出登录 | 头像菜单 → **Log out**(退出登录) | + +## 你的个人资料 + +打开 **Account → Profile** —— 通过应用切换器进入 **Account** 应用,或通过头像菜单的 **Profile** 入口。 + +| 设置项 | 能改吗? | +|---|---| +| **头像** | 能 —— 点击 **Upload**(上传)选一张图片。凡是你出现的地方,同事都会看到它:任务指派、活动流、审批。 | +| **姓名** | 能 —— 在 Personal Information(个人信息)卡片中编辑。 | +| **邮箱** | 不能 —— 邮箱是你账户的标识,无法在这里更改。 | +| **角色** | 只读 —— 显示管理员给你分配的角色。 | + +点击 **Save Changes**(保存更改)使编辑生效。 + +Personal Information 下方是 **Change Password**(修改密码)卡片:输入当前密码和新密码,就完成了。你的其他设备会保持登录直到各自会话结束 —— 想彻底清场,就从 **Active Sessions** 把它们退出。 + +## 偏好设置:主题与语言 + +点击右上角你的头像。在你的姓名和邮箱旁边就是偏好设置: + +| 偏好项 | 选项 | +|---|---| +| **Theme**(主题) | 浅色或深色 —— 整个 Console 立即跟随。 | +| **Language**(语言) | 选择 Console 界面的显示语言。 | + +两者都即时生效;没有保存按钮,也不用刷新。 + +## 安全 + +打开 **Account → Security**,这里有两样值得时不时检查的东西: + +| 板块 | 用来做什么 | +|---|---| +| **Linked Accounts**(已关联账号) | 与你账户连接的社交或企业登录方式(SSO)—— 一眼看清关联了什么。 | +| **Active Sessions**(活跃会话) | 当前以你的身份登录的每台设备和浏览器。检查这份列表,把你不认识或不再使用的会话**退出**。 | + +> **提示:**在共用或借来的机器上登录后忘了退出?你不需要那台机器 —— 在任何设备上从 **Active Sessions** 撤销那个会话即可。 + +## Account 应用里还有 + +| 板块 | 用来做什么 | +|---|---| +| **Inbox → My Organizations**(收件箱 → 我的组织) | 你的账户所属的组织。 | +| **Inbox → Notifications / Approvals** | 你的个人收件箱 —— 详见[通知](/docs/use/notifications)和[审批](/docs/use/approvals)。 | +| **Developer → API Keys, OAuth Applications**(开发者 → API Key、OAuth 应用) | 用于把外部工具接入 ObjectOS 的凭证。**大多数用户永远用不到这个板块** —— 如果你不确定自己是否需要,那就是不需要。 | + +## 退出登录 + +打开头像菜单,点击 **Log out**。这会结束你当前设备上的会话;其他设备会保留各自的会话,直到你从 **Active Sessions** 把它们退出。 + +## 下一步 + +| 现在做什么 | 阅读 | +|---|---| +| 让收件箱井井有条 | [通知](/docs/use/notifications) | +| 处理路由给你的请求 | [审批](/docs/use/approvals) | +| 高效使用你的数据 | [记录](/docs/use/records) | +| 以日常用户身份导览 Console | [使用 ObjectOS](/docs/use) | diff --git a/content/docs/use/records.zh-Hans.mdx b/content/docs/use/records.zh-Hans.mdx new file mode 100644 index 0000000..9b6d3c6 --- /dev/null +++ b/content/docs/use/records.zh-Hans.mdx @@ -0,0 +1,82 @@ +--- +title: 记录操作 +description: 打开、阅读、创建和编辑支撑你日常工作的记录 —— 任务、项目、客户,什么都行。 +--- + +# 记录操作 + +ObjectOS 中的一切都是**记录** —— 一个任务、一个项目、一个客户、一张发票。本页教你如何打开一条记录、读懂记录页、创建新记录,以及一次编辑多条记录。 + +## 打开记录 + +在任何列表中,**记录的标题就是链接** —— 点击即可打开记录页。要返回时,使用记录顶部的返回链接(例如 **< All Tasks**),或左上角的面包屑。 + +## 记录页的结构 + +从上到下,一个记录页依次显示: + +| 元素 | 是什么 | +|---|---| +| **返回链接** | 带你回到来时的列表(如 **< All Tasks**) | +| **状态流水线条** | 仅出现在带工作流的对象上 —— 用箭头形阶段显示记录在流程中的位置 | +| **要点栏** | 记录的关键字段一览(如项目、负责人、优先级、截止日期、进度) | +| **操作按钮** | 为这条记录定义的操作,例如 **Log Time**(记录工时) | +| **字段分组** | 其余字段,按卡片分组(如 Overview、Schedule、Details) | +| **Show N empty fields**(显示 N 个空字段) | 分组末尾的开关 —— 空字段默认折叠,以保持页面简短 | + +### 读懂状态流水线 + +当对象带有工作流时,页面顶部的条会把每个阶段显示为一个箭头形:已完成的阶段带对勾(✓ Backlog → ✓ To Do → ✓ In Progress),**当前阶段高亮显示**。一眼就能看出记录进行到了哪一步。 + +> **提示:**如果一条记录看起来内容稀少,点击 **Show N empty fields** —— 字段是存在的,只是还没有值。 + +## 创建记录 + +点击任意列表顶部的 **+ New**(新建)。弹出一个模态框(例如 *"Create Task — Add a new Task to your database"*)。表单中: + +| 你会看到 | 如何使用 | +|---|---| +| 标有 `*` 的字段 | 必填 —— 不填就无法保存 | +| **Select…** 选择器 | 指向另一条记录的查找(lookup)字段(一个项目、一个用户)。输入即可搜索,或用表格浏览按钮从完整列表中挑选。 | +| 下拉框 | 状态、优先级之类的选项字段 —— 选一个值 | +| 日期选择器 | 点击日期字段,从日历中选一天 | + +最后点 **Create**(创建)完成,或点 **Cancel**(取消)放弃。 + +## 内联编辑记录 + +改动记录不必逐条打开。在表格视图中,打开工具栏上的 **Edit inline**(内联编辑)开关 —— 单元格变为可编辑,你可以像用电子表格一样,直接在列表里修正状态、重新指派任务或更新日期。 + +## 行内操作菜单 + +表格中的每一行末尾都有一个 `⋮` **操作**菜单。它包含对这单条记录可用的操作 —— 无需先打开记录的快捷方式。 + +## 一次选中多条记录 + +表格每行的开头都有一个**复选框**: + +1. 勾选你要的行(或勾选表头复选框选中整页)。 +2. 该对象的**批量操作** —— 列表顶部的按钮,如 **Reassign…**(重新指派)—— 现在会作用于你选中的所有记录。 + +> **提示:**批量操作因对象而异;它们就是列表头部 **+ New** 和 **Import** 旁边的那些按钮。 + +## 列表头部全览 + +每个列表上方都有: + +| 按钮 | 作用 | +|---|---| +| **+ New** | 创建记录(即上文描述的模态框) | +| **Import**(导入) | 从文件批量导入记录,不必逐条手输 | +| 自定义批量操作 | 对象特有的操作,如 **Reassign…**,作用于你选中的记录 | +| 铃铛静音开关 | 屏蔽这个对象的通知 —— 参见[通知](/docs/use/notifications) | + +## 下一步 + +| 我想…… | 阅读 | +|---|---| +| 改变列表本身的样子 —— 过滤、分组、切换到看板 | [使用视图](/docs/use/views) | +| 看汇总和趋势,而不是一行行记录 | [仪表盘](/docs/use/dashboards) | +| 处理等我审批的记录 | [审批](/docs/use/approvals) | +| 控制哪些更新会通知我 | [通知](/docs/use/notifications) | +| 回到 Console 导览 | [使用 ObjectOS](/docs/use) | diff --git a/content/docs/use/views.zh-Hans.mdx b/content/docs/use/views.zh-Hans.mdx new file mode 100644 index 0000000..29094a8 --- /dev/null +++ b/content/docs/use/views.zh-Hans.mdx @@ -0,0 +1,86 @@ +--- +title: 使用视图 +description: 把同一批记录看成表格、看板、日历或时间线 —— 并按你的方式过滤、分组和排序。 +--- + +# 使用视图 + +**视图**是查看一个对象记录的已保存方式。同一批任务可以显示为电子表格式的表格、看板或日历 —— 你可以自由切换,数据本身不会有任何变化。 + +## 视图标签页 + +每个列表的顶部都会以标签页形式列出该对象的视图,例如: + +**All Tasks ★ | In Progress | Urgent | Done | … 9 more | +** + +| 标签页元素 | 含义 | +|---|---| +| **★** | 默认视图 —— 最先打开的那个 | +| 具名标签页(*In Progress*、*Urgent*) | 为你的团队预设的已过滤视图。标签页上的小漏斗角标表示它带有内置过滤条件。 | +| **… 9 more** | 视图多到放不下时的溢出菜单 | +| **+** | 创建你自己的**个人视图** —— 你的过滤条件、你的列,只为你保存 | + +> **提示:**与其每天早上重复设置同一个过滤条件,不如点一次 **+**,把它存成个人视图。 + +## 切换视图类型 + +右侧工具栏显示当前视图类型(如 **Grid**)。点击它可以在六种查看同一批记录的方式之间切换: + +| 类型 | 最适合 | +|---|---| +| **Grid**(表格) | 日常表格 —— 浏览、排序和批量编辑记录 | +| **Kanban**(看板) | 进行中的工作 —— 卡片在看板上的阶段之间流动 | +| **Gallery**(画廊) | 图片很重要的记录 —— 产品、设计、人员 | +| **Calendar**(日历) | 任何带日期的东西 —— 截止日期、事件、预订,按月或按周查看 | +| **Timeline**(时间线) | 按时间顺序排列的记录流 | +| **Gantt**(甘特图) | 项目计划 —— 用条形显示开始、结束和持续时间 | + +## 视图工具栏 + +视图类型切换器旁边是: + +| 控件 | 作用 | +|---|---| +| **Edit inline**(内联编辑) | 在表格中开关电子表格式的单元格编辑 —— 参见[记录操作](/docs/use/records) | +| **Filter**(过滤) | 只显示符合你所选条件的记录 | +| **Group**(分组) | 把行按标题归类(如按项目或负责人) | +| **Sort**(排序) | 给记录排序;角标显示当前生效的排序规则数量 | +| 列设置 | 选择显示哪些列以及列的顺序 | +| 放大镜 | **视图内搜索** —— 输入即可收窄当前列表 | + +> **提示:**视图内搜索只过滤你所在的列表;`⌘K` / `Ctrl-K` 搜索所有对象的记录。知道在*哪个*列表时用放大镜,不知道时用 `⌘K`。 + +## 读懂表格 + +表格在单元格里塞进了不少额外信号: + +| 你会看到 | 它表示 | +|---|---| +| 行复选框和 `#` 序号 | 选中行以进行批量操作;一眼数清行数 | +| 彩色胶囊标签 | 状态、优先级之类选项字段的值 —— 颜色跟随取值变化 | +| 红色日期("Overdue 6d") | 日期已过 —— 逾期了那么多天 | +| 进度条 | 以条形绘制的完成百分比字段 | +| 行末的 `⋮` | 行内操作菜单 | +| 底部计数("10 records") | 当前视图包含多少条记录 | + +## 看板 + +切换到 **Kanban**,记录就变成**分列排布的卡片**。列来自该对象的某个选项字段 —— 对任务来说通常是状态,于是你会看到 *Backlog*、*To Do*、*In Progress*、*In Review* 等等: + +| 看板元素 | 显示什么 | +|---|---| +| 列头 | 阶段名称和该列卡片的**数量** | +| 卡片 | 记录标题加关键信息 —— 负责人、一个优先级胶囊标签 | +| 工具栏 | 与表格相同的 **Filter / Group / Sort** 控件 | + +看板一眼就能回答"每个阶段压了多少工作?"—— 列上的数字就是你的瓶颈探测器。 + +## 下一步 + +| 我想…… | 阅读 | +|---|---| +| 打开并编辑视图背后的记录 | [记录操作](/docs/use/records) | +| 看聚合数字,而不是一行行记录 | [仪表盘](/docs/use/dashboards) | +| 处理审批请求 | [审批](/docs/use/approvals) | +| 调整铃铛告诉我什么 | [通知](/docs/use/notifications) | +| 回到 Console 导览 | [使用 ObjectOS](/docs/use) |