# 组织与用户初始化架构

这篇文档说明 Xpert 当前“组织创建”和“用户加入组织”两条初始化链路的触发点、异步处理方式、幂等设计和可配置项。

## 设计目标

这套初始化架构解决的是 4 个问题：

- 新建组织后，自动创建组织默认 workspace
- 新用户加入组织后，自动创建该用户在该组织下的个人 workspace
- 组织初始化时，为分析模块创建语义模型基础数据
- 整个初始化过程异步执行、可重试、可重复投递而不产生重复数据

## 核心原则

### 1. 用户初始化不挂在裸 `user.created`

系统采用“组织内每用户一个 workspace”的模型，因此 AI 侧初始化的真实触发点不是单纯的用户创建，而是用户成功加入某个组织。

这意味着：

- 仅创建 `User` 记录时，不做 workspace 初始化
- 只有 `UserOrganization` 关系真正创建成功后，才触发用户侧 bootstrap

对应内部事件为：

- `user.organization.created`

### 2. 组织初始化必须在初始成员关系落库后触发

组织初始化依赖组织 owner 和当前组织成员，因此 `organization.created` 必须在以下动作都完成后再发出：

- `Organization` 已创建
- 初始 `UserOrganization` 关系已创建

这样后续的默认 workspace 成员同步和 owner 选择才是稳定的。

### 3. 所有初始化任务都走异步队列

组织创建和用户加入组织的主流程不等待 bootstrap 完成。

事件发出后：

- `server-ai` 监听事件并入队
- `analytics` 监听组织创建事件并入队
- 后续由各自的 queue processor 在后台消费

这样主链路更短，失败也不会回滚业务主事务。

### 4. 所有组织感知写操作都必须带 request context

当前仓库里不少 service 是 organization-aware 的。

如果异步任务直接写库而不显式补齐组织上下文，`organizationId` 可能被覆盖成 `null`。

因此异步 bootstrap 中所有写库逻辑都必须包在：

```ts
runWithRequestContext(
  {
    user,
    headers: {
      'organization-id': organizationId
    }
  },
  () => {
    // bootstrap writes
  }
)
```

## 事件模型

当前引入了两个内部事件：

### `organization.created`

```ts
{
  tenantId: string
  organizationId: string
  ownerUserId?: string | null
}
```

触发时机：

- 组织创建完成
- 初始组织成员关系完成

### `user.organization.created`

```ts
{
  tenantId: string
  organizationId: string
  userId: string
  bootstrapPersonalWorkspace: boolean
}
```

触发时机：

- `UserOrganization` 关系首次创建成功后

说明：

- 该事件由统一的“加入组织”写入层发出，而不是只挂在注册流程
- 这样“已有用户后来加入另一个组织”也能触发个人 workspace 初始化
- `bootstrapPersonalWorkspace=false` 用于组织创建时给租户 super admin 建组织成员关系但不创建个人 workspace

## 总体流程

```mermaid
flowchart TD
  A["Create organization"] --> B["Persist Organization"]
  B --> C["Persist initial UserOrganization"]
  C --> D["Emit organization.created"]
  D --> E["server-ai enqueue org-bootstrap:{organizationId}"]
  D --> F["analytics enqueue org-bootstrap:{organizationId}"]
  E --> G["Run in organization request context"]
  F --> H["Run in organization request context"]
  G --> I["Create org default workspace and environment"]
  I --> J["Register default skill repositories from template yaml"]
  J --> K["Sync organization members"]
  K --> L["Import template xperts"]
  E --> M["Enqueue org-skill-repository-sync jobs"]
  M --> N["Async full index sync for each repository"]
  H --> O["Initialize semantic model data"]

  P["Create UserOrganization"] --> Q["Emit user.organization.created"]
  Q --> R["server-ai enqueue user-org-bootstrap:{organizationId}:{userId}"]
  R --> S["Run in organization request context"]
  S --> T["Optionally create personal workspace"]
  T --> U["Create personal default environment"]
  U --> V["Ensure user joins org default workspace"]
  V --> W["Enqueue default workspace skills bootstrap"]
  W --> X["Install configured default skills when indexes are ready"]
```

## `server-ai` 初始化职责

`server-ai` 处理两类任务：组织 bootstrap 和用户 bootstrap。

### 组织 bootstrap

组织初始化任务的职责包括：

- 幂等创建组织默认 workspace
- 为该 workspace 幂等创建默认 environment
- 从模板资产配置中幂等注册默认 skill repositories
- 为这些 repositories 异步入队 full index sync 任务
- 把当前组织成员全部加入组织默认 workspace
- 按环境变量导入默认模板 xperts

