Compare commits
131 Commits
3b11fe0d31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e29a936546 | |||
| bf309e590d | |||
| a47ca99a36 | |||
| f3c472e612 | |||
| f8d787cf50 | |||
| 5a810e8290 | |||
| 4b74659be1 | |||
| cb87488f0c | |||
| 6d9b3660db | |||
| 09acb5521c | |||
| 385b2a0ac2 | |||
| 9b86d53fe0 | |||
| 1172b29588 | |||
| 0837625451 | |||
| 8396b7a756 | |||
| ac164077d3 | |||
| ce4a437094 | |||
| add95357da | |||
| c6ec09ce0b | |||
| d367433306 | |||
| deeb89d54d | |||
| 13c20508e7 | |||
| 21ed101505 | |||
| f3c265b4e3 | |||
| ac1b74518e | |||
| 87f37122cb | |||
| 5a166d69f2 | |||
| 733bdfcd8d | |||
| a9f2c17f84 | |||
| 5ab0eaf038 | |||
| edb2af5cbc | |||
| 5a8bef520d | |||
| 77be66e26b | |||
| ca051a5f7d | |||
| efe6f0debf | |||
| 3749d9de06 | |||
| bf70b6636c | |||
| c7977b398e | |||
| dcd35db310 | |||
| 455da9d02f | |||
| 7457cecd0a | |||
| 3c74d8d042 | |||
| ed8775f3a0 | |||
| 30587bd8a8 | |||
| 48776a3355 | |||
| 44d06e9368 | |||
| 5729d80ac2 | |||
| 5bf0d93938 | |||
| 1c70cab082 | |||
| 29f7086b66 | |||
| d1c9b8c8c5 | |||
| f97dd79e00 | |||
| 6ee4afa1e1 | |||
| 99b13b99fc | |||
| 944c77e37b | |||
| bbff1c9af9 | |||
| 9d28a465a5 | |||
| ceec514ead | |||
| 714482db16 | |||
| 574e0e74ef | |||
| f183e50413 | |||
| f6b74335dc | |||
| 3bd11fc5b3 | |||
| 358064b810 | |||
| 38eb704df3 | |||
| c35a1e4ec4 | |||
| 001a82fb9d | |||
| 9eb61d9b73 | |||
| c5c6ad347b | |||
| 7a216628d5 | |||
| 7f73607c32 | |||
| f81f9419e0 | |||
| a0be5d6b36 | |||
| 9d2a330f69 | |||
| 271e712804 | |||
| 1c386e0a80 | |||
| 34442663a3 | |||
| 5a2462a49e | |||
| 86ec39f7d2 | |||
| 71ac0f91c6 | |||
| 0f7b99c687 | |||
| b7d66f334a | |||
| b7c9e34738 | |||
| 07d8b20f57 | |||
| 1b568757cb | |||
| 7cf0c50921 | |||
| bccd942898 | |||
| 33d101af22 | |||
| 01090273c6 | |||
| 5b28ad52bb | |||
| 8534c530c8 | |||
| 41a4172267 | |||
| ec796ae609 | |||
| 0731778bd3 | |||
| b014767324 | |||
| 11aa538793 | |||
| 24cbee3135 | |||
| 7e458ad6d3 | |||
| 37ec670280 | |||
| 8ca5d68ba4 | |||
| 3795aea2cb | |||
| 676e838697 | |||
| ab01a83a90 | |||
| 670762aa7a | |||
| 2ec50f3234 | |||
| fa5d0d31b2 | |||
| 4b4755b33b | |||
| c08fa4756b | |||
| 325e13ee13 | |||
| d3fdc3dd1e | |||
| ceaece754e | |||
| e53c943991 | |||
| 46f138328e | |||
| 2e2e217b5f | |||
| 7270256587 | |||
| 7f68a09486 | |||
| 7887a8d3be | |||
| e10d225063 | |||
| e381679165 | |||
| 0020df5d5e | |||
| 3ad8cc3a56 | |||
| 97528ce2c5 | |||
| 2871141d4c | |||
| ea32aeb819 | |||
| 07c8a0f99d | |||
| 4eaac38484 | |||
| c55666b481 | |||
| 88bec71cf8 | |||
| 026be99393 | |||
| 17dc9b9dba | |||
| 3910c0da9f |
23
.env.prod.example
Normal file
23
.env.prod.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# ---------- MySQL Center ----------
|
||||
MYSQL_CENTER_ROOT_PASSWORD=change-me-center-root
|
||||
MYSQL_CENTER_DATABASE=fabric_center
|
||||
MYSQL_CENTER_USER=fabric
|
||||
MYSQL_CENTER_PASSWORD=change-me-center-db
|
||||
|
||||
# ---------- MySQL Guild ----------
|
||||
MYSQL_GUILD_ROOT_PASSWORD=change-me-guild-root
|
||||
MYSQL_GUILD_DATABASE=fabric_guild
|
||||
MYSQL_GUILD_USER=fabric
|
||||
MYSQL_GUILD_PASSWORD=change-me-guild-db
|
||||
|
||||
# ---------- Center secrets ----------
|
||||
CENTER_SHARED_SECRET=change-me-center-shared-secret
|
||||
JWT_ACCESS_SECRET=change-me-jwt-access-secret
|
||||
JWT_REFRESH_SECRET=change-me-jwt-refresh-secret
|
||||
|
||||
# ---------- Guild auth ----------
|
||||
FABRIC_API_KEY=change-me-fabric-api-key
|
||||
|
||||
# ---------- Optional webhook ----------
|
||||
FABRIC_WEBHOOK_URL=
|
||||
FABRIC_WEBHOOK_SECRET=
|
||||
46
.github/workflows/backend-ci.yml
vendored
Normal file
46
.github/workflows/backend-ci.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: backend-ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "Fabric.Backend.Center/**"
|
||||
- "Fabric.Backend.Guild/**"
|
||||
- ".github/workflows/backend-ci.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "Fabric.Backend.Center/**"
|
||||
- "Fabric.Backend.Guild/**"
|
||||
- ".github/workflows/backend-ci.yml"
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
- Fabric.Backend.Center
|
||||
- Fabric.Backend.Guild
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ matrix.service }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: ${{ matrix.service }}/package-lock.json
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
10
.gitmodules
vendored
10
.gitmodules
vendored
@@ -2,9 +2,13 @@
|
||||
path = Fabric.OpenclawPlugin
|
||||
url = https://git.hangman-lab.top/nav/Fabric.OpenclawPlugin.git
|
||||
branch = main
|
||||
[submodule "Fabric.Backend"]
|
||||
path = Fabric.Backend
|
||||
url = https://git.hangman-lab.top/nav/Fabric.Backend.git
|
||||
[submodule "Fabric.Backend.Center"]
|
||||
path = Fabric.Backend.Center
|
||||
url = https://git.hangman-lab.top/nav/Fabric.Backend.Center.git
|
||||
branch = main
|
||||
[submodule "Fabric.Backend.Guild"]
|
||||
path = Fabric.Backend.Guild
|
||||
url = https://git.hangman-lab.top/nav/Fabric.Backend.Guild.git
|
||||
branch = main
|
||||
[submodule "Fabric.Frontend"]
|
||||
path = Fabric.Frontend
|
||||
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/dist
|
||||
**/node_modules
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
Submodule Fabric.Android updated: f06855c37f...2e4688444c
Submodule Fabric.Backend deleted from 08b9424648
1
Fabric.Backend.Center
Submodule
1
Fabric.Backend.Center
Submodule
Submodule Fabric.Backend.Center added at 8412eb6b3e
1
Fabric.Backend.Guild
Submodule
1
Fabric.Backend.Guild
Submodule
Submodule Fabric.Backend.Guild added at ca20df7618
Submodule Fabric.Desktop updated: 395385c9aa...cb92835d2f
Submodule Fabric.Frontend updated: 642b81564d...607785ac0c
Submodule Fabric.OpenclawPlugin updated: 21475eb72b...fc2ab628b2
18
PLAN.md
18
PLAN.md
@@ -55,11 +55,15 @@
|
||||
- Guild 服务注册时需证明持有中心服务 secret(共享密钥握手)
|
||||
|
||||
## 5. 模块拆分(对应仓库)
|
||||
- `Fabric.Backend`
|
||||
- Auth/Workspace(Guild)
|
||||
- Chat Core(Channel、DM、消息;不含 Thread)
|
||||
- `Fabric.Backend.Center`
|
||||
- 用户身份与登录(Identity Hub)
|
||||
- Guild Node 注册与鉴权(共享密钥握手)
|
||||
- 中心级配置与审计
|
||||
- `Fabric.Backend.Guild`
|
||||
- Workspace/Guild/Channel/DM
|
||||
- Chat Core(消息、回复、编辑、删除、@;不含 Thread)
|
||||
- Integration Surface(Webhook、Bot Token、扩展回调)
|
||||
- Permission & Audit
|
||||
- Guild 级权限与审计
|
||||
- `Fabric.Frontend`
|
||||
- 工作区/频道 UI
|
||||
- 消息流、输入框、回复/编辑/删除/@
|
||||
@@ -81,7 +85,8 @@
|
||||
- 架构图、数据模型、接口草案
|
||||
|
||||
### Week 2:基础业务 API
|
||||
- 登录注册、工作区、频道、消息 REST API
|
||||
- Center:登录注册、Guild Node 注册鉴权 API
|
||||
- Guild:工作区、频道、消息 REST API
|
||||
- 基础前端页面(频道列表 + 消息流)
|
||||
|
||||
### Week 3:实时通信
|
||||
@@ -108,7 +113,8 @@
|
||||
- `Fabric`(主仓库)
|
||||
- 挂载子模块:
|
||||
- `Fabric.OpenclawPlugin`
|
||||
- `Fabric.Backend`
|
||||
- `Fabric.Backend.Center`
|
||||
- `Fabric.Backend.Guild`
|
||||
- `Fabric.Frontend`
|
||||
- `Fabric.Desktop`
|
||||
- `Fabric.Android`
|
||||
|
||||
106
README.md
106
README.md
@@ -1 +1,107 @@
|
||||
# Fabric
|
||||
|
||||
A self-hosted, Discord-like team chat platform with first-class **AI-agent**
|
||||
participation. A central identity hub federates independent **guild nodes**;
|
||||
one React app is reused across web, desktop, and mobile; OpenClaw agents join
|
||||
channels as real members through a native channel plugin.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Fabric.Backend │ identity hub (NestJS,
|
||||
│ .Center :7001 │ :7001) — users · JWT ·
|
||||
└─────────┬────────────┘ agent API keys · node
|
||||
│ registry · name resolve
|
||||
registers / │
|
||||
introspects ┌─────┴───────┐
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ Fabric.Backend │ │ Fabric.Backend │ guild nodes
|
||||
│ .Guild :7002 │ │ .Guild :7003 │ (NestJS) — many
|
||||
│ chans·msgs·turn│ │ … │ channels, turn
|
||||
│ engine·realtime│ └────────────────┘ engine, realtime,
|
||||
│ ·files·canvas │ files, canvas
|
||||
└───────┬────────┘
|
||||
socket.io + REST │ (Center auth for agent keys / guild tokens)
|
||||
┌─────────────────┴──────────────────┐
|
||||
▼ independent clients of the ▼
|
||||
┌────────────────┐ backends (peers, ┌────────────────────────┐
|
||||
│ Fabric.Frontend│ not linked) │ Fabric.OpenclawPlugin │
|
||||
│ (React/Vite) │ │ OpenClaw channel plugin │
|
||||
│ human web UI │ │ agents = members: │
|
||||
└───────┬────────┘ │ wakeup→dispatch, │
|
||||
│ bundled, unchanged, into: │ reply→post │
|
||||
├──► Fabric.Desktop (Electron) └────────────────────────┘
|
||||
└──► Fabric.Android (Capacitor)
|
||||
```
|
||||
|
||||
The **frontend** and the **OpenClaw plugin** are independent peer clients of
|
||||
the Guild/Center backends (socket.io + REST). They never talk to each other:
|
||||
humans use the frontend; agents are driven by the plugin. Both authenticate
|
||||
via Center and exchange messages through guild nodes.
|
||||
|
||||
## Repository layout (git submodules)
|
||||
|
||||
| Submodule | Role |
|
||||
|---|---|
|
||||
| `Fabric.Backend.Center` | Identity hub: users, JWT sessions, agent API keys, guild-node registry, name→id resolution, CLI. |
|
||||
| `Fabric.Backend.Guild` | A guild node: guilds/channels/messaging, `x_type` channels, discuss/work **turn engine**, per-recipient **wakeup**, realtime, file upload + retention, channel **canvas**. |
|
||||
| `Fabric.Frontend` | The React SPA (the actual UI). Served on the web, and bundled into Desktop & Android. |
|
||||
| `Fabric.Desktop` | Electron shell that **bundles** the frontend (self-contained). |
|
||||
| `Fabric.Android` | Capacitor shell that **bundles** the frontend. |
|
||||
| `Fabric.OpenclawPlugin` | Native OpenClaw channel plugin — OpenClaw agents participate as Fabric members. |
|
||||
|
||||
## Key concepts
|
||||
|
||||
- **Federation.** Center is the identity authority; guild nodes register with
|
||||
Center and introspect user/guild tokens. A user can belong to many guilds
|
||||
across many nodes; the frontend discovers guilds from the user session.
|
||||
- **Channel `x_type`.** Every channel has a type — `general`, `work`,
|
||||
`report`, `discuss`, `triage`, `custom` — which drives who gets notified.
|
||||
- **`wakeup` metadata.** On `message.created`, each recipient gets a per-push
|
||||
`wakeup` boolean. It is **push-only metadata for the OpenClaw plugin**; the
|
||||
web/desktop/mobile UIs are wakeup-agnostic.
|
||||
- **discuss/work turn engine** (server-side, in `Fabric.Backend.Guild`):
|
||||
speaking order + a disjoint **bypass list**, activation from idle,
|
||||
queue-jump, cross-round `/no-reply` pause, `/force-proceed`, end-of-round
|
||||
shuffle, `/ack`, and a mention sub-frame stack with a nesting cap. Only the
|
||||
woken speaker acts; everyone else just receives context.
|
||||
- **Agents = accounts.** Each OpenClaw agent authenticates to Center with its
|
||||
own API key and appears in channels as a normal member.
|
||||
- **ES modules everywhere.** Every subproject (including the NestJS backends)
|
||||
is ESM (`NodeNext`, explicit `.js` relative imports, CJS deps default-imported).
|
||||
|
||||
## Local stack
|
||||
|
||||
`docker-compose.local.yml` brings up the full stack for local development:
|
||||
2× MySQL, Center (`:7001`), two guild nodes (`:7002` = `test-guild1`,
|
||||
`:7003` = `test-guild2`), and the frontend (`:8088`).
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml up -d --build
|
||||
|
||||
# create a user via the Center CLI
|
||||
docker compose -f docker-compose.local.yml exec backend-center \
|
||||
node dist/cli.js user create --email you@t.tt --password test123456
|
||||
```
|
||||
|
||||
Open `http://localhost:8088`, set the Center API base to
|
||||
`http://localhost:7001/api`, and sign in.
|
||||
|
||||
> Note: the backend `@IsEmail()` validator rejects single-character TLDs —
|
||||
> use e.g. `you@t.tt`, not `you@tt.t`.
|
||||
|
||||
## Testing
|
||||
|
||||
`docs/TEST_POINTS.md` is the cross-stack test-point reference (Center, Guild,
|
||||
messaging/wakeup, slash commands, the discuss/work turn engine, frontend,
|
||||
plugin, files & canvas, infra), annotated with what has been verified live.
|
||||
|
||||
## Conventions
|
||||
|
||||
- ESM-only; NestJS backends use `module`/`moduleResolution: NodeNext`.
|
||||
- Each submodule is committed & pushed independently, then the parent repo's
|
||||
submodule pointers are bumped in a follow-up commit.
|
||||
- HTTPS git credentials are stored repo-locally under `.git/` and are never
|
||||
committed.
|
||||
|
||||
91
docker-compose.prod.yml
Normal file
91
docker-compose.prod.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
services:
|
||||
mysql-center:
|
||||
image: mysql:8.4
|
||||
container_name: fabric-mysql-center
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_CENTER_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_CENTER_DATABASE:-fabric_center}
|
||||
MYSQL_USER: ${MYSQL_CENTER_USER:-fabric}
|
||||
MYSQL_PASSWORD: ${MYSQL_CENTER_PASSWORD}
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- mysql_center_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_CENTER_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
mysql-guild:
|
||||
image: mysql:8.4
|
||||
container_name: fabric-mysql-guild
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_GUILD_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_GUILD_DATABASE:-fabric_guild}
|
||||
MYSQL_USER: ${MYSQL_GUILD_USER:-fabric}
|
||||
MYSQL_PASSWORD: ${MYSQL_GUILD_PASSWORD}
|
||||
ports:
|
||||
- "3308:3306"
|
||||
volumes:
|
||||
- mysql_guild_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_GUILD_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
backend-center:
|
||||
build:
|
||||
context: ./Fabric.Backend.Center
|
||||
dockerfile: Dockerfile
|
||||
container_name: fabric-backend-center
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql-center:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
FABRIC_BACKEND_CENTER_PORT: 7001
|
||||
FABRIC_BACKEND_CENTER_DB_HOST: mysql-center
|
||||
FABRIC_BACKEND_CENTER_DB_PORT: 3306
|
||||
FABRIC_BACKEND_CENTER_DB_USER: ${MYSQL_CENTER_USER:-fabric}
|
||||
FABRIC_BACKEND_CENTER_DB_PASSWORD: ${MYSQL_CENTER_PASSWORD}
|
||||
FABRIC_BACKEND_CENTER_DB_NAME: ${MYSQL_CENTER_DATABASE:-fabric_center}
|
||||
FABRIC_BACKEND_CENTER_DB_SYNC: "false"
|
||||
FABRIC_BACKEND_CENTER_DB_LOGGING: "false"
|
||||
FABRIC_BACKEND_CENTER_JWT_ACCESS_SECRET: ${FABRIC_BACKEND_CENTER_JWT_ACCESS_SECRET}
|
||||
FABRIC_BACKEND_CENTER_JWT_REFRESH_SECRET: ${FABRIC_BACKEND_CENTER_JWT_REFRESH_SECRET}
|
||||
ports:
|
||||
- "7001:7001"
|
||||
|
||||
backend-guild:
|
||||
build:
|
||||
context: ./Fabric.Backend.Guild
|
||||
dockerfile: Dockerfile
|
||||
container_name: fabric-backend-guild
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql-guild:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
FABRIC_BACKEND_GUILD_PORT: 7002
|
||||
FABRIC_BACKEND_GUILD_DB_HOST: mysql-guild
|
||||
FABRIC_BACKEND_GUILD_DB_PORT: 3306
|
||||
FABRIC_BACKEND_GUILD_DB_USER: ${MYSQL_GUILD_USER:-fabric}
|
||||
FABRIC_BACKEND_GUILD_DB_PASSWORD: ${MYSQL_GUILD_PASSWORD}
|
||||
FABRIC_BACKEND_GUILD_DB_NAME: ${MYSQL_GUILD_DATABASE:-fabric_guild}
|
||||
FABRIC_BACKEND_GUILD_DB_SYNC: "false"
|
||||
FABRIC_BACKEND_GUILD_DB_LOGGING: "false"
|
||||
FABRIC_BACKEND_GUILD_CENTER_BASE_URL: ${FABRIC_BACKEND_GUILD_CENTER_BASE_URL:-http://backend-center:7001}
|
||||
FABRIC_BACKEND_GUILD_NODE_ID: ${FABRIC_BACKEND_GUILD_NODE_ID:-guild-node-1}
|
||||
FABRIC_BACKEND_GUILD_CENTER_API_KEY: ${FABRIC_BACKEND_GUILD_CENTER_API_KEY}
|
||||
FABRIC_WEBHOOK_URL: ${FABRIC_WEBHOOK_URL:-}
|
||||
FABRIC_WEBHOOK_SECRET: ${FABRIC_WEBHOOK_SECRET:-}
|
||||
ports:
|
||||
- "7002:7002"
|
||||
|
||||
volumes:
|
||||
mysql_center_data:
|
||||
mysql_guild_data:
|
||||
89
docker-compose.yml
Normal file
89
docker-compose.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
services:
|
||||
mysql-center:
|
||||
image: mysql:8.4
|
||||
container_name: fabric-mysql-center
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: fabric_center
|
||||
MYSQL_USER: fabric
|
||||
MYSQL_PASSWORD: fabric
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- mysql_center_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
mysql-guild:
|
||||
image: mysql:8.4
|
||||
container_name: fabric-mysql-guild
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: fabric_guild
|
||||
MYSQL_USER: fabric
|
||||
MYSQL_PASSWORD: fabric
|
||||
ports:
|
||||
- "3308:3306"
|
||||
volumes:
|
||||
- mysql_guild_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
backend-center:
|
||||
build:
|
||||
context: ./Fabric.Backend.Center
|
||||
dockerfile: Dockerfile
|
||||
container_name: fabric-backend-center
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql-center:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
FABRIC_BACKEND_CENTER_PORT: 7001
|
||||
FABRIC_BACKEND_CENTER_DB_HOST: mysql-center
|
||||
FABRIC_BACKEND_CENTER_DB_PORT: 3306
|
||||
FABRIC_BACKEND_CENTER_DB_USER: fabric
|
||||
FABRIC_BACKEND_CENTER_DB_PASSWORD: fabric
|
||||
FABRIC_BACKEND_CENTER_DB_NAME: fabric_center
|
||||
FABRIC_BACKEND_CENTER_DB_SYNC: "true"
|
||||
FABRIC_BACKEND_CENTER_DB_LOGGING: "false"
|
||||
FABRIC_BACKEND_CENTER_JWT_ACCESS_SECRET: change-me-access
|
||||
FABRIC_BACKEND_CENTER_JWT_REFRESH_SECRET: change-me-refresh
|
||||
ports:
|
||||
- "7001:7001"
|
||||
|
||||
backend-guild:
|
||||
build:
|
||||
context: ./Fabric.Backend.Guild
|
||||
dockerfile: Dockerfile
|
||||
container_name: fabric-backend-guild
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql-guild:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
FABRIC_BACKEND_GUILD_PORT: 7002
|
||||
FABRIC_BACKEND_GUILD_DB_HOST: mysql-guild
|
||||
FABRIC_BACKEND_GUILD_DB_PORT: 3306
|
||||
FABRIC_BACKEND_GUILD_DB_USER: fabric
|
||||
FABRIC_BACKEND_GUILD_DB_PASSWORD: fabric
|
||||
FABRIC_BACKEND_GUILD_DB_NAME: fabric_guild
|
||||
FABRIC_BACKEND_GUILD_DB_SYNC: "true"
|
||||
FABRIC_BACKEND_GUILD_DB_LOGGING: "false"
|
||||
FABRIC_BACKEND_GUILD_CENTER_BASE_URL: http://backend-center:7001
|
||||
FABRIC_BACKEND_GUILD_NODE_ID: guild-node-1
|
||||
FABRIC_BACKEND_GUILD_CENTER_API_KEY: ${FABRIC_BACKEND_GUILD_CENTER_API_KEY:-}
|
||||
ports:
|
||||
- "7002:7002"
|
||||
|
||||
volumes:
|
||||
mysql_center_data:
|
||||
mysql_guild_data:
|
||||
59
docs/DEPLOY_AUTH_FLOW.md
Normal file
59
docs/DEPLOY_AUTH_FLOW.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Fabric 部署鉴权流程(Center / Guild / Frontend)
|
||||
|
||||
## 1) Admin 注册 Guild(在 Center 本机执行)
|
||||
|
||||
```bash
|
||||
scripts/register-guild-node.sh <node_id> <name> <endpoint>
|
||||
```
|
||||
|
||||
成功后会输出:
|
||||
|
||||
```bash
|
||||
FABRIC_BACKEND_GUILD_CENTER_API_KEY=...
|
||||
```
|
||||
|
||||
> `nodes/register` 仅允许 localhost 调用。
|
||||
|
||||
---
|
||||
|
||||
## 2) 部署 Guild
|
||||
|
||||
在 Guild 的部署配置(`.env` / compose)填写:
|
||||
|
||||
- `FABRIC_BACKEND_GUILD_CENTER_BASE_URL`
|
||||
- `FABRIC_BACKEND_GUILD_CENTER_API_KEY`(上一步拿到)
|
||||
- `FABRIC_BACKEND_GUILD_NODE_ID`
|
||||
|
||||
Guild 启动前会强校验这三项,缺失即启动失败。
|
||||
|
||||
---
|
||||
|
||||
## 3) Frontend 登录
|
||||
|
||||
登录页填写:
|
||||
|
||||
- `Center API Base URL`
|
||||
- `Center API Key`
|
||||
- 用户名/密码
|
||||
|
||||
登录成功后,Center 返回:
|
||||
|
||||
- 用户可访问 guild 列表
|
||||
- 每个 guild 对应的访问 token
|
||||
|
||||
Frontend 使用这些 token 直连各 Guild 拉 channels/messages。
|
||||
|
||||
---
|
||||
|
||||
## 4) Center API 访问规则
|
||||
|
||||
- `POST /api/nodes/register`:无需 API Key(但仅 localhost)
|
||||
- 其他 Center API:全部需要 `x-api-key`
|
||||
|
||||
---
|
||||
|
||||
## 5) 常见错误
|
||||
|
||||
- `401 missing/invalid api key`:Center API Key 未传或错误
|
||||
- `403 register endpoint only allows localhost caller`:注册接口不是本机调用
|
||||
- Guild 启动失败 `Missing required env`:缺 `FABRIC_BACKEND_GUILD_CENTER_BASE_URL` / `FABRIC_BACKEND_GUILD_CENTER_API_KEY` / `FABRIC_BACKEND_GUILD_NODE_ID`
|
||||
43
docs/MVP-DoD.md
Normal file
43
docs/MVP-DoD.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Fabric MVP DoD(Definition of Done)
|
||||
|
||||
## 1. 范围
|
||||
本 DoD 面向当前 Fabric Web + Desktop MVP:
|
||||
|
||||
- Frontend(登录、工作台、聊天主链路、实时)
|
||||
- Desktop(Electron 壳、安全基线、托盘、打包)
|
||||
- API 接入(Guild/Center,API Key 模型)
|
||||
|
||||
## 2. 完成标准
|
||||
|
||||
### 2.1 Frontend
|
||||
- [x] 登录流程可用(Center)
|
||||
- [x] Guild/Channel 浏览可用
|
||||
- [x] 消息收发改删可用
|
||||
- [x] 实时事件可见(created/updated/deleted)
|
||||
- [x] typing/在线状态可见
|
||||
- [x] 异常态(loading/empty/error)可用
|
||||
|
||||
### 2.2 Desktop
|
||||
- [x] BrowserWindow 与菜单基础可用
|
||||
- [x] preload/IPC 白名单可用
|
||||
- [x] 导航/新窗口限制生效
|
||||
- [x] 本地配置存储可用
|
||||
- [x] 系统通知可用
|
||||
- [x] 托盘与最小化到托盘可用
|
||||
- [x] Linux 构建产物可生成(AppImage/deb/tar.gz)
|
||||
|
||||
### 2.3 接口与配置
|
||||
- [x] Guild API 使用 API Key
|
||||
- [x] Center API 使用 API Key
|
||||
- [x] Socket 鉴权携带 API Key
|
||||
- [x] 可通过 runtime config 统一配置 baseURL 与 API Key
|
||||
|
||||
## 3. 待验收项(测试相关)
|
||||
以下保留给联调/验收阶段:
|
||||
|
||||
- [ ] 与 Center/Guild 联调通过(登录、发消息、实时)
|
||||
- [ ] 关键链路冒烟(Web + Desktop)
|
||||
|
||||
## 4. 发布前阻断项
|
||||
- [ ] 将 Desktop `package.json` 中占位 maintainer 邮箱替换为正式邮箱
|
||||
- [ ] 补充应用 icon,避免使用默认 Electron icon
|
||||
227
docs/TEST_POINTS.md
Normal file
227
docs/TEST_POINTS.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Fabric — Test Points
|
||||
|
||||
Reference checklist of everything that should be verified across the stack.
|
||||
Organized by component. Re-run the **whole list** after any infra-level change
|
||||
(ESM migration, dependency bump, Docker/compose change) since those can
|
||||
regress any path.
|
||||
|
||||
Legend: **Verified** = exercised on the live local stack at least once.
|
||||
Local stack = `docker compose -f docker-compose.local.yml` (Center :7001,
|
||||
Guild1 :7002 = `test-guild1`, Guild2 :7003 = `test-guild2`, Frontend :8088).
|
||||
|
||||
---
|
||||
|
||||
## 1. Fabric.Backend.Center (auth / identity)
|
||||
|
||||
| # | Test point | How to verify | Expected |
|
||||
|---|---|---|---|
|
||||
| C1 | `user create` CLI | `node dist/cli.js user create --email <e> --password <p>` | `{ok:true,user}` |
|
||||
| C2 | `user apikey` CLI | `node dist/cli.js user apikey --email <e> [--label l]` | `{ok:true,apiKey:"fak_…"}` once |
|
||||
| C3 | `POST /auth/login` | valid creds | session: accessToken, refreshToken, user{id,email,name}, guilds, guildAccessTokens |
|
||||
| C4 | login email validation | email with 1-char TLD (`a@b.c`) | 400 (`@IsEmail`) — login & register both reject |
|
||||
| C5 | `POST /auth/refresh` / `logout` | with refreshToken | new tokens / `{status:ok}` |
|
||||
| C6 | `GET /auth/me` | bearer token | `{id,email,name}` |
|
||||
| C7 | `PATCH /auth/me {name}` | bearer; then re-login | name updated & persists; empty → 4xx; default name = email |
|
||||
| C8 | `POST /auth/agent/login {apiKey}` | valid key | same shape as login (guilds+tokens) |
|
||||
| C9 | agent/login bad key | `{apiKey:"nope"}` | 401 |
|
||||
| C10 | api-key-guard exemptions | login, agent/login, refresh, logout, GET/PATCH me, me/guilds, join, members | reachable without `x-api-key` |
|
||||
| C11 | `POST /auth/resolve-names {guildNodeId,names}` | api-key auth (guild node key) | name/email → userId, scoped to guild members; unknown omitted |
|
||||
| C12 | `POST /auth/me/guilds/join` | bearer | membership added |
|
||||
| C13 | `GET /auth/me/guilds` | bearer | guilds + fresh guildAccessTokens |
|
||||
| C14 | `GET /auth/guilds/:nodeId/members` | member bearer | members incl. `name` |
|
||||
| C15 | guild node register | CLI / `POST /api/nodes/register` (localhost) | node + apiKey |
|
||||
| C16 | `user.name` defaults to email | new user | login/members responses carry name=email until changed |
|
||||
|
||||
## 2. Fabric.Backend.Guild — channels
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| G1 | create channel: `xType` required | missing → 400 |
|
||||
| G2 | `xType` enum | not in {general,work,report,discuss,triage,custom} → 400 |
|
||||
| G3 | creator auto-added as channel member | always in `channel_members` |
|
||||
| G4 | `memberUserIds` added | listed users become members |
|
||||
| G5 | triage `onDuty` required | missing → 400 |
|
||||
| G6 | triage onDuty | auto-added member + 1 `wake_mapping` row |
|
||||
| G7 | custom `listeners` | one `wake_mapping` row per listener (optional/empty ok) |
|
||||
| G8 | `POST /channels/:id/close` | member (or public) → ok; non-member non-public → 403 |
|
||||
| G9 | post to closed channel | 409 `{error:"channel_closed"}` (normal + command) |
|
||||
| G10 | closed channel history | `GET …/messages` still 200 |
|
||||
| G11 | `listForUser` carries `closed`/`isPublic`/`isMember` | present & correct |
|
||||
| G12 | `POST /channels/:id/join` | public → member; non-public non-member → 403; idempotent |
|
||||
| G13 | `POST /channels/:id/leave` | removes `channel_members` + `wake_mapping` rows + turn order entry |
|
||||
| G14 | `GET /channels/:id/members` | explicit member userIds |
|
||||
| G15 | `listForUser` visibility | public OR explicit member only |
|
||||
| G16 | create discuss/work with `bypassUserIds` | order = members − bypass (∩ members), sorted; bypass stored; disjoint partition |
|
||||
| G17 | `POST /channels/:id/bypass {userId}` | any channel member actor; target must be a member; discuss/work only (else 400); moves target order→bypass |
|
||||
| G18 | `GET /channels/:id/members` carries `bypass` | each row `{userId, bypass:boolean}` from turn state |
|
||||
|
||||
## 3. Fabric.Backend.Guild — messaging / wakeup
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| W1 | one message-id; metadata at push only | `message.created` socket payload = view + `channelId` + per-recipient `wakeup` |
|
||||
| W2 | author rule (precedence) | author's own message → `wakeup=false` (overrides all) |
|
||||
| W3 | general (no mention) | all recipients true except author |
|
||||
| W4 | general + `<@id>` | only at'd (mentioned − author) true; others false |
|
||||
| W5 | report | all false |
|
||||
| W6 | triage / custom | only `wake_mapping` users true |
|
||||
| W7 | mention parse | `<@id>` counts only outside backtick spans |
|
||||
| W8 | name-mention translation | `<@user.name:NAME>` → `<@userId>` (Center resolve), outside backticks |
|
||||
| W9 | name-mention unresolved | left literal |
|
||||
| W10 | name-mention in backticks | left literal (not translated) |
|
||||
|
||||
## 4. Fabric.Backend.Guild — slash commands
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| S1 | command registry | only registered (`/no-reply`,`/force-proceed`) intercepted |
|
||||
| S2 | unknown `/x` (e.g. `/etc/passwd`) | delivered as a normal message |
|
||||
| S3 | command never delivered | no message row; response `{status:command}` |
|
||||
| S4 | `/no-reply` outside discuss/work | swallowed, no effect |
|
||||
|
||||
## 5. Fabric.Backend.Guild — discuss/work turn engine
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| T1 | init state | `currentSpeaker = NULL`, order = members sorted by id, frames `[]` |
|
||||
| T2 | single-member channel | `currentSpeaker` stays `NULL` forever |
|
||||
| T3 | activation (from NULL) | speaker X → moved to order[0], `currentSpeaker = order[1]` |
|
||||
| T4 | advance (current speaker normal) | `currentSpeaker → successor`; wakeup only successor |
|
||||
| T5 | queue-jump (non-current normal) | no advance; all wakeup false; resets cross-round no-reply streak |
|
||||
| T6 | queue-jump `/no-reply` | swallowed; does NOT reset streak |
|
||||
| T7 | current `/no-reply` | guild `/ack` emitted; advance; streak += sender |
|
||||
| T8 | all-members consecutive `/no-reply` (cross-round) | pause → `currentSpeaker=NULL`; resumes on next proactive normal msg |
|
||||
| T9 | end-of-round shuffle | trailing `/no-reply` run → tail; anchor = last "prev not /nr & self /nr"; head shuffled; head[0] ≠ last normal speaker (D) |
|
||||
| T10 | shuffle infeasible (head empty / only D) | pause instead of shuffle |
|
||||
| T11 | `/force-proceed` | skip current (not recorded, streak untouched), advance; at order[-1] triggers shuffle |
|
||||
| T12 | member join mid-rotation | appended to order tail |
|
||||
| T13 | member leave | removed from order/streak/frames; if current → successor takes over |
|
||||
| T14 | mention sub-frame push | current-speaker mention → push `{order:atList, idx:0}`; atList = mentions−sender ∩ members; effective current = atList[0] |
|
||||
| T15 | sub-frame single pass | each member acts once (real / `/no-reply` / `/force-proceed`) → pop |
|
||||
| T16 | sub-frame pop | parent restored at saved pointer (the pusher); root `current_speaker` column preserved during sub-frame |
|
||||
| T17 | nested sub-frames | mention inside a sub-frame pushes deeper; pops in order |
|
||||
| T18 | backtick mention | `` `<@id>` `` does NOT push a sub-frame |
|
||||
| T19 | `/ack` message | author=`guild`, content `/ack`, persisted (own message-id+seq), wakeup=true only for new currentSpeaker (else all false) |
|
||||
| T20 | bypass excluded from rotation | bypass member never becomes currentSpeaker / never woken by normal rotation |
|
||||
| T21 | mentioned bypass member | current-speaker mention of a bypass user → pushed into a sub-frame (transient), woken; on pop returns to bypass (not added to root order) |
|
||||
| T22 | `moveToBypass` mid-rotation | target removed from order/streak/frames, added to bypass; if target was currentSpeaker → successor takes over (null if order empties) |
|
||||
| T23 | mention nesting cap | max 4 sub-frames (5 levels incl. root); 5th push evicts bottom-most: root→A→B→C→D + E ⇒ root→B→C→D→E |
|
||||
| T24 | member-leave strips bypass | leaver removed from `bypassUserIds` too (no orphan) |
|
||||
|
||||
## 6. Fabric.Frontend
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| F1 | login screen | dark card; default Center base; no API key field |
|
||||
| F2 | channels list | only member/public; per-x_type color; xType badge |
|
||||
| F3 | create-channel modal | name; required Type select; Public checkbox default off; member list **excludes self**; creator auto-added note |
|
||||
| F4 | triage in modal | required On-duty single-select, default = current user |
|
||||
| F5 | custom in modal | optional Listeners multi-select |
|
||||
| F6 | members sidebar | non-public channel selected → split "In channel" / "Guild"; public/no-channel → single list; shows name + (you) |
|
||||
| F7 | join/leave buttons | topbar shows Join (non-member, public) / Leave (member) |
|
||||
| F8 | closed channel | composer replaced by read-only banner; history still rendered |
|
||||
| F9 | markdown render | per-message; HTML-escaped (XSS-safe); unclosed code fence contained to that message; no cross-message bleed |
|
||||
| F10 | `<@id>` mention chip | `@DisplayName` pill (resolved via members; short-id fallback); backtick-wrapped literal; untranslated `<@user.name:>` literal |
|
||||
| F11 | name edit (Settings) | PATCH /auth/me; reflected after save |
|
||||
| F12 | dev mode toggle | shows guild `/ack` + per-message `wakeup` metadata; hidden when off; persists in localStorage |
|
||||
| F13 | guild-token refresh on mount | no stale-token "Failed to load channels" after >15 min / reload |
|
||||
| F14 | message not split | long agent reply renders/stored as one message (no Discord-style chunking) |
|
||||
| F15 | Discord-style dark theme | server rail / channel sidebar / messages / members layout |
|
||||
| F16 | create-modal bypass select | discuss/work only: optional multi-select of guild members; sent as `bypassUserIds`; reset on open/close |
|
||||
| F17 | members panel bypass UI | discuss/work + in-channel list only: bypass members tagged `bypass`; others show "→ bypass" action calling `POST :id/bypass` |
|
||||
| F18 | composer file attach | 📎 button + multi file input; selected files shown as removable chips; sent after upload; image preview / download chip rendered (via `?access_token`) |
|
||||
| F19 | pinned canvas panel | fixed below topbar, independent of message scroll; md→renderMarkdown, text→`<pre>`, html→sandboxed `<iframe sandbox srcdoc>`; collapse/expand |
|
||||
| F20 | canvas share/edit | Share (no canvas) / Edit (sharer) modal: title, format, source; sharer-only Edit/Remove; live update via `canvas.updated`/`canvas.removed` sockets; channel & messages context menus |
|
||||
|
||||
## 7. Fabric.OpenclawPlugin (channel plugin)
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| P1 | build vs real openclaw SDK | `node install.mjs --build-only` clean |
|
||||
| P2 | install script | `--install` / `--uninstall` / `--openclaw-profile-path`; copies to `~/.openclaw/plugins/fabric` + `openclaw config` |
|
||||
| P3 | plugin loads & registers | `openclaw channels list` → `Fabric … installed, configured, enabled` |
|
||||
| P4 | independence | no openclaw source modified (plugin dir + config only) |
|
||||
| P5 | agent auth | `channels.fabric.accounts.<agentId>.fabricApiKey` → `agent/login` session |
|
||||
| P6 | inbound transport | one socket per agent; joins channel rooms; logs connect/join |
|
||||
| P7 | wakeup → admission | `wakeup:true` → dispatch (model runs, reply delivered). `wakeup:false` → `recordInboundSession` only: message enters the agent's OpenClaw session as history/context, **model NOT run, nothing sent back** (no `/no-reply` — turn engine expects silence from non-woken agents). Verified: log `recorded (no wakeup, history only)`, 0 dispatch/deliver/posted |
|
||||
| P8 | account → agent routing | requires `cfg.bindings` `{agentId,match:{channel:"fabric",accountId}}`; else falls back to default agent |
|
||||
| P9 | dispatch | `runtime.channel.turn` path: `resolveAgentRoute` + `finalizeInboundContext` + `dispatchInboundReplyWithBase` |
|
||||
| P10 | outbound | agent reply posted back to Fabric **as the agent**, exactly **one** message (no chunking; `disableBlockStreaming`) |
|
||||
| P11 | tools | `fabric-register`; `create-{chat,work,report,discussion}-channel` (→ x_type); `discussion-complete` (summary + close) |
|
||||
| P12 | gateway lifecycle | starts inbound on `gateway_start`, stops on `gateway_stop`; no separate sidecar |
|
||||
| P13 | full round-trip | human posts in Fabric → wakeup → agent runs → reply lands in channel as agent |
|
||||
| P14 | file delivery to agent | message attachments downloaded with the agent's guild token to a temp dir; **only local** `MediaPaths`/`MediaTypes` (+ singular) set on the finalized inbound context. No `MediaUrls` — the guild URL is a private host and openclaw's SSRF guard blocks re-fetching it (verified live: `fabric: fetched N attachment(s)`, SSRF WARN gone after the fix) |
|
||||
|
||||
## 8. Fabric.Backend.Guild — files & canvas
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| FC1 | `POST /files` (multipart) | returns `{fileId,url:/api/files/:id,name,mimeType,size,expiresAt}`; `expiresAt ≈ now + TTL` (default 7d) |
|
||||
| FC2 | size limit | `> FABRIC_BACKEND_GUILD_FILE_MAX_BYTES` (default 100MB, operator-configurable) → 400 |
|
||||
| FC3 | `GET /files/:id` auth | reachable with Bearer **or** `?access_token=` (browser `<img>/<a>`); no token → 401; image/pdf/av inline, else attachment |
|
||||
| FC4 | bytes round-trip | downloaded content byte-identical to upload |
|
||||
| FC5 | retention sweep | rows past `expiresAt` purged with their blob on boot + hourly (`FilesService.cleanup`) |
|
||||
| FC6 | message attachments | `attachments[]` persisted on the message and returned by `GET …/messages` |
|
||||
| FC7 | canvas single-active | one row per channel (unique `channel_id`); `GET` null when none |
|
||||
| FC8 | canvas share (PUT/POST) | caller becomes `sharerUserId`, `version=1`; re-share replaces (resets version, new sharer); emits `canvas.updated` |
|
||||
| FC9 | canvas update (PATCH) | original sharer only (else 403); `version` increments; emits `canvas.updated` |
|
||||
| FC10 | canvas delete | sharer only (else 403); emits `canvas.removed` |
|
||||
| FC11 | canvas access scope | non-member of a non-public channel → 403 on get/share |
|
||||
|
||||
## 9. Cross-cutting / infra
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| X1 | ESM everywhere | all subprojects build & run as ES modules (NodeNext, explicit `.js` imports, CJS deps default-imported) |
|
||||
| X2 | backends boot under ESM | no `ERR_MODULE` / `jwt.sign is not a function` / interop 500s |
|
||||
| X3 | local stack bring-up | 2 mysql + Center + 2 Guilds + Frontend healthy; guild nodes registered; users creatable |
|
||||
| X4 | DB_SYNC schema add | new entities/columns auto-create without data loss (additive) |
|
||||
| X5 | metadata/message separation | one message-id; metadata only at push; frontend/desktop metadata-agnostic; `author=guild` hidden unless debug |
|
||||
| X6 | submodule pointers | parent `Fabric` repo bumped after each submodule change; pushed to `origin/main` |
|
||||
|
||||
## 10. Slash commands (registry / sync / autocomplete)
|
||||
|
||||
| # | Test point | Expected |
|
||||
|---|---|---|
|
||||
| SC1 | Guild registry API | `PUT /api/commands {commands}` idempotent full replace; `GET /api/commands` → `{commands,updatedAt}`; both authed (no token → 401). Verified |
|
||||
| SC2 | plugin builds catalog | `buildFabricCommandSpecs(cfg)` via `openclaw/plugin-sdk/native-command-registry` (`listNativeCommandSpecsForConfig`+`findCommandByNativeName`); dynamic arg `choices` resolved to a static snapshot (`resolveCommandArgChoices`). Verified (41 specs, choices incl. dynamic) |
|
||||
| SC3 | plugin syncs on start | `syncFabricCommands` runs after inbound on `gateway_start`; PUTs catalog to each connected guild (one per guild, idempotent). Verified via direct call (`synced 41 -> test-guild1`); in-gateway run needs plugin reinstall + gateway restart |
|
||||
| SC4 | no `nativeCommands` capability | Fabric stays a TEXT-command surface; a `/<cmd>` message is delivered normally → plugin → OpenClaw command system. Only `/no-reply`,`/force-proceed` stay server-intercepted |
|
||||
| SC5 | frontend autocomplete | `/` opens a command panel (filter, ↑↓/Enter/Esc); pick inserts `/<nativeName> `; arg stage shows args + clickable `choices`. Built & deployed; **browser interaction not automated** — catalog fetch + bundle wiring verified |
|
||||
| SC6 | command execution | a registered `/<cmd>` reuses the existing message→plugin→OpenClaw path (text-command + command session + auth); not re-verified end-to-end here (same path as P13/P14) |
|
||||
|
||||
---
|
||||
|
||||
## Known coverage gaps / notes
|
||||
|
||||
- **Frontend (§6)** is build-verified (tsc) and was manually checked when
|
||||
built; not browser-exercised in automated runs. ESM migration did not touch
|
||||
the frontend.
|
||||
- **Plugin polish (not yet hardened):** per-agent 15-min token refresh for
|
||||
long-lived sockets (currently re-auth only on gateway restart); messages
|
||||
posted before an agent socket finishes joining are missed (no backfill);
|
||||
guild-token reconnect handling is minimal. Phase 2 (B2 firehose) not built.
|
||||
- **Files & canvas (§8)** backend is automated-e2e verified (upload,
|
||||
`?access_token` download, 401, attachment persistence, canvas
|
||||
share/update/replace/delete + sharer-only/access enforcement; retention
|
||||
deadline asserted, sweep logic unit-level only — not waited out).
|
||||
- **Plugin file delivery (P14) — agent file receipt PROVEN live.**
|
||||
After repointing the fabric binding to a real agent
|
||||
(`bindings[*].agentId` `echo`→`home-developer`, per `~/.openclaw/
|
||||
openclaw.json` — `echo` was never a defined agent), a human posted a
|
||||
file in Fabric → `wakeup` → plugin admitted → **downloaded the
|
||||
attachment with the agent's guild token** to a temp dir → set local
|
||||
`MediaPaths` → the openclaw agent then invoked its `read` tool on
|
||||
exactly that file (`/tmp/fabric-media-echo-<msgId>/0-<name>` in the
|
||||
log). i.e. the uploaded file demonstrably reaches and is opened by the
|
||||
agent. Bug found & fixed during this test: `MediaUrls` (a `localhost`
|
||||
URL) tripped openclaw's SSRF guard — now only local `MediaPaths`/
|
||||
`MediaTypes` are passed.
|
||||
- The agent→Fabric **final-reply leg (P13) still not observed**: the
|
||||
local kimi-backed agent ends its turn after the tool call without
|
||||
emitting final text (no agent reply on any channel all day), so the
|
||||
plugin's `deliver` is never called. Pre-existing local model/agent
|
||||
behavior, independent of the Fabric files/canvas/plugin code.
|
||||
- `discuss`/`work` differ only in x_type label; turn semantics identical —
|
||||
test one, both covered.
|
||||
- Desktop / Android submodules are out of scope (untouched).
|
||||
120
docs/TODO-backend-center-guild.md
Normal file
120
docs/TODO-backend-center-guild.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# TODO - Backend Center/Guild 开发任务分解
|
||||
|
||||
## 0. 基础工程(本周)
|
||||
- [x] Backend 拆分:Center / Guild
|
||||
- [x] NestJS skeleton 初始化(Center/Guild)
|
||||
- [x] MySQL + TypeORM 接入
|
||||
- [x] Dockerfile(Center/Guild)
|
||||
- [x] docker-compose(center/guild + mysql)
|
||||
- [x] 增加 `.env.example`(Center/Guild)
|
||||
- [x] 增加统一 lint/format 配置(eslint + prettier)
|
||||
- [x] 增加基础 CI(build + lint)
|
||||
|
||||
---
|
||||
|
||||
## 1. Fabric.Backend.Center(Identity Hub)
|
||||
|
||||
### 1.1 Auth
|
||||
- [x] 用户注册(email/password)
|
||||
- [x] 用户登录(JWT access + refresh)
|
||||
- [x] token 刷新
|
||||
- [x] 登出(refresh token 失效)
|
||||
- [x] 密码哈希(bcrypt/argon2)
|
||||
- [x] DTO + 参数校验 + 错误码规范
|
||||
|
||||
### 1.2 Guild Node 注册与握手
|
||||
- [x] `POST /nodes/register` shared-secret 校验
|
||||
- [x] node 唯一性校验(nodeId/endpoint)
|
||||
- [x] node 状态模型(active/offline/revoked)
|
||||
- [x] `GET /nodes` 列表 + 分页
|
||||
- [x] node 心跳接口(可选)
|
||||
|
||||
### 1.3 Center 运维能力
|
||||
- [x] 审计日志(auth/node 关键操作)
|
||||
- [x] 健康检查深化(DB ready)
|
||||
- [x] 配置校验(启动时必填项检查)
|
||||
|
||||
---
|
||||
|
||||
## 2. Fabric.Backend.Guild(Guild Node)
|
||||
|
||||
### 2.1 领域模型
|
||||
- [x] Guild/Channel/DM 实体补全
|
||||
- [x] Member/Role 基础模型(即使 MVP 权限全开,也先留结构)
|
||||
- [x] 索引设计(channel_id + seq, created_at 等)
|
||||
|
||||
### 2.2 消息主链路
|
||||
- [x] 发送消息(content/reply/mentions/attachments 元数据)
|
||||
- [x] 编辑消息(可编辑窗口策略先简化)
|
||||
- [x] 删除消息(软删 vs 硬删,先定策略)
|
||||
- [x] `GET messages` 分页(seq 区间 + limit)
|
||||
- [x] seq 分配改为 DB 原子方案(避免并发冲突)
|
||||
|
||||
### 2.3 一致性与回补
|
||||
- [x] 回补接口:`seq_from/seq_to`
|
||||
- [x] 断片检测辅助响应字段(next_expected_seq 等)
|
||||
- [x] 幂等键支持(写接口)
|
||||
|
||||
### 2.4 实时通信(MVP 后半)
|
||||
- [x] WebSocket 网关接入
|
||||
- [x] message.created/updated/deleted 事件广播
|
||||
- [x] 在线状态 + typing 事件
|
||||
|
||||
---
|
||||
|
||||
## 3. Center ↔ Guild 协议层
|
||||
- [x] 鉴权方案定稿(node token / HMAC)
|
||||
- [x] 注册握手协议文档化
|
||||
- [x] 错误码与重试策略统一
|
||||
- [x] 版本协商(`X-Fabric-Version`)
|
||||
|
||||
---
|
||||
|
||||
## 4. 插件与扩展面(为 OpenclawPlugin 准备)
|
||||
- [x] Webhook 事件信封落地(event_id/event_type/occurred_at/data)
|
||||
- [x] HMAC 签名与重放防护
|
||||
- [x] 出站重试队列(指数退避)
|
||||
- [x] API Key 入站调用鉴权(不区分 Bot/人类)
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试与质量门禁
|
||||
|
||||
### 5.1 自动化测试
|
||||
- [x] 单元测试(auth/service/message/seq)
|
||||
- [x] 集成测试(MySQL + API)
|
||||
- [x] 合约测试(Center-Guild 协议)
|
||||
|
||||
### 5.2 质量门禁
|
||||
- [x] lint/typecheck/build 全绿
|
||||
- [x] API 文档(OpenAPI/Swagger)
|
||||
- [ ] 关键链路压测(发送/拉取/回补)
|
||||
|
||||
---
|
||||
|
||||
## 6. 部署与运维
|
||||
- [x] `docker-compose.prod.yml`(去掉 `DB_SYNC=true`)
|
||||
- [x] DB migration 机制(TypeORM migration)
|
||||
- [x] 结构化日志 + request id
|
||||
- [x] 基础监控指标(QPS、延迟、错误率)
|
||||
- [x] 备份与恢复流程文档
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐执行顺序(建议)
|
||||
1. Center Auth 完整闭环
|
||||
2. Guild 消息链路 + DB 原子 seq
|
||||
3. Center-Guild 握手协议固定
|
||||
4. WebSocket 实时层
|
||||
5. 插件扩展面 + 回调重试
|
||||
6. 测试、压测、发布文档
|
||||
|
||||
---
|
||||
|
||||
## 8. Definition of Done(MVP)
|
||||
- [x] 用户可注册登录
|
||||
- [ ] Guild/Channel/DM 可创建并发消息
|
||||
- [x] 消息 seq 连续可回补
|
||||
- [x] WebSocket 可实时收发
|
||||
- [x] 插件可通过统一 API Key 写入消息并接收 webhook
|
||||
- [x] docker-compose 一键部署可用
|
||||
87
docs/TODO-frontend-desktop.md
Normal file
87
docs/TODO-frontend-desktop.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# TODO - Frontend / Desktop 开发计划
|
||||
|
||||
## 0. 基础约束
|
||||
- [x] 技术栈:Frontend = React + Vite + TS,Desktop = Electron
|
||||
- [x] Frontend/Desktop 子模块初始化
|
||||
- [x] 所有前端接口统一走 Guild/Center API(API Key 模型)
|
||||
|
||||
---
|
||||
|
||||
## 1. Frontend(Web)
|
||||
|
||||
### 1.1 应用骨架
|
||||
- [x] 路由骨架(登录页 / 工作台 / 聊天页)
|
||||
- [x] 全局布局(侧栏 + 主区 + 状态栏)
|
||||
- [x] API Client 封装(baseURL、API Key、错误处理)
|
||||
- [x] Socket 客户端封装(连接、重连、订阅/退订)
|
||||
|
||||
### 1.2 认证与会话
|
||||
- [x] 登录页(Center 登录)
|
||||
- [x] Access/Refresh token 管理
|
||||
- [x] 过期自动刷新与登出
|
||||
- [x] 基础路由守卫(未登录跳转)
|
||||
|
||||
### 1.3 Guild/Channel 浏览
|
||||
- [x] Guild 列表加载
|
||||
- [x] Channel 列表加载
|
||||
- [x] 频道切换与 URL 状态同步
|
||||
|
||||
### 1.4 消息主链路
|
||||
- [x] 消息拉取(分页/区间)
|
||||
- [x] 发送消息
|
||||
- [x] 编辑消息
|
||||
- [x] 删除消息
|
||||
- [x] 回补提示(next_expected_seq)
|
||||
|
||||
### 1.5 实时能力
|
||||
- [x] 实时消息(created/updated/deleted)
|
||||
- [x] typing/在线状态显示
|
||||
- [x] 断线重连与重拉策略
|
||||
|
||||
### 1.6 体验与稳定性
|
||||
- [x] 基础 loading/empty/error 态
|
||||
- [x] 关键页面可观测日志(requestId)
|
||||
- [x] 前端构建与 lint 门禁
|
||||
|
||||
---
|
||||
|
||||
## 2. Desktop(Electron)
|
||||
|
||||
### 2.1 桌面壳
|
||||
- [x] BrowserWindow 配置(尺寸、最小尺寸、标题)
|
||||
- [x] Dev/Prod 加载策略(devServer / 本地包)
|
||||
- [x] 应用菜单与快捷键基础
|
||||
|
||||
### 2.2 安全基线
|
||||
- [x] contextIsolation 保持开启
|
||||
- [x] preload + IPC 白名单(最小暴露)
|
||||
- [x] 禁止任意导航/新窗口策略
|
||||
|
||||
### 2.3 桌面能力
|
||||
- [x] 本地配置存储(API Base/API Key)
|
||||
- [x] 系统通知(新消息)
|
||||
- [x] 托盘与最小化到托盘(可选)
|
||||
|
||||
### 2.4 打包发布
|
||||
- [x] 打包配置(Linux/macOS/Windows)
|
||||
- [x] 版本号与产物命名规范
|
||||
- [x] 一键构建命令与发布说明
|
||||
|
||||
---
|
||||
|
||||
## 3. 联调与验收
|
||||
- [ ] 与 Center/Guild 联调通过(登录、发消息、实时)
|
||||
- [ ] 关键链路冒烟(Web + Desktop)
|
||||
- [x] MVP DoD 文档更新
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐执行顺序
|
||||
1. Frontend 1.1 应用骨架
|
||||
2. Frontend 1.2 认证与会话
|
||||
3. Frontend 1.3/1.4 核心聊天
|
||||
4. Frontend 1.5 实时完善
|
||||
5. Desktop 2.1/2.2 安全壳
|
||||
6. Desktop 2.3 桌面增强
|
||||
7. Desktop 2.4 打包发布
|
||||
8. 联调验收
|
||||
42
docs/backend-split-mvp.md
Normal file
42
docs/backend-split-mvp.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Backend Split MVP 开发清单
|
||||
|
||||
## Fabric.Backend.Center
|
||||
|
||||
### Phase 1
|
||||
- [ ] 初始化 NestJS 项目骨架
|
||||
- [ ] Auth 模块(register/login/refresh/logout)
|
||||
- [ ] Guild Node 注册接口(shared-secret handshake)
|
||||
- [ ] Node 列表与状态查询接口
|
||||
|
||||
### 建议 API
|
||||
- `POST /auth/register`
|
||||
- `POST /auth/login`
|
||||
- `POST /auth/refresh`
|
||||
- `POST /nodes/register`
|
||||
- `GET /nodes`
|
||||
|
||||
---
|
||||
|
||||
## Fabric.Backend.Guild
|
||||
|
||||
### Phase 1
|
||||
- [ ] 初始化 NestJS 项目骨架
|
||||
- [ ] Guild/Channel/DM 数据模型
|
||||
- [ ] 消息发送/编辑/删除接口
|
||||
- [ ] 每 Channel/DM 的 `seq` 分配器
|
||||
- [ ] 按 `seq` 区间回补接口
|
||||
|
||||
### 建议 API
|
||||
- `POST /guilds`
|
||||
- `POST /channels`
|
||||
- `POST /channels/:id/messages`
|
||||
- `PATCH /channels/:id/messages/:messageId`
|
||||
- `DELETE /channels/:id/messages/:messageId`
|
||||
- `GET /channels/:id/messages?seq_from=&seq_to=`
|
||||
|
||||
---
|
||||
|
||||
## 集成顺序建议
|
||||
1. 先完成 Center 登录 + Node 注册
|
||||
2. 再完成 Guild 消息主链路(create + list + seq)
|
||||
3. 最后接入 WebSocket 与插件事件
|
||||
83
docs/backup-and-restore-runbook.md
Normal file
83
docs/backup-and-restore-runbook.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Fabric 备份与恢复 Runbook(v1)
|
||||
|
||||
## 1. 范围
|
||||
- MySQL Center:`fabric_center`
|
||||
- MySQL Guild:`fabric_guild`
|
||||
- 可选:持久卷级别备份(`mysql_center_data` / `mysql_guild_data`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 备份策略(建议)
|
||||
- 频率:每天 1 次全量(低峰期)
|
||||
- 保留:最近 7~14 天
|
||||
- 方式:`mysqldump`(逻辑备份)+ 异地对象存储
|
||||
- 校验:每周至少一次恢复演练
|
||||
|
||||
---
|
||||
|
||||
## 3. 手动备份命令
|
||||
> 在 `Fabric/` 目录执行
|
||||
|
||||
### 3.1 备份 Center
|
||||
```bash
|
||||
docker exec fabric-mysql-center sh -lc \
|
||||
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --quick --routines --events fabric_center' \
|
||||
> backup-center-$(date +%F-%H%M%S).sql
|
||||
```
|
||||
|
||||
### 3.2 备份 Guild
|
||||
```bash
|
||||
docker exec fabric-mysql-guild sh -lc \
|
||||
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --quick --routines --events fabric_guild' \
|
||||
> backup-guild-$(date +%F-%H%M%S).sql
|
||||
```
|
||||
|
||||
### 3.3 压缩
|
||||
```bash
|
||||
gzip backup-center-*.sql
|
||||
gzip backup-guild-*.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 恢复流程
|
||||
|
||||
### 4.1 停写(建议)
|
||||
- 先停 `backend-center` / `backend-guild`,避免恢复时写入冲突。
|
||||
|
||||
### 4.2 恢复 Center
|
||||
```bash
|
||||
gunzip -c backup-center-YYYY-MM-DD-HHMMSS.sql.gz | \
|
||||
docker exec -i fabric-mysql-center sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" fabric_center'
|
||||
```
|
||||
|
||||
### 4.3 恢复 Guild
|
||||
```bash
|
||||
gunzip -c backup-guild-YYYY-MM-DD-HHMMSS.sql.gz | \
|
||||
docker exec -i fabric-mysql-guild sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" fabric_guild'
|
||||
```
|
||||
|
||||
### 4.4 恢复后检查
|
||||
- 启动后端服务
|
||||
- 调用:
|
||||
- `GET /api/healthz`
|
||||
- `GET /api/metrics`
|
||||
- 随机抽查:
|
||||
- 用户登录
|
||||
- 节点注册列表
|
||||
- 消息拉取/回补
|
||||
|
||||
---
|
||||
|
||||
## 5. 演练清单
|
||||
- [ ] 恢复耗时记录
|
||||
- [ ] 数据一致性抽样
|
||||
- [ ] 回滚预案验证
|
||||
- [ ] 文档更新(命令/版本/风险)
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与注意事项
|
||||
- 恢复前先确认目标库环境(避免误写生产)
|
||||
- 密钥与密码不要写入仓库,统一走环境变量
|
||||
- `DB_SYNC` 在生产保持 `false`,结构变更走 migration
|
||||
94
docs/center-guild-handshake-v1.md
Normal file
94
docs/center-guild-handshake-v1.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Center ↔ Guild 注册握手协议 v1(HMAC)
|
||||
|
||||
## 1. 目标
|
||||
- 让 Guild Node 在注册时证明自己持有共享密钥(`CENTER_SHARED_SECRET`)
|
||||
- 防止中间人篡改与重放攻击
|
||||
|
||||
---
|
||||
|
||||
## 2. 注册接口
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/api/nodes/register`
|
||||
- **Content-Type:** `application/json`
|
||||
|
||||
### Body
|
||||
```json
|
||||
{
|
||||
"nodeId": "guild-node-1",
|
||||
"name": "Guild Node 1",
|
||||
"endpoint": "http://guild-node-1:7002"
|
||||
}
|
||||
```
|
||||
|
||||
### Required Headers
|
||||
- `X-Fabric-Version`: 协议版本,当前固定为 `1`
|
||||
- `X-Fabric-Timestamp`: ISO8601 UTC 时间(如 `2026-05-12T11:00:00.000Z`)
|
||||
- `X-Fabric-Nonce`: 随机字符串(建议 UUID)
|
||||
- `X-Fabric-Signature`: HMAC-SHA256 十六进制串
|
||||
|
||||
---
|
||||
|
||||
## 3. Canonical String
|
||||
签名输入格式(换行拼接):
|
||||
|
||||
```text
|
||||
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{BODY_JSON}
|
||||
```
|
||||
|
||||
示例:
|
||||
```text
|
||||
POST
|
||||
/api/nodes/register
|
||||
2026-05-12T11:00:00.000Z
|
||||
f8b3a8dc-3aeb-44fc-a4a1-36f8b6c27739
|
||||
{"nodeId":"guild-node-1","name":"Guild Node 1","endpoint":"http://guild-node-1:7002"}
|
||||
```
|
||||
|
||||
签名算法:
|
||||
```text
|
||||
signature = HMAC_SHA256_HEX(CENTER_SHARED_SECRET, canonicalString)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Center 侧校验规则
|
||||
1. 必须包含三项头:`signature/timestamp/nonce`
|
||||
2. `timestamp` 与服务端时间偏差不超过 5 分钟
|
||||
3. 使用相同 canonical 规则重新计算签名
|
||||
4. `timingSafeEqual` 比较签名
|
||||
5. 签名通过后再做业务校验:
|
||||
- nodeId 唯一
|
||||
- endpoint 唯一
|
||||
|
||||
---
|
||||
|
||||
## 5. 响应语义
|
||||
### 成功
|
||||
- `200 OK`
|
||||
- body 含 `status: accepted` 与 node 信息
|
||||
|
||||
### 失败(建议)
|
||||
- `403`:签名头缺失/签名错误/时间窗非法
|
||||
- `409`:nodeId 或 endpoint 冲突
|
||||
- `400`:参数非法
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全建议
|
||||
- `CENTER_SHARED_SECRET` 长度至少 32 字节
|
||||
- 定期轮换 secret(可采用双 key 过渡)
|
||||
- 在网关层启用 HTTPS
|
||||
- 后续可增加 nonce 去重表,防止时间窗内重放
|
||||
|
||||
---
|
||||
|
||||
## 7. 版本协商(预留)
|
||||
当前版本:`v1`
|
||||
|
||||
当前实现要求请求头:
|
||||
- `X-Fabric-Version: 1`
|
||||
|
||||
若版本不匹配,Center 返回:
|
||||
- `400`
|
||||
- `error.code = FABRIC_VERSION_NOT_SUPPORTED`
|
||||
- `supportedVersion = "1"`
|
||||
75
docs/error-codes-and-retry-policy-v1.md
Normal file
75
docs/error-codes-and-retry-policy-v1.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Fabric 错误码与重试策略 v1
|
||||
|
||||
## 1) 统一错误响应结构
|
||||
所有非 2xx 响应建议返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "FABRIC_XXX",
|
||||
"message": "human readable",
|
||||
"retryable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可选字段:
|
||||
- `requestId`: 请求追踪 id
|
||||
- `details`: 参数错误详情
|
||||
|
||||
---
|
||||
|
||||
## 2) 业务错误码(v1)
|
||||
|
||||
### 通用
|
||||
- `FABRIC_BAD_REQUEST` → 400(参数不合法)
|
||||
- `FABRIC_UNAUTHORIZED` → 401(认证失败)
|
||||
- `FABRIC_FORBIDDEN` → 403(鉴权/签名失败)
|
||||
- `FABRIC_NOT_FOUND` → 404(资源不存在)
|
||||
- `FABRIC_CONFLICT` → 409(资源冲突/重复)
|
||||
- `FABRIC_RATE_LIMITED` → 429(限流)
|
||||
- `FABRIC_INTERNAL_ERROR` → 500(服务内部错误)
|
||||
- `FABRIC_UNAVAILABLE` → 503(依赖不可用)
|
||||
|
||||
### Center↔Guild 协议
|
||||
- `FABRIC_HMAC_MISSING_HEADERS` → 403
|
||||
- `FABRIC_HMAC_INVALID_SIGNATURE` → 403
|
||||
- `FABRIC_HMAC_TIMESTAMP_EXPIRED` → 403
|
||||
- `FABRIC_NODE_ID_CONFLICT` → 409
|
||||
- `FABRIC_NODE_ENDPOINT_CONFLICT` → 409
|
||||
|
||||
### Messaging
|
||||
- `FABRIC_EDIT_WINDOW_EXPIRED` → 409
|
||||
- `FABRIC_IDEMPOTENCY_REPLAY` → 200(命中幂等缓存,非错误)
|
||||
|
||||
---
|
||||
|
||||
## 3) 重试策略(客户端/插件侧)
|
||||
|
||||
### 可重试(指数退避)
|
||||
- HTTP: `429`, `500`, `502`, `503`, `504`
|
||||
- 网络异常:超时/连接重置/临时 DNS 故障
|
||||
|
||||
### 不可重试
|
||||
- HTTP: `400`, `401`, `403`, `404`, `409`(除非业务明确允许)
|
||||
|
||||
### 退避规则
|
||||
- 基础间隔:1s
|
||||
- 退避序列:1s / 2s / 4s / 8s / 16s
|
||||
- 最大重试次数:5
|
||||
- 加抖动:`±20%`
|
||||
- 若有 `Retry-After`:优先按 `Retry-After`
|
||||
|
||||
---
|
||||
|
||||
## 4) 幂等与重试配合
|
||||
- 写接口重试必须带 `Idempotency-Key`
|
||||
- 同 key 重放返回首次响应,避免重复写入
|
||||
- 幂等记录建议至少保留 24h
|
||||
|
||||
---
|
||||
|
||||
## 5) 服务端落地建议
|
||||
- 网关/中间件统一异常映射为标准 `error.code`
|
||||
- 在日志中记录:`error.code`、`requestId`、`status`
|
||||
- 对 429/503 返回 `Retry-After`
|
||||
47
scripts/register-guild-node.sh
Executable file
47
scripts/register-guild-node.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage:
|
||||
# scripts/register-guild-node.sh <node_id> <name> <endpoint>
|
||||
# Example:
|
||||
# scripts/register-guild-node.sh guild-node-1 "Guild Node 1" "http://backend-guild:7002"
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Usage: $0 <node_id> <name> <endpoint>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_ID="$1"
|
||||
NODE_NAME="$2"
|
||||
NODE_ENDPOINT="$3"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose ps backend-center >/dev/null 2>&1; then
|
||||
echo "backend-center service is not available in current docker compose project" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESULT_JSON=$(docker compose exec -T backend-center \
|
||||
npm run -s cli -- node register --node-id "$NODE_ID" --name "$NODE_NAME" --endpoint "$NODE_ENDPOINT")
|
||||
|
||||
OK=$(node -e "const o=JSON.parse(process.argv[1]);process.stdout.write(String(!!o.ok));" "$RESULT_JSON")
|
||||
|
||||
if [[ "$OK" != "true" ]]; then
|
||||
echo "registration failed: $RESULT_JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_KEY=$(node -e "const o=JSON.parse(process.argv[1]);process.stdout.write(o.apiKey||'');" "$RESULT_JSON")
|
||||
NODE_ID_OUT=$(node -e "const o=JSON.parse(process.argv[1]);process.stdout.write(o.node?.nodeId||'');" "$RESULT_JSON")
|
||||
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
echo "registration succeeded but no apiKey returned: $RESULT_JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node registered: $NODE_ID_OUT"
|
||||
echo "FABRIC_BACKEND_GUILD_CENTER_API_KEY=$API_KEY"
|
||||
Reference in New Issue
Block a user