默认命名如下：

- 组织默认 workspace：`Default Workspace`
- 默认 environment：`Default`

组织默认 workspace 通过 `workspace.settings.system.kind = 'org-default'` 标记。

### 默认 skill repositories

组织 bootstrap 会从下面的模板文件补齐租户级默认 skill repositories：

```text
${XPERT_TEMPLATE_DIR}/skill-repositories.yaml
```

当前 YAML 结构如下：

```yaml
repositories:
  - name: anthropics/skills
    provider: github
    options:
      url: https://github.com/anthropics/skills
      branch: main
      path: skills
  - name: clawhub/official
    provider: clawhub
    options:
      registryUrl: https://clawhub.ai
      officialOnly: true
      maxSkills: 500
```

说明：

- 该文件会 seed 到 `XPERT_TEMPLATE_DIR`
- 如果所有默认 skill 都通过 `skill-packages/*/` 提供，这个文件可以保持为空
- 每个条目支持 `name`、`provider`、`options`、`credentials` 等 repository 字段
- bootstrap 以 `name + provider` 为幂等键查找已有 repository
- 若已存在则执行更新，不会重复创建
- 模板配置中后续删除的条目不会反向删除组织里已存在的自定义 repository

repository 注册完成后，会为每个 repository 单独入队一个异步 sync job，并执行 full index sync。这样既能保证组织创建主链路足够轻，也能让默认 skills 在 bootstrap 后尽快可用。

### 用户 bootstrap

用户加入组织后的初始化职责包括：

- 当 `bootstrapPersonalWorkspace=true` 时，为该用户在当前组织下幂等创建个人 workspace
- 为个人 workspace 幂等创建默认 environment
- 把该用户幂等加入组织默认 workspace
- 当个人 workspace 是本次新创建时，异步把配置好的默认 skills 安装进去

个人 workspace 通过以下字段标记：

```ts
settings.system.kind = 'user-default'
settings.system.userId = userId
```

个人 workspace 名称规则为：

```ts
${displayName || email} Workspace
```

默认 workspace skills 通过外部模板资产目录中的文件配置：

```text
${XPERT_TEMPLATE_DIR}/workspace-defaults.yaml
```

当前 YAML 结构如下：

```yaml
userDefault:
  skills: []
```

说明：

- 该文件会和 `skills-market.yaml`、`skill-repositories.yaml` 一起 seed 到 `XPERT_TEMPLATE_DIR`
- ref 只使用机器字段：`provider`、`repositoryName`、`skillId`
- 只有同时出现在 `skills-market.yaml` 里的 ref 才会进入默认安装链路
- 已存在的个人 workspace 不会自动回填；默认安装 job 只针对新创建的 `user-default` workspace
- 本地 bundle skill 从 `skill-packages/*/bundle.yaml` 读取，同目录放置 `SKILL.md` 和资源文件
- 用户 bootstrap 会为这些安装动作单独入队异步 job，不会阻塞 workspace 创建主链路
- 未解析到的 ref 会在 repository index 可用前持续重试，重复安装按 `workspaceId + skillIndexId` 幂等收敛

### 为什么要有 `ensureMember`

组织默认 workspace 的成员会在两处被更新：

- 组织 bootstrap 时批量同步当前成员
- 用户加入组织时增量加入该用户

如果继续使用“整表替换 members”的写法，并发下容易互相覆盖。

因此实现上新增了增量接口：

- `ensureMember(workspaceId, userId)`

它只负责“如果不存在则加入”，不重写整份成员列表。

## Membership 驱动的 Copilot 访问语义

Copilot model 的可用性由当前用户或计费用户的有效 membership plan scope 决定，而不是由“当前组织 scope 是否配置了 copilot model”直接决定。

核心规则：

- tenant scope 请求只使用 tenant scope membership。
- organization scope 请求优先使用当前组织 scope membership。
- 当前组织没有 active membership plan 时，才回退 tenant scope membership。
- 当前组织只要存在 active membership plan，就进入组织自管模式；即使组织暂时没有 enabled local copilots，也不会回退 tenant models。
- tenant copilot 只能被 tenant membership 使用、限流和记账。
- organization copilot 只能被同一 organization membership 使用、限流和记账。
- `includedPoints = null` 表示 unlimited plan：不触发总点数上限，但仍写 usage ledger，也仍执行显式 `rateLimits`。

### 组织 membership 初始化

组织 membership 初始化是幂等的。初始化入口会确保当前组织有一个 active default plan，并给全部 active `UserOrganization` 成员补齐 active `UserMembership`。

默认 plan 为：

```ts
{
  code: 'default-unlimited',
  name: 'Default Unlimited',
  includedPoints: null,
  tokensPerPoint: 1000,
  isDefault: true,
  status: 'active'
}
```

初始化策略：

- 已有 active default plan：直接使用它。
- 有 active plan 但没有 default：把第一个 active plan 设为 default。
- 只有 archived `default-unlimited`：重新激活并设为 default。
- 没有可用 plan：创建新的 `default-unlimited`。
- 成员补齐只给缺少 active membership 的 active organization members 创建记录；已有 active membership 不覆盖。
- unlimited assignment ledger 的 `pointsDelta = 0`。

### 运行时自愈

为兼容旧数据，运行时允许一次幂等自愈：

- 如果 organization scope 下存在 enabled local copilots，但组织没有 membership plan，查询可用模型时会先初始化组织 membership，再只返回组织 local models。
- 如果用户直接调用 organization local copilot，`assertCanUse` / `recordUsage` 发现 `copilotOrganizationId === request organizationId` 且组织 membership 缺失时，会先初始化组织 membership，再重新校验。
- 如果请求的是 tenant copilot，但当前有效 membership 是 organization scope，则拒绝使用。
- 如果请求的是 organization copilot，但当前有效 membership 是 tenant scope，也拒绝使用，除非它属于当前组织并触发了上面的组织初始化自愈。

因此，组织管理员在首次登录向导或设置页中移除 tenant membership plan，并启用组织自有 copilot models 后，系统会自动进入组织自管 membership 模式，聊天和 ledger 都落在 organization scope。

### Scope 状态与管理入口

后端提供两个 scope 管理接口：

- `GET /api/membership/scope/status`
- `POST /api/membership/scope/initialize`

这两个接口使用当前 request scope，并要求 `AIPermissionsEnum.MEMBERSHIP_EDIT`。

`/settings/membership` 是 dual-scope 页面：

- tenant scope：展示租户计划状态和计划目录。
- organization scope：展示组织自管状态、default plan、active members、assigned members、local copilot models 数量。
- 未初始化时显示 “Initialize organization membership”。
- 已有 plan 但成员 membership 不完整时显示 “Repair assignments”。

组织 local admin 只要具备 `MEMBERSHIP_EDIT` 权限即可访问组织 scope 的会员页面。

### 用户加入组织后的补齐

用户 bootstrap 会调用 membership 补齐逻辑：

- 如果当前组织已经有 active membership plan，则为新加入用户补 active membership。
- 如果当前组织没有 active plan，不会创建 plan，也不会把普通组织误切换为自管 membership 模式。

## Workspace 访问语义

Xpert workspace 现在明确区分两种访问语义：

- **Runtime / 使用语义**：用于聊天、ClawXpert、assistant binding、已发布 Xpert 的读取和运行。组织 scope 用户可以读取和运行 tenant-shared workspace 中已发布的 Xpert。
- **Authoring / 编辑语义**：用于 `/xpert/w` 工作空间首页、workspace 下拉、创建 Xpert、安装 skill / plugin resource、编辑 workspace 资源和设置默认 workspace。组织 scope 用户只能看到和操作当前组织内 owner/member workspace，不会看到 tenant-shared workspace。

后端接口约定：

- `GET /api/xpert-workspace/my` 默认按 `purpose=runtime` 返回，保持向后兼容。
- `GET /api/xpert-workspace/my?purpose=authoring` 只返回当前用户可作为编辑/写入目标的 workspace。
- `GET /api/xpert-workspace/my/default?purpose=authoring` 会忽略不可编辑的 tenant-shared 默认 workspace，并回退到可编辑 workspace。
- `POST /api/xpert-workspace/:id/default` 要求 authoring/write access；组织 scope 用户不能把 tenant-shared workspace 设为默认。
- workspace 下的资源列表和安装类接口，例如 xperts、toolsets、knowledgebases、skills、prompt workflows、plugin resources、template install，按 authoring 语义校验目标 workspace。

权限结果保持分层：

- 组织 scope 用户访问 tenant-shared workspace 时仍然是 `canRead=true`、`canRun=true`。
- 同一用户在组织 scope 下对 tenant-shared workspace 的 `canWrite=false`、`canManage=false`。
- tenant scope 的 owner/admin 仍可管理 tenant-shared workspace。

因此 `/api/assistant-binding/xperts`、published Xpert runtime access、`assertCanRun` 等使用链路不受 workspace authoring 收紧影响。将来如果需要让组织用户浏览 tenant-shared 公共资源，应新增独立入口，不复用 `/xpert/w` 这种 workspace 编辑页。

## 模板 xpert 导入

组织默认 xpert 导入由环境变量控制：

```bash
ORG_DEFAULT_XPERT_TEMPLATE_KEYS=id1,id2,id3
```

它的行为是：

- 逐个读取模板详情
- 解析模板 YAML
- 把导入目标 workspace 改成组织默认 workspace
- 在导入结果上写入 bootstrap 元数据

用于幂等识别的字段为：

```ts
options.bootstrap = {
  source: 'template',
  templateKey: string,
  workspaceKind: 'org-default'
}
```

导入前，系统会先按以下条件查找是否已经导入过：

- `workspaceId`
- `options.bootstrap.templateKey`
- `options.bootstrap.workspaceKind = 'org-default'`
- `latest = true`
- `deletedAt IS NULL`

查到已有记录时，执行覆盖导入；否则创建新 xpert。

### 模板重名处理

当前已发布 xpert 的名称校验仍然接近全局唯一，因此组织导入时采用固定降级规则：

1. 原模板名
2. `模板名 (组织名)`
3. `模板名 (组织名 organizationId前8位)`

## `analytics` 初始化职责

`analytics` 当前只监听组织创建事件。

它会复用分析模块现有 seed 逻辑，但拆成更细粒度、可复用的组织初始化入口。

默认模式为：

- `semantic-only`

可选模式为：

- `full-demo`

对应环境变量：

```bash
ORG_ANALYTICS_BOOTSTRAP_MODE=semantic-only
```

### `semantic-only` 会做什么

该模式只初始化语义模型运行所必需的数据：

- data source
- business area
- business-area-user
- semantic model
- semantic model roles
- catalog update

### `full-demo` 会额外做什么

在 `semantic-only` 的基础上额外导入：

- demo indicators
- demo story

## 幂等与重试设计

这套初始化链路默认假设“事件可能重复投递”。

因此每层都做了幂等处理。

### 队列幂等键

- 组织 bootstrap：`org-bootstrap:${organizationId}`
- 组织 skill repository sync：`org-skill-repository-sync:${organizationId}:${repositoryId}`
- 用户 bootstrap：`user-org-bootstrap:${organizationId}:${userId}`

### 记录级幂等标记

- 组织默认 workspace：`settings.system.kind = 'org-default'`
- 用户默认 workspace：`settings.system.kind = 'user-default'` + `settings.system.userId`
- 模板导入 xpert：`options.bootstrap.templateKey` + `options.bootstrap.workspaceKind`

### 行为级幂等

- environment 创建前先查默认 environment
- 默认 skill repositories 按 `name + provider` 执行 upsert
- workspace 成员加入使用 `ensureMember`
- analytics seed 逻辑按组织范围查重

## 失败语义

初始化失败不会回滚组织创建或用户加入组织主流程。

当前失败策略为：

- 主流程成功即可返回
- bootstrap 后台重试
- 失败记录结构化日志
- 允许后续重复投递重新收敛到目标状态

## 入口与扩展点

如果你要扩展初始化逻辑，建议按下面的边界继续演进：

### 适合放进组织 bootstrap 的能力

- 组织级默认 workspace 初始化
- 组织级默认 environment 初始化
- 组织级模板 xpert 导入
- 组织级分析语义模型初始化

### 适合放进用户 bootstrap 的能力

- 组织内个人 workspace 初始化
- 默认 environment 初始化
- 自动加入组织共享 workspace
- 后续可能的用户偏好或默认 assistant 绑定

### 暂时不要塞进这条链路的能力

- 已发布 xpert 的全局 slug 作用域改造
- 大量依赖外部系统的同步任务
- 需要强事务一致性的长链路写操作

这些能力更适合拆成独立工程，而不是和 bootstrap 链路绑死。

## 推荐部署配置

分析型组织比较适合的起步配置如下：

```bash
ORG_DEFAULT_XPERT_TEMPLATE_KEYS=af7133cb-32b3-47ff-90c1-b144c4d4887e,af7133cb-32b3-47ff-90c1-b144c4d48872
ORG_ANALYTICS_BOOTSTRAP_MODE=semantic-only
```

上面的两个模板分别对应：

- `ChatBI with Sales Analysis Expert`
- `Text2SQL-ChatDB`

## 一句话总结

当前的初始化架构本质上是：

`业务主流程发事件 -> AI/Analytics 异步入队 -> 在组织上下文中幂等收敛到目标状态`

这让系统既能自动完成组织和用户的初始化，又不会把主流程绑死在复杂的后台写操作上。